sillyspec 3.26.11 → 3.26.14
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 +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +5 -5
- package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
- package/README.md +7 -5
- package/SKILL.md +5 -4
- package/package.json +1 -1
- package/packages/dashboard/server/executor.js +10 -1
- package/packages/dashboard/server/index.js +108 -46
- package/src/config-schema.js +3 -0
- package/src/contract-matrix.js +22 -13
- package/src/db.js +24 -4
- package/src/dispatch/probe.js +99 -63
- package/src/docs-check.js +58 -13
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +208 -21
- package/src/fs-atomic.js +3 -1
- package/src/git-helper.js +7 -4
- package/src/hooks/worktree-guard.js +8 -2
- package/src/index.js +200 -27
- package/src/init.js +3 -2
- package/src/local-detect.js +9 -1
- package/src/modules.js +7 -5
- package/src/progress/stage-machine.js +589 -578
- package/src/progress/step-store.js +1 -1
- package/src/progress.js +187 -30
- package/src/quicklog.js +47 -16
- package/src/run/command.js +88 -46
- package/src/run/complete-handlers.js +27 -15
- package/src/run/complete.js +1186 -1121
- package/src/run/concurrent-detect.js +3 -1
- package/src/run/gates.js +172 -25
- package/src/run/prompt.js +17 -9
- package/src/run/scan-profile.js +35 -23
- package/src/run/shared.js +164 -63
- package/src/run/stage.js +41 -1
- package/src/setup.js +8 -10
- package/src/sillyhub-mcp/client.js +74 -6
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +43 -8
- package/src/stage-contract.js +12 -38
- package/src/stage-review.js +40 -1
- package/src/stages/archive.js +4 -3
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +27 -13
- package/src/stages/doctor.js +29 -16
- package/src/stages/execute.js +170 -154
- package/src/stages/knowledge.js +18 -3
- package/src/stages/plan-postcheck.js +72 -37
- package/src/stages/plan.js +70 -48
- package/src/stages/quick.js +3 -1
- package/src/stages/verify.js +1 -1
- package/src/sync.js +339 -225
- package/src/task-review.js +281 -64
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +48 -35
- package/src/worktree-apply.js +58 -34
- package/src/worktree-deps.js +120 -20
- package/src/worktree.js +55 -9
- package/.npmrc-publish +0 -2
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* writer 落盘到 <authoritySpecDir>/.runtime/。
|
|
22
22
|
*/
|
|
23
23
|
import { openDatabase, pluckGet, pluckAll } from './db-engine.js';
|
|
24
|
-
import { existsSync, statSync, readFileSync, readdirSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
24
|
+
import { existsSync, statSync, readFileSync, readdirSync, mkdirSync, writeFileSync, unlinkSync, rmSync } from 'fs';
|
|
25
25
|
import { join } from 'path';
|
|
26
26
|
import { CHECK_SEVERITY } from './constants.js';
|
|
27
27
|
import { checkPlatformManaged } from './run/shared.js';
|
|
@@ -101,6 +101,14 @@ function probeDb(dbPath) {
|
|
|
101
101
|
schema_version: pick('SELECT schema_version FROM project LIMIT 1'),
|
|
102
102
|
change_count: pick('SELECT count(*) FROM changes', 0),
|
|
103
103
|
active_changes: pickCol("SELECT name FROM changes WHERE status='active'"),
|
|
104
|
+
// SS-2b(2026-08-20):active 行 name → last_active 映射,供 D4 空壳判定做时间门槛
|
|
105
|
+
//(刚创建的合法变更目录本来就是空的,见 GHOST_EMPTY_DIR_STALE_MS 注释)。
|
|
106
|
+
active_last_active: (() => {
|
|
107
|
+
try {
|
|
108
|
+
const rows = db.prepare("SELECT name, last_active FROM changes WHERE status='active'").all();
|
|
109
|
+
return Object.fromEntries(rows.map((r) => [r.name, r.last_active]));
|
|
110
|
+
} catch { return {}; }
|
|
111
|
+
})(),
|
|
104
112
|
last_active: pick('SELECT MAX(last_active) FROM changes'),
|
|
105
113
|
execute_status_by_change: pickExecuteStatusByChange(),
|
|
106
114
|
};
|
|
@@ -211,6 +219,10 @@ function detectMultiDb(cwd, pointer) {
|
|
|
211
219
|
change_count: d.change_count,
|
|
212
220
|
last_active: d.last_active,
|
|
213
221
|
active: d.readable ? summarizeActive(d.active_changes) : null,
|
|
222
|
+
// SS-2:全量名单随摘要透传,供 D4 精确集合对账(ghost/orphan)消费。
|
|
223
|
+
active_changes: d.readable ? (d.active_changes || []) : [],
|
|
224
|
+
// SS-2b:name → last_active 映射随摘要透传,供 D4 空壳判定(时间门槛)消费。
|
|
225
|
+
active_last_active: d.readable ? (d.active_last_active || {}) : {},
|
|
214
226
|
})),
|
|
215
227
|
};
|
|
216
228
|
}
|
|
@@ -359,6 +371,35 @@ function detectChangesSplit(cwd, pointer) {
|
|
|
359
371
|
|
|
360
372
|
// ── D4 change↔db 一致性(针对权威 db + 权威 changes 目录)──────────────
|
|
361
373
|
|
|
374
|
+
// SS-2b(2026-08-20):空壳目录判定的 last_active 时间门槛(7 天)。
|
|
375
|
+
// 背景:brainstorm 刚注册的合法变更目录本来就是 0 文件(首份产物 proposal.md
|
|
376
|
+
// 落盘前有空窗,2026-08-20 task-truth-unify 即空目录数小时)。「active 行 + 目录
|
|
377
|
+
// 0 文件」不能直接判幽灵,必须叠加 last_active 陈旧度;门槛取 7 天(quick 会话
|
|
378
|
+
// 生命周期通常当天结束,正规变更 brainstorm 首产物也远早于 7 天)。
|
|
379
|
+
const GHOST_EMPTY_DIR_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
380
|
+
|
|
381
|
+
/** 目录内是否 0 个常规文件(递归;空目录/仅空子目录都算 0 文件)。读失败按非空处理(宁漏勿杀)。 */
|
|
382
|
+
function dirHasNoFiles(dirPath) {
|
|
383
|
+
const stack = [dirPath];
|
|
384
|
+
while (stack.length > 0) {
|
|
385
|
+
const cur = stack.pop();
|
|
386
|
+
let entries;
|
|
387
|
+
try { entries = readdirSync(cur, { withFileTypes: true }); } catch { return false; }
|
|
388
|
+
for (const e of entries) {
|
|
389
|
+
if (e.isFile()) return false;
|
|
390
|
+
if (e.isDirectory()) stack.push(join(cur, e.name));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** last_active 是否陈旧超过 GHOST_EMPTY_DIR_STALE_MS。无值/不可解析按非陈旧处理(宁漏勿杀)。 */
|
|
397
|
+
function isStaleLastActive(lastActive, now = Date.now()) {
|
|
398
|
+
if (!lastActive) return false;
|
|
399
|
+
const ts = Date.parse(lastActive);
|
|
400
|
+
return Number.isFinite(ts) && (now - ts) > GHOST_EMPTY_DIR_STALE_MS;
|
|
401
|
+
}
|
|
402
|
+
|
|
362
403
|
function detectChangeDbConsistency(cwd, pointer, multiDb) {
|
|
363
404
|
const auth = (multiDb.dbs || []).find((d) => d.role === DB_ROLE.AUTHORITY);
|
|
364
405
|
if (!auth) {
|
|
@@ -373,25 +414,52 @@ function detectChangeDbConsistency(cwd, pointer, multiDb) {
|
|
|
373
414
|
}
|
|
374
415
|
const authoritySpecRoot = pointer.present && pointer.specRoot ? pointer.specRoot : join(cwd, '.sillyspec');
|
|
375
416
|
const dirChanges = listChanges(authoritySpecRoot);
|
|
376
|
-
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
417
|
+
// SS-2(2026-08-20):从 count 粗对齐升级为精确集合对账——ghost_rows(db active
|
|
418
|
+
// 无目录)与 orphan_dirs(目录无 db active 行)逐名列出,供 doctor --cleanup-ghosts
|
|
419
|
+
// 消费,不再让 52 条残留只停留在「需 dump 后细对齐」。
|
|
420
|
+
const activeNames = (auth.active_changes && Array.isArray(auth.active_changes))
|
|
421
|
+
? auth.active_changes
|
|
422
|
+
: [];
|
|
423
|
+
const dirSet = new Set(dirChanges);
|
|
424
|
+
const activeSet = new Set(activeNames);
|
|
425
|
+
const ghostRows = activeNames.filter((n) => !dirSet.has(n));
|
|
426
|
+
const orphanDirs = dirChanges.filter((n) => !activeSet.has(n));
|
|
427
|
+
// SS-2b:空壳——db active + 目录存在但 0 文件 + last_active 超过 7 天。有目录所以
|
|
428
|
+
// 逃过 ghostRows 的「无目录」判定(2026-08-15 清理时 6 个空壳即此漏网形态)。
|
|
429
|
+
const activeLastActive = (auth.active_last_active && typeof auth.active_last_active === 'object')
|
|
430
|
+
? auth.active_last_active
|
|
431
|
+
: {};
|
|
432
|
+
const emptyShells = activeNames.filter((n) =>
|
|
433
|
+
dirSet.has(n) && isStaleLastActive(activeLastActive[n]) && dirHasNoFiles(join(authoritySpecRoot, 'changes', n)));
|
|
382
434
|
const findings = [];
|
|
383
435
|
let pass = true;
|
|
384
436
|
let severity = null;
|
|
385
|
-
|
|
386
|
-
if (Math.abs(dirCount - dbCount) > Math.max(2, dirCount * 0.2)) {
|
|
437
|
+
if (ghostRows.length > 0) {
|
|
387
438
|
pass = false;
|
|
388
439
|
severity = CHECK_SEVERITY.WARNING;
|
|
389
440
|
findings.push(
|
|
390
|
-
|
|
391
|
-
|
|
441
|
+
`幽灵记录 ${ghostRows.length} 条(db active 但无目录):${ghostRows.slice(0, 10).join(', ')}${ghostRows.length > 10 ? ' …' : ''}——` +
|
|
442
|
+
`可用 sillyspec doctor --cleanup-ghosts --confirm 归档清理`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
if (emptyShells.length > 0) {
|
|
446
|
+
pass = false;
|
|
447
|
+
severity = severity || CHECK_SEVERITY.WARNING;
|
|
448
|
+
findings.push(
|
|
449
|
+
`空壳目录 ${emptyShells.length} 个(db active + 目录 0 文件 + last_active 超 7 天):${emptyShells.slice(0, 10).join(', ')}${emptyShells.length > 10 ? ' …' : ''}——` +
|
|
450
|
+
`可用 sillyspec doctor --cleanup-ghosts --confirm 归档行并移除空目录`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
if (orphanDirs.length > 0) {
|
|
454
|
+
pass = false;
|
|
455
|
+
severity = severity || CHECK_SEVERITY.WARNING;
|
|
456
|
+
findings.push(
|
|
457
|
+
`孤儿目录 ${orphanDirs.length} 个(目录存在但无 db active 行):${orphanDirs.slice(0, 10).join(', ')}${orphanDirs.length > 10 ? ' …' : ''}——` +
|
|
458
|
+
`先 dump 确认再手工归位,doctor 不自动删目录`
|
|
392
459
|
);
|
|
393
|
-
}
|
|
394
|
-
|
|
460
|
+
}
|
|
461
|
+
if (pass) {
|
|
462
|
+
findings.push(`权威 db active changes (${activeNames.length}) 与 changes/ 目录数 (${dirChanges.length}) 完全一致`);
|
|
395
463
|
}
|
|
396
464
|
return {
|
|
397
465
|
name: 'change_db_consistency',
|
|
@@ -399,10 +467,15 @@ function detectChangeDbConsistency(cwd, pointer, multiDb) {
|
|
|
399
467
|
pass,
|
|
400
468
|
severity,
|
|
401
469
|
findings,
|
|
402
|
-
safe_actions:
|
|
470
|
+
safe_actions: ghostRows.length > 0 || emptyShells.length > 0
|
|
471
|
+
? ['sillyspec doctor --cleanup-ghosts(dry-run)→ 加 --confirm 归档幽灵行/空壳目录']
|
|
472
|
+
: [],
|
|
403
473
|
authority_spec_root: authoritySpecRoot,
|
|
404
|
-
dir_count:
|
|
405
|
-
db_active_count:
|
|
474
|
+
dir_count: dirChanges.length,
|
|
475
|
+
db_active_count: activeNames.length,
|
|
476
|
+
ghost_rows: ghostRows,
|
|
477
|
+
orphan_dirs: orphanDirs,
|
|
478
|
+
empty_shells: emptyShells,
|
|
406
479
|
};
|
|
407
480
|
}
|
|
408
481
|
|
|
@@ -416,15 +489,17 @@ function detectChangeDbConsistency(cwd, pointer, multiDb) {
|
|
|
416
489
|
* @returns {{total:number, checked:number} | null} null = plan/tasks.md 都不存在或无 task 行
|
|
417
490
|
*/
|
|
418
491
|
function readPlanCheckboxStatus(changeDir) {
|
|
419
|
-
|
|
492
|
+
// 2026-08-20-task-truth-unify:tasks.md(任务注册表)优先;plan.md 纯 ID 引用行无 checkbox,
|
|
493
|
+
// 旧「plan 优先」顺序对新契约变更恒 null(D5 诊断静默致盲)。tasks.md 缺失回退 plan.md(旧变更兼容)。
|
|
420
494
|
const tasksPath = join(changeDir, 'tasks.md');
|
|
495
|
+
const planPath = join(changeDir, 'plan.md');
|
|
421
496
|
let content = null;
|
|
422
|
-
if (existsSync(
|
|
423
|
-
try { content = readFileSync(planPath, 'utf8'); } catch { content = null; }
|
|
424
|
-
}
|
|
425
|
-
if (content == null && existsSync(tasksPath)) {
|
|
497
|
+
if (existsSync(tasksPath)) {
|
|
426
498
|
try { content = readFileSync(tasksPath, 'utf8'); } catch { content = null; }
|
|
427
499
|
}
|
|
500
|
+
if (content == null && existsSync(planPath)) {
|
|
501
|
+
try { content = readFileSync(planPath, 'utf8'); } catch { content = null; }
|
|
502
|
+
}
|
|
428
503
|
if (content == null) return null;
|
|
429
504
|
// match both "- [ ] task-01: title" and "- [x] task-01: title"
|
|
430
505
|
const taskLine = /(?:^- \[[ x]\] )task-\d+[^:]*:?\s*.+$/gm;
|
|
@@ -709,3 +784,115 @@ function writeDump(result, authoritySpecDir) {
|
|
|
709
784
|
return { ...result, dump_write_error: e.message };
|
|
710
785
|
}
|
|
711
786
|
}
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* SS-2(2026-08-20):归档幽灵行——db active 但 changes/ 无同名目录的记录。
|
|
791
|
+
* SS-2b(2026-08-20):扩空壳目录——db active + 目录存在但 0 文件 + last_active 超
|
|
792
|
+
* 7 天(GHOST_EMPTY_DIR_STALE_MS)。两者都是 quick 收尾注销缺陷(工具侧已修)在
|
|
793
|
+
* 历史会话中累积的僵尸 active 行,污染 listChanges/progress show 的「活跃变更」
|
|
794
|
+
* 列表;旧 doctor 只 WARNING 不给动作(stage-machine 提示「可用 doctor 清理」却清不掉)。
|
|
795
|
+
*
|
|
796
|
+
* 安全设计:
|
|
797
|
+
* - 仅把 changes.status 从 active 改为 archived(对齐 change-registry 归档语义,
|
|
798
|
+
* 状态可逆:手工改回 status='active' 即恢复,不删任何行)。
|
|
799
|
+
* - 默认 dry-run 只列名单;加 confirm 才写。
|
|
800
|
+
* - 空壳目录归档时同时移除空目录(删前复查仍为 0 文件,复查非空整条放过);
|
|
801
|
+
* 有内容的目录一律不动。孤儿目录(有目录无行)不自动删,留给人工归位。
|
|
802
|
+
*/
|
|
803
|
+
export async function cleanupGhostChanges({ cwd, specDir = null, confirm = false }) {
|
|
804
|
+
const pointer = resolvePointer(cwd);
|
|
805
|
+
const localSpec = join(cwd, '.sillyspec');
|
|
806
|
+
const candidates = [
|
|
807
|
+
join(localSpec, '.runtime', 'sillyspec.db'),
|
|
808
|
+
join(localSpec, 'sillyspec.db'),
|
|
809
|
+
];
|
|
810
|
+
if (pointer.specRoot) {
|
|
811
|
+
candidates.unshift(
|
|
812
|
+
join(pointer.specRoot, '.runtime', 'sillyspec.db'),
|
|
813
|
+
join(pointer.specRoot, 'sillyspec.db'),
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
let dbPath = null;
|
|
817
|
+
for (const p of candidates) {
|
|
818
|
+
try {
|
|
819
|
+
if (existsSync(p) && statSync(p).size > 0) { dbPath = p; break; }
|
|
820
|
+
} catch { /* stat 失败跳过 */ }
|
|
821
|
+
}
|
|
822
|
+
if (!dbPath) {
|
|
823
|
+
return { action: 'skipped', reason: '未找到非空 sillyspec.db', ghosts: [], archived: [], errors: [], count: 0 };
|
|
824
|
+
}
|
|
825
|
+
const authoritySpecRoot = (pointer.present && pointer.specRoot && existsSync(pointer.specRoot))
|
|
826
|
+
? pointer.specRoot
|
|
827
|
+
: localSpec;
|
|
828
|
+
const changesDir = join(authoritySpecRoot, 'changes');
|
|
829
|
+
let dirNames = [];
|
|
830
|
+
try {
|
|
831
|
+
dirNames = readdirSync(changesDir, { withFileTypes: true })
|
|
832
|
+
.filter((e) => e.isDirectory() && e.name !== 'archive')
|
|
833
|
+
.map((e) => e.name);
|
|
834
|
+
} catch { /* 目录缺失 → 全部 active 都是幽灵 */ }
|
|
835
|
+
const dirSet = new Set(dirNames);
|
|
836
|
+
|
|
837
|
+
let db = null;
|
|
838
|
+
const archived = [];
|
|
839
|
+
const errors = [];
|
|
840
|
+
const skippedNonEmpty = [];
|
|
841
|
+
const removedDirs = [];
|
|
842
|
+
try {
|
|
843
|
+
db = openDatabase(dbPath, { readOnly: !confirm });
|
|
844
|
+
// last_active 与 name 一起取:空壳判定需要时间门槛(见 isStaleLastActive)。
|
|
845
|
+
const activeRows = db.prepare("SELECT name, last_active FROM changes WHERE status='active'").all();
|
|
846
|
+
const ghosts = activeRows.filter((r) => !dirSet.has(r.name)).map((r) => r.name);
|
|
847
|
+
const emptyShells = activeRows
|
|
848
|
+
.filter((r) => dirSet.has(r.name) && isStaleLastActive(r.last_active) && dirHasNoFiles(join(changesDir, r.name)))
|
|
849
|
+
.map((r) => r.name);
|
|
850
|
+
if (confirm) {
|
|
851
|
+
const now = new Date().toISOString();
|
|
852
|
+
const stmt = db.prepare("UPDATE changes SET status = 'archived', last_active = ? WHERE name = ? AND status = 'active'");
|
|
853
|
+
for (const n of ghosts) {
|
|
854
|
+
try {
|
|
855
|
+
stmt.run(now, n);
|
|
856
|
+
archived.push(n);
|
|
857
|
+
} catch (e) {
|
|
858
|
+
errors.push({ name: n, error: e.message });
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
for (const n of emptyShells) {
|
|
862
|
+
// 删目录前复查非空:dry-run 与 --confirm 之间并发会话可能已往空目录写入首份产物
|
|
863
|
+
//(如 brainstorm 落 proposal.md)。复查非空 → 整条放过(行也不归档),宁漏勿杀。
|
|
864
|
+
const dirPath = join(changesDir, n);
|
|
865
|
+
if (!dirHasNoFiles(dirPath)) {
|
|
866
|
+
skippedNonEmpty.push(n);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
try {
|
|
870
|
+
stmt.run(now, n);
|
|
871
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
872
|
+
removedDirs.push(n);
|
|
873
|
+
archived.push(n);
|
|
874
|
+
} catch (e) {
|
|
875
|
+
errors.push({ name: n, error: e.message });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const activeAfter = confirm ? pluckAll(db, "SELECT name FROM changes WHERE status='active'") : null;
|
|
880
|
+
return {
|
|
881
|
+
action: confirm ? 'archived' : 'dry_run',
|
|
882
|
+
db_path: dbPath,
|
|
883
|
+
changes_dir: changesDir,
|
|
884
|
+
ghosts: confirm ? archived : ghosts,
|
|
885
|
+
empty_shells: confirm ? removedDirs : emptyShells,
|
|
886
|
+
skipped_nonempty: skippedNonEmpty,
|
|
887
|
+
removed_dirs: removedDirs,
|
|
888
|
+
archived,
|
|
889
|
+
errors,
|
|
890
|
+
count: ghosts.length + emptyShells.length,
|
|
891
|
+
active_after: activeAfter,
|
|
892
|
+
};
|
|
893
|
+
} catch (e) {
|
|
894
|
+
return { action: 'error', reason: e.message, ghosts: [], archived: [], errors: [{ error: e.message }], count: 0 };
|
|
895
|
+
} finally {
|
|
896
|
+
if (db) { try { db.close(); } catch { /* 忽略关闭失败 */ } }
|
|
897
|
+
}
|
|
898
|
+
}
|
package/src/fs-atomic.js
CHANGED
|
@@ -58,7 +58,9 @@ export function renameSyncRetry(from, to, retries = 5) {
|
|
|
58
58
|
*/
|
|
59
59
|
export function writeAtomicSync(filePath, content) {
|
|
60
60
|
const dir = dirname(filePath);
|
|
61
|
-
|
|
61
|
+
// pid + 随机段双因子:Windows PID 重用激进,两进程可能撞同 pid,tmp 名碰撞 rename 互相覆盖
|
|
62
|
+
const rnd = Math.random().toString(36).slice(2, 10);
|
|
63
|
+
const tmpPath = join(dir, `.${basename(filePath)}.${process.pid}.${rnd}.tmp`);
|
|
62
64
|
writeFileSync(tmpPath, content);
|
|
63
65
|
try {
|
|
64
66
|
renameSyncRetry(tmpPath, filePath);
|
package/src/git-helper.js
CHANGED
|
@@ -53,13 +53,16 @@ export function safeGit(cwd, args, opts = {}) {
|
|
|
53
53
|
* 调用方自行 catch;不经 shell,参数以数组元素传递。
|
|
54
54
|
* @param {string} cwd
|
|
55
55
|
* @param {string[]} args
|
|
56
|
-
* @param {{ trim?: boolean, timeout?: number }} [opts]
|
|
57
|
-
*
|
|
56
|
+
* @param {{ trim?: boolean, timeout?: number, encoding?: string }} [opts]
|
|
57
|
+
* - encoding:'buffer' 返回原始 Buffer(git diff --binary 等**二进制输出**专用——
|
|
58
|
+
* utf8 解码会破坏 NUL 字节使补丁不可应用;Buffer 输出跳过 trim)
|
|
59
|
+
* @returns {string|Buffer}
|
|
58
60
|
*/
|
|
59
61
|
export function git(cwd, args, opts = {}) {
|
|
60
|
-
const { trim = true, timeout = 5000 } = opts
|
|
62
|
+
const { trim = true, timeout = 5000, encoding = 'utf8' } = opts
|
|
61
63
|
const fullArgs = ['-c', `safe.directory=${cwd}`, '-C', cwd, ...args]
|
|
62
|
-
const value = execFileSync('git', fullArgs, { encoding
|
|
64
|
+
const value = execFileSync('git', fullArgs, { encoding, timeout, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
65
|
+
if (Buffer.isBuffer(value)) return value
|
|
63
66
|
return trim ? value.trim() : value
|
|
64
67
|
}
|
|
65
68
|
|
|
@@ -121,8 +121,14 @@ function readWorktreeMeta(cwd, changeName) {
|
|
|
121
121
|
|
|
122
122
|
function isPathInside(child, parent) {
|
|
123
123
|
if (!child || !parent) return false
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
let absChild = path.resolve(child)
|
|
125
|
+
let absParent = path.resolve(parent)
|
|
126
|
+
// Windows 文件系统大小写不敏感:git worktree list / 编辑器给出的路径大小写可能与
|
|
127
|
+
// 注册的 meta.worktreePath 不一致,敏感比较会把 worktree 内合法写入误判为越权(体检 BUG-09)
|
|
128
|
+
if (process.platform === 'win32') {
|
|
129
|
+
absChild = absChild.toLowerCase()
|
|
130
|
+
absParent = absParent.toLowerCase()
|
|
131
|
+
}
|
|
126
132
|
return absChild === absParent || absChild.startsWith(absParent + path.sep)
|
|
127
133
|
}
|
|
128
134
|
|