@kudusov.takhir/ba-toolkit 1.3.2 → 1.4.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 CHANGED
@@ -11,6 +11,27 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
11
11
 
12
12
  ---
13
13
 
14
+ ## [1.4.0] — 2026-04-08
15
+
16
+ ### Added
17
+
18
+ - **`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`.
19
+ - **`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.
20
+ - **`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.
21
+ - **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.
22
+ - **`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.
23
+
24
+ ### Fixed
25
+
26
+ - **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.
27
+
28
+ ### Internal
29
+
30
+ - **`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.
31
+ - **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.
32
+
33
+ ---
34
+
14
35
  ## [1.3.2] — 2026-04-08
15
36
 
16
37
  ### Fixed
@@ -246,7 +267,8 @@ CI scripts that relied on the old behaviour (`init` creates files only, `install
246
267
 
247
268
  ---
248
269
 
249
- [Unreleased]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.3.2...HEAD
270
+ [Unreleased]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.4.0...HEAD
271
+ [1.4.0]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.3.2...v1.4.0
250
272
  [1.3.2]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.3.1...v1.3.2
251
273
  [1.3.1]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.3.0...v1.3.1
252
274
  [1.3.0]: https://github.com/TakhirKudusov/ba-toolkit/compare/v1.2.5...v1.3.0
package/bin/ba-toolkit.js CHANGED
@@ -497,10 +497,39 @@ async function cmdInit(args) {
497
497
  log('');
498
498
  }
499
499
 
500
- // Core install logic. Shared between `cmdInstall` (standalone) and `cmdInit`
501
- // (full setup). Returns true on success, false if the user declined to
502
- // overwrite an existing destination.
503
- async function runInstall({ agentId, isGlobal, isProject, dryRun, showHeader = true }) {
500
+ // Marker file written into the install destination after a successful copy.
501
+ // Lets `upgrade` and `status` (future command) tell which package version
502
+ // is currently installed without diffing every file. Hidden file with no
503
+ // `.md` / `.mdc` extension so the agent's skill loader ignores it.
504
+ const SENTINEL_FILENAME = '.ba-toolkit-version';
505
+
506
+ function readSentinel(destDir) {
507
+ const p = path.join(destDir, SENTINEL_FILENAME);
508
+ if (!fs.existsSync(p)) return null;
509
+ try {
510
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
511
+ } catch {
512
+ return null;
513
+ }
514
+ }
515
+
516
+ function writeSentinel(destDir) {
517
+ const payload = {
518
+ version: PKG.version,
519
+ installedAt: new Date().toISOString(),
520
+ };
521
+ fs.writeFileSync(
522
+ path.join(destDir, SENTINEL_FILENAME),
523
+ JSON.stringify(payload, null, 2) + '\n',
524
+ );
525
+ }
526
+
527
+ // Core install logic. Shared between `cmdInstall` (standalone), `cmdInit`
528
+ // (full setup), and `cmdUpgrade`. Returns true on success, false if the
529
+ // user declined to overwrite an existing destination. Pass `force: true`
530
+ // to skip the overwrite prompt — `cmdUpgrade` uses this because it has
531
+ // already wiped the destination and explicitly knows the overwrite is ok.
532
+ async function runInstall({ agentId, isGlobal, isProject, dryRun, showHeader = true, force = false }) {
504
533
  const agent = AGENTS[agentId];
505
534
  if (!agent) {
506
535
  logError(`Unknown agent: ${agentId}`);
@@ -538,7 +567,7 @@ async function runInstall({ agentId, isGlobal, isProject, dryRun, showHeader = t
538
567
  log(` format: ${agent.format === 'mdc' ? '.mdc (converted from SKILL.md)' : 'SKILL.md (native)'}`);
539
568
  if (dryRun) log(' ' + yellow('mode: dry-run (no files will be written)'));
540
569
 
541
- if (fs.existsSync(destDir) && !dryRun) {
570
+ if (fs.existsSync(destDir) && !dryRun && !force) {
542
571
  const answer = await prompt(` ${destDir} already exists. Overwrite? (y/N): `);
543
572
  if (answer.toLowerCase() !== 'y') {
544
573
  log(' cancelled.');
@@ -555,6 +584,10 @@ async function runInstall({ agentId, isGlobal, isProject, dryRun, showHeader = t
555
584
  process.exit(1);
556
585
  }
557
586
 
587
+ if (!dryRun) {
588
+ writeSentinel(destDir);
589
+ }
590
+
558
591
  log(' ' + green(`${dryRun ? 'would copy' : 'copied'} ${copied.length} files.`));
559
592
  if (!dryRun && agent.format === 'mdc') {
560
593
  log(' ' + gray('SKILL.md files converted to .mdc rule format.'));
@@ -584,6 +617,271 @@ async function cmdInstall(args) {
584
617
  log('');
585
618
  }
586
619
 
620
+ // Resolve agent + scope (project vs global) into the target directory
621
+ // path. Shared validation for cmdUninstall — `runInstall` does its own
622
+ // version of this inline; both could be unified later.
623
+ function resolveAgentDestination({ agentId, isGlobal, isProject }) {
624
+ const agent = AGENTS[agentId];
625
+ if (!agent) {
626
+ logError(`Unknown agent: ${agentId}`);
627
+ log('Supported: ' + Object.keys(AGENTS).join(', '));
628
+ process.exit(1);
629
+ }
630
+ let effectiveGlobal = !!isGlobal;
631
+ if (!isGlobal && !isProject) {
632
+ effectiveGlobal = !agent.projectPath;
633
+ }
634
+ if (effectiveGlobal && !agent.globalPath) {
635
+ logError(`${agent.name} does not support --global install.`);
636
+ process.exit(1);
637
+ }
638
+ if (!effectiveGlobal && !agent.projectPath) {
639
+ logError(`${agent.name} does not support project-level install. Use --global.`);
640
+ process.exit(1);
641
+ }
642
+ const destDir = effectiveGlobal ? agent.globalPath : path.resolve(process.cwd(), agent.projectPath);
643
+ return { agent, destDir, effectiveGlobal };
644
+ }
645
+
646
+ function cmdStatus() {
647
+ log('');
648
+ log(' ' + cyan('BA Toolkit — Installation Status'));
649
+ log(' ' + cyan('================================'));
650
+ log('');
651
+ log(` package version: ${PKG.version}`);
652
+ log(` scanning from: ${process.cwd()}`);
653
+ log('');
654
+
655
+ // Walk every (agent × scope) combination and collect the ones whose
656
+ // destination directory actually exists. Project-scope paths resolve
657
+ // against the current working directory; global paths are absolute.
658
+ const rows = [];
659
+ for (const [agentId, agent] of Object.entries(AGENTS)) {
660
+ if (agent.projectPath) {
661
+ const projectDir = path.resolve(process.cwd(), agent.projectPath);
662
+ if (fs.existsSync(projectDir)) {
663
+ const sentinel = readSentinel(projectDir);
664
+ rows.push({
665
+ agentName: agent.name,
666
+ agentId,
667
+ scope: 'project',
668
+ path: projectDir,
669
+ version: sentinel ? sentinel.version : null,
670
+ installedAt: sentinel ? sentinel.installedAt : null,
671
+ });
672
+ }
673
+ }
674
+ if (agent.globalPath) {
675
+ if (fs.existsSync(agent.globalPath)) {
676
+ const sentinel = readSentinel(agent.globalPath);
677
+ rows.push({
678
+ agentName: agent.name,
679
+ agentId,
680
+ scope: 'global',
681
+ path: agent.globalPath,
682
+ version: sentinel ? sentinel.version : null,
683
+ installedAt: sentinel ? sentinel.installedAt : null,
684
+ });
685
+ }
686
+ }
687
+ }
688
+
689
+ if (rows.length === 0) {
690
+ log(' ' + gray('No BA Toolkit installations found in any known location.'));
691
+ log(' ' + gray("Run 'ba-toolkit install --for <agent>' to install one."));
692
+ log('');
693
+ return;
694
+ }
695
+
696
+ log(` Found ${bold(rows.length)} installation${rows.length === 1 ? '' : 's'}:`);
697
+ log('');
698
+
699
+ for (const row of rows) {
700
+ let versionLabel;
701
+ if (!row.version) {
702
+ versionLabel = gray('(unknown — pre-1.4 install with no sentinel)');
703
+ } else if (row.version === PKG.version) {
704
+ versionLabel = green(row.version + ' (current)');
705
+ } else {
706
+ versionLabel = yellow(row.version + ' (outdated)');
707
+ }
708
+ log(` ${bold(row.agentName)} ${gray('(' + row.agentId + ', ' + row.scope + ')')}`);
709
+ log(` path: ${row.path}`);
710
+ log(` version: ${versionLabel}`);
711
+ if (row.installedAt) {
712
+ log(` installed: ${gray(row.installedAt)}`);
713
+ }
714
+ log('');
715
+ }
716
+
717
+ const stale = rows.filter((r) => !r.version || r.version !== PKG.version);
718
+ if (stale.length > 0) {
719
+ log(' ' + yellow(`${stale.length} installation${stale.length === 1 ? '' : 's'} not at version ${PKG.version}.`));
720
+ log(' ' + gray("Run 'ba-toolkit upgrade --for <agent>' to refresh."));
721
+ log('');
722
+ } else {
723
+ log(' ' + green('All installations are up to date.'));
724
+ log('');
725
+ }
726
+ }
727
+
728
+ async function cmdUpgrade(args) {
729
+ const agentId = args.flags.for;
730
+ if (!agentId || agentId === true) {
731
+ logError('--for <agent> is required.');
732
+ log('Supported agents: ' + Object.keys(AGENTS).join(', '));
733
+ process.exit(1);
734
+ }
735
+ const { agent, destDir, effectiveGlobal } = resolveAgentDestination({
736
+ agentId,
737
+ isGlobal: !!args.flags.global,
738
+ isProject: !!args.flags.project,
739
+ });
740
+ const dryRun = !!args.flags['dry-run'];
741
+
742
+ log('');
743
+ log(' ' + cyan(`BA Toolkit — Upgrade for ${agent.name}`));
744
+ log(' ' + cyan('================================'));
745
+ log('');
746
+ log(` destination: ${destDir}`);
747
+ log(` scope: ${effectiveGlobal ? 'global (user-wide)' : 'project-level'}`);
748
+
749
+ if (!fs.existsSync(destDir)) {
750
+ log('');
751
+ log(' ' + gray(`No installation found at ${destDir}.`));
752
+ log(' ' + gray(`Run \`ba-toolkit install --for ${agentId}\` first.`));
753
+ log('');
754
+ return;
755
+ }
756
+
757
+ const sentinel = readSentinel(destDir);
758
+ const currentVersion = PKG.version;
759
+ const installedVersion = sentinel ? sentinel.version : null;
760
+
761
+ if (installedVersion === currentVersion) {
762
+ log(` installed: ${installedVersion} (current)`);
763
+ log(` package: ${currentVersion}`);
764
+ log('');
765
+ log(' ' + green('Already up to date.'));
766
+ log(' ' + gray(`To force a clean reinstall, run \`ba-toolkit install --for ${agentId}\`.`));
767
+ log('');
768
+ return;
769
+ }
770
+
771
+ log(` installed: ${installedVersion || gray('(unknown — pre-1.4 install with no sentinel)')}`);
772
+ log(` package: ${currentVersion}`);
773
+ if (dryRun) log(' ' + yellow('mode: dry-run (no files will be written)'));
774
+ log('');
775
+
776
+ // Safety: same guard as cmdUninstall — never rmSync anything that
777
+ // doesn't look like a ba-toolkit folder.
778
+ if (path.basename(destDir) !== 'ba-toolkit') {
779
+ logError(`Refusing to upgrade suspicious destination (not a ba-toolkit folder): ${destDir}`);
780
+ process.exit(1);
781
+ }
782
+
783
+ // Count files in the existing install for the dry-run preview.
784
+ if (dryRun) {
785
+ let existingCount = 0;
786
+ (function walk(d) {
787
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
788
+ const p = path.join(d, entry.name);
789
+ if (entry.isDirectory()) walk(p);
790
+ else existingCount++;
791
+ }
792
+ })(destDir);
793
+ log(' ' + yellow(`would remove ${existingCount} existing files`));
794
+ } else {
795
+ log(' ' + green('Removing previous install...'));
796
+ fs.rmSync(destDir, { recursive: true, force: true });
797
+ }
798
+
799
+ const ok = await runInstall({
800
+ agentId,
801
+ isGlobal: effectiveGlobal,
802
+ isProject: !effectiveGlobal,
803
+ dryRun,
804
+ showHeader: false,
805
+ force: true,
806
+ });
807
+ log('');
808
+ if (ok && !dryRun) {
809
+ log(' ' + cyan(`Upgraded to ${currentVersion}.`));
810
+ log(' ' + yellow(agent.restartHint));
811
+ }
812
+ log('');
813
+ }
814
+
815
+ async function cmdUninstall(args) {
816
+ const agentId = args.flags.for;
817
+ if (!agentId || agentId === true) {
818
+ logError('--for <agent> is required.');
819
+ log('Supported agents: ' + Object.keys(AGENTS).join(', '));
820
+ process.exit(1);
821
+ }
822
+ const { agent, destDir, effectiveGlobal } = resolveAgentDestination({
823
+ agentId,
824
+ isGlobal: !!args.flags.global,
825
+ isProject: !!args.flags.project,
826
+ });
827
+ const dryRun = !!args.flags['dry-run'];
828
+
829
+ log('');
830
+ log(' ' + cyan(`BA Toolkit — Uninstall from ${agent.name}`));
831
+ log(' ' + cyan('================================'));
832
+ log('');
833
+ log(` destination: ${destDir}`);
834
+ log(` scope: ${effectiveGlobal ? 'global (user-wide)' : 'project-level'}`);
835
+ if (dryRun) log(' ' + yellow('mode: dry-run (no files will be removed)'));
836
+ log('');
837
+
838
+ // Safety: this is the only place in the CLI that calls fs.rmSync with
839
+ // recursive: true. Refuse to proceed unless the destination is clearly
840
+ // a ba-toolkit folder (the install paths in AGENTS all end in
841
+ // `ba-toolkit/`). Without this check, a corrupted AGENTS entry or a
842
+ // future bug could turn this into `rm -rf $HOME`.
843
+ if (path.basename(destDir) !== 'ba-toolkit') {
844
+ logError(`Refusing to remove suspicious destination (not a ba-toolkit folder): ${destDir}`);
845
+ process.exit(1);
846
+ }
847
+
848
+ if (!fs.existsSync(destDir)) {
849
+ log(' ' + gray(`Nothing to uninstall — ${destDir} does not exist.`));
850
+ log('');
851
+ return;
852
+ }
853
+
854
+ // Count files for the preview message and final confirmation.
855
+ let fileCount = 0;
856
+ (function walk(d) {
857
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
858
+ const p = path.join(d, entry.name);
859
+ if (entry.isDirectory()) walk(p);
860
+ else fileCount++;
861
+ }
862
+ })(destDir);
863
+
864
+ log(` Found ${bold(fileCount)} files in the destination.`);
865
+
866
+ if (dryRun) {
867
+ log(' ' + yellow(`would remove ${fileCount} files from ${destDir}.`));
868
+ log('');
869
+ return;
870
+ }
871
+
872
+ log('');
873
+ const answer = await prompt(` Remove ${destDir}? (y/N): `);
874
+ if (answer.toLowerCase() !== 'y') {
875
+ log(' Cancelled.');
876
+ log('');
877
+ return;
878
+ }
879
+ fs.rmSync(destDir, { recursive: true, force: true });
880
+ log(' ' + green(`Removed ${fileCount} files from ${destDir}.`));
881
+ log(' ' + yellow(agent.restartHint));
882
+ log('');
883
+ }
884
+
587
885
  function cmdHelp() {
588
886
  log(`${bold('ba-toolkit')} v${PKG.version} — AI-powered Business Analyst pipeline
589
887
 
@@ -597,6 +895,18 @@ ${bold('COMMANDS')}
597
895
  skills into the chosen agent's directory.
598
896
  install --for <agent> Install (or re-install) skills into an
599
897
  agent's directory without creating a project.
898
+ uninstall --for <agent> Remove BA Toolkit skills from an agent's
899
+ directory. Asks for confirmation before
900
+ deleting; supports --dry-run.
901
+ upgrade --for <agent> Refresh skills after a toolkit version bump.
902
+ Compares the installed version sentinel
903
+ against the package version, wipes the old
904
+ install on mismatch, and re-runs install.
905
+ Aliased as 'update'.
906
+ status Scan all known install locations for every
907
+ supported agent (project + global) and
908
+ report which versions are installed where.
909
+ Read-only; no flags.
600
910
 
601
911
  ${bold('INIT OPTIONS')}
602
912
  --name <name> Skip the project name prompt
@@ -617,6 +927,20 @@ ${bold('INSTALL OPTIONS')}
617
927
  --project Project-level install (default when supported)
618
928
  --dry-run Preview without writing files
619
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
943
+
620
944
  ${bold('GENERAL OPTIONS')}
621
945
  --version, -v Print version and exit
622
946
  --help, -h Print this help and exit
@@ -635,6 +959,18 @@ ${bold('EXAMPLES')}
635
959
  ba-toolkit install --for claude-code
636
960
  ba-toolkit install --for cursor --dry-run
637
961
 
962
+ # Remove skills from an agent (asks for confirmation).
963
+ ba-toolkit uninstall --for claude-code
964
+ ba-toolkit uninstall --for claude-code --global
965
+ ba-toolkit uninstall --for cursor --dry-run
966
+
967
+ # After 'npm update -g @kudusov.takhir/ba-toolkit', refresh the skills.
968
+ ba-toolkit upgrade --for claude-code
969
+ ba-toolkit upgrade --for cursor --dry-run
970
+
971
+ # See where (and which version) BA Toolkit is installed.
972
+ ba-toolkit status
973
+
638
974
  ${bold('LEARN MORE')}
639
975
  https://github.com/TakhirKudusov/ba-toolkit
640
976
  `);
@@ -663,6 +999,16 @@ async function main() {
663
999
  case 'install':
664
1000
  await cmdInstall(args);
665
1001
  break;
1002
+ case 'uninstall':
1003
+ await cmdUninstall(args);
1004
+ break;
1005
+ case 'upgrade':
1006
+ case 'update':
1007
+ await cmdUpgrade(args);
1008
+ break;
1009
+ case 'status':
1010
+ cmdStatus();
1011
+ break;
666
1012
  case 'help':
667
1013
  cmdHelp();
668
1014
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudusov.takhir/ba-toolkit",
3
- "version": "1.3.2",
3
+ "version": "1.4.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",