@kudusov.takhir/ba-toolkit 1.4.0 → 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 +29 -1
- package/bin/ba-toolkit.js +282 -149
- package/package.json +2 -2
- package/skills/references/templates/agents-template.md +57 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,33 @@ 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
|
+
|
|
14
41
|
## [1.4.0] — 2026-04-08
|
|
15
42
|
|
|
16
43
|
### Added
|
|
@@ -267,7 +294,8 @@ CI scripts that relied on the old behaviour (`init` creates files only, `install
|
|
|
267
294
|
|
|
268
295
|
---
|
|
269
296
|
|
|
270
|
-
[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
|
|
271
299
|
[1.4.0]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.3.2...v1.4.0
|
|
272
300
|
[1.3.2]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.3.1...v1.3.2
|
|
273
301
|
[1.3.1]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.3.0...v1.3.1
|
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.
|
|
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');
|
|
300
431
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
- Domain: ${domain}
|
|
315
|
-
- (Add constraints after /brief completes)
|
|
316
|
-
|
|
317
|
-
## Key Stakeholder Roles
|
|
318
|
-
|
|
319
|
-
- (Populated after /srs completes)
|
|
320
|
-
|
|
321
|
-
## Open Questions
|
|
322
|
-
|
|
323
|
-
- (None yet)
|
|
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}]: `);
|
|
@@ -596,8 +719,8 @@ async function runInstall({ agentId, isGlobal, isProject, dryRun, showHeader = t
|
|
|
596
719
|
}
|
|
597
720
|
|
|
598
721
|
async function cmdInstall(args) {
|
|
599
|
-
const agentId = args
|
|
600
|
-
if (!agentId
|
|
722
|
+
const agentId = stringFlag(args, 'for');
|
|
723
|
+
if (!agentId) {
|
|
601
724
|
logError('--for <agent> is required.');
|
|
602
725
|
log('Supported agents: ' + Object.keys(AGENTS).join(', '));
|
|
603
726
|
process.exit(1);
|
|
@@ -726,8 +849,8 @@ function cmdStatus() {
|
|
|
726
849
|
}
|
|
727
850
|
|
|
728
851
|
async function cmdUpgrade(args) {
|
|
729
|
-
const agentId = args
|
|
730
|
-
if (!agentId
|
|
852
|
+
const agentId = stringFlag(args, 'for');
|
|
853
|
+
if (!agentId) {
|
|
731
854
|
logError('--for <agent> is required.');
|
|
732
855
|
log('Supported agents: ' + Object.keys(AGENTS).join(', '));
|
|
733
856
|
process.exit(1);
|
|
@@ -813,8 +936,8 @@ async function cmdUpgrade(args) {
|
|
|
813
936
|
}
|
|
814
937
|
|
|
815
938
|
async function cmdUninstall(args) {
|
|
816
|
-
const agentId = args
|
|
817
|
-
if (!agentId
|
|
939
|
+
const agentId = stringFlag(args, 'for');
|
|
940
|
+
if (!agentId) {
|
|
818
941
|
logError('--for <agent> is required.');
|
|
819
942
|
log('Supported agents: ' + Object.keys(AGENTS).join(', '));
|
|
820
943
|
process.exit(1);
|
|
@@ -908,38 +1031,24 @@ ${bold('COMMANDS')}
|
|
|
908
1031
|
report which versions are installed where.
|
|
909
1032
|
Read-only; no flags.
|
|
910
1033
|
|
|
911
|
-
${bold('
|
|
912
|
-
--name <name>
|
|
913
|
-
--slug <slug>
|
|
914
|
-
|
|
915
|
-
--
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
--
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
--
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
--
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
${bold('UNINSTALL OPTIONS')}
|
|
931
|
-
--for <agent> One of: ${Object.keys(AGENTS).join(', ')}
|
|
932
|
-
--global Remove the user-wide install
|
|
933
|
-
--project Remove the project-level install
|
|
934
|
-
(default when the agent supports it)
|
|
935
|
-
--dry-run Preview without removing files
|
|
936
|
-
|
|
937
|
-
${bold('UPGRADE OPTIONS')}
|
|
938
|
-
--for <agent> One of: ${Object.keys(AGENTS).join(', ')}
|
|
939
|
-
--global Upgrade the user-wide install
|
|
940
|
-
--project Upgrade the project-level install
|
|
941
|
-
(default when the agent supports it)
|
|
942
|
-
--dry-run Preview without writing or removing files
|
|
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
|
|
943
1052
|
|
|
944
1053
|
${bold('GENERAL OPTIONS')}
|
|
945
1054
|
--version, -v Print version and exit
|
|
@@ -980,6 +1089,7 @@ ${bold('LEARN MORE')}
|
|
|
980
1089
|
|
|
981
1090
|
async function main() {
|
|
982
1091
|
const args = parseArgs(process.argv.slice(2));
|
|
1092
|
+
validateFlags(args);
|
|
983
1093
|
|
|
984
1094
|
if (args.flags.version || args.flags.v) {
|
|
985
1095
|
log(PKG.version);
|
|
@@ -1019,26 +1129,49 @@ async function main() {
|
|
|
1019
1129
|
}
|
|
1020
1130
|
}
|
|
1021
1131
|
|
|
1022
|
-
//
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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.'));
|
|
1036
1159
|
closeReadline();
|
|
1037
|
-
|
|
1038
|
-
logError('Input stream closed before all prompts could be answered.');
|
|
1039
|
-
log('Pass remaining values as flags (e.g. --name, --domain, --for) or run interactively.');
|
|
1040
|
-
process.exit(1);
|
|
1041
|
-
}
|
|
1042
|
-
logError(err && (err.stack || err.message) || String(err));
|
|
1043
|
-
process.exit(1);
|
|
1160
|
+
process.exit(130);
|
|
1044
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)
|