@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,221 @@
1
+ /**
2
+ * Running the package manager (CLI contract SPEC §5 steps 8 and 10, §6, §7).
3
+ *
4
+ * Three jobs, all of them small and all of them easy to get subtly wrong:
5
+ *
6
+ * - **Find npm.** SPEC §6 and §10 both say the `npm` next to
7
+ * `process.execPath`, not whatever is on PATH. Under nvm, fnm or volta those
8
+ * are routinely different npms belonging to different Nodes, and asking the
9
+ * wrong one `npm root -g` gets a global folder this install is not in — which
10
+ * would report a perfectly ordinary global install as a `dependency` and
11
+ * refuse to update it.
12
+ * - **No shell.** `spawnSync(npm, args)` with the default `shell: false`, so a
13
+ * package name or a path with a space in it cannot become two arguments.
14
+ * Windows is the exception and has to be: `npm` there is `npm.cmd`, which
15
+ * Node refuses to spawn without a shell since its fix for CVE-2024-27980
16
+ * (SPEC §7). That path quotes its arguments itself.
17
+ * - **Can we write there?** SPEC §5 step 5. Asked before anything is stopped,
18
+ * and answered by actually creating a file rather than by `access()` alone —
19
+ * see {@link isWritable}.
20
+ *
21
+ * Generic: wave C4 lifts this into `reference/`. Node built-ins only.
22
+ */
23
+ import { spawnSync } from "node:child_process";
24
+ import fs from "node:fs";
25
+ import os from "node:os";
26
+ import path from "node:path";
27
+ export const spawnSyncRunner = (cmd, args, opts = {}) => {
28
+ const r = spawnSync(cmd, args, {
29
+ encoding: "utf8",
30
+ ...(opts.env !== undefined ? { env: opts.env } : {}),
31
+ ...(opts.shell === true ? { shell: true } : {}),
32
+ ...(opts.timeoutMs !== undefined ? { timeout: opts.timeoutMs } : {}),
33
+ maxBuffer: 32 * 1024 * 1024,
34
+ });
35
+ return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
36
+ };
37
+ /**
38
+ * The npm that belongs to the Node running us.
39
+ *
40
+ * `<dirname(execPath)>/npm` on POSIX; on Windows the sibling is `npm.cmd`.
41
+ * Falls back to the bare name — if the sibling is not there (a distro that
42
+ * splits node and npm into separate packages), PATH is the only answer left and
43
+ * is better than failing.
44
+ */
45
+ export function npmBeside(execPath = process.execPath, platform = process.platform) {
46
+ const dir = path.dirname(execPath);
47
+ const candidate = path.join(dir, platform === "win32" ? "npm.cmd" : "npm");
48
+ try {
49
+ if (fs.existsSync(candidate))
50
+ return candidate;
51
+ }
52
+ catch {
53
+ /* fall through */
54
+ }
55
+ return platform === "win32" ? "npm.cmd" : "npm";
56
+ }
57
+ /** Run npm with no shell (except on Windows, where `npm.cmd` needs one). */
58
+ export function runNpm(args, ctx = {}) {
59
+ const platform = ctx.platform ?? process.platform;
60
+ const cmd = npmBeside(ctx.execPath ?? process.execPath, platform);
61
+ const run = ctx.spawn ?? spawnSyncRunner;
62
+ return run(cmd, args, {
63
+ ...(ctx.env !== undefined ? { env: ctx.env } : {}),
64
+ // The ONE place a shell is used, and only where Node leaves no choice. The
65
+ // arguments passed through here are package specs and npm flags we build
66
+ // ourselves, never user text.
67
+ ...(platform === "win32" ? { shell: true } : {}),
68
+ timeoutMs: 15 * 60 * 1000,
69
+ });
70
+ }
71
+ /** `npm root -g`, or undefined when npm could not answer. */
72
+ export function npmGlobalRoot(ctx = {}) {
73
+ const r = runNpm(["root", "-g"], ctx);
74
+ if (r.status !== 0)
75
+ return undefined;
76
+ const out = r.stdout.trim().split("\n").at(-1)?.trim();
77
+ return out !== undefined && out !== "" ? out : undefined;
78
+ }
79
+ /** `npm config get <key>`, or undefined. Used to resolve the registry (SPEC §5 step 3). */
80
+ export function npmConfigGet(key, ctx = {}) {
81
+ const r = runNpm(["config", "get", key], ctx);
82
+ if (r.status !== 0)
83
+ return undefined;
84
+ const out = r.stdout.trim().split("\n").at(-1)?.trim();
85
+ return out !== undefined && out !== "" && out !== "undefined" && out !== "null" ? out : undefined;
86
+ }
87
+ /**
88
+ * Really writable? (SPEC §5 step 5.)
89
+ *
90
+ * `access(W_OK)` alone is not enough and the difference is the whole point of
91
+ * this check. It consults the mode bits, which say yes for root and say
92
+ * nothing about a read-only mount, an immutable flag, or an ACL — the usual
93
+ * shapes of "npm install -g needs sudo". So the directory is probed by
94
+ * CREATING something in it and removing it again, which is the same syscall
95
+ * npm is about to make.
96
+ *
97
+ * A missing directory walks up to its nearest existing parent: npm would create
98
+ * it, and whether it can is the parent's question.
99
+ */
100
+ export function isWritable(dir) {
101
+ let probe = dir;
102
+ for (;;) {
103
+ if (fs.existsSync(probe))
104
+ break;
105
+ const parent = path.dirname(probe);
106
+ if (parent === probe)
107
+ return false;
108
+ probe = parent;
109
+ }
110
+ try {
111
+ fs.accessSync(probe, fs.constants.W_OK);
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ try {
117
+ const tmp = fs.mkdtempSync(path.join(probe, ".update-probe-"));
118
+ fs.rmSync(tmp, { recursive: true, force: true });
119
+ return true;
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
125
+ /**
126
+ * Where a package manager would put the package, so {@link isWritable} has a
127
+ * directory to ask about.
128
+ *
129
+ * Only npm can be asked properly (`npm root -g`). For the others the install
130
+ * command's own target is a good enough proxy — and if it is wrong, the install
131
+ * fails with that manager's own message and the rollback runs, which is a worse
132
+ * experience than the sudo hint but not a worse outcome.
133
+ */
134
+ export function globalRootFor(manager, ctx = {}) {
135
+ if (manager === "npm")
136
+ return npmGlobalRoot(ctx);
137
+ return undefined;
138
+ }
139
+ /** The argv for a global install, per SPEC §6's last column. */
140
+ export function installArgs(manager, spec) {
141
+ switch (manager) {
142
+ case "npm":
143
+ // `--no-audit --no-fund` keeps the output to the point; `--global` is
144
+ // spelled out rather than `-g` so a log of what ran reads plainly.
145
+ return { cmd: "npm", args: ["install", "--global", "--no-audit", "--no-fund", spec] };
146
+ case "pnpm":
147
+ return { cmd: "pnpm", args: ["add", "-g", spec] };
148
+ case "bun":
149
+ return { cmd: "bun", args: ["add", "-g", spec] };
150
+ case "volta":
151
+ return { cmd: "volta", args: ["install", spec] };
152
+ }
153
+ }
154
+ /**
155
+ * Install `<name>@<version>` with the manager this install came from.
156
+ *
157
+ * npm goes through {@link runNpm}, so it is the npm beside this Node. The other
158
+ * three are looked up on PATH, which is the only place they are: pnpm, bun and
159
+ * volta are not installed beside node.
160
+ */
161
+ export function installPackage(o) {
162
+ const spec = `${o.packageName}@${o.version}`;
163
+ const { cmd, args } = installArgs(o.manager, spec);
164
+ if (o.manager === "npm") {
165
+ return runNpm(args, {
166
+ ...(o.spawn !== undefined ? { spawn: o.spawn } : {}),
167
+ ...(o.env !== undefined ? { env: o.env } : {}),
168
+ ...(o.execPath !== undefined ? { execPath: o.execPath } : {}),
169
+ ...(o.platform !== undefined ? { platform: o.platform } : {}),
170
+ });
171
+ }
172
+ const run = o.spawn ?? spawnSyncRunner;
173
+ return run(cmd, args, {
174
+ ...(o.env !== undefined ? { env: o.env } : {}),
175
+ ...((o.platform ?? process.platform) === "win32" ? { shell: true } : {}),
176
+ timeoutMs: 15 * 60 * 1000,
177
+ });
178
+ }
179
+ /**
180
+ * `npm cache add <name>@<version>` — fetch before stopping anything
181
+ * (SPEC §5 step 8).
182
+ *
183
+ * Both the target and the CURRENT version are cached, and caching the current
184
+ * one is the half that is easy to leave out and expensive to miss: it is what
185
+ * gives the rollback a package to reinstall when the reason the update failed
186
+ * was the network. Done while the old version is still serving, so a failure
187
+ * here costs nothing but a message.
188
+ *
189
+ * `npm cache add` is used whatever the manager is, because every one of these
190
+ * layouts has an npm available (it ships with Node) and the point is only to
191
+ * have the tarball on the machine. A failure is reported and not fatal — the
192
+ * install below may still succeed from the network.
193
+ */
194
+ export function cacheAdd(packageName, version, ctx = {}) {
195
+ return runNpm(["cache", "add", `${packageName}@${version}`], ctx);
196
+ }
197
+ /**
198
+ * Ask a CLI what version it is, by running it (`node <cli> --version`).
199
+ *
200
+ * This is how `update` proves that the install actually replaced the files
201
+ * before it hands over to the new CLI. The alternative — reading the
202
+ * `package.json` next to it — answers a different question: npm writes the
203
+ * manifest and the files in one operation, but a partially-updated global
204
+ * folder, a stale symlink, or a pnpm layout whose versioned realpath moved all
205
+ * show up here and not there.
206
+ */
207
+ export function cliVersion(cliPath, ctx = {}) {
208
+ const run = ctx.spawn ?? spawnSyncRunner;
209
+ const r = run(ctx.execPath ?? process.execPath, [cliPath, "--version"], {
210
+ ...(ctx.env !== undefined ? { env: ctx.env } : {}),
211
+ timeoutMs: 60_000,
212
+ });
213
+ if (r.status !== 0)
214
+ return undefined;
215
+ const line = r.stdout.trim().split("\n")[0]?.trim();
216
+ return line !== undefined && /^\d+\.\d+\.\d+/.test(line) ? line : undefined;
217
+ }
218
+ /** A throwaway directory, for callers that need one (the writability probe's sibling). */
219
+ export function tmpDir(prefix) {
220
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
221
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The npm registry, as `update` asks it (CLI contract SPEC §5 step 3).
3
+ *
4
+ * Two requests, both with `fetch` and a 10-second abort, and nothing else:
5
+ *
6
+ * GET <registry>/-/package/<name>/dist-tags which version is `latest`
7
+ * GET <registry>/<name>/<version> that version's `engines`
8
+ *
9
+ * Deliberately NOT `npm view`: spawning npm to read two small JSON documents
10
+ * costs a second of process start-up, needs npm to be on PATH at all, and puts
11
+ * the registry's answer through a program whose output format is not a
12
+ * contract. `fetch` is in Node 22 and the registry's REST surface has been
13
+ * stable for a decade.
14
+ *
15
+ * **Nothing here is called except by a person.** SPEC §3: only `update`,
16
+ * `update --check` and `doctor` contact the registry, and only when someone
17
+ * runs them. There is no background check and no check at startup, which is why
18
+ * this module is reachable from the CLI and from nowhere in the server.
19
+ *
20
+ * Generic: wave C4 lifts it into `reference/registry.ts`.
21
+ */
22
+ /** SPEC §5: 10 seconds, and it is the budget for each request rather than the pair. */
23
+ export declare const REGISTRY_TIMEOUT_MS = 10000;
24
+ export declare const DEFAULT_REGISTRY = "https://registry.npmjs.org";
25
+ export interface RegistryOptions {
26
+ packageName: string;
27
+ /** Base URL, no trailing slash required — {@link registryUrl} normalises it. */
28
+ registry: string;
29
+ fetch?: typeof globalThis.fetch;
30
+ timeoutMs?: number;
31
+ }
32
+ /**
33
+ * Which registry to ask: `npm_config_registry`, else `npm config get registry`,
34
+ * else the public one (SPEC §5 step 3).
35
+ *
36
+ * The first is what makes the fake-registry tests possible without touching a
37
+ * global npm config — an `npm_config_registry` in the environment is how npm
38
+ * itself takes the same setting, so a test and a real `.npmrc` arrive by the
39
+ * same door.
40
+ */
41
+ export declare function resolveRegistry(env: NodeJS.ProcessEnv, npmConfigGet?: (key: string) => string | undefined): string;
42
+ /**
43
+ * Join a path onto the registry base.
44
+ *
45
+ * A scoped name's `/` is written `%2f` (SPEC §5 step 3). That is required by
46
+ * the dist-tags route and accepted by the manifest route, so both are built the
47
+ * same way — one rule instead of two, and no chance of only one of them being
48
+ * right for `@scope/name`.
49
+ */
50
+ export declare function registryUrl(registry: string, ...parts: string[]): string;
51
+ /** Everything that went wrong talking to the registry, as one reportable error. */
52
+ export declare class RegistryError extends Error {
53
+ readonly url: string;
54
+ constructor(url: string, message: string);
55
+ }
56
+ /** `{ latest: "0.81.0", next: "0.82.0-rc.1" }`. */
57
+ export declare function fetchDistTags(o: RegistryOptions): Promise<Record<string, string>>;
58
+ export interface PackageManifest {
59
+ name?: string;
60
+ version?: string;
61
+ engines?: {
62
+ node?: string;
63
+ };
64
+ }
65
+ /** One version's manifest — read for `engines` (SPEC §5 step 3). */
66
+ export declare function fetchManifest(o: RegistryOptions & {
67
+ version: string;
68
+ }): Promise<PackageManifest>;
69
+ export interface ResolveTargetInput extends RegistryOptions {
70
+ /** `--to <version|tag>`, or undefined for `latest`. */
71
+ to?: string;
72
+ }
73
+ export interface ResolvedTarget {
74
+ version: string;
75
+ /** The dist-tag asked for, or null when `--to` named a version (schema field). */
76
+ tag: string | null;
77
+ engines: string | null;
78
+ }
79
+ /**
80
+ * Turn `--to` (or its absence) into one concrete version, plus its `engines`.
81
+ *
82
+ * `--to` is a version or a dist-tag and the two are told apart by shape, not by
83
+ * asking: anything that parses as a version is a version, everything else is a
84
+ * tag. That ordering matters because a registry will happily serve a manifest
85
+ * for a tag name as well, and resolving `--to next` as a literal version would
86
+ * silently install whatever `next` pointed at while REPORTING the target as
87
+ * "next".
88
+ */
89
+ export declare function resolveTarget(o: ResolveTargetInput): Promise<ResolvedTarget>;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The npm registry, as `update` asks it (CLI contract SPEC §5 step 3).
3
+ *
4
+ * Two requests, both with `fetch` and a 10-second abort, and nothing else:
5
+ *
6
+ * GET <registry>/-/package/<name>/dist-tags which version is `latest`
7
+ * GET <registry>/<name>/<version> that version's `engines`
8
+ *
9
+ * Deliberately NOT `npm view`: spawning npm to read two small JSON documents
10
+ * costs a second of process start-up, needs npm to be on PATH at all, and puts
11
+ * the registry's answer through a program whose output format is not a
12
+ * contract. `fetch` is in Node 22 and the registry's REST surface has been
13
+ * stable for a decade.
14
+ *
15
+ * **Nothing here is called except by a person.** SPEC §3: only `update`,
16
+ * `update --check` and `doctor` contact the registry, and only when someone
17
+ * runs them. There is no background check and no check at startup, which is why
18
+ * this module is reachable from the CLI and from nowhere in the server.
19
+ *
20
+ * Generic: wave C4 lifts it into `reference/registry.ts`.
21
+ */
22
+ /** SPEC §5: 10 seconds, and it is the budget for each request rather than the pair. */
23
+ export const REGISTRY_TIMEOUT_MS = 10_000;
24
+ export const DEFAULT_REGISTRY = "https://registry.npmjs.org";
25
+ /**
26
+ * Which registry to ask: `npm_config_registry`, else `npm config get registry`,
27
+ * else the public one (SPEC §5 step 3).
28
+ *
29
+ * The first is what makes the fake-registry tests possible without touching a
30
+ * global npm config — an `npm_config_registry` in the environment is how npm
31
+ * itself takes the same setting, so a test and a real `.npmrc` arrive by the
32
+ * same door.
33
+ */
34
+ export function resolveRegistry(env, npmConfigGet) {
35
+ const fromEnv = env.npm_config_registry ?? env.NPM_CONFIG_REGISTRY;
36
+ if (fromEnv !== undefined && fromEnv.trim() !== "")
37
+ return fromEnv.trim();
38
+ const fromNpm = npmConfigGet?.("registry");
39
+ if (fromNpm !== undefined && fromNpm.trim() !== "" && fromNpm.trim() !== "undefined") {
40
+ return fromNpm.trim();
41
+ }
42
+ return DEFAULT_REGISTRY;
43
+ }
44
+ /**
45
+ * Join a path onto the registry base.
46
+ *
47
+ * A scoped name's `/` is written `%2f` (SPEC §5 step 3). That is required by
48
+ * the dist-tags route and accepted by the manifest route, so both are built the
49
+ * same way — one rule instead of two, and no chance of only one of them being
50
+ * right for `@scope/name`.
51
+ */
52
+ export function registryUrl(registry, ...parts) {
53
+ const base = registry.replace(/\/+$/, "");
54
+ return `${base}/${parts.map((p) => p.replace(/\//g, "%2f")).join("/")}`;
55
+ }
56
+ /** Everything that went wrong talking to the registry, as one reportable error. */
57
+ export class RegistryError extends Error {
58
+ url;
59
+ constructor(url, message) {
60
+ super(message);
61
+ this.url = url;
62
+ }
63
+ }
64
+ async function getJson(url, o) {
65
+ const doFetch = o.fetch ?? globalThis.fetch;
66
+ let res;
67
+ try {
68
+ res = await doFetch(url, {
69
+ signal: AbortSignal.timeout(o.timeoutMs ?? REGISTRY_TIMEOUT_MS),
70
+ headers: { accept: "application/json" },
71
+ });
72
+ }
73
+ catch (err) {
74
+ const why = err.name === "TimeoutError" || err.name === "AbortError"
75
+ ? `no answer within ${(o.timeoutMs ?? REGISTRY_TIMEOUT_MS) / 1000} seconds`
76
+ : (err.message ?? String(err));
77
+ throw new RegistryError(url, `could not reach the registry: ${why}`);
78
+ }
79
+ if (res.status === 404)
80
+ throw new RegistryError(url, "the registry has no such package or version");
81
+ if (!res.ok)
82
+ throw new RegistryError(url, `the registry answered HTTP ${res.status}`);
83
+ try {
84
+ return (await res.json());
85
+ }
86
+ catch {
87
+ throw new RegistryError(url, "the registry's answer was not JSON");
88
+ }
89
+ }
90
+ /** `{ latest: "0.81.0", next: "0.82.0-rc.1" }`. */
91
+ export async function fetchDistTags(o) {
92
+ const url = registryUrl(o.registry, "-", "package", o.packageName, "dist-tags");
93
+ const body = await getJson(url, o);
94
+ const tags = {};
95
+ for (const [k, v] of Object.entries(body))
96
+ if (typeof v === "string")
97
+ tags[k] = v;
98
+ if (Object.keys(tags).length === 0)
99
+ throw new RegistryError(url, "the registry listed no dist-tags");
100
+ return tags;
101
+ }
102
+ /** One version's manifest — read for `engines` (SPEC §5 step 3). */
103
+ export async function fetchManifest(o) {
104
+ const url = registryUrl(o.registry, o.packageName, o.version);
105
+ return getJson(url, o);
106
+ }
107
+ /**
108
+ * Turn `--to` (or its absence) into one concrete version, plus its `engines`.
109
+ *
110
+ * `--to` is a version or a dist-tag and the two are told apart by shape, not by
111
+ * asking: anything that parses as a version is a version, everything else is a
112
+ * tag. That ordering matters because a registry will happily serve a manifest
113
+ * for a tag name as well, and resolving `--to next` as a literal version would
114
+ * silently install whatever `next` pointed at while REPORTING the target as
115
+ * "next".
116
+ */
117
+ export async function resolveTarget(o) {
118
+ const { to } = o;
119
+ let version;
120
+ let tag;
121
+ if (to === undefined) {
122
+ const tags = await fetchDistTags(o);
123
+ const latest = tags.latest;
124
+ if (latest === undefined) {
125
+ throw new RegistryError(registryUrl(o.registry, "-", "package", o.packageName, "dist-tags"), "the registry has no `latest` dist-tag for this package");
126
+ }
127
+ version = latest;
128
+ tag = "latest";
129
+ }
130
+ else if (/^v?\d+\.\d+\.\d+/.test(to.trim())) {
131
+ version = to.trim().replace(/^v/, "");
132
+ tag = null;
133
+ }
134
+ else {
135
+ const tags = await fetchDistTags(o);
136
+ const resolved = tags[to];
137
+ if (resolved === undefined) {
138
+ throw new RegistryError(registryUrl(o.registry, "-", "package", o.packageName, "dist-tags"), `no dist-tag \`${to}\` (the registry has: ${Object.keys(tags).sort().join(", ")})`);
139
+ }
140
+ version = resolved;
141
+ tag = to;
142
+ }
143
+ const manifest = await fetchManifest({ ...o, version });
144
+ return { version, tag, engines: manifest.engines?.node ?? null };
145
+ }
@@ -0,0 +1,68 @@
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
+ export interface Version {
20
+ major: number;
21
+ minor: number;
22
+ patch: number;
23
+ /** The dot-separated identifiers after `-`, empty for a release. */
24
+ prerelease: (string | number)[];
25
+ }
26
+ /** `1.2.3-rc.1` → a {@link Version}, or undefined if it is not a version. */
27
+ export declare function parseVersion(raw: string): Version | undefined;
28
+ /** Is this a prerelease? SPEC §5: prereleases count only when `--to` names them. */
29
+ export declare function isPrerelease(raw: string): boolean;
30
+ /**
31
+ * -1, 0 or 1, by the semver rules — including the prerelease ones, which are
32
+ * the only interesting part: `1.0.0-rc.1` is LOWER than `1.0.0`, numeric
33
+ * identifiers compare numerically and sort below alphanumeric ones, and a
34
+ * longer run of identifiers wins when every shared one is equal.
35
+ *
36
+ * Build metadata is ignored, as semver says it must be.
37
+ */
38
+ export declare function compareVersions(a: string, b: string): number;
39
+ /** Is `a` strictly newer than `b`? */
40
+ export declare const isNewer: (a: string, b: string) => boolean;
41
+ /** The highest of a list, ignoring anything unparseable. Undefined if none parse. */
42
+ export declare function maxVersion(versions: readonly string[]): string | undefined;
43
+ /**
44
+ * Does `version` satisfy `range`? Undefined means "this file cannot tell".
45
+ *
46
+ * Supports the forms an `engines.node` actually uses: `>=22`, `>=22.0.0`,
47
+ * `^22.1.0`, `~22.1`, `22`, `22.x`, `>=20 <23`, and `||` between any of those.
48
+ * `*` and an empty range accept everything.
49
+ */
50
+ export declare function satisfies(version: string, range: string): boolean | undefined;
51
+ export interface EnginesVerdict {
52
+ /** True pass, false genuine fail, undefined when the range could not be read. */
53
+ ok: boolean | undefined;
54
+ /** The range as published, for the message. */
55
+ range: string | undefined;
56
+ }
57
+ /**
58
+ * Will the target version run on this Node? (SPEC §5 step 4.)
59
+ *
60
+ * The three-way answer is the point. A target with no `engines.node` at all, or
61
+ * one whose range this file cannot parse, must NOT block an update: the cost of
62
+ * a false refusal is a tool that cannot update itself and says something the
63
+ * user cannot act on, while the cost of letting an incompatible Node through is
64
+ * the failure `engines` would have produced anyway, with a rollback behind it.
65
+ */
66
+ export declare function satisfiesEngines(nodeVersion: string, engines: {
67
+ node?: string;
68
+ } | undefined): EnginesVerdict;