@edspencer/paddock 0.79.1 → 0.80.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 (96) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/app.js +18 -14
  3. package/packages/server/dist/cli/args.d.ts +47 -7
  4. package/packages/server/dist/cli/args.js +112 -18
  5. package/packages/server/dist/cli/exit-codes.d.ts +49 -0
  6. package/packages/server/dist/cli/exit-codes.js +48 -0
  7. package/packages/server/dist/cli/paddock.js +84 -37
  8. package/packages/server/dist/cli/service/backend.d.ts +48 -4
  9. package/packages/server/dist/cli/service/index.d.ts +56 -2
  10. package/packages/server/dist/cli/service/index.js +283 -56
  11. package/packages/server/dist/cli/service/launchd.d.ts +8 -0
  12. package/packages/server/dist/cli/service/launchd.js +29 -7
  13. package/packages/server/dist/cli/service/logs.d.ts +39 -0
  14. package/packages/server/dist/cli/service/logs.js +115 -0
  15. package/packages/server/dist/cli/service/path-env.d.ts +27 -0
  16. package/packages/server/dist/cli/service/path-env.js +120 -0
  17. package/packages/server/dist/cli/service/ready.d.ts +22 -0
  18. package/packages/server/dist/cli/service/ready.js +30 -0
  19. package/packages/server/dist/cli/service/spec.d.ts +45 -6
  20. package/packages/server/dist/cli/service/spec.js +34 -7
  21. package/packages/server/dist/cli/service/systemd.d.ts +47 -4
  22. package/packages/server/dist/cli/service/systemd.js +127 -22
  23. package/packages/server/dist/management-mcp-server.d.ts +21 -3
  24. package/packages/server/dist/management-mcp-server.js +4 -13
  25. package/packages/server/dist/routes/meta.js +11 -3
  26. package/packages/server/dist/service-log.d.ts +29 -0
  27. package/packages/server/dist/service-log.js +104 -0
  28. package/packages/server/dist/start.js +11 -0
  29. package/packages/server/dist/version.d.ts +3 -0
  30. package/packages/server/dist/version.js +32 -0
  31. package/packages/server/package.json +1 -1
  32. package/packages/web/dist/assets/{DeleteProjectDialog-BcxJMA1w.js → DeleteProjectDialog-DHczLhwQ.js} +1 -1
  33. package/packages/web/dist/assets/DiscoverPage-BSGSzTEs.js +2 -0
  34. package/packages/web/dist/assets/{DiscoverView-DOmTmiVv.js → DiscoverView-DtTOpTji.js} +1 -1
  35. package/packages/web/dist/assets/{InstanceConfigPage-DWLQqHW0.js → InstanceConfigPage-BRoBh1C1.js} +1 -1
  36. package/packages/web/dist/assets/{MarkdownRenderer-Y25JWlMR.js → MarkdownRenderer-DJf8IJuk.js} +1 -1
  37. package/packages/web/dist/assets/{ProjectRedirect-CCI1VWM6.js → ProjectRedirect-EioguUKW.js} +1 -1
  38. package/packages/web/dist/assets/{ProjectView-DzfVhjya.js → ProjectView-BiKnGfk6.js} +3 -3
  39. package/packages/web/dist/assets/{ProjectsGrid-yIyyb2HA.js → ProjectsGrid-D6kfaIal.js} +1 -1
  40. package/packages/web/dist/assets/{RootHome-x0Om9-Fd.js → RootHome-DpcyuV6e.js} +1 -1
  41. package/packages/web/dist/assets/{Surfaces-wbcMQIsK.js → Surfaces-CHM29PhP.js} +1 -1
  42. package/packages/web/dist/assets/{arc-tWN7FYIq.js → arc-fiqD05Hu.js} +1 -1
  43. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Ccj6Vlyy.js → architectureDiagram-3BPJPVTR-BuuB2Nkf.js} +1 -1
  44. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-B7iGXwqY.js → blockDiagram-GPEHLZMM-DQMBuzfi.js} +1 -1
  45. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-SkLKFSso.js → c4Diagram-AAUBKEIU-BKN0UxCH.js} +1 -1
  46. package/packages/web/dist/assets/channel-B7yh6Su_.js +2 -0
  47. package/packages/web/dist/assets/{chunk-2J33WTMH-BrS8C-YK.js → chunk-2J33WTMH-DW37EupX.js} +1 -1
  48. package/packages/web/dist/assets/{chunk-4BX2VUAB-BUR6nG8w.js → chunk-4BX2VUAB-Dyv7zV_c.js} +1 -1
  49. package/packages/web/dist/assets/{chunk-55IACEB6-BRBNJb-u.js → chunk-55IACEB6-DzMR0dOk.js} +1 -1
  50. package/packages/web/dist/assets/{chunk-727SXJPM-DKeNPTXz.js → chunk-727SXJPM-BdZ3Sor8.js} +1 -1
  51. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BOS1K-DX.js → chunk-AQP2D5EJ-B4i7_og0.js} +1 -1
  52. package/packages/web/dist/assets/{chunk-FMBD7UC4-D3PZL8fo.js → chunk-FMBD7UC4-jcTr85it.js} +1 -1
  53. package/packages/web/dist/assets/{chunk-ND2GUHAM-DA9Y77Zj.js → chunk-ND2GUHAM-fgMsadqB.js} +1 -1
  54. package/packages/web/dist/assets/{chunk-QZHKN3VN-wPph-weq.js → chunk-QZHKN3VN-bJNHYS1W.js} +1 -1
  55. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DIlhMPgi.js +2 -0
  56. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DIlhMPgi.js +2 -0
  57. package/packages/web/dist/assets/{core-I1PQdGMJ.js → core-Bk4KFrmN.js} +1 -1
  58. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BA7N2vVK.js → cose-bilkent-S5V4N54A-BIqCSlQy.js} +1 -1
  59. package/packages/web/dist/assets/{dagre-BM42HDAG-DKpON5p2.js → dagre-BM42HDAG-6nyhtVQz.js} +1 -1
  60. package/packages/web/dist/assets/{diagram-2AECGRRQ-Db0I-eEL.js → diagram-2AECGRRQ-CjizfipR.js} +1 -1
  61. package/packages/web/dist/assets/{diagram-5GNKFQAL-BEuqOTfn.js → diagram-5GNKFQAL-DlgB5vWb.js} +1 -1
  62. package/packages/web/dist/assets/{diagram-KO2AKTUF-CnyX_0Xk.js → diagram-KO2AKTUF-CNC9qanV.js} +1 -1
  63. package/packages/web/dist/assets/{diagram-LMA3HP47-CYvknzDd.js → diagram-LMA3HP47-DSJ-s9lh.js} +1 -1
  64. package/packages/web/dist/assets/{diagram-OG6HWLK6-DlgKxbYz.js → diagram-OG6HWLK6-C9FoHBDg.js} +1 -1
  65. package/packages/web/dist/assets/{documentTitle-Cvob-HVs.js → documentTitle-Cj_AY97j.js} +1 -1
  66. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-BkO5JmiM.js → erDiagram-TEJ5UH35-z6gku4Yt.js} +1 -1
  67. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DG0o_zc1.js → flowDiagram-I6XJVG4X-Dl5c_FgU.js} +1 -1
  68. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-Bs71No2S.js → ganttDiagram-6RSMTGT7-CIscJ-Dt.js} +1 -1
  69. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CxODhRKd.js → gitGraphDiagram-PVQCEYII-CvttOXM_.js} +1 -1
  70. package/packages/web/dist/assets/{index-C4tsPSvG.js → index-DN82do6Z.js} +2 -2
  71. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-C412TQaw.js → infoDiagram-5YYISTIA-dzWYdDJj.js} +1 -1
  72. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DUlcZXQQ.js → ishikawaDiagram-YF4QCWOH-8yX9k4bo.js} +1 -1
  73. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-iL0klVhn.js → journeyDiagram-JHISSGLW-BmitzJDR.js} +1 -1
  74. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-CO02Zl_Q.js → kanban-definition-UN3LZRKU-BZmIkfkW.js} +1 -1
  75. package/packages/web/dist/assets/{linear-DtmLY8AG.js → linear-BBLXE4o7.js} +1 -1
  76. package/packages/web/dist/assets/{mermaid.core-Dvzxug80.js → mermaid.core-mSKff4yu.js} +4 -4
  77. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-q950AAWL.js → mindmap-definition-RKZ34NQL-BWAUa4CP.js} +1 -1
  78. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BVrv8A-H.js → pieDiagram-4H26LBE5-VmVdiL4C.js} +1 -1
  79. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Bp5D2MFq.js → quadrantDiagram-W4KKPZXB-Dybfv6NU.js} +1 -1
  80. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-yZfQU4Ix.js → requirementDiagram-4Y6WPE33-BPnOvB7w.js} +1 -1
  81. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-5R8_OZir.js → sankeyDiagram-5OEKKPKP-YXIEmKw7.js} +1 -1
  82. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-C3A9rinG.js → sequenceDiagram-3UESZ5HK-DintIRUe.js} +1 -1
  83. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-Cshrt_XB.js → stateDiagram-AJRCARHV-eMsjahWp.js} +1 -1
  84. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CpAOGzbO.js +2 -0
  85. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-kNkiddCX.js → timeline-definition-PNZ67QCA-as4zRCPn.js} +1 -1
  86. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-BiNJm17g.js → vennDiagram-CIIHVFJN-CPzvUkM0.js} +1 -1
  87. package/packages/web/dist/assets/{wardley-L42UT6IY-CjExbCuX.js → wardley-L42UT6IY-q6moXj5g.js} +1 -1
  88. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CMn3z-G2.js → wardleyDiagram-YWT4CUSO-BsEEb9Gm.js} +1 -1
  89. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-B-UhnooW.js → xychartDiagram-2RQKCTM6-DSgZlcZW.js} +1 -1
  90. package/packages/web/dist/index.html +1 -1
  91. package/packages/web/dist/sw.js +1 -1
  92. package/packages/web/dist/assets/DiscoverPage-CSPRrFVk.js +0 -2
  93. package/packages/web/dist/assets/channel-B2NSmlBB.js +0 -2
  94. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B3tLFeZC.js +0 -2
  95. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B3tLFeZC.js +0 -2
  96. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DgiazF78.js +0 -2
