robot-resources 1.8.1 → 1.9.0
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 +100 -2
- package/lib/tool-config.js +15 -2
- package/lib/wizard.js +0 -3
- package/package.json +1 -1
package/lib/service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
1
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
@@ -6,6 +6,7 @@ import { readProviderKeys } from '@robot-resources/cli-core/config.mjs';
|
|
|
6
6
|
|
|
7
7
|
const LABEL = 'ai.robotresources.router';
|
|
8
8
|
const SERVICE_NAME = 'robot-resources-router.service';
|
|
9
|
+
const TASK_NAME = 'RobotResourcesRouter';
|
|
9
10
|
const ROUTER_PORT = 3838;
|
|
10
11
|
|
|
11
12
|
// Maps config.json provider_keys names to environment variable names
|
|
@@ -57,7 +58,11 @@ function resolveProviderEnv() {
|
|
|
57
58
|
resolvedKeys[envName] = configKeys[configName];
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
// Windows inherits PATH from the user profile when schtasks fires the task;
|
|
62
|
+
// overwriting it to a Unix path would break every `python.exe` resolution.
|
|
63
|
+
if (process.platform !== 'win32') {
|
|
64
|
+
resolvedKeys['PATH'] = '/usr/local/bin:/usr/bin:/bin';
|
|
65
|
+
}
|
|
61
66
|
return resolvedKeys;
|
|
62
67
|
}
|
|
63
68
|
|
|
@@ -317,6 +322,91 @@ function getLinuxUnitPath() {
|
|
|
317
322
|
return isRoot() ? SYSTEM_UNIT_PATH : getUserUnitPath();
|
|
318
323
|
}
|
|
319
324
|
|
|
325
|
+
// ─── Windows (Task Scheduler) ───────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
function getWrapperCmdPath() {
|
|
328
|
+
return join(homedir(), '.robot-resources', 'rr-router-run.cmd');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function buildWrapperCmd(venvPythonPath, envFilePath, logsDir) {
|
|
332
|
+
// The wrapper sources router.env line-by-line into `set` (so quoting is
|
|
333
|
+
// handled by cmd.exe), then launches the router. stdout/stderr append to
|
|
334
|
+
// log files so the scheduled task — which has no attached console — leaves
|
|
335
|
+
// a trail the user can read.
|
|
336
|
+
return [
|
|
337
|
+
'@echo off',
|
|
338
|
+
'setlocal enabledelayedexpansion',
|
|
339
|
+
`for /f "usebackq tokens=* delims=" %%a in ("${envFilePath}") do set "%%a"`,
|
|
340
|
+
`"${venvPythonPath}" -m robot_resources.cli.main start >> "${join(logsDir, 'router.stdout.log')}" 2>> "${join(logsDir, 'router.stderr.log')}"`,
|
|
341
|
+
'',
|
|
342
|
+
].join('\r\n');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function runSchtasks(args, { ignoreStatus = false } = {}) {
|
|
346
|
+
// spawnSync (not execSync) because paths like `C:\Users\Some Person\...`
|
|
347
|
+
// have spaces and shell re-tokenization via execSync is a quoting footgun.
|
|
348
|
+
// spawnSync with an arg array passes each arg verbatim to schtasks.exe.
|
|
349
|
+
const res = spawnSync('schtasks.exe', args, { stdio: 'pipe', encoding: 'utf-8' });
|
|
350
|
+
if (!ignoreStatus && res.status !== 0) {
|
|
351
|
+
const stderr = (res.stderr || '').toString().trim();
|
|
352
|
+
const stdout = (res.stdout || '').toString().trim();
|
|
353
|
+
throw new Error(`schtasks ${args[0]} failed (status ${res.status}): ${stderr || stdout || 'unknown error'}`);
|
|
354
|
+
}
|
|
355
|
+
return res;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function installTaskScheduler(venvPythonPath) {
|
|
359
|
+
const logsDir = join(homedir(), '.robot-resources', 'logs');
|
|
360
|
+
mkdirSync(logsDir, { recursive: true });
|
|
361
|
+
|
|
362
|
+
const resolvedKeys = resolveProviderEnv();
|
|
363
|
+
const envFilePath = writeEnvFile(resolvedKeys);
|
|
364
|
+
const wrapperPath = getWrapperCmdPath();
|
|
365
|
+
writeFileSync(wrapperPath, buildWrapperCmd(venvPythonPath, envFilePath, logsDir));
|
|
366
|
+
|
|
367
|
+
// /rl LIMITED is the default — explicit here for readability. /f overwrites
|
|
368
|
+
// an existing task of the same name so the install is idempotent.
|
|
369
|
+
runSchtasks([
|
|
370
|
+
'/create', '/sc', 'onlogon',
|
|
371
|
+
'/tn', TASK_NAME,
|
|
372
|
+
'/tr', `"${wrapperPath}"`,
|
|
373
|
+
'/rl', 'LIMITED',
|
|
374
|
+
'/f',
|
|
375
|
+
]);
|
|
376
|
+
|
|
377
|
+
// Launch now so the wizard's post-install health check can hit port 3838.
|
|
378
|
+
// Best-effort — if the immediate run fails, the task is still registered
|
|
379
|
+
// and will fire at next logon.
|
|
380
|
+
runSchtasks(['/run', '/tn', TASK_NAME], { ignoreStatus: true });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function uninstallTaskScheduler() {
|
|
384
|
+
runSchtasks(['/end', '/tn', TASK_NAME], { ignoreStatus: true });
|
|
385
|
+
runSchtasks(['/delete', '/tn', TASK_NAME, '/f'], { ignoreStatus: true });
|
|
386
|
+
if (existsSync(getWrapperCmdPath())) unlinkSync(getWrapperCmdPath());
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function isTaskSchedulerRunning() {
|
|
390
|
+
const res = spawnSync(
|
|
391
|
+
'schtasks.exe',
|
|
392
|
+
['/query', '/tn', TASK_NAME, '/fo', 'LIST', '/v'],
|
|
393
|
+
{ stdio: 'pipe', encoding: 'utf-8' },
|
|
394
|
+
);
|
|
395
|
+
if (res.status !== 0) return false;
|
|
396
|
+
// 0x41301 is TASK_RUNNING (HRESULT). Locale-independent — the localized
|
|
397
|
+
// "Running" / "In corso" / "実行中" string is unreliable across Windows SKUs.
|
|
398
|
+
return (res.stdout || '').includes('0x41301');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function isTaskSchedulerInstalled() {
|
|
402
|
+
const res = spawnSync(
|
|
403
|
+
'schtasks.exe',
|
|
404
|
+
['/query', '/tn', TASK_NAME],
|
|
405
|
+
{ stdio: 'pipe' },
|
|
406
|
+
);
|
|
407
|
+
return res.status === 0;
|
|
408
|
+
}
|
|
409
|
+
|
|
320
410
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
321
411
|
|
|
322
412
|
/**
|
|
@@ -367,6 +457,11 @@ export function installService(venvPythonPath) {
|
|
|
367
457
|
return { type: 'systemd-user', path: getUserUnitPath() };
|
|
368
458
|
}
|
|
369
459
|
|
|
460
|
+
if (process.platform === 'win32') {
|
|
461
|
+
installTaskScheduler(venvPythonPath);
|
|
462
|
+
return { type: 'schtasks', path: getWrapperCmdPath() };
|
|
463
|
+
}
|
|
464
|
+
|
|
370
465
|
throw new Error(
|
|
371
466
|
`Service registration not supported on ${process.platform}.\n` +
|
|
372
467
|
` Run the router manually: rr-router start`
|
|
@@ -383,6 +478,7 @@ export function uninstallService() {
|
|
|
383
478
|
if (existsSync(SYSTEM_UNIT_PATH)) return uninstallSystemdSystem();
|
|
384
479
|
if (existsSync(getUserUnitPath())) return uninstallSystemdUser();
|
|
385
480
|
}
|
|
481
|
+
if (process.platform === 'win32') return uninstallTaskScheduler();
|
|
386
482
|
}
|
|
387
483
|
|
|
388
484
|
/**
|
|
@@ -407,6 +503,7 @@ export function isServiceRunning() {
|
|
|
407
503
|
return false;
|
|
408
504
|
}
|
|
409
505
|
}
|
|
506
|
+
if (process.platform === 'win32') return isTaskSchedulerRunning();
|
|
410
507
|
return false;
|
|
411
508
|
}
|
|
412
509
|
|
|
@@ -416,6 +513,7 @@ export function isServiceRunning() {
|
|
|
416
513
|
export function isServiceInstalled() {
|
|
417
514
|
if (process.platform === 'darwin') return existsSync(getPlistPath());
|
|
418
515
|
if (process.platform === 'linux') return existsSync(SYSTEM_UNIT_PATH) || existsSync(getUserUnitPath());
|
|
516
|
+
if (process.platform === 'win32') return isTaskSchedulerInstalled();
|
|
419
517
|
return false;
|
|
420
518
|
}
|
|
421
519
|
|
package/lib/tool-config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
3
|
+
import { readFileSync, writeFileSync, copyFileSync, cpSync, mkdirSync, existsSync, rmSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { isOpenClawInstalled, isOpenClawPluginInstalled, getOpenClawAuthMode, isClaudeCodeInstalled, isCursorInstalled } from './detect.js';
|
|
@@ -98,7 +98,11 @@ function registerScraperMcp() {
|
|
|
98
98
|
*
|
|
99
99
|
* The plugin ships as a CLI dependency (@robot-resources/openclaw-plugin).
|
|
100
100
|
* Instead of spawning `openclaw plugins install` (30s npm overhead),
|
|
101
|
-
* we copy
|
|
101
|
+
* we copy files directly. Same destination, same result.
|
|
102
|
+
*
|
|
103
|
+
* Since 0.5.5, the plugin is a thin shim (index.js) that imports the rest
|
|
104
|
+
* of its code from ./lib/*.js — copy the lib/ directory too, or the shim
|
|
105
|
+
* fails to load with MODULE_NOT_FOUND.
|
|
102
106
|
*/
|
|
103
107
|
function installPluginFiles() {
|
|
104
108
|
const require = createRequire(import.meta.url);
|
|
@@ -111,6 +115,15 @@ function installPluginFiles() {
|
|
|
111
115
|
for (const file of ['index.js', 'openclaw.plugin.json', 'package.json']) {
|
|
112
116
|
copyFileSync(join(pluginDir, file), join(targetDir, file));
|
|
113
117
|
}
|
|
118
|
+
|
|
119
|
+
// Copy lib/ recursively. Clear the destination first so files removed in
|
|
120
|
+
// a new version don't linger from a previous install.
|
|
121
|
+
const srcLib = join(pluginDir, 'lib');
|
|
122
|
+
const dstLib = join(targetDir, 'lib');
|
|
123
|
+
if (existsSync(srcLib)) {
|
|
124
|
+
rmSync(dstLib, { recursive: true, force: true });
|
|
125
|
+
cpSync(srcLib, dstLib, { recursive: true });
|
|
126
|
+
}
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
/**
|
package/lib/wizard.js
CHANGED
|
@@ -162,9 +162,6 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
162
162
|
if (isServiceRunning()) {
|
|
163
163
|
success('Router service already running');
|
|
164
164
|
results.service = true;
|
|
165
|
-
} else if (process.platform === 'win32') {
|
|
166
|
-
warn('Windows detected — automatic service not supported');
|
|
167
|
-
info('Run the router manually: rr-router start');
|
|
168
165
|
} else {
|
|
169
166
|
// Check port availability
|
|
170
167
|
if (!isPortAvailable()) {
|