@opengsd/gsd-pi 1.0.2-dev.50223bc → 1.0.2-dev.5961fbf

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 (251) hide show
  1. package/dist/resource-loader.d.ts +5 -0
  2. package/dist/resource-loader.js +24 -8
  3. package/dist/resources/.managed-resources-content-hash +1 -1
  4. package/dist/resources/extensions/gsd/auto/loop.js +19 -0
  5. package/dist/resources/extensions/gsd/auto/phases.js +1 -1
  6. package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
  7. package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
  8. package/dist/web/standalone/.next/BUILD_ID +1 -1
  9. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  10. package/dist/web/standalone/.next/build-manifest.json +2 -2
  11. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  12. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  29. package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
  30. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  31. package/dist/web/standalone/.next/server/app/index.html +1 -1
  32. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  39. package/dist/web/standalone/.next/server/chunks/1834.js +1 -1
  40. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  41. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  42. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  43. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  44. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  45. package/dist/web/standalone/package.json +0 -1
  46. package/dist/worktree-cli.d.ts +0 -2
  47. package/dist/worktree-cli.js +21 -9
  48. package/package.json +9 -4
  49. package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
  50. package/packages/cloud-mcp-gateway/package.json +5 -4
  51. package/packages/contracts/package.json +2 -2
  52. package/packages/daemon/bin/gsd-daemon.js +14 -0
  53. package/packages/daemon/bin/gsd-mcp-runtime.js +14 -0
  54. package/packages/daemon/bin/gsd-mcp.js +14 -0
  55. package/packages/daemon/dist/channel-manager.d.ts +53 -0
  56. package/packages/daemon/dist/channel-manager.d.ts.map +1 -0
  57. package/packages/daemon/dist/channel-manager.js +167 -0
  58. package/packages/daemon/dist/channel-manager.js.map +1 -0
  59. package/packages/daemon/dist/cli.d.ts +3 -0
  60. package/packages/daemon/dist/cli.d.ts.map +1 -0
  61. package/packages/daemon/dist/cli.js +94 -0
  62. package/packages/daemon/dist/cli.js.map +1 -0
  63. package/packages/daemon/dist/cloud-cli.d.ts +7 -0
  64. package/packages/daemon/dist/cloud-cli.d.ts.map +1 -0
  65. package/packages/daemon/dist/cloud-cli.js +96 -0
  66. package/packages/daemon/dist/cloud-cli.js.map +1 -0
  67. package/packages/daemon/dist/cloud-config.d.ts +18 -0
  68. package/packages/daemon/dist/cloud-config.d.ts.map +1 -0
  69. package/packages/daemon/dist/cloud-config.js +209 -0
  70. package/packages/daemon/dist/cloud-config.js.map +1 -0
  71. package/packages/daemon/dist/cloud-config.test.d.ts +2 -0
  72. package/packages/daemon/dist/cloud-config.test.d.ts.map +1 -0
  73. package/packages/daemon/dist/cloud-config.test.js +132 -0
  74. package/packages/daemon/dist/cloud-config.test.js.map +1 -0
  75. package/packages/daemon/dist/cloud-runtime.d.ts +26 -0
  76. package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -0
  77. package/packages/daemon/dist/cloud-runtime.js +180 -0
  78. package/packages/daemon/dist/cloud-runtime.js.map +1 -0
  79. package/packages/daemon/dist/cloud-runtime.test.d.ts +2 -0
  80. package/packages/daemon/dist/cloud-runtime.test.d.ts.map +1 -0
  81. package/packages/daemon/dist/cloud-runtime.test.js +28 -0
  82. package/packages/daemon/dist/cloud-runtime.test.js.map +1 -0
  83. package/packages/daemon/dist/cloud-token.d.ts +3 -0
  84. package/packages/daemon/dist/cloud-token.d.ts.map +1 -0
  85. package/packages/daemon/dist/cloud-token.js +37 -0
  86. package/packages/daemon/dist/cloud-token.js.map +1 -0
  87. package/packages/daemon/dist/commands.d.ts +25 -0
  88. package/packages/daemon/dist/commands.d.ts.map +1 -0
  89. package/packages/daemon/dist/commands.js +81 -0
  90. package/packages/daemon/dist/commands.js.map +1 -0
  91. package/packages/daemon/dist/config.d.ts +17 -0
  92. package/packages/daemon/dist/config.d.ts.map +1 -0
  93. package/packages/daemon/dist/config.js +146 -0
  94. package/packages/daemon/dist/config.js.map +1 -0
  95. package/packages/daemon/dist/daemon.d.ts +38 -0
  96. package/packages/daemon/dist/daemon.d.ts.map +1 -0
  97. package/packages/daemon/dist/daemon.js +194 -0
  98. package/packages/daemon/dist/daemon.js.map +1 -0
  99. package/packages/daemon/dist/daemon.test.d.ts +2 -0
  100. package/packages/daemon/dist/daemon.test.d.ts.map +1 -0
  101. package/packages/daemon/dist/daemon.test.js +692 -0
  102. package/packages/daemon/dist/daemon.test.js.map +1 -0
  103. package/packages/daemon/dist/discord-bot.d.ts +70 -0
  104. package/packages/daemon/dist/discord-bot.d.ts.map +1 -0
  105. package/packages/daemon/dist/discord-bot.js +433 -0
  106. package/packages/daemon/dist/discord-bot.js.map +1 -0
  107. package/packages/daemon/dist/discord-bot.test.d.ts +2 -0
  108. package/packages/daemon/dist/discord-bot.test.d.ts.map +1 -0
  109. package/packages/daemon/dist/discord-bot.test.js +667 -0
  110. package/packages/daemon/dist/discord-bot.test.js.map +1 -0
  111. package/packages/daemon/dist/event-bridge.d.ts +72 -0
  112. package/packages/daemon/dist/event-bridge.d.ts.map +1 -0
  113. package/packages/daemon/dist/event-bridge.js +366 -0
  114. package/packages/daemon/dist/event-bridge.js.map +1 -0
  115. package/packages/daemon/dist/event-bridge.test.d.ts +9 -0
  116. package/packages/daemon/dist/event-bridge.test.d.ts.map +1 -0
  117. package/packages/daemon/dist/event-bridge.test.js +528 -0
  118. package/packages/daemon/dist/event-bridge.test.js.map +1 -0
  119. package/packages/daemon/dist/event-formatter.d.ts +34 -0
  120. package/packages/daemon/dist/event-formatter.d.ts.map +1 -0
  121. package/packages/daemon/dist/event-formatter.js +355 -0
  122. package/packages/daemon/dist/event-formatter.js.map +1 -0
  123. package/packages/daemon/dist/event-formatter.test.d.ts +2 -0
  124. package/packages/daemon/dist/event-formatter.test.d.ts.map +1 -0
  125. package/packages/daemon/dist/event-formatter.test.js +333 -0
  126. package/packages/daemon/dist/event-formatter.test.js.map +1 -0
  127. package/packages/daemon/dist/index.d.ts +25 -0
  128. package/packages/daemon/dist/index.d.ts.map +1 -0
  129. package/packages/daemon/dist/index.js +17 -0
  130. package/packages/daemon/dist/index.js.map +1 -0
  131. package/packages/daemon/dist/launchd.d.ts +49 -0
  132. package/packages/daemon/dist/launchd.d.ts.map +1 -0
  133. package/packages/daemon/dist/launchd.js +188 -0
  134. package/packages/daemon/dist/launchd.js.map +1 -0
  135. package/packages/daemon/dist/launchd.test.d.ts +2 -0
  136. package/packages/daemon/dist/launchd.test.d.ts.map +1 -0
  137. package/packages/daemon/dist/launchd.test.js +296 -0
  138. package/packages/daemon/dist/launchd.test.js.map +1 -0
  139. package/packages/daemon/dist/local-tool-executor.d.ts +22 -0
  140. package/packages/daemon/dist/local-tool-executor.d.ts.map +1 -0
  141. package/packages/daemon/dist/local-tool-executor.js +307 -0
  142. package/packages/daemon/dist/local-tool-executor.js.map +1 -0
  143. package/packages/daemon/dist/local-tool-executor.test.d.ts +2 -0
  144. package/packages/daemon/dist/local-tool-executor.test.d.ts.map +1 -0
  145. package/packages/daemon/dist/local-tool-executor.test.js +111 -0
  146. package/packages/daemon/dist/local-tool-executor.test.js.map +1 -0
  147. package/packages/daemon/dist/logger.d.ts +25 -0
  148. package/packages/daemon/dist/logger.d.ts.map +1 -0
  149. package/packages/daemon/dist/logger.js +72 -0
  150. package/packages/daemon/dist/logger.js.map +1 -0
  151. package/packages/daemon/dist/mcp-cli.d.ts +3 -0
  152. package/packages/daemon/dist/mcp-cli.d.ts.map +1 -0
  153. package/packages/daemon/dist/mcp-cli.js +8 -0
  154. package/packages/daemon/dist/mcp-cli.js.map +1 -0
  155. package/packages/daemon/dist/mcp-cli.test.d.ts +2 -0
  156. package/packages/daemon/dist/mcp-cli.test.d.ts.map +1 -0
  157. package/packages/daemon/dist/mcp-cli.test.js +13 -0
  158. package/packages/daemon/dist/mcp-cli.test.js.map +1 -0
  159. package/packages/daemon/dist/mcp-runtime-cli.d.ts +3 -0
  160. package/packages/daemon/dist/mcp-runtime-cli.d.ts.map +1 -0
  161. package/packages/daemon/dist/mcp-runtime-cli.js +8 -0
  162. package/packages/daemon/dist/mcp-runtime-cli.js.map +1 -0
  163. package/packages/daemon/dist/message-batcher.d.ts +78 -0
  164. package/packages/daemon/dist/message-batcher.d.ts.map +1 -0
  165. package/packages/daemon/dist/message-batcher.js +173 -0
  166. package/packages/daemon/dist/message-batcher.js.map +1 -0
  167. package/packages/daemon/dist/message-batcher.test.d.ts +2 -0
  168. package/packages/daemon/dist/message-batcher.test.d.ts.map +1 -0
  169. package/packages/daemon/dist/message-batcher.test.js +242 -0
  170. package/packages/daemon/dist/message-batcher.test.js.map +1 -0
  171. package/packages/daemon/dist/orchestrator.d.ts +98 -0
  172. package/packages/daemon/dist/orchestrator.d.ts.map +1 -0
  173. package/packages/daemon/dist/orchestrator.js +359 -0
  174. package/packages/daemon/dist/orchestrator.js.map +1 -0
  175. package/packages/daemon/dist/orchestrator.test.d.ts +8 -0
  176. package/packages/daemon/dist/orchestrator.test.d.ts.map +1 -0
  177. package/packages/daemon/dist/orchestrator.test.js +425 -0
  178. package/packages/daemon/dist/orchestrator.test.js.map +1 -0
  179. package/packages/daemon/dist/project-scanner.d.ts +18 -0
  180. package/packages/daemon/dist/project-scanner.d.ts.map +1 -0
  181. package/packages/daemon/dist/project-scanner.js +90 -0
  182. package/packages/daemon/dist/project-scanner.js.map +1 -0
  183. package/packages/daemon/dist/project-scanner.test.d.ts +5 -0
  184. package/packages/daemon/dist/project-scanner.test.d.ts.map +1 -0
  185. package/packages/daemon/dist/project-scanner.test.js +183 -0
  186. package/packages/daemon/dist/project-scanner.test.js.map +1 -0
  187. package/packages/daemon/dist/session-manager.d.ts +70 -0
  188. package/packages/daemon/dist/session-manager.d.ts.map +1 -0
  189. package/packages/daemon/dist/session-manager.js +358 -0
  190. package/packages/daemon/dist/session-manager.js.map +1 -0
  191. package/packages/daemon/dist/session-manager.test.d.ts +9 -0
  192. package/packages/daemon/dist/session-manager.test.d.ts.map +1 -0
  193. package/packages/daemon/dist/session-manager.test.js +616 -0
  194. package/packages/daemon/dist/session-manager.test.js.map +1 -0
  195. package/packages/daemon/dist/types.d.ts +133 -0
  196. package/packages/daemon/dist/types.d.ts.map +1 -0
  197. package/packages/daemon/dist/types.js +8 -0
  198. package/packages/daemon/dist/types.js.map +1 -0
  199. package/packages/daemon/dist/verbosity.d.ts +27 -0
  200. package/packages/daemon/dist/verbosity.d.ts.map +1 -0
  201. package/packages/daemon/dist/verbosity.js +86 -0
  202. package/packages/daemon/dist/verbosity.js.map +1 -0
  203. package/packages/daemon/dist/verbosity.test.d.ts +2 -0
  204. package/packages/daemon/dist/verbosity.test.d.ts.map +1 -0
  205. package/packages/daemon/dist/verbosity.test.js +136 -0
  206. package/packages/daemon/dist/verbosity.test.js.map +1 -0
  207. package/packages/daemon/package.json +9 -8
  208. package/packages/gsd-agent-core/package.json +6 -6
  209. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  210. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
  211. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  212. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  213. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
  214. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  215. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
  216. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
  217. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
  218. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
  219. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  220. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
  221. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  222. package/packages/gsd-agent-modes/package.json +8 -8
  223. package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
  224. package/packages/mcp-server/package.json +6 -5
  225. package/packages/native/package.json +3 -3
  226. package/packages/pi-agent-core/package.json +4 -4
  227. package/packages/pi-ai/bin/pi-ai.js +14 -0
  228. package/packages/pi-ai/dist/models.generated.d.ts +0 -17
  229. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  230. package/packages/pi-ai/dist/models.generated.js +18 -35
  231. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  232. package/packages/pi-ai/package.json +5 -4
  233. package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
  234. package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
  235. package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
  236. package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
  237. package/packages/pi-coding-agent/package.json +9 -9
  238. package/packages/pi-tui/package.json +2 -2
  239. package/packages/rpc-client/package.json +3 -3
  240. package/pkg/package.json +1 -1
  241. package/scripts/ensure-workspace-builds.cjs +4 -4
  242. package/scripts/install/deps.js +10 -0
  243. package/src/resources/extensions/gsd/auto/loop.ts +22 -0
  244. package/src/resources/extensions/gsd/auto/phases.ts +1 -1
  245. package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
  246. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
  247. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
  248. package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
  249. package/dist/tsconfig.extensions.tsbuildinfo +0 -1
  250. /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_buildManifest.js +0 -0
  251. /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_ssgManifest.js +0 -0
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Tests for the project scanner module.
3
+ */
4
+ import { describe, it, afterEach } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, chmodSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { tmpdir, platform } from 'node:os';
9
+ import { randomUUID } from 'node:crypto';
10
+ import { scanForProjects } from './project-scanner.js';
11
+ // ---------- helpers ----------
12
+ function tmpDir() {
13
+ return mkdtempSync(join(tmpdir(), `scanner-test-${randomUUID().slice(0, 8)}-`));
14
+ }
15
+ const cleanupDirs = [];
16
+ afterEach(() => {
17
+ while (cleanupDirs.length) {
18
+ const d = cleanupDirs.pop();
19
+ if (existsSync(d))
20
+ rmSync(d, { recursive: true, force: true });
21
+ }
22
+ });
23
+ /** Create a project directory with specified marker files/dirs */
24
+ function createProject(root, name, markers) {
25
+ const projDir = join(root, name);
26
+ mkdirSync(projDir, { recursive: true });
27
+ for (const marker of markers) {
28
+ const markerPath = join(projDir, marker);
29
+ if (marker.startsWith('.') && !marker.includes('.')) {
30
+ // Likely a directory marker (.git, .gsd)
31
+ mkdirSync(markerPath, { recursive: true });
32
+ }
33
+ else {
34
+ // File marker (package.json, Cargo.toml, etc.)
35
+ writeFileSync(markerPath, '{}');
36
+ }
37
+ }
38
+ return projDir;
39
+ }
40
+ // ---------- tests ----------
41
+ describe('scanForProjects', () => {
42
+ it('finds projects with marker files', async () => {
43
+ const root = tmpDir();
44
+ cleanupDirs.push(root);
45
+ createProject(root, 'my-app', ['.git', 'package.json']);
46
+ const results = await scanForProjects([root]);
47
+ assert.equal(results.length, 1);
48
+ assert.equal(results[0].name, 'my-app');
49
+ assert.equal(results[0].path, join(root, 'my-app'));
50
+ assert.ok(results[0].markers.includes('git'));
51
+ assert.ok(results[0].markers.includes('node'));
52
+ assert.ok(results[0].lastModified > 0);
53
+ });
54
+ it('handles missing scan_root gracefully', async () => {
55
+ const results = await scanForProjects(['/nonexistent/path/that/does/not/exist']);
56
+ assert.deepEqual(results, []);
57
+ });
58
+ it('handles permission errors on entries', { skip: platform() === 'win32' ? 'chmod not reliable on Windows' : undefined }, async () => {
59
+ const root = tmpDir();
60
+ cleanupDirs.push(root);
61
+ // Create an accessible project
62
+ createProject(root, 'accessible', ['.git']);
63
+ // Create an inaccessible directory
64
+ const noAccess = join(root, 'locked');
65
+ mkdirSync(noAccess);
66
+ chmodSync(noAccess, 0o000);
67
+ const results = await scanForProjects([root]);
68
+ // Restore permissions for cleanup
69
+ chmodSync(noAccess, 0o755);
70
+ // Should find the accessible project but skip the locked one
71
+ assert.equal(results.length, 1);
72
+ assert.equal(results[0].name, 'accessible');
73
+ });
74
+ it('detects multiple marker types', async () => {
75
+ const root = tmpDir();
76
+ cleanupDirs.push(root);
77
+ createProject(root, 'full-stack', ['.git', 'package.json', '.gsd']);
78
+ const results = await scanForProjects([root]);
79
+ assert.equal(results.length, 1);
80
+ assert.equal(results[0].markers.length, 3);
81
+ assert.ok(results[0].markers.includes('git'));
82
+ assert.ok(results[0].markers.includes('node'));
83
+ assert.ok(results[0].markers.includes('gsd'));
84
+ });
85
+ it('returns results sorted alphabetically by name', async () => {
86
+ const root = tmpDir();
87
+ cleanupDirs.push(root);
88
+ createProject(root, 'zebra-project', ['.git']);
89
+ createProject(root, 'alpha-project', ['.git']);
90
+ createProject(root, 'middle-project', ['.git']);
91
+ const results = await scanForProjects([root]);
92
+ assert.equal(results.length, 3);
93
+ assert.equal(results[0].name, 'alpha-project');
94
+ assert.equal(results[1].name, 'middle-project');
95
+ assert.equal(results[2].name, 'zebra-project');
96
+ });
97
+ it('ignores hidden directories', async () => {
98
+ const root = tmpDir();
99
+ cleanupDirs.push(root);
100
+ createProject(root, 'visible', ['.git']);
101
+ createProject(root, '.hidden', ['.git']);
102
+ const results = await scanForProjects([root]);
103
+ assert.equal(results.length, 1);
104
+ assert.equal(results[0].name, 'visible');
105
+ });
106
+ it('ignores node_modules', async () => {
107
+ const root = tmpDir();
108
+ cleanupDirs.push(root);
109
+ createProject(root, 'real-project', ['package.json']);
110
+ createProject(root, 'node_modules', ['package.json']);
111
+ const results = await scanForProjects([root]);
112
+ assert.equal(results.length, 1);
113
+ assert.equal(results[0].name, 'real-project');
114
+ });
115
+ it('skips directories with no markers', async () => {
116
+ const root = tmpDir();
117
+ cleanupDirs.push(root);
118
+ createProject(root, 'has-markers', ['.git']);
119
+ // Create a plain directory with no markers
120
+ mkdirSync(join(root, 'no-markers'));
121
+ const results = await scanForProjects([root]);
122
+ assert.equal(results.length, 1);
123
+ assert.equal(results[0].name, 'has-markers');
124
+ });
125
+ it('scans multiple roots', async () => {
126
+ const root1 = tmpDir();
127
+ const root2 = tmpDir();
128
+ cleanupDirs.push(root1, root2);
129
+ createProject(root1, 'proj-a', ['.git']);
130
+ createProject(root2, 'proj-b', ['Cargo.toml']);
131
+ const results = await scanForProjects([root1, root2]);
132
+ assert.equal(results.length, 2);
133
+ assert.equal(results[0].name, 'proj-a');
134
+ assert.ok(results[0].markers.includes('git'));
135
+ assert.equal(results[1].name, 'proj-b');
136
+ assert.ok(results[1].markers.includes('rust'));
137
+ });
138
+ it('detects all supported marker types', async () => {
139
+ const root = tmpDir();
140
+ cleanupDirs.push(root);
141
+ createProject(root, 'git-proj', ['.git']);
142
+ createProject(root, 'node-proj', ['package.json']);
143
+ createProject(root, 'gsd-proj', ['.gsd']);
144
+ createProject(root, 'rust-proj', ['Cargo.toml']);
145
+ createProject(root, 'python-proj', ['pyproject.toml']);
146
+ createProject(root, 'go-proj', ['go.mod']);
147
+ const results = await scanForProjects([root]);
148
+ assert.equal(results.length, 6);
149
+ const byName = new Map(results.map(r => [r.name, r]));
150
+ assert.deepEqual(byName.get('git-proj').markers, ['git']);
151
+ assert.deepEqual(byName.get('node-proj').markers, ['node']);
152
+ assert.deepEqual(byName.get('gsd-proj').markers, ['gsd']);
153
+ assert.deepEqual(byName.get('rust-proj').markers, ['rust']);
154
+ assert.deepEqual(byName.get('python-proj').markers, ['python']);
155
+ assert.deepEqual(byName.get('go-proj').markers, ['go']);
156
+ });
157
+ it('skips non-directory entries', async () => {
158
+ const root = tmpDir();
159
+ cleanupDirs.push(root);
160
+ createProject(root, 'real-project', ['.git']);
161
+ // Create a regular file at the root level — should be ignored
162
+ writeFileSync(join(root, 'some-file.txt'), 'not a directory');
163
+ const results = await scanForProjects([root]);
164
+ assert.equal(results.length, 1);
165
+ assert.equal(results[0].name, 'real-project');
166
+ });
167
+ it('returns empty array for empty scan_roots', async () => {
168
+ const results = await scanForProjects([]);
169
+ assert.deepEqual(results, []);
170
+ });
171
+ it('deduplicates when same root appears twice', async () => {
172
+ const root = tmpDir();
173
+ cleanupDirs.push(root);
174
+ createProject(root, 'only-once', ['.git']);
175
+ const results = await scanForProjects([root, root]);
176
+ // Same directory scanned twice — results will have duplicates
177
+ // (this is acceptable; the caller can deduplicate by path if needed)
178
+ assert.equal(results.length, 2);
179
+ assert.equal(results[0].name, 'only-once');
180
+ assert.equal(results[1].name, 'only-once');
181
+ });
182
+ });
183
+ //# sourceMappingURL=project-scanner.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-scanner.test.js","sourceRoot":"","sources":["../src/project-scanner.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Tests for the project scanner module.\n */\n\nimport { describe, it, afterEach } from 'node:test';\nimport assert from 'node:assert/strict';\nimport { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, chmodSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { tmpdir, platform } from 'node:os';\nimport { randomUUID } from 'node:crypto';\nimport { scanForProjects } from './project-scanner.js';\n\n// ---------- helpers ----------\n\nfunction tmpDir(): string {\n return mkdtempSync(join(tmpdir(), `scanner-test-${randomUUID().slice(0, 8)}-`));\n}\n\nconst cleanupDirs: string[] = [];\nafterEach(() => {\n while (cleanupDirs.length) {\n const d = cleanupDirs.pop()!;\n if (existsSync(d)) rmSync(d, { recursive: true, force: true });\n }\n});\n\n/** Create a project directory with specified marker files/dirs */\nfunction createProject(root: string, name: string, markers: string[]): string {\n const projDir = join(root, name);\n mkdirSync(projDir, { recursive: true });\n for (const marker of markers) {\n const markerPath = join(projDir, marker);\n if (marker.startsWith('.') && !marker.includes('.')) {\n // Likely a directory marker (.git, .gsd)\n mkdirSync(markerPath, { recursive: true });\n } else {\n // File marker (package.json, Cargo.toml, etc.)\n writeFileSync(markerPath, '{}');\n }\n }\n return projDir;\n}\n\n// ---------- tests ----------\n\ndescribe('scanForProjects', () => {\n it('finds projects with marker files', async () => {\n const root = tmpDir();\n cleanupDirs.push(root);\n\n createProject(root, 'my-app', ['.git', 'package.json']);\n\n const results = await scanForProjects([root]);\n\n assert.equal(results.length, 1);\n assert.equal(results[0]!.name, 'my-app');\n assert.equal(results[0]!.path, join(root, 'my-app'));\n assert.ok(results[0]!.markers.includes('git'));\n assert.ok(results[0]!.markers.includes('node'));\n assert.ok(results[0]!.lastModified > 0);\n });\n\n it('handles missing scan_root gracefully', async () => {\n const results = await scanForProjects(['/nonexistent/path/that/does/not/exist']);\n assert.deepEqual(results, []);\n });\n\n it('handles permission errors on entries', { skip: platform() === 'win32' ? 'chmod not reliable on Windows' : undefined }, async () => {\n const root = tmpDir();\n cleanupDirs.push(root);\n\n // Create an accessible project\n createProject(root, 'accessible', ['.git']);\n\n // Create an inaccessible directory\n const noAccess = join(root, 'locked');\n mkdirSync(noAccess);\n chmodSync(noAccess, 0o000);\n\n const results = await scanForProjects([root]);\n\n // Restore permissions for cleanup\n chmodSync(noAccess, 0o755);\n\n // Should find the accessible project but skip the locked one\n assert.equal(results.length, 1);\n assert.equal(results[0]!.name, 'accessible');\n });\n\n it('detects multiple marker types', async () => {\n const root = tmpDir();\n cleanupDirs.push(root);\n\n createProject(root, 'full-stack', ['.git', 'package.json', '.gsd']);\n\n const results = await scanForProjects([root]);\n\n assert.equal(results.length, 1);\n assert.equal(results[0]!.markers.length, 3);\n assert.ok(results[0]!.markers.includes('git'));\n assert.ok(results[0]!.markers.includes('node'));\n assert.ok(results[0]!.markers.includes('gsd'));\n });\n\n it('returns results sorted alphabetically by name', async () => {\n const root = tmpDir();\n cleanupDirs.push(root);\n\n createProject(root, 'zebra-project', ['.git']);\n createProject(root, 'alpha-project', ['.git']);\n createProject(root, 'middle-project', ['.git']);\n\n const results = await scanForProjects([root]);\n\n assert.equal(results.length, 3);\n assert.equal(results[0]!.name, 'alpha-project');\n assert.equal(results[1]!.name, 'middle-project');\n assert.equal(results[2]!.name, 'zebra-project');\n });\n\n it('ignores hidden directories', async () => {\n const root = tmpDir();\n cleanupDirs.push(root);\n\n createProject(root, 'visible', ['.git']);\n createProject(root, '.hidden', ['.git']);\n\n const results = await scanForProjects([root]);\n\n assert.equal(results.length, 1);\n assert.equal(results[0]!.name, 'visible');\n });\n\n it('ignores node_modules', async () => {\n const root = tmpDir();\n cleanupDirs.push(root);\n\n createProject(root, 'real-project', ['package.json']);\n createProject(root, 'node_modules', ['package.json']);\n\n const results = await scanForProjects([root]);\n\n assert.equal(results.length, 1);\n assert.equal(results[0]!.name, 'real-project');\n });\n\n it('skips directories with no markers', async () => {\n const root = tmpDir();\n cleanupDirs.push(root);\n\n createProject(root, 'has-markers', ['.git']);\n // Create a plain directory with no markers\n mkdirSync(join(root, 'no-markers'));\n\n const results = await scanForProjects([root]);\n\n assert.equal(results.length, 1);\n assert.equal(results[0]!.name, 'has-markers');\n });\n\n it('scans multiple roots', async () => {\n const root1 = tmpDir();\n const root2 = tmpDir();\n cleanupDirs.push(root1, root2);\n\n createProject(root1, 'proj-a', ['.git']);\n createProject(root2, 'proj-b', ['Cargo.toml']);\n\n const results = await scanForProjects([root1, root2]);\n\n assert.equal(results.length, 2);\n assert.equal(results[0]!.name, 'proj-a');\n assert.ok(results[0]!.markers.includes('git'));\n assert.equal(results[1]!.name, 'proj-b');\n assert.ok(results[1]!.markers.includes('rust'));\n });\n\n it('detects all supported marker types', async () => {\n const root = tmpDir();\n cleanupDirs.push(root);\n\n createProject(root, 'git-proj', ['.git']);\n createProject(root, 'node-proj', ['package.json']);\n createProject(root, 'gsd-proj', ['.gsd']);\n createProject(root, 'rust-proj', ['Cargo.toml']);\n createProject(root, 'python-proj', ['pyproject.toml']);\n createProject(root, 'go-proj', ['go.mod']);\n\n const results = await scanForProjects([root]);\n\n assert.equal(results.length, 6);\n\n const byName = new Map(results.map(r => [r.name, r]));\n assert.deepEqual(byName.get('git-proj')!.markers, ['git']);\n assert.deepEqual(byName.get('node-proj')!.markers, ['node']);\n assert.deepEqual(byName.get('gsd-proj')!.markers, ['gsd']);\n assert.deepEqual(byName.get('rust-proj')!.markers, ['rust']);\n assert.deepEqual(byName.get('python-proj')!.markers, ['python']);\n assert.deepEqual(byName.get('go-proj')!.markers, ['go']);\n });\n\n it('skips non-directory entries', async () => {\n const root = tmpDir();\n cleanupDirs.push(root);\n\n createProject(root, 'real-project', ['.git']);\n // Create a regular file at the root level — should be ignored\n writeFileSync(join(root, 'some-file.txt'), 'not a directory');\n\n const results = await scanForProjects([root]);\n\n assert.equal(results.length, 1);\n assert.equal(results[0]!.name, 'real-project');\n });\n\n it('returns empty array for empty scan_roots', async () => {\n const results = await scanForProjects([]);\n assert.deepEqual(results, []);\n });\n\n it('deduplicates when same root appears twice', async () => {\n const root = tmpDir();\n cleanupDirs.push(root);\n\n createProject(root, 'only-once', ['.git']);\n\n const results = await scanForProjects([root, root]);\n\n // Same directory scanned twice — results will have duplicates\n // (this is acceptable; the caller can deduplicate by path if needed)\n assert.equal(results.length, 2);\n assert.equal(results[0]!.name, 'only-once');\n assert.equal(results[1]!.name, 'only-once');\n });\n});\n"]}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * SessionManager — manages RpcClient lifecycle for daemon-driven GSD execution.
3
+ *
4
+ * Extends EventEmitter to emit typed session lifecycle events.
5
+ * One active session per projectDir. Tracks events in a ring buffer,
6
+ * detects blockers, tracks terminal state, and accumulates cost using
7
+ * the cumulative-max pattern (K004).
8
+ *
9
+ * Adapted from packages/mcp-server/src/session-manager.ts with:
10
+ * - Logger integration for structured logging
11
+ * - EventEmitter for session lifecycle events
12
+ * - getAllSessions() for cross-project status (R035)
13
+ * - projectName field on ManagedSession
14
+ */
15
+ import { EventEmitter } from 'node:events';
16
+ import type { ManagedSession, StartSessionOptions } from './types.js';
17
+ import type { Logger } from './logger.js';
18
+ export declare class SessionManager extends EventEmitter {
19
+ private readonly logger;
20
+ /** Sessions keyed by resolved projectDir for duplicate-start prevention */
21
+ private sessions;
22
+ constructor(logger: Logger);
23
+ /**
24
+ * Start a new GSD auto-mode session for the given project directory.
25
+ *
26
+ * Rejects if a session already exists for this projectDir.
27
+ * Creates an RpcClient, starts the process, performs the v2 init handshake,
28
+ * wires event tracking, and sends '/gsd auto' to begin execution.
29
+ */
30
+ startSession(options: StartSessionOptions): Promise<string>;
31
+ /**
32
+ * Look up a session by sessionId.
33
+ * Linear scan is fine — we expect <10 concurrent sessions.
34
+ */
35
+ getSession(sessionId: string): ManagedSession | undefined;
36
+ /**
37
+ * Look up a session by project directory (direct map lookup).
38
+ */
39
+ getSessionByDir(projectDir: string): ManagedSession | undefined;
40
+ /**
41
+ * Return all tracked sessions (R035 — cross-project status).
42
+ */
43
+ getAllSessions(): ManagedSession[];
44
+ /**
45
+ * Resolve a pending blocker by sending a UI response.
46
+ */
47
+ resolveBlocker(sessionId: string, response: string): Promise<void>;
48
+ /**
49
+ * Cancel a running session — abort current operation then stop the process.
50
+ */
51
+ cancelSession(sessionId: string): Promise<void>;
52
+ cancelSessionByDir(projectDir: string): Promise<void>;
53
+ /**
54
+ * Build a HeadlessJsonResult-shaped object from accumulated session state.
55
+ */
56
+ getResult(sessionId: string): Record<string, unknown>;
57
+ /**
58
+ * Stop all active sessions and clean up resources.
59
+ */
60
+ cleanup(): Promise<void>;
61
+ /**
62
+ * Resolve the GSD CLI path.
63
+ *
64
+ * 1. GSD_CLI_PATH env var (highest priority)
65
+ * 2. `which gsd` → resolve to the actual dist/cli.js
66
+ */
67
+ static resolveCLIPath(): string;
68
+ private handleEvent;
69
+ }
70
+ //# sourceMappingURL=session-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EACV,cAAc,EACd,mBAAmB,EAEpB,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAwC1C,qBAAa,cAAe,SAAQ,YAAY;IAIlC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHnC,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAqC;gBAExB,MAAM,EAAE,MAAM;IAI3C;;;;;;OAMG;IACG,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;IAyFjE;;;OAGG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAOzD;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAI/D;;OAEG;IACH,cAAc,IAAI,cAAc,EAAE;IAIlC;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBxE;;OAEG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB/C,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM3D;;OAEG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAqBrD;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB9B;;;;;OAKG;IACH,MAAM,CAAC,cAAc,IAAI,MAAM;IAoB/B,OAAO,CAAC,WAAW;CAuEpB"}
@@ -0,0 +1,358 @@
1
+ /**
2
+ * SessionManager — manages RpcClient lifecycle for daemon-driven GSD execution.
3
+ *
4
+ * Extends EventEmitter to emit typed session lifecycle events.
5
+ * One active session per projectDir. Tracks events in a ring buffer,
6
+ * detects blockers, tracks terminal state, and accumulates cost using
7
+ * the cumulative-max pattern (K004).
8
+ *
9
+ * Adapted from packages/mcp-server/src/session-manager.ts with:
10
+ * - Logger integration for structured logging
11
+ * - EventEmitter for session lifecycle events
12
+ * - getAllSessions() for cross-project status (R035)
13
+ * - projectName field on ManagedSession
14
+ */
15
+ import { execSync } from 'node:child_process';
16
+ import { basename, resolve } from 'node:path';
17
+ import { EventEmitter } from 'node:events';
18
+ import { RpcClient } from '@opengsd/rpc-client';
19
+ import { MAX_EVENTS, INIT_TIMEOUT_MS } from './types.js';
20
+ // ---------------------------------------------------------------------------
21
+ // Inlined detection logic (from headless-events.ts — no internal package imports)
22
+ // ---------------------------------------------------------------------------
23
+ const FIRE_AND_FORGET_METHODS = new Set([
24
+ 'notify', 'setStatus', 'setWidget', 'setTitle', 'set_editor_text',
25
+ ]);
26
+ const TERMINAL_PREFIXES = [
27
+ 'auto-mode stopped',
28
+ 'step-mode stopped',
29
+ 'auto-mode complete',
30
+ 'no active milestone',
31
+ 'auto-mode idle',
32
+ ];
33
+ function isTerminalNotification(event) {
34
+ if (event.type !== 'extension_ui_request' || event.method !== 'notify')
35
+ return false;
36
+ const message = String(event.message ?? '').toLowerCase();
37
+ return TERMINAL_PREFIXES.some((prefix) => message.startsWith(prefix));
38
+ }
39
+ function isBlockedNotification(event) {
40
+ if (event.type !== 'extension_ui_request' || event.method !== 'notify')
41
+ return false;
42
+ const message = String(event.message ?? '').toLowerCase();
43
+ return message.includes('blocked:');
44
+ }
45
+ function isBlockingUIRequest(event) {
46
+ if (event.type !== 'extension_ui_request')
47
+ return false;
48
+ const method = String(event.method ?? '');
49
+ return !FIRE_AND_FORGET_METHODS.has(method);
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // SessionManager
53
+ // ---------------------------------------------------------------------------
54
+ export class SessionManager extends EventEmitter {
55
+ logger;
56
+ /** Sessions keyed by resolved projectDir for duplicate-start prevention */
57
+ sessions = new Map();
58
+ constructor(logger) {
59
+ super();
60
+ this.logger = logger;
61
+ }
62
+ /**
63
+ * Start a new GSD auto-mode session for the given project directory.
64
+ *
65
+ * Rejects if a session already exists for this projectDir.
66
+ * Creates an RpcClient, starts the process, performs the v2 init handshake,
67
+ * wires event tracking, and sends '/gsd auto' to begin execution.
68
+ */
69
+ async startSession(options) {
70
+ const { projectDir } = options;
71
+ if (!projectDir || projectDir.trim() === '') {
72
+ throw new Error('projectDir is required and cannot be empty');
73
+ }
74
+ const resolvedDir = resolve(projectDir);
75
+ const projectName = basename(resolvedDir);
76
+ const existing = this.sessions.get(resolvedDir);
77
+ if (existing) {
78
+ throw new Error(`Session already active for ${resolvedDir} (sessionId: ${existing.sessionId}, status: ${existing.status})`);
79
+ }
80
+ const cliPath = options.cliPath ?? SessionManager.resolveCLIPath();
81
+ const args = ['--mode', 'rpc'];
82
+ if (options.model)
83
+ args.push('--model', options.model);
84
+ if (options.bare)
85
+ args.push('--bare');
86
+ const client = new RpcClient({
87
+ cliPath,
88
+ cwd: resolvedDir,
89
+ args,
90
+ });
91
+ // Build the session shell before async operations so we can track state
92
+ const session = {
93
+ sessionId: '', // filled after init
94
+ projectDir: resolvedDir,
95
+ projectName,
96
+ status: 'starting',
97
+ client,
98
+ events: [],
99
+ pendingBlocker: null,
100
+ cost: { totalCost: 0, tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },
101
+ startTime: Date.now(),
102
+ };
103
+ // Insert into map early (keyed by dir) so concurrent starts are rejected
104
+ this.sessions.set(resolvedDir, session);
105
+ try {
106
+ // Start the process with timeout
107
+ await Promise.race([
108
+ client.start(),
109
+ timeout(INIT_TIMEOUT_MS, `RpcClient.start() timed out after ${INIT_TIMEOUT_MS}ms`),
110
+ ]);
111
+ // Perform v2 init handshake
112
+ const initResult = await Promise.race([
113
+ client.init(),
114
+ timeout(INIT_TIMEOUT_MS, `RpcClient.init() timed out after ${INIT_TIMEOUT_MS}ms`),
115
+ ]);
116
+ session.sessionId = initResult.sessionId;
117
+ session.status = 'running';
118
+ // Wire event tracking
119
+ session.unsubscribe = client.onEvent((event) => {
120
+ this.handleEvent(session, event);
121
+ });
122
+ // Kick off auto-mode
123
+ const command = options.command ?? '/gsd auto';
124
+ await client.prompt(command);
125
+ this.logger.info('session started', { sessionId: session.sessionId, projectDir: resolvedDir });
126
+ this.emit('session:started', { sessionId: session.sessionId, projectDir: resolvedDir, projectName });
127
+ return session.sessionId;
128
+ }
129
+ catch (err) {
130
+ session.status = 'error';
131
+ session.error = err instanceof Error ? err.message : String(err);
132
+ // Attempt cleanup
133
+ try {
134
+ await client.stop();
135
+ }
136
+ catch { /* swallow cleanup errors */ }
137
+ this.logger.error('session error', { sessionId: session.sessionId, projectDir: resolvedDir, error: session.error });
138
+ this.emit('session:error', { sessionId: session.sessionId, projectDir: resolvedDir, projectName, error: session.error });
139
+ // Keep session in map so callers can inspect the error
140
+ throw new Error(`Failed to start session for ${resolvedDir}: ${session.error}`);
141
+ }
142
+ }
143
+ /**
144
+ * Look up a session by sessionId.
145
+ * Linear scan is fine — we expect <10 concurrent sessions.
146
+ */
147
+ getSession(sessionId) {
148
+ for (const session of this.sessions.values()) {
149
+ if (session.sessionId === sessionId)
150
+ return session;
151
+ }
152
+ return undefined;
153
+ }
154
+ /**
155
+ * Look up a session by project directory (direct map lookup).
156
+ */
157
+ getSessionByDir(projectDir) {
158
+ return this.sessions.get(resolve(projectDir));
159
+ }
160
+ /**
161
+ * Return all tracked sessions (R035 — cross-project status).
162
+ */
163
+ getAllSessions() {
164
+ return Array.from(this.sessions.values());
165
+ }
166
+ /**
167
+ * Resolve a pending blocker by sending a UI response.
168
+ */
169
+ async resolveBlocker(sessionId, response) {
170
+ const session = this.getSession(sessionId);
171
+ if (!session)
172
+ throw new Error(`Session not found: ${sessionId}`);
173
+ if (!session.pendingBlocker)
174
+ throw new Error(`No pending blocker for session ${sessionId}`);
175
+ const blocker = session.pendingBlocker;
176
+ session.client.sendUIResponse(blocker.id, { value: response });
177
+ session.pendingBlocker = null;
178
+ if (session.status === 'blocked') {
179
+ session.status = 'running';
180
+ }
181
+ this.logger.info('blocker resolved', {
182
+ sessionId,
183
+ projectDir: session.projectDir,
184
+ blockerId: blocker.id,
185
+ blockerMethod: blocker.method,
186
+ });
187
+ }
188
+ /**
189
+ * Cancel a running session — abort current operation then stop the process.
190
+ */
191
+ async cancelSession(sessionId) {
192
+ const session = this.getSession(sessionId);
193
+ if (!session)
194
+ throw new Error(`Session not found: ${sessionId}`);
195
+ try {
196
+ await session.client.abort();
197
+ }
198
+ catch { /* may already be stopped */ }
199
+ try {
200
+ await session.client.stop();
201
+ }
202
+ catch { /* swallow */ }
203
+ session.status = 'cancelled';
204
+ session.unsubscribe?.();
205
+ this.logger.info('session cancelled', { sessionId, projectDir: session.projectDir });
206
+ }
207
+ async cancelSessionByDir(projectDir) {
208
+ const session = this.getSessionByDir(projectDir);
209
+ if (!session)
210
+ throw new Error(`Session not found for projectDir: ${projectDir}`);
211
+ await this.cancelSession(session.sessionId);
212
+ }
213
+ /**
214
+ * Build a HeadlessJsonResult-shaped object from accumulated session state.
215
+ */
216
+ getResult(sessionId) {
217
+ const session = this.getSession(sessionId);
218
+ if (!session)
219
+ throw new Error(`Session not found: ${sessionId}`);
220
+ const durationMs = Date.now() - session.startTime;
221
+ return {
222
+ sessionId: session.sessionId,
223
+ projectDir: session.projectDir,
224
+ projectName: session.projectName,
225
+ status: session.status,
226
+ durationMs,
227
+ cost: session.cost,
228
+ recentEvents: session.events.slice(-10),
229
+ pendingBlocker: session.pendingBlocker
230
+ ? { id: session.pendingBlocker.id, method: session.pendingBlocker.method, message: session.pendingBlocker.message }
231
+ : null,
232
+ error: session.error ?? null,
233
+ };
234
+ }
235
+ /**
236
+ * Stop all active sessions and clean up resources.
237
+ */
238
+ async cleanup() {
239
+ const stopPromises = [];
240
+ for (const session of this.sessions.values()) {
241
+ session.unsubscribe?.();
242
+ if (session.status === 'running' || session.status === 'starting' || session.status === 'blocked') {
243
+ stopPromises.push(session.client.stop().catch(() => { }));
244
+ session.status = 'cancelled';
245
+ }
246
+ }
247
+ await Promise.allSettled(stopPromises);
248
+ }
249
+ /**
250
+ * Resolve the GSD CLI path.
251
+ *
252
+ * 1. GSD_CLI_PATH env var (highest priority)
253
+ * 2. `which gsd` → resolve to the actual dist/cli.js
254
+ */
255
+ static resolveCLIPath() {
256
+ const envPath = process.env['GSD_CLI_PATH'];
257
+ if (envPath)
258
+ return resolve(envPath);
259
+ try {
260
+ const gsdBin = execSync('which gsd', { encoding: 'utf-8' }).trim();
261
+ if (gsdBin)
262
+ return resolve(gsdBin);
263
+ }
264
+ catch {
265
+ // which failed
266
+ }
267
+ throw new Error('Cannot find GSD CLI. Set GSD_CLI_PATH environment variable or ensure `gsd` is in PATH.');
268
+ }
269
+ // ---------------------------------------------------------------------------
270
+ // Private: Event Handling
271
+ // ---------------------------------------------------------------------------
272
+ handleEvent(session, event) {
273
+ // Ring buffer: push and trim
274
+ session.events.push(event);
275
+ if (session.events.length > MAX_EVENTS) {
276
+ session.events.splice(0, session.events.length - MAX_EVENTS);
277
+ }
278
+ // Forward event to listeners
279
+ this.logger.debug('session event', { sessionId: session.sessionId, type: event.type });
280
+ this.emit('session:event', { sessionId: session.sessionId, projectDir: session.projectDir, event });
281
+ // Cost tracking (K004 — cumulative-max)
282
+ if (event.type === 'cost_update') {
283
+ const costEvent = event;
284
+ session.cost.totalCost = Math.max(session.cost.totalCost, costEvent.cumulativeCost ?? 0);
285
+ if (costEvent.tokens) {
286
+ session.cost.tokens.input = Math.max(session.cost.tokens.input, costEvent.tokens.input ?? 0);
287
+ session.cost.tokens.output = Math.max(session.cost.tokens.output, costEvent.tokens.output ?? 0);
288
+ session.cost.tokens.cacheRead = Math.max(session.cost.tokens.cacheRead, costEvent.tokens.cacheRead ?? 0);
289
+ session.cost.tokens.cacheWrite = Math.max(session.cost.tokens.cacheWrite, costEvent.tokens.cacheWrite ?? 0);
290
+ }
291
+ }
292
+ // Terminal detection — auto-mode/step-mode stopped
293
+ if (isTerminalNotification(event)) {
294
+ if (isBlockedNotification(event)) {
295
+ session.status = 'blocked';
296
+ session.pendingBlocker = extractBlocker(event);
297
+ this.logger.info('session blocked', {
298
+ sessionId: session.sessionId,
299
+ projectDir: session.projectDir,
300
+ blockerId: session.pendingBlocker.id,
301
+ blockerMethod: session.pendingBlocker.method,
302
+ });
303
+ this.emit('session:blocked', {
304
+ sessionId: session.sessionId,
305
+ projectDir: session.projectDir,
306
+ projectName: session.projectName,
307
+ blocker: session.pendingBlocker,
308
+ });
309
+ }
310
+ else {
311
+ session.status = 'completed';
312
+ session.unsubscribe?.();
313
+ this.logger.info('session completed', { sessionId: session.sessionId, projectDir: session.projectDir });
314
+ this.emit('session:completed', {
315
+ sessionId: session.sessionId,
316
+ projectDir: session.projectDir,
317
+ projectName: session.projectName,
318
+ });
319
+ }
320
+ return;
321
+ }
322
+ // Blocker detection — non-fire-and-forget extension_ui_request
323
+ if (isBlockingUIRequest(event)) {
324
+ session.status = 'blocked';
325
+ session.pendingBlocker = extractBlocker(event);
326
+ this.logger.info('session blocked', {
327
+ sessionId: session.sessionId,
328
+ projectDir: session.projectDir,
329
+ blockerId: session.pendingBlocker.id,
330
+ blockerMethod: session.pendingBlocker.method,
331
+ });
332
+ this.emit('session:blocked', {
333
+ sessionId: session.sessionId,
334
+ projectDir: session.projectDir,
335
+ projectName: session.projectName,
336
+ blocker: session.pendingBlocker,
337
+ });
338
+ }
339
+ }
340
+ }
341
+ // ---------------------------------------------------------------------------
342
+ // Helpers
343
+ // ---------------------------------------------------------------------------
344
+ function timeout(ms, message) {
345
+ return new Promise((_, reject) => {
346
+ setTimeout(() => reject(new Error(message)), ms);
347
+ });
348
+ }
349
+ function extractBlocker(event) {
350
+ const uiEvent = event;
351
+ return {
352
+ id: String(uiEvent.id ?? ''),
353
+ method: uiEvent.method,
354
+ message: String(uiEvent.title ?? uiEvent.message ?? ''),
355
+ event: uiEvent,
356
+ };
357
+ }
358
+ //# sourceMappingURL=session-manager.js.map