gsd-pi 2.73.0-dev.27730dc → 2.73.0-dev.e1c09f2

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.
Files changed (51) hide show
  1. package/dist/resources/extensions/gsd/auto.js +1 -5
  2. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +7 -18
  3. package/dist/resources/extensions/gsd/crash-recovery.js +0 -51
  4. package/dist/resources/extensions/gsd/gsd-db.js +2 -36
  5. package/dist/resources/extensions/gsd/milestone-actions.js +1 -19
  6. package/dist/web/standalone/.next/BUILD_ID +1 -1
  7. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  8. package/dist/web/standalone/.next/build-manifest.json +2 -2
  9. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  10. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  11. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  12. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  19. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/index.html +1 -1
  27. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  34. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  35. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  36. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  37. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  38. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  39. package/package.json +1 -1
  40. package/src/resources/extensions/gsd/auto.ts +0 -5
  41. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +7 -19
  42. package/src/resources/extensions/gsd/crash-recovery.ts +0 -59
  43. package/src/resources/extensions/gsd/gsd-db.ts +2 -52
  44. package/src/resources/extensions/gsd/milestone-actions.ts +1 -19
  45. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -59
  46. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +0 -64
  47. package/dist/resources/extensions/gsd/bootstrap/crash-log.js +0 -31
  48. package/src/resources/extensions/gsd/bootstrap/crash-log.ts +0 -32
  49. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +0 -235
  50. /package/dist/web/standalone/.next/static/{jNiH700EcljeLnbQ2_RCv → _XD_gUDcZNBbWV5rI8RgS}/_buildManifest.js +0 -0
  51. /package/dist/web/standalone/.next/static/{jNiH700EcljeLnbQ2_RCv → _XD_gUDcZNBbWV5rI8RgS}/_ssgManifest.js +0 -0
@@ -11,9 +11,6 @@ import { registerJournalTools } from "./journal-tools.js";
11
11
  import { registerQueryTools } from "./query-tools.js";
12
12
  import { registerHooks } from "./register-hooks.js";
13
13
  import { registerShortcuts } from "./register-shortcuts.js";
14
- import { writeCrashLog } from "./crash-log.js";
15
-
16
- export { writeCrashLog } from "./crash-log.js";
17
14
 
