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,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { copyPath } = require("./copy-path");
|
|
7
|
+
const {
|
|
8
|
+
normalizeRelativePath,
|
|
9
|
+
targetPath,
|
|
10
|
+
pathKind,
|
|
11
|
+
} = require("./path-utils");
|
|
12
|
+
|
|
13
|
+
class FileTransaction {
|
|
14
|
+
constructor(targetDir, deps = {}) {
|
|
15
|
+
this.targetDir = path.resolve(targetDir);
|
|
16
|
+
this.fs = deps.fs || fs;
|
|
17
|
+
this.path = deps.path || path;
|
|
18
|
+
this.copyPath = deps.copyPath || copyPath;
|
|
19
|
+
this.pathKind = deps.pathKind || pathKind;
|
|
20
|
+
this.backupRoot = deps.backupRoot || this.fs.mkdtempSync(
|
|
21
|
+
this.path.join((deps.os || os).tmpdir(), "create-yss-spec-backup-"),
|
|
22
|
+
);
|
|
23
|
+
this.backups = [];
|
|
24
|
+
this.mutated = [];
|
|
25
|
+
this.createdDirectories = [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
relative(absolutePath) {
|
|
29
|
+
return normalizeRelativePath(this.path.relative(this.targetDir, absolutePath));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
backupPath(relativePath) {
|
|
33
|
+
return this.path.join(this.backupRoot, ...normalizeRelativePath(relativePath).split("/"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
prepare(relativePaths) {
|
|
37
|
+
const candidates = [...new Set(relativePaths)]
|
|
38
|
+
.map((relativePath) => normalizeRelativePath(relativePath))
|
|
39
|
+
.filter(
|
|
40
|
+
(relativePath) =>
|
|
41
|
+
relativePath &&
|
|
42
|
+
this.pathKind(targetPath(this.targetDir, relativePath)) !== "missing",
|
|
43
|
+
)
|
|
44
|
+
.sort((left, right) => left.split("/").length - right.split("/").length);
|
|
45
|
+
|
|
46
|
+
for (const relativePath of candidates) {
|
|
47
|
+
if (
|
|
48
|
+
this.backups.some(
|
|
49
|
+
(backup) =>
|
|
50
|
+
relativePath === backup.relativePath ||
|
|
51
|
+
relativePath.startsWith(`${backup.relativePath}/`),
|
|
52
|
+
)
|
|
53
|
+
) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const source = targetPath(this.targetDir, relativePath);
|
|
58
|
+
const destination = this.backupPath(relativePath);
|
|
59
|
+
this.copyPath(source, destination);
|
|
60
|
+
this.backups.push({ relativePath, destination });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ensureParent(absolutePath) {
|
|
65
|
+
const missing = [];
|
|
66
|
+
let current = this.path.dirname(absolutePath);
|
|
67
|
+
|
|
68
|
+
while (current !== this.targetDir && !this.fs.existsSync(current)) {
|
|
69
|
+
missing.push(current);
|
|
70
|
+
current = this.path.dirname(current);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (current !== this.targetDir && this.pathKind(current) !== "directory") {
|
|
74
|
+
throw new Error(`目标父路径不是目录:${this.relative(current)}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const directory of missing.reverse()) {
|
|
78
|
+
this.fs.mkdirSync(directory);
|
|
79
|
+
this.createdDirectories.push(this.relative(directory));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
mark(absolutePath) {
|
|
84
|
+
this.mutated.push(this.relative(absolutePath));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
writeFile(absolutePath, content, options = "utf8") {
|
|
88
|
+
this.ensureParent(absolutePath);
|
|
89
|
+
this.fs.writeFileSync(absolutePath, content, options);
|
|
90
|
+
this.mark(absolutePath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
copyFile(sourcePath, destinationPath) {
|
|
94
|
+
this.ensureParent(destinationPath);
|
|
95
|
+
this.copyPath(sourcePath, destinationPath);
|
|
96
|
+
this.mark(destinationPath);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
remove(absolutePath) {
|
|
100
|
+
if (!this.fs.existsSync(absolutePath)) return;
|
|
101
|
+
this.fs.rmSync(absolutePath, { recursive: true, force: true });
|
|
102
|
+
this.mark(absolutePath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
move(sourcePath, destinationPath) {
|
|
106
|
+
this.ensureParent(destinationPath);
|
|
107
|
+
this.copyPath(sourcePath, destinationPath);
|
|
108
|
+
this.mark(destinationPath);
|
|
109
|
+
this.remove(sourcePath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
rollback() {
|
|
113
|
+
for (const relativePath of [...new Set(this.mutated)].sort(
|
|
114
|
+
(left, right) => right.split("/").length - left.split("/").length,
|
|
115
|
+
)) {
|
|
116
|
+
this.fs.rmSync(targetPath(this.targetDir, relativePath), {
|
|
117
|
+
recursive: true,
|
|
118
|
+
force: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const relativePath of [...new Set(this.createdDirectories)].sort(
|
|
123
|
+
(left, right) => right.split("/").length - left.split("/").length,
|
|
124
|
+
)) {
|
|
125
|
+
const directory = targetPath(this.targetDir, relativePath);
|
|
126
|
+
if (this.fs.existsSync(directory) && this.fs.readdirSync(directory).length === 0) {
|
|
127
|
+
this.fs.rmdirSync(directory);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const backup of [...this.backups].sort(
|
|
132
|
+
(left, right) =>
|
|
133
|
+
left.relativePath.split("/").length - right.relativePath.split("/").length,
|
|
134
|
+
)) {
|
|
135
|
+
this.copyPath(
|
|
136
|
+
backup.destination,
|
|
137
|
+
targetPath(this.targetDir, backup.relativePath),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
finish() {
|
|
143
|
+
if (this.backups.length === 0) {
|
|
144
|
+
this.fs.rmSync(this.backupRoot, { recursive: true, force: true });
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return this.backupRoot;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
FileTransaction,
|
|
153
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawnSync } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
const GITLINK_MODE = "160000";
|
|
8
|
+
const GITLINK_PATH_CACHE = new Map();
|
|
9
|
+
|
|
10
|
+
function posixRelative(from, to) {
|
|
11
|
+
return path.relative(from, to).split(path.sep).join("/");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseGitmodules(content) {
|
|
15
|
+
const modules = [];
|
|
16
|
+
if (typeof content !== "string" || content.length === 0) return modules;
|
|
17
|
+
let current = null;
|
|
18
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
19
|
+
const line = rawLine.trim();
|
|
20
|
+
const name = line.match(/^\[submodule "(.+)"\]$/);
|
|
21
|
+
if (name) {
|
|
22
|
+
current = { name: name[1], path: "", url: "" };
|
|
23
|
+
modules.push(current);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!current) continue;
|
|
27
|
+
const pathMatch = line.match(/^path\s*=\s*(.+)$/);
|
|
28
|
+
if (pathMatch) {
|
|
29
|
+
current.path = pathMatch[1].trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
30
|
+
}
|
|
31
|
+
const urlMatch = line.match(/^url\s*=\s*(.+)$/);
|
|
32
|
+
if (urlMatch) current.url = urlMatch[1].trim();
|
|
33
|
+
}
|
|
34
|
+
return modules;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readGitmodules(repoRoot, deps = {}) {
|
|
38
|
+
const file = path.join(repoRoot, ".gitmodules");
|
|
39
|
+
const pathKind =
|
|
40
|
+
deps.pathKind ||
|
|
41
|
+
((value) => {
|
|
42
|
+
try {
|
|
43
|
+
const stat = fs.lstatSync(value);
|
|
44
|
+
if (stat.isFile()) return "file";
|
|
45
|
+
if (stat.isDirectory()) return "directory";
|
|
46
|
+
return "other";
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error.code === "ENOENT") return "missing";
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (!fs.existsSync(file) || pathKind(file) !== "file") return [];
|
|
53
|
+
return parseGitmodules(fs.readFileSync(file, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function gitlinkPaths(repoRoot, deps = {}) {
|
|
57
|
+
const resolvedRoot = path.resolve(repoRoot);
|
|
58
|
+
const useCache = !deps.disableCache && !deps.spawn;
|
|
59
|
+
if (useCache && GITLINK_PATH_CACHE.has(resolvedRoot)) {
|
|
60
|
+
return GITLINK_PATH_CACHE.get(resolvedRoot);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const spawn = deps.spawn || spawnSync;
|
|
64
|
+
const result = spawn(
|
|
65
|
+
"git",
|
|
66
|
+
["-C", resolvedRoot, "ls-files", "--stage", "-z"],
|
|
67
|
+
{
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
timeout: 5000,
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
const paths = new Set();
|
|
73
|
+
if (result.status === 0) {
|
|
74
|
+
for (const record of (result.stdout || "").split("\0")) {
|
|
75
|
+
const match = record.match(/^(160000)\s+[0-9a-f]+\s+\d+\s+(.+)$/i);
|
|
76
|
+
if (match) paths.add(match[2].replaceAll("\\", "/"));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (useCache) GITLINK_PATH_CACHE.set(resolvedRoot, paths);
|
|
81
|
+
return paths;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function clearGitlinkPathCache(repoRoot = null) {
|
|
85
|
+
if (repoRoot === null) {
|
|
86
|
+
GITLINK_PATH_CACHE.clear();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
GITLINK_PATH_CACHE.delete(path.resolve(repoRoot));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isGitSubmoduleMount(repoRoot, targetPathValue, deps = {}) {
|
|
93
|
+
const relative = posixRelative(repoRoot, path.resolve(targetPathValue));
|
|
94
|
+
if (
|
|
95
|
+
!relative ||
|
|
96
|
+
relative === "." ||
|
|
97
|
+
relative.startsWith("..") ||
|
|
98
|
+
path.isAbsolute(relative)
|
|
99
|
+
) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (readGitmodules(repoRoot, deps).some((item) => item.path === relative)) return true;
|
|
103
|
+
return gitlinkPaths(repoRoot, deps).has(relative);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
GITLINK_MODE,
|
|
108
|
+
GITLINK_PATH_CACHE,
|
|
109
|
+
parseGitmodules,
|
|
110
|
+
readGitmodules,
|
|
111
|
+
gitlinkPaths,
|
|
112
|
+
clearGitlinkPathCache,
|
|
113
|
+
isGitSubmoduleMount,
|
|
114
|
+
posixRelative,
|
|
115
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawnSync } = require("node:child_process");
|
|
6
|
+
const { isGitSubmoduleMount, posixRelative } = require("./submodule");
|
|
7
|
+
|
|
8
|
+
function collectGitRoots(start) {
|
|
9
|
+
const roots = [];
|
|
10
|
+
if (!start) return roots;
|
|
11
|
+
let current = path.resolve(start);
|
|
12
|
+
while (true) {
|
|
13
|
+
const gitPath = path.join(current, ".git");
|
|
14
|
+
let validGitEntry = false;
|
|
15
|
+
try {
|
|
16
|
+
const gitStat = fs.lstatSync(gitPath);
|
|
17
|
+
validGitEntry = gitStat.isDirectory() ? fs.readdirSync(gitPath).length > 0 : gitStat.isFile();
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error.code !== "ENOENT") throw error;
|
|
20
|
+
}
|
|
21
|
+
if (validGitEntry || fs.existsSync(path.join(current, ".gitmodules"))) roots.push(current);
|
|
22
|
+
const parent = path.dirname(current);
|
|
23
|
+
if (parent === current) break;
|
|
24
|
+
current = parent;
|
|
25
|
+
}
|
|
26
|
+
return roots;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function gitShowSuperproject(cwd, spawn = spawnSync) {
|
|
30
|
+
const result = spawn("git", ["-C", cwd, "rev-parse", "--show-superproject-working-tree"], { encoding: "utf8", timeout: 5000 });
|
|
31
|
+
return result.status === 0 ? (result.stdout || "").trim() : "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function gitAbbrevRef(cwd, spawn = spawnSync) {
|
|
35
|
+
const result = spawn("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8", timeout: 5000 });
|
|
36
|
+
return result.status === 0 ? (result.stdout || "").trim() : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isEmptyDir(dir) {
|
|
40
|
+
if (!fs.existsSync(dir)) return true;
|
|
41
|
+
try {
|
|
42
|
+
return fs.readdirSync(dir).filter((name) => name !== "." && name !== "..").length === 0;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function inspectCheckoutState(repoRoot, targetPathValue, deps = {}) {
|
|
49
|
+
const resolved = path.resolve(targetPathValue);
|
|
50
|
+
const relative = posixRelative(path.resolve(repoRoot), resolved);
|
|
51
|
+
const isMount = isGitSubmoduleMount(repoRoot, resolved, deps) || Boolean(fs.existsSync(path.join(resolved, ".git")) && gitShowSuperproject(resolved, deps.spawn || spawnSync));
|
|
52
|
+
const hasGit = fs.existsSync(path.join(resolved, ".git"));
|
|
53
|
+
if (isMount && !hasGit) return isEmptyDir(resolved) ? "empty-gitlink" : "uninitialized";
|
|
54
|
+
if (isMount && hasGit) return gitAbbrevRef(resolved, deps.spawn || spawnSync) === "HEAD" ? "detached-head" : "attached-branch";
|
|
55
|
+
if (!relative || relative.startsWith("..")) return null;
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function gitDirtyWarning(targetDir, spawn = spawnSync) {
|
|
60
|
+
const probe = spawn("git", ["rev-parse", "--is-inside-work-tree"], { cwd: targetDir, encoding: "utf8", timeout: 5000 });
|
|
61
|
+
if (probe.status !== 0 || (probe.stdout || "").trim() !== "true") return null;
|
|
62
|
+
const status = spawn("git", ["status", "--porcelain"], { cwd: targetDir, encoding: "utf8", timeout: 5000 });
|
|
63
|
+
if (status.status === 0 && (status.stdout || "").trim()) {
|
|
64
|
+
return "警告:目标 Git worktree 存在未提交改动;CLI 不会自动 stash 或提交,请在结果后检查 git diff / git status";
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { collectGitRoots, gitShowSuperproject, gitAbbrevRef, isEmptyDir, inspectCheckoutState, gitDirtyWarning };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
classifyAttachOperations,
|
|
5
|
+
createAttachPlan,
|
|
6
|
+
} = require("./attach-planner");
|
|
7
|
+
const {
|
|
8
|
+
ownershipAwareUnmanagedReason,
|
|
9
|
+
decoratePlanWithOwnership,
|
|
10
|
+
} = require("./ownership-runtime");
|
|
11
|
+
const {
|
|
12
|
+
applyLifecycleToOperations,
|
|
13
|
+
decoratePlanWithLifecycle,
|
|
14
|
+
} = require("./lifecycle-runtime");
|
|
15
|
+
|
|
16
|
+
function buildAttachPlanFromRuntime({
|
|
17
|
+
targetDir,
|
|
18
|
+
desiredOperations,
|
|
19
|
+
migration,
|
|
20
|
+
warning = null,
|
|
21
|
+
getUnmanagedReason,
|
|
22
|
+
getPathKind,
|
|
23
|
+
getFileHash,
|
|
24
|
+
}) {
|
|
25
|
+
const lifecycleOperations = applyLifecycleToOperations(desiredOperations);
|
|
26
|
+
const classified = classifyAttachOperations({
|
|
27
|
+
desiredOperations: lifecycleOperations,
|
|
28
|
+
getUnmanagedReason: (operation) =>
|
|
29
|
+
ownershipAwareUnmanagedReason(operation, getUnmanagedReason),
|
|
30
|
+
getPathKind,
|
|
31
|
+
getFileHash,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const basePlan = createAttachPlan({
|
|
35
|
+
targetDir,
|
|
36
|
+
classified,
|
|
37
|
+
migration,
|
|
38
|
+
warnings: warning ? [warning] : [],
|
|
39
|
+
});
|
|
40
|
+
const ownershipPlan = decoratePlanWithOwnership(basePlan, lifecycleOperations);
|
|
41
|
+
const plan = decoratePlanWithLifecycle(ownershipPlan, lifecycleOperations);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
classified,
|
|
45
|
+
plan,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
buildAttachPlanFromRuntime,
|
|
51
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function classifyAttachOperations({
|
|
4
|
+
desiredOperations = [],
|
|
5
|
+
getUnmanagedReason = () => null,
|
|
6
|
+
getPathKind,
|
|
7
|
+
getFileHash,
|
|
8
|
+
}) {
|
|
9
|
+
if (typeof getPathKind !== "function") {
|
|
10
|
+
throw new TypeError("classifyAttachOperations requires getPathKind");
|
|
11
|
+
}
|
|
12
|
+
if (typeof getFileHash !== "function") {
|
|
13
|
+
throw new TypeError("classifyAttachOperations requires getFileHash");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const missing = [];
|
|
17
|
+
const matched = [];
|
|
18
|
+
const identity = [];
|
|
19
|
+
const conflicts = [];
|
|
20
|
+
const forceableConflicts = [];
|
|
21
|
+
const unsafe = [];
|
|
22
|
+
|
|
23
|
+
for (const operation of desiredOperations) {
|
|
24
|
+
const unmanagedReason = getUnmanagedReason(operation);
|
|
25
|
+
if (unmanagedReason) {
|
|
26
|
+
unsafe.push({ ...operation, reason: unmanagedReason });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const kind = getPathKind(operation);
|
|
31
|
+
if (kind === "missing") {
|
|
32
|
+
missing.push(operation);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (kind !== "file") {
|
|
37
|
+
unsafe.push({
|
|
38
|
+
...operation,
|
|
39
|
+
reason: `目标路径类型为 ${kind},无法安全判断是否可以替换`,
|
|
40
|
+
});
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const currentHash = getFileHash(operation);
|
|
45
|
+
if (currentHash === operation.desiredHash) {
|
|
46
|
+
matched.push(operation);
|
|
47
|
+
} else if (operation.identityConversion) {
|
|
48
|
+
identity.push(operation);
|
|
49
|
+
} else {
|
|
50
|
+
const conflict = {
|
|
51
|
+
...operation,
|
|
52
|
+
reason:
|
|
53
|
+
operation.mergeStrategy === "manual"
|
|
54
|
+
? "目标可定制文件已存在且内容不一致;mergeStrategy=manual,必须人工处理"
|
|
55
|
+
: "目标受管文件已存在且内容不一致",
|
|
56
|
+
};
|
|
57
|
+
conflicts.push(conflict);
|
|
58
|
+
if (operation.mergeStrategy !== "manual") {
|
|
59
|
+
forceableConflicts.push(conflict);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
missing,
|
|
66
|
+
matched,
|
|
67
|
+
identity,
|
|
68
|
+
conflicts,
|
|
69
|
+
forceableConflicts,
|
|
70
|
+
unsafe,
|
|
71
|
+
desiredOperations,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createAttachPlan({
|
|
76
|
+
targetDir,
|
|
77
|
+
classified,
|
|
78
|
+
migration = null,
|
|
79
|
+
warnings = [],
|
|
80
|
+
}) {
|
|
81
|
+
if (!classified || typeof classified !== "object") {
|
|
82
|
+
throw new TypeError("createAttachPlan requires classified attach operations");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const missing = classified.missing || [];
|
|
86
|
+
const matched = classified.matched || [];
|
|
87
|
+
const identity = classified.identity || [];
|
|
88
|
+
const classifiedConflicts = classified.conflicts || [];
|
|
89
|
+
const forceableConflicts = classified.forceableConflicts || classifiedConflicts;
|
|
90
|
+
const classifiedUnsafe = classified.unsafe || [];
|
|
91
|
+
const migrationConflicts = migration?.conflicts || [];
|
|
92
|
+
const migrationUnsafe = migration?.unsafe || [];
|
|
93
|
+
|
|
94
|
+
const conflicts = [
|
|
95
|
+
...classifiedConflicts.map((operation) => ({
|
|
96
|
+
path: operation.relativePath,
|
|
97
|
+
reason: operation.reason,
|
|
98
|
+
forceable: forceableConflicts.includes(operation),
|
|
99
|
+
source: "managed-file",
|
|
100
|
+
})),
|
|
101
|
+
...migrationConflicts.map((item) => ({
|
|
102
|
+
path: item.from || item.path || item.to,
|
|
103
|
+
from: item.from,
|
|
104
|
+
to: item.to,
|
|
105
|
+
reason: item.reason,
|
|
106
|
+
forceable: false,
|
|
107
|
+
source: "migration",
|
|
108
|
+
})),
|
|
109
|
+
];
|
|
110
|
+
const unsafe = [
|
|
111
|
+
...classifiedUnsafe.map((operation) => ({
|
|
112
|
+
path: operation.relativePath,
|
|
113
|
+
reason: operation.reason,
|
|
114
|
+
source: "managed-file",
|
|
115
|
+
})),
|
|
116
|
+
...migrationUnsafe.map((item) => ({
|
|
117
|
+
path: item.path,
|
|
118
|
+
reason: item.reason,
|
|
119
|
+
source: "migration",
|
|
120
|
+
})),
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
operation: "attach",
|
|
125
|
+
targetDir,
|
|
126
|
+
template: null,
|
|
127
|
+
changes: [
|
|
128
|
+
...missing.map((operation) => ({
|
|
129
|
+
action: "add",
|
|
130
|
+
path: operation.relativePath,
|
|
131
|
+
})),
|
|
132
|
+
...identity.map((operation) => ({
|
|
133
|
+
action: "identity-convert",
|
|
134
|
+
path: operation.relativePath,
|
|
135
|
+
})),
|
|
136
|
+
],
|
|
137
|
+
conflicts,
|
|
138
|
+
unsafe,
|
|
139
|
+
migration: migration || {
|
|
140
|
+
operations: [],
|
|
141
|
+
legacy: [],
|
|
142
|
+
conflicts: [],
|
|
143
|
+
unsafe: [],
|
|
144
|
+
},
|
|
145
|
+
warnings,
|
|
146
|
+
blocked: unsafe.length > 0 || migrationConflicts.length > 0,
|
|
147
|
+
stats: {
|
|
148
|
+
missing: missing.length,
|
|
149
|
+
matched: matched.length,
|
|
150
|
+
identity: identity.length,
|
|
151
|
+
conflicts: conflicts.length,
|
|
152
|
+
unsafe: unsafe.length,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
classifyAttachOperations,
|
|
159
|
+
createAttachPlan,
|
|
160
|
+
};
|