@edspencer/paddock 0.82.0 → 0.83.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/cli/args.d.ts +29 -4
- package/packages/server/dist/cli/args.js +145 -7
- package/packages/server/dist/cli/busy-command.js +1 -1
- package/packages/server/dist/cli/busy.js +10 -12
- package/packages/server/dist/cli/doctor.d.ts +60 -0
- package/packages/server/dist/cli/doctor.js +580 -0
- 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 +37 -12
- package/packages/server/dist/cli/service/index.d.ts +30 -2
- package/packages/server/dist/cli/service/index.js +163 -31
- package/packages/server/dist/cli/service/launchd.d.ts +31 -84
- package/packages/server/dist/cli/service/launchd.js +114 -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/ready.d.ts +16 -23
- package/packages/server/dist/cli/service/ready.js +42 -0
- 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 +118 -261
- 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/dist/runtime-file.d.ts +1 -1
- package/packages/server/dist/runtime-file.js +9 -13
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{DeleteProjectDialog-CphdGB3R.js → DeleteProjectDialog-B07MKgv4.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-D5C2fkeQ.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-B_kcF4sm.js → DiscoverView-DpAAyBEf.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-CvUK5DKW.js → InstanceConfigPage-YX0I_jVA.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DvZ-j_Ao.js → MarkdownRenderer-C-CUuFhT.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DayHvUH2.js → ProjectRedirect-qyXbTK56.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-CCw83gUi.js → ProjectView-CguNOv8k.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-B3Klg2H4.js → ProjectsGrid-CH2Lz2Hf.js} +1 -1
- package/packages/web/dist/assets/{RootHome-BxcDvpgI.js → RootHome-CUGUc8za.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-7UlhFBSG.js → Surfaces-11or1q_3.js} +1 -1
- package/packages/web/dist/assets/{arc-DNkh5o5Q.js → arc-CQHeCzrk.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BrgN3oLF.js → architectureDiagram-3BPJPVTR-dGcuFxAa.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-D05XOJSk.js → blockDiagram-GPEHLZMM-CGc9h97S.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-CKaMBhMP.js → c4Diagram-AAUBKEIU-BpX0sqhH.js} +1 -1
- package/packages/web/dist/assets/channel-BAeygx-e.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-8hgadCQc.js → chunk-2J33WTMH-uORGdO9D.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-CBfEC-pu.js → chunk-4BX2VUAB-C8zI2PFy.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-DV1eMgHk.js → chunk-55IACEB6-D22Iv6j6.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-Cfgl5fxY.js → chunk-727SXJPM-DdbHXAtS.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BXhqD295.js → chunk-AQP2D5EJ-BeKbob3a.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-TFGOPtek.js → chunk-FMBD7UC4-DtLvFU_y.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-CHFM665R.js → chunk-ND2GUHAM-BWJFo6B1.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-CqEf6MMT.js → chunk-QZHKN3VN-DeFkNg3S.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-Cq2-sBMa.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-Cq2-sBMa.js +2 -0
- package/packages/web/dist/assets/{core-DmEaKxBb.js → core-CTNNIHx5.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BnGI6jkr.js → cose-bilkent-S5V4N54A-BmtlyA-v.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-CKoPx61E.js → dagre-BM42HDAG-oS3rC9CI.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-mMmtlBl1.js → diagram-2AECGRRQ-BzIBqz6A.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-Cdz9Mtnt.js → diagram-5GNKFQAL-BgIT2g8R.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-DQxM3wYj.js → diagram-KO2AKTUF-DACT41Re.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-BWrinPsq.js → diagram-LMA3HP47-BstA3UkI.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-U6MYUve2.js → diagram-OG6HWLK6-Dp3Cn5n_.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-DGjRXuTP.js → documentTitle-DRSQmA8q.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-xqMCsXxY.js → erDiagram-TEJ5UH35-wf8PyGx_.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-PMfy_czF.js → flowDiagram-I6XJVG4X-CJ4xx6EQ.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-whhYYE4N.js → ganttDiagram-6RSMTGT7-z9obPrPM.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-C51JW4tf.js → gitGraphDiagram-PVQCEYII-Cf4Cu7Tk.js} +1 -1
- package/packages/web/dist/assets/{index-CHOtvlnO.js → index-Bsv-raz2.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-SH8cr905.js → infoDiagram-5YYISTIA-BIfuiYxX.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BjqaoU3U.js → ishikawaDiagram-YF4QCWOH-1pACxMTX.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CBmdEVrd.js → journeyDiagram-JHISSGLW-XGbzfw-v.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-U8njjMLZ.js → kanban-definition-UN3LZRKU-Bvl-nMak.js} +1 -1
- package/packages/web/dist/assets/{linear-DF66GPs1.js → linear-uNToO4bv.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-CouJz7Ph.js → mermaid.core-CrChvyj6.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BYIf0oFt.js → mindmap-definition-RKZ34NQL-C6DaQ4Fm.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-Cm8rgg09.js → pieDiagram-4H26LBE5-CEh8lKyF.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-C3Il5kOH.js → quadrantDiagram-W4KKPZXB-z4zvICSM.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-D3PE2nTe.js → requirementDiagram-4Y6WPE33-BBltkjsj.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-DdmvBM18.js → sankeyDiagram-5OEKKPKP-D0s95wQ4.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-B8M_GAIq.js → sequenceDiagram-3UESZ5HK-kQa65Nbt.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DZkXnK1_.js → stateDiagram-AJRCARHV-BqVIoyED.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-B6OXxEra.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-IHXq7rqi.js → timeline-definition-PNZ67QCA-Bo5L62WI.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Dga7tLtT.js → vennDiagram-CIIHVFJN-CN9rH4lT.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-CmcwnEJ-.js → wardley-L42UT6IY-BKhnD_W_.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-zHuSLrD4.js → wardleyDiagram-YWT4CUSO-XHTbZzXj.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-D93MU_ic.js → xychartDiagram-2RQKCTM6--t9XJLbR.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,48 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* from `reference/exit-codes.ts` because the reference module did not exist yet
|
|
6
|
-
* when Paddock aligned; wave C4 re-syncs this file to it and adds the usual
|
|
7
|
-
* provenance header. The numbers, and what each means, are the contract.
|
|
8
|
-
*
|
|
9
|
-
* | 0 | done; running and answering; up to date; not busy
|
|
10
|
-
* | 1 | failed
|
|
11
|
-
* | 2 | usage: an unknown command or flag, a bad value
|
|
12
|
-
* | 3 | not running: installed but not running or not answering
|
|
13
|
-
* | 4 | not installed: there is no service unit
|
|
14
|
-
* | 5 | this install cannot update itself (`update`, not yet built)
|
|
15
|
-
* | 6 | the update failed and was rolled back (`update`, not yet built)
|
|
16
|
-
* | 7 | the update failed and could not be rolled back
|
|
17
|
-
* | 75 | busy: refused, and why was printed (`busy`, not yet built)
|
|
18
|
-
*
|
|
19
|
-
* Codes 3 and 4 are what `systemctl status` and LSB init scripts use for "not
|
|
20
|
-
* running" and "no such service", so a script that already branches on them
|
|
21
|
-
* keeps working. 75 is `EX_TEMPFAIL` from `sysexits.h`, which is also the code
|
|
22
|
-
* that makes Watchtower's pre-update hook skip an update.
|
|
23
|
-
*
|
|
24
|
-
* The three `update` codes are declared now and unused until wave C2. They are
|
|
25
|
-
* here because the contract's whole point is that the numbers mean the same
|
|
26
|
-
* thing in every tool: adding them later, from a different file, is how two
|
|
27
|
-
* tools end up disagreeing about what 5 means.
|
|
28
|
-
*/
|
|
1
|
+
// From edspencer/cli-contract reference/exit-codes.ts @ 0.2; local changes: none.
|
|
2
|
+
// The exit codes every tool shares (SPEC §2). Copy this file; don't renumber it.
|
|
3
|
+
//
|
|
4
|
+
// Set `process.exitCode`, never call `process.exit()`, so piped output isn't cut short.
|
|
29
5
|
export const EXIT = {
|
|
30
|
-
/** Done
|
|
31
|
-
|
|
32
|
-
/** Failed
|
|
33
|
-
|
|
34
|
-
/** Usage: an unknown command or flag,
|
|
35
|
-
|
|
36
|
-
/** Not running: installed
|
|
37
|
-
|
|
38
|
-
/** Not installed: there
|
|
39
|
-
|
|
40
|
-
/** This install
|
|
41
|
-
|
|
6
|
+
/** Done; running and answering; up to date; not busy. */
|
|
7
|
+
OK: 0,
|
|
8
|
+
/** Failed. */
|
|
9
|
+
FAILED: 1,
|
|
10
|
+
/** Usage: an unknown command or flag, a bad value. The usage is printed after the message. */
|
|
11
|
+
USAGE: 2,
|
|
12
|
+
/** Not running: installed but not running or not answering, or no instance answers. */
|
|
13
|
+
NOT_RUNNING: 3,
|
|
14
|
+
/** Not installed: there's no service unit. */
|
|
15
|
+
NOT_INSTALLED: 4,
|
|
16
|
+
/** This install can't update itself; the right command was printed. */
|
|
17
|
+
CANNOT_UPDATE: 5,
|
|
42
18
|
/** The update failed and was rolled back; the old version is running again. */
|
|
43
|
-
|
|
44
|
-
/** The update failed and
|
|
45
|
-
|
|
46
|
-
/** Busy: refused, and why was printed. Try later, or use `--force`. */
|
|
47
|
-
|
|
19
|
+
ROLLED_BACK: 6,
|
|
20
|
+
/** The update failed and couldn't be rolled back; a person is needed. */
|
|
21
|
+
ROLLBACK_FAILED: 7,
|
|
22
|
+
/** Busy: refused, and why was printed. Try later, or use `--force`. `EX_TEMPFAIL` from sysexits.h. */
|
|
23
|
+
BUSY: 75,
|
|
48
24
|
};
|
|
25
|
+
/** A tool may use 8 to 63 for its own commands (SPEC §2). */
|
|
26
|
+
export function isToolOwnCode(code) {
|
|
27
|
+
return Number.isInteger(code) && code >= 8 && code <= 63;
|
|
28
|
+
}
|
|
29
|
+
/** The SPEC's name for a shared code, for messages ("expected 4 (not installed), got 0 (ok)"). */
|
|
30
|
+
export function describeExitCode(code) {
|
|
31
|
+
if (code === null)
|
|
32
|
+
return 'no exit code (killed by a signal)';
|
|
33
|
+
const names = {
|
|
34
|
+
0: 'ok',
|
|
35
|
+
1: 'failed',
|
|
36
|
+
2: 'usage',
|
|
37
|
+
3: 'not running',
|
|
38
|
+
4: 'not installed',
|
|
39
|
+
5: "can't update itself",
|
|
40
|
+
6: 'rolled back',
|
|
41
|
+
7: "couldn't roll back",
|
|
42
|
+
75: 'busy',
|
|
43
|
+
};
|
|
44
|
+
return names[code] ? `${code} (${names[code]})` : String(code);
|
|
45
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/output.ts @
|
|
2
|
-
// (Paddock compiles to ESM-with-extensions, not a TS runtime);
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// already has a `UsageError` (a `CliError` subclass carrying its own code) that the entrypoint already reports.
|
|
1
|
+
// From edspencer/cli-contract reference/output.ts @ 0.2; local changes: `./exit-codes.ts` → `./exit-codes.js`
|
|
2
|
+
// (Paddock compiles to ESM-with-extensions, not a TS runtime); and the reference's `UsageError` / `reportUsageError`
|
|
3
|
+
// are NOT copied, because `cli/args.ts` already has a `UsageError` (a `CliError` subclass carrying its own code) that
|
|
4
|
+
// the entrypoint already reports.
|
|
6
5
|
//
|
|
7
6
|
// A CLI's output, the way SPEC §2 and §3 say: errors as `<tool>: <message>` on stderr, colour only on a TTY (honouring
|
|
8
7
|
// NO_COLOR and FORCE_COLOR), and with `--json`, one JSON document on stdout and every word of prose on stderr.
|
|
@@ -56,7 +55,7 @@ export function createOutput(o) {
|
|
|
56
55
|
warn(s) {
|
|
57
56
|
stderr.write(line(s));
|
|
58
57
|
},
|
|
59
|
-
error(message, code = EXIT.
|
|
58
|
+
error(message, code = EXIT.FAILED) {
|
|
60
59
|
stderr.write(line(`${o.tool}: ${message}`));
|
|
61
60
|
return code;
|
|
62
61
|
},
|
|
@@ -23,7 +23,7 @@ import os from "node:os";
|
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { spawn } from "node:child_process";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
-
import { CliError, UsageError, USAGE, USAGE_SYNOPSIS, SERVICE_USAGE, CONFIG_USAGE, BUSY_USAGE, UPDATE_USAGE, parseCommand, nodeVersionProblem, explainListenError, } from "./args.js";
|
|
26
|
+
import { CliError, UsageError, USAGE, USAGE_SYNOPSIS, SERVICE_USAGE, CONFIG_USAGE, BUSY_USAGE, UPDATE_USAGE, DOCTOR_USAGE, parseCommand, nodeVersionProblem, explainListenError, } from "./args.js";
|
|
27
27
|
import { EXIT } from "./exit-codes.js";
|
|
28
28
|
import { runService, safeHomeDir, realpathOr } from "./service/index.js";
|
|
29
29
|
import { PADDOCK_NAME, PADDOCK_VERSION } from "../version.js";
|
|
@@ -78,9 +78,9 @@ const packageRoot = findUp("package.json") ?? path.resolve(moduleDir, "../..");
|
|
|
78
78
|
* says the usage follows the message — the short one, not {@link USAGE}'s
|
|
79
79
|
* thousand words, which would bury the line that says what was wrong.
|
|
80
80
|
*/
|
|
81
|
-
function fail(message, code = EXIT.
|
|
81
|
+
function fail(message, code = EXIT.FAILED) {
|
|
82
82
|
console.error(`paddock: ${message}`);
|
|
83
|
-
if (code === EXIT.
|
|
83
|
+
if (code === EXIT.USAGE) {
|
|
84
84
|
console.error("");
|
|
85
85
|
console.error(USAGE_SYNOPSIS);
|
|
86
86
|
}
|
|
@@ -175,7 +175,7 @@ async function service(command) {
|
|
|
175
175
|
// which `main` has already handled. Typed rather than asserted.
|
|
176
176
|
if (action === undefined) {
|
|
177
177
|
console.log(SERVICE_USAGE);
|
|
178
|
-
return EXIT.
|
|
178
|
+
return EXIT.OK;
|
|
179
179
|
}
|
|
180
180
|
try {
|
|
181
181
|
return await runService(action, opts, serviceContext());
|
|
@@ -246,13 +246,13 @@ async function config(command) {
|
|
|
246
246
|
// which `main` has already handled. Typed rather than asserted.
|
|
247
247
|
if (action === undefined) {
|
|
248
248
|
console.log(CONFIG_USAGE);
|
|
249
|
-
return EXIT.
|
|
249
|
+
return EXIT.OK;
|
|
250
250
|
}
|
|
251
251
|
process.env.PADDOCK_DATA_DIR = resolveDataDir(opts);
|
|
252
252
|
const { runConfig } = await import("./config.js");
|
|
253
253
|
try {
|
|
254
254
|
await runConfig(action, opts);
|
|
255
|
-
return EXIT.
|
|
255
|
+
return EXIT.OK;
|
|
256
256
|
}
|
|
257
257
|
catch (err) {
|
|
258
258
|
// A config that will not load reaches here as a CliError already carrying
|
|
@@ -289,13 +289,15 @@ async function main() {
|
|
|
289
289
|
? BUSY_USAGE
|
|
290
290
|
: command.verb === "update"
|
|
291
291
|
? UPDATE_USAGE
|
|
292
|
-
:
|
|
293
|
-
|
|
292
|
+
: command.verb === "doctor"
|
|
293
|
+
? DOCTOR_USAGE
|
|
294
|
+
: USAGE);
|
|
295
|
+
return EXIT.OK;
|
|
294
296
|
}
|
|
295
297
|
if (opts.version) {
|
|
296
298
|
// Bare: the version and a newline, nothing else (SPEC §1). Scripts parse this.
|
|
297
299
|
console.log(PADDOCK_VERSION);
|
|
298
|
-
return EXIT.
|
|
300
|
+
return EXIT.OK;
|
|
299
301
|
}
|
|
300
302
|
const problem = nodeVersionProblem(process.versions.node);
|
|
301
303
|
if (problem !== undefined)
|
|
@@ -329,6 +331,21 @@ async function main() {
|
|
|
329
331
|
throw err;
|
|
330
332
|
}
|
|
331
333
|
}
|
|
334
|
+
// `doctor` (SPEC §1). Dynamic for the same reason `busy` and `update` are; it
|
|
335
|
+
// loads the config only to check it, and starts nothing.
|
|
336
|
+
if (command.verb === "doctor") {
|
|
337
|
+
const { runDoctor } = await import("./doctor.js");
|
|
338
|
+
try {
|
|
339
|
+
return await runDoctor(opts, serviceContext());
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
if (err instanceof CliError)
|
|
343
|
+
return fail(err.message, err.exitCode);
|
|
344
|
+
if (err instanceof Error)
|
|
345
|
+
return fail(err.message);
|
|
346
|
+
throw err;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
332
349
|
addBundledBinsToPath();
|
|
333
350
|
// NOTHING here reads `process.cwd()`, and that is the point (#798). `--here`
|
|
334
351
|
// used to make the current directory the workspace by setting
|
|
@@ -352,7 +369,10 @@ async function main() {
|
|
|
352
369
|
// dynamic import below is picked up with no special-casing in config.ts.
|
|
353
370
|
// Precedence is explicit flag > existing env > our default.
|
|
354
371
|
const dataDir = resolveDataDir(opts);
|
|
355
|
-
|
|
372
|
+
// `--quiet` (SPEC §3): the banner line and nothing else, so not the
|
|
373
|
+
// first-run note either (it is prose on stderr, but prose all the same).
|
|
374
|
+
if (opts.quiet !== true)
|
|
375
|
+
noteFirstRun(dataDir);
|
|
356
376
|
process.env.PADDOCK_DATA_DIR = dataDir;
|
|
357
377
|
if (opts.port)
|
|
358
378
|
process.env.PORT = opts.port;
|
|
@@ -404,6 +424,11 @@ async function main() {
|
|
|
404
424
|
// runner, and by anything that wants the URL without guessing the port — so
|
|
405
425
|
// the friendly block below it stays, but below it.
|
|
406
426
|
console.log(`${PADDOCK_NAME} ${PADDOCK_VERSION}: ${url}`);
|
|
427
|
+
if (opts.quiet === true) {
|
|
428
|
+
if (opts.open)
|
|
429
|
+
openBrowser(url);
|
|
430
|
+
return EXIT.OK;
|
|
431
|
+
}
|
|
407
432
|
// Still name the data dir, even though it no longer varies with cwd: "which
|
|
408
433
|
// instance am I looking at" is a real question the moment anyone passes
|
|
409
434
|
// `--data-dir`, and the answer should be readable off the two lines the tool
|
|
@@ -420,7 +445,7 @@ async function main() {
|
|
|
420
445
|
// 0, but NOT yet: a foreground serve keeps running because the server holds
|
|
421
446
|
// the event loop open. `process.exitCode` is set and never acted on until the
|
|
422
447
|
// listener closes, which is what SIGTERM does (start.ts).
|
|
423
|
-
return EXIT.
|
|
448
|
+
return EXIT.OK;
|
|
424
449
|
}
|
|
425
450
|
/**
|
|
426
451
|
* A closed pipe exits quietly (SPEC §2).
|
|
@@ -440,5 +465,5 @@ main().then((code) => {
|
|
|
440
465
|
process.exitCode = code;
|
|
441
466
|
}, (err) => {
|
|
442
467
|
console.error("fatal:", err);
|
|
443
|
-
process.exitCode = EXIT.
|
|
468
|
+
process.exitCode = EXIT.FAILED;
|
|
444
469
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type CliOptions, type ServiceAction } from "../args.js";
|
|
2
|
-
import { isNpxPath, serviceDir
|
|
3
|
-
import { type Runner } from "./
|
|
2
|
+
import { isNpxPath, serviceDir } from "./spec.js";
|
|
3
|
+
import { type Runner, type ServiceManager as ServiceBackend, type ServiceSpec } from "./manager.js";
|
|
4
4
|
import { type UpdateSummary } from "../update/update-check.js";
|
|
5
5
|
import { type HealthAnswer, type Readiness } from "./ready.js";
|
|
6
6
|
export interface ServiceContext {
|
|
@@ -44,7 +44,33 @@ export interface ServiceContext {
|
|
|
44
44
|
force?: boolean;
|
|
45
45
|
json?: boolean;
|
|
46
46
|
}) => Promise<number | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Who holds `host:port` — nothing, a Paddock, or something else — asked by
|
|
49
|
+
* `install` before it writes a unit (SPEC §1). Injected so a test's answer
|
|
50
|
+
* does not depend on what happens to be listening on the box running it.
|
|
51
|
+
*/
|
|
52
|
+
portOwner?: (host: string, port: number) => Promise<PortOwner>;
|
|
47
53
|
}
|
|
54
|
+
export type PortOwner = "free" | "paddock" | "other";
|
|
55
|
+
/** The real {@link ServiceContext.portOwner}: a TCP connect, then `/api/health` and `/api/busy`. */
|
|
56
|
+
export declare function portOwner(host: string, port: number): Promise<PortOwner>;
|
|
57
|
+
/**
|
|
58
|
+
* Why `install` won't write a unit that runs a checkout (SPEC §1, §6).
|
|
59
|
+
*
|
|
60
|
+
* A checkout (or an `npm link`) is a working tree: it gets rebuilt, switched to
|
|
61
|
+
* another branch, cleaned and moved, and a unit that runs it follows every one
|
|
62
|
+
* of those — half-built `dist/` included — at the next login, unattended. The
|
|
63
|
+
* flag is there for working on Paddock itself, where that is the point.
|
|
64
|
+
*/
|
|
65
|
+
export declare function checkoutRefusal(packageRoot: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* Pick a backend, or explain why there isn't one.
|
|
68
|
+
*
|
|
69
|
+
* Windows is the honest gap. It has no per-user service concept that maps onto
|
|
70
|
+
* either of these without a scheduled task or a service wrapper, and shipping
|
|
71
|
+
* an untested third writer would be worse than saying so.
|
|
72
|
+
*/
|
|
73
|
+
export declare function serviceBackend(ctx: ServiceContext): ServiceBackend;
|
|
48
74
|
/**
|
|
49
75
|
* The version of the Paddock the unit points at.
|
|
50
76
|
*
|
|
@@ -104,6 +130,8 @@ export interface StatusReport {
|
|
|
104
130
|
/** A stale unit, a missing Node — things that are wrong but not fatal. */
|
|
105
131
|
warnings: string[];
|
|
106
132
|
}
|
|
133
|
+
/** What `status` reports, and what `doctor`'s service checks read: one report, so the two agree. */
|
|
134
|
+
export declare function buildStatus(ctx: ServiceContext, backend: ServiceBackend): Promise<StatusReport>;
|
|
107
135
|
/**
|
|
108
136
|
* Dispatch, returning the exit code rather than setting one (SPEC §2).
|
|
109
137
|
*
|
|
@@ -27,15 +27,41 @@ import os from "node:os";
|
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import { CliError } from "../args.js";
|
|
29
29
|
import { EXIT } from "../exit-codes.js";
|
|
30
|
-
import { buildSpec, defaultDataDir, hostFromArgv, isNpxPath, NPX_REFUSAL, portFromArgv, serviceDir, serviceLogPath, } from "./spec.js";
|
|
31
|
-
import { spawnRunner, } from "./
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
30
|
+
import { buildSpec, defaultDataDir, hostFromArgv, isNpxPath, NPX_REFUSAL, portFromArgv, serviceDir, serviceLogPath, PADDOCK_SERVICE, } from "./spec.js";
|
|
31
|
+
import { spawnRunner, } from "./manager.js";
|
|
32
|
+
import { createLaunchd, renderPlist } from "./launchd.js";
|
|
33
|
+
import { createSystemd, renderUnit } from "./systemd.js";
|
|
34
|
+
import { underNodeModules } from "../update/install-method.js";
|
|
34
35
|
import { missingPaths } from "./path-env.js";
|
|
35
36
|
import { DEFAULT_LOG_LINES, followFile, sizeOf, tailFile } from "./logs.js";
|
|
36
37
|
import { readLiveRuntimeFile } from "../../runtime-file.js";
|
|
38
|
+
import { PADDOCK_VERSION } from "../../version.js";
|
|
37
39
|
import { updateSummary } from "../update/update-check.js";
|
|
38
|
-
import { waitForReady, fetchHealth, READY_TIMEOUT_LABEL, } from "./ready.js";
|
|
40
|
+
import { waitForReady, fetchHealth, identifyPaddock, isListening, READY_TIMEOUT_LABEL, } from "./ready.js";
|
|
41
|
+
/** The real {@link ServiceContext.portOwner}: a TCP connect, then `/api/health` and `/api/busy`. */
|
|
42
|
+
export async function portOwner(host, port) {
|
|
43
|
+
const probe = host === "0.0.0.0" || host === "" ? "127.0.0.1" : host === "::" ? "::1" : host;
|
|
44
|
+
if (!(await isListening(probe, port)))
|
|
45
|
+
return "free";
|
|
46
|
+
const who = await identifyPaddock(`http://${probe.includes(":") ? `[${probe}]` : probe}:${port}`);
|
|
47
|
+
return who?.paddock === true ? "paddock" : "other";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Why `install` won't write a unit that runs a checkout (SPEC §1, §6).
|
|
51
|
+
*
|
|
52
|
+
* A checkout (or an `npm link`) is a working tree: it gets rebuilt, switched to
|
|
53
|
+
* another branch, cleaned and moved, and a unit that runs it follows every one
|
|
54
|
+
* of those — half-built `dist/` included — at the next login, unattended. The
|
|
55
|
+
* flag is there for working on Paddock itself, where that is the point.
|
|
56
|
+
*/
|
|
57
|
+
export function checkoutRefusal(packageRoot) {
|
|
58
|
+
return (`this copy of Paddock is a checkout (${packageRoot}), so \`service install\` won't use it.\n` +
|
|
59
|
+
" A unit that runs a checkout follows every rebuild, branch switch and move of\n" +
|
|
60
|
+
" it, at your next login, unattended. Install it properly first:\n\n" +
|
|
61
|
+
" npm i -g @edspencer/paddock && paddock service install\n\n" +
|
|
62
|
+
" Or run this checkout as the service anyway (when working on Paddock):\n" +
|
|
63
|
+
" paddock service install --from-checkout\n");
|
|
64
|
+
}
|
|
39
65
|
/**
|
|
40
66
|
* Pick a backend, or explain why there isn't one.
|
|
41
67
|
*
|
|
@@ -43,12 +69,20 @@ import { waitForReady, fetchHealth, READY_TIMEOUT_LABEL, } from "./ready.js";
|
|
|
43
69
|
* either of these without a scheduled task or a service wrapper, and shipping
|
|
44
70
|
* an untested third writer would be worse than saying so.
|
|
45
71
|
*/
|
|
72
|
+
export function serviceBackend(ctx) {
|
|
73
|
+
return backendFor(ctx);
|
|
74
|
+
}
|
|
46
75
|
function backendFor(ctx) {
|
|
47
76
|
const run = ctx.run ?? spawnRunner;
|
|
48
77
|
if (ctx.platform === "darwin")
|
|
49
|
-
return
|
|
50
|
-
if (ctx.platform === "linux")
|
|
51
|
-
return
|
|
78
|
+
return createLaunchd(PADDOCK_SERVICE, { run, homeDir: ctx.homeDir });
|
|
79
|
+
if (ctx.platform === "linux") {
|
|
80
|
+
return createSystemd(PADDOCK_SERVICE, {
|
|
81
|
+
run,
|
|
82
|
+
homeDir: ctx.homeDir,
|
|
83
|
+
...(ctx.xdgConfigHome !== undefined ? { xdgConfigHome: ctx.xdgConfigHome } : {}),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
52
86
|
throw new CliError(`\`paddock service\` supports macOS (launchd) and Linux (systemd --user), not ${ctx.platform}.\n` +
|
|
53
87
|
"Run `paddock` in a terminal, or keep it up with whatever your platform uses.");
|
|
54
88
|
}
|
|
@@ -182,6 +216,12 @@ function lingerNote(linger) {
|
|
|
182
216
|
async function install(opts, ctx, backend) {
|
|
183
217
|
if (isNpxPath(ctx.packageRoot))
|
|
184
218
|
throw new CliError(NPX_REFUSAL);
|
|
219
|
+
// A checkout is a package root that isn't under any `node_modules` (SPEC §6):
|
|
220
|
+
// `packages/server` in the repo, `<prefix>/lib/node_modules/@edspencer/paddock/
|
|
221
|
+
// packages/server` from npm. `npm link` reads the same, on purpose.
|
|
222
|
+
if (!underNodeModules(ctx.packageRoot) && opts.fromCheckout !== true) {
|
|
223
|
+
throw new CliError(checkoutRefusal(ctx.packageRoot));
|
|
224
|
+
}
|
|
185
225
|
// `--refresh` (SPEC §5 step 11): re-render an EXISTING unit, keeping what it
|
|
186
226
|
// records. With nothing installed there is nothing to refresh — exit 4, the
|
|
187
227
|
// same code every other command that needs a unit returns, rather than
|
|
@@ -190,17 +230,57 @@ async function install(opts, ctx, backend) {
|
|
|
190
230
|
// CLI, and a silent fallback would move a `--port 9000` service to 7233 on
|
|
191
231
|
// the one run nobody is watching.
|
|
192
232
|
const refreshing = opts.refresh === true;
|
|
193
|
-
|
|
233
|
+
// A reinstall keeps what the installed unit records — its port, host, data
|
|
234
|
+
// dir and verbosity — unless a flag says otherwise (SPEC §1). It used to do
|
|
235
|
+
// that only with `--refresh`, so `service install` over a `--port 9000`
|
|
236
|
+
// service quietly moved it to 7233. `--refresh` still differs in two ways:
|
|
237
|
+
// it ignores `PADDOCK_DATA_DIR` in this shell, and with no unit it exits 4.
|
|
238
|
+
const current = backend.status();
|
|
239
|
+
const existing = current.registered ? current : undefined;
|
|
194
240
|
if (refreshing) {
|
|
195
|
-
existing
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
241
|
+
if (existing === undefined) {
|
|
242
|
+
if (opts.dryRun === true && opts.json === true) {
|
|
243
|
+
// `--dry-run --json` prints its own document (runService leaves it be).
|
|
244
|
+
console.error(notInstalled(backend));
|
|
245
|
+
console.log(JSON.stringify({ ok: false, action: "install", dryRun: true, error: "not installed", unit: backend.unitPath }, null, 2));
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
console.log(notInstalled(backend));
|
|
249
|
+
}
|
|
250
|
+
return EXIT.NOT_INSTALLED;
|
|
199
251
|
}
|
|
200
252
|
}
|
|
201
253
|
const { spec, dataDir } = specFor(existing !== undefined ? inherit(opts, existing) : opts, ctx, refreshing);
|
|
202
254
|
const url = `http://${hostFromArgv(spec.args)}:${portFromArgv(spec.args)}`;
|
|
203
|
-
|
|
255
|
+
// `--dry-run` (SPEC §1): the unit, on stdout, and nothing written — not the
|
|
256
|
+
// unit, not the service or log folders `backend.install` would make. The
|
|
257
|
+
// where-it-would-go line is on stderr, so `> paddock.service` captures the
|
|
258
|
+
// unit alone.
|
|
259
|
+
if (opts.dryRun === true) {
|
|
260
|
+
const contents = backend.platform === "darwin" ? renderPlist(PADDOCK_SERVICE, spec) : renderUnit(PADDOCK_SERVICE, spec);
|
|
261
|
+
if (opts.json === true) {
|
|
262
|
+
console.log(JSON.stringify({ ok: true, action: "install", dryRun: true, unit: backend.unitPath, contents }, null, 2));
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
console.error(`paddock: dry run: this is the unit \`service install\` would write to ${backend.unitPath}. Nothing was written.`);
|
|
266
|
+
console.log(contents.trimEnd());
|
|
267
|
+
}
|
|
268
|
+
return EXIT.OK;
|
|
269
|
+
}
|
|
270
|
+
// Refuse a port something else holds (SPEC §1): the service would start,
|
|
271
|
+
// fail to bind, and restart every ten seconds, for good. A Paddock holding it
|
|
272
|
+
// is fine — that's the reinstall this replaces — and so is the unit's own
|
|
273
|
+
// running service when it's too old to say it's Paddock (before 0.81).
|
|
274
|
+
const port = Number(portFromArgv(spec.args));
|
|
275
|
+
const owner = await (ctx.portOwner ?? portOwner)(hostFromArgv(spec.args), port);
|
|
276
|
+
if (owner === "other") {
|
|
277
|
+
const itsOwn = current.registered && current.running && portFromArgv(current.argv) === String(port);
|
|
278
|
+
if (!itsOwn) {
|
|
279
|
+
throw new CliError(`port ${port} is held by something that isn't Paddock, so the service could not start.\n` +
|
|
280
|
+
` Stop it, or pick another port: paddock service install --port ${port + 1}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (existing !== undefined) {
|
|
204
284
|
console.log(`\n Re-writing ${backend.unitPath}\n Keeping: paddock ${spec.args.join(" ")}`);
|
|
205
285
|
}
|
|
206
286
|
console.log(installProgress(backend));
|
|
@@ -241,7 +321,7 @@ async function install(opts, ctx, backend) {
|
|
|
241
321
|
].join("\n"));
|
|
242
322
|
// "It is installed but did not answer" is a failure, and used to exit 0 — so
|
|
243
323
|
// an Ansible task or a CI step could not tell it from a working install.
|
|
244
|
-
return readiness === "ready" ? EXIT.
|
|
324
|
+
return readiness === "ready" ? EXIT.OK : EXIT.FAILED;
|
|
245
325
|
}
|
|
246
326
|
function uninstall(ctx, backend) {
|
|
247
327
|
const existed = backend.uninstall();
|
|
@@ -255,7 +335,7 @@ function uninstall(ctx, backend) {
|
|
|
255
335
|
// 0 either way: "there was nothing to remove" is the state the caller asked
|
|
256
336
|
// for. `uninstall` is idempotent on purpose, so a teardown script can run it
|
|
257
337
|
// without first asking whether it has anything to do.
|
|
258
|
-
return EXIT.
|
|
338
|
+
return EXIT.OK;
|
|
259
339
|
}
|
|
260
340
|
/**
|
|
261
341
|
* The data dir the INSTALLED unit uses, read out of its own argument vector.
|
|
@@ -318,10 +398,11 @@ export function installedVersionOf(cliPath) {
|
|
|
318
398
|
}
|
|
319
399
|
/** `{ update }` when there is a recent check to repeat, and `{}` otherwise. */
|
|
320
400
|
function withUpdate(home) {
|
|
321
|
-
const summary = updateSummary(home);
|
|
401
|
+
const summary = updateSummary(home, { currentVersion: PADDOCK_VERSION });
|
|
322
402
|
return summary !== undefined ? { update: summary } : {};
|
|
323
403
|
}
|
|
324
|
-
|
|
404
|
+
/** What `status` reports, and what `doctor`'s service checks read: one report, so the two agree. */
|
|
405
|
+
export async function buildStatus(ctx, backend) {
|
|
325
406
|
const state = backend.status();
|
|
326
407
|
const base = {
|
|
327
408
|
installed: state.registered,
|
|
@@ -432,7 +513,7 @@ async function status(opts, ctx, backend) {
|
|
|
432
513
|
// that one StatusReport is supposed to prevent. A newer version is worth
|
|
433
514
|
// knowing about whether or not a unit exists.
|
|
434
515
|
noteUpdate(report);
|
|
435
|
-
return EXIT.
|
|
516
|
+
return EXIT.NOT_INSTALLED;
|
|
436
517
|
}
|
|
437
518
|
const versions = report.runningVersion !== null && report.runningVersion !== report.installedVersion
|
|
438
519
|
? ` Version: ${report.runningVersion} running, ${report.installedVersion ?? "unknown"} installed`
|
|
@@ -495,10 +576,10 @@ function noteUpdate(report) {
|
|
|
495
576
|
/** The exit code a report means (SPEC §2). */
|
|
496
577
|
function statusExit(report) {
|
|
497
578
|
if (!report.installed)
|
|
498
|
-
return EXIT.
|
|
579
|
+
return EXIT.NOT_INSTALLED;
|
|
499
580
|
if (!report.running || !report.answering)
|
|
500
|
-
return EXIT.
|
|
501
|
-
return EXIT.
|
|
581
|
+
return EXIT.NOT_RUNNING;
|
|
582
|
+
return EXIT.OK;
|
|
502
583
|
}
|
|
503
584
|
/**
|
|
504
585
|
* The URL an installed unit is actually serving, or undefined if none is.
|
|
@@ -539,12 +620,12 @@ async function startOrRestart(action, opts, ctx, backend) {
|
|
|
539
620
|
const before = backend.status();
|
|
540
621
|
if (!before.registered) {
|
|
541
622
|
console.log(notInstalled(backend));
|
|
542
|
-
return EXIT.
|
|
623
|
+
return EXIT.NOT_INSTALLED;
|
|
543
624
|
}
|
|
544
625
|
const url = installedUrl(before);
|
|
545
626
|
if (action === "start" && before.running) {
|
|
546
627
|
console.log(`\n Paddock is already running${pid(before)}.\n\n URL: ${url}\n`);
|
|
547
|
-
return EXIT.
|
|
628
|
+
return EXIT.OK;
|
|
548
629
|
}
|
|
549
630
|
// A restart stops the instance first, so it asks the same question `stop`
|
|
550
631
|
// asks (SPEC §1): refuse with 75 unless `--force`. `start` does not — there
|
|
@@ -578,7 +659,7 @@ async function startOrRestart(action, opts, ctx, backend) {
|
|
|
578
659
|
].join("\n"));
|
|
579
660
|
// A start that never answered is a failure, not a success with a caveat: the
|
|
580
661
|
// caller — often an Ansible task — has no other way to find out.
|
|
581
|
-
return readiness === "ready" ? EXIT.
|
|
662
|
+
return readiness === "ready" ? EXIT.OK : EXIT.FAILED;
|
|
582
663
|
}
|
|
583
664
|
/**
|
|
584
665
|
* The busy check, asked of the instance the UNIT describes (SPEC §1, §4).
|
|
@@ -603,11 +684,11 @@ async function stop(opts, ctx, backend) {
|
|
|
603
684
|
const state = backend.status();
|
|
604
685
|
if (!state.registered) {
|
|
605
686
|
console.log(notInstalled(backend));
|
|
606
|
-
return EXIT.
|
|
687
|
+
return EXIT.NOT_INSTALLED;
|
|
607
688
|
}
|
|
608
689
|
if (!state.running) {
|
|
609
690
|
console.log(`\n Paddock is already stopped.\n Unit: ${backend.unitPath}\n`);
|
|
610
|
-
return EXIT.
|
|
691
|
+
return EXIT.OK;
|
|
611
692
|
}
|
|
612
693
|
// Refuse with 75 if it is in the middle of something, unless --force (SPEC
|
|
613
694
|
// §4). Deliberately AFTER the two cases above: an unit that is not there, or
|
|
@@ -626,7 +707,7 @@ async function stop(opts, ctx, backend) {
|
|
|
626
707
|
" paddock service start",
|
|
627
708
|
"",
|
|
628
709
|
].join("\n"));
|
|
629
|
-
return EXIT.
|
|
710
|
+
return EXIT.OK;
|
|
630
711
|
}
|
|
631
712
|
function pid(state) {
|
|
632
713
|
return state.pid !== undefined ? ` (pid ${state.pid})` : "";
|
|
@@ -648,7 +729,7 @@ async function logs(opts, ctx, backend) {
|
|
|
648
729
|
const state = backend.status();
|
|
649
730
|
if (!state.registered) {
|
|
650
731
|
console.log(notInstalled(backend));
|
|
651
|
-
return EXIT.
|
|
732
|
+
return EXIT.NOT_INSTALLED;
|
|
652
733
|
}
|
|
653
734
|
const file = logPathOf(state, dataDirOf(state, ctx));
|
|
654
735
|
const lines = opts.lines ?? DEFAULT_LOG_LINES;
|
|
@@ -657,7 +738,7 @@ async function logs(opts, ctx, backend) {
|
|
|
657
738
|
// would be mixed into the log output itself.
|
|
658
739
|
console.error(`paddock: nothing has been written to ${file} yet.`);
|
|
659
740
|
if (opts.follow !== true)
|
|
660
|
-
return EXIT.
|
|
741
|
+
return EXIT.OK;
|
|
661
742
|
}
|
|
662
743
|
else {
|
|
663
744
|
const tail = tailFile(file, lines);
|
|
@@ -668,7 +749,7 @@ async function logs(opts, ctx, backend) {
|
|
|
668
749
|
console.error(`paddock: following ${file} — Ctrl-C to stop.`);
|
|
669
750
|
await followFile(file, { from: sizeOf(file) });
|
|
670
751
|
}
|
|
671
|
-
return EXIT.
|
|
752
|
+
return EXIT.OK;
|
|
672
753
|
}
|
|
673
754
|
/**
|
|
674
755
|
* Dispatch, returning the exit code rather than setting one (SPEC §2).
|
|
@@ -680,6 +761,57 @@ export async function runService(action, opts, ctx) {
|
|
|
680
761
|
// `--force` is rejected on every action but `stop` and `restart` — in
|
|
681
762
|
// `parseCommand`, once, for every verb. See the note there.
|
|
682
763
|
const backend = backendFor(ctx);
|
|
764
|
+
// `--json` on the actions that change something (SPEC §1): their prose goes
|
|
765
|
+
// to stderr, and stdout gets one document — what was done, and the status
|
|
766
|
+
// afterwards. `status` has its own; `--dry-run` prints its own.
|
|
767
|
+
if (opts.json === true && action !== "status" && action !== "logs" && opts.dryRun !== true) {
|
|
768
|
+
return withJson(action, () => act(action, opts, ctx, backend), ctx, backend);
|
|
769
|
+
}
|
|
770
|
+
return act(action, opts, ctx, backend);
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Run `act` with its prose on stderr, then print `{ ok, action, error?,
|
|
774
|
+
* ...status }` on stdout (SPEC §1, §3) — the shape Hallway's `service … --json`
|
|
775
|
+
* prints, and `status --json`'s fields after `ok` and `action`, so a script
|
|
776
|
+
* reads one shape whatever it asked.
|
|
777
|
+
*
|
|
778
|
+
* Every line in this file goes through `console.log`, so the prose is moved by
|
|
779
|
+
* pointing that at stderr for the duration, rather than by threading a writer
|
|
780
|
+
* through every function. A failure still prints its document, and is then
|
|
781
|
+
* re-thrown, so `paddock: <message>` and the exit code are what they are
|
|
782
|
+
* without `--json`.
|
|
783
|
+
*/
|
|
784
|
+
async function withJson(action, run, ctx, backend) {
|
|
785
|
+
const log = console.log;
|
|
786
|
+
console.log = (...a) => console.error(...a);
|
|
787
|
+
let code;
|
|
788
|
+
let failure;
|
|
789
|
+
try {
|
|
790
|
+
code = await run();
|
|
791
|
+
}
|
|
792
|
+
catch (err) {
|
|
793
|
+
failure = err;
|
|
794
|
+
code = err instanceof CliError ? err.exitCode : EXIT.FAILED;
|
|
795
|
+
}
|
|
796
|
+
finally {
|
|
797
|
+
console.log = log;
|
|
798
|
+
}
|
|
799
|
+
const error = failure !== undefined
|
|
800
|
+
? (failure instanceof Error ? failure.message : String(failure)).split("\n")[0]
|
|
801
|
+
: code === EXIT.BUSY
|
|
802
|
+
? "busy"
|
|
803
|
+
: code === EXIT.NOT_INSTALLED
|
|
804
|
+
? "not installed"
|
|
805
|
+
: code !== EXIT.OK
|
|
806
|
+
? "failed"
|
|
807
|
+
: undefined;
|
|
808
|
+
const report = await buildStatus(ctx, backend);
|
|
809
|
+
console.log(JSON.stringify({ ok: code === EXIT.OK, action, ...(error !== undefined ? { error } : {}), ...report }, null, 2));
|
|
810
|
+
if (failure !== undefined)
|
|
811
|
+
throw failure;
|
|
812
|
+
return code;
|
|
813
|
+
}
|
|
814
|
+
async function act(action, opts, ctx, backend) {
|
|
683
815
|
if (action === "install")
|
|
684
816
|
return install(opts, ctx, backend);
|
|
685
817
|
if (action === "uninstall")
|