@@ -23,8 +23,10 @@ import os from "node:os";
23
23
  import path from "node:path";
24
24
  import { spawn } from "node:child_process";
25
25
  import { fileURLToPath } from "node:url";
26
- import { CliError, USAGE, SERVICE_USAGE, CONFIG_USAGE, parseCommand, nodeVersionProblem, explainListenError, } from "./args.js";
27
- import { runService, safeHomeDir } from "./service/index.js";
26
+ import { CliError, UsageError, USAGE, USAGE_SYNOPSIS, SERVICE_USAGE, CONFIG_USAGE, parseCommand, nodeVersionProblem, explainListenError, } from "./args.js";
27
+ import { EXIT } from "./exit-codes.js";
28
+ import { runService, safeHomeDir, realpathOr } from "./service/index.js";
29
+ import { PADDOCK_NAME, PADDOCK_VERSION } from "../version.js";
28
30
  /**
29
31
  * This module's own file and directory: `<…>/packages/server/dist/cli/`.
30
32
  *
@@ -62,18 +64,27 @@ function findUp(rel) {
62
64
  }
63
65
  /** Nearest ancestor holding a `package.json` — `packages/server` in both layouts. */
64
66
  const packageRoot = findUp("package.json") ?? path.resolve(moduleDir, "../..");
