@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.
- package/package.json +1 -1
- package/packages/server/dist/app.d.ts +7 -0
- package/packages/server/dist/app.js +1 -1
- package/packages/server/dist/auth.d.ts +3 -2
- package/packages/server/dist/auth.js +11 -0
- package/packages/server/dist/background-live.d.ts +17 -0
- package/packages/server/dist/background-live.js +24 -0
- package/packages/server/dist/busy.d.ts +96 -0
- package/packages/server/dist/busy.js +88 -0
- package/packages/server/dist/cli/args.d.ts +70 -29
- package/packages/server/dist/cli/args.js +306 -7
- package/packages/server/dist/cli/busy-command.d.ts +58 -0
- package/packages/server/dist/cli/busy-command.js +108 -0
- package/packages/server/dist/cli/busy.d.ts +69 -0
- package/packages/server/dist/cli/busy.js +150 -0
- package/packages/server/dist/cli/output.d.ts +52 -0
- package/packages/server/dist/cli/output.js +84 -0
- package/packages/server/dist/cli/paddock.js +65 -21
- package/packages/server/dist/cli/service/index.d.ts +64 -0
- package/packages/server/dist/cli/service/index.js +208 -7
- package/packages/server/dist/cli/service/launchd.d.ts +15 -1
- package/packages/server/dist/cli/service/launchd.js +116 -18
- package/packages/server/dist/cli/service/ready.d.ts +30 -0
- package/packages/server/dist/cli/service/ready.js +35 -0
- package/packages/server/dist/cli/service/systemd.js +27 -0
- package/packages/server/dist/cli/update/install-method.d.ts +131 -0
- package/packages/server/dist/cli/update/install-method.js +258 -0
- package/packages/server/dist/cli/update/lock.d.ts +40 -0
- package/packages/server/dist/cli/update/lock.js +108 -0
- package/packages/server/dist/cli/update/package-manager.d.ts +109 -0
- package/packages/server/dist/cli/update/package-manager.js +221 -0
- package/packages/server/dist/cli/update/registry.d.ts +89 -0
- package/packages/server/dist/cli/update/registry.js +145 -0
- package/packages/server/dist/cli/update/semver.d.ts +68 -0
- package/packages/server/dist/cli/update/semver.js +232 -0
- package/packages/server/dist/cli/update/update-check.d.ts +80 -0
- package/packages/server/dist/cli/update/update-check.js +116 -0
- package/packages/server/dist/cli/update/update.d.ts +160 -0
- package/packages/server/dist/cli/update/update.js +609 -0
- package/packages/server/dist/cli/update-command.d.ts +20 -0
- package/packages/server/dist/cli/update-command.js +113 -0
- package/packages/server/dist/herdctl.d.ts +19 -0
- package/packages/server/dist/herdctl.js +26 -0
- package/packages/server/dist/routes/meta.js +45 -0
- package/packages/server/dist/runtime-file.d.ts +26 -0
- package/packages/server/dist/runtime-file.js +91 -0
- package/packages/server/dist/start.d.ts +33 -0
- package/packages/server/dist/start.js +115 -8
- package/packages/server/dist/ws-context.d.ts +9 -0
- package/packages/server/dist/ws-turn.js +1 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{DeleteProjectDialog-DHczLhwQ.js → DeleteProjectDialog-CphdGB3R.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-C4OPLNO8.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-DtTOpTji.js → DiscoverView-B_kcF4sm.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-BRoBh1C1.js → InstanceConfigPage-CvUK5DKW.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DJf8IJuk.js → MarkdownRenderer-DvZ-j_Ao.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-EioguUKW.js → ProjectRedirect-DayHvUH2.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-BiKnGfk6.js → ProjectView-CCw83gUi.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-D6kfaIal.js → ProjectsGrid-B3Klg2H4.js} +1 -1
- package/packages/web/dist/assets/{RootHome-DpcyuV6e.js → RootHome-BxcDvpgI.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-CHM29PhP.js → Surfaces-7UlhFBSG.js} +1 -1
- package/packages/web/dist/assets/{arc-fiqD05Hu.js → arc-DNkh5o5Q.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BuuB2Nkf.js → architectureDiagram-3BPJPVTR-BrgN3oLF.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DQMBuzfi.js → blockDiagram-GPEHLZMM-D05XOJSk.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-BKN0UxCH.js → c4Diagram-AAUBKEIU-CKaMBhMP.js} +1 -1
- package/packages/web/dist/assets/channel-JrEedxB8.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-DW37EupX.js → chunk-2J33WTMH-8hgadCQc.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-Dyv7zV_c.js → chunk-4BX2VUAB-CBfEC-pu.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-DzMR0dOk.js → chunk-55IACEB6-DV1eMgHk.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-BdZ3Sor8.js → chunk-727SXJPM-Cfgl5fxY.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-B4i7_og0.js → chunk-AQP2D5EJ-BXhqD295.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-jcTr85it.js → chunk-FMBD7UC4-TFGOPtek.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-fgMsadqB.js → chunk-ND2GUHAM-CHFM665R.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-bJNHYS1W.js → chunk-QZHKN3VN-CqEf6MMT.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CHJiQp-W.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CHJiQp-W.js +2 -0
- package/packages/web/dist/assets/{core-Bk4KFrmN.js → core-DmEaKxBb.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BIqCSlQy.js → cose-bilkent-S5V4N54A-BnGI6jkr.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-6nyhtVQz.js → dagre-BM42HDAG-CKoPx61E.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-CjizfipR.js → diagram-2AECGRRQ-mMmtlBl1.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-DlgB5vWb.js → diagram-5GNKFQAL-Cdz9Mtnt.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-CNC9qanV.js → diagram-KO2AKTUF-DQxM3wYj.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-DSJ-s9lh.js → diagram-LMA3HP47-BWrinPsq.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-C9FoHBDg.js → diagram-OG6HWLK6-U6MYUve2.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-Cj_AY97j.js → documentTitle-DGjRXuTP.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-z6gku4Yt.js → erDiagram-TEJ5UH35-xqMCsXxY.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-Dl5c_FgU.js → flowDiagram-I6XJVG4X-PMfy_czF.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CIscJ-Dt.js → ganttDiagram-6RSMTGT7-whhYYE4N.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CvttOXM_.js → gitGraphDiagram-PVQCEYII-C51JW4tf.js} +1 -1
- package/packages/web/dist/assets/{index-DN82do6Z.js → index-CHOtvlnO.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-dzWYdDJj.js → infoDiagram-5YYISTIA-SH8cr905.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-8yX9k4bo.js → ishikawaDiagram-YF4QCWOH-BjqaoU3U.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-BmitzJDR.js → journeyDiagram-JHISSGLW-CBmdEVrd.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BZmIkfkW.js → kanban-definition-UN3LZRKU-U8njjMLZ.js} +1 -1
- package/packages/web/dist/assets/{linear-BBLXE4o7.js → linear-DF66GPs1.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-mSKff4yu.js → mermaid.core-CouJz7Ph.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BWAUa4CP.js → mindmap-definition-RKZ34NQL-BYIf0oFt.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-VmVdiL4C.js → pieDiagram-4H26LBE5-Cm8rgg09.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Dybfv6NU.js → quadrantDiagram-W4KKPZXB-C3Il5kOH.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-BPnOvB7w.js → requirementDiagram-4Y6WPE33-D3PE2nTe.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-YXIEmKw7.js → sankeyDiagram-5OEKKPKP-DdmvBM18.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-DintIRUe.js → sequenceDiagram-3UESZ5HK-B8M_GAIq.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-eMsjahWp.js → stateDiagram-AJRCARHV-DZkXnK1_.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CjCdT9Ls.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-as4zRCPn.js → timeline-definition-PNZ67QCA-IHXq7rqi.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-CPzvUkM0.js → vennDiagram-CIIHVFJN-Dga7tLtT.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-q6moXj5g.js → wardley-L42UT6IY-CmcwnEJ-.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-BsEEb9Gm.js → wardleyDiagram-YWT4CUSO-zHuSLrD4.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DSgZlcZW.js → xychartDiagram-2RQKCTM6-D93MU_ic.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/DiscoverPage-BSGSzTEs.js +0 -2
- package/packages/web/dist/assets/channel-B7yh6Su_.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DIlhMPgi.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DIlhMPgi.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CpAOGzbO.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;
|