@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/services/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import yaml from 'yaml';
|
|
4
|
-
import { AIA_DIR, FEATURE_STEPS, STEP_STATUS } from '../constants.js';
|
|
4
|
+
import { AIA_DIR, FEATURE_STEPS, STEP_STATUS, FEATURE_TYPES } from '../constants.js';
|
|
5
5
|
|
|
6
6
|
function statusPath(feature, root) {
|
|
7
7
|
return path.join(root, AIA_DIR, 'features', feature, 'status.yaml');
|
|
@@ -79,3 +79,71 @@ export async function resetStep(feature, step, root = process.cwd()) {
|
|
|
79
79
|
|
|
80
80
|
// Keep the existing output — it will be fed back as context on re-run
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
export async function updateType(feature, type, root = process.cwd()) {
|
|
84
|
+
// Validate type
|
|
85
|
+
if (!FEATURE_TYPES.includes(type)) {
|
|
86
|
+
throw new Error(`Invalid type "${type}". Valid types: ${FEATURE_TYPES.join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const status = await loadStatus(feature, root);
|
|
90
|
+
status.type = type;
|
|
91
|
+
|
|
92
|
+
// Bug type forces quick flow, but only if no steps have been completed
|
|
93
|
+
if (type === 'bug') {
|
|
94
|
+
const allPending = Object.values(status.steps).every(s => s === STEP_STATUS.PENDING);
|
|
95
|
+
if (allPending) {
|
|
96
|
+
status.flow = 'quick';
|
|
97
|
+
status.current_step = 'dev-plan';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const content = yaml.stringify(status);
|
|
102
|
+
await fs.writeFile(statusPath(feature, root), content, 'utf-8');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function updateApps(feature, apps, root = process.cwd()) {
|
|
106
|
+
// Validate apps - must be an array of strings
|
|
107
|
+
if (!Array.isArray(apps)) {
|
|
108
|
+
throw new Error('apps must be an array');
|
|
109
|
+
}
|
|
110
|
+
const validApps = apps.filter(a => typeof a === 'string' && a.trim()).map(a => a.trim());
|
|
111
|
+
|
|
112
|
+
const status = await loadStatus(feature, root);
|
|
113
|
+
status.apps = validApps;
|
|
114
|
+
const content = yaml.stringify(status);
|
|
115
|
+
await fs.writeFile(statusPath(feature, root), content, 'utf-8');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Soft delete a feature by setting deletedAt timestamp
|
|
120
|
+
* @param {string} feature - Feature name
|
|
121
|
+
* @param {string} root - Project root directory
|
|
122
|
+
*/
|
|
123
|
+
export async function softDeleteFeature(feature, root = process.cwd()) {
|
|
124
|
+
const status = await loadStatus(feature, root);
|
|
125
|
+
status.deletedAt = new Date().toISOString();
|
|
126
|
+
const content = yaml.stringify(status);
|
|
127
|
+
await fs.writeFile(statusPath(feature, root), content, 'utf-8');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Restore a deleted feature by clearing deletedAt
|
|
132
|
+
* @param {string} feature - Feature name
|
|
133
|
+
* @param {string} root - Project root directory
|
|
134
|
+
*/
|
|
135
|
+
export async function restoreFeature(feature, root = process.cwd()) {
|
|
136
|
+
const status = await loadStatus(feature, root);
|
|
137
|
+
delete status.deletedAt;
|
|
138
|
+
const content = yaml.stringify(status);
|
|
139
|
+
await fs.writeFile(statusPath(feature, root), content, 'utf-8');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if a feature is deleted
|
|
144
|
+
* @param {object} status - Feature status object
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
export function isFeatureDeleted(status) {
|
|
148
|
+
return !!(status && status.deletedAt != null);
|
|
149
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Service layer for test-quick feature
|
|
3
|
+
* Provides business logic for managing test-quick items
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import fs from 'fs-extra';
|
|
8
|
+
import yaml from 'yaml';
|
|
9
|
+
import { AIA_DIR } from '../constants.js';
|
|
10
|
+
import {
|
|
11
|
+
TEST_QUICK_STATUS,
|
|
12
|
+
isValidTestQuickName,
|
|
13
|
+
createTestQuickItem,
|
|
14
|
+
} from '../types/test-quick.js';
|
|
15
|
+
|
|
16
|
+
const TEST_QUICK_DIR = 'test-quick';
|
|
17
|
+
const DATA_FILE = 'items.yaml';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gets the test-quick directory path
|
|
21
|
+
* @param {string} root - Project root directory
|
|
22
|
+
* @returns {string} Path to test-quick directory
|
|
23
|
+
*/
|
|
24
|
+
function getTestQuickDir(root) {
|
|
25
|
+
return path.join(root, AIA_DIR, TEST_QUICK_DIR);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Gets the data file path
|
|
30
|
+
* @param {string} root - Project root directory
|
|
31
|
+
* @returns {string} Path to data file
|
|
32
|
+
*/
|
|
33
|
+
function getDataFilePath(root) {
|
|
34
|
+
return path.join(getTestQuickDir(root), DATA_FILE);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Ensures the test-quick directory exists
|
|
39
|
+
* @param {string} root - Project root directory
|
|
40
|
+
*/
|
|
41
|
+
async function ensureTestQuickDir(root) {
|
|
42
|
+
await fs.ensureDir(getTestQuickDir(root));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Loads all test-quick items from storage
|
|
47
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
48
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickItem[]>} List of items
|
|
49
|
+
*/
|
|
50
|
+
export async function loadItems(root = process.cwd()) {
|
|
51
|
+
const dataPath = getDataFilePath(root);
|
|
52
|
+
|
|
53
|
+
if (!(await fs.pathExists(dataPath))) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const content = await fs.readFile(dataPath, 'utf-8');
|
|
58
|
+
const data = yaml.parse(content);
|
|
59
|
+
|
|
60
|
+
return Array.isArray(data?.items) ? data.items : [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Saves all test-quick items to storage
|
|
65
|
+
* @param {import('../types/test-quick.js').TestQuickItem[]} items - Items to save
|
|
66
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
67
|
+
*/
|
|
68
|
+
async function saveItems(items, root = process.cwd()) {
|
|
69
|
+
await ensureTestQuickDir(root);
|
|
70
|
+
const dataPath = getDataFilePath(root);
|
|
71
|
+
const content = yaml.stringify({ items, updatedAt: new Date().toISOString() });
|
|
72
|
+
await fs.writeFile(dataPath, content, 'utf-8');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Creates a new test-quick item
|
|
77
|
+
* @param {Object} data - Item data
|
|
78
|
+
* @param {string} data.name - Item name
|
|
79
|
+
* @param {string} [data.description] - Item description
|
|
80
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
81
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickResult>} Creation result
|
|
82
|
+
*/
|
|
83
|
+
export async function createItem(data, root = process.cwd()) {
|
|
84
|
+
const { name, description } = data;
|
|
85
|
+
|
|
86
|
+
if (!name) {
|
|
87
|
+
return { success: false, error: 'Name is required' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!isValidTestQuickName(name)) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: 'Invalid name. Use lowercase alphanumeric with hyphens (e.g., my-item)',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const items = await loadItems(root);
|
|
98
|
+
|
|
99
|
+
if (items.some(item => item.name === name)) {
|
|
100
|
+
return { success: false, error: `Item "${name}" already exists` };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const newItem = createTestQuickItem({ name, description });
|
|
104
|
+
items.push(newItem);
|
|
105
|
+
|
|
106
|
+
await saveItems(items, root);
|
|
107
|
+
|
|
108
|
+
return { success: true, message: `Item "${name}" created`, data: newItem };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Gets a single test-quick item by name
|
|
113
|
+
* @param {string} name - Item name
|
|
114
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
115
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickResult>} Item result
|
|
116
|
+
*/
|
|
117
|
+
export async function getItem(name, root = process.cwd()) {
|
|
118
|
+
const items = await loadItems(root);
|
|
119
|
+
const item = items.find(i => i.name === name);
|
|
120
|
+
|
|
121
|
+
if (!item) {
|
|
122
|
+
return { success: false, error: `Item "${name}" not found` };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { success: true, data: item };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Lists all test-quick items with optional filtering
|
|
130
|
+
* @param {Object} [options={}] - Filter options
|
|
131
|
+
* @param {string} [options.status] - Filter by status
|
|
132
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
133
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickListResult>} List result
|
|
134
|
+
*/
|
|
135
|
+
export async function listItems(options = {}, root = process.cwd()) {
|
|
136
|
+
let items = await loadItems(root);
|
|
137
|
+
|
|
138
|
+
if (options.status) {
|
|
139
|
+
items = items.filter(item => item.status === options.status);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { success: true, items, total: items.length };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Updates an existing test-quick item
|
|
147
|
+
* @param {string} name - Item name to update
|
|
148
|
+
* @param {Object} updates - Fields to update
|
|
149
|
+
* @param {string} [updates.description] - New description
|
|
150
|
+
* @param {string} [updates.status] - New status
|
|
151
|
+
* @param {Object} [updates.metadata] - New metadata
|
|
152
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
153
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickResult>} Update result
|
|
154
|
+
*/
|
|
155
|
+
export async function updateItem(name, updates, root = process.cwd()) {
|
|
156
|
+
const items = await loadItems(root);
|
|
157
|
+
const index = items.findIndex(i => i.name === name);
|
|
158
|
+
|
|
159
|
+
if (index === -1) {
|
|
160
|
+
return { success: false, error: `Item "${name}" not found` };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const validStatuses = Object.values(TEST_QUICK_STATUS);
|
|
164
|
+
if (updates.status && !validStatuses.includes(updates.status)) {
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
error: `Invalid status. Valid values: ${validStatuses.join(', ')}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const updatedItem = {
|
|
172
|
+
...items[index],
|
|
173
|
+
...updates,
|
|
174
|
+
updatedAt: new Date().toISOString(),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
items[index] = updatedItem;
|
|
178
|
+
await saveItems(items, root);
|
|
179
|
+
|
|
180
|
+
return { success: true, message: `Item "${name}" updated`, data: updatedItem };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Deletes a test-quick item
|
|
185
|
+
* @param {string} name - Item name to delete
|
|
186
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
187
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickResult>} Delete result
|
|
188
|
+
*/
|
|
189
|
+
export async function deleteItem(name, root = process.cwd()) {
|
|
190
|
+
const items = await loadItems(root);
|
|
191
|
+
const index = items.findIndex(i => i.name === name);
|
|
192
|
+
|
|
193
|
+
if (index === -1) {
|
|
194
|
+
return { success: false, error: `Item "${name}" not found` };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const deletedItem = items[index];
|
|
198
|
+
items.splice(index, 1);
|
|
199
|
+
|
|
200
|
+
await saveItems(items, root);
|
|
201
|
+
|
|
202
|
+
return { success: true, message: `Item "${name}" deleted`, data: deletedItem };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Updates the status of a test-quick item
|
|
207
|
+
* @param {string} name - Item name
|
|
208
|
+
* @param {string} status - New status
|
|
209
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
210
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickResult>} Update result
|
|
211
|
+
*/
|
|
212
|
+
export async function updateStatus(name, status, root = process.cwd()) {
|
|
213
|
+
return updateItem(name, { status }, root);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Clears all completed items
|
|
218
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
219
|
+
* @returns {Promise<{success: boolean, cleared: number}>} Clear result
|
|
220
|
+
*/
|
|
221
|
+
export async function clearCompleted(root = process.cwd()) {
|
|
222
|
+
const items = await loadItems(root);
|
|
223
|
+
const remaining = items.filter(item => item.status !== TEST_QUICK_STATUS.COMPLETED);
|
|
224
|
+
const clearedCount = items.length - remaining.length;
|
|
225
|
+
|
|
226
|
+
await saveItems(remaining, root);
|
|
227
|
+
|
|
228
|
+
return { success: true, cleared: clearedCount };
|
|
229
|
+
}
|
|
@@ -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');
|