@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
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @axis-bootstrap/cli
2
+
3
+ > CLI for the [AXIS framework](../FRAMEWORK.md) — bootstrap AI-augmented projects with Spec + Harness + Memory.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # one-shot, no install
9
+ npx @axis-bootstrap/cli init
10
+
11
+ # or install globally
12
+ npm i -g @axis-bootstrap/cli
13
+ axis init
14
+ ```
15
+
16
+ > Binary command stays short: `axis`. Package name is scoped: `@axis-bootstrap/cli`.
17
+
18
+ ## How it works
19
+
20
+ `axis init` **auto-detects** your context and asks in **PT or EN** based on `$LANG`:
21
+
22
+ | Detected | Default mode | What happens |
23
+ | -------- | ------------ | ------------ |
24
+ | Empty directory | **Quick scaffold** | Interactive prompts → fills templates with your answers (no AI needed) |
25
+ | Existing project (has `package.json`, etc.) | **AI-driven** | Installs the `axis-bootstrap` skill bundle. You then ask your AI tool (Claude Code / Cursor / Copilot) to "execute axis-bootstrap" — the agent reads your code, runs 5 phases with gates, and generates customized `.ai/` skills/rules/docs |
26
+ | Already has `.ai/` | Asks before overwriting | — |
27
+
28
+ You can override: pick **Quick**, **AI-driven**, or **Audit-only** at the prompt.
29
+
30
+ ## After AI-driven init
31
+
32
+ The agent finishes Phase 5 → you run:
33
+
34
+ ```bash
35
+ axis cleanup
36
+ ```
37
+
38
+ This removes `.ai/skills/axis-bootstrap/` (it has done its job). Your project keeps:
39
+
40
+ - `.ai/INSTRUCTIONS.md` (custom for your project)
41
+ - `.ai/skills/<your-domains>/` (generated based on your code)
42
+ - `.ai/rules/`, `.ai/docs/`, `.ai/docs/STATE.md`
43
+ - `.claude/settings.json`, symlinks
44
+
45
+ → **Fully self-sufficient.** No dependency on `@axis-bootstrap/cli` after this. You can `npm uninstall -g @axis-bootstrap/cli` if you want.
46
+
47
+ (Optional: keep it around for `axis doctor` / `axis spdd canvas` per-feature workflow.)
48
+
49
+ ## Commands
50
+
51
+ | Command | Locale | What it does |
52
+ | ------- | ------ | ------------ |
53
+ | `axis init` | PT/EN | Interactive bootstrap (auto-detects new vs existing) |
54
+ | `axis audit` | EN | Reports what AXIS layers are missing |
55
+ | `axis doctor` | EN | Validates limits (INSTRUCTIONS 100-180, SKILL ≤60), symlinks, settings |
56
+ | `axis link` | EN | Runs `setup-ide-links.sh` (idempotent) |
57
+ | `axis state` | EN | Opens `.ai/docs/STATE.md` in `$EDITOR` |
58
+ | `axis spdd <step>` | EN | Per-feature SPDD pipeline step |
59
+ | `axis cleanup` | PT/EN | Removes axis-bootstrap meta-skill after AI-driven init |
60
+
61
+ ## SPDD pipeline (per feature)
62
+
63
+ Each step prints the trigger phrase to paste into your AI tool:
64
+
65
+ ```bash
66
+ axis spdd canvas pricing-quote # scaffold .ai/docs/canvases/pricing-quote.md
67
+ axis spdd story # → AI fills R section
68
+ axis spdd align # → AI fills O + N + S₂
69
+ axis spdd design # → AI fills E + A + S₁
70
+ # … generate code in your AI tool …
71
+ axis spdd review # AI verifies diff against Canvas
72
+ ```
73
+
74
+ ## Removing AXIS entirely
75
+
76
+ ```bash
77
+ rm -rf .ai .claude .cursor .agents AGENTS.md CLAUDE.md setup-ide-links.sh
78
+ ```
79
+
80
+ That's it. No leftover config, no `node_modules` pollution, no entries in `package.json`.
81
+
82
+ ## Tech
83
+
84
+ - Node 18+, ESM
85
+ - `@clack/prompts` (TUI), `picocolors` (ANSI)
86
+ - Zero other deps. Single file per command under `src/commands/`.
87
+
88
+ ## License
89
+
90
+ MIT
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@axis-bootstrap/cli",
3
+ "version": "0.1.0",
4
+ "description": "AXIS — Harness-first scaffolding & SPDD pipeline CLI for AI-augmented projects",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "axis": "src/index.js"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "templates",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "scripts": {
21
+ "start": "node src/index.js",
22
+ "test": "node src/index.js --help",
23
+ "pack:dry": "npm pack --dry-run",
24
+ "pack:tarball": "npm pack",
25
+ "prepublishOnly": "node src/index.js --help"
26
+ },
27
+ "keywords": [
28
+ "axis",
29
+ "spdd",
30
+ "ai",
31
+ "harness",
32
+ "spec-driven",
33
+ "claude-code",
34
+ "cursor",
35
+ "scaffolding"
36
+ ],
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@clack/prompts": "^0.7.0",
40
+ "picocolors": "^1.0.1"
41
+ }
42
+ }
@@ -0,0 +1,53 @@
1
+ import { outro, note, log } from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import path from 'node:path';
4
+ import { findTarget, exists } from '../lib/paths.js';
5
+
6
+ export async function audit(argv) {
7
+ const target = path.resolve(argv[0] || findTarget());
8
+ log.info(`Target: ${pc.cyan(target)}`);
9
+
10
+ const layers = {
11
+ Spec: [
12
+ ['.ai/INSTRUCTIONS.md', 'Entry point for AI'],
13
+ ['.ai/skills/', 'Domain skills'],
14
+ ['.ai/rules/', 'Behavior rules (optional)'],
15
+ ['.ai/docs/', 'Reference docs (architecture, schema, glossary)'],
16
+ ],
17
+ Harness: [
18
+ ['.claude/settings.json', 'Versioned permissions/hooks'],
19
+ ['setup-ide-links.sh', 'Idempotent symlink installer'],
20
+ ['AGENTS.md', 'Symlink to INSTRUCTIONS'],
21
+ ['CLAUDE.md', 'Symlink to INSTRUCTIONS'],
22
+ ],
23
+ Memory: [
24
+ ['.ai/CONVENTIONS.md', 'Maintenance protocol'],
25
+ ['.ai/docs/STATE.md', 'Curated playbook'],
26
+ ['.ai/docs/canvases/', 'REASONS Canvases (per feature)'],
27
+ ],
28
+ };
29
+
30
+ for (const [layer, items] of Object.entries(layers)) {
31
+ const lines = items.map(([file, purpose]) => {
32
+ const p = path.join(target, file);
33
+ const ok = exists(p);
34
+ return `${ok ? pc.green('✓') : pc.red('✗')} ${pc.bold(file.padEnd(28))} ${pc.dim(purpose)}`;
35
+ });
36
+ note(lines.join('\n'), `${layer} Layer`);
37
+ }
38
+
39
+ const missing = Object.values(layers)
40
+ .flat()
41
+ .filter(([file]) => !exists(path.join(target, file)));
42
+
43
+ if (missing.length === 0) {
44
+ outro(pc.green('✓ All AXIS layers present.'));
45
+ } else {
46
+ outro(
47
+ pc.yellow(
48
+ `${missing.length} item(s) missing. Run ${pc.cyan('axis init')} to scaffold, or fix individually.`
49
+ )
50
+ );
51
+ process.exitCode = 1;
52
+ }
53
+ }
@@ -0,0 +1,42 @@
1
+ import { confirm, intro, outro, isCancel, cancel, log, note } from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import path from 'node:path';
4
+ import { findTarget, exists } from '../lib/paths.js';
5
+ import { rmDir } from '../lib/copy.js';
6
+ import { detectLocale, t } from '../lib/i18n.js';
7
+
8
+ export async function cleanup(argv) {
9
+ const target = path.resolve(argv[0] || findTarget());
10
+ const locale = detectLocale();
11
+ const T = (k) => t(locale, k);
12
+
13
+ intro(pc.bgRed(pc.white(' axis cleanup ')));
14
+
15
+ const skill = path.join(target, '.ai', 'skills', 'axis-bootstrap');
16
+ if (!exists(skill)) {
17
+ log.info(T('cleanupNotFound'));
18
+ return outro(pc.dim(T('done')));
19
+ }
20
+
21
+ note(
22
+ [
23
+ pc.dim(T('target') + ': ' + target),
24
+ '',
25
+ pc.bold(pc.red('Will remove:')),
26
+ ` ${pc.red('-')} .ai/skills/axis-bootstrap/`,
27
+ '',
28
+ pc.bold(pc.green('Will keep:')),
29
+ ` ${pc.green('+')} .ai/INSTRUCTIONS.md, CONVENTIONS.md`,
30
+ ` ${pc.green('+')} .ai/skills/* (your generated skills)`,
31
+ ` ${pc.green('+')} .ai/rules/, .ai/docs/, STATE.md`,
32
+ ` ${pc.green('+')} .claude/settings.json, symlinks`,
33
+ ].join('\n'),
34
+ 'Cleanup plan'
35
+ );
36
+
37
+ const ok = await confirm({ message: T('cleanupConfirm'), initialValue: true });
38
+ if (isCancel(ok) || !ok) return cancel(T('cleanupAborted'));
39
+
40
+ rmDir(skill);
41
+ outro(pc.green('✓ ' + T('cleanupRemoved')));
42
+ }
@@ -0,0 +1,137 @@
1
+ import { outro, log, note } from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import path from 'node:path';
4
+ import fs from 'node:fs';
5
+ import { findTarget, exists, countLines, read } from '../lib/paths.js';
6
+ import { ok, fail, warn } from '../lib/ui.js';
7
+
8
+ export async function doctor(argv) {
9
+ const target = path.resolve(argv[0] || findTarget());
10
+ log.info(`Target: ${pc.cyan(target)}`);
11
+
12
+ const checks = [];
13
+
14
+ // .ai/ exists
15
+ checks.push(check('.ai/ directory exists', exists(path.join(target, '.ai'))));
16
+
17
+ // INSTRUCTIONS.md size
18
+ const instr = path.join(target, '.ai', 'INSTRUCTIONS.md');
19
+ if (exists(instr)) {
20
+ const lines = countLines(instr);
21
+ const inRange = lines >= 100 && lines <= 180;
22
+ checks.push(
23
+ check(
24
+ `INSTRUCTIONS.md size (${lines} lines, target 100-180)`,
25
+ inRange,
26
+ lines < 100 ? 'too short — likely superficial' : lines > 180 ? 'too long — loses focus' : null
27
+ )
28
+ );
29
+ } else {
30
+ checks.push(check('INSTRUCTIONS.md exists', false));
31
+ }
32
+
33
+ // SKILL.md sizes
34
+ const skillsDir = path.join(target, '.ai', 'skills');
35
+ if (exists(skillsDir)) {
36
+ const skills = fs.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
37
+ if (skills.length === 0) {
38
+ checks.push(check('skills/ has at least one skill', false, 'add skills with axis spdd ...'));
39
+ }
40
+ for (const s of skills) {
41
+ const p = path.join(skillsDir, s.name, 'SKILL.md');
42
+ if (!exists(p)) {
43
+ checks.push(check(`${s.name}/SKILL.md exists`, false));
44
+ continue;
45
+ }
46
+ const lines = countLines(p);
47
+ const okSize = lines <= 60;
48
+ checks.push(check(`${s.name}/SKILL.md ≤ 60 lines (${lines})`, okSize, okSize ? null : 'move details to references/'));
49
+
50
+ // description quality: pass if multi-line OR ≥ 150 chars (substantial single line)
51
+ const head = read(p).split('---')[1] || '';
52
+ const descLine = (head.match(/description:\s*([\s\S]*?)(?:\n[a-z_]+:|$)/) || [])[1] || '';
53
+ const trimmed = descLine.trim();
54
+ const descLineCount = trimmed.split('\n').length;
55
+ const descLen = trimmed.length;
56
+ const goodDesc = descLineCount >= 2 || descLen >= 150;
57
+ checks.push(
58
+ check(
59
+ `${s.name} description quality (${descLineCount}L/${descLen}c)`,
60
+ goodDesc,
61
+ goodDesc ? null : 'too short — agent may ignore the skill (target: ≥2 lines or ≥150 chars)'
62
+ )
63
+ );
64
+ }
65
+ } else {
66
+ checks.push(check('.ai/skills/ exists', false));
67
+ }
68
+
69
+ // STATE.md
70
+ const stateP = path.join(target, '.ai', 'docs', 'STATE.md');
71
+ if (exists(stateP)) {
72
+ const lines = countLines(stateP);
73
+ checks.push(check(`STATE.md ≤ 80 lines (${lines})`, lines <= 80, lines > 80 ? 'curate — remove resolved entries' : null));
74
+ } else {
75
+ checks.push(check('STATE.md exists', false, 'memory layer missing'));
76
+ }
77
+
78
+ // CONVENTIONS.md
79
+ checks.push(check('CONVENTIONS.md exists', exists(path.join(target, '.ai', 'CONVENTIONS.md'))));
80
+
81
+ // Symlinks
82
+ const symlinks = [
83
+ ['AGENTS.md', '.ai/INSTRUCTIONS.md'],
84
+ ['CLAUDE.md', '.ai/INSTRUCTIONS.md'],
85
+ ];
86
+ for (const [link] of symlinks) {
87
+ const p = path.join(target, link);
88
+ let resolves = false;
89
+ try {
90
+ const stat = fs.lstatSync(p);
91
+ if (stat.isSymbolicLink()) {
92
+ const realP = fs.realpathSync(p);
93
+ resolves = exists(realP);
94
+ }
95
+ } catch {
96
+ resolves = false;
97
+ }
98
+ checks.push(check(`${link} symlink resolves`, resolves));
99
+ }
100
+
101
+ // Settings
102
+ const settings = path.join(target, '.claude', 'settings.json');
103
+ if (exists(settings)) {
104
+ try {
105
+ const json = JSON.parse(read(settings));
106
+ const hasAllow = Array.isArray(json?.permissions?.allow) && json.permissions.allow.length > 0;
107
+ const hasDeny = Array.isArray(json?.permissions?.deny) && json.permissions.deny.length > 0;
108
+ checks.push(check('.claude/settings.json has allow + deny', hasAllow && hasDeny));
109
+ } catch (e) {
110
+ checks.push(check('.claude/settings.json valid JSON', false, e.message));
111
+ }
112
+ } else {
113
+ checks.push(check('.claude/settings.json exists', false, 'harness layer incomplete'));
114
+ }
115
+
116
+ // Print
117
+ console.log();
118
+ for (const c of checks) {
119
+ if (c.pass) ok(c.label);
120
+ else if (c.warn) warn(`${c.label} — ${c.detail || ''}`);
121
+ else fail(`${c.label}${c.detail ? ' — ' + c.detail : ''}`);
122
+ }
123
+
124
+ const passed = checks.filter((c) => c.pass).length;
125
+ const failed = checks.filter((c) => !c.pass).length;
126
+ console.log();
127
+ if (failed === 0) {
128
+ outro(pc.green(`✓ All ${passed} checks passed. Recursiveness contract upheld.`));
129
+ } else {
130
+ outro(pc.yellow(`${passed} passed, ${pc.red(failed)} failed. Run \`axis init\` or fix above.`));
131
+ process.exitCode = 1;
132
+ }
133
+ }
134
+
135
+ function check(label, pass, detail = null) {
136
+ return { label, pass, detail };
137
+ }
@@ -0,0 +1,297 @@
1
+ import { text, multiselect, select, confirm, spinner, note, log, isCancel, cancel } from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import path from 'node:path';
4
+ import fs from 'node:fs';
5
+ import { execSync } from 'node:child_process';
6
+ import { TEMPLATES, exists, read, write, ensureDir } from '../lib/paths.js';
7
+ import { detectLocale, t } from '../lib/i18n.js';
8
+ import { detectProject } from '../lib/detect.js';
9
+ import { copyDir } from '../lib/copy.js';
10
+
11
+ const IDES = [
12
+ { value: 'claude', label: 'Claude Code', hint: '.claude/' },
13
+ { value: 'cursor', label: 'Cursor', hint: '.cursor/' },
14
+ { value: 'agents', label: 'Generic agents (Windsurf, etc.)', hint: '.agents/' },
15
+ { value: 'github', label: 'GitHub Copilot', hint: '.github/' },
16
+ ];
17
+
18
+ const SPDD_SKILLS = [
19
+ { value: 'story-decompose', label: 'story-decompose', hint: 'fills R' },
20
+ { value: 'alignment', label: 'alignment', hint: 'fills O + N + S₂' },
21
+ { value: 'abstraction-first', label: 'abstraction-first', hint: 'fills E + A + S₁' },
22
+ { value: 'iterative-review', label: 'iterative-review', hint: 'Track A/B' },
23
+ ];
24
+
25
+ export async function init(argv) {
26
+ const target = path.resolve(argv[0] || process.cwd());
27
+
28
+ // 1. Pick language (auto-detect, ask once)
29
+ const detected = detectLocale();
30
+ let locale = detected;
31
+ const langChoice = await select({
32
+ message: t(detected, 'pickLang'),
33
+ options: [
34
+ { value: 'en', label: t(detected, 'langEN') },
35
+ { value: 'pt', label: t(detected, 'langPT') },
36
+ ],
37
+ initialValue: detected,
38
+ });
39
+ if (isCancel(langChoice)) return cancel(t(detected, 'aborted'));
40
+ locale = langChoice;
41
+ const T = (k) => t(locale, k);
42
+
43
+ // 2. Detect project state
44
+ const detection = detectProject(target);
45
+ log.info(`${T('target')}: ${pc.cyan(target)}`);
46
+
47
+ if (detection.state === 'already-bootstrapped') {
48
+ log.warn(T('detectedAlreadyBootstrapped'));
49
+ const cont = await confirm({ message: T('overwrite'), initialValue: false });
50
+ if (isCancel(cont) || !cont) return cancel(T('abortedAlready'));
51
+ }
52
+
53
+ if (detection.state === 'existing-software' || detection.state === 'existing-other') {
54
+ const lines = [pc.bold(T('detectedExisting'))];
55
+ if (detection.stackHints.length) {
56
+ lines.push('', pc.dim(`Markers: ${detection.stackHints.join(', ')}`));
57
+ }
58
+ if (detection.topFiles.length) {
59
+ lines.push(pc.dim(`Top files: ${detection.topFiles.slice(0, 8).join(', ')}`));
60
+ }
61
+ note(lines.join('\n'), T('bootstrapPlan'));
62
+ } else {
63
+ note(T('detectedEmpty'), T('bootstrapPlan'));
64
+ }
65
+
66
+ // 3. Pick mode (default: AI for existing projects, quick for empty)
67
+ const defaultMode = detection.state === 'empty' ? 'quick' : 'ai';
68
+ const mode = await select({
69
+ message: T('pickMode'),
70
+ options: [
71
+ { value: 'ai', label: T('modeAI') },
72
+ { value: 'quick', label: T('modeQuick') },
73
+ { value: 'audit', label: T('modeAudit') },
74
+ ],
75
+ initialValue: defaultMode,
76
+ });
77
+ if (isCancel(mode)) return cancel(T('aborted'));
78
+
79
+ if (mode === 'audit') {
80
+ const { audit } = await import('./audit.js');
81
+ return audit([target]);
82
+ }
83
+
84
+ if (mode === 'ai') {
85
+ return aiBootstrap(target, locale);
86
+ }
87
+
88
+ return quickBootstrap(target, locale);
89
+ }
90
+
91
+ /**
92
+ * AI-driven path: copy axis-bootstrap skill bundle, print trigger phrase.
93
+ * The agent does the real discovery + generation.
94
+ */
95
+ async function aiBootstrap(target, locale) {
96
+ const T = (k) => t(locale, k);
97
+
98
+ const s = spinner();
99
+ s.start(T('aiInstalling'));
100
+
101
+ // Create minimal scaffolding so the agent has a place to put .ai/
102
+ ensureDir(path.join(target, '.ai', 'skills'));
103
+ ensureDir(path.join(target, '.ai', 'docs'));
104
+
105
+ // Copy bootstrap skill bundle
106
+ copyDir(path.join(TEMPLATES, 'bootstrap-skill'), path.join(target, '.ai', 'skills', 'axis-bootstrap'));
107
+
108
+ // Drop minimal CLAUDE.md / AGENTS.md pointing the agent to the skill
109
+ const stub = stubInstructions(locale);
110
+ if (!exists(path.join(target, '.ai', 'INSTRUCTIONS.md'))) {
111
+ write(path.join(target, '.ai', 'INSTRUCTIONS.md'), stub);
112
+ }
113
+ // Root entry points (actual files for now — symlinks come after Phase 3 of the bootstrap)
114
+ if (!exists(path.join(target, 'CLAUDE.md'))) {
115
+ write(path.join(target, 'CLAUDE.md'), stub);
116
+ }
117
+ if (!exists(path.join(target, 'AGENTS.md'))) {
118
+ write(path.join(target, 'AGENTS.md'), stub);
119
+ }
120
+
121
+ s.stop(pc.green('✓ ' + T('aiInstalling').replace('…', ' ' + T('done'))));
122
+
123
+ note(
124
+ [
125
+ pc.bold(T('aiOpenAgent')),
126
+ '',
127
+ pc.bold(T('aiTrigger')),
128
+ '',
129
+ pc.cyan(' ' + T('aiTriggerText')),
130
+ '',
131
+ pc.bold(T('aiCleanup')),
132
+ ].join('\n'),
133
+ T('aiNextSteps')
134
+ );
135
+ }
136
+
137
+ function stubInstructions(locale) {
138
+ if (locale === 'pt') {
139
+ return `# Instruções para o Agente — Bootstrap pendente
140
+
141
+ > Este projeto está usando o **AXIS Framework**. O scaffold mínimo já existe; o trabalho real (gerar skills, rules, docs customizados) cabe a você, agente de IA.
142
+
143
+ ## Sua tarefa agora
144
+
145
+ 1. Carregue a skill em [.ai/skills/axis-bootstrap/SKILL.md](.ai/skills/axis-bootstrap/SKILL.md)
146
+ 2. Siga o [PLANNER.md](.ai/skills/axis-bootstrap/PLANNER.md) na ordem
147
+ 3. **Antes de qualquer pergunta:** leia README, package.json (ou equivalente), e estrutura de arquivos do projeto
148
+ 4. Pause e peça confirmação entre cada Phase
149
+
150
+ ## Após terminar a Phase 5
151
+
152
+ Avise o usuário para rodar \`axis cleanup\` — isso remove a skill axis-bootstrap (já cumpriu seu papel) e deixa o projeto autossuficiente.
153
+
154
+ ## Regras invioláveis
155
+
156
+ - Nunca invente: se faltar informação, pergunte
157
+ - Nunca pule fases: cada uma tem gate explícito
158
+ - Nunca duplique conteúdo entre arquivos: use links e symlinks
159
+ `;
160
+ }
161
+ return `# Agent Instructions — Bootstrap pending
162
+
163
+ > This project is using the **AXIS Framework**. Minimal scaffolding is in place; the real work (generating customized skills, rules, docs) is your job, AI agent.
164
+
165
+ ## Your task now
166
+
167
+ 1. Load the skill at [.ai/skills/axis-bootstrap/SKILL.md](.ai/skills/axis-bootstrap/SKILL.md)
168
+ 2. Follow [PLANNER.md](.ai/skills/axis-bootstrap/PLANNER.md) in order
169
+ 3. **Before any question:** read the README, package.json (or equivalent), and the project file tree
170
+ 4. Pause for explicit confirmation between phases
171
+
172
+ ## After Phase 5 completes
173
+
174
+ Tell the user to run \`axis cleanup\` — it removes the axis-bootstrap skill (it has done its job) and leaves the project self-sufficient.
175
+
176
+ ## Inviolable rules
177
+
178
+ - Never fabricate: if information is missing, ask
179
+ - Never skip phases: each has an explicit gate
180
+ - Never duplicate content across files: use links and symlinks
181
+ `;
182
+ }
183
+
184
+ /**
185
+ * Quick path: interactive scaffold without AI.
186
+ * Good for new projects where the user already knows the stack.
187
+ */
188
+ async function quickBootstrap(target, locale) {
189
+ const T = (k) => t(locale, k);
190
+
191
+ const name = await text({
192
+ message: T('askName'),
193
+ placeholder: path.basename(target),
194
+ defaultValue: path.basename(target),
195
+ });
196
+ if (isCancel(name)) return cancel(T('aborted'));
197
+
198
+ const purpose = await text({
199
+ message: T('askPurpose'),
200
+ placeholder: T('askPurposeHint'),
201
+ validate: (v) => (v && v.length > 5 ? undefined : T('askPurposeShort')),
202
+ });
203
+ if (isCancel(purpose)) return cancel(T('aborted'));
204
+
205
+ const stack = await text({
206
+ message: T('askStack'),
207
+ placeholder: T('askStackHint'),
208
+ defaultValue: '',
209
+ });
210
+ if (isCancel(stack)) return cancel(T('aborted'));
211
+
212
+ const ides = await multiselect({
213
+ message: T('askIDEs'),
214
+ options: IDES,
215
+ initialValues: ['claude', 'agents'],
216
+ required: true,
217
+ });
218
+ if (isCancel(ides)) return cancel(T('aborted'));
219
+
220
+ const spddSkills = await multiselect({
221
+ message: T('askSpdd') + ' ' + pc.dim('— ' + T('askSpddHint')),
222
+ options: SPDD_SKILLS,
223
+ initialValues: SPDD_SKILLS.map((s) => s.value),
224
+ required: false,
225
+ });
226
+ if (isCancel(spddSkills)) return cancel(T('aborted'));
227
+
228
+ const s = spinner();
229
+ s.start(T('quickScaffolding'));
230
+
231
+ ensureDir(path.join(target, '.ai', 'skills'));
232
+ ensureDir(path.join(target, '.ai', 'rules'));
233
+ ensureDir(path.join(target, '.ai', 'docs', 'canvases', 'done'));
234
+
235
+ const replace = (content) =>
236
+ content
237
+ .replace(/{{PROJECT_NAME}}/g, name)
238
+ .replace(/{{PROJECT_PURPOSE}}/g, purpose)
239
+ .replace(/{{STACK}}/g, stack || (locale === 'pt' ? '(não-software)' : '(non-software)'));
240
+
241
+ write(path.join(target, '.ai', 'INSTRUCTIONS.md'), replace(read(path.join(TEMPLATES, 'INSTRUCTIONS.md'))));
242
+ write(path.join(target, '.ai', 'CONVENTIONS.md'), replace(read(path.join(TEMPLATES, 'CONVENTIONS.md'))));
243
+ write(path.join(target, '.ai', 'docs', 'STATE.md'), replace(read(path.join(TEMPLATES, 'STATE.md'))));
244
+ s.message(T('quickSpecReady'));
245
+
246
+ // SPDD skills
247
+ for (const skill of spddSkills) {
248
+ const skillDir = path.join(target, '.ai', 'skills', skill);
249
+ ensureDir(skillDir);
250
+ const src = path.join(TEMPLATES, 'skills', `${skill}.md`);
251
+ if (exists(src)) {
252
+ fs.copyFileSync(src, path.join(skillDir, 'SKILL.md'));
253
+ }
254
+ }
255
+
256
+ // Harness
257
+ if (ides.includes('claude')) {
258
+ ensureDir(path.join(target, '.claude'));
259
+ write(path.join(target, '.claude', 'settings.json'), read(path.join(TEMPLATES, 'settings.json')));
260
+ }
261
+ s.message(T('quickHarnessReady'));
262
+
263
+ // setup-ide-links.sh
264
+ write(path.join(target, 'setup-ide-links.sh'), read(path.join(TEMPLATES, 'setup-ide-links.sh')));
265
+ fs.chmodSync(path.join(target, 'setup-ide-links.sh'), 0o755);
266
+ s.message(T('quickInstallerReady'));
267
+
268
+ try {
269
+ execSync('bash setup-ide-links.sh', { cwd: target, stdio: 'pipe' });
270
+ s.stop(T('quickSymlinksInstalled'));
271
+ } catch {
272
+ s.stop(T('quickDoneScaffolding'));
273
+ log.warn(T('quickRunLink'));
274
+ }
275
+
276
+ const created = [
277
+ '.ai/INSTRUCTIONS.md',
278
+ '.ai/CONVENTIONS.md',
279
+ '.ai/docs/STATE.md',
280
+ `.ai/skills/ (${spddSkills.length} SPDD)`,
281
+ 'setup-ide-links.sh',
282
+ ...(ides.includes('claude') ? ['.claude/settings.json'] : []),
283
+ 'AGENTS.md → .ai/INSTRUCTIONS.md',
284
+ 'CLAUDE.md → .ai/INSTRUCTIONS.md',
285
+ ];
286
+ note(created.map((c) => pc.green('+ ') + c).join('\n'), T('quickCreated'));
287
+
288
+ note(
289
+ [
290
+ `${pc.bold('1.')} ${T('quickNext1')}`,
291
+ `${pc.bold('2.')} ${T('quickNext2')}`,
292
+ `${pc.bold('3.')} ${T('quickNext3')}`,
293
+ `${pc.bold('4.')} ${T('quickNext4')}`,
294
+ ].join('\n'),
295
+ locale === 'pt' ? 'Próximos passos' : 'Next steps'
296
+ );
297
+ }
@@ -0,0 +1,31 @@
1
+ import { spinner, outro, log } from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import path from 'node:path';
4
+ import { execSync } from 'node:child_process';
5
+ import { findTarget, exists, read, write } from '../lib/paths.js';
6
+ import { TEMPLATES } from '../lib/paths.js';
7
+ import fs from 'node:fs';
8
+
9
+ export async function link(argv) {
10
+ const target = path.resolve(argv[0] || findTarget());
11
+ const script = path.join(target, 'setup-ide-links.sh');
12
+
13
+ if (!exists(script)) {
14
+ log.warn('setup-ide-links.sh not found — installing from template.');
15
+ write(script, read(path.join(TEMPLATES, 'setup-ide-links.sh')));
16
+ fs.chmodSync(script, 0o755);
17
+ }
18
+
19
+ const s = spinner();
20
+ s.start('Running setup-ide-links.sh');
21
+ try {
22
+ const out = execSync('bash setup-ide-links.sh', { cwd: target }).toString();
23
+ s.stop('Symlinks installed');
24
+ if (out.trim()) console.log(pc.dim(out.trim()));
25
+ outro(pc.green('✓ done'));
26
+ } catch (e) {
27
+ s.stop('Failed');
28
+ log.error(e.message);
29
+ process.exit(1);
30
+ }
31
+ }