Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Shadow dev workflow 的确定性脚手架 CLI。所有命令走 plan → execute
| 命令 | 说明 |
|------|------|
| `repo inspect` | 查看仓库状态(分支、HEAD、脏文件) |
| `change create\|approve` | 创建/批准变更 brief |
| `change create\|approve\|list` | 创建/批准变更 brief;列出活动变更 |
| `issue plan\|execute` | 创建 GitHub issue |
| `branch plan\|execute` | 建功能分支 |
| `sync plan\|execute` | fast-forward 同步上游 |
Expand All @@ -33,6 +33,7 @@ CLI 在 stderr 渲染一层人类提示:进场横幅(命令+参数)、收
- 语言解析:`--lang zh|en` > `SHADOW_DEV_LANG` > 系统 locale 自动探测 > 默认 `zh`。非法取值报 `INVALID_LANG`(退出码 2)。
- 关闭提示:`SHADOW_DEV_QUIET=1`(或 `true`)时 stderr 零输出,适合日志管道。
- 语言只影响 stderr 文案;错误 code、JSON 结构、`nextStep` 模板均不本地化。
- 缺必填参数报错时,stderr 逐行列出该命令在命令目录中的完整参数描述(`flag * 说明`,含示例值与来源位置),示例行的占位符与目录一致(如 `--name <change-name>`)——提示与人用 help 共享同一事实源 `lib/commands.mjs`。
- `shadow-dev help` 概览默认只回最小面:`data.help`(命令一览字符串,约 350 字节);agent 需要结构化明细(usage/参数/必填/示例/nextStep)时用 `shadow-dev help --full`。`shadow-dev help <命令>` 查看单组详情,恒定结构化(组面小)。stderr 中文命令表不受 `--full` 影响。

