@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
JavaScript
#!/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