create-yss-spec 3.1.0 → 3.1.3
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 +117 -91
- package/bin/create-yss-spec.js +11 -3
- package/package.json +9 -2
- package/src/api/_sync-service.js +191 -0
- package/src/api/index.js +22 -0
- package/src/api/project-diff.js +9 -0
- package/src/api/project-doctor.js +10 -0
- package/src/api/template-apply.js +10 -0
- package/src/api/template-plan.js +9 -0
- package/src/cli/args.js +60 -0
- package/src/cli/error-output.js +52 -0
- package/src/cli/flags.js +9 -0
- package/src/cli/help.js +95 -0
- package/src/cli/index.js +82 -0
- package/src/cli/plan-output.js +61 -0
- package/src/cli/prompts.js +91 -0
- package/src/cli/router.js +41 -0
- package/src/cli.js +5 -2253
- package/src/commands/attach.js +280 -0
- package/src/commands/diff.js +23 -0
- package/src/commands/doctor.js +417 -0
- package/src/commands/init.js +193 -0
- package/src/commands/sync.js +146 -0
- package/src/commands/update.js +14 -0
- package/src/contracts/apply-result-v1.json +45 -0
- package/src/contracts/customization-policy-v1.json +28 -0
- package/src/contracts/doctor-report-v1.json +29 -0
- package/src/contracts/error-envelope-v1.json +24 -0
- package/src/contracts/generator-policy-v1.json +24 -0
- package/src/contracts/ownership-policy-v1.json +40 -0
- package/src/contracts/plan-schema-v1.json +115 -0
- package/src/family-identity.js +365 -0
- package/src/family-runtime.js +23 -0
- package/src/filesystem/apply-plan.js +44 -0
- package/src/filesystem/copy-path.js +28 -0
- package/src/filesystem/path-utils.js +67 -0
- package/src/filesystem/transaction-runner.js +41 -0
- package/src/filesystem/transaction.js +153 -0
- package/src/git/submodule.js +115 -0
- package/src/git/worktree.js +69 -0
- package/src/template/attach-planner-runtime.js +51 -0
- package/src/template/attach-planner.js +160 -0
- package/src/template/instance-runtime.js +463 -0
- package/src/template/lifecycle-metadata.js +99 -0
- package/src/template/lifecycle-policy.js +129 -0
- package/src/template/lifecycle-runtime.js +90 -0
- package/src/template/migration-planner.js +95 -0
- package/src/template/migration-runtime.js +247 -0
- package/src/template/ownership-metadata.js +79 -0
- package/src/template/ownership-policy.js +149 -0
- package/src/template/ownership-runtime.js +109 -0
- package/src/template/plan-schema.js +36 -0
- package/src/template/sync-planner-runtime.js +57 -0
- package/src/template/sync-planner.js +206 -0
- package/src/template/verification-runtime.js +154 -0
- package/src/validation/identity.js +41 -0
- package/src/validation/metadata.js +105 -0
- package/src/validation/security.js +67 -0
- package/src/validation/snapshot.js +47 -0
- package/template/.agents/skills/.strategic-design-skills-manifest.json +1 -1
- package/template/.agents/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.agents/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.agents/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.agents/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.agents/skills/yss-ui/SKILL.md +2 -0
- package/template/.claude/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.claude/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.claude/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.claude/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.claude/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.claude/skills/yss-ui/SKILL.md +2 -0
- package/template/.codex/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.codex/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.codex/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.codex/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.codex/skills/yss-ui/SKILL.md +2 -0
- package/template/.cursor/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.cursor/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.cursor/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.cursor/skills/yss-ui/SKILL.md +2 -0
- package/template/.pi/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.pi/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.pi/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.pi/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.pi/skills/yss-ui/SKILL.md +2 -0
- package/template/.qoder/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.qoder/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.qoder/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.qoder/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.qoder/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.qoder/skills/yss-ui/SKILL.md +2 -0
- package/template/.trae/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.trae/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.trae/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.trae/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.trae/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.trae/skills/yss-ui/SKILL.md +2 -0
- package/template/CONTEXT.md +2 -0
- package/template/README.md +4 -0
- package/template/docs/agents/digital-human-roles.yaml +1 -0
- package/template/docs/process/frontend-backend-delivery.md +72 -0
- package/template/docs/process/implementation-repo-integration.md +2 -0
- package/template/docs/process/lifecycle-artifact-map.md +1 -0
- package/template/docs/process/lifecycle-registry-baseline.json +2 -1
- package/template/docs/process/lifecycle-registry.yaml +5 -0
- package/template/docs/process/schemas/backend-delivery-verification.schema.json +113 -0
- package/template/docs/process/schemas/backend-delivery.schema.json +301 -0
- package/template/docs/process/schemas/digital-human-task-package.schema.json +8 -0
- package/template/docs/process/schemas/frontend-delivery-acceptance.schema.json +200 -0
- package/template/docs/process/schemas/frontend-implementation-evidence.schema.json +3 -0
- package/template/docs/process/template-verification-profiles.yaml +6 -0
- package/template/docs/user-guide//346/210/230/346/234/257/350/256/276/350/256/241/345/255/220/351/241/271/347/233/256/347/224/250/346/210/267/346/211/213/345/206/214.md +20 -206
- package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214.md +63 -963
- package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214/347/264/242/345/274/225.md +11 -14
- package/template/docs/user-guide//350/256/276/345/244/207/345/200/237/347/224/250/350/264/257/347/251/277/346/241/210/344/276/213.md +127 -0
- package/template/scripts/backend-delivery +14 -0
- package/template/scripts/fixtures/backend-delivery/revision-server.mjs +11 -0
- package/template/scripts/instantiate-harness +69 -0
- package/template/scripts/lib/backend-delivery.mjs +164 -0
- package/template/scripts/lib/frontend-delivery-boundary.mjs +49 -0
- package/template/scripts/lib/frontend-delivery.mjs +73 -0
- package/template/scripts/lib/harness-execution-scope.mjs +52 -0
- package/template/scripts/lib/implementation-contract-compiler.mjs +12 -2
- package/template/scripts/lib/lifecycle-transition.mjs +7 -0
- package/template/scripts/lib/strategic-handoff-consumption.mjs +14 -7
- package/template/scripts/lib/strategic-handoff.mjs +1 -1
- package/template/scripts/lib/task-package.mjs +7 -0
- package/template/scripts/sync-strategic-handoff-tools +3 -1
- package/template/scripts/verify-frontend-delivery +9 -0
- package/template/scripts/verify-frontend-delivery-scenarios +131 -0
- package/template/scripts/verify-frontend-implementation-evidence +2 -0
- package/template/skills-lock.json +11 -11
- package/template.manifest.json +43 -1
- package/template.snapshot.json +5 -5
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const FAMILIES = [
|
|
7
|
+
{
|
|
8
|
+
name: "create-yss-spec",
|
|
9
|
+
metadata: ".yss-template.json",
|
|
10
|
+
template: "yss-spec-project-template",
|
|
11
|
+
profile: null,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "create-yss-harness-design",
|
|
15
|
+
metadata: ".yss-harness-design.json",
|
|
16
|
+
template: "yss-harness-design-agent",
|
|
17
|
+
profile: "harness.business-ddd-strategy-handoff",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "create-yss-harness-dev",
|
|
21
|
+
metadata: ".yss-harness-dev.json",
|
|
22
|
+
template: "yss-harness-dev-agent",
|
|
23
|
+
profile: "harness.dev-agent-slice",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "create-yss-harness-backend",
|
|
27
|
+
metadata: ".yss-harness-backend.json",
|
|
28
|
+
template: "yss-harness-backend-agent",
|
|
29
|
+
profile: "harness.backend-delivery",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "create-yss-harness-frontend",
|
|
33
|
+
metadata: ".yss-harness-frontend.json",
|
|
34
|
+
template: "yss-harness-frontend-agent",
|
|
35
|
+
profile: "harness.frontend-delivery",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const PROFILE_PATH = "docs/process/harness-profile.yaml";
|
|
40
|
+
|
|
41
|
+
function statOrMissing(file) {
|
|
42
|
+
try {
|
|
43
|
+
return fs.lstatSync(file);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error.code === "ENOENT") return null;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readIdentityFile(root, relative) {
|
|
51
|
+
let current = root;
|
|
52
|
+
const parts = relative.split("/");
|
|
53
|
+
for (const [index, part] of parts.entries()) {
|
|
54
|
+
current = path.join(current, part);
|
|
55
|
+
const stat = statOrMissing(current);
|
|
56
|
+
if (!stat) return null;
|
|
57
|
+
if (
|
|
58
|
+
stat.isSymbolicLink() ||
|
|
59
|
+
(index < parts.length - 1 ? !stat.isDirectory() : !stat.isFile())
|
|
60
|
+
) {
|
|
61
|
+
throw new Error(`身份路径不能是符号链接或特殊文件:${relative}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return fs.readFileSync(current, "utf8");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function topLevelJsonKeys(text) {
|
|
68
|
+
const keys = [];
|
|
69
|
+
let depth = 0;
|
|
70
|
+
let index = 0;
|
|
71
|
+
|
|
72
|
+
while (index < text.length) {
|
|
73
|
+
const char = text[index];
|
|
74
|
+
if (char === "{") {
|
|
75
|
+
depth += 1;
|
|
76
|
+
index += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (char === "}") {
|
|
80
|
+
depth -= 1;
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (char !== '"') {
|
|
85
|
+
index += 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const start = index;
|
|
90
|
+
index += 1;
|
|
91
|
+
let escaped = false;
|
|
92
|
+
while (index < text.length) {
|
|
93
|
+
const current = text[index];
|
|
94
|
+
if (escaped) {
|
|
95
|
+
escaped = false;
|
|
96
|
+
} else if (current === "\\") {
|
|
97
|
+
escaped = true;
|
|
98
|
+
} else if (current === '"') {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
index += 1;
|
|
102
|
+
}
|
|
103
|
+
if (index >= text.length) break;
|
|
104
|
+
|
|
105
|
+
const literal = text.slice(start, index + 1);
|
|
106
|
+
index += 1;
|
|
107
|
+
let cursor = index;
|
|
108
|
+
while (/\s/.test(text[cursor] || "")) cursor += 1;
|
|
109
|
+
if (depth === 1 && text[cursor] === ":") {
|
|
110
|
+
keys.push(JSON.parse(literal));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return keys;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseJsonObject(text, relative) {
|
|
118
|
+
let value;
|
|
119
|
+
try {
|
|
120
|
+
value = JSON.parse(text);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw new Error(`身份文件格式非法 ${relative}:${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
125
|
+
throw new Error(`身份文件格式非法 ${relative}:必须是对象`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
for (const key of topLevelJsonKeys(text)) {
|
|
130
|
+
if (seen.has(key)) {
|
|
131
|
+
throw new Error(`身份文件格式非法 ${relative}:重复字段 ${key}`);
|
|
132
|
+
}
|
|
133
|
+
seen.add(key);
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function stripYamlComment(line) {
|
|
139
|
+
let single = false;
|
|
140
|
+
let double = false;
|
|
141
|
+
let escaped = false;
|
|
142
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
143
|
+
const char = line[index];
|
|
144
|
+
if (double && escaped) {
|
|
145
|
+
escaped = false;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (double && char === "\\") {
|
|
149
|
+
escaped = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (!double && char === "'") single = !single;
|
|
153
|
+
else if (!single && char === '"') double = !double;
|
|
154
|
+
else if (!single && !double && char === "#") return line.slice(0, index);
|
|
155
|
+
}
|
|
156
|
+
return line;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseYamlScalar(raw, relative, lineNumber) {
|
|
160
|
+
const value = raw.trim();
|
|
161
|
+
if (!value) return undefined;
|
|
162
|
+
|
|
163
|
+
if (value.startsWith("'") || value.startsWith('"')) {
|
|
164
|
+
const quote = value[0];
|
|
165
|
+
if (!value.endsWith(quote) || value.length < 2) {
|
|
166
|
+
throw new Error(`身份文件格式非法 ${relative}:第 ${lineNumber} 行字符串未闭合`);
|
|
167
|
+
}
|
|
168
|
+
if (quote === "'") {
|
|
169
|
+
return value.slice(1, -1).replaceAll("''", "'");
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(value);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
throw new Error(`身份文件格式非法 ${relative}:第 ${lineNumber} 行 ${error.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (/^[+-]?\d+$/.test(value)) return Number(value);
|
|
179
|
+
if (value === "true") return true;
|
|
180
|
+
if (value === "false") return false;
|
|
181
|
+
if (value === "null" || value === "~") return null;
|
|
182
|
+
if (/^[\[\]{}|>&*!]/.test(value)) {
|
|
183
|
+
throw new Error(`身份文件格式非法 ${relative}:第 ${lineNumber} 行不支持复杂 YAML 值`);
|
|
184
|
+
}
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseProfileYaml(text, relative = PROFILE_PATH) {
|
|
189
|
+
const root = {};
|
|
190
|
+
const rootKeys = new Set();
|
|
191
|
+
const nestedKeys = new Set();
|
|
192
|
+
let section = null;
|
|
193
|
+
|
|
194
|
+
for (const [offset, rawLine] of String(text).split(/\r?\n/).entries()) {
|
|
195
|
+
const lineNumber = offset + 1;
|
|
196
|
+
if (rawLine.includes("\t")) {
|
|
197
|
+
throw new Error(`身份文件格式非法 ${relative}:第 ${lineNumber} 行不允许 Tab 缩进`);
|
|
198
|
+
}
|
|
199
|
+
const uncommented = stripYamlComment(rawLine).replace(/\s+$/, "");
|
|
200
|
+
if (!uncommented.trim()) continue;
|
|
201
|
+
|
|
202
|
+
const indent = uncommented.match(/^ */)[0].length;
|
|
203
|
+
if (![0, 2].includes(indent)) {
|
|
204
|
+
throw new Error(`身份文件格式非法 ${relative}:第 ${lineNumber} 行缩进非法`);
|
|
205
|
+
}
|
|
206
|
+
const content = uncommented.slice(indent);
|
|
207
|
+
const match = content.match(/^([A-Za-z_][A-Za-z0-9_-]*):(?:\s*(.*))?$/);
|
|
208
|
+
if (!match) {
|
|
209
|
+
throw new Error(`身份文件格式非法 ${relative}:第 ${lineNumber} 行格式非法`);
|
|
210
|
+
}
|
|
211
|
+
const [, key, rawValue = ""] = match;
|
|
212
|
+
|
|
213
|
+
if (indent === 0) {
|
|
214
|
+
section = null;
|
|
215
|
+
if (rootKeys.has(key)) {
|
|
216
|
+
throw new Error(`身份文件格式非法 ${relative}:重复字段 ${key}`);
|
|
217
|
+
}
|
|
218
|
+
rootKeys.add(key);
|
|
219
|
+
if (!rawValue.trim()) {
|
|
220
|
+
if (key !== "instantiation") {
|
|
221
|
+
throw new Error(`身份文件格式非法 ${relative}:第 ${lineNumber} 行缺少值`);
|
|
222
|
+
}
|
|
223
|
+
root[key] = {};
|
|
224
|
+
section = key;
|
|
225
|
+
} else {
|
|
226
|
+
root[key] = parseYamlScalar(rawValue, relative, lineNumber);
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (section !== "instantiation" || !root.instantiation) {
|
|
232
|
+
throw new Error(`身份文件格式非法 ${relative}:第 ${lineNumber} 行存在未知嵌套`);
|
|
233
|
+
}
|
|
234
|
+
if (nestedKeys.has(key)) {
|
|
235
|
+
throw new Error(`身份文件格式非法 ${relative}:instantiation 重复字段 ${key}`);
|
|
236
|
+
}
|
|
237
|
+
nestedKeys.add(key);
|
|
238
|
+
const parsed = parseYamlScalar(rawValue, relative, lineNumber);
|
|
239
|
+
if (parsed === undefined) {
|
|
240
|
+
throw new Error(`身份文件格式非法 ${relative}:第 ${lineNumber} 行缺少值`);
|
|
241
|
+
}
|
|
242
|
+
root.instantiation[key] = parsed;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return root;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function createFamilyGuard(_packageRoot, expectedName) {
|
|
249
|
+
const expected = FAMILIES.find(({ name }) => name === expectedName);
|
|
250
|
+
if (!expected) throw new Error(`未知 CLI 家族:${expectedName}`);
|
|
251
|
+
const source = `github:iloveZzz/${expected.template}`;
|
|
252
|
+
|
|
253
|
+
function checkFields(value, fields, relative) {
|
|
254
|
+
for (const [key, wanted] of Object.entries(fields)) {
|
|
255
|
+
if (
|
|
256
|
+
Object.hasOwn(value, key) &&
|
|
257
|
+
(wanted === null || value[key] !== wanted)
|
|
258
|
+
) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`身份不一致:${relative} 的 ${key} 与 ${expectedName} 不匹配;--force 不能绕过`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return function checkTargetFamily(targetDir, { snapshot } = {}) {
|
|
267
|
+
const present = FAMILIES.filter(({ metadata }) =>
|
|
268
|
+
statOrMissing(path.join(targetDir, metadata)),
|
|
269
|
+
);
|
|
270
|
+
if (present.length > 1) {
|
|
271
|
+
throw new Error("目标存在多个模板家族身份;请人工核对,CLI 不自动修复");
|
|
272
|
+
}
|
|
273
|
+
if (present.length && present[0].name !== expectedName) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`目标身份属于 ${present[0].name}(${present[0].metadata}),不能使用 ${expectedName};不支持跨家族迁移,--force 不能绕过`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (present.length) {
|
|
280
|
+
const metadataText = readIdentityFile(targetDir, expected.metadata);
|
|
281
|
+
const value = parseJsonObject(metadataText, expected.metadata);
|
|
282
|
+
if (
|
|
283
|
+
Object.hasOwn(value, "metadataSchemaVersion") &&
|
|
284
|
+
(!Number.isInteger(value.metadataSchemaVersion) ||
|
|
285
|
+
value.metadataSchemaVersion < 1 ||
|
|
286
|
+
value.metadataSchemaVersion > (expectedName === "create-yss-spec" ? 2 : 1))
|
|
287
|
+
) {
|
|
288
|
+
throw new Error("身份 metadata schema 版本非法或不支持");
|
|
289
|
+
}
|
|
290
|
+
const legacy =
|
|
291
|
+
value.metadataSchemaVersion === undefined ||
|
|
292
|
+
(expectedName === "create-yss-spec" && value.metadataSchemaVersion === 1);
|
|
293
|
+
checkFields(
|
|
294
|
+
value,
|
|
295
|
+
{
|
|
296
|
+
templateName: expectedName,
|
|
297
|
+
templateSource:
|
|
298
|
+
legacy && value.templateSource === "legacy-attach"
|
|
299
|
+
? "legacy-attach"
|
|
300
|
+
: source,
|
|
301
|
+
template_source: source,
|
|
302
|
+
profileId: expected.profile,
|
|
303
|
+
profile_id: expected.profile,
|
|
304
|
+
},
|
|
305
|
+
expected.metadata,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const profileText = readIdentityFile(targetDir, PROFILE_PATH);
|
|
310
|
+
if (profileText !== null) {
|
|
311
|
+
const profile = parseProfileYaml(profileText, PROFILE_PATH);
|
|
312
|
+
if (
|
|
313
|
+
profile.schema_version !== 1 ||
|
|
314
|
+
!FAMILIES.some(
|
|
315
|
+
(family) =>
|
|
316
|
+
family.profile !== null && family.profile === profile.profile_id,
|
|
317
|
+
)
|
|
318
|
+
) {
|
|
319
|
+
throw new Error(`未知或非法 profile:${PROFILE_PATH}`);
|
|
320
|
+
}
|
|
321
|
+
if (expected.profile === null || profile.profile_id !== expected.profile) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`profile 身份与 ${expectedName} 不一致;不支持跨家族迁移,--force 不能绕过`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (profile.instantiation !== undefined) {
|
|
327
|
+
if (
|
|
328
|
+
!profile.instantiation ||
|
|
329
|
+
typeof profile.instantiation !== "object" ||
|
|
330
|
+
Array.isArray(profile.instantiation)
|
|
331
|
+
) {
|
|
332
|
+
throw new Error(`profile 身份配置非法:${PROFILE_PATH}`);
|
|
333
|
+
}
|
|
334
|
+
checkFields(
|
|
335
|
+
profile.instantiation,
|
|
336
|
+
{
|
|
337
|
+
metadata_file: expected.metadata,
|
|
338
|
+
template_source: source,
|
|
339
|
+
cli_package: expectedName,
|
|
340
|
+
},
|
|
341
|
+
PROFILE_PATH,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (snapshot) {
|
|
347
|
+
if (
|
|
348
|
+
snapshot.templateName !== expected.template ||
|
|
349
|
+
snapshot.templateSource !== source ||
|
|
350
|
+
(expected.profile
|
|
351
|
+
? snapshot.profileId !== expected.profile || profileText === null
|
|
352
|
+
: Object.hasOwn(snapshot, "profileId"))
|
|
353
|
+
) {
|
|
354
|
+
throw new Error(`模板快照家族身份与 ${expectedName} 不一致`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
module.exports = {
|
|
361
|
+
FAMILIES,
|
|
362
|
+
PROFILE_PATH,
|
|
363
|
+
parseProfileYaml,
|
|
364
|
+
createFamilyGuard,
|
|
365
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const packageManifest = require("../package.json");
|
|
5
|
+
const { createFamilyGuard } = require("./family-identity");
|
|
6
|
+
|
|
7
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
8
|
+
const BUNDLED_TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "template");
|
|
9
|
+
const checkTargetFamily = createFamilyGuard(PACKAGE_ROOT, packageManifest.name);
|
|
10
|
+
|
|
11
|
+
function assertBundledFamily(snapshot) {
|
|
12
|
+
checkTargetFamily(BUNDLED_TEMPLATE_ROOT, { snapshot });
|
|
13
|
+
return snapshot;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function assertTargetFamily(targetDir) {
|
|
17
|
+
checkTargetFamily(path.resolve(targetDir));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
assertBundledFamily,
|
|
22
|
+
assertTargetFamily,
|
|
23
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const { targetPath } = require("./path-utils");
|
|
5
|
+
|
|
6
|
+
function applyMigrationOperations(migrationPlan, transaction) {
|
|
7
|
+
for (const operation of migrationPlan.operations || []) {
|
|
8
|
+
if (operation.kind === "remove") {
|
|
9
|
+
transaction.remove(targetPath(migrationPlan.targetDir, operation.path));
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (operation.kind === "move") {
|
|
13
|
+
transaction.move(
|
|
14
|
+
targetPath(migrationPlan.targetDir, operation.from),
|
|
15
|
+
targetPath(migrationPlan.targetDir, operation.to),
|
|
16
|
+
);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`不支持的迁移操作:${operation.kind}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function applyManagedOperation(operation, transaction) {
|
|
24
|
+
if (operation.type === "render") {
|
|
25
|
+
transaction.ensureParent(operation.targetPath);
|
|
26
|
+
fs.writeFileSync(operation.targetPath, operation.desiredContent);
|
|
27
|
+
fs.chmodSync(operation.targetPath, fs.statSync(operation.sourcePath).mode & 0o777);
|
|
28
|
+
transaction.mark(operation.targetPath);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (operation.type === "copy") {
|
|
33
|
+
transaction.copyFile(operation.sourcePath, operation.targetPath);
|
|
34
|
+
fs.chmodSync(operation.targetPath, fs.statSync(operation.sourcePath).mode & 0o777);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
throw new Error(`不支持的受管操作:${operation.type}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
applyMigrationOperations,
|
|
43
|
+
applyManagedOperation,
|
|
44
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { pathKind } = require("./path-utils");
|
|
6
|
+
|
|
7
|
+
function copyPath(sourcePath, destinationPath) {
|
|
8
|
+
const kind = pathKind(sourcePath);
|
|
9
|
+
if (kind === "other") {
|
|
10
|
+
throw new Error(`拒绝复制符号链接或特殊文件:${sourcePath}`);
|
|
11
|
+
}
|
|
12
|
+
if (kind === "missing") {
|
|
13
|
+
throw new Error(`复制源路径不存在:${sourcePath}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
17
|
+
if (kind === "directory") {
|
|
18
|
+
fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
23
|
+
fs.chmodSync(destinationPath, fs.statSync(sourcePath).mode & 0o777);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
copyPath,
|
|
28
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
function normalizeRelativePath(value) {
|
|
7
|
+
return String(value || "")
|
|
8
|
+
.replaceAll("\\", "/")
|
|
9
|
+
.replace(/^\.\//, "")
|
|
10
|
+
.replace(/\/+$/, "");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function pathKind(absolutePath) {
|
|
14
|
+
let stat;
|
|
15
|
+
try {
|
|
16
|
+
stat = fs.lstatSync(absolutePath);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error.code === "ENOENT") return "missing";
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
if (stat.isDirectory()) return "directory";
|
|
22
|
+
if (stat.isFile()) return "file";
|
|
23
|
+
return "other";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function targetPath(targetDir, relativePath) {
|
|
27
|
+
const root = path.resolve(targetDir);
|
|
28
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
29
|
+
const resolved = path.resolve(root, normalized);
|
|
30
|
+
const relative = path.relative(root, resolved);
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
relative === ".." ||
|
|
34
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
35
|
+
path.isAbsolute(relative)
|
|
36
|
+
) {
|
|
37
|
+
throw new Error(`模板路径越界:${relativePath}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (pathKind(root) === "directory") {
|
|
41
|
+
let current = root;
|
|
42
|
+
const segments = relative.split(path.sep).filter(Boolean);
|
|
43
|
+
segments.forEach((segment, index) => {
|
|
44
|
+
current = path.join(current, segment);
|
|
45
|
+
const kind = pathKind(current);
|
|
46
|
+
if (kind === "missing") return;
|
|
47
|
+
|
|
48
|
+
const stat = fs.lstatSync(current);
|
|
49
|
+
if (stat.isSymbolicLink() && index < segments.length - 1) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`模板目标路径包含中间符号链接,无法安全写入:${relativePath}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (index < segments.length - 1 && kind !== "directory") {
|
|
55
|
+
throw new Error(`模板目标父路径不是目录:${relativePath}`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return resolved;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
normalizeRelativePath,
|
|
65
|
+
targetPath,
|
|
66
|
+
pathKind,
|
|
67
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { FileTransaction } = require("./transaction");
|
|
4
|
+
|
|
5
|
+
function runInTransaction({
|
|
6
|
+
targetDir,
|
|
7
|
+
affectedPaths = [],
|
|
8
|
+
execute,
|
|
9
|
+
operation = "operation",
|
|
10
|
+
TransactionClass = FileTransaction,
|
|
11
|
+
}) {
|
|
12
|
+
if (typeof execute !== "function") {
|
|
13
|
+
throw new TypeError("runInTransaction requires execute callback");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const transaction = new TransactionClass(targetDir);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
transaction.prepare(affectedPaths);
|
|
20
|
+
const result = execute(transaction);
|
|
21
|
+
const backupPath = transaction.finish();
|
|
22
|
+
return {
|
|
23
|
+
result,
|
|
24
|
+
backupPath,
|
|
25
|
+
transaction,
|
|
26
|
+
};
|
|
27
|
+
} catch (error) {
|
|
28
|
+
try {
|
|
29
|
+
transaction.rollback();
|
|
30
|
+
} catch (rollbackError) {
|
|
31
|
+
throw new Error(`${error.message}\n回滚失败:${rollbackError.message}`);
|
|
32
|
+
}
|
|
33
|
+
throw new Error(
|
|
34
|
+
`${error.message}\n已回滚本次 ${operation};临时备份保留于 ${transaction.backupRoot}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
runInTransaction,
|
|
41
|
+
};
|