@edspencer/paddock 0.82.0 → 0.82.1

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 (107) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/cli/args.js +2 -2
  3. package/packages/server/dist/cli/busy-command.js +1 -1
  4. package/packages/server/dist/cli/busy.js +10 -12
  5. package/packages/server/dist/cli/exit-codes.d.ts +21 -45
  6. package/packages/server/dist/cli/exit-codes.js +42 -45
  7. package/packages/server/dist/cli/output.js +5 -6
  8. package/packages/server/dist/cli/paddock.js +9 -9
  9. package/packages/server/dist/cli/service/index.d.ts +2 -2
  10. package/packages/server/dist/cli/service/index.js +30 -24
  11. package/packages/server/dist/cli/service/launchd.d.ts +31 -84
  12. package/packages/server/dist/cli/service/launchd.js +104 -304
  13. package/packages/server/dist/cli/service/logs.d.ts +9 -28
  14. package/packages/server/dist/cli/service/logs.js +33 -75
  15. package/packages/server/dist/cli/service/manager.d.ts +93 -0
  16. package/packages/server/dist/cli/service/manager.js +21 -0
  17. package/packages/server/dist/cli/service/path-env.d.ts +7 -18
  18. package/packages/server/dist/cli/service/path-env.js +30 -81
  19. package/packages/server/dist/cli/service/spec.d.ts +4 -31
  20. package/packages/server/dist/cli/service/spec.js +9 -0
  21. package/packages/server/dist/cli/service/systemd.d.ts +27 -101
  22. package/packages/server/dist/cli/service/systemd.js +106 -262
  23. package/packages/server/dist/cli/update/health.d.ts +48 -0
  24. package/packages/server/dist/cli/update/health.js +64 -0
  25. package/packages/server/dist/cli/update/install-method.d.ts +29 -76
  26. package/packages/server/dist/cli/update/install-method.js +103 -178
  27. package/packages/server/dist/cli/update/lock.d.ts +7 -12
  28. package/packages/server/dist/cli/update/lock.js +28 -62
  29. package/packages/server/dist/cli/update/package-manager.d.ts +39 -74
  30. package/packages/server/dist/cli/update/package-manager.js +95 -160
  31. package/packages/server/dist/cli/update/registry.d.ts +17 -54
  32. package/packages/server/dist/cli/update/registry.js +46 -89
  33. package/packages/server/dist/cli/update/semver.d.ts +18 -41
  34. package/packages/server/dist/cli/update/semver.js +74 -124
  35. package/packages/server/dist/cli/update/update-check.d.ts +15 -26
  36. package/packages/server/dist/cli/update/update-check.js +30 -60
  37. package/packages/server/dist/cli/update/update.d.ts +57 -91
  38. package/packages/server/dist/cli/update/update.js +230 -431
  39. package/packages/server/dist/cli/update-command.js +3 -5
  40. package/packages/server/package.json +1 -1
  41. package/packages/web/dist/assets/{DeleteProjectDialog-CphdGB3R.js → DeleteProjectDialog-CgHpQctt.js} +1 -1
  42. package/packages/web/dist/assets/DiscoverPage-CG6cf-0i.js +2 -0
  43. package/packages/web/dist/assets/{DiscoverView-B_kcF4sm.js → DiscoverView-BKqI0I1E.js} +1 -1
  44. package/packages/web/dist/assets/{InstanceConfigPage-CvUK5DKW.js → InstanceConfigPage-DL3raCK1.js} +1 -1
  45. package/packages/web/dist/assets/{MarkdownRenderer-DvZ-j_Ao.js → MarkdownRenderer-DcjEuf3s.js} +1 -1
  46. package/packages/web/dist/assets/{ProjectRedirect-DayHvUH2.js → ProjectRedirect-DerkA-nE.js} +1 -1
  47. package/packages/web/dist/assets/{ProjectView-CCw83gUi.js → ProjectView-NwfC3_w7.js} +3 -3
  48. package/packages/web/dist/assets/{ProjectsGrid-B3Klg2H4.js → ProjectsGrid-BeORa0UH.js} +1 -1
  49. package/packages/web/dist/assets/{RootHome-BxcDvpgI.js → RootHome-BYJs8qJ6.js} +1 -1
  50. package/packages/web/dist/assets/{Surfaces-7UlhFBSG.js → Surfaces-w-kEaAYU.js} +1 -1
  51. package/packages/web/dist/assets/{arc-DNkh5o5Q.js → arc-Bv56hasM.js} +1 -1
  52. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BrgN3oLF.js → architectureDiagram-3BPJPVTR-BHEknx9i.js} +1 -1
  53. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-D05XOJSk.js → blockDiagram-GPEHLZMM-DtxTMrPX.js} +1 -1
  54. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-CKaMBhMP.js → c4Diagram-AAUBKEIU-DRpEYOlM.js} +1 -1
  55. package/packages/web/dist/assets/channel-CdIiAqY2.js +2 -0
  56. package/packages/web/dist/assets/{chunk-2J33WTMH-8hgadCQc.js → chunk-2J33WTMH-S4s7GCg7.js} +1 -1
  57. package/packages/web/dist/assets/{chunk-4BX2VUAB-CBfEC-pu.js → chunk-4BX2VUAB-BkWVPLf1.js} +1 -1
  58. package/packages/web/dist/assets/{chunk-55IACEB6-DV1eMgHk.js → chunk-55IACEB6-4-FQp01F.js} +1 -1
  59. package/packages/web/dist/assets/{chunk-727SXJPM-Cfgl5fxY.js → chunk-727SXJPM-BeyDWdIb.js} +1 -1
  60. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BXhqD295.js → chunk-AQP2D5EJ-ZSxKy745.js} +1 -1
  61. package/packages/web/dist/assets/{chunk-FMBD7UC4-TFGOPtek.js → chunk-FMBD7UC4-Bq8uV9j-.js} +1 -1
  62. package/packages/web/dist/assets/{chunk-ND2GUHAM-CHFM665R.js → chunk-ND2GUHAM-BOFsRyHk.js} +1 -1
  63. package/packages/web/dist/assets/{chunk-QZHKN3VN-CqEf6MMT.js → chunk-QZHKN3VN-uZ6oNc4m.js} +1 -1
  64. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DCCUHba0.js +2 -0
  65. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DCCUHba0.js +2 -0
  66. package/packages/web/dist/assets/{core-DmEaKxBb.js → core-CYuJAM2U.js} +1 -1
  67. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BnGI6jkr.js → cose-bilkent-S5V4N54A-DIzkVSFb.js} +1 -1
  68. package/packages/web/dist/assets/{dagre-BM42HDAG-CKoPx61E.js → dagre-BM42HDAG-CHR6jJBv.js} +1 -1
  69. package/packages/web/dist/assets/{diagram-2AECGRRQ-mMmtlBl1.js → diagram-2AECGRRQ-BAFXCH8Q.js} +1 -1
  70. package/packages/web/dist/assets/{diagram-5GNKFQAL-Cdz9Mtnt.js → diagram-5GNKFQAL-BJ5t5gbz.js} +1 -1
  71. package/packages/web/dist/assets/{diagram-KO2AKTUF-DQxM3wYj.js → diagram-KO2AKTUF-D9Xt0IVg.js} +1 -1
  72. package/packages/web/dist/assets/{diagram-LMA3HP47-BWrinPsq.js → diagram-LMA3HP47-Bq38u6sy.js} +1 -1
  73. package/packages/web/dist/assets/{diagram-OG6HWLK6-U6MYUve2.js → diagram-OG6HWLK6-CMQVqkpi.js} +1 -1
  74. package/packages/web/dist/assets/{documentTitle-DGjRXuTP.js → documentTitle-M1lrfXRU.js} +1 -1
  75. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-xqMCsXxY.js → erDiagram-TEJ5UH35-C63M1g9b.js} +1 -1
  76. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-PMfy_czF.js → flowDiagram-I6XJVG4X-D3j8ACN7.js} +1 -1
  77. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-whhYYE4N.js → ganttDiagram-6RSMTGT7-B1Fd1A2Y.js} +1 -1
  78. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-C51JW4tf.js → gitGraphDiagram-PVQCEYII-CEhZhDdR.js} +1 -1
  79. package/packages/web/dist/assets/{index-CHOtvlnO.js → index-1E9yHciS.js} +2 -2
  80. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-SH8cr905.js → infoDiagram-5YYISTIA-Ctquaew6.js} +1 -1
  81. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BjqaoU3U.js → ishikawaDiagram-YF4QCWOH-DV4GvA1R.js} +1 -1
  82. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CBmdEVrd.js → journeyDiagram-JHISSGLW-C_Cy9waA.js} +1 -1
  83. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-U8njjMLZ.js → kanban-definition-UN3LZRKU-CGr9ctEf.js} +1 -1
  84. package/packages/web/dist/assets/{linear-DF66GPs1.js → linear-D3U8NOkt.js} +1 -1
  85. package/packages/web/dist/assets/{mermaid.core-CouJz7Ph.js → mermaid.core-BiArdWKV.js} +4 -4
  86. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BYIf0oFt.js → mindmap-definition-RKZ34NQL-CZIpzj8Z.js} +1 -1
  87. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-Cm8rgg09.js → pieDiagram-4H26LBE5-1MaeS3rt.js} +1 -1
  88. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-C3Il5kOH.js → quadrantDiagram-W4KKPZXB-B5pgQ54e.js} +1 -1
  89. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-D3PE2nTe.js → requirementDiagram-4Y6WPE33-BNnxrpGr.js} +1 -1
  90. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-DdmvBM18.js → sankeyDiagram-5OEKKPKP-DsxVH-hM.js} +1 -1
  91. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-B8M_GAIq.js → sequenceDiagram-3UESZ5HK-Ke9LcFZI.js} +1 -1
  92. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DZkXnK1_.js → stateDiagram-AJRCARHV-UJzmflND.js} +1 -1
  93. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DgUc3HkP.js +2 -0
  94. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-IHXq7rqi.js → timeline-definition-PNZ67QCA-BRjNALVI.js} +1 -1
  95. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Dga7tLtT.js → vennDiagram-CIIHVFJN-ClMAspxz.js} +1 -1
  96. package/packages/web/dist/assets/{wardley-L42UT6IY-CmcwnEJ-.js → wardley-L42UT6IY-UPv4JEmz.js} +1 -1
  97. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-zHuSLrD4.js → wardleyDiagram-YWT4CUSO-BgYJbdEx.js} +1 -1
  98. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-D93MU_ic.js → xychartDiagram-2RQKCTM6-Bfk5HjGx.js} +1 -1
  99. package/packages/web/dist/index.html +1 -1
  100. package/packages/web/dist/sw.js +1 -1
  101. package/packages/server/dist/cli/service/backend.d.ts +0 -115
  102. package/packages/server/dist/cli/service/backend.js +0 -19
  103. package/packages/web/dist/assets/DiscoverPage-C4OPLNO8.js +0 -2
  104. package/packages/web/dist/assets/channel-JrEedxB8.js +0 -2
  105. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CHJiQp-W.js +0 -2
  106. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CHJiQp-W.js +0 -2
  107. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CjCdT9Ls.js +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edspencer/paddock",
