gsd-pi 2.76.0-dev.82e249f7b → 2.76.0-dev.fe143342a
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/dist/mcp-server.d.ts +7 -0
- package/dist/mcp-server.js +35 -1
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +2 -8
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +66 -4
- package/dist/resources/extensions/gsd/auto-start.js +27 -14
- package/dist/resources/extensions/gsd/auto.js +11 -11
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +35 -0
- package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
- package/dist/resources/extensions/gsd/error-classifier.js +10 -3
- package/dist/resources/extensions/gsd/exec-history.js +120 -0
- package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
- package/dist/resources/extensions/gsd/gsd-db.js +62 -4
- package/dist/resources/extensions/gsd/init-wizard.js +15 -1
- package/dist/resources/extensions/gsd/key-manager.js +6 -0
- package/dist/resources/extensions/gsd/pre-execution-checks.js +13 -3
- package/dist/resources/extensions/gsd/preferences-types.js +9 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
- package/dist/resources/extensions/gsd/preferences.js +17 -17
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
- package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +64 -25
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
- package/packages/mcp-server/src/workflow-tools.ts +84 -43
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
- package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
- package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
- package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
- package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +67 -4
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +137 -2
- package/src/resources/extensions/gsd/auto-start.ts +28 -15
- package/src/resources/extensions/gsd/auto.ts +11 -11
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -0
- package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
- package/src/resources/extensions/gsd/error-classifier.ts +10 -3
- package/src/resources/extensions/gsd/exec-history.ts +153 -0
- package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
- package/src/resources/extensions/gsd/gsd-db.ts +68 -4
- package/src/resources/extensions/gsd/init-wizard.ts +15 -1
- package/src/resources/extensions/gsd/key-manager.ts +6 -0
- package/src/resources/extensions/gsd/pre-execution-checks.ts +13 -3
- package/src/resources/extensions/gsd/preferences-types.ts +38 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
- package/src/resources/extensions/gsd/preferences.ts +17 -17
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
- package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
- package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
- /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → n21VtX2hZlkpdEUO_nU4z}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → n21VtX2hZlkpdEUO_nU4z}/_ssgManifest.js +0 -0
|
@@ -3,12 +3,14 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import * as os from 'node:os';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
6
7
|
import {
|
|
7
8
|
openDatabase,
|
|
8
9
|
closeDatabase,
|
|
9
10
|
isDbAvailable,
|
|
10
11
|
wasDbOpenAttempted,
|
|
11
12
|
getDbProvider,
|
|
13
|
+
getDbStatus,
|
|
12
14
|
insertDecision,
|
|
13
15
|
getDecisionById,
|
|
14
16
|
insertRequirement,
|
|
@@ -26,6 +28,8 @@ import {
|
|
|
26
28
|
checkpointDatabase,
|
|
27
29
|
} from '../gsd-db.ts';
|
|
28
30
|
|
|
31
|
+
const _require = createRequire(import.meta.url);
|
|
32
|
+
|
|
29
33
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
30
34
|
// Helper: create a temp file path for file-backed DB tests
|
|
31
35
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -59,6 +63,20 @@ function withPlatform<T>(platform: NodeJS.Platform, fn: () => T): T {
|
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
function openRawSqliteForTest(dbPath: string): { exec(sql: string): void; close(): void } {
|
|
67
|
+
try {
|
|
68
|
+
const mod = _require('node:sqlite') as { DatabaseSync: new (path: string) => { exec(sql: string): void; close(): void } };
|
|
69
|
+
return new mod.DatabaseSync(dbPath);
|
|
70
|
+
} catch {
|
|
71
|
+
type SqliteCtor = new (path: string) => { exec(sql: string): void; close(): void };
|
|
72
|
+
const mod = _require('better-sqlite3') as
|
|
73
|
+
| SqliteCtor
|
|
74
|
+
| { default: SqliteCtor };
|
|
75
|
+
const DatabaseCtor: SqliteCtor = typeof mod === 'function' ? mod : mod.default;
|
|
76
|
+
return new DatabaseCtor(dbPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
62
80
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
63
81
|
// gsd-db tests
|
|
64
82
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -404,6 +422,53 @@ describe('gsd-db', () => {
|
|
|
404
422
|
cleanup(dbPath);
|
|
405
423
|
});
|
|
406
424
|
|
|
425
|
+
test('gsd-db: legacy DB missing memories.scope opens and bootstraps index columns', () => {
|
|
426
|
+
const dbPath = tempDbPath();
|
|
427
|
+
const legacyDb = openRawSqliteForTest(dbPath);
|
|
428
|
+
legacyDb.exec(`
|
|
429
|
+
CREATE TABLE schema_version (
|
|
430
|
+
version INTEGER NOT NULL,
|
|
431
|
+
applied_at TEXT NOT NULL
|
|
432
|
+
);
|
|
433
|
+
INSERT INTO schema_version(version, applied_at) VALUES (17, '2026-04-20T00:00:00.000Z');
|
|
434
|
+
CREATE TABLE memories (
|
|
435
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
436
|
+
id TEXT NOT NULL UNIQUE,
|
|
437
|
+
category TEXT NOT NULL,
|
|
438
|
+
content TEXT NOT NULL,
|
|
439
|
+
confidence REAL NOT NULL DEFAULT 0.8,
|
|
440
|
+
source_unit_type TEXT,
|
|
441
|
+
source_unit_id TEXT,
|
|
442
|
+
created_at TEXT NOT NULL,
|
|
443
|
+
updated_at TEXT NOT NULL,
|
|
444
|
+
superseded_by TEXT DEFAULT NULL,
|
|
445
|
+
hit_count INTEGER NOT NULL DEFAULT 0
|
|
446
|
+
);
|
|
447
|
+
INSERT INTO memories(id, category, content, created_at, updated_at)
|
|
448
|
+
VALUES ('legacy-memory', 'note', 'legacy row', '2026-04-20T00:00:00.000Z', '2026-04-20T00:00:00.000Z');
|
|
449
|
+
`);
|
|
450
|
+
legacyDb.close();
|
|
451
|
+
|
|
452
|
+
assert.equal(openDatabase(dbPath), true, 'openDatabase should succeed for legacy DB missing memories.scope');
|
|
453
|
+
|
|
454
|
+
const adapter = _getAdapter()!;
|
|
455
|
+
const columns = adapter.prepare('PRAGMA table_info(memories)').all();
|
|
456
|
+
const names = columns.map((row) => row['name']);
|
|
457
|
+
assert.ok(names.includes('scope'), 'memories.scope should be added during bootstrap');
|
|
458
|
+
assert.ok(names.includes('tags'), 'memories.tags should be added during bootstrap');
|
|
459
|
+
|
|
460
|
+
const row = adapter.prepare(`SELECT scope, tags FROM memories WHERE id = 'legacy-memory'`).get();
|
|
461
|
+
assert.equal(row?.['scope'], 'project', 'legacy rows should receive default scope');
|
|
462
|
+
assert.equal(row?.['tags'], '[]', 'legacy rows should receive default tags');
|
|
463
|
+
|
|
464
|
+
const index = adapter.prepare(
|
|
465
|
+
"SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'idx_memories_scope'",
|
|
466
|
+
).get();
|
|
467
|
+
assert.equal(index?.['name'], 'idx_memories_scope', 'scope index should be created after bootstrap columns are present');
|
|
468
|
+
|
|
469
|
+
cleanup(dbPath);
|
|
470
|
+
});
|
|
471
|
+
|
|
407
472
|
test('gsd-db: rowToTask tolerates legacy comma-separated task arrays', () => {
|
|
408
473
|
openDatabase(':memory:');
|
|
409
474
|
|
|
@@ -561,6 +626,92 @@ describe('gsd-db', () => {
|
|
|
561
626
|
});
|
|
562
627
|
});
|
|
563
628
|
|
|
629
|
+
// ─── getDbStatus ───────────────────────────────────────────────────────────
|
|
630
|
+
|
|
631
|
+
describe('getDbStatus', () => {
|
|
632
|
+
test('getDbStatus: initial state before any open', () => {
|
|
633
|
+
closeDatabase();
|
|
634
|
+
const status = getDbStatus();
|
|
635
|
+
assert.strictEqual(status.available, false, 'available false before open');
|
|
636
|
+
assert.strictEqual(status.attempted, false, 'attempted false before open');
|
|
637
|
+
assert.strictEqual(status.lastError, null, 'lastError null before open');
|
|
638
|
+
assert.strictEqual(status.lastPhase, null, 'lastPhase null before open');
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test('getDbStatus: available after successful open', () => {
|
|
642
|
+
openDatabase(':memory:');
|
|
643
|
+
const status = getDbStatus();
|
|
644
|
+
assert.strictEqual(status.available, true, 'available true after open');
|
|
645
|
+
assert.strictEqual(status.attempted, true, 'attempted true after open');
|
|
646
|
+
assert.ok(status.provider !== null, 'provider set after open');
|
|
647
|
+
assert.strictEqual(status.lastError, null, 'lastError null on success');
|
|
648
|
+
assert.strictEqual(status.lastPhase, null, 'lastPhase null on success');
|
|
649
|
+
closeDatabase();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test('getDbStatus: resets lastError/lastPhase after closeDatabase', () => {
|
|
653
|
+
// Simulate a failed open to set error state
|
|
654
|
+
const corruptPath = path.join(os.tmpdir(), `gsd-corrupt-${Date.now()}.db`);
|
|
655
|
+
fs.writeFileSync(corruptPath, Buffer.from('not a sqlite file at all!!!!!'));
|
|
656
|
+
try {
|
|
657
|
+
openDatabase(corruptPath);
|
|
658
|
+
} catch {
|
|
659
|
+
// expected
|
|
660
|
+
}
|
|
661
|
+
assert.ok(getDbStatus().lastError !== null, 'lastError set after failed open');
|
|
662
|
+
|
|
663
|
+
// closeDatabase should clear it even though no DB was opened
|
|
664
|
+
closeDatabase();
|
|
665
|
+
const status = getDbStatus();
|
|
666
|
+
assert.strictEqual(status.lastError, null, 'lastError cleared by closeDatabase');
|
|
667
|
+
assert.strictEqual(status.lastPhase, null, 'lastPhase cleared by closeDatabase');
|
|
668
|
+
assert.strictEqual(status.attempted, false, 'attempted reset by closeDatabase');
|
|
669
|
+
fs.unlinkSync(corruptPath);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test('getDbStatus: captures open-phase error on corrupt file', () => {
|
|
673
|
+
closeDatabase();
|
|
674
|
+
const corruptPath = path.join(os.tmpdir(), `gsd-corrupt-${Date.now()}.db`);
|
|
675
|
+
fs.writeFileSync(corruptPath, Buffer.from('not a sqlite file at all!!!!!'));
|
|
676
|
+
try {
|
|
677
|
+
openDatabase(corruptPath);
|
|
678
|
+
} catch {
|
|
679
|
+
// expected — both providers should reject a non-SQLite file
|
|
680
|
+
}
|
|
681
|
+
const status = getDbStatus();
|
|
682
|
+
if (!status.available) {
|
|
683
|
+
// open failed (expected in most environments)
|
|
684
|
+
assert.strictEqual(status.attempted, true, 'attempted true after failed open');
|
|
685
|
+
// provider may reject at raw-open level ("open") or at SQL init level ("initSchema")
|
|
686
|
+
assert.ok(
|
|
687
|
+
status.lastPhase === 'open' || status.lastPhase === 'initSchema',
|
|
688
|
+
`lastPhase should be "open" or "initSchema", got: ${status.lastPhase}`,
|
|
689
|
+
);
|
|
690
|
+
assert.ok(status.lastError instanceof Error, 'lastError is an Error');
|
|
691
|
+
}
|
|
692
|
+
// If somehow it succeeded (unlikely with garbage content), that's also fine
|
|
693
|
+
closeDatabase();
|
|
694
|
+
try { fs.unlinkSync(corruptPath); } catch { /* best effort */ }
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test('getDbStatus: error state resets on next successful open', () => {
|
|
698
|
+
closeDatabase();
|
|
699
|
+
const corruptPath = path.join(os.tmpdir(), `gsd-corrupt-${Date.now()}.db`);
|
|
700
|
+
fs.writeFileSync(corruptPath, Buffer.from('not a sqlite file at all!!!!!'));
|
|
701
|
+
try { openDatabase(corruptPath); } catch { /* expected */ }
|
|
702
|
+
assert.ok(!getDbStatus().available, 'DB unavailable after corrupt open');
|
|
703
|
+
|
|
704
|
+
// Now open a valid in-memory DB — error state should clear
|
|
705
|
+
openDatabase(':memory:');
|
|
706
|
+
const status = getDbStatus();
|
|
707
|
+
assert.strictEqual(status.available, true, 'available after valid open');
|
|
708
|
+
assert.strictEqual(status.lastError, null, 'lastError cleared on successful open');
|
|
709
|
+
assert.strictEqual(status.lastPhase, null, 'lastPhase cleared on successful open');
|
|
710
|
+
closeDatabase();
|
|
711
|
+
try { fs.unlinkSync(corruptPath); } catch { /* best effort */ }
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
564
715
|
// ─── Final Report ──────────────────────────────────────────────────────────
|
|
565
716
|
|
|
566
717
|
});
|
|
@@ -178,6 +178,33 @@ test("init-wizard: multiple project files detected together", (t) => {
|
|
|
178
178
|
}
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
// ─── Git init + initial commit regression (#4530) ───────────────────────────
|
|
182
|
+
|
|
183
|
+
import { execFileSync } from "node:child_process";
|
|
184
|
+
import { nativeInit, nativeAddAll, nativeCommit } from "../native-git-bridge.ts";
|
|
185
|
+
|
|
186
|
+
test("init-wizard: nativeInit + nativeAddAll + nativeCommit produces a reachable HEAD (#4530)", (t) => {
|
|
187
|
+
// Regression: showProjectInit called nativeInit but never committed, leaving
|
|
188
|
+
// the branch unborn. git log and git worktree add both fail on zero-commit repos.
|
|
189
|
+
const dir = makeTempDir("git-init-commit");
|
|
190
|
+
t.after(() => { cleanup(dir); });
|
|
191
|
+
|
|
192
|
+
nativeInit(dir, "main");
|
|
193
|
+
execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: dir });
|
|
194
|
+
execFileSync("git", ["config", "user.name", "Test"], { cwd: dir });
|
|
195
|
+
writeFileSync(join(dir, ".gitignore"), "*.log\n", "utf-8");
|
|
196
|
+
|
|
197
|
+
nativeAddAll(dir);
|
|
198
|
+
nativeCommit(dir, "chore: init project");
|
|
199
|
+
|
|
200
|
+
// git log must succeed (was: fatal: your current branch 'main' does not have any commits yet)
|
|
201
|
+
const subject = execFileSync("git", ["log", "-1", "--format=%s"], {
|
|
202
|
+
cwd: dir,
|
|
203
|
+
encoding: "utf-8",
|
|
204
|
+
}).trim();
|
|
205
|
+
assert.equal(subject, "chore: init project");
|
|
206
|
+
});
|
|
207
|
+
|
|
181
208
|
test("init-wizard: v1 with both .planning/ and .gsd/ prioritizes v2", (t) => {
|
|
182
209
|
const dir = makeTempDir("both-v1-v2");
|
|
183
210
|
try {
|
|
@@ -42,7 +42,7 @@ describe('isolation:none stale branch guard (#3675)', () => {
|
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
test('guard is conditional on isolation mode "none"', () => {
|
|
45
|
-
assert.match(source, /getIsolationMode\(
|
|
45
|
+
assert.match(source, /getIsolationMode\([^)]*\)\s*===\s*["']none["']/,
|
|
46
46
|
'guard should only activate when isolation mode is "none"');
|
|
47
47
|
});
|
|
48
48
|
|
|
@@ -143,6 +143,13 @@ test("PROVIDER_REGISTRY includes all major LLM providers", () => {
|
|
|
143
143
|
assert.ok(ids.includes("groq"));
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
test("PROVIDER_REGISTRY includes claude-code as a first-class LLM provider (#4541)", () => {
|
|
147
|
+
const entry = PROVIDER_REGISTRY.find((p) => p.id === "claude-code");
|
|
148
|
+
assert.ok(entry, "claude-code must be in PROVIDER_REGISTRY");
|
|
149
|
+
assert.equal(entry!.category, "llm");
|
|
150
|
+
assert.ok(entry!.hasOAuth, "claude-code uses OAuth (CLI auth)");
|
|
151
|
+
});
|
|
152
|
+
|
|
146
153
|
test("PROVIDER_REGISTRY includes all tool/search providers", () => {
|
|
147
154
|
const ids = PROVIDER_REGISTRY.map((p) => p.id);
|
|
148
155
|
assert.ok(ids.includes("tavily"));
|
|
@@ -30,6 +30,20 @@ describe('normalizeFilePath backtick stripping (#3649)', () => {
|
|
|
30
30
|
assert.equal(normalizeFilePath('``src/foo.ts`` (current state)'), 'src/foo.ts')
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
+
it('strips stray backticks from dash-annotated bare paths (#4550)', () => {
|
|
34
|
+
assert.equal(
|
|
35
|
+
normalizeFilePath('.gsd/KNOWLEDGE.md` — append-only S05 lessons section'),
|
|
36
|
+
'.gsd/KNOWLEDGE.md',
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('prefers a backticked path inside a dash-annotated prefix (#4550)', () => {
|
|
41
|
+
assert.equal(
|
|
42
|
+
normalizeFilePath('Input `src/foo.ts` — current state'),
|
|
43
|
+
'src/foo.ts',
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
33
47
|
it('strips backticks even when mixed with other normalization', () => {
|
|
34
48
|
assert.equal(normalizeFilePath('`./src//bar.ts`'), 'src/bar.ts')
|
|
35
49
|
})
|
|
@@ -140,6 +140,25 @@ import type { Request } from 'express';
|
|
|
140
140
|
assert.ok(packages.includes("typescript"));
|
|
141
141
|
assert.ok(!packages.includes("-D"));
|
|
142
142
|
});
|
|
143
|
+
|
|
144
|
+
// Regression tests for #4388: prose containing `from "..."` must not produce false-positive packages
|
|
145
|
+
test("does not treat prose 'from \"What's Next\"' as a package name (#4388)", () => {
|
|
146
|
+
const desc = 'Build the feature described from "What\'s Next" in the roadmap';
|
|
147
|
+
const packages = extractPackageReferences(desc);
|
|
148
|
+
assert.deepEqual(packages, [], `prose 'from "What\\'s Next"' must not produce package names, got: ${JSON.stringify(packages)}`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("does not treat prose \"from 'master'\" as a package name (#4388)", () => {
|
|
152
|
+
const desc = "Review changes from 'master' branch before merging";
|
|
153
|
+
const packages = extractPackageReferences(desc);
|
|
154
|
+
assert.deepEqual(packages, [], `prose "from 'master'" must not produce package names, got: ${JSON.stringify(packages)}`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("still extracts import statements in code blocks after #4388 fix", () => {
|
|
158
|
+
const desc = "```typescript\nimport express from 'express';\nimport { Router } from 'express';\n```";
|
|
159
|
+
const packages = extractPackageReferences(desc);
|
|
160
|
+
assert.ok(packages.includes("express"), "import...from in code blocks must still be recognized");
|
|
161
|
+
});
|
|
143
162
|
});
|
|
144
163
|
|
|
145
164
|
// ─── File Path Consistency Tests ─────────────────────────────────────────────
|
|
@@ -186,6 +186,45 @@ test("flat_rate_providers is a recognized preference key (no warning)", () => {
|
|
|
186
186
|
);
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
+
test("slice_parallel preferences validate and pass through", () => {
|
|
190
|
+
const { preferences, errors, warnings } = validatePreferences({
|
|
191
|
+
slice_parallel: { enabled: true, max_workers: 8 },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
assert.equal(errors.length, 0);
|
|
195
|
+
assert.equal(warnings.filter(w => w.includes("slice_parallel")).length, 0);
|
|
196
|
+
assert.deepEqual(preferences.slice_parallel, { enabled: true, max_workers: 8 });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("slice_parallel rejects invalid values and warns on unknown keys", () => {
|
|
200
|
+
const { preferences, errors, warnings } = validatePreferences({
|
|
201
|
+
slice_parallel: {
|
|
202
|
+
enabled: "yes",
|
|
203
|
+
max_workers: 9,
|
|
204
|
+
future_mode: true,
|
|
205
|
+
},
|
|
206
|
+
} as any);
|
|
207
|
+
|
|
208
|
+
assert.ok(errors.some(e => e.includes("slice_parallel.enabled")), "should reject non-boolean enabled");
|
|
209
|
+
assert.ok(errors.some(e => e.includes("slice_parallel.max_workers")), "should reject max_workers outside 1..8");
|
|
210
|
+
assert.ok(warnings.some(w => w.includes('unknown slice_parallel key "future_mode"')));
|
|
211
|
+
assert.equal(preferences.slice_parallel, undefined);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("slice_parallel numeric max_workers is bounded to 1..8", () => {
|
|
215
|
+
const low = validatePreferences({ slice_parallel: { max_workers: 1 } });
|
|
216
|
+
const high = validatePreferences({ slice_parallel: { max_workers: 8 } });
|
|
217
|
+
const tooLow = validatePreferences({ slice_parallel: { max_workers: 0 } });
|
|
218
|
+
const tooHigh = validatePreferences({ slice_parallel: { max_workers: 9 } });
|
|
219
|
+
|
|
220
|
+
assert.equal(low.errors.length, 0);
|
|
221
|
+
assert.equal(low.preferences.slice_parallel?.max_workers, 1);
|
|
222
|
+
assert.equal(high.errors.length, 0);
|
|
223
|
+
assert.equal(high.preferences.slice_parallel?.max_workers, 8);
|
|
224
|
+
assert.ok(tooLow.errors.some(e => e.includes("slice_parallel.max_workers")));
|
|
225
|
+
assert.ok(tooHigh.errors.some(e => e.includes("slice_parallel.max_workers")));
|
|
226
|
+
});
|
|
227
|
+
|
|
189
228
|
test("valid values pass through correctly", () => {
|
|
190
229
|
const { preferences: p1 } = validatePreferences({ budget_enforcement: "halt" });
|
|
191
230
|
assert.equal(p1.budget_enforcement, "halt");
|
|
@@ -606,6 +645,44 @@ test("loadEffectiveGSDPreferences preserves experimental prefs across global+pro
|
|
|
606
645
|
}
|
|
607
646
|
});
|
|
608
647
|
|
|
648
|
+
test("loadEffectiveGSDPreferences exposes slice_parallel prefs to runtime callers", () => {
|
|
649
|
+
const originalCwd = process.cwd();
|
|
650
|
+
const originalGsdHome = process.env.GSD_HOME;
|
|
651
|
+
const tempProject = mkdtempSync(join(tmpdir(), "gsd-slice-parallel-project-"));
|
|
652
|
+
const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-slice-parallel-home-"));
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
mkdirSync(join(tempProject, ".gsd"), { recursive: true });
|
|
656
|
+
|
|
657
|
+
writeFileSync(
|
|
658
|
+
join(tempProject, ".gsd", "PREFERENCES.md"),
|
|
659
|
+
[
|
|
660
|
+
"---",
|
|
661
|
+
"version: 1",
|
|
662
|
+
"slice_parallel:",
|
|
663
|
+
" enabled: true",
|
|
664
|
+
" max_workers: 3",
|
|
665
|
+
"---",
|
|
666
|
+
].join("\n"),
|
|
667
|
+
"utf-8",
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
process.env.GSD_HOME = tempGsdHome;
|
|
671
|
+
process.chdir(tempProject);
|
|
672
|
+
|
|
673
|
+
const loaded = loadEffectiveGSDPreferences();
|
|
674
|
+
assert.notEqual(loaded, null);
|
|
675
|
+
assert.equal(loaded!.preferences.slice_parallel?.enabled, true);
|
|
676
|
+
assert.equal(loaded!.preferences.slice_parallel?.max_workers, 3);
|
|
677
|
+
} finally {
|
|
678
|
+
process.chdir(originalCwd);
|
|
679
|
+
if (originalGsdHome === undefined) delete process.env.GSD_HOME;
|
|
680
|
+
else process.env.GSD_HOME = originalGsdHome;
|
|
681
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
682
|
+
rmSync(tempGsdHome, { recursive: true, force: true });
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
609
686
|
test("preferences paths use canonical uppercase filenames", () => {
|
|
610
687
|
const originalCwd = process.cwd();
|
|
611
688
|
const originalGsdHome = process.env.GSD_HOME;
|
|
@@ -632,6 +709,39 @@ test("preferences paths use canonical uppercase filenames", () => {
|
|
|
632
709
|
}
|
|
633
710
|
});
|
|
634
711
|
|
|
712
|
+
test("explicit base path preference loading survives a deleted cwd (#4498)", (t) => {
|
|
713
|
+
const originalCwd = process.cwd();
|
|
714
|
+
const originalGsdHome = process.env.GSD_HOME;
|
|
715
|
+
const tempProject = mkdtempSync(join(tmpdir(), "gsd-prefs-base-project-"));
|
|
716
|
+
const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-prefs-base-home-"));
|
|
717
|
+
const deletedCwd = mkdtempSync(join(tmpdir(), "gsd-prefs-deleted-cwd-"));
|
|
718
|
+
|
|
719
|
+
t.after(() => {
|
|
720
|
+
process.chdir(originalCwd);
|
|
721
|
+
if (originalGsdHome === undefined) delete process.env.GSD_HOME;
|
|
722
|
+
else process.env.GSD_HOME = originalGsdHome;
|
|
723
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
724
|
+
rmSync(tempGsdHome, { recursive: true, force: true });
|
|
725
|
+
rmSync(deletedCwd, { recursive: true, force: true });
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
mkdirSync(join(tempProject, ".gsd"), { recursive: true });
|
|
729
|
+
writeFileSync(
|
|
730
|
+
join(tempProject, ".gsd", "PREFERENCES.md"),
|
|
731
|
+
"---\nversion: 1\nlanguage: Swedish\ngit:\n isolation: worktree\n---\n",
|
|
732
|
+
"utf-8",
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
process.env.GSD_HOME = tempGsdHome;
|
|
736
|
+
process.chdir(deletedCwd);
|
|
737
|
+
rmSync(deletedCwd, { recursive: true, force: true });
|
|
738
|
+
|
|
739
|
+
const loaded = loadEffectiveGSDPreferences(tempProject);
|
|
740
|
+
assert.notEqual(loaded, null);
|
|
741
|
+
assert.equal(loaded!.preferences.language, "Swedish");
|
|
742
|
+
assert.equal(getIsolationMode(tempProject), "worktree");
|
|
743
|
+
});
|
|
744
|
+
|
|
635
745
|
test("uppercase PREFERENCES.md wins over legacy lowercase preferences.md", () => {
|
|
636
746
|
const originalCwd = process.cwd();
|
|
637
747
|
const originalGsdHome = process.env.GSD_HOME;
|
|
@@ -45,6 +45,13 @@ test("classifyError treats usage-limit phrasing as transient rate-limit (#4373)"
|
|
|
45
45
|
assert.equal(result.kind, "rate-limit");
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
test("classifyError treats extra-usage phrasing as transient rate-limit (#4397)", () => {
|
|
49
|
+
const result = classifyError("You are out of extra usage. Please wait before retrying.");
|
|
50
|
+
assert.ok(isTransient(result));
|
|
51
|
+
assert.equal(result.kind, "rate-limit");
|
|
52
|
+
assert.ok("retryAfterMs" in result && result.retryAfterMs === 60_000);
|
|
53
|
+
});
|
|
54
|
+
|
|
48
55
|
test("classifyError treats OpenRouter affordability errors as transient rate-limit class", () => {
|
|
49
56
|
const result = classifyError(
|
|
50
57
|
"402 This request requires more credits, or fewer max_tokens. You requested up to 32000 tokens, but can only afford 329.",
|
|
@@ -673,6 +680,47 @@ test("MAX_TRANSIENT_AUTO_RESUMES is at least 8 for sustained overload resilience
|
|
|
673
680
|
);
|
|
674
681
|
});
|
|
675
682
|
|
|
683
|
+
// ── Stream idle timeout / partial response (#4558) ──────────────────────────
|
|
684
|
+
|
|
685
|
+
test("classifyError: 'Stream idle timeout - partial response received' is transient network", () => {
|
|
686
|
+
const result = classifyError("API Error: Stream idle timeout - partial response received");
|
|
687
|
+
assert.ok(isTransient(result), "stream idle timeout must be transient");
|
|
688
|
+
assert.equal(result.kind, "network");
|
|
689
|
+
assert.ok("retryAfterMs" in result && result.retryAfterMs > 0);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test("classifyError: 'stream idle timeout' (lowercase) is transient network", () => {
|
|
693
|
+
const result = classifyError("stream idle timeout");
|
|
694
|
+
assert.ok(isTransient(result), "lowercase stream idle timeout must be transient");
|
|
695
|
+
assert.equal(result.kind, "network");
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
test("classifyError: 'partial response received' alone is transient network", () => {
|
|
699
|
+
const result = classifyError("partial response received");
|
|
700
|
+
assert.ok(isTransient(result), "partial response received must be transient");
|
|
701
|
+
assert.equal(result.kind, "network");
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// ── Context overflow / context window exceeded (#4528) ───────────────────────
|
|
705
|
+
|
|
706
|
+
test("classifyError: MiniMax context window error is transient server", () => {
|
|
707
|
+
const result = classifyError("400 invalid params, context window exceeds limit (2013)");
|
|
708
|
+
assert.ok(isTransient(result), "context window exceeded must be transient");
|
|
709
|
+
assert.equal(result.kind, "server");
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test("classifyError: 'context length exceeded' is transient server", () => {
|
|
713
|
+
const result = classifyError("context length exceeded: max 128000 tokens");
|
|
714
|
+
assert.ok(isTransient(result), "context length exceeded must be transient");
|
|
715
|
+
assert.equal(result.kind, "server");
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
test("classifyError: 'context window' with 'exceed' is transient server", () => {
|
|
719
|
+
const result = classifyError("context window exceeded for this model");
|
|
720
|
+
assert.ok(isTransient(result), "context window exceeded must be transient");
|
|
721
|
+
assert.equal(result.kind, "server");
|
|
722
|
+
});
|
|
723
|
+
|
|
676
724
|
// ── agent-session retryable regex handles server_error (#1166) ──────────────
|
|
677
725
|
|
|
678
726
|
test("agent-session retryable error regex matches server_error (underscore)", () => {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test suite for save_gate_result renderResult.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that renderResult does not print "undefined: undefined" when
|
|
5
|
+
* `details` is empty, and that the error fallback does not produce a
|
|
6
|
+
* duplicated `Error: Error:` prefix when `content[0].text` already starts
|
|
7
|
+
* with `Error:`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { registerDbTools } from '../bootstrap/db-tools.ts';
|
|
13
|
+
|
|
14
|
+
function makeMockPi() {
|
|
15
|
+
const tools: any[] = [];
|
|
16
|
+
return {
|
|
17
|
+
registerTool: (tool: any) => tools.push(tool),
|
|
18
|
+
tools,
|
|
19
|
+
} as any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fakeTheme = {
|
|
23
|
+
fg: (_color: string, text: string) => text,
|
|
24
|
+
bold: (text: string) => text,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function getSaveGateResultTool() {
|
|
28
|
+
const pi = makeMockPi();
|
|
29
|
+
registerDbTools(pi);
|
|
30
|
+
const tool = pi.tools.find((t: any) => t.name === 'gsd_save_gate_result');
|
|
31
|
+
assert.ok(tool, 'gsd_save_gate_result should be registered');
|
|
32
|
+
return tool;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('save_gate_result renderResult falls back to content text when details is empty', () => {
|
|
36
|
+
const tool = getSaveGateResultTool();
|
|
37
|
+
const result = {
|
|
38
|
+
content: [{ type: 'text', text: 'Gate Q3 result saved: verdict=pass' }],
|
|
39
|
+
details: {},
|
|
40
|
+
isError: false,
|
|
41
|
+
};
|
|
42
|
+
const rendered = tool.renderResult(result, {}, fakeTheme);
|
|
43
|
+
const text = String(rendered.content ?? rendered.text ?? rendered);
|
|
44
|
+
assert.ok(!text.includes('undefined'), `got: ${text}`);
|
|
45
|
+
assert.ok(
|
|
46
|
+
text.includes('Gate Q3') || text.includes('verdict=pass'),
|
|
47
|
+
`expected content summary — got: ${text}`,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('save_gate_result renderResult uses structured details when present', () => {
|
|
52
|
+
const tool = getSaveGateResultTool();
|
|
53
|
+
const result = {
|
|
54
|
+
content: [{ type: 'text', text: 'Gate Q3 result saved: verdict=flag' }],
|
|
55
|
+
details: { operation: 'save_gate_result', gateId: 'Q3', verdict: 'flag' },
|
|
56
|
+
isError: false,
|
|
57
|
+
};
|
|
58
|
+
const rendered = tool.renderResult(result, {}, fakeTheme);
|
|
59
|
+
const text = String(rendered.content ?? rendered.text ?? rendered);
|
|
60
|
+
assert.ok(text.includes('Q3'), `got: ${text}`);
|
|
61
|
+
assert.ok(text.includes('flag'), `got: ${text}`);
|
|
62
|
+
assert.ok(!text.includes('undefined'), `got: ${text}`);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('save_gate_result renderResult shows error from content when details.error is missing', () => {
|
|
66
|
+
const tool = getSaveGateResultTool();
|
|
67
|
+
const result = {
|
|
68
|
+
content: [{ type: 'text', text: 'Error: Invalid gateId "Z1"' }],
|
|
69
|
+
details: {},
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
const rendered = tool.renderResult(result, {}, fakeTheme);
|
|
73
|
+
const text = String(rendered.content ?? rendered.text ?? rendered);
|
|
74
|
+
assert.ok(
|
|
75
|
+
text.includes('Invalid gateId') || text.includes('Error'),
|
|
76
|
+
`got: ${text}`,
|
|
77
|
+
);
|
|
78
|
+
assert.ok(!text.includes('undefined'), `got: ${text}`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('save_gate_result renderResult does not duplicate Error: prefix', () => {
|
|
82
|
+
const tool = getSaveGateResultTool();
|
|
83
|
+
const result = {
|
|
84
|
+
content: [{ type: 'text', text: 'Error: Invalid gateId "Z1"' }],
|
|
85
|
+
details: {},
|
|
86
|
+
isError: true,
|
|
87
|
+
};
|
|
88
|
+
const rendered = tool.renderResult(result, {}, fakeTheme);
|
|
89
|
+
const text = String(rendered.content ?? rendered.text ?? rendered);
|
|
90
|
+
assert.ok(
|
|
91
|
+
!/Error:\s*Error:/i.test(text),
|
|
92
|
+
`expected a single Error: prefix — got: ${text}`,
|
|
93
|
+
);
|
|
94
|
+
assert.ok(text.includes('Invalid gateId'), `got: ${text}`);
|
|
95
|
+
});
|
|
@@ -72,7 +72,9 @@ const autoStartSrc = readFileSync(
|
|
|
72
72
|
const symlinkIdx = autoStartSrc.indexOf("ensureGsdSymlink(base)");
|
|
73
73
|
assertTrue(symlinkIdx >= 0, "auto-start.ts calls ensureGsdSymlink(base)");
|
|
74
74
|
|
|
75
|
-
const afterSymlink = symlinkIdx >= 0
|
|
75
|
+
const afterSymlink = symlinkIdx >= 0
|
|
76
|
+
? autoStartSrc.slice(symlinkIdx, autoStartSrc.indexOf("Initialize GitServiceImpl", symlinkIdx))
|
|
77
|
+
: "";
|
|
76
78
|
|
|
77
79
|
// The milestones bootstrap must check milestones path, not gsdDir
|
|
78
80
|
// Old (dead) code: if (!existsSync(gsdDir)) { mkdirSync(join(gsdDir, "milestones"), ...) }
|