## 平台兼容
Expand Down
3 changes: 2 additions & 1 deletion cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ async function handle(r, p, o) {
if (d === 'task' && a === 'set') return { ok: true, command: 'task.set', data: task.set(r, o) }
if (d === 'change' && a === 'create') return { ok: true, command: 'change.create', data: change.create(r, o) }
if (d === 'change' && a === 'approve') return { ok: true, command: 'change.approve', data: change.approve(r, o) }
if (d === 'change' && a === 'list') return { ok: true, command: 'change.list', data: change.list(r) }
if (Object.hasOwn(DOMAINS, d)) {
const verb = a === 'rebuild' ? s : a, c = a === 'rebuild' ? `${d}.rebuild` : d
if (verb === 'plan') return await planDomain(c, DOMAINS[d], r, o)
Expand Down Expand Up @@ -98,7 +99,7 @@ try {
}
if (jsonEnabled(o)) out(v)
} catch (e) {
human.error(L, e, p)
human.error(L, e, p, o)
if (jsonEnabled(o)) fail(e.code || e.message, e.message, e.status || 1)
else process.exitCode = e.status || 1
}
51 changes: 26 additions & 25 deletions lib/commands.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// summary/args.desc 提供 zh/en 双版(机器流不本地化,人用层按语言取用);next 为稳定英文命令模板,{x} 由执行结果填充。
const f = (flag, required, zh, en) => ({ flag, required, desc: { zh, en } })
const c = (usage, zh, en, args, example, next = null) => ({ usage, summary: { zh, en }, args, example, next })
const N = f('--name', true, '变更名,如 20260917-feature-x', 'change name, e.g. 20260917-feature-x')
const N = f('--name', true, '变更名(shadow-docs/changes/ 下的子目录,如 20260917-feature-x)', 'change name (subdirectory under shadow-docs/changes/, e.g. 20260917-feature-x)')
const CF = f('--confirm', true, '写操作显式确认', 'explicit confirmation for mutating commands')
const PH = f('--plan-hash', false, '缺省时读取 brief 中持久化的 planHash', 'defaults to the planHash persisted in the brief')
const T = f('--title', false, '标题,缺省为变更名', 'title, defaults to change name')
Expand All @@ -12,30 +12,31 @@ const M = f('--message', false, '提交信息', 'commit message')

export const COMMANDS = {
'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 <n> --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 <n> --confirm', 'branch plan --name {name}'),
'issue.plan': c('issue plan', '预览创建 GitHub issue', 'plan a GitHub issue', [N, T, B, f('--labels', false, '逗号分隔标签', 'comma-separated labels')], 'shadow-dev issue plan --name <n> --title "标题" --labels feature', 'issue execute --name {name} --title "{title}" --body "{body}" --labels {labels} --confirm'),
'issue.execute': c('issue execute', '创建 GitHub issue', 'create the GitHub issue', [N, T, B, f('--labels', false, '逗号分隔标签', 'comma-separated labels'), PH, CF], 'shadow-dev issue execute --name <n> --confirm', null),
'branch.plan': c('branch plan', '预览建功能分支', 'plan creating the feature branch', [N], 'shadow-dev branch plan --name <n>', 'branch execute --name {name} --confirm'),
'branch.execute': c('branch execute', '从基线分支创建并切换', 'create and switch to the feature branch', [N, PH, CF], 'shadow-dev branch execute --name <n> --confirm', null),
'sync.plan': c('sync plan', '预览 fast-forward 同步上游', 'plan a fast-forward sync with upstream', [N], 'shadow-dev sync plan --name <n>', 'sync execute --name {name} --confirm'),
'sync.execute': c('sync execute', 'fetch 后仅 fast-forward 合并上游', 'fetch then ff-only merge upstream', [N, PH, CF], 'shadow-dev sync execute --name <n> --confirm', null),
'conflict.inspect': c('conflict inspect', '检查与其他 active brief 的文件重叠', 'check file overlaps with other active briefs', [N], 'shadow-dev conflict inspect --name <n>', null),
'task.list': c('task list', '列出 brief 任务清单', 'list the brief task checklist', [N], 'shadow-dev task list --name <n>', null),
'task.set': c('task set', '勾选/取消任务', 'tick or untick a task', [N, f('--task', true, '任务 id,如 task-3', 'task id, e.g. task-3'), f('--state', true, 'todo|done', 'todo|done'), CF], 'shadow-dev task set --name <n> --task task-1 --state done --confirm', null),
'review.plan': c('review plan', '预览审查记录', 'plan the review record', [N], 'shadow-dev review plan --name <n>', 'review execute --name {name} --conclusion passed --confirm'),
'review.execute': c('review execute', '写入审查结论与知识评估(任务未全部勾选会被拒绝)', 'persist review conclusion and knowledge action (blocked until all tasks are done)', [N, f('--conclusion', false, 'passed|blocked,默认 passed', 'passed|blocked, default passed'), f('--knowledge', false, '新增|更新|废弃|无需变更', 'knowledge action: 新增|更新|废弃|无需变更'), f('--target', false, '知识卡片路径', 'knowledge card path'), f('--reason', false, '知识动作理由', 'knowledge action reason'), PH, CF], 'shadow-dev review execute --name <n> --conclusion passed --confirm', null),
'commit.plan': c('commit plan', '预览按显式文件列表提交', 'plan an explicit-file-list commit', [N, f('--files', true, '逗号分隔文件列表', 'comma-separated file list'), f('--message', true, '提交信息', 'commit message')], 'shadow-dev commit plan --name <n> --files a.mjs,b.mjs --message "fix: x"', 'commit execute --name {name} --files {files} --message "{message}" --confirm'),
'commit.execute': c('commit execute', '提交并写 checkpoint(参数须与 plan 完全一致)', 'commit and write checkpoint (args must match the plan exactly)', [N, FI, M, PH, CF], 'shadow-dev commit execute --name <n> --files a.mjs --message "fix: x" --confirm', null),
'publish.plan': c('publish plan', '预览推送分支并创建/复用 PR', 'plan pushing the branch and creating/reusing the PR', [N, T, B], 'shadow-dev publish plan --name <n> --title "标题"', 'publish execute --name {name} --title "{title}" --body "{body}" --confirm'),
'publish.execute': c('publish execute', '推送分支并创建/复用 PR(参数须与 plan 完全一致)', 'push and create/reuse the PR (args must match the plan exactly)', [N, T, B, PH, CF], 'shadow-dev publish execute --name <n> --confirm', 'archive plan --name {name}'),
'release.plan': c('release plan', '预览提交+推送+PR 复合发布', 'plan the commit+push+PR composite release', [N, FI, M, T, B], 'shadow-dev release plan --name <n> --files a.mjs --message "feat: x" --title "标题"', 'release execute --name {name} --confirm'),
'release.execute': c('release execute', '执行复合发布(缺省参数回退 brief workflow.release)', 'run the composite release (params default to the stored workflow.release plan)', [N, FI, M, T, B, PH, CF], 'shadow-dev release execute --name <n> --confirm', 'archive plan --name {name}'),
'pr.inspect': c('pr inspect', '查看 brief 关联 PR', 'inspect the PR linked to the brief', [N], 'shadow-dev pr inspect --name <n>', null),
'reconcile.plan': c('reconcile plan', '预览 brief 状态与实际进度对齐', 'plan reconciling brief state with actual progress', [N], 'shadow-dev reconcile plan --name <n>', 'reconcile execute --name {name} --confirm'),
'reconcile.execute': c('reconcile execute', '回写对齐后的状态', 'persist the reconciled state', [N, PH, CF], 'shadow-dev reconcile execute --name <n> --confirm', null),
'archive.plan': c('archive plan', '预览归档(要求 review passed 且 PR merged)', 'plan archiving (requires review passed and PR merged)', [N], 'shadow-dev archive plan --name <n>', 'archive execute --name {name} --confirm'),
'archive.execute': c('archive execute', '移入 archive 并重建 INDEX', 'move into archive and rebuild INDEX', [N, PH, CF], 'shadow-dev archive execute --name <n> --confirm', null),
'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'),
'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}'),
'change.list': c('change list', '列出活动变更的名称、类型、状态与分支', 'list active changes with name, type, status and branch', [], 'shadow-dev change list'),
'issue.plan': c('issue plan', '预览创建 GitHub issue', 'plan a GitHub issue', [N, T, B, f('--labels', false, '逗号分隔标签', 'comma-separated labels')], 'shadow-dev issue plan --name <change-name> --title "标题" --labels feature', 'issue execute --name {name} --title "{title}" --body "{body}" --labels {labels} --confirm'),
'issue.execute': c('issue execute', '创建 GitHub issue', 'create the GitHub issue', [N, T, B, f('--labels', false, '逗号分隔标签', 'comma-separated labels'), PH, CF], 'shadow-dev issue execute --name <change-name> --confirm', null),
'branch.plan': c('branch plan', '预览建功能分支', 'plan creating the feature branch', [N], 'shadow-dev branch plan --name <change-name>', 'branch execute --name {name} --confirm'),
'branch.execute': c('branch execute', '从基线分支创建并切换', 'create and switch to the feature branch', [N, PH, CF], 'shadow-dev branch execute --name <change-name> --confirm', null),
'sync.plan': c('sync plan', '预览 fast-forward 同步上游', 'plan a fast-forward sync with upstream', [N], 'shadow-dev sync plan --name <change-name>', 'sync execute --name {name} --confirm'),
'sync.execute': c('sync execute', 'fetch 后仅 fast-forward 合并上游', 'fetch then ff-only merge upstream', [N, PH, CF], 'shadow-dev sync execute --name <change-name> --confirm', null),
'conflict.inspect': c('conflict inspect', '检查与其他 active brief 的文件重叠', 'check file overlaps with other active briefs', [N], 'shadow-dev conflict inspect --name <change-name>', null),
'task.list': c('task list', '列出 brief 任务清单', 'list the brief task checklist', [N], 'shadow-dev task list --name <change-name>', null),
'task.set': c('task set', '勾选/取消任务', 'tick or untick a task', [N, f('--task', true, '任务 id,如 task-3', 'task id, e.g. task-3'), f('--state', true, 'todo|done', 'todo|done'), CF], 'shadow-dev task set --name <change-name> --task task-1 --state done --confirm', null),
'review.plan': c('review plan', '预览审查记录', 'plan the review record', [N], 'shadow-dev review plan --name <change-name>', 'review execute --name {name} --conclusion passed --confirm'),
'review.execute': c('review execute', '写入审查结论与知识评估(任务未全部勾选会被拒绝)', 'persist review conclusion and knowledge action (blocked until all tasks are done)', [N, f('--conclusion', false, 'passed|blocked,默认 passed', 'passed|blocked, default passed'), f('--knowledge', false, '新增|更新|废弃|无需变更', 'knowledge action: 新增|更新|废弃|无需变更'), f('--target', false, '知识卡片路径', 'knowledge card path'), f('--reason', false, '知识动作理由', 'knowledge action reason'), PH, CF], 'shadow-dev review execute --name <change-name> --conclusion passed --confirm', null),
'commit.plan': c('commit plan', '预览按显式文件列表提交', 'plan an explicit-file-list commit', [N, f('--files', true, '逗号分隔文件列表', 'comma-separated file list'), f('--message', true, '提交信息', 'commit message')], 'shadow-dev commit plan --name <change-name> --files a.mjs,b.mjs --message "fix: x"', 'commit execute --name {name} --files {files} --message "{message}" --confirm'),
'commit.execute': c('commit execute', '提交并写 checkpoint(参数须与 plan 完全一致)', 'commit and write checkpoint (args must match the plan exactly)', [N, FI, M, PH, CF], 'shadow-dev commit execute --name <change-name> --files a.mjs --message "fix: x" --confirm', null),
'publish.plan': c('publish plan', '预览推送分支并创建/复用 PR', 'plan pushing the branch and creating/reusing the PR', [N, T, B], 'shadow-dev publish plan --name <change-name> --title "标题"', 'publish execute --name {name} --title "{title}" --body "{body}" --confirm'),
'publish.execute': c('publish execute', '推送分支并创建/复用 PR(参数须与 plan 完全一致)', 'push and create/reuse the PR (args must match the plan exactly)', [N, T, B, PH, CF], 'shadow-dev publish execute --name <change-name> --confirm', 'archive plan --name {name}'),
'release.plan': c('release plan', '预览提交+推送+PR 复合发布', 'plan the commit+push+PR composite release', [N, FI, M, T, B], 'shadow-dev release plan --name <change-name> --files a.mjs --message "feat: x" --title "标题"', 'release execute --name {name} --confirm'),
'release.execute': c('release execute', '执行复合发布(缺省参数回退 brief workflow.release)', 'run the composite release (params default to the stored workflow.release plan)', [N, FI, M, T, B, PH, CF], 'shadow-dev release execute --name <change-name> --confirm', 'archive plan --name {name}'),
'pr.inspect': c('pr inspect', '查看 brief 关联 PR', 'inspect the PR linked to the brief', [N], 'shadow-dev pr inspect --name <change-name>', null),
'reconcile.plan': c('reconcile plan', '预览 brief 状态与实际进度对齐', 'plan reconciling brief state with actual progress', [N], 'shadow-dev reconcile plan --name <change-name>', 'reconcile execute --name {name} --confirm'),
'reconcile.execute': c('reconcile execute', '回写对齐后的状态', 'persist the reconciled state', [N, PH, CF], 'shadow-dev reconcile execute --name <change-name> --confirm', null),
'archive.plan': c('archive plan', '预览归档(要求 review passed 且 PR merged)', 'plan archiving (requires review passed and PR merged)', [N], 'shadow-dev archive plan --name <change-name>', 'archive execute --name {name} --confirm'),
'archive.execute': c('archive execute', '移入 archive 并重建 INDEX', 'move into archive and rebuild INDEX', [N, PH, CF], 'shadow-dev archive execute --name <change-name> --confirm', null),
'index.rebuild.plan': c('index rebuild plan', '预览变更索引重建', 'plan rebuilding the change index', [], 'shadow-dev index rebuild plan', 'index rebuild execute --plan-hash {planHash} --confirm'),
'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),
}
Expand Down
17 changes: 16 additions & 1 deletion lib/domains/change.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { existsSync } from 'node:fs'
import { existsSync, readdirSync } from 'node:fs'
import { join } from 'node:path'
import { brief, write, ap } from '../brief.mjs'
import { name, confirm, readBody, fileList } from '../input.mjs'
import { err } from '../errors.mjs'
Expand Down Expand Up @@ -28,6 +29,20 @@ export function create(r, o) {
return { name: n, path: b.path }
}

