UNPKG

@camoneart/maestro

Version:

A CLI tool that conducts Git worktrees like an orchestra and accelerates parallel development with Claude Code

1,351 lines (1,332 loc) 323 kB
#!/usr/bin/env node // src/cli.ts import { Command as Command21 } from "commander"; import chalk24 from "chalk"; import { readFileSync as readFileSync4 } from "fs"; import { fileURLToPath as fileURLToPath2 } from "url"; import { dirname as dirname2, join as join2 } from "path"; // src/commands/create.ts import { Command } from "commander"; import chalk3 from "chalk"; import ora from "ora"; import inquirer3 from "inquirer"; // src/core/git.ts import simpleGit from "simple-git"; // src/core/config.ts import { z } from "zod"; import Conf from "conf"; import path from "path"; import fs from "fs/promises"; var ConfigSchema = z.object({ // Git worktree設定 worktrees: z.object({ // worktreeを作成するディレクトリ(デフォルト: .git/orchestrations) path: z.string().optional(), // ブランチ名のプレフィックス branchPrefix: z.string().optional(), // ディレクトリ名のプレフィックス(デフォルト: 空文字列) directoryPrefix: z.string().optional() }).optional(), // 開発環境設定 development: z.object({ // 自動でnpm installを実行 autoSetup: z.boolean().default(true), // 同期するファイル(.envなど) syncFiles: z.array(z.string()).default([".env", ".env.local"]), // デフォルトのエディタ defaultEditor: z.enum(["vscode", "cursor", "none"]).default("cursor") }).optional(), // tmux統合設定 tmux: z.object({ enabled: z.boolean().default(false), // 新規ウィンドウかペインか openIn: z.enum(["window", "pane"]).default("window"), // セッション名の命名規則 sessionNaming: z.string().default("{branch}") }).optional(), // Claude Code統合設定 claude: z.object({ // CLAUDE.mdの処理方法 markdownMode: z.enum(["shared", "split"]).default("shared") }).optional(), // GitHub統合設定 github: z.object({ // 自動でfetchを実行 autoFetch: z.boolean().default(true), // ブランチ命名規則 branchNaming: z.object({ // PR用のテンプレート (例: "pr-{number}-{title}") prTemplate: z.string().default("pr-{number}"), // Issue用のテンプレート (例: "issue-{number}-{title}") issueTemplate: z.string().default("issue-{number}") }).optional() }).optional(), // UI表示設定 ui: z.object({ // パス表示形式 ('absolute' | 'relative') pathDisplay: z.enum(["absolute", "relative"]).default("absolute") }).optional(), // カスタムコマンドとファイルコピー設定 hooks: z.object({ // worktree作成後に実行(文字列または配列) afterCreate: z.union([z.string(), z.array(z.string())]).optional(), // worktree削除前に実行 beforeDelete: z.string().optional() }).optional(), // worktree作成時の処理 postCreate: z.object({ // コピーするファイル(gitignoreファイルも含む) copyFiles: z.array(z.string()).optional(), // 実行するコマンド commands: z.array(z.string()).optional() }).optional() }); var DEFAULT_CONFIG = { worktrees: { path: "../maestro-{branch}", directoryPrefix: "" }, development: { autoSetup: true, syncFiles: [".env", ".env.local"], defaultEditor: "cursor" }, tmux: { enabled: false, openIn: "window", sessionNaming: "{branch}" }, claude: { markdownMode: "shared" }, github: { autoFetch: true }, ui: { pathDisplay: "absolute" }, hooks: {} }; var ConfigManager = class { conf; projectConfig = null; userConfig = null; constructor() { this.conf = new Conf({ projectName: "maestro", defaults: DEFAULT_CONFIG }); } async loadProjectConfig() { try { await this.loadUserConfig(); const configPaths = [ path.join(process.cwd(), ".maestro.json"), path.join(process.cwd(), ".maestrorc.json"), path.join(process.cwd(), "maestro.config.json"), // グローバル設定ファイル path.join(process.env.HOME || "~", ".maestrorc"), path.join(process.env.HOME || "~", ".maestrorc.json") ]; for (const configPath of configPaths) { try { const configData = await fs.readFile(configPath, "utf-8"); const parsedConfig = JSON.parse(configData); this.projectConfig = ConfigSchema.parse(parsedConfig); return; } catch { } } } catch (error) { console.error("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u8A2D\u5B9A\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error); } } async loadUserConfig() { try { const userConfigPath = path.join(process.cwd(), ".maestro.local.json"); const configData = await fs.readFile(userConfigPath, "utf-8"); const parsedConfig = JSON.parse(configData); this.userConfig = ConfigSchema.parse(parsedConfig); } catch { this.userConfig = null; } } // 設定を取得(ユーザー設定 > プロジェクト設定 > グローバル設定 > デフォルト) get(key) { if (this.userConfig && this.userConfig[key] !== void 0) { return this.userConfig[key]; } if (this.projectConfig && this.projectConfig[key] !== void 0) { return this.projectConfig[key]; } return this.conf.get(key) ?? DEFAULT_CONFIG[key]; } // 設定を更新(グローバル設定のみ) set(key, value) { this.conf.set(key, value); } // 全設定を取得 getAll() { const globalConfig = this.conf.store; return { ...DEFAULT_CONFIG, ...globalConfig, ...this.projectConfig || {}, ...this.userConfig || {} }; } // 設定ファイルのパスを取得 getConfigPath() { return this.conf.path; } // ドット記法で設定値を取得 getConfigValue(keyPath) { const keys = keyPath.split("."); const config = this.getAll(); return keys.reduce((obj, key) => { if (obj && typeof obj === "object" && key in obj) { return obj[key]; } return void 0; }, config); } // ドット記法で設定値を設定 async setConfigValue(keyPath, value, target = "project") { if (target === "user") { await this.setUserConfigValue(keyPath, value); } else { await this.setProjectConfigValue(keyPath, value); } } // ユーザー設定を設定 async setUserConfigValue(keyPath, value) { const configPath = path.join(process.cwd(), ".maestro.local.json"); let userConfig = {}; try { const configData = await fs.readFile(configPath, "utf-8"); userConfig = JSON.parse(configData); } catch { } const keys = keyPath.split("."); let current = userConfig; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!key) continue; if (!current[key] || typeof current[key] !== "object") { current[key] = {}; } current = current[key]; } const lastKey = keys[keys.length - 1]; if (lastKey) { current[lastKey] = this.parseValue(value); } const validatedConfig = ConfigSchema.parse(userConfig); await fs.writeFile(configPath, JSON.stringify(validatedConfig, null, 2) + "\n", "utf-8"); this.userConfig = validatedConfig; } // プロジェクト設定を設定 async setProjectConfigValue(keyPath, value) { const configPath = path.join(process.cwd(), ".maestro.json"); let projectConfig = {}; try { const configData = await fs.readFile(configPath, "utf-8"); projectConfig = JSON.parse(configData); } catch { } const keys = keyPath.split("."); let current = projectConfig; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!key) continue; if (!current[key] || typeof current[key] !== "object") { current[key] = {}; } current = current[key]; } const lastKey = keys[keys.length - 1]; if (lastKey) { current[lastKey] = this.parseValue(value); } const validatedConfig = ConfigSchema.parse(projectConfig); await fs.writeFile(configPath, JSON.stringify(validatedConfig, null, 2) + "\n", "utf-8"); this.projectConfig = validatedConfig; } // 設定値をリセット(デフォルトに戻す) async resetConfigValue(keyPath) { const configPath = path.join(process.cwd(), ".maestro.json"); let projectConfig = {}; try { const configData = await fs.readFile(configPath, "utf-8"); projectConfig = JSON.parse(configData); } catch { return; } const keys = keyPath.split("."); let current = projectConfig; const parents = []; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!key || !current[key]) { return; } parents.push({ obj: current, key }); current = current[key]; } const lastKey = keys[keys.length - 1]; if (lastKey && current[lastKey] !== void 0) { delete current[lastKey]; } this.cleanEmptyObjects(projectConfig, keyPath.split(".").slice(0, -1)); await fs.writeFile(configPath, JSON.stringify(projectConfig, null, 2) + "\n", "utf-8"); this.projectConfig = Object.keys(projectConfig).length > 0 ? projectConfig : null; } // 値の型変換 parseValue(value) { if (typeof value === "string") { if (value === "true") return true; if (value === "false") return false; if (/^\d+$/.test(value)) return parseInt(value, 10); if (/^\d+\.\d+$/.test(value)) return parseFloat(value); } return value; } // 空のオブジェクトを削除 cleanEmptyObjects(obj, keys) { if (keys.length === 0) return; let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!key || !current[key]) return; current = current[key]; } const lastKey = keys[keys.length - 1]; if (lastKey && current[lastKey] && typeof current[lastKey] === "object" && Object.keys(current[lastKey]).length === 0) { delete current[lastKey]; this.cleanEmptyObjects(obj, keys.slice(0, -1)); } } // プロジェクト設定ファイルの作成 async createProjectConfig(configPath) { const targetPath = configPath || path.join(process.cwd(), ".maestro.json"); const exampleConfig = { worktrees: { path: "../maestro-{branch}", branchPrefix: "feature/", directoryPrefix: "maestro-" }, development: { autoSetup: true, syncFiles: [".env", ".env.local"], defaultEditor: "cursor" }, tmux: { enabled: true, openIn: "window", sessionNaming: "{branch}" }, claude: { markdownMode: "shared" }, github: { autoFetch: true, branchNaming: { prTemplate: "pr-{number}", issueTemplate: "issue-{number}" } }, ui: { pathDisplay: "absolute" }, hooks: { afterCreate: "npm install", beforeDelete: 'echo "\u30AA\u30FC\u30B1\u30B9\u30C8\u30E9\u30E1\u30F3\u30D0\u30FC\u3092\u89E3\u6563\u3057\u307E\u3059: $MAESTRO_BRANCH"' }, postCreate: { copyFiles: [".env", ".env.local"], commands: ["pnpm install", "pnpm run dev"] } }; await fs.writeFile(targetPath, JSON.stringify(exampleConfig, null, 2) + "\n", "utf-8"); } }; // src/core/git.ts import path2 from "path"; import fs2 from "fs/promises"; import chalk from "chalk"; import inquirer from "inquirer"; var GitWorktreeManager = class { git; configManager; constructor(baseDir) { this.git = simpleGit(baseDir || process.cwd()); this.configManager = new ConfigManager(); } async createWorktree(branchName, baseBranch, skipDirCheck) { await this.checkBranchNameCollision(branchName); await this.configManager.loadProjectConfig(); const worktreeConfig = this.configManager.get("worktrees"); const directoryPrefix = worktreeConfig?.directoryPrefix || ""; const repoRoot = await this.getRepositoryRoot(); const worktreePath = path2.join(repoRoot, "..", `${directoryPrefix}${branchName}`); if (!skipDirCheck) { const dirExists = await this.checkDirectoryExists(worktreePath); if (dirExists) { const action = await this.handleExistingDirectory(worktreePath, branchName); if (action === "cancel") { throw new Error("\u30EF\u30FC\u30AF\u30C4\u30EA\u30FC\u306E\u4F5C\u6210\u304C\u30AD\u30E3\u30F3\u30BB\u30EB\u3055\u308C\u307E\u3057\u305F"); } else if (action === "rename") { const branches = await this.getAllBranches(); const allBranches = [ ...branches.local, ...branches.remote.map((r) => r.replace(/^[^/]+\//, "")) ]; const alternativeName = this.generateAlternativeBranchName(branchName, allBranches); console.log(chalk.yellow(` \u65B0\u3057\u3044\u30D6\u30E9\u30F3\u30C1\u540D: ${alternativeName}`)); return this.createWorktree(alternativeName, baseBranch, true); } else if (action === "delete") { await fs2.rm(worktreePath, { recursive: true, force: true }); console.log( chalk.gray(`\u{1F5D1}\uFE0F \u65E2\u5B58\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u524A\u9664\u3057\u307E\u3057\u305F: ${path2.basename(worktreePath)}`) ); } } } if (!baseBranch) { const status = await this.git.status(); baseBranch = status.current || "main"; } await this.git.raw(["worktree", "add", "-b", branchName, worktreePath, baseBranch]); return path2.resolve(worktreePath); } async attachWorktree(existingBranch, skipDirCheck) { await this.configManager.loadProjectConfig(); const worktreeConfig = this.configManager.get("worktrees"); const directoryPrefix = worktreeConfig?.directoryPrefix || ""; const repoRoot = await this.getRepositoryRoot(); const safeBranchName = existingBranch.replace(/\//g, "-"); const worktreePath = path2.join(repoRoot, "..", `${directoryPrefix}${safeBranchName}`); if (!skipDirCheck) { const dirExists = await this.checkDirectoryExists(worktreePath); if (dirExists) { const action = await this.handleExistingDirectory(worktreePath, safeBranchName); if (action === "cancel") { throw new Error("\u30EF\u30FC\u30AF\u30C4\u30EA\u30FC\u306E\u4F5C\u6210\u304C\u30AD\u30E3\u30F3\u30BB\u30EB\u3055\u308C\u307E\u3057\u305F"); } else if (action === "rename") { const branches = await this.getAllBranches(); const allBranches = [ ...branches.local, ...branches.remote.map((r) => r.replace(/^[^/]+\//, "")) ]; const alternativeName = this.generateAlternativeBranchName(safeBranchName, allBranches); const newWorktreePath = path2.join(repoRoot, "..", `${directoryPrefix}${alternativeName}`); console.log(chalk.yellow(` \u65B0\u3057\u3044\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u540D: ${alternativeName}`)); await this.git.raw(["worktree", "add", newWorktreePath, existingBranch]); return path2.resolve(newWorktreePath); } else if (action === "delete") { await fs2.rm(worktreePath, { recursive: true, force: true }); console.log( chalk.gray(`\u{1F5D1}\uFE0F \u65E2\u5B58\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u524A\u9664\u3057\u307E\u3057\u305F: ${path2.basename(worktreePath)}`) ); } } } await this.git.raw(["worktree", "add", worktreePath, existingBranch]); return path2.resolve(worktreePath); } async listWorktrees() { const output = await this.git.raw(["worktree", "list", "--porcelain"]); const worktrees = []; const lines = output.split("\n").filter((line) => line.trim()); let currentWorktree = {}; for (const line of lines) { if (line.startsWith("worktree ")) { if (currentWorktree.path) { worktrees.push(currentWorktree); } currentWorktree = { path: line.substring(9), detached: false, prunable: false, locked: false }; } else if (line.startsWith("HEAD ")) { currentWorktree.head = line.substring(5); } else if (line.startsWith("branch ")) { currentWorktree.branch = line.substring(7); } else if (line === "detached") { currentWorktree.detached = true; } else if (line === "prunable") { currentWorktree.prunable = true; } else if (line.startsWith("locked")) { currentWorktree.locked = true; if (line.includes(" ")) { currentWorktree.reason = line.substring(line.indexOf(" ") + 1); } } } if (currentWorktree.path) { worktrees.push(currentWorktree); } return worktrees; } async deleteWorktree(branchName, force = false) { const worktrees = await this.listWorktrees(); const worktree = worktrees.find((wt) => { const branch = wt.branch?.replace("refs/heads/", ""); return branch === branchName; }); if (!worktree) { throw new Error(`\u30EF\u30FC\u30AF\u30C4\u30EA\u30FC '${branchName}' \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093`); } const args = ["worktree", "remove"]; if (force) args.push("--force"); args.push(worktree.path); await this.git.raw(args); await this.cleanupEmptyDirectories(worktree.path); try { await this.git.branch(["-d", branchName]); } catch (error) { if (error instanceof Error && error.message.includes("not fully merged")) { await this.git.branch(["-D", branchName]); } else { throw error; } } } async getCurrentBranch() { const status = await this.git.status(); return status.current; } async isGitRepository() { try { await this.git.status(); return true; } catch { return false; } } async getAllBranches() { const localBranches = await this.git.branchLocal(); const remoteBranches = await this.git.branch(["-r"]); return { local: localBranches.all.filter((b) => !b.startsWith("remotes/")), remote: remoteBranches.all.filter((b) => b.startsWith("remotes/")).map((b) => b.replace("remotes/", "")) }; } async listLocalBranches() { const localBranches = await this.git.branchLocal(); return localBranches.all.filter((b) => !b.startsWith("remotes/")); } async fetchAll() { await this.git.fetch(["--all"]); } async getLastCommit(worktreePath) { try { const gitInWorktree = simpleGit(worktreePath); const log = await gitInWorktree.log({ maxCount: 1 }); if (log.latest) { return { date: log.latest.date, message: log.latest.message, hash: log.latest.hash.substring(0, 7) }; } return null; } catch { return null; } } async getRepositoryRoot() { try { const output = await this.git.raw(["rev-parse", "--show-toplevel"]); return output.trim(); } catch { throw new Error("\u30EA\u30DD\u30B8\u30C8\u30EA\u30EB\u30FC\u30C8\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F"); } } async isGitignored(filePath) { try { await this.git.raw(["check-ignore", filePath]); return true; } catch { return false; } } async checkBranchNameCollision(branchName) { const branches = await this.getAllBranches(); const allBranches = [...branches.local, ...branches.remote.map((r) => r.replace(/^[^/]+\//, ""))]; if (allBranches.includes(branchName)) { throw new Error(`\u30D6\u30E9\u30F3\u30C1 '${branchName}' \u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059`); } const conflictingBranches = allBranches.filter( (existing) => existing.startsWith(branchName + "/") ); if (conflictingBranches.length > 0) { const examples = conflictingBranches.slice(0, 3).join(", "); throw new Error( `\u30D6\u30E9\u30F3\u30C1 '${branchName}' \u3092\u4F5C\u6210\u3067\u304D\u307E\u305B\u3093\u3002\u4EE5\u4E0B\u306E\u65E2\u5B58\u30D6\u30E9\u30F3\u30C1\u3068\u7AF6\u5408\u3057\u307E\u3059: ${examples}${conflictingBranches.length > 3 ? ` \u306A\u3069 (${conflictingBranches.length}\u4EF6)` : ""}` ); } const parentConflicts = allBranches.filter((existing) => branchName.startsWith(existing + "/")); if (parentConflicts.length > 0) { const examples = parentConflicts.slice(0, 3).join(", "); throw new Error( `\u30D6\u30E9\u30F3\u30C1 '${branchName}' \u3092\u4F5C\u6210\u3067\u304D\u307E\u305B\u3093\u3002\u4EE5\u4E0B\u306E\u65E2\u5B58\u30D6\u30E9\u30F3\u30C1\u306E\u30B5\u30D6\u30D6\u30E9\u30F3\u30C1\u306B\u306A\u308A\u307E\u3059: ${examples}${parentConflicts.length > 3 ? ` \u306A\u3069 (${parentConflicts.length}\u4EF6)` : ""}` ); } } generateAlternativeBranchName(originalName, allBranches) { let counter = 1; let alternativeName = `${originalName}-${counter}`; while (allBranches.includes(alternativeName) || allBranches.some( (b) => b.startsWith(alternativeName + "/") || alternativeName.startsWith(b + "/") )) { counter++; alternativeName = `${originalName}-${counter}`; } return alternativeName; } async cleanupEmptyDirectories(worktreePath) { const repoRoot = await this.getRepositoryRoot(); const baseDir = path2.join(repoRoot, ".."); let currentDir = path2.dirname(worktreePath); while (currentDir !== baseDir && currentDir !== path2.dirname(currentDir)) { try { const entries = await fs2.readdir(currentDir); if (entries.length === 0) { await fs2.rmdir(currentDir); console.log(chalk.gray(`\u{1F9F9} Removed empty directory: ${path2.basename(currentDir)}`)); currentDir = path2.dirname(currentDir); } else { break; } } catch { break; } } } async checkDirectoryExists(dirPath) { try { const stats = await fs2.stat(dirPath); return stats.isDirectory(); } catch { return false; } } async handleExistingDirectory(dirPath, branchName) { const repoRoot = await this.getRepositoryRoot(); const relativePath = path2.relative(repoRoot, dirPath); console.log(chalk.yellow(` \u26A0\uFE0F \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA '${relativePath}' \u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059`)); const choices = [ { name: "\u65E2\u5B58\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u524A\u9664\u3057\u3066\u65B0\u898F\u4F5C\u6210", value: "delete" }, { name: `\u5225\u306E\u540D\u524D\u3092\u4F7F\u7528\uFF08${branchName}-2\u306A\u3069\uFF09`, value: "rename" }, { name: "\u30AD\u30E3\u30F3\u30BB\u30EB", value: "cancel" } ]; const answer = await inquirer.prompt([ { type: "list", name: "action", message: "\u3069\u306E\u3088\u3046\u306B\u51E6\u7406\u3057\u307E\u3059\u304B\uFF1F", choices } ]); return answer.action; } }; // src/commands/create.ts import { execa as execa4 } from "execa"; import path6 from "path"; import fs3 from "fs/promises"; import { spawn as spawn3 } from "child_process"; // src/utils/tmuxSession.ts import { execa as execa3 } from "execa"; import chalk2 from "chalk"; import inquirer2 from "inquirer"; // src/utils/tmux.ts import { execa } from "execa"; import { spawn } from "child_process"; async function setupTmuxStatusLine() { try { await execa("tmux", [ "set-option", "-g", "status-right", '#[fg=yellow]#{?client_prefix,#[reverse]<Prefix>#[noreverse] ,}#[fg=cyan]#(cd #{pane_current_path} && git branch --show-current 2>/dev/null || echo "no branch") #[fg=white]| %H:%M' ]); await execa("tmux", ["set-option", "-g", "pane-border-status", "top"]); await execa("tmux", ["set-option", "-g", "pane-border-format", " #{pane_title} "]); } catch { } } async function isInTmuxSession() { return process.env.TMUX !== void 0; } async function executeTmuxCommand(command, options = {}) { const { cwd, env, paneType = "new-window", sessionName } = options; if (!await isInTmuxSession()) { throw new Error("tmux\u30AA\u30D7\u30B7\u30E7\u30F3\u3092\u4F7F\u7528\u3059\u308B\u306B\u306Ftmux\u30BB\u30C3\u30B7\u30E7\u30F3\u5185\u306B\u3044\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059"); } let tmuxArgs; switch (paneType) { case "new-window": tmuxArgs = ["new-window", "-n", sessionName || "maestro"]; if (cwd) tmuxArgs.push("-c", cwd); tmuxArgs.push(...command); break; case "vertical-split": tmuxArgs = ["split-window", "-v"]; if (cwd) tmuxArgs.push("-c", cwd); tmuxArgs.push(...command); break; case "horizontal-split": tmuxArgs = ["split-window", "-h"]; if (cwd) tmuxArgs.push("-c", cwd); tmuxArgs.push(...command); break; default: throw new Error(`Unknown pane type: ${paneType}`); } const tmuxEnv = { ...process.env, ...env }; try { const tmuxProcess = spawn("tmux", tmuxArgs, { stdio: "inherit", env: tmuxEnv }); return new Promise((resolve, reject) => { tmuxProcess.on("exit", (code) => { if (code === 0) { resolve(); } else { reject(new Error(`tmux command failed with exit code ${code}`)); } }); tmuxProcess.on("error", (error) => { reject(error); }); }); } catch (error) { throw new Error( `Failed to execute tmux command: ${error instanceof Error ? error.message : "Unknown error"}` ); } } async function startTmuxShell(options = {}) { const { cwd, branchName, paneType = "new-window", sessionName } = options; const env = { MAESTRO: "1", MAESTRO_NAME: branchName || "unknown", MAESTRO_PATH: cwd || process.cwd() }; const shell = process.env.SHELL || "/bin/bash"; await executeTmuxCommand([shell], { cwd, env, paneType, sessionName: sessionName || branchName }); } async function executeTmuxCommandInPane(command, options = {}) { const { cwd, branchName, paneType = "new-window", sessionName } = options; const env = { MAESTRO: "1", MAESTRO_BRANCH: branchName || "unknown", MAESTRO_PATH: cwd || process.cwd() }; await executeTmuxCommand(["sh", "-c", command], { cwd, env, paneType, sessionName: sessionName || branchName }); } // src/utils/nativeTmux.ts import { spawn as spawn2 } from "child_process"; import { execa as execa2 } from "execa"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import { existsSync, chmodSync } from "fs"; var NativeTmuxHelper = class { // Cached path to the native tmux helper script (lazy initialization) static _helperScript = null; /** * Get the path to the tmux helper script with lazy initialization * Only resolves the path when tmux functionality is actually needed */ static getHelperScript() { if (this._helperScript !== null) { return this._helperScript; } if (process.env.NODE_ENV === "test" || process.env.VITEST) { this._helperScript = "/mock/path/maestro-tmux-attach"; return this._helperScript; } const currentDir = dirname(fileURLToPath(import.meta.url)); const possiblePaths = [ // Built package: dist/utils -> ../../scripts/ (from issue-144/dist/utils to issue-144/scripts) join(dirname(dirname(currentDir)), "scripts", "maestro-tmux-attach"), // Development/source: src/utils -> ../../scripts/ (from issue-144/src/utils to issue-144/scripts) join(dirname(dirname(currentDir)), "scripts", "maestro-tmux-attach"), // npm package root: node_modules/@camoneart/maestro/dist/utils -> ../../../scripts/ join(dirname(dirname(dirname(currentDir))), "scripts", "maestro-tmux-attach"), // Direct path for current development structure join(process.cwd(), "scripts", "maestro-tmux-attach") ]; let scriptPath = null; for (const path14 of possiblePaths) { if (existsSync(path14)) { scriptPath = path14; break; } } if (!scriptPath) { throw new Error( `Maestro tmux helper script not found. Searched paths: ${possiblePaths.join(", ")} Note: This error occurs only when tmux functionality is used. Non-tmux commands like "config init" should work without tmux installed.` ); } try { chmodSync(scriptPath, "755"); } catch { console.warn(`Warning: Could not set executable permissions for ${scriptPath}`); } this._helperScript = scriptPath; return this._helperScript; } /** * Attach to an existing tmux session using native shell script with exec * This function replaces the current Node.js process with tmux */ static async attachToSession(sessionName) { if (!sessionName || typeof sessionName !== "string") { throw new Error("Session name must be a non-empty string"); } try { await execa2("tmux", ["has-session", "-t", sessionName]); } catch { throw new Error(`Tmux session '${sessionName}' does not exist`); } const helperProcess = spawn2(this.getHelperScript(), ["attach", sessionName], { stdio: "inherit", detached: false }); helperProcess.on("exit", (code) => { process.exit(code || 0); }); helperProcess.on("error", (error) => { console.error(`Failed to attach to tmux session: ${error.message}`); process.exit(1); }); return new Promise(() => { }); } /** * Create a new tmux session and attach using native shell script with exec * This function replaces the current Node.js process with tmux */ static async createAndAttachSession(sessionName, workingDirectory, command) { if (!sessionName || typeof sessionName !== "string") { throw new Error("Session name must be a non-empty string"); } try { await execa2("tmux", ["has-session", "-t", sessionName]); console.warn(`Warning: tmux session '${sessionName}' already exists, attaching instead`); return this.attachToSession(sessionName); } catch { } const args = ["new", sessionName]; if (workingDirectory) { args.push(workingDirectory); } if (command) { args.push(command); } const helperProcess = spawn2(this.getHelperScript(), args, { stdio: "inherit", detached: false }); helperProcess.on("exit", (code) => { process.exit(code || 0); }); helperProcess.on("error", (error) => { console.error(`Failed to create tmux session: ${error.message}`); process.exit(1); }); return new Promise(() => { }); } /** * Switch tmux client to another session (only works from within tmux) * This doesn't need process replacement since it's an internal tmux operation */ static async switchClient(sessionName) { if (!sessionName || typeof sessionName !== "string") { throw new Error("Session name must be a non-empty string"); } try { await execa2(this.getHelperScript(), ["switch", sessionName]); } catch (error) { throw new Error( `Failed to switch tmux client: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * Check if a tmux session exists */ static async sessionExists(sessionName) { try { await execa2("tmux", ["has-session", "-t", sessionName]); return true; } catch { return false; } } /** * List all tmux sessions */ static async listSessions() { try { const { stdout } = await execa2("tmux", [ "list-sessions", "-F", "#{session_name}:#{session_attached}" ]); return stdout.split("\n").filter(Boolean).map((line) => { const [name, attached] = line.split(":"); return { name: name || "unknown", attached: attached === "1" }; }); } catch { return []; } } }; // src/utils/tty.ts async function attachToTmuxWithProperTTY(sessionName) { await NativeTmuxHelper.attachToSession(sessionName); } async function switchTmuxClientWithProperTTY(sessionName) { await NativeTmuxHelper.switchClient(sessionName); } async function createAndAttachTmuxSession(sessionName, cwd, command) { await NativeTmuxHelper.createAndAttachSession(sessionName, cwd, command); } // src/utils/tmuxSession.ts function getPaneConfiguration(options) { const paneCount = options?.tmuxHPanes || options?.tmuxVPanes || 2; const isHorizontal = Boolean(options?.tmuxH || options?.tmuxHPanes); return { paneCount, isHorizontal }; } function validatePaneCount(paneCount, isHorizontal) { const maxReasonablePanes = isHorizontal ? 10 : 15; if (paneCount > maxReasonablePanes) { const splitType = isHorizontal ? "\u6C34\u5E73" : "\u5782\u76F4"; throw new Error( `\u753B\u9762\u30B5\u30A4\u30BA\u306B\u5BFE\u3057\u3066\u30DA\u30A4\u30F3\u6570\uFF08${paneCount}\u500B\uFF09\u304C\u591A\u3059\u304E\u308B\u305F\u3081\u3001\u30BB\u30C3\u30B7\u30E7\u30F3\u304C\u4F5C\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u30BF\u30FC\u30DF\u30CA\u30EB\u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u5927\u304D\u304F\u3059\u308B\u304B\u3001\u30DA\u30A4\u30F3\u6570\u3092\u6E1B\u3089\u3057\u3066\u304F\u3060\u3055\u3044\u3002\uFF08${splitType}\u5206\u5272\uFF09` ); } } function generateTmuxMessage(options) { const paneCountMsg = options?.tmuxHPanes || options?.tmuxVPanes ? `${options.tmuxHPanes || options.tmuxVPanes}\u3064\u306E\u30DA\u30A4\u30F3\u306B` : ""; const splitTypeMsg = options?.tmuxH || options?.tmuxHPanes ? "\u6C34\u5E73" : "\u5782\u76F4"; const layoutMsg = options?.tmuxLayout ? ` (${options.tmuxLayout}\u30EC\u30A4\u30A2\u30A6\u30C8)` : ""; return { paneCountMsg, splitTypeMsg, layoutMsg }; } async function createMultiplePanes(sessionName, worktreePath, paneCount, isHorizontal) { for (let i = 1; i < paneCount; i++) { const splitArgs = sessionName ? ["split-window", "-t", sessionName] : ["split-window"]; if (isHorizontal) { splitArgs.push("-h"); } else { splitArgs.push("-v"); } splitArgs.push("-c", worktreePath); const shell = process.env.SHELL || "/bin/bash"; splitArgs.push(shell, "-l"); try { await execa3("tmux", splitArgs); } catch (error) { if (error instanceof Error && error.message.includes("no space for new pane")) { const splitType = isHorizontal ? "\u6C34\u5E73" : "\u5782\u76F4"; throw new Error( `\u753B\u9762\u30B5\u30A4\u30BA\u306B\u5BFE\u3057\u3066\u30DA\u30A4\u30F3\u6570\uFF08${paneCount}\u500B\uFF09\u304C\u591A\u3059\u304E\u307E\u3059\u3002\u30BF\u30FC\u30DF\u30CA\u30EB\u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u5927\u304D\u304F\u3059\u308B\u304B\u3001\u30DA\u30A4\u30F3\u6570\u3092\u6E1B\u3089\u3057\u3066\u304F\u3060\u3055\u3044\u3002\uFF08${splitType}\u5206\u5272\uFF09` ); } const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`tmux\u30DA\u30A4\u30F3\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errorMessage}`); } } } async function applyTmuxLayout(sessionName, options, paneCount, isHorizontal) { if (options?.tmuxLayout) { const layoutArgs = sessionName ? ["select-layout", "-t", sessionName, options.tmuxLayout] : ["select-layout", options.tmuxLayout]; await execa3("tmux", layoutArgs); } else if (paneCount && paneCount > 2) { const defaultLayout = isHorizontal ? "even-horizontal" : "even-vertical"; const layoutArgs = sessionName ? ["select-layout", "-t", sessionName, defaultLayout] : ["select-layout", defaultLayout]; await execa3("tmux", layoutArgs); } } async function setTitleForAllPanes(sessionName, branchName, paneCount) { for (let i = 0; i < paneCount; i++) { try { const titleArgs = sessionName ? ["select-pane", "-t", `${sessionName}:0.${i}`, "-T", branchName] : ["select-pane", "-t", `${i}`, "-T", branchName]; await execa3("tmux", titleArgs); } catch { } } } function attachToTmuxSession(sessionName) { return attachToTmuxWithProperTTY(sessionName); } function switchTmuxClient(sessionName) { return switchTmuxClientWithProperTTY(sessionName); } async function handleNewSessionPaneSplit(sessionName, branchName, worktreePath, options) { const { paneCount, isHorizontal } = getPaneConfiguration(options); const shell = process.env.SHELL || "/bin/bash"; await execa3("tmux", ["new-session", "-d", "-s", sessionName, "-c", worktreePath, shell, "-l"]); await createMultiplePanes(sessionName, worktreePath, paneCount, isHorizontal); await applyTmuxLayout(sessionName, options, paneCount, isHorizontal); await setTitleForAllPanes(sessionName, branchName, paneCount); await execa3("tmux", ["select-pane", "-t", `${sessionName}:0.0`]); await execa3("tmux", ["rename-window", "-t", sessionName, branchName]); await setupTmuxStatusLine(); } async function handleInsideTmuxPaneSplit(branchName, worktreePath, options) { const { paneCount, isHorizontal } = getPaneConfiguration(options); await createMultiplePanes(null, worktreePath, paneCount, isHorizontal); await applyTmuxLayout(null, options, paneCount, isHorizontal); await setTitleForAllPanes(null, branchName, paneCount); await execa3("tmux", ["select-pane", "-t", "0"]); await setupTmuxStatusLine(); } async function createTmuxSession(options) { const { sessionName: branchName, worktreePath, interactiveAttach = true } = options; const sessionName = branchName.replace(/[^a-zA-Z0-9_-]/g, "-"); try { if (options.tmuxH || options.tmuxV || options.tmuxHPanes || options.tmuxVPanes || options.tmuxLayout) { const isInsideTmux = process.env.TMUX !== void 0; if (!isInsideTmux) { try { await execa3("tmux", ["has-session", "-t", sessionName]); console.log(chalk2.yellow(`tmux\u30BB\u30C3\u30B7\u30E7\u30F3 '${sessionName}' \u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059`)); await attachToTmuxSession(sessionName); return; } catch { } await handleNewSessionPaneSplit(sessionName, branchName, worktreePath, options); const { paneCountMsg, splitTypeMsg, layoutMsg } = generateTmuxMessage(options); console.log( chalk2.green( `\u2728 tmux\u30BB\u30C3\u30B7\u30E7\u30F3 '${sessionName}' \u3092\u4F5C\u6210\u3057\u3001${paneCountMsg}${splitTypeMsg}\u5206\u5272\u3057\u307E\u3057\u305F${layoutMsg}` ) ); if (interactiveAttach && process.stdout.isTTY && process.stdin.isTTY) { try { const { shouldAttach } = await inquirer2.prompt([ { type: "confirm", name: "shouldAttach", message: "\u30BB\u30C3\u30B7\u30E7\u30F3\u306B\u30A2\u30BF\u30C3\u30C1\u3057\u307E\u3059\u304B\uFF1F", default: true } ]); if (shouldAttach) { console.log(chalk2.cyan(`\u{1F3B5} tmux\u30BB\u30C3\u30B7\u30E7\u30F3 '${sessionName}' \u306B\u30A2\u30BF\u30C3\u30C1\u3057\u3066\u3044\u307E\u3059...`)); await attachToTmuxSession(sessionName); } else { console.log( chalk2.yellow(` \u{1F4DD} \u5F8C\u3067\u30A2\u30BF\u30C3\u30C1\u3059\u308B\u306B\u306F\u4EE5\u4E0B\u306E\u30B3\u30DE\u30F3\u30C9\u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044:`) ); console.log(chalk2.white(` tmux attach -t ${sessionName}`)); console.log(chalk2.gray(` \u{1F4A1} \u30D2\u30F3\u30C8: Ctrl+B, D \u3067\u30BB\u30C3\u30B7\u30E7\u30F3\u304B\u3089\u30C7\u30BF\u30C3\u30C1\u3067\u304D\u307E\u3059`)); } } catch { console.log(chalk2.yellow(` \u{1F4DD} \u5F8C\u3067\u30A2\u30BF\u30C3\u30C1\u3059\u308B\u306B\u306F\u4EE5\u4E0B\u306E\u30B3\u30DE\u30F3\u30C9\u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044:`)); console.log(chalk2.white(` tmux attach -t ${sessionName}`)); console.log(chalk2.gray(` \u{1F4A1} \u30D2\u30F3\u30C8: Ctrl+B, D \u3067\u30BB\u30C3\u30B7\u30E7\u30F3\u304B\u3089\u30C7\u30BF\u30C3\u30C1\u3067\u304D\u307E\u3059`)); } } else { console.log( chalk2.yellow(` \u{1F4DD} tmux\u30BB\u30C3\u30B7\u30E7\u30F3\u306B\u30A2\u30BF\u30C3\u30C1\u3059\u308B\u306B\u306F\u4EE5\u4E0B\u306E\u30B3\u30DE\u30F3\u30C9\u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044:`) ); console.log(chalk2.white(` tmux attach -t ${sessionName}`)); console.log(chalk2.gray(` \u{1F4A1} \u30D2\u30F3\u30C8: Ctrl+B, D \u3067\u30BB\u30C3\u30B7\u30E7\u30F3\u304B\u3089\u30C7\u30BF\u30C3\u30C1\u3067\u304D\u307E\u3059`)); } return; } else { await handleInsideTmuxPaneSplit(branchName, worktreePath, options); const { paneCountMsg, splitTypeMsg, layoutMsg } = generateTmuxMessage(options); console.log( chalk2.green( `\u2705 tmux\u30DA\u30A4\u30F3\u3092${paneCountMsg}${splitTypeMsg}\u5206\u5272\u3057\u307E\u3057\u305F${layoutMsg}: ${branchName}` ) ); return; } } try { await execa3("tmux", ["has-session", "-t", sessionName]); console.log(chalk2.yellow(`tmux\u30BB\u30C3\u30B7\u30E7\u30F3 '${sessionName}' \u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059`)); return; } catch { } const shell = process.env.SHELL || "/bin/bash"; await execa3("tmux", ["new-session", "-d", "-s", sessionName, "-c", worktreePath, shell, "-l"]); await execa3("tmux", ["rename-window", "-t", sessionName, branchName]); await setTitleForAllPanes(sessionName, branchName, 1); console.log(chalk2.green(`\u2728 tmux\u30BB\u30C3\u30B7\u30E7\u30F3 '${sessionName}' \u3092\u4F5C\u6210\u3057\u307E\u3057\u305F`)); if (interactiveAttach && process.stdout.isTTY && process.stdin.isTTY) { try { const { shouldAttach } = await inquirer2.prompt([ { type: "confirm", name: "shouldAttach", message: "\u30BB\u30C3\u30B7\u30E7\u30F3\u306B\u30A2\u30BF\u30C3\u30C1\u3057\u307E\u3059\u304B\uFF1F", default: true } ]); if (shouldAttach) { console.log(chalk2.cyan(`\u{1F3B5} tmux\u30BB\u30C3\u30B7\u30E7\u30F3 '${sessionName}' \u306B\u30A2\u30BF\u30C3\u30C1\u3057\u3066\u3044\u307E\u3059...`)); const isInsideTmux = process.env.TMUX !== void 0; if (isInsideTmux) { await switchTmuxClient(sessionName); } else { await attachToTmuxSession(sessionName); } } else { console.log(chalk2.yellow(` \u{1F4DD} \u5F8C\u3067\u30A2\u30BF\u30C3\u30C1\u3059\u308B\u306B\u306F\u4EE5\u4E0B\u306E\u30B3\u30DE\u30F3\u30C9\u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044:`)); console.log(chalk2.white(` tmux attach -t ${sessionName}`)); console.log(chalk2.gray(` \u{1F4A1} \u30D2\u30F3\u30C8: Ctrl+B, D \u3067\u30BB\u30C3\u30B7\u30E7\u30F3\u304B\u3089\u30C7\u30BF\u30C3\u30C1\u3067\u304D\u307E\u3059`)); } } catch { console.log(chalk2.yellow(` \u{1F4DD} \u5F8C\u3067\u30A2\u30BF\u30C3\u30C1\u3059\u308B\u306B\u306F\u4EE5\u4E0B\u306E\u30B3\u30DE\u30F3\u30C9\u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044:`)); console.log(chalk2.white(` tmux attach -t ${sessionName}`)); console.log(chalk2.gray(` \u{1F4A1} \u30D2\u30F3\u30C8: Ctrl+B, D \u3067\u30BB\u30C3\u30B7\u30E7\u30F3\u304B\u3089\u30C7\u30BF\u30C3\u30C1\u3067\u304D\u307E\u3059`)); } } else { console.log( chalk2.yellow(` \u{1F4DD} tmux\u30BB\u30C3\u30B7\u30E7\u30F3\u306B\u30A2\u30BF\u30C3\u30C1\u3059\u308B\u306B\u306F\u4EE5\u4E0B\u306E\u30B3\u30DE\u30F3\u30C9\u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044:`) ); console.log(chalk2.white(` tmux attach -t ${sessionName}`)); console.log(chalk2.gray(` \u{1F4A1} \u30D2\u30F3\u30C8: Ctrl+B, D \u3067\u30BB\u30C3\u30B7\u30E7\u30F3\u304B\u3089\u30C7\u30BF\u30C3\u30C1\u3067\u304D\u307E\u3059`)); } } catch (error) { if (error instanceof Error) { console.error(chalk2.red(`\u2716 ${error.message}`)); } else { console.error(chalk2.red(`\u2716 tmux\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error}`)); } throw error; } } function validateTmuxOptions(options) { const { paneCount, isHorizontal } = getPaneConfiguration(options); if (paneCount > 2) { validatePaneCount(paneCount, isHorizontal); } } // src/utils/packageManager.ts import { existsSync as existsSync2, readFileSync } from "fs"; import path3 from "path"; function detectPackageManager(projectPath) { if (existsSync2(path3.join(projectPath, "pnpm-lock.yaml"))) { return "pnpm"; } if (existsSync2(path3.join(projectPath, "yarn.lock"))) { return "yarn"; } if (existsSync2(path3.join(projectPath, "package-lock.json"))) { return "npm"; } const packageJsonPath = path3.join(projectPath, "package.json"); if (existsSync2(packageJsonPath)) { try { const packageJson2 = JSON.parse(readFileSync(packageJsonPath, "utf-8")); if (packageJson2.packageManager) { const manager = packageJson2.packageManager.split("@")[0]; if (["pnpm", "npm", "yarn"].includes(manager)) { return manager; } } } catch { } } return "npm"; } // src/utils/gitignore.ts import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, appendFileSync } from "fs"; import path4 from "path"; async function addToGitignore(projectPath, entry, comment) { const gitignorePath = path4.join(projectPath, ".gitignore"); let gitignoreContent = ""; let entryExists = false; if (existsSync3(gitignorePath)) { gitignoreContent = readFileSync2(gitignorePath, "utf-8"); const lines = gitignoreContent.split("\n"); entryExists = lines.some((line) => line.trim() === entry.trim()); } if (!entryExists) { let entryToAdd; if (comment) { const commentLine = `# ${comment}`; entryToAdd = gitignoreContent && !gitignoreContent.endsWith("\n") ? ` ${commentLine} ${entry} ` : ` ${commentLine} ${entry} `; } else { entryToAdd = gitignoreContent && !gitignoreContent.endsWith("\n") ? ` ${entry} ` : `${entry} `; } if (existsSync3(gitignorePath)) { appendFileSync(gitignorePath, entryToAdd, "utf-8"); } else { writeFileSync(gitignorePath, entryToAdd, "utf-8"); } } } // src/utils/path.ts import path5 from "path"; function formatPath(absolutePath, config) { const pathDisplay = config.ui?.pathDisplay || "absolute"; if (pathDisplay === "relative") { const relativePath = path5.relative(process.cwd(), absolutePath); return relativePath === "" ? "." : relativePath; } return absolutePath; } // src/commands/create.ts function parseIssueNumber(input) { const issueMatch = input.match(/^#?(\d+)$/) || input.match(/^issue-(\d+)$/i); if (issueMatch) { const issueNumber = issueMatch[1]; return { isIssue: true, issueNumber, branchName: `issue-${issueNumber}` }; } return { isIssue: false, branchName: input }; } async function fetchGitHubItem(issueNumber, type) { const { stdout } = await execa4("gh", [ type, "view", issueNumber, "--json", "number,title,body,author,labels,assignees,milestone,url" ]); return JSON.parse(stdout); } function convertToMetadata(item, type) { return { type, title: item.title, body: item.body || "", author: item.author?.login || "", labels: item.labels?.map((l) => l.name) || [], assignees: item.assignees?.map((a) => a.login) || [], milestone: item.milestone?.title, url: item.url }; } async function fetchGitHubMetadata(issueNumber) { try { try { const pr = await fetchGitHubItem(issueNumber, "pr"); return convertToMetadata(pr, "pr"); } catch { const issue = await fetchGitHubItem(issueNumber, "issue"); return convertToMetadata(issue, "issue"); } } catch { return null; } } async function saveWorktreeMetadata(worktreePath, branchName, metadata) { const metadataPath = path6.join(worktreePath, ".maestro-metadata.json"); const metadataContent = { createdAt: (/* @__PURE__ */ new Date()).toISOString(), branch: branchName, worktreePath, ...metadata }; try { await fs3.writeFile(metadataPath, JSON.stringify(metadataContent, null, 2)); await addToGitignore(worktreePath, ".maestro-metadata.json", "maestro metadata"); } catch { } } async function createTmuxSession2(branchName, worktreePath, options) { await createTmuxSession({ sessionName: branchName, worktreePath, tmux: options?.tmux, tmuxH: options?.tmuxH, tmuxV: options?.tmuxV, tmuxHPanes: options?.tmuxHPanes, tmuxVPanes: options?.tmuxVPanes, tmuxLayout: options?.tmuxLayout, interactiveAttach: true }); } async function handleClaudeMarkdown(worktreePath, config) { const claudeMode = config.claude?.markdownMode || "shared"; const rootClaudePath = path6.join(process.cwd(), "CLAUDE.md"); const worktreeClaudePath = path6.join(worktreePath, "CLAUDE.md"); try { if (claudeMode === "shared") { if (await fs3.access(rootClaudePath).then(() => true).catch(() => false)) { try { await fs3.unlink(worktreeClaudePath); } catch { } await fs3.symlink(path6.relative(worktreePath, rootClaudePath), worktreeClaudePath); console.log(chalk3.green(`\u2728 CLAUDE.md \u3092\u5171\u6709\u30E2\u30FC\u30C9\u3067\u8A2D\u5B9A\u3057\u307E\u3057\u305F`)); } } else if (claudeMode === "split") { const splitContent = `# ${path6.basename(worktreePath)} - Claude Code Instructions This is a dedicated CLAUDE.md for this worktree. ## Project Context - Branch: ${path6.basename(worktreePath)} - Worktree Path: ${worktreePath} ## Instructions Add specific instructions for this worktree here. `; await fs3.writeFile(worktreeClaudePath, splitContent); console.log(chalk3.green(`\u2728 CLAUDE.md \u3092\u5206\u5272\u30E2\u30FC\u30C9\u3067\u4F5C\u6210\u3057\u307E\u3057\u305F`)); } } catch (error) { console.warn(chalk3.yellow(`CLAUDE.md\u306E\u51E6\u7406\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error}`)); } } function processBranchName(branchName, config) { const { isIssue, issueNumber, branchName: parsedBranchName } = parseIssueNumber(branchName); let finalBranchName = parsedBranch