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.
@@ -3,9 +3,14 @@
3
3
  *
4
4
  * 在 worktree.create() 的 baseline overlay 之后调用,让 worktree 立即可构建/测试。
5
5
  * 策略:junction/symlink 快路径(lockfile 一致)+ install 兜底;多语言按 local.yaml
6
- * project.type + lockfile 推断 install 命令。供给可失败,但状态写进 meta 供验证硬门读取。
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
- * 取目录下首个命中的 lockfile 的 sha256 前 16 位;
22
- * 无 lockfile 则 hash package.json;都没有返回 null。
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 lf of LOCKFILES) {
27
- const p = join(dir, lf);
28
- if (existsSync(p)) {
29
- return createHash('sha256').update(readFileSync(p)).digest('hex').slice(0, 16);
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
- if (existsSync(join(worktreePath, 'pom.xml'))) return 'maven';
89
- if (existsSync(join(worktreePath, 'build.gradle')) || existsSync(join(worktreePath, 'build.gradle.kts'))) return 'gradle';
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 + lockfile 推断 install 命令(无 commands.install 时)*/
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
- switch (projectType) {
101
- case 'nodejs':
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 msg = e.killed ? `timeout after ${timeout}ms` : ((e.stderr && e.stderr.toString()) || e.message);
256
- return { ok: false, error: `${seg} failed (cwd ${curCwd}): ${msg}` };
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} 且 node_modules 不存在
497
- * - stale: wtHash 与 meta.depsLockHash 不一致(worktree 自身 lockfile 在上次供给后变化)
498
- * - main-drift: wtHash 与主仓 mainHash 不一致(主仓 lockfile 漂移;复用 linkOneDir 177-178 的 mismatch 判据)
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(无 lockfile/package.json)时相关比较优雅降级——不报 stale / main-drift,
502
- * 避免无 lockfile 项目(如纯 generic / 非 nodejs)误判漂移。
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 但 node_modules 已丢失(对齐 doctor 914-915 / ensure 403)
525
- const nmExists = wtPath ? existsSync(join(wtPath, 'node_modules')) : false;
526
- if (['linked', 'installed'].includes(depsStatus) && !nmExists) {
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} 但 node_modules 缺失`,
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 自身 lockfile 与 meta 快照不一致(对齐 doctor 916-917 / ensure 404)
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: `lockfile 变化 (${metaLockHash} -> ${wtHash})`,
655
+ detail: `依赖清单变化 (${metaLockHash} -> ${wtHash})`,
558
656
  wtHash, mainHash, metaLockHash,
559
657
  };
560
658
  }
561
659
 
562
- // 4. main-drift(新增):wtHash 与主仓 mainHash 不一致——主仓 lockfile 更新过、worktree 未跟
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 lockfile 与主仓不一致 (wt=${wtHash} main=${mainHash})`,
664
+ detail: `worktree 依赖清单与主仓不一致 (wt=${wtHash} main=${mainHash})`,
567
665
  wtHash, mainHash, metaLockHash,
568
666
  };
569
667
  }