@slayzone/cli 0.36.0-beta.10 → 0.36.0-beta.11

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 +23 -0
  2. package/dist/slay.js +864 -239
  3. package/package.json +1 -1
package/dist/slay.js CHANGED
@@ -7248,7 +7248,7 @@ var require_BufferList = __commonJS({
7248
7248
  this.head = this.tail = null;
7249
7249
  this.length = 0;
7250
7250
  };
7251
- BufferList.prototype.join = function join5(s) {
7251
+ BufferList.prototype.join = function join7(s) {
7252
7252
  if (this.length === 0) return "";
7253
7253
  var p2 = this.head;
7254
7254
  var ret = "" + p2.data;
@@ -46794,9 +46794,13 @@ complete -c slay -n '__fish_use_subcommand' -a 'tags' -d 'Manage tags'
46794
46794
  complete -c slay -n '__fish_use_subcommand' -a 'templates' -d 'Manage task templates'
46795
46795
  complete -c slay -n '__fish_use_subcommand' -a 'automations' -d 'Manage automations'
46796
46796
  complete -c slay -n '__fish_use_subcommand' -a 'processes' -d 'Manage processes'
46797
+ complete -c slay -n '__fish_use_subcommand' -a 'hub' -d 'Run, list and target SlayZone hubs'
46798
+ complete -c slay -n '__fish_use_subcommand' -a 'runner' -d 'Run and manage SlayZone runners'
46797
46799
  complete -c slay -n '__fish_use_subcommand' -a 'completions' -d 'Print shell completions'
46798
46800
  complete -c slay -n '__fish_seen_subcommand_from tasks' -a 'list create view done update archive delete open search subtasks subtask-add tag'
46799
46801
  complete -c slay -n '__fish_seen_subcommand_from processes' -a 'list logs kill follow'
46802
+ complete -c slay -n '__fish_seen_subcommand_from hub' -a 'ls create start stop rm restart logs registered use current forget'
46803
+ complete -c slay -n '__fish_seen_subcommand_from runner' -a 'ls create start stop rm restart logs'
46800
46804
  complete -c slay -n '__fish_seen_subcommand_from projects' -a 'list create update'
46801
46805
  complete -c slay -n '__fish_seen_subcommand_from tags' -a 'list create delete'
46802
46806
  complete -c slay -n '__fish_seen_subcommand_from templates' -a 'list view create update delete'
@@ -46814,6 +46818,8 @@ _slay() {
46814
46818
  'templates:Manage task templates'
46815
46819
  'automations:Manage automations'
46816
46820
  'processes:Manage processes'
46821
+ 'hub:Run, list and target SlayZone hubs'
46822
+ 'runner:Run and manage SlayZone runners'
46817
46823
  'completions:Print shell completions'
46818
46824
  )
46819
46825
  local -a task_commands
@@ -46863,6 +46869,30 @@ _slay() {
46863
46869
  'run:Run automation manually'
46864
46870
  'runs:View execution history'
46865
46871
  )
46872
+ local -a hub_commands
46873
+ hub_commands=(
46874
+ 'ls:List running hubs'
46875
+ 'create:Create a hub here and keep it running'
46876
+ 'start:Start a stopped hub'
46877
+ 'stop:Stop a hub, keeping it registered'
46878
+ 'rm:Stop a hub and remove its registration'
46879
+ 'restart:Restart a hub'
46880
+ 'logs:Show a hub log'
46881
+ 'registered:List hubs registered with the OS service manager'
46882
+ 'use:Point this CLI at a hub'
46883
+ 'current:Show which hub this CLI targets'
46884
+ 'forget:Drop the stored hub target'
46885
+ )
46886
+ local -a runner_commands
46887
+ runner_commands=(
46888
+ 'ls:List installed runners'
46889
+ 'create:Install a runner here and keep it running'
46890
+ 'start:Start a stopped runner'
46891
+ 'stop:Stop a runner, keeping it registered'
46892
+ 'rm:Stop a runner and remove its registration'
46893
+ 'restart:Restart a runner'
46894
+ 'logs:Show a runner log'
46895
+ )
46866
46896
  case $words[2] in
46867
46897
  tasks)
46868
46898
  _describe 'task commands' task_commands
@@ -46882,6 +46912,12 @@ _slay() {
46882
46912
  automations)
46883
46913
  _describe 'automation commands' automation_commands
46884
46914
  ;;
46915
+ hub)
46916
+ _describe 'hub commands' hub_commands
46917
+ ;;
46918
+ runner)
46919
+ _describe 'runner commands' runner_commands
46920
+ ;;
46885
46921
  completions)
46886
46922
  _arguments ':shell:(fish zsh bash)'
46887
46923
  ;;
@@ -46899,7 +46935,7 @@ _slay_completions() {
46899
46935
  cur="\${COMP_WORDS[COMP_CWORD]}"
46900
46936
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
46901
46937
  if [ $COMP_CWORD -eq 1 ]; then
46902
- COMPREPLY=( $(compgen -W "tasks projects tags templates automations processes completions" -- "$cur") )
46938
+ COMPREPLY=( $(compgen -W "tasks projects tags templates automations processes hub runner completions" -- "$cur") )
46903
46939
  elif [ $COMP_CWORD -eq 2 ]; then
46904
46940
  case "$prev" in
46905
46941
  tasks)
@@ -46920,6 +46956,12 @@ _slay_completions() {
46920
46956
  automations)
46921
46957
  COMPREPLY=( $(compgen -W "list view create update delete toggle run runs" -- "$cur") )
46922
46958
  ;;
46959
+ hub)
46960
+ COMPREPLY=( $(compgen -W "ls create start stop rm restart logs registered use current forget" -- "$cur") )
46961
+ ;;
46962
+ runner)
46963
+ COMPREPLY=( $(compgen -W "ls create start stop rm restart logs" -- "$cur") )
46964
+ ;;
46923
46965
  completions)
46924
46966
  COMPREPLY=( $(compgen -W "fish zsh bash" -- "$cur") )
46925
46967
  ;;
@@ -51987,25 +52029,45 @@ function panelsCommand() {
51987
52029
  }
51988
52030
 
51989
52031
  // src/commands/hub.ts
51990
- var import_node_child_process2 = require("node:child_process");
51991
- var import_node_module = require("node:module");
51992
- var import_node_fs6 = require("node:fs");
51993
- var import_node_path6 = require("node:path");
52032
+ var import_node_child_process3 = require("node:child_process");
52033
+ var import_node_fs7 = require("node:fs");
52034
+ var import_node_path7 = require("node:path");
51994
52035
 
51995
- // ../../shared/platform/src/hub-service.ts
52036
+ // ../../shared/platform/src/service-unit.ts
51996
52037
  var import_node_child_process = require("node:child_process");
51997
52038
  var import_node_fs5 = require("node:fs");
51998
52039
  var import_node_os = require("node:os");
51999
52040
  var import_node_path5 = require("node:path");
