From bf46f2687e9a4f0942641b9744d80e1b8ea11ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E7=BA=A202?= <596540@ky-tech.com.cn> Date: Fri, 18 Sep 2026 11:41:03 +0800 Subject: [PATCH 1/2] feat(cli): version command for runtime self-check - catalog entry first HELP row, entry intercept before root, package.json is the version source (v1.3.0) --- README.md | 1 + cli.mjs | 8 +- lib/args.mjs | 2 +- lib/commands.mjs | 10 +- lib/human.mjs | 2 + lib/i18n.mjs | 2 + lib/version.mjs | 5 + package.json | 2 +- .../20260918-feature-cli-version/brief.md | 98 +++++++++++++++++++ test/cli.test.mjs | 25 +++++ 10 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 lib/version.mjs create mode 100644 shadow-docs/changes/20260918-feature-cli-version/brief.md diff --git a/README.md b/README.md index 3743ca7..dc9f752 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Shadow dev workflow 的确定性脚手架 CLI。所有命令走 plan → execute | 命令 | 说明 | |------|------| +| `version` | CLI 版本(任意目录可用,不要求 git 仓库) | | `repo inspect` | 查看仓库状态(分支、HEAD、脏文件) | | `change create\|approve\|list` | 创建/批准变更 brief;`list` 默认只列活动变更,`--all` 合并归档、`--archived` 只列归档(条目带 `archived` 布尔) | | `issue plan\|execute` | 创建 GitHub issue(正文由 brief 确定性渲染,见「Issue 正文结构契约」) | diff --git a/cli.mjs b/cli.mjs index 375c3c7..ee88d19 100755 --- a/cli.mjs +++ b/cli.mjs @@ -8,6 +8,7 @@ import { confirm, name } from './lib/input.mjs' import { err } from './lib/errors.mjs' import { resolveLang } from './lib/i18n.mjs' import { HELP, COMMANDS } from './lib/commands.mjs' +import { VERSION } from './lib/version.mjs' import * as human from './lib/human.mjs' import * as branch from './lib/domains/branch.mjs' import * as sync from './lib/domains/sync.mjs' @@ -89,9 +90,14 @@ try { p = parsed.p; o = parsed.o L = resolveLang(o) const helpMode = !p.length || p.includes('--help') || p[0] === 'help' + // 元命令 version 与 help 同类:不要求 git 仓库,在 root() 之前拦截;--version 先于 help 判定 + const versionMode = p[0] === 'version' && p.length === 1 || ('version' in o && !p.length) const t0 = Date.now() let v - if (helpMode) { + if (versionMode) { + v = { ok: true, command: 'version', data: { version: VERSION } } + human.printVersion(L, VERSION) + } else if (helpMode) { v = helpEnvelope(p, o) human.printHelp(L, v) } else { diff --git a/lib/args.mjs b/lib/args.mjs index 318a977..34c8210 100644 --- a/lib/args.mjs +++ b/lib/args.mjs @@ -6,7 +6,7 @@ export function args(a) { const p = [], o = {} for (let i = 0; i < a.length; i++) { if (!a[i].startsWith('--')) p.push(a[i]) - else { const k = a[i].slice(2); if (['confirm', 'json', 'full', 'help', 'all', 'archived'].includes(k)) o[k] = true; else o[k] = a[++i] } + else { const k = a[i].slice(2); if (['confirm', 'json', 'full', 'help', 'all', 'archived', 'version'].includes(k)) o[k] = true; else o[k] = a[++i] } } return { p, o } } diff --git a/lib/commands.mjs b/lib/commands.mjs index e04c65f..51149b1 100644 --- a/lib/commands.mjs +++ b/lib/commands.mjs @@ -11,6 +11,7 @@ const FI = f('--files', false, '逗号分隔文件列表(反斜杠自动归一 const M = f('--message', false, '提交信息', 'commit message') export const COMMANDS = { + 'version': c('version', 'CLI 版本(任意目录可用,不要求 git 仓库)', 'CLI version (any directory, no git repository required)', [], 'shadow-dev version'), 'repo.inspect': c('repo inspect', '查看仓库状态(分支、HEAD、脏文件)', 'show repository state (branch, HEAD, dirty files)', [], 'shadow-dev repo inspect'), 'change.create': c('change create', '创建变更 brief', 'create a change brief', [N, f('--type', false, 'feature|fix|build|chore|docs|refactor|style|test,默认 feat', 'one of feature|fix|build|chore|docs|refactor|style|test, default feat'), f('--scope', false, '影响范围', 'scope'), f('--base-branch', false, '基线分支,默认 main', 'base branch, default main'), FI, f('--body-file', false, 'brief 正文来源文件', 'file supplying the brief body'), f('--repository', false, 'GitHub owner/repo', 'GitHub owner/repo'), CF], 'shadow-dev change create --name --type feature --confirm', 'change approve --name {name} --confirm'), 'change.approve': c('change approve', '批准 brief(draft → proposed)', 'approve the brief (draft → proposed)', [N, CF], 'shadow-dev change approve --name --confirm', 'branch plan --name {name}'), @@ -41,13 +42,16 @@ export const COMMANDS = { 'index.rebuild.execute': c('index rebuild execute', '重建 INDEX.md(无 brief 域,--plan-hash 为唯一凭证)', 'rebuild INDEX.md (briefless command: --plan-hash is the only credential)', [f('--plan-hash', true, 'index rebuild plan 的输出', 'hash from index rebuild plan'), CF], 'shadow-dev index rebuild execute --plan-hash --confirm', null), } -// HELP 由目录派生,保持既有的分组行格式(`branch plan|execute`),向后兼容既有断言 +// HELP 由目录派生,保持既有的分组行格式(`branch plan|execute`),向后兼容既有断言; +// 单段 key(如 version)是平铺元命令,整行即命令名 export const HELP = (() => { const rows = [], index = new Map() for (const key of Object.keys(COMMANDS)) { - const parts = key.split('.'), base = parts.slice(0, -1).join(' '), action = parts.at(-1) + const parts = key.split('.') + if (parts.length === 1) { rows.push([key, null]); continue } + const base = parts.slice(0, -1).join(' '), action = parts.at(-1) if (!index.has(base)) { index.set(base, rows.length); rows.push([base, []]) } rows[index.get(base)][1].push(action) } - return rows.map(([base, actions]) => `${base} ${actions.join('|')}`).join('\n') + return rows.map(([base, actions]) => (actions === null ? base : `${base} ${actions.join('|')}`)).join('\n') })() diff --git a/lib/human.mjs b/lib/human.mjs index 9ce4e91..b017195 100644 --- a/lib/human.mjs +++ b/lib/human.mjs @@ -27,6 +27,8 @@ export function error(L, e, p, o = {}) { w(ui(L, 'example', { example: spec.example })) } +export function printVersion(L, v) { w(ui(L, 'version', { v })) } + export function printHelp(L, v) { if (v.command === 'help') { w(ui(L, 'helpHead')) diff --git a/lib/i18n.mjs b/lib/i18n.mjs index bed4e58..97fd301 100644 --- a/lib/i18n.mjs +++ b/lib/i18n.mjs @@ -9,6 +9,7 @@ const UI = { next: '⤷ 下一步: {step}', error: '✗ {code}: {hint}', example: ' 示例: {example}', + version: 'ℹ shadow-dev-cli 版本 {v}', helpHead: 'shadow-dev 命令一览(单命令详情: shadow-dev help <命令>)', globals: ' 全局参数: --lang zh|en(或 SHADOW_DEV_LANG)· SHADOW_DEV_QUIET=1 关闭本提示层 · 管道默认输出 JSON 契约,交互终端用 --json 显式开启', }, @@ -19,6 +20,7 @@ const UI = { next: '⤷ next: {step}', error: '✗ {code}: {hint}', example: ' example: {example}', + version: 'ℹ shadow-dev-cli version {v}', helpHead: 'shadow-dev commands (detail: shadow-dev help )', globals: ' global: --lang zh|en (or SHADOW_DEV_LANG) · SHADOW_DEV_QUIET=1 silences this layer · stdout JSON is default in pipes, opt in with --json on a TTY', }, diff --git a/lib/version.mjs b/lib/version.mjs new file mode 100644 index 0000000..53be5ac --- /dev/null +++ b/lib/version.mjs @@ -0,0 +1,5 @@ +import { readFileSync } from 'node:fs' + +// 版本号唯一事实源 = 同源 package.json(与 issue-render 的 cliVersion 同一读法): +// 运行时读取,零编译期注入,release 布局(cli.mjs/lib/package.json 同目录物化)天然成立 +export const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version diff --git a/package.json b/package.json index b4f137c..07d8f90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shadow-dev-cli", - "version": "1.2.0", + "version": "1.3.0", "description": "Deterministic scaffolding CLI for the Shadow dev workflow: brief lifecycle, plan/execute with plan hashes, git and GitHub operations.", "type": "module", "engines": { diff --git a/shadow-docs/changes/20260918-feature-cli-version/brief.md b/shadow-docs/changes/20260918-feature-cli-version/brief.md new file mode 100644 index 0000000..c9ab433 --- /dev/null +++ b/shadow-docs/changes/20260918-feature-cli-version/brief.md @@ -0,0 +1,98 @@ +--- +{ + "schema": "shadow-dev/v1", + "name": "20260918-feature-cli-version", + "type": "feature", + "scope": "cli", + "status": "reviewed", + "baseBranch": "main", + "branch": "feature/20260918-feature-cli-version", + "files": [ + "README.md", + "cli.mjs", + "lib/args.mjs", + "lib/commands.mjs", + "lib/human.mjs", + "lib/i18n.mjs", + "lib/version.mjs", + "package.json", + "test/cli.test.mjs" + ], + "github": { + "repository": "stack-wuh/shadow-dev-cli", + "issue": 27, + "issueUrl": "https://github.com/stack-wuh/shadow-dev-cli/issues/27", + "pullRequest": null, + "pullRequestUrl": null + }, + "review": { + "conclusion": "passed", + "verifiedCommit": "c32f32ad373ae449b29d55d10e4bf655c6379bac", + "verifiedAt": "2026-09-18T03:40:27.336Z" + }, + "workflow": { + "operation": null, + "checkpoint": "issue:27", + "planHash": "e91178e0aff22b5c13aa226a7ef74bdf6e1e1797baef91d512d5c44bdb7c645c", + "updatedAt": null, + "lastError": null, + "issuePlan": { + "title": "[feature] CLI version 命令——运行时版本自检", + "titleRaw": null, + "supplement": "", + "body": "## 动机\n插件侧 bootstrap 以 package.json cliVersion + CURRENT 指针锁定安装版本,但「当前跑的是哪个 CLI」在 CLI 本身无自证手段:agent 排障、双仓开发(link 轨)都只能翻指针文件确认。version 与 help 同属不依赖仓库的元命令,应补齐。\n\n## 引用规范\n- cli-output-contract:新增输出必须二选一(stdout JSON 契约 / stderr 人用层);COMMANDS 目录是 HELP、help JSON、人用提示的单一事实源。\n- install-distribution:LINK → CURRENT 双轨指针解析——version 只做展示,不参与指针逻辑。\n\n## 决策\n- COMMANDS 新增平铺目录条目 `version`(usage `version`,任意目录可用;HELP 派生加单段 key 守卫,保持既有 `domain.action` 分组行格式不变)。\n- 入口在 helpMode 之前拦截 versionMode:`shadow-dev version` 与 `--version` 皆命中,不要求 git 仓库,输出 `{\"ok\":true,\"command\":\"version\",\"data\":{\"version\":...}}`。\n- 版本号运行时读同源 package.json(复用 issue-render 的 `readFileSync(new URL('../package.json', import.meta.url))` 形态),零漂移,无编译期注入。\n- 人用层向 stderr 输出版本行(语言随 --lang,stdout JSON 与语言无关)。`--version` 在 args 白名单登记为无值布尔。\n- 版本 bump 1.2.0→1.3.0 随本变更提交;合入后 tag v1.3.0、pack.mjs 产物上 GitHub Release。\n\n## 任务\n- [ ] lib/version.mjs + COMMANDS 登记与 HELP 派生守卫 — lib/version.mjs,lib/commands.mjs — 目录单源\n- [ ] 入口 versionMode 拦截与参数/输出层接入 — cli.mjs,lib/args.mjs,lib/i18n.mjs,lib/human.mjs — 任意目录可用\n- [ ] 契约测试:JSON 形态、flag 别名、仓库内外、HELP 行与语言不变性 — test/cli.test.mjs — 钉死契约\n- [ ] README 命令表与 package.json 版本 1.3.0 — README.md,package.json — 文档随代码\n\n完整 brief:shadow-docs/changes/20260918-feature-cli-version/brief.md\n\n\n", + "labels": [ + "feature" + ] + }, + "release": { + "files": [ + "README.md", + "cli.mjs", + "lib/args.mjs", + "lib/commands.mjs", + "lib/human.mjs", + "lib/i18n.mjs", + "lib/version.mjs", + "package.json", + "shadow-docs/changes/20260918-feature-cli-version/brief.md", + "test/cli.test.mjs" + ], + "message": "feat(cli): version command for runtime self-check - catalog entry first HELP row, entry intercept before root, package.json is the version source (v1.3.0)", + "title": "feat(cli): version command (v1.3.0)", + "body": "" + } + }, + "knowledge": { + "action": "无需变更", + "target": null, + "reason": "版本事实由 README/help/COMMANDS 目录承载,不构成独立执行约束" + } +} +--- + +# CLI version 命令——运行时版本自检 + +## 动机 + +插件侧 bootstrap 以 package.json cliVersion + CURRENT 指针锁定安装版本,但「当前跑的是哪个 CLI」在 CLI 本身无自证手段:agent 排障、双仓开发(link 轨)都只能翻指针文件确认。version 与 help 同属不依赖仓库的元命令,应补齐。 + +## 引用规范 + +- cli-output-contract:新增输出必须二选一(stdout JSON 契约 / stderr 人用层);COMMANDS 目录是 HELP、help JSON、人用提示的单一事实源。 +- install-distribution:LINK → CURRENT 双轨指针解析——version 只做展示,不参与指针逻辑。 + +## 决策 + +- COMMANDS 新增平铺目录条目 `version`(usage `version`,任意目录可用;HELP 派生加单段 key 守卫,保持既有 `domain.action` 分组行格式不变)。 +- 入口在 helpMode 之前拦截 versionMode:`shadow-dev version` 与 `--version` 皆命中,不要求 git 仓库,输出 `{"ok":true,"command":"version","data":{"version":...}}`。 +- 版本号运行时读同源 package.json(复用 issue-render 的 `readFileSync(new URL('../package.json', import.meta.url))` 形态),零漂移,无编译期注入。 +- 人用层向 stderr 输出版本行(语言随 --lang,stdout JSON 与语言无关)。`--version` 在 args 白名单登记为无值布尔。 +- 版本 bump 1.2.0→1.3.0 随本变更提交;合入后 tag v1.3.0、pack.mjs 产物上 GitHub Release。 + +## 任务 + +- [x] lib/version.mjs + COMMANDS 登记与 HELP 派生守卫 — lib/version.mjs,lib/commands.mjs — 目录单源 +- [x] 入口 versionMode 拦截与参数/输出层接入 — cli.mjs,lib/args.mjs,lib/i18n.mjs,lib/human.mjs — 任意目录可用 +- [x] 契约测试:JSON 形态、flag 别名、仓库内外、HELP 行与语言不变性 — test/cli.test.mjs — 钉死契约 +- [x] README 命令表与 package.json 版本 1.3.0 — README.md,package.json — 文档随代码 diff --git a/test/cli.test.mjs b/test/cli.test.mjs index 0067fdf..4daa225 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -111,6 +111,31 @@ test('help lists deterministic workflow commands', () => { assert.match(result.stdout, /archive plan\|execute/) }) +test('version returns the package version without a git repository', () => { + const expected = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version + const outside = run(['version', '--json'], tmpdir()) + assert.equal(outside.status, 0, outside.stderr) + const o = JSON.parse(outside.stdout) + assert.equal(o.ok, true) + assert.equal(o.command, 'version') + assert.equal(o.data.version, expected) + // 仓库内同样可用 + const root = fixture() + assert.equal(JSON.parse(run(['version', '--json'], root).stdout).data.version, expected) + // --version 别名与 help 目录行 + assert.equal(JSON.parse(run(['--version', '--json'], tmpdir()).stdout).data.version, expected) + assert.match(JSON.parse(run(['--help']).stdout).data.help, /^version\n/) + assert.match(JSON.parse(run(['help', 'version', '--json']).stdout).data.commands.version.usage, /^version$/) +}) + +test('version stdout is language-invariant while human line localizes', () => { + const zh = run(['version', '--json', '--lang', 'zh'], tmpdir()) + const en = run(['version', '--json', '--lang', 'en'], tmpdir()) + assert.equal(JSON.stringify(JSON.parse(zh.stdout)), JSON.stringify(JSON.parse(en.stdout))) + assert.match(zh.stderr, /版本/) + assert.match(en.stderr, /version/) +}) + test('unknown commands return a stable JSON error', () => { const result = run(['unknown', '--json']) assert.equal(result.status, 1) From a80e5499840b16cbc2e0a8b597920a56b3aabc30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E7=BA=A202?= <596540@ky-tech.com.cn> Date: Fri, 18 Sep 2026 11:41:31 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore(shadow-docs):=2020260918-feature-cli-?= =?UTF-8?q?version=20release=20=E7=BB=93=E6=9E=9C=E5=9B=9E=E5=A1=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shadow-docs/changes/20260918-feature-cli-version/brief.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shadow-docs/changes/20260918-feature-cli-version/brief.md b/shadow-docs/changes/20260918-feature-cli-version/brief.md index c9ab433..bb01bc0 100644 --- a/shadow-docs/changes/20260918-feature-cli-version/brief.md +++ b/shadow-docs/changes/20260918-feature-cli-version/brief.md @@ -4,7 +4,7 @@ "name": "20260918-feature-cli-version", "type": "feature", "scope": "cli", - "status": "reviewed", + "status": "published", "baseBranch": "main", "branch": "feature/20260918-feature-cli-version", "files": [ @@ -22,8 +22,8 @@ "repository": "stack-wuh/shadow-dev-cli", "issue": 27, "issueUrl": "https://github.com/stack-wuh/shadow-dev-cli/issues/27", - "pullRequest": null, - "pullRequestUrl": null + "pullRequest": 28, + "pullRequestUrl": "https://github.com/stack-wuh/shadow-dev-cli/pull/28" }, "review": { "conclusion": "passed", @@ -32,7 +32,7 @@ }, "workflow": { "operation": null, - "checkpoint": "issue:27", + "checkpoint": "pr:28", "planHash": "e91178e0aff22b5c13aa226a7ef74bdf6e1e1797baef91d512d5c44bdb7c645c", "updatedAt": null, "lastError": null,