robot-resources 1.15.2 → 1.15.3
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/package.json +7 -49
- package/README.md +0 -104
- package/bin/setup.js +0 -43
- package/lib/auth.mjs +0 -261
- package/lib/config.mjs +0 -55
- package/lib/detect.js +0 -254
- package/lib/health-report.js +0 -130
- package/lib/install-node-shim.js +0 -188
- package/lib/install-python-shim.js +0 -107
- package/lib/install-router-files.js +0 -48
- package/lib/json5.js +0 -16
- package/lib/login.mjs +0 -54
- package/lib/machine-id.js +0 -31
- package/lib/non-oc-wizard.js +0 -615
- package/lib/shell-config.js +0 -183
- package/lib/source-edit-attach.js +0 -469
- package/lib/tool-config.js +0 -504
- package/lib/ui.js +0 -87
- package/lib/uninstall.js +0 -208
- package/lib/venv-detect.js +0 -85
- package/lib/windows-env.js +0 -202
- package/lib/wizard.js +0 -523
package/lib/uninstall.js
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { stripJson5 } from './json5.js';
|
|
5
|
-
import { removeShellLine } from './shell-config.js';
|
|
6
|
-
import { detectVenv } from './venv-detect.js';
|
|
7
|
-
import { spawnSync } from 'node:child_process';
|
|
8
|
-
import { removePersistedNodeOptions } from './windows-env.js';
|
|
9
|
-
import { findAgentSourceFile, hasSourceMarker, removeSourceMarker } from './source-edit-attach.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Single source of truth for `npx robot-resources --uninstall`.
|
|
13
|
-
*
|
|
14
|
-
* Reverses every install path the wizard might have taken:
|
|
15
|
-
* 1. OC plugin directories under ~/.openclaw/extensions/ (Phase 0)
|
|
16
|
-
* 2. Our entries in openclaw.json (plugins.entries + plugins.allow +
|
|
17
|
-
* mcp.servers) (Phase 0)
|
|
18
|
-
* 3. NODE_OPTIONS marker block in shell rc files (Phase 3 — Node shim)
|
|
19
|
-
* 4. `robot-resources` PyPI package in the resolved venv (Phase 3 —
|
|
20
|
-
* Python shim)
|
|
21
|
-
* 5. With --purge: ~/.robot-resources/ config dir (api_key + claim_url)
|
|
22
|
-
*
|
|
23
|
-
* `~/.robot-resources/config.json` is preserved by default so a subsequent
|
|
24
|
-
* re-install reuses the same api_key (and the user's claim_url stays valid).
|
|
25
|
-
*
|
|
26
|
-
* Returns { components_removed: string[], errors: { component, message }[] }
|
|
27
|
-
* for telemetry. Failure to remove one component never aborts the others —
|
|
28
|
-
* a partial uninstall is still progress, and we want to record what worked.
|
|
29
|
-
*/
|
|
30
|
-
export function runUninstall({ purge = false } = {}) {
|
|
31
|
-
const components_removed = [];
|
|
32
|
-
const errors = [];
|
|
33
|
-
|
|
34
|
-
// 1. Plugin directories under ~/.openclaw/extensions/
|
|
35
|
-
const pluginDirs = [
|
|
36
|
-
{ id: 'robot-resources-router', label: 'router_plugin_dir' },
|
|
37
|
-
{ id: 'robot-resources-scraper-oc-plugin', label: 'scraper_plugin_dir' },
|
|
38
|
-
];
|
|
39
|
-
for (const { id, label } of pluginDirs) {
|
|
40
|
-
const path = join(homedir(), '.openclaw', 'extensions', id);
|
|
41
|
-
if (!existsSync(path)) continue;
|
|
42
|
-
try {
|
|
43
|
-
rmSync(path, { recursive: true, force: true });
|
|
44
|
-
components_removed.push(label);
|
|
45
|
-
} catch (err) {
|
|
46
|
-
errors.push({ component: label, message: err.message });
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 2. openclaw.json — strip our entries from plugins.entries, plugins.allow,
|
|
51
|
-
// and mcp.servers. Leave everything else (other plugins, user config) alone.
|
|
52
|
-
// Idempotent: if openclaw.json is missing or malformed, skip silently —
|
|
53
|
-
// that's the right behavior for "cleanup what you can find."
|
|
54
|
-
const ocConfigPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
55
|
-
if (existsSync(ocConfigPath)) {
|
|
56
|
-
try {
|
|
57
|
-
const config = JSON.parse(stripJson5(readFileSync(ocConfigPath, 'utf-8')));
|
|
58
|
-
let mutated = false;
|
|
59
|
-
|
|
60
|
-
if (config?.plugins?.entries) {
|
|
61
|
-
for (const id of ['robot-resources-router', 'robot-resources-scraper-oc-plugin']) {
|
|
62
|
-
if (config.plugins.entries[id]) {
|
|
63
|
-
delete config.plugins.entries[id];
|
|
64
|
-
mutated = true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (Array.isArray(config?.plugins?.allow)) {
|
|
70
|
-
const before = config.plugins.allow.length;
|
|
71
|
-
config.plugins.allow = config.plugins.allow.filter(
|
|
72
|
-
(id) => id !== 'robot-resources-router' && id !== 'robot-resources-scraper-oc-plugin',
|
|
73
|
-
);
|
|
74
|
-
if (config.plugins.allow.length !== before) mutated = true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (config?.mcp?.servers?.['robot-resources-scraper']) {
|
|
78
|
-
delete config.mcp.servers['robot-resources-scraper'];
|
|
79
|
-
mutated = true;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (mutated) {
|
|
83
|
-
writeFileSync(ocConfigPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
84
|
-
components_removed.push('openclaw_config_entries');
|
|
85
|
-
}
|
|
86
|
-
} catch (err) {
|
|
87
|
-
errors.push({ component: 'openclaw_config_entries', message: err.message });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 3. Shell config / Windows registry — remove the NODE_OPTIONS line
|
|
92
|
-
// Phase 3's wizard wrote to. Idempotent: no-op if not present.
|
|
93
|
-
if (process.platform === 'win32') {
|
|
94
|
-
// Phase 9: restore from backup or clear HKCU\Environment\NODE_OPTIONS.
|
|
95
|
-
try {
|
|
96
|
-
const result = removePersistedNodeOptions();
|
|
97
|
-
if (result.ok && result.action !== 'noop') {
|
|
98
|
-
components_removed.push(`win_node_options_${result.action}`);
|
|
99
|
-
} else if (!result.ok) {
|
|
100
|
-
errors.push({ component: 'win_node_options', message: 'reg_restore_failed' });
|
|
101
|
-
}
|
|
102
|
-
} catch (err) {
|
|
103
|
-
errors.push({ component: 'win_node_options', message: err.message });
|
|
104
|
-
}
|
|
105
|
-
} else {
|
|
106
|
-
try {
|
|
107
|
-
const result = removeShellLine();
|
|
108
|
-
if (result.removed.length > 0) {
|
|
109
|
-
components_removed.push('shell_config_node_options');
|
|
110
|
-
}
|
|
111
|
-
for (const e of result.errors) {
|
|
112
|
-
errors.push({ component: 'shell_config_node_options', message: `${e.path}: ${e.message}` });
|
|
113
|
-
}
|
|
114
|
-
} catch (err) {
|
|
115
|
-
errors.push({ component: 'shell_config_node_options', message: err.message });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// 3a. Phase 11 — source-edit auto-attach line. If the wizard injected
|
|
120
|
-
// `require('@robot-resources/router/auto')` at the top of the user's
|
|
121
|
-
// entry file, peel the marker block out (or restore from .rr-backup
|
|
122
|
-
// when --purge). Idempotent: no-op when no entry detected or marker
|
|
123
|
-
// not present. Runs against cwd, so users who --uninstall from a
|
|
124
|
-
// different repo won't accidentally wipe the wrong file.
|
|
125
|
-
try {
|
|
126
|
-
// Phase 11.1: detector renamed to findAgentSourceFile, returns
|
|
127
|
-
// { winner, candidates, ... }. Adapt to the same `path` field name
|
|
128
|
-
// so the rest of the block reads naturally.
|
|
129
|
-
const scan = findAgentSourceFile();
|
|
130
|
-
const detection = { path: scan.winner };
|
|
131
|
-
if (detection.path && hasSourceMarker(detection.path)) {
|
|
132
|
-
const r = removeSourceMarker(detection.path, { restoreFromBackup: !!purge });
|
|
133
|
-
if (r.ok && (r.removed || r.restored)) {
|
|
134
|
-
components_removed.push(r.restored ? 'node_entry_restored_from_backup' : 'node_entry_marker_removed');
|
|
135
|
-
} else if (!r.ok) {
|
|
136
|
-
errors.push({ component: 'node_entry_source_edit', message: r.error || 'unknown' });
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
// --purge also wipes any leftover .rr-backup (covers the case where the
|
|
140
|
-
// user manually deleted the marker but kept the backup file around).
|
|
141
|
-
if (purge && detection.path && existsSync(`${detection.path}.rr-backup`)) {
|
|
142
|
-
try {
|
|
143
|
-
rmSync(`${detection.path}.rr-backup`, { force: true });
|
|
144
|
-
components_removed.push('node_entry_backup_purged');
|
|
145
|
-
} catch (err) {
|
|
146
|
-
errors.push({ component: 'node_entry_backup_purged', message: err.message });
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
} catch (err) {
|
|
150
|
-
errors.push({ component: 'node_entry_source_edit', message: err.message });
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 3b. Copied router dir at ~/.robot-resources/router/ (Phase 8). The shell
|
|
154
|
-
// line points at this absolute path — once the line is gone, the
|
|
155
|
-
// copied files are dead weight. Remove them.
|
|
156
|
-
const routerDir = join(homedir(), '.robot-resources', 'router');
|
|
157
|
-
if (existsSync(routerDir)) {
|
|
158
|
-
try {
|
|
159
|
-
rmSync(routerDir, { recursive: true, force: true });
|
|
160
|
-
components_removed.push('node_shim_router_dir');
|
|
161
|
-
} catch (err) {
|
|
162
|
-
errors.push({ component: 'node_shim_router_dir', message: err.message });
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// 4. Python shim — `pip uninstall -y robot-resources` against the resolved
|
|
167
|
-
// venv. Skip silently if no venv detected (the user may have installed
|
|
168
|
-
// via the wizard but already deleted the venv themselves).
|
|
169
|
-
try {
|
|
170
|
-
const venv = detectVenv();
|
|
171
|
-
if (venv.python) {
|
|
172
|
-
const result = spawnSync(venv.python, ['-m', 'pip', 'uninstall', '-y', 'robot-resources'], {
|
|
173
|
-
encoding: 'utf-8',
|
|
174
|
-
timeout: 60_000,
|
|
175
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
176
|
-
});
|
|
177
|
-
// pip exits 0 if removed, non-zero if package wasn't installed (also acceptable)
|
|
178
|
-
if (result.status === 0) {
|
|
179
|
-
components_removed.push('pip_robot_resources');
|
|
180
|
-
} else if (result.stderr && /not installed|skipping/i.test(result.stderr)) {
|
|
181
|
-
// Already gone — count as success silently.
|
|
182
|
-
} else if (result.status !== null) {
|
|
183
|
-
// Some other failure; record but don't abort
|
|
184
|
-
errors.push({
|
|
185
|
-
component: 'pip_robot_resources',
|
|
186
|
-
message: `pip exit ${result.status}: ${(result.stderr || '').slice(-200)}`,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
} catch (err) {
|
|
191
|
-
errors.push({ component: 'pip_robot_resources', message: err.message });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// 5. Optionally wipe ~/.robot-resources/config.json (and any siblings)
|
|
195
|
-
if (purge) {
|
|
196
|
-
const rrDir = join(homedir(), '.robot-resources');
|
|
197
|
-
if (existsSync(rrDir)) {
|
|
198
|
-
try {
|
|
199
|
-
rmSync(rrDir, { recursive: true, force: true });
|
|
200
|
-
components_removed.push('rr_config_dir');
|
|
201
|
-
} catch (err) {
|
|
202
|
-
errors.push({ component: 'rr_config_dir', message: err.message });
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return { components_removed, errors };
|
|
208
|
-
}
|
package/lib/venv-detect.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Resolve the Python interpreter we'll use for `pip install robot-resources`.
|
|
7
|
-
*
|
|
8
|
-
* Resolution order (decided in the plan; never silently install into system
|
|
9
|
-
* Python — that can break OS Python on Linux):
|
|
10
|
-
*
|
|
11
|
-
* 1. $VIRTUAL_ENV — currently-active venv. Strongest signal.
|
|
12
|
-
* 2. ./.venv/bin/python — common cwd venv (uv, hatch, plain venv default).
|
|
13
|
-
* 3. ./venv/bin/python — alternative cwd venv name.
|
|
14
|
-
* 4. pyproject.toml [tool.uv]/[tool.poetry] hint — best effort.
|
|
15
|
-
* 5. Bail with confidence='low'. Caller prompts user or errors out with
|
|
16
|
-
* a `--python=/path/to/python` instruction in non-interactive mode.
|
|
17
|
-
*
|
|
18
|
-
* Returns:
|
|
19
|
-
* { python: string, kind: 'active'|'cwd-venv'|'pyproject', confidence: 'high' }
|
|
20
|
-
* OR
|
|
21
|
-
* { python: null, kind: 'none', confidence: 'low' }
|
|
22
|
-
*/
|
|
23
|
-
export function detectVenv(cwd = process.cwd()) {
|
|
24
|
-
// 1. Active venv — strongest signal.
|
|
25
|
-
const activeVenv = process.env.VIRTUAL_ENV;
|
|
26
|
-
if (activeVenv) {
|
|
27
|
-
const candidate = join(activeVenv, binSubdir(), 'python');
|
|
28
|
-
const candidate3 = join(activeVenv, binSubdir(), 'python3');
|
|
29
|
-
if (existsSync(candidate)) {
|
|
30
|
-
return { python: candidate, kind: 'active', confidence: 'high' };
|
|
31
|
-
}
|
|
32
|
-
if (existsSync(candidate3)) {
|
|
33
|
-
return { python: candidate3, kind: 'active', confidence: 'high' };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 2. ./.venv (uv default, hatch default, plain venv default)
|
|
38
|
-
for (const dirname of ['.venv', 'venv']) {
|
|
39
|
-
const venvDir = join(cwd, dirname);
|
|
40
|
-
const cands = [
|
|
41
|
-
join(venvDir, binSubdir(), 'python'),
|
|
42
|
-
join(venvDir, binSubdir(), 'python3'),
|
|
43
|
-
];
|
|
44
|
-
for (const c of cands) {
|
|
45
|
-
if (existsSync(c)) {
|
|
46
|
-
return { python: c, kind: 'cwd-venv', confidence: 'high' };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 4. Bail. Never silently install into system Python.
|
|
52
|
-
return { python: null, kind: 'none', confidence: 'low' };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function binSubdir() {
|
|
56
|
-
return process.platform === 'win32' ? 'Scripts' : 'bin';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Run `python -m pip install <package>` against the resolved interpreter.
|
|
61
|
-
* Captures exit code + stderr tail for telemetry. Never throws.
|
|
62
|
-
*
|
|
63
|
-
* Phase 3 ships with `--upgrade` so existing 0.1.0 installs migrate to
|
|
64
|
-
* the auto-attach-capable 0.2.0 transparently.
|
|
65
|
-
*/
|
|
66
|
-
export function runPipInstall({ python, packageSpec, timeoutMs = 120_000 }) {
|
|
67
|
-
if (!python) {
|
|
68
|
-
return { ok: false, code: -1, stderr: 'no python interpreter resolved' };
|
|
69
|
-
}
|
|
70
|
-
const args = ['-m', 'pip', 'install', '--upgrade', packageSpec];
|
|
71
|
-
const result = spawnSync(python, args, {
|
|
72
|
-
encoding: 'utf-8',
|
|
73
|
-
timeout: timeoutMs,
|
|
74
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Trim stderr to a sane size for telemetry — pip's full output is huge.
|
|
78
|
-
const stderr = (result.stderr || '').slice(-500);
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
ok: result.status === 0,
|
|
82
|
-
code: result.status,
|
|
83
|
-
stderr,
|
|
84
|
-
};
|
|
85
|
-
}
|
package/lib/windows-env.js
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
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
|
-
}
|