@edspencer/paddock 0.59.0 → 0.60.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/adoptable.d.ts +72 -3
- package/packages/server/dist/adoptable.js +231 -25
- package/packages/server/dist/cli/args.d.ts +56 -0
- package/packages/server/dist/cli/args.js +159 -0
- package/packages/server/dist/cli/paddock.d.ts +1 -30
- package/packages/server/dist/cli/paddock.js +6 -146
- package/packages/server/dist/herdctl.d.ts +80 -0
- package/packages/server/dist/herdctl.js +159 -0
- package/packages/server/dist/routes/chats.js +59 -3
- package/packages/server/dist/run-provenance.d.ts +11 -0
- package/packages/server/dist/run-provenance.js +18 -0
- package/packages/server/dist/ws-turn.d.ts +0 -13
- package/packages/server/dist/ws-turn.js +50 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-Cli_WHsM.js → InstanceConfigPage-xn9Rvoah.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-BYK9nmr4.js → MarkdownRenderer-DMONssKA.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DWcMpdZJ.js → ProjectRedirect-DHoWtvtY.js} +1 -1
- package/packages/web/dist/assets/ProjectView-BmSdUwY2.js +13 -0
- package/packages/web/dist/assets/{ProjectsGrid-Bf2cx9-Z.js → ProjectsGrid-CjfwpeFj.js} +1 -1
- package/packages/web/dist/assets/{arc-CZ9CFd-g.js → arc-DdFdbD-c.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BzOXUlLu.js → architectureDiagram-3BPJPVTR-DC8UKtwV.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-Bnkcx-Fn.js → blockDiagram-GPEHLZMM-DqFFe_5c.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-C-nqckPS.js → c4Diagram-AAUBKEIU-DbffFSM-.js} +1 -1
- package/packages/web/dist/assets/channel-QxsJGL6F.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-jVn7J_5N.js → chunk-2J33WTMH-8PP2RHft.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-D5eys7Hi.js → chunk-4BX2VUAB-Hag68B5N.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CqpdXIoL.js → chunk-55IACEB6-BXp_gQIa.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-SQikk09l.js → chunk-727SXJPM-DlntjQux.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-rfBNpUwD.js → chunk-AQP2D5EJ-BhI9KPrE.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-CnL0tA20.js → chunk-FMBD7UC4-WhfORs2z.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-CZGi4swC.js → chunk-ND2GUHAM-CIskmv20.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-Du4WKuCj.js → chunk-QZHKN3VN-CNOTpCW4.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CI7p84hR.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CI7p84hR.js +2 -0
- package/packages/web/dist/assets/{core-LJ--cHnT.js → core-usOVRkLM.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CEovKrYZ.js → cose-bilkent-S5V4N54A-CniuMIIW.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-jQhK8147.js → dagre-BM42HDAG-5CteYfQY.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-BmiauiWQ.js → diagram-2AECGRRQ-BIQjD2LA.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-VYdTkqGm.js → diagram-5GNKFQAL-1vy3ErTt.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-DcOTKmwo.js → diagram-KO2AKTUF-D45iqFk6.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-DaehIKcD.js → diagram-LMA3HP47-DNktDktn.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-DTqk6D4N.js → diagram-OG6HWLK6-BQ9JgV70.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-Dj4rI8M1.js → erDiagram-TEJ5UH35-oBasZ1uv.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-D0FWSohH.js → flowDiagram-I6XJVG4X-DxnJHMBS.js} +1 -1
- package/packages/web/dist/assets/{format-CgkZTf5F.js → format-CIAat6sU.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-DgaRh12R.js → ganttDiagram-6RSMTGT7-CtJQ2m7z.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-5qiry4ao.js → gitGraphDiagram-PVQCEYII-5RPscwD_.js} +1 -1
- package/packages/web/dist/assets/index-B6etJCHf.css +1 -0
- package/packages/web/dist/assets/{index-CcYeBUNk.js → index-DtRblEY_.js} +4 -4
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-CZmDwouw.js → infoDiagram-5YYISTIA-zQu3xkW4.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-knkHstDa.js → ishikawaDiagram-YF4QCWOH-ChoEjcdw.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-ByfdmIVy.js → journeyDiagram-JHISSGLW-DM2KWmDr.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DSzly2xb.js → kanban-definition-UN3LZRKU-DWzTpYgh.js} +1 -1
- package/packages/web/dist/assets/{linear-BX6qYIpB.js → linear-Bpy9Aj-K.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-BXPz6_-1.js → mermaid.core-D43x5bFb.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-fzfZWy91.js → mindmap-definition-RKZ34NQL-BhceB-cv.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-M99j1K9t.js → pieDiagram-4H26LBE5-BgcGlFFA.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B7fKq1ce.js → quadrantDiagram-W4KKPZXB-ZRVaOoSp.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-xCBFrXav.js → requirementDiagram-4Y6WPE33-2hHcJHBt.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-Dp9KLMhd.js → sankeyDiagram-5OEKKPKP-KPUQ9Mu5.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-CNNVWpta.js → sequenceDiagram-3UESZ5HK-D4BXQSSM.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-CzKRXREb.js → stateDiagram-AJRCARHV-DftS7dIo.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CgBC1jpO.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-DfvQTe5J.js → timeline-definition-PNZ67QCA-C0OAxyWY.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-2PBM01eq.js → vennDiagram-CIIHVFJN-C94fINN3.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-DC1EIjci.js → wardley-L42UT6IY-CbFrlwHE.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-4kYoKkfS.js → wardleyDiagram-YWT4CUSO-CHG2nndd.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-Bin8O1V8.js → xychartDiagram-2RQKCTM6-U9ESc19z.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/ProjectView-CvD9WQsW.js +0 -13
- package/packages/web/dist/assets/channel-By51cUN7.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-sIDgFl-L.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-sIDgFl-L.js +0 -2
- package/packages/web/dist/assets/index-CvGiQddO.css +0 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-LKJwobEd.js +0 -2
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure argument parsing and message formatting for the `paddock` CLI.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is its own module
|
|
5
|
+
*
|
|
6
|
+
* These functions want unit tests, and the entrypoint they used to live in
|
|
7
|
+
* cannot be imported without running. The first attempt kept them in
|
|
8
|
+
* `paddock.ts` behind a run-directly guard:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* if (pathToFileURL(process.argv[1]).href === import.meta.url) main();
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* **That guard shipped a silent no-op to npm.** npm installs a `bin` as a
|
|
15
|
+
* SYMLINK at `node_modules/.bin/paddock`, so `process.argv[1]` is the symlink
|
|
16
|
+
* path while `import.meta.url` is the module's realpath. They never match, the
|
|
17
|
+
* condition is always false, and `npx @edspencer/paddock` printed nothing and
|
|
18
|
+
* exited 0 — in 0.57.0, 0.58.0 and 0.59.0. It went unnoticed because the manual
|
|
19
|
+
* checks invoked `node <file>` directly, which is the one path where the guard
|
|
20
|
+
* does hold.
|
|
21
|
+
*
|
|
22
|
+
* `realpathSync(argv[1])` would fix that instance. Splitting the module removes
|
|
23
|
+
* the need for the guard entirely, so there is no condition left to get wrong on
|
|
24
|
+
* the next shim, platform or package manager. `paddock.ts` now always runs.
|
|
25
|
+
*/
|
|
26
|
+
/** A usage error. Thrown rather than exiting, so parsing stays testable. */
|
|
27
|
+
export class CliError extends Error {
|
|
28
|
+
}
|
|
29
|
+
export const MIN_NODE_MAJOR = 22;
|
|
30
|
+
export function parseArgs(argv) {
|
|
31
|
+
const opts = {
|
|
32
|
+
here: false,
|
|
33
|
+
open: false,
|
|
34
|
+
verbose: false,
|
|
35
|
+
help: false,
|
|
36
|
+
version: false,
|
|
37
|
+
};
|
|
38
|
+
for (let i = 0; i < argv.length; i++) {
|
|
39
|
+
const arg = argv[i];
|
|
40
|
+
const next = () => {
|
|
41
|
+
const v = argv[++i];
|
|
42
|
+
if (v === undefined)
|
|
43
|
+
throw new CliError(`${arg} needs a value`);
|
|
44
|
+
return v;
|
|
45
|
+
};
|
|
46
|
+
switch (arg) {
|
|
47
|
+
case "-p":
|
|
48
|
+
case "--port":
|
|
49
|
+
opts.port = next();
|
|
50
|
+
break;
|
|
51
|
+
case "--host":
|
|
52
|
+
opts.host = next();
|
|
53
|
+
break;
|
|
54
|
+
case "-d":
|
|
55
|
+
case "--data-dir":
|
|
56
|
+
opts.dataDir = next();
|
|
57
|
+
break;
|
|
58
|
+
case "--here":
|
|
59
|
+
opts.here = true;
|
|
60
|
+
break;
|
|
61
|
+
case "-o":
|
|
62
|
+
case "--open":
|
|
63
|
+
opts.open = true;
|
|
64
|
+
break;
|
|
65
|
+
case "--verbose":
|
|
66
|
+
opts.verbose = true;
|
|
67
|
+
break;
|
|
68
|
+
case "-h":
|
|
69
|
+
case "--help":
|
|
70
|
+
opts.help = true;
|
|
71
|
+
break;
|
|
72
|
+
case "-v":
|
|
73
|
+
case "--version":
|
|
74
|
+
opts.version = true;
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
throw new CliError(`unknown option: ${arg}\nRun \`paddock --help\` for usage.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return opts;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Why this Node is unusable, or `undefined` if it is fine.
|
|
84
|
+
*
|
|
85
|
+
* `engines` only warns by default, and the failure it eventually produces is an
|
|
86
|
+
* unexplained syntax error deep inside a dependency.
|
|
87
|
+
*/
|
|
88
|
+
export function nodeVersionProblem(nodeVersion) {
|
|
89
|
+
const major = Number(nodeVersion.split(".")[0]);
|
|
90
|
+
if (!Number.isFinite(major) || major >= MIN_NODE_MAJOR)
|
|
91
|
+
return undefined;
|
|
92
|
+
return (`Node ${nodeVersion} is too old — Paddock needs Node ${MIN_NODE_MAJOR}+.\n` +
|
|
93
|
+
`Upgrade Node, then re-run. (nodejs.org, or \`nvm install ${MIN_NODE_MAJOR}\`)`);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Translate a listen failure into something a human can act on.
|
|
97
|
+
*
|
|
98
|
+
* `EADDRINUSE` on 4000 is the likeliest first-run failure — a popular port and
|
|
99
|
+
* Paddock's own default — and Node's raw error names neither the port nor the
|
|
100
|
+
* flag that fixes it.
|
|
101
|
+
*/
|
|
102
|
+
export function explainListenError(err, host, port) {
|
|
103
|
+
const code = err?.code;
|
|
104
|
+
if (code === "EADDRINUSE") {
|
|
105
|
+
return (`port ${port} is already in use.\n` +
|
|
106
|
+
`Something else is listening on ${host}:${port} — possibly another Paddock.\n` +
|
|
107
|
+
`Pick a different port: paddock --port ${Number(port) + 1}`);
|
|
108
|
+
}
|
|
109
|
+
if (code === "EACCES") {
|
|
110
|
+
return (`not allowed to bind ${host}:${port}.\n` +
|
|
111
|
+
`Ports below 1024 need elevated privileges — use a higher one: paddock --port 4000`);
|
|
112
|
+
}
|
|
113
|
+
return String(err?.message ?? err);
|
|
114
|
+
}
|
|
115
|
+
export const USAGE = `paddock — run a Paddock instance locally
|
|
116
|
+
|
|
117
|
+
Usage
|
|
118
|
+
npx @edspencer/paddock [options]
|
|
119
|
+
|
|
120
|
+
Options
|
|
121
|
+
-p, --port <port> HTTP/WS port (default 4000, or $PORT)
|
|
122
|
+
--host <host> Bind address (default 127.0.0.1)
|
|
123
|
+
-d, --data-dir <path> Projects + state (default ~/.paddock, or $PADDOCK_DATA_DIR)
|
|
124
|
+
--here Open the CURRENT directory as the workspace (see below)
|
|
125
|
+
-o, --open Open the app in your browser once it is listening
|
|
126
|
+
--verbose Show the server's own logs (quiet by default)
|
|
127
|
+
-v, --version Print the Paddock version and exit
|
|
128
|
+
-h, --help Show this help
|
|
129
|
+
|
|
130
|
+
Opening a directory (--here)
|
|
131
|
+
Run \`paddock --here\` inside a project and Paddock opens THAT directory as its
|
|
132
|
+
workspace: Claude works in your files, and any Claude Code sessions you already
|
|
133
|
+
have for the directory are offered for import. It:
|
|
134
|
+
|
|
135
|
+
· creates .paddock/ in the directory for this workspace's own state
|
|
136
|
+
· creates .chats/ for transcripts, and adds both to .gitignore
|
|
137
|
+
· links ~/.claude/projects/<encoded-dir> at this workspace
|
|
138
|
+
|
|
139
|
+
Once done, later runs in the same directory resume it — no flag needed.
|
|
140
|
+
Without --here, Paddock never touches the directory you ran it from.
|
|
141
|
+
|
|
142
|
+
Credentials
|
|
143
|
+
Paddock drives Claude Code, so it needs Claude credentials in the
|
|
144
|
+
environment: CLAUDE_CODE_OAUTH_TOKEN (Max/Pro) or ANTHROPIC_API_KEY (API
|
|
145
|
+
billing). If you already use Claude Code on this machine, an existing login
|
|
146
|
+
under ~/.claude is picked up automatically. Otherwise run \`claude setup-token\`.
|
|
147
|
+
|
|
148
|
+
Your data
|
|
149
|
+
Everything lives in one directory — ~/.paddock unless you pass --data-dir.
|
|
150
|
+
Projects, chat transcripts and settings all persist there between runs. Move
|
|
151
|
+
that directory to move your instance; delete it to start over. Nothing is
|
|
152
|
+
stored anywhere else.
|
|
153
|
+
|
|
154
|
+
Notes
|
|
155
|
+
Binds loopback with authentication disabled, which is safe for a laptop. To
|
|
156
|
+
expose it on a network, set PADDOCK_AUTH_MODE first — Paddock refuses to bind
|
|
157
|
+
a routable interface wide open. See AUTH.md.
|
|
158
|
+
|
|
159
|
+
Docs: https://github.com/edspencer/paddock`;
|
|
@@ -1,31 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export
|
|
3
|
-
port?: string;
|
|
4
|
-
host?: string;
|
|
5
|
-
dataDir?: string;
|
|
6
|
-
here: boolean;
|
|
7
|
-
open: boolean;
|
|
8
|
-
verbose: boolean;
|
|
9
|
-
help: boolean;
|
|
10
|
-
version: boolean;
|
|
11
|
-
}
|
|
12
|
-
/** A usage error. Thrown rather than exiting, so `parseArgs` stays testable. */
|
|
13
|
-
export declare class CliError extends Error {
|
|
14
|
-
}
|
|
15
|
-
export declare function parseArgs(argv: string[]): CliOptions;
|
|
16
|
-
/**
|
|
17
|
-
* Why this Node is unusable, or `undefined` if it is fine.
|
|
18
|
-
*
|
|
19
|
-
* Split from the exit so it can be tested. `engines` only warns by default, and
|
|
20
|
-
* the failure it eventually produces is an unexplained syntax error deep inside
|
|
21
|
-
* a dependency — which is nobody's idea of a first-run experience.
|
|
22
|
-
*/
|
|
23
|
-
export declare function nodeVersionProblem(nodeVersion: string): string | undefined;
|
|
24
|
-
/**
|
|
25
|
-
* Translate a listen failure into something a human can act on.
|
|
26
|
-
*
|
|
27
|
-
* `EADDRINUSE` on 4000 is the single most likely first-run failure — it is a
|
|
28
|
-
* popular port and Paddock's own default — and Node's raw error names neither
|
|
29
|
-
* the port nor the flag that fixes it.
|
|
30
|
-
*/
|
|
31
|
-
export declare function explainListenError(err: unknown, host: string, port: string): string;
|
|
2
|
+
export {};
|
|
@@ -22,9 +22,9 @@ import fs from "node:fs";
|
|
|
22
22
|
import os from "node:os";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { spawn } from "node:child_process";
|
|
25
|
-
import { fileURLToPath
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { HERE_MARKER, isHereWorkspace, countClaudeSessions, ensureGitignored } from "./here.js";
|
|
27
|
-
|
|
27
|
+
import { CliError, USAGE, parseArgs, nodeVersionProblem, explainListenError, } from "./args.js";
|
|
28
28
|
/** This module's own directory: `<…>/packages/server/dist/cli`. */
|
|
29
29
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
30
30
|
/**
|
|
@@ -53,61 +53,6 @@ function findUp(rel) {
|
|
|
53
53
|
}
|
|
54
54
|
/** Nearest ancestor holding a `package.json` — `packages/server` in both layouts. */
|
|
55
55
|
const packageRoot = findUp("package.json") ?? path.resolve(moduleDir, "../..");
|
|
56
|
-
/** A usage error. Thrown rather than exiting, so `parseArgs` stays testable. */
|
|
57
|
-
export class CliError extends Error {
|
|
58
|
-
}
|
|
59
|
-
export function parseArgs(argv) {
|
|
60
|
-
const opts = {
|
|
61
|
-
here: false,
|
|
62
|
-
open: false,
|
|
63
|
-
verbose: false,
|
|
64
|
-
help: false,
|
|
65
|
-
version: false,
|
|
66
|
-
};
|
|
67
|
-
for (let i = 0; i < argv.length; i++) {
|
|
68
|
-
const arg = argv[i];
|
|
69
|
-
const next = () => {
|
|
70
|
-
const v = argv[++i];
|
|
71
|
-
if (v === undefined)
|
|
72
|
-
throw new CliError(`${arg} needs a value`);
|
|
73
|
-
return v;
|
|
74
|
-
};
|
|
75
|
-
switch (arg) {
|
|
76
|
-
case "-p":
|
|
77
|
-
case "--port":
|
|
78
|
-
opts.port = next();
|
|
79
|
-
break;
|
|
80
|
-
case "--host":
|
|
81
|
-
opts.host = next();
|
|
82
|
-
break;
|
|
83
|
-
case "-d":
|
|
84
|
-
case "--data-dir":
|
|
85
|
-
opts.dataDir = next();
|
|
86
|
-
break;
|
|
87
|
-
case "--here":
|
|
88
|
-
opts.here = true;
|
|
89
|
-
break;
|
|
90
|
-
case "-o":
|
|
91
|
-
case "--open":
|
|
92
|
-
opts.open = true;
|
|
93
|
-
break;
|
|
94
|
-
case "--verbose":
|
|
95
|
-
opts.verbose = true;
|
|
96
|
-
break;
|
|
97
|
-
case "-h":
|
|
98
|
-
case "--help":
|
|
99
|
-
opts.help = true;
|
|
100
|
-
break;
|
|
101
|
-
case "-v":
|
|
102
|
-
case "--version":
|
|
103
|
-
opts.version = true;
|
|
104
|
-
break;
|
|
105
|
-
default:
|
|
106
|
-
throw new CliError(`unknown option: ${arg}\nRun \`paddock --help\` for usage.`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return opts;
|
|
110
|
-
}
|
|
111
56
|
function fail(message) {
|
|
112
57
|
console.error(`paddock: ${message}`);
|
|
113
58
|
process.exit(1);
|
|
@@ -121,65 +66,6 @@ function readVersion() {
|
|
|
121
66
|
return "unknown";
|
|
122
67
|
}
|
|
123
68
|
}
|
|
124
|
-
const USAGE = `paddock — run a Paddock instance locally
|
|
125
|
-
|
|
126
|
-
Usage
|
|
127
|
-
npx @edspencer/paddock [options]
|
|
128
|
-
|
|
129
|
-
Options
|
|
130
|
-
-p, --port <port> HTTP/WS port (default 4000, or $PORT)
|
|
131
|
-
--host <host> Bind address (default 127.0.0.1)
|
|
132
|
-
-d, --data-dir <path> Projects + state (default ~/.paddock, or $PADDOCK_DATA_DIR)
|
|
133
|
-
--here Open the CURRENT directory as the workspace (see below)
|
|
134
|
-
-o, --open Open the app in your browser once it is listening
|
|
135
|
-
--verbose Show the server's own logs (quiet by default)
|
|
136
|
-
-v, --version Print the Paddock version and exit
|
|
137
|
-
-h, --help Show this help
|
|
138
|
-
|
|
139
|
-
Opening a directory (--here)
|
|
140
|
-
Run \`paddock --here\` inside a project and Paddock opens THAT directory as its
|
|
141
|
-
workspace: Claude works in your files, and any Claude Code sessions you already
|
|
142
|
-
have for the directory are offered for import. It:
|
|
143
|
-
|
|
144
|
-
· creates .paddock/ in the directory for this workspace's own state
|
|
145
|
-
· creates .chats/ for transcripts, and adds both to .gitignore
|
|
146
|
-
· links ~/.claude/projects/<encoded-dir> at this workspace
|
|
147
|
-
|
|
148
|
-
Once done, later runs in the same directory resume it — no flag needed.
|
|
149
|
-
Without --here, Paddock never touches the directory you ran it from.
|
|
150
|
-
|
|
151
|
-
Credentials
|
|
152
|
-
Paddock drives Claude Code, so it needs Claude credentials in the
|
|
153
|
-
environment: CLAUDE_CODE_OAUTH_TOKEN (Max/Pro) or ANTHROPIC_API_KEY (API
|
|
154
|
-
billing). If you already use Claude Code on this machine, an existing login
|
|
155
|
-
under ~/.claude is picked up automatically. Otherwise run \`claude setup-token\`.
|
|
156
|
-
|
|
157
|
-
Your data
|
|
158
|
-
Everything lives in one directory — ~/.paddock unless you pass --data-dir.
|
|
159
|
-
Projects, chat transcripts and settings all persist there between runs. Move
|
|
160
|
-
that directory to move your instance; delete it to start over. Nothing is
|
|
161
|
-
stored anywhere else.
|
|
162
|
-
|
|
163
|
-
Notes
|
|
164
|
-
Binds loopback with authentication disabled, which is safe for a laptop. To
|
|
165
|
-
expose it on a network, set PADDOCK_AUTH_MODE first — Paddock refuses to bind
|
|
166
|
-
a routable interface wide open. See AUTH.md.
|
|
167
|
-
|
|
168
|
-
Docs: https://github.com/edspencer/paddock`;
|
|
169
|
-
/**
|
|
170
|
-
* Why this Node is unusable, or `undefined` if it is fine.
|
|
171
|
-
*
|
|
172
|
-
* Split from the exit so it can be tested. `engines` only warns by default, and
|
|
173
|
-
* the failure it eventually produces is an unexplained syntax error deep inside
|
|
174
|
-
* a dependency — which is nobody's idea of a first-run experience.
|
|
175
|
-
*/
|
|
176
|
-
export function nodeVersionProblem(nodeVersion) {
|
|
177
|
-
const major = Number(nodeVersion.split(".")[0]);
|
|
178
|
-
if (!Number.isFinite(major) || major >= MIN_NODE_MAJOR)
|
|
179
|
-
return undefined;
|
|
180
|
-
return (`Node ${nodeVersion} is too old — Paddock needs Node ${MIN_NODE_MAJOR}+.\n` +
|
|
181
|
-
`Upgrade Node, then re-run. (nodejs.org, or \`nvm install ${MIN_NODE_MAJOR}\`)`);
|
|
182
|
-
}
|
|
183
69
|
/**
|
|
184
70
|
* Put our bundled `claude` on PATH for child processes.
|
|
185
71
|
*
|
|
@@ -308,26 +194,6 @@ function openBrowser(url) {
|
|
|
308
194
|
/* as above */
|
|
309
195
|
}
|
|
310
196
|
}
|
|
311
|
-
/**
|
|
312
|
-
* Translate a listen failure into something a human can act on.
|
|
313
|
-
*
|
|
314
|
-
* `EADDRINUSE` on 4000 is the single most likely first-run failure — it is a
|
|
315
|
-
* popular port and Paddock's own default — and Node's raw error names neither
|
|
316
|
-
* the port nor the flag that fixes it.
|
|
317
|
-
*/
|
|
318
|
-
export function explainListenError(err, host, port) {
|
|
319
|
-
const code = err?.code;
|
|
320
|
-
if (code === "EADDRINUSE") {
|
|
321
|
-
return (`port ${port} is already in use.\n` +
|
|
322
|
-
`Something else is listening on ${host}:${port} — possibly another Paddock.\n` +
|
|
323
|
-
`Pick a different port: paddock --port ${Number(port) + 1}`);
|
|
324
|
-
}
|
|
325
|
-
if (code === "EACCES") {
|
|
326
|
-
return (`not allowed to bind ${host}:${port}.\n` +
|
|
327
|
-
`Ports below 1024 need elevated privileges — use a higher one: paddock --port 4000`);
|
|
328
|
-
}
|
|
329
|
-
return String(err?.message ?? err);
|
|
330
|
-
}
|
|
331
197
|
async function main() {
|
|
332
198
|
let opts;
|
|
333
199
|
try {
|
|
@@ -433,13 +299,7 @@ async function main() {
|
|
|
433
299
|
if (opts.open)
|
|
434
300
|
openBrowser(url);
|
|
435
301
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (process.argv[1] !== undefined && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
441
|
-
main().catch((err) => {
|
|
442
|
-
console.error("fatal:", err);
|
|
443
|
-
process.exit(1);
|
|
444
|
-
});
|
|
445
|
-
}
|
|
302
|
+
main().catch((err) => {
|
|
303
|
+
console.error("fatal:", err);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
});
|
|
@@ -134,6 +134,24 @@ export declare class HerdctlService {
|
|
|
134
134
|
* {@link cancel}). Entries are removed when the turn's stream ends.
|
|
135
135
|
*/
|
|
136
136
|
private liveSessions;
|
|
137
|
+
/**
|
|
138
|
+
* Background-phase turns keyed by the synthetic job id the sink minted for
|
|
139
|
+
* them, valued by the chat session they belong to (paddock#528).
|
|
140
|
+
*
|
|
141
|
+
* After a turn's primary `result` lands, {@link chatSession} returns and drops
|
|
142
|
+
* its {@link liveSessions} entry — but the session itself may live on, held
|
|
143
|
+
* open by the reaper because the turn launched background work, streaming
|
|
144
|
+
* autonomous re-invocation turns. Those turns are real, running, and shown as
|
|
145
|
+
* such; until now they were also uncancellable, because nothing registered
|
|
146
|
+
* them anywhere Stop could reach.
|
|
147
|
+
*
|
|
148
|
+
* `interrupt()` would be the wrong tool even if it could: it targets an
|
|
149
|
+
* in-flight model turn, and the wedge case is a session sitting idle holding
|
|
150
|
+
* background work that will never finish. So these route to a force-reap
|
|
151
|
+
* instead — end the session, let the stream end, and let the ordinary unwind
|
|
152
|
+
* emit `chat:complete`. Entries are removed when the background stream ends.
|
|
153
|
+
*/
|
|
154
|
+
private backgroundTurns;
|
|
137
155
|
/**
|
|
138
156
|
* Incremental index of `<stateDir>/jobs`, backing the unread-badge reads
|
|
139
157
|
* (`lastTurnCompletedAt*`) — one per service, reused across requests so a warm
|
|
@@ -325,12 +343,61 @@ export declare class HerdctlService {
|
|
|
325
343
|
*/
|
|
326
344
|
adoptChats(project: Project, opts?: {
|
|
327
345
|
sourceCwd?: string;
|
|
346
|
+
/**
|
|
347
|
+
* Import only these sessions (#660). Anything else the engine adopts along
|
|
348
|
+
* the way is released again, exactly as a noise-filtered session is — see
|
|
349
|
+
* the "honouring the noise filter" note above, which this reuses wholesale.
|
|
350
|
+
* Undefined means "everything on offer", which is what a confirmed
|
|
351
|
+
* select-all sends and what the CLI has always sent.
|
|
352
|
+
*/
|
|
353
|
+
sessionIds?: string[];
|
|
328
354
|
mode?: AdoptionPlacementMode;
|
|
329
355
|
dryRun?: boolean;
|
|
330
356
|
}): Promise<{
|
|
331
357
|
adopted: string[];
|
|
332
358
|
skipped: AdoptSkipped[];
|
|
333
359
|
}>;
|
|
360
|
+
/**
|
|
361
|
+
* Did an import of `sessionId` from `fromWorkingDir` place a NEW file?
|
|
362
|
+
*
|
|
363
|
+
* The same test `releaseWithheld` makes before deleting anything: an in-place
|
|
364
|
+
* adoption resolves the destination and the origin to one path, so there is no
|
|
365
|
+
* copy and nothing that may be removed.
|
|
366
|
+
*/
|
|
367
|
+
private placedACopy;
|
|
368
|
+
/**
|
|
369
|
+
* What the LAST import into each workspace did, so it can be undone (#660).
|
|
370
|
+
*
|
|
371
|
+
* Keyed by workspace key (which may be `""` — the root). Holds the sessions
|
|
372
|
+
* that import actually brought in, and of those, the ones where it PLACED a
|
|
373
|
+
* copy and may therefore delete one again.
|
|
374
|
+
*
|
|
375
|
+
* Server-side rather than a token the client hands back: undo deletes files, so
|
|
376
|
+
* the set of paths it may touch must come from what this process actually did,
|
|
377
|
+
* not from a request body. A restart forgets it, which is the right failure —
|
|
378
|
+
* the offer to undo is a transient toast, not a durable feature.
|
|
379
|
+
*
|
|
380
|
+
* Only the most recent import per workspace is kept. Undoing an import two
|
|
381
|
+
* imports ago is not offered and would be a lie to describe as "undo".
|
|
382
|
+
*/
|
|
383
|
+
private readonly lastImport;
|
|
384
|
+
/**
|
|
385
|
+
* Undo the import this process most recently performed into `project` (#660).
|
|
386
|
+
*
|
|
387
|
+
* Releases each adoption and deletes the copies that import placed, leaving the
|
|
388
|
+
* user's own `~/.claude` history exactly as it was. Returns the sessions it
|
|
389
|
+
* released; an empty array means there was nothing to undo (nothing imported
|
|
390
|
+
* yet, already undone, or the server restarted since).
|
|
391
|
+
*
|
|
392
|
+
* `sessionIds` narrows it to part of that import — the rest stays. Ids that
|
|
393
|
+
* were not part of it are ignored rather than rejected: undo is a best-effort
|
|
394
|
+
* safety net, and the alternative is a 400 for a button the user pressed once.
|
|
395
|
+
*/
|
|
396
|
+
unadoptChats(project: Project, opts?: {
|
|
397
|
+
sessionIds?: string[];
|
|
398
|
+
}): Promise<{
|
|
399
|
+
released: string[];
|
|
400
|
+
}>;
|
|
334
401
|
/**
|
|
335
402
|
* Undo the adoption of a session detection had withheld, restoring the state
|
|
336
403
|
* that existed before this import. See {@link adoptChats} for why the file
|
|
@@ -452,11 +519,24 @@ export declare class HerdctlService {
|
|
|
452
519
|
}): Promise<{
|
|
453
520
|
sessionId: string | null;
|
|
454
521
|
}>;
|
|
522
|
+
/**
|
|
523
|
+
* Register a background-phase turn so Stop can reach it (paddock#528). Called
|
|
524
|
+
* by the background turn sink once it knows both the synthetic job id it
|
|
525
|
+
* published and the session that stream belongs to. Re-registering the same
|
|
526
|
+
* job id with a later session id is fine — the sink refreshes the mapping if
|
|
527
|
+
* the stream resolves a different session.
|
|
528
|
+
*/
|
|
529
|
+
registerBackgroundTurn(jobId: string, sessionId: string): void;
|
|
530
|
+
/** Forget a background-phase turn once its stream has ended. */
|
|
531
|
+
unregisterBackgroundTurn(jobId: string): void;
|
|
455
532
|
/**
|
|
456
533
|
* Cancel a running turn (Stop button → WS chat:cancel). Handles BOTH drive
|
|
457
534
|
* modes off the single id the client holds as `jobId`:
|
|
458
535
|
* - **session mode** — the id is a synthetic turn id in {@link liveSessions};
|
|
459
536
|
* interrupt the live `RuntimeSession` (there is no herdctl job to cancel).
|
|
537
|
+
* - **background phase** — the id is a synthetic id in
|
|
538
|
+
* {@link backgroundTurns}; there is no model turn to interrupt, so reap the
|
|
539
|
+
* session (paddock#528).
|
|
460
540
|
* - **batch mode** — the id is a real herdctl job id; abort it via `cancelJob`
|
|
461
541
|
* (which kills the CLI subprocess / aborts the SDK query).
|
|
462
542
|
*
|