@bamptee/aia-code 0.10.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/commands/quick.js +8 -46
- package/src/commands/ui.js +19 -0
- package/src/providers/anthropic.js +2 -2
- package/src/providers/cli-runner.js +3 -1
- package/src/providers/gemini.js +2 -2
- package/src/providers/openai.js +2 -2
- package/src/services/model-call.js +2 -2
- package/src/services/quick.js +40 -0
- package/src/services/runner.js +2 -2
- package/src/ui/api/config.js +81 -0
- package/src/ui/api/constants.js +8 -0
- package/src/ui/api/features.js +169 -0
- package/src/ui/api/index.js +11 -0
- package/src/ui/api/logs.js +15 -0
- package/src/ui/public/components/config-view.js +160 -0
- package/src/ui/public/components/dashboard.js +201 -0
- package/src/ui/public/components/feature-detail.js +278 -0
- package/src/ui/public/index.html +44 -0
- package/src/ui/public/main.js +119 -0
- package/src/ui/router.js +47 -0
- package/src/ui/server.js +105 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { registerStatusCommand } from './commands/status.js';
|
|
|
7
7
|
import { registerResetCommand } from './commands/reset.js';
|
|
8
8
|
import { registerNextCommand } from './commands/next.js';
|
|
9
9
|
import { registerQuickCommand } from './commands/quick.js';
|
|
10
|
+
import { registerUiCommand } from './commands/ui.js';
|
|
10
11
|
|
|
11
12
|
export function createCli() {
|
|
12
13
|
const program = new Command();
|
|
@@ -24,6 +25,7 @@ export function createCli() {
|
|
|
24
25
|
registerRepoCommand(program);
|
|
25
26
|
registerStatusCommand(program);
|
|
26
27
|
registerResetCommand(program);
|
|
28
|
+
registerUiCommand(program);
|
|
27
29
|
|
|
28
30
|
return program;
|
|
29
31
|
}
|
package/src/commands/quick.js
CHANGED
|
@@ -1,58 +1,20 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import yaml from 'yaml';
|
|
5
|
-
import { AIA_DIR, FEATURE_STEPS, QUICK_STEPS, STEP_STATUS } from '../constants.js';
|
|
6
|
-
import { createFeature, validateFeatureName } from '../services/feature.js';
|
|
7
|
-
import { runStep } from '../services/runner.js';
|
|
8
|
-
|
|
9
|
-
async function skipEarlySteps(feature, root) {
|
|
10
|
-
const statusFile = path.join(root, AIA_DIR, 'features', feature, 'status.yaml');
|
|
11
|
-
const raw = await fs.readFile(statusFile, 'utf-8');
|
|
12
|
-
const status = yaml.parse(raw);
|
|
13
|
-
|
|
14
|
-
for (const step of FEATURE_STEPS) {
|
|
15
|
-
if (!QUICK_STEPS.includes(step)) {
|
|
16
|
-
status.steps[step] = STEP_STATUS.DONE;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
status.current_step = QUICK_STEPS[0];
|
|
20
|
-
|
|
21
|
-
await fs.writeFile(statusFile, yaml.stringify(status), 'utf-8');
|
|
22
|
-
}
|
|
2
|
+
import { runQuick } from '../services/quick.js';
|
|
3
|
+
import { QUICK_STEPS } from '../constants.js';
|
|
23
4
|
|
|
24
5
|
export function registerQuickCommand(program) {
|
|
25
6
|
program
|
|
26
7
|
.command('quick <name> [description]')
|
|
27
|
-
.description(
|
|
8
|
+
.description(`Quick story/ticket: create feature then run ${QUICK_STEPS.join(' \u2192 ')}`)
|
|
28
9
|
.option('-v, --verbose', 'Show AI thinking/tool usage')
|
|
29
10
|
.option('-a, --apply', 'Force agent mode (file editing) for all steps')
|
|
30
11
|
.action(async (name, description, opts) => {
|
|
31
12
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!(await fs.pathExists(featureDir))) {
|
|
38
|
-
await createFeature(name, root);
|
|
39
|
-
console.log(chalk.green(`Feature "${name}" created.`));
|
|
40
|
-
const initPath = path.join(AIA_DIR, 'features', name, 'init.md');
|
|
41
|
-
console.log(chalk.gray(`Using ${initPath} as input.`));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
await skipEarlySteps(name, root);
|
|
45
|
-
|
|
46
|
-
for (const step of QUICK_STEPS) {
|
|
47
|
-
console.log(chalk.cyan(`\n--- Running ${step} ---\n`));
|
|
48
|
-
await runStep(step, name, {
|
|
49
|
-
description,
|
|
50
|
-
verbose: opts.verbose,
|
|
51
|
-
apply: opts.apply,
|
|
52
|
-
root,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
13
|
+
await runQuick(name, {
|
|
14
|
+
description,
|
|
15
|
+
verbose: opts.verbose,
|
|
16
|
+
apply: opts.apply,
|
|
17
|
+
});
|
|
56
18
|
console.log(chalk.green(`\nQuick pipeline completed for "${name}".`));
|
|
57
19
|
} catch (err) {
|
|
58
20
|
console.error(chalk.red(err.message));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { startServer } from '../ui/server.js';
|
|
3
|
+
|
|
4
|
+
function openBrowser(url) {
|
|
5
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
6
|
+
try { execSync(`${cmd} ${url}`, { stdio: 'ignore' }); } catch {}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function registerUiCommand(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('ui')
|
|
12
|
+
.description('Launch the local web UI to manage features and config')
|
|
13
|
+
.option('-p, --port <port>', 'Port to listen on (scans from 3100 if not specified)')
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
const port = opts.port ? parseInt(opts.port, 10) : undefined;
|
|
16
|
+
const { port: actualPort } = await startServer(port);
|
|
17
|
+
openBrowser(`http://127.0.0.1:${actualPort}`);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runCli } from './cli-runner.js';
|
|
2
2
|
|
|
3
|
-
export async function generate(prompt, model, { verbose = false, apply = false } = {}) {
|
|
3
|
+
export async function generate(prompt, model, { verbose = false, apply = false, onData } = {}) {
|
|
4
4
|
const args = ['-p'];
|
|
5
5
|
if (model) {
|
|
6
6
|
args.push('--model', model);
|
|
@@ -13,5 +13,5 @@ export async function generate(prompt, model, { verbose = false, apply = false }
|
|
|
13
13
|
}
|
|
14
14
|
args.push('-');
|
|
15
15
|
|
|
16
|
-
return runCli('claude', args, { stdin: prompt, verbose: verbose || apply, apply });
|
|
16
|
+
return runCli('claude', args, { stdin: prompt, verbose: verbose || apply, apply, onData });
|
|
17
17
|
}
|
|
@@ -4,7 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
const DEFAULT_IDLE_TIMEOUT_MS = 180_000;
|
|
5
5
|
const AGENT_IDLE_TIMEOUT_MS = 600_000;
|
|
6
6
|
|
|
7
|
-
export function runCli(command, args, { stdin: stdinData, verbose = false, apply = false, idleTimeoutMs } = {}) {
|
|
7
|
+
export function runCli(command, args, { stdin: stdinData, verbose = false, apply = false, idleTimeoutMs, onData } = {}) {
|
|
8
8
|
if (!idleTimeoutMs) {
|
|
9
9
|
idleTimeoutMs = apply ? AGENT_IDLE_TIMEOUT_MS : DEFAULT_IDLE_TIMEOUT_MS;
|
|
10
10
|
}
|
|
@@ -41,6 +41,7 @@ export function runCli(command, args, { stdin: stdinData, verbose = false, apply
|
|
|
41
41
|
const text = data.toString();
|
|
42
42
|
process.stdout.write(text);
|
|
43
43
|
chunks.push(text);
|
|
44
|
+
if (onData) onData({ type: 'stdout', text });
|
|
44
45
|
resetTimer();
|
|
45
46
|
});
|
|
46
47
|
|
|
@@ -50,6 +51,7 @@ export function runCli(command, args, { stdin: stdinData, verbose = false, apply
|
|
|
50
51
|
if (verbose) {
|
|
51
52
|
process.stderr.write(chalk.gray(text));
|
|
52
53
|
}
|
|
54
|
+
if (onData) onData({ type: 'stderr', text });
|
|
53
55
|
resetTimer();
|
|
54
56
|
});
|
|
55
57
|
|
package/src/providers/gemini.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runCli } from './cli-runner.js';
|
|
2
2
|
|
|
3
|
-
export async function generate(prompt, model, { verbose = false, apply = false } = {}) {
|
|
3
|
+
export async function generate(prompt, model, { verbose = false, apply = false, onData } = {}) {
|
|
4
4
|
const args = [];
|
|
5
5
|
if (model) {
|
|
6
6
|
args.push('-m', model);
|
|
@@ -10,5 +10,5 @@ export async function generate(prompt, model, { verbose = false, apply = false }
|
|
|
10
10
|
}
|
|
11
11
|
args.push('-');
|
|
12
12
|
|
|
13
|
-
return runCli('gemini', args, { stdin: prompt, verbose: verbose || apply, apply });
|
|
13
|
+
return runCli('gemini', args, { stdin: prompt, verbose: verbose || apply, apply, onData });
|
|
14
14
|
}
|
package/src/providers/openai.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runCli } from './cli-runner.js';
|
|
2
2
|
|
|
3
|
-
export async function generate(prompt, model, { verbose = false, apply = false } = {}) {
|
|
3
|
+
export async function generate(prompt, model, { verbose = false, apply = false, onData } = {}) {
|
|
4
4
|
const args = ['exec'];
|
|
5
5
|
if (model) {
|
|
6
6
|
args.push('-c', `model="${model}"`);
|
|
@@ -10,5 +10,5 @@ export async function generate(prompt, model, { verbose = false, apply = false }
|
|
|
10
10
|
}
|
|
11
11
|
args.push('-');
|
|
12
12
|
|
|
13
|
-
return runCli('codex', args, { stdin: prompt, verbose: verbose || apply, apply });
|
|
13
|
+
return runCli('codex', args, { stdin: prompt, verbose: verbose || apply, apply, onData });
|
|
14
14
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { resolveModelAlias } from '../providers/registry.js';
|
|
3
3
|
|
|
4
|
-
export async function callModel(model, prompt, { verbose = false, apply = false } = {}) {
|
|
4
|
+
export async function callModel(model, prompt, { verbose = false, apply = false, onData } = {}) {
|
|
5
5
|
const resolved = resolveModelAlias(model);
|
|
6
6
|
const displayName = resolved.model ?? `${model} (CLI default)`;
|
|
7
7
|
const mode = apply ? 'agent' : 'print';
|
|
8
8
|
|
|
9
9
|
console.log(chalk.yellow(`[AI] Calling ${displayName} (${mode} mode)...`));
|
|
10
10
|
|
|
11
|
-
return resolved.provider.generate(prompt, resolved.model, { verbose, apply });
|
|
11
|
+
return resolved.provider.generate(prompt, resolved.model, { verbose, apply, onData });
|
|
12
12
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
import { AIA_DIR, FEATURE_STEPS, QUICK_STEPS, STEP_STATUS } from '../constants.js';
|
|
5
|
+
import { createFeature, validateFeatureName } from './feature.js';
|
|
6
|
+
import { runStep } from './runner.js';
|
|
7
|
+
|
|
8
|
+
export async function skipEarlySteps(feature, root) {
|
|
9
|
+
const statusFile = path.join(root, AIA_DIR, 'features', feature, 'status.yaml');
|
|
10
|
+
const raw = await fs.readFile(statusFile, 'utf-8');
|
|
11
|
+
const status = yaml.parse(raw);
|
|
12
|
+
|
|
13
|
+
for (const step of FEATURE_STEPS) {
|
|
14
|
+
if (!QUICK_STEPS.includes(step)) {
|
|
15
|
+
status.steps[step] = STEP_STATUS.DONE;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
status.current_step = QUICK_STEPS[0];
|
|
19
|
+
|
|
20
|
+
await fs.writeFile(statusFile, yaml.stringify(status), 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runQuick(name, { description, verbose = false, apply = false, root = process.cwd(), onData } = {}) {
|
|
24
|
+
validateFeatureName(name);
|
|
25
|
+
|
|
26
|
+
const featureDir = path.join(root, AIA_DIR, 'features', name);
|
|
27
|
+
const created = !(await fs.pathExists(featureDir));
|
|
28
|
+
|
|
29
|
+
if (created) {
|
|
30
|
+
await createFeature(name, root);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await skipEarlySteps(name, root);
|
|
34
|
+
|
|
35
|
+
for (const step of QUICK_STEPS) {
|
|
36
|
+
await runStep(step, name, { description, verbose, apply, root, onData });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { created };
|
|
40
|
+
}
|
package/src/services/runner.js
CHANGED
|
@@ -8,7 +8,7 @@ import { callModel } from './model-call.js';
|
|
|
8
8
|
import { loadStatus, updateStepStatus } from './status.js';
|
|
9
9
|
import { logExecution } from '../logger.js';
|
|
10
10
|
|
|
11
|
-
export async function runStep(step, feature, { description, verbose = false, apply = false, root = process.cwd() } = {}) {
|
|
11
|
+
export async function runStep(step, feature, { description, verbose = false, apply = false, root = process.cwd(), onData } = {}) {
|
|
12
12
|
if (!FEATURE_STEPS.includes(step)) {
|
|
13
13
|
throw new Error(`Unknown step "${step}". Valid steps: ${FEATURE_STEPS.join(', ')}`);
|
|
14
14
|
}
|
|
@@ -30,7 +30,7 @@ export async function runStep(step, feature, { description, verbose = false, app
|
|
|
30
30
|
const prompt = await buildPrompt(feature, step, { description, root });
|
|
31
31
|
|
|
32
32
|
const start = performance.now();
|
|
33
|
-
const output = await callModel(model, prompt, { verbose, apply: shouldApply });
|
|
33
|
+
const output = await callModel(model, prompt, { verbose, apply: shouldApply, onData });
|
|
34
34
|
const duration = performance.now() - start;
|
|
35
35
|
|
|
36
36
|
const outputPath = path.join(root, AIA_DIR, 'features', feature, `${step}.md`);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
import { AIA_DIR } from '../../constants.js';
|
|
5
|
+
import { json, error } from '../router.js';
|
|
6
|
+
|
|
7
|
+
export function registerConfigRoutes(router) {
|
|
8
|
+
// Get config
|
|
9
|
+
router.get('/api/config', async (req, res, { root }) => {
|
|
10
|
+
const configPath = path.join(root, AIA_DIR, 'config.yaml');
|
|
11
|
+
if (!(await fs.pathExists(configPath))) {
|
|
12
|
+
return error(res, 'config.yaml not found', 404);
|
|
13
|
+
}
|
|
14
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
15
|
+
json(res, { content, parsed: yaml.parse(content) });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Save config
|
|
19
|
+
router.put('/api/config', async (req, res, { root, parseBody }) => {
|
|
20
|
+
const body = await parseBody();
|
|
21
|
+
const configPath = path.join(root, AIA_DIR, 'config.yaml');
|
|
22
|
+
await fs.writeFile(configPath, body.content, 'utf-8');
|
|
23
|
+
json(res, { ok: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// List context files
|
|
27
|
+
router.get('/api/context', async (req, res, { root }) => {
|
|
28
|
+
const dir = path.join(root, AIA_DIR, 'context');
|
|
29
|
+
if (!(await fs.pathExists(dir))) return json(res, []);
|
|
30
|
+
const files = await fs.readdir(dir);
|
|
31
|
+
json(res, files.filter(f => f.endsWith('.md')));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Read context file
|
|
35
|
+
router.get('/api/context/:filename', async (req, res, { params, root }) => {
|
|
36
|
+
const filePath = path.join(root, AIA_DIR, 'context', params.filename);
|
|
37
|
+
if (!(await fs.pathExists(filePath))) return error(res, 'Not found', 404);
|
|
38
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
39
|
+
json(res, { filename: params.filename, content });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Save context file
|
|
43
|
+
router.put('/api/context/:filename', async (req, res, { params, root, parseBody }) => {
|
|
44
|
+
const body = await parseBody();
|
|
45
|
+
const filePath = path.join(root, AIA_DIR, 'context', params.filename);
|
|
46
|
+
await fs.writeFile(filePath, body.content, 'utf-8');
|
|
47
|
+
json(res, { ok: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// List knowledge categories
|
|
51
|
+
router.get('/api/knowledge', async (req, res, { root }) => {
|
|
52
|
+
const dir = path.join(root, AIA_DIR, 'knowledge');
|
|
53
|
+
if (!(await fs.pathExists(dir))) return json(res, []);
|
|
54
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
55
|
+
const categories = [];
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
const files = await fs.readdir(path.join(dir, entry.name));
|
|
59
|
+
categories.push({ name: entry.name, files: files.filter(f => f.endsWith('.md')) });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
json(res, categories);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Read knowledge file
|
|
66
|
+
router.get('/api/knowledge/:category/:filename', async (req, res, { params, root }) => {
|
|
67
|
+
const filePath = path.join(root, AIA_DIR, 'knowledge', params.category, params.filename);
|
|
68
|
+
if (!(await fs.pathExists(filePath))) return error(res, 'Not found', 404);
|
|
69
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
70
|
+
json(res, { filename: params.filename, category: params.category, content });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Save knowledge file
|
|
74
|
+
router.put('/api/knowledge/:category/:filename', async (req, res, { params, root, parseBody }) => {
|
|
75
|
+
const body = await parseBody();
|
|
76
|
+
const filePath = path.join(root, AIA_DIR, 'knowledge', params.category, params.filename);
|
|
77
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
78
|
+
await fs.writeFile(filePath, body.content, 'utf-8');
|
|
79
|
+
json(res, { ok: true });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FEATURE_STEPS, STEP_STATUS, QUICK_STEPS } from '../../constants.js';
|
|
2
|
+
import { json } from '../router.js';
|
|
3
|
+
|
|
4
|
+
export function registerConstantsRoutes(router) {
|
|
5
|
+
router.get('/api/constants', (req, res) => {
|
|
6
|
+
json(res, { FEATURE_STEPS, STEP_STATUS, QUICK_STEPS });
|
|
7
|
+
});
|
|
8
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { AIA_DIR } from '../../constants.js';
|
|
4
|
+
import { loadStatus, updateStepStatus, resetStep } from '../../services/status.js';
|
|
5
|
+
import { createFeature, validateFeatureName } from '../../services/feature.js';
|
|
6
|
+
import { runStep } from '../../services/runner.js';
|
|
7
|
+
import { runQuick } from '../../services/quick.js';
|
|
8
|
+
import { json, error } from '../router.js';
|
|
9
|
+
|
|
10
|
+
function sseHeaders(res) {
|
|
11
|
+
res.writeHead(200, {
|
|
12
|
+
'Content-Type': 'text/event-stream',
|
|
13
|
+
'Cache-Control': 'no-cache',
|
|
14
|
+
'Connection': 'keep-alive',
|
|
15
|
+
'Access-Control-Allow-Origin': '*',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sseSend(res, event, data) {
|
|
20
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerFeatureRoutes(router) {
|
|
24
|
+
// List all features
|
|
25
|
+
router.get('/api/features', async (req, res, { root }) => {
|
|
26
|
+
const featuresDir = path.join(root, AIA_DIR, 'features');
|
|
27
|
+
if (!(await fs.pathExists(featuresDir))) {
|
|
28
|
+
return json(res, []);
|
|
29
|
+
}
|
|
30
|
+
const entries = await fs.readdir(featuresDir, { withFileTypes: true });
|
|
31
|
+
const features = [];
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
try {
|
|
35
|
+
const status = await loadStatus(entry.name, root);
|
|
36
|
+
features.push({ name: entry.name, ...status });
|
|
37
|
+
} catch {
|
|
38
|
+
features.push({ name: entry.name, error: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
json(res, features);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Get a single feature
|
|
46
|
+
router.get('/api/features/:name', async (req, res, { params, root }) => {
|
|
47
|
+
try {
|
|
48
|
+
const status = await loadStatus(params.name, root);
|
|
49
|
+
const featureDir = path.join(root, AIA_DIR, 'features', params.name);
|
|
50
|
+
const files = await fs.readdir(featureDir);
|
|
51
|
+
json(res, { name: params.name, ...status, files });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
error(res, err.message, 404);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Read a feature file
|
|
58
|
+
router.get('/api/features/:name/files/:filename', async (req, res, { params, root }) => {
|
|
59
|
+
const filePath = path.join(root, AIA_DIR, 'features', params.name, params.filename);
|
|
60
|
+
if (!(await fs.pathExists(filePath))) {
|
|
61
|
+
return error(res, 'File not found', 404);
|
|
62
|
+
}
|
|
63
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
64
|
+
json(res, { filename: params.filename, content });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Save a feature file
|
|
68
|
+
router.put('/api/features/:name/files/:filename', async (req, res, { params, root, parseBody }) => {
|
|
69
|
+
const body = await parseBody();
|
|
70
|
+
const filePath = path.join(root, AIA_DIR, 'features', params.name, params.filename);
|
|
71
|
+
await fs.writeFile(filePath, body.content, 'utf-8');
|
|
72
|
+
json(res, { ok: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Create a new feature
|
|
76
|
+
router.post('/api/features', async (req, res, { root, parseBody }) => {
|
|
77
|
+
const body = await parseBody();
|
|
78
|
+
try {
|
|
79
|
+
validateFeatureName(body.name);
|
|
80
|
+
await createFeature(body.name, root);
|
|
81
|
+
json(res, { ok: true, name: body.name }, 201);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
error(res, err.message, 400);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Run a step with SSE streaming
|
|
88
|
+
router.post('/api/features/:name/run/:step', async (req, res, { params, root, parseBody }) => {
|
|
89
|
+
const body = await parseBody();
|
|
90
|
+
sseHeaders(res);
|
|
91
|
+
sseSend(res, 'status', { step: params.step, status: 'started' });
|
|
92
|
+
|
|
93
|
+
const onData = ({ type, text }) => {
|
|
94
|
+
try { sseSend(res, 'log', { type, text }); } catch {}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const output = await runStep(params.step, params.name, {
|
|
99
|
+
description: body.description,
|
|
100
|
+
verbose: true,
|
|
101
|
+
apply: body.apply || false,
|
|
102
|
+
root,
|
|
103
|
+
onData,
|
|
104
|
+
});
|
|
105
|
+
sseSend(res, 'done', { step: params.step, output: output.slice(0, 500) });
|
|
106
|
+
} catch (err) {
|
|
107
|
+
sseSend(res, 'error', { message: err.message });
|
|
108
|
+
}
|
|
109
|
+
res.end();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Quick ticket with SSE streaming (dev-plan -> implement -> review)
|
|
113
|
+
router.post('/api/features/:name/quick', async (req, res, { params, root, parseBody }) => {
|
|
114
|
+
const body = await parseBody();
|
|
115
|
+
sseHeaders(res);
|
|
116
|
+
sseSend(res, 'status', { status: 'started', mode: 'quick' });
|
|
117
|
+
|
|
118
|
+
const onData = ({ type, text }) => {
|
|
119
|
+
try { sseSend(res, 'log', { type, text }); } catch {}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await runQuick(params.name, {
|
|
124
|
+
description: body.description,
|
|
125
|
+
apply: body.apply || false,
|
|
126
|
+
root,
|
|
127
|
+
onData,
|
|
128
|
+
});
|
|
129
|
+
sseSend(res, 'done', { status: 'completed' });
|
|
130
|
+
} catch (err) {
|
|
131
|
+
sseSend(res, 'error', { message: err.message });
|
|
132
|
+
}
|
|
133
|
+
res.end();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Quick ticket with SSE streaming (create + run)
|
|
137
|
+
router.post('/api/quick', async (req, res, { root, parseBody }) => {
|
|
138
|
+
const body = await parseBody();
|
|
139
|
+
sseHeaders(res);
|
|
140
|
+
sseSend(res, 'status', { status: 'started', mode: 'quick', name: body.name });
|
|
141
|
+
|
|
142
|
+
const onData = ({ type, text }) => {
|
|
143
|
+
try { sseSend(res, 'log', { type, text }); } catch {}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await runQuick(body.name, {
|
|
148
|
+
description: body.description,
|
|
149
|
+
apply: body.apply || false,
|
|
150
|
+
root,
|
|
151
|
+
onData,
|
|
152
|
+
});
|
|
153
|
+
sseSend(res, 'done', { status: 'completed', name: body.name });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
sseSend(res, 'error', { message: err.message });
|
|
156
|
+
}
|
|
157
|
+
res.end();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Reset a step
|
|
161
|
+
router.post('/api/features/:name/reset/:step', async (req, res, { params, root }) => {
|
|
162
|
+
try {
|
|
163
|
+
await resetStep(params.name, params.step, root);
|
|
164
|
+
json(res, { ok: true });
|
|
165
|
+
} catch (err) {
|
|
166
|
+
error(res, err.message, 400);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { registerFeatureRoutes } from './features.js';
|
|
2
|
+
import { registerConfigRoutes } from './config.js';
|
|
3
|
+
import { registerLogRoutes } from './logs.js';
|
|
4
|
+
import { registerConstantsRoutes } from './constants.js';
|
|
5
|
+
|
|
6
|
+
export function registerApiRoutes(router, root) {
|
|
7
|
+
registerFeatureRoutes(router);
|
|
8
|
+
registerConfigRoutes(router);
|
|
9
|
+
registerLogRoutes(router);
|
|
10
|
+
registerConstantsRoutes(router);
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { AIA_DIR } from '../../constants.js';
|
|
4
|
+
import { json } from '../router.js';
|
|
5
|
+
|
|
6
|
+
export function registerLogRoutes(router) {
|
|
7
|
+
router.get('/api/logs', async (req, res, { root }) => {
|
|
8
|
+
const logPath = path.join(root, AIA_DIR, 'logs', 'execution.log');
|
|
9
|
+
if (!(await fs.pathExists(logPath))) {
|
|
10
|
+
return json(res, { content: '' });
|
|
11
|
+
}
|
|
12
|
+
const content = await fs.readFile(logPath, 'utf-8');
|
|
13
|
+
json(res, { content });
|
|
14
|
+
});
|
|
15
|
+
}
|