@imdeadpool/guardex 7.0.10 → 7.0.12
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 +121 -23
- package/bin/multiagent-safety.js +175 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
> [!WARNING]
|
|
10
10
|
> Not affiliated with OpenAI, Anthropic, or Codex. Not an official tool.
|
|
11
11
|
|
|
12
|
+
> [!IMPORTANT]
|
|
13
|
+
> GitGuardex is still being tested in real multi-agent repos. If something feels rough or broken, especially around cleanup, finish, merge, or recovery flows, sorry. We need to test those paths under real load first, and we'll patch issues as we find them.
|
|
14
|
+
|
|
12
15
|
---
|
|
13
16
|
|
|
14
17
|
## The problem
|
|
@@ -17,22 +20,24 @@ I was running ~30 Codex agents in parallel and hit a wall: they kept working on
|
|
|
17
20
|
|
|
18
21
|
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.
|
|
19
22
|
|
|
20
|
-

|
|
21
|
-
|
|
22
23
|
```mermaid
|
|
23
24
|
flowchart LR
|
|
24
|
-
A[Agent A
|
|
25
|
-
B[Agent B
|
|
26
|
-
C[Agent C
|
|
27
|
-
D[Agent D
|
|
28
|
-
E[Agent E
|
|
29
|
-
S --> F[
|
|
30
|
-
F --> G[
|
|
31
|
-
G --> H[
|
|
32
|
-
H --> I[Regression risk
|
|
33
|
-
I -->
|
|
25
|
+
A[Agent A adds assertions in a shared test] --> S[Several agents touch the same files]
|
|
26
|
+
B[Agent B rewrites the same test flow] --> S
|
|
27
|
+
C[Agent C updates the shared helper] --> S
|
|
28
|
+
D[Agent D deletes lines Agent A just added] --> S
|
|
29
|
+
E[Agent E saves an older snapshot of the file] --> S
|
|
30
|
+
S --> F[One agent overwrites another agent's edits]
|
|
31
|
+
F --> G[Another agent deletes code the others just added]
|
|
32
|
+
G --> H[Lost work, rework, and review confusion]
|
|
33
|
+
H --> I[Regression risk and flaky fixes grow]
|
|
34
|
+
I --> S
|
|
34
35
|
```
|
|
35
36
|
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
Coming soon: [recodee.com](https://recodee.com) — live account health, usage, routing, and capacity in one place.
|
|
40
|
+
|
|
36
41
|
---
|
|
37
42
|
|
|
38
43
|
## What it does
|
|
@@ -41,7 +46,7 @@ flowchart LR
|
|
|
41
46
|
- **Explicit file lock claiming** — an agent declares which files it's editing before it edits them.
|
|
42
47
|
- **Deletion guard** — claimed files can't be removed by another agent.
|
|
43
48
|
- **Protected-base safety** — `main`, `dev`, `master` are blocked by default; agents must go through PRs.
|
|
44
|
-
- **Auto-merges agent configs into every worktree** — `oh-my-codex`, `oh-my-
|
|
49
|
+
- **Auto-merges agent configs into every worktree** — `oh-my-codex`, `oh-my-claudecode`, caveman mode, and OpenSpec all get applied automatically so every spawned agent starts tuned, not bare.
|
|
45
50
|
- **Repair/doctor flow** — when drift happens (and it will), `gx doctor` gets you back to a clean state.
|
|
46
51
|
- **Auto-finish** — when Codex exits a session, Guardex commits sandbox changes, syncs against the base, retries once if the base moved, and opens a PR.
|
|
47
52
|
|
|
@@ -59,6 +64,16 @@ That's it. Setup installs hooks, scripts, templates, and scaffolds OpenSpec/cave
|
|
|
59
64
|
|
|
60
65
|
---
|
|
61
66
|
|
|
67
|
+
## What `gx` shows first
|
|
68
|
+
|
|
69
|
+
Before you branch, repair, or start agents, run plain `gx`. It gives you a one-screen status view for the CLI, global helpers, repo safety service, current repo path, and active branch.
|
|
70
|
+
|
|
71
|
+

|
|
72
|
+
|
|
73
|
+
Use `gx setup` the first time you wire GitGuardex into a repo. It bootstraps the managed hooks, scripts, templates, and optional workspace/OpenSpec wiring. If the repo drifts later, use `gx doctor` as the repair path: it reapplies the managed safety files, verifies the setup, and on protected `main` it auto-sandboxes the repair so your visible base branch stays clean.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
62
77
|
## Daily workflow
|
|
63
78
|
|
|
64
79
|
Per new agent task:
|
|
@@ -107,7 +122,7 @@ gx finish --all
|
|
|
107
122
|
|
|
108
123
|
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.
|
|
109
124
|
|
|
110
|
-

|
|
111
126
|
|
|
112
127
|
---
|
|
113
128
|
|
|
@@ -222,7 +237,7 @@ A few things worth knowing up front:
|
|
|
222
237
|
|
|
223
238
|
- Running `gx` with no command opens the status/health view.
|
|
224
239
|
- `gx init` is just an alias for `gx setup`.
|
|
225
|
-
- Setup/doctor can install missing
|
|
240
|
+
- Setup/doctor can install missing companion tooling (OMC runtime, OpenSpec, cavemem, codex-auth, caveman, cavekit) — but only with explicit Y/N confirmation.
|
|
226
241
|
- Direct commits/pushes to protected branches are **blocked** by default. Agents must use the `agent/*` + PR flow.
|
|
227
242
|
- **Exception:** VS Code Source Control commits are allowed on protected branches that exist only locally (no upstream, no remote branch).
|
|
228
243
|
- On protected `main`, `gx doctor` auto-runs in a sandbox agent branch/worktree so it can't touch your real main.
|
|
@@ -240,21 +255,84 @@ git config multiagent.allowVscodeProtectedBranchWrites true
|
|
|
240
255
|
|
|
241
256
|
## Companion tools
|
|
242
257
|
|
|
243
|
-
GitGuardex is designed to work alongside these. All optional — but if you're running many agents, you probably want them.
|
|
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 companion helpers, including local `caveman` / `cavekit` installs when their home-directory footprints are present.
|
|
244
259
|
|
|
245
|
-
|
|
260
|
+
```text
|
|
261
|
+
● oh-my-codex: active
|
|
262
|
+
● oh-my-claude-sisyphus: active
|
|
263
|
+
● @fission-ai/openspec: active
|
|
264
|
+
● cavemem: active
|
|
265
|
+
● cavekit: active
|
|
266
|
+
● caveman: active
|
|
267
|
+
● @imdeadpool/codex-account-switcher: active
|
|
268
|
+
● gh: active
|
|
269
|
+
```
|
|
246
270
|
|
|
247
|
-
|
|
271
|
+
### oh-my-codex — Codex config + skills framework
|
|
272
|
+
|
|
273
|
+
Loads skills, slash commands, and session defaults into Codex. Guardex merges `oh-my-codex` into every agent worktree automatically, so every spawned agent starts with the same tuned config instead of vanilla Codex.
|
|
248
274
|
|
|
249
275
|
```sh
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
276
|
+
npm i -g oh-my-codex
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Repo: <https://github.com/Yeachan-Heo/oh-my-codex>
|
|
280
|
+
|
|
281
|
+
### oh-my-claudecode — Claude Code equivalent
|
|
282
|
+
|
|
283
|
+
Claude-side mirror of oh-my-codex. Same idea: skills, commands, and defaults loaded into every Claude Code session. Guardex merges it into worktrees alongside oh-my-codex so mixed Codex + Claude agent fleets behave consistently. For the npm CLI/runtime path, the published package name is `oh-my-claude-sisyphus`.
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
npm i -g oh-my-claude-sisyphus@latest
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Repo: <https://github.com/Yeachan-Heo/oh-my-claudecode>
|
|
290
|
+
|
|
291
|
+
### Caveman — output compression for long agent runs
|
|
292
|
+
|
|
293
|
+
Ultra-compressed response mode for Claude/Codex-style agents. Useful when you want less output-token churn during long reviews, debug loops, or multi-agent sessions.
|
|
294
|
+
|
|
295
|
+
```sh
|
|
296
|
+
npx skills add JuliusBrussee/caveman
|
|
253
297
|
```
|
|
254
298
|
|
|
299
|
+
Repo: <https://github.com/JuliusBrussee/caveman>
|
|
300
|
+
|
|
301
|
+
### Cavemem — local persistent memory for agents
|
|
302
|
+
|
|
303
|
+
Cross-agent memory with local SQLite + MCP. Helpful when you want Codex or Claude sessions to retain compressed history across runs. `gx setup` can install the CLI; you still run the IDE wiring once per machine.
|
|
304
|
+
|
|
305
|
+
```sh
|
|
306
|
+
npm install -g cavemem
|
|
307
|
+
cavemem install --ide codex
|
|
308
|
+
cavemem status
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Repo: <https://github.com/JuliusBrussee/cavemem>
|
|
312
|
+
|
|
313
|
+
### Cavekit — spec-driven build loop
|
|
314
|
+
|
|
315
|
+
Spec-driven workflow layer for building from durable specs with explicit build/check commands. The current install path also brings in its `spec`, `build`, `check`, `caveman`, and `backprop` skills.
|
|
316
|
+
|
|
317
|
+
```sh
|
|
318
|
+
npx skills add JuliusBrussee/cavekit
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Repo: <https://github.com/JuliusBrussee/cavekit>
|
|
322
|
+
|
|
323
|
+
### OpenSpec — spec-driven workflows
|
|
324
|
+
|
|
325
|
+
Structured plan/change/apply/archive flow for agents. Prevents them from drifting off-task on long jobs. Full guide: [`docs/openspec-getting-started.md`](./docs/openspec-getting-started.md).
|
|
326
|
+
|
|
327
|
+
```sh
|
|
328
|
+
npm i -g @fission-ai/openspec
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Repo: <https://github.com/Fission-AI/OpenSpec>
|
|
332
|
+
|
|
255
333
|
### codex-auth — multi-account switcher
|
|
256
334
|
|
|
257
|
-
For multi-identity Codex workflows. I built this because switching accounts manually for 30 agents was impossible.
|
|
335
|
+
For multi-identity Codex workflows. I built this because switching accounts manually for 30 agents was impossible. Auto-registers accounts to a dashboard on `codex login` so you can see every account and switch with one command.
|
|
258
336
|
|
|
259
337
|
```sh
|
|
260
338
|
npm i -g @imdeadpool/codex-account-switcher
|
|
@@ -267,6 +345,16 @@ codex-auth current
|
|
|
267
345
|
|
|
268
346
|
Repo: [recodeecom/codex-account-switcher-cli](https://github.com/recodeecom/codex-account-switcher-cli)
|
|
269
347
|
|
|
348
|
+
### GitHub CLI (`gh`)
|
|
349
|
+
|
|
350
|
+
Required for PR/merge automation. `agent-branch-finish.sh` and `codex-agent.sh` auto-finish both depend on it.
|
|
351
|
+
|
|
352
|
+
```sh
|
|
353
|
+
# https://cli.github.com/
|
|
354
|
+
gh --version
|
|
355
|
+
gh auth status
|
|
356
|
+
```
|
|
357
|
+
|
|
270
358
|
### Pull app — fork auto-sync
|
|
271
359
|
|
|
272
360
|
Guardex installs a starter config at `.github/pull.yml.example`.
|
|
@@ -283,7 +371,7 @@ Validate: `https://pull.git.ci/check/<owner>/<repo>`
|
|
|
283
371
|
|
|
284
372
|
Install: <https://github.com/apps/cr-gpt>
|
|
285
373
|
|
|
286
|
-
`gx setup` installs `.github/workflows/cr.yml`.
|
|
374
|
+
`gx setup` installs `.github/workflows/cr.yml`. Add `OPENAI_API_KEY` under `Settings → Secrets and variables → Actions → Secrets`. After that, new and updated PRs get reviewed automatically.
|
|
287
375
|
|
|
288
376
|
---
|
|
289
377
|
|
|
@@ -402,6 +490,16 @@ npm pack --dry-run
|
|
|
402
490
|
<details>
|
|
403
491
|
<summary><strong>v7.x</strong></summary>
|
|
404
492
|
|
|
493
|
+
### v7.0.12
|
|
494
|
+
- 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.
|
|
495
|
+
- 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.
|
|
496
|
+
- Bumped `@imdeadpool/guardex` from `7.0.11` → `7.0.12`.
|
|
497
|
+
|
|
498
|
+
### v7.0.11
|
|
499
|
+
- 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.
|
|
500
|
+
- This keeps the GitHub `npm` environment from collecting duplicate cancelled deploy cards for the same version and leaves one canonical release deployment to monitor.
|
|
501
|
+
- Bumped `@imdeadpool/guardex` from `7.0.10` → `7.0.11` so the next release can publish cleanly after `7.0.10` was already taken on npm.
|
|
502
|
+
|
|
405
503
|
### v7.0.10
|
|
406
504
|
- Primary user-facing long name is now **GitGuardex**. CLI/help presents `gitguardex` as the long-form command; `gx` stays the preferred short alias; `guardex` remains as legacy compatibility.
|
|
407
505
|
- Installed Codex/Claude startup files now use `gitguardex` paths: `.codex/skills/gitguardex/SKILL.md` and `.claude/commands/gitguardex.md`.
|
package/bin/multiagent-safety.js
CHANGED
|
@@ -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
|
|
|
@@ -11,12 +12,37 @@ const TOOL_NAME = 'gitguardex';
|
|
|
11
12
|
const SHORT_TOOL_NAME = 'gx';
|
|
12
13
|
const LEGACY_NAMES = ['guardex', 'multiagent-safety'];
|
|
13
14
|
const OPENSPEC_PACKAGE = '@fission-ai/openspec';
|
|
15
|
+
const OMC_PACKAGE = 'oh-my-claude-sisyphus';
|
|
16
|
+
const CAVEMEM_PACKAGE = 'cavemem';
|
|
17
|
+
const NPX_BIN = process.env.GUARDEX_NPX_BIN || 'npx';
|
|
18
|
+
const GUARDEX_HOME_DIR = path.resolve(process.env.GUARDEX_HOME_DIR || os.homedir());
|
|
14
19
|
const GLOBAL_TOOLCHAIN_PACKAGES = [
|
|
15
20
|
'oh-my-codex',
|
|
16
|
-
|
|
21
|
+
OMC_PACKAGE,
|
|
17
22
|
OPENSPEC_PACKAGE,
|
|
23
|
+
CAVEMEM_PACKAGE,
|
|
18
24
|
'@imdeadpool/codex-account-switcher',
|
|
19
25
|
];
|
|
26
|
+
const OPTIONAL_LOCAL_COMPANION_TOOLS = [
|
|
27
|
+
{
|
|
28
|
+
name: 'cavekit',
|
|
29
|
+
candidatePaths: [
|
|
30
|
+
'.cavekit/plugin.json',
|
|
31
|
+
'.codex/local-marketplaces/cavekit/.agents/plugins/marketplace.json',
|
|
32
|
+
],
|
|
33
|
+
installCommand: `${NPX_BIN} skills add JuliusBrussee/cavekit`,
|
|
34
|
+
installArgs: ['skills', 'add', 'JuliusBrussee/cavekit'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'caveman',
|
|
38
|
+
candidatePaths: [
|
|
39
|
+
'.config/caveman/config.json',
|
|
40
|
+
'.cavekit/skills/caveman/SKILL.md',
|
|
41
|
+
],
|
|
42
|
+
installCommand: `${NPX_BIN} skills add JuliusBrussee/caveman`,
|
|
43
|
+
installArgs: ['skills', 'add', 'JuliusBrussee/caveman'],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
20
46
|
const GH_BIN = process.env.GUARDEX_GH_BIN || 'gh';
|
|
21
47
|
const REQUIRED_SYSTEM_TOOLS = [
|
|
22
48
|
{
|
|
@@ -3390,9 +3416,15 @@ function maybeSelfUpdateBeforeStatus() {
|
|
|
3390
3416
|
}
|
|
3391
3417
|
|
|
3392
3418
|
console.log(`[${TOOL_NAME}] ✅ Updated to latest published version.`);
|
|
3419
|
+
restartIntoUpdatedGuardex(check.latest);
|
|
3393
3420
|
}
|
|
3394
3421
|
|
|
3395
3422
|
function readInstalledGuardexVersion() {
|
|
3423
|
+
const installInfo = readInstalledGuardexInstallInfo();
|
|
3424
|
+
return installInfo ? installInfo.version : null;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
function readInstalledGuardexInstallInfo() {
|
|
3396
3428
|
// Resolves the globally-installed package's on-disk version so we can
|
|
3397
3429
|
// verify npm actually wrote new bytes. Uses `npm root -g` to locate the
|
|
3398
3430
|
// global install root so we don't accidentally read the running source
|
|
@@ -3414,7 +3446,24 @@ function readInstalledGuardexVersion() {
|
|
|
3414
3446
|
}
|
|
3415
3447
|
const parsed = JSON.parse(fs.readFileSync(installedPkgPath, 'utf8'));
|
|
3416
3448
|
if (parsed && typeof parsed.version === 'string') {
|
|
3417
|
-
|
|
3449
|
+
let binRelative = null;
|
|
3450
|
+
if (typeof parsed.bin === 'string') {
|
|
3451
|
+
binRelative = parsed.bin;
|
|
3452
|
+
} else if (parsed.bin && typeof parsed.bin === 'object') {
|
|
3453
|
+
const invokedName = path.basename(process.argv[1] || '');
|
|
3454
|
+
binRelative =
|
|
3455
|
+
parsed.bin[invokedName] ||
|
|
3456
|
+
parsed.bin[SHORT_TOOL_NAME] ||
|
|
3457
|
+
Object.values(parsed.bin).find((value) => typeof value === 'string') ||
|
|
3458
|
+
null;
|
|
3459
|
+
}
|
|
3460
|
+
const packageRoot = path.dirname(installedPkgPath);
|
|
3461
|
+
const binPath = binRelative ? path.join(packageRoot, binRelative) : null;
|
|
3462
|
+
return {
|
|
3463
|
+
version: parsed.version,
|
|
3464
|
+
packageRoot,
|
|
3465
|
+
binPath,
|
|
3466
|
+
};
|
|
3418
3467
|
}
|
|
3419
3468
|
} catch (error) {
|
|
3420
3469
|
return null;
|
|
@@ -3422,6 +3471,38 @@ function readInstalledGuardexVersion() {
|
|
|
3422
3471
|
return null;
|
|
3423
3472
|
}
|
|
3424
3473
|
|
|
3474
|
+
function restartIntoUpdatedGuardex(expectedVersion) {
|
|
3475
|
+
const installInfo = readInstalledGuardexInstallInfo();
|
|
3476
|
+
if (!installInfo || installInfo.version !== expectedVersion || installInfo.version === packageJson.version) {
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
if (!installInfo.binPath || !fs.existsSync(installInfo.binPath)) {
|
|
3480
|
+
console.log(`[${TOOL_NAME}] Restart required to use ${installInfo.version}. Rerun ${SHORT_TOOL_NAME}.`);
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
console.log(`[${TOOL_NAME}] Restarting into ${installInfo.version}…`);
|
|
3485
|
+
const restartResult = cp.spawnSync(
|
|
3486
|
+
process.execPath,
|
|
3487
|
+
[installInfo.binPath, ...process.argv.slice(2)],
|
|
3488
|
+
{
|
|
3489
|
+
cwd: process.cwd(),
|
|
3490
|
+
env: {
|
|
3491
|
+
...process.env,
|
|
3492
|
+
GUARDEX_SKIP_UPDATE_CHECK: '1',
|
|
3493
|
+
},
|
|
3494
|
+
stdio: 'inherit',
|
|
3495
|
+
},
|
|
3496
|
+
);
|
|
3497
|
+
if (restartResult.error) {
|
|
3498
|
+
console.log(
|
|
3499
|
+
`[${TOOL_NAME}] Restart into ${installInfo.version} failed. Rerun ${SHORT_TOOL_NAME}.`,
|
|
3500
|
+
);
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
process.exit(restartResult.status == null ? 0 : restartResult.status);
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3425
3506
|
function checkForOpenSpecPackageUpdate() {
|
|
3426
3507
|
if (envFlagEnabled('GUARDEX_SKIP_OPENSPEC_UPDATE_CHECK')) {
|
|
3427
3508
|
return { checked: false, reason: 'disabled' };
|
|
@@ -3613,15 +3694,44 @@ function detectRequiredSystemTools() {
|
|
|
3613
3694
|
return services;
|
|
3614
3695
|
}
|
|
3615
3696
|
|
|
3697
|
+
function detectOptionalLocalCompanionTools() {
|
|
3698
|
+
return OPTIONAL_LOCAL_COMPANION_TOOLS.map((tool) => {
|
|
3699
|
+
const detectedPath = tool.candidatePaths
|
|
3700
|
+
.map((relativePath) => path.join(GUARDEX_HOME_DIR, relativePath))
|
|
3701
|
+
.find((candidatePath) => fs.existsSync(candidatePath));
|
|
3702
|
+
return {
|
|
3703
|
+
name: tool.name,
|
|
3704
|
+
displayName: tool.displayName || tool.name,
|
|
3705
|
+
installCommand: tool.installCommand,
|
|
3706
|
+
installArgs: [...tool.installArgs],
|
|
3707
|
+
status: detectedPath ? 'active' : 'inactive',
|
|
3708
|
+
detectedPath: detectedPath || null,
|
|
3709
|
+
};
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
function describeCompanionInstallCommands(missingPackages, missingLocalTools) {
|
|
3714
|
+
const commands = [];
|
|
3715
|
+
if (missingPackages.length > 0) {
|
|
3716
|
+
commands.push(`${NPM_BIN} i -g ${missingPackages.join(' ')}`);
|
|
3717
|
+
}
|
|
3718
|
+
for (const tool of missingLocalTools) {
|
|
3719
|
+
commands.push(tool.installCommand);
|
|
3720
|
+
}
|
|
3721
|
+
return commands;
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3616
3724
|
function askGlobalInstallForMissing(options, missingPackages) {
|
|
3617
3725
|
const approval = resolveGlobalInstallApproval(options);
|
|
3618
3726
|
if (!approval.approved) {
|
|
3619
3727
|
return approval;
|
|
3620
3728
|
}
|
|
3621
3729
|
|
|
3730
|
+
const missingLocalTools = detectOptionalLocalCompanionTools().filter((tool) => tool.status !== 'active');
|
|
3731
|
+
const installCommands = describeCompanionInstallCommands(missingPackages, missingLocalTools);
|
|
3622
3732
|
if (approval.source === 'prompt') {
|
|
3623
3733
|
const approved = promptYesNoStrict(
|
|
3624
|
-
`Install missing
|
|
3734
|
+
`Install missing companion tools now? (${installCommands.join(' && ')})`,
|
|
3625
3735
|
);
|
|
3626
3736
|
return { approved, source: 'prompt' };
|
|
3627
3737
|
}
|
|
@@ -3635,36 +3745,61 @@ function installGlobalToolchain(options) {
|
|
|
3635
3745
|
}
|
|
3636
3746
|
|
|
3637
3747
|
const detection = detectGlobalToolchainPackages();
|
|
3748
|
+
const localCompanionTools = detectOptionalLocalCompanionTools();
|
|
3638
3749
|
if (!detection.ok) {
|
|
3639
3750
|
console.log(`[${TOOL_NAME}] ⚠️ Could not detect global packages: ${detection.error}`);
|
|
3640
3751
|
} else {
|
|
3641
3752
|
if (detection.installed.length > 0) {
|
|
3642
3753
|
console.log(`[${TOOL_NAME}] Already installed globally: ${detection.installed.join(', ')}`);
|
|
3643
3754
|
}
|
|
3644
|
-
|
|
3755
|
+
const installedLocalTools = localCompanionTools
|
|
3756
|
+
.filter((tool) => tool.status === 'active')
|
|
3757
|
+
.map((tool) => tool.name);
|
|
3758
|
+
if (installedLocalTools.length > 0) {
|
|
3759
|
+
console.log(`[${TOOL_NAME}] Already installed locally: ${installedLocalTools.join(', ')}`);
|
|
3760
|
+
}
|
|
3761
|
+
if (detection.missing.length === 0 && localCompanionTools.every((tool) => tool.status === 'active')) {
|
|
3645
3762
|
return { status: 'already-installed' };
|
|
3646
3763
|
}
|
|
3647
3764
|
}
|
|
3648
3765
|
|
|
3649
3766
|
const missingPackages = detection.ok ? detection.missing : [...GLOBAL_TOOLCHAIN_PACKAGES];
|
|
3767
|
+
const missingLocalTools = localCompanionTools.filter((tool) => tool.status !== 'active');
|
|
3650
3768
|
const approval = askGlobalInstallForMissing(options, missingPackages);
|
|
3651
3769
|
if (!approval.approved) {
|
|
3652
3770
|
return { status: 'skipped', reason: approval.source };
|
|
3653
3771
|
}
|
|
3654
3772
|
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
const
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3773
|
+
const installed = [];
|
|
3774
|
+
if (missingPackages.length > 0) {
|
|
3775
|
+
console.log(
|
|
3776
|
+
`[${TOOL_NAME}] Installing global toolchain: npm i -g ${missingPackages.join(' ')}`,
|
|
3777
|
+
);
|
|
3778
|
+
const result = run(NPM_BIN, ['i', '-g', ...missingPackages], { stdio: 'inherit' });
|
|
3779
|
+
if (result.status !== 0) {
|
|
3780
|
+
const stderr = (result.stderr || '').trim();
|
|
3781
|
+
return {
|
|
3782
|
+
status: 'failed',
|
|
3783
|
+
reason: stderr || 'npm global install failed',
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
installed.push(...missingPackages);
|
|
3665
3787
|
}
|
|
3666
3788
|
|
|
3667
|
-
|
|
3789
|
+
for (const tool of missingLocalTools) {
|
|
3790
|
+
console.log(`[${TOOL_NAME}] Installing local companion tool: ${tool.installCommand}`);
|
|
3791
|
+
const result = run(NPX_BIN, tool.installArgs, { stdio: 'inherit' });
|
|
3792
|
+
if (result.status !== 0) {
|
|
3793
|
+
const stderr = (result.stderr || '').trim();
|
|
3794
|
+
return {
|
|
3795
|
+
status: 'failed',
|
|
3796
|
+
reason: stderr || `${tool.name} install failed`,
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
installed.push(tool.name);
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
return { status: 'installed', packages: installed };
|
|
3668
3803
|
}
|
|
3669
3804
|
|
|
3670
3805
|
function gitRefExists(repoRoot, refName) {
|
|
@@ -4015,9 +4150,15 @@ function status(rawArgs) {
|
|
|
4015
4150
|
status: toolchain.installed.includes(pkg) ? 'active' : 'inactive',
|
|
4016
4151
|
};
|
|
4017
4152
|
});
|
|
4153
|
+
const localCompanionServices = detectOptionalLocalCompanionTools().map((tool) => ({
|
|
4154
|
+
name: tool.name,
|
|
4155
|
+
displayName: tool.displayName || tool.name,
|
|
4156
|
+
status: tool.status,
|
|
4157
|
+
}));
|
|
4018
4158
|
const requiredSystemTools = detectRequiredSystemTools();
|
|
4019
4159
|
const services = [
|
|
4020
4160
|
...npmServices,
|
|
4161
|
+
...localCompanionServices,
|
|
4021
4162
|
...requiredSystemTools.map((tool) => ({
|
|
4022
4163
|
name: tool.name,
|
|
4023
4164
|
displayName: tool.displayName || tool.name,
|
|
@@ -4076,6 +4217,17 @@ function status(rawArgs) {
|
|
|
4076
4217
|
const serviceLabel = service.displayName || service.name;
|
|
4077
4218
|
console.log(` - ${statusDot(service.status)} ${serviceLabel}: ${service.status}`);
|
|
4078
4219
|
}
|
|
4220
|
+
const inactiveOptionalCompanions = [...npmServices, ...localCompanionServices]
|
|
4221
|
+
.filter((service) => service.status !== 'active')
|
|
4222
|
+
.map((service) => service.displayName || service.name);
|
|
4223
|
+
if (inactiveOptionalCompanions.length > 0) {
|
|
4224
|
+
console.log(
|
|
4225
|
+
`[${TOOL_NAME}] Optional companion tools inactive: ${inactiveOptionalCompanions.join(', ')}`,
|
|
4226
|
+
);
|
|
4227
|
+
console.log(
|
|
4228
|
+
`[${TOOL_NAME}] Run '${SHORT_TOOL_NAME} setup' to install missing companions with an explicit Y/N prompt.`,
|
|
4229
|
+
);
|
|
4230
|
+
}
|
|
4079
4231
|
const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
|
|
4080
4232
|
if (missingSystemTools.length > 0) {
|
|
4081
4233
|
const tools = missingSystemTools
|
|
@@ -4710,19 +4862,23 @@ function setup(rawArgs) {
|
|
|
4710
4862
|
const globalInstallStatus = installGlobalToolchain(options);
|
|
4711
4863
|
if (globalInstallStatus.status === 'installed') {
|
|
4712
4864
|
console.log(
|
|
4713
|
-
`[${TOOL_NAME}] ✅
|
|
4865
|
+
`[${TOOL_NAME}] ✅ Companion tools installed (${(globalInstallStatus.packages || []).join(', ')}).`,
|
|
4714
4866
|
);
|
|
4715
4867
|
} else if (globalInstallStatus.status === 'already-installed') {
|
|
4716
|
-
console.log(`[${TOOL_NAME}] ✅
|
|
4868
|
+
console.log(`[${TOOL_NAME}] ✅ Companion tools already installed. Skipping.`);
|
|
4717
4869
|
} else if (globalInstallStatus.status === 'failed') {
|
|
4870
|
+
const installCommands = describeCompanionInstallCommands(
|
|
4871
|
+
GLOBAL_TOOLCHAIN_PACKAGES,
|
|
4872
|
+
OPTIONAL_LOCAL_COMPANION_TOOLS,
|
|
4873
|
+
);
|
|
4718
4874
|
console.log(
|
|
4719
4875
|
`[${TOOL_NAME}] ⚠️ Global install failed: ${globalInstallStatus.reason}\n` +
|
|
4720
4876
|
`[${TOOL_NAME}] Continue with local safety setup. You can retry later with:\n` +
|
|
4721
|
-
` ${
|
|
4877
|
+
installCommands.map((command) => ` ${command}`).join('\n'),
|
|
4722
4878
|
);
|
|
4723
4879
|
} else if (globalInstallStatus.status === 'skipped' && globalInstallStatus.reason === 'non-interactive-default') {
|
|
4724
4880
|
console.log(
|
|
4725
|
-
`[${TOOL_NAME}] Skipping
|
|
4881
|
+
`[${TOOL_NAME}] Skipping companion installs (non-interactive mode). ` +
|
|
4726
4882
|
`Use --yes-global-install to force or run interactively for Y/N prompt.`,
|
|
4727
4883
|
);
|
|
4728
4884
|
}
|