@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
|
@@ -5,8 +5,15 @@ import { TranscriptionError } from "../transcribe.js";
|
|
|
5
5
|
import { resolveModels, resolveDefaultModel } from "../models.js";
|
|
6
6
|
import { sendProjectError } from "../route-errors.js";
|
|
7
7
|
import { cspFor, parseRangeHeader } from "../http-bytes.js";
|
|
8
|
+
import { PADDOCK_VERSION } from "../version.js";
|
|
9
|
+
import { collectBusy } from "../busy.js";
|
|
8
10
|
export function registerMetaRoutes(app, ctx) {
|
|
9
11
|
const { herdctl, transcriber, attachments, cfg } = ctx;
|
|
12
|
+
// Both optional for the same reason `turn-interlock.ts` takes an optional hub:
|
|
13
|
+
// a server built without the WS layer (several integration tests) has neither,
|
|
14
|
+
// and the honest answer for it is zero rather than a crash.
|
|
15
|
+
const hub = ctx.managementOpsContext?.hub;
|
|
16
|
+
const background = ctx.managementOpsContext?.background;
|
|
10
17
|
// --- voice dictation (#voice): capability probe + transcription -------
|
|
11
18
|
// The composer polls this to decide whether to show a mic button. `available`
|
|
12
19
|
// is false on instances with dictation off (or a misconfigured remote).
|
|
@@ -106,20 +113,66 @@ export function registerMetaRoutes(app, ctx) {
|
|
|
106
113
|
},
|
|
107
114
|
},
|
|
108
115
|
}, async (req) => req.user);
|
|
116
|
+
// The CLI contract's one required server endpoint (SPEC §8): `{ ok, version }`,
|
|
117
|
+
// no auth. The version is load-bearing rather than decorative — it is how
|
|
118
|
+
// `paddock service status` can say that the version RUNNING differs from the
|
|
119
|
+
// one installed (an upgrade that never restarted), and how wave C2's
|
|
120
|
+
// `paddock update` will confirm the service came back as the version it just
|
|
121
|
+
// installed rather than merely coming back. `ALWAYS_PUBLIC_PATHS` in auth.ts
|
|
122
|
+
// keeps it answerable whatever `PADDOCK_AUTH_MODE` is set to.
|
|
109
123
|
app.get("/api/health", {
|
|
110
124
|
schema: {
|
|
111
125
|
tags: ["System"],
|
|
112
126
|
summary: "Health check",
|
|
113
|
-
description: "Liveness probe. Returns a JSON object `{ ok: true }`.",
|
|
127
|
+
description: "Liveness probe. Returns a JSON object `{ ok: true, version: \"<semver>\" }`. Never requires authentication.",
|
|
114
128
|
response: {
|
|
115
129
|
200: {
|
|
116
|
-
description: "Service is up.",
|
|
130
|
+
description: "Service is up, and the version it is running.",
|
|
117
131
|
type: "object",
|
|
118
132
|
additionalProperties: true,
|
|
119
133
|
},
|
|
120
134
|
},
|
|
121
135
|
},
|
|
122
|
-
}, async () => ({ ok: true }));
|
|
136
|
+
}, async () => ({ ok: true, version: PADDOCK_VERSION }));
|
|
137
|
+
// The contract's second server endpoint (SPEC §4, §8): is this instance busy?
|
|
138
|
+
//
|
|
139
|
+
// **Counts only, and no auth, and those two facts are the same fact.** There
|
|
140
|
+
// is nothing in the answer to protect — no chat title, no session id, no
|
|
141
|
+
// project name, just three integers and the version — which is why the route
|
|
142
|
+
// can sit beside `/api/health` in `HEALTH_PATHS` without widening what an
|
|
143
|
+
// unauthenticated caller can learn beyond "something is happening here".
|
|
144
|
+
// `reasons` (a line per piece of work, for a person) waits on SPEC §10's first
|
|
145
|
+
// question: how a CLI proves it is local, given that a reverse proxy on the
|
|
146
|
+
// same host also connects from loopback. Until that is designed, this answer
|
|
147
|
+
// is the whole answer for every caller — so nothing here may grow a string
|
|
148
|
+
// field without revisiting the auth decision above it.
|
|
149
|
+
//
|
|
150
|
+
// The callers that make the cheapness requirement real: `paddock service
|
|
151
|
+
// stop`, `paddock update`, and Watchtower's pre-update hook, which runs
|
|
152
|
+
// `paddock busy` before every image pull. `collectBusy` is synchronous and
|
|
153
|
+
// reads three in-memory structures — see busy.ts for why `job` does NOT come
|
|
154
|
+
// from `fleetStatus()`.
|
|
155
|
+
app.get("/api/busy", {
|
|
156
|
+
schema: {
|
|
157
|
+
tags: ["System"],
|
|
158
|
+
summary: "Is this instance busy?",
|
|
159
|
+
description: "The CLI contract's busy check (SPEC §4). Returns `{ busy, version, counts }`, where `counts` has `chat` (chat turns running, including fired triggers and cron fires), `background` (live sessions holding background work after their turn ended) and `job` (herdctl scheduler jobs). `busy` is true only for work a restart would lose or cut short. Counts only — never labels, ids or titles — so it needs no authentication. Reads in-memory state, never the disk.",
|
|
160
|
+
response: {
|
|
161
|
+
200: {
|
|
162
|
+
description: "Whether work a restart would cut short is under way.",
|
|
163
|
+
type: "object",
|
|
164
|
+
additionalProperties: true,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
}, async () => collectBusy({
|
|
169
|
+
runningChats: () => hub?.runningSessions().length ?? 0,
|
|
170
|
+
// Tasks in a session count only while its SDK session is still live —
|
|
171
|
+
// see `BackgroundRegistry.liveTaskCount`, which explains why a stale
|
|
172
|
+
// batch-mode entry must not wedge this at busy.
|
|
173
|
+
backgroundTasks: () => background?.liveTaskCount((id) => herdctl.isSdkSessionLive(id)) ?? 0,
|
|
174
|
+
runningJobs: () => herdctl.runningJobCount(),
|
|
175
|
+
}, PADDOCK_VERSION));
|
|
123
176
|
// --- instance-wide settings (issue #385) --------------------------------
|
|
124
177
|
// A top-level admin screen over the frozen instance config. GET reports every
|
|
125
178
|
// surfaced field (value / default / editable / sensitive / env-shadow); PUT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface RuntimeInfo {
|
|
2
|
+
pid: number;
|
|
3
|
+
port: number;
|
|
4
|
+
/** Where the CLI reaches it, e.g. `http://127.0.0.1:3729`. */
|
|
5
|
+
url: string;
|
|
6
|
+
version: string;
|
|
7
|
+
/** ISO 8601. */
|
|
8
|
+
startedAt: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const RUNTIME_FILE = "runtime.json";
|
|
11
|
+
export declare function runtimeFilePath(home: string): string;
|
|
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, "startedAt"> & {
|
|
14
|
+
startedAt?: string;
|
|
15
|
+
}): RuntimeInfo;
|
|
16
|
+
/** The file's contents, or null if it's missing or isn't a runtime file (a half-written or foreign file). */
|
|
17
|
+
export declare function readRuntimeFile(home: string): RuntimeInfo | null;
|
|
18
|
+
/**
|
|
19
|
+
* Removes the file, but only if it's still ours: a second instance may have started since and written its own.
|
|
20
|
+
* Call it on a clean exit. Returns whether it removed anything.
|
|
21
|
+
*/
|
|
22
|
+
export declare function removeRuntimeFile(home: string, pid?: number): boolean;
|
|
23
|
+
/** Whether a process with this pid exists. EPERM means it exists but isn't ours, which still counts. */
|
|
24
|
+
export declare function isPidAlive(pid: number): boolean;
|
|
25
|
+
/** The runtime file, if the process it names is still alive. A crashed instance leaves a stale file behind. */
|
|
26
|
+
export declare function readLiveRuntimeFile(home: string): RuntimeInfo | null;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// From edspencer/cli-contract reference/runtime-file.ts @ SPEC 0.1; local changes: `node:fs` / `node:path` imports
|
|
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
|
+
//
|
|
6
|
+
// `<home>/runtime.json`: what a running instance tells its own CLI (SPEC §4, ROADMAP). `serve` writes it once it's
|
|
7
|
+
// listening and removes it on a clean exit; `busy`, `service status` and `update` read it to find the instance.
|
|
8
|
+
//
|
|
9
|
+
// It holds `{ pid, port, url, version, startedAt }`. A token for /api/busy will join it once SPEC §10's first question
|
|
10
|
+
// is settled; the file is written 0600 already, so adding one later changes nothing about who can read it.
|
|
11
|
+
// Node built-ins only.
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
export const RUNTIME_FILE = "runtime.json";
|
|
15
|
+
export function runtimeFilePath(home) {
|
|
16
|
+
return path.join(home, RUNTIME_FILE);
|
|
17
|
+
}
|
|
18
|
+
/** Writes the file atomically (a temporary file, then a rename), readable only by this user. */
|
|
19
|
+
export function writeRuntimeFile(home, info) {
|
|
20
|
+
const full = {
|
|
21
|
+
pid: info.pid,
|
|
22
|
+
port: info.port,
|
|
23
|
+
url: info.url,
|
|
24
|
+
version: info.version,
|
|
25
|
+
startedAt: info.startedAt ?? new Date().toISOString(),
|
|
26
|
+
};
|
|
27
|
+
fs.mkdirSync(home, { recursive: true });
|
|
28
|
+
const file = runtimeFilePath(home);
|
|
29
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
30
|
+
fs.writeFileSync(tmp, `${JSON.stringify(full, null, 2)}\n`, { mode: 0o600 });
|
|
31
|
+
try {
|
|
32
|
+
fs.renameSync(tmp, file);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
fs.rmSync(tmp, { force: true });
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
return full;
|
|
39
|
+
}
|
|
40
|
+
/** The file's contents, or null if it's missing or isn't a runtime file (a half-written or foreign file). */
|
|
41
|
+
export function readRuntimeFile(home) {
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = fs.readFileSync(runtimeFilePath(home), "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const v = JSON.parse(raw);
|
|
51
|
+
if (Number.isInteger(v.pid) &&
|
|
52
|
+
Number.isInteger(v.port) &&
|
|
53
|
+
typeof v.url === "string" &&
|
|
54
|
+
typeof v.version === "string" &&
|
|
55
|
+
typeof v.startedAt === "string") {
|
|
56
|
+
return v;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// fall through
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Removes the file, but only if it's still ours: a second instance may have started since and written its own.
|
|
66
|
+
* Call it on a clean exit. Returns whether it removed anything.
|
|
67
|
+
*/
|
|
68
|
+
export function removeRuntimeFile(home, pid = process.pid) {
|
|
69
|
+
const info = readRuntimeFile(home);
|
|
70
|
+
if (!info || info.pid !== pid)
|
|
71
|
+
return false;
|
|
72
|
+
fs.rmSync(runtimeFilePath(home), { force: true });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
/** Whether a process with this pid exists. EPERM means it exists but isn't ours, which still counts. */
|
|
76
|
+
export function isPidAlive(pid) {
|
|
77
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
78
|
+
return false;
|
|
79
|
+
try {
|
|
80
|
+
process.kill(pid, 0);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
return err.code === "EPERM";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** The runtime file, if the process it names is still alive. A crashed instance leaves a stale file behind. */
|
|
88
|
+
export function readLiveRuntimeFile(home) {
|
|
89
|
+
const info = readRuntimeFile(home);
|
|
90
|
+
return info && isPidAlive(info.pid) ? info : null;
|
|
91
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Rotate once the log passes this. */
|
|
2
|
+
export declare const ROTATE_AT_BYTES: number;
|
|
3
|
+
/** How many rotated generations to keep (`service.log.1` … `.3`). */
|
|
4
|
+
export declare const KEEP_GENERATIONS = 3;
|
|
5
|
+
/** How often to look. */
|
|
6
|
+
export declare const ROTATE_CHECK_MS: number;
|
|
7
|
+
export interface RotateOptions {
|
|
8
|
+
maxBytes?: number;
|
|
9
|
+
keep?: number;
|
|
10
|
+
/** Where a warning goes. Failing to rotate is never fatal. */
|
|
11
|
+
warn?: (message: string) => void;
|
|
12
|
+
}
|
|
13
|
+
/** `<dataDir>/logs/service.log` — the one file both streams append to. */
|
|
14
|
+
export declare function serviceLogFile(dataDir: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Rotate `<dataDir>/logs/service.log` if it has grown past the limit.
|
|
17
|
+
*
|
|
18
|
+
* Returns true if it rotated. Never throws: a log directory that has become
|
|
19
|
+
* read-only is worth a warning, not a server that will not start.
|
|
20
|
+
*/
|
|
21
|
+
export declare function rotateServiceLog(dataDir: string, opts?: RotateOptions): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Rotate now, then every {@link ROTATE_CHECK_MS}. Returns a stop function.
|
|
24
|
+
*
|
|
25
|
+
* The timer is unref'd, so this cannot hold the process open — which matters
|
|
26
|
+
* because the one thing worse than an unrotated log is a `paddock serve` that
|
|
27
|
+
* does not exit on Ctrl-C.
|
|
28
|
+
*/
|
|
29
|
+
export declare function startServiceLogRotation(dataDir: string, opts?: RotateOptions): () => void;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rotating the service log (SPEC §7: "the tool rotates its own log").
|
|
3
|
+
*
|
|
4
|
+
* The service's stdout and stderr are appended to `<dataDir>/logs/service.log`
|
|
5
|
+
* by the supervisor itself — launchd's `StandardOutPath`, systemd's
|
|
6
|
+
* `StandardOutput=append:`. Nothing else rotates it: a `systemd --user` unit
|
|
7
|
+
* gets no journal rotation because it is not using the journal, and macOS has
|
|
8
|
+
* never had anything that would. Left alone, an instance that has been up for
|
|
9
|
+
* months has a log file nobody can open.
|
|
10
|
+
*
|
|
11
|
+
* ## Why copy-and-truncate, and not rename
|
|
12
|
+
*
|
|
13
|
+
* The usual rotation is `rename(log, log.1)` then create a new `log`. That is
|
|
14
|
+
* exactly wrong here, because **we are not the writer**. The supervisor holds
|
|
15
|
+
* an open append-mode descriptor on the inode; renaming the path does not move
|
|
16
|
+
* the descriptor, so every subsequent line goes to `service.log.1` and the
|
|
17
|
+
* fresh `service.log` stays empty for ever. `logrotate`'s `copytruncate` exists
|
|
18
|
+
* for this case and this is it: copy the bytes out, then `truncate()` the
|
|
19
|
+
* original to zero. The descriptor's next write — `O_APPEND`, so the offset is
|
|
20
|
+
* re-derived from the file length — lands at 0.
|
|
21
|
+
*
|
|
22
|
+
* The cost is honest and small: a line written between the copy and the
|
|
23
|
+
* truncate is lost. The alternative costs every line after the first rotation.
|
|
24
|
+
*
|
|
25
|
+
* ## When it runs
|
|
26
|
+
*
|
|
27
|
+
* At boot, and then on an interval. Not on every write, because the writes do
|
|
28
|
+
* not come through this process's own logger — they come through a file
|
|
29
|
+
* descriptor the supervisor opened — so there is no write to hook. The timer is
|
|
30
|
+
* `unref`'d: a rotation pending must never be the reason the process will not
|
|
31
|
+
* exit.
|
|
32
|
+
*
|
|
33
|
+
* It is a no-op anywhere there is no service log, which is every foreground
|
|
34
|
+
* `paddock serve`: the file does not exist, so nothing is copied and nothing is
|
|
35
|
+
* truncated. No caller has to know whether it is running under a supervisor.
|
|
36
|
+
*/
|
|
37
|
+
import fs from "node:fs";
|
|
38
|
+
import path from "node:path";
|
|
39
|
+
/** Rotate once the log passes this. */
|
|
40
|
+
export const ROTATE_AT_BYTES = 8 * 1024 * 1024;
|
|
41
|
+
/** How many rotated generations to keep (`service.log.1` … `.3`). */
|
|
42
|
+
export const KEEP_GENERATIONS = 3;
|
|
43
|
+
/** How often to look. */
|
|
44
|
+
export const ROTATE_CHECK_MS = 15 * 60 * 1000;
|
|
45
|
+
/** `<dataDir>/logs/service.log` — the one file both streams append to. */
|
|
46
|
+
export function serviceLogFile(dataDir) {
|
|
47
|
+
return path.join(dataDir, "logs", "service.log");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Rotate `<dataDir>/logs/service.log` if it has grown past the limit.
|
|
51
|
+
*
|
|
52
|
+
* Returns true if it rotated. Never throws: a log directory that has become
|
|
53
|
+
* read-only is worth a warning, not a server that will not start.
|
|
54
|
+
*/
|
|
55
|
+
export function rotateServiceLog(dataDir, opts = {}) {
|
|
56
|
+
const maxBytes = opts.maxBytes ?? ROTATE_AT_BYTES;
|
|
57
|
+
const keep = opts.keep ?? KEEP_GENERATIONS;
|
|
58
|
+
const file = serviceLogFile(dataDir);
|
|
59
|
+
try {
|
|
60
|
+
const size = fs.statSync(file).size;
|
|
61
|
+
if (size <= maxBytes)
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// No log file: a foreground run, or a service that has not written yet.
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
// Shuffle the generations down, oldest first, so nothing is overwritten
|
|
70
|
+
// before it has been moved. The oldest is dropped.
|
|
71
|
+
const gen = (n) => `${file}.${String(n)}`;
|
|
72
|
+
fs.rmSync(gen(keep), { force: true });
|
|
73
|
+
for (let n = keep - 1; n >= 1; n--) {
|
|
74
|
+
if (fs.existsSync(gen(n)))
|
|
75
|
+
fs.renameSync(gen(n), gen(n + 1));
|
|
76
|
+
}
|
|
77
|
+
fs.copyFileSync(file, gen(1));
|
|
78
|
+
// The truncate, not an unlink: see the header. The supervisor's descriptor
|
|
79
|
+
// has to keep working, and it only does if the inode survives.
|
|
80
|
+
fs.truncateSync(file, 0);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
opts.warn?.(`could not rotate ${file}: ${err.message}`);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Rotate now, then every {@link ROTATE_CHECK_MS}. Returns a stop function.
|
|
90
|
+
*
|
|
91
|
+
* The timer is unref'd, so this cannot hold the process open — which matters
|
|
92
|
+
* because the one thing worse than an unrotated log is a `paddock serve` that
|
|
93
|
+
* does not exit on Ctrl-C.
|
|
94
|
+
*/
|
|
95
|
+
export function startServiceLogRotation(dataDir, opts = {}) {
|
|
96
|
+
rotateServiceLog(dataDir, opts);
|
|
97
|
+
const timer = setInterval(() => {
|
|
98
|
+
rotateServiceLog(dataDir, opts);
|
|
99
|
+
}, ROTATE_CHECK_MS);
|
|
100
|
+
timer.unref();
|
|
101
|
+
return () => {
|
|
102
|
+
clearInterval(timer);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -1 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a clean shutdown gets before we exit anyway.
|
|
3
|
+
*
|
|
4
|
+
* The CLI contract requires the SIGTERM path to exit "within the unit's stop
|
|
5
|
+
* timeout" (SPEC §4, §8), and the tightest of those is **launchd's**: with no
|
|
6
|
+
* `ExitTimeOut` in the plist, a LaunchAgent gets 20 seconds before SIGKILL
|
|
7
|
+
* (systemd's user default is 90). So the budget is 20 seconds, and this sits
|
|
8
|
+
* inside it with room for the exit itself.
|
|
9
|
+
*
|
|
10
|
+
* The deadline matters because the two awaits below are not bounded by anything
|
|
11
|
+
* else. `app.close()` waits for in-flight requests and open WebSockets, and a
|
|
12
|
+
* browser holding `/ws` is the normal state of a running Paddock; `fleet.stop()`
|
|
13
|
+
* talks to subprocesses. Exceeding the budget is strictly worse than exiting at
|
|
14
|
+
* it: SIGKILL skips the rest of this function, so the runtime file would be left
|
|
15
|
+
* behind for the next instance's `busy` to find and the log would have no record
|
|
16
|
+
* of what was cut short.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SHUTDOWN_DEADLINE_MS = 15000;
|
|
19
|
+
/**
|
|
20
|
+
* The URL to record in `runtime.json` — the one a CLI on this machine can
|
|
21
|
+
* actually fetch, which is not always the one we bound.
|
|
22
|
+
*
|
|
23
|
+
* A wildcard bind is the normal case for a container or a dev box (`HOST=0.0.0.0`
|
|
24
|
+
* is what this repo's own `pm` wrapper injects), and `http://0.0.0.0:<port>` is
|
|
25
|
+
* not an address to connect TO. Node happens to resolve it to loopback on Linux
|
|
26
|
+
* and fails on some other stacks, so the file records loopback explicitly rather
|
|
27
|
+
* than relying on that. A real interface address is left alone: someone who
|
|
28
|
+
* bound `192.168.1.10` meant it, and that address reaches the server from here
|
|
29
|
+
* as well as from anywhere else.
|
|
30
|
+
*
|
|
31
|
+
* IPv6 gets brackets, because `new URL('http://::1:7233')` is not a URL.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runtimeUrl(host: string, port: number): string;
|
|
1
34
|
export declare function start(): Promise<void>;
|
|
@@ -1,21 +1,139 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Process lifecycle for a Paddock server: bind the port
|
|
3
|
-
* shutdown. Split out of `index.ts` so
|
|
4
|
-
* and the plain `node dist/index.js`
|
|
2
|
+
* Process lifecycle for a Paddock server: bind the port, publish
|
|
3
|
+
* `runtime.json`, and wire signal-driven shutdown. Split out of `index.ts` so
|
|
4
|
+
* the `paddock` CLI (src/cli/paddock.ts) and the plain `node dist/index.js`
|
|
5
|
+
* entrypoint share one implementation.
|
|
5
6
|
*
|
|
6
7
|
* Config is resolved INSIDE `buildApp()`, not at module load, so a caller may
|
|
7
8
|
* still mutate `process.env` right up until this function is invoked. The CLI
|
|
8
9
|
* depends on that to apply its own defaults (e.g. a data dir under $HOME).
|
|
9
10
|
*/
|
|
10
11
|
import { buildApp } from "./app.js";
|
|
12
|
+
import { startServiceLogRotation } from "./service-log.js";
|
|
13
|
+
import { removeRuntimeFile, writeRuntimeFile } from "./runtime-file.js";
|
|
14
|
+
import { PADDOCK_VERSION } from "./version.js";
|
|
15
|
+
/**
|
|
16
|
+
* How long a clean shutdown gets before we exit anyway.
|
|
17
|
+
*
|
|
18
|
+
* The CLI contract requires the SIGTERM path to exit "within the unit's stop
|
|
19
|
+
* timeout" (SPEC §4, §8), and the tightest of those is **launchd's**: with no
|
|
20
|
+
* `ExitTimeOut` in the plist, a LaunchAgent gets 20 seconds before SIGKILL
|
|
21
|
+
* (systemd's user default is 90). So the budget is 20 seconds, and this sits
|
|
22
|
+
* inside it with room for the exit itself.
|
|
23
|
+
*
|
|
24
|
+
* The deadline matters because the two awaits below are not bounded by anything
|
|
25
|
+
* else. `app.close()` waits for in-flight requests and open WebSockets, and a
|
|
26
|
+
* browser holding `/ws` is the normal state of a running Paddock; `fleet.stop()`
|
|
27
|
+
* talks to subprocesses. Exceeding the budget is strictly worse than exiting at
|
|
28
|
+
* it: SIGKILL skips the rest of this function, so the runtime file would be left
|
|
29
|
+
* behind for the next instance's `busy` to find and the log would have no record
|
|
30
|
+
* of what was cut short.
|
|
31
|
+
*/
|
|
32
|
+
export const SHUTDOWN_DEADLINE_MS = 15_000;
|
|
33
|
+
/**
|
|
34
|
+
* The URL to record in `runtime.json` — the one a CLI on this machine can
|
|
35
|
+
* actually fetch, which is not always the one we bound.
|
|
36
|
+
*
|
|
37
|
+
* A wildcard bind is the normal case for a container or a dev box (`HOST=0.0.0.0`
|
|
38
|
+
* is what this repo's own `pm` wrapper injects), and `http://0.0.0.0:<port>` is
|
|
39
|
+
* not an address to connect TO. Node happens to resolve it to loopback on Linux
|
|
40
|
+
* and fails on some other stacks, so the file records loopback explicitly rather
|
|
41
|
+
* than relying on that. A real interface address is left alone: someone who
|
|
42
|
+
* bound `192.168.1.10` meant it, and that address reaches the server from here
|
|
43
|
+
* as well as from anywhere else.
|
|
44
|
+
*
|
|
45
|
+
* IPv6 gets brackets, because `new URL('http://::1:7233')` is not a URL.
|
|
46
|
+
*/
|
|
47
|
+
export function runtimeUrl(host, port) {
|
|
48
|
+
const loopback = {
|
|
49
|
+
"0.0.0.0": "127.0.0.1",
|
|
50
|
+
"::": "::1",
|
|
51
|
+
"": "127.0.0.1",
|
|
52
|
+
};
|
|
53
|
+
const h = loopback[host] ?? host;
|
|
54
|
+
return `http://${h.includes(":") ? `[${h}]` : h}:${String(port)}`;
|
|
55
|
+
}
|
|
11
56
|
export async function start() {
|
|
12
|
-
const { app, cfg, close } = await buildApp();
|
|
13
|
-
|
|
14
|
-
|
|
57
|
+
const { app, cfg, close, hub } = await buildApp();
|
|
58
|
+
// Nothing else rotates the service log: the supervisor appends to it through
|
|
59
|
+
// its own descriptor, a `systemd --user` unit writing to a file gets no
|
|
60
|
+
// journal rotation, and macOS has nothing that would (SPEC §7). A no-op on a
|
|
61
|
+
// foreground run, where that file does not exist — see service-log.ts.
|
|
62
|
+
const stopRotation = startServiceLogRotation(cfg.dataDir, {
|
|
63
|
+
warn: (message) => {
|
|
64
|
+
app.log.warn(message);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Exit on SIGTERM/SIGINT having said what was cut short (SPEC §4, §8).
|
|
69
|
+
*
|
|
70
|
+
* Three things are deliberate here.
|
|
71
|
+
*
|
|
72
|
+
* **It records rather than finishes.** A chat turn is a `claude` subprocess
|
|
73
|
+
* that can run for minutes; no 20-second stop timeout can wait for one, and
|
|
74
|
+
* `herdctl.stop()` ends the fleet with `waitForJobs: false` precisely because
|
|
75
|
+
* waiting is not an option. So the contract's other half applies — record the
|
|
76
|
+
* work under way — and the record is this `warn` line in the service log,
|
|
77
|
+
* which is the one artefact that survives the restart and which
|
|
78
|
+
* `paddock service logs` reads. What is NOT done, and should not be, is
|
|
79
|
+
* writing a note into the cut-short chat's transcript: `claude` owns that file
|
|
80
|
+
* and may append to it after we are gone, so a server-side write cannot be
|
|
81
|
+
* ordered against the subprocess's own and would be the resurrection race
|
|
82
|
+
* `turn-interlock.ts` exists to avoid.
|
|
83
|
+
*
|
|
84
|
+
* The reason a count is enough: `/api/busy` reports the same numbers BEFORE
|
|
85
|
+
* the stop, and `paddock service stop` refuses on them. The log line is for
|
|
86
|
+
* the race §4 admits — work that started between the check and the signal.
|
|
87
|
+
*
|
|
88
|
+
* **It is idempotent.** A supervisor that sends SIGTERM twice, or a Ctrl-C
|
|
89
|
+
* after a SIGTERM, used to re-enter `close()` concurrently: a second
|
|
90
|
+
* `app.close()` on a closing Fastify rejects, and a second `fleet.stop()`
|
|
91
|
+
* races the first. The guard makes the extra signals no-ops.
|
|
92
|
+
*
|
|
93
|
+
* **It exits 0.** The unit's `Restart=always` / `KeepAlive` means the
|
|
94
|
+
* supervisor brings Paddock back whatever the code; 0 is what makes a logout
|
|
95
|
+
* or a `service stop` read as a clean stop rather than a crash in the logs.
|
|
96
|
+
*/
|
|
97
|
+
let shuttingDown = false;
|
|
98
|
+
const shutdown = async (signal) => {
|
|
99
|
+
if (shuttingDown)
|
|
100
|
+
return;
|
|
101
|
+
shuttingDown = true;
|
|
102
|
+
const running = hub.runningSessions().length;
|
|
103
|
+
app.log.warn(running > 0
|
|
104
|
+
? `${signal}: shutting down with ${running} chat turn(s) in flight — they are being cut short. ` +
|
|
105
|
+
"Run `paddock busy` before a stop or an update to see this before it happens."
|
|
106
|
+
: `${signal}: shutting down, no chat turn in flight.`);
|
|
107
|
+
stopRotation();
|
|
108
|
+
// Published once we were listening; removed here so a `busy` or a
|
|
109
|
+
// `service status` run after this exit does not find a live-looking
|
|
110
|
+
// instance. Before the slow awaits, because this is the part that must land
|
|
111
|
+
// even if they overrun the deadline below.
|
|
112
|
+
removeRuntimeFile(cfg.dataDir);
|
|
113
|
+
// Bounded: see SHUTDOWN_DEADLINE_MS. `exit(0)` either way, so a hung
|
|
114
|
+
// `app.close()` cannot turn a stop into a SIGKILL.
|
|
115
|
+
await Promise.race([close(), new Promise((r) => setTimeout(r, SHUTDOWN_DEADLINE_MS))]).catch(() => undefined);
|
|
15
116
|
process.exit(0);
|
|
16
117
|
};
|
|
17
|
-
process.on("SIGINT", shutdown);
|
|
18
|
-
process.on("SIGTERM", shutdown);
|
|
118
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
119
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
19
120
|
await app.listen({ port: cfg.port, host: cfg.host });
|
|
121
|
+
// `<dataDir>/runtime.json`, 0600, written only once we are actually listening
|
|
122
|
+
// (SPEC §4) — it is how `paddock busy`, and later `update`, find THIS instance
|
|
123
|
+
// without guessing a port. A failure to write it is logged and not fatal: the
|
|
124
|
+
// server is up and serving, and the one thing that stops working is a CLI
|
|
125
|
+
// finding it, which is better than refusing to run.
|
|
126
|
+
try {
|
|
127
|
+
writeRuntimeFile(cfg.dataDir, {
|
|
128
|
+
pid: process.pid,
|
|
129
|
+
port: cfg.port,
|
|
130
|
+
url: runtimeUrl(cfg.host, cfg.port),
|
|
131
|
+
version: PADDOCK_VERSION,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
app.log.warn(`could not write ${cfg.dataDir}/runtime.json (${err.message}) — ` +
|
|
136
|
+
"`paddock busy` will report this instance as not running");
|
|
137
|
+
}
|
|
20
138
|
app.log.info(`paddock-server listening on http://${cfg.host}:${cfg.port}`);
|
|
21
139
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The running Paddock version, resolved once.
|
|
3
|
+
*
|
|
4
|
+
* Three surfaces have to agree on this string and now do, because they read it
|
|
5
|
+
* from one place: the OpenAPI document's `info.version` (`app.ts`), the
|
|
6
|
+
* management MCP server's advertised version, and — the reason this module
|
|
7
|
+
* exists — `GET /api/health`, which the CLI contract requires to carry the
|
|
8
|
+
* version so that `paddock service status` can compare the version that is
|
|
9
|
+
* RUNNING with the version that is installed (SPEC §8).
|
|
10
|
+
*
|
|
11
|
+
* `createRequire(import.meta.url)` resolves against this module's own path, so
|
|
12
|
+
* `../package.json` is `packages/server/package.json` from `dist/version.js` in
|
|
13
|
+
* both layouts the package ships in (the repo, and the synthesized npm tarball
|
|
14
|
+
* — see `scripts/make-npm-package.mjs`, which keeps that manifest precisely so
|
|
15
|
+
* a version is readable at runtime).
|
|
16
|
+
*
|
|
17
|
+
* Never throws. A missing or unreadable manifest degrades to `"0.0.0"`: a
|
|
18
|
+
* health endpoint that 500s because it could not find its own version would be
|
|
19
|
+
* a worse failure than one that answers with a version nobody shipped.
|
|
20
|
+
*/
|
|
21
|
+
import { createRequire } from "node:module";
|
|
22
|
+
export const PADDOCK_VERSION = (() => {
|
|
23
|
+
try {
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
return require("../package.json").version ?? "0.0.0";
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return "0.0.0";
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
/** What the product is called in prose, for `serve`'s banner line (SPEC §3). */
|
|
32
|
+
export const PADDOCK_NAME = "Paddock";
|
|
@@ -14,6 +14,7 @@ import type { SweepService } from "./sweep.js";
|
|
|
14
14
|
import type { PaddockConfig } from "./config.js";
|
|
15
15
|
import type { DriveMode } from "./models.js";
|
|
16
16
|
import type { SessionHub } from "./session-hub.js";
|
|
17
|
+
import type { BackgroundRegistry } from "./background-live.js";
|
|
17
18
|
import type { AttachmentStore } from "./attachments.js";
|
|
18
19
|
import type { QueuedMessageStore } from "./queued-message.js";
|
|
19
20
|
import type { ArchiveStore } from "./archive.js";
|
|
@@ -152,6 +153,14 @@ export type FireTrigger = (slug: string, triggerName: string) => Promise<string
|
|
|
152
153
|
export interface ChatHandlerContext {
|
|
153
154
|
deps: ChatHandlerDeps;
|
|
154
155
|
hub: SessionHub;
|
|
156
|
+
/**
|
|
157
|
+
* The live background-work registry (#604). Here for the same reason `hub` is:
|
|
158
|
+
* it is per-server state owned by the WS layer that a NON-socket caller has to
|
|
159
|
+
* be able to read. `GET /api/busy` is that caller — a turn can end while its
|
|
160
|
+
* background tasks keep running, so the hub alone would report a session with
|
|
161
|
+
* a live sub-agent as idle (CLI contract SPEC §4).
|
|
162
|
+
*/
|
|
163
|
+
background: BackgroundRegistry;
|
|
155
164
|
startAgentTurn: StartAgentTurn;
|
|
156
165
|
composePreloadedPrompt: ComposePreloadedPrompt;
|
|
157
166
|
fireTrigger: FireTrigger;
|
package/packages/web/dist/assets/{DeleteProjectDialog-BcxJMA1w.js → DeleteProjectDialog-D8oIJ8HC.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-CvaWErFv.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
|
|