@edspencer/paddock 0.79.1 → 0.81.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 (116) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/app.d.ts +7 -0
  3. package/packages/server/dist/app.js +19 -15
  4. package/packages/server/dist/auth.d.ts +3 -2
  5. package/packages/server/dist/auth.js +11 -0
  6. package/packages/server/dist/background-live.d.ts +17 -0
  7. package/packages/server/dist/background-live.js +24 -0
  8. package/packages/server/dist/busy.d.ts +96 -0
  9. package/packages/server/dist/busy.js +88 -0
  10. package/packages/server/dist/cli/args.d.ts +63 -7
  11. package/packages/server/dist/cli/args.js +227 -20
  12. package/packages/server/dist/cli/busy-command.d.ts +58 -0
  13. package/packages/server/dist/cli/busy-command.js +108 -0
  14. package/packages/server/dist/cli/busy.d.ts +69 -0
  15. package/packages/server/dist/cli/busy.js +150 -0
  16. package/packages/server/dist/cli/exit-codes.d.ts +49 -0
  17. package/packages/server/dist/cli/exit-codes.js +48 -0
  18. package/packages/server/dist/cli/output.d.ts +52 -0
  19. package/packages/server/dist/cli/output.js +84 -0
  20. package/packages/server/dist/cli/paddock.js +98 -37
  21. package/packages/server/dist/cli/service/backend.d.ts +48 -4
  22. package/packages/server/dist/cli/service/index.d.ts +68 -2
  23. package/packages/server/dist/cli/service/index.js +321 -60
  24. package/packages/server/dist/cli/service/launchd.d.ts +23 -1
  25. package/packages/server/dist/cli/service/launchd.js +145 -25
  26. package/packages/server/dist/cli/service/logs.d.ts +39 -0
  27. package/packages/server/dist/cli/service/logs.js +115 -0
  28. package/packages/server/dist/cli/service/path-env.d.ts +27 -0
  29. package/packages/server/dist/cli/service/path-env.js +120 -0
  30. package/packages/server/dist/cli/service/ready.d.ts +22 -0
  31. package/packages/server/dist/cli/service/ready.js +30 -0
  32. package/packages/server/dist/cli/service/spec.d.ts +45 -6
  33. package/packages/server/dist/cli/service/spec.js +34 -7
  34. package/packages/server/dist/cli/service/systemd.d.ts +47 -4
  35. package/packages/server/dist/cli/service/systemd.js +127 -22
  36. package/packages/server/dist/herdctl.d.ts +19 -0
  37. package/packages/server/dist/herdctl.js +26 -0
  38. package/packages/server/dist/management-mcp-server.d.ts +21 -3
  39. package/packages/server/dist/management-mcp-server.js +4 -13
  40. package/packages/server/dist/routes/meta.js +56 -3
  41. package/packages/server/dist/runtime-file.d.ts +26 -0
  42. package/packages/server/dist/runtime-file.js +91 -0
  43. package/packages/server/dist/service-log.d.ts +29 -0
  44. package/packages/server/dist/service-log.js +104 -0
  45. package/packages/server/dist/start.d.ts +33 -0
  46. package/packages/server/dist/start.js +126 -8
  47. package/packages/server/dist/version.d.ts +3 -0
  48. package/packages/server/dist/version.js +32 -0
  49. package/packages/server/dist/ws-context.d.ts +9 -0
  50. package/packages/server/dist/ws-turn.js +1 -0
  51. package/packages/server/package.json +1 -1
  52. package/packages/web/dist/assets/{DeleteProjectDialog-BcxJMA1w.js → DeleteProjectDialog-D8oIJ8HC.js} +1 -1
  53. package/packages/web/dist/assets/DiscoverPage-CQlzxsn4.js +2 -0
  54. package/packages/web/dist/assets/{DiscoverView-DOmTmiVv.js → DiscoverView-o0KCr3aB.js} +1 -1
  55. package/packages/web/dist/assets/{InstanceConfigPage-DWLQqHW0.js → InstanceConfigPage-RSJz7oL3.js} +1 -1
  56. package/packages/web/dist/assets/{MarkdownRenderer-Y25JWlMR.js → MarkdownRenderer-39mafH8b.js} +1 -1
  57. package/packages/web/dist/assets/{ProjectRedirect-CCI1VWM6.js → ProjectRedirect-Bq7GVguY.js} +1 -1
  58. package/packages/web/dist/assets/{ProjectView-DzfVhjya.js → ProjectView-BoBIru1g.js} +3 -3
  59. package/packages/web/dist/assets/{ProjectsGrid-yIyyb2HA.js → ProjectsGrid-mJ4gqbr6.js} +1 -1
  60. package/packages/web/dist/assets/{RootHome-x0Om9-Fd.js → RootHome-C0kwFyYp.js} +1 -1
  61. package/packages/web/dist/assets/{Surfaces-wbcMQIsK.js → Surfaces-MkhXlB8z.js} +1 -1
  62. package/packages/web/dist/assets/{arc-tWN7FYIq.js → arc-DExlwAwO.js} +1 -1
  63. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Ccj6Vlyy.js → architectureDiagram-3BPJPVTR-CZly3W79.js} +1 -1
  64. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-B7iGXwqY.js → blockDiagram-GPEHLZMM-AgES73i3.js} +1 -1
  65. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-SkLKFSso.js → c4Diagram-AAUBKEIU-N9mD8Pfk.js} +1 -1
  66. package/packages/web/dist/assets/channel-ExsAG7qT.js +2 -0
  67. package/packages/web/dist/assets/{chunk-2J33WTMH-BrS8C-YK.js → chunk-2J33WTMH-Dsx_kfUl.js} +1 -1
  68. package/packages/web/dist/assets/{chunk-4BX2VUAB-BUR6nG8w.js → chunk-4BX2VUAB-BQjqcjPj.js} +1 -1
  69. package/packages/web/dist/assets/{chunk-55IACEB6-BRBNJb-u.js → chunk-55IACEB6-CCxpYQpY.js} +1 -1
  70. package/packages/web/dist/assets/{chunk-727SXJPM-DKeNPTXz.js → chunk-727SXJPM-DpFRdnUa.js} +1 -1
  71. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BOS1K-DX.js → chunk-AQP2D5EJ-D2BAlzoO.js} +1 -1
  72. package/packages/web/dist/assets/{chunk-FMBD7UC4-D3PZL8fo.js → chunk-FMBD7UC4-BFaMRzUF.js} +1 -1
  73. package/packages/web/dist/assets/{chunk-ND2GUHAM-DA9Y77Zj.js → chunk-ND2GUHAM-B9NGJ9xZ.js} +1 -1
  74. package/packages/web/dist/assets/{chunk-QZHKN3VN-wPph-weq.js → chunk-QZHKN3VN-CF1IHyMS.js} +1 -1
  75. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-si7g9okq.js +2 -0
  76. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-si7g9okq.js +2 -0
  77. package/packages/web/dist/assets/{core-I1PQdGMJ.js → core-XjElsvQ7.js} +1 -1
  78. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BA7N2vVK.js → cose-bilkent-S5V4N54A-CF972Flx.js} +1 -1
  79. package/packages/web/dist/assets/{dagre-BM42HDAG-DKpON5p2.js → dagre-BM42HDAG-DsneI8YE.js} +1 -1
  80. package/packages/web/dist/assets/{diagram-2AECGRRQ-Db0I-eEL.js → diagram-2AECGRRQ-BHyrVzIk.js} +1 -1
  81. package/packages/web/dist/assets/{diagram-5GNKFQAL-BEuqOTfn.js → diagram-5GNKFQAL-Dj4q8xEi.js} +1 -1
  82. package/packages/web/dist/assets/{diagram-KO2AKTUF-CnyX_0Xk.js → diagram-KO2AKTUF-BHe3Puv_.js} +1 -1
  83. package/packages/web/dist/assets/{diagram-LMA3HP47-CYvknzDd.js → diagram-LMA3HP47-wzt6G2FE.js} +1 -1
  84. package/packages/web/dist/assets/{diagram-OG6HWLK6-DlgKxbYz.js → diagram-OG6HWLK6-CZMbMRJq.js} +1 -1
  85. package/packages/web/dist/assets/{documentTitle-Cvob-HVs.js → documentTitle-BwTIA8rI.js} +1 -1
  86. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-BkO5JmiM.js → erDiagram-TEJ5UH35-Dtu14BL1.js} +1 -1
  87. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DG0o_zc1.js → flowDiagram-I6XJVG4X-T34ql9j0.js} +1 -1
  88. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-Bs71No2S.js → ganttDiagram-6RSMTGT7-DQLOeCXK.js} +1 -1
  89. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CxODhRKd.js → gitGraphDiagram-PVQCEYII-Bk11zea6.js} +1 -1
  90. package/packages/web/dist/assets/{index-C4tsPSvG.js → index-CvaWErFv.js} +2 -2
  91. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-C412TQaw.js → infoDiagram-5YYISTIA-CsE83lu0.js} +1 -1
  92. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DUlcZXQQ.js → ishikawaDiagram-YF4QCWOH-DNVgA93n.js} +1 -1
  93. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-iL0klVhn.js → journeyDiagram-JHISSGLW-CLpSNjM3.js} +1 -1
  94. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-CO02Zl_Q.js → kanban-definition-UN3LZRKU-BRzuXcKr.js} +1 -1
  95. package/packages/web/dist/assets/{linear-DtmLY8AG.js → linear-BxOawONt.js} +1 -1
  96. package/packages/web/dist/assets/{mermaid.core-Dvzxug80.js → mermaid.core-Dvs1QCcz.js} +4 -4
  97. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-q950AAWL.js → mindmap-definition-RKZ34NQL-Dmt7pYwF.js} +1 -1
  98. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BVrv8A-H.js → pieDiagram-4H26LBE5-BHiZaJD-.js} +1 -1
  99. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Bp5D2MFq.js → quadrantDiagram-W4KKPZXB-U0A1noRE.js} +1 -1
  100. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-yZfQU4Ix.js → requirementDiagram-4Y6WPE33-CZLsr6yi.js} +1 -1
  101. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-5R8_OZir.js → sankeyDiagram-5OEKKPKP-BTlLGILU.js} +1 -1
  102. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-C3A9rinG.js → sequenceDiagram-3UESZ5HK-DpSS_Gb1.js} +1 -1
  103. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-Cshrt_XB.js → stateDiagram-AJRCARHV-D9srE-tV.js} +1 -1
  104. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-AxWqsDh-.js +2 -0
  105. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-kNkiddCX.js → timeline-definition-PNZ67QCA-BYQ5CKab.js} +1 -1
  106. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-BiNJm17g.js → vennDiagram-CIIHVFJN-B4W2CtSV.js} +1 -1
  107. package/packages/web/dist/assets/{wardley-L42UT6IY-CjExbCuX.js → wardley-L42UT6IY--XUV7GvD.js} +1 -1
  108. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CMn3z-G2.js → wardleyDiagram-YWT4CUSO-Bl2VIEGY.js} +1 -1
  109. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-B-UhnooW.js → xychartDiagram-2RQKCTM6-Bj2H9Rcf.js} +1 -1
  110. package/packages/web/dist/index.html +1 -1
  111. package/packages/web/dist/sw.js +1 -1
  112. package/packages/web/dist/assets/DiscoverPage-CSPRrFVk.js +0 -2
  113. package/packages/web/dist/assets/channel-B2NSmlBB.js +0 -2
  114. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B3tLFeZC.js +0 -2
  115. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B3tLFeZC.js +0 -2
  116. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DgiazF78.js +0 -2
