@betterdanlins/ai-memory 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -3
- package/README.zh-CN.md +71 -3
- package/bin/cli.js +105 -3
- package/package.json +1 -1
- package/src/framework.js +57 -17
- package/src/managed-blocks.js +62 -0
- package/src/manifest.js +7 -1
- package/src/model-routing.js +108 -0
- package/src/workflow.js +185 -0
- package/templates/claude/.claude/agents/economy-test-worker.md +8 -0
- package/templates/claude/.claude/agents/premium-implementer.md +8 -0
- package/templates/claude/.claude/agents/premium-planner.md +8 -0
- package/templates/claude/.claude/agents/premium-reviewer.md +8 -0
- package/templates/claude/.claude/agents/standard-implementer.md +8 -0
- package/templates/claude/.claude/agents/standard-test-worker.md +8 -0
- package/templates/claude/.claude/skills/model-routing/SKILL.md +6 -0
- package/templates/claude/CLAUDE.md +6 -0
- package/templates/codex/.agents/skills/model-routing/SKILL.md +6 -0
- package/templates/codex/.codex/agents/economy_test_worker.toml +6 -0
- package/templates/codex/.codex/agents/premium_implementer.toml +6 -0
- package/templates/codex/.codex/agents/premium_planner.toml +6 -0
- package/templates/codex/.codex/agents/premium_reviewer.toml +6 -0
- package/templates/codex/.codex/agents/standard_implementer.toml +6 -0
- package/templates/codex/.codex/agents/standard_test_worker.toml +6 -0
- package/templates/codex/AGENTS.md +6 -0
- package/templates/common/.ai/README.md +4 -1
- package/templates/common/.ai/config/model-routing.json +5 -0
- package/templates/common/.ai/runs/.gitignore.template +2 -0
- package/templates/common/.ai/skills/critic.md +1 -0
- package/templates/common/.ai/skills/delivery-readiness.md +2 -0
- package/templates/common/.ai/skills/feature-design.md +2 -0
- package/templates/common/.ai/skills/model-routing.md +39 -0
- package/templates/common/.ai/skills/requirements-flow.md +2 -0
package/README.md
CHANGED
|
@@ -21,6 +21,9 @@ npx @betterdanlins/ai-memory init
|
|
|
21
21
|
# Non-interactive
|
|
22
22
|
npx @betterdanlins/ai-memory init --name demo --stack "PHP + Vue" --tools claude,codex --yes
|
|
23
23
|
|
|
24
|
+
# Opt in to staged model routing
|
|
25
|
+
npx @betterdanlins/ai-memory init --model-profile balanced --yes
|
|
26
|
+
|
|
24
27
|
# Import user-level memory (profile/feedback) from an existing project
|
|
25
28
|
npx @betterdanlins/ai-memory init --import /path/to/other-project
|
|
26
29
|
|
|
@@ -29,6 +32,9 @@ npx @betterdanlins/ai-memory update --dry-run
|
|
|
29
32
|
|
|
30
33
|
# Apply only conflict-free safe updates; abort entirely on user changes
|
|
31
34
|
npx @betterdanlins/ai-memory update --yes
|
|
35
|
+
|
|
36
|
+
# Switch an initialized project from inherited models to staged routing
|
|
37
|
+
npx @betterdanlins/ai-memory models configure --profile balanced
|
|
32
38
|
```
|
|
33
39
|
|
|
34
40
|
Nothing is ever overwritten silently: existing files are asked about one by one (interactive) or skipped (`--yes`).
|
|
@@ -36,12 +42,62 @@ An invalid, non-directory, or unreadable `--import` project path fails explicitl
|
|
|
36
42
|
Before writing, the CLI checks for duplicate template destinations, path traversal, and symbolic links, then reads and renders every planned file. If an actual filesystem write fails midway, the error summary lists both written and not-yet-written files.
|
|
37
43
|
An initialized project cannot be initialized again. A newer CLI first uses `update --dry-run` to identify versions and user modifications. `update --yes` applies only additions and baseline-matching safe updates, aborting before any write when merge or review items exist.
|
|
38
44
|
|
|
45
|
+
## Versions and compatibility
|
|
46
|
+
|
|
47
|
+
### v0.5.0 — managed entry blocks
|
|
48
|
+
|
|
49
|
+
- Wraps the framework-owned sections of generated `AGENTS.md` and `CLAUDE.md` in explicit `ai-memory:managed` markers. Future updates replace only that block and preserve project commands and custom rules outside it.
|
|
50
|
+
- An unchanged v0.4.0 entry file can migrate to marked blocks automatically because its generated baseline hash proves it has not been edited. A modified unmarked v0.4.0 file requires one manual merge; after markers are established, later framework updates no longer conflict with user-section changes.
|
|
51
|
+
- Missing, duplicated, or malformed markers fail conservatively instead of triggering a whole-file overwrite. Managed-block updates also re-check the current file hash immediately before writing.
|
|
52
|
+
- `.claude/settings.json` remains a mixed JSON file and still requires review when both the framework and user changed it; Markdown markers are intentionally not applied to JSON.
|
|
53
|
+
- Fixes npm's automatic `.gitignore` template renaming and safely replaces the unchanged v0.4.0 `.ai/runs/.npmignore`; a locally edited obsolete file still requires review.
|
|
54
|
+
|
|
55
|
+
### v0.4.0 — staged model routing and verified handoffs
|
|
56
|
+
|
|
57
|
+
- Adds opt-in `inherit`, `balanced`, and `quality` profiles. New and upgraded projects default to `inherit`, so installing v0.4.0 does not silently increase model cost or change v0.3.0 workflow depth.
|
|
58
|
+
- Adds native Claude Code and Codex agents for premium planning/review, standard or premium implementation, and economy or standard test authoring. Model routing selects an executor only after the existing S/M/L workflow decides which stages are required.
|
|
59
|
+
- Adds `models show/configure` plus `workflow prepare/verify/complete`. Cross-model handoffs reference formal requirements, designs, and plans by path and SHA-256; stale inputs, unresolved decisions, route changes, unsafe paths, and incomplete acceptance receipts fail closed.
|
|
60
|
+
- Keeps `.ai/config/model-routing.json` user-owned and `.ai/runs/` local-only. Exact account entitlements are not auto-discovered: Claude uses native model aliases, while Codex agents use native reasoning-effort settings.
|
|
61
|
+
|
|
62
|
+
### v0.3.0 — safe framework upgrades and engineering workflow
|
|
63
|
+
|
|
64
|
+
- Introduced `.ai/ai-memory.json` framework/schema metadata, file ownership, generated baseline hashes, legacy v0.1 detection, and explicit migration planning.
|
|
65
|
+
- Added `update --dry-run` and conflict-free `update --yes`. User memory, requirements, architecture, and design assets are preserved; changed framework or mixed files require review instead of being overwritten.
|
|
66
|
+
- Added duplicate-destination checks, path traversal and symlink/junction protection, three-phase scaffolding, and diagnostic partial-write summaries.
|
|
67
|
+
- Added the language-agnostic `project-inception`, `feature-design`, and `delivery-readiness` workflows covering data modeling, technical contracts, deployment, performance, observability, scalability, reliability, security, and cost.
|
|
68
|
+
|
|
69
|
+
### Upgrade from v0.3.0 to v0.4.0
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Preview only; this must not modify the project
|
|
73
|
+
npx @betterdanlins/ai-memory@0.4.0 update --dry-run
|
|
74
|
+
|
|
75
|
+
# Apply only when the preview has no merge/review blockers
|
|
76
|
+
npx @betterdanlins/ai-memory@0.4.0 update --yes
|
|
77
|
+
|
|
78
|
+
# Optional: explicitly enable staged routing after the safe update
|
|
79
|
+
npx @betterdanlins/ai-memory@0.4.0 models configure --profile balanced
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
After the update, the profile remains `inherit` until explicitly changed. Do not re-run `init` on an existing v0.3.0 project. If dry-run reports merge or review items, merge them with the generated v0.4.0 files as references, then run dry-run again.
|
|
83
|
+
|
|
84
|
+
### Upgrade from v0.4.0 to v0.5.0
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx @betterdanlins/ai-memory@0.5.0 update --dry-run
|
|
88
|
+
npx @betterdanlins/ai-memory@0.5.0 update --yes
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If `AGENTS.md` or `CLAUDE.md` is still the unchanged v0.4.0 generated version, it migrates automatically. If either unmarked file was customized, dry-run reports `merge`; compare it with a fresh v0.5.0 reference, keep project-specific content in the user block, and retry. Once markers exist, edit only outside `<!-- ai-memory:managed:start/end -->`.
|
|
92
|
+
|
|
39
93
|
## What it generates
|
|
40
94
|
|
|
41
95
|
```
|
|
42
96
|
.ai/ # single source of truth, shared by all agents
|
|
43
97
|
├── README.md # session entry protocol
|
|
44
98
|
├── ai-memory.json # version, schema, tools, ownership, baseline hashes
|
|
99
|
+
├── config/
|
|
100
|
+
│ └── model-routing.json # inherit/balanced/quality stage routing; user-owned
|
|
45
101
|
├── memory/
|
|
46
102
|
│ ├── MEMORY.md # index — agents read this first
|
|
47
103
|
│ ├── project-state.md # stack, current version, requirement progress
|
|
@@ -49,8 +105,9 @@ An initialized project cannot be initialized again. A newer CLI first uses `upda
|
|
|
49
105
|
│ ├── user-profile.md # background, preferences, communication style
|
|
50
106
|
│ ├── feedback.md # behavioral norms distilled from feedback
|
|
51
107
|
│ └── features/ # per-feature dossiers
|
|
108
|
+
├── runs/ # local handoffs and stage receipts; ignored by Git
|
|
52
109
|
└── skills/ # methodology: requirements-flow, architecture,
|
|
53
|
-
#
|
|
110
|
+
# feature-design, model-routing, delivery-readiness, etc.
|
|
54
111
|
|
|
55
112
|
docs/architecture/ # engineering baseline for 0-to-1 projects
|
|
56
113
|
├── system-context.md # boundaries, actors, modules, and key flows
|
|
@@ -66,7 +123,7 @@ docs/design/vX.Y.Z/ # M/L-risk feature how, technical contracts, delta
|
|
|
66
123
|
|
|
67
124
|
# Thin adapters, generated only for the tools you enable:
|
|
68
125
|
CLAUDE.md + .claude/ # Claude Code: commands, skills, agents, settings
|
|
69
|
-
AGENTS.md + .agents/
|
|
126
|
+
AGENTS.md + .agents/ + .codex/# Codex: skills and model-specific custom agents
|
|
70
127
|
```
|
|
71
128
|
|
|
72
129
|
## CLI commands
|
|
@@ -76,6 +133,11 @@ AGENTS.md + .agents/ # Codex: skills
|
|
|
76
133
|
| `init` | Initialize a new project; refuses to run over an existing ai-memory installation |
|
|
77
134
|
| `update --dry-run` | Read-only analysis of the upgrade plan from an existing project to this CLI version |
|
|
78
135
|
| `update --yes` | Apply conflict-free safe updates; refuse user changes, mixed-file conflicts, or missing migration paths |
|
|
136
|
+
| `models show` | Show the selected profile and resolved tier for every workflow stage |
|
|
137
|
+
| `models configure --profile <name>` | Select `inherit`, `balanced`, or `quality` without changing requirements, designs, or code |
|
|
138
|
+
| `workflow prepare` | Create a local handoff manifest with hashes of formal input documents |
|
|
139
|
+
| `workflow verify` | Reject missing, stale, unresolved, or route-mismatched handoffs before execution |
|
|
140
|
+
| `workflow complete` | Validate and store a structured stage receipt for downstream review |
|
|
79
141
|
|
|
80
142
|
### `init` options
|
|
81
143
|
|
|
@@ -84,17 +146,21 @@ AGENTS.md + .agents/ # Codex: skills
|
|
|
84
146
|
| `--name <name>` | Project name (defaults to the current directory name) |
|
|
85
147
|
| `--stack <desc>` | Tech-stack description |
|
|
86
148
|
| `--tools <list>` | Comma-separated adapters to enable: `claude`, `codex` |
|
|
149
|
+
| `--model-profile <profile>` | `inherit` (default), `balanced`, or `quality`; `--yes` never opts into costly routing implicitly |
|
|
87
150
|
| `--import <path>` | Import `user-profile` / `feedback` from an existing project |
|
|
88
151
|
| `--yes` | Non-interactive: fill defaults, skip on any conflict |
|
|
89
152
|
|
|
90
153
|
## How it works
|
|
91
154
|
|
|
92
|
-
The generator is a small set of
|
|
155
|
+
The generator is a small set of focused modules:
|
|
93
156
|
|
|
94
157
|
- **manifest** — walks the `common` / `claude` / `codex` template groups, keeping only the groups whose tools are enabled, and produces a flat `{src, dest}` list; duplicate destinations fail before writing and list every source.
|
|
95
158
|
- **render** — substitutes `{{variable}}` placeholders and throws on any undefined variable, so a broken template fails loudly instead of shipping `{{...}}`.
|
|
96
159
|
- **scaffold** — validates safe destinations and symbolic links, resolves conflicts, and reads/renders the complete plan in memory before writing; it also validates `--import` and pulls `user-profile` / `feedback` from it (falling back to the template with an explicit report when an individual source file is missing).
|
|
97
160
|
- **framework metadata** — a fresh init records framework/schema versions, ownership, and generated hashes; update uses them to distinguish safe updates from user changes. Metadata-free v0.1 projects are planned conservatively as legacy installations.
|
|
161
|
+
- **managed entry blocks** — Markdown entry files update only their validated framework block. Unmarked or malformed customized files remain manual merges; JSON settings keep conservative whole-file review semantics.
|
|
162
|
+
- **model routing** — optional profiles map existing workflow stages to premium, standard, economy, inherited, or no-model execution. Claude and Codex adapters use native custom-agent configuration; routing never creates extra S/M/L stages.
|
|
163
|
+
- **workflow handoff** — a local manifest references formal requirements/design/plan files by path and SHA-256. Verification fails closed on stale inputs, unresolved decisions, changed routing, path escape, or symbolic links.
|
|
98
164
|
|
|
99
165
|
## Design principles
|
|
100
166
|
|
|
@@ -103,6 +169,8 @@ The generator is a small set of pure functions:
|
|
|
103
169
|
- **Dual requirement directories** — human draft (`draft/`) → AI final (`final/`) behind a mandatory critic gate.
|
|
104
170
|
- **Project engineering baseline** — 0-to-1 projects establish language-agnostic system, data, quality-attribute, and deployment baselines once; ordinary features record only deltas.
|
|
105
171
|
- **Risk-tiered design** — S-risk requirements proceed directly to implementation; M/L-risk work uses feature-design for how, technical contracts, and engineering impact, with Superpowers selected only when justified by risk.
|
|
172
|
+
- **Optional cost routing** — `inherit` preserves prior behavior; explicit profiles can use stronger models for planning/review and cheaper workers for bounded test work while deterministic test execution uses no model.
|
|
173
|
+
- **Artifact-based handoff** — cross-model stages read formal artifacts through a verified handoff rather than relying on conversational summaries; implementation receipts expose coverage, deviations, and unresolved risk to the final reviewer.
|
|
106
174
|
- **Evidence-based delivery** — before release, verify applicable contracts, tests, migration, rollback, performance, observability, and operations, producing an explicit readiness verdict.
|
|
107
175
|
- **Low-noise memory** — persist independently verifiable engineering nodes, durable decisions, status changes, and session handoffs instead of every small coding step.
|
|
108
176
|
- **Idempotent** — existing files are asked about one by one; `--yes` always skips. Never a silent overwrite.
|
package/README.zh-CN.md
CHANGED
|
@@ -21,6 +21,9 @@ npx @betterdanlins/ai-memory init
|
|
|
21
21
|
# 非交互式
|
|
22
22
|
npx @betterdanlins/ai-memory init --name demo --stack "PHP + Vue" --tools claude,codex --yes
|
|
23
23
|
|
|
24
|
+
# 显式启用分阶段模型路由
|
|
25
|
+
npx @betterdanlins/ai-memory init --model-profile balanced --yes
|
|
26
|
+
|
|
24
27
|
# 从已有项目导入用户级记忆(profile/feedback)
|
|
25
28
|
npx @betterdanlins/ai-memory init --import /path/to/other-project
|
|
26
29
|
|
|
@@ -29,6 +32,9 @@ npx @betterdanlins/ai-memory update --dry-run
|
|
|
29
32
|
|
|
30
33
|
# 只应用无冲突的安全更新;有用户修改时整体拒绝
|
|
31
34
|
npx @betterdanlins/ai-memory update --yes
|
|
35
|
+
|
|
36
|
+
# 已初始化项目从继承模型切换到分层路由
|
|
37
|
+
npx @betterdanlins/ai-memory models configure --profile balanced
|
|
32
38
|
```
|
|
33
39
|
|
|
34
40
|
绝不静默覆盖:已存在的文件会逐个询问(交互式),或直接跳过(`--yes`)。
|
|
@@ -36,12 +42,62 @@ npx @betterdanlins/ai-memory update --yes
|
|
|
36
42
|
生成前会检查重复模板目标、路径越界和符号链接;模板读取与渲染全部通过后才开始写入。若实际磁盘写入中途失败,错误摘要会列出已经写入和尚未写入的文件。
|
|
37
43
|
已初始化项目不能重新运行 `init`;新版 CLI 先通过 `update --dry-run` 识别版本和用户修改。`update --yes` 只应用新增和基线哈希匹配的安全更新,存在合并/审查项时在写入前整体拒绝。
|
|
38
44
|
|
|
45
|
+
## 版本与兼容性
|
|
46
|
+
|
|
47
|
+
### v0.5.0 —— 入口文件受管区块
|
|
48
|
+
|
|
49
|
+
- 为生成的 `AGENTS.md` 和 `CLAUDE.md` 框架区域增加明确的 `ai-memory:managed` 标记。后续升级只替换该区块,区块外的项目命令和自定义规则保持不变。
|
|
50
|
+
- 未修改的 v0.4.0 入口文件可依据生成基线哈希自动迁移为带标记结构。已经修改且没有标记的 v0.4.0 文件需要一次人工合并;标记建立后,后续框架升级不再与用户区块修改冲突。
|
|
51
|
+
- 标记缺失、重复或格式损坏时保守拒绝自动合并,不会退化为整文件覆盖。写入受管区块前还会再次核对当前文件哈希。
|
|
52
|
+
- `.claude/settings.json` 仍是混合 JSON 文件;框架和用户都修改时继续要求审查,不会向 JSON 强行加入 Markdown 标记。
|
|
53
|
+
- 修复 npm 自动改名 `.gitignore` 模板的问题,并安全替换未修改的 v0.4.0 `.ai/runs/.npmignore`;若该废弃文件被本地修改,仍要求人工审查。
|
|
54
|
+
|
|
55
|
+
### v0.4.0 —— 分阶段模型路由与可靠交接
|
|
56
|
+
|
|
57
|
+
- 新增可选的 `inherit`、`balanced`、`quality` profile。新项目和升级项目默认保持 `inherit`,安装 v0.4.0 不会静默增加模型成本,也不会改变 v0.3.0 的工作流深度。
|
|
58
|
+
- 为 Claude Code 和 Codex 新增原生分层 agents,覆盖高级规划/审查、中等或高级实现、低成本或中等测试编写。仍先由原有 S/M/L 流程决定是否需要某个阶段,模型路由只选择执行者。
|
|
59
|
+
- 新增 `models show/configure` 和 `workflow prepare/verify/complete`。跨模型交接通过路径和 SHA-256 引用正式需求、设计与计划;输入过期、未决问题、路由变化、不安全路径和验收回执缺失都会关闭式失败。
|
|
60
|
+
- `.ai/config/model-routing.json` 属于用户配置,`.ai/runs/` 只保存本地运行状态。框架不会自动探测账号模型权限:Claude 使用原生模型别名,Codex agents 使用原生推理等级。
|
|
61
|
+
|
|
62
|
+
### v0.3.0 —— 安全升级与工程工作流
|
|
63
|
+
|
|
64
|
+
- 引入 `.ai/ai-memory.json` 框架/Schema 元数据、文件所有权、生成基线哈希、v0.1 legacy 检测和显式迁移规划。
|
|
65
|
+
- 新增 `update --dry-run` 与无冲突 `update --yes`。用户记忆、需求、架构和设计资产始终保留;修改过的框架文件或混合文件进入人工审查,不会被直接覆盖。
|
|
66
|
+
- 增加重复目标、路径穿越、符号链接/junction 防护、三阶段生成和可诊断的部分写入摘要。
|
|
67
|
+
- 增加语言无关的 `project-inception`、`feature-design`、`delivery-readiness`,覆盖数据建模、技术接口、部署、性能、可观测性、扩展性、可靠性、安全和成本。
|
|
68
|
+
|
|
69
|
+
### 从 v0.3.0 升级到 v0.4.0
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# 只读预览,不得修改项目
|
|
73
|
+
npx @betterdanlins/ai-memory@0.4.0 update --dry-run
|
|
74
|
+
|
|
75
|
+
# 仅在预览没有 merge/review 阻塞项时应用
|
|
76
|
+
npx @betterdanlins/ai-memory@0.4.0 update --yes
|
|
77
|
+
|
|
78
|
+
# 可选:安全升级后显式启用分阶段模型路由
|
|
79
|
+
npx @betterdanlins/ai-memory@0.4.0 models configure --profile balanced
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
升级完成后 profile 仍为 `inherit`,只有显式配置才会改变。不要在已有 v0.3.0 项目重新运行 `init`。若 dry-run 出现 merge/review 项,应以新生成的 v0.4.0 文件为参考人工合并,然后重新执行 dry-run。
|
|
83
|
+
|
|
84
|
+
### 从 v0.4.0 升级到 v0.5.0
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx @betterdanlins/ai-memory@0.5.0 update --dry-run
|
|
88
|
+
npx @betterdanlins/ai-memory@0.5.0 update --yes
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
如果 `AGENTS.md` 或 `CLAUDE.md` 仍是未修改的 v0.4.0 生成版本,会自动迁移。若无标记入口已被定制,dry-run 会报告 `merge`;应对照全新 v0.5.0 参考文件,把项目内容放进 user 区块后重试。标记建立后,只在 `<!-- ai-memory:managed:start/end -->` 之外编辑。
|
|
92
|
+
|
|
39
93
|
## 生成什么
|
|
40
94
|
|
|
41
95
|
```
|
|
42
96
|
.ai/ # 单一源,所有 agent 共享
|
|
43
97
|
├── README.md # 会话进场协议
|
|
44
98
|
├── ai-memory.json # 版本、Schema、工具、所有权和生成基线哈希
|
|
99
|
+
├── config/
|
|
100
|
+
│ └── model-routing.json # inherit/balanced/quality 阶段路由;属于用户配置
|
|
45
101
|
├── memory/
|
|
46
102
|
│ ├── MEMORY.md # 索引 —— agent 进场先读
|
|
47
103
|
│ ├── project-state.md # 技术栈、当前版本、需求进度
|
|
@@ -49,8 +105,9 @@ npx @betterdanlins/ai-memory update --yes
|
|
|
49
105
|
│ ├── user-profile.md # 背景、偏好、沟通方式
|
|
50
106
|
│ ├── feedback.md # 从反馈提炼的行为规范
|
|
51
107
|
│ └── features/ # 功能档案
|
|
108
|
+
├── runs/ # 本地交接与阶段回执;默认不进入 Git
|
|
52
109
|
└── skills/ # 方法论:requirements-flow、architecture、
|
|
53
|
-
#
|
|
110
|
+
# feature-design、model-routing、delivery-readiness 等
|
|
54
111
|
|
|
55
112
|
docs/architecture/ # 0→1 项目工程基线
|
|
56
113
|
├── system-context.md # 系统边界、参与者、模块和关键流程
|
|
@@ -66,7 +123,7 @@ docs/design/vX.Y.Z/ # M/L 级功能 how、技术接口与工程增量
|
|
|
66
123
|
|
|
67
124
|
# 薄适配层,仅为你启用的工具生成:
|
|
68
125
|
CLAUDE.md + .claude/ # Claude Code:commands、skills、agents、settings
|
|
69
|
-
AGENTS.md + .agents/
|
|
126
|
+
AGENTS.md + .agents/ + .codex/# Codex:skills 与分层自定义 agents
|
|
70
127
|
```
|
|
71
128
|
|
|
72
129
|
## CLI 命令
|
|
@@ -76,6 +133,11 @@ AGENTS.md + .agents/ # Codex:skills
|
|
|
76
133
|
| `init` | 初始化全新项目;检测到已有 ai-memory 安装时拒绝执行 |
|
|
77
134
|
| `update --dry-run` | 只读分析旧项目到当前 CLI 版本的升级计划 |
|
|
78
135
|
| `update --yes` | 应用无冲突安全更新;用户修改、混合文件冲突或缺失迁移路径会拒绝执行 |
|
|
136
|
+
| `models show` | 显示当前 profile 和每个阶段解析后的模型等级 |
|
|
137
|
+
| `models configure --profile <name>` | 选择 `inherit`、`balanced` 或 `quality`,不修改需求、设计或代码 |
|
|
138
|
+
| `workflow prepare` | 用正式输入文档的哈希创建本地交接清单 |
|
|
139
|
+
| `workflow verify` | 执行前拒绝缺失、过期、有未决问题或路由不匹配的交接 |
|
|
140
|
+
| `workflow complete` | 校验并保存供后续审查使用的结构化阶段回执 |
|
|
79
141
|
|
|
80
142
|
### `init` 选项
|
|
81
143
|
|
|
@@ -84,17 +146,21 @@ AGENTS.md + .agents/ # Codex:skills
|
|
|
84
146
|
| `--name <name>` | 项目名(默认取当前目录名) |
|
|
85
147
|
| `--stack <desc>` | 技术栈描述 |
|
|
86
148
|
| `--tools <list>` | 逗号分隔的适配层:`claude`、`codex` |
|
|
149
|
+
| `--model-profile <profile>` | `inherit`(默认)、`balanced` 或 `quality`;`--yes` 不会静默启用高成本路由 |
|
|
87
150
|
| `--import <path>` | 从已有项目导入 `user-profile` / `feedback` |
|
|
88
151
|
| `--yes` | 非交互模式:缺参用默认值,冲突一律跳过 |
|
|
89
152
|
|
|
90
153
|
## 工作原理
|
|
91
154
|
|
|
92
|
-
|
|
155
|
+
生成器由一组职责明确的小模块构成:
|
|
93
156
|
|
|
94
157
|
- **manifest** —— 遍历 `common` / `claude` / `codex` 模板组,只保留已启用工具对应的组,产出一份扁平的 `{src, dest}` 清单;重复目标会在写入前报错并列出来源。
|
|
95
158
|
- **render** —— 替换 `{{变量}}` 占位符,遇到任何未定义变量即抛错,让坏掉的模板大声失败,而不是把 `{{...}}` 发出去。
|
|
96
159
|
- **scaffold** —— 预检安全目标路径和符号链接,处理冲突并在内存中完成全部读取/渲染后再逐个写入;提供 `--import` 时先校验导入目录,再从中拉取 `user-profile` / `feedback`(单个源文件缺失则回退到模板并明确报告)。
|
|
97
160
|
- **framework metadata** —— 全新 init 记录 framework/schema 版本、文件所有权和生成哈希;update 据此区分安全更新与用户修改。无元数据的 v0.1 项目按 legacy 保守规划。
|
|
161
|
+
- **managed entry blocks** —— Markdown 入口只更新通过校验的框架区块;无标记或标记损坏的定制文件仍人工合并,JSON settings 继续采用保守整文件审查。
|
|
162
|
+
- **model routing** —— 可选 profile 把现有阶段映射为 premium、standard、economy、inherit 或无模型执行;Claude/Codex 使用原生自定义 agent 配置,路由不会增加 S/M/L 阶段。
|
|
163
|
+
- **workflow handoff** —— 本地清单只用路径和 SHA-256 引用正式需求/设计/计划;输入过期、未决问题、路由变化、路径越界或符号链接都会关闭式失败。
|
|
98
164
|
|
|
99
165
|
## 设计原则
|
|
100
166
|
|
|
@@ -103,6 +169,8 @@ AGENTS.md + .agents/ # Codex:skills
|
|
|
103
169
|
- **需求双目录** —— 人工粗稿(`draft/`)→ AI 定稿(`final/`),中间隔一道强制 critic 门。
|
|
104
170
|
- **项目工程基线** —— 0→1 项目先建立语言无关的系统、数据、质量属性和部署基线;普通功能只记录增量,不重复设计全局架构。
|
|
105
171
|
- **风险分级设计** —— S 级需求直接实现;M/L 级用 feature-design 补齐 how、技术接口和工程影响,Superpowers 只按风险选择使用。
|
|
172
|
+
- **可选成本路由** —— `inherit` 保持旧行为;显式 profile 才用高级模型规划/审查和低成本 worker 做有边界的测试工作,确定性测试执行不调用模型。
|
|
173
|
+
- **工程产物交接** —— 跨模型阶段通过校验后的正式文档交接,不依赖对话摘要;阶段回执向最终 reviewer 暴露验收覆盖、设计偏差和未决风险。
|
|
106
174
|
- **证据化交付** —— 发布前按风险检查契约、测试、迁移、回滚、性能、可观测性和运行保障,输出明确 ready 状态。
|
|
107
175
|
- **低噪声记忆** —— 只在可验收工程节点、关键决策、状态变化和会话切换时落盘,不记录每个细碎代码步骤。
|
|
108
176
|
- **幂等** —— 已存在的文件逐个询问;`--yes` 一律跳过。绝不静默覆盖。
|
package/bin/cli.js
CHANGED
|
@@ -4,7 +4,12 @@ import { readFile } from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { applyFrameworkUpdate, detectInstallation, METADATA_DEST, planFrameworkUpdate, writeFrameworkMetadata } from '../src/framework.js';
|
|
7
|
+
import {
|
|
8
|
+
createModelRoutingConfig, MODEL_PROFILES, readModelRoutingConfig,
|
|
9
|
+
resolveModelRouting, writeModelRoutingConfig,
|
|
10
|
+
} from '../src/model-routing.js';
|
|
7
11
|
import { ScaffoldError, scaffold } from '../src/scaffold.js';
|
|
12
|
+
import { prepareHandoff, recordStageResult, verifyHandoff } from '../src/workflow.js';
|
|
8
13
|
|
|
9
14
|
const TEMPLATES = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'templates');
|
|
10
15
|
const PACKAGE = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -18,6 +23,7 @@ program
|
|
|
18
23
|
.option('--name <name>', '项目名')
|
|
19
24
|
.option('--stack <desc>', '技术栈描述')
|
|
20
25
|
.option('--tools <list>', '启用工具,逗号分隔:claude,codex')
|
|
26
|
+
.option('--model-profile <profile>', '模型路由策略:inherit,balanced,quality', 'inherit')
|
|
21
27
|
.option('--import <path>', '从已有项目导入 user-profile/feedback')
|
|
22
28
|
.option('--yes', '非交互模式:缺参用默认值,冲突一律跳过')
|
|
23
29
|
.action(async (opts) => {
|
|
@@ -30,6 +36,9 @@ program
|
|
|
30
36
|
throw new Error(`${label};不要重新运行 init,请先执行 ai-memory update --dry-run`);
|
|
31
37
|
}
|
|
32
38
|
let { name, stack, tools: toolsRaw } = opts;
|
|
39
|
+
if (!MODEL_PROFILES.includes(opts.modelProfile)) {
|
|
40
|
+
throw new Error(`model profile 仅支持 ${MODEL_PROFILES.join('、')},收到: ${opts.modelProfile}`);
|
|
41
|
+
}
|
|
33
42
|
|
|
34
43
|
// Validate CLI-supplied --tools before entering interactive prompts
|
|
35
44
|
if (toolsRaw !== undefined) {
|
|
@@ -43,7 +52,7 @@ program
|
|
|
43
52
|
|
|
44
53
|
let importFrom = opts.import;
|
|
45
54
|
if (!opts.yes) {
|
|
46
|
-
const { input, checkbox } = await import('@inquirer/prompts');
|
|
55
|
+
const { input, checkbox, select } = await import('@inquirer/prompts');
|
|
47
56
|
name ??= await input({ message: '项目名:', default: path.basename(targetDir) });
|
|
48
57
|
stack ??= await input({ message: '技术栈描述:', default: '(待补充)' });
|
|
49
58
|
toolsRaw ??= (await checkbox({
|
|
@@ -56,6 +65,17 @@ program
|
|
|
56
65
|
importFrom ??= (await input({
|
|
57
66
|
message: '从已有项目导入 user-profile/feedback?(输入项目路径,留空跳过)', default: '',
|
|
58
67
|
})) || undefined;
|
|
68
|
+
if (opts.modelProfile === 'inherit') {
|
|
69
|
+
opts.modelProfile = await select({
|
|
70
|
+
message: '模型路由策略:',
|
|
71
|
+
choices: [
|
|
72
|
+
{ name: 'inherit - 继承当前会话模型,兼容旧行为', value: 'inherit' },
|
|
73
|
+
{ name: 'balanced - 高级规划/中等实现/便宜测试/高级审查', value: 'balanced' },
|
|
74
|
+
{ name: 'quality - 关键实现与诊断也使用高级模型', value: 'quality' },
|
|
75
|
+
],
|
|
76
|
+
default: 'inherit',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
59
79
|
}
|
|
60
80
|
name ??= path.basename(targetDir);
|
|
61
81
|
stack ??= '(待补充)';
|
|
@@ -74,7 +94,9 @@ program
|
|
|
74
94
|
};
|
|
75
95
|
|
|
76
96
|
const date = new Date().toISOString().slice(0, 10);
|
|
77
|
-
const vars = {
|
|
97
|
+
const vars = {
|
|
98
|
+
projectName: name, techStack: stack, date, modelProfile: opts.modelProfile, frameworkVersion: PACKAGE.version,
|
|
99
|
+
};
|
|
78
100
|
const r = await scaffold({ templatesRoot: TEMPLATES, targetDir, vars, tools, onConflict, importFrom });
|
|
79
101
|
if (r.skipped.length === 0) {
|
|
80
102
|
try {
|
|
@@ -93,12 +115,81 @@ program
|
|
|
93
115
|
console.log('警告:存在冲突跳过项,未创建框架元数据;当前结果不能自动升级');
|
|
94
116
|
}
|
|
95
117
|
console.log(`完成:写入 ${r.written.length},跳过 ${r.skipped.length}`);
|
|
118
|
+
console.log(`模型路由:${opts.modelProfile}`);
|
|
96
119
|
if (r.imported.length) for (const d of r.imported) console.log(` 已导入 ${d}`);
|
|
97
120
|
if (r.fallback.length) for (const d of r.fallback) console.log(` 导入源缺失,使用模板 ${d}`);
|
|
98
121
|
if (r.skipped.length) for (const d of r.skipped) console.log(` 跳过 ${d}`);
|
|
99
122
|
console.log('下一步:打开项目让 agent 读 .ai/memory/MEMORY.md;把项目命令补进 CLAUDE.md/AGENTS.md。');
|
|
100
123
|
});
|
|
101
124
|
|
|
125
|
+
const models = program.command('models').description('查看或配置阶段模型路由');
|
|
126
|
+
|
|
127
|
+
models
|
|
128
|
+
.command('show')
|
|
129
|
+
.description('显示当前模型路由配置与阶段映射')
|
|
130
|
+
.action(async () => {
|
|
131
|
+
const config = await readModelRoutingConfig(process.cwd());
|
|
132
|
+
console.log(`模型路由:${config.profile}`);
|
|
133
|
+
for (const [stage, tier] of Object.entries(resolveModelRouting(config))) console.log(` ${stage}: ${tier}`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
models
|
|
137
|
+
.command('configure')
|
|
138
|
+
.description('切换模型路由预设;不修改需求、设计或代码')
|
|
139
|
+
.requiredOption('--profile <profile>', 'inherit,balanced,quality')
|
|
140
|
+
.action(async (opts) => {
|
|
141
|
+
const installation = await detectInstallation(process.cwd());
|
|
142
|
+
if (installation.kind === 'none') throw new Error('当前目录不是 ai-memory 项目,请先运行 init');
|
|
143
|
+
const current = await readModelRoutingConfig(process.cwd());
|
|
144
|
+
const next = createModelRoutingConfig(opts.profile);
|
|
145
|
+
next.overrides = current.overrides;
|
|
146
|
+
await writeModelRoutingConfig(process.cwd(), next);
|
|
147
|
+
console.log(`模型路由已切换:${current.profile} → ${next.profile}`);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const workflow = program.command('workflow').description('创建并校验跨模型阶段交接');
|
|
151
|
+
|
|
152
|
+
workflow
|
|
153
|
+
.command('prepare')
|
|
154
|
+
.description('根据正式文档生成带哈希的本地交接清单')
|
|
155
|
+
.requiredOption('--feature <id>', '功能标识,不得包含路径分隔符')
|
|
156
|
+
.requiredOption('--stage <stage>', '目标工作流阶段')
|
|
157
|
+
.requiredOption('--risk <level>', 'S,M,L')
|
|
158
|
+
.option('--input <path>', '正式输入文件,可重复', collect, [])
|
|
159
|
+
.option('--acceptance <id>', '验收标准编号,可重复', collect, [])
|
|
160
|
+
.option('--scope <path>', '允许变更范围,可重复', collect, [])
|
|
161
|
+
.option('--unresolved <text>', '未决问题,可重复;存在时 verify 会阻止进入阶段', collect, [])
|
|
162
|
+
.action(async (opts) => {
|
|
163
|
+
const result = await prepareHandoff({
|
|
164
|
+
targetDir: process.cwd(), feature: opts.feature, stage: opts.stage, risk: opts.risk,
|
|
165
|
+
inputs: opts.input, acceptanceCriteria: opts.acceptance, allowedChangeScope: opts.scope,
|
|
166
|
+
unresolvedDecisions: opts.unresolved,
|
|
167
|
+
});
|
|
168
|
+
console.log(`交接清单已生成:${result.path}`);
|
|
169
|
+
console.log(`执行等级:${result.handoff.executorTier}`);
|
|
170
|
+
if (result.handoff.unresolvedDecisions.length) console.log(`警告:仍有 ${result.handoff.unresolvedDecisions.length} 个未决问题`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
workflow
|
|
174
|
+
.command('verify')
|
|
175
|
+
.description('验证交接输入、哈希、路由和未决问题')
|
|
176
|
+
.requiredOption('--feature <id>', '功能标识')
|
|
177
|
+
.option('--stage <stage>', '期望工作流阶段')
|
|
178
|
+
.action(async (opts) => {
|
|
179
|
+
const handoff = await verifyHandoff({ targetDir: process.cwd(), feature: opts.feature, expectedStage: opts.stage });
|
|
180
|
+
console.log(`交接校验通过:${handoff.featureId} / ${handoff.stage} / ${handoff.executorTier}`);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
workflow
|
|
184
|
+
.command('complete')
|
|
185
|
+
.description('校验阶段回执并保存到对应 run 目录')
|
|
186
|
+
.requiredOption('--feature <id>', '功能标识')
|
|
187
|
+
.requiredOption('--result <path>', '项目内阶段回执 JSON 路径')
|
|
188
|
+
.action(async (opts) => {
|
|
189
|
+
const recorded = await recordStageResult({ targetDir: process.cwd(), feature: opts.feature, resultPath: opts.result });
|
|
190
|
+
console.log(`阶段回执已记录:${recorded.path} / ${recorded.result.status}`);
|
|
191
|
+
});
|
|
192
|
+
|
|
102
193
|
program
|
|
103
194
|
.command('update')
|
|
104
195
|
.description('检查当前项目升级到本 CLI 版本所需的变更')
|
|
@@ -112,8 +203,9 @@ program
|
|
|
112
203
|
printUpdatePlan(plan);
|
|
113
204
|
if (opts.yes) {
|
|
114
205
|
const result = await applyFrameworkUpdate({ targetDir: process.cwd(), templatesRoot: TEMPLATES, plan });
|
|
115
|
-
console.log(`安全更新完成:写入 ${result.written.length}`);
|
|
206
|
+
console.log(`安全更新完成:写入 ${result.written.length},移除 ${result.removed.length}`);
|
|
116
207
|
for (const dest of result.written) console.log(` ${dest}`);
|
|
208
|
+
for (const dest of result.removed) console.log(` 移除 ${dest}`);
|
|
117
209
|
}
|
|
118
210
|
});
|
|
119
211
|
|
|
@@ -125,6 +217,10 @@ program.parseAsync().catch((e) => {
|
|
|
125
217
|
if (e.name === 'ScaffoldError') {
|
|
126
218
|
console.error(`已写入 ${e.summary.written.length}:`);
|
|
127
219
|
for (const d of e.summary.written) console.error(` ${d}`);
|
|
220
|
+
if (e.summary.removed?.length) {
|
|
221
|
+
console.error(`已移除 ${e.summary.removed.length}:`);
|
|
222
|
+
for (const d of e.summary.removed) console.error(` ${d}`);
|
|
223
|
+
}
|
|
128
224
|
console.error(`尚未写入 ${e.notWritten.length}:`);
|
|
129
225
|
for (const d of e.notWritten) console.error(` ${d}`);
|
|
130
226
|
if (e.summary.skipped.length) {
|
|
@@ -147,6 +243,8 @@ function printUpdatePlan(plan) {
|
|
|
147
243
|
const groups = [
|
|
148
244
|
['add', '新增'],
|
|
149
245
|
['update', '可安全更新'],
|
|
246
|
+
['update-managed', '更新受管区块'],
|
|
247
|
+
['remove', '安全移除'],
|
|
150
248
|
['merge', '需要合并'],
|
|
151
249
|
['review', '需要人工审查'],
|
|
152
250
|
['review-remove', '需要确认移除'],
|
|
@@ -167,3 +265,7 @@ function printUpdatePlan(plan) {
|
|
|
167
265
|
console.log('升级计划完成');
|
|
168
266
|
if (process.argv.includes('--dry-run')) console.log('dry-run:未修改任何文件');
|
|
169
267
|
}
|
|
268
|
+
|
|
269
|
+
function collect(value, previous) {
|
|
270
|
+
return [...previous, value];
|
|
271
|
+
}
|
package/package.json
CHANGED
package/src/framework.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { access, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { buildManifest } from './manifest.js';
|
|
5
5
|
import { matchesLegacyV01Baseline } from './legacy-v0.1.js';
|
|
6
|
+
import { hasValidManagedBlock, isManagedBlockFile, replaceManagedBlock } from './managed-blocks.js';
|
|
6
7
|
import { migrationsBetween } from './migrations.js';
|
|
7
8
|
import { assertNoSymlinkPath, resolveSafeDestination } from './path-safety.js';
|
|
8
9
|
import { render } from './render.js';
|
|
@@ -12,6 +13,7 @@ export const CURRENT_SCHEMA_VERSION = 1;
|
|
|
12
13
|
export const METADATA_DEST = '.ai/ai-memory.json';
|
|
13
14
|
|
|
14
15
|
const USER_PREFIXES = [
|
|
16
|
+
'.ai/config/',
|
|
15
17
|
'.ai/memory/',
|
|
16
18
|
'docs/architecture/',
|
|
17
19
|
'docs/requirements/v',
|
|
@@ -88,7 +90,8 @@ export async function planFrameworkUpdate({ targetDir, templatesRoot, frameworkV
|
|
|
88
90
|
if (installation.kind === 'none') throw new Error('当前目录不是 ai-memory 项目,请先运行 init');
|
|
89
91
|
|
|
90
92
|
const tools = installation.kind === 'metadata' ? installation.metadata.tools : installation.tools;
|
|
91
|
-
const
|
|
93
|
+
const rawVars = installation.kind === 'metadata' ? installation.metadata.templateVars : installation.templateVars;
|
|
94
|
+
const vars = { modelProfile: 'inherit', ...rawVars, frameworkVersion };
|
|
92
95
|
if (installation.kind === 'metadata' && compareVersions(installation.metadata.frameworkVersion, frameworkVersion) > 0) {
|
|
93
96
|
throw new Error(`项目框架版本 ${installation.metadata.frameworkVersion} 高于当前 CLI ${frameworkVersion},请使用更新版本的 CLI`);
|
|
94
97
|
}
|
|
@@ -109,6 +112,9 @@ export async function planFrameworkUpdate({ targetDir, templatesRoot, frameworkV
|
|
|
109
112
|
|
|
110
113
|
const currentHash = hashContent(current);
|
|
111
114
|
const desiredHash = hashContent(desired);
|
|
115
|
+
if (isManagedBlockFile(dest) && !hasValidManagedBlock(desired)) {
|
|
116
|
+
throw new Error(`受管模板缺少有效 managed 标记: ${dest}`);
|
|
117
|
+
}
|
|
112
118
|
if (currentHash === desiredHash) {
|
|
113
119
|
actions.push({ action: 'unchanged', dest, ownership });
|
|
114
120
|
} else if (ownership === 'user') {
|
|
@@ -117,18 +123,33 @@ export async function planFrameworkUpdate({ targetDir, templatesRoot, frameworkV
|
|
|
117
123
|
const matchesBaseline = installation.kind === 'metadata'
|
|
118
124
|
? installation.metadata.files[dest]?.sha256 === currentHash
|
|
119
125
|
: matchesLegacyV01Baseline(dest, current, vars);
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
if (isManagedBlockFile(dest) && hasValidManagedBlock(current)) {
|
|
127
|
+
const merged = replaceManagedBlock(current, desired);
|
|
128
|
+
actions.push({
|
|
129
|
+
action: merged === current ? 'unchanged' : 'update-managed',
|
|
130
|
+
dest, ownership, currentHash, desiredHash,
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
const action = matchesBaseline ? 'update' : ownership === 'mixed' ? 'merge' : 'review';
|
|
134
|
+
actions.push({ action, dest, ownership, currentHash, desiredHash });
|
|
135
|
+
}
|
|
122
136
|
}
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
if (installation.kind === 'metadata') {
|
|
126
140
|
for (const [dest, record] of Object.entries(installation.metadata.files)) {
|
|
127
|
-
|
|
141
|
+
const destPath = resolveSafeDestination(targetDir, dest);
|
|
142
|
+
const current = await readOptional(destPath);
|
|
143
|
+
if (desiredDests.has(dest) || current === undefined) continue;
|
|
144
|
+
const currentHash = hashContent(current);
|
|
145
|
+
const canRemove = record.ownership === 'framework'
|
|
146
|
+
&& ownershipFor(dest) === 'framework'
|
|
147
|
+
&& record.sha256 === currentHash;
|
|
128
148
|
actions.push({
|
|
129
|
-
action: record.ownership === 'user' ? 'preserve' : 'review-remove',
|
|
149
|
+
action: record.ownership === 'user' ? 'preserve' : canRemove ? 'remove' : 'review-remove',
|
|
130
150
|
dest,
|
|
131
151
|
ownership: record.ownership,
|
|
152
|
+
currentHash,
|
|
132
153
|
});
|
|
133
154
|
}
|
|
134
155
|
}
|
|
@@ -138,6 +159,7 @@ export async function planFrameworkUpdate({ targetDir, templatesRoot, frameworkV
|
|
|
138
159
|
frameworkVersion,
|
|
139
160
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
140
161
|
tools,
|
|
162
|
+
templateVars: vars,
|
|
141
163
|
migrations,
|
|
142
164
|
actions,
|
|
143
165
|
};
|
|
@@ -153,33 +175,51 @@ export async function applyFrameworkUpdate({ targetDir, templatesRoot, plan }) {
|
|
|
153
175
|
throw new Error(`存在不可自动执行的 Schema 迁移: ${manualMigrations.map(item => item.id).join(', ')}`);
|
|
154
176
|
}
|
|
155
177
|
|
|
156
|
-
const vars = plan.
|
|
157
|
-
? plan.installation.metadata.templateVars
|
|
158
|
-
: plan.installation.templateVars;
|
|
178
|
+
const vars = plan.templateVars;
|
|
159
179
|
const manifest = await buildManifest(templatesRoot, plan.tools);
|
|
160
180
|
const sources = new Map(manifest.map(entry => [entry.dest, entry.src]));
|
|
161
181
|
const candidates = [];
|
|
162
182
|
|
|
163
|
-
for (const action of plan.actions.filter(item =>
|
|
183
|
+
for (const action of plan.actions.filter(item => ['add', 'update', 'update-managed', 'remove'].includes(item.action))) {
|
|
184
|
+
if (action.action === 'remove') {
|
|
185
|
+
const destPath = resolveSafeDestination(targetDir, action.dest);
|
|
186
|
+
await assertNoSymlinkPath(targetDir, destPath);
|
|
187
|
+
const current = await readFile(destPath, 'utf8');
|
|
188
|
+
if (hashContent(current) !== action.currentHash) {
|
|
189
|
+
throw new Error(`文件在预检后发生变化,请重新 dry-run: ${action.dest}`);
|
|
190
|
+
}
|
|
191
|
+
candidates.push({ ...action, destPath });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
164
194
|
const src = sources.get(action.dest);
|
|
165
195
|
if (!src) throw new Error(`升级计划缺少当前模板: ${action.dest}`);
|
|
166
196
|
const destPath = resolveSafeDestination(targetDir, action.dest);
|
|
167
197
|
await assertNoSymlinkPath(targetDir, destPath);
|
|
168
|
-
|
|
169
|
-
|
|
198
|
+
let current;
|
|
199
|
+
if (action.action !== 'add') {
|
|
200
|
+
current = await readFile(destPath, 'utf8');
|
|
170
201
|
if (action.currentHash && hashContent(current) !== action.currentHash) {
|
|
171
202
|
throw new Error(`文件在预检后发生变化,请重新 dry-run: ${action.dest}`);
|
|
172
203
|
}
|
|
173
204
|
}
|
|
174
|
-
|
|
205
|
+
const desired = render(await readFile(src, 'utf8'), vars);
|
|
206
|
+
const content = action.action === 'update-managed'
|
|
207
|
+
? replaceManagedBlock(current, desired)
|
|
208
|
+
: desired;
|
|
209
|
+
candidates.push({ ...action, destPath, content });
|
|
175
210
|
}
|
|
176
211
|
|
|
177
|
-
const summary = { written: [], skipped: [], imported: [], fallback: [] };
|
|
212
|
+
const summary = { written: [], removed: [], skipped: [], imported: [], fallback: [] };
|
|
178
213
|
for (const [index, item] of candidates.entries()) {
|
|
179
214
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
215
|
+
if (item.action === 'remove') {
|
|
216
|
+
await unlink(item.destPath);
|
|
217
|
+
summary.removed.push(item.dest);
|
|
218
|
+
} else {
|
|
219
|
+
await mkdir(path.dirname(item.destPath), { recursive: true });
|
|
220
|
+
await writeFile(item.destPath, item.content, { flag: item.action === 'add' ? 'wx' : 'w' });
|
|
221
|
+
summary.written.push(item.dest);
|
|
222
|
+
}
|
|
183
223
|
} catch (err) {
|
|
184
224
|
throw new ScaffoldError({
|
|
185
225
|
phase: 'update', dest: item.dest, destPath: item.destPath, cause: err, summary,
|