Skip to content

Commit f175eee

Browse files
authored
Merge pull request #28 from stack-wuh/feature/20260918-feature-cli-version
feat(cli): version command (v1.3.0)
2 parents c32f32a + a80e549 commit f175eee

10 files changed

Lines changed: 149 additions & 6 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Shadow dev workflow 的确定性脚手架 CLI。所有命令走 plan → execute
88

99
| 命令 | 说明 |
1010
|------|------|
11+
| `version` | CLI 版本(任意目录可用,不要求 git 仓库) |
1112
| `repo inspect` | 查看仓库状态(分支、HEAD、脏文件) |
1213
| `change create\|approve\|list` | 创建/批准变更 brief;`list` 默认只列活动变更,`--all` 合并归档、`--archived` 只列归档(条目带 `archived` 布尔) |
1314
| `issue plan\|execute` | 创建 GitHub issue(正文由 brief 确定性渲染,见「Issue 正文结构契约」) |

‎cli.mjs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { confirm, name } from './lib/input.mjs'
88
import { err } from './lib/errors.mjs'
99
import { resolveLang } from './lib/i18n.mjs'
1010
import { HELP, COMMANDS } from './lib/commands.mjs'
11+
import { VERSION } from './lib/version.mjs'
1112
import * as human from './lib/human.mjs'
1213
import * as branch from './lib/domains/branch.mjs'
1314
import * as sync from './lib/domains/sync.mjs'
@@ -89,9 +90,14 @@ try {
8990
p = parsed.p; o = parsed.o
9091
L = resolveLang(o)
9192
const helpMode = !p.length || p.includes('--help') || p[0] === 'help'
93+
// 元命令 version 与 help 同类:不要求 git 仓库,在 root() 之前拦截;--version 先于 help 判定
94+
const versionMode = p[0] === 'version' && p.length === 1 || ('version' in o && !p.length)
9295
const t0 = Date.now()
9396
let v
94-
if (helpMode) {
97+
if (versionMode) {
98+
v = { ok: true, command: 'version', data: { version: VERSION } }
99+
human.printVersion(L, VERSION)
100+
} else if (helpMode) {
95101
v = helpEnvelope(p, o)
96102
human.printHelp(L, v)
97103
} else {

‎lib/args.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function args(a) {
66
const p = [], o = {}
77
for (let i = 0; i < a.length; i++) {
88
if (!a[i].startsWith('--')) p.push(a[i])
9-
else { const k = a[i].slice(2); if (['confirm', 'json', 'full', 'help', 'all', 'archived'].includes(k)) o[k] = true; else o[k] = a[++i] }
9+
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] }
1010
}
1111
return { p, o }
1212
}

‎lib/commands.mjs‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const FI = f('--files', false, '逗号分隔文件列表(反斜杠自动归一
1111
const M = f('--message', false, '提交信息', 'commit message')
1212

1313
export const COMMANDS = {
14+
'version': c('version', 'CLI 版本(任意目录可用,不要求 git 仓库)', 'CLI version (any directory, no git repository required)', [], 'shadow-dev version'),
1415
'repo.inspect': c('repo inspect', '查看仓库状态(分支、HEAD、脏文件)', 'show repository state (branch, HEAD, dirty files)', [], 'shadow-dev repo inspect'),
1516
'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 <change-name> --type feature --confirm', 'change approve --name {name} --confirm'),
1617
'change.approve': c('change approve', '批准 brief(draft → proposed)', 'approve the brief (draft → proposed)', [N, CF], 'shadow-dev change approve --name <change-name> --confirm', 'branch plan --name {name}'),
@@ -41,13 +42,16 @@ export const COMMANDS = {
4142
'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 <hash> --confirm', null),
4243
}
4344

44-
// HELP 由目录派生,保持既有的分组行格式(`branch plan|execute`),向后兼容既有断言
45+
// HELP 由目录派生,保持既有的分组行格式(`branch plan|execute`),向后兼容既有断言;
46+
// 单段 key(如 version)是平铺元命令,整行即命令名
4547
export const HELP = (() => {
4648
const rows = [], index = new Map()
4749
for (const key of Object.keys(COMMANDS)) {
48-
const parts = key.split('.'), base = parts.slice(0, -1).join(' '), action = parts.at(-1)
50+
const parts = key.split('.')
51+
if (parts.length === 1) { rows.push([key, null]); continue }
52+
const base = parts.slice(0, -1).join(' '), action = parts.at(-1)
4953
if (!index.has(base)) { index.set(base, rows.length); rows.push([base, []]) }
5054
rows[index.get(base)][1].push(action)
5155
}
52-
return rows.map(([base, actions]) => `${base} ${actions.join('|')}`).join('\n')
56+
return rows.map(([base, actions]) => (actions === null ? base : `${base} ${actions.join('|')}`)).join('\n')
5357
})()

‎lib/human.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export function error(L, e, p, o = {}) {
2727
w(ui(L, 'example', { example: spec.example }))
2828
}
2929

30+
export function printVersion(L, v) { w(ui(L, 'version', { v })) }
31+
3032
export function printHelp(L, v) {
3133
if (v.command === 'help') {
3234
w(ui(L, 'helpHead'))

‎lib/i18n.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const UI = {
99
next: '⤷ 下一步: {step}',
1010
error: '✗ {code}: {hint}',
1111
example: ' 示例: {example}',
12+
version: 'ℹ shadow-dev-cli 版本 {v}',
1213
helpHead: 'shadow-dev 命令一览(单命令详情: shadow-dev help <命令>)',
1314
globals: ' 全局参数: --lang zh|en(或 SHADOW_DEV_LANG)· SHADOW_DEV_QUIET=1 关闭本提示层 · 管道默认输出 JSON 契约,交互终端用 --json 显式开启',
1415
},
@@ -19,6 +20,7 @@ const UI = {
1920
next: '⤷ next: {step}',
2021
error: '✗ {code}: {hint}',
2122
example: ' example: {example}',
23+
version: 'ℹ shadow-dev-cli version {v}',
2224
helpHead: 'shadow-dev commands (detail: shadow-dev help <command>)',
2325
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',
2426
},

‎lib/version.mjs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { readFileSync } from 'node:fs'
2+
3+
// 版本号唯一事实源 = 同源 package.json(与 issue-render 的 cliVersion 同一读法):
4+
// 运行时读取,零编译期注入,release 布局(cli.mjs/lib/package.json 同目录物化)天然成立
5+
export const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "shadow-dev-cli",
3-
"version": "1.2.0",
3+
"version": "1.3.0",
44
"description": "Deterministic scaffolding CLI for the Shadow dev workflow: brief lifecycle, plan/execute with plan hashes, git and GitHub operations.",
55
"type": "module",
66
"engines": {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
{
3+
"schema": "shadow-dev/v1",
4+
"name": "20260918-feature-cli-version",
5+
"type": "feature",
6+
"scope": "cli",
7+
"status": "published",
8+
"baseBranch": "main",
9+
"branch": "feature/20260918-feature-cli-version",
10+
"files": [
11+
"README.md",
12+
"cli.mjs",
13+
"lib/args.mjs",
14+
"lib/commands.mjs",
15+
"lib/human.mjs",
16+
"lib/i18n.mjs",
17+
"lib/version.mjs",
18+
"package.json",
19+
"test/cli.test.mjs"
20+
],
21+
"github": {
22+
"repository": "stack-wuh/shadow-dev-cli",
23+
"issue": 27,
24+
"issueUrl": "https://github.com/stack-wuh/shadow-dev-cli/issues/27",
25+
"pullRequest": 28,
26+
"pullRequestUrl": "https://github.com/stack-wuh/shadow-dev-cli/pull/28"
27+
},
28+
"review": {
29+
"conclusion": "passed",
30+
"verifiedCommit": "c32f32ad373ae449b29d55d10e4bf655c6379bac",
31+
"verifiedAt": "2026-09-18T03:40:27.336Z"
32+
},
33+
"workflow": {
34+
"operation": null,
35+
"checkpoint": "pr:28",
36+
"planHash": "e91178e0aff22b5c13aa226a7ef74bdf6e1e1797baef91d512d5c44bdb7c645c",
37+
"updatedAt": null,
38+
"lastError": null,
39+
"issuePlan": {
40+
"title": "[feature] CLI version 命令——运行时版本自检",
41+
"titleRaw": null,
42+
"supplement": "",
43+
"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<!-- shadow-dev:issue-metadata {\"name\":\"20260918-feature-cli-version\",\"type\":\"feature\",\"scope\":\"cli\",\"status\":\"proposed\",\"branch\":null,\"baseBranch\":\"main\",\"briefPath\":\"shadow-docs/changes/20260918-feature-cli-version/brief.md\",\"cliVersion\":\"1.2.0\",\"prUrl\":null,\"issueNumber\":null} -->\n",
44+
"labels": [
45+
"feature"
46+
]
47+
},
48+
"release": {
49+
"files": [
50+
"README.md",
51+
"cli.mjs",
52+
"lib/args.mjs",
53+
"lib/commands.mjs",
54+
"lib/human.mjs",
55+
"lib/i18n.mjs",
56+
"lib/version.mjs",
57+
"package.json",
58+
"shadow-docs/changes/20260918-feature-cli-version/brief.md",
59+
"test/cli.test.mjs"
60+
],
61+
"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)",
62+
"title": "feat(cli): version command (v1.3.0)",
63+
"body": ""
64+
}
65+
},
66+
"knowledge": {
67+
"action": "无需变更",
68+
"target": null,
69+
"reason": "版本事实由 README/help/COMMANDS 目录承载,不构成独立执行约束"
70+
}
71+
}
72+
---
73+
74+
# CLI version 命令——运行时版本自检
75+
76+
## 动机
77+
78+
插件侧 bootstrap 以 package.json cliVersion + CURRENT 指针锁定安装版本,但「当前跑的是哪个 CLI」在 CLI 本身无自证手段:agent 排障、双仓开发(link 轨)都只能翻指针文件确认。version 与 help 同属不依赖仓库的元命令,应补齐。
79+
80+
## 引用规范
81+
82+
- cli-output-contract:新增输出必须二选一(stdout JSON 契约 / stderr 人用层);COMMANDS 目录是 HELP、help JSON、人用提示的单一事实源。
83+
- install-distribution:LINK → CURRENT 双轨指针解析——version 只做展示,不参与指针逻辑。
84+
85+
## 决策
86+
87+
- COMMANDS 新增平铺目录条目 `version`(usage `version`,任意目录可用;HELP 派生加单段 key 守卫,保持既有 `domain.action` 分组行格式不变)。
88+
- 入口在 helpMode 之前拦截 versionMode:`shadow-dev version` 与 `--version` 皆命中,不要求 git 仓库,输出 `{"ok":true,"command":"version","data":{"version":...}}`。
89+
- 版本号运行时读同源 package.json(复用 issue-render 的 `readFileSync(new URL('../package.json', import.meta.url))` 形态),零漂移,无编译期注入。
90+
- 人用层向 stderr 输出版本行(语言随 --lang,stdout JSON 与语言无关)。`--version` 在 args 白名单登记为无值布尔。
91+
- 版本 bump 1.2.0→1.3.0 随本变更提交;合入后 tag v1.3.0、pack.mjs 产物上 GitHub Release。
92+
93+
## 任务
94+
95+
- [x] lib/version.mjs + COMMANDS 登记与 HELP 派生守卫 — lib/version.mjs,lib/commands.mjs — 目录单源
96+
- [x] 入口 versionMode 拦截与参数/输出层接入 — cli.mjs,lib/args.mjs,lib/i18n.mjs,lib/human.mjs — 任意目录可用
97+
- [x] 契约测试:JSON 形态、flag 别名、仓库内外、HELP 行与语言不变性 — test/cli.test.mjs — 钉死契约
98+
- [x] README 命令表与 package.json 版本 1.3.0 — README.md,package.json — 文档随代码

‎test/cli.test.mjs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,31 @@ test('help lists deterministic workflow commands', () => {
111111
assert.match(result.stdout, /archive plan\|execute/)
112112
})
113113

114+
test('version returns the package version without a git repository', () => {
115+
const expected = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
116+
const outside = run(['version', '--json'], tmpdir())
117+
assert.equal(outside.status, 0, outside.stderr)
118+
const o = JSON.parse(outside.stdout)
119+
assert.equal(o.ok, true)
120+
assert.equal(o.command, 'version')
121+
assert.equal(o.data.version, expected)
122+
// 仓库内同样可用
123+
const root = fixture()
124+
assert.equal(JSON.parse(run(['version', '--json'], root).stdout).data.version, expected)
125+
// --version 别名与 help 目录行
126+
assert.equal(JSON.parse(run(['--version', '--json'], tmpdir()).stdout).data.version, expected)
127+
assert.match(JSON.parse(run(['--help']).stdout).data.help, /^version\n/)
128+
assert.match(JSON.parse(run(['help', 'version', '--json']).stdout).data.commands.version.usage, /^version$/)
129+
})
130+
131+
test('version stdout is language-invariant while human line localizes', () => {
132+
const zh = run(['version', '--json', '--lang', 'zh'], tmpdir())
133+
const en = run(['version', '--json', '--lang', 'en'], tmpdir())
134+
assert.equal(JSON.stringify(JSON.parse(zh.stdout)), JSON.stringify(JSON.parse(en.stdout)))
135+
assert.match(zh.stderr, /版本/)
136+
assert.match(en.stderr, /version/)
137+
})
138+
114139
test('unknown commands return a stable JSON error', () => {
115140
const result = run(['unknown', '--json'])
116141
assert.equal(result.status, 1)

0 commit comments

Comments
 (0)