@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamptee/aia-code",
3
- "version": "0.10.0",
3
+ "version": "1.0.1",
4
4
  "description": "AI Architecture Assistant - orchestrate AI-assisted development workflows via CLI tools (Claude, Codex, Gemini)",
5
5
  "type": "module",
6
6
  "license": "MIT",
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
  }
@@ -1,58 +1,20 @@
1
1
  import chalk from 'chalk';
2
- import fs from 'fs-extra';
3
- import path from 'node:path';
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('Quick story/ticket: create feature then run dev-plan implement → review')
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
- validateFeatureName(name);
33
-
34
- const root = process.cwd();
35
- const featureDir = path.join(root, AIA_DIR, 'features', name);
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
 
@@ -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
  }
@@ -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
+ }
@@ -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
+ }