@the-librarian/cli 1.0.0-rc.21

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 (155) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +94 -0
  3. package/dist/bin.d.ts +3 -0
  4. package/dist/bin.d.ts.map +1 -0
  5. package/dist/bin.js +15 -0
  6. package/dist/bin.js.map +1 -0
  7. package/dist/commands/install.d.ts +28 -0
  8. package/dist/commands/install.d.ts.map +1 -0
  9. package/dist/commands/install.js +204 -0
  10. package/dist/commands/install.js.map +1 -0
  11. package/dist/commands/shared.d.ts +16 -0
  12. package/dist/commands/shared.d.ts.map +1 -0
  13. package/dist/commands/shared.js +30 -0
  14. package/dist/commands/shared.js.map +1 -0
  15. package/dist/commands/uninstall.d.ts +18 -0
  16. package/dist/commands/uninstall.d.ts.map +1 -0
  17. package/dist/commands/uninstall.js +67 -0
  18. package/dist/commands/uninstall.js.map +1 -0
  19. package/dist/commands/update.d.ts +21 -0
  20. package/dist/commands/update.d.ts.map +1 -0
  21. package/dist/commands/update.js +75 -0
  22. package/dist/commands/update.js.map +1 -0
  23. package/dist/config.d.ts +40 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +65 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/doctor.d.ts +23 -0
  28. package/dist/doctor.d.ts.map +1 -0
  29. package/dist/doctor.js +81 -0
  30. package/dist/doctor.js.map +1 -0
  31. package/dist/env.d.ts +39 -0
  32. package/dist/env.d.ts.map +1 -0
  33. package/dist/env.js +208 -0
  34. package/dist/env.js.map +1 -0
  35. package/dist/exec.d.ts +37 -0
  36. package/dist/exec.d.ts.map +1 -0
  37. package/dist/exec.js +81 -0
  38. package/dist/exec.js.map +1 -0
  39. package/dist/harnesses/claude.d.ts +3 -0
  40. package/dist/harnesses/claude.d.ts.map +1 -0
  41. package/dist/harnesses/claude.js +88 -0
  42. package/dist/harnesses/claude.js.map +1 -0
  43. package/dist/harnesses/cli.d.ts +4 -0
  44. package/dist/harnesses/cli.d.ts.map +1 -0
  45. package/dist/harnesses/cli.js +16 -0
  46. package/dist/harnesses/cli.js.map +1 -0
  47. package/dist/harnesses/codex.d.ts +3 -0
  48. package/dist/harnesses/codex.d.ts.map +1 -0
  49. package/dist/harnesses/codex.js +183 -0
  50. package/dist/harnesses/codex.js.map +1 -0
  51. package/dist/harnesses/hermes.d.ts +16 -0
  52. package/dist/harnesses/hermes.d.ts.map +1 -0
  53. package/dist/harnesses/hermes.js +158 -0
  54. package/dist/harnesses/hermes.js.map +1 -0
  55. package/dist/harnesses/index.d.ts +12 -0
  56. package/dist/harnesses/index.d.ts.map +1 -0
  57. package/dist/harnesses/index.js +29 -0
  58. package/dist/harnesses/index.js.map +1 -0
  59. package/dist/harnesses/opencode.d.ts +3 -0
  60. package/dist/harnesses/opencode.d.ts.map +1 -0
  61. package/dist/harnesses/opencode.js +141 -0
  62. package/dist/harnesses/opencode.js.map +1 -0
  63. package/dist/harnesses/pi.d.ts +3 -0
  64. package/dist/harnesses/pi.d.ts.map +1 -0
  65. package/dist/harnesses/pi.js +72 -0
  66. package/dist/harnesses/pi.js.map +1 -0
  67. package/dist/harnesses/types.d.ts +48 -0
  68. package/dist/harnesses/types.d.ts.map +1 -0
  69. package/dist/harnesses/types.js +22 -0
  70. package/dist/harnesses/types.js.map +1 -0
  71. package/dist/index.d.ts +11 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +15 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/machine.d.ts +10 -0
  76. package/dist/machine.d.ts.map +1 -0
  77. package/dist/machine.js +39 -0
  78. package/dist/machine.js.map +1 -0
  79. package/dist/parse-args.d.ts +12 -0
  80. package/dist/parse-args.d.ts.map +1 -0
  81. package/dist/parse-args.js +54 -0
  82. package/dist/parse-args.js.map +1 -0
  83. package/dist/paths.d.ts +32 -0
  84. package/dist/paths.d.ts.map +1 -0
  85. package/dist/paths.js +75 -0
  86. package/dist/paths.js.map +1 -0
  87. package/dist/prompt.d.ts +53 -0
  88. package/dist/prompt.d.ts.map +1 -0
  89. package/dist/prompt.js +165 -0
  90. package/dist/prompt.js.map +1 -0
  91. package/dist/runtime.d.ts +45 -0
  92. package/dist/runtime.d.ts.map +1 -0
  93. package/dist/runtime.js +370 -0
  94. package/dist/runtime.js.map +1 -0
  95. package/dist/semver.d.ts +13 -0
  96. package/dist/semver.d.ts.map +1 -0
  97. package/dist/semver.js +98 -0
  98. package/dist/semver.js.map +1 -0
  99. package/dist/server/admin.d.ts +31 -0
  100. package/dist/server/admin.d.ts.map +1 -0
  101. package/dist/server/admin.js +110 -0
  102. package/dist/server/admin.js.map +1 -0
  103. package/dist/server/boot.d.ts +51 -0
  104. package/dist/server/boot.d.ts.map +1 -0
  105. package/dist/server/boot.js +233 -0
  106. package/dist/server/boot.js.map +1 -0
  107. package/dist/server/deploy-state.d.ts +33 -0
  108. package/dist/server/deploy-state.d.ts.map +1 -0
  109. package/dist/server/deploy-state.js +79 -0
  110. package/dist/server/deploy-state.js.map +1 -0
  111. package/dist/server/docker.d.ts +37 -0
  112. package/dist/server/docker.d.ts.map +1 -0
  113. package/dist/server/docker.js +138 -0
  114. package/dist/server/docker.js.map +1 -0
  115. package/dist/server/down.d.ts +19 -0
  116. package/dist/server/down.d.ts.map +1 -0
  117. package/dist/server/down.js +62 -0
  118. package/dist/server/down.js.map +1 -0
  119. package/dist/server/index.d.ts +8 -0
  120. package/dist/server/index.d.ts.map +1 -0
  121. package/dist/server/index.js +48 -0
  122. package/dist/server/index.js.map +1 -0
  123. package/dist/server/logs.d.ts +50 -0
  124. package/dist/server/logs.d.ts.map +1 -0
  125. package/dist/server/logs.js +162 -0
  126. package/dist/server/logs.js.map +1 -0
  127. package/dist/server/preflight.d.ts +24 -0
  128. package/dist/server/preflight.d.ts.map +1 -0
  129. package/dist/server/preflight.js +58 -0
  130. package/dist/server/preflight.js.map +1 -0
  131. package/dist/server/redact.d.ts +2 -0
  132. package/dist/server/redact.d.ts.map +1 -0
  133. package/dist/server/redact.js +26 -0
  134. package/dist/server/redact.js.map +1 -0
  135. package/dist/server/status.d.ts +20 -0
  136. package/dist/server/status.d.ts.map +1 -0
  137. package/dist/server/status.js +103 -0
  138. package/dist/server/status.js.map +1 -0
  139. package/dist/server/up.d.ts +211 -0
  140. package/dist/server/up.d.ts.map +1 -0
  141. package/dist/server/up.js +625 -0
  142. package/dist/server/up.js.map +1 -0
  143. package/dist/server/update.d.ts +35 -0
  144. package/dist/server/update.d.ts.map +1 -0
  145. package/dist/server/update.js +308 -0
  146. package/dist/server/update.js.map +1 -0
  147. package/dist/status.d.ts +18 -0
  148. package/dist/status.d.ts.map +1 -0
  149. package/dist/status.js +113 -0
  150. package/dist/status.js.map +1 -0
  151. package/dist/version.d.ts +2 -0
  152. package/dist/version.d.ts.map +1 -0
  153. package/dist/version.js +20 -0
  154. package/dist/version.js.map +1 -0
  155. package/package.json +36 -0