@@ -23,8 +23,34 @@
23
23
  * the need for the guard entirely, so there is no condition left to get wrong on
24
24
  * the next shim, platform or package manager. `paddock.ts` now always runs.
25
25
  */
26
- /** A usage error. Thrown rather than exiting, so parsing stays testable. */
26
+ import { EXIT } from "./exit-codes.js";
27
+ /**
28
+ * A failure with an exit code attached (SPEC §2).
29
+ *
30
+ * Thrown rather than exiting, so parsing stays testable. The code travels with
31
+ * the error because the thing that KNOWS which code applies is whatever threw —
32
+ * `main` cannot tell an unsupported platform (a failure, 1) from a misspelled
33
+ * flag (usage, 2) by looking at a string.
34
+ */
27
35
  export class CliError extends Error {
36
+ exitCode;
37
+ constructor(message, exitCode = EXIT.failure) {
38
+ super(message);
39
+ this.exitCode = exitCode;
40
+ }
41
+ }
42
+ /**
43
+ * An unknown command, an unknown flag, or a bad value: exit 2 (SPEC §2).
44
+ *
45
+ * **This is a behaviour change, and a deliberate one.** Every one of these used
46
+ * to exit 1, which is also what a genuine failure exits — so a script could not
47
+ * tell "you typed it wrong" from "it tried and could not". The contract splits
48
+ * them, and a script that tested `$? -eq 1` for a typo now needs `-eq 2`.
49
+ */
50
+ export class UsageError extends CliError {
51
+ constructor(message) {
52
+ super(message, EXIT.usage);
53
+ }
28
54
  }
