@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.
Files changed (3) hide show
  1. package/README.md +30 -1
  2. package/dist/cli.js +101 -12
  3. 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, and the background service.
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.1.0" : "0.0.0-dev";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2565
2565
  }
2566
+ function unescapeXml(value) {
2567
+ return value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&amp;/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) return { state: "installed", detail: "LaunchAgent present but not loaded.", unitPath };
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 checkService() {
2762
- const status = serviceStatus();
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
- return warn(
2769
- "service",
2770
- "Background service",
2771
- "Not installed \u2014 the daemon only runs while a terminal is open.",
2772
- "Run `lumi-runner service install` to keep this Ship online whenever the machine is."
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.1.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.",