@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
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
|
+
}
|