@assistkick/create 1.2.0 → 1.3.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/package.json +2 -1
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +231 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +49 -2
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +82 -0
- package/templates/assistkick-product-system/packages/backend/src/server.ts +19 -6
- package/templates/assistkick-product-system/packages/backend/src/services/github_app_service.ts +146 -0
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +69 -2
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +71 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +87 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +194 -0
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -17
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +114 -39
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +28 -14
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +151 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +352 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +208 -95
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +17 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +238 -105
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +15 -13
- package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +1 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +4 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/dashboard.tsx +22 -4
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +486 -38
- package/templates/assistkick-product-system/packages/shared/db/migrations/0001_vengeful_wallop.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0002_greedy_excalibur.sql +4 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0001_snapshot.json +826 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0002_snapshot.json +854 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +14 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +5 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +54 -1
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +25 -0
- package/templates/assistkick-product-system/packages/shared/lib/pipeline-state-store.ts +4 -0
- package/templates/assistkick-product-system/packages/shared/lib/pipeline.ts +329 -89
- package/templates/assistkick-product-system/packages/shared/lib/pipeline_orchestrator.ts +186 -0
- package/templates/assistkick-product-system/packages/shared/tools/db_explorer.ts +275 -0
- package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/move_card.ts +3 -2
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
- package/templates/assistkick-product-system/tests/kanban.test.ts +1 -1
- package/templates/assistkick-product-system/tests/pipeline_stats_all_cards.test.ts +1 -1
- package/templates/assistkick-product-system/tests/web_terminal.test.ts +189 -150
- package/templates/skills/assistkick-bootstrap/SKILL.md +33 -25
- package/templates/skills/assistkick-code-reviewer/SKILL.md +23 -15
- package/templates/skills/assistkick-db-explorer/SKILL.md +86 -0
- package/templates/skills/assistkick-debugger/SKILL.md +30 -22
- package/templates/skills/assistkick-developer/SKILL.md +37 -29
- package/templates/skills/assistkick-interview/SKILL.md +34 -26
package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PTY Session Manager — manages
|
|
2
|
+
* PTY Session Manager — manages named, project-scoped terminal sessions.
|
|
3
3
|
* Implements dec_029 (Command Prefix Whitelist) and dec_030 (Server-Side PTY Session Persistence).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* validates complete commands against the whitelist, then spawns only the
|
|
8
|
-
* validated command as a PTY process. While a command is running, raw input
|
|
9
|
-
* is forwarded to it. When the command exits, the session returns to idle.
|
|
5
|
+
* Each session has a unique ID, a human-readable name (project name + creation timestamp),
|
|
6
|
+
* and is permanently bound to a project. Sessions persist across WebSocket disconnects.
|
|
10
7
|
*/
|
|
11
8
|
|
|
12
9
|
import type { IPty } from 'node-pty';
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
13
11
|
|
|
14
12
|
const OUTPUT_BUFFER_MAX = 50_000;
|
|
15
13
|
|
|
@@ -27,7 +25,10 @@ interface PtySessionManagerDeps {
|
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
interface PtySession {
|
|
30
|
-
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
projectId: string;
|
|
31
|
+
projectName: string;
|
|
31
32
|
pty: IPty | null;
|
|
32
33
|
state: 'idle' | 'running';
|
|
33
34
|
cols: number;
|
|
@@ -38,6 +39,15 @@ interface PtySession {
|
|
|
38
39
|
createdAt: Date;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
export interface SessionInfo {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
projectId: string;
|
|
46
|
+
projectName: string;
|
|
47
|
+
state: 'idle' | 'running';
|
|
48
|
+
createdAt: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
export class PtySessionManager {
|
|
42
52
|
private readonly sessions = new Map<string, PtySession>();
|
|
43
53
|
private readonly spawn: PtySessionManagerDeps['spawn'];
|
|
@@ -50,6 +60,20 @@ export class PtySessionManager {
|
|
|
50
60
|
this.projectRoot = projectRoot;
|
|
51
61
|
}
|
|
52
62
|
|
|
63
|
+
generateId = (): string => {
|
|
64
|
+
const hex = randomBytes(3).toString('hex');
|
|
65
|
+
return `term_${hex}`;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
formatSessionName = (projectName: string, date: Date): string => {
|
|
69
|
+
const dd = String(date.getDate()).padStart(2, '0');
|
|
70
|
+
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
71
|
+
const yy = String(date.getFullYear()).slice(2);
|
|
72
|
+
const hh = String(date.getHours()).padStart(2, '0');
|
|
73
|
+
const min = String(date.getMinutes()).padStart(2, '0');
|
|
74
|
+
return `${projectName} - ${dd}/${mm}/${yy} - ${hh}:${min}`;
|
|
75
|
+
};
|
|
76
|
+
|
|
53
77
|
validateCommand = (input: string): { valid: boolean; error?: string } => {
|
|
54
78
|
const trimmed = input.trim();
|
|
55
79
|
if (!trimmed) {
|
|
@@ -69,19 +93,31 @@ export class PtySessionManager {
|
|
|
69
93
|
return { valid: true };
|
|
70
94
|
};
|
|
71
95
|
|
|
72
|
-
|
|
73
|
-
return this.sessions.
|
|
96
|
+
listSessions = (): SessionInfo[] => {
|
|
97
|
+
return Array.from(this.sessions.values()).map(s => ({
|
|
98
|
+
id: s.id,
|
|
99
|
+
name: s.name,
|
|
100
|
+
projectId: s.projectId,
|
|
101
|
+
projectName: s.projectName,
|
|
102
|
+
state: s.state,
|
|
103
|
+
createdAt: s.createdAt.toISOString(),
|
|
104
|
+
}));
|
|
74
105
|
};
|
|
75
106
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
107
|
+
getSession = (sessionId: string): PtySession | undefined => {
|
|
108
|
+
return this.sessions.get(sessionId);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
createSession = (projectId: string, projectName: string, cols: number, rows: number): PtySession => {
|
|
112
|
+
const id = this.generateId();
|
|
113
|
+
const createdAt = new Date();
|
|
114
|
+
const name = this.formatSessionName(projectName, createdAt);
|
|
82
115
|
|
|
83
116
|
const session: PtySession = {
|
|
84
|
-
|
|
117
|
+
id,
|
|
118
|
+
name,
|
|
119
|
+
projectId,
|
|
120
|
+
projectName,
|
|
85
121
|
pty: null,
|
|
86
122
|
state: 'idle',
|
|
87
123
|
cols,
|
|
@@ -89,35 +125,35 @@ export class PtySessionManager {
|
|
|
89
125
|
outputBuffer: '',
|
|
90
126
|
inputBuffer: '',
|
|
91
127
|
listeners: new Set(),
|
|
92
|
-
createdAt
|
|
128
|
+
createdAt,
|
|
93
129
|
};
|
|
94
130
|
|
|
95
|
-
this.sessions.set(
|
|
96
|
-
this.log('PTY', `Created
|
|
131
|
+
this.sessions.set(id, session);
|
|
132
|
+
this.log('PTY', `Created session "${name}" (${id}) for project ${projectId}`);
|
|
97
133
|
|
|
98
|
-
// Show welcome message
|
|
134
|
+
// Show welcome message then auto-launch claude with project context
|
|
99
135
|
this.emitOutput(session, WELCOME_MSG);
|
|
100
|
-
this.
|
|
136
|
+
this.autoLaunchClaude(session);
|
|
101
137
|
|
|
102
138
|
return session;
|
|
103
139
|
};
|
|
104
140
|
|
|
105
|
-
addListener = (
|
|
106
|
-
const session = this.sessions.get(
|
|
141
|
+
addListener = (sessionId: string, listener: (data: string) => void): void => {
|
|
142
|
+
const session = this.sessions.get(sessionId);
|
|
107
143
|
if (session) {
|
|
108
144
|
session.listeners.add(listener);
|
|
109
145
|
}
|
|
110
146
|
};
|
|
111
147
|
|
|
112
|
-
removeListener = (
|
|
113
|
-
const session = this.sessions.get(
|
|
148
|
+
removeListener = (sessionId: string, listener: (data: string) => void): void => {
|
|
149
|
+
const session = this.sessions.get(sessionId);
|
|
114
150
|
if (session) {
|
|
115
151
|
session.listeners.delete(listener);
|
|
116
152
|
}
|
|
117
153
|
};
|
|
118
154
|
|
|
119
|
-
writeToSession = (
|
|
120
|
-
const session = this.sessions.get(
|
|
155
|
+
writeToSession = (sessionId: string, data: string): void => {
|
|
156
|
+
const session = this.sessions.get(sessionId);
|
|
121
157
|
if (!session) return;
|
|
122
158
|
|
|
123
159
|
if (session.state === 'running' && session.pty) {
|
|
@@ -129,8 +165,8 @@ export class PtySessionManager {
|
|
|
129
165
|
}
|
|
130
166
|
};
|
|
131
167
|
|
|
132
|
-
resizeSession = (
|
|
133
|
-
const session = this.sessions.get(
|
|
168
|
+
resizeSession = (sessionId: string, cols: number, rows: number): void => {
|
|
169
|
+
const session = this.sessions.get(sessionId);
|
|
134
170
|
if (session) {
|
|
135
171
|
session.cols = cols;
|
|
136
172
|
session.rows = rows;
|
|
@@ -140,25 +176,25 @@ export class PtySessionManager {
|
|
|
140
176
|
}
|
|
141
177
|
};
|
|
142
178
|
|
|
143
|
-
destroySession = (
|
|
144
|
-
const session = this.sessions.get(
|
|
179
|
+
destroySession = (sessionId: string): void => {
|
|
180
|
+
const session = this.sessions.get(sessionId);
|
|
145
181
|
if (session) {
|
|
146
182
|
if (session.pty) {
|
|
147
183
|
session.pty.kill();
|
|
148
184
|
}
|
|
149
|
-
this.sessions.delete(
|
|
150
|
-
this.log('PTY', `Destroyed session
|
|
185
|
+
this.sessions.delete(sessionId);
|
|
186
|
+
this.log('PTY', `Destroyed session "${session.name}" (${sessionId})`);
|
|
151
187
|
}
|
|
152
188
|
};
|
|
153
189
|
|
|
154
190
|
destroyAll = (): void => {
|
|
155
|
-
for (const [
|
|
156
|
-
this.destroySession(
|
|
191
|
+
for (const [sessionId] of this.sessions) {
|
|
192
|
+
this.destroySession(sessionId);
|
|
157
193
|
}
|
|
158
194
|
};
|
|
159
195
|
|
|
160
|
-
getBufferedOutput = (
|
|
161
|
-
const session = this.sessions.get(
|
|
196
|
+
getBufferedOutput = (sessionId: string): string => {
|
|
197
|
+
const session = this.sessions.get(sessionId);
|
|
162
198
|
return session?.outputBuffer ?? '';
|
|
163
199
|
};
|
|
164
200
|
|
|
@@ -202,6 +238,45 @@ export class PtySessionManager {
|
|
|
202
238
|
}
|
|
203
239
|
};
|
|
204
240
|
|
|
241
|
+
private autoLaunchClaude = (session: PtySession): void => {
|
|
242
|
+
const projectContext = `We are working on project-id ${session.projectId}`;
|
|
243
|
+
const args = ['--dangerously-skip-permissions', '--append-system-prompt', projectContext];
|
|
244
|
+
|
|
245
|
+
this.log('PTY', `Auto-launching claude for session ${session.id} (project ${session.projectId})`);
|
|
246
|
+
|
|
247
|
+
let spawnedPty: IPty;
|
|
248
|
+
try {
|
|
249
|
+
spawnedPty = this.spawn('claude', args, {
|
|
250
|
+
name: 'xterm-256color',
|
|
251
|
+
cols: session.cols,
|
|
252
|
+
rows: session.rows,
|
|
253
|
+
cwd: this.projectRoot,
|
|
254
|
+
env: this.buildEnv(),
|
|
255
|
+
});
|
|
256
|
+
} catch (err) {
|
|
257
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
258
|
+
this.emitOutput(session, `\x1b[31mFailed to auto-launch claude: ${msg}\x1b[0m\r\n`);
|
|
259
|
+
this.log('PTY', `Auto-launch failed for session ${session.id}: ${msg}`);
|
|
260
|
+
this.emitOutput(session, PROMPT);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
session.pty = spawnedPty;
|
|
265
|
+
session.state = 'running';
|
|
266
|
+
|
|
267
|
+
spawnedPty.onData((data: string) => {
|
|
268
|
+
this.emitOutput(session, data);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
spawnedPty.onExit(({ exitCode }) => {
|
|
272
|
+
this.log('PTY', `Auto-launched claude exited with code ${exitCode} for session ${session.id}`);
|
|
273
|
+
session.pty = null;
|
|
274
|
+
session.state = 'idle';
|
|
275
|
+
this.emitOutput(session, '\r\n');
|
|
276
|
+
this.emitOutput(session, PROMPT);
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
|
|
205
280
|
private buildEnv = (): Record<string, string> => {
|
|
206
281
|
const env = { ...process.env } as Record<string, string>;
|
|
207
282
|
const home = env.HOME || '/root';
|
|
@@ -228,7 +303,7 @@ export class PtySessionManager {
|
|
|
228
303
|
const cmd = parts[0];
|
|
229
304
|
const args = parts.slice(1);
|
|
230
305
|
|
|
231
|
-
this.log('PTY', `Executing validated command "${command}" for
|
|
306
|
+
this.log('PTY', `Executing validated command "${command}" for session ${session.id}`);
|
|
232
307
|
|
|
233
308
|
let spawnedPty: IPty;
|
|
234
309
|
try {
|
|
@@ -259,7 +334,7 @@ export class PtySessionManager {
|
|
|
259
334
|
});
|
|
260
335
|
|
|
261
336
|
spawnedPty.onExit(({ exitCode }) => {
|
|
262
|
-
this.log('PTY', `Command exited with code ${exitCode} for
|
|
337
|
+
this.log('PTY', `Command exited with code ${exitCode} for session ${session.id}`);
|
|
263
338
|
session.pty = null;
|
|
264
339
|
session.state = 'idle';
|
|
265
340
|
this.emitOutput(session, '\r\n');
|
package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WebSocket handler for terminal connections.
|
|
3
3
|
* Authenticates via cookie, enforces admin-only access,
|
|
4
|
-
* and bridges WebSocket ↔ PTY session.
|
|
4
|
+
* and bridges WebSocket ↔ PTY session by sessionId.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { IncomingMessage } from 'node:http';
|
|
@@ -50,7 +50,8 @@ export class TerminalWsHandler {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
handleUpgrade = (req: IncomingMessage, socket: Duplex, head: Buffer): void => {
|
|
53
|
-
|
|
53
|
+
const url = new URL(req.url || '', 'http://localhost');
|
|
54
|
+
if (url.pathname !== '/api/terminal') {
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -84,14 +85,27 @@ export class TerminalWsHandler {
|
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
// Parse sessionId from query params
|
|
89
|
+
const url = new URL(req.url || '', 'http://localhost');
|
|
90
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
if (!sessionId) {
|
|
93
|
+
this.log('TERMINAL', 'Connection rejected — no sessionId provided');
|
|
94
|
+
ws.close(4004, 'Session ID required');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const session = this.ptyManager.getSession(sessionId);
|
|
99
|
+
if (!session) {
|
|
100
|
+
this.log('TERMINAL', `Connection rejected — session ${sessionId} not found`);
|
|
101
|
+
ws.close(4004, 'Session not found');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.log('TERMINAL', `Admin ${payload.email} connected to session ${session.name}`);
|
|
92
106
|
|
|
93
|
-
// Send buffered output from previous
|
|
94
|
-
const buffered = this.ptyManager.getBufferedOutput(
|
|
107
|
+
// Send buffered output from previous connection
|
|
108
|
+
const buffered = this.ptyManager.getBufferedOutput(sessionId);
|
|
95
109
|
if (buffered) {
|
|
96
110
|
ws.send(JSON.stringify({ type: 'output', data: buffered }));
|
|
97
111
|
}
|
|
@@ -102,7 +116,7 @@ export class TerminalWsHandler {
|
|
|
102
116
|
ws.send(JSON.stringify({ type: 'output', data }));
|
|
103
117
|
}
|
|
104
118
|
};
|
|
105
|
-
this.ptyManager.addListener(
|
|
119
|
+
this.ptyManager.addListener(sessionId, outputListener);
|
|
106
120
|
|
|
107
121
|
ws.on('message', (raw: Buffer | string) => {
|
|
108
122
|
let msg: TerminalMessage;
|
|
@@ -113,21 +127,21 @@ export class TerminalWsHandler {
|
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
if (msg.type === 'input' && typeof msg.data === 'string') {
|
|
116
|
-
this.ptyManager.writeToSession(
|
|
130
|
+
this.ptyManager.writeToSession(sessionId, msg.data);
|
|
117
131
|
} else if (msg.type === 'resize' && typeof msg.cols === 'number' && typeof msg.rows === 'number') {
|
|
118
|
-
this.ptyManager.resizeSession(
|
|
132
|
+
this.ptyManager.resizeSession(sessionId, msg.cols, msg.rows);
|
|
119
133
|
}
|
|
120
134
|
});
|
|
121
135
|
|
|
122
136
|
ws.on('close', () => {
|
|
123
|
-
this.log('TERMINAL', `Admin ${payload.email} disconnected from
|
|
124
|
-
this.ptyManager.removeListener(
|
|
137
|
+
this.log('TERMINAL', `Admin ${payload.email} disconnected from session ${session.name}`);
|
|
138
|
+
this.ptyManager.removeListener(sessionId, outputListener);
|
|
125
139
|
// PTY session persists (dec_030) — not destroyed on disconnect
|
|
126
140
|
});
|
|
127
141
|
|
|
128
142
|
ws.on('error', (err) => {
|
|
129
143
|
this.log('TERMINAL', `WebSocket error: ${err.message}`);
|
|
130
|
-
this.ptyManager.removeListener(
|
|
144
|
+
this.ptyManager.removeListener(sessionId, outputListener);
|
|
131
145
|
});
|
|
132
146
|
};
|
|
133
147
|
}
|
|
@@ -17,7 +17,7 @@ export function App() {
|
|
|
17
17
|
<Route path="/reset-password" element={<ResetPasswordRoute />} />
|
|
18
18
|
<Route path="/accept-invitation" element={<AcceptInvitationRoute />} />
|
|
19
19
|
<Route element={<ProtectedRoute />}>
|
|
20
|
-
<Route path="/" element={<Navigate to="/
|
|
20
|
+
<Route path="/" element={<Navigate to="/kanban" replace />} />
|
|
21
21
|
<Route path="/graph" element={<DashboardRoute />} />
|
|
22
22
|
<Route path="/kanban" element={<DashboardRoute />} />
|
|
23
23
|
<Route path="/coherence" element={<DashboardRoute />} />
|
|
@@ -190,6 +190,54 @@ export class ApiClient {
|
|
|
190
190
|
return resp.json();
|
|
191
191
|
};
|
|
192
192
|
|
|
193
|
+
// --- Orchestrator (Play All) ---
|
|
194
|
+
|
|
195
|
+
startPlayAll = async (projectId?: string) => {
|
|
196
|
+
const qs = projectId ? `?project_id=${encodeURIComponent(projectId)}` : '';
|
|
197
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/pipeline/play-all${qs}`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: { 'Content-Type': 'application/json' },
|
|
200
|
+
});
|
|
201
|
+
if (!resp.ok) {
|
|
202
|
+
let msg = `Failed to start Play All: ${resp.status}`;
|
|
203
|
+
try { const err = await resp.json(); msg = err.error || msg; } catch { /* non-JSON */ }
|
|
204
|
+
throw new Error(msg);
|
|
205
|
+
}
|
|
206
|
+
return resp.json();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
stopPlayAll = async () => {
|
|
210
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/pipeline/stop-all`, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: { 'Content-Type': 'application/json' },
|
|
213
|
+
});
|
|
214
|
+
if (!resp.ok) {
|
|
215
|
+
let msg = `Failed to stop Play All: ${resp.status}`;
|
|
216
|
+
try { const err = await resp.json(); msg = err.error || msg; } catch { /* non-JSON */ }
|
|
217
|
+
throw new Error(msg);
|
|
218
|
+
}
|
|
219
|
+
return resp.json();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
getOrchestratorStatus = async () => {
|
|
223
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/pipeline/orchestrator-status`);
|
|
224
|
+
if (!resp.ok) throw new Error(`Failed to get orchestrator status: ${resp.status}`);
|
|
225
|
+
return resp.json();
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
resumePipeline = async (featureId: string) => {
|
|
229
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/kanban/${featureId}/resume`, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: { 'Content-Type': 'application/json' },
|
|
232
|
+
});
|
|
233
|
+
if (!resp.ok) {
|
|
234
|
+
let msg = `Failed to resume pipeline: ${resp.status}`;
|
|
235
|
+
try { const err = await resp.json(); msg = err.error || msg; } catch { /* non-JSON response */ }
|
|
236
|
+
throw new Error(msg);
|
|
237
|
+
}
|
|
238
|
+
return resp.json();
|
|
239
|
+
};
|
|
240
|
+
|
|
193
241
|
unblockCard = async (featureId: string) => {
|
|
194
242
|
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/kanban/${featureId}/unblock`, {
|
|
195
243
|
method: 'POST',
|
|
@@ -368,6 +416,109 @@ export class ApiClient {
|
|
|
368
416
|
return resp.json();
|
|
369
417
|
};
|
|
370
418
|
|
|
419
|
+
// --- Git repository management ---
|
|
420
|
+
|
|
421
|
+
getGitStatus = async (projectId: string) => {
|
|
422
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/projects/${projectId}/git/status`);
|
|
423
|
+
if (!resp.ok) throw new Error(`Failed to get git status: ${resp.status}`);
|
|
424
|
+
return resp.json();
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
connectGitRepo = async (projectId: string, opts: {
|
|
428
|
+
repoUrl?: string;
|
|
429
|
+
githubInstallationId?: string;
|
|
430
|
+
githubRepoFullName?: string;
|
|
431
|
+
baseBranch?: string;
|
|
432
|
+
}) => {
|
|
433
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/projects/${projectId}/git/connect`, {
|
|
434
|
+
method: 'POST',
|
|
435
|
+
headers: { 'Content-Type': 'application/json' },
|
|
436
|
+
body: JSON.stringify(opts),
|
|
437
|
+
});
|
|
438
|
+
if (!resp.ok) {
|
|
439
|
+
const data = await resp.json();
|
|
440
|
+
throw new Error(data.error || `Failed to connect repo: ${resp.status}`);
|
|
441
|
+
}
|
|
442
|
+
return resp.json();
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
initGitRepo = async (projectId: string) => {
|
|
446
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/projects/${projectId}/git/init`, {
|
|
447
|
+
method: 'POST',
|
|
448
|
+
headers: { 'Content-Type': 'application/json' },
|
|
449
|
+
});
|
|
450
|
+
if (!resp.ok) {
|
|
451
|
+
const data = await resp.json();
|
|
452
|
+
throw new Error(data.error || `Failed to init repo: ${resp.status}`);
|
|
453
|
+
}
|
|
454
|
+
return resp.json();
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
disconnectGitRepo = async (projectId: string) => {
|
|
458
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/projects/${projectId}/git/disconnect`, {
|
|
459
|
+
method: 'POST',
|
|
460
|
+
headers: { 'Content-Type': 'application/json' },
|
|
461
|
+
});
|
|
462
|
+
if (!resp.ok) {
|
|
463
|
+
const data = await resp.json();
|
|
464
|
+
throw new Error(data.error || `Failed to disconnect repo: ${resp.status}`);
|
|
465
|
+
}
|
|
466
|
+
return resp.json();
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
testGitHubApp = async (projectId: string) => {
|
|
470
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/projects/${projectId}/git/test`, {
|
|
471
|
+
method: 'POST',
|
|
472
|
+
headers: { 'Content-Type': 'application/json' },
|
|
473
|
+
});
|
|
474
|
+
if (!resp.ok) throw new Error(`Failed to test GitHub App: ${resp.status}`);
|
|
475
|
+
return resp.json();
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
listGitHubInstallations = async (projectId: string) => {
|
|
479
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/projects/${projectId}/git/installations`);
|
|
480
|
+
if (!resp.ok) throw new Error(`Failed to list installations: ${resp.status}`);
|
|
481
|
+
return resp.json();
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
listGitHubRepos = async (projectId: string, installationId: string) => {
|
|
485
|
+
const resp = await this.fetchWithRefresh(
|
|
486
|
+
`${this.baseUrl}/api/projects/${projectId}/git/repos?installation_id=${encodeURIComponent(installationId)}`,
|
|
487
|
+
);
|
|
488
|
+
if (!resp.ok) throw new Error(`Failed to list repos: ${resp.status}`);
|
|
489
|
+
return resp.json();
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
listTerminalSessions = async () => {
|
|
493
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/terminal/sessions`);
|
|
494
|
+
if (!resp.ok) throw new Error(`Failed to list terminal sessions: ${resp.status}`);
|
|
495
|
+
return resp.json();
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
createTerminalSession = async (projectId: string, projectName: string) => {
|
|
499
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/terminal/sessions`, {
|
|
500
|
+
method: 'POST',
|
|
501
|
+
headers: { 'Content-Type': 'application/json' },
|
|
502
|
+
body: JSON.stringify({ projectId, projectName }),
|
|
503
|
+
});
|
|
504
|
+
if (!resp.ok) {
|
|
505
|
+
const data = await resp.json();
|
|
506
|
+
throw new Error(data.error || `Failed to create terminal session: ${resp.status}`);
|
|
507
|
+
}
|
|
508
|
+
return resp.json();
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
killTerminalSession = async (sessionId: string) => {
|
|
512
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/terminal/sessions/${sessionId}`, {
|
|
513
|
+
method: 'DELETE',
|
|
514
|
+
});
|
|
515
|
+
if (!resp.ok) {
|
|
516
|
+
const data = await resp.json();
|
|
517
|
+
throw new Error(data.error || `Failed to kill terminal session: ${resp.status}`);
|
|
518
|
+
}
|
|
519
|
+
return resp.json();
|
|
520
|
+
};
|
|
521
|
+
|
|
371
522
|
acceptInvitation = async (token: string, email: string, password: string) => {
|
|
372
523
|
const resp = await fetch(`${this.baseUrl}/api/auth/accept-invitation`, {
|
|
373
524
|
method: 'POST',
|