@edspencer/paddock 0.81.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 (91) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/cli/args.d.ts +56 -31
  3. package/packages/server/dist/cli/args.js +208 -22
  4. package/packages/server/dist/cli/paddock.js +51 -21
  5. package/packages/server/dist/cli/service/index.d.ts +52 -0
  6. package/packages/server/dist/cli/service/index.js +170 -3
  7. package/packages/server/dist/cli/service/ready.d.ts +30 -0
  8. package/packages/server/dist/cli/service/ready.js +35 -0
  9. package/packages/server/dist/cli/service/systemd.js +27 -0
  10. package/packages/server/dist/cli/update/install-method.d.ts +131 -0
  11. package/packages/server/dist/cli/update/install-method.js +258 -0
  12. package/packages/server/dist/cli/update/lock.d.ts +40 -0
  13. package/packages/server/dist/cli/update/lock.js +108 -0
  14. package/packages/server/dist/cli/update/package-manager.d.ts +109 -0
  15. package/packages/server/dist/cli/update/package-manager.js +221 -0
  16. package/packages/server/dist/cli/update/registry.d.ts +89 -0
  17. package/packages/server/dist/cli/update/registry.js +145 -0
  18. package/packages/server/dist/cli/update/semver.d.ts +68 -0
  19. package/packages/server/dist/cli/update/semver.js +232 -0
  20. package/packages/server/dist/cli/update/update-check.d.ts +80 -0
  21. package/packages/server/dist/cli/update/update-check.js +116 -0
  22. package/packages/server/dist/cli/update/update.d.ts +160 -0
  23. package/packages/server/dist/cli/update/update.js +609 -0
  24. package/packages/server/dist/cli/update-command.d.ts +20 -0
  25. package/packages/server/dist/cli/update-command.js +113 -0
  26. package/packages/server/package.json +1 -1
  27. package/packages/web/dist/assets/{DeleteProjectDialog-D8oIJ8HC.js → DeleteProjectDialog-CphdGB3R.js} +1 -1
  28. package/packages/web/dist/assets/DiscoverPage-C4OPLNO8.js +2 -0
  29. package/packages/web/dist/assets/{DiscoverView-o0KCr3aB.js → DiscoverView-B_kcF4sm.js} +1 -1
  30. package/packages/web/dist/assets/{InstanceConfigPage-RSJz7oL3.js → InstanceConfigPage-CvUK5DKW.js} +1 -1
  31. package/packages/web/dist/assets/{MarkdownRenderer-39mafH8b.js → MarkdownRenderer-DvZ-j_Ao.js} +1 -1
  32. package/packages/web/dist/assets/{ProjectRedirect-Bq7GVguY.js → ProjectRedirect-DayHvUH2.js} +1 -1
  33. package/packages/web/dist/assets/{ProjectView-BoBIru1g.js → ProjectView-CCw83gUi.js} +3 -3
  34. package/packages/web/dist/assets/{ProjectsGrid-mJ4gqbr6.js → ProjectsGrid-B3Klg2H4.js} +1 -1
  35. package/packages/web/dist/assets/{RootHome-C0kwFyYp.js → RootHome-BxcDvpgI.js} +1 -1
  36. package/packages/web/dist/assets/{Surfaces-MkhXlB8z.js → Surfaces-7UlhFBSG.js} +1 -1
  37. package/packages/web/dist/assets/{arc-DExlwAwO.js → arc-DNkh5o5Q.js} +1 -1
  38. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CZly3W79.js → architectureDiagram-3BPJPVTR-BrgN3oLF.js} +1 -1
  39. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-AgES73i3.js → blockDiagram-GPEHLZMM-D05XOJSk.js} +1 -1
  40. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-N9mD8Pfk.js → c4Diagram-AAUBKEIU-CKaMBhMP.js} +1 -1
  41. package/packages/web/dist/assets/channel-JrEedxB8.js +2 -0
  42. package/packages/web/dist/assets/{chunk-2J33WTMH-Dsx_kfUl.js → chunk-2J33WTMH-8hgadCQc.js} +1 -1
  43. package/packages/web/dist/assets/{chunk-4BX2VUAB-BQjqcjPj.js → chunk-4BX2VUAB-CBfEC-pu.js} +1 -1
  44. package/packages/web/dist/assets/{chunk-55IACEB6-CCxpYQpY.js → chunk-55IACEB6-DV1eMgHk.js} +1 -1
  45. package/packages/web/dist/assets/{chunk-727SXJPM-DpFRdnUa.js → chunk-727SXJPM-Cfgl5fxY.js} +1 -1
  46. package/packages/web/dist/assets/{chunk-AQP2D5EJ-D2BAlzoO.js → chunk-AQP2D5EJ-BXhqD295.js} +1 -1
  47. package/packages/web/dist/assets/{chunk-FMBD7UC4-BFaMRzUF.js → chunk-FMBD7UC4-TFGOPtek.js} +1 -1
  48. package/packages/web/dist/assets/{chunk-ND2GUHAM-B9NGJ9xZ.js → chunk-ND2GUHAM-CHFM665R.js} +1 -1
  49. package/packages/web/dist/assets/{chunk-QZHKN3VN-CF1IHyMS.js → chunk-QZHKN3VN-CqEf6MMT.js} +1 -1
  50. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CHJiQp-W.js +2 -0
  51. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CHJiQp-W.js +2 -0
  52. package/packages/web/dist/assets/{core-XjElsvQ7.js → core-DmEaKxBb.js} +1 -1
  53. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CF972Flx.js → cose-bilkent-S5V4N54A-BnGI6jkr.js} +1 -1
  54. package/packages/web/dist/assets/{dagre-BM42HDAG-DsneI8YE.js → dagre-BM42HDAG-CKoPx61E.js} +1 -1
  55. package/packages/web/dist/assets/{diagram-2AECGRRQ-BHyrVzIk.js → diagram-2AECGRRQ-mMmtlBl1.js} +1 -1
  56. package/packages/web/dist/assets/{diagram-5GNKFQAL-Dj4q8xEi.js → diagram-5GNKFQAL-Cdz9Mtnt.js} +1 -1
  57. package/packages/web/dist/assets/{diagram-KO2AKTUF-BHe3Puv_.js → diagram-KO2AKTUF-DQxM3wYj.js} +1 -1
  58. package/packages/web/dist/assets/{diagram-LMA3HP47-wzt6G2FE.js → diagram-LMA3HP47-BWrinPsq.js} +1 -1
  59. package/packages/web/dist/assets/{diagram-OG6HWLK6-CZMbMRJq.js → diagram-OG6HWLK6-U6MYUve2.js} +1 -1
  60. package/packages/web/dist/assets/{documentTitle-BwTIA8rI.js → documentTitle-DGjRXuTP.js} +1 -1
  61. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-Dtu14BL1.js → erDiagram-TEJ5UH35-xqMCsXxY.js} +1 -1
  62. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-T34ql9j0.js → flowDiagram-I6XJVG4X-PMfy_czF.js} +1 -1
  63. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-DQLOeCXK.js → ganttDiagram-6RSMTGT7-whhYYE4N.js} +1 -1
  64. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-Bk11zea6.js → gitGraphDiagram-PVQCEYII-C51JW4tf.js} +1 -1
  65. package/packages/web/dist/assets/{index-CvaWErFv.js → index-CHOtvlnO.js} +2 -2
  66. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-CsE83lu0.js → infoDiagram-5YYISTIA-SH8cr905.js} +1 -1
  67. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DNVgA93n.js → ishikawaDiagram-YF4QCWOH-BjqaoU3U.js} +1 -1
  68. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CLpSNjM3.js → journeyDiagram-JHISSGLW-CBmdEVrd.js} +1 -1
  69. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BRzuXcKr.js → kanban-definition-UN3LZRKU-U8njjMLZ.js} +1 -1
  70. package/packages/web/dist/assets/{linear-BxOawONt.js → linear-DF66GPs1.js} +1 -1
  71. package/packages/web/dist/assets/{mermaid.core-Dvs1QCcz.js → mermaid.core-CouJz7Ph.js} +4 -4
  72. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-Dmt7pYwF.js → mindmap-definition-RKZ34NQL-BYIf0oFt.js} +1 -1
  73. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BHiZaJD-.js → pieDiagram-4H26LBE5-Cm8rgg09.js} +1 -1
  74. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-U0A1noRE.js → quadrantDiagram-W4KKPZXB-C3Il5kOH.js} +1 -1
  75. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CZLsr6yi.js → requirementDiagram-4Y6WPE33-D3PE2nTe.js} +1 -1
  76. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-BTlLGILU.js → sankeyDiagram-5OEKKPKP-DdmvBM18.js} +1 -1
  77. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-DpSS_Gb1.js → sequenceDiagram-3UESZ5HK-B8M_GAIq.js} +1 -1
  78. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-D9srE-tV.js → stateDiagram-AJRCARHV-DZkXnK1_.js} +1 -1
  79. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CjCdT9Ls.js +2 -0
  80. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BYQ5CKab.js → timeline-definition-PNZ67QCA-IHXq7rqi.js} +1 -1
  81. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-B4W2CtSV.js → vennDiagram-CIIHVFJN-Dga7tLtT.js} +1 -1
  82. package/packages/web/dist/assets/{wardley-L42UT6IY--XUV7GvD.js → wardley-L42UT6IY-CmcwnEJ-.js} +1 -1
  83. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-Bl2VIEGY.js → wardleyDiagram-YWT4CUSO-zHuSLrD4.js} +1 -1
  84. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-Bj2H9Rcf.js → xychartDiagram-2RQKCTM6-D93MU_ic.js} +1 -1
  85. package/packages/web/dist/index.html +1 -1
  86. package/packages/web/dist/sw.js +1 -1
  87. package/packages/web/dist/assets/DiscoverPage-CQlzxsn4.js +0 -2
  88. package/packages/web/dist/assets/channel-ExsAG7qT.js +0 -2
  89. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-si7g9okq.js +0 -2
  90. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-si7g9okq.js +0 -2
  91. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-AxWqsDh-.js +0 -2
