robot-resources 1.13.0 → 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 +60 -41
- package/lib/non-oc-wizard.js +12 -3
- package/lib/uninstall.js +25 -10
- package/lib/windows-env.js +202 -0
- package/package.json +1 -1
package/lib/install-node-shim.js
CHANGED
|
@@ -2,54 +2,31 @@ import { writeShellLine, hasShellLine } from './shell-config.js';
|
|
|
2
2
|
import { readConfig } from './config.mjs';
|
|
3
3
|
import { detectNodeAgent } from './detect.js';
|
|
4
4
|
import { installRouterFiles } from './install-router-files.js';
|
|
5
|
+
import { writePersistedNodeOptions } from './windows-env.js';
|
|
5
6
|
|
|
6
7
|
const PLATFORM_URL = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* Install the Node shim into the user's shell config
|
|
10
|
-
*
|
|
10
|
+
* Install the Node shim into the user's shell config (POSIX) or user
|
|
11
|
+
* environment registry (Windows).
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
* 1. Copy
|
|
14
|
-
* path
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* resolved when the user was cd'd inside a project that had the
|
|
18
|
-
* package in its node_modules. From any other cwd, EVERY `node`
|
|
19
|
-
* command crashed with "Cannot find module".
|
|
20
|
-
* 2. Append the marker block to detected rc files (zsh / bash / fish)
|
|
21
|
-
* with the ABSOLUTE PATH to the copied auto.cjs.
|
|
22
|
-
* 3. Emit `node_shim_installed` telemetry.
|
|
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).
|
|
23
18
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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.
|
|
27
23
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* 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.
|
|
31
26
|
*
|
|
32
27
|
* Returns a UI-friendly result the wizard can format and print.
|
|
33
28
|
*/
|
|
34
29
|
export async function installNodeShim({ cwd = process.cwd(), dryRun = false } = {}) {
|
|
35
|
-
if (process.platform === 'win32') {
|
|
36
|
-
await emit({
|
|
37
|
-
shell: 'unsupported',
|
|
38
|
-
shell_config_path: null,
|
|
39
|
-
sdks_detected: detectSdks(cwd),
|
|
40
|
-
dry_run: dryRun,
|
|
41
|
-
reason: 'windows_not_supported_yet',
|
|
42
|
-
});
|
|
43
|
-
return {
|
|
44
|
-
ok: false,
|
|
45
|
-
reason: 'windows_not_supported_yet',
|
|
46
|
-
message:
|
|
47
|
-
'Windows shell-config writing is not yet supported. Set ' +
|
|
48
|
-
'NODE_OPTIONS to point at ~/.robot-resources/router/auto.cjs manually ' +
|
|
49
|
-
'in your system environment variables, or wait for Phase 6.',
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
30
|
const sdks = detectSdks(cwd);
|
|
54
31
|
|
|
55
32
|
if (dryRun) {
|
|
@@ -60,18 +37,18 @@ export async function installNodeShim({ cwd = process.cwd(), dryRun = false } =
|
|
|
60
37
|
dry_run: true,
|
|
61
38
|
reason: null,
|
|
62
39
|
});
|
|
63
|
-
return { ok: true, message: 'Dry-run: would have written NODE_OPTIONS
|
|
40
|
+
return { ok: true, message: 'Dry-run: would have written NODE_OPTIONS.' };
|
|
64
41
|
}
|
|
65
42
|
|
|
66
43
|
// Phase 8: copy router to an absolute path under ~/.robot-resources/router/
|
|
67
|
-
// before we wire the
|
|
68
|
-
// broken NODE_OPTIONS line.
|
|
44
|
+
// before we wire the env config. If the copy fails, we don't write a
|
|
45
|
+
// broken NODE_OPTIONS line on either platform.
|
|
69
46
|
let autoPath;
|
|
70
47
|
try {
|
|
71
48
|
autoPath = installRouterFiles();
|
|
72
49
|
} catch (err) {
|
|
73
50
|
await emit({
|
|
74
|
-
shell: 'unknown',
|
|
51
|
+
shell: process.platform === 'win32' ? 'win32' : 'unknown',
|
|
75
52
|
shell_config_path: null,
|
|
76
53
|
sdks_detected: sdks,
|
|
77
54
|
dry_run: false,
|
|
@@ -84,6 +61,48 @@ export async function installNodeShim({ cwd = process.cwd(), dryRun = false } =
|
|
|
84
61
|
};
|
|
85
62
|
}
|
|
86
63
|
|
|
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 });
|
|
69
|
+
await emit({
|
|
70
|
+
shell: 'win32',
|
|
71
|
+
shell_config_path: 'HKCU\\Environment\\NODE_OPTIONS',
|
|
72
|
+
sdks_detected: sdks,
|
|
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,
|
|
81
|
+
});
|
|
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
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
const alreadyInstalled = hasShellLine();
|
|
88
107
|
const result = writeShellLine({ autoPath });
|
|
89
108
|
|
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/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,32 @@ 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 });
|
|
95
103
|
}
|
|
96
|
-
|
|
97
|
-
|
|
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 });
|
|
98
115
|
}
|
|
99
|
-
} catch (err) {
|
|
100
|
-
errors.push({ component: 'shell_config_node_options', message: err.message });
|
|
101
116
|
}
|
|
102
117
|
|
|
103
118
|
// 3b. Copied router dir at ~/.robot-resources/router/ (Phase 8). The shell
|
|
@@ -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
|
+
}
|