@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 +47 -18
- package/bin/multiagent-safety.js +253 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# GitGuardex — Guardian T-Rex for your repo
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@imdeadpool/guardex)
|
|
4
|
-
[](https://www.npmjs.com/package/@imdeadpool/guardex)
|
|
4
|
+
[](https://www.npmjs.com/package/@imdeadpool/guardex)
|
|
5
|
+
[](https://github.com/recodeee/gitguardex/stargazers)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
[](https://github.com/recodeee/gitguardex/actions/workflows/ci.yml)
|
|
9
|
+
[](https://github.com/recodeee/gitguardex/actions/workflows/release.yml)
|
|
10
|
+
[](https://github.com/recodeee/gitguardex/actions/workflows/codeql.yml)
|
|
5
11
|
[](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
|
-
|
|
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
|
|
30
|
-
B[Agent B
|
|
31
|
-
C[Agent C
|
|
32
|
-
D[Agent D
|
|
33
|
-
E[Agent E
|
|
34
|
-
S --> F[
|
|
35
|
-
F --> G[
|
|
36
|
-
G --> H[
|
|
37
|
-
H --> I[Regression risk
|
|
38
|
-
I -->
|
|
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
|
+

|
|
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
|
-

|
|
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
|
|
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
|
|
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
|
+
[](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
|
+
[](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
|
+
[](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
|
+
[](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
|
+
[](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
|
+
[](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
|
+
[](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.
|
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
|
|
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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: '
|
|
3666
|
-
reason:
|
|
3813
|
+
status: 'skipped',
|
|
3814
|
+
reason: approval.source,
|
|
3815
|
+
missingPackages,
|
|
3816
|
+
missingLocalTools,
|
|
3667
3817
|
};
|
|
3668
3818
|
}
|
|
3669
3819
|
|
|
3670
|
-
|
|
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 {
|
|
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:
|
|
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}] ✅
|
|
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
|
|
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
|
-
` ${
|
|
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
|
|
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');
|