create-yss-spec 3.5.4 → 3.5.6
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 +10 -0
- package/cli-core.lock.json +130 -0
- package/package.json +7 -3
- package/resources/upgrade-skill/SKILL.md +26 -0
- package/resources/upgrade-skill/protocol.md +40 -0
- package/resources/upgrade-skill/references/cli-families.md +14 -0
- package/src/cli/help.js +1 -0
- package/src/cli/index.js +12 -0
- package/src/filesystem/transaction-runner.js +27 -0
- package/src/template/asset-runtime.js +9 -1
- package/src/template/instance-runtime.js +4 -2
- package/src/template/verification-runtime.js +9 -1
- package/template/.agents/skills/.strategic-design-skills-manifest.json +2 -2
- package/template/.agents/skills/yss-harness-upgrade/SKILL.md +26 -0
- package/template/.agents/skills/yss-harness-upgrade/references/cli-families.md +14 -0
- package/template/.agents/skills/yss-implementation-contract-compiler/references/boundaries.md +1 -0
- package/template/.agents/skills/yss-product-lifecycle/SKILL.md +15 -19
- package/template/.agents/skills/yss-stage-decision/SKILL.md +3 -0
- package/template/.agents/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +8 -4
- package/template/.agents/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +8 -4
- package/template/.agents/skills/yss-stage-decision/tests/run-scenarios.mjs +13 -1
- package/template/.codex/skills/yss-harness-upgrade/SKILL.md +26 -0
- package/template/.codex/skills/yss-harness-upgrade/references/cli-families.md +14 -0
- package/template/.codex/skills/yss-implementation-contract-compiler/references/boundaries.md +1 -0
- package/template/.codex/skills/yss-product-lifecycle/SKILL.md +15 -19
- package/template/.codex/skills/yss-stage-decision/SKILL.md +3 -0
- package/template/.codex/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +8 -4
- package/template/.codex/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +8 -4
- package/template/.codex/skills/yss-stage-decision/tests/run-scenarios.mjs +13 -1
- package/template/.cursor/skills/yss-harness-upgrade/SKILL.md +26 -0
- package/template/.cursor/skills/yss-harness-upgrade/references/cli-families.md +14 -0
- package/template/.cursor/skills/yss-implementation-contract-compiler/references/boundaries.md +1 -0
- package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +15 -19
- package/template/.cursor/skills/yss-stage-decision/SKILL.md +3 -0
- package/template/.cursor/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +8 -4
- package/template/.cursor/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +8 -4
- package/template/.cursor/skills/yss-stage-decision/tests/run-scenarios.mjs +13 -1
- package/template/.pi/skills/yss-harness-upgrade/SKILL.md +26 -0
- package/template/.pi/skills/yss-harness-upgrade/references/cli-families.md +14 -0
- package/template/.pi/skills/yss-implementation-contract-compiler/references/boundaries.md +1 -0
- package/template/.pi/skills/yss-product-lifecycle/SKILL.md +15 -19
- package/template/.pi/skills/yss-stage-decision/SKILL.md +3 -0
- package/template/.pi/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +8 -4
- package/template/.pi/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +8 -4
- package/template/.pi/skills/yss-stage-decision/tests/run-scenarios.mjs +13 -1
- package/template/.template-spec/agents/yss-skill-registry.yaml +9 -1
- package/template/.template-spec/process/contract-reading.md +1 -1
- package/template/.template-spec/process/document-writing.md +12 -0
- package/template/.template-spec/process/harness-upgrade.md +40 -0
- package/template/.template-spec/process/lifecycle-artifact-map.md +2 -2
- package/template/.template-spec/process/lifecycle-registry.yaml +3 -1
- package/template/.template-spec/process/schemas/approval-record.schema.json +23 -0
- package/template/.template-spec/process/stage-tracking.md +2 -2
- package/template/.template-spec/process/structured-assets.md +35 -0
- package/template/.template-spec/process/templates/lifecycle-checkpoint-template.json +48 -0
- package/template/.template-spec/templates/approval-record-template.json +23 -0
- package/template/scripts/check-plan-stage-coverage +12 -0
- package/template/scripts/contract +6 -0
- package/template/scripts/lib/approval-record.mjs +4 -3
- package/template/scripts/lib/asset-command.mjs +35 -0
- package/template/scripts/lib/asset-migration.mjs +101 -0
- package/template/scripts/lib/asset-transactions.mjs +235 -0
- package/template/scripts/lib/checkpoint-boundary.mjs +3 -0
- package/template/scripts/lib/contract-views.mjs +7 -1
- package/template/scripts/lib/json-schema.mjs +43 -4
- package/template/scripts/lib/lifecycle-context-query.mjs +7 -0
- package/template/scripts/lib/lifecycle-presentation.mjs +77 -0
- package/template/scripts/lib/lifecycle-status.mjs +16 -1
- package/template/scripts/lib/plan-stage-coverage.mjs +40 -0
- package/template/scripts/lib/reading-view-bundle.mjs +1 -0
- package/template/scripts/lib/reading-view-markdown.mjs +8 -6
- package/template/scripts/lib/stage-tracking-migration.mjs +11 -3
- package/template/scripts/lib/structured-assets.mjs +81 -0
- package/template/scripts/lib/task-package.mjs +4 -1
- package/template/scripts/lifecycle-status +6 -3
- package/template/scripts/verify-lifecycle-checkpoint +8 -5
- package/template/skills-lock.json +21 -9
- package/template.manifest.json +2 -1
- package/template.snapshot.json +5 -5
- package/vendor/cli-core/build.mjs +302 -0
- package/vendor/cli-core/bundle.mjs +198 -0
- package/vendor/cli-core/cli.mjs +182 -0
- package/vendor/cli-core/command-runner.mjs +107 -0
- package/vendor/cli-core/engine.mjs +239 -0
- package/vendor/cli-core/family.mjs +28 -0
- package/vendor/cli-core/gitignore.mjs +15 -0
- package/vendor/cli-core/identity.mjs +274 -0
- package/vendor/cli-core/io.mjs +153 -0
- package/vendor/cli-core/layout-migration.mjs +64 -0
- package/vendor/cli-core/legacy.mjs +19 -0
- package/vendor/cli-core/migrate.mjs +237 -0
- package/vendor/cli-core/migration-adapter.mjs +19 -0
- package/vendor/cli-core/package.json +13 -0
- package/vendor/cli-core/scaffold.mjs +117 -0
- package/vendor/cli-core/spec-migration-adapter.mjs +83 -0
- package/vendor/cli-core/tests/build.test.mjs +40 -0
- package/vendor/cli-core/tests/cli.test.mjs +1065 -0
- package/vendor/cli-core/tests/migration-version.test.mjs +8 -0
- package/vendor/cli-core/tests/package-contract.mjs +89 -0
- package/vendor/cli-core/transaction.mjs +529 -0
- package/vendor/cli-core/update.mjs +79 -0
- package/vendor/cli-core/vendor/NOTICES.md +6 -0
- package/vendor/cli-core/vendor/yaml.LICENSE +13 -0
- package/vendor/cli-core/vendor/yaml.mjs +6311 -0
- package/vendor/cli-core/verification.mjs +131 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// Build-time only: source revisions are resolved before reading any distribution bytes.
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import {
|
|
7
|
+
ensure,
|
|
8
|
+
hash,
|
|
9
|
+
json,
|
|
10
|
+
relative,
|
|
11
|
+
governance,
|
|
12
|
+
safe,
|
|
13
|
+
write,
|
|
14
|
+
readJson,
|
|
15
|
+
} from "./io.mjs";
|
|
16
|
+
import { yaml, PROFILE, loadBundle } from "./bundle.mjs";
|
|
17
|
+
import { familyFor } from "./family.mjs";
|
|
18
|
+
const CORE_PATH = ".template-source/cli-core";
|
|
19
|
+
const UPGRADE_SKILL = ".agents/skills/yss-harness-upgrade";
|
|
20
|
+
const UPGRADE_PROTOCOL = ".template-spec/process/harness-upgrade.md";
|
|
21
|
+
function upgradeResources(root) {
|
|
22
|
+
if (!fs.existsSync(path.join(root, UPGRADE_SKILL))) return {};
|
|
23
|
+
const files = {};
|
|
24
|
+
for (const ref of Object.keys(inventory(path.join(root, UPGRADE_SKILL)))) {
|
|
25
|
+
let bytes = fs.readFileSync(path.join(root, UPGRADE_SKILL, ref));
|
|
26
|
+
if (ref === 'SKILL.md') bytes = Buffer.from(bytes.toString().replace('../../../.template-spec/process/harness-upgrade.md', 'protocol.md'));
|
|
27
|
+
files[ref] = bytes;
|
|
28
|
+
}
|
|
29
|
+
files['protocol.md'] = fs.readFileSync(path.join(root, UPGRADE_PROTOCOL));
|
|
30
|
+
return files;
|
|
31
|
+
}
|
|
32
|
+
function archive(source, ref, consume, paths = []) {
|
|
33
|
+
const revision = execFileSync(
|
|
34
|
+
"git",
|
|
35
|
+
["-C", source, "rev-parse", `${ref === "WORKTREE" ? "HEAD" : ref}^{commit}`],
|
|
36
|
+
{ encoding: "utf8" },
|
|
37
|
+
).trim();
|
|
38
|
+
ensure(/^[a-f0-9]{40}$/.test(revision), "source revision 必须是完整提交");
|
|
39
|
+
if (ref === "WORKTREE") {
|
|
40
|
+
return consume(fs.realpathSync(source), revision, "working-tree");
|
|
41
|
+
}
|
|
42
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "harness-source-"));
|
|
43
|
+
try {
|
|
44
|
+
const tar = execFileSync("git", ["-C", source, "archive", revision, ...paths], {
|
|
45
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
46
|
+
});
|
|
47
|
+
execFileSync("tar", ["-xf", "-", "-C", scratch], { input: tar });
|
|
48
|
+
return consume(fs.realpathSync(scratch), revision, "committed");
|
|
49
|
+
} finally {
|
|
50
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function inventory(root) {
|
|
54
|
+
const files = {};
|
|
55
|
+
function walk(dir) {
|
|
56
|
+
for (const entry of fs
|
|
57
|
+
.readdirSync(path.join(root, dir), { withFileTypes: true })
|
|
58
|
+
.sort((a, b) => a.name.localeCompare(b.name, "en"))) {
|
|
59
|
+
const ref = dir ? `${dir}/${entry.name}` : entry.name;
|
|
60
|
+
if (entry.isDirectory()) walk(ref);
|
|
61
|
+
else {
|
|
62
|
+
ensure(entry.isFile(), `核心不允许链接: ${ref}`);
|
|
63
|
+
files[ref] = {
|
|
64
|
+
digest: hash(fs.readFileSync(path.join(root, ref))),
|
|
65
|
+
mode: fs.statSync(path.join(root, ref)).mode & 0o111 ? 0o755 : 0o644,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
walk("");
|
|
71
|
+
return Object.fromEntries(
|
|
72
|
+
Object.entries(files).sort(([a], [b]) => (a < b ? -1 : 1)),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
export function verifyCore(packageRoot) {
|
|
76
|
+
const lock = readJson(packageRoot, "cli-core.lock.json");
|
|
77
|
+
ensure(
|
|
78
|
+
lock.schemaVersion === 1 &&
|
|
79
|
+
lock.protocolVersion === 1 &&
|
|
80
|
+
["committed", "working-tree"].includes(lock.sourceState || "committed") &&
|
|
81
|
+
/^[a-f0-9]{40}$/.test(lock.sourceRevision),
|
|
82
|
+
"核心锁格式错误",
|
|
83
|
+
);
|
|
84
|
+
const files = inventory(path.join(packageRoot, "vendor/cli-core"));
|
|
85
|
+
ensure(
|
|
86
|
+
JSON.stringify(files) === JSON.stringify(lock.files) &&
|
|
87
|
+
hash(JSON.stringify(files)) === lock.digest,
|
|
88
|
+
"vendor/cli-core 漂移",
|
|
89
|
+
);
|
|
90
|
+
ensure(
|
|
91
|
+
readJson(packageRoot, "vendor/cli-core/package.json").version ===
|
|
92
|
+
lock.coreVersion,
|
|
93
|
+
"核心版本漂移",
|
|
94
|
+
);
|
|
95
|
+
if (lock.upgradeResources) ensure(JSON.stringify(inventory(path.join(packageRoot,'resources/upgrade-skill'))) === JSON.stringify(lock.upgradeResources), '升级技能资源漂移');
|
|
96
|
+
return lock;
|
|
97
|
+
}
|
|
98
|
+
export function syncCore(source, ref, packageRoot, check = false) {
|
|
99
|
+
const paths = [CORE_PATH];
|
|
100
|
+
for (const candidate of [UPGRADE_SKILL, UPGRADE_PROTOCOL]) {
|
|
101
|
+
if (ref === 'WORKTREE') { if (fs.existsSync(path.join(source,candidate))) paths.push(candidate); }
|
|
102
|
+
else { try { execFileSync('git',['-C',source,'cat-file','-e',`${ref}:${candidate}`],{stdio:'ignore'}); paths.push(candidate); } catch {} }
|
|
103
|
+
}
|
|
104
|
+
return archive(source, ref, (root, revision, sourceState) => {
|
|
105
|
+
const sourceRoot = path.join(root, CORE_PATH),
|
|
106
|
+
files = inventory(sourceRoot),
|
|
107
|
+
coreVersion = readJson(sourceRoot, "package.json").version;
|
|
108
|
+
const resources = upgradeResources(root);
|
|
109
|
+
const resourceInventory = Object.fromEntries(Object.entries(resources).sort(([a],[b])=>a<b?-1:1).map(([ref,bytes])=>[ref,{digest:hash(bytes),mode:0o644}]));
|
|
110
|
+
const lock = {
|
|
111
|
+
schemaVersion: 1,
|
|
112
|
+
sourceRepository:
|
|
113
|
+
"https://github.com/iloveZzz/yss-spec-project-template.git",
|
|
114
|
+
sourceRevision: revision,
|
|
115
|
+
sourceState,
|
|
116
|
+
sourcePath: CORE_PATH,
|
|
117
|
+
coreVersion,
|
|
118
|
+
protocolVersion: 1,
|
|
119
|
+
files,
|
|
120
|
+
digest: hash(JSON.stringify(files)),
|
|
121
|
+
...(Object.keys(resources).length ? {upgradeResources:resourceInventory} : {}),
|
|
122
|
+
};
|
|
123
|
+
if (check) {
|
|
124
|
+
ensure(
|
|
125
|
+
json(readJson(packageRoot, "cli-core.lock.json")) === json(lock),
|
|
126
|
+
"核心来源锁漂移",
|
|
127
|
+
);
|
|
128
|
+
verifyCore(packageRoot);
|
|
129
|
+
return lock;
|
|
130
|
+
}
|
|
131
|
+
fs.rmSync(path.join(packageRoot, "vendor/cli-core"), {
|
|
132
|
+
recursive: true,
|
|
133
|
+
force: true,
|
|
134
|
+
});
|
|
135
|
+
for (const [ref, entry] of Object.entries(files))
|
|
136
|
+
write(
|
|
137
|
+
packageRoot,
|
|
138
|
+
`vendor/cli-core/${ref}`,
|
|
139
|
+
fs.readFileSync(path.join(sourceRoot, ref)),
|
|
140
|
+
entry.mode,
|
|
141
|
+
);
|
|
142
|
+
write(packageRoot, "cli-core.lock.json", json(lock));
|
|
143
|
+
if (Object.keys(resources).length) {
|
|
144
|
+
fs.rmSync(path.join(packageRoot,'resources/upgrade-skill'),{recursive:true,force:true});
|
|
145
|
+
for (const [ref,bytes] of Object.entries(resources)) write(packageRoot,`resources/upgrade-skill/${ref}`,bytes);
|
|
146
|
+
}
|
|
147
|
+
return lock;
|
|
148
|
+
}, paths);
|
|
149
|
+
}
|
|
150
|
+
function selected(ref, m) {
|
|
151
|
+
const top = ref.split("/")[0],
|
|
152
|
+
prefix = (p, base) => p === base || p.startsWith(base + "/");
|
|
153
|
+
return (
|
|
154
|
+
((m.allowRootEntries || []).includes(top) ||
|
|
155
|
+
(m.allowRootFiles || []).includes(ref) ||
|
|
156
|
+
(m.allowFiles || []).includes(ref)) &&
|
|
157
|
+
![
|
|
158
|
+
...(m.excludeRootEntries || []),
|
|
159
|
+
...(m.initExcludeRootEntries || []),
|
|
160
|
+
].includes(top) &&
|
|
161
|
+
![
|
|
162
|
+
...(m.excludeRootFiles || []),
|
|
163
|
+
...(m.initExcludeRootFiles || []),
|
|
164
|
+
].includes(ref) &&
|
|
165
|
+
![...(m.excludePaths || []), ...(m.initExcludePaths || [])].some((x) =>
|
|
166
|
+
prefix(ref, x),
|
|
167
|
+
) &&
|
|
168
|
+
ref !== "scripts/instantiate-harness"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
export function syncTemplate(source, ref, packageRoot, check = false) {
|
|
172
|
+
return archive(source, ref, (root, revision, sourceState) => {
|
|
173
|
+
const profile = yaml(fs.readFileSync(path.join(root, PROFILE))),
|
|
174
|
+
identity = yaml(fs.readFileSync(path.join(root, "yss-project.yaml")));
|
|
175
|
+
ensure(
|
|
176
|
+
identity.schema_version === 1 &&
|
|
177
|
+
identity.repository_mode === "template-source",
|
|
178
|
+
"来源不是 template-source",
|
|
179
|
+
);
|
|
180
|
+
const side = profile.profile_id === 'harness.business-ddd-strategy-handoff' ? 'design' : profile.profile_id?.match(/^harness\.(backend|frontend)-delivery$/)?.[1];
|
|
181
|
+
const family = familyFor(side);
|
|
182
|
+
ensure(readJson(packageRoot,'package.json').name === family.packageName, '来源 profile 与薄包身份不一致');
|
|
183
|
+
let manifestBytes;
|
|
184
|
+
if (side === 'design') {
|
|
185
|
+
const raw = yaml(fs.readFileSync(safe(root,'.template-spec/process/instance-distribution-manifest.yaml')));
|
|
186
|
+
ensure(raw.profile_id === family.profileId && raw.cli_package === family.packageName && raw.template_source === family.templateSource, '战略分发清单身份矛盾');
|
|
187
|
+
const manifest = {profileId:raw.profile_id};
|
|
188
|
+
for (const key of ['allow_root_entries','allow_root_files','allow_files','exclude_root_entries','exclude_root_files','exclude_paths','render_paths','init_exclude_root_entries','init_exclude_root_files','init_exclude_paths','instance_forbidden_paths']) manifest[key.replace(/_([a-z])/g,(_,c)=>c.toUpperCase())]=raw[key] || [];
|
|
189
|
+
manifestBytes = Buffer.from(json(manifest));
|
|
190
|
+
} else {
|
|
191
|
+
ensure(profile.instantiation?.cli_package === family.packageName && profile.instantiation?.metadata_file === family.metadataFile && profile.instantiation?.template_source === family.templateSource, '来源实例化合同矛盾');
|
|
192
|
+
manifestBytes = fs.readFileSync(safe(root, profile.instantiation.distribution_manifest));
|
|
193
|
+
}
|
|
194
|
+
const manifest = JSON.parse(manifestBytes),
|
|
195
|
+
entries = {},
|
|
196
|
+
blobs = new Map(),
|
|
197
|
+
canonical = path.join(root, ".agents/skills"),
|
|
198
|
+
captured = new Map();
|
|
199
|
+
function capture(physical, logical, ancestors = []) {
|
|
200
|
+
relative(logical);
|
|
201
|
+
const s = fs.lstatSync(physical);
|
|
202
|
+
if (s.isSymbolicLink()) {
|
|
203
|
+
const link = fs.readlinkSync(physical),
|
|
204
|
+
resolved = fs.realpathSync(physical);
|
|
205
|
+
ensure(
|
|
206
|
+
!path.isAbsolute(link) &&
|
|
207
|
+
resolved.startsWith(canonical + path.sep) &&
|
|
208
|
+
/^\.(agents|claude|codex|cursor|pi|qoder|trae)\/skills\//.test(
|
|
209
|
+
logical,
|
|
210
|
+
),
|
|
211
|
+
`不允许的投影链接: ${logical}`,
|
|
212
|
+
);
|
|
213
|
+
ensure(!ancestors.includes(resolved), "投影链接循环");
|
|
214
|
+
return capture(resolved, logical, [...ancestors, resolved]);
|
|
215
|
+
}
|
|
216
|
+
if (s.isDirectory()) {
|
|
217
|
+
for (const name of fs.readdirSync(physical).sort()) {
|
|
218
|
+
if (name === ".DS_Store") continue;
|
|
219
|
+
capture(path.join(physical, name), `${logical}/${name}`, ancestors);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (!selected(logical, manifest)) return;
|
|
224
|
+
governance(logical);
|
|
225
|
+
ensure(s.isFile(), `未知分发类型: ${logical}`);
|
|
226
|
+
let content = captured.get(physical);
|
|
227
|
+
if (!content) {
|
|
228
|
+
const bytes = fs.readFileSync(physical);
|
|
229
|
+
content = { bytes, digest: hash(bytes) };
|
|
230
|
+
captured.set(physical, content);
|
|
231
|
+
}
|
|
232
|
+
const { bytes, digest } = content;
|
|
233
|
+
entries[logical] = {
|
|
234
|
+
type: "file",
|
|
235
|
+
digest,
|
|
236
|
+
mode: s.mode & 0o111 ? 0o755 : 0o644,
|
|
237
|
+
blob: `blobs/${digest}`,
|
|
238
|
+
};
|
|
239
|
+
blobs.set(digest, bytes);
|
|
240
|
+
}
|
|
241
|
+
for (const name of fs.readdirSync(root).sort())
|
|
242
|
+
if (selected(name, manifest)) capture(path.join(root, name), name);
|
|
243
|
+
// Explicit allowFiles may lie below an otherwise excluded directory.
|
|
244
|
+
for (const ref of manifest.allowFiles || [])
|
|
245
|
+
if (fs.existsSync(safe(root, ref))) capture(safe(root, ref), ref);
|
|
246
|
+
const files = Object.fromEntries(
|
|
247
|
+
Object.entries(entries).sort(([a], [b]) => (a < b ? -1 : 1)),
|
|
248
|
+
);
|
|
249
|
+
let previous = null;
|
|
250
|
+
try {
|
|
251
|
+
previous = process.env.YSS_TEMPLATE_BASELINE_SNAPSHOT
|
|
252
|
+
? JSON.parse(fs.readFileSync(process.env.YSS_TEMPLATE_BASELINE_SNAPSHOT, "utf8"))
|
|
253
|
+
: readJson(packageRoot, "template.snapshot.json");
|
|
254
|
+
} catch {}
|
|
255
|
+
const retiredFiles = Object.fromEntries(
|
|
256
|
+
Object.entries({...(previous?.retiredFiles || {}), ...(previous?.files || {})})
|
|
257
|
+
.filter(([ref]) => !Object.hasOwn(files, ref))
|
|
258
|
+
.map(([ref, item]) => [ref, {type:"file", digest:item.digest, mode:item.mode}])
|
|
259
|
+
.sort(([a], [b]) => (a < b ? -1 : 1)),
|
|
260
|
+
);
|
|
261
|
+
const transitionBaselines = Object.fromEntries(
|
|
262
|
+
Object.entries({...(previous?.files || {}), ...(previous?.transitionBaselines || {})})
|
|
263
|
+
.filter(([ref, item]) => Object.hasOwn(files, ref) && item.digest !== files[ref].digest)
|
|
264
|
+
.map(([ref, item]) => [ref, {type:"file", digest:item.digest, mode:item.mode}])
|
|
265
|
+
.sort(([a], [b]) => (a < b ? -1 : 1)),
|
|
266
|
+
);
|
|
267
|
+
const snapshot = {
|
|
268
|
+
schemaVersion: 1,
|
|
269
|
+
...family,
|
|
270
|
+
templateCommit: revision,
|
|
271
|
+
sourceState,
|
|
272
|
+
manifestHash: hash(manifestBytes),
|
|
273
|
+
files,
|
|
274
|
+
retiredFiles,
|
|
275
|
+
retiredFilesHash: hash(JSON.stringify(retiredFiles)),
|
|
276
|
+
transitionBaselines,
|
|
277
|
+
transitionBaselinesHash: hash(JSON.stringify(transitionBaselines)),
|
|
278
|
+
snapshotHash: hash(JSON.stringify(files)),
|
|
279
|
+
projectionRepresentation: "materialized-files",
|
|
280
|
+
pathEncoding: "sha256-blobs-v1",
|
|
281
|
+
};
|
|
282
|
+
if (check) {
|
|
283
|
+
ensure(
|
|
284
|
+
json(readJson(packageRoot, "template.snapshot.json")) ===
|
|
285
|
+
json(snapshot),
|
|
286
|
+
"模板来源快照漂移",
|
|
287
|
+
);
|
|
288
|
+
loadBundle(packageRoot);
|
|
289
|
+
return snapshot;
|
|
290
|
+
}
|
|
291
|
+
fs.rmSync(path.join(packageRoot, "template"), {
|
|
292
|
+
recursive: true,
|
|
293
|
+
force: true,
|
|
294
|
+
});
|
|
295
|
+
for (const [digest, bytes] of blobs)
|
|
296
|
+
write(packageRoot, `template/blobs/${digest}`, bytes);
|
|
297
|
+
write(packageRoot, "template.manifest.json", manifestBytes);
|
|
298
|
+
write(packageRoot, "template.snapshot.json", json(snapshot));
|
|
299
|
+
write(packageRoot, "config/family.json", json(family));
|
|
300
|
+
return snapshot;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { parseDocument } from "./vendor/yaml.mjs";
|
|
3
|
+
import { ensure, hash, readJson, safe, governance } from "./io.mjs";
|
|
4
|
+
import { familyFor, CHECKS } from './family.mjs';
|
|
5
|
+
export const PROFILE = ".template-spec/process/harness-profile.yaml";
|
|
6
|
+
export function yaml(bytes) {
|
|
7
|
+
const doc = parseDocument(String(bytes), { uniqueKeys: true });
|
|
8
|
+
ensure(!doc.errors.length, "YAML 不合法");
|
|
9
|
+
const data = doc.toJS({ maxAliasCount: 0 });
|
|
10
|
+
ensure(
|
|
11
|
+
data && typeof data === "object" && !Array.isArray(data),
|
|
12
|
+
"YAML 必须是对象",
|
|
13
|
+
);
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
16
|
+
export function loadBundle(root) {
|
|
17
|
+
const pkg = readJson(root, "package.json"),
|
|
18
|
+
family = readJson(root, "config/family.json"),
|
|
19
|
+
snapshot = readJson(root, "template.snapshot.json"),
|
|
20
|
+
core = readJson(root, "cli-core.lock.json");
|
|
21
|
+
const expected = familyFor(family.side);
|
|
22
|
+
for (const [key, value] of Object.entries(expected))
|
|
23
|
+
ensure(
|
|
24
|
+
family[key] === value && snapshot[key] === value,
|
|
25
|
+
`包内身份不一致: ${key}`,
|
|
26
|
+
"IDENTITY",
|
|
27
|
+
);
|
|
28
|
+
ensure(
|
|
29
|
+
pkg.name === family.packageName && snapshot.schemaVersion === 1,
|
|
30
|
+
"包身份或快照 schema 不支持",
|
|
31
|
+
"IDENTITY",
|
|
32
|
+
);
|
|
33
|
+
ensure(
|
|
34
|
+
/^[0-9a-f]{40}$/.test(snapshot.templateCommit) &&
|
|
35
|
+
["committed", "working-tree"].includes(snapshot.sourceState || "committed") &&
|
|
36
|
+
hash(JSON.stringify(snapshot.files)) === snapshot.snapshotHash,
|
|
37
|
+
"快照摘要或 revision 不一致",
|
|
38
|
+
"BUNDLE",
|
|
39
|
+
);
|
|
40
|
+
ensure(
|
|
41
|
+
hash(fs.readFileSync(safe(root, "template.manifest.json"))) ===
|
|
42
|
+
snapshot.manifestHash,
|
|
43
|
+
"分发清单摘要不一致",
|
|
44
|
+
"BUNDLE",
|
|
45
|
+
);
|
|
46
|
+
const retiredFiles = snapshot.retiredFiles || {};
|
|
47
|
+
ensure(
|
|
48
|
+
(!snapshot.retiredFilesHash && !Object.keys(retiredFiles).length) ||
|
|
49
|
+
hash(JSON.stringify(retiredFiles)) === snapshot.retiredFilesHash,
|
|
50
|
+
"退出分发基线摘要不一致",
|
|
51
|
+
"BUNDLE",
|
|
52
|
+
);
|
|
53
|
+
for (const [ref, item] of Object.entries(retiredFiles)) {
|
|
54
|
+
governance(ref);
|
|
55
|
+
ensure(
|
|
56
|
+
item.type === "file" &&
|
|
57
|
+
[0o644, 0o755].includes(item.mode) &&
|
|
58
|
+
/^[0-9a-f]{64}$/.test(item.digest),
|
|
59
|
+
`退出分发基线不合法: ${ref}`,
|
|
60
|
+
"BUNDLE",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const transitionBaselines = snapshot.transitionBaselines || {};
|
|
64
|
+
ensure(
|
|
65
|
+
(!snapshot.transitionBaselinesHash && !Object.keys(transitionBaselines).length) ||
|
|
66
|
+
hash(JSON.stringify(transitionBaselines)) === snapshot.transitionBaselinesHash,
|
|
67
|
+
"受管范围迁移基线摘要不一致",
|
|
68
|
+
"BUNDLE",
|
|
69
|
+
);
|
|
70
|
+
for (const [ref, item] of Object.entries(transitionBaselines)) {
|
|
71
|
+
governance(ref);
|
|
72
|
+
ensure(
|
|
73
|
+
snapshot.files[ref] && item.type === "file" &&
|
|
74
|
+
[0o644, 0o755].includes(item.mode) &&
|
|
75
|
+
/^[0-9a-f]{64}$/.test(item.digest),
|
|
76
|
+
`受管范围迁移基线不合法: ${ref}`,
|
|
77
|
+
"BUNDLE",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
ensure(
|
|
81
|
+
core.schemaVersion === 1 &&
|
|
82
|
+
core.protocolVersion === 1 &&
|
|
83
|
+
["committed", "working-tree"].includes(core.sourceState || "committed") &&
|
|
84
|
+
/^[0-9a-f]{40}$/.test(core.sourceRevision) &&
|
|
85
|
+
/^[0-9a-f]{64}$/.test(core.digest),
|
|
86
|
+
"核心锁不合法",
|
|
87
|
+
"BUNDLE",
|
|
88
|
+
);
|
|
89
|
+
if (core.coreVersion === '0.2.0') ensure(core.files && Object.keys(core.files).length > 0, '核心文件清单缺失', 'BUNDLE');
|
|
90
|
+
for (const [ref, item] of Object.entries(core.files || {})) {
|
|
91
|
+
const file=safe(root, `vendor/cli-core/${ref}`);
|
|
92
|
+
ensure(hash(fs.readFileSync(file))===item.digest && (fs.statSync(file).mode & 0o111 ? 0o755 : 0o644)===item.mode, `核心文件漂移: ${ref}`, 'BUNDLE');
|
|
93
|
+
}
|
|
94
|
+
if (Object.keys(core.files || {}).length) ensure(hash(JSON.stringify(core.files))===core.digest, '核心清单摘要不一致', 'BUNDLE');
|
|
95
|
+
const manifest = readJson(root, "template.manifest.json");
|
|
96
|
+
const files = new Map();
|
|
97
|
+
const blobs = new Map();
|
|
98
|
+
for (const [ref, item] of Object.entries(snapshot.files)) {
|
|
99
|
+
governance(ref);
|
|
100
|
+
ensure(!(manifest.instanceForbiddenPaths || []).some(p=>ref===p || ref.startsWith(p+"/")), `快照含家族禁止分发路径: ${ref}`, "BUNDLE");
|
|
101
|
+
ensure(
|
|
102
|
+
item.type === "file" &&
|
|
103
|
+
[0o644, 0o755].includes(item.mode) &&
|
|
104
|
+
item.blob === `blobs/${item.digest}` &&
|
|
105
|
+
/^[0-9a-f]{64}$/.test(item.digest),
|
|
106
|
+
`快照类型/编码非法: ${ref}`,
|
|
107
|
+
"BUNDLE",
|
|
108
|
+
);
|
|
109
|
+
let bytes = blobs.get(item.digest);
|
|
110
|
+
if (!bytes) {
|
|
111
|
+
bytes = fs.readFileSync(safe(root, `template/${item.blob}`));
|
|
112
|
+
ensure(hash(bytes) === item.digest, `快照内容摘要不一致: ${ref}`, "BUNDLE");
|
|
113
|
+
blobs.set(item.digest, bytes);
|
|
114
|
+
}
|
|
115
|
+
files.set(ref, {
|
|
116
|
+
bytes: Buffer.from(bytes),
|
|
117
|
+
baseline: { type: "file", digest: item.digest, mode: item.mode },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
for (const ref of [
|
|
121
|
+
"README.md",
|
|
122
|
+
"AGENTS.md",
|
|
123
|
+
"CONTEXT.md",
|
|
124
|
+
"yss-project.yaml",
|
|
125
|
+
PROFILE,
|
|
126
|
+
])
|
|
127
|
+
ensure(files.has(ref), `缺少必需合同: ${ref}`, "BUNDLE");
|
|
128
|
+
const profile = yaml(files.get(PROFILE).bytes),
|
|
129
|
+
identity = yaml(files.get("yss-project.yaml").bytes);
|
|
130
|
+
ensure(
|
|
131
|
+
identity.schema_version === 1 &&
|
|
132
|
+
identity.repository_mode === "template-source",
|
|
133
|
+
"包内不是模板源",
|
|
134
|
+
"IDENTITY",
|
|
135
|
+
);
|
|
136
|
+
ensure(
|
|
137
|
+
[1, 2].includes(profile.schema_version) &&
|
|
138
|
+
profile.profile_id === family.profileId &&
|
|
139
|
+
(family.side === "design" || (profile.instantiation?.cli_package === family.packageName &&
|
|
140
|
+
profile.instantiation?.metadata_file === family.metadataFile &&
|
|
141
|
+
profile.instantiation?.template_source === family.templateSource)),
|
|
142
|
+
"模板 profile 与包身份矛盾",
|
|
143
|
+
"IDENTITY",
|
|
144
|
+
);
|
|
145
|
+
if (files.has("skills-lock.json")) for (const [, ref] of CHECKS) ensure(files.has(ref), `缺少实例校验器: ${ref}`, "BUNDLE");
|
|
146
|
+
return {
|
|
147
|
+
root,
|
|
148
|
+
pkg,
|
|
149
|
+
family,
|
|
150
|
+
snapshot,
|
|
151
|
+
core,
|
|
152
|
+
manifest,
|
|
153
|
+
retiredFiles,
|
|
154
|
+
transitionBaselines,
|
|
155
|
+
files,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export function render(bundle, variables) {
|
|
159
|
+
const result = new Map();
|
|
160
|
+
for (const [ref, item] of bundle.files) {
|
|
161
|
+
if (
|
|
162
|
+
!variables.includeExampleDocs &&
|
|
163
|
+
bundle.manifest.exampleDocPaths?.some(
|
|
164
|
+
(x) => ref === x || ref.startsWith(x + "/"),
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
continue;
|
|
168
|
+
let bytes = item.bytes;
|
|
169
|
+
if (ref === "yss-project.yaml")
|
|
170
|
+
bytes = Buffer.from(
|
|
171
|
+
"schema_version: 1\nrepository_mode: project-instance\n",
|
|
172
|
+
);
|
|
173
|
+
else if (ref === "README.md")
|
|
174
|
+
bytes = Buffer.from(
|
|
175
|
+
`# ${variables.projectName}\n\n本仓是 \`${bundle.family.profileId}\` 的 \`project-instance\`。\n\n先读 [AGENTS.md](AGENTS.md)、[CONTEXT.md](CONTEXT.md) 与 [profile](.template-spec/process/harness-profile.yaml)。\n\n业务领域:${variables.businessDomain}\n团队规模:${variables.teamSize}\n`,
|
|
176
|
+
);
|
|
177
|
+
if (bundle.family.side === "design" && ref === "AGENTS.md")
|
|
178
|
+
bytes = Buffer.from(bytes.toString().replace("**项目名称:** [填写]", () => `**项目名称:** ${variables.projectName}`).replace("**业务领域:** [填写]", () => `**业务领域:** ${variables.businessDomain}`).replace("**团队规模:** [填写]", () => `**团队规模:** ${variables.teamSize}`));
|
|
179
|
+
if (ref === ".template-spec/agents/issue-tracker.md")
|
|
180
|
+
bytes = Buffer.from(
|
|
181
|
+
bytes
|
|
182
|
+
.toString()
|
|
183
|
+
.replace(
|
|
184
|
+
"platform: local-markdown",
|
|
185
|
+
`platform: ${variables.issueTracker}`,
|
|
186
|
+
)
|
|
187
|
+
.replace(
|
|
188
|
+
"| `platform` | `local-markdown` |",
|
|
189
|
+
`| \`platform\` | \`${variables.issueTracker}\` |`,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
result.set(ref, {
|
|
193
|
+
bytes,
|
|
194
|
+
baseline: { ...item.baseline, digest: hash(bytes) },
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { runMigration } from "./migration-adapter.mjs";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { doctor } from "./verification.mjs";
|
|
4
|
+
import { inspectState, recoveryPreview, recover } from "./transaction.mjs";
|
|
5
|
+
import { identity, recoveryIdentity } from "./identity.mjs";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { loadBundle } from "./bundle.mjs";
|
|
9
|
+
import { update } from "./update.mjs";
|
|
10
|
+
import { execute } from "./engine.mjs";
|
|
11
|
+
import { ensure, targetPath, stat, write, json } from "./io.mjs";
|
|
12
|
+
export function parse(argv) {
|
|
13
|
+
const args = [...argv],
|
|
14
|
+
options = {
|
|
15
|
+
command: "init",
|
|
16
|
+
projectName: "",
|
|
17
|
+
businessDomain: "未指定",
|
|
18
|
+
teamSize: "未指定",
|
|
19
|
+
issueTracker: "local-markdown",
|
|
20
|
+
includeExampleDocs: true,
|
|
21
|
+
};
|
|
22
|
+
if (args[0] && !args[0].startsWith("-")) options.command = args.shift();
|
|
23
|
+
const values = {
|
|
24
|
+
"--target-dir": "targetDir",
|
|
25
|
+
"--project-name": "projectName",
|
|
26
|
+
"--business-domain": "businessDomain",
|
|
27
|
+
"--team-size": "teamSize",
|
|
28
|
+
"--issue-tracker": "issueTracker",
|
|
29
|
+
};
|
|
30
|
+
const flags = {
|
|
31
|
+
"--plan": "plan",
|
|
32
|
+
"--prune": "prune",
|
|
33
|
+
"--migrate-layout": "migrateLayout",
|
|
34
|
+
"--apply": "apply",
|
|
35
|
+
"--dry-run": "dryRun",
|
|
36
|
+
"--force": "force",
|
|
37
|
+
"--json": "json",
|
|
38
|
+
"--git-init": "gitInit",
|
|
39
|
+
"--help": "help",
|
|
40
|
+
"-h": "help",
|
|
41
|
+
"--version": "version",
|
|
42
|
+
"-v": "version",
|
|
43
|
+
"--include-example-docs": "includeExampleDocs",
|
|
44
|
+
};
|
|
45
|
+
while (args.length) {
|
|
46
|
+
const flag = args.shift();
|
|
47
|
+
if (values[flag]) {
|
|
48
|
+
ensure(args.length && !args[0].startsWith("-"), `${flag} 缺少值`);
|
|
49
|
+
options[values[flag]] = args.shift();
|
|
50
|
+
} else if (flag === "--no-example-docs") options.includeExampleDocs = false;
|
|
51
|
+
else if (flags[flag]) options[flags[flag]] = true;
|
|
52
|
+
else throw new Error(`未知参数: ${flag}`);
|
|
53
|
+
}
|
|
54
|
+
ensure(
|
|
55
|
+
["init", "attach", "sync", "diff", "doctor", "recover", "update", "upgrade"].includes(options.command),
|
|
56
|
+
"未知命令",
|
|
57
|
+
);
|
|
58
|
+
ensure(
|
|
59
|
+
!(options.apply && (options.dryRun || options.plan)),
|
|
60
|
+
"--apply 与 --dry-run 不可同时使用",
|
|
61
|
+
);
|
|
62
|
+
ensure(
|
|
63
|
+
!options.gitInit || options.command === "init",
|
|
64
|
+
"--git-init 仅用于 init",
|
|
65
|
+
);
|
|
66
|
+
const accepted = {
|
|
67
|
+
init: ['projectName','businessDomain','teamSize','targetDir','issueTracker','gitInit','includeExampleDocs','dryRun'],
|
|
68
|
+
attach: ['projectName','businessDomain','teamSize','targetDir','issueTracker','includeExampleDocs','dryRun','apply','force','plan','migrateLayout'],
|
|
69
|
+
sync: ['targetDir','dryRun','apply','force','plan','prune','migrateLayout'],
|
|
70
|
+
diff: ['targetDir','dryRun'], doctor: ['targetDir'], recover: ['targetDir','dryRun','apply'],
|
|
71
|
+
update: ['dryRun','force'], upgrade: ['dryRun','force'],
|
|
72
|
+
};
|
|
73
|
+
if (!options.help && !options.version) for (const flag of argv.filter(x=>x.startsWith('-'))) {
|
|
74
|
+
const key = flag === '--no-example-docs' ? 'includeExampleDocs' : values[flag] || flags[flag];
|
|
75
|
+
ensure(['help','version','json'].includes(key) || accepted[options.command].includes(key), `${flag} 不适用于 ${options.command}`);
|
|
76
|
+
}
|
|
77
|
+
if (options.issueTracker === "local") options.issueTracker = "local-markdown";
|
|
78
|
+
ensure(
|
|
79
|
+
["local-markdown", "github", "gitlab"].includes(options.issueTracker),
|
|
80
|
+
"issue-tracker 必须为 local-markdown、github 或 gitlab",
|
|
81
|
+
);
|
|
82
|
+
for (const k of ["projectName", "businessDomain", "teamSize"])
|
|
83
|
+
ensure(!/[\x00-\x1f\x7f]/.test(options[k]), `${k} 不允许控制字符`);
|
|
84
|
+
return options;
|
|
85
|
+
}
|
|
86
|
+
export async function main(packageRoot, argv = process.argv.slice(2)) {
|
|
87
|
+
packageRoot = path.resolve(packageRoot);
|
|
88
|
+
const wantsJson = argv.includes("--json");
|
|
89
|
+
try {
|
|
90
|
+
if (argv[0] === "migrate") {
|
|
91
|
+
const result = runMigration(packageRoot, argv.slice(1));
|
|
92
|
+
process.stdout.write(json(result));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const opts = parse(argv);
|
|
96
|
+
const pkg = JSON.parse(
|
|
97
|
+
fs.readFileSync(path.join(packageRoot, "package.json")),
|
|
98
|
+
);
|
|
99
|
+
if (opts.help) {
|
|
100
|
+
const help = `${pkg.name} init|attach|sync|diff|doctor|recover|update|upgrade|migrate\nmigrate plan|apply|status|recover|rollback:固定计划、持久归档和最近升级回退。\n--target-dir <目录> --project-name <名称> --business-domain <领域> --team-size <规模>\n--issue-tracker <local-markdown|github|gitlab> --git-init --no-example-docs\nattach/sync 默认只预览,写入必须 --apply;冲突显式 --apply --force。\n--plan --prune(仅 sync) --migrate-layout(旧目录显式迁移) --dry-run --json --version\ndoctor/diff/recover 默认只读;recover --apply 恢复未完成事务。init 只接受空目录。\n`;
|
|
101
|
+
process.stdout.write(wantsJson ? json({schemaVersion:1,command:"help",status:"ok",help}) : help);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (opts.version) {
|
|
105
|
+
process.stdout.write(wantsJson ? json({schemaVersion:1,command:"version",status:"ok",packageName:pkg.name,cliVersion:pkg.version}) : pkg.version + "\n");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (["update", "upgrade"].includes(opts.command)) {
|
|
109
|
+
const result = update(packageRoot, pkg, opts);
|
|
110
|
+
process.stdout.write(
|
|
111
|
+
wantsJson
|
|
112
|
+
? json(result)
|
|
113
|
+
: `${result.status}: ${result.currentVersion} → ${result.latestVersion}\n${result.advice}\n`,
|
|
114
|
+
);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (pkg.name === 'create-yss-harness-design' && opts.command === 'init') {
|
|
118
|
+
const required = ['projectName','businessDomain','targetDir'];
|
|
119
|
+
const missing = required.filter(k=>!opts[k] || opts[k]==='未指定');
|
|
120
|
+
if (missing.length && process.stdin.isTTY && !wantsJson) {
|
|
121
|
+
const rl=createInterface({input:process.stdin,output:process.stderr});
|
|
122
|
+
try { for(const key of missing) opts[key]=(await rl.question(({projectName:'项目名称',businessDomain:'业务领域',targetDir:'目标目录'})[key]+': ')).trim(); }
|
|
123
|
+
finally { rl.close(); }
|
|
124
|
+
}
|
|
125
|
+
ensure(required.every(k=>opts[k] && opts[k]!=='未指定'),'init 需要 --project-name、--business-domain 和 --target-dir');
|
|
126
|
+
ensure(!required.some(k=>/[\x00-\x1f\x7f]/.test(opts[k])),'初始化参数含控制字符');
|
|
127
|
+
}
|
|
128
|
+
const bundle = loadBundle(packageRoot),
|
|
129
|
+
target = targetPath(opts.targetDir || process.cwd());
|
|
130
|
+
ensure(
|
|
131
|
+
target !== packageRoot &&
|
|
132
|
+
!target.startsWith(packageRoot + path.sep) &&
|
|
133
|
+
!packageRoot.startsWith(target + path.sep),
|
|
134
|
+
"目标不得覆盖 CLI 安装目录",
|
|
135
|
+
);
|
|
136
|
+
let result;
|
|
137
|
+
if (opts.command === 'doctor') result = doctor(bundle,target);
|
|
138
|
+
else if (opts.command === 'recover') {
|
|
139
|
+
ensure(stat(target)?.isDirectory(),'恢复目标不存在','PATH');
|
|
140
|
+
const state=inspectState(target,bundle.family);
|
|
141
|
+
if (!state.pending.length) { identity(target,bundle,'recover');result={schemaVersion:1,command:'recover',status:'preview',target,transactions:[],message:'没有未完成事务'}; }
|
|
142
|
+
else {
|
|
143
|
+
const validate=()=>recoveryIdentity(target,bundle,state);
|
|
144
|
+
result=opts.apply ? recover(target,bundle.family,state,validate) : recoveryPreview(target,bundle.family,state,validate);
|
|
145
|
+
result.command='recover';
|
|
146
|
+
}
|
|
147
|
+
} else result = execute(bundle, target, opts);
|
|
148
|
+
if (result.status === 'error') process.exitCode=1;
|
|
149
|
+
process.stdout.write(wantsJson ? json(result) : formatPlan(result));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (wantsJson)
|
|
152
|
+
process.stdout.write(
|
|
153
|
+
json({
|
|
154
|
+
schemaVersion: 1,
|
|
155
|
+
command: argv[0]?.startsWith("-") ? "init" : argv[0] || "init",
|
|
156
|
+
...error.result,
|
|
157
|
+
status: "error",
|
|
158
|
+
code: error.code || "INVALID",
|
|
159
|
+
message: error.message,
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
if (error.result && !wantsJson)
|
|
163
|
+
process.stdout.write(formatPlan(error.result));
|
|
164
|
+
process.stderr.write(error.message + "\n");
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatPlan(result) {
|
|
170
|
+
const lines = [
|
|
171
|
+
`${result.status}: ${result.target}`,
|
|
172
|
+
`CLI ${result.cliVersion || "-"} / core ${result.coreVersion || "-"} / template ${result.templateCommit || "-"}`,
|
|
173
|
+
];
|
|
174
|
+
for (const change of result.changes || [])
|
|
175
|
+
lines.push(`${change.action.padEnd(10)} ${change.path}`);
|
|
176
|
+
for (const check of result.checks || []) lines.push(`[${check.status}] ${check.name}: ${check.message || ''}`);
|
|
177
|
+
for (const transaction of result.transactions || []) lines.push(`恢复事务 ${transaction.id || transaction}: ${transaction.backupPath || ''}`);
|
|
178
|
+
if (result.advice) lines.push(result.advice);
|
|
179
|
+
if (result.backupPath) lines.push(`备份和恢复清单:${result.backupPath}`);
|
|
180
|
+
if (result.message) lines.push(result.message);
|
|
181
|
+
return lines.join("\n") + "\n";
|
|
182
|
+
}
|