@braingrid/cli 0.2.42 → 0.2.43

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
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.43] - 2026-02-17
11
+
12
+ ### Fixed
13
+
14
+ - **grep -c newline bug in verify-acceptance-criteria.sh** — `grep -c` exits non-zero on 0 matches, causing `|| echo "0"` to produce `"0\n0"` artifacts that break arithmetic expansion; replaced with `|| true` and `${VAR:-0}` default
15
+
10
16
  ## [0.2.42] - 2026-02-17
11
17
 
12
18
  ### Added
package/dist/cli.js CHANGED
@@ -222,7 +222,7 @@ async function axiosWithRetry(config2, options) {
222
222
 
223
223
  // src/build-config.ts
224
224
  var BUILD_ENV = true ? "production" : process.env.NODE_ENV === "test" ? "development" : "production";
225
- var CLI_VERSION = true ? "0.2.42" : "0.0.0-test";
225
+ var CLI_VERSION = true ? "0.2.43" : "0.0.0-test";
226
226
  var PRODUCTION_CONFIG = {
227
227
  apiUrl: "https://app.braingrid.ai",
228
228
  workosAuthUrl: "https://auth.braingrid.ai",
@@ -2658,7 +2658,7 @@ async function installStatusLineScript(scriptContent, targetPath = ".claude/stat
2658
2658
  throw new Error(`Failed to install status line script to ${targetPath}: ${errorMessage}`);
2659
2659
  }
2660
2660
  }
2661
- async function installHookScript(scriptContent, targetPath = ".claude/hooks/sync-braingrid-task.sh") {
2661
+ async function installHookScript(scriptContent, targetPath) {
2662
2662
  try {
2663
2663
  const parentDir = path2.dirname(targetPath);
2664
2664
  await fs2.mkdir(parentDir, { recursive: true });
@@ -2679,7 +2679,64 @@ async function copyBraingridReadme(targetPath = ".braingrid/README.md") {
2679
2679
  return false;
2680
2680
  }
2681
2681
  }
2682
- async function updateClaudeSettings(settingsPath = ".claude/settings.json", scriptPath2 = ".claude/statusline.sh", hookScriptPath = ".claude/hooks/sync-braingrid-task.sh", createHookScriptPath = ".claude/hooks/create-braingrid-task.sh", verifyHookScriptPath = ".claude/hooks/verify-acceptance-criteria.sh", postTaskUpdatePromptPath = ".claude/hooks/post-task-update-prompt.sh", preTaskCreatePath = ".claude/hooks/pre-task-create-naming.sh", preTaskUpdatePath = ".claude/hooks/pre-task-update-instructions.sh", sessionStartPath = ".claude/hooks/check-stale-build-sentinel.sh", taskCompletedPath = ".claude/hooks/task-completed-validate.sh") {
2682
+ var HOOK_REGISTRY = [
2683
+ {
2684
+ hookType: "PostToolUse",
2685
+ matcher: "TaskUpdate",
2686
+ mergeStrategy: "upsertByMatcher",
2687
+ commands: [
2688
+ { script: "sync-braingrid-task.sh", timeout: 1e4 },
2689
+ { script: "post-task-update-prompt.sh" }
2690
+ ]
2691
+ },
2692
+ {
2693
+ hookType: "PostToolUse",
2694
+ matcher: "TaskCreate",
2695
+ mergeStrategy: "upsertByMatcher",
2696
+ commands: [{ script: "create-braingrid-task.sh", timeout: 1e4 }]
2697
+ },
2698
+ {
2699
+ hookType: "PreToolUse",
2700
+ matcher: "TaskCreate",
2701
+ mergeStrategy: "upsertByMatcher",
2702
+ commands: [{ script: "pre-task-create-naming.sh" }]
2703
+ },
2704
+ {
2705
+ hookType: "PreToolUse",
2706
+ matcher: "TaskUpdate",
2707
+ mergeStrategy: "upsertByMatcher",
2708
+ commands: [{ script: "pre-task-update-instructions.sh" }]
2709
+ },
2710
+ {
2711
+ hookType: "Stop",
2712
+ mergeStrategy: "appendDedup",
2713
+ dedupSubstring: "verify-acceptance-criteria",
2714
+ commands: [{ script: "verify-acceptance-criteria.sh" }]
2715
+ },
2716
+ {
2717
+ hookType: "SessionStart",
2718
+ mergeStrategy: "appendDedup",
2719
+ dedupSubstring: "check-stale-build-sentinel",
2720
+ commands: [{ script: "check-stale-build-sentinel.sh" }]
2721
+ },
2722
+ {
2723
+ hookType: "TaskCompleted",
2724
+ mergeStrategy: "appendDedup",
2725
+ dedupSubstring: "task-completed-validate",
2726
+ commands: [{ script: "task-completed-validate.sh" }]
2727
+ }
2728
+ ];
2729
+ function buildHookCommand(cmd) {
2730
+ const result = {
2731
+ type: "command",
2732
+ command: `.claude/hooks/${cmd.script}`
2733
+ };
2734
+ if (cmd.timeout) {
2735
+ result.timeout = cmd.timeout;
2736
+ }
2737
+ return result;
2738
+ }
2739
+ async function updateClaudeSettings(settingsPath = ".claude/settings.json", statusLineScriptPath = ".claude/statusline.sh", hookRegistry = HOOK_REGISTRY) {
2683
2740
  try {
2684
2741
  let settings = {};
2685
2742
  try {
@@ -2688,133 +2745,46 @@ async function updateClaudeSettings(settingsPath = ".claude/settings.json", scri
2688
2745
  } catch {
2689
2746
  }
2690
2747
  const existingStatusLine = settings.statusLine;
2691
- if (existingStatusLine?.command?.includes(scriptPath2)) {
2748
+ if (existingStatusLine?.command?.includes(statusLineScriptPath)) {
2692
2749
  } else {
2693
2750
  settings.statusLine = {
2694
2751
  type: "command",
2695
- command: scriptPath2,
2752
+ command: statusLineScriptPath,
2696
2753
  padding: 0
2697
2754
  };
2698
2755
  }
2699
- const ourHookEntry = {
2700
- matcher: "TaskUpdate",
2701
- hooks: [
2702
- {
2703
- type: "command",
2704
- command: hookScriptPath,
2705
- timeout: 1e4
2706
- },
2707
- {
2708
- type: "command",
2709
- command: postTaskUpdatePromptPath
2710
- }
2711
- ]
2712
- };
2713
2756
  const existingHooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2714
- const existingPostToolUse = Array.isArray(existingHooks.PostToolUse) ? existingHooks.PostToolUse : [];
2715
- const taskUpdateIdx = existingPostToolUse.findIndex((e) => e.matcher === "TaskUpdate");
2716
- let mergedPostToolUse;
2717
- if (taskUpdateIdx >= 0) {
2718
- mergedPostToolUse = [...existingPostToolUse];
2719
- mergedPostToolUse[taskUpdateIdx] = ourHookEntry;
2720
- } else {
2721
- mergedPostToolUse = [...existingPostToolUse, ourHookEntry];
2722
- }
2723
- const ourCreateHookEntry = {
2724
- matcher: "TaskCreate",
2725
- hooks: [
2726
- {
2727
- type: "command",
2728
- command: createHookScriptPath,
2729
- timeout: 1e4
2730
- }
2731
- ]
2732
- };
2733
- const taskCreatePostIdx = mergedPostToolUse.findIndex((e) => e.matcher === "TaskCreate");
2734
- if (taskCreatePostIdx >= 0) {
2735
- mergedPostToolUse[taskCreatePostIdx] = ourCreateHookEntry;
2736
- } else {
2737
- mergedPostToolUse = [...mergedPostToolUse, ourCreateHookEntry];
2738
- }
2739
- const ourPreToolUseEntry = {
2740
- matcher: "TaskCreate",
2741
- hooks: [
2742
- {
2743
- type: "command",
2744
- command: preTaskCreatePath
2757
+ const mergedByType = {};
2758
+ for (const entry of hookRegistry) {
2759
+ const hookType = entry.hookType;
2760
+ if (!mergedByType[hookType]) {
2761
+ const existing = Array.isArray(existingHooks[hookType]) ? [...existingHooks[hookType]] : [];
2762
+ mergedByType[hookType] = existing;
2763
+ }
2764
+ const merged = mergedByType[hookType];
2765
+ const hooks = entry.commands.map(buildHookCommand);
2766
+ if (entry.mergeStrategy === "upsertByMatcher") {
2767
+ const matcher = entry.matcher ?? "";
2768
+ const newEntry = { matcher, hooks };
2769
+ const idx = merged.findIndex((e) => e.matcher === matcher);
2770
+ if (idx >= 0) {
2771
+ merged[idx] = newEntry;
2772
+ } else {
2773
+ merged.push(newEntry);
2745
2774
  }
2746
- ]
2747
- };
2748
- const existingPreToolUse = Array.isArray(existingHooks.PreToolUse) ? existingHooks.PreToolUse : [];
2749
- const taskCreateIdx = existingPreToolUse.findIndex((e) => e.matcher === "TaskCreate");
2750
- let mergedPreToolUse;
2751
- if (taskCreateIdx >= 0) {
2752
- mergedPreToolUse = [...existingPreToolUse];
2753
- mergedPreToolUse[taskCreateIdx] = ourPreToolUseEntry;
2754
- } else {
2755
- mergedPreToolUse = [...existingPreToolUse, ourPreToolUseEntry];
2756
- }
2757
- const ourPreToolUseTaskUpdateEntry = {
2758
- matcher: "TaskUpdate",
2759
- hooks: [
2760
- {
2761
- type: "command",
2762
- command: preTaskUpdatePath
2775
+ } else {
2776
+ const substring = entry.dedupSubstring ?? "";
2777
+ const alreadyPresent = merged.some(
2778
+ (e) => e.hooks?.some((h) => h.command?.includes(substring))
2779
+ );
2780
+ if (!alreadyPresent) {
2781
+ merged.push({ hooks });
2763
2782
  }
2764
- ]
2765
- };
2766
- const taskUpdatePreIdx = mergedPreToolUse.findIndex((e) => e.matcher === "TaskUpdate");
2767
- if (taskUpdatePreIdx >= 0) {
2768
- mergedPreToolUse[taskUpdatePreIdx] = ourPreToolUseTaskUpdateEntry;
2769
- } else {
2770
- mergedPreToolUse = [...mergedPreToolUse, ourPreToolUseTaskUpdateEntry];
2783
+ }
2771
2784
  }
2772
- const ourStopHookEntry = {
2773
- hooks: [
2774
- {
2775
- type: "command",
2776
- command: verifyHookScriptPath
2777
- }
2778
- ]
2779
- };
2780
- const existingStop = Array.isArray(existingHooks.Stop) ? existingHooks.Stop : [];
2781
- const hasVerifyHook = existingStop.some(
2782
- (entry) => entry.hooks?.some((h) => h.command?.includes("verify-acceptance-criteria"))
2783
- );
2784
- const mergedStop = hasVerifyHook ? existingStop : [...existingStop, ourStopHookEntry];
2785
- const ourSessionStartEntry = {
2786
- hooks: [
2787
- {
2788
- type: "command",
2789
- command: sessionStartPath
2790
- }
2791
- ]
2792
- };
2793
- const existingSessionStart = Array.isArray(existingHooks.SessionStart) ? existingHooks.SessionStart : [];
2794
- const hasSessionStartHook = existingSessionStart.some(
2795
- (entry) => entry.hooks?.some((h) => h.command?.includes("check-stale-build-sentinel"))
2796
- );
2797
- const mergedSessionStart = hasSessionStartHook ? existingSessionStart : [...existingSessionStart, ourSessionStartEntry];
2798
- const ourTaskCompletedEntry = {
2799
- hooks: [
2800
- {
2801
- type: "command",
2802
- command: taskCompletedPath
2803
- }
2804
- ]
2805
- };
2806
- const existingTaskCompleted = Array.isArray(existingHooks.TaskCompleted) ? existingHooks.TaskCompleted : [];
2807
- const hasTaskCompletedHook = existingTaskCompleted.some(
2808
- (entry) => entry.hooks?.some((h) => h.command?.includes("task-completed-validate"))
2809
- );
2810
- const mergedTaskCompleted = hasTaskCompletedHook ? existingTaskCompleted : [...existingTaskCompleted, ourTaskCompletedEntry];
2811
2785
  settings.hooks = {
2812
2786
  ...existingHooks,
2813
- PreToolUse: mergedPreToolUse,
2814
- PostToolUse: mergedPostToolUse,
2815
- Stop: mergedStop,
2816
- SessionStart: mergedSessionStart,
2817
- TaskCompleted: mergedTaskCompleted
2787
+ ...mergedByType
2818
2788
  };
2819
2789
  const parentDir = path2.dirname(settingsPath);
2820
2790
  await fs2.mkdir(parentDir, { recursive: true });
@@ -3547,8 +3517,7 @@ function buildSuccessMessage(config2, installedPerDir, extras) {
3547
3517
 
3548
3518
  `) + chalk8.dim("Files installed:\n") + dirLines + extras + chalk8.dim(` Content injected into: ${config2.injection.targetFile}
3549
3519
 
3550
- `) + chalk8.dim("Next steps:\n") + chalk8.dim(" 1. Review the integration files\n") + chalk8.dim(` 2. Open ${config2.name}
3551
- `) + chalk8.dim(" 3. Try the /specify or /breakdown commands\n") + chalk8.dim(" 4. Learn more: ") + chalk8.cyan(config2.docsUrl);
3520
+ `) + chalk8.dim("Next, try:\n") + chalk8.dim(" /build REQ-X \u2192 build a requirement\n") + chalk8.dim(' /specify "add two-factor auth" \u2192 specify a requirement\n') + chalk8.dim(" Learn more: ") + chalk8.cyan(config2.docsUrl);
3552
3521
  }
3553
3522
  function isSetupResult(result) {
3554
3523
  return "data" in result && result.success === true && !("message" in result);
@@ -3596,111 +3565,23 @@ async function handleSetupClaudeCode(opts) {
3596
3565
  error instanceof Error ? error.message : String(error)
3597
3566
  );
3598
3567
  }
3599
- let syncHookInstalled = false;
3600
- try {
3601
- const syncContent = await fetchFileFromGitHub("claude-code/hooks/sync-braingrid-task.sh");
3602
- await installHookScript(syncContent);
3603
- syncHookInstalled = true;
3604
- } catch (error) {
3605
- console.error(
3606
- chalk8.yellow("\u26A0\uFE0F Failed to install sync hook:"),
3607
- error instanceof Error ? error.message : String(error)
3608
- );
3609
- }
3610
- let createHookInstalled = false;
3611
- try {
3612
- const createContent = await fetchFileFromGitHub("claude-code/hooks/create-braingrid-task.sh");
3613
- await installHookScript(createContent, ".claude/hooks/create-braingrid-task.sh");
3614
- createHookInstalled = true;
3615
- } catch (error) {
3616
- console.error(
3617
- chalk8.yellow("\u26A0\uFE0F Failed to install create hook:"),
3618
- error instanceof Error ? error.message : String(error)
3619
- );
3620
- }
3621
- let verifyHookInstalled = false;
3622
- try {
3623
- const verifyContent = await fetchFileFromGitHub(
3624
- "claude-code/hooks/verify-acceptance-criteria.sh"
3625
- );
3626
- await installHookScript(verifyContent, ".claude/hooks/verify-acceptance-criteria.sh");
3627
- verifyHookInstalled = true;
3628
- } catch (error) {
3629
- console.error(
3630
- chalk8.yellow("\u26A0\uFE0F Failed to install verify hook:"),
3631
- error instanceof Error ? error.message : String(error)
3632
- );
3633
- }
3634
- let preTaskCreateInstalled = false;
3635
- try {
3636
- const preTaskCreateContent = await fetchFileFromGitHub(
3637
- "claude-code/hooks/pre-task-create-naming.sh"
3638
- );
3639
- await installHookScript(preTaskCreateContent, ".claude/hooks/pre-task-create-naming.sh");
3640
- preTaskCreateInstalled = true;
3641
- } catch (error) {
3642
- console.error(
3643
- chalk8.yellow("\u26A0\uFE0F Failed to install pre-task-create hook:"),
3644
- error instanceof Error ? error.message : String(error)
3645
- );
3646
- }
3647
- let preTaskUpdateInstalled = false;
3648
- try {
3649
- const preTaskUpdateContent = await fetchFileFromGitHub(
3650
- "claude-code/hooks/pre-task-update-instructions.sh"
3651
- );
3652
- await installHookScript(
3653
- preTaskUpdateContent,
3654
- ".claude/hooks/pre-task-update-instructions.sh"
3655
- );
3656
- preTaskUpdateInstalled = true;
3657
- } catch (error) {
3658
- console.error(
3659
- chalk8.yellow("\u26A0\uFE0F Failed to install pre-task-update hook:"),
3660
- error instanceof Error ? error.message : String(error)
3661
- );
3662
- }
3663
- let postTaskUpdateInstalled = false;
3664
- try {
3665
- const postTaskUpdateContent = await fetchFileFromGitHub(
3666
- "claude-code/hooks/post-task-update-prompt.sh"
3667
- );
3668
- await installHookScript(postTaskUpdateContent, ".claude/hooks/post-task-update-prompt.sh");
3669
- postTaskUpdateInstalled = true;
3670
- } catch (error) {
3671
- console.error(
3672
- chalk8.yellow("\u26A0\uFE0F Failed to install post-task-update hook:"),
3673
- error instanceof Error ? error.message : String(error)
3674
- );
3675
- }
3676
- let sessionStartInstalled = false;
3677
- try {
3678
- const sessionStartContent = await fetchFileFromGitHub(
3679
- "claude-code/hooks/check-stale-build-sentinel.sh"
3680
- );
3681
- await installHookScript(sessionStartContent, ".claude/hooks/check-stale-build-sentinel.sh");
3682
- sessionStartInstalled = true;
3683
- } catch (error) {
3684
- console.error(
3685
- chalk8.yellow("\u26A0\uFE0F Failed to install session-start hook:"),
3686
- error instanceof Error ? error.message : String(error)
3687
- );
3688
- }
3689
- let taskCompletedInstalled = false;
3690
- try {
3691
- const taskCompletedContent = await fetchFileFromGitHub(
3692
- "claude-code/hooks/task-completed-validate.sh"
3693
- );
3694
- await installHookScript(taskCompletedContent, ".claude/hooks/task-completed-validate.sh");
3695
- taskCompletedInstalled = true;
3696
- } catch (error) {
3697
- console.error(
3698
- chalk8.yellow("\u26A0\uFE0F Failed to install task-completed hook:"),
3699
- error instanceof Error ? error.message : String(error)
3700
- );
3568
+ const allScripts = HOOK_REGISTRY.flatMap((e) => e.commands.map((c) => c.script));
3569
+ const installedHooks = [];
3570
+ for (const script of allScripts) {
3571
+ try {
3572
+ const content = await fetchFileFromGitHub(`claude-code/hooks/${script}`);
3573
+ await installHookScript(content, `.claude/hooks/${script}`);
3574
+ installedHooks.push(script);
3575
+ } catch (error) {
3576
+ console.error(
3577
+ chalk8.yellow(`\u26A0\uFE0F Failed to install hook ${script}:`),
3578
+ error instanceof Error ? error.message : String(error)
3579
+ );
3580
+ }
3701
3581
  }
3702
3582
  const statusLineMessage = statusLineInstalled ? chalk8.dim(" Status line: .claude/statusline.sh\n") : "";
3703
- const hooksMessage = (syncHookInstalled ? chalk8.dim(" Hook script: .claude/hooks/sync-braingrid-task.sh\n") : "") + (createHookInstalled ? chalk8.dim(" Hook script: .claude/hooks/create-braingrid-task.sh\n") : "") + (verifyHookInstalled ? chalk8.dim(" Hook script: .claude/hooks/verify-acceptance-criteria.sh\n") : "") + (preTaskCreateInstalled ? chalk8.dim(" Hook script: .claude/hooks/pre-task-create-naming.sh\n") : "") + (preTaskUpdateInstalled ? chalk8.dim(" Hook script: .claude/hooks/pre-task-update-instructions.sh\n") : "") + (postTaskUpdateInstalled ? chalk8.dim(" Hook script: .claude/hooks/post-task-update-prompt.sh\n") : "") + (sessionStartInstalled ? chalk8.dim(" Hook script: .claude/hooks/check-stale-build-sentinel.sh\n") : "") + (taskCompletedInstalled ? chalk8.dim(" Hook script: .claude/hooks/task-completed-validate.sh\n") : "");
3583
+ const hooksMessage = installedHooks.map((s) => chalk8.dim(` Hook script: .claude/hooks/${s}
3584
+ `)).join("");
3704
3585
  return {
3705
3586
  success: true,
3706
3587
  message: buildSuccessMessage(config2, displayPerDir, statusLineMessage + hooksMessage)