@gajae-gateway/cli 0.1.1 → 0.1.3

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/dist/main.js CHANGED
@@ -2,9 +2,10 @@
2
2
  // @bun
3
3
 
4
4
  // src/main.ts
5
- import { copyFile, stat } from "fs/promises";
6
- import { isAbsolute, join as join2 } from "path";
7
- import { LOOPBACK_ORIGIN, originKey as originKey2, parseOriginKey } from "@gajae-gateway/protocol";
5
+ import { Database } from "bun:sqlite";
6
+ import { copyFile, lstat, readFile as readFile3, stat as stat2 } from "fs/promises";
7
+ import { isAbsolute, join as join3 } from "path";
8
+ import { LOOPBACK_ORIGIN, originKey as originKey2 } from "@gajae-gateway/protocol";
8
9
  import { GajaewayClient } from "@gajae-gateway/sdk";
9
10
 
10
11
  // src/list.ts
@@ -126,6 +127,12 @@ function renderList(columns, rows, options, envelope) {
126
127
  return [JSON.stringify({ ...envelope.result, [envelope.key]: projected })];
127
128
  }
128
129
 
130
+ // src/restart-stack.ts
131
+ import { randomUUID } from "crypto";
132
+ import { access, mkdir as mkdir2, readFile as readFile2, rename, stat, writeFile as writeFile2 } from "fs/promises";
133
+ import { homedir as homedir2 } from "os";
134
+ import { join as join2 } from "path";
135
+
129
136
  // src/services.ts
130
137
  import { chmod, mkdir, readFile, writeFile } from "fs/promises";
131
138
  import { homedir } from "os";
@@ -134,22 +141,34 @@ import { parseRuntimeConfig, RuntimeConfigError } from "@gajae-gateway/protocol"
134
141
  var CONFIG_SCHEMA_VERSION = 1;
