@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
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paddock doctor [--json] [--offline]` (CLI contract SPEC §1).
|
|
3
|
+
*
|
|
4
|
+
* One `✓` / `!` / `✗` line a check (`-` for one that doesn't apply here), with a
|
|
5
|
+
* `fix:` line under every failure and warning. Exit 1 if any check failed;
|
|
6
|
+
* warnings don't change the code.
|
|
7
|
+
*
|
|
8
|
+
* The contract's eight come first, with the ids every tool uses so a script can
|
|
9
|
+
* read any of them: `node-engines`, `install-method`, `unit`,
|
|
10
|
+
* `running-version`, `port`, `linger`, `data-home` and `update`. Then Paddock's
|
|
11
|
+
* own: the config loads (`config`), the projects folder (`projects`), a Claude
|
|
12
|
+
* login it can use (`claude-login`) and a `claude` on PATH (`claude-cli`).
|
|
13
|
+
*
|
|
14
|
+
* It only reads, with one exception: like `update --check`, the `update` check
|
|
15
|
+
* writes what it found to `<data-dir>/update-check.json`, so `service status`
|
|
16
|
+
* can repeat it (SPEC §3) — but only into a data dir that's already there, so
|
|
17
|
+
* `doctor` never creates an instance. It asks the npm registry only because a
|
|
18
|
+
* person ran it, and `--offline` doesn't. It never prints a credential: the
|
|
19
|
+
* login check says which KIND of login it found.
|
|
20
|
+
*
|
|
21
|
+
* The service checks reuse `service status`'s own report (`buildStatus`), so the
|
|
22
|
+
* two cannot disagree about what is installed and what is running.
|
|
23
|
+
*/
|
|
24
|
+
import fs from "node:fs";
|
|
25
|
+
import os from "node:os";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { MIN_NODE_MAJOR } from "./args.js";
|
|
28
|
+
import { EXIT } from "./exit-codes.js";
|
|
29
|
+
import { buildStatus, serviceBackend, } from "./service/index.js";
|
|
30
|
+
import { defaultDataDir, DEFAULT_PORT } from "./service/spec.js";
|
|
31
|
+
import { missingPaths, which } from "./service/path-env.js";
|
|
32
|
+
import { identifyPaddock as identify, isListening } from "./service/ready.js";
|
|
33
|
+
import { describeInstall, detectInstall, updateCommand } from "./update/install-method.js";
|
|
34
|
+
import { npmConfigGet, npmGlobalRoot } from "./update/package-manager.js";
|
|
35
|
+
import { RegistryError, resolveRegistry, resolveTarget } from "./update/registry.js";
|
|
36
|
+
import { compareVersions, isPrerelease, satisfiesEngines } from "./update/semver.js";
|
|
37
|
+
import { updateNotice, writeUpdateCheck } from "./update/update-check.js";
|
|
38
|
+
import { readLiveRuntimeFile } from "../runtime-file.js";
|
|
39
|
+
import { PADDOCK_NAME, PADDOCK_VERSION } from "../version.js";
|
|
40
|
+
import { PADDOCK_IMAGE, PADDOCK_PACKAGE, paddockChangelogUrl, } from "./update-command.js";
|
|
41
|
+
const MARK = { pass: "✓", warn: "!", fail: "✗", skip: "-" };
|
|
42
|
+
/** The variables that log Claude Code in without a file (the same list `claude-home.ts` checks at boot). */
|
|
43
|
+
const TOKEN_ENV_VARS = ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
|
|
44
|
+
/** `/home/me/x` → `~/x`, for lines a person reads. */
|
|
45
|
+
function tildify(p, home) {
|
|
46
|
+
if (p === home)
|
|
47
|
+
return "~";
|
|
48
|
+
return p.startsWith(`${home}${path.sep}`) ? `~${p.slice(home.length)}` : p;
|
|
49
|
+
}
|
|
50
|
+
export async function diagnose(i) {
|
|
51
|
+
const checks = [];
|
|
52
|
+
const add = (c) => void checks.push(c);
|
|
53
|
+
const ctx = i.ctx;
|
|
54
|
+
const env = i.env ?? process.env;
|
|
55
|
+
const t = (p) => tildify(p, ctx.homeDir);
|
|
56
|
+
let notice;
|
|
57
|
+
// ---- how it was installed (SPEC §6). First, because the engines check reads
|
|
58
|
+
// the package root it finds.
|
|
59
|
+
const info = detectInstall({
|
|
60
|
+
packageName: PADDOCK_PACKAGE,
|
|
61
|
+
envPrefix: "PADDOCK",
|
|
62
|
+
entry: i.entry,
|
|
63
|
+
env,
|
|
64
|
+
npmGlobalRoot: i.npmGlobalRoot ?? (() => npmGlobalRoot({ env })),
|
|
65
|
+
});
|
|
66
|
+
// ---- this Node against the package's `engines`
|
|
67
|
+
const nodeVersion = i.nodeVersion ?? process.versions.node;
|
|
68
|
+
const engines = enginesOf(info.packageRoot) ?? `>=${MIN_NODE_MAJOR}`;
|
|
69
|
+
if (satisfiesEngines(nodeVersion, { node: engines }).ok === false) {
|
|
70
|
+
add({
|
|
71
|
+
id: "node-engines",
|
|
72
|
+
status: "fail",
|
|
73
|
+
message: `Node ${nodeVersion} is too old: Paddock ${PADDOCK_VERSION} needs Node ${engines}`,
|
|
74
|
+
fix: `install Node ${engines} (nvm, fnm, Homebrew, nodejs.org), then run paddock with it`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
add({ id: "node-engines", status: "pass", message: `Node ${nodeVersion} (Paddock needs ${engines})` });
|
|
79
|
+
}
|
|
80
|
+
const how = describeInstall(info, PADDOCK_PACKAGE);
|
|
81
|
+
if (info.canUpdate) {
|
|
82
|
+
add({
|
|
83
|
+
id: "install-method",
|
|
84
|
+
status: "pass",
|
|
85
|
+
message: `Paddock ${PADDOCK_VERSION}, ${how}: \`paddock update\` can update it`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const cmd = updateCommand(info, { packageName: PADDOCK_PACKAGE, image: PADDOCK_IMAGE });
|
|
90
|
+
add({
|
|
91
|
+
id: "install-method",
|
|
92
|
+
status: "warn",
|
|
93
|
+
message: `Paddock ${PADDOCK_VERSION}, ${how}: \`paddock update\` can't update it`,
|
|
94
|
+
fix: `${cmd.command}${cmd.note !== undefined ? ` (${cmd.note})` : ""}`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// ---- the unit: present, its Node and CLI still there, the same install as this CLI
|
|
98
|
+
let backend;
|
|
99
|
+
let report;
|
|
100
|
+
try {
|
|
101
|
+
backend = serviceBackend(ctx);
|
|
102
|
+
report = await buildStatus(ctx, backend);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
add({
|
|
106
|
+
id: "unit",
|
|
107
|
+
status: "skip",
|
|
108
|
+
message: `there's no service here: ${firstLine(err.message)}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (report !== undefined && !report.installed) {
|
|
112
|
+
add({
|
|
113
|
+
id: "unit",
|
|
114
|
+
status: "skip",
|
|
115
|
+
message: "Paddock isn't installed as a service (`paddock service install` runs it from login)",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else if (report !== undefined) {
|
|
119
|
+
const before = checks.length;
|
|
120
|
+
for (const gone of missingPaths([report.node ?? undefined, report.cli ?? undefined])) {
|
|
121
|
+
add({
|
|
122
|
+
id: "unit",
|
|
123
|
+
status: "fail",
|
|
124
|
+
message: `the service runs ${t(gone)}, which isn't there any more (a Node version removed with nvm, fnm or volta, or Paddock reinstalled elsewhere)`,
|
|
125
|
+
fix: "paddock service install --refresh (re-writes the unit for this Node and this install, keeping its port, host and data dir)",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (report.warnings.some((w) => w.includes("#872"))) {
|
|
129
|
+
add({
|
|
130
|
+
id: "unit",
|
|
131
|
+
status: "warn",
|
|
132
|
+
message: "the unit predates a fix (#872): a clean stop (sleep, logout, a stray kill) leaves Paddock down until it's started by hand",
|
|
133
|
+
fix: "paddock service install --refresh",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (report.cli !== null && fs.existsSync(report.cli) && !samePath(report.cli, ctx.scriptPath)) {
|
|
137
|
+
add({
|
|
138
|
+
id: "unit",
|
|
139
|
+
status: "warn",
|
|
140
|
+
message: `the service runs a different Paddock install (${t(report.cli)}) from this one (${t(ctx.scriptPath)})`,
|
|
141
|
+
fix: "run `paddock service install --refresh` from the install the service should run",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (!report.running) {
|
|
145
|
+
add({
|
|
146
|
+
id: "unit",
|
|
147
|
+
status: "warn",
|
|
148
|
+
message: `the service is installed (${report.manager}) but not running`,
|
|
149
|
+
fix: "paddock service start (and `paddock service logs` if it doesn't stay up)",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else if (!report.answering) {
|
|
153
|
+
add({
|
|
154
|
+
id: "unit",
|
|
155
|
+
status: "warn",
|
|
156
|
+
message: `the service is running, but ${report.url ?? "its URL"} isn't answering`,
|
|
157
|
+
fix: "paddock service logs",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (checks.length === before) {
|
|
161
|
+
add({
|
|
162
|
+
id: "unit",
|
|
163
|
+
status: "pass",
|
|
164
|
+
message: `the service is installed (${report.manager}), running and answering at ${report.url ?? "?"}: ${t(report.unit)}`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// The instance this run is about: an explicit --data-dir, else the unit's,
|
|
169
|
+
// else PADDOCK_DATA_DIR, else ~/.paddock. It's where `update-check.json` goes,
|
|
170
|
+
// and where `service status` reads it back from.
|
|
171
|
+
const home = path.resolve(i.dataDir ??
|
|
172
|
+
(report?.installed === true && report.dataDir !== null ? report.dataDir : undefined) ??
|
|
173
|
+
ctx.envDataDir ??
|
|
174
|
+
defaultDataDir(ctx.homeDir));
|
|
175
|
+
// ---- the running version is the installed one: the service's, or a `serve` in a terminal
|
|
176
|
+
const live = readLiveRuntimeFile(home);
|
|
177
|
+
const running = report?.answering === true
|
|
178
|
+
? { version: report.runningVersion, where: report.url ?? "the service", installed: report.installedVersion, via: "service" }
|
|
179
|
+
: live !== null
|
|
180
|
+
? { version: live.version, where: live.url, installed: PADDOCK_VERSION, via: "serve" }
|
|
181
|
+
: undefined;
|
|
182
|
+
if (running === undefined) {
|
|
183
|
+
add({ id: "running-version", status: "skip", message: "Paddock isn't running" });
|
|
184
|
+
}
|
|
185
|
+
else if (running.version !== null && running.installed !== null && running.version !== running.installed) {
|
|
186
|
+
add({
|
|
187
|
+
id: "running-version",
|
|
188
|
+
status: "fail",
|
|
189
|
+
message: `${running.where} is running ${running.version}, but ${running.installed} is installed: it's still running the code it started with`,
|
|
190
|
+
fix: running.via === "service"
|
|
191
|
+
? "paddock service restart"
|
|
192
|
+
: "stop the `paddock serve` in its terminal (Ctrl-C) and start it again",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
add({
|
|
197
|
+
id: "running-version",
|
|
198
|
+
status: "pass",
|
|
199
|
+
message: `${running.where} is running ${running.version ?? "an unknown version"}${running.via === "serve" ? " (in a terminal)" : ""}`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// ---- the port: free, or held by Paddock
|
|
203
|
+
const port = Number(report?.installed === true && report.port !== null
|
|
204
|
+
? report.port
|
|
205
|
+
: (live?.port ?? env.PORT ?? DEFAULT_PORT));
|
|
206
|
+
const host = loopbackFor(report?.installed === true && report.host !== null ? report.host : "127.0.0.1");
|
|
207
|
+
const listening = await (i.isListening ?? isListening)(host, port);
|
|
208
|
+
if (!listening) {
|
|
209
|
+
add({ id: "port", status: "pass", message: `port ${port} is free` });
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const who = await (i.identify ?? identify)(`http://${urlHost(host)}:${port}`);
|
|
213
|
+
if (who?.paddock === true) {
|
|
214
|
+
add({
|
|
215
|
+
id: "port",
|
|
216
|
+
status: "pass",
|
|
217
|
+
message: `port ${port} is Paddock's${who.version !== undefined ? ` (${who.version})` : ""}`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
add({
|
|
222
|
+
id: "port",
|
|
223
|
+
status: "fail",
|
|
224
|
+
// Something answered /api/health but not /api/busy: another tool, or a
|
|
225
|
+
// Paddock from before the busy check, which can't be told apart.
|
|
226
|
+
message: who !== undefined
|
|
227
|
+
? `something that isn't Paddock (or is a Paddock older than 0.81) is listening on port ${port}`
|
|
228
|
+
: `something that isn't Paddock is listening on port ${port}`,
|
|
229
|
+
fix: report?.installed === true
|
|
230
|
+
? "stop it, or move the service: `paddock service install --port <n>`"
|
|
231
|
+
: "stop it, or run Paddock on another port: `paddock --port <n>`",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// ---- linger, on Linux: without it the service stops when you log out
|
|
236
|
+
let linger;
|
|
237
|
+
try {
|
|
238
|
+
linger = backend?.linger();
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
linger = undefined;
|
|
242
|
+
}
|
|
243
|
+
if (linger === undefined || !linger.applies) {
|
|
244
|
+
add({ id: "linger", status: "skip", message: "linger only applies to Linux" });
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const user = linger.user ?? "$USER";
|
|
248
|
+
if (linger.enabled === true) {
|
|
249
|
+
add({ id: "linger", status: "pass", message: `linger is on for ${user}: the service keeps running when you log out` });
|
|
250
|
+
}
|
|
251
|
+
else if (linger.enabled === false && report?.installed === true) {
|
|
252
|
+
add({
|
|
253
|
+
id: "linger",
|
|
254
|
+
status: "warn",
|
|
255
|
+
message: `linger is off for ${user}: the service stops when you log out, and starts only when you log in`,
|
|
256
|
+
fix: `sudo loginctl enable-linger ${user} (Paddock never runs sudo itself)`,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
else if (linger.enabled === false) {
|
|
260
|
+
add({
|
|
261
|
+
id: "linger",
|
|
262
|
+
status: "pass",
|
|
263
|
+
message: `linger is off for ${user}, which only matters once Paddock runs as a service`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
add({ id: "linger", status: "skip", message: "couldn't ask loginctl about linger" });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// ---- the data home is writable
|
|
271
|
+
const writable = writableOrCreatable(home);
|
|
272
|
+
if (writable === true) {
|
|
273
|
+
add({ id: "data-home", status: "pass", message: `the data dir ${t(home)} is writable` });
|
|
274
|
+
}
|
|
275
|
+
else if (writable === "creatable") {
|
|
276
|
+
add({
|
|
277
|
+
id: "data-home",
|
|
278
|
+
status: "pass",
|
|
279
|
+
message: `the data dir ${t(home)} doesn't exist yet; Paddock makes it the first time it runs`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
add({
|
|
284
|
+
id: "data-home",
|
|
285
|
+
status: "fail",
|
|
286
|
+
message: `the data dir ${t(home)} isn't writable: ${writable}`,
|
|
287
|
+
fix: `check the permissions of ${t(home)}: it holds your projects, chats and settings`,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
// ---- Paddock's own: the config loads, and what it resolves to
|
|
291
|
+
let cfg;
|
|
292
|
+
try {
|
|
293
|
+
cfg = await (i.loadConfig ?? loadInstanceConfig)(home);
|
|
294
|
+
add({ id: "config", status: "pass", message: "the config loads" });
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
add({
|
|
298
|
+
id: "config",
|
|
299
|
+
status: "fail",
|
|
300
|
+
message: `the config doesn't load, so \`paddock serve\` would fail too: ${firstLine(err.message)}`,
|
|
301
|
+
fix: "fix what it names (`paddock config show` prints the same error)",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (cfg === undefined) {
|
|
305
|
+
add({ id: "projects", status: "skip", message: "the config didn't load, so the projects folder is unknown" });
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
const projects = writableOrCreatable(cfg.projectsRoot);
|
|
309
|
+
if (projects === true) {
|
|
310
|
+
add({ id: "projects", status: "pass", message: `the projects folder ${t(cfg.projectsRoot)} is writable` });
|
|
311
|
+
}
|
|
312
|
+
else if (projects === "creatable") {
|
|
313
|
+
add({
|
|
314
|
+
id: "projects",
|
|
315
|
+
status: "pass",
|
|
316
|
+
message: `the projects folder ${t(cfg.projectsRoot)} doesn't exist yet; Paddock makes it the first time it runs`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
add({
|
|
321
|
+
id: "projects",
|
|
322
|
+
status: "fail",
|
|
323
|
+
message: `the projects folder ${t(cfg.projectsRoot)} isn't writable: ${projects}`,
|
|
324
|
+
fix: "fix its permissions, or point PADDOCK_PROJECTS_DIR (or `projectsRoot:` in paddock.config.yaml) somewhere writable",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// ---- a Claude login it can use. Which KIND, never the value.
|
|
329
|
+
if (cfg !== undefined)
|
|
330
|
+
add(await loginCheck(cfg, env, i, t, report?.installed === true));
|
|
331
|
+
else
|
|
332
|
+
add({ id: "claude-login", status: "skip", message: "the config didn't load, so Paddock's Claude home is unknown" });
|
|
333
|
+
// ---- a `claude` on PATH: the sweeper always shells out to it, and on
|
|
334
|
+
// driveMode: batch so do chats and triggers.
|
|
335
|
+
const claude = which("claude", env.PATH ?? "");
|
|
336
|
+
if (claude !== undefined) {
|
|
337
|
+
add({ id: "claude-cli", status: "pass", message: `claude is on PATH (${t(claude)})` });
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
const batch = cfg?.driveMode === "batch";
|
|
341
|
+
add({
|
|
342
|
+
id: "claude-cli",
|
|
343
|
+
status: batch ? "fail" : "warn",
|
|
344
|
+
message: batch
|
|
345
|
+
? "there's no `claude` on PATH, and driveMode is batch: every chat turn and trigger runs it, so they'll all fail"
|
|
346
|
+
: "there's no `claude` on PATH: chats work (they use the SDK's own), but the sweeper that keeps OVERVIEW.md and CHANGELOG.md current runs `claude` and will fail",
|
|
347
|
+
fix: "npm install -g @anthropic-ai/claude-code (then re-run `paddock service install --refresh` if it's a service, so the unit's PATH has it)",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
// ---- a newer version (SPEC §1). The only check that asks the network, and --offline skips it.
|
|
351
|
+
if (i.offline === true) {
|
|
352
|
+
add({ id: "update", status: "skip", message: "not asking the registry for a newer version (--offline)" });
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
try {
|
|
356
|
+
const target = await resolveTarget({
|
|
357
|
+
packageName: PADDOCK_PACKAGE,
|
|
358
|
+
registry: i.registry ?? resolveRegistry(env, (k) => npmConfigGet(k, { env })),
|
|
359
|
+
...(i.registryFetch !== undefined ? { fetch: i.registryFetch } : {}),
|
|
360
|
+
});
|
|
361
|
+
const cmp = compareVersions(target.version, PADDOCK_VERSION);
|
|
362
|
+
const available = cmp > 0 && !isPrerelease(target.version);
|
|
363
|
+
const cmd = info.canUpdate
|
|
364
|
+
? { command: "paddock update" }
|
|
365
|
+
: updateCommand(info, { packageName: PADDOCK_PACKAGE, target: target.version, image: PADDOCK_IMAGE });
|
|
366
|
+
// Only into a data dir that's there. Making it here would make the instance:
|
|
367
|
+
// `serve` prints its first-run welcome only when the data dir is new, and
|
|
368
|
+
// `config show` stays out of it for the same reason.
|
|
369
|
+
if (fs.existsSync(home))
|
|
370
|
+
writeUpdateCheck(home, {
|
|
371
|
+
name: PADDOCK_PACKAGE,
|
|
372
|
+
current: PADDOCK_VERSION,
|
|
373
|
+
target: target.version,
|
|
374
|
+
tag: target.tag,
|
|
375
|
+
updateAvailable: available,
|
|
376
|
+
installMethod: info.method,
|
|
377
|
+
canUpdate: info.canUpdate,
|
|
378
|
+
command: available ? cmd.command : null,
|
|
379
|
+
engines: target.engines,
|
|
380
|
+
nodeOk: satisfiesEngines(nodeVersion, target.engines !== null ? { node: target.engines } : undefined).ok !== false,
|
|
381
|
+
changelog: paddockChangelogUrl(target.version),
|
|
382
|
+
checkedAt: new Date().toISOString(),
|
|
383
|
+
});
|
|
384
|
+
if (available) {
|
|
385
|
+
notice = updateNotice({
|
|
386
|
+
name: PADDOCK_NAME,
|
|
387
|
+
current: PADDOCK_VERSION,
|
|
388
|
+
target: target.version,
|
|
389
|
+
command: cmd.command,
|
|
390
|
+
...("note" in cmd && cmd.note !== undefined ? { note: cmd.note } : {}),
|
|
391
|
+
changelog: paddockChangelogUrl(target.version),
|
|
392
|
+
});
|
|
393
|
+
add({
|
|
394
|
+
id: "update",
|
|
395
|
+
status: "warn",
|
|
396
|
+
message: `Paddock ${target.version} is available (you have ${PADDOCK_VERSION})`,
|
|
397
|
+
fix: cmd.command,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
add({
|
|
402
|
+
id: "update",
|
|
403
|
+
status: "pass",
|
|
404
|
+
message: cmp < 0
|
|
405
|
+
? `${PADDOCK_VERSION} is newer than the registry's latest (${target.version})`
|
|
406
|
+
: `${PADDOCK_VERSION} is the latest`,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
add({
|
|
412
|
+
id: "update",
|
|
413
|
+
status: "warn",
|
|
414
|
+
message: `couldn't ask the registry for a newer version: ${err instanceof RegistryError ? `${err.message} (${err.url})` : firstLine(err.message)}`,
|
|
415
|
+
fix: "check the network (or npm_config_registry), or skip this check with --offline",
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { checks, ...(notice !== undefined ? { notice } : {}) };
|
|
420
|
+
}
|
|
421
|
+
/** The `claude-login` check: what kind of login Paddock would find, never its value. */
|
|
422
|
+
async function loginCheck(cfg, env, i, t, service) {
|
|
423
|
+
const tokenVar = TOKEN_ENV_VARS.find((v) => (env[v] ?? "").trim() !== "");
|
|
424
|
+
if (tokenVar !== undefined) {
|
|
425
|
+
return {
|
|
426
|
+
id: "claude-login",
|
|
427
|
+
status: "pass",
|
|
428
|
+
message: `${tokenVar} is set in this shell` +
|
|
429
|
+
(service ? " (the service doesn't inherit this shell's environment, so it needs a login of its own)" : ""),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (fs.existsSync(path.join(cfg.claudeHome, ".credentials.json"))) {
|
|
433
|
+
return { id: "claude-login", status: "pass", message: `a Claude login is in Paddock's Claude home (${t(cfg.claudeHome)})` };
|
|
434
|
+
}
|
|
435
|
+
const userHome = i.userClaudeHome ?? path.join(os.homedir(), ".claude");
|
|
436
|
+
if (cfg.credentials === "host") {
|
|
437
|
+
if (fs.existsSync(path.join(userHome, ".credentials.json"))) {
|
|
438
|
+
return {
|
|
439
|
+
id: "claude-login",
|
|
440
|
+
status: "pass",
|
|
441
|
+
message: `Paddock shares this machine's Claude Code login (${t(userHome)}, claude.credentials: host)`,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
if (i.ctx.platform === "darwin") {
|
|
445
|
+
const keychain = await (i.keychain ?? probeKeychain)();
|
|
446
|
+
if (keychain === "found") {
|
|
447
|
+
return {
|
|
448
|
+
id: "claude-login",
|
|
449
|
+
status: "pass",
|
|
450
|
+
message: "Paddock shares this machine's Claude Code login, from the macOS Keychain (claude.credentials: host)",
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
id: "claude-login",
|
|
457
|
+
status: "warn",
|
|
458
|
+
message: `no Claude login found for Paddock: none in its Claude home (${t(cfg.claudeHome)}), no ${TOKEN_ENV_VARS.join("/")} in the environment` +
|
|
459
|
+
(cfg.credentials === "host" ? ", and none on this machine to share" : ", and claude.credentials: own shares none of this machine's") +
|
|
460
|
+
". Chats will fail with \"Not logged in\"",
|
|
461
|
+
fix: `CLAUDE_CONFIG_DIR=${t(cfg.claudeHome)} claude login (or \`claude setup-token\` and put the token in CLAUDE_CODE_OAUTH_TOKEN)`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
/** The checklist, for a person. */
|
|
465
|
+
export function renderChecks(checks) {
|
|
466
|
+
return (checks
|
|
467
|
+
.map((c) => `${MARK[c.status]} ${c.message}` +
|
|
468
|
+
(c.fix !== undefined && (c.status === "fail" || c.status === "warn") ? `\n fix: ${c.fix}` : ""))
|
|
469
|
+
.join("\n") + "\n");
|
|
470
|
+
}
|
|
471
|
+
/** 1 if any check failed (SPEC §1); warnings don't change it. */
|
|
472
|
+
export function doctorExit(checks) {
|
|
473
|
+
return checks.some((c) => c.status === "fail") ? EXIT.FAILED : EXIT.OK;
|
|
474
|
+
}
|
|
475
|
+
/** `paddock doctor`: run the checks, print them, and hand back the exit code. */
|
|
476
|
+
export async function runDoctor(opts, ctx) {
|
|
477
|
+
const { checks, notice } = await diagnose({
|
|
478
|
+
ctx,
|
|
479
|
+
...(opts.dataDir !== undefined ? { dataDir: opts.dataDir } : {}),
|
|
480
|
+
...(opts.offline === true ? { offline: true } : {}),
|
|
481
|
+
entry: process.argv[1] ?? ctx.scriptPath,
|
|
482
|
+
});
|
|
483
|
+
const code = doctorExit(checks);
|
|
484
|
+
if (opts.json === true) {
|
|
485
|
+
process.stdout.write(`${JSON.stringify({ ok: code === EXIT.OK, version: PADDOCK_VERSION, checks }, null, 2)}\n`);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
process.stdout.write(renderChecks(checks));
|
|
489
|
+
}
|
|
490
|
+
// The update notice goes to stderr (SPEC §3), with or without --json.
|
|
491
|
+
if (notice !== undefined)
|
|
492
|
+
process.stderr.write(`\n${notice}\n`);
|
|
493
|
+
return code;
|
|
494
|
+
}
|
|
495
|
+
// ---- the real implementations of the injected seams
|
|
496
|
+
/** The package's `engines.node`, read from the package root §6's walk-up found. */
|
|
497
|
+
function enginesOf(packageRoot) {
|
|
498
|
+
if (packageRoot === undefined)
|
|
499
|
+
return undefined;
|
|
500
|
+
try {
|
|
501
|
+
const raw = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
502
|
+
return typeof raw.engines?.node === "string" ? raw.engines.node : undefined;
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Load the instance's config the way `paddock serve` would, without creating
|
|
510
|
+
* anything. `loadPaddockConfig` reads `PADDOCK_DATA_DIR`, so it's set for the
|
|
511
|
+
* call and put back.
|
|
512
|
+
*/
|
|
513
|
+
async function loadInstanceConfig(dataDir) {
|
|
514
|
+
const before = process.env.PADDOCK_DATA_DIR;
|
|
515
|
+
process.env.PADDOCK_DATA_DIR = dataDir;
|
|
516
|
+
try {
|
|
517
|
+
const { loadPaddockConfig } = await import("../config.js");
|
|
518
|
+
const cfg = loadPaddockConfig({ createDataDir: false });
|
|
519
|
+
return {
|
|
520
|
+
projectsRoot: cfg.projectsRoot,
|
|
521
|
+
claudeHome: cfg.claudeHome,
|
|
522
|
+
credentials: cfg.claude.credentials,
|
|
523
|
+
driveMode: cfg.driveMode,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
finally {
|
|
527
|
+
if (before === undefined)
|
|
528
|
+
delete process.env.PADDOCK_DATA_DIR;
|
|
529
|
+
else
|
|
530
|
+
process.env.PADDOCK_DATA_DIR = before;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async function probeKeychain() {
|
|
534
|
+
const { probeMacosKeychainLogin } = await import("../claude-home.js");
|
|
535
|
+
return probeMacosKeychainLogin();
|
|
536
|
+
}
|
|
537
|
+
/** True if `dir` is writable; "creatable" if it doesn't exist but its nearest existing parent is; else why not. */
|
|
538
|
+
export function writableOrCreatable(dir) {
|
|
539
|
+
let probe = dir;
|
|
540
|
+
while (!fs.existsSync(probe)) {
|
|
541
|
+
const parent = path.dirname(probe);
|
|
542
|
+
if (parent === probe)
|
|
543
|
+
return "no part of it exists";
|
|
544
|
+
probe = parent;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
if (!fs.statSync(probe).isDirectory())
|
|
548
|
+
return `${probe} isn't a folder`;
|
|
549
|
+
fs.accessSync(probe, fs.constants.W_OK);
|
|
550
|
+
return probe === dir ? true : "creatable";
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
return err.message;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function samePath(a, b) {
|
|
557
|
+
const real = (p) => {
|
|
558
|
+
try {
|
|
559
|
+
return fs.realpathSync(p);
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
return path.resolve(p);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
return real(a) === real(b);
|
|
566
|
+
}
|
|
567
|
+
/** A wildcard bind is reached on loopback. */
|
|
568
|
+
function loopbackFor(host) {
|
|
569
|
+
if (host === "0.0.0.0" || host === "")
|
|
570
|
+
return "127.0.0.1";
|
|
571
|
+
if (host === "::" || host === "[::]")
|
|
572
|
+
return "::1";
|
|
573
|
+
return host;
|
|
574
|
+
}
|
|
575
|
+
function urlHost(host) {
|
|
576
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
577
|
+
}
|
|
578
|
+
function firstLine(s) {
|
|
579
|
+
return s.split("\n")[0] ?? s;
|
|
580
|
+
}
|
|
@@ -1,49 +1,25 @@
|
|
|
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
1
|
export declare const EXIT: {
|
|
30
|
-
/** Done
|
|
31
|
-
readonly
|
|
32
|
-
/** Failed
|
|
33
|
-
readonly
|
|
34
|
-
/** Usage: an unknown command or flag,
|
|
35
|
-
readonly
|
|
36
|
-
/** Not running: installed
|
|
37
|
-
readonly
|
|
38
|
-
/** Not installed: there
|
|
39
|
-
readonly
|
|
40
|
-
/** This install
|
|
41
|
-
readonly
|
|
2
|
+
/** Done; running and answering; up to date; not busy. */
|
|
3
|
+
readonly OK: 0;
|
|
4
|
+
/** Failed. */
|
|
5
|
+
readonly FAILED: 1;
|
|
6
|
+
/** Usage: an unknown command or flag, a bad value. The usage is printed after the message. */
|
|
7
|
+
readonly USAGE: 2;
|
|
8
|
+
/** Not running: installed but not running or not answering, or no instance answers. */
|
|
9
|
+
readonly NOT_RUNNING: 3;
|
|
10
|
+
/** Not installed: there's no service unit. */
|
|
11
|
+
readonly NOT_INSTALLED: 4;
|
|
12
|
+
/** This install can't update itself; the right command was printed. */
|
|
13
|
+
readonly CANNOT_UPDATE: 5;
|
|
42
14
|
/** The update failed and was rolled back; the old version is running again. */
|
|
43
|
-
readonly
|
|
44
|
-
/** The update failed and
|
|
45
|
-
readonly
|
|
46
|
-
/** Busy: refused, and why was printed. Try later, or use `--force`. */
|
|
47
|
-
readonly
|
|
15
|
+
readonly ROLLED_BACK: 6;
|
|
16
|
+
/** The update failed and couldn't be rolled back; a person is needed. */
|
|
17
|
+
readonly ROLLBACK_FAILED: 7;
|
|
18
|
+
/** Busy: refused, and why was printed. Try later, or use `--force`. `EX_TEMPFAIL` from sysexits.h. */
|
|
19
|
+
readonly BUSY: 75;
|
|
48
20
|
};
|
|
49
21
|
export type ExitCode = (typeof EXIT)[keyof typeof EXIT];
|
|
22
|
+
/** A tool may use 8 to 63 for its own commands (SPEC §2). */
|
|
23
|
+
export declare function isToolOwnCode(code: number): boolean;
|
|
24
|
+
/** The SPEC's name for a shared code, for messages ("expected 4 (not installed), got 0 (ok)"). */
|
|
25
|
+
export declare function describeExitCode(code: number | null): string;
|