@alecrust/workbox 0.4.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.
@@ -0,0 +1,67 @@
1
+ import { getWorkboxWorktree } from "../core/git";
2
+ import { runCommand } from "../core/process";
3
+ import { UsageError } from "../ui/errors";
4
+ import { parseArgsOrUsage } from "./parse";
5
+ import type { CommandDefinition } from "./types";
6
+
7
+ const splitExecArgs = (args: string[]): { name: string; command: string[] } => {
8
+ const { tokens } = parseArgsOrUsage({
9
+ args,
10
+ allowPositionals: true,
11
+ strict: false,
12
+ tokens: true,
13
+ });
14
+
15
+ const separator = tokens?.find((token) => token.kind === "option-terminator");
16
+ if (!separator) {
17
+ throw new UsageError(
18
+ "Missing command separator '--'. Usage: workbox exec <name> -- <command>."
19
+ );
20
+ }
21
+
22
+ const before = args.slice(0, separator.index);
23
+ if (before.length !== 1) {
24
+ throw new UsageError("Usage: workbox exec <name> -- <command>.");
25
+ }
26
+
27
+ const [name] = before;
28
+ const command = args.slice(separator.index + 1);
29
+ if (!name) {
30
+ throw new UsageError("Missing worktree name.");
31
+ }
32
+ if (command.length === 0) {
33
+ throw new UsageError("Missing command to execute after '--'.");
34
+ }
35
+
36
+ return { name, command };
37
+ };
38
+
39
+ export const execCommand: CommandDefinition = {
40
+ name: "exec",
41
+ summary: "Run a command inside a sandbox",
42
+ description: "Execute a command inside a workbox sandbox worktree.",
43
+ usage: "workbox exec <name> -- <command>",
44
+ run: async (context, args) => {
45
+ const { name, command } = splitExecArgs(args);
46
+
47
+ const worktree = await getWorkboxWorktree({
48
+ repoRoot: context.repoRoot,
49
+ worktreesDir: context.config.worktrees.directory,
50
+ branchPrefix: context.config.worktrees.branch_prefix,
51
+ name,
52
+ });
53
+
54
+ const mode = context.flags.json ? "capture" : "inherit";
55
+ const result = await runCommand({ cmd: command, cwd: worktree.path, mode });
56
+
57
+ return {
58
+ message: "",
59
+ data: {
60
+ worktree,
61
+ command,
62
+ result,
63
+ },
64
+ exitCode: result.exitCode,
65
+ };
66
+ },
67
+ };
@@ -0,0 +1,23 @@
1
+ import { devCommand } from "./dev";
2
+ import { execCommand } from "./exec";
3
+ import { listCommand } from "./list";
4
+ import { newCommand } from "./new";
5
+ import { pruneCommand } from "./prune";
6
+ import { rmCommand } from "./rm";
7
+ import { setupCommand } from "./setup";
8
+ import { statusCommand } from "./status";
9
+ import type { CommandDefinition } from "./types";
10
+
11
+ export const commands: CommandDefinition[] = [
12
+ newCommand,
13
+ rmCommand,
14
+ listCommand,
15
+ pruneCommand,
16
+ statusCommand,
17
+ setupCommand,
18
+ devCommand,
19
+ execCommand,
20
+ ];
21
+
22
+ export const getCommand = (name: string): CommandDefinition | undefined =>
23
+ commands.find((command) => command.name === name);
@@ -0,0 +1,42 @@
1
+ import { getWorkboxWorktrees } from "../core/git";
2
+ import { UsageError } from "../ui/errors";
3
+ import { parseArgsOrUsage } from "./parse";
4
+ import type { CommandDefinition } from "./types";
5
+
6
+ export const listCommand: CommandDefinition = {
7
+ name: "list",
8
+ summary: "List sandbox worktrees",
9
+ description: "List all workbox sandbox worktrees.",
10
+ usage: "workbox list",
11
+ run: async (context, args) => {
12
+ const { positionals } = parseArgsOrUsage({
13
+ args,
14
+ allowPositionals: true,
15
+ strict: true,
16
+ });
17
+ if (positionals.length > 0) {
18
+ throw new UsageError(`Unexpected arguments: ${positionals.join(" ")}`);
19
+ }
20
+
21
+ const worktrees = await getWorkboxWorktrees({
22
+ repoRoot: context.repoRoot,
23
+ worktreesDir: context.config.worktrees.directory,
24
+ branchPrefix: context.config.worktrees.branch_prefix,
25
+ });
26
+
27
+ return {
28
+ message:
29
+ worktrees.length === 0
30
+ ? "No workbox worktrees found."
31
+ : [
32
+ "Workbox worktrees:",
33
+ ...worktrees.map((w) => {
34
+ const branch = w.branch ?? "(detached)";
35
+ const status = w.managed ? "managed" : "unmanaged";
36
+ return `- ${w.name}\t${branch}\t${status}\t${w.path}`;
37
+ }),
38
+ ].join("\n"),
39
+ data: worktrees,
40
+ };
41
+ },
42
+ };
@@ -0,0 +1,73 @@
1
+ import { createWorktree } from "../core/git";
2
+ import { runProvision } from "../provision/runner";
3
+ import { UsageError } from "../ui/errors";
4
+ import { parseArgsOrUsage } from "./parse";
5
+ import type { CommandDefinition } from "./types";
6
+
7
+ export const newCommand: CommandDefinition = {
8
+ name: "new",
9
+ summary: "Create a new sandbox worktree",
10
+ description: "Create a new workbox sandbox worktree with the given name.",
11
+ usage: "workbox new <name> [--from <ref>]",
12
+ run: async (context, args) => {
13
+ const parsed = parseArgsOrUsage({
14
+ args,
15
+ options: {
16
+ from: { type: "string" },
17
+ },
18
+ allowPositionals: true,
19
+ strict: true,
20
+ });
21
+ const { positionals } = parsed;
22
+ const [name, ...rest] = positionals;
23
+ if (!name) {
24
+ const message = context.flags.nonInteractive
25
+ ? "Missing worktree name in non-interactive mode."
26
+ : "Missing worktree name.";
27
+ throw new UsageError(message);
28
+ }
29
+ if (rest.length > 0) {
30
+ throw new UsageError(`Unexpected arguments: ${rest.join(" ")}`);
31
+ }
32
+
33
+ const fromValue = parsed.values.from;
34
+ const baseRef =
35
+ (typeof fromValue === "string" ? fromValue : undefined) ?? context.config.worktrees.base_ref;
36
+ if (!baseRef) {
37
+ throw new UsageError(
38
+ "Missing base ref. Provide --from <ref> or set worktrees.base_ref in config."
39
+ );
40
+ }
41
+
42
+ const worktree = await createWorktree({
43
+ repoRoot: context.repoRoot,
44
+ name,
45
+ worktreesDir: context.config.worktrees.directory,
46
+ branchPrefix: context.config.worktrees.branch_prefix,
47
+ baseRef,
48
+ });
49
+
50
+ let provisionResult: Awaited<ReturnType<typeof runProvision>> | undefined;
51
+ if (context.config.provision.enabled) {
52
+ provisionResult = await runProvision(context.config.provision, {
53
+ sourceRoot: context.worktreeRoot,
54
+ targetRoot: worktree.path,
55
+ worktreeName: worktree.name,
56
+ mode: context.flags.json ? "capture" : "inherit",
57
+ });
58
+
59
+ if (provisionResult.exitCode !== 0) {
60
+ return {
61
+ message: provisionResult.message,
62
+ data: { worktree, provision: provisionResult },
63
+ exitCode: provisionResult.exitCode,
64
+ };
65
+ }
66
+ }
67
+
68
+ return {
69
+ message: `Created worktree "${worktree.name}" at ${worktree.path} on branch ${worktree.managedBranch}.`,
70
+ data: provisionResult ? { worktree, provision: provisionResult } : worktree,
71
+ };
72
+ },
73
+ };
@@ -0,0 +1,14 @@
1
+ import { parseArgs } from "node:util";
2
+
3
+ import { UsageError } from "../ui/errors";
4
+
5
+ type ParseArgsOptions = Parameters<typeof parseArgs>[0];
6
+
7
+ export const parseArgsOrUsage = (options: ParseArgsOptions) => {
8
+ try {
9
+ return parseArgs(options);
10
+ } catch (error) {
11
+ const message = error instanceof Error ? error.message : String(error);
12
+ throw new UsageError(message);
13
+ }
14
+ };
@@ -0,0 +1,30 @@
1
+ import { pruneWorktrees } from "../core/git";
2
+ import { UsageError } from "../ui/errors";
3
+ import { parseArgsOrUsage } from "./parse";
4
+ import type { CommandDefinition } from "./types";
5
+
6
+ export const pruneCommand: CommandDefinition = {
7
+ name: "prune",
8
+ summary: "Prune stale git worktree metadata",
9
+ description: "Prune stale git worktree metadata (does not delete branches).",
10
+ usage: "workbox prune",
11
+ run: async (context, args) => {
12
+ const { positionals } = parseArgsOrUsage({
13
+ args,
14
+ allowPositionals: true,
15
+ strict: true,
16
+ });
17
+ if (positionals.length > 0) {
18
+ throw new UsageError(`Unexpected arguments: ${positionals.join(" ")}`);
19
+ }
20
+
21
+ const result = await pruneWorktrees(context.repoRoot);
22
+ return {
23
+ message:
24
+ result.stdout.length > 0
25
+ ? `Pruned worktree metadata:\n${result.stdout}`
26
+ : "Pruned worktree metadata.",
27
+ data: result,
28
+ };
29
+ },
30
+ };
@@ -0,0 +1,64 @@
1
+ import { getWorkboxWorktree, removeWorktree } from "../core/git";
2
+ import { UsageError } from "../ui/errors";
3
+ import { parseArgsOrUsage } from "./parse";
4
+ import type { CommandDefinition } from "./types";
5
+
6
+ export const rmCommand: CommandDefinition = {
7
+ name: "rm",
8
+ summary: "Remove a sandbox worktree",
9
+ description: "Remove a workbox sandbox worktree by name.",
10
+ usage: "workbox rm <name> [--force] [--unmanaged] [--delete-branch]",
11
+ run: async (context, args) => {
12
+ const parsed = parseArgsOrUsage({
13
+ args,
14
+ options: {
15
+ force: { type: "boolean" },
16
+ unmanaged: { type: "boolean" },
17
+ "delete-branch": { type: "boolean" },
18
+ },
19
+ allowPositionals: true,
20
+ strict: true,
21
+ });
22
+ const { positionals } = parsed;
23
+ const [name, ...rest] = positionals;
24
+ if (!name) {
25
+ const message = context.flags.nonInteractive
26
+ ? "Missing worktree name in non-interactive mode."
27
+ : "Missing worktree name.";
28
+ throw new UsageError(message);
29
+ }
30
+ if (rest.length > 0) {
31
+ throw new UsageError(`Unexpected arguments: ${rest.join(" ")}`);
32
+ }
33
+
34
+ const worktree = await getWorkboxWorktree({
35
+ repoRoot: context.repoRoot,
36
+ worktreesDir: context.config.worktrees.directory,
37
+ branchPrefix: context.config.worktrees.branch_prefix,
38
+ name,
39
+ });
40
+
41
+ if (!worktree.managed && parsed.values.unmanaged !== true) {
42
+ throw new UsageError(
43
+ `Refusing to remove unmanaged worktree "${name}". Re-run with --unmanaged to confirm.`
44
+ );
45
+ }
46
+
47
+ await removeWorktree({
48
+ repoRoot: context.repoRoot,
49
+ worktreesDir: context.config.worktrees.directory,
50
+ branchPrefix: context.config.worktrees.branch_prefix,
51
+ name,
52
+ force: parsed.values.force === true,
53
+ deleteBranch: parsed.values["delete-branch"] === true,
54
+ });
55
+
56
+ return {
57
+ message:
58
+ parsed.values["delete-branch"] === true
59
+ ? `Removed worktree "${worktree.name}" at ${worktree.path} and deleted branch ${worktree.managedBranch}.`
60
+ : `Removed worktree "${worktree.name}" at ${worktree.path}. No branches were deleted.`,
61
+ data: worktree,
62
+ };
63
+ },
64
+ };
@@ -0,0 +1,41 @@
1
+ import { runBootstrap } from "../bootstrap/runner";
2
+ import { UsageError } from "../ui/errors";
3
+ import { parseArgsOrUsage } from "./parse";
4
+ import type { CommandDefinition } from "./types";
5
+
6
+ export const setupCommand: CommandDefinition = {
7
+ name: "setup",
8
+ summary: "Run bootstrap steps",
9
+ description: "Run configured bootstrap steps for a workbox sandbox.",
10
+ usage: "workbox setup",
11
+ run: async (context, args) => {
12
+ const { positionals } = parseArgsOrUsage({
13
+ args,
14
+ allowPositionals: true,
15
+ strict: true,
16
+ });
17
+ if (positionals.length > 0) {
18
+ throw new UsageError(`Unexpected arguments: ${positionals.join(" ")}`);
19
+ }
20
+
21
+ if (!context.config.bootstrap.enabled) {
22
+ return {
23
+ message: "bootstrap is disabled in config.",
24
+ data: {
25
+ status: "disabled",
26
+ steps: context.config.bootstrap.steps.map((step) => step.name),
27
+ },
28
+ };
29
+ }
30
+
31
+ const result = await runBootstrap(context.config.bootstrap.steps, {
32
+ repoRoot: context.worktreeRoot,
33
+ mode: context.flags.json ? "capture" : "inherit",
34
+ });
35
+ return {
36
+ message: result.message,
37
+ data: result,
38
+ exitCode: result.exitCode,
39
+ };
40
+ },
41
+ };
@@ -0,0 +1,42 @@
1
+ import { getWorktreeStatus } from "../core/git";
2
+ import { UsageError } from "../ui/errors";
3
+ import { parseArgsOrUsage } from "./parse";
4
+ import type { CommandDefinition } from "./types";
5
+
6
+ export const statusCommand: CommandDefinition = {
7
+ name: "status",
8
+ summary: "Show sandbox status",
9
+ description: "Show status for workbox worktrees.",
10
+ usage: "workbox status [name]",
11
+ run: async (context, args) => {
12
+ const { positionals } = parseArgsOrUsage({
13
+ args,
14
+ allowPositionals: true,
15
+ strict: true,
16
+ });
17
+ if (positionals.length > 1) {
18
+ throw new UsageError(`Unexpected arguments: ${positionals.join(" ")}`);
19
+ }
20
+
21
+ const result = await getWorktreeStatus({
22
+ repoRoot: context.repoRoot,
23
+ worktreesDir: context.config.worktrees.directory,
24
+ branchPrefix: context.config.worktrees.branch_prefix,
25
+ name: positionals[0],
26
+ });
27
+ return {
28
+ message:
29
+ result.length === 0
30
+ ? "No workbox worktrees found."
31
+ : [
32
+ "Workbox status:",
33
+ ...result.map((item) => {
34
+ const branch = item.branch ?? "(detached)";
35
+ const managed = item.managed ? "managed" : "unmanaged";
36
+ return `- ${item.name}\t${branch}\t${managed}\t${item.clean ? "clean" : "dirty"}\t${item.path}`;
37
+ }),
38
+ ].join("\n"),
39
+ data: result,
40
+ };
41
+ },
42
+ };
@@ -0,0 +1,30 @@
1
+ import type { ResolvedWorkboxConfig } from "../core/config";
2
+
3
+ type GlobalFlags = {
4
+ help: boolean;
5
+ json: boolean;
6
+ nonInteractive: boolean;
7
+ };
8
+
9
+ type CommandContext = {
10
+ cwd: string;
11
+ repoRoot: string;
12
+ worktreeRoot: string;
13
+ config: ResolvedWorkboxConfig;
14
+ configPath: string;
15
+ flags: GlobalFlags;
16
+ };
17
+
18
+ type CommandResult = {
19
+ message: string;
20
+ data?: unknown;
21
+ exitCode?: number;
22
+ };
23
+
24
+ export type CommandDefinition = {
25
+ name: string;
26
+ summary: string;
27
+ description: string;
28
+ usage: string;
29
+ run: (context: CommandContext, args: string[]) => Promise<CommandResult>;
30
+ };