@paleo/workspace 0.17.0 → 0.19.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/cli.d.ts +2 -1
- package/dist/cli.js +11 -4
- package/dist/slots.d.ts +1 -1
- package/dist/slots.js +1 -1
- package/dist/workspace.js +14 -14
- package/dist/worktree.d.ts +2 -4
- package/dist/worktree.js +31 -19
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type WorkspaceCommand = {
|
|
|
2
2
|
kind: "setup";
|
|
3
3
|
branch?: string;
|
|
4
4
|
newBranch: boolean;
|
|
5
|
+
from?: string;
|
|
5
6
|
owner?: string;
|
|
6
7
|
slot?: string;
|
|
7
8
|
force: boolean;
|
|
@@ -9,7 +10,7 @@ export type WorkspaceCommand = {
|
|
|
9
10
|
} | {
|
|
10
11
|
kind: "remove";
|
|
11
12
|
branch?: string;
|
|
12
|
-
|
|
13
|
+
force: boolean;
|
|
13
14
|
} | {
|
|
14
15
|
kind: "list";
|
|
15
16
|
} | {
|
package/dist/cli.js
CHANGED
|
@@ -43,6 +43,7 @@ function parseSetup(tokens) {
|
|
|
43
43
|
args: tokens,
|
|
44
44
|
options: {
|
|
45
45
|
"new-branch": { type: "boolean", short: "c" },
|
|
46
|
+
from: { type: "string" },
|
|
46
47
|
owner: { type: "string" },
|
|
47
48
|
slot: { type: "string", short: "s" },
|
|
48
49
|
force: { type: "boolean" },
|
|
@@ -57,11 +58,15 @@ function parseSetup(tokens) {
|
|
|
57
58
|
if (newBranch && branch === undefined) {
|
|
58
59
|
throw new ConfigError("`workspace setup <branch> -c` requires a branch name.");
|
|
59
60
|
}
|
|
61
|
+
if (values.from !== undefined && !newBranch) {
|
|
62
|
+
throw new ConfigError("`--from` requires `-c`/`--new-branch`.");
|
|
63
|
+
}
|
|
60
64
|
return {
|
|
61
65
|
command: {
|
|
62
66
|
kind: "setup",
|
|
63
67
|
branch,
|
|
64
68
|
newBranch,
|
|
69
|
+
from: values.from,
|
|
65
70
|
owner: values.owner,
|
|
66
71
|
slot: values.slot,
|
|
67
72
|
force: values.force ?? false,
|
|
@@ -74,7 +79,7 @@ function parseRemove(tokens) {
|
|
|
74
79
|
const { values, positionals } = parseArgs({
|
|
75
80
|
args: tokens,
|
|
76
81
|
options: {
|
|
77
|
-
|
|
82
|
+
force: { type: "boolean" },
|
|
78
83
|
verbose: { type: "boolean", short: "v" },
|
|
79
84
|
},
|
|
80
85
|
allowPositionals: true,
|
|
@@ -82,7 +87,7 @@ function parseRemove(tokens) {
|
|
|
82
87
|
});
|
|
83
88
|
const branch = takeOptionalPositional(positionals, "remove");
|
|
84
89
|
return {
|
|
85
|
-
command: { kind: "remove", branch,
|
|
90
|
+
command: { kind: "remove", branch, force: values.force ?? false },
|
|
86
91
|
verbose: values.verbose ?? false,
|
|
87
92
|
};
|
|
88
93
|
}
|
|
@@ -176,13 +181,15 @@ export function printWorkspaceHelp() {
|
|
|
176
181
|
"Manage workspaces: a git worktree plus its own dev setup (ports, config, database, dev server).",
|
|
177
182
|
"",
|
|
178
183
|
"Commands:",
|
|
179
|
-
" setup [<branch>] [-c|--new-branch] [--owner <name>] [-s|--slot <port>] [--force] [--wait]",
|
|
184
|
+
" setup [<branch>] [-c|--new-branch] [--from <ref>] [--owner <name>] [-s|--slot <port>] [--force] [--wait]",
|
|
180
185
|
" Set up the workspace. With <branch>, create a sibling worktree for it",
|
|
181
186
|
" (add -c to create the branch first). Without, set up the current worktree",
|
|
182
187
|
" (idempotent; bootstrap and retry path).",
|
|
188
|
+
" With -c, the new branch starts at the current worktree's HEAD, or at <ref> with --from.",
|
|
183
189
|
" Finalize runs in the background; add --wait to block until it reaches READY.",
|
|
184
|
-
" remove [<branch>] [--
|
|
190
|
+
" remove [<branch>] [--force]",
|
|
185
191
|
" Remove a workspace by branch, or the current one when omitted.",
|
|
192
|
+
" Refuses on uncommitted changes unless --force.",
|
|
186
193
|
" list",
|
|
187
194
|
" List all registered workspaces (slot, status, branch, path, owner, created).",
|
|
188
195
|
" status [-s|--slot <port>]",
|
package/dist/slots.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PortScheme } from "./ports.js";
|
|
2
|
-
export declare const REGISTRY_SUBDIR = "
|
|
2
|
+
export declare const REGISTRY_SUBDIR = "workspace-registry";
|
|
3
3
|
export declare function registryDirFor(runtimeDir: string): string;
|
|
4
4
|
/** `registryDir` is gone from the config types but may linger in a consumer's config file. */
|
|
5
5
|
export declare function warnLegacyRegistryDir(config: {
|
package/dist/slots.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { allPorts, isValidPort } from "./ports.js";
|
|
5
5
|
import { getWorktreeBranch } from "./worktree.js";
|
|
6
|
-
export const REGISTRY_SUBDIR = "
|
|
6
|
+
export const REGISTRY_SUBDIR = "workspace-registry";
|
|
7
7
|
const SLOTS_FILENAME = "slots.json";
|
|
8
8
|
export function registryDirFor(runtimeDir) {
|
|
9
9
|
return join(runtimeDir, REGISTRY_SUBDIR);
|
package/dist/workspace.js
CHANGED
|
@@ -8,7 +8,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, mergeSlots, readSlots, REGISTRY_SUBDIR, registryDirFor, resolveAndRegisterSlot, resolveCurrentSlot, validateSlotAvailability, warnLegacyRegistryDir, writeSlots, } from "./slots.js";
|
|
11
|
-
import { createBranch, detectWorktree,
|
|
11
|
+
import { createBranch, detectWorktree, getWorktreeBranch, isWorktreeDirty, removeWorktree, useExistingBranch, } from "./worktree.js";
|
|
12
12
|
export async function runWorkspace(config) {
|
|
13
13
|
let command;
|
|
14
14
|
let verbose;
|
|
@@ -53,7 +53,6 @@ export async function runWorkspace(config) {
|
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
const ctx = detectWorktree();
|
|
56
|
-
enforceWorktreeMode(command, ctx);
|
|
57
56
|
const run = { verbose };
|
|
58
57
|
switch (command.kind) {
|
|
59
58
|
case "remove":
|
|
@@ -123,7 +122,7 @@ async function runSetup(command, ctx, run, config, registryDir) {
|
|
|
123
122
|
});
|
|
124
123
|
}
|
|
125
124
|
linkSharedDirectories(setupCtx, config.sharedDirs, verboseLog);
|
|
126
|
-
|
|
125
|
+
linkWorkspaceRegistry(setupCtx, config.runtimeDir, verboseLog);
|
|
127
126
|
generateConfigFiles(setupCtx, config.configFiles, slot, ports, command.force, verboseLog);
|
|
128
127
|
teeLog(config.printSummary({
|
|
129
128
|
slot,
|
|
@@ -390,9 +389,6 @@ async function handleRemove(command, ctx, run, config, registryDir) {
|
|
|
390
389
|
`Run 'workspace wait --slot ${target.slotPort}' to wait for it to finish (or fail), then retry the removal.`);
|
|
391
390
|
process.exit(1);
|
|
392
391
|
}
|
|
393
|
-
if (!command.noRemoteCheck) {
|
|
394
|
-
verifyBranchAbsentFromRemote(target.branch, run);
|
|
395
|
-
}
|
|
396
392
|
const ownerSuffix = target.owner ? `, owner ${target.owner}` : "";
|
|
397
393
|
if (!existsSync(target.worktreePath)) {
|
|
398
394
|
console.warn(`Warning: Worktree directory ${target.worktreePath} not found. Cleaning up registry only.`);
|
|
@@ -401,6 +397,10 @@ async function handleRemove(command, ctx, run, config, registryDir) {
|
|
|
401
397
|
console.log(`Removed registry entry for branch "${target.branch}" (slot ${target.slotPort}${ownerSuffix}).`);
|
|
402
398
|
return;
|
|
403
399
|
}
|
|
400
|
+
if (!command.force && isWorktreeDirty(target.worktreePath)) {
|
|
401
|
+
console.error(`Error: Uncommitted changes in ${target.worktreePath}. Commit or stash them, or pass --force.`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
404
|
const targetEntry = findOwnEntry(ctx.mainWorktree, registryDir, target.worktreePath);
|
|
405
405
|
if (targetEntry) {
|
|
406
406
|
stopTargetDevServer(config.devServerScript, target.worktreePath, verboseLog);
|
|
@@ -459,7 +459,7 @@ function handleSetOwnerMode(command, ctx, registryDir) {
|
|
|
459
459
|
}
|
|
460
460
|
console.log(`Owner for slot ${slotPort}: ${newOwner ?? "(none)"}`);
|
|
461
461
|
}
|
|
462
|
-
/** Transitional (0.16 only): merge a pre-0.16 registry into `${runtimeDir}/
|
|
462
|
+
/** Transitional (0.16 only): merge a pre-0.16 registry into `${runtimeDir}/workspace-registry`. */
|
|
463
463
|
function handleMigrate(command, config, newRel) {
|
|
464
464
|
const ctx = detectWorktree();
|
|
465
465
|
const oldRel = command.oldRegistryDir;
|
|
@@ -504,7 +504,7 @@ function relinkWorktrees(slots, mainWorktree, runtimeDir) {
|
|
|
504
504
|
console.warn(`Warning: worktree ${entry.worktree} not found; skipping symlink.`);
|
|
505
505
|
continue;
|
|
506
506
|
}
|
|
507
|
-
|
|
507
|
+
linkWorkspaceRegistry({ currentWorktree: entry.worktree, mainWorktree, isMainWorktree: false }, runtimeDir, console.log, { force: true });
|
|
508
508
|
++count;
|
|
509
509
|
}
|
|
510
510
|
return count;
|
|
@@ -513,7 +513,7 @@ function ensureWorktree(command, ctx, run, dirNameFn) {
|
|
|
513
513
|
if (command.branch === undefined)
|
|
514
514
|
return ctx;
|
|
515
515
|
if (command.newBranch)
|
|
516
|
-
return createBranch(command.branch, ctx, run, dirNameFn);
|
|
516
|
+
return createBranch(command.branch, ctx, run, dirNameFn, command.from);
|
|
517
517
|
return useExistingBranch(command.branch, ctx, run, dirNameFn);
|
|
518
518
|
}
|
|
519
519
|
function linkSharedDirectories(ctx, dirs, log) {
|
|
@@ -534,11 +534,11 @@ function linkSharedDirectories(ctx, dirs, log) {
|
|
|
534
534
|
}
|
|
535
535
|
}
|
|
536
536
|
/**
|
|
537
|
-
* Symlinks the linked worktree's `${runtimeDir}/
|
|
537
|
+
* Symlinks the linked worktree's `${runtimeDir}/workspace-registry` to the main worktree's, so the
|
|
538
538
|
* cwd-relative registry read in `resolveCurrentSlot` reaches main. `runtimeDir` is per-worktree and
|
|
539
539
|
* not in `sharedDirs`, so this is distinct from {@link linkSharedDirectories}.
|
|
540
540
|
*/
|
|
541
|
-
function
|
|
541
|
+
function linkWorkspaceRegistry(ctx, runtimeDir, log, opts) {
|
|
542
542
|
if (ctx.isMainWorktree)
|
|
543
543
|
return;
|
|
544
544
|
const mainDir = join(ctx.mainWorktree, runtimeDir, REGISTRY_SUBDIR);
|
|
@@ -551,17 +551,17 @@ function linkSharedRegistry(ctx, runtimeDir, log, opts) {
|
|
|
551
551
|
const linkStat = lstatSync(link, { throwIfNoEntry: false });
|
|
552
552
|
if (linkStat) {
|
|
553
553
|
if (!linkStat.isSymbolicLink()) {
|
|
554
|
-
log("Skipped
|
|
554
|
+
log("Skipped workspace-registry symlink (a real directory exists here).");
|
|
555
555
|
return;
|
|
556
556
|
}
|
|
557
557
|
if (!opts?.force && existsSync(link)) {
|
|
558
|
-
log("Skipped
|
|
558
|
+
log("Skipped workspace-registry symlink (already exists).");
|
|
559
559
|
return;
|
|
560
560
|
}
|
|
561
561
|
rmSync(link);
|
|
562
562
|
}
|
|
563
563
|
symlinkSync(relative(runtimeRoot, mainDir), link);
|
|
564
|
-
log("Created
|
|
564
|
+
log("Created workspace-registry symlink → main worktree.");
|
|
565
565
|
}
|
|
566
566
|
function generateConfigFiles(ctx, entries, slot, ports, force, log) {
|
|
567
567
|
for (const entry of entries) {
|
package/dist/worktree.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { WorkspaceCommand } from "./cli.js";
|
|
2
1
|
export interface WorktreeContext {
|
|
3
2
|
currentWorktree: string;
|
|
4
3
|
mainWorktree: string;
|
|
@@ -8,10 +7,9 @@ export interface RunCtx {
|
|
|
8
7
|
verbose: boolean;
|
|
9
8
|
}
|
|
10
9
|
export declare function detectWorktree(): WorktreeContext;
|
|
11
|
-
export declare function enforceWorktreeMode(command: WorkspaceCommand, ctx: WorktreeContext): void;
|
|
12
10
|
export declare function useExistingBranch(branch: string, ctx: WorktreeContext, run: RunCtx, dirNameFn?: WorktreeDirNameFn): WorktreeContext;
|
|
13
|
-
export declare function createBranch(requestedBranch: string, ctx: WorktreeContext, run: RunCtx, dirNameFn?: WorktreeDirNameFn): WorktreeContext;
|
|
14
|
-
export declare function
|
|
11
|
+
export declare function createBranch(requestedBranch: string, ctx: WorktreeContext, run: RunCtx, dirNameFn?: WorktreeDirNameFn, from?: string): WorktreeContext;
|
|
12
|
+
export declare function isWorktreeDirty(worktreePath: string): boolean;
|
|
15
13
|
export declare function getWorktreeBranch(worktreePath: string): string | undefined;
|
|
16
14
|
export declare function removeWorktree(worktreePath: string, run: RunCtx): void;
|
|
17
15
|
/** Pure function that produces the basename of a worktree directory from a branch. */
|
package/dist/worktree.js
CHANGED
|
@@ -10,14 +10,6 @@ export function detectWorktree() {
|
|
|
10
10
|
const isMainWorktree = resolve(currentWorktree) === resolve(mainWorktree);
|
|
11
11
|
return { currentWorktree, mainWorktree, isMainWorktree };
|
|
12
12
|
}
|
|
13
|
-
export function enforceWorktreeMode(command, ctx) {
|
|
14
|
-
// Adding a worktree for a branch must happen from the main worktree. A branch-less
|
|
15
|
-
// `workspace setup` runs anywhere: linked worktree (retry path) or main (initial bootstrap).
|
|
16
|
-
if (command.kind === "setup" && command.branch !== undefined && !ctx.isMainWorktree) {
|
|
17
|
-
console.error("Error: Adding a workspace for a branch must be run from the main worktree.");
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
13
|
export function useExistingBranch(branch, ctx, run, dirNameFn = defaultWorktreeDirName) {
|
|
22
14
|
if (!branchExists(branch)) {
|
|
23
15
|
console.error(`Error: Branch "${branch}" does not exist locally or on the remote.`);
|
|
@@ -27,7 +19,9 @@ export function useExistingBranch(branch, ctx, run, dirNameFn = defaultWorktreeD
|
|
|
27
19
|
execFileSync("git", ["worktree", "add", worktreePath, branch], { stdio: stdioFor(run) });
|
|
28
20
|
return { ...ctx, currentWorktree: worktreePath, isMainWorktree: false };
|
|
29
21
|
}
|
|
30
|
-
export function createBranch(requestedBranch, ctx, run, dirNameFn = defaultWorktreeDirName) {
|
|
22
|
+
export function createBranch(requestedBranch, ctx, run, dirNameFn = defaultWorktreeDirName, from) {
|
|
23
|
+
if (from !== undefined)
|
|
24
|
+
verifyFromRef(from);
|
|
31
25
|
let finalBranch = requestedBranch;
|
|
32
26
|
if (branchExists(finalBranch)) {
|
|
33
27
|
let suffix = 2;
|
|
@@ -38,18 +32,36 @@ export function createBranch(requestedBranch, ctx, run, dirNameFn = defaultWorkt
|
|
|
38
32
|
console.warn(`Warning: Branch "${requestedBranch}" already exists; using "${finalBranch}" instead.`);
|
|
39
33
|
}
|
|
40
34
|
const worktreePath = dedupeWorktreePath(computeWorktreePath(ctx.mainWorktree, finalBranch, dirNameFn));
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
const addArgs = ["worktree", "add", "-b", finalBranch, "--end-of-options", worktreePath];
|
|
36
|
+
if (from !== undefined)
|
|
37
|
+
addArgs.push(from);
|
|
38
|
+
execFileSync("git", addArgs, { stdio: stdioFor(run) });
|
|
44
39
|
return { ...ctx, currentWorktree: worktreePath, isMainWorktree: false };
|
|
45
40
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
function verifyFromRef(from) {
|
|
42
|
+
try {
|
|
43
|
+
// `^{commit}` accepts any commit-ish: branch, origin/x, tag, SHA.
|
|
44
|
+
// `--end-of-options` guards against option-like refs (rev-parse treats args after `--` as paths).
|
|
45
|
+
execFileSync("git", ["rev-parse", "--verify", "--end-of-options", `${from}^{commit}`], {
|
|
46
|
+
stdio: "pipe",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
console.error(`Error: --from ref "${from}" does not resolve to a commit.`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function isWorktreeDirty(worktreePath) {
|
|
55
|
+
try {
|
|
56
|
+
const out = execFileSync("git", ["status", "--porcelain"], {
|
|
57
|
+
stdio: "pipe",
|
|
58
|
+
cwd: worktreePath,
|
|
59
|
+
encoding: "utf-8",
|
|
60
|
+
});
|
|
61
|
+
return out.trim().length > 0;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
console.error(`Error: Cannot check for uncommitted changes in ${worktreePath}. Pass --force to remove anyway.`);
|
|
53
65
|
process.exit(1);
|
|
54
66
|
}
|
|
55
67
|
}
|