groundcrew-cli 0.16.6 → 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 +57 -67
- package/package.json +1 -1
- package/src/index.ts +81 -80
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) {
|
|
@@ -1116,7 +1110,6 @@ ${bold("groundcrew")} \u2014 CLI companion for the Groundcrew Copilot plugin
|
|
|
1116
1110
|
${bold("Usage:")}
|
|
1117
1111
|
groundcrew chat Interactive chat mode (recommended)
|
|
1118
1112
|
groundcrew chat --session <id> Chat with a specific session
|
|
1119
|
-
groundcrew init Initialize .groundcrew/ in current dir
|
|
1120
1113
|
groundcrew add <task> Add a task to the queue
|
|
1121
1114
|
groundcrew add --priority <task> Add an urgent task (processed first)
|
|
1122
1115
|
groundcrew add --session <id> <task> Add to a specific session
|
|
@@ -1127,9 +1120,9 @@ ${bold("Usage:")}
|
|
|
1127
1120
|
groundcrew sessions List all sessions
|
|
1128
1121
|
groundcrew history Show completed tasks
|
|
1129
1122
|
groundcrew clear Clear all pending tasks
|
|
1130
|
-
groundcrew stop Stop all
|
|
1123
|
+
groundcrew stop Stop all sessions for current repo
|
|
1131
1124
|
groundcrew stop --session <id> Stop a specific session
|
|
1132
|
-
groundcrew destroy Delete all sessions
|
|
1125
|
+
groundcrew destroy Delete all sessions for current repo
|
|
1133
1126
|
groundcrew destroy --session <id> Delete a specific session
|
|
1134
1127
|
|
|
1135
1128
|
${bold("Session targeting:")}
|
|
@@ -1161,9 +1154,6 @@ async function main() {
|
|
|
1161
1154
|
const { value: explicitSession, remaining: args } = extractFlag(rawArgs, "--session");
|
|
1162
1155
|
const command = args[0];
|
|
1163
1156
|
switch (command) {
|
|
1164
|
-
case "init":
|
|
1165
|
-
await init();
|
|
1166
|
-
return;
|
|
1167
1157
|
case "chat":
|
|
1168
1158
|
await chat(explicitSession);
|
|
1169
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
|
|
|
@@ -1241,7 +1246,6 @@ ${bold("groundcrew")} — CLI companion for the Groundcrew Copilot plugin
|
|
|
1241
1246
|
${bold("Usage:")}
|
|
1242
1247
|
groundcrew chat Interactive chat mode (recommended)
|
|
1243
1248
|
groundcrew chat --session <id> Chat with a specific session
|
|
1244
|
-
groundcrew init Initialize .groundcrew/ in current dir
|
|
1245
1249
|
groundcrew add <task> Add a task to the queue
|
|
1246
1250
|
groundcrew add --priority <task> Add an urgent task (processed first)
|
|
1247
1251
|
groundcrew add --session <id> <task> Add to a specific session
|
|
@@ -1252,9 +1256,9 @@ ${bold("Usage:")}
|
|
|
1252
1256
|
groundcrew sessions List all sessions
|
|
1253
1257
|
groundcrew history Show completed tasks
|
|
1254
1258
|
groundcrew clear Clear all pending tasks
|
|
1255
|
-
groundcrew stop Stop all
|
|
1259
|
+
groundcrew stop Stop all sessions for current repo
|
|
1256
1260
|
groundcrew stop --session <id> Stop a specific session
|
|
1257
|
-
groundcrew destroy Delete all sessions
|
|
1261
|
+
groundcrew destroy Delete all sessions for current repo
|
|
1258
1262
|
groundcrew destroy --session <id> Delete a specific session
|
|
1259
1263
|
|
|
1260
1264
|
${bold("Session targeting:")}
|
|
@@ -1295,9 +1299,6 @@ async function main(): Promise<void> {
|
|
|
1295
1299
|
|
|
1296
1300
|
// Commands that don't need a session (handle their own resolution)
|
|
1297
1301
|
switch (command) {
|
|
1298
|
-
case "init":
|
|
1299
|
-
await init();
|
|
1300
|
-
return;
|
|
1301
1302
|
case "chat":
|
|
1302
1303
|
await chat(explicitSession);
|
|
1303
1304
|
return;
|