@bobbyg603/mog 0.1.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 ADDED
@@ -0,0 +1,146 @@
1
+ <img width="300" alt="claude moggin" src="https://github.com/user-attachments/assets/089db43c-7381-4e62-87bc-af2e7cd0129f" />
2
+
3
+
4
+ # mog — Sandboxed Claude Issue Mogging
5
+
6
+ One command to go from GitHub issue to pull request, powered by Claude Code running in a Docker sandbox.
7
+
8
+ ```
9
+ mog workingdevshero/automate-it 123
10
+ ```
11
+
12
+ That's it. `mog` will:
13
+
14
+ 1. Fetch the issue title, description, and labels via `gh` CLI
15
+ 2. Create a git worktree on a clean branch (`123-fix-broken-login`)
16
+ 3. Run Claude Code inside a persistent Docker sandbox (microVM) with `--dangerously-skip-permissions`
17
+ 4. Push the branch and open a PR that `Closes #123`
18
+
19
+ ## Prerequisites
20
+
21
+ - **macOS or Windows** (Docker sandbox microVMs require Docker Desktop)
22
+ - **Docker Desktop** — running and up to date (must support `docker sandbox`)
23
+ - **Bun** — install from [bun.sh](https://bun.sh)
24
+ - **GitHub CLI** (`gh`) — authenticated via `gh auth login`
25
+ - **Git** with push access to your target repos
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ bun install -g @bobbyg603/mog
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ ```bash
36
+ # 1. One-time setup: create sandbox & authenticate
37
+ mog init
38
+ # This launches Claude Code — use /login to authenticate with your Max subscription
39
+ # Once logged in, type /exit to return
40
+
41
+ # 2. Start mogging issues
42
+ mog workingdevshero/automate-it 123
43
+ ```
44
+
45
+ ## How authentication works
46
+
47
+ `mog init` creates a **persistent** Docker sandbox named `mog`. When it launches, you authenticate once using `/login` inside the Claude Code session. Your auth persists in the sandbox across all future `mog` runs — you never need to login again.
48
+
49
+ If your session ever expires, just run `mog init` again to re-authenticate.
50
+
51
+ ## Usage
52
+
53
+ ```bash
54
+ # One-time setup
55
+ mog init
56
+
57
+ # Basic usage
58
+ mog owner/repo issue_number
59
+
60
+ # Examples
61
+ mog workingdevshero/automate-it 123
62
+ mog sparx-tech/hub-firmware 45
63
+ ```
64
+
65
+ ## How it works
66
+
67
+ ```
68
+ ┌──────────────────────────────────────────────────────────┐
69
+ │ Host machine │
70
+ │ │
71
+ │ 1. gh issue view #123 → fetch title, body, labels │
72
+ │ 2. git worktree add → clean branch from default branch │
73
+ │ │
74
+ │ ┌────────────────────────────────────────────────────┐ │
75
+ │ │ Docker sandbox "mog" (persistent microVM) │ │
76
+ │ │ │ │
77
+ │ │ • ~/mog-repos mounted as workspace │ │
78
+ │ │ • Auth persists across runs (login once) │ │
79
+ │ │ • Isolated from host (own Docker daemon) │ │
80
+ │ │ • claude --dangerously-skip-permissions -p "..." │ │
81
+ │ │ • Reads code, implements fix, commits │ │
82
+ │ └────────────────────────────────────────────────────┘ │
83
+ │ │
84
+ │ 3. git push origin branch │
85
+ │ 4. gh pr create --body "Closes #123" │
86
+ └──────────────────────────────────────────────────────────┘
87
+ ```
88
+
89
+ ## Configuration
90
+
91
+ | Environment Variable | Default | Description |
92
+ |---|---|---|
93
+ | `MOG_REPOS_DIR` | `~/mog-repos` | Where repos are cloned and worktrees created (also the sandbox workspace) |
94
+
95
+ ## Worktree management
96
+
97
+ `mog` uses bare clones and git worktrees so you can run multiple issues concurrently without conflicts:
98
+
99
+ ```
100
+ ~/mog-repos/
101
+ owner/
102
+ repo/ ← bare clone (or full clone)
103
+ repo-worktrees/
104
+ 123-fix-broken-login/ ← worktree for issue #123
105
+ 456-add-dark-mode/ ← worktree for issue #456
106
+ ```
107
+
108
+ Clean up when done:
109
+
110
+ ```bash
111
+ cd ~/mog-repos/owner/repo
112
+ git worktree remove ../repo-worktrees/123-fix-broken-login
113
+ ```
114
+
115
+ ## Security notes
116
+
117
+ - Claude Code runs inside a **microVM** via Docker sandbox — it has its own Docker daemon and cannot access your host system, terminal, or files outside `~/mog-repos`.
118
+ - `--dangerously-skip-permissions` is safe here because the sandbox provides the isolation boundary.
119
+ - `gh` credentials stay on your host — the sandbox has **no access** to your GitHub token. Pushing and PR creation happen on the host after Claude finishes.
120
+ - The sandbox has network access (required for the Anthropic API).
121
+
122
+ ## Troubleshooting
123
+
124
+ **"Docker sandbox not available"** — Make sure Docker Desktop is running and up to date.
125
+
126
+ **"Sandbox 'mog' not found"** — Run `mog init` first to create the sandbox and authenticate.
127
+
128
+ **"Failed to fetch issue"** — Check `gh auth status` and verify the repo/issue exist.
129
+
130
+ **"No changes detected"** — Claude may have struggled with the issue. Check the worktree manually, or re-run with a more detailed issue description.
131
+
132
+ **"Failed to push"** — Ensure `gh` is authenticated with push access. Try `gh auth login` and select HTTPS.
133
+
134
+ ## Managing the sandbox
135
+
136
+ ```bash
137
+ # List sandboxes
138
+ docker sandbox ls
139
+
140
+ # Stop the sandbox (preserves auth)
141
+ docker sandbox stop mog
142
+
143
+ # Remove and recreate (you'll need to /login again)
144
+ docker sandbox rm mog
145
+ mog init
146
+ ```
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@bobbyg603/mog",
3
+ "version": "0.1.0",
4
+ "description": "One command to go from GitHub issue to pull request, powered by Claude Code in a Docker sandbox",
5
+ "module": "src/index.ts",
6
+ "type": "module",
7
+ "bin": {
8
+ "mog": "src/index.ts"
9
+ },
10
+ "files": [
11
+ "src"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/workingdevshero/mog.git"
16
+ },
17
+ "keywords": [
18
+ "claude",
19
+ "claude-code",
20
+ "github",
21
+ "automation",
22
+ "docker",
23
+ "sandbox"
24
+ ],
25
+ "license": "MIT",
26
+ "devDependencies": {
27
+ "@types/bun": "latest"
28
+ },
29
+ "peerDependencies": {
30
+ "typescript": "^5"
31
+ }
32
+ }
package/src/github.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { log } from "./log";
2
+
3
+ export interface Issue {
4
+ title: string;
5
+ body: string;
6
+ labels: string;
7
+ }
8
+
9
+ export async function fetchIssue(repo: string, issueNum: string): Promise<Issue> {
10
+ log.info(`Fetching issue #${issueNum} from ${repo}...`);
11
+
12
+ const proc = Bun.spawnSync([
13
+ "gh", "issue", "view", issueNum,
14
+ "--repo", repo,
15
+ "--json", "title,body,labels",
16
+ ]);
17
+
18
+ if (proc.exitCode !== 0) {
19
+ log.die(`Failed to fetch issue #${issueNum}. Check repo name and issue number.`);
20
+ }
21
+
22
+ const json = JSON.parse(proc.stdout.toString());
23
+
24
+ return {
25
+ title: json.title,
26
+ body: json.body || "No description provided.",
27
+ labels: json.labels?.map((l: { name: string }) => l.name).join(", ") || "none",
28
+ };
29
+ }
30
+
31
+ export async function pushAndCreatePR(
32
+ repo: string,
33
+ worktreeDir: string,
34
+ branchName: string,
35
+ defaultBranch: string,
36
+ issueNum: string,
37
+ issue: Issue
38
+ ) {
39
+ // Check for unpushed commits or uncommitted changes
40
+ const unpushed = Bun.spawnSync(["git", "log", `origin/${defaultBranch}..HEAD`, "--oneline"], { cwd: worktreeDir });
41
+ const diffCheck = Bun.spawnSync(["git", "diff", "--quiet"], { cwd: worktreeDir });
42
+ const cachedCheck = Bun.spawnSync(["git", "diff", "--cached", "--quiet"], { cwd: worktreeDir });
43
+
44
+ const hasUnpushed = unpushed.stdout.toString().trim().length > 0;
45
+ const hasUncommitted = diffCheck.exitCode !== 0 || cachedCheck.exitCode !== 0;
46
+
47
+ if (!hasUnpushed && !hasUncommitted) {
48
+ log.warn("No changes detected. Claude may not have made any modifications.");
49
+ log.warn(`Worktree: ${worktreeDir}`);
50
+ return;
51
+ }
52
+
53
+ // Stage any unstaged changes Claude might have left
54
+ if (hasUncommitted) {
55
+ log.info("Staging uncommitted changes...");
56
+ Bun.spawnSync(["git", "add", "-A"], { cwd: worktreeDir });
57
+ Bun.spawnSync(["git", "commit", "-m", `fix: address issue #${issueNum} - ${issue.title}`], { cwd: worktreeDir });
58
+ }
59
+
60
+ // Push
61
+ log.info(`Pushing branch '${branchName}' to origin...`);
62
+ const push = Bun.spawnSync(["git", "push", "-u", "origin", branchName], { cwd: worktreeDir });
63
+ if (push.exitCode !== 0) {
64
+ log.die("Failed to push. Check your git credentials.");
65
+ }
66
+ log.ok("Branch pushed.");
67
+
68
+ // Create PR
69
+ log.info("Opening pull request...");
70
+
71
+ const prBody = `## Summary
72
+
73
+ Closes #${issueNum}
74
+
75
+ This PR was generated by [mog](https://github.com/workingdevshero/mog) using Claude Code in a Docker sandbox.
76
+
77
+ ### Issue: ${issue.title}
78
+
79
+ ${issue.body}
80
+
81
+ ---
82
+ *Please review the changes carefully before merging.*`;
83
+
84
+ const prTitle = `fix: ${issue.title} [#${issueNum}]`;
85
+
86
+ const pr = Bun.spawnSync([
87
+ "gh", "pr", "create",
88
+ "--repo", repo,
89
+ "--base", defaultBranch,
90
+ "--head", branchName,
91
+ "--title", prTitle,
92
+ "--body", prBody,
93
+ ], { cwd: worktreeDir });
94
+
95
+ if (pr.exitCode !== 0) {
96
+ log.die("Failed to create PR. You may need to push first or the PR may already exist.");
97
+ }
98
+
99
+ const prUrl = pr.stdout.toString().trim();
100
+ log.ok("Pull request created!");
101
+ console.log(`\x1b[0;32m${prUrl}\x1b[0m`);
102
+
103
+ console.log();
104
+ log.ok(`All done! Issue #${issueNum} → Branch '${branchName}' → PR opened.`);
105
+ log.info(`Worktree: ${worktreeDir}`);
106
+ log.info(`To clean up the worktree later: git worktree remove ${worktreeDir}`);
107
+ }
package/src/index.ts ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { fetchIssue } from "./github";
4
+ import { ensureRepo, createWorktree } from "./worktree";
5
+ import { ensureSandbox, runClaude } from "./sandbox";
6
+ import { pushAndCreatePR } from "./github";
7
+ import { log } from "./log";
8
+
9
+ const SANDBOX_NAME = "mog";
10
+
11
+ async function init() {
12
+ log.info("Initializing mog sandbox...");
13
+
14
+ const reposDir = getReposDir();
15
+ const exists = await sandboxExists(SANDBOX_NAME);
16
+
17
+ if (exists) {
18
+ log.warn(`Sandbox '${SANDBOX_NAME}' already exists.`);
19
+ log.info("Launching sandbox so you can authenticate with /login...");
20
+ } else {
21
+ log.info(`Creating persistent sandbox '${SANDBOX_NAME}'...`);
22
+ log.info(`Workspace: ${reposDir}`);
23
+ await run(["docker", "sandbox", "create", "--name", SANDBOX_NAME, "claude", reposDir]);
24
+ log.ok("Sandbox created.");
25
+ console.log();
26
+ log.info("Launching sandbox — authenticate with /login to use your Max subscription.");
27
+ log.info("Once logged in, type /exit or Ctrl+C to return.");
28
+ console.log();
29
+ }
30
+
31
+ const proc = Bun.spawn(["docker", "sandbox", "run", SANDBOX_NAME], {
32
+ stdin: "inherit",
33
+ stdout: "inherit",
34
+ stderr: "inherit",
35
+ });
36
+ await proc.exited;
37
+
38
+ log.ok("mog is ready. Run: mog <owner/repo> <issue_number>");
39
+ }
40
+
41
+ async function main() {
42
+ const args = process.argv.slice(2);
43
+
44
+ // Validate dependencies
45
+ for (const cmd of ["gh", "git", "docker"]) {
46
+ const which = Bun.spawnSync(["which", cmd]);
47
+ if (which.exitCode !== 0) {
48
+ log.die(`Required command not found: ${cmd}`);
49
+ }
50
+ }
51
+
52
+ // Check docker sandbox is available
53
+ const sandboxCheck = Bun.spawnSync(["docker", "sandbox", "ls"]);
54
+ if (sandboxCheck.exitCode !== 0) {
55
+ log.die("Docker sandbox not available. Make sure Docker Desktop is running and up to date.");
56
+ }
57
+
58
+ if (args[0] === "init") {
59
+ await init();
60
+ return;
61
+ }
62
+
63
+ if (args.length < 2) {
64
+ console.log("Usage:");
65
+ console.log(" mog init — one-time setup (create sandbox & login)");
66
+ console.log(" mog <owner/repo> <issue_num> — fetch issue, run Claude, open PR");
67
+ console.log();
68
+ console.log("Example:");
69
+ console.log(" mog init");
70
+ console.log(" mog workingdevshero/automate-it 123");
71
+ return;
72
+ }
73
+
74
+ const repo = args[0];
75
+ const issueNum = args[1];
76
+ const [owner, repoName] = repo.split("/");
77
+
78
+ if (!owner || !repoName) {
79
+ log.die("Invalid repo format. Use: owner/repo");
80
+ }
81
+
82
+ // Verify sandbox exists
83
+ if (!(await sandboxExists(SANDBOX_NAME))) {
84
+ log.die(`Sandbox '${SANDBOX_NAME}' not found. Run 'mog init' first.`);
85
+ }
86
+
87
+ // Fetch issue
88
+ const issue = await fetchIssue(repo, issueNum);
89
+ log.ok(`Issue: ${issue.title}`);
90
+
91
+ // Ensure repo & worktree
92
+ const reposDir = getReposDir();
93
+ const { defaultBranch } = await ensureRepo(repo, owner, repoName, reposDir);
94
+ log.info(`Default branch: ${defaultBranch}`);
95
+
96
+ const { worktreeDir, branchName } = await createWorktree(
97
+ reposDir, owner, repoName, defaultBranch, issueNum, issue.title
98
+ );
99
+
100
+ // Build prompt
101
+ const prompt = buildPrompt(repo, issueNum, issue);
102
+
103
+ // Run Claude in sandbox
104
+ log.info("Launching Claude Code in sandbox...");
105
+ log.info(`Branch: ${branchName}`);
106
+ log.info(`Worktree: ${worktreeDir}`);
107
+ console.log();
108
+
109
+ await runClaude(SANDBOX_NAME, worktreeDir, prompt);
110
+
111
+ // Push and create PR
112
+ await pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue);
113
+ }
114
+
115
+ function getReposDir(): string {
116
+ return process.env.MOG_REPOS_DIR || `${process.env.HOME}/mog-repos`;
117
+ }
118
+
119
+ async function sandboxExists(name: string): Promise<boolean> {
120
+ const result = Bun.spawnSync(["docker", "sandbox", "ls"]);
121
+ const output = result.stdout.toString();
122
+ return output.includes(name);
123
+ }
124
+
125
+ async function run(cmd: string[]): Promise<string> {
126
+ const proc = Bun.spawnSync(cmd);
127
+ if (proc.exitCode !== 0) {
128
+ throw new Error(`Command failed: ${cmd.join(" ")}\n${proc.stderr.toString()}`);
129
+ }
130
+ return proc.stdout.toString().trim();
131
+ }
132
+
133
+ function buildPrompt(repo: string, issueNum: string, issue: { title: string; body: string; labels: string }): string {
134
+ return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
135
+
136
+ ## Issue: ${issue.title}
137
+
138
+ ### Description
139
+ ${issue.body}
140
+
141
+ ### Labels
142
+ ${issue.labels}
143
+
144
+ ## Instructions
145
+ 1. Read and understand the codebase structure first.
146
+ 2. Implement the changes described in the issue above.
147
+ 3. Write clean, well-documented code that follows the existing project conventions.
148
+ 4. Add or update tests if applicable.
149
+ 5. Make sure the code builds/lints without errors if there's a build system.
150
+ 6. Commit your changes with a clear commit message referencing issue #${issueNum}.
151
+
152
+ When you are done, make a single commit (or a small, logical set of commits) with
153
+ a message like: "fix: <short description> (#${issueNum})"`;
154
+ }
155
+
156
+ main().catch((err) => {
157
+ log.die(err.message);
158
+ });
package/src/log.ts ADDED
@@ -0,0 +1,18 @@
1
+ const RED = "\x1b[0;31m";
2
+ const GREEN = "\x1b[0;32m";
3
+ const YELLOW = "\x1b[1;33m";
4
+ const CYAN = "\x1b[0;36m";
5
+ const NC = "\x1b[0m";
6
+
7
+ export const log = {
8
+ info: (msg: string) => console.log(`${CYAN}[mog]${NC} ${msg}`),
9
+ ok: (msg: string) => console.log(`${GREEN}[mog]${NC} ${msg}`),
10
+ warn: (msg: string) => console.log(`${YELLOW}[mog]${NC} ${msg}`),
11
+ err: (msg: string) => console.error(`${RED}[mog]${NC} ${msg}`),
12
+ die: (msg: string) => {
13
+ log.err(msg);
14
+ process.exit(1);
15
+ },
16
+ tool: (name: string, detail: string) => console.log(`${CYAN}[${name}]${NC} ${detail}`),
17
+ done: (msg: string) => console.log(`\n${GREEN}[done]${NC} ${msg}`),
18
+ };
package/src/sandbox.ts ADDED
@@ -0,0 +1,131 @@
1
+ import { log } from "./log";
2
+
3
+ interface StreamEvent {
4
+ type: string;
5
+ subtype?: string;
6
+ message?: {
7
+ content?: Array<{
8
+ type: string;
9
+ text?: string;
10
+ name?: string;
11
+ input?: Record<string, unknown>;
12
+ }>;
13
+ };
14
+ result?: string;
15
+ is_error?: boolean;
16
+ }
17
+
18
+ export async function ensureSandbox(name: string, reposDir: string): Promise<void> {
19
+ const ls = Bun.spawnSync(["docker", "sandbox", "ls"]);
20
+ if (ls.stdout.toString().includes(name)) {
21
+ return;
22
+ }
23
+
24
+ log.info(`Creating persistent sandbox '${name}'...`);
25
+ const create = Bun.spawnSync(["docker", "sandbox", "create", "--name", name, "claude", reposDir]);
26
+ if (create.exitCode !== 0) {
27
+ log.die(`Failed to create sandbox: ${create.stderr.toString()}`);
28
+ }
29
+ log.ok("Sandbox created.");
30
+ }
31
+
32
+ export async function runClaude(sandboxName: string, worktreeDir: string, prompt: string): Promise<void> {
33
+ const proc = Bun.spawn([
34
+ "docker", "sandbox", "exec",
35
+ "-w", worktreeDir,
36
+ sandboxName,
37
+ "claude", "--dangerously-skip-permissions",
38
+ "--verbose", "--output-format", "stream-json",
39
+ "-p", prompt,
40
+ ], {
41
+ stdout: "pipe",
42
+ stderr: "pipe",
43
+ });
44
+
45
+ // Stream and parse JSON lines from stdout
46
+ const reader = proc.stdout.getReader();
47
+ const decoder = new TextDecoder();
48
+ let buffer = "";
49
+
50
+ while (true) {
51
+ const { done, value } = await reader.read();
52
+ if (done) break;
53
+
54
+ buffer += decoder.decode(value, { stream: true });
55
+
56
+ // Process complete lines
57
+ const lines = buffer.split("\n");
58
+ buffer = lines.pop() || "";
59
+
60
+ for (const line of lines) {
61
+ if (!line.trim()) continue;
62
+
63
+ try {
64
+ const event: StreamEvent = JSON.parse(line);
65
+ printEvent(event);
66
+ } catch {
67
+ // Skip malformed JSON lines
68
+ }
69
+ }
70
+ }
71
+
72
+ // Process any remaining buffer
73
+ if (buffer.trim()) {
74
+ try {
75
+ const event: StreamEvent = JSON.parse(buffer);
76
+ printEvent(event);
77
+ } catch {
78
+ // Skip
79
+ }
80
+ }
81
+
82
+ const exitCode = await proc.exited;
83
+
84
+ if (exitCode !== 0) {
85
+ const stderr = await new Response(proc.stderr).text();
86
+ if (stderr.trim()) {
87
+ log.warn(stderr.trim());
88
+ }
89
+ log.warn(`Claude Code exited with code ${exitCode}.`);
90
+ }
91
+ }
92
+
93
+ function printEvent(event: StreamEvent): void {
94
+ if (event.type === "assistant" && event.message?.content) {
95
+ for (const block of event.message.content) {
96
+ if (block.type === "text" && block.text) {
97
+ console.log(block.text);
98
+ } else if (block.type === "tool_use" && block.name) {
99
+ const detail = getToolDetail(block.name, block.input);
100
+ log.tool(block.name, detail);
101
+ }
102
+ }
103
+ } else if (event.type === "result") {
104
+ if (event.is_error) {
105
+ log.err(event.result || "Unknown error");
106
+ } else if (event.result) {
107
+ log.done(event.result.slice(0, 200));
108
+ }
109
+ }
110
+ }
111
+
112
+ function getToolDetail(name: string, input?: Record<string, unknown>): string {
113
+ if (!input) return "";
114
+
115
+ switch (name) {
116
+ case "Read":
117
+ return String(input.file_path || "");
118
+ case "Edit":
119
+ return String(input.file_path || "");
120
+ case "Write":
121
+ return String(input.file_path || "");
122
+ case "Bash":
123
+ return String(input.description || input.command || "");
124
+ case "Glob":
125
+ return String(input.pattern || "");
126
+ case "Grep":
127
+ return String(input.pattern || "");
128
+ default:
129
+ return JSON.stringify(input).slice(0, 120);
130
+ }
131
+ }
@@ -0,0 +1,91 @@
1
+ import { log } from "./log";
2
+
3
+ export async function ensureRepo(
4
+ repo: string,
5
+ owner: string,
6
+ repoName: string,
7
+ reposDir: string
8
+ ): Promise<{ defaultBranch: string }> {
9
+ const repoDir = `${reposDir}/${owner}/${repoName}`;
10
+ const fs = await import("fs");
11
+
12
+ if (!fs.existsSync(repoDir)) {
13
+ log.info(`Cloning ${repo} into ${repoDir}...`);
14
+ fs.mkdirSync(`${reposDir}/${owner}`, { recursive: true });
15
+
16
+ const clone = Bun.spawnSync(["gh", "repo", "clone", repo, repoDir], {
17
+ stdout: "inherit",
18
+ stderr: "inherit",
19
+ });
20
+
21
+ if (clone.exitCode !== 0) {
22
+ log.die(`Failed to clone ${repo}.`);
23
+ }
24
+ }
25
+
26
+ // Get default branch
27
+ const branchProc = Bun.spawnSync([
28
+ "gh", "repo", "view", repo,
29
+ "--json", "defaultBranchRef",
30
+ "--jq", ".defaultBranchRef.name",
31
+ ]);
32
+
33
+ if (branchProc.exitCode !== 0) {
34
+ log.die("Failed to determine default branch.");
35
+ }
36
+
37
+ return { defaultBranch: branchProc.stdout.toString().trim() };
38
+ }
39
+
40
+ export async function createWorktree(
41
+ reposDir: string,
42
+ owner: string,
43
+ repoName: string,
44
+ defaultBranch: string,
45
+ issueNum: string,
46
+ issueTitle: string
47
+ ): Promise<{ worktreeDir: string; branchName: string }> {
48
+ const safeTitle = issueTitle
49
+ .toLowerCase()
50
+ .replace(/[^a-z0-9]/g, "-")
51
+ .replace(/-+/g, "-")
52
+ .replace(/^-|-$/g, "")
53
+ .slice(0, 50);
54
+
55
+ const branchName = `${issueNum}-${safeTitle}`;
56
+ const repoDir = `${reposDir}/${owner}/${repoName}`;
57
+ const worktreeDir = `${reposDir}/${owner}/${repoName}-worktrees/${branchName}`;
58
+
59
+ const fs = await import("fs");
60
+
61
+ if (fs.existsSync(worktreeDir)) {
62
+ log.warn(`Worktree already exists at ${worktreeDir}, reusing.`);
63
+ return { worktreeDir, branchName };
64
+ }
65
+
66
+ log.info(`Creating worktree for branch '${branchName}'...`);
67
+
68
+ // Fetch latest
69
+ Bun.spawnSync(["git", "fetch", "origin", defaultBranch], { cwd: repoDir });
70
+
71
+ // Try creating branch + worktree
72
+ const result = Bun.spawnSync(
73
+ ["git", "worktree", "add", "-b", branchName, worktreeDir, `origin/${defaultBranch}`],
74
+ { cwd: repoDir }
75
+ );
76
+
77
+ if (result.exitCode !== 0) {
78
+ // Try using existing branch
79
+ const fallback = Bun.spawnSync(
80
+ ["git", "worktree", "add", worktreeDir, branchName],
81
+ { cwd: repoDir }
82
+ );
83
+
84
+ if (fallback.exitCode !== 0) {
85
+ log.die(`Failed to create worktree. Branch '${branchName}' may already exist.`);
86
+ }
87
+ }
88
+
89
+ log.ok(`Worktree created at ${worktreeDir}`);
90
+ return { worktreeDir, branchName };
91
+ }