@edspencer/paddock 0.80.0 → 0.82.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 +1 -1
  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 +70 -29
  11. package/packages/server/dist/cli/args.js +306 -7
  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/output.d.ts +52 -0
  17. package/packages/server/dist/cli/output.js +84 -0
  18. package/packages/server/dist/cli/paddock.js +65 -21
  19. package/packages/server/dist/cli/service/index.d.ts +64 -0
  20. package/packages/server/dist/cli/service/index.js +208 -7
  21. package/packages/server/dist/cli/service/launchd.d.ts +15 -1
  22. package/packages/server/dist/cli/service/launchd.js +116 -18
  23. package/packages/server/dist/cli/service/ready.d.ts +30 -0
  24. package/packages/server/dist/cli/service/ready.js +35 -0
  25. package/packages/server/dist/cli/service/systemd.js +27 -0
  26. package/packages/server/dist/cli/update/install-method.d.ts +131 -0
  27. package/packages/server/dist/cli/update/install-method.js +258 -0
  28. package/packages/server/dist/cli/update/lock.d.ts +40 -0
  29. package/packages/server/dist/cli/update/lock.js +108 -0
  30. package/packages/server/dist/cli/update/package-manager.d.ts +109 -0
  31. package/packages/server/dist/cli/update/package-manager.js +221 -0
  32. package/packages/server/dist/cli/update/registry.d.ts +89 -0
  33. package/packages/server/dist/cli/update/registry.js +145 -0
  34. package/packages/server/dist/cli/update/semver.d.ts +68 -0
  35. package/packages/server/dist/cli/update/semver.js +232 -0
  36. package/packages/server/dist/cli/update/update-check.d.ts +80 -0
  37. package/packages/server/dist/cli/update/update-check.js +116 -0
  38. package/packages/server/dist/cli/update/update.d.ts +160 -0
  39. package/packages/server/dist/cli/update/update.js +609 -0
  40. package/packages/server/dist/cli/update-command.d.ts +20 -0
  41. package/packages/server/dist/cli/update-command.js +113 -0
  42. package/packages/server/dist/herdctl.d.ts +19 -0
  43. package/packages/server/dist/herdctl.js +26 -0
  44. package/packages/server/dist/routes/meta.js +45 -0
  45. package/packages/server/dist/runtime-file.d.ts +26 -0
  46. package/packages/server/dist/runtime-file.js +91 -0
  47. package/packages/server/dist/start.d.ts +33 -0
  48. package/packages/server/dist/start.js +115 -8
  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-DHczLhwQ.js → DeleteProjectDialog-CphdGB3R.js} +1 -1
  53. package/packages/web/dist/assets/DiscoverPage-C4OPLNO8.js +2 -0
  54. package/packages/web/dist/assets/{DiscoverView-DtTOpTji.js → DiscoverView-B_kcF4sm.js} +1 -1
  55. package/packages/web/dist/assets/{InstanceConfigPage-BRoBh1C1.js → InstanceConfigPage-CvUK5DKW.js} +1 -1
  56. package/packages/web/dist/assets/{MarkdownRenderer-DJf8IJuk.js → MarkdownRenderer-DvZ-j_Ao.js} +1 -1
  57. package/packages/web/dist/assets/{ProjectRedirect-EioguUKW.js → ProjectRedirect-DayHvUH2.js} +1 -1
  58. package/packages/web/dist/assets/{ProjectView-BiKnGfk6.js → ProjectView-CCw83gUi.js} +3 -3
  59. package/packages/web/dist/assets/{ProjectsGrid-D6kfaIal.js → ProjectsGrid-B3Klg2H4.js} +1 -1
  60. package/packages/web/dist/assets/{RootHome-DpcyuV6e.js → RootHome-BxcDvpgI.js} +1 -1
  61. package/packages/web/dist/assets/{Surfaces-CHM29PhP.js → Surfaces-7UlhFBSG.js} +1 -1
  62. package/packages/web/dist/assets/{arc-fiqD05Hu.js → arc-DNkh5o5Q.js} +1 -1
  63. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BuuB2Nkf.js → architectureDiagram-3BPJPVTR-BrgN3oLF.js} +1 -1
  64. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DQMBuzfi.js → blockDiagram-GPEHLZMM-D05XOJSk.js} +1 -1
  65. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-BKN0UxCH.js → c4Diagram-AAUBKEIU-CKaMBhMP.js} +1 -1
  66. package/packages/web/dist/assets/channel-JrEedxB8.js +2 -0
  67. package/packages/web/dist/assets/{chunk-2J33WTMH-DW37EupX.js → chunk-2J33WTMH-8hgadCQc.js} +1 -1
  68. package/packages/web/dist/assets/{chunk-4BX2VUAB-Dyv7zV_c.js → chunk-4BX2VUAB-CBfEC-pu.js} +1 -1
  69. package/packages/web/dist/assets/{chunk-55IACEB6-DzMR0dOk.js → chunk-55IACEB6-DV1eMgHk.js} +1 -1
  70. package/packages/web/dist/assets/{chunk-727SXJPM-BdZ3Sor8.js → chunk-727SXJPM-Cfgl5fxY.js} +1 -1
  71. package/packages/web/dist/assets/{chunk-AQP2D5EJ-B4i7_og0.js → chunk-AQP2D5EJ-BXhqD295.js} +1 -1
  72. package/packages/web/dist/assets/{chunk-FMBD7UC4-jcTr85it.js → chunk-FMBD7UC4-TFGOPtek.js} +1 -1
  73. package/packages/web/dist/assets/{chunk-ND2GUHAM-fgMsadqB.js → chunk-ND2GUHAM-CHFM665R.js} +1 -1
  74. package/packages/web/dist/assets/{chunk-QZHKN3VN-bJNHYS1W.js → chunk-QZHKN3VN-CqEf6MMT.js} +1 -1
  75. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CHJiQp-W.js +2 -0
  76. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CHJiQp-W.js +2 -0
  77. package/packages/web/dist/assets/{core-Bk4KFrmN.js → core-DmEaKxBb.js} +1 -1
  78. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BIqCSlQy.js → cose-bilkent-S5V4N54A-BnGI6jkr.js} +1 -1
  79. package/packages/web/dist/assets/{dagre-BM42HDAG-6nyhtVQz.js → dagre-BM42HDAG-CKoPx61E.js} +1 -1
  80. package/packages/web/dist/assets/{diagram-2AECGRRQ-CjizfipR.js → diagram-2AECGRRQ-mMmtlBl1.js} +1 -1
  81. package/packages/web/dist/assets/{diagram-5GNKFQAL-DlgB5vWb.js → diagram-5GNKFQAL-Cdz9Mtnt.js} +1 -1
  82. package/packages/web/dist/assets/{diagram-KO2AKTUF-CNC9qanV.js → diagram-KO2AKTUF-DQxM3wYj.js} +1 -1
  83. package/packages/web/dist/assets/{diagram-LMA3HP47-DSJ-s9lh.js → diagram-LMA3HP47-BWrinPsq.js} +1 -1
  84. package/packages/web/dist/assets/{diagram-OG6HWLK6-C9FoHBDg.js → diagram-OG6HWLK6-U6MYUve2.js} +1 -1
  85. package/packages/web/dist/assets/{documentTitle-Cj_AY97j.js → documentTitle-DGjRXuTP.js} +1 -1
  86. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-z6gku4Yt.js → erDiagram-TEJ5UH35-xqMCsXxY.js} +1 -1
  87. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-Dl5c_FgU.js → flowDiagram-I6XJVG4X-PMfy_czF.js} +1 -1
  88. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CIscJ-Dt.js → ganttDiagram-6RSMTGT7-whhYYE4N.js} +1 -1
  89. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CvttOXM_.js → gitGraphDiagram-PVQCEYII-C51JW4tf.js} +1 -1
  90. package/packages/web/dist/assets/{index-DN82do6Z.js → index-CHOtvlnO.js} +2 -2
  91. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-dzWYdDJj.js → infoDiagram-5YYISTIA-SH8cr905.js} +1 -1
  92. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-8yX9k4bo.js → ishikawaDiagram-YF4QCWOH-BjqaoU3U.js} +1 -1
  93. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-BmitzJDR.js → journeyDiagram-JHISSGLW-CBmdEVrd.js} +1 -1
  94. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BZmIkfkW.js → kanban-definition-UN3LZRKU-U8njjMLZ.js} +1 -1
  95. package/packages/web/dist/assets/{linear-BBLXE4o7.js → linear-DF66GPs1.js} +1 -1
  96. package/packages/web/dist/assets/{mermaid.core-mSKff4yu.js → mermaid.core-CouJz7Ph.js} +4 -4
  97. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BWAUa4CP.js → mindmap-definition-RKZ34NQL-BYIf0oFt.js} +1 -1
  98. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-VmVdiL4C.js → pieDiagram-4H26LBE5-Cm8rgg09.js} +1 -1
  99. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Dybfv6NU.js → quadrantDiagram-W4KKPZXB-C3Il5kOH.js} +1 -1
  100. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-BPnOvB7w.js → requirementDiagram-4Y6WPE33-D3PE2nTe.js} +1 -1
  101. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-YXIEmKw7.js → sankeyDiagram-5OEKKPKP-DdmvBM18.js} +1 -1
  102. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-DintIRUe.js → sequenceDiagram-3UESZ5HK-B8M_GAIq.js} +1 -1
  103. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-eMsjahWp.js → stateDiagram-AJRCARHV-DZkXnK1_.js} +1 -1
  104. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CjCdT9Ls.js +2 -0
  105. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-as4zRCPn.js → timeline-definition-PNZ67QCA-IHXq7rqi.js} +1 -1
  106. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-CPzvUkM0.js → vennDiagram-CIIHVFJN-Dga7tLtT.js} +1 -1
  107. package/packages/web/dist/assets/{wardley-L42UT6IY-q6moXj5g.js → wardley-L42UT6IY-CmcwnEJ-.js} +1 -1
  108. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-BsEEb9Gm.js → wardleyDiagram-YWT4CUSO-zHuSLrD4.js} +1 -1
  109. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DSgZlcZW.js → xychartDiagram-2RQKCTM6-D93MU_ic.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-BSGSzTEs.js +0 -2
  113. package/packages/web/dist/assets/channel-B7yh6Su_.js +0 -2
  114. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DIlhMPgi.js +0 -2
  115. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DIlhMPgi.js +0 -2
  116. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CpAOGzbO.js +0 -2