@@ -0,0 +1,232 @@
1
+ /**
2
+ * The small semver `update` needs, and no more (CLI contract SPEC §5 step 4).
3
+ *
4
+ * Three questions are asked of versions in this contract: is the target newer
5
+ * than what we have, is it a prerelease, and does the target's `engines.node`
6
+ * accept the Node running the update. That is it — so this is ~120 lines of
7
+ * Node built-ins rather than a dependency, which is what SPEC §5 asks for and
8
+ * what keeps `update` runnable from a CLI that must not load the server.
9
+ *
10
+ * **What it deliberately does not do.** No `semver` package behaviours beyond
11
+ * the above: no ranges with hyphens (`1.2 - 2.3`), no `x`-ranges (`1.x`), no
12
+ * build metadata ordering. An `engines.node` we cannot parse is reported as
13
+ * "cannot tell" rather than as "fails", because refusing an update on the
14
+ * strength of a range this file does not understand would be the worse error of
15
+ * the two — see {@link satisfiesEngines}.
16
+ *
17
+ * Generic: wave C4 lifts this into `reference/` with the rest of `update`.
18
+ */
19
+ const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
20
+ /** `1.2.3-rc.1` → a {@link Version}, or undefined if it is not a version. */
21
+ export function parseVersion(raw) {
22
+ const m = VERSION_RE.exec(raw.trim());
23
+ if (m === null)
24
+ return undefined;
25
+ const prerelease = m[4] === undefined
26
+ ? []
27
+ : m[4].split(".").map((id) => (/^\d+$/.test(id) ? Number(id) : id));
28
+ return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), prerelease };
29
+ }
30
+ /** Is this a prerelease? SPEC §5: prereleases count only when `--to` names them. */
31
+ export function isPrerelease(raw) {
32
+ return (parseVersion(raw)?.prerelease.length ?? 0) > 0;
33
+ }
34
+ /**
35
+ * -1, 0 or 1, by the semver rules — including the prerelease ones, which are
36
+ * the only interesting part: `1.0.0-rc.1` is LOWER than `1.0.0`, numeric
37
+ * identifiers compare numerically and sort below alphanumeric ones, and a
38
+ * longer run of identifiers wins when every shared one is equal.
39
+ *
40
+ * Build metadata is ignored, as semver says it must be.
41
+ */
42
+ export function compareVersions(a, b) {
43
+ const va = parseVersion(a);
44
+ const vb = parseVersion(b);
45
+ // Unparseable compares as equal rather than throwing: the callers all have a
46
+ // better answer for "I cannot tell" than a crash (`update` says the registry
47
+ // gave it something it did not understand, and stops).
48
+ if (va === undefined || vb === undefined)
49
+ return 0;
50
+ for (const k of ["major", "minor", "patch"]) {
51
+ if (va[k] !== vb[k])
52
+ return va[k] < vb[k] ? -1 : 1;
53
+ }
54
+ if (va.prerelease.length === 0 && vb.prerelease.length === 0)
55
+ return 0;
56
+ if (va.prerelease.length === 0)
57
+ return 1;
58
+ if (vb.prerelease.length === 0)
59
+ return -1;
60
+ const n = Math.max(va.prerelease.length, vb.prerelease.length);
61
+ for (let i = 0; i < n; i++) {
62
+ const x = va.prerelease[i];
63
+ const y = vb.prerelease[i];
64
+ if (x === undefined)
65
+ return -1;
66
+ if (y === undefined)
67
+ return 1;
68
+ if (x === y)
69
+ continue;
70
+ const xNum = typeof x === "number";
71
+ const yNum = typeof y === "number";
72
+ if (xNum && yNum)
73
+ return x < y ? -1 : 1;
74
+ if (xNum)
75
+ return -1;
76
+ if (yNum)
77
+ return 1;
78
+ return String(x) < String(y) ? -1 : 1;
79
+ }
80
+ return 0;
81
+ }
82
+ /** Is `a` strictly newer than `b`? */
83
+ export const isNewer = (a, b) => compareVersions(a, b) > 0;
84
+ /** The highest of a list, ignoring anything unparseable. Undefined if none parse. */
85
+ export function maxVersion(versions) {
86
+ let best;
87
+ for (const v of versions) {
88
+ if (parseVersion(v) === undefined)
89
+ continue;
90
+ if (best === undefined || isNewer(v, best))
91
+ best = v;
92
+ }
93
+ return best;
94
+ }
95
+ const COMPARATOR_RE = /^(>=|<=|>|<|=|\^|~)?\s*v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?$/;
96
+ /**
97
+ * Expand one range atom into the comparators it means.
98
+ *
99
+ * `^22.1.0` is `>=22.1.0 <23.0.0`; `~22.1.0` is `>=22.1.0 <22.2.0`; a bare
100
+ * `22` is `>=22.0.0 <23.0.0` (npm's partial-version semantics, which is what
101
+ * `engines: { node: "22" }` means in the wild). Returns undefined for anything
102
+ * this file does not understand, which propagates up as "cannot tell".
103
+ */
104
+ function comparatorsFor(atom) {
105
+ const m = COMPARATOR_RE.exec(atom);
106
+ if (m === null)
107
+ return undefined;
108
+ const op = m[1] ?? "=";
109
+ const major = Number(m[2]);
110
+ const wild = (s) => s === undefined || s === "x" || s === "X" || s === "*";
111
+ const minorWild = wild(m[3]);
112
+ const patchWild = wild(m[4]);
113
+ const minor = minorWild ? 0 : Number(m[3]);
114
+ const patch = patchWild ? 0 : Number(m[4]);
115
+ const prerelease = m[5] === undefined ? [] : m[5].split(".").map((id) => (/^\d+$/.test(id) ? Number(id) : id));
116
+ const at = (ma, mi, pa) => ({
117
+ major: ma,
118
+ minor: mi,
119
+ patch: pa,
120
+ prerelease: [],
121
+ });
122
+ const lower = { major, minor, patch, prerelease };
123
+ if (op === "^") {
124
+ // `^0.x` tightens: ^0.2.3 is >=0.2.3 <0.3.0, ^0.0.3 is >=0.0.3 <0.0.4.
125
+ const upper = major > 0
126
+ ? at(major + 1, 0, 0)
127
+ : minor > 0 || !minorWild
128
+ ? at(0, minor + 1, 0)
129
+ : at(1, 0, 0);
130
+ return [
131
+ { op: ">=", version: lower },
132
+ { op: "<", version: upper },
133
+ ];
134
+ }
135
+ if (op === "~") {
136
+ const upper = minorWild ? at(major + 1, 0, 0) : at(major, minor + 1, 0);
137
+ return [
138
+ { op: ">=", version: lower },
139
+ { op: "<", version: upper },
140
+ ];
141
+ }
142
+ if (op === "=" && (minorWild || patchWild)) {
143
+ // A partial version with no operator is a range, not an equality: `22`
144
+ // means the whole 22 line, and `22.1` means the whole 22.1 line.
145
+ const upper = minorWild ? at(major + 1, 0, 0) : at(major, minor + 1, 0);
146
+ return [
147
+ { op: ">=", version: lower },
148
+ { op: "<", version: upper },
149
+ ];
150
+ }
151
+ if (minorWild || patchWild) {
152
+ // `>=22` / `<23`: fill the missing parts with zero, which is what the
153
+ // comparison wants in both directions.
154
+ return [{ op: op, version: lower }];
155
+ }
156
+ return [{ op: op, version: lower }];
157
+ }
158
+ function satisfiesComparator(v, c) {
159
+ const cmp = compare(v, c.version);
160
+ switch (c.op) {
161
+ case ">=":
162
+ return cmp >= 0;
163
+ case "<=":
164
+ return cmp <= 0;
165
+ case ">":
166
+ return cmp > 0;
167
+ case "<":
168
+ return cmp < 0;
169
+ case "=":
170
+ return cmp === 0;
171
+ }
172
+ }
173
+ function compare(a, b) {
174
+ return compareVersions(stringify(a), stringify(b));
175
+ }
176
+ function stringify(v) {
177
+ const base = `${v.major}.${v.minor}.${v.patch}`;
178
+ return v.prerelease.length === 0 ? base : `${base}-${v.prerelease.join(".")}`;
179
+ }
180
+ /**
181
+ * Does `version` satisfy `range`? Undefined means "this file cannot tell".
182
+ *
183
+ * Supports the forms an `engines.node` actually uses: `>=22`, `>=22.0.0`,
184
+ * `^22.1.0`, `~22.1`, `22`, `22.x`, `>=20 <23`, and `||` between any of those.
185
+ * `*` and an empty range accept everything.
186
+ */
187
+ export function satisfies(version, range) {
188
+ const v = parseVersion(version);
189
+ if (v === undefined)
190
+ return undefined;
191
+ const trimmed = range.trim();
192
+ if (trimmed === "" || trimmed === "*" || trimmed === "x")
193
+ return true;
194
+ const alternatives = trimmed.split("||");
195
+ let known = false;
196
+ for (const alt of alternatives) {
197
+ const atoms = alt.trim().split(/\s+/).filter(Boolean);
198
+ const comparators = [];
199
+ let parsed = true;
200
+ for (const atom of atoms) {
201
+ const cs = comparatorsFor(atom);
202
+ if (cs === undefined) {
203
+ parsed = false;
204
+ break;
205
+ }
206
+ comparators.push(...cs);
207
+ }
208
+ if (!parsed)
209
+ continue;
210
+ known = true;
211
+ if (comparators.every((c) => satisfiesComparator(v, c)))
212
+ return true;
213
+ }
214
+ // Every alternative we could read said no. If we could read none of them, say
215
+ // so rather than claiming a refusal.
216
+ return known ? false : undefined;
217
+ }
218
+ /**
219
+ * Will the target version run on this Node? (SPEC §5 step 4.)
220
+ *
221
+ * The three-way answer is the point. A target with no `engines.node` at all, or
222
+ * one whose range this file cannot parse, must NOT block an update: the cost of
223
+ * a false refusal is a tool that cannot update itself and says something the
224
+ * user cannot act on, while the cost of letting an incompatible Node through is
225
+ * the failure `engines` would have produced anyway, with a rollback behind it.
226
+ */
227
+ export function satisfiesEngines(nodeVersion, engines) {
228
+ const range = engines?.node;
229
+ if (range === undefined || range.trim() === "")
230
+ return { ok: true, range: undefined };
231
+ return { ok: satisfies(nodeVersion, range), range };
232
+ }
@@ -0,0 +1,80 @@
1
+ import type { InstallMethod } from "./install-method.js";
2
+ export declare const UPDATE_CHECK_FILE = "update-check.json";
3
+ export declare function updateCheckPath(home: string): string;
4
+ /** `update --check --json`, and the cached copy. Matches `schemas/update-check.schema.json`. */
5
+ export interface UpdateCheck {
6
+ name: string;
7
+ current: string;
8
+ target: string;
9
+ /** The dist-tag asked for, or null when `--to` named a version. */
10
+ tag: string | null;
11
+ updateAvailable: boolean;
12
+ installMethod: InstallMethod;
13
+ canUpdate: boolean;
14
+ /** `<tool> update`, or the command this install needs instead. */
15
+ command: string | null;
16
+ engines: string | null;
17
+ nodeOk: boolean;
18
+ changelog: string | null;
19
+ /** ISO 8601. */
20
+ checkedAt: string;
21
+ }
22
+ /** What `service status --json` carries (`schemas/service-status.schema.json`). */
23
+ export interface UpdateSummary {
24
+ latest: string;
25
+ updateAvailable: boolean;
26
+ checkedAt: string;
27
+ }
28
+ /**
29
+ * Write the cache atomically.
30
+ *
31
+ * Atomic because `service status` reads it on a path where a torn read has no
32
+ * recovery worth having — it would report `updateAvailable` as absent and say
33
+ * nothing, which is indistinguishable from "never checked". Best-effort:
34
+ * failing to write a cache must never fail an update, so this throws nothing.
35
+ */
36
+ export declare function writeUpdateCheck(home: string, check: UpdateCheck): void;
37
+ /** The cache, or null when there isn't one (or it isn't one). */
38
+ export declare function readUpdateCheck(home: string): UpdateCheck | null;
39
+ /**
40
+ * How long a cached answer is worth repeating.
41
+ *
42
+ * `status` says "0.81.0 is available" on the strength of a file, so the file
43
+ * has to be recent enough that the claim is still likely true. A week is long
44
+ * enough to cover "I checked on Monday, it is Friday" and short enough that a
45
+ * months-old file does not have `status` advertising a version that has since
46
+ * been superseded twice.
47
+ */
48
+ export declare const UPDATE_CHECK_MAX_AGE_MS: number;
49
+ /**
50
+ * The summary `service status` shows, or undefined when there is nothing worth
51
+ * repeating: no cache, a cache from a different install, or one too old.
52
+ */
53
+ export declare function updateSummary(home: string, o?: {
54
+ now?: number;
55
+ maxAgeMs?: number;
56
+ }): UpdateSummary | undefined;
57
+ export interface NoticeInput {
58
+ name: string;
59
+ current: string;
60
+ target: string;
61
+ /** `paddock update`, or the command this install needs instead (SPEC §3, §6). */
62
+ command: string;
63
+ /** The parenthetical after the command: `the global folder isn't writable`. */
64
+ note?: string;
65
+ changelog?: string | null;
66
+ }
67
+ /**
68
+ * SPEC §3's update notice, verbatim in shape:
69
+ *
70
+ * ```
71
+ * Paddock 0.80.0 is available (you have 0.79.1).
72
+ * Run: paddock update
73
+ * Changes: https://github.com/edspencer/paddock/releases/tag/v0.80.0
74
+ * ```
75
+ *
76
+ * Returned as a string rather than printed, because the caller decides the
77
+ * stream: §3 puts this on **stderr**, always, so that a `--json` document on
78
+ * stdout stays the only thing on stdout.
79
+ */
80
+ export declare function updateNotice(o: NoticeInput): string;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * `<home>/update-check.json` — what the last registry question found
3
+ * (CLI contract SPEC §3).
4
+ *
5
+ * The contract's rule is that **nothing contacts the registry by itself**: only
6
+ * `update`, `update --check` and `doctor` ask, and only when a person runs
7
+ * them. But `service status` should still be able to mention a newer version,
8
+ * because that is where someone looks. The cache is how both hold: the three
9
+ * commands that ask write what they found here, and `status` reads the file and
10
+ * never opens a socket.
11
+ *
12
+ * The document is the one `schemas/update-check.schema.json` describes, so
13
+ * `update --check --json` and this file are the same shape — one less thing to
14
+ * keep in step, and `status --json`'s `update` object is a projection of it.
15
+ *
16
+ * Generic: wave C4 lifts this into `reference/`. Node built-ins only.
17
+ */
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+ export const UPDATE_CHECK_FILE = "update-check.json";
21
+ export function updateCheckPath(home) {
22
+ return path.join(home, UPDATE_CHECK_FILE);
23
+ }
24
+ /**
25
+ * Write the cache atomically.
26
+ *
27
+ * Atomic because `service status` reads it on a path where a torn read has no
28
+ * recovery worth having — it would report `updateAvailable` as absent and say
29
+ * nothing, which is indistinguishable from "never checked". Best-effort:
30
+ * failing to write a cache must never fail an update, so this throws nothing.
31
+ */
32
+ export function writeUpdateCheck(home, check) {
33
+ try {
34
+ fs.mkdirSync(home, { recursive: true });
35
+ const file = updateCheckPath(home);
36
+ const tmp = `${file}.${process.pid}.tmp`;
37
+ fs.writeFileSync(tmp, `${JSON.stringify(check, null, 2)}\n`, { mode: 0o600 });
38
+ try {
39
+ fs.renameSync(tmp, file);
40
+ }
41
+ catch (err) {
42
+ fs.rmSync(tmp, { force: true });
43
+ throw err;
44
+ }
45
+ }
46
+ catch {
47
+ /* a cache that could not be written is not a failed update */
48
+ }
49
+ }
50
+ /** The cache, or null when there isn't one (or it isn't one). */
51
+ export function readUpdateCheck(home) {
52
+ try {
53
+ const raw = JSON.parse(fs.readFileSync(updateCheckPath(home), "utf8"));
54
+ if (typeof raw.name === "string" &&
55
+ typeof raw.current === "string" &&
56
+ typeof raw.target === "string" &&
57
+ typeof raw.updateAvailable === "boolean" &&
58
+ typeof raw.checkedAt === "string") {
59
+ return raw;
60
+ }
61
+ }
62
+ catch {
63
+ /* missing, half-written, or from something else */
64
+ }
65
+ return null;
66
+ }
67
+ /**
68
+ * How long a cached answer is worth repeating.
69
+ *
70
+ * `status` says "0.81.0 is available" on the strength of a file, so the file
71
+ * has to be recent enough that the claim is still likely true. A week is long
72
+ * enough to cover "I checked on Monday, it is Friday" and short enough that a
73
+ * months-old file does not have `status` advertising a version that has since
74
+ * been superseded twice.
75
+ */
76
+ export const UPDATE_CHECK_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
77
+ /**
78
+ * The summary `service status` shows, or undefined when there is nothing worth
79
+ * repeating: no cache, a cache from a different install, or one too old.
80
+ */
81
+ export function updateSummary(home, o = {}) {
82
+ const check = readUpdateCheck(home);
83
+ if (check === null)
84
+ return undefined;
85
+ const at = Date.parse(check.checkedAt);
86
+ if (!Number.isFinite(at))
87
+ return undefined;
88
+ const age = (o.now ?? Date.now()) - at;
89
+ if (age > (o.maxAgeMs ?? UPDATE_CHECK_MAX_AGE_MS))
90
+ return undefined;
91
+ return {
92
+ latest: check.target,
93
+ updateAvailable: check.updateAvailable,
94
+ checkedAt: check.checkedAt,
95
+ };
96
+ }
97
+ /**
98
+ * SPEC §3's update notice, verbatim in shape:
99
+ *
100
+ * ```
101
+ * Paddock 0.80.0 is available (you have 0.79.1).
102
+ * Run: paddock update
103
+ * Changes: https://github.com/edspencer/paddock/releases/tag/v0.80.0
104
+ * ```
105
+ *
106
+ * Returned as a string rather than printed, because the caller decides the
107
+ * stream: §3 puts this on **stderr**, always, so that a `--json` document on
108
+ * stdout stays the only thing on stdout.
109
+ */
110
+ export function updateNotice(o) {
111
+ const lines = [`${o.name} ${o.target} is available (you have ${o.current}).`];
112
+ lines.push(` Run: ${o.command}${o.note !== undefined ? ` (${o.note})` : ""}`);
113
+ if (o.changelog !== undefined && o.changelog !== null)
114
+ lines.push(` Changes: ${o.changelog}`);
115
+ return lines.join("\n");
116
+ }
@@ -0,0 +1,160 @@
1
+ import fsDefault from "node:fs";
2
+ import type { Output } from "../output.js";
3
+ import { type InstallInfo } from "./install-method.js";
4
+ import { type RunResult, type Spawn } from "./package-manager.js";
5
+ /** SPEC §5 step 12: 30 seconds unless the tool sets its own. */
6
+ export declare const HEALTH_TIMEOUT_MS = 30000;
7
+ /** How many log lines a failure prints (SPEC §5 step 13). */
8
+ export declare const LOG_LINES_ON_FAILURE = 20;
9
+ /** What `update` was asked to do. */
10
+ export interface UpdateOptions {
11
+ /** Report and change nothing (SPEC §1). */
12
+ check?: boolean;
13
+ /** A version or a dist-tag. The only way to move to a lower version. */
14
+ to?: string;
15
+ /** Skip the question. Required without a TTY. */
16
+ yes?: boolean;
17
+ /** Skip the busy check, and nothing else. */
18
+ force?: boolean;
19
+ json?: boolean;
20
+ }
21
+ /** What the service looks like from `update`'s point of view. */
22
+ export interface ServiceSnapshot {
23
+ installed: boolean;
24
+ running: boolean;
25
+ /** The URL the installed unit serves, when there is one. */
26
+ url?: string;
27
+ /** The log file the unit names, for the failure tail. */
28
+ log?: string;
29
+ }
30
+ export interface UpdateServiceHooks {
31
+ /** Is a unit installed, is it running, and where. */
32
+ inspect(): ServiceSnapshot;
33
+ /** Stop the service. Throws with the supervisor's own words on failure. */
34
+ stop(): void;
35
+ /**
36
+ * `<cli> service install --refresh`, run through the NEW CLI (SPEC §5 step
37
+ * 11). Returns the subprocess result so the caller can show what it said.
38
+ */
39
+ refresh(cliPath: string): RunResult;
40
+ /** The last `lines` lines of the service log, or '' when there is no log. */
41
+ tail(lines: number): string;
42
+ }
43
+ /** "Did the target answer?" — the three outcomes that matter (SPEC §5 step 12). */
44
+ export type HealthVerdict = {
45
+ state: "ok";
46
+ version: string;
47
+ }
48
+ /** Something answers, but it is not the version we installed. */
49
+ | {
50
+ state: "wrong-version";
51
+ version: string;
52
+ } | {
53
+ state: "no-answer";
54
+ };
55
+ export interface UpdateProduct {
56
+ /** `Paddock`. */
57
+ name: string;
58
+ /** `paddock` — the bin, for `<tool>: <message>` and printed commands. */
59
+ tool: string;
60
+ /** `@edspencer/paddock`. */
61
+ packageName: string;
62
+ /** `PADDOCK` — gives `PADDOCK_IN_DOCKER` and `PADDOCK_UPDATE_HINT`. */
63
+ envPrefix: string;
64
+ /** The data home: `<home>/update.lock` and `<home>/update-check.json`. */
65
+ home: string;
66
+ /** This CLI's own version, from its `package.json`. */
67
+ currentVersion: string;
68
+ /**
69
+ * `process.argv[1]` — NOT realpath'd.
70
+ *
71
+ * Both forms are needed and for different reasons. The unresolved path is a
72
+ * bin shim, and re-resolving it AFTER the install is what finds the new CLI
73
+ * under a package manager whose realpath carries the version in it (pnpm).
74
+ * The realpath is what SPEC §6's table walks up from.
75
+ */
76
+ entry: string;
77
+ /** Where the CLI sits inside the package: `packages/server/dist/cli/paddock.js`. */
78
+ binSubpath: string;
79
+ /** The container image, for the Docker notice: `ghcr.io/edspencer/paddock`. */
80
+ image?: string;
81
+ /**
82
+ * The synopsis printed after `update`'s own usage error (SPEC §2: the usage
83
+ * follows the message).
84
+ *
85
+ * `update` has exactly one — "there is no terminal to ask, so `--yes` is
86
+ * required" — and it is the command's own usage rather than the tool's,
87
+ * because the reader's problem is one flag on the command they just typed.
88
+ */
89
+ usage?: string;
90
+ /** The release notes for a version. */
91
+ changelogUrl(version: string): string;
92
+ healthTimeoutMs?: number;
93
+ /** The busy check (SPEC §5 step 6): null to go ahead, or 75 having printed why. */
94
+ guardBusy(o: {
95
+ force: boolean;
96
+ home: string;
97
+ }): Promise<number | null>;
98
+ service: UpdateServiceHooks;
99
+ /** Poll `<url>/api/health` until it reports `version`, or the budget runs out. */
100
+ waitForVersion(o: {
101
+ url: string;
102
+ version: string;
103
+ timeoutMs: number;
104
+ }): Promise<HealthVerdict>;
105
+ }
106
+ /** Everything that touches the world, injected so the sequence is testable. */
107
+ export interface UpdateEnvironment {
108
+ out: Output;
109
+ env?: NodeJS.ProcessEnv;
110
+ spawn?: Spawn;
111
+ execPath?: string;
112
+ platform?: NodeJS.Platform;
113
+ fetch?: typeof globalThis.fetch;
114
+ fs?: Pick<typeof fsDefault, "existsSync" | "readFileSync" | "realpathSync">;
115
+ /** Is there a person at a keyboard? (SPEC §3.) */
116
+ isTTY?: boolean;
117
+ /**
118
+ * The Node to test the target's `engines.node` against.
119
+ *
120
+ * Injected so the "the target needs a newer Node" path can be proven, which
121
+ * it otherwise cannot be: a test runs on whatever Node CI has, and the only
122
+ * Nodes that fail a real `engines` range are ones the suite cannot run on.
123
+ */
124
+ nodeVersion?: string;
125
+ /** Ask the question. Only called when {@link isTTY} and `--yes` was not given. */
126
+ confirm?: (question: string) => Promise<boolean>;
127
+ /** Injected for tests: `isWritable`. */
128
+ writable?: (dir: string) => boolean;
129
+ }
130
+ /**
131
+ * Run `update`, and return the exit code (SPEC §2: the caller assigns
132
+ * `process.exitCode`; nothing here calls `process.exit`).
133
+ */
134
+ export declare function runUpdate(product: UpdateProduct, opts: UpdateOptions, envp: UpdateEnvironment): Promise<number>;
135
+ /**
136
+ * Find the CLI on disk that reports `wanted`, trying the places it can be.
137
+ *
138
+ * Three candidates, in the order that copes with the most layouts:
139
+ *
140
+ * 1. **`realpath(argv[1])`, resolved NOW.** The bin shim is a symlink, and a
141
+ * package manager that puts the version in the package's real path (pnpm's
142
+ * store) repoints that symlink on install. Re-resolving it after the install
143
+ * is the only candidate that follows.
144
+ * 2. `<npm root -g>/<name>/<binSubpath>` — the npm-global layout, asked of npm
145
+ * rather than assumed.
146
+ * 3. The package root §6 found, plus the same subpath — right for every layout
147
+ * whose path does not move.
148
+ *
149
+ * Each is run (`node <cli> --version`) and the first that reports `wanted`
150
+ * wins. That is deliberately stricter than "the file exists": the thing this
151
+ * guards against is handing `service install --refresh` to a CLI that is still
152
+ * the old version, which would produce a green update that changed nothing.
153
+ */
154
+ export declare function findCli(product: UpdateProduct, info: InstallInfo | undefined, wanted: string, ctx: {
155
+ spawn?: Spawn;
156
+ execPath?: string;
157
+ platform?: NodeJS.Platform;
158
+ env?: NodeJS.ProcessEnv;
159
+ fs?: Pick<typeof fsDefault, "existsSync" | "readFileSync" | "realpathSync">;
160
+ }): string | undefined;