@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.
- package/GETTING_STARTED.md +109 -0
- package/README.md +310 -0
- package/dist/analyzers/detect-apis.d.ts +19 -0
- package/dist/analyzers/detect-apis.d.ts.map +1 -0
- package/dist/analyzers/detect-apis.js +121 -0
- package/dist/analyzers/detect-apis.js.map +1 -0
- package/dist/analyzers/detect-deps.d.ts +30 -0
- package/dist/analyzers/detect-deps.d.ts.map +1 -0
- package/dist/analyzers/detect-deps.js +121 -0
- package/dist/analyzers/detect-deps.js.map +1 -0
- package/dist/analyzers/detect-existing.d.ts +15 -0
- package/dist/analyzers/detect-existing.d.ts.map +1 -0
- package/dist/analyzers/detect-existing.js +65 -0
- package/dist/analyzers/detect-existing.js.map +1 -0
- package/dist/analyzers/detect-ports.d.ts +20 -0
- package/dist/analyzers/detect-ports.d.ts.map +1 -0
- package/dist/analyzers/detect-ports.js +103 -0
- package/dist/analyzers/detect-ports.js.map +1 -0
- package/dist/analyzers/index.d.ts +29 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +74 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/analyzers/readme-summarizer.d.ts +13 -0
- package/dist/analyzers/readme-summarizer.d.ts.map +1 -0
- package/dist/analyzers/readme-summarizer.js +59 -0
- package/dist/analyzers/readme-summarizer.js.map +1 -0
- package/dist/analyzers/tech-stack.d.ts +15 -0
- package/dist/analyzers/tech-stack.d.ts.map +1 -0
- package/dist/analyzers/tech-stack.js +284 -0
- package/dist/analyzers/tech-stack.js.map +1 -0
- package/dist/commands/create.d.ts +17 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +111 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +50 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +32 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/logs.d.ts +12 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +54 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/open.d.ts +9 -0
- package/dist/commands/open.d.ts.map +1 -0
- package/dist/commands/open.js +49 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/start.d.ts +13 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +97 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +11 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +47 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +11 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +47 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/ui.d.ts +13 -0
- package/dist/commands/ui.d.ts.map +1 -0
- package/dist/commands/ui.js +44 -0
- package/dist/commands/ui.js.map +1 -0
- package/dist/core/config.d.ts +32 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +68 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/scanner.d.ts +18 -0
- package/dist/core/scanner.d.ts.map +1 -0
- package/dist/core/scanner.js +73 -0
- package/dist/core/scanner.js.map +1 -0
- package/dist/core/workspace.d.ts +53 -0
- package/dist/core/workspace.d.ts.map +1 -0
- package/dist/core/workspace.js +114 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/core/worktree.d.ts +27 -0
- package/dist/core/worktree.d.ts.map +1 -0
- package/dist/core/worktree.js +35 -0
- package/dist/core/worktree.js.map +1 -0
- package/dist/generators/base.d.ts +15 -0
- package/dist/generators/base.d.ts.map +1 -0
- package/dist/generators/base.js +175 -0
- package/dist/generators/base.js.map +1 -0
- package/dist/generators/claude.d.ts +17 -0
- package/dist/generators/claude.d.ts.map +1 -0
- package/dist/generators/claude.js +29 -0
- package/dist/generators/claude.js.map +1 -0
- package/dist/generators/codex.d.ts +16 -0
- package/dist/generators/codex.d.ts.map +1 -0
- package/dist/generators/codex.js +41 -0
- package/dist/generators/codex.js.map +1 -0
- package/dist/generators/copilot.d.ts +16 -0
- package/dist/generators/copilot.d.ts.map +1 -0
- package/dist/generators/copilot.js +49 -0
- package/dist/generators/copilot.js.map +1 -0
- package/dist/generators/cursor.d.ts +17 -0
- package/dist/generators/cursor.d.ts.map +1 -0
- package/dist/generators/cursor.js +37 -0
- package/dist/generators/cursor.js.map +1 -0
- package/dist/generators/index.d.ts +18 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +95 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/gui/assets/index-Cq9V485S.js +21 -0
- package/dist/gui/assets/index-Dh1b5gSZ.css +2 -0
- package/dist/gui/favicon.svg +1 -0
- package/dist/gui/icons.svg +24 -0
- package/dist/gui/index.html +18 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +169 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestration/detect.d.ts +29 -0
- package/dist/orchestration/detect.d.ts.map +1 -0
- package/dist/orchestration/detect.js +292 -0
- package/dist/orchestration/detect.js.map +1 -0
- package/dist/orchestration/index.d.ts +7 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +7 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/runner.d.ts +40 -0
- package/dist/orchestration/runner.d.ts.map +1 -0
- package/dist/orchestration/runner.js +242 -0
- package/dist/orchestration/runner.js.map +1 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +419 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +210 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/detect-ai.d.ts +21 -0
- package/dist/utils/detect-ai.d.ts.map +1 -0
- package/dist/utils/detect-ai.js +76 -0
- package/dist/utils/detect-ai.js.map +1 -0
- package/dist/utils/detect-editors.d.ts +19 -0
- package/dist/utils/detect-editors.d.ts.map +1 -0
- package/dist/utils/detect-editors.js +49 -0
- package/dist/utils/detect-editors.js.map +1 -0
- package/dist/utils/git.d.ts +27 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +57 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/prompts.d.ts +27 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +119 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/session-finder.d.ts +29 -0
- package/dist/utils/session-finder.d.ts.map +1 -0
- package/dist/utils/session-finder.js +462 -0
- package/dist/utils/session-finder.js.map +1 -0
- package/gui/README.md +73 -0
- package/gui/eslint.config.js +22 -0
- package/gui/index.html +17 -0
- package/gui/package-lock.json +3054 -0
- package/gui/package.json +33 -0
- package/gui/public/favicon.svg +1 -0
- package/gui/public/icons.svg +24 -0
- package/gui/src/App.css +1 -0
- package/gui/src/App.tsx +1949 -0
- package/gui/src/assets/hero.png +0 -0
- package/gui/src/assets/react.svg +1 -0
- package/gui/src/assets/vite.svg +1 -0
- package/gui/src/index.css +1 -0
- package/gui/src/main.tsx +10 -0
- package/gui/tsconfig.app.json +25 -0
- package/gui/tsconfig.json +7 -0
- package/gui/tsconfig.node.json +24 -0
- package/gui/vite.config.ts +12 -0
- package/package.json +48 -0
- package/src/analyzers/detect-apis.ts +138 -0
- package/src/analyzers/detect-deps.ts +138 -0
- package/src/analyzers/detect-existing.ts +74 -0
- package/src/analyzers/detect-ports.ts +110 -0
- package/src/analyzers/index.ts +85 -0
- package/src/analyzers/readme-summarizer.ts +68 -0
- package/src/analyzers/tech-stack.ts +255 -0
- package/src/commands/create.ts +142 -0
- package/src/commands/init.ts +58 -0
- package/src/commands/list.ts +46 -0
- package/src/commands/logs.ts +63 -0
- package/src/commands/open.ts +54 -0
- package/src/commands/start.ts +117 -0
- package/src/commands/status.ts +54 -0
- package/src/commands/stop.ts +55 -0
- package/src/commands/ui.ts +47 -0
- package/src/core/config.ts +79 -0
- package/src/core/scanner.ts +91 -0
- package/src/core/workspace.ts +149 -0
- package/src/core/worktree.ts +50 -0
- package/src/generators/base.ts +187 -0
- package/src/generators/claude.ts +34 -0
- package/src/generators/codex.ts +48 -0
- package/src/generators/copilot.ts +56 -0
- package/src/generators/cursor.ts +44 -0
- package/src/generators/index.ts +122 -0
- package/src/index.ts +178 -0
- package/src/orchestration/detect.ts +313 -0
- package/src/orchestration/index.ts +7 -0
- package/src/orchestration/runner.ts +279 -0
- package/src/server.ts +466 -0
- package/src/types.ts +294 -0
- package/src/utils/detect-ai.ts +81 -0
- package/src/utils/detect-editors.ts +54 -0
- package/src/utils/git.ts +61 -0
- package/src/utils/prompts.ts +135 -0
- package/src/utils/session-finder.ts +483 -0
- 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
|
+
}
|