sillyspec 3.27.7 → 3.27.9
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/package.json +1 -1
- package/src/contract-matrix.js +39 -11
- package/src/index.js +25 -0
- package/src/progress/stage-machine.js +3 -1
- package/src/run/command.js +46 -15
- package/src/run/complete-handlers.js +20 -0
- package/src/run/complete.js +10 -1
- package/src/run/gates.js +38 -3
- package/src/run/multi-repo-context.js +42 -4
- package/src/run/shared.js +1 -1
- package/src/run/stage.js +28 -2
- package/src/stage-contract.js +20 -7
- package/src/stages/execute.js +35 -15
- package/src/stages/plan.js +8 -0
- package/src/task-review.js +7 -0
- package/src/worktree-apply.js +196 -4
- package/src/worktree-cross.js +294 -0
- package/src/worktree-deps.js +156 -58
package/src/worktree-deps.js
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 在 worktree.create() 的 baseline overlay 之后调用,让 worktree 立即可构建/测试。
|
|
5
5
|
* 策略:junction/symlink 快路径(lockfile 一致)+ install 兜底;多语言按 local.yaml
|
|
6
|
-
* project.type +
|
|
6
|
+
* project.type + ECOSYSTEMS 清单表推断 install 命令。供给可失败,但状态写进 meta 供验证硬门读取。
|
|
7
7
|
*
|
|
8
8
|
* 见 change 2026-06-28-worktree-deps-provision / D-005@v1, D-007@v1。
|
|
9
|
+
*
|
|
10
|
+
* 生态中立(坑 deps-ecosystem-hardcode,2026-08-27):本模块所有「按语言判断」一律走下方
|
|
11
|
+
* ECOSYSTEMS 清单表(单一事实源),不再散落 nodejs 特判——此前 lockfileHash 只认 nodejs
|
|
12
|
+
* 清单、missing 只认 node_modules、maven/gradle 无清单基准(pom.xml 改了不触发重供给)、
|
|
13
|
+
* node 校验与 java 校验各说各话。新生态补一行表项即可;表外项目落 generic → n/a 不阻断。
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
import { existsSync, readFileSync, realpathSync, lstatSync, readdirSync, unlinkSync } from 'fs';
|
|
@@ -18,21 +23,117 @@ const LOCKFILES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock'];
|
|
|
18
23
|
const DEFAULT_TIMEOUT_MS = 300 * 1000;
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
26
|
+
* 生态清单表——deps 链路「按语言判断」的单一事实源。每项声明:
|
|
27
|
+
* type: project.type 值(local.yaml 可显式声明覆盖探测)
|
|
28
|
+
* detect: 文件特征探测(表序即优先级;polyglot 目录按先命中者定性,hash 基准与 install
|
|
29
|
+
* 同取该生态——检测与供给/新鲜度始终同生态,不自相矛盾)
|
|
30
|
+
* manifests: 依赖清单文件(按优先级取首个存在者作 freshness 的 hash 基准:清单变 = 依赖集变
|
|
31
|
+
* = stale 触发重供给。nodejs 原以 lockfile/package.json 充当;maven/gradle/python
|
|
32
|
+
* 原先无基准 → pom.xml 改了也不触发,正是「node 校验的和 java 的不一样」的病根)
|
|
33
|
+
* marker: depsStatus=installed/linked 后应存在的 worktree 本地产物(missing 判定)。
|
|
34
|
+
* null = 产物在用户级仓库(maven ~/.m2、gradle ~/.gradle)或安装方式两可
|
|
35
|
+
* (python:uv sync 建 .venv、pip 装进当前环境),无 worktree 内统一标记,
|
|
36
|
+
* freshness 只信 depsStatus + 清单 hash——不重蹈 non-nodejs-missing-misjudge
|
|
37
|
+
* install: 无 commands.install 时的默认命令(null = 无默认,根供给 n/a)
|
|
38
|
+
*/
|
|
39
|
+
const ECOSYSTEMS = [
|
|
40
|
+
{
|
|
41
|
+
type: 'maven',
|
|
42
|
+
manifests: ['pom.xml'],
|
|
43
|
+
marker: null,
|
|
44
|
+
detect: (d) => existsSync(join(d, 'pom.xml')),
|
|
45
|
+
install: () => 'mvn -o test',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'gradle',
|
|
49
|
+
manifests: ['build.gradle.kts', 'build.gradle', 'settings.gradle', 'gradle.lockfile'],
|
|
50
|
+
marker: null,
|
|
51
|
+
detect: (d) => existsSync(join(d, 'build.gradle')) || existsSync(join(d, 'build.gradle.kts')),
|
|
52
|
+
// win32 cmd.exe 跑不了 ./gradlew(体检 BUG-08 同型):优先 gradlew.bat,否则全局 gradle
|
|
53
|
+
install: (d) => (process.platform === 'win32'
|
|
54
|
+
? (existsSync(join(d, 'gradlew.bat')) ? 'gradlew.bat test' : 'gradle test')
|
|
55
|
+
: './gradlew test'),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'nodejs',
|
|
59
|
+
manifests: ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'package.json'],
|
|
60
|
+
marker: 'node_modules',
|
|
61
|
+
// 探测含 LOCKFILES(与 hasNodeMarker 同口径):只有 lockfile 没有 package.json 的目录
|
|
62
|
+
//(子包/残缺 checkout)也是 nodejs——missing 护栏对它们同样生效
|
|
63
|
+
detect: (d) => existsSync(join(d, 'package.json')) || LOCKFILES.some(lf => existsSync(join(d, lf))),
|
|
64
|
+
install: (d) => {
|
|
65
|
+
if (existsSync(join(d, 'pnpm-lock.yaml'))) return 'pnpm install --frozen-lockfile';
|
|
66
|
+
if (existsSync(join(d, 'package-lock.json'))) return 'npm ci';
|
|
67
|
+
if (existsSync(join(d, 'yarn.lock'))) return 'yarn install --frozen-lockfile';
|
|
68
|
+
return 'npm install'; // 无 lockfile 兜底(X-2)
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'python',
|
|
73
|
+
manifests: ['uv.lock', 'poetry.lock', 'requirements.txt', 'pyproject.toml'],
|
|
74
|
+
marker: null,
|
|
75
|
+
detect: (d) => existsSync(join(d, 'pyproject.toml')) || existsSync(join(d, 'requirements.txt')),
|
|
76
|
+
// uv 优先(现代 Python 工具链,pyproject/uv.lock 项目走 uv sync 建 .venv + 装依赖,
|
|
77
|
+
// 与 execute worktree 环境预告一致);纯 requirements.txt(无 pyproject)回退 pip
|
|
78
|
+
install: (d) => (existsSync(join(d, 'uv.lock')) || existsSync(join(d, 'pyproject.toml')) ? 'uv sync' : 'pip install -r requirements.txt'),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'go',
|
|
82
|
+
manifests: ['go.mod', 'go.sum'],
|
|
83
|
+
marker: null,
|
|
84
|
+
detect: (d) => existsSync(join(d, 'go.mod')),
|
|
85
|
+
install: () => 'go mod download',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
type: 'rust',
|
|
89
|
+
manifests: ['Cargo.lock', 'Cargo.toml'],
|
|
90
|
+
marker: null,
|
|
91
|
+
detect: (d) => existsSync(join(d, 'Cargo.toml')),
|
|
92
|
+
install: () => 'cargo fetch --locked',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: 'php',
|
|
96
|
+
manifests: ['composer.lock', 'composer.json'],
|
|
97
|
+
marker: null,
|
|
98
|
+
detect: (d) => existsSync(join(d, 'composer.json')),
|
|
99
|
+
install: () => 'composer install',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: 'ruby',
|
|
103
|
+
manifests: ['Gemfile.lock', 'Gemfile'],
|
|
104
|
+
marker: null,
|
|
105
|
+
detect: (d) => existsSync(join(d, 'Gemfile')),
|
|
106
|
+
install: () => 'bundle install',
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
/** 目录命中的生态表项(表序优先;无命中 → null = generic)。polyglot 目录取先命中生态。 */
|
|
111
|
+
export function detectEcosystem(dir) {
|
|
112
|
+
if (!dir || !existsSync(dir)) return null;
|
|
113
|
+
for (const eco of ECOSYSTEMS) {
|
|
114
|
+
if (eco.detect(dir)) return eco;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 取目录下首个命中的生态清单文件的 sha256 前 16 位(全生态:pnpm-lock/yarn.lock/pom.xml/
|
|
121
|
+
* go.mod/Cargo.lock/composer.lock/Gemfile.lock…按 ECOSYSTEMS 表序);无任何清单返回 null。
|
|
122
|
+
* 函数名保留 lockfileHash(历史导出名,worktree.js/modules 链接一致性判定等多处消费),
|
|
123
|
+
* 语义自 2026-08-27 起泛化为「依赖清单 hash」——maven 的 pom.xml 等与 nodejs 的 lockfile
|
|
124
|
+
* 同权作 stale/main-drift 基准。polyglot 目录首次泛化后基准可能从 package.json 切到先命中
|
|
125
|
+
* 生态的清单(一次性 stale 重供给,自愈后以新基准续跑)。
|
|
23
126
|
*/
|
|
24
127
|
export function lockfileHash(dir) {
|
|
25
128
|
if (!dir || !existsSync(dir)) return null;
|
|
26
|
-
for (const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
129
|
+
for (const eco of ECOSYSTEMS) {
|
|
130
|
+
for (const mf of eco.manifests) {
|
|
131
|
+
const p = join(dir, mf);
|
|
132
|
+
if (existsSync(p)) {
|
|
133
|
+
return createHash('sha256').update(readFileSync(p)).digest('hex').slice(0, 16);
|
|
134
|
+
}
|
|
30
135
|
}
|
|
31
136
|
}
|
|
32
|
-
const pkg = join(dir, 'package.json');
|
|
33
|
-
if (existsSync(pkg)) {
|
|
34
|
-
return createHash('sha256').update(readFileSync(pkg)).digest('hex').slice(0, 16);
|
|
35
|
-
}
|
|
36
137
|
return null;
|
|
37
138
|
}
|
|
38
139
|
|
|
@@ -78,47 +179,24 @@ function extractUserInstall(yamlText) {
|
|
|
78
179
|
return null;
|
|
79
180
|
}
|
|
80
181
|
|
|
81
|
-
/** 从 local.yaml 提取 project.type
|
|
182
|
+
/** 从 local.yaml 提取 project.type;缺失时按 ECOSYSTEMS 表文件特征推断(表序优先) */
|
|
82
183
|
export function detectProjectType(worktreePath, specBase) {
|
|
83
184
|
const yamlText = readLocalYaml(specBase, worktreePath);
|
|
84
185
|
if (yamlText) {
|
|
85
186
|
const m = yamlText.match(/type:\s*(\S+)/);
|
|
86
187
|
if (m && m[1]) return m[1];
|
|
87
188
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (existsSync(join(worktreePath, 'package.json'))) return 'nodejs';
|
|
91
|
-
// Python:pyproject.toml(uv/poetry/pdm)或 requirements.txt(pip)——治 worktree 内 ruff/pre-commit 等
|
|
92
|
-
// 二进制不供给(exec 复盘 3-②:detectProjectType 原无 python 分支,python 项目根被误判 generic → n/a)
|
|
93
|
-
if (existsSync(join(worktreePath, 'pyproject.toml')) || existsSync(join(worktreePath, 'requirements.txt'))) return 'python';
|
|
94
|
-
return 'generic';
|
|
189
|
+
const eco = detectEcosystem(worktreePath);
|
|
190
|
+
return eco ? eco.type : 'generic';
|
|
95
191
|
}
|
|
96
192
|
|
|
97
|
-
/** 按 project.type
|
|
193
|
+
/** 按 project.type(+ 表内 install 探测)推断 install 命令(无 commands.install 时)。
|
|
194
|
+
* 表外 project.type(含 local.yaml 显式声明的自定义值)→ null → 根供给 n/a,
|
|
195
|
+
* 需要执行的装法在 local.yaml commands.install 显式声明(仍过白名单+元字符门)。 */
|
|
98
196
|
export function inferInstallCommand(projectType, worktreePath, userInstall) {
|
|
99
197
|
if (userInstall) return userInstall;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (existsSync(join(worktreePath, 'pnpm-lock.yaml'))) return 'pnpm install --frozen-lockfile';
|
|
103
|
-
if (existsSync(join(worktreePath, 'package-lock.json'))) return 'npm ci';
|
|
104
|
-
if (existsSync(join(worktreePath, 'yarn.lock'))) return 'yarn install --frozen-lockfile';
|
|
105
|
-
return 'npm install'; // 无 lockfile 兜底(X-2)
|
|
106
|
-
case 'maven':
|
|
107
|
-
return 'mvn -o test';
|
|
108
|
-
case 'gradle':
|
|
109
|
-
// win32 cmd.exe 跑不了 ./gradlew(体检 BUG-08 同型):优先 gradlew.bat,否则全局 gradle
|
|
110
|
-
if (process.platform === 'win32') {
|
|
111
|
-
return existsSync(join(worktreePath, 'gradlew.bat')) ? 'gradlew.bat test' : 'gradle test';
|
|
112
|
-
}
|
|
113
|
-
return './gradlew test';
|
|
114
|
-
case 'python':
|
|
115
|
-
// uv 优先(现代 Python 工具链,pyproject.toml/uv.lock 项目走 uv sync 建 .venv + 装依赖,
|
|
116
|
-
// 与 execute worktree 环境预告一致);纯 requirements.txt(无 pyproject)回退 pip
|
|
117
|
-
if (existsSync(join(worktreePath, 'uv.lock')) || existsSync(join(worktreePath, 'pyproject.toml'))) return 'uv sync';
|
|
118
|
-
return 'pip install -r requirements.txt';
|
|
119
|
-
default:
|
|
120
|
-
return null; // generic → n/a
|
|
121
|
-
}
|
|
198
|
+
const eco = ECOSYSTEMS.find(e => e.type === projectType);
|
|
199
|
+
return eco ? eco.install(worktreePath) : null;
|
|
122
200
|
}
|
|
123
201
|
|
|
124
202
|
/** 在 worktreePath 创建 node_modules 链接到 mainNodeModules;失败回退 */
|
|
@@ -202,7 +280,7 @@ function tryLink(mainNodeModules, linkPath) {
|
|
|
202
280
|
* 全程 argv 数组执行不经 shell、任一段失败即停(&& 语义)。安全面不变:每个执行段仍是
|
|
203
281
|
* 白名单包管理器命令,`||`/管道/`;`/后台均不支持。
|
|
204
282
|
*/
|
|
205
|
-
const INSTALL_BINARY_WHITELIST = /^(?:\.\/)?(?:pnpm|npm|yarn|bun|mvn|gradle|gradlew|uv|pip|pip3|python|python3|poetry|make)(?:\.cmd|\.bat|\.exe)?(?:\s|$)/;
|
|
283
|
+
const INSTALL_BINARY_WHITELIST = /^(?:\.\/)?(?:pnpm|npm|yarn|bun|mvn|mvnd|gradle|gradlew|uv|pip|pip3|python|python3|poetry|make|go|cargo|composer|bundle|dotnet)(?:\.cmd|\.bat|\.exe)?(?:\s|$)/;
|
|
206
284
|
const SHELL_METACHARS = /[;&|<>$`\n]/;
|
|
207
285
|
const WIN_SHELL_METACHARS = /[;&|<>$`\n%]/;
|
|
208
286
|
const CD_SEGMENT_RE = /^cd\s+([^\s]+)$/;
|
|
@@ -252,8 +330,15 @@ function tryInstall(cmd, cwd, timeout) {
|
|
|
252
330
|
execFileSync(argv[0], argv.slice(1), { cwd: curCwd, timeout, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
253
331
|
}
|
|
254
332
|
} catch (e) {
|
|
255
|
-
const
|
|
256
|
-
|
|
333
|
+
const raw = e.killed ? `timeout after ${timeout}ms` : ((e.stderr && e.stderr.toString()) || e.message);
|
|
334
|
+
// 命令找不到 → 追加 PATH 定向指引(坑 deps-tool-not-on-path-failed-loop,2026-08-27 实证):
|
|
335
|
+
// CLI 子进程继承调用方 shell 的 PATH,无 mvn/gradle 等工具链的环境里 install 必败,裸报
|
|
336
|
+
// "'mvn' 不是内部或外部命令" 让人误以为是项目配置问题。指引换环境重跑 doctor --fix。
|
|
337
|
+
const notFoundRe = /is not recognized|不是内部或外部命令|command not found|no such file or directory|ENOENT/i;
|
|
338
|
+
const hint = (!e.killed && notFoundRe.test(raw))
|
|
339
|
+
? `(工具 ${argv[0]} 不在当前 shell 的 PATH——CLI 子进程继承调用方环境。请在含该工具链的环境重跑 sillyspec worktree doctor --fix --change <变更名>,或把工具目录加入 PATH 后重试)`
|
|
340
|
+
: '';
|
|
341
|
+
return { ok: false, error: `${seg} failed (cwd ${curCwd}): ${raw}${hint}` };
|
|
257
342
|
}
|
|
258
343
|
}
|
|
259
344
|
return { ok: true };
|
|
@@ -491,15 +576,21 @@ export function provisionDeps(worktreePath, mainCwd, opts = {}) {
|
|
|
491
576
|
* 统一的 worktree deps 新鲜度判定(H1,change 2026-08-05-tooling-feedback-fixes)。
|
|
492
577
|
* 供 doctor(worktree.js)与 ensureDepsFreshness(run/stage.js)共用,消除两处双写漂移。
|
|
493
578
|
*
|
|
579
|
+
* 生态中立(坑 deps-ecosystem-hardcode,2026-08-27):判定基准全部来自 ECOSYSTEMS 清单表,
|
|
580
|
+
* 各生态同一套语义——不再有「node 一套校验、java 另一套」:
|
|
581
|
+
*
|
|
494
582
|
* 判定优先级:failed → missing → stale → main-drift → fresh
|
|
495
583
|
* - failed: meta.depsStatus==='failed'(上次供给失败,占最高优先级)
|
|
496
|
-
* - missing: depsStatus ∈ {linked, installed}
|
|
497
|
-
*
|
|
498
|
-
*
|
|
584
|
+
* - missing: depsStatus ∈ {linked, installed} 且生态 marker(如 nodejs 的 node_modules)
|
|
585
|
+
* 不存在。仅对声明了 marker 的生态生效(nodejs);maven/gradle/python 等产物在用户级
|
|
586
|
+
* 仓库或安装方式两可(表内 marker=null),只信 depsStatus + 清单 hash
|
|
587
|
+
* - stale: wtHash 与 meta.depsLockHash 不一致(worktree 自身依赖清单在上次供给后变化——
|
|
588
|
+
* nodejs 的 lockfile/package.json、maven 的 pom.xml、go 的 go.mod 等同权)
|
|
589
|
+
* - main-drift: wtHash 与主仓 mainHash 不一致(主仓清单漂移;复用 linkOneDir 的 mismatch 判据)
|
|
499
590
|
* - fresh: 其余情况
|
|
500
591
|
*
|
|
501
|
-
* lockfileHash 返回 null
|
|
502
|
-
*
|
|
592
|
+
* lockfileHash 返回 null(目录内无任何生态清单文件)时相关比较优雅降级——不报 stale /
|
|
593
|
+
* main-drift,避免无清单项目(如纯 generic)误判漂移。
|
|
503
594
|
*
|
|
504
595
|
* @param {object} [meta] - worktree meta(读 depsStatus / depsLockHash / depsError)
|
|
505
596
|
* @param {string} wtPath - worktree 根目录
|
|
@@ -521,12 +612,18 @@ export function checkDepsFreshness(meta, wtPath, mainCwd) {
|
|
|
521
612
|
};
|
|
522
613
|
}
|
|
523
614
|
|
|
524
|
-
// 2. missing:曾 link/install
|
|
525
|
-
|
|
526
|
-
|
|
615
|
+
// 2. missing:曾 link/install 但生态 marker 已丢失(对齐 doctor 914-915 / ensure 403)。
|
|
616
|
+
// marker 来自 ECOSYSTEMS 表:nodejs → node_modules。marker=null 的生态(maven/gradle/python…)
|
|
617
|
+
// 产物在 ~/.m2、~/.gradle 或安装方式两可,worktree 内无统一标记——此前无此门会把这类项目
|
|
618
|
+
// 每次 execute 入口都误判 missing → 重跑 install → 无工具链 PATH 的 shell 里失败 →
|
|
619
|
+
// depsStatus 被打回 failed → deps 门控阻断 --done(坑 non-nodejs-missing-misjudge,2026-08-27
|
|
620
|
+
// maven 项目实证)。
|
|
621
|
+
const eco = detectEcosystem(wtPath);
|
|
622
|
+
const marker = eco ? eco.marker : null;
|
|
623
|
+
if (marker && ['linked', 'installed'].includes(depsStatus) && wtPath && !existsSync(join(wtPath, marker))) {
|
|
527
624
|
return {
|
|
528
625
|
status: 'missing',
|
|
529
|
-
detail: `meta.depsStatus=${depsStatus} 但
|
|
626
|
+
detail: `meta.depsStatus=${depsStatus} 但 ${marker} 缺失(${eco.type} 生态就绪标记)`,
|
|
530
627
|
wtHash, mainHash, metaLockHash,
|
|
531
628
|
};
|
|
532
629
|
}
|
|
@@ -550,20 +647,21 @@ export function checkDepsFreshness(meta, wtPath, mainCwd) {
|
|
|
550
647
|
}
|
|
551
648
|
}
|
|
552
649
|
|
|
553
|
-
// 3. stale:worktree
|
|
650
|
+
// 3. stale:worktree 自身依赖清单与 meta 快照不一致(对齐 doctor 916-917 / ensure 404;
|
|
651
|
+
// 清单=ECOSYSTEMS 表内文件:nodejs lockfile/package.json、maven pom.xml、go go.mod 等同权)
|
|
554
652
|
if (metaLockHash && wtHash && wtHash !== metaLockHash) {
|
|
555
653
|
return {
|
|
556
654
|
status: 'stale',
|
|
557
|
-
detail:
|
|
655
|
+
detail: `依赖清单变化 (${metaLockHash} -> ${wtHash})`,
|
|
558
656
|
wtHash, mainHash, metaLockHash,
|
|
559
657
|
};
|
|
560
658
|
}
|
|
561
659
|
|
|
562
|
-
// 4. main-drift(新增):wtHash 与主仓 mainHash
|
|
660
|
+
// 4. main-drift(新增):wtHash 与主仓 mainHash 不一致——主仓依赖清单更新过、worktree 未跟
|
|
563
661
|
if (wtHash && mainHash && wtHash !== mainHash) {
|
|
564
662
|
return {
|
|
565
663
|
status: 'main-drift',
|
|
566
|
-
detail: `worktree
|
|
664
|
+
detail: `worktree 依赖清单与主仓不一致 (wt=${wtHash} main=${mainHash})`,
|
|
567
665
|
wtHash, mainHash, metaLockHash,
|
|
568
666
|
};
|
|
569
667
|
}
|