18
15
  export function handleRecoverableExtensionProcessError(err: Error): boolean {
19
16
  if ((err as NodeJS.ErrnoException).code === "EPIPE") {
@@ -36,25 +33,16 @@ export function handleRecoverableExtensionProcessError(err: Error): boolean {
36
33
  function installEpipeGuard(): void {
37
34
  if (!process.listeners("uncaughtException").some((listener) => listener.name === "_gsdEpipeGuard")) {
38
35
  const _gsdEpipeGuard = (err: Error): void => {
39
- if (handleRecoverableExtensionProcessError(err)) return;
40
- // Write crash log and exit cleanly for unrecoverable errors.
41
- // Logging and continuing was the original double-fault fix (#3163), but
42
- // continuing in an indeterminate state is worse than a clean exit (#3348).
43
- writeCrashLog(err, "uncaughtException");
44
- process.exit(1);
36
+ if (handleRecoverableExtensionProcessError(err)) {
37
+ return;
38
+ }
39
+ // Log unhandled errors instead of re-throwing throwing inside an
40
+ // uncaughtException handler is a fatal double-fault in Node.js (#3163).
41
+ process.stderr.write(`[gsd] uncaught extension error (non-fatal): ${err.message}\n`);
42
+ if (err.stack) process.stderr.write(`${err.stack}\n`);
45
43
  };
46
44
  process.on("uncaughtException", _gsdEpipeGuard);
47
45
  }
48
-
49
- if (!process.listeners("unhandledRejection").some((listener) => listener.name === "_gsdRejectionGuard")) {
50
- const _gsdRejectionGuard = (reason: unknown, _promise: Promise<unknown>): void => {
51
- const err = reason instanceof Error ? reason : new Error(String(reason));
52
- if (handleRecoverableExtensionProcessError(err)) return;
53
- writeCrashLog(err, "unhandledRejection");
54
- process.exit(1);
55
- };
56
- process.on("unhandledRejection", _gsdRejectionGuard);
57
- }
58
46
  }
59
47
 
60
48
  export function registerGsdExtension(pi: ExtensionAPI): void {
@@ -15,7 +15,6 @@ import { join } from "node:path";
15
15
  import { gsdRoot } from "./paths.js";
16
16
  import { atomicWriteSync } from "./atomic-write.js";
17
17
  import { effectiveLockFile } from "./session-lock.js";
18
- import { emitJournalEvent, queryJournal } from "./journal.js";
19
18
 
20
19
  export interface LockData {
21
20
  pid: number;
@@ -119,61 +118,3 @@ export function formatCrashInfo(lock: LockData): string {
119
118
 
120
119
  return lines.join("\n");
121
120
  }
122
-
123
- /**
124
- * Emit a synthetic unit-end event for a unit that crashed without emitting its own.
125
- *
126
- * Queries the journal to find the most recent unit-start for the crashed unit.
127
- * If a matching unit-end already exists (e.g. the hard timeout fired), this is a
128
- * no-op. Called during crash recovery, before clearing the stale lock.
129
- *
130
- * Addresses the gap reported in #3348 where `unit-start` was emitted but no
131
- * `unit-end` followed — side effects landed but the worker died before closeout.
132
- */
133
- export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): void {
134
- // Skip bootstrap / starting pseudo-units — they have no meaningful unit-start event.
135
- if (!lock.unitType || !lock.unitId || lock.unitType === "starting") return;
136
-
137
- try {
138
- const all = queryJournal(basePath);
139
-
140
- // Find the most recent unit-start for this unitId
141
- const starts = all.filter(
142
- (e) => e.eventType === "unit-start" && e.data?.unitId === lock.unitId,
143
- );
144
- if (starts.length === 0) return;
145
-
146
- const lastStart = starts[starts.length - 1];
147
-
148
- // Check if a unit-end was already emitted (e.g. hard timeout fired after the crash)
149
- const alreadyClosed = all.some(
150
- (e) =>
151
- e.eventType === "unit-end" &&
152
- e.data?.unitId === lock.unitId &&
153
- e.causedBy?.flowId === lastStart.flowId &&
154
- e.causedBy?.seq === lastStart.seq,
155
- );
156
- if (alreadyClosed) return;
157
-
158
- // Find the highest seq in this flow for monotonic ordering
159
- const maxSeq = all
160
- .filter((e) => e.flowId === lastStart.flowId)
161
- .reduce((max, e) => Math.max(max, e.seq), lastStart.seq);
162
-
163
- emitJournalEvent(basePath, {
164
- ts: new Date().toISOString(),
165
- flowId: lastStart.flowId,
166
- seq: maxSeq + 1,
167
- eventType: "unit-end",
168
- data: {
169
- unitType: lock.unitType,
170
- unitId: lock.unitId,
171
- status: "crash-recovered",
172
- artifactVerified: false,
173
- },
174
- causedBy: { flowId: lastStart.flowId, seq: lastStart.seq },
175
- });
176
- } catch {
177
- // Never throw from crash recovery path — journal failure must not block recovery
178
- }
179
- }
@@ -1564,23 +1564,6 @@ export interface TaskRow {
1564
1564
  sequence: number;
1565
1565
  }
1566
1566
 
1567
- function parseTaskArrayColumn(raw: unknown): string[] {
1568
- if (typeof raw !== "string" || raw.trim() === "") return [];
1569
-
1570
- try {
1571
- const parsed = JSON.parse(raw);
1572
- if (Array.isArray(parsed)) return parsed.map((value) => String(value));
1573
- if (parsed === null || parsed === undefined || parsed === "") return [];
1574
- return [String(parsed)];
1575
- } catch {
1576
- // Older/corrupt rows may contain comma-separated strings instead of JSON.
1577
- return raw
1578
- .split(",")
1579
- .map((value) => value.trim())
1580
- .filter(Boolean);
1581
- }
1582
- }
1583
-
1584
1567
  function rowToTask(row: Record<string, unknown>): TaskRow {
1585
1568
  const parseTaskArray = (value: unknown): string[] => {
1586
1569
  if (Array.isArray(value)) {
@@ -1620,8 +1603,8 @@ function rowToTask(row: Record<string, unknown>): TaskRow {
1620
1603
  blocker_discovered: (row["blocker_discovered"] as number) === 1,
1621
1604
  deviations: row["deviations"] as string,
1622
1605
  known_issues: row["known_issues"] as string,
1623
- key_files: parseTaskArrayColumn(row["key_files"]),
1624
- key_decisions: parseTaskArrayColumn(row["key_decisions"]),
1606
+ key_files: JSON.parse((row["key_files"] as string) || "[]"),
1607
+ key_decisions: JSON.parse((row["key_decisions"] as string) || "[]"),
1625
1608
  full_summary_md: row["full_summary_md"] as string,
1626
1609
  description: (row["description"] as string) ?? "",
1627
1610
  estimate: (row["estimate"] as string) ?? "",
@@ -2217,39 +2200,6 @@ export function deleteSlice(milestoneId: string, sliceId: string): void {
2217
2200
  });
2218
2201
  }
2219
2202
 
2220
- export function deleteMilestone(milestoneId: string): void {
2221
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2222
- transaction(() => {
2223
- currentDb!.prepare(
2224
- `DELETE FROM verification_evidence WHERE milestone_id = :mid`,
2225
- ).run({ ":mid": milestoneId });
2226
- currentDb!.prepare(
2227
- `DELETE FROM quality_gates WHERE milestone_id = :mid`,
2228
- ).run({ ":mid": milestoneId });
2229
- currentDb!.prepare(
2230
- `DELETE FROM tasks WHERE milestone_id = :mid`,
2231
- ).run({ ":mid": milestoneId });
2232
- currentDb!.prepare(
2233
- `DELETE FROM slice_dependencies WHERE milestone_id = :mid`,
2234
- ).run({ ":mid": milestoneId });
2235
- currentDb!.prepare(
2236
- `DELETE FROM slices WHERE milestone_id = :mid`,
2237
- ).run({ ":mid": milestoneId });
2238
- currentDb!.prepare(
2239
- `DELETE FROM replan_history WHERE milestone_id = :mid`,
2240
- ).run({ ":mid": milestoneId });
2241
- currentDb!.prepare(
2242
- `DELETE FROM assessments WHERE milestone_id = :mid`,
2243
- ).run({ ":mid": milestoneId });
2244
- currentDb!.prepare(
2245
- `DELETE FROM artifacts WHERE milestone_id = :mid`,
2246
- ).run({ ":mid": milestoneId });
2247
- currentDb!.prepare(
2248
- `DELETE FROM milestones WHERE id = :mid`,
2249
- ).run({ ":mid": milestoneId });
2250
- });
2251
- }
2252
-
2253
2203
  export function updateSliceFields(milestoneId: string, sliceId: string, fields: {
2254
2204
  title?: string;
2255
2205
  risk?: string;
@@ -20,8 +20,7 @@ import {
20
20
  } from "./paths.js";
21
21
  import { invalidateAllCaches } from "./cache.js";
22
22
  import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
23
- import { deleteMilestone, getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
24
- import { removeWorktree } from "./worktree-manager.js";
23
+ import { getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
25
24
  import { logWarning } from "./workflow-logger.js";
26
25
 
27
26
  // ─── Park ──────────────────────────────────────────────────────────────────
@@ -111,15 +110,6 @@ export function discardMilestone(basePath: string, milestoneId: string): boolean
111
110
  const mDir = resolveMilestonePath(basePath, milestoneId);
112
111
  if (!mDir || !existsSync(mDir)) return false;
113
112
 
114
- try {
115
- removeWorktree(basePath, milestoneId, {
116
- branch: `milestone/${milestoneId}`,
117
- deleteBranch: true,
118
- });
119
- } catch (err) {
120
- logWarning("engine", `discardMilestone worktree cleanup failed for ${milestoneId}: ${(err as Error).message}`);
121
- }
122
-
123
113
  rmSync(mDir, { recursive: true, force: true });
124
114
 
125
115
  // Prune from queue order if present
@@ -128,14 +118,6 @@ export function discardMilestone(basePath: string, milestoneId: string): boolean
128
118
  saveQueueOrder(basePath, order.filter(id => id !== milestoneId));
129
119
  }
130
120
 
131
- if (isDbAvailable()) {
132
- try {
133
- deleteMilestone(milestoneId);
134
- } catch (err) {
135
- logWarning("engine", `discardMilestone DB cleanup failed for ${milestoneId}: ${(err as Error).message}`);
136
- }
137
- }
138
-
139
121
  invalidateAllCaches();
140
122
  return true;
141
123
  }
@@ -15,14 +15,10 @@ import {
15
15
  getRequirementById,
16
16
  getActiveDecisions,
17
17
  getActiveRequirements,
18
+ getTask,
18
19
  transaction,
19
20
  _getAdapter,
20
21
  _resetProvider,
21
- insertMilestone,
22
- insertSlice,
23
- insertTask,
24
- getTask,
25
- getSliceTasks,
26
22
  } from '../gsd-db.ts';
27
23
 
28
24
  // ═══════════════════════════════════════════════════════════════════════════
@@ -464,60 +460,6 @@ describe('gsd-db', () => {
464
460
  assert.ok(!wasDbOpenAttempted(), 'wasDbOpenAttempted should reset after closeDatabase');
465
461
  });
466
462
 
467
- test('gsd-db: rowToTask tolerates corrupt comma-separated task arrays', () => {
468
- openDatabase(':memory:');
469
- insertMilestone({ id: 'M001', status: 'active' });
470
- insertSlice({ milestoneId: 'M001', id: 'S01', status: 'active' });
471
- insertTask({
472
- milestoneId: 'M001',
473
- sliceId: 'S01',
474
- id: 'T01',
475
- title: 'Recover corrupt arrays',
476
- planning: {
477
- description: 'desc',
478
- estimate: 'small',
479
- files: ['src/original.ts'],
480
- verify: 'npm test',
481
- inputs: ['docs/original.md'],
482
- expectedOutput: ['dist/original.md'],
483
- observabilityImpact: '',
484
- },
485
- });
486
-
487
- const adapter = _getAdapter()!;
488
- adapter.prepare(
489
- `UPDATE tasks
490
- SET files = ?, inputs = ?, expected_output = ?, key_files = ?, key_decisions = ?
491
- WHERE milestone_id = ? AND slice_id = ? AND id = ?`,
492
- ).run(
493
- 'src-erf/Models/foo.cs, src-erf/Models/bar.cs',
494
- 'docs/input-a.md, docs/input-b.md',
495
- 'dist/out-a.md, dist/out-b.md',
496
- 'src/resources/extensions/gsd/gsd-db.ts, src/resources/extensions/gsd/state.ts',
497
- '"decision-1"',
498
- 'M001',
499
- 'S01',
500
- 'T01',
501
- );
502
-
503
- const task = getTask('M001', 'S01', 'T01');
504
- assert.ok(task, 'getTask should still return the corrupt row');
505
- assert.deepStrictEqual(task!.files, ['src-erf/Models/foo.cs', 'src-erf/Models/bar.cs']);
506
- assert.deepStrictEqual(task!.inputs, ['docs/input-a.md', 'docs/input-b.md']);
507
- assert.deepStrictEqual(task!.expected_output, ['dist/out-a.md', 'dist/out-b.md']);
508
- assert.deepStrictEqual(
509
- task!.key_files,
510
- ['src/resources/extensions/gsd/gsd-db.ts', 'src/resources/extensions/gsd/state.ts'],
511
- );
512
- assert.deepStrictEqual(task!.key_decisions, ['decision-1']);
513
-
514
- const sliceTasks = getSliceTasks('M001', 'S01');
515
- assert.equal(sliceTasks.length, 1, 'getSliceTasks should also survive corrupt rows');
516
- assert.deepStrictEqual(sliceTasks[0]!.files, task!.files);
517
-
518
- closeDatabase();
519
- });
520
-
521
463
  // ─── Final Report ──────────────────────────────────────────────────────────
522
464
 
523
465
  });
@@ -3,22 +3,10 @@ import assert from 'node:assert/strict';
3
3
  import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { tmpdir } from 'node:os';
6
- import { execSync } from 'node:child_process';
7
6
 
8
7
  import { deriveState, invalidateStateCache, getActiveMilestoneId } from '../state.ts';
9
8
  import { clearPathCache } from '../paths.ts';
10
9
  import { parkMilestone, unparkMilestone, discardMilestone, isParked, getParkedReason } from '../milestone-actions.ts';
11
- import {
12
- closeDatabase,
13
- getMilestone,
14
- getMilestoneSlices,
15
- getSliceTasks,
16
- insertMilestone,
17
- insertSlice,
18
- insertTask,
19
- openDatabase,
20
- } from "../gsd-db.ts";
21
- import { createWorktree } from "../worktree-manager.ts";
22
10
 
23
11
 
24
12
 
@@ -72,29 +60,9 @@ function createMilestone(base: string, mid: string, opts?: { withRoadmap?: boole
72
60
  }
73
61
 
74
62
  function cleanup(base: string): void {
75
- try {
76
- closeDatabase();
77
- } catch {
78
- // ignore
79
- }
80
63
  rmSync(base, { recursive: true, force: true });
81
64
  }
82
65
 
83
- function run(cmd: string, cwd: string): string {
84
- return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
85
- }
86
-
87
- function initGitRepo(base: string): void {
88
- writeFileSync(join(base, "README.md"), "# test\n", "utf-8");
89
- writeFileSync(join(base, ".gsd", "STATE.md"), "# State\n", "utf-8");
90
- run("git init", base);
91
- run("git config user.email test@test.com", base);
92
- run("git config user.name Test", base);
93
- run("git add .", base);
94
- run('git commit -m "init"', base);
95
- run("git branch -M main", base);
96
- }
97
-
98
66
  function clearCaches(): void {
99
67
  clearPathCache();
100
68
  invalidateStateCache();
@@ -326,38 +294,6 @@ test('discardMilestone updates queue order', () => {
326
294
  }
327
295
  });
328
296
 
329
- test('discardMilestone removes DB rows, worktree, and milestone branch', () => {
330
- const base = createFixtureBase();
331
- try {
332
- createMilestone(base, 'M001', { withRoadmap: true });
333
- initGitRepo(base);
334
- clearCaches();
335
-
336
- assert.ok(openDatabase(join(base, '.gsd', 'gsd.db')), 'database opens');
337
- insertMilestone({ id: 'M001', title: 'Discard me', status: 'active' });
338
- insertSlice({ milestoneId: 'M001', id: 'S01', title: 'Only slice', status: 'pending' });
339
- insertTask({ milestoneId: 'M001', sliceId: 'S01', id: 'T01', title: 'Only task', status: 'pending' });
340
-
341
- const wt = createWorktree(base, 'M001', { branch: 'milestone/M001' });
342
- assert.ok(existsSync(wt.path), 'worktree exists before discard');
343
- assert.ok(run('git branch', base).includes('milestone/M001'), 'milestone branch exists before discard');
344
- assert.ok(getMilestone('M001'), 'milestone exists in DB before discard');
345
- assert.equal(getMilestoneSlices('M001').length, 1, 'slice exists in DB before discard');
346
- assert.equal(getSliceTasks('M001', 'S01').length, 1, 'task exists in DB before discard');
347
-
348
- const success = discardMilestone(base, 'M001');
349
- assert.ok(success, 'discardMilestone returns true');
350
-
351
- assert.equal(getMilestone('M001'), null, 'milestone row removed from DB');
352
- assert.equal(getMilestoneSlices('M001').length, 0, 'slice rows removed from DB');
353
- assert.equal(getSliceTasks('M001', 'S01').length, 0, 'task rows removed from DB');
354
- assert.ok(!existsSync(wt.path), 'worktree removed after discard');
355
- assert.ok(!run('git branch', base).includes('milestone/M001'), 'milestone branch removed after discard');
356
- } finally {
357
- cleanup(base);
358
- }
359
- });
360
-
361
297
  // ─── Test 12: All milestones parked → no active milestone ─────────────
362
298
  test('All milestones parked → no active', async () => {
363
299
  const base = createFixtureBase();
@@ -1,31 +0,0 @@
1
- /**
2
- * crash-log.ts — Write crash diagnostics to ~/.gsd/crash/<timestamp>.log
3
- *
4
- * Zero cross-dependencies: only uses Node.js built-ins so it can be imported
5
- * safely from uncaughtException / unhandledRejection handlers and from tests
6
- * without pulling in the full extension dependency tree.
7
- */
8
- import { appendFileSync, mkdirSync } from "node:fs";
9
- import { homedir } from "node:os";
10
- import { join } from "node:path";
11
- /**
12
- * Write a crash log to ~/.gsd/crash/<timestamp>.log (or $GSD_HOME/crash/).
13
- * Never throws — must be safe to call from any error handler.
14
- */
15
- export function writeCrashLog(err, source) {
16
- try {
17
- const crashDir = join(process.env.GSD_HOME ?? join(homedir(), ".gsd"), "crash");
18
- mkdirSync(crashDir, { recursive: true });
19
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
20
- const logPath = join(crashDir, `${ts}.log`);
21
- const lines = [
22
- `[gsd] ${source}: ${err.message}`,
23
- `timestamp: ${new Date().toISOString()}`,
24
- `pid: ${process.pid}`,
25
- err.stack ?? "(no stack trace available)",
26
- "",
27
- ];
28
- appendFileSync(logPath, lines.join("\n"));
29
- }
30
- catch { /* never throw from crash handler */ }
31
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * crash-log.ts — Write crash diagnostics to ~/.gsd/crash/<timestamp>.log
3
- *
4
- * Zero cross-dependencies: only uses Node.js built-ins so it can be imported
5
- * safely from uncaughtException / unhandledRejection handlers and from tests
6
- * without pulling in the full extension dependency tree.
7
- */
8
-
9
- import { appendFileSync, mkdirSync } from "node:fs";
10
- import { homedir } from "node:os";
11
- import { join } from "node:path";
12
-
13
- /**
14
- * Write a crash log to ~/.gsd/crash/<timestamp>.log (or $GSD_HOME/crash/).
15
- * Never throws — must be safe to call from any error handler.
16
- */
17
- export function writeCrashLog(err: Error, source: string): void {
18
- try {
19
- const crashDir = join(process.env.GSD_HOME ?? join(homedir(), ".gsd"), "crash");
20
- mkdirSync(crashDir, { recursive: true });
21
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
22
- const logPath = join(crashDir, `${ts}.log`);
23
- const lines = [
24
- `[gsd] ${source}: ${err.message}`,
25
- `timestamp: ${new Date().toISOString()}`,
26
- `pid: ${process.pid}`,
27
- err.stack ?? "(no stack trace available)",
28
- "",
29
- ];
30
- appendFileSync(logPath, lines.join("\n"));
31
- } catch { /* never throw from crash handler */ }
32
- }