@mrpatronz/nexusflow 0.1.0

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 (212) hide show
  1. package/GETTING_STARTED.md +109 -0
  2. package/README.md +310 -0
  3. package/dist/analyzers/detect-apis.d.ts +19 -0
  4. package/dist/analyzers/detect-apis.d.ts.map +1 -0
  5. package/dist/analyzers/detect-apis.js +121 -0
  6. package/dist/analyzers/detect-apis.js.map +1 -0
  7. package/dist/analyzers/detect-deps.d.ts +30 -0
  8. package/dist/analyzers/detect-deps.d.ts.map +1 -0
  9. package/dist/analyzers/detect-deps.js +121 -0
  10. package/dist/analyzers/detect-deps.js.map +1 -0
  11. package/dist/analyzers/detect-existing.d.ts +15 -0
  12. package/dist/analyzers/detect-existing.d.ts.map +1 -0
  13. package/dist/analyzers/detect-existing.js +65 -0
  14. package/dist/analyzers/detect-existing.js.map +1 -0
  15. package/dist/analyzers/detect-ports.d.ts +20 -0
  16. package/dist/analyzers/detect-ports.d.ts.map +1 -0
  17. package/dist/analyzers/detect-ports.js +103 -0
  18. package/dist/analyzers/detect-ports.js.map +1 -0
  19. package/dist/analyzers/index.d.ts +29 -0
  20. package/dist/analyzers/index.d.ts.map +1 -0
  21. package/dist/analyzers/index.js +74 -0
  22. package/dist/analyzers/index.js.map +1 -0
  23. package/dist/analyzers/readme-summarizer.d.ts +13 -0
  24. package/dist/analyzers/readme-summarizer.d.ts.map +1 -0
  25. package/dist/analyzers/readme-summarizer.js +59 -0
  26. package/dist/analyzers/readme-summarizer.js.map +1 -0
  27. package/dist/analyzers/tech-stack.d.ts +15 -0
  28. package/dist/analyzers/tech-stack.d.ts.map +1 -0
  29. package/dist/analyzers/tech-stack.js +284 -0
  30. package/dist/analyzers/tech-stack.js.map +1 -0
  31. package/dist/commands/create.d.ts +17 -0
  32. package/dist/commands/create.d.ts.map +1 -0
  33. package/dist/commands/create.js +111 -0
  34. package/dist/commands/create.js.map +1 -0
  35. package/dist/commands/init.d.ts +10 -0
  36. package/dist/commands/init.d.ts.map +1 -0
  37. package/dist/commands/init.js +50 -0
  38. package/dist/commands/init.js.map +1 -0
  39. package/dist/commands/list.d.ts +9 -0
  40. package/dist/commands/list.d.ts.map +1 -0
  41. package/dist/commands/list.js +32 -0
  42. package/dist/commands/list.js.map +1 -0
  43. package/dist/commands/logs.d.ts +12 -0
  44. package/dist/commands/logs.d.ts.map +1 -0
  45. package/dist/commands/logs.js +54 -0
  46. package/dist/commands/logs.js.map +1 -0
  47. package/dist/commands/open.d.ts +9 -0
  48. package/dist/commands/open.d.ts.map +1 -0
  49. package/dist/commands/open.js +49 -0
  50. package/dist/commands/open.js.map +1 -0
  51. package/dist/commands/start.d.ts +13 -0
  52. package/dist/commands/start.d.ts.map +1 -0
  53. package/dist/commands/start.js +97 -0
  54. package/dist/commands/start.js.map +1 -0
  55. package/dist/commands/status.d.ts +11 -0
  56. package/dist/commands/status.d.ts.map +1 -0
  57. package/dist/commands/status.js +47 -0
  58. package/dist/commands/status.js.map +1 -0
  59. package/dist/commands/stop.d.ts +11 -0
  60. package/dist/commands/stop.d.ts.map +1 -0
  61. package/dist/commands/stop.js +47 -0
  62. package/dist/commands/stop.js.map +1 -0
  63. package/dist/commands/ui.d.ts +13 -0
  64. package/dist/commands/ui.d.ts.map +1 -0
  65. package/dist/commands/ui.js +44 -0
  66. package/dist/commands/ui.js.map +1 -0
  67. package/dist/core/config.d.ts +32 -0
  68. package/dist/core/config.d.ts.map +1 -0
  69. package/dist/core/config.js +68 -0
  70. package/dist/core/config.js.map +1 -0
  71. package/dist/core/scanner.d.ts +18 -0
  72. package/dist/core/scanner.d.ts.map +1 -0
  73. package/dist/core/scanner.js +73 -0
  74. package/dist/core/scanner.js.map +1 -0
  75. package/dist/core/workspace.d.ts +53 -0
  76. package/dist/core/workspace.d.ts.map +1 -0
  77. package/dist/core/workspace.js +114 -0
  78. package/dist/core/workspace.js.map +1 -0
  79. package/dist/core/worktree.d.ts +27 -0
  80. package/dist/core/worktree.d.ts.map +1 -0
  81. package/dist/core/worktree.js +35 -0
  82. package/dist/core/worktree.js.map +1 -0
  83. package/dist/generators/base.d.ts +15 -0
  84. package/dist/generators/base.d.ts.map +1 -0
  85. package/dist/generators/base.js +175 -0
  86. package/dist/generators/base.js.map +1 -0
  87. package/dist/generators/claude.d.ts +17 -0
  88. package/dist/generators/claude.d.ts.map +1 -0
  89. package/dist/generators/claude.js +29 -0
  90. package/dist/generators/claude.js.map +1 -0
  91. package/dist/generators/codex.d.ts +16 -0
  92. package/dist/generators/codex.d.ts.map +1 -0
  93. package/dist/generators/codex.js +41 -0
  94. package/dist/generators/codex.js.map +1 -0
  95. package/dist/generators/copilot.d.ts +16 -0
  96. package/dist/generators/copilot.d.ts.map +1 -0
  97. package/dist/generators/copilot.js +49 -0
  98. package/dist/generators/copilot.js.map +1 -0
  99. package/dist/generators/cursor.d.ts +17 -0
  100. package/dist/generators/cursor.d.ts.map +1 -0
  101. package/dist/generators/cursor.js +37 -0
  102. package/dist/generators/cursor.js.map +1 -0
  103. package/dist/generators/index.d.ts +18 -0
  104. package/dist/generators/index.d.ts.map +1 -0
  105. package/dist/generators/index.js +95 -0
  106. package/dist/generators/index.js.map +1 -0
  107. package/dist/gui/assets/index-Cq9V485S.js +21 -0
  108. package/dist/gui/assets/index-Dh1b5gSZ.css +2 -0
  109. package/dist/gui/favicon.svg +1 -0
  110. package/dist/gui/icons.svg +24 -0
  111. package/dist/gui/index.html +18 -0
  112. package/dist/index.d.ts +11 -0
  113. package/dist/index.d.ts.map +1 -0
  114. package/dist/index.js +169 -0
  115. package/dist/index.js.map +1 -0
  116. package/dist/orchestration/detect.d.ts +29 -0
  117. package/dist/orchestration/detect.d.ts.map +1 -0
  118. package/dist/orchestration/detect.js +292 -0
  119. package/dist/orchestration/detect.js.map +1 -0
  120. package/dist/orchestration/index.d.ts +7 -0
  121. package/dist/orchestration/index.d.ts.map +1 -0
  122. package/dist/orchestration/index.js +7 -0
  123. package/dist/orchestration/index.js.map +1 -0
  124. package/dist/orchestration/runner.d.ts +40 -0
  125. package/dist/orchestration/runner.d.ts.map +1 -0
  126. package/dist/orchestration/runner.js +242 -0
  127. package/dist/orchestration/runner.js.map +1 -0
  128. package/dist/server.d.ts +16 -0
  129. package/dist/server.d.ts.map +1 -0
  130. package/dist/server.js +419 -0
  131. package/dist/server.js.map +1 -0
  132. package/dist/types.d.ts +210 -0
  133. package/dist/types.d.ts.map +1 -0
  134. package/dist/types.js +6 -0
  135. package/dist/types.js.map +1 -0
  136. package/dist/utils/detect-ai.d.ts +21 -0
  137. package/dist/utils/detect-ai.d.ts.map +1 -0
  138. package/dist/utils/detect-ai.js +76 -0
  139. package/dist/utils/detect-ai.js.map +1 -0
  140. package/dist/utils/detect-editors.d.ts +19 -0
  141. package/dist/utils/detect-editors.d.ts.map +1 -0
  142. package/dist/utils/detect-editors.js +49 -0
  143. package/dist/utils/detect-editors.js.map +1 -0
  144. package/dist/utils/git.d.ts +27 -0
  145. package/dist/utils/git.d.ts.map +1 -0
  146. package/dist/utils/git.js +57 -0
  147. package/dist/utils/git.js.map +1 -0
  148. package/dist/utils/prompts.d.ts +27 -0
  149. package/dist/utils/prompts.d.ts.map +1 -0
  150. package/dist/utils/prompts.js +119 -0
  151. package/dist/utils/prompts.js.map +1 -0
  152. package/dist/utils/session-finder.d.ts +29 -0
  153. package/dist/utils/session-finder.d.ts.map +1 -0
  154. package/dist/utils/session-finder.js +462 -0
  155. package/dist/utils/session-finder.js.map +1 -0
  156. package/gui/README.md +73 -0
  157. package/gui/eslint.config.js +22 -0
  158. package/gui/index.html +17 -0
  159. package/gui/package-lock.json +3054 -0
  160. package/gui/package.json +33 -0
  161. package/gui/public/favicon.svg +1 -0
  162. package/gui/public/icons.svg +24 -0
  163. package/gui/src/App.css +1 -0
  164. package/gui/src/App.tsx +1949 -0
  165. package/gui/src/assets/hero.png +0 -0
  166. package/gui/src/assets/react.svg +1 -0
  167. package/gui/src/assets/vite.svg +1 -0
  168. package/gui/src/index.css +1 -0
  169. package/gui/src/main.tsx +10 -0
  170. package/gui/tsconfig.app.json +25 -0
  171. package/gui/tsconfig.json +7 -0
  172. package/gui/tsconfig.node.json +24 -0
  173. package/gui/vite.config.ts +12 -0
  174. package/package.json +48 -0
  175. package/src/analyzers/detect-apis.ts +138 -0
  176. package/src/analyzers/detect-deps.ts +138 -0
  177. package/src/analyzers/detect-existing.ts +74 -0
  178. package/src/analyzers/detect-ports.ts +110 -0
  179. package/src/analyzers/index.ts +85 -0
  180. package/src/analyzers/readme-summarizer.ts +68 -0
  181. package/src/analyzers/tech-stack.ts +255 -0
  182. package/src/commands/create.ts +142 -0
  183. package/src/commands/init.ts +58 -0
  184. package/src/commands/list.ts +46 -0
  185. package/src/commands/logs.ts +63 -0
  186. package/src/commands/open.ts +54 -0
  187. package/src/commands/start.ts +117 -0
  188. package/src/commands/status.ts +54 -0
  189. package/src/commands/stop.ts +55 -0
  190. package/src/commands/ui.ts +47 -0
  191. package/src/core/config.ts +79 -0
  192. package/src/core/scanner.ts +91 -0
  193. package/src/core/workspace.ts +149 -0
  194. package/src/core/worktree.ts +50 -0
  195. package/src/generators/base.ts +187 -0
  196. package/src/generators/claude.ts +34 -0
  197. package/src/generators/codex.ts +48 -0
  198. package/src/generators/copilot.ts +56 -0
  199. package/src/generators/cursor.ts +44 -0
  200. package/src/generators/index.ts +122 -0
  201. package/src/index.ts +178 -0
  202. package/src/orchestration/detect.ts +313 -0
  203. package/src/orchestration/index.ts +7 -0
  204. package/src/orchestration/runner.ts +279 -0
  205. package/src/server.ts +466 -0
  206. package/src/types.ts +294 -0
  207. package/src/utils/detect-ai.ts +81 -0
  208. package/src/utils/detect-editors.ts +54 -0
  209. package/src/utils/git.ts +61 -0
  210. package/src/utils/prompts.ts +135 -0
  211. package/src/utils/session-finder.ts +483 -0
  212. package/tsconfig.json +19 -0
