code-squad-cli 1.3.0 → 2.0.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 CHANGED
@@ -1,11 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // dist/index.js
4
- import * as path15 from "path";
5
- import * as fs13 from "fs";
6
- import * as os7 from "os";
7
- import * as crypto from "crypto";
8
- import chalk3 from "chalk";
4
+ import * as path11 from "path";
5
+ import chalk from "chalk";
9
6
 
10
7
  // dist/adapters/GitAdapter.js
11
8
  import { exec as execCallback } from "child_process";
@@ -53,11 +50,11 @@ var GitAdapter = class {
53
50
  const headMatch = headLine.match(/^HEAD (.+)$/);
54
51
  const branchMatch = branchLine?.match(/^branch refs\/heads\/(.+)$/);
55
52
  if (pathMatch && headMatch) {
56
- const path16 = pathMatch[1];
53
+ const path12 = pathMatch[1];
57
54
  const head = headMatch[1];
58
55
  const branch = branchMatch ? branchMatch[1] : "HEAD";
59
- if (path16 !== workspaceRoot) {
60
- worktrees.push({ path: path16, branch, head });
56
+ if (path12 !== workspaceRoot) {
57
+ worktrees.push({ path: path12, branch, head });
61
58
  }
62
59
  }
63
60
  i += 3;
@@ -90,19 +87,19 @@ var GitAdapter = class {
90
87
  throw new Error(`Failed to delete branch: ${error.message}`);
91
88
  }
92
89
  }
93
- async isValidWorktree(path16, workspaceRoot) {
90
+ async isValidWorktree(path12, workspaceRoot) {
94
91
  try {
95
- await fs.promises.access(path16, fs.constants.R_OK);
92
+ await fs.promises.access(path12, fs.constants.R_OK);
96
93
  } catch {
97
94
  return false;
98
95
  }
99
96
  try {
100
- await exec(`cd "${path16}" && git rev-parse --git-dir`, execOptions);
97
+ await exec(`cd "${path12}" && git rev-parse --git-dir`, execOptions);
101
98
  } catch {
102
99
  return false;
103
100
  }
104
101
  const worktrees = await this.listWorktrees(workspaceRoot);
105
- return worktrees.some((wt) => wt.path === path16);
102
+ return worktrees.some((wt) => wt.path === path12);
106
103
  }
107
104
  async getWorktreeBranch(worktreePath) {
108
105
  try {
@@ -152,277 +149,8 @@ var GitAdapter = class {
152
149
  }
153
150
  };
154
151
 
155
- // dist/ui/prompts.js
156
- import { createPrompt, useState, useKeypress, usePrefix, isEnterKey, isBackspaceKey } from "@inquirer/core";
157
- import { confirm } from "@inquirer/prompts";
158
- import chalk from "chalk";
159
- import * as path from "path";
160
- var cancelableInput = createPrompt((config, done) => {
161
- const [value, setValue] = useState(config.default || "");
162
- const [error, setError] = useState(null);
163
- const prefix = usePrefix({ status: "idle" });
164
- useKeypress((key, rl) => {
165
- if (key.name === "escape") {
166
- done(null);
167
- } else if (isEnterKey(key)) {
168
- if (config.validate) {
169
- const result = config.validate(value);
170
- if (result !== true) {
171
- setError(typeof result === "string" ? result : "Invalid input");
172
- return;
173
- }
174
- }
175
- done(value);
176
- } else if (isBackspaceKey(key)) {
177
- setValue(value.slice(0, -1));
178
- setError(null);
179
- } else if (key.ctrl || key.name === "tab" || key.name === "up" || key.name === "down") {
180
- } else {
181
- const seq = key.sequence;
182
- if (seq && seq.length === 1 && seq >= " ") {
183
- setValue(value + seq);
184
- setError(null);
185
- }
186
- }
187
- });
188
- const errorMsg = error ? chalk.red(`
189
- ${error}`) : "";
190
- return `${prefix} ${config.message} ${chalk.cyan(value)}${errorMsg}
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);
224
- }
225
- } else {
226
- const shortcut = shortcuts.find((s) => s.key === key.name);
227
- if (shortcut) {
228
- done(shortcut.value);
229
- }
230
- }
231
- });
232
- const totalItems = enabledChoices.length;
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
- }
304
- }
305
- });
306
- const totalItems = allChoices.length;
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;
316
- }
317
- }
318
- const visibleChoices = allChoices.slice(startIndex, startIndex + pageSize);
319
- const lines = visibleChoices.map((choice, i) => {
320
- const actualIndex = startIndex + i;
321
- const isSelected = actualIndex === selectedIndex;
322
- const cursor = isSelected ? chalk.cyan("\u276F") : " ";
323
- const name = isSelected ? chalk.cyan(choice.name) : choice.name;
324
- return `${cursor} ${name}`;
325
- });
326
- if (startIndex > 0) {
327
- lines.unshift(chalk.dim(" \u2191 more"));
328
- }
329
- if (startIndex + pageSize < totalItems) {
330
- lines.push(chalk.dim(" \u2193 more"));
331
- }
332
- const hint = chalk.dim("j/k:nav n:new d:delete q:quit");
333
- return `${prefix} ${config.message}
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";
377
- }
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
- }
400
- });
401
- return name || null;
402
- }
403
- async function confirmDeleteWorktree(threadName) {
404
- const confirmed = await confirm({
405
- message: `'${threadName}' \uC6CC\uD06C\uD2B8\uB9AC\uB97C \uC0AD\uC81C\uD560\uAE4C\uC694?`,
406
- default: true
407
- });
408
- if (!confirmed) {
409
- return { confirmed: false, removeGitWorktree: false };
410
- }
411
- const removeGitWorktree = await confirm({
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
- }
423
-
424
152
  // dist/index.js
425
- import { confirm as confirm3 } from "@inquirer/prompts";
153
+ import { confirm } from "@inquirer/prompts";
426
154
 
427
155
  // dist/flip/server/Server.js
428
156
  import express2 from "express";
@@ -433,7 +161,7 @@ import net from "net";
433
161
  // dist/flip/routes/files.js
434
162
  import { Router } from "express";
435
163
  import fs2 from "fs";
436
- import path2 from "path";
164
+ import path from "path";
437
165
 
438
166
  // dist/flip/constants/filters.js
439
167
  var FILTERED_PATTERNS = /* @__PURE__ */ new Set([
@@ -500,8 +228,8 @@ function buildFileTree(rootPath, currentPath, maxDepth, depth = 0) {
500
228
  return a.name.localeCompare(b.name);
501
229
  });
502
230
  for (const entry of entries) {
503
- const fullPath = path2.join(currentPath, entry.name);
504
- const relativePath = path2.relative(rootPath, fullPath);
231
+ const fullPath = path.join(currentPath, entry.name);
232
+ const relativePath = path.relative(rootPath, fullPath);
505
233
  const filtered = isFiltered(entry.name);
506
234
  const node = {
507
235
  path: relativePath,
@@ -521,8 +249,8 @@ function collectFlatFiles(rootPath, currentPath, maxDepth, depth = 0, result = {
521
249
  return result;
522
250
  const entries = fs2.readdirSync(currentPath, { withFileTypes: true });
523
251
  for (const entry of entries) {
524
- const fullPath = path2.join(currentPath, entry.name);
525
- const relativePath = path2.relative(rootPath, fullPath);
252
+ const fullPath = path.join(currentPath, entry.name);
253
+ const relativePath = path.relative(rootPath, fullPath);
526
254
  if (isFiltered(entry.name)) {
527
255
  if (entry.isDirectory()) {
528
256
  result.filteredDirs.push(relativePath);
@@ -592,7 +320,7 @@ router.get("/flat", (req, res) => {
592
320
  // dist/flip/routes/file.js
593
321
  import { Router as Router2 } from "express";
594
322
  import fs3 from "fs";
595
- import path3 from "path";
323
+ import path2 from "path";
596
324
  var router2 = Router2();
597
325
  var extensionMap = {
598
326
  // JavaScript/TypeScript
@@ -705,11 +433,11 @@ var filenameMap = {
705
433
  "CODEOWNERS": "gitignore"
706
434
  };
707
435
  function detectLanguage(filePath) {
708
- const basename5 = path3.basename(filePath);
709
- if (filenameMap[basename5]) {
710
- return filenameMap[basename5];
436
+ const basename2 = path2.basename(filePath);
437
+ if (filenameMap[basename2]) {
438
+ return filenameMap[basename2];
711
439
  }
712
- const ext = path3.extname(filePath).slice(1).toLowerCase();
440
+ const ext = path2.extname(filePath).slice(1).toLowerCase();
713
441
  if (extensionMap[ext]) {
714
442
  return extensionMap[ext];
715
443
  }
@@ -738,9 +466,9 @@ router2.get("/", (req, res) => {
738
466
  res.status(400).json({ error: "Missing path parameter" });
739
467
  return;
740
468
  }
741
- const filePath = path3.join(cwd, relativePath);
742
- const resolvedPath = path3.resolve(filePath);
743
- const resolvedCwd = path3.resolve(cwd);
469
+ const filePath = path2.join(cwd, relativePath);
470
+ const resolvedPath = path2.resolve(filePath);
471
+ const resolvedCwd = path2.resolve(cwd);
744
472
  if (!resolvedPath.startsWith(resolvedCwd)) {
745
473
  res.status(403).json({ error: "Access denied" });
746
474
  return;
@@ -767,7 +495,7 @@ router2.get("/", (req, res) => {
767
495
  import { Router as Router3 } from "express";
768
496
  import { execSync } from "child_process";
769
497
  import * as fs4 from "fs";
770
- import * as path4 from "path";
498
+ import * as path3 from "path";
771
499
  var router3 = Router3();
772
500
  function parseGitStatus(output) {
773
501
  const files = [];
@@ -927,7 +655,7 @@ router3.get("/diff", (req, res) => {
927
655
  res.status(400).json({ error: "Missing path parameter" });
928
656
  return;
929
657
  }
930
- const fullPath = path4.join(cwd, relativePath);
658
+ const fullPath = path3.join(cwd, relativePath);
931
659
  let fileStatus = "modified";
932
660
  try {
933
661
  const statusOutput = execSync(`git status --porcelain -- "${relativePath}"`, {
@@ -1003,10 +731,10 @@ function formatComments(comments) {
1003
731
  // dist/flip/output/clipboard.js
1004
732
  import { spawn } from "child_process";
1005
733
  import fs5 from "fs";
1006
- import path5 from "path";
734
+ import path4 from "path";
1007
735
  import os from "os";
1008
736
  async function copyToClipboard(text) {
1009
- const tmpFile = path5.join(os.tmpdir(), `flip-clipboard-${Date.now()}.txt`);
737
+ const tmpFile = path4.join(os.tmpdir(), `flip-clipboard-${Date.now()}.txt`);
1010
738
  const cleanupTmpFile = () => {
1011
739
  try {
1012
740
  fs5.unlinkSync(tmpFile);
@@ -1052,7 +780,7 @@ async function copyToClipboard(text) {
1052
780
  // dist/flip/output/autopaste.js
1053
781
  import { spawn as spawn2 } from "child_process";
1054
782
  import fs6 from "fs";
1055
- import path6 from "path";
783
+ import path5 from "path";
1056
784
  import os2 from "os";
1057
785
  async function schedulePaste(sessionId) {
1058
786
  if (process.platform !== "darwin") {
@@ -1062,7 +790,7 @@ async function schedulePaste(sessionId) {
1062
790
  await pasteToOriginalSession(sessionId);
1063
791
  }
1064
792
  async function pasteToOriginalSession(sessionId) {
1065
- const sessionFile = path6.join(os2.tmpdir(), `flip-view-session-${sessionId}`);
793
+ const sessionFile = path5.join(os2.tmpdir(), `flip-view-session-${sessionId}`);
1066
794
  let itermSessionId;
1067
795
  try {
1068
796
  itermSessionId = fs6.readFileSync(sessionFile, "utf-8").trim();
@@ -1109,7 +837,7 @@ async function pasteToOriginalSession(sessionId) {
1109
837
  `;
1110
838
  }
1111
839
  try {
1112
- const tmpScript = path6.join(os2.tmpdir(), `flip-paste-${Date.now()}.scpt`);
840
+ const tmpScript = path5.join(os2.tmpdir(), `flip-paste-${Date.now()}.scpt`);
1113
841
  fs6.writeFileSync(tmpScript, script);
1114
842
  const cleanupScript = () => {
1115
843
  try {
@@ -1188,21 +916,21 @@ router5.post("/", async (req, res) => {
1188
916
  // dist/flip/routes/static.js
1189
917
  import { Router as Router6 } from "express";
1190
918
  import express from "express";
1191
- import path7 from "path";
919
+ import path6 from "path";
1192
920
  import { fileURLToPath } from "url";
1193
921
  import fs7 from "fs";
1194
922
  function createStaticRouter() {
1195
923
  const router6 = Router6();
1196
924
  const __filename = fileURLToPath(import.meta.url);
1197
- const __dirname = path7.dirname(__filename);
1198
- let distPath = path7.resolve(__dirname, "flip-ui/dist");
925
+ const __dirname = path6.dirname(__filename);
926
+ let distPath = path6.resolve(__dirname, "flip-ui/dist");
1199
927
  if (!fs7.existsSync(distPath)) {
1200
- distPath = path7.resolve(__dirname, "../../../flip-ui/dist");
928
+ distPath = path6.resolve(__dirname, "../../../flip-ui/dist");
1201
929
  }
1202
930
  if (fs7.existsSync(distPath)) {
1203
931
  router6.use(express.static(distPath));
1204
932
  router6.get("*", (req, res) => {
1205
- const indexPath = path7.join(distPath, "index.html");
933
+ const indexPath = path6.join(distPath, "index.html");
1206
934
  if (fs7.existsSync(indexPath)) {
1207
935
  res.sendFile(indexPath);
1208
936
  } else {
@@ -1310,7 +1038,7 @@ function createChangesRouter(sessionManager) {
1310
1038
 
1311
1039
  // dist/flip/watcher/FileWatcher.js
1312
1040
  import chokidar from "chokidar";
1313
- import * as path8 from "path";
1041
+ import * as path7 from "path";
1314
1042
  var FileWatcher = class {
1315
1043
  watcher = null;
1316
1044
  cwd;
@@ -1327,8 +1055,8 @@ var FileWatcher = class {
1327
1055
  }
1328
1056
  start() {
1329
1057
  const shouldIgnore = (filePath) => {
1330
- const relativePath = path8.relative(this.cwd, filePath);
1331
- const segments = relativePath.split(path8.sep);
1058
+ const relativePath = path7.relative(this.cwd, filePath);
1059
+ const segments = relativePath.split(path7.sep);
1332
1060
  return segments.some((segment) => segment !== ".git" && FILTERED_PATTERNS.has(segment));
1333
1061
  };
1334
1062
  this.watcher = chokidar.watch(this.cwd, {
@@ -1376,7 +1104,7 @@ var FileWatcher = class {
1376
1104
  this.gitChangedCallbacks.push(callback);
1377
1105
  }
1378
1106
  handleFileEvent(filePath, event) {
1379
- const relativePath = path8.relative(this.cwd, filePath);
1107
+ const relativePath = path7.relative(this.cwd, filePath);
1380
1108
  if (relativePath.startsWith(".git")) {
1381
1109
  this.emitGitChanged();
1382
1110
  return;
@@ -1390,7 +1118,7 @@ var FileWatcher = class {
1390
1118
  this.emitGitChanged();
1391
1119
  }
1392
1120
  handleDirEvent(dirPath, event) {
1393
- const relativePath = path8.relative(this.cwd, dirPath);
1121
+ const relativePath = path7.relative(this.cwd, dirPath);
1394
1122
  if (relativePath.startsWith(".git")) {
1395
1123
  return;
1396
1124
  }
@@ -1750,7 +1478,7 @@ async function findExistingServer(startPort, endPort) {
1750
1478
 
1751
1479
  // dist/flip/index.js
1752
1480
  import open from "open";
1753
- import path9 from "path";
1481
+ import path8 from "path";
1754
1482
  import fs8 from "fs";
1755
1483
  import os3 from "os";
1756
1484
  import http2 from "http";
@@ -1849,7 +1577,7 @@ async function runFlip(args) {
1849
1577
  } else {
1850
1578
  command = "oneshot";
1851
1579
  }
1852
- const cwd = pathArg ? path9.resolve(pathArg) : process.cwd();
1580
+ const cwd = pathArg ? path8.resolve(pathArg) : process.cwd();
1853
1581
  const finalSessionId = sessionId || getSessionId();
1854
1582
  switch (command) {
1855
1583
  case "setup": {
@@ -1963,7 +1691,7 @@ async function setupHotkey() {
1963
1691
  } catch {
1964
1692
  csqPath = new URL(import.meta.url).pathname;
1965
1693
  }
1966
- const nodeDir = path9.dirname(nodePath);
1694
+ const nodeDir = path8.dirname(nodePath);
1967
1695
  const wrapperScript = `#!/bin/bash
1968
1696
  # Add node to PATH (coprocess doesn't inherit shell PATH)
1969
1697
  export PATH="${nodeDir}:$PATH"
@@ -1989,8 +1717,8 @@ else
1989
1717
  exec ${csqPath} flip
1990
1718
  fi
1991
1719
  `;
1992
- const scriptDir = path9.join(os3.homedir(), ".config", "csq");
1993
- const scriptPath = path9.join(scriptDir, "flip-hotkey.sh");
1720
+ const scriptDir = path8.join(os3.homedir(), ".config", "csq");
1721
+ const scriptPath = path8.join(scriptDir, "flip-hotkey.sh");
1994
1722
  if (!fs8.existsSync(scriptDir)) {
1995
1723
  fs8.mkdirSync(scriptDir, { recursive: true });
1996
1724
  }
@@ -2016,1595 +1744,213 @@ fi
2016
1744
  console.log("");
2017
1745
  }
2018
1746
 
2019
- // dist/dash/index.js
2020
- import chalk2 from "chalk";
2021
- import * as path14 from "path";
2022
- import { confirm as confirm2 } from "@inquirer/prompts";
2023
-
2024
- // dist/dash/TmuxAdapter.js
2025
- import { exec as execCallback2 } from "child_process";
2026
- import { promisify as promisify2 } from "util";
2027
- var exec2 = promisify2(execCallback2);
2028
- var execOptions2 = { maxBuffer: 1024 * 1024 };
2029
- var TmuxAdapter = class {
2030
- /**
2031
- * tmux 설치 여부 확인
2032
- */
2033
- async isTmuxAvailable() {
2034
- try {
2035
- await exec2("which tmux", execOptions2);
2036
- return true;
2037
- } catch {
2038
- return false;
1747
+ // dist/config.js
1748
+ import * as fs9 from "fs";
1749
+ import * as os4 from "os";
1750
+ import * as path9 from "path";
1751
+ var GLOBAL_CONFIG_PATH = path9.join(os4.homedir(), ".code-squad", "config.json");
1752
+ async function loadGlobalConfig() {
1753
+ try {
1754
+ const content = await fs9.promises.readFile(GLOBAL_CONFIG_PATH, "utf-8");
1755
+ return JSON.parse(content);
1756
+ } catch (error) {
1757
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1758
+ return {};
2039
1759
  }
1760
+ console.warn(`[Code Squad] Warning: Could not load global config at ${GLOBAL_CONFIG_PATH}.`, error);
1761
+ return {};
2040
1762
  }
2041
- /**
2042
- * 현재 tmux 세션 내부인지 확인
2043
- */
2044
- isInsideTmux() {
2045
- return !!process.env.TMUX;
1763
+ }
1764
+ async function loadConfig(workspaceRoot) {
1765
+ const globalConfig = await loadGlobalConfig();
1766
+ const normalizedPath = path9.resolve(workspaceRoot);
1767
+ const projectConfig = globalConfig.projects?.[normalizedPath] ?? {};
1768
+ const defaults = globalConfig.defaults ?? {};
1769
+ return {
1770
+ ...defaults,
1771
+ ...projectConfig,
1772
+ worktreeCopyPatterns: [
1773
+ .../* @__PURE__ */ new Set([
1774
+ ...defaults.worktreeCopyPatterns ?? [],
1775
+ ...projectConfig.worktreeCopyPatterns ?? []
1776
+ ])
1777
+ ]
1778
+ };
1779
+ }
1780
+ function getWorktreeCopyPatterns(config) {
1781
+ return config.worktreeCopyPatterns ?? [];
1782
+ }
1783
+
1784
+ // dist/fileUtils.js
1785
+ import * as fs10 from "fs";
1786
+ import * as path10 from "path";
1787
+ import fg from "fast-glob";
1788
+ async function copyFilesWithPatterns(sourceRoot, destRoot, patterns) {
1789
+ const copied = [];
1790
+ const failed = [];
1791
+ if (patterns.length === 0) {
1792
+ return { copied, failed };
2046
1793
  }
2047
- /**
2048
- * 세션 존재 여부 확인
2049
- */
2050
- async hasSession(name) {
1794
+ for (const pattern of patterns) {
2051
1795
  try {
2052
- await exec2(`tmux has-session -t "${name}"`, execOptions2);
2053
- return true;
1796
+ const files = await fg(pattern, {
1797
+ cwd: sourceRoot,
1798
+ absolute: true,
1799
+ onlyFiles: true,
1800
+ dot: true
1801
+ // .env 같은 dotfile도 매칭
1802
+ });
1803
+ for (const absolutePath of files) {
1804
+ try {
1805
+ await copySingleFile(absolutePath, sourceRoot, destRoot);
1806
+ const relativePath = path10.relative(sourceRoot, absolutePath);
1807
+ copied.push(relativePath);
1808
+ } catch {
1809
+ const relativePath = path10.relative(sourceRoot, absolutePath);
1810
+ failed.push(relativePath);
1811
+ }
1812
+ }
2054
1813
  } catch {
2055
- return false;
2056
1814
  }
2057
1815
  }
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;
1816
+ return { copied, failed };
1817
+ }
1818
+ async function copySingleFile(absolutePath, sourceRoot, destRoot) {
1819
+ const relativePath = path10.relative(sourceRoot, absolutePath);
1820
+ const destPath = path10.join(destRoot, relativePath);
1821
+ const destDir = path10.dirname(destPath);
1822
+ await fs10.promises.mkdir(destDir, { recursive: true });
1823
+ await fs10.promises.copyFile(absolutePath, destPath);
1824
+ }
1825
+
1826
+ // dist/index.js
1827
+ process.on("SIGINT", () => {
1828
+ process.exit(130);
1829
+ });
1830
+ var gitAdapter = new GitAdapter();
1831
+ async function main() {
1832
+ const args = process.argv.slice(2);
1833
+ const command = args[0];
1834
+ if (command === "--init" || command === "init") {
1835
+ printShellInit();
1836
+ return;
2074
1837
  }
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
- });
1838
+ if (command === "flip") {
1839
+ await runFlip(args.slice(1));
1840
+ return;
2090
1841
  }
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();
1842
+ const workspaceRoot = await findGitRoot(process.cwd());
1843
+ if (!workspaceRoot) {
1844
+ console.error(chalk.red("Error: Not a git repository"));
1845
+ process.exit(1);
2104
1846
  }
2105
- /**
2106
- * 특정 pane 선택 (포커스)
2107
- */
2108
- async selectPane(paneId) {
2109
- await exec2(`tmux select-pane -t "${paneId}"`, execOptions2);
1847
+ switch (command) {
1848
+ case "new":
1849
+ await createWorktreeCommand(workspaceRoot, args.slice(1));
1850
+ break;
1851
+ case "quit":
1852
+ await quitWorktreeCommand();
1853
+ break;
1854
+ case "list":
1855
+ default:
1856
+ await listWorktrees(workspaceRoot);
2110
1857
  }
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
- }
1858
+ }
1859
+ async function findGitRoot(cwd) {
1860
+ if (!await gitAdapter.isGitRepository(cwd)) {
1861
+ return null;
2129
1862
  }
2130
- /**
2131
- * pane 종료
2132
- */
2133
- async killPane(paneId) {
2134
- await exec2(`tmux kill-pane -t "${paneId}"`, execOptions2);
1863
+ return cwd;
1864
+ }
1865
+ function printShellInit() {
1866
+ const script = `
1867
+ csq() {
1868
+ if [[ "$1" == "--init" ]] || [[ "$1" == "init" ]]; then
1869
+ command csq "$@"
1870
+ return
1871
+ fi
1872
+
1873
+ local output
1874
+ output=$(command csq "$@" 2>&1)
1875
+ local exit_code=$?
1876
+
1877
+ if [[ $exit_code -ne 0 ]]; then
1878
+ echo "$output"
1879
+ return $exit_code
1880
+ fi
1881
+
1882
+ # \uB9C8\uC9C0\uB9C9 \uC904\uC774 \uB514\uB809\uD1A0\uB9AC\uBA74 cd
1883
+ local last_line=$(echo "$output" | tail -1)
1884
+ if [[ -d "$last_line" ]]; then
1885
+ # \uB9C8\uC9C0\uB9C9 \uC904 \uC81C\uC678\uD55C \uB098\uBA38\uC9C0 \uCD9C\uB825
1886
+ echo "$output" | sed '$d'
1887
+ cd "$last_line"
1888
+ else
1889
+ echo "$output"
1890
+ fi
1891
+ }
1892
+ `.trim();
1893
+ console.log(script);
1894
+ }
1895
+ async function listWorktrees(workspaceRoot) {
1896
+ const worktrees = await gitAdapter.listWorktrees(workspaceRoot);
1897
+ if (worktrees.length === 0) {
1898
+ console.log(chalk.dim("No worktrees found."));
1899
+ return;
2135
1900
  }
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);
1901
+ for (const wt of worktrees) {
1902
+ console.log(`${chalk.cyan("[W]")} ${wt.branch.padEnd(20)} ${chalk.dim(wt.path)}`);
2142
1903
  }
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);
1904
+ }
1905
+ async function createWorktreeCommand(workspaceRoot, args) {
1906
+ const name = args.find((a) => !a.startsWith("-"));
1907
+ if (!name) {
1908
+ console.error(chalk.red("Error: Name is required"));
1909
+ console.error(chalk.dim("Usage: csq new <name>"));
1910
+ process.exit(1);
2148
1911
  }
2149
- /**
2150
- * pane에 Enter 전송
2151
- */
2152
- async sendEnter(paneId) {
2153
- await exec2(`tmux send-keys -t "${paneId}" Enter`, execOptions2);
1912
+ const repoName = path11.basename(workspaceRoot);
1913
+ const defaultBasePath = path11.join(path11.dirname(workspaceRoot), `${repoName}.worktree`);
1914
+ const worktreePath = path11.join(defaultBasePath, name);
1915
+ try {
1916
+ await gitAdapter.createWorktree(worktreePath, name, workspaceRoot);
1917
+ console.log(chalk.green(`\u2713 Created worktree: ${name}`));
1918
+ await copyWorktreeFiles(workspaceRoot, worktreePath);
1919
+ console.log(worktreePath);
1920
+ } catch (error) {
1921
+ console.error(chalk.red(`Failed to create worktree: ${error.message}`));
1922
+ process.exit(1);
2154
1923
  }
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);
1924
+ }
1925
+ async function quitWorktreeCommand() {
1926
+ const cwd = process.cwd();
1927
+ const context = await gitAdapter.getWorktreeContext(cwd);
1928
+ if (!context.isWorktree) {
1929
+ console.error(chalk.red("Error: Not in a worktree"));
1930
+ process.exit(1);
2161
1931
  }
2162
- /**
2163
- * pane 크기 조정
2164
- */
2165
- async resizePane(paneId, direction, amount) {
2166
- await exec2(`tmux resize-pane -t "${paneId}" -${direction} ${amount}`, execOptions2);
1932
+ if (!context.mainRoot || !context.branch) {
1933
+ console.error(chalk.red("Error: Could not determine worktree context"));
1934
+ process.exit(1);
2167
1935
  }
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;
3396
- }
3397
- const workspaceRoot = await findGitRoot(process.cwd());
3398
- if (!workspaceRoot) {
3399
- console.error(chalk3.red("Error: Not a git repository"));
3400
- process.exit(1);
3401
- }
3402
- switch (command) {
3403
- case "list":
3404
- await listThreads(workspaceRoot);
3405
- break;
3406
- case "new":
3407
- await createWorktreeCommand(workspaceRoot, filteredArgs.slice(1));
3408
- break;
3409
- case "quit":
3410
- await quitWorktreeCommand();
3411
- break;
3412
- case "dash":
3413
- await runDash(workspaceRoot);
3414
- break;
3415
- case "--legacy":
3416
- if (persistentMode) {
3417
- await persistentInteractiveMode(workspaceRoot);
3418
- } else {
3419
- await interactiveMode(workspaceRoot);
3420
- }
3421
- break;
3422
- default:
3423
- await runDash(workspaceRoot);
3424
- }
3425
- }
3426
- async function findGitRoot(cwd) {
3427
- if (!await gitAdapter5.isGitRepository(cwd)) {
3428
- return null;
3429
- }
3430
- return cwd;
3431
- }
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
- function printShellInit() {
3493
- const script = `
3494
- csq() {
3495
- if [[ "$1" == "--init" ]] || [[ "$1" == "init" ]]; then
3496
- command csq "$@"
3497
- return
3498
- fi
3499
-
3500
- local output
3501
- output=$(command csq "$@" 2>&1)
3502
- local exit_code=$?
3503
-
3504
- if [[ $exit_code -ne 0 ]]; then
3505
- echo "$output"
3506
- return $exit_code
3507
- fi
3508
-
3509
- # \uB9C8\uC9C0\uB9C9 \uC904\uC774 \uB514\uB809\uD1A0\uB9AC\uBA74 cd
3510
- local last_line=$(echo "$output" | tail -1)
3511
- if [[ -d "$last_line" ]]; then
3512
- # \uB9C8\uC9C0\uB9C9 \uC904 \uC81C\uC678\uD55C \uB098\uBA38\uC9C0 \uCD9C\uB825
3513
- echo "$output" | sed '$d'
3514
- cd "$last_line"
3515
- else
3516
- echo "$output"
3517
- fi
3518
- }
3519
- `.trim();
3520
- console.log(script);
3521
- }
3522
- async function listThreads(workspaceRoot) {
3523
- const threads = await getAllThreads(workspaceRoot);
3524
- if (threads.length === 0) {
3525
- console.log(chalk3.dim("No threads found."));
3526
- return;
3527
- }
3528
- for (const t of threads) {
3529
- const typeLabel = t.type === "worktree" ? chalk3.cyan("[W]") : chalk3.yellow("[L]");
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
- }
3542
- }
3543
- return { name, split };
3544
- }
3545
- async function createWorktreeCommand(workspaceRoot, args) {
3546
- const { name, split } = parseNewArgs(args);
3547
- if (!name) {
3548
- console.error(chalk3.red("Error: Name is required"));
3549
- console.error(chalk3.dim("Usage: csq new <name> [-s|--split]"));
3550
- process.exit(1);
3551
- }
3552
- const repoName = path15.basename(workspaceRoot);
3553
- const defaultBasePath = path15.join(path15.dirname(workspaceRoot), `${repoName}.worktree`);
3554
- const worktreePath = path15.join(defaultBasePath, name);
3555
- try {
3556
- await gitAdapter5.createWorktree(worktreePath, name, workspaceRoot);
3557
- console.log(chalk3.green(`\u2713 Created worktree: ${name}`));
3558
- await copyWorktreeFiles(workspaceRoot, worktreePath);
3559
- if (split) {
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
- }
3570
- } catch (error) {
3571
- console.error(chalk3.red(`Failed to create worktree: ${error.message}`));
3572
- process.exit(1);
3573
- }
3574
- }
3575
- async function quitWorktreeCommand() {
3576
- const cwd = process.cwd();
3577
- const context = await gitAdapter5.getWorktreeContext(cwd);
3578
- if (!context.isWorktree) {
3579
- console.error(chalk3.red("Error: Not in a worktree"));
3580
- process.exit(1);
3581
- }
3582
- if (!context.mainRoot || !context.branch) {
3583
- console.error(chalk3.red("Error: Could not determine worktree context"));
3584
- process.exit(1);
3585
- }
3586
- const isDirty = await gitAdapter5.hasDirtyState(cwd);
1936
+ const isDirty = await gitAdapter.hasDirtyState(cwd);
3587
1937
  if (isDirty) {
3588
- const confirmed = await confirm3({
1938
+ const confirmed = await confirm({
3589
1939
  message: "Uncommitted changes detected. Delete anyway?",
3590
1940
  default: false
3591
1941
  });
3592
1942
  if (!confirmed) {
3593
- console.log(chalk3.dim("Cancelled."));
1943
+ console.log(chalk.dim("Cancelled."));
3594
1944
  process.exit(0);
3595
1945
  }
3596
1946
  }
3597
1947
  try {
3598
- await gitAdapter5.removeWorktree(context.currentPath, context.mainRoot, true);
3599
- await gitAdapter5.deleteBranch(context.branch, context.mainRoot, true);
3600
- console.log(chalk3.green(`\u2713 Deleted worktree and branch: ${context.branch}`));
3601
- if (process.platform === "darwin") {
3602
- await cdInCurrentTerminal(context.mainRoot);
3603
- } else {
3604
- console.log(context.mainRoot);
3605
- }
1948
+ await gitAdapter.removeWorktree(context.currentPath, context.mainRoot, true);
1949
+ await gitAdapter.deleteBranch(context.branch, context.mainRoot, true);
1950
+ console.log(chalk.green(`\u2713 Deleted worktree and branch: ${context.branch}`));
1951
+ console.log(context.mainRoot);
3606
1952
  } catch (error) {
3607
- console.error(chalk3.red(`Failed to quit: ${error.message}`));
1953
+ console.error(chalk.red(`Failed to quit: ${error.message}`));
3608
1954
  process.exit(1);
3609
1955
  }
3610
1956
  }
@@ -3616,224 +1962,16 @@ async function copyWorktreeFiles(sourceRoot, destRoot) {
3616
1962
  }
3617
1963
  const { copied, failed } = await copyFilesWithPatterns(sourceRoot, destRoot, patterns);
3618
1964
  if (copied.length > 0) {
3619
- console.log(chalk3.green(`\u2713 Copied ${copied.length} file(s) to worktree`));
1965
+ console.log(chalk.green(`\u2713 Copied ${copied.length} file(s) to worktree`));
3620
1966
  }
3621
1967
  if (failed.length > 0) {
3622
- console.log(chalk3.yellow(`\u26A0 Failed to copy ${failed.length} file(s)`));
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
- }
1968
+ console.log(chalk.yellow(`\u26A0 Failed to copy ${failed.length} file(s)`));
3827
1969
  }
3828
- if (targetPath) {
3829
- return { cdPath: targetPath };
3830
- }
3831
- return null;
3832
1970
  }
3833
1971
  main().catch((error) => {
3834
1972
  if (error.message?.includes("SIGINT") || error.message?.includes("force closed")) {
3835
1973
  process.exit(130);
3836
1974
  }
3837
- console.error(chalk3.red(`Error: ${error.message}`));
1975
+ console.error(chalk.red(`Error: ${error.message}`));
3838
1976
  process.exit(1);
3839
1977
  });