robot-resources 1.9.3 → 1.9.5
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/wizard.js +96 -20
- package/package.json +1 -1
package/lib/wizard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { homedir, hostname } from 'node:os';
|
|
3
|
+
import { homedir, hostname, release as osRelease } from 'node:os';
|
|
4
4
|
import { readConfig, writeConfig } from '@robot-resources/cli-core/config.mjs';
|
|
5
5
|
import { findPython, isPortAvailable, isHeadless, isOpenClawInstalled } from './detect.js';
|
|
6
6
|
import { getOrCreateMachineId } from './machine-id.js';
|
|
@@ -19,12 +19,25 @@ import { header, step, success, warn, error, info, blank, summary } from './ui.j
|
|
|
19
19
|
*/
|
|
20
20
|
function classifyRouterError(err) {
|
|
21
21
|
const msg = (err?.message || String(err)).toLowerCase();
|
|
22
|
+
const stderr = (err?.stderr || '').toString().toLowerCase();
|
|
23
|
+
const combined = msg + '\n' + stderr;
|
|
22
24
|
let reason = 'unknown';
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
// Order matters — check specific patterns before generic ones.
|
|
27
|
+
// python_venv_missing is specific (Debian/Ubuntu ships python3 without
|
|
28
|
+
// the venv module) — previously showed up as 'unknown' in telemetry.
|
|
29
|
+
if (/python venv module|ensurepip.*not (installed|available)|python\d*-venv/.test(combined)) {
|
|
30
|
+
reason = 'python_venv_missing';
|
|
31
|
+
} else if (msg.includes('python 3.10+') || msg.includes('python is required')) {
|
|
25
32
|
reason = 'python_not_found';
|
|
26
33
|
} else if (err?.code === 'ENOENT' || msg.includes('enoent')) {
|
|
27
34
|
reason = 'spawn_enoent';
|
|
35
|
+
} else if (/failed building wheel|metadata-generation-failed|cargo|rust compiler|subprocess-exited-with-error/.test(combined)) {
|
|
36
|
+
// Wheel-build failures: pip tried to compile a native dep from source
|
|
37
|
+
// because no binary wheel was available for the user's platform. This
|
|
38
|
+
// was silently categorized as 'pip_install_failed' before — surfacing
|
|
39
|
+
// it separately lets us see affected packages in aggregate.
|
|
40
|
+
reason = 'wheel_build_failed';
|
|
28
41
|
} else if (msg.includes('timeout') || msg.includes('timed out') || err?.code === 'ETIMEDOUT') {
|
|
29
42
|
reason = 'timeout';
|
|
30
43
|
} else if (msg.includes('exited with code') || msg.includes('pip install')) {
|
|
@@ -56,6 +69,8 @@ function classifyRouterError(err) {
|
|
|
56
69
|
export async function runWizard({ nonInteractive = false } = {}) {
|
|
57
70
|
header();
|
|
58
71
|
|
|
72
|
+
const wizardStartMs = Date.now();
|
|
73
|
+
|
|
59
74
|
const results = {
|
|
60
75
|
auth: false,
|
|
61
76
|
authMethod: null, // 'config' | 'apikey' | 'github'
|
|
@@ -63,6 +78,16 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
63
78
|
routerError: null,
|
|
64
79
|
providerKeys: false,
|
|
65
80
|
service: false,
|
|
81
|
+
// Diagnostic fields populated as the wizard progresses. All are sent
|
|
82
|
+
// in install_complete (success AND failure) so we can distinguish
|
|
83
|
+
// "pip installed but router never served a request" from a real
|
|
84
|
+
// working setup in post-hoc telemetry.
|
|
85
|
+
serviceType: null,
|
|
86
|
+
pluginInstalled: false,
|
|
87
|
+
openclawDetected: false,
|
|
88
|
+
openclawConfigPatched: false,
|
|
89
|
+
scraperMcpRegistered: false,
|
|
90
|
+
healthCheck: { attempted: false },
|
|
66
91
|
};
|
|
67
92
|
|
|
68
93
|
// ── Step 0: Provision API key (before anything else) ────────────────────
|
|
@@ -209,6 +234,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
209
234
|
|
|
210
235
|
try {
|
|
211
236
|
const svc = installService(getVenvPythonPath());
|
|
237
|
+
results.serviceType = svc.type || null;
|
|
212
238
|
if (svc.type === 'skipped') {
|
|
213
239
|
warn(svc.reason);
|
|
214
240
|
results.service = false;
|
|
@@ -234,6 +260,15 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
234
260
|
const toolResults = configureToolRouting();
|
|
235
261
|
results.tools = toolResults;
|
|
236
262
|
|
|
263
|
+
// Surface OC-specific signals for install_complete diagnostics.
|
|
264
|
+
results.openclawDetected = isOpenClawInstalled();
|
|
265
|
+
const ocResult = toolResults.find((r) => r.name === 'OpenClaw');
|
|
266
|
+
if (ocResult) {
|
|
267
|
+
results.pluginInstalled =
|
|
268
|
+
ocResult.action === 'installed' || ocResult.action === 'already_configured';
|
|
269
|
+
results.openclawConfigPatched = Boolean(ocResult.configActivated);
|
|
270
|
+
}
|
|
271
|
+
|
|
237
272
|
if (toolResults.length === 0) {
|
|
238
273
|
info('No supported AI tools detected');
|
|
239
274
|
info('Point your tool at http://localhost:3838 to enable cost optimization');
|
|
@@ -276,6 +311,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
276
311
|
if (scraperRegistered) {
|
|
277
312
|
success('Scraper MCP registered in OpenClaw — scraper_compress_url(url) available');
|
|
278
313
|
results.scraper = true;
|
|
314
|
+
results.scraperMcpRegistered = true;
|
|
279
315
|
} else {
|
|
280
316
|
// Either already registered, or no openclaw.json
|
|
281
317
|
try {
|
|
@@ -283,6 +319,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
283
319
|
if (ocConfig?.mcp?.servers?.['robot-resources-scraper']) {
|
|
284
320
|
success('Scraper MCP already registered in OpenClaw');
|
|
285
321
|
results.scraper = true;
|
|
322
|
+
results.scraperMcpRegistered = true;
|
|
286
323
|
}
|
|
287
324
|
} catch {
|
|
288
325
|
// No openclaw.json — not on OC, skip
|
|
@@ -290,13 +327,25 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
290
327
|
}
|
|
291
328
|
|
|
292
329
|
// ── Step 4.5: Router Healthcheck ──────────────────────────────────────
|
|
330
|
+
//
|
|
331
|
+
// Verify the router is actually serving /health — not just that pip
|
|
332
|
+
// exited 0. Runs regardless of whether service registration succeeded:
|
|
333
|
+
// a router started by the wizard's spawn (or by a running OC) still
|
|
334
|
+
// deserves to be probed, and a router that pip-installed but fails to
|
|
335
|
+
// respond means the install is NOT actually complete.
|
|
336
|
+
//
|
|
337
|
+
// If we declared router=true from Step 1 (pip success) but /health
|
|
338
|
+
// won't answer, downgrade router→false with a dedicated error reason.
|
|
339
|
+
// This closes the "install looks green but nothing works" gap that
|
|
340
|
+
// produced 34 silent-after-install real users with no diagnostics.
|
|
293
341
|
|
|
294
|
-
|
|
295
|
-
if (results.service) {
|
|
342
|
+
if (results.router) {
|
|
296
343
|
blank();
|
|
297
344
|
step('Verifying Router is responding...');
|
|
298
345
|
|
|
299
|
-
|
|
346
|
+
const checkStart = Date.now();
|
|
347
|
+
let healthData = null;
|
|
348
|
+
let lastErr = null;
|
|
300
349
|
// Retry a few times — the service may need a moment to start
|
|
301
350
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
302
351
|
try {
|
|
@@ -306,20 +355,38 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
306
355
|
if (res.ok) {
|
|
307
356
|
const data = await res.json();
|
|
308
357
|
if (data.status === 'healthy' || data.status === 'degraded') {
|
|
309
|
-
|
|
310
|
-
healthy = true;
|
|
358
|
+
healthData = data;
|
|
311
359
|
break;
|
|
312
360
|
}
|
|
313
361
|
}
|
|
314
|
-
} catch {
|
|
315
|
-
|
|
316
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
362
|
+
} catch (err) {
|
|
363
|
+
lastErr = err?.message || String(err);
|
|
317
364
|
}
|
|
365
|
+
if (attempt < 2) await new Promise((r) => setTimeout(r, 2000));
|
|
318
366
|
}
|
|
319
367
|
|
|
320
|
-
|
|
321
|
-
|
|
368
|
+
results.healthCheck = {
|
|
369
|
+
attempted: true,
|
|
370
|
+
passed: Boolean(healthData),
|
|
371
|
+
version: healthData?.version ?? null,
|
|
372
|
+
status: healthData?.status ?? null,
|
|
373
|
+
latencyMs: Date.now() - checkStart,
|
|
374
|
+
error: healthData ? null : lastErr,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
if (healthData) {
|
|
378
|
+
success(`Router healthy (v${healthData.version || 'unknown'})`);
|
|
379
|
+
} else {
|
|
380
|
+
warn('Router not responding — marking install as failed.');
|
|
322
381
|
info('Check manually: curl http://localhost:3838/health');
|
|
382
|
+
// Bug fix: previously we left router=true here. Now we downgrade
|
|
383
|
+
// so install_complete reflects reality and the error is classified.
|
|
384
|
+
results.router = false;
|
|
385
|
+
results.routerError = {
|
|
386
|
+
reason: 'health_check_failed',
|
|
387
|
+
detail: (lastErr || 'no response').slice(-500),
|
|
388
|
+
exitCode: null,
|
|
389
|
+
};
|
|
323
390
|
}
|
|
324
391
|
}
|
|
325
392
|
|
|
@@ -336,22 +403,31 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
336
403
|
try {
|
|
337
404
|
const config = readConfig();
|
|
338
405
|
const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
406
|
+
// Everything populated unconditionally so success installs carry
|
|
407
|
+
// the same diagnostic weight as failures. Prior versions only
|
|
408
|
+
// captured routerError+platform on failure, leaving 34 "successful"
|
|
409
|
+
// installs with no post-hoc signal to explain why they never emit
|
|
410
|
+
// another event.
|
|
339
411
|
const installPayload = {
|
|
412
|
+
source: 'wizard',
|
|
340
413
|
router: results.router || false,
|
|
341
414
|
service: results.service || false,
|
|
342
415
|
scraper: results.scraper || false,
|
|
343
|
-
|
|
416
|
+
platform: process.platform,
|
|
417
|
+
os_release: osRelease(),
|
|
418
|
+
node_version: process.version,
|
|
419
|
+
install_duration_ms: Date.now() - wizardStartMs,
|
|
420
|
+
python_source: results.pythonSource ?? null,
|
|
421
|
+
service_type: results.serviceType ?? null,
|
|
422
|
+
health_check: results.healthCheck,
|
|
423
|
+
plugin_installed: results.pluginInstalled,
|
|
424
|
+
openclaw_detected: results.openclawDetected,
|
|
425
|
+
openclaw_config_patched: results.openclawConfigPatched,
|
|
426
|
+
scraper_mcp_registered: results.scraperMcpRegistered,
|
|
344
427
|
};
|
|
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
428
|
if (results.routerError && typeof results.routerError === 'object') {
|
|
352
429
|
installPayload.routerError = results.routerError.reason;
|
|
353
430
|
installPayload.routerErrorDetail = results.routerError.detail;
|
|
354
|
-
installPayload.platform = process.platform;
|
|
355
431
|
}
|
|
356
432
|
const body = JSON.stringify({
|
|
357
433
|
product: 'cli',
|