29
55
  export const MIN_NODE_MAJOR = 22;
30
56
  /**
@@ -43,6 +69,7 @@ export const SERVICE_ACTIONS = [
43
69
  "start",
44
70
  "stop",
45
71
  "restart",
72
+ "logs",
46
73
  ];
47
74
  /**
48
75
  * What `paddock config` can be asked to do (#878).
@@ -53,8 +80,15 @@ export const SERVICE_ACTIONS = [
53
80
  * the command that makes it unnecessary.
54
81
  */
55
82
  export const CONFIG_ACTIONS = ["show", "eject"];
56
- /** The leading words {@link parseCommand} recognises. Anything else is a flag. */
57
- export const VERBS = ["start", "service", "config"];
83
+ /**
84
+ * The leading words {@link parseCommand} recognises. Anything else is a flag.
85
+ *
86
+ * `serve` is the contract's name for running in the foreground (SPEC §1), and
87
+ * `start` is kept as an alias **for good** — the spec says so explicitly for
88
+ * Paddock and Managers, every existing service unit's `ExecStart` names it, and
89
+ * it is in two years of other people's scripts. They parse to the same command.
90
+ */
91
+ export const VERBS = ["serve", "start", "service", "config", "busy"];
58
92
  /**
59
93
  * Split an action off a verb's argv, then parse what follows as flags.
60
94
  *
@@ -67,7 +101,7 @@ function parseAction(verb, actions, rest) {
67
101
  let action;
68
102
  if (head !== undefined && !head.startsWith("-")) {
69
103
  if (!actions.includes(head)) {
70
- throw new CliError(`unknown ${verb} action: ${head}\n` +
104
+ throw new UsageError(`unknown ${verb} action: ${head}\n` +
71
105
  `Expected one of: ${actions.join(", ")}.\n` +
72
106
  `Run \`paddock ${verb} --help\` for usage.`);
73
107
  }
@@ -76,7 +110,7 @@ function parseAction(verb, actions, rest) {
76
110
  const opts = parseArgs(action === undefined ? rest : rest.slice(1));
77
111
  // `--help` wins over a missing action: asking for usage is not a usage error.
78
112
  if (action === undefined && !opts.help) {
79
- throw new CliError(`\`paddock ${verb}\` needs an action: ${actions.join(", ")}.\n` +
113
+ throw new UsageError(`\`paddock ${verb}\` needs an action: ${actions.join(", ")}.\n` +
80
114
  `Run \`paddock ${verb} --help\` for usage.`);
81
115
  }
82
116
  return { action, opts };
@@ -108,6 +142,30 @@ function parseAction(verb, actions, rest) {
108
142
  * and load-bearing; it was only the example's error that was misdescribed.
109
143
  */
