@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.
Files changed (38) hide show
  1. package/README.md +90 -0
  2. package/package.json +42 -0
  3. package/src/commands/audit.js +53 -0
  4. package/src/commands/cleanup.js +42 -0
  5. package/src/commands/doctor.js +137 -0
  6. package/src/commands/init.js +297 -0
  7. package/src/commands/link.js +31 -0
  8. package/src/commands/spdd.js +139 -0
  9. package/src/commands/state.js +21 -0
  10. package/src/index.js +113 -0
  11. package/src/lib/copy.js +19 -0
  12. package/src/lib/detect.js +70 -0
  13. package/src/lib/i18n.js +147 -0
  14. package/src/lib/paths.js +45 -0
  15. package/src/lib/ui.js +29 -0
  16. package/templates/CANVAS.md +48 -0
  17. package/templates/CONVENTIONS.md +43 -0
  18. package/templates/INSTRUCTIONS.md +49 -0
  19. package/templates/STATE.md +27 -0
  20. package/templates/bootstrap-skill/PLANNER.md +221 -0
  21. package/templates/bootstrap-skill/PROMPT-TEMPLATE.md +128 -0
  22. package/templates/bootstrap-skill/SKILL.md +56 -0
  23. package/templates/bootstrap-skill/references/CANVAS-REASONS.md +111 -0
  24. package/templates/bootstrap-skill/references/PATTERNS.md +372 -0
  25. package/templates/bootstrap-skill/references/PHASE-1-DISCOVERY.md +120 -0
  26. package/templates/bootstrap-skill/references/PHASE-2-SPEC.md +250 -0
  27. package/templates/bootstrap-skill/references/PHASE-3-HARNESS.md +331 -0
  28. package/templates/bootstrap-skill/references/PHASE-4-MEMORY.md +187 -0
  29. package/templates/bootstrap-skill/references/PHASE-5-VALIDATION.md +194 -0
  30. package/templates/bootstrap-skill/references/QUICKSTART.md +144 -0
  31. package/templates/bootstrap-skill/references/TEMPLATES.md +602 -0
  32. package/templates/bootstrap-skill/references/UNIVERSAL-MAP.md +216 -0
  33. package/templates/settings.json +29 -0
  34. package/templates/setup-ide-links.sh +33 -0
  35. package/templates/skills/abstraction-first.md +55 -0
  36. package/templates/skills/alignment.md +53 -0
  37. package/templates/skills/iterative-review.md +55 -0
  38. 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
+ });
@@ -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
+ }
@@ -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
+ }
@@ -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