@edspencer/paddock 0.82.0 → 0.82.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/server/dist/cli/args.js +2 -2
- package/packages/server/dist/cli/busy-command.js +1 -1
- package/packages/server/dist/cli/busy.js +10 -12
- package/packages/server/dist/cli/exit-codes.d.ts +21 -45
- package/packages/server/dist/cli/exit-codes.js +42 -45
- package/packages/server/dist/cli/output.js +5 -6
- package/packages/server/dist/cli/paddock.js +9 -9
- package/packages/server/dist/cli/service/index.d.ts +2 -2
- package/packages/server/dist/cli/service/index.js +30 -24
- package/packages/server/dist/cli/service/launchd.d.ts +31 -84
- package/packages/server/dist/cli/service/launchd.js +104 -304
- package/packages/server/dist/cli/service/logs.d.ts +9 -28
- package/packages/server/dist/cli/service/logs.js +33 -75
- package/packages/server/dist/cli/service/manager.d.ts +93 -0
- package/packages/server/dist/cli/service/manager.js +21 -0
- package/packages/server/dist/cli/service/path-env.d.ts +7 -18
- package/packages/server/dist/cli/service/path-env.js +30 -81
- package/packages/server/dist/cli/service/spec.d.ts +4 -31
- package/packages/server/dist/cli/service/spec.js +9 -0
- package/packages/server/dist/cli/service/systemd.d.ts +27 -101
- package/packages/server/dist/cli/service/systemd.js +106 -262
- package/packages/server/dist/cli/update/health.d.ts +48 -0
- package/packages/server/dist/cli/update/health.js +64 -0
- package/packages/server/dist/cli/update/install-method.d.ts +29 -76
- package/packages/server/dist/cli/update/install-method.js +103 -178
- package/packages/server/dist/cli/update/lock.d.ts +7 -12
- package/packages/server/dist/cli/update/lock.js +28 -62
- package/packages/server/dist/cli/update/package-manager.d.ts +39 -74
- package/packages/server/dist/cli/update/package-manager.js +95 -160
- package/packages/server/dist/cli/update/registry.d.ts +17 -54
- package/packages/server/dist/cli/update/registry.js +46 -89
- package/packages/server/dist/cli/update/semver.d.ts +18 -41
- package/packages/server/dist/cli/update/semver.js +74 -124
- package/packages/server/dist/cli/update/update-check.d.ts +15 -26
- package/packages/server/dist/cli/update/update-check.js +30 -60
- package/packages/server/dist/cli/update/update.d.ts +57 -91
- package/packages/server/dist/cli/update/update.js +230 -431
- package/packages/server/dist/cli/update-command.js +3 -5
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{DeleteProjectDialog-CphdGB3R.js → DeleteProjectDialog-CgHpQctt.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-CG6cf-0i.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-B_kcF4sm.js → DiscoverView-BKqI0I1E.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-CvUK5DKW.js → InstanceConfigPage-DL3raCK1.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DvZ-j_Ao.js → MarkdownRenderer-DcjEuf3s.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DayHvUH2.js → ProjectRedirect-DerkA-nE.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-CCw83gUi.js → ProjectView-NwfC3_w7.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-B3Klg2H4.js → ProjectsGrid-BeORa0UH.js} +1 -1
- package/packages/web/dist/assets/{RootHome-BxcDvpgI.js → RootHome-BYJs8qJ6.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-7UlhFBSG.js → Surfaces-w-kEaAYU.js} +1 -1
- package/packages/web/dist/assets/{arc-DNkh5o5Q.js → arc-Bv56hasM.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BrgN3oLF.js → architectureDiagram-3BPJPVTR-BHEknx9i.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-D05XOJSk.js → blockDiagram-GPEHLZMM-DtxTMrPX.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-CKaMBhMP.js → c4Diagram-AAUBKEIU-DRpEYOlM.js} +1 -1
- package/packages/web/dist/assets/channel-CdIiAqY2.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-8hgadCQc.js → chunk-2J33WTMH-S4s7GCg7.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-CBfEC-pu.js → chunk-4BX2VUAB-BkWVPLf1.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-DV1eMgHk.js → chunk-55IACEB6-4-FQp01F.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-Cfgl5fxY.js → chunk-727SXJPM-BeyDWdIb.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BXhqD295.js → chunk-AQP2D5EJ-ZSxKy745.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-TFGOPtek.js → chunk-FMBD7UC4-Bq8uV9j-.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-CHFM665R.js → chunk-ND2GUHAM-BOFsRyHk.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-CqEf6MMT.js → chunk-QZHKN3VN-uZ6oNc4m.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DCCUHba0.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DCCUHba0.js +2 -0
- package/packages/web/dist/assets/{core-DmEaKxBb.js → core-CYuJAM2U.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BnGI6jkr.js → cose-bilkent-S5V4N54A-DIzkVSFb.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-CKoPx61E.js → dagre-BM42HDAG-CHR6jJBv.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-mMmtlBl1.js → diagram-2AECGRRQ-BAFXCH8Q.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-Cdz9Mtnt.js → diagram-5GNKFQAL-BJ5t5gbz.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-DQxM3wYj.js → diagram-KO2AKTUF-D9Xt0IVg.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-BWrinPsq.js → diagram-LMA3HP47-Bq38u6sy.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-U6MYUve2.js → diagram-OG6HWLK6-CMQVqkpi.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-DGjRXuTP.js → documentTitle-M1lrfXRU.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-xqMCsXxY.js → erDiagram-TEJ5UH35-C63M1g9b.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-PMfy_czF.js → flowDiagram-I6XJVG4X-D3j8ACN7.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-whhYYE4N.js → ganttDiagram-6RSMTGT7-B1Fd1A2Y.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-C51JW4tf.js → gitGraphDiagram-PVQCEYII-CEhZhDdR.js} +1 -1
- package/packages/web/dist/assets/{index-CHOtvlnO.js → index-1E9yHciS.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-SH8cr905.js → infoDiagram-5YYISTIA-Ctquaew6.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BjqaoU3U.js → ishikawaDiagram-YF4QCWOH-DV4GvA1R.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CBmdEVrd.js → journeyDiagram-JHISSGLW-C_Cy9waA.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-U8njjMLZ.js → kanban-definition-UN3LZRKU-CGr9ctEf.js} +1 -1
- package/packages/web/dist/assets/{linear-DF66GPs1.js → linear-D3U8NOkt.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-CouJz7Ph.js → mermaid.core-BiArdWKV.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BYIf0oFt.js → mindmap-definition-RKZ34NQL-CZIpzj8Z.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-Cm8rgg09.js → pieDiagram-4H26LBE5-1MaeS3rt.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-C3Il5kOH.js → quadrantDiagram-W4KKPZXB-B5pgQ54e.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-D3PE2nTe.js → requirementDiagram-4Y6WPE33-BNnxrpGr.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-DdmvBM18.js → sankeyDiagram-5OEKKPKP-DsxVH-hM.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-B8M_GAIq.js → sequenceDiagram-3UESZ5HK-Ke9LcFZI.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DZkXnK1_.js → stateDiagram-AJRCARHV-UJzmflND.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DgUc3HkP.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-IHXq7rqi.js → timeline-definition-PNZ67QCA-BRjNALVI.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Dga7tLtT.js → vennDiagram-CIIHVFJN-ClMAspxz.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-CmcwnEJ-.js → wardley-L42UT6IY-UPv4JEmz.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-zHuSLrD4.js → wardleyDiagram-YWT4CUSO-BgYJbdEx.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-D93MU_ic.js → xychartDiagram-2RQKCTM6-Bfk5HjGx.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/server/dist/cli/service/backend.d.ts +0 -115
- package/packages/server/dist/cli/service/backend.js +0 -19
- package/packages/web/dist/assets/DiscoverPage-C4OPLNO8.js +0 -2
- package/packages/web/dist/assets/channel-JrEedxB8.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CHJiQp-W.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CHJiQp-W.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CjCdT9Ls.js +0 -2
|
@@ -1,86 +1,78 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* 13. and on any failure from step 10 on, roll back and report 6 or 7.
|
|
35
|
-
*
|
|
36
|
-
* Node built-ins only. Nothing here imports the server.
|
|
37
|
-
*/
|
|
38
|
-
import path from "node:path";
|
|
39
|
-
import fsDefault from "node:fs";
|
|
40
|
-
import { createInterface } from "node:readline";
|
|
41
|
-
import { EXIT } from "../exit-codes.js";
|
|
42
|
-
import { detectInstall, describeInstall, updateCommand, } from "./install-method.js";
|
|
43
|
-
import { acquireLock, LockHeldError } from "./lock.js";
|
|
44
|
-
import { RegistryError, resolveRegistry, resolveTarget, } from "./registry.js";
|
|
45
|
-
import { cacheAdd, cliVersion, globalRootFor, installArgs, installPackage, isWritable, npmConfigGet, npmGlobalRoot, } from "./package-manager.js";
|
|
46
|
-
import { compareVersions, isPrerelease, satisfiesEngines } from "./semver.js";
|
|
47
|
-
import { updateNotice, writeUpdateCheck, } from "./update-check.js";
|
|
1
|
+
// From edspencer/cli-contract reference/update.ts @ 0.2; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
|
|
2
|
+
// `<tool> update`: SPEC §5, step by step, for every tool. Node built-ins only; nothing here imports a server.
|
|
3
|
+
//
|
|
4
|
+
// It's a sequence over injected hooks. A tool supplies what's its own (`UpdateProduct`): its names, its busy check,
|
|
5
|
+
// and its service (inspect, stop, refresh, tail). Everything that touches the machine comes in through
|
|
6
|
+
// `UpdateEnvironment`, so every branch, including the rollbacks to 6 and 7, can be tested without one.
|
|
7
|
+
//
|
|
8
|
+
// 1. the lock, so two updates can't interleave;
|
|
9
|
+
// 2. the install method (§6), so one that can't work says so first (but after step 4, since its notice names the
|
|
10
|
+
// target);
|
|
11
|
+
// 3. the registry, for the target and its `engines`;
|
|
12
|
+
// 4. the comparison, and `--check` stops here;
|
|
13
|
+
// 5. the writability check, which turns "EACCES half-way through" into a `sudo` line while everything still runs;
|
|
14
|
+
// 6. the busy check, before anything stops;
|
|
15
|
+
// 7. the question, which `--yes` answers and no TTY forbids;
|
|
16
|
+
// 8. `npm cache add` for both versions, so a network failure happens while the old version still serves;
|
|
17
|
+
// 9. stop, before installing: npm moves the old package aside before unpacking, and a running server would then
|
|
18
|
+
// load files that are gone;
|
|
19
|
+
// 10. install, with the manager this install came from;
|
|
20
|
+
// 11. hand over to the new CLI (`service install --refresh`), which writes the unit in its own format;
|
|
21
|
+
// 12. wait until the target version answers, not merely until something does;
|
|
22
|
+
// 13. and from step 10 on, roll back on any failure, and exit 6 or 7.
|
|
23
|
+
import fsDefault from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { createInterface } from 'node:readline';
|
|
26
|
+
import { EXIT } from '../exit-codes.js';
|
|
27
|
+
import { fetchHealth, waitForVersion as pollVersion } from './health.js';
|
|
28
|
+
import { describeInstall, detectInstall, updateCommand } from './install-method.js';
|
|
29
|
+
import { acquireLock, LockHeldError } from './lock.js';
|
|
30
|
+
import { cacheAdd, cliVersion, globalRootFor, installArgs, installPackage, isWritable, npmConfigGet, npmGlobalRoot, } from './package-manager.js';
|
|
31
|
+
import { RegistryError, resolveRegistry, resolveTarget } from './registry.js';
|
|
32
|
+
import { compareVersions, isPrerelease, satisfiesEngines } from './semver.js';
|
|
33
|
+
import { updateNotice, writeUpdateCheck } from './update-check.js';
|
|
48
34
|
/** SPEC §5 step 12: 30 seconds unless the tool sets its own. */
|
|
49
35
|
export const HEALTH_TIMEOUT_MS = 30_000;
|
|
36
|
+
/** How long to wait for the old version to stop answering after the stop (step 9). */
|
|
37
|
+
export const STOP_TIMEOUT_MS = 15_000;
|
|
50
38
|
/** How many log lines a failure prints (SPEC §5 step 13). */
|
|
51
39
|
export const LOG_LINES_ON_FAILURE = 20;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
const indent = (text) => text
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map((l) => (l === '' ? l : ` ${l}`))
|
|
43
|
+
.join('\n');
|
|
44
|
+
const said = (r) => indent(r.stderr.trim() || r.stdout.trim());
|
|
45
|
+
/** Runs `update` and returns the exit code; the caller sets `process.exitCode`. */
|
|
56
46
|
export async function runUpdate(product, opts, envp) {
|
|
57
47
|
const out = envp.out;
|
|
58
48
|
const env = envp.env ?? process.env;
|
|
59
49
|
const fs = envp.fs ?? fsDefault;
|
|
50
|
+
const npm = {
|
|
51
|
+
env,
|
|
52
|
+
...(envp.spawn !== undefined ? { spawn: envp.spawn } : {}),
|
|
53
|
+
...(envp.execPath !== undefined ? { execPath: envp.execPath } : {}),
|
|
54
|
+
...(envp.platform !== undefined ? { platform: envp.platform } : {}),
|
|
55
|
+
};
|
|
60
56
|
let emitted = false;
|
|
61
|
-
/** `out.result` promises exactly one document, so guard it here rather than at ten call sites. */
|
|
62
57
|
const emit = (doc) => {
|
|
63
58
|
if (emitted)
|
|
64
59
|
return;
|
|
65
60
|
emitted = true;
|
|
66
61
|
out.result(doc);
|
|
67
62
|
};
|
|
68
|
-
|
|
63
|
+
const healthTimeoutMs = product.healthTimeoutMs ?? HEALTH_TIMEOUT_MS;
|
|
64
|
+
const waitFor = (url, version, timeoutMs = healthTimeoutMs) => product.waitForVersion
|
|
65
|
+
? product.waitForVersion({ url, version, timeoutMs })
|
|
66
|
+
: pollVersion({ url, version, timeoutMs, ...(envp.fetch !== undefined ? { fetch: envp.fetch } : {}) });
|
|
67
|
+
// ---- 1. the lock
|
|
69
68
|
let lock;
|
|
70
69
|
try {
|
|
71
|
-
lock = acquireLock({
|
|
72
|
-
home: product.home,
|
|
73
|
-
command: `${product.tool} update${opts.to !== undefined ? ` --to ${opts.to}` : ""}`,
|
|
74
|
-
});
|
|
70
|
+
lock = acquireLock({ home: product.home, command: `${product.tool} update${opts.to !== undefined ? ` --to ${opts.to}` : ''}` });
|
|
75
71
|
}
|
|
76
72
|
catch (err) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
82
|
-
emit({ outcome: "failed", message });
|
|
83
|
-
return out.error(`could not take the update lock: ${message}`);
|
|
73
|
+
const message = err instanceof LockHeldError ? err.message : `couldn't take the update lock: ${err.message}`;
|
|
74
|
+
emit({ outcome: 'failed', message });
|
|
75
|
+
return out.error(message);
|
|
84
76
|
}
|
|
85
77
|
try {
|
|
86
78
|
return await sequence();
|
|
@@ -89,481 +81,310 @@ export async function runUpdate(product, opts, envp) {
|
|
|
89
81
|
lock.release();
|
|
90
82
|
}
|
|
91
83
|
async function sequence() {
|
|
92
|
-
|
|
93
|
-
...(envp.spawn !== undefined ? { spawn: envp.spawn } : {}),
|
|
94
|
-
...(envp.execPath !== undefined ? { execPath: envp.execPath } : {}),
|
|
95
|
-
...(envp.platform !== undefined ? { platform: envp.platform } : {}),
|
|
96
|
-
env,
|
|
97
|
-
};
|
|
98
|
-
// ---- 2. how was it installed? (SPEC §6) ------------------------------
|
|
99
|
-
//
|
|
100
|
-
// Detected now, reported now, but NOT acted on yet: the notice an install
|
|
101
|
-
// that cannot update itself needs (`docker compose pull … (image …:0.81.0)`)
|
|
102
|
-
// names the target version, so the exit-5 refusal waits until step 4 has
|
|
103
|
-
// one. An install that is already up to date exits 0 instead, which is the
|
|
104
|
-
// right answer in a container too.
|
|
84
|
+
// ---- 2. how was it installed? Acted on after step 4, which has the target its notice names.
|
|
105
85
|
const info = detectInstall({
|
|
106
86
|
packageName: product.packageName,
|
|
107
87
|
envPrefix: product.envPrefix,
|
|
108
88
|
entry: product.entry,
|
|
109
89
|
env,
|
|
110
|
-
npmGlobalRoot: () => npmGlobalRoot(
|
|
90
|
+
npmGlobalRoot: () => npmGlobalRoot(npm),
|
|
111
91
|
fs,
|
|
112
92
|
});
|
|
113
|
-
// ---- 3. the registry
|
|
114
|
-
const registry = resolveRegistry(env, (key) => npmConfigGet(key, spawnCtx));
|
|
93
|
+
// ---- 3. the registry
|
|
115
94
|
let target;
|
|
116
95
|
try {
|
|
117
96
|
target = await resolveTarget({
|
|
118
97
|
packageName: product.packageName,
|
|
119
|
-
registry,
|
|
98
|
+
registry: resolveRegistry(env, (key) => npmConfigGet(key, npm)),
|
|
120
99
|
...(opts.to !== undefined ? { to: opts.to } : {}),
|
|
121
100
|
...(envp.fetch !== undefined ? { fetch: envp.fetch } : {}),
|
|
122
101
|
});
|
|
123
102
|
}
|
|
124
103
|
catch (err) {
|
|
125
|
-
const message = err instanceof RegistryError ? `${err.message}\n
|
|
126
|
-
emit({ outcome:
|
|
104
|
+
const message = err instanceof RegistryError ? `${err.message}\n ${err.url}` : String(err);
|
|
105
|
+
emit({ outcome: 'failed', message });
|
|
127
106
|
return out.error(message);
|
|
128
107
|
}
|
|
129
|
-
// ---- 4. compare
|
|
108
|
+
// ---- 4. compare
|
|
130
109
|
const cmp = compareVersions(target.version, product.currentVersion);
|
|
110
|
+
// A prerelease counts only when `--to` names it. `latest` pointing at one is a mis-tagged release.
|
|
111
|
+
const misTagged = opts.to === undefined && isPrerelease(target.version);
|
|
131
112
|
const nodeVersion = envp.nodeVersion ?? process.versions.node;
|
|
132
|
-
const engines = satisfiesEngines(nodeVersion, {
|
|
133
|
-
|
|
113
|
+
const engines = satisfiesEngines(nodeVersion, target.engines !== null ? { node: target.engines } : undefined);
|
|
114
|
+
const command = updateCommand(info, {
|
|
115
|
+
packageName: product.packageName,
|
|
116
|
+
target: target.version,
|
|
117
|
+
...(product.image !== undefined ? { image: product.image } : {}),
|
|
134
118
|
});
|
|
135
|
-
const command = commandFor(product, info, target.version);
|
|
136
119
|
const check = {
|
|
137
120
|
name: product.packageName,
|
|
138
121
|
current: product.currentVersion,
|
|
139
122
|
target: target.version,
|
|
140
123
|
tag: target.tag,
|
|
141
|
-
updateAvailable: cmp > 0,
|
|
124
|
+
updateAvailable: cmp > 0 && !misTagged,
|
|
142
125
|
installMethod: info.method,
|
|
143
126
|
canUpdate: info.canUpdate,
|
|
144
127
|
command: info.canUpdate ? `${product.tool} update` : command.command,
|
|
145
128
|
engines: target.engines,
|
|
146
|
-
//
|
|
147
|
-
// SPEC's field is a boolean — report the permissive answer, which is also
|
|
148
|
-
// the one the code acts on. The range itself is in `engines` either way.
|
|
129
|
+
// A range that couldn't be read is a pass (semver.ts), and the field is a boolean.
|
|
149
130
|
nodeOk: engines.ok !== false,
|
|
150
131
|
changelog: product.changelogUrl(target.version),
|
|
151
132
|
checkedAt: new Date().toISOString(),
|
|
152
133
|
};
|
|
153
|
-
// Written whatever
|
|
154
|
-
// let `service status` repeat the last answer (SPEC §3) — including "you
|
|
155
|
-
// are up to date", which is the answer that stops it nagging.
|
|
134
|
+
// Written whatever happens next: "you're up to date" is the answer that stops `status` repeating an old notice.
|
|
156
135
|
writeUpdateCheck(product.home, check);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
136
|
+
const notice = (cmd, note) => updateNotice({
|
|
137
|
+
name: product.name,
|
|
138
|
+
current: product.currentVersion,
|
|
139
|
+
target: target.version,
|
|
140
|
+
command: cmd,
|
|
141
|
+
...(note !== undefined ? { note } : {}),
|
|
142
|
+
changelog: check.changelog,
|
|
143
|
+
});
|
|
144
|
+
if (misTagged) {
|
|
145
|
+
const message = `The registry's latest is a prerelease (${target.version}), so it isn't installed.\n To go there: ${product.tool} update --to ${target.version}`;
|
|
146
|
+
emit({ ...check, outcome: 'ahead', message });
|
|
164
147
|
out.say(message);
|
|
165
|
-
return EXIT.
|
|
148
|
+
return EXIT.OK;
|
|
166
149
|
}
|
|
167
150
|
if (cmp === 0) {
|
|
168
151
|
const message = `${product.name} ${product.currentVersion} is up to date.`;
|
|
169
|
-
emit({ ...check, outcome:
|
|
152
|
+
emit({ ...check, outcome: 'up-to-date', message });
|
|
170
153
|
out.say(message);
|
|
171
|
-
return EXIT.
|
|
154
|
+
return EXIT.OK;
|
|
172
155
|
}
|
|
173
|
-
// A lower version needs `--to` (SPEC §5 step 4). Without it this is the
|
|
174
|
-
// ordinary "you are on a prerelease or a hand-built version" case, and
|
|
175
|
-
// there is nothing to do.
|
|
176
156
|
if (cmp < 0 && opts.to === undefined) {
|
|
177
|
-
const message = `${product.name} ${product.currentVersion} is newer than the registry's ` +
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
emit({ ...check, outcome: "ahead", message });
|
|
157
|
+
const message = `${product.name} ${product.currentVersion} is newer than the registry's ${target.tag ?? 'latest'} (${target.version}). Nothing to do.\n` +
|
|
158
|
+
` To go back to it: ${product.tool} update --to ${target.version}`;
|
|
159
|
+
emit({ ...check, outcome: 'ahead', message });
|
|
181
160
|
out.say(message);
|
|
182
|
-
return EXIT.
|
|
161
|
+
return EXIT.OK;
|
|
183
162
|
}
|
|
184
|
-
const direction = cmp < 0 ? "Downgrade" : "Update";
|
|
185
|
-
const arrow = `${product.currentVersion} → ${target.version}`;
|
|
186
163
|
if (opts.check === true) {
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
target: target.version,
|
|
194
|
-
command: check.command ?? `${product.tool} update`,
|
|
195
|
-
...(command.note !== undefined && !info.canUpdate ? { note: command.note } : {}),
|
|
196
|
-
changelog: check.changelog,
|
|
197
|
-
}));
|
|
198
|
-
if (engines.ok === false) {
|
|
199
|
-
out.warn(` Note: ${target.version} needs Node ${target.engines ?? "?"}; this is Node ${nodeVersion}.`);
|
|
200
|
-
}
|
|
201
|
-
return EXIT.ok;
|
|
164
|
+
// Exit 0 whether or not there's an update (SPEC §2); scripts read `updateAvailable`. The notice goes to stderr.
|
|
165
|
+
emit({ ...check, outcome: 'available' });
|
|
166
|
+
out.warn(notice(check.command ?? `${product.tool} update`, info.canUpdate ? undefined : command.note));
|
|
167
|
+
if (engines.ok === false)
|
|
168
|
+
out.warn(` Note: ${target.version} needs Node ${target.engines}; this is Node ${nodeVersion}.`);
|
|
169
|
+
return EXIT.OK;
|
|
202
170
|
}
|
|
203
|
-
// Now that there IS a target, an install that cannot replace itself prints
|
|
204
|
-
// the right command and exits 5 (SPEC §5 step 2, §6).
|
|
205
171
|
if (!info.canUpdate) {
|
|
206
|
-
out.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
command: command.command,
|
|
211
|
-
...(command.note !== undefined ? { note: command.note } : {}),
|
|
212
|
-
changelog: check.changelog,
|
|
213
|
-
}));
|
|
214
|
-
const message = `this install cannot update itself — it is ${describeInstall(info, product.packageName)}.`;
|
|
215
|
-
emit({ ...check, outcome: "cannot-self-update", message });
|
|
216
|
-
return out.error(message, EXIT.cannotSelfUpdate);
|
|
172
|
+
out.warn(notice(command.command, command.note));
|
|
173
|
+
const message = `this install can't update itself: it's ${describeInstall(info, product.packageName)}.`;
|
|
174
|
+
emit({ ...check, outcome: 'cannot-self-update', message });
|
|
175
|
+
return out.error(message, EXIT.CANNOT_UPDATE);
|
|
217
176
|
}
|
|
218
|
-
const manager = info.manager ??
|
|
177
|
+
const manager = info.manager ?? 'npm';
|
|
219
178
|
if (engines.ok === false) {
|
|
220
|
-
const message = `${target.version} needs Node ${target.engines
|
|
221
|
-
|
|
222
|
-
emit({ ...check, outcome: "node-too-old", message });
|
|
179
|
+
const message = `${target.version} needs Node ${target.engines}, and this is Node ${nodeVersion}. Upgrade Node, then run \`${product.tool} update\` again.`;
|
|
180
|
+
emit({ ...check, outcome: 'node-too-old', message });
|
|
223
181
|
return out.error(message);
|
|
224
182
|
}
|
|
225
|
-
// ---- 5. can we write there?
|
|
226
|
-
const globalRoot = globalRootFor(manager,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
});
|
|
234
|
-
out.say(updateNotice({
|
|
235
|
-
name: product.name,
|
|
236
|
-
current: product.currentVersion,
|
|
237
|
-
target: target.version,
|
|
238
|
-
command: sudo.command,
|
|
239
|
-
note: sudo.note ?? "the global folder isn't writable",
|
|
240
|
-
changelog: check.changelog,
|
|
241
|
-
}));
|
|
242
|
-
const message = `${globalRoot} is not writable by this user, so the install would fail half-way.`;
|
|
243
|
-
emit({ ...check, outcome: "not-writable", message, command: sudo.command });
|
|
244
|
-
// Never run sudo ourselves (SPEC §5 step 5).
|
|
245
|
-
return out.error(message, EXIT.cannotSelfUpdate);
|
|
183
|
+
// ---- 5. can we write there? Never run sudo.
|
|
184
|
+
const globalRoot = globalRootFor(manager, npm);
|
|
185
|
+
if (globalRoot !== undefined && !(envp.writable ?? isWritable)(globalRoot)) {
|
|
186
|
+
const sudo = updateCommand(info, { packageName: product.packageName, target: target.version, sudo: true });
|
|
187
|
+
out.warn(notice(sudo.command, sudo.note));
|
|
188
|
+
const message = `${globalRoot} isn't writable by this user, so the install would fail half-way.`;
|
|
189
|
+
emit({ ...check, outcome: 'not-writable', message, command: sudo.command });
|
|
190
|
+
return out.error(message, EXIT.CANNOT_UPDATE);
|
|
246
191
|
}
|
|
247
|
-
// ---- 6. busy?
|
|
248
|
-
//
|
|
249
|
-
// Before ANYTHING is stopped, and before the question is asked: being told
|
|
250
|
-
// "it is busy" after answering yes to "the service will restart" is the
|
|
251
|
-
// wrong order to learn it in.
|
|
192
|
+
// ---- 6. busy? Before anything stops, and before the question.
|
|
252
193
|
const refused = await product.guardBusy({ force: opts.force === true, home: product.home });
|
|
253
194
|
if (refused !== null) {
|
|
254
|
-
emit({ ...check, outcome:
|
|
195
|
+
emit({ ...check, outcome: 'busy' });
|
|
255
196
|
return refused;
|
|
256
197
|
}
|
|
257
198
|
const service = product.service.inspect();
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
(service.installed ? " The service will restart." : "");
|
|
199
|
+
const arrow = `${product.currentVersion} → ${target.version}`;
|
|
200
|
+
// ---- 7. ask
|
|
261
201
|
if (opts.yes !== true) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
emit({ ...check, outcome: "needs-yes", message });
|
|
267
|
-
// Exit 2, as SPEC §3 requires of a command that would have asked, with
|
|
268
|
-
// the usage after the message, as SPEC §2 requires of exit 2.
|
|
269
|
-
const code = out.error(message, EXIT.usage);
|
|
202
|
+
if (!(envp.isTTY ?? (process.stdin.isTTY === true && process.stderr.isTTY === true))) {
|
|
203
|
+
const message = `there's no terminal to ask, so --yes is needed: ${product.tool} update --yes`;
|
|
204
|
+
emit({ ...check, outcome: 'needs-yes', message });
|
|
205
|
+
const code = out.error(message, EXIT.USAGE);
|
|
270
206
|
if (product.usage !== undefined)
|
|
271
|
-
out.warn(`\n${product.usage}`);
|
|
207
|
+
out.warn(`\n${product.usage.trimEnd()}`);
|
|
272
208
|
return code;
|
|
273
209
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (!
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return EXIT.ok;
|
|
210
|
+
const question = `${cmp < 0 ? 'Downgrade' : 'Update'} ${product.name} ${arrow}?${service.installed ? ' The service will restart.' : ''}`;
|
|
211
|
+
out.warn(`${question}\n Changes: ${check.changelog ?? ''}`);
|
|
212
|
+
if (!(await (envp.confirm ?? askOnTty)(' Continue? [y/N] '))) {
|
|
213
|
+
emit({ ...check, outcome: 'declined', message: 'Nothing was changed.' });
|
|
214
|
+
out.say('Nothing was changed.');
|
|
215
|
+
return EXIT.OK;
|
|
281
216
|
}
|
|
282
217
|
}
|
|
283
|
-
// ---- 8. fetch first
|
|
284
|
-
//
|
|
285
|
-
// Both versions, and the CURRENT one matters most: it is what the rollback
|
|
286
|
-
// reinstalls, and a rollback that has to reach the network to find the
|
|
287
|
-
// version that was working ten seconds ago is the rollback that fails.
|
|
218
|
+
// ---- 8. fetch first, both versions: the current one is what a rollback reinstalls.
|
|
288
219
|
out.warn(` Fetching ${product.packageName}@${target.version}…`);
|
|
289
|
-
const fetched = cacheAdd(product.packageName, target.version,
|
|
220
|
+
const fetched = cacheAdd(product.packageName, target.version, npm);
|
|
290
221
|
if (fetched.status !== 0) {
|
|
291
|
-
const message = `
|
|
292
|
-
|
|
293
|
-
emit({ ...check, outcome: "failed", message });
|
|
294
|
-
// Nothing has been stopped or changed, so this is a plain failure.
|
|
222
|
+
const message = `couldn't fetch ${product.packageName}@${target.version}:\n${said(fetched)}`;
|
|
223
|
+
emit({ ...check, outcome: 'failed', message });
|
|
295
224
|
return out.error(message);
|
|
296
225
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
out.warn(` Note: ${product.packageName}@${product.currentVersion} is not in the npm cache, so a ` +
|
|
300
|
-
"rollback would need the network.");
|
|
226
|
+
if (cacheAdd(product.packageName, product.currentVersion, npm).status !== 0) {
|
|
227
|
+
out.warn(` Note: ${product.packageName}@${product.currentVersion} isn't in npm's cache, so a rollback would need the network.`);
|
|
301
228
|
}
|
|
302
|
-
// ---- 9. stop
|
|
229
|
+
// ---- 9. stop
|
|
303
230
|
if (service.installed) {
|
|
304
231
|
out.warn(` Stopping the ${product.name} service…`);
|
|
305
232
|
try {
|
|
306
233
|
product.service.stop();
|
|
307
234
|
}
|
|
308
235
|
catch (err) {
|
|
309
|
-
const message = `
|
|
310
|
-
emit({ ...check, outcome:
|
|
236
|
+
const message = `couldn't stop the service: ${err.message}`;
|
|
237
|
+
emit({ ...check, outcome: 'failed', message });
|
|
311
238
|
return out.error(message);
|
|
312
239
|
}
|
|
240
|
+
if (service.url !== undefined && !(await stopped(service.url))) {
|
|
241
|
+
out.warn(` Note: ${service.url} still answered ${STOP_TIMEOUT_MS / 1000} seconds after the stop.`);
|
|
242
|
+
}
|
|
313
243
|
}
|
|
314
244
|
else if (service.running && service.url !== undefined) {
|
|
315
|
-
// A foreground `serve
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
version: product.currentVersion,
|
|
321
|
-
timeoutMs: 2_000,
|
|
322
|
-
});
|
|
323
|
-
if (verdict.state !== "no-answer") {
|
|
324
|
-
const message = `something is serving ${product.name} at ${service.url} and there is no service to stop.\n` +
|
|
325
|
-
" Stop it (Ctrl-C in its terminal), then run this again.";
|
|
326
|
-
emit({ ...check, outcome: "failed", message });
|
|
327
|
-
return out.error(message, EXIT.notRunning);
|
|
245
|
+
// A foreground `serve`: we can't stop someone's terminal, and installing under it is what step 9 is for.
|
|
246
|
+
if ((await fetchHealth({ url: service.url, ...(envp.fetch !== undefined ? { fetch: envp.fetch } : {}) })) !== undefined) {
|
|
247
|
+
const message = `${product.name} is running at ${service.url}, and there's no service to stop. Stop it (Ctrl-C in its terminal), then run this again.`;
|
|
248
|
+
emit({ ...check, outcome: 'failed', message });
|
|
249
|
+
return out.error(message, EXIT.NOT_RUNNING);
|
|
328
250
|
}
|
|
329
251
|
}
|
|
330
|
-
// ---- 10. install
|
|
252
|
+
// ---- 10. install. If npm fails, it has already put the old package back.
|
|
331
253
|
out.warn(` Installing ${product.packageName}@${target.version} with ${manager}…`);
|
|
332
|
-
const installed = installPackage({
|
|
333
|
-
|
|
334
|
-
packageName: product.packageName,
|
|
335
|
-
version: target.version,
|
|
336
|
-
...spawnCtx,
|
|
337
|
-
});
|
|
254
|
+
const installed = installPackage({ ...npm, manager, packageName: product.packageName, version: target.version });
|
|
255
|
+
const back = { check, emit, product, envp, npm, manager, service, waitFor };
|
|
338
256
|
if (installed.status !== 0) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
out.warn(indent(installed.stderr.trim() || installed.stdout.trim()));
|
|
342
|
-
return await rollback({
|
|
343
|
-
why: `installing ${target.version} failed`,
|
|
344
|
-
check,
|
|
345
|
-
emit,
|
|
346
|
-
product,
|
|
347
|
-
envp,
|
|
348
|
-
spawnCtx,
|
|
349
|
-
manager,
|
|
350
|
-
service,
|
|
351
|
-
alreadyRestored: true,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
// Prove the install took, by asking the CLI on disk what it is. See
|
|
355
|
-
// `cliVersion` for why this is not read out of a manifest.
|
|
356
|
-
const newCli = findCli(product, info, target.version, { ...spawnCtx, fs });
|
|
357
|
-
if (newCli === undefined) {
|
|
358
|
-
return await rollback({
|
|
359
|
-
why: `${target.version} was installed, but no CLI on disk reports that version`,
|
|
360
|
-
check,
|
|
361
|
-
emit,
|
|
362
|
-
product,
|
|
363
|
-
envp,
|
|
364
|
-
spawnCtx,
|
|
365
|
-
manager,
|
|
366
|
-
service,
|
|
367
|
-
});
|
|
257
|
+
out.warn(said(installed));
|
|
258
|
+
return rollback({ ...back, why: `installing ${target.version} failed`, alreadyRestored: true });
|
|
368
259
|
}
|
|
369
|
-
|
|
260
|
+
const newCli = findCli(product, info, target.version, { ...npm, fs });
|
|
261
|
+
if (newCli === undefined)
|
|
262
|
+
return rollback({ ...back, why: `${target.version} was installed, but no CLI on disk reports that version` });
|
|
263
|
+
// Without a service: steps 1 to 8 and 10 (SPEC §5).
|
|
370
264
|
if (!service.installed) {
|
|
371
265
|
noteInstalled(product.home, check);
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return EXIT.ok;
|
|
266
|
+
const restart = service.running ? `\n Restart ${product.name} to use ${target.version}.` : '';
|
|
267
|
+
emit({ ...check, outcome: service.running ? 'restart-required' : 'updated', message: `Updated ${product.name} ${arrow}.${restart}` });
|
|
268
|
+
out.say(`Updated ${product.name} ${arrow}.${restart}`);
|
|
269
|
+
return EXIT.OK;
|
|
377
270
|
}
|
|
378
|
-
// ---- 11. hand over to the new version
|
|
379
|
-
out.warn(
|
|
271
|
+
// ---- 11. hand over to the new version
|
|
272
|
+
out.warn(' Refreshing the service unit with the new version…');
|
|
380
273
|
const refreshed = product.service.refresh(newCli);
|
|
381
274
|
if (refreshed.status !== 0) {
|
|
382
|
-
out.warn(
|
|
383
|
-
return
|
|
384
|
-
why: `${target.version} could not refresh the service unit`,
|
|
385
|
-
check,
|
|
386
|
-
emit,
|
|
387
|
-
product,
|
|
388
|
-
envp,
|
|
389
|
-
spawnCtx,
|
|
390
|
-
manager,
|
|
391
|
-
service,
|
|
392
|
-
});
|
|
275
|
+
out.warn(said(refreshed));
|
|
276
|
+
return rollback({ ...back, why: `${target.version} couldn't refresh the service unit` });
|
|
393
277
|
}
|
|
394
|
-
// ---- 12. health
|
|
278
|
+
// ---- 12. health
|
|
395
279
|
const url = product.service.inspect().url ?? service.url;
|
|
396
280
|
if (url === undefined) {
|
|
397
|
-
// Nothing to poll
|
|
398
|
-
// success as the answer rather than inventing a URL to ask.
|
|
281
|
+
// Nothing to poll; the refresh waited for health itself.
|
|
399
282
|
noteInstalled(product.home, check);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return EXIT.ok;
|
|
283
|
+
emit({ ...check, outcome: 'updated', message: `Updated ${product.name} ${arrow}.` });
|
|
284
|
+
out.say(`Updated ${product.name} ${arrow}.`);
|
|
285
|
+
return EXIT.OK;
|
|
404
286
|
}
|
|
405
287
|
out.warn(` Waiting for ${target.version} to answer at ${url}…`);
|
|
406
|
-
const verdict = await
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
timeoutMs: product.healthTimeoutMs ?? HEALTH_TIMEOUT_MS,
|
|
410
|
-
});
|
|
411
|
-
if (verdict.state !== "ok") {
|
|
412
|
-
const detail = verdict.state === "wrong-version"
|
|
288
|
+
const verdict = await waitFor(url, target.version);
|
|
289
|
+
if (verdict.state !== 'ok') {
|
|
290
|
+
const why = verdict.state === 'wrong-version'
|
|
413
291
|
? `${url} answers with ${verdict.version}, not ${target.version}`
|
|
414
|
-
: `${url}
|
|
415
|
-
return
|
|
416
|
-
why: detail,
|
|
417
|
-
check,
|
|
418
|
-
emit,
|
|
419
|
-
product,
|
|
420
|
-
envp,
|
|
421
|
-
spawnCtx,
|
|
422
|
-
manager,
|
|
423
|
-
service,
|
|
424
|
-
showLog: true,
|
|
425
|
-
});
|
|
292
|
+
: `${url} didn't answer within ${healthTimeoutMs / 1000} seconds`;
|
|
293
|
+
return rollback({ ...back, why, showLog: true });
|
|
426
294
|
}
|
|
427
|
-
// ---- 15. done
|
|
295
|
+
// ---- 15. done
|
|
428
296
|
noteInstalled(product.home, check);
|
|
429
297
|
const message = `Updated ${product.name} ${arrow}. Running at ${url}`;
|
|
430
|
-
emit({ ...check, outcome:
|
|
298
|
+
emit({ ...check, outcome: 'updated', message, url });
|
|
431
299
|
out.say(message);
|
|
432
|
-
return EXIT.
|
|
300
|
+
return EXIT.OK;
|
|
301
|
+
}
|
|
302
|
+
/** Polls until the stopped service's URL stops answering (step 9: wait until the port is free). */
|
|
303
|
+
async function stopped(url) {
|
|
304
|
+
const sleep = envp.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
305
|
+
for (let i = 0; i < STOP_TIMEOUT_MS / 250; i++) {
|
|
306
|
+
if ((await fetchHealth({ url, timeoutMs: 1000, ...(envp.fetch !== undefined ? { fetch: envp.fetch } : {}) })) === undefined)
|
|
307
|
+
return true;
|
|
308
|
+
await sleep(250);
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
433
311
|
}
|
|
434
312
|
}
|
|
435
313
|
/**
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
* Step 4 wrote a cache saying an update was available, which was true then and
|
|
439
|
-
* is a lie the moment the install succeeds. `service status` reads that file
|
|
440
|
-
* and would go on advertising a version the machine is already running — the
|
|
441
|
-
* one piece of nagging nobody could act on, because the action had already been
|
|
442
|
-
* taken.
|
|
314
|
+
* After a successful update the target is the installed version, so the cache step 4 wrote ("an update is
|
|
315
|
+
* available") is rewritten; otherwise `status` goes on advertising the version the machine is already running.
|
|
443
316
|
*/
|
|
444
317
|
function noteInstalled(home, check) {
|
|
445
|
-
writeUpdateCheck(home, {
|
|
446
|
-
...check,
|
|
447
|
-
current: check.target,
|
|
448
|
-
updateAvailable: false,
|
|
449
|
-
command: null,
|
|
450
|
-
checkedAt: new Date().toISOString(),
|
|
451
|
-
});
|
|
318
|
+
writeUpdateCheck(home, { ...check, current: check.target, updateAvailable: false, command: null, checkedAt: new Date().toISOString() });
|
|
452
319
|
}
|
|
453
|
-
/**
|
|
320
|
+
/** Asks on the terminal: the question on stderr (SPEC §3), the answer from stdin. */
|
|
454
321
|
async function askOnTty(question) {
|
|
455
322
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
456
323
|
try {
|
|
457
|
-
|
|
458
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
324
|
+
return /^y(es)?$/i.test((await new Promise((resolve) => rl.question(question, resolve))).trim());
|
|
459
325
|
}
|
|
460
326
|
finally {
|
|
461
327
|
rl.close();
|
|
462
328
|
}
|
|
463
329
|
}
|
|
464
330
|
/**
|
|
465
|
-
* SPEC §5 step 13: put the old version back and say which of 6 or 7 happened.
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
* nothing to do: the old version is installed and answering, and the only cost
|
|
469
|
-
* was a minute of downtime. **7** means the machine is not serving and no
|
|
470
|
-
* further automation will fix it, so the output has to be the commands to run
|
|
471
|
-
* by hand. Collapsing the two into "the update failed" is what makes an
|
|
472
|
-
* operator read logs to find out whether their service is up.
|
|
331
|
+
* SPEC §5 step 13: put the old version back, and say which of 6 or 7 happened. 6 means nobody has to do anything:
|
|
332
|
+
* the old version is installed and answering. 7 means the machine isn't serving and nothing automatic will fix it, so
|
|
333
|
+
* the output is the commands to run by hand.
|
|
473
334
|
*/
|
|
474
335
|
async function rollback(o) {
|
|
475
|
-
const { product,
|
|
476
|
-
const out = envp.out;
|
|
477
|
-
const
|
|
336
|
+
const { product, check } = o;
|
|
337
|
+
const out = o.envp.out;
|
|
338
|
+
const current = product.currentVersion;
|
|
478
339
|
out.error(`${o.why}.`);
|
|
479
340
|
if (o.showLog === true) {
|
|
480
341
|
const tail = product.service.tail(LOG_LINES_ON_FAILURE);
|
|
481
|
-
if (tail.trim() !==
|
|
482
|
-
out.warn(`\n The last ${LOG_LINES_ON_FAILURE} lines of ${product.service.inspect().log ??
|
|
483
|
-
out.warn(indent(tail));
|
|
484
|
-
}
|
|
342
|
+
if (tail.trim() !== '')
|
|
343
|
+
out.warn(`\n The last ${LOG_LINES_ON_FAILURE} lines of ${product.service.inspect().log ?? 'the log'}:\n${indent(tail)}`);
|
|
485
344
|
}
|
|
486
|
-
out.warn(`\n Rolling back to ${
|
|
487
|
-
const
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
345
|
+
out.warn(`\n Rolling back to ${current}…`);
|
|
346
|
+
const reinstall = installArgs(o.manager, `${product.packageName}@${current}`);
|
|
347
|
+
const byHand = [` ${reinstall.cmd} ${reinstall.args.join(' ')}`, ` ${product.tool} service install --refresh`, ` ${product.tool} service status`].join('\n');
|
|
348
|
+
const needsHuman = (what) => {
|
|
349
|
+
const message = `${check.target} didn't start, and ${what}. Run these by hand:\n${byHand}`;
|
|
350
|
+
o.emit({ ...check, outcome: 'needs-human', message });
|
|
351
|
+
return out.error(message, EXIT.ROLLBACK_FAILED);
|
|
352
|
+
};
|
|
493
353
|
if (o.alreadyRestored !== true) {
|
|
494
|
-
const reinstalled = installPackage({
|
|
495
|
-
manager: o.manager,
|
|
496
|
-
packageName: product.packageName,
|
|
497
|
-
version: product.currentVersion,
|
|
498
|
-
...o.spawnCtx,
|
|
499
|
-
});
|
|
354
|
+
const reinstalled = installPackage({ ...o.npm, manager: o.manager, packageName: product.packageName, version: current });
|
|
500
355
|
if (reinstalled.status !== 0) {
|
|
501
|
-
out.warn(
|
|
502
|
-
|
|
503
|
-
` Run these by hand:\n${manual}`;
|
|
504
|
-
o.emit({ ...check, outcome: "needs-human", message });
|
|
505
|
-
return out.error(message, EXIT.needsHuman);
|
|
356
|
+
out.warn(said(reinstalled));
|
|
357
|
+
return needsHuman(`${current} couldn't be reinstalled`);
|
|
506
358
|
}
|
|
507
359
|
}
|
|
508
|
-
// The unit
|
|
509
|
-
// unit in the NEW version's format, which the old one may not understand.
|
|
360
|
+
// The unit is refreshed by the old CLI: the new one may have written a format the old one doesn't read.
|
|
510
361
|
if (o.service.installed) {
|
|
511
|
-
const oldCli = findCli(product, undefined,
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
});
|
|
515
|
-
if (oldCli === undefined) {
|
|
516
|
-
const message = `${check.target} did not start, and ${product.currentVersion}'s CLI could not be found ` +
|
|
517
|
-
`to restore the service unit.\n Run these by hand:\n${manual}`;
|
|
518
|
-
o.emit({ ...check, outcome: "needs-human", message });
|
|
519
|
-
return out.error(message, EXIT.needsHuman);
|
|
520
|
-
}
|
|
362
|
+
const oldCli = findCli(product, undefined, current, { ...o.npm, ...(o.envp.fs !== undefined ? { fs: o.envp.fs } : {}) });
|
|
363
|
+
if (oldCli === undefined)
|
|
364
|
+
return needsHuman(`${current}'s CLI couldn't be found to restore the service unit`);
|
|
521
365
|
const restored = product.service.refresh(oldCli);
|
|
522
366
|
if (restored.status !== 0) {
|
|
523
|
-
out.warn(
|
|
524
|
-
|
|
525
|
-
`${product.currentVersion}.\n Run these by hand:\n${manual}`;
|
|
526
|
-
o.emit({ ...check, outcome: "needs-human", message });
|
|
527
|
-
return out.error(message, EXIT.needsHuman);
|
|
367
|
+
out.warn(said(restored));
|
|
368
|
+
return needsHuman(`the service couldn't be started again on ${current}`);
|
|
528
369
|
}
|
|
529
370
|
const url = product.service.inspect().url ?? o.service.url;
|
|
530
|
-
if (url !== undefined)
|
|
531
|
-
|
|
532
|
-
url,
|
|
533
|
-
version: product.currentVersion,
|
|
534
|
-
timeoutMs: product.healthTimeoutMs ?? HEALTH_TIMEOUT_MS,
|
|
535
|
-
});
|
|
536
|
-
if (verdict.state !== "ok") {
|
|
537
|
-
const message = `${check.target} did not start, and ${product.currentVersion} is not answering at ${url} ` +
|
|
538
|
-
"either.\n Run these by hand:\n" +
|
|
539
|
-
manual;
|
|
540
|
-
o.emit({ ...check, outcome: "needs-human", message });
|
|
541
|
-
return out.error(message, EXIT.needsHuman);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
371
|
+
if (url !== undefined && (await o.waitFor(url, current)).state !== 'ok')
|
|
372
|
+
return needsHuman(`${current} isn't answering at ${url} either`);
|
|
544
373
|
}
|
|
545
|
-
const message = `${check.target} didn't start; ${
|
|
546
|
-
o.emit({ ...check, outcome:
|
|
547
|
-
return out.error(message, EXIT.
|
|
374
|
+
const message = `${check.target} didn't start; ${current} is running again.`;
|
|
375
|
+
o.emit({ ...check, outcome: 'rolled-back', message });
|
|
376
|
+
return out.error(message, EXIT.ROLLED_BACK);
|
|
548
377
|
}
|
|
549
378
|
/**
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
* Three candidates, in the order that copes with the most layouts:
|
|
379
|
+
* The CLI on disk that reports `wanted`, tried in this order (keep it):
|
|
553
380
|
*
|
|
554
|
-
* 1.
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
* 2. `<npm root -g>/<name>/<binSubpath>` — the npm-global layout, asked of npm
|
|
559
|
-
* rather than assumed.
|
|
560
|
-
* 3. The package root §6 found, plus the same subpath — right for every layout
|
|
561
|
-
* whose path does not move.
|
|
381
|
+
* 1. `realpath(argv[1])`, resolved now: a manager whose real path carries the version (pnpm's store) repoints the
|
|
382
|
+
* bin's symlink on install, and only this candidate follows it;
|
|
383
|
+
* 2. `<npm root -g>/<name>/<binSubpath>`;
|
|
384
|
+
* 3. the package root §6 found, plus the same subpath.
|
|
562
385
|
*
|
|
563
|
-
* Each is run
|
|
564
|
-
*
|
|
565
|
-
* guards against is handing `service install --refresh` to a CLI that is still
|
|
566
|
-
* the old version, which would produce a green update that changed nothing.
|
|
386
|
+
* Each is run with `--version`, and the first that reports `wanted` wins. "The file exists" isn't enough: handing the
|
|
387
|
+
* refresh to a CLI that's still the old version would make a green update that changed nothing.
|
|
567
388
|
*/
|
|
568
389
|
export function findCli(product, info, wanted, ctx) {
|
|
569
390
|
const fs = ctx.fs ?? fsDefault;
|
|
@@ -575,35 +396,13 @@ export function findCli(product, info, wanted, ctx) {
|
|
|
575
396
|
candidates.push(product.entry);
|
|
576
397
|
}
|
|
577
398
|
const globalRoot = npmGlobalRoot(ctx);
|
|
578
|
-
if (globalRoot !== undefined)
|
|
399
|
+
if (globalRoot !== undefined)
|
|
579
400
|
candidates.push(path.join(globalRoot, product.packageName, product.binSubpath));
|
|
580
|
-
|
|
581
|
-
if (info?.packageRoot !== undefined) {
|
|
401
|
+
if (info?.packageRoot !== undefined)
|
|
582
402
|
candidates.push(path.join(info.packageRoot, product.binSubpath));
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
for (const candidate of candidates) {
|
|
586
|
-
if (seen.has(candidate))
|
|
587
|
-
continue;
|
|
588
|
-
seen.add(candidate);
|
|
589
|
-
if (!fs.existsSync(candidate))
|
|
590
|
-
continue;
|
|
591
|
-
if (cliVersion(candidate, ctx) === wanted)
|
|
403
|
+
for (const candidate of new Set(candidates)) {
|
|
404
|
+
if (fs.existsSync(candidate) && cliVersion(candidate, ctx) === wanted)
|
|
592
405
|
return candidate;
|
|
593
406
|
}
|
|
594
407
|
return undefined;
|
|
595
408
|
}
|
|
596
|
-
/** The command SPEC §3's notice prints for this install, with the product's own details filled in. */
|
|
597
|
-
function commandFor(product, info, target) {
|
|
598
|
-
return updateCommand(info, {
|
|
599
|
-
packageName: product.packageName,
|
|
600
|
-
target,
|
|
601
|
-
...(product.image !== undefined ? { image: product.image } : {}),
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
function indent(text) {
|
|
605
|
-
return text
|
|
606
|
-
.split("\n")
|
|
607
|
-
.map((l) => (l === "" ? l : ` ${l}`))
|
|
608
|
-
.join("\n");
|
|
609
|
-
}
|