@bamptee/aia-code 2.0.12 → 2.0.13
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 +2 -21
- package/src/providers/cli-runner.js +5 -3
- 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
|
@@ -146,24 +146,15 @@ export function getFeatureBranch(featureName) {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
|
-
*
|
|
150
|
-
* @
|
|
151
|
-
*/
|
|
152
|
-
export function startServices(wtPath) {
|
|
153
|
-
const composePath = path.join(wtPath, 'docker-compose.wt.yml');
|
|
154
|
-
if (fs.existsSync(composePath)) {
|
|
155
|
-
execSync('docker-compose -f docker-compose.wt.yml up -d', { cwd: wtPath, stdio: 'inherit' });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Stop docker-compose services in a worktree
|
|
161
|
-
* @param {string} wtPath - Worktree directory path
|
|
149
|
+
* Check if Docker daemon is running
|
|
150
|
+
* @returns {boolean}
|
|
162
151
|
*/
|
|
163
|
-
export function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
152
|
+
export function isDockerRunning() {
|
|
153
|
+
try {
|
|
154
|
+
execSync('docker info', { stdio: 'ignore' });
|
|
155
|
+
return true;
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
167
158
|
}
|
|
168
159
|
}
|
|
169
160
|
|
|
@@ -179,19 +170,142 @@ export function hasDockerServices(wtPath) {
|
|
|
179
170
|
}
|
|
180
171
|
|
|
181
172
|
/**
|
|
182
|
-
*
|
|
173
|
+
* List services defined in docker-compose.wt.yml
|
|
183
174
|
* @param {string} wtPath - Worktree directory path
|
|
184
|
-
* @returns {Array} List of service
|
|
175
|
+
* @returns {Array<string>} List of service names
|
|
176
|
+
*/
|
|
177
|
+
export function listComposeServices(wtPath) {
|
|
178
|
+
if (!hasDockerServices(wtPath)) return [];
|
|
179
|
+
try {
|
|
180
|
+
const output = execSync('docker-compose -f docker-compose.wt.yml config --services', {
|
|
181
|
+
cwd: wtPath,
|
|
182
|
+
encoding: 'utf-8',
|
|
183
|
+
});
|
|
184
|
+
return output.trim().split('\n').filter(Boolean);
|
|
185
|
+
} catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get status of all services from docker-compose
|
|
192
|
+
* @param {string} wtPath - Worktree directory path
|
|
193
|
+
* @returns {Array} List of service objects with name, state, status
|
|
185
194
|
*/
|
|
186
195
|
export function getServicesStatus(wtPath) {
|
|
187
196
|
if (!hasDockerServices(wtPath)) return [];
|
|
197
|
+
|
|
198
|
+
// Get all defined services
|
|
199
|
+
const definedServices = listComposeServices(wtPath);
|
|
200
|
+
if (!definedServices.length) return [];
|
|
201
|
+
|
|
202
|
+
// Get running containers status
|
|
203
|
+
let runningServices = [];
|
|
188
204
|
try {
|
|
189
205
|
const output = execSync('docker-compose -f docker-compose.wt.yml ps --format json', {
|
|
190
206
|
cwd: wtPath,
|
|
191
207
|
encoding: 'utf-8',
|
|
192
208
|
});
|
|
193
|
-
|
|
209
|
+
// docker-compose ps --format json outputs one JSON per line
|
|
210
|
+
runningServices = output.trim().split('\n').filter(Boolean).map(line => {
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(line);
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}).filter(Boolean);
|
|
194
217
|
} catch {
|
|
195
|
-
|
|
218
|
+
// docker-compose ps failed, all services are stopped
|
|
196
219
|
}
|
|
220
|
+
|
|
221
|
+
// Map defined services to their status
|
|
222
|
+
return definedServices.map(serviceName => {
|
|
223
|
+
const running = runningServices.find(s =>
|
|
224
|
+
s.Service === serviceName || s.Name?.includes(serviceName)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Extract published ports from Publishers array
|
|
228
|
+
const ports = (running?.Publishers || [])
|
|
229
|
+
.filter(p => p.PublishedPort)
|
|
230
|
+
.map(p => ({
|
|
231
|
+
published: p.PublishedPort,
|
|
232
|
+
target: p.TargetPort,
|
|
233
|
+
protocol: p.Protocol || 'tcp',
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
name: serviceName,
|
|
238
|
+
state: running?.State || 'stopped',
|
|
239
|
+
status: running?.Status || 'Stopped',
|
|
240
|
+
health: running?.Health || null,
|
|
241
|
+
ports,
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Start a docker-compose service
|
|
248
|
+
* @param {string} wtPath - Worktree directory path
|
|
249
|
+
* @param {string} serviceName - Service name to start
|
|
250
|
+
*/
|
|
251
|
+
export function startComposeService(wtPath, serviceName) {
|
|
252
|
+
if (!hasDockerServices(wtPath)) {
|
|
253
|
+
throw new Error('No docker-compose.wt.yml found');
|
|
254
|
+
}
|
|
255
|
+
// Validate service name
|
|
256
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(serviceName)) {
|
|
257
|
+
throw new Error(`Invalid service name: "${serviceName}"`);
|
|
258
|
+
}
|
|
259
|
+
execSync(`docker-compose -f docker-compose.wt.yml up -d ${serviceName}`, {
|
|
260
|
+
cwd: wtPath,
|
|
261
|
+
stdio: 'inherit',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Stop a docker-compose service
|
|
267
|
+
* @param {string} wtPath - Worktree directory path
|
|
268
|
+
* @param {string} serviceName - Service name to stop
|
|
269
|
+
*/
|
|
270
|
+
export function stopComposeService(wtPath, serviceName) {
|
|
271
|
+
if (!hasDockerServices(wtPath)) {
|
|
272
|
+
throw new Error('No docker-compose.wt.yml found');
|
|
273
|
+
}
|
|
274
|
+
// Validate service name
|
|
275
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(serviceName)) {
|
|
276
|
+
throw new Error(`Invalid service name: "${serviceName}"`);
|
|
277
|
+
}
|
|
278
|
+
execSync(`docker-compose -f docker-compose.wt.yml stop ${serviceName}`, {
|
|
279
|
+
cwd: wtPath,
|
|
280
|
+
stdio: 'inherit',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Start all docker-compose services
|
|
286
|
+
* @param {string} wtPath - Worktree directory path
|
|
287
|
+
*/
|
|
288
|
+
export function startAllComposeServices(wtPath) {
|
|
289
|
+
if (!hasDockerServices(wtPath)) {
|
|
290
|
+
throw new Error('No docker-compose.wt.yml found');
|
|
291
|
+
}
|
|
292
|
+
execSync('docker-compose -f docker-compose.wt.yml up -d', {
|
|
293
|
+
cwd: wtPath,
|
|
294
|
+
stdio: 'inherit',
|
|
295
|
+
});
|
|
197
296
|
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Stop all docker-compose services
|
|
300
|
+
* @param {string} wtPath - Worktree directory path
|
|
301
|
+
*/
|
|
302
|
+
export function stopAllComposeServices(wtPath) {
|
|
303
|
+
if (!hasDockerServices(wtPath)) {
|
|
304
|
+
throw new Error('No docker-compose.wt.yml found');
|
|
305
|
+
}
|
|
306
|
+
execSync('docker-compose -f docker-compose.wt.yml stop', {
|
|
307
|
+
cwd: wtPath,
|
|
308
|
+
stdio: 'inherit',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Type definitions for test-quick feature
|
|
3
|
+
* Uses JSDoc for type definitions in this JavaScript codebase
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} TestQuickConfig
|
|
8
|
+
* @property {string} name - Name of the test-quick item
|
|
9
|
+
* @property {string} [description] - Optional description
|
|
10
|
+
* @property {boolean} [enabled=true] - Whether the item is enabled
|
|
11
|
+
* @property {string} [createdAt] - ISO timestamp of creation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} TestQuickItem
|
|
16
|
+
* @property {string} id - Unique identifier
|
|
17
|
+
* @property {string} name - Item name
|
|
18
|
+
* @property {string} [description] - Optional description
|
|
19
|
+
* @property {'pending'|'active'|'completed'|'error'} status - Current status
|
|
20
|
+
* @property {Object} [metadata] - Additional metadata
|
|
21
|
+
* @property {string} createdAt - ISO timestamp of creation
|
|
22
|
+
* @property {string} [updatedAt] - ISO timestamp of last update
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} TestQuickResult
|
|
27
|
+
* @property {boolean} success - Whether the operation succeeded
|
|
28
|
+
* @property {string} [message] - Result message
|
|
29
|
+
* @property {TestQuickItem} [data] - Result data
|
|
30
|
+
* @property {string} [error] - Error message if failed
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Object} TestQuickListResult
|
|
35
|
+
* @property {boolean} success - Whether the operation succeeded
|
|
36
|
+
* @property {TestQuickItem[]} items - List of items
|
|
37
|
+
* @property {number} total - Total count
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Valid status values for test-quick items
|
|
42
|
+
* @type {Object.<string, string>}
|
|
43
|
+
*/
|
|
44
|
+
export const TEST_QUICK_STATUS = {
|
|
45
|
+
PENDING: 'pending',
|
|
46
|
+
ACTIVE: 'active',
|
|
47
|
+
COMPLETED: 'completed',
|
|
48
|
+
ERROR: 'error',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Default configuration for test-quick
|
|
53
|
+
* @type {TestQuickConfig}
|
|
54
|
+
*/
|
|
55
|
+
export const DEFAULT_TEST_QUICK_CONFIG = {
|
|
56
|
+
name: '',
|
|
57
|
+
description: '',
|
|
58
|
+
enabled: true,
|
|
59
|
+
createdAt: null,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validates a test-quick item name
|
|
64
|
+
* @param {string} name - Name to validate
|
|
65
|
+
* @returns {boolean} Whether the name is valid
|
|
66
|
+
*/
|
|
67
|
+
export function isValidTestQuickName(name) {
|
|
68
|
+
if (!name || typeof name !== 'string') return false;
|
|
69
|
+
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Creates a new test-quick item with defaults
|
|
74
|
+
* @param {Partial<TestQuickItem>} data - Partial item data
|
|
75
|
+
* @returns {TestQuickItem} Complete item with defaults
|
|
76
|
+
*/
|
|
77
|
+
export function createTestQuickItem(data) {
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
return {
|
|
80
|
+
id: data.id || `tq-${Date.now()}`,
|
|
81
|
+
name: data.name || '',
|
|
82
|
+
description: data.description || '',
|
|
83
|
+
status: data.status || TEST_QUICK_STATUS.PENDING,
|
|
84
|
+
metadata: data.metadata || {},
|
|
85
|
+
createdAt: data.createdAt || now,
|
|
86
|
+
updatedAt: now,
|
|
87
|
+
};
|
|
88
|
+
}
|
package/src/ui/api/config.js
CHANGED
|
@@ -2,10 +2,37 @@ import path from 'node:path';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import yaml from 'yaml';
|
|
4
4
|
import { AIA_DIR } from '../../constants.js';
|
|
5
|
-
import { loadGlobalConfig, saveGlobalConfig, getGlobalConfigPath } from '../../services/config.js';
|
|
5
|
+
import { loadGlobalConfig, saveGlobalConfig, getGlobalConfigPath, getApps, setApps, toggleApp } from '../../services/config.js';
|
|
6
|
+
import { scanApps } from '../../services/apps.js';
|
|
6
7
|
import { json, error } from '../router.js';
|
|
7
8
|
|
|
8
9
|
export function registerConfigRoutes(router) {
|
|
10
|
+
// GET /api/apps - List configured apps
|
|
11
|
+
router.get('/api/apps', async (req, res, { root }) => {
|
|
12
|
+
const apps = await getApps(root);
|
|
13
|
+
json(res, apps);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// POST /api/apps/scan - Re-scan and update apps
|
|
17
|
+
router.post('/api/apps/scan', async (req, res, { root }) => {
|
|
18
|
+
const scanned = await scanApps(root);
|
|
19
|
+
const existing = await getApps(root);
|
|
20
|
+
// Merge: keep enabled status from existing apps
|
|
21
|
+
const merged = scanned.map(s => {
|
|
22
|
+
const ex = existing.find(e => e.path === s.path);
|
|
23
|
+
return ex ? { ...s, enabled: ex.enabled } : s;
|
|
24
|
+
});
|
|
25
|
+
await setApps(merged, root);
|
|
26
|
+
json(res, merged);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// PATCH /api/apps/:name - Toggle app enabled status
|
|
30
|
+
router.patch('/api/apps/:name', async (req, res, { params, root, parseBody }) => {
|
|
31
|
+
const { enabled } = await parseBody();
|
|
32
|
+
await toggleApp(params.name, enabled, root);
|
|
33
|
+
json(res, { ok: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
9
36
|
// Get project config
|
|
10
37
|
router.get('/api/config', async (req, res, { root }) => {
|
|
11
38
|
const configPath = path.join(root, AIA_DIR, 'config.yaml');
|
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
|
}
|