@paleo/workspace 0.21.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/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;
@@ -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<void> | void;
57
+ finalizeWorktree: (ctx: SetupContext) => Promise<FinalizeResult | undefined> | FinalizeResult | undefined;
54
58
  /**
55
- * Destructive infrastructure teardown on `workspace remove` (e.g. `docker compose down -v` to
56
- * wipe volumes). Runs after the dev-server stop. Best-effort; errors should be swallowed.
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();
@@ -223,8 +223,8 @@ async function runFinalize(command, config, registryDir) {
223
223
  verbose: false,
224
224
  };
225
225
  try {
226
- await config.finalizeWorktree(setupContext);
227
- markSlotReady(ctx.mainWorktree, registryDir, slot);
226
+ const result = await config.finalizeWorktree(setupContext);
227
+ markSlotReady(ctx.mainWorktree, registryDir, slot, result?.extra);
228
228
  appendLog("============================================================");
229
229
  appendLog(`READY: branch ${branch} (slot ${slot})`);
230
230
  appendLog("============================================================");
@@ -399,7 +399,7 @@ function hintLiveOrphans(liveOrphans) {
399
399
  console.log(`\nNote: ${liveOrphans.length} workspace(s) have a deleted worktree but a still-running ` +
400
400
  "dev-server. Run `workspace prune` to stop them and clean up.");
401
401
  }
402
- async function runPrune(registryDir) {
402
+ async function runPrune(config, registryDir, verbose) {
403
403
  const ctx = detectWorktree();
404
404
  const registry = readSlots(ctx.mainWorktree, registryDir);
405
405
  const orphanPorts = findOrphanPorts(registry);
@@ -407,6 +407,13 @@ async function runPrune(registryDir) {
407
407
  for (const port of orphanPorts) {
408
408
  const entry = registry.slots[port];
409
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
+ });
410
417
  delete registry.slots[port];
411
418
  const ownerSuffix = entry.owner ? `, owner ${entry.owner}` : "";
412
419
  console.log(`Pruned slot ${port} (${entry.worktree}${ownerSuffix}).`);
@@ -419,16 +426,18 @@ async function runPrune(registryDir) {
419
426
  return;
420
427
  }
421
428
  console.log(`Pruned ${orphanPorts.length} orphaned workspace(s).`);
422
- if (stoppedProcesses > 0) {
423
- console.log(`Stopped ${stoppedProcesses} orphaned process(es). Note: infrastructure managed by callback ` +
424
- "servers (e.g. `docker compose`) is not torn down automatically — check for leftover containers.");
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.");
425
434
  }
426
435
  }
427
436
  /**
428
437
  * Stop a gone worktree's dev-server the only way left: its dir (and `dev-server.mjs`) is deleted, so
429
438
  * we can't shell out to `dev down` to run callback stop() — we kill the recorded spawn PIDs directly
430
- * and drop the dev-server entry. Returns the count of live PIDs stopped. Callback-managed infra is
431
- * not torn down (see `runPrune`'s caveat).
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.
432
441
  */
433
442
  async function stopOrphanedDevServer(mainWorktree, registryDir, worktree) {
434
443
  const devEntry = findOwnEntry(mainWorktree, registryDir, worktree);
@@ -509,6 +518,13 @@ async function handleRemove(command, ctx, run, config, registryDir) {
509
518
  if (!existsSync(target.worktreePath)) {
510
519
  console.warn(`Warning: Worktree directory ${target.worktreePath} not found. Cleaning up registry only.`);
511
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
+ });
512
528
  delete registry.slots[target.slotPort];
513
529
  writeSlots(ctx.mainWorktree, registryDir, registry);
514
530
  pruneGitWorktrees(ctx.mainWorktree);
@@ -526,13 +542,13 @@ async function handleRemove(command, ctx, run, config, registryDir) {
526
542
  else {
527
543
  verboseLog(`No dev-server running in ${target.worktreePath}; skipping stop.`);
528
544
  }
529
- if (config.purgeInfrastructure) {
530
- await config.purgeInfrastructure({
531
- worktree: target.worktreePath,
532
- mainWorktree: ctx.mainWorktree,
533
- verbose: run.verbose,
534
- });
535
- }
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
+ });
536
552
  delete registry.slots[target.slotPort];
537
553
  writeSlots(ctx.mainWorktree, registryDir, registry);
538
554
  removeDevServerEntryByWorktree(ctx.mainWorktree, registryDir, target.worktreePath);
@@ -546,6 +562,10 @@ async function handleRemove(command, ctx, run, config, registryDir) {
546
562
  console.log(`Now run: cd ${ctx.mainWorktree}`);
547
563
  }
548
564
  }
565
+ async function runPurgeInfrastructure(config, ctx) {
566
+ if (config.purgeInfrastructure)
567
+ await config.purgeInfrastructure(ctx);
568
+ }
549
569
  function handleSetOwnerMode(command, ctx, registryDir) {
550
570
  const newOwner = command.name;
551
571
  const { slotPort } = handleSetOwner({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paleo/workspace",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Run multiple git-worktree dev environments side by side.",
5
5
  "keywords": [
6
6
  "workspace",