unbound-cli 1.7.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/package.json +1 -1
- package/src/commands/setup.js +56 -5
- package/src/commands/tools.js +1 -0
- package/src/index.js +3 -1
- package/src/toolHealth.js +24 -4
- package/test/setup-args.test.js +59 -4
- package/test/tool-health.test.js +66 -2
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ Interactive batch setup:
|
|
|
65
65
|
| Command | Description |
|
|
66
66
|
|---------|-------------|
|
|
67
67
|
| `unbound setup` | Select and install multiple tools interactively |
|
|
68
|
-
| `unbound setup --all` | Install the default bundle: Cursor + Claude Code hooks + Codex hooks |
|
|
68
|
+
| `unbound setup --all` | Install the default bundle: Cursor + Copilot + Augment + Claude Code hooks + Codex hooks |
|
|
69
69
|
|
|
70
70
|
Automated setup (downloads scripts, sets env vars, configures tool):
|
|
71
71
|
|
|
@@ -73,6 +73,7 @@ Automated setup (downloads scripts, sets env vars, configures tool):
|
|
|
73
73
|
|---------|-------------|
|
|
74
74
|
| `unbound setup cursor` | Download hooks, set env, restart Cursor |
|
|
75
75
|
| `unbound setup copilot` | Download hooks, set env, configure GitHub Copilot |
|
|
76
|
+
| `unbound setup augment` | Download hooks, set env, configure Augment Code |
|
|
76
77
|
| `unbound setup claude-code` | Interactive mode selection (subscription or gateway) |
|
|
77
78
|
| `unbound setup claude-code --subscription` | Hooks only (keep your Claude subscription) |
|
|
78
79
|
| `unbound setup claude-code --gateway` | Use Unbound as the AI provider |
|
|
@@ -96,6 +97,7 @@ Remove configuration:
|
|
|
96
97
|
|---------|-------------|
|
|
97
98
|
| `unbound setup cursor --clear` | Remove Unbound config for Cursor |
|
|
98
99
|
| `unbound setup copilot --clear` | Remove Unbound config for GitHub Copilot |
|
|
100
|
+
| `unbound setup augment --clear` | Remove Unbound config for Augment Code |
|
|
99
101
|
| `unbound setup claude-code --clear` | Remove Unbound config for Claude Code |
|
|
100
102
|
| `unbound setup gemini-cli --clear` | Remove Unbound config for Gemini CLI |
|
|
101
103
|
| `unbound setup codex --clear` | Remove Unbound config for Codex |
|
|
@@ -110,7 +112,7 @@ Configure all users on a device via MDM. Requires root.
|
|
|
110
112
|
| `sudo unbound setup --api-key KEY cursor codex-subscription` | Set up specific tools |
|
|
111
113
|
| `sudo unbound setup --clear cursor` | Remove config for specific tools |
|
|
112
114
|
|
|
113
|
-
Available tools: `cursor`, `copilot`, `claude-code-subscription`, `claude-code-gateway`, `gemini-cli`, `codex-subscription`, `codex-gateway`
|
|
115
|
+
Available tools: `cursor`, `copilot`, `augment`, `claude-code-subscription`, `claude-code-gateway`, `gemini-cli`, `codex-subscription`, `codex-gateway`
|
|
114
116
|
|
|
115
117
|
`claude-code-subscription` and `claude-code-gateway` are mutually exclusive. `codex-subscription` and `codex-gateway` are mutually exclusive. When using `--all`, subscription mode is used by default for Claude Code and Codex.
|
|
116
118
|
|
|
@@ -252,7 +254,7 @@ Alias: `unbound groups` works the same as `unbound user-groups`.
|
|
|
252
254
|
| `unbound tools connect <type>` | Connect a tool |
|
|
253
255
|
| `unbound tools approved` | List approved tool types |
|
|
254
256
|
|
|
255
|
-
Supported tool types: `CLAUDE_CODE`, `UNBOUND_CLAUDE_CODE`, `CURSOR`, `COPILOT`, `ROO_CODE`, `CLINE`, `GEMINI_CLI`, `CODEX`, `UNBOUND_CODEX`, `KILO_CODE`, `CUSTOM_ACCESS`
|
|
257
|
+
Supported tool types: `CLAUDE_CODE`, `UNBOUND_CLAUDE_CODE`, `CURSOR`, `COPILOT`, `AUGMENT_CODE`, `ROO_CODE`, `CLINE`, `GEMINI_CLI`, `CODEX`, `UNBOUND_CODEX`, `KILO_CODE`, `CUSTOM_ACCESS`
|
|
256
258
|
|
|
257
259
|
## Configuration
|
|
258
260
|
|
package/package.json
CHANGED
package/src/commands/setup.js
CHANGED
|
@@ -29,6 +29,7 @@ function isWindowsNative() {
|
|
|
29
29
|
const SETUP_TOOLS = [
|
|
30
30
|
{ label: 'Cursor', value: 'cursor', script: 'cursor/setup.py' },
|
|
31
31
|
{ label: 'GitHub Copilot', value: 'copilot', script: 'copilot/hooks/setup.py' },
|
|
32
|
+
{ label: 'Augment Code', value: 'augment', script: 'augment/hooks/setup.py' },
|
|
32
33
|
{ label: 'Claude Code \u2014 subscription (hooks)', value: 'claude-sub', script: 'claude-code/hooks/setup.py', group: 'claude-code' },
|
|
33
34
|
{ label: 'Claude Code \u2014 gateway (gateway)', value: 'claude-gw', script: 'claude-code/gateway/setup.py', group: 'claude-code' },
|
|
34
35
|
{ label: 'Gemini CLI', value: 'gemini', script: 'gemini-cli/gateway/setup.py' },
|
|
@@ -41,6 +42,7 @@ const SETUP_TOOLS = [
|
|
|
41
42
|
const MDM_TOOLS = {
|
|
42
43
|
'cursor': { label: 'Cursor', script: 'cursor/mdm/setup.py' },
|
|
43
44
|
'copilot': { label: 'GitHub Copilot', script: 'copilot/hooks/mdm/setup.py' },
|
|
45
|
+
'augment': { label: 'Augment Code', script: 'augment/hooks/mdm/setup.py' },
|
|
44
46
|
'claude-code-subscription': { label: 'Claude Code', script: 'claude-code/hooks/mdm/setup.py' },
|
|
45
47
|
'claude-code-gateway': { label: 'Claude Code (gateway)', script: 'claude-code/gateway/mdm/setup.py' },
|
|
46
48
|
'gemini-cli': { label: 'Gemini CLI', script: 'gemini-cli/gateway/mdm/setup.py' },
|
|
@@ -49,21 +51,22 @@ const MDM_TOOLS = {
|
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
// Default MDM tools for `sudo unbound onboard` (subscription mode for Claude Code/Codex since only one can be active)
|
|
52
|
-
const MDM_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot'];
|
|
54
|
+
const MDM_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot', 'augment'];
|
|
53
55
|
|
|
54
56
|
// Tools for `sudo unbound setup --all` — identical to MDM_ALL_TOOLS today; split kept for future flexibility.
|
|
55
|
-
const MDM_SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot'];
|
|
57
|
+
const MDM_SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot', 'augment'];
|
|
56
58
|
|
|
57
59
|
// Default tools for `unbound onboard` (Cursor, Claude Code hooks, Codex hooks, Copilot hooks; no Gemini CLI).
|
|
58
|
-
const ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot'];
|
|
60
|
+
const ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot', 'augment'];
|
|
59
61
|
|
|
60
62
|
// Tools for `unbound setup --all` — identical to ALL_TOOLS today; split kept for future flexibility.
|
|
61
|
-
const SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot'];
|
|
63
|
+
const SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot', 'augment'];
|
|
62
64
|
|
|
63
65
|
// Tool name → script mapping for automated tools
|
|
64
66
|
const SETUP_TOOL_MAP = {
|
|
65
67
|
'cursor': { label: 'Cursor', script: 'cursor/setup.py' },
|
|
66
68
|
'copilot': { label: 'GitHub Copilot', script: 'copilot/hooks/setup.py' },
|
|
69
|
+
'augment': { label: 'Augment Code', script: 'augment/hooks/setup.py' },
|
|
67
70
|
'claude-code-subscription': { label: 'Claude Code', script: 'claude-code/hooks/setup.py' },
|
|
68
71
|
'claude-code-gateway': { label: 'Claude Code (gateway)', script: 'claude-code/gateway/setup.py' },
|
|
69
72
|
'gemini-cli': { label: 'Gemini CLI', script: 'gemini-cli/gateway/setup.py' },
|
|
@@ -327,6 +330,7 @@ const NUKE_ENV_VARS = [
|
|
|
327
330
|
'UNBOUND_CODEX_API_KEY',
|
|
328
331
|
'UNBOUND_COPILOT_API_KEY',
|
|
329
332
|
'UNBOUND_CURSOR_API_KEY',
|
|
333
|
+
'UNBOUND_AUGMENT_API_KEY',
|
|
330
334
|
'ANTHROPIC_BASE_URL',
|
|
331
335
|
];
|
|
332
336
|
|
|
@@ -444,6 +448,45 @@ function removeBinaryInstall({
|
|
|
444
448
|
return { removed, failed };
|
|
445
449
|
}
|
|
446
450
|
|
|
451
|
+
|
|
452
|
+
// Run the binary's own `unbound-hook clear` to deregister managed configs
|
|
453
|
+
// (strip our hooks from shared configs, remove our scripts + per-tool logs)
|
|
454
|
+
// BEFORE the install tree is deleted. Essential on binary-only machines that
|
|
455
|
+
// have no python3 to run the downloaded per-tool clears, and authoritative for
|
|
456
|
+
// binary installs generally — it reuses the same clear_setup() as the python
|
|
457
|
+
// clears. Fail-open: never throws; returns { ran, ok }. `binaryPath`/`runner`
|
|
458
|
+
// are overridable so tests stay hermetic.
|
|
459
|
+
function runBinaryClear({
|
|
460
|
+
binaryPath = path.join(BINARY_INSTALL_PATHS.optDir, 'current', 'unbound-hook', 'unbound-hook'),
|
|
461
|
+
runner = spawnSync,
|
|
462
|
+
} = {}) {
|
|
463
|
+
// lstatSync (not existsSync, which follows symlinks) so a present-but-broken
|
|
464
|
+
// `current/...` symlink is still detected — we attempt the clear and surface
|
|
465
|
+
// the failure instead of silently skipping it. A truly absent path (python-only
|
|
466
|
+
// install) throws → no-op.
|
|
467
|
+
let present = false;
|
|
468
|
+
try { fs.lstatSync(binaryPath); present = true; } catch (e) { present = false; }
|
|
469
|
+
if (!present) return { ran: false, ok: true };
|
|
470
|
+
try {
|
|
471
|
+
// timeout so a hung clear can never block nuke from removing the binary.
|
|
472
|
+
const r = runner(binaryPath, ['clear'], { stdio: 'pipe', timeout: 120000, killSignal: 'SIGKILL' });
|
|
473
|
+
if (r.error) {
|
|
474
|
+
const why = r.error.code === 'ETIMEDOUT' ? 'timed out (120s)' : r.error.message;
|
|
475
|
+
output.warn(`nuke: unbound-hook clear ${why}; continuing.`);
|
|
476
|
+
return { ran: true, ok: false };
|
|
477
|
+
}
|
|
478
|
+
if (r.status !== 0) {
|
|
479
|
+
const stderr = r.stderr ? r.stderr.toString().trim() : '';
|
|
480
|
+
output.warn(`nuke: unbound-hook clear exited ${r.status}: ${stderr || '(no stderr)'}`);
|
|
481
|
+
return { ran: true, ok: false };
|
|
482
|
+
}
|
|
483
|
+
return { ran: true, ok: true };
|
|
484
|
+
} catch (err) {
|
|
485
|
+
output.warn(`nuke: unbound-hook clear errored: ${err.message}`);
|
|
486
|
+
return { ran: true, ok: false };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
447
490
|
/**
|
|
448
491
|
* Returns true when the process has the privileges needed to touch system-level
|
|
449
492
|
* (MDM) configuration. On Windows, `net session` succeeds only when elevated, so
|
|
@@ -585,6 +628,7 @@ Scope is chosen automatically from your privileges:
|
|
|
585
628
|
Available tools:
|
|
586
629
|
cursor Cursor IDE
|
|
587
630
|
copilot GitHub Copilot
|
|
631
|
+
augment Augment Code
|
|
588
632
|
claude-code Claude Code (use --subscription or --gateway)
|
|
589
633
|
gemini-cli Gemini CLI
|
|
590
634
|
codex Codex (use --subscription or --gateway)
|
|
@@ -603,11 +647,12 @@ Examples:
|
|
|
603
647
|
Single tool:
|
|
604
648
|
$ unbound setup cursor Set up Cursor
|
|
605
649
|
$ unbound setup copilot Set up GitHub Copilot
|
|
650
|
+
$ unbound setup augment Set up Augment Code
|
|
606
651
|
$ unbound setup claude-code --gateway Claude Code gateway mode
|
|
607
652
|
$ unbound setup claude-code --subscription Claude Code hooks only
|
|
608
653
|
$ unbound setup codex --gateway Codex gateway mode
|
|
609
654
|
|
|
610
|
-
Install the default bundle (Cursor + Copilot + Claude Code hooks + Codex hooks):
|
|
655
|
+
Install the default bundle (Cursor + Copilot + Augment + Claude Code hooks + Codex hooks):
|
|
611
656
|
$ unbound setup --all Set up the default bundle
|
|
612
657
|
$ unbound setup --all --api-key <key> Login + set up the bundle
|
|
613
658
|
|
|
@@ -1041,8 +1086,13 @@ Examples:
|
|
|
1041
1086
|
// `_confirmProductionPaths` is the explicit "this is the real call site"
|
|
1042
1087
|
// ack — the function refuses production defaults without it as a guard
|
|
1043
1088
|
// against tests forgetting to override paths.
|
|
1089
|
+
// Deregister managed configs via the binary's own clear FIRST (works
|
|
1090
|
+
// without python3, which a binary-only machine lacks); reuses the same
|
|
1091
|
+
// clear_setup() as the python clears. Must precede the rm below.
|
|
1092
|
+
const binClear = runBinaryClear();
|
|
1044
1093
|
const { removed: binaryRemoved, failed: bf } = removeBinaryInstall({ _confirmProductionPaths: true });
|
|
1045
1094
|
binaryFailed = bf;
|
|
1095
|
+
if (binClear.ran && !binClear.ok) binaryFailed.push('unbound-hook clear');
|
|
1046
1096
|
if (binaryRemoved.length) {
|
|
1047
1097
|
output.info(`Removed binary install: ${binaryRemoved.join('; ')}.`);
|
|
1048
1098
|
}
|
|
@@ -1135,5 +1185,6 @@ module.exports = {
|
|
|
1135
1185
|
clearUnboundEnvsEverywhere,
|
|
1136
1186
|
NUKE_ENV_VARS,
|
|
1137
1187
|
removeBinaryInstall,
|
|
1188
|
+
runBinaryClear,
|
|
1138
1189
|
BINARY_INSTALL_PATHS,
|
|
1139
1190
|
};
|
package/src/commands/tools.js
CHANGED
package/src/index.js
CHANGED
|
@@ -60,9 +60,10 @@ ONBOARDING (one-step install + discover; scope auto-detected from sudo)
|
|
|
60
60
|
|
|
61
61
|
TOOL SETUP
|
|
62
62
|
$ unbound setup Select and install multiple tools interactively
|
|
63
|
-
$ unbound setup --all Set up the default bundle (Cursor + Copilot + Claude Code hooks + Codex hooks)
|
|
63
|
+
$ unbound setup --all Set up the default bundle (Cursor + Copilot + Augment + Claude Code hooks + Codex hooks)
|
|
64
64
|
$ unbound setup cursor Set up Cursor
|
|
65
65
|
$ unbound setup copilot Set up GitHub Copilot
|
|
66
|
+
$ unbound setup augment Set up Augment Code
|
|
66
67
|
$ unbound setup claude-code Set up Claude Code (interactive mode selection)
|
|
67
68
|
$ unbound setup claude-code --gateway Use Unbound as AI provider
|
|
68
69
|
$ unbound setup claude-code --subscription Hooks only (keep your subscription)
|
|
@@ -86,6 +87,7 @@ TOOL SETUP
|
|
|
86
87
|
Remove configuration (no API key needed to clear):
|
|
87
88
|
$ unbound setup cursor --clear Remove Unbound config for Cursor
|
|
88
89
|
$ unbound setup copilot --clear Remove Unbound config for GitHub Copilot
|
|
90
|
+
$ unbound setup augment --clear Remove Unbound config for Augment Code
|
|
89
91
|
$ unbound setup claude-code --clear Remove Unbound config for Claude Code
|
|
90
92
|
$ unbound setup gemini-cli --clear Remove Unbound config for Gemini CLI
|
|
91
93
|
$ unbound setup codex --clear Remove Unbound config for Codex
|
package/src/toolHealth.js
CHANGED
|
@@ -181,10 +181,10 @@ function envCheck(label, name, expected, kind = 'aux') {
|
|
|
181
181
|
// and a managed config that points at a missing hook script is a broken (tampered)
|
|
182
182
|
// MDM install, not a healthy one. Returns { status: 'healthy'|'tampered'|null, checks }.
|
|
183
183
|
function mdmDetect(family, dirOverride, binaryPath) {
|
|
184
|
-
// Only Cursor / Claude Code / Codex have a managed (MDM) directory.
|
|
185
|
-
// Gemini have none — their org install writes the same per-user
|
|
186
|
-
// every profile, so they're checked exactly like a user-level tool.
|
|
187
|
-
const configName = { cursor: 'hooks.json', 'claude-code': 'managed-settings.json', codex: 'managed-settings.json' };
|
|
184
|
+
// Only Cursor / Claude Code / Codex / Augment have a managed (MDM) directory.
|
|
185
|
+
// Copilot and Gemini have none — their org install writes the same per-user
|
|
186
|
+
// config into every profile, so they're checked exactly like a user-level tool.
|
|
187
|
+
const configName = { cursor: 'hooks.json', 'claude-code': 'managed-settings.json', codex: 'managed-settings.json', augment: 'settings.json' };
|
|
188
188
|
if (!configName[family]) return { status: null, checks: [] };
|
|
189
189
|
|
|
190
190
|
const mac = '/Library/Application Support';
|
|
@@ -193,6 +193,9 @@ function mdmDetect(family, dirOverride, binaryPath) {
|
|
|
193
193
|
cursor: { darwin: `${mac}/Cursor`, linux: '/etc/cursor', win32: path.join(win, 'Cursor') },
|
|
194
194
|
'claude-code': { darwin: `${mac}/ClaudeCode`, linux: '/etc/claude-code', win32: path.join(win, 'ClaudeCode') },
|
|
195
195
|
codex: { darwin: `${mac}/Codex`, linux: '/etc/codex', win32: path.join(win, 'Codex') },
|
|
196
|
+
// Augment's managed dir is /etc/augment on BOTH macOS and Linux (mirrors the
|
|
197
|
+
// setup repo's get_managed_settings_dir), not Library/Application Support.
|
|
198
|
+
augment: { darwin: '/etc/augment', linux: '/etc/augment', win32: path.join(win, 'Augment') },
|
|
196
199
|
};
|
|
197
200
|
const dir = dirOverride || dirs[family][process.platform] || dirs[family].linux;
|
|
198
201
|
const configPath = path.join(dir, configName[family]);
|
|
@@ -305,6 +308,23 @@ function buildVariants(gatewayUrl, binaryPath) {
|
|
|
305
308
|
return checks;
|
|
306
309
|
},
|
|
307
310
|
},
|
|
311
|
+
{
|
|
312
|
+
key: 'augment', label: 'Augment Code', family: 'augment', mode: null,
|
|
313
|
+
checks: () => {
|
|
314
|
+
// ~/.augment/settings.json is shared with the user's own Augment config and may be
|
|
315
|
+
// JSONC, so match the Unbound hook marker on RAW TEXT (not strict JSON). Like
|
|
316
|
+
// copilot, a user install can point at the python unbound.py hook OR the shared
|
|
317
|
+
// unbound-hook binary — check whichever the config references so a binary-backed
|
|
318
|
+
// install isn't misread as tampered.
|
|
319
|
+
const cfgText = readText('~/.augment/settings.json');
|
|
320
|
+
const hasBinary = cfgText != null && cfgText.includes(BINARY_MARKER);
|
|
321
|
+
const checks = [configCheck('Config', '~/.augment/settings.json', { json: false, test: (t) => t.includes('unbound.py') || t.includes(BINARY_MARKER) })];
|
|
322
|
+
if (hasBinary) checks.push(scriptCheck('Hook binary', binaryPath || BINARY_PATH));
|
|
323
|
+
else checks.push(scriptCheck('Hook script', '~/.augment/hooks/unbound.py'));
|
|
324
|
+
checks.push(envKeyCheck('API key env', 'UNBOUND_AUGMENT_API_KEY'));
|
|
325
|
+
return checks;
|
|
326
|
+
},
|
|
327
|
+
},
|
|
308
328
|
// Gemini CLI is intentionally omitted here — it isn't part of `setup --all`
|
|
309
329
|
// and has no managed directory. Add it back when its scope is settled.
|
|
310
330
|
];
|
package/test/setup-args.test.js
CHANGED
|
@@ -3,7 +3,7 @@ const assert = require('node:assert/strict');
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { buildScriptArgs, scriptSupportsBackfill, resolveSetupAllTools, clearUnboundEnvsEverywhere, NUKE_ENV_VARS, removeBinaryInstall, BINARY_INSTALL_PATHS } = require('../src/commands/setup');
|
|
6
|
+
const { buildScriptArgs, scriptSupportsBackfill, resolveSetupAllTools, clearUnboundEnvsEverywhere, NUKE_ENV_VARS, removeBinaryInstall, runBinaryClear, BINARY_INSTALL_PATHS } = require('../src/commands/setup');
|
|
7
7
|
|
|
8
8
|
// shellEscape single-quotes every value, so a real key surfaces as
|
|
9
9
|
// --api-key '<key>' at the head of the argv tail.
|
|
@@ -130,8 +130,8 @@ test('buildScriptArgs: mdm clear without key emits --clear and no --api-key/--do
|
|
|
130
130
|
test('resolveSetupAllTools(false): install bundle is subscription-only, no gateway/gemini', () => {
|
|
131
131
|
const tools = resolveSetupAllTools(false);
|
|
132
132
|
// Membership + length (order-independent) so reordering SETUP_ALL_TOOLS doesn't break this.
|
|
133
|
-
assert.equal(tools.length,
|
|
134
|
-
for (const t of ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot']) {
|
|
133
|
+
assert.equal(tools.length, 5, `expected 5 tools, got ${tools.join(',')}`);
|
|
134
|
+
for (const t of ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot', 'augment']) {
|
|
135
135
|
assert.ok(tools.includes(t), `install bundle missing ${t}: ${tools.join(',')}`);
|
|
136
136
|
}
|
|
137
137
|
assert.ok(!tools.includes('claude-code-gateway'), tools.join(','));
|
|
@@ -141,7 +141,7 @@ test('resolveSetupAllTools(false): install bundle is subscription-only, no gatew
|
|
|
141
141
|
|
|
142
142
|
test('resolveSetupAllTools(true): clear-all covers every tool incl. gateway modes + gemini', () => {
|
|
143
143
|
const tools = resolveSetupAllTools(true);
|
|
144
|
-
for (const t of ['cursor', 'copilot', 'claude-code-subscription', 'claude-code-gateway',
|
|
144
|
+
for (const t of ['cursor', 'copilot', 'augment', 'claude-code-subscription', 'claude-code-gateway',
|
|
145
145
|
'gemini-cli', 'codex-subscription', 'codex-gateway']) {
|
|
146
146
|
assert.ok(tools.includes(t), `clear-all missing ${t}: ${tools.join(',')}`);
|
|
147
147
|
}
|
|
@@ -396,3 +396,58 @@ test('removeBinaryInstall: refuses production-default paths without _confirmProd
|
|
|
396
396
|
test('BINARY_INSTALL_PATHS: is frozen', () => {
|
|
397
397
|
assert.ok(Object.isFrozen(BINARY_INSTALL_PATHS));
|
|
398
398
|
});
|
|
399
|
+
|
|
400
|
+
test('runBinaryClear: binary present — runs `<bin> clear` before removal, reports ok', () => {
|
|
401
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-binclear-'));
|
|
402
|
+
const bin = path.join(tmp, 'unbound-hook');
|
|
403
|
+
fs.writeFileSync(bin, '#');
|
|
404
|
+
const calls = [];
|
|
405
|
+
try {
|
|
406
|
+
const r = runBinaryClear({ binaryPath: bin, runner: (b, args, opts) => { calls.push([b, args, opts]); return { status: 0 }; } });
|
|
407
|
+
assert.deepEqual(r, { ran: true, ok: true });
|
|
408
|
+
assert.equal(calls.length, 1, 'binary clear should run exactly once');
|
|
409
|
+
assert.equal(calls[0][0], bin);
|
|
410
|
+
assert.deepEqual(calls[0][1], ['clear']);
|
|
411
|
+
assert.ok(calls[0][2].timeout > 0, 'clear must run with a timeout so a hang cannot block nuke');
|
|
412
|
+
} finally {
|
|
413
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('runBinaryClear: dangling binary symlink — still attempts (surfaces, not silently skipped)', () => {
|
|
418
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-binclear-'));
|
|
419
|
+
const link = path.join(tmp, 'unbound-hook');
|
|
420
|
+
fs.symlinkSync(path.join(tmp, 'missing-target'), link); // dangling: target absent
|
|
421
|
+
let called = false;
|
|
422
|
+
try {
|
|
423
|
+
const r = runBinaryClear({ binaryPath: link, runner: () => { called = true; return { error: { code: 'ENOENT', message: 'spawn ENOENT' } }; } });
|
|
424
|
+
assert.equal(called, true, 'a dangling symlink must still be attempted, not treated as absent');
|
|
425
|
+
assert.deepEqual(r, { ran: true, ok: false });
|
|
426
|
+
} finally {
|
|
427
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test('runBinaryClear: binary absent (python-only install) — no-op, runner never called', () => {
|
|
432
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-binclear-'));
|
|
433
|
+
let called = false;
|
|
434
|
+
try {
|
|
435
|
+
const r = runBinaryClear({ binaryPath: path.join(tmp, 'nope'), runner: () => { called = true; return { status: 0 }; } });
|
|
436
|
+
assert.deepEqual(r, { ran: false, ok: true });
|
|
437
|
+
assert.equal(called, false);
|
|
438
|
+
} finally {
|
|
439
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test('runBinaryClear: clear exits non-zero — fail-open (ran:true, ok:false), never throws', () => {
|
|
444
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-binclear-'));
|
|
445
|
+
const bin = path.join(tmp, 'unbound-hook');
|
|
446
|
+
fs.writeFileSync(bin, '#');
|
|
447
|
+
try {
|
|
448
|
+
const r = runBinaryClear({ binaryPath: bin, runner: () => ({ status: 1, stderr: Buffer.from('boom') }) });
|
|
449
|
+
assert.deepEqual(r, { ran: true, ok: false });
|
|
450
|
+
} finally {
|
|
451
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
452
|
+
}
|
|
453
|
+
});
|
package/test/tool-health.test.js
CHANGED
|
@@ -188,6 +188,70 @@ test('MDM: a user-level install takes precedence over an MDM config', () => {
|
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
test('augment user install: ~/.augment settings + hook + key → healthy (user scope, not mdm)', () => {
|
|
192
|
+
withHome((tmp, th) => {
|
|
193
|
+
writeFile(path.join(tmp, '.augment', 'settings.json'), JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ command: path.join(tmp, '.augment', 'hooks', 'unbound.py') }] }] } }));
|
|
194
|
+
writeFile(path.join(tmp, '.augment', 'hooks', 'unbound.py'), '#');
|
|
195
|
+
process.env.UNBOUND_AUGMENT_API_KEY = 'k';
|
|
196
|
+
try {
|
|
197
|
+
const t = th.detectTools({ apiKey: 'k' }).find((x) => x.family === 'augment');
|
|
198
|
+
assert.equal(t.status, 'healthy');
|
|
199
|
+
assert.notEqual(t.scope, 'mdm');
|
|
200
|
+
} finally { delete process.env.UNBOUND_AUGMENT_API_KEY; }
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('augment user install: JSONC settings (a // comment strict JSON.parse rejects) still detected via raw-text marker → healthy', () => {
|
|
205
|
+
withHome((tmp, th) => {
|
|
206
|
+
const scriptPath = path.join(tmp, '.augment', 'hooks', 'unbound.py');
|
|
207
|
+
const jsonc = `{\n // unbound hook (JSONC comment)\n "hooks": { "PreToolUse": [{ "hooks": [{ "command": ${JSON.stringify(scriptPath)} }] }] }\n}`;
|
|
208
|
+
writeFile(path.join(tmp, '.augment', 'settings.json'), jsonc);
|
|
209
|
+
writeFile(scriptPath, '#');
|
|
210
|
+
process.env.UNBOUND_AUGMENT_API_KEY = 'k';
|
|
211
|
+
try {
|
|
212
|
+
const t = th.detectTools({ apiKey: 'k' }).find((x) => x.family === 'augment');
|
|
213
|
+
assert.equal(t.status, 'healthy');
|
|
214
|
+
} finally { delete process.env.UNBOUND_AUGMENT_API_KEY; }
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('augment user install: settings point at the unbound-hook binary + binary present → healthy (not tampered, not mdm)', () => {
|
|
219
|
+
withHome((tmp, th) => {
|
|
220
|
+
const bin = path.join(tmp, 'opt', 'unbound-hook', 'unbound-hook');
|
|
221
|
+
writeFile(bin, '#');
|
|
222
|
+
writeFile(path.join(tmp, '.augment', 'settings.json'), JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ command: `"${bin}" hook augment PreToolUse` }] }] } }));
|
|
223
|
+
process.env.UNBOUND_AUGMENT_API_KEY = 'k';
|
|
224
|
+
try {
|
|
225
|
+
const t = th.detectTools({ apiKey: 'k', _binaryPath: bin }).find((x) => x.family === 'augment');
|
|
226
|
+
assert.equal(t.status, 'healthy');
|
|
227
|
+
assert.notEqual(t.scope, 'mdm');
|
|
228
|
+
} finally { delete process.env.UNBOUND_AUGMENT_API_KEY; }
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('MDM: augment managed config (python) references unbound + hook present → managed by MDM', () => {
|
|
233
|
+
withHome((tmp, th) => {
|
|
234
|
+
const mdmDir = path.join(tmp, 'mdm', 'Augment');
|
|
235
|
+
writeFile(path.join(mdmDir, 'settings.json'), JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ command: path.join(mdmDir, 'hooks', 'unbound.py') }] }] } }));
|
|
236
|
+
writeFile(path.join(mdmDir, 'hooks', 'unbound.py'), '#');
|
|
237
|
+
const t = th.detectTools({ apiKey: 'k', _mdmDirs: { augment: mdmDir } }).find((x) => x.family === 'augment');
|
|
238
|
+
assert.equal(t.status, 'managed-by-mdm');
|
|
239
|
+
assert.equal(t.scope, 'mdm');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('MDM: augment managed config (binary) references the hook binary + binary present → managed by MDM', () => {
|
|
244
|
+
withHome((tmp, th) => {
|
|
245
|
+
const mdmDir = path.join(tmp, 'mdm', 'Augment');
|
|
246
|
+
const bin = path.join(tmp, 'opt', 'unbound-hook', 'unbound-hook');
|
|
247
|
+
writeFile(bin, '#');
|
|
248
|
+
writeFile(path.join(mdmDir, 'settings.json'), JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ command: `"${bin}" hook augment PreToolUse` }] }] } }));
|
|
249
|
+
const t = th.detectTools({ apiKey: 'k', _mdmDirs: { augment: mdmDir }, _binaryPath: bin }).find((x) => x.family === 'augment');
|
|
250
|
+
assert.equal(t.status, 'managed-by-mdm');
|
|
251
|
+
assert.equal(t.scope, 'mdm');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
191
255
|
test('codex subscription: hooks.json + script present but the feature flag is off → tampered', () => {
|
|
192
256
|
withHome((tmp, th) => {
|
|
193
257
|
writeFile(path.join(tmp, '.codex', 'hooks.json'), JSON.stringify({ hooks: { x: [{ command: path.join(tmp, '.codex', 'hooks', 'unbound.py') }] } }));
|
|
@@ -206,10 +270,10 @@ test('copilot has no MDM scope — even with a managed dir it never reports mana
|
|
|
206
270
|
});
|
|
207
271
|
});
|
|
208
272
|
|
|
209
|
-
test('only
|
|
273
|
+
test('only five tools are reported (Gemini CLI is omitted)', () => {
|
|
210
274
|
withHome((tmp, th) => {
|
|
211
275
|
const keys = th.detectTools({ apiKey: 'k' }).map((t) => t.family);
|
|
212
|
-
assert.deepEqual(new Set(keys), new Set(['cursor', 'claude-code', 'codex', 'copilot']));
|
|
276
|
+
assert.deepEqual(new Set(keys), new Set(['cursor', 'claude-code', 'codex', 'copilot', 'augment']));
|
|
213
277
|
});
|
|
214
278
|
});
|
|
215
279
|
|