code-squad-cli 1.0.8 → 1.1.2
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/flip/index.d.ts +1 -0
- package/dist/flip/index.js +247 -0
- package/dist/flip/output/autopaste.d.ts +5 -0
- package/dist/flip/output/autopaste.js +80 -0
- package/dist/flip/output/clipboard.d.ts +4 -0
- package/dist/flip/output/clipboard.js +7 -0
- package/dist/flip/output/formatter.d.ts +13 -0
- package/dist/flip/output/formatter.js +18 -0
- package/dist/flip/output/index.d.ts +4 -0
- package/dist/flip/output/index.js +3 -0
- package/dist/flip/routes/cancel.d.ts +6 -0
- package/dist/flip/routes/cancel.js +13 -0
- package/dist/flip/routes/file.d.ts +8 -0
- package/dist/flip/routes/file.js +77 -0
- package/dist/flip/routes/files.d.ts +16 -0
- package/dist/flip/routes/files.js +102 -0
- package/dist/flip/routes/git.d.ts +11 -0
- package/dist/flip/routes/git.js +83 -0
- package/dist/flip/routes/static.d.ts +2 -0
- package/dist/flip/routes/static.js +35 -0
- package/dist/flip/routes/submit.d.ts +20 -0
- package/dist/flip/routes/submit.js +42 -0
- package/dist/flip/server/Server.d.ts +11 -0
- package/dist/flip/server/Server.js +63 -0
- package/dist/flip-ui/dist/assets/index-2_ZJL2eQ.css +10 -0
- package/dist/flip-ui/dist/assets/index-BGxWXtBJ.js +66 -0
- package/dist/flip-ui/dist/index.html +13 -0
- package/dist/index.js +711 -22
- package/package.json +14 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// dist/index.js
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
4
|
+
import * as path7 from "path";
|
|
5
|
+
import * as fs7 from "fs";
|
|
6
|
+
import * as os3 from "os";
|
|
7
7
|
import * as crypto from "crypto";
|
|
8
8
|
import chalk2 from "chalk";
|
|
9
9
|
|
|
@@ -51,11 +51,11 @@ var GitAdapter = class {
|
|
|
51
51
|
const headMatch = headLine.match(/^HEAD (.+)$/);
|
|
52
52
|
const branchMatch = branchLine?.match(/^branch refs\/heads\/(.+)$/);
|
|
53
53
|
if (pathMatch && headMatch) {
|
|
54
|
-
const
|
|
54
|
+
const path8 = pathMatch[1];
|
|
55
55
|
const head = headMatch[1];
|
|
56
56
|
const branch = branchMatch ? branchMatch[1] : "HEAD";
|
|
57
|
-
if (
|
|
58
|
-
worktrees.push({ path:
|
|
57
|
+
if (path8 !== workspaceRoot) {
|
|
58
|
+
worktrees.push({ path: path8, branch, head });
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
i += 3;
|
|
@@ -101,14 +101,14 @@ var GitAdapter = class {
|
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
|
-
async isValidWorktree(
|
|
104
|
+
async isValidWorktree(path8, workspaceRoot) {
|
|
105
105
|
try {
|
|
106
|
-
await fs.promises.access(
|
|
106
|
+
await fs.promises.access(path8, fs.constants.R_OK);
|
|
107
107
|
} catch {
|
|
108
108
|
return false;
|
|
109
109
|
}
|
|
110
110
|
const isGitRepo = await new Promise((resolve) => {
|
|
111
|
-
exec(`cd "${
|
|
111
|
+
exec(`cd "${path8}" && git rev-parse --git-dir`, { maxBuffer: 1024 * 1024 }, (error) => {
|
|
112
112
|
resolve(!error);
|
|
113
113
|
});
|
|
114
114
|
});
|
|
@@ -116,7 +116,7 @@ var GitAdapter = class {
|
|
|
116
116
|
return false;
|
|
117
117
|
}
|
|
118
118
|
const worktrees = await this.listWorktrees(workspaceRoot);
|
|
119
|
-
return worktrees.some((wt) => wt.path ===
|
|
119
|
+
return worktrees.some((wt) => wt.path === path8);
|
|
120
120
|
}
|
|
121
121
|
async getWorktreeBranch(worktreePath) {
|
|
122
122
|
return new Promise((resolve, reject) => {
|
|
@@ -401,6 +401,691 @@ async function confirmDeleteLocal(threadName) {
|
|
|
401
401
|
});
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
+
// dist/flip/server/Server.js
|
|
405
|
+
import express2 from "express";
|
|
406
|
+
import cors from "cors";
|
|
407
|
+
import http from "http";
|
|
408
|
+
import net from "net";
|
|
409
|
+
|
|
410
|
+
// dist/flip/routes/files.js
|
|
411
|
+
import { Router } from "express";
|
|
412
|
+
import fs2 from "fs";
|
|
413
|
+
import path2 from "path";
|
|
414
|
+
var router = Router();
|
|
415
|
+
var DEFAULT_IGNORES = [
|
|
416
|
+
"node_modules",
|
|
417
|
+
".git",
|
|
418
|
+
".svn",
|
|
419
|
+
".hg",
|
|
420
|
+
"dist",
|
|
421
|
+
"build",
|
|
422
|
+
"out",
|
|
423
|
+
".cache",
|
|
424
|
+
".next",
|
|
425
|
+
".nuxt",
|
|
426
|
+
"coverage",
|
|
427
|
+
"__pycache__",
|
|
428
|
+
".pytest_cache",
|
|
429
|
+
"target",
|
|
430
|
+
"Cargo.lock",
|
|
431
|
+
"package-lock.json",
|
|
432
|
+
"pnpm-lock.yaml",
|
|
433
|
+
"yarn.lock",
|
|
434
|
+
".DS_Store"
|
|
435
|
+
];
|
|
436
|
+
function shouldIgnore(name) {
|
|
437
|
+
if (name.startsWith(".") && name !== ".gitignore" && name !== ".env.example") {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
return DEFAULT_IGNORES.includes(name);
|
|
441
|
+
}
|
|
442
|
+
function buildFileTree(rootPath, currentPath, maxDepth, depth = 0) {
|
|
443
|
+
if (depth > maxDepth)
|
|
444
|
+
return [];
|
|
445
|
+
const entries = fs2.readdirSync(currentPath, { withFileTypes: true });
|
|
446
|
+
const nodes = [];
|
|
447
|
+
entries.sort((a, b) => {
|
|
448
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
449
|
+
return -1;
|
|
450
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
451
|
+
return 1;
|
|
452
|
+
return a.name.localeCompare(b.name);
|
|
453
|
+
});
|
|
454
|
+
for (const entry of entries) {
|
|
455
|
+
if (shouldIgnore(entry.name))
|
|
456
|
+
continue;
|
|
457
|
+
const fullPath = path2.join(currentPath, entry.name);
|
|
458
|
+
const relativePath = path2.relative(rootPath, fullPath);
|
|
459
|
+
const node = {
|
|
460
|
+
path: relativePath,
|
|
461
|
+
name: entry.name,
|
|
462
|
+
type: entry.isDirectory() ? "directory" : "file"
|
|
463
|
+
};
|
|
464
|
+
if (entry.isDirectory()) {
|
|
465
|
+
node.children = buildFileTree(rootPath, fullPath, maxDepth, depth + 1);
|
|
466
|
+
}
|
|
467
|
+
nodes.push(node);
|
|
468
|
+
}
|
|
469
|
+
return nodes;
|
|
470
|
+
}
|
|
471
|
+
function collectFlatFiles(rootPath, currentPath, maxDepth, depth = 0) {
|
|
472
|
+
if (depth > maxDepth)
|
|
473
|
+
return [];
|
|
474
|
+
const entries = fs2.readdirSync(currentPath, { withFileTypes: true });
|
|
475
|
+
const files = [];
|
|
476
|
+
for (const entry of entries) {
|
|
477
|
+
if (shouldIgnore(entry.name))
|
|
478
|
+
continue;
|
|
479
|
+
const fullPath = path2.join(currentPath, entry.name);
|
|
480
|
+
const relativePath = path2.relative(rootPath, fullPath);
|
|
481
|
+
if (entry.isFile()) {
|
|
482
|
+
files.push(relativePath);
|
|
483
|
+
} else if (entry.isDirectory()) {
|
|
484
|
+
files.push(...collectFlatFiles(rootPath, fullPath, maxDepth, depth + 1));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return files;
|
|
488
|
+
}
|
|
489
|
+
router.get("/", (req, res) => {
|
|
490
|
+
const state = req.app.locals.state;
|
|
491
|
+
const tree = buildFileTree(state.cwd, state.cwd, 10);
|
|
492
|
+
const response = {
|
|
493
|
+
root: state.cwd,
|
|
494
|
+
tree
|
|
495
|
+
};
|
|
496
|
+
res.json(response);
|
|
497
|
+
});
|
|
498
|
+
router.get("/flat", (req, res) => {
|
|
499
|
+
const state = req.app.locals.state;
|
|
500
|
+
const files = collectFlatFiles(state.cwd, state.cwd, 10);
|
|
501
|
+
files.sort();
|
|
502
|
+
const response = {
|
|
503
|
+
files
|
|
504
|
+
};
|
|
505
|
+
res.json(response);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// dist/flip/routes/file.js
|
|
509
|
+
import { Router as Router2 } from "express";
|
|
510
|
+
import fs3 from "fs";
|
|
511
|
+
import path3 from "path";
|
|
512
|
+
var router2 = Router2();
|
|
513
|
+
function detectLanguage(filePath) {
|
|
514
|
+
const ext = path3.extname(filePath).slice(1).toLowerCase();
|
|
515
|
+
const languageMap = {
|
|
516
|
+
rs: "rust",
|
|
517
|
+
js: "javascript",
|
|
518
|
+
ts: "typescript",
|
|
519
|
+
tsx: "tsx",
|
|
520
|
+
jsx: "jsx",
|
|
521
|
+
py: "python",
|
|
522
|
+
go: "go",
|
|
523
|
+
java: "java",
|
|
524
|
+
c: "c",
|
|
525
|
+
cpp: "cpp",
|
|
526
|
+
cc: "cpp",
|
|
527
|
+
cxx: "cpp",
|
|
528
|
+
h: "cpp",
|
|
529
|
+
hpp: "cpp",
|
|
530
|
+
md: "markdown",
|
|
531
|
+
json: "json",
|
|
532
|
+
yaml: "yaml",
|
|
533
|
+
yml: "yaml",
|
|
534
|
+
toml: "toml",
|
|
535
|
+
html: "html",
|
|
536
|
+
css: "css",
|
|
537
|
+
scss: "scss",
|
|
538
|
+
sh: "bash",
|
|
539
|
+
bash: "bash",
|
|
540
|
+
sql: "sql",
|
|
541
|
+
rb: "ruby",
|
|
542
|
+
swift: "swift",
|
|
543
|
+
kt: "kotlin",
|
|
544
|
+
kts: "kotlin",
|
|
545
|
+
xml: "xml",
|
|
546
|
+
vue: "vue"
|
|
547
|
+
};
|
|
548
|
+
return languageMap[ext] || "plaintext";
|
|
549
|
+
}
|
|
550
|
+
router2.get("/", (req, res) => {
|
|
551
|
+
const state = req.app.locals.state;
|
|
552
|
+
const relativePath = req.query.path;
|
|
553
|
+
if (!relativePath) {
|
|
554
|
+
res.status(400).json({ error: "Missing path parameter" });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const filePath = path3.join(state.cwd, relativePath);
|
|
558
|
+
const resolvedPath = path3.resolve(filePath);
|
|
559
|
+
const resolvedCwd = path3.resolve(state.cwd);
|
|
560
|
+
if (!resolvedPath.startsWith(resolvedCwd)) {
|
|
561
|
+
res.status(403).json({ error: "Access denied" });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
const content = fs3.readFileSync(resolvedPath, "utf-8");
|
|
566
|
+
const language = detectLanguage(relativePath);
|
|
567
|
+
const response = {
|
|
568
|
+
path: relativePath,
|
|
569
|
+
content,
|
|
570
|
+
language
|
|
571
|
+
};
|
|
572
|
+
res.json(response);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
if (err.code === "ENOENT") {
|
|
575
|
+
res.status(404).json({ error: "File not found" });
|
|
576
|
+
} else {
|
|
577
|
+
res.status(500).json({ error: "Failed to read file" });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// dist/flip/routes/git.js
|
|
583
|
+
import { Router as Router3 } from "express";
|
|
584
|
+
import { execSync } from "child_process";
|
|
585
|
+
var router3 = Router3();
|
|
586
|
+
function parseGitStatus(output) {
|
|
587
|
+
const files = [];
|
|
588
|
+
for (const line of output.split("\n")) {
|
|
589
|
+
if (line.length < 3)
|
|
590
|
+
continue;
|
|
591
|
+
const statusCode = line.substring(0, 2);
|
|
592
|
+
let filePath = line.substring(3);
|
|
593
|
+
filePath = filePath.replace(/^"|"$/g, "");
|
|
594
|
+
let status;
|
|
595
|
+
if (statusCode === "??") {
|
|
596
|
+
status = "untracked";
|
|
597
|
+
} else if (statusCode.includes("M")) {
|
|
598
|
+
status = "modified";
|
|
599
|
+
} else if (statusCode.includes("D")) {
|
|
600
|
+
status = "deleted";
|
|
601
|
+
} else if (statusCode.includes("A")) {
|
|
602
|
+
status = "added";
|
|
603
|
+
} else if (statusCode.includes("R")) {
|
|
604
|
+
status = "renamed";
|
|
605
|
+
} else if (statusCode.includes("C")) {
|
|
606
|
+
status = "copied";
|
|
607
|
+
} else if (statusCode === "UU") {
|
|
608
|
+
status = "unmerged";
|
|
609
|
+
} else {
|
|
610
|
+
status = "modified";
|
|
611
|
+
}
|
|
612
|
+
files.push({ path: filePath, status });
|
|
613
|
+
}
|
|
614
|
+
return files;
|
|
615
|
+
}
|
|
616
|
+
router3.get("/status", (req, res) => {
|
|
617
|
+
const state = req.app.locals.state;
|
|
618
|
+
let isGitRepo = false;
|
|
619
|
+
try {
|
|
620
|
+
execSync("git rev-parse --git-dir", {
|
|
621
|
+
cwd: state.cwd,
|
|
622
|
+
stdio: "pipe"
|
|
623
|
+
});
|
|
624
|
+
isGitRepo = true;
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
if (!isGitRepo) {
|
|
628
|
+
const response2 = {
|
|
629
|
+
isGitRepo: false,
|
|
630
|
+
unstaged: []
|
|
631
|
+
};
|
|
632
|
+
res.json(response2);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
let unstaged = [];
|
|
636
|
+
try {
|
|
637
|
+
const output = execSync("git status --porcelain", {
|
|
638
|
+
cwd: state.cwd,
|
|
639
|
+
encoding: "utf-8"
|
|
640
|
+
});
|
|
641
|
+
unstaged = parseGitStatus(output);
|
|
642
|
+
} catch {
|
|
643
|
+
}
|
|
644
|
+
const response = {
|
|
645
|
+
isGitRepo: true,
|
|
646
|
+
unstaged
|
|
647
|
+
};
|
|
648
|
+
res.json(response);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// dist/flip/routes/submit.js
|
|
652
|
+
import { Router as Router4 } from "express";
|
|
653
|
+
|
|
654
|
+
// dist/flip/output/formatter.js
|
|
655
|
+
function formatComments(comments) {
|
|
656
|
+
if (comments.length === 0) {
|
|
657
|
+
return "";
|
|
658
|
+
}
|
|
659
|
+
let output = "The user has annotated the following code locations:\n\n";
|
|
660
|
+
for (const comment of comments) {
|
|
661
|
+
const lineRange = comment.endLine && comment.endLine !== comment.line ? `${comment.line}-${comment.endLine}` : `${comment.line}`;
|
|
662
|
+
const location = `${comment.file}:${lineRange}`;
|
|
663
|
+
output += `- ${location} -> "${comment.text}"
|
|
664
|
+
`;
|
|
665
|
+
}
|
|
666
|
+
output += "\nPlease review these comments and address them.";
|
|
667
|
+
return output;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// dist/flip/output/clipboard.js
|
|
671
|
+
import clipboardy from "clipboardy";
|
|
672
|
+
async function copyToClipboard(text) {
|
|
673
|
+
await clipboardy.write(text);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// dist/flip/output/autopaste.js
|
|
677
|
+
import { execSync as execSync2 } from "child_process";
|
|
678
|
+
import fs4 from "fs";
|
|
679
|
+
import path4 from "path";
|
|
680
|
+
import os from "os";
|
|
681
|
+
async function schedulePaste(sessionId) {
|
|
682
|
+
if (process.platform !== "darwin") {
|
|
683
|
+
console.error("Auto-paste not supported on this platform. Please paste manually (Ctrl+V).");
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
await pasteToOriginalSession(sessionId);
|
|
687
|
+
}
|
|
688
|
+
async function pasteToOriginalSession(sessionId) {
|
|
689
|
+
const sessionFile = path4.join(os.tmpdir(), `flip-view-session-${sessionId}`);
|
|
690
|
+
let itermSessionId;
|
|
691
|
+
try {
|
|
692
|
+
itermSessionId = fs4.readFileSync(sessionFile, "utf-8").trim();
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
let script;
|
|
696
|
+
if (itermSessionId) {
|
|
697
|
+
script = `
|
|
698
|
+
tell application "iTerm2"
|
|
699
|
+
set found to false
|
|
700
|
+
repeat with w in windows
|
|
701
|
+
repeat with t in tabs of w
|
|
702
|
+
repeat with s in sessions of t
|
|
703
|
+
if id of s is "${itermSessionId}" then
|
|
704
|
+
set found to true
|
|
705
|
+
select s
|
|
706
|
+
tell s to write text (the clipboard)
|
|
707
|
+
tell s to write text ""
|
|
708
|
+
return
|
|
709
|
+
end if
|
|
710
|
+
end repeat
|
|
711
|
+
end repeat
|
|
712
|
+
end repeat
|
|
713
|
+
|
|
714
|
+
if not found then
|
|
715
|
+
display notification "Original session closed. Output copied to clipboard." with title "flip"
|
|
716
|
+
end if
|
|
717
|
+
end tell
|
|
718
|
+
`;
|
|
719
|
+
try {
|
|
720
|
+
fs4.unlinkSync(sessionFile);
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
script = `
|
|
725
|
+
tell application "iTerm2"
|
|
726
|
+
activate
|
|
727
|
+
delay 0.1
|
|
728
|
+
tell current session of current window
|
|
729
|
+
write text (the clipboard)
|
|
730
|
+
write text ""
|
|
731
|
+
end tell
|
|
732
|
+
end tell
|
|
733
|
+
`;
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
execSync2(`osascript -e '${script.replace(/'/g, `'"'"'`)}'`, {
|
|
737
|
+
stdio: "pipe"
|
|
738
|
+
});
|
|
739
|
+
} catch (e) {
|
|
740
|
+
console.error("Failed to paste to iTerm:", e);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// dist/flip/routes/submit.js
|
|
745
|
+
var router4 = Router4();
|
|
746
|
+
router4.post("/", async (req, res) => {
|
|
747
|
+
const state = req.app.locals.state;
|
|
748
|
+
const body = req.body;
|
|
749
|
+
if (!body.items || body.items.length === 0) {
|
|
750
|
+
res.status(400).json({ error: "No items to submit" });
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const comments = body.items.map((item) => ({
|
|
754
|
+
file: item.filePath,
|
|
755
|
+
line: item.startLine,
|
|
756
|
+
endLine: item.endLine !== item.startLine ? item.endLine : void 0,
|
|
757
|
+
text: item.comment
|
|
758
|
+
}));
|
|
759
|
+
const formatted = formatComments(comments);
|
|
760
|
+
try {
|
|
761
|
+
await copyToClipboard(formatted);
|
|
762
|
+
} catch (e) {
|
|
763
|
+
console.error("Failed to copy to clipboard:", e);
|
|
764
|
+
}
|
|
765
|
+
try {
|
|
766
|
+
await schedulePaste(body.session_id);
|
|
767
|
+
} catch (e) {
|
|
768
|
+
console.error("Failed to schedule paste:", e);
|
|
769
|
+
}
|
|
770
|
+
res.json({ status: "ok" });
|
|
771
|
+
if (state.resolve) {
|
|
772
|
+
state.resolve(formatted);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// dist/flip/routes/cancel.js
|
|
777
|
+
import { Router as Router5 } from "express";
|
|
778
|
+
var router5 = Router5();
|
|
779
|
+
router5.post("/", (req, res) => {
|
|
780
|
+
const state = req.app.locals.state;
|
|
781
|
+
res.json({ status: "ok" });
|
|
782
|
+
if (state.resolve) {
|
|
783
|
+
state.resolve(null);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// dist/flip/routes/static.js
|
|
788
|
+
import { Router as Router6 } from "express";
|
|
789
|
+
import express from "express";
|
|
790
|
+
import path5 from "path";
|
|
791
|
+
import { fileURLToPath } from "url";
|
|
792
|
+
import fs5 from "fs";
|
|
793
|
+
function createStaticRouter() {
|
|
794
|
+
const router6 = Router6();
|
|
795
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
796
|
+
const __dirname = path5.dirname(__filename);
|
|
797
|
+
const distPath = path5.resolve(__dirname, "flip-ui/dist");
|
|
798
|
+
if (fs5.existsSync(distPath)) {
|
|
799
|
+
router6.use(express.static(distPath));
|
|
800
|
+
router6.get("*", (req, res) => {
|
|
801
|
+
const indexPath = path5.join(distPath, "index.html");
|
|
802
|
+
if (fs5.existsSync(indexPath)) {
|
|
803
|
+
res.sendFile(indexPath);
|
|
804
|
+
} else {
|
|
805
|
+
res.status(404).send("Not Found");
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
} else {
|
|
809
|
+
router6.get("*", (req, res) => {
|
|
810
|
+
res.redirect(`http://localhost:5173${req.url}`);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
return router6;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// dist/flip/server/Server.js
|
|
817
|
+
var Server = class {
|
|
818
|
+
cwd;
|
|
819
|
+
port;
|
|
820
|
+
constructor(cwd, port) {
|
|
821
|
+
this.cwd = cwd;
|
|
822
|
+
this.port = port;
|
|
823
|
+
}
|
|
824
|
+
async run() {
|
|
825
|
+
return new Promise((resolve) => {
|
|
826
|
+
const state = {
|
|
827
|
+
cwd: this.cwd,
|
|
828
|
+
resolve: null
|
|
829
|
+
};
|
|
830
|
+
const app = express2();
|
|
831
|
+
app.use(cors());
|
|
832
|
+
app.use(express2.json());
|
|
833
|
+
app.locals.state = state;
|
|
834
|
+
app.use("/api/files", router);
|
|
835
|
+
app.use("/api/file", router2);
|
|
836
|
+
app.use("/api/git", router3);
|
|
837
|
+
app.use("/api/submit", router4);
|
|
838
|
+
app.use("/api/cancel", router5);
|
|
839
|
+
app.use(createStaticRouter());
|
|
840
|
+
const server = http.createServer(app);
|
|
841
|
+
state.resolve = (output) => {
|
|
842
|
+
server.close();
|
|
843
|
+
resolve(output);
|
|
844
|
+
};
|
|
845
|
+
server.listen(this.port, "127.0.0.1", () => {
|
|
846
|
+
console.log(`Server running at http://localhost:${this.port}`);
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
async function findFreePort(preferred) {
|
|
852
|
+
return new Promise((resolve) => {
|
|
853
|
+
const server = net.createServer();
|
|
854
|
+
server.listen(preferred, "127.0.0.1", () => {
|
|
855
|
+
server.close(() => {
|
|
856
|
+
resolve(preferred);
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
server.on("error", () => {
|
|
860
|
+
findFreePort(preferred + 1).then(resolve);
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// dist/flip/index.js
|
|
866
|
+
import open from "open";
|
|
867
|
+
import path6 from "path";
|
|
868
|
+
import fs6 from "fs";
|
|
869
|
+
import os2 from "os";
|
|
870
|
+
import { execSync as execSync3 } from "child_process";
|
|
871
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
872
|
+
import clipboardy2 from "clipboardy";
|
|
873
|
+
var DEFAULT_PORT = 51234;
|
|
874
|
+
function formatTime() {
|
|
875
|
+
const now = /* @__PURE__ */ new Date();
|
|
876
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
877
|
+
const mins = String(now.getMinutes()).padStart(2, "0");
|
|
878
|
+
const secs = String(now.getSeconds()).padStart(2, "0");
|
|
879
|
+
return `${hours}:${mins}:${secs}`;
|
|
880
|
+
}
|
|
881
|
+
async function runFlip(args) {
|
|
882
|
+
let sessionId;
|
|
883
|
+
const sessionIdx = args.indexOf("--session");
|
|
884
|
+
if (sessionIdx !== -1 && args[sessionIdx + 1]) {
|
|
885
|
+
sessionId = args[sessionIdx + 1];
|
|
886
|
+
}
|
|
887
|
+
const filteredArgs = args.filter((arg, idx) => {
|
|
888
|
+
if (arg === "--session")
|
|
889
|
+
return false;
|
|
890
|
+
if (idx > 0 && args[idx - 1] === "--session")
|
|
891
|
+
return false;
|
|
892
|
+
return true;
|
|
893
|
+
});
|
|
894
|
+
let command;
|
|
895
|
+
let pathArg;
|
|
896
|
+
if (filteredArgs.length > 0) {
|
|
897
|
+
switch (filteredArgs[0]) {
|
|
898
|
+
case "serve":
|
|
899
|
+
command = "serve";
|
|
900
|
+
pathArg = filteredArgs[1];
|
|
901
|
+
break;
|
|
902
|
+
case "open":
|
|
903
|
+
command = "open";
|
|
904
|
+
break;
|
|
905
|
+
case "setup":
|
|
906
|
+
command = "setup";
|
|
907
|
+
break;
|
|
908
|
+
case "--help":
|
|
909
|
+
case "-h":
|
|
910
|
+
printUsage();
|
|
911
|
+
return;
|
|
912
|
+
default:
|
|
913
|
+
command = "oneshot";
|
|
914
|
+
pathArg = filteredArgs[0];
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
} else {
|
|
918
|
+
command = "oneshot";
|
|
919
|
+
}
|
|
920
|
+
const cwd = pathArg ? path6.resolve(pathArg) : process.cwd();
|
|
921
|
+
switch (command) {
|
|
922
|
+
case "setup": {
|
|
923
|
+
await setupHotkey();
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
case "serve": {
|
|
927
|
+
const port = await findFreePort(DEFAULT_PORT);
|
|
928
|
+
console.log(`Server running at http://localhost:${port}`);
|
|
929
|
+
console.log("Press Ctrl+C to stop");
|
|
930
|
+
console.log("");
|
|
931
|
+
console.log("To open browser, run: csq flip open");
|
|
932
|
+
console.log(`Or use hotkey to open: open http://localhost:${port}`);
|
|
933
|
+
while (true) {
|
|
934
|
+
const server = new Server(cwd, port);
|
|
935
|
+
const result = await server.run();
|
|
936
|
+
if (result) {
|
|
937
|
+
console.log(`[${formatTime()}] Submitted ${result.length} characters`);
|
|
938
|
+
} else {
|
|
939
|
+
console.log(`[${formatTime()}] Cancelled`);
|
|
940
|
+
}
|
|
941
|
+
console.log(`[${formatTime()}] Ready for next session...`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
case "open": {
|
|
945
|
+
const url = `http://localhost:${DEFAULT_PORT}`;
|
|
946
|
+
console.log(`Opening ${url} in browser...`);
|
|
947
|
+
try {
|
|
948
|
+
await open(url);
|
|
949
|
+
} catch (e) {
|
|
950
|
+
console.error("Failed to open browser:", e);
|
|
951
|
+
console.error("Is the server running? Start with: csq flip serve");
|
|
952
|
+
}
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
case "oneshot":
|
|
956
|
+
default: {
|
|
957
|
+
const port = await findFreePort(DEFAULT_PORT);
|
|
958
|
+
const url = sessionId ? `http://localhost:${port}?session=${sessionId}` : `http://localhost:${port}`;
|
|
959
|
+
console.log(`Opening ${url} in browser...`);
|
|
960
|
+
try {
|
|
961
|
+
await open(url);
|
|
962
|
+
} catch (e) {
|
|
963
|
+
console.error("Failed to open browser:", e);
|
|
964
|
+
console.log(`Please open ${url} manually`);
|
|
965
|
+
}
|
|
966
|
+
const server = new Server(cwd, port);
|
|
967
|
+
const result = await server.run();
|
|
968
|
+
if (result) {
|
|
969
|
+
console.log(`
|
|
970
|
+
Submitted ${result.length} characters`);
|
|
971
|
+
} else {
|
|
972
|
+
console.log("\nCancelled");
|
|
973
|
+
}
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function printUsage() {
|
|
979
|
+
console.error("Usage: csq flip [command] [options]");
|
|
980
|
+
console.error("");
|
|
981
|
+
console.error("Commands:");
|
|
982
|
+
console.error(" serve [path] Start server in daemon mode (keeps running)");
|
|
983
|
+
console.error(" open Open browser to existing server");
|
|
984
|
+
console.error(" setup Setup iTerm2 hotkey");
|
|
985
|
+
console.error(" (no command) Start server + open browser (one-shot mode)");
|
|
986
|
+
console.error("");
|
|
987
|
+
console.error("Options:");
|
|
988
|
+
console.error(" path Directory to serve (default: current directory)");
|
|
989
|
+
console.error(" --session <uuid> Session ID for paste-back tracking");
|
|
990
|
+
}
|
|
991
|
+
async function setupHotkey() {
|
|
992
|
+
const configDir = path6.join(os2.homedir(), ".config", "flip");
|
|
993
|
+
const applescriptPath = path6.join(configDir, "flip.applescript");
|
|
994
|
+
const shPath = path6.join(configDir, "flip.sh");
|
|
995
|
+
let nodePath;
|
|
996
|
+
try {
|
|
997
|
+
nodePath = execSync3("which node", { encoding: "utf-8" }).trim();
|
|
998
|
+
} catch {
|
|
999
|
+
nodePath = "/usr/local/bin/node";
|
|
1000
|
+
}
|
|
1001
|
+
const csqPath = new URL(import.meta.url).pathname;
|
|
1002
|
+
fs6.mkdirSync(configDir, { recursive: true });
|
|
1003
|
+
const applescriptContent = `#!/usr/bin/osascript
|
|
1004
|
+
|
|
1005
|
+
-- flip hotkey script
|
|
1006
|
+
-- Generated by: csq flip setup
|
|
1007
|
+
|
|
1008
|
+
tell application "iTerm2"
|
|
1009
|
+
tell current session of current window
|
|
1010
|
+
set originalSessionId to id
|
|
1011
|
+
set sessionUUID to do shell script "uuidgen"
|
|
1012
|
+
set currentPath to variable named "path"
|
|
1013
|
+
do shell script "echo '" & originalSessionId & "' > /tmp/flip-view-session-" & sessionUUID
|
|
1014
|
+
do shell script "nohup ${nodePath} ${csqPath} flip --session " & sessionUUID & " " & quoted form of currentPath & " > /tmp/flip.log 2>&1 &"
|
|
1015
|
+
end tell
|
|
1016
|
+
end tell
|
|
1017
|
+
|
|
1018
|
+
return ""`;
|
|
1019
|
+
fs6.writeFileSync(applescriptPath, applescriptContent);
|
|
1020
|
+
fs6.chmodSync(applescriptPath, "755");
|
|
1021
|
+
const shContent = `#!/bin/bash
|
|
1022
|
+
osascript ${applescriptPath} > /dev/null 2>&1
|
|
1023
|
+
`;
|
|
1024
|
+
fs6.writeFileSync(shPath, shContent);
|
|
1025
|
+
fs6.chmodSync(shPath, "755");
|
|
1026
|
+
console.log("");
|
|
1027
|
+
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
1028
|
+
console.log("\u2502 Flip Hotkey Setup Wizard \u2502");
|
|
1029
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
1030
|
+
console.log("");
|
|
1031
|
+
console.log("\u2713 Scripts generated:");
|
|
1032
|
+
console.log(` ${shPath}`);
|
|
1033
|
+
console.log("");
|
|
1034
|
+
const openSettings = await confirm2({
|
|
1035
|
+
message: "Open iTerm2 Settings? (Keys \u2192 Key Bindings)",
|
|
1036
|
+
default: true
|
|
1037
|
+
});
|
|
1038
|
+
if (openSettings) {
|
|
1039
|
+
try {
|
|
1040
|
+
execSync3(`osascript -e 'tell application "iTerm2" to activate' -e 'tell application "System Events" to keystroke "," using command down'`);
|
|
1041
|
+
console.log("");
|
|
1042
|
+
console.log(" \u2192 iTerm2 Settings opened. Navigate to: Keys \u2192 Key Bindings");
|
|
1043
|
+
} catch {
|
|
1044
|
+
console.log(" \u2192 Could not open settings automatically. Open manually: iTerm2 \u2192 Settings \u2192 Keys \u2192 Key Bindings");
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
console.log("");
|
|
1048
|
+
const ready = await confirm2({
|
|
1049
|
+
message: "Ready to add Key Binding? (Click + button in Key Bindings tab)",
|
|
1050
|
+
default: true
|
|
1051
|
+
});
|
|
1052
|
+
if (!ready) {
|
|
1053
|
+
console.log("");
|
|
1054
|
+
console.log("Run `csq flip setup` again when ready.");
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
console.log("");
|
|
1058
|
+
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
1059
|
+
console.log("\u2502 Configure the Key Binding: \u2502");
|
|
1060
|
+
console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
1061
|
+
console.log("\u2502 1. Keyboard Shortcut: Press your hotkey \u2502");
|
|
1062
|
+
console.log("\u2502 (e.g., \u2318\u21E7F) \u2502");
|
|
1063
|
+
console.log("\u2502 \u2502");
|
|
1064
|
+
console.log('\u2502 2. Action: Select "Run Coprocess" \u2502');
|
|
1065
|
+
console.log("\u2502 \u2502");
|
|
1066
|
+
console.log("\u2502 3. Command: Paste the path (copied below) \u2502");
|
|
1067
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
1068
|
+
console.log("");
|
|
1069
|
+
await clipboardy2.write(shPath);
|
|
1070
|
+
console.log(`\u2713 Copied to clipboard: ${shPath}`);
|
|
1071
|
+
console.log("");
|
|
1072
|
+
await confirm2({
|
|
1073
|
+
message: "Done configuring? (Paste the path and click OK)",
|
|
1074
|
+
default: true
|
|
1075
|
+
});
|
|
1076
|
+
console.log("");
|
|
1077
|
+
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
1078
|
+
console.log("\u2502 Setup Complete! \u2502");
|
|
1079
|
+
console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
1080
|
+
console.log("\u2502 Usage: \u2502");
|
|
1081
|
+
console.log("\u2502 1. Focus on any terminal panel \u2502");
|
|
1082
|
+
console.log("\u2502 2. Press your hotkey \u2192 browser opens \u2502");
|
|
1083
|
+
console.log("\u2502 3. Select files, add comments \u2502");
|
|
1084
|
+
console.log("\u2502 4. Submit \u2192 text appears in terminal \u2502");
|
|
1085
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
1086
|
+
console.log("");
|
|
1087
|
+
}
|
|
1088
|
+
|
|
404
1089
|
// dist/index.js
|
|
405
1090
|
process.on("SIGINT", () => {
|
|
406
1091
|
process.exit(130);
|
|
@@ -415,6 +1100,10 @@ async function main() {
|
|
|
415
1100
|
printShellInit();
|
|
416
1101
|
return;
|
|
417
1102
|
}
|
|
1103
|
+
if (command === "flip") {
|
|
1104
|
+
await runFlip(filteredArgs.slice(1));
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
418
1107
|
const workspaceRoot = await findGitRoot(process.cwd());
|
|
419
1108
|
if (!workspaceRoot) {
|
|
420
1109
|
console.error(chalk2.red("Error: Not a git repository"));
|
|
@@ -446,13 +1135,13 @@ function getProjectHash(workspaceRoot) {
|
|
|
446
1135
|
}
|
|
447
1136
|
function getSessionsPath(workspaceRoot) {
|
|
448
1137
|
const projectHash = getProjectHash(workspaceRoot);
|
|
449
|
-
const projectName =
|
|
450
|
-
return
|
|
1138
|
+
const projectName = path7.basename(workspaceRoot);
|
|
1139
|
+
return path7.join(os3.homedir(), ".code-squad", "sessions", `${projectName}-${projectHash}.json`);
|
|
451
1140
|
}
|
|
452
1141
|
async function loadLocalThreads(workspaceRoot) {
|
|
453
1142
|
const sessionsPath = getSessionsPath(workspaceRoot);
|
|
454
1143
|
try {
|
|
455
|
-
const content = await
|
|
1144
|
+
const content = await fs7.promises.readFile(sessionsPath, "utf-8");
|
|
456
1145
|
const data = JSON.parse(content);
|
|
457
1146
|
return data.localThreads || [];
|
|
458
1147
|
} catch {
|
|
@@ -461,9 +1150,9 @@ async function loadLocalThreads(workspaceRoot) {
|
|
|
461
1150
|
}
|
|
462
1151
|
async function saveLocalThreads(workspaceRoot, threads) {
|
|
463
1152
|
const sessionsPath = getSessionsPath(workspaceRoot);
|
|
464
|
-
const dir =
|
|
465
|
-
await
|
|
466
|
-
await
|
|
1153
|
+
const dir = path7.dirname(sessionsPath);
|
|
1154
|
+
await fs7.promises.mkdir(dir, { recursive: true });
|
|
1155
|
+
await fs7.promises.writeFile(sessionsPath, JSON.stringify({ localThreads: threads }, null, 2));
|
|
467
1156
|
}
|
|
468
1157
|
async function addLocalThread(workspaceRoot, name) {
|
|
469
1158
|
const threads = await loadLocalThreads(workspaceRoot);
|
|
@@ -543,13 +1232,13 @@ async function listThreads(workspaceRoot) {
|
|
|
543
1232
|
}
|
|
544
1233
|
}
|
|
545
1234
|
async function createWorktree(workspaceRoot, name) {
|
|
546
|
-
const repoName =
|
|
547
|
-
const defaultBasePath =
|
|
1235
|
+
const repoName = path7.basename(workspaceRoot);
|
|
1236
|
+
const defaultBasePath = path7.join(path7.dirname(workspaceRoot), `${repoName}.worktree`);
|
|
548
1237
|
let worktreeName;
|
|
549
1238
|
let worktreePath;
|
|
550
1239
|
if (name) {
|
|
551
1240
|
worktreeName = name;
|
|
552
|
-
worktreePath =
|
|
1241
|
+
worktreePath = path7.join(defaultBasePath, name);
|
|
553
1242
|
} else {
|
|
554
1243
|
const form = await newWorktreeForm(defaultBasePath);
|
|
555
1244
|
if (!form) {
|
|
@@ -571,7 +1260,7 @@ async function createWorktree(workspaceRoot, name) {
|
|
|
571
1260
|
async function writeCdFile(targetPath) {
|
|
572
1261
|
const cdFile = process.env.CSQ_CD_FILE;
|
|
573
1262
|
if (cdFile) {
|
|
574
|
-
await
|
|
1263
|
+
await fs7.promises.writeFile(cdFile, targetPath);
|
|
575
1264
|
}
|
|
576
1265
|
}
|
|
577
1266
|
async function openNewTerminal(targetPath) {
|
|
@@ -665,7 +1354,7 @@ async function executeDelete(thread, workspaceRoot) {
|
|
|
665
1354
|
}
|
|
666
1355
|
async function runInteraction(workspaceRoot, _persistent = false) {
|
|
667
1356
|
const threads = await getAllThreads(workspaceRoot);
|
|
668
|
-
const repoName =
|
|
1357
|
+
const repoName = path7.basename(workspaceRoot);
|
|
669
1358
|
const choice = await selectThread(threads, repoName);
|
|
670
1359
|
if (choice.type === "exit") {
|
|
671
1360
|
return { exit: true };
|
|
@@ -696,7 +1385,7 @@ async function runInteraction(workspaceRoot, _persistent = false) {
|
|
|
696
1385
|
return null;
|
|
697
1386
|
}
|
|
698
1387
|
if (newType === "worktree") {
|
|
699
|
-
const defaultBasePath =
|
|
1388
|
+
const defaultBasePath = path7.join(path7.dirname(workspaceRoot), `${repoName}.worktree`);
|
|
700
1389
|
const form = await newWorktreeForm(defaultBasePath);
|
|
701
1390
|
if (!form) {
|
|
702
1391
|
return null;
|