52000
- var LABEL_PREFIX = "com.slayzone.hub.";
52001
- var SYSTEMD_PREFIX = "slayzone-hub-";
52002
- function isValidHubName(name) {
52041
+ var KINDS = {
52042
+ hub: {
52043
+ labelPrefix: "com.slayzone.hub.",
52044
+ systemdPrefix: "slayzone-hub-",
52045
+ description: "SlayZone hub",
52046
+ /** launchd stdout/stderr basenames (systemd uses journald). */
52047
+ outLog: "hub.out.log",
52048
+ errLog: "hub.err.log",
52049
+ /** The `slay … logs <name>` command, quoted in the systemd unit's comment. */
52050
+ logsCommand: "slay hub logs",
52051
+ /** How an operator picks a different name — quoted in the invalid-name error. */
52052
+ renameHint: "Rename with `--name` or the config.json `hubName` key."
52053
+ },
52054
+ runner: {
52055
+ labelPrefix: "com.slayzone.runner.",
52056
+ systemdPrefix: "slayzone-runner-",
52057
+ description: "SlayZone runner",
52058
+ outLog: "runner.out.log",
52059
+ errLog: "runner.err.log",
52060
+ logsCommand: "slay runner logs",
52061
+ renameHint: "Pick a different name for `slay runner create`."
52062
+ }
52063
+ };
52064
+ function isValidServiceName(name) {
52003
52065
  return /^[A-Za-z0-9_][A-Za-z0-9._-]*$/.test(name);
52004
52066
  }
52005
- function assertValidHubName(name) {
52006
- if (!isValidHubName(name)) {
52067
+ function assertValidServiceName(kind, name) {
52068
+ if (!isValidServiceName(name)) {
52007
52069
  throw new Error(
52008
- `[slayzone] invalid hub name ${JSON.stringify(name)} \u2014 a name may contain only letters, digits, dot, dash and underscore (no slashes, spaces or newlines), and must not start with a dot. Rename with \`--name\` or the config.json \`hubName\` key.`
52070
+ `[slayzone] invalid ${kind} name ${JSON.stringify(name)} \u2014 a name may contain only letters, digits, dot, dash and underscore (no slashes, spaces or newlines), and must not start with a dot. ${KINDS[kind].renameHint}`
52009
52071
  );
52010
52072
  }
52011
52073
  }
@@ -52031,41 +52093,44 @@ function detectBackend() {
52031
52093
  return "none";
52032
52094
  }
52033
52095
  }
52034
- function hubUnitPath(name, backend, unitDir = defaultUnitDir(backend)) {
52035
- assertValidHubName(name);
52036
- return backend === "launchd" ? (0, import_node_path5.join)(unitDir, `${LABEL_PREFIX}${name}.plist`) : (0, import_node_path5.join)(unitDir, `${SYSTEMD_PREFIX}${name}.service`);
52096
+ function unitPath(kind, name, backend, unitDir = defaultUnitDir(backend)) {
52097
+ assertValidServiceName(kind, name);
52098
+ return backend === "launchd" ? (0, import_node_path5.join)(unitDir, `${KINDS[kind].labelPrefix}${name}.plist`) : (0, import_node_path5.join)(unitDir, `${KINDS[kind].systemdPrefix}${name}.service`);
52037
52099
  }
52038
- function nameFromUnitFile(file2, backend) {
52100
+ function nameFromUnitFile(kind, file2, backend) {
52039
52101
  if (backend === "launchd") {
52040
- if (!file2.startsWith(LABEL_PREFIX) || !file2.endsWith(".plist")) return null;
52041
- return file2.slice(LABEL_PREFIX.length, -".plist".length) || null;
52102
+ const prefix2 = KINDS[kind].labelPrefix;
52103
+ if (!file2.startsWith(prefix2) || !file2.endsWith(".plist")) return null;
52104
+ return file2.slice(prefix2.length, -".plist".length) || null;
52042
52105
  }
52043
- if (!file2.startsWith(SYSTEMD_PREFIX) || !file2.endsWith(".service")) return null;
52044
- return file2.slice(SYSTEMD_PREFIX.length, -".service".length) || null;
52106
+ const prefix = KINDS[kind].systemdPrefix;
52107
+ if (!file2.startsWith(prefix) || !file2.endsWith(".service")) return null;
52108
+ return file2.slice(prefix.length, -".service".length) || null;
52045
52109
  }
52046
52110
  function xmlEscape(value) {
52047
52111
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
52048
52112
  }
52049
- function hubEnv(spec) {
52050
- const merged = {
52051
- SLAYZONE_ROOT: spec.root,
52052
- SLAYZONE_HUB_NAME: spec.name
52053
- };
52054
- if (spec.port !== void 0) merged.SLAYZONE_HUB_ADDRESS = `127.0.0.1:${spec.port}`;
52113
+ function unitEnv(spec) {
52114
+ const merged = { SLAYZONE_ROOT: spec.root };
52115
+ if (spec.kind === "hub") {
52116
+ merged.SLAYZONE_HUB_NAME = spec.name;
52117
+ if (spec.port !== void 0) merged.SLAYZONE_HUB_ADDRESS = `127.0.0.1:${spec.port}`;
52118
+ }
52055
52119
  Object.assign(merged, spec.env ?? {});
52056
52120
  return Object.entries(merged);
52057
52121
  }
52058
52122
  function renderLaunchdPlist(spec) {
52059
- assertValidHubName(spec.name);
52123
+ assertValidServiceName(spec.kind, spec.name);
52124
+ const kind = KINDS[spec.kind];
52060
52125
  const programArgs = [spec.command, ...spec.args].map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
52061
- const envEntries = hubEnv(spec).map(([k, v2]) => ` <key>${xmlEscape(k)}</key>
52126
+ const envEntries = unitEnv(spec).map(([k, v2]) => ` <key>${xmlEscape(k)}</key>
52062
52127
  <string>${xmlEscape(v2)}</string>`).join("\n");
52063
52128
  return `<?xml version="1.0" encoding="UTF-8"?>
52064
52129
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
52065
52130
  <plist version="1.0">
52066
52131
  <dict>
52067
52132
  <key>Label</key>
52068
- <string>${LABEL_PREFIX}${spec.name}</string>
52133
+ <string>${kind.labelPrefix}${spec.name}</string>
52069
52134
  <key>ProgramArguments</key>
52070
52135
  <array>
52071
52136
  ${programArgs}
@@ -52084,9 +52149,9 @@ ${envEntries}
52084
52149
  <false/>
52085
52150
  </dict>
52086
52151
  <key>StandardOutPath</key>
52087
- <string>${xmlEscape((0, import_node_path5.join)(spec.logDir, "hub.out.log"))}</string>
52152
+ <string>${xmlEscape((0, import_node_path5.join)(spec.logDir, kind.outLog))}</string>
52088
52153
  <key>StandardErrorPath</key>
52089
- <string>${xmlEscape((0, import_node_path5.join)(spec.logDir, "hub.err.log"))}</string>
52154
+ <string>${xmlEscape((0, import_node_path5.join)(spec.logDir, kind.errLog))}</string>
52090
52155
  <key>ProcessType</key>
52091
52156
  <string>Background</string>
52092
52157
  </dict>
@@ -52094,11 +52159,12 @@ ${envEntries}
52094
52159
  `;
52095
52160
  }
52096
52161
  function renderSystemdUnit(spec) {
52097
- assertValidHubName(spec.name);
52162
+ assertValidServiceName(spec.kind, spec.name);
52163
+ const kind = KINDS[spec.kind];
52098
52164
  const exec = [spec.command, ...spec.args].join(" ");
52099
- const envLines = hubEnv(spec).map(([k, v2]) => `Environment=${k}=${v2}`).join("\n");
52165
+ const envLines = unitEnv(spec).map(([k, v2]) => `Environment=${k}=${v2}`).join("\n");
52100
52166
  return `[Unit]
52101
- Description=SlayZone hub (${spec.name})
52167
+ Description=${kind.description} (${spec.name})
52102
52168
  Documentation=https://github.com/debuglebowski/slayzone
52103
52169
  After=network.target
52104
52170
 
@@ -52109,7 +52175,7 @@ ${envLines}
52109
52175
  ExecStart=${exec}
52110
52176
  Restart=on-failure
52111
52177
  RestartSec=2
52112
- # stdout/stderr go to journald; read them with \`slay hub logs ${spec.name}\`.
52178
+ # stdout/stderr go to journald; read them with \`${kind.logsCommand} ${spec.name}\`.
52113
52179
  StandardOutput=journal
52114
52180
  StandardError=journal
52115
52181
 
@@ -52117,20 +52183,20 @@ StandardError=journal
52117
52183
  WantedBy=default.target
52118
52184
  `;
52119
52185
  }
52120
- function writeHubUnit(spec, backend, unitDir = defaultUnitDir(backend)) {
52121
- const path10 = hubUnitPath(spec.name, backend, unitDir);
52186
+ function writeUnit(spec, backend, unitDir = defaultUnitDir(backend)) {
52187
+ const path10 = unitPath(spec.kind, spec.name, backend, unitDir);
52122
52188
  (0, import_node_fs5.mkdirSync)(unitDir, { recursive: true });
52123
52189
  const content = backend === "launchd" ? renderLaunchdPlist(spec) : renderSystemdUnit(spec);
52124
52190
  (0, import_node_fs5.writeFileSync)(path10, content, { mode: 420 });
52125
52191
  return path10;
52126
52192
  }
52127
- function removeHubUnit(name, backend, unitDir = defaultUnitDir(backend)) {
52128
- const path10 = hubUnitPath(name, backend, unitDir);
52193
+ function removeUnit(kind, name, backend, unitDir = defaultUnitDir(backend)) {
52194
+ const path10 = unitPath(kind, name, backend, unitDir);
52129
52195
  if (!(0, import_node_fs5.existsSync)(path10)) return false;
52130
52196
  (0, import_node_fs5.rmSync)(path10, { force: true });
52131
52197
  return true;
52132
52198
  }
52133
- function listRegisteredHubs(backend, unitDir = defaultUnitDir(backend)) {
52199
+ function listRegisteredUnits(kind, backend, unitDir = defaultUnitDir(backend)) {
52134
52200
  let files;
52135
52201
  try {
52136
52202
  files = (0, import_node_fs5.readdirSync)(unitDir);
@@ -52139,15 +52205,15 @@ function listRegisteredHubs(backend, unitDir = defaultUnitDir(backend)) {
52139
52205
  }
52140
52206
  const out = [];
52141
52207
  for (const file2 of files) {
52142
- const name = nameFromUnitFile(file2, backend);
52208
+ const name = nameFromUnitFile(kind, file2, backend);
52143
52209
  if (name) out.push({ name, unitPath: (0, import_node_path5.join)(unitDir, file2) });
52144
52210
  }
52145
52211
  return out.sort((a, b) => a.name.localeCompare(b.name));
52146
52212
  }
52147
- function readHubUnitRoot(name, backend, unitDir = defaultUnitDir(backend)) {
52213
+ function readUnitRoot(kind, name, backend, unitDir = defaultUnitDir(backend)) {
52148
52214
  let text;
52149
52215
  try {
52150
- text = (0, import_node_fs5.readFileSync)(hubUnitPath(name, backend, unitDir), "utf8");
52216
+ text = (0, import_node_fs5.readFileSync)(unitPath(kind, name, backend, unitDir), "utf8");
52151
52217
  } catch {
52152
52218
  return null;
52153
52219
  }
@@ -52156,75 +52222,55 @@ function readHubUnitRoot(name, backend, unitDir = defaultUnitDir(backend)) {
52156
52222
  if (!raw) return null;
52157
52223
  return backend === "launchd" ? raw.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&amp;/g, "&") : raw;
52158
52224
  }
52159
- function systemdUnitName(name) {
52160
- assertValidHubName(name);
52161
- return `${SYSTEMD_PREFIX}${name}.service`;
52225
+ function serviceLogNames(kind) {
52226
+ return { out: KINDS[kind].outLog, err: KINDS[kind].errLog };
52227
+ }
52228
+ function systemdUnitName(kind, name) {
52229
+ assertValidServiceName(kind, name);
52230
+ return `${KINDS[kind].systemdPrefix}${name}.service`;
52162
52231
  }
52163
- function launchdLabel(name) {
52164
- assertValidHubName(name);
52165
- return `${LABEL_PREFIX}${name}`;
52232
+ function launchdLabel(kind, name) {
52233
+ assertValidServiceName(kind, name);
52234
+ return `${KINDS[kind].labelPrefix}${name}`;
52166
52235
  }
52167
52236
 
52168
- // src/commands/hub.ts
52169
- var HUB_PACKAGE = "@slayzone/hub";
52170
- var CLI_VERSION = true ? "0.36.0-beta.10" : "";
52237
+ // src/service.ts
52238
+ var import_node_child_process2 = require("node:child_process");
52239
+ var import_node_fs6 = require("node:fs");
52240
+ var import_node_module = require("node:module");
52241
+ var import_node_path6 = require("node:path");
52242
+ var CLI_VERSION = true ? "0.36.0-beta.11" : "";
52171
52243
  function fail(message) {
52172
52244
  console.error(message);
52173
52245
  process.exit(1);
52174
52246
  }
52175
- function outOfBlockPorts() {
52176
- if (!hasLocalDatabase()) return [];
52177
- try {
52178
- const port = getServerPort();
52179
- return port ? [port] : [];
52180
- } catch {
52181
- return [];
52182
- }
52183
- }
52184
- function discoverAllHubs() {
52185
- return discoverHubs({ extraPorts: outOfBlockPorts() });
52186
- }
52187
- function findAnyHub(nameOrPort) {
52188
- return findHub(nameOrPort, { extraPorts: outOfBlockPorts() });
52189
- }
52190
- function formatUptime(ms) {
52191
- const s = Math.floor(ms / 1e3);
52192
- if (s < 60) return `${s}s`;
52193
- const m = Math.floor(s / 60);
52194
- if (m < 60) return `${m}m`;
52195
- const h = Math.floor(m / 60);
52196
- if (h < 24) return `${h}h ${m % 60}m`;
52197
- return `${Math.floor(h / 24)}d ${h % 24}h`;
52198
- }
52199
52247
  function shortenPath(path10) {
52200
52248
  const home = process.env.HOME ?? "";
52201
52249
  return home && path10.startsWith(home) ? `~${path10.slice(home.length)}` : path10;
52202
52250
  }
52203
- async function requireHub(nameOrPort) {
52204
- const hub = await findAnyHub(nameOrPort);
52205
- if (!hub) {
52206
- const running = await discoverAllHubs();
52207
- const hint = running.length > 0 ? `Running hubs: ${running.map((h) => `${h.name} (${h.port})`).join(", ")}` : "No hubs are running on this machine.";
52208
- fail(`No hub named or listening on "${nameOrPort}".
52209
- ${hint}`);
52210
- }
52211
- return hub;
52251
+ function resolveBackend() {
52252
+ return process.env.SLZ_FORCE_NO_SERVICE === "1" ? "none" : detectBackend();
52212
52253
  }
52213
- function refuseSupervised(hub, verb) {
52214
- if (hub.supervised) {
52215
- fail(
52216
- `Refusing to ${verb} "${hub.name}" \u2014 it is the hub inside the SlayZone desktop app, which manages its own lifecycle. Quit the app instead.`
52217
- );
52254
+ var PACKAGES = {
52255
+ hub: { pkg: "@slayzone/hub", bin: "slayzone-hub", devDir: "hub", envOverride: "SLZ_HUB_BIN" },
52256
+ runner: {
52257
+ pkg: "@slayzone/runner",
52258
+ bin: "slayzone-runner",
52259
+ devDir: "runner",
52260
+ envOverride: "SLZ_RUNNER_BIN"
52218
52261
  }
52262
+ };
52263
+ function servicePackage(kind) {
52264
+ return PACKAGES[kind].pkg;
52219
52265
  }
52220
- function readHubPkg(pkgJsonPath) {
52266
+ function readServicePkg(kind, pkgJsonPath) {
52221
52267
  let pkg;
52222
52268
  try {
52223
52269
  pkg = JSON.parse((0, import_node_fs6.readFileSync)(pkgJsonPath, "utf8"));
52224
52270
  } catch {
52225
52271
  return null;
52226
52272
  }
52227
- const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["slayzone-hub"];
52273
+ const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.[PACKAGES[kind].bin];
52228
52274
  if (!binRel) return null;
52229
52275
  const binPath = (0, import_node_path6.join)((0, import_node_path6.dirname)(pkgJsonPath), binRel);
52230
52276
  if (!(0, import_node_fs6.existsSync)(binPath)) return null;
@@ -52256,8 +52302,9 @@ function interpreterFor(binPath, version2) {
52256
52302
  }
52257
52303
  return { command: process.execPath, args: [binPath], version: version2 };
52258
52304
  }
52259
- function resolveHubBin() {
52260
- const override = process.env.SLZ_HUB_BIN;
52305
+ function resolveServiceBin(kind) {
52306
+ const { pkg, devDir, envOverride } = PACKAGES[kind];
52307
+ const override = process.env[envOverride];
52261
52308
  if (override) return { command: process.execPath, args: [override], version: "override" };
52262
52309
  const candidates = [
52263
52310
  // (1) Anchored on argv[1] (the slay script) — `import.meta` does not exist in
@@ -52265,30 +52312,30 @@ function resolveHubBin() {
52265
52312
  () => {
52266
52313
  try {
52267
52314
  return (0, import_node_module.createRequire)(process.argv[1] ?? (0, import_node_path6.join)(process.cwd(), "x")).resolve(
52268
- `${HUB_PACKAGE}/package.json`
52315
+ `${pkg}/package.json`
52269
52316
  );
52270
52317
  } catch {
52271
52318
  return null;
52272
52319
  }
52273
52320
  },
52274
- // (2) Dev tree: <cli>/dist/slay.js → ../../hub/package.json
52321
+ // (2) Dev tree: <cli>/dist/slay.js → ../../<kind>/package.json
52275
52322
  () => {
52276
52323
  const self2 = process.argv[1];
52277
52324
  if (!self2) return null;
52278
- const sibling = (0, import_node_path6.join)((0, import_node_path6.dirname)(self2), "..", "..", "hub", "package.json");
52325
+ const sibling = (0, import_node_path6.join)((0, import_node_path6.dirname)(self2), "..", "..", devDir, "package.json");
52279
52326
  return (0, import_node_fs6.existsSync)(sibling) ? sibling : null;
52280
52327
  }
52281
52328
  ];
52282
52329
  for (const find of candidates) {
52283
52330
  const path10 = find();
52284
52331
  if (!path10) continue;
52285
- const found2 = readHubPkg(path10);
52332
+ const found2 = readServicePkg(kind, path10);
52286
52333
  if (found2) return interpreterFor(found2.binPath, found2.version);
52287
52334
  }
52288
- const prefix = (0, import_node_path6.join)(getDataDir(), "hub-runtime");
52289
- const installed = (0, import_node_path6.join)(prefix, "node_modules", HUB_PACKAGE, "package.json");
52290
- const wanted = CLI_VERSION ? `${HUB_PACKAGE}@${CLI_VERSION}` : `${HUB_PACKAGE}@beta`;
52291
- if (!(0, import_node_fs6.existsSync)(installed) || CLI_VERSION && readHubPkg(installed)?.version !== CLI_VERSION) {
52335
+ const prefix = (0, import_node_path6.join)(getDataDir(), `${kind}-runtime`);
52336
+ const installed = (0, import_node_path6.join)(prefix, "node_modules", pkg, "package.json");
52337
+ const wanted = CLI_VERSION ? `${pkg}@${CLI_VERSION}` : `${pkg}@beta`;
52338
+ if (!(0, import_node_fs6.existsSync)(installed) || CLI_VERSION && readServicePkg(kind, installed)?.version !== CLI_VERSION) {
52292
52339
  console.log(`Installing ${wanted}\u2026`);
52293
52340
  (0, import_node_fs6.mkdirSync)(prefix, { recursive: true });
52294
52341
  try {
@@ -52304,20 +52351,232 @@ function resolveHubBin() {
52304
52351
  );
52305
52352
  }
52306
52353
  }
52307
- const found = readHubPkg(installed);
52354
+ const found = readServicePkg(kind, installed);
52308
52355
  if (!found) {
52309
52356
  fail(
52310
- `Installed ${HUB_PACKAGE} but found no usable bin at ${installed}. Install it globally and retry:
52311
- npm install -g ${HUB_PACKAGE}`
52357
+ `Installed ${pkg} but found no usable bin at ${installed}. Install it globally and retry:
52358
+ npm install -g ${pkg}`
52312
52359
  );
52313
52360
  }
52314
52361
  return interpreterFor(found.binPath, found.version);
52315
52362
  }
52316
- function ensureLogDir(root) {
52317
- const dir = (0, import_node_path6.join)(root, "storage", "logs");
52363
+ function ensureLogDir(kind, root) {
52364
+ const dir = logDirFor(kind, root);
52318
52365
  (0, import_node_fs6.mkdirSync)(dir, { recursive: true });
52319
52366
  return dir;
52320
52367
  }
52368
+ function logDirFor(kind, root) {
52369
+ return kind === "hub" ? (0, import_node_path6.join)(root, "storage", "logs") : (0, import_node_path6.join)(root, "logs");
52370
+ }
52371
+ function serviceLogPaths(kind, root) {
52372
+ const dir = logDirFor(kind, root);
52373
+ const names = serviceLogNames(kind);
52374
+ return { out: (0, import_node_path6.join)(dir, names.out), err: (0, import_node_path6.join)(dir, names.err) };
52375
+ }
52376
+ function failWithLog(kind, name, logPath, what) {
52377
+ let detail = "";
52378
+ try {
52379
+ detail = (0, import_node_fs6.readFileSync)(logPath, "utf8").trim().split("\n").slice(-12).join("\n");
52380
+ } catch {
52381
+ }
52382
+ const label = kind === "hub" ? "Hub" : "Runner";
52383
+ fail(
52384
+ `${label} "${name}" ${what}.
52385
+ ` + (detail ? `
52386
+ Last output (${shortenPath(logPath)}):
52387
+ ${detail}
52388
+ ` : `
52389
+ No output was captured in ${shortenPath(logPath)}.
52390
+ `)
52391
+ );
52392
+ }
52393
+ function readServiceLogTail(kind, root, lines) {
52394
+ const dir = logDirFor(kind, root);
52395
+ let files;
52396
+ try {
52397
+ files = (0, import_node_fs6.readdirSync)(dir).filter((f) => f.endsWith(".log"));
52398
+ } catch {
52399
+ return "";
52400
+ }
52401
+ const chunks = [];
52402
+ for (const f of files) {
52403
+ try {
52404
+ chunks.push((0, import_node_fs6.readFileSync)((0, import_node_path6.join)(dir, f), "utf8"));
52405
+ } catch {
52406
+ }
52407
+ }
52408
+ return chunks.join("\n").trim().split("\n").slice(-lines).join("\n");
52409
+ }
52410
+ function followServiceLog(args) {
52411
+ const { kind, name, root, lines, follow, systemdUnit } = args;
52412
+ if (systemdUnit) {
52413
+ const jargs = ["--user", "-u", systemdUnitName(kind, name), "-n", String(lines)];
52414
+ if (follow) jargs.push("-f");
52415
+ const child2 = (0, import_node_child_process2.spawn)("journalctl", jargs, { stdio: "inherit" });
52416
+ child2.on("exit", (code) => process.exit(code ?? 0));
52417
+ return;
52418
+ }
52419
+ const dir = logDirFor(kind, root);
52420
+ if (!(0, import_node_fs6.existsSync)(dir)) fail(`No log directory at ${shortenPath(dir)}.`);
52421
+ const candidates = (0, import_node_fs6.readdirSync)(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, path: (0, import_node_path6.join)(dir, f), mtime: (0, import_node_fs6.statSync)((0, import_node_path6.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
52422
+ if (candidates.length === 0) fail(`No log files in ${shortenPath(dir)}.`);
52423
+ const target = candidates[0].path;
52424
+ const targs = follow ? ["-f", "-n", String(lines), target] : ["-n", String(lines), target];
52425
+ const child = (0, import_node_child_process2.spawn)("tail", targs, { stdio: "inherit" });
52426
+ child.on("exit", (code) => process.exit(code ?? 0));
52427
+ }
52428
+ var SupervisorError = class extends Error {
52429
+ constructor(command, output) {
52430
+ super(`${command} failed`);
52431
+ this.command = command;
52432
+ this.output = output;
52433
+ }
52434
+ };
52435
+ function run(file2, args) {
52436
+ try {
52437
+ (0, import_node_child_process2.execFileSync)(file2, args, { stdio: ["ignore", "inherit", "pipe"] });
52438
+ } catch (e) {
52439
+ const stderr = e.stderr;
52440
+ throw new SupervisorError(
52441
+ `${file2} ${args.join(" ")}`,
52442
+ (typeof stderr === "string" ? stderr : stderr?.toString() ?? "").trim()
52443
+ );
52444
+ }
52445
+ }
52446
+ function launchdDomain() {
52447
+ return `gui/${process.getuid?.() ?? ""}`;
52448
+ }
52449
+ function supervisorStart(kind, backend, name, unitFilePath) {
52450
+ if (backend === "launchd") {
52451
+ const label = launchdLabel(kind, name);
52452
+ const domain2 = launchdDomain();
52453
+ try {
52454
+ (0, import_node_child_process2.execFileSync)("launchctl", ["bootout", `${domain2}/${label}`], { stdio: "ignore" });
52455
+ } catch {
52456
+ }
52457
+ run("launchctl", ["bootstrap", domain2, unitFilePath]);
52458
+ return;
52459
+ }
52460
+ const unit = systemdUnitName(kind, name);
52461
+ run("systemctl", ["--user", "daemon-reload"]);
52462
+ run("systemctl", ["--user", "enable", "--now", unit]);
52463
+ try {
52464
+ (0, import_node_child_process2.execFileSync)("loginctl", ["enable-linger", process.env.USER ?? ""], { stdio: "ignore" });
52465
+ } catch {
52466
+ console.log(
52467
+ `Note: could not enable systemd lingering \u2014 the ${kind} will stop when you log out.`
52468
+ );
52469
+ console.log(` Fix with: sudo loginctl enable-linger ${process.env.USER ?? "<user>"}`);
52470
+ }
52471
+ }
52472
+ function supervisorStop(kind, backend, name) {
52473
+ if (backend === "launchd") {
52474
+ try {
52475
+ (0, import_node_child_process2.execFileSync)("launchctl", ["bootout", `${launchdDomain()}/${launchdLabel(kind, name)}`], {
52476
+ stdio: "ignore"
52477
+ });
52478
+ } catch {
52479
+ }
52480
+ return;
52481
+ }
52482
+ try {
52483
+ (0, import_node_child_process2.execFileSync)("systemctl", ["--user", "disable", "--now", systemdUnitName(kind, name)], {
52484
+ stdio: "ignore"
52485
+ });
52486
+ } catch {
52487
+ }
52488
+ }
52489
+ function supervisorStopQuiet(kind, backend, name) {
52490
+ try {
52491
+ supervisorStop(kind, backend, name);
52492
+ } catch {
52493
+ }
52494
+ }
52495
+ function supervisorStatus(kind, backend, name) {
52496
+ const none = { running: false, pid: null, lastExit: null };
52497
+ if (backend === "none") return none;
52498
+ if (backend === "launchd") {
52499
+ let out2;
52500
+ try {
52501
+ out2 = (0, import_node_child_process2.execFileSync)("launchctl", ["list", launchdLabel(kind, name)], {
52502
+ encoding: "utf8",
52503
+ stdio: ["ignore", "pipe", "ignore"]
52504
+ });
52505
+ } catch {
52506
+ return none;
52507
+ }
52508
+ const pid = /"PID"\s*=\s*(\d+)/.exec(out2)?.[1];
52509
+ const exit2 = /"LastExitStatus"\s*=\s*(-?\d+)/.exec(out2)?.[1];
52510
+ return {
52511
+ running: pid !== void 0,
52512
+ pid: pid === void 0 ? null : Number(pid),
52513
+ lastExit: exit2 === void 0 ? null : Number(exit2)
52514
+ };
52515
+ }
52516
+ let out;
52517
+ try {
52518
+ out = (0, import_node_child_process2.execFileSync)(
52519
+ "systemctl",
52520
+ ["--user", "show", systemdUnitName(kind, name), "-p", "ActiveState", "-p", "MainPID", "-p", "ExecMainStatus"],
52521
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
52522
+ );
52523
+ } catch {
52524
+ return none;
52525
+ }
52526
+ const field = (key) => new RegExp(`^${key}=(.*)$`, "m").exec(out)?.[1]?.trim();
52527
+ const mainPid = Number(field("MainPID") ?? "0");
52528
+ const exit = field("ExecMainStatus");
52529
+ return {
52530
+ running: field("ActiveState") === "active" && mainPid > 0,
52531
+ // MainPID is 0 for an inactive unit — report absence, not pid 0.
52532
+ pid: mainPid > 0 ? mainPid : null,
52533
+ lastExit: exit === void 0 || exit === "" ? null : Number(exit)
52534
+ };
52535
+ }
52536
+
52537
+ // src/commands/hub.ts
52538
+ var HUB_PACKAGE = servicePackage("hub");
52539
+ function outOfBlockPorts() {
52540
+ if (!hasLocalDatabase()) return [];
52541
+ try {
52542
+ const port = getServerPort();
52543
+ return port ? [port] : [];
52544
+ } catch {
52545
+ return [];
52546
+ }
52547
+ }
52548
+ function discoverAllHubs() {
52549
+ return discoverHubs({ extraPorts: outOfBlockPorts() });
52550
+ }
52551
+ function findAnyHub(nameOrPort) {
52552
+ return findHub(nameOrPort, { extraPorts: outOfBlockPorts() });
52553
+ }
52554
+ function formatUptime(ms) {
52555
+ const s = Math.floor(ms / 1e3);
52556
+ if (s < 60) return `${s}s`;
52557
+ const m = Math.floor(s / 60);
52558
+ if (m < 60) return `${m}m`;
52559
+ const h = Math.floor(m / 60);
52560
+ if (h < 24) return `${h}h ${m % 60}m`;
52561
+ return `${Math.floor(h / 24)}d ${h % 24}h`;
52562
+ }
52563
+ async function requireHub(nameOrPort) {
52564
+ const hub = await findAnyHub(nameOrPort);
52565
+ if (!hub) {
52566
+ const running = await discoverAllHubs();
52567
+ const hint = running.length > 0 ? `Running hubs: ${running.map((h) => `${h.name} (${h.port})`).join(", ")}` : "No hubs are running on this machine.";
52568
+ fail(`No hub named or listening on "${nameOrPort}".
52569
+ ${hint}`);
52570
+ }
52571
+ return hub;
52572
+ }
52573
+ function refuseSupervised(hub, verb) {
52574
+ if (hub.supervised) {
52575
+ fail(
52576
+ `Refusing to ${verb} "${hub.name}" \u2014 it is the hub inside the SlayZone desktop app, which manages its own lifecycle. Quit the app instead.`
52577
+ );
52578
+ }
52579
+ }
52321
52580
  async function waitForHub(predicate, timeoutMs) {
52322
52581
  const deadline = Date.now() + timeoutMs;
52323
52582
  for (; ; ) {
@@ -52335,17 +52594,15 @@ async function waitForHubGone(port, timeoutMs) {
52335
52594
  await new Promise((r) => setTimeout(r, 200));
52336
52595
  }
52337
52596
  }
52338
- function resolveBackend() {
52339
- return process.env.SLZ_FORCE_NO_SERVICE === "1" ? "none" : detectBackend();
52340
- }
52341
52597
  async function launchHub(args) {
52342
52598
  const { name, root, port, backend, creating } = args;
52343
- const bin = resolveHubBin();
52344
- const logDir = ensureLogDir(root);
52599
+ const bin = resolveServiceBin("hub");
52600
+ const logDir = ensureLogDir("hub", root);
52601
+ const logs = serviceLogPaths("hub", root);
52345
52602
  const findIt = () => port === void 0 ? findAnyHub(name) : findHub(String(port));
52346
52603
  if (backend === "none") {
52347
- const out = (0, import_node_fs6.openSync)((0, import_node_path6.join)(logDir, "hub.out.log"), "a");
52348
- const child = (0, import_node_child_process2.spawn)(bin.command, bin.args, {
52604
+ const out = (0, import_node_fs7.openSync)(logs.out, "a");
52605
+ const child = (0, import_node_child_process3.spawn)(bin.command, bin.args, {
52349
52606
  cwd: root,
52350
52607
  detached: true,
52351
52608
  stdio: ["ignore", out, out],
@@ -52361,7 +52618,7 @@ async function launchHub(args) {
52361
52618
  });
52362
52619
  child.unref();
52363
52620
  const started2 = await waitForHub(findIt, 2e4);
52364
- if (!started2) failWithLog(name, (0, import_node_path6.join)(logDir, "hub.out.log"), "did not come up within 20s");
52621
+ if (!started2) failWithLog("hub", name, logs.out, "did not come up within 20s");
52365
52622
  console.log(`Hub "${started2.name}" started on port ${started2.port} (pid ${started2.pid}).`);
52366
52623
  console.log(` Root: ${shortenPath(root)}`);
52367
52624
  console.log(` Logs: ${shortenPath(logDir)}`);
@@ -52370,8 +52627,9 @@ async function launchHub(args) {
52370
52627
  );
52371
52628
  return;
52372
52629
  }
52373
- const unitPath = writeHubUnit(
52630
+ const writtenUnitPath = writeUnit(
52374
52631
  {
52632
+ kind: "hub",
52375
52633
  name,
52376
52634
  root,
52377
52635
  command: bin.command,
@@ -52382,12 +52640,12 @@ async function launchHub(args) {
52382
52640
  },
52383
52641
  backend
52384
52642
  );
52385
- if (creating) console.log(`Registered ${unitPath}`);
52643
+ if (creating) console.log(`Registered ${writtenUnitPath}`);
52386
52644
  console.log(`Starting hub "${name}" (${HUB_PACKAGE}@${bin.version})\u2026`);
52387
52645
  try {
52388
- supervisorStart(backend, name, unitPath);
52646
+ supervisorStart("hub", backend, name, writtenUnitPath);
52389
52647
  } catch (e) {
52390
- if (creating) removeHubUnit(name, backend);
52648
+ if (creating) removeUnit("hub", name, backend);
52391
52649
  if (!(e instanceof SupervisorError)) throw e;
52392
52650
  const noBus = /Failed to connect to bus|No medium found/i.test(e.output);
52393
52651
  fail(
@@ -52406,28 +52664,29 @@ then log out and back in, and retry. If this account is not meant to have one (a
52406
52664
  const started = await waitForHub(findIt, 2e4);
52407
52665
  if (!started) {
52408
52666
  if (creating) {
52409
- supervisorStop(backend, name);
52410
- removeHubUnit(name, backend);
52667
+ supervisorStop("hub", backend, name);
52668
+ removeUnit("hub", name, backend);
52411
52669
  } else {
52412
- supervisorStop(backend, name);
52670
+ supervisorStop("hub", backend, name);
52413
52671
  }
52414
52672
  failWithLog(
52673
+ "hub",
52415
52674
  name,
52416
- (0, import_node_path6.join)(logDir, "hub.err.log"),
52675
+ logs.err,
52417
52676
  creating ? "failed to start, so it was unregistered again" : "failed to start (its registration was left in place)"
52418
52677
  );
52419
52678
  }
52420
52679
  console.log(`Hub "${started.name}" running on port ${started.port} (pid ${started.pid}).`);
52421
52680
  console.log(` Root: ${shortenPath(root)}`);
52422
52681
  console.log(` Logs: ${shortenPath(logDir)}`);
52423
- console.log(` Unit: ${unitPath}`);
52682
+ console.log(` Unit: ${writtenUnitPath}`);
52424
52683
  console.log(` Hub: ${HUB_PACKAGE}@${bin.version}`);
52425
52684
  console.log("Restarts automatically if it crashes, and starts again when you log in.");
52426
52685
  }
52427
52686
  async function haltHub(hub, backend, opts) {
52428
- if (backend !== "none" && (0, import_node_fs6.existsSync)(hubUnitPath(hub.name, backend))) {
52429
- supervisorStop(backend, hub.name);
52430
- if (opts.unregister) removeHubUnit(hub.name, backend);
52687
+ if (backend !== "none" && (0, import_node_fs7.existsSync)(unitPath("hub", hub.name, backend))) {
52688
+ supervisorStop("hub", backend, hub.name);
52689
+ if (opts.unregister) removeUnit("hub", hub.name, backend);
52431
52690
  } else {
52432
52691
  try {
52433
52692
  process.kill(hub.pid, "SIGTERM");
@@ -52442,83 +52701,6 @@ async function haltHub(hub, backend, opts) {
52442
52701
  );
52443
52702
  }
52444
52703
  }
52445
- var SupervisorError = class extends Error {
52446
- constructor(command, output) {
52447
- super(`${command} failed`);
52448
- this.command = command;
52449
- this.output = output;
52450
- }
52451
- };
52452
- function run(file2, args) {
52453
- try {
52454
- (0, import_node_child_process2.execFileSync)(file2, args, { stdio: ["ignore", "inherit", "pipe"] });
52455
- } catch (e) {
52456
- const stderr = e.stderr;
52457
- throw new SupervisorError(
52458
- `${file2} ${args.join(" ")}`,
52459
- (typeof stderr === "string" ? stderr : stderr?.toString() ?? "").trim()
52460
- );
52461
- }
52462
- }
52463
- function failWithLog(name, logPath, what) {
52464
- let detail = "";
52465
- try {
52466
- detail = (0, import_node_fs6.readFileSync)(logPath, "utf8").trim().split("\n").slice(-12).join("\n");
52467
- } catch {
52468
- }
52469
- fail(
52470
- `Hub "${name}" ${what}.
52471
- ` + (detail ? `
52472
- Last output (${shortenPath(logPath)}):
52473
- ${detail}
52474
- ` : `
52475
- No output was captured in ${shortenPath(logPath)}.
52476
- `)
52477
- );
52478
- }
52479
- function supervisorStart(backend, name, unitPath) {
52480
- if (backend === "launchd") {
52481
- const label = launchdLabel(name);
52482
- const domain2 = `gui/${process.getuid?.() ?? ""}`;
52483
- try {
52484
- (0, import_node_child_process2.execFileSync)("launchctl", ["bootout", `${domain2}/${label}`], { stdio: "ignore" });
52485
- } catch {
52486
- }
52487
- run("launchctl", ["bootstrap", domain2, unitPath]);
52488
- return;
52489
- }
52490
- const unit = systemdUnitName(name);
52491
- run("systemctl", ["--user", "daemon-reload"]);
52492
- run("systemctl", ["--user", "enable", "--now", unit]);
52493
- try {
52494
- (0, import_node_child_process2.execFileSync)("loginctl", ["enable-linger", process.env.USER ?? ""], { stdio: "ignore" });
52495
- } catch {
52496
- console.log("Note: could not enable systemd lingering \u2014 the hub will stop when you log out.");
52497
- console.log(` Fix with: sudo loginctl enable-linger ${process.env.USER ?? "<user>"}`);
52498
- }
52499
- }
52500
- function supervisorStopQuiet(backend, name) {
52501
- try {
52502
- supervisorStop(backend, name);
52503
- } catch {
52504
- }
52505
- }
52506
- function supervisorStop(backend, name) {
52507
- if (backend === "launchd") {
52508
- const domain2 = `gui/${process.getuid?.() ?? ""}`;
52509
- try {
52510
- (0, import_node_child_process2.execFileSync)("launchctl", ["bootout", `${domain2}/${launchdLabel(name)}`], { stdio: "ignore" });
52511
- } catch {
52512
- }
52513
- return;
52514
- }
52515
- try {
52516
- (0, import_node_child_process2.execFileSync)("systemctl", ["--user", "disable", "--now", systemdUnitName(name)], {
52517
- stdio: "ignore"
52518
- });
52519
- } catch {
52520
- }
52521
- }
52522
52704
  function hubCommand() {
52523
52705
  const cmd = new Command("hub").description("Run, list and target SlayZone hubs").showSuggestionAfterError(true).showHelpAfterError(true);
52524
52706
  cmd.command("ls").description("List the hubs running on this machine").option("--json", "Output as JSON").action(async (opts) => {
@@ -52550,14 +52732,14 @@ function hubCommand() {
52550
52732
  }
52551
52733
  });
52552
52734
  cmd.command("create <name>").description("Create a hub here and keep it running (crash-restart + start at login)").option("--root <dir>", "Hub root \u2014 its config, storage and logs (default: cwd)").option("--port <port>", "Bind a specific port (default: first free hub port)").action(async (name, opts) => {
52553
- const root = (0, import_node_path6.resolve)(opts.root ?? process.cwd());
52735
+ const root = (0, import_node_path7.resolve)(opts.root ?? process.cwd());
52554
52736
  const port = opts.port === void 0 ? void 0 : Number(opts.port);
52555
52737
  if (port !== void 0 && (!Number.isInteger(port) || port < 1 || port > 65535)) {
52556
52738
  fail(`Invalid --port ${opts.port} \u2014 expected an integer 1-65535.`);
52557
52739
  }
52558
52740
  const backend = resolveBackend();
52559
- if (backend !== "none" && (0, import_node_fs6.existsSync)(hubUnitPath(name, backend))) {
52560
- const existingRoot = readHubUnitRoot(name, backend);
52741
+ if (backend !== "none" && (0, import_node_fs7.existsSync)(unitPath("hub", name, backend))) {
52742
+ const existingRoot = readUnitRoot("hub", name, backend);
52561
52743
  fail(
52562
52744
  `A hub named "${name}" already exists${existingRoot ? ` (root ${shortenPath(existingRoot)})` : ""}.
52563
52745
  Start it with \`slay hub start ${name}\`, or remove it with \`slay hub rm ${name}\`.`
@@ -52592,12 +52774,12 @@ Start it with \`slay hub start ${name}\`, or remove it with \`slay hub rm ${name
52592
52774
  `This platform has no user service manager, so hubs are not registered and \`start\` has nothing to act on. Use \`slay hub create ${name}\`.`
52593
52775
  );
52594
52776
  }
52595
- if (!(0, import_node_fs6.existsSync)(hubUnitPath(name, backend))) {
52777
+ if (!(0, import_node_fs7.existsSync)(unitPath("hub", name, backend))) {
52596
52778
  fail(
52597
52779
  `No hub named "${name}". Create it with \`slay hub create ${name}\`, or list what exists with \`slay hub registered\`.`
52598
52780
  );
52599
52781
  }
52600
- const root = readHubUnitRoot(name, backend);
52782
+ const root = readUnitRoot("hub", name, backend);
52601
52783
  if (!root) {
52602
52784
  fail(
52603
52785
  `The unit for "${name}" does not record a root \u2014 it may be hand-edited. Remove it with \`slay hub rm ${name}\` and create it again.`
@@ -52609,7 +52791,7 @@ Start it with \`slay hub start ${name}\`, or remove it with \`slay hub rm ${name
52609
52791
  const hub = await requireHub(nameOrPort);
52610
52792
  refuseSupervised(hub, "stop");
52611
52793
  const backend = resolveBackend();
52612
- const registered = backend !== "none" && (0, import_node_fs6.existsSync)(hubUnitPath(hub.name, backend));
52794
+ const registered = backend !== "none" && (0, import_node_fs7.existsSync)(unitPath("hub", hub.name, backend));
52613
52795
  await haltHub(hub, backend, { unregister: false });
52614
52796
  console.log(
52615
52797
  registered ? `Stopped "${hub.name}". Start it again with \`slay hub start ${hub.name}\`.` : `Stopped "${hub.name}" (pid ${hub.pid}). It was not registered by slay.`
@@ -52619,10 +52801,10 @@ Start it with \`slay hub start ${name}\`, or remove it with \`slay hub rm ${name
52619
52801
  const backend = resolveBackend();
52620
52802
  const hub = await findAnyHub(nameOrPort);
52621
52803
  if (!hub) {
52622
- if (backend !== "none" && (0, import_node_fs6.existsSync)(hubUnitPath(nameOrPort, backend))) {
52623
- const root = readHubUnitRoot(nameOrPort, backend);
52624
- supervisorStopQuiet(backend, nameOrPort);
52625
- removeHubUnit(nameOrPort, backend);
52804
+ if (backend !== "none" && (0, import_node_fs7.existsSync)(unitPath("hub", nameOrPort, backend))) {
52805
+ const root = readUnitRoot("hub", nameOrPort, backend);
52806
+ supervisorStopQuiet("hub", backend, nameOrPort);
52807
+ removeUnit("hub", nameOrPort, backend);
52626
52808
  console.log(
52627
52809
  `Removed "${nameOrPort}" (it was not running).` + (root ? ` Its data is still in ${shortenPath(root)}.` : "")
52628
52810
  );
@@ -52639,17 +52821,18 @@ Start it with \`slay hub start ${name}\`, or remove it with \`slay hub rm ${name
52639
52821
  const hub = await requireHub(nameOrPort);
52640
52822
  refuseSupervised(hub, "restart");
52641
52823
  const backend = resolveBackend();
52642
- if (backend === "none" || !(0, import_node_fs6.existsSync)(hubUnitPath(hub.name, backend))) {
52824
+ if (backend === "none" || !(0, import_node_fs7.existsSync)(unitPath("hub", hub.name, backend))) {
52643
52825
  fail(
52644
52826
  `"${hub.name}" is not managed by slay, so it cannot be restarted from here. Stop it where it was started, then \`slay hub create ${hub.name}\`.`
52645
52827
  );
52646
52828
  }
52647
52829
  const { name, root } = hub;
52648
- const logDir = ensureLogDir(root);
52830
+ const logDir = ensureLogDir("hub", root);
52649
52831
  if (opts.upgrade) {
52650
- const bin = resolveHubBin();
52651
- writeHubUnit(
52832
+ const bin = resolveServiceBin("hub");
52833
+ writeUnit(
52652
52834
  {
52835
+ kind: "hub",
52653
52836
  name,
52654
52837
  root,
52655
52838
  command: bin.command,
@@ -52663,9 +52846,9 @@ Start it with \`slay hub start ${name}\`, or remove it with \`slay hub rm ${name
52663
52846
  );
52664
52847
  console.log(`Unit updated to ${HUB_PACKAGE}@${bin.version}.`);
52665
52848
  }
52666
- supervisorStop(backend, name);
52849
+ supervisorStop("hub", backend, name);
52667
52850
  await waitForHubGone(hub.port, 15e3);
52668
- supervisorStart(backend, name, hubUnitPath(name, backend));
52851
+ supervisorStart("hub", backend, name, unitPath("hub", name, backend));
52669
52852
  const back = await waitForHub(() => findHub(name), 2e4);
52670
52853
  if (!back) fail(`Hub "${name}" did not come back within 20s. Check ${shortenPath(logDir)}.`);
52671
52854
  console.log(`Hub "${back.name}" restarted on port ${back.port} (pid ${back.pid}).`);
@@ -52675,21 +52858,14 @@ Start it with \`slay hub start ${name}\`, or remove it with \`slay hub rm ${name
52675
52858
  const lines = Number(opts.lines);
52676
52859
  if (!Number.isInteger(lines) || lines < 1) fail(`Invalid --lines ${opts.lines}.`);
52677
52860
  const backend = detectBackend();
52678
- if (backend === "systemd" && (0, import_node_fs6.existsSync)(hubUnitPath(hub.name, backend))) {
52679
- const args2 = ["--user", "-u", systemdUnitName(hub.name), "-n", String(lines)];
52680
- if (opts.follow) args2.push("-f");
52681
- const child2 = (0, import_node_child_process2.spawn)("journalctl", args2, { stdio: "inherit" });
52682
- child2.on("exit", (code) => process.exit(code ?? 0));
52683
- return;
52684
- }
52685
- const logDir = (0, import_node_path6.join)(hub.root, "storage", "logs");
52686
- if (!(0, import_node_fs6.existsSync)(logDir)) fail(`No log directory at ${shortenPath(logDir)}.`);
52687
- const candidates = (0, import_node_fs6.readdirSync)(logDir).filter((f) => f.endsWith(".log")).map((f) => ({ f, path: (0, import_node_path6.join)(logDir, f), mtime: (0, import_node_fs6.statSync)((0, import_node_path6.join)(logDir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
52688
- if (candidates.length === 0) fail(`No log files in ${shortenPath(logDir)}.`);
52689
- const target = candidates[0].path;
52690
- const args = opts.follow ? ["-f", "-n", String(lines), target] : ["-n", String(lines), target];
52691
- const child = (0, import_node_child_process2.spawn)("tail", args, { stdio: "inherit" });
52692
- child.on("exit", (code) => process.exit(code ?? 0));
52861
+ followServiceLog({
52862
+ kind: "hub",
52863
+ name: hub.name,
52864
+ root: hub.root,
52865
+ lines,
52866
+ follow: opts.follow === true,
52867
+ systemdUnit: backend === "systemd" && (0, import_node_fs7.existsSync)(unitPath("hub", hub.name, backend))
52868
+ });
52693
52869
  });
52694
52870
  cmd.command("use <name|url>").description("Point this CLI at a hub \u2014 a name from `hub ls`, or a full URL").option("--token <token>", "Bearer token sent as Authorization header").action(async (target, opts) => {
52695
52871
  const looksLikeUrl = target.includes("://");
@@ -52742,7 +52918,7 @@ Start it with \`slay hub start ${name}\`, or remove it with \`slay hub rm ${name
52742
52918
  console.log("This platform has no user service manager \u2014 no hubs can be registered.");
52743
52919
  return;
52744
52920
  }
52745
- const units = listRegisteredHubs(backend);
52921
+ const units = listRegisteredUnits("hub", backend);
52746
52922
  if (units.length === 0) {
52747
52923
  console.log("No registered hubs.");
52748
52924
  return;
@@ -52752,7 +52928,7 @@ Start it with \`slay hub start ${name}\`, or remove it with \`slay hub rm ${name
52752
52928
  const running = live.has(u.name);
52753
52929
  console.log(`${u.name}${running ? " (running)" : " (stopped)"}`);
52754
52930
  console.log(` ${u.unitPath}`);
52755
- const root = readHubUnitRoot(u.name, backend);
52931
+ const root = readUnitRoot("hub", u.name, backend);
52756
52932
  if (root) console.log(` root: ${shortenPath(root)}`);
52757
52933
  if (!running) console.log(` start: slay hub start ${u.name}`);
52758
52934
  }
@@ -52760,13 +52936,461 @@ Start it with \`slay hub start ${name}\`, or remove it with \`slay hub rm ${name
52760
52936
  return cmd;
52761
52937
  }
52762
52938
 
52939
+ // src/commands/runner.ts
52940
+ var import_node_child_process4 = require("node:child_process");
52941
+ var import_node_fs9 = require("node:fs");
52942
+ var import_node_path9 = require("node:path");
52943
+
52944
+ // ../../shared/platform/src/join-token.ts
52945
+ var JOIN_TOKEN_PREFIX = "szjt1";
52946
+ function decodeJoinToken(token) {
52947
+ const dot = token.indexOf(".");
52948
+ if (dot === -1 || token.slice(0, dot) !== JOIN_TOKEN_PREFIX) return null;
52949
+ const body = token.slice(dot + 1);
52950
+ if (!body) return null;
52951
+ try {
52952
+ const parsed = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
52953
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
52954
+ const { hubUrl, certFingerprint, secret } = parsed;
52955
+ if (typeof hubUrl !== "string" || typeof certFingerprint !== "string" || typeof secret !== "string") {
52956
+ return null;
52957
+ }
52958
+ return { hubUrl, certFingerprint, secret };
52959
+ } catch {
52960
+ return null;
52961
+ }
52962
+ }
52963
+
52964
+ // ../../shared/platform/src/slayzone-config.ts
52965
+ var import_node_fs8 = require("node:fs");
52966
+ var import_node_path8 = require("node:path");
52967
+ var DEFAULT_LOCAL_RUNNER_NAME = "local-runner";
52968
+ function getSlayzoneConfigPath() {
52969
+ return (0, import_node_path8.join)(getSlayzoneHomeDir(), "config.json");
52970
+ }
52971
+ function coerce(raw) {
52972
+ const cfg = {};
52973
+ if (typeof raw.runnerTransportSecret === "string" && raw.runnerTransportSecret.length > 0)
52974
+ cfg.runnerTransportSecret = raw.runnerTransportSecret;
52975
+ if (typeof raw.address === "string" && raw.address.length > 0) cfg.address = raw.address;
52976
+ if (typeof raw.publicAddress === "string" && raw.publicAddress.length > 0)
52977
+ cfg.publicAddress = raw.publicAddress;
52978
+ if (typeof raw.hubName === "string" && raw.hubName.trim().length > 0)
52979
+ cfg.hubName = raw.hubName.trim();
52980
+ if (typeof raw.port === "number" && Number.isInteger(raw.port)) cfg.port = raw.port;
52981
+ if (typeof raw.publicUrl === "string" && raw.publicUrl.length > 0) cfg.publicUrl = raw.publicUrl;
52982
+ if (typeof raw.joinToken === "string" && raw.joinToken.length > 0) cfg.joinToken = raw.joinToken;
52983
+ if (typeof raw.runnerName === "string" && raw.runnerName.length > 0)
52984
+ cfg.runnerName = raw.runnerName;
52985
+ if (typeof raw.hubUrl === "string" && raw.hubUrl.length > 0) cfg.hubUrl = raw.hubUrl;
52986
+ if (Array.isArray(raw.allowedRoots)) {
52987
+ const roots = raw.allowedRoots.filter(
52988
+ (r) => typeof r === "string" && r.length > 0
52989
+ );
52990
+ if (roots.length > 0) cfg.allowedRoots = roots;
52991
+ }
52992
+ if (typeof raw.pinnedCertSha256 === "string" && raw.pinnedCertSha256.length > 0)
52993
+ cfg.pinnedCertSha256 = raw.pinnedCertSha256;
52994
+ return cfg;
52995
+ }
52996
+ function loadSlayzoneConfig(configPath = getSlayzoneConfigPath()) {
52997
+ let raw;
52998
+ try {
52999
+ raw = (0, import_node_fs8.readFileSync)(configPath, "utf8");
53000
+ } catch (err) {
53001
+ if (err.code === "ENOENT") return {};
53002
+ process.stderr.write(
53003
+ `[slayzone-config] cannot read ${configPath}: ${err instanceof Error ? err.message : String(err)} \u2014 using empty config
53004
+ `
53005
+ );
53006
+ return {};
53007
+ }
53008
+ let parsed;
53009
+ try {
53010
+ parsed = JSON.parse(raw);
53011
+ } catch (err) {
53012
+ process.stderr.write(
53013
+ `[slayzone-config] ${configPath} is not valid JSON: ${err instanceof Error ? err.message : String(err)} \u2014 using empty config
53014
+ `
53015
+ );
53016
+ return {};
53017
+ }
53018
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
53019
+ process.stderr.write(`[slayzone-config] ${configPath} is not a JSON object \u2014 using empty config
53020
+ `);
53021
+ return {};
53022
+ }
53023
+ return coerce(parsed);
53024
+ }
53025
+ function saveSlayzoneConfig(cfg, configPath = getSlayzoneConfigPath()) {
53026
+ const dir = (0, import_node_path8.dirname)(configPath);
53027
+ (0, import_node_fs8.mkdirSync)(dir, { recursive: true, mode: 448 });
53028
+ const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
53029
+ (0, import_node_fs8.writeFileSync)(tmpPath, `${JSON.stringify(cfg, null, 2)}
53030
+ `, { mode: 384 });
53031
+ try {
53032
+ (0, import_node_fs8.renameSync)(tmpPath, configPath);
53033
+ } catch (err) {
53034
+ (0, import_node_fs8.rmSync)(tmpPath, { force: true });
53035
+ throw err;
53036
+ }
53037
+ }
53038
+ function updateSlayzoneConfig(patch, configPath = getSlayzoneConfigPath()) {
53039
+ const current = loadSlayzoneConfig(configPath);
53040
+ const merged = { ...current };
53041
+ for (const [k, v2] of Object.entries(patch)) {
53042
+ if (v2 !== void 0) merged[k] = v2;
53043
+ }
53044
+ saveSlayzoneConfig(merged, configPath);
53045
+ return merged;
53046
+ }
53047
+
53048
+ // src/commands/runner.ts
53049
+ var RUNNER_PACKAGE = servicePackage("runner");
53050
+ var ENROLL_TIMEOUT_MS = 3e4;
53051
+ var CONNECTED_RE = /"mode":"(enroll|hello)"/;
53052
+ function configPathFor(root) {
53053
+ return (0, import_node_path9.join)(root, "config.json");
53054
+ }
53055
+ function isEnrolled(root) {
53056
+ try {
53057
+ return (0, import_node_fs9.readdirSync)((0, import_node_path9.join)(root, "runners")).some((f) => f.endsWith(".json"));
53058
+ } catch {
53059
+ return false;
53060
+ }
53061
+ }
53062
+ function requireRegistered(name, backend) {
53063
+ if (backend === "none") {
53064
+ fail(
53065
+ `This platform has no user service manager, so runners cannot be registered here. Run one in the foreground instead:
53066
+ npx ${RUNNER_PACKAGE}`
53067
+ );
53068
+ }
53069
+ if (!(0, import_node_fs9.existsSync)(unitPath("runner", name, backend))) {
53070
+ const known = listRegisteredUnits("runner", backend).map((u) => u.name);
53071
+ fail(
53072
+ `No runner named "${name}" on this machine.
53073
+ ` + (known.length > 0 ? `Registered runners: ${known.join(", ")}` : "No runners are registered. Create one with `slay runner create <name> --token <token>`.")
53074
+ );
53075
+ }
53076
+ const root = readUnitRoot("runner", name, backend);
53077
+ if (!root) {
53078
+ fail(
53079
+ `The unit for "${name}" does not record a root \u2014 it may be hand-edited. Remove it with \`slay runner rm ${name}\` and create it again.`
53080
+ );
53081
+ }
53082
+ return { root };
53083
+ }
53084
+ async function waitForConnected(root, timeoutMs) {
53085
+ const deadline = Date.now() + timeoutMs;
53086
+ for (; ; ) {
53087
+ if (CONNECTED_RE.test(readServiceLogTail("runner", root, 200))) return true;
53088
+ if (Date.now() >= deadline) return false;
53089
+ await new Promise((r) => setTimeout(r, 300));
53090
+ }
53091
+ }
53092
+ async function launchRunner(args) {
53093
+ const { name, root, backend, creating } = args;
53094
+ const bin = resolveServiceBin("runner");
53095
+ const logDir = ensureLogDir("runner", root);
53096
+ const logs = serviceLogPaths("runner", root);
53097
+ if (backend === "none") {
53098
+ const out = (0, import_node_fs9.openSync)(logs.out, "a");
53099
+ const child = (0, import_node_child_process4.spawn)(bin.command, bin.args, {
53100
+ cwd: root,
53101
+ detached: true,
53102
+ stdio: ["ignore", out, out],
53103
+ env: {
53104
+ ...process.env,
53105
+ SLAYZONE_ROOT: root,
53106
+ // A detached runner has no TTY, but be explicit: the first-run prompt must
53107
+ // never block a spawn nobody is watching.
53108
+ SLAYZONE_NONINTERACTIVE: "1",
53109
+ // Same interpreter requirement as the unit path: a dev-tree runner needs
53110
+ // ELECTRON_RUN_AS_NODE or its Electron-ABI natives fail to load.
53111
+ ...bin.env ?? {}
53112
+ }
53113
+ });
53114
+ child.unref();
53115
+ if (!await waitForConnected(root, ENROLL_TIMEOUT_MS)) {
53116
+ failWithLog("runner", name, logs.out, `did not reach its hub within ${ENROLL_TIMEOUT_MS / 1e3}s`);
53117
+ }
53118
+ console.log(`Runner "${name}" started (pid ${child.pid ?? "?"}) and reached its hub.`);
53119
+ console.log(` Root: ${shortenPath(root)}`);
53120
+ console.log(` Logs: ${shortenPath(logDir)}`);
53121
+ console.log(
53122
+ "Note: this platform has no user service manager, so the runner will NOT restart if it crashes, and will not come back after a reboot."
53123
+ );
53124
+ return;
53125
+ }
53126
+ const writtenUnitPath = writeUnit(
53127
+ {
53128
+ kind: "runner",
53129
+ name,
53130
+ root,
53131
+ command: bin.command,
53132
+ args: bin.args,
53133
+ logDir,
53134
+ ...bin.env ? { env: bin.env } : {}
53135
+ },
53136
+ backend
53137
+ );
53138
+ if (creating) console.log(`Registered ${writtenUnitPath}`);
53139
+ console.log(`Starting runner "${name}" (${RUNNER_PACKAGE}@${bin.version})\u2026`);
53140
+ try {
53141
+ supervisorStart("runner", backend, name, writtenUnitPath);
53142
+ } catch (e) {
53143
+ if (creating) removeUnit("runner", name, backend);
53144
+ if (!(e instanceof SupervisorError)) throw e;
53145
+ const noBus = /Failed to connect to bus|No medium found/i.test(e.output);
53146
+ fail(
53147
+ `Could not register runner "${name}" with ${backend}.
53148
+
53149
+ ${e.command}
53150
+ ${e.output || "(no output)"}
53151
+
53152
+ ` + (noBus ? `systemd has no user session bus for this account, so \`systemctl --user\` cannot work. Enable a persistent user manager:
53153
+ sudo loginctl enable-linger ${process.env.USER ?? "<user>"}
53154
+ then log out and back in, and retry. If this account is not meant to have one (a container, or a root-only box), run the runner under the system manager or in the foreground instead:
53155
+ npx ${RUNNER_PACKAGE}
53156
+ ` : "")
53157
+ );
53158
+ }
53159
+ if (!await waitForConnected(root, ENROLL_TIMEOUT_MS)) {
53160
+ if (creating) {
53161
+ supervisorStop("runner", backend, name);
53162
+ removeUnit("runner", name, backend);
53163
+ } else {
53164
+ supervisorStop("runner", backend, name);
53165
+ }
53166
+ failWithLog(
53167
+ "runner",
53168
+ name,
53169
+ logs.err,
53170
+ creating ? `did not reach its hub within ${ENROLL_TIMEOUT_MS / 1e3}s, so it was unregistered again` : `did not reach its hub within ${ENROLL_TIMEOUT_MS / 1e3}s (its registration was left in place)`
53171
+ );
53172
+ }
53173
+ const status = supervisorStatus("runner", backend, name);
53174
+ console.log(
53175
+ `Runner "${name}" running${status.pid ? ` (pid ${status.pid})` : ""} and connected to its hub.`
53176
+ );
53177
+ console.log(` Root: ${shortenPath(root)}`);
53178
+ console.log(` Logs: ${shortenPath(logDir)}`);
53179
+ console.log(` Unit: ${writtenUnitPath}`);
53180
+ console.log(` Runner: ${RUNNER_PACKAGE}@${bin.version}`);
53181
+ console.log("Restarts automatically if it crashes, and starts again when you log in.");
53182
+ }
53183
+ async function waitForStopped(name, backend, timeoutMs) {
53184
+ const deadline = Date.now() + timeoutMs;
53185
+ for (; ; ) {
53186
+ if (!supervisorStatus("runner", backend, name).running) return true;
53187
+ if (Date.now() >= deadline) return false;
53188
+ await new Promise((r) => setTimeout(r, 250));
53189
+ }
53190
+ }
53191
+ function runnerCommand() {
53192
+ const cmd = new Command("runner").description("Run and manage SlayZone runners on this machine").showSuggestionAfterError(true).showHelpAfterError(true);
53193
+ cmd.command("ls").description("List the runners installed on this machine").option("--json", "Output as JSON").action((opts) => {
53194
+ const backend = detectBackend();
53195
+ if (backend === "none") {
53196
+ console.log("This platform has no user service manager \u2014 no runners can be registered.");
53197
+ return;
53198
+ }
53199
+ const units = listRegisteredUnits("runner", backend);
53200
+ const rows = units.map((u) => {
53201
+ const root = readUnitRoot("runner", u.name, backend);
53202
+ const status = supervisorStatus("runner", backend, u.name);
53203
+ const hubUrl = root ? loadSlayzoneConfig(configPathFor(root)).hubUrl ?? null : null;
53204
+ return {
53205
+ name: u.name,
53206
+ unitPath: u.unitPath,
53207
+ root,
53208
+ running: status.running,
53209
+ pid: status.pid,
53210
+ lastExit: status.lastExit,
53211
+ hubUrl,
53212
+ enrolled: root ? isEnrolled(root) : false
53213
+ };
53214
+ });
53215
+ if (opts.json) {
53216
+ console.log(JSON.stringify(rows, null, 2));
53217
+ return;
53218
+ }
53219
+ if (rows.length === 0) {
53220
+ console.log(
53221
+ "No runners installed. Create one with `slay runner create <name> --token <token>`."
53222
+ );
53223
+ return;
53224
+ }
53225
+ const nameW = Math.max(4, ...rows.map((r) => r.name.length));
53226
+ const rootW = Math.max(4, ...rows.map((r) => r.root ? shortenPath(r.root).length : 1));
53227
+ const hubW = Math.max(3, ...rows.map((r) => (r.hubUrl ?? "-").length));
53228
+ console.log(
53229
+ `${"NAME".padEnd(nameW)} ${"STATE".padEnd(7)} ${"PID".padEnd(7)} ${"ROOT".padEnd(rootW)} ${"HUB".padEnd(hubW)} ENROLLED`
53230
+ );
53231
+ console.log(
53232
+ `${"-".repeat(nameW)} ${"-".repeat(7)} ${"-".repeat(7)} ${"-".repeat(rootW)} ${"-".repeat(hubW)} ${"-".repeat(8)}`
53233
+ );
53234
+ for (const r of rows) {
53235
+ console.log(
53236
+ `${r.name.padEnd(nameW)} ${(r.running ? "running" : "stopped").padEnd(7)} ${String(r.pid ?? "-").padEnd(7)} ${(r.root ? shortenPath(r.root) : "?").padEnd(rootW)} ${(r.hubUrl ?? "-").padEnd(hubW)} ${r.enrolled ? "yes" : "no"}`
53237
+ );
53238
+ }
53239
+ for (const r of rows.filter((x2) => !x2.enrolled)) {
53240
+ console.log(
53241
+ `
53242
+ Runner "${r.name}" has never reached a hub` + (r.lastExit !== null && r.lastExit !== 0 ? ` (last exit ${r.lastExit})` : "") + `. Check \`slay runner logs ${r.name}\`.`
53243
+ );
53244
+ }
53245
+ for (const r of rows.filter((x2) => !x2.running)) {
53246
+ console.log(`
53247
+ Runner "${r.name}" is stopped. Start it: slay runner start ${r.name}`);
53248
+ }
53249
+ });
53250
+ cmd.command("create <name>").description("Install a runner here and keep it running (crash-restart + start at login)").requiredOption("--token <token>", "Join token minted on the hub (szjt1.\u2026)").option("--root <dir>", "Runner root \u2014 its config, credentials and logs (default: cwd)").option(
53251
+ "--allow <dir>",
53252
+ "Filesystem root the runner may operate under (repeatable; default: the runner root)",
53253
+ (value, previous = []) => [...previous, value]
53254
+ ).action(async (name, opts) => {
53255
+ const root = (0, import_node_path9.resolve)(opts.root ?? process.cwd());
53256
+ const backend = resolveBackend();
53257
+ const decoded = decodeJoinToken(opts.token);
53258
+ if (!decoded) {
53259
+ fail(
53260
+ `--token is not a valid SlayZone join token (expected \`szjt1.<payload>\`).
53261
+ Mint one on the hub:
53262
+ curl -X POST http://127.0.0.1:<hub-port>/api/runners/join-token \\
53263
+ -H 'content-type: application/json' -d '{"label":"${name}"}'`
53264
+ );
53265
+ }
53266
+ if (name === DEFAULT_LOCAL_RUNNER_NAME) {
53267
+ fail(
53268
+ `"${DEFAULT_LOCAL_RUNNER_NAME}" is reserved for the runner inside the SlayZone desktop app \u2014 a second runner using it would collide with that one on the hub. Choose another name.`
53269
+ );
53270
+ }
53271
+ if (backend !== "none" && (0, import_node_fs9.existsSync)(unitPath("runner", name, backend))) {
53272
+ const existingRoot = readUnitRoot("runner", name, backend);
53273
+ fail(
53274
+ `A runner named "${name}" already exists${existingRoot ? ` (root ${shortenPath(existingRoot)})` : ""}.
53275
+ Start it with \`slay runner start ${name}\`, or remove it with \`slay runner rm ${name}\`.`
53276
+ );
53277
+ }
53278
+ const occupant = loadSlayzoneConfig(configPathFor(root)).runnerName;
53279
+ if (occupant !== void 0) {
53280
+ fail(
53281
+ `A runner is already installed in ${shortenPath(root)} \u2014 "${occupant}".
53282
+ ` + (occupant === name ? `Start it with \`slay runner start ${name}\`, or remove it with \`slay runner rm ${name}\`.` : `Give this runner its own directory with \`--root <dir>\`, or remove "${occupant}" first with \`slay runner rm ${occupant}\`.`)
53283
+ );
53284
+ }
53285
+ const allowedRoots = opts.allow && opts.allow.length > 0 ? opts.allow.map((d) => (0, import_node_path9.resolve)(d)) : [root];
53286
+ updateSlayzoneConfig(
53287
+ {
53288
+ joinToken: opts.token,
53289
+ hubUrl: decoded.hubUrl,
53290
+ runnerName: name,
53291
+ allowedRoots
53292
+ },
53293
+ configPathFor(root)
53294
+ );
53295
+ console.log(`Hub: ${decoded.hubUrl}`);
53296
+ console.log(`Allow: ${allowedRoots.map((d) => shortenPath(d)).join(", ")}`);
53297
+ await launchRunner({ name, root, backend, creating: true });
53298
+ });
53299
+ cmd.command("start <name>").description("Start an existing runner that is stopped").action(async (name) => {
53300
+ const backend = resolveBackend();
53301
+ const { root } = requireRegistered(name, backend);
53302
+ if (backend !== "none" && supervisorStatus("runner", backend, name).running) {
53303
+ const status = supervisorStatus("runner", backend, name);
53304
+ console.log(
53305
+ `Runner "${name}" is already running${status.pid ? ` (pid ${status.pid})` : ""}.`
53306
+ );
53307
+ return;
53308
+ }
53309
+ await launchRunner({ name, root, backend, creating: false });
53310
+ });
53311
+ cmd.command("stop <name>").description("Stop a runner, keeping it registered so `start` can bring it back").action(async (name) => {
53312
+ const backend = resolveBackend();
53313
+ requireRegistered(name, backend);
53314
+ if (backend === "none") return;
53315
+ supervisorStop("runner", backend, name);
53316
+ if (!await waitForStopped(name, backend, 15e3)) {
53317
+ fail(
53318
+ `Runner "${name}" is still running after 15s according to ${backend}. It may be managed elsewhere (docker, a system unit).`
53319
+ );
53320
+ }
53321
+ console.log(`Stopped "${name}". Start it again with \`slay runner start ${name}\`.`);
53322
+ });
53323
+ cmd.command("rm <name>").description("Stop a runner and remove its registration").action((name) => {
53324
+ const backend = resolveBackend();
53325
+ const { root } = requireRegistered(name, backend);
53326
+ if (backend === "none") return;
53327
+ supervisorStopQuiet("runner", backend, name);
53328
+ removeUnit("runner", name, backend);
53329
+ console.log(`Removed "${name}". Its config and credentials are still in ${shortenPath(root)}.`);
53330
+ console.log(
53331
+ `It is still enrolled on its hub \u2014 revoke it there if this machine is going away.`
53332
+ );
53333
+ });
53334
+ cmd.command("restart <name>").description("Restart a runner").option("--upgrade", `Re-resolve ${RUNNER_PACKAGE} first (picks up a newer version)`).action(async (name, opts) => {
53335
+ const backend = resolveBackend();
53336
+ const { root } = requireRegistered(name, backend);
53337
+ if (backend === "none") return;
53338
+ const logDir = ensureLogDir("runner", root);
53339
+ if (opts.upgrade) {
53340
+ const bin = resolveServiceBin("runner");
53341
+ writeUnit(
53342
+ {
53343
+ kind: "runner",
53344
+ name,
53345
+ root,
53346
+ command: bin.command,
53347
+ args: bin.args,
53348
+ logDir,
53349
+ // Must carry the interpreter env: dropping it here would rewrite a
53350
+ // working unit into one that crash-loops on Electron-ABI natives.
53351
+ ...bin.env ? { env: bin.env } : {}
53352
+ },
53353
+ backend
53354
+ );
53355
+ console.log(`Unit updated to ${RUNNER_PACKAGE}@${bin.version}.`);
53356
+ }
53357
+ supervisorStop("runner", backend, name);
53358
+ await waitForStopped(name, backend, 15e3);
53359
+ supervisorStart("runner", backend, name, unitPath("runner", name, backend));
53360
+ if (!await waitForConnected(root, ENROLL_TIMEOUT_MS)) {
53361
+ fail(
53362
+ `Runner "${name}" did not reconnect within ${ENROLL_TIMEOUT_MS / 1e3}s. Check \`slay runner logs ${name}\`.`
53363
+ );
53364
+ }
53365
+ const status = supervisorStatus("runner", backend, name);
53366
+ console.log(
53367
+ `Runner "${name}" restarted${status.pid ? ` (pid ${status.pid})` : ""} and reconnected.`
53368
+ );
53369
+ });
53370
+ cmd.command("logs <name>").description("Show a runner's log output").option("-n, --lines <n>", "Last N lines", "50").option("-f, --follow", "Follow the log").action((name, opts) => {
53371
+ const backend = detectBackend();
53372
+ const { root } = requireRegistered(name, backend);
53373
+ const lines = Number(opts.lines);
53374
+ if (!Number.isInteger(lines) || lines < 1) fail(`Invalid --lines ${opts.lines}.`);
53375
+ followServiceLog({
53376
+ kind: "runner",
53377
+ name,
53378
+ root,
53379
+ lines,
53380
+ follow: opts.follow === true,
53381
+ systemdUnit: backend === "systemd" && (0, import_node_fs9.existsSync)(unitPath("runner", name, backend))
53382
+ });
53383
+ });
53384
+ return cmd;
53385
+ }
53386
+
52763
53387
  // src/index.ts
52764
53388
  process.removeAllListeners("warning");
52765
53389
  process.on("warning", (warning) => {
52766
53390
  if (warning.name === "ExperimentalWarning" && /SQLite/i.test(warning.message)) return;
52767
53391
  console.warn(`${warning.name}: ${warning.message}`);
52768
53392
  });
52769
- var program2 = new Command().name("slay").description("SlayZone CLI").version("0.36.0-beta.10").option("--dev", "Use development database (slayzone.dev.sqlite)").option("--hub <name|port>", "Target a specific hub on this machine (see `slay hub ls`)").showSuggestionAfterError(true).showHelpAfterError(true).hook("preAction", async (thisCommand) => {
53393
+ var program2 = new Command().name("slay").description("SlayZone CLI").version("0.36.0-beta.11").option("--dev", "Use development database (slayzone.dev.sqlite)").option("--hub <name|port>", "Target a specific hub on this machine (see `slay hub ls`)").showSuggestionAfterError(true).showHelpAfterError(true).hook("preAction", async (thisCommand) => {
52770
53394
  const root = thisCommand.parent ?? thisCommand;
52771
53395
  const opts = root.opts();
52772
53396
  if (opts.dev) process.env.SLAYZONE_DEV = "1";
@@ -52803,6 +53427,7 @@ program2.addCommand(completionsCommand());
52803
53427
  program2.addCommand(initCommand());
52804
53428
  program2.addCommand(ptyCommand());
52805
53429
  program2.addCommand(hubCommand());
53430
+ program2.addCommand(runnerCommand());
52806
53431
  program2.parseAsync(process.argv).catch((err) => {
52807
53432
  console.error(err instanceof Error ? err.message : String(err));
52808
53433
  process.exit(1);