// 活动变更发现入口:变更名即 shadow-docs/changes/ 下子目录名;archive 与解析失败的目录跳过(与 indexer 同规则)
export function list(r) {
const base = join(r, 'shadow-docs', 'changes'), changes = []
if (existsSync(base)) for (const e of readdirSync(base, { withFileTypes: true })) {
if (!e.isDirectory() || e.name === 'archive') continue
try {
const d = brief(r, e.name).data
changes.push({ name: d.name ?? e.name, type: d.type ?? null, status: d.status ?? null, branch: d.branch ?? null })
} catch {}
}
changes.sort((a, b) => a.name.localeCompare(b.name))
return { changes }
}

export function approve(r, o) {
confirm(o)
const b = brief(r, name(o))
Expand Down
12 changes: 9 additions & 3 deletions lib/human.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@ export function done(L, v, ms) {
if (v.data?.nextStep) w(ui(L, 'next', { step: v.data.nextStep }))
}

export function error(L, e, p) {
// 参数行单一格式源:help 详情与缺参错误共用,内容恒从命令目录派生
const argLine = (L, a) => ` ${a.flag}${a.required ? ' *' : ' '} ${a.desc[L] ?? a.desc.zh}`

export function error(L, e, p, o = {}) {
const code = e.code || e.message
w(ui(L, 'error', { code, hint: hint(L, code) }))
const spec = lookup(p)
if (spec) w(ui(L, 'example', { example: spec.example }))
if (!spec) return
// 必填参数缺失时逐行列出目录中该参数的完整描述(flag → option key 即去掉 -- 前缀)
for (const a of spec.args) if (a.required && o[a.flag.slice(2)] === undefined) w(argLine(L, a))
w(ui(L, 'example', { example: spec.example }))
}

export function printHelp(L, v) {
Expand All @@ -34,7 +40,7 @@ export function printHelp(L, v) {
}
for (const e of Object.values(v.data.commands)) {
w(` ${e.usage} — ${e.summary[L] ?? e.summary.zh}`)
for (const a of e.args) w(` ${a.flag}${a.required ? ' *' : ' '} ${a.desc[L] ?? a.desc.zh}`)
for (const a of e.args) w(argLine(L, a))
w(ui(L, 'example', { example: e.example }))
}
}
Expand Down
Loading
Loading