code-squad-cli 1.3.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +439 -2142
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +124 -0
- package/package.json +6 -9
- package/dist/dash/InkDashboard.d.ts +0 -13
- package/dist/dash/InkDashboard.js +0 -442
- package/dist/dash/TmuxAdapter.d.ts +0 -233
- package/dist/dash/TmuxAdapter.js +0 -520
- package/dist/dash/index.d.ts +0 -4
- package/dist/dash/index.js +0 -216
- package/dist/dash/pathUtils.d.ts +0 -27
- package/dist/dash/pathUtils.js +0 -70
- package/dist/dash/threadHelpers.d.ts +0 -9
- package/dist/dash/threadHelpers.js +0 -37
- package/dist/dash/types.d.ts +0 -42
- package/dist/dash/types.js +0 -1
- package/dist/dash/useDirectorySuggestions.d.ts +0 -23
- package/dist/dash/useDirectorySuggestions.js +0 -136
- package/dist/dash/usePathValidation.d.ts +0 -9
- package/dist/dash/usePathValidation.js +0 -34
- package/dist/dash/windowHelpers.d.ts +0 -10
- package/dist/dash/windowHelpers.js +0 -43
- package/dist/flip-ui/dist/assets/index-DYY1gRRa.css +0 -1
- package/dist/flip-ui/dist/assets/index-KAtdqB2p.js +0 -217
- package/dist/flip-ui/dist/index.html +0 -13
- package/dist/ui/prompts.d.ts +0 -47
- package/dist/ui/prompts.js +0 -328
package/dist/index.js
CHANGED
|
@@ -1,428 +1,385 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
9
11
|
|
|
10
12
|
// dist/adapters/GitAdapter.js
|
|
11
13
|
import { exec as execCallback } from "child_process";
|
|
12
14
|
import { promisify } from "util";
|
|
13
15
|
import * as fs from "fs";
|
|
14
|
-
var exec
|
|
15
|
-
var
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 "";
|
|
16
|
+
var exec, execOptions, GitAdapter;
|
|
17
|
+
var init_GitAdapter = __esm({
|
|
18
|
+
"dist/adapters/GitAdapter.js"() {
|
|
19
|
+
"use strict";
|
|
20
|
+
exec = promisify(execCallback);
|
|
21
|
+
execOptions = { maxBuffer: 1024 * 1024 };
|
|
22
|
+
GitAdapter = class {
|
|
23
|
+
async isGitRepository(workspaceRoot) {
|
|
24
|
+
try {
|
|
25
|
+
await exec(`cd "${workspaceRoot}" && git rev-parse --git-dir`, execOptions);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
35
30
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const branchLine = lines[i + 2];
|
|
48
|
-
if (!worktreeLine || !headLine) {
|
|
49
|
-
i++;
|
|
50
|
-
continue;
|
|
31
|
+
async getCurrentBranch(workspaceRoot) {
|
|
32
|
+
try {
|
|
33
|
+
const { stdout } = await exec(`cd "${workspaceRoot}" && git rev-parse --abbrev-ref HEAD`, execOptions);
|
|
34
|
+
return stdout.trim();
|
|
35
|
+
} catch {
|
|
36
|
+
try {
|
|
37
|
+
const { stdout } = await exec(`cd "${workspaceRoot}" && git symbolic-ref --short HEAD`, execOptions);
|
|
38
|
+
return stdout.trim();
|
|
39
|
+
} catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
51
42
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
43
|
+
}
|
|
44
|
+
async listWorktrees(workspaceRoot) {
|
|
45
|
+
try {
|
|
46
|
+
const { stdout } = await exec(`cd "${workspaceRoot}" && git worktree list --porcelain`, execOptions);
|
|
47
|
+
const worktrees = [];
|
|
48
|
+
const lines = stdout.split("\n").filter((line) => line.trim());
|
|
49
|
+
let i = 0;
|
|
50
|
+
while (i < lines.length) {
|
|
51
|
+
const worktreeLine = lines[i];
|
|
52
|
+
const headLine = lines[i + 1];
|
|
53
|
+
const branchLine = lines[i + 2];
|
|
54
|
+
if (!worktreeLine || !headLine) {
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const pathMatch = worktreeLine.match(/^worktree (.+)$/);
|
|
59
|
+
const headMatch = headLine.match(/^HEAD (.+)$/);
|
|
60
|
+
const branchMatch = branchLine?.match(/^branch refs\/heads\/(.+)$/);
|
|
61
|
+
if (pathMatch && headMatch) {
|
|
62
|
+
const path13 = pathMatch[1];
|
|
63
|
+
const head = headMatch[1];
|
|
64
|
+
const branch = branchMatch ? branchMatch[1] : "HEAD";
|
|
65
|
+
if (path13 !== workspaceRoot) {
|
|
66
|
+
worktrees.push({ path: path13, branch, head });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
i += 3;
|
|
61
70
|
}
|
|
71
|
+
return worktrees;
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
62
74
|
}
|
|
63
|
-
i += 3;
|
|
64
75
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
async createWorktree(worktreePath, branch, workspaceRoot) {
|
|
77
|
+
await exec(`cd "${workspaceRoot}" && git worktree prune`, execOptions).catch(() => {
|
|
78
|
+
});
|
|
79
|
+
const parentDir = worktreePath.substring(0, worktreePath.lastIndexOf("/"));
|
|
80
|
+
const mkdirCmd = parentDir ? `mkdir -p "${parentDir}" && ` : "";
|
|
81
|
+
await exec(`cd "${workspaceRoot}" && ${mkdirCmd}git worktree add -f "${worktreePath}" -b "${branch}"`, execOptions);
|
|
82
|
+
}
|
|
83
|
+
async removeWorktree(worktreePath, workspaceRoot, force = false) {
|
|
84
|
+
const forceFlag = force ? " --force" : "";
|
|
85
|
+
try {
|
|
86
|
+
await exec(`cd "${workspaceRoot}" && git worktree remove "${worktreePath}"${forceFlag}`, execOptions);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new Error(`Failed to remove worktree: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async deleteBranch(branchName, workspaceRoot, force = false) {
|
|
92
|
+
const deleteFlag = force ? "-D" : "-d";
|
|
93
|
+
try {
|
|
94
|
+
await exec(`cd "${workspaceRoot}" && git branch ${deleteFlag} "${branchName}"`, execOptions);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new Error(`Failed to delete branch: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async isValidWorktree(path13, workspaceRoot) {
|
|
100
|
+
try {
|
|
101
|
+
await fs.promises.access(path13, fs.constants.R_OK);
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
await exec(`cd "${path13}" && git rev-parse --git-dir`, execOptions);
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const worktrees = await this.listWorktrees(workspaceRoot);
|
|
111
|
+
return worktrees.some((wt) => wt.path === path13);
|
|
112
|
+
}
|
|
113
|
+
async getWorktreeBranch(worktreePath) {
|
|
114
|
+
try {
|
|
115
|
+
const { stdout } = await exec(`cd "${worktreePath}" && git rev-parse --abbrev-ref HEAD`, execOptions);
|
|
116
|
+
return stdout.trim();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new Error(`Failed to get branch name: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 현재 디렉토리가 워크트리인지 확인하고 컨텍스트 반환
|
|
123
|
+
*/
|
|
124
|
+
async getWorktreeContext(cwd) {
|
|
125
|
+
let commonDir = null;
|
|
126
|
+
try {
|
|
127
|
+
const { stdout } = await exec(`cd "${cwd}" && git rev-parse --git-common-dir`, execOptions);
|
|
128
|
+
commonDir = stdout.trim();
|
|
129
|
+
} catch {
|
|
130
|
+
return { isWorktree: false, mainRoot: null, currentPath: cwd, branch: null };
|
|
131
|
+
}
|
|
132
|
+
const isWorktree = commonDir !== ".git";
|
|
133
|
+
if (!isWorktree) {
|
|
134
|
+
return { isWorktree: false, mainRoot: cwd, currentPath: cwd, branch: null };
|
|
135
|
+
}
|
|
136
|
+
let mainRoot = null;
|
|
137
|
+
try {
|
|
138
|
+
const { stdout } = await exec(`cd "${cwd}" && git worktree list --porcelain`, execOptions);
|
|
139
|
+
const match = stdout.match(/^worktree (.+)$/m);
|
|
140
|
+
mainRoot = match ? match[1] : null;
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
const branch = await this.getWorktreeBranch(cwd).catch(() => null);
|
|
144
|
+
return { isWorktree, mainRoot, currentPath: cwd, branch };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* staged 또는 unstaged 변경사항이 있는지 확인 (untracked 제외)
|
|
148
|
+
*/
|
|
149
|
+
async hasDirtyState(workspacePath) {
|
|
150
|
+
try {
|
|
151
|
+
const { stdout } = await exec(`cd "${workspacePath}" && git status --porcelain`, execOptions);
|
|
152
|
+
const lines = stdout.split("\n").filter((line) => line.trim());
|
|
153
|
+
const dirtyLines = lines.filter((line) => !line.startsWith("??"));
|
|
154
|
+
return dirtyLines.length > 0;
|
|
155
|
+
} catch {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
84
160
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// dist/config.js
|
|
164
|
+
import * as fs9 from "fs";
|
|
165
|
+
import * as os4 from "os";
|
|
166
|
+
import * as path9 from "path";
|
|
167
|
+
async function loadGlobalConfig() {
|
|
168
|
+
try {
|
|
169
|
+
const content = await fs9.promises.readFile(GLOBAL_CONFIG_PATH, "utf-8");
|
|
170
|
+
return JSON.parse(content);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
173
|
+
return {};
|
|
91
174
|
}
|
|
175
|
+
console.warn(`[Code Squad] Warning: Could not load global config at ${GLOBAL_CONFIG_PATH}.`, error);
|
|
176
|
+
return {};
|
|
92
177
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
178
|
+
}
|
|
179
|
+
async function loadConfig(workspaceRoot) {
|
|
180
|
+
const globalConfig = await loadGlobalConfig();
|
|
181
|
+
const normalizedPath = path9.resolve(workspaceRoot);
|
|
182
|
+
const projectConfig = globalConfig.projects?.[normalizedPath] ?? {};
|
|
183
|
+
const defaults = globalConfig.defaults ?? {};
|
|
184
|
+
return {
|
|
185
|
+
...defaults,
|
|
186
|
+
...projectConfig,
|
|
187
|
+
worktreeCopyPatterns: [
|
|
188
|
+
.../* @__PURE__ */ new Set([
|
|
189
|
+
...defaults.worktreeCopyPatterns ?? [],
|
|
190
|
+
...projectConfig.worktreeCopyPatterns ?? []
|
|
191
|
+
])
|
|
192
|
+
]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function getWorktreeCopyPatterns(config) {
|
|
196
|
+
return config.worktreeCopyPatterns ?? [];
|
|
197
|
+
}
|
|
198
|
+
var GLOBAL_CONFIG_PATH;
|
|
199
|
+
var init_config = __esm({
|
|
200
|
+
"dist/config.js"() {
|
|
201
|
+
"use strict";
|
|
202
|
+
GLOBAL_CONFIG_PATH = path9.join(os4.homedir(), ".code-squad", "config.json");
|
|
106
203
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// dist/fileUtils.js
|
|
207
|
+
import * as fs10 from "fs";
|
|
208
|
+
import * as path10 from "path";
|
|
209
|
+
import fg from "fast-glob";
|
|
210
|
+
async function copyFilesWithPatterns(sourceRoot, destRoot, patterns) {
|
|
211
|
+
const copied = [];
|
|
212
|
+
const failed = [];
|
|
213
|
+
if (patterns.length === 0) {
|
|
214
|
+
return { copied, failed };
|
|
114
215
|
}
|
|
115
|
-
|
|
116
|
-
* 현재 디렉토리가 워크트리인지 확인하고 컨텍스트 반환
|
|
117
|
-
*/
|
|
118
|
-
async getWorktreeContext(cwd) {
|
|
119
|
-
let commonDir = null;
|
|
120
|
-
try {
|
|
121
|
-
const { stdout } = await exec(`cd "${cwd}" && git rev-parse --git-common-dir`, execOptions);
|
|
122
|
-
commonDir = stdout.trim();
|
|
123
|
-
} catch {
|
|
124
|
-
return { isWorktree: false, mainRoot: null, currentPath: cwd, branch: null };
|
|
125
|
-
}
|
|
126
|
-
const isWorktree = commonDir !== ".git";
|
|
127
|
-
if (!isWorktree) {
|
|
128
|
-
return { isWorktree: false, mainRoot: cwd, currentPath: cwd, branch: null };
|
|
129
|
-
}
|
|
130
|
-
let mainRoot = null;
|
|
216
|
+
for (const pattern of patterns) {
|
|
131
217
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
218
|
+
const files = await fg(pattern, {
|
|
219
|
+
cwd: sourceRoot,
|
|
220
|
+
absolute: true,
|
|
221
|
+
onlyFiles: true,
|
|
222
|
+
dot: true
|
|
223
|
+
// .env 같은 dotfile도 매칭
|
|
224
|
+
});
|
|
225
|
+
for (const absolutePath of files) {
|
|
226
|
+
try {
|
|
227
|
+
await copySingleFile(absolutePath, sourceRoot, destRoot);
|
|
228
|
+
const relativePath = path10.relative(sourceRoot, absolutePath);
|
|
229
|
+
copied.push(relativePath);
|
|
230
|
+
} catch {
|
|
231
|
+
const relativePath = path10.relative(sourceRoot, absolutePath);
|
|
232
|
+
failed.push(relativePath);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
135
235
|
} catch {
|
|
136
236
|
}
|
|
137
|
-
const branch = await this.getWorktreeBranch(cwd).catch(() => null);
|
|
138
|
-
return { isWorktree, mainRoot, currentPath: cwd, branch };
|
|
139
237
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
238
|
+
return { copied, failed };
|
|
239
|
+
}
|
|
240
|
+
async function copySingleFile(absolutePath, sourceRoot, destRoot) {
|
|
241
|
+
const relativePath = path10.relative(sourceRoot, absolutePath);
|
|
242
|
+
const destPath = path10.join(destRoot, relativePath);
|
|
243
|
+
const destDir = path10.dirname(destPath);
|
|
244
|
+
await fs10.promises.mkdir(destDir, { recursive: true });
|
|
245
|
+
await fs10.promises.copyFile(absolutePath, destPath);
|
|
246
|
+
}
|
|
247
|
+
var init_fileUtils = __esm({
|
|
248
|
+
"dist/fileUtils.js"() {
|
|
249
|
+
"use strict";
|
|
152
250
|
}
|
|
153
|
-
};
|
|
251
|
+
});
|
|
154
252
|
|
|
155
|
-
// dist/
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
253
|
+
// dist/tui/App.js
|
|
254
|
+
var App_exports = {};
|
|
255
|
+
__export(App_exports, {
|
|
256
|
+
runTui: () => runTui
|
|
257
|
+
});
|
|
258
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
259
|
+
import { useState, useEffect, useCallback } from "react";
|
|
260
|
+
import { render, Box, Text, useInput, useApp } from "ink";
|
|
261
|
+
import * as path11 from "path";
|
|
262
|
+
import * as os5 from "os";
|
|
263
|
+
function shorten(p) {
|
|
264
|
+
const home = os5.homedir();
|
|
265
|
+
return p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
266
|
+
}
|
|
267
|
+
function App({ initialWorktrees, root }) {
|
|
268
|
+
const { exit } = useApp();
|
|
269
|
+
const [view, setView] = useState("list");
|
|
270
|
+
const [worktrees, setWorktrees] = useState(initialWorktrees);
|
|
271
|
+
const [cursor, setCursor] = useState(0);
|
|
272
|
+
const [input, setInput] = useState("");
|
|
273
|
+
const [msg, setMsg] = useState(null);
|
|
274
|
+
const [busy, setBusy] = useState(false);
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
if (!msg)
|
|
277
|
+
return;
|
|
278
|
+
const t = setTimeout(() => setMsg(null), 2e3);
|
|
279
|
+
return () => clearTimeout(t);
|
|
280
|
+
}, [msg]);
|
|
281
|
+
const refresh = useCallback(async () => {
|
|
282
|
+
const wts = await git.listWorktrees(root);
|
|
283
|
+
setWorktrees(wts);
|
|
284
|
+
setCursor((c) => Math.min(c, Math.max(0, wts.length - 1)));
|
|
285
|
+
}, [root]);
|
|
286
|
+
useInput((ch, key) => {
|
|
287
|
+
if (busy)
|
|
288
|
+
return;
|
|
289
|
+
if (view === "list") {
|
|
290
|
+
if (key.upArrow) {
|
|
291
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
292
|
+
} else if (key.downArrow) {
|
|
293
|
+
setCursor((c) => Math.min(worktrees.length - 1, c + 1));
|
|
294
|
+
} else if (key.return && worktrees.length > 0) {
|
|
295
|
+
process.stdout.write(worktrees[cursor].path + "\n");
|
|
296
|
+
exit();
|
|
297
|
+
} else if (ch === "n") {
|
|
298
|
+
setView("create");
|
|
299
|
+
setInput("");
|
|
300
|
+
} else if (ch === "d" && worktrees.length > 0) {
|
|
301
|
+
setView("delete");
|
|
302
|
+
} else if (ch === "q" || key.escape) {
|
|
303
|
+
exit();
|
|
185
304
|
}
|
|
305
|
+
return;
|
|
186
306
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
${chalk.dim("ESC:cancel Enter:confirm")}`;
|
|
192
|
-
});
|
|
193
|
-
function truncatePath(fullPath, maxLen) {
|
|
194
|
-
if (fullPath.length <= maxLen)
|
|
195
|
-
return fullPath;
|
|
196
|
-
const home = process.env.HOME || "";
|
|
197
|
-
let display = fullPath.startsWith(home) ? "~" + fullPath.slice(home.length) : fullPath;
|
|
198
|
-
if (display.length <= maxLen)
|
|
199
|
-
return display;
|
|
200
|
-
const parts = display.split(path.sep);
|
|
201
|
-
if (parts.length > 2) {
|
|
202
|
-
return "\u2026/" + parts.slice(-2).join("/");
|
|
203
|
-
}
|
|
204
|
-
return "\u2026" + display.slice(-maxLen + 1);
|
|
205
|
-
}
|
|
206
|
-
var vimSelect = createPrompt((config, done) => {
|
|
207
|
-
const { choices, pageSize = 15, shortcuts = [] } = config;
|
|
208
|
-
const enabledChoices = choices.filter((c) => !c.disabled);
|
|
209
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
210
|
-
const prefix = usePrefix({ status: "idle" });
|
|
211
|
-
useKeypress((key) => {
|
|
212
|
-
if (key.name === "j" || key.name === "down") {
|
|
213
|
-
const nextIndex = (selectedIndex + 1) % enabledChoices.length;
|
|
214
|
-
setSelectedIndex(nextIndex);
|
|
215
|
-
} else if (key.name === "k" || key.name === "up") {
|
|
216
|
-
const prevIndex = (selectedIndex - 1 + enabledChoices.length) % enabledChoices.length;
|
|
217
|
-
setSelectedIndex(prevIndex);
|
|
218
|
-
} else if (isEnterKey(key)) {
|
|
219
|
-
done(enabledChoices[selectedIndex].value);
|
|
220
|
-
} else if (key.name === "escape" || key.name === "b") {
|
|
221
|
-
const backShortcut = shortcuts.find((s) => s.key === "back");
|
|
222
|
-
if (backShortcut) {
|
|
223
|
-
done(backShortcut.value);
|
|
307
|
+
if (view === "create") {
|
|
308
|
+
if (key.escape) {
|
|
309
|
+
setView("list");
|
|
310
|
+
return;
|
|
224
311
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
312
|
+
if (key.return && input.trim()) {
|
|
313
|
+
const name = input.trim();
|
|
314
|
+
setBusy(true);
|
|
315
|
+
const base = path11.join(path11.dirname(root), `${path11.basename(root)}.worktree`);
|
|
316
|
+
const wtPath = path11.join(base, name);
|
|
317
|
+
git.createWorktree(wtPath, name, root).then(async () => {
|
|
318
|
+
const config = await loadConfig(root);
|
|
319
|
+
const patterns = getWorktreeCopyPatterns(config);
|
|
320
|
+
if (patterns.length > 0) {
|
|
321
|
+
await copyFilesWithPatterns(root, wtPath, patterns);
|
|
322
|
+
}
|
|
323
|
+
process.stdout.write(wtPath + "\n");
|
|
324
|
+
exit();
|
|
325
|
+
}).catch((e) => {
|
|
326
|
+
setMsg({ text: e.message, color: "red" });
|
|
327
|
+
setView("list");
|
|
328
|
+
setBusy(false);
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
229
331
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const halfPage = Math.floor(pageSize / 2);
|
|
234
|
-
let startIndex = 0;
|
|
235
|
-
if (totalItems > pageSize) {
|
|
236
|
-
if (selectedIndex <= halfPage) {
|
|
237
|
-
startIndex = 0;
|
|
238
|
-
} else if (selectedIndex >= totalItems - halfPage) {
|
|
239
|
-
startIndex = totalItems - pageSize;
|
|
240
|
-
} else {
|
|
241
|
-
startIndex = selectedIndex - halfPage;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
const visibleChoices = enabledChoices.slice(startIndex, startIndex + pageSize);
|
|
245
|
-
const lines = visibleChoices.map((choice, i) => {
|
|
246
|
-
const actualIndex = startIndex + i;
|
|
247
|
-
const isSelected = actualIndex === selectedIndex;
|
|
248
|
-
const cursor = isSelected ? chalk.cyan("\u276F") : " ";
|
|
249
|
-
const name = isSelected ? chalk.cyan(choice.name) : choice.name;
|
|
250
|
-
return `${cursor} ${name}`;
|
|
251
|
-
});
|
|
252
|
-
if (startIndex > 0) {
|
|
253
|
-
lines.unshift(chalk.dim(" \u2191 more"));
|
|
254
|
-
}
|
|
255
|
-
if (startIndex + pageSize < totalItems) {
|
|
256
|
-
lines.push(chalk.dim(" \u2193 more"));
|
|
257
|
-
}
|
|
258
|
-
const shortcutHints = shortcuts.filter((s) => s.description).map((s) => chalk.dim(`${s.key}:${s.description}`)).join(" ");
|
|
259
|
-
const hint = chalk.dim("j/k:navigate enter:select") + (shortcutHints ? " " + shortcutHints : "");
|
|
260
|
-
return `${prefix} ${chalk.bold(config.message)}
|
|
261
|
-
${lines.join("\n")}
|
|
262
|
-
${hint}`;
|
|
263
|
-
});
|
|
264
|
-
async function selectThread(threads, repoName) {
|
|
265
|
-
const cols = process.stdout.columns || 80;
|
|
266
|
-
const nameWidth = 18;
|
|
267
|
-
const prefixWidth = 6;
|
|
268
|
-
const pathMaxLen = Math.max(20, cols - nameWidth - prefixWidth - 5);
|
|
269
|
-
const threadChoices = threads.map((t) => {
|
|
270
|
-
const typeIcon = t.type === "worktree" ? chalk.cyan("[W]") : chalk.yellow("[L]");
|
|
271
|
-
const displayPath = truncatePath(t.path, pathMaxLen);
|
|
272
|
-
return {
|
|
273
|
-
name: `${typeIcon} ${t.name.padEnd(nameWidth)} ${chalk.dim(displayPath)}`,
|
|
274
|
-
value: { type: "existing", thread: t },
|
|
275
|
-
thread: t
|
|
276
|
-
};
|
|
277
|
-
});
|
|
278
|
-
const newChoice = {
|
|
279
|
-
name: chalk.green("+ \uC0C8 \uC791\uC5C5"),
|
|
280
|
-
value: { type: "new" },
|
|
281
|
-
thread: null
|
|
282
|
-
};
|
|
283
|
-
const allChoices = [...threadChoices, newChoice];
|
|
284
|
-
const threadSelect = createPrompt((config, done) => {
|
|
285
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
286
|
-
const prefix = usePrefix({ status: "idle" });
|
|
287
|
-
const pageSize = 15;
|
|
288
|
-
useKeypress((key) => {
|
|
289
|
-
if (key.name === "j" || key.name === "down") {
|
|
290
|
-
setSelectedIndex((selectedIndex + 1) % allChoices.length);
|
|
291
|
-
} else if (key.name === "k" || key.name === "up") {
|
|
292
|
-
setSelectedIndex((selectedIndex - 1 + allChoices.length) % allChoices.length);
|
|
293
|
-
} else if (isEnterKey(key)) {
|
|
294
|
-
done(allChoices[selectedIndex].value);
|
|
295
|
-
} else if (key.name === "n") {
|
|
296
|
-
done({ type: "new" });
|
|
297
|
-
} else if (key.name === "q") {
|
|
298
|
-
done({ type: "exit" });
|
|
299
|
-
} else if (key.name === "d") {
|
|
300
|
-
const current = allChoices[selectedIndex];
|
|
301
|
-
if (current.thread) {
|
|
302
|
-
done({ type: "delete-selected", thread: current.thread });
|
|
303
|
-
}
|
|
332
|
+
if (key.backspace || key.delete) {
|
|
333
|
+
setInput((v) => v.slice(0, -1));
|
|
334
|
+
return;
|
|
304
335
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const halfPage = Math.floor(pageSize / 2);
|
|
308
|
-
let startIndex = 0;
|
|
309
|
-
if (totalItems > pageSize) {
|
|
310
|
-
if (selectedIndex <= halfPage) {
|
|
311
|
-
startIndex = 0;
|
|
312
|
-
} else if (selectedIndex >= totalItems - halfPage) {
|
|
313
|
-
startIndex = totalItems - pageSize;
|
|
314
|
-
} else {
|
|
315
|
-
startIndex = selectedIndex - halfPage;
|
|
336
|
+
if (ch && !key.ctrl && !key.meta) {
|
|
337
|
+
setInput((v) => v + ch);
|
|
316
338
|
}
|
|
339
|
+
return;
|
|
317
340
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
${lines.join("\n")}
|
|
335
|
-
${hint}`;
|
|
336
|
-
});
|
|
337
|
-
return threadSelect({ message: chalk.bold(repoName) });
|
|
338
|
-
}
|
|
339
|
-
async function selectThreadAction(threadName) {
|
|
340
|
-
const choices = [
|
|
341
|
-
{ name: "\uD130\uBBF8\uB110 \uC5F4\uAE30", value: "open" },
|
|
342
|
-
{ name: chalk.red("\uC0AD\uC81C\uD558\uAE30"), value: "delete" },
|
|
343
|
-
{ name: chalk.dim("\u2190 \uB4A4\uB85C"), value: "back" }
|
|
344
|
-
];
|
|
345
|
-
return vimSelect({
|
|
346
|
-
message: `'${threadName}'`,
|
|
347
|
-
choices,
|
|
348
|
-
shortcuts: [{ key: "back", value: "back" }]
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
async function selectNewThreadType() {
|
|
352
|
-
const choices = [
|
|
353
|
-
{
|
|
354
|
-
name: chalk.cyan("\uC6CC\uD06C\uD2B8\uB9AC") + chalk.dim(" - \uC0C8 \uBE0C\uB79C\uCE58\uC640 \uB514\uB809\uD1A0\uB9AC \uC0DD\uC131"),
|
|
355
|
-
value: "worktree"
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
name: chalk.yellow("\uB85C\uCEEC") + chalk.dim(" - \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC791\uC5C5"),
|
|
359
|
-
value: "local"
|
|
360
|
-
},
|
|
361
|
-
{ name: chalk.dim("\u2190 \uB4A4\uB85C"), value: "back" }
|
|
362
|
-
];
|
|
363
|
-
return vimSelect({
|
|
364
|
-
message: "\uC0C8 \uC791\uC5C5 \uD0C0\uC785",
|
|
365
|
-
choices,
|
|
366
|
-
shortcuts: [{ key: "back", value: "back" }]
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
async function newWorktreeForm(defaultBasePath) {
|
|
370
|
-
const name = await cancelableInput({
|
|
371
|
-
message: "\uC6CC\uD06C\uD2B8\uB9AC \uC774\uB984:",
|
|
372
|
-
validate: (value) => {
|
|
373
|
-
if (!value.trim())
|
|
374
|
-
return "\uC774\uB984\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694";
|
|
375
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
|
|
376
|
-
return "\uC601\uBB38, \uC22B\uC790, -, _ \uB9CC \uC0AC\uC6A9 \uAC00\uB2A5";
|
|
341
|
+
if (view === "delete") {
|
|
342
|
+
if (ch === "y" || key.return) {
|
|
343
|
+
const target = worktrees[cursor];
|
|
344
|
+
setBusy(true);
|
|
345
|
+
git.removeWorktree(target.path, root, true).then(() => git.deleteBranch(target.branch, root, true)).then(async () => {
|
|
346
|
+
setMsg({ text: `Deleted ${target.branch}`, color: "green" });
|
|
347
|
+
await refresh();
|
|
348
|
+
setBusy(false);
|
|
349
|
+
setView("list");
|
|
350
|
+
}).catch((e) => {
|
|
351
|
+
setMsg({ text: e.message, color: "red" });
|
|
352
|
+
setBusy(false);
|
|
353
|
+
setView("list");
|
|
354
|
+
});
|
|
355
|
+
} else if (ch === "n" || key.escape) {
|
|
356
|
+
setView("list");
|
|
377
357
|
}
|
|
378
|
-
return true;
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
if (!name)
|
|
382
|
-
return null;
|
|
383
|
-
const defaultPath = `${defaultBasePath}/${name}`;
|
|
384
|
-
const pathInput = await cancelableInput({
|
|
385
|
-
message: "\uACBD\uB85C:",
|
|
386
|
-
default: defaultPath
|
|
387
|
-
});
|
|
388
|
-
if (!pathInput)
|
|
389
|
-
return null;
|
|
390
|
-
return { name, path: pathInput };
|
|
391
|
-
}
|
|
392
|
-
async function newLocalForm() {
|
|
393
|
-
const name = await cancelableInput({
|
|
394
|
-
message: "\uB85C\uCEEC \uC2A4\uB808\uB4DC \uC774\uB984:",
|
|
395
|
-
validate: (value) => {
|
|
396
|
-
if (!value.trim())
|
|
397
|
-
return "\uC774\uB984\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694";
|
|
398
|
-
return true;
|
|
399
358
|
}
|
|
400
359
|
});
|
|
401
|
-
return
|
|
402
|
-
}
|
|
403
|
-
async function
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
360
|
+
return _jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Code Squad" }), busy && _jsx(Text, { color: "yellow", children: " ..." })] }), view === "list" && (worktrees.length === 0 ? _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "No worktrees yet. Press " }), _jsx(Text, { color: "yellow", children: "n" }), _jsx(Text, { dimColor: true, children: " to create one." })] }) : _jsx(Box, { flexDirection: "column", marginBottom: 1, children: worktrees.map((wt, i) => _jsxs(Box, { children: [_jsx(Text, { color: i === cursor ? "cyan" : void 0, children: i === cursor ? "\u276F " : " " }), _jsx(Text, { bold: i === cursor, children: wt.branch.padEnd(20) }), _jsxs(Text, { dimColor: true, children: [" ", shorten(wt.path)] })] }, wt.path)) })), view === "create" && _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, children: "New Worktree" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "> " }), _jsx(Text, { color: "cyan", children: input }), _jsx(Text, { dimColor: true, children: "\u2588" })] })] }), view === "delete" && worktrees[cursor] && _jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsxs(Text, { children: ["Delete ", _jsx(Text, { bold: true, color: "yellow", children: worktrees[cursor].branch }), "?"] }) }), msg && _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: msg.color, children: msg.text }) }), _jsxs(Box, { gap: 2, children: [view === "list" && _jsxs(_Fragment, { children: [worktrees.length > 0 && _jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2191\u2193" }), " ", _jsx(Text, { dimColor: true, children: "navigate" })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u21B5" }), " ", _jsx(Text, { dimColor: true, children: "switch" })] })] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "n" }), " ", _jsx(Text, { dimColor: true, children: "new" })] }), worktrees.length > 0 && _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "d" }), " ", _jsx(Text, { dimColor: true, children: "delete" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "q" }), " ", _jsx(Text, { dimColor: true, children: "quit" })] })] }), view === "create" && _jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u21B5" }), " ", _jsx(Text, { dimColor: true, children: "create" })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "esc" }), " ", _jsx(Text, { dimColor: true, children: "cancel" })] })] }), view === "delete" && _jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "y" }), " ", _jsx(Text, { dimColor: true, children: "confirm" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "n" }), " ", _jsx(Text, { dimColor: true, children: "cancel" })] })] })] })] });
|
|
361
|
+
}
|
|
362
|
+
async function runTui(workspaceRoot) {
|
|
363
|
+
const worktrees = await git.listWorktrees(workspaceRoot);
|
|
364
|
+
const { waitUntilExit } = render(_jsx(App, { initialWorktrees: worktrees, root: workspaceRoot }), { stdout: process.stderr });
|
|
365
|
+
await waitUntilExit();
|
|
366
|
+
}
|
|
367
|
+
var git;
|
|
368
|
+
var init_App = __esm({
|
|
369
|
+
"dist/tui/App.js"() {
|
|
370
|
+
"use strict";
|
|
371
|
+
init_GitAdapter();
|
|
372
|
+
init_config();
|
|
373
|
+
init_fileUtils();
|
|
374
|
+
git = new GitAdapter();
|
|
410
375
|
}
|
|
411
|
-
|
|
412
|
-
message: "Git worktree\uC640 \uBE0C\uB79C\uCE58\uB3C4 \uD568\uAED8 \uC0AD\uC81C\uD560\uAE4C\uC694?",
|
|
413
|
-
default: true
|
|
414
|
-
});
|
|
415
|
-
return { confirmed, removeGitWorktree };
|
|
416
|
-
}
|
|
417
|
-
async function confirmDeleteLocal(threadName) {
|
|
418
|
-
return await confirm({
|
|
419
|
-
message: `'${threadName}' \uB85C\uCEEC \uC2A4\uB808\uB4DC\uB97C \uC0AD\uC81C\uD560\uAE4C\uC694?`,
|
|
420
|
-
default: true
|
|
421
|
-
});
|
|
422
|
-
}
|
|
376
|
+
});
|
|
423
377
|
|
|
424
378
|
// dist/index.js
|
|
425
|
-
|
|
379
|
+
init_GitAdapter();
|
|
380
|
+
import * as path12 from "path";
|
|
381
|
+
import chalk from "chalk";
|
|
382
|
+
import { confirm } from "@inquirer/prompts";
|
|
426
383
|
|
|
427
384
|
// dist/flip/server/Server.js
|
|
428
385
|
import express2 from "express";
|
|
@@ -433,7 +390,7 @@ import net from "net";
|
|
|
433
390
|
// dist/flip/routes/files.js
|
|
434
391
|
import { Router } from "express";
|
|
435
392
|
import fs2 from "fs";
|
|
436
|
-
import
|
|
393
|
+
import path from "path";
|
|
437
394
|
|
|
438
395
|
// dist/flip/constants/filters.js
|
|
439
396
|
var FILTERED_PATTERNS = /* @__PURE__ */ new Set([
|
|
@@ -500,8 +457,8 @@ function buildFileTree(rootPath, currentPath, maxDepth, depth = 0) {
|
|
|
500
457
|
return a.name.localeCompare(b.name);
|
|
501
458
|
});
|
|
502
459
|
for (const entry of entries) {
|
|
503
|
-
const fullPath =
|
|
504
|
-
const relativePath =
|
|
460
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
461
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
505
462
|
const filtered = isFiltered(entry.name);
|
|
506
463
|
const node = {
|
|
507
464
|
path: relativePath,
|
|
@@ -521,8 +478,8 @@ function collectFlatFiles(rootPath, currentPath, maxDepth, depth = 0, result = {
|
|
|
521
478
|
return result;
|
|
522
479
|
const entries = fs2.readdirSync(currentPath, { withFileTypes: true });
|
|
523
480
|
for (const entry of entries) {
|
|
524
|
-
const fullPath =
|
|
525
|
-
const relativePath =
|
|
481
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
482
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
526
483
|
if (isFiltered(entry.name)) {
|
|
527
484
|
if (entry.isDirectory()) {
|
|
528
485
|
result.filteredDirs.push(relativePath);
|
|
@@ -592,7 +549,7 @@ router.get("/flat", (req, res) => {
|
|
|
592
549
|
// dist/flip/routes/file.js
|
|
593
550
|
import { Router as Router2 } from "express";
|
|
594
551
|
import fs3 from "fs";
|
|
595
|
-
import
|
|
552
|
+
import path2 from "path";
|
|
596
553
|
var router2 = Router2();
|
|
597
554
|
var extensionMap = {
|
|
598
555
|
// JavaScript/TypeScript
|
|
@@ -705,11 +662,11 @@ var filenameMap = {
|
|
|
705
662
|
"CODEOWNERS": "gitignore"
|
|
706
663
|
};
|
|
707
664
|
function detectLanguage(filePath) {
|
|
708
|
-
const
|
|
709
|
-
if (filenameMap[
|
|
710
|
-
return filenameMap[
|
|
665
|
+
const basename3 = path2.basename(filePath);
|
|
666
|
+
if (filenameMap[basename3]) {
|
|
667
|
+
return filenameMap[basename3];
|
|
711
668
|
}
|
|
712
|
-
const ext =
|
|
669
|
+
const ext = path2.extname(filePath).slice(1).toLowerCase();
|
|
713
670
|
if (extensionMap[ext]) {
|
|
714
671
|
return extensionMap[ext];
|
|
715
672
|
}
|
|
@@ -738,9 +695,9 @@ router2.get("/", (req, res) => {
|
|
|
738
695
|
res.status(400).json({ error: "Missing path parameter" });
|
|
739
696
|
return;
|
|
740
697
|
}
|
|
741
|
-
const filePath =
|
|
742
|
-
const resolvedPath =
|
|
743
|
-
const resolvedCwd =
|
|
698
|
+
const filePath = path2.join(cwd, relativePath);
|
|
699
|
+
const resolvedPath = path2.resolve(filePath);
|
|
700
|
+
const resolvedCwd = path2.resolve(cwd);
|
|
744
701
|
if (!resolvedPath.startsWith(resolvedCwd)) {
|
|
745
702
|
res.status(403).json({ error: "Access denied" });
|
|
746
703
|
return;
|
|
@@ -767,7 +724,7 @@ router2.get("/", (req, res) => {
|
|
|
767
724
|
import { Router as Router3 } from "express";
|
|
768
725
|
import { execSync } from "child_process";
|
|
769
726
|
import * as fs4 from "fs";
|
|
770
|
-
import * as
|
|
727
|
+
import * as path3 from "path";
|
|
771
728
|
var router3 = Router3();
|
|
772
729
|
function parseGitStatus(output) {
|
|
773
730
|
const files = [];
|
|
@@ -927,7 +884,7 @@ router3.get("/diff", (req, res) => {
|
|
|
927
884
|
res.status(400).json({ error: "Missing path parameter" });
|
|
928
885
|
return;
|
|
929
886
|
}
|
|
930
|
-
const fullPath =
|
|
887
|
+
const fullPath = path3.join(cwd, relativePath);
|
|
931
888
|
let fileStatus = "modified";
|
|
932
889
|
try {
|
|
933
890
|
const statusOutput = execSync(`git status --porcelain -- "${relativePath}"`, {
|
|
@@ -1003,10 +960,10 @@ function formatComments(comments) {
|
|
|
1003
960
|
// dist/flip/output/clipboard.js
|
|
1004
961
|
import { spawn } from "child_process";
|
|
1005
962
|
import fs5 from "fs";
|
|
1006
|
-
import
|
|
963
|
+
import path4 from "path";
|
|
1007
964
|
import os from "os";
|
|
1008
965
|
async function copyToClipboard(text) {
|
|
1009
|
-
const tmpFile =
|
|
966
|
+
const tmpFile = path4.join(os.tmpdir(), `flip-clipboard-${Date.now()}.txt`);
|
|
1010
967
|
const cleanupTmpFile = () => {
|
|
1011
968
|
try {
|
|
1012
969
|
fs5.unlinkSync(tmpFile);
|
|
@@ -1052,7 +1009,7 @@ async function copyToClipboard(text) {
|
|
|
1052
1009
|
// dist/flip/output/autopaste.js
|
|
1053
1010
|
import { spawn as spawn2 } from "child_process";
|
|
1054
1011
|
import fs6 from "fs";
|
|
1055
|
-
import
|
|
1012
|
+
import path5 from "path";
|
|
1056
1013
|
import os2 from "os";
|
|
1057
1014
|
async function schedulePaste(sessionId) {
|
|
1058
1015
|
if (process.platform !== "darwin") {
|
|
@@ -1062,7 +1019,7 @@ async function schedulePaste(sessionId) {
|
|
|
1062
1019
|
await pasteToOriginalSession(sessionId);
|
|
1063
1020
|
}
|
|
1064
1021
|
async function pasteToOriginalSession(sessionId) {
|
|
1065
|
-
const sessionFile =
|
|
1022
|
+
const sessionFile = path5.join(os2.tmpdir(), `flip-view-session-${sessionId}`);
|
|
1066
1023
|
let itermSessionId;
|
|
1067
1024
|
try {
|
|
1068
1025
|
itermSessionId = fs6.readFileSync(sessionFile, "utf-8").trim();
|
|
@@ -1109,7 +1066,7 @@ async function pasteToOriginalSession(sessionId) {
|
|
|
1109
1066
|
`;
|
|
1110
1067
|
}
|
|
1111
1068
|
try {
|
|
1112
|
-
const tmpScript =
|
|
1069
|
+
const tmpScript = path5.join(os2.tmpdir(), `flip-paste-${Date.now()}.scpt`);
|
|
1113
1070
|
fs6.writeFileSync(tmpScript, script);
|
|
1114
1071
|
const cleanupScript = () => {
|
|
1115
1072
|
try {
|
|
@@ -1188,21 +1145,21 @@ router5.post("/", async (req, res) => {
|
|
|
1188
1145
|
// dist/flip/routes/static.js
|
|
1189
1146
|
import { Router as Router6 } from "express";
|
|
1190
1147
|
import express from "express";
|
|
1191
|
-
import
|
|
1148
|
+
import path6 from "path";
|
|
1192
1149
|
import { fileURLToPath } from "url";
|
|
1193
1150
|
import fs7 from "fs";
|
|
1194
1151
|
function createStaticRouter() {
|
|
1195
1152
|
const router6 = Router6();
|
|
1196
1153
|
const __filename = fileURLToPath(import.meta.url);
|
|
1197
|
-
const __dirname =
|
|
1198
|
-
let distPath =
|
|
1154
|
+
const __dirname = path6.dirname(__filename);
|
|
1155
|
+
let distPath = path6.resolve(__dirname, "flip-ui/dist");
|
|
1199
1156
|
if (!fs7.existsSync(distPath)) {
|
|
1200
|
-
distPath =
|
|
1157
|
+
distPath = path6.resolve(__dirname, "../../../flip-ui/dist");
|
|
1201
1158
|
}
|
|
1202
1159
|
if (fs7.existsSync(distPath)) {
|
|
1203
1160
|
router6.use(express.static(distPath));
|
|
1204
1161
|
router6.get("*", (req, res) => {
|
|
1205
|
-
const indexPath =
|
|
1162
|
+
const indexPath = path6.join(distPath, "index.html");
|
|
1206
1163
|
if (fs7.existsSync(indexPath)) {
|
|
1207
1164
|
res.sendFile(indexPath);
|
|
1208
1165
|
} else {
|
|
@@ -1310,7 +1267,7 @@ function createChangesRouter(sessionManager) {
|
|
|
1310
1267
|
|
|
1311
1268
|
// dist/flip/watcher/FileWatcher.js
|
|
1312
1269
|
import chokidar from "chokidar";
|
|
1313
|
-
import * as
|
|
1270
|
+
import * as path7 from "path";
|
|
1314
1271
|
var FileWatcher = class {
|
|
1315
1272
|
watcher = null;
|
|
1316
1273
|
cwd;
|
|
@@ -1327,8 +1284,8 @@ var FileWatcher = class {
|
|
|
1327
1284
|
}
|
|
1328
1285
|
start() {
|
|
1329
1286
|
const shouldIgnore = (filePath) => {
|
|
1330
|
-
const relativePath =
|
|
1331
|
-
const segments = relativePath.split(
|
|
1287
|
+
const relativePath = path7.relative(this.cwd, filePath);
|
|
1288
|
+
const segments = relativePath.split(path7.sep);
|
|
1332
1289
|
return segments.some((segment) => segment !== ".git" && FILTERED_PATTERNS.has(segment));
|
|
1333
1290
|
};
|
|
1334
1291
|
this.watcher = chokidar.watch(this.cwd, {
|
|
@@ -1376,7 +1333,7 @@ var FileWatcher = class {
|
|
|
1376
1333
|
this.gitChangedCallbacks.push(callback);
|
|
1377
1334
|
}
|
|
1378
1335
|
handleFileEvent(filePath, event) {
|
|
1379
|
-
const relativePath =
|
|
1336
|
+
const relativePath = path7.relative(this.cwd, filePath);
|
|
1380
1337
|
if (relativePath.startsWith(".git")) {
|
|
1381
1338
|
this.emitGitChanged();
|
|
1382
1339
|
return;
|
|
@@ -1390,7 +1347,7 @@ var FileWatcher = class {
|
|
|
1390
1347
|
this.emitGitChanged();
|
|
1391
1348
|
}
|
|
1392
1349
|
handleDirEvent(dirPath, event) {
|
|
1393
|
-
const relativePath =
|
|
1350
|
+
const relativePath = path7.relative(this.cwd, dirPath);
|
|
1394
1351
|
if (relativePath.startsWith(".git")) {
|
|
1395
1352
|
return;
|
|
1396
1353
|
}
|
|
@@ -1750,7 +1707,7 @@ async function findExistingServer(startPort, endPort) {
|
|
|
1750
1707
|
|
|
1751
1708
|
// dist/flip/index.js
|
|
1752
1709
|
import open from "open";
|
|
1753
|
-
import
|
|
1710
|
+
import path8 from "path";
|
|
1754
1711
|
import fs8 from "fs";
|
|
1755
1712
|
import os3 from "os";
|
|
1756
1713
|
import http2 from "http";
|
|
@@ -1849,7 +1806,7 @@ async function runFlip(args) {
|
|
|
1849
1806
|
} else {
|
|
1850
1807
|
command = "oneshot";
|
|
1851
1808
|
}
|
|
1852
|
-
const cwd = pathArg ?
|
|
1809
|
+
const cwd = pathArg ? path8.resolve(pathArg) : process.cwd();
|
|
1853
1810
|
const finalSessionId = sessionId || getSessionId();
|
|
1854
1811
|
switch (command) {
|
|
1855
1812
|
case "setup": {
|
|
@@ -1963,7 +1920,7 @@ async function setupHotkey() {
|
|
|
1963
1920
|
} catch {
|
|
1964
1921
|
csqPath = new URL(import.meta.url).pathname;
|
|
1965
1922
|
}
|
|
1966
|
-
const nodeDir =
|
|
1923
|
+
const nodeDir = path8.dirname(nodePath);
|
|
1967
1924
|
const wrapperScript = `#!/bin/bash
|
|
1968
1925
|
# Add node to PATH (coprocess doesn't inherit shell PATH)
|
|
1969
1926
|
export PATH="${nodeDir}:$PATH"
|
|
@@ -1989,8 +1946,8 @@ else
|
|
|
1989
1946
|
exec ${csqPath} flip
|
|
1990
1947
|
fi
|
|
1991
1948
|
`;
|
|
1992
|
-
const scriptDir =
|
|
1993
|
-
const scriptPath =
|
|
1949
|
+
const scriptDir = path8.join(os3.homedir(), ".config", "csq");
|
|
1950
|
+
const scriptPath = path8.join(scriptDir, "flip-hotkey.sh");
|
|
1994
1951
|
if (!fs8.existsSync(scriptDir)) {
|
|
1995
1952
|
fs8.mkdirSync(scriptDir, { recursive: true });
|
|
1996
1953
|
}
|
|
@@ -2016,1479 +1973,54 @@ fi
|
|
|
2016
1973
|
console.log("");
|
|
2017
1974
|
}
|
|
2018
1975
|
|
|
2019
|
-
// dist/
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
*/
|
|
2033
|
-
async isTmuxAvailable() {
|
|
2034
|
-
try {
|
|
2035
|
-
await exec2("which tmux", execOptions2);
|
|
2036
|
-
return true;
|
|
2037
|
-
} catch {
|
|
2038
|
-
return false;
|
|
2039
|
-
}
|
|
1976
|
+
// dist/index.js
|
|
1977
|
+
init_config();
|
|
1978
|
+
init_fileUtils();
|
|
1979
|
+
process.on("SIGINT", () => {
|
|
1980
|
+
process.exit(130);
|
|
1981
|
+
});
|
|
1982
|
+
var gitAdapter = new GitAdapter();
|
|
1983
|
+
async function main() {
|
|
1984
|
+
const args = process.argv.slice(2);
|
|
1985
|
+
const command = args[0];
|
|
1986
|
+
if (command === "--init" || command === "init") {
|
|
1987
|
+
printShellInit();
|
|
1988
|
+
return;
|
|
2040
1989
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
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
|
-
|
|
2550
|
-
// dist/config.js
|
|
2551
|
-
import * as fs9 from "fs";
|
|
2552
|
-
import * as os4 from "os";
|
|
2553
|
-
import * as path10 from "path";
|
|
2554
|
-
var GLOBAL_CONFIG_PATH = path10.join(os4.homedir(), ".code-squad", "config.json");
|
|
2555
|
-
async function loadGlobalConfig() {
|
|
2556
|
-
try {
|
|
2557
|
-
const content = await fs9.promises.readFile(GLOBAL_CONFIG_PATH, "utf-8");
|
|
2558
|
-
return JSON.parse(content);
|
|
2559
|
-
} catch (error) {
|
|
2560
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2561
|
-
return {};
|
|
2562
|
-
}
|
|
2563
|
-
console.warn(`[Code Squad] Warning: Could not load global config at ${GLOBAL_CONFIG_PATH}.`, error);
|
|
2564
|
-
return {};
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
async function loadConfig(workspaceRoot) {
|
|
2568
|
-
const globalConfig = await loadGlobalConfig();
|
|
2569
|
-
const normalizedPath = path10.resolve(workspaceRoot);
|
|
2570
|
-
const projectConfig = globalConfig.projects?.[normalizedPath] ?? {};
|
|
2571
|
-
const defaults = globalConfig.defaults ?? {};
|
|
2572
|
-
return {
|
|
2573
|
-
...defaults,
|
|
2574
|
-
...projectConfig,
|
|
2575
|
-
worktreeCopyPatterns: [
|
|
2576
|
-
.../* @__PURE__ */ new Set([
|
|
2577
|
-
...defaults.worktreeCopyPatterns ?? [],
|
|
2578
|
-
...projectConfig.worktreeCopyPatterns ?? []
|
|
2579
|
-
])
|
|
2580
|
-
]
|
|
2581
|
-
};
|
|
2582
|
-
}
|
|
2583
|
-
function getWorktreeCopyPatterns(config) {
|
|
2584
|
-
return config.worktreeCopyPatterns ?? [];
|
|
2585
|
-
}
|
|
2586
|
-
|
|
2587
|
-
// dist/fileUtils.js
|
|
2588
|
-
import * as fs10 from "fs";
|
|
2589
|
-
import * as path11 from "path";
|
|
2590
|
-
import fg from "fast-glob";
|
|
2591
|
-
async function copyFilesWithPatterns(sourceRoot, destRoot, patterns) {
|
|
2592
|
-
const copied = [];
|
|
2593
|
-
const failed = [];
|
|
2594
|
-
if (patterns.length === 0) {
|
|
2595
|
-
return { copied, failed };
|
|
2596
|
-
}
|
|
2597
|
-
for (const pattern of patterns) {
|
|
2598
|
-
try {
|
|
2599
|
-
const files = await fg(pattern, {
|
|
2600
|
-
cwd: sourceRoot,
|
|
2601
|
-
absolute: true,
|
|
2602
|
-
onlyFiles: true,
|
|
2603
|
-
dot: true
|
|
2604
|
-
// .env 같은 dotfile도 매칭
|
|
2605
|
-
});
|
|
2606
|
-
for (const absolutePath of files) {
|
|
2607
|
-
try {
|
|
2608
|
-
await copySingleFile(absolutePath, sourceRoot, destRoot);
|
|
2609
|
-
const relativePath = path11.relative(sourceRoot, absolutePath);
|
|
2610
|
-
copied.push(relativePath);
|
|
2611
|
-
} catch {
|
|
2612
|
-
const relativePath = path11.relative(sourceRoot, absolutePath);
|
|
2613
|
-
failed.push(relativePath);
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
} catch {
|
|
2617
|
-
}
|
|
2618
|
-
}
|
|
2619
|
-
return { copied, failed };
|
|
2620
|
-
}
|
|
2621
|
-
async function copySingleFile(absolutePath, sourceRoot, destRoot) {
|
|
2622
|
-
const relativePath = path11.relative(sourceRoot, absolutePath);
|
|
2623
|
-
const destPath = path11.join(destRoot, relativePath);
|
|
2624
|
-
const destDir = path11.dirname(destPath);
|
|
2625
|
-
await fs10.promises.mkdir(destDir, { recursive: true });
|
|
2626
|
-
await fs10.promises.copyFile(absolutePath, destPath);
|
|
2627
|
-
}
|
|
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
|
-
|
|
3379
|
-
// dist/index.js
|
|
3380
|
-
process.on("SIGINT", () => {
|
|
3381
|
-
process.exit(130);
|
|
3382
|
-
});
|
|
3383
|
-
var gitAdapter5 = new GitAdapter();
|
|
3384
|
-
async function main() {
|
|
3385
|
-
const args = process.argv.slice(2);
|
|
3386
|
-
const persistentMode = args.includes("-p") || args.includes("--persistent");
|
|
3387
|
-
const filteredArgs = args.filter((a) => a !== "-p" && a !== "--persistent");
|
|
3388
|
-
const command = filteredArgs[0];
|
|
3389
|
-
if (command === "--init" || command === "init") {
|
|
3390
|
-
printShellInit();
|
|
3391
|
-
return;
|
|
3392
|
-
}
|
|
3393
|
-
if (command === "flip") {
|
|
3394
|
-
await runFlip(filteredArgs.slice(1));
|
|
3395
|
-
return;
|
|
1990
|
+
if (command === "flip") {
|
|
1991
|
+
await runFlip(args.slice(1));
|
|
1992
|
+
return;
|
|
3396
1993
|
}
|
|
3397
1994
|
const workspaceRoot = await findGitRoot(process.cwd());
|
|
3398
1995
|
if (!workspaceRoot) {
|
|
3399
|
-
console.error(
|
|
1996
|
+
console.error(chalk.red("Error: Not a git repository"));
|
|
3400
1997
|
process.exit(1);
|
|
3401
1998
|
}
|
|
3402
1999
|
switch (command) {
|
|
3403
|
-
case "list":
|
|
3404
|
-
await listThreads(workspaceRoot);
|
|
3405
|
-
break;
|
|
3406
2000
|
case "new":
|
|
3407
|
-
await createWorktreeCommand(workspaceRoot,
|
|
2001
|
+
await createWorktreeCommand(workspaceRoot, args.slice(1));
|
|
3408
2002
|
break;
|
|
3409
2003
|
case "quit":
|
|
3410
2004
|
await quitWorktreeCommand();
|
|
3411
2005
|
break;
|
|
3412
|
-
case "
|
|
3413
|
-
await
|
|
2006
|
+
case "list":
|
|
2007
|
+
await listWorktrees(workspaceRoot);
|
|
3414
2008
|
break;
|
|
3415
|
-
|
|
3416
|
-
if (
|
|
3417
|
-
await
|
|
2009
|
+
default:
|
|
2010
|
+
if (process.stdin.isTTY) {
|
|
2011
|
+
const { runTui: runTui2 } = await Promise.resolve().then(() => (init_App(), App_exports));
|
|
2012
|
+
await runTui2(workspaceRoot);
|
|
3418
2013
|
} else {
|
|
3419
|
-
await
|
|
2014
|
+
await listWorktrees(workspaceRoot);
|
|
3420
2015
|
}
|
|
3421
|
-
break;
|
|
3422
|
-
default:
|
|
3423
|
-
await runDash(workspaceRoot);
|
|
3424
2016
|
}
|
|
3425
2017
|
}
|
|
3426
2018
|
async function findGitRoot(cwd) {
|
|
3427
|
-
if (!await
|
|
2019
|
+
if (!await gitAdapter.isGitRepository(cwd)) {
|
|
3428
2020
|
return null;
|
|
3429
2021
|
}
|
|
3430
2022
|
return cwd;
|
|
3431
2023
|
}
|
|
3432
|
-
function getProjectHash(workspaceRoot) {
|
|
3433
|
-
return crypto.createHash("sha256").update(workspaceRoot).digest("hex").slice(0, 8);
|
|
3434
|
-
}
|
|
3435
|
-
function getSessionsPath(workspaceRoot) {
|
|
3436
|
-
const projectHash = getProjectHash(workspaceRoot);
|
|
3437
|
-
const projectName = path15.basename(workspaceRoot);
|
|
3438
|
-
return path15.join(os7.homedir(), ".code-squad", "sessions", `${projectName}-${projectHash}.json`);
|
|
3439
|
-
}
|
|
3440
|
-
async function loadLocalThreads(workspaceRoot) {
|
|
3441
|
-
const sessionsPath = getSessionsPath(workspaceRoot);
|
|
3442
|
-
try {
|
|
3443
|
-
const content = await fs13.promises.readFile(sessionsPath, "utf-8");
|
|
3444
|
-
const data = JSON.parse(content);
|
|
3445
|
-
return data.localThreads || [];
|
|
3446
|
-
} catch {
|
|
3447
|
-
return [];
|
|
3448
|
-
}
|
|
3449
|
-
}
|
|
3450
|
-
async function saveLocalThreads(workspaceRoot, threads) {
|
|
3451
|
-
const sessionsPath = getSessionsPath(workspaceRoot);
|
|
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));
|
|
3455
|
-
}
|
|
3456
|
-
async function addLocalThread(workspaceRoot, name) {
|
|
3457
|
-
const threads = await loadLocalThreads(workspaceRoot);
|
|
3458
|
-
const newThread = {
|
|
3459
|
-
id: Date.now().toString(),
|
|
3460
|
-
name,
|
|
3461
|
-
path: workspaceRoot,
|
|
3462
|
-
createdAt: Date.now()
|
|
3463
|
-
};
|
|
3464
|
-
threads.push(newThread);
|
|
3465
|
-
await saveLocalThreads(workspaceRoot, threads);
|
|
3466
|
-
return newThread;
|
|
3467
|
-
}
|
|
3468
|
-
async function removeLocalThread(workspaceRoot, id) {
|
|
3469
|
-
const threads = await loadLocalThreads(workspaceRoot);
|
|
3470
|
-
const filtered = threads.filter((t) => t.id !== id);
|
|
3471
|
-
await saveLocalThreads(workspaceRoot, filtered);
|
|
3472
|
-
}
|
|
3473
|
-
async function getAllThreads(workspaceRoot) {
|
|
3474
|
-
const worktrees = await gitAdapter5.listWorktrees(workspaceRoot);
|
|
3475
|
-
const localThreads = await loadLocalThreads(workspaceRoot);
|
|
3476
|
-
const threads = [
|
|
3477
|
-
...worktrees.map((wt) => ({
|
|
3478
|
-
type: "worktree",
|
|
3479
|
-
name: wt.branch,
|
|
3480
|
-
path: wt.path,
|
|
3481
|
-
branch: wt.branch
|
|
3482
|
-
})),
|
|
3483
|
-
...localThreads.map((lt) => ({
|
|
3484
|
-
type: "local",
|
|
3485
|
-
name: lt.name,
|
|
3486
|
-
path: lt.path,
|
|
3487
|
-
id: lt.id
|
|
3488
|
-
}))
|
|
3489
|
-
];
|
|
3490
|
-
return threads;
|
|
3491
|
-
}
|
|
3492
2024
|
function printShellInit() {
|
|
3493
2025
|
const script = `
|
|
3494
2026
|
csq() {
|
|
@@ -3498,7 +2030,7 @@ csq() {
|
|
|
3498
2030
|
fi
|
|
3499
2031
|
|
|
3500
2032
|
local output
|
|
3501
|
-
output=$(command csq "$@"
|
|
2033
|
+
output=$(command csq "$@")
|
|
3502
2034
|
local exit_code=$?
|
|
3503
2035
|
|
|
3504
2036
|
if [[ $exit_code -ne 0 ]]; then
|
|
@@ -3519,92 +2051,65 @@ csq() {
|
|
|
3519
2051
|
`.trim();
|
|
3520
2052
|
console.log(script);
|
|
3521
2053
|
}
|
|
3522
|
-
async function
|
|
3523
|
-
const
|
|
3524
|
-
if (
|
|
3525
|
-
console.log(
|
|
2054
|
+
async function listWorktrees(workspaceRoot) {
|
|
2055
|
+
const worktrees = await gitAdapter.listWorktrees(workspaceRoot);
|
|
2056
|
+
if (worktrees.length === 0) {
|
|
2057
|
+
console.log(chalk.dim("No worktrees found."));
|
|
3526
2058
|
return;
|
|
3527
2059
|
}
|
|
3528
|
-
for (const
|
|
3529
|
-
|
|
3530
|
-
console.log(`${typeLabel} ${t.name.padEnd(20)} ${chalk3.dim(t.path)}`);
|
|
3531
|
-
}
|
|
3532
|
-
}
|
|
3533
|
-
function parseNewArgs(args) {
|
|
3534
|
-
let name;
|
|
3535
|
-
let split = false;
|
|
3536
|
-
for (const arg of args) {
|
|
3537
|
-
if (arg === "-s" || arg === "--split") {
|
|
3538
|
-
split = true;
|
|
3539
|
-
} else if (!arg.startsWith("-") && !name) {
|
|
3540
|
-
name = arg;
|
|
3541
|
-
}
|
|
2060
|
+
for (const wt of worktrees) {
|
|
2061
|
+
console.log(`${chalk.cyan("[W]")} ${wt.branch.padEnd(20)} ${chalk.dim(wt.path)}`);
|
|
3542
2062
|
}
|
|
3543
|
-
return { name, split };
|
|
3544
2063
|
}
|
|
3545
2064
|
async function createWorktreeCommand(workspaceRoot, args) {
|
|
3546
|
-
const
|
|
2065
|
+
const name = args.find((a) => !a.startsWith("-"));
|
|
3547
2066
|
if (!name) {
|
|
3548
|
-
console.error(
|
|
3549
|
-
console.error(
|
|
2067
|
+
console.error(chalk.red("Error: Name is required"));
|
|
2068
|
+
console.error(chalk.dim("Usage: csq new <name>"));
|
|
3550
2069
|
process.exit(1);
|
|
3551
2070
|
}
|
|
3552
|
-
const repoName =
|
|
3553
|
-
const defaultBasePath =
|
|
3554
|
-
const worktreePath =
|
|
2071
|
+
const repoName = path12.basename(workspaceRoot);
|
|
2072
|
+
const defaultBasePath = path12.join(path12.dirname(workspaceRoot), `${repoName}.worktree`);
|
|
2073
|
+
const worktreePath = path12.join(defaultBasePath, name);
|
|
3555
2074
|
try {
|
|
3556
|
-
await
|
|
3557
|
-
console.log(
|
|
2075
|
+
await gitAdapter.createWorktree(worktreePath, name, workspaceRoot);
|
|
2076
|
+
console.log(chalk.green(`\u2713 Created worktree: ${name}`));
|
|
3558
2077
|
await copyWorktreeFiles(workspaceRoot, worktreePath);
|
|
3559
|
-
|
|
3560
|
-
await openNewTerminal(worktreePath);
|
|
3561
|
-
} else if (process.platform === "darwin") {
|
|
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
|
-
}
|
|
3567
|
-
} else {
|
|
3568
|
-
console.log(worktreePath);
|
|
3569
|
-
}
|
|
2078
|
+
console.log(worktreePath);
|
|
3570
2079
|
} catch (error) {
|
|
3571
|
-
console.error(
|
|
2080
|
+
console.error(chalk.red(`Failed to create worktree: ${error.message}`));
|
|
3572
2081
|
process.exit(1);
|
|
3573
2082
|
}
|
|
3574
2083
|
}
|
|
3575
2084
|
async function quitWorktreeCommand() {
|
|
3576
2085
|
const cwd = process.cwd();
|
|
3577
|
-
const context = await
|
|
2086
|
+
const context = await gitAdapter.getWorktreeContext(cwd);
|
|
3578
2087
|
if (!context.isWorktree) {
|
|
3579
|
-
console.error(
|
|
2088
|
+
console.error(chalk.red("Error: Not in a worktree"));
|
|
3580
2089
|
process.exit(1);
|
|
3581
2090
|
}
|
|
3582
2091
|
if (!context.mainRoot || !context.branch) {
|
|
3583
|
-
console.error(
|
|
2092
|
+
console.error(chalk.red("Error: Could not determine worktree context"));
|
|
3584
2093
|
process.exit(1);
|
|
3585
2094
|
}
|
|
3586
|
-
const isDirty = await
|
|
2095
|
+
const isDirty = await gitAdapter.hasDirtyState(cwd);
|
|
3587
2096
|
if (isDirty) {
|
|
3588
|
-
const confirmed = await
|
|
2097
|
+
const confirmed = await confirm({
|
|
3589
2098
|
message: "Uncommitted changes detected. Delete anyway?",
|
|
3590
2099
|
default: false
|
|
3591
2100
|
});
|
|
3592
2101
|
if (!confirmed) {
|
|
3593
|
-
console.log(
|
|
2102
|
+
console.log(chalk.dim("Cancelled."));
|
|
3594
2103
|
process.exit(0);
|
|
3595
2104
|
}
|
|
3596
2105
|
}
|
|
3597
2106
|
try {
|
|
3598
|
-
await
|
|
3599
|
-
await
|
|
3600
|
-
console.log(
|
|
3601
|
-
|
|
3602
|
-
await cdInCurrentTerminal(context.mainRoot);
|
|
3603
|
-
} else {
|
|
3604
|
-
console.log(context.mainRoot);
|
|
3605
|
-
}
|
|
2107
|
+
await gitAdapter.removeWorktree(context.currentPath, context.mainRoot, true);
|
|
2108
|
+
await gitAdapter.deleteBranch(context.branch, context.mainRoot, true);
|
|
2109
|
+
console.log(chalk.green(`\u2713 Deleted worktree and branch: ${context.branch}`));
|
|
2110
|
+
console.log(context.mainRoot);
|
|
3606
2111
|
} catch (error) {
|
|
3607
|
-
console.error(
|
|
2112
|
+
console.error(chalk.red(`Failed to quit: ${error.message}`));
|
|
3608
2113
|
process.exit(1);
|
|
3609
2114
|
}
|
|
3610
2115
|
}
|
|
@@ -3616,224 +2121,16 @@ async function copyWorktreeFiles(sourceRoot, destRoot) {
|
|
|
3616
2121
|
}
|
|
3617
2122
|
const { copied, failed } = await copyFilesWithPatterns(sourceRoot, destRoot, patterns);
|
|
3618
2123
|
if (copied.length > 0) {
|
|
3619
|
-
console.log(
|
|
2124
|
+
console.log(chalk.green(`\u2713 Copied ${copied.length} file(s) to worktree`));
|
|
3620
2125
|
}
|
|
3621
2126
|
if (failed.length > 0) {
|
|
3622
|
-
console.log(
|
|
3623
|
-
}
|
|
3624
|
-
}
|
|
3625
|
-
async function cdInCurrentTerminal(targetPath) {
|
|
3626
|
-
const { exec: exec3 } = await import("child_process");
|
|
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
|
-
}
|
|
3667
|
-
const hasIterm = await new Promise((resolve2) => {
|
|
3668
|
-
exec3('mdfind "kMDItemCFBundleIdentifier == com.googlecode.iterm2"', (error, stdout) => {
|
|
3669
|
-
resolve2(!error && stdout.trim().length > 0);
|
|
3670
|
-
});
|
|
3671
|
-
});
|
|
3672
|
-
if (hasIterm) {
|
|
3673
|
-
const script = `
|
|
3674
|
-
tell application "iTerm2"
|
|
3675
|
-
tell current session of current window
|
|
3676
|
-
write text "cd '${escapedPath}'"
|
|
3677
|
-
end tell
|
|
3678
|
-
end tell`;
|
|
3679
|
-
return new Promise((resolve2) => {
|
|
3680
|
-
exec3(`osascript -e '${script}'`, (error) => {
|
|
3681
|
-
if (error) {
|
|
3682
|
-
console.log(targetPath);
|
|
3683
|
-
}
|
|
3684
|
-
resolve2(true);
|
|
3685
|
-
});
|
|
3686
|
-
});
|
|
3687
|
-
}
|
|
3688
|
-
console.log(targetPath);
|
|
3689
|
-
return true;
|
|
3690
|
-
}
|
|
3691
|
-
async function openNewTerminal(targetPath) {
|
|
3692
|
-
const { exec: exec3 } = await import("child_process");
|
|
3693
|
-
const escapedPath = targetPath.replace(/'/g, "'\\''");
|
|
3694
|
-
const hasIterm = await new Promise((resolve2) => {
|
|
3695
|
-
exec3('mdfind "kMDItemCFBundleIdentifier == com.googlecode.iterm2"', (error, stdout) => {
|
|
3696
|
-
resolve2(!error && stdout.trim().length > 0);
|
|
3697
|
-
});
|
|
3698
|
-
});
|
|
3699
|
-
if (hasIterm) {
|
|
3700
|
-
const script = `
|
|
3701
|
-
tell application "iTerm2"
|
|
3702
|
-
tell current session of current window
|
|
3703
|
-
set newSession to (split vertically with default profile)
|
|
3704
|
-
tell newSession
|
|
3705
|
-
write text "cd '${escapedPath}'"
|
|
3706
|
-
end tell
|
|
3707
|
-
end tell
|
|
3708
|
-
end tell`;
|
|
3709
|
-
return new Promise((resolve2) => {
|
|
3710
|
-
exec3(`osascript -e '${script}'`, (error) => {
|
|
3711
|
-
resolve2(!error);
|
|
3712
|
-
});
|
|
3713
|
-
});
|
|
3714
|
-
}
|
|
3715
|
-
const terminalScript = `
|
|
3716
|
-
tell application "Terminal"
|
|
3717
|
-
activate
|
|
3718
|
-
do script "cd '${escapedPath}'"
|
|
3719
|
-
end tell`;
|
|
3720
|
-
return new Promise((resolve2) => {
|
|
3721
|
-
exec3(`osascript -e '${terminalScript}'`, (error) => {
|
|
3722
|
-
resolve2(!error);
|
|
3723
|
-
});
|
|
3724
|
-
});
|
|
3725
|
-
}
|
|
3726
|
-
async function interactiveMode(workspaceRoot) {
|
|
3727
|
-
const result = await runInteraction(workspaceRoot);
|
|
3728
|
-
if (result?.cdPath) {
|
|
3729
|
-
await cdInCurrentTerminal(result.cdPath);
|
|
3730
|
-
}
|
|
3731
|
-
}
|
|
3732
|
-
async function persistentInteractiveMode(workspaceRoot) {
|
|
3733
|
-
while (true) {
|
|
3734
|
-
console.clear();
|
|
3735
|
-
const result = await runInteraction(workspaceRoot, true);
|
|
3736
|
-
if (result?.exit) {
|
|
3737
|
-
break;
|
|
3738
|
-
}
|
|
3739
|
-
if (result?.cdPath) {
|
|
3740
|
-
await openNewTerminal(result.cdPath);
|
|
3741
|
-
}
|
|
3742
|
-
}
|
|
3743
|
-
}
|
|
3744
|
-
async function executeDelete(thread, workspaceRoot) {
|
|
3745
|
-
if (thread.type === "local") {
|
|
3746
|
-
const confirmed = await confirmDeleteLocal(thread.name);
|
|
3747
|
-
if (confirmed) {
|
|
3748
|
-
await removeLocalThread(workspaceRoot, thread.id);
|
|
3749
|
-
console.log(chalk3.green(`\u2713 Deleted local thread: ${thread.name}`));
|
|
3750
|
-
} else {
|
|
3751
|
-
console.log(chalk3.dim("Cancelled."));
|
|
3752
|
-
}
|
|
3753
|
-
} else {
|
|
3754
|
-
const { confirmed, removeGitWorktree } = await confirmDeleteWorktree(thread.name);
|
|
3755
|
-
if (confirmed && removeGitWorktree) {
|
|
3756
|
-
try {
|
|
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}`));
|
|
3760
|
-
} catch (error) {
|
|
3761
|
-
console.error(chalk3.red(`Failed to delete: ${error.message}`));
|
|
3762
|
-
}
|
|
3763
|
-
} else if (confirmed) {
|
|
3764
|
-
console.log(chalk3.yellow("Worktree kept."));
|
|
3765
|
-
} else {
|
|
3766
|
-
console.log(chalk3.dim("Cancelled."));
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
|
-
}
|
|
3770
|
-
async function runInteraction(workspaceRoot, _persistent = false) {
|
|
3771
|
-
const threads = await getAllThreads(workspaceRoot);
|
|
3772
|
-
const repoName = path15.basename(workspaceRoot);
|
|
3773
|
-
const choice = await selectThread(threads, repoName);
|
|
3774
|
-
if (choice.type === "exit") {
|
|
3775
|
-
return { exit: true };
|
|
3776
|
-
}
|
|
3777
|
-
let targetPath;
|
|
3778
|
-
switch (choice.type) {
|
|
3779
|
-
case "existing": {
|
|
3780
|
-
const thread = choice.thread;
|
|
3781
|
-
const action = await selectThreadAction(thread.name);
|
|
3782
|
-
if (action === "open") {
|
|
3783
|
-
targetPath = thread.path;
|
|
3784
|
-
} else if (action === "delete") {
|
|
3785
|
-
await executeDelete(thread, workspaceRoot);
|
|
3786
|
-
return null;
|
|
3787
|
-
} else {
|
|
3788
|
-
return null;
|
|
3789
|
-
}
|
|
3790
|
-
break;
|
|
3791
|
-
}
|
|
3792
|
-
case "delete-selected": {
|
|
3793
|
-
const thread = choice.thread;
|
|
3794
|
-
await executeDelete(thread, workspaceRoot);
|
|
3795
|
-
return null;
|
|
3796
|
-
}
|
|
3797
|
-
case "new": {
|
|
3798
|
-
const newType = await selectNewThreadType();
|
|
3799
|
-
if (newType === "back") {
|
|
3800
|
-
return null;
|
|
3801
|
-
}
|
|
3802
|
-
if (newType === "worktree") {
|
|
3803
|
-
const defaultBasePath = path15.join(path15.dirname(workspaceRoot), `${repoName}.worktree`);
|
|
3804
|
-
const form = await newWorktreeForm(defaultBasePath);
|
|
3805
|
-
if (!form) {
|
|
3806
|
-
return null;
|
|
3807
|
-
}
|
|
3808
|
-
try {
|
|
3809
|
-
await gitAdapter5.createWorktree(form.path, form.name, workspaceRoot);
|
|
3810
|
-
console.log(chalk3.green(`\u2713 Created worktree: ${form.name}`));
|
|
3811
|
-
await copyWorktreeFiles(workspaceRoot, form.path);
|
|
3812
|
-
targetPath = form.path;
|
|
3813
|
-
} catch (error) {
|
|
3814
|
-
console.error(chalk3.red(`Failed to create worktree: ${error.message}`));
|
|
3815
|
-
}
|
|
3816
|
-
} else {
|
|
3817
|
-
const name = await newLocalForm();
|
|
3818
|
-
if (!name) {
|
|
3819
|
-
return null;
|
|
3820
|
-
}
|
|
3821
|
-
await addLocalThread(workspaceRoot, name);
|
|
3822
|
-
console.log(chalk3.green(`\u2713 Created local thread: ${name}`));
|
|
3823
|
-
targetPath = workspaceRoot;
|
|
3824
|
-
}
|
|
3825
|
-
break;
|
|
3826
|
-
}
|
|
2127
|
+
console.log(chalk.yellow(`\u26A0 Failed to copy ${failed.length} file(s)`));
|
|
3827
2128
|
}
|
|
3828
|
-
if (targetPath) {
|
|
3829
|
-
return { cdPath: targetPath };
|
|
3830
|
-
}
|
|
3831
|
-
return null;
|
|
3832
2129
|
}
|
|
3833
2130
|
main().catch((error) => {
|
|
3834
2131
|
if (error.message?.includes("SIGINT") || error.message?.includes("force closed")) {
|
|
3835
2132
|
process.exit(130);
|
|
3836
2133
|
}
|
|
3837
|
-
console.error(
|
|
2134
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
3838
2135
|
process.exit(1);
|
|
3839
2136
|
});
|