@edspencer/paddock 0.66.2 → 0.68.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/README.md +24 -30
- package/package.json +1 -1
- package/packages/server/dist/adoptable.d.ts +57 -0
- package/packages/server/dist/adoptable.js +26 -3
- package/packages/server/dist/agent-errors.js +1 -1
- package/packages/server/dist/claude-home.d.ts +4 -4
- package/packages/server/dist/claude-home.js +4 -4
- package/packages/server/dist/claude-mcp.d.ts +4 -2
- package/packages/server/dist/cli/args.d.ts +41 -2
- package/packages/server/dist/cli/args.js +119 -17
- package/packages/server/dist/cli/paddock.js +87 -96
- package/packages/server/dist/cli/service/backend.d.ts +38 -0
- package/packages/server/dist/cli/service/backend.js +19 -0
- package/packages/server/dist/cli/service/index.d.ts +28 -0
- package/packages/server/dist/cli/service/index.js +179 -0
- package/packages/server/dist/cli/service/launchd.d.ts +47 -0
- package/packages/server/dist/cli/service/launchd.js +206 -0
- package/packages/server/dist/cli/service/spec.d.ts +106 -0
- package/packages/server/dist/cli/service/spec.js +124 -0
- package/packages/server/dist/cli/service/systemd.d.ts +48 -0
- package/packages/server/dist/cli/service/systemd.js +155 -0
- package/packages/server/dist/discover.d.ts +170 -0
- package/packages/server/dist/discover.js +365 -0
- package/packages/server/dist/herdctl.d.ts +25 -2
- package/packages/server/dist/herdctl.js +30 -1
- package/packages/server/dist/routes/chats.js +14 -2
- package/packages/server/dist/routes/discover.d.ts +3 -0
- package/packages/server/dist/routes/discover.js +140 -0
- package/packages/server/dist/routes.d.ts +1 -0
- package/packages/server/dist/routes.js +2 -0
- package/packages/server/dist/session-hub.d.ts +10 -0
- package/packages/server/dist/session-hub.js +16 -2
- package/packages/server/dist/ws-protocol.d.ts +15 -0
- package/packages/server/dist/ws.js +1 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/Chip-DIdRqcqU.js +2 -0
- package/packages/web/dist/assets/DiscoverPage-BmCWnkjN.js +2 -0
- package/packages/web/dist/assets/DiscoverView-DwhtBdN2.js +2 -0
- package/packages/web/dist/assets/InstanceConfigPage-D01x1BUg.js +2 -0
- package/packages/web/dist/assets/{MarkdownRenderer-DDVO0zhl.js → MarkdownRenderer-BCheNET2.js} +2 -2
- package/packages/web/dist/assets/ProjectMenu-BqlUQuxM.js +2 -0
- package/packages/web/dist/assets/ProjectRedirect-D8QDxYLG.js +2 -0
- package/packages/web/dist/assets/ProjectView-cevSYdcl.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-yd18DXdV.js +2 -0
- package/packages/web/dist/assets/RootHome-DZEkOjko.js +2 -0
- package/packages/web/dist/assets/Surfaces-DvRztndg.js +2 -0
- package/packages/web/dist/assets/{arc-DXqVnx_r.js → arc-DFqBni1E.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-FgkfD1Bk.js → architectureDiagram-3BPJPVTR-DXFeq3pL.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-WwkIIJsI.js → blockDiagram-GPEHLZMM-DpeYsIwP.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-BIzkTibx.js → c4Diagram-AAUBKEIU-Bw5r8wjW.js} +1 -1
- package/packages/web/dist/assets/channel-DU94S7yB.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-C4TBH3R6.js → chunk-2J33WTMH-B0SAvGOr.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-DTKJZlRM.js → chunk-4BX2VUAB-BaUGs1hD.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-BC9_Qc8h.js → chunk-55IACEB6-BDHRluF5.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-D3gQ4rP3.js → chunk-727SXJPM-BX93DBZO.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-My2AXBvz.js → chunk-AQP2D5EJ-3RiQGxW2.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-D09zE6Ow.js → chunk-FMBD7UC4-DMkjv9SP.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-CUSgeCLl.js → chunk-ND2GUHAM-CHiQfq4F.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-B6jVPYgy.js → chunk-QZHKN3VN-B1wCSHbM.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-BcRv1OPp.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-BcRv1OPp.js +2 -0
- package/packages/web/dist/assets/{core-DPY0seI-.js → core-C-49RP59.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BasCiARv.js → cose-bilkent-S5V4N54A-BIwwgSTZ.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-Cimn11Qe.js → dagre-BM42HDAG-BN45kshV.js} +1 -1
- package/packages/web/dist/assets/diagram-2AECGRRQ-C5NekbfR.js +44 -0
- package/packages/web/dist/assets/diagram-5GNKFQAL-De7wdssd.js +11 -0
- package/packages/web/dist/assets/{diagram-KO2AKTUF-dLORrSiu.js → diagram-KO2AKTUF-CgG0UA0n.js} +3 -3
- package/packages/web/dist/assets/{diagram-LMA3HP47-iUMeaR-J.js → diagram-LMA3HP47-DC-bDqxV.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-oEH6olHK.js → diagram-OG6HWLK6-XXfHDMhE.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-VtQqF9SY.js → erDiagram-TEJ5UH35-CHEd8JiS.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-wvF2EkJa.js → flowDiagram-I6XJVG4X-BbuYxd15.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-ng4qJ0E1.js → ganttDiagram-6RSMTGT7-BkF77xn5.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-BV4aWy8k.js → gitGraphDiagram-PVQCEYII-BdL5zVez.js} +2 -2
- package/packages/web/dist/assets/index-C3MdCCzF.js +71 -0
- package/packages/web/dist/assets/index-DYccdR9x.css +1 -0
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-BpGTxAjU.js → infoDiagram-5YYISTIA-vhTRATYg.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-Cj36LyoS.js → ishikawaDiagram-YF4QCWOH-CxjN-7YF.js} +2 -2
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-0vNlg3um.js → journeyDiagram-JHISSGLW-DJ414TaX.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-wijkEDaR.js → kanban-definition-UN3LZRKU-gaQeFRvZ.js} +1 -1
- package/packages/web/dist/assets/{linear-LqRymMD_.js → linear-CcvdFgxd.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-DPrhSs95.js → mermaid.core-Es6hedDB.js} +5 -5
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-en-uGqgC.js → mindmap-definition-RKZ34NQL-Dk7qf3FR.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-DKBZ2Vnx.js → pieDiagram-4H26LBE5-CSWXwU6d.js} +3 -3
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB--g-WtxyL.js → quadrantDiagram-W4KKPZXB-CBVdURbr.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-Dl4eYxDI.js → requirementDiagram-4Y6WPE33-CbU18IZA.js} +3 -3
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-7lt4gqSw.js → sankeyDiagram-5OEKKPKP-BeAlNgjE.js} +4 -4
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-COgq-dXg.js → sequenceDiagram-3UESZ5HK-CKVKzxTs.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-CL6XC79S.js → stateDiagram-AJRCARHV-B-aSaO4W.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-TptFh32C.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-C5egKeGT.js → timeline-definition-PNZ67QCA-CP9JHEBr.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-C_UhtC8u.js → vennDiagram-CIIHVFJN-Dbtwwvvd.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-Bj9chUqz.js → wardley-L42UT6IY-NonvEhoT.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-D7VnZoRp.js → wardleyDiagram-YWT4CUSO-CDnpv9Ws.js} +2 -2
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-VrWLDoi4.js → xychartDiagram-2RQKCTM6-C1i-Wk0H.js} +1 -1
- package/packages/web/dist/fonts/alegreya-latin-italic.woff2 +0 -0
- package/packages/web/dist/fonts/alegreya-latin.woff2 +0 -0
- package/packages/web/dist/fonts/alegreya-sc-latin-500.woff2 +0 -0
- package/packages/web/dist/fonts/alegreya-sc-latin-700.woff2 +0 -0
- package/packages/web/dist/fonts/courier-prime-latin-400.woff2 +0 -0
- package/packages/web/dist/fonts/courier-prime-latin-700.woff2 +0 -0
- package/packages/web/dist/fonts/courier-prime-latin-italic.woff2 +0 -0
- package/packages/web/dist/fonts/iosevka-terminal-400.woff2 +0 -0
- package/packages/web/dist/fonts/iosevka-terminal-700.woff2 +0 -0
- package/packages/web/dist/fonts/martian-mono-latin.woff2 +0 -0
- package/packages/web/dist/fonts/saira-latin.woff2 +0 -0
- package/packages/web/dist/index.html +61 -12
- package/packages/web/dist/sw.js +1 -1
- package/packages/server/dist/cli/here.d.ts +0 -25
- package/packages/server/dist/cli/here.js +0 -104
- package/packages/web/dist/assets/InstanceConfigPage-DKzAHOhx.js +0 -2
- package/packages/web/dist/assets/ProjectRedirect-WczXTV0_.js +0 -2
- package/packages/web/dist/assets/ProjectView-Dd_myWpV.js +0 -13
- package/packages/web/dist/assets/ProjectsGrid-BeJj0q6Y.js +0 -2
- package/packages/web/dist/assets/channel-DUsOl5_p.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CIdDKEqH.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CIdDKEqH.js +0 -2
- package/packages/web/dist/assets/diagram-2AECGRRQ-fe9cDHyn.js +0 -44
- package/packages/web/dist/assets/diagram-5GNKFQAL-T0tQlfbl.js +0 -11
- package/packages/web/dist/assets/format-CvRqOKZU.js +0 -2
- package/packages/web/dist/assets/index-DGNSeqL-.js +0 -71
- package/packages/web/dist/assets/index-DMRbU1Jl.css +0 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BDlRyphY.js +0 -2
|
@@ -23,10 +23,19 @@ import os from "node:os";
|
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { spawn } from "node:child_process";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
/**
|
|
29
|
-
|
|
26
|
+
import { CliError, USAGE, SERVICE_USAGE, parseCommand, nodeVersionProblem, explainListenError, } from "./args.js";
|
|
27
|
+
import { runService, safeHomeDir } from "./service/index.js";
|
|
28
|
+
/**
|
|
29
|
+
* This module's own file and directory: `<…>/packages/server/dist/cli/`.
|
|
30
|
+
*
|
|
31
|
+
* `import.meta.url` is the module's REALPATH, which is why the service unit
|
|
32
|
+
* names this rather than `process.argv[1]` — npm installs a `bin` as a symlink,
|
|
33
|
+
* and a unit file pointing at `node_modules/.bin/paddock` would depend on that
|
|
34
|
+
* symlink surviving. (The same realpath-vs-argv[1] mismatch is what shipped a
|
|
35
|
+
* silent no-op to npm three times; see args.ts.)
|
|
36
|
+
*/
|
|
37
|
+
const entryScript = fileURLToPath(import.meta.url);
|
|
38
|
+
const moduleDir = path.dirname(entryScript);
|
|
30
39
|
/**
|
|
31
40
|
* Walk up from this module looking for `rel`, returning the containing dir.
|
|
32
41
|
*
|
|
@@ -95,6 +104,14 @@ function addBundledBinsToPath() {
|
|
|
95
104
|
* `npx` is stateless enough that people reasonably assume the whole thing is
|
|
96
105
|
* ephemeral. It is not — projects, chat transcripts, and settings all persist
|
|
97
106
|
* here across runs, so say so once, at the moment the directory appears.
|
|
107
|
+
*
|
|
108
|
+
* The third line replaces `--here`'s discovery hint (#798). That hint scanned
|
|
109
|
+
* the CURRENT directory's Claude Code history and named a flag; both halves are
|
|
110
|
+
* gone. What a new instance actually offers is Discover (#745), which reads the
|
|
111
|
+
* whole history rather than one directory — so the CLI names it and lets the
|
|
112
|
+
* UI, which can show the list with tick-boxes against it, do the work. No
|
|
113
|
+
* filesystem scan happens here: the line is true whatever a scan would find,
|
|
114
|
+
* and an empty instance's Home leads with the same thing.
|
|
98
115
|
*/
|
|
99
116
|
function noteFirstRun(dataDir) {
|
|
100
117
|
if (fs.existsSync(dataDir))
|
|
@@ -103,57 +120,7 @@ function noteFirstRun(dataDir) {
|
|
|
103
120
|
"",
|
|
104
121
|
` Welcome to Paddock. Creating a new instance in ${dataDir}`,
|
|
105
122
|
" Projects, chats and settings persist there between runs.",
|
|
106
|
-
"",
|
|
107
|
-
].join("\n"));
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Say what `--here` is about to do to this directory, on the run that does it.
|
|
111
|
-
*
|
|
112
|
-
* The flag IS the consent — there is no prompt, and no `--yes` to remember.
|
|
113
|
-
* But consent only means something if you can know what you agreed to, so the
|
|
114
|
-
* effects are announced rather than asked about. Printed only on the first
|
|
115
|
-
* `--here` in a directory; resuming an already-opened one says nothing.
|
|
116
|
-
*/
|
|
117
|
-
function announceHereConsent(dir, dataDir) {
|
|
118
|
-
console.log([
|
|
119
|
-
"",
|
|
120
|
-
` Opening ${dir} as a Paddock workspace.`,
|
|
121
|
-
` · ${path.relative(dir, dataDir) || HERE_MARKER}/ and .chats/ created here, and added to .gitignore`,
|
|
122
|
-
// Worth stating because it is not what you would predict: paddock reading
|
|
123
|
-
// the home a user's sessions live in does not turn them into paddock's
|
|
124
|
-
// chats. Verified against a running instance — a pre-existing transcript
|
|
125
|
-
// for the directory comes back from `adoptable-chats` (offered) and NOT
|
|
126
|
-
// from `chats` (listed), because session discovery is attribution-based,
|
|
127
|
-
// not "whatever is in the folder". So #663's confirmation still gates
|
|
128
|
-
// every import.
|
|
129
|
-
" · ~/.claude sessions for this directory are offered for import (nothing is moved)",
|
|
130
|
-
" Later runs here resume it — no flag needed.",
|
|
131
|
-
"",
|
|
132
|
-
].join("\n"));
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Tell the user their existing Claude history could be opened here.
|
|
136
|
-
*
|
|
137
|
-
* The only way anyone discovers `--here` exists. Read-only: a bare run counts
|
|
138
|
-
* transcripts and prints, and changes nothing on disk.
|
|
139
|
-
*/
|
|
140
|
-
function offerHereIfSessionsExist(dir) {
|
|
141
|
-
// The USER's own home, always: this is looking for `claude` history they made
|
|
142
|
-
// in a terminal, which is by definition in `~/.claude` and never in paddock's.
|
|
143
|
-
const claudeHome = path.join(os.homedir(), ".claude");
|
|
144
|
-
let sessions = 0;
|
|
145
|
-
try {
|
|
146
|
-
sessions = countClaudeSessions(claudeHome, dir);
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
return; // detection is a courtesy; never let it break a boot
|
|
150
|
-
}
|
|
151
|
-
if (sessions === 0)
|
|
152
|
-
return;
|
|
153
|
-
console.log([
|
|
154
|
-
"",
|
|
155
|
-
` This directory has ${sessions} Claude Code session${sessions === 1 ? "" : "s"}.`,
|
|
156
|
-
" Run `paddock --here` to open it as a workspace, where they can be imported.",
|
|
123
|
+
" Open the app to find directories you have used Claude Code in.",
|
|
157
124
|
"",
|
|
158
125
|
].join("\n"));
|
|
159
126
|
}
|
|
@@ -175,18 +142,59 @@ function openBrowser(url) {
|
|
|
175
142
|
/* as above */
|
|
176
143
|
}
|
|
177
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* `paddock service …` — register/inspect the background service (#796).
|
|
147
|
+
*
|
|
148
|
+
* Split out so `main` stays the start path. Note what it is handed: the
|
|
149
|
+
* interpreter and script by absolute path, and `packageRoot` — which is tested
|
|
150
|
+
* for npx's cache, because a unit file pointing into a prunable hash-keyed
|
|
151
|
+
* directory rots at some future login with nobody watching.
|
|
152
|
+
*/
|
|
153
|
+
function service(command) {
|
|
154
|
+
const { action, opts } = command;
|
|
155
|
+
// Unreachable: `parseCommand` only omits the action when `--help` was given,
|
|
156
|
+
// which `main` has already handled. Typed rather than asserted.
|
|
157
|
+
if (action === undefined) {
|
|
158
|
+
console.log(SERVICE_USAGE);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
runService(action, opts, {
|
|
163
|
+
platform: process.platform,
|
|
164
|
+
nodePath: process.execPath,
|
|
165
|
+
scriptPath: entryScript,
|
|
166
|
+
packageRoot,
|
|
167
|
+
homeDir: safeHomeDir(),
|
|
168
|
+
...(process.env.PADDOCK_DATA_DIR !== undefined
|
|
169
|
+
? { envDataDir: process.env.PADDOCK_DATA_DIR }
|
|
170
|
+
: {}),
|
|
171
|
+
...(process.env.XDG_CONFIG_HOME !== undefined
|
|
172
|
+
? { xdgConfigHome: process.env.XDG_CONFIG_HOME }
|
|
173
|
+
: {}),
|
|
174
|
+
...(process.env.PATH !== undefined ? { pathEnv: process.env.PATH } : {}),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
if (err instanceof CliError)
|
|
179
|
+
fail(err.message);
|
|
180
|
+
if (err instanceof Error)
|
|
181
|
+
fail(err.message);
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
178
185
|
async function main() {
|
|
179
|
-
let
|
|
186
|
+
let command;
|
|
180
187
|
try {
|
|
181
|
-
|
|
188
|
+
command = parseCommand(process.argv.slice(2));
|
|
182
189
|
}
|
|
183
190
|
catch (err) {
|
|
184
191
|
if (err instanceof CliError)
|
|
185
192
|
fail(err.message);
|
|
186
193
|
throw err;
|
|
187
194
|
}
|
|
195
|
+
const opts = command.opts;
|
|
188
196
|
if (opts.help) {
|
|
189
|
-
console.log(USAGE);
|
|
197
|
+
console.log(command.verb === "service" ? SERVICE_USAGE : USAGE);
|
|
190
198
|
return;
|
|
191
199
|
}
|
|
192
200
|
if (opts.version) {
|
|
@@ -196,23 +204,21 @@ async function main() {
|
|
|
196
204
|
const problem = nodeVersionProblem(process.versions.node);
|
|
197
205
|
if (problem !== undefined)
|
|
198
206
|
fail(problem);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// (config.ts). On macOS a /tmp vs /private/tmp divergence would silently
|
|
203
|
-
// break adoption — and macOS is most laptops.
|
|
204
|
-
let cwd = process.cwd();
|
|
205
|
-
try {
|
|
206
|
-
cwd = fs.realpathSync(cwd);
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
/* keep the literal path; the server will surface any real problem */
|
|
207
|
+
if (command.verb === "service") {
|
|
208
|
+
service(command);
|
|
209
|
+
return;
|
|
210
210
|
}
|
|
211
|
-
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
211
|
+
addBundledBinsToPath();
|
|
212
|
+
// NOTHING here reads `process.cwd()`, and that is the point (#798). `--here`
|
|
213
|
+
// used to make the current directory the workspace by setting
|
|
214
|
+
// `PADDOCK_PROJECTS_DIR = cwd`, which was the only place that variable was
|
|
215
|
+
// ever set from cwd. It also made the instance you got depend on where you
|
|
216
|
+
// stood — and because its marker directory was `.paddock`, the same name as
|
|
217
|
+
// the default data dir, a bare run from `$HOME` on any machine that had ever
|
|
218
|
+
// run paddock resumed `$HOME` itself as the workspace, silently, even with an
|
|
219
|
+
// explicit `--data-dir`. An instance is now decided by its data dir alone;
|
|
220
|
+
// directories are added inside the app as linked projects (Discover, #745),
|
|
221
|
+
// and the CLI writes nothing into any of them.
|
|
216
222
|
// No Claude-home choice happens here any more (#691). The CLI used to point
|
|
217
223
|
// `CLAUDE_HOME` at `~/.claude` so a macOS Keychain login would be visible
|
|
218
224
|
// (#683); paddock now always owns its home, and what a user actually wanted
|
|
@@ -224,24 +230,8 @@ async function main() {
|
|
|
224
230
|
// server resolves config inside `buildApp()`, so anything set before the
|
|
225
231
|
// dynamic import below is picked up with no special-casing in config.ts.
|
|
226
232
|
// Precedence is explicit flag > existing env > our default.
|
|
227
|
-
const dataDir = path.resolve(opts.dataDir ??
|
|
228
|
-
|
|
229
|
-
(hereMode ? path.join(cwd, HERE_MARKER) : path.join(os.homedir(), ".paddock")));
|
|
230
|
-
if (hereMode) {
|
|
231
|
-
if (!resuming)
|
|
232
|
-
announceHereConsent(cwd, dataDir);
|
|
233
|
-
// The root workspace IS `projectsRoot` (its key is ""), so pointing this at
|
|
234
|
-
// the user's directory makes that directory the workspace. No schema change,
|
|
235
|
-
// no new project type — see cli/here.ts.
|
|
236
|
-
process.env.PADDOCK_PROJECTS_DIR = cwd;
|
|
237
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
238
|
-
ensureGitignored(cwd, `/${HERE_MARKER}/`);
|
|
239
|
-
ensureGitignored(cwd, "/.chats/");
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
noteFirstRun(dataDir);
|
|
243
|
-
offerHereIfSessionsExist(cwd);
|
|
244
|
-
}
|
|
233
|
+
const dataDir = path.resolve(opts.dataDir ?? process.env.PADDOCK_DATA_DIR ?? path.join(os.homedir(), ".paddock"));
|
|
234
|
+
noteFirstRun(dataDir);
|
|
245
235
|
process.env.PADDOCK_DATA_DIR = dataDir;
|
|
246
236
|
if (opts.port)
|
|
247
237
|
process.env.PORT = opts.port;
|
|
@@ -288,13 +278,14 @@ async function main() {
|
|
|
288
278
|
fail(explainListenError(err, host, port));
|
|
289
279
|
}
|
|
290
280
|
const url = `http://${host}:${port}`;
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
281
|
+
// Still name the data dir, even though it no longer varies with cwd: "which
|
|
282
|
+
// instance am I looking at" is a real question the moment anyone passes
|
|
283
|
+
// `--data-dir`, and the answer should be readable off the two lines the tool
|
|
284
|
+
// just printed rather than inferred.
|
|
294
285
|
console.log([
|
|
295
286
|
"",
|
|
296
287
|
` Paddock is running at ${url}`,
|
|
297
|
-
`
|
|
288
|
+
` Data: ${dataDir}`,
|
|
298
289
|
" Press Ctrl-C to stop.",
|
|
299
290
|
"",
|
|
300
291
|
].join("\n"));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ServiceSpec } from "./spec.js";
|
|
2
|
+
export interface RunResult {
|
|
3
|
+
status: number | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Run an external command.
|
|
9
|
+
*
|
|
10
|
+
* Injected rather than imported so the install/uninstall/status flows can be
|
|
11
|
+
* driven in tests on a box that has neither `launchctl` nor a user systemd.
|
|
12
|
+
*/
|
|
13
|
+
export type Runner = (cmd: string, args: string[]) => RunResult;
|
|
14
|
+
export declare const spawnRunner: Runner;
|
|
15
|
+
export interface ServiceState {
|
|
16
|
+
/** Is a unit file installed for us? */
|
|
17
|
+
registered: boolean;
|
|
18
|
+
/** Does the init system say the process is up right now? */
|
|
19
|
+
running: boolean;
|
|
20
|
+
pid?: string;
|
|
21
|
+
/** The Paddock arguments recorded in the installed unit, interpreter stripped. */
|
|
22
|
+
argv: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface ServiceBackend {
|
|
25
|
+
platform: "darwin" | "linux";
|
|
26
|
+
/** Where the plist / unit file goes. */
|
|
27
|
+
unitPath: string;
|
|
28
|
+
/** launchd label or systemd unit name — the string a user would type. */
|
|
29
|
+
label: string;
|
|
30
|
+
install(spec: ServiceSpec): void;
|
|
31
|
+
/** True if something was actually there to remove. */
|
|
32
|
+
uninstall(): boolean;
|
|
33
|
+
status(): ServiceState;
|
|
34
|
+
/** How to read the logs, as printed lines. */
|
|
35
|
+
logsHint(spec: ServiceSpec): string;
|
|
36
|
+
/** The `enable-linger` warning, or undefined where it does not apply. */
|
|
37
|
+
lingerNote(): string | undefined;
|
|
38
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam between "what a Paddock service is" and "how this init system spells
|
|
3
|
+
* it" (#796).
|
|
4
|
+
*
|
|
5
|
+
* Two backends implement this: `launchd.ts` and `systemd.ts`. They differ in
|
|
6
|
+
* more than syntax — launchd wants log file paths and systemd sends everything
|
|
7
|
+
* to journald; systemd needs `loginctl enable-linger` and launchd has no
|
|
8
|
+
* equivalent — so the interface carries those asymmetries explicitly rather
|
|
9
|
+
* than pretending the two are the same shape with different quotes.
|
|
10
|
+
*/
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
export const spawnRunner = (cmd, args) => {
|
|
13
|
+
const r = spawnSync(cmd, args, { encoding: "utf8" });
|
|
14
|
+
return {
|
|
15
|
+
status: r.status,
|
|
16
|
+
stdout: r.stdout ?? "",
|
|
17
|
+
stderr: r.stderr ?? "",
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type CliOptions, type ServiceAction } from "../args.js";
|
|
2
|
+
import { isNpxPath, serviceDir, type ServiceSpec } from "./spec.js";
|
|
3
|
+
import { type Runner } from "./backend.js";
|
|
4
|
+
export interface ServiceContext {
|
|
5
|
+
/** `process.platform`. */
|
|
6
|
+
platform: NodeJS.Platform;
|
|
7
|
+
/** `process.execPath` — the interpreter the unit will name explicitly. */
|
|
8
|
+
nodePath: string;
|
|
9
|
+
/** Absolute realpath of `dist/cli/paddock.js`. */
|
|
10
|
+
scriptPath: string;
|
|
11
|
+
/** The `findUp("package.json")` root, tested for an npx cache path. */
|
|
12
|
+
packageRoot: string;
|
|
13
|
+
homeDir: string;
|
|
14
|
+
/** `process.env.PADDOCK_DATA_DIR`, if the installing shell had one. */
|
|
15
|
+
envDataDir?: string;
|
|
16
|
+
/** `process.env.XDG_CONFIG_HOME`, if set — where the systemd user unit goes. */
|
|
17
|
+
xdgConfigHome?: string;
|
|
18
|
+
pathEnv?: string;
|
|
19
|
+
run?: Runner;
|
|
20
|
+
}
|
|
21
|
+
export declare function runService(action: ServiceAction, opts: CliOptions, ctx: ServiceContext): void;
|
|
22
|
+
/** Exported for the entrypoint's context assembly, and for tests. */
|
|
23
|
+
export { serviceDir, isNpxPath };
|
|
24
|
+
export type { ServiceSpec };
|
|
25
|
+
/** Best-effort home dir; `os.homedir()` throws on a machine with no passwd entry. */
|
|
26
|
+
export declare function safeHomeDir(): string;
|
|
27
|
+
/** Realpath, falling back to the input — a broken symlink is not worth a crash. */
|
|
28
|
+
export declare function realpathOr(p: string): string;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paddock service install | uninstall | status` (#796).
|
|
3
|
+
*
|
|
4
|
+
* Registers Paddock as a **per-user** background service — a launchd
|
|
5
|
+
* LaunchAgent on macOS, a `systemd --user` unit on Linux — so it is up from
|
|
6
|
+
* login rather than for as long as a terminal tab.
|
|
7
|
+
*
|
|
8
|
+
* This file is the dispatch and the human output; the two backends do the
|
|
9
|
+
* writing and `spec.ts` holds every decision that is not init-system-specific.
|
|
10
|
+
*
|
|
11
|
+
* Three things this deliberately does not do:
|
|
12
|
+
*
|
|
13
|
+
* - **It does not set `PADDOCK_DATA_DIR`.** With nothing named, the service and
|
|
14
|
+
* a `paddock` typed into a terminal both land on `~/.paddock`: one instance,
|
|
15
|
+
* two ways to reach it.
|
|
16
|
+
* - **It does not generate an auth token.** A service is up longer than an
|
|
17
|
+
* `npx` run, but not reachable by anything new: a local process that can
|
|
18
|
+
* reach the port could already read the same Claude login as the same user.
|
|
19
|
+
* Duration, not reach. `PADDOCK_AUTH_MODE` is there for anyone who wants one
|
|
20
|
+
* anyway, and the bind-safety guard (#435) still refuses a routable interface
|
|
21
|
+
* with auth off.
|
|
22
|
+
* - **It does not claim to start at boot.** See `launchd.ts`.
|
|
23
|
+
*/
|
|
24
|
+
import fs from "node:fs";
|
|
25
|
+
import os from "node:os";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { CliError } from "../args.js";
|
|
28
|
+
import { buildSpec, defaultDataDir, hostFromArgv, isNpxPath, NPX_REFUSAL, portFromArgv, serviceDir, } from "./spec.js";
|
|
29
|
+
import { spawnRunner } from "./backend.js";
|
|
30
|
+
import { createLaunchdBackend } from "./launchd.js";
|
|
31
|
+
import { createSystemdBackend } from "./systemd.js";
|
|
32
|
+
/**
|
|
33
|
+
* Pick a backend, or explain why there isn't one.
|
|
34
|
+
*
|
|
35
|
+
* Windows is the honest gap. It has no per-user service concept that maps onto
|
|
36
|
+
* either of these without a scheduled task or a service wrapper, and shipping
|
|
37
|
+
* an untested third writer would be worse than saying so.
|
|
38
|
+
*/
|
|
39
|
+
function backendFor(ctx) {
|
|
40
|
+
const run = ctx.run ?? spawnRunner;
|
|
41
|
+
if (ctx.platform === "darwin")
|
|
42
|
+
return createLaunchdBackend(run, ctx.homeDir);
|
|
43
|
+
if (ctx.platform === "linux")
|
|
44
|
+
return createSystemdBackend(run, ctx.homeDir, ctx.xdgConfigHome);
|
|
45
|
+
throw new CliError(`\`paddock service\` supports macOS (launchd) and Linux (systemd --user), not ${ctx.platform}.\n` +
|
|
46
|
+
"Run `paddock` in a terminal, or keep it up with whatever your platform uses.");
|
|
47
|
+
}
|
|
48
|
+
/** The data dir the service will use, and whether it has to be named explicitly. */
|
|
49
|
+
function resolveDataDir(opts, ctx) {
|
|
50
|
+
// Mirrors `paddock.ts`: flag beats env beats default. An env var set in the
|
|
51
|
+
// installing shell is an explicit choice too — but it is one the service
|
|
52
|
+
// would NOT inherit, so it gets written into the unit rather than silently
|
|
53
|
+
// dropped in favour of ~/.paddock.
|
|
54
|
+
const chosen = opts.dataDir ?? ctx.envDataDir;
|
|
55
|
+
if (chosen !== undefined) {
|
|
56
|
+
const resolved = path.resolve(chosen);
|
|
57
|
+
return { dataDir: resolved, explicit: resolved };
|
|
58
|
+
}
|
|
59
|
+
return { dataDir: defaultDataDir(ctx.homeDir), explicit: undefined };
|
|
60
|
+
}
|
|
61
|
+
function specFor(opts, ctx) {
|
|
62
|
+
const { dataDir, explicit } = resolveDataDir(opts, ctx);
|
|
63
|
+
const spec = buildSpec({
|
|
64
|
+
nodePath: ctx.nodePath,
|
|
65
|
+
scriptPath: ctx.scriptPath,
|
|
66
|
+
dataDir,
|
|
67
|
+
...(opts.port !== undefined ? { port: opts.port } : {}),
|
|
68
|
+
...(opts.host !== undefined ? { host: opts.host } : {}),
|
|
69
|
+
...(explicit !== undefined ? { dataDirArg: explicit } : {}),
|
|
70
|
+
...(opts.verbose ? { verbose: true } : {}),
|
|
71
|
+
...(ctx.pathEnv !== undefined ? { pathEnv: ctx.pathEnv } : {}),
|
|
72
|
+
});
|
|
73
|
+
return { spec, dataDir };
|
|
74
|
+
}
|
|
75
|
+
/** The line every surface has to carry, because otherwise it gets filed as a bug. */
|
|
76
|
+
const AT_LOGIN = " Paddock starts when you LOG IN, not when the machine boots — it runs as\n" +
|
|
77
|
+
" you, so it can use the Claude login you already have. After a restart\n" +
|
|
78
|
+
" that nobody logs into, Paddock is not running.";
|
|
79
|
+
function install(opts, ctx, backend) {
|
|
80
|
+
if (isNpxPath(ctx.packageRoot))
|
|
81
|
+
throw new CliError(NPX_REFUSAL);
|
|
82
|
+
const { spec, dataDir } = specFor(opts, ctx);
|
|
83
|
+
backend.install(spec);
|
|
84
|
+
const url = `http://${hostFromArgv(spec.args)}:${portFromArgv(spec.args)}`;
|
|
85
|
+
const kind = backend.platform === "darwin" ? "a launchd LaunchAgent" : "a systemd --user service";
|
|
86
|
+
const linger = backend.lingerNote();
|
|
87
|
+
console.log([
|
|
88
|
+
"",
|
|
89
|
+
` Paddock is installed as ${kind}, and running now.`,
|
|
90
|
+
"",
|
|
91
|
+
` URL: ${url}`,
|
|
92
|
+
` Data: ${dataDir}`,
|
|
93
|
+
` Unit: ${backend.unitPath}`,
|
|
94
|
+
backend.logsHint(spec),
|
|
95
|
+
"",
|
|
96
|
+
AT_LOGIN,
|
|
97
|
+
...(linger !== undefined ? ["", linger] : []),
|
|
98
|
+
"",
|
|
99
|
+
" paddock service status is it up, and where",
|
|
100
|
+
" paddock service uninstall stop it and remove the unit",
|
|
101
|
+
"",
|
|
102
|
+
].join("\n"));
|
|
103
|
+
}
|
|
104
|
+
function uninstall(ctx, backend) {
|
|
105
|
+
const existed = backend.uninstall();
|
|
106
|
+
console.log(existed
|
|
107
|
+
? `\n Paddock's ${backend.platform === "darwin" ? "LaunchAgent" : "user service"} is stopped and removed.\n ${backend.unitPath}\n\n Your data is untouched — it was never in there.\n`
|
|
108
|
+
: `\n No Paddock service was installed (${backend.unitPath}).\n Nothing to do.\n`);
|
|
109
|
+
// Leave the service directory: it holds logs, and the last thing anyone wants
|
|
110
|
+
// from `uninstall` is for the record of why they are uninstalling to vanish.
|
|
111
|
+
void ctx;
|
|
112
|
+
}
|
|
113
|
+
function status(opts, ctx, backend) {
|
|
114
|
+
const state = backend.status();
|
|
115
|
+
if (!state.registered) {
|
|
116
|
+
console.log([
|
|
117
|
+
"",
|
|
118
|
+
" Paddock is not installed as a service.",
|
|
119
|
+
` Looked for: ${backend.unitPath}`,
|
|
120
|
+
"",
|
|
121
|
+
" paddock service install register it and start it now",
|
|
122
|
+
"",
|
|
123
|
+
].join("\n"));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Everything below is read back out of the INSTALLED unit, not recomputed
|
|
127
|
+
// from today's flags — the installed port is the one the URL has to name.
|
|
128
|
+
const url = `http://${hostFromArgv(state.argv)}:${portFromArgv(state.argv)}`;
|
|
129
|
+
const dataFromUnit = state.argv.indexOf("--data-dir");
|
|
130
|
+
const dataDir = dataFromUnit >= 0 && state.argv[dataFromUnit + 1] !== undefined
|
|
131
|
+
? state.argv[dataFromUnit + 1]
|
|
132
|
+
: defaultDataDir(ctx.homeDir);
|
|
133
|
+
const { spec } = specFor(opts, { ...ctx, envDataDir: dataDir });
|
|
134
|
+
const linger = backend.lingerNote();
|
|
135
|
+
console.log([
|
|
136
|
+
"",
|
|
137
|
+
state.running
|
|
138
|
+
? ` Paddock is running${state.pid !== undefined ? ` (pid ${state.pid})` : ""}.`
|
|
139
|
+
: " Paddock is registered as a service, but not running right now.",
|
|
140
|
+
"",
|
|
141
|
+
` URL: ${url}`,
|
|
142
|
+
` Data: ${dataDir}`,
|
|
143
|
+
` Unit: ${backend.unitPath}`,
|
|
144
|
+
` Args: paddock ${state.argv.join(" ")}`,
|
|
145
|
+
backend.logsHint(spec),
|
|
146
|
+
"",
|
|
147
|
+
AT_LOGIN,
|
|
148
|
+
...(linger !== undefined && !state.running ? ["", linger] : []),
|
|
149
|
+
"",
|
|
150
|
+
].join("\n"));
|
|
151
|
+
}
|
|
152
|
+
export function runService(action, opts, ctx) {
|
|
153
|
+
const backend = backendFor(ctx);
|
|
154
|
+
if (action === "install")
|
|
155
|
+
return install(opts, ctx, backend);
|
|
156
|
+
if (action === "uninstall")
|
|
157
|
+
return uninstall(ctx, backend);
|
|
158
|
+
return status(opts, ctx, backend);
|
|
159
|
+
}
|
|
160
|
+
/** Exported for the entrypoint's context assembly, and for tests. */
|
|
161
|
+
export { serviceDir, isNpxPath };
|
|
162
|
+
/** Best-effort home dir; `os.homedir()` throws on a machine with no passwd entry. */
|
|
163
|
+
export function safeHomeDir() {
|
|
164
|
+
try {
|
|
165
|
+
return os.homedir();
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return process.env.HOME ?? "/";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Realpath, falling back to the input — a broken symlink is not worth a crash. */
|
|
172
|
+
export function realpathOr(p) {
|
|
173
|
+
try {
|
|
174
|
+
return fs.realpathSync(p);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return p;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ServiceSpec } from "./spec.js";
|
|
2
|
+
import type { Runner, ServiceBackend } from "./backend.js";
|
|
3
|
+
/** `~/Library/LaunchAgents/net.edspencer.paddock.plist`. */
|
|
4
|
+
export declare function plistPath(homeDir?: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Render the plist.
|
|
7
|
+
*
|
|
8
|
+
* `KeepAlive` is a dict rather than `<true/>` on purpose: `SuccessfulExit:
|
|
9
|
+
* false` means "restart it if it exits nonzero, leave it alone if it exits
|
|
10
|
+
* cleanly". A bare `<true/>` would fight a deliberate `launchctl bootout` and
|
|
11
|
+
* make stopping Paddock a wrestling match.
|
|
12
|
+
*
|
|
13
|
+
* `EnvironmentVariables` carries PATH and nothing else. It carries PATH because
|
|
14
|
+
* launchd hands a process a stub PATH that a version manager's `node`, and the
|
|
15
|
+
* `git` an agent will want, are both missing from. It carries nothing else
|
|
16
|
+
* because a plist is a world-readable file in the user's home, and the one
|
|
17
|
+
* category of value that would be tempting to put here — a credential — is
|
|
18
|
+
* precisely what the agent shape exists to avoid needing.
|
|
19
|
+
*
|
|
20
|
+
* No `PADDOCK_DATA_DIR`: see `buildSpec`. One `~/.paddock` instance, reachable
|
|
21
|
+
* from the service and from a terminal.
|
|
22
|
+
*/
|
|
23
|
+
export declare function renderPlist(spec: ServiceSpec): string;
|
|
24
|
+
/**
|
|
25
|
+
* Pull `ProgramArguments` back out of a plist we wrote.
|
|
26
|
+
*
|
|
27
|
+
* `status` reports the port the service is ACTUALLY installed with, which is
|
|
28
|
+
* only knowable from the installed unit — recomputing it from today's flags
|
|
29
|
+
* would print a confident URL for a server listening somewhere else. Regex
|
|
30
|
+
* rather than a plist parser because the input is a file this module wrote, in
|
|
31
|
+
* a shape this module controls, and adding an XML dependency to read it back
|
|
32
|
+
* would be the larger risk.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parsePlistArgv(xml: string): string[];
|
|
35
|
+
/**
|
|
36
|
+
* Read liveness out of `launchctl print gui/<uid>/<label>`.
|
|
37
|
+
*
|
|
38
|
+
* That command is the honest answer to "is it running": it reports the service
|
|
39
|
+
* as launchd currently understands it, including `state = running` and the pid.
|
|
40
|
+
* The alternative — checking whether the plist file exists — answers "is it
|
|
41
|
+
* registered", which is a different and much weaker question.
|
|
42
|
+
*/
|
|
43
|
+
export declare function parseLaunchctlPrint(out: string): {
|
|
44
|
+
running: boolean;
|
|
45
|
+
pid?: string;
|
|
46
|
+
};
|
|
47
|
+
export declare function createLaunchdBackend(run: Runner, homeDir?: string): ServiceBackend;
|