sillyspec 3.26.10 → 3.26.13
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/.claude/skills/sillyspec-archive/SKILL.md +85 -85
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
- package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
- package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
- package/.claude/skills/sillyspec-quick/SKILL.md +115 -111
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
- package/.claude/skills/sillyspec-state/SKILL.md +64 -64
- package/.claude/skills/sillyspec-status/SKILL.md +54 -54
- package/.claude/skills/sillyspec-verify/SKILL.md +83 -83
- package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
- package/.gitattributes +2 -0
- package/.husky/pre-push +20 -20
- package/README.md +203 -201
- package/SKILL.md +91 -90
- package/package.json +39 -39
- package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +95 -86
- package/packages/dashboard/server/index.js +663 -601
- package/packages/dashboard/server/parser.js +526 -526
- package/src/change-risk-profile.js +254 -254
- package/src/classify-change.js +109 -109
- package/src/config-schema.js +313 -310
- package/src/contract-matrix.js +388 -379
- package/src/db.js +370 -350
- package/src/dispatch/backends/local-agent.js +155 -155
- package/src/dispatch/backends/sillyhub-mcp.js +245 -245
- package/src/dispatch/probe.js +246 -210
- package/src/dispatch/strategy.js +121 -121
- package/src/docs-check.js +188 -16
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +898 -711
- package/src/endpoint-extractor.js +401 -401
- package/src/fs-atomic.js +71 -69
- package/src/git-helper.js +82 -79
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +854 -848
- package/src/index.js +251 -20
- package/src/init.js +624 -623
- package/src/knowledge-match.js +130 -130
- package/src/local-detect.js +128 -120
- package/src/machine-interface.js +543 -543
- package/src/migrate.js +127 -127
- package/src/modules.js +7 -5
- package/src/progress/change-registry.js +297 -260
- package/src/progress/consistency-doctor.js +355 -355
- package/src/progress/shared.js +36 -36
- package/src/progress/stage-machine.js +589 -550
- package/src/progress/step-store.js +261 -261
- package/src/progress.js +1285 -1127
- package/src/quicklog.js +724 -693
- package/src/review-tier.js +77 -77
- package/src/run/command.js +1408 -1364
- package/src/run/complete-handlers.js +1450 -1399
- package/src/run/complete.js +1181 -920
- package/src/run/gates.js +878 -755
- package/src/run/prompt.js +765 -761
- package/src/run/quick-audit.js +155 -146
- package/src/run/shared.js +1348 -1215
- package/src/run/stage.js +527 -479
- package/src/scan-postcheck.js +371 -371
- package/src/setup.js +414 -416
- package/src/sillyhub-mcp/client.js +597 -529
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +358 -319
- package/src/stage-contract.js +871 -897
- package/src/stage-review.js +549 -549
- package/src/stages/archive.js +156 -155
- package/src/stages/brainstorm-auto.js +236 -236
- package/src/stages/brainstorm.js +597 -597
- package/src/stages/cmd-existence.js +145 -145
- package/src/stages/doctor.js +386 -371
- package/src/stages/execute.js +1014 -1003
- package/src/stages/explore.js +34 -34
- package/src/stages/index.js +29 -29
- package/src/stages/knowledge.js +498 -498
- package/src/stages/plan-postcheck.js +1323 -1302
- package/src/stages/plan.js +642 -623
- package/src/stages/quick.js +153 -151
- package/src/stages/scan.js +607 -607
- package/src/stages/status.js +64 -64
- package/src/stages/verify.js +264 -264
- package/src/sync.js +1325 -1135
- package/src/task-review.js +1204 -1019
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +1199 -1186
- package/src/workflow.js +600 -600
- package/src/worktree-apply.js +1237 -1136
- package/src/worktree-deps.js +453 -353
- package/src/worktree.js +1527 -1481
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
package/src/worktree-deps.js
CHANGED
|
@@ -1,353 +1,453 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* worktree-deps.js — Worktree 依赖供给引擎
|
|
3
|
-
*
|
|
4
|
-
* 在 worktree.create() 的 baseline overlay 之后调用,让 worktree 立即可构建/测试。
|
|
5
|
-
* 策略:junction/symlink 快路径(lockfile 一致)+ install 兜底;多语言按 local.yaml
|
|
6
|
-
* project.type + lockfile 推断 install 命令。供给可失败,但状态写进 meta 供验证硬门读取。
|
|
7
|
-
*
|
|
8
|
-
* 见 change 2026-06-28-worktree-deps-provision / D-005@v1, D-007@v1。
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { existsSync, readFileSync, realpathSync } from 'fs';
|
|
12
|
-
import { join } from 'path';
|
|
13
|
-
import {
|
|
14
|
-
import { createHash } from 'crypto';
|
|
15
|
-
|
|
16
|
-
const LOCKFILES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock'];
|
|
17
|
-
const DEFAULT_TIMEOUT_MS = 300 * 1000;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 取目录下首个命中的 lockfile 的 sha256 前 16 位;
|
|
21
|
-
* 无 lockfile 则 hash package.json;都没有返回 null。
|
|
22
|
-
*/
|
|
23
|
-
export function lockfileHash(dir) {
|
|
24
|
-
if (!dir || !existsSync(dir)) return null;
|
|
25
|
-
for (const lf of LOCKFILES) {
|
|
26
|
-
const p = join(dir, lf);
|
|
27
|
-
if (existsSync(p)) {
|
|
28
|
-
return createHash('sha256').update(readFileSync(p)).digest('hex').slice(0, 16);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const pkg = join(dir, 'package.json');
|
|
32
|
-
if (existsSync(pkg)) {
|
|
33
|
-
return createHash('sha256').update(readFileSync(pkg)).digest('hex').slice(0, 16);
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** 命中目录下的 lockfile 文件名(用于判断 nodejs 子类)*/
|
|
39
|
-
function detectLockfile(dir) {
|
|
40
|
-
if (!dir || !existsSync(dir)) return null;
|
|
41
|
-
for (const lf of LOCKFILES) {
|
|
42
|
-
if (existsSync(join(dir, lf))) return lf;
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return '
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* @
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
1
|
+
/**
|
|
2
|
+
* worktree-deps.js — Worktree 依赖供给引擎
|
|
3
|
+
*
|
|
4
|
+
* 在 worktree.create() 的 baseline overlay 之后调用,让 worktree 立即可构建/测试。
|
|
5
|
+
* 策略:junction/symlink 快路径(lockfile 一致)+ install 兜底;多语言按 local.yaml
|
|
6
|
+
* project.type + lockfile 推断 install 命令。供给可失败,但状态写进 meta 供验证硬门读取。
|
|
7
|
+
*
|
|
8
|
+
* 见 change 2026-06-28-worktree-deps-provision / D-005@v1, D-007@v1。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, realpathSync } from 'fs';
|
|
12
|
+
import { join, isAbsolute, resolve as resolvePath, sep as pathSep } from 'path';
|
|
13
|
+
import { execFileSync } from 'child_process';
|
|
14
|
+
import { createHash } from 'crypto';
|
|
15
|
+
|
|
16
|
+
const LOCKFILES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock'];
|
|
17
|
+
const DEFAULT_TIMEOUT_MS = 300 * 1000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 取目录下首个命中的 lockfile 的 sha256 前 16 位;
|
|
21
|
+
* 无 lockfile 则 hash package.json;都没有返回 null。
|
|
22
|
+
*/
|
|
23
|
+
export function lockfileHash(dir) {
|
|
24
|
+
if (!dir || !existsSync(dir)) return null;
|
|
25
|
+
for (const lf of LOCKFILES) {
|
|
26
|
+
const p = join(dir, lf);
|
|
27
|
+
if (existsSync(p)) {
|
|
28
|
+
return createHash('sha256').update(readFileSync(p)).digest('hex').slice(0, 16);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const pkg = join(dir, 'package.json');
|
|
32
|
+
if (existsSync(pkg)) {
|
|
33
|
+
return createHash('sha256').update(readFileSync(pkg)).digest('hex').slice(0, 16);
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 命中目录下的 lockfile 文件名(用于判断 nodejs 子类)*/
|
|
39
|
+
function detectLockfile(dir) {
|
|
40
|
+
if (!dir || !existsSync(dir)) return null;
|
|
41
|
+
for (const lf of LOCKFILES) {
|
|
42
|
+
if (existsSync(join(dir, lf))) return lf;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 读取 local.yaml 文本(specBase 优先,回退 worktreePath/.sillyspec;不读 process.cwd 避免环境配置泄漏)。
|
|
49
|
+
* 返回 { text, source }——source 标记信任级(体检 SEC-01):
|
|
50
|
+
* - 'specBase':主仓配置,可信(其 commands.install 允许执行,仍过白名单+元字符门)
|
|
51
|
+
* - 'worktree':worktree 内副本,agent/子代理可写区——只作 project.type / modules 等
|
|
52
|
+
* 只读探测,其 commands.install 永不执行(否则绕过 worktree-guard 的危险命令拦截)
|
|
53
|
+
*/
|
|
54
|
+
function readLocalYamlSourced(specBase, worktreePath) {
|
|
55
|
+
if (specBase) {
|
|
56
|
+
const p = join(specBase, 'local.yaml');
|
|
57
|
+
if (existsSync(p)) return { text: readFileSync(p, 'utf8'), source: 'specBase' };
|
|
58
|
+
}
|
|
59
|
+
if (worktreePath) {
|
|
60
|
+
const p = join(worktreePath, '.sillyspec', 'local.yaml');
|
|
61
|
+
if (existsSync(p)) return { text: readFileSync(p, 'utf8'), source: 'worktree' };
|
|
62
|
+
}
|
|
63
|
+
return { text: null, source: null };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 兼容旧签名:只要文本 */
|
|
67
|
+
function readLocalYaml(specBase, worktreePath) {
|
|
68
|
+
return readLocalYamlSourced(specBase, worktreePath).text;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 从 local.yaml 文本提取 commands.install(轻量正则,不引 yaml 依赖,与 scan-postcheck 一致)*/
|
|
72
|
+
function extractUserInstall(yamlText) {
|
|
73
|
+
if (!yamlText) return null;
|
|
74
|
+
// 匹配 commands: 段下的 install: "..." 或 install: '...'(unavailable 视为无)
|
|
75
|
+
const m = yamlText.match(/install:\s*["']([^"']+)["']/);
|
|
76
|
+
if (m && m[1] && m[1].toLowerCase() !== 'unavailable') return m[1];
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 从 local.yaml 提取 project.type;缺失时按文件特征推断 */
|
|
81
|
+
export function detectProjectType(worktreePath, specBase) {
|
|
82
|
+
const yamlText = readLocalYaml(specBase, worktreePath);
|
|
83
|
+
if (yamlText) {
|
|
84
|
+
const m = yamlText.match(/type:\s*(\S+)/);
|
|
85
|
+
if (m && m[1]) return m[1];
|
|
86
|
+
}
|
|
87
|
+
if (existsSync(join(worktreePath, 'pom.xml'))) return 'maven';
|
|
88
|
+
if (existsSync(join(worktreePath, 'build.gradle')) || existsSync(join(worktreePath, 'build.gradle.kts'))) return 'gradle';
|
|
89
|
+
if (existsSync(join(worktreePath, 'package.json'))) return 'nodejs';
|
|
90
|
+
// Python:pyproject.toml(uv/poetry/pdm)或 requirements.txt(pip)——治 worktree 内 ruff/pre-commit 等
|
|
91
|
+
// 二进制不供给(exec 复盘 3-②:detectProjectType 原无 python 分支,python 项目根被误判 generic → n/a)
|
|
92
|
+
if (existsSync(join(worktreePath, 'pyproject.toml')) || existsSync(join(worktreePath, 'requirements.txt'))) return 'python';
|
|
93
|
+
return 'generic';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 按 project.type + lockfile 推断 install 命令(无 commands.install 时)*/
|
|
97
|
+
export function inferInstallCommand(projectType, worktreePath, userInstall) {
|
|
98
|
+
if (userInstall) return userInstall;
|
|
99
|
+
switch (projectType) {
|
|
100
|
+
case 'nodejs':
|
|
101
|
+
if (existsSync(join(worktreePath, 'pnpm-lock.yaml'))) return 'pnpm install --frozen-lockfile';
|
|
102
|
+
if (existsSync(join(worktreePath, 'package-lock.json'))) return 'npm ci';
|
|
103
|
+
if (existsSync(join(worktreePath, 'yarn.lock'))) return 'yarn install --frozen-lockfile';
|
|
104
|
+
return 'npm install'; // 无 lockfile 兜底(X-2)
|
|
105
|
+
case 'maven':
|
|
106
|
+
return 'mvn -o test';
|
|
107
|
+
case 'gradle':
|
|
108
|
+
// win32 cmd.exe 跑不了 ./gradlew(体检 BUG-08 同型):优先 gradlew.bat,否则全局 gradle
|
|
109
|
+
if (process.platform === 'win32') {
|
|
110
|
+
return existsSync(join(worktreePath, 'gradlew.bat')) ? 'gradlew.bat test' : 'gradle test';
|
|
111
|
+
}
|
|
112
|
+
return './gradlew test';
|
|
113
|
+
case 'python':
|
|
114
|
+
// uv 优先(现代 Python 工具链,pyproject.toml/uv.lock 项目走 uv sync 建 .venv + 装依赖,
|
|
115
|
+
// 与 execute worktree 环境预告一致);纯 requirements.txt(无 pyproject)回退 pip
|
|
116
|
+
if (existsSync(join(worktreePath, 'uv.lock')) || existsSync(join(worktreePath, 'pyproject.toml'))) return 'uv sync';
|
|
117
|
+
return 'pip install -r requirements.txt';
|
|
118
|
+
default:
|
|
119
|
+
return null; // generic → n/a
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 在 worktreePath 创建 node_modules 链接到 mainNodeModules;失败回退 */
|
|
124
|
+
function tryLink(mainNodeModules, linkPath) {
|
|
125
|
+
// 已存在:确认是否已指向 main(幂等)
|
|
126
|
+
if (existsSync(linkPath)) {
|
|
127
|
+
try {
|
|
128
|
+
const resolved = realpathSync(linkPath);
|
|
129
|
+
const resolvedTarget = realpathSync(mainNodeModules);
|
|
130
|
+
if (resolved === resolvedTarget) return { ok: true, method: process.platform === 'win32' ? 'junction' : 'symlink' };
|
|
131
|
+
} catch {}
|
|
132
|
+
// 指向别处 → 不 clobber,视为已有依赖(installed 语义)
|
|
133
|
+
return { ok: true, method: 'install', preexisting: true };
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
if (process.platform === 'win32') {
|
|
137
|
+
// execFileSync 数组形式不经 shell:POSIX 双引号内 `/$() 会执行、cmd.exe 双引号内 %VAR% 仍展开,
|
|
138
|
+
// linkPath 含 local.yaml 模块 path,属项目内可配置值(安全收敛,与 git-helper 同范式)
|
|
139
|
+
execFileSync('cmd.exe', ['/c', 'mklink', '/J', linkPath, mainNodeModules], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
140
|
+
return { ok: true, method: 'junction' };
|
|
141
|
+
}
|
|
142
|
+
execFileSync('ln', ['-s', mainNodeModules, linkPath], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
143
|
+
return { ok: true, method: 'symlink' };
|
|
144
|
+
} catch (e) {
|
|
145
|
+
return { ok: false, error: e.message };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 执行 install 命令(带超时)。
|
|
151
|
+
*
|
|
152
|
+
* 安全收口(体检 SEC-01):commands.install 来自 local.yaml 配置,若经 execSync(shell)
|
|
153
|
+
* 原样执行,yaml 里的 `$(...)`/反引号/`;` 必然被解释——且 worktree 副本属 agent 可写区。
|
|
154
|
+
* 三道门:
|
|
155
|
+
* 1. 调用方保证来源可信(provisionDeps 只对 source==='specBase' 的 userInstall 走到这里)
|
|
156
|
+
* 2. 可执行前缀白名单(包管理器家族)
|
|
157
|
+
* 3. shell 元字符黑名单(win32 额外拦 %,cmd.exe 会展开 %VAR%),通过后拆 argv 数组执行不经 shell
|
|
158
|
+
*
|
|
159
|
+
* 链式命令支持(坑 worktree-install-whitelist-monorepo-chain,2026-08-20 实证):monorepo 的
|
|
160
|
+
* commands.install 常为链式写法(`cd web && pnpm install` / `npm install && npm run build:pkg`),
|
|
161
|
+
* 旧实现整条过白名单 + 元字符门,`&&` 必拒 → depsStatus=failed → execute deps 门控卡死且无自愈
|
|
162
|
+
* 路径(改 local.yaml 成单命令对需要 post-install build 的 monorepo 不可行)。现按 `&&` 拆段
|
|
163
|
+
* 逐段校验执行:每段独立过白名单 + 元字符门(拆分后残余的单 `&`/`|`/`;` 仍被元字符门拦),
|
|
164
|
+
* 段内允许 `cd <相对子路径>`(resolve 后必须仍在 worktree 根内,防越界建产物/读外部),
|
|
165
|
+
* 全程 argv 数组执行不经 shell、任一段失败即停(&& 语义)。安全面不变:每个执行段仍是
|
|
166
|
+
* 白名单包管理器命令,`||`/管道/`;`/后台均不支持。
|
|
167
|
+
*/
|
|
168
|
+
const INSTALL_BINARY_WHITELIST = /^(?:\.\/)?(?:pnpm|npm|yarn|bun|mvn|gradle|gradlew|uv|pip|pip3|python|python3|poetry|make)(?:\.cmd|\.bat|\.exe)?(?:\s|$)/;
|
|
169
|
+
const SHELL_METACHARS = /[;&|<>$`\n]/;
|
|
170
|
+
const WIN_SHELL_METACHARS = /[;&|<>$`\n%]/;
|
|
171
|
+
const CD_SEGMENT_RE = /^cd\s+([^\s]+)$/;
|
|
172
|
+
|
|
173
|
+
function tryInstall(cmd, cwd, timeout) {
|
|
174
|
+
const trimmed = String(cmd || '').trim();
|
|
175
|
+
if (!trimmed) {
|
|
176
|
+
return { ok: false, error: 'install 命令为空' };
|
|
177
|
+
}
|
|
178
|
+
const metaRe = process.platform === 'win32' ? WIN_SHELL_METACHARS : SHELL_METACHARS;
|
|
179
|
+
// && 拆段(单段命令与原路径完全一致,零回归);空段(`a &&`/`&& b`)视为畸形命令拒绝
|
|
180
|
+
const segments = trimmed.split('&&').map(s => s.trim());
|
|
181
|
+
if (segments.some(s => s === '')) {
|
|
182
|
+
return { ok: false, error: `install 链式命令含空段,拒绝执行: ${trimmed}` };
|
|
183
|
+
}
|
|
184
|
+
let curCwd = cwd;
|
|
185
|
+
const root = resolvePath(cwd);
|
|
186
|
+
const planned = [];
|
|
187
|
+
for (const seg of segments) {
|
|
188
|
+
// 白名单先于元字符(保持原判定顺序):`curl … | sh` 报非白名单而非元字符,
|
|
189
|
+
// `npm install; rm …` 过白名单后被元字符门拦——两类拒绝语义与原实现一致
|
|
190
|
+
const cd = seg.match(CD_SEGMENT_RE);
|
|
191
|
+
if (!cd && !INSTALL_BINARY_WHITELIST.test(seg)) {
|
|
192
|
+
return { ok: false, error: `install 命令不在包管理器白名单内,拒绝执行: ${seg}(支持 && 链式与 cd <子目录> 段,各执行段仍须为白名单包管理器命令)` };
|
|
193
|
+
}
|
|
194
|
+
if (metaRe.test(seg)) {
|
|
195
|
+
return { ok: false, error: `install 命令段含 shell 元字符,拒绝执行: ${seg}(链式仅支持 &&;|| / 管道 / ; / 后台不支持)` };
|
|
196
|
+
}
|
|
197
|
+
if (cd) {
|
|
198
|
+
const target = resolvePath(curCwd, cd[1]);
|
|
199
|
+
if (target !== root && !target.startsWith(root + pathSep)) {
|
|
200
|
+
return { ok: false, error: `install 命令的 cd 段越出 worktree 根,拒绝执行: ${seg}(cwd ${cwd})` };
|
|
201
|
+
}
|
|
202
|
+
curCwd = target;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
planned.push(seg);
|
|
206
|
+
}
|
|
207
|
+
for (const seg of planned) {
|
|
208
|
+
const argv = seg.split(/\s+/);
|
|
209
|
+
try {
|
|
210
|
+
if (process.platform === 'win32') {
|
|
211
|
+
// Windows 包管理器是 .cmd 垫片,无 shell 的 spawn 无法解析——经 cmd.exe /c 传参;
|
|
212
|
+
// 元字符(含 %)已拦,残余风险与 tryLink 的 mklink 同款
|
|
213
|
+
execFileSync('cmd.exe', ['/c', ...argv], { cwd: curCwd, timeout, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
214
|
+
} else {
|
|
215
|
+
execFileSync(argv[0], argv.slice(1), { cwd: curCwd, timeout, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
const msg = e.killed ? `timeout after ${timeout}ms` : ((e.stderr && e.stderr.toString()) || e.message);
|
|
219
|
+
return { ok: false, error: `${seg} failed (cwd ${curCwd}): ${msg}` };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { ok: true };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 目录是否含 nodejs 标记(package.json 或任一 lockfile)——识别 monorepo 里的 nodejs 子模块 */
|
|
226
|
+
function hasNodeMarker(dir) {
|
|
227
|
+
if (!dir || !existsSync(dir)) return false;
|
|
228
|
+
if (existsSync(join(dir, 'package.json'))) return true;
|
|
229
|
+
return LOCKFILES.some(lf => existsSync(join(dir, lf)));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 从 local.yaml 提取 modules 块的 path 列表(generic monorepo 子模块路径)。
|
|
234
|
+
* 兼容 inline flow(`frontend: { path: "frontend/" }`)与 block(`frontend:\n path: frontend/`)。
|
|
235
|
+
* 轻量正则,不引 yaml 依赖(与 extractUserInstall / scan-postcheck 风格一致)。
|
|
236
|
+
* 坑 execute-worktree-pnpm-monorepo-no-node-modules:generic 项目 worktree 子模块无 node_modules。
|
|
237
|
+
* @returns {string[]} 去重 + 去 trailing 斜杠的相对路径
|
|
238
|
+
*/
|
|
239
|
+
function extractModulePaths(yamlText) {
|
|
240
|
+
if (!yamlText) return [];
|
|
241
|
+
const idx = yamlText.search(/^modules:\s*$/m);
|
|
242
|
+
if (idx < 0) return [];
|
|
243
|
+
const nlIdx = yamlText.indexOf('\n', idx);
|
|
244
|
+
if (nlIdx < 0) return [];
|
|
245
|
+
let rest = yamlText.slice(nlIdx + 1);
|
|
246
|
+
const nextTop = rest.search(/^\S/m); // 下一个顶层 key(行首非空白)
|
|
247
|
+
const block = nextTop >= 0 ? rest.slice(0, nextTop) : rest;
|
|
248
|
+
const paths = [];
|
|
249
|
+
const re = /path:\s*["']?([^"'\s,}]+)/g;
|
|
250
|
+
let pm;
|
|
251
|
+
while ((pm = re.exec(block)) !== null) {
|
|
252
|
+
const p = pm[1].replace(/\/+$/, '').trim();
|
|
253
|
+
if (p && p !== '.') paths.push(p);
|
|
254
|
+
}
|
|
255
|
+
return [...new Set(paths)];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* modules 块 path 安全校验(体检 SEC-07):只允许 worktree 内的相对子路径——
|
|
260
|
+
* 拒绝绝对路径与 `..` 段,防止 local.yaml 配置把 junction/symlink 建到 worktree 之外。
|
|
261
|
+
*/
|
|
262
|
+
function isSafeModulePath(p) {
|
|
263
|
+
if (!p || isAbsolute(p)) return false;
|
|
264
|
+
const segs = p.split(/[\\/]+/);
|
|
265
|
+
return !segs.includes('..');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 对单个子模块目录 tryLink main 的 node_modules → wt 的 node_modules(modules 子模块专用)。
|
|
270
|
+
* 仅走 link 快路径(不 install——子模块 install 慢且易失败;lockfile 不一致时交给用户 pnpm install)。
|
|
271
|
+
* lockfile 一致才 link,避免误链不匹配的 deps。
|
|
272
|
+
*/
|
|
273
|
+
function linkOneDir(wtDir, mainDir) {
|
|
274
|
+
const mainNodeModules = join(mainDir, 'node_modules');
|
|
275
|
+
if (!existsSync(mainNodeModules)) return { status: 'skipped', reason: 'main 无 node_modules' };
|
|
276
|
+
const wtHash = lockfileHash(wtDir);
|
|
277
|
+
const mainHash = lockfileHash(mainDir);
|
|
278
|
+
if (wtHash && mainHash && wtHash !== mainHash) {
|
|
279
|
+
return { status: 'mismatch', reason: `lockfile 不一致 main=${mainHash} wt=${wtHash}` };
|
|
280
|
+
}
|
|
281
|
+
const r = tryLink(mainNodeModules, join(wtDir, 'node_modules'));
|
|
282
|
+
if (r.ok) return { status: 'linked', method: r.method };
|
|
283
|
+
return { status: 'failed', error: r.error };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 供给依赖。返回 deps 状态对象(合并进 meta)。两段:
|
|
288
|
+
* 1. 根目录供给(原有逻辑:lockfile 一致 link / 不一致 install / generic→根 n/a)
|
|
289
|
+
* 2. modules 块的 nodejs 子模块 link(generic monorepo:根无 deps 但 frontend/daemon 等子模块需要)
|
|
290
|
+
* generic + 子模块 link 成功时,整体 depsStatus 从 n/a 升级为 linked(deps gate 不再误判 unknown 阻断)。
|
|
291
|
+
*
|
|
292
|
+
* @param {string} worktreePath - worktree 根目录
|
|
293
|
+
* @param {string} mainCwd - 主 checkout 根目录(node_modules 来源)
|
|
294
|
+
* @param {{ specBase?: string, timeout?: number, force?: boolean }} opts
|
|
295
|
+
* - force=true(doctor --fix 强制重装):绕过根目录 lockfile 一致快路径(下方 line 216 区块),
|
|
296
|
+
* 强制走 install 分支重装。该快路径是 tryLink 的唯一根调用点,绕过它即同时绕过 tryLink
|
|
297
|
+
* 的 preexisting 幂等短路(101-110),实现"强制重装"语义。配合调用方先解 junction
|
|
298
|
+
* (worktree.js _doctorReprovision),避免 install 经 junction 误改主仓 node_modules。
|
|
299
|
+
* modules 子模块 link 不受 force 影响(仅根 install)。
|
|
300
|
+
* @returns {{ depsStatus, depsMethod, depsSource, depsLockHash, depsCheckedAt, depsError?, depsModules? }}
|
|
301
|
+
*/
|
|
302
|
+
export function provisionDeps(worktreePath, mainCwd, opts = {}) {
|
|
303
|
+
const { specBase = null, timeout = DEFAULT_TIMEOUT_MS, force = false } = opts;
|
|
304
|
+
const depsCheckedAt = new Date().toISOString();
|
|
305
|
+
const { text: yamlText, source: yamlSource } = readLocalYamlSourced(specBase, worktreePath);
|
|
306
|
+
const wtHash = lockfileHash(worktreePath);
|
|
307
|
+
|
|
308
|
+
// ── 1. 根目录供给 ──
|
|
309
|
+
const projectType = detectProjectType(worktreePath, specBase);
|
|
310
|
+
// SEC-01:commands.install 只信主仓 specBase 的 local.yaml——worktree 副本是 agent 可写区,
|
|
311
|
+
// 其 install 命令若执行等于绕过 worktree-guard 的危险命令拦截
|
|
312
|
+
const userInstall = yamlSource === 'specBase' ? extractUserInstall(yamlText) : null;
|
|
313
|
+
if (yamlSource === 'worktree' && extractUserInstall(yamlText)) {
|
|
314
|
+
console.warn('⚠️ worktree 内 local.yaml 的 commands.install 被忽略(agent 可写区,只信主仓 .sillyspec/local.yaml)');
|
|
315
|
+
}
|
|
316
|
+
const installCmd = inferInstallCommand(projectType, worktreePath, userInstall);
|
|
317
|
+
|
|
318
|
+
let result;
|
|
319
|
+
if (!installCmd) {
|
|
320
|
+
// generic / 无可执行 install → 根无 deps 动作(n/a);但下方 modules 子模块仍可能 link 升级
|
|
321
|
+
result = { depsStatus: 'n/a', depsMethod: null, depsSource: null, depsLockHash: wtHash };
|
|
322
|
+
} else {
|
|
323
|
+
// 快路径:main 有 node_modules 且 lockfile hash 一致 → junction/symlink
|
|
324
|
+
// force=true 时绕过(doctor --fix 强制重装;绕过此块即同时绕过 tryLink 101-110 幂等短路)
|
|
325
|
+
const mainNodeModules = mainCwd ? join(mainCwd, 'node_modules') : null;
|
|
326
|
+
const mainHash = lockfileHash(mainCwd);
|
|
327
|
+
let linked = false;
|
|
328
|
+
if (!force && mainNodeModules && existsSync(mainNodeModules) && mainHash && wtHash && mainHash === wtHash) {
|
|
329
|
+
const linkResult = tryLink(mainNodeModules, join(worktreePath, 'node_modules'));
|
|
330
|
+
if (linkResult.ok) {
|
|
331
|
+
result = {
|
|
332
|
+
depsStatus: 'linked',
|
|
333
|
+
depsMethod: linkResult.method,
|
|
334
|
+
depsSource: linkResult.preexisting ? 'install' : 'main-checkout',
|
|
335
|
+
depsLockHash: wtHash,
|
|
336
|
+
};
|
|
337
|
+
linked = true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (!linked) {
|
|
341
|
+
// 兜底:install
|
|
342
|
+
const installResult = tryInstall(installCmd, worktreePath, timeout);
|
|
343
|
+
result = installResult.ok
|
|
344
|
+
? { depsStatus: 'installed', depsMethod: 'install', depsSource: 'install', depsLockHash: wtHash }
|
|
345
|
+
: { depsStatus: 'failed', depsMethod: null, depsSource: null, depsLockHash: wtHash, depsError: installResult.error };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
result.depsCheckedAt = depsCheckedAt;
|
|
349
|
+
|
|
350
|
+
// ── 2. modules 块的 nodejs 子模块 link(坑 execute-worktree-pnpm-monorepo-no-node-modules)──
|
|
351
|
+
// generic monorepo(如 multi-agent-platform)worktree 子模块 frontend/sillyhub-daemon 无 node_modules,
|
|
352
|
+
// 跑 pnpm test 失败。读 local.yaml modules 块,对 nodejs 子模块 tryLink main 的 node_modules。
|
|
353
|
+
const modulePaths = extractModulePaths(yamlText);
|
|
354
|
+
const moduleResults = [];
|
|
355
|
+
for (const mp of modulePaths) {
|
|
356
|
+
if (!isSafeModulePath(mp)) {
|
|
357
|
+
moduleResults.push({ path: mp, status: 'skipped', reason: 'path 越界(绝对路径或 .. 段),拒绝 link' });
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const wtDir = join(worktreePath, mp);
|
|
361
|
+
const mainDir = mainCwd ? join(mainCwd, mp) : null;
|
|
362
|
+
if (!existsSync(wtDir) || !mainDir || !hasNodeMarker(wtDir)) continue;
|
|
363
|
+
moduleResults.push({ path: mp, ...linkOneDir(wtDir, mainDir) });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (moduleResults.length > 0) {
|
|
367
|
+
result.depsModules = moduleResults;
|
|
368
|
+
const linkedModules = moduleResults.filter(m => m.status === 'linked');
|
|
369
|
+
if (linkedModules.length > 0 && result.depsStatus === 'n/a') {
|
|
370
|
+
// 标 generic 但子模块 link 成功 → 升级为 linked(deps gate 不再误判 unknown 阻断 execute)
|
|
371
|
+
result.depsStatus = 'linked';
|
|
372
|
+
result.depsMethod = linkedModules[0].method;
|
|
373
|
+
result.depsSource = 'main-checkout';
|
|
374
|
+
}
|
|
375
|
+
const failedModules = moduleResults.filter(m => m.status === 'failed');
|
|
376
|
+
if (failedModules.length > 0) {
|
|
377
|
+
const fmsg = failedModules.map(m => `${m.path}: ${m.error}`).join('; ');
|
|
378
|
+
result.depsError = result.depsError ? `${result.depsError}; ${fmsg}` : fmsg;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 统一的 worktree deps 新鲜度判定(H1,change 2026-08-05-tooling-feedback-fixes)。
|
|
387
|
+
* 供 doctor(worktree.js)与 ensureDepsFreshness(run/stage.js)共用,消除两处双写漂移。
|
|
388
|
+
*
|
|
389
|
+
* 判定优先级:failed → missing → stale → main-drift → fresh
|
|
390
|
+
* - failed: meta.depsStatus==='failed'(上次供给失败,占最高优先级)
|
|
391
|
+
* - missing: depsStatus ∈ {linked, installed} 且 node_modules 不存在
|
|
392
|
+
* - stale: wtHash 与 meta.depsLockHash 不一致(worktree 自身 lockfile 在上次供给后变化)
|
|
393
|
+
* - main-drift: wtHash 与主仓 mainHash 不一致(主仓 lockfile 漂移;复用 linkOneDir 177-178 的 mismatch 判据)
|
|
394
|
+
* - fresh: 其余情况
|
|
395
|
+
*
|
|
396
|
+
* lockfileHash 返回 null(无 lockfile/package.json)时相关比较优雅降级——不报 stale / main-drift,
|
|
397
|
+
* 避免无 lockfile 项目(如纯 generic / 非 nodejs)误判漂移。
|
|
398
|
+
*
|
|
399
|
+
* @param {object} [meta] - worktree meta(读 depsStatus / depsLockHash / depsError)
|
|
400
|
+
* @param {string} wtPath - worktree 根目录
|
|
401
|
+
* @param {string} mainCwd - 主 checkout 根目录
|
|
402
|
+
* @returns {{ status: 'fresh'|'missing'|'stale'|'main-drift'|'failed', detail: string, wtHash: (string|null), mainHash: (string|null), metaLockHash: (string|null) }}
|
|
403
|
+
*/
|
|
404
|
+
export function checkDepsFreshness(meta, wtPath, mainCwd) {
|
|
405
|
+
const depsStatus = meta && meta.depsStatus ? meta.depsStatus : null;
|
|
406
|
+
const metaLockHash = meta && meta.depsLockHash ? meta.depsLockHash : null;
|
|
407
|
+
const wtHash = lockfileHash(wtPath);
|
|
408
|
+
const mainHash = lockfileHash(mainCwd);
|
|
409
|
+
|
|
410
|
+
// 1. failed 最高优先级(对齐 task-01 蓝图:failed → missing → stale → main-drift → fresh)
|
|
411
|
+
if (depsStatus === 'failed') {
|
|
412
|
+
return {
|
|
413
|
+
status: 'failed',
|
|
414
|
+
detail: '上次依赖供给失败' + (meta && meta.depsError ? ': ' + meta.depsError : ''),
|
|
415
|
+
wtHash, mainHash, metaLockHash,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 2. missing:曾 link/install 但 node_modules 已丢失(对齐 doctor 914-915 / ensure 403)
|
|
420
|
+
const nmExists = wtPath ? existsSync(join(wtPath, 'node_modules')) : false;
|
|
421
|
+
if (['linked', 'installed'].includes(depsStatus) && !nmExists) {
|
|
422
|
+
return {
|
|
423
|
+
status: 'missing',
|
|
424
|
+
detail: `meta.depsStatus=${depsStatus} 但 node_modules 缺失`,
|
|
425
|
+
wtHash, mainHash, metaLockHash,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 3. stale:worktree 自身 lockfile 与 meta 快照不一致(对齐 doctor 916-917 / ensure 404)
|
|
430
|
+
if (metaLockHash && wtHash && wtHash !== metaLockHash) {
|
|
431
|
+
return {
|
|
432
|
+
status: 'stale',
|
|
433
|
+
detail: `lockfile 变化 (${metaLockHash} -> ${wtHash})`,
|
|
434
|
+
wtHash, mainHash, metaLockHash,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 4. main-drift(新增):wtHash 与主仓 mainHash 不一致——主仓 lockfile 更新过、worktree 未跟
|
|
439
|
+
if (wtHash && mainHash && wtHash !== mainHash) {
|
|
440
|
+
return {
|
|
441
|
+
status: 'main-drift',
|
|
442
|
+
detail: `worktree lockfile 与主仓不一致 (wt=${wtHash} main=${mainHash})`,
|
|
443
|
+
wtHash, mainHash, metaLockHash,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 5. fresh
|
|
448
|
+
return {
|
|
449
|
+
status: 'fresh',
|
|
450
|
+
detail: '依赖新鲜',
|
|
451
|
+
wtHash, mainHash, metaLockHash,
|
|
452
|
+
};
|
|
453
|
+
}
|