@bytetrue/byspace 0.14.6 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/commands/daemon/index.js +16 -0
  2. package/dist/commands/daemon/install-service.d.ts +15 -0
  3. package/dist/commands/daemon/install-service.js +69 -0
  4. package/dist/commands/daemon/local-daemon.d.ts +1 -0
  5. package/dist/commands/daemon/local-daemon.js +1 -1
  6. package/dist/commands/daemon/service/backend.d.ts +16 -0
  7. package/dist/commands/daemon/service/backend.js +50 -0
  8. package/dist/commands/daemon/service/launchd.d.ts +15 -0
  9. package/dist/commands/daemon/service/launchd.js +101 -0
  10. package/dist/commands/daemon/service/service-spec.d.ts +54 -0
  11. package/dist/commands/daemon/service/service-spec.js +95 -0
  12. package/dist/commands/daemon/service/service-status.d.ts +28 -0
  13. package/dist/commands/daemon/service/service-status.js +32 -0
  14. package/dist/commands/daemon/service/systemd.d.ts +22 -0
  15. package/dist/commands/daemon/service/systemd.js +90 -0
  16. package/dist/commands/daemon/service/windows-task.d.ts +12 -0
  17. package/dist/commands/daemon/service/windows-task.js +75 -0
  18. package/dist/commands/daemon/service-status.d.ts +13 -0
  19. package/dist/commands/daemon/service-status.js +30 -0
  20. package/dist/commands/daemon/uninstall-service.d.ts +11 -0
  21. package/dist/commands/daemon/uninstall-service.js +53 -0
  22. package/node_modules/@bytetrue/client/package.json +3 -3
  23. package/node_modules/@bytetrue/highlight/package.json +1 -1
  24. package/node_modules/@bytetrue/protocol/dist/generated/validation/ws-outbound.aot.js +10901 -10861
  25. package/node_modules/@bytetrue/protocol/dist/messages.d.ts +231 -92
  26. package/node_modules/@bytetrue/protocol/dist/messages.js +29 -0
  27. package/node_modules/@bytetrue/protocol/dist/validation/ws-outbound-schema-metadata.d.ts +44 -22
  28. package/node_modules/@bytetrue/protocol/package.json +1 -1
  29. package/node_modules/@bytetrue/relay/package.json +1 -1
  30. package/node_modules/@bytetrue/server/dist/server/server/byspace-env.d.ts +1 -0
  31. package/node_modules/@bytetrue/server/dist/server/server/byspace-env.js +9 -0
  32. package/node_modules/@bytetrue/server/dist/server/server/daemon-config-store.d.ts +11 -0
  33. package/node_modules/@bytetrue/server/dist/server/server/daemon-config-store.js +71 -1
  34. package/node_modules/@bytetrue/server/dist/server/server/session/daemon/daemon-service-install.d.ts +25 -0
  35. package/node_modules/@bytetrue/server/dist/server/server/session/daemon/daemon-service-install.js +182 -0
  36. package/node_modules/@bytetrue/server/dist/server/server/session/daemon/daemon-service-manager.d.ts +21 -0
  37. package/node_modules/@bytetrue/server/dist/server/server/session/daemon/daemon-service-manager.js +154 -0
  38. package/node_modules/@bytetrue/server/dist/server/server/websocket-server.js +2 -0
  39. package/node_modules/@bytetrue/server/dist/server/terminal/agent-hooks/pi/pi-extension.d.ts +1 -1
  40. package/node_modules/@bytetrue/server/dist/server/terminal/agent-hooks/pi/pi-extension.js +16 -1
  41. package/node_modules/@bytetrue/server/dist/server/terminal/terminal.js +6 -0
  42. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/{index-679e8946b965f52a7a27dc9c54f59a4c.js → index-eade2661e21bbc08a50d56f1c6b0917a.js} +537 -535
  43. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/index-eade2661e21bbc08a50d56f1c6b0917a.js.br +0 -0
  44. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/index-eade2661e21bbc08a50d56f1c6b0917a.js.gz +0 -0
  45. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-d61d5dd974e90216cec99e62c343ffc7.js → indexeddb-attachment-store-a2e8491be0374dc2a52abfe002ecabe9.js} +1 -1
  46. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-d61d5dd974e90216cec99e62c343ffc7.js.br → indexeddb-attachment-store-a2e8491be0374dc2a52abfe002ecabe9.js.br} +0 -0
  47. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-a2e8491be0374dc2a52abfe002ecabe9.js.gz +0 -0
  48. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/{native-file-attachment-store-df0c0ebe8d233a5a74eddb59a110f00b.js → native-file-attachment-store-6ba28513708667bbad7dd5434a0f8d5b.js} +3 -3
  49. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-6ba28513708667bbad7dd5434a0f8d5b.js.br +0 -0
  50. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-6ba28513708667bbad7dd5434a0f8d5b.js.gz +0 -0
  51. package/node_modules/@bytetrue/server/dist/server/web-ui/apple-touch-icon.png +0 -0
  52. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-dark-attention.9c2d51faf93468f68840811c0d9a8c6f.png +0 -0
  53. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-dark-running.a52155010955dca55546513b60bb2e2b.png +0 -0
  54. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-dark.ee7787d5c6bd1ad67c31b9949a832f7a.png +0 -0
  55. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-light-attention.9c2d51faf93468f68840811c0d9a8c6f.png +0 -0
  56. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-light-running.a52155010955dca55546513b60bb2e2b.png +0 -0
  57. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-light.ee7787d5c6bd1ad67c31b9949a832f7a.png +0 -0
  58. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/notification-icon.ba5a7dce236a7febe5d803b16f027b70.png +0 -0
  59. package/node_modules/@bytetrue/server/dist/server/web-ui/favicon.ico +0 -0
  60. package/node_modules/@bytetrue/server/dist/server/web-ui/index.html +1 -1
  61. package/node_modules/@bytetrue/server/dist/server/web-ui/index.html.br +0 -0
  62. package/node_modules/@bytetrue/server/dist/server/web-ui/index.html.gz +0 -0
  63. package/node_modules/@bytetrue/server/dist/server/web-ui/manifest.json +14 -2
  64. package/node_modules/@bytetrue/server/dist/server/web-ui/manifest.json.br +0 -0
  65. package/node_modules/@bytetrue/server/dist/server/web-ui/manifest.json.gz +0 -0
  66. package/node_modules/@bytetrue/server/dist/server/web-ui/pwa-icon-192.png +0 -0
  67. package/node_modules/@bytetrue/server/dist/server/web-ui/pwa-icon-512.png +0 -0
  68. package/node_modules/@bytetrue/server/dist/server/web-ui/pwa-maskable-192.png +0 -0
  69. package/node_modules/@bytetrue/server/dist/server/web-ui/pwa-maskable-512.png +0 -0
  70. package/node_modules/@bytetrue/server/dist/src/server/byspace-env.js +9 -0
  71. package/node_modules/@bytetrue/server/dist/src/server/daemon-config-store.js +71 -1
  72. package/node_modules/@bytetrue/server/dist/src/server/session/daemon/daemon-service-install.js +182 -0
  73. package/node_modules/@bytetrue/server/dist/src/server/session/daemon/daemon-service-manager.js +154 -0
  74. package/node_modules/@bytetrue/server/dist/src/server/session/daemon/install-origin.js +64 -0
  75. package/node_modules/@bytetrue/server/dist/src/server/session/daemon/npm-global-cli.js +98 -0
  76. package/node_modules/@bytetrue/server/dist/src/terminal/terminal.js +6 -0
  77. package/node_modules/@bytetrue/server/package.json +15 -5
  78. package/package.json +7 -7
  79. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/index-679e8946b965f52a7a27dc9c54f59a4c.js.br +0 -0
  80. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/index-679e8946b965f52a7a27dc9c54f59a4c.js.gz +0 -0
  81. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-d61d5dd974e90216cec99e62c343ffc7.js.gz +0 -0
  82. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-df0c0ebe8d233a5a74eddb59a110f00b.js.br +0 -0
  83. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-df0c0ebe8d233a5a74eddb59a110f00b.js.gz +0 -0
  84. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-dark-attention.882b3a27dcb2073e9e31b334f9ed9728.png +0 -0
  85. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-dark-running.8112342ff0d39e047a7f8d4fad9402f3.png +0 -0
  86. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-dark.8005ed36ac07a5a7c60de25780897bd4.png +0 -0
  87. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-light-attention.882b3a27dcb2073e9e31b334f9ed9728.png +0 -0
  88. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-light-running.8112342ff0d39e047a7f8d4fad9402f3.png +0 -0
  89. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-light.8005ed36ac07a5a7c60de25780897bd4.png +0 -0
  90. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/notification-icon.3bf81d33ddbf380606bdd248ba83e158.png +0 -0