135
142
  var FALLBACK_PATH_DIRS = ["/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
136
143
  var LOGIN_PATH_COMMAND = 'printf %s "$PATH"';
144
+ var GATEWAY_UNIT = "gajaeway-gateway.service";
137
145
  var SERVICE_SPECS = [
138
- { id: "gateway", label: "dev.gajaeway.gateway", binary: "gajaeway-gateway", args: ["daemon"] },
146
+ {
147
+ id: "gateway",
148
+ label: "dev.gajaeway.gateway",
149
+ binary: "gajaeway-gateway",
150
+ args: ["daemon"],
151
+ dependsOnGateway: false
152
+ },
139
153
  {
140
154
  id: "adapter-discord",
141
155
  label: "dev.gajaeway.adapter-discord",
142
156
  binary: "gajaeway-discord",
143
- args: []
157
+ args: [],
158
+ dependsOnGateway: true
144
159
  },
145
160
  {
146
161
  id: "adapter-slack",
147
162
  label: "dev.gajaeway.adapter-slack",
148
163
  binary: "gajaeway-slack",
149
- args: []
164
+ args: [],
165
+ dependsOnGateway: true
150
166
  },
151
- { id: "admin", label: "dev.gajaeway.admin", binary: "gajaeway-admin", args: ["serve"] }
167
+ { id: "admin", label: "dev.gajaeway.admin", binary: "gajaeway-admin", args: ["serve"], dependsOnGateway: true }
152
168
  ];
169
+ function serviceSpecs() {
170
+ return SERVICE_SPECS;
171
+ }
153
172
  function homeForEnvironment(env) {
154
173
  return env.GAJAEWAY_HOME || join(env.HOME || homedir(), ".gajaeway");
155
174
  }
@@ -237,6 +256,70 @@ function renderLaunchAgent(spec, binDir, home, path) {
237
256
  </dict></plist>
238
257
  `;
239
258
  }
259
+ function unitEnvironment(name, value) {
260
+ return `Environment="${name}=${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
261
+ }
262
+ function systemdUnitName(spec) {
263
+ return `gajaeway-${spec.id}.service`;
264
+ }
265
+ function renderSystemdUnit(spec, binDir, home, path) {
266
+ const execStart = [join(binDir, spec.binary), ...spec.args].join(" ");
267
+ const unit = [
268
+ "[Unit]",
269
+ `Description=gajaeway ${spec.id}`,
270
+ ...spec.dependsOnGateway ? [`BindsTo=${GATEWAY_UNIT}`, `After=${GATEWAY_UNIT}`, `PartOf=${GATEWAY_UNIT}`] : [],
271
+ "",
272
+ "[Service]",
273
+ `ExecStart=${execStart}`,
274
+ `WorkingDirectory=${binDir}`,
275
+ unitEnvironment("GAJAEWAY_HOME", home),
276
+ unitEnvironment("PATH", path),
277
+ "Restart=always",
278
+ "RestartSec=2",
279
+ ...spec.dependsOnGateway ? [] : ["KillMode=process", "TimeoutStopSec=30s"],
280
+ "",
281
+ "[Install]",
282
+ `WantedBy=${spec.dependsOnGateway ? GATEWAY_UNIT : "default.target"}`,
283
+ ""
284
+ ];
285
+ return unit.join(`
286
+ `);
287
+ }
288
+ function systemdUnitDir(env, userHome) {
289
+ const base = env.XDG_CONFIG_HOME || join(userHome, ".config");
290
+ return join(base, "systemd", "user");
291
+ }
292
+ function launchAgentPlistPath(spec, launchAgentsDir) {
293
+ return join(launchAgentsDir, `dev.gajaeway.${spec.id}.plist`);
294
+ }
295
+ function systemdUnitPath(spec, unitDir) {
296
+ return join(unitDir, systemdUnitName(spec));
297
+ }
298
+ function restartStackCommands(platform, uid) {
299
+ if (platform === "linux")
300
+ return [["systemctl", "--user", "restart", GATEWAY_UNIT]];
301
+ if (platform === "darwin") {
302
+ const target = uid ?? process.getuid?.() ?? 0;
303
+ return restartOrder().map((spec) => ["launchctl", "kickstart", "-k", `gui/${target}/${spec.label}`]);
304
+ }
305
+ throw new Error(`unsupported service platform: ${platform}`);
306
+ }
307
+ function restartOrder() {
308
+ return [
309
+ ...SERVICE_SPECS.filter((spec) => !spec.dependsOnGateway),
310
+ ...SERVICE_SPECS.filter((spec) => spec.dependsOnGateway)
311
+ ];
312
+ }
313
+ function requirePlatform(platform) {
314
+ if (platform !== "darwin" && platform !== "linux")
315
+ throw new Error(`unsupported service platform: ${platform}`);
316
+ return platform;
317
+ }
318
+ function currentPlatform() {
319
+ if (process.platform === "darwin" || process.platform === "linux")
320
+ return process.platform;
321
+ throw new Error(`unsupported service platform: ${process.platform}`);
322
+ }
240
323
  async function readRuntime(home) {
241
324
  const configPath = join(home, "config.json");
242
325
  let raw;
@@ -267,10 +350,11 @@ async function installServices(options) {
267
350
  if (options.binDir.length === 0)
268
351
  throw new Error("services requires a non-empty --bin-dir DIR");
269
352
  const env = options.env ?? process.env;
353
+ const platform = options.platform === undefined ? currentPlatform() : requirePlatform(options.platform);
270
354
  const userHome = env.HOME || homedir();
271
355
  const home = homeForEnvironment(env);
272
356
  const binDir = expandHome(options.binDir, userHome);
273
- const launchAgentsDir = expandHome(options.launchAgentsDir ?? join(userHome, "Library", "LaunchAgents"), userHome);
357
+ const targetDir = platform === "darwin" ? expandHome(options.launchAgentsDir ?? join(userHome, "Library", "LaunchAgents"), userHome) : expandHome(options.unitDir ?? systemdUnitDir(env, userHome), userHome);
274
358
  const runtime = await readRuntime(home);
275
359
  const path = await resolvePath({
276
360
  binDir,
@@ -279,24 +363,298 @@ async function installServices(options) {
279
363
  env,
280
364
  loginPathRunner: options.loginPathRunner
281
365
  });
282
- await mkdir(launchAgentsDir, { recursive: true, mode: 448 });
283
- const plists = SERVICE_SPECS.map((spec) => ({
284
- path: join(launchAgentsDir, `dev.gajaeway.${spec.id}.plist`),
366
+ await mkdir(targetDir, { recursive: true, mode: 448 });
367
+ const definitions = SERVICE_SPECS.map((spec) => platform === "darwin" ? {
368
+ path: join(targetDir, `dev.gajaeway.${spec.id}.plist`),
285
369
  contents: renderLaunchAgent(spec, binDir, home, path)
286
- }));
370
+ } : { path: join(targetDir, systemdUnitName(spec)), contents: renderSystemdUnit(spec, binDir, home, path) });
287
371
  const write = options.writeFile;
288
- for (const plist of plists) {
372
+ for (const definition of definitions) {
289
373
  if (write)
290
- await write(plist.path, plist.contents);
374
+ await write(definition.path, definition.contents);
291
375
  else {
292
- await writeFile(plist.path, plist.contents, { encoding: "utf8", mode: 384 });
293
- await chmod(plist.path, 384);
376
+ await writeFile(definition.path, definition.contents, { encoding: "utf8", mode: 384 });
377
+ await chmod(definition.path, 384);
294
378
  }
295
379
  }
296
- return plists.map((plist) => plist.path);
380
+ return definitions.map((definition) => definition.path);
297
381
  }
298
382
  function serviceUsage() {
299
- return "usage: gajaeway services install|repair --bin-dir DIR [--launch-agents-dir DIR]";
383
+ return "usage: gajaeway services install|repair --bin-dir DIR [--launch-agents-dir DIR] [--unit-dir DIR] [--platform darwin|linux]";
384
+ }
385
+
386
+ // src/restart-stack.ts
387
+ var RECEIPT_FILE = "restart-stack.json";
388
+ var VERIFY_TIMEOUT_MS = 90000;
389
+ var POLL_MS = 1000;
390
+ function restartReceiptPath(home) {
391
+ return join2(home, RECEIPT_FILE);
392
+ }
393
+ async function readRestartReceipt(home) {
394
+ let raw;
395
+ try {
396
+ raw = await readFile2(restartReceiptPath(home), "utf8");
397
+ } catch (error) {
398
+ if (error.code === "ENOENT")
399
+ return;
400
+ throw error;
401
+ }
402
+ return JSON.parse(raw);
403
+ }
404
+ async function writeReceipt(home, receipt) {
405
+ await mkdir2(home, { recursive: true });
406
+ const path = restartReceiptPath(home);
407
+ const temporary = `${path}.${process.pid}.tmp`;
408
+ await writeFile2(temporary, `${JSON.stringify(receipt, null, 2)}
409
+ `, { mode: 384 });
410
+ await rename(temporary, path);
411
+ }
412
+ function newReceipt(id, platform, uid, now) {
413
+ const commands = restartStackCommands(platform, uid);
414
+ return {
415
+ id,
416
+ state: "queued",
417
+ platform,
418
+ requestedAt: new Date(now).toISOString(),
419
+ steps: restartOrder().map((spec, index) => ({
420
+ label: spec.label,
421
+ ...platform === "darwin" ? { command: commands[index] } : index === 0 ? { command: commands[0] } : {},
422
+ result: "pending"
423
+ }))
424
+ };
425
+ }
426
+ async function defaultCommandRunner(command) {
427
+ const child = Bun.spawn([...command], { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
428
+ return await child.exited;
429
+ }
430
+ async function capture(command) {
431
+ const child = Bun.spawn([...command], {
432
+ stdin: "ignore",
433
+ stdout: "pipe",
434
+ stderr: "ignore",
435
+ env: { ...process.env, LC_ALL: "C" }
436
+ });
437
+ const output = await new Response(child.stdout).text();
438
+ return await child.exited === 0 ? output : undefined;
439
+ }
440
+ function defaultServiceProbe(platform) {
441
+ return async (label) => {
442
+ const spec = serviceSpecs().find((candidate) => candidate.label === label);
443
+ if (spec === undefined)
444
+ throw new Error(`unknown service label: ${label}`);
445
+ let pid;
446
+ if (platform === "darwin") {
447
+ const listed = await capture(["launchctl", "list", label]);
448
+ const match = listed?.match(/"PID"\s*=\s*(\d+);/);
449
+ pid = match ? Number(match[1]) : undefined;
450
+ } else {
451
+ const shown = await capture(["systemctl", "--user", "show", "-p", "MainPID", "--value", systemdUnitName(spec)]);
452
+ pid = shown === undefined ? undefined : Number(shown.trim());
453
+ }
454
+ if (pid === undefined || !Number.isInteger(pid) || pid <= 0)
455
+ return;
456
+ const lstart = await capture(["ps", "-o", "lstart=", "-p", String(pid)]);
457
+ const args = await capture(["ps", "-ww", "-o", "args=", "-p", String(pid)]);
458
+ if (lstart === undefined || args === undefined)
459
+ return;
460
+ const startedAt = Date.parse(lstart.trim());
461
+ if (!Number.isFinite(startedAt))
462
+ return;
463
+ const suffix = spec.args.length > 0 ? ` ${spec.args.join(" ")}` : "";
464
+ const line = args.trim();
465
+ const binary = suffix && line.endsWith(suffix) ? line.slice(0, -suffix.length) : line;
466
+ return { pid, startedAt, binary };
467
+ };
468
+ }
469
+ async function defaultBinaryModifiedAt(path) {
470
+ return (await stat(path)).mtimeMs;
471
+ }
472
+ function defaultIsServiceInstalled(options) {
473
+ return async (label) => {
474
+ const spec = serviceSpecs().find((s) => s.label === label);
475
+ if (spec === undefined)
476
+ return false;
477
+ if (options.platform === "darwin") {
478
+ const userHome2 = options.env.HOME || homedir2();
479
+ const agentsDir = options.launchAgentsDir || join2(userHome2, "Library", "LaunchAgents");
480
+ const plistPath = launchAgentPlistPath(spec, agentsDir);
481
+ try {
482
+ await access(plistPath);
483
+ return true;
484
+ } catch {
485
+ return false;
486
+ }
487
+ }
488
+ const userHome = options.env.HOME || homedir2();
489
+ const unitDirPath = options.unitDir || systemdUnitDir(options.env, userHome);
490
+ const unitPath = systemdUnitPath(spec, unitDirPath);
491
+ try {
492
+ await access(unitPath);
493
+ return true;
494
+ } catch {
495
+ return false;
496
+ }
497
+ };
498
+ }
499
+ function floorSecond(ms) {
500
+ return Math.floor(ms / 1000) * 1000;
501
+ }
502
+ async function runRestartStack(options) {
503
+ const platform = options.platform ?? currentPlatform();
504
+ const runner = options.runner ?? defaultCommandRunner;
505
+ const probe = options.probe ?? defaultServiceProbe(platform);
506
+ const modifiedAt = options.binaryModifiedAt ?? defaultBinaryModifiedAt;
507
+ const isInstalled = options.isServiceInstalled ?? defaultIsServiceInstalled({
508
+ platform,
509
+ env: options.env ?? process.env,
510
+ launchAgentsDir: options.launchAgentsDir,
511
+ unitDir: options.unitDir
512
+ });
513
+ const now = options.now ?? Date.now;
514
+ const sleep = options.sleep ?? ((ms) => Bun.sleep(ms));
515
+ const timeoutMs = options.verifyTimeoutMs ?? VERIFY_TIMEOUT_MS;
516
+ const pollMs = options.pollMs ?? POLL_MS;
517
+ const existing = await readRestartReceipt(options.home);
518
+ const receipt = existing?.id === options.id && existing.state === "queued" ? existing : newReceipt(options.id, platform, options.uid, now());
519
+ receipt.state = "running";
520
+ receipt.supervisorPid = process.pid;
521
+ await writeReceipt(options.home, receipt);
522
+ let issuedAt = now();
523
+ for (const step of receipt.steps) {
524
+ if (receipt.state !== "running") {
525
+ step.result = "skipped";
526
+ continue;
527
+ }
528
+ try {
529
+ const installed = await isInstalled(step.label);
530
+ if (!installed) {
531
+ step.result = "skipped";
532
+ step.detail = "service is not installed";
533
+ await writeReceipt(options.home, receipt);
534
+ continue;
535
+ }
536
+ if (step.command !== undefined) {
537
+ issuedAt = now();
538
+ const status = await runner(step.command);
539
+ step.exitStatus = status;
540
+ if (status !== 0) {
541
+ step.result = "failed";
542
+ step.detail = `${step.command.join(" ")} exited with status ${status}`;
543
+ }
544
+ }
545
+ if (step.result === "pending")
546
+ await verifyStep(step, issuedAt);
547
+ } catch (error) {
548
+ step.result = "failed";
549
+ step.detail = error instanceof Error ? error.message : String(error);
550
+ }
551
+ if (step.result !== "ok" && step.result !== "skipped") {
552
+ receipt.state = step.result === "stale" ? "stale" : "failed";
553
+ }
554
+ await writeReceipt(options.home, receipt);
555
+ }
556
+ if (receipt.state === "running")
557
+ receipt.state = "ok";
558
+ receipt.finishedAt = new Date(now()).toISOString();
559
+ await writeReceipt(options.home, receipt);
560
+ return receipt;
561
+ async function verifyStep(step, since) {
562
+ const deadline = now() + timeoutMs;
563
+ for (;; ) {
564
+ const running = await probe(step.label);
565
+ let binaryModifiedAt = 0;
566
+ if (running !== undefined) {
567
+ binaryModifiedAt = await modifiedAt(running.binary);
568
+ step.pid = running.pid;
569
+ step.processStartedAt = new Date(running.startedAt).toISOString();
570
+ step.binary = running.binary;
571
+ step.binaryModifiedAt = new Date(binaryModifiedAt).toISOString();
572
+ if (running.startedAt >= floorSecond(Math.max(binaryModifiedAt, since))) {
573
+ step.result = "ok";
574
+ delete step.detail;
575
+ return;
576
+ }
577
+ }
578
+ if (now() >= deadline) {
579
+ if (running === undefined) {
580
+ step.result = "failed";
581
+ step.detail = "no running process after restart";
582
+ } else {
583
+ step.result = "stale";
584
+ step.detail = running.startedAt < binaryModifiedAt ? "process started before the deployed binary was modified" : "process was not restarted by this sequence";
585
+ }
586
+ return;
587
+ }
588
+ await sleep(pollMs);
589
+ }
590
+ }
591
+ }
592
+ function cliCommand() {
593
+ return Bun.main.startsWith("/$bunfs/") ? [process.execPath] : [process.execPath, Bun.main];
594
+ }
595
+ function alive(pid) {
596
+ if (pid === undefined)
597
+ return false;
598
+ try {
599
+ process.kill(pid, 0);
600
+ return true;
601
+ } catch {
602
+ return false;
603
+ }
604
+ }
605
+ async function launchRestartStack(options) {
606
+ const platform = options.platform ?? currentPlatform();
607
+ const existing = await readRestartReceipt(options.home);
608
+ if (existing?.state === "running" && alive(existing.supervisorPid))
609
+ throw new Error(`restart-stack ${existing.id} is still running (supervisor pid ${existing.supervisorPid})`);
610
+ const receipt = newReceipt(randomUUID(), platform, options.uid, Date.now());
611
+ await writeReceipt(options.home, receipt);
612
+ const worker = [...options.worker ?? [...cliCommand(), "ops", "restart-stack"], "--run", receipt.id];
613
+ const child = Bun.spawn(supervisorArgv(platform, worker, options.home, receipt.id), {
614
+ detached: true,
615
+ stdin: "ignore",
616
+ stdout: "ignore",
617
+ stderr: "ignore",
618
+ env: { ...options.env ?? process.env, GAJAEWAY_HOME: options.home }
619
+ });
620
+ child.unref();
621
+ return { receipt, supervisorPid: child.pid };
622
+ }
623
+ function supervisorArgv(platform, worker, home, id) {
624
+ if (platform === "darwin")
625
+ return [...worker];
626
+ return [
627
+ "systemd-run",
628
+ "--user",
629
+ "--collect",
630
+ "--quiet",
631
+ `--unit=gajaeway-restart-stack-${id}`,
632
+ `--setenv=GAJAEWAY_HOME=${home}`,
633
+ "--",
634
+ ...worker
635
+ ];
636
+ }
637
+ function effectiveRestartState(receipt) {
638
+ if (receipt.state === "running" && !alive(receipt.supervisorPid))
639
+ return "interrupted";
640
+ return receipt.state;
641
+ }
642
+ function renderRestartReceipt(receipt) {
643
+ const lines = [
644
+ `restart-stack ${receipt.id}: ${effectiveRestartState(receipt)}`,
645
+ `requestedAt: ${receipt.requestedAt}`,
646
+ `finishedAt: ${receipt.finishedAt ?? "-"}`
647
+ ];
648
+ for (const step of receipt.steps) {
649
+ const facts = [
650
+ step.pid === undefined ? undefined : `pid=${step.pid}`,
651
+ step.processStartedAt === undefined ? undefined : `started=${step.processStartedAt}`,
652
+ step.binaryModifiedAt === undefined ? undefined : `binary=${step.binaryModifiedAt}`,
653
+ step.detail
654
+ ].filter((fact) => fact !== undefined);
655
+ lines.push(`${step.label}: ${step.result}${facts.length > 0 ? ` ${facts.join(" ")}` : ""}`);
656
+ }
657
+ return lines;
300
658
  }
301
659
 
302
660
  // src/main.ts
@@ -315,7 +673,7 @@ var COMMANDS = [
315
673
  "work",
316
674
  "services"
317
675
  ];
318
- var CLI_USAGE = "usage: gajaeway [--socket PATH] status|shutdown|chat|daemon run|sessions list [--json] [--fields a,b,c] [--limit N] [--offset N]|sessions inspect <originKey-or-index>|memory audit|memory search <query>|monitors ...|work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] [--notify originKey (start only)] <text>|work status <name>|work steer <name> <text>|work retire <name>|work jobs|ops backup <path>|ops cycle [--json]|ops integrity|ops restore <backupPath>|services install|repair --bin-dir DIR [--launch-agents-dir DIR] (work run waits for a response; caller timeout does not end the attempt)";
676
+ var CLI_USAGE = "usage: gajaeway [--socket PATH] status|shutdown|chat|daemon run|sessions list [--json] [--fields a,b,c] [--limit N] [--offset N]|sessions inspect <originKey-or-index>|memory audit|memory search <query>|monitors ... (test <id> [--type T] [--payload J] [--wait[=SECONDS]])|work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] <text>|work status <name>|work steer <name> <text>|work retire <name>|work jobs|ops backup <path>|ops redeliver <deliveryId>|ops redeliver --since <iso>|ops cycle [--json]|ops integrity|ops restore <backupPath>|ops restart-stack [--status]|services install|repair --bin-dir DIR [--launch-agents-dir DIR] [--unit-dir DIR] [--platform darwin|linux] (work run waits for a response; caller timeout does not end the attempt)";
319
677
  var USAGE_EXIT_CODE = 2;
320
678
  function usageFor(command) {
321
679
  return command !== undefined && COMMANDS.includes(command) ? undefined : CLI_USAGE;
@@ -340,22 +698,171 @@ function parseServicesArgs(args) {
340
698
  throw new Error(serviceUsage());
341
699
  let binDir;
342
700
  let launchAgentsDir;
701
+ let unitDir;
702
+ let platform;
343
703
  for (let i = 1;i < args.length; i++) {
344
704
  const flag = args[i];
345
- if (flag === "--bin-dir" || flag === "--launch-agents-dir") {
705
+ if (flag === "--bin-dir" || flag === "--launch-agents-dir" || flag === "--unit-dir") {
346
706
  const value = args[++i];
347
707
  if (value === undefined || value.length === 0 || value.startsWith("--"))
348
708
  throw new Error(`${flag} expects a non-empty DIR`);
349
709
  if (flag === "--bin-dir")
350
710
  binDir = value;
711
+ else if (flag === "--unit-dir")
712
+ unitDir = value;
351
713
  else
352
714
  launchAgentsDir = value;
715
+ } else if (flag === "--platform") {
716
+ const value = args[++i];
717
+ if (value !== "darwin" && value !== "linux")
718
+ throw new Error("--platform expects darwin or linux");
719
+ platform = value;
353
720
  } else
354
721
  throw new Error(`unknown option: ${flag}`);
355
722
  }
356
723
  if (binDir === undefined)
357
724
  throw new Error("services requires --bin-dir DIR");
358
- return { action, binDir, ...launchAgentsDir === undefined ? {} : { launchAgentsDir } };
725
+ return {
726
+ action,
727
+ binDir,
728
+ ...launchAgentsDir === undefined ? {} : { launchAgentsDir },
729
+ ...unitDir === undefined ? {} : { unitDir },
730
+ ...platform === undefined ? {} : { platform }
731
+ };
732
+ }
733
+ var MONITOR_UPDATE_USAGE = "usage: gajaeway monitors update <id> (--json '<partial MonitorSpec JSON>'|--schedule '<cron>' [--enabled true|false]|--enabled true|false [--schedule '<cron>'])";
734
+ function parseMonitorUpdateArgs(args) {
735
+ const [monitorId, ...options] = args;
736
+ if (!monitorId || monitorId.startsWith("--") || options.length === 0)
737
+ throw new Error(MONITOR_UPDATE_USAGE);
738
+ let jsonPatch;
739
+ let schedule;
740
+ let enabled;
741
+ for (let i = 0;i < options.length; i++) {
742
+ const option = options[i];
743
+ if (option === "--json") {
744
+ const value = options[++i];
745
+ if (jsonPatch !== undefined || value === undefined || value.startsWith("--"))
746
+ throw new Error(MONITOR_UPDATE_USAGE);
747
+ let parsed;
748
+ try {
749
+ parsed = JSON.parse(value);
750
+ } catch {
751
+ throw new Error(MONITOR_UPDATE_USAGE);
752
+ }
753
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
754
+ throw new Error(MONITOR_UPDATE_USAGE);
755
+ jsonPatch = parsed;
756
+ } else if (option === "--schedule") {
757
+ const value = options[++i];
758
+ if (schedule !== undefined || !value || value.startsWith("--"))
759
+ throw new Error(MONITOR_UPDATE_USAGE);
760
+ schedule = value;
761
+ } else if (option === "--enabled") {
762
+ const value = options[++i];
763
+ if (enabled !== undefined || value !== "true" && value !== "false")
764
+ throw new Error(MONITOR_UPDATE_USAGE);
765
+ enabled = value === "true";
766
+ } else
767
+ throw new Error(MONITOR_UPDATE_USAGE);
768
+ }
769
+ if (jsonPatch !== undefined && (schedule !== undefined || enabled !== undefined))
770
+ throw new Error(MONITOR_UPDATE_USAGE);
771
+ if (jsonPatch !== undefined)
772
+ return { monitorId, params: { ...jsonPatch, monitorId } };
773
+ return {
774
+ monitorId,
775
+ params: {
776
+ monitorId,
777
+ ...schedule === undefined ? {} : { schedule },
778
+ ...enabled === undefined ? {} : { enabled }
779
+ }
780
+ };
781
+ }
782
+ var MONITOR_TEST_USAGE = "usage: gajaeway monitors test <id> [--type T] [--payload J] [--wait[=SECONDS]]";
783
+ var MONITOR_WAIT_ERROR = `${MONITOR_TEST_USAGE}
784
+ --wait expects a finite non-negative integer up to 86400`;
785
+ function parseMonitorTestArgs(args) {
786
+ const [monitorId, ...flags] = args;
787
+ if (!monitorId || monitorId.startsWith("--"))
788
+ throw new Error(MONITOR_TEST_USAGE);
789
+ let eventType;
790
+ let payload = {};
791
+ let waitSeconds;
792
+ const seen = new Set;
793
+ for (let i = 0;i < flags.length; i++) {
794
+ const flag = flags[i];
795
+ if (flag === "--type" || flag === "--payload") {
796
+ if (seen.has(flag))
797
+ throw new Error(`${MONITOR_TEST_USAGE}
798
+ duplicate ${flag}`);
799
+ seen.add(flag);
800
+ const value = flags[++i];
801
+ if (value === undefined || value.length === 0 || value.startsWith("--"))
802
+ throw new Error(`${MONITOR_TEST_USAGE}
803
+ ${flag} requires a value`);
804
+ if (flag === "--type")
805
+ eventType = value;
806
+ else
807
+ payload = JSON.parse(value);
808
+ } else if (flag === "--wait" || flag.startsWith("--wait=")) {
809
+ if (seen.has("--wait"))
810
+ throw new Error(`${MONITOR_TEST_USAGE}
811
+ duplicate --wait`);
812
+ seen.add("--wait");
813
+ const value = flag === "--wait" ? undefined : flag.slice("--wait=".length);
814
+ if (value === undefined)
815
+ waitSeconds = 30;
816
+ else {
817
+ if (!/^\d+$/.test(value))
818
+ throw new Error(MONITOR_WAIT_ERROR);
819
+ const seconds = Number(value);
820
+ if (!Number.isFinite(seconds) || !Number.isInteger(seconds) || seconds > 86400)
821
+ throw new Error(MONITOR_WAIT_ERROR);
822
+ waitSeconds = seconds;
823
+ }
824
+ } else
825
+ throw new Error(`${MONITOR_TEST_USAGE}
826
+ unknown argument: ${flag}`);
827
+ }
828
+ return {
829
+ monitorId,
830
+ ...eventType === undefined ? {} : { eventType },
831
+ payload,
832
+ ...waitSeconds === undefined ? {} : { waitSeconds }
833
+ };
834
+ }
835
+ var TERMINAL_MONITOR_STAGES = new Set(["delivered", "authored_no_delivery", "failed", "failed_no_retry"]);
836
+ function waitForMonitorStage(client, eventId, timeoutSeconds) {
837
+ return new Promise((resolve) => {
838
+ let stage = "admitted";
839
+ let settled = false;
840
+ let timer;
841
+ let unsubscribe;
842
+ const finish = () => {
843
+ if (settled)
844
+ return;
845
+ settled = true;
846
+ if (timer !== undefined)
847
+ clearTimeout(timer);
848
+ unsubscribe?.();
849
+ resolve(stage);
850
+ };
851
+ unsubscribe = client.on("monitor.event", (payload) => {
852
+ if (typeof payload !== "object" || payload === null)
853
+ return;
854
+ const event = payload;
855
+ if (event.eventId !== eventId || typeof event.stage !== "string")
856
+ return;
857
+ stage = event.stage;
858
+ if (TERMINAL_MONITOR_STAGES.has(stage))
859
+ finish();
860
+ });
861
+ if (settled)
862
+ unsubscribe();
863
+ else
864
+ timer = setTimeout(finish, timeoutSeconds * 1000);
865
+ });
359
866
  }
360
867
  function renderCycle(cycle) {
361
868
  const lines = [];
@@ -370,7 +877,14 @@ function renderCycle(cycle) {
370
877
  lines.push(`context: unread=${cycle.contextDiff.unread} expired=${cycle.contextDiff.expired} truncated=${cycle.contextDiff.truncated} omitted_oldest=${cycle.contextDiff.omittedOldestAt ?? "-"} omitted_newest=${cycle.contextDiff.omittedNewestAt ?? "-"}`);
371
878
  lines.push(`deliveries: pending=${cycle.deliveries.pending} inflight=${cycle.deliveries.inflight} confirmed=${cycle.deliveries.confirmed} failed_ambiguous=${cycle.deliveries.failedAmbiguous} expired=${cycle.deliveries.expired}`);
372
879
  lines.push(`memory: queued=${cycle.memoryIntents.queued} written=${cycle.memoryIntents.written} committed=${cycle.memoryIntents.committed} receipted=${cycle.memoryIntents.receipted} quarantined=${cycle.memoryIntents.quarantined}${cycle.memoryClosing ? " (closing)" : ""}`);
880
+ if (cycle.agentDisk) {
881
+ const { path, freeBytes, totalBytes } = cycle.agentDisk;
882
+ const gib = (bytes) => (bytes / 1024 ** 3).toFixed(1);
883
+ lines.push(freeBytes === null || totalBytes === null ? `agent_disk: ${path} unobservable` : `agent_disk: ${path} free=${gib(freeBytes)}GiB total=${gib(totalBytes)}GiB`);
884
+ }
373
885
  lines.push(`monitors: ${cycle.monitorEvents.length ? cycle.monitorEvents.map((m) => `${m.stage}=${m.count}`).join(" ") : "none"}`);
886
+ if (cycle.monitorAuthoringLost.length > 0)
887
+ lines.push(`monitor authoring lost: ${cycle.monitorAuthoringLost.map((m) => `${m.eventType}=${m.consecutive} (last ${m.lastFiredAt})`).join(" ")}`);
374
888
  if (cycle.sessions.length > 0) {
375
889
  lines.push("sessions:");
376
890
  lines.push("INDEX ORIGIN EPOCH SESSION PENDING UNSETTLED OLDEST");
@@ -385,11 +899,61 @@ function renderCycle(cycle) {
385
899
  function cycleExitCode(cycle) {
386
900
  return cycle.gates.length > 0 ? 1 : 0;
387
901
  }
902
+ function verifyBackupIntegrity(backupPath) {
903
+ let database;
904
+ try {
905
+ database = new Database(backupPath, { readonly: true });
906
+ } catch (error) {
907
+ throw new Error(`Backup is not readable: ${backupPath} (${error instanceof Error ? error.message : String(error)})`);
908
+ }
909
+ let detail;
910
+ let pageCount;
911
+ try {
912
+ pageCount = database.query("PRAGMA page_count").get()?.page_count ?? 0;
913
+ detail = database.query("PRAGMA integrity_check").get()?.integrity_check ?? "unknown";
914
+ } catch (error) {
915
+ throw new Error(`Backup is not a readable SQLite database: ${backupPath} (${error instanceof Error ? error.message : String(error)})`);
916
+ } finally {
917
+ database.close();
918
+ }
919
+ if (pageCount === 0)
920
+ throw new Error(`Backup is an empty SQLite database: ${backupPath}`);
921
+ if (detail !== "ok")
922
+ throw new Error(`Backup failed SQLite integrity_check: ${backupPath} (${detail})`);
923
+ }
924
+ async function restoreTargetPath(home = gatewayHome()) {
925
+ const configPath = join3(home, "config.json");
926
+ let raw;
927
+ try {
928
+ raw = await readFile3(configPath, "utf8");
929
+ } catch (error) {
930
+ const code = error.code;
931
+ const entryExists = await lstat(configPath).then(() => true, () => false);
932
+ if (code !== "ENOENT" || entryExists)
933
+ throw new Error(`Cannot read ${configPath}; refusing restore.`);
934
+ }
935
+ if (raw === undefined)
936
+ return join3(home, "gateway.db");
937
+ let parsed;
938
+ try {
939
+ parsed = JSON.parse(raw);
940
+ } catch {
941
+ throw new Error(`Cannot parse ${configPath}; refusing restore.`);
942
+ }
943
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
944
+ throw new Error(`Invalid configuration ${configPath}: config must be an object; refusing restore.`);
945
+ const dbPath = parsed.dbPath;
946
+ if (dbPath === undefined)
947
+ return join3(home, "gateway.db");
948
+ if (typeof dbPath !== "string" || dbPath.length === 0)
949
+ throw new Error(`Invalid configuration ${configPath}: dbPath must be a non-empty string; refusing restore.`);
950
+ return dbPath;
951
+ }
388
952
  async function restoreDatabase(socket, backupPath) {
389
953
  if (!isAbsolute(backupPath))
390
954
  throw new Error("ops restore requires an absolute backup path");
391
955
  try {
392
- await stat(socket);
956
+ await stat2(socket);
393
957
  throw new Error(`Refusing restore: gateway socket ${socket} exists. Stop the daemon before restoring.`);
394
958
  } catch (error) {
395
959
  if (error instanceof Error && error.message.startsWith("Refusing restore:"))
@@ -397,15 +961,8 @@ async function restoreDatabase(socket, backupPath) {
397
961
  if (error.code !== "ENOENT")
398
962
  throw new Error(`Cannot verify gateway socket ${socket}; refusing restore.`);
399
963
  }
400
- let header;
401
- try {
402
- header = new Uint8Array(await Bun.file(backupPath).slice(0, 16).arrayBuffer());
403
- } catch {
404
- throw new Error(`Backup is not readable: ${backupPath}`);
405
- }
406
- if (new TextDecoder().decode(header) !== "SQLite format 3\x00")
407
- throw new Error(`Backup is not a SQLite database: ${backupPath}`);
408
- const databasePath = join2(gatewayHome(), "gateway.db");
964
+ verifyBackupIntegrity(backupPath);
965
+ const databasePath = await restoreTargetPath();
409
966
  const preservedPath = `${databasePath}.pre-restore-${new Date().toISOString().replace(/[:.]/g, "-")}`;
410
967
  await copyFile(databasePath, preservedPath);
411
968
  await copyFile(backupPath, databasePath);
@@ -413,6 +970,7 @@ async function restoreDatabase(socket, backupPath) {
413
970
  console.log(`Copied current database to: ${preservedPath}`);
414
971
  console.log(`Restored backup to: ${databasePath}`);
415
972
  }
973
+ var CHAT_TURN_TIMEOUT_MS = 30 * 60000;
416
974
  async function chat(socket) {
417
975
  let client;
418
976
  try {
@@ -437,9 +995,9 @@ async function chat(socket) {
437
995
  await new Promise((resolve) => {
438
996
  const timer = setTimeout(() => {
439
997
  turnWaiters.delete(turnId);
440
- console.error("(turn timed out after 120s)");
998
+ console.error(`(turn timed out after ${CHAT_TURN_TIMEOUT_MS / 60000}m)`);
441
999
  resolve();
442
- }, 120000);
1000
+ }, CHAT_TURN_TIMEOUT_MS);
443
1001
  turnWaiters.set(turnId, () => {
444
1002
  clearTimeout(timer);
445
1003
  turnWaiters.delete(turnId);
@@ -549,10 +1107,51 @@ async function main(args = process.argv.slice(2), options = {}) {
549
1107
  await restoreDatabase(parsed.socket, path);
550
1108
  break;
551
1109
  }
1110
+ if (command === "restart-stack") {
1111
+ const usage2 = "usage: gajaeway ops restart-stack [--status]";
1112
+ const home = gatewayHome();
1113
+ const flag = parsed.rest[1];
1114
+ if (flag === "--run" && parsed.rest.length === 3 && parsed.rest[2]) {
1115
+ const receipt = await runRestartStack({
1116
+ ...options.restartStack?.run,
1117
+ home,
1118
+ id: parsed.rest[2]
1119
+ });
1120
+ for (const line of renderRestartReceipt(receipt))
1121
+ console.log(line);
1122
+ if (receipt.state !== "ok")
1123
+ process.exitCode = 1;
1124
+ } else if (flag === "--status" && parsed.rest.length === 2) {
1125
+ const receipt = await readRestartReceipt(home);
1126
+ if (receipt === undefined)
1127
+ throw new Error("no restart-stack receipt");
1128
+ for (const line of renderRestartReceipt(receipt))
1129
+ console.log(line);
1130
+ if (effectiveRestartState(receipt) !== "ok")
1131
+ process.exitCode = 1;
1132
+ } else if (parsed.rest.length === 1) {
1133
+ const { receipt, supervisorPid } = await launchRestartStack({ ...options.restartStack?.launch, home });
1134
+ console.log(`restart-stack ${receipt.id}: queued (supervisor pid ${supervisorPid})`);
1135
+ console.log("read the outcome with: gajaeway ops restart-stack --status");
1136
+ } else
1137
+ throw new Error(usage2);
1138
+ break;
1139
+ }
1140
+ let redeliverParams;
1141
+ if (command === "redeliver") {
1142
+ if (parsed.rest.length === 2 && path && path !== "--since")
1143
+ redeliverParams = { deliveryId: path };
1144
+ else if (parsed.rest.length === 3 && path === "--since" && parsed.rest[2] && Number.isFinite(Date.parse(parsed.rest[2])))
1145
+ redeliverParams = { since: parsed.rest[2] };
1146
+ else
1147
+ throw new Error("usage: gajaeway ops redeliver <deliveryId>|--since <iso>");
1148
+ }
552
1149
  const client = await GajaewayClient.connectSocket(parsed.socket);
553
1150
  try {
554
1151
  if (command === "backup" && path)
555
1152
  console.log(JSON.stringify(await client.request("ops.backup", { path })));
1153
+ else if (redeliverParams)
1154
+ console.log(JSON.stringify(await client.request("ops.redeliver", redeliverParams)));
556
1155
  else if (command === "integrity")
557
1156
  console.log(JSON.stringify(await client.request("ops.integrity")));
558
1157
  else if (command === "cycle") {
@@ -566,7 +1165,7 @@ async function main(args = process.argv.slice(2), options = {}) {
566
1165
  console.log(line);
567
1166
  process.exitCode = cycleExitCode(cycle);
568
1167
  } else
569
- throw new Error("usage: gajaeway ops backup <path>|cycle [--json]|integrity|restore <backupPath>");
1168
+ throw new Error("usage: gajaeway ops backup <path>|redeliver <deliveryId>|redeliver --since <iso>|cycle [--json]|integrity|restore <backupPath>|restart-stack [--status]");
570
1169
  } finally {
571
1170
  await client.close();
572
1171
  }
@@ -595,12 +1194,16 @@ async function main(args = process.argv.slice(2), options = {}) {
595
1194
  break;
596
1195
  }
597
1196
  case "monitors": {
598
- const listOptions = parsed.rest[0] === "list" ? parseListOptions(parsed.rest.slice(1), MONITOR_COLUMNS) : undefined;
1197
+ const [command, ...args2] = parsed.rest;
1198
+ const listOptions = command === "list" ? parseListOptions(args2, MONITOR_COLUMNS) : undefined;
1199
+ const update = command === "update" ? parseMonitorUpdateArgs(args2) : undefined;
1200
+ const monitorTest = command === "test" ? parseMonitorTestArgs(args2) : undefined;
599
1201
  const client = await GajaewayClient.connectSocket(parsed.socket);
600
1202
  try {
601
- const [command, ...args2] = parsed.rest;
602
1203
  if (command === "add" && args2[0] === "--json" && args2[1])
603
1204
  console.log(JSON.stringify(await client.request("monitor.add", JSON.parse(args2[1]))));
1205
+ else if (command === "update" && update)
1206
+ console.log(JSON.stringify(await client.request("monitor.update", update.params)));
604
1207
  else if (command === "list") {
605
1208
  const options2 = listOptions;
606
1209
  const result = await client.request("monitor.list");
@@ -613,22 +1216,20 @@ async function main(args = process.argv.slice(2), options = {}) {
613
1216
  console.log(JSON.stringify(await client.request("monitor.inspect", { monitorId: args2[0] })));
614
1217
  else if (command === "remove" && args2[0])
615
1218
  console.log(JSON.stringify(await client.request("monitor.remove", { monitorId: args2[0] })));
616
- else if (command === "test" && args2[0]) {
617
- let eventType;
618
- let payload = {};
619
- for (let i = 1;i < args2.length; i++) {
620
- if (args2[i] === "--type")
621
- eventType = args2[++i];
622
- else if (args2[i] === "--payload")
623
- payload = JSON.parse(args2[++i] ?? "");
1219
+ else if (command === "test" && monitorTest) {
1220
+ const result = await client.request("monitor.test", {
1221
+ monitorId: monitorTest.monitorId,
1222
+ ...monitorTest.eventType === undefined ? {} : { eventType: monitorTest.eventType },
1223
+ payload: monitorTest.payload
1224
+ });
1225
+ if (monitorTest.waitSeconds === undefined)
1226
+ console.log(JSON.stringify(result));
1227
+ else {
1228
+ const stage = await waitForMonitorStage(client, result.eventId, monitorTest.waitSeconds);
1229
+ console.log(JSON.stringify({ eventId: result.eventId, stage }));
624
1230
  }
625
- console.log(JSON.stringify(await client.request("monitor.test", {
626
- monitorId: args2[0],
627
- ...eventType ? { eventType } : {},
628
- payload
629
- })));
630
1231
  } else
631
- throw new Error(`usage: gajaeway monitors add --json '<MonitorSpec json>'|list [--json] [--fields ${columnNames(MONITOR_COLUMNS).join(",")}] [--limit N] [--offset N]|inspect <id>|remove <id>|test <id> [--type T] [--payload J]`);
1232
+ throw new Error(`usage: gajaeway monitors add --json '<MonitorSpec json>'|update <id> (--json '<partial MonitorSpec JSON>'|--schedule '<cron>' [--enabled true|false]|--enabled true|false [--schedule '<cron>'])|list [--json] [--fields ${columnNames(MONITOR_COLUMNS).join(",")}] [--limit N] [--offset N]|inspect <id>|remove <id>|test <id> [--type T] [--payload J] [--wait[=SECONDS]]`);
632
1233
  } finally {
633
1234
  await client.close();
634
1235
  }
@@ -636,7 +1237,7 @@ async function main(args = process.argv.slice(2), options = {}) {
636
1237
  }
637
1238
  case "work": {
638
1239
  const [command, ...args2] = parsed.rest;
639
- const usage2 = 'usage: gajaeway work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] [--notify originKey (start only)] "<task text>"|status <name>|steer <name> <text>|retire <name>|jobs';
1240
+ const usage2 = 'usage: gajaeway work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] "<task text>"|status <name>|steer <name> <text>|retire <name>|jobs';
640
1241
  if (command === "status" || command === "steer") {
641
1242
  const name2 = args2[0];
642
1243
  const text2 = args2.slice(1).join(" ").trim();
@@ -670,7 +1271,8 @@ async function main(args = process.argv.slice(2), options = {}) {
670
1271
  for (const job of result.jobs) {
671
1272
  const name2 = job.lane_key.startsWith("work-") ? job.lane_key.slice("work-".length) : job.lane_key;
672
1273
  const state = job.quarantined ? `HELD: quarantined reason=${job.reason} historical_state=${job.state}` : job.state;
673
- console.log(`${name2} ${state} session=${job.session_id || "-"} last=${job.last_activity_at || "-"} ${job.worktree_path}`);
1274
+ const head = job.last_commit ? `${job.last_commit.sha.slice(0, 7)}@${job.last_commit.committed_at} ${JSON.stringify(job.last_commit.subject)}` : "-";
1275
+ console.log(`${name2} ${state} session=${job.session_id || "-"} accepted=${job.accepted_at || "-"} op=${job.attempt?.op_ref || "-"} last=${job.last_activity_at || "-"} head=${head} ${job.worktree_path}${job.reports && (job.reports.pending !== 0 || job.reports.claimed !== 0 || job.reports.held !== 0 || job.reports.undeliverable !== 0) ? ` reports=p:${job.reports.pending} c:${job.reports.claimed} h:${job.reports.held} u:${job.reports.undeliverable}` : ""}`);
674
1276
  }
675
1277
  }
676
1278
  } finally {
@@ -684,11 +1286,10 @@ async function main(args = process.argv.slice(2), options = {}) {
684
1286
  let cwd;
685
1287
  let resume = false;
686
1288
  let model;
687
- let notify;
688
1289
  const textParts = [];
689
1290
  for (let i = 1;i < args2.length; i++) {
690
1291
  const arg = args2[i];
691
- if (arg === "--cwd" || arg === "--model" || arg === "--preset" || arg === "--notify") {
1292
+ if (arg === "--cwd" || arg === "--model" || arg === "--preset") {
692
1293
  const value = args2[++i];
693
1294
  if (!value?.trim() || value.startsWith("--"))
694
1295
  throw new Error(usage2);
@@ -696,15 +1297,6 @@ async function main(args = process.argv.slice(2), options = {}) {
696
1297
  if (cwd !== undefined || !isAbsolute(value))
697
1298
  throw new Error(usage2);
698
1299
  cwd = value;
699
- } else if (arg === "--notify") {
700
- if (command !== "start" || notify !== undefined)
701
- throw new Error(usage2);
702
- try {
703
- notify = parseOriginKey(value);
704
- } catch {
705
- throw new Error(`${usage2}
706
- invalid --notify originKey`);
707
- }
708
1300
  } else {
709
1301
  if (model !== undefined)
710
1302
  throw new Error(`${usage2}
@@ -725,13 +1317,14 @@ invalid --notify originKey`);
725
1317
  requestTimeoutMs: command === "run" ? 3600000 : 120000
726
1318
  });
727
1319
  try {
1320
+ const callerSessionId = process.env.GJC_SESSION_ID?.trim();
728
1321
  const params = {
729
1322
  name,
730
1323
  text,
731
1324
  ...cwd ? { cwd } : {},
732
1325
  ...resume ? { resume: true } : {},
733
1326
  ...model === undefined ? {} : { model },
734
- ...notify === undefined ? {} : { notify }
1327
+ ...callerSessionId ? { callerSessionId } : {}
735
1328
  };
736
1329
  if (command === "start") {
737
1330
  const result = await client.request("work.start", params);
@@ -761,10 +1354,14 @@ job: ${result.jobId} state: ${result.state}`);
761
1354
  const written = await installServices({
762
1355
  binDir: service.binDir,
763
1356
  ...service.launchAgentsDir === undefined ? {} : { launchAgentsDir: service.launchAgentsDir },
1357
+ ...service.unitDir === undefined ? {} : { unitDir: service.unitDir },
1358
+ ...service.platform === undefined ? {} : { platform: service.platform },
764
1359
  env: process.env,
765
1360
  ...options.services
766
1361
  });
767
- console.log(`services ${service.action}: wrote ${written.length} LaunchAgent plists`);
1362
+ console.log(`services ${service.action}: wrote ${written.length} service definitions`);
1363
+ for (const definition of written)
1364
+ console.log(definition);
768
1365
  break;
769
1366
  }
770
1367
  default:
@@ -787,6 +1384,8 @@ export {
787
1384
  parseServicesArgs,
788
1385
  renderCycle,
789
1386
  restoreDatabase,
1387
+ restoreTargetPath,
790
1388
  socketPath,
791
- usageFor
1389
+ usageFor,
1390
+ verifyBackupIntegrity
792
1391
  };