code-squad-cli 1.2.21 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/GitAdapter.js +18 -4
- package/dist/dash/InkDashboard.d.ts +13 -0
- package/dist/dash/InkDashboard.js +442 -0
- package/dist/dash/TmuxAdapter.d.ts +233 -0
- package/dist/dash/TmuxAdapter.js +520 -0
- package/dist/dash/index.d.ts +4 -0
- package/dist/dash/index.js +216 -0
- package/dist/dash/pathUtils.d.ts +27 -0
- package/dist/dash/pathUtils.js +70 -0
- package/dist/dash/threadHelpers.d.ts +9 -0
- package/dist/dash/threadHelpers.js +37 -0
- package/dist/dash/types.d.ts +42 -0
- package/dist/dash/types.js +1 -0
- package/dist/dash/useDirectorySuggestions.d.ts +23 -0
- package/dist/dash/useDirectorySuggestions.js +136 -0
- package/dist/dash/usePathValidation.d.ts +9 -0
- package/dist/dash/usePathValidation.js +34 -0
- package/dist/dash/windowHelpers.d.ts +10 -0
- package/dist/dash/windowHelpers.js +43 -0
- package/dist/index.js +1422 -85
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// dist/index.js
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
4
|
+
import * as path15 from "path";
|
|
5
|
+
import * as fs13 from "fs";
|
|
6
|
+
import * as os7 from "os";
|
|
7
7
|
import * as crypto from "crypto";
|
|
8
|
-
import
|
|
8
|
+
import chalk3 from "chalk";
|
|
9
9
|
|
|
10
10
|
// dist/adapters/GitAdapter.js
|
|
11
11
|
import { exec as execCallback } from "child_process";
|
|
@@ -23,8 +23,17 @@ var GitAdapter = class {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
async getCurrentBranch(workspaceRoot) {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
try {
|
|
27
|
+
const { stdout } = await exec(`cd "${workspaceRoot}" && git rev-parse --abbrev-ref HEAD`, execOptions);
|
|
28
|
+
return stdout.trim();
|
|
29
|
+
} catch {
|
|
30
|
+
try {
|
|
31
|
+
const { stdout } = await exec(`cd "${workspaceRoot}" && git symbolic-ref --short HEAD`, execOptions);
|
|
32
|
+
return stdout.trim();
|
|
33
|
+
} catch {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
28
37
|
}
|
|
29
38
|
async listWorktrees(workspaceRoot) {
|
|
30
39
|
try {
|
|
@@ -44,11 +53,11 @@ var GitAdapter = class {
|
|
|
44
53
|
const headMatch = headLine.match(/^HEAD (.+)$/);
|
|
45
54
|
const branchMatch = branchLine?.match(/^branch refs\/heads\/(.+)$/);
|
|
46
55
|
if (pathMatch && headMatch) {
|
|
47
|
-
const
|
|
56
|
+
const path16 = pathMatch[1];
|
|
48
57
|
const head = headMatch[1];
|
|
49
58
|
const branch = branchMatch ? branchMatch[1] : "HEAD";
|
|
50
|
-
if (
|
|
51
|
-
worktrees.push({ path:
|
|
59
|
+
if (path16 !== workspaceRoot) {
|
|
60
|
+
worktrees.push({ path: path16, branch, head });
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
63
|
i += 3;
|
|
@@ -59,9 +68,11 @@ var GitAdapter = class {
|
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
70
|
async createWorktree(worktreePath, branch, workspaceRoot) {
|
|
71
|
+
await exec(`cd "${workspaceRoot}" && git worktree prune`, execOptions).catch(() => {
|
|
72
|
+
});
|
|
62
73
|
const parentDir = worktreePath.substring(0, worktreePath.lastIndexOf("/"));
|
|
63
74
|
const mkdirCmd = parentDir ? `mkdir -p "${parentDir}" && ` : "";
|
|
64
|
-
await exec(`cd "${workspaceRoot}" && ${mkdirCmd}git worktree add "${worktreePath}" -b "${branch}"`, execOptions);
|
|
75
|
+
await exec(`cd "${workspaceRoot}" && ${mkdirCmd}git worktree add -f "${worktreePath}" -b "${branch}"`, execOptions);
|
|
65
76
|
}
|
|
66
77
|
async removeWorktree(worktreePath, workspaceRoot, force = false) {
|
|
67
78
|
const forceFlag = force ? " --force" : "";
|
|
@@ -79,19 +90,19 @@ var GitAdapter = class {
|
|
|
79
90
|
throw new Error(`Failed to delete branch: ${error.message}`);
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
|
-
async isValidWorktree(
|
|
93
|
+
async isValidWorktree(path16, workspaceRoot) {
|
|
83
94
|
try {
|
|
84
|
-
await fs.promises.access(
|
|
95
|
+
await fs.promises.access(path16, fs.constants.R_OK);
|
|
85
96
|
} catch {
|
|
86
97
|
return false;
|
|
87
98
|
}
|
|
88
99
|
try {
|
|
89
|
-
await exec(`cd "${
|
|
100
|
+
await exec(`cd "${path16}" && git rev-parse --git-dir`, execOptions);
|
|
90
101
|
} catch {
|
|
91
102
|
return false;
|
|
92
103
|
}
|
|
93
104
|
const worktrees = await this.listWorktrees(workspaceRoot);
|
|
94
|
-
return worktrees.some((wt) => wt.path ===
|
|
105
|
+
return worktrees.some((wt) => wt.path === path16);
|
|
95
106
|
}
|
|
96
107
|
async getWorktreeBranch(worktreePath) {
|
|
97
108
|
try {
|
|
@@ -411,7 +422,7 @@ async function confirmDeleteLocal(threadName) {
|
|
|
411
422
|
}
|
|
412
423
|
|
|
413
424
|
// dist/index.js
|
|
414
|
-
import { confirm as
|
|
425
|
+
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
415
426
|
|
|
416
427
|
// dist/flip/server/Server.js
|
|
417
428
|
import express2 from "express";
|
|
@@ -694,9 +705,9 @@ var filenameMap = {
|
|
|
694
705
|
"CODEOWNERS": "gitignore"
|
|
695
706
|
};
|
|
696
707
|
function detectLanguage(filePath) {
|
|
697
|
-
const
|
|
698
|
-
if (filenameMap[
|
|
699
|
-
return filenameMap[
|
|
708
|
+
const basename5 = path3.basename(filePath);
|
|
709
|
+
if (filenameMap[basename5]) {
|
|
710
|
+
return filenameMap[basename5];
|
|
700
711
|
}
|
|
701
712
|
const ext = path3.extname(filePath).slice(1).toLowerCase();
|
|
702
713
|
if (extensionMap[ext]) {
|
|
@@ -2005,6 +2016,537 @@ fi
|
|
|
2005
2016
|
console.log("");
|
|
2006
2017
|
}
|
|
2007
2018
|
|
|
2019
|
+
// dist/dash/index.js
|
|
2020
|
+
import chalk2 from "chalk";
|
|
2021
|
+
import * as path14 from "path";
|
|
2022
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
2023
|
+
|
|
2024
|
+
// dist/dash/TmuxAdapter.js
|
|
2025
|
+
import { exec as execCallback2 } from "child_process";
|
|
2026
|
+
import { promisify as promisify2 } from "util";
|
|
2027
|
+
var exec2 = promisify2(execCallback2);
|
|
2028
|
+
var execOptions2 = { maxBuffer: 1024 * 1024 };
|
|
2029
|
+
var TmuxAdapter = class {
|
|
2030
|
+
/**
|
|
2031
|
+
* tmux 설치 여부 확인
|
|
2032
|
+
*/
|
|
2033
|
+
async isTmuxAvailable() {
|
|
2034
|
+
try {
|
|
2035
|
+
await exec2("which tmux", execOptions2);
|
|
2036
|
+
return true;
|
|
2037
|
+
} catch {
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* 현재 tmux 세션 내부인지 확인
|
|
2043
|
+
*/
|
|
2044
|
+
isInsideTmux() {
|
|
2045
|
+
return !!process.env.TMUX;
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* 세션 존재 여부 확인
|
|
2049
|
+
*/
|
|
2050
|
+
async hasSession(name) {
|
|
2051
|
+
try {
|
|
2052
|
+
await exec2(`tmux has-session -t "${name}"`, execOptions2);
|
|
2053
|
+
return true;
|
|
2054
|
+
} catch {
|
|
2055
|
+
return false;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* 새 tmux 세션 생성
|
|
2060
|
+
*/
|
|
2061
|
+
async createSession(name, cwd) {
|
|
2062
|
+
await exec2(`tmux new-session -d -s "${name}" -c "${cwd}"`, execOptions2);
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* 세션 생성 또는 기존 세션 반환
|
|
2066
|
+
* @returns true if new session created, false if existing session
|
|
2067
|
+
*/
|
|
2068
|
+
async ensureSession(name, cwd) {
|
|
2069
|
+
if (await this.hasSession(name)) {
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
await this.createSession(name, cwd);
|
|
2073
|
+
return true;
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* 세션에 attach (spawn 사용)
|
|
2077
|
+
*/
|
|
2078
|
+
async attachSession(name) {
|
|
2079
|
+
const { spawn: spawn3 } = await import("child_process");
|
|
2080
|
+
return new Promise((resolve2, reject) => {
|
|
2081
|
+
const tmux = spawn3("tmux", ["attach-session", "-t", name], { stdio: "inherit" });
|
|
2082
|
+
tmux.on("close", (code) => {
|
|
2083
|
+
if (code === 0)
|
|
2084
|
+
resolve2();
|
|
2085
|
+
else
|
|
2086
|
+
reject(new Error(`tmux attach failed with code ${code}`));
|
|
2087
|
+
});
|
|
2088
|
+
tmux.on("error", reject);
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* 현재 pane 분할 (수직/수평)
|
|
2093
|
+
* @param direction 'v' = 수직 (좌우), 'h' = 수평 (상하)
|
|
2094
|
+
* @param cwd 새 pane의 작업 디렉토리
|
|
2095
|
+
* @param percent 새 pane의 크기 비율 (옵션)
|
|
2096
|
+
*/
|
|
2097
|
+
async splitWindow(direction, cwd, percent) {
|
|
2098
|
+
const flag = direction === "v" ? "-h" : "-v";
|
|
2099
|
+
const cwdFlag = cwd ? `-c "${cwd}"` : "";
|
|
2100
|
+
const percentFlag = percent ? `-p ${percent}` : "";
|
|
2101
|
+
await exec2(`tmux split-window ${flag} ${cwdFlag} ${percentFlag}`, execOptions2);
|
|
2102
|
+
const { stdout } = await exec2('tmux display-message -p "#{pane_id}"', execOptions2);
|
|
2103
|
+
return stdout.trim();
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* 특정 pane 선택 (포커스)
|
|
2107
|
+
*/
|
|
2108
|
+
async selectPane(paneId) {
|
|
2109
|
+
await exec2(`tmux select-pane -t "${paneId}"`, execOptions2);
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* pane 목록 조회
|
|
2113
|
+
*/
|
|
2114
|
+
async listPanes() {
|
|
2115
|
+
try {
|
|
2116
|
+
const { stdout } = await exec2('tmux list-panes -F "#{pane_id}|#{pane_index}|#{pane_active}|#{pane_current_path}"', execOptions2);
|
|
2117
|
+
return stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
2118
|
+
const [id, index, active, cwd] = line.split("|");
|
|
2119
|
+
return {
|
|
2120
|
+
id,
|
|
2121
|
+
index: parseInt(index, 10),
|
|
2122
|
+
active: active === "1",
|
|
2123
|
+
cwd
|
|
2124
|
+
};
|
|
2125
|
+
});
|
|
2126
|
+
} catch {
|
|
2127
|
+
return [];
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* pane 종료
|
|
2132
|
+
*/
|
|
2133
|
+
async killPane(paneId) {
|
|
2134
|
+
await exec2(`tmux kill-pane -t "${paneId}"`, execOptions2);
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* pane에 키 전송
|
|
2138
|
+
*/
|
|
2139
|
+
async sendKeys(paneId, keys) {
|
|
2140
|
+
const escaped = keys.replace(/"/g, '\\"');
|
|
2141
|
+
await exec2(`tmux send-keys -t "${paneId}" "${escaped}"`, execOptions2);
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* pane에 특수 키 전송 (C-l, C-c, Enter 등 tmux 키 이름)
|
|
2145
|
+
*/
|
|
2146
|
+
async sendSpecialKey(paneId, keyName) {
|
|
2147
|
+
await exec2(`tmux send-keys -t "${paneId}" ${keyName}`, execOptions2);
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* pane에 Enter 키 전송
|
|
2151
|
+
*/
|
|
2152
|
+
async sendEnter(paneId) {
|
|
2153
|
+
await exec2(`tmux send-keys -t "${paneId}" Enter`, execOptions2);
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* 현재 window의 레이아웃 설정
|
|
2157
|
+
* @param layout 'even-horizontal' | 'even-vertical' | 'main-horizontal' | 'main-vertical' | 'tiled'
|
|
2158
|
+
*/
|
|
2159
|
+
async setLayout(layout) {
|
|
2160
|
+
await exec2(`tmux select-layout ${layout}`, execOptions2);
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* pane 크기 조정
|
|
2164
|
+
*/
|
|
2165
|
+
async resizePane(paneId, direction, amount) {
|
|
2166
|
+
await exec2(`tmux resize-pane -t "${paneId}" -${direction} ${amount}`, execOptions2);
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* pane 너비를 절대값으로 설정
|
|
2170
|
+
*/
|
|
2171
|
+
async setPaneWidth(paneId, width) {
|
|
2172
|
+
await exec2(`tmux resize-pane -t "${paneId}" -x ${width}`, execOptions2);
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* pane을 별도 window로 분리 (백그라운드 실행 유지)
|
|
2176
|
+
* @returns 새로 생성된 window ID
|
|
2177
|
+
*/
|
|
2178
|
+
async breakPaneToWindow(paneId) {
|
|
2179
|
+
const { stdout } = await exec2(`tmux break-pane -d -s "${paneId}" -P -F "#{window_id}"`, execOptions2);
|
|
2180
|
+
return stdout.trim();
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* 특정 session:index에 window 생성
|
|
2184
|
+
* @returns 새로 생성된 window ID
|
|
2185
|
+
*/
|
|
2186
|
+
async createWindowAtIndex(sessionName, index, cwd) {
|
|
2187
|
+
const { stdout } = await exec2(`tmux new-window -d -t "${sessionName}:${index}" -c "${cwd}" -P -F "#{window_id}"`, execOptions2);
|
|
2188
|
+
return stdout.trim();
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* 숨겨진 window 생성 (단일 pane)
|
|
2192
|
+
*/
|
|
2193
|
+
async createHiddenWindow(cwd) {
|
|
2194
|
+
const cwdFlag = cwd ? `-c "${cwd}"` : "";
|
|
2195
|
+
const { stdout } = await exec2(`tmux new-window -d ${cwdFlag} -P -F "#{window_id}"`, execOptions2);
|
|
2196
|
+
return stdout.trim();
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* window의 pane을 특정 pane과 swap
|
|
2200
|
+
*/
|
|
2201
|
+
async swapPaneWithWindow(windowId, targetPaneId) {
|
|
2202
|
+
await exec2(`tmux swap-pane -d -s "${windowId}.0" -t "${targetPaneId}"`, execOptions2);
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* window 내 단일 pane 너비 설정
|
|
2206
|
+
*/
|
|
2207
|
+
async setWindowPaneWidth(windowId, width) {
|
|
2208
|
+
await exec2(`tmux resize-pane -t "${windowId}.0" -x ${width}`, execOptions2);
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* pane index로 pane ID 조회 (현재 window)
|
|
2212
|
+
*/
|
|
2213
|
+
async getPaneIdByIndex(index) {
|
|
2214
|
+
try {
|
|
2215
|
+
const { stdout } = await exec2('tmux list-panes -F "#{pane_index}|#{pane_id}"', execOptions2);
|
|
2216
|
+
const match = stdout.trim().split("\n").map((line) => line.split("|")).find(([idx]) => parseInt(idx, 10) === index);
|
|
2217
|
+
return match?.[1] ?? null;
|
|
2218
|
+
} catch {
|
|
2219
|
+
return null;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* 숨겨진 window에서 pane을 현재 window로 가져옴
|
|
2224
|
+
* @param windowId 숨겨진 window ID
|
|
2225
|
+
* @param targetPaneId 옆에 배치할 target pane
|
|
2226
|
+
* @returns 가져온 pane의 새 ID
|
|
2227
|
+
*/
|
|
2228
|
+
async joinPaneFromWindow(windowId, targetPaneId) {
|
|
2229
|
+
await exec2(`tmux join-pane -h -s "${windowId}.0" -t "${targetPaneId}"`, execOptions2);
|
|
2230
|
+
const { stdout } = await exec2('tmux display-message -p "#{pane_id}"', execOptions2);
|
|
2231
|
+
return stdout.trim();
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* pane 숨기기 (width=0) - deprecated, use breakPaneToWindow
|
|
2235
|
+
*/
|
|
2236
|
+
async hidePane(paneId) {
|
|
2237
|
+
await exec2(`tmux resize-pane -t "${paneId}" -x 0`, execOptions2);
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* pane 표시 (지정 너비로 복원) - deprecated, use joinPaneFromWindow
|
|
2241
|
+
*/
|
|
2242
|
+
async showPane(paneId, width) {
|
|
2243
|
+
await exec2(`tmux resize-pane -t "${paneId}" -x ${width}`, execOptions2);
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* 현재 window 너비 조회
|
|
2247
|
+
*/
|
|
2248
|
+
async getWindowWidth() {
|
|
2249
|
+
const { stdout } = await exec2('tmux display-message -p "#{window_width}"', execOptions2);
|
|
2250
|
+
return parseInt(stdout.trim(), 10);
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* pane 너비 조회
|
|
2254
|
+
*/
|
|
2255
|
+
async getPaneWidth(paneId) {
|
|
2256
|
+
const { stdout } = await exec2(`tmux display-message -t "${paneId}" -p "#{pane_width}"`, execOptions2);
|
|
2257
|
+
return parseInt(stdout.trim(), 10);
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* pane 높이 조회
|
|
2261
|
+
*/
|
|
2262
|
+
async getPaneHeight(paneId) {
|
|
2263
|
+
const { stdout } = await exec2(`tmux display-message -t "${paneId}" -p "#{pane_height}"`, execOptions2);
|
|
2264
|
+
return parseInt(stdout.trim(), 10);
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* 현재 세션 이름 조회
|
|
2268
|
+
*/
|
|
2269
|
+
async getSessionName() {
|
|
2270
|
+
try {
|
|
2271
|
+
const { stdout } = await exec2('tmux display-message -p "#{session_name}"', execOptions2);
|
|
2272
|
+
return stdout.trim();
|
|
2273
|
+
} catch {
|
|
2274
|
+
return null;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* 현재 pane ID 조회
|
|
2279
|
+
*/
|
|
2280
|
+
async getCurrentPaneId() {
|
|
2281
|
+
try {
|
|
2282
|
+
const { stdout } = await exec2('tmux display-message -p "#{pane_id}"', execOptions2);
|
|
2283
|
+
return stdout.trim();
|
|
2284
|
+
} catch {
|
|
2285
|
+
return null;
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* UX 개선 설정 적용
|
|
2290
|
+
* - 마우스 모드 활성화 (클릭 선택, 드래그 리사이징)
|
|
2291
|
+
* - 활성 pane 테두리 강조
|
|
2292
|
+
* - pane swap 단축키 (Ctrl+b m)
|
|
2293
|
+
*/
|
|
2294
|
+
async applyUXSettings() {
|
|
2295
|
+
const settings = [
|
|
2296
|
+
"set -g mouse on",
|
|
2297
|
+
// extended-keys 활성화 (Shift+Tab 등 modifier key 조합 인식에 필요)
|
|
2298
|
+
"set -g extended-keys on",
|
|
2299
|
+
"set -g pane-active-border-style 'fg=cyan,bold'",
|
|
2300
|
+
"set -g pane-border-style 'fg=#444444'",
|
|
2301
|
+
"set -g pane-border-lines single",
|
|
2302
|
+
"set -g pane-border-status top",
|
|
2303
|
+
"set -g pane-border-format ' #{pane_current_path} '",
|
|
2304
|
+
// pane 번호 표시 시간 늘리기
|
|
2305
|
+
"set -g display-panes-time 5000",
|
|
2306
|
+
// pane 번호 색상
|
|
2307
|
+
"set -g display-panes-colour '#444444'",
|
|
2308
|
+
"set -g display-panes-active-colour cyan"
|
|
2309
|
+
];
|
|
2310
|
+
for (const setting of settings) {
|
|
2311
|
+
try {
|
|
2312
|
+
await exec2(`tmux ${setting}`, execOptions2);
|
|
2313
|
+
} catch {
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
try {
|
|
2317
|
+
await exec2(`tmux bind-key m if-shell -F "#{!=:#{pane_index},0}" "display-menu -T 'Move Pane' -x P -y P 'Move Left' l 'if-shell -F \\"#{>:#{pane_index},1}\\" \\"swap-pane -U\\"' 'Move Right' r 'swap-pane -D' '' 'Swap #1' 1 'swap-pane -t 1' 'Swap #2' 2 'swap-pane -t 2' 'Swap #3' 3 'swap-pane -t 3' 'Swap #4' 4 'swap-pane -t 4' '' 'Cancel' q ''"`, execOptions2);
|
|
2318
|
+
} catch {
|
|
2319
|
+
}
|
|
2320
|
+
try {
|
|
2321
|
+
await exec2(`tmux bind-key -n BTab if-shell -F "#{==:#{pane_index},0}" "select-pane -t 1" "select-pane -t 0"`, execOptions2);
|
|
2322
|
+
} catch {
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* 대시보드 pane 리사이즈 훅 설정
|
|
2327
|
+
*/
|
|
2328
|
+
async setDashboardResizeHook(paneId, width) {
|
|
2329
|
+
try {
|
|
2330
|
+
await exec2(`tmux set -g @csq_dash_pane "${paneId}"`, execOptions2);
|
|
2331
|
+
await exec2(`tmux set -g @csq_dash_width "${width}"`, execOptions2);
|
|
2332
|
+
await exec2(`tmux set-hook after-resize-pane 'resize-pane -t "#{@csq_dash_pane}" -x #{@csq_dash_width}'`, execOptions2);
|
|
2333
|
+
} catch {
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* main-vertical 레이아웃 적용 (왼쪽 고정, 오른쪽 분할)
|
|
2338
|
+
* @param mainWidth 왼쪽 메인 pane 너비 (columns)
|
|
2339
|
+
*/
|
|
2340
|
+
async applyMainVerticalLayout(mainWidth = 30) {
|
|
2341
|
+
try {
|
|
2342
|
+
await exec2(`tmux set-window-option main-pane-width ${mainWidth}`, execOptions2);
|
|
2343
|
+
await exec2("tmux select-layout main-vertical", execOptions2);
|
|
2344
|
+
} catch {
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* 특정 pane 기준으로 분할
|
|
2349
|
+
* @param targetPaneId 분할할 대상 pane
|
|
2350
|
+
* @param direction 'v' = 수직 (좌우), 'h' = 수평 (상하)
|
|
2351
|
+
* @param cwd 새 pane의 작업 디렉토리
|
|
2352
|
+
*/
|
|
2353
|
+
async splitPane(targetPaneId, direction, cwd) {
|
|
2354
|
+
const flag = direction === "v" ? "-h" : "-v";
|
|
2355
|
+
const cwdFlag = cwd ? `-c "${cwd}"` : "";
|
|
2356
|
+
await exec2(`tmux split-window ${flag} -t "${targetPaneId}" ${cwdFlag}`, execOptions2);
|
|
2357
|
+
const { stdout } = await exec2('tmux display-message -p "#{pane_id}"', execOptions2);
|
|
2358
|
+
return stdout.trim();
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* 현재 세션 종료
|
|
2362
|
+
*/
|
|
2363
|
+
async killCurrentSession() {
|
|
2364
|
+
await exec2("tmux kill-session", execOptions2);
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* 현재 클라이언트 detach (세션은 유지)
|
|
2368
|
+
*/
|
|
2369
|
+
async detachClient() {
|
|
2370
|
+
await exec2("tmux detach-client", execOptions2);
|
|
2371
|
+
}
|
|
2372
|
+
/**
|
|
2373
|
+
* Detach client and kill a window in one atomic tmux command.
|
|
2374
|
+
* Prevents flash of another window between kill and detach.
|
|
2375
|
+
*/
|
|
2376
|
+
async detachAndKillWindow(sessionName, windowIndex) {
|
|
2377
|
+
await exec2(`tmux detach-client \\; kill-window -t "${sessionName}:${windowIndex}"`, execOptions2);
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* 특정 window 종료
|
|
2381
|
+
*/
|
|
2382
|
+
async killWindow(windowId) {
|
|
2383
|
+
await exec2(`tmux kill-window -t "${windowId}"`, execOptions2);
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* pane 화면 클리어 (scrollback 포함)
|
|
2387
|
+
*/
|
|
2388
|
+
async clearPane(paneId) {
|
|
2389
|
+
await exec2(`tmux send-keys -t "${paneId}" -R`, execOptions2);
|
|
2390
|
+
await exec2(`tmux clear-history -t "${paneId}"`, execOptions2);
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* 클라이언트 강제 리프레시
|
|
2394
|
+
*/
|
|
2395
|
+
async refreshClient() {
|
|
2396
|
+
await exec2("tmux refresh-client -S", execOptions2);
|
|
2397
|
+
}
|
|
2398
|
+
/**
|
|
2399
|
+
* pane 위치 swap
|
|
2400
|
+
* @param paneId 이동할 pane
|
|
2401
|
+
* @param direction 'left' | 'right' | 'up' | 'down'
|
|
2402
|
+
*/
|
|
2403
|
+
async swapPane(paneId, direction) {
|
|
2404
|
+
const dirMap = {
|
|
2405
|
+
left: "-U",
|
|
2406
|
+
// swap with pane above/left
|
|
2407
|
+
right: "-D",
|
|
2408
|
+
// swap with pane below/right
|
|
2409
|
+
up: "-U",
|
|
2410
|
+
down: "-D"
|
|
2411
|
+
};
|
|
2412
|
+
await exec2(`tmux swap-pane -t "${paneId}" ${dirMap[direction]}`, execOptions2);
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* 대시보드 제외하고 오른쪽 pane들 균등 분할
|
|
2416
|
+
* @param dashPaneId 대시보드 pane ID
|
|
2417
|
+
* @param dashWidth 대시보드 너비 (columns)
|
|
2418
|
+
*/
|
|
2419
|
+
async distributeRightPanes(dashPaneId, dashWidth = 35) {
|
|
2420
|
+
try {
|
|
2421
|
+
const { stdout: widthStr } = await exec2('tmux display-message -p "#{window_width}"', execOptions2);
|
|
2422
|
+
const totalWidth = parseInt(widthStr.trim(), 10);
|
|
2423
|
+
const panes = await this.listPanes();
|
|
2424
|
+
const rightPanes = panes.filter((p) => p.id !== dashPaneId);
|
|
2425
|
+
if (rightPanes.length === 0)
|
|
2426
|
+
return;
|
|
2427
|
+
const rightAreaWidth = totalWidth - dashWidth - 1;
|
|
2428
|
+
const paneWidth = Math.floor(rightAreaWidth / rightPanes.length);
|
|
2429
|
+
for (const pane of rightPanes) {
|
|
2430
|
+
await exec2(`tmux resize-pane -t "${pane.id}" -x ${paneWidth}`, execOptions2);
|
|
2431
|
+
}
|
|
2432
|
+
await exec2(`tmux resize-pane -t "${dashPaneId}" -x ${dashWidth}`, execOptions2);
|
|
2433
|
+
} catch {
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
// ============================================================
|
|
2437
|
+
// Window 관련 메서드 (Task 1, 8)
|
|
2438
|
+
// ============================================================
|
|
2439
|
+
/**
|
|
2440
|
+
* 현재 세션의 모든 window 목록 조회
|
|
2441
|
+
*/
|
|
2442
|
+
async listWindows() {
|
|
2443
|
+
try {
|
|
2444
|
+
const { stdout } = await exec2('tmux list-windows -F "#{window_id}|#{window_index}|#{window_name}|#{pane_current_path}|#{window_active}"', execOptions2);
|
|
2445
|
+
return stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
2446
|
+
const [id, index, name, cwd, active] = line.split("|");
|
|
2447
|
+
return {
|
|
2448
|
+
id,
|
|
2449
|
+
index: parseInt(index, 10),
|
|
2450
|
+
name,
|
|
2451
|
+
cwd,
|
|
2452
|
+
active: active === "1"
|
|
2453
|
+
};
|
|
2454
|
+
});
|
|
2455
|
+
} catch {
|
|
2456
|
+
return [];
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* 특정 window로 포커스 전환
|
|
2461
|
+
*/
|
|
2462
|
+
async selectWindow(windowId) {
|
|
2463
|
+
await exec2(`tmux select-window -t "${windowId}"`, execOptions2);
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* 새 window 생성
|
|
2467
|
+
* @param cwd 작업 디렉토리 (옵션)
|
|
2468
|
+
* @param name window 이름 (옵션)
|
|
2469
|
+
* @returns 생성된 window ID
|
|
2470
|
+
*/
|
|
2471
|
+
async createNewWindow(cwd, name) {
|
|
2472
|
+
const cwdFlag = cwd ? `-c "${cwd}"` : "";
|
|
2473
|
+
const nameFlag = name ? `-n "${name}"` : "";
|
|
2474
|
+
const { stdout } = await exec2(`tmux new-window -d ${cwdFlag} ${nameFlag} -P -F "#{window_id}"`, execOptions2);
|
|
2475
|
+
return stdout.trim();
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* 현재 window ID 조회
|
|
2479
|
+
*/
|
|
2480
|
+
async getCurrentWindowId() {
|
|
2481
|
+
try {
|
|
2482
|
+
const { stdout } = await exec2('tmux display-message -p "#{window_id}"', execOptions2);
|
|
2483
|
+
return stdout.trim();
|
|
2484
|
+
} catch {
|
|
2485
|
+
return null;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* 현재 window index 조회
|
|
2490
|
+
*/
|
|
2491
|
+
async getCurrentWindowIndex() {
|
|
2492
|
+
try {
|
|
2493
|
+
const { stdout } = await exec2('tmux display-message -p "#{window_index}"', execOptions2);
|
|
2494
|
+
return parseInt(stdout.trim(), 10);
|
|
2495
|
+
} catch {
|
|
2496
|
+
return null;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
/**
|
|
2500
|
+
* window 이름 변경
|
|
2501
|
+
*/
|
|
2502
|
+
async renameWindow(windowId, name) {
|
|
2503
|
+
await exec2(`tmux rename-window -t "${windowId}" "${name}"`, execOptions2);
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
|
|
2507
|
+
// dist/dash/InkDashboard.js
|
|
2508
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2509
|
+
import { useState as useState4, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef3 } from "react";
|
|
2510
|
+
import { render, Box, Text, useInput, useStdin } from "ink";
|
|
2511
|
+
import TextInput from "ink-text-input";
|
|
2512
|
+
|
|
2513
|
+
// dist/dash/windowHelpers.js
|
|
2514
|
+
var gitAdapter = new GitAdapter();
|
|
2515
|
+
async function loadAllWindows(tmuxAdapter2, dashWindowIndex) {
|
|
2516
|
+
const rawWindows = await tmuxAdapter2.listWindows();
|
|
2517
|
+
const filteredWindows = rawWindows.filter((w) => w.index !== dashWindowIndex);
|
|
2518
|
+
const windowsWithGitInfo = await Promise.all(filteredWindows.map(async (w) => {
|
|
2519
|
+
const isGitRepo = await gitAdapter.isGitRepository(w.cwd);
|
|
2520
|
+
let worktreeBranch;
|
|
2521
|
+
let projectRoot;
|
|
2522
|
+
if (isGitRepo) {
|
|
2523
|
+
try {
|
|
2524
|
+
const context = await gitAdapter.getWorktreeContext(w.cwd);
|
|
2525
|
+
worktreeBranch = context.branch ?? void 0;
|
|
2526
|
+
projectRoot = context.mainRoot ?? void 0;
|
|
2527
|
+
} catch {
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
return {
|
|
2531
|
+
windowId: w.id,
|
|
2532
|
+
windowIndex: w.index,
|
|
2533
|
+
name: w.name,
|
|
2534
|
+
cwd: w.cwd,
|
|
2535
|
+
isActive: w.active,
|
|
2536
|
+
isGitRepo,
|
|
2537
|
+
worktreeBranch,
|
|
2538
|
+
projectRoot
|
|
2539
|
+
};
|
|
2540
|
+
}));
|
|
2541
|
+
return windowsWithGitInfo;
|
|
2542
|
+
}
|
|
2543
|
+
async function deleteWindowById(tmuxAdapter2, windowId) {
|
|
2544
|
+
await tmuxAdapter2.killWindow(windowId);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// dist/dash/threadHelpers.js
|
|
2548
|
+
import * as path12 from "path";
|
|
2549
|
+
|
|
2008
2550
|
// dist/config.js
|
|
2009
2551
|
import * as fs9 from "fs";
|
|
2010
2552
|
import * as os4 from "os";
|
|
@@ -2084,11 +2626,761 @@ async function copySingleFile(absolutePath, sourceRoot, destRoot) {
|
|
|
2084
2626
|
await fs10.promises.copyFile(absolutePath, destPath);
|
|
2085
2627
|
}
|
|
2086
2628
|
|
|
2629
|
+
// dist/dash/threadHelpers.js
|
|
2630
|
+
var gitAdapter2 = new GitAdapter();
|
|
2631
|
+
async function createThread(workspaceRoot, name, rootOverride) {
|
|
2632
|
+
const baseRoot = rootOverride || workspaceRoot;
|
|
2633
|
+
const repoName = path12.basename(baseRoot);
|
|
2634
|
+
const defaultBasePath = path12.join(path12.dirname(baseRoot), `${repoName}.worktree`);
|
|
2635
|
+
const worktreePath = path12.join(defaultBasePath, name);
|
|
2636
|
+
await gitAdapter2.createWorktree(worktreePath, name, baseRoot);
|
|
2637
|
+
const configData = await loadConfig(baseRoot);
|
|
2638
|
+
const patterns = getWorktreeCopyPatterns(configData);
|
|
2639
|
+
if (patterns.length > 0) {
|
|
2640
|
+
await copyFilesWithPatterns(baseRoot, worktreePath, patterns);
|
|
2641
|
+
}
|
|
2642
|
+
return {
|
|
2643
|
+
id: worktreePath,
|
|
2644
|
+
name,
|
|
2645
|
+
path: worktreePath,
|
|
2646
|
+
branch: name
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
async function deleteThread(workspaceRoot, thread) {
|
|
2650
|
+
await gitAdapter2.removeWorktree(thread.path, workspaceRoot, true);
|
|
2651
|
+
if (thread.branch) {
|
|
2652
|
+
await gitAdapter2.deleteBranch(thread.branch, workspaceRoot, true);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
// dist/dash/pathUtils.js
|
|
2657
|
+
import * as os5 from "os";
|
|
2658
|
+
import * as path13 from "path";
|
|
2659
|
+
import * as fs11 from "fs";
|
|
2660
|
+
var gitAdapter3 = new GitAdapter();
|
|
2661
|
+
function expandTilde(p) {
|
|
2662
|
+
if (p === "~")
|
|
2663
|
+
return os5.homedir();
|
|
2664
|
+
if (p.startsWith("~/"))
|
|
2665
|
+
return path13.join(os5.homedir(), p.slice(2));
|
|
2666
|
+
return p;
|
|
2667
|
+
}
|
|
2668
|
+
function splitPathForCompletion(p) {
|
|
2669
|
+
const expanded = expandTilde(p);
|
|
2670
|
+
if (expanded.endsWith("/")) {
|
|
2671
|
+
return { parentDir: expanded, leafPrefix: "" };
|
|
2672
|
+
}
|
|
2673
|
+
return {
|
|
2674
|
+
parentDir: path13.dirname(expanded),
|
|
2675
|
+
leafPrefix: path13.basename(expanded)
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
async function validatePath(p) {
|
|
2679
|
+
const expanded = expandTilde(p);
|
|
2680
|
+
if (!expanded || !path13.isAbsolute(expanded)) {
|
|
2681
|
+
return { status: "invalid", isGitRepo: false };
|
|
2682
|
+
}
|
|
2683
|
+
try {
|
|
2684
|
+
const stat = await fs11.promises.stat(expanded);
|
|
2685
|
+
if (stat.isDirectory()) {
|
|
2686
|
+
const isGitRepo = await gitAdapter3.isGitRepository(expanded);
|
|
2687
|
+
return { status: "valid", isGitRepo };
|
|
2688
|
+
}
|
|
2689
|
+
return { status: "invalid", isGitRepo: false };
|
|
2690
|
+
} catch {
|
|
2691
|
+
const parent = path13.dirname(expanded);
|
|
2692
|
+
try {
|
|
2693
|
+
const parentStat = await fs11.promises.stat(parent);
|
|
2694
|
+
if (parentStat.isDirectory()) {
|
|
2695
|
+
return { status: "creatable", isGitRepo: false };
|
|
2696
|
+
}
|
|
2697
|
+
} catch {
|
|
2698
|
+
}
|
|
2699
|
+
return { status: "invalid", isGitRepo: false };
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// dist/dash/useDirectorySuggestions.js
|
|
2704
|
+
import { useState as useState2, useRef, useCallback, useMemo } from "react";
|
|
2705
|
+
import * as fs12 from "fs";
|
|
2706
|
+
import * as os6 from "os";
|
|
2707
|
+
var MAX_VISIBLE = 5;
|
|
2708
|
+
function useDirectorySuggestions() {
|
|
2709
|
+
const [suggestions, setSuggestions] = useState2([]);
|
|
2710
|
+
const [selectedIndex, setSelectedIndex] = useState2(0);
|
|
2711
|
+
const [isOpen, setIsOpen] = useState2(false);
|
|
2712
|
+
const requestIdRef = useRef(0);
|
|
2713
|
+
const clearSuggestions = useCallback(() => {
|
|
2714
|
+
setSuggestions([]);
|
|
2715
|
+
setSelectedIndex(0);
|
|
2716
|
+
setIsOpen(false);
|
|
2717
|
+
}, []);
|
|
2718
|
+
const triggerComplete = useCallback(async (inputPath) => {
|
|
2719
|
+
const reqId = ++requestIdRef.current;
|
|
2720
|
+
const { parentDir, leafPrefix } = splitPathForCompletion(inputPath);
|
|
2721
|
+
let entries;
|
|
2722
|
+
try {
|
|
2723
|
+
const dirEntries = await fs12.promises.readdir(parentDir, { withFileTypes: true });
|
|
2724
|
+
entries = dirEntries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).filter((e) => e.name.toLowerCase().startsWith(leafPrefix.toLowerCase())).map((e) => e.name).sort();
|
|
2725
|
+
} catch {
|
|
2726
|
+
return null;
|
|
2727
|
+
}
|
|
2728
|
+
if (reqId !== requestIdRef.current)
|
|
2729
|
+
return null;
|
|
2730
|
+
if (entries.length === 0) {
|
|
2731
|
+
clearSuggestions();
|
|
2732
|
+
return null;
|
|
2733
|
+
}
|
|
2734
|
+
if (entries.length === 1) {
|
|
2735
|
+
clearSuggestions();
|
|
2736
|
+
const completed = parentDir.endsWith("/") ? parentDir + entries[0] + "/" : parentDir + "/" + entries[0] + "/";
|
|
2737
|
+
return collapseHome(completed);
|
|
2738
|
+
}
|
|
2739
|
+
const commonPrefix = findCommonPrefix(entries);
|
|
2740
|
+
setSuggestions(entries);
|
|
2741
|
+
setSelectedIndex(0);
|
|
2742
|
+
setIsOpen(true);
|
|
2743
|
+
if (commonPrefix.length > leafPrefix.length) {
|
|
2744
|
+
const completed = parentDir.endsWith("/") ? parentDir + commonPrefix : parentDir + "/" + commonPrefix;
|
|
2745
|
+
return collapseHome(completed);
|
|
2746
|
+
}
|
|
2747
|
+
return null;
|
|
2748
|
+
}, [clearSuggestions]);
|
|
2749
|
+
const selectNext = useCallback(() => {
|
|
2750
|
+
setSelectedIndex((prev) => (prev + 1) % Math.max(suggestions.length, 1));
|
|
2751
|
+
}, [suggestions.length]);
|
|
2752
|
+
const selectPrev = useCallback(() => {
|
|
2753
|
+
setSelectedIndex((prev) => (prev - 1 + Math.max(suggestions.length, 1)) % Math.max(suggestions.length, 1));
|
|
2754
|
+
}, [suggestions.length]);
|
|
2755
|
+
const acceptSelected = useCallback((inputPath) => {
|
|
2756
|
+
if (!isOpen || suggestions.length === 0)
|
|
2757
|
+
return null;
|
|
2758
|
+
const selected = suggestions[selectedIndex];
|
|
2759
|
+
if (!selected)
|
|
2760
|
+
return null;
|
|
2761
|
+
const { parentDir } = splitPathForCompletion(inputPath);
|
|
2762
|
+
const completed = parentDir.endsWith("/") ? parentDir + selected + "/" : parentDir + "/" + selected + "/";
|
|
2763
|
+
clearSuggestions();
|
|
2764
|
+
return collapseHome(completed);
|
|
2765
|
+
}, [isOpen, suggestions, selectedIndex, clearSuggestions]);
|
|
2766
|
+
const visibleSuggestions = useMemo(() => {
|
|
2767
|
+
if (suggestions.length === 0)
|
|
2768
|
+
return [];
|
|
2769
|
+
let start;
|
|
2770
|
+
if (suggestions.length <= MAX_VISIBLE) {
|
|
2771
|
+
start = 0;
|
|
2772
|
+
} else {
|
|
2773
|
+
start = Math.min(Math.max(0, selectedIndex - Math.floor(MAX_VISIBLE / 2)), suggestions.length - MAX_VISIBLE);
|
|
2774
|
+
}
|
|
2775
|
+
const end = Math.min(start + MAX_VISIBLE, suggestions.length);
|
|
2776
|
+
return suggestions.slice(start, end).map((name) => ({
|
|
2777
|
+
name,
|
|
2778
|
+
isSelected: name === suggestions[selectedIndex]
|
|
2779
|
+
}));
|
|
2780
|
+
}, [suggestions, selectedIndex]);
|
|
2781
|
+
const hasMore = suggestions.length > MAX_VISIBLE;
|
|
2782
|
+
return {
|
|
2783
|
+
visibleSuggestions,
|
|
2784
|
+
hasMore,
|
|
2785
|
+
isOpen,
|
|
2786
|
+
triggerComplete,
|
|
2787
|
+
selectNext,
|
|
2788
|
+
selectPrev,
|
|
2789
|
+
acceptSelected,
|
|
2790
|
+
clearSuggestions
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
function findCommonPrefix(strs) {
|
|
2794
|
+
if (strs.length === 0)
|
|
2795
|
+
return "";
|
|
2796
|
+
let prefix = strs[0];
|
|
2797
|
+
for (let i = 1; i < strs.length; i++) {
|
|
2798
|
+
while (!strs[i].toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
2799
|
+
prefix = prefix.slice(0, -1);
|
|
2800
|
+
if (prefix === "")
|
|
2801
|
+
return "";
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
return prefix;
|
|
2805
|
+
}
|
|
2806
|
+
function collapseHome(p) {
|
|
2807
|
+
const home = os6.homedir();
|
|
2808
|
+
if (p === home)
|
|
2809
|
+
return "~";
|
|
2810
|
+
if (p.startsWith(home + "/"))
|
|
2811
|
+
return "~" + p.slice(home.length);
|
|
2812
|
+
return p;
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// dist/dash/usePathValidation.js
|
|
2816
|
+
import { useState as useState3, useEffect, useRef as useRef2 } from "react";
|
|
2817
|
+
function usePathValidation(inputPath) {
|
|
2818
|
+
const [validation, setValidation] = useState3(null);
|
|
2819
|
+
const [isGitRepo, setIsGitRepo] = useState3(false);
|
|
2820
|
+
const timerRef = useRef2(null);
|
|
2821
|
+
const requestIdRef = useRef2(0);
|
|
2822
|
+
useEffect(() => {
|
|
2823
|
+
if (timerRef.current)
|
|
2824
|
+
clearTimeout(timerRef.current);
|
|
2825
|
+
const expanded = expandTilde(inputPath);
|
|
2826
|
+
if (!expanded || expanded.length < 2) {
|
|
2827
|
+
setValidation(null);
|
|
2828
|
+
setIsGitRepo(false);
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
timerRef.current = setTimeout(async () => {
|
|
2832
|
+
const reqId = ++requestIdRef.current;
|
|
2833
|
+
const result = await validatePath(inputPath);
|
|
2834
|
+
if (reqId !== requestIdRef.current)
|
|
2835
|
+
return;
|
|
2836
|
+
setValidation(result);
|
|
2837
|
+
setIsGitRepo(result.isGitRepo);
|
|
2838
|
+
}, 300);
|
|
2839
|
+
return () => {
|
|
2840
|
+
if (timerRef.current)
|
|
2841
|
+
clearTimeout(timerRef.current);
|
|
2842
|
+
};
|
|
2843
|
+
}, [inputPath]);
|
|
2844
|
+
return { validation, isGitRepo };
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
// dist/dash/InkDashboard.js
|
|
2848
|
+
function parseMouseEvent(data) {
|
|
2849
|
+
const match = data.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
|
2850
|
+
if (!match)
|
|
2851
|
+
return null;
|
|
2852
|
+
return {
|
|
2853
|
+
button: parseInt(match[1], 10),
|
|
2854
|
+
col: parseInt(match[2], 10),
|
|
2855
|
+
row: parseInt(match[3], 10),
|
|
2856
|
+
release: match[4] === "m"
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
function useMouse(onMouseClick, enabled = true) {
|
|
2860
|
+
const { stdin } = useStdin();
|
|
2861
|
+
useEffect2(() => {
|
|
2862
|
+
if (!enabled) {
|
|
2863
|
+
process.stdout.write("\x1B[?1006l");
|
|
2864
|
+
process.stdout.write("\x1B[?1000l");
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
process.stdout.write("\x1B[?1000h");
|
|
2868
|
+
process.stdout.write("\x1B[?1006h");
|
|
2869
|
+
const handleData = (data) => {
|
|
2870
|
+
const str = data.toString();
|
|
2871
|
+
if (str.includes("\x1B[<")) {
|
|
2872
|
+
const mouseEvent = parseMouseEvent(str);
|
|
2873
|
+
if (mouseEvent && mouseEvent.button === 0 && !mouseEvent.release) {
|
|
2874
|
+
onMouseClick(mouseEvent.row, mouseEvent.col);
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
};
|
|
2878
|
+
stdin?.on("data", handleData);
|
|
2879
|
+
return () => {
|
|
2880
|
+
process.stdout.write("\x1B[?1006l");
|
|
2881
|
+
process.stdout.write("\x1B[?1000l");
|
|
2882
|
+
stdin?.off("data", handleData);
|
|
2883
|
+
};
|
|
2884
|
+
}, [stdin, onMouseClick, enabled]);
|
|
2885
|
+
}
|
|
2886
|
+
function WindowCard({ window, isSelected }) {
|
|
2887
|
+
const borderColor = isSelected ? "cyan" : "gray";
|
|
2888
|
+
const nameColor = isSelected ? "cyan" : "white";
|
|
2889
|
+
const statusIcon = window.isActive ? "\u25CF" : "\u25CB";
|
|
2890
|
+
const statusColor = window.isActive ? "green" : "gray";
|
|
2891
|
+
const projectName = window.projectRoot ? window.projectRoot.split("/").slice(-1)[0] : window.cwd.split("/").slice(-1)[0];
|
|
2892
|
+
const threadName = window.worktreeBranch ?? window.name;
|
|
2893
|
+
return _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor, paddingX: 1, marginBottom: 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: statusColor, children: [statusIcon, " "] }), _jsx(Text, { color: nameColor, bold: isSelected, children: window.name }), isSelected && _jsx(Text, { color: "red", children: " \u2715" })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: projectName }), _jsx(Text, { color: "gray", children: " \u2192 " }), _jsx(Text, { color: "cyan", children: threadName })] })] });
|
|
2894
|
+
}
|
|
2895
|
+
function NewWindowForm({ windowName, onWindowNameChange, rootPath, onRootPathChange, validation, onSubmit, onCancel }) {
|
|
2896
|
+
const [focusedField, setFocusedField] = useState4("name");
|
|
2897
|
+
const dirSuggestions = useDirectorySuggestions();
|
|
2898
|
+
useInput(async (input, key) => {
|
|
2899
|
+
if (key.escape) {
|
|
2900
|
+
if (dirSuggestions.isOpen) {
|
|
2901
|
+
dirSuggestions.clearSuggestions();
|
|
2902
|
+
} else {
|
|
2903
|
+
onCancel();
|
|
2904
|
+
}
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
if (key.return) {
|
|
2908
|
+
if (dirSuggestions.isOpen) {
|
|
2909
|
+
const accepted = dirSuggestions.acceptSelected(rootPath);
|
|
2910
|
+
if (accepted)
|
|
2911
|
+
onRootPathChange(accepted);
|
|
2912
|
+
} else {
|
|
2913
|
+
onSubmit();
|
|
2914
|
+
}
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
if (key.tab) {
|
|
2918
|
+
if (dirSuggestions.isOpen) {
|
|
2919
|
+
dirSuggestions.clearSuggestions();
|
|
2920
|
+
}
|
|
2921
|
+
setFocusedField((prev) => prev === "name" ? "root" : "name");
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
if (key.upArrow) {
|
|
2925
|
+
if (dirSuggestions.isOpen) {
|
|
2926
|
+
dirSuggestions.selectPrev();
|
|
2927
|
+
} else {
|
|
2928
|
+
setFocusedField("name");
|
|
2929
|
+
}
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
if (key.downArrow) {
|
|
2933
|
+
if (dirSuggestions.isOpen) {
|
|
2934
|
+
dirSuggestions.selectNext();
|
|
2935
|
+
} else {
|
|
2936
|
+
setFocusedField("root");
|
|
2937
|
+
}
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
});
|
|
2941
|
+
const handleRootPathChange = useCallback2(async (value) => {
|
|
2942
|
+
onRootPathChange(value);
|
|
2943
|
+
if (value.endsWith("/")) {
|
|
2944
|
+
const completed = await dirSuggestions.triggerComplete(value);
|
|
2945
|
+
if (completed)
|
|
2946
|
+
onRootPathChange(completed);
|
|
2947
|
+
} else if (value.length > 1 && value.includes("/")) {
|
|
2948
|
+
await dirSuggestions.triggerComplete(value);
|
|
2949
|
+
} else {
|
|
2950
|
+
dirSuggestions.clearSuggestions();
|
|
2951
|
+
}
|
|
2952
|
+
}, [dirSuggestions, onRootPathChange]);
|
|
2953
|
+
let validationIcon = "";
|
|
2954
|
+
let validationColor;
|
|
2955
|
+
let validationText = "";
|
|
2956
|
+
if (validation) {
|
|
2957
|
+
if (validation.status === "valid") {
|
|
2958
|
+
if (validation.isGitRepo) {
|
|
2959
|
+
validationIcon = "\u2713";
|
|
2960
|
+
validationColor = "green";
|
|
2961
|
+
validationText = "git repo";
|
|
2962
|
+
} else {
|
|
2963
|
+
validationIcon = "\u2717";
|
|
2964
|
+
validationColor = "red";
|
|
2965
|
+
validationText = "not a git repo";
|
|
2966
|
+
}
|
|
2967
|
+
} else if (validation.status === "creatable") {
|
|
2968
|
+
validationIcon = "\u2717";
|
|
2969
|
+
validationColor = "red";
|
|
2970
|
+
validationText = "not a git repo";
|
|
2971
|
+
} else {
|
|
2972
|
+
validationIcon = "\u2717";
|
|
2973
|
+
validationColor = "red";
|
|
2974
|
+
validationText = "invalid path";
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
return _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "+ New Window" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: focusedField === "name" ? "cyan" : void 0, children: "Name: " }), _jsx(TextInput, { value: windowName, onChange: focusedField === "name" ? onWindowNameChange : () => {
|
|
2978
|
+
}, placeholder: "window-name", focus: focusedField === "name" })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: focusedField === "root" ? "cyan" : "gray", children: "Root: " }), focusedField === "root" ? _jsx(TextInput, { value: rootPath, onChange: handleRootPathChange, placeholder: "/path/to/dir", focus: true }) : _jsx(Text, { color: "gray", children: rootPath })] }), validation && _jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsxs(Text, { color: validationColor, children: [validationIcon, " ", validationText] })] }), dirSuggestions.isOpen && dirSuggestions.visibleSuggestions.length > 0 && _jsxs(Box, { flexDirection: "column", children: [dirSuggestions.visibleSuggestions.map((s) => _jsx(Box, { children: _jsxs(Text, { color: s.isSelected ? "cyan" : "gray", children: [s.isSelected ? " > " : " ", s.name, "/"] }) }, s.name)), dirSuggestions.hasMore && _jsx(Text, { color: "gray", children: " \u2191\u2193 for more" })] }), dirSuggestions.isOpen ? _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "\u2191\u2193:select Enter:pick Esc:close" }) }) : _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "Tab:field Enter:ok Esc:cancel" }) })] });
|
|
2979
|
+
}
|
|
2980
|
+
function DeleteConfirm({ windowName, onConfirm, onCancel }) {
|
|
2981
|
+
useInput((input, key) => {
|
|
2982
|
+
if (input === "y" || input === "Y") {
|
|
2983
|
+
onConfirm();
|
|
2984
|
+
} else if (input === "n" || input === "N" || key.escape) {
|
|
2985
|
+
onCancel();
|
|
2986
|
+
}
|
|
2987
|
+
});
|
|
2988
|
+
return _jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsxs(Text, { color: "red", children: ['Delete "', windowName, '"? '] }), _jsx(Text, { color: "gray", children: "(y/n)" })] });
|
|
2989
|
+
}
|
|
2990
|
+
function HintBar({ mode }) {
|
|
2991
|
+
if (mode === "new-window") {
|
|
2992
|
+
return null;
|
|
2993
|
+
}
|
|
2994
|
+
return _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "\u2191\u2193/jk: nav Enter: switch r: refresh q: detach" }) });
|
|
2995
|
+
}
|
|
2996
|
+
function Dashboard({ workspaceRoot, repoName, currentBranch, initialWindows, tmuxAdapter: tmuxAdapter2, dashWindowIndex, paneHeight }) {
|
|
2997
|
+
const [windows, setWindows] = useState4(initialWindows);
|
|
2998
|
+
const [selectedIndex, setSelectedIndex] = useState4(0);
|
|
2999
|
+
const [inputMode, setInputMode] = useState4("normal");
|
|
3000
|
+
const [newWindowName, setNewWindowName] = useState4("");
|
|
3001
|
+
const [rootPath, setRootPath] = useState4(workspaceRoot);
|
|
3002
|
+
const { validation, isGitRepo: formIsGitRepo } = usePathValidation(rootPath);
|
|
3003
|
+
const handleWindowNameChange = useCallback2((value) => {
|
|
3004
|
+
const sanitized = value.replace(/\s/g, "");
|
|
3005
|
+
setNewWindowName(sanitized);
|
|
3006
|
+
}, []);
|
|
3007
|
+
const [status, setStatus] = useState4("");
|
|
3008
|
+
const [isProcessing, setIsProcessing] = useState4(false);
|
|
3009
|
+
const activeWindowIdRef = useRef3(null);
|
|
3010
|
+
const statusTimerRef = useRef3(null);
|
|
3011
|
+
const showStatus = useCallback2((msg) => {
|
|
3012
|
+
if (statusTimerRef.current)
|
|
3013
|
+
clearTimeout(statusTimerRef.current);
|
|
3014
|
+
setStatus(msg);
|
|
3015
|
+
statusTimerRef.current = setTimeout(() => setStatus(""), 2e3);
|
|
3016
|
+
}, []);
|
|
3017
|
+
const refreshWindows = useCallback2(async () => {
|
|
3018
|
+
const updatedWindows = await loadAllWindows(tmuxAdapter2, dashWindowIndex);
|
|
3019
|
+
setWindows(updatedWindows);
|
|
3020
|
+
return { windows: updatedWindows };
|
|
3021
|
+
}, [tmuxAdapter2, dashWindowIndex]);
|
|
3022
|
+
useEffect2(() => {
|
|
3023
|
+
if (initialWindows.length === 0)
|
|
3024
|
+
return;
|
|
3025
|
+
const first = initialWindows[0];
|
|
3026
|
+
if (first) {
|
|
3027
|
+
void handleSelectWindow(first);
|
|
3028
|
+
}
|
|
3029
|
+
}, []);
|
|
3030
|
+
const HEADER_ROWS = 3;
|
|
3031
|
+
const NEW_WINDOW_ROWS = 3;
|
|
3032
|
+
const WINDOW_START_ROW = HEADER_ROWS + NEW_WINDOW_ROWS + 2;
|
|
3033
|
+
const WINDOW_HEIGHT = 4;
|
|
3034
|
+
const handleMouseClick = useCallback2((row, col) => {
|
|
3035
|
+
if (isProcessing || inputMode !== "normal")
|
|
3036
|
+
return;
|
|
3037
|
+
if (row >= HEADER_ROWS + 1 && row <= HEADER_ROWS + NEW_WINDOW_ROWS + 1) {
|
|
3038
|
+
setInputMode("new-window");
|
|
3039
|
+
setNewWindowName("");
|
|
3040
|
+
setRootPath(workspaceRoot);
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
if (row >= WINDOW_START_ROW && windows.length > 0) {
|
|
3044
|
+
const windowIndex = Math.floor((row - WINDOW_START_ROW) / WINDOW_HEIGHT);
|
|
3045
|
+
if (windowIndex >= 0 && windowIndex < windows.length) {
|
|
3046
|
+
setSelectedIndex(windowIndex);
|
|
3047
|
+
void handleSelectWindow(windows[windowIndex]);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
}, [isProcessing, inputMode, windows, workspaceRoot]);
|
|
3051
|
+
useMouse(handleMouseClick, inputMode === "normal");
|
|
3052
|
+
const findRightPane = async () => {
|
|
3053
|
+
const panes = await tmuxAdapter2.listPanes();
|
|
3054
|
+
return panes.find((p) => p.index !== 0);
|
|
3055
|
+
};
|
|
3056
|
+
const handleSelectWindow = async (win) => {
|
|
3057
|
+
try {
|
|
3058
|
+
if (win.windowId === activeWindowIdRef.current) {
|
|
3059
|
+
const rightPane2 = await findRightPane();
|
|
3060
|
+
if (rightPane2)
|
|
3061
|
+
await tmuxAdapter2.selectPane(rightPane2.id);
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
if (activeWindowIdRef.current) {
|
|
3065
|
+
const rightPane2 = await findRightPane();
|
|
3066
|
+
if (rightPane2) {
|
|
3067
|
+
try {
|
|
3068
|
+
await tmuxAdapter2.swapPaneWithWindow(activeWindowIdRef.current, rightPane2.id);
|
|
3069
|
+
} catch {
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
const rightPane = await findRightPane();
|
|
3074
|
+
if (rightPane) {
|
|
3075
|
+
await tmuxAdapter2.swapPaneWithWindow(win.windowId, rightPane.id);
|
|
3076
|
+
activeWindowIdRef.current = win.windowId;
|
|
3077
|
+
await tmuxAdapter2.selectPane((await findRightPane()).id);
|
|
3078
|
+
}
|
|
3079
|
+
showStatus(`Switched: ${win.name}`);
|
|
3080
|
+
} catch (error) {
|
|
3081
|
+
showStatus(`Error: ${error.message}`);
|
|
3082
|
+
}
|
|
3083
|
+
};
|
|
3084
|
+
useInput(async (input, key) => {
|
|
3085
|
+
if (isProcessing) {
|
|
3086
|
+
if (key.escape) {
|
|
3087
|
+
setIsProcessing(false);
|
|
3088
|
+
setInputMode("normal");
|
|
3089
|
+
showStatus("Cancelled");
|
|
3090
|
+
}
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
if (inputMode !== "normal")
|
|
3094
|
+
return;
|
|
3095
|
+
if (input === "j" || key.downArrow) {
|
|
3096
|
+
setSelectedIndex((prev) => (prev + 1) % Math.max(windows.length, 1));
|
|
3097
|
+
} else if (input === "k" || key.upArrow) {
|
|
3098
|
+
setSelectedIndex((prev) => (prev - 1 + Math.max(windows.length, 1)) % Math.max(windows.length, 1));
|
|
3099
|
+
} else if (input === "q") {
|
|
3100
|
+
try {
|
|
3101
|
+
if (activeWindowIdRef.current) {
|
|
3102
|
+
const rightPane = await findRightPane();
|
|
3103
|
+
if (rightPane) {
|
|
3104
|
+
try {
|
|
3105
|
+
await tmuxAdapter2.swapPaneWithWindow(activeWindowIdRef.current, rightPane.id);
|
|
3106
|
+
} catch {
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
const sessionName = `csq-${repoName}`;
|
|
3111
|
+
await tmuxAdapter2.detachAndKillWindow(sessionName, dashWindowIndex);
|
|
3112
|
+
} catch {
|
|
3113
|
+
}
|
|
3114
|
+
} else if (key.return) {
|
|
3115
|
+
const selected = windows[selectedIndex];
|
|
3116
|
+
if (selected) {
|
|
3117
|
+
await handleSelectWindow(selected);
|
|
3118
|
+
}
|
|
3119
|
+
} else if (input === "n" || input === "+") {
|
|
3120
|
+
setInputMode("new-window");
|
|
3121
|
+
setNewWindowName("");
|
|
3122
|
+
setRootPath(workspaceRoot);
|
|
3123
|
+
} else if (input === "d") {
|
|
3124
|
+
if (windows[selectedIndex]) {
|
|
3125
|
+
setInputMode("confirm-delete");
|
|
3126
|
+
}
|
|
3127
|
+
} else if (input === "r") {
|
|
3128
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
3129
|
+
process.stdout.emit("resize");
|
|
3130
|
+
await refreshWindows();
|
|
3131
|
+
showStatus("Refreshed");
|
|
3132
|
+
}
|
|
3133
|
+
});
|
|
3134
|
+
const handleCreateWindow = async () => {
|
|
3135
|
+
if (!newWindowName.trim()) {
|
|
3136
|
+
showStatus("Window name required");
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
if (!validation || !formIsGitRepo) {
|
|
3140
|
+
showStatus("Root must be a git repo");
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
setIsProcessing(true);
|
|
3144
|
+
setStatus(`Creating ${newWindowName}...`);
|
|
3145
|
+
try {
|
|
3146
|
+
const expandedRoot = expandTilde(rootPath);
|
|
3147
|
+
const newThread = await createThread(workspaceRoot, newWindowName.trim(), expandedRoot);
|
|
3148
|
+
const newWindowId = await tmuxAdapter2.createNewWindow(newThread.path, newWindowName.trim());
|
|
3149
|
+
const { windows: updatedWindows } = await refreshWindows();
|
|
3150
|
+
setInputMode("normal");
|
|
3151
|
+
setNewWindowName("");
|
|
3152
|
+
showStatus(`Created: ${newWindowName}`);
|
|
3153
|
+
const newWin = updatedWindows.find((w) => w.windowId === newWindowId);
|
|
3154
|
+
if (newWin) {
|
|
3155
|
+
await handleSelectWindow(newWin);
|
|
3156
|
+
}
|
|
3157
|
+
} catch (error) {
|
|
3158
|
+
showStatus(`Error: ${error.message}`);
|
|
3159
|
+
}
|
|
3160
|
+
setIsProcessing(false);
|
|
3161
|
+
};
|
|
3162
|
+
const handleDeleteWindow = async () => {
|
|
3163
|
+
const selected = windows[selectedIndex];
|
|
3164
|
+
if (!selected)
|
|
3165
|
+
return;
|
|
3166
|
+
setIsProcessing(true);
|
|
3167
|
+
setStatus(`Deleting ${selected.name}...`);
|
|
3168
|
+
try {
|
|
3169
|
+
if (selected.windowId === activeWindowIdRef.current) {
|
|
3170
|
+
const rightPane = await findRightPane();
|
|
3171
|
+
if (rightPane) {
|
|
3172
|
+
try {
|
|
3173
|
+
await tmuxAdapter2.swapPaneWithWindow(selected.windowId, rightPane.id);
|
|
3174
|
+
} catch {
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
activeWindowIdRef.current = null;
|
|
3178
|
+
}
|
|
3179
|
+
await deleteWindowById(tmuxAdapter2, selected.windowId);
|
|
3180
|
+
if (selected.worktreeBranch) {
|
|
3181
|
+
try {
|
|
3182
|
+
await deleteThread(workspaceRoot, {
|
|
3183
|
+
id: selected.cwd,
|
|
3184
|
+
name: selected.worktreeBranch,
|
|
3185
|
+
path: selected.cwd,
|
|
3186
|
+
branch: selected.worktreeBranch
|
|
3187
|
+
});
|
|
3188
|
+
} catch {
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
const { windows: updatedWindows } = await refreshWindows();
|
|
3192
|
+
const newIndex = Math.min(selectedIndex, Math.max(0, updatedWindows.length - 1));
|
|
3193
|
+
setSelectedIndex(newIndex);
|
|
3194
|
+
const fallbackWin = updatedWindows[newIndex];
|
|
3195
|
+
if (fallbackWin) {
|
|
3196
|
+
await handleSelectWindow(fallbackWin);
|
|
3197
|
+
}
|
|
3198
|
+
setInputMode("normal");
|
|
3199
|
+
showStatus(`Deleted: ${selected.name}`);
|
|
3200
|
+
} catch (error) {
|
|
3201
|
+
showStatus(`Error: ${error.message}`);
|
|
3202
|
+
setInputMode("normal");
|
|
3203
|
+
}
|
|
3204
|
+
setIsProcessing(false);
|
|
3205
|
+
};
|
|
3206
|
+
return _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, height: paneHeight, children: [_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "white", children: repoName }), _jsxs(Text, { color: "cyan", children: [" [", currentBranch, "]"] }), _jsxs(Text, { color: "gray", children: [" (", windows.length, ")"] })] }), inputMode === "new-window" ? _jsx(NewWindowForm, { windowName: newWindowName, onWindowNameChange: handleWindowNameChange, rootPath, onRootPathChange: setRootPath, validation, onSubmit: handleCreateWindow, onCancel: () => {
|
|
3207
|
+
setInputMode("normal");
|
|
3208
|
+
setNewWindowName("");
|
|
3209
|
+
} }) : _jsx(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginBottom: 1, children: _jsx(Text, { color: "gray", children: "+ New Window (press + or n)" }) }), inputMode === "confirm-delete" && windows[selectedIndex] && _jsx(DeleteConfirm, { windowName: windows[selectedIndex].name, onConfirm: handleDeleteWindow, onCancel: () => setInputMode("normal") }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: windows.length === 0 ? _jsx(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(Text, { color: "gray", children: "No windows yet" }) }) : windows.map((win, i) => _jsx(WindowCard, { window: win, isSelected: i === selectedIndex }, win.windowId)) }), _jsx(HintBar, { mode: inputMode }), status && _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: status }) })] });
|
|
3210
|
+
}
|
|
3211
|
+
async function runInkDashboard(config) {
|
|
3212
|
+
render(_jsx(Dashboard, { ...config }));
|
|
3213
|
+
await new Promise(() => {
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
// dist/dash/index.js
|
|
3218
|
+
var tmuxAdapter = new TmuxAdapter();
|
|
3219
|
+
var gitAdapter4 = new GitAdapter();
|
|
3220
|
+
async function installTmux() {
|
|
3221
|
+
const platform = process.platform;
|
|
3222
|
+
const { spawn: spawn3 } = await import("child_process");
|
|
3223
|
+
let command;
|
|
3224
|
+
let args;
|
|
3225
|
+
if (platform === "darwin") {
|
|
3226
|
+
console.log(chalk2.dim("Installing tmux via Homebrew..."));
|
|
3227
|
+
command = "brew";
|
|
3228
|
+
args = ["install", "tmux"];
|
|
3229
|
+
} else if (platform === "linux") {
|
|
3230
|
+
const { exec: exec3 } = await import("child_process");
|
|
3231
|
+
const { promisify: promisify3 } = await import("util");
|
|
3232
|
+
const execAsync = promisify3(exec3);
|
|
3233
|
+
try {
|
|
3234
|
+
await execAsync("which apt-get");
|
|
3235
|
+
console.log(chalk2.dim("Installing tmux via apt..."));
|
|
3236
|
+
command = "sudo";
|
|
3237
|
+
args = ["apt-get", "install", "-y", "tmux"];
|
|
3238
|
+
} catch {
|
|
3239
|
+
try {
|
|
3240
|
+
await execAsync("which yum");
|
|
3241
|
+
console.log(chalk2.dim("Installing tmux via yum..."));
|
|
3242
|
+
command = "sudo";
|
|
3243
|
+
args = ["yum", "install", "-y", "tmux"];
|
|
3244
|
+
} catch {
|
|
3245
|
+
console.error(chalk2.red("Could not find apt or yum package manager"));
|
|
3246
|
+
return false;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
} else {
|
|
3250
|
+
console.error(chalk2.red(`Unsupported platform: ${platform}`));
|
|
3251
|
+
console.error(chalk2.dim("Please install tmux manually"));
|
|
3252
|
+
return false;
|
|
3253
|
+
}
|
|
3254
|
+
return new Promise((resolve2) => {
|
|
3255
|
+
const proc = spawn3(command, args, { stdio: "inherit" });
|
|
3256
|
+
proc.on("close", (code) => {
|
|
3257
|
+
if (code === 0) {
|
|
3258
|
+
console.log(chalk2.green("\u2713 tmux installed successfully"));
|
|
3259
|
+
resolve2(true);
|
|
3260
|
+
} else {
|
|
3261
|
+
console.error(chalk2.red(`Installation failed with code ${code}`));
|
|
3262
|
+
resolve2(false);
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3265
|
+
proc.on("error", (error) => {
|
|
3266
|
+
console.error(chalk2.red(`Installation error: ${error.message}`));
|
|
3267
|
+
resolve2(false);
|
|
3268
|
+
});
|
|
3269
|
+
});
|
|
3270
|
+
}
|
|
3271
|
+
async function runDash(workspaceRoot) {
|
|
3272
|
+
const repoName = path14.basename(workspaceRoot);
|
|
3273
|
+
if (!await tmuxAdapter.isTmuxAvailable()) {
|
|
3274
|
+
console.log(chalk2.yellow("tmux is not installed."));
|
|
3275
|
+
console.log(chalk2.dim("tmux is required for the dashboard mode."));
|
|
3276
|
+
console.log("");
|
|
3277
|
+
const shouldInstall = await confirm2({
|
|
3278
|
+
message: "Would you like to install tmux now?",
|
|
3279
|
+
default: true
|
|
3280
|
+
});
|
|
3281
|
+
if (shouldInstall) {
|
|
3282
|
+
const success = await installTmux();
|
|
3283
|
+
if (!success) {
|
|
3284
|
+
console.error(chalk2.red("\nFailed to install tmux."));
|
|
3285
|
+
console.error(chalk2.dim("Please install manually:"));
|
|
3286
|
+
console.error(chalk2.dim(" macOS: brew install tmux"));
|
|
3287
|
+
console.error(chalk2.dim(" Ubuntu: sudo apt install tmux"));
|
|
3288
|
+
process.exit(1);
|
|
3289
|
+
}
|
|
3290
|
+
console.log("");
|
|
3291
|
+
} else {
|
|
3292
|
+
console.log(chalk2.dim("\nTo use dashboard mode, install tmux:"));
|
|
3293
|
+
console.error(chalk2.dim(" macOS: brew install tmux"));
|
|
3294
|
+
console.error(chalk2.dim(" Ubuntu: sudo apt install tmux"));
|
|
3295
|
+
console.log(chalk2.dim("\nOr use legacy mode: csq --legacy"));
|
|
3296
|
+
process.exit(0);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
if (!tmuxAdapter.isInsideTmux()) {
|
|
3300
|
+
const sessionName = `csq-${repoName}`;
|
|
3301
|
+
try {
|
|
3302
|
+
const isNewSession = await tmuxAdapter.ensureSession(sessionName, workspaceRoot);
|
|
3303
|
+
await tmuxAdapter.applyUXSettings();
|
|
3304
|
+
const scriptPath = process.argv[1];
|
|
3305
|
+
const nodeCmd = `node "${scriptPath}"`;
|
|
3306
|
+
if (isNewSession) {
|
|
3307
|
+
console.log(chalk2.dim("Starting new tmux session..."));
|
|
3308
|
+
await tmuxAdapter.sendKeys(`${sessionName}:0`, nodeCmd);
|
|
3309
|
+
await tmuxAdapter.sendEnter(`${sessionName}:0`);
|
|
3310
|
+
} else {
|
|
3311
|
+
console.log(chalk2.dim("Restoring dashboard..."));
|
|
3312
|
+
try {
|
|
3313
|
+
await tmuxAdapter.killWindow(`${sessionName}:0`);
|
|
3314
|
+
} catch {
|
|
3315
|
+
}
|
|
3316
|
+
await tmuxAdapter.createWindowAtIndex(sessionName, 0, workspaceRoot);
|
|
3317
|
+
await tmuxAdapter.selectWindow(`${sessionName}:0`);
|
|
3318
|
+
await tmuxAdapter.sendKeys(`${sessionName}:0`, nodeCmd);
|
|
3319
|
+
await tmuxAdapter.sendEnter(`${sessionName}:0`);
|
|
3320
|
+
}
|
|
3321
|
+
const { spawn: spawn3 } = await import("child_process");
|
|
3322
|
+
const tmux = spawn3("tmux", ["attach-session", "-t", sessionName], {
|
|
3323
|
+
stdio: "inherit"
|
|
3324
|
+
});
|
|
3325
|
+
await new Promise((resolve2, reject) => {
|
|
3326
|
+
tmux.on("close", (code) => {
|
|
3327
|
+
if (code === 0) {
|
|
3328
|
+
resolve2();
|
|
3329
|
+
} else {
|
|
3330
|
+
reject(new Error(`tmux exited with code ${code}`));
|
|
3331
|
+
}
|
|
3332
|
+
});
|
|
3333
|
+
tmux.on("error", reject);
|
|
3334
|
+
});
|
|
3335
|
+
} catch (error) {
|
|
3336
|
+
console.error(chalk2.red(`Failed to start tmux session: ${error.message}`));
|
|
3337
|
+
process.exit(1);
|
|
3338
|
+
}
|
|
3339
|
+
return;
|
|
3340
|
+
}
|
|
3341
|
+
console.clear();
|
|
3342
|
+
console.log(chalk2.bold.cyan("Code Squad Dashboard"));
|
|
3343
|
+
console.log(chalk2.dim("Setting up layout..."));
|
|
3344
|
+
await tmuxAdapter.applyUXSettings();
|
|
3345
|
+
try {
|
|
3346
|
+
const dashPaneId = await tmuxAdapter.getCurrentPaneId();
|
|
3347
|
+
if (!dashPaneId) {
|
|
3348
|
+
throw new Error("Could not get current pane ID");
|
|
3349
|
+
}
|
|
3350
|
+
const dashWindowIndex = await tmuxAdapter.getCurrentWindowIndex();
|
|
3351
|
+
if (dashWindowIndex === null) {
|
|
3352
|
+
throw new Error("Could not get current window index");
|
|
3353
|
+
}
|
|
3354
|
+
await tmuxAdapter.setDashboardResizeHook(dashPaneId, 35);
|
|
3355
|
+
const windows = await loadAllWindows(tmuxAdapter, dashWindowIndex);
|
|
3356
|
+
const currentBranch = await gitAdapter4.getCurrentBranch(workspaceRoot);
|
|
3357
|
+
const initialTerminalPaneId = await tmuxAdapter.splitWindow("v", workspaceRoot, 80);
|
|
3358
|
+
await tmuxAdapter.selectPane(dashPaneId);
|
|
3359
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
3360
|
+
process.stdout.columns = await tmuxAdapter.getPaneWidth(dashPaneId);
|
|
3361
|
+
process.stdout.rows = await tmuxAdapter.getPaneHeight(dashPaneId);
|
|
3362
|
+
console.clear();
|
|
3363
|
+
const paneHeight = await tmuxAdapter.getPaneHeight(dashPaneId);
|
|
3364
|
+
await runInkDashboard({
|
|
3365
|
+
workspaceRoot,
|
|
3366
|
+
repoName,
|
|
3367
|
+
currentBranch,
|
|
3368
|
+
initialWindows: windows,
|
|
3369
|
+
tmuxAdapter,
|
|
3370
|
+
dashWindowIndex,
|
|
3371
|
+
paneHeight
|
|
3372
|
+
});
|
|
3373
|
+
} catch (error) {
|
|
3374
|
+
console.error(chalk2.red(`Dashboard error: ${error.message}`));
|
|
3375
|
+
process.exit(1);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
|
|
2087
3379
|
// dist/index.js
|
|
2088
3380
|
process.on("SIGINT", () => {
|
|
2089
3381
|
process.exit(130);
|
|
2090
3382
|
});
|
|
2091
|
-
var
|
|
3383
|
+
var gitAdapter5 = new GitAdapter();
|
|
2092
3384
|
async function main() {
|
|
2093
3385
|
const args = process.argv.slice(2);
|
|
2094
3386
|
const persistentMode = args.includes("-p") || args.includes("--persistent");
|
|
@@ -2104,7 +3396,7 @@ async function main() {
|
|
|
2104
3396
|
}
|
|
2105
3397
|
const workspaceRoot = await findGitRoot(process.cwd());
|
|
2106
3398
|
if (!workspaceRoot) {
|
|
2107
|
-
console.error(
|
|
3399
|
+
console.error(chalk3.red("Error: Not a git repository"));
|
|
2108
3400
|
process.exit(1);
|
|
2109
3401
|
}
|
|
2110
3402
|
switch (command) {
|
|
@@ -2117,16 +3409,22 @@ async function main() {
|
|
|
2117
3409
|
case "quit":
|
|
2118
3410
|
await quitWorktreeCommand();
|
|
2119
3411
|
break;
|
|
2120
|
-
|
|
3412
|
+
case "dash":
|
|
3413
|
+
await runDash(workspaceRoot);
|
|
3414
|
+
break;
|
|
3415
|
+
case "--legacy":
|
|
2121
3416
|
if (persistentMode) {
|
|
2122
3417
|
await persistentInteractiveMode(workspaceRoot);
|
|
2123
3418
|
} else {
|
|
2124
3419
|
await interactiveMode(workspaceRoot);
|
|
2125
3420
|
}
|
|
3421
|
+
break;
|
|
3422
|
+
default:
|
|
3423
|
+
await runDash(workspaceRoot);
|
|
2126
3424
|
}
|
|
2127
3425
|
}
|
|
2128
3426
|
async function findGitRoot(cwd) {
|
|
2129
|
-
if (!await
|
|
3427
|
+
if (!await gitAdapter5.isGitRepository(cwd)) {
|
|
2130
3428
|
return null;
|
|
2131
3429
|
}
|
|
2132
3430
|
return cwd;
|
|
@@ -2136,13 +3434,13 @@ function getProjectHash(workspaceRoot) {
|
|
|
2136
3434
|
}
|
|
2137
3435
|
function getSessionsPath(workspaceRoot) {
|
|
2138
3436
|
const projectHash = getProjectHash(workspaceRoot);
|
|
2139
|
-
const projectName =
|
|
2140
|
-
return
|
|
3437
|
+
const projectName = path15.basename(workspaceRoot);
|
|
3438
|
+
return path15.join(os7.homedir(), ".code-squad", "sessions", `${projectName}-${projectHash}.json`);
|
|
2141
3439
|
}
|
|
2142
3440
|
async function loadLocalThreads(workspaceRoot) {
|
|
2143
3441
|
const sessionsPath = getSessionsPath(workspaceRoot);
|
|
2144
3442
|
try {
|
|
2145
|
-
const content = await
|
|
3443
|
+
const content = await fs13.promises.readFile(sessionsPath, "utf-8");
|
|
2146
3444
|
const data = JSON.parse(content);
|
|
2147
3445
|
return data.localThreads || [];
|
|
2148
3446
|
} catch {
|
|
@@ -2151,9 +3449,9 @@ async function loadLocalThreads(workspaceRoot) {
|
|
|
2151
3449
|
}
|
|
2152
3450
|
async function saveLocalThreads(workspaceRoot, threads) {
|
|
2153
3451
|
const sessionsPath = getSessionsPath(workspaceRoot);
|
|
2154
|
-
const dir =
|
|
2155
|
-
await
|
|
2156
|
-
await
|
|
3452
|
+
const dir = path15.dirname(sessionsPath);
|
|
3453
|
+
await fs13.promises.mkdir(dir, { recursive: true });
|
|
3454
|
+
await fs13.promises.writeFile(sessionsPath, JSON.stringify({ localThreads: threads }, null, 2));
|
|
2157
3455
|
}
|
|
2158
3456
|
async function addLocalThread(workspaceRoot, name) {
|
|
2159
3457
|
const threads = await loadLocalThreads(workspaceRoot);
|
|
@@ -2173,7 +3471,7 @@ async function removeLocalThread(workspaceRoot, id) {
|
|
|
2173
3471
|
await saveLocalThreads(workspaceRoot, filtered);
|
|
2174
3472
|
}
|
|
2175
3473
|
async function getAllThreads(workspaceRoot) {
|
|
2176
|
-
const worktrees = await
|
|
3474
|
+
const worktrees = await gitAdapter5.listWorktrees(workspaceRoot);
|
|
2177
3475
|
const localThreads = await loadLocalThreads(workspaceRoot);
|
|
2178
3476
|
const threads = [
|
|
2179
3477
|
...worktrees.map((wt) => ({
|
|
@@ -2224,12 +3522,12 @@ csq() {
|
|
|
2224
3522
|
async function listThreads(workspaceRoot) {
|
|
2225
3523
|
const threads = await getAllThreads(workspaceRoot);
|
|
2226
3524
|
if (threads.length === 0) {
|
|
2227
|
-
console.log(
|
|
3525
|
+
console.log(chalk3.dim("No threads found."));
|
|
2228
3526
|
return;
|
|
2229
3527
|
}
|
|
2230
3528
|
for (const t of threads) {
|
|
2231
|
-
const typeLabel = t.type === "worktree" ?
|
|
2232
|
-
console.log(`${typeLabel} ${t.name.padEnd(20)} ${
|
|
3529
|
+
const typeLabel = t.type === "worktree" ? chalk3.cyan("[W]") : chalk3.yellow("[L]");
|
|
3530
|
+
console.log(`${typeLabel} ${t.name.padEnd(20)} ${chalk3.dim(t.path)}`);
|
|
2233
3531
|
}
|
|
2234
3532
|
}
|
|
2235
3533
|
function parseNewArgs(args) {
|
|
@@ -2247,62 +3545,66 @@ function parseNewArgs(args) {
|
|
|
2247
3545
|
async function createWorktreeCommand(workspaceRoot, args) {
|
|
2248
3546
|
const { name, split } = parseNewArgs(args);
|
|
2249
3547
|
if (!name) {
|
|
2250
|
-
console.error(
|
|
2251
|
-
console.error(
|
|
3548
|
+
console.error(chalk3.red("Error: Name is required"));
|
|
3549
|
+
console.error(chalk3.dim("Usage: csq new <name> [-s|--split]"));
|
|
2252
3550
|
process.exit(1);
|
|
2253
3551
|
}
|
|
2254
|
-
const repoName =
|
|
2255
|
-
const defaultBasePath =
|
|
2256
|
-
const worktreePath =
|
|
3552
|
+
const repoName = path15.basename(workspaceRoot);
|
|
3553
|
+
const defaultBasePath = path15.join(path15.dirname(workspaceRoot), `${repoName}.worktree`);
|
|
3554
|
+
const worktreePath = path15.join(defaultBasePath, name);
|
|
2257
3555
|
try {
|
|
2258
|
-
await
|
|
2259
|
-
console.log(
|
|
3556
|
+
await gitAdapter5.createWorktree(worktreePath, name, workspaceRoot);
|
|
3557
|
+
console.log(chalk3.green(`\u2713 Created worktree: ${name}`));
|
|
2260
3558
|
await copyWorktreeFiles(workspaceRoot, worktreePath);
|
|
2261
3559
|
if (split) {
|
|
2262
3560
|
await openNewTerminal(worktreePath);
|
|
2263
3561
|
} else if (process.platform === "darwin") {
|
|
2264
|
-
await cdInCurrentTerminal(worktreePath);
|
|
3562
|
+
const success = await cdInCurrentTerminal(worktreePath);
|
|
3563
|
+
if (!success) {
|
|
3564
|
+
console.log(chalk3.dim("\nNote: Auto-cd may require shell function setup."));
|
|
3565
|
+
console.log(chalk3.dim('Run: eval "$(csq --init)"'));
|
|
3566
|
+
}
|
|
2265
3567
|
} else {
|
|
2266
3568
|
console.log(worktreePath);
|
|
2267
3569
|
}
|
|
2268
3570
|
} catch (error) {
|
|
2269
|
-
console.error(
|
|
3571
|
+
console.error(chalk3.red(`Failed to create worktree: ${error.message}`));
|
|
2270
3572
|
process.exit(1);
|
|
2271
3573
|
}
|
|
2272
3574
|
}
|
|
2273
3575
|
async function quitWorktreeCommand() {
|
|
2274
3576
|
const cwd = process.cwd();
|
|
2275
|
-
const context = await
|
|
3577
|
+
const context = await gitAdapter5.getWorktreeContext(cwd);
|
|
2276
3578
|
if (!context.isWorktree) {
|
|
2277
|
-
console.error(
|
|
3579
|
+
console.error(chalk3.red("Error: Not in a worktree"));
|
|
2278
3580
|
process.exit(1);
|
|
2279
3581
|
}
|
|
2280
3582
|
if (!context.mainRoot || !context.branch) {
|
|
2281
|
-
console.error(
|
|
3583
|
+
console.error(chalk3.red("Error: Could not determine worktree context"));
|
|
2282
3584
|
process.exit(1);
|
|
2283
3585
|
}
|
|
2284
|
-
const isDirty = await
|
|
3586
|
+
const isDirty = await gitAdapter5.hasDirtyState(cwd);
|
|
2285
3587
|
if (isDirty) {
|
|
2286
|
-
const confirmed = await
|
|
3588
|
+
const confirmed = await confirm3({
|
|
2287
3589
|
message: "Uncommitted changes detected. Delete anyway?",
|
|
2288
3590
|
default: false
|
|
2289
3591
|
});
|
|
2290
3592
|
if (!confirmed) {
|
|
2291
|
-
console.log(
|
|
3593
|
+
console.log(chalk3.dim("Cancelled."));
|
|
2292
3594
|
process.exit(0);
|
|
2293
3595
|
}
|
|
2294
3596
|
}
|
|
2295
3597
|
try {
|
|
2296
|
-
await
|
|
2297
|
-
await
|
|
2298
|
-
console.log(
|
|
3598
|
+
await gitAdapter5.removeWorktree(context.currentPath, context.mainRoot, true);
|
|
3599
|
+
await gitAdapter5.deleteBranch(context.branch, context.mainRoot, true);
|
|
3600
|
+
console.log(chalk3.green(`\u2713 Deleted worktree and branch: ${context.branch}`));
|
|
2299
3601
|
if (process.platform === "darwin") {
|
|
2300
3602
|
await cdInCurrentTerminal(context.mainRoot);
|
|
2301
3603
|
} else {
|
|
2302
3604
|
console.log(context.mainRoot);
|
|
2303
3605
|
}
|
|
2304
3606
|
} catch (error) {
|
|
2305
|
-
console.error(
|
|
3607
|
+
console.error(chalk3.red(`Failed to quit: ${error.message}`));
|
|
2306
3608
|
process.exit(1);
|
|
2307
3609
|
}
|
|
2308
3610
|
}
|
|
@@ -2314,17 +3616,56 @@ async function copyWorktreeFiles(sourceRoot, destRoot) {
|
|
|
2314
3616
|
}
|
|
2315
3617
|
const { copied, failed } = await copyFilesWithPatterns(sourceRoot, destRoot, patterns);
|
|
2316
3618
|
if (copied.length > 0) {
|
|
2317
|
-
console.log(
|
|
3619
|
+
console.log(chalk3.green(`\u2713 Copied ${copied.length} file(s) to worktree`));
|
|
2318
3620
|
}
|
|
2319
3621
|
if (failed.length > 0) {
|
|
2320
|
-
console.log(
|
|
3622
|
+
console.log(chalk3.yellow(`\u26A0 Failed to copy ${failed.length} file(s)`));
|
|
2321
3623
|
}
|
|
2322
3624
|
}
|
|
2323
3625
|
async function cdInCurrentTerminal(targetPath) {
|
|
2324
|
-
const { exec:
|
|
3626
|
+
const { exec: exec3 } = await import("child_process");
|
|
2325
3627
|
const escapedPath = targetPath.replace(/'/g, "'\\''");
|
|
3628
|
+
const termProgram = process.env.TERM_PROGRAM;
|
|
3629
|
+
if (termProgram === "vscode" || termProgram?.includes("cursor")) {
|
|
3630
|
+
console.log(targetPath);
|
|
3631
|
+
return true;
|
|
3632
|
+
}
|
|
3633
|
+
if (termProgram === "iTerm.app") {
|
|
3634
|
+
const script = `
|
|
3635
|
+
tell application "iTerm"
|
|
3636
|
+
tell current session of current window
|
|
3637
|
+
write text "cd '${escapedPath}'"
|
|
3638
|
+
end tell
|
|
3639
|
+
end tell`;
|
|
3640
|
+
return new Promise((resolve2) => {
|
|
3641
|
+
exec3(`osascript -e '${script}'`, (error, stdout, stderr) => {
|
|
3642
|
+
if (error) {
|
|
3643
|
+
console.log(targetPath);
|
|
3644
|
+
resolve2(true);
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
resolve2(true);
|
|
3648
|
+
});
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
if (termProgram === "Apple_Terminal") {
|
|
3652
|
+
const terminalScript = `
|
|
3653
|
+
tell application "Terminal"
|
|
3654
|
+
do script "cd '${escapedPath}'" in front window
|
|
3655
|
+
end tell`;
|
|
3656
|
+
return new Promise((resolve2) => {
|
|
3657
|
+
exec3(`osascript -e '${terminalScript}'`, (error, stdout, stderr) => {
|
|
3658
|
+
if (error) {
|
|
3659
|
+
console.log(targetPath);
|
|
3660
|
+
resolve2(true);
|
|
3661
|
+
return;
|
|
3662
|
+
}
|
|
3663
|
+
resolve2(true);
|
|
3664
|
+
});
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
2326
3667
|
const hasIterm = await new Promise((resolve2) => {
|
|
2327
|
-
|
|
3668
|
+
exec3('mdfind "kMDItemCFBundleIdentifier == com.googlecode.iterm2"', (error, stdout) => {
|
|
2328
3669
|
resolve2(!error && stdout.trim().length > 0);
|
|
2329
3670
|
});
|
|
2330
3671
|
});
|
|
@@ -2336,26 +3677,22 @@ tell application "iTerm2"
|
|
|
2336
3677
|
end tell
|
|
2337
3678
|
end tell`;
|
|
2338
3679
|
return new Promise((resolve2) => {
|
|
2339
|
-
|
|
2340
|
-
|
|
3680
|
+
exec3(`osascript -e '${script}'`, (error) => {
|
|
3681
|
+
if (error) {
|
|
3682
|
+
console.log(targetPath);
|
|
3683
|
+
}
|
|
3684
|
+
resolve2(true);
|
|
2341
3685
|
});
|
|
2342
3686
|
});
|
|
2343
3687
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
do script "cd '${escapedPath}'" in front window
|
|
2347
|
-
end tell`;
|
|
2348
|
-
return new Promise((resolve2) => {
|
|
2349
|
-
exec2(`osascript -e '${terminalScript}'`, (error) => {
|
|
2350
|
-
resolve2(!error);
|
|
2351
|
-
});
|
|
2352
|
-
});
|
|
3688
|
+
console.log(targetPath);
|
|
3689
|
+
return true;
|
|
2353
3690
|
}
|
|
2354
3691
|
async function openNewTerminal(targetPath) {
|
|
2355
|
-
const { exec:
|
|
3692
|
+
const { exec: exec3 } = await import("child_process");
|
|
2356
3693
|
const escapedPath = targetPath.replace(/'/g, "'\\''");
|
|
2357
3694
|
const hasIterm = await new Promise((resolve2) => {
|
|
2358
|
-
|
|
3695
|
+
exec3('mdfind "kMDItemCFBundleIdentifier == com.googlecode.iterm2"', (error, stdout) => {
|
|
2359
3696
|
resolve2(!error && stdout.trim().length > 0);
|
|
2360
3697
|
});
|
|
2361
3698
|
});
|
|
@@ -2370,7 +3707,7 @@ tell application "iTerm2"
|
|
|
2370
3707
|
end tell
|
|
2371
3708
|
end tell`;
|
|
2372
3709
|
return new Promise((resolve2) => {
|
|
2373
|
-
|
|
3710
|
+
exec3(`osascript -e '${script}'`, (error) => {
|
|
2374
3711
|
resolve2(!error);
|
|
2375
3712
|
});
|
|
2376
3713
|
});
|
|
@@ -2381,7 +3718,7 @@ tell application "Terminal"
|
|
|
2381
3718
|
do script "cd '${escapedPath}'"
|
|
2382
3719
|
end tell`;
|
|
2383
3720
|
return new Promise((resolve2) => {
|
|
2384
|
-
|
|
3721
|
+
exec3(`osascript -e '${terminalScript}'`, (error) => {
|
|
2385
3722
|
resolve2(!error);
|
|
2386
3723
|
});
|
|
2387
3724
|
});
|
|
@@ -2409,30 +3746,30 @@ async function executeDelete(thread, workspaceRoot) {
|
|
|
2409
3746
|
const confirmed = await confirmDeleteLocal(thread.name);
|
|
2410
3747
|
if (confirmed) {
|
|
2411
3748
|
await removeLocalThread(workspaceRoot, thread.id);
|
|
2412
|
-
console.log(
|
|
3749
|
+
console.log(chalk3.green(`\u2713 Deleted local thread: ${thread.name}`));
|
|
2413
3750
|
} else {
|
|
2414
|
-
console.log(
|
|
3751
|
+
console.log(chalk3.dim("Cancelled."));
|
|
2415
3752
|
}
|
|
2416
3753
|
} else {
|
|
2417
3754
|
const { confirmed, removeGitWorktree } = await confirmDeleteWorktree(thread.name);
|
|
2418
3755
|
if (confirmed && removeGitWorktree) {
|
|
2419
3756
|
try {
|
|
2420
|
-
await
|
|
2421
|
-
await
|
|
2422
|
-
console.log(
|
|
3757
|
+
await gitAdapter5.removeWorktree(thread.path, workspaceRoot, true);
|
|
3758
|
+
await gitAdapter5.deleteBranch(thread.branch, workspaceRoot, true);
|
|
3759
|
+
console.log(chalk3.green(`\u2713 Deleted worktree and branch: ${thread.name}`));
|
|
2423
3760
|
} catch (error) {
|
|
2424
|
-
console.error(
|
|
3761
|
+
console.error(chalk3.red(`Failed to delete: ${error.message}`));
|
|
2425
3762
|
}
|
|
2426
3763
|
} else if (confirmed) {
|
|
2427
|
-
console.log(
|
|
3764
|
+
console.log(chalk3.yellow("Worktree kept."));
|
|
2428
3765
|
} else {
|
|
2429
|
-
console.log(
|
|
3766
|
+
console.log(chalk3.dim("Cancelled."));
|
|
2430
3767
|
}
|
|
2431
3768
|
}
|
|
2432
3769
|
}
|
|
2433
3770
|
async function runInteraction(workspaceRoot, _persistent = false) {
|
|
2434
3771
|
const threads = await getAllThreads(workspaceRoot);
|
|
2435
|
-
const repoName =
|
|
3772
|
+
const repoName = path15.basename(workspaceRoot);
|
|
2436
3773
|
const choice = await selectThread(threads, repoName);
|
|
2437
3774
|
if (choice.type === "exit") {
|
|
2438
3775
|
return { exit: true };
|
|
@@ -2463,18 +3800,18 @@ async function runInteraction(workspaceRoot, _persistent = false) {
|
|
|
2463
3800
|
return null;
|
|
2464
3801
|
}
|
|
2465
3802
|
if (newType === "worktree") {
|
|
2466
|
-
const defaultBasePath =
|
|
3803
|
+
const defaultBasePath = path15.join(path15.dirname(workspaceRoot), `${repoName}.worktree`);
|
|
2467
3804
|
const form = await newWorktreeForm(defaultBasePath);
|
|
2468
3805
|
if (!form) {
|
|
2469
3806
|
return null;
|
|
2470
3807
|
}
|
|
2471
3808
|
try {
|
|
2472
|
-
await
|
|
2473
|
-
console.log(
|
|
3809
|
+
await gitAdapter5.createWorktree(form.path, form.name, workspaceRoot);
|
|
3810
|
+
console.log(chalk3.green(`\u2713 Created worktree: ${form.name}`));
|
|
2474
3811
|
await copyWorktreeFiles(workspaceRoot, form.path);
|
|
2475
3812
|
targetPath = form.path;
|
|
2476
3813
|
} catch (error) {
|
|
2477
|
-
console.error(
|
|
3814
|
+
console.error(chalk3.red(`Failed to create worktree: ${error.message}`));
|
|
2478
3815
|
}
|
|
2479
3816
|
} else {
|
|
2480
3817
|
const name = await newLocalForm();
|
|
@@ -2482,7 +3819,7 @@ async function runInteraction(workspaceRoot, _persistent = false) {
|
|
|
2482
3819
|
return null;
|
|
2483
3820
|
}
|
|
2484
3821
|
await addLocalThread(workspaceRoot, name);
|
|
2485
|
-
console.log(
|
|
3822
|
+
console.log(chalk3.green(`\u2713 Created local thread: ${name}`));
|
|
2486
3823
|
targetPath = workspaceRoot;
|
|
2487
3824
|
}
|
|
2488
3825
|
break;
|
|
@@ -2497,6 +3834,6 @@ main().catch((error) => {
|
|
|
2497
3834
|
if (error.message?.includes("SIGINT") || error.message?.includes("force closed")) {
|
|
2498
3835
|
process.exit(130);
|
|
2499
3836
|
}
|
|
2500
|
-
console.error(
|
|
3837
|
+
console.error(chalk3.red(`Error: ${error.message}`));
|
|
2501
3838
|
process.exit(1);
|
|
2502
3839
|
});
|