package/dist/semver.js ADDED
@@ -0,0 +1,98 @@
1
+ // A tiny, dependency-light semver-ish comparison.
2
+ //
3
+ // We only need "is the installed version behind the latest release?" — not a
4
+ // full spec-compliant semver implementation. This tolerates the prerelease
5
+ // shapes the monorepo actually ships (`1.0.0`, `1.0.0-rc.2`, `v1.2.3`) and
6
+ // degrades gracefully on anything it can't parse (returns 0 → "equal", so an
7
+ // unparseable version is never reported as an update).
8
+ //
9
+ // Precedence rules (subset of semver §11):
10
+ // - numeric release fields compare numerically (1.2.0 < 1.10.0);
11
+ // - a version WITH a prerelease has LOWER precedence than the same version
12
+ // WITHOUT one (1.0.0-rc.1 < 1.0.0);
13
+ // - prerelease identifiers compare left-to-right; numeric identifiers
14
+ // compare numerically, others lexically, numeric < non-numeric.
15
+ /** Strip a leading `v` and any build-metadata (`+…`) we don't compare on. */
16
+ function parse(version) {
17
+ const trimmed = version.trim().replace(/^v/i, "");
18
+ if (!trimmed)
19
+ return null;
20
+ const [core, ...preParts] = trimmed.split("-");
21
+ // Drop build metadata (`+sha`) from the prerelease tail and the core.
22
+ const prerelease = preParts.join("-").split("+")[0] ?? "";
23
+ const coreNoBuild = (core ?? "").split("+")[0] ?? "";
24
+ const release = coreNoBuild.split(".").map((n) => Number.parseInt(n, 10));
25
+ if (release.length === 0 || release.some((n) => Number.isNaN(n)))
26
+ return null;
27
+ return {
28
+ release,
29
+ prerelease: prerelease.length > 0 ? prerelease.split(".") : [],
30
+ };
31
+ }
32
+ /**
33
+ * Compare two version strings. Returns <0 if `a` precedes `b`, >0 if `a`
34
+ * follows `b`, 0 if equal or either is unparseable (so an unknown version
35
+ * never reads as "behind").
36
+ */
37
+ export function compareVersions(a, b) {
38
+ const pa = parse(a);
39
+ const pb = parse(b);
40
+ if (!pa || !pb)
41
+ return 0;
42
+ const len = Math.max(pa.release.length, pb.release.length);
43
+ for (let i = 0; i < len; i += 1) {
44
+ const da = pa.release[i] ?? 0;
45
+ const db = pb.release[i] ?? 0;
46
+ if (da !== db)
47
+ return da < db ? -1 : 1;
48
+ }
49
+ // Equal release fields → a version with a prerelease is LOWER than one
50
+ // without (1.0.0-rc.1 < 1.0.0).
51
+ const aPre = pa.prerelease.length > 0;
52
+ const bPre = pb.prerelease.length > 0;
53
+ if (aPre && !bPre)
54
+ return -1;
55
+ if (!aPre && bPre)
56
+ return 1;
57
+ if (!aPre && !bPre)
58
+ return 0;
59
+ return comparePrerelease(pa.prerelease, pb.prerelease);
60
+ }
61
+ function comparePrerelease(a, b) {
62
+ const len = Math.max(a.length, b.length);
63
+ for (let i = 0; i < len; i += 1) {
64
+ // A longer prerelease list (with all-else-equal prefixes) has higher
65
+ // precedence (rc.1 < rc.1.1).
66
+ if (i >= a.length)
67
+ return -1;
68
+ if (i >= b.length)
69
+ return 1;
70
+ const ai = a[i] ?? "";
71
+ const bi = b[i] ?? "";
72
+ const an = /^\d+$/.test(ai);
73
+ const bn = /^\d+$/.test(bi);
74
+ if (an && bn) {
75
+ const na = Number.parseInt(ai, 10);
76
+ const nb = Number.parseInt(bi, 10);
77
+ if (na !== nb)
78
+ return na < nb ? -1 : 1;
79
+ continue;
80
+ }
81
+ if (an !== bn)
82
+ return an ? -1 : 1; // numeric identifiers rank below alphanumeric
83
+ if (ai !== bi)
84
+ return ai < bi ? -1 : 1;
85
+ }
86
+ return 0;
87
+ }
88
+ /**
89
+ * True iff `installed` is strictly behind `latest`. Either side being unknown
90
+ * (empty / unparseable) yields `false` — the caller renders `?`, never a
91
+ * false "update available".
92
+ */
93
+ export function isBehind(installed, latest) {
94
+ if (!installed || !latest)
95
+ return false;
96
+ return compareVersions(installed, latest) < 0;
97
+ }
98
+ //# sourceMappingURL=semver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semver.js","sourceRoot":"","sources":["../src/semver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /** The curated admin verbs exposed under `server admin` (spec §7). */
2
+ export declare const ADMIN_VERBS: readonly ["backup", "restore", "auth", "rebuild"];
3
+ export type AdminVerb = (typeof ADMIN_VERBS)[number];
4
+ /** A teaching error from `admin`; the runtime renders `.message` as one stderr line. */
5
+ export declare class AdminError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ export interface AdminOptions {
9
+ /** Platform for preflight's daemon hint. Default `process.platform`. */
10
+ platform?: NodeJS.Platform | undefined;
11
+ /**
12
+ * Whether the run is interactive (a TTY is attached). When true we add
13
+ * `docker exec -it` so the in-container CLI can prompt (e.g. `restore`'s
14
+ * `--secret-key` prompt, `auth reset-password`). A non-interactive run omits
15
+ * `-it` so it works in scripts/CI without a TTY.
16
+ */
17
+ interactive?: boolean | undefined;
18
+ }
19
+ export interface AdminResult {
20
+ /** Human-readable report for stdout (the in-container CLI's own output). */
21
+ output: string;
22
+ }
23
+ /**
24
+ * Run `server admin <verb> [args…]`. Validates the verb is curated, preflights
25
+ * docker, confirms the container is running, then runs
26
+ * `docker exec [-it] the-librarian the-librarian <verb> [args…]` — passing the
27
+ * args through VERBATIM. A rejected verb / down container is a teaching
28
+ * `AdminError`; no `docker exec` runs in either case.
29
+ */
30
+ export declare function runAdmin(argv: readonly string[], options?: AdminOptions): Promise<AdminResult>;
31
+ //# sourceMappingURL=admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../../src/server/admin.ts"],"names":[],"mappings":"AA+BA,sEAAsE;AACtE,eAAO,MAAM,WAAW,mDAAoD,CAAC;AAE7E,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,wFAAwF;AACxF,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,GAAG,SAAS,CAAC;IACvC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACnC;AAED,MAAM,WAAW,WAAW;IAC1B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;CAChB;AAyCD;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,WAAW,CAAC,CAqCtB"}
@@ -0,0 +1,110 @@
1
+ // `librarian server admin <verb> [args…]` — run a curated subset of the
2
+ // folded-in admin CLI (`@librarian/cli`, the `the-librarian` binary) INSIDE the
3
+ // running all-in-one container. Spec §7 ("Folded-in admin").
4
+ //
5
+ // One uniform mechanism: `docker exec the-librarian the-librarian <verb> [args…]`.
6
+ // Running it in the container (not on the host) gives the admin CLI direct
7
+ // access to the data dir + the store/settings the server already uses — which is
8
+ // exactly what lets `auth` recovery bypass a locked dashboard and lets
9
+ // `backup`/`restore` reuse the store rather than re-implement it. (The CLI is
10
+ // bundled into the runtime image in the same slice — see all-in-one.Dockerfile.)
11
+ //
12
+ // CURATED set (spec §7): only `backup | restore | auth | rebuild` are exposed.
13
+ // `seed` / `migrate-data-dir` / `export` / `handoffs` exist in @librarian/cli but
14
+ // are deliberately NOT folded in here (seed is dev-only; migrate-data-dir runs
15
+ // automatically on `update`; export + handoffs are power-user/dashboard surfaces).
16
+ // They stay reachable via a raw `docker exec` if ever truly needed.
17
+ //
18
+ // Args after the verb are passed through VERBATIM, in order — this module never
19
+ // parses, reorders, or rewrites them (so e.g. `auth reset-password --user x` and
20
+ // `restore --secret-key …` reach the CLI exactly as typed). A bearer/secret may
21
+ // ride through as an arg the CLI itself reads; this module never logs or persists
22
+ // it (AGENTS.md: privacy beats convenience).
23
+ //
24
+ // Everything goes through the injectable `docker.ts` seam, so tests assert the
25
+ // exact argv without a real daemon or container.
26
+ import { run } from "./docker.js";
27
+ import { preflight } from "./preflight.js";
28
+ import { redactSecrets } from "./redact.js";
29
+ import { CONTAINER_NAME } from "./up.js";
30
+ /** The curated admin verbs exposed under `server admin` (spec §7). */
31
+ export const ADMIN_VERBS = ["backup", "restore", "auth", "rebuild"];
32
+ /** A teaching error from `admin`; the runtime renders `.message` as one stderr line. */
33
+ export class AdminError extends Error {
34
+ constructor(message) {
35
+ super(message);
36
+ this.name = "AdminError";
37
+ }
38
+ }
39
+ /** True iff `verb` is one of the curated admin verbs. */
40
+ function isAdminVerb(verb) {
41
+ return ADMIN_VERBS.includes(verb);
42
+ }
43
+ /** The teaching message for an unknown/dropped verb — names what IS available. */
44
+ function unknownVerbMessage(verb) {
45
+ const allowed = ADMIN_VERBS.join(" | ");
46
+ const lead = verb === undefined
47
+ ? "`librarian server admin` needs a subcommand."
48
+ : `\`${verb}\` is not an \`admin\` subcommand.`;
49
+ return [
50
+ lead,
51
+ "",
52
+ `Allowed: ${allowed}.`,
53
+ "",
54
+ "Not exposed here (by design): `seed` (dev-only), `migrate-data-dir` (runs",
55
+ "automatically on `librarian server update`), `export`, and `handoffs`",
56
+ "(power-user / dashboard surfaces).",
57
+ ].join("\n");
58
+ }
59
+ /**
60
+ * Confirm the container is up before exec'ing into it. `docker inspect` reports
61
+ * `running` when it is; any other status (or a failed inspect → absent) is a
62
+ * teaching error pointing at `server up`, with NO `docker exec`.
63
+ */
64
+ async function assertContainerRunning() {
65
+ const result = await run("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME]);
66
+ const status = result.code === 0 ? result.stdout.trim() : null;
67
+ if (status !== "running") {
68
+ throw new AdminError("The server isn't running — run `librarian server up` first, then re-run " +
69
+ "this admin command.");
70
+ }
71
+ }
72
+ /**
73
+ * Run `server admin <verb> [args…]`. Validates the verb is curated, preflights
74
+ * docker, confirms the container is running, then runs
75
+ * `docker exec [-it] the-librarian the-librarian <verb> [args…]` — passing the
76
+ * args through VERBATIM. A rejected verb / down container is a teaching
77
+ * `AdminError`; no `docker exec` runs in either case.
78
+ */
79
+ export async function runAdmin(argv, options = {}) {
80
+ const [verb, ...rest] = argv;
81
+ // Validate FIRST — a rejected verb must never reach a `docker exec` (and we
82
+ // don't even need a daemon to tell the user the verb is wrong).
83
+ if (verb === undefined || !isAdminVerb(verb)) {
84
+ throw new AdminError(unknownVerbMessage(verb));
85
+ }
86
+ await preflight(options.platform ? { platform: options.platform } : {});
87
+ await assertContainerRunning();
88
+ // `docker exec [-it] the-librarian the-librarian <verb> [args…]` — args verbatim.
89
+ const execArgs = [
90
+ "exec",
91
+ ...(options.interactive ? ["-it"] : []),
92
+ CONTAINER_NAME,
93
+ "the-librarian",
94
+ verb,
95
+ ...rest,
96
+ ];
97
+ const result = await run("docker", execArgs);
98
+ if (result.code !== 0) {
99
+ // `admin` forwards secret-bearing args (`restore --secret-key …`, `auth …`),
100
+ // so a failed in-container step can echo them back. Redact before surfacing,
101
+ // identically to `update`/`up` (I-3) — the shared helper is the choke point.
102
+ const detail = redactSecrets(result.stderr.trim() || result.stdout.trim());
103
+ throw new AdminError(`\`the-librarian ${verb}\` failed in the container (exit ${result.code ?? "signal"})` +
104
+ (detail ? `:\n${detail}` : ".") +
105
+ `\n\nResolve the error above, then re-run \`librarian server admin ${verb}\`.`);
106
+ }
107
+ // The in-container CLI's own output is the report (stdout, then any stderr).
108
+ return { output: result.stdout + result.stderr };
109
+ }
110
+ //# sourceMappingURL=admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.js","sourceRoot":"","sources":["../../src/server/admin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /** The systemd unit file name (single instance per host). */
2
+ export declare const UNIT_NAME = "the-librarian.service";
3
+ /** The absolute path the unit is installed at (the system unit, via sudo). */
4
+ export declare function unitPath(): string;
5
+ export interface BootOptions {
6
+ /** Platform — gates the macOS deferral. Default `process.platform`. */
7
+ platform?: NodeJS.Platform | undefined;
8
+ }
9
+ /** A teaching error from boot ops; the runtime renders `.message` as one stderr line. */
10
+ export declare class BootError extends Error {
11
+ constructor(message: string);
12
+ }
13
+ export interface BootResult {
14
+ /** Human-readable report for stdout. */
15
+ output: string;
16
+ }
17
+ /** Inputs to the pure unit generator (only a docker path — NEVER a secret). */
18
+ export interface UnitInput {
19
+ /** The absolute path to the `docker` binary used in ExecStart/ExecStop. */
20
+ dockerPath: string;
21
+ }
22
+ /**
23
+ * Generate the systemd unit text. PURE + unit-tested. The headline invariant:
24
+ * it contains NO secret — no agent token, no `LIBRARIAN_AGENT_TOKEN`, no
25
+ * `docker run` / `-e` env injection — only a `docker start --attach` /
26
+ * `docker stop` of the EXISTING named container. The container already carries
27
+ * its env (incl. the token) in Docker's own state, so referencing it by name
28
+ * needs no secret in this file.
29
+ */
30
+ export declare function generateUnit(input: UnitInput): string;
31
+ /** The one-line macOS-deferred notice (shared by enable/disable + `up`). */
32
+ export declare const MACOS_NOTICE: string;
33
+ /**
34
+ * Enable boot persistence. On Linux: write the unit to `/etc/systemd/system`
35
+ * (root, so temp-file + `sudo cp` + `sudo chmod 644`), `sudo systemctl
36
+ * daemon-reload`, then `sudo systemctl enable --now the-librarian.service`.
37
+ * Idempotent — re-running rewrites the SAME unit path and re-enables, never
38
+ * duplicating. On macOS: print the deferred notice and skip cleanly (NOT an
39
+ * error).
40
+ */
41
+ export declare function enableBoot(options?: BootOptions): Promise<BootResult>;
42
+ /**
43
+ * Disable boot persistence. On Linux: `sudo systemctl disable --now
44
+ * the-librarian.service`, remove the unit file (`sudo rm -f`), then `sudo
45
+ * systemctl daemon-reload`. Safe if already disabled/absent — a non-zero
46
+ * `disable` (unit not loaded) becomes a friendly teaching message, never a
47
+ * crash; the `rm -f` + reload still run so a stale unit file is cleaned up. On
48
+ * macOS: print the deferred notice and skip cleanly.
49
+ */
50
+ export declare function disableBoot(options?: BootOptions): Promise<BootResult>;
51
+ //# sourceMappingURL=boot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot.d.ts","sourceRoot":"","sources":["../../src/server/boot.ts"],"names":[],"mappings":"AAoCA,6DAA6D;AAC7D,eAAO,MAAM,SAAS,0BAA0B,CAAC;AAKjD,8EAA8E;AAC9E,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAKD,MAAM,WAAW,WAAW;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,GAAG,SAAS,CAAC;CACxC;AAED,yFAAyF;AACzF,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,UAAU;IACzB,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,+EAA+E;AAC/E,MAAM,WAAW,SAAS;IACxB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CA+BrD;AAED,4EAA4E;AAC5E,eAAO,MAAM,YAAY,QAGsC,CAAC;AAkChE;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CA2B/E;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CA+BhF"}
@@ -0,0 +1,233 @@
1
+ // `librarian server enable-boot` / `disable-boot` — Linux systemd boot
2
+ // persistence for the all-in-one container. (macOS launchd is deferred — §9.)
3
+ //
4
+ // SECURITY — the whole point of this slice. The systemd unit MUST NOT contain
5
+ // any secret. We do NOT bake a `docker run -e LIBRARIAN_AGENT_TOKEN=…` into
6
+ // `ExecStart`: that would write the agent token into a world-readable
7
+ // `/etc/systemd/system/*.service` file — a leak. Instead the unit references the
8
+ // EXISTING named container (created by `up`/`update`, whose env — including the
9
+ // token — lives in Docker's own container state, not a file we manage):
10
+ //
11
+ // ExecStart=/usr/bin/docker start --attach the-librarian
12
+ // ExecStop=/usr/bin/docker stop the-librarian
13
+ //
14
+ // `docker start --attach` re-uses the container exactly as `up` created it
15
+ // (same name, same env, same volume) and stays in the foreground so systemd's
16
+ // `Type=simple` supervision + `Restart=always` work. This survives a `server
17
+ // update` (which recreates the SAME container name) and reboots, with NO secret
18
+ // anywhere on disk that we write.
19
+ //
20
+ // We default to a SYSTEM unit (`/etc/systemd/system/…`, via `sudo`) because it
21
+ // boots reliably without a login session. (A `--user` unit under
22
+ // `~/.config/systemd/user/` would need `loginctl enable-linger` to survive
23
+ // logout — out of scope for v1; deliberately not built.)
24
+ //
25
+ // Writing to `/etc/systemd/system` needs root, so the unit is written to a
26
+ // temp file first (NON-SECRET content) and then `sudo cp`'d into place — every
27
+ // privileged step (`sudo cp`/`chmod`/`rm`, `sudo systemctl …`) routes through
28
+ // the injectable `docker.ts` runner so tests assert the exact argv and never
29
+ // touch real systemd, sudo, or docker.
30
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
31
+ import { tmpdir } from "node:os";
32
+ import path from "node:path";
33
+ import { run, which } from "./docker.js";
34
+ import { CONTAINER_NAME } from "./up.js";
35
+ /** The systemd unit file name (single instance per host). */
36
+ export const UNIT_NAME = "the-librarian.service";
37
+ /** The system-unit directory (a system unit boots without a login session). */
38
+ const SYSTEMD_SYSTEM_DIR = "/etc/systemd/system";
39
+ /** The absolute path the unit is installed at (the system unit, via sudo). */
40
+ export function unitPath() {
41
+ return path.join(SYSTEMD_SYSTEM_DIR, UNIT_NAME);
42
+ }
43
+ /** Fallback docker path if `which docker` can't be resolved (well-known location). */
44
+ const DEFAULT_DOCKER_PATH = "/usr/bin/docker";
45
+ /** A teaching error from boot ops; the runtime renders `.message` as one stderr line. */
46
+ export class BootError extends Error {
47
+ constructor(message) {
48
+ super(message);
49
+ this.name = "BootError";
50
+ }
51
+ }
52
+ /**
53
+ * Generate the systemd unit text. PURE + unit-tested. The headline invariant:
54
+ * it contains NO secret — no agent token, no `LIBRARIAN_AGENT_TOKEN`, no
55
+ * `docker run` / `-e` env injection — only a `docker start --attach` /
56
+ * `docker stop` of the EXISTING named container. The container already carries
57
+ * its env (incl. the token) in Docker's own state, so referencing it by name
58
+ * needs no secret in this file.
59
+ */
60
+ export function generateUnit(input) {
61
+ const { dockerPath } = input;
62
+ return [
63
+ "[Unit]",
64
+ "Description=The Librarian (all-in-one)",
65
+ "After=docker.service",
66
+ "Requires=docker.service",
67
+ "",
68
+ "[Service]",
69
+ "Type=simple",
70
+ // SC5 (ADR 0008) — env-file POINTER, deliberately a comment, not a directive.
71
+ // `docker start --attach` re-uses the container's env exactly as `up`/`update`
72
+ // baked it from <deployDir>/deploy.env (0600); it does NOT read systemd's
73
+ // `Environment*`. A live `EnvironmentFile=` here would be silently ignored —
74
+ // misleading config — so this stays a comment that just tells a reader where
75
+ // the credentials live, with no value of any kind written into this file.
76
+ `# Container env (incl. credentials) is baked into the '${CONTAINER_NAME}' container`,
77
+ "# at `librarian server up`/`update`, sourced from the 0600 deploy.env in the",
78
+ "# deploy dir (default ~/.librarian/server). `docker start` re-uses that frozen",
79
+ "# env, so nothing sensitive is (or should be) written into this unit file.",
80
+ // Reference the EXISTING named container — NO `docker run`, NO `-e <secret>`.
81
+ // `--attach` keeps it in the foreground so Type=simple supervision works.
82
+ `ExecStart=${dockerPath} start --attach ${CONTAINER_NAME}`,
83
+ `ExecStop=${dockerPath} stop ${CONTAINER_NAME}`,
84
+ "Restart=always",
85
+ "RestartSec=5",
86
+ "",
87
+ "[Install]",
88
+ "WantedBy=multi-user.target",
89
+ "",
90
+ ].join("\n");
91
+ }
92
+ /** The one-line macOS-deferred notice (shared by enable/disable + `up`). */
93
+ export const MACOS_NOTICE = "Boot persistence is Linux-only for now (launchd support is deferred). " +
94
+ "Skipping — on macOS, start the server manually with `librarian server up`, " +
95
+ "or use Docker Desktop's 'start on login' for the container.";
96
+ /** True iff boot persistence is unsupported on this platform (macOS for now). */
97
+ function isUnsupportedPlatform(platform) {
98
+ return platform === "darwin";
99
+ }
100
+ /**
101
+ * Boot-specific preflight: `docker` must be on PATH (the unit runs `docker
102
+ * start` at boot). Unlike the container-op preflight this does NOT require git
103
+ * or a reachable daemon — enabling/disabling a unit file needs neither. A
104
+ * missing docker is a teaching `BootError`.
105
+ */
106
+ async function requireDocker() {
107
+ if ((await which("docker")) === null) {
108
+ throw new BootError("Docker is required but was not found on your PATH. " +
109
+ "The boot unit runs `docker start` — install Docker Engine (Linux) " +
110
+ "— see https://docs.docker.com/get-docker/ — then re-run this command.");
111
+ }
112
+ }
113
+ /**
114
+ * Resolve the absolute `docker` path for the unit's ExecStart. Goes through the
115
+ * injectable runner (`which docker`); falls back to the well-known
116
+ * `/usr/bin/docker` so a non-standard `which` output never breaks unit
117
+ * generation. The path is non-secret, so a fallback is safe.
118
+ */
119
+ async function resolveDockerPath() {
120
+ const resolved = await which("docker");
121
+ return resolved && resolved.trim().length > 0 ? resolved.trim() : DEFAULT_DOCKER_PATH;
122
+ }
123
+ /**
124
+ * Enable boot persistence. On Linux: write the unit to `/etc/systemd/system`
125
+ * (root, so temp-file + `sudo cp` + `sudo chmod 644`), `sudo systemctl
126
+ * daemon-reload`, then `sudo systemctl enable --now the-librarian.service`.
127
+ * Idempotent — re-running rewrites the SAME unit path and re-enables, never
128
+ * duplicating. On macOS: print the deferred notice and skip cleanly (NOT an
129
+ * error).
130
+ */
131
+ export async function enableBoot(options = {}) {
132
+ const platform = options.platform ?? process.platform;
133
+ if (isUnsupportedPlatform(platform)) {
134
+ return { output: MACOS_NOTICE };
135
+ }
136
+ // Boot persistence runs `docker start` at boot, so it needs `docker` on PATH
137
+ // — but NOT git (no clone here) and NOT a reachable daemon right now (the unit
138
+ // fires at boot, not at enable time). A lighter, boot-specific preflight.
139
+ await requireDocker();
140
+ const dockerPath = await resolveDockerPath();
141
+ const unit = generateUnit({ dockerPath });
142
+ await installUnit(unit);
143
+ // daemon-reload BEFORE enable --now so systemd sees the (possibly rewritten)
144
+ // unit. enable --now both enables-on-boot and starts the container now.
145
+ await sudoSystemctl(["daemon-reload"]);
146
+ await sudoSystemctl(["enable", "--now", UNIT_NAME]);
147
+ return {
148
+ output: [
149
+ `Boot persistence enabled — ${UNIT_NAME} will start the container on boot.`,
150
+ ` Unit: ${unitPath()} (references the existing '${CONTAINER_NAME}' container; no secret in the file)`,
151
+ "Disable it again with `librarian server disable-boot`.",
152
+ ].join("\n"),
153
+ };
154
+ }
155
+ /**
156
+ * Disable boot persistence. On Linux: `sudo systemctl disable --now
157
+ * the-librarian.service`, remove the unit file (`sudo rm -f`), then `sudo
158
+ * systemctl daemon-reload`. Safe if already disabled/absent — a non-zero
159
+ * `disable` (unit not loaded) becomes a friendly teaching message, never a
160
+ * crash; the `rm -f` + reload still run so a stale unit file is cleaned up. On
161
+ * macOS: print the deferred notice and skip cleanly.
162
+ */
163
+ export async function disableBoot(options = {}) {
164
+ const platform = options.platform ?? process.platform;
165
+ if (isUnsupportedPlatform(platform)) {
166
+ return { output: MACOS_NOTICE };
167
+ }
168
+ await requireDocker();
169
+ // disable --now: tolerate "unit does not exist / not loaded" — already off.
170
+ const disable = await run("sudo", ["systemctl", "disable", "--now", UNIT_NAME]);
171
+ const alreadyAbsent = disable.code !== 0 && isUnitAbsent(disable.stderr);
172
+ if (disable.code !== 0 && !alreadyAbsent) {
173
+ failIfNonZero("sudo", ["systemctl", "disable", "--now", UNIT_NAME], disable);
174
+ }
175
+ // Remove the unit file (idempotent with `-f`), then daemon-reload so systemd
176
+ // forgets it. `rm -f` never errors on an absent file.
177
+ await sudoRun(["rm", "-f", unitPath()]);
178
+ await sudoSystemctl(["daemon-reload"]);
179
+ return {
180
+ output: alreadyAbsent
181
+ ? [
182
+ `Boot persistence was not enabled (${UNIT_NAME} was not loaded) — nothing to disable.`,
183
+ `Removed any stale unit file at ${unitPath()} and reloaded systemd.`,
184
+ ].join("\n")
185
+ : [
186
+ `Boot persistence disabled — ${UNIT_NAME} removed and systemd reloaded.`,
187
+ "The container itself is untouched; stop it with `librarian server down`.",
188
+ ].join("\n"),
189
+ };
190
+ }
191
+ // --- privileged steps (every one through the injectable runner) ----------
192
+ /**
193
+ * Write the unit to the system path. `/etc/systemd/system` needs root, so the
194
+ * NON-SECRET unit text is written to a temp file first, then `sudo cp`'d into
195
+ * place and `sudo chmod 644`'d (world-readable is fine — there is no secret in
196
+ * it, by construction). The temp file is removed afterward. The `cp`/`chmod`
197
+ * argv is asserted by tests; the temp source carries the unit content for the
198
+ * test's no-secret check (the fake runner doesn't really move it).
199
+ */
200
+ async function installUnit(unit) {
201
+ const dir = mkdtempSync(path.join(tmpdir(), "librarian-unit-"));
202
+ const tmp = path.join(dir, UNIT_NAME);
203
+ writeFileSync(tmp, unit, "utf8");
204
+ try {
205
+ await sudoRun(["cp", tmp, unitPath()]);
206
+ await sudoRun(["chmod", "644", unitPath()]);
207
+ }
208
+ finally {
209
+ rmSync(dir, { recursive: true, force: true });
210
+ }
211
+ }
212
+ /** Run `sudo systemctl <args…>`; a non-zero exit is a teaching error. */
213
+ async function sudoSystemctl(args) {
214
+ await sudoRun(["systemctl", ...args]);
215
+ }
216
+ /** Run `sudo <args…>`; a non-zero exit is a teaching error. */
217
+ async function sudoRun(args) {
218
+ const result = await run("sudo", args);
219
+ failIfNonZero("sudo", args, result);
220
+ }
221
+ /** True when a `systemctl disable` failure means the unit simply isn't loaded. */
222
+ function isUnitAbsent(stderr) {
223
+ return /does not exist|not loaded|no such file|not found/i.test(stderr);
224
+ }
225
+ function failIfNonZero(cmd, args, result) {
226
+ if (result.code === 0)
227
+ return;
228
+ const detail = result.stderr.trim() || result.stdout.trim();
229
+ throw new BootError(`\`${cmd} ${args[0]}\` failed (exit ${result.code ?? "signal"})` +
230
+ (detail ? `:\n${detail}` : ".") +
231
+ "\n\nResolve the error above (you may need sudo privileges), then re-run the command.");
232
+ }
233
+ //# sourceMappingURL=boot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot.js","sourceRoot":"","sources":["../../src/server/boot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The deploy-state recorded after a successful `up` (and rewritten by `update`).
3
+ * Every field is NON-SECRET — see the module header. Do NOT add a token/key.
4
+ */
5
+ export interface DeployState {
6
+ /** The container name every `server` command operates on. */
7
+ containerName: string;
8
+ /** The resolved bind host the container publishes on (`127.0.0.1`, a tailnet IP, `0.0.0.0`). */
9
+ host: string;
10
+ /** The named data volume mounted at `/data` (sacred across `down`/`update`). */
11
+ dataVolume: string;
12
+ /** The deployed ref — a `vX.Y.Z` tag or `main` (what was checked out + built). */
13
+ ref: string;
14
+ /** The built image tag (`the-librarian:<ref>`). */
15
+ imageTag: string;
16
+ }
17
+ /** `<dir>/deploy-state.json` — the deploy-state file path within a deploy dir. */
18
+ export declare function deployStatePath(dir: string): string;
19
+ /**
20
+ * Write the deploy-state to `<dir>/deploy-state.json`, creating `dir` if absent.
21
+ *
22
+ * Only the five declared fields are persisted — a `pick`, not a spread — so no
23
+ * extra key (e.g. a smuggled token) can ride along into the file. The file is
24
+ * non-secret, so it gets ordinary (not 0600) permissions.
25
+ */
26
+ export declare function writeDeployState(dir: string, state: DeployState): void;
27
+ /**
28
+ * Read the deploy-state back, or `null` if the file is absent, unparseable, or
29
+ * missing any required field. Never throws — a missing/corrupt state file means
30
+ * the caller falls back (e.g. `status` uses `git describe`), never crashes.
31
+ */
32
+ export declare function readDeployState(dir: string): DeployState | null;
33
+ //# sourceMappingURL=deploy-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploy-state.d.ts","sourceRoot":"","sources":["../../src/server/deploy-state.ts"],"names":[],"mappings":"AAmBA;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,6DAA6D;IAC7D,aAAa,EAAE,MAAM,CAAC;IACtB,gGAAgG;IAChG,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAKD,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAYtE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAyB/D"}