@@ -3,6 +3,9 @@ import { startCommand } from "./start.js";
3
3
  import { runStatusCommand } from "./status.js";
4
4
  import { runStopCommand } from "./stop.js";
5
5
  import { runRestartCommand } from "./restart.js";
6
+ import { runInstallServiceCommand } from "./install-service.js";
7
+ import { runUninstallServiceCommand } from "./uninstall-service.js";
8
+ import { runServiceStatusCommand } from "./service-status.js";
6
9
  import { runSetPasswordCommand } from "./set-password.js";
7
10
  import { pairCommand } from "./pair.js";
8
11
  import { runDaemonReloadCommand } from "./reload.js";
@@ -55,6 +58,19 @@ export function createDaemonCommand() {
55
58
  .description("Prompt for and save a hashed daemon password to config.json"))
56
59
  .option("--home <path>", "BySpace home directory (default: ~/.byspace)")
57
60
  .action(withOutput(runSetPasswordCommand));
61
+ addJsonOption(daemon
62
+ .command("install-service")
63
+ .description("Register the daemon as a login service (macOS LaunchAgent)"))
64
+ .option("--home <path>", "BySpace home directory (default: ~/.byspace)")
65
+ .action(withOutput(runInstallServiceCommand));
66
+ addJsonOption(daemon.command("uninstall-service").description("Unregister the daemon login service"))
67
+ .option("--home <path>", "BySpace home directory (default: ~/.byspace)")
68
+ .action(withOutput(runUninstallServiceCommand));
69
+ addJsonOption(daemon
70
+ .command("service-status")
71
+ .description("Show whether the daemon is managed by the OS service manager"))
72
+ .option("--home <path>", "BySpace home directory (default: ~/.byspace)")
73
+ .action(withOutput(runServiceStatusCommand));
58
74
  return daemon;
