@lumi.ai/runner 0.1.0 → 0.3.0-dev.6
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/README.md +30 -1
- package/dist/cli.js +101 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ lumi-runner login --token <customToken> --api-key <key> --project <projectId>
|
|
|
52
52
|
| `ship add [ids…]` / `remove` / `list` | Which Ships this machine serves (omit ids to pick from a list) |
|
|
53
53
|
| `doctor` | **Preflight** — can this machine actually run a job? Non-zero exit if not |
|
|
54
54
|
| `service install` / `uninstall` / `restart` / `status` | Run the daemon in the background, always |
|
|
55
|
+
| `uninstall` | Remove the service, then tell you the `npm rm -g` step (`--purge` also clears config) |
|
|
55
56
|
| `start` | Run the daemon in the foreground |
|
|
56
57
|
| `status` | Ships, approval state, current job, queue depth, today's tokens |
|
|
57
58
|
| `logs [-f]` | The daemon's own rotating log file |
|
|
@@ -69,7 +70,7 @@ Run this first when something isn't working. Everything it checks used to be dis
|
|
|
69
70
|
running job*, surfacing as a failed task on your board minutes later: a missing `claude` binary,
|
|
70
71
|
unsaved Ship credentials, a machine no captain approved. It checks Node ≥ 20, your stored session,
|
|
71
72
|
per-Ship approval and credentials, the engine binary, `git`/`gh` when an agent uses GitHub, server
|
|
72
|
-
reachability,
|
|
73
|
+
reachability, the background service, and whether that service still points at a CLI that exists.
|
|
73
74
|
|
|
74
75
|
### Background service
|
|
75
76
|
|
|
@@ -104,6 +105,34 @@ from the transcript before upload. `lumi-runner doctor` tells you when a Ship is
|
|
|
104
105
|
|
|
105
106
|
The only thing stored locally is your own session, in `~/.lumi-runner/config.json` (mode `0600`).
|
|
106
107
|
|
|
108
|
+
## Uninstalling
|
|
109
|
+
|
|
110
|
+
Remove the service **before** the package:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
lumi-runner uninstall
|
|
114
|
+
npm rm -g @lumi.ai/runner
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Order matters, and getting it wrong is silent. The launchd plist / systemd unit records an
|
|
118
|
+
*absolute path* to the CLI inside `node_modules` — it has to, since the OS needs something to exec.
|
|
119
|
+
`npm rm -g` deletes that file but knows nothing about your service, so the service stays registered
|
|
120
|
+
and keeps respawning a binary that is no longer there, roughly every ten seconds, for as long as
|
|
121
|
+
the machine lives. npm cannot help here: since npm 7 it does not run `preuninstall`/`postuninstall`
|
|
122
|
+
scripts at all, so no package can clean up after itself.
|
|
123
|
+
|
|
124
|
+
Already removed the package? Reinstall it, uninstall properly, then remove it again:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm i -g @lumi.ai/runner && lumi-runner uninstall && npm rm -g @lumi.ai/runner
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`lumi-runner doctor` reports a machine that is already in this state, naming the missing path.
|
|
131
|
+
|
|
132
|
+
Neither command touches `~/.lumi-runner`. That directory is this machine's **identity** — its
|
|
133
|
+
runner id and your session — so uninstalling and reinstalling keeps the approvals your captains
|
|
134
|
+
already granted. Add `--purge` to delete it too, and expect to be approved again from scratch.
|
|
135
|
+
|
|
107
136
|
## Upgrading from `crew-runner`
|
|
108
137
|
|
|
109
138
|
This package was previously distributed inside the Lumi monorepo as `@lumi/crew-runner`, with a
|
package/dist/cli.js
CHANGED
|
@@ -502,7 +502,7 @@ function mcpUrl(config2) {
|
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
// src/version.ts
|
|
505
|
-
var RUNNER_VERSION = true ? "0.
|
|
505
|
+
var RUNNER_VERSION = true ? "0.3.0-dev.6" : "0.0.0-dev";
|
|
506
506
|
|
|
507
507
|
// src/auth.ts
|
|
508
508
|
import os2 from "node:os";
|
|
@@ -2563,6 +2563,34 @@ ${envEntries}
|
|
|
2563
2563
|
function escapeXml(value) {
|
|
2564
2564
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2565
2565
|
}
|
|
2566
|
+
function unescapeXml(value) {
|
|
2567
|
+
return value.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/&/g, "&");
|
|
2568
|
+
}
|
|
2569
|
+
function parsePlistCliPath(plist) {
|
|
2570
|
+
const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(plist);
|
|
2571
|
+
if (!block) return void 0;
|
|
2572
|
+
const args = [...block[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => unescapeXml(m[1]));
|
|
2573
|
+
return args.length >= 2 ? args[1] : void 0;
|
|
2574
|
+
}
|
|
2575
|
+
function parseSystemdCliPath(unit) {
|
|
2576
|
+
const line = /^ExecStart=(.*)$/m.exec(unit);
|
|
2577
|
+
if (!line) return void 0;
|
|
2578
|
+
const command = line[1].trim();
|
|
2579
|
+
if (!command.endsWith(" start")) return void 0;
|
|
2580
|
+
const parts = command.slice(0, -" start".length).split(" ");
|
|
2581
|
+
return parts.length === 2 ? parts[1] : void 0;
|
|
2582
|
+
}
|
|
2583
|
+
function execFacts(unitPath, parse) {
|
|
2584
|
+
let text;
|
|
2585
|
+
try {
|
|
2586
|
+
text = fs4.readFileSync(unitPath, "utf8");
|
|
2587
|
+
} catch {
|
|
2588
|
+
return {};
|
|
2589
|
+
}
|
|
2590
|
+
const execPath = parse(text);
|
|
2591
|
+
if (!execPath || !path4.isAbsolute(execPath)) return {};
|
|
2592
|
+
return { execPath, execMissing: !fs4.existsSync(execPath) };
|
|
2593
|
+
}
|
|
2566
2594
|
function systemdUnit() {
|
|
2567
2595
|
const env = serviceEnv();
|
|
2568
2596
|
const envLines = Object.entries(env).map(([k, v]) => `Environment="${k}=${v}"`).join("\n");
|
|
@@ -2588,13 +2616,17 @@ function serviceStatus() {
|
|
|
2588
2616
|
if (process.platform === "darwin") {
|
|
2589
2617
|
const unitPath = launchAgentPath();
|
|
2590
2618
|
if (!fs4.existsSync(unitPath)) return { state: "not-installed", detail: "No LaunchAgent installed." };
|
|
2619
|
+
const exec = execFacts(unitPath, parsePlistCliPath);
|
|
2591
2620
|
const printed = run("launchctl", ["print", `gui/${uid()}/${SERVICE_LABEL}`]);
|
|
2592
|
-
if (!printed.ok)
|
|
2621
|
+
if (!printed.ok) {
|
|
2622
|
+
return { state: "installed", detail: "LaunchAgent present but not loaded.", unitPath, ...exec };
|
|
2623
|
+
}
|
|
2593
2624
|
const running = /\bpid = \d+/.test(printed.out);
|
|
2594
2625
|
return {
|
|
2595
2626
|
state: running ? "running" : "installed",
|
|
2596
2627
|
detail: running ? "LaunchAgent loaded and running." : "LaunchAgent loaded but not running.",
|
|
2597
|
-
unitPath
|
|
2628
|
+
unitPath,
|
|
2629
|
+
...exec
|
|
2598
2630
|
};
|
|
2599
2631
|
}
|
|
2600
2632
|
if (process.platform === "linux") {
|
|
@@ -2604,7 +2636,8 @@ function serviceStatus() {
|
|
|
2604
2636
|
return {
|
|
2605
2637
|
state: active.out === "active" ? "running" : "installed",
|
|
2606
2638
|
detail: `systemd user unit is ${active.out || "unknown"}.`,
|
|
2607
|
-
unitPath
|
|
2639
|
+
unitPath,
|
|
2640
|
+
...execFacts(unitPath, parseSystemdCliPath)
|
|
2608
2641
|
};
|
|
2609
2642
|
}
|
|
2610
2643
|
if (process.platform === "win32") {
|
|
@@ -2758,19 +2791,32 @@ async function checkMcp(url) {
|
|
|
2758
2791
|
);
|
|
2759
2792
|
}
|
|
2760
2793
|
}
|
|
2761
|
-
function
|
|
2762
|
-
|
|
2794
|
+
function serviceCheckFrom(status) {
|
|
2795
|
+
if (status.execMissing) {
|
|
2796
|
+
return warn(
|
|
2797
|
+
"service",
|
|
2798
|
+
"Background service",
|
|
2799
|
+
`Installed, but the unit points at a CLI that no longer exists (${status.execPath}). It is registered and retrying forever without ever starting.`,
|
|
2800
|
+
"Run `lumi-runner uninstall` to remove it, or `lumi-runner service install` to repoint it at this install."
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2763
2803
|
if (status.state === "running") return ok("service", "Background service", status.detail);
|
|
2764
2804
|
if (status.state === "installed") {
|
|
2765
2805
|
return warn("service", "Background service", status.detail, "Run `lumi-runner service restart`.");
|
|
2766
2806
|
}
|
|
2767
2807
|
if (status.state === "unsupported") return warn("service", "Background service", status.detail);
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2808
|
+
if (status.state === "not-installed") {
|
|
2809
|
+
return warn(
|
|
2810
|
+
"service",
|
|
2811
|
+
"Background service",
|
|
2812
|
+
"Not installed \u2014 the daemon only runs while a terminal is open.",
|
|
2813
|
+
"Run `lumi-runner service install` to keep this Ship online whenever the machine is."
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
return warn("service", "Background service", `Unrecognised service state: ${status.state}.`);
|
|
2817
|
+
}
|
|
2818
|
+
function checkService() {
|
|
2819
|
+
return serviceCheckFrom(serviceStatus());
|
|
2774
2820
|
}
|
|
2775
2821
|
async function checkShips(session) {
|
|
2776
2822
|
const { config: config2, fb, user } = session;
|
|
@@ -3142,6 +3188,48 @@ async function runServiceStatus() {
|
|
|
3142
3188
|
}
|
|
3143
3189
|
say.line(` ${status.detail}`);
|
|
3144
3190
|
if (status.unitPath) say.line(` ${status.unitPath}`);
|
|
3191
|
+
if (status.execMissing) {
|
|
3192
|
+
say.warn(`The unit runs ${status.execPath}, which no longer exists. Run \`lumi-runner uninstall\`.`);
|
|
3193
|
+
}
|
|
3194
|
+
return 0;
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
// src/cli/commands/uninstall.ts
|
|
3198
|
+
import fs5 from "node:fs";
|
|
3199
|
+
async function runUninstall(options) {
|
|
3200
|
+
const before = serviceStatus();
|
|
3201
|
+
const dir = configDir();
|
|
3202
|
+
const hadService = before.state !== "not-installed" && before.state !== "unsupported";
|
|
3203
|
+
if (hadService) uninstallService();
|
|
3204
|
+
let purged = false;
|
|
3205
|
+
if (options.purge && fs5.existsSync(dir)) {
|
|
3206
|
+
const confirmed = await promptConfirm({
|
|
3207
|
+
message: `Delete ${dir}? This machine loses its identity \u2014 a captain has to approve it again after reinstalling.`,
|
|
3208
|
+
initialValue: false
|
|
3209
|
+
});
|
|
3210
|
+
if (confirmed) {
|
|
3211
|
+
fs5.rmSync(dir, { recursive: true, force: true });
|
|
3212
|
+
purged = true;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
if (isJson()) {
|
|
3216
|
+
emitJson({
|
|
3217
|
+
serviceRemoved: hadService,
|
|
3218
|
+
unitPath: before.unitPath ?? null,
|
|
3219
|
+
configDir: dir,
|
|
3220
|
+
configRemoved: purged,
|
|
3221
|
+
// The step this command cannot take for the user — see the note above.
|
|
3222
|
+
remaining: "npm rm -g @lumi.ai/runner"
|
|
3223
|
+
});
|
|
3224
|
+
return 0;
|
|
3225
|
+
}
|
|
3226
|
+
if (hadService) say.success(`Service removed${before.unitPath ? ` (${before.unitPath})` : ""}.`);
|
|
3227
|
+
else say.info("No background service was installed.");
|
|
3228
|
+
if (purged) say.success(`Removed ${dir}.`);
|
|
3229
|
+
else if (options.purge) say.info(`Left ${dir} alone.`);
|
|
3230
|
+
else say.info(`Kept ${dir} \u2014 your session and this machine's runner id. Add --purge to remove it.`);
|
|
3231
|
+
say.step("Now remove the package:");
|
|
3232
|
+
say.line(" npm rm -g @lumi.ai/runner");
|
|
3145
3233
|
return 0;
|
|
3146
3234
|
}
|
|
3147
3235
|
|
|
@@ -3425,6 +3513,7 @@ service.command("install").description("Install and start the background service
|
|
|
3425
3513
|
service.command("uninstall").description("Stop and remove the background service").action(action(runServiceUninstall));
|
|
3426
3514
|
service.command("restart").description("Restart the background service").action(action(runServiceRestart));
|
|
3427
3515
|
service.command("status").description("Is the background service installed and running?").action(action(runServiceStatus));
|
|
3516
|
+
program.command("uninstall").description("Remove the background service and print the final npm step").option("--purge", "also delete the config directory (loses this machine's runner id)").action(action(async (options) => runUninstall(options)));
|
|
3428
3517
|
var config = program.command("config").description("Local per-machine preferences");
|
|
3429
3518
|
config.command("list").description("Show current settings").action(action(runConfigList));
|
|
3430
3519
|
config.command("set <key> <value>").description("Change a setting (notifications | keepAwake \u2192 on/off)").action(action(async (key, value) => runConfigSet(key, value)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumi.ai/runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-dev.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lumi Crew runner daemon — claims jobs from your Ships and executes them as headless Claude sessions on your own machine.",
|
|
6
6
|
"//name": "The ONLY package in this monorepo published to the public registry, so it is the one that does not follow the internal @lumi/crew-* convention: `@lumi` is not a scope we own, `@lumi.ai` is (the npm org). The workspace DIRECTORY stays packages/crew/runner — renaming the package is not renaming the folder.",
|