@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.
@@ -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
- * Start docker-compose services in a worktree
150
- * @param {string} wtPath - Worktree directory path
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 stopServices(wtPath) {
164
- const composePath = path.join(wtPath, 'docker-compose.wt.yml');
165
- if (fs.existsSync(composePath)) {
166
- execSync('docker-compose -f docker-compose.wt.yml down', { cwd: wtPath, stdio: 'inherit' });
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
- * Get docker services status
173
+ * List services defined in docker-compose.wt.yml
183
174
  * @param {string} wtPath - Worktree directory path
184
- * @returns {Array} List of service objects with name and status
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
- return JSON.parse(output);
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
- return [];
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
+ }
@@ -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');