65
- function fail(message) {
67
+ /**
68
+ * Report a failure, and hand back the code to exit with (SPEC §2, §3).
69
+ *
70
+ * Two things here are the contract rather than style. Errors go to **stderr**
71
+ * as `paddock: <message>`, so a `--json` document on stdout stays the only
72
+ * thing on stdout. And the code is RETURNED rather than passed to
73
+ * `process.exit()`: exiting mid-write truncates piped output, which is how a
74
+ * `paddock service status | jq` loses the end of its document. `main` assigns
75
+ * `process.exitCode` and lets the process end on its own.
76
+ *
77
+ * A usage error also prints the synopsis after the message, because the spec
78
+ * says the usage follows the message — the short one, not {@link USAGE}'s
79
+ * thousand words, which would bury the line that says what was wrong.
80
+ */
81
+ function fail(message, code = EXIT.failure) {
66
82
  console.error(`paddock: ${message}`);
67
- process.exit(1);
68
- }
69
- function readVersion() {
70
- try {
71
- const raw = fs.readFileSync(path.join(packageRoot, "package.json"), "utf8");
72
- return JSON.parse(raw).version ?? "unknown";
73
- }
74
- catch {
75
- return "unknown";
83
+ if (code === EXIT.usage) {
84
+ console.error("");
85
+ console.error(USAGE_SYNOPSIS);
76
86
  }
87
+ return code;
77
88
  }
78
89
  /**
79
90
  * Put our bundled `claude` on PATH for child processes.
@@ -121,7 +132,10 @@ function addBundledBinsToPath() {
121
132
  function noteFirstRun(dataDir) {
122
133
  if (fs.existsSync(dataDir))
123
134
  return;
124
- console.log([
135
+ // stderr, because stdout's first line belongs to the banner (SPEC §3). This
136
+ // block is prose for a person and it only appears on a first run — which is
137
+ // exactly the run a conformance check or a script reading the URL performs.
138
+ console.error([
125
139
  "",
126
140
  ` Welcome to Paddock. Creating a new instance in ${dataDir}`,
127
141
  " Projects, chats and settings persist there between runs.",
@@ -161,12 +175,17 @@ async function service(command) {
161
175
  // which `main` has already handled. Typed rather than asserted.
162
176
  if (action === undefined) {
163
177
  console.log(SERVICE_USAGE);
164
- return;
178
+ return EXIT.ok;
165
179
  }
166
180
  try {
167
- await runService(action, opts, {
181
+ return await runService(action, opts, {
168
182
  platform: process.platform,
169
- nodePath: process.execPath,
183
+ // Realpath'd (SPEC §7). `process.execPath` under a version manager is
184
+ // routinely a symlink into a per-shell directory — fnm's
185
+ // `fnm_multishells/<pid>_<ts>/bin/node` is deleted when that terminal
186
+ // exits — so a unit naming the symlink stops working when the tab closes,
187
+ // at some later login, with nothing in the logs because it never ran.
188
+ nodePath: realpathOr(process.execPath),
170
189
  scriptPath: entryScript,
171
190
  packageRoot,
172
191
  homeDir: safeHomeDir(),
@@ -180,10 +199,12 @@ async function service(command) {
180
199
  });
181
200
  }
182
201
  catch (err) {
202
+ // A CliError carries its own code: an unsupported platform or an npx
203
+ // refusal is a failure (1), a misspelled flag is usage (2).
183
204
  if (err instanceof CliError)
184
- fail(err.message);
205
+ return fail(err.message, err.exitCode);
185
206
  if (err instanceof Error)
186
- fail(err.message);
207
+ return fail(err.message);
187
208
  throw err;
188
209
  }
189
210
  }
@@ -214,12 +235,13 @@ async function config(command) {
214
235
  // which `main` has already handled. Typed rather than asserted.
215
236
  if (action === undefined) {
216
237
  console.log(CONFIG_USAGE);
217
- return;
238
+ return EXIT.ok;
218
239
  }
219
240
  process.env.PADDOCK_DATA_DIR = resolveDataDir(opts);
220
241
  const { runConfig } = await import("./config.js");
221
242
  try {
222
243
  await runConfig(action, opts);
244
+ return EXIT.ok;
223
245
  }
224
246
  catch (err) {
225
247
  // A config that will not load reaches here as a CliError already carrying
@@ -227,9 +249,9 @@ async function config(command) {
227
249
  // because it is a bug in this command rather than in the configuration.
228
250
  // Same shape as `service` above.
229
251
  if (err instanceof CliError)
230
- fail(err.message);
252
+ return fail(err.message, err.exitCode);
231
253
  if (err instanceof Error)
232
- fail(err.message);
254
+ return fail(err.message);
233
255
  throw err;
234
256
  }
235
257
  }
@@ -239,30 +261,30 @@ async function main() {
239
261
  command = parseCommand(process.argv.slice(2));
240
262
  }
241
263
  catch (err) {
264
+ // Parse failures are usage failures: exit 2, not 1 (SPEC §2).
265
+ if (err instanceof UsageError)
266
+ return fail(err.message, err.exitCode);
242
267
  if (err instanceof CliError)
243
- fail(err.message);
268
+ return fail(err.message, err.exitCode);
244
269
  throw err;
245
270
  }
246
271
  const opts = command.opts;
247
272
  if (opts.help) {
248
273
  console.log(command.verb === "service" ? SERVICE_USAGE : command.verb === "config" ? CONFIG_USAGE : USAGE);
249
- return;
274
+ return EXIT.ok;
250
275
  }
251
276
  if (opts.version) {
252
- console.log(readVersion());
253
- return;
277
+ // Bare: the version and a newline, nothing else (SPEC §1). Scripts parse this.
278
+ console.log(PADDOCK_VERSION);
279
+ return EXIT.ok;
254
280
  }
255
281
  const problem = nodeVersionProblem(process.versions.node);
256
282
  if (problem !== undefined)
257
- fail(problem);
258
- if (command.verb === "service") {
259
- await service(command);
260
- return;
261
- }
262
- if (command.verb === "config") {
263
- await config(command);
264
- return;
265
- }
283
+ return fail(problem);
284
+ if (command.verb === "service")
285
+ return service(command);
286
+ if (command.verb === "config")
287
+ return config(command);
266
288
  addBundledBinsToPath();
267
289
  // NOTHING here reads `process.cwd()`, and that is the point (#798). `--here`
268
290
  // used to make the current directory the workspace by setting
@@ -330,9 +352,14 @@ async function main() {
330
352
  await start();
331
353
  }
332
354
  catch (err) {
333
- fail(explainListenError(err, host, port));
355
+ return fail(explainListenError(err, host, port));
334
356
  }
335
357
  const url = `http://${host}:${port}`;
358
+ // The contract's banner line, FIRST on stdout and in exactly this shape:
359
+ // `<Name> <version>: <url>` (SPEC §3). It is parsed — by the conformance
360
+ // runner, and by anything that wants the URL without guessing the port — so
361
+ // the friendly block below it stays, but below it.
362
+ console.log(`${PADDOCK_NAME} ${PADDOCK_VERSION}: ${url}`);
336
363
  // Still name the data dir, even though it no longer varies with cwd: "which
337
364
  // instance am I looking at" is a real question the moment anyone passes
338
365
  // `--data-dir`, and the answer should be readable off the two lines the tool
@@ -346,8 +373,28 @@ async function main() {
346
373
  ].join("\n"));
347
374
  if (opts.open)
348
375
  openBrowser(url);
376
+ // 0, but NOT yet: a foreground serve keeps running because the server holds
377
+ // the event loop open. `process.exitCode` is set and never acted on until the
378
+ // listener closes, which is what SIGTERM does (start.ts).
379
+ return EXIT.ok;
349
380
  }
350
- main().catch((err) => {
381
+ /**
382
+ * A closed pipe exits quietly (SPEC §2).
383
+ *
384
+ * `paddock service logs | head -5` closes the pipe under us, and Node's default
385
+ * is an unhandled EPIPE with a stack trace — printed to the terminal, after the
386
+ * output the user asked for, about a thing that is not a problem.
387
+ */
388
+ process.stdout.on("error", (err) => {
389
+ if (err.code === "EPIPE")
390
+ process.exit(0);
391
+ });
392
+ main().then((code) => {
393
+ // `process.exitCode`, never `process.exit()` (SPEC §2): exiting mid-write
394
+ // truncates piped output, which is how `paddock config show --json | jq`
395
+ // loses the end of a large document.
396
+ process.exitCode = code;
397
+ }, (err) => {
351
398
  console.error("fatal:", err);
352
- process.exit(1);
399
+ process.exitCode = EXIT.failure;
353
400
  });
@@ -12,6 +12,23 @@ export interface RunResult {
12
12
  */
13
13
  export type Runner = (cmd: string, args: string[]) => RunResult;
14
14
  export declare const spawnRunner: Runner;
15
+ /**
16
+ * Whether the user lingers — Linux only (SPEC §7).
17
+ *
18
+ * `loginctl show-user <user> -p Linger` is the question, and all three answers
19
+ * are distinct and useful: on (nothing to say), off (the service dies at
20
+ * logout, and the fix needs `sudo`), and *unknown* — no `loginctl`, no logind,
21
+ * a container. Collapsing unknown into "off" would print a `sudo` instruction
22
+ * to someone whose machine has no `loginctl` to run it with.
23
+ */
24
+ export interface LingerState {
25
+ /** False where the platform has no such concept (darwin). */
26
+ applies: boolean;
27
+ /** On, off, or undefined when the check could not answer. */
28
+ enabled?: boolean;
29
+ /** The user the check asked about, for the printed command. */
30
+ user?: string;
31
+ }
15
32
  export interface ServiceState {
16
33
  /** Is a unit file installed for us? */
17
34
  registered: boolean;
@@ -20,6 +37,28 @@ export interface ServiceState {
20
37
  pid?: string;
21
38
  /** The Paddock arguments recorded in the installed unit, interpreter stripped. */
22
39
  argv: string[];
40
+ /**
41
+ * The interpreter and script the installed unit actually names.
42
+ *
43
+ * Reported separately from {@link argv} because `status` has to be able to
44
+ * say "the Node it runs is not there any more" (SPEC §7), which is only
45
+ * answerable from the paths in the FILE. Under nvm, fnm or volta, removing a
46
+ * Node version leaves a unit naming an interpreter that no longer exists —
47
+ * and the symptom is Paddock being absent with an empty log, because it never
48
+ * got as far as running. Undefined for a unit we could not parse.
49
+ */
50
+ nodePath?: string;
51
+ cliPath?: string;
52
+ /**
53
+ * The log file the installed unit names, if it names one.
54
+ *
55
+ * Read back rather than recomputed so `status` and `logs` report the file the
56
+ * service is really appending to. A unit written before the logs moved to
57
+ * `<dataDir>/logs/service.log` names the old pair under `<dataDir>/service/`,
58
+ * and telling its owner to read a file nothing writes to is worse than
59
+ * telling them nothing.
60
+ */
61
+ logPath?: string;
23
62
  /**
24
63
  * Does the unit ON DISK come back after a graceful stop (#872)?
25
64
  *
@@ -64,8 +103,13 @@ export interface ServiceBackend {
64
103
  start(): void;
65
104
  stop(): void;
66
105
  restart(): void;
67
- /** How to read the logs, as printed lines. */
68
- logsHint(spec: ServiceSpec): string;
69
- /** The `enable-linger` warning, or undefined where it does not apply. */
70
- lingerNote(): string | undefined;
106
+ /**
107
+ * Does this user linger? (SPEC §7.) Asked of the OS, not assumed.
108
+ *
109
+ * The note used to be printed unconditionally on Linux, which meant the
110
+ * people who had already run `enable-linger` were told to run it at every
111
+ * install and every status — so the one line that mattered to the people who
112
+ * had NOT was indistinguishable from boilerplate.
113
+ */
114
+ linger(): LingerState;
71
115
  }
@@ -1,7 +1,7 @@
1
1
  import { type CliOptions, type ServiceAction } from "../args.js";
2
2
  import { isNpxPath, serviceDir, type ServiceSpec } from "./spec.js";
3
3
  import { type Runner } from "./backend.js";
4
- import { type Readiness } from "./ready.js";
4
+ import { type HealthAnswer, type Readiness } from "./ready.js";
5
5
  export interface ServiceContext {
6
6
  /** `process.platform`. */
7
7
  platform: NodeJS.Platform;
@@ -26,8 +26,62 @@ export interface ServiceContext {
26
26
  * message — twenty seconds per case, and green for the wrong reason.
27
27
  */
28
28
  checkReady?: (url: string) => Promise<Readiness>;
29
+ /**
30
+ * "What does `/api/health` say?", injected for the same reason `checkReady`
31
+ * is — `status` has to be assertable without a server to ask.
32
+ */
33
+ probeHealth?: (url: string) => Promise<HealthAnswer | undefined>;
34
+ }
35
+ /**
36
+ * The version of the Paddock the unit points at.
37
+ *
38
+ * Found by walking up from the `cli.js` in the unit to the nearest
39
+ * `package.json`, which is `packages/server/package.json` in both the repo and
40
+ * the published tarball — the same hop the CLI itself makes to answer
41
+ * `--version`. Undefined when the unit names a path that is no longer there,
42
+ * which is a state `status` reports rather than guesses around.
43
+ */
44
+ export declare function installedVersionOf(cliPath: string | undefined): string | undefined;
45
+ /**
46
+ * What `service status` knows, before it is either printed or serialised.
47
+ *
48
+ * One shape for both renderings, so `--json` cannot quietly disagree with the
49
+ * prose above it — which is the usual way a `--json` flag rots.
50
+ */
51
+ export interface StatusReport {
52
+ installed: boolean;
53
+ running: boolean;
54
+ /** Did `/api/health` answer? Only then is the service actually usable. */
55
+ answering: boolean;
56
+ /** The version the unit points at, and the version that answered. */
57
+ installedVersion: string | null;
58
+ runningVersion: string | null;
59
+ port: number | null;
60
+ host: string | null;
61
+ url: string | null;
62
+ pid: number | null;
63
+ /** The Paddock arguments the unit records, interpreter and script stripped. */
64
+ args: string[];
65
+ /** The plist / unit file — present whether or not it exists. */
66
+ unit: string;
67
+ manager: "launchd" | "systemd";
68
+ /** The one file both streams append to. */
69
+ log: string | null;
70
+ dataDir: string | null;
71
+ node: string | null;
72
+ cli: string | null;
73
+ /** Linux only: null where the platform has no such concept. */
74
+ linger: boolean | null;
75
+ /** A stale unit, a missing Node — things that are wrong but not fatal. */
76
+ warnings: string[];
29
77
  }
30
- export declare function runService(action: ServiceAction, opts: CliOptions, ctx: ServiceContext): Promise<void>;
78
+ /**
79
+ * Dispatch, returning the exit code rather than setting one (SPEC §2).
80
+ *
81
+ * The code is returned up to `paddock.ts`, which assigns `process.exitCode` —
82
+ * never `process.exit()`, which truncates piped output mid-write.
83
+ */
84
+ export declare function runService(action: ServiceAction, opts: CliOptions, ctx: ServiceContext): Promise<number>;
31
85
  /** Exported for the entrypoint's context assembly, and for tests. */
32
86
  export { serviceDir, isNpxPath };
33
87
  export type { ServiceSpec };