@iloom/cli 0.1.14
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/LICENSE +33 -0
- package/README.md +711 -0
- package/dist/ClaudeContextManager-XOSXQ67R.js +13 -0
- package/dist/ClaudeContextManager-XOSXQ67R.js.map +1 -0
- package/dist/ClaudeService-YSZ6EXWP.js +12 -0
- package/dist/ClaudeService-YSZ6EXWP.js.map +1 -0
- package/dist/GitHubService-F7Z3XJOS.js +11 -0
- package/dist/GitHubService-F7Z3XJOS.js.map +1 -0
- package/dist/LoomLauncher-MODG2SEM.js +263 -0
- package/dist/LoomLauncher-MODG2SEM.js.map +1 -0
- package/dist/NeonProvider-PAGPUH7F.js +12 -0
- package/dist/NeonProvider-PAGPUH7F.js.map +1 -0
- package/dist/PromptTemplateManager-7FINLRDE.js +9 -0
- package/dist/PromptTemplateManager-7FINLRDE.js.map +1 -0
- package/dist/SettingsManager-VAZF26S2.js +19 -0
- package/dist/SettingsManager-VAZF26S2.js.map +1 -0
- package/dist/SettingsMigrationManager-MTQIMI54.js +146 -0
- package/dist/SettingsMigrationManager-MTQIMI54.js.map +1 -0
- package/dist/add-issue-22JBNOML.js +54 -0
- package/dist/add-issue-22JBNOML.js.map +1 -0
- package/dist/agents/iloom-issue-analyze-and-plan.md +580 -0
- package/dist/agents/iloom-issue-analyzer.md +290 -0
- package/dist/agents/iloom-issue-complexity-evaluator.md +224 -0
- package/dist/agents/iloom-issue-enhancer.md +266 -0
- package/dist/agents/iloom-issue-implementer.md +262 -0
- package/dist/agents/iloom-issue-planner.md +358 -0
- package/dist/agents/iloom-issue-reviewer.md +63 -0
- package/dist/chunk-2ZPFJQ3B.js +63 -0
- package/dist/chunk-2ZPFJQ3B.js.map +1 -0
- package/dist/chunk-37DYYFVK.js +29 -0
- package/dist/chunk-37DYYFVK.js.map +1 -0
- package/dist/chunk-BLCTGFZN.js +121 -0
- package/dist/chunk-BLCTGFZN.js.map +1 -0
- package/dist/chunk-CP2NU2JC.js +545 -0
- package/dist/chunk-CP2NU2JC.js.map +1 -0
- package/dist/chunk-CWR2SANQ.js +39 -0
- package/dist/chunk-CWR2SANQ.js.map +1 -0
- package/dist/chunk-F3XBU2R7.js +110 -0
- package/dist/chunk-F3XBU2R7.js.map +1 -0
- package/dist/chunk-GEHQXLEI.js +130 -0
- package/dist/chunk-GEHQXLEI.js.map +1 -0
- package/dist/chunk-GYCR2LOU.js +143 -0
- package/dist/chunk-GYCR2LOU.js.map +1 -0
- package/dist/chunk-GZP4UGGM.js +48 -0
- package/dist/chunk-GZP4UGGM.js.map +1 -0
- package/dist/chunk-H4E4THUZ.js +55 -0
- package/dist/chunk-H4E4THUZ.js.map +1 -0
- package/dist/chunk-HPJJSYNS.js +644 -0
- package/dist/chunk-HPJJSYNS.js.map +1 -0
- package/dist/chunk-JBH2ZYYZ.js +220 -0
- package/dist/chunk-JBH2ZYYZ.js.map +1 -0
- package/dist/chunk-JNKJ7NJV.js +78 -0
- package/dist/chunk-JNKJ7NJV.js.map +1 -0
- package/dist/chunk-JQ7VOSTC.js +437 -0
- package/dist/chunk-JQ7VOSTC.js.map +1 -0
- package/dist/chunk-KQDEK2ZW.js +199 -0
- package/dist/chunk-KQDEK2ZW.js.map +1 -0
- package/dist/chunk-O2QWO64Z.js +179 -0
- package/dist/chunk-O2QWO64Z.js.map +1 -0
- package/dist/chunk-OC4H6HJD.js +248 -0
- package/dist/chunk-OC4H6HJD.js.map +1 -0
- package/dist/chunk-PR7FKQBG.js +120 -0
- package/dist/chunk-PR7FKQBG.js.map +1 -0
- package/dist/chunk-PXZBAC2M.js +250 -0
- package/dist/chunk-PXZBAC2M.js.map +1 -0
- package/dist/chunk-QEPVTTHD.js +383 -0
- package/dist/chunk-QEPVTTHD.js.map +1 -0
- package/dist/chunk-RSRO7564.js +203 -0
- package/dist/chunk-RSRO7564.js.map +1 -0
- package/dist/chunk-SJUQ2NDR.js +146 -0
- package/dist/chunk-SJUQ2NDR.js.map +1 -0
- package/dist/chunk-SPYPLHMK.js +177 -0
- package/dist/chunk-SPYPLHMK.js.map +1 -0
- package/dist/chunk-SSCQCCJ7.js +75 -0
- package/dist/chunk-SSCQCCJ7.js.map +1 -0
- package/dist/chunk-SSR5AVRJ.js +41 -0
- package/dist/chunk-SSR5AVRJ.js.map +1 -0
- package/dist/chunk-T7QPXANZ.js +315 -0
- package/dist/chunk-T7QPXANZ.js.map +1 -0
- package/dist/chunk-U3WU5OWO.js +203 -0
- package/dist/chunk-U3WU5OWO.js.map +1 -0
- package/dist/chunk-W3DQTW63.js +124 -0
- package/dist/chunk-W3DQTW63.js.map +1 -0
- package/dist/chunk-WKEWRSDB.js +151 -0
- package/dist/chunk-WKEWRSDB.js.map +1 -0
- package/dist/chunk-Y7SAGNUT.js +66 -0
- package/dist/chunk-Y7SAGNUT.js.map +1 -0
- package/dist/chunk-YETJNRQM.js +39 -0
- package/dist/chunk-YETJNRQM.js.map +1 -0
- package/dist/chunk-YYSKGAZT.js +384 -0
- package/dist/chunk-YYSKGAZT.js.map +1 -0
- package/dist/chunk-ZZZWQGTS.js +169 -0
- package/dist/chunk-ZZZWQGTS.js.map +1 -0
- package/dist/claude-7LUVDZZ4.js +17 -0
- package/dist/claude-7LUVDZZ4.js.map +1 -0
- package/dist/cleanup-3LUWPSM7.js +412 -0
- package/dist/cleanup-3LUWPSM7.js.map +1 -0
- package/dist/cli-overrides-XFZWY7CM.js +16 -0
- package/dist/cli-overrides-XFZWY7CM.js.map +1 -0
- package/dist/cli.js +603 -0
- package/dist/cli.js.map +1 -0
- package/dist/color-ZVALX37U.js +21 -0
- package/dist/color-ZVALX37U.js.map +1 -0
- package/dist/enhance-XJIQHVPD.js +166 -0
- package/dist/enhance-XJIQHVPD.js.map +1 -0
- package/dist/env-MDFL4ZXL.js +23 -0
- package/dist/env-MDFL4ZXL.js.map +1 -0
- package/dist/feedback-23CLXKFT.js +158 -0
- package/dist/feedback-23CLXKFT.js.map +1 -0
- package/dist/finish-CY4CIH6O.js +1608 -0
- package/dist/finish-CY4CIH6O.js.map +1 -0
- package/dist/git-LVRZ57GJ.js +43 -0
- package/dist/git-LVRZ57GJ.js.map +1 -0
- package/dist/ignite-WXEF2ID5.js +359 -0
- package/dist/ignite-WXEF2ID5.js.map +1 -0
- package/dist/index.d.ts +1341 -0
- package/dist/index.js +3058 -0
- package/dist/index.js.map +1 -0
- package/dist/init-RHACUR4E.js +123 -0
- package/dist/init-RHACUR4E.js.map +1 -0
- package/dist/installation-detector-VARGFFRZ.js +11 -0
- package/dist/installation-detector-VARGFFRZ.js.map +1 -0
- package/dist/logger-MKYH4UDV.js +12 -0
- package/dist/logger-MKYH4UDV.js.map +1 -0
- package/dist/mcp/chunk-6SDFJ42P.js +62 -0
- package/dist/mcp/chunk-6SDFJ42P.js.map +1 -0
- package/dist/mcp/claude-YHHHLSXH.js +249 -0
- package/dist/mcp/claude-YHHHLSXH.js.map +1 -0
- package/dist/mcp/color-QS5BFCNN.js +168 -0
- package/dist/mcp/color-QS5BFCNN.js.map +1 -0
- package/dist/mcp/github-comment-server.js +165 -0
- package/dist/mcp/github-comment-server.js.map +1 -0
- package/dist/mcp/terminal-SDCMDVD7.js +202 -0
- package/dist/mcp/terminal-SDCMDVD7.js.map +1 -0
- package/dist/open-X6BTENPV.js +278 -0
- package/dist/open-X6BTENPV.js.map +1 -0
- package/dist/prompt-ANTQWHUF.js +13 -0
- package/dist/prompt-ANTQWHUF.js.map +1 -0
- package/dist/prompts/issue-prompt.txt +230 -0
- package/dist/prompts/pr-prompt.txt +35 -0
- package/dist/prompts/regular-prompt.txt +14 -0
- package/dist/run-2JCPQAX3.js +278 -0
- package/dist/run-2JCPQAX3.js.map +1 -0
- package/dist/schema/settings.schema.json +221 -0
- package/dist/start-LWVRBJ6S.js +982 -0
- package/dist/start-LWVRBJ6S.js.map +1 -0
- package/dist/terminal-3D6TUAKJ.js +16 -0
- package/dist/terminal-3D6TUAKJ.js.map +1 -0
- package/dist/test-git-XPF4SZXJ.js +52 -0
- package/dist/test-git-XPF4SZXJ.js.map +1 -0
- package/dist/test-prefix-XGFXFAYN.js +68 -0
- package/dist/test-prefix-XGFXFAYN.js.map +1 -0
- package/dist/test-tabs-JRKY3QMM.js +69 -0
- package/dist/test-tabs-JRKY3QMM.js.map +1 -0
- package/dist/test-webserver-M2I3EV4J.js +62 -0
- package/dist/test-webserver-M2I3EV4J.js.map +1 -0
- package/dist/update-3ZT2XX2G.js +79 -0
- package/dist/update-3ZT2XX2G.js.map +1 -0
- package/dist/update-notifier-QSSEB5KC.js +11 -0
- package/dist/update-notifier-QSSEB5KC.js.map +1 -0
- package/package.json +113 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
executeGitCommand,
|
|
4
|
+
extractPRNumber,
|
|
5
|
+
generateWorktreePath,
|
|
6
|
+
getCurrentBranch,
|
|
7
|
+
getDefaultBranch,
|
|
8
|
+
getRepoRoot,
|
|
9
|
+
hasUncommittedChanges,
|
|
10
|
+
isPRBranch,
|
|
11
|
+
isValidGitRepo,
|
|
12
|
+
parseWorktreeList
|
|
13
|
+
} from "./chunk-JQ7VOSTC.js";
|
|
14
|
+
|
|
15
|
+
// src/lib/GitWorktreeManager.ts
|
|
16
|
+
import path from "path";
|
|
17
|
+
import fs from "fs-extra";
|
|
18
|
+
var GitWorktreeManager = class {
|
|
19
|
+
constructor(workingDirectory = process.cwd()) {
|
|
20
|
+
this._workingDirectory = workingDirectory;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get the working directory for git operations (main worktree path)
|
|
24
|
+
*/
|
|
25
|
+
get workingDirectory() {
|
|
26
|
+
return this._workingDirectory;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* List all worktrees in the repository
|
|
30
|
+
* Defaults to porcelain format for reliable machine parsing
|
|
31
|
+
* Equivalent to: git worktree list --porcelain
|
|
32
|
+
*/
|
|
33
|
+
async listWorktrees(options = {}) {
|
|
34
|
+
const args = ["worktree", "list"];
|
|
35
|
+
if (options.porcelain !== false) args.push("--porcelain");
|
|
36
|
+
if (options.verbose) args.push("-v");
|
|
37
|
+
const output = await executeGitCommand(args, { cwd: this._workingDirectory });
|
|
38
|
+
return parseWorktreeList(output);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Find worktree for a specific branch
|
|
42
|
+
* Ports: find_worktree_for_branch() from find-worktree-for-branch.sh
|
|
43
|
+
*/
|
|
44
|
+
async findWorktreeForBranch(branchName) {
|
|
45
|
+
const worktrees = await this.listWorktrees();
|
|
46
|
+
return worktrees.find((wt) => wt.branch === branchName) ?? null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check if a worktree is the main repository worktree
|
|
50
|
+
* The main worktree is the first one listed by git worktree list (Git guarantee)
|
|
51
|
+
* This cannot be determined by path comparison because --show-toplevel returns
|
|
52
|
+
* the same value for all worktrees.
|
|
53
|
+
*/
|
|
54
|
+
async isMainWorktree(worktree) {
|
|
55
|
+
const worktrees = await this.listWorktrees();
|
|
56
|
+
const mainWorktree = worktrees[0];
|
|
57
|
+
return mainWorktree !== void 0 && mainWorktree.path === worktree.path;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if a worktree is a PR worktree based on naming patterns
|
|
61
|
+
* Ports: is_pr_worktree() from worktree-utils.sh
|
|
62
|
+
*/
|
|
63
|
+
isPRWorktree(worktree) {
|
|
64
|
+
return isPRBranch(worktree.branch);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get PR number from worktree branch name
|
|
68
|
+
* Ports: get_pr_number_from_worktree() from worktree-utils.sh
|
|
69
|
+
*/
|
|
70
|
+
getPRNumberFromWorktree(worktree) {
|
|
71
|
+
return extractPRNumber(worktree.branch);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create a new worktree
|
|
75
|
+
* Ports worktree creation logic from new-branch-workflow.sh
|
|
76
|
+
* @returns The absolute path to the created worktree
|
|
77
|
+
*/
|
|
78
|
+
async createWorktree(options) {
|
|
79
|
+
if (!options.branch) {
|
|
80
|
+
throw new Error("Branch name is required");
|
|
81
|
+
}
|
|
82
|
+
const absolutePath = path.resolve(options.path);
|
|
83
|
+
if (await fs.pathExists(absolutePath)) {
|
|
84
|
+
if (!options.force) {
|
|
85
|
+
throw new Error(`Path already exists: ${absolutePath}`);
|
|
86
|
+
}
|
|
87
|
+
await fs.remove(absolutePath);
|
|
88
|
+
}
|
|
89
|
+
const args = ["worktree", "add"];
|
|
90
|
+
if (options.createBranch) {
|
|
91
|
+
args.push("-b", options.branch);
|
|
92
|
+
}
|
|
93
|
+
if (options.force) {
|
|
94
|
+
args.push("--force");
|
|
95
|
+
}
|
|
96
|
+
args.push(absolutePath);
|
|
97
|
+
if (!options.createBranch) {
|
|
98
|
+
args.push(options.branch);
|
|
99
|
+
} else if (options.baseBranch) {
|
|
100
|
+
args.push(options.baseBranch);
|
|
101
|
+
}
|
|
102
|
+
await executeGitCommand(args, { cwd: this._workingDirectory });
|
|
103
|
+
return absolutePath;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Remove a worktree and optionally clean up associated files
|
|
107
|
+
* Ports worktree removal logic from cleanup-worktree.sh
|
|
108
|
+
* @returns A message describing what was done (for dry-run mode)
|
|
109
|
+
*/
|
|
110
|
+
async removeWorktree(worktreePath, options = {}) {
|
|
111
|
+
const worktrees = await this.listWorktrees({ porcelain: true });
|
|
112
|
+
const worktree = worktrees.find((wt) => wt.path === worktreePath);
|
|
113
|
+
if (!worktree) {
|
|
114
|
+
const { logger } = await import("./logger-MKYH4UDV.js");
|
|
115
|
+
logger.debug(`Looking for worktree path: ${worktreePath}`);
|
|
116
|
+
logger.debug(`Found ${worktrees.length} worktrees:`);
|
|
117
|
+
worktrees.forEach((wt, i) => {
|
|
118
|
+
logger.debug(` ${i}: path="${wt.path}", branch="${wt.branch}"`);
|
|
119
|
+
});
|
|
120
|
+
throw new Error(`Worktree not found: ${worktreePath}`);
|
|
121
|
+
}
|
|
122
|
+
if (!options.force && !options.dryRun) {
|
|
123
|
+
const hasChanges = await hasUncommittedChanges(worktreePath);
|
|
124
|
+
if (hasChanges) {
|
|
125
|
+
throw new Error(`Worktree has uncommitted changes: ${worktreePath}. Use --force to override.`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (options.dryRun) {
|
|
129
|
+
const actions = ["Remove worktree registration"];
|
|
130
|
+
if (options.removeDirectory) actions.push("Remove directory from disk");
|
|
131
|
+
if (options.removeBranch) actions.push(`Remove branch: ${worktree.branch}`);
|
|
132
|
+
return `Would perform: ${actions.join(", ")}`;
|
|
133
|
+
}
|
|
134
|
+
const args = ["worktree", "remove"];
|
|
135
|
+
if (options.force) args.push("--force");
|
|
136
|
+
args.push(worktreePath);
|
|
137
|
+
await executeGitCommand(args, { cwd: this._workingDirectory });
|
|
138
|
+
if (options.removeDirectory && await fs.pathExists(worktreePath)) {
|
|
139
|
+
await fs.remove(worktreePath);
|
|
140
|
+
}
|
|
141
|
+
if (options.removeBranch && !worktree.bare) {
|
|
142
|
+
try {
|
|
143
|
+
await executeGitCommand(["branch", "-D", worktree.branch], {
|
|
144
|
+
cwd: this._workingDirectory
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Worktree removed but failed to delete branch ${worktree.branch}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Validate worktree state and integrity
|
|
155
|
+
*/
|
|
156
|
+
async validateWorktree(worktreePath) {
|
|
157
|
+
const issues = [];
|
|
158
|
+
let existsOnDisk = false;
|
|
159
|
+
let isValidRepo = false;
|
|
160
|
+
let hasValidBranch = false;
|
|
161
|
+
try {
|
|
162
|
+
existsOnDisk = await fs.pathExists(worktreePath);
|
|
163
|
+
if (!existsOnDisk) {
|
|
164
|
+
issues.push("Worktree directory does not exist on disk");
|
|
165
|
+
}
|
|
166
|
+
if (existsOnDisk) {
|
|
167
|
+
isValidRepo = await isValidGitRepo(worktreePath);
|
|
168
|
+
if (!isValidRepo) {
|
|
169
|
+
issues.push("Directory is not a valid Git repository");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (isValidRepo) {
|
|
173
|
+
const currentBranch = await getCurrentBranch(worktreePath);
|
|
174
|
+
hasValidBranch = currentBranch !== null;
|
|
175
|
+
if (!hasValidBranch) {
|
|
176
|
+
issues.push("Could not determine current branch");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const worktrees = await this.listWorktrees();
|
|
180
|
+
const isRegistered = worktrees.some((wt) => wt.path === worktreePath);
|
|
181
|
+
if (!isRegistered) {
|
|
182
|
+
issues.push("Worktree is not registered with Git");
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
issues.push(`Validation error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
isValid: issues.length === 0,
|
|
189
|
+
issues,
|
|
190
|
+
existsOnDisk,
|
|
191
|
+
isValidRepo,
|
|
192
|
+
hasValidBranch
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Get detailed status information for a worktree
|
|
197
|
+
*/
|
|
198
|
+
async getWorktreeStatus(worktreePath) {
|
|
199
|
+
const statusOutput = await executeGitCommand(["status", "--porcelain=v1"], {
|
|
200
|
+
cwd: worktreePath
|
|
201
|
+
});
|
|
202
|
+
let modified = 0;
|
|
203
|
+
let staged = 0;
|
|
204
|
+
let deleted = 0;
|
|
205
|
+
let untracked = 0;
|
|
206
|
+
const lines = statusOutput.trim().split("\n").filter(Boolean);
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
const status = line.substring(0, 2);
|
|
209
|
+
if (status[0] === "M" || status[1] === "M") modified++;
|
|
210
|
+
if (status[0] === "A" || status[0] === "D" || status[0] === "R") staged++;
|
|
211
|
+
if (status[0] === "D" || status[1] === "D") deleted++;
|
|
212
|
+
if (status === "??") untracked++;
|
|
213
|
+
}
|
|
214
|
+
const currentBranch = await getCurrentBranch(worktreePath) ?? "unknown";
|
|
215
|
+
const detached = currentBranch === "unknown";
|
|
216
|
+
let ahead = 0;
|
|
217
|
+
let behind = 0;
|
|
218
|
+
try {
|
|
219
|
+
const aheadBehindOutput = await executeGitCommand(
|
|
220
|
+
["rev-list", "--left-right", "--count", `origin/${currentBranch}...HEAD`],
|
|
221
|
+
{ cwd: worktreePath }
|
|
222
|
+
);
|
|
223
|
+
const parts = aheadBehindOutput.trim().split(" ");
|
|
224
|
+
const behindStr = parts[0];
|
|
225
|
+
const aheadStr = parts[1];
|
|
226
|
+
behind = behindStr ? parseInt(behindStr, 10) || 0 : 0;
|
|
227
|
+
ahead = aheadStr ? parseInt(aheadStr, 10) || 0 : 0;
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
modified,
|
|
232
|
+
staged,
|
|
233
|
+
deleted,
|
|
234
|
+
untracked,
|
|
235
|
+
hasChanges: modified + staged + deleted + untracked > 0,
|
|
236
|
+
branch: currentBranch,
|
|
237
|
+
detached,
|
|
238
|
+
ahead,
|
|
239
|
+
behind
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Generate a suggested worktree path for a branch
|
|
244
|
+
*/
|
|
245
|
+
generateWorktreePath(branchName, customRoot, options) {
|
|
246
|
+
const root = customRoot ?? this._workingDirectory;
|
|
247
|
+
return generateWorktreePath(branchName, root, options);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Sanitize a branch name for use as a directory name
|
|
251
|
+
* Replaces slashes with dashes and removes invalid filesystem characters
|
|
252
|
+
* Ports logic from bash script line 593: ${BRANCH_NAME//\\//-}
|
|
253
|
+
*/
|
|
254
|
+
sanitizeBranchName(branchName) {
|
|
255
|
+
return branchName.replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Check if repository is in a valid state for worktree operations
|
|
259
|
+
*/
|
|
260
|
+
async isRepoReady() {
|
|
261
|
+
try {
|
|
262
|
+
const repoRoot = await getRepoRoot(this._workingDirectory);
|
|
263
|
+
return repoRoot !== null;
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get repository information
|
|
270
|
+
*/
|
|
271
|
+
async getRepoInfo() {
|
|
272
|
+
const root = await getRepoRoot(this._workingDirectory);
|
|
273
|
+
const defaultBranch = await getDefaultBranch(this._workingDirectory);
|
|
274
|
+
const currentBranch = await getCurrentBranch(this._workingDirectory);
|
|
275
|
+
return {
|
|
276
|
+
root,
|
|
277
|
+
defaultBranch,
|
|
278
|
+
currentBranch
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Prune stale worktree entries (worktrees that no longer exist on disk)
|
|
283
|
+
*/
|
|
284
|
+
async pruneWorktrees() {
|
|
285
|
+
await executeGitCommand(["worktree", "prune", "-v"], { cwd: this._workingDirectory });
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Lock a worktree to prevent it from being pruned or moved
|
|
289
|
+
*/
|
|
290
|
+
async lockWorktree(worktreePath, reason) {
|
|
291
|
+
const args = ["worktree", "lock", worktreePath];
|
|
292
|
+
if (reason) args.push("--reason", reason);
|
|
293
|
+
await executeGitCommand(args, { cwd: this._workingDirectory });
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Unlock a previously locked worktree
|
|
297
|
+
*/
|
|
298
|
+
async unlockWorktree(worktreePath) {
|
|
299
|
+
await executeGitCommand(["worktree", "unlock", worktreePath], { cwd: this._workingDirectory });
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Find worktrees matching an identifier (branch name, path, or PR number)
|
|
303
|
+
*/
|
|
304
|
+
async findWorktreesByIdentifier(identifier) {
|
|
305
|
+
const worktrees = await this.listWorktrees({ porcelain: true });
|
|
306
|
+
return worktrees.filter(
|
|
307
|
+
(wt) => {
|
|
308
|
+
var _a;
|
|
309
|
+
return wt.branch.includes(identifier) || wt.path.includes(identifier) || ((_a = this.getPRNumberFromWorktree(wt)) == null ? void 0 : _a.toString()) === identifier;
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Find worktree for a specific issue number using exact pattern matching
|
|
315
|
+
* Matches: issue-{N} at start OR after /, -, _ (but NOT issue-{N}X where X is a digit)
|
|
316
|
+
* Supports patterns like: issue-44, feat/issue-44-feature, feat-issue-44, bugfix_issue-44, etc.
|
|
317
|
+
* Avoids false matches like: tissue-44, myissue-44
|
|
318
|
+
* Ports: find_existing_worktree() from bash script lines 131-165
|
|
319
|
+
*/
|
|
320
|
+
async findWorktreeForIssue(issueNumber) {
|
|
321
|
+
const worktrees = await this.listWorktrees({ porcelain: true });
|
|
322
|
+
const pattern = new RegExp(`(?:^|[/_-])issue-${issueNumber}(?:-|$)`);
|
|
323
|
+
return worktrees.find((wt) => pattern.test(wt.branch)) ?? null;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Find worktree for a specific PR by branch name
|
|
327
|
+
* Ports: find_existing_worktree() for PR type from bash script lines 149-160
|
|
328
|
+
*/
|
|
329
|
+
async findWorktreeForPR(prNumber, branchName) {
|
|
330
|
+
const worktrees = await this.listWorktrees({ porcelain: true });
|
|
331
|
+
const byBranch = worktrees.find((wt) => wt.branch === branchName);
|
|
332
|
+
if (byBranch) return byBranch;
|
|
333
|
+
const pathPattern = new RegExp(`_pr_${prNumber}$`);
|
|
334
|
+
return worktrees.find((wt) => pathPattern.test(wt.path)) ?? null;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Remove multiple worktrees
|
|
338
|
+
* Returns a summary of successes and failures
|
|
339
|
+
* Automatically filters out the main worktree
|
|
340
|
+
*/
|
|
341
|
+
async removeWorktrees(worktrees, options = {}) {
|
|
342
|
+
const successes = [];
|
|
343
|
+
const failures = [];
|
|
344
|
+
const skipped = [];
|
|
345
|
+
for (const worktree of worktrees) {
|
|
346
|
+
if (await this.isMainWorktree(worktree)) {
|
|
347
|
+
skipped.push({ worktree, reason: "Cannot remove main worktree" });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
await this.removeWorktree(worktree.path, {
|
|
352
|
+
...options,
|
|
353
|
+
removeDirectory: true
|
|
354
|
+
});
|
|
355
|
+
successes.push({ worktree });
|
|
356
|
+
} catch (error) {
|
|
357
|
+
failures.push({
|
|
358
|
+
worktree,
|
|
359
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return { successes, failures, skipped };
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Format worktree information for display
|
|
367
|
+
*/
|
|
368
|
+
formatWorktree(worktree) {
|
|
369
|
+
const prNumber = this.getPRNumberFromWorktree(worktree);
|
|
370
|
+
const prLabel = prNumber ? ` (PR #${prNumber})` : "";
|
|
371
|
+
const bareLabel = worktree.bare ? " [main]" : "";
|
|
372
|
+
return {
|
|
373
|
+
title: `${worktree.branch}${prLabel}${bareLabel}`,
|
|
374
|
+
path: worktree.path,
|
|
375
|
+
commit: worktree.commit.substring(0, 7)
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
export {
|
|
381
|
+
GitWorktreeManager
|
|
382
|
+
};
|
|
383
|
+
//# sourceMappingURL=chunk-QEPVTTHD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/GitWorktreeManager.ts"],"sourcesContent":["import path from 'path'\nimport fs from 'fs-extra'\nimport {\n type GitWorktree,\n type WorktreeCreateOptions,\n type WorktreeListOptions,\n type WorktreeValidation,\n type WorktreeStatus,\n type WorktreeCleanupOptions,\n} from '../types/worktree.js'\nimport {\n executeGitCommand,\n parseWorktreeList,\n isPRBranch,\n extractPRNumber,\n generateWorktreePath,\n isValidGitRepo,\n getCurrentBranch,\n getRepoRoot,\n hasUncommittedChanges,\n getDefaultBranch,\n} from '../utils/git.js'\n\n/**\n * Manages Git worktrees for the iloom CLI\n * Ports functionality from bash scripts into TypeScript\n */\nexport class GitWorktreeManager {\n private readonly _workingDirectory: string\n\n constructor(workingDirectory: string = process.cwd()) {\n this._workingDirectory = workingDirectory\n }\n\n /**\n * Get the working directory for git operations (main worktree path)\n */\n get workingDirectory(): string {\n return this._workingDirectory\n }\n\n /**\n * List all worktrees in the repository\n * Defaults to porcelain format for reliable machine parsing\n * Equivalent to: git worktree list --porcelain\n */\n async listWorktrees(options: WorktreeListOptions = {}): Promise<GitWorktree[]> {\n const args = ['worktree', 'list']\n // Default to porcelain format for consistent parsing (can be disabled with porcelain: false)\n if (options.porcelain !== false) args.push('--porcelain')\n if (options.verbose) args.push('-v')\n\n const output = await executeGitCommand(args, { cwd: this._workingDirectory })\n return parseWorktreeList(output)\n }\n\n /**\n * Find worktree for a specific branch\n * Ports: find_worktree_for_branch() from find-worktree-for-branch.sh\n */\n async findWorktreeForBranch(branchName: string): Promise<GitWorktree | null> {\n const worktrees = await this.listWorktrees()\n return worktrees.find(wt => wt.branch === branchName) ?? null\n }\n\n /**\n * Check if a worktree is the main repository worktree\n * The main worktree is the first one listed by git worktree list (Git guarantee)\n * This cannot be determined by path comparison because --show-toplevel returns\n * the same value for all worktrees.\n */\n async isMainWorktree(worktree: GitWorktree): Promise<boolean> {\n const worktrees = await this.listWorktrees()\n // The first worktree is always the main worktree (Git design)\n const mainWorktree = worktrees[0]\n return mainWorktree !== undefined && mainWorktree.path === worktree.path\n }\n\n /**\n * Check if a worktree is a PR worktree based on naming patterns\n * Ports: is_pr_worktree() from worktree-utils.sh\n */\n isPRWorktree(worktree: GitWorktree): boolean {\n return isPRBranch(worktree.branch)\n }\n\n /**\n * Get PR number from worktree branch name\n * Ports: get_pr_number_from_worktree() from worktree-utils.sh\n */\n getPRNumberFromWorktree(worktree: GitWorktree): number | null {\n return extractPRNumber(worktree.branch)\n }\n\n /**\n * Create a new worktree\n * Ports worktree creation logic from new-branch-workflow.sh\n * @returns The absolute path to the created worktree\n */\n async createWorktree(options: WorktreeCreateOptions): Promise<string> {\n // Validate inputs\n if (!options.branch) {\n throw new Error('Branch name is required')\n }\n\n // Ensure path is absolute\n const absolutePath = path.resolve(options.path)\n\n // Check if path already exists and handle force flag\n if (await fs.pathExists(absolutePath)) {\n if (!options.force) {\n throw new Error(`Path already exists: ${absolutePath}`)\n }\n // Remove existing directory if force is true\n await fs.remove(absolutePath)\n }\n\n // Build git worktree add command\n const args = ['worktree', 'add']\n\n if (options.createBranch) {\n args.push('-b', options.branch)\n }\n\n if (options.force) {\n args.push('--force')\n }\n\n args.push(absolutePath)\n\n // Add branch name if not creating new branch\n if (!options.createBranch) {\n args.push(options.branch)\n } else if (options.baseBranch) {\n args.push(options.baseBranch)\n }\n\n await executeGitCommand(args, { cwd: this._workingDirectory })\n return absolutePath\n }\n\n /**\n * Remove a worktree and optionally clean up associated files\n * Ports worktree removal logic from cleanup-worktree.sh\n * @returns A message describing what was done (for dry-run mode)\n */\n async removeWorktree(\n worktreePath: string,\n options: WorktreeCleanupOptions = {}\n ): Promise<string | void> {\n // Validate worktree exists - use porcelain format for consistent parsing\n const worktrees = await this.listWorktrees({ porcelain: true })\n const worktree = worktrees.find(wt => wt.path === worktreePath)\n\n if (!worktree) {\n // Add debug logging to help diagnose the issue\n const { logger } = await import('../utils/logger.js')\n logger.debug(`Looking for worktree path: ${worktreePath}`)\n logger.debug(`Found ${worktrees.length} worktrees:`)\n worktrees.forEach((wt, i) => {\n logger.debug(` ${i}: path=\"${wt.path}\", branch=\"${wt.branch}\"`)\n })\n throw new Error(`Worktree not found: ${worktreePath}`)\n }\n\n // Check for uncommitted changes unless force is specified\n if (!options.force && !options.dryRun) {\n const hasChanges = await hasUncommittedChanges(worktreePath)\n if (hasChanges) {\n throw new Error(`Worktree has uncommitted changes: ${worktreePath}. Use --force to override.`)\n }\n }\n\n if (options.dryRun) {\n const actions = ['Remove worktree registration']\n if (options.removeDirectory) actions.push('Remove directory from disk')\n if (options.removeBranch) actions.push(`Remove branch: ${worktree.branch}`)\n\n return `Would perform: ${actions.join(', ')}`\n }\n\n // Remove worktree registration\n const args = ['worktree', 'remove']\n if (options.force) args.push('--force')\n args.push(worktreePath)\n\n await executeGitCommand(args, { cwd: this._workingDirectory })\n\n // Remove directory if requested\n if (options.removeDirectory && (await fs.pathExists(worktreePath))) {\n await fs.remove(worktreePath)\n }\n\n // Remove branch if requested and safe to do so\n if (options.removeBranch && !worktree.bare) {\n try {\n await executeGitCommand(['branch', '-D', worktree.branch], {\n cwd: this._workingDirectory,\n })\n } catch (error) {\n // Don't fail the whole operation if branch deletion fails\n // Just log a warning (caller can handle this)\n throw new Error(\n `Worktree removed but failed to delete branch ${worktree.branch}: ${error instanceof Error ? error.message : 'Unknown error'}`\n )\n }\n }\n }\n\n /**\n * Validate worktree state and integrity\n */\n async validateWorktree(worktreePath: string): Promise<WorktreeValidation> {\n const issues: string[] = []\n let existsOnDisk = false\n let isValidRepo = false\n let hasValidBranch = false\n\n try {\n // Check if path exists on disk\n existsOnDisk = await fs.pathExists(worktreePath)\n if (!existsOnDisk) {\n issues.push('Worktree directory does not exist on disk')\n }\n\n // Check if it's a valid Git repository\n if (existsOnDisk) {\n isValidRepo = await isValidGitRepo(worktreePath)\n if (!isValidRepo) {\n issues.push('Directory is not a valid Git repository')\n }\n }\n\n // Check if branch reference is valid\n if (isValidRepo) {\n const currentBranch = await getCurrentBranch(worktreePath)\n hasValidBranch = currentBranch !== null\n if (!hasValidBranch) {\n issues.push('Could not determine current branch')\n }\n }\n\n // Check if worktree is registered with Git\n const worktrees = await this.listWorktrees()\n const isRegistered = worktrees.some(wt => wt.path === worktreePath)\n if (!isRegistered) {\n issues.push('Worktree is not registered with Git')\n }\n } catch (error) {\n issues.push(`Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`)\n }\n\n return {\n isValid: issues.length === 0,\n issues,\n existsOnDisk,\n isValidRepo,\n hasValidBranch,\n }\n }\n\n /**\n * Get detailed status information for a worktree\n */\n async getWorktreeStatus(worktreePath: string): Promise<WorktreeStatus> {\n const statusOutput = await executeGitCommand(['status', '--porcelain=v1'], {\n cwd: worktreePath,\n })\n\n let modified = 0\n let staged = 0\n let deleted = 0\n let untracked = 0\n\n const lines = statusOutput.trim().split('\\n').filter(Boolean)\n for (const line of lines) {\n const status = line.substring(0, 2)\n if (status[0] === 'M' || status[1] === 'M') modified++\n if (status[0] === 'A' || status[0] === 'D' || status[0] === 'R') staged++\n if (status[0] === 'D' || status[1] === 'D') deleted++\n if (status === '??') untracked++\n }\n\n const currentBranch = (await getCurrentBranch(worktreePath)) ?? 'unknown'\n const detached = currentBranch === 'unknown'\n\n // Get ahead/behind information\n let ahead = 0\n let behind = 0\n try {\n const aheadBehindOutput = await executeGitCommand(\n ['rev-list', '--left-right', '--count', `origin/${currentBranch}...HEAD`],\n { cwd: worktreePath }\n )\n const parts = aheadBehindOutput.trim().split('\\t')\n const behindStr = parts[0]\n const aheadStr = parts[1]\n behind = behindStr ? parseInt(behindStr, 10) || 0 : 0\n ahead = aheadStr ? parseInt(aheadStr, 10) || 0 : 0\n } catch {\n // Ignore errors for ahead/behind calculation\n }\n\n return {\n modified,\n staged,\n deleted,\n untracked,\n hasChanges: modified + staged + deleted + untracked > 0,\n branch: currentBranch,\n detached,\n ahead,\n behind,\n }\n }\n\n /**\n * Generate a suggested worktree path for a branch\n */\n generateWorktreePath(\n branchName: string,\n customRoot?: string,\n options?: { isPR?: boolean; prNumber?: number; prefix?: string }\n ): string {\n const root = customRoot ?? this._workingDirectory\n return generateWorktreePath(branchName, root, options)\n }\n\n /**\n * Sanitize a branch name for use as a directory name\n * Replaces slashes with dashes and removes invalid filesystem characters\n * Ports logic from bash script line 593: ${BRANCH_NAME//\\\\//-}\n */\n sanitizeBranchName(branchName: string): string {\n return branchName\n .replace(/\\//g, '-') // Replace slashes with dashes\n .replace(/[^a-zA-Z0-9-]/g, '-') // Replace invalid chars (including underscores) with dashes\n .replace(/-+/g, '-') // Collapse multiple dashes\n .replace(/^-|-$/g, '') // Remove leading/trailing dashes\n .toLowerCase()\n }\n\n /**\n * Check if repository is in a valid state for worktree operations\n */\n async isRepoReady(): Promise<boolean> {\n try {\n const repoRoot = await getRepoRoot(this._workingDirectory)\n return repoRoot !== null\n } catch {\n return false\n }\n }\n\n /**\n * Get repository information\n */\n async getRepoInfo(): Promise<{\n root: string | null\n defaultBranch: string\n currentBranch: string | null\n }> {\n const root = await getRepoRoot(this._workingDirectory)\n const defaultBranch = await getDefaultBranch(this._workingDirectory)\n const currentBranch = await getCurrentBranch(this._workingDirectory)\n\n return {\n root,\n defaultBranch,\n currentBranch,\n }\n }\n\n /**\n * Prune stale worktree entries (worktrees that no longer exist on disk)\n */\n async pruneWorktrees(): Promise<void> {\n await executeGitCommand(['worktree', 'prune', '-v'], { cwd: this._workingDirectory })\n }\n\n /**\n * Lock a worktree to prevent it from being pruned or moved\n */\n async lockWorktree(worktreePath: string, reason?: string): Promise<void> {\n const args = ['worktree', 'lock', worktreePath]\n if (reason) args.push('--reason', reason)\n\n await executeGitCommand(args, { cwd: this._workingDirectory })\n }\n\n /**\n * Unlock a previously locked worktree\n */\n async unlockWorktree(worktreePath: string): Promise<void> {\n await executeGitCommand(['worktree', 'unlock', worktreePath], { cwd: this._workingDirectory })\n }\n\n /**\n * Find worktrees matching an identifier (branch name, path, or PR number)\n */\n async findWorktreesByIdentifier(identifier: string): Promise<GitWorktree[]> {\n const worktrees = await this.listWorktrees({ porcelain: true })\n return worktrees.filter(\n wt =>\n wt.branch.includes(identifier) ||\n wt.path.includes(identifier) ||\n this.getPRNumberFromWorktree(wt)?.toString() === identifier\n )\n }\n\n /**\n * Find worktree for a specific issue number using exact pattern matching\n * Matches: issue-{N} at start OR after /, -, _ (but NOT issue-{N}X where X is a digit)\n * Supports patterns like: issue-44, feat/issue-44-feature, feat-issue-44, bugfix_issue-44, etc.\n * Avoids false matches like: tissue-44, myissue-44\n * Ports: find_existing_worktree() from bash script lines 131-165\n */\n async findWorktreeForIssue(issueNumber: number): Promise<GitWorktree | null> {\n const worktrees = await this.listWorktrees({ porcelain: true })\n\n // Pattern: starts with 'issue-{N}' OR has '/issue-{N}', '-issue-{N}', '_issue-{N}' but not 'issue-{N}{digit}'\n const pattern = new RegExp(`(?:^|[/_-])issue-${issueNumber}(?:-|$)`)\n\n return worktrees.find(wt => pattern.test(wt.branch)) ?? null\n }\n\n /**\n * Find worktree for a specific PR by branch name\n * Ports: find_existing_worktree() for PR type from bash script lines 149-160\n */\n async findWorktreeForPR(prNumber: number, branchName: string): Promise<GitWorktree | null> {\n const worktrees = await this.listWorktrees({ porcelain: true })\n\n // Find by exact branch name match (prioritized)\n const byBranch = worktrees.find(wt => wt.branch === branchName)\n if (byBranch) return byBranch\n\n // Also check directory name pattern: *_pr_{N}\n const pathPattern = new RegExp(`_pr_${prNumber}$`)\n return worktrees.find(wt => pathPattern.test(wt.path)) ?? null\n }\n\n /**\n * Remove multiple worktrees\n * Returns a summary of successes and failures\n * Automatically filters out the main worktree\n */\n async removeWorktrees(\n worktrees: GitWorktree[],\n options: WorktreeCleanupOptions = {}\n ): Promise<{\n successes: Array<{ worktree: GitWorktree }>\n failures: Array<{ worktree: GitWorktree; error: string }>\n skipped: Array<{ worktree: GitWorktree; reason: string }>\n }> {\n const successes: Array<{ worktree: GitWorktree }> = []\n const failures: Array<{ worktree: GitWorktree; error: string }> = []\n const skipped: Array<{ worktree: GitWorktree; reason: string }> = []\n\n for (const worktree of worktrees) {\n // Skip main worktree\n if (await this.isMainWorktree(worktree)) {\n skipped.push({ worktree, reason: 'Cannot remove main worktree' })\n continue\n }\n\n try {\n await this.removeWorktree(worktree.path, {\n ...options,\n removeDirectory: true,\n })\n successes.push({ worktree })\n } catch (error) {\n failures.push({\n worktree,\n error: error instanceof Error ? error.message : 'Unknown error',\n })\n }\n }\n\n return { successes, failures, skipped }\n }\n\n /**\n * Format worktree information for display\n */\n formatWorktree(worktree: GitWorktree): {\n title: string\n path: string\n commit: string\n } {\n const prNumber = this.getPRNumberFromWorktree(worktree)\n const prLabel = prNumber ? ` (PR #${prNumber})` : ''\n const bareLabel = worktree.bare ? ' [main]' : ''\n\n return {\n title: `${worktree.branch}${prLabel}${bareLabel}`,\n path: worktree.path,\n commit: worktree.commit.substring(0, 7),\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/terminal.ts
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
function detectPlatform() {
|
|
7
|
+
const platform = process.platform;
|
|
8
|
+
if (platform === "darwin") return "darwin";
|
|
9
|
+
if (platform === "linux") return "linux";
|
|
10
|
+
if (platform === "win32") return "win32";
|
|
11
|
+
return "unsupported";
|
|
12
|
+
}
|
|
13
|
+
async function detectITerm2() {
|
|
14
|
+
const platform = detectPlatform();
|
|
15
|
+
if (platform !== "darwin") return false;
|
|
16
|
+
return existsSync("/Applications/iTerm.app");
|
|
17
|
+
}
|
|
18
|
+
async function openTerminalWindow(options) {
|
|
19
|
+
const platform = detectPlatform();
|
|
20
|
+
if (platform !== "darwin") {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Terminal window launching not yet supported on ${platform}. Currently only macOS is supported.`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const applescript = buildAppleScript(options);
|
|
26
|
+
try {
|
|
27
|
+
await execa("osascript", ["-e", applescript]);
|
|
28
|
+
await execa("osascript", ["-e", 'tell application "Terminal" to activate']);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Failed to open terminal window: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function buildAppleScript(options) {
|
|
36
|
+
const {
|
|
37
|
+
workspacePath,
|
|
38
|
+
command,
|
|
39
|
+
backgroundColor,
|
|
40
|
+
port,
|
|
41
|
+
includeEnvSetup,
|
|
42
|
+
includePortExport
|
|
43
|
+
} = options;
|
|
44
|
+
const commands = [];
|
|
45
|
+
if (workspacePath) {
|
|
46
|
+
commands.push(`cd '${escapePathForAppleScript(workspacePath)}'`);
|
|
47
|
+
}
|
|
48
|
+
if (includeEnvSetup) {
|
|
49
|
+
commands.push("source .env");
|
|
50
|
+
}
|
|
51
|
+
if (includePortExport && port !== void 0) {
|
|
52
|
+
commands.push(`export PORT=${port}`);
|
|
53
|
+
}
|
|
54
|
+
if (command) {
|
|
55
|
+
commands.push(command);
|
|
56
|
+
}
|
|
57
|
+
const fullCommand = commands.join(" && ");
|
|
58
|
+
const historyFreeCommand = ` ${fullCommand}`;
|
|
59
|
+
let script = `tell application "Terminal"
|
|
60
|
+
`;
|
|
61
|
+
script += ` set newTab to do script "${escapeForAppleScript(historyFreeCommand)}"
|
|
62
|
+
`;
|
|
63
|
+
if (backgroundColor) {
|
|
64
|
+
const { r, g, b } = backgroundColor;
|
|
65
|
+
script += ` set background color of newTab to {${Math.round(r * 257)}, ${Math.round(g * 257)}, ${Math.round(b * 257)}}
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
script += `end tell`;
|
|
69
|
+
return script;
|
|
70
|
+
}
|
|
71
|
+
function escapePathForAppleScript(path) {
|
|
72
|
+
return path.replace(/'/g, "'\\''");
|
|
73
|
+
}
|
|
74
|
+
function escapeForAppleScript(command) {
|
|
75
|
+
return command.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
76
|
+
}
|
|
77
|
+
function buildCommandSequence(options) {
|
|
78
|
+
const {
|
|
79
|
+
workspacePath,
|
|
80
|
+
command,
|
|
81
|
+
port,
|
|
82
|
+
includeEnvSetup,
|
|
83
|
+
includePortExport
|
|
84
|
+
} = options;
|
|
85
|
+
const commands = [];
|
|
86
|
+
if (workspacePath) {
|
|
87
|
+
commands.push(`cd '${escapePathForAppleScript(workspacePath)}'`);
|
|
88
|
+
}
|
|
89
|
+
if (includeEnvSetup) {
|
|
90
|
+
commands.push("source .env");
|
|
91
|
+
}
|
|
92
|
+
if (includePortExport && port !== void 0) {
|
|
93
|
+
commands.push(`export PORT=${port}`);
|
|
94
|
+
}
|
|
95
|
+
if (command) {
|
|
96
|
+
commands.push(command);
|
|
97
|
+
}
|
|
98
|
+
const fullCommand = commands.join(" && ");
|
|
99
|
+
return ` ${fullCommand}`;
|
|
100
|
+
}
|
|
101
|
+
function buildITerm2MultiTabScript(optionsArray) {
|
|
102
|
+
if (optionsArray.length < 2) {
|
|
103
|
+
throw new Error("buildITerm2MultiTabScript requires at least 2 terminal options");
|
|
104
|
+
}
|
|
105
|
+
let script = 'tell application id "com.googlecode.iterm2"\n';
|
|
106
|
+
script += " create window with default profile\n";
|
|
107
|
+
script += " set newWindow to current window\n";
|
|
108
|
+
const options1 = optionsArray[0];
|
|
109
|
+
if (!options1) {
|
|
110
|
+
throw new Error("First terminal option is undefined");
|
|
111
|
+
}
|
|
112
|
+
const command1 = buildCommandSequence(options1);
|
|
113
|
+
script += " set s1 to current session of newWindow\n\n";
|
|
114
|
+
if (options1.backgroundColor) {
|
|
115
|
+
const { r, g, b } = options1.backgroundColor;
|
|
116
|
+
script += ` set background color of s1 to {${Math.round(r * 257)}, ${Math.round(g * 257)}, ${Math.round(b * 257)}}
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
script += ` tell s1 to write text "${escapeForAppleScript(command1)}"
|
|
120
|
+
|
|
121
|
+
`;
|
|
122
|
+
if (options1.title) {
|
|
123
|
+
script += ` set name of s1 to "${escapeForAppleScript(options1.title)}"
|
|
124
|
+
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
for (let i = 1; i < optionsArray.length; i++) {
|
|
128
|
+
const options = optionsArray[i];
|
|
129
|
+
if (!options) {
|
|
130
|
+
throw new Error(`Terminal option at index ${i} is undefined`);
|
|
131
|
+
}
|
|
132
|
+
const command = buildCommandSequence(options);
|
|
133
|
+
const sessionVar = `s${i + 1}`;
|
|
134
|
+
script += " tell newWindow\n";
|
|
135
|
+
script += ` set newTab${i} to (create tab with default profile)
|
|
136
|
+
`;
|
|
137
|
+
script += " end tell\n";
|
|
138
|
+
script += ` set ${sessionVar} to current session of newTab${i}
|
|
139
|
+
|
|
140
|
+
`;
|
|
141
|
+
if (options.backgroundColor) {
|
|
142
|
+
const { r, g, b } = options.backgroundColor;
|
|
143
|
+
script += ` set background color of ${sessionVar} to {${Math.round(r * 257)}, ${Math.round(g * 257)}, ${Math.round(b * 257)}}
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
script += ` tell ${sessionVar} to write text "${escapeForAppleScript(command)}"
|
|
147
|
+
|
|
148
|
+
`;
|
|
149
|
+
if (options.title) {
|
|
150
|
+
script += ` set name of ${sessionVar} to "${escapeForAppleScript(options.title)}"
|
|
151
|
+
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
script += " activate\n";
|
|
156
|
+
script += "end tell";
|
|
157
|
+
return script;
|
|
158
|
+
}
|
|
159
|
+
async function openMultipleTerminalWindows(optionsArray) {
|
|
160
|
+
if (optionsArray.length < 2) {
|
|
161
|
+
throw new Error("openMultipleTerminalWindows requires at least 2 terminal options. Use openTerminalWindow for single terminal.");
|
|
162
|
+
}
|
|
163
|
+
const platform = detectPlatform();
|
|
164
|
+
if (platform !== "darwin") {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Terminal window launching not yet supported on ${platform}. Currently only macOS is supported.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const hasITerm2 = await detectITerm2();
|
|
170
|
+
if (hasITerm2) {
|
|
171
|
+
const applescript = buildITerm2MultiTabScript(optionsArray);
|
|
172
|
+
try {
|
|
173
|
+
await execa("osascript", ["-e", applescript]);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Failed to open iTerm2 window: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
for (let i = 0; i < optionsArray.length; i++) {
|
|
181
|
+
const options = optionsArray[i];
|
|
182
|
+
if (!options) {
|
|
183
|
+
throw new Error(`Terminal option at index ${i} is undefined`);
|
|
184
|
+
}
|
|
185
|
+
await openTerminalWindow(options);
|
|
186
|
+
if (i < optionsArray.length - 1) {
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function openDualTerminalWindow(options1, options2) {
|
|
193
|
+
await openMultipleTerminalWindows([options1, options2]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export {
|
|
197
|
+
detectPlatform,
|
|
198
|
+
detectITerm2,
|
|
199
|
+
openTerminalWindow,
|
|
200
|
+
openMultipleTerminalWindows,
|
|
201
|
+
openDualTerminalWindow
|
|
202
|
+
};
|
|
203
|
+
//# sourceMappingURL=chunk-RSRO7564.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/terminal.ts"],"sourcesContent":["import { execa } from 'execa'\nimport { existsSync } from 'node:fs'\nimport type { Platform } from '../types/index.js'\n\nexport interface TerminalWindowOptions {\n\tworkspacePath?: string\n\tcommand?: string\n\tbackgroundColor?: { r: number; g: number; b: number }\n\tport?: number\n\tincludeEnvSetup?: boolean // source .env\n\tincludePortExport?: boolean // export PORT=<port>\n\ttitle?: string // Terminal tab title\n}\n\n/**\n * Detect current platform\n */\nexport function detectPlatform(): Platform {\n\tconst platform = process.platform\n\tif (platform === 'darwin') return 'darwin'\n\tif (platform === 'linux') return 'linux'\n\tif (platform === 'win32') return 'win32'\n\treturn 'unsupported'\n}\n\n/**\n * Detect if iTerm2 is installed on macOS\n * Returns false on non-macOS platforms\n */\nexport async function detectITerm2(): Promise<boolean> {\n\tconst platform = detectPlatform()\n\tif (platform !== 'darwin') return false\n\n\t// Check if iTerm.app exists at standard location\n\treturn existsSync('/Applications/iTerm.app')\n}\n\n/**\n * Open new terminal window with specified options\n * Currently supports macOS only\n */\nexport async function openTerminalWindow(\n\toptions: TerminalWindowOptions\n): Promise<void> {\n\tconst platform = detectPlatform()\n\n\tif (platform !== 'darwin') {\n\t\tthrow new Error(\n\t\t\t`Terminal window launching not yet supported on ${platform}. ` +\n\t\t\t\t`Currently only macOS is supported.`\n\t\t)\n\t}\n\n\t// macOS implementation using AppleScript\n\tconst applescript = buildAppleScript(options)\n\n\ttry {\n\t\tawait execa('osascript', ['-e', applescript])\n\n\t\t// Activate Terminal.app to bring windows to front\n\t\tawait execa('osascript', ['-e', 'tell application \"Terminal\" to activate'])\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`Failed to open terminal window: ${error instanceof Error ? error.message : 'Unknown error'}`\n\t\t)\n\t}\n}\n\n/**\n * Build AppleScript for macOS Terminal.app\n */\nfunction buildAppleScript(options: TerminalWindowOptions): string {\n\tconst {\n\t\tworkspacePath,\n\t\tcommand,\n\t\tbackgroundColor,\n\t\tport,\n\t\tincludeEnvSetup,\n\t\tincludePortExport,\n\t} = options\n\n\t// Build command sequence\n\tconst commands: string[] = []\n\n\t// Navigate to workspace\n\tif (workspacePath) {\n\t\tcommands.push(`cd '${escapePathForAppleScript(workspacePath)}'`)\n\t}\n\n\t// Source .env file\n\tif (includeEnvSetup) {\n\t\tcommands.push('source .env')\n\t}\n\n\t// Export PORT variable\n\tif (includePortExport && port !== undefined) {\n\t\tcommands.push(`export PORT=${port}`)\n\t}\n\n\t// Add custom command\n\tif (command) {\n\t\tcommands.push(command)\n\t}\n\n\t// Join with &&\n\tconst fullCommand = commands.join(' && ')\n\n\t// Prefix with space to prevent shell history pollution\n\t// Most shells (bash/zsh) ignore commands starting with space when HISTCONTROL=ignorespace\n\tconst historyFreeCommand = ` ${fullCommand}`\n\n\t// Build AppleScript\n\tlet script = `tell application \"Terminal\"\\n`\n\tscript += ` set newTab to do script \"${escapeForAppleScript(historyFreeCommand)}\"\\n`\n\n\t// Apply background color if provided\n\tif (backgroundColor) {\n\t\tconst { r, g, b } = backgroundColor\n\t\t// Convert 8-bit RGB (0-255) to 16-bit RGB (0-65535)\n\t\tscript += ` set background color of newTab to {${Math.round(r * 257)}, ${Math.round(g * 257)}, ${Math.round(b * 257)}}\\n`\n\t}\n\n\tscript += `end tell`\n\n\treturn script\n}\n\n/**\n * Escape path for AppleScript string\n * Single quotes in path need special escaping\n */\nfunction escapePathForAppleScript(path: string): string {\n\t// Replace single quote with '\\''\n\treturn path.replace(/'/g, \"'\\\\''\")\n}\n\n/**\n * Escape command for AppleScript do script\n * Must handle double quotes and backslashes\n */\nfunction escapeForAppleScript(command: string): string {\n\treturn (\n\t\tcommand\n\t\t\t.replace(/\\\\/g, '\\\\\\\\') // Escape backslashes\n\t\t\t.replace(/\"/g, '\\\\\"') // Escape double quotes\n\t)\n}\n\n/**\n * Build command sequence for terminal\n */\nfunction buildCommandSequence(options: TerminalWindowOptions): string {\n\tconst {\n\t\tworkspacePath,\n\t\tcommand,\n\t\tport,\n\t\tincludeEnvSetup,\n\t\tincludePortExport,\n\t} = options\n\n\tconst commands: string[] = []\n\n\t// Navigate to workspace\n\tif (workspacePath) {\n\t\tcommands.push(`cd '${escapePathForAppleScript(workspacePath)}'`)\n\t}\n\n\t// Source .env file\n\tif (includeEnvSetup) {\n\t\tcommands.push('source .env')\n\t}\n\n\t// Export PORT variable\n\tif (includePortExport && port !== undefined) {\n\t\tcommands.push(`export PORT=${port}`)\n\t}\n\n\t// Add custom command\n\tif (command) {\n\t\tcommands.push(command)\n\t}\n\n\t// Join with &&\n\tconst fullCommand = commands.join(' && ')\n\n\t// Prefix with space to prevent shell history pollution\n\treturn ` ${fullCommand}`\n}\n\n/**\n * Build iTerm2 AppleScript for multiple tabs (2+) in single window\n */\nfunction buildITerm2MultiTabScript(\n\toptionsArray: TerminalWindowOptions[]\n): string {\n\tif (optionsArray.length < 2) {\n\t\tthrow new Error('buildITerm2MultiTabScript requires at least 2 terminal options')\n\t}\n\n\tlet script = 'tell application id \"com.googlecode.iterm2\"\\n'\n\tscript += ' create window with default profile\\n'\n\tscript += ' set newWindow to current window\\n'\n\n\t// First tab\n\tconst options1 = optionsArray[0]\n\tif (!options1) {\n\t\tthrow new Error('First terminal option is undefined')\n\t}\n\tconst command1 = buildCommandSequence(options1)\n\n\tscript += ' set s1 to current session of newWindow\\n\\n'\n\n\t// Set background color for first tab\n\tif (options1.backgroundColor) {\n\t\tconst { r, g, b } = options1.backgroundColor\n\t\tscript += ` set background color of s1 to {${Math.round(r * 257)}, ${Math.round(g * 257)}, ${Math.round(b * 257)}}\\n`\n\t}\n\n\t// Execute command in first tab\n\tscript += ` tell s1 to write text \"${escapeForAppleScript(command1)}\"\\n\\n`\n\n\t// Set tab title for first tab\n\tif (options1.title) {\n\t\tscript += ` set name of s1 to \"${escapeForAppleScript(options1.title)}\"\\n\\n`\n\t}\n\n\t// Subsequent tabs (2, 3, ...)\n\tfor (let i = 1; i < optionsArray.length; i++) {\n\t\tconst options = optionsArray[i]\n\t\tif (!options) {\n\t\t\tthrow new Error(`Terminal option at index ${i} is undefined`)\n\t\t}\n\t\tconst command = buildCommandSequence(options)\n\t\tconst sessionVar = `s${i + 1}`\n\n\t\t// Create tab\n\t\tscript += ' tell newWindow\\n'\n\t\tscript += ` set newTab${i} to (create tab with default profile)\\n`\n\t\tscript += ' end tell\\n'\n\t\tscript += ` set ${sessionVar} to current session of newTab${i}\\n\\n`\n\n\t\t// Set background color\n\t\tif (options.backgroundColor) {\n\t\t\tconst { r, g, b } = options.backgroundColor\n\t\t\tscript += ` set background color of ${sessionVar} to {${Math.round(r * 257)}, ${Math.round(g * 257)}, ${Math.round(b * 257)}}\\n`\n\t\t}\n\n\t\t// Execute command\n\t\tscript += ` tell ${sessionVar} to write text \"${escapeForAppleScript(command)}\"\\n\\n`\n\n\t\t// Set tab title\n\t\tif (options.title) {\n\t\t\tscript += ` set name of ${sessionVar} to \"${escapeForAppleScript(options.title)}\"\\n\\n`\n\t\t}\n\t}\n\n\t// Activate iTerm2\n\tscript += ' activate\\n'\n\tscript += 'end tell'\n\n\treturn script\n}\n\n/**\n * Open multiple terminal windows/tabs (2+) with specified options\n * If iTerm2 is available on macOS, creates single window with multiple tabs\n * Otherwise falls back to multiple separate Terminal.app windows\n */\nexport async function openMultipleTerminalWindows(\n\toptionsArray: TerminalWindowOptions[]\n): Promise<void> {\n\tif (optionsArray.length < 2) {\n\t\tthrow new Error('openMultipleTerminalWindows requires at least 2 terminal options. Use openTerminalWindow for single terminal.')\n\t}\n\n\tconst platform = detectPlatform()\n\n\tif (platform !== 'darwin') {\n\t\tthrow new Error(\n\t\t\t`Terminal window launching not yet supported on ${platform}. ` +\n\t\t\t\t`Currently only macOS is supported.`\n\t\t)\n\t}\n\n\t// Detect if iTerm2 is available\n\tconst hasITerm2 = await detectITerm2()\n\n\tif (hasITerm2) {\n\t\t// Use iTerm2 with multiple tabs in single window\n\t\tconst applescript = buildITerm2MultiTabScript(optionsArray)\n\n\t\ttry {\n\t\t\tawait execa('osascript', ['-e', applescript])\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to open iTerm2 window: ${error instanceof Error ? error.message : 'Unknown error'}`\n\t\t\t)\n\t\t}\n\t} else {\n\t\t// Fall back to multiple Terminal.app windows\n\t\tfor (let i = 0; i < optionsArray.length; i++) {\n\t\t\tconst options = optionsArray[i]\n\t\t\tif (!options) {\n\t\t\t\tthrow new Error(`Terminal option at index ${i} is undefined`)\n\t\t\t}\n\t\t\tawait openTerminalWindow(options)\n\n\t\t\t// Brief pause between terminals (except after last one)\n\t\t\tif (i < optionsArray.length - 1) {\n\t\t\t\t// eslint-disable-next-line no-undef\n\t\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 1000))\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Open dual terminal windows/tabs with specified options\n * If iTerm2 is available on macOS, creates single window with two tabs\n * Otherwise falls back to two separate Terminal.app windows\n */\nexport async function openDualTerminalWindow(\n\toptions1: TerminalWindowOptions,\n\toptions2: TerminalWindowOptions\n): Promise<void> {\n\t// Delegate to openMultipleTerminalWindows for consistency\n\tawait openMultipleTerminalWindows([options1, options2])\n}\n"],"mappings":"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","names":[]}
|