groundcrew-cli 0.16.5 → 0.17.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 +64 -72
- package/package.json +1 -1
- package/src/index.ts +88 -85
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import{createRequire}from'module';const require=createRequire(import.meta.url);
|
|
|
4
4
|
import fs from "fs/promises";
|
|
5
5
|
import { existsSync } from "fs";
|
|
6
6
|
import path from "path";
|
|
7
|
+
import os from "os";
|
|
7
8
|
import readline from "readline";
|
|
8
9
|
import { execFile, execFileSync } from "child_process";
|
|
9
10
|
import { promisify } from "util";
|
|
@@ -73,35 +74,21 @@ close access fp`], { timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] });
|
|
|
73
74
|
return null;
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
|
-
var
|
|
77
|
-
var SESSIONS_DIR = path.join(
|
|
78
|
-
var ACTIVE_SESSIONS_FILE = path.join(
|
|
79
|
-
var HISTORY_FILE = path.join(
|
|
77
|
+
var GROUNDCREW_HOME = path.join(os.homedir(), ".groundcrew");
|
|
78
|
+
var SESSIONS_DIR = path.join(GROUNDCREW_HOME, "sessions");
|
|
79
|
+
var ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_HOME, "active-sessions.json");
|
|
80
|
+
var HISTORY_FILE = path.join(GROUNDCREW_HOME, "history.json");
|
|
81
|
+
var REPO_NAME = "";
|
|
80
82
|
async function resolveRoot() {
|
|
81
83
|
let root = null;
|
|
82
84
|
try {
|
|
83
85
|
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
|
|
84
|
-
|
|
85
|
-
if (gitRoot && existsSync(path.join(gitRoot, ".groundcrew"))) {
|
|
86
|
-
root = gitRoot;
|
|
87
|
-
}
|
|
86
|
+
root = stdout.trim() || null;
|
|
88
87
|
} catch {
|
|
89
88
|
}
|
|
90
|
-
if (!root) {
|
|
91
|
-
let dir = process.cwd();
|
|
92
|
-
while (dir !== path.dirname(dir)) {
|
|
93
|
-
if (existsSync(path.join(dir, ".groundcrew"))) {
|
|
94
|
-
root = dir;
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
dir = path.dirname(dir);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
89
|
if (!root) root = process.cwd();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_DIR, "active-sessions.json");
|
|
104
|
-
HISTORY_FILE = path.join(GROUNDCREW_DIR, "history.json");
|
|
90
|
+
REPO_NAME = path.basename(root).replace(/[^a-zA-Z0-9_-]/g, "_") || "unknown";
|
|
91
|
+
await fs.mkdir(SESSIONS_DIR, { recursive: true });
|
|
105
92
|
}
|
|
106
93
|
async function readActiveSessions() {
|
|
107
94
|
try {
|
|
@@ -110,6 +97,9 @@ async function readActiveSessions() {
|
|
|
110
97
|
return {};
|
|
111
98
|
}
|
|
112
99
|
}
|
|
100
|
+
function isRepoSession(sessionId) {
|
|
101
|
+
return sessionId.startsWith(REPO_NAME + "-");
|
|
102
|
+
}
|
|
113
103
|
async function resolveSessionDir(explicitSession) {
|
|
114
104
|
if (explicitSession) {
|
|
115
105
|
const dir = path.join(SESSIONS_DIR, explicitSession);
|
|
@@ -119,10 +109,10 @@ async function resolveSessionDir(explicitSession) {
|
|
|
119
109
|
return dir;
|
|
120
110
|
}
|
|
121
111
|
const sessions2 = await readActiveSessions();
|
|
122
|
-
const ids = Object.keys(sessions2);
|
|
112
|
+
const ids = Object.keys(sessions2).filter(isRepoSession);
|
|
123
113
|
if (ids.length === 0) {
|
|
124
114
|
try {
|
|
125
|
-
const dirs = await fs.readdir(SESSIONS_DIR);
|
|
115
|
+
const dirs = (await fs.readdir(SESSIONS_DIR)).filter(isRepoSession);
|
|
126
116
|
if (dirs.length === 1) return path.join(SESSIONS_DIR, dirs[0]);
|
|
127
117
|
if (dirs.length > 1) {
|
|
128
118
|
let latest2 = { dir: dirs[0], mtime: 0 };
|
|
@@ -139,7 +129,7 @@ async function resolveSessionDir(explicitSession) {
|
|
|
139
129
|
}
|
|
140
130
|
} catch {
|
|
141
131
|
}
|
|
142
|
-
throw new Error(
|
|
132
|
+
throw new Error(`No active sessions for repo "${REPO_NAME}". Start Copilot with groundcrew first.`);
|
|
143
133
|
}
|
|
144
134
|
if (ids.length === 1) {
|
|
145
135
|
return path.join(SESSIONS_DIR, ids[0]);
|
|
@@ -185,10 +175,6 @@ var cyan = (t) => color(36, t);
|
|
|
185
175
|
var dim = (t) => color(2, t);
|
|
186
176
|
var bold = (t) => color(1, t);
|
|
187
177
|
var red = (t) => color(31, t);
|
|
188
|
-
async function init() {
|
|
189
|
-
await fs.mkdir(SESSIONS_DIR, { recursive: true });
|
|
190
|
-
console.log(green("Groundcrew initialized.") + ` ${dim(GROUNDCREW_DIR + "/ created")}`);
|
|
191
|
-
}
|
|
192
178
|
async function add(taskText, priority, sessionDir) {
|
|
193
179
|
const queue = await readQueue(sessionDir);
|
|
194
180
|
const task = {
|
|
@@ -308,23 +294,35 @@ async function sessions() {
|
|
|
308
294
|
console.log(dim("No sessions found."));
|
|
309
295
|
return;
|
|
310
296
|
}
|
|
311
|
-
|
|
297
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
312
298
|
for (const dir of allDirs) {
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
299
|
+
const dashIdx = dir.lastIndexOf("-");
|
|
300
|
+
const repo = dashIdx > 0 ? dir.substring(0, dashIdx) : "unknown";
|
|
301
|
+
if (!byRepo.has(repo)) byRepo.set(repo, []);
|
|
302
|
+
byRepo.get(repo).push(dir);
|
|
303
|
+
}
|
|
304
|
+
console.log(bold("Sessions:\n"));
|
|
305
|
+
for (const [repo, dirs] of byRepo) {
|
|
306
|
+
const isCurrent = repo === REPO_NAME;
|
|
307
|
+
console.log(` ${isCurrent ? green(repo) : dim(repo)}`);
|
|
308
|
+
for (const dir of dirs) {
|
|
309
|
+
const isActive = ids.includes(dir);
|
|
310
|
+
const sessionDir = path.join(SESSIONS_DIR, dir);
|
|
311
|
+
let info = "";
|
|
312
|
+
try {
|
|
313
|
+
const session = JSON.parse(await fs.readFile(path.join(sessionDir, "session.json"), "utf-8"));
|
|
314
|
+
const startTime = new Date(session.started).getTime();
|
|
315
|
+
const minutes = Math.round((Date.now() - startTime) / 6e4);
|
|
316
|
+
const statusColor = session.status === "active" ? green : session.status === "parked" ? yellow : dim;
|
|
317
|
+
info = `${statusColor(session.status)} | ${minutes}min | ${session.tasksCompleted || 0} tasks done`;
|
|
318
|
+
} catch {
|
|
319
|
+
info = dim("no session data");
|
|
320
|
+
}
|
|
321
|
+
const queue = await readQueue(sessionDir);
|
|
322
|
+
const marker = isActive ? green("*") : " ";
|
|
323
|
+
const shortId = dir.substring(repo.length + 1);
|
|
324
|
+
console.log(` ${marker} ${cyan(shortId)} ${info} | ${queue.tasks.length} queued`);
|
|
324
325
|
}
|
|
325
|
-
const queue = await readQueue(sessionDir);
|
|
326
|
-
const marker = isActive ? green("*") : " ";
|
|
327
|
-
console.log(` ${marker} ${cyan(dir)} ${info} | ${queue.tasks.length} queued`);
|
|
328
326
|
}
|
|
329
327
|
if (ids.length > 0) {
|
|
330
328
|
console.log(dim(`
|
|
@@ -381,14 +379,14 @@ async function destroyOne(sessionId) {
|
|
|
381
379
|
}
|
|
382
380
|
async function stopAll() {
|
|
383
381
|
const active = await readActiveSessions();
|
|
384
|
-
const ids = Object.keys(active);
|
|
382
|
+
const ids = Object.keys(active).filter(isRepoSession);
|
|
385
383
|
let allDirs = [];
|
|
386
384
|
try {
|
|
387
|
-
allDirs = await fs.readdir(SESSIONS_DIR);
|
|
385
|
+
allDirs = (await fs.readdir(SESSIONS_DIR)).filter(isRepoSession);
|
|
388
386
|
} catch {
|
|
389
387
|
}
|
|
390
388
|
if (ids.length === 0 && allDirs.length === 0) {
|
|
391
|
-
console.log(dim(
|
|
389
|
+
console.log(dim(`No sessions to stop for repo "${REPO_NAME}".`));
|
|
392
390
|
return;
|
|
393
391
|
}
|
|
394
392
|
for (const id of ids) {
|
|
@@ -404,36 +402,32 @@ async function stopAll() {
|
|
|
404
402
|
console.log(` ${green("cleaned")} ${cyan(dir)} ${dim("(orphaned)")}`);
|
|
405
403
|
}
|
|
406
404
|
}
|
|
407
|
-
await
|
|
408
|
-
|
|
405
|
+
const remaining = await readActiveSessions();
|
|
406
|
+
for (const id of ids) delete remaining[id];
|
|
407
|
+
await fs.writeFile(ACTIVE_SESSIONS_FILE, JSON.stringify(remaining, null, 2));
|
|
408
|
+
console.log(green(`
|
|
409
|
+
All ${REPO_NAME} sessions stopped.`));
|
|
409
410
|
}
|
|
410
411
|
async function destroyAll() {
|
|
411
412
|
await stopAll();
|
|
412
413
|
try {
|
|
413
|
-
const dirs = await fs.readdir(SESSIONS_DIR);
|
|
414
|
+
const dirs = (await fs.readdir(SESSIONS_DIR)).filter(isRepoSession);
|
|
414
415
|
for (const dir of dirs) {
|
|
415
416
|
await fs.rm(path.join(SESSIONS_DIR, dir), { recursive: true, force: true });
|
|
416
417
|
}
|
|
417
418
|
} catch {
|
|
418
419
|
}
|
|
419
420
|
try {
|
|
420
|
-
await fs.unlink(
|
|
421
|
-
} catch {
|
|
422
|
-
}
|
|
423
|
-
try {
|
|
424
|
-
await fs.unlink(ACTIVE_SESSIONS_FILE);
|
|
425
|
-
} catch {
|
|
426
|
-
}
|
|
427
|
-
try {
|
|
428
|
-
await fs.unlink(path.join(GROUNDCREW_DIR, "tool-history.csv"));
|
|
421
|
+
await fs.unlink(path.join(GROUNDCREW_HOME, "tool-history.csv"));
|
|
429
422
|
} catch {
|
|
430
423
|
}
|
|
431
|
-
console.log(green(
|
|
424
|
+
console.log(green(`All ${REPO_NAME} session data deleted.`));
|
|
432
425
|
}
|
|
433
426
|
async function listSessionChoices() {
|
|
434
427
|
const active = await readActiveSessions();
|
|
435
428
|
const choices = [];
|
|
436
429
|
for (const [id, entry] of Object.entries(active)) {
|
|
430
|
+
if (!isRepoSession(id)) continue;
|
|
437
431
|
const dir = path.join(SESSIONS_DIR, id);
|
|
438
432
|
let status2 = "active";
|
|
439
433
|
let minutes = 0;
|
|
@@ -458,7 +452,7 @@ async function listSessionChoices() {
|
|
|
458
452
|
async function pickSession(rl) {
|
|
459
453
|
const choices = await listSessionChoices();
|
|
460
454
|
if (choices.length === 0) {
|
|
461
|
-
console.log(red(
|
|
455
|
+
console.log(red(`No active sessions for repo "${REPO_NAME}". Start Copilot with groundcrew first.`));
|
|
462
456
|
return null;
|
|
463
457
|
}
|
|
464
458
|
if (choices.length === 1) {
|
|
@@ -500,6 +494,7 @@ function readMultilineInput(sessionId, projectName, gitCtx, sessionDir) {
|
|
|
500
494
|
let crow = 0;
|
|
501
495
|
let ccol = 0;
|
|
502
496
|
const padWidth = sessionId.length + 5;
|
|
497
|
+
const linePad = (i) => i === 0 ? padWidth : 0;
|
|
503
498
|
let lastTermRow = 0;
|
|
504
499
|
let pasteBuffer = "";
|
|
505
500
|
let isPasting = false;
|
|
@@ -523,7 +518,7 @@ function readMultilineInput(sessionId, projectName, gitCtx, sessionDir) {
|
|
|
523
518
|
if (i === 0) {
|
|
524
519
|
buf.push(dim(`[${sessionId}]`) + " " + bold(">") + " " + lines[i]);
|
|
525
520
|
} else {
|
|
526
|
-
buf.push(
|
|
521
|
+
buf.push(lines[i]);
|
|
527
522
|
}
|
|
528
523
|
}
|
|
529
524
|
let suggestionRows = 0;
|
|
@@ -540,17 +535,18 @@ function readMultilineInput(sessionId, projectName, gitCtx, sessionDir) {
|
|
|
540
535
|
}
|
|
541
536
|
const lastRow = lines.length - 1;
|
|
542
537
|
const termRowsForLine = (i) => {
|
|
543
|
-
const lineLen = (i
|
|
538
|
+
const lineLen = linePad(i) + lines[i].length;
|
|
544
539
|
return lineLen === 0 ? 1 : Math.max(1, Math.ceil(lineLen / termW));
|
|
545
540
|
};
|
|
546
541
|
let rowsBelowCursor = suggestionRows;
|
|
547
542
|
for (let i = lastRow; i > crow; i--) rowsBelowCursor += termRowsForLine(i);
|
|
548
543
|
const cursorLineTermRows = termRowsForLine(crow);
|
|
549
|
-
const
|
|
544
|
+
const cursorPad = linePad(crow);
|
|
545
|
+
const cursorRowWithinLine = Math.floor((cursorPad + ccol) / termW);
|
|
550
546
|
rowsBelowCursor += cursorLineTermRows - 1 - cursorRowWithinLine;
|
|
551
547
|
if (rowsBelowCursor > 0) buf.push(`\x1B[${rowsBelowCursor}A`);
|
|
552
548
|
buf.push("\r");
|
|
553
|
-
const col = (
|
|
549
|
+
const col = (cursorPad + ccol) % termW;
|
|
554
550
|
if (col > 0) buf.push(`\x1B[${col}C`);
|
|
555
551
|
let rowsAbove = 1;
|
|
556
552
|
for (let i = 0; i < crow; i++) rowsAbove += termRowsForLine(i);
|
|
@@ -572,7 +568,7 @@ function readMultilineInput(sessionId, projectName, gitCtx, sessionDir) {
|
|
|
572
568
|
if (i === 0) {
|
|
573
569
|
buf.push(dim(`[${sessionId}]`) + " " + bold(">") + " " + lines[i]);
|
|
574
570
|
} else {
|
|
575
|
-
buf.push(
|
|
571
|
+
buf.push(lines[i]);
|
|
576
572
|
}
|
|
577
573
|
}
|
|
578
574
|
buf.push("\n");
|
|
@@ -1114,7 +1110,6 @@ ${bold("groundcrew")} \u2014 CLI companion for the Groundcrew Copilot plugin
|
|
|
1114
1110
|
${bold("Usage:")}
|
|
1115
1111
|
groundcrew chat Interactive chat mode (recommended)
|
|
1116
1112
|
groundcrew chat --session <id> Chat with a specific session
|
|
1117
|
-
groundcrew init Initialize .groundcrew/ in current dir
|
|
1118
1113
|
groundcrew add <task> Add a task to the queue
|
|
1119
1114
|
groundcrew add --priority <task> Add an urgent task (processed first)
|
|
1120
1115
|
groundcrew add --session <id> <task> Add to a specific session
|
|
@@ -1125,9 +1120,9 @@ ${bold("Usage:")}
|
|
|
1125
1120
|
groundcrew sessions List all sessions
|
|
1126
1121
|
groundcrew history Show completed tasks
|
|
1127
1122
|
groundcrew clear Clear all pending tasks
|
|
1128
|
-
groundcrew stop Stop all
|
|
1123
|
+
groundcrew stop Stop all sessions for current repo
|
|
1129
1124
|
groundcrew stop --session <id> Stop a specific session
|
|
1130
|
-
groundcrew destroy Delete all sessions
|
|
1125
|
+
groundcrew destroy Delete all sessions for current repo
|
|
1131
1126
|
groundcrew destroy --session <id> Delete a specific session
|
|
1132
1127
|
|
|
1133
1128
|
${bold("Session targeting:")}
|
|
@@ -1159,9 +1154,6 @@ async function main() {
|
|
|
1159
1154
|
const { value: explicitSession, remaining: args } = extractFlag(rawArgs, "--session");
|
|
1160
1155
|
const command = args[0];
|
|
1161
1156
|
switch (command) {
|
|
1162
|
-
case "init":
|
|
1163
|
-
await init();
|
|
1164
|
-
return;
|
|
1165
1157
|
case "chat":
|
|
1166
1158
|
await chat(explicitSession);
|
|
1167
1159
|
return;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import os from "os";
|
|
4
5
|
import readline from "readline";
|
|
5
6
|
import { execFile, execFileSync } from "child_process";
|
|
6
7
|
import { promisify } from "util";
|
|
@@ -68,17 +69,16 @@ close access fp`], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
|
|
|
68
69
|
} catch { return null; }
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
// ── Centralized storage at ~/.groundcrew ─────────────────────────────────────
|
|
73
|
+
const GROUNDCREW_HOME = path.join(os.homedir(), ".groundcrew");
|
|
74
|
+
const SESSIONS_DIR = path.join(GROUNDCREW_HOME, "sessions");
|
|
75
|
+
const ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_HOME, "active-sessions.json");
|
|
76
|
+
const HISTORY_FILE = path.join(GROUNDCREW_HOME, "history.json");
|
|
77
|
+
|
|
78
|
+
let REPO_NAME = "";
|
|
76
79
|
|
|
77
80
|
/**
|
|
78
|
-
*
|
|
79
|
-
* Uses git rev-parse --show-toplevel for worktree support, then walks up
|
|
80
|
-
* from CWD as fallback. This ensures the CLI works from subdirectories
|
|
81
|
-
* and git worktrees.
|
|
81
|
+
* Derive repo name from CWD (git-aware for worktree/subdirectory support).
|
|
82
82
|
*/
|
|
83
83
|
async function resolveRoot(): Promise<void> {
|
|
84
84
|
let root: string | null = null;
|
|
@@ -86,31 +86,16 @@ async function resolveRoot(): Promise<void> {
|
|
|
86
86
|
// 1. Try git rev-parse --show-toplevel (worktree-aware)
|
|
87
87
|
try {
|
|
88
88
|
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
|
|
89
|
-
|
|
90
|
-
if (gitRoot && existsSync(path.join(gitRoot, ".groundcrew"))) {
|
|
91
|
-
root = gitRoot;
|
|
92
|
-
}
|
|
89
|
+
root = stdout.trim() || null;
|
|
93
90
|
} catch { /* not a git repo or git not installed */ }
|
|
94
91
|
|
|
95
|
-
// 2.
|
|
96
|
-
if (!root) {
|
|
97
|
-
let dir = process.cwd();
|
|
98
|
-
while (dir !== path.dirname(dir)) {
|
|
99
|
-
if (existsSync(path.join(dir, ".groundcrew"))) {
|
|
100
|
-
root = dir;
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
dir = path.dirname(dir);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// 3. Fallback to CWD (for `groundcrew init`)
|
|
92
|
+
// 2. Fallback to CWD
|
|
108
93
|
if (!root) root = process.cwd();
|
|
109
94
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
95
|
+
REPO_NAME = path.basename(root).replace(/[^a-zA-Z0-9_-]/g, "_") || "unknown";
|
|
96
|
+
|
|
97
|
+
// Ensure centralized dirs exist
|
|
98
|
+
await fs.mkdir(SESSIONS_DIR, { recursive: true });
|
|
114
99
|
}
|
|
115
100
|
|
|
116
101
|
interface Task {
|
|
@@ -147,9 +132,17 @@ async function readActiveSessions(): Promise<Record<string, ActiveSessionEntry>>
|
|
|
147
132
|
}
|
|
148
133
|
}
|
|
149
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Filter session IDs that belong to the current repo.
|
|
137
|
+
* Session IDs are prefixed with repo name: "mekari_credit-a1b2c3d4"
|
|
138
|
+
*/
|
|
139
|
+
function isRepoSession(sessionId: string): boolean {
|
|
140
|
+
return sessionId.startsWith(REPO_NAME + "-");
|
|
141
|
+
}
|
|
142
|
+
|
|
150
143
|
/**
|
|
151
144
|
* Resolve which session to target.
|
|
152
|
-
* Priority: --session flag > single active session > error if ambiguous.
|
|
145
|
+
* Priority: --session flag > single active repo session > error if ambiguous.
|
|
153
146
|
*/
|
|
154
147
|
async function resolveSessionDir(explicitSession?: string): Promise<string> {
|
|
155
148
|
if (explicitSession) {
|
|
@@ -161,12 +154,13 @@ async function resolveSessionDir(explicitSession?: string): Promise<string> {
|
|
|
161
154
|
}
|
|
162
155
|
|
|
163
156
|
const sessions = await readActiveSessions();
|
|
164
|
-
|
|
157
|
+
// Filter to sessions for THIS repo
|
|
158
|
+
const ids = Object.keys(sessions).filter(isRepoSession);
|
|
165
159
|
|
|
166
160
|
if (ids.length === 0) {
|
|
167
|
-
// Fallback: check
|
|
161
|
+
// Fallback: check session dirs on disk for this repo
|
|
168
162
|
try {
|
|
169
|
-
const dirs = await fs.readdir(SESSIONS_DIR);
|
|
163
|
+
const dirs = (await fs.readdir(SESSIONS_DIR)).filter(isRepoSession);
|
|
170
164
|
if (dirs.length === 1) return path.join(SESSIONS_DIR, dirs[0]);
|
|
171
165
|
if (dirs.length > 1) {
|
|
172
166
|
// Pick most recently modified
|
|
@@ -182,7 +176,7 @@ async function resolveSessionDir(explicitSession?: string): Promise<string> {
|
|
|
182
176
|
return path.join(SESSIONS_DIR, latest.dir);
|
|
183
177
|
}
|
|
184
178
|
} catch { /* no sessions dir */ }
|
|
185
|
-
throw new Error(
|
|
179
|
+
throw new Error(`No active sessions for repo "${REPO_NAME}". Start Copilot with groundcrew first.`);
|
|
186
180
|
}
|
|
187
181
|
|
|
188
182
|
if (ids.length === 1) {
|
|
@@ -244,11 +238,6 @@ const red = (t: string) => color(31, t);
|
|
|
244
238
|
|
|
245
239
|
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
246
240
|
|
|
247
|
-
async function init(): Promise<void> {
|
|
248
|
-
await fs.mkdir(SESSIONS_DIR, { recursive: true });
|
|
249
|
-
console.log(green("Groundcrew initialized.") + ` ${dim(GROUNDCREW_DIR + "/ created")}`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
241
|
async function add(taskText: string, priority: number, sessionDir: string): Promise<void> {
|
|
253
242
|
const queue = await readQueue(sessionDir);
|
|
254
243
|
const task: Task = {
|
|
@@ -397,27 +386,43 @@ async function sessions(): Promise<void> {
|
|
|
397
386
|
return;
|
|
398
387
|
}
|
|
399
388
|
|
|
389
|
+
// Group by repo prefix
|
|
390
|
+
const byRepo = new Map<string, string[]>();
|
|
391
|
+
for (const dir of allDirs) {
|
|
392
|
+
const dashIdx = dir.lastIndexOf("-");
|
|
393
|
+
const repo = dashIdx > 0 ? dir.substring(0, dashIdx) : "unknown";
|
|
394
|
+
if (!byRepo.has(repo)) byRepo.set(repo, []);
|
|
395
|
+
byRepo.get(repo)!.push(dir);
|
|
396
|
+
}
|
|
397
|
+
|
|
400
398
|
console.log(bold("Sessions:\n"));
|
|
401
399
|
|
|
402
|
-
for (const
|
|
403
|
-
const
|
|
404
|
-
|
|
400
|
+
for (const [repo, dirs] of byRepo) {
|
|
401
|
+
const isCurrent = repo === REPO_NAME;
|
|
402
|
+
console.log(` ${isCurrent ? green(repo) : dim(repo)}`);
|
|
405
403
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
const startTime = new Date(session.started).getTime();
|
|
410
|
-
const minutes = Math.round((Date.now() - startTime) / 60000);
|
|
411
|
-
const statusColor = session.status === "active" ? green : session.status === "parked" ? yellow : dim;
|
|
412
|
-
info = `${statusColor(session.status)} | ${minutes}min | ${session.tasksCompleted || 0} tasks done`;
|
|
413
|
-
} catch {
|
|
414
|
-
info = dim("no session data");
|
|
415
|
-
}
|
|
404
|
+
for (const dir of dirs) {
|
|
405
|
+
const isActive = ids.includes(dir);
|
|
406
|
+
const sessionDir = path.join(SESSIONS_DIR, dir);
|
|
416
407
|
|
|
417
|
-
|
|
418
|
-
|
|
408
|
+
let info = "";
|
|
409
|
+
try {
|
|
410
|
+
const session = JSON.parse(await fs.readFile(path.join(sessionDir, "session.json"), "utf-8"));
|
|
411
|
+
const startTime = new Date(session.started).getTime();
|
|
412
|
+
const minutes = Math.round((Date.now() - startTime) / 60000);
|
|
413
|
+
const statusColor = session.status === "active" ? green : session.status === "parked" ? yellow : dim;
|
|
414
|
+
info = `${statusColor(session.status)} | ${minutes}min | ${session.tasksCompleted || 0} tasks done`;
|
|
415
|
+
} catch {
|
|
416
|
+
info = dim("no session data");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const queue = await readQueue(sessionDir);
|
|
420
|
+
const marker = isActive ? green("*") : " ";
|
|
421
|
+
// Show only the hex suffix for cleaner display
|
|
422
|
+
const shortId = dir.substring(repo.length + 1);
|
|
419
423
|
|
|
420
|
-
|
|
424
|
+
console.log(` ${marker} ${cyan(shortId)} ${info} | ${queue.tasks.length} queued`);
|
|
425
|
+
}
|
|
421
426
|
}
|
|
422
427
|
|
|
423
428
|
if (ids.length > 0) {
|
|
@@ -484,13 +489,14 @@ async function destroyOne(sessionId: string): Promise<void> {
|
|
|
484
489
|
|
|
485
490
|
async function stopAll(): Promise<void> {
|
|
486
491
|
const active = await readActiveSessions();
|
|
487
|
-
|
|
492
|
+
// Scope to current repo
|
|
493
|
+
const ids = Object.keys(active).filter(isRepoSession);
|
|
488
494
|
|
|
489
495
|
let allDirs: string[] = [];
|
|
490
|
-
try { allDirs = await fs.readdir(SESSIONS_DIR); } catch {}
|
|
496
|
+
try { allDirs = (await fs.readdir(SESSIONS_DIR)).filter(isRepoSession); } catch {}
|
|
491
497
|
|
|
492
498
|
if (ids.length === 0 && allDirs.length === 0) {
|
|
493
|
-
console.log(dim(
|
|
499
|
+
console.log(dim(`No sessions to stop for repo "${REPO_NAME}".`));
|
|
494
500
|
return;
|
|
495
501
|
}
|
|
496
502
|
|
|
@@ -511,33 +517,29 @@ async function stopAll(): Promise<void> {
|
|
|
511
517
|
}
|
|
512
518
|
}
|
|
513
519
|
|
|
514
|
-
//
|
|
515
|
-
await
|
|
516
|
-
|
|
520
|
+
// Remove stopped sessions from active-sessions.json (keep other repos)
|
|
521
|
+
const remaining = await readActiveSessions();
|
|
522
|
+
for (const id of ids) delete remaining[id];
|
|
523
|
+
await fs.writeFile(ACTIVE_SESSIONS_FILE, JSON.stringify(remaining, null, 2));
|
|
524
|
+
console.log(green(`\nAll ${REPO_NAME} sessions stopped.`));
|
|
517
525
|
}
|
|
518
526
|
|
|
519
527
|
async function destroyAll(): Promise<void> {
|
|
520
|
-
// Stop
|
|
528
|
+
// Stop this repo's sessions first
|
|
521
529
|
await stopAll();
|
|
522
530
|
|
|
523
|
-
// Delete
|
|
531
|
+
// Delete this repo's session directories only
|
|
524
532
|
try {
|
|
525
|
-
const dirs = await fs.readdir(SESSIONS_DIR);
|
|
533
|
+
const dirs = (await fs.readdir(SESSIONS_DIR)).filter(isRepoSession);
|
|
526
534
|
for (const dir of dirs) {
|
|
527
535
|
await fs.rm(path.join(SESSIONS_DIR, dir), { recursive: true, force: true });
|
|
528
536
|
}
|
|
529
537
|
} catch { /* no sessions dir */ }
|
|
530
538
|
|
|
531
|
-
// Delete history
|
|
532
|
-
try { await fs.unlink(HISTORY_FILE); } catch {}
|
|
533
|
-
|
|
534
|
-
// Delete active sessions file
|
|
535
|
-
try { await fs.unlink(ACTIVE_SESSIONS_FILE); } catch {}
|
|
536
|
-
|
|
537
539
|
// Delete tool history
|
|
538
|
-
try { await fs.unlink(path.join(
|
|
540
|
+
try { await fs.unlink(path.join(GROUNDCREW_HOME, "tool-history.csv")); } catch {}
|
|
539
541
|
|
|
540
|
-
console.log(green(
|
|
542
|
+
console.log(green(`All ${REPO_NAME} session data deleted.`));
|
|
541
543
|
}
|
|
542
544
|
|
|
543
545
|
// ── Chat Mode ────────────────────────────────────────────────────────────────
|
|
@@ -557,6 +559,9 @@ async function listSessionChoices(): Promise<SessionChoice[]> {
|
|
|
557
559
|
const choices: SessionChoice[] = [];
|
|
558
560
|
|
|
559
561
|
for (const [id, entry] of Object.entries(active)) {
|
|
562
|
+
// Only show sessions for the current repo
|
|
563
|
+
if (!isRepoSession(id)) continue;
|
|
564
|
+
|
|
560
565
|
const dir = path.join(SESSIONS_DIR, id);
|
|
561
566
|
let status = "active";
|
|
562
567
|
let minutes = 0;
|
|
@@ -585,7 +590,7 @@ async function pickSession(rl: readline.Interface): Promise<SessionChoice | null
|
|
|
585
590
|
const choices = await listSessionChoices();
|
|
586
591
|
|
|
587
592
|
if (choices.length === 0) {
|
|
588
|
-
console.log(red(
|
|
593
|
+
console.log(red(`No active sessions for repo "${REPO_NAME}". Start Copilot with groundcrew first.`));
|
|
589
594
|
return null;
|
|
590
595
|
}
|
|
591
596
|
|
|
@@ -647,6 +652,7 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
|
|
|
647
652
|
|
|
648
653
|
// Visible width of prompt: "[sessionId] > "
|
|
649
654
|
const padWidth = sessionId.length + 5; // [ + id + ] + space + > + space = len+5
|
|
655
|
+
const linePad = (i: number) => i === 0 ? padWidth : 0;
|
|
650
656
|
|
|
651
657
|
// Track how many rows up from cursor to top of rendered area (including separator)
|
|
652
658
|
let lastTermRow = 0;
|
|
@@ -680,7 +686,7 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
|
|
|
680
686
|
if (i === 0) {
|
|
681
687
|
buf.push(dim(`[${sessionId}]`) + " " + bold(">") + " " + lines[i]);
|
|
682
688
|
} else {
|
|
683
|
-
buf.push(
|
|
689
|
+
buf.push(lines[i]);
|
|
684
690
|
}
|
|
685
691
|
}
|
|
686
692
|
|
|
@@ -702,7 +708,7 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
|
|
|
702
708
|
|
|
703
709
|
// Calculate actual terminal rows each line occupies (for wrapped lines)
|
|
704
710
|
const termRowsForLine = (i: number): number => {
|
|
705
|
-
const lineLen = (i
|
|
711
|
+
const lineLen = linePad(i) + lines[i].length;
|
|
706
712
|
return lineLen === 0 ? 1 : Math.max(1, Math.ceil(lineLen / termW));
|
|
707
713
|
};
|
|
708
714
|
|
|
@@ -712,13 +718,14 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
|
|
|
712
718
|
for (let i = lastRow; i > crow; i--) rowsBelowCursor += termRowsForLine(i);
|
|
713
719
|
// Add any extra wrapped rows on the cursor line itself (below the cursor's row within wraps)
|
|
714
720
|
const cursorLineTermRows = termRowsForLine(crow);
|
|
715
|
-
const
|
|
721
|
+
const cursorPad = linePad(crow);
|
|
722
|
+
const cursorRowWithinLine = Math.floor((cursorPad + ccol) / termW);
|
|
716
723
|
rowsBelowCursor += (cursorLineTermRows - 1 - cursorRowWithinLine);
|
|
717
724
|
|
|
718
725
|
if (rowsBelowCursor > 0) buf.push(`\x1b[${rowsBelowCursor}A`);
|
|
719
726
|
|
|
720
727
|
buf.push("\r");
|
|
721
|
-
const col = (
|
|
728
|
+
const col = (cursorPad + ccol) % termW;
|
|
722
729
|
if (col > 0) buf.push(`\x1b[${col}C`);
|
|
723
730
|
|
|
724
731
|
// lastTermRow = terminal rows above cursor (separator + wrapped input lines above crow + cursor's wrapped rows above)
|
|
@@ -747,7 +754,7 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
|
|
|
747
754
|
if (i === 0) {
|
|
748
755
|
buf.push(dim(`[${sessionId}]`) + " " + bold(">") + " " + lines[i]);
|
|
749
756
|
} else {
|
|
750
|
-
buf.push(
|
|
757
|
+
buf.push(lines[i]);
|
|
751
758
|
}
|
|
752
759
|
}
|
|
753
760
|
buf.push("\n");
|
|
@@ -1239,7 +1246,6 @@ ${bold("groundcrew")} — CLI companion for the Groundcrew Copilot plugin
|
|
|
1239
1246
|
${bold("Usage:")}
|
|
1240
1247
|
groundcrew chat Interactive chat mode (recommended)
|
|
1241
1248
|
groundcrew chat --session <id> Chat with a specific session
|
|
1242
|
-
groundcrew init Initialize .groundcrew/ in current dir
|
|
1243
1249
|
groundcrew add <task> Add a task to the queue
|
|
1244
1250
|
groundcrew add --priority <task> Add an urgent task (processed first)
|
|
1245
1251
|
groundcrew add --session <id> <task> Add to a specific session
|
|
@@ -1250,9 +1256,9 @@ ${bold("Usage:")}
|
|
|
1250
1256
|
groundcrew sessions List all sessions
|
|
1251
1257
|
groundcrew history Show completed tasks
|
|
1252
1258
|
groundcrew clear Clear all pending tasks
|
|
1253
|
-
groundcrew stop Stop all
|
|
1259
|
+
groundcrew stop Stop all sessions for current repo
|
|
1254
1260
|
groundcrew stop --session <id> Stop a specific session
|
|
1255
|
-
groundcrew destroy Delete all sessions
|
|
1261
|
+
groundcrew destroy Delete all sessions for current repo
|
|
1256
1262
|
groundcrew destroy --session <id> Delete a specific session
|
|
1257
1263
|
|
|
1258
1264
|
${bold("Session targeting:")}
|
|
@@ -1293,9 +1299,6 @@ async function main(): Promise<void> {
|
|
|
1293
1299
|
|
|
1294
1300
|
// Commands that don't need a session (handle their own resolution)
|
|
1295
1301
|
switch (command) {
|
|
1296
|
-
case "init":
|
|
1297
|
-
await init();
|
|
1298
|
-
return;
|
|
1299
1302
|
case "chat":
|
|
1300
1303
|
await chat(explicitSession);
|
|
1301
1304
|
return;
|