package/src/server.ts ADDED
@@ -0,0 +1,466 @@
1
+ /**
2
+ * @module server
3
+ * Hono local web server for the NexusFlow GUI.
4
+ */
5
+
6
+ import { Hono } from 'hono';
7
+ import { serve } from '@hono/node-server';
8
+ import { serveStatic } from '@hono/node-server/serve-static';
9
+ import { cors } from 'hono/cors';
10
+ import * as fs from 'node:fs/promises';
11
+ import * as path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { execa } from 'execa';
14
+
15
+ import { loadConfig, saveConfig, getConfigDir } from './core/config.js';
16
+ import { scanForRepos } from './core/scanner.js';
17
+ import { createWorkspace, listWorkspaces, loadFeatureConfig } from './core/workspace.js';
18
+ import { analyzeAllRepos } from './analyzers/index.js';
19
+ import { generateContextFiles } from './generators/index.js';
20
+ import { detectAIAssistants } from './utils/detect-ai.js';
21
+ import { detectEditors } from './utils/detect-editors.js';
22
+ import { findSessions, getSessionTranscript } from './utils/session-finder.js';
23
+ import {
24
+ detectAllServices,
25
+ detectOrchestrationTools,
26
+ startServices,
27
+ stopServices,
28
+ loadRunningState,
29
+ } from './orchestration/index.js';
30
+ import type { Feature, RepoInfo, WorkspaceContext } from './types.js';
31
+
32
+ // Resolve static files directory
33
+ const __filename = fileURLToPath(import.meta.url);
34
+ const __dirname = path.dirname(__filename);
35
+
36
+ // In development, the static GUI is built to dist/gui
37
+ // In production, it will be served from dist/gui as well
38
+ const guiPath = path.join(__dirname, 'gui');
39
+
40
+ export const app = new Hono();
41
+
42
+ // Enable CORS for frontend dev server
43
+ app.use('/api/*', cors());
44
+
45
+ // ─── API Endpoints ────────────────────────────────────────────────────────
46
+
47
+ // 1. Get current configuration
48
+ app.get('/api/config', async (c) => {
49
+ try {
50
+ const configPath = path.join(getConfigDir(), 'config.json');
51
+ let exists = false;
52
+ try {
53
+ await fs.access(configPath);
54
+ exists = true;
55
+ } catch {}
56
+
57
+ const config = await loadConfig();
58
+ return c.json({ config, exists });
59
+ } catch (error) {
60
+ const msg = error instanceof Error ? error.message : String(error);
61
+ return c.json({ error: msg }, 500);
62
+ }
63
+ });
64
+
65
+ // 2. Save configuration
66
+ app.post('/api/config', async (c) => {
67
+ try {
68
+ const newConfig = await c.req.json();
69
+ await saveConfig(newConfig);
70
+ return c.json({ success: true, config: newConfig });
71
+ } catch (error) {
72
+ const msg = error instanceof Error ? error.message : String(error);
73
+ return c.json({ error: msg }, 500);
74
+ }
75
+ });
76
+
77
+ // 3. Scan development directory for repositories
78
+ app.get('/api/repos', async (c) => {
79
+ try {
80
+ const config = await loadConfig();
81
+ const repos = await scanForRepos(config.devDir, config.scanDepth);
82
+ return c.json(repos);
83
+ } catch (error) {
84
+ const msg = error instanceof Error ? error.message : String(error);
85
+ return c.json({ error: msg }, 500);
86
+ }
87
+ });
88
+
89
+ // 4. List existing workspaces
90
+ app.get('/api/workspaces', async (c) => {
91
+ try {
92
+ const config = await loadConfig();
93
+ const workspaces = await listWorkspaces(config.workspacesDir);
94
+ return c.json(workspaces);
95
+ } catch (error) {
96
+ const msg = error instanceof Error ? error.message : String(error);
97
+ return c.json({ error: msg }, 500);
98
+ }
99
+ });
100
+
101
+ // 5. Detect available AI assistants
102
+ app.get('/api/ai-detect', async (c) => {
103
+ try {
104
+ const assistants = await detectAIAssistants();
105
+ return c.json(assistants);
106
+ } catch (error) {
107
+ const msg = error instanceof Error ? error.message : String(error);
108
+ return c.json({ error: msg }, 500);
109
+ }
110
+ });
111
+
112
+ // 6. Detect available editors
113
+ app.get('/api/editor-detect', async (c) => {
114
+ try {
115
+ const editors = await detectEditors();
116
+ return c.json(editors);
117
+ } catch (error) {
118
+ const msg = error instanceof Error ? error.message : String(error);
119
+ return c.json({ error: msg }, 500);
120
+ }
121
+ });
122
+
123
+ // 7. Create workspace
124
+ app.post('/api/workspace', async (c) => {
125
+ try {
126
+ const body = await c.req.json() as {
127
+ branchName: string;
128
+ description: string;
129
+ repos: RepoInfo[];
130
+ assistants: any[];
131
+ resumption?: {
132
+ testCommand?: string;
133
+ mockCommand?: string;
134
+ startCommand?: string;
135
+ };
136
+ };
137
+
138
+ const config = await loadConfig();
139
+ const workspacePath = path.join(config.workspacesDir, body.branchName);
140
+
141
+ const feature: Feature = {
142
+ id: body.branchName,
143
+ branchName: body.branchName,
144
+ description: body.description,
145
+ repos: body.repos.map((r) => r.path),
146
+ assistants: body.assistants,
147
+ workspacePath,
148
+ createdAt: new Date().toISOString(),
149
+ resumption: body.resumption,
150
+ };
151
+
152
+ // Create worktrees
153
+ await createWorkspace(feature, body.repos);
154
+
155
+ // Analyze repos
156
+ const analysis = await analyzeAllRepos(body.repos);
157
+
158
+ // Convert map to plain object for context generators if needed
159
+ const ctx: WorkspaceContext = {
160
+ feature,
161
+ repos: body.repos,
162
+ analysis,
163
+ };
164
+
165
+ // Generate AI context files
166
+ await generateContextFiles(ctx, body.assistants, workspacePath);
167
+
168
+ return c.json({ success: true, workspacePath, feature });
169
+ } catch (error) {
170
+ const msg = error instanceof Error ? error.message : String(error);
171
+ return c.json({ error: msg }, 500);
172
+ }
173
+ });
174
+
175
+ // 8. Open workspace in editor
176
+ app.post('/api/open-editor', async (c) => {
177
+ try {
178
+ const { workspacePath, command } = await c.req.json() as {
179
+ workspacePath: string;
180
+ command: string;
181
+ };
182
+
183
+ // Spawn editor process
184
+ execa(command, [workspacePath], { detached: true, stdio: 'ignore' }).unref();
185
+ return c.json({ success: true });
186
+ } catch (error) {
187
+ const msg = error instanceof Error ? error.message : String(error);
188
+ return c.json({ error: msg }, 500);
189
+ }
190
+ });
191
+
192
+ // 9. Get status of running services in all/specific workspace
193
+ app.get('/api/workspace/:id/services', async (c) => {
194
+ try {
195
+ const id = c.req.param('id');
196
+ const config = await loadConfig();
197
+ const workspacePath = path.join(config.workspacesDir, id);
198
+
199
+ // Detect all services
200
+ const services = await detectAllServices(workspacePath);
201
+ // Detect existing tools
202
+ const tools = await detectOrchestrationTools(workspacePath);
203
+ // Get currently running services from running state
204
+ const runningState = await loadRunningState(workspacePath);
205
+
206
+ return c.json({
207
+ services,
208
+ orchestrationTools: tools,
209
+ runningState: runningState?.services || [],
210
+ });
211
+ } catch (error) {
212
+ const msg = error instanceof Error ? error.message : String(error);
213
+ return c.json({ error: msg }, 500);
214
+ }
215
+ });
216
+
217
+ // 10. Start services in workspace
218
+ app.post('/api/workspace/:id/services/start', async (c) => {
219
+ try {
220
+ const id = c.req.param('id');
221
+ const { services } = await c.req.json() as { services: any[] };
222
+ const config = await loadConfig();
223
+ const workspacePath = path.join(config.workspacesDir, id);
224
+ const logDir = path.join(workspacePath, '.nexusflow-logs');
225
+
226
+ await startServices(services, workspacePath, logDir);
227
+ return c.json({ success: true });
228
+ } catch (error) {
229
+ const msg = error instanceof Error ? error.message : String(error);
230
+ return c.json({ error: msg }, 500);
231
+ }
232
+ });
233
+
234
+ // 11. Stop services in workspace
235
+ app.post('/api/workspace/:id/services/stop', async (c) => {
236
+ try {
237
+ const id = c.req.param('id');
238
+ const config = await loadConfig();
239
+ const workspacePath = path.join(config.workspacesDir, id);
240
+
241
+ await stopServices(workspacePath);
242
+ return c.json({ success: true });
243
+ } catch (error) {
244
+ const msg = error instanceof Error ? error.message : String(error);
245
+ return c.json({ error: msg }, 500);
246
+ }
247
+ });
248
+
249
+ // 12. Get service logs
250
+ app.get('/api/workspace/:id/services/logs/:serviceName', async (c) => {
251
+ try {
252
+ const id = c.req.param('id');
253
+ const serviceName = c.req.param('serviceName');
254
+ const config = await loadConfig();
255
+ const workspacePath = path.join(config.workspacesDir, id);
256
+ const logFile = path.join(workspacePath, '.nexusflow-logs', `${serviceName}.log`);
257
+
258
+ let content = '';
259
+ try {
260
+ // Read last 200 lines or 50KB
261
+ const stats = await fs.stat(logFile);
262
+ const size = stats.size;
263
+ const start = Math.max(0, size - 50000);
264
+ const fd = await fs.open(logFile, 'r');
265
+ const buffer = Buffer.alloc(size - start);
266
+ await fd.read(buffer, 0, buffer.length, start);
267
+ await fd.close();
268
+ content = buffer.toString('utf-8');
269
+ } catch {
270
+ content = 'No logs available yet.';
271
+ }
272
+
273
+ return c.json({ logs: content });
274
+ } catch (error) {
275
+ const msg = error instanceof Error ? error.message : String(error);
276
+ return c.json({ error: msg }, 500);
277
+ }
278
+ });
279
+
280
+ // 13. Get git changes in workspace sub-repositories
281
+ app.get('/api/workspace/:id/changes', async (c) => {
282
+ try {
283
+ const id = c.req.param('id');
284
+ const config = await loadConfig();
285
+ const workspacePath = path.join(config.workspacesDir, id);
286
+
287
+ // Load feature config to get repo paths
288
+ const feature = await loadFeatureConfig(workspacePath);
289
+ if (!feature) {
290
+ return c.json({ error: 'Workspace configuration not found.' }, 404);
291
+ }
292
+
293
+ const results: any[] = [];
294
+
295
+ // Check git status in each repo
296
+ for (const repoPath of feature.repos) {
297
+ const repoName = path.basename(repoPath);
298
+ const worktreePath = path.join(workspacePath, repoName);
299
+
300
+ try {
301
+ const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: worktreePath });
302
+ const lines = stdout.split('\n').map((l) => l.trim()).filter(Boolean);
303
+
304
+ const files = lines.map((line) => {
305
+ const status = line.slice(0, 2).trim();
306
+ const file = line.slice(2).trim();
307
+
308
+ let type = 'modified';
309
+ if (status === 'A' || status === '??') type = 'added';
310
+ else if (status === 'D') type = 'deleted';
311
+
312
+ return { file, type, rawStatus: status };
313
+ });
314
+
315
+ results.push({
316
+ repoName,
317
+ repoPath: worktreePath,
318
+ files,
319
+ });
320
+ } catch (error) {
321
+ results.push({
322
+ repoName,
323
+ repoPath: worktreePath,
324
+ files: [],
325
+ error: error instanceof Error ? error.message : String(error),
326
+ });
327
+ }
328
+ }
329
+
330
+ return c.json({ changes: results });
331
+ } catch (error) {
332
+ const msg = error instanceof Error ? error.message : String(error);
333
+ return c.json({ error: msg }, 500);
334
+ }
335
+ });
336
+
337
+ // 14. Resume session in workspace (copies CLI resume command and opens editor)
338
+ app.post('/api/workspace/:id/resume', async (c) => {
339
+ try {
340
+ const id = decodeURIComponent(c.req.param('id'));
341
+ const config = await loadConfig();
342
+ const workspacePath = path.join(config.workspacesDir, id);
343
+
344
+ // Load feature config to get repo paths
345
+ const feature = await loadFeatureConfig(workspacePath);
346
+ if (!feature) {
347
+ return c.json({ error: 'Workspace configuration not found.' }, 404);
348
+ }
349
+
350
+ const body = await c.req.json().catch(() => ({}));
351
+ const targetSessionId = body.sessionId;
352
+ const targetAssistant = body.assistant;
353
+
354
+ // Find the session to resume
355
+ let resumeCommand = '';
356
+ let selectedAssistant = targetAssistant || feature.assistants[0] || 'antigravity';
357
+ let selectedSessionId = targetSessionId;
358
+
359
+ if (!selectedSessionId) {
360
+ // Find the most recent session matching this workspace
361
+ const sessions = await findSessions(workspacePath, feature.repos);
362
+ if (sessions.length > 0) {
363
+ selectedSessionId = sessions[0].id;
364
+ selectedAssistant = sessions[0].assistant;
365
+ }
366
+ }
367
+
368
+ if (selectedSessionId) {
369
+ if (selectedAssistant === 'antigravity') {
370
+ resumeCommand = `agy --conversation ${selectedSessionId}`;
371
+ } else if (selectedAssistant === 'claude') {
372
+ resumeCommand = `claude --resume ${selectedSessionId}`;
373
+ } else if (selectedAssistant === 'codex') {
374
+ resumeCommand = `codex resume ${selectedSessionId}`;
375
+ } else if (selectedAssistant === 'copilot') {
376
+ resumeCommand = `copilot --resume ${selectedSessionId}`;
377
+ }
378
+ } else {
379
+ // Fallback to continue command
380
+ if (selectedAssistant === 'antigravity') {
381
+ resumeCommand = 'agy --continue';
382
+ } else if (selectedAssistant === 'claude') {
383
+ resumeCommand = 'claude --continue';
384
+ } else if (selectedAssistant === 'codex') {
385
+ resumeCommand = 'codex resume --last';
386
+ } else if (selectedAssistant === 'copilot') {
387
+ resumeCommand = 'copilot --continue';
388
+ } else {
389
+ resumeCommand = 'agy --continue';
390
+ }
391
+ }
392
+
393
+ // Open in editor if command is provided
394
+ if (body.command) {
395
+ try {
396
+ execa(body.command, [workspacePath], { detached: true, stdio: 'ignore' }).unref();
397
+ } catch (e) {
398
+ console.error('Failed to launch editor:', e);
399
+ }
400
+ }
401
+
402
+ return c.json({ success: true, resumeCommand, workspacePath });
403
+ } catch (error) {
404
+ const msg = error instanceof Error ? error.message : String(error);
405
+ return c.json({ error: msg }, 500);
406
+ }
407
+ });
408
+
409
+ // 15. List past AI sessions for a workspace
410
+ app.get('/api/workspace/:id/sessions', async (c) => {
411
+ try {
412
+ const id = decodeURIComponent(c.req.param('id'));
413
+ const config = await loadConfig();
414
+ const workspacePath = path.join(config.workspacesDir, id);
415
+
416
+ const feature = await loadFeatureConfig(workspacePath);
417
+ if (!feature) {
418
+ return c.json({ error: 'Workspace configuration not found.' }, 404);
419
+ }
420
+
421
+ const sessions = await findSessions(workspacePath, feature.repos);
422
+ return c.json({ sessions });
423
+ } catch (error) {
424
+ const msg = error instanceof Error ? error.message : String(error);
425
+ return c.json({ error: msg }, 500);
426
+ }
427
+ });
428
+
429
+ // 16. Fetch transcript for a specific AI session
430
+ app.get('/api/session/:assistant/:sessionId/transcript', async (c) => {
431
+ try {
432
+ const assistant = c.req.param('assistant');
433
+ const sessionId = c.req.param('sessionId');
434
+ const messages = await getSessionTranscript(assistant, sessionId);
435
+ return c.json({ messages });
436
+ } catch (error) {
437
+ const msg = error instanceof Error ? error.message : String(error);
438
+ return c.json({ error: msg }, 500);
439
+ }
440
+ });
441
+
442
+ // Serve index.html explicitly on root endpoint
443
+ app.get('/', async (c) => {
444
+ try {
445
+ const html = await fs.readFile(path.join(guiPath, 'index.html'), 'utf-8');
446
+ return c.html(html);
447
+ } catch (error) {
448
+ return c.text('GUI dashboard built assets not found. Run "npm run build" first.', 404);
449
+ }
450
+ });
451
+
452
+ // Serve static assets from GUI build folder
453
+ app.use('/*', serveStatic({ root: path.relative(process.cwd(), guiPath) }));
454
+
455
+ /**
456
+ * Starts the local GUI web server.
457
+ *
458
+ * @param port - Port to run on.
459
+ */
460
+ export function startServer(port = 3000): Promise<{ port: number; server: any }> {
461
+ return new Promise((resolve) => {
462
+ const server = serve({ fetch: app.fetch, port }, (info) => {
463
+ resolve({ port: info.port, server });
464
+ });
465
+ });
466
+ }