59
75
  }
60
76
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,15 @@
1
+ import type { Command } from "commander";
2
+ import type { CommandOptions, SingleResult } from "../../output/index.js";
3
+ interface InstallServiceResult {
4
+ action: "installed";
5
+ label: string;
6
+ plistPath: string;
7
+ runnerEntry: string;
8
+ nodePath: string;
9
+ logPath: string;
10
+ message: string;
11
+ }
12
+ export type InstallServiceCommandResult = SingleResult<InstallServiceResult>;
13
+ export declare function runInstallServiceCommand(options: CommandOptions, _command: Command): Promise<InstallServiceCommandResult>;
14
+ export {};
15
+ //# sourceMappingURL=install-service.d.ts.map
@@ -0,0 +1,69 @@
1
+ import { buildDaemonServiceSpec, launchdPlistPath } from "./service/service-spec.js";
2
+ import { installLaunchdService } from "./service/launchd.js";
3
+ import { isRunningInDocker, validateServiceInstallOrigin } from "./service/service-status.js";
4
+ import { validateDaemonInstallOrigin, daemonInstallOriginRuntime, } from "@bytetrue/server/daemon-install-origin";
5
+ import { npmGlobalBySpaceCli, BYSPACE_CLI_PACKAGE } from "@bytetrue/server/npm-global-cli";
6
+ import { resolveLocalDaemonState, resolveDaemonRunnerEntry } from "./local-daemon.js";
7
+ const installServiceResultSchema = {
8
+ idField: "action",
9
+ columns: [
10
+ {
11
+ header: "STATUS",
12
+ field: "action",
13
+ color: () => "green",
14
+ },
15
+ { header: "LABEL", field: "label" },
16
+ { header: "PLIST", field: "plistPath" },
17
+ { header: "RUNNER", field: "runnerEntry" },
18
+ { header: "MESSAGE", field: "message" },
19
+ ],
20
+ };
21
+ export async function runInstallServiceCommand(options, _command) {
22
+ const home = typeof options.home === "string" ? options.home : undefined;
23
+ const state = resolveLocalDaemonState({ home });
24
+ try {
25
+ const dockerError = validateServiceInstallOrigin({
26
+ installOriginError: null,
27
+ isDocker: isRunningInDocker(),
28
+ });
29
+ if (dockerError)
30
+ throw new Error(dockerError);
31
+ const install = await npmGlobalBySpaceCli.inspect();
32
+ const originError = validateServiceInstallOrigin({
33
+ installOriginError: validateDaemonInstallOrigin(install, install.version, daemonInstallOriginRuntime),
34
+ isDocker: false,
35
+ });
36
+ if (originError)
37
+ throw new Error(originError);
38
+ const spec = buildDaemonServiceSpec({
39
+ home: state.home,
40
+ logPath: state.logPath,
41
+ runnerEntry: resolveDaemonRunnerEntry(),
42
+ env: process.env,
43
+ });
44
+ installLaunchdService(spec, state.home);
45
+ return {
46
+ type: "single",
47
+ data: {
48
+ action: "installed",
49
+ label: spec.label,
50
+ plistPath: launchdPlistPath(spec.label, state.home),
51
+ runnerEntry: spec.runnerEntry,
52
+ nodePath: spec.nodePath,
53
+ logPath: spec.logPath,
54
+ message: `Registered ${spec.label}; the daemon starts at login and survives terminal exit.`,
55
+ },
56
+ schema: installServiceResultSchema,
57
+ };
58
+ }
59
+ catch (err) {
60
+ const message = err instanceof Error ? err.message : String(err);
61
+ const error = {
62
+ code: "INSTALL_SERVICE_FAILED",
63
+ message: `Failed to install the daemon service: ${message}`,
64
+ details: `Only daemons running from the npm global ${BYSPACE_CLI_PACKAGE} install can be registered as a service.`,
65
+ };
66
+ throw error;
67
+ }
68
+ }
69
+ //# sourceMappingURL=install-service.js.map
@@ -66,6 +66,7 @@ export interface DaemonLaunchRuntime {
66
66
  }
67
67
  export declare const DEFAULT_STOP_TIMEOUT_MS = 15000;
68
68
  export declare const DEFAULT_KILL_TIMEOUT_MS = 3000;
69
+ export declare function resolveDaemonRunnerEntry(): string;
69
70
  export declare function resolveLocalBySpaceHome(home?: string): string;
70
71
  export declare function resolveTcpHostFromListen(listen: string): string | null;
