@paleo/workspace 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -0
- package/dist/cli.d.ts +47 -0
- package/dist/cli.js +235 -0
- package/dist/dev-server.d.ts +52 -0
- package/dist/dev-server.js +349 -0
- package/dist/dev-servers-registry.d.ts +42 -0
- package/dist/dev-servers-registry.js +140 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +18 -0
- package/dist/helpers.d.ts +31 -0
- package/dist/helpers.js +141 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/log-polling.d.ts +17 -0
- package/dist/log-polling.js +41 -0
- package/dist/port-holder.d.ts +25 -0
- package/dist/port-holder.js +147 -0
- package/dist/ports.d.ts +16 -0
- package/dist/ports.js +37 -0
- package/dist/process-control.d.ts +4 -0
- package/dist/process-control.js +41 -0
- package/dist/server-descriptor.d.ts +45 -0
- package/dist/server-descriptor.js +1 -0
- package/dist/slots.d.ts +63 -0
- package/dist/slots.js +164 -0
- package/dist/workspace.d.ts +144 -0
- package/dist/workspace.js +550 -0
- package/dist/worktree.d.ts +28 -0
- package/dist/worktree.js +124 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# @paleo/workspace
|
|
2
|
+
|
|
3
|
+
Run multiple local dev environments side by side, one per git worktree, with isolated ports, databases, and config files. Built for branches worked in parallel, by humans or AI agents.
|
|
4
|
+
|
|
5
|
+
Each project writes two custom scripts on top, using these entry points:
|
|
6
|
+
|
|
7
|
+
- `runWorkspace(config)` — worktree lifecycle (setup / remove / set-owner).
|
|
8
|
+
- `runDevServer(config)` — background dev-server start / stop / list.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
The `workspace-guide` skill is a setup-time companion. Install the skill (globally or locally):
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx skills add https://github.com/paleo/alignfirst --skill workspace-guide
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then, in your project, ask your agent:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
Use your workspace-guide skill. Set up worktree-based local environments in this project.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The agent reads the skill, adapts the reference scripts to your stack, installs `@paleo/workspace` as a dev dependency, and wires the npm scripts. After that, you can uninstall the skill, it won't be used by your project anymore.
|
|
25
|
+
|
|
26
|
+
## Workflow
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm run workspace -- setup feat/42 -c # new branch + worktree + isolated env
|
|
30
|
+
npm run dev:up # start dev server in the background (no-op if already running here)
|
|
31
|
+
npm run dev:up -- --restart # stop the dev-server in this worktree if running, then start fresh
|
|
32
|
+
npm run dev:up -- --evict # if devLimit is reached, evict the oldest dev-server and start
|
|
33
|
+
npm run dev:list # active dev-servers across all worktrees
|
|
34
|
+
npm run dev:down # stop dev server (infrastructure stays up)
|
|
35
|
+
npm run workspace -- remove feat/42 # full teardown
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { fileURLToPath } from "node:url";
|
|
42
|
+
import { runWorkspace, helpers } from "@paleo/workspace";
|
|
43
|
+
|
|
44
|
+
await runWorkspace({
|
|
45
|
+
scriptPath: fileURLToPath(import.meta.url),
|
|
46
|
+
devServerScript: fileURLToPath(new URL("./dev-server.mjs", import.meta.url)),
|
|
47
|
+
basePort: 8100,
|
|
48
|
+
portNames: ["server", "frontend", "db"],
|
|
49
|
+
sharedDirs: [".local", ".plans"],
|
|
50
|
+
runtimeDir: ".local-wt",
|
|
51
|
+
registryDir: ".local/wt-registry",
|
|
52
|
+
configFiles: [
|
|
53
|
+
{
|
|
54
|
+
path: ".env",
|
|
55
|
+
patch: (content, { ports }) =>
|
|
56
|
+
helpers.patchEnvFile(content, {
|
|
57
|
+
PORT: String(ports.frontend),
|
|
58
|
+
SERVER_PORT: String(ports.server),
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
preSetup: ({ currentWorktree, isMainWorktree, log }) => {
|
|
63
|
+
// Idempotent. Bootstrap source files the kernel expects to find (e.g. seed `.env` from
|
|
64
|
+
// `.env.example` on the main worktree). MUST NOT mutate the main worktree from a linked
|
|
65
|
+
// worktree setup — bootstrap the main first via `workspace setup`.
|
|
66
|
+
},
|
|
67
|
+
finalizeWorktree: async ({ currentWorktree }) => {
|
|
68
|
+
// MUST be idempotent. Install deps, start containers, seed a database, etc.
|
|
69
|
+
},
|
|
70
|
+
printSummary: ({ slot, branch, owner, ports, isMainWorktree, status }) =>
|
|
71
|
+
`Type: ${isMainWorktree ? "main" : "linked"}\nStatus: ${status}\nSlot: ${slot}\nBranch: ${branch}${owner ? `\nOwner: ${owner}` : ""}\nServer: :${ports.server}`,
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`branch` is resolved live from the worktree on each call (not persisted in the registry — `git checkout` makes any stored value stale). For detached HEAD or missing directory, it falls back to `"(detached)"`. `status` is the slot's finalize status: `"pending"` until `finalizeWorktree` succeeds, then `"ready"` (or `"failed"`).
|
|
76
|
+
|
|
77
|
+
Setup runs in two phases: a fast foreground Part 1 creates the worktree and config, then a detached Part 2 runs `finalizeWorktree` and writes progress to `<runtimeDir>/wt-setup.log`. If Part 2 fails, `cd` into the worktree and run `workspace setup` — it is idempotent and retries the finalize step. To block until Part 2 finishes (CI, agent orchestration), run `workspace wait` from inside the worktree (or `workspace wait --slot 8110` from anywhere) — exits 0 on `READY`, 1 on `FAILED`.
|
|
78
|
+
|
|
79
|
+
**Bootstrap the main worktree first.** Linked-worktree setup copies config sources from the main worktree, so the main must already have those files. Run `workspace setup` once on the main checkout. Use `preSetup` (with `isMainWorktree === true`) to seed sources from examples or templates. `configFiles` entries are required by default; mark `optional: true` for sources that may legitimately be missing.
|
|
80
|
+
|
|
81
|
+
`--evict` is best-effort: the cap check and the subsequent register are not atomic, so two concurrent `dev:up --evict` from different worktrees can both pass the check and end up at `devLimit + 1` live servers. The window is narrow; if it matters, `dev:list` + `dev:down` deterministically.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { runDevServer, helpers } from "@paleo/workspace";
|
|
85
|
+
|
|
86
|
+
await runDevServer({
|
|
87
|
+
basePort: 8100,
|
|
88
|
+
runtimeDir: ".local-wt",
|
|
89
|
+
registryDir: ".local/wt-registry",
|
|
90
|
+
devLimit: 5,
|
|
91
|
+
servers: [
|
|
92
|
+
{
|
|
93
|
+
kind: "spawn",
|
|
94
|
+
name: "dev",
|
|
95
|
+
exec: { command: "npm", args: ["run", "dev"] },
|
|
96
|
+
port: helpers.readPortFromEnvFile(".env", "PORT"),
|
|
97
|
+
detectSuccess: (log) => log.includes("Server is ready on port"),
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
printSummary: ({ slot, servers }) =>
|
|
101
|
+
`Dev servers started in slot ${slot.slot}${slot.owner ? ` (${slot.owner})` : ""}: ${servers
|
|
102
|
+
.map((s) => `${s.server.name} :${s.port} (PID ${s.pid})`)
|
|
103
|
+
.join(", ")}`,
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Build / test
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
npm install
|
|
111
|
+
npm run build
|
|
112
|
+
npm test
|
|
113
|
+
```
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type WorkspaceCommand = {
|
|
2
|
+
kind: "setup";
|
|
3
|
+
branch?: string;
|
|
4
|
+
newBranch: boolean;
|
|
5
|
+
owner?: string;
|
|
6
|
+
slot?: string;
|
|
7
|
+
force: boolean;
|
|
8
|
+
wait: boolean;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "remove";
|
|
11
|
+
branch?: string;
|
|
12
|
+
noRemoteCheck: boolean;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "list";
|
|
15
|
+
} | {
|
|
16
|
+
kind: "info";
|
|
17
|
+
slot?: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "wait";
|
|
20
|
+
slot?: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "set-owner";
|
|
23
|
+
name: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "finalize";
|
|
26
|
+
slot: string;
|
|
27
|
+
force: boolean;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "help";
|
|
30
|
+
};
|
|
31
|
+
export interface ParsedWorkspaceArgs {
|
|
32
|
+
command: WorkspaceCommand;
|
|
33
|
+
verbose: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function parseWorkspaceArgs(argv?: string[]): ParsedWorkspaceArgs;
|
|
36
|
+
export declare function printWorkspaceHelp(): void;
|
|
37
|
+
export interface DevServerArgs {
|
|
38
|
+
help?: boolean;
|
|
39
|
+
stop?: boolean;
|
|
40
|
+
list?: boolean;
|
|
41
|
+
all?: boolean;
|
|
42
|
+
evict?: boolean;
|
|
43
|
+
restart?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function parseDevServerArgs(argv?: string[]): DevServerArgs;
|
|
46
|
+
export declare function printDevServerHelp(): void;
|
|
47
|
+
export declare function validateDevServerFlags(args: DevServerArgs): void;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { ConfigError } from "./errors.js";
|
|
3
|
+
const DEV_SERVER_OPTIONS = {
|
|
4
|
+
help: { type: "boolean", short: "h", description: "Show this help message" },
|
|
5
|
+
stop: { type: "boolean", description: "Stop dev servers in the current worktree" },
|
|
6
|
+
list: { type: "boolean", description: "List active dev-servers across all worktrees" },
|
|
7
|
+
all: { type: "boolean", description: "Apply --stop to every active dev-server" },
|
|
8
|
+
evict: { type: "boolean", description: "Evict the oldest dev-server when the cap is reached" },
|
|
9
|
+
restart: {
|
|
10
|
+
type: "boolean",
|
|
11
|
+
description: "If a dev-server is already running in this worktree, stop it first, then start",
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export function parseWorkspaceArgs(argv = process.argv.slice(2)) {
|
|
15
|
+
const [subcommand, ...tokens] = argv;
|
|
16
|
+
if (subcommand === undefined || subcommand === "--help" || subcommand === "-h") {
|
|
17
|
+
return { command: { kind: "help" }, verbose: false };
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return parseSubcommand(subcommand, tokens);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
if (err instanceof ConfigError)
|
|
24
|
+
throw err;
|
|
25
|
+
throw new ConfigError(`Error: ${err.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function parseSubcommand(subcommand, tokens) {
|
|
29
|
+
switch (subcommand) {
|
|
30
|
+
case "setup":
|
|
31
|
+
return parseSetup(tokens);
|
|
32
|
+
case "remove":
|
|
33
|
+
return parseRemove(tokens);
|
|
34
|
+
case "list":
|
|
35
|
+
return parseList(tokens);
|
|
36
|
+
case "info":
|
|
37
|
+
return parseInfo(tokens);
|
|
38
|
+
case "wait":
|
|
39
|
+
return parseWait(tokens);
|
|
40
|
+
case "set-owner":
|
|
41
|
+
return parseSetOwner(tokens);
|
|
42
|
+
case "__finalize":
|
|
43
|
+
return parseFinalize(tokens);
|
|
44
|
+
default:
|
|
45
|
+
throw new ConfigError(`Error: Unknown command "${subcommand}". Run \`workspace --help\`.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function parseSetup(tokens) {
|
|
49
|
+
const { values, positionals } = parseArgs({
|
|
50
|
+
args: tokens,
|
|
51
|
+
options: {
|
|
52
|
+
"new-branch": { type: "boolean", short: "c" },
|
|
53
|
+
owner: { type: "string" },
|
|
54
|
+
slot: { type: "string", short: "s" },
|
|
55
|
+
force: { type: "boolean" },
|
|
56
|
+
wait: { type: "boolean" },
|
|
57
|
+
verbose: { type: "boolean", short: "v" },
|
|
58
|
+
},
|
|
59
|
+
allowPositionals: true,
|
|
60
|
+
strict: true,
|
|
61
|
+
});
|
|
62
|
+
const branch = takeOptionalPositional(positionals, "setup");
|
|
63
|
+
const newBranch = values["new-branch"] ?? false;
|
|
64
|
+
if (newBranch && branch === undefined) {
|
|
65
|
+
throw new ConfigError("Error: `workspace setup <branch> -c` requires a branch name.");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
command: {
|
|
69
|
+
kind: "setup",
|
|
70
|
+
branch,
|
|
71
|
+
newBranch,
|
|
72
|
+
owner: values.owner,
|
|
73
|
+
slot: values.slot,
|
|
74
|
+
force: values.force ?? false,
|
|
75
|
+
wait: values.wait ?? false,
|
|
76
|
+
},
|
|
77
|
+
verbose: values.verbose ?? false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function parseRemove(tokens) {
|
|
81
|
+
const { values, positionals } = parseArgs({
|
|
82
|
+
args: tokens,
|
|
83
|
+
options: {
|
|
84
|
+
"no-remote-check": { type: "boolean" },
|
|
85
|
+
verbose: { type: "boolean", short: "v" },
|
|
86
|
+
},
|
|
87
|
+
allowPositionals: true,
|
|
88
|
+
strict: true,
|
|
89
|
+
});
|
|
90
|
+
const branch = takeOptionalPositional(positionals, "remove");
|
|
91
|
+
return {
|
|
92
|
+
command: { kind: "remove", branch, noRemoteCheck: values["no-remote-check"] ?? false },
|
|
93
|
+
verbose: values.verbose ?? false,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function parseList(tokens) {
|
|
97
|
+
const { values, positionals } = parseArgs({
|
|
98
|
+
args: tokens,
|
|
99
|
+
options: { verbose: { type: "boolean", short: "v" } },
|
|
100
|
+
allowPositionals: true,
|
|
101
|
+
strict: true,
|
|
102
|
+
});
|
|
103
|
+
rejectPositionals(positionals, "list");
|
|
104
|
+
return { command: { kind: "list" }, verbose: values.verbose ?? false };
|
|
105
|
+
}
|
|
106
|
+
function parseInfo(tokens) {
|
|
107
|
+
const { values, positionals } = parseArgs({
|
|
108
|
+
args: tokens,
|
|
109
|
+
options: {
|
|
110
|
+
slot: { type: "string", short: "s" },
|
|
111
|
+
verbose: { type: "boolean", short: "v" },
|
|
112
|
+
},
|
|
113
|
+
allowPositionals: true,
|
|
114
|
+
strict: true,
|
|
115
|
+
});
|
|
116
|
+
rejectPositionals(positionals, "info");
|
|
117
|
+
return { command: { kind: "info", slot: values.slot }, verbose: values.verbose ?? false };
|
|
118
|
+
}
|
|
119
|
+
function parseWait(tokens) {
|
|
120
|
+
const { values, positionals } = parseArgs({
|
|
121
|
+
args: tokens,
|
|
122
|
+
options: {
|
|
123
|
+
slot: { type: "string", short: "s" },
|
|
124
|
+
verbose: { type: "boolean", short: "v" },
|
|
125
|
+
},
|
|
126
|
+
allowPositionals: true,
|
|
127
|
+
strict: true,
|
|
128
|
+
});
|
|
129
|
+
rejectPositionals(positionals, "wait");
|
|
130
|
+
return { command: { kind: "wait", slot: values.slot }, verbose: values.verbose ?? false };
|
|
131
|
+
}
|
|
132
|
+
function parseSetOwner(tokens) {
|
|
133
|
+
const { values, positionals } = parseArgs({
|
|
134
|
+
args: tokens,
|
|
135
|
+
options: { verbose: { type: "boolean", short: "v" } },
|
|
136
|
+
allowPositionals: true,
|
|
137
|
+
strict: true,
|
|
138
|
+
});
|
|
139
|
+
const name = takeRequiredPositional(positionals, "set-owner", "name");
|
|
140
|
+
return { command: { kind: "set-owner", name }, verbose: values.verbose ?? false };
|
|
141
|
+
}
|
|
142
|
+
function parseFinalize(tokens) {
|
|
143
|
+
const { values, positionals } = parseArgs({
|
|
144
|
+
args: tokens,
|
|
145
|
+
options: { force: { type: "boolean" } },
|
|
146
|
+
allowPositionals: true,
|
|
147
|
+
strict: true,
|
|
148
|
+
});
|
|
149
|
+
const slot = takeRequiredPositional(positionals, "__finalize", "slot");
|
|
150
|
+
return { command: { kind: "finalize", slot, force: values.force ?? false }, verbose: false };
|
|
151
|
+
}
|
|
152
|
+
function takeOptionalPositional(positionals, command) {
|
|
153
|
+
if (positionals.length > 1) {
|
|
154
|
+
throw new ConfigError(`Error: \`workspace ${command}\` accepts at most one positional argument.`);
|
|
155
|
+
}
|
|
156
|
+
return positionals[0];
|
|
157
|
+
}
|
|
158
|
+
function takeRequiredPositional(positionals, command, label) {
|
|
159
|
+
if (positionals.length !== 1) {
|
|
160
|
+
throw new ConfigError(`Error: \`workspace ${command}\` requires exactly one ${label}.`);
|
|
161
|
+
}
|
|
162
|
+
return positionals[0];
|
|
163
|
+
}
|
|
164
|
+
function rejectPositionals(positionals, command) {
|
|
165
|
+
if (positionals.length > 0) {
|
|
166
|
+
throw new ConfigError(`Error: \`workspace ${command}\` takes no positional arguments.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export function printWorkspaceHelp() {
|
|
170
|
+
console.log([
|
|
171
|
+
"Usage: workspace <command> [options]",
|
|
172
|
+
"",
|
|
173
|
+
"Manage worktree lifecycle: creation, local environment setup, and removal.",
|
|
174
|
+
"",
|
|
175
|
+
"Commands:",
|
|
176
|
+
" setup [<branch>] [-c|--new-branch] [--owner <name>] [-s|--slot <port>] [--force] [--wait]",
|
|
177
|
+
" Set up the local environment. With <branch>, create a sibling worktree for it",
|
|
178
|
+
" (add -c to create the branch first). Without, set up the current worktree",
|
|
179
|
+
" (idempotent; bootstrap and retry path).",
|
|
180
|
+
" remove [<branch>] [--no-remote-check]",
|
|
181
|
+
" Remove a worktree by branch, or the current one when omitted.",
|
|
182
|
+
" list",
|
|
183
|
+
" List all registered worktrees (slot, status, branch, path, owner, created).",
|
|
184
|
+
" info [-s|--slot <port>]",
|
|
185
|
+
" Print a worktree summary (ports, branch, readiness).",
|
|
186
|
+
" wait [-s|--slot <port>]",
|
|
187
|
+
" Block until the background finalize reaches READY (exit 0) or FAILED (exit 1).",
|
|
188
|
+
" set-owner <name>",
|
|
189
|
+
" Update the current linked worktree's slot owner (no rebuild).",
|
|
190
|
+
"",
|
|
191
|
+
"Global options:",
|
|
192
|
+
" -v, --verbose Show intermediate output.",
|
|
193
|
+
].join("\n"));
|
|
194
|
+
}
|
|
195
|
+
export function parseDevServerArgs(argv) {
|
|
196
|
+
return parseOptions(argv, DEV_SERVER_OPTIONS);
|
|
197
|
+
}
|
|
198
|
+
export function printDevServerHelp() {
|
|
199
|
+
console.log(formatHelp("dev-server [options]", "Start, stop, or list background dev-server processes.", DEV_SERVER_OPTIONS));
|
|
200
|
+
}
|
|
201
|
+
export function validateDevServerFlags(args) {
|
|
202
|
+
if (args.all && !args.stop) {
|
|
203
|
+
throw new ConfigError("Error: --all requires --stop.");
|
|
204
|
+
}
|
|
205
|
+
if (args.list && (args.stop || args.all)) {
|
|
206
|
+
throw new ConfigError("Error: --list is mutually exclusive with --stop and --all.");
|
|
207
|
+
}
|
|
208
|
+
if (args.evict && (args.stop || args.list || args.all)) {
|
|
209
|
+
const conflict = args.stop ? "--stop" : args.list ? "--list" : "--all";
|
|
210
|
+
throw new ConfigError(`Error: --evict cannot be combined with ${conflict}.`);
|
|
211
|
+
}
|
|
212
|
+
if (args.restart && (args.stop || args.list || args.all)) {
|
|
213
|
+
const conflict = args.stop ? "--stop" : args.list ? "--list" : "--all";
|
|
214
|
+
throw new ConfigError(`Error: --restart cannot be combined with ${conflict}.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function parseOptions(argv, options) {
|
|
218
|
+
const cfg = { options: options, strict: true };
|
|
219
|
+
if (argv)
|
|
220
|
+
cfg.args = argv;
|
|
221
|
+
const { values } = parseArgs(cfg);
|
|
222
|
+
return values;
|
|
223
|
+
}
|
|
224
|
+
function formatHelp(usage, intro, options) {
|
|
225
|
+
const lines = [`Usage: ${usage}`, "", intro, ""];
|
|
226
|
+
for (const [name, opt] of Object.entries(options)) {
|
|
227
|
+
if (opt.description === "")
|
|
228
|
+
continue;
|
|
229
|
+
const shortFlag = opt.short ? `-${opt.short}, ` : "";
|
|
230
|
+
const argSuffix = opt.arg ? ` <${opt.arg}>` : "";
|
|
231
|
+
const flag = `${shortFlag}--${name}${argSuffix}`;
|
|
232
|
+
lines.push(` ${flag.padEnd(28)} ${opt.description}`);
|
|
233
|
+
}
|
|
234
|
+
return lines.join("\n");
|
|
235
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { CallbackServer, ServerContext, ServerDescriptor, SpawnServer } from "./server-descriptor.js";
|
|
2
|
+
import { type ResolvedSlot, type SlotEntry } from "./slots.js";
|
|
3
|
+
export type { CallbackServer, ServerContext, ServerDescriptor, SpawnServer };
|
|
4
|
+
/** Configuration accepted by {@link runDevServer}. */
|
|
5
|
+
export interface DevServerConfig {
|
|
6
|
+
/** Anchor port for the slot range. Used to synthesize the main worktree's slot. */
|
|
7
|
+
basePort: number;
|
|
8
|
+
/** Per-worktree runtime directory, relative to the worktree root (e.g. `.local-wt`). */
|
|
9
|
+
runtimeDir: string;
|
|
10
|
+
/**
|
|
11
|
+
* Shared registry directory, relative to a worktree root (e.g. `.local/wt-registry`).
|
|
12
|
+
* Holds `slots.json` and `dev-servers.json`. Must resolve to the same physical directory
|
|
13
|
+
* across linked worktrees — typically via a symlink (e.g. `.local`).
|
|
14
|
+
*/
|
|
15
|
+
registryDir: string;
|
|
16
|
+
/** Maximum concurrent dev-servers across all worktrees. Omit for no limit. */
|
|
17
|
+
devLimit?: number;
|
|
18
|
+
/** One entry per server to start. Started in array order; stopped in reverse order. */
|
|
19
|
+
servers: ServerDescriptor[];
|
|
20
|
+
/** Builds the post-start summary printed to stdout. Defaults to a generic layout. */
|
|
21
|
+
printSummary?: (ctx: DevServerSummaryContext) => string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Context passed to {@link DevServerConfig.printSummary}. `port` and `pid` are present only for
|
|
25
|
+
* `kind: "spawn"` servers; callback servers expose neither.
|
|
26
|
+
*/
|
|
27
|
+
export interface DevServerSummaryContext {
|
|
28
|
+
slot: ResolvedSlot;
|
|
29
|
+
servers: {
|
|
30
|
+
server: ServerDescriptor;
|
|
31
|
+
port?: number;
|
|
32
|
+
pid?: number;
|
|
33
|
+
}[];
|
|
34
|
+
}
|
|
35
|
+
export declare function runDevServer(config: DevServerConfig): Promise<void>;
|
|
36
|
+
export type WorktreeReadyCheck = {
|
|
37
|
+
ok: true;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
message: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Pure builder for the `dev:up` worktree-readiness gate. Returns `ok` when the slot is `ready`
|
|
44
|
+
* or absent (synthesized main); otherwise returns the user-facing error message.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildWorktreeReadyMessage(input: {
|
|
47
|
+
slotPort: number;
|
|
48
|
+
worktreePath: string;
|
|
49
|
+
runtimeDir: string;
|
|
50
|
+
entry: SlotEntry | undefined;
|
|
51
|
+
now: number;
|
|
52
|
+
}): WorktreeReadyCheck;
|