@paleo/worktree-env 0.6.2 → 0.7.1
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 +7 -0
- package/dist/dev-server.js +2 -1
- package/dist/helpers.d.ts +8 -1
- package/dist/helpers.js +24 -4
- package/dist/server-descriptor.d.ts +5 -1
- package/dist/setup-worktree.d.ts +27 -2
- package/dist/setup-worktree.js +12 -3
- package/dist/slots.d.ts +2 -0
- package/dist/slots.js +2 -0
- package/dist/worktree.js +1 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,11 @@ await runSetupWorktree({
|
|
|
58
58
|
}),
|
|
59
59
|
},
|
|
60
60
|
],
|
|
61
|
+
preSetup: ({ currentWorktree, isMainWorktree, log }) => {
|
|
62
|
+
// Idempotent. Bootstrap source files the kernel expects to find (e.g. seed `.env` from
|
|
63
|
+
// `.env.example` on the main worktree). MUST NOT mutate the main worktree from a linked
|
|
64
|
+
// worktree setup — bootstrap the main first via `setup-worktree --here`.
|
|
65
|
+
},
|
|
61
66
|
finalizeWorktree: async ({ currentWorktree }) => {
|
|
62
67
|
// MUST be idempotent. Install deps, start containers, seed a database, etc.
|
|
63
68
|
},
|
|
@@ -68,6 +73,8 @@ await runSetupWorktree({
|
|
|
68
73
|
|
|
69
74
|
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 `setup-worktree --here` — it is idempotent and retries the finalize step. To block until Part 2 finishes (CI, agent orchestration), run `setup-worktree --wait` from inside the worktree (or `setup-worktree --wait --slot 8110` from anywhere) — exits 0 on `READY`, 1 on `FAILED`.
|
|
70
75
|
|
|
76
|
+
**Bootstrap the main worktree first.** Linked-worktree setup copies config sources from the main worktree, so the main must already have those files. Run `setup-worktree --here` 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.
|
|
77
|
+
|
|
71
78
|
`--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.
|
|
72
79
|
|
|
73
80
|
```ts
|
package/dist/dev-server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import { parseDevServerArgs, printDevServerHelp, validateDevServerFlags, } from "./cli.js";
|
|
6
6
|
import { evictOldest, findOwnEntry, listDevServers, printActiveServers, pruneAndPersist, registerDevServer, removeDevServerEntryByWorktree, stopAllRegistered, unregisterDevServer, } from "./dev-servers-registry.js";
|
|
7
7
|
import { ConfigError, StartupError } from "./errors.js";
|
|
8
|
+
import { detectCommonJsError } from "./helpers.js";
|
|
8
9
|
import { awaitAllReady, handleStartupFailure } from "./log-polling.js";
|
|
9
10
|
import { isProcessAlive, stopProcessGroup } from "./process-control.js";
|
|
10
11
|
import { resolveCurrentSlot } from "./slots.js";
|
|
@@ -80,7 +81,7 @@ async function start(config, mainWorktree, { evict }) {
|
|
|
80
81
|
name: s.name,
|
|
81
82
|
logFile: logFileFor(config.runtimeDir, s.name),
|
|
82
83
|
detectSuccess: s.detectSuccess,
|
|
83
|
-
detectError: s.detectError,
|
|
84
|
+
detectError: s.detectError ?? detectCommonJsError,
|
|
84
85
|
}));
|
|
85
86
|
const pollPids = spawnEntries.map((s) => spawnPids[s.name]);
|
|
86
87
|
await awaitAllReady(pollables, pollPids);
|
package/dist/helpers.d.ts
CHANGED
|
@@ -15,4 +15,11 @@ export interface CopyAndPatchCtx {
|
|
|
15
15
|
mainWorktree: string;
|
|
16
16
|
log: (msg: string) => void;
|
|
17
17
|
}
|
|
18
|
-
export declare function copyAndPatchFile(ctx: CopyAndPatchCtx, relPath: string, patchFn: (content: string) => string, label: string, force: boolean,
|
|
18
|
+
export declare function copyAndPatchFile(ctx: CopyAndPatchCtx, relPath: string, patchFn: (content: string) => string, label: string, force: boolean, optional?: boolean): void;
|
|
19
|
+
/**
|
|
20
|
+
* Detects common fatal JS startup failures in a log buffer. Returns a short marker string
|
|
21
|
+
* naming the matched pattern, or `false` when none match. Used as the default `detectError`
|
|
22
|
+
* for spawn servers that don't supply one. A custom `detectError` can compose with this:
|
|
23
|
+
* `detectError: (log) => myDetector(log) || helpers.detectCommonJsError(log)`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function detectCommonJsError(log: string): string | false;
|
package/dist/helpers.js
CHANGED
|
@@ -59,7 +59,7 @@ export function readPortFromJsonFile(file, jsonPath) {
|
|
|
59
59
|
}
|
|
60
60
|
return toPort(String(cur), file);
|
|
61
61
|
}
|
|
62
|
-
export function copyAndPatchFile(ctx, relPath, patchFn, label, force,
|
|
62
|
+
export function copyAndPatchFile(ctx, relPath, patchFn, label, force, optional = false) {
|
|
63
63
|
const targetPath = join(ctx.currentWorktree, relPath);
|
|
64
64
|
const sourcePath = join(ctx.mainWorktree, relPath);
|
|
65
65
|
const alreadyExists = existsSync(targetPath);
|
|
@@ -68,11 +68,12 @@ export function copyAndPatchFile(ctx, relPath, patchFn, label, force, required =
|
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
70
|
if (!existsSync(sourcePath)) {
|
|
71
|
-
if (
|
|
72
|
-
console.error(`Error: ${relPath} not found in main worktree
|
|
71
|
+
if (!optional) {
|
|
72
|
+
console.error(`Error: ${relPath} not found in main worktree. Bootstrap the main worktree first ` +
|
|
73
|
+
"(setup-worktree --here), or mark the entry as optional.");
|
|
73
74
|
process.exit(1);
|
|
74
75
|
}
|
|
75
|
-
ctx.log(`Warning: ${relPath} not found in main worktree, skipping.`);
|
|
76
|
+
ctx.log(`Warning: ${relPath} not found in main worktree, skipping (optional).`);
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
79
|
const content = readFileSync(sourcePath, "utf-8");
|
|
@@ -81,6 +82,25 @@ export function copyAndPatchFile(ctx, relPath, patchFn, label, force, required =
|
|
|
81
82
|
writeFileSync(targetPath, patched);
|
|
82
83
|
ctx.log(`${alreadyExists ? "Overwritten" : "Created"} ${label}.`);
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Detects common fatal JS startup failures in a log buffer. Returns a short marker string
|
|
87
|
+
* naming the matched pattern, or `false` when none match. Used as the default `detectError`
|
|
88
|
+
* for spawn servers that don't supply one. A custom `detectError` can compose with this:
|
|
89
|
+
* `detectError: (log) => myDetector(log) || helpers.detectCommonJsError(log)`.
|
|
90
|
+
*/
|
|
91
|
+
export function detectCommonJsError(log) {
|
|
92
|
+
if (log.includes("[nodemon] app crashed"))
|
|
93
|
+
return "[nodemon] app crashed";
|
|
94
|
+
if (/^Node\.js v/m.test(log))
|
|
95
|
+
return "Node.js v";
|
|
96
|
+
if (log.includes("Error: Cannot find module "))
|
|
97
|
+
return "Error: Cannot find module";
|
|
98
|
+
if (/^SyntaxError: /m.test(log))
|
|
99
|
+
return "SyntaxError";
|
|
100
|
+
if (log.includes("UnhandledPromiseRejection"))
|
|
101
|
+
return "UnhandledPromiseRejection";
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
84
104
|
function toPort(raw, file) {
|
|
85
105
|
const port = Number(raw);
|
|
86
106
|
if (!Number.isFinite(port)) {
|
|
@@ -22,7 +22,11 @@ export interface SpawnServer {
|
|
|
22
22
|
port: number;
|
|
23
23
|
/** Returns `true` once the log content indicates the server is ready. */
|
|
24
24
|
detectSuccess: (logContent: string) => boolean;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Returns a non-empty marker string when the log content indicates a fatal error, or `false`.
|
|
27
|
+
* When omitted, `helpers.detectCommonJsError` is used as a default. To disable detection,
|
|
28
|
+
* pass `() => false`.
|
|
29
|
+
*/
|
|
26
30
|
detectError?: (logContent: string) => string | false;
|
|
27
31
|
}
|
|
28
32
|
/**
|
package/dist/setup-worktree.d.ts
CHANGED
|
@@ -39,11 +39,21 @@ export interface SetupWorktreeConfig {
|
|
|
39
39
|
registryDir: string;
|
|
40
40
|
/** Config files copied from the main worktree and patched per slot. */
|
|
41
41
|
configFiles: ConfigFileEntry[];
|
|
42
|
+
/**
|
|
43
|
+
* Runs before `configFiles` are copied. Use this to bootstrap source files the kernel expects
|
|
44
|
+
* to find (e.g. seed `config.json` from `config.example.json` on the main worktree, decrypt
|
|
45
|
+
* an env file). MUST be idempotent. On a linked-worktree setup, MUST NOT mutate the main
|
|
46
|
+
* worktree — bootstrap the main worktree first via `setup-worktree --here`.
|
|
47
|
+
*/
|
|
48
|
+
preSetup?: (ctx: PreSetupContext) => Promise<void> | void;
|
|
42
49
|
/**
|
|
43
50
|
* MUST be idempotent. After a failure, the user re-runs `setup-worktree --here` from inside
|
|
44
51
|
* the worktree — this callback will be invoked again with the same context. Re-runs must not
|
|
45
52
|
* error on pre-existing state (created directories, started containers, ran migrations,
|
|
46
53
|
* installed deps, etc.).
|
|
54
|
+
*
|
|
55
|
+
* Runs in a detached child whose stdout/stderr are already redirected to
|
|
56
|
+
* `<runtimeDir>/wt-setup.log`. `console.log` and child-process `stdio: "inherit"` land there.
|
|
47
57
|
*/
|
|
48
58
|
finalizeWorktree: (ctx: SetupContext) => Promise<void> | void;
|
|
49
59
|
/**
|
|
@@ -61,6 +71,17 @@ export interface SetupWorktreeConfig {
|
|
|
61
71
|
*/
|
|
62
72
|
worktreeDirName?: WorktreeDirNameFn;
|
|
63
73
|
}
|
|
74
|
+
/** Context passed to {@link SetupWorktreeConfig.preSetup}. */
|
|
75
|
+
export interface PreSetupContext {
|
|
76
|
+
currentWorktree: string;
|
|
77
|
+
mainWorktree: string;
|
|
78
|
+
/** `true` when running on the main worktree (i.e. `--here` from the main checkout). */
|
|
79
|
+
isMainWorktree: boolean;
|
|
80
|
+
/** Mirrors `--force`. Hooks may use it to overwrite previously bootstrapped files. */
|
|
81
|
+
force: boolean;
|
|
82
|
+
/** Writes to stdout and the setup log. */
|
|
83
|
+
log: (msg: string) => void;
|
|
84
|
+
}
|
|
64
85
|
/** Context passed to {@link SetupWorktreeConfig.finalizeWorktree}. */
|
|
65
86
|
export interface SetupContext {
|
|
66
87
|
currentWorktree: string;
|
|
@@ -97,8 +118,12 @@ export interface ConfigFileEntry {
|
|
|
97
118
|
path: string;
|
|
98
119
|
/** Returns the patched content given the source content and the slot's ports. */
|
|
99
120
|
patch: (content: string, ctx: PatchContext) => string;
|
|
100
|
-
/**
|
|
101
|
-
|
|
121
|
+
/**
|
|
122
|
+
* When `true`, a missing source on the main worktree logs a warning and skips the entry.
|
|
123
|
+
* Default: required (missing source aborts setup). Bootstrap the main worktree first via
|
|
124
|
+
* `setup-worktree --here`, or seed sources in `preSetup`.
|
|
125
|
+
*/
|
|
126
|
+
optional?: boolean;
|
|
102
127
|
}
|
|
103
128
|
/** Context passed to {@link ConfigFileEntry.patch}. */
|
|
104
129
|
export interface PatchContext {
|
package/dist/setup-worktree.js
CHANGED
|
@@ -88,6 +88,7 @@ async function runSetup(args, ctx, run, config) {
|
|
|
88
88
|
scheme,
|
|
89
89
|
branch,
|
|
90
90
|
requestedOwner: args.owner,
|
|
91
|
+
isMainWorktree: setupCtx.isMainWorktree,
|
|
91
92
|
});
|
|
92
93
|
const ports = portsFn(slot);
|
|
93
94
|
const runtimeDir = join(setupCtx.currentWorktree, config.runtimeDir);
|
|
@@ -111,6 +112,15 @@ async function runSetup(args, ctx, run, config) {
|
|
|
111
112
|
verboseLog(`Using slot ${slot} (${Object.entries(ports)
|
|
112
113
|
.map(([k, v]) => `${k}: ${v}`)
|
|
113
114
|
.join(", ")})`);
|
|
115
|
+
if (config.preSetup) {
|
|
116
|
+
await config.preSetup({
|
|
117
|
+
currentWorktree: setupCtx.currentWorktree,
|
|
118
|
+
mainWorktree: setupCtx.mainWorktree,
|
|
119
|
+
isMainWorktree: setupCtx.isMainWorktree,
|
|
120
|
+
force: args.force ?? false,
|
|
121
|
+
log: teeLog,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
114
124
|
linkSharedDirectories(setupCtx, config.sharedDirs, verboseLog);
|
|
115
125
|
generateConfigFiles(setupCtx, config.configFiles, slot, ports, args.force ?? false, verboseLog);
|
|
116
126
|
teeLog(config.printSummary({
|
|
@@ -199,8 +209,7 @@ function printWorktreeInfo(config, slot, worktreeForLog, fallback) {
|
|
|
199
209
|
const ports = resolvePortsFn(config)(slot);
|
|
200
210
|
const branch = entry?.branch ?? fallback.branch;
|
|
201
211
|
const owner = entry?.owner ?? fallback.owner;
|
|
202
|
-
|
|
203
|
-
const slotStatus = entry?.status ?? (ctx.isMainWorktree ? "ready" : "pending");
|
|
212
|
+
const slotStatus = entry?.status ?? "pending";
|
|
204
213
|
const logHint = ` (tail ${join(worktreeForLog, config.runtimeDir, "wt-setup.log")})`;
|
|
205
214
|
const display = slotStatus === "ready"
|
|
206
215
|
? "ready"
|
|
@@ -377,7 +386,7 @@ function generateConfigFiles(ctx, entries, slot, ports, force, log) {
|
|
|
377
386
|
ports,
|
|
378
387
|
mainWorktree: ctx.mainWorktree,
|
|
379
388
|
currentWorktree: ctx.currentWorktree,
|
|
380
|
-
}), entry.path, force, entry.
|
|
389
|
+
}), entry.path, force, entry.optional ?? false);
|
|
381
390
|
}
|
|
382
391
|
}
|
|
383
392
|
function resolveRemoveTarget(args, ctx, registry, removeHere) {
|
package/dist/slots.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface RegisterSlotInput {
|
|
|
30
30
|
scheme: PortScheme;
|
|
31
31
|
branch: string;
|
|
32
32
|
requestedOwner?: string;
|
|
33
|
+
/** When `true`, the slot is forced to `scheme.basePort` regardless of `slot` arg. */
|
|
34
|
+
isMainWorktree: boolean;
|
|
33
35
|
}
|
|
34
36
|
export declare function resolveAndRegisterSlot(input: RegisterSlotInput): {
|
|
35
37
|
port: number;
|
package/dist/slots.js
CHANGED
|
@@ -104,6 +104,8 @@ export function handleSetOwner(input) {
|
|
|
104
104
|
}
|
|
105
105
|
function pickSlotPort(args, registry) {
|
|
106
106
|
const resolvedCurrent = resolve(args.currentWorktree);
|
|
107
|
+
if (args.isMainWorktree)
|
|
108
|
+
return args.scheme.basePort;
|
|
107
109
|
if (args.slot !== undefined) {
|
|
108
110
|
const port = Number(args.slot);
|
|
109
111
|
if (!isValidPort(port, args.scheme)) {
|
package/dist/worktree.js
CHANGED
|
@@ -17,12 +17,7 @@ export function enforceWorktreeMode(args, ctx) {
|
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
if (ctx.isMainWorktree) {
|
|
22
|
-
console.error("Error: --here must be run from a linked worktree, not from the main worktree.");
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
20
|
+
// --here runs in any worktree: linked worktree (retry path) or main (initial bootstrap).
|
|
26
21
|
}
|
|
27
22
|
export function useExistingBranch(branch, ctx, run, dirNameFn = defaultWorktreeDirName) {
|
|
28
23
|
if (!branchExists(branch)) {
|