@edspencer/paddock 0.82.1 → 0.83.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/server/dist/cli/args.d.ts +29 -4
- package/packages/server/dist/cli/args.js +143 -5
- package/packages/server/dist/cli/busy.js +1 -1
- 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.js +1 -1
- package/packages/server/dist/cli/output.js +1 -1
- package/packages/server/dist/cli/paddock.js +28 -3
- package/packages/server/dist/cli/service/index.d.ts +34 -1
- package/packages/server/dist/cli/service/index.js +142 -16
- package/packages/server/dist/cli/service/launchd.js +12 -2
- package/packages/server/dist/cli/service/logs.js +1 -1
- package/packages/server/dist/cli/service/manager.js +1 -1
- package/packages/server/dist/cli/service/path-env.js +1 -1
- 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/systemd.js +16 -3
- package/packages/server/dist/cli/update/health.js +1 -1
- package/packages/server/dist/cli/update/install-method.js +1 -1
- package/packages/server/dist/cli/update/lock.js +1 -1
- package/packages/server/dist/cli/update/package-manager.js +1 -1
- package/packages/server/dist/cli/update/registry.js +1 -1
- package/packages/server/dist/cli/update/semver.js +1 -1
- package/packages/server/dist/cli/update/update-check.js +1 -1
- package/packages/server/dist/cli/update/update.js +1 -1
- 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-CgHpQctt.js → DeleteProjectDialog-BPwSkY3s.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-DVoF9Vil.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-BKqI0I1E.js → DiscoverView-Bz4Rxg-V.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-DL3raCK1.js → InstanceConfigPage-CXHem0Ab.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DcjEuf3s.js → MarkdownRenderer-2vrOdDh7.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DerkA-nE.js → ProjectRedirect-V77rycxm.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-NwfC3_w7.js → ProjectView-C1qr6nlt.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-BeORa0UH.js → ProjectsGrid-Jq1LRFcs.js} +1 -1
- package/packages/web/dist/assets/{RootHome-BYJs8qJ6.js → RootHome-H2k4p0ry.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-w-kEaAYU.js → Surfaces-By65IjL4.js} +1 -1
- package/packages/web/dist/assets/{arc-Bv56hasM.js → arc-BzdIzLZ7.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BHEknx9i.js → architectureDiagram-3BPJPVTR-DT9cGrnU.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DtxTMrPX.js → blockDiagram-GPEHLZMM-BjD-1DXr.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DRpEYOlM.js → c4Diagram-AAUBKEIU-DV5q5E08.js} +1 -1
- package/packages/web/dist/assets/channel-D0zKU6oW.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-S4s7GCg7.js → chunk-2J33WTMH-mzG-wWrp.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-BkWVPLf1.js → chunk-4BX2VUAB-BJijOYsf.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-4-FQp01F.js → chunk-55IACEB6-CSHJ3zhe.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-BeyDWdIb.js → chunk-727SXJPM-BfdGsNJG.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-ZSxKy745.js → chunk-AQP2D5EJ-2OJN-ttV.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-Bq8uV9j-.js → chunk-FMBD7UC4-bvjojDsP.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-BOFsRyHk.js → chunk-ND2GUHAM-DnDJhgvR.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-uZ6oNc4m.js → chunk-QZHKN3VN-Bw6DAHLR.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-bF2OmpN6.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-bF2OmpN6.js +2 -0
- package/packages/web/dist/assets/{core-CYuJAM2U.js → core-BKN_gaN-.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-DIzkVSFb.js → cose-bilkent-S5V4N54A-BUfjSTaX.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-CHR6jJBv.js → dagre-BM42HDAG-LwxKA-3H.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-BAFXCH8Q.js → diagram-2AECGRRQ-DHS0Xgf9.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-BJ5t5gbz.js → diagram-5GNKFQAL-CNDLt1m9.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-D9Xt0IVg.js → diagram-KO2AKTUF-D7eVz-kU.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-Bq38u6sy.js → diagram-LMA3HP47-Cc0shYXj.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-CMQVqkpi.js → diagram-OG6HWLK6-CdiThnZF.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-M1lrfXRU.js → documentTitle-BgvYvKAe.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-C63M1g9b.js → erDiagram-TEJ5UH35--YKwrR0M.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-D3j8ACN7.js → flowDiagram-I6XJVG4X-BuWFXtpn.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-B1Fd1A2Y.js → ganttDiagram-6RSMTGT7-F6CLJBJ1.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CEhZhDdR.js → gitGraphDiagram-PVQCEYII-BXrk9SVF.js} +1 -1
- package/packages/web/dist/assets/{index-1E9yHciS.js → index-2MWb3aF0.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-Ctquaew6.js → infoDiagram-5YYISTIA-D9iTq2aK.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DV4GvA1R.js → ishikawaDiagram-YF4QCWOH-CE2vjaAw.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-C_Cy9waA.js → journeyDiagram-JHISSGLW-CyuASyD1.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-CGr9ctEf.js → kanban-definition-UN3LZRKU-C-JcQkTl.js} +1 -1
- package/packages/web/dist/assets/{linear-D3U8NOkt.js → linear-vd9dAgq3.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-BiArdWKV.js → mermaid.core-C-uiHBIT.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CZIpzj8Z.js → mindmap-definition-RKZ34NQL-CQQKVCUB.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-1MaeS3rt.js → pieDiagram-4H26LBE5-CB92bTxC.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B5pgQ54e.js → quadrantDiagram-W4KKPZXB-Cu5Ux2WT.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-BNnxrpGr.js → requirementDiagram-4Y6WPE33-FJxTktFV.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-DsxVH-hM.js → sankeyDiagram-5OEKKPKP-Bkxk5fIB.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-Ke9LcFZI.js → sequenceDiagram-3UESZ5HK-gw8EPzuw.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-UJzmflND.js → stateDiagram-AJRCARHV-CIHq9Q4N.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-Bp8j_o9f.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BRjNALVI.js → timeline-definition-PNZ67QCA-CI_99nZc.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-ClMAspxz.js → vennDiagram-CIIHVFJN-D5e169vs.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-UPv4JEmz.js → wardley-L42UT6IY-C-EFyR0g.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-BgYJbdEx.js → wardleyDiagram-YWT4CUSO-Cp6Fld6d.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-Bfk5HjGx.js → xychartDiagram-2RQKCTM6-D0iq_qGw.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-CG6cf-0i.js +0 -2
- package/packages/web/dist/assets/channel-CdIiAqY2.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DCCUHba0.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DCCUHba0.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DgUc3HkP.js +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type CliOptions, type ServiceAction } from "../args.js";
|
|
2
2
|
import { isNpxPath, serviceDir } from "./spec.js";
|
|
3
|
-
import { type Runner, type ServiceSpec } from "./manager.js";
|
|
3
|
+
import { type LingerState, 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,38 @@ 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;
|
|
74
|
+
/**
|
|
75
|
+
* The `enable-linger` line, printed only to the people it applies to (SPEC §7): when linger is off. Nothing when it's
|
|
76
|
+
* on, and nothing when loginctl couldn't answer (`doctor` reports that check as skipped).
|
|
77
|
+
*/
|
|
78
|
+
export declare function lingerNote(linger: LingerState): string | undefined;
|
|
48
79
|
/**
|
|
49
80
|
* The version of the Paddock the unit points at.
|
|
50
81
|
*
|
|
@@ -104,6 +135,8 @@ export interface StatusReport {
|
|
|
104
135
|
/** A stale unit, a missing Node — things that are wrong but not fatal. */
|
|
105
136
|
warnings: string[];
|
|
106
137
|
}
|
|
138
|
+
/** What `status` reports, and what `doctor`'s service checks read: one report, so the two agree. */
|
|
139
|
+
export declare function buildStatus(ctx: ServiceContext, backend: ServiceBackend): Promise<StatusReport>;
|
|
107
140
|
/**
|
|
108
141
|
* Dispatch, returning the exit code rather than setting one (SPEC §2).
|
|
109
142
|
*
|
|
@@ -29,14 +29,39 @@ import { CliError } from "../args.js";
|
|
|
29
29
|
import { EXIT } from "../exit-codes.js";
|
|
30
30
|
import { buildSpec, defaultDataDir, hostFromArgv, isNpxPath, NPX_REFUSAL, portFromArgv, serviceDir, serviceLogPath, PADDOCK_SERVICE, } from "./spec.js";
|
|
31
31
|
import { spawnRunner, } from "./manager.js";
|
|
32
|
-
import { createLaunchd } from "./launchd.js";
|
|
33
|
-
import { createSystemd } from "./systemd.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";
|
|
37
38
|
import { PADDOCK_VERSION } from "../../version.js";
|
|
38
39
|
import { updateSummary } from "../update/update-check.js";
|
|
39
|
-
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
|
+
}
|
|
40
65
|
/**
|
|
41
66
|
* Pick a backend, or explain why there isn't one.
|
|
42
67
|
*
|
|
@@ -44,6 +69,9 @@ import { waitForReady, fetchHealth, READY_TIMEOUT_LABEL, } from "./ready.js";
|
|
|
44
69
|
* either of these without a scheduled task or a service wrapper, and shipping
|
|
45
70
|
* an untested third writer would be worse than saying so.
|
|
46
71
|
*/
|
|
72
|
+
export function serviceBackend(ctx) {
|
|
73
|
+
return backendFor(ctx);
|
|
74
|
+
}
|
|
47
75
|
function backendFor(ctx) {
|
|
48
76
|
const run = ctx.run ?? spawnRunner;
|
|
49
77
|
if (ctx.platform === "darwin")
|
|
@@ -174,20 +202,26 @@ function installProgress(backend) {
|
|
|
174
202
|
" macOS may take a few seconds to approve the background item."
|
|
175
203
|
: `\n Installing ${kind} and starting Paddock…`;
|
|
176
204
|
}
|
|
177
|
-
/**
|
|
178
|
-
|
|
179
|
-
|
|
205
|
+
/**
|
|
206
|
+
* The `enable-linger` line, printed only to the people it applies to (SPEC §7): when linger is off. Nothing when it's
|
|
207
|
+
* on, and nothing when loginctl couldn't answer (`doctor` reports that check as skipped).
|
|
208
|
+
*/
|
|
209
|
+
export function lingerNote(linger) {
|
|
210
|
+
if (!linger.applies || linger.enabled !== false)
|
|
180
211
|
return undefined;
|
|
181
|
-
|
|
182
|
-
? " Lingering is off, so this service stops when you log out — including"
|
|
183
|
-
: " If you log out (this check could not reach loginctl), a user service";
|
|
184
|
-
return (`${head}\n` +
|
|
212
|
+
return (" Lingering is off, so this service stops when you log out — including\n" +
|
|
185
213
|
" out of an SSH session. To keep Paddock up, once:\n" +
|
|
186
214
|
` sudo loginctl enable-linger ${linger.user ?? "$USER"}`);
|
|
187
215
|
}
|
|
188
216
|
async function install(opts, ctx, backend) {
|
|
189
217
|
if (isNpxPath(ctx.packageRoot))
|
|
190
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
|
+
}
|
|
191
225
|
// `--refresh` (SPEC §5 step 11): re-render an EXISTING unit, keeping what it
|
|
192
226
|
// records. With nothing installed there is nothing to refresh — exit 4, the
|
|
193
227
|
// same code every other command that needs a unit returns, rather than
|
|
@@ -196,17 +230,57 @@ async function install(opts, ctx, backend) {
|
|
|
196
230
|
// CLI, and a silent fallback would move a `--port 9000` service to 7233 on
|
|
197
231
|
// the one run nobody is watching.
|
|
198
232
|
const refreshing = opts.refresh === true;
|
|
199
|
-
|
|
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;
|
|
200
240
|
if (refreshing) {
|
|
201
|
-
existing
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
}
|
|
204
250
|
return EXIT.NOT_INSTALLED;
|
|
205
251
|
}
|
|
206
252
|
}
|
|
207
253
|
const { spec, dataDir } = specFor(existing !== undefined ? inherit(opts, existing) : opts, ctx, refreshing);
|
|
208
254
|
const url = `http://${hostFromArgv(spec.args)}:${portFromArgv(spec.args)}`;
|
|
209
|
-
|
|
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) {
|
|
210
284
|
console.log(`\n Re-writing ${backend.unitPath}\n Keeping: paddock ${spec.args.join(" ")}`);
|
|
211
285
|
}
|
|
212
286
|
console.log(installProgress(backend));
|
|
@@ -327,7 +401,8 @@ function withUpdate(home) {
|
|
|
327
401
|
const summary = updateSummary(home, { currentVersion: PADDOCK_VERSION });
|
|
328
402
|
return summary !== undefined ? { update: summary } : {};
|
|
329
403
|
}
|
|
330
|
-
|
|
404
|
+
/** What `status` reports, and what `doctor`'s service checks read: one report, so the two agree. */
|
|
405
|
+
export async function buildStatus(ctx, backend) {
|
|
331
406
|
const state = backend.status();
|
|
332
407
|
const base = {
|
|
333
408
|
installed: state.registered,
|
|
@@ -686,6 +761,57 @@ export async function runService(action, opts, ctx) {
|
|
|
686
761
|
// `--force` is rejected on every action but `stop` and `restart` — in
|
|
687
762
|
// `parseCommand`, once, for every verb. See the note there.
|
|
688
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) {
|
|
689
815
|
if (action === "install")
|
|
690
816
|
return install(opts, ctx, backend);
|
|
691
817
|
if (action === "uninstall")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/service/launchd.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/service/launchd.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
|
|
2
2
|
// macOS: a launchd LaunchAgent in the `gui/<uid>` domain (SPEC §7). It starts at login, not at boot, and runs as the
|
|
3
3
|
// user, inside their login session (so the login keychain is unlocked). Node built-ins only.
|
|
4
4
|
//
|
|
@@ -135,6 +135,7 @@ export function createLaunchd(id, o) {
|
|
|
135
135
|
label: id.label,
|
|
136
136
|
kind: 'a launchd LaunchAgent',
|
|
137
137
|
install(spec) {
|
|
138
|
+
const existed = fs.existsSync(file);
|
|
138
139
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
139
140
|
fs.mkdirSync(spec.workingDirectory, { recursive: true });
|
|
140
141
|
// launchd doesn't create the log's folder; without it the job fails to spawn, and says nothing.
|
|
@@ -142,7 +143,16 @@ export function createLaunchd(id, o) {
|
|
|
142
143
|
fs.writeFileSync(file, renderPlist(id, spec), 'utf8');
|
|
143
144
|
// A bootout first, because bootstrap refuses a label that's loaded. It fails on a first install, as expected.
|
|
144
145
|
bootoutAndWait();
|
|
145
|
-
|
|
146
|
+
try {
|
|
147
|
+
bootstrap();
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
// A first install launchd refused mustn't leave a plist behind: `status` would then say it's installed, and
|
|
151
|
+
// the next login would try it again. A reinstall keeps its plist.
|
|
152
|
+
if (!existed)
|
|
153
|
+
fs.rmSync(file, { force: true });
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
146
156
|
run('launchctl', ['kickstart', '-k', target]);
|
|
147
157
|
},
|
|
148
158
|
uninstall() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/service/logs.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/service/logs.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
|
|
2
2
|
// `<tool> service logs [-f] [-n <lines>]` (SPEC §1, §7): one Node implementation on every OS, because every unit
|
|
3
3
|
// writes to `<home>/logs/service.log`. No `tail`: `tail -f` isn't on Windows, and which `tail` won a PATH lookup would
|
|
4
4
|
// change the output. Node built-ins only.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/service/manager.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/service/manager.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
|
|
2
2
|
// The seam between what a tool's service is and how a service manager spells it (SPEC §7). launchd.ts and
|
|
3
3
|
// systemd.ts implement `ServiceManager`; Task Scheduler (Windows, P3) is the third, later, behind the same interface.
|
|
4
4
|
// Every command goes through a `Runner`, so the lifecycle is tested with a fake one, on any OS. Node built-ins only.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/service/path-env.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/service/path-env.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
|
|
2
2
|
// The PATH a unit gives the service (SPEC §7): Node's own bin folder first, then the installing shell's PATH without
|
|
3
3
|
// its per-shell entries, then the system folders. Only folders that exist are kept. Node built-ins only.
|
|
4
4
|
//
|
|
@@ -1,26 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* "Is it actually up?" — the bounded readiness poll the service CLI uses before
|
|
3
|
-
* it claims success (#873, #861).
|
|
4
|
-
*
|
|
5
|
-
* Every service command that starts something has the same failure mode without
|
|
6
|
-
* this: the supervisor accepts the request and returns, so the CLI prints a URL
|
|
7
|
-
* and a cheerful past tense while the server is still binding — or is already
|
|
8
|
-
* crash-looping on a port clash, which `launchctl` and `systemctl` will both
|
|
9
|
-
* happily report as "running" for the ten seconds between restarts. A message
|
|
10
|
-
* that can sit over a crash-loop and still say "running at <url>" is not a
|
|
11
|
-
* status line, it is a guess.
|
|
12
|
-
*
|
|
13
|
-
* So the check is deliberately end-to-end: an HTTP request to the URL a human
|
|
14
|
-
* would open, not a pid lookup. `/api/health` is the probe because it is in
|
|
15
|
-
* `ALWAYS_PUBLIC_PATHS` (`auth.ts`) — it answers whatever `PADDOCK_AUTH_MODE`
|
|
16
|
-
* the installed unit runs with, so this does not quietly stop working for the
|
|
17
|
-
* people who configured a credential.
|
|
18
|
-
*
|
|
19
|
-
* The result is a tri-state on purpose. "I could not confirm it" is a real and
|
|
20
|
-
* common answer — an unusual bind host, a slow first boot — and it is not the
|
|
21
|
-
* same claim as "it failed". Callers print it as a caveat, not an error, and
|
|
22
|
-
* point at the logs.
|
|
23
|
-
*/
|
|
24
1
|
/** Where the poll got to when it stopped. */
|
|
25
2
|
export type Readiness = "ready" | "timeout";
|
|
26
3
|
/** What `/api/health` said, when it said anything (SPEC §8). */
|
|
@@ -111,3 +88,19 @@ export type VersionVerdict = {
|
|
|
111
88
|
export declare function waitForVersion(opts: ReadyOptions & {
|
|
112
89
|
version: string;
|
|
113
90
|
}): Promise<VersionVerdict>;
|
|
91
|
+
/** Is anything accepting connections at host:port? A second at most. */
|
|
92
|
+
export declare function isListening(host: string, port: number): Promise<boolean>;
|
|
93
|
+
/**
|
|
94
|
+
* Is what answers at `url` a Paddock, and which version? Undefined when nothing
|
|
95
|
+
* answers `/api/health` there.
|
|
96
|
+
*
|
|
97
|
+
* `/api/health`'s `{ ok, version }` is the CLI contract's, so every one of the
|
|
98
|
+
* six tools answers it alike; `/api/busy`'s `counts.chat` and
|
|
99
|
+
* `counts.background` are Paddock's own. A Paddock older than 0.81 has no
|
|
100
|
+
* `/api/busy`, and reads as not-Paddock here — callers that can tell it apart
|
|
101
|
+
* some other way (an installed unit on that port) should.
|
|
102
|
+
*/
|
|
103
|
+
export declare function identifyPaddock(url: string, fetchImpl?: typeof fetch): Promise<{
|
|
104
|
+
paddock: boolean;
|
|
105
|
+
version?: string;
|
|
106
|
+
} | undefined>;
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* same claim as "it failed". Callers print it as a caveat, not an error, and
|
|
22
22
|
* point at the logs.
|
|
23
23
|
*/
|
|
24
|
+
import net from "node:net";
|
|
24
25
|
/**
|
|
25
26
|
* Ask `<url>/api/health` once, and say what it answered.
|
|
26
27
|
*
|
|
@@ -134,3 +135,44 @@ export async function waitForVersion(opts) {
|
|
|
134
135
|
await sleep(intervalMs);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
138
|
+
/** Is anything accepting connections at host:port? A second at most. */
|
|
139
|
+
export function isListening(host, port) {
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
const s = net.connect({ host, port });
|
|
142
|
+
s.setTimeout(1000);
|
|
143
|
+
s.once("connect", () => {
|
|
144
|
+
s.destroy();
|
|
145
|
+
resolve(true);
|
|
146
|
+
});
|
|
147
|
+
s.once("timeout", () => {
|
|
148
|
+
s.destroy();
|
|
149
|
+
resolve(false);
|
|
150
|
+
});
|
|
151
|
+
s.once("error", () => resolve(false));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Is what answers at `url` a Paddock, and which version? Undefined when nothing
|
|
156
|
+
* answers `/api/health` there.
|
|
157
|
+
*
|
|
158
|
+
* `/api/health`'s `{ ok, version }` is the CLI contract's, so every one of the
|
|
159
|
+
* six tools answers it alike; `/api/busy`'s `counts.chat` and
|
|
160
|
+
* `counts.background` are Paddock's own. A Paddock older than 0.81 has no
|
|
161
|
+
* `/api/busy`, and reads as not-Paddock here — callers that can tell it apart
|
|
162
|
+
* some other way (an installed unit on that port) should.
|
|
163
|
+
*/
|
|
164
|
+
export async function identifyPaddock(url, fetchImpl = fetch) {
|
|
165
|
+
const health = await fetchHealth(url, { fetchImpl });
|
|
166
|
+
if (health === undefined)
|
|
167
|
+
return undefined;
|
|
168
|
+
const version = health.version !== undefined ? { version: health.version } : {};
|
|
169
|
+
try {
|
|
170
|
+
const res = await fetchImpl(`${url}/api/busy`, { signal: AbortSignal.timeout(2_000) });
|
|
171
|
+
const body = (await res.json());
|
|
172
|
+
const counts = body.counts ?? {};
|
|
173
|
+
return { paddock: health.ok && typeof counts.chat === "number" && "background" in counts, ...version };
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return { paddock: false, ...version };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/service/systemd.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/service/systemd.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
|
|
2
2
|
// Linux: a systemd user unit (`systemctl --user`, SPEC §7), running as the user. Without linger the user's manager,
|
|
3
3
|
// and the service with it, stops at their last logout, SSH sessions included; `linger()` asks, and the tool prints
|
|
4
4
|
// the `sudo loginctl enable-linger` line rather than running it. Node built-ins only.
|
|
@@ -103,14 +103,27 @@ export function createSystemd(id, o) {
|
|
|
103
103
|
kind: 'a systemd --user service',
|
|
104
104
|
install(spec) {
|
|
105
105
|
// Asked before the unit is overwritten, while it still describes what's running.
|
|
106
|
-
const
|
|
106
|
+
const existed = fs.existsSync(file);
|
|
107
|
+
const wasActive = existed && isActive();
|
|
107
108
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
108
109
|
fs.mkdirSync(spec.workingDirectory, { recursive: true });
|
|
109
110
|
fs.mkdirSync(path.dirname(spec.logPath), { recursive: true });
|
|
110
111
|
fs.writeFileSync(file, renderUnit(id, spec), 'utf8');
|
|
111
112
|
// Without a reload the manager keeps serving its cached copy of the old unit.
|
|
112
113
|
run('systemctl', ['--user', 'daemon-reload']);
|
|
113
|
-
|
|
114
|
+
try {
|
|
115
|
+
systemctl('enable', '--now', id.unit);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
// A first install systemd refused (no systemd here, no user bus) mustn't leave a unit behind: `status` would
|
|
119
|
+
// then say it's installed. A reinstall keeps its unit, which is what's installed either way.
|
|
120
|
+
if (!existed) {
|
|
121
|
+
run('systemctl', ['--user', 'disable', id.unit]);
|
|
122
|
+
fs.rmSync(file, { force: true });
|
|
123
|
+
run('systemctl', ['--user', 'daemon-reload']);
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
114
127
|
// `enable --now` doesn't touch a unit that's already active: a reinstall over a running service would leave the
|
|
115
128
|
// old process serving and report success. That's the state `update`'s rollback refreshes in, while the failed
|
|
116
129
|
// version is still up. So restart, but only then: on a stopped unit it would be two starts milliseconds apart,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/health.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/health.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
|
|
2
2
|
// Asking `/api/health` (SPEC §8): once, until it answers, or until it answers with one version. The service commands
|
|
3
3
|
// wait on it before they claim success, and `update` waits for the target version (SPEC §5 step 12). It's an HTTP
|
|
4
4
|
// request to the URL a person would open, not a pid lookup: launchd and systemd both report a crash-looping job as
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/install-method.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/install-method.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
|
|
2
2
|
// How was this copy installed? (SPEC §6.) `update` can't run `npm install -g` on a checkout, in a container or out of
|
|
3
3
|
// npx's cache, so before it touches anything it works out which of the table's ten rows it's in, and either picks the
|
|
4
4
|
// package manager or prints the command that would work and exits 5.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/lock.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/lock.ts @ 1.0; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
|
|
2
2
|
// `<home>/update.lock`: one update at a time (SPEC §5 step 1). Two at once is easy to cause (an Ansible run and a
|
|
3
3
|
// person at a terminal) and bad: the second stops the service the first just started, or installs an older target
|
|
4
4
|
// over a newer one, and the loser's rollback "restores" a version nobody asked for.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/package-manager.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/package-manager.ts @ 1.0; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
|
|
2
2
|
// Running the package manager (SPEC §5 steps 5, 8 and 10, §6, §7). Node built-ins only.
|
|
3
3
|
//
|
|
4
4
|
// - **npm is the one beside `process.execPath`**, run as `node <npm-cli.js>`, never whichever `npm` is first on PATH.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/registry.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/registry.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
|
|
2
2
|
// The npm registry, as `update` asks it (SPEC §5 step 3): two requests with `fetch` and a 10-second abort.
|
|
3
3
|
//
|
|
4
4
|
// GET <registry>/-/package/<name>/dist-tags which version `latest` (or `--to <tag>`) is
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/semver.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/semver.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
|
|
2
2
|
// The small semver `update` needs (SPEC §5 step 4): is the target newer, is it a prerelease, and does its
|
|
3
3
|
// `engines.node` accept this Node. Node built-ins only.
|
|
4
4
|
//
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/update-check.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/update-check.ts @ 1.0; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
|
|
2
2
|
// `<home>/update-check.json`: what the last registry question found (SPEC §3). Only `update`, `update --check` and
|
|
3
3
|
// `doctor` ask the registry; they write the answer here, and `service status` repeats it without opening a socket.
|
|
4
4
|
// It's the document `update --check --json` prints (`schemas/update-check.schema.json`), and `service status --json`'s
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/update.ts @ 0
|
|
1
|
+
// From edspencer/cli-contract reference/update.ts @ 1.0; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
|
|
2
2
|
// `<tool> update`: SPEC §5, step by step, for every tool. Node built-ins only; nothing here imports a server.
|
|
3
3
|
//
|
|
4
4
|
// It's a sequence over injected hooks. A tool supplies what's its own (`UpdateProduct`): its names, its busy check,
|
|
@@ -10,7 +10,7 @@ export interface RuntimeInfo {
|
|
|
10
10
|
export declare const RUNTIME_FILE = "runtime.json";
|
|
11
11
|
export declare function runtimeFilePath(home: string): string;
|
|
12
12
|
/** Writes the file atomically (a temporary file, then a rename), readable only by this user. */
|
|
13
|
-
export declare function writeRuntimeFile(home: string, info: Omit<RuntimeInfo,
|
|
13
|
+
export declare function writeRuntimeFile(home: string, info: Omit<RuntimeInfo, 'startedAt'> & {
|
|
14
14
|
startedAt?: string;
|
|
15
15
|
}): RuntimeInfo;
|
|
16
16
|
/** The file's contents, or null if it's missing or isn't a runtime file (a half-written or foreign file). */
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
// From edspencer/cli-contract reference/runtime-file.ts @
|
|
2
|
-
// unchanged, nothing else — the module is Node built-ins only and Paddock's `<home>` is its data dir, so it needed
|
|
3
|
-
// no adaptation. Kept verbatim on purpose: wave C4 re-syncs it, and a divergence here would be a divergence in the
|
|
4
|
-
// one file every tool's `busy`, `service status` and `update` agree on.
|
|
5
|
-
//
|
|
1
|
+
// From edspencer/cli-contract reference/runtime-file.ts @ 1.0; local changes: none.
|
|
6
2
|
// `<home>/runtime.json`: what a running instance tells its own CLI (SPEC §4, ROADMAP). `serve` writes it once it's
|
|
7
3
|
// listening and removes it on a clean exit; `busy`, `service status` and `update` read it to find the instance.
|
|
8
4
|
//
|
|
9
5
|
// It holds `{ pid, port, url, version, startedAt }`. A token for /api/busy will join it once SPEC §10's first question
|
|
10
6
|
// is settled; the file is written 0600 already, so adding one later changes nothing about who can read it.
|
|
11
7
|
// Node built-ins only.
|
|
12
|
-
import fs from
|
|
13
|
-
import path from
|
|
14
|
-
export const RUNTIME_FILE =
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
export const RUNTIME_FILE = 'runtime.json';
|
|
15
11
|
export function runtimeFilePath(home) {
|
|
16
12
|
return path.join(home, RUNTIME_FILE);
|
|
17
13
|
}
|
|
@@ -41,7 +37,7 @@ export function writeRuntimeFile(home, info) {
|
|
|
41
37
|
export function readRuntimeFile(home) {
|
|
42
38
|
let raw;
|
|
43
39
|
try {
|
|
44
|
-
raw = fs.readFileSync(runtimeFilePath(home),
|
|
40
|
+
raw = fs.readFileSync(runtimeFilePath(home), 'utf8');
|
|
45
41
|
}
|
|
46
42
|
catch {
|
|
47
43
|
return null;
|
|
@@ -50,9 +46,9 @@ export function readRuntimeFile(home) {
|
|
|
50
46
|
const v = JSON.parse(raw);
|
|
51
47
|
if (Number.isInteger(v.pid) &&
|
|
52
48
|
Number.isInteger(v.port) &&
|
|
53
|
-
typeof v.url ===
|
|
54
|
-
typeof v.version ===
|
|
55
|
-
typeof v.startedAt ===
|
|
49
|
+
typeof v.url === 'string' &&
|
|
50
|
+
typeof v.version === 'string' &&
|
|
51
|
+
typeof v.startedAt === 'string') {
|
|
56
52
|
return v;
|
|
57
53
|
}
|
|
58
54
|
}
|
|
@@ -81,7 +77,7 @@ export function isPidAlive(pid) {
|
|
|
81
77
|
return true;
|
|
82
78
|
}
|
|
83
79
|
catch (err) {
|
|
84
|
-
return err.code ===
|
|
80
|
+
return err.code === 'EPERM';
|
|
85
81
|
}
|
|
86
82
|
}
|
|
87
83
|
/** The runtime file, if the process it names is still alive. A crashed instance leaves a stale file behind. */
|
package/packages/web/dist/assets/{DeleteProjectDialog-CgHpQctt.js → DeleteProjectDialog-BPwSkY3s.js}
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as l,e as k,j as s,A as y,d as p,a as w}from"./index-
|
|
1
|
+
import{r as l,e as k,j as s,A as y,d as p,a as w}from"./index-2MWb3aF0.js";import{c as N}from"./lastTab-CU1zsF0j.js";function v({open:e,title:t,message:c,confirmLabel:n="Delete",cancelLabel:o="Cancel",danger:a=!0,wide:h=!1,dismissOnBackdrop:b=!0,onConfirm:g,onClose:d}){const[r,m]=l.useState(!1),[f,u]=l.useState(null);if(l.useEffect(()=>{e?u(null):m(!1)},[e]),k(e&&!r,d),!e)return null;const j=async()=>{m(!0),u(null);try{await g()}catch(i){u(i instanceof Error?i.message:"Action failed"),m(!1)}};return s.jsx("div",{className:"fixed inset-0 z-[60] flex items-center justify-center bg-overlay p-4 backdrop-blur-sm",onClick:()=>b&&!r&&d(),children:s.jsxs("div",{className:`w-full ${h?"max-w-md":"max-w-sm"} animate-scale-in rounded-2xl border border-edge bg-surface-raised p-6 shadow-2xl`,onClick:i=>i.stopPropagation(),role:"alertdialog","aria-modal":"true",children:[s.jsxs("div",{className:"flex items-start gap-3",children:[a&&s.jsx("span",{className:"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-danger-soft text-danger",children:s.jsx(y,{width:18,height:18})}),s.jsxs("div",{className:"min-w-0",children:[s.jsx("h2",{className:"text-base font-semibold",children:t}),s.jsx("div",{className:"mt-1.5 text-sm text-fg-muted",children:c})]})]}),f&&s.jsx("p",{className:"mt-4 rounded-lg bg-danger-soft px-3 py-2 text-sm text-danger",children:f}),s.jsxs("div",{className:"mt-5 flex justify-end gap-2",children:[s.jsx("button",{type:"button",className:"btn-ghost",onClick:d,disabled:r,children:o}),s.jsx("button",{type:"button",className:a?"btn btn-danger":"btn-primary",onClick:j,disabled:r,children:r?"Working…":n})]})]})})}function P(e,t){return e===t?!0:e.startsWith(t.endsWith("/")?t:`${t}/`)}function x(e){return!P(e.workingDir,e.dir)}function C({project:e}){const t=s.jsx("span",{className:"font-medium text-fg",children:e.name});return x(e)?s.jsxs(s.Fragment,{children:[t," will be unlinked. Paddock’s notes, settings and chats for it are permanently removed, and this cannot be undone — but the directory it works in is left alone:",s.jsx("span",{className:"mt-1.5 block break-all font-mono text-xs text-fg",children:e.workingDir}),s.jsx("span",{className:"mt-1.5 block",children:"Nothing in there is deleted."})]}):s.jsxs(s.Fragment,{children:[t," and all its chats and files will be permanently removed. This cannot be undone."]})}function D(){const{remove:e}=p();return l.useCallback(async t=>{await w.deleteProject(t),e(t),N(t)},[e])}function W({project:e,open:t,onClose:c,onDeleted:n}){const o=D(),a=x(e);return s.jsx(v,{open:t,title:"Delete project?",message:s.jsx(C,{project:e}),confirmLabel:"Delete project",wide:a,dismissOnBackdrop:!a,onConfirm:async()=>{await o(e.slug),n==null||n()},onClose:c})}export{v as C,W as D,x as d};
|
|
2
2
|
|