@paleo/workspace 0.20.0 → 0.22.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 +2 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +9 -1
- package/dist/index.d.ts +1 -1
- package/dist/slots.d.ts +4 -1
- package/dist/slots.js +7 -1
- package/dist/workspace.d.ts +20 -3
- package/dist/workspace.js +54 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,8 @@ The agent reads the skill, adapts the reference scripts to your stack, installs
|
|
|
26
26
|
## Workflow
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
|
-
npm run workspace -- setup feat/42 -c
|
|
29
|
+
npm run workspace -- setup feat/42 -c # new branch + worktree + isolated env
|
|
30
|
+
npm run workspace -- setup feat/42 -c --go # …then drop into a shell there (exit to return)
|
|
30
31
|
npm run dev # foreground: stream logs, CTRL+C stops; attaches if already running
|
|
31
32
|
npm run dev -- up # start in the background (no-op if already running here)
|
|
32
33
|
npm run dev -- up --restart # stop the dev-server in this worktree if running, then start fresh
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -53,6 +53,7 @@ function parseSetup(tokens) {
|
|
|
53
53
|
slot: { type: "string", short: "s" },
|
|
54
54
|
force: { type: "boolean" },
|
|
55
55
|
wait: { type: "boolean" },
|
|
56
|
+
go: { type: "boolean" },
|
|
56
57
|
verbose: { type: "boolean", short: "v" },
|
|
57
58
|
},
|
|
58
59
|
allowPositionals: true,
|
|
@@ -60,12 +61,16 @@ function parseSetup(tokens) {
|
|
|
60
61
|
});
|
|
61
62
|
const branch = takeOptionalPositional(positionals, "setup");
|
|
62
63
|
const newBranch = values["new-branch"] ?? false;
|
|
64
|
+
const go = values.go ?? false;
|
|
63
65
|
if (newBranch && branch === undefined) {
|
|
64
66
|
throw new ConfigError("`workspace setup <branch> -c` requires a branch name.");
|
|
65
67
|
}
|
|
66
68
|
if (values.from !== undefined && !newBranch) {
|
|
67
69
|
throw new ConfigError("`--from` requires `-c`/`--new-branch`.");
|
|
68
70
|
}
|
|
71
|
+
if (go && branch === undefined) {
|
|
72
|
+
throw new ConfigError("`--go` requires a branch (the worktree to enter).");
|
|
73
|
+
}
|
|
69
74
|
return {
|
|
70
75
|
command: {
|
|
71
76
|
kind: "setup",
|
|
@@ -76,6 +81,7 @@ function parseSetup(tokens) {
|
|
|
76
81
|
slot: values.slot,
|
|
77
82
|
force: values.force ?? false,
|
|
78
83
|
wait: values.wait ?? false,
|
|
84
|
+
go,
|
|
79
85
|
},
|
|
80
86
|
verbose: values.verbose ?? false,
|
|
81
87
|
};
|
|
@@ -196,12 +202,14 @@ export function printWorkspaceHelp() {
|
|
|
196
202
|
"Manage workspaces: a git worktree plus its own dev setup (ports, config, database, dev server).",
|
|
197
203
|
"",
|
|
198
204
|
"Commands:",
|
|
199
|
-
" setup [<branch>] [-c|--new-branch] [--from <ref>] [--owner <name>] [-s|--slot <port>] [--force] [--wait]",
|
|
205
|
+
" setup [<branch>] [-c|--new-branch] [--from <ref>] [--owner <name>] [-s|--slot <port>] [--force] [--wait] [--go]",
|
|
200
206
|
" Set up the workspace. With <branch>, create a sibling worktree for it",
|
|
201
207
|
" (add -c to create the branch first). Without, set up the current worktree",
|
|
202
208
|
" (idempotent; bootstrap and retry path).",
|
|
203
209
|
" With -c, the new branch starts at the current worktree's HEAD, or at <ref> with --from.",
|
|
204
210
|
" Finalize runs in the background; add --wait to block until it reaches READY.",
|
|
211
|
+
" With --go, drop into an interactive shell in the new worktree (exit to return);",
|
|
212
|
+
" combine with --wait to enter only once it is READY. Requires a branch and $SHELL.",
|
|
205
213
|
" remove [<branch>] [--force]",
|
|
206
214
|
" Remove a workspace by branch, or the current one when omitted.",
|
|
207
215
|
" Refuses on uncommitted changes unless --force.",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { runWorkspace } from "./workspace.js";
|
|
2
2
|
export { defaultWorktreeDirName } from "./worktree.js";
|
|
3
3
|
export type { WorktreeDirNameFn } from "./worktree.js";
|
|
4
|
-
export type { WorkspaceConfig, SetupContext, SummaryContext, PatchContext, ConfigFileEntry, ConfigFileSource, ConfigFileSourceSpec, PurgeContext, } from "./workspace.js";
|
|
4
|
+
export type { WorkspaceConfig, SetupContext, FinalizeResult, SummaryContext, PatchContext, ConfigFileEntry, ConfigFileSource, ConfigFileSourceSpec, PurgeContext, } from "./workspace.js";
|
|
5
5
|
export { runDevServer } from "./dev-server.js";
|
|
6
6
|
export type { DevServerConfig, DevServerSummaryContext, ServerDescriptor, ServerContext, SpawnServer, CallbackServer, } from "./dev-server.js";
|
|
7
7
|
export type { ResolvedSlot } from "./slots.js";
|
package/dist/slots.d.ts
CHANGED
|
@@ -25,6 +25,9 @@ export interface SlotEntry {
|
|
|
25
25
|
};
|
|
26
26
|
/** `true` for the main-worktree entry. Absent on linked entries. */
|
|
27
27
|
main?: boolean;
|
|
28
|
+
/** Opaque blob the consumer returns from `finalizeWorktree`, handed back to `purgeInfrastructure`
|
|
29
|
+
* so an orphan's infrastructure can be torn down by name after its worktree (and config) is gone. */
|
|
30
|
+
extra?: unknown;
|
|
28
31
|
}
|
|
29
32
|
export interface SlotsRegistry {
|
|
30
33
|
slots: Record<string, SlotEntry>;
|
|
@@ -50,7 +53,7 @@ export declare function resolveAndRegisterSlot(input: RegisterSlotInput): {
|
|
|
50
53
|
owner: string | undefined;
|
|
51
54
|
status: SlotStatus;
|
|
52
55
|
};
|
|
53
|
-
export declare function markSlotReady(mainWorktree: string, registryDir: string, slotPort: number): void;
|
|
56
|
+
export declare function markSlotReady(mainWorktree: string, registryDir: string, slotPort: number, extra?: unknown): void;
|
|
54
57
|
export declare function markSlotFailed(mainWorktree: string, registryDir: string, slotPort: number, message: string): void;
|
|
55
58
|
export declare function validateSlotAvailability(slotArg: string | undefined, ctx: {
|
|
56
59
|
currentWorktree: string;
|
package/dist/slots.js
CHANGED
|
@@ -50,17 +50,21 @@ export function resolveAndRegisterSlot(input) {
|
|
|
50
50
|
entry.main = true;
|
|
51
51
|
if (owner !== undefined)
|
|
52
52
|
entry.owner = owner;
|
|
53
|
+
if (existing?.extra !== undefined)
|
|
54
|
+
entry.extra = existing.extra;
|
|
53
55
|
registry.slots[String(port)] = entry;
|
|
54
56
|
writeSlots(input.mainWorktree, input.registryDir, registry);
|
|
55
57
|
return { port, owner, status };
|
|
56
58
|
}
|
|
57
|
-
export function markSlotReady(mainWorktree, registryDir, slotPort) {
|
|
59
|
+
export function markSlotReady(mainWorktree, registryDir, slotPort, extra) {
|
|
58
60
|
const registry = readSlots(mainWorktree, registryDir);
|
|
59
61
|
const entry = registry.slots[String(slotPort)];
|
|
60
62
|
if (!entry)
|
|
61
63
|
return;
|
|
62
64
|
entry.status = "ready";
|
|
63
65
|
delete entry.failure;
|
|
66
|
+
if (extra !== undefined)
|
|
67
|
+
entry.extra = extra;
|
|
64
68
|
writeSlots(mainWorktree, registryDir, registry);
|
|
65
69
|
}
|
|
66
70
|
export function markSlotFailed(mainWorktree, registryDir, slotPort, message) {
|
|
@@ -116,6 +120,8 @@ export function handleSetOwner(input) {
|
|
|
116
120
|
};
|
|
117
121
|
if (slotData.failure)
|
|
118
122
|
updated.failure = slotData.failure;
|
|
123
|
+
if (slotData.extra !== undefined)
|
|
124
|
+
updated.extra = slotData.extra;
|
|
119
125
|
if (input.newOwner !== undefined)
|
|
120
126
|
updated.owner = input.newOwner;
|
|
121
127
|
registry.slots[slotPort] = updated;
|
package/dist/workspace.d.ts
CHANGED
|
@@ -49,11 +49,19 @@ export interface WorkspaceConfig {
|
|
|
49
49
|
*
|
|
50
50
|
* Runs in a detached child whose stdout/stderr are already redirected to
|
|
51
51
|
* `<runtimeDir>/logs/workspace-setup.log`. `console.log` and child-process `stdio: "inherit"` land there.
|
|
52
|
+
*
|
|
53
|
+
* May return `{ extra }` — an opaque blob persisted on the slot entry and handed back to
|
|
54
|
+
* {@link purgeInfrastructure}. Use it to record what infrastructure to tear down by name (e.g.
|
|
55
|
+
* container and volume names) so an orphaned worktree can still be cleaned up after its config is gone.
|
|
52
56
|
*/
|
|
53
|
-
finalizeWorktree: (ctx: SetupContext) => Promise<
|
|
57
|
+
finalizeWorktree: (ctx: SetupContext) => Promise<FinalizeResult | undefined> | FinalizeResult | undefined;
|
|
54
58
|
/**
|
|
55
|
-
* Destructive infrastructure teardown
|
|
56
|
-
*
|
|
59
|
+
* Destructive infrastructure teardown (e.g. `docker compose down -v` to wipe volumes). Runs after
|
|
60
|
+
* the dev-server stop on `workspace remove`, and on `workspace prune` / removing an orphaned
|
|
61
|
+
* worktree. MUST be idempotent and tolerate already-absent infrastructure: it may run when the
|
|
62
|
+
* worktree directory is gone (`ctx.extra` carries the recorded teardown identifiers; `ctx.worktree`
|
|
63
|
+
* no longer exists), so branch on the worktree's presence and tear down by name in that case.
|
|
64
|
+
* Best-effort; errors should be swallowed.
|
|
57
65
|
*/
|
|
58
66
|
purgeInfrastructure?: (ctx: PurgeContext) => Promise<void> | void;
|
|
59
67
|
/** Builds the post-setup summary printed to stdout. */
|
|
@@ -77,6 +85,10 @@ export interface PreSetupContext {
|
|
|
77
85
|
/** Writes to stdout and the setup log. */
|
|
78
86
|
log: (msg: string) => void;
|
|
79
87
|
}
|
|
88
|
+
/** Return value of {@link WorkspaceConfig.finalizeWorktree}. */
|
|
89
|
+
export interface FinalizeResult {
|
|
90
|
+
extra: unknown;
|
|
91
|
+
}
|
|
80
92
|
/** Context passed to {@link WorkspaceConfig.finalizeWorktree}. */
|
|
81
93
|
export interface SetupContext {
|
|
82
94
|
currentWorktree: string;
|
|
@@ -111,8 +123,13 @@ export interface SummaryContext {
|
|
|
111
123
|
}
|
|
112
124
|
/** Context passed to {@link WorkspaceConfig.purgeInfrastructure}. */
|
|
113
125
|
export interface PurgeContext {
|
|
126
|
+
/** The target worktree. May no longer exist on disk when purging an orphan — check before
|
|
127
|
+
* running cwd-bound commands; tear down by name (from {@link extra}) in that case. */
|
|
114
128
|
worktree: string;
|
|
115
129
|
mainWorktree: string;
|
|
130
|
+
slot: number;
|
|
131
|
+
/** The blob the consumer returned from `finalizeWorktree`, if any. */
|
|
132
|
+
extra?: unknown;
|
|
116
133
|
verbose: boolean;
|
|
117
134
|
}
|
|
118
135
|
/** A `{ path }` (relative to the main worktree) or `{ content }` (verbatim) initial source. */
|
package/dist/workspace.js
CHANGED
|
@@ -58,7 +58,7 @@ export async function runWorkspace(config) {
|
|
|
58
58
|
runList(registryDir);
|
|
59
59
|
return;
|
|
60
60
|
case "prune":
|
|
61
|
-
await runPrune(registryDir);
|
|
61
|
+
await runPrune(config, registryDir, verbose);
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
64
|
const ctx = detectWorktree();
|
|
@@ -71,13 +71,29 @@ export async function runWorkspace(config) {
|
|
|
71
71
|
handleSetOwnerMode(command, ctx, registryDir);
|
|
72
72
|
return;
|
|
73
73
|
case "setup": {
|
|
74
|
-
const { slot } = await runSetup(command, ctx, run, config, registryDir);
|
|
74
|
+
const { slot, worktree } = await runSetup(command, ctx, run, config, registryDir);
|
|
75
75
|
if (command.wait)
|
|
76
76
|
await waitForSlot(slot, config, registryDir, { printSummary: false });
|
|
77
|
+
if (command.go)
|
|
78
|
+
enterWorktree(worktree);
|
|
77
79
|
return;
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* `--go`: open an interactive shell in the freshly set-up worktree (exit to return). Falls back to
|
|
85
|
+
* printing a `cd` hint when there is no `$SHELL` or stdin is not a tty (scripts, pipes) — dropping
|
|
86
|
+
* into an interactive shell there would hang.
|
|
87
|
+
*/
|
|
88
|
+
function enterWorktree(worktree) {
|
|
89
|
+
const shell = process.env.SHELL;
|
|
90
|
+
if (shell === undefined || !process.stdin.isTTY) {
|
|
91
|
+
console.log(`Now run: cd ${worktree}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
console.error(`Entering ${worktree} (exit to return).`);
|
|
95
|
+
spawnSync(shell, [], { cwd: worktree, stdio: "inherit" });
|
|
96
|
+
}
|
|
81
97
|
async function runSetup(command, ctx, run, config, registryDir) {
|
|
82
98
|
const scheme = resolvePortScheme(config);
|
|
83
99
|
const portsFn = resolvePortsFn(config);
|
|
@@ -158,7 +174,7 @@ async function runSetup(command, ctx, run, config, registryDir) {
|
|
|
158
174
|
});
|
|
159
175
|
child.unref();
|
|
160
176
|
closeSync(logFd);
|
|
161
|
-
return { slot };
|
|
177
|
+
return { slot, worktree: setupCtx.currentWorktree };
|
|
162
178
|
}
|
|
163
179
|
function refuseIfFinalizePending(ctx, registryDir, force) {
|
|
164
180
|
if (force)
|
|
@@ -207,8 +223,8 @@ async function runFinalize(command, config, registryDir) {
|
|
|
207
223
|
verbose: false,
|
|
208
224
|
};
|
|
209
225
|
try {
|
|
210
|
-
await config.finalizeWorktree(setupContext);
|
|
211
|
-
markSlotReady(ctx.mainWorktree, registryDir, slot);
|
|
226
|
+
const result = await config.finalizeWorktree(setupContext);
|
|
227
|
+
markSlotReady(ctx.mainWorktree, registryDir, slot, result?.extra);
|
|
212
228
|
appendLog("============================================================");
|
|
213
229
|
appendLog(`READY: branch ${branch} (slot ${slot})`);
|
|
214
230
|
appendLog("============================================================");
|
|
@@ -383,7 +399,7 @@ function hintLiveOrphans(liveOrphans) {
|
|
|
383
399
|
console.log(`\nNote: ${liveOrphans.length} workspace(s) have a deleted worktree but a still-running ` +
|
|
384
400
|
"dev-server. Run `workspace prune` to stop them and clean up.");
|
|
385
401
|
}
|
|
386
|
-
async function runPrune(registryDir) {
|
|
402
|
+
async function runPrune(config, registryDir, verbose) {
|
|
387
403
|
const ctx = detectWorktree();
|
|
388
404
|
const registry = readSlots(ctx.mainWorktree, registryDir);
|
|
389
405
|
const orphanPorts = findOrphanPorts(registry);
|
|
@@ -391,6 +407,13 @@ async function runPrune(registryDir) {
|
|
|
391
407
|
for (const port of orphanPorts) {
|
|
392
408
|
const entry = registry.slots[port];
|
|
393
409
|
stoppedProcesses += await stopOrphanedDevServer(ctx.mainWorktree, registryDir, entry.worktree);
|
|
410
|
+
await runPurgeInfrastructure(config, {
|
|
411
|
+
worktree: entry.worktree,
|
|
412
|
+
mainWorktree: ctx.mainWorktree,
|
|
413
|
+
slot: Number(port),
|
|
414
|
+
extra: entry.extra,
|
|
415
|
+
verbose,
|
|
416
|
+
});
|
|
394
417
|
delete registry.slots[port];
|
|
395
418
|
const ownerSuffix = entry.owner ? `, owner ${entry.owner}` : "";
|
|
396
419
|
console.log(`Pruned slot ${port} (${entry.worktree}${ownerSuffix}).`);
|
|
@@ -403,16 +426,18 @@ async function runPrune(registryDir) {
|
|
|
403
426
|
return;
|
|
404
427
|
}
|
|
405
428
|
console.log(`Pruned ${orphanPorts.length} orphaned workspace(s).`);
|
|
406
|
-
if (stoppedProcesses > 0)
|
|
407
|
-
console.log(`Stopped ${stoppedProcesses} orphaned process(es)
|
|
408
|
-
|
|
429
|
+
if (stoppedProcesses > 0)
|
|
430
|
+
console.log(`Stopped ${stoppedProcesses} orphaned process(es).`);
|
|
431
|
+
if (config.purgeInfrastructure === undefined) {
|
|
432
|
+
console.log("Note: infrastructure managed by callback servers (e.g. `docker compose`) is not torn down " +
|
|
433
|
+
"automatically — check for leftover containers.");
|
|
409
434
|
}
|
|
410
435
|
}
|
|
411
436
|
/**
|
|
412
437
|
* Stop a gone worktree's dev-server the only way left: its dir (and `dev-server.mjs`) is deleted, so
|
|
413
438
|
* we can't shell out to `dev down` to run callback stop() — we kill the recorded spawn PIDs directly
|
|
414
|
-
* and drop the dev-server entry. Returns the count of live PIDs stopped.
|
|
415
|
-
*
|
|
439
|
+
* and drop the dev-server entry. Returns the count of live PIDs stopped. The caller separately runs
|
|
440
|
+
* `purgeInfrastructure` (by name, from the slot's `extra`) to tear down callback-managed infra.
|
|
416
441
|
*/
|
|
417
442
|
async function stopOrphanedDevServer(mainWorktree, registryDir, worktree) {
|
|
418
443
|
const devEntry = findOwnEntry(mainWorktree, registryDir, worktree);
|
|
@@ -493,6 +518,13 @@ async function handleRemove(command, ctx, run, config, registryDir) {
|
|
|
493
518
|
if (!existsSync(target.worktreePath)) {
|
|
494
519
|
console.warn(`Warning: Worktree directory ${target.worktreePath} not found. Cleaning up registry only.`);
|
|
495
520
|
await stopOrphanedDevServer(ctx.mainWorktree, registryDir, target.worktreePath);
|
|
521
|
+
await runPurgeInfrastructure(config, {
|
|
522
|
+
worktree: target.worktreePath,
|
|
523
|
+
mainWorktree: ctx.mainWorktree,
|
|
524
|
+
slot: Number(target.slotPort),
|
|
525
|
+
extra: registry.slots[target.slotPort]?.extra,
|
|
526
|
+
verbose: run.verbose,
|
|
527
|
+
});
|
|
496
528
|
delete registry.slots[target.slotPort];
|
|
497
529
|
writeSlots(ctx.mainWorktree, registryDir, registry);
|
|
498
530
|
pruneGitWorktrees(ctx.mainWorktree);
|
|
@@ -510,13 +542,13 @@ async function handleRemove(command, ctx, run, config, registryDir) {
|
|
|
510
542
|
else {
|
|
511
543
|
verboseLog(`No dev-server running in ${target.worktreePath}; skipping stop.`);
|
|
512
544
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
545
|
+
await runPurgeInfrastructure(config, {
|
|
546
|
+
worktree: target.worktreePath,
|
|
547
|
+
mainWorktree: ctx.mainWorktree,
|
|
548
|
+
slot: Number(target.slotPort),
|
|
549
|
+
extra: registry.slots[target.slotPort]?.extra,
|
|
550
|
+
verbose: run.verbose,
|
|
551
|
+
});
|
|
520
552
|
delete registry.slots[target.slotPort];
|
|
521
553
|
writeSlots(ctx.mainWorktree, registryDir, registry);
|
|
522
554
|
removeDevServerEntryByWorktree(ctx.mainWorktree, registryDir, target.worktreePath);
|
|
@@ -530,6 +562,10 @@ async function handleRemove(command, ctx, run, config, registryDir) {
|
|
|
530
562
|
console.log(`Now run: cd ${ctx.mainWorktree}`);
|
|
531
563
|
}
|
|
532
564
|
}
|
|
565
|
+
async function runPurgeInfrastructure(config, ctx) {
|
|
566
|
+
if (config.purgeInfrastructure)
|
|
567
|
+
await config.purgeInfrastructure(ctx);
|
|
568
|
+
}
|
|
533
569
|
function handleSetOwnerMode(command, ctx, registryDir) {
|
|
534
570
|
const newOwner = command.name;
|
|
535
571
|
const { slotPort } = handleSetOwner({
|