robot-resources 1.12.4 → 1.14.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/lib/install-node-shim.js +80 -34
- package/lib/install-router-files.js +48 -0
- package/lib/non-oc-wizard.js +12 -3
- package/lib/shell-config.js +25 -10
- package/lib/uninstall.js +38 -10
- package/lib/windows-env.js +202 -0
- package/package.json +1 -1
package/lib/install-node-shim.js
CHANGED
|
@@ -1,65 +1,110 @@
|
|
|
1
1
|
import { writeShellLine, hasShellLine } from './shell-config.js';
|
|
2
2
|
import { readConfig } from './config.mjs';
|
|
3
3
|
import { detectNodeAgent } from './detect.js';
|
|
4
|
+
import { installRouterFiles } from './install-router-files.js';
|
|
5
|
+
import { writePersistedNodeOptions } from './windows-env.js';
|
|
4
6
|
|
|
5
7
|
const PLATFORM_URL = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
|
-
* Install the Node shim into the user's shell config
|
|
9
|
-
*
|
|
10
|
+
* Install the Node shim into the user's shell config (POSIX) or user
|
|
11
|
+
* environment registry (Windows).
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
* 1.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* detected, dry-run flag, plus per-file errors.
|
|
13
|
+
* POSIX path (Phases 3 + 8):
|
|
14
|
+
* 1. Copy `@robot-resources/router` to ~/.robot-resources/router/
|
|
15
|
+
* (absolute path; survives cwd changes + npm/npx cache cleanup).
|
|
16
|
+
* 2. Append marker block with `--require <abs path>` to detected rc files
|
|
17
|
+
* (zsh / bash / fish).
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Windows path (Phase 9):
|
|
20
|
+
* 1. Same router-files copy — `homedir()` is platform-aware.
|
|
21
|
+
* 2. `setx NODE_OPTIONS "..."` writes to HKCU\\Environment so every new
|
|
22
|
+
* cmd / PowerShell / Win+R-launched Node process inherits it.
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* for Windows users in `non-oc-wizard.js`.
|
|
24
|
+
* Both paths emit `node_shim_installed` telemetry. The user has to open a
|
|
25
|
+
* new terminal for the change to take effect.
|
|
26
26
|
*
|
|
27
27
|
* Returns a UI-friendly result the wizard can format and print.
|
|
28
28
|
*/
|
|
29
29
|
export async function installNodeShim({ cwd = process.cwd(), dryRun = false } = {}) {
|
|
30
|
-
|
|
30
|
+
const sdks = detectSdks(cwd);
|
|
31
|
+
|
|
32
|
+
if (dryRun) {
|
|
33
|
+
await emit({
|
|
34
|
+
shell: 'dryrun',
|
|
35
|
+
shell_config_path: null,
|
|
36
|
+
sdks_detected: sdks,
|
|
37
|
+
dry_run: true,
|
|
38
|
+
reason: null,
|
|
39
|
+
});
|
|
40
|
+
return { ok: true, message: 'Dry-run: would have written NODE_OPTIONS.' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Phase 8: copy router to an absolute path under ~/.robot-resources/router/
|
|
44
|
+
// before we wire the env config. If the copy fails, we don't write a
|
|
45
|
+
// broken NODE_OPTIONS line on either platform.
|
|
46
|
+
let autoPath;
|
|
47
|
+
try {
|
|
48
|
+
autoPath = installRouterFiles();
|
|
49
|
+
} catch (err) {
|
|
31
50
|
await emit({
|
|
32
|
-
shell: '
|
|
51
|
+
shell: process.platform === 'win32' ? 'win32' : 'unknown',
|
|
33
52
|
shell_config_path: null,
|
|
34
|
-
sdks_detected:
|
|
35
|
-
dry_run:
|
|
36
|
-
reason: '
|
|
53
|
+
sdks_detected: sdks,
|
|
54
|
+
dry_run: false,
|
|
55
|
+
reason: 'router_copy_failed',
|
|
56
|
+
error_messages: [err.message],
|
|
37
57
|
});
|
|
38
58
|
return {
|
|
39
59
|
ok: false,
|
|
40
|
-
|
|
41
|
-
message:
|
|
42
|
-
'Windows shell-config writing is not yet supported. Set ' +
|
|
43
|
-
'NODE_OPTIONS=--require @robot-resources/router/auto manually in your ' +
|
|
44
|
-
'system environment variables, or wait for Phase 6.',
|
|
60
|
+
message: `Could not copy router files to ~/.robot-resources/router/: ${err.message}`,
|
|
45
61
|
};
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
// Windows branch — Phase 9. Mirrors the POSIX flow in shape: detect
|
|
65
|
+
// already-installed via the persisted registry value, write via setx,
|
|
66
|
+
// emit equivalent telemetry.
|
|
67
|
+
if (process.platform === 'win32') {
|
|
68
|
+
const winResult = writePersistedNodeOptions({ autoPath });
|
|
51
69
|
await emit({
|
|
52
|
-
shell: '
|
|
53
|
-
shell_config_path:
|
|
70
|
+
shell: 'win32',
|
|
71
|
+
shell_config_path: 'HKCU\\Environment\\NODE_OPTIONS',
|
|
54
72
|
sdks_detected: sdks,
|
|
55
|
-
dry_run:
|
|
56
|
-
|
|
73
|
+
dry_run: false,
|
|
74
|
+
already_installed: !!winResult.already,
|
|
75
|
+
files_written: winResult.ok && !winResult.already ? 1 : 0,
|
|
76
|
+
files_with_errors: winResult.ok ? 0 : 1,
|
|
77
|
+
error_messages: winResult.ok ? [] : [winResult.error_message || winResult.reason || 'unknown'],
|
|
78
|
+
auto_path: autoPath,
|
|
79
|
+
win_node_options_length: winResult.length,
|
|
80
|
+
reason: winResult.ok ? null : winResult.reason,
|
|
57
81
|
});
|
|
58
|
-
|
|
82
|
+
if (winResult.ok && winResult.already) {
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
already: true,
|
|
86
|
+
message: 'NODE_OPTIONS already includes the auto-attach line. No changes made.',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (!winResult.ok) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: winResult.reason,
|
|
93
|
+
message: `Could not set NODE_OPTIONS via setx (${winResult.reason}): ${winResult.error_message || ''}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
written: ['HKCU\\Environment\\NODE_OPTIONS'],
|
|
99
|
+
errors: [],
|
|
100
|
+
message:
|
|
101
|
+
'Set NODE_OPTIONS in your user environment (HKCU\\Environment). ' +
|
|
102
|
+
'Open a new terminal for it to take effect. Existing terminals will not see the change.',
|
|
103
|
+
};
|
|
59
104
|
}
|
|
60
105
|
|
|
61
106
|
const alreadyInstalled = hasShellLine();
|
|
62
|
-
const result = writeShellLine();
|
|
107
|
+
const result = writeShellLine({ autoPath });
|
|
63
108
|
|
|
64
109
|
// Single shell value for the funnel even though we may have written to
|
|
65
110
|
// multiple rc files. Pick the dominant one for telemetry.
|
|
@@ -74,6 +119,7 @@ export async function installNodeShim({ cwd = process.cwd(), dryRun = false } =
|
|
|
74
119
|
files_written: result.written.length,
|
|
75
120
|
files_with_errors: result.errors.length,
|
|
76
121
|
error_messages: result.errors.map((e) => `${e.path}: ${e.message}`).slice(0, 3),
|
|
122
|
+
auto_path: autoPath,
|
|
77
123
|
});
|
|
78
124
|
|
|
79
125
|
if (alreadyInstalled && result.written.length === 0) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, copyFileSync, cpSync, rmSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Copy `@robot-resources/router` to ~/.robot-resources/router/ and return
|
|
10
|
+
* the absolute path to its auto.cjs.
|
|
11
|
+
*
|
|
12
|
+
* Phase 8 fix. Mirrors `installPluginFiles()` in tool-config.js (the OC
|
|
13
|
+
* plugin path that's worked since Phase 0). The destination is a stable
|
|
14
|
+
* user-scoped location that survives npm/npx cache cleanups, so the
|
|
15
|
+
* NODE_OPTIONS line we write to shell rc doesn't break when caches expire.
|
|
16
|
+
*
|
|
17
|
+
* Files copied: auto.cjs, index.js, package.json, lib/ (recursive).
|
|
18
|
+
* Each call wipes lib/ first so files removed in newer router versions
|
|
19
|
+
* don't linger from a previous install.
|
|
20
|
+
*
|
|
21
|
+
* Why this is its own module: extracted from install-node-shim.js for
|
|
22
|
+
* testability — vitest can mock the whole module without us mocking
|
|
23
|
+
* node:fs / node:module manually in every install-shim test case.
|
|
24
|
+
*/
|
|
25
|
+
export function installRouterFiles({ home = homedir() } = {}) {
|
|
26
|
+
const pkgPath = require.resolve('@robot-resources/router/package.json');
|
|
27
|
+
const pkgDir = dirname(pkgPath);
|
|
28
|
+
const targetDir = join(home, '.robot-resources', 'router');
|
|
29
|
+
mkdirSync(targetDir, { recursive: true });
|
|
30
|
+
|
|
31
|
+
for (const file of ['auto.cjs', 'index.js', 'package.json']) {
|
|
32
|
+
const src = join(pkgDir, file);
|
|
33
|
+
if (existsSync(src)) {
|
|
34
|
+
copyFileSync(src, join(targetDir, file));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Refresh lib/ — wipe + recopy so we don't accumulate stale files across
|
|
39
|
+
// router upgrades. Same pattern as tool-config.js installPluginFiles().
|
|
40
|
+
const srcLib = join(pkgDir, 'lib');
|
|
41
|
+
const dstLib = join(targetDir, 'lib');
|
|
42
|
+
if (existsSync(srcLib)) {
|
|
43
|
+
rmSync(dstLib, { recursive: true, force: true });
|
|
44
|
+
cpSync(srcLib, dstLib, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return join(targetDir, 'auto.cjs');
|
|
48
|
+
}
|
package/lib/non-oc-wizard.js
CHANGED
|
@@ -107,12 +107,21 @@ async function showJsPath() {
|
|
|
107
107
|
blank();
|
|
108
108
|
info('Once your shell picks up the new NODE_OPTIONS, every Node agent on');
|
|
109
109
|
info('this machine routes Anthropic SDK calls through Robot Resources.');
|
|
110
|
-
|
|
110
|
+
if (process.platform === 'win32') {
|
|
111
|
+
info('Open a new cmd / PowerShell window — current terminals will not see the change.');
|
|
112
|
+
} else {
|
|
113
|
+
info('Open a new terminal — or run: source ~/.zshrc (or your shell rc)');
|
|
114
|
+
}
|
|
111
115
|
} else {
|
|
112
116
|
warn(result.message);
|
|
113
117
|
blank();
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
if (process.platform === 'win32') {
|
|
119
|
+
info('Manual install: in a new cmd, run:');
|
|
120
|
+
info(' setx NODE_OPTIONS "--require %USERPROFILE%\\.robot-resources\\router\\auto.cjs"');
|
|
121
|
+
} else {
|
|
122
|
+
info('Manual install (paste into ~/.zshrc or ~/.bashrc):');
|
|
123
|
+
info(' export NODE_OPTIONS="${NODE_OPTIONS:-} --require ~/.robot-resources/router/auto.cjs"');
|
|
124
|
+
}
|
|
116
125
|
}
|
|
117
126
|
blank();
|
|
118
127
|
info('Docs: https://robotresources.ai/docs/langchain');
|
package/lib/shell-config.js
CHANGED
|
@@ -11,10 +11,19 @@ import { join } from 'node:path';
|
|
|
11
11
|
* regex-matching against the user's actual shell content:
|
|
12
12
|
*
|
|
13
13
|
* # >>> robot-resources: NODE_OPTIONS auto-attach >>>
|
|
14
|
-
* export NODE_OPTIONS="${NODE_OPTIONS:-} --require
|
|
14
|
+
* export NODE_OPTIONS="${NODE_OPTIONS:-} --require /Users/x/.robot-resources/router/auto.cjs"
|
|
15
15
|
* # <<< robot-resources <<<
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* Phase 8 fix: NODE_OPTIONS now uses an ABSOLUTE PATH to the auto.cjs the
|
|
18
|
+
* wizard copied to ~/.robot-resources/router/. The previous bare-module
|
|
19
|
+
* form `--require @robot-resources/router/auto` only resolved when the user
|
|
20
|
+
* was cd'd inside a project that had `@robot-resources/router` in its
|
|
21
|
+
* node_modules — and broke EVERY Node command from any other cwd with
|
|
22
|
+
* `Cannot find module`. Result: every wizard-success Node user pre-Phase-8
|
|
23
|
+
* had a NODE_OPTIONS line that crashed `node`/`npm`/etc. Symptom in
|
|
24
|
+
* Supabase: `node_shim_installed: 8` but `adapter_attached: 0`.
|
|
25
|
+
*
|
|
26
|
+
* Behavior decisions (preserved from Phase 3):
|
|
18
27
|
* - If NODE_OPTIONS is already set with a different --require (rare; e.g.
|
|
19
28
|
* dd-trace), append ours after theirs. Both load. The user keeps their
|
|
20
29
|
* existing tooling. The shell expansion `${NODE_OPTIONS:-} ...` handles
|
|
@@ -27,12 +36,14 @@ import { join } from 'node:path';
|
|
|
27
36
|
const MARK_BEGIN = '# >>> robot-resources: NODE_OPTIONS auto-attach >>>';
|
|
28
37
|
const MARK_END = '# <<< robot-resources <<<';
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
function buildPosixLine(autoPath) {
|
|
40
|
+
return `export NODE_OPTIONS="\${NODE_OPTIONS:-} --require ${autoPath}"`;
|
|
41
|
+
}
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
function buildFishLine(autoPath) {
|
|
44
|
+
// Fish has different syntax (no `export`, uses `set -x`).
|
|
45
|
+
return `set -x NODE_OPTIONS "$NODE_OPTIONS --require ${autoPath}"`;
|
|
46
|
+
}
|
|
36
47
|
|
|
37
48
|
/**
|
|
38
49
|
* Discover which rc files are present for this user. Returns a list of
|
|
@@ -73,7 +84,11 @@ export function hasShellLine(home = homedir()) {
|
|
|
73
84
|
* others on one failure. Per-file errors are returned as warnings the
|
|
74
85
|
* caller can surface.
|
|
75
86
|
*/
|
|
76
|
-
export function writeShellLine(home = homedir()) {
|
|
87
|
+
export function writeShellLine({ autoPath, home = homedir() }) {
|
|
88
|
+
if (!autoPath) {
|
|
89
|
+
throw new Error('writeShellLine requires { autoPath } — absolute path to auto.cjs');
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
const rcs = listShellRcFiles(home);
|
|
78
93
|
const written = [];
|
|
79
94
|
const errors = [];
|
|
@@ -97,7 +112,7 @@ export function writeShellLine(home = homedir()) {
|
|
|
97
112
|
continue;
|
|
98
113
|
}
|
|
99
114
|
|
|
100
|
-
const line = rc.kind === 'fish' ?
|
|
115
|
+
const line = rc.kind === 'fish' ? buildFishLine(autoPath) : buildPosixLine(autoPath);
|
|
101
116
|
const block =
|
|
102
117
|
(text && !text.endsWith('\n') ? '\n' : '') +
|
|
103
118
|
'\n' + MARK_BEGIN + '\n' + line + '\n' + MARK_END + '\n';
|
|
@@ -165,4 +180,4 @@ function getMode(path) {
|
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
// Exported for tests + telemetry payloads.
|
|
168
|
-
export { MARK_BEGIN, MARK_END,
|
|
183
|
+
export { MARK_BEGIN, MARK_END, buildPosixLine, buildFishLine };
|
package/lib/uninstall.js
CHANGED
|
@@ -5,6 +5,7 @@ import { stripJson5 } from './json5.js';
|
|
|
5
5
|
import { removeShellLine } from './shell-config.js';
|
|
6
6
|
import { detectVenv } from './venv-detect.js';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { removePersistedNodeOptions } from './windows-env.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Single source of truth for `npx robot-resources --uninstall`.
|
|
@@ -86,18 +87,45 @@ export function runUninstall({ purge = false } = {}) {
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
// 3. Shell config — remove the NODE_OPTIONS
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
// 3. Shell config / Windows registry — remove the NODE_OPTIONS line
|
|
91
|
+
// Phase 3's wizard wrote to. Idempotent: no-op if not present.
|
|
92
|
+
if (process.platform === 'win32') {
|
|
93
|
+
// Phase 9: restore from backup or clear HKCU\Environment\NODE_OPTIONS.
|
|
94
|
+
try {
|
|
95
|
+
const result = removePersistedNodeOptions();
|
|
96
|
+
if (result.ok && result.action !== 'noop') {
|
|
97
|
+
components_removed.push(`win_node_options_${result.action}`);
|
|
98
|
+
} else if (!result.ok) {
|
|
99
|
+
errors.push({ component: 'win_node_options', message: 'reg_restore_failed' });
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
errors.push({ component: 'win_node_options', message: err.message });
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
try {
|
|
106
|
+
const result = removeShellLine();
|
|
107
|
+
if (result.removed.length > 0) {
|
|
108
|
+
components_removed.push('shell_config_node_options');
|
|
109
|
+
}
|
|
110
|
+
for (const e of result.errors) {
|
|
111
|
+
errors.push({ component: 'shell_config_node_options', message: `${e.path}: ${e.message}` });
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
errors.push({ component: 'shell_config_node_options', message: err.message });
|
|
95
115
|
}
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 3b. Copied router dir at ~/.robot-resources/router/ (Phase 8). The shell
|
|
119
|
+
// line points at this absolute path — once the line is gone, the
|
|
120
|
+
// copied files are dead weight. Remove them.
|
|
121
|
+
const routerDir = join(homedir(), '.robot-resources', 'router');
|
|
122
|
+
if (existsSync(routerDir)) {
|
|
123
|
+
try {
|
|
124
|
+
rmSync(routerDir, { recursive: true, force: true });
|
|
125
|
+
components_removed.push('node_shim_router_dir');
|
|
126
|
+
} catch (err) {
|
|
127
|
+
errors.push({ component: 'node_shim_router_dir', message: err.message });
|
|
98
128
|
}
|
|
99
|
-
} catch (err) {
|
|
100
|
-
errors.push({ component: 'shell_config_node_options', message: err.message });
|
|
101
129
|
}
|
|
102
130
|
|
|
103
131
|
// 4. Python shim — `pip uninstall -y robot-resources` against the resolved
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Windows NODE_OPTIONS persistence (Phase 9).
|
|
8
|
+
*
|
|
9
|
+
* The POSIX path uses a marker block in `~/.bashrc` / `~/.zshrc`. Windows
|
|
10
|
+
* has no equivalent shell rc that all Node-launched processes read — cmd,
|
|
11
|
+
* PowerShell, and Win+R-launched .exe all draw env vars from the user
|
|
12
|
+
* registry under HKCU\Environment.
|
|
13
|
+
*
|
|
14
|
+
* `setx NODE_OPTIONS "value"` writes there. New processes pick it up.
|
|
15
|
+
* The current shell does NOT see the change — user has to open a new
|
|
16
|
+
* terminal. Same UX caveat as POSIX.
|
|
17
|
+
*
|
|
18
|
+
* Why not edit PowerShell `$PROFILE`: ExecutionPolicy on locked-down
|
|
19
|
+
* corporate fleets often blocks unsigned `.ps1`. cmd-launched Node
|
|
20
|
+
* processes also miss it. setx is universal across both.
|
|
21
|
+
*
|
|
22
|
+
* Idempotency: we read the persisted NODE_OPTIONS via `reg query` (not
|
|
23
|
+
* `process.env.NODE_OPTIONS`, which is the current-shell value, not the
|
|
24
|
+
* persistent one). If our `--require <auto>` is already present, no-op.
|
|
25
|
+
*
|
|
26
|
+
* Uninstall: backup the user's PRE-modification value to
|
|
27
|
+
* `~/.robot-resources/windows-prior-node-options.txt`. On `--uninstall`,
|
|
28
|
+
* restore from backup; if the backup is missing or empty, clear the var.
|
|
29
|
+
*
|
|
30
|
+
* Truncation note: `setx` truncates values at 1024 chars. If a user
|
|
31
|
+
* already has a long NODE_OPTIONS plus other dev tooling, we may be
|
|
32
|
+
* close to the limit. We surface the merged length in telemetry so we
|
|
33
|
+
* can spot truncation in Supabase.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const REG_PATH = 'HKCU\\Environment';
|
|
37
|
+
const VAR_NAME = 'NODE_OPTIONS';
|
|
38
|
+
const SETX_LIMIT = 1024;
|
|
39
|
+
|
|
40
|
+
function backupFilePath(home = homedir()) {
|
|
41
|
+
return join(home, '.robot-resources', 'windows-prior-node-options.txt');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read the persisted NODE_OPTIONS value from HKCU\Environment.
|
|
46
|
+
* Returns the string (possibly empty) or null if reading failed.
|
|
47
|
+
*/
|
|
48
|
+
export function readPersistedNodeOptions() {
|
|
49
|
+
const res = spawnSync(
|
|
50
|
+
'reg.exe',
|
|
51
|
+
['query', REG_PATH, '/v', VAR_NAME],
|
|
52
|
+
{ stdio: 'pipe', encoding: 'utf-8' },
|
|
53
|
+
);
|
|
54
|
+
if (res.status !== 0) {
|
|
55
|
+
// `reg query` returns non-zero when the value doesn't exist. That's a
|
|
56
|
+
// valid state — empty NODE_OPTIONS — not an error.
|
|
57
|
+
const stderr = (res.stderr || '').toLowerCase();
|
|
58
|
+
if (stderr.includes('unable to find') || stderr.includes('not exist')) {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
// Output looks like:
|
|
64
|
+
// HKEY_CURRENT_USER\Environment
|
|
65
|
+
// NODE_OPTIONS REG_SZ --require ...
|
|
66
|
+
// We extract the value after REG_SZ. The value can contain spaces; take
|
|
67
|
+
// everything after the last "REG_SZ" occurrence on its line.
|
|
68
|
+
const lines = (res.stdout || '').split(/\r?\n/);
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const m = line.match(/^\s*NODE_OPTIONS\s+REG_(?:SZ|EXPAND_SZ)\s+(.*)$/);
|
|
71
|
+
if (m) return m[1].trim();
|
|
72
|
+
}
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Set NODE_OPTIONS in HKCU\Environment to the given value (idempotent
|
|
78
|
+
* append of `--require <autoPath>` to whatever was there).
|
|
79
|
+
*
|
|
80
|
+
* Returns:
|
|
81
|
+
* { ok: true, already: boolean, prior: string, written: string, length: number }
|
|
82
|
+
* { ok: false, reason, error_message? }
|
|
83
|
+
*/
|
|
84
|
+
export function writePersistedNodeOptions({ autoPath, home = homedir() }) {
|
|
85
|
+
if (!autoPath) {
|
|
86
|
+
return { ok: false, reason: 'missing_auto_path' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const prior = readPersistedNodeOptions();
|
|
90
|
+
if (prior === null) {
|
|
91
|
+
return { ok: false, reason: 'reg_query_failed' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Quote the path so a path with spaces survives Node's --require parser.
|
|
95
|
+
// Node accepts both quoted and unquoted forms; quoting is safer.
|
|
96
|
+
const ourArg = `--require "${autoPath}"`;
|
|
97
|
+
|
|
98
|
+
if (prior.includes(ourArg) || prior.includes(`--require ${autoPath}`)) {
|
|
99
|
+
return {
|
|
100
|
+
ok: true,
|
|
101
|
+
already: true,
|
|
102
|
+
prior,
|
|
103
|
+
written: prior,
|
|
104
|
+
length: prior.length,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const merged = prior ? `${prior} ${ourArg}` : ourArg;
|
|
109
|
+
|
|
110
|
+
if (merged.length > SETX_LIMIT) {
|
|
111
|
+
// setx truncates silently at 1024 chars. Refuse rather than write a
|
|
112
|
+
// broken value. User must shorten their existing NODE_OPTIONS first.
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
reason: 'setx_limit_exceeded',
|
|
116
|
+
error_message: `merged value is ${merged.length} chars; setx truncates at ${SETX_LIMIT}`,
|
|
117
|
+
length: merged.length,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Backup the prior value BEFORE writing, so --uninstall can restore.
|
|
122
|
+
// Even if backup write fails (disk full, permissions), we still proceed —
|
|
123
|
+
// the registry-level setx is the source of truth, the backup is just a
|
|
124
|
+
// convenience for restore.
|
|
125
|
+
try {
|
|
126
|
+
const backup = backupFilePath(home);
|
|
127
|
+
mkdirSync(join(home, '.robot-resources'), { recursive: true });
|
|
128
|
+
writeFileSync(backup, prior, { encoding: 'utf-8' });
|
|
129
|
+
} catch {
|
|
130
|
+
// Best-effort backup; continue.
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const setxRes = spawnSync(
|
|
134
|
+
'setx.exe',
|
|
135
|
+
[VAR_NAME, merged],
|
|
136
|
+
{ stdio: 'pipe', encoding: 'utf-8' },
|
|
137
|
+
);
|
|
138
|
+
if (setxRes.status !== 0) {
|
|
139
|
+
const stderr = (setxRes.stderr || '').toString().trim();
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
reason: 'setx_failed',
|
|
143
|
+
error_message: stderr.slice(0, 200) || `exit ${setxRes.status}`,
|
|
144
|
+
length: merged.length,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
already: false,
|
|
151
|
+
prior,
|
|
152
|
+
written: merged,
|
|
153
|
+
length: merged.length,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Reverse `writePersistedNodeOptions`. Reads the backup file (if present)
|
|
159
|
+
* and restores that value via setx. If no backup is present or the backup
|
|
160
|
+
* is empty, clears the registry value entirely (`reg delete`).
|
|
161
|
+
*
|
|
162
|
+
* Returns { ok, restored_to: string, action: 'restored'|'cleared'|'noop' }
|
|
163
|
+
*/
|
|
164
|
+
export function removePersistedNodeOptions({ home = homedir() } = {}) {
|
|
165
|
+
const current = readPersistedNodeOptions();
|
|
166
|
+
if (current === null) return { ok: false, action: 'noop' };
|
|
167
|
+
if (current === '') return { ok: true, restored_to: '', action: 'noop' };
|
|
168
|
+
|
|
169
|
+
const backupPath = backupFilePath(home);
|
|
170
|
+
let priorValue = '';
|
|
171
|
+
if (existsSync(backupPath)) {
|
|
172
|
+
try { priorValue = readFileSync(backupPath, 'utf-8'); } catch { /* */ }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (priorValue === '') {
|
|
176
|
+
// No prior value to restore — clear the var entirely.
|
|
177
|
+
const res = spawnSync(
|
|
178
|
+
'reg.exe',
|
|
179
|
+
['delete', REG_PATH, '/v', VAR_NAME, '/f'],
|
|
180
|
+
{ stdio: 'pipe', encoding: 'utf-8' },
|
|
181
|
+
);
|
|
182
|
+
if (existsSync(backupPath)) try { unlinkSync(backupPath); } catch { /* */ }
|
|
183
|
+
return {
|
|
184
|
+
ok: res.status === 0,
|
|
185
|
+
restored_to: '',
|
|
186
|
+
action: 'cleared',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Restore the prior value.
|
|
191
|
+
const res = spawnSync(
|
|
192
|
+
'setx.exe',
|
|
193
|
+
[VAR_NAME, priorValue],
|
|
194
|
+
{ stdio: 'pipe', encoding: 'utf-8' },
|
|
195
|
+
);
|
|
196
|
+
if (existsSync(backupPath)) try { unlinkSync(backupPath); } catch { /* */ }
|
|
197
|
+
return {
|
|
198
|
+
ok: res.status === 0,
|
|
199
|
+
restored_to: priorValue,
|
|
200
|
+
action: 'restored',
|
|
201
|
+
};
|
|
202
|
+
}
|