@edspencer/paddock 0.79.1 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/server/dist/app.d.ts +7 -0
- package/packages/server/dist/app.js +19 -15
- package/packages/server/dist/auth.d.ts +3 -2
- package/packages/server/dist/auth.js +11 -0
- package/packages/server/dist/background-live.d.ts +17 -0
- package/packages/server/dist/background-live.js +24 -0
- package/packages/server/dist/busy.d.ts +96 -0
- package/packages/server/dist/busy.js +88 -0
- package/packages/server/dist/cli/args.d.ts +63 -7
- package/packages/server/dist/cli/args.js +227 -20
- package/packages/server/dist/cli/busy-command.d.ts +58 -0
- package/packages/server/dist/cli/busy-command.js +108 -0
- package/packages/server/dist/cli/busy.d.ts +69 -0
- package/packages/server/dist/cli/busy.js +150 -0
- package/packages/server/dist/cli/exit-codes.d.ts +49 -0
- package/packages/server/dist/cli/exit-codes.js +48 -0
- package/packages/server/dist/cli/output.d.ts +52 -0
- package/packages/server/dist/cli/output.js +84 -0
- package/packages/server/dist/cli/paddock.js +98 -37
- package/packages/server/dist/cli/service/backend.d.ts +48 -4
- package/packages/server/dist/cli/service/index.d.ts +68 -2
- package/packages/server/dist/cli/service/index.js +321 -60
- package/packages/server/dist/cli/service/launchd.d.ts +23 -1
- package/packages/server/dist/cli/service/launchd.js +145 -25
- package/packages/server/dist/cli/service/logs.d.ts +39 -0
- package/packages/server/dist/cli/service/logs.js +115 -0
- package/packages/server/dist/cli/service/path-env.d.ts +27 -0
- package/packages/server/dist/cli/service/path-env.js +120 -0
- package/packages/server/dist/cli/service/ready.d.ts +22 -0
- package/packages/server/dist/cli/service/ready.js +30 -0
- package/packages/server/dist/cli/service/spec.d.ts +45 -6
- package/packages/server/dist/cli/service/spec.js +34 -7
- package/packages/server/dist/cli/service/systemd.d.ts +47 -4
- package/packages/server/dist/cli/service/systemd.js +127 -22
- package/packages/server/dist/herdctl.d.ts +19 -0
- package/packages/server/dist/herdctl.js +26 -0
- package/packages/server/dist/management-mcp-server.d.ts +21 -3
- package/packages/server/dist/management-mcp-server.js +4 -13
- package/packages/server/dist/routes/meta.js +56 -3
- package/packages/server/dist/runtime-file.d.ts +26 -0
- package/packages/server/dist/runtime-file.js +91 -0
- package/packages/server/dist/service-log.d.ts +29 -0
- package/packages/server/dist/service-log.js +104 -0
- package/packages/server/dist/start.d.ts +33 -0
- package/packages/server/dist/start.js +126 -8
- package/packages/server/dist/version.d.ts +3 -0
- package/packages/server/dist/version.js +32 -0
- package/packages/server/dist/ws-context.d.ts +9 -0
- package/packages/server/dist/ws-turn.js +1 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{DeleteProjectDialog-BcxJMA1w.js → DeleteProjectDialog-D8oIJ8HC.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-CQlzxsn4.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-DOmTmiVv.js → DiscoverView-o0KCr3aB.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-DWLQqHW0.js → InstanceConfigPage-RSJz7oL3.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-Y25JWlMR.js → MarkdownRenderer-39mafH8b.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-CCI1VWM6.js → ProjectRedirect-Bq7GVguY.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-DzfVhjya.js → ProjectView-BoBIru1g.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-yIyyb2HA.js → ProjectsGrid-mJ4gqbr6.js} +1 -1
- package/packages/web/dist/assets/{RootHome-x0Om9-Fd.js → RootHome-C0kwFyYp.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-wbcMQIsK.js → Surfaces-MkhXlB8z.js} +1 -1
- package/packages/web/dist/assets/{arc-tWN7FYIq.js → arc-DExlwAwO.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Ccj6Vlyy.js → architectureDiagram-3BPJPVTR-CZly3W79.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-B7iGXwqY.js → blockDiagram-GPEHLZMM-AgES73i3.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-SkLKFSso.js → c4Diagram-AAUBKEIU-N9mD8Pfk.js} +1 -1
- package/packages/web/dist/assets/channel-ExsAG7qT.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-BrS8C-YK.js → chunk-2J33WTMH-Dsx_kfUl.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-BUR6nG8w.js → chunk-4BX2VUAB-BQjqcjPj.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-BRBNJb-u.js → chunk-55IACEB6-CCxpYQpY.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-DKeNPTXz.js → chunk-727SXJPM-DpFRdnUa.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BOS1K-DX.js → chunk-AQP2D5EJ-D2BAlzoO.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-D3PZL8fo.js → chunk-FMBD7UC4-BFaMRzUF.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-DA9Y77Zj.js → chunk-ND2GUHAM-B9NGJ9xZ.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-wPph-weq.js → chunk-QZHKN3VN-CF1IHyMS.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-si7g9okq.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-si7g9okq.js +2 -0
- package/packages/web/dist/assets/{core-I1PQdGMJ.js → core-XjElsvQ7.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BA7N2vVK.js → cose-bilkent-S5V4N54A-CF972Flx.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-DKpON5p2.js → dagre-BM42HDAG-DsneI8YE.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-Db0I-eEL.js → diagram-2AECGRRQ-BHyrVzIk.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-BEuqOTfn.js → diagram-5GNKFQAL-Dj4q8xEi.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-CnyX_0Xk.js → diagram-KO2AKTUF-BHe3Puv_.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-CYvknzDd.js → diagram-LMA3HP47-wzt6G2FE.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-DlgKxbYz.js → diagram-OG6HWLK6-CZMbMRJq.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-Cvob-HVs.js → documentTitle-BwTIA8rI.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-BkO5JmiM.js → erDiagram-TEJ5UH35-Dtu14BL1.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DG0o_zc1.js → flowDiagram-I6XJVG4X-T34ql9j0.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-Bs71No2S.js → ganttDiagram-6RSMTGT7-DQLOeCXK.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CxODhRKd.js → gitGraphDiagram-PVQCEYII-Bk11zea6.js} +1 -1
- package/packages/web/dist/assets/{index-C4tsPSvG.js → index-CvaWErFv.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-C412TQaw.js → infoDiagram-5YYISTIA-CsE83lu0.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DUlcZXQQ.js → ishikawaDiagram-YF4QCWOH-DNVgA93n.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-iL0klVhn.js → journeyDiagram-JHISSGLW-CLpSNjM3.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-CO02Zl_Q.js → kanban-definition-UN3LZRKU-BRzuXcKr.js} +1 -1
- package/packages/web/dist/assets/{linear-DtmLY8AG.js → linear-BxOawONt.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-Dvzxug80.js → mermaid.core-Dvs1QCcz.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-q950AAWL.js → mindmap-definition-RKZ34NQL-Dmt7pYwF.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BVrv8A-H.js → pieDiagram-4H26LBE5-BHiZaJD-.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Bp5D2MFq.js → quadrantDiagram-W4KKPZXB-U0A1noRE.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-yZfQU4Ix.js → requirementDiagram-4Y6WPE33-CZLsr6yi.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-5R8_OZir.js → sankeyDiagram-5OEKKPKP-BTlLGILU.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-C3A9rinG.js → sequenceDiagram-3UESZ5HK-DpSS_Gb1.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-Cshrt_XB.js → stateDiagram-AJRCARHV-D9srE-tV.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-AxWqsDh-.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-kNkiddCX.js → timeline-definition-PNZ67QCA-BYQ5CKab.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-BiNJm17g.js → vennDiagram-CIIHVFJN-B4W2CtSV.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-CjExbCuX.js → wardley-L42UT6IY--XUV7GvD.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CMn3z-G2.js → wardleyDiagram-YWT4CUSO-Bl2VIEGY.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-B-UhnooW.js → xychartDiagram-2RQKCTM6-Bj2H9Rcf.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/DiscoverPage-CSPRrFVk.js +0 -2
- package/packages/web/dist/assets/channel-B2NSmlBB.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B3tLFeZC.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B3tLFeZC.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DgiazF78.js +0 -2
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// From edspencer/cli-contract reference/busy-client.ts @ SPEC 0.1; local changes: `.ts` import specifiers → `.js`
|
|
2
|
+
// and `../runtime-file.js` (the writer lives server-side, next to the boot path that writes the file); `EXIT.OK` /
|
|
3
|
+
// `EXIT.BUSY` / `EXIT.NOT_RUNNING` / `EXIT.FAILED` → Paddock's lowercase `EXIT.ok` / `busy` / `notRunning` /
|
|
4
|
+
// `failure` (see `exit-codes.ts`, which wave C4 re-syncs); and `hint` widened from a string to
|
|
5
|
+
// `string | ((result) => string | undefined)`, with {@link runBusyCommand} printing it too — that is what carries
|
|
6
|
+
// Paddock's keeper-chat trap (ROADMAP "Busy, per tool"), where the line to print depends on WHICH counts are
|
|
7
|
+
// non-zero, so a fixed string could not say it.
|
|
8
|
+
//
|
|
9
|
+
// The CLI's side of the busy check (SPEC §4): find the running instance through `<home>/runtime.json`, ask its
|
|
10
|
+
// `/api/busy`, and map the answer to exit 0 (idle), 75 (busy) or 3 (nothing answers), with §4's messages.
|
|
11
|
+
// Used by `<tool> busy`, and before `service stop`, `service restart` and `update`. Node built-ins only.
|
|
12
|
+
import { EXIT } from "./exit-codes.js";
|
|
13
|
+
import { readLiveRuntimeFile } from "../runtime-file.js";
|
|
14
|
+
/**
|
|
15
|
+
* Asks the running instance whether it's busy. There's no fallback to a default port: an instance that's running
|
|
16
|
+
* wrote `runtime.json`, and guessing a port would ask whatever else is listening there.
|
|
17
|
+
*/
|
|
18
|
+
export async function checkBusy(o) {
|
|
19
|
+
const base = o.url ?? readLiveRuntimeFile(o.home)?.url;
|
|
20
|
+
if (!base)
|
|
21
|
+
return { state: "not-running" };
|
|
22
|
+
const url = new URL("/api/busy", base).toString();
|
|
23
|
+
const doFetch = o.fetch ?? globalThis.fetch;
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await doFetch(url, {
|
|
27
|
+
signal: AbortSignal.timeout(o.timeoutMs ?? 3000),
|
|
28
|
+
headers: { accept: "application/json" },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Refused, reset or timed out: nothing is answering, which §4 calls not running.
|
|
33
|
+
return { state: "not-running" };
|
|
34
|
+
}
|
|
35
|
+
if (!res.ok)
|
|
36
|
+
return { state: "unknown", url, detail: `HTTP ${res.status}` };
|
|
37
|
+
let body;
|
|
38
|
+
try {
|
|
39
|
+
body = await res.json();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { state: "unknown", url, detail: "the answer isn't JSON" };
|
|
43
|
+
}
|
|
44
|
+
if (!isBusyAnswer(body))
|
|
45
|
+
return { state: "unknown", url, detail: "the answer has no boolean `busy`" };
|
|
46
|
+
return { state: body.busy ? "busy" : "idle", url, answer: body };
|
|
47
|
+
}
|
|
48
|
+
function isBusyAnswer(v) {
|
|
49
|
+
if (typeof v !== "object" || v === null || typeof v.busy !== "boolean")
|
|
50
|
+
return false;
|
|
51
|
+
const counts = v.counts;
|
|
52
|
+
if (counts === undefined)
|
|
53
|
+
return true;
|
|
54
|
+
return (typeof counts === "object" &&
|
|
55
|
+
counts !== null &&
|
|
56
|
+
Object.values(counts).every((n) => Number.isInteger(n) && n >= 0));
|
|
57
|
+
}
|
|
58
|
+
/** `2 chats and 1 job are running`, from the non-zero counts; '' if there are none. */
|
|
59
|
+
export function describeCounts(counts = {}, nouns = {}) {
|
|
60
|
+
const parts = Object.entries(counts)
|
|
61
|
+
.filter(([, n]) => n > 0)
|
|
62
|
+
.map(([kind, n]) => {
|
|
63
|
+
const [one, many] = nouns[kind] ?? [kind, `${kind}s`];
|
|
64
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
65
|
+
});
|
|
66
|
+
if (parts.length === 0)
|
|
67
|
+
return "";
|
|
68
|
+
const list = parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
|
|
69
|
+
const total = Object.values(counts).reduce((a, n) => a + (n > 0 ? n : 0), 0);
|
|
70
|
+
return `${list} ${total === 1 ? "is" : "are"} running`;
|
|
71
|
+
}
|
|
72
|
+
/** The sentence for a person (SPEC §4): `Paddock is busy: 2 chats are running.` */
|
|
73
|
+
export function busyMessage(name, result, nouns = {}) {
|
|
74
|
+
switch (result.state) {
|
|
75
|
+
case "not-running":
|
|
76
|
+
return `${name} isn't running.`;
|
|
77
|
+
case "idle":
|
|
78
|
+
return `${name} isn't busy.`;
|
|
79
|
+
case "busy": {
|
|
80
|
+
const what = describeCounts(result.answer.counts, nouns);
|
|
81
|
+
return what ? `${name} is busy: ${what}.` : `${name} is busy.`;
|
|
82
|
+
}
|
|
83
|
+
case "unknown":
|
|
84
|
+
return `Something answered at ${result.url}, but not as ${name}'s busy check (${result.detail}).`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** 0 idle, 75 busy, 3 nothing answers, 1 an answer that isn't a busy answer. */
|
|
88
|
+
export function busyExitCode(result) {
|
|
89
|
+
switch (result.state) {
|
|
90
|
+
case "idle":
|
|
91
|
+
return EXIT.ok;
|
|
92
|
+
case "busy":
|
|
93
|
+
return EXIT.busy;
|
|
94
|
+
case "not-running":
|
|
95
|
+
return EXIT.notRunning;
|
|
96
|
+
case "unknown":
|
|
97
|
+
return EXIT.failure;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** What `<tool> busy --json` prints: valid against `schemas/busy.schema.json`. */
|
|
101
|
+
export function busyJson(result) {
|
|
102
|
+
if (result.state === "not-running")
|
|
103
|
+
return { running: false, busy: false };
|
|
104
|
+
if (result.state === "unknown")
|
|
105
|
+
return { running: true, busy: false, url: base(result.url), error: result.detail };
|
|
106
|
+
return {
|
|
107
|
+
running: true,
|
|
108
|
+
busy: result.answer.busy,
|
|
109
|
+
url: base(result.url),
|
|
110
|
+
...(result.answer.version !== undefined ? { version: result.answer.version } : {}),
|
|
111
|
+
counts: result.answer.counts ?? {},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const base = (busyUrl) => busyUrl.replace(/\/api\/busy$/, "");
|
|
115
|
+
function resolveHint(hint, result) {
|
|
116
|
+
return typeof hint === "function" ? hint(result) : hint;
|
|
117
|
+
}
|
|
118
|
+
/** `<tool> busy [--json]`. Returns the exit code; the caller sets `process.exitCode`. */
|
|
119
|
+
export async function runBusyCommand(o) {
|
|
120
|
+
const result = await checkBusy(o);
|
|
121
|
+
const message = busyMessage(o.name, result, o.nouns);
|
|
122
|
+
o.out.result(busyJson(result));
|
|
123
|
+
if (result.state === "unknown")
|
|
124
|
+
o.out.error(message);
|
|
125
|
+
else
|
|
126
|
+
o.out.say(message);
|
|
127
|
+
// LOCAL CHANGE: the hint rides along with a busy answer here too, not only on the guard's refusal — someone who
|
|
128
|
+
// ran `paddock busy` from inside a chat needs the same sentence as someone whose `service stop` was refused.
|
|
129
|
+
const hint = result.state === "busy" ? resolveHint(o.hint, result) : undefined;
|
|
130
|
+
if (hint !== undefined)
|
|
131
|
+
o.out.say(hint);
|
|
132
|
+
return busyExitCode(result);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The check before `service stop`, `service restart` and `update` (SPEC §4). Returns null to go ahead, or 75 after
|
|
136
|
+
* printing the refusal. With `--force` it prints the same message, prefixed `Overriding:`, and goes ahead.
|
|
137
|
+
* Nothing answering goes ahead: there's nothing to cut short.
|
|
138
|
+
*/
|
|
139
|
+
export async function guardBusy(o) {
|
|
140
|
+
const result = await checkBusy(o);
|
|
141
|
+
if (result.state !== "busy")
|
|
142
|
+
return null;
|
|
143
|
+
const message = busyMessage(o.name, result, o.nouns);
|
|
144
|
+
if (o.force) {
|
|
145
|
+
o.out.warn(`Overriding: ${message}`);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const hint = resolveHint(o.hint, result) ?? "Try again once it has finished, or use --force.";
|
|
149
|
+
return o.out.error(`${message} ${hint}`, EXIT.busy);
|
|
150
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI contract's exit codes (SPEC §2), as constants.
|
|
3
|
+
*
|
|
4
|
+
* From edspencer/cli-contract SPEC.md §2 @ 0.1. Written here rather than copied
|
|
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
|
+
*/
|
|
29
|
+
export declare const EXIT: {
|
|
30
|
+
/** Done. Running and answering. Up to date. Not busy. */
|
|
31
|
+
readonly ok: 0;
|
|
32
|
+
/** Failed — the catch-all. */
|
|
33
|
+
readonly failure: 1;
|
|
34
|
+
/** Usage: an unknown command or flag, or a bad value. Usage follows the message. */
|
|
35
|
+
readonly usage: 2;
|
|
36
|
+
/** Not running: installed, but not running or not answering. */
|
|
37
|
+
readonly notRunning: 3;
|
|
38
|
+
/** Not installed: there is no service unit. */
|
|
39
|
+
readonly notInstalled: 4;
|
|
40
|
+
/** This install cannot update itself; the right command was printed. */
|
|
41
|
+
readonly cannotSelfUpdate: 5;
|
|
42
|
+
/** The update failed and was rolled back; the old version is running again. */
|
|
43
|
+
readonly rolledBack: 6;
|
|
44
|
+
/** The update failed and could not be rolled back; a person is needed. */
|
|
45
|
+
readonly needsHuman: 7;
|
|
46
|
+
/** Busy: refused, and why was printed. Try later, or use `--force`. */
|
|
47
|
+
readonly busy: 75;
|
|
48
|
+
};
|
|
49
|
+
export type ExitCode = (typeof EXIT)[keyof typeof EXIT];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI contract's exit codes (SPEC §2), as constants.
|
|
3
|
+
*
|
|
4
|
+
* From edspencer/cli-contract SPEC.md §2 @ 0.1. Written here rather than copied
|
|
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
|
+
*/
|
|
29
|
+
export const EXIT = {
|
|
30
|
+
/** Done. Running and answering. Up to date. Not busy. */
|
|
31
|
+
ok: 0,
|
|
32
|
+
/** Failed — the catch-all. */
|
|
33
|
+
failure: 1,
|
|
34
|
+
/** Usage: an unknown command or flag, or a bad value. Usage follows the message. */
|
|
35
|
+
usage: 2,
|
|
36
|
+
/** Not running: installed, but not running or not answering. */
|
|
37
|
+
notRunning: 3,
|
|
38
|
+
/** Not installed: there is no service unit. */
|
|
39
|
+
notInstalled: 4,
|
|
40
|
+
/** This install cannot update itself; the right command was printed. */
|
|
41
|
+
cannotSelfUpdate: 5,
|
|
42
|
+
/** The update failed and was rolled back; the old version is running again. */
|
|
43
|
+
rolledBack: 6,
|
|
44
|
+
/** The update failed and could not be rolled back; a person is needed. */
|
|
45
|
+
needsHuman: 7,
|
|
46
|
+
/** Busy: refused, and why was printed. Try later, or use `--force`. */
|
|
47
|
+
busy: 75,
|
|
48
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface OutputStream {
|
|
2
|
+
write(chunk: string): unknown;
|
|
3
|
+
isTTY?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface OutputOptions {
|
|
6
|
+
/** The bin, for `<tool>: <message>`. */
|
|
7
|
+
tool: string;
|
|
8
|
+
/** `--json` was given: stdout carries exactly one JSON document, and prose goes to stderr. */
|
|
9
|
+
json?: boolean;
|
|
10
|
+
stdout?: OutputStream;
|
|
11
|
+
stderr?: OutputStream;
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether to colour what goes to this stream (SPEC §3).
|
|
16
|
+
*
|
|
17
|
+
* - `NO_COLOR` set to anything but the empty string: never (https://no-color.org).
|
|
18
|
+
* - `FORCE_COLOR` set: `0` or `false` means never, anything else always.
|
|
19
|
+
* - Otherwise only on a TTY, and not when `TERM=dumb`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useColor(stream: OutputStream, env?: NodeJS.ProcessEnv): boolean;
|
|
22
|
+
export interface Paint {
|
|
23
|
+
bold(s: string): string;
|
|
24
|
+
dim(s: string): string;
|
|
25
|
+
red(s: string): string;
|
|
26
|
+
green(s: string): string;
|
|
27
|
+
yellow(s: string): string;
|
|
28
|
+
}
|
|
29
|
+
/** Colour functions that do nothing when `on` is false. */
|
|
30
|
+
export declare function paint(on: boolean): Paint;
|
|
31
|
+
export interface Output {
|
|
32
|
+
readonly json: boolean;
|
|
33
|
+
/** Colour for prose on stdout (never with `--json`, which has no prose on stdout). */
|
|
34
|
+
readonly color: Paint;
|
|
35
|
+
/** Colour for stderr. */
|
|
36
|
+
readonly errColor: Paint;
|
|
37
|
+
/** Prose for a person: stdout, or stderr under `--json`. */
|
|
38
|
+
say(line: string): void;
|
|
39
|
+
/** A warning on stderr, always. */
|
|
40
|
+
warn(line: string): void;
|
|
41
|
+
/** `<tool>: <message>` on stderr. Returns `code` (default 1), for `return out.error(…)`. Sets nothing itself. */
|
|
42
|
+
error(message: string, code?: number): number;
|
|
43
|
+
/** The one JSON document `--json` promises. Prints nothing without `--json`. */
|
|
44
|
+
result(doc: unknown): void;
|
|
45
|
+
}
|
|
46
|
+
/** One place for a command's output. Make it once per command, after the flags are parsed. */
|
|
47
|
+
export declare function createOutput(o: OutputOptions): Output;
|
|
48
|
+
/**
|
|
49
|
+
* A closed pipe (`<tool> … | head -1`) exits quietly (SPEC §2): without this, Node throws EPIPE from a later write
|
|
50
|
+
* and prints a stack trace. Call once, at startup.
|
|
51
|
+
*/
|
|
52
|
+
export declare function exitQuietlyOnEpipe(stream?: NodeJS.WriteStream, exit?: (code: number) => void): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// From edspencer/cli-contract reference/output.ts @ SPEC 0.1; local changes: `./exit-codes.ts` → `./exit-codes.js`
|
|
2
|
+
// (Paddock compiles to ESM-with-extensions, not a TS runtime); `EXIT.FAILED`/`EXIT.USAGE` → Paddock's lowercase
|
|
3
|
+
// `EXIT.failure`/`EXIT.usage`, because Paddock's own `exit-codes.ts` predates the reference module and wave C4
|
|
4
|
+
// re-syncs that file; and the reference's `UsageError` / `reportUsageError` are NOT copied, because `cli/args.ts`
|
|
5
|
+
// already has a `UsageError` (a `CliError` subclass carrying its own code) that the entrypoint already reports.
|
|
6
|
+
//
|
|
7
|
+
// A CLI's output, the way SPEC §2 and §3 say: errors as `<tool>: <message>` on stderr, colour only on a TTY (honouring
|
|
8
|
+
// NO_COLOR and FORCE_COLOR), and with `--json`, one JSON document on stdout and every word of prose on stderr.
|
|
9
|
+
// Node built-ins only.
|
|
10
|
+
import { EXIT } from "./exit-codes.js";
|
|
11
|
+
/**
|
|
12
|
+
* Whether to colour what goes to this stream (SPEC §3).
|
|
13
|
+
*
|
|
14
|
+
* - `NO_COLOR` set to anything but the empty string: never (https://no-color.org).
|
|
15
|
+
* - `FORCE_COLOR` set: `0` or `false` means never, anything else always.
|
|
16
|
+
* - Otherwise only on a TTY, and not when `TERM=dumb`.
|
|
17
|
+
*/
|
|
18
|
+
export function useColor(stream, env = process.env) {
|
|
19
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== "")
|
|
20
|
+
return false;
|
|
21
|
+
if (env.FORCE_COLOR !== undefined)
|
|
22
|
+
return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "false";
|
|
23
|
+
if (env.TERM === "dumb")
|
|
24
|
+
return false;
|
|
25
|
+
return stream.isTTY === true;
|
|
26
|
+
}
|
|
27
|
+
const sgr = (open, close) => (s) => `\u001b[${open}m${s}\u001b[${close}m`;
|
|
28
|
+
const plain = (s) => s;
|
|
29
|
+
/** Colour functions that do nothing when `on` is false. */
|
|
30
|
+
export function paint(on) {
|
|
31
|
+
return on
|
|
32
|
+
? {
|
|
33
|
+
bold: sgr(1, 22),
|
|
34
|
+
dim: sgr(2, 22),
|
|
35
|
+
red: sgr(31, 39),
|
|
36
|
+
green: sgr(32, 39),
|
|
37
|
+
yellow: sgr(33, 39),
|
|
38
|
+
}
|
|
39
|
+
: { bold: plain, dim: plain, red: plain, green: plain, yellow: plain };
|
|
40
|
+
}
|
|
41
|
+
/** One place for a command's output. Make it once per command, after the flags are parsed. */
|
|
42
|
+
export function createOutput(o) {
|
|
43
|
+
const stdout = o.stdout ?? process.stdout;
|
|
44
|
+
const stderr = o.stderr ?? process.stderr;
|
|
45
|
+
const env = o.env ?? process.env;
|
|
46
|
+
const json = o.json === true;
|
|
47
|
+
let printed = false;
|
|
48
|
+
const line = (s) => (s.endsWith("\n") ? s : `${s}\n`);
|
|
49
|
+
return {
|
|
50
|
+
json,
|
|
51
|
+
color: paint(!json && useColor(stdout, env)),
|
|
52
|
+
errColor: paint(useColor(stderr, env)),
|
|
53
|
+
say(s) {
|
|
54
|
+
(json ? stderr : stdout).write(line(s));
|
|
55
|
+
},
|
|
56
|
+
warn(s) {
|
|
57
|
+
stderr.write(line(s));
|
|
58
|
+
},
|
|
59
|
+
error(message, code = EXIT.failure) {
|
|
60
|
+
stderr.write(line(`${o.tool}: ${message}`));
|
|
61
|
+
return code;
|
|
62
|
+
},
|
|
63
|
+
result(doc) {
|
|
64
|
+
if (!json)
|
|
65
|
+
return;
|
|
66
|
+
if (printed)
|
|
67
|
+
throw new Error("--json prints one document; result() was called twice");
|
|
68
|
+
printed = true;
|
|
69
|
+
stdout.write(`${JSON.stringify(doc, null, 2)}\n`);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A closed pipe (`<tool> … | head -1`) exits quietly (SPEC §2): without this, Node throws EPIPE from a later write
|
|
75
|
+
* and prints a stack trace. Call once, at startup.
|
|
76
|
+
*/
|
|
77
|
+
export function exitQuietlyOnEpipe(stream = process.stdout, exit = (c) => process.exit(c)) {
|
|
78
|
+
stream.on("error", (err) => {
|
|
79
|
+
if (err.code === "EPIPE")
|
|
80
|
+
exit(typeof process.exitCode === "number" ? process.exitCode : 0);
|
|
81
|
+
else
|
|
82
|
+
throw err;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -23,8 +23,10 @@ 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, USAGE, SERVICE_USAGE, CONFIG_USAGE, parseCommand, nodeVersionProblem, explainListenError, } from "./args.js";
|
|
27
|
-
import {
|
|
26
|
+
import { CliError, UsageError, USAGE, USAGE_SYNOPSIS, SERVICE_USAGE, CONFIG_USAGE, BUSY_USAGE, parseCommand, nodeVersionProblem, explainListenError, } from "./args.js";
|
|
27
|
+
import { EXIT } from "./exit-codes.js";
|
|
28
|
+
import { runService, safeHomeDir, realpathOr } from "./service/index.js";
|
|
29
|
+
import { PADDOCK_NAME, PADDOCK_VERSION } from "../version.js";
|
|
28
30
|
/**
|
|
29
31
|
* This module's own file and directory: `<…>/packages/server/dist/cli/`.
|
|
30
32
|
*
|
|
@@ -62,18 +64,27 @@ function findUp(rel) {
|
|
|
62
64
|
}
|
|
63
65
|
/** Nearest ancestor holding a `package.json` — `packages/server` in both layouts. */
|
|
64
66
|
const packageRoot = findUp("package.json") ?? path.resolve(moduleDir, "../..");
|
|
65
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Report a failure, and hand back the code to exit with (SPEC §2, §3).
|
|
69
|
+
*
|
|
70
|
+
* Two things here are the contract rather than style. Errors go to **stderr**
|
|
71
|
+
* as `paddock: <message>`, so a `--json` document on stdout stays the only
|
|
72
|
+
* thing on stdout. And the code is RETURNED rather than passed to
|
|
73
|
+
* `process.exit()`: exiting mid-write truncates piped output, which is how a
|
|
74
|
+
* `paddock service status | jq` loses the end of its document. `main` assigns
|
|
75
|
+
* `process.exitCode` and lets the process end on its own.
|
|
76
|
+
*
|
|
77
|
+
* A usage error also prints the synopsis after the message, because the spec
|
|
78
|
+
* says the usage follows the message — the short one, not {@link USAGE}'s
|
|
79
|
+
* thousand words, which would bury the line that says what was wrong.
|
|
80
|
+
*/
|
|
81
|
+
function fail(message, code = EXIT.failure) {
|
|
66
82
|
console.error(`paddock: ${message}`);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
const raw = fs.readFileSync(path.join(packageRoot, "package.json"), "utf8");
|
|
72
|
-
return JSON.parse(raw).version ?? "unknown";
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return "unknown";
|
|
83
|
+
if (code === EXIT.usage) {
|
|
84
|
+
console.error("");
|
|
85
|
+
console.error(USAGE_SYNOPSIS);
|
|
76
86
|
}
|
|
87
|
+
return code;
|
|
77
88
|
}
|
|
78
89
|
/**
|
|
79
90
|
* Put our bundled `claude` on PATH for child processes.
|
|
@@ -121,7 +132,10 @@ function addBundledBinsToPath() {
|
|
|
121
132
|
function noteFirstRun(dataDir) {
|
|
122
133
|
if (fs.existsSync(dataDir))
|
|
123
134
|
return;
|
|
124
|
-
|
|
135
|
+
// stderr, because stdout's first line belongs to the banner (SPEC §3). This
|
|
136
|
+
// block is prose for a person and it only appears on a first run — which is
|
|
137
|
+
// exactly the run a conformance check or a script reading the URL performs.
|
|
138
|
+
console.error([
|
|
125
139
|
"",
|
|
126
140
|
` Welcome to Paddock. Creating a new instance in ${dataDir}`,
|
|
127
141
|
" Projects, chats and settings persist there between runs.",
|
|
@@ -161,12 +175,17 @@ async function service(command) {
|
|
|
161
175
|
// which `main` has already handled. Typed rather than asserted.
|
|
162
176
|
if (action === undefined) {
|
|
163
177
|
console.log(SERVICE_USAGE);
|
|
164
|
-
return;
|
|
178
|
+
return EXIT.ok;
|
|
165
179
|
}
|
|
166
180
|
try {
|
|
167
|
-
await runService(action, opts, {
|
|
181
|
+
return await runService(action, opts, {
|
|
168
182
|
platform: process.platform,
|
|
169
|
-
|
|
183
|
+
// Realpath'd (SPEC §7). `process.execPath` under a version manager is
|
|
184
|
+
// routinely a symlink into a per-shell directory — fnm's
|
|
185
|
+
// `fnm_multishells/<pid>_<ts>/bin/node` is deleted when that terminal
|
|
186
|
+
// exits — so a unit naming the symlink stops working when the tab closes,
|
|
187
|
+
// at some later login, with nothing in the logs because it never ran.
|
|
188
|
+
nodePath: realpathOr(process.execPath),
|
|
170
189
|
scriptPath: entryScript,
|
|
171
190
|
packageRoot,
|
|
172
191
|
homeDir: safeHomeDir(),
|
|
@@ -180,10 +199,12 @@ async function service(command) {
|
|
|
180
199
|
});
|
|
181
200
|
}
|
|
182
201
|
catch (err) {
|
|
202
|
+
// A CliError carries its own code: an unsupported platform or an npx
|
|
203
|
+
// refusal is a failure (1), a misspelled flag is usage (2).
|
|
183
204
|
if (err instanceof CliError)
|
|
184
|
-
fail(err.message);
|
|
205
|
+
return fail(err.message, err.exitCode);
|
|
185
206
|
if (err instanceof Error)
|
|
186
|
-
fail(err.message);
|
|
207
|
+
return fail(err.message);
|
|
187
208
|
throw err;
|
|
188
209
|
}
|
|
189
210
|
}
|
|
@@ -214,12 +235,13 @@ async function config(command) {
|
|
|
214
235
|
// which `main` has already handled. Typed rather than asserted.
|
|
215
236
|
if (action === undefined) {
|
|
216
237
|
console.log(CONFIG_USAGE);
|
|
217
|
-
return;
|
|
238
|
+
return EXIT.ok;
|
|
218
239
|
}
|
|
219
240
|
process.env.PADDOCK_DATA_DIR = resolveDataDir(opts);
|
|
220
241
|
const { runConfig } = await import("./config.js");
|
|
221
242
|
try {
|
|
222
243
|
await runConfig(action, opts);
|
|
244
|
+
return EXIT.ok;
|
|
223
245
|
}
|
|
224
246
|
catch (err) {
|
|
225
247
|
// A config that will not load reaches here as a CliError already carrying
|
|
@@ -227,9 +249,9 @@ async function config(command) {
|
|
|
227
249
|
// because it is a bug in this command rather than in the configuration.
|
|
228
250
|
// Same shape as `service` above.
|
|
229
251
|
if (err instanceof CliError)
|
|
230
|
-
fail(err.message);
|
|
252
|
+
return fail(err.message, err.exitCode);
|
|
231
253
|
if (err instanceof Error)
|
|
232
|
-
fail(err.message);
|
|
254
|
+
return fail(err.message);
|
|
233
255
|
throw err;
|
|
234
256
|
}
|
|
235
257
|
}
|
|
@@ -239,29 +261,43 @@ async function main() {
|
|
|
239
261
|
command = parseCommand(process.argv.slice(2));
|
|
240
262
|
}
|
|
241
263
|
catch (err) {
|
|
264
|
+
// Parse failures are usage failures: exit 2, not 1 (SPEC §2).
|
|
265
|
+
if (err instanceof UsageError)
|
|
266
|
+
return fail(err.message, err.exitCode);
|
|
242
267
|
if (err instanceof CliError)
|
|
243
|
-
fail(err.message);
|
|
268
|
+
return fail(err.message, err.exitCode);
|
|
244
269
|
throw err;
|
|
245
270
|
}
|
|
246
271
|
const opts = command.opts;
|
|
247
272
|
if (opts.help) {
|
|
248
|
-
console.log(command.verb === "service"
|
|
249
|
-
|
|
273
|
+
console.log(command.verb === "service"
|
|
274
|
+
? SERVICE_USAGE
|
|
275
|
+
: command.verb === "config"
|
|
276
|
+
? CONFIG_USAGE
|
|
277
|
+
: command.verb === "busy"
|
|
278
|
+
? BUSY_USAGE
|
|
279
|
+
: USAGE);
|
|
280
|
+
return EXIT.ok;
|
|
250
281
|
}
|
|
251
282
|
if (opts.version) {
|
|
252
|
-
|
|
253
|
-
|
|
283
|
+
// Bare: the version and a newline, nothing else (SPEC §1). Scripts parse this.
|
|
284
|
+
console.log(PADDOCK_VERSION);
|
|
285
|
+
return EXIT.ok;
|
|
254
286
|
}
|
|
255
287
|
const problem = nodeVersionProblem(process.versions.node);
|
|
256
288
|
if (problem !== undefined)
|
|
257
|
-
fail(problem);
|
|
258
|
-
if (command.verb === "service")
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
289
|
+
return fail(problem);
|
|
290
|
+
if (command.verb === "service")
|
|
291
|
+
return service(command);
|
|
292
|
+
if (command.verb === "config")
|
|
293
|
+
return config(command);
|
|
294
|
+
// `busy` (SPEC §1, §4). Nothing is imported from the server for this: the
|
|
295
|
+
// answer comes off the wire, and `cli/busy.ts` is Node built-ins only — which
|
|
296
|
+
// is what keeps a question meant for a Watchtower hook from loading Fastify,
|
|
297
|
+
// herdctl and the project layer to ask it.
|
|
298
|
+
if (command.verb === "busy") {
|
|
299
|
+
const { runBusy } = await import("./busy-command.js");
|
|
300
|
+
return runBusy(resolveDataDir(opts), opts.json === true);
|
|
265
301
|
}
|
|
266
302
|
addBundledBinsToPath();
|
|
267
303
|
// NOTHING here reads `process.cwd()`, and that is the point (#798). `--here`
|
|
@@ -330,9 +366,14 @@ async function main() {
|
|
|
330
366
|
await start();
|
|
331
367
|
}
|
|
332
368
|
catch (err) {
|
|
333
|
-
fail(explainListenError(err, host, port));
|
|
369
|
+
return fail(explainListenError(err, host, port));
|
|
334
370
|
}
|
|
335
371
|
const url = `http://${host}:${port}`;
|
|
372
|
+
// The contract's banner line, FIRST on stdout and in exactly this shape:
|
|
373
|
+
// `<Name> <version>: <url>` (SPEC §3). It is parsed — by the conformance
|
|
374
|
+
// runner, and by anything that wants the URL without guessing the port — so
|
|
375
|
+
// the friendly block below it stays, but below it.
|
|
376
|
+
console.log(`${PADDOCK_NAME} ${PADDOCK_VERSION}: ${url}`);
|
|
336
377
|
// Still name the data dir, even though it no longer varies with cwd: "which
|
|
337
378
|
// instance am I looking at" is a real question the moment anyone passes
|
|
338
379
|
// `--data-dir`, and the answer should be readable off the two lines the tool
|
|
@@ -346,8 +387,28 @@ async function main() {
|
|
|
346
387
|
].join("\n"));
|
|
347
388
|
if (opts.open)
|
|
348
389
|
openBrowser(url);
|
|
390
|
+
// 0, but NOT yet: a foreground serve keeps running because the server holds
|
|
391
|
+
// the event loop open. `process.exitCode` is set and never acted on until the
|
|
392
|
+
// listener closes, which is what SIGTERM does (start.ts).
|
|
393
|
+
return EXIT.ok;
|
|
349
394
|
}
|
|
350
|
-
|
|
395
|
+
/**
|
|
396
|
+
* A closed pipe exits quietly (SPEC §2).
|
|
397
|
+
*
|
|
398
|
+
* `paddock service logs | head -5` closes the pipe under us, and Node's default
|
|
399
|
+
* is an unhandled EPIPE with a stack trace — printed to the terminal, after the
|
|
400
|
+
* output the user asked for, about a thing that is not a problem.
|
|
401
|
+
*/
|
|
402
|
+
process.stdout.on("error", (err) => {
|
|
403
|
+
if (err.code === "EPIPE")
|
|
404
|
+
process.exit(0);
|
|
405
|
+
});
|
|
406
|
+
main().then((code) => {
|
|
407
|
+
// `process.exitCode`, never `process.exit()` (SPEC §2): exiting mid-write
|
|
408
|
+
// truncates piped output, which is how `paddock config show --json | jq`
|
|
409
|
+
// loses the end of a large document.
|
|
410
|
+
process.exitCode = code;
|
|
411
|
+
}, (err) => {
|
|
351
412
|
console.error("fatal:", err);
|
|
352
|
-
process.
|
|
413
|
+
process.exitCode = EXIT.failure;
|
|
353
414
|
});
|