@kudusov.takhir/ba-toolkit 1.3.2 → 1.5.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/CHANGELOG.md +51 -1
- package/bin/ba-toolkit.js +616 -137
- package/package.json +2 -2
- package/skills/references/templates/agents-template.md +57 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,54 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [1.5.0] — 2026-04-08
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Typo detection on unknown CLI flags**, with a Levenshtein-based "Did you mean ...?" hint. Previously the parser silently accepted any `--flag` and stored it in `args.flags`, where most code paths then ignored it — users would hit `ba-toolkit init --drry-run`, get no error, and watch the script start an interactive session wondering why their flag did nothing. Now `validateFlags` runs immediately after parsing and rejects anything not in `KNOWN_FLAGS`:
|
|
19
|
+
```
|
|
20
|
+
$ ba-toolkit init --drry-run
|
|
21
|
+
error: Unknown option: --drry-run
|
|
22
|
+
Did you mean --dry-run?
|
|
23
|
+
Run ba-toolkit --help for the full list of options.
|
|
24
|
+
```
|
|
25
|
+
The Levenshtein threshold is calibrated (`max(1, floor(input.length / 3))`) so common typos like `--domian`, `--gloabl`, `--no-installl`, `--foo`/`--for` get matched, but unrelated inputs like `--foobar` (distance 3 from "global") get no suggestion at all — better to say nothing than suggest something wildly off.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Help output is ~20 lines shorter.** Four nearly-duplicate options sections (`INIT OPTIONS`, `INSTALL OPTIONS`, `UNINSTALL OPTIONS`, `UPGRADE OPTIONS`) collapsed into a single `OPTIONS` section. Each flag is listed exactly once with a per-command scope annotation (`init only — ...`, `install/uninstall/upgrade — ...`). Three sections after v1.4.0 added `uninstall`/`upgrade` repeated the same four flags with only wording differences; the dedup removes the noise without losing any information.
|
|
30
|
+
- **Domain and agent menu column widths are now computed dynamically** from the longest entry instead of being hardcoded to magic constants (`padEnd(13)` and `padEnd(20)`). If a future commit adds a domain or agent with a longer display name, the em-dash column on the right stays aligned automatically. No visible change for the current set.
|
|
31
|
+
|
|
32
|
+
### Internal
|
|
33
|
+
|
|
34
|
+
- **`AGENTS.md` template extracted** to `skills/references/templates/agents-template.md`. Previously it lived as a 44-line embedded string in `bin/ba-toolkit.js`, which was the only artifact template not in `skills/references/templates/`. Now follows the same `[NAME]` / `[SLUG]` / `[DOMAIN]` / `[DATE]` placeholder convention as `brief-template.md`, `srs-template.md`, etc. `renderAgentsMd` shrinks from a 60-line string template to a 12-line file read + four `String.replace` calls. Adding a field to the auto-generated AGENTS.md is now a Markdown edit, not a JS edit.
|
|
35
|
+
- **`stringFlag(args, key)` helper extracted** to centralise the `flag && flag !== true` (and inverse `!flag || flag === true`) pattern that was repeated across `cmdInit` (4 sites), `cmdInstall`, `cmdUninstall`, `cmdUpgrade` (3 sites). Returns the string value or `null` for absent / boolean / empty-string flags. The seven call sites collapse to single-line `stringFlag(args, 'name')` reads.
|
|
36
|
+
- **`parseSkillFrontmatter(content)` mini-parser** replaces the three regexes that `skillToMdc` used to extract `name` and `description` from SKILL.md frontmatter. The previous folded-scalar regex (`description:\s*>\s*\r?\n([\s\S]*?)(?:\r?\n\w|$)`) used a fragile `\r?\n\w` lookahead that would have over-captured on multi-paragraph descriptions and silently misparsed the `|` literal block scalar form (no shipped SKILL.md uses it yet, but the trap was there). The new walker handles inline / folded / literal forms uniformly, recognises chomping indicators (`>+`/`>-`/`|+`/`|-`), and correctly collapses multi-paragraph blocks with blank lines. Backward-compat verified by running `ba-toolkit install --for cursor` and confirming all 21 generated `.mdc` files have the same descriptions as before.
|
|
37
|
+
- **Unit-test infrastructure (78 → 91 tests).** Pure helper functions (`sanitiseSlug`, `parseArgs`, `resolveDomain`, `resolveAgent`, `stringFlag`, `parseSkillFrontmatter`, `readSentinel`, `renderAgentsMd`, `levenshtein`, `closestMatch`) are now exported from `bin/ba-toolkit.js` (guarded by `require.main === module` so the CLI still runs directly), and covered by `test/cli.test.js` using Node's built-in `node:test` runner — zero added dependencies. The test file does not ship to npm consumers (`test/` is not in `package.json`'s `files` whitelist). Includes one integration test that loads every shipped `SKILL.md` and asserts the parser produces a non-empty single-line description for each — catches regressions where a future skill is added with a frontmatter form the parser doesn't understand.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## [1.4.0] — 2026-04-08
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **`ba-toolkit uninstall --for <agent>`** — remove BA Toolkit skills from an agent's directory. Symmetric to `install`: same `--for`, `--global`, `--project`, `--dry-run` flag set, same project-vs-global default. Counts files in the destination, asks `Remove {dest}? (y/N)` (defaults to N), and prints `Removed N files` after the rm completes. The pre-removal safety guard refuses to proceed unless `path.basename(destDir) === 'ba-toolkit'` — this is the only place in the CLI that calls `fs.rmSync({recursive: true})`, so it gets the strictest validation against future bugs that could turn it into `rm -rf $HOME`.
|
|
46
|
+
- **`ba-toolkit upgrade --for <agent>`** (aliased as `update`) — refresh skills after a toolkit version bump. Reads the new version sentinel (see below), compares to `PKG.version`, and either prints `Already up to date` or wipes the destination wholesale and re-runs install with `force: true` (skipping the overwrite prompt). The wipe-and-reinstall approach guarantees that files removed from the toolkit between versions don't linger as ghost files in the destination — fixes the same class of bug that motivated `cmdUninstall`'s safety check. Pre-1.4 installs with no sentinel are treated as out-of-date and get a clean reinstall on first upgrade.
|
|
47
|
+
- **`ba-toolkit status`** — pure read-only inspection: scans every (agent × scope) combination — 5 agents × project/global where supported, 8 real locations in total — and reports which versions of BA Toolkit are installed where. Output is grouped per installation, multi-line for readability, with colored version labels (`green: current` / `yellow: outdated` / `gray: pre-1.4 install with no sentinel`) and a summary footer pointing at `upgrade` for stale installs. Drives the natural follow-up to the version sentinel: now there's something to read it back with.
|
|
48
|
+
- **Version sentinel `.ba-toolkit-version`** — `runInstall` now writes a hidden JSON marker file (`{"version": "1.4.0", "installedAt": "..."}`) into the install destination after a successful copy. The file has no `.md`/`.mdc` extension, so all five supported agents' skill loaders ignore it. Lets `upgrade` and `status` tell which package version is currently installed without diffing every file.
|
|
49
|
+
- **`runInstall({..., force: true})` option** — skips the existing-destination overwrite prompt. Used by `cmdUpgrade` because it has already wiped the destination (or is in dry-run) and the prompt would just be noise. Not exposed as a CLI flag — `force` is an internal API surface only.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **Five Tier 1 CLI fixes were already shipped in 1.3.2**, but they're worth restating in context here because 1.4.0 builds directly on the same `bin/ba-toolkit.js` improvements: `cmdInit` now honours `runInstall`'s return value (no false success message after a declined overwrite), `parseArgs` accepts `--key=value` form, the readline lifecycle and SIGINT handling are unified through a single shared interface with `INPUT_CLOSED` rejection, the slug-derivation path prints a clear error when the input has no ASCII letters, and `AGENTS.md` now lists all 21 skills (the previous template was missing the 8 cross-cutting utilities added in v1.1 and v1.2). See the 1.3.2 entry below for the per-fix detail.
|
|
54
|
+
|
|
55
|
+
### Internal
|
|
56
|
+
|
|
57
|
+
- **`resolveAgentDestination(...)` helper** — extracted scope-resolution logic from `runInstall`'s inline checks. `cmdUninstall` and `cmdUpgrade` reuse it. `runInstall` itself could be refactored to call the helper too in a follow-up — kept inline for now to keep this release's diff focused on the user-facing features.
|
|
58
|
+
- **Documentation:** `CLAUDE.md` added at the repo root, with project conventions, the release flow (including the `.claude/settings.local.json` stash dance), the npm publish CI gotchas (curl tarball bypass for the broken bundled npm, `_authToken` strip for OIDC), and the do-not-touch list. Future Claude Code sessions get the institutional context without reading the full git log.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
14
62
|
## [1.3.2] — 2026-04-08
|
|
15
63
|
|
|
16
64
|
### Fixed
|
|
@@ -246,7 +294,9 @@ CI scripts that relied on the old behaviour (`init` creates files only, `install
|
|
|
246
294
|
|
|
247
295
|
---
|
|
248
296
|
|
|
249
|
-
[Unreleased]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.
|
|
297
|
+
[Unreleased]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.5.0...HEAD
|
|
298
|
+
[1.5.0]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.4.0...v1.5.0
|
|
299
|
+
[1.4.0]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.3.2...v1.4.0
|
|
250
300
|
[1.3.2]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.3.1...v1.3.2
|
|
251
301
|
[1.3.1]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.3.0...v1.3.1
|
|
252
302
|
[1.3.0]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.2.5...v1.3.0
|
package/bin/ba-toolkit.js
CHANGED
|
@@ -195,6 +195,103 @@ function resolveAgent(raw) {
|
|
|
195
195
|
return AGENTS[trimmed] ? trimmed : null;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// Returns the string value of a flag, or null if it's absent, was passed
|
|
199
|
+
// as a bare boolean (e.g. `--name` with no following value, which
|
|
200
|
+
// parseArgs stores as `true`), or has an empty value (e.g. `--name=`).
|
|
201
|
+
// Centralises the `flag && flag !== true` / `!flag || flag === true`
|
|
202
|
+
// pattern that was repeated across cmdInit, cmdInstall, cmdUninstall,
|
|
203
|
+
// cmdUpgrade.
|
|
204
|
+
function stringFlag(args, key) {
|
|
205
|
+
const v = args.flags[key];
|
|
206
|
+
return (typeof v === 'string' && v.length > 0) ? v : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Every flag the CLI accepts. Typos that don't match anything in this
|
|
210
|
+
// set are rejected by validateFlags() with a "Did you mean ...?" hint.
|
|
211
|
+
// Single-letter aliases (-v, -h) are listed by their letter form.
|
|
212
|
+
const KNOWN_FLAGS = new Set([
|
|
213
|
+
'name', 'slug', 'domain', 'for', 'no-install',
|
|
214
|
+
'global', 'project', 'dry-run',
|
|
215
|
+
'version', 'v', 'help', 'h',
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
// Levenshtein distance with the standard two-row optimisation. Used by
|
|
219
|
+
// closestMatch() to suggest a fix for unknown flags. Pure, exported for
|
|
220
|
+
// tests.
|
|
221
|
+
function levenshtein(a, b) {
|
|
222
|
+
if (a === b) return 0;
|
|
223
|
+
const m = a.length;
|
|
224
|
+
const n = b.length;
|
|
225
|
+
if (m === 0) return n;
|
|
226
|
+
if (n === 0) return m;
|
|
227
|
+
let prev = new Array(n + 1);
|
|
228
|
+
let curr = new Array(n + 1);
|
|
229
|
+
for (let j = 0; j <= n; j++) prev[j] = j;
|
|
230
|
+
for (let i = 1; i <= m; i++) {
|
|
231
|
+
curr[0] = i;
|
|
232
|
+
for (let j = 1; j <= n; j++) {
|
|
233
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
234
|
+
curr[j] = Math.min(
|
|
235
|
+
prev[j] + 1, // deletion
|
|
236
|
+
curr[j - 1] + 1, // insertion
|
|
237
|
+
prev[j - 1] + cost, // substitution
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
[prev, curr] = [curr, prev];
|
|
241
|
+
}
|
|
242
|
+
return prev[n];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Find the candidate with the lowest Levenshtein distance to `input`,
|
|
246
|
+
// but only if the distance is small enough to be a plausible typo.
|
|
247
|
+
// Threshold scales with the input length: ~1 edit per 3 input chars,
|
|
248
|
+
// minimum 1. This catches the common cases (transposition like
|
|
249
|
+
// `--domian` for `--domain`, single insertion like `--drry-run` for
|
|
250
|
+
// `--dry-run`) without producing absurd suggestions for things that
|
|
251
|
+
// happen to share a few letters (`--foobar` should NOT suggest
|
|
252
|
+
// `--global` even though they have distance 3).
|
|
253
|
+
function closestMatch(input, candidates) {
|
|
254
|
+
if (!input) return null;
|
|
255
|
+
const threshold = Math.max(1, Math.floor(input.length / 3));
|
|
256
|
+
let best = null;
|
|
257
|
+
let bestDist = Infinity;
|
|
258
|
+
for (const c of candidates) {
|
|
259
|
+
const d = levenshtein(input, c);
|
|
260
|
+
if (d < bestDist && d <= threshold) {
|
|
261
|
+
best = c;
|
|
262
|
+
bestDist = d;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return best;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Reject unknown flags with a helpful "Did you mean ...?" suggestion
|
|
269
|
+
// when one is plausible. Called from main() after parseArgs.
|
|
270
|
+
//
|
|
271
|
+
// This catches typos like `--drry-run` or `--for-claude-code` (instead
|
|
272
|
+
// of `--for claude-code`) that the previous version of the CLI would
|
|
273
|
+
// silently store and then ignore. Both cases used to leave the user
|
|
274
|
+
// staring at an interactive prompt wondering why their flag did
|
|
275
|
+
// nothing.
|
|
276
|
+
function validateFlags(args) {
|
|
277
|
+
const unknown = [];
|
|
278
|
+
for (const key of Object.keys(args.flags)) {
|
|
279
|
+
if (!KNOWN_FLAGS.has(key)) unknown.push(key);
|
|
280
|
+
}
|
|
281
|
+
if (unknown.length === 0) return;
|
|
282
|
+
for (const flag of unknown) {
|
|
283
|
+
const dashes = flag.length === 1 ? '-' : '--';
|
|
284
|
+
logError(`Unknown option: ${dashes}${flag}`);
|
|
285
|
+
const suggestion = closestMatch(flag, [...KNOWN_FLAGS]);
|
|
286
|
+
if (suggestion) {
|
|
287
|
+
const sugDashes = suggestion.length === 1 ? '-' : '--';
|
|
288
|
+
log(` Did you mean ${cyan(sugDashes + suggestion)}?`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
log('Run ' + cyan('ba-toolkit --help') + ' for the full list of options.');
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
|
|
198
295
|
function today() {
|
|
199
296
|
return new Date().toISOString().slice(0, 10);
|
|
200
297
|
}
|
|
@@ -230,6 +327,79 @@ function copyDir(src, dest, { dryRun = false, transform = null } = {}) {
|
|
|
230
327
|
return copied;
|
|
231
328
|
}
|
|
232
329
|
|
|
330
|
+
// Minimal YAML frontmatter parser for SKILL.md files.
|
|
331
|
+
//
|
|
332
|
+
// Replaces the previous regex-based extraction, which used a fragile
|
|
333
|
+
// lookahead (`description:\s*>\s*\r?\n([\s\S]*?)(?:\r?\n\w|$)`) that
|
|
334
|
+
// didn't handle blank lines inside descriptions, didn't recognise the
|
|
335
|
+
// `|` literal block scalar form, and silently produced wrong output
|
|
336
|
+
// if the description happened to contain a line whose first character
|
|
337
|
+
// wasn't a word character.
|
|
338
|
+
//
|
|
339
|
+
// This is NOT a general YAML parser. It only handles the subset that
|
|
340
|
+
// SKILL.md files actually use:
|
|
341
|
+
// - Top-level keys at column 0: `key: value`
|
|
342
|
+
// - Inline scalar values: `name: foo`
|
|
343
|
+
// - Folded block scalars: `description: >` followed by indented text
|
|
344
|
+
// - Literal block scalars: `description: |` followed by indented text
|
|
345
|
+
// - Block chomping indicators (>+, >-, |+, |-)
|
|
346
|
+
// - Multi-paragraph block scalars with blank lines between paragraphs
|
|
347
|
+
//
|
|
348
|
+
// Unsupported (not used by any SKILL.md and YAGNI for the toolkit):
|
|
349
|
+
// - Nested mappings, sequences, anchors, aliases, tags
|
|
350
|
+
// - Quoted scalars (single or double quoted) — names would keep quotes
|
|
351
|
+
//
|
|
352
|
+
// Returns { name, description, body }. `description` is always
|
|
353
|
+
// flattened to a single line (whitespace collapsed) because the .mdc
|
|
354
|
+
// rule format expects a one-line description.
|
|
355
|
+
function parseSkillFrontmatter(content) {
|
|
356
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
357
|
+
if (!fmMatch) {
|
|
358
|
+
return { name: null, description: '', body: content };
|
|
359
|
+
}
|
|
360
|
+
const frontmatter = fmMatch[1];
|
|
361
|
+
const body = fmMatch[2];
|
|
362
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
363
|
+
|
|
364
|
+
const fields = {};
|
|
365
|
+
let currentKey = null;
|
|
366
|
+
let buffer = [];
|
|
367
|
+
|
|
368
|
+
const flush = () => {
|
|
369
|
+
if (currentKey !== null) {
|
|
370
|
+
fields[currentKey] = buffer.join(' ').replace(/\s+/g, ' ').trim();
|
|
371
|
+
}
|
|
372
|
+
currentKey = null;
|
|
373
|
+
buffer = [];
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
for (const line of lines) {
|
|
377
|
+
// A top-level YAML key starts at column 0 with `name:` style.
|
|
378
|
+
const m = line.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
|
379
|
+
if (m) {
|
|
380
|
+
flush();
|
|
381
|
+
currentKey = m[1];
|
|
382
|
+
const inlineValue = m[2].trim();
|
|
383
|
+
// Block scalar markers (>, |, >+, >-, |+, |-) introduce a block
|
|
384
|
+
// — they aren't part of the value themselves, so don't push them.
|
|
385
|
+
if (inlineValue && !/^[>|][+-]?$/.test(inlineValue)) {
|
|
386
|
+
buffer.push(inlineValue);
|
|
387
|
+
}
|
|
388
|
+
} else if (currentKey) {
|
|
389
|
+
// Continuation line for the active block scalar. Indentation and
|
|
390
|
+
// blank lines are folded into a single space by `flush()`.
|
|
391
|
+
buffer.push(line.trim());
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
flush();
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
name: fields.name || null,
|
|
398
|
+
description: fields.description || '',
|
|
399
|
+
body,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
233
403
|
// Transform SKILL.md → .mdc for Cursor / Windsurf.
|
|
234
404
|
// Other files (references/, templates/) are copied as-is.
|
|
235
405
|
function skillToMdc(srcPath, destPath) {
|
|
@@ -238,90 +408,39 @@ function skillToMdc(srcPath, destPath) {
|
|
|
238
408
|
return { destPath, content: fs.readFileSync(srcPath) };
|
|
239
409
|
}
|
|
240
410
|
const content = fs.readFileSync(srcPath, 'utf8');
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (fmMatch) {
|
|
245
|
-
frontmatter = fmMatch[1];
|
|
246
|
-
body = fmMatch[2];
|
|
247
|
-
}
|
|
248
|
-
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
249
|
-
// description is usually a multi-line block with `description: >`
|
|
250
|
-
const descMatch = frontmatter.match(/description:\s*>\s*\r?\n([\s\S]*?)(?:\r?\n\w|$)/);
|
|
251
|
-
const descInlineMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
252
|
-
const ruleName = nameMatch ? nameMatch[1].trim() : path.basename(path.dirname(srcPath));
|
|
253
|
-
const rawDesc = descMatch ? descMatch[1] : (descInlineMatch ? descInlineMatch[1] : '');
|
|
254
|
-
const ruleDesc = rawDesc.replace(/\s+/g, ' ').trim();
|
|
255
|
-
const mdcFrontmatter = `---\ndescription: ${ruleDesc}\nalwaysApply: false\n---\n\n`;
|
|
411
|
+
const { name, description, body } = parseSkillFrontmatter(content);
|
|
412
|
+
const ruleName = name || path.basename(path.dirname(srcPath));
|
|
413
|
+
const mdcFrontmatter = `---\ndescription: ${description}\nalwaysApply: false\n---\n\n`;
|
|
256
414
|
const newDestPath = path.join(path.dirname(destPath), `${ruleName}.mdc`);
|
|
257
415
|
return { destPath: newDestPath, content: mdcFrontmatter + body };
|
|
258
416
|
}
|
|
259
417
|
|
|
260
|
-
//
|
|
418
|
+
// Path to the AGENTS.md template file. Lives next to the rest of the
|
|
419
|
+
// reference templates so the convention stays uniform: anything that
|
|
420
|
+
// looks like generated artifact content lives in
|
|
421
|
+
// skills/references/templates/, not embedded as a string in the CLI.
|
|
422
|
+
//
|
|
423
|
+
// MAINTENANCE: when adding a new skill, update both tables in
|
|
424
|
+
// agents-template.md:
|
|
261
425
|
// - Sequential pipeline stages (numbered 0-11 + 7a sub-step) go in
|
|
262
426
|
// "Pipeline Status" — they have a per-project status that progresses.
|
|
263
427
|
// - Cross-cutting utilities (no fixed stage) go in "Cross-cutting Tools".
|
|
264
428
|
// The README.md pipeline table is the canonical source of truth for the
|
|
265
|
-
// 21-skill list and ordering; keep
|
|
266
|
-
|
|
267
|
-
return `# BA Toolkit — Project Context
|
|
268
|
-
|
|
269
|
-
> Auto-generated by \`ba-toolkit init\` on ${today()}. Updated automatically by /brief and /srs.
|
|
270
|
-
|
|
271
|
-
## Active Project
|
|
272
|
-
|
|
273
|
-
**Project:** ${name}
|
|
274
|
-
**Slug:** ${slug}
|
|
275
|
-
**Domain:** ${domain}
|
|
276
|
-
**Language:** English
|
|
277
|
-
**Output folder:** output/${slug}/
|
|
278
|
-
|
|
279
|
-
## Pipeline Status
|
|
280
|
-
|
|
281
|
-
| Stage | Skill | Status | File |
|
|
282
|
-
|-------|-------|--------|------|
|
|
283
|
-
| 0 | /principles | ⬜ Not started | — |
|
|
284
|
-
| 1 | /brief | ⬜ Not started | — |
|
|
285
|
-
| 2 | /srs | ⬜ Not started | — |
|
|
286
|
-
| 3 | /stories | ⬜ Not started | — |
|
|
287
|
-
| 4 | /usecases | ⬜ Not started | — |
|
|
288
|
-
| 5 | /ac | ⬜ Not started | — |
|
|
289
|
-
| 6 | /nfr | ⬜ Not started | — |
|
|
290
|
-
| 7 | /datadict | ⬜ Not started | — |
|
|
291
|
-
| 7a | /research | ⬜ Not started | — |
|
|
292
|
-
| 8 | /apicontract | ⬜ Not started | — |
|
|
293
|
-
| 9 | /wireframes | ⬜ Not started | — |
|
|
294
|
-
| 10 | /scenarios | ⬜ Not started | — |
|
|
295
|
-
| 11 | /handoff | ⬜ Not started | — |
|
|
296
|
-
|
|
297
|
-
## Cross-cutting Tools
|
|
298
|
-
|
|
299
|
-
Utilities available throughout the pipeline. No fixed stage — invoke whenever they help. See README.md for the prerequisites of each.
|
|
300
|
-
|
|
301
|
-
| Tool | Purpose |
|
|
302
|
-
|------|---------|
|
|
303
|
-
| /trace | Traceability Matrix + coverage gaps |
|
|
304
|
-
| /clarify [focus] | Targeted ambiguity resolution for any artifact |
|
|
305
|
-
| /analyze | Cross-artifact quality report with severity-rated findings |
|
|
306
|
-
| /estimate | Effort estimation — Fibonacci SP, T-shirt sizes, or person-days |
|
|
307
|
-
| /glossary | Unified project glossary with terminology drift detection |
|
|
308
|
-
| /export [format] | Export User Stories to Jira / GitHub Issues / Linear / CSV |
|
|
309
|
-
| /risk | Risk register — probability × impact matrix, mitigation per risk |
|
|
310
|
-
| /sprint | Sprint plan — stories grouped by velocity and capacity with sprint goals |
|
|
311
|
-
|
|
312
|
-
## Key Constraints
|
|
313
|
-
|
|
314
|
-
- Domain: ${domain}
|
|
315
|
-
- (Add constraints after /brief completes)
|
|
429
|
+
// 21-skill list and ordering; keep that template in sync with it.
|
|
430
|
+
const AGENTS_TEMPLATE_PATH = path.join(SKILLS_DIR, 'references', 'templates', 'agents-template.md');
|
|
316
431
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
432
|
+
function renderAgentsMd({ name, slug, domain }) {
|
|
433
|
+
let template;
|
|
434
|
+
try {
|
|
435
|
+
template = fs.readFileSync(AGENTS_TEMPLATE_PATH, 'utf8');
|
|
436
|
+
} catch (err) {
|
|
437
|
+
throw new Error(`Failed to read AGENTS.md template at ${AGENTS_TEMPLATE_PATH}: ${err.message}`);
|
|
438
|
+
}
|
|
439
|
+
return template
|
|
440
|
+
.replace(/\[NAME\]/g, name)
|
|
441
|
+
.replace(/\[SLUG\]/g, slug)
|
|
442
|
+
.replace(/\[DOMAIN\]/g, domain)
|
|
443
|
+
.replace(/\[DATE\]/g, today());
|
|
325
444
|
}
|
|
326
445
|
|
|
327
446
|
// --- Commands ----------------------------------------------------------
|
|
@@ -333,8 +452,8 @@ async function cmdInit(args) {
|
|
|
333
452
|
log('');
|
|
334
453
|
|
|
335
454
|
// --- 1. Project name (slug derives from it) ---
|
|
336
|
-
const
|
|
337
|
-
let name =
|
|
455
|
+
const nameFlag = stringFlag(args, 'name');
|
|
456
|
+
let name = nameFlag;
|
|
338
457
|
if (!name) name = await prompt(' Project name (e.g. My App): ');
|
|
339
458
|
name = String(name || '').trim();
|
|
340
459
|
if (!name) {
|
|
@@ -344,11 +463,11 @@ async function cmdInit(args) {
|
|
|
344
463
|
|
|
345
464
|
// --- 2. Slug (auto-derived from name; confirmed interactively unless
|
|
346
465
|
// both --name and --slug were passed on the command line) ---
|
|
347
|
-
const
|
|
348
|
-
let slug =
|
|
466
|
+
const slugFlag = stringFlag(args, 'slug');
|
|
467
|
+
let slug = slugFlag;
|
|
349
468
|
if (!slug) {
|
|
350
469
|
const derived = sanitiseSlug(name);
|
|
351
|
-
if (
|
|
470
|
+
if (nameFlag) {
|
|
352
471
|
// Non-interactive path. Either accept the derived slug, or fail
|
|
353
472
|
// loudly with a hint when the name has no ASCII letters/digits to
|
|
354
473
|
// derive from (e.g. `--name "Проект"` or `--name "🚀"`). Without
|
|
@@ -375,20 +494,22 @@ async function cmdInit(args) {
|
|
|
375
494
|
}
|
|
376
495
|
|
|
377
496
|
// --- 3. Domain (numbered menu) ---
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
497
|
+
const domainFlag = stringFlag(args, 'domain');
|
|
498
|
+
let domain;
|
|
499
|
+
if (domainFlag) {
|
|
500
|
+
domain = resolveDomain(domainFlag);
|
|
381
501
|
if (!domain) {
|
|
382
|
-
logError(`Unknown domain: ${
|
|
502
|
+
logError(`Unknown domain: ${domainFlag}`);
|
|
383
503
|
log('Valid ids: ' + DOMAINS.map((d) => d.id).join(', '));
|
|
384
504
|
process.exit(1);
|
|
385
505
|
}
|
|
386
506
|
} else {
|
|
387
507
|
log('');
|
|
388
508
|
log(' ' + yellow('Pick a domain:'));
|
|
509
|
+
const domainNameWidth = Math.max(...DOMAINS.map((d) => d.name.length));
|
|
389
510
|
DOMAINS.forEach((d, i) => {
|
|
390
511
|
const idx = String(i + 1).padStart(2);
|
|
391
|
-
log(` ${idx}) ${bold(d.name.padEnd(
|
|
512
|
+
log(` ${idx}) ${bold(d.name.padEnd(domainNameWidth))} ${gray('— ' + d.desc)}`);
|
|
392
513
|
});
|
|
393
514
|
log('');
|
|
394
515
|
const raw = await prompt(` Select [1-${DOMAINS.length}]: `);
|
|
@@ -401,12 +522,13 @@ async function cmdInit(args) {
|
|
|
401
522
|
|
|
402
523
|
// --- 4. Agent (numbered menu), unless --no-install ---
|
|
403
524
|
const skipInstall = !!args.flags['no-install'];
|
|
404
|
-
|
|
525
|
+
const forFlag = stringFlag(args, 'for');
|
|
526
|
+
let agentId = null;
|
|
405
527
|
if (!skipInstall) {
|
|
406
|
-
if (
|
|
407
|
-
agentId = resolveAgent(
|
|
528
|
+
if (forFlag) {
|
|
529
|
+
agentId = resolveAgent(forFlag);
|
|
408
530
|
if (!agentId) {
|
|
409
|
-
logError(`Unknown agent: ${
|
|
531
|
+
logError(`Unknown agent: ${forFlag}`);
|
|
410
532
|
log('Supported: ' + Object.keys(AGENTS).join(', '));
|
|
411
533
|
process.exit(1);
|
|
412
534
|
}
|
|
@@ -414,9 +536,10 @@ async function cmdInit(args) {
|
|
|
414
536
|
log('');
|
|
415
537
|
log(' ' + yellow('Pick your AI agent:'));
|
|
416
538
|
const agentEntries = Object.entries(AGENTS);
|
|
539
|
+
const agentNameWidth = Math.max(...agentEntries.map(([, a]) => a.name.length));
|
|
417
540
|
agentEntries.forEach(([id, a], i) => {
|
|
418
541
|
const idx = String(i + 1).padStart(2);
|
|
419
|
-
log(` ${idx}) ${bold(a.name.padEnd(
|
|
542
|
+
log(` ${idx}) ${bold(a.name.padEnd(agentNameWidth))} ${gray('(' + id + ')')}`);
|
|
420
543
|
});
|
|
421
544
|
log('');
|
|
422
545
|
const raw = await prompt(` Select [1-${agentEntries.length}]: `);
|
|
@@ -497,10 +620,39 @@ async function cmdInit(args) {
|
|
|
497
620
|
log('');
|
|
498
621
|
}
|
|
499
622
|
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
|
|
623
|
+
// Marker file written into the install destination after a successful copy.
|
|
624
|
+
// Lets `upgrade` and `status` (future command) tell which package version
|
|
625
|
+
// is currently installed without diffing every file. Hidden file with no
|
|
626
|
+
// `.md` / `.mdc` extension so the agent's skill loader ignores it.
|
|
627
|
+
const SENTINEL_FILENAME = '.ba-toolkit-version';
|
|
628
|
+
|
|
629
|
+
function readSentinel(destDir) {
|
|
630
|
+
const p = path.join(destDir, SENTINEL_FILENAME);
|
|
631
|
+
if (!fs.existsSync(p)) return null;
|
|
632
|
+
try {
|
|
633
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
634
|
+
} catch {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function writeSentinel(destDir) {
|
|
640
|
+
const payload = {
|
|
641
|
+
version: PKG.version,
|
|
642
|
+
installedAt: new Date().toISOString(),
|
|
643
|
+
};
|
|
644
|
+
fs.writeFileSync(
|
|
645
|
+
path.join(destDir, SENTINEL_FILENAME),
|
|
646
|
+
JSON.stringify(payload, null, 2) + '\n',
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Core install logic. Shared between `cmdInstall` (standalone), `cmdInit`
|
|
651
|
+
// (full setup), and `cmdUpgrade`. Returns true on success, false if the
|
|
652
|
+
// user declined to overwrite an existing destination. Pass `force: true`
|
|
653
|
+
// to skip the overwrite prompt — `cmdUpgrade` uses this because it has
|
|
654
|
+
// already wiped the destination and explicitly knows the overwrite is ok.
|
|
655
|
+
async function runInstall({ agentId, isGlobal, isProject, dryRun, showHeader = true, force = false }) {
|
|
504
656
|
const agent = AGENTS[agentId];
|
|
505
657
|
if (!agent) {
|
|
506
658
|
logError(`Unknown agent: ${agentId}`);
|
|
@@ -538,7 +690,7 @@ async function runInstall({ agentId, isGlobal, isProject, dryRun, showHeader = t
|
|
|
538
690
|
log(` format: ${agent.format === 'mdc' ? '.mdc (converted from SKILL.md)' : 'SKILL.md (native)'}`);
|
|
539
691
|
if (dryRun) log(' ' + yellow('mode: dry-run (no files will be written)'));
|
|
540
692
|
|
|
541
|
-
if (fs.existsSync(destDir) && !dryRun) {
|
|
693
|
+
if (fs.existsSync(destDir) && !dryRun && !force) {
|
|
542
694
|
const answer = await prompt(` ${destDir} already exists. Overwrite? (y/N): `);
|
|
543
695
|
if (answer.toLowerCase() !== 'y') {
|
|
544
696
|
log(' cancelled.');
|
|
@@ -555,6 +707,10 @@ async function runInstall({ agentId, isGlobal, isProject, dryRun, showHeader = t
|
|
|
555
707
|
process.exit(1);
|
|
556
708
|
}
|
|
557
709
|
|
|
710
|
+
if (!dryRun) {
|
|
711
|
+
writeSentinel(destDir);
|
|
712
|
+
}
|
|
713
|
+
|
|
558
714
|
log(' ' + green(`${dryRun ? 'would copy' : 'copied'} ${copied.length} files.`));
|
|
559
715
|
if (!dryRun && agent.format === 'mdc') {
|
|
560
716
|
log(' ' + gray('SKILL.md files converted to .mdc rule format.'));
|
|
@@ -563,8 +719,8 @@ async function runInstall({ agentId, isGlobal, isProject, dryRun, showHeader = t
|
|
|
563
719
|
}
|
|
564
720
|
|
|
565
721
|
async function cmdInstall(args) {
|
|
566
|
-
const agentId = args
|
|
567
|
-
if (!agentId
|
|
722
|
+
const agentId = stringFlag(args, 'for');
|
|
723
|
+
if (!agentId) {
|
|
568
724
|
logError('--for <agent> is required.');
|
|
569
725
|
log('Supported agents: ' + Object.keys(AGENTS).join(', '));
|
|
570
726
|
process.exit(1);
|
|
@@ -584,6 +740,271 @@ async function cmdInstall(args) {
|
|
|
584
740
|
log('');
|
|
585
741
|
}
|
|
586
742
|
|
|
743
|
+
// Resolve agent + scope (project vs global) into the target directory
|
|
744
|
+
// path. Shared validation for cmdUninstall — `runInstall` does its own
|
|
745
|
+
// version of this inline; both could be unified later.
|
|
746
|
+
function resolveAgentDestination({ agentId, isGlobal, isProject }) {
|
|
747
|
+
const agent = AGENTS[agentId];
|
|
748
|
+
if (!agent) {
|
|
749
|
+
logError(`Unknown agent: ${agentId}`);
|
|
750
|
+
log('Supported: ' + Object.keys(AGENTS).join(', '));
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
let effectiveGlobal = !!isGlobal;
|
|
754
|
+
if (!isGlobal && !isProject) {
|
|
755
|
+
effectiveGlobal = !agent.projectPath;
|
|
756
|
+
}
|
|
757
|
+
if (effectiveGlobal && !agent.globalPath) {
|
|
758
|
+
logError(`${agent.name} does not support --global install.`);
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
if (!effectiveGlobal && !agent.projectPath) {
|
|
762
|
+
logError(`${agent.name} does not support project-level install. Use --global.`);
|
|
763
|
+
process.exit(1);
|
|
764
|
+
}
|
|
765
|
+
const destDir = effectiveGlobal ? agent.globalPath : path.resolve(process.cwd(), agent.projectPath);
|
|
766
|
+
return { agent, destDir, effectiveGlobal };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function cmdStatus() {
|
|
770
|
+
log('');
|
|
771
|
+
log(' ' + cyan('BA Toolkit — Installation Status'));
|
|
772
|
+
log(' ' + cyan('================================'));
|
|
773
|
+
log('');
|
|
774
|
+
log(` package version: ${PKG.version}`);
|
|
775
|
+
log(` scanning from: ${process.cwd()}`);
|
|
776
|
+
log('');
|
|
777
|
+
|
|
778
|
+
// Walk every (agent × scope) combination and collect the ones whose
|
|
779
|
+
// destination directory actually exists. Project-scope paths resolve
|
|
780
|
+
// against the current working directory; global paths are absolute.
|
|
781
|
+
const rows = [];
|
|
782
|
+
for (const [agentId, agent] of Object.entries(AGENTS)) {
|
|
783
|
+
if (agent.projectPath) {
|
|
784
|
+
const projectDir = path.resolve(process.cwd(), agent.projectPath);
|
|
785
|
+
if (fs.existsSync(projectDir)) {
|
|
786
|
+
const sentinel = readSentinel(projectDir);
|
|
787
|
+
rows.push({
|
|
788
|
+
agentName: agent.name,
|
|
789
|
+
agentId,
|
|
790
|
+
scope: 'project',
|
|
791
|
+
path: projectDir,
|
|
792
|
+
version: sentinel ? sentinel.version : null,
|
|
793
|
+
installedAt: sentinel ? sentinel.installedAt : null,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (agent.globalPath) {
|
|
798
|
+
if (fs.existsSync(agent.globalPath)) {
|
|
799
|
+
const sentinel = readSentinel(agent.globalPath);
|
|
800
|
+
rows.push({
|
|
801
|
+
agentName: agent.name,
|
|
802
|
+
agentId,
|
|
803
|
+
scope: 'global',
|
|
804
|
+
path: agent.globalPath,
|
|
805
|
+
version: sentinel ? sentinel.version : null,
|
|
806
|
+
installedAt: sentinel ? sentinel.installedAt : null,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (rows.length === 0) {
|
|
813
|
+
log(' ' + gray('No BA Toolkit installations found in any known location.'));
|
|
814
|
+
log(' ' + gray("Run 'ba-toolkit install --for <agent>' to install one."));
|
|
815
|
+
log('');
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
log(` Found ${bold(rows.length)} installation${rows.length === 1 ? '' : 's'}:`);
|
|
820
|
+
log('');
|
|
821
|
+
|
|
822
|
+
for (const row of rows) {
|
|
823
|
+
let versionLabel;
|
|
824
|
+
if (!row.version) {
|
|
825
|
+
versionLabel = gray('(unknown — pre-1.4 install with no sentinel)');
|
|
826
|
+
} else if (row.version === PKG.version) {
|
|
827
|
+
versionLabel = green(row.version + ' (current)');
|
|
828
|
+
} else {
|
|
829
|
+
versionLabel = yellow(row.version + ' (outdated)');
|
|
830
|
+
}
|
|
831
|
+
log(` ${bold(row.agentName)} ${gray('(' + row.agentId + ', ' + row.scope + ')')}`);
|
|
832
|
+
log(` path: ${row.path}`);
|
|
833
|
+
log(` version: ${versionLabel}`);
|
|
834
|
+
if (row.installedAt) {
|
|
835
|
+
log(` installed: ${gray(row.installedAt)}`);
|
|
836
|
+
}
|
|
837
|
+
log('');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const stale = rows.filter((r) => !r.version || r.version !== PKG.version);
|
|
841
|
+
if (stale.length > 0) {
|
|
842
|
+
log(' ' + yellow(`${stale.length} installation${stale.length === 1 ? '' : 's'} not at version ${PKG.version}.`));
|
|
843
|
+
log(' ' + gray("Run 'ba-toolkit upgrade --for <agent>' to refresh."));
|
|
844
|
+
log('');
|
|
845
|
+
} else {
|
|
846
|
+
log(' ' + green('All installations are up to date.'));
|
|
847
|
+
log('');
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function cmdUpgrade(args) {
|
|
852
|
+
const agentId = stringFlag(args, 'for');
|
|
853
|
+
if (!agentId) {
|
|
854
|
+
logError('--for <agent> is required.');
|
|
855
|
+
log('Supported agents: ' + Object.keys(AGENTS).join(', '));
|
|
856
|
+
process.exit(1);
|
|
857
|
+
}
|
|
858
|
+
const { agent, destDir, effectiveGlobal } = resolveAgentDestination({
|
|
859
|
+
agentId,
|
|
860
|
+
isGlobal: !!args.flags.global,
|
|
861
|
+
isProject: !!args.flags.project,
|
|
862
|
+
});
|
|
863
|
+
const dryRun = !!args.flags['dry-run'];
|
|
864
|
+
|
|
865
|
+
log('');
|
|
866
|
+
log(' ' + cyan(`BA Toolkit — Upgrade for ${agent.name}`));
|
|
867
|
+
log(' ' + cyan('================================'));
|
|
868
|
+
log('');
|
|
869
|
+
log(` destination: ${destDir}`);
|
|
870
|
+
log(` scope: ${effectiveGlobal ? 'global (user-wide)' : 'project-level'}`);
|
|
871
|
+
|
|
872
|
+
if (!fs.existsSync(destDir)) {
|
|
873
|
+
log('');
|
|
874
|
+
log(' ' + gray(`No installation found at ${destDir}.`));
|
|
875
|
+
log(' ' + gray(`Run \`ba-toolkit install --for ${agentId}\` first.`));
|
|
876
|
+
log('');
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const sentinel = readSentinel(destDir);
|
|
881
|
+
const currentVersion = PKG.version;
|
|
882
|
+
const installedVersion = sentinel ? sentinel.version : null;
|
|
883
|
+
|
|
884
|
+
if (installedVersion === currentVersion) {
|
|
885
|
+
log(` installed: ${installedVersion} (current)`);
|
|
886
|
+
log(` package: ${currentVersion}`);
|
|
887
|
+
log('');
|
|
888
|
+
log(' ' + green('Already up to date.'));
|
|
889
|
+
log(' ' + gray(`To force a clean reinstall, run \`ba-toolkit install --for ${agentId}\`.`));
|
|
890
|
+
log('');
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
log(` installed: ${installedVersion || gray('(unknown — pre-1.4 install with no sentinel)')}`);
|
|
895
|
+
log(` package: ${currentVersion}`);
|
|
896
|
+
if (dryRun) log(' ' + yellow('mode: dry-run (no files will be written)'));
|
|
897
|
+
log('');
|
|
898
|
+
|
|
899
|
+
// Safety: same guard as cmdUninstall — never rmSync anything that
|
|
900
|
+
// doesn't look like a ba-toolkit folder.
|
|
901
|
+
if (path.basename(destDir) !== 'ba-toolkit') {
|
|
902
|
+
logError(`Refusing to upgrade suspicious destination (not a ba-toolkit folder): ${destDir}`);
|
|
903
|
+
process.exit(1);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Count files in the existing install for the dry-run preview.
|
|
907
|
+
if (dryRun) {
|
|
908
|
+
let existingCount = 0;
|
|
909
|
+
(function walk(d) {
|
|
910
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
911
|
+
const p = path.join(d, entry.name);
|
|
912
|
+
if (entry.isDirectory()) walk(p);
|
|
913
|
+
else existingCount++;
|
|
914
|
+
}
|
|
915
|
+
})(destDir);
|
|
916
|
+
log(' ' + yellow(`would remove ${existingCount} existing files`));
|
|
917
|
+
} else {
|
|
918
|
+
log(' ' + green('Removing previous install...'));
|
|
919
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const ok = await runInstall({
|
|
923
|
+
agentId,
|
|
924
|
+
isGlobal: effectiveGlobal,
|
|
925
|
+
isProject: !effectiveGlobal,
|
|
926
|
+
dryRun,
|
|
927
|
+
showHeader: false,
|
|
928
|
+
force: true,
|
|
929
|
+
});
|
|
930
|
+
log('');
|
|
931
|
+
if (ok && !dryRun) {
|
|
932
|
+
log(' ' + cyan(`Upgraded to ${currentVersion}.`));
|
|
933
|
+
log(' ' + yellow(agent.restartHint));
|
|
934
|
+
}
|
|
935
|
+
log('');
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function cmdUninstall(args) {
|
|
939
|
+
const agentId = stringFlag(args, 'for');
|
|
940
|
+
if (!agentId) {
|
|
941
|
+
logError('--for <agent> is required.');
|
|
942
|
+
log('Supported agents: ' + Object.keys(AGENTS).join(', '));
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}
|
|
945
|
+
const { agent, destDir, effectiveGlobal } = resolveAgentDestination({
|
|
946
|
+
agentId,
|
|
947
|
+
isGlobal: !!args.flags.global,
|
|
948
|
+
isProject: !!args.flags.project,
|
|
949
|
+
});
|
|
950
|
+
const dryRun = !!args.flags['dry-run'];
|
|
951
|
+
|
|
952
|
+
log('');
|
|
953
|
+
log(' ' + cyan(`BA Toolkit — Uninstall from ${agent.name}`));
|
|
954
|
+
log(' ' + cyan('================================'));
|
|
955
|
+
log('');
|
|
956
|
+
log(` destination: ${destDir}`);
|
|
957
|
+
log(` scope: ${effectiveGlobal ? 'global (user-wide)' : 'project-level'}`);
|
|
958
|
+
if (dryRun) log(' ' + yellow('mode: dry-run (no files will be removed)'));
|
|
959
|
+
log('');
|
|
960
|
+
|
|
961
|
+
// Safety: this is the only place in the CLI that calls fs.rmSync with
|
|
962
|
+
// recursive: true. Refuse to proceed unless the destination is clearly
|
|
963
|
+
// a ba-toolkit folder (the install paths in AGENTS all end in
|
|
964
|
+
// `ba-toolkit/`). Without this check, a corrupted AGENTS entry or a
|
|
965
|
+
// future bug could turn this into `rm -rf $HOME`.
|
|
966
|
+
if (path.basename(destDir) !== 'ba-toolkit') {
|
|
967
|
+
logError(`Refusing to remove suspicious destination (not a ba-toolkit folder): ${destDir}`);
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (!fs.existsSync(destDir)) {
|
|
972
|
+
log(' ' + gray(`Nothing to uninstall — ${destDir} does not exist.`));
|
|
973
|
+
log('');
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Count files for the preview message and final confirmation.
|
|
978
|
+
let fileCount = 0;
|
|
979
|
+
(function walk(d) {
|
|
980
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
981
|
+
const p = path.join(d, entry.name);
|
|
982
|
+
if (entry.isDirectory()) walk(p);
|
|
983
|
+
else fileCount++;
|
|
984
|
+
}
|
|
985
|
+
})(destDir);
|
|
986
|
+
|
|
987
|
+
log(` Found ${bold(fileCount)} files in the destination.`);
|
|
988
|
+
|
|
989
|
+
if (dryRun) {
|
|
990
|
+
log(' ' + yellow(`would remove ${fileCount} files from ${destDir}.`));
|
|
991
|
+
log('');
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
log('');
|
|
996
|
+
const answer = await prompt(` Remove ${destDir}? (y/N): `);
|
|
997
|
+
if (answer.toLowerCase() !== 'y') {
|
|
998
|
+
log(' Cancelled.');
|
|
999
|
+
log('');
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
1003
|
+
log(' ' + green(`Removed ${fileCount} files from ${destDir}.`));
|
|
1004
|
+
log(' ' + yellow(agent.restartHint));
|
|
1005
|
+
log('');
|
|
1006
|
+
}
|
|
1007
|
+
|
|
587
1008
|
function cmdHelp() {
|
|
588
1009
|
log(`${bold('ba-toolkit')} v${PKG.version} — AI-powered Business Analyst pipeline
|
|
589
1010
|
|
|
@@ -597,25 +1018,37 @@ ${bold('COMMANDS')}
|
|
|
597
1018
|
skills into the chosen agent's directory.
|
|
598
1019
|
install --for <agent> Install (or re-install) skills into an
|
|
599
1020
|
agent's directory without creating a project.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
--
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
--
|
|
616
|
-
|
|
617
|
-
--
|
|
618
|
-
|
|
1021
|
+
uninstall --for <agent> Remove BA Toolkit skills from an agent's
|
|
1022
|
+
directory. Asks for confirmation before
|
|
1023
|
+
deleting; supports --dry-run.
|
|
1024
|
+
upgrade --for <agent> Refresh skills after a toolkit version bump.
|
|
1025
|
+
Compares the installed version sentinel
|
|
1026
|
+
against the package version, wipes the old
|
|
1027
|
+
install on mismatch, and re-runs install.
|
|
1028
|
+
Aliased as 'update'.
|
|
1029
|
+
status Scan all known install locations for every
|
|
1030
|
+
supported agent (project + global) and
|
|
1031
|
+
report which versions are installed where.
|
|
1032
|
+
Read-only; no flags.
|
|
1033
|
+
|
|
1034
|
+
${bold('OPTIONS')}
|
|
1035
|
+
--name <name> init only — skip the project name prompt
|
|
1036
|
+
--slug <slug> init only — skip the slug prompt (auto-derived
|
|
1037
|
+
from name)
|
|
1038
|
+
--domain <id> init only — skip the domain menu
|
|
1039
|
+
(e.g. saas, fintech)
|
|
1040
|
+
--no-install init only — create the project structure
|
|
1041
|
+
without installing skills (useful for CI)
|
|
1042
|
+
--for <agent> install/uninstall/upgrade — pick the target
|
|
1043
|
+
agent. One of: ${Object.keys(AGENTS).join(', ')}.
|
|
1044
|
+
init also accepts this to skip the agent menu.
|
|
1045
|
+
--global install/uninstall/upgrade — target the
|
|
1046
|
+
user-wide install
|
|
1047
|
+
--project install/uninstall/upgrade — target the
|
|
1048
|
+
project-level install (default when the
|
|
1049
|
+
agent supports it)
|
|
1050
|
+
--dry-run init/install/uninstall/upgrade — preview
|
|
1051
|
+
without writing or removing files
|
|
619
1052
|
|
|
620
1053
|
${bold('GENERAL OPTIONS')}
|
|
621
1054
|
--version, -v Print version and exit
|
|
@@ -635,6 +1068,18 @@ ${bold('EXAMPLES')}
|
|
|
635
1068
|
ba-toolkit install --for claude-code
|
|
636
1069
|
ba-toolkit install --for cursor --dry-run
|
|
637
1070
|
|
|
1071
|
+
# Remove skills from an agent (asks for confirmation).
|
|
1072
|
+
ba-toolkit uninstall --for claude-code
|
|
1073
|
+
ba-toolkit uninstall --for claude-code --global
|
|
1074
|
+
ba-toolkit uninstall --for cursor --dry-run
|
|
1075
|
+
|
|
1076
|
+
# After 'npm update -g @kudusov.takhir/ba-toolkit', refresh the skills.
|
|
1077
|
+
ba-toolkit upgrade --for claude-code
|
|
1078
|
+
ba-toolkit upgrade --for cursor --dry-run
|
|
1079
|
+
|
|
1080
|
+
# See where (and which version) BA Toolkit is installed.
|
|
1081
|
+
ba-toolkit status
|
|
1082
|
+
|
|
638
1083
|
${bold('LEARN MORE')}
|
|
639
1084
|
https://github.com/TakhirKudusov/ba-toolkit
|
|
640
1085
|
`);
|
|
@@ -644,6 +1089,7 @@ ${bold('LEARN MORE')}
|
|
|
644
1089
|
|
|
645
1090
|
async function main() {
|
|
646
1091
|
const args = parseArgs(process.argv.slice(2));
|
|
1092
|
+
validateFlags(args);
|
|
647
1093
|
|
|
648
1094
|
if (args.flags.version || args.flags.v) {
|
|
649
1095
|
log(PKG.version);
|
|
@@ -663,6 +1109,16 @@ async function main() {
|
|
|
663
1109
|
case 'install':
|
|
664
1110
|
await cmdInstall(args);
|
|
665
1111
|
break;
|
|
1112
|
+
case 'uninstall':
|
|
1113
|
+
await cmdUninstall(args);
|
|
1114
|
+
break;
|
|
1115
|
+
case 'upgrade':
|
|
1116
|
+
case 'update':
|
|
1117
|
+
await cmdUpgrade(args);
|
|
1118
|
+
break;
|
|
1119
|
+
case 'status':
|
|
1120
|
+
cmdStatus();
|
|
1121
|
+
break;
|
|
666
1122
|
case 'help':
|
|
667
1123
|
cmdHelp();
|
|
668
1124
|
break;
|
|
@@ -673,26 +1129,49 @@ async function main() {
|
|
|
673
1129
|
}
|
|
674
1130
|
}
|
|
675
1131
|
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1132
|
+
// Exports for tests. Pure functions only — anything that prompts or
|
|
1133
|
+
// touches the filesystem stays internal. The bin file is still
|
|
1134
|
+
// runnable as a CLI; the `require.main === module` guard below
|
|
1135
|
+
// prevents `main()` from firing when the file is loaded as a module
|
|
1136
|
+
// from the test runner.
|
|
1137
|
+
module.exports = {
|
|
1138
|
+
sanitiseSlug,
|
|
1139
|
+
parseArgs,
|
|
1140
|
+
resolveDomain,
|
|
1141
|
+
resolveAgent,
|
|
1142
|
+
stringFlag,
|
|
1143
|
+
levenshtein,
|
|
1144
|
+
closestMatch,
|
|
1145
|
+
parseSkillFrontmatter,
|
|
1146
|
+
readSentinel,
|
|
1147
|
+
renderAgentsMd,
|
|
1148
|
+
KNOWN_FLAGS,
|
|
1149
|
+
DOMAINS,
|
|
1150
|
+
AGENTS,
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
if (require.main === module) {
|
|
1154
|
+
// Clean exit on Ctrl+C: print on a fresh line so we don't append to a
|
|
1155
|
+
// half-typed prompt, close the readline interface so the terminal is
|
|
1156
|
+
// returned to a sane state, then exit with the conventional 130 code.
|
|
1157
|
+
process.on('SIGINT', () => {
|
|
1158
|
+
console.log('\n ' + yellow('Cancelled.'));
|
|
690
1159
|
closeReadline();
|
|
691
|
-
|
|
692
|
-
logError('Input stream closed before all prompts could be answered.');
|
|
693
|
-
log('Pass remaining values as flags (e.g. --name, --domain, --for) or run interactively.');
|
|
694
|
-
process.exit(1);
|
|
695
|
-
}
|
|
696
|
-
logError(err && (err.stack || err.message) || String(err));
|
|
697
|
-
process.exit(1);
|
|
1160
|
+
process.exit(130);
|
|
698
1161
|
});
|
|
1162
|
+
|
|
1163
|
+
main()
|
|
1164
|
+
.then(() => {
|
|
1165
|
+
closeReadline();
|
|
1166
|
+
})
|
|
1167
|
+
.catch((err) => {
|
|
1168
|
+
closeReadline();
|
|
1169
|
+
if (err && err.code === 'INPUT_CLOSED') {
|
|
1170
|
+
logError('Input stream closed before all prompts could be answered.');
|
|
1171
|
+
log('Pass remaining values as flags (e.g. --name, --domain, --for) or run interactively.');
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
}
|
|
1174
|
+
logError(err && (err.stack || err.message) || String(err));
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
});
|
|
1177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kudusov.takhir/ba-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "AI-powered Business Analyst pipeline — 21 skills from project brief to development handoff. Works with Claude Code, Codex CLI, Gemini CLI, Cursor, and Windsurf.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"business-analyst",
|
|
@@ -43,6 +43,6 @@
|
|
|
43
43
|
"node": ">=18"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
|
-
"test": "node
|
|
46
|
+
"test": "node --test test/cli.test.js"
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# BA Toolkit — Project Context
|
|
2
|
+
|
|
3
|
+
> Auto-generated by `ba-toolkit init` on [DATE]. Updated automatically by /brief and /srs.
|
|
4
|
+
|
|
5
|
+
## Active Project
|
|
6
|
+
|
|
7
|
+
**Project:** [NAME]
|
|
8
|
+
**Slug:** [SLUG]
|
|
9
|
+
**Domain:** [DOMAIN]
|
|
10
|
+
**Language:** English
|
|
11
|
+
**Output folder:** output/[SLUG]/
|
|
12
|
+
|
|
13
|
+
## Pipeline Status
|
|
14
|
+
|
|
15
|
+
| Stage | Skill | Status | File |
|
|
16
|
+
|-------|-------|--------|------|
|
|
17
|
+
| 0 | /principles | ⬜ Not started | — |
|
|
18
|
+
| 1 | /brief | ⬜ Not started | — |
|
|
19
|
+
| 2 | /srs | ⬜ Not started | — |
|
|
20
|
+
| 3 | /stories | ⬜ Not started | — |
|
|
21
|
+
| 4 | /usecases | ⬜ Not started | — |
|
|
22
|
+
| 5 | /ac | ⬜ Not started | — |
|
|
23
|
+
| 6 | /nfr | ⬜ Not started | — |
|
|
24
|
+
| 7 | /datadict | ⬜ Not started | — |
|
|
25
|
+
| 7a | /research | ⬜ Not started | — |
|
|
26
|
+
| 8 | /apicontract | ⬜ Not started | — |
|
|
27
|
+
| 9 | /wireframes | ⬜ Not started | — |
|
|
28
|
+
| 10 | /scenarios | ⬜ Not started | — |
|
|
29
|
+
| 11 | /handoff | ⬜ Not started | — |
|
|
30
|
+
|
|
31
|
+
## Cross-cutting Tools
|
|
32
|
+
|
|
33
|
+
Utilities available throughout the pipeline. No fixed stage — invoke whenever they help. See README.md for the prerequisites of each.
|
|
34
|
+
|
|
35
|
+
| Tool | Purpose |
|
|
36
|
+
|------|---------|
|
|
37
|
+
| /trace | Traceability Matrix + coverage gaps |
|
|
38
|
+
| /clarify [focus] | Targeted ambiguity resolution for any artifact |
|
|
39
|
+
| /analyze | Cross-artifact quality report with severity-rated findings |
|
|
40
|
+
| /estimate | Effort estimation — Fibonacci SP, T-shirt sizes, or person-days |
|
|
41
|
+
| /glossary | Unified project glossary with terminology drift detection |
|
|
42
|
+
| /export [format] | Export User Stories to Jira / GitHub Issues / Linear / CSV |
|
|
43
|
+
| /risk | Risk register — probability × impact matrix, mitigation per risk |
|
|
44
|
+
| /sprint | Sprint plan — stories grouped by velocity and capacity with sprint goals |
|
|
45
|
+
|
|
46
|
+
## Key Constraints
|
|
47
|
+
|
|
48
|
+
- Domain: [DOMAIN]
|
|
49
|
+
- (Add constraints after /brief completes)
|
|
50
|
+
|
|
51
|
+
## Key Stakeholder Roles
|
|
52
|
+
|
|
53
|
+
- (Populated after /srs completes)
|
|
54
|
+
|
|
55
|
+
## Open Questions
|
|
56
|
+
|
|
57
|
+
- (None yet)
|