@edspencer/paddock 0.79.0 → 0.80.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 +2 -2
- package/packages/server/dist/app.js +18 -14
- package/packages/server/dist/cli/args.d.ts +47 -7
- package/packages/server/dist/cli/args.js +112 -18
- 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/paddock.js +84 -37
- package/packages/server/dist/cli/service/backend.d.ts +48 -4
- package/packages/server/dist/cli/service/index.d.ts +56 -2
- package/packages/server/dist/cli/service/index.js +283 -56
- package/packages/server/dist/cli/service/launchd.d.ts +8 -0
- package/packages/server/dist/cli/service/launchd.js +29 -7
- 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/management-mcp-server.d.ts +21 -3
- package/packages/server/dist/management-mcp-server.js +4 -13
- package/packages/server/dist/routes/meta.js +11 -3
- package/packages/server/dist/service-log.d.ts +29 -0
- package/packages/server/dist/service-log.js +104 -0
- package/packages/server/dist/start.js +11 -0
- package/packages/server/dist/version.d.ts +3 -0
- package/packages/server/dist/version.js +32 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{DeleteProjectDialog-DE5KhPD_.js → DeleteProjectDialog-DHczLhwQ.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-BSGSzTEs.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-DCEp5YDh.js → DiscoverView-DtTOpTji.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-D09mz7nR.js → InstanceConfigPage-BRoBh1C1.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-CujBOaSt.js → MarkdownRenderer-DJf8IJuk.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DaOQz7Bx.js → ProjectRedirect-EioguUKW.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-BxwVtKmQ.js → ProjectView-BiKnGfk6.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-BkULnAQF.js → ProjectsGrid-D6kfaIal.js} +1 -1
- package/packages/web/dist/assets/{RootHome-C7PQt4Mc.js → RootHome-DpcyuV6e.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-XlC0TXmU.js → Surfaces-CHM29PhP.js} +1 -1
- package/packages/web/dist/assets/{arc-C1crrky_.js → arc-fiqD05Hu.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR--3BQUIXc.js → architectureDiagram-3BPJPVTR-BuuB2Nkf.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-EisqobqG.js → blockDiagram-GPEHLZMM-DQMBuzfi.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-e6-wiFTa.js → c4Diagram-AAUBKEIU-BKN0UxCH.js} +1 -1
- package/packages/web/dist/assets/channel-B7yh6Su_.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-ToP1Bx9O.js → chunk-2J33WTMH-DW37EupX.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-BD2icQlZ.js → chunk-4BX2VUAB-Dyv7zV_c.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-D803_50z.js → chunk-55IACEB6-DzMR0dOk.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-CiJOKx_U.js → chunk-727SXJPM-BdZ3Sor8.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BYni6r_j.js → chunk-AQP2D5EJ-B4i7_og0.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-D6QbCpWh.js → chunk-FMBD7UC4-jcTr85it.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-CvJSg6SC.js → chunk-ND2GUHAM-fgMsadqB.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-Cz0BtHbV.js → chunk-QZHKN3VN-bJNHYS1W.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DIlhMPgi.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DIlhMPgi.js +2 -0
- package/packages/web/dist/assets/{core-BaJJFoBn.js → core-Bk4KFrmN.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CzAP4gqt.js → cose-bilkent-S5V4N54A-BIqCSlQy.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-CwIEzm8u.js → dagre-BM42HDAG-6nyhtVQz.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-BEvdaFOg.js → diagram-2AECGRRQ-CjizfipR.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-mk5qWzPB.js → diagram-5GNKFQAL-DlgB5vWb.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-C4lD_Kit.js → diagram-KO2AKTUF-CNC9qanV.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-BmduqHdC.js → diagram-LMA3HP47-DSJ-s9lh.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-CzbvsaAK.js → diagram-OG6HWLK6-C9FoHBDg.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-DvZmF8pw.js → documentTitle-Cj_AY97j.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-B3Nm1nfI.js → erDiagram-TEJ5UH35-z6gku4Yt.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DTAf1VHv.js → flowDiagram-I6XJVG4X-Dl5c_FgU.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-BOeIF25v.js → ganttDiagram-6RSMTGT7-CIscJ-Dt.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-DR0ETY9X.js → gitGraphDiagram-PVQCEYII-CvttOXM_.js} +1 -1
- package/packages/web/dist/assets/{index-BqXu07yh.js → index-DN82do6Z.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-iFsN2k7Z.js → infoDiagram-5YYISTIA-dzWYdDJj.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-D0aKZ4O0.js → ishikawaDiagram-YF4QCWOH-8yX9k4bo.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Dzt-NowV.js → journeyDiagram-JHISSGLW-BmitzJDR.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-EurYbfWQ.js → kanban-definition-UN3LZRKU-BZmIkfkW.js} +1 -1
- package/packages/web/dist/assets/{linear-DjReSGMv.js → linear-BBLXE4o7.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-CPXZwBWr.js → mermaid.core-mSKff4yu.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BZKr8aHp.js → mindmap-definition-RKZ34NQL-BWAUa4CP.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-CGIjsXTW.js → pieDiagram-4H26LBE5-VmVdiL4C.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-CMk1ecJ2.js → quadrantDiagram-W4KKPZXB-Dybfv6NU.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-DR2dI24k.js → requirementDiagram-4Y6WPE33-BPnOvB7w.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-BP6UMMTr.js → sankeyDiagram-5OEKKPKP-YXIEmKw7.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-4jrB80y7.js → sequenceDiagram-3UESZ5HK-DintIRUe.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-yAWdzPCD.js → stateDiagram-AJRCARHV-eMsjahWp.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CpAOGzbO.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-MFxU8aFE.js → timeline-definition-PNZ67QCA-as4zRCPn.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-DptEBZ6E.js → vennDiagram-CIIHVFJN-CPzvUkM0.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-Dk-HljGs.js → wardley-L42UT6IY-q6moXj5g.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-DhIz3dBE.js → wardleyDiagram-YWT4CUSO-BsEEb9Gm.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-ytw3T4h8.js → xychartDiagram-2RQKCTM6-DSgZlcZW.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-vaaC-Dji.js +0 -2
- package/packages/web/dist/assets/channel-D4JZjkXm.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-BQVXohi4.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-BQVXohi4.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-D0dmQsgw.js +0 -2
|
@@ -59,6 +59,39 @@ export function unitPath(homeDir, xdgConfigHome) {
|
|
|
59
59
|
: (process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config")));
|
|
60
60
|
return path.join(base, "systemd", "user", SYSTEMD_UNIT);
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Unit-file escaping, ported from Local Review's `server/service/systemd.ts`
|
|
64
|
+
* (`pct`, `cEscape`, `cUnescape`, `execWord`, `execWords`) @ cli-contract spec
|
|
65
|
+
* 0.1; local changes: doc comments only. Wave C4 moves these to
|
|
66
|
+
* `reference/service/` and re-syncs.
|
|
67
|
+
*
|
|
68
|
+
* `%` starts a specifier EVERYWHERE in a unit file, so it is doubled; `$`
|
|
69
|
+
* starts a variable in `ExecStart`, so it is doubled there too.
|
|
70
|
+
*/
|
|
71
|
+
const pct = (s) => s.replace(/%/g, "%%");
|
|
72
|
+
const cEscape = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
73
|
+
const cUnescape = (s) => s.replace(/\\(.)/g, (_, c) => (c === "n" ? "\n" : c === "t" ? "\t" : c));
|
|
74
|
+
/** One `ExecStart` word: specifiers and variables escaped, quoted when it has to be. */
|
|
75
|
+
export function execWord(a) {
|
|
76
|
+
const s = pct(a).replace(/\$/g, "$$$$");
|
|
77
|
+
return s === "" || /[\s"'\\;]/.test(s) ? `"${cEscape(s)}"` : s;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The other way: the words of an `ExecStart` line.
|
|
81
|
+
*
|
|
82
|
+
* Reads both shapes on purpose. A unit written before #796's quoting is a bare
|
|
83
|
+
* whitespace-separated line, and its owner is exactly the person whose `status`
|
|
84
|
+
* has to keep working — so unquoted words pass through unchanged, and quoted
|
|
85
|
+
* ones are unescaped.
|
|
86
|
+
*/
|
|
87
|
+
export function execWords(line) {
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const m of line.matchAll(/"((?:[^"\\]|\\.)*)"|(\S+)/g)) {
|
|
90
|
+
const w = m[1] !== undefined ? cUnescape(m[1]) : m[2];
|
|
91
|
+
out.push(w.replace(/\$\$/g, "$").replace(/%%/g, "%"));
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
62
95
|
/**
|
|
63
96
|
* Render the unit.
|
|
64
97
|
*
|
|
@@ -78,21 +111,44 @@ export function unitPath(homeDir, xdgConfigHome) {
|
|
|
78
111
|
* "start at login" for a user unit; `multi-user.target` is the system-unit
|
|
79
112
|
* spelling and does nothing here.
|
|
80
113
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
114
|
+
* ## Logs go to a file, not the journal (SPEC §7)
|
|
115
|
+
*
|
|
116
|
+
* `StandardOutput=append:` / `StandardError=append:` name one file, the same
|
|
117
|
+
* one launchd's two keys name. The journal was the obvious choice and the wrong
|
|
118
|
+
* one for three reasons: `journalctl --user` shows nothing at all on a distro
|
|
119
|
+
* whose journal is volatile (the output is gone after a reboot, which is
|
|
120
|
+
* exactly when someone looks); it makes "where are the logs" have a different
|
|
121
|
+
* answer on each platform, so `paddock service logs` would be two
|
|
122
|
+
* implementations; and it rules out Windows, which the contract requires
|
|
123
|
+
* nothing rule out.
|
|
124
|
+
*
|
|
125
|
+
* The directory has to exist first — systemd creates the file, not its parent —
|
|
126
|
+
* which `install` below does.
|
|
127
|
+
*
|
|
128
|
+
* ## Every ExecStart word is quoted
|
|
129
|
+
*
|
|
130
|
+
* `%` starts a specifier anywhere in a unit file and `$` starts a variable in
|
|
131
|
+
* `ExecStart`, so both are escaped; a word containing whitespace, a quote or a
|
|
132
|
+
* backslash is quoted. Paths with a space in them are the common case
|
|
133
|
+
* (`/Users/Ed Spencer/.paddock`), and the old unqualified `join(" ")` wrote a
|
|
134
|
+
* unit systemd then split into the wrong arguments — a service that fails at
|
|
135
|
+
* login having printed nothing.
|
|
84
136
|
*/
|
|
85
137
|
export function renderUnit(spec) {
|
|
86
|
-
const exec = [spec.nodePath, spec.scriptPath, ...spec.args].join(" ");
|
|
87
|
-
return
|
|
138
|
+
const exec = [spec.nodePath, spec.scriptPath, ...spec.args].map(execWord).join(" ");
|
|
139
|
+
return `# Generated by Paddock — \`paddock service install\` rewrites this file.
|
|
140
|
+
# Node: ${spec.nodePath}
|
|
141
|
+
[Unit]
|
|
88
142
|
Description=Paddock — persistent, resumable Claude Code sessions, by project
|
|
89
143
|
Documentation=https://paddock.edspencer.net
|
|
90
144
|
|
|
91
145
|
[Service]
|
|
92
146
|
Type=simple
|
|
93
147
|
ExecStart=${exec}
|
|
94
|
-
WorkingDirectory=${spec.workingDirectory}
|
|
95
|
-
Environment=PATH=${spec.pathEnv}
|
|
148
|
+
WorkingDirectory=${pct(spec.workingDirectory)}
|
|
149
|
+
Environment="PATH=${cEscape(pct(spec.pathEnv))}"
|
|
150
|
+
StandardOutput=append:${pct(spec.logPath)}
|
|
151
|
+
StandardError=append:${pct(spec.logPath)}
|
|
96
152
|
Restart=always
|
|
97
153
|
RestartSec=10
|
|
98
154
|
|
|
@@ -100,6 +156,11 @@ RestartSec=10
|
|
|
100
156
|
WantedBy=default.target
|
|
101
157
|
`;
|
|
102
158
|
}
|
|
159
|
+
/** The log file an installed unit appends to, or undefined if it names none. */
|
|
160
|
+
export function parseUnitLogPath(text) {
|
|
161
|
+
const m = /^StandardOutput=append:(.*)$/m.exec(text);
|
|
162
|
+
return m === null ? undefined : m[1].trim().replace(/%%/g, "%");
|
|
163
|
+
}
|
|
103
164
|
/**
|
|
104
165
|
* Does an installed unit carry `Restart=always` (#872)?
|
|
105
166
|
*
|
|
@@ -117,15 +178,40 @@ export function parseUnitArgv(text) {
|
|
|
117
178
|
const m = /^ExecStart=(.*)$/m.exec(text);
|
|
118
179
|
if (m === null)
|
|
119
180
|
return [];
|
|
120
|
-
return m[1].trim()
|
|
181
|
+
return execWords(m[1].trim());
|
|
121
182
|
}
|
|
122
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* What a `systemctl` failure means, in the manager's own words where it has any.
|
|
185
|
+
*
|
|
186
|
+
* The `status === null` branch is the one QA found: `spawnSync` reports a
|
|
187
|
+
* missing binary that way, with no output at all, so the message was
|
|
188
|
+
* `systemctl --user enable --now failed (exit null).` and then a blank line —
|
|
189
|
+
* which tells someone on a container with no systemd nothing whatsoever. That
|
|
190
|
+
* is also the single likeliest way this command fails on a machine it was never
|
|
191
|
+
* going to work on.
|
|
192
|
+
*/
|
|
193
|
+
export function systemctlFailure(args, r) {
|
|
194
|
+
if (r.status === null || r.status === 127) {
|
|
195
|
+
return ("systemctl is not available, so Paddock cannot install a service here.\n" +
|
|
196
|
+
"It needs systemd's per-user instance (`systemctl --user`) — a container " +
|
|
197
|
+
"or a non-systemd distribution has none.\n" +
|
|
198
|
+
"Run `paddock serve` in a terminal, or keep it up with whatever this " +
|
|
199
|
+
"machine uses.");
|
|
200
|
+
}
|
|
201
|
+
if (/Failed to connect to (user scope )?bus|DBUS_SESSION_BUS_ADDRESS|XDG_RUNTIME_DIR/.test(r.stderr)) {
|
|
202
|
+
return (`cannot reach systemd's per-user instance (${r.stderr.trim()}).\n` +
|
|
203
|
+
"Run this from a login session, and check that $XDG_RUNTIME_DIR is set " +
|
|
204
|
+
"(usually /run/user/$(id -u)).");
|
|
205
|
+
}
|
|
206
|
+
return (`systemctl ${args.join(" ")} failed (exit ${String(r.status)}).\n` +
|
|
207
|
+
`${r.stderr || r.stdout}`.trim());
|
|
208
|
+
}
|
|
209
|
+
/** Run a `systemctl` and turn a non-zero exit into something actionable. */
|
|
123
210
|
function systemctlOrThrow(run, args) {
|
|
124
211
|
const r = run("systemctl", args);
|
|
125
212
|
if (r.status === 0)
|
|
126
213
|
return;
|
|
127
|
-
throw new Error(
|
|
128
|
-
`${r.stderr || r.stdout}`.trim());
|
|
214
|
+
throw new Error(systemctlFailure(args, r));
|
|
129
215
|
}
|
|
130
216
|
export function createSystemdBackend(run, homeDir, xdgConfigHome) {
|
|
131
217
|
// Both forwarded, neither defaulted here: `unitPath` owns the precedence, and
|
|
@@ -140,15 +226,19 @@ export function createSystemdBackend(run, homeDir, xdgConfigHome) {
|
|
|
140
226
|
install(spec) {
|
|
141
227
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
142
228
|
fs.mkdirSync(spec.workingDirectory, { recursive: true });
|
|
229
|
+
// systemd creates the log FILE for `append:` but not its directory, and a
|
|
230
|
+
// missing directory fails the unit at start with the server never having
|
|
231
|
+
// run — no output anywhere, because the thing that would have written it
|
|
232
|
+
// is what failed.
|
|
233
|
+
fs.mkdirSync(path.dirname(spec.logPath), { recursive: true });
|
|
143
234
|
fs.writeFileSync(file, renderUnit(spec), "utf8");
|
|
144
235
|
// Without this the manager keeps serving the previous unit from cache and
|
|
145
236
|
// an "upgrade" silently changes nothing.
|
|
146
237
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
238
|
+
const args = ["--user", "enable", "--now", SYSTEMD_UNIT];
|
|
239
|
+
const enabled = run("systemctl", args);
|
|
240
|
+
if (enabled.status !== 0)
|
|
241
|
+
throw new Error(systemctlFailure(args, enabled));
|
|
152
242
|
},
|
|
153
243
|
uninstall() {
|
|
154
244
|
const existed = fs.existsSync(file);
|
|
@@ -188,21 +278,36 @@ export function createSystemdBackend(run, homeDir, xdgConfigHome) {
|
|
|
188
278
|
const active = run("systemctl", ["--user", "is-active", SYSTEMD_UNIT]);
|
|
189
279
|
const pid = run("systemctl", ["--user", "show", "-p", "MainPID", "--value", SYSTEMD_UNIT]);
|
|
190
280
|
const mainPid = pid.stdout.trim();
|
|
281
|
+
const logPath = parseUnitLogPath(text);
|
|
191
282
|
return {
|
|
192
283
|
registered: true,
|
|
193
284
|
running: active.stdout.trim() === "active",
|
|
194
285
|
...(mainPid !== "" && mainPid !== "0" ? { pid: mainPid } : {}),
|
|
195
286
|
argv: argv.slice(2),
|
|
287
|
+
...(argv[0] !== undefined ? { nodePath: argv[0] } : {}),
|
|
288
|
+
...(argv[1] !== undefined ? { cliPath: argv[1] } : {}),
|
|
289
|
+
...(logPath !== undefined ? { logPath } : {}),
|
|
196
290
|
keepsAlive: unitKeepsAlive(text),
|
|
197
291
|
};
|
|
198
292
|
},
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
293
|
+
/**
|
|
294
|
+
* `loginctl show-user <user> -p Linger` (SPEC §7).
|
|
295
|
+
*
|
|
296
|
+
* Parsed rather than trusted to exit codes: `loginctl` is absent in plenty
|
|
297
|
+
* of containers and exits non-zero for a user logind has no record of, and
|
|
298
|
+
* neither of those is "linger is off". An unparseable answer is reported as
|
|
299
|
+
* unknown, which prints nothing — the alternative is handing a `sudo`
|
|
300
|
+
* command to someone whose machine has no `loginctl` to run it with.
|
|
301
|
+
*/
|
|
302
|
+
linger() {
|
|
203
303
|
const user = os.userInfo().username;
|
|
204
|
-
|
|
205
|
-
|
|
304
|
+
const r = run("loginctl", ["show-user", user, "-p", "Linger"]);
|
|
305
|
+
const m = /Linger=(yes|no)/i.exec(`${r.stdout}${r.stderr}`);
|
|
306
|
+
return {
|
|
307
|
+
applies: true,
|
|
308
|
+
user,
|
|
309
|
+
...(m !== null ? { enabled: m[1].toLowerCase() === "yes" } : {}),
|
|
310
|
+
};
|
|
206
311
|
},
|
|
207
312
|
};
|
|
208
313
|
}
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP server for external Management API callers (issue #312 M2).
|
|
3
|
+
*
|
|
4
|
+
* Adapts the SAME tool definitions the in-process keeper gets — an
|
|
5
|
+
* {@link InjectedMcpServerDef} produced by `selfMcpServerDefForOps` — onto the
|
|
6
|
+
* official MCP SDK, so an external client sees exactly the toolset a keeper
|
|
7
|
+
* sees, minus whatever its scope hides. Nothing is redefined here: if a tool is
|
|
8
|
+
* added to the self-MCP, it appears over `/mcp` with no further work, which is
|
|
9
|
+
* the "upgrade the server, every client's toolset upgrades" property the epic
|
|
10
|
+
* calls out.
|
|
11
|
+
*
|
|
12
|
+
* ── Why the LOW-LEVEL `Server`, not `McpServer` ────────────────────────────
|
|
13
|
+
* The high-level `McpServer.registerTool` wants Zod schemas and converts them to
|
|
14
|
+
* JSON Schema for the wire. Our tools already carry hand-written JSON Schema
|
|
15
|
+
* (shared with herdctl's injected-MCP bridge), so going through Zod would mean
|
|
16
|
+
* translating JSON Schema → Zod → JSON Schema and hoping the round trip is
|
|
17
|
+
* faithful. Implementing `tools/list` + `tools/call` directly passes our schemas
|
|
18
|
+
* through verbatim and keeps ONE source of truth for the tool surface.
|
|
19
|
+
*/
|
|
1
20
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
21
|
import type { InjectedMcpServerDef } from "@herdctl/core";
|
|
3
22
|
import { type ManagementPrincipal } from "./management-policy.js";
|
|
@@ -5,9 +24,8 @@ import { type ManagementPrincipal } from "./management-policy.js";
|
|
|
5
24
|
export declare const MANAGEMENT_MCP_SERVER_NAME = "paddock";
|
|
6
25
|
/**
|
|
7
26
|
* Package version, advertised to the client so it can report which Paddock it
|
|
8
|
-
* is talking to.
|
|
9
|
-
*
|
|
10
|
-
* so it tracks releases without a build step.
|
|
27
|
+
* is talking to. One resolution, shared with the OpenAPI document and
|
|
28
|
+
* `/api/health` (version.ts), so the three cannot disagree.
|
|
11
29
|
*/
|
|
12
30
|
export declare const MANAGEMENT_MCP_SERVER_VERSION: string;
|
|
13
31
|
/**
|
|
@@ -17,27 +17,18 @@
|
|
|
17
17
|
* faithful. Implementing `tools/list` + `tools/call` directly passes our schemas
|
|
18
18
|
* through verbatim and keeps ONE source of truth for the tool surface.
|
|
19
19
|
*/
|
|
20
|
-
import { createRequire } from "node:module";
|
|
21
20
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
21
|
import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
23
22
|
import { ManagementDeniedError } from "./management-policy.js";
|
|
23
|
+
import { PADDOCK_VERSION } from "./version.js";
|
|
24
24
|
/** Server identity advertised in the MCP `initialize` handshake. */
|
|
25
25
|
export const MANAGEMENT_MCP_SERVER_NAME = "paddock";
|
|
26
26
|
/**
|
|
27
27
|
* Package version, advertised to the client so it can report which Paddock it
|
|
28
|
-
* is talking to.
|
|
29
|
-
*
|
|
30
|
-
* so it tracks releases without a build step.
|
|
28
|
+
* is talking to. One resolution, shared with the OpenAPI document and
|
|
29
|
+
* `/api/health` (version.ts), so the three cannot disagree.
|
|
31
30
|
*/
|
|
32
|
-
export const MANAGEMENT_MCP_SERVER_VERSION =
|
|
33
|
-
try {
|
|
34
|
-
const require = createRequire(import.meta.url);
|
|
35
|
-
return require("../package.json").version ?? "0.0.0";
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return "0.0.0";
|
|
39
|
-
}
|
|
40
|
-
})();
|
|
31
|
+
export const MANAGEMENT_MCP_SERVER_VERSION = PADDOCK_VERSION;
|
|
41
32
|
/**
|
|
42
33
|
* Build an MCP server exposing `def`'s tools to `principal`.
|
|
43
34
|
*
|
|
@@ -5,6 +5,7 @@ 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";
|
|
8
9
|
export function registerMetaRoutes(app, ctx) {
|
|
9
10
|
const { herdctl, transcriber, attachments, cfg } = ctx;
|
|
10
11
|
// --- voice dictation (#voice): capability probe + transcription -------
|
|
@@ -106,20 +107,27 @@ export function registerMetaRoutes(app, ctx) {
|
|
|
106
107
|
},
|
|
107
108
|
},
|
|
108
109
|
}, async (req) => req.user);
|
|
110
|
+
// The CLI contract's one required server endpoint (SPEC §8): `{ ok, version }`,
|
|
111
|
+
// no auth. The version is load-bearing rather than decorative — it is how
|
|
112
|
+
// `paddock service status` can say that the version RUNNING differs from the
|
|
113
|
+
// one installed (an upgrade that never restarted), and how wave C2's
|
|
114
|
+
// `paddock update` will confirm the service came back as the version it just
|
|
115
|
+
// installed rather than merely coming back. `ALWAYS_PUBLIC_PATHS` in auth.ts
|
|
116
|
+
// keeps it answerable whatever `PADDOCK_AUTH_MODE` is set to.
|
|
109
117
|
app.get("/api/health", {
|
|
110
118
|
schema: {
|
|
111
119
|
tags: ["System"],
|
|
112
120
|
summary: "Health check",
|
|
113
|
-
description: "Liveness probe. Returns a JSON object `{ ok: true }`.",
|
|
121
|
+
description: "Liveness probe. Returns a JSON object `{ ok: true, version: \"<semver>\" }`. Never requires authentication.",
|
|
114
122
|
response: {
|
|
115
123
|
200: {
|
|
116
|
-
description: "Service is up.",
|
|
124
|
+
description: "Service is up, and the version it is running.",
|
|
117
125
|
type: "object",
|
|
118
126
|
additionalProperties: true,
|
|
119
127
|
},
|
|
120
128
|
},
|
|
121
129
|
},
|
|
122
|
-
}, async () => ({ ok: true }));
|
|
130
|
+
}, async () => ({ ok: true, version: PADDOCK_VERSION }));
|
|
123
131
|
// --- instance-wide settings (issue #385) --------------------------------
|
|
124
132
|
// A top-level admin screen over the frozen instance config. GET reports every
|
|
125
133
|
// surfaced field (value / default / editable / sensitive / env-shadow); PUT
|
|
@@ -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
|
+
}
|
|
@@ -8,9 +8,20 @@
|
|
|
8
8
|
* depends on that to apply its own defaults (e.g. a data dir under $HOME).
|
|
9
9
|
*/
|
|
10
10
|
import { buildApp } from "./app.js";
|
|
11
|
+
import { startServiceLogRotation } from "./service-log.js";
|
|
11
12
|
export async function start() {
|
|
12
13
|
const { app, cfg, close } = await buildApp();
|
|
14
|
+
// Nothing else rotates the service log: the supervisor appends to it through
|
|
15
|
+
// its own descriptor, a `systemd --user` unit writing to a file gets no
|
|
16
|
+
// journal rotation, and macOS has nothing that would (SPEC §7). A no-op on a
|
|
17
|
+
// foreground run, where that file does not exist — see service-log.ts.
|
|
18
|
+
const stopRotation = startServiceLogRotation(cfg.dataDir, {
|
|
19
|
+
warn: (message) => {
|
|
20
|
+
app.log.warn(message);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
13
23
|
const shutdown = async () => {
|
|
24
|
+
stopRotation();
|
|
14
25
|
await close();
|
|
15
26
|
process.exit(0);
|
|
16
27
|
};
|
|
@@ -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";
|
package/packages/web/dist/assets/{DeleteProjectDialog-DE5KhPD_.js → DeleteProjectDialog-DHczLhwQ.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-DN82do6Z.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
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{h as oe,d as ie,a1 as de,r as u,a as A,j as s,W as Q,A as X,a5 as Y,J as $,a2 as ce,U as le,i as he,a6 as ue,a7 as J,K as T,c as U,a8 as pe,v as xe,Z as V}from"./index-BqXu07yh.js";import{E as R,C as me}from"./Surfaces-XlC0TXmU.js";function ge(e){const t={};for(const n of e)t[n.path]={checked:!0,expanded:!1,loading:!1,error:null};return t}function fe(e){if(!e.sessions||!e.selected)return e.checked?"on":"off";const t=e.sessions.sessions.length,n=e.selected.size;return t===0||n===0?"off":n===t?"on":"mixed"}function C(e,t){return e.sessions&&e.selected?e.selected.size:e.checked?t.sessionCount:0}function je(e,t){return t.reduce((n,r)=>n+C(e[r.path]??O(),r),0)}function be(e,t){return t.filter(n=>C(e[n.path]??O(),n)>0)}function we(e,t,n){const r=e[t];if(!r)return e;const a={...r,checked:n};return r.sessions&&(a.selected=n?new Set(r.sessions.sessions.map(c=>c.sessionId)):new Set),{...e,[t]:a}}function ye(e,t,n){const r=e[t];if(!r||!r.sessions)return e;const a=new Set(r.selected??[]);return a.delete(n)||a.add(n),{...e,[t]:{...r,selected:a,checked:a.size>0}}}function ve(e,t,n){const r=e[t];if(!r)return e;const a=r.checked?new Set(n.sessions.map(c=>c.sessionId)):new Set;return{...e,[t]:{...r,sessions:n,selected:a,loading:!1,error:null}}}function D(e,t,n){const r=e[t];return r?{...e,[t]:{...r,...n}}:e}function Ne(e,t){const n=e??O();return C(n,t)===0?null:n.sessions&&n.selected?{sessionIds:[...n.selected]}:{}}function O(){return{checked:!1,expanded:!1,loading:!1,error:null}}function ke(e){return e.error!==void 0?e.stage==="create"?"danger":"warn":e.adopted>0?"success":"warn"}async function Se(e,t,n){let r;try{r=await e.createProject({name:t.name,slug:t.suggestedSlug,path:t.path,managed:!1})}catch(a){return{stage:"create",adopted:0,skipped:[],error:Z(a)}}try{const a=await e.adoptChats(r.slug,{sourceCwd:r.workingDir,...n.sessionIds?{sessionIds:n.sessionIds}:{}});return{project:r,adopted:a.adopted.length,skipped:a.skipped}}catch(a){return{stage:"import",project:r,adopted:0,skipped:[],error:Z(a)}}}async function $e(e,t,n){const r=[];for(const a of t){n(a.candidate.path,"running");const c=await Se(e,a.candidate,a.plan);r.push(c),n(a.candidate.path,"done",c)}return r}const Ce={sidechain:"sub-agent transcript","already-adopted":"already adopted","destination-exists":"a chat with that id already exists here","attributed-to-run":"belongs to a Paddock run",unreadable:"transcript unreadable","placement-failed":"copy failed","record-failed":"could not be recorded"};function W(e){if(e.length===0)return"";const t=[...new Set(e.map(n=>Ce[n.reason]??n.reason))];return`${e.length} skipped (${t.join(", ")})`}function Ie(e,t){var l;if(e.stage==="create")return`Couldn't create the project: ${e.error}`;const n=((l=e.project)==null?void 0:l.slug)??t.suggestedSlug;if(e.stage==="import")return`Project “${n}” was created, but importing its chats failed: ${e.error}. It is there and empty — you can import them from the project itself.`;if(e.adopted===0){const m=W(e.skipped);return t.recordedPath!==void 0?`Project “${n}” was created but no chats came with it. Its transcripts record ${t.recordedPath}, a different spelling of ${t.path}, and an import matches on the exact path.`:m?`Project “${n}” was created but no chats came with it — ${m}.`:`Project “${n}” was created but no chats came with it.`}const r=e.adopted,a=W(e.skipped),c=`Adopted ${r} chat${r===1?"":"s"} into “${n}”`;return a?`${c} — ${a}.`:`${c}.`}function Z(e){return e instanceof Error?e.message:String(e)}function Ge({firstRun:e=!1,embedded:t=!1,onLeave:n,onStartChat:r}){const a=oe(),{refresh:c}=ie(),l=de(),[m,d]=u.useState(!1),[j,I]=u.useState(!1),[h,x]=u.useState(null),[y,z]=u.useState(null),[F,P]=u.useState(!0),[f,v]=u.useState({}),[k,G]=u.useState(!1),[L,H]=u.useState({}),[E,B]=u.useState({}),[S,ee]=u.useState(!1);u.useEffect(()=>{let o=!0;return P(!0),z(null),A.discover({includeNonGit:m,includeOutsideHome:j}).then(i=>{o&&(x(i),v(ge(i.candidates)),P(!1))}).catch(i=>{o&&(z(i instanceof Error?i.message:"Discovery failed"),P(!1))}),()=>{o=!1}},[m,j]);const w=(h==null?void 0:h.candidates)??[],M=u.useMemo(()=>je(f,w),[f,w]),b=u.useMemo(()=>be(f,w),[f,w]),se=u.useCallback(o=>{v(p=>{const g=p[o.path];if(!g)return p;const N=D(p,o.path,{expanded:!g.expanded});return g.expanded||g.sessions||g.loading?N:D(N,o.path,{loading:!0,error:null})});const i=f[o.path];!i||i.expanded||i.sessions||i.loading||A.discoverSessions(o.path).then(p=>v(g=>ve(g,o.path,p))).catch(p=>v(g=>D(g,o.path,{loading:!1,error:p instanceof Error?p.message:"Could not read this directory's sessions"})))},[f]),te=u.useCallback(async()=>{if(k)return;G(!0),H(Object.fromEntries(b.map(i=>[i.path,"pending"]))),B({});const o=b.map(i=>({candidate:i,plan:Ne(f[i.path],i)??{}}));await $e(A,o,(i,p,g)=>{H(N=>({...N,[i]:p})),g&&B(N=>({...N,[i]:g}))}),G(!1),ee(!0),await c()},[b,f,k,c]),ne=u.useCallback(async()=>{await c(),n==null||n(),a("/")},[a,c,n]),ae=Object.values(E).reduce((o,i)=>o+i.adopted,0),re=Object.values(E).filter(o=>o.project).length,K=s.jsxs(s.Fragment,{children:[s.jsx("p",{className:"mb-5 text-sm text-fg-muted",children:e?"If you have used Claude Code in a terminal on this machine, Paddock can adopt that history: each directory becomes a project with its conversations already in it.":"Directories on this machine with existing Claude Code history. Adopting one links it as a project and brings its conversations in; your own history is never moved or deleted."}),S&&s.jsx(Te,{imported:ae,projects:re,onGetStarted:ne}),y!==null&&s.jsx(Q,{tone:"danger",icon:s.jsx(X,{width:16,height:16}),children:y}),F&&s.jsx(De,{}),!F&&y===null&&h!==null&&s.jsxs(s.Fragment,{children:[h.scanned===0?s.jsx(Ae,{claudeHome:h.claudeHome,onStartChat:r}):w.length===0?s.jsx(R,{variant:"panel",icon:s.jsx(Y,{width:22,height:22}),title:"Nothing to adopt",body:"Paddock found Claude Code history on this machine, but none of it is available to adopt right now. The tally below says what was skipped, and the two toggles relax the rules that are safe to relax."}):s.jsx("ul",{className:"space-y-2",children:w.map(o=>s.jsx(Pe,{candidate:o,row:f[o.path],status:L[o.path],outcome:E[o.path],locked:k||S,onToggleRow:i=>v(p=>we(p,o.path,i)),onToggleSession:i=>v(p=>ye(p,o.path,i)),onToggleExpanded:()=>se(o)},o.path))}),!S&&h.scanned>0&&s.jsx(Ee,{result:h,includeNonGit:m,includeOutsideHome:j,disabled:k,onIncludeNonGit:d,onIncludeOutsideHome:I}),w.length>0&&!S&&s.jsxs("div",{className:"mt-5 flex items-center justify-between gap-3 border-t border-edge pt-4",children:[s.jsxs("span",{className:"text-sm text-fg-muted",children:[M," conversation",M===1?"":"s"," from ",b.length," director",b.length===1?"y":"ies"]}),s.jsxs($,{variant:"primary",onClick:()=>void te(),disabled:b.length===0,loading:k,loadingLabel:"Adopting…",children:["Adopt ",b.length," project",b.length===1?"":"s"]})]})]})]});return t?K:s.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[s.jsxs("header",{className:"pt-safe flex items-center gap-2 border-b border-edge px-3 pb-2.5 sm:px-6 lg:py-4",children:[s.jsx("button",{type:"button",onClick:()=>l==null?void 0:l.openNav(),"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:s.jsx(ce,{width:20,height:20})}),s.jsx(le,{width:18,height:18,className:"shrink-0 text-fg-subtle"}),s.jsx("h1",{className:"text-md font-semibold tracking-tight",children:"Discover"})]}),s.jsx("div",{className:"flex-1 overflow-y-auto overscroll-contain",children:s.jsx("div",{className:"mx-auto max-w-3xl px-6 py-6",children:K})})]})}function Pe({candidate:e,row:t,status:n,outcome:r,locked:a,onToggleRow:c,onToggleSession:l,onToggleExpanded:m}){const d=t??{checked:!1,expanded:!1,loading:!1,error:null},j=fe(d),I=C(d,e),h=r?ke(r):null;return s.jsx("li",{children:s.jsxs(me,{flush:!0,className:U("overflow-hidden",h==="success"&&"border-success-edge bg-success-soft",h==="warn"&&"border-warn-edge bg-warn-soft",h==="danger"&&"border-danger-edge bg-danger-soft",n==="running"&&"border-accent-edge"),"data-testid":`discover-row-${e.suggestedSlug}`,"data-tone":h??void 0,children:[s.jsxs("div",{className:"flex items-start gap-3 px-3 py-2.5",children:[s.jsx(J,{className:"mt-1",checked:j==="on",indeterminate:j==="mixed",disabled:a,"aria-label":`Adopt ${e.path}`,onChange:x=>c(x.target.checked)}),s.jsxs("div",{className:"min-w-0 flex-1",children:[s.jsx("code",{className:"block break-all text-sm text-fg",children:e.path}),s.jsxs("div",{className:"mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-fg-muted",children:[s.jsxs("span",{className:"tabular",children:[e.sessionCount," conversation",e.sessionCount===1?"":"s"]}),e.filteredCount>0&&s.jsxs("span",{className:"tabular",title:"Empty, slash-command-only or sweeper transcripts, withheld as noise",children:["· ",e.filteredCount," filtered out"]}),e.lastSessionAt!==void 0&&s.jsxs("span",{className:"tabular",children:["· last ",q(e.lastSessionAt)]}),e.gitRemote!==void 0&&s.jsx(T,{tone:"lineage",size:"sm",children:e.gitRemote}),!e.hasGit&&s.jsx(T,{tone:"neutral",size:"sm",children:"no git"}),!e.insideHome&&s.jsx(T,{tone:"neutral",size:"sm",children:"outside home"})]}),e.recordedPath!==void 0&&s.jsx(Q,{tone:"warn",className:"mt-2",icon:s.jsx(X,{width:14,height:14}),children:s.jsxs("span",{className:"text-xs",children:["These transcripts record"," ",s.jsx("code",{className:"break-all",children:e.recordedPath})," — a different spelling of this path. The import matches on the exact path, so it may bring nothing in."]})}),r&&s.jsx("p",{className:U("mt-2 text-xs",h==="success"&&"text-success",h==="warn"&&"text-warn",h==="danger"&&"text-danger"),children:Ie(r,e)}),n==="running"&&!r&&s.jsx("p",{className:"mt-2 text-xs text-fg-muted",children:"Adopting…"})]}),s.jsxs("span",{className:"flex shrink-0 items-center gap-1",children:[s.jsx("span",{className:"tabular text-xs text-fg-subtle",children:I}),s.jsx($,{size:"icon-sm",variant:"subtle","aria-label":d.expanded?"Hide conversations":"Show conversations","aria-expanded":d.expanded,onClick:m,children:d.expanded?s.jsx(pe,{width:14,height:14}):s.jsx(xe,{width:14,height:14})})]})]}),d.expanded&&s.jsxs("div",{className:"border-t border-edge px-3 py-2",children:[d.loading&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Reading transcripts…"}),d.error!==null&&s.jsx("p",{className:"py-2 text-xs text-danger",children:d.error}),d.sessions&&d.sessions.sessions.length===0&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Nothing to adopt here any more — it may have been adopted since the scan."}),d.sessions&&d.sessions.sessions.length>0&&s.jsx("ul",{className:"space-y-1",children:d.sessions.sessions.map(x=>{var y;return s.jsx("li",{children:s.jsxs("label",{className:"flex cursor-pointer items-start gap-2 rounded-lg px-2 py-1.5 transition-colors hover:bg-surface-hover",children:[s.jsx(J,{checked:((y=d.selected)==null?void 0:y.has(x.sessionId))??!1,disabled:a,"aria-label":_(x),onChange:()=>l(x.sessionId)}),s.jsxs("span",{className:"min-w-0 flex-1",children:[s.jsx("span",{className:"block truncate text-xs text-fg",children:_(x)}),s.jsxs("span",{className:"tabular mt-0.5 block text-3xs text-fg-muted",children:[q(x.mtime)," · ",Oe(x.sizeBytes)]})]})]})},x.sessionId)})}),d.sessions&&d.sessions.filtered.length>0&&s.jsxs("p",{className:"mt-2 px-2 text-3xs text-fg-subtle",children:[d.sessions.filtered.length," more withheld as noise (",[...new Set(d.sessions.filtered.map(x=>x.reason))].join(", "),")."]})]})]})})}function Ee({result:e,includeNonGit:t,includeOutsideHome:n,disabled:r,onIncludeNonGit:a,onIncludeOutsideHome:c}){const l=e.excluded["no-git"]??0,m=e.excluded["outside-home"]??0,d=l>0||t,j=m>0||n;return e.scanned===0?null:s.jsxs("div",{className:"mt-5 space-y-2 border-t border-edge pt-4",children:[s.jsx("p",{className:"text-xs text-fg-muted",children:Re(e)}),d&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(V,{checked:t,disabled:r,onChange:a,label:"Also offer directories without a git repository"}),s.jsxs("span",{children:["Also offer directories without a git repository",l>0&&` (${l} hidden)`]})]}),j&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(V,{checked:n,disabled:r,onChange:c,label:"Also offer directories outside your home directory"}),s.jsxs("span",{children:["Also offer directories outside ",s.jsx("code",{children:e.homeDir}),m>0&&` (${m} hidden)`]})]})]})}function Ae({claudeHome:e,onStartChat:t}){return s.jsx(R,{variant:"panel",icon:s.jsx(ue,{width:22,height:22}),title:"Nothing to adopt yet",body:s.jsxs(s.Fragment,{children:["No Claude Code history has been recorded under"," ",s.jsx("code",{className:"break-all",children:e}),". That is normal on a new install, and on a container whose Claude home is its own rather than yours — nothing is wrong. Start a chat here and Paddock writes its own history; or bind-mount an existing one there and re-open this page, or import it headlessly with ",s.jsx("code",{children:"npm run import-chats"}),"."]}),action:t&&s.jsx($,{variant:"primary",icon:s.jsx(he,{width:14,height:14}),onClick:t,children:"Start a chat"})})}function Te({imported:e,projects:t,onGetStarted:n}){return s.jsx("div",{className:"mb-6",children:s.jsx(R,{variant:"panel",icon:s.jsx(Y,{width:22,height:22}),title:t===0?"Nothing was adopted":`${t} project${t===1?"":"s"}, ${e} conversation${e===1?"":"s"}`,body:t===0?"Every row failed. Each one says why below.":"They are in the sidebar now. Anything that did not land says why below.",action:s.jsx($,{variant:"primary",onClick:()=>void n(),children:"Get started"})})})}function De(){return s.jsx("div",{className:"space-y-2","aria-label":"Scanning",role:"status",children:[0,1,2].map(e=>s.jsx("div",{className:"h-16 animate-pulse rounded-2xl bg-surface-active"},e))})}function Re(e){const t=[],n=[["temp-root",a=>`${a} in temp directories`],["no-git",a=>`${a} without a git repository`],["outside-home",a=>`${a} outside your home directory`],["already-managed",a=>`${a} already a project`],["no-sessions",a=>`${a} with nothing to adopt`],["no-recorded-cwd",a=>`${a} with no recorded directory`],["missing",a=>`${a} no longer on disk`],["home-root",a=>`${a} your home directory itself`],["system-path",a=>`${a} system directories`],["paddock-internal",a=>`${a} inside Paddock's own data`]];for(const[a,c]of n){const l=e.excluded[a];l!==void 0&&l>0&&t.push(c(l))}const r=`${e.scanned} transcript folder${e.scanned===1?"":"s"} scanned`;return t.length===0?`${r}.`:`${r}; skipped ${t.join(", ")}.`}function _(e){var r,a;const t=(r=e.autoName)==null?void 0:r.trim();if(t!==void 0&&t!=="")return t;const n=(a=e.preview)==null?void 0:a.trim();return n!==void 0&&n!==""?n:e.sessionId}function q(e){const t=new Date(e);return Number.isNaN(t.getTime())?"unknown date":t.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"})}function Oe(e){return e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} kB`:`${(e/(1024*1024)).toFixed(1)} MB`}export{Ge as D};
|
|
1
|
+
import{h as oe,d as ie,a1 as de,r as u,a as A,j as s,W as Q,A as X,a5 as Y,J as $,a2 as ce,U as le,i as he,a6 as ue,a7 as J,K as T,c as U,a8 as pe,v as xe,Z as V}from"./index-DN82do6Z.js";import{E as R,C as me}from"./Surfaces-CHM29PhP.js";function ge(e){const t={};for(const n of e)t[n.path]={checked:!0,expanded:!1,loading:!1,error:null};return t}function fe(e){if(!e.sessions||!e.selected)return e.checked?"on":"off";const t=e.sessions.sessions.length,n=e.selected.size;return t===0||n===0?"off":n===t?"on":"mixed"}function C(e,t){return e.sessions&&e.selected?e.selected.size:e.checked?t.sessionCount:0}function je(e,t){return t.reduce((n,r)=>n+C(e[r.path]??O(),r),0)}function be(e,t){return t.filter(n=>C(e[n.path]??O(),n)>0)}function we(e,t,n){const r=e[t];if(!r)return e;const a={...r,checked:n};return r.sessions&&(a.selected=n?new Set(r.sessions.sessions.map(c=>c.sessionId)):new Set),{...e,[t]:a}}function ye(e,t,n){const r=e[t];if(!r||!r.sessions)return e;const a=new Set(r.selected??[]);return a.delete(n)||a.add(n),{...e,[t]:{...r,selected:a,checked:a.size>0}}}function ve(e,t,n){const r=e[t];if(!r)return e;const a=r.checked?new Set(n.sessions.map(c=>c.sessionId)):new Set;return{...e,[t]:{...r,sessions:n,selected:a,loading:!1,error:null}}}function D(e,t,n){const r=e[t];return r?{...e,[t]:{...r,...n}}:e}function Ne(e,t){const n=e??O();return C(n,t)===0?null:n.sessions&&n.selected?{sessionIds:[...n.selected]}:{}}function O(){return{checked:!1,expanded:!1,loading:!1,error:null}}function ke(e){return e.error!==void 0?e.stage==="create"?"danger":"warn":e.adopted>0?"success":"warn"}async function Se(e,t,n){let r;try{r=await e.createProject({name:t.name,slug:t.suggestedSlug,path:t.path,managed:!1})}catch(a){return{stage:"create",adopted:0,skipped:[],error:Z(a)}}try{const a=await e.adoptChats(r.slug,{sourceCwd:r.workingDir,...n.sessionIds?{sessionIds:n.sessionIds}:{}});return{project:r,adopted:a.adopted.length,skipped:a.skipped}}catch(a){return{stage:"import",project:r,adopted:0,skipped:[],error:Z(a)}}}async function $e(e,t,n){const r=[];for(const a of t){n(a.candidate.path,"running");const c=await Se(e,a.candidate,a.plan);r.push(c),n(a.candidate.path,"done",c)}return r}const Ce={sidechain:"sub-agent transcript","already-adopted":"already adopted","destination-exists":"a chat with that id already exists here","attributed-to-run":"belongs to a Paddock run",unreadable:"transcript unreadable","placement-failed":"copy failed","record-failed":"could not be recorded"};function W(e){if(e.length===0)return"";const t=[...new Set(e.map(n=>Ce[n.reason]??n.reason))];return`${e.length} skipped (${t.join(", ")})`}function Ie(e,t){var l;if(e.stage==="create")return`Couldn't create the project: ${e.error}`;const n=((l=e.project)==null?void 0:l.slug)??t.suggestedSlug;if(e.stage==="import")return`Project “${n}” was created, but importing its chats failed: ${e.error}. It is there and empty — you can import them from the project itself.`;if(e.adopted===0){const m=W(e.skipped);return t.recordedPath!==void 0?`Project “${n}” was created but no chats came with it. Its transcripts record ${t.recordedPath}, a different spelling of ${t.path}, and an import matches on the exact path.`:m?`Project “${n}” was created but no chats came with it — ${m}.`:`Project “${n}” was created but no chats came with it.`}const r=e.adopted,a=W(e.skipped),c=`Adopted ${r} chat${r===1?"":"s"} into “${n}”`;return a?`${c} — ${a}.`:`${c}.`}function Z(e){return e instanceof Error?e.message:String(e)}function Ge({firstRun:e=!1,embedded:t=!1,onLeave:n,onStartChat:r}){const a=oe(),{refresh:c}=ie(),l=de(),[m,d]=u.useState(!1),[j,I]=u.useState(!1),[h,x]=u.useState(null),[y,z]=u.useState(null),[F,P]=u.useState(!0),[f,v]=u.useState({}),[k,G]=u.useState(!1),[L,H]=u.useState({}),[E,B]=u.useState({}),[S,ee]=u.useState(!1);u.useEffect(()=>{let o=!0;return P(!0),z(null),A.discover({includeNonGit:m,includeOutsideHome:j}).then(i=>{o&&(x(i),v(ge(i.candidates)),P(!1))}).catch(i=>{o&&(z(i instanceof Error?i.message:"Discovery failed"),P(!1))}),()=>{o=!1}},[m,j]);const w=(h==null?void 0:h.candidates)??[],M=u.useMemo(()=>je(f,w),[f,w]),b=u.useMemo(()=>be(f,w),[f,w]),se=u.useCallback(o=>{v(p=>{const g=p[o.path];if(!g)return p;const N=D(p,o.path,{expanded:!g.expanded});return g.expanded||g.sessions||g.loading?N:D(N,o.path,{loading:!0,error:null})});const i=f[o.path];!i||i.expanded||i.sessions||i.loading||A.discoverSessions(o.path).then(p=>v(g=>ve(g,o.path,p))).catch(p=>v(g=>D(g,o.path,{loading:!1,error:p instanceof Error?p.message:"Could not read this directory's sessions"})))},[f]),te=u.useCallback(async()=>{if(k)return;G(!0),H(Object.fromEntries(b.map(i=>[i.path,"pending"]))),B({});const o=b.map(i=>({candidate:i,plan:Ne(f[i.path],i)??{}}));await $e(A,o,(i,p,g)=>{H(N=>({...N,[i]:p})),g&&B(N=>({...N,[i]:g}))}),G(!1),ee(!0),await c()},[b,f,k,c]),ne=u.useCallback(async()=>{await c(),n==null||n(),a("/")},[a,c,n]),ae=Object.values(E).reduce((o,i)=>o+i.adopted,0),re=Object.values(E).filter(o=>o.project).length,K=s.jsxs(s.Fragment,{children:[s.jsx("p",{className:"mb-5 text-sm text-fg-muted",children:e?"If you have used Claude Code in a terminal on this machine, Paddock can adopt that history: each directory becomes a project with its conversations already in it.":"Directories on this machine with existing Claude Code history. Adopting one links it as a project and brings its conversations in; your own history is never moved or deleted."}),S&&s.jsx(Te,{imported:ae,projects:re,onGetStarted:ne}),y!==null&&s.jsx(Q,{tone:"danger",icon:s.jsx(X,{width:16,height:16}),children:y}),F&&s.jsx(De,{}),!F&&y===null&&h!==null&&s.jsxs(s.Fragment,{children:[h.scanned===0?s.jsx(Ae,{claudeHome:h.claudeHome,onStartChat:r}):w.length===0?s.jsx(R,{variant:"panel",icon:s.jsx(Y,{width:22,height:22}),title:"Nothing to adopt",body:"Paddock found Claude Code history on this machine, but none of it is available to adopt right now. The tally below says what was skipped, and the two toggles relax the rules that are safe to relax."}):s.jsx("ul",{className:"space-y-2",children:w.map(o=>s.jsx(Pe,{candidate:o,row:f[o.path],status:L[o.path],outcome:E[o.path],locked:k||S,onToggleRow:i=>v(p=>we(p,o.path,i)),onToggleSession:i=>v(p=>ye(p,o.path,i)),onToggleExpanded:()=>se(o)},o.path))}),!S&&h.scanned>0&&s.jsx(Ee,{result:h,includeNonGit:m,includeOutsideHome:j,disabled:k,onIncludeNonGit:d,onIncludeOutsideHome:I}),w.length>0&&!S&&s.jsxs("div",{className:"mt-5 flex items-center justify-between gap-3 border-t border-edge pt-4",children:[s.jsxs("span",{className:"text-sm text-fg-muted",children:[M," conversation",M===1?"":"s"," from ",b.length," director",b.length===1?"y":"ies"]}),s.jsxs($,{variant:"primary",onClick:()=>void te(),disabled:b.length===0,loading:k,loadingLabel:"Adopting…",children:["Adopt ",b.length," project",b.length===1?"":"s"]})]})]})]});return t?K:s.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[s.jsxs("header",{className:"pt-safe flex items-center gap-2 border-b border-edge px-3 pb-2.5 sm:px-6 lg:py-4",children:[s.jsx("button",{type:"button",onClick:()=>l==null?void 0:l.openNav(),"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:s.jsx(ce,{width:20,height:20})}),s.jsx(le,{width:18,height:18,className:"shrink-0 text-fg-subtle"}),s.jsx("h1",{className:"text-md font-semibold tracking-tight",children:"Discover"})]}),s.jsx("div",{className:"flex-1 overflow-y-auto overscroll-contain",children:s.jsx("div",{className:"mx-auto max-w-3xl px-6 py-6",children:K})})]})}function Pe({candidate:e,row:t,status:n,outcome:r,locked:a,onToggleRow:c,onToggleSession:l,onToggleExpanded:m}){const d=t??{checked:!1,expanded:!1,loading:!1,error:null},j=fe(d),I=C(d,e),h=r?ke(r):null;return s.jsx("li",{children:s.jsxs(me,{flush:!0,className:U("overflow-hidden",h==="success"&&"border-success-edge bg-success-soft",h==="warn"&&"border-warn-edge bg-warn-soft",h==="danger"&&"border-danger-edge bg-danger-soft",n==="running"&&"border-accent-edge"),"data-testid":`discover-row-${e.suggestedSlug}`,"data-tone":h??void 0,children:[s.jsxs("div",{className:"flex items-start gap-3 px-3 py-2.5",children:[s.jsx(J,{className:"mt-1",checked:j==="on",indeterminate:j==="mixed",disabled:a,"aria-label":`Adopt ${e.path}`,onChange:x=>c(x.target.checked)}),s.jsxs("div",{className:"min-w-0 flex-1",children:[s.jsx("code",{className:"block break-all text-sm text-fg",children:e.path}),s.jsxs("div",{className:"mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-fg-muted",children:[s.jsxs("span",{className:"tabular",children:[e.sessionCount," conversation",e.sessionCount===1?"":"s"]}),e.filteredCount>0&&s.jsxs("span",{className:"tabular",title:"Empty, slash-command-only or sweeper transcripts, withheld as noise",children:["· ",e.filteredCount," filtered out"]}),e.lastSessionAt!==void 0&&s.jsxs("span",{className:"tabular",children:["· last ",q(e.lastSessionAt)]}),e.gitRemote!==void 0&&s.jsx(T,{tone:"lineage",size:"sm",children:e.gitRemote}),!e.hasGit&&s.jsx(T,{tone:"neutral",size:"sm",children:"no git"}),!e.insideHome&&s.jsx(T,{tone:"neutral",size:"sm",children:"outside home"})]}),e.recordedPath!==void 0&&s.jsx(Q,{tone:"warn",className:"mt-2",icon:s.jsx(X,{width:14,height:14}),children:s.jsxs("span",{className:"text-xs",children:["These transcripts record"," ",s.jsx("code",{className:"break-all",children:e.recordedPath})," — a different spelling of this path. The import matches on the exact path, so it may bring nothing in."]})}),r&&s.jsx("p",{className:U("mt-2 text-xs",h==="success"&&"text-success",h==="warn"&&"text-warn",h==="danger"&&"text-danger"),children:Ie(r,e)}),n==="running"&&!r&&s.jsx("p",{className:"mt-2 text-xs text-fg-muted",children:"Adopting…"})]}),s.jsxs("span",{className:"flex shrink-0 items-center gap-1",children:[s.jsx("span",{className:"tabular text-xs text-fg-subtle",children:I}),s.jsx($,{size:"icon-sm",variant:"subtle","aria-label":d.expanded?"Hide conversations":"Show conversations","aria-expanded":d.expanded,onClick:m,children:d.expanded?s.jsx(pe,{width:14,height:14}):s.jsx(xe,{width:14,height:14})})]})]}),d.expanded&&s.jsxs("div",{className:"border-t border-edge px-3 py-2",children:[d.loading&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Reading transcripts…"}),d.error!==null&&s.jsx("p",{className:"py-2 text-xs text-danger",children:d.error}),d.sessions&&d.sessions.sessions.length===0&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Nothing to adopt here any more — it may have been adopted since the scan."}),d.sessions&&d.sessions.sessions.length>0&&s.jsx("ul",{className:"space-y-1",children:d.sessions.sessions.map(x=>{var y;return s.jsx("li",{children:s.jsxs("label",{className:"flex cursor-pointer items-start gap-2 rounded-lg px-2 py-1.5 transition-colors hover:bg-surface-hover",children:[s.jsx(J,{checked:((y=d.selected)==null?void 0:y.has(x.sessionId))??!1,disabled:a,"aria-label":_(x),onChange:()=>l(x.sessionId)}),s.jsxs("span",{className:"min-w-0 flex-1",children:[s.jsx("span",{className:"block truncate text-xs text-fg",children:_(x)}),s.jsxs("span",{className:"tabular mt-0.5 block text-3xs text-fg-muted",children:[q(x.mtime)," · ",Oe(x.sizeBytes)]})]})]})},x.sessionId)})}),d.sessions&&d.sessions.filtered.length>0&&s.jsxs("p",{className:"mt-2 px-2 text-3xs text-fg-subtle",children:[d.sessions.filtered.length," more withheld as noise (",[...new Set(d.sessions.filtered.map(x=>x.reason))].join(", "),")."]})]})]})})}function Ee({result:e,includeNonGit:t,includeOutsideHome:n,disabled:r,onIncludeNonGit:a,onIncludeOutsideHome:c}){const l=e.excluded["no-git"]??0,m=e.excluded["outside-home"]??0,d=l>0||t,j=m>0||n;return e.scanned===0?null:s.jsxs("div",{className:"mt-5 space-y-2 border-t border-edge pt-4",children:[s.jsx("p",{className:"text-xs text-fg-muted",children:Re(e)}),d&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(V,{checked:t,disabled:r,onChange:a,label:"Also offer directories without a git repository"}),s.jsxs("span",{children:["Also offer directories without a git repository",l>0&&` (${l} hidden)`]})]}),j&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(V,{checked:n,disabled:r,onChange:c,label:"Also offer directories outside your home directory"}),s.jsxs("span",{children:["Also offer directories outside ",s.jsx("code",{children:e.homeDir}),m>0&&` (${m} hidden)`]})]})]})}function Ae({claudeHome:e,onStartChat:t}){return s.jsx(R,{variant:"panel",icon:s.jsx(ue,{width:22,height:22}),title:"Nothing to adopt yet",body:s.jsxs(s.Fragment,{children:["No Claude Code history has been recorded under"," ",s.jsx("code",{className:"break-all",children:e}),". That is normal on a new install, and on a container whose Claude home is its own rather than yours — nothing is wrong. Start a chat here and Paddock writes its own history; or bind-mount an existing one there and re-open this page, or import it headlessly with ",s.jsx("code",{children:"npm run import-chats"}),"."]}),action:t&&s.jsx($,{variant:"primary",icon:s.jsx(he,{width:14,height:14}),onClick:t,children:"Start a chat"})})}function Te({imported:e,projects:t,onGetStarted:n}){return s.jsx("div",{className:"mb-6",children:s.jsx(R,{variant:"panel",icon:s.jsx(Y,{width:22,height:22}),title:t===0?"Nothing was adopted":`${t} project${t===1?"":"s"}, ${e} conversation${e===1?"":"s"}`,body:t===0?"Every row failed. Each one says why below.":"They are in the sidebar now. Anything that did not land says why below.",action:s.jsx($,{variant:"primary",onClick:()=>void n(),children:"Get started"})})})}function De(){return s.jsx("div",{className:"space-y-2","aria-label":"Scanning",role:"status",children:[0,1,2].map(e=>s.jsx("div",{className:"h-16 animate-pulse rounded-2xl bg-surface-active"},e))})}function Re(e){const t=[],n=[["temp-root",a=>`${a} in temp directories`],["no-git",a=>`${a} without a git repository`],["outside-home",a=>`${a} outside your home directory`],["already-managed",a=>`${a} already a project`],["no-sessions",a=>`${a} with nothing to adopt`],["no-recorded-cwd",a=>`${a} with no recorded directory`],["missing",a=>`${a} no longer on disk`],["home-root",a=>`${a} your home directory itself`],["system-path",a=>`${a} system directories`],["paddock-internal",a=>`${a} inside Paddock's own data`]];for(const[a,c]of n){const l=e.excluded[a];l!==void 0&&l>0&&t.push(c(l))}const r=`${e.scanned} transcript folder${e.scanned===1?"":"s"} scanned`;return t.length===0?`${r}.`:`${r}; skipped ${t.join(", ")}.`}function _(e){var r,a;const t=(r=e.autoName)==null?void 0:r.trim();if(t!==void 0&&t!=="")return t;const n=(a=e.preview)==null?void 0:a.trim();return n!==void 0&&n!==""?n:e.sessionId}function q(e){const t=new Date(e);return Number.isNaN(t.getTime())?"unknown date":t.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"})}function Oe(e){return e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} kB`:`${(e/(1024*1024)).toFixed(1)} MB`}export{Ge as D};
|
|
2
2
|
|