@paleo/workspace 0.15.1 → 0.16.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 CHANGED
@@ -27,7 +27,7 @@ The agent reads the skill, adapts the reference scripts to your stack, installs
27
27
 
28
28
  ```sh
29
29
  npm run workspace -- setup feat/42 -c # new branch + worktree + isolated env
30
- npm run dev # foreground: stream logs from startup, CTRL+C stops; attaches if already running (CTRL+C then detaches)
30
+ npm run dev # foreground: stream logs, CTRL+C stops; attaches if already running
31
31
  npm run dev -- up # start in the background (no-op if already running here)
32
32
  npm run dev -- up --restart # stop the dev-server in this worktree if running, then start fresh
33
33
  npm run dev -- up --evict # if devLimit is reached, evict the oldest dev-server and start
package/dist/cli.js CHANGED
@@ -287,7 +287,7 @@ export function printDevHelp() {
287
287
  "",
288
288
  "Commands:",
289
289
  " dev Start in the foreground, streaming logs from startup; CTRL+C stops it.",
290
- " If one is already running here, attach to its logs instead",
290
+ " If one is already running here, attach to its logs instead.",
291
291
  " dev up Start in the background and return once ready.",
292
292
  " dev restart Stop this worktree's dev-server if running, then start in the background.",
293
293
  " dev down [--all] Stop this worktree's dev-server, or every dev-server with --all.",
@@ -1,3 +1,4 @@
1
+ import { StartupError } from "./errors.js";
1
2
  import type { CallbackServer, ServerContext, ServerDescriptor, SpawnServer } from "./server-descriptor.js";
2
3
  import { type ResolvedSlot, type SlotEntry } from "./slots.js";
3
4
  export type { CallbackServer, ServerContext, ServerDescriptor, SpawnServer };
@@ -33,6 +34,12 @@ export interface DevServerSummaryContext {
33
34
  }[];
34
35
  }
35
36
  export declare function runDevServer(config: DevServerConfig): Promise<void>;
37
+ /**
38
+ * Wraps an error thrown by a callback server's `start()` into a {@link StartupError}, so a failing
39
+ * callback (e.g. `docker compose up -d`) gets the same clean rollback-and-report path as a spawn
40
+ * server instead of surfacing as a raw unhandled-rejection stack trace.
41
+ */
42
+ export declare function toCallbackStartupError(name: string, err: unknown): StartupError;
36
43
  /**
37
44
  * Poll the foreground's own spawn PIDs; when none remain alive, the servers were stopped from
38
45
  * outside this process (`dev down`, `down --all`, eviction, or a manual kill). Fires `onStopped`
@@ -207,6 +207,16 @@ async function spawnWithRollback(config, ctx, state, isAborted = () => false, on
207
207
  }
208
208
  return !isAborted();
209
209
  }
210
+ /**
211
+ * Wraps an error thrown by a callback server's `start()` into a {@link StartupError}, so a failing
212
+ * callback (e.g. `docker compose up -d`) gets the same clean rollback-and-report path as a spawn
213
+ * server instead of surfacing as a raw unhandled-rejection stack trace.
214
+ */
215
+ export function toCallbackStartupError(name, err) {
216
+ if (err instanceof StartupError)
217
+ return err;
218
+ return new StartupError(name, err instanceof Error ? err.message : String(err));
219
+ }
210
220
  async function spawnAndAwait(config, ctx, state, onSpawned) {
211
221
  for (const server of config.servers) {
212
222
  console.log(`Starting ${server.name} dev server...`);
@@ -214,7 +224,12 @@ async function spawnAndAwait(config, ctx, state, onSpawned) {
214
224
  state.spawnPids[server.name] = spawnServer(server, config.runtimeDir, ctx.cwd);
215
225
  }
216
226
  else {
217
- await server.start(ctx);
227
+ try {
228
+ await server.start(ctx);
229
+ }
230
+ catch (err) {
231
+ throw toCallbackStartupError(server.name, err);
232
+ }
218
233
  state.startedCallbacks.push(server);
219
234
  }
220
235
  }
package/dist/helpers.d.ts CHANGED
@@ -14,10 +14,15 @@ export declare function readPortFromEnvFile(file: string, varName: string): numb
14
14
  export declare function readPortFromJsonFile(file: string, jsonPath: string): number;
15
15
  export interface CopyAndPatchCtx {
16
16
  currentWorktree: string;
17
- mainWorktree: string;
18
17
  log: (msg: string) => void;
19
18
  }
20
- export declare function copyAndPatchFile(ctx: CopyAndPatchCtx, relPath: string, patchFn: (content: string) => string, label: string, force: boolean, optional?: boolean): void;
19
+ /** Resolved initial-content source for {@link copyAndPatchFile}. `path` is absolute. */
20
+ export type ResolvedFileSource = {
21
+ path: string;
22
+ } | {
23
+ content: string;
24
+ };
25
+ export declare function copyAndPatchFile(ctx: CopyAndPatchCtx, relPath: string, source: ResolvedFileSource, patchFn: (content: string) => string, label: string, force: boolean, optional?: boolean): void;
21
26
  /**
22
27
  * Formats a millisecond duration as the two largest units among `d`/`h`/`m`/`s`.
23
28
  * Drops the smaller unit when zero (`5d` instead of `5d 0h`). Sub-second values
package/dist/helpers.js CHANGED
@@ -67,24 +67,29 @@ export function readPortFromJsonFile(file, jsonPath) {
67
67
  }
68
68
  return toPort(String(cur), file);
69
69
  }
70
- export function copyAndPatchFile(ctx, relPath, patchFn, label, force, optional = false) {
70
+ export function copyAndPatchFile(ctx, relPath, source, patchFn, label, force, optional = false) {
71
71
  const targetPath = join(ctx.currentWorktree, relPath);
72
- const sourcePath = join(ctx.mainWorktree, relPath);
73
72
  const alreadyExists = existsSync(targetPath);
74
73
  if (alreadyExists && !force) {
75
74
  ctx.log(`Skipped ${label} (already exists; use --force to overwrite).`);
76
75
  return;
77
76
  }
78
- if (!existsSync(sourcePath)) {
79
- if (!optional) {
80
- console.error(`Error: ${relPath} not found in main worktree. Bootstrap the main worktree first ` +
81
- "(`workspace setup`), or mark the entry as optional.");
82
- process.exit(1);
77
+ let content;
78
+ if ("content" in source) {
79
+ content = source.content;
80
+ }
81
+ else {
82
+ if (!existsSync(source.path)) {
83
+ if (!optional) {
84
+ console.error(`Error: source ${source.path} not found. Bootstrap the main worktree first ` +
85
+ "(`workspace setup`), provide a `source`, or mark the entry as optional.");
86
+ process.exit(1);
87
+ }
88
+ ctx.log(`Warning: source ${source.path} not found, skipping (optional).`);
89
+ return;
83
90
  }
84
- ctx.log(`Warning: ${relPath} not found in main worktree, skipping (optional).`);
85
- return;
91
+ content = readFileSync(source.path, "utf-8");
86
92
  }
87
- const content = readFileSync(sourcePath, "utf-8");
88
93
  const patched = patchFn(content);
89
94
  mkdirSync(dirname(targetPath), { recursive: true });
90
95
  writeFileSync(targetPath, patched);
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, PurgeContext, } from "./workspace.js";
4
+ export type { WorkspaceConfig, SetupContext, 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";
@@ -121,16 +121,38 @@ export interface PurgeContext {
121
121
  mainWorktree: string;
122
122
  verbose: boolean;
123
123
  }
124
- /** One config file copied from the main worktree and patched per slot. */
124
+ /** A `{ path }` (relative to the main worktree) or `{ content }` (verbatim) initial source. */
125
+ export type ConfigFileSourceSpec = {
126
+ path: string;
127
+ } | {
128
+ content: string;
129
+ };
130
+ /**
131
+ * Overrides where a {@link ConfigFileEntry}'s initial content comes from. Defaults to reading
132
+ * `entry.path` from the main worktree.
133
+ *
134
+ * - `{ path }` — read this path (relative to the main worktree) instead of `entry.path`,
135
+ * e.g. seed from a committed example: `{ path: "packages/api/.env.local.example" }`.
136
+ * - `{ content }` — use this string as the initial content verbatim.
137
+ * - a callback returning either of the above, resolved per worktree with the same
138
+ * {@link PatchContext} the `patch` callback receives.
139
+ */
140
+ export type ConfigFileSource = ConfigFileSourceSpec | ((ctx: PatchContext) => ConfigFileSourceSpec);
141
+ /** One config file seeded from its source (the main worktree by default) and patched per slot. */
125
142
  export interface ConfigFileEntry {
126
- /** Path relative to the worktree root. Same path is read from main and written to current. */
143
+ /** Path relative to the worktree root. Written to the current worktree. */
127
144
  path: string;
145
+ /**
146
+ * Overrides the initial content's source. Defaults to reading `path` from the main worktree.
147
+ * Use this to seed from a committed example or supplied content instead. See {@link ConfigFileSource}.
148
+ */
149
+ source?: ConfigFileSource;
128
150
  /** Returns the patched content given the source content and the slot's ports. */
129
151
  patch: (content: string, ctx: PatchContext) => string;
130
152
  /**
131
- * When `true`, a missing source on the main worktree logs a warning and skips the entry.
153
+ * When `true`, a missing `{ path }` source logs a warning and skips the entry.
132
154
  * Default: required (missing source aborts setup). Bootstrap the main worktree first via
133
- * `workspace setup`, or seed sources in `preSetup`.
155
+ * `workspace setup`, or seed sources in `preSetup`. Ignored for `{ content }` sources.
134
156
  */
135
157
  optional?: boolean;
136
158
  }
package/dist/workspace.js CHANGED
@@ -4,7 +4,7 @@ import { dirname, join, relative, resolve } from "node:path";
4
4
  import { parseWorkspaceArgs, printWorkspaceHelp } from "./cli.js";
5
5
  import { findOwnEntry, liveWorktrees, readDevServers, removeDevServerEntryByWorktree, } from "./dev-servers-registry.js";
6
6
  import { ConfigError } from "./errors.js";
7
- import { copyAndPatchFile, formatDuration, setupLogPath } from "./helpers.js";
7
+ import { copyAndPatchFile, formatDuration, setupLogPath, } from "./helpers.js";
8
8
  import { isProcessAlive } from "./process-control.js";
9
9
  import { defaultComputePorts, isValidPort, resolvePortScheme } from "./ports.js";
10
10
  import { handleSetOwner, markSlotFailed, markSlotReady, readSlots, resolveAndRegisterSlot, resolveCurrentSlot, validateSlotAvailability, writeSlots, } from "./slots.js";
@@ -478,14 +478,23 @@ function linkSharedDirectories(ctx, dirs, log) {
478
478
  }
479
479
  function generateConfigFiles(ctx, entries, slot, ports, force, log) {
480
480
  for (const entry of entries) {
481
- copyAndPatchFile({ currentWorktree: ctx.currentWorktree, mainWorktree: ctx.mainWorktree, log }, entry.path, (content) => entry.patch(content, {
481
+ const patchCtx = {
482
482
  slot,
483
483
  ports,
484
484
  mainWorktree: ctx.mainWorktree,
485
485
  currentWorktree: ctx.currentWorktree,
486
- }), entry.path, force, entry.optional ?? false);
486
+ };
487
+ copyAndPatchFile({ currentWorktree: ctx.currentWorktree, log }, entry.path, resolveConfigSource(entry, patchCtx), (content) => entry.patch(content, patchCtx), entry.path, force, entry.optional ?? false);
487
488
  }
488
489
  }
490
+ function resolveConfigSource(entry, ctx) {
491
+ const spec = typeof entry.source === "function" ? entry.source(ctx) : entry.source;
492
+ if (spec === undefined)
493
+ return { path: join(ctx.mainWorktree, entry.path) };
494
+ if ("content" in spec)
495
+ return { content: spec.content };
496
+ return { path: join(ctx.mainWorktree, spec.path) };
497
+ }
489
498
  function resolveRemoveTarget(command, ctx, registry) {
490
499
  if (command.branch === undefined) {
491
500
  if (ctx.isMainWorktree) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paleo/workspace",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Run multiple git-worktree dev environments side by side.",
5
5
  "keywords": [
6
6
  "workspace",