@@ -88,7 +88,7 @@ export const CONFIG_ACTIONS = ["show", "eject"];
88
88
  * Paddock and Managers, every existing service unit's `ExecStart` names it, and
89
89
  * it is in two years of other people's scripts. They parse to the same command.
90
90
  */
91
- export const VERBS = ["serve", "start", "service", "config"];
91
+ export const VERBS = ["serve", "start", "service", "config", "busy", "update", "upgrade"];
92
92
  /**
93
93
  * Split an action off a verb's argv, then parse what follows as flags.
94
94
  *
@@ -141,7 +141,74 @@ function parseAction(verb, actions, rest) {
141
141
  * for. Corrected rather than deleted because the grammar it documents is real
142
142
  * and load-bearing; it was only the example's error that was misdescribed.
143
143
  */
144
+ /**
145
+ * Flags that mean something on SOME commands only, and the commands they mean
146
+ * it on.
147
+ *
148
+ * Rejecting a flag the command cannot act on, rather than parsing and dropping
149
+ * it, is the difference between being told and guessing: the two readings of a
150
+ * quiet `service uninstall --force` are "it overrode something" and "it did
151
+ * nothing", and the exit code is 0 either way. The same trap sits under
152
+ * `service status --refresh` and `serve --check`.
153
+ *
154
+ * One table, checked once in {@link parseCommand}, rather than a rule per
155
+ * command — a second copy of this logic is a second chance for the two to
156
+ * disagree about where a flag applies. `where` is matched against the
157
+ * invocation as the user would write it (`service stop`, `update`).
158
+ */
159
+ const SCOPED_FLAGS = [
160
+ {
161
+ flag: "--force",
162
+ read: (o) => o.force === true,
163
+ where: ["service stop", "service restart", "update"],
164
+ why: "It overrides the busy check, and those are the commands that run one.",
165
+ },
166
+ {
167
+ flag: "--check",
168
+ read: (o) => o.checkOnly === true,
169
+ where: ["update"],
170
+ why: "It reports what an update would do, and changes nothing.",
171
+ },
172
+ {
173
+ flag: "--to",
174
+ read: (o) => o.to !== undefined,
175
+ where: ["update"],
176
+ why: "It names the version or dist-tag to move to.",
177
+ },
178
+ {
179
+ flag: "--yes",
180
+ read: (o) => o.yes === true,
181
+ where: ["update"],
182
+ why: "`update` is the only command that asks a question.",
183
+ },
184
+ {
185
+ flag: "--refresh",
186
+ read: (o) => o.refresh === true,
187
+ where: ["service install"],
188
+ why: "It re-renders an existing unit, keeping the settings it carries.",
189
+ },
190
+ ];
144
191
  export function parseCommand(argv) {
192
+ const command = dispatch(argv);
193
+ // `--help` wins over every rule below, the same way it wins over a missing
194
+ // action above: asking for the usage is not a usage error.
195
+ if (command.opts.help)
196
+ return command;
197
+ const said = "action" in command && command.action !== undefined
198
+ ? `${command.verb} ${command.action}`
199
+ : command.verb;
200
+ for (const rule of SCOPED_FLAGS) {
201
+ if (!rule.read(command.opts))
202
+ continue;
203
+ if (rule.where.includes(said))
204
+ continue;
205
+ const quoted = rule.where.map((w) => `\`${w}\``);
206
+ const list = quoted.length === 1 ? quoted[0] : `${quoted.slice(0, -1).join(", ")} and ${quoted.at(-1)}`;
207
+ throw new UsageError(`${rule.flag} applies to ${list} only, not \`${said}\`.\n${rule.why}`);
208
+ }
209
+ return command;
210
+ }
211
+ function dispatch(argv) {
145
212
  const [first, ...rest] = argv;
146
213
  if (first === "service") {
147
214
  return { verb: "service", ...parseAction("service", SERVICE_ACTIONS, rest) };
@@ -149,6 +216,14 @@ export function parseCommand(argv) {
149
216
  if (first === "config") {
150
217
  return { verb: "config", ...parseAction("config", CONFIG_ACTIONS, rest) };
151
218
  }
219
+ // `busy` (SPEC §1, §4). A top-level verb rather than a `service` action
220
+ // because its whole point is to be askable where there is no service to ask:
221
+ // a Docker container, a Watchtower pre-update hook, an Ansible task.
222
+ if (first === "busy")
223
+ return { verb: "busy", opts: parseArgs(rest) };
224
+ // `update`, with `upgrade` accepted and undocumented (SPEC §1).
225
+ if (first === "update" || first === "upgrade")
226
+ return { verb: "update", opts: parseArgs(rest) };
152
227
  // `serve` and `start` are one command: see VERBS.
153
228
  if (first === "serve" || first === "start")
154
229
  return { verb: "serve", opts: parseArgs(rest) };
@@ -220,6 +295,31 @@ export function parseArgs(argv) {
220
295
  case "--include-env":
221
296
  opts.includeEnv = true;
222
297
  break;
298
+ // `service stop` / `service restart` / `update`: override the busy check
299
+ // (SPEC §4).
300
+ case "--force":
301
+ opts.force = true;
302
+ break;
303
+ // `update` flags (SPEC §1).
304
+ case "--check":
305
+ opts.checkOnly = true;
306
+ break;
307
+ case "--to": {
308
+ const v = next();
309
+ if (v === "" || v.startsWith("-")) {
310
+ throw new UsageError(`${arg} needs a version or a dist-tag, got: ${JSON.stringify(v)}`);
311
+ }
312
+ opts.to = v;
313
+ break;
314
+ }
315
+ case "-y":
316
+ case "--yes":
317
+ opts.yes = true;
318
+ break;
319
+ // `service install --refresh` (SPEC §5 step 11).
320
+ case "--refresh":
321
+ opts.refresh = true;
322
+ break;
223
323
  // `service logs` flags, the names `tail`, `journalctl` and every other
224
324
  // log reader uses — this is not the place to be original.
225
325
  case "-f":
@@ -293,6 +393,8 @@ export function explainListenError(err, host, port) {
293
393
  export const USAGE_SYNOPSIS = `Usage
294
394
  paddock [serve] [options] run the server in this terminal
295
395
  paddock service <action> [options] install|uninstall|status|start|stop|restart|logs
396
+ paddock busy [--json] is the running instance busy?
397
+ paddock update [--check] [--to <v>] install a newer Paddock
296
398
  paddock config <show|eject> [options] what this instance's config resolved to
297
399
  paddock -v | --version print the version and exit
298
400
  paddock -h | --help the full help`;
@@ -306,10 +408,16 @@ Usage
306
408
  paddock service uninstall stop it and remove the unit
307
409
  paddock service status [--json] installed? running? which version?
308
410
  paddock service start start an installed service
309
- paddock service stop stop it, leave it installed
310
- paddock service restart stop it and start it again
411
+ paddock service stop [--force] stop it, leave it installed
412
+ paddock service restart [--force] stop it and start it again
311
413
  paddock service logs [-f] [-n <lines>] the service's log
312
414
  (\`paddock service --help\`)
415
+ paddock busy [--json] is it busy? exit 0 no, 75 yes, 3
416
+ nothing answering
417
+ (\`paddock busy --help\`)
418
+ paddock update [--check] [--to <v>] install a newer Paddock and restart
419
+ the service
420
+ (\`paddock update --help\`)
313
421
  paddock config show [--resolved] what this instance's config resolved
314
422
  to, and where each value came from
315
423
  paddock config eject [--write] freeze that resolution into
@@ -326,11 +434,15 @@ Options
326
434
  -h, --help Show this help
327
435
 
328
436
  Exit codes
329
- 0 done; running and answering
437
+ 0 done; running and answering; up to date; not busy
330
438
  1 failed
331
439
  2 usage: an unknown command or flag, or a bad value
332
- 3 not running: installed, but not running or not answering
440
+ 3 not running: installed, but not running or not answering; nothing answers
333
441
  4 not installed: there is no service unit
442
+ 5 this install cannot update itself; the right command was printed (update)
443
+ 6 the update failed and was rolled back; the old version is back (update)
444
+ 7 the update failed and could not be rolled back; a person is needed
445
+ 75 busy: refused, and why was printed. Try later, or use --force
334
446
 
335
447
  Opening your own directories
336
448
  Where you run this from does not matter — the instance is decided by its data
@@ -434,18 +546,44 @@ export const SERVICE_USAGE = `paddock service — keep Paddock running in the ba
434
546
 
435
547
  Usage
436
548
  paddock service install [options] register it and start it now
549
+ paddock service install --refresh re-write the unit, keeping its settings
437
550
  paddock service uninstall stop it and deregister it
438
551
  paddock service status [--json] is it registered, is it running, which
439
552
  version, where are the logs
440
553
  paddock service start start an installed service that is stopped
441
- paddock service stop stop it, and leave it installed
442
- paddock service restart stop it and start it again, re-reading config
554
+ paddock service stop [--force] stop it, and leave it installed
555
+ paddock service restart [--force] stop it and start it again, re-reading config
443
556
  paddock service logs [-f] [-n <n>] the service's log (the last 50 lines)
444
557
 
445
558
  Options (logs)
446
559
  -f, --follow keep printing what gets appended, until Ctrl-C
447
560
  -n, --lines <n> how many lines to print first (default 50)
448
561
 
562
+ Options (stop, restart)
563
+ --force stop it even though it is busy
564
+
565
+ Options (install)
566
+ --refresh Re-render the unit from the settings the EXISTING one
567
+ carries — its port, host, data dir and verbosity —
568
+ rather than from this shell's flags and environment.
569
+ This is what \`paddock update\` runs, through the new
570
+ version's CLI, so the unit is written in the format
571
+ that version writes. Exit 4 with nothing installed:
572
+ there is no unit to refresh, and doing a first
573
+ install with defaults instead is how a --port 9000
574
+ service comes back on 7233.
575
+
576
+ Busy
577
+ \`stop\` and \`restart\` ask the running instance whether it is busy first, and
578
+ refuse with exit 75 if it is — a chat turn in flight, a background task, a
579
+ scheduled job. \`paddock busy\` asks the same question on its own.
580
+
581
+ Being refused is the useful case: a restart kills a chat turn mid-sentence,
582
+ and the turn cannot be resumed from where it was. Wait, or --force.
583
+
584
+ \`uninstall\` does not ask. Removing the service is an explicit act, and
585
+ someone uninstalling it has already decided.
586
+
449
587
  Options (install only — recorded in the generated unit)
450
588
  -p, --port <port> HTTP/WS port (default 7233)
451
589
  --host <host> Bind address (default 127.0.0.1)
@@ -490,6 +628,7 @@ Exit codes
490
628
  4 not installed: there is no unit (status, logs, start, stop, restart)
491
629
  1 something failed
492
630
  2 usage: an unknown action or flag, or a bad value
631
+ 75 busy, and --force was not given (stop, restart)
493
632
 
494
633
  Installed from npx?
495
634
  \`service install\` refuses. An npx cache path is hash-keyed and npm may prune
@@ -505,6 +644,66 @@ A note on access
505
644
  process that could reach the port could already read the same Claude login as
506
645
  you. Set PADDOCK_AUTH_MODE if you want a credential on it anyway.
507
646
 
647
+ Docs: https://github.com/edspencer/paddock`;
648
+ export const BUSY_USAGE = `paddock busy — is the running instance in the middle of something?
649
+
650
+ Usage
651
+ paddock busy ask, and say so in a sentence
652
+ paddock busy --json the same answer as one JSON document
653
+ paddock busy -d <path> ask a particular instance
654
+
655
+ Exit codes — this command is FOR its exit code
656
+ 0 not busy. Nothing a restart would cut short.
657
+ 75 busy. What is running was printed.
658
+ 3 nothing answered. No instance is running, so there is nothing to wait for.
659
+ 1 something answered, but not as Paddock's busy check.
660
+
661
+ Options
662
+ -d, --data-dir <path> Which instance to ask (default ~/.paddock, or
663
+ \$PADDOCK_DATA_DIR) — the same rule \`paddock serve\`
664
+ uses, so the two always mean the same instance
665
+ --json One JSON document on stdout: running, busy, url,
666
+ version, counts. Prose goes to stderr
667
+ -h, --help Show this help
668
+
669
+ What counts as busy
670
+ Only work a restart would LOSE or cut short:
671
+
672
+ chat a chat turn running — a human turn, a fired trigger, a cron
673
+ fire. A turn is a live claude subprocess writing a transcript,
674
+ and it cannot be resumed from where it was interrupted.
675
+ background a background task a live session is still holding after its
676
+ turn ended: a background shell, a sub-agent, a Monitor.
677
+ job a scheduled job herdctl is running.
678
+
679
+ Not busy: an idle session (it resumes), an open browser tab (it reconnects),
680
+ a pending curation sweep (it re-runs from its watermark).
681
+
682
+ Where to run it from
683
+ From a terminal. A Paddock chat that runs this is itself one of the chat turns
684
+ it counts, so it will always see itself as busy — and \`service stop --force\`
685
+ from inside a chat stops the service that is running the chat, which does not
686
+ come back with an explanation. That is why the refusal says so.
687
+
688
+ For scripts, containers and Watchtower
689
+ \`busy\` is there for callers with no service to ask. 75 is EX_TEMPFAIL, which
690
+ is the code Watchtower's pre-update hook reads as "skip this one":
691
+
692
+ labels:
693
+ com.centurylinklabs.watchtower.lifecycle.pre-update: paddock busy
694
+
695
+ An Ansible task does the same thing with a retry. Nothing here contacts the
696
+ network or the npm registry, and the question is answered from memory — it is
697
+ cheap enough to ask in a loop.
698
+
699
+ How it finds the instance
700
+ \`<data-dir>/runtime.json\`, written 0600 when the server starts listening and
701
+ removed when it stops. There is no fallback to a default port: guessing one
702
+ would ask whatever else happened to be listening there, and get an answer.
703
+
704
+ A Paddock from before this feature shipped writes no such file, so it reads as
705
+ not running (exit 3). Upgrade it and restart it once.
706
+
508
707
  Docs: https://github.com/edspencer/paddock`;
509
708
  export const CONFIG_USAGE = `paddock config — what this instance's configuration actually resolved to
510
709
 
@@ -602,3 +801,103 @@ Reading nothing, writing nothing
602
801
  JWT signing material here to hide or reveal.
603
802
 
604
803
  Docs: https://github.com/edspencer/paddock`;
804
+ /**
805
+ * `update`'s own synopsis, printed after a usage error from it (SPEC §2).
806
+ *
807
+ * `update` has one usage error of its own — "there is no terminal to ask, so
808
+ * `--yes` is required" — and the contract says the usage follows the message.
809
+ * {@link USAGE_SYNOPSIS} is the wrong text there: it lists every command, and
810
+ * the reader's problem is one flag on the command they already typed.
811
+ */
812
+ export const UPDATE_SYNOPSIS = `Usage
813
+ paddock update [--yes] install the latest release
814
+ paddock update --check [--json] what a new version would be; changes nothing
815
+ paddock update --to <version|tag> a particular version, or a dist-tag
816
+ paddock update --force skip the busy check, and nothing else
817
+ paddock update --help the full help`;
818
+ export const UPDATE_USAGE = `paddock update — install a newer Paddock, and put the service back up
819
+
820
+ Usage
821
+ paddock update update to the latest release, asking first
822
+ paddock update --check what a new version would be; changes nothing
823
+ paddock update --yes do not ask (required when there is no terminal)
824
+ paddock update --to 0.79.1 a particular version — the only way to go BACK
825
+ paddock update --to next a dist-tag
826
+ paddock update --json one JSON document on stdout
827
+
828
+ Options
829
+ --check Ask the registry, report, and change nothing. Exits 0
830
+ whether or not there is an update — scripts read
831
+ \`updateAvailable\` from --json
832
+ --to <version|tag> Install this version or dist-tag instead of latest.
833
+ Required to move to a LOWER version: an update never
834
+ goes backwards by accident
835
+ --yes, -y Skip the question. Required when stdin is not a
836
+ terminal, where a command that would have asked exits
837
+ 2 rather than guessing
838
+ --force Skip the busy check, and nothing else — the health
839
+ check and the rollback still run
840
+ --json One JSON document on stdout; all prose on stderr
841
+ -h, --help Show this help
842
+
843
+ What it does, in order
844
+ 1 takes <data-dir>/update.lock, so two updates cannot interleave
845
+ 2 works out how this copy was installed (see "Installs that cannot update
846
+ themselves" below)
847
+ 3 asks the registry for the target version and its \`engines.node\`
848
+ 4 compares, and stops here with --check
849
+ 5 checks the global folder is writable, and prints a sudo line if not.
850
+ It never runs sudo itself
851
+ 6 asks the running instance whether it is BUSY — before anything is stopped
852
+ 7 asks you, unless --yes
853
+ 8 fetches both the new and the current version into the npm cache, while
854
+ the old version is still serving, so a rollback has its package to hand
855
+ 9 stops the service
856
+ 10 installs with the package manager this copy came from
857
+ 11 hands over to the NEW CLI, which re-writes the service unit
858
+ (\`service install --refresh\`) and starts it
859
+ 12 waits for /api/health to report the version it just installed
860
+ 13 and if any of 10–12 fails, puts the old version back
861
+
862
+ Exit codes — this command is FOR its exit code
863
+ 0 updated, or already up to date, or --check reported
864
+ 1 something failed, and nothing had been stopped yet
865
+ 2 usage — including "there is no terminal to ask, so --yes is required"
866
+ 5 this install cannot update itself. The right command was printed
867
+ 6 the new version did not start; the old one is running again
868
+ 7 the new version did not start and the old one could not be restored.
869
+ The commands to run by hand were printed
870
+ 75 busy: a chat turn, a background task or a scheduled job is running.
871
+ Wait, or --force
872
+
873
+ Installs that cannot update themselves (exit 5)
874
+ Paddock reads where it is running from, and five layouts are not ours to
875
+ replace. Each prints the command that WOULD work:
876
+
877
+ a container image docker compose pull && docker compose up -d
878
+ a managed install whatever PADDOCK_UPDATE_HINT says
879
+ npx / pnpm dlx npx @edspencer/paddock@latest already runs the latest
880
+ a checkout git pull && npm install && npm run build
881
+ a project dependency update it in that project
882
+ a yarn classic global yarn global add @edspencer/paddock
883
+
884
+ A global npm, pnpm, bun or volta install updates in place.
885
+
886
+ Where it runs from
887
+ From a terminal, never from inside the service. An updater the service
888
+ started — a Paddock chat, say — is in the service's own process group, so
889
+ stopping the service kills the updater half way through step 9. The busy
890
+ check refuses first and says so; --force from inside a chat would stop the
891
+ service running that chat, which then never gets to report what happened.
892
+
893
+ Asking the registry
894
+ Only \`update\`, \`update --check\` and \`doctor\` ever contact the npm
895
+ registry, and only when you run them. There is no background check and no
896
+ check at startup; the server never asks. What the last check found is cached
897
+ in <data-dir>/update-check.json, which is what \`paddock service status\`
898
+ repeats — it never asks itself.
899
+
900
+ The registry is \`npm_config_registry\`, else \`npm config get registry\`,
901
+ else https://registry.npmjs.org.
902
+
903
+ Docs: https://github.com/edspencer/paddock`;
@@ -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>;