robot-resources 1.9.1 → 1.9.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/python-bridge.js +17 -7
- package/lib/wizard.js +70 -20
- package/package.json +1 -1
package/lib/python-bridge.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
findPython,
|
|
3
|
+
findOrInstallPython,
|
|
3
4
|
ensureVenv,
|
|
4
5
|
installRouter,
|
|
5
6
|
isRouterInstalled,
|
|
@@ -7,22 +8,31 @@ import {
|
|
|
7
8
|
} from '@robot-resources/cli-core/python-bridge.mjs';
|
|
8
9
|
|
|
9
10
|
// Re-export shared primitives used by wizard.js and other CLI code.
|
|
10
|
-
export { ensureVenv, isRouterInstalled, getVenvPythonPath };
|
|
11
|
+
export { findPython, ensureVenv, isRouterInstalled, getVenvPythonPath };
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* Full setup: find Python, create venv, install router.
|
|
14
|
-
*
|
|
14
|
+
* Full setup: find Python (or bootstrap one via uv), create venv, install router.
|
|
15
|
+
*
|
|
16
|
+
* The uv-fallback path is what unblocks users without system Python. It
|
|
17
|
+
* downloads the uv binary to ~/.robot-resources/bin/ and uses it to
|
|
18
|
+
* install a standalone Python — nothing touches the user's system.
|
|
19
|
+
*
|
|
20
|
+
* Returns { venvPython, pythonVersion, pythonSource } or throws.
|
|
15
21
|
*/
|
|
16
22
|
export async function setupRouter() {
|
|
17
|
-
const python =
|
|
23
|
+
const python = await findOrInstallPython();
|
|
18
24
|
if (!python) {
|
|
19
25
|
throw new Error(
|
|
20
|
-
'Python 3.10+ not found
|
|
21
|
-
'
|
|
26
|
+
'Python 3.10+ not found and uv bootstrap failed. ' +
|
|
27
|
+
'Install Python from https://python.org and try again.'
|
|
22
28
|
);
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
const venvPython = ensureVenv(python.bin);
|
|
26
32
|
await installRouter();
|
|
27
|
-
return {
|
|
33
|
+
return {
|
|
34
|
+
venvPython,
|
|
35
|
+
pythonVersion: python.version,
|
|
36
|
+
pythonSource: python.source, // 'system' | 'uv'
|
|
37
|
+
};
|
|
28
38
|
}
|
package/lib/wizard.js
CHANGED
|
@@ -9,6 +9,40 @@ import { installService, isServiceRunning, isServiceInstalled } from './service.
|
|
|
9
9
|
import { configureToolRouting, registerScraperMcp, restartOpenClawGateway } from './tool-config.js';
|
|
10
10
|
import { checkHealth } from './health-report.js';
|
|
11
11
|
import { header, step, success, warn, error, info, blank, summary } from './ui.js';
|
|
12
|
+
/**
|
|
13
|
+
* Classify an install error into a short reason code + bounded detail string.
|
|
14
|
+
*
|
|
15
|
+
* Before this existed, install_complete telemetry reported router:false with
|
|
16
|
+
* no context — 100% of rr-router installs failed and we couldn't diagnose.
|
|
17
|
+
* The reason code slots into a small enum so we can aggregate in the admin
|
|
18
|
+
* dashboard; detail is the tail of stderr/error message for deep-dives.
|
|
19
|
+
*/
|
|
20
|
+
function classifyRouterError(err) {
|
|
21
|
+
const msg = (err?.message || String(err)).toLowerCase();
|
|
22
|
+
let reason = 'unknown';
|
|
23
|
+
|
|
24
|
+
if (msg.includes('python 3.10+') || msg.includes('python is required')) {
|
|
25
|
+
reason = 'python_not_found';
|
|
26
|
+
} else if (err?.code === 'ENOENT' || msg.includes('enoent')) {
|
|
27
|
+
reason = 'spawn_enoent';
|
|
28
|
+
} else if (msg.includes('timeout') || msg.includes('timed out') || err?.code === 'ETIMEDOUT') {
|
|
29
|
+
reason = 'timeout';
|
|
30
|
+
} else if (msg.includes('exited with code') || msg.includes('pip install')) {
|
|
31
|
+
reason = 'pip_install_failed';
|
|
32
|
+
} else if (msg.includes('permission denied') || err?.code === 'EACCES') {
|
|
33
|
+
reason = 'permission_denied';
|
|
34
|
+
} else if (msg.includes('disk') || msg.includes('space') || err?.code === 'ENOSPC') {
|
|
35
|
+
reason = 'disk_full';
|
|
36
|
+
} else if (msg.includes('network') || msg.includes('getaddrinfo') || msg.includes('enetunreach')) {
|
|
37
|
+
reason = 'network';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rawDetail = err?.stderr?.trim?.() || err?.message || String(err);
|
|
41
|
+
const detail = rawDetail.slice(-500);
|
|
42
|
+
|
|
43
|
+
return { reason, detail, exitCode: err?.exitCode ?? null };
|
|
44
|
+
}
|
|
45
|
+
|
|
12
46
|
/**
|
|
13
47
|
* Main setup wizard. Handles the full onboarding flow:
|
|
14
48
|
* 1. Router installation (Python venv + pip)
|
|
@@ -124,22 +158,26 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
124
158
|
results.router = true;
|
|
125
159
|
} else {
|
|
126
160
|
const python = findPython();
|
|
127
|
-
if (
|
|
128
|
-
warn('Python 3.10+ not found — skipping Router installation');
|
|
129
|
-
info('Install Python from https://python.org and re-run this wizard');
|
|
130
|
-
info('Scraper works without Python');
|
|
131
|
-
} else {
|
|
161
|
+
if (python) {
|
|
132
162
|
info(`Found Python ${python.version} (${python.bin})`);
|
|
133
|
-
|
|
163
|
+
} else {
|
|
164
|
+
// No system Python — setupRouter() will bootstrap uv + install a
|
|
165
|
+
// standalone Python into ~/.robot-resources/. This used to be a hard
|
|
166
|
+
// fail; now it's the dominant auto-heal for the python_not_found
|
|
167
|
+
// cohort (2/3 of failures in recent telemetry).
|
|
168
|
+
warn('No system Python detected — bootstrapping one via uv.');
|
|
169
|
+
info('This downloads the uv binary (~15MB) and a managed Python to ~/.robot-resources/');
|
|
170
|
+
}
|
|
171
|
+
step('Installing Router (this may take a moment)...');
|
|
134
172
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
173
|
+
try {
|
|
174
|
+
const { pythonSource } = await setupRouter();
|
|
175
|
+
success(`Router installed${pythonSource === 'uv' ? ' (uv-managed Python)' : ''}`);
|
|
176
|
+
results.router = true;
|
|
177
|
+
results.pythonSource = pythonSource;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
error(`Router installation failed: ${err.message}`);
|
|
180
|
+
results.routerError = classifyRouterError(err);
|
|
143
181
|
}
|
|
144
182
|
}
|
|
145
183
|
|
|
@@ -298,15 +336,27 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
298
336
|
try {
|
|
299
337
|
const config = readConfig();
|
|
300
338
|
const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
339
|
+
const installPayload = {
|
|
340
|
+
router: results.router || false,
|
|
341
|
+
service: results.service || false,
|
|
342
|
+
scraper: results.scraper || false,
|
|
343
|
+
source: 'wizard',
|
|
344
|
+
};
|
|
345
|
+
if (results.pythonSource) {
|
|
346
|
+
// 'system' when the user had Python installed, 'uv' when we
|
|
347
|
+
// auto-bootstrapped one. Lets us measure how many installs were
|
|
348
|
+
// rescued by the uv fallback.
|
|
349
|
+
installPayload.pythonSource = results.pythonSource;
|
|
350
|
+
}
|
|
351
|
+
if (results.routerError && typeof results.routerError === 'object') {
|
|
352
|
+
installPayload.routerError = results.routerError.reason;
|
|
353
|
+
installPayload.routerErrorDetail = results.routerError.detail;
|
|
354
|
+
installPayload.platform = process.platform;
|
|
355
|
+
}
|
|
301
356
|
const body = JSON.stringify({
|
|
302
357
|
product: 'cli',
|
|
303
358
|
event_type: 'install_complete',
|
|
304
|
-
payload:
|
|
305
|
-
router: results.router || false,
|
|
306
|
-
service: results.service || false,
|
|
307
|
-
scraper: results.scraper || false,
|
|
308
|
-
source: 'wizard',
|
|
309
|
-
},
|
|
359
|
+
payload: installPayload,
|
|
310
360
|
});
|
|
311
361
|
|
|
312
362
|
for (let attempt = 0; attempt < 2; attempt++) {
|