@paleo/workspace 0.17.0 → 0.18.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/workspace.js +6 -6
- 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/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":
|
|
@@ -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);
|
|
@@ -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) {
|
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
|
}
|