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.
@@ -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. Phase 3 entry for
9
- * the non-OC Node path.
10
+ * Install the Node shim into the user's shell config (POSIX) or user
11
+ * environment registry (Windows).
10
12
  *
11
- * Steps:
12
- * 1. Append the marker block to detected rc files (zsh / bash / fish)
13
- * via shell-config.writeShellLine. Idempotent: re-running does nothing
14
- * if the block is already present.
15
- * 2. Emit `node_shim_installed` telemetry with the shell list, sdks
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
- * The user has to start a new shell (or `source` the file) for the
19
- * NODE_OPTIONS to take effect we tell them this in the wizard's
20
- * post-install message. For Phase 3 we don't try to mutate the running
21
- * shell; that's a Phase 6 nice-to-have.
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
- * Windows: shell-config.writeShellLine returns no rc files on Windows
24
- * (we only support POSIX in P3). The wizard prints manual instructions
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
- if (process.platform === 'win32') {
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: 'unsupported',
51
+ shell: process.platform === 'win32' ? 'win32' : 'unknown',
33
52
  shell_config_path: null,
34
- sdks_detected: detectSdks(cwd),
35
- dry_run: dryRun,
36
- reason: 'windows_not_supported_yet',
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
- reason: 'windows_not_supported_yet',
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
- const sdks = detectSdks(cwd);
49
-
50
- if (dryRun) {
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: 'dryrun',
53
- shell_config_path: null,
70
+ shell: 'win32',
71
+ shell_config_path: 'HKCU\\Environment\\NODE_OPTIONS',
54
72
  sdks_detected: sdks,
55
- dry_run: true,
56
- reason: null,
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
- return { ok: true, message: 'Dry-run: would have written NODE_OPTIONS to shell rc.' };
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
+ }
@@ -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
- info('Open a new terminal — or run: source ~/.zshrc (or your shell rc)');
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
- info('Manual install (paste into ~/.zshrc or ~/.bashrc):');
115
- info(' export NODE_OPTIONS="${NODE_OPTIONS:-} --require @robot-resources/router/auto"');
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');
@@ -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 @robot-resources/router/auto"
14
+ * export NODE_OPTIONS="${NODE_OPTIONS:-} --require /Users/x/.robot-resources/router/auto.cjs"
15
15
  * # <<< robot-resources <<<
16
16
  *
17
- * Behavior decisions (see plan):
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
- const POSIX_LINE =
31
- 'export NODE_OPTIONS="${NODE_OPTIONS:-} --require @robot-resources/router/auto"';
39
+ function buildPosixLine(autoPath) {
40
+ return `export NODE_OPTIONS="\${NODE_OPTIONS:-} --require ${autoPath}"`;
41
+ }
32
42
 
33
- // Fish has different syntax (no `export`, uses `set -x`). Detected separately.
34
- const FISH_LINE =
35
- 'set -x NODE_OPTIONS "$NODE_OPTIONS --require @robot-resources/router/auto"';
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' ? FISH_LINE : POSIX_LINE;
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, POSIX_LINE, FISH_LINE };
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 marker block from any rc
90
- // files Phase 3's wizard wrote to. Idempotent: no-op if not present.
91
- try {
92
- const result = removeShellLine();
93
- if (result.removed.length > 0) {
94
- components_removed.push('shell_config_node_options');
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
- for (const e of result.errors) {
97
- errors.push({ component: 'shell_config_node_options', message: `${e.path}: ${e.message}` });
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robot-resources",
3
- "version": "1.12.4",
3
+ "version": "1.14.0",
4
4
  "description": "Robot Resources — AI agent tools. One command to install everything.",
5
5
  "type": "module",
6
6
  "bin": {