3
- "version": "0.82.0",
3
+ "version": "0.82.1",
4
4
  "description": "Project-first launchpad for Claude Code: server-hosted, persistent, resumable sessions organized by project.",
5
5
  "keywords": [
6
6
  "claude",
@@ -34,7 +34,7 @@ import { EXIT } from "./exit-codes.js";
34
34
  */
35
35
  export class CliError extends Error {
36
36
  exitCode;
37
- constructor(message, exitCode = EXIT.failure) {
37
+ constructor(message, exitCode = EXIT.FAILED) {
38
38
  super(message);
39
39
  this.exitCode = exitCode;
40
40
  }
@@ -49,7 +49,7 @@ export class CliError extends Error {
49
49
  */
50
50
  export class UsageError extends CliError {
51
51
  constructor(message) {
52
- super(message, EXIT.usage);
52
+ super(message, EXIT.USAGE);
53
53
  }
54
54
  }
55
55
  export const MIN_NODE_MAJOR = 22;
@@ -104,5 +104,5 @@ export async function guardServiceBusy(action, home, opts) {
104
104
  return null;
105
105
  // Spell out the way past it, after the hint about where to run it from.
106
106
  out.warn(`\n paddock service ${action} --force ${action} it anyway\n`);
107
- return EXIT.busy;
107
+ return EXIT.BUSY;
108
108
  }
@@ -1,10 +1,8 @@
1
- // From edspencer/cli-contract reference/busy-client.ts @ SPEC 0.1; local changes: `.ts` import specifiers → `.js`
2
- // and `../runtime-file.js` (the writer lives server-side, next to the boot path that writes the file); `EXIT.OK` /
3
- // `EXIT.BUSY` / `EXIT.NOT_RUNNING` / `EXIT.FAILED` → Paddock's lowercase `EXIT.ok` / `busy` / `notRunning` /
4
- // `failure` (see `exit-codes.ts`, which wave C4 re-syncs); and `hint` widened from a string to
5
- // `string | ((result) => string | undefined)`, with {@link runBusyCommand} printing it too — that is what carries
6
- // Paddock's keeper-chat trap (ROADMAP "Busy, per tool"), where the line to print depends on WHICH counts are
7
- // non-zero, so a fixed string could not say it.
1
+ // From edspencer/cli-contract reference/busy-client.ts @ 0.2; local changes: `.ts` import specifiers → `.js` and
2
+ // `../runtime-file.js` (the writer lives server-side, next to the boot path that writes the file); and `hint` widened
3
+ // from a string to `string | ((result) => string | undefined)`, with {@link runBusyCommand} printing it too — that is
4
+ // what carries Paddock's keeper-chat trap (ROADMAP "Busy, per tool"), where the line to print depends on WHICH counts
5
+ // are non-zero, so a fixed string could not say it.
8
6
  //
9
7
  // The CLI's side of the busy check (SPEC §4): find the running instance through `<home>/runtime.json`, ask its
10
8
  // `/api/busy`, and map the answer to exit 0 (idle), 75 (busy) or 3 (nothing answers), with §4's messages.
@@ -88,13 +86,13 @@ export function busyMessage(name, result, nouns = {}) {
88
86
  export function busyExitCode(result) {
89
87
  switch (result.state) {
90
88
  case "idle":
91
- return EXIT.ok;
89
+ return EXIT.OK;
92
90
  case "busy":
93
- return EXIT.busy;
91
+ return EXIT.BUSY;
94
92
  case "not-running":
95
- return EXIT.notRunning;
93
+ return EXIT.NOT_RUNNING;
96
94
  case "unknown":
97
- return EXIT.failure;
95
+ return EXIT.FAILED;
98
96
  }
99
97
  }
100
98
  /** What `<tool> busy --json` prints: valid against `schemas/busy.schema.json`. */
@@ -146,5 +144,5 @@ export async function guardBusy(o) {
146
144
  return null;
147
145
  }
148
146
  const hint = resolveHint(o.hint, result) ?? "Try again once it has finished, or use --force.";
149
- return o.out.error(`${message} ${hint}`, EXIT.busy);
147
+ return o.out.error(`${message} ${hint}`, EXIT.BUSY);
150
148
  }
@@ -1,49 +1,25 @@
1
- /**
2
- * The CLI contract's exit codes (SPEC §2), as constants.
3
- *
4
- * From edspencer/cli-contract SPEC.md §2 @ 0.1. Written here rather than copied
5
- * from `reference/exit-codes.ts` because the reference module did not exist yet
6
- * when Paddock aligned; wave C4 re-syncs this file to it and adds the usual
7
- * provenance header. The numbers, and what each means, are the contract.
8
- *
9
- * | 0 | done; running and answering; up to date; not busy
10
- * | 1 | failed
11
- * | 2 | usage: an unknown command or flag, a bad value
12
- * | 3 | not running: installed but not running or not answering
13
- * | 4 | not installed: there is no service unit
14
- * | 5 | this install cannot update itself (`update`, not yet built)
15
- * | 6 | the update failed and was rolled back (`update`, not yet built)
16
- * | 7 | the update failed and could not be rolled back
17
- * | 75 | busy: refused, and why was printed (`busy`, not yet built)
18
- *
19
- * Codes 3 and 4 are what `systemctl status` and LSB init scripts use for "not
20
- * running" and "no such service", so a script that already branches on them
21
- * keeps working. 75 is `EX_TEMPFAIL` from `sysexits.h`, which is also the code
22
- * that makes Watchtower's pre-update hook skip an update.
23
- *
24
- * The three `update` codes are declared now and unused until wave C2. They are
25
- * here because the contract's whole point is that the numbers mean the same
26
- * thing in every tool: adding them later, from a different file, is how two
27
- * tools end up disagreeing about what 5 means.
28
- */
29
1
  export declare const EXIT: {
30
- /** Done. Running and answering. Up to date. Not busy. */
31
- readonly ok: 0;
32
- /** Failed — the catch-all. */
33
- readonly failure: 1;
34
- /** Usage: an unknown command or flag, or a bad value. Usage follows the message. */
35
- readonly usage: 2;
36
- /** Not running: installed, but not running or not answering. */
37
- readonly notRunning: 3;
38
- /** Not installed: there is no service unit. */
39
- readonly notInstalled: 4;
40
- /** This install cannot update itself; the right command was printed. */
41
- readonly cannotSelfUpdate: 5;
2
+ /** Done; running and answering; up to date; not busy. */
3
+ readonly OK: 0;
4
+ /** Failed. */
5
+ readonly FAILED: 1;
6
+ /** Usage: an unknown command or flag, a bad value. The usage is printed after the message. */
7
+ readonly USAGE: 2;
8
+ /** Not running: installed but not running or not answering, or no instance answers. */
9
+ readonly NOT_RUNNING: 3;
10
+ /** Not installed: there's no service unit. */
11
+ readonly NOT_INSTALLED: 4;
12
+ /** This install can't update itself; the right command was printed. */
13
+ readonly CANNOT_UPDATE: 5;
42
14
  /** The update failed and was rolled back; the old version is running again. */
43
- readonly rolledBack: 6;
44
- /** The update failed and could not be rolled back; a person is needed. */
45
- readonly needsHuman: 7;
46
- /** Busy: refused, and why was printed. Try later, or use `--force`. */
47
- readonly busy: 75;
15
+ readonly ROLLED_BACK: 6;
16
+ /** The update failed and couldn't be rolled back; a person is needed. */
17
+ readonly ROLLBACK_FAILED: 7;
18
+ /** Busy: refused, and why was printed. Try later, or use `--force`. `EX_TEMPFAIL` from sysexits.h. */
19
+ readonly BUSY: 75;
48
20
  };
49
21
  export type ExitCode = (typeof EXIT)[keyof typeof EXIT];
22
+ /** A tool may use 8 to 63 for its own commands (SPEC §2). */
23
+ export declare function isToolOwnCode(code: number): boolean;
24
+ /** The SPEC's name for a shared code, for messages ("expected 4 (not installed), got 0 (ok)"). */
25
+ export declare function describeExitCode(code: number | null): string;
@@ -1,48 +1,45 @@
1
- /**
2
- * The CLI contract's exit codes (SPEC §2), as constants.
3
- *
4
- * From edspencer/cli-contract SPEC.md §2 @ 0.1. Written here rather than copied
5
- * from `reference/exit-codes.ts` because the reference module did not exist yet
6
- * when Paddock aligned; wave C4 re-syncs this file to it and adds the usual
7
- * provenance header. The numbers, and what each means, are the contract.
8
- *
9
- * | 0 | done; running and answering; up to date; not busy
10
- * | 1 | failed
11
- * | 2 | usage: an unknown command or flag, a bad value
12
- * | 3 | not running: installed but not running or not answering
13
- * | 4 | not installed: there is no service unit
14
- * | 5 | this install cannot update itself (`update`, not yet built)
15
- * | 6 | the update failed and was rolled back (`update`, not yet built)
16
- * | 7 | the update failed and could not be rolled back
17
- * | 75 | busy: refused, and why was printed (`busy`, not yet built)
18
- *
19
- * Codes 3 and 4 are what `systemctl status` and LSB init scripts use for "not
20
- * running" and "no such service", so a script that already branches on them
21
- * keeps working. 75 is `EX_TEMPFAIL` from `sysexits.h`, which is also the code
22
- * that makes Watchtower's pre-update hook skip an update.
23
- *
24
- * The three `update` codes are declared now and unused until wave C2. They are
25
- * here because the contract's whole point is that the numbers mean the same
26
- * thing in every tool: adding them later, from a different file, is how two
27
- * tools end up disagreeing about what 5 means.
28
- */
1
+ // From edspencer/cli-contract reference/exit-codes.ts @ 0.2; local changes: none.
2
+ // The exit codes every tool shares (SPEC §2). Copy this file; don't renumber it.
3
+ //
4
+ // Set `process.exitCode`, never call `process.exit()`, so piped output isn't cut short.
29
5
  export const EXIT = {
30
- /** Done. Running and answering. Up to date. Not busy. */
31
- ok: 0,
32
- /** Failed — the catch-all. */
33
- failure: 1,
34
- /** Usage: an unknown command or flag, or a bad value. Usage follows the message. */
35
- usage: 2,
36
- /** Not running: installed, but not running or not answering. */
37
- notRunning: 3,
38
- /** Not installed: there is no service unit. */
39
- notInstalled: 4,
40
- /** This install cannot update itself; the right command was printed. */
41
- cannotSelfUpdate: 5,
6
+ /** Done; running and answering; up to date; not busy. */
7
+ OK: 0,
8
+ /** Failed. */
9
+ FAILED: 1,
10
+ /** Usage: an unknown command or flag, a bad value. The usage is printed after the message. */
11
+ USAGE: 2,
12
+ /** Not running: installed but not running or not answering, or no instance answers. */
13
+ NOT_RUNNING: 3,
14
+ /** Not installed: there's no service unit. */
15
+ NOT_INSTALLED: 4,
16
+ /** This install can't update itself; the right command was printed. */
17
+ CANNOT_UPDATE: 5,
42
18
  /** The update failed and was rolled back; the old version is running again. */
43
- rolledBack: 6,
44
- /** The update failed and could not be rolled back; a person is needed. */
45
- needsHuman: 7,
46
- /** Busy: refused, and why was printed. Try later, or use `--force`. */
47
- busy: 75,
19
+ ROLLED_BACK: 6,
20
+ /** The update failed and couldn't be rolled back; a person is needed. */
21
+ ROLLBACK_FAILED: 7,
22
+ /** Busy: refused, and why was printed. Try later, or use `--force`. `EX_TEMPFAIL` from sysexits.h. */
23
+ BUSY: 75,
48
24
  };
25
+ /** A tool may use 8 to 63 for its own commands (SPEC §2). */
26
+ export function isToolOwnCode(code) {
27
+ return Number.isInteger(code) && code >= 8 && code <= 63;
28
+ }
29
+ /** The SPEC's name for a shared code, for messages ("expected 4 (not installed), got 0 (ok)"). */
30
+ export function describeExitCode(code) {
31
+ if (code === null)
32
+ return 'no exit code (killed by a signal)';
33
+ const names = {
34
+ 0: 'ok',
35
+ 1: 'failed',
36
+ 2: 'usage',
37
+ 3: 'not running',
38
+ 4: 'not installed',
39
+ 5: "can't update itself",
40
+ 6: 'rolled back',
41
+ 7: "couldn't roll back",
42
+ 75: 'busy',
43
+ };
44
+ return names[code] ? `${code} (${names[code]})` : String(code);
45
+ }
@@ -1,8 +1,7 @@
1
- // From edspencer/cli-contract reference/output.ts @ SPEC 0.1; local changes: `./exit-codes.ts` → `./exit-codes.js`
2
- // (Paddock compiles to ESM-with-extensions, not a TS runtime); `EXIT.FAILED`/`EXIT.USAGE` → Paddock's lowercase
3
- // `EXIT.failure`/`EXIT.usage`, because Paddock's own `exit-codes.ts` predates the reference module and wave C4
4
- // re-syncs that file; and the reference's `UsageError` / `reportUsageError` are NOT copied, because `cli/args.ts`
5
- // already has a `UsageError` (a `CliError` subclass carrying its own code) that the entrypoint already reports.
1
+ // From edspencer/cli-contract reference/output.ts @ 0.2; local changes: `./exit-codes.ts` → `./exit-codes.js`
2
+ // (Paddock compiles to ESM-with-extensions, not a TS runtime); and the reference's `UsageError` / `reportUsageError`
3
+ // are NOT copied, because `cli/args.ts` already has a `UsageError` (a `CliError` subclass carrying its own code) that
4
+ // the entrypoint already reports.
6
5
  //
7
6
  // A CLI's output, the way SPEC §2 and §3 say: errors as `<tool>: <message>` on stderr, colour only on a TTY (honouring
8
7
  // NO_COLOR and FORCE_COLOR), and with `--json`, one JSON document on stdout and every word of prose on stderr.
@@ -56,7 +55,7 @@ export function createOutput(o) {
56
55
  warn(s) {
57
56
  stderr.write(line(s));
58
57
  },
59
- error(message, code = EXIT.failure) {
58
+ error(message, code = EXIT.FAILED) {
60
59
  stderr.write(line(`${o.tool}: ${message}`));
61
60
  return code;
62
61
  },
@@ -78,9 +78,9 @@ const packageRoot = findUp("package.json") ?? path.resolve(moduleDir, "../..");
78
78
  * says the usage follows the message — the short one, not {@link USAGE}'s
79
79
  * thousand words, which would bury the line that says what was wrong.
80
80
  */
81
- function fail(message, code = EXIT.failure) {
81
+ function fail(message, code = EXIT.FAILED) {
82
82
  console.error(`paddock: ${message}`);
83
- if (code === EXIT.usage) {
83
+ if (code === EXIT.USAGE) {
84
84
  console.error("");
85
85
  console.error(USAGE_SYNOPSIS);
86
86
  }
@@ -175,7 +175,7 @@ async function service(command) {
175
175
  // which `main` has already handled. Typed rather than asserted.
176
176
  if (action === undefined) {
177
177
  console.log(SERVICE_USAGE);
178
- return EXIT.ok;
178
+ return EXIT.OK;
179
179
  }
180
180
  try {
181
181
  return await runService(action, opts, serviceContext());
@@ -246,13 +246,13 @@ async function config(command) {
246
246
  // which `main` has already handled. Typed rather than asserted.
247
247
  if (action === undefined) {
248
248
  console.log(CONFIG_USAGE);
249
- return EXIT.ok;
249
+ return EXIT.OK;
250
250
  }
251
251
  process.env.PADDOCK_DATA_DIR = resolveDataDir(opts);
252
252
  const { runConfig } = await import("./config.js");
253
253
  try {
254
254
  await runConfig(action, opts);
255
- return EXIT.ok;
255
+ return EXIT.OK;
256
256
  }
257
257
  catch (err) {
258
258
  // A config that will not load reaches here as a CliError already carrying
@@ -290,12 +290,12 @@ async function main() {
290
290
  : command.verb === "update"
291
291
  ? UPDATE_USAGE
292
292
  : USAGE);
293
- return EXIT.ok;
293
+ return EXIT.OK;
294
294
  }
295
295
  if (opts.version) {
296
296
  // Bare: the version and a newline, nothing else (SPEC §1). Scripts parse this.
297
297
  console.log(PADDOCK_VERSION);
298
- return EXIT.ok;
298
+ return EXIT.OK;
299
299
  }
300
300
  const problem = nodeVersionProblem(process.versions.node);
301
301
  if (problem !== undefined)
@@ -420,7 +420,7 @@ async function main() {
420
420
  // 0, but NOT yet: a foreground serve keeps running because the server holds
421
421
  // the event loop open. `process.exitCode` is set and never acted on until the
422
422
  // listener closes, which is what SIGTERM does (start.ts).
423
- return EXIT.ok;
423
+ return EXIT.OK;
424
424
  }
425
425
  /**
426
426
  * A closed pipe exits quietly (SPEC §2).
@@ -440,5 +440,5 @@ main().then((code) => {
440
440
  process.exitCode = code;
441
441
  }, (err) => {
442
442
  console.error("fatal:", err);
443
- process.exitCode = EXIT.failure;
443
+ process.exitCode = EXIT.FAILED;
444
444
  });
@@ -1,6 +1,6 @@
1
1
  import { type CliOptions, type ServiceAction } from "../args.js";
2
- import { isNpxPath, serviceDir, type ServiceSpec } from "./spec.js";
3
- import { type Runner } from "./backend.js";
2
+ import { isNpxPath, serviceDir } from "./spec.js";
3
+ import { type Runner, type ServiceSpec } from "./manager.js";
4
4
  import { type UpdateSummary } from "../update/update-check.js";
5
5
  import { type HealthAnswer, type Readiness } from "./ready.js";
6
6
  export interface ServiceContext {
@@ -27,13 +27,14 @@ import os from "node:os";
27
27
  import path from "node:path";
28
28
  import { CliError } from "../args.js";
29
29
  import { EXIT } from "../exit-codes.js";
30
- import { buildSpec, defaultDataDir, hostFromArgv, isNpxPath, NPX_REFUSAL, portFromArgv, serviceDir, serviceLogPath, } from "./spec.js";
31
- import { spawnRunner, } from "./backend.js";
32
- import { createLaunchdBackend } from "./launchd.js";
33
- import { createSystemdBackend } from "./systemd.js";
30
+ import { buildSpec, defaultDataDir, hostFromArgv, isNpxPath, NPX_REFUSAL, portFromArgv, serviceDir, serviceLogPath, PADDOCK_SERVICE, } from "./spec.js";
31
+ import { spawnRunner, } from "./manager.js";
32
+ import { createLaunchd } from "./launchd.js";
33
+ import { createSystemd } from "./systemd.js";
34
34
  import { missingPaths } from "./path-env.js";
35
35
  import { DEFAULT_LOG_LINES, followFile, sizeOf, tailFile } from "./logs.js";
36
36
  import { readLiveRuntimeFile } from "../../runtime-file.js";
37
+ import { PADDOCK_VERSION } from "../../version.js";
37
38
  import { updateSummary } from "../update/update-check.js";
38
39
  import { waitForReady, fetchHealth, READY_TIMEOUT_LABEL, } from "./ready.js";
39
40
  /**
@@ -46,9 +47,14 @@ import { waitForReady, fetchHealth, READY_TIMEOUT_LABEL, } from "./ready.js";
46
47
  function backendFor(ctx) {
47
48
  const run = ctx.run ?? spawnRunner;
48
49
  if (ctx.platform === "darwin")
49
- return createLaunchdBackend(run, ctx.homeDir);
50
- if (ctx.platform === "linux")
51
- return createSystemdBackend(run, ctx.homeDir, ctx.xdgConfigHome);
50
+ return createLaunchd(PADDOCK_SERVICE, { run, homeDir: ctx.homeDir });
51
+ if (ctx.platform === "linux") {
52
+ return createSystemd(PADDOCK_SERVICE, {
53
+ run,
54
+ homeDir: ctx.homeDir,
55
+ ...(ctx.xdgConfigHome !== undefined ? { xdgConfigHome: ctx.xdgConfigHome } : {}),
56
+ });
57
+ }
52
58
  throw new CliError(`\`paddock service\` supports macOS (launchd) and Linux (systemd --user), not ${ctx.platform}.\n` +
53
59
  "Run `paddock` in a terminal, or keep it up with whatever your platform uses.");
54
60
  }
@@ -195,7 +201,7 @@ async function install(opts, ctx, backend) {
195
201
  existing = backend.status();
196
202
  if (!existing.registered) {
197
203
  console.log(notInstalled(backend));
198
- return EXIT.notInstalled;
204
+ return EXIT.NOT_INSTALLED;
199
205
  }
200
206
  }
201
207
  const { spec, dataDir } = specFor(existing !== undefined ? inherit(opts, existing) : opts, ctx, refreshing);
@@ -241,7 +247,7 @@ async function install(opts, ctx, backend) {
241
247
  ].join("\n"));
242
248
  // "It is installed but did not answer" is a failure, and used to exit 0 — so
243
249
  // an Ansible task or a CI step could not tell it from a working install.
244
- return readiness === "ready" ? EXIT.ok : EXIT.failure;
250
+ return readiness === "ready" ? EXIT.OK : EXIT.FAILED;
245
251
  }
246
252
  function uninstall(ctx, backend) {
247
253
  const existed = backend.uninstall();
@@ -255,7 +261,7 @@ function uninstall(ctx, backend) {
255
261
  // 0 either way: "there was nothing to remove" is the state the caller asked
256
262
  // for. `uninstall` is idempotent on purpose, so a teardown script can run it
257
263
  // without first asking whether it has anything to do.
258
- return EXIT.ok;
264
+ return EXIT.OK;
259
265
  }
260
266
  /**
261
267
  * The data dir the INSTALLED unit uses, read out of its own argument vector.
@@ -318,7 +324,7 @@ export function installedVersionOf(cliPath) {
318
324
  }
319
325
  /** `{ update }` when there is a recent check to repeat, and `{}` otherwise. */
320
326
  function withUpdate(home) {
321
- const summary = updateSummary(home);
327
+ const summary = updateSummary(home, { currentVersion: PADDOCK_VERSION });
322
328
  return summary !== undefined ? { update: summary } : {};
323
329
  }
324
330
  async function buildStatus(ctx, backend) {
@@ -432,7 +438,7 @@ async function status(opts, ctx, backend) {
432
438
  // that one StatusReport is supposed to prevent. A newer version is worth
433
439
  // knowing about whether or not a unit exists.
434
440
  noteUpdate(report);
435
- return EXIT.notInstalled;
441
+ return EXIT.NOT_INSTALLED;
436
442
  }
437
443
  const versions = report.runningVersion !== null && report.runningVersion !== report.installedVersion
438
444
  ? ` Version: ${report.runningVersion} running, ${report.installedVersion ?? "unknown"} installed`
@@ -495,10 +501,10 @@ function noteUpdate(report) {
495
501
  /** The exit code a report means (SPEC §2). */
496
502
  function statusExit(report) {
497
503
  if (!report.installed)
498
- return EXIT.notInstalled;
504
+ return EXIT.NOT_INSTALLED;
499
505
  if (!report.running || !report.answering)
500
- return EXIT.notRunning;
501
- return EXIT.ok;
506
+ return EXIT.NOT_RUNNING;
507
+ return EXIT.OK;
502
508
  }
503
509
  /**
504
510
  * The URL an installed unit is actually serving, or undefined if none is.
@@ -539,12 +545,12 @@ async function startOrRestart(action, opts, ctx, backend) {
539
545
  const before = backend.status();
540
546
  if (!before.registered) {
541
547
  console.log(notInstalled(backend));
542
- return EXIT.notInstalled;
548
+ return EXIT.NOT_INSTALLED;
543
549
  }
544
550
  const url = installedUrl(before);
545
551
  if (action === "start" && before.running) {
546
552
  console.log(`\n Paddock is already running${pid(before)}.\n\n URL: ${url}\n`);
547
- return EXIT.ok;
553
+ return EXIT.OK;
548
554
  }
549
555
  // A restart stops the instance first, so it asks the same question `stop`
550
556
  // asks (SPEC §1): refuse with 75 unless `--force`. `start` does not — there
@@ -578,7 +584,7 @@ async function startOrRestart(action, opts, ctx, backend) {
578
584
  ].join("\n"));
579
585
  // A start that never answered is a failure, not a success with a caveat: the
580
586
  // caller — often an Ansible task — has no other way to find out.
581
- return readiness === "ready" ? EXIT.ok : EXIT.failure;
587
+ return readiness === "ready" ? EXIT.OK : EXIT.FAILED;
582
588
  }
583
589
  /**
584
590
  * The busy check, asked of the instance the UNIT describes (SPEC §1, §4).
@@ -603,11 +609,11 @@ async function stop(opts, ctx, backend) {
603
609
  const state = backend.status();
604
610
  if (!state.registered) {
605
611
  console.log(notInstalled(backend));
606
- return EXIT.notInstalled;
612
+ return EXIT.NOT_INSTALLED;
607
613
  }
608
614
  if (!state.running) {
609
615
  console.log(`\n Paddock is already stopped.\n Unit: ${backend.unitPath}\n`);
610
- return EXIT.ok;
616
+ return EXIT.OK;
611
617
  }
612
618
  // Refuse with 75 if it is in the middle of something, unless --force (SPEC
613
619
  // §4). Deliberately AFTER the two cases above: an unit that is not there, or
@@ -626,7 +632,7 @@ async function stop(opts, ctx, backend) {
626
632
  " paddock service start",
627
633
  "",
628
634
  ].join("\n"));
629
- return EXIT.ok;
635
+ return EXIT.OK;
630
636
  }
631
637
  function pid(state) {
632
638
  return state.pid !== undefined ? ` (pid ${state.pid})` : "";
@@ -648,7 +654,7 @@ async function logs(opts, ctx, backend) {
648
654
  const state = backend.status();
649
655
  if (!state.registered) {
650
656
  console.log(notInstalled(backend));
651
- return EXIT.notInstalled;
657
+ return EXIT.NOT_INSTALLED;
652
658
  }
653
659
  const file = logPathOf(state, dataDirOf(state, ctx));
654
660
  const lines = opts.lines ?? DEFAULT_LOG_LINES;
@@ -657,7 +663,7 @@ async function logs(opts, ctx, backend) {
657
663
  // would be mixed into the log output itself.
658
664
  console.error(`paddock: nothing has been written to ${file} yet.`);
659
665
  if (opts.follow !== true)
660
- return EXIT.ok;
666
+ return EXIT.OK;
661
667
  }
662
668
  else {
663
669
  const tail = tailFile(file, lines);
@@ -668,7 +674,7 @@ async function logs(opts, ctx, backend) {
668
674
  console.error(`paddock: following ${file} — Ctrl-C to stop.`);
669
675
  await followFile(file, { from: sizeOf(file) });
670
676
  }
671
- return EXIT.ok;
677
+ return EXIT.OK;
672
678
  }
673
679
  /**
674
680
  * Dispatch, returning the exit code rather than setting one (SPEC §2).
@@ -1,92 +1,39 @@
1
- import { type ServiceSpec } from "./spec.js";
2
- import type { Runner, ServiceBackend } from "./backend.js";
3
- /** `~/Library/LaunchAgents/net.edspencer.paddock.plist`. */
4
- export declare function plistPath(homeDir?: string): string;
1
+ import { type Runner, type ServiceIdentity, type ServiceManager, type ServiceSpec } from './manager.js';
2
+ /** `~/Library/LaunchAgents/<label>.plist`. */
3
+ export declare function plistPath(id: Pick<ServiceIdentity, 'label'>, homeDir?: string): string;
5
4
  /**
6
- * Render the plist.
7
- *
8
- * `KeepAlive` is unconditional `<true/>`, and that is a deliberate reversal
9
- * (#872). It used to be `{ SuccessfulExit: false }` — "restart it if it exits
10
- * nonzero, leave it alone if it exits cleanly" — which reads like the polite
11
- * choice and is, for a service, a trap. `start.ts` handles SIGTERM by shutting
12
- * down cleanly and exiting `0`, so **every** SIGTERM the OS routinely sends a
13
- * LaunchAgent (sleep, logout, a stray `kill`) looked to launchd like "it meant
14
- * to stop", and Paddock stayed down until the next login. A hard crash was the
15
- * survivable case; the graceful stop was the terminal one.
16
- *
17
- * The worry that motivated the dict — that `<true/>` fights a deliberate stop —
18
- * does not hold for the stop this CLI actually performs. `launchctl bootout`
19
- * unloads the job from the domain, and KeepAlive only applies to a job that is
20
- * still loaded, so `uninstall` still stops Paddock in one call. What `<true/>`
21
- * does fight is `launchctl stop`, which is why nothing here uses it.
22
- *
23
- * Intent belongs in an explicit command, not in an exit code.
24
- *
25
- * `EnvironmentVariables` carries PATH and nothing else. It carries PATH because
26
- * launchd hands a process a stub PATH that a version manager's `node`, and the
27
- * `git` an agent will want, are both missing from. It carries nothing else
28
- * because a plist is a world-readable file in the user's home, and the one
29
- * category of value that would be tempting to put here — a credential — is
30
- * precisely what the agent shape exists to avoid needing.
31
- *
32
- * No `PADDOCK_DATA_DIR`: see `buildSpec`. One `~/.paddock` instance, reachable
33
- * from the service and from a terminal.
34
- *
35
- * `StandardOutPath` and `StandardErrorPath` name the SAME file (SPEC §7).
36
- * launchd opens both in append mode, so interleaving is what a reader wants
37
- * anyway — a warning and the stack trace that followed it used to land in two
38
- * separate files, in an order nothing recorded. One file is also what makes
39
- * `paddock service logs` one implementation across platforms.
40
- */
41
- export declare function renderPlist(spec: ServiceSpec): string;
42
- /**
43
- * Pull `ProgramArguments` back out of a plist we wrote.
44
- *
45
- * `status` reports the port the service is ACTUALLY installed with, which is
46
- * only knowable from the installed unit — recomputing it from today's flags
47
- * would print a confident URL for a server listening somewhere else. Regex
48
- * rather than a plist parser because the input is a file this module wrote, in
49
- * a shape this module controls, and adding an XML dependency to read it back
50
- * would be the larger risk.
5
+ * The plist.
6
+ *
7
+ * - `KeepAlive` is an unconditional `<true/>`. `{ SuccessfulExit: false }` reads as the polite choice, but a server
8
+ * exits 0 on SIGTERM, so every SIGTERM launchd sends an agent (sleep, logout) would leave it down until the next
9
+ * login. `bootout` unloads the job, so `KeepAlive` doesn't fight a deliberate stop.
10
+ * - `StandardOutPath` and `StandardErrorPath` name the same file.
11
+ * - `EnvironmentVariables` has PATH (launchd's is a stub) and the spec's `env`. Never a credential: a plist is a
12
+ * readable file.
51
13
  */
14
+ export declare function renderPlist(id: Pick<ServiceIdentity, 'label'>, spec: ServiceSpec): string;
15
+ /** `ProgramArguments`, read back from a plist this wrote. A regex is enough for a file whose shape we control. */
52
16
  export declare function parsePlistArgv(xml: string): string[];
53
- /**
54
- * Does an installed plist carry the unconditional `KeepAlive` from #872?
55
- *
56
- * Deliberately narrow: it matches `<key>KeepAlive</key>` followed by `<true/>`
57
- * and nothing else. The shape it must NOT accept is the old
58
- * `<dict><key>SuccessfulExit</key><false/></dict>`, which is the one that
59
- * leaves a service dead after a graceful stop — so a `false` here is the signal
60
- * `status` turns into "re-run install", and defaulting to `true` on anything
61
- * unrecognised would silence exactly the case this exists to catch.
62
- */
63
- export declare function plistKeepsAlive(xml: string): boolean;
64
- /** The log file an installed plist appends to, read back from `StandardOutPath`. */
17
+ /** The log the plist names (`StandardOutPath`). */
65
18
  export declare function parsePlistLogPath(xml: string): string | undefined;
66
- /**
67
- * Read liveness out of `launchctl print gui/<uid>/<label>`.
68
- *
69
- * That command is the honest answer to "is it running": it reports the service
70
- * as launchd currently understands it, including `state = running` and the pid.
71
- * The alternative — checking whether the plist file exists — answers "is it
72
- * registered", which is a different and much weaker question.
73
- */
19
+ /** Whether the plist has an unconditional `KeepAlive` (the old `SuccessfulExit` dict doesn't count). */
20
+ export declare function plistKeepsAlive(xml: string): boolean;
21
+ /** `state = running` and the pid, from `launchctl print gui/<uid>/<label>`. */
74
22
  export declare function parseLaunchctlPrint(out: string): {
75
23
  running: boolean;
76
24
  pid?: string;
77
25
  };
78
- /**
79
- * Block this thread for `ms`.
80
- *
81
- * The backend's {@link Runner} is synchronous (`execFileSync`) and so is every
82
- * `ServiceBackend` method, so the wait has to be too — making `install` async
83
- * would ripple through the whole service CLI for a sleep. `Atomics.wait` on a
84
- * throwaway buffer is the built-in way to do it; `setTimeout` cannot help here
85
- * and spinning on `Date.now()` would burn a core.
86
- *
87
- * Injectable because the unit tests drive the retry path with a fake runner
88
- * that fails every attempt: with the real sleep, pinning the retry count would
89
- * cost three seconds per case.
90
- */
91
- export declare function sleepSync(ms: number): void;
92
- export declare function createLaunchdBackend(run: Runner, homeDir?: string, sleep?: (ms: number) => void): ServiceBackend;
26
+ export declare const BOOTOUT_POLLS = 40;
27
+ export declare const BOOTOUT_POLL_MS = 250;
28
+ export declare const BOOTSTRAP_ATTEMPTS = 6;
29
+ export declare const BOOTSTRAP_BACKOFF_MS = 500;
30
+ /** 5 is `EIO`; 37 is "operation already in progress". Both mean the last bootout hasn't finished. */
31
+ export declare const BOOTSTRAP_RACE_CODES: ReadonlySet<number>;
32
+ export interface LaunchdOptions {
33
+ run: Runner;
34
+ homeDir?: string;
35
+ uid?: number;
36
+ /** Injected for tests. */
37
+ sleep?: (ms: number) => void;
38
+ }
39
+ export declare function createLaunchd(id: ServiceIdentity, o: LaunchdOptions): ServiceManager;