@imdeadpool/guardex 7.0.11 → 7.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,13 @@
1
1
  # GitGuardex — Guardian T-Rex for your repo
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/%40imdeadpool%2Fguardex?color=cb3837&logo=npm)](https://www.npmjs.com/package/@imdeadpool/guardex)
4
- [![CI](https://github.com/recodeee/gitguardex/actions/workflows/ci.yml/badge.svg)](https://github.com/recodeee/gitguardex/actions/workflows/ci.yml)
3
+ [![npm version](https://img.shields.io/npm/v/%40imdeadpool%2Fguardex?label=npm&color=cb3837&logo=npm)](https://www.npmjs.com/package/@imdeadpool/guardex)
4
+ [![npm downloads](https://img.shields.io/npm/dm/%40imdeadpool%2Fguardex?label=downloads&color=0b76c5)](https://www.npmjs.com/package/@imdeadpool/guardex)
5
+ [![GitHub stars](https://img.shields.io/github/stars/recodeee/gitguardex?label=stars&color=d4ac0d)](https://github.com/recodeee/gitguardex/stargazers)
6
+ [![License](https://img.shields.io/npm/l/%40imdeadpool%2Fguardex?label=License&color=97ca00)](./LICENSE)
7
+
8
+ [![CI](https://img.shields.io/github/actions/workflow/status/recodeee/gitguardex/ci.yml?branch=main&label=CI)](https://github.com/recodeee/gitguardex/actions/workflows/ci.yml)
9
+ [![Release](https://img.shields.io/github/actions/workflow/status/recodeee/gitguardex/release.yml?label=Release)](https://github.com/recodeee/gitguardex/actions/workflows/release.yml)
10
+ [![CodeQL](https://img.shields.io/github/actions/workflow/status/recodeee/gitguardex/codeql.yml?branch=main&label=CodeQL)](https://github.com/recodeee/gitguardex/actions/workflows/codeql.yml)
5
11
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/recodeee/gitguardex/badge)](https://securityscorecards.dev/viewer/?uri=github.com/recodeee/gitguardex)
6
12
 
7
13
  **GitGuardex is a safety layer for parallel agent work in git repos.** If you're running more than one Codex or Claude agent on the same codebase, this is what keeps them from deleting each other's work.
@@ -20,24 +26,28 @@ I was running ~30 Codex agents in parallel and hit a wall: they kept working on
20
26
 
21
27
  GitGuardex exists to stop that loop. Every agent gets its own worktree, claims the files it's touching, and can't clobber files another agent has claimed. Your local branch stays clean; agents stay in their lanes.
22
28
 
23
- ![Multi-agent dashboard example](https://raw.githubusercontent.com/recodeee/gitguardex/main/docs/images/dashboard-multi-agent.png)
24
-
25
- Coming soon: [recodee.com](https://recodee.com) — live account health, usage, routing, and capacity in one place.
29
+ ### Solution
26
30
 
27
31
  ```mermaid
28
32
  flowchart LR
29
- A[Agent A edits shared files] --> S[Same target surface]
30
- B[Agent B edits shared files] --> S
31
- C[Agent C edits shared files] --> S
32
- D[Agent D edits shared files] --> S
33
- E[Agent E edits shared files] --> S
34
- S --> F[Conflict / overwrite churn]
35
- F --> G[Deleted or lost code]
36
- G --> H[Rework and confusion]
37
- H --> I[Regression risk grows]
38
- I --> F
33
+ A[Agent A adds assertions in a shared test] --> S[Several agents touch the same files]
34
+ B[Agent B rewrites the same test flow] --> S
35
+ C[Agent C updates the shared helper] --> S
36
+ D[Agent D deletes lines Agent A just added] --> S
37
+ E[Agent E saves an older snapshot of the file] --> S
38
+ S --> F[One agent overwrites another agent's edits]
39
+ F --> G[Another agent deletes code the others just added]
40
+ G --> H[Lost work, rework, and review confusion]
41
+ H --> I[Regression risk and flaky fixes grow]
42
+ I --> S
39
43
  ```
40
44
 
45
+ ### Dashboard
46
+
47
+ ![Multi-agent dashboard example](https://raw.githubusercontent.com/recodeee/gitguardex/main/docs/images/dashboard-multi-agent.png)
48
+
49
+ Coming soon: [recodee.com](https://recodee.com) — live account health, usage, routing, and capacity in one place.
50
+
41
51
  ---
42
52
 
43
53
  ## What it does
@@ -122,7 +132,7 @@ gx finish --all
122
132
 
123
133
  This is the real Source Control shape Guardex is aiming for: isolated agent branches, clear OpenSpec artifacts, and no pile-up on one shared checkout.
124
134
 
125
- ![Exact VS Code Source Control workflow screenshot](https://raw.githubusercontent.com/recodeee/gitguardex/main/docs/images/workflow-vscode-source-control-exact.png)
135
+ ![Guarded VS Code Source Control example](https://raw.githubusercontent.com/recodeee/gitguardex/main/docs/images/workflow-source-control-grouped.png)
126
136
 
127
137
  ---
128
138
 
@@ -237,7 +247,7 @@ A few things worth knowing up front:
237
247
 
238
248
  - Running `gx` with no command opens the status/health view.
239
249
  - `gx init` is just an alias for `gx setup`.
240
- - Setup/doctor can install missing global companion CLIs (OMC runtime, OpenSpec, cavemem, codex-auth) — but only with explicit Y/N confirmation.
250
+ - Setup/doctor can install missing companion tooling (OMC runtime, OpenSpec, cavemem, codex-auth, caveman, cavekit) — but only with explicit Y/N confirmation.
241
251
  - Direct commits/pushes to protected branches are **blocked** by default. Agents must use the `agent/*` + PR flow.
242
252
  - **Exception:** VS Code Source Control commits are allowed on protected branches that exist only locally (no upstream, no remote branch).
243
253
  - On protected `main`, `gx doctor` auto-runs in a sandbox agent branch/worktree so it can't touch your real main.
@@ -255,13 +265,15 @@ git config multiagent.allowVscodeProtectedBranchWrites true
255
265
 
256
266
  ## Companion tools
257
267
 
258
- GitGuardex is designed to work alongside these. All optional — but if you're running many agents, you probably want them. `gx status` reports the machine-detectable global helpers; plugin/skills-first add-ons like `caveman` and `cavekit` are documented below for manual setup.
268
+ GitGuardex is designed to work alongside these. All optional — but if you're running many agents, you probably want them. `gx status` reports the machine-detectable companion helpers, including local `caveman` / `cavekit` installs when their home-directory footprints are present.
259
269
 
260
270
  ```text
261
271
  ● oh-my-codex: active
262
272
  ● oh-my-claude-sisyphus: active
263
273
  ● @fission-ai/openspec: active
264
274
  ● cavemem: active
275
+ ● cavekit: active
276
+ ● caveman: active
265
277
  ● @imdeadpool/codex-account-switcher: active
266
278
  ● gh: active
267
279
  ```
@@ -275,6 +287,7 @@ npm i -g oh-my-codex
275
287
  ```
276
288
 
277
289
  Repo: <https://github.com/Yeachan-Heo/oh-my-codex>
290
+ [![GitHub stars](https://img.shields.io/github/stars/Yeachan-Heo/oh-my-codex?style=social)](https://github.com/Yeachan-Heo/oh-my-codex)
278
291
 
279
292
  ### oh-my-claudecode — Claude Code equivalent
280
293
 
@@ -285,6 +298,7 @@ npm i -g oh-my-claude-sisyphus@latest
285
298
  ```
286
299
 
287
300
  Repo: <https://github.com/Yeachan-Heo/oh-my-claudecode>
301
+ [![GitHub stars](https://img.shields.io/github/stars/Yeachan-Heo/oh-my-claudecode?style=social)](https://github.com/Yeachan-Heo/oh-my-claudecode)
288
302
 
289
303
  ### Caveman — output compression for long agent runs
290
304
 
@@ -295,6 +309,7 @@ npx skills add JuliusBrussee/caveman
295
309
  ```
296
310
 
297
311
  Repo: <https://github.com/JuliusBrussee/caveman>
312
+ [![GitHub stars](https://img.shields.io/github/stars/JuliusBrussee/caveman?style=social)](https://github.com/JuliusBrussee/caveman)
298
313
 
299
314
  ### Cavemem — local persistent memory for agents
300
315
 
@@ -307,6 +322,7 @@ cavemem status
307
322
  ```
308
323
 
309
324
  Repo: <https://github.com/JuliusBrussee/cavemem>
325
+ [![GitHub stars](https://img.shields.io/github/stars/JuliusBrussee/cavemem?style=social)](https://github.com/JuliusBrussee/cavemem)
310
326
 
311
327
  ### Cavekit — spec-driven build loop
312
328
 
@@ -317,6 +333,7 @@ npx skills add JuliusBrussee/cavekit
317
333
  ```
318
334
 
319
335
  Repo: <https://github.com/JuliusBrussee/cavekit>
336
+ [![GitHub stars](https://img.shields.io/github/stars/JuliusBrussee/cavekit?style=social)](https://github.com/JuliusBrussee/cavekit)
320
337
 
321
338
  ### OpenSpec — spec-driven workflows
322
339
 
@@ -327,6 +344,7 @@ npm i -g @fission-ai/openspec
327
344
  ```
328
345
 
329
346
  Repo: <https://github.com/Fission-AI/OpenSpec>
347
+ [![GitHub stars](https://img.shields.io/github/stars/Fission-AI/OpenSpec?style=social)](https://github.com/Fission-AI/OpenSpec)
330
348
 
331
349
  ### codex-auth — multi-account switcher
332
350
 
@@ -342,6 +360,7 @@ codex-auth current
342
360
  ```
343
361
 
344
362
  Repo: [recodeecom/codex-account-switcher-cli](https://github.com/recodeecom/codex-account-switcher-cli)
363
+ [![GitHub stars](https://img.shields.io/github/stars/recodeecom/codex-account-switcher-cli?style=social)](https://github.com/recodeecom/codex-account-switcher-cli)
345
364
 
346
365
  ### GitHub CLI (`gh`)
347
366
 
@@ -488,6 +507,16 @@ npm pack --dry-run
488
507
  <details>
489
508
  <summary><strong>v7.x</strong></summary>
490
509
 
510
+ ### v7.0.13
511
+ - `gx status` and `gx setup` now present the Claude companion as `oh-my-claudecode` while still installing the published npm package `oh-my-claude-sisyphus`.
512
+ - When that dependency is inactive or the user declines the optional install, Guardex now prints the upstream repo URL so the missing dependency is explicit instead of hidden behind the npm package name.
513
+ - Bumped `@imdeadpool/guardex` from `7.0.12` → `7.0.13` after npm rejected a republish over the already-published `7.0.12`.
514
+
515
+ ### v7.0.12
516
+ - Fixed the self-update handoff after `gx` installs a newer global package. When the on-disk install advances, GitGuardex now restarts into the installed CLI instead of continuing in the old process and printing the stale in-memory version.
517
+ - This removes the confusing `Updated to latest published version` followed by `CLI: ...7.0.10` mismatch that happened when `7.0.11` finished installing during the same `gx` invocation.
518
+ - Bumped `@imdeadpool/guardex` from `7.0.11` → `7.0.12`.
519
+
491
520
  ### v7.0.11
492
521
  - Fixed the npm release workflow trigger so publishes run from `release.published` or explicit manual dispatch, instead of double-firing on both the tag push and the release event.
493
522
  - This keeps the GitHub `npm` environment from collecting duplicate cancelled deploy cards for the same version and leaves one canonical release deployment to monitor.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const fs = require('node:fs');
4
+ const os = require('node:os');
4
5
  const path = require('node:path');
5
6
  const cp = require('node:child_process');
6
7
 
@@ -12,13 +13,46 @@ const SHORT_TOOL_NAME = 'gx';
12
13
  const LEGACY_NAMES = ['guardex', 'multiagent-safety'];
13
14
  const OPENSPEC_PACKAGE = '@fission-ai/openspec';
14
15
  const OMC_PACKAGE = 'oh-my-claude-sisyphus';
16
+ const OMC_REPO_URL = 'https://github.com/Yeachan-Heo/oh-my-claudecode';
15
17
  const CAVEMEM_PACKAGE = 'cavemem';
18
+ const NPX_BIN = process.env.GUARDEX_NPX_BIN || 'npx';
19
+ const GUARDEX_HOME_DIR = path.resolve(process.env.GUARDEX_HOME_DIR || os.homedir());
20
+ const GLOBAL_TOOLCHAIN_SERVICES = [
21
+ { name: 'oh-my-codex', packageName: 'oh-my-codex' },
22
+ {
23
+ name: 'oh-my-claudecode',
24
+ packageName: OMC_PACKAGE,
25
+ dependencyUrl: OMC_REPO_URL,
26
+ },
27
+ { name: OPENSPEC_PACKAGE, packageName: OPENSPEC_PACKAGE },
28
+ { name: CAVEMEM_PACKAGE, packageName: CAVEMEM_PACKAGE },
29
+ {
30
+ name: '@imdeadpool/codex-account-switcher',
31
+ packageName: '@imdeadpool/codex-account-switcher',
32
+ },
33
+ ];
16
34
  const GLOBAL_TOOLCHAIN_PACKAGES = [
17
- 'oh-my-codex',
18
- OMC_PACKAGE,
19
- OPENSPEC_PACKAGE,
20
- CAVEMEM_PACKAGE,
21
- '@imdeadpool/codex-account-switcher',
35
+ ...GLOBAL_TOOLCHAIN_SERVICES.map((service) => service.packageName),
36
+ ];
37
+ const OPTIONAL_LOCAL_COMPANION_TOOLS = [
38
+ {
39
+ name: 'cavekit',
40
+ candidatePaths: [
41
+ '.cavekit/plugin.json',
42
+ '.codex/local-marketplaces/cavekit/.agents/plugins/marketplace.json',
43
+ ],
44
+ installCommand: `${NPX_BIN} skills add JuliusBrussee/cavekit`,
45
+ installArgs: ['skills', 'add', 'JuliusBrussee/cavekit'],
46
+ },
47
+ {
48
+ name: 'caveman',
49
+ candidatePaths: [
50
+ '.config/caveman/config.json',
51
+ '.cavekit/skills/caveman/SKILL.md',
52
+ ],
53
+ installCommand: `${NPX_BIN} skills add JuliusBrussee/caveman`,
54
+ installArgs: ['skills', 'add', 'JuliusBrussee/caveman'],
55
+ },
22
56
  ];
23
57
  const GH_BIN = process.env.GUARDEX_GH_BIN || 'gh';
24
58
  const REQUIRED_SYSTEM_TOOLS = [
@@ -3393,9 +3427,15 @@ function maybeSelfUpdateBeforeStatus() {
3393
3427
  }
3394
3428
 
3395
3429
  console.log(`[${TOOL_NAME}] ✅ Updated to latest published version.`);
3430
+ restartIntoUpdatedGuardex(check.latest);
3396
3431
  }
3397
3432
 
3398
3433
  function readInstalledGuardexVersion() {
3434
+ const installInfo = readInstalledGuardexInstallInfo();
3435
+ return installInfo ? installInfo.version : null;
3436
+ }
3437
+
3438
+ function readInstalledGuardexInstallInfo() {
3399
3439
  // Resolves the globally-installed package's on-disk version so we can
3400
3440
  // verify npm actually wrote new bytes. Uses `npm root -g` to locate the
3401
3441
  // global install root so we don't accidentally read the running source
@@ -3417,7 +3457,24 @@ function readInstalledGuardexVersion() {
3417
3457
  }
3418
3458
  const parsed = JSON.parse(fs.readFileSync(installedPkgPath, 'utf8'));
3419
3459
  if (parsed && typeof parsed.version === 'string') {
3420
- return parsed.version;
3460
+ let binRelative = null;
3461
+ if (typeof parsed.bin === 'string') {
3462
+ binRelative = parsed.bin;
3463
+ } else if (parsed.bin && typeof parsed.bin === 'object') {
3464
+ const invokedName = path.basename(process.argv[1] || '');
3465
+ binRelative =
3466
+ parsed.bin[invokedName] ||
3467
+ parsed.bin[SHORT_TOOL_NAME] ||
3468
+ Object.values(parsed.bin).find((value) => typeof value === 'string') ||
3469
+ null;
3470
+ }
3471
+ const packageRoot = path.dirname(installedPkgPath);
3472
+ const binPath = binRelative ? path.join(packageRoot, binRelative) : null;
3473
+ return {
3474
+ version: parsed.version,
3475
+ packageRoot,
3476
+ binPath,
3477
+ };
3421
3478
  }
3422
3479
  } catch (error) {
3423
3480
  return null;
@@ -3425,6 +3482,38 @@ function readInstalledGuardexVersion() {
3425
3482
  return null;
3426
3483
  }
3427
3484
 
3485
+ function restartIntoUpdatedGuardex(expectedVersion) {
3486
+ const installInfo = readInstalledGuardexInstallInfo();
3487
+ if (!installInfo || installInfo.version !== expectedVersion || installInfo.version === packageJson.version) {
3488
+ return;
3489
+ }
3490
+ if (!installInfo.binPath || !fs.existsSync(installInfo.binPath)) {
3491
+ console.log(`[${TOOL_NAME}] Restart required to use ${installInfo.version}. Rerun ${SHORT_TOOL_NAME}.`);
3492
+ return;
3493
+ }
3494
+
3495
+ console.log(`[${TOOL_NAME}] Restarting into ${installInfo.version}…`);
3496
+ const restartResult = cp.spawnSync(
3497
+ process.execPath,
3498
+ [installInfo.binPath, ...process.argv.slice(2)],
3499
+ {
3500
+ cwd: process.cwd(),
3501
+ env: {
3502
+ ...process.env,
3503
+ GUARDEX_SKIP_UPDATE_CHECK: '1',
3504
+ },
3505
+ stdio: 'inherit',
3506
+ },
3507
+ );
3508
+ if (restartResult.error) {
3509
+ console.log(
3510
+ `[${TOOL_NAME}] Restart into ${installInfo.version} failed. Rerun ${SHORT_TOOL_NAME}.`,
3511
+ );
3512
+ return;
3513
+ }
3514
+ process.exit(restartResult.status == null ? 0 : restartResult.status);
3515
+ }
3516
+
3428
3517
  function checkForOpenSpecPackageUpdate() {
3429
3518
  if (envFlagEnabled('GUARDEX_SKIP_OPENSPEC_UPDATE_CHECK')) {
3430
3519
  return { checked: false, reason: 'disabled' };
@@ -3551,6 +3640,36 @@ function resolveGlobalInstallApproval(options) {
3551
3640
  return { approved: true, source: 'prompt' };
3552
3641
  }
3553
3642
 
3643
+ function getGlobalToolchainService(packageName) {
3644
+ const service = GLOBAL_TOOLCHAIN_SERVICES.find(
3645
+ (candidate) => candidate.packageName === packageName,
3646
+ );
3647
+ return service || { name: packageName, packageName };
3648
+ }
3649
+
3650
+ function formatGlobalToolchainServiceName(packageName) {
3651
+ return getGlobalToolchainService(packageName).name;
3652
+ }
3653
+
3654
+ function describeMissingGlobalDependencyWarnings(packageNames) {
3655
+ return packageNames
3656
+ .map((packageName) => getGlobalToolchainService(packageName))
3657
+ .filter((service) => service.dependencyUrl)
3658
+ .map(
3659
+ (service) =>
3660
+ `Guardex needs ${service.name} as a dependency: ${service.dependencyUrl}`,
3661
+ );
3662
+ }
3663
+
3664
+ function buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools) {
3665
+ const dependencyWarnings = describeMissingGlobalDependencyWarnings(missingPackages);
3666
+ const installCommands = describeCompanionInstallCommands(missingPackages, missingLocalTools);
3667
+ const dependencyPrefix = dependencyWarnings.length > 0
3668
+ ? `${dependencyWarnings.join(' ')} `
3669
+ : '';
3670
+ return `${dependencyPrefix}Install missing companion tools now? (${installCommands.join(' && ')})`;
3671
+ }
3672
+
3554
3673
  function detectGlobalToolchainPackages() {
3555
3674
  const result = run(NPM_BIN, ['list', '-g', '--depth=0', '--json']);
3556
3675
  if (result.status !== 0) {
@@ -3616,7 +3735,34 @@ function detectRequiredSystemTools() {
3616
3735
  return services;
3617
3736
  }
3618
3737
 
3619
- function askGlobalInstallForMissing(options, missingPackages) {
3738
+ function detectOptionalLocalCompanionTools() {
3739
+ return OPTIONAL_LOCAL_COMPANION_TOOLS.map((tool) => {
3740
+ const detectedPath = tool.candidatePaths
3741
+ .map((relativePath) => path.join(GUARDEX_HOME_DIR, relativePath))
3742
+ .find((candidatePath) => fs.existsSync(candidatePath));
3743
+ return {
3744
+ name: tool.name,
3745
+ displayName: tool.displayName || tool.name,
3746
+ installCommand: tool.installCommand,
3747
+ installArgs: [...tool.installArgs],
3748
+ status: detectedPath ? 'active' : 'inactive',
3749
+ detectedPath: detectedPath || null,
3750
+ };
3751
+ });
3752
+ }
3753
+
3754
+ function describeCompanionInstallCommands(missingPackages, missingLocalTools) {
3755
+ const commands = [];
3756
+ if (missingPackages.length > 0) {
3757
+ commands.push(`${NPM_BIN} i -g ${missingPackages.join(' ')}`);
3758
+ }
3759
+ for (const tool of missingLocalTools) {
3760
+ commands.push(tool.installCommand);
3761
+ }
3762
+ return commands;
3763
+ }
3764
+
3765
+ function askGlobalInstallForMissing(options, missingPackages, missingLocalTools) {
3620
3766
  const approval = resolveGlobalInstallApproval(options);
3621
3767
  if (!approval.approved) {
3622
3768
  return approval;
@@ -3624,7 +3770,7 @@ function askGlobalInstallForMissing(options, missingPackages) {
3624
3770
 
3625
3771
  if (approval.source === 'prompt') {
3626
3772
  const approved = promptYesNoStrict(
3627
- `Install missing global tools now? (npm i -g ${missingPackages.join(' ')})`,
3773
+ buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools),
3628
3774
  );
3629
3775
  return { approved, source: 'prompt' };
3630
3776
  }
@@ -3638,36 +3784,69 @@ function installGlobalToolchain(options) {
3638
3784
  }
3639
3785
 
3640
3786
  const detection = detectGlobalToolchainPackages();
3787
+ const localCompanionTools = detectOptionalLocalCompanionTools();
3641
3788
  if (!detection.ok) {
3642
3789
  console.log(`[${TOOL_NAME}] ⚠️ Could not detect global packages: ${detection.error}`);
3643
3790
  } else {
3644
3791
  if (detection.installed.length > 0) {
3645
- console.log(`[${TOOL_NAME}] Already installed globally: ${detection.installed.join(', ')}`);
3792
+ console.log(
3793
+ `[${TOOL_NAME}] Already installed globally: ` +
3794
+ `${detection.installed.map((pkg) => formatGlobalToolchainServiceName(pkg)).join(', ')}`,
3795
+ );
3796
+ }
3797
+ const installedLocalTools = localCompanionTools
3798
+ .filter((tool) => tool.status === 'active')
3799
+ .map((tool) => tool.name);
3800
+ if (installedLocalTools.length > 0) {
3801
+ console.log(`[${TOOL_NAME}] Already installed locally: ${installedLocalTools.join(', ')}`);
3646
3802
  }
3647
- if (detection.missing.length === 0) {
3803
+ if (detection.missing.length === 0 && localCompanionTools.every((tool) => tool.status === 'active')) {
3648
3804
  return { status: 'already-installed' };
3649
3805
  }
3650
3806
  }
3651
3807
 
3652
3808
  const missingPackages = detection.ok ? detection.missing : [...GLOBAL_TOOLCHAIN_PACKAGES];
3653
- const approval = askGlobalInstallForMissing(options, missingPackages);
3809
+ const missingLocalTools = localCompanionTools.filter((tool) => tool.status !== 'active');
3810
+ const approval = askGlobalInstallForMissing(options, missingPackages, missingLocalTools);
3654
3811
  if (!approval.approved) {
3655
- return { status: 'skipped', reason: approval.source };
3656
- }
3657
-
3658
- console.log(
3659
- `[${TOOL_NAME}] Installing global toolchain: npm i -g ${missingPackages.join(' ')}`,
3660
- );
3661
- const result = run(NPM_BIN, ['i', '-g', ...missingPackages], { stdio: 'inherit' });
3662
- if (result.status !== 0) {
3663
- const stderr = (result.stderr || '').trim();
3664
3812
  return {
3665
- status: 'failed',
3666
- reason: stderr || 'npm global install failed',
3813
+ status: 'skipped',
3814
+ reason: approval.source,
3815
+ missingPackages,
3816
+ missingLocalTools,
3667
3817
  };
3668
3818
  }
3669
3819
 
3670
- return { status: 'installed', packages: missingPackages };
3820
+ const installed = [];
3821
+ if (missingPackages.length > 0) {
3822
+ console.log(
3823
+ `[${TOOL_NAME}] Installing global toolchain: npm i -g ${missingPackages.join(' ')}`,
3824
+ );
3825
+ const result = run(NPM_BIN, ['i', '-g', ...missingPackages], { stdio: 'inherit' });
3826
+ if (result.status !== 0) {
3827
+ const stderr = (result.stderr || '').trim();
3828
+ return {
3829
+ status: 'failed',
3830
+ reason: stderr || 'npm global install failed',
3831
+ };
3832
+ }
3833
+ installed.push(...missingPackages);
3834
+ }
3835
+
3836
+ for (const tool of missingLocalTools) {
3837
+ console.log(`[${TOOL_NAME}] Installing local companion tool: ${tool.installCommand}`);
3838
+ const result = run(NPX_BIN, tool.installArgs, { stdio: 'inherit' });
3839
+ if (result.status !== 0) {
3840
+ const stderr = (result.stderr || '').trim();
3841
+ return {
3842
+ status: 'failed',
3843
+ reason: stderr || `${tool.name} install failed`,
3844
+ };
3845
+ }
3846
+ installed.push(tool.name);
3847
+ }
3848
+
3849
+ return { status: 'installed', packages: installed };
3671
3850
  }
3672
3851
 
3673
3852
  function gitRefExists(repoRoot, refName) {
@@ -4010,17 +4189,33 @@ function status(rawArgs) {
4010
4189
 
4011
4190
  const toolchain = detectGlobalToolchainPackages();
4012
4191
  const npmServices = GLOBAL_TOOLCHAIN_PACKAGES.map((pkg) => {
4192
+ const service = getGlobalToolchainService(pkg);
4013
4193
  if (!toolchain.ok) {
4014
- return { name: pkg, status: 'unknown' };
4194
+ return {
4195
+ name: service.name,
4196
+ displayName: service.name,
4197
+ packageName: pkg,
4198
+ dependencyUrl: service.dependencyUrl || null,
4199
+ status: 'unknown',
4200
+ };
4015
4201
  }
4016
4202
  return {
4017
- name: pkg,
4203
+ name: service.name,
4204
+ displayName: service.name,
4205
+ packageName: pkg,
4206
+ dependencyUrl: service.dependencyUrl || null,
4018
4207
  status: toolchain.installed.includes(pkg) ? 'active' : 'inactive',
4019
4208
  };
4020
4209
  });
4210
+ const localCompanionServices = detectOptionalLocalCompanionTools().map((tool) => ({
4211
+ name: tool.name,
4212
+ displayName: tool.displayName || tool.name,
4213
+ status: tool.status,
4214
+ }));
4021
4215
  const requiredSystemTools = detectRequiredSystemTools();
4022
4216
  const services = [
4023
4217
  ...npmServices,
4218
+ ...localCompanionServices,
4024
4219
  ...requiredSystemTools.map((tool) => ({
4025
4220
  name: tool.name,
4026
4221
  displayName: tool.displayName || tool.name,
@@ -4079,6 +4274,24 @@ function status(rawArgs) {
4079
4274
  const serviceLabel = service.displayName || service.name;
4080
4275
  console.log(` - ${statusDot(service.status)} ${serviceLabel}: ${service.status}`);
4081
4276
  }
4277
+ const inactiveOptionalCompanions = [...npmServices, ...localCompanionServices]
4278
+ .filter((service) => service.status !== 'active')
4279
+ .map((service) => service.displayName || service.name);
4280
+ if (inactiveOptionalCompanions.length > 0) {
4281
+ console.log(
4282
+ `[${TOOL_NAME}] Optional companion tools inactive: ${inactiveOptionalCompanions.join(', ')}`,
4283
+ );
4284
+ for (const warning of describeMissingGlobalDependencyWarnings(
4285
+ npmServices
4286
+ .filter((service) => service.status === 'inactive')
4287
+ .map((service) => service.packageName),
4288
+ )) {
4289
+ console.log(`[${TOOL_NAME}] ${warning}`);
4290
+ }
4291
+ console.log(
4292
+ `[${TOOL_NAME}] Run '${SHORT_TOOL_NAME} setup' to install missing companions with an explicit Y/N prompt.`,
4293
+ );
4294
+ }
4082
4295
  const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
4083
4296
  if (missingSystemTools.length > 0) {
4084
4297
  const tools = missingSystemTools
@@ -4713,21 +4926,32 @@ function setup(rawArgs) {
4713
4926
  const globalInstallStatus = installGlobalToolchain(options);
4714
4927
  if (globalInstallStatus.status === 'installed') {
4715
4928
  console.log(
4716
- `[${TOOL_NAME}] ✅ Global tools installed (${(globalInstallStatus.packages || []).join(', ')}).`,
4929
+ `[${TOOL_NAME}] ✅ Companion tools installed (${(globalInstallStatus.packages || []).join(', ')}).`,
4717
4930
  );
4718
4931
  } else if (globalInstallStatus.status === 'already-installed') {
4719
- console.log(`[${TOOL_NAME}] ✅ Companion npm global tools already installed. Skipping.`);
4932
+ console.log(`[${TOOL_NAME}] ✅ Companion tools already installed. Skipping.`);
4720
4933
  } else if (globalInstallStatus.status === 'failed') {
4934
+ const installCommands = describeCompanionInstallCommands(
4935
+ GLOBAL_TOOLCHAIN_PACKAGES,
4936
+ OPTIONAL_LOCAL_COMPANION_TOOLS,
4937
+ );
4721
4938
  console.log(
4722
4939
  `[${TOOL_NAME}] ⚠️ Global install failed: ${globalInstallStatus.reason}\n` +
4723
4940
  `[${TOOL_NAME}] Continue with local safety setup. You can retry later with:\n` +
4724
- ` ${NPM_BIN} i -g ${GLOBAL_TOOLCHAIN_PACKAGES.join(' ')}`,
4941
+ installCommands.map((command) => ` ${command}`).join('\n'),
4725
4942
  );
4726
4943
  } else if (globalInstallStatus.status === 'skipped' && globalInstallStatus.reason === 'non-interactive-default') {
4727
4944
  console.log(
4728
- `[${TOOL_NAME}] Skipping global installs (non-interactive mode). ` +
4945
+ `[${TOOL_NAME}] Skipping companion installs (non-interactive mode). ` +
4729
4946
  `Use --yes-global-install to force or run interactively for Y/N prompt.`,
4730
4947
  );
4948
+ } else if (globalInstallStatus.status === 'skipped') {
4949
+ console.log(`[${TOOL_NAME}] ⚠️ Companion installs skipped by user choice.`);
4950
+ for (const warning of describeMissingGlobalDependencyWarnings(
4951
+ globalInstallStatus.missingPackages || [],
4952
+ )) {
4953
+ console.log(`[${TOOL_NAME}] ⚠️ ${warning}`);
4954
+ }
4731
4955
  }
4732
4956
  const requiredSystemTools = detectRequiredSystemTools();
4733
4957
  const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/guardex",
3
- "version": "7.0.11",
3
+ "version": "7.0.13",
4
4
  "description": "GitGuardex: hardened multi-agent git guardrails for parallel agent work.",
5
5
  "license": "MIT",
6
6
  "preferGlobal": true,