@bamptee/aia-code 2.0.12 → 2.0.14
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/README.md +408 -34
- package/package.json +11 -2
- package/src/constants.js +24 -0
- package/src/providers/anthropic.js +6 -21
- package/src/providers/cli-runner.js +131 -7
- package/src/providers/gemini.js +4 -2
- package/src/providers/openai.js +3 -2
- package/src/services/agent-sessions.js +110 -0
- package/src/services/apps.js +132 -0
- package/src/services/config.js +41 -0
- package/src/services/feature.js +28 -7
- package/src/services/model-call.js +2 -2
- package/src/services/runner.js +23 -1
- package/src/services/status.js +69 -1
- package/src/services/test-quick.js +229 -0
- package/src/services/worktrunk.js +135 -21
- package/src/types/test-quick.js +88 -0
- package/src/ui/api/config.js +28 -1
- package/src/ui/api/features.js +160 -50
- package/src/ui/api/index.js +2 -0
- package/src/ui/api/test-quick.js +207 -0
- package/src/ui/api/worktrunk.js +63 -25
- package/src/ui/public/components/config-view.js +95 -0
- package/src/ui/public/components/dashboard.js +823 -163
- package/src/ui/public/components/feature-detail.js +517 -124
- package/src/ui/public/components/test-quick.js +276 -0
- package/src/ui/public/components/worktrunk-panel.js +187 -25
- package/src/ui/public/index.html +13 -0
- package/src/ui/public/main.js +5 -1
- package/src/ui/server.js +97 -67
package/src/ui/api/features.js
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import Busboy from 'busboy';
|
|
4
|
-
import { AIA_DIR } from '../../constants.js';
|
|
5
|
-
import { loadStatus, updateStepStatus, resetStep, updateFlowType } from '../../services/status.js';
|
|
4
|
+
import { AIA_DIR, DELETION_FILTER, QUICK_STEPS } from '../../constants.js';
|
|
5
|
+
import { loadStatus, updateStepStatus, resetStep, updateFlowType, updateType, updateApps, softDeleteFeature, restoreFeature, isFeatureDeleted } from '../../services/status.js';
|
|
6
|
+
import { FEATURE_TYPES } from '../../constants.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a feature is completed (all relevant steps are done)
|
|
10
|
+
* @param {Object} status - Feature status object with steps and flow
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
function isFeatureCompleted(status) {
|
|
14
|
+
const steps = status.steps || {};
|
|
15
|
+
const isQuickFlow = status.flow === 'quick';
|
|
16
|
+
const relevantSteps = isQuickFlow
|
|
17
|
+
? Object.entries(steps).filter(([k]) => QUICK_STEPS.includes(k))
|
|
18
|
+
: Object.entries(steps);
|
|
19
|
+
|
|
20
|
+
if (relevantSteps.length === 0) return false;
|
|
21
|
+
return relevantSteps.every(([_, s]) => s === 'done');
|
|
22
|
+
}
|
|
6
23
|
import { createFeature, validateFeatureName } from '../../services/feature.js';
|
|
7
24
|
import { runStep } from '../../services/runner.js';
|
|
8
25
|
import { runQuick } from '../../services/quick.js';
|
|
@@ -12,6 +29,7 @@ import { callModel } from '../../services/model-call.js';
|
|
|
12
29
|
import { loadConfig } from '../../models.js';
|
|
13
30
|
import { json, error } from '../router.js';
|
|
14
31
|
import { isWtInstalled, hasWorktree, getFeatureBranch } from '../../services/worktrunk.js';
|
|
32
|
+
import { getSession, isRunning, addSseClient, removeSseClient } from '../../services/agent-sessions.js';
|
|
15
33
|
|
|
16
34
|
const MAX_DESCRIPTION_LENGTH = 50000; // 50KB
|
|
17
35
|
|
|
@@ -21,24 +39,15 @@ function sseHeaders(res) {
|
|
|
21
39
|
'Cache-Control': 'no-cache',
|
|
22
40
|
'Connection': 'keep-alive',
|
|
23
41
|
'Access-Control-Allow-Origin': '*',
|
|
42
|
+
'X-Accel-Buffering': 'no', // Disable nginx buffering if behind proxy
|
|
24
43
|
});
|
|
44
|
+
res.flushHeaders(); // Force headers to be sent immediately
|
|
25
45
|
}
|
|
26
46
|
|
|
27
47
|
function sseSend(res, event, data) {
|
|
28
48
|
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// F6: Extracted validation helpers to avoid code duplication
|
|
32
|
-
function validateHistory(rawHistory) {
|
|
33
|
-
return Array.isArray(rawHistory)
|
|
34
|
-
? rawHistory
|
|
35
|
-
.filter(msg => msg && typeof msg === 'object' && typeof msg.content === 'string')
|
|
36
|
-
.map(msg => ({
|
|
37
|
-
role: msg.role === 'agent' ? 'agent' : 'user',
|
|
38
|
-
content: String(msg.content).slice(0, 50000),
|
|
39
|
-
}))
|
|
40
|
-
.slice(-20)
|
|
41
|
-
: [];
|
|
49
|
+
// Force flush for SSE streaming
|
|
50
|
+
if (res.flush) res.flush();
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
function validateAttachments(rawAttachments) {
|
|
@@ -51,7 +60,10 @@ function validateAttachments(rawAttachments) {
|
|
|
51
60
|
|
|
52
61
|
export function registerFeatureRoutes(router) {
|
|
53
62
|
// List all features
|
|
54
|
-
|
|
63
|
+
// Query params:
|
|
64
|
+
// ?filter=active|deleted|all (default: active)
|
|
65
|
+
// ?minimal=true - Return minimal data for faster initial load (name, type, flow, createdAt, deletedAt only)
|
|
66
|
+
router.get('/api/features', async (req, res, { root, query }) => {
|
|
55
67
|
const featuresDir = path.join(root, AIA_DIR, 'features');
|
|
56
68
|
if (!(await fs.pathExists(featuresDir))) {
|
|
57
69
|
return json(res, []);
|
|
@@ -59,14 +71,49 @@ export function registerFeatureRoutes(router) {
|
|
|
59
71
|
const entries = await fs.readdir(featuresDir, { withFileTypes: true });
|
|
60
72
|
const features = [];
|
|
61
73
|
const wtInstalled = isWtInstalled();
|
|
74
|
+
|
|
75
|
+
// Parse filter from query params (default: active, with validation)
|
|
76
|
+
const validFilters = [DELETION_FILTER.ACTIVE, DELETION_FILTER.DELETED, DELETION_FILTER.ALL];
|
|
77
|
+
const filter = validFilters.includes(query?.filter) ? query.filter : DELETION_FILTER.ACTIVE;
|
|
78
|
+
|
|
79
|
+
// Parse minimal flag for lazy loading support
|
|
80
|
+
const minimal = query?.minimal === 'true';
|
|
81
|
+
|
|
62
82
|
for (const entry of entries) {
|
|
63
83
|
if (entry.isDirectory()) {
|
|
64
84
|
try {
|
|
65
85
|
const status = await loadStatus(entry.name, root);
|
|
66
|
-
const
|
|
67
|
-
|
|
86
|
+
const deleted = isFeatureDeleted(status);
|
|
87
|
+
|
|
88
|
+
// Apply deletion filter
|
|
89
|
+
if (filter === DELETION_FILTER.ACTIVE && deleted) continue;
|
|
90
|
+
if (filter === DELETION_FILTER.DELETED && !deleted) continue;
|
|
91
|
+
// DELETION_FILTER.ALL shows everything
|
|
92
|
+
|
|
93
|
+
if (minimal) {
|
|
94
|
+
// Minimal mode: return basic info + essential computed fields for fast initial render
|
|
95
|
+
// Include hasWorktree and isCompleted since they're needed for filtering
|
|
96
|
+
const hasWt = wtInstalled && hasWorktree(getFeatureBranch(entry.name), root);
|
|
97
|
+
const completed = isFeatureCompleted(status);
|
|
98
|
+
features.push({
|
|
99
|
+
name: entry.name,
|
|
100
|
+
type: status.type,
|
|
101
|
+
flow: status.flow,
|
|
102
|
+
createdAt: status.createdAt,
|
|
103
|
+
deletedAt: status.deletedAt,
|
|
104
|
+
isDeleted: deleted,
|
|
105
|
+
hasWorktree: hasWt,
|
|
106
|
+
isCompleted: completed,
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
// Full mode: return all data including computed states
|
|
110
|
+
const hasWt = wtInstalled && hasWorktree(getFeatureBranch(entry.name), root);
|
|
111
|
+
const running = isRunning(entry.name);
|
|
112
|
+
const completed = isFeatureCompleted(status);
|
|
113
|
+
features.push({ name: entry.name, ...status, hasWorktree: hasWt, isDeleted: deleted, agentRunning: running, isCompleted: completed });
|
|
114
|
+
}
|
|
68
115
|
} catch {
|
|
69
|
-
features.push({ name: entry.name, error: true, hasWorktree: false });
|
|
116
|
+
features.push({ name: entry.name, error: true, hasWorktree: false, isDeleted: false, agentRunning: false });
|
|
70
117
|
}
|
|
71
118
|
}
|
|
72
119
|
}
|
|
@@ -85,6 +132,36 @@ export function registerFeatureRoutes(router) {
|
|
|
85
132
|
}
|
|
86
133
|
});
|
|
87
134
|
|
|
135
|
+
// Get agent running status for a feature
|
|
136
|
+
router.get('/api/features/:name/agent-status', (req, res, { params }) => {
|
|
137
|
+
const session = getSession(params.name);
|
|
138
|
+
if (!session) {
|
|
139
|
+
return json(res, { running: false });
|
|
140
|
+
}
|
|
141
|
+
json(res, {
|
|
142
|
+
running: true,
|
|
143
|
+
step: session.step,
|
|
144
|
+
startedAt: session.startedAt,
|
|
145
|
+
logs: session.logs,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// SSE stream for agent logs (reconnection endpoint)
|
|
150
|
+
router.get('/api/features/:name/agent-stream', (req, res, { params }) => {
|
|
151
|
+
const session = getSession(params.name);
|
|
152
|
+
if (!session) {
|
|
153
|
+
return error(res, 'No active session', 404);
|
|
154
|
+
}
|
|
155
|
+
sseHeaders(res);
|
|
156
|
+
// Replay buffered logs
|
|
157
|
+
for (const log of session.logs) {
|
|
158
|
+
sseSend(res, 'log', { text: log.text, type: log.type });
|
|
159
|
+
}
|
|
160
|
+
// Register for live updates
|
|
161
|
+
addSseClient(params.name, res);
|
|
162
|
+
req.on('close', () => removeSseClient(params.name, res));
|
|
163
|
+
});
|
|
164
|
+
|
|
88
165
|
// Read a feature file
|
|
89
166
|
router.get('/api/features/:name/files/:filename', async (req, res, { params, root }) => {
|
|
90
167
|
const filePath = path.join(root, AIA_DIR, 'features', params.name, params.filename);
|
|
@@ -107,9 +184,10 @@ export function registerFeatureRoutes(router) {
|
|
|
107
184
|
router.post('/api/features', async (req, res, { root, parseBody }) => {
|
|
108
185
|
const body = await parseBody();
|
|
109
186
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
187
|
+
const { name, type = 'feature', apps = [] } = body;
|
|
188
|
+
validateFeatureName(name);
|
|
189
|
+
await createFeature(name, root, { type, apps });
|
|
190
|
+
json(res, { ok: true, name, type, apps }, 201);
|
|
113
191
|
} catch (err) {
|
|
114
192
|
error(res, err.message, 400);
|
|
115
193
|
}
|
|
@@ -126,12 +204,10 @@ export function registerFeatureRoutes(router) {
|
|
|
126
204
|
};
|
|
127
205
|
|
|
128
206
|
try {
|
|
129
|
-
const validatedHistory = validateHistory(body.history || []);
|
|
130
207
|
const validatedAttachments = validateAttachments(body.attachments || []);
|
|
131
208
|
|
|
132
209
|
const output = await runStep(params.step, params.name, {
|
|
133
210
|
description: body.description,
|
|
134
|
-
history: validatedHistory,
|
|
135
211
|
attachments: validatedAttachments,
|
|
136
212
|
model: body.model || undefined,
|
|
137
213
|
verbose: body.verbose !== undefined ? body.verbose : true,
|
|
@@ -170,30 +246,6 @@ export function registerFeatureRoutes(router) {
|
|
|
170
246
|
res.end();
|
|
171
247
|
});
|
|
172
248
|
|
|
173
|
-
// Quick ticket with SSE streaming (create + run)
|
|
174
|
-
router.post('/api/quick', async (req, res, { root, parseBody }) => {
|
|
175
|
-
const body = await parseBody();
|
|
176
|
-
sseHeaders(res);
|
|
177
|
-
sseSend(res, 'status', { status: 'started', mode: 'quick', name: body.name });
|
|
178
|
-
|
|
179
|
-
const onData = ({ type, text }) => {
|
|
180
|
-
try { sseSend(res, 'log', { type, text }); } catch {}
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
await runQuick(body.name, {
|
|
185
|
-
description: body.description,
|
|
186
|
-
apply: body.apply || false,
|
|
187
|
-
root,
|
|
188
|
-
onData,
|
|
189
|
-
});
|
|
190
|
-
sseSend(res, 'done', { status: 'completed', name: body.name });
|
|
191
|
-
} catch (err) {
|
|
192
|
-
sseSend(res, 'error', { message: err.message });
|
|
193
|
-
}
|
|
194
|
-
res.end();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
249
|
// Iterate a step with SSE streaming (reset + re-run with instructions)
|
|
198
250
|
router.post('/api/features/:name/iterate/:step', async (req, res, { params, root, parseBody }) => {
|
|
199
251
|
const body = await parseBody();
|
|
@@ -207,12 +259,10 @@ export function registerFeatureRoutes(router) {
|
|
|
207
259
|
try { sseSend(res, 'log', { type, text }); } catch {}
|
|
208
260
|
};
|
|
209
261
|
|
|
210
|
-
const validatedHistory = validateHistory(body.history || []);
|
|
211
262
|
const validatedAttachments = validateAttachments(body.attachments || []);
|
|
212
263
|
|
|
213
264
|
await runStep(params.step, params.name, {
|
|
214
265
|
instructions: body.instructions,
|
|
215
|
-
history: validatedHistory,
|
|
216
266
|
attachments: validatedAttachments,
|
|
217
267
|
model: body.model || undefined,
|
|
218
268
|
verbose: body.verbose !== undefined ? body.verbose : true,
|
|
@@ -490,4 +540,64 @@ IMPORTANT:
|
|
|
490
540
|
error(res, err.message, 400);
|
|
491
541
|
}
|
|
492
542
|
});
|
|
543
|
+
|
|
544
|
+
// Update feature type (feature/bug)
|
|
545
|
+
router.patch('/api/features/:name/type', async (req, res, { params, root, parseBody }) => {
|
|
546
|
+
const body = await parseBody();
|
|
547
|
+
const { type } = body;
|
|
548
|
+
|
|
549
|
+
if (!type || !FEATURE_TYPES.includes(type)) {
|
|
550
|
+
return error(res, `Invalid type. Must be one of: ${FEATURE_TYPES.join(', ')}`, 400);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
await updateType(params.name, type, root);
|
|
555
|
+
json(res, { ok: true, type });
|
|
556
|
+
} catch (err) {
|
|
557
|
+
error(res, err.message, 400);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Update feature apps/scope
|
|
562
|
+
router.patch('/api/features/:name/apps', async (req, res, { params, root, parseBody }) => {
|
|
563
|
+
const body = await parseBody();
|
|
564
|
+
const { apps } = body;
|
|
565
|
+
|
|
566
|
+
if (!Array.isArray(apps)) {
|
|
567
|
+
return error(res, 'apps must be an array', 400);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Validate all elements are non-empty strings
|
|
571
|
+
const invalidApps = apps.filter(a => typeof a !== 'string' || !a.trim());
|
|
572
|
+
if (invalidApps.length > 0) {
|
|
573
|
+
return error(res, 'apps must contain only non-empty strings', 400);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
await updateApps(params.name, apps, root);
|
|
578
|
+
json(res, { ok: true, apps });
|
|
579
|
+
} catch (err) {
|
|
580
|
+
error(res, err.message, 400);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// Soft delete a feature
|
|
585
|
+
router.delete('/api/features/:name', async (req, res, { params, root }) => {
|
|
586
|
+
try {
|
|
587
|
+
await softDeleteFeature(params.name, root);
|
|
588
|
+
json(res, { ok: true, deletedAt: new Date().toISOString() });
|
|
589
|
+
} catch (err) {
|
|
590
|
+
error(res, err.message, 400);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Restore a deleted feature
|
|
595
|
+
router.post('/api/features/:name/restore', async (req, res, { params, root }) => {
|
|
596
|
+
try {
|
|
597
|
+
await restoreFeature(params.name, root);
|
|
598
|
+
json(res, { ok: true });
|
|
599
|
+
} catch (err) {
|
|
600
|
+
error(res, err.message, 400);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
493
603
|
}
|
package/src/ui/api/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { registerConfigRoutes } from './config.js';
|
|
|
3
3
|
import { registerLogRoutes } from './logs.js';
|
|
4
4
|
import { registerConstantsRoutes } from './constants.js';
|
|
5
5
|
import { registerWorktrunkRoutes } from './worktrunk.js';
|
|
6
|
+
import { registerTestQuickRoutes } from './test-quick.js';
|
|
6
7
|
|
|
7
8
|
export function registerApiRoutes(router, root) {
|
|
8
9
|
registerFeatureRoutes(router);
|
|
@@ -10,4 +11,5 @@ export function registerApiRoutes(router, root) {
|
|
|
10
11
|
registerLogRoutes(router);
|
|
11
12
|
registerConstantsRoutes(router);
|
|
12
13
|
registerWorktrunkRoutes(router);
|
|
14
|
+
registerTestQuickRoutes(router);
|
|
13
15
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview API routes for test-quick feature
|
|
3
|
+
* Provides REST endpoints for managing test-quick items
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { json, error } from '../router.js';
|
|
7
|
+
import {
|
|
8
|
+
loadItems,
|
|
9
|
+
createItem,
|
|
10
|
+
getItem,
|
|
11
|
+
listItems,
|
|
12
|
+
updateItem,
|
|
13
|
+
deleteItem,
|
|
14
|
+
updateStatus,
|
|
15
|
+
clearCompleted,
|
|
16
|
+
} from '../../services/test-quick.js';
|
|
17
|
+
import { TEST_QUICK_STATUS, isValidTestQuickName } from '../../types/test-quick.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Registers test-quick API routes
|
|
21
|
+
* @param {Object} router - Router instance
|
|
22
|
+
*/
|
|
23
|
+
export function registerTestQuickRoutes(router) {
|
|
24
|
+
/**
|
|
25
|
+
* GET /api/test-quick/statuses
|
|
26
|
+
* Get available status values
|
|
27
|
+
* Note: Must be registered before :name route to avoid conflict
|
|
28
|
+
*/
|
|
29
|
+
router.get('/api/test-quick/statuses', async (req, res) => {
|
|
30
|
+
json(res, { statuses: Object.values(TEST_QUICK_STATUS) });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* GET /api/test-quick
|
|
35
|
+
* List all test-quick items with optional status filter
|
|
36
|
+
*/
|
|
37
|
+
router.get('/api/test-quick', async (req, res, { root, query }) => {
|
|
38
|
+
try {
|
|
39
|
+
const options = {};
|
|
40
|
+
if (query?.status) {
|
|
41
|
+
options.status = query.status;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await listItems(options, root);
|
|
45
|
+
json(res, result);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
error(res, err.message, 500);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* GET /api/test-quick/:name
|
|
53
|
+
* Get a single test-quick item by name
|
|
54
|
+
*/
|
|
55
|
+
router.get('/api/test-quick/:name', async (req, res, { params, root }) => {
|
|
56
|
+
try {
|
|
57
|
+
const result = await getItem(params.name, root);
|
|
58
|
+
|
|
59
|
+
if (!result.success) {
|
|
60
|
+
return error(res, result.error, 404);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
json(res, result);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
error(res, err.message, 500);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* POST /api/test-quick
|
|
71
|
+
* Create a new test-quick item
|
|
72
|
+
*/
|
|
73
|
+
router.post('/api/test-quick', async (req, res, { root, parseBody }) => {
|
|
74
|
+
try {
|
|
75
|
+
const body = await parseBody();
|
|
76
|
+
|
|
77
|
+
if (!body.name) {
|
|
78
|
+
return error(res, 'Name is required', 400);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!isValidTestQuickName(body.name)) {
|
|
82
|
+
return error(
|
|
83
|
+
res,
|
|
84
|
+
'Invalid name. Use lowercase alphanumeric with hyphens (e.g., my-item)',
|
|
85
|
+
400
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result = await createItem(
|
|
90
|
+
{
|
|
91
|
+
name: body.name,
|
|
92
|
+
description: body.description,
|
|
93
|
+
},
|
|
94
|
+
root
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!result.success) {
|
|
98
|
+
return error(res, result.error, 400);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
json(res, result, 201);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
error(res, err.message, 500);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* PUT /api/test-quick/:name
|
|
109
|
+
* Update a test-quick item
|
|
110
|
+
*/
|
|
111
|
+
router.put('/api/test-quick/:name', async (req, res, { params, root, parseBody }) => {
|
|
112
|
+
try {
|
|
113
|
+
const body = await parseBody();
|
|
114
|
+
const updates = {};
|
|
115
|
+
|
|
116
|
+
if (body.description !== undefined) {
|
|
117
|
+
updates.description = body.description;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (body.status !== undefined) {
|
|
121
|
+
const validStatuses = Object.values(TEST_QUICK_STATUS);
|
|
122
|
+
if (!validStatuses.includes(body.status)) {
|
|
123
|
+
return error(res, `Invalid status. Valid values: ${validStatuses.join(', ')}`, 400);
|
|
124
|
+
}
|
|
125
|
+
updates.status = body.status;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (body.metadata !== undefined) {
|
|
129
|
+
updates.metadata = body.metadata;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (Object.keys(updates).length === 0) {
|
|
133
|
+
return error(res, 'No valid updates provided', 400);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = await updateItem(params.name, updates, root);
|
|
137
|
+
|
|
138
|
+
if (!result.success) {
|
|
139
|
+
return error(res, result.error, 404);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
json(res, result);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
error(res, err.message, 500);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* PATCH /api/test-quick/:name/status
|
|
150
|
+
* Update only the status of a test-quick item
|
|
151
|
+
*/
|
|
152
|
+
router.patch('/api/test-quick/:name/status', async (req, res, { params, root, parseBody }) => {
|
|
153
|
+
try {
|
|
154
|
+
const body = await parseBody();
|
|
155
|
+
|
|
156
|
+
if (!body.status) {
|
|
157
|
+
return error(res, 'Status is required', 400);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const validStatuses = Object.values(TEST_QUICK_STATUS);
|
|
161
|
+
if (!validStatuses.includes(body.status)) {
|
|
162
|
+
return error(res, `Invalid status. Valid values: ${validStatuses.join(', ')}`, 400);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result = await updateStatus(params.name, body.status, root);
|
|
166
|
+
|
|
167
|
+
if (!result.success) {
|
|
168
|
+
return error(res, result.error, 404);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
json(res, result);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
error(res, err.message, 500);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* DELETE /api/test-quick/:name
|
|
179
|
+
* Delete a test-quick item
|
|
180
|
+
*/
|
|
181
|
+
router.delete('/api/test-quick/:name', async (req, res, { params, root }) => {
|
|
182
|
+
try {
|
|
183
|
+
const result = await deleteItem(params.name, root);
|
|
184
|
+
|
|
185
|
+
if (!result.success) {
|
|
186
|
+
return error(res, result.error, 404);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
json(res, result);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
error(res, err.message, 500);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* POST /api/test-quick/clear-completed
|
|
197
|
+
* Clear all completed items
|
|
198
|
+
*/
|
|
199
|
+
router.post('/api/test-quick/clear-completed', async (req, res, { root }) => {
|
|
200
|
+
try {
|
|
201
|
+
const result = await clearCompleted(root);
|
|
202
|
+
json(res, result);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
error(res, err.message, 500);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
package/src/ui/api/worktrunk.js
CHANGED
|
@@ -6,10 +6,13 @@ import {
|
|
|
6
6
|
hasWorktree,
|
|
7
7
|
removeWorktree,
|
|
8
8
|
getFeatureBranch,
|
|
9
|
-
startServices,
|
|
10
|
-
stopServices,
|
|
11
9
|
hasDockerServices,
|
|
10
|
+
isDockerRunning,
|
|
12
11
|
getServicesStatus,
|
|
12
|
+
startComposeService,
|
|
13
|
+
stopComposeService,
|
|
14
|
+
startAllComposeServices,
|
|
15
|
+
stopAllComposeServices,
|
|
13
16
|
} from '../../services/worktrunk.js';
|
|
14
17
|
import { json, error } from '../router.js';
|
|
15
18
|
|
|
@@ -28,21 +31,25 @@ export function registerWorktrunkRoutes(router) {
|
|
|
28
31
|
installed: false,
|
|
29
32
|
hasWorktree: false,
|
|
30
33
|
path: null,
|
|
31
|
-
|
|
34
|
+
docker: { available: false, hasComposeFile: false },
|
|
32
35
|
});
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
const branch = getFeatureBranch(params.name);
|
|
36
39
|
const wtPath = getWorktreePath(branch, root);
|
|
37
40
|
const hasWt = wtPath !== null;
|
|
38
|
-
const
|
|
41
|
+
const dockerRunning = isDockerRunning();
|
|
42
|
+
const hasComposeFile = hasWt && hasDockerServices(wtPath);
|
|
39
43
|
|
|
40
44
|
json(res, {
|
|
41
45
|
installed: true,
|
|
42
46
|
hasWorktree: hasWt,
|
|
43
47
|
path: wtPath,
|
|
44
48
|
branch,
|
|
45
|
-
|
|
49
|
+
docker: {
|
|
50
|
+
available: dockerRunning,
|
|
51
|
+
hasComposeFile,
|
|
52
|
+
},
|
|
46
53
|
});
|
|
47
54
|
});
|
|
48
55
|
|
|
@@ -94,29 +101,45 @@ export function registerWorktrunkRoutes(router) {
|
|
|
94
101
|
}
|
|
95
102
|
});
|
|
96
103
|
|
|
97
|
-
//
|
|
98
|
-
router.
|
|
104
|
+
// Get services status (from docker-compose.wt.yml)
|
|
105
|
+
router.get('/api/features/:name/wt/services', async (req, res, { params, root }) => {
|
|
99
106
|
const branch = getFeatureBranch(params.name);
|
|
100
107
|
const wtPath = getWorktreePath(branch, root);
|
|
101
108
|
|
|
102
109
|
if (!wtPath) {
|
|
103
|
-
return
|
|
110
|
+
return json(res, { services: [], hasComposeFile: false, dockerAvailable: false });
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
const dockerAvailable = isDockerRunning();
|
|
114
|
+
const hasComposeFile = hasDockerServices(wtPath);
|
|
115
|
+
|
|
116
|
+
if (!dockerAvailable || !hasComposeFile) {
|
|
117
|
+
return json(res, { services: [], hasComposeFile, dockerAvailable });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const services = getServicesStatus(wtPath);
|
|
121
|
+
json(res, { services, hasComposeFile, dockerAvailable });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Start a specific service
|
|
125
|
+
router.post('/api/features/:name/wt/services/:service/start', async (req, res, { params, root }) => {
|
|
126
|
+
const branch = getFeatureBranch(params.name);
|
|
127
|
+
const wtPath = getWorktreePath(branch, root);
|
|
128
|
+
|
|
129
|
+
if (!wtPath) {
|
|
130
|
+
return error(res, 'No worktree found for this feature', 404);
|
|
108
131
|
}
|
|
109
132
|
|
|
110
133
|
try {
|
|
111
|
-
|
|
134
|
+
startComposeService(wtPath, params.service);
|
|
112
135
|
json(res, { ok: true });
|
|
113
136
|
} catch (err) {
|
|
114
|
-
error(res, `Failed to start
|
|
137
|
+
error(res, `Failed to start service: ${err.message}`, 500);
|
|
115
138
|
}
|
|
116
139
|
});
|
|
117
140
|
|
|
118
|
-
// Stop
|
|
119
|
-
router.post('/api/features/:name/wt/services/stop', async (req, res, { params, root }) => {
|
|
141
|
+
// Stop a specific service
|
|
142
|
+
router.post('/api/features/:name/wt/services/:service/stop', async (req, res, { params, root }) => {
|
|
120
143
|
const branch = getFeatureBranch(params.name);
|
|
121
144
|
const wtPath = getWorktreePath(branch, root);
|
|
122
145
|
|
|
@@ -124,30 +147,45 @@ export function registerWorktrunkRoutes(router) {
|
|
|
124
147
|
return error(res, 'No worktree found for this feature', 404);
|
|
125
148
|
}
|
|
126
149
|
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
try {
|
|
151
|
+
stopComposeService(wtPath, params.service);
|
|
152
|
+
json(res, { ok: true });
|
|
153
|
+
} catch (err) {
|
|
154
|
+
error(res, `Failed to stop service: ${err.message}`, 500);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Start all services
|
|
159
|
+
router.post('/api/features/:name/wt/services/start', async (req, res, { params, root }) => {
|
|
160
|
+
const branch = getFeatureBranch(params.name);
|
|
161
|
+
const wtPath = getWorktreePath(branch, root);
|
|
162
|
+
|
|
163
|
+
if (!wtPath) {
|
|
164
|
+
return error(res, 'No worktree found for this feature', 404);
|
|
129
165
|
}
|
|
130
166
|
|
|
131
167
|
try {
|
|
132
|
-
|
|
168
|
+
startAllComposeServices(wtPath);
|
|
133
169
|
json(res, { ok: true });
|
|
134
170
|
} catch (err) {
|
|
135
|
-
error(res, `Failed to
|
|
171
|
+
error(res, `Failed to start services: ${err.message}`, 500);
|
|
136
172
|
}
|
|
137
173
|
});
|
|
138
174
|
|
|
139
|
-
//
|
|
140
|
-
router.
|
|
175
|
+
// Stop all services
|
|
176
|
+
router.post('/api/features/:name/wt/services/stop', async (req, res, { params, root }) => {
|
|
141
177
|
const branch = getFeatureBranch(params.name);
|
|
142
178
|
const wtPath = getWorktreePath(branch, root);
|
|
143
179
|
|
|
144
180
|
if (!wtPath) {
|
|
145
|
-
return
|
|
181
|
+
return error(res, 'No worktree found for this feature', 404);
|
|
146
182
|
}
|
|
147
183
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
184
|
+
try {
|
|
185
|
+
stopAllComposeServices(wtPath);
|
|
186
|
+
json(res, { ok: true });
|
|
187
|
+
} catch (err) {
|
|
188
|
+
error(res, `Failed to stop services: ${err.message}`, 500);
|
|
189
|
+
}
|
|
152
190
|
});
|
|
153
191
|
}
|