groundcrew-cli 0.15.17 → 0.16.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 +134 -4
- package/package.json +1 -1
- package/src/index.ts +103 -6
package/dist/index.js
CHANGED
|
@@ -5,9 +5,42 @@ import fs from "fs/promises";
|
|
|
5
5
|
import { existsSync } from "fs";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import readline from "readline";
|
|
8
|
-
import { execFile } from "child_process";
|
|
8
|
+
import { execFile, execFileSync } from "child_process";
|
|
9
9
|
import { promisify } from "util";
|
|
10
10
|
var execFileAsync = promisify(execFile);
|
|
11
|
+
function getGitContext() {
|
|
12
|
+
try {
|
|
13
|
+
const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
timeout: 500,
|
|
16
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
17
|
+
}).trim();
|
|
18
|
+
let dirty = "";
|
|
19
|
+
try {
|
|
20
|
+
const status2 = execFileSync("git", ["status", "--porcelain", "-uno"], {
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
timeout: 500,
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
24
|
+
}).trim();
|
|
25
|
+
if (status2) {
|
|
26
|
+
const hasStaged = status2.split("\n").some((l) => l[0] !== " " && l[0] !== "?");
|
|
27
|
+
const hasUnstaged = status2.split("\n").some((l) => l[1] === "M" || l[1] === "D");
|
|
28
|
+
if (hasStaged) dirty += "+";
|
|
29
|
+
if (hasUnstaged) dirty += "*";
|
|
30
|
+
}
|
|
31
|
+
const untracked = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
timeout: 500,
|
|
34
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
35
|
+
}).trim();
|
|
36
|
+
if (untracked) dirty += "%";
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
return { branch, dirty };
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
11
44
|
var GROUNDCREW_DIR = ".groundcrew";
|
|
12
45
|
var SESSIONS_DIR = path.join(GROUNDCREW_DIR, "sessions");
|
|
13
46
|
var ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_DIR, "active-sessions.json");
|
|
@@ -429,7 +462,7 @@ var CHAT_COMMANDS = [
|
|
|
429
462
|
{ cmd: "/clear", desc: "Clear pending tasks" },
|
|
430
463
|
{ cmd: "/exit", desc: "Exit chat" }
|
|
431
464
|
];
|
|
432
|
-
function readMultilineInput(sessionId) {
|
|
465
|
+
function readMultilineInput(sessionId, projectName, gitCtx) {
|
|
433
466
|
return new Promise((resolve) => {
|
|
434
467
|
const lines = [""];
|
|
435
468
|
let crow = 0;
|
|
@@ -444,7 +477,13 @@ function readMultilineInput(sessionId) {
|
|
|
444
477
|
if (lastTermRow > 0) buf.push(`\x1B[${lastTermRow}A`);
|
|
445
478
|
buf.push("\r\x1B[J");
|
|
446
479
|
const termW = process.stdout.columns || 80;
|
|
447
|
-
|
|
480
|
+
let info = ` ${sessionId} `;
|
|
481
|
+
const ctxParts = [];
|
|
482
|
+
if (projectName) ctxParts.push(projectName);
|
|
483
|
+
if (gitCtx) {
|
|
484
|
+
ctxParts.push(`git:(${gitCtx.branch}${gitCtx.dirty})`);
|
|
485
|
+
}
|
|
486
|
+
if (ctxParts.length) info += ` ${ctxParts.join(" ")} `;
|
|
448
487
|
const dashRight = "\u2500".repeat(Math.max(0, termW - 4 - info.length));
|
|
449
488
|
buf.push(dim("\u2500\u2500\u2500" + info + dashRight));
|
|
450
489
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -599,6 +638,89 @@ function readMultilineInput(sessionId) {
|
|
|
599
638
|
continue;
|
|
600
639
|
}
|
|
601
640
|
if (str[i] === "\x1B" && i + 1 < str.length && str[i + 1] === "[") {
|
|
641
|
+
const csiMatch = str.slice(i).match(/^\x1b\[(\d+);(\d+)u/);
|
|
642
|
+
if (csiMatch) {
|
|
643
|
+
const codepoint = parseInt(csiMatch[1], 10);
|
|
644
|
+
const modifier = parseInt(csiMatch[2], 10);
|
|
645
|
+
const isCtrl = modifier - 1 & 4;
|
|
646
|
+
const seqLen = csiMatch[0].length;
|
|
647
|
+
if (isCtrl) {
|
|
648
|
+
switch (codepoint) {
|
|
649
|
+
case 99:
|
|
650
|
+
if (fullText() || lines.length > 1 || lines[0].length > 0) {
|
|
651
|
+
const lastRow = lines.length - 1;
|
|
652
|
+
const rowsDown = lastRow - crow;
|
|
653
|
+
if (rowsDown > 0) process.stdout.write(`\x1B[${rowsDown}B`);
|
|
654
|
+
process.stdout.write("\r\n");
|
|
655
|
+
lines.length = 0;
|
|
656
|
+
lines.push("");
|
|
657
|
+
crow = 0;
|
|
658
|
+
ccol = 0;
|
|
659
|
+
lastTermRow = 0;
|
|
660
|
+
render();
|
|
661
|
+
} else {
|
|
662
|
+
process.stdout.write("\r\n");
|
|
663
|
+
finish(null);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
i += seqLen;
|
|
667
|
+
continue;
|
|
668
|
+
case 100:
|
|
669
|
+
if (fullText()) {
|
|
670
|
+
doDelete();
|
|
671
|
+
} else {
|
|
672
|
+
process.stdout.write("\n");
|
|
673
|
+
finish(null);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
i += seqLen;
|
|
677
|
+
continue;
|
|
678
|
+
case 97:
|
|
679
|
+
ccol = 0;
|
|
680
|
+
render();
|
|
681
|
+
i += seqLen;
|
|
682
|
+
continue;
|
|
683
|
+
case 101:
|
|
684
|
+
ccol = lines[crow].length;
|
|
685
|
+
render();
|
|
686
|
+
i += seqLen;
|
|
687
|
+
continue;
|
|
688
|
+
case 117:
|
|
689
|
+
lines[crow] = lines[crow].slice(ccol);
|
|
690
|
+
ccol = 0;
|
|
691
|
+
render();
|
|
692
|
+
i += seqLen;
|
|
693
|
+
continue;
|
|
694
|
+
case 107:
|
|
695
|
+
lines[crow] = lines[crow].slice(0, ccol);
|
|
696
|
+
render();
|
|
697
|
+
i += seqLen;
|
|
698
|
+
continue;
|
|
699
|
+
case 119:
|
|
700
|
+
{
|
|
701
|
+
const before = lines[crow].slice(0, ccol);
|
|
702
|
+
const stripped = before.replace(/\s+$/, "");
|
|
703
|
+
const sp = stripped.lastIndexOf(" ");
|
|
704
|
+
const newBefore = sp >= 0 ? stripped.slice(0, sp + 1) : "";
|
|
705
|
+
lines[crow] = newBefore + lines[crow].slice(ccol);
|
|
706
|
+
ccol = newBefore.length;
|
|
707
|
+
render();
|
|
708
|
+
}
|
|
709
|
+
i += seqLen;
|
|
710
|
+
continue;
|
|
711
|
+
case 108:
|
|
712
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
713
|
+
lastTermRow = 0;
|
|
714
|
+
render();
|
|
715
|
+
i += seqLen;
|
|
716
|
+
continue;
|
|
717
|
+
default:
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
i += seqLen;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
602
724
|
let j = i + 2;
|
|
603
725
|
while (j < str.length && str.charCodeAt(j) >= 48 && str.charCodeAt(j) <= 63) j++;
|
|
604
726
|
if (j < str.length) j++;
|
|
@@ -677,6 +799,13 @@ function readMultilineInput(sessionId) {
|
|
|
677
799
|
i++;
|
|
678
800
|
continue;
|
|
679
801
|
}
|
|
802
|
+
if (str[i] === "\f") {
|
|
803
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
804
|
+
lastTermRow = 0;
|
|
805
|
+
render();
|
|
806
|
+
i++;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
680
809
|
if (str[i] === "\n") {
|
|
681
810
|
insertNewline();
|
|
682
811
|
i++;
|
|
@@ -819,7 +948,8 @@ async function chat(explicitSession) {
|
|
|
819
948
|
process.exit(0);
|
|
820
949
|
};
|
|
821
950
|
while (true) {
|
|
822
|
-
const
|
|
951
|
+
const gitCtx = getGitContext();
|
|
952
|
+
const text = await readMultilineInput(current.id, projectName, gitCtx);
|
|
823
953
|
if (text === null) exitChat();
|
|
824
954
|
if (!text) continue;
|
|
825
955
|
const trimmed = text.trim();
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -2,11 +2,40 @@ import fs from "fs/promises";
|
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import readline from "readline";
|
|
5
|
-
import { execFile } from "child_process";
|
|
5
|
+
import { execFile, execFileSync } from "child_process";
|
|
6
6
|
import { promisify } from "util";
|
|
7
7
|
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
9
|
|
|
10
|
+
// Git context for separator line (cached, refreshed periodically)
|
|
11
|
+
function getGitContext(): { branch: string; dirty: string } | null {
|
|
12
|
+
try {
|
|
13
|
+
const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
14
|
+
encoding: "utf8", timeout: 500, stdio: ["pipe", "pipe", "pipe"],
|
|
15
|
+
}).trim();
|
|
16
|
+
// Status indicators matching Copilot CLI: * unstaged, + staged, % untracked
|
|
17
|
+
let dirty = "";
|
|
18
|
+
try {
|
|
19
|
+
const status = execFileSync("git", ["status", "--porcelain", "-uno"], {
|
|
20
|
+
encoding: "utf8", timeout: 500, stdio: ["pipe", "pipe", "pipe"],
|
|
21
|
+
}).trim();
|
|
22
|
+
if (status) {
|
|
23
|
+
const hasStaged = status.split("\n").some(l => l[0] !== " " && l[0] !== "?");
|
|
24
|
+
const hasUnstaged = status.split("\n").some(l => l[1] === "M" || l[1] === "D");
|
|
25
|
+
if (hasStaged) dirty += "+";
|
|
26
|
+
if (hasUnstaged) dirty += "*";
|
|
27
|
+
}
|
|
28
|
+
const untracked = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
29
|
+
encoding: "utf8", timeout: 500, stdio: ["pipe", "pipe", "pipe"],
|
|
30
|
+
}).trim();
|
|
31
|
+
if (untracked) dirty += "%";
|
|
32
|
+
} catch { /* ignore status errors */ }
|
|
33
|
+
return { branch, dirty };
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
10
39
|
// Resolved at startup by resolveRoot() — git-aware project root discovery
|
|
11
40
|
let GROUNDCREW_DIR = ".groundcrew";
|
|
12
41
|
let SESSIONS_DIR = path.join(GROUNDCREW_DIR, "sessions");
|
|
@@ -578,7 +607,7 @@ const CHAT_COMMANDS: Array<{ cmd: string; desc: string }> = [
|
|
|
578
607
|
* Tab: slash command completion
|
|
579
608
|
* Paste: bracketed paste with multiline support
|
|
580
609
|
*/
|
|
581
|
-
function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
610
|
+
function readMultilineInput(sessionId: string, projectName: string, gitCtx: { branch: string; dirty: string } | null): Promise<string | null> {
|
|
582
611
|
return new Promise((resolve) => {
|
|
583
612
|
const lines: string[] = [""];
|
|
584
613
|
let crow = 0; // cursor row in lines[]
|
|
@@ -601,9 +630,15 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
601
630
|
if (lastTermRow > 0) buf.push(`\x1b[${lastTermRow}A`);
|
|
602
631
|
buf.push("\r\x1b[J"); // col 0 + clear to end of screen
|
|
603
632
|
|
|
604
|
-
// Separator line: ─── sessionId
|
|
633
|
+
// Separator line: ─── sessionId projectName git:(branch*) ───
|
|
605
634
|
const termW = process.stdout.columns || 80;
|
|
606
|
-
|
|
635
|
+
let info = ` ${sessionId} `;
|
|
636
|
+
const ctxParts: string[] = [];
|
|
637
|
+
if (projectName) ctxParts.push(projectName);
|
|
638
|
+
if (gitCtx) {
|
|
639
|
+
ctxParts.push(`git:(${gitCtx.branch}${gitCtx.dirty})`);
|
|
640
|
+
}
|
|
641
|
+
if (ctxParts.length) info += ` ${ctxParts.join(" ")} `;
|
|
607
642
|
const dashRight = "─".repeat(Math.max(0, termW - 4 - info.length));
|
|
608
643
|
buf.push(dim("───" + info + dashRight));
|
|
609
644
|
|
|
@@ -748,8 +783,63 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
748
783
|
// End (\x1b[F)
|
|
749
784
|
if (str.startsWith("\x1b[F", i)) { ccol = lines[crow].length; render(); i += 3; continue; }
|
|
750
785
|
|
|
751
|
-
//
|
|
786
|
+
// CSI u (Kitty keyboard protocol) — decode \x1b[{codepoint};{modifier}u
|
|
787
|
+
// modifier bit 3 (value 4) = Ctrl, sent as bits+1 so modifier 5 = Ctrl
|
|
752
788
|
if (str[i] === "\x1b" && i + 1 < str.length && str[i + 1] === "[") {
|
|
789
|
+
const csiMatch = str.slice(i).match(/^\x1b\[(\d+);(\d+)u/);
|
|
790
|
+
if (csiMatch) {
|
|
791
|
+
const codepoint = parseInt(csiMatch[1], 10);
|
|
792
|
+
const modifier = parseInt(csiMatch[2], 10);
|
|
793
|
+
const isCtrl = (modifier - 1) & 4;
|
|
794
|
+
const seqLen = csiMatch[0].length;
|
|
795
|
+
|
|
796
|
+
if (isCtrl) {
|
|
797
|
+
switch (codepoint) {
|
|
798
|
+
case 99: // Ctrl+C
|
|
799
|
+
if (fullText() || lines.length > 1 || lines[0].length > 0) {
|
|
800
|
+
const lastRow = lines.length - 1;
|
|
801
|
+
const rowsDown = lastRow - crow;
|
|
802
|
+
if (rowsDown > 0) process.stdout.write(`\x1b[${rowsDown}B`);
|
|
803
|
+
process.stdout.write("\r\n");
|
|
804
|
+
lines.length = 0; lines.push("");
|
|
805
|
+
crow = 0; ccol = 0; lastTermRow = 0;
|
|
806
|
+
render();
|
|
807
|
+
} else {
|
|
808
|
+
process.stdout.write("\r\n");
|
|
809
|
+
finish(null); return;
|
|
810
|
+
}
|
|
811
|
+
i += seqLen; continue;
|
|
812
|
+
case 100: // Ctrl+D
|
|
813
|
+
if (fullText()) { doDelete(); } else { process.stdout.write("\n"); finish(null); return; }
|
|
814
|
+
i += seqLen; continue;
|
|
815
|
+
case 97: // Ctrl+A — home
|
|
816
|
+
ccol = 0; render(); i += seqLen; continue;
|
|
817
|
+
case 101: // Ctrl+E — end
|
|
818
|
+
ccol = lines[crow].length; render(); i += seqLen; continue;
|
|
819
|
+
case 117: // Ctrl+U — clear before cursor
|
|
820
|
+
lines[crow] = lines[crow].slice(ccol); ccol = 0; render(); i += seqLen; continue;
|
|
821
|
+
case 107: // Ctrl+K — clear after cursor
|
|
822
|
+
lines[crow] = lines[crow].slice(0, ccol); render(); i += seqLen; continue;
|
|
823
|
+
case 119: // Ctrl+W — delete word before cursor
|
|
824
|
+
{ const before = lines[crow].slice(0, ccol);
|
|
825
|
+
const stripped = before.replace(/\s+$/, "");
|
|
826
|
+
const sp = stripped.lastIndexOf(" ");
|
|
827
|
+
const newBefore = sp >= 0 ? stripped.slice(0, sp + 1) : "";
|
|
828
|
+
lines[crow] = newBefore + lines[crow].slice(ccol);
|
|
829
|
+
ccol = newBefore.length; render(); }
|
|
830
|
+
i += seqLen; continue;
|
|
831
|
+
case 108: // Ctrl+L — clear screen
|
|
832
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
833
|
+
lastTermRow = 0; render();
|
|
834
|
+
i += seqLen; continue;
|
|
835
|
+
default: break;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// Unhandled CSI u — skip it
|
|
839
|
+
i += seqLen; continue;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Skip unknown CSI sequences (non-u final byte)
|
|
753
843
|
let j = i + 2;
|
|
754
844
|
while (j < str.length && str.charCodeAt(j) >= 0x30 && str.charCodeAt(j) <= 0x3f) j++;
|
|
755
845
|
if (j < str.length) j++; // skip final byte
|
|
@@ -804,6 +894,11 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
804
894
|
lines[crow] = newBefore + lines[crow].slice(ccol);
|
|
805
895
|
ccol = newBefore.length; render(); i++; continue;
|
|
806
896
|
}
|
|
897
|
+
// Ctrl+L — clear screen (legacy byte)
|
|
898
|
+
if (str[i] === "\x0c") {
|
|
899
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
900
|
+
lastTermRow = 0; render(); i++; continue;
|
|
901
|
+
}
|
|
807
902
|
|
|
808
903
|
// Ctrl+J (LF, 0x0A) — newline (cross-terminal)
|
|
809
904
|
if (str[i] === "\n") { insertNewline(); i++; continue; }
|
|
@@ -963,7 +1058,9 @@ async function chat(explicitSession?: string): Promise<void> {
|
|
|
963
1058
|
|
|
964
1059
|
// ── Main chat loop ─────────────────────────────────────────────────────────────────
|
|
965
1060
|
while (true) {
|
|
966
|
-
|
|
1061
|
+
// Refresh git context each turn (branch may change between prompts)
|
|
1062
|
+
const gitCtx = getGitContext();
|
|
1063
|
+
const text = await readMultilineInput(current.id, projectName, gitCtx);
|
|
967
1064
|
|
|
968
1065
|
if (text === null) exitChat();
|
|
969
1066
|
if (!text) continue;
|