@axis-bootstrap/cli 0.1.0
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 +90 -0
- package/package.json +42 -0
- package/src/commands/audit.js +53 -0
- package/src/commands/cleanup.js +42 -0
- package/src/commands/doctor.js +137 -0
- package/src/commands/init.js +297 -0
- package/src/commands/link.js +31 -0
- package/src/commands/spdd.js +139 -0
- package/src/commands/state.js +21 -0
- package/src/index.js +113 -0
- package/src/lib/copy.js +19 -0
- package/src/lib/detect.js +70 -0
- package/src/lib/i18n.js +147 -0
- package/src/lib/paths.js +45 -0
- package/src/lib/ui.js +29 -0
- package/templates/CANVAS.md +48 -0
- package/templates/CONVENTIONS.md +43 -0
- package/templates/INSTRUCTIONS.md +49 -0
- package/templates/STATE.md +27 -0
- package/templates/bootstrap-skill/PLANNER.md +221 -0
- package/templates/bootstrap-skill/PROMPT-TEMPLATE.md +128 -0
- package/templates/bootstrap-skill/SKILL.md +56 -0
- package/templates/bootstrap-skill/references/CANVAS-REASONS.md +111 -0
- package/templates/bootstrap-skill/references/PATTERNS.md +372 -0
- package/templates/bootstrap-skill/references/PHASE-1-DISCOVERY.md +120 -0
- package/templates/bootstrap-skill/references/PHASE-2-SPEC.md +250 -0
- package/templates/bootstrap-skill/references/PHASE-3-HARNESS.md +331 -0
- package/templates/bootstrap-skill/references/PHASE-4-MEMORY.md +187 -0
- package/templates/bootstrap-skill/references/PHASE-5-VALIDATION.md +194 -0
- package/templates/bootstrap-skill/references/QUICKSTART.md +144 -0
- package/templates/bootstrap-skill/references/TEMPLATES.md +602 -0
- package/templates/bootstrap-skill/references/UNIVERSAL-MAP.md +216 -0
- package/templates/settings.json +29 -0
- package/templates/setup-ide-links.sh +33 -0
- package/templates/skills/abstraction-first.md +55 -0
- package/templates/skills/alignment.md +53 -0
- package/templates/skills/iterative-review.md +55 -0
- package/templates/skills/story-decompose.md +54 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { intro, outro, text, log, note, confirm } from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { findTarget, exists, read, write, TEMPLATES, ensureDir } from '../lib/paths.js';
|
|
6
|
+
|
|
7
|
+
const STEPS = {
|
|
8
|
+
story: {
|
|
9
|
+
skill: 'story-decompose',
|
|
10
|
+
fills: 'R (Requirements)',
|
|
11
|
+
desc: 'Break a large requirement into INVEST stories with G/W/T ACs.',
|
|
12
|
+
},
|
|
13
|
+
align: {
|
|
14
|
+
skill: 'alignment',
|
|
15
|
+
fills: 'O scope + N (Norms) + S₂ (Safeguards)',
|
|
16
|
+
desc: 'Lock intent, scope, engineering norms, and non-negotiable invariants.',
|
|
17
|
+
},
|
|
18
|
+
design: {
|
|
19
|
+
skill: 'abstraction-first',
|
|
20
|
+
fills: 'E (Entities) + A (Approach) + S₁ (System structure)',
|
|
21
|
+
desc: 'Design objects, layer boundaries, and variation points before code.',
|
|
22
|
+
},
|
|
23
|
+
review: {
|
|
24
|
+
skill: 'iterative-review',
|
|
25
|
+
fills: 'Canvas ⇄ code drift',
|
|
26
|
+
desc: 'Two-track review: logic correction (spec → code) or refactor sync (code → spec).',
|
|
27
|
+
},
|
|
28
|
+
sync: {
|
|
29
|
+
skill: 'iterative-review',
|
|
30
|
+
fills: 'Canvas ⇄ code (Track B)',
|
|
31
|
+
desc: 'Sync Canvas back to code after a refactor (no behavior change).',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export async function spdd(argv) {
|
|
36
|
+
const sub = argv[0];
|
|
37
|
+
const rest = argv.slice(1);
|
|
38
|
+
|
|
39
|
+
if (!sub || sub === 'help' || sub === '--help') {
|
|
40
|
+
printSpddHelp();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (sub === 'canvas') {
|
|
45
|
+
await scaffoldCanvas(rest);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!STEPS[sub]) {
|
|
50
|
+
log.error(`Unknown SPDD step: ${pc.red(sub)}`);
|
|
51
|
+
printSpddHelp();
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await invokeSkill(sub, rest);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printSpddHelp() {
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(pc.bold('SPDD pipeline') + pc.dim(' — produces a REASONS Canvas, then code'));
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(` ${pc.cyan('axis spdd story')} ${pc.dim('→ R')} Decompose into INVEST stories`);
|
|
63
|
+
console.log(` ${pc.cyan('axis spdd align')} ${pc.dim('→ O+N+S₂')} Lock scope, norms, safeguards`);
|
|
64
|
+
console.log(` ${pc.cyan('axis spdd design')} ${pc.dim('→ E+A+S₁')} Entities, approach, structure`);
|
|
65
|
+
console.log(` ${pc.cyan('axis spdd canvas')} ${pc.dim('<slug>')} Scaffold a new Canvas file`);
|
|
66
|
+
console.log(` ${pc.cyan('axis spdd review')} ${pc.dim('Track A/B')} Iterative review after code gen`);
|
|
67
|
+
console.log(` ${pc.cyan('axis spdd sync')} ${pc.dim('Track B')} Sync Canvas after refactor`);
|
|
68
|
+
console.log();
|
|
69
|
+
console.log(pc.dim('Each step prints the trigger phrase to paste into your AI tool (Claude Code, Cursor, etc.).'));
|
|
70
|
+
console.log();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function scaffoldCanvas(rest) {
|
|
74
|
+
intro(pc.bgBlue(pc.white(' axis spdd canvas ')));
|
|
75
|
+
const target = findTarget();
|
|
76
|
+
let slug = rest[0];
|
|
77
|
+
if (!slug) {
|
|
78
|
+
slug = await text({
|
|
79
|
+
message: 'Canvas slug (kebab-case)?',
|
|
80
|
+
placeholder: 'pricing-quote',
|
|
81
|
+
validate: (v) => (/^[a-z0-9-]+$/.test(v) ? undefined : 'kebab-case only: a-z, 0-9, -'),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const dest = path.join(target, '.ai', 'docs', 'canvases', `${slug}.md`);
|
|
86
|
+
if (exists(dest)) {
|
|
87
|
+
const overwrite = await confirm({ message: `${slug}.md exists. Overwrite?`, initialValue: false });
|
|
88
|
+
if (!overwrite || typeof overwrite === 'symbol') {
|
|
89
|
+
outro(pc.yellow('Aborted.'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ensureDir(path.dirname(dest));
|
|
95
|
+
const tpl = read(path.join(TEMPLATES, 'CANVAS.md')).replace(/{{SLUG}}/g, slug);
|
|
96
|
+
write(dest, tpl);
|
|
97
|
+
|
|
98
|
+
note(
|
|
99
|
+
[
|
|
100
|
+
`${pc.green('✓')} Scaffolded ${pc.cyan(path.relative(target, dest))}`,
|
|
101
|
+
'',
|
|
102
|
+
`${pc.bold('Next steps (in order):')}`,
|
|
103
|
+
` 1. ${pc.cyan('axis spdd story')} — fill R`,
|
|
104
|
+
` 2. ${pc.cyan('axis spdd align')} — fill O + N + S₂`,
|
|
105
|
+
` 3. ${pc.cyan('axis spdd design')} — fill E + A + S₁`,
|
|
106
|
+
` 4. Generate code (your AI tool, using O as prompt)`,
|
|
107
|
+
` 5. ${pc.cyan('axis spdd review')} — verify diff against Canvas`,
|
|
108
|
+
].join('\n'),
|
|
109
|
+
'Canvas created'
|
|
110
|
+
);
|
|
111
|
+
outro(pc.green('done'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function invokeSkill(step, rest) {
|
|
115
|
+
const meta = STEPS[step];
|
|
116
|
+
intro(pc.bgBlue(pc.white(` axis spdd ${step} `)));
|
|
117
|
+
|
|
118
|
+
const target = findTarget();
|
|
119
|
+
const skillPath = path.join(target, '.ai', 'skills', meta.skill, 'SKILL.md');
|
|
120
|
+
const hasSkill = exists(skillPath);
|
|
121
|
+
|
|
122
|
+
note(
|
|
123
|
+
[
|
|
124
|
+
`${pc.bold('Skill:')} ${pc.cyan(meta.skill)} ${hasSkill ? pc.green('(installed)') : pc.yellow('(not installed in this project)')}`,
|
|
125
|
+
`${pc.bold('Fills:')} ${meta.fills}`,
|
|
126
|
+
`${pc.bold('Purpose:')} ${pc.dim(meta.desc)}`,
|
|
127
|
+
].join('\n'),
|
|
128
|
+
`SPDD step: ${step}`
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (!hasSkill) {
|
|
132
|
+
log.warn(`Skill ${meta.skill} not present in .ai/skills/. Add it from the AXIS framework repo or copy SKILL.md.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const trigger = `Load the ${meta.skill} skill and apply it. Update the active Canvas at .ai/docs/canvases/<slug>.md.`;
|
|
136
|
+
|
|
137
|
+
note(pc.cyan(trigger), 'Paste this into your AI tool');
|
|
138
|
+
outro(pc.green('done'));
|
|
139
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { log } from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { findTarget, exists } from '../lib/paths.js';
|
|
6
|
+
|
|
7
|
+
export async function state(argv) {
|
|
8
|
+
const target = path.resolve(argv[0] || findTarget());
|
|
9
|
+
const p = path.join(target, '.ai', 'docs', 'STATE.md');
|
|
10
|
+
|
|
11
|
+
if (!exists(p)) {
|
|
12
|
+
log.error(`Not found: ${pc.red(p)}\nRun \`axis init\` first.`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
17
|
+
log.info(`Opening ${pc.cyan(p)} in ${pc.bold(editor)}`);
|
|
18
|
+
|
|
19
|
+
const child = spawn(editor, [p], { stdio: 'inherit' });
|
|
20
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
21
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { intro, outro, log } from '@clack/prompts';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { init } from './commands/init.js';
|
|
5
|
+
import { doctor } from './commands/doctor.js';
|
|
6
|
+
import { link } from './commands/link.js';
|
|
7
|
+
import { audit } from './commands/audit.js';
|
|
8
|
+
import { state } from './commands/state.js';
|
|
9
|
+
import { spdd } from './commands/spdd.js';
|
|
10
|
+
import { cleanup } from './commands/cleanup.js';
|
|
11
|
+
|
|
12
|
+
const VERSION = '0.1.0';
|
|
13
|
+
|
|
14
|
+
const BANNER = `
|
|
15
|
+
${pc.cyan(' █████╗ ██╗ ██╗██╗███████╗')}
|
|
16
|
+
${pc.cyan(' ██╔══██╗╚██╗██╔╝██║██╔════╝')}
|
|
17
|
+
${pc.cyan(' ███████║ ╚███╔╝ ██║███████╗')}
|
|
18
|
+
${pc.cyan(' ██╔══██║ ██╔██╗ ██║╚════██║')}
|
|
19
|
+
${pc.cyan(' ██║ ██║██╔╝ ██╗██║███████║')}
|
|
20
|
+
${pc.cyan(' ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝')}
|
|
21
|
+
${pc.dim(' Harness · Spec · Memory · v' + VERSION)}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const HELP = `
|
|
25
|
+
${pc.bold('axis')} ${pc.dim('— Harness-first AI project framework')}
|
|
26
|
+
|
|
27
|
+
${pc.bold('Usage:')}
|
|
28
|
+
${pc.cyan('axis')} ${pc.yellow('<command>')} ${pc.dim('[options]')}
|
|
29
|
+
|
|
30
|
+
${pc.bold('Commands:')}
|
|
31
|
+
${pc.yellow('init')} Bootstrap (auto-detects new vs existing project, asks PT/EN)
|
|
32
|
+
${pc.yellow('audit')} Audit existing project for AXIS gaps
|
|
33
|
+
${pc.yellow('doctor')} Validate limits, symlinks, and recursiveness contract
|
|
34
|
+
${pc.yellow('link')} Run setup-ide-links.sh (idempotent symlink installer)
|
|
35
|
+
${pc.yellow('state')} Open .ai/docs/STATE.md in $EDITOR
|
|
36
|
+
${pc.yellow('spdd')} <step> Run SPDD pipeline step (story | align | design | canvas | review | sync)
|
|
37
|
+
${pc.yellow('cleanup')} Remove the axis-bootstrap meta-skill after AI-driven init completes
|
|
38
|
+
${pc.yellow('help')} Show this help
|
|
39
|
+
${pc.yellow('version')} Print version
|
|
40
|
+
|
|
41
|
+
${pc.bold('SPDD pipeline:')}
|
|
42
|
+
${pc.cyan('axis spdd story')} Decompose requirement → INVEST stories ${pc.dim('(fills R)')}
|
|
43
|
+
${pc.cyan('axis spdd align')} Lock O scope, Norms, Safeguards ${pc.dim('(fills O + N + S₂)')}
|
|
44
|
+
${pc.cyan('axis spdd design')} Entities, Approach, System structure ${pc.dim('(fills E + A + S₁)')}
|
|
45
|
+
${pc.cyan('axis spdd canvas')} Scaffold a new REASONS Canvas
|
|
46
|
+
${pc.cyan('axis spdd review')} Iterative review (Track A or B)
|
|
47
|
+
${pc.cyan('axis spdd sync')} Sync Canvas ⇄ code after refactor
|
|
48
|
+
|
|
49
|
+
${pc.bold('Examples:')}
|
|
50
|
+
${pc.dim('$')} axis init
|
|
51
|
+
${pc.dim('$')} axis doctor
|
|
52
|
+
${pc.dim('$')} axis spdd canvas pricing-quote
|
|
53
|
+
${pc.dim('$')} axis audit ./my-project
|
|
54
|
+
|
|
55
|
+
${pc.dim('Docs:')} ${pc.underline('https://github.com/axis-framework')}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const args = process.argv.slice(2);
|
|
59
|
+
const cmd = args[0];
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
63
|
+
console.log(BANNER);
|
|
64
|
+
console.log(HELP);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (cmd === 'version' || cmd === '--version' || cmd === '-v') {
|
|
68
|
+
console.log(VERSION);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(BANNER);
|
|
73
|
+
|
|
74
|
+
const rest = args.slice(1);
|
|
75
|
+
switch (cmd) {
|
|
76
|
+
case 'init':
|
|
77
|
+
intro(pc.bgCyan(pc.black(' axis init ')));
|
|
78
|
+
await init(rest);
|
|
79
|
+
outro(pc.green('done'));
|
|
80
|
+
break;
|
|
81
|
+
case 'audit':
|
|
82
|
+
intro(pc.bgYellow(pc.black(' axis audit ')));
|
|
83
|
+
await audit(rest);
|
|
84
|
+
break;
|
|
85
|
+
case 'doctor':
|
|
86
|
+
intro(pc.bgGreen(pc.black(' axis doctor ')));
|
|
87
|
+
await doctor(rest);
|
|
88
|
+
break;
|
|
89
|
+
case 'link':
|
|
90
|
+
intro(pc.bgMagenta(pc.black(' axis link ')));
|
|
91
|
+
await link(rest);
|
|
92
|
+
break;
|
|
93
|
+
case 'state':
|
|
94
|
+
await state(rest);
|
|
95
|
+
break;
|
|
96
|
+
case 'spdd':
|
|
97
|
+
await spdd(rest);
|
|
98
|
+
break;
|
|
99
|
+
case 'cleanup':
|
|
100
|
+
await cleanup(rest);
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
log.error(`Unknown command: ${pc.red(cmd)}`);
|
|
104
|
+
console.log(HELP);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main().catch((err) => {
|
|
110
|
+
log.error(pc.red(err.message));
|
|
111
|
+
if (process.env.AXIS_DEBUG) console.error(err.stack);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|
package/src/lib/copy.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** Recursively copy a directory. Skips if src missing. */
|
|
5
|
+
export function copyDir(src, dest) {
|
|
6
|
+
if (!fs.existsSync(src)) return;
|
|
7
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
8
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
9
|
+
const s = path.join(src, entry.name);
|
|
10
|
+
const d = path.join(dest, entry.name);
|
|
11
|
+
if (entry.isDirectory()) copyDir(s, d);
|
|
12
|
+
else fs.copyFileSync(s, d);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function rmDir(p) {
|
|
17
|
+
if (!fs.existsSync(p)) return;
|
|
18
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
19
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const IGNORED = new Set(['.git', '.DS_Store', 'node_modules', '.idea', '.vscode', 'dist', 'build', '.next']);
|
|
5
|
+
|
|
6
|
+
const PROJECT_MARKERS = {
|
|
7
|
+
software: [
|
|
8
|
+
'package.json',
|
|
9
|
+
'pyproject.toml',
|
|
10
|
+
'setup.py',
|
|
11
|
+
'requirements.txt',
|
|
12
|
+
'Cargo.toml',
|
|
13
|
+
'go.mod',
|
|
14
|
+
'pom.xml',
|
|
15
|
+
'build.gradle',
|
|
16
|
+
'build.gradle.kts',
|
|
17
|
+
'composer.json',
|
|
18
|
+
'Gemfile',
|
|
19
|
+
'mix.exs',
|
|
20
|
+
'pubspec.yaml',
|
|
21
|
+
'Package.swift',
|
|
22
|
+
'CMakeLists.txt',
|
|
23
|
+
'Makefile',
|
|
24
|
+
'Dockerfile',
|
|
25
|
+
],
|
|
26
|
+
// Non-software heuristics
|
|
27
|
+
content: ['_config.yml', 'mkdocs.yml', 'hugo.toml', 'astro.config.mjs', 'next.config.js'],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} target absolute path
|
|
32
|
+
* @returns {{ state: 'empty'|'existing-software'|'existing-other'|'already-bootstrapped',
|
|
33
|
+
* stackHints: string[], topFiles: string[] }}
|
|
34
|
+
*/
|
|
35
|
+
export function detectProject(target) {
|
|
36
|
+
if (fs.existsSync(path.join(target, '.ai', 'INSTRUCTIONS.md'))) {
|
|
37
|
+
return { state: 'already-bootstrapped', stackHints: [], topFiles: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = fs.readdirSync(target).filter((e) => !IGNORED.has(e));
|
|
43
|
+
} catch {
|
|
44
|
+
return { state: 'empty', stackHints: [], topFiles: [] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (entries.length === 0) {
|
|
48
|
+
return { state: 'empty', stackHints: [], topFiles: [] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const stackHints = [];
|
|
52
|
+
for (const marker of PROJECT_MARKERS.software) {
|
|
53
|
+
if (fs.existsSync(path.join(target, marker))) {
|
|
54
|
+
stackHints.push(marker);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const topFiles = entries.slice(0, 12);
|
|
59
|
+
|
|
60
|
+
if (stackHints.length > 0) {
|
|
61
|
+
return { state: 'existing-software', stackHints, topFiles };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Has files but no software markers — might be content/research/business
|
|
65
|
+
if (entries.length > 0) {
|
|
66
|
+
return { state: 'existing-other', stackHints: [], topFiles };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { state: 'empty', stackHints: [], topFiles: [] };
|
|
70
|
+
}
|
package/src/lib/i18n.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal i18n. Detects locale from env; user can override.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function detectLocale() {
|
|
6
|
+
const lang = (process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || '').toLowerCase();
|
|
7
|
+
if (lang.startsWith('pt')) return 'pt';
|
|
8
|
+
return 'en';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const STRINGS = {
|
|
12
|
+
en: {
|
|
13
|
+
// language picker
|
|
14
|
+
pickLang: 'Language / Idioma?',
|
|
15
|
+
langEN: 'English',
|
|
16
|
+
langPT: 'Português',
|
|
17
|
+
|
|
18
|
+
// detection messages
|
|
19
|
+
detectingProject: 'Inspecting target directory…',
|
|
20
|
+
detectedEmpty: 'Empty directory detected — new project flow.',
|
|
21
|
+
detectedExisting: 'Existing project detected. AXIS will let an AI agent analyze it before generating skills.',
|
|
22
|
+
detectedAlreadyBootstrapped: '.ai/INSTRUCTIONS.md already exists.',
|
|
23
|
+
|
|
24
|
+
// mode picker
|
|
25
|
+
pickMode: 'How do you want to bootstrap?',
|
|
26
|
+
modeQuick: 'Quick scaffold (no AI) — good for new projects, fills templates from your answers',
|
|
27
|
+
modeAI: 'AI-driven bootstrap (recommended for existing projects) — installs the axis-bootstrap skill so an AI agent reads your code and generates customized skills/rules/docs',
|
|
28
|
+
modeAudit: 'Audit only — report what is missing without writing',
|
|
29
|
+
|
|
30
|
+
// project meta
|
|
31
|
+
askName: 'Project name?',
|
|
32
|
+
askPurpose: 'One-sentence purpose of the project?',
|
|
33
|
+
askPurposeHint: 'e.g. "Pricing engine for marketplace plans"',
|
|
34
|
+
askPurposeShort: 'Please write at least one sentence',
|
|
35
|
+
askStack: 'Stack / tools (comma-separated)?',
|
|
36
|
+
askStackHint: 'e.g. "TypeScript, Node 20, PostgreSQL" — leave empty for non-software',
|
|
37
|
+
askIDEs: 'Which IDEs / agents will read this project?',
|
|
38
|
+
askSpdd: 'Which SPDD skills should be installed?',
|
|
39
|
+
askSpddHint: 'These power the per-feature workflow: story → align → design → review',
|
|
40
|
+
|
|
41
|
+
// ai-driven flow
|
|
42
|
+
aiInstalling: 'Installing axis-bootstrap skill bundle…',
|
|
43
|
+
aiNextSteps: 'Next steps',
|
|
44
|
+
aiOpenAgent: '1. Open this project in Claude Code, Cursor, Windsurf, or any IDE with AI',
|
|
45
|
+
aiTrigger: '2. Send this prompt to the agent:',
|
|
46
|
+
aiTriggerText:
|
|
47
|
+
'Load the axis-bootstrap skill (.ai/skills/axis-bootstrap/SKILL.md) and execute it on this project. Read the codebase first, then run all 5 phases with gates. Stop and ask between phases.',
|
|
48
|
+
aiCleanup: '3. After the agent finishes Phase 5, run `axis cleanup` to remove the bootstrap skill (it is no longer needed — your project is now self-sufficient).',
|
|
49
|
+
|
|
50
|
+
// quick flow
|
|
51
|
+
quickScaffolding: 'Scaffolding .ai/ structure',
|
|
52
|
+
quickSpecReady: 'Spec layer ready',
|
|
53
|
+
quickHarnessReady: 'Harness layer ready',
|
|
54
|
+
quickInstallerReady: 'Symlink installer ready',
|
|
55
|
+
quickSymlinksInstalled: 'Symlinks installed',
|
|
56
|
+
quickDoneScaffolding: 'Scaffolding done (symlinks not auto-installed)',
|
|
57
|
+
quickRunLink: 'Run `axis link` manually after reviewing setup-ide-links.sh',
|
|
58
|
+
quickCreated: 'Created',
|
|
59
|
+
quickNext1: 'Open .ai/INSTRUCTIONS.md and refine the architecture table',
|
|
60
|
+
quickNext2: 'Add your first skill: $EDITOR .ai/skills/<name>/SKILL.md',
|
|
61
|
+
quickNext3: 'Run `axis doctor` to verify limits and symlinks',
|
|
62
|
+
quickNext4: 'Per feature: `axis spdd canvas <slug>` → fill REASONS Canvas',
|
|
63
|
+
|
|
64
|
+
// cleanup
|
|
65
|
+
cleanupConfirm:
|
|
66
|
+
'This will remove .ai/skills/axis-bootstrap/ (the meta bootstrap skill). Your generated skills, rules, docs, and STATE.md stay intact. Continue?',
|
|
67
|
+
cleanupRemoved: 'Bootstrap skill removed. Project is fully self-sufficient now.',
|
|
68
|
+
cleanupNotFound: 'No axis-bootstrap skill found in .ai/skills/. Nothing to remove.',
|
|
69
|
+
cleanupAborted: 'Aborted — no changes made.',
|
|
70
|
+
|
|
71
|
+
// common
|
|
72
|
+
abortedAlready: 'Aborted. Run `axis audit` to inspect existing structure instead.',
|
|
73
|
+
overwrite: 'Overwrite?',
|
|
74
|
+
aborted: 'Aborted.',
|
|
75
|
+
done: 'done',
|
|
76
|
+
bootstrapPlan: 'Bootstrap plan',
|
|
77
|
+
target: 'Target',
|
|
78
|
+
},
|
|
79
|
+
pt: {
|
|
80
|
+
pickLang: 'Idioma / Language?',
|
|
81
|
+
langEN: 'English',
|
|
82
|
+
langPT: 'Português',
|
|
83
|
+
|
|
84
|
+
detectingProject: 'Inspecionando o diretório-alvo…',
|
|
85
|
+
detectedEmpty: 'Diretório vazio detectado — fluxo de projeto novo.',
|
|
86
|
+
detectedExisting:
|
|
87
|
+
'Projeto existente detectado. O AXIS vai deixar um agente de IA analisar o código antes de gerar skills.',
|
|
88
|
+
detectedAlreadyBootstrapped: '.ai/INSTRUCTIONS.md já existe.',
|
|
89
|
+
|
|
90
|
+
pickMode: 'Como deseja inicializar?',
|
|
91
|
+
modeQuick:
|
|
92
|
+
'Scaffold rápido (sem IA) — bom para projetos novos; preenche templates com suas respostas',
|
|
93
|
+
modeAI:
|
|
94
|
+
'Bootstrap guiado por IA (recomendado para projetos existentes) — instala a skill axis-bootstrap para que um agente leia seu código e gere skills/rules/docs customizadas',
|
|
95
|
+
modeAudit: 'Apenas auditar — relata o que falta sem escrever',
|
|
96
|
+
|
|
97
|
+
askName: 'Nome do projeto?',
|
|
98
|
+
askPurpose: 'Em uma frase: para que serve o projeto?',
|
|
99
|
+
askPurposeHint: 'ex.: "Motor de preços para planos de marketplace"',
|
|
100
|
+
askPurposeShort: 'Escreva ao menos uma frase',
|
|
101
|
+
askStack: 'Stack / ferramentas (separadas por vírgula)?',
|
|
102
|
+
askStackHint: 'ex.: "TypeScript, Node 20, PostgreSQL" — deixe vazio se não for software',
|
|
103
|
+
askIDEs: 'Quais IDEs / agentes vão ler este projeto?',
|
|
104
|
+
askSpdd: 'Quais skills SPDD instalar?',
|
|
105
|
+
askSpddHint: 'Compõem o pipeline por feature: story → align → design → review',
|
|
106
|
+
|
|
107
|
+
aiInstalling: 'Instalando bundle da skill axis-bootstrap…',
|
|
108
|
+
aiNextSteps: 'Próximos passos',
|
|
109
|
+
aiOpenAgent:
|
|
110
|
+
'1. Abra este projeto no Claude Code, Cursor, Windsurf ou qualquer IDE com IA',
|
|
111
|
+
aiTrigger: '2. Envie este prompt para o agente:',
|
|
112
|
+
aiTriggerText:
|
|
113
|
+
'Carregue a skill axis-bootstrap (.ai/skills/axis-bootstrap/SKILL.md) e execute neste projeto. Leia o código primeiro, depois rode as 5 fases com gates. Pause e pergunte entre cada fase.',
|
|
114
|
+
aiCleanup:
|
|
115
|
+
'3. Quando o agente terminar a Phase 5, rode `axis cleanup` para remover a skill bootstrap (não é mais necessária — seu projeto agora é autossuficiente).',
|
|
116
|
+
|
|
117
|
+
quickScaffolding: 'Criando estrutura .ai/',
|
|
118
|
+
quickSpecReady: 'Spec layer pronta',
|
|
119
|
+
quickHarnessReady: 'Harness layer pronta',
|
|
120
|
+
quickInstallerReady: 'Instalador de symlinks pronto',
|
|
121
|
+
quickSymlinksInstalled: 'Symlinks instalados',
|
|
122
|
+
quickDoneScaffolding: 'Scaffold concluído (symlinks não foram instalados)',
|
|
123
|
+
quickRunLink: 'Rode `axis link` manualmente após revisar setup-ide-links.sh',
|
|
124
|
+
quickCreated: 'Criado',
|
|
125
|
+
quickNext1: 'Abra .ai/INSTRUCTIONS.md e refine a tabela de arquitetura',
|
|
126
|
+
quickNext2: 'Adicione sua primeira skill: $EDITOR .ai/skills/<nome>/SKILL.md',
|
|
127
|
+
quickNext3: 'Rode `axis doctor` para validar limites e symlinks',
|
|
128
|
+
quickNext4: 'Por feature: `axis spdd canvas <slug>` → preencha o Canvas REASONS',
|
|
129
|
+
|
|
130
|
+
cleanupConfirm:
|
|
131
|
+
'Isso remove .ai/skills/axis-bootstrap/ (a meta-skill do bootstrap). Suas skills, rules, docs e STATE.md geradas permanecem. Continuar?',
|
|
132
|
+
cleanupRemoved: 'Skill bootstrap removida. O projeto agora é autossuficiente.',
|
|
133
|
+
cleanupNotFound: 'Nenhuma skill axis-bootstrap encontrada em .ai/skills/. Nada a remover.',
|
|
134
|
+
cleanupAborted: 'Cancelado — nada foi alterado.',
|
|
135
|
+
|
|
136
|
+
abortedAlready: 'Cancelado. Rode `axis audit` para inspecionar a estrutura existente.',
|
|
137
|
+
overwrite: 'Sobrescrever?',
|
|
138
|
+
aborted: 'Cancelado.',
|
|
139
|
+
done: 'concluído',
|
|
140
|
+
bootstrapPlan: 'Plano de bootstrap',
|
|
141
|
+
target: 'Destino',
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export function t(locale, key) {
|
|
146
|
+
return (STRINGS[locale] && STRINGS[locale][key]) || STRINGS.en[key] || key;
|
|
147
|
+
}
|
package/src/lib/paths.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
export const CLI_ROOT = path.resolve(__dirname, '..', '..');
|
|
9
|
+
export const TEMPLATES = path.join(CLI_ROOT, 'templates');
|
|
10
|
+
|
|
11
|
+
/** AXIS framework repo (where this CLI lives — for fallback templates). */
|
|
12
|
+
export const FRAMEWORK_ROOT = path.resolve(CLI_ROOT, '..');
|
|
13
|
+
|
|
14
|
+
/** Look up the target project from CWD upward (where .ai/ exists or will be created). */
|
|
15
|
+
export function findTarget(start = process.cwd()) {
|
|
16
|
+
let dir = path.resolve(start);
|
|
17
|
+
while (true) {
|
|
18
|
+
if (fs.existsSync(path.join(dir, '.ai'))) return dir;
|
|
19
|
+
const parent = path.dirname(dir);
|
|
20
|
+
if (parent === dir) return path.resolve(start); // not found, default to cwd
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ensureDir(p) {
|
|
26
|
+
fs.mkdirSync(p, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function exists(p) {
|
|
30
|
+
return fs.existsSync(p);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function read(p) {
|
|
34
|
+
return fs.readFileSync(p, 'utf8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function write(p, content) {
|
|
38
|
+
ensureDir(path.dirname(p));
|
|
39
|
+
fs.writeFileSync(p, content);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function countLines(p) {
|
|
43
|
+
if (!exists(p)) return 0;
|
|
44
|
+
return read(p).split('\n').length;
|
|
45
|
+
}
|
package/src/lib/ui.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { log } from '@clack/prompts';
|
|
3
|
+
|
|
4
|
+
export const ok = (msg) => log.success(pc.green('✓ ') + msg);
|
|
5
|
+
export const fail = (msg) => log.error(pc.red('✗ ') + msg);
|
|
6
|
+
export const warn = (msg) => log.warn(pc.yellow('! ') + msg);
|
|
7
|
+
export const info = (msg) => log.info(pc.cyan('· ') + msg);
|
|
8
|
+
|
|
9
|
+
export function tag(label, color = 'cyan') {
|
|
10
|
+
return pc[color](`[${label}]`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function box(title, lines) {
|
|
14
|
+
const width = Math.max(title.length, ...lines.map((l) => stripAnsi(l).length)) + 4;
|
|
15
|
+
const top = '╭' + '─'.repeat(width - 2) + '╮';
|
|
16
|
+
const bot = '╰' + '─'.repeat(width - 2) + '╯';
|
|
17
|
+
console.log(pc.dim(top));
|
|
18
|
+
console.log(pc.dim('│ ') + pc.bold(title) + ' '.repeat(width - 4 - title.length) + pc.dim(' │'));
|
|
19
|
+
console.log(pc.dim('│' + ' '.repeat(width - 2) + '│'));
|
|
20
|
+
for (const l of lines) {
|
|
21
|
+
const pad = width - 4 - stripAnsi(l).length;
|
|
22
|
+
console.log(pc.dim('│ ') + l + ' '.repeat(Math.max(0, pad)) + pc.dim(' │'));
|
|
23
|
+
}
|
|
24
|
+
console.log(pc.dim(bot));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stripAnsi(s) {
|
|
28
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Canvas — {{SLUG}}
|
|
2
|
+
|
|
3
|
+
> One-page REASONS spec. If it doesn't fit, run `axis spdd story` to re-decompose.
|
|
4
|
+
|
|
5
|
+
## R — Requirements
|
|
6
|
+
**Story:** As a <role>, I want <capability>, so that <value>.
|
|
7
|
+
**ACs (Given/When/Then):**
|
|
8
|
+
- Given …, When …, Then … (concrete numeric expected value)
|
|
9
|
+
- Given …, When …, Then …
|
|
10
|
+
**Definition of Done:**
|
|
11
|
+
- [ ] All ACs verified with automated tests
|
|
12
|
+
- [ ] …
|
|
13
|
+
|
|
14
|
+
## E — Entities
|
|
15
|
+
- **<EntityA>** — single responsibility: …; relates to <EntityB> via …
|
|
16
|
+
- **<EntityB>** — …
|
|
17
|
+
|
|
18
|
+
## A — Approach (strategy)
|
|
19
|
+
<1-3 sentences on the strategy chosen to satisfy R.>
|
|
20
|
+
|
|
21
|
+
## S₁ — System structure
|
|
22
|
+
**Layers:** Controller → Service → Repository → Domain
|
|
23
|
+
**Components:**
|
|
24
|
+
- `<Service>` (new/modified) — …
|
|
25
|
+
- `<Strategy>` interface — …
|
|
26
|
+
**File tree (closed scope):**
|
|
27
|
+
```text
|
|
28
|
+
src/
|
|
29
|
+
└── …
|
|
30
|
+
tests/
|
|
31
|
+
└── …
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## O — Operations
|
|
35
|
+
- [ ] `<function or endpoint>(input) → output` — references AC #1
|
|
36
|
+
- [ ] …
|
|
37
|
+
|
|
38
|
+
## N — Norms
|
|
39
|
+
- Naming: …
|
|
40
|
+
- Logging: structured JSON with `correlationId` on every Service entry/exit
|
|
41
|
+
- Errors: throw `DomainError` for business rule violations
|
|
42
|
+
- Tests: AAA, no shared mutable fixtures
|
|
43
|
+
|
|
44
|
+
## S₂ — Safeguards (invariants)
|
|
45
|
+
- <Invariant 1>
|
|
46
|
+
- <Invariant 2>
|
|
47
|
+
- No PII in logs
|
|
48
|
+
- DROP/TRUNCATE never executed without explicit confirmation
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Conventions — How {{PROJECT_NAME}} Maintains Its AI Layer
|
|
2
|
+
|
|
3
|
+
## Single Source of Truth
|
|
4
|
+
|
|
5
|
+
All AI content lives in `.ai/`. IDE folders (`.claude/`, `.cursor/`, `.agents/`, `.github/`) contain only symlinks created by `setup-ide-links.sh`.
|
|
6
|
+
|
|
7
|
+
## Progressive Disclosure
|
|
8
|
+
|
|
9
|
+
| Layer | Loads | Limit |
|
|
10
|
+
| ----- | ----- | ----- |
|
|
11
|
+
| Discovery | Always | INSTRUCTIONS ≤ 180 lines |
|
|
12
|
+
| Index | When relevant | SKILL.md ≤ 60 lines |
|
|
13
|
+
| On-demand | When needed | references/*.md |
|
|
14
|
+
|
|
15
|
+
## REASONS Canvas (SPDD pipeline)
|
|
16
|
+
|
|
17
|
+
Every non-trivial feature has a Canvas in `.ai/docs/canvases/<slug>.md`. Filled by:
|
|
18
|
+
1. `story-decompose` → R (Requirements)
|
|
19
|
+
2. `alignment` → O scope + N (Norms) + S₂ (Safeguards)
|
|
20
|
+
3. `abstraction-first` → E (Entities) + A (Approach) + S₁ (System structure)
|
|
21
|
+
4. (code generation)
|
|
22
|
+
5. `iterative-review` → keeps Canvas ⇄ code in sync
|
|
23
|
+
|
|
24
|
+
## Bidirectional Spec-Code Sync
|
|
25
|
+
|
|
26
|
+
| Change | Direction |
|
|
27
|
+
| ------ | --------- |
|
|
28
|
+
| New requirement / bug fix | spec → code (update Canvas first) |
|
|
29
|
+
| Refactor (no behavior change) | code → spec (sync Canvas after) |
|
|
30
|
+
|
|
31
|
+
## Knowledge Verification Chain
|
|
32
|
+
|
|
33
|
+
Before asserting anything: codebase → project docs → official docs → web → mark uncertain. Never fabricate.
|
|
34
|
+
|
|
35
|
+
## Adding a New IDE
|
|
36
|
+
|
|
37
|
+
Add 3-4 `ln -s` lines to `setup-ide-links.sh`. Re-run script (idempotent).
|
|
38
|
+
|
|
39
|
+
## Session Closing Protocol
|
|
40
|
+
|
|
41
|
+
1. Update `docs/STATE.md` (curate, don't append)
|
|
42
|
+
2. Identify skills/docs affected by behavioral changes
|
|
43
|
+
3. Update them in the same session
|