robot-resources 1.9.5 → 1.9.7
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/service.js +58 -8
- package/lib/wizard.js +35 -2
- package/package.json +1 -1
package/lib/service.js
CHANGED
|
@@ -216,6 +216,28 @@ WantedBy=default.target
|
|
|
216
216
|
`;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Check whether linger is enabled for the current user.
|
|
221
|
+
*
|
|
222
|
+
* Without linger, systemd-user services are torn down when the user logs
|
|
223
|
+
* out (SSH disconnect, login manager logout). On the Finland signup
|
|
224
|
+
* (2026-04-23) this was the root cause: 3 heartbeats, then session ended,
|
|
225
|
+
* then the router died with the session.
|
|
226
|
+
*/
|
|
227
|
+
function isLingerEnabled() {
|
|
228
|
+
try {
|
|
229
|
+
const user = process.env.USER || process.env.LOGNAME;
|
|
230
|
+
if (!user) return false;
|
|
231
|
+
const res = spawnSync('loginctl', ['show-user', user, '--property=Linger'], {
|
|
232
|
+
stdio: 'pipe', encoding: 'utf-8',
|
|
233
|
+
});
|
|
234
|
+
if (res.status !== 0) return false;
|
|
235
|
+
return /^Linger=yes\s*$/m.test(res.stdout || '');
|
|
236
|
+
} catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
219
241
|
function installSystemdUser(venvPythonPath) {
|
|
220
242
|
const unitPath = getUserUnitPath();
|
|
221
243
|
const logsDir = join(homedir(), '.robot-resources', 'logs');
|
|
@@ -232,12 +254,17 @@ function installSystemdUser(venvPythonPath) {
|
|
|
232
254
|
execSync('systemctl --user enable robot-resources-router.service', { stdio: 'pipe' });
|
|
233
255
|
execSync('systemctl --user start robot-resources-router.service', { stdio: 'pipe' });
|
|
234
256
|
|
|
235
|
-
// Enable linger so the service survives SSH disconnects (critical for VMs)
|
|
257
|
+
// Enable linger so the service survives SSH disconnects (critical for VMs).
|
|
258
|
+
// On many distros this needs polkit auth and silently no-ops from a
|
|
259
|
+
// non-interactive shell — we attempt it then VERIFY the result.
|
|
260
|
+
let lingerEnabled = false;
|
|
236
261
|
try {
|
|
237
262
|
execSync('loginctl enable-linger', { stdio: 'pipe' });
|
|
238
263
|
} catch {
|
|
239
|
-
//
|
|
264
|
+
// fall through to verification
|
|
240
265
|
}
|
|
266
|
+
lingerEnabled = isLingerEnabled();
|
|
267
|
+
return { lingerEnabled };
|
|
241
268
|
}
|
|
242
269
|
|
|
243
270
|
function uninstallSystemdUser() {
|
|
@@ -603,8 +630,29 @@ export function installService(venvPythonPath) {
|
|
|
603
630
|
}
|
|
604
631
|
|
|
605
632
|
// mode === 'user'
|
|
606
|
-
installSystemdUser(venvPythonPath);
|
|
607
|
-
|
|
633
|
+
const { lingerEnabled } = installSystemdUser(venvPythonPath);
|
|
634
|
+
|
|
635
|
+
// Belt-and-suspenders: ALSO install crontab @reboot so the router comes
|
|
636
|
+
// back on reboot even if linger isn't taking effect (polkit denied,
|
|
637
|
+
// container restrictions, etc.). Idempotent — removes any existing
|
|
638
|
+
// RR crontab entry before adding the fresh one. Safe to call even
|
|
639
|
+
// when crontab is absent (we skip silently).
|
|
640
|
+
let crontabFallback = false;
|
|
641
|
+
if (hasCrontab()) {
|
|
642
|
+
try {
|
|
643
|
+
installCrontab(venvPythonPath);
|
|
644
|
+
crontabFallback = true;
|
|
645
|
+
} catch {
|
|
646
|
+
// Non-fatal — systemd-user still works while user is logged in.
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
type: 'systemd-user',
|
|
652
|
+
path: getUserUnitPath(),
|
|
653
|
+
lingerEnabled,
|
|
654
|
+
crontabFallback,
|
|
655
|
+
};
|
|
608
656
|
}
|
|
609
657
|
|
|
610
658
|
if (process.platform === 'win32') {
|
|
@@ -624,10 +672,12 @@ export function installService(venvPythonPath) {
|
|
|
624
672
|
export function uninstallService() {
|
|
625
673
|
if (process.platform === 'darwin') return uninstallLaunchd();
|
|
626
674
|
if (process.platform === 'linux') {
|
|
627
|
-
// Clean up
|
|
628
|
-
|
|
629
|
-
if (existsSync(
|
|
630
|
-
if (
|
|
675
|
+
// Clean up whatever variants are installed. systemd-user users may
|
|
676
|
+
// also have a crontab belt installed alongside — remove both.
|
|
677
|
+
if (existsSync(SYSTEM_UNIT_PATH)) uninstallSystemdSystem();
|
|
678
|
+
if (existsSync(getUserUnitPath())) uninstallSystemdUser();
|
|
679
|
+
if (isCrontabInstalled()) uninstallCrontab();
|
|
680
|
+
return;
|
|
631
681
|
}
|
|
632
682
|
if (process.platform === 'win32') return uninstallTaskScheduler();
|
|
633
683
|
}
|
package/lib/wizard.js
CHANGED
|
@@ -9,6 +9,22 @@ 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
|
+
// Stamped onto every CLI telemetry payload so we can tell which `robot-resources`
|
|
14
|
+
// version a user actually ran. Without this, npx-cached old installers look
|
|
15
|
+
// identical to fresh runs in Supabase — which is exactly the visibility gap
|
|
16
|
+
// that left us blind on real-user install failures despite shipping rich
|
|
17
|
+
// diagnostics in PR #163. Read once at module load; safe to fail (telemetry
|
|
18
|
+
// just lands without the field).
|
|
19
|
+
const CLI_VERSION = (() => {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(
|
|
22
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf-8'),
|
|
23
|
+
).version;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
})();
|
|
12
28
|
/**
|
|
13
29
|
* Classify an install error into a short reason code + bounded detail string.
|
|
14
30
|
*
|
|
@@ -83,6 +99,8 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
83
99
|
// "pip installed but router never served a request" from a real
|
|
84
100
|
// working setup in post-hoc telemetry.
|
|
85
101
|
serviceType: null,
|
|
102
|
+
lingerEnabled: null,
|
|
103
|
+
crontabFallback: null,
|
|
86
104
|
pluginInstalled: false,
|
|
87
105
|
openclawDetected: false,
|
|
88
106
|
openclawConfigPatched: false,
|
|
@@ -163,6 +181,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
163
181
|
product: 'cli',
|
|
164
182
|
event_type: 'wizard_started',
|
|
165
183
|
payload: {
|
|
184
|
+
cli_version: CLI_VERSION,
|
|
166
185
|
auth_method: results.authMethod,
|
|
167
186
|
non_interactive: nonInteractive,
|
|
168
187
|
},
|
|
@@ -235,12 +254,24 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
235
254
|
try {
|
|
236
255
|
const svc = installService(getVenvPythonPath());
|
|
237
256
|
results.serviceType = svc.type || null;
|
|
257
|
+
// systemd-user only survives user sessions with linger enabled; the
|
|
258
|
+
// installer now verifies the bit actually flipped and installs a
|
|
259
|
+
// crontab @reboot belt when it didn't. Capture both signals so we
|
|
260
|
+
// can tell which users land on a live-forever setup vs one that
|
|
261
|
+
// dies on logout.
|
|
262
|
+
results.lingerEnabled = svc.lingerEnabled ?? null;
|
|
263
|
+
results.crontabFallback = svc.crontabFallback ?? null;
|
|
238
264
|
if (svc.type === 'skipped') {
|
|
239
265
|
warn(svc.reason);
|
|
240
266
|
results.service = false;
|
|
241
267
|
} else {
|
|
242
268
|
success(`Router registered as ${svc.type} service`);
|
|
243
269
|
info(`Config: ${svc.path}`);
|
|
270
|
+
if (svc.type === 'systemd-user') {
|
|
271
|
+
if (svc.lingerEnabled) info('Linger enabled — router survives logout');
|
|
272
|
+
else warn('Linger not enabled — router may stop when you log out');
|
|
273
|
+
if (svc.crontabFallback) info('Crontab @reboot installed as fallback');
|
|
274
|
+
}
|
|
244
275
|
info('Router will start automatically and restart on crash');
|
|
245
276
|
results.service = true;
|
|
246
277
|
}
|
|
@@ -410,6 +441,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
410
441
|
// another event.
|
|
411
442
|
const installPayload = {
|
|
412
443
|
source: 'wizard',
|
|
444
|
+
cli_version: CLI_VERSION,
|
|
413
445
|
router: results.router || false,
|
|
414
446
|
service: results.service || false,
|
|
415
447
|
scraper: results.scraper || false,
|
|
@@ -419,6 +451,8 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
419
451
|
install_duration_ms: Date.now() - wizardStartMs,
|
|
420
452
|
python_source: results.pythonSource ?? null,
|
|
421
453
|
service_type: results.serviceType ?? null,
|
|
454
|
+
linger_enabled: results.lingerEnabled,
|
|
455
|
+
crontab_fallback: results.crontabFallback,
|
|
422
456
|
health_check: results.healthCheck,
|
|
423
457
|
plugin_installed: results.pluginInstalled,
|
|
424
458
|
openclaw_detected: results.openclawDetected,
|
|
@@ -540,10 +574,9 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
540
574
|
try {
|
|
541
575
|
const statusDir = join(homedir(), '.robot-resources');
|
|
542
576
|
mkdirSync(statusDir, { recursive: true });
|
|
543
|
-
const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version;
|
|
544
577
|
writeFileSync(join(statusDir, 'wizard-status.json'), JSON.stringify({
|
|
545
578
|
completed_at: new Date().toISOString(),
|
|
546
|
-
version:
|
|
579
|
+
version: CLI_VERSION,
|
|
547
580
|
router: results.router || false,
|
|
548
581
|
service: results.service || false,
|
|
549
582
|
scraper: results.scraper || false,
|