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/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
- }
@@ -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
- }
@@ -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
- }