110
144
  export function parseCommand(argv) {
145
+ const command = dispatch(argv);
146
+ // `--force` overrides the busy check (SPEC §1, §4) and nothing else, so it
147
+ // only means anything on the two commands that run one. Rejecting it
148
+ // everywhere else, rather than parsing and dropping it, is the difference
149
+ // between being told and guessing: the two readings of a quiet
150
+ // `service uninstall --force` are "it overrode something" and "it did
151
+ // nothing", and the exit code is 0 either way.
152
+ //
153
+ // Checked here, once, rather than in each command: a second copy of this rule
154
+ // is a second chance for the two to drift apart about what `--force` means.
155
+ if (command.opts.force === true &&
156
+ // `--help` wins, the same way it wins over a missing action above: asking
157
+ // for the usage is not a usage error.
158
+ !command.opts.help &&
159
+ !(command.verb === "service" && (command.action === "stop" || command.action === "restart"))) {
160
+ const what = command.verb === "service" && command.action !== undefined
161
+ ? `\`service ${command.action}\``
162
+ : `\`${command.verb}\``;
163
+ throw new UsageError(`--force applies to \`service stop\` and \`service restart\` only, not ${what}.\n` +
164
+ "It overrides the busy check, and those are the two commands that run one.");
165
+ }
166
+ return command;
167
+ }
168
+ function dispatch(argv) {
111
169
  const [first, ...rest] = argv;
112
170
  if (first === "service") {
113
171
  return { verb: "service", ...parseAction("service", SERVICE_ACTIONS, rest) };
@@ -115,9 +173,15 @@ export function parseCommand(argv) {
115
173
  if (first === "config") {
116
174
  return { verb: "config", ...parseAction("config", CONFIG_ACTIONS, rest) };
117
175
  }
118
- if (first === "start")
119
- return { verb: "start", opts: parseArgs(rest) };
120
- return { verb: "start", opts: parseArgs(argv) };
176
+ // `busy` (SPEC §1, §4). A top-level verb rather than a `service` action
177
+ // because its whole point is to be askable where there is no service to ask:
178
+ // a Docker container, a Watchtower pre-update hook, an Ansible task.
179
+ if (first === "busy")
180
+ return { verb: "busy", opts: parseArgs(rest) };
181
+ // `serve` and `start` are one command: see VERBS.
182
+ if (first === "serve" || first === "start")
183
+ return { verb: "serve", opts: parseArgs(rest) };
184
+ return { verb: "serve", opts: parseArgs(argv) };
121
185
  }
122
186
  export function parseArgs(argv) {
123
187
  const opts = {
@@ -131,7 +195,7 @@ export function parseArgs(argv) {
131
195
  const next = () => {
132
196
  const v = argv[++i];
133
197
  if (v === undefined)
134
- throw new CliError(`${arg} needs a value`);
198
+ throw new UsageError(`${arg} needs a value`);
135
199
  return v;
136
200
  };
137
201
  switch (arg) {
@@ -148,7 +212,7 @@ export function parseArgs(argv) {
148
212
  // into a hard error.
149
213
  const v = next();
150
214
  if (v !== "" && (!/^\d+$/.test(v) || Number(v) < 1 || Number(v) > 65535)) {
151
- throw new CliError(`${arg} needs a number between 1 and 65535, got: ${v}`);
215
+ throw new UsageError(`${arg} needs a number between 1 and 65535, got: ${v}`);
152
216
  }
153
217
  opts.port = v;
154
218
  break;
@@ -185,6 +249,25 @@ export function parseArgs(argv) {
185
249
  case "--include-env":
186
250
  opts.includeEnv = true;
187
251
  break;
252
+ // `service stop` / `service restart`: override the busy check (SPEC §4).
253
+ case "--force":
254
+ opts.force = true;
255
+ break;
256
+ // `service logs` flags, the names `tail`, `journalctl` and every other
257
+ // log reader uses — this is not the place to be original.
258
+ case "-f":
259
+ case "--follow":
260
+ opts.follow = true;
261
+ break;
262
+ case "-n":
263
+ case "--lines": {
264
+ const v = next();
265
+ if (!/^\d+$/.test(v) || Number(v) < 1) {
266
+ throw new UsageError(`${arg} needs a positive number of lines, got: ${v}`);
267
+ }
268
+ opts.lines = Number(v);
269
+ break;
270
+ }
188
271
  case "-h":
189
272
  case "--help":
190
273
  opts.help = true;
@@ -194,7 +277,7 @@ export function parseArgs(argv) {
194
277
  opts.version = true;
195
278
  break;
196
279
  default:
197
- throw new CliError(`unknown option: ${arg}\nRun \`paddock --help\` for usage.`);
280
+ throw new UsageError(`unknown option: ${arg}`);
198
281
  }
199
282
  }
200
283
  return opts;
@@ -232,14 +315,38 @@ export function explainListenError(err, host, port) {
232
315
  }
233
316
  return String(err?.message ?? err);
234
317
  }
318
+ /**
319
+ * The command list, on its own.
320
+ *
321
+ * Printed after a usage error, because SPEC §2 says the usage follows the
322
+ * message — and because {@link USAGE} is a thousand words of guide that would
323
+ * bury the one line explaining what went wrong. The full text is one
324
+ * `--help` away, and the last line here says so.
325
+ */
326
+ export const USAGE_SYNOPSIS = `Usage
327
+ paddock [serve] [options] run the server in this terminal
328
+ paddock service <action> [options] install|uninstall|status|start|stop|restart|logs
329
+ paddock busy [--json] is the running instance busy?
330
+ paddock config <show|eject> [options] what this instance's config resolved to
331
+ paddock -v | --version print the version and exit
332
+ paddock -h | --help the full help`;
235
333
  export const USAGE = `paddock — run a Paddock instance locally
236
334
 
237
335
  Usage
238
- npx @edspencer/paddock [options] start the server (the default)
239
- paddock start [options] the same thing, said out loud
240
- paddock service <install|uninstall|status>
241
- run it in the background from login
336
+ npx @edspencer/paddock [options] run the server (the default)
337
+ paddock serve [options] the same thing, said out loud
338
+ (\`paddock start\` is a permanent alias)
339
+ paddock service install [options] run it in the background from login
340
+ paddock service uninstall stop it and remove the unit
341
+ paddock service status [--json] installed? running? which version?
342
+ paddock service start start an installed service
343
+ paddock service stop [--force] stop it, leave it installed
344
+ paddock service restart [--force] stop it and start it again
345
+ paddock service logs [-f] [-n <lines>] the service's log
242
346
  (\`paddock service --help\`)
347
+ paddock busy [--json] is it busy? exit 0 no, 75 yes, 3
348
+ nothing answering
349
+ (\`paddock busy --help\`)
243
350
  paddock config show [--resolved] what this instance's config resolved
244
351
  to, and where each value came from
245
352
  paddock config eject [--write] freeze that resolution into
@@ -255,6 +362,14 @@ Options
255
362
  -v, --version Print the Paddock version and exit
256
363
  -h, --help Show this help
257
364
 
365
+ Exit codes
366
+ 0 done; running and answering; not busy
367
+ 1 failed
368
+ 2 usage: an unknown command or flag, or a bad value
369
+ 3 not running: installed, but not running or not answering; nothing answers
370
+ 4 not installed: there is no service unit
371
+ 75 busy: refused, and why was printed. Try later, or use --force
372
+
258
373
  Opening your own directories
259
374
  Where you run this from does not matter — the instance is decided by its data
260
375
  dir alone. Directories are added inside the app, on the Discover screen: it
@@ -358,10 +473,30 @@ export const SERVICE_USAGE = `paddock service — keep Paddock running in the ba
358
473
  Usage
359
474
  paddock service install [options] register it and start it now
360
475
  paddock service uninstall stop it and deregister it
361
- paddock service status is it registered, is it running, where are the logs
476
+ paddock service status [--json] is it registered, is it running, which
477
+ version, where are the logs
362
478
  paddock service start start an installed service that is stopped
363
- paddock service stop stop it, and leave it installed
364
- paddock service restart stop it and start it again, re-reading config
479
+ paddock service stop [--force] stop it, and leave it installed
480
+ paddock service restart [--force] stop it and start it again, re-reading config
481
+ paddock service logs [-f] [-n <n>] the service's log (the last 50 lines)
482
+
483
+ Options (logs)
484
+ -f, --follow keep printing what gets appended, until Ctrl-C
485
+ -n, --lines <n> how many lines to print first (default 50)
486
+
487
+ Options (stop, restart)
488
+ --force stop it even though it is busy
489
+
490
+ Busy
491
+ \`stop\` and \`restart\` ask the running instance whether it is busy first, and
492
+ refuse with exit 75 if it is — a chat turn in flight, a background task, a
493
+ scheduled job. \`paddock busy\` asks the same question on its own.
494
+
495
+ Being refused is the useful case: a restart kills a chat turn mid-sentence,
496
+ and the turn cannot be resumed from where it was. Wait, or --force.
497
+
498
+ \`uninstall\` does not ask. Removing the service is an explicit act, and
499
+ someone uninstalling it has already decided.
365
500
 
366
501
  Options (install only — recorded in the generated unit)
367
502
  -p, --port <port> HTTP/WS port (default 7233)
@@ -393,9 +528,21 @@ At login, not at boot
393
528
 
394
529
  Where it lives
395
530
  macOS ~/Library/LaunchAgents/net.edspencer.paddock.plist
396
- logs in <data-dir>/service/
397
531
  Linux ~/.config/systemd/user/paddock.service
398
- logs via journalctl --user -u paddock.service -f
532
+
533
+ Both write stdout and stderr to ONE file, <data-dir>/logs/service.log, which
534
+ is what \`paddock service logs\` reads. Not the journal on Linux: a volatile
535
+ journal has nothing in it after the reboot you are asking about, and one file
536
+ means one answer to "where are the logs" on every OS. Paddock rotates it
537
+ itself at 8 MB, keeping three older generations beside it.
538
+
539
+ Exit codes
540
+ 0 it is installed, running and answering
541
+ 3 installed, but not running or not answering
542
+ 4 not installed: there is no unit (status, logs, start, stop, restart)
543
+ 1 something failed
544
+ 2 usage: an unknown action or flag, or a bad value
545
+ 75 busy, and --force was not given (stop, restart)
399
546
 
400
547
  Installed from npx?
401
548
  \`service install\` refuses. An npx cache path is hash-keyed and npm may prune
@@ -411,6 +558,66 @@ A note on access
411
558
  process that could reach the port could already read the same Claude login as
412
559
  you. Set PADDOCK_AUTH_MODE if you want a credential on it anyway.
413
560
 
561
+ Docs: https://github.com/edspencer/paddock`;
562
+ export const BUSY_USAGE = `paddock busy — is the running instance in the middle of something?
563
+
564
+ Usage
565
+ paddock busy ask, and say so in a sentence
566
+ paddock busy --json the same answer as one JSON document
567
+ paddock busy -d <path> ask a particular instance
568
+
569
+ Exit codes — this command is FOR its exit code
570
+ 0 not busy. Nothing a restart would cut short.
571
+ 75 busy. What is running was printed.
572
+ 3 nothing answered. No instance is running, so there is nothing to wait for.
573
+ 1 something answered, but not as Paddock's busy check.
574
+
575
+ Options
576
+ -d, --data-dir <path> Which instance to ask (default ~/.paddock, or
577
+ \$PADDOCK_DATA_DIR) — the same rule \`paddock serve\`
578
+ uses, so the two always mean the same instance
579
+ --json One JSON document on stdout: running, busy, url,
580
+ version, counts. Prose goes to stderr
581
+ -h, --help Show this help
582
+
583
+ What counts as busy
584
+ Only work a restart would LOSE or cut short:
585
+
586
+ chat a chat turn running — a human turn, a fired trigger, a cron
587
+ fire. A turn is a live claude subprocess writing a transcript,
588
+ and it cannot be resumed from where it was interrupted.
589
+ background a background task a live session is still holding after its
590
+ turn ended: a background shell, a sub-agent, a Monitor.
591
+ job a scheduled job herdctl is running.
592
+
593
+ Not busy: an idle session (it resumes), an open browser tab (it reconnects),
594
+ a pending curation sweep (it re-runs from its watermark).
595
+
596
+ Where to run it from
597
+ From a terminal. A Paddock chat that runs this is itself one of the chat turns
598
+ it counts, so it will always see itself as busy — and \`service stop --force\`
599
+ from inside a chat stops the service that is running the chat, which does not
600
+ come back with an explanation. That is why the refusal says so.
601
+
602
+ For scripts, containers and Watchtower
603
+ \`busy\` is there for callers with no service to ask. 75 is EX_TEMPFAIL, which
604
+ is the code Watchtower's pre-update hook reads as "skip this one":
605
+
606
+ labels:
607
+ com.centurylinklabs.watchtower.lifecycle.pre-update: paddock busy
608
+
609
+ An Ansible task does the same thing with a retry. Nothing here contacts the
610
+ network or the npm registry, and the question is answered from memory — it is
611
+ cheap enough to ask in a loop.
612
+
613
+ How it finds the instance
614
+ \`<data-dir>/runtime.json\`, written 0600 when the server starts listening and
615
+ removed when it stops. There is no fallback to a default port: guessing one
616
+ would ask whatever else happened to be listening there, and get an answer.
617
+
618
+ A Paddock from before this feature shipped writes no such file, so it reads as
619
+ not running (exit 3). Upgrade it and restart it once.
620
+
414
621
  Docs: https://github.com/edspencer/paddock`;
415
622
  export const CONFIG_USAGE = `paddock config — what this instance's configuration actually resolved to
416
623
 
@@ -0,0 +1,58 @@
1
+ import { type BusyResult, type Nouns } from "./busy.js";
2
+ /**
3
+ * How each count in `/api/busy` is said in a sentence (`busy.ts` on the server
4
+ * decides the keys).
5
+ *
6
+ * `background` is spelled out rather than left to the default pluraliser,
7
+ * because "3 backgrounds are running" is not English — and because "background
8
+ * task" is the thing a user recognises from the running-work bar in the UI,
9
+ * where they watched it start.
10
+ */
11
+ export declare const PADDOCK_NOUNS: Nouns;
12
+ /**
13
+ * What to do about a busy answer — and for Paddock, the first thing to say is
14
+ * **where to run the command from**.
15
+ *
16
+ * This is the keeper-chat trap, and it is specific to Paddock and Managers
17
+ * among the six tools: Paddock's agents can drive Paddock. A keeper chat that
18
+ * runs `paddock service restart` or `paddock update` is itself one of the chat
19
+ * turns the answer just counted, so it will always see itself as busy — and
20
+ * `--force` from there does not override a mistaken reading, it kills the chat
21
+ * that typed it, mid-sentence, along with whatever it had not finished. The
22
+ * agent then has no way to report what happened, because the thing that would
23
+ * report it is the thing that died.
24
+ *
25
+ * So when `chat` is non-zero the advice is not "wait or use --force": it is
26
+ * "run this from a terminal". When the busy work is background tasks or
27
+ * scheduled jobs only, nobody is holding the keyboard inside the service and
28
+ * the ordinary advice applies.
29
+ */
30
+ export declare function paddockBusyHint(result: BusyResult): string | undefined;
31
+ /**
32
+ * `paddock busy [--json]` — exit 0 not busy, 75 busy, 3 nothing answering
33
+ * (SPEC §1, §2).
34
+ *
35
+ * The instance is found through `<data-dir>/runtime.json` and nowhere else, so
36
+ * this reports on the same instance `paddock serve` and `paddock config show`
37
+ * resolve — `resolveDataDir` in paddock.ts is the one rule all three use.
38
+ */
39
+ export declare function runBusy(home: string, json: boolean): Promise<number>;
40
+ /**
41
+ * The check before `service stop` and `service restart` (SPEC §1): refuse with
42
+ * 75 unless `--force`, which prints the same message prefixed `Overriding:` and
43
+ * carries on. Returns null to go ahead.
44
+ *
45
+ * `home` is the data dir of the INSTALLED unit, not whatever `--data-dir` this
46
+ * invocation carries — the same rule `status` and `logs` follow (#824), because
47
+ * the instance being stopped is the one the unit describes.
48
+ *
49
+ * Two deliberate non-refusals. Nothing answering goes ahead: there is nothing
50
+ * to cut short, which is also the answer for a service that is already stopped.
51
+ * And an instance running a Paddock from before this release has no
52
+ * `runtime.json`, so it reads as not running and the stop proceeds exactly as
53
+ * it did before — an upgrade cannot make a stop start failing.
54
+ */
55
+ export declare function guardServiceBusy(action: "stop" | "restart", home: string, opts: {
56
+ force?: boolean;
57
+ json?: boolean;
58
+ }): Promise<number | null>;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Paddock's bindings for the copied busy client (`cli/busy.ts`): the nouns its
3
+ * message uses, the keeper-chat trap, and the two entry points — `paddock busy`
4
+ * and the check `service stop` / `service restart` run first (CLI contract
5
+ * SPEC §1, §4).
6
+ *
7
+ * Kept separate from `cli/busy.ts` so that file stays a straight copy of
8
+ * `reference/busy-client.ts`, which is what wave C4 re-syncs. Everything in
9
+ * here is Paddock-specific by definition: the contract says each tool's kinds of
10
+ * work are its own.
11
+ */
12
+ import { EXIT } from "./exit-codes.js";
13
+ import { createOutput } from "./output.js";
14
+ import { guardBusy, runBusyCommand, } from "./busy.js";
15
+ import { PADDOCK_NAME } from "../version.js";
16
+ /**
17
+ * How each count in `/api/busy` is said in a sentence (`busy.ts` on the server
18
+ * decides the keys).
19
+ *
20
+ * `background` is spelled out rather than left to the default pluraliser,
21
+ * because "3 backgrounds are running" is not English — and because "background
22
+ * task" is the thing a user recognises from the running-work bar in the UI,
23
+ * where they watched it start.
24
+ */
25
+ export const PADDOCK_NOUNS = {
26
+ chat: ["chat turn", "chat turns"],
27
+ background: ["background task", "background tasks"],
28
+ job: ["scheduled job", "scheduled jobs"],
29
+ };
30
+ /**
31
+ * What to do about a busy answer — and for Paddock, the first thing to say is
32
+ * **where to run the command from**.
33
+ *
34
+ * This is the keeper-chat trap, and it is specific to Paddock and Managers
35
+ * among the six tools: Paddock's agents can drive Paddock. A keeper chat that
36
+ * runs `paddock service restart` or `paddock update` is itself one of the chat
37
+ * turns the answer just counted, so it will always see itself as busy — and
38
+ * `--force` from there does not override a mistaken reading, it kills the chat
39
+ * that typed it, mid-sentence, along with whatever it had not finished. The
40
+ * agent then has no way to report what happened, because the thing that would
41
+ * report it is the thing that died.
42
+ *
43
+ * So when `chat` is non-zero the advice is not "wait or use --force": it is
44
+ * "run this from a terminal". When the busy work is background tasks or
45
+ * scheduled jobs only, nobody is holding the keyboard inside the service and
46
+ * the ordinary advice applies.
47
+ */
48
+ export function paddockBusyHint(result) {
49
+ if (result.state !== "busy")
50
+ return undefined;
51
+ const chats = result.answer.counts?.chat ?? 0;
52
+ if (chats > 0) {
53
+ // The lead sentence is short because `guardBusy` prints it on the same line
54
+ // as the message, after `paddock: `. The reasoning goes underneath, indented
55
+ // like every other continuation this CLI prints.
56
+ return ("Run it from a terminal, not from inside a Paddock chat.\n" +
57
+ " One of the turns counted above may be the chat asking, and --force from there\n" +
58
+ " stops the service running that chat — which then never gets to say so.\n" +
59
+ " Otherwise: wait for the turns to finish.");
60
+ }
61
+ return "Try again once it has finished, or use --force.";
62
+ }
63
+ /** The options both entry points share: name, nouns and hint are fixed for Paddock. */
64
+ function paddockBusyOptions(home, out) {
65
+ return { home, out, name: PADDOCK_NAME, nouns: PADDOCK_NOUNS, hint: paddockBusyHint };
66
+ }
67
+ /**
68
+ * `paddock busy [--json]` — exit 0 not busy, 75 busy, 3 nothing answering
69
+ * (SPEC §1, §2).
70
+ *
71
+ * The instance is found through `<data-dir>/runtime.json` and nowhere else, so
72
+ * this reports on the same instance `paddock serve` and `paddock config show`
73
+ * resolve — `resolveDataDir` in paddock.ts is the one rule all three use.
74
+ */
75
+ export async function runBusy(home, json) {
76
+ const out = createOutput({ tool: "paddock", json });
77
+ return runBusyCommand(paddockBusyOptions(home, out));
78
+ }
79
+ /**
80
+ * The check before `service stop` and `service restart` (SPEC §1): refuse with
81
+ * 75 unless `--force`, which prints the same message prefixed `Overriding:` and
82
+ * carries on. Returns null to go ahead.
83
+ *
84
+ * `home` is the data dir of the INSTALLED unit, not whatever `--data-dir` this
85
+ * invocation carries — the same rule `status` and `logs` follow (#824), because
86
+ * the instance being stopped is the one the unit describes.
87
+ *
88
+ * Two deliberate non-refusals. Nothing answering goes ahead: there is nothing
89
+ * to cut short, which is also the answer for a service that is already stopped.
90
+ * And an instance running a Paddock from before this release has no
91
+ * `runtime.json`, so it reads as not running and the stop proceeds exactly as
92
+ * it did before — an upgrade cannot make a stop start failing.
93
+ */
94
+ export async function guardServiceBusy(action, home, opts) {
95
+ // No `result()` call on this path, so `--json` only decides which stream the
96
+ // prose goes to — the one JSON document a `--json` stop may print belongs to
97
+ // the stop, not to the check it ran first.
98
+ const out = createOutput({ tool: "paddock", json: opts.json === true });
99
+ const refusal = await guardBusy({
100
+ ...paddockBusyOptions(home, out),
101
+ ...(opts.force === true ? { force: true } : {}),
102
+ });
103
+ if (refusal === null)
104
+ return null;
105
+ // Spell out the way past it, after the hint about where to run it from.
106
+ out.warn(`\n paddock service ${action} --force ${action} it anyway\n`);
107
+ return EXIT.busy;
108
+ }
@@ -0,0 +1,69 @@
1
+ import type { Output } from "./output.js";
2
+ /** What `/api/busy` answers (SPEC §4). Counts only, until §10's first question is settled. */
3
+ export interface BusyAnswer {
4
+ busy: boolean;
5
+ version?: string;
6
+ counts?: Record<string, number>;
7
+ }
8
+ export type BusyResult = {
9
+ state: "not-running";
10
+ } | {
11
+ state: "idle" | "busy";
12
+ url: string;
13
+ answer: BusyAnswer;
14
+ }
15
+ /** Something answered, but not with a busy answer: an older version without `/api/busy`, or another program. */
16
+ | {
17
+ state: "unknown";
18
+ url: string;
19
+ detail: string;
20
+ };
21
+ export interface FindOptions {
22
+ /** The tool's data home, holding `runtime.json`. */
23
+ home: string;
24
+ /** Ask this URL instead of the one in `runtime.json` (`--url`, or a port the user named). */
25
+ url?: string;
26
+ /** How long to wait for an answer. §4 asks for well under a second; this allows for a slow machine. */
27
+ timeoutMs?: number;
28
+ fetch?: typeof globalThis.fetch;
29
+ }
30
+ /**
31
+ * Asks the running instance whether it's busy. There's no fallback to a default port: an instance that's running
32
+ * wrote `runtime.json`, and guessing a port would ask whatever else is listening there.
33
+ */
34
+ export declare function checkBusy(o: FindOptions): Promise<BusyResult>;
35
+ /** A kind's singular and plural, where adding an "s" is wrong: `{ batch: ['batch', 'batches'] }`. */
36
+ export type Nouns = Record<string, [singular: string, plural: string]>;
37
+ /** `2 chats and 1 job are running`, from the non-zero counts; '' if there are none. */
38
+ export declare function describeCounts(counts?: Record<string, number>, nouns?: Nouns): string;
39
+ /** The sentence for a person (SPEC §4): `Paddock is busy: 2 chats are running.` */
40
+ export declare function busyMessage(name: string, result: BusyResult, nouns?: Nouns): string;
41
+ /** 0 idle, 75 busy, 3 nothing answers, 1 an answer that isn't a busy answer. */
42
+ export declare function busyExitCode(result: BusyResult): number;
43
+ /** What `<tool> busy --json` prints: valid against `schemas/busy.schema.json`. */
44
+ export declare function busyJson(result: BusyResult): Record<string, unknown>;
45
+ /**
46
+ * LOCAL CHANGE (see the header): what to suggest when the answer is busy, as a string or as a function of the
47
+ * result. {@link resolveHint} is what both the command and the guard call, so the two can never print different
48
+ * advice about the same answer.
49
+ */
50
+ export type BusyHint = string | ((result: BusyResult) => string | undefined);
51
+ export interface BusyCommandOptions extends FindOptions {
52
+ /** The product's name, for messages: `Local Review`. */
53
+ name: string;
54
+ out: Output;
55
+ nouns?: Nouns;
56
+ /** LOCAL CHANGE: the advice printed after a busy message — Paddock's keeper-chat trap. */
57
+ hint?: BusyHint;
58
+ }
59
+ /** `<tool> busy [--json]`. Returns the exit code; the caller sets `process.exitCode`. */
60
+ export declare function runBusyCommand(o: BusyCommandOptions): Promise<number>;
61
+ export interface GuardOptions extends BusyCommandOptions {
62
+ force?: boolean;
63
+ }
64
+ /**
65
+ * The check before `service stop`, `service restart` and `update` (SPEC §4). Returns null to go ahead, or 75 after
66
+ * printing the refusal. With `--force` it prints the same message, prefixed `Overriding:`, and goes ahead.
67
+ * Nothing answering goes ahead: there's nothing to cut short.
68
+ */
69
+ export declare function guardBusy(o: GuardOptions): Promise<number | null>;