71
72
  export declare function resolveLocalDaemonState(options?: {
@@ -98,7 +98,7 @@ function resolveServerRunnerFromDir(currentDir) {
98
98
  return null;
99
99
  }
100
100
  }
101
- function resolveDaemonRunnerEntry() {
101
+ export function resolveDaemonRunnerEntry() {
102
102
  const serverExportPath = require.resolve("@bytetrue/server");
103
103
  let currentDir = path.dirname(serverExportPath);
104
104
  while (true) {
@@ -0,0 +1,16 @@
1
+ import type { DaemonServiceSpec } from "./service-spec.js";
2
+ /**
3
+ * Which service-manager backend applies on this host. The command layer dispatches
4
+ * on this instead of scattering `process.platform` branches.
5
+ */
6
+ export type ServiceBackend = "launchd" | "systemd" | "windows-task" | "unsupported";
7
+ export declare function resolveServiceBackend(platform?: NodeJS.Platform): ServiceBackend;
8
+ export interface ServiceBackendApi {
9
+ install(spec: DaemonServiceSpec): void;
10
+ uninstall(label: string): void;
11
+ /** The pid the service manager reports, or null when not loaded/running. */
12
+ servicePid(label: string): number | null;
13
+ }
14
+ /** Backend dispatch. Each module is imported lazily so unused platform code never loads. */
15
+ export declare function serviceBackend(backend: ServiceBackend): ServiceBackendApi;
16
+ //# sourceMappingURL=backend.d.ts.map
@@ -0,0 +1,50 @@
1
+ export function resolveServiceBackend(platform = process.platform) {
2
+ switch (platform) {
3
+ case "darwin":
4
+ return "launchd";
5
+ case "linux":
6
+ return "systemd";
7
+ case "win32":
8
+ return "windows-task";
9
+ default:
10
+ return "unsupported";
11
+ }
12
+ }
13
+ /** Backend dispatch. Each module is imported lazily so unused platform code never loads. */
14
+ export function serviceBackend(backend) {
15
+ switch (backend) {
16
+ case "launchd": {
17
+ const launchd = require("./launchd.js");
18
+ return {
19
+ install: (spec) => {
20
+ const { resolveBySpaceHome } = require("@bytetrue/server");
21
+ launchd.installLaunchdService(spec, resolveBySpaceHome());
22
+ },
23
+ uninstall: (label) => {
24
+ const { resolveBySpaceHome } = require("@bytetrue/server");
25
+ launchd.uninstallLaunchdService(label, resolveBySpaceHome());
26
+ },
27
+ servicePid: (label) => launchd.launchdServicePid(label),
28
+ };
29
+ }
30
+ case "systemd": {
31
+ const systemd = require("./systemd.js");
32
+ return {
33
+ install: (spec) => systemd.installSystemdService(spec),
34
+ uninstall: (label) => systemd.uninstallSystemdService(label),
35
+ servicePid: (label) => systemd.systemdServicePid(label),
36
+ };
37
+ }
38
+ case "windows-task": {
39
+ const win = require("./windows-task.js");
40
+ return {
41
+ install: (spec) => win.installWindowsTask(spec),
42
+ uninstall: (label) => win.uninstallWindowsTask(label),
43
+ servicePid: () => null,
44
+ };
45
+ }
46
+ default:
47
+ throw new Error("This platform has no supported service manager integration (expected macOS, Linux, or Windows).");
48
+ }
49
+ }
50
+ //# sourceMappingURL=backend.js.map
@@ -0,0 +1,15 @@
1
+ import type { DaemonServiceSpec } from "./service-spec.js";
2
+ /**
3
+ * Render the launchd plist XML for the spec. KeepAlive is deliberately false: the
4
+ * supervisor already restarts crashed workers, and launchd KeepAlive would resurrect
5
+ * the daemon right after `byspace daemon stop`, breaking stop semantics. RunAtLoad
6
+ * provides the "start at login" behavior.
7
+ */
8
+ export declare function renderLaunchdPlist(spec: DaemonServiceSpec): string;
9
+ /** Write the plist and load it with `launchctl bootstrap gui/<uid>`. */
10
+ export declare function installLaunchdService(spec: DaemonServiceSpec, home: string): void;
11
+ /** Unload and delete the plist. Idempotent: succeeds when nothing is installed. */
12
+ export declare function uninstallLaunchdService(label: string, home: string): void;
13
+ /** Query the pid launchd reports for the label, or null when not loaded. */
14
+ export declare function launchdServicePid(label: string): number | null;
15
+ //# sourceMappingURL=launchd.d.ts.map
@@ -0,0 +1,101 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { launchdPlistPath } from "./service-spec.js";
5
+ /**
6
+ * Render the launchd plist XML for the spec. KeepAlive is deliberately false: the
7
+ * supervisor already restarts crashed workers, and launchd KeepAlive would resurrect
8
+ * the daemon right after `byspace daemon stop`, breaking stop semantics. RunAtLoad
9
+ * provides the "start at login" behavior.
10
+ */
11
+ export function renderLaunchdPlist(spec) {
12
+ const programArgs = [spec.nodePath, spec.runnerEntry, ...spec.runnerArgs];
13
+ const args = programArgs
14
+ .map((a) => `<string>${escapeXml(a)}</string>`)
15
+ .map((s) => ` ${s}`)
16
+ .join("\n");
17
+ const envEntries = Object.entries(spec.env)
18
+ .map(([key, value]) => ` <key>${escapeXml(key)}</key>\n <string>${escapeXml(value)}</string>`)
19
+ .join("\n");
20
+ return `<?xml version="1.0" encoding="UTF-8"?>
21
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
22
+ <plist version="1.0">
23
+ <dict>
24
+ <key>Label</key>
25
+ <string>${escapeXml(spec.label)}</string>
26
+ <key>ProgramArguments</key>
27
+ <array>
28
+ ${args}
29
+ </array>
30
+ <key>EnvironmentVariables</key>
31
+ <dict>
32
+ ${envEntries}
33
+ </dict>
34
+ <key>RunAtLoad</key>
35
+ <true/>
36
+ <key>KeepAlive</key>
37
+ <false/>
38
+ <key>StandardOutPath</key>
39
+ <string>${escapeXml(spec.logPath)}</string>
40
+ <key>StandardErrorPath</key>
41
+ <string>${escapeXml(spec.logPath)}</string>
42
+ <key>ProcessType</key>
43
+ <string>Background</string>
44
+ </dict>
45
+ </plist>
46
+ `;
47
+ }
48
+ function escapeXml(value) {
49
+ return value
50
+ .replace(/&/g, "&amp;")
51
+ .replace(/</g, "&lt;")
52
+ .replace(/>/g, "&gt;")
53
+ .replace(/"/g, "&quot;");
54
+ }
55
+ function launchctl(args, input) {
56
+ const result = spawnSync("launchctl", args, {
57
+ encoding: "utf8",
58
+ input,
59
+ timeout: 15000,
60
+ });
61
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
62
+ return { ok: result.status === 0, output };
63
+ }
64
+ /** Write the plist and load it with `launchctl bootstrap gui/<uid>`. */
65
+ export function installLaunchdService(spec, home) {
66
+ const plistPath = launchdPlistPath(spec.label, home);
67
+ mkdirSync(path.dirname(plistPath), { recursive: true });
68
+ writeFileSync(plistPath, renderLaunchdPlist(spec), { mode: 0o644 });
69
+ const uid = process.getuid?.() ??
70
+ (() => {
71
+ throw new Error("cannot resolve uid");
72
+ })();
73
+ // bootout first so re-installing over a loaded service is idempotent.
74
+ launchctl(["bootout", `gui/${uid}/${spec.label}`]);
75
+ const boot = launchctl(["bootstrap", `gui/${String(uid)}`, plistPath]);
76
+ if (!boot.ok) {
77
+ throw new Error(`launchctl bootstrap failed: ${boot.output}`);
78
+ }
79
+ }
80
+ /** Unload and delete the plist. Idempotent: succeeds when nothing is installed. */
81
+ export function uninstallLaunchdService(label, home) {
82
+ const uid = process.getuid?.() ??
83
+ (() => {
84
+ throw new Error("cannot resolve uid");
85
+ })();
86
+ launchctl(["bootout", `gui/${uid}/${label}`]);
87
+ const plistPath = launchdPlistPath(label, home);
88
+ rmSync(plistPath, { force: true });
89
+ }
90
+ /** Query the pid launchd reports for the label, or null when not loaded. */
91
+ export function launchdServicePid(label) {
92
+ const uid = process.getuid?.();
93
+ if (uid === undefined)
94
+ return null;
95
+ const { ok, output } = launchctl(["print", `gui/${uid}/${label}`]);
96
+ if (!ok)
97
+ return null;
98
+ const match = output.match(/^\s*pid\s*=\s*(\d+)/m);
99
+ return match ? Number(match[1]) : null;
100
+ }
101
+ //# sourceMappingURL=launchd.js.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Platform-agnostic description of the daemon service definition. Platform backends
3
+ * (launchd/systemd/Task Scheduler) translate this into their native format.
4
+ *
5
+ * Everything here must be an absolute path or an exact value: service managers do not
6
+ * read shell profiles, so nothing may depend on the login environment at run time.
7
+ */
8
+ export interface DaemonServiceSpec {
9
+ /** Reverse-DNS service label, e.g. `cc.cd.byspace.daemon`. */
10
+ label: string;
11
+ /** Absolute path to the node executable that runs the daemon. */
12
+ nodePath: string;
13
+ /** Absolute path to the daemon runner entry (supervisor entrypoint). */
14
+ runnerEntry: string;
15
+ /** Extra arguments passed to the runner. */
16
+ runnerArgs: string[];
17
+ /** Absolute path to the daemon log file. */
18
+ logPath: string;
19
+ /** Environment variables snapshotted at install time. */
20
+ env: Record<string, string>;
21
+ }
22
+ /**
23
+ * Home directories that macOS `launchctl bootstrap gui/<uid>` reads LaunchAgents from,
24
+ * in check order. XDG overrides the legacy path when it exists.
25
+ */
26
+ export declare function launchAgentDirs(home: string): string[];
27
+ /** Absolute plist path for the label, resolved against the first existing dir. */
28
+ export declare function launchdPlistPath(label: string, home: string): string;
29
+ /**
30
+ * True when a plist for the label exists in any LaunchAgents dir. Presence alone does
31
+ * not mean the service is loaded — see `readLaunchdPlist`.
32
+ */
33
+ export declare function launchdPlistExists(label: string, home: string): boolean;
34
+ /** Read and parse the installed plist for the label, or null when absent/unparsable. */
35
+ export declare function readLaunchdPlist(label: string, home: string): {
36
+ path: string;
37
+ program: string[];
38
+ env: Record<string, string>;
39
+ } | null;
40
+ /**
41
+ * Collect the env vars the daemon needs to survive in a bare launchd environment:
42
+ * BYSPACE_* overrides plus a PATH that can resolve agent binaries (pi/claude/...) and
43
+ * git. Snapshot from the *installing* shell — that is the environment that worked.
44
+ */
45
+ export declare function snapshotServiceEnv(env: NodeJS.ProcessEnv): Record<string, string>;
46
+ /** Build the ServiceSpec for the current process. Throws when prerequisites are missing. */
47
+ export declare function buildDaemonServiceSpec(options: {
48
+ home: string;
49
+ logPath: string;
50
+ runnerEntry: string;
51
+ runnerArgs?: string[];
52
+ env?: NodeJS.ProcessEnv;
53
+ }): DaemonServiceSpec;
54
+ //# sourceMappingURL=service-spec.d.ts.map
@@ -0,0 +1,95 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ /**
5
+ * Home directories that macOS `launchctl bootstrap gui/<uid>` reads LaunchAgents from,
6
+ * in check order. XDG overrides the legacy path when it exists.
7
+ */
8
+ export function launchAgentDirs(home) {
9
+ const xdg = process.env.XDG_CONFIG_HOME;
10
+ const dirs = [];
11
+ if (xdg)
12
+ dirs.push(path.join(xdg, "LaunchAgents"));
13
+ dirs.push(path.join(home, "Library", "LaunchAgents"));
14
+ return dirs;
15
+ }
16
+ /** Absolute plist path for the label, resolved against the first existing dir. */
17
+ export function launchdPlistPath(label, home) {
18
+ for (const dir of launchAgentDirs(home)) {
19
+ if (existsSync(dir))
20
+ return path.join(dir, `${label}.plist`);
21
+ }
22
+ return path.join(home, "Library", "LaunchAgents", `${label}.plist`);
23
+ }
24
+ /**
25
+ * True when a plist for the label exists in any LaunchAgents dir. Presence alone does
26
+ * not mean the service is loaded — see `readLaunchdPlist`.
27
+ */
28
+ export function launchdPlistExists(label, home) {
29
+ return launchAgentDirs(home).some((dir) => existsSync(path.join(dir, `${label}.plist`)));
30
+ }
31
+ /** Read and parse the installed plist for the label, or null when absent/unparsable. */
32
+ export function readLaunchdPlist(label, home) {
33
+ for (const dir of launchAgentDirs(home)) {
34
+ const plistPath = path.join(dir, `${label}.plist`);
35
+ if (!existsSync(plistPath))
36
+ continue;
37
+ try {
38
+ const doc = parsePlistJson(plistPath);
39
+ const program = doc.ProgramArguments ?? [];
40
+ const envDict = doc.EnvironmentVariables ?? {};
41
+ return { path: plistPath, program, env: envDict };
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ /** Parse a plist via the macOS-native `plutil -convert json` (no npm dependency). */
50
+ function parsePlistJson(plistPath) {
51
+ const result = spawnSync("plutil", ["-convert", "json", "-o", "-", plistPath], {
52
+ encoding: "utf8",
53
+ timeout: 10000,
54
+ });
55
+ if (result.status !== 0)
56
+ throw new Error(`plutil failed: ${result.stderr ?? ""}`);
57
+ return JSON.parse(result.stdout);
58
+ }
59
+ /**
60
+ * Collect the env vars the daemon needs to survive in a bare launchd environment:
61
+ * BYSPACE_* overrides plus a PATH that can resolve agent binaries (pi/claude/...) and
62
+ * git. Snapshot from the *installing* shell — that is the environment that worked.
63
+ */
64
+ export function snapshotServiceEnv(env) {
65
+ const snapshot = {};
66
+ for (const [key, value] of Object.entries(env)) {
67
+ if (key.startsWith("BYSPACE_") && value !== undefined)
68
+ snapshot[key] = value;
69
+ }
70
+ snapshot.PATH = env.PATH ?? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
71
+ return snapshot;
72
+ }
73
+ /** Build the ServiceSpec for the current process. Throws when prerequisites are missing. */
74
+ export function buildDaemonServiceSpec(options) {
75
+ const env = options.env ?? process.env;
76
+ const nodePath = process.execPath;
77
+ if (!nodePath.startsWith("/")) {
78
+ throw new Error(`node executable is not an absolute path: ${nodePath}`);
79
+ }
80
+ if (!options.runnerEntry || !path.isAbsolute(options.runnerEntry)) {
81
+ throw new Error(`daemon runner entry must be an absolute path, got: ${options.runnerEntry || "(empty)"}`);
82
+ }
83
+ if (!existsSync(options.runnerEntry)) {
84
+ throw new Error(`daemon runner entry does not exist: ${options.runnerEntry}`);
85
+ }
86
+ return {
87
+ label: "cc.cd.byspace.daemon",
88
+ nodePath,
89
+ runnerEntry: options.runnerEntry,
90
+ runnerArgs: options.runnerArgs ?? [],
91
+ logPath: options.logPath,
92
+ env: snapshotServiceEnv(env),
93
+ };
94
+ }
95
+ //# sourceMappingURL=service-spec.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Service management state. Deliberately distinct from "is a daemon running": the
3
+ * service definition can exist while the daemon runs from a terminal instead, and a
4
+ * stale plist can exist with nothing behind it. Report what is verifiable; the UI must
5
+ * not claim "managed" without the pid matching.
6
+ */
7
+ export type DaemonServiceState = "not-installed" | "installed-stopped" | "installed-running-not-this-process" | "managed-by-service" | "unknown";
8
+ export interface DaemonServiceStatus {
9
+ state: DaemonServiceState;
10
+ label: string;
11
+ /** Pid launchd reports for the service, when loaded. */
12
+ servicePid: number | null;
13
+ /** Pid of the currently running process (the caller's own daemon pid). */
14
+ currentPid: number | null;
15
+ }
16
+ export declare function resolveLaunchdServiceStatus(label: string, home: string, currentPid: number | null): DaemonServiceStatus;
17
+ /**
18
+ * Guard for install: only a daemon running from the published npm global install may
19
+ * register itself as a service, otherwise the service would point at a temporary
20
+ * checkout (dev) or a container filesystem (Docker) and break on the next reboot.
21
+ */
22
+ export declare function validateServiceInstallOrigin(options: {
23
+ installOriginError: string | null;
24
+ isDocker: boolean;
25
+ }): string | null;
26
+ /** True when running inside a Docker container. */
27
+ export declare function isRunningInDocker(): boolean;
28
+ //# sourceMappingURL=service-status.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { existsSync } from "node:fs";
2
+ import { launchdPlistExists } from "./service-spec.js";
3
+ import { launchdServicePid } from "./launchd.js";
4
+ export function resolveLaunchdServiceStatus(label, home, currentPid) {
5
+ if (!launchdPlistExists(label, home)) {
6
+ return { state: "not-installed", label, servicePid: null, currentPid };
7
+ }
8
+ const servicePid = launchdServicePid(label);
9
+ if (servicePid === null) {
10
+ return { state: "installed-stopped", label, servicePid: null, currentPid };
11
+ }
12
+ if (currentPid !== null && servicePid === currentPid) {
13
+ return { state: "managed-by-service", label, servicePid, currentPid };
14
+ }
15
+ return { state: "installed-running-not-this-process", label, servicePid, currentPid };
16
+ }
17
+ /**
18
+ * Guard for install: only a daemon running from the published npm global install may
19
+ * register itself as a service, otherwise the service would point at a temporary
20
+ * checkout (dev) or a container filesystem (Docker) and break on the next reboot.
21
+ */
22
+ export function validateServiceInstallOrigin(options) {
23
+ if (options.isDocker) {
24
+ return "Registering a service is not supported inside Docker. Use the container runtime's restart policy instead.";
25
+ }
26
+ return options.installOriginError;
27
+ }
28
+ /** True when running inside a Docker container. */
29
+ export function isRunningInDocker() {
30
+ return existsSync("/.dockerenv") || existsSync("/run/.containerenv");
31
+ }
32
+ //# sourceMappingURL=service-status.js.map
@@ -0,0 +1,22 @@
1
+ import type { DaemonServiceSpec } from "./service-spec.js";
2
+ /** Absolute path of the user unit for the label. */
3
+ export declare function systemdUnitPath(label: string): string;
4
+ /**
5
+ * Render the systemd user unit. Restart=on-failure, never always: `always` would
6
+ * resurrect the daemon after `byspace daemon stop`, breaking stop semantics. The
7
+ * supervisor's own crash restart handles worker failures. WantedBy=default.target
8
+ * is the user-level equivalent of "start at login".
9
+ */
10
+ export declare function renderSystemdUnit(spec: DaemonServiceSpec): string;
11
+ /** Write the unit, reload the manager, and enable --now the service. */
12
+ export declare function installSystemdService(spec: DaemonServiceSpec): void;
13
+ /** Stop, disable, and delete the unit. Idempotent. */
14
+ export declare function uninstallSystemdService(label: string): void;
15
+ /** MainPID from `systemctl --user show`, or null when the unit is not loaded. */
16
+ export declare function systemdServicePid(label: string): number | null;
17
+ /**
18
+ * Whether the user lingers (user manager runs without an active login session).
19
+ * Without lingering the service stops at logout — report it, never silently degrade.
20
+ */
21
+ export declare function isLingerEnabled(user: string): boolean;
22
+ //# sourceMappingURL=systemd.d.ts.map
@@ -0,0 +1,90 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { homedir } from "node:os";
5
+ /** Absolute path of the user unit for the label. */
6
+ export function systemdUnitPath(label) {
7
+ const configHome = process.env.XDG_CONFIG_HOME ?? path.join(homedir(), ".config");
8
+ return path.join(configHome, "systemd", "user", `${label}.service`);
9
+ }
10
+ /**
11
+ * Render the systemd user unit. Restart=on-failure, never always: `always` would
12
+ * resurrect the daemon after `byspace daemon stop`, breaking stop semantics. The
13
+ * supervisor's own crash restart handles worker failures. WantedBy=default.target
14
+ * is the user-level equivalent of "start at login".
15
+ */
16
+ export function renderSystemdUnit(spec) {
17
+ const execStart = [spec.nodePath, spec.runnerEntry, ...spec.runnerArgs]
18
+ .map((part) => part.replace(/([\\"])/g, "\\$1"))
19
+ .join(" ");
20
+ const envLines = Object.entries(spec.env)
21
+ .map(([key, value]) => `Environment="${key}=${value.replace(/([\\"])/g, "\\$1")}"`)
22
+ .join("\n");
23
+ return `# Generated by 'byspace daemon install-service'. Edits are overwritten on reinstall.
24
+ [Unit]
25
+ Description=BySpace daemon
26
+ After=network-online.target
27
+ Wants=network-online.target
28
+
29
+ [Service]
30
+ Type=exec
31
+ ExecStart=${execStart}
32
+ ${envLines}
33
+ Restart=on-failure
34
+ RestartSec=5
35
+
36
+ [Install]
37
+ WantedBy=default.target
38
+ `;
39
+ }
40
+ function systemctl(args) {
41
+ const result = spawnSync("systemctl", ["--user", ...args], {
42
+ encoding: "utf8",
43
+ timeout: 20000,
44
+ });
45
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
46
+ return { ok: result.status === 0, output };
47
+ }
48
+ /** Write the unit, reload the manager, and enable --now the service. */
49
+ export function installSystemdService(spec) {
50
+ const unitPath = systemdUnitPath(spec.label);
51
+ mkdirSync(path.dirname(unitPath), { recursive: true });
52
+ writeFileSync(unitPath, renderSystemdUnit(spec), { mode: 0o644 });
53
+ const daemonReload = systemctl(["daemon-reload"]);
54
+ if (!daemonReload.ok) {
55
+ throw new Error(`systemctl --user daemon-reload failed: ${daemonReload.output}`);
56
+ }
57
+ const enable = systemctl(["enable", "--now", `${spec.label}.service`]);
58
+ if (!enable.ok) {
59
+ throw new Error(`systemctl --user enable --now failed: ${enable.output}`);
60
+ }
61
+ }
62
+ /** Stop, disable, and delete the unit. Idempotent. */
63
+ export function uninstallSystemdService(label) {
64
+ systemctl(["disable", "--now", `${label}.service`]);
65
+ rmSync(systemdUnitPath(label), { force: true });
66
+ systemctl(["daemon-reload"]);
67
+ }
68
+ /** MainPID from `systemctl --user show`, or null when the unit is not loaded. */
69
+ export function systemdServicePid(label) {
70
+ const { ok, output } = systemctl(["show", `${label}.service`, "--property", "MainPID"]);
71
+ if (!ok)
72
+ return null;
73
+ const match = output.match(/^MainPID=(\d+)$/m);
74
+ if (!match)
75
+ return null;
76
+ const pid = Number(match[1]);
77
+ return pid > 0 ? pid : null;
78
+ }
79
+ /**
80
+ * Whether the user lingers (user manager runs without an active login session).
81
+ * Without lingering the service stops at logout — report it, never silently degrade.
82
+ */
83
+ export function isLingerEnabled(user) {
84
+ const result = spawnSync("loginctl", ["show-user", user, "--property", "Linger"], {
85
+ encoding: "utf8",
86
+ timeout: 10000,
87
+ });
88
+ return result.status === 0 && /Linger=yes/.test(result.stdout ?? "");
89
+ }
90
+ //# sourceMappingURL=systemd.js.map
@@ -0,0 +1,12 @@
1
+ import type { DaemonServiceSpec } from "./service-spec.js";
2
+ /** The scheduled task name for the daemon (folder-qualified for the API surface). */
3
+ export declare function scheduledTaskName(label: string): string;
4
+ /** Build the Register-ScheduledTask argument vector. Exposed for tests. */
5
+ export declare function buildRegisterArgs(spec: DaemonServiceSpec): string[];
6
+ /** Register the scheduled task. Env vars are snapshotted into the task definition. */
7
+ export declare function installWindowsTask(spec: DaemonServiceSpec): void;
8
+ /** Unregister the task. Idempotent: succeeds when nothing is registered. */
9
+ export declare function uninstallWindowsTask(label: string): void;
10
+ /** The pid of the daemon process the task last launched, via task state. */
11
+ export declare function windowsTaskPid(label: string): number | null;
12
+ //# sourceMappingURL=windows-task.d.ts.map