panopticon-cli 0.4.0 → 0.4.4
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/README.md +442 -109
- package/dist/{agents-RCAPFJG7.js → agents-B5NRTVHK.js} +3 -4
- package/dist/chunk-7HHDVXBM.js +349 -0
- package/dist/chunk-7HHDVXBM.js.map +1 -0
- package/dist/chunk-H45CLB7E.js +2044 -0
- package/dist/chunk-H45CLB7E.js.map +1 -0
- package/dist/{chunk-7BGFIAWQ.js → chunk-ITI4IC5A.js} +2 -2
- package/dist/cli/index.js +1525 -432
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/public/assets/index-BDd8hGYb.css +32 -0
- package/dist/dashboard/public/assets/index-sFwLPko-.js +556 -0
- package/dist/dashboard/public/index.html +3 -2
- package/dist/dashboard/server.js +21702 -14954
- package/dist/index.d.ts +125 -1
- package/dist/index.js +30 -5
- package/package.json +9 -2
- package/templates/claude-md/sections/warnings.md +27 -2
- package/templates/context/CLAUDE.md.template +22 -0
- package/templates/context/REOPEN_PROMPT.md.template +75 -0
- package/dist/chunk-4BIZ4OVN.js +0 -827
- package/dist/chunk-4BIZ4OVN.js.map +0 -1
- package/dist/chunk-DGUM43GV.js +0 -11
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/chunk-U4LCHEVU.js +0 -116
- package/dist/chunk-U4LCHEVU.js.map +0 -1
- package/dist/dashboard/public/assets/index-BZXQno9X.js +0 -540
- package/dist/dashboard/public/assets/index-BtAxF_yl.css +0 -32
- package/dist/js-yaml-DLUPUHNL.js +0 -2648
- package/dist/js-yaml-DLUPUHNL.js.map +0 -1
- /package/dist/{agents-RCAPFJG7.js.map → agents-B5NRTVHK.js.map} +0 -0
- /package/dist/{chunk-7BGFIAWQ.js.map → chunk-ITI4IC5A.js.map} +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -16,11 +16,12 @@ import {
|
|
|
16
16
|
restoreBackup,
|
|
17
17
|
saveConfig,
|
|
18
18
|
syncHooks
|
|
19
|
-
} from "../chunk-
|
|
19
|
+
} from "../chunk-ITI4IC5A.js";
|
|
20
20
|
import {
|
|
21
21
|
autoRecoverAgents,
|
|
22
22
|
checkHook,
|
|
23
23
|
clearHook,
|
|
24
|
+
createSession,
|
|
24
25
|
detectCrashedAgents,
|
|
25
26
|
formatCV,
|
|
26
27
|
generateFixedPointPrompt,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
getAgentRankings,
|
|
30
31
|
getAgentRuntimeState,
|
|
31
32
|
getAgentState,
|
|
33
|
+
getModelId,
|
|
32
34
|
killSession,
|
|
33
35
|
listRunningAgents,
|
|
34
36
|
messageAgent,
|
|
@@ -43,7 +45,7 @@ import {
|
|
|
43
45
|
sessionExists,
|
|
44
46
|
spawnAgent,
|
|
45
47
|
stopAgent
|
|
46
|
-
} from "../chunk-
|
|
48
|
+
} from "../chunk-H45CLB7E.js";
|
|
47
49
|
import {
|
|
48
50
|
AGENTS_DIR,
|
|
49
51
|
CERTS_DIR,
|
|
@@ -60,15 +62,17 @@ import {
|
|
|
60
62
|
SYNC_TARGETS,
|
|
61
63
|
TRAEFIK_CERTS_DIR,
|
|
62
64
|
TRAEFIK_DIR,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
} from "../chunk-DGUM43GV.js";
|
|
65
|
+
__require,
|
|
66
|
+
isDevMode,
|
|
67
|
+
loadSettings
|
|
68
|
+
} from "../chunk-7HHDVXBM.js";
|
|
68
69
|
|
|
69
70
|
// src/cli/index.ts
|
|
71
|
+
import { readFileSync as readFileSync40, existsSync as existsSync47 } from "fs";
|
|
72
|
+
import { join as join48 } from "path";
|
|
73
|
+
import { homedir as homedir20 } from "os";
|
|
70
74
|
import { Command } from "commander";
|
|
71
|
-
import
|
|
75
|
+
import chalk47 from "chalk";
|
|
72
76
|
|
|
73
77
|
// src/cli/commands/init.ts
|
|
74
78
|
import { existsSync, mkdirSync, readdirSync, cpSync } from "fs";
|
|
@@ -181,6 +185,7 @@ async function initCommand() {
|
|
|
181
185
|
// src/cli/commands/sync.ts
|
|
182
186
|
import chalk2 from "chalk";
|
|
183
187
|
import ora2 from "ora";
|
|
188
|
+
import { execSync } from "child_process";
|
|
184
189
|
import { existsSync as existsSync3, readdirSync as readdirSync2, statSync, symlinkSync, mkdirSync as mkdirSync3 } from "fs";
|
|
185
190
|
import { join as join3, dirname as dirname2 } from "path";
|
|
186
191
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -208,8 +213,8 @@ function saveProjectsConfig(config2) {
|
|
|
208
213
|
if (!existsSync2(dir)) {
|
|
209
214
|
mkdirSync2(dir, { recursive: true });
|
|
210
215
|
}
|
|
211
|
-
const
|
|
212
|
-
writeFileSync(PROJECTS_CONFIG_FILE,
|
|
216
|
+
const yaml2 = stringifyYaml(config2, { indent: 2 });
|
|
217
|
+
writeFileSync(PROJECTS_CONFIG_FILE, yaml2, "utf-8");
|
|
213
218
|
}
|
|
214
219
|
function listProjects() {
|
|
215
220
|
const config2 = loadProjectsConfig();
|
|
@@ -333,6 +338,14 @@ projects:
|
|
|
333
338
|
var __filename3 = fileURLToPath2(import.meta.url);
|
|
334
339
|
var __dirname3 = dirname2(__filename3);
|
|
335
340
|
var BUNDLED_GIT_HOOKS_DIR = join3(__dirname3, "..", "..", "scripts", "git-hooks");
|
|
341
|
+
function checkCommand(cmd) {
|
|
342
|
+
try {
|
|
343
|
+
execSync(`which ${cmd}`, { stdio: "pipe" });
|
|
344
|
+
return true;
|
|
345
|
+
} catch {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
336
349
|
async function syncCommand(options) {
|
|
337
350
|
const config2 = loadConfig();
|
|
338
351
|
const targets = config2.sync?.targets;
|
|
@@ -442,6 +455,19 @@ async function syncCommand(options) {
|
|
|
442
455
|
} else {
|
|
443
456
|
hooksSpinner.info("No hooks to sync");
|
|
444
457
|
}
|
|
458
|
+
const hasRouter = checkCommand("claude-code-router");
|
|
459
|
+
if (!hasRouter) {
|
|
460
|
+
const routerSpinner = ora2("Installing claude-code-router...").start();
|
|
461
|
+
try {
|
|
462
|
+
execSync("npm install -g @musistudio/claude-code-router", {
|
|
463
|
+
stdio: "pipe",
|
|
464
|
+
timeout: 12e4
|
|
465
|
+
});
|
|
466
|
+
routerSpinner.succeed("claude-code-router installed");
|
|
467
|
+
} catch (error) {
|
|
468
|
+
routerSpinner.warn("Failed to install claude-code-router - run: npm install -g @musistudio/claude-code-router");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
445
471
|
const projects = listProjects();
|
|
446
472
|
if (projects.length > 0 && existsSync3(BUNDLED_GIT_HOOKS_DIR)) {
|
|
447
473
|
const gitHooksSpinner = ora2("Installing git hooks in registered projects...").start();
|
|
@@ -483,9 +509,9 @@ async function syncCommand(options) {
|
|
|
483
509
|
if (readlinkSync2(target) === source) continue;
|
|
484
510
|
} catch {
|
|
485
511
|
}
|
|
486
|
-
const { renameSync } = await import("fs");
|
|
512
|
+
const { renameSync: renameSync2 } = await import("fs");
|
|
487
513
|
try {
|
|
488
|
-
|
|
514
|
+
renameSync2(target, `${target}.backup`);
|
|
489
515
|
} catch {
|
|
490
516
|
}
|
|
491
517
|
}
|
|
@@ -687,7 +713,7 @@ async function updateLinearToInProgress(apiKey, issueIdentifier) {
|
|
|
687
713
|
(s) => s.name === "In Progress" || s.type === "started"
|
|
688
714
|
);
|
|
689
715
|
if (!inProgressState) return false;
|
|
690
|
-
await issue.
|
|
716
|
+
await client.updateIssue(issue.id, { stateId: inProgressState.id });
|
|
691
717
|
return true;
|
|
692
718
|
} catch (error) {
|
|
693
719
|
return false;
|
|
@@ -752,6 +778,31 @@ function readPlanningContext(workspacePath) {
|
|
|
752
778
|
}
|
|
753
779
|
return null;
|
|
754
780
|
}
|
|
781
|
+
function validateAndCleanStateFile(workspacePath, issueId) {
|
|
782
|
+
const statePath = join5(workspacePath, ".planning", "STATE.md");
|
|
783
|
+
if (!existsSync5(statePath)) {
|
|
784
|
+
return { valid: true, removed: false };
|
|
785
|
+
}
|
|
786
|
+
try {
|
|
787
|
+
const content = readFileSync3(statePath, "utf-8");
|
|
788
|
+
const firstLine = content.split("\n")[0] || "";
|
|
789
|
+
const issueMatch = firstLine.match(/^#\s*([A-Z]+-\d+)/i);
|
|
790
|
+
if (issueMatch) {
|
|
791
|
+
const stateIssueId = issueMatch[1].toUpperCase();
|
|
792
|
+
const currentIssueId = issueId.toUpperCase();
|
|
793
|
+
if (stateIssueId !== currentIssueId) {
|
|
794
|
+
const { unlinkSync: unlinkSync5 } = __require("fs");
|
|
795
|
+
unlinkSync5(statePath);
|
|
796
|
+
console.warn(chalk6.yellow(`\u26A0\uFE0F Removed stale STATE.md (was for ${stateIssueId}, not ${currentIssueId})`));
|
|
797
|
+
console.warn(chalk6.dim(" This can happen when branches are merged. The agent will start fresh."));
|
|
798
|
+
return { valid: false, removed: true, wrongIssue: stateIssueId };
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return { valid: true, removed: false };
|
|
802
|
+
} catch (error) {
|
|
803
|
+
return { valid: true, removed: false };
|
|
804
|
+
}
|
|
805
|
+
}
|
|
755
806
|
function extractStitchDesigns(stateContent) {
|
|
756
807
|
if (!stateContent) return null;
|
|
757
808
|
const stitchPatterns = [
|
|
@@ -830,6 +881,33 @@ function readBeadsTasks(workspacePath, projectRoot, issueId) {
|
|
|
830
881
|
}
|
|
831
882
|
return tasks;
|
|
832
883
|
}
|
|
884
|
+
function buildPolyrepoContext(issueId, workspacePath) {
|
|
885
|
+
const teamPrefix = extractTeamPrefix(issueId);
|
|
886
|
+
const projectConfig = teamPrefix ? findProjectByTeam(teamPrefix) : null;
|
|
887
|
+
if (!projectConfig?.workspace?.type || projectConfig.workspace.type !== "polyrepo" || !projectConfig.workspace.repos) {
|
|
888
|
+
return "";
|
|
889
|
+
}
|
|
890
|
+
const repos = projectConfig.workspace.repos;
|
|
891
|
+
const lines = [
|
|
892
|
+
"## Project Structure (Polyrepo)",
|
|
893
|
+
"",
|
|
894
|
+
"**IMPORTANT:** This project uses a **polyrepo** structure. The workspace root is NOT a git repository.",
|
|
895
|
+
"Each subdirectory is a separate git worktree:",
|
|
896
|
+
"",
|
|
897
|
+
"| Directory | Purpose |",
|
|
898
|
+
"|-----------|---------|"
|
|
899
|
+
];
|
|
900
|
+
for (const repo of repos) {
|
|
901
|
+
lines.push(`| \`${repo.name}/\` | Git worktree for ${repo.path} |`);
|
|
902
|
+
}
|
|
903
|
+
lines.push("");
|
|
904
|
+
lines.push("**Git operations:**");
|
|
905
|
+
lines.push("- Run `git status`, `git log`, etc. INSIDE the subdirectories (e.g., `cd fe && git status`)");
|
|
906
|
+
lines.push(`- The workspace root (\`${workspacePath}\`) has no \`.git\` directory`);
|
|
907
|
+
lines.push(`- Each subdirectory has its own branch: \`${repos[0]?.branch_prefix || "feature/"}${issueId.toLowerCase()}\``);
|
|
908
|
+
lines.push("");
|
|
909
|
+
return lines.join("\n");
|
|
910
|
+
}
|
|
833
911
|
function buildAgentPrompt(issueId, workspacePath, projectRoot) {
|
|
834
912
|
const lines = [
|
|
835
913
|
`# Working on Issue: ${issueId}`,
|
|
@@ -837,6 +915,10 @@ function buildAgentPrompt(issueId, workspacePath, projectRoot) {
|
|
|
837
915
|
`**Workspace:** ${workspacePath}`,
|
|
838
916
|
""
|
|
839
917
|
];
|
|
918
|
+
const polyrepoContext = buildPolyrepoContext(issueId, workspacePath);
|
|
919
|
+
if (polyrepoContext) {
|
|
920
|
+
lines.push(polyrepoContext);
|
|
921
|
+
}
|
|
840
922
|
const hasStateFile = existsSync5(join5(workspacePath, ".planning", "STATE.md"));
|
|
841
923
|
const hasClaudeMd = existsSync5(join5(workspacePath, "CLAUDE.md"));
|
|
842
924
|
const hasProjectClaudeMd = existsSync5(join5(projectRoot, "CLAUDE.md"));
|
|
@@ -979,6 +1061,11 @@ async function issueCommand(id, options) {
|
|
|
979
1061
|
console.log(` Beads: ${beadsTasks2.length} tasks`);
|
|
980
1062
|
return;
|
|
981
1063
|
}
|
|
1064
|
+
spinner.text = "Validating workspace state...";
|
|
1065
|
+
const stateValidation = validateAndCleanStateFile(workspace, id);
|
|
1066
|
+
if (stateValidation.removed) {
|
|
1067
|
+
spinner.warn(`Cleaned stale planning state from ${stateValidation.wrongIssue}`);
|
|
1068
|
+
}
|
|
982
1069
|
spinner.text = "Building agent prompt with planning context...";
|
|
983
1070
|
const prompt = buildAgentPrompt(id, workspace, projectRoot);
|
|
984
1071
|
spinner.text = "Spawning agent...";
|
|
@@ -1122,7 +1209,7 @@ import ora5 from "ora";
|
|
|
1122
1209
|
import { existsSync as existsSync7, writeFileSync as writeFileSync2, readFileSync as readFileSync5 } from "fs";
|
|
1123
1210
|
import { join as join7 } from "path";
|
|
1124
1211
|
import { homedir as homedir2 } from "os";
|
|
1125
|
-
import { execSync } from "child_process";
|
|
1212
|
+
import { execSync as execSync2 } from "child_process";
|
|
1126
1213
|
function getLinearApiKey2() {
|
|
1127
1214
|
const envFile = join7(homedir2(), ".panopticon.env");
|
|
1128
1215
|
if (existsSync7(envFile)) {
|
|
@@ -1134,7 +1221,7 @@ function getLinearApiKey2() {
|
|
|
1134
1221
|
}
|
|
1135
1222
|
function checkGhCli() {
|
|
1136
1223
|
try {
|
|
1137
|
-
|
|
1224
|
+
execSync2("which gh", { stdio: "pipe" });
|
|
1138
1225
|
return true;
|
|
1139
1226
|
} catch {
|
|
1140
1227
|
return false;
|
|
@@ -1142,12 +1229,12 @@ function checkGhCli() {
|
|
|
1142
1229
|
}
|
|
1143
1230
|
function findPRForBranch(workspace) {
|
|
1144
1231
|
try {
|
|
1145
|
-
const branch =
|
|
1232
|
+
const branch = execSync2("git rev-parse --abbrev-ref HEAD", {
|
|
1146
1233
|
cwd: workspace,
|
|
1147
1234
|
encoding: "utf-8",
|
|
1148
1235
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1149
1236
|
}).trim();
|
|
1150
|
-
const prJson =
|
|
1237
|
+
const prJson = execSync2(`gh pr list --head "${branch}" --json number,url --limit 1`, {
|
|
1151
1238
|
cwd: workspace,
|
|
1152
1239
|
encoding: "utf-8",
|
|
1153
1240
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1163,7 +1250,7 @@ function findPRForBranch(workspace) {
|
|
|
1163
1250
|
}
|
|
1164
1251
|
function mergePR(workspace, prNumber) {
|
|
1165
1252
|
try {
|
|
1166
|
-
|
|
1253
|
+
execSync2(`gh pr merge ${prNumber} --squash`, {
|
|
1167
1254
|
cwd: workspace,
|
|
1168
1255
|
encoding: "utf-8",
|
|
1169
1256
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1389,6 +1476,65 @@ async function doneCommand(id, options = {}) {
|
|
|
1389
1476
|
console.log("");
|
|
1390
1477
|
console.log(chalk12.dim("Ready for review. When approved, run:"));
|
|
1391
1478
|
console.log(chalk12.dim(` pan work approve ${issueId}`));
|
|
1479
|
+
console.log("");
|
|
1480
|
+
try {
|
|
1481
|
+
const dashboardUrl = "http://localhost:3011";
|
|
1482
|
+
const http = await import("http");
|
|
1483
|
+
const checkDashboard = () => new Promise((resolve2) => {
|
|
1484
|
+
const req = http.request(`${dashboardUrl}/health`, { method: "GET", timeout: 1e3 }, (res) => {
|
|
1485
|
+
resolve2(res.statusCode === 200);
|
|
1486
|
+
});
|
|
1487
|
+
req.on("error", () => resolve2(false));
|
|
1488
|
+
req.on("timeout", () => {
|
|
1489
|
+
req.destroy();
|
|
1490
|
+
resolve2(false);
|
|
1491
|
+
});
|
|
1492
|
+
req.end();
|
|
1493
|
+
});
|
|
1494
|
+
const dashboardRunning = await checkDashboard();
|
|
1495
|
+
if (dashboardRunning) {
|
|
1496
|
+
console.log(chalk12.dim("Auto-triggering review & test..."));
|
|
1497
|
+
const reviewReq = () => new Promise((resolve2, reject) => {
|
|
1498
|
+
const postData = JSON.stringify({});
|
|
1499
|
+
const req = http.request(
|
|
1500
|
+
`${dashboardUrl}/api/workspaces/${issueId}/review`,
|
|
1501
|
+
{ method: "POST", headers: { "Content-Type": "application/json" }, timeout: 5e3 },
|
|
1502
|
+
(res) => {
|
|
1503
|
+
let data = "";
|
|
1504
|
+
res.on("data", (chunk) => data += chunk);
|
|
1505
|
+
res.on("end", () => {
|
|
1506
|
+
try {
|
|
1507
|
+
resolve2(JSON.parse(data));
|
|
1508
|
+
} catch {
|
|
1509
|
+
resolve2({ success: false, error: "Invalid response" });
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
);
|
|
1514
|
+
req.on("error", reject);
|
|
1515
|
+
req.on("timeout", () => {
|
|
1516
|
+
req.destroy();
|
|
1517
|
+
reject(new Error("Timeout"));
|
|
1518
|
+
});
|
|
1519
|
+
req.write(postData);
|
|
1520
|
+
req.end();
|
|
1521
|
+
});
|
|
1522
|
+
const result = await reviewReq();
|
|
1523
|
+
if (result.success) {
|
|
1524
|
+
console.log(chalk12.green(` \u2713 Review & test ${result.queued ? "queued" : "started"} automatically`));
|
|
1525
|
+
} else {
|
|
1526
|
+
console.log(chalk12.yellow(` \u26A0 Auto-review not triggered: ${result.error || result.message || "Unknown error"}`));
|
|
1527
|
+
if (result.alreadyReviewed) {
|
|
1528
|
+
console.log(chalk12.dim(` Manual review needed - click "Review and Test" in dashboard`));
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
} else {
|
|
1532
|
+
console.log(chalk12.dim(" Dashboard not running - skipping auto-review"));
|
|
1533
|
+
console.log(chalk12.dim(" Start dashboard with: pan up"));
|
|
1534
|
+
}
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
console.log(chalk12.dim(` Could not auto-trigger review: ${error.message}`));
|
|
1537
|
+
}
|
|
1392
1538
|
} catch (error) {
|
|
1393
1539
|
spinner.fail(error.message);
|
|
1394
1540
|
process.exit(1);
|
|
@@ -3098,8 +3244,8 @@ async function runHealthCheck(config2 = {
|
|
|
3098
3244
|
} catch {
|
|
3099
3245
|
}
|
|
3100
3246
|
if (existsSync13(AGENTS_DIR)) {
|
|
3101
|
-
const { readdirSync:
|
|
3102
|
-
const dirs =
|
|
3247
|
+
const { readdirSync: readdirSync17 } = await import("fs");
|
|
3248
|
+
const dirs = readdirSync17(AGENTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith("agent-")).map((d) => d.name);
|
|
3103
3249
|
for (const dir of dirs) {
|
|
3104
3250
|
if (!sessions.includes(dir)) {
|
|
3105
3251
|
sessions.push(dir);
|
|
@@ -3611,8 +3757,8 @@ async function wipeCommand(issueId, options) {
|
|
|
3611
3757
|
const projectsYamlPath = join14(homedir7(), ".panopticon", "projects.yaml");
|
|
3612
3758
|
if (existsSync15(projectsYamlPath)) {
|
|
3613
3759
|
try {
|
|
3614
|
-
const
|
|
3615
|
-
const projectsConfig =
|
|
3760
|
+
const yaml2 = await import("js-yaml");
|
|
3761
|
+
const projectsConfig = yaml2.load(readFileSync13(projectsYamlPath, "utf-8"));
|
|
3616
3762
|
for (const [, config2] of Object.entries(projectsConfig.projects || {})) {
|
|
3617
3763
|
const projConfig = config2;
|
|
3618
3764
|
if (projConfig.linear_team?.toUpperCase() === prefix) {
|
|
@@ -3698,7 +3844,7 @@ async function wipeCommand(issueId, options) {
|
|
|
3698
3844
|
// src/cli/commands/work/index.ts
|
|
3699
3845
|
function registerWorkCommands(program2) {
|
|
3700
3846
|
const work = program2.command("work").description("Agent and work management");
|
|
3701
|
-
work.command("issue <id>").description("Spawn agent for Linear issue").option("--model <model>", "
|
|
3847
|
+
work.command("issue <id>").description("Spawn agent for Linear issue").option("--model <model>", "Model to use (sonnet/opus/haiku/kimi-k2.5/etc) - defaults to settings.json or kimi-k2.5").option("--runtime <runtime>", "AI runtime (claude/codex)", "claude").option("--dry-run", "Show what would be created").action(issueCommand);
|
|
3702
3848
|
work.command("status").description("Show all running agents").option("--json", "Output as JSON").action(statusCommand);
|
|
3703
3849
|
work.command("tell <id> <message>").description("Send message to running agent").action(tellCommand);
|
|
3704
3850
|
work.command("kill <id>").description("Kill an agent").option("--force", "Kill without confirmation").action(killCommand);
|
|
@@ -3734,11 +3880,11 @@ import { existsSync as existsSync19, mkdirSync as mkdirSync11, writeFileSync as
|
|
|
3734
3880
|
import { join as join18, basename as basename3 } from "path";
|
|
3735
3881
|
|
|
3736
3882
|
// src/lib/worktree.ts
|
|
3737
|
-
import { execSync as
|
|
3883
|
+
import { execSync as execSync3 } from "child_process";
|
|
3738
3884
|
import { mkdirSync as mkdirSync8 } from "fs";
|
|
3739
3885
|
import { dirname as dirname5 } from "path";
|
|
3740
3886
|
function listWorktrees(repoPath) {
|
|
3741
|
-
const output =
|
|
3887
|
+
const output = execSync3("git worktree list --porcelain", {
|
|
3742
3888
|
cwd: repoPath,
|
|
3743
3889
|
encoding: "utf8"
|
|
3744
3890
|
});
|
|
@@ -3762,22 +3908,22 @@ function listWorktrees(repoPath) {
|
|
|
3762
3908
|
function createWorktree(repoPath, targetPath, branchName) {
|
|
3763
3909
|
mkdirSync8(dirname5(targetPath), { recursive: true });
|
|
3764
3910
|
try {
|
|
3765
|
-
|
|
3911
|
+
execSync3(`git show-ref --verify --quiet refs/heads/${branchName}`, {
|
|
3766
3912
|
cwd: repoPath
|
|
3767
3913
|
});
|
|
3768
|
-
|
|
3914
|
+
execSync3(`git worktree add "${targetPath}" "${branchName}"`, {
|
|
3769
3915
|
cwd: repoPath,
|
|
3770
3916
|
stdio: "pipe"
|
|
3771
3917
|
});
|
|
3772
3918
|
} catch {
|
|
3773
|
-
|
|
3919
|
+
execSync3(`git worktree add -b "${branchName}" "${targetPath}"`, {
|
|
3774
3920
|
cwd: repoPath,
|
|
3775
3921
|
stdio: "pipe"
|
|
3776
3922
|
});
|
|
3777
3923
|
}
|
|
3778
3924
|
}
|
|
3779
3925
|
function removeWorktree(repoPath, worktreePath) {
|
|
3780
|
-
|
|
3926
|
+
execSync3(`git worktree remove "${worktreePath}" --force`, {
|
|
3781
3927
|
cwd: repoPath,
|
|
3782
3928
|
stdio: "pipe"
|
|
3783
3929
|
});
|
|
@@ -3852,7 +3998,7 @@ import {
|
|
|
3852
3998
|
writeFileSync as writeFileSync8
|
|
3853
3999
|
} from "fs";
|
|
3854
4000
|
import { join as join16 } from "path";
|
|
3855
|
-
import { execSync as
|
|
4001
|
+
import { execSync as execSync4 } from "child_process";
|
|
3856
4002
|
function detectContentOrigin(path, repoPath) {
|
|
3857
4003
|
try {
|
|
3858
4004
|
const stat = lstatSync(path);
|
|
@@ -3863,7 +4009,7 @@ function detectContentOrigin(path, repoPath) {
|
|
|
3863
4009
|
}
|
|
3864
4010
|
}
|
|
3865
4011
|
try {
|
|
3866
|
-
|
|
4012
|
+
execSync4(`git ls-files --error-unmatch "${path}" 2>/dev/null`, {
|
|
3867
4013
|
cwd: repoPath,
|
|
3868
4014
|
stdio: "pipe"
|
|
3869
4015
|
});
|
|
@@ -5260,12 +5406,12 @@ ${config2.name || key}`));
|
|
|
5260
5406
|
// src/cli/commands/install.ts
|
|
5261
5407
|
import chalk26 from "chalk";
|
|
5262
5408
|
import ora14 from "ora";
|
|
5263
|
-
import { execSync as
|
|
5409
|
+
import { execSync as execSync5 } from "child_process";
|
|
5264
5410
|
import { existsSync as existsSync21, mkdirSync as mkdirSync13, writeFileSync as writeFileSync12, readFileSync as readFileSync17, copyFileSync as copyFileSync2, readdirSync as readdirSync10, statSync as statSync2 } from "fs";
|
|
5265
5411
|
import { join as join20 } from "path";
|
|
5266
5412
|
import { homedir as homedir10, platform } from "os";
|
|
5267
5413
|
function registerInstallCommand(program2) {
|
|
5268
|
-
program2.command("install").description("Install Panopticon prerequisites").option("--check", "Check prerequisites only").option("--minimal", "Skip Traefik and mkcert (use port-based routing)").option("--skip-mkcert", "Skip mkcert/HTTPS setup").option("--skip-docker", "Skip Docker network setup").option("--skip-beads", "Skip beads CLI installation").action(installCommand);
|
|
5414
|
+
program2.command("install").description("Install Panopticon prerequisites").option("--check", "Check prerequisites only").option("--minimal", "Skip Traefik and mkcert (use port-based routing)").option("--skip-mkcert", "Skip mkcert/HTTPS setup").option("--skip-docker", "Skip Docker network setup").option("--skip-beads", "Skip beads CLI installation").option("--skip-router", "Skip claude-code-router installation").action(installCommand);
|
|
5269
5415
|
}
|
|
5270
5416
|
function detectPlatform() {
|
|
5271
5417
|
const os = platform();
|
|
@@ -5298,9 +5444,9 @@ function copyDirectoryRecursive(source, dest) {
|
|
|
5298
5444
|
}
|
|
5299
5445
|
}
|
|
5300
5446
|
}
|
|
5301
|
-
function
|
|
5447
|
+
function checkCommand2(cmd) {
|
|
5302
5448
|
try {
|
|
5303
|
-
|
|
5449
|
+
execSync5(`which ${cmd}`, { stdio: "pipe" });
|
|
5304
5450
|
return true;
|
|
5305
5451
|
} catch {
|
|
5306
5452
|
return false;
|
|
@@ -5316,18 +5462,18 @@ function checkPrerequisites() {
|
|
|
5316
5462
|
message: nodeMajor >= 18 ? `v${nodeVersion}` : `v${nodeVersion} (need v18+)`,
|
|
5317
5463
|
fix: "Install Node.js 18+ from https://nodejs.org"
|
|
5318
5464
|
});
|
|
5319
|
-
const hasGit =
|
|
5465
|
+
const hasGit = checkCommand2("git");
|
|
5320
5466
|
results.push({
|
|
5321
5467
|
name: "Git",
|
|
5322
5468
|
passed: hasGit,
|
|
5323
5469
|
message: hasGit ? "installed" : "not found",
|
|
5324
5470
|
fix: "Install git from your package manager"
|
|
5325
5471
|
});
|
|
5326
|
-
const hasDocker =
|
|
5472
|
+
const hasDocker = checkCommand2("docker");
|
|
5327
5473
|
let dockerRunning = false;
|
|
5328
5474
|
if (hasDocker) {
|
|
5329
5475
|
try {
|
|
5330
|
-
|
|
5476
|
+
execSync5("docker info", { stdio: "pipe" });
|
|
5331
5477
|
dockerRunning = true;
|
|
5332
5478
|
} catch {
|
|
5333
5479
|
}
|
|
@@ -5338,25 +5484,25 @@ function checkPrerequisites() {
|
|
|
5338
5484
|
message: dockerRunning ? "running" : hasDocker ? "not running" : "not found",
|
|
5339
5485
|
fix: hasDocker ? "Start Docker Desktop or docker service" : "Install Docker"
|
|
5340
5486
|
});
|
|
5341
|
-
const hasTmux =
|
|
5487
|
+
const hasTmux = checkCommand2("tmux");
|
|
5342
5488
|
results.push({
|
|
5343
5489
|
name: "tmux",
|
|
5344
5490
|
passed: hasTmux,
|
|
5345
5491
|
message: hasTmux ? "installed" : "not found",
|
|
5346
5492
|
fix: "apt install tmux / brew install tmux"
|
|
5347
5493
|
});
|
|
5348
|
-
const hasMkcert =
|
|
5494
|
+
const hasMkcert = checkCommand2("mkcert");
|
|
5349
5495
|
results.push({
|
|
5350
5496
|
name: "mkcert",
|
|
5351
5497
|
passed: hasMkcert,
|
|
5352
5498
|
message: hasMkcert ? "installed" : "not found (optional)",
|
|
5353
5499
|
fix: "brew install mkcert / apt install mkcert"
|
|
5354
5500
|
});
|
|
5355
|
-
const hasBeads =
|
|
5501
|
+
const hasBeads = checkCommand2("bd");
|
|
5356
5502
|
let beadsVersion = "";
|
|
5357
5503
|
if (hasBeads) {
|
|
5358
5504
|
try {
|
|
5359
|
-
const output =
|
|
5505
|
+
const output = execSync5("bd --version", { encoding: "utf-8" }).trim();
|
|
5360
5506
|
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
5361
5507
|
beadsVersion = match ? match[1] : "unknown";
|
|
5362
5508
|
} catch {
|
|
@@ -5368,7 +5514,14 @@ function checkPrerequisites() {
|
|
|
5368
5514
|
message: hasBeads ? `v${beadsVersion}` : "not found (will auto-install)",
|
|
5369
5515
|
fix: "curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash"
|
|
5370
5516
|
});
|
|
5371
|
-
const
|
|
5517
|
+
const hasRouter = checkCommand2("claude-code-router");
|
|
5518
|
+
results.push({
|
|
5519
|
+
name: "claude-code-router",
|
|
5520
|
+
passed: hasRouter,
|
|
5521
|
+
message: hasRouter ? "installed" : "not found (will auto-install)",
|
|
5522
|
+
fix: "npm install -g @musistudio/claude-code-router"
|
|
5523
|
+
});
|
|
5524
|
+
const hasTtyd = checkCommand2("ttyd") || existsSync21(join20(homedir10(), "bin", "ttyd"));
|
|
5372
5525
|
results.push({
|
|
5373
5526
|
name: "ttyd",
|
|
5374
5527
|
passed: hasTtyd,
|
|
@@ -5377,8 +5530,8 @@ function checkPrerequisites() {
|
|
|
5377
5530
|
});
|
|
5378
5531
|
return {
|
|
5379
5532
|
results,
|
|
5380
|
-
// mkcert, ttyd, and
|
|
5381
|
-
allPassed: results.filter((r) => r.name !== "mkcert" && r.name !== "ttyd" && r.name !== "Beads CLI (bd)").every((r) => r.passed)
|
|
5533
|
+
// mkcert, ttyd, beads, and claude-code-router are optional (will be auto-installed or skipped)
|
|
5534
|
+
allPassed: results.filter((r) => r.name !== "mkcert" && r.name !== "ttyd" && r.name !== "Beads CLI (bd)" && r.name !== "claude-code-router").every((r) => r.passed)
|
|
5382
5535
|
};
|
|
5383
5536
|
}
|
|
5384
5537
|
function printPrereqStatus(prereqs) {
|
|
@@ -5442,23 +5595,23 @@ async function installCommand(options) {
|
|
|
5442
5595
|
if (!options.skipDocker) {
|
|
5443
5596
|
spinner.start("Creating Docker network...");
|
|
5444
5597
|
try {
|
|
5445
|
-
|
|
5598
|
+
execSync5("docker network create panopticon 2>/dev/null || true", { stdio: "pipe" });
|
|
5446
5599
|
spinner.succeed("Docker network ready");
|
|
5447
5600
|
} catch (error) {
|
|
5448
5601
|
spinner.warn("Docker network setup failed (may already exist)");
|
|
5449
5602
|
}
|
|
5450
5603
|
}
|
|
5451
5604
|
if (!options.skipMkcert && !options.minimal) {
|
|
5452
|
-
const hasMkcert =
|
|
5605
|
+
const hasMkcert = checkCommand2("mkcert");
|
|
5453
5606
|
if (hasMkcert) {
|
|
5454
5607
|
spinner.start("Setting up mkcert CA...");
|
|
5455
5608
|
try {
|
|
5456
|
-
|
|
5609
|
+
execSync5("mkcert -install", { stdio: "pipe" });
|
|
5457
5610
|
spinner.succeed("mkcert CA installed");
|
|
5458
5611
|
spinner.start("Generating wildcard certificates...");
|
|
5459
5612
|
const traefikCertFile = join20(TRAEFIK_CERTS_DIR, "_wildcard.pan.localhost.pem");
|
|
5460
5613
|
const traefikKeyFile = join20(TRAEFIK_CERTS_DIR, "_wildcard.pan.localhost-key.pem");
|
|
5461
|
-
|
|
5614
|
+
execSync5(
|
|
5462
5615
|
`mkcert -cert-file "${traefikCertFile}" -key-file "${traefikKeyFile}" "*.pan.localhost" "*.localhost" localhost 127.0.0.1 ::1`,
|
|
5463
5616
|
{ stdio: "pipe" }
|
|
5464
5617
|
);
|
|
@@ -5474,7 +5627,7 @@ async function installCommand(options) {
|
|
|
5474
5627
|
spinner.info("Skipping mkcert (not installed)");
|
|
5475
5628
|
}
|
|
5476
5629
|
}
|
|
5477
|
-
const hasTtyd =
|
|
5630
|
+
const hasTtyd = checkCommand2("ttyd") || existsSync21(join20(homedir10(), "bin", "ttyd"));
|
|
5478
5631
|
if (!hasTtyd) {
|
|
5479
5632
|
spinner.start("Installing ttyd (web terminal)...");
|
|
5480
5633
|
try {
|
|
@@ -5485,7 +5638,7 @@ async function installCommand(options) {
|
|
|
5485
5638
|
let downloadUrl = "";
|
|
5486
5639
|
if (plat2 === "darwin") {
|
|
5487
5640
|
try {
|
|
5488
|
-
|
|
5641
|
+
execSync5("brew install ttyd", { stdio: "pipe" });
|
|
5489
5642
|
spinner.succeed("ttyd installed via Homebrew");
|
|
5490
5643
|
} catch {
|
|
5491
5644
|
spinner.warn("ttyd installation failed - install manually: brew install ttyd");
|
|
@@ -5493,7 +5646,7 @@ async function installCommand(options) {
|
|
|
5493
5646
|
} else {
|
|
5494
5647
|
downloadUrl = "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.x86_64";
|
|
5495
5648
|
try {
|
|
5496
|
-
|
|
5649
|
+
execSync5(`curl -sL "${downloadUrl}" -o "${ttydPath}" && chmod +x "${ttydPath}"`, {
|
|
5497
5650
|
stdio: "pipe",
|
|
5498
5651
|
timeout: 6e4
|
|
5499
5652
|
});
|
|
@@ -5511,18 +5664,18 @@ async function installCommand(options) {
|
|
|
5511
5664
|
if (options.skipBeads) {
|
|
5512
5665
|
spinner.info("Skipping beads installation (--skip-beads)");
|
|
5513
5666
|
} else {
|
|
5514
|
-
const hasBeadsNow =
|
|
5667
|
+
const hasBeadsNow = checkCommand2("bd");
|
|
5515
5668
|
if (!hasBeadsNow) {
|
|
5516
5669
|
spinner.start("Installing beads CLI (bd)...");
|
|
5517
5670
|
try {
|
|
5518
5671
|
const plat2 = detectPlatform();
|
|
5519
5672
|
if (plat2 === "darwin") {
|
|
5520
5673
|
try {
|
|
5521
|
-
|
|
5674
|
+
execSync5("brew install steveyegge/beads/bd", { stdio: "pipe", timeout: 12e4 });
|
|
5522
5675
|
spinner.succeed("beads installed via Homebrew");
|
|
5523
5676
|
} catch {
|
|
5524
5677
|
try {
|
|
5525
|
-
|
|
5678
|
+
execSync5("curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash", {
|
|
5526
5679
|
stdio: "pipe",
|
|
5527
5680
|
timeout: 12e4
|
|
5528
5681
|
});
|
|
@@ -5533,7 +5686,7 @@ async function installCommand(options) {
|
|
|
5533
5686
|
}
|
|
5534
5687
|
} else {
|
|
5535
5688
|
try {
|
|
5536
|
-
|
|
5689
|
+
execSync5("curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash", {
|
|
5537
5690
|
stdio: "pipe",
|
|
5538
5691
|
timeout: 12e4
|
|
5539
5692
|
});
|
|
@@ -5547,7 +5700,7 @@ async function installCommand(options) {
|
|
|
5547
5700
|
}
|
|
5548
5701
|
} else {
|
|
5549
5702
|
try {
|
|
5550
|
-
const output =
|
|
5703
|
+
const output = execSync5("bd --version", { encoding: "utf-8" }).trim();
|
|
5551
5704
|
const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
5552
5705
|
if (match) {
|
|
5553
5706
|
const [, major, minor, patch] = match.map(Number);
|
|
@@ -5566,6 +5719,25 @@ async function installCommand(options) {
|
|
|
5566
5719
|
}
|
|
5567
5720
|
}
|
|
5568
5721
|
}
|
|
5722
|
+
if (options.skipRouter) {
|
|
5723
|
+
spinner.info("Skipping claude-code-router installation (--skip-router)");
|
|
5724
|
+
} else {
|
|
5725
|
+
const hasRouterNow = checkCommand2("claude-code-router");
|
|
5726
|
+
if (!hasRouterNow) {
|
|
5727
|
+
spinner.start("Installing claude-code-router...");
|
|
5728
|
+
try {
|
|
5729
|
+
execSync5("npm install -g @musistudio/claude-code-router", {
|
|
5730
|
+
stdio: "pipe",
|
|
5731
|
+
timeout: 12e4
|
|
5732
|
+
});
|
|
5733
|
+
spinner.succeed("claude-code-router installed via npm");
|
|
5734
|
+
} catch (error) {
|
|
5735
|
+
spinner.warn("claude-code-router installation failed - install manually: npm install -g @musistudio/claude-code-router");
|
|
5736
|
+
}
|
|
5737
|
+
} else {
|
|
5738
|
+
spinner.info("claude-code-router already installed");
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5569
5741
|
if (!options.minimal) {
|
|
5570
5742
|
spinner.start("Setting up Traefik configuration...");
|
|
5571
5743
|
try {
|
|
@@ -5621,7 +5793,7 @@ async function installCommand(options) {
|
|
|
5621
5793
|
console.log(` 4. Access dashboard at ${chalk26.cyan("https://pan.localhost")}`);
|
|
5622
5794
|
} else {
|
|
5623
5795
|
console.log(` 2. Run ${chalk26.cyan("pan up")} to start the dashboard`);
|
|
5624
|
-
console.log(` 3. Access dashboard at ${chalk26.cyan("http://localhost:
|
|
5796
|
+
console.log(` 3. Access dashboard at ${chalk26.cyan("http://localhost:3011")}`);
|
|
5625
5797
|
}
|
|
5626
5798
|
console.log(` ${!options.minimal ? "5" : "4"}. Create a workspace with ${chalk26.cyan("pan workspace create <issue-id>")}`);
|
|
5627
5799
|
console.log("");
|
|
@@ -6111,6 +6283,8 @@ function parseClaudeSession(sessionFile) {
|
|
|
6111
6283
|
cacheReadTokens: 0,
|
|
6112
6284
|
cacheWriteTokens: 0
|
|
6113
6285
|
};
|
|
6286
|
+
const modelBreakdown = {};
|
|
6287
|
+
let totalCostV2 = 0;
|
|
6114
6288
|
for (const line of lines) {
|
|
6115
6289
|
try {
|
|
6116
6290
|
const msg = JSON.parse(line);
|
|
@@ -6126,16 +6300,42 @@ function parseClaudeSession(sessionFile) {
|
|
|
6126
6300
|
}
|
|
6127
6301
|
}
|
|
6128
6302
|
const usage = msg.message?.usage || msg.usage;
|
|
6129
|
-
const
|
|
6303
|
+
const modelId = msg.message?.model || msg.model;
|
|
6130
6304
|
if (usage) {
|
|
6131
6305
|
totalUsage.inputTokens += usage.input_tokens || 0;
|
|
6132
6306
|
totalUsage.outputTokens += usage.output_tokens || 0;
|
|
6133
6307
|
totalUsage.cacheReadTokens = (totalUsage.cacheReadTokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
6134
6308
|
totalUsage.cacheWriteTokens = (totalUsage.cacheWriteTokens || 0) + (usage.cache_creation_input_tokens || 0);
|
|
6135
6309
|
messageCount++;
|
|
6310
|
+
if (modelId) {
|
|
6311
|
+
const { provider: provider2, model: normalizedModel } = normalizeModelName(modelId);
|
|
6312
|
+
const pricing2 = getPricing(provider2, normalizedModel);
|
|
6313
|
+
if (pricing2) {
|
|
6314
|
+
const msgUsage = {
|
|
6315
|
+
inputTokens: usage.input_tokens || 0,
|
|
6316
|
+
outputTokens: usage.output_tokens || 0,
|
|
6317
|
+
cacheReadTokens: usage.cache_read_input_tokens || 0,
|
|
6318
|
+
cacheWriteTokens: usage.cache_creation_input_tokens || 0
|
|
6319
|
+
};
|
|
6320
|
+
const msgCost = calculateCost(msgUsage, pricing2);
|
|
6321
|
+
totalCostV2 += msgCost;
|
|
6322
|
+
if (!modelBreakdown[modelId]) {
|
|
6323
|
+
modelBreakdown[modelId] = {
|
|
6324
|
+
cost: 0,
|
|
6325
|
+
inputTokens: 0,
|
|
6326
|
+
outputTokens: 0,
|
|
6327
|
+
messageCount: 0
|
|
6328
|
+
};
|
|
6329
|
+
}
|
|
6330
|
+
modelBreakdown[modelId].cost += msgCost;
|
|
6331
|
+
modelBreakdown[modelId].inputTokens += msgUsage.inputTokens;
|
|
6332
|
+
modelBreakdown[modelId].outputTokens += msgUsage.outputTokens;
|
|
6333
|
+
modelBreakdown[modelId].messageCount++;
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
6136
6336
|
}
|
|
6137
|
-
if (
|
|
6138
|
-
primaryModel =
|
|
6337
|
+
if (modelId && !primaryModel) {
|
|
6338
|
+
primaryModel = modelId;
|
|
6139
6339
|
}
|
|
6140
6340
|
} catch {
|
|
6141
6341
|
}
|
|
@@ -6149,6 +6349,8 @@ function parseClaudeSession(sessionFile) {
|
|
|
6149
6349
|
if (!primaryModel) {
|
|
6150
6350
|
primaryModel = "claude-sonnet-4";
|
|
6151
6351
|
}
|
|
6352
|
+
const normalizedModels = Object.keys(modelBreakdown).map((id) => normalizeModelName(id).model);
|
|
6353
|
+
const modelDisplay = normalizedModels.length > 0 ? normalizedModels.length > 1 ? normalizedModels.join(" \u2192 ") : normalizedModels[0] : normalizeModelName(primaryModel).model;
|
|
6152
6354
|
const { provider, model } = normalizeModelName(primaryModel);
|
|
6153
6355
|
const pricing = getPricing(provider, model);
|
|
6154
6356
|
const cost = pricing ? calculateCost(totalUsage, pricing) : 0;
|
|
@@ -6157,10 +6359,15 @@ function parseClaudeSession(sessionFile) {
|
|
|
6157
6359
|
sessionFile,
|
|
6158
6360
|
startTime: startTime || (/* @__PURE__ */ new Date()).toISOString(),
|
|
6159
6361
|
endTime: endTime || (/* @__PURE__ */ new Date()).toISOString(),
|
|
6160
|
-
model:
|
|
6362
|
+
model: modelDisplay,
|
|
6161
6363
|
usage: totalUsage,
|
|
6162
6364
|
cost,
|
|
6163
|
-
|
|
6365
|
+
// DEPRECATED: First-model pricing
|
|
6366
|
+
cost_v2: totalCostV2 > 0 ? totalCostV2 : void 0,
|
|
6367
|
+
// NEW: Accurate per-message pricing
|
|
6368
|
+
messageCount,
|
|
6369
|
+
modelBreakdown: Object.keys(modelBreakdown).length > 0 ? modelBreakdown : void 0
|
|
6370
|
+
// NEW: Cost breakdown by model
|
|
6164
6371
|
};
|
|
6165
6372
|
}
|
|
6166
6373
|
|
|
@@ -6373,7 +6580,7 @@ async function getSpecialistStatus(name) {
|
|
|
6373
6580
|
const sessionId = getSessionId(name);
|
|
6374
6581
|
const running = await isRunning(name);
|
|
6375
6582
|
const contextTokens = countContextTokens(name);
|
|
6376
|
-
const { getAgentRuntimeState: getAgentRuntimeState2 } = await import("../agents-
|
|
6583
|
+
const { getAgentRuntimeState: getAgentRuntimeState2 } = await import("../agents-B5NRTVHK.js");
|
|
6377
6584
|
const tmuxSession = getTmuxSessionName(name);
|
|
6378
6585
|
const runtimeState = getAgentRuntimeState2(tmuxSession);
|
|
6379
6586
|
let state;
|
|
@@ -6433,6 +6640,13 @@ async function initializeSpecialist(name) {
|
|
|
6433
6640
|
}
|
|
6434
6641
|
const tmuxSession = getTmuxSessionName(name);
|
|
6435
6642
|
const cwd = process.env.HOME || "/home/eltmon";
|
|
6643
|
+
let model = "claude-sonnet-4-5";
|
|
6644
|
+
try {
|
|
6645
|
+
const workTypeId = `specialist-${name}`;
|
|
6646
|
+
model = getModelId(workTypeId);
|
|
6647
|
+
} catch (error) {
|
|
6648
|
+
console.warn(`Warning: Could not resolve model for ${name}, using default model`);
|
|
6649
|
+
}
|
|
6436
6650
|
const identityPrompt = `You are the ${name} specialist agent for Panopticon.
|
|
6437
6651
|
Your role: ${name === "merge-agent" ? "Resolve merge conflicts and ensure clean integrations" : name === "review-agent" ? "Review code changes and provide quality feedback" : name === "test-agent" ? "Execute and analyze test results" : "Assist with development tasks"}
|
|
6438
6652
|
|
|
@@ -6447,7 +6661,7 @@ Say: "I am the ${name} specialist, ready and waiting for tasks."`;
|
|
|
6447
6661
|
writeFileSync14(launcherScript, `#!/bin/bash
|
|
6448
6662
|
cd "${cwd}"
|
|
6449
6663
|
prompt=$(cat "${promptFile}")
|
|
6450
|
-
exec claude --dangerously-skip-permissions "$prompt"
|
|
6664
|
+
exec claude --dangerously-skip-permissions --model ${model} "$prompt"
|
|
6451
6665
|
`, { mode: 493 });
|
|
6452
6666
|
await execAsync7(
|
|
6453
6667
|
`tmux new-session -d -s "${tmuxSession}" "bash '${launcherScript}'"`,
|
|
@@ -6565,7 +6779,7 @@ async function wakeSpecialist(name, taskPrompt, options = {}) {
|
|
|
6565
6779
|
await execAsync7(`tmux send-keys -t "${tmuxSession}" C-m`, { encoding: "utf-8" });
|
|
6566
6780
|
}
|
|
6567
6781
|
recordWake(name, sessionId || void 0);
|
|
6568
|
-
const { saveAgentRuntimeState: saveAgentRuntimeState2 } = await import("../agents-
|
|
6782
|
+
const { saveAgentRuntimeState: saveAgentRuntimeState2 } = await import("../agents-B5NRTVHK.js");
|
|
6569
6783
|
saveAgentRuntimeState2(tmuxSession, {
|
|
6570
6784
|
state: "active",
|
|
6571
6785
|
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6666,7 +6880,7 @@ IMPORTANT: Do NOT hand off to merge-agent. Human clicks Merge button when ready.
|
|
|
6666
6880
|
async function wakeSpecialistOrQueue(name, task, options = {}) {
|
|
6667
6881
|
const { priority = "normal", source = "handoff" } = options;
|
|
6668
6882
|
const running = await isRunning(name);
|
|
6669
|
-
const { getAgentRuntimeState: getAgentRuntimeState2 } = await import("../agents-
|
|
6883
|
+
const { getAgentRuntimeState: getAgentRuntimeState2 } = await import("../agents-B5NRTVHK.js");
|
|
6670
6884
|
const tmuxSession = getTmuxSessionName(name);
|
|
6671
6885
|
const runtimeState = getAgentRuntimeState2(tmuxSession);
|
|
6672
6886
|
const idle = runtimeState?.state === "idle" || runtimeState?.state === "suspended";
|
|
@@ -6697,10 +6911,11 @@ async function wakeSpecialistOrQueue(name, task, options = {}) {
|
|
|
6697
6911
|
};
|
|
6698
6912
|
}
|
|
6699
6913
|
}
|
|
6700
|
-
const { saveAgentRuntimeState: saveAgentRuntimeState2 } = await import("../agents-
|
|
6914
|
+
const { saveAgentRuntimeState: saveAgentRuntimeState2 } = await import("../agents-B5NRTVHK.js");
|
|
6701
6915
|
saveAgentRuntimeState2(tmuxSession, {
|
|
6702
6916
|
state: "active",
|
|
6703
|
-
lastActivity: (/* @__PURE__ */ new Date()).toISOString()
|
|
6917
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6918
|
+
currentIssue: task.issueId
|
|
6704
6919
|
});
|
|
6705
6920
|
console.log(`[specialist] ${name} marked active (preventing concurrent wakes)`);
|
|
6706
6921
|
try {
|
|
@@ -6708,7 +6923,8 @@ async function wakeSpecialistOrQueue(name, task, options = {}) {
|
|
|
6708
6923
|
if (!wakeResult.success) {
|
|
6709
6924
|
saveAgentRuntimeState2(tmuxSession, {
|
|
6710
6925
|
state: "idle",
|
|
6711
|
-
lastActivity: (/* @__PURE__ */ new Date()).toISOString()
|
|
6926
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6927
|
+
currentIssue: void 0
|
|
6712
6928
|
});
|
|
6713
6929
|
}
|
|
6714
6930
|
return {
|
|
@@ -6720,7 +6936,8 @@ async function wakeSpecialistOrQueue(name, task, options = {}) {
|
|
|
6720
6936
|
} catch (error) {
|
|
6721
6937
|
saveAgentRuntimeState2(tmuxSession, {
|
|
6722
6938
|
state: "idle",
|
|
6723
|
-
lastActivity: (/* @__PURE__ */ new Date()).toISOString()
|
|
6939
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6940
|
+
currentIssue: void 0
|
|
6724
6941
|
});
|
|
6725
6942
|
const msg = error instanceof Error ? error.message : String(error);
|
|
6726
6943
|
return {
|
|
@@ -8061,7 +8278,7 @@ function getCostSummary() {
|
|
|
8061
8278
|
// src/lib/cloister/session-rotation.ts
|
|
8062
8279
|
import { writeFileSync as writeFileSync20 } from "fs";
|
|
8063
8280
|
import { join as join34 } from "path";
|
|
8064
|
-
import { execSync as
|
|
8281
|
+
import { execSync as execSync6 } from "child_process";
|
|
8065
8282
|
var SESSION_ROTATION_THRESHOLD = 1e5;
|
|
8066
8283
|
var DEFAULT_MEMORY_TIERS = {
|
|
8067
8284
|
recent_summary: 100,
|
|
@@ -8083,7 +8300,7 @@ function buildMergeAgentMemory(workingDir, tiers = DEFAULT_MEMORY_TIERS) {
|
|
|
8083
8300
|
const merges = [];
|
|
8084
8301
|
try {
|
|
8085
8302
|
const totalMerges = Math.max(tiers.recent_summary, tiers.recent_detailed, tiers.recent_full);
|
|
8086
|
-
const gitLog =
|
|
8303
|
+
const gitLog = execSync6(
|
|
8087
8304
|
`git log --merges --format="%H|%s|%an|%ad|%D" -n ${totalMerges}`,
|
|
8088
8305
|
{ cwd: workingDir, encoding: "utf-8" }
|
|
8089
8306
|
);
|
|
@@ -8118,13 +8335,13 @@ function buildMergeAgentMemory(workingDir, tiers = DEFAULT_MEMORY_TIERS) {
|
|
|
8118
8335
|
if (merge.branch) memory += `- Branch: ${merge.branch}
|
|
8119
8336
|
`;
|
|
8120
8337
|
try {
|
|
8121
|
-
const files =
|
|
8338
|
+
const files = execSync6(`git show --name-only --format= ${merge.hash}`, {
|
|
8122
8339
|
cwd: workingDir,
|
|
8123
8340
|
encoding: "utf-8"
|
|
8124
8341
|
}).trim().split("\n").filter((f) => f);
|
|
8125
8342
|
memory += `- Files changed: ${files.length}
|
|
8126
8343
|
`;
|
|
8127
|
-
const diff =
|
|
8344
|
+
const diff = execSync6(`git show ${merge.hash} --stat`, {
|
|
8128
8345
|
cwd: workingDir,
|
|
8129
8346
|
encoding: "utf-8",
|
|
8130
8347
|
maxBuffer: 10 * 1024 * 1024
|
|
@@ -8201,7 +8418,7 @@ async function rotateSpecialistSession(specialistName, workingDir) {
|
|
|
8201
8418
|
}
|
|
8202
8419
|
const tmuxSession = getTmuxSessionName(specialistName);
|
|
8203
8420
|
try {
|
|
8204
|
-
|
|
8421
|
+
execSync6(`tmux kill-session -t "${tmuxSession}"`, { encoding: "utf-8" });
|
|
8205
8422
|
console.log(`Killed session: ${tmuxSession}`);
|
|
8206
8423
|
} catch (error) {
|
|
8207
8424
|
console.log(`Session ${tmuxSession} not found or already killed`);
|
|
@@ -8481,6 +8698,17 @@ async function checkAndSuspendIdleAgents() {
|
|
|
8481
8698
|
continue;
|
|
8482
8699
|
}
|
|
8483
8700
|
const runtimeState = getAgentRuntimeState(agent.id);
|
|
8701
|
+
if (runtimeState && runtimeState.lastActivity) {
|
|
8702
|
+
const state = getAgentState(agent.id);
|
|
8703
|
+
if (state) {
|
|
8704
|
+
const runtimeLastActivity = runtimeState.lastActivity;
|
|
8705
|
+
const stateLastActivity = state.lastActivity;
|
|
8706
|
+
if (!stateLastActivity || new Date(runtimeLastActivity) > new Date(stateLastActivity)) {
|
|
8707
|
+
state.lastActivity = runtimeLastActivity;
|
|
8708
|
+
saveAgentState(state);
|
|
8709
|
+
}
|
|
8710
|
+
}
|
|
8711
|
+
}
|
|
8484
8712
|
if (!runtimeState || runtimeState.state !== "idle") {
|
|
8485
8713
|
continue;
|
|
8486
8714
|
}
|
|
@@ -8489,6 +8717,13 @@ async function checkAndSuspendIdleAgents() {
|
|
|
8489
8717
|
const idleMinutes = idleMs / (1e3 * 60);
|
|
8490
8718
|
const isSpecialist = specialistNames.has(agent.id);
|
|
8491
8719
|
const timeoutMinutes = isSpecialist ? 5 : 10;
|
|
8720
|
+
const isWorkAgent = agent.id.startsWith("agent-") && !isSpecialist;
|
|
8721
|
+
if (isWorkAgent) {
|
|
8722
|
+
const completedFile = join35(getAgentDir(agent.id), "completed");
|
|
8723
|
+
if (existsSync35(completedFile)) {
|
|
8724
|
+
continue;
|
|
8725
|
+
}
|
|
8726
|
+
}
|
|
8492
8727
|
if (idleMinutes > timeoutMinutes) {
|
|
8493
8728
|
console.log(`[deacon] Auto-suspending ${agent.id} (idle for ${Math.round(idleMinutes)} minutes)`);
|
|
8494
8729
|
try {
|
|
@@ -8701,7 +8936,7 @@ async function runPatrol() {
|
|
|
8701
8936
|
if (nextTask) {
|
|
8702
8937
|
console.log(`[deacon] Auto-resuming suspended ${specialist.name} for queued task: ${nextTask.payload.issueId}`);
|
|
8703
8938
|
try {
|
|
8704
|
-
const { resumeAgent } = await import("../agents-
|
|
8939
|
+
const { resumeAgent } = await import("../agents-B5NRTVHK.js");
|
|
8705
8940
|
const message = `# Queued Work
|
|
8706
8941
|
|
|
8707
8942
|
Processing queued task: ${nextTask.payload.issueId}`;
|
|
@@ -9580,11 +9815,11 @@ function registerCloisterCommands(program2) {
|
|
|
9580
9815
|
import chalk30 from "chalk";
|
|
9581
9816
|
import { readFileSync as readFileSync30, writeFileSync as writeFileSync23, existsSync as existsSync37, mkdirSync as mkdirSync24, copyFileSync as copyFileSync3, chmodSync as chmodSync2 } from "fs";
|
|
9582
9817
|
import { join as join37 } from "path";
|
|
9583
|
-
import { execSync as
|
|
9818
|
+
import { execSync as execSync7 } from "child_process";
|
|
9584
9819
|
import { homedir as homedir15 } from "os";
|
|
9585
9820
|
function checkJqInstalled() {
|
|
9586
9821
|
try {
|
|
9587
|
-
|
|
9822
|
+
execSync7("which jq", { stdio: "pipe" });
|
|
9588
9823
|
return true;
|
|
9589
9824
|
} catch {
|
|
9590
9825
|
return false;
|
|
@@ -9596,8 +9831,8 @@ function installJq() {
|
|
|
9596
9831
|
const platform3 = process.platform;
|
|
9597
9832
|
if (platform3 === "darwin") {
|
|
9598
9833
|
try {
|
|
9599
|
-
|
|
9600
|
-
|
|
9834
|
+
execSync7("brew --version", { stdio: "pipe" });
|
|
9835
|
+
execSync7("brew install jq", { stdio: "inherit" });
|
|
9601
9836
|
console.log(chalk30.green("\u2713 jq installed via Homebrew"));
|
|
9602
9837
|
return true;
|
|
9603
9838
|
} catch {
|
|
@@ -9605,14 +9840,14 @@ function installJq() {
|
|
|
9605
9840
|
}
|
|
9606
9841
|
} else if (platform3 === "linux") {
|
|
9607
9842
|
try {
|
|
9608
|
-
|
|
9609
|
-
|
|
9843
|
+
execSync7("apt-get --version", { stdio: "pipe" });
|
|
9844
|
+
execSync7("sudo apt-get update && sudo apt-get install -y jq", { stdio: "inherit" });
|
|
9610
9845
|
console.log(chalk30.green("\u2713 jq installed via apt"));
|
|
9611
9846
|
return true;
|
|
9612
9847
|
} catch {
|
|
9613
9848
|
try {
|
|
9614
|
-
|
|
9615
|
-
|
|
9849
|
+
execSync7("yum --version", { stdio: "pipe" });
|
|
9850
|
+
execSync7("sudo yum install -y jq", { stdio: "inherit" });
|
|
9616
9851
|
console.log(chalk30.green("\u2713 jq installed via yum"));
|
|
9617
9852
|
return true;
|
|
9618
9853
|
} catch {
|
|
@@ -10347,7 +10582,7 @@ async function doneCommand2(specialist, issueId, options) {
|
|
|
10347
10582
|
}
|
|
10348
10583
|
break;
|
|
10349
10584
|
case "merge":
|
|
10350
|
-
status.mergeStatus = options.status;
|
|
10585
|
+
status.mergeStatus = options.status === "passed" ? "merged" : "failed";
|
|
10351
10586
|
if (options.status === "passed") {
|
|
10352
10587
|
console.log(chalk36.green(`\u2713 Merge completed for ${normalizedIssueId}`));
|
|
10353
10588
|
status.readyForMerge = false;
|
|
@@ -10399,32 +10634,652 @@ function registerSpecialistsCommands(program2) {
|
|
|
10399
10634
|
specialists.command("done <type> <issueId>").description("Signal specialist completion (deterministic status update)").requiredOption("--status <status>", "Result status: passed or failed").option("--notes <notes>", "Optional notes about the result").action(doneCommand2);
|
|
10400
10635
|
}
|
|
10401
10636
|
|
|
10402
|
-
// src/cli/commands/
|
|
10637
|
+
// src/cli/commands/convoy/start.ts
|
|
10403
10638
|
import chalk37 from "chalk";
|
|
10404
|
-
import
|
|
10405
|
-
|
|
10639
|
+
import ora15 from "ora";
|
|
10640
|
+
|
|
10641
|
+
// src/lib/convoy.ts
|
|
10642
|
+
import { existsSync as existsSync41, mkdirSync as mkdirSync26, writeFileSync as writeFileSync27, readFileSync as readFileSync33, readdirSync as readdirSync14 } from "fs";
|
|
10643
|
+
import { join as join41 } from "path";
|
|
10644
|
+
import { homedir as homedir17 } from "os";
|
|
10645
|
+
import { exec as exec13 } from "child_process";
|
|
10646
|
+
import { promisify as promisify13 } from "util";
|
|
10647
|
+
import { parse as parseYaml2 } from "yaml";
|
|
10648
|
+
|
|
10649
|
+
// src/lib/convoy-templates.ts
|
|
10650
|
+
var CODE_REVIEW_TEMPLATE = {
|
|
10651
|
+
name: "code-review",
|
|
10652
|
+
description: "Parallel code review with automatic synthesis",
|
|
10653
|
+
agents: [
|
|
10654
|
+
{
|
|
10655
|
+
role: "correctness",
|
|
10656
|
+
subagent: "code-review-correctness",
|
|
10657
|
+
parallel: true
|
|
10658
|
+
},
|
|
10659
|
+
{
|
|
10660
|
+
role: "security",
|
|
10661
|
+
subagent: "code-review-security",
|
|
10662
|
+
parallel: true
|
|
10663
|
+
},
|
|
10664
|
+
{
|
|
10665
|
+
role: "performance",
|
|
10666
|
+
subagent: "code-review-performance",
|
|
10667
|
+
parallel: true
|
|
10668
|
+
},
|
|
10669
|
+
{
|
|
10670
|
+
role: "synthesis",
|
|
10671
|
+
subagent: "code-review-synthesis",
|
|
10672
|
+
parallel: false,
|
|
10673
|
+
dependsOn: ["correctness", "security", "performance"]
|
|
10674
|
+
}
|
|
10675
|
+
],
|
|
10676
|
+
config: {
|
|
10677
|
+
outputDir: ".claude/reviews",
|
|
10678
|
+
maxParallel: 3,
|
|
10679
|
+
// Limit parallel reviewers
|
|
10680
|
+
timeout: 6e5
|
|
10681
|
+
// 10 minutes per agent
|
|
10682
|
+
}
|
|
10683
|
+
};
|
|
10684
|
+
var PLANNING_TEMPLATE = {
|
|
10685
|
+
name: "planning",
|
|
10686
|
+
description: "Codebase exploration and planning",
|
|
10687
|
+
agents: [
|
|
10688
|
+
{
|
|
10689
|
+
role: "planner",
|
|
10690
|
+
subagent: "planning-agent",
|
|
10691
|
+
parallel: false
|
|
10692
|
+
}
|
|
10693
|
+
],
|
|
10694
|
+
config: {
|
|
10695
|
+
outputDir: ".claude/planning",
|
|
10696
|
+
timeout: 9e5
|
|
10697
|
+
// 15 minutes
|
|
10698
|
+
}
|
|
10699
|
+
};
|
|
10700
|
+
var TRIAGE_TEMPLATE = {
|
|
10701
|
+
name: "triage",
|
|
10702
|
+
description: "Parallel issue triage and categorization",
|
|
10703
|
+
agents: [
|
|
10704
|
+
// Agents are dynamically added based on issues to triage
|
|
10705
|
+
// This is a placeholder template; actual agents created at runtime
|
|
10706
|
+
],
|
|
10707
|
+
config: {
|
|
10708
|
+
outputDir: ".panopticon/triage",
|
|
10709
|
+
maxParallel: 5
|
|
10710
|
+
// Limit concurrent triage agents
|
|
10711
|
+
}
|
|
10712
|
+
};
|
|
10713
|
+
var HEALTH_MONITOR_TEMPLATE = {
|
|
10714
|
+
name: "health-monitor",
|
|
10715
|
+
description: "Monitor health of running agents",
|
|
10716
|
+
agents: [
|
|
10717
|
+
{
|
|
10718
|
+
role: "monitor",
|
|
10719
|
+
subagent: "health-monitor",
|
|
10720
|
+
parallel: false
|
|
10721
|
+
}
|
|
10722
|
+
],
|
|
10723
|
+
config: {
|
|
10724
|
+
outputDir: ".panopticon/health"
|
|
10725
|
+
}
|
|
10726
|
+
};
|
|
10727
|
+
var CONVOY_TEMPLATES = {
|
|
10728
|
+
"code-review": CODE_REVIEW_TEMPLATE,
|
|
10729
|
+
"planning": PLANNING_TEMPLATE,
|
|
10730
|
+
"triage": TRIAGE_TEMPLATE,
|
|
10731
|
+
"health-monitor": HEALTH_MONITOR_TEMPLATE
|
|
10732
|
+
};
|
|
10733
|
+
function getConvoyTemplate(name) {
|
|
10734
|
+
return CONVOY_TEMPLATES[name];
|
|
10735
|
+
}
|
|
10736
|
+
function getExecutionOrder(template) {
|
|
10737
|
+
const agents = [...template.agents];
|
|
10738
|
+
const phases = [];
|
|
10739
|
+
const completed = /* @__PURE__ */ new Set();
|
|
10740
|
+
while (agents.length > 0) {
|
|
10741
|
+
const ready = agents.filter((agent) => {
|
|
10742
|
+
const deps = agent.dependsOn || [];
|
|
10743
|
+
return deps.every((dep) => completed.has(dep));
|
|
10744
|
+
});
|
|
10745
|
+
if (ready.length === 0) {
|
|
10746
|
+
throw new Error("Cannot determine execution order: circular dependency or invalid template");
|
|
10747
|
+
}
|
|
10748
|
+
const parallel = ready.filter((a) => a.parallel);
|
|
10749
|
+
const sequential = ready.filter((a) => !a.parallel);
|
|
10750
|
+
if (parallel.length > 0) {
|
|
10751
|
+
phases.push(parallel);
|
|
10752
|
+
parallel.forEach((a) => completed.add(a.role));
|
|
10753
|
+
agents.splice(agents.indexOf(parallel[0]), parallel.length);
|
|
10754
|
+
}
|
|
10755
|
+
for (const agent of sequential) {
|
|
10756
|
+
phases.push([agent]);
|
|
10757
|
+
completed.add(agent.role);
|
|
10758
|
+
agents.splice(agents.indexOf(agent), 1);
|
|
10759
|
+
}
|
|
10760
|
+
}
|
|
10761
|
+
return phases;
|
|
10762
|
+
}
|
|
10763
|
+
|
|
10764
|
+
// src/lib/convoy.ts
|
|
10765
|
+
var execAsync13 = promisify13(exec13);
|
|
10766
|
+
var CONVOY_DIR = join41(homedir17(), ".panopticon", "convoys");
|
|
10767
|
+
function getConvoyStateFile(convoyId) {
|
|
10768
|
+
return join41(CONVOY_DIR, `${convoyId}.json`);
|
|
10769
|
+
}
|
|
10770
|
+
function getConvoyOutputDir(convoyId, template) {
|
|
10771
|
+
const baseDir = template.config?.outputDir || ".panopticon/convoy-output";
|
|
10772
|
+
return join41(process.cwd(), baseDir, convoyId);
|
|
10773
|
+
}
|
|
10774
|
+
function saveConvoyState(state) {
|
|
10775
|
+
mkdirSync26(CONVOY_DIR, { recursive: true });
|
|
10776
|
+
writeFileSync27(getConvoyStateFile(state.id), JSON.stringify(state, null, 2));
|
|
10777
|
+
}
|
|
10778
|
+
function loadConvoyState(convoyId) {
|
|
10779
|
+
const stateFile = getConvoyStateFile(convoyId);
|
|
10780
|
+
if (!existsSync41(stateFile)) {
|
|
10781
|
+
return void 0;
|
|
10782
|
+
}
|
|
10783
|
+
try {
|
|
10784
|
+
const content = readFileSync33(stateFile, "utf-8");
|
|
10785
|
+
return JSON.parse(content);
|
|
10786
|
+
} catch {
|
|
10787
|
+
return void 0;
|
|
10788
|
+
}
|
|
10789
|
+
}
|
|
10790
|
+
function getConvoyStatus(convoyId) {
|
|
10791
|
+
return loadConvoyState(convoyId);
|
|
10792
|
+
}
|
|
10793
|
+
function listConvoys(filter) {
|
|
10794
|
+
if (!existsSync41(CONVOY_DIR)) {
|
|
10795
|
+
return [];
|
|
10796
|
+
}
|
|
10797
|
+
const files = readdirSync14(CONVOY_DIR).filter((f) => f.endsWith(".json"));
|
|
10798
|
+
const convoys = [];
|
|
10799
|
+
for (const file of files) {
|
|
10800
|
+
const convoyId = file.replace(".json", "");
|
|
10801
|
+
const state = loadConvoyState(convoyId);
|
|
10802
|
+
if (state) {
|
|
10803
|
+
if (!filter?.status || state.status === filter.status) {
|
|
10804
|
+
convoys.push(state);
|
|
10805
|
+
}
|
|
10806
|
+
}
|
|
10807
|
+
}
|
|
10808
|
+
return convoys.sort(
|
|
10809
|
+
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
|
|
10810
|
+
);
|
|
10811
|
+
}
|
|
10812
|
+
function parseAgentTemplate(templatePath) {
|
|
10813
|
+
if (!existsSync41(templatePath)) {
|
|
10814
|
+
throw new Error(`Agent template not found: ${templatePath}`);
|
|
10815
|
+
}
|
|
10816
|
+
const content = readFileSync33(templatePath, "utf-8");
|
|
10817
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]+?)\n---\n([\s\S]*)$/);
|
|
10818
|
+
if (!frontmatterMatch) {
|
|
10819
|
+
throw new Error(`Invalid agent template format (missing frontmatter): ${templatePath}`);
|
|
10820
|
+
}
|
|
10821
|
+
const frontmatter = parseYaml2(frontmatterMatch[1]);
|
|
10822
|
+
const promptContent = frontmatterMatch[2].trim();
|
|
10823
|
+
return {
|
|
10824
|
+
name: frontmatter.name || "unknown",
|
|
10825
|
+
description: frontmatter.description || "",
|
|
10826
|
+
model: frontmatter.model || "sonnet",
|
|
10827
|
+
tools: frontmatter.tools || [],
|
|
10828
|
+
content: promptContent
|
|
10829
|
+
};
|
|
10830
|
+
}
|
|
10831
|
+
function mapConvoyRoleToWorkType(role) {
|
|
10832
|
+
const roleMap = {
|
|
10833
|
+
"security": "convoy:security-reviewer",
|
|
10834
|
+
"performance": "convoy:performance-reviewer",
|
|
10835
|
+
"correctness": "convoy:correctness-reviewer",
|
|
10836
|
+
"synthesis": "convoy:synthesis-agent"
|
|
10837
|
+
};
|
|
10838
|
+
return roleMap[role] || null;
|
|
10839
|
+
}
|
|
10840
|
+
async function spawnConvoyAgent(convoy, agent, agentState, context) {
|
|
10841
|
+
const { role, subagent } = agent;
|
|
10842
|
+
const templatePath = join41(AGENTS_DIR, `${subagent}.md`);
|
|
10843
|
+
const template = parseAgentTemplate(templatePath);
|
|
10844
|
+
let model = template.model;
|
|
10845
|
+
try {
|
|
10846
|
+
const workTypeId = mapConvoyRoleToWorkType(role);
|
|
10847
|
+
if (workTypeId) {
|
|
10848
|
+
model = getModelId(workTypeId);
|
|
10849
|
+
}
|
|
10850
|
+
} catch (error) {
|
|
10851
|
+
console.warn(`Warning: Could not resolve model for convoy role ${role}, using template default`);
|
|
10852
|
+
}
|
|
10853
|
+
const agentContext = {
|
|
10854
|
+
...context,
|
|
10855
|
+
convoy: {
|
|
10856
|
+
id: convoy.id,
|
|
10857
|
+
template: convoy.template,
|
|
10858
|
+
role,
|
|
10859
|
+
outputDir: convoy.outputDir
|
|
10860
|
+
}
|
|
10861
|
+
};
|
|
10862
|
+
let prompt = template.content;
|
|
10863
|
+
const contextInstructions = `
|
|
10864
|
+
# Convoy Context
|
|
10865
|
+
|
|
10866
|
+
You are part of a convoy: **${convoy.template}**
|
|
10867
|
+
Your role: **${role}**
|
|
10868
|
+
|
|
10869
|
+
**Output Directory**: ${convoy.outputDir}
|
|
10870
|
+
**Output File**: ${agentState.outputFile || "Not specified"}
|
|
10871
|
+
|
|
10872
|
+
${context.files ? `**Files to review**: ${context.files.join(", ")}` : ""}
|
|
10873
|
+
${context.prUrl ? `**Pull Request**: ${context.prUrl}` : ""}
|
|
10874
|
+
${context.issueId ? `**Issue ID**: ${context.issueId}` : ""}
|
|
10875
|
+
|
|
10876
|
+
---
|
|
10877
|
+
|
|
10878
|
+
`;
|
|
10879
|
+
prompt = contextInstructions + prompt;
|
|
10880
|
+
mkdirSync26(convoy.outputDir, { recursive: true });
|
|
10881
|
+
const promptFile = join41(convoy.outputDir, `${role}-prompt.md`);
|
|
10882
|
+
writeFileSync27(promptFile, prompt);
|
|
10883
|
+
const claudeCmd = `claude --dangerously-skip-permissions --model ${model}`;
|
|
10884
|
+
createSession(agentState.tmuxSession, convoy.context.projectPath, claudeCmd, {
|
|
10885
|
+
env: {
|
|
10886
|
+
PANOPTICON_CONVOY_ID: convoy.id,
|
|
10887
|
+
PANOPTICON_CONVOY_ROLE: role
|
|
10888
|
+
}
|
|
10889
|
+
});
|
|
10890
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1500));
|
|
10891
|
+
await execAsync13(`tmux load-buffer "${promptFile}"`);
|
|
10892
|
+
await execAsync13(`tmux paste-buffer -t ${agentState.tmuxSession}`);
|
|
10893
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
10894
|
+
await execAsync13(`tmux send-keys -t ${agentState.tmuxSession} Enter`);
|
|
10895
|
+
agentState.status = "running";
|
|
10896
|
+
agentState.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10897
|
+
saveConvoyState(convoy);
|
|
10898
|
+
}
|
|
10899
|
+
async function startConvoy(templateName, context) {
|
|
10900
|
+
const template = getConvoyTemplate(templateName);
|
|
10901
|
+
if (!template) {
|
|
10902
|
+
throw new Error(`Unknown convoy template: ${templateName}`);
|
|
10903
|
+
}
|
|
10904
|
+
const timestamp = Date.now();
|
|
10905
|
+
const convoyId = `convoy-${templateName}-${timestamp}`;
|
|
10906
|
+
const outputDir = getConvoyOutputDir(convoyId, template);
|
|
10907
|
+
mkdirSync26(outputDir, { recursive: true });
|
|
10908
|
+
const state = {
|
|
10909
|
+
id: convoyId,
|
|
10910
|
+
template: templateName,
|
|
10911
|
+
status: "running",
|
|
10912
|
+
agents: [],
|
|
10913
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10914
|
+
outputDir,
|
|
10915
|
+
context
|
|
10916
|
+
};
|
|
10917
|
+
for (const agent of template.agents) {
|
|
10918
|
+
const tmuxSession = `${convoyId}-${agent.role}`;
|
|
10919
|
+
const outputFile = join41(outputDir, `${agent.role}.md`);
|
|
10920
|
+
state.agents.push({
|
|
10921
|
+
role: agent.role,
|
|
10922
|
+
subagent: agent.subagent,
|
|
10923
|
+
tmuxSession,
|
|
10924
|
+
status: "pending",
|
|
10925
|
+
outputFile
|
|
10926
|
+
});
|
|
10927
|
+
}
|
|
10928
|
+
saveConvoyState(state);
|
|
10929
|
+
const phases = getExecutionOrder(template);
|
|
10930
|
+
if (phases.length > 0) {
|
|
10931
|
+
await executePhase(state, template, phases[0], context);
|
|
10932
|
+
}
|
|
10933
|
+
startPhaseMonitor(state.id, template, phases, context);
|
|
10934
|
+
return state;
|
|
10935
|
+
}
|
|
10936
|
+
async function executePhase(convoy, template, phaseAgents, context) {
|
|
10937
|
+
const spawnPromises = [];
|
|
10938
|
+
for (const agent of phaseAgents) {
|
|
10939
|
+
const agentState = convoy.agents.find((a) => a.role === agent.role);
|
|
10940
|
+
if (!agentState) {
|
|
10941
|
+
throw new Error(`Agent state not found for role: ${agent.role}`);
|
|
10942
|
+
}
|
|
10943
|
+
const deps = agent.dependsOn || [];
|
|
10944
|
+
const allDepsCompleted = deps.every((depRole) => {
|
|
10945
|
+
const depAgent = convoy.agents.find((a) => a.role === depRole);
|
|
10946
|
+
return depAgent?.status === "completed";
|
|
10947
|
+
});
|
|
10948
|
+
if (!allDepsCompleted) {
|
|
10949
|
+
throw new Error(`Dependencies not met for agent ${agent.role}`);
|
|
10950
|
+
}
|
|
10951
|
+
const agentContext = { ...context };
|
|
10952
|
+
for (const depRole of deps) {
|
|
10953
|
+
const depAgent = convoy.agents.find((a) => a.role === depRole);
|
|
10954
|
+
if (depAgent?.outputFile && existsSync41(depAgent.outputFile)) {
|
|
10955
|
+
agentContext[`${depRole}_output`] = readFileSync33(depAgent.outputFile, "utf-8");
|
|
10956
|
+
}
|
|
10957
|
+
}
|
|
10958
|
+
spawnPromises.push(spawnConvoyAgent(convoy, agent, agentState, agentContext));
|
|
10959
|
+
}
|
|
10960
|
+
await Promise.all(spawnPromises);
|
|
10961
|
+
}
|
|
10962
|
+
function startPhaseMonitor(convoyId, template, phases, context) {
|
|
10963
|
+
const monitorLoop = async () => {
|
|
10964
|
+
let currentPhaseIndex = 1;
|
|
10965
|
+
while (currentPhaseIndex < phases.length) {
|
|
10966
|
+
await new Promise((resolve2) => setTimeout(resolve2, 5e3));
|
|
10967
|
+
const state = loadConvoyState(convoyId);
|
|
10968
|
+
if (!state || state.status !== "running") {
|
|
10969
|
+
break;
|
|
10970
|
+
}
|
|
10971
|
+
const prevPhase = phases[currentPhaseIndex - 1];
|
|
10972
|
+
const allCompleted = prevPhase.every((agent) => {
|
|
10973
|
+
const agentState = state.agents.find((a) => a.role === agent.role);
|
|
10974
|
+
return agentState?.status === "completed" || agentState?.status === "failed";
|
|
10975
|
+
});
|
|
10976
|
+
if (allCompleted) {
|
|
10977
|
+
const anyFailed = prevPhase.some((agent) => {
|
|
10978
|
+
const agentState = state.agents.find((a) => a.role === agent.role);
|
|
10979
|
+
return agentState?.status === "failed";
|
|
10980
|
+
});
|
|
10981
|
+
if (anyFailed) {
|
|
10982
|
+
state.status = "partial";
|
|
10983
|
+
saveConvoyState(state);
|
|
10984
|
+
console.log(`[convoy] Phase ${currentPhaseIndex - 1} had failures. Stopping convoy.`);
|
|
10985
|
+
break;
|
|
10986
|
+
}
|
|
10987
|
+
console.log(`[convoy] Starting phase ${currentPhaseIndex}`);
|
|
10988
|
+
await executePhase(state, template, phases[currentPhaseIndex], context);
|
|
10989
|
+
currentPhaseIndex++;
|
|
10990
|
+
}
|
|
10991
|
+
updateAgentStatuses(state);
|
|
10992
|
+
}
|
|
10993
|
+
const finalState = loadConvoyState(convoyId);
|
|
10994
|
+
if (finalState) {
|
|
10995
|
+
const allDone = finalState.agents.every(
|
|
10996
|
+
(a) => a.status === "completed" || a.status === "failed"
|
|
10997
|
+
);
|
|
10998
|
+
if (allDone) {
|
|
10999
|
+
const anyFailed = finalState.agents.some((a) => a.status === "failed");
|
|
11000
|
+
finalState.status = anyFailed ? "partial" : "completed";
|
|
11001
|
+
finalState.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11002
|
+
saveConvoyState(finalState);
|
|
11003
|
+
console.log(`[convoy] Convoy ${convoyId} ${finalState.status}`);
|
|
11004
|
+
}
|
|
11005
|
+
}
|
|
11006
|
+
};
|
|
11007
|
+
monitorLoop().catch((err) => {
|
|
11008
|
+
console.error(`[convoy] Monitor error for ${convoyId}:`, err);
|
|
11009
|
+
});
|
|
11010
|
+
}
|
|
11011
|
+
function updateAgentStatuses(convoy) {
|
|
11012
|
+
let updated = false;
|
|
11013
|
+
for (const agent of convoy.agents) {
|
|
11014
|
+
if (agent.status === "running" && !sessionExists(agent.tmuxSession)) {
|
|
11015
|
+
agent.status = "completed";
|
|
11016
|
+
agent.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11017
|
+
updated = true;
|
|
11018
|
+
if (agent.outputFile && existsSync41(agent.outputFile)) {
|
|
11019
|
+
agent.exitCode = 0;
|
|
11020
|
+
} else {
|
|
11021
|
+
agent.exitCode = 1;
|
|
11022
|
+
agent.status = "failed";
|
|
11023
|
+
}
|
|
11024
|
+
}
|
|
11025
|
+
}
|
|
11026
|
+
if (updated) {
|
|
11027
|
+
saveConvoyState(convoy);
|
|
11028
|
+
}
|
|
11029
|
+
}
|
|
11030
|
+
async function stopConvoy(convoyId) {
|
|
11031
|
+
const state = loadConvoyState(convoyId);
|
|
11032
|
+
if (!state) {
|
|
11033
|
+
throw new Error(`Convoy not found: ${convoyId}`);
|
|
11034
|
+
}
|
|
11035
|
+
for (const agent of state.agents) {
|
|
11036
|
+
if (sessionExists(agent.tmuxSession)) {
|
|
11037
|
+
killSession(agent.tmuxSession);
|
|
11038
|
+
}
|
|
11039
|
+
}
|
|
11040
|
+
state.status = "failed";
|
|
11041
|
+
state.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11042
|
+
saveConvoyState(state);
|
|
11043
|
+
}
|
|
11044
|
+
|
|
11045
|
+
// src/cli/commands/convoy/start.ts
|
|
11046
|
+
async function startCommand2(templateName, options) {
|
|
11047
|
+
const spinner = ora15("Starting convoy...").start();
|
|
11048
|
+
try {
|
|
11049
|
+
const template = getConvoyTemplate(templateName);
|
|
11050
|
+
if (!template) {
|
|
11051
|
+
spinner.fail(chalk37.red(`Unknown template: ${templateName}`));
|
|
11052
|
+
console.log(chalk37.dim("\nAvailable templates: code-review, planning, triage, health-monitor"));
|
|
11053
|
+
process.exit(1);
|
|
11054
|
+
}
|
|
11055
|
+
const context = {
|
|
11056
|
+
projectPath: options.projectPath || process.cwd()
|
|
11057
|
+
};
|
|
11058
|
+
if (options.files) {
|
|
11059
|
+
context.files = [options.files];
|
|
11060
|
+
}
|
|
11061
|
+
if (options.prUrl) {
|
|
11062
|
+
context.prUrl = options.prUrl;
|
|
11063
|
+
}
|
|
11064
|
+
if (options.issueId) {
|
|
11065
|
+
context.issueId = options.issueId;
|
|
11066
|
+
}
|
|
11067
|
+
spinner.text = `Starting convoy: ${template.name}`;
|
|
11068
|
+
const convoy = await startConvoy(templateName, context);
|
|
11069
|
+
spinner.succeed(chalk37.green("Convoy started"));
|
|
11070
|
+
console.log("");
|
|
11071
|
+
console.log(chalk37.bold("Convoy Details:"));
|
|
11072
|
+
console.log(chalk37.dim(" ID: ") + convoy.id);
|
|
11073
|
+
console.log(chalk37.dim(" Template: ") + convoy.template);
|
|
11074
|
+
console.log(chalk37.dim(" Agents: ") + convoy.agents.length);
|
|
11075
|
+
console.log(chalk37.dim(" Output: ") + convoy.outputDir);
|
|
11076
|
+
console.log("");
|
|
11077
|
+
console.log(chalk37.bold("Agents:"));
|
|
11078
|
+
for (const agent of convoy.agents) {
|
|
11079
|
+
const statusColor = agent.status === "running" ? chalk37.green : chalk37.dim;
|
|
11080
|
+
console.log(` ${statusColor(`\u2022 ${agent.role}`)} (${agent.subagent}) - ${agent.status}`);
|
|
11081
|
+
}
|
|
11082
|
+
console.log("");
|
|
11083
|
+
console.log(chalk37.dim("Monitor status with: ") + chalk37.cyan(`pan convoy status ${convoy.id}`));
|
|
11084
|
+
console.log(chalk37.dim("Attach to agent: ") + chalk37.cyan(`tmux attach -t ${convoy.agents[0]?.tmuxSession}`));
|
|
11085
|
+
} catch (error) {
|
|
11086
|
+
spinner.fail(chalk37.red("Failed to start convoy"));
|
|
11087
|
+
if (error instanceof Error) {
|
|
11088
|
+
console.error(chalk37.red("\n" + error.message));
|
|
11089
|
+
}
|
|
11090
|
+
process.exit(1);
|
|
11091
|
+
}
|
|
11092
|
+
}
|
|
11093
|
+
|
|
11094
|
+
// src/cli/commands/convoy/status.ts
|
|
11095
|
+
import chalk38 from "chalk";
|
|
11096
|
+
function statusCommand3(convoyId, options) {
|
|
11097
|
+
let id = convoyId;
|
|
11098
|
+
if (!id) {
|
|
11099
|
+
const running = listConvoys({ status: "running" });
|
|
11100
|
+
if (running.length === 0) {
|
|
11101
|
+
console.log(chalk38.yellow("No running convoys"));
|
|
11102
|
+
return;
|
|
11103
|
+
}
|
|
11104
|
+
id = running[0].id;
|
|
11105
|
+
}
|
|
11106
|
+
const convoy = getConvoyStatus(id);
|
|
11107
|
+
if (!convoy) {
|
|
11108
|
+
console.error(chalk38.red(`Convoy not found: ${id}`));
|
|
11109
|
+
process.exit(1);
|
|
11110
|
+
}
|
|
11111
|
+
if (options.json) {
|
|
11112
|
+
console.log(JSON.stringify(convoy, null, 2));
|
|
11113
|
+
return;
|
|
11114
|
+
}
|
|
11115
|
+
const statusColor = convoy.status === "completed" ? chalk38.green : convoy.status === "running" ? chalk38.cyan : convoy.status === "failed" ? chalk38.red : chalk38.yellow;
|
|
11116
|
+
console.log("");
|
|
11117
|
+
console.log(chalk38.bold("Convoy Status"));
|
|
11118
|
+
console.log(chalk38.dim("\u2500".repeat(60)));
|
|
11119
|
+
console.log(chalk38.dim(" ID: ") + convoy.id);
|
|
11120
|
+
console.log(chalk38.dim(" Template: ") + convoy.template);
|
|
11121
|
+
console.log(chalk38.dim(" Status: ") + statusColor(convoy.status));
|
|
11122
|
+
console.log(chalk38.dim(" Started: ") + new Date(convoy.startedAt).toLocaleString());
|
|
11123
|
+
if (convoy.completedAt) {
|
|
11124
|
+
console.log(chalk38.dim(" Completed:") + new Date(convoy.completedAt).toLocaleString());
|
|
11125
|
+
}
|
|
11126
|
+
console.log(chalk38.dim(" Output: ") + convoy.outputDir);
|
|
11127
|
+
console.log("");
|
|
11128
|
+
console.log(chalk38.bold("Agents:"));
|
|
11129
|
+
for (const agent of convoy.agents) {
|
|
11130
|
+
const statusColor2 = agent.status === "completed" ? chalk38.green : agent.status === "running" ? chalk38.cyan : agent.status === "failed" ? chalk38.red : chalk38.dim;
|
|
11131
|
+
const statusIcon = agent.status === "completed" ? "\u2713" : agent.status === "running" ? "\u27F3" : agent.status === "failed" ? "\u2717" : "\u25CB";
|
|
11132
|
+
console.log(` ${statusColor2(statusIcon)} ${chalk38.bold(agent.role)} (${agent.subagent})`);
|
|
11133
|
+
console.log(` ${chalk38.dim("Status:")} ${statusColor2(agent.status)}`);
|
|
11134
|
+
console.log(` ${chalk38.dim("Tmux:")} ${agent.tmuxSession}`);
|
|
11135
|
+
if (agent.outputFile) {
|
|
11136
|
+
console.log(` ${chalk38.dim("Output:")} ${agent.outputFile}`);
|
|
11137
|
+
}
|
|
11138
|
+
if (agent.startedAt) {
|
|
11139
|
+
console.log(` ${chalk38.dim("Started:")} ${new Date(agent.startedAt).toLocaleString()}`);
|
|
11140
|
+
}
|
|
11141
|
+
if (agent.completedAt) {
|
|
11142
|
+
console.log(` ${chalk38.dim("Completed:")} ${new Date(agent.completedAt).toLocaleString()}`);
|
|
11143
|
+
}
|
|
11144
|
+
console.log("");
|
|
11145
|
+
}
|
|
11146
|
+
if (convoy.status === "running") {
|
|
11147
|
+
console.log(chalk38.dim("Commands:"));
|
|
11148
|
+
const runningAgent = convoy.agents.find((a) => a.status === "running");
|
|
11149
|
+
if (runningAgent) {
|
|
11150
|
+
console.log(chalk38.dim(" Attach to agent: ") + chalk38.cyan(`tmux attach -t ${runningAgent.tmuxSession}`));
|
|
11151
|
+
}
|
|
11152
|
+
console.log(chalk38.dim(" Stop convoy: ") + chalk38.cyan(`pan convoy stop ${convoy.id}`));
|
|
11153
|
+
}
|
|
11154
|
+
if (convoy.status === "completed") {
|
|
11155
|
+
console.log(chalk38.dim("View outputs in: ") + chalk38.cyan(convoy.outputDir));
|
|
11156
|
+
}
|
|
11157
|
+
console.log("");
|
|
11158
|
+
}
|
|
11159
|
+
|
|
11160
|
+
// src/cli/commands/convoy/list.ts
|
|
11161
|
+
import chalk39 from "chalk";
|
|
11162
|
+
function listCommand4(options) {
|
|
11163
|
+
const filter = options.status ? { status: options.status } : void 0;
|
|
11164
|
+
const convoys = listConvoys(filter);
|
|
11165
|
+
if (convoys.length === 0) {
|
|
11166
|
+
console.log(chalk39.yellow("No convoys found"));
|
|
11167
|
+
return;
|
|
11168
|
+
}
|
|
11169
|
+
if (options.json) {
|
|
11170
|
+
console.log(JSON.stringify(convoys, null, 2));
|
|
11171
|
+
return;
|
|
11172
|
+
}
|
|
11173
|
+
console.log("");
|
|
11174
|
+
console.log(chalk39.bold("Convoys"));
|
|
11175
|
+
console.log(chalk39.dim("\u2500".repeat(80)));
|
|
11176
|
+
console.log("");
|
|
11177
|
+
for (const convoy of convoys) {
|
|
11178
|
+
const statusColor = convoy.status === "completed" ? chalk39.green : convoy.status === "running" ? chalk39.cyan : convoy.status === "failed" ? chalk39.red : chalk39.yellow;
|
|
11179
|
+
const statusIcon = convoy.status === "completed" ? "\u2713" : convoy.status === "running" ? "\u27F3" : convoy.status === "failed" ? "\u2717" : "\u25CB";
|
|
11180
|
+
console.log(chalk39.bold(`${statusColor(statusIcon)} ${convoy.id}`));
|
|
11181
|
+
console.log(` ${chalk39.dim("Template:")} ${convoy.template}`);
|
|
11182
|
+
console.log(` ${chalk39.dim("Status:")} ${statusColor(convoy.status)}`);
|
|
11183
|
+
console.log(` ${chalk39.dim("Started:")} ${new Date(convoy.startedAt).toLocaleString()}`);
|
|
11184
|
+
if (convoy.completedAt) {
|
|
11185
|
+
console.log(` ${chalk39.dim("Completed:")} ${new Date(convoy.completedAt).toLocaleString()}`);
|
|
11186
|
+
}
|
|
11187
|
+
const agentCounts = convoy.agents.reduce((acc, agent) => {
|
|
11188
|
+
acc[agent.status] = (acc[agent.status] || 0) + 1;
|
|
11189
|
+
return acc;
|
|
11190
|
+
}, {});
|
|
11191
|
+
const summary = Object.entries(agentCounts).map(([status, count]) => `${count} ${status}`).join(", ");
|
|
11192
|
+
console.log(` ${chalk39.dim("Agents:")} ${summary}`);
|
|
11193
|
+
console.log("");
|
|
11194
|
+
}
|
|
11195
|
+
console.log(chalk39.dim(`Total: ${convoys.length} convoy(s)`));
|
|
11196
|
+
console.log("");
|
|
11197
|
+
console.log(chalk39.dim("Use ") + chalk39.cyan("pan convoy status <id>") + chalk39.dim(" to see details"));
|
|
11198
|
+
console.log("");
|
|
11199
|
+
}
|
|
11200
|
+
|
|
11201
|
+
// src/cli/commands/convoy/stop.ts
|
|
11202
|
+
import chalk40 from "chalk";
|
|
11203
|
+
import ora16 from "ora";
|
|
11204
|
+
import inquirer4 from "inquirer";
|
|
11205
|
+
async function stopCommand2(convoyId, options) {
|
|
11206
|
+
const convoy = getConvoyStatus(convoyId);
|
|
11207
|
+
if (!convoy) {
|
|
11208
|
+
console.error(chalk40.red(`Convoy not found: ${convoyId}`));
|
|
11209
|
+
process.exit(1);
|
|
11210
|
+
}
|
|
11211
|
+
if (convoy.status !== "running") {
|
|
11212
|
+
console.log(chalk40.yellow(`Convoy is not running (status: ${convoy.status})`));
|
|
11213
|
+
return;
|
|
11214
|
+
}
|
|
11215
|
+
if (!options.force) {
|
|
11216
|
+
const runningAgents = convoy.agents.filter((a) => a.status === "running");
|
|
11217
|
+
console.log("");
|
|
11218
|
+
console.log(chalk40.bold(`Stopping convoy: ${convoy.id}`));
|
|
11219
|
+
console.log(chalk40.dim(` Template: ${convoy.template}`));
|
|
11220
|
+
console.log(chalk40.dim(` Running agents: ${runningAgents.length}`));
|
|
11221
|
+
console.log("");
|
|
11222
|
+
const { confirmed } = await inquirer4.prompt([
|
|
11223
|
+
{
|
|
11224
|
+
type: "confirm",
|
|
11225
|
+
name: "confirmed",
|
|
11226
|
+
message: "Are you sure you want to stop this convoy?",
|
|
11227
|
+
default: false
|
|
11228
|
+
}
|
|
11229
|
+
]);
|
|
11230
|
+
if (!confirmed) {
|
|
11231
|
+
console.log(chalk40.dim("Cancelled"));
|
|
11232
|
+
return;
|
|
11233
|
+
}
|
|
11234
|
+
}
|
|
11235
|
+
const spinner = ora16("Stopping convoy...").start();
|
|
11236
|
+
try {
|
|
11237
|
+
await stopConvoy(convoyId);
|
|
11238
|
+
spinner.succeed(chalk40.green("Convoy stopped"));
|
|
11239
|
+
} catch (error) {
|
|
11240
|
+
spinner.fail(chalk40.red("Failed to stop convoy"));
|
|
11241
|
+
if (error instanceof Error) {
|
|
11242
|
+
console.error(chalk40.red("\n" + error.message));
|
|
11243
|
+
}
|
|
11244
|
+
process.exit(1);
|
|
11245
|
+
}
|
|
11246
|
+
}
|
|
11247
|
+
|
|
11248
|
+
// src/cli/commands/convoy/index.ts
|
|
11249
|
+
function registerConvoyCommands(program2) {
|
|
11250
|
+
const convoy = program2.command("convoy").description("Multi-agent convoy orchestration");
|
|
11251
|
+
convoy.command("start <template>").description("Start a new convoy").option("--files <pattern>", 'File pattern (e.g., "src/**/*.ts")').option("--pr-url <url>", "Pull request URL").option("--issue-id <id>", "Issue ID").option("--project-path <path>", "Project path (defaults to cwd)").action(startCommand2);
|
|
11252
|
+
convoy.command("status [convoy-id]").description("Show convoy status (current convoy if no ID specified)").option("--json", "Output as JSON").action(statusCommand3);
|
|
11253
|
+
convoy.command("list").description("List all convoys").option("--status <status>", "Filter by status (running|completed|failed|partial)").option("--json", "Output as JSON").action(listCommand4);
|
|
11254
|
+
convoy.command("stop <convoy-id>").description("Stop a running convoy").option("--force", "Skip confirmation").action(stopCommand2);
|
|
11255
|
+
}
|
|
11256
|
+
|
|
11257
|
+
// src/cli/commands/project.ts
|
|
11258
|
+
import chalk41 from "chalk";
|
|
11259
|
+
import { existsSync as existsSync42, readFileSync as readFileSync34, symlinkSync as symlinkSync4, mkdirSync as mkdirSync27, readdirSync as readdirSync15, statSync as statSync5 } from "fs";
|
|
11260
|
+
import { join as join42, resolve, dirname as dirname9 } from "path";
|
|
10406
11261
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10407
11262
|
var __filename4 = fileURLToPath3(import.meta.url);
|
|
10408
11263
|
var __dirname4 = dirname9(__filename4);
|
|
10409
|
-
var BUNDLED_HOOKS_DIR =
|
|
11264
|
+
var BUNDLED_HOOKS_DIR = join42(__dirname4, "..", "..", "scripts", "git-hooks");
|
|
10410
11265
|
function installGitHooks(gitDir) {
|
|
10411
|
-
const hooksTarget =
|
|
11266
|
+
const hooksTarget = join42(gitDir, "hooks");
|
|
10412
11267
|
let installed = 0;
|
|
10413
|
-
if (!
|
|
10414
|
-
|
|
11268
|
+
if (!existsSync42(hooksTarget)) {
|
|
11269
|
+
mkdirSync27(hooksTarget, { recursive: true });
|
|
10415
11270
|
}
|
|
10416
|
-
if (!
|
|
11271
|
+
if (!existsSync42(BUNDLED_HOOKS_DIR)) {
|
|
10417
11272
|
return 0;
|
|
10418
11273
|
}
|
|
10419
11274
|
try {
|
|
10420
|
-
const hooks =
|
|
10421
|
-
const p =
|
|
10422
|
-
return
|
|
11275
|
+
const hooks = readdirSync15(BUNDLED_HOOKS_DIR).filter((f) => {
|
|
11276
|
+
const p = join42(BUNDLED_HOOKS_DIR, f);
|
|
11277
|
+
return existsSync42(p) && statSync5(p).isFile();
|
|
10423
11278
|
});
|
|
10424
11279
|
for (const hook of hooks) {
|
|
10425
|
-
const source =
|
|
10426
|
-
const target =
|
|
10427
|
-
if (
|
|
11280
|
+
const source = join42(BUNDLED_HOOKS_DIR, hook);
|
|
11281
|
+
const target = join42(hooksTarget, hook);
|
|
11282
|
+
if (existsSync42(target)) {
|
|
10428
11283
|
try {
|
|
10429
11284
|
const { readlinkSync: readlinkSync2 } = __require("fs");
|
|
10430
11285
|
if (readlinkSync2(target) === source) {
|
|
@@ -10433,9 +11288,9 @@ function installGitHooks(gitDir) {
|
|
|
10433
11288
|
} catch {
|
|
10434
11289
|
}
|
|
10435
11290
|
}
|
|
10436
|
-
if (
|
|
10437
|
-
const { renameSync } = __require("fs");
|
|
10438
|
-
|
|
11291
|
+
if (existsSync42(target)) {
|
|
11292
|
+
const { renameSync: renameSync2 } = __require("fs");
|
|
11293
|
+
renameSync2(target, `${target}.backup`);
|
|
10439
11294
|
}
|
|
10440
11295
|
symlinkSync4(source, target);
|
|
10441
11296
|
installed++;
|
|
@@ -10446,24 +11301,24 @@ function installGitHooks(gitDir) {
|
|
|
10446
11301
|
}
|
|
10447
11302
|
async function projectAddCommand(projectPath, options = {}) {
|
|
10448
11303
|
const fullPath = resolve(projectPath);
|
|
10449
|
-
if (!
|
|
10450
|
-
console.log(
|
|
11304
|
+
if (!existsSync42(fullPath)) {
|
|
11305
|
+
console.log(chalk41.red(`Path does not exist: ${fullPath}`));
|
|
10451
11306
|
return;
|
|
10452
11307
|
}
|
|
10453
11308
|
const name = options.name || fullPath.split("/").pop() || "unknown";
|
|
10454
11309
|
const key = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
10455
11310
|
const existing = getProject(key);
|
|
10456
11311
|
if (existing) {
|
|
10457
|
-
console.log(
|
|
10458
|
-
console.log(
|
|
10459
|
-
console.log(
|
|
11312
|
+
console.log(chalk41.yellow(`Project already registered with key: ${key}`));
|
|
11313
|
+
console.log(chalk41.dim(`Existing path: ${existing.path}`));
|
|
11314
|
+
console.log(chalk41.dim(`To update, first run: pan project remove ${key}`));
|
|
10460
11315
|
return;
|
|
10461
11316
|
}
|
|
10462
11317
|
let linearTeam = options.linearTeam;
|
|
10463
11318
|
if (!linearTeam) {
|
|
10464
|
-
const projectToml =
|
|
10465
|
-
if (
|
|
10466
|
-
const content =
|
|
11319
|
+
const projectToml = join42(fullPath, ".panopticon", "project.toml");
|
|
11320
|
+
if (existsSync42(projectToml)) {
|
|
11321
|
+
const content = readFileSync34(projectToml, "utf-8");
|
|
10467
11322
|
const match = content.match(/team\s*=\s*"([^"]+)"/);
|
|
10468
11323
|
if (match) linearTeam = match[1];
|
|
10469
11324
|
}
|
|
@@ -10476,26 +11331,26 @@ async function projectAddCommand(projectPath, options = {}) {
|
|
|
10476
11331
|
projectConfig.linear_team = linearTeam.toUpperCase();
|
|
10477
11332
|
}
|
|
10478
11333
|
registerProject(key, projectConfig);
|
|
10479
|
-
console.log(
|
|
10480
|
-
console.log(
|
|
10481
|
-
console.log(
|
|
11334
|
+
console.log(chalk41.green(`\u2713 Added project: ${name}`));
|
|
11335
|
+
console.log(chalk41.dim(` Key: ${key}`));
|
|
11336
|
+
console.log(chalk41.dim(` Path: ${fullPath}`));
|
|
10482
11337
|
if (linearTeam) {
|
|
10483
|
-
console.log(
|
|
11338
|
+
console.log(chalk41.dim(` Linear team: ${linearTeam}`));
|
|
10484
11339
|
}
|
|
10485
11340
|
console.log("");
|
|
10486
|
-
const hasDevcontainer =
|
|
10487
|
-
const hasInfra =
|
|
10488
|
-
const hasDevcontainerTemplate =
|
|
10489
|
-
const hasRootGit =
|
|
11341
|
+
const hasDevcontainer = existsSync42(join42(fullPath, ".devcontainer"));
|
|
11342
|
+
const hasInfra = existsSync42(join42(fullPath, "infra"));
|
|
11343
|
+
const hasDevcontainerTemplate = existsSync42(join42(fullPath, "infra", ".devcontainer-template")) || existsSync42(join42(fullPath, ".devcontainer-template"));
|
|
11344
|
+
const hasRootGit = existsSync42(join42(fullPath, ".git"));
|
|
10490
11345
|
const subRepos = [];
|
|
10491
11346
|
if (!hasRootGit) {
|
|
10492
|
-
const { readdirSync:
|
|
11347
|
+
const { readdirSync: readdirSync17, statSync: statSync7 } = await import("fs");
|
|
10493
11348
|
try {
|
|
10494
|
-
const entries =
|
|
11349
|
+
const entries = readdirSync17(fullPath);
|
|
10495
11350
|
for (const entry of entries) {
|
|
10496
|
-
const entryPath =
|
|
11351
|
+
const entryPath = join42(fullPath, entry);
|
|
10497
11352
|
try {
|
|
10498
|
-
if (statSync7(entryPath).isDirectory() &&
|
|
11353
|
+
if (statSync7(entryPath).isDirectory() && existsSync42(join42(entryPath, ".git"))) {
|
|
10499
11354
|
subRepos.push(entry);
|
|
10500
11355
|
}
|
|
10501
11356
|
} catch {
|
|
@@ -10507,72 +11362,72 @@ async function projectAddCommand(projectPath, options = {}) {
|
|
|
10507
11362
|
const isPolyrepo = !hasRootGit && subRepos.length > 0;
|
|
10508
11363
|
let hooksInstalled = 0;
|
|
10509
11364
|
if (hasRootGit) {
|
|
10510
|
-
hooksInstalled = installGitHooks(
|
|
11365
|
+
hooksInstalled = installGitHooks(join42(fullPath, ".git"));
|
|
10511
11366
|
if (hooksInstalled > 0) {
|
|
10512
|
-
console.log(
|
|
11367
|
+
console.log(chalk41.green(`\u2713 Installed ${hooksInstalled} git hook(s) for branch protection`));
|
|
10513
11368
|
}
|
|
10514
11369
|
} else if (isPolyrepo) {
|
|
10515
11370
|
for (const repo of subRepos) {
|
|
10516
|
-
const count = installGitHooks(
|
|
11371
|
+
const count = installGitHooks(join42(fullPath, repo, ".git"));
|
|
10517
11372
|
hooksInstalled += count;
|
|
10518
11373
|
}
|
|
10519
11374
|
if (hooksInstalled > 0) {
|
|
10520
|
-
console.log(
|
|
11375
|
+
console.log(chalk41.green(`\u2713 Installed git hooks in ${subRepos.length} repositories`));
|
|
10521
11376
|
}
|
|
10522
11377
|
}
|
|
10523
11378
|
if (hooksInstalled > 0) {
|
|
10524
|
-
console.log(
|
|
11379
|
+
console.log(chalk41.dim(" (Prevents agents from checking out branches in main project)"));
|
|
10525
11380
|
console.log("");
|
|
10526
11381
|
}
|
|
10527
|
-
console.log(
|
|
11382
|
+
console.log(chalk41.bold("Next Steps:\n"));
|
|
10528
11383
|
if (isPolyrepo) {
|
|
10529
|
-
console.log(
|
|
10530
|
-
console.log(
|
|
11384
|
+
console.log(chalk41.yellow.bold("\u26A0\uFE0F POLYREPO DETECTED"));
|
|
11385
|
+
console.log(chalk41.yellow(` Found ${subRepos.length} git repositories: ${subRepos.join(", ")}`));
|
|
10531
11386
|
console.log("");
|
|
10532
|
-
console.log(
|
|
10533
|
-
console.log(
|
|
11387
|
+
console.log(chalk41.cyan("0. Configure as polyrepo"));
|
|
11388
|
+
console.log(chalk41.dim(` Edit ${PROJECTS_CONFIG_FILE} and add:`));
|
|
10534
11389
|
console.log("");
|
|
10535
|
-
console.log(
|
|
10536
|
-
console.log(
|
|
10537
|
-
console.log(
|
|
10538
|
-
console.log(
|
|
10539
|
-
console.log(
|
|
11390
|
+
console.log(chalk41.dim(" workspace:"));
|
|
11391
|
+
console.log(chalk41.dim(" type: polyrepo"));
|
|
11392
|
+
console.log(chalk41.dim(" workspaces_dir: workspaces"));
|
|
11393
|
+
console.log(chalk41.dim(" default_branch: main"));
|
|
11394
|
+
console.log(chalk41.dim(" repos:"));
|
|
10540
11395
|
for (const repo of subRepos) {
|
|
10541
|
-
console.log(
|
|
10542
|
-
console.log(
|
|
10543
|
-
console.log(
|
|
11396
|
+
console.log(chalk41.dim(` - name: ${repo}`));
|
|
11397
|
+
console.log(chalk41.dim(` path: ${repo}`));
|
|
11398
|
+
console.log(chalk41.dim(` branch_prefix: "feature/"`));
|
|
10544
11399
|
}
|
|
10545
11400
|
console.log("");
|
|
10546
|
-
console.log(
|
|
11401
|
+
console.log(chalk41.dim(' See README "Polyrepo Workspace Configuration" for full example.'));
|
|
10547
11402
|
console.log("");
|
|
10548
11403
|
}
|
|
10549
|
-
console.log(
|
|
10550
|
-
console.log(
|
|
10551
|
-
console.log(
|
|
11404
|
+
console.log(chalk41.cyan(`${isPolyrepo ? "1" : "1"}. Configure workspace settings`));
|
|
11405
|
+
console.log(chalk41.dim(` Edit ${PROJECTS_CONFIG_FILE}`));
|
|
11406
|
+
console.log(chalk41.dim(" Add workspace, dns, docker, and service configuration"));
|
|
10552
11407
|
console.log("");
|
|
10553
11408
|
if (!hasDevcontainerTemplate && !hasDevcontainer) {
|
|
10554
|
-
console.log(
|
|
10555
|
-
console.log(
|
|
10556
|
-
console.log(
|
|
10557
|
-
console.log(
|
|
10558
|
-
console.log(
|
|
11409
|
+
console.log(chalk41.cyan("2. Create workspace templates (for Docker-based workspaces)"));
|
|
11410
|
+
console.log(chalk41.dim(" Your project needs:"));
|
|
11411
|
+
console.log(chalk41.dim(" \u2022 infra/.devcontainer-template/docker-compose.devcontainer.yml.template"));
|
|
11412
|
+
console.log(chalk41.dim(" \u2022 infra/.devcontainer-template/Dockerfile"));
|
|
11413
|
+
console.log(chalk41.dim(' See README "What Your Project Needs to Provide" section'));
|
|
10559
11414
|
console.log("");
|
|
10560
11415
|
} else {
|
|
10561
|
-
console.log(
|
|
11416
|
+
console.log(chalk41.green("\u2713 Found existing container templates"));
|
|
10562
11417
|
console.log("");
|
|
10563
11418
|
}
|
|
10564
|
-
console.log(
|
|
10565
|
-
console.log(
|
|
10566
|
-
console.log(
|
|
11419
|
+
console.log(chalk41.cyan(`${hasDevcontainerTemplate || hasDevcontainer ? "2" : "3"}. Test workspace creation`));
|
|
11420
|
+
console.log(chalk41.dim(" pan workspace create TEST-123"));
|
|
11421
|
+
console.log(chalk41.dim(" pan workspace destroy TEST-123"));
|
|
10567
11422
|
console.log("");
|
|
10568
|
-
console.log(
|
|
11423
|
+
console.log(chalk41.dim("Documentation: https://github.com/eltmon/panopticon#what-your-project-needs-to-provide"));
|
|
10569
11424
|
}
|
|
10570
11425
|
async function projectListCommand(options = {}) {
|
|
10571
11426
|
const projects = listProjects();
|
|
10572
11427
|
if (projects.length === 0) {
|
|
10573
|
-
console.log(
|
|
10574
|
-
console.log(
|
|
10575
|
-
console.log(
|
|
11428
|
+
console.log(chalk41.dim("No projects registered."));
|
|
11429
|
+
console.log(chalk41.dim("Add one with: pan project add <path> --linear-team <TEAM>"));
|
|
11430
|
+
console.log(chalk41.dim(`Or edit: ${PROJECTS_CONFIG_FILE}`));
|
|
10576
11431
|
return;
|
|
10577
11432
|
}
|
|
10578
11433
|
if (options.json) {
|
|
@@ -10583,51 +11438,51 @@ async function projectListCommand(options = {}) {
|
|
|
10583
11438
|
console.log(JSON.stringify(output, null, 2));
|
|
10584
11439
|
return;
|
|
10585
11440
|
}
|
|
10586
|
-
console.log(
|
|
11441
|
+
console.log(chalk41.bold("\nRegistered Projects:\n"));
|
|
10587
11442
|
for (const { key, config: config2 } of projects) {
|
|
10588
|
-
const exists =
|
|
10589
|
-
const statusIcon = exists ?
|
|
10590
|
-
console.log(`${statusIcon} ${
|
|
10591
|
-
console.log(` ${
|
|
11443
|
+
const exists = existsSync42(config2.path);
|
|
11444
|
+
const statusIcon = exists ? chalk41.green("\u2713") : chalk41.red("\u2717");
|
|
11445
|
+
console.log(`${statusIcon} ${chalk41.bold(config2.name)} ${chalk41.dim(`(${key})`)}`);
|
|
11446
|
+
console.log(` ${chalk41.dim(config2.path)}`);
|
|
10592
11447
|
if (config2.linear_team) {
|
|
10593
|
-
console.log(` ${
|
|
11448
|
+
console.log(` ${chalk41.cyan(`Linear: ${config2.linear_team}`)}`);
|
|
10594
11449
|
}
|
|
10595
11450
|
if (config2.issue_routing && config2.issue_routing.length > 0) {
|
|
10596
|
-
console.log(` ${
|
|
11451
|
+
console.log(` ${chalk41.dim(`Routes: ${config2.issue_routing.length} rules`)}`);
|
|
10597
11452
|
}
|
|
10598
11453
|
console.log("");
|
|
10599
11454
|
}
|
|
10600
|
-
console.log(
|
|
11455
|
+
console.log(chalk41.dim(`Config: ${PROJECTS_CONFIG_FILE}`));
|
|
10601
11456
|
}
|
|
10602
11457
|
async function projectRemoveCommand(nameOrPath) {
|
|
10603
11458
|
const projects = listProjects();
|
|
10604
11459
|
if (unregisterProject(nameOrPath)) {
|
|
10605
|
-
console.log(
|
|
11460
|
+
console.log(chalk41.green(`\u2713 Removed project: ${nameOrPath}`));
|
|
10606
11461
|
return;
|
|
10607
11462
|
}
|
|
10608
11463
|
for (const { key, config: config2 } of projects) {
|
|
10609
11464
|
if (config2.name === nameOrPath || config2.path === resolve(nameOrPath)) {
|
|
10610
11465
|
unregisterProject(key);
|
|
10611
|
-
console.log(
|
|
11466
|
+
console.log(chalk41.green(`\u2713 Removed project: ${config2.name}`));
|
|
10612
11467
|
return;
|
|
10613
11468
|
}
|
|
10614
11469
|
}
|
|
10615
|
-
console.log(
|
|
10616
|
-
console.log(
|
|
11470
|
+
console.log(chalk41.red(`Project not found: ${nameOrPath}`));
|
|
11471
|
+
console.log(chalk41.dim(`Use 'pan project list' to see registered projects.`));
|
|
10617
11472
|
}
|
|
10618
11473
|
async function projectInitCommand() {
|
|
10619
|
-
if (
|
|
10620
|
-
console.log(
|
|
11474
|
+
if (existsSync42(PROJECTS_CONFIG_FILE)) {
|
|
11475
|
+
console.log(chalk41.yellow(`Config already exists: ${PROJECTS_CONFIG_FILE}`));
|
|
10621
11476
|
return;
|
|
10622
11477
|
}
|
|
10623
11478
|
initializeProjectsConfig();
|
|
10624
|
-
console.log(
|
|
11479
|
+
console.log(chalk41.green("\u2713 Projects config initialized"));
|
|
10625
11480
|
console.log("");
|
|
10626
|
-
console.log(
|
|
11481
|
+
console.log(chalk41.dim(`Edit ${PROJECTS_CONFIG_FILE} to add your projects.`));
|
|
10627
11482
|
console.log("");
|
|
10628
|
-
console.log(
|
|
11483
|
+
console.log(chalk41.bold("Quick start:"));
|
|
10629
11484
|
console.log(
|
|
10630
|
-
|
|
11485
|
+
chalk41.dim(
|
|
10631
11486
|
' pan project add /path/to/project --name "My Project" --linear-team MIN'
|
|
10632
11487
|
)
|
|
10633
11488
|
);
|
|
@@ -10646,13 +11501,13 @@ async function projectShowCommand(keyOrName) {
|
|
|
10646
11501
|
}
|
|
10647
11502
|
}
|
|
10648
11503
|
if (!found) {
|
|
10649
|
-
console.error(
|
|
10650
|
-
console.log(
|
|
11504
|
+
console.error(chalk41.red(`Project not found: ${keyOrName}`));
|
|
11505
|
+
console.log(chalk41.dim(`Use 'pan project list' to see registered projects.`));
|
|
10651
11506
|
process.exit(1);
|
|
10652
11507
|
}
|
|
10653
|
-
const pathExists =
|
|
10654
|
-
const pathStatus = pathExists ?
|
|
10655
|
-
console.log(
|
|
11508
|
+
const pathExists = existsSync42(found.path);
|
|
11509
|
+
const pathStatus = pathExists ? chalk41.green("\u2713") : chalk41.red("\u2717");
|
|
11510
|
+
console.log(chalk41.bold(`
|
|
10656
11511
|
Project: ${foundKey}
|
|
10657
11512
|
`));
|
|
10658
11513
|
console.log(` Name: ${found.name}`);
|
|
@@ -10661,7 +11516,7 @@ Project: ${foundKey}
|
|
|
10661
11516
|
console.log(` Team: ${found.linear_team}`);
|
|
10662
11517
|
}
|
|
10663
11518
|
if (found.issue_routing && found.issue_routing.length > 0) {
|
|
10664
|
-
console.log("\n " +
|
|
11519
|
+
console.log("\n " + chalk41.bold("Routing Rules:"));
|
|
10665
11520
|
for (const rule of found.issue_routing) {
|
|
10666
11521
|
if (rule.labels) {
|
|
10667
11522
|
console.log(` Labels: ${rule.labels.join(", ")}`);
|
|
@@ -10676,33 +11531,33 @@ Project: ${foundKey}
|
|
|
10676
11531
|
}
|
|
10677
11532
|
|
|
10678
11533
|
// src/cli/commands/doctor.ts
|
|
10679
|
-
import
|
|
10680
|
-
import { existsSync as
|
|
10681
|
-
import { execSync as
|
|
10682
|
-
import { homedir as
|
|
10683
|
-
import { join as
|
|
10684
|
-
function
|
|
11534
|
+
import chalk42 from "chalk";
|
|
11535
|
+
import { existsSync as existsSync43, readdirSync as readdirSync16, readFileSync as readFileSync35 } from "fs";
|
|
11536
|
+
import { execSync as execSync8 } from "child_process";
|
|
11537
|
+
import { homedir as homedir18 } from "os";
|
|
11538
|
+
import { join as join43 } from "path";
|
|
11539
|
+
function checkCommand3(cmd) {
|
|
10685
11540
|
try {
|
|
10686
|
-
|
|
11541
|
+
execSync8(`which ${cmd}`, { encoding: "utf-8", stdio: "pipe" });
|
|
10687
11542
|
return true;
|
|
10688
11543
|
} catch {
|
|
10689
11544
|
return false;
|
|
10690
11545
|
}
|
|
10691
11546
|
}
|
|
10692
11547
|
function checkDirectory(path) {
|
|
10693
|
-
return
|
|
11548
|
+
return existsSync43(path);
|
|
10694
11549
|
}
|
|
10695
11550
|
function countItems(path) {
|
|
10696
|
-
if (!
|
|
11551
|
+
if (!existsSync43(path)) return 0;
|
|
10697
11552
|
try {
|
|
10698
|
-
return
|
|
11553
|
+
return readdirSync16(path).length;
|
|
10699
11554
|
} catch {
|
|
10700
11555
|
return 0;
|
|
10701
11556
|
}
|
|
10702
11557
|
}
|
|
10703
11558
|
async function doctorCommand() {
|
|
10704
|
-
console.log(
|
|
10705
|
-
console.log(
|
|
11559
|
+
console.log(chalk42.bold("\nPanopticon Doctor\n"));
|
|
11560
|
+
console.log(chalk42.dim("Checking system health...\n"));
|
|
10706
11561
|
const checks = [];
|
|
10707
11562
|
const requiredCommands = [
|
|
10708
11563
|
{ cmd: "git", name: "Git", fix: "Install git" },
|
|
@@ -10711,7 +11566,7 @@ async function doctorCommand() {
|
|
|
10711
11566
|
{ cmd: "claude", name: "Claude CLI", fix: "Install: npm install -g @anthropic-ai/claude-code" }
|
|
10712
11567
|
];
|
|
10713
11568
|
for (const { cmd, name, fix } of requiredCommands) {
|
|
10714
|
-
if (
|
|
11569
|
+
if (checkCommand3(cmd)) {
|
|
10715
11570
|
checks.push({ name, status: "ok", message: "Installed" });
|
|
10716
11571
|
} else {
|
|
10717
11572
|
checks.push({ name, status: "error", message: "Not found", fix });
|
|
@@ -10723,7 +11578,7 @@ async function doctorCommand() {
|
|
|
10723
11578
|
{ cmd: "docker", name: "Docker", fix: "Install Docker for workspace containers" }
|
|
10724
11579
|
];
|
|
10725
11580
|
for (const { cmd, name, fix } of optionalCommands) {
|
|
10726
|
-
if (
|
|
11581
|
+
if (checkCommand3(cmd)) {
|
|
10727
11582
|
checks.push({ name, status: "ok", message: "Installed" });
|
|
10728
11583
|
} else {
|
|
10729
11584
|
checks.push({ name, status: "warn", message: "Not installed (optional)", fix });
|
|
@@ -10744,8 +11599,8 @@ async function doctorCommand() {
|
|
|
10744
11599
|
}
|
|
10745
11600
|
}
|
|
10746
11601
|
if (checkDirectory(CLAUDE_DIR)) {
|
|
10747
|
-
const skillsCount = countItems(
|
|
10748
|
-
const commandsCount = countItems(
|
|
11602
|
+
const skillsCount = countItems(join43(CLAUDE_DIR, "skills"));
|
|
11603
|
+
const commandsCount = countItems(join43(CLAUDE_DIR, "commands"));
|
|
10749
11604
|
checks.push({
|
|
10750
11605
|
name: "Claude Code Skills",
|
|
10751
11606
|
status: skillsCount > 0 ? "ok" : "warn",
|
|
@@ -10766,8 +11621,8 @@ async function doctorCommand() {
|
|
|
10766
11621
|
fix: "Install Claude Code first"
|
|
10767
11622
|
});
|
|
10768
11623
|
}
|
|
10769
|
-
const envFile =
|
|
10770
|
-
if (
|
|
11624
|
+
const envFile = join43(homedir18(), ".panopticon.env");
|
|
11625
|
+
if (existsSync43(envFile)) {
|
|
10771
11626
|
checks.push({ name: "Config File", status: "ok", message: "~/.panopticon.env exists" });
|
|
10772
11627
|
} else {
|
|
10773
11628
|
checks.push({
|
|
@@ -10779,8 +11634,8 @@ async function doctorCommand() {
|
|
|
10779
11634
|
}
|
|
10780
11635
|
if (process.env.LINEAR_API_KEY) {
|
|
10781
11636
|
checks.push({ name: "LINEAR_API_KEY", status: "ok", message: "Set in environment" });
|
|
10782
|
-
} else if (
|
|
10783
|
-
const content =
|
|
11637
|
+
} else if (existsSync43(envFile)) {
|
|
11638
|
+
const content = readFileSync35(envFile, "utf-8");
|
|
10784
11639
|
if (content.includes("LINEAR_API_KEY")) {
|
|
10785
11640
|
checks.push({ name: "LINEAR_API_KEY", status: "ok", message: "Set in config file" });
|
|
10786
11641
|
} else {
|
|
@@ -10800,7 +11655,7 @@ async function doctorCommand() {
|
|
|
10800
11655
|
});
|
|
10801
11656
|
}
|
|
10802
11657
|
try {
|
|
10803
|
-
const sessions =
|
|
11658
|
+
const sessions = execSync8("tmux list-sessions 2>/dev/null || true", { encoding: "utf-8" });
|
|
10804
11659
|
const agentSessions = sessions.split("\n").filter((s) => s.includes("agent-")).length;
|
|
10805
11660
|
checks.push({
|
|
10806
11661
|
name: "Running Agents",
|
|
@@ -10815,46 +11670,46 @@ async function doctorCommand() {
|
|
|
10815
11670
|
});
|
|
10816
11671
|
}
|
|
10817
11672
|
const icons = {
|
|
10818
|
-
ok:
|
|
10819
|
-
warn:
|
|
10820
|
-
error:
|
|
11673
|
+
ok: chalk42.green("\u2713"),
|
|
11674
|
+
warn: chalk42.yellow("\u26A0"),
|
|
11675
|
+
error: chalk42.red("\u2717")
|
|
10821
11676
|
};
|
|
10822
11677
|
let hasErrors = false;
|
|
10823
11678
|
let hasWarnings = false;
|
|
10824
11679
|
for (const check of checks) {
|
|
10825
11680
|
const icon = icons[check.status];
|
|
10826
|
-
const message = check.status === "error" ?
|
|
11681
|
+
const message = check.status === "error" ? chalk42.red(check.message) : check.status === "warn" ? chalk42.yellow(check.message) : chalk42.dim(check.message);
|
|
10827
11682
|
console.log(`${icon} ${check.name}: ${message}`);
|
|
10828
11683
|
if (check.fix && check.status !== "ok") {
|
|
10829
|
-
console.log(
|
|
11684
|
+
console.log(chalk42.dim(` Fix: ${check.fix}`));
|
|
10830
11685
|
}
|
|
10831
11686
|
if (check.status === "error") hasErrors = true;
|
|
10832
11687
|
if (check.status === "warn") hasWarnings = true;
|
|
10833
11688
|
}
|
|
10834
11689
|
console.log("");
|
|
10835
11690
|
if (hasErrors) {
|
|
10836
|
-
console.log(
|
|
10837
|
-
console.log(
|
|
11691
|
+
console.log(chalk42.red("Some required components are missing."));
|
|
11692
|
+
console.log(chalk42.dim("Fix the errors above before using Panopticon."));
|
|
10838
11693
|
} else if (hasWarnings) {
|
|
10839
|
-
console.log(
|
|
11694
|
+
console.log(chalk42.yellow("System is functional with some optional features missing."));
|
|
10840
11695
|
} else {
|
|
10841
|
-
console.log(
|
|
11696
|
+
console.log(chalk42.green("All systems operational!"));
|
|
10842
11697
|
}
|
|
10843
11698
|
console.log("");
|
|
10844
11699
|
}
|
|
10845
11700
|
|
|
10846
11701
|
// src/cli/commands/update.ts
|
|
10847
|
-
import { execSync as
|
|
10848
|
-
import
|
|
10849
|
-
import { readFileSync as
|
|
11702
|
+
import { execSync as execSync9 } from "child_process";
|
|
11703
|
+
import chalk43 from "chalk";
|
|
11704
|
+
import { readFileSync as readFileSync36 } from "fs";
|
|
10850
11705
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
10851
|
-
import { dirname as dirname10, join as
|
|
11706
|
+
import { dirname as dirname10, join as join44 } from "path";
|
|
10852
11707
|
function getCurrentVersion() {
|
|
10853
11708
|
try {
|
|
10854
11709
|
const __filename5 = fileURLToPath4(import.meta.url);
|
|
10855
11710
|
const __dirname5 = dirname10(__filename5);
|
|
10856
|
-
const pkgPath =
|
|
10857
|
-
const pkg = JSON.parse(
|
|
11711
|
+
const pkgPath = join44(__dirname5, "..", "..", "..", "package.json");
|
|
11712
|
+
const pkg = JSON.parse(readFileSync36(pkgPath, "utf-8"));
|
|
10858
11713
|
return pkg.version;
|
|
10859
11714
|
} catch {
|
|
10860
11715
|
return "unknown";
|
|
@@ -10862,7 +11717,7 @@ function getCurrentVersion() {
|
|
|
10862
11717
|
}
|
|
10863
11718
|
async function getLatestVersion() {
|
|
10864
11719
|
try {
|
|
10865
|
-
const result =
|
|
11720
|
+
const result = execSync9("npm view panopticon-cli version", {
|
|
10866
11721
|
encoding: "utf8",
|
|
10867
11722
|
stdio: ["pipe", "pipe", "pipe"]
|
|
10868
11723
|
});
|
|
@@ -10887,62 +11742,62 @@ function isNewer(latest, current) {
|
|
|
10887
11742
|
return l.patch > c.patch;
|
|
10888
11743
|
}
|
|
10889
11744
|
async function updateCommand(options) {
|
|
10890
|
-
console.log(
|
|
11745
|
+
console.log(chalk43.bold("Panopticon Update\n"));
|
|
10891
11746
|
const currentVersion = getCurrentVersion();
|
|
10892
|
-
console.log(`Current version: ${
|
|
11747
|
+
console.log(`Current version: ${chalk43.cyan(currentVersion)}`);
|
|
10893
11748
|
let latestVersion;
|
|
10894
11749
|
try {
|
|
10895
|
-
console.log(
|
|
11750
|
+
console.log(chalk43.dim("Checking npm for latest version..."));
|
|
10896
11751
|
latestVersion = await getLatestVersion();
|
|
10897
|
-
console.log(`Latest version: ${
|
|
11752
|
+
console.log(`Latest version: ${chalk43.cyan(latestVersion)}`);
|
|
10898
11753
|
} catch (error) {
|
|
10899
|
-
console.error(
|
|
10900
|
-
console.error(
|
|
11754
|
+
console.error(chalk43.red("Failed to check for updates"));
|
|
11755
|
+
console.error(chalk43.dim("Make sure you have internet connectivity"));
|
|
10901
11756
|
process.exit(1);
|
|
10902
11757
|
}
|
|
10903
11758
|
const needsUpdate = isNewer(latestVersion, currentVersion);
|
|
10904
11759
|
if (!needsUpdate) {
|
|
10905
|
-
console.log(
|
|
11760
|
+
console.log(chalk43.green("\n\u2713 You are on the latest version"));
|
|
10906
11761
|
return;
|
|
10907
11762
|
}
|
|
10908
11763
|
console.log(
|
|
10909
|
-
|
|
11764
|
+
chalk43.yellow(`
|
|
10910
11765
|
\u2191 Update available: ${currentVersion} \u2192 ${latestVersion}`)
|
|
10911
11766
|
);
|
|
10912
11767
|
if (options.check) {
|
|
10913
|
-
console.log(
|
|
11768
|
+
console.log(chalk43.dim("\nRun `pan update` to install"));
|
|
10914
11769
|
return;
|
|
10915
11770
|
}
|
|
10916
|
-
console.log(
|
|
11771
|
+
console.log(chalk43.dim("\nUpdating Panopticon..."));
|
|
10917
11772
|
try {
|
|
10918
|
-
|
|
11773
|
+
execSync9("npm install -g panopticon-cli@latest", {
|
|
10919
11774
|
stdio: "inherit"
|
|
10920
11775
|
});
|
|
10921
|
-
console.log(
|
|
11776
|
+
console.log(chalk43.green(`
|
|
10922
11777
|
\u2713 Updated to ${latestVersion}`));
|
|
10923
11778
|
const config2 = loadConfig();
|
|
10924
11779
|
if (config2.sync.auto_sync) {
|
|
10925
|
-
console.log(
|
|
11780
|
+
console.log(chalk43.dim("\nRunning auto-sync..."));
|
|
10926
11781
|
await syncCommand({});
|
|
10927
11782
|
}
|
|
10928
|
-
console.log(
|
|
11783
|
+
console.log(chalk43.dim("\nRestart any running agents to use the new version."));
|
|
10929
11784
|
} catch (error) {
|
|
10930
|
-
console.error(
|
|
11785
|
+
console.error(chalk43.red("\nUpdate failed"));
|
|
10931
11786
|
console.error(
|
|
10932
|
-
|
|
11787
|
+
chalk43.dim("Try running with sudo: sudo npm install -g panopticon-cli@latest")
|
|
10933
11788
|
);
|
|
10934
11789
|
process.exit(1);
|
|
10935
11790
|
}
|
|
10936
11791
|
}
|
|
10937
11792
|
|
|
10938
11793
|
// src/cli/commands/db.ts
|
|
10939
|
-
import
|
|
10940
|
-
import
|
|
10941
|
-
import { existsSync as
|
|
10942
|
-
import { join as
|
|
10943
|
-
import { exec as
|
|
10944
|
-
import { promisify as
|
|
10945
|
-
var
|
|
11794
|
+
import chalk44 from "chalk";
|
|
11795
|
+
import ora17 from "ora";
|
|
11796
|
+
import { existsSync as existsSync44, readFileSync as readFileSync37, writeFileSync as writeFileSync28, mkdirSync as mkdirSync28, statSync as statSync6 } from "fs";
|
|
11797
|
+
import { join as join45, dirname as dirname11 } from "path";
|
|
11798
|
+
import { exec as exec14 } from "child_process";
|
|
11799
|
+
import { promisify as promisify14 } from "util";
|
|
11800
|
+
var execAsync14 = promisify14(exec14);
|
|
10946
11801
|
function loadFullProjects() {
|
|
10947
11802
|
const config2 = loadProjectsConfig();
|
|
10948
11803
|
const projects = config2.projects;
|
|
@@ -10961,12 +11816,12 @@ function registerDbCommands(program2) {
|
|
|
10961
11816
|
const db2 = program2.command("db").description("Database seeding and management");
|
|
10962
11817
|
db2.command("snapshot").description("Create a database snapshot from an external source").option("--project <key>", "Project key (e.g., myn)").option("--output <path>", "Output file path").option("--sanitize", "Run sanitization script after snapshot").action(snapshotCommand);
|
|
10963
11818
|
db2.command("seed <workspaceOrIssue>").description("Seed a workspace database with the configured seed file").option("--force", "Force reseed even if already initialized").option("--file <path>", "Override seed file path").action(seedCommand);
|
|
10964
|
-
db2.command("status").description("Check database status for a workspace").argument("[workspaceOrIssue]", "Workspace folder or issue ID").action(
|
|
11819
|
+
db2.command("status").description("Check database status for a workspace").argument("[workspaceOrIssue]", "Workspace folder or issue ID").action(statusCommand4);
|
|
10965
11820
|
db2.command("clean <file>").description("Clean kubectl/stderr garbage from a pg_dump file").option("--output <path>", "Output file (default: overwrite input)").option("--dry-run", "Show what would be cleaned without modifying").action(cleanCommand);
|
|
10966
11821
|
db2.command("config").description("Show database configuration for a project").argument("[project]", "Project key").action(configCommand);
|
|
10967
11822
|
}
|
|
10968
11823
|
async function snapshotCommand(options) {
|
|
10969
|
-
const spinner =
|
|
11824
|
+
const spinner = ora17("Creating database snapshot...").start();
|
|
10970
11825
|
try {
|
|
10971
11826
|
let projectConfig;
|
|
10972
11827
|
if (options.project) {
|
|
@@ -10986,8 +11841,8 @@ async function snapshotCommand(options) {
|
|
|
10986
11841
|
const dbConfig = projectConfig.workspace?.database;
|
|
10987
11842
|
if (!dbConfig?.snapshot_command && !dbConfig?.external_db) {
|
|
10988
11843
|
spinner.fail(`No snapshot configuration for project ${projectConfig.key}`);
|
|
10989
|
-
console.log(
|
|
10990
|
-
console.log(
|
|
11844
|
+
console.log(chalk44.dim("\nAdd database config to projects.yaml:"));
|
|
11845
|
+
console.log(chalk44.dim(`
|
|
10991
11846
|
${projectConfig.key}:
|
|
10992
11847
|
workspace:
|
|
10993
11848
|
database:
|
|
@@ -11001,10 +11856,10 @@ async function snapshotCommand(options) {
|
|
|
11001
11856
|
`));
|
|
11002
11857
|
return;
|
|
11003
11858
|
}
|
|
11004
|
-
const outputPath = options.output || dbConfig.seed_file ||
|
|
11859
|
+
const outputPath = options.output || dbConfig.seed_file || join45(projectConfig.path, "infra", "seed", "seed.sql");
|
|
11005
11860
|
const outputDir = dirname11(outputPath);
|
|
11006
|
-
if (!
|
|
11007
|
-
|
|
11861
|
+
if (!existsSync44(outputDir)) {
|
|
11862
|
+
mkdirSync28(outputDir, { recursive: true });
|
|
11008
11863
|
}
|
|
11009
11864
|
spinner.text = "Running snapshot command...";
|
|
11010
11865
|
let snapshotCmd;
|
|
@@ -11024,13 +11879,13 @@ async function snapshotCommand(options) {
|
|
|
11024
11879
|
}
|
|
11025
11880
|
const fullCmd = `${snapshotCmd} > "${outputPath}" 2>&1`;
|
|
11026
11881
|
try {
|
|
11027
|
-
await
|
|
11882
|
+
await execAsync14(fullCmd, { timeout: 3e5 });
|
|
11028
11883
|
} catch (error) {
|
|
11029
|
-
if (
|
|
11030
|
-
const content2 =
|
|
11884
|
+
if (existsSync44(outputPath)) {
|
|
11885
|
+
const content2 = readFileSync37(outputPath, "utf-8");
|
|
11031
11886
|
if (content2.includes("PostgreSQL database dump")) {
|
|
11032
11887
|
spinner.warn("Snapshot completed with warnings (stderr captured)");
|
|
11033
|
-
console.log(
|
|
11888
|
+
console.log(chalk44.dim(" Run `pan db clean` to remove stderr noise from the file"));
|
|
11034
11889
|
} else {
|
|
11035
11890
|
spinner.fail(`Snapshot failed: ${error.message}`);
|
|
11036
11891
|
return;
|
|
@@ -11040,7 +11895,7 @@ async function snapshotCommand(options) {
|
|
|
11040
11895
|
return;
|
|
11041
11896
|
}
|
|
11042
11897
|
}
|
|
11043
|
-
const content =
|
|
11898
|
+
const content = readFileSync37(outputPath, "utf-8");
|
|
11044
11899
|
if (content.includes("Defaulted container") || content.includes("Unable to use a TTY")) {
|
|
11045
11900
|
spinner.text = "Cleaning kubectl output from snapshot...";
|
|
11046
11901
|
await cleanFile(outputPath);
|
|
@@ -11048,7 +11903,7 @@ async function snapshotCommand(options) {
|
|
|
11048
11903
|
if (options.sanitize && dbConfig.seed_command) {
|
|
11049
11904
|
spinner.text = "Running sanitization...";
|
|
11050
11905
|
try {
|
|
11051
|
-
await
|
|
11906
|
+
await execAsync14(dbConfig.seed_command, { cwd: projectConfig.path });
|
|
11052
11907
|
} catch (error) {
|
|
11053
11908
|
spinner.warn(`Sanitization warning: ${error.message}`);
|
|
11054
11909
|
}
|
|
@@ -11056,13 +11911,13 @@ async function snapshotCommand(options) {
|
|
|
11056
11911
|
spinner.succeed(`Snapshot saved to ${outputPath}`);
|
|
11057
11912
|
const stats = statSync6(outputPath);
|
|
11058
11913
|
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
11059
|
-
console.log(
|
|
11914
|
+
console.log(chalk44.dim(` Size: ${sizeMB} MB`));
|
|
11060
11915
|
} catch (error) {
|
|
11061
11916
|
spinner.fail(`Snapshot failed: ${error.message}`);
|
|
11062
11917
|
}
|
|
11063
11918
|
}
|
|
11064
11919
|
async function seedCommand(workspaceOrIssue, options) {
|
|
11065
|
-
const spinner =
|
|
11920
|
+
const spinner = ora17("Seeding database...").start();
|
|
11066
11921
|
try {
|
|
11067
11922
|
const normalizedId = workspaceOrIssue.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
11068
11923
|
const folderName = normalizedId.startsWith("feature-") ? normalizedId : `feature-${normalizedId}`;
|
|
@@ -11072,27 +11927,27 @@ async function seedCommand(workspaceOrIssue, options) {
|
|
|
11072
11927
|
spinner.fail("Could not find project workspace configuration");
|
|
11073
11928
|
return;
|
|
11074
11929
|
}
|
|
11075
|
-
const workspacePath =
|
|
11076
|
-
if (!
|
|
11930
|
+
const workspacePath = join45(projectConfig.path, projectConfig.workspace.workspaces_dir || "workspaces", folderName);
|
|
11931
|
+
if (!existsSync44(workspacePath)) {
|
|
11077
11932
|
spinner.fail(`Workspace not found: ${workspacePath}`);
|
|
11078
11933
|
return;
|
|
11079
11934
|
}
|
|
11080
11935
|
const dbConfig = projectConfig.workspace.database;
|
|
11081
11936
|
const seedFile = options.file || dbConfig?.seed_file;
|
|
11082
|
-
if (!seedFile || !
|
|
11937
|
+
if (!seedFile || !existsSync44(seedFile)) {
|
|
11083
11938
|
spinner.fail(`Seed file not found: ${seedFile || "(not configured)"}`);
|
|
11084
|
-
console.log(
|
|
11939
|
+
console.log(chalk44.dim("\nConfigure seed_file in projects.yaml or use --file"));
|
|
11085
11940
|
return;
|
|
11086
11941
|
}
|
|
11087
11942
|
const projectName = `${projectConfig.name?.toLowerCase().replace(/\s+/g, "-")}-${folderName}`;
|
|
11088
11943
|
const containerName = dbConfig?.container_name?.replace("{{PROJECT}}", projectName) || `${projectName}-postgres-1`;
|
|
11089
11944
|
spinner.text = `Finding database container ${containerName}...`;
|
|
11090
11945
|
try {
|
|
11091
|
-
const { stdout } = await
|
|
11946
|
+
const { stdout } = await execAsync14(`docker ps --filter "name=${containerName}" --format "{{.Names}}"`);
|
|
11092
11947
|
if (!stdout.trim()) {
|
|
11093
11948
|
spinner.fail(`Database container not running: ${containerName}`);
|
|
11094
|
-
console.log(
|
|
11095
|
-
console.log(
|
|
11949
|
+
console.log(chalk44.dim("\nStart the workspace containers first:"));
|
|
11950
|
+
console.log(chalk44.dim(` pan workspace create ${workspaceOrIssue} --docker`));
|
|
11096
11951
|
return;
|
|
11097
11952
|
}
|
|
11098
11953
|
} catch {
|
|
@@ -11101,7 +11956,7 @@ async function seedCommand(workspaceOrIssue, options) {
|
|
|
11101
11956
|
}
|
|
11102
11957
|
if (!options.force) {
|
|
11103
11958
|
try {
|
|
11104
|
-
const { stdout } = await
|
|
11959
|
+
const { stdout } = await execAsync14(
|
|
11105
11960
|
`docker exec ${containerName} psql -U postgres -d myn -c "SELECT count(*) FROM flyway_schema_history" -t 2>/dev/null`
|
|
11106
11961
|
);
|
|
11107
11962
|
const count = parseInt(stdout.trim(), 10);
|
|
@@ -11115,7 +11970,7 @@ async function seedCommand(workspaceOrIssue, options) {
|
|
|
11115
11970
|
if (options.force) {
|
|
11116
11971
|
spinner.text = "Dropping existing database...";
|
|
11117
11972
|
try {
|
|
11118
|
-
await
|
|
11973
|
+
await execAsync14(
|
|
11119
11974
|
`docker exec ${containerName} psql -U postgres -c "DROP DATABASE IF EXISTS myn; CREATE DATABASE myn;"`
|
|
11120
11975
|
);
|
|
11121
11976
|
} catch (error) {
|
|
@@ -11123,10 +11978,10 @@ async function seedCommand(workspaceOrIssue, options) {
|
|
|
11123
11978
|
}
|
|
11124
11979
|
}
|
|
11125
11980
|
spinner.text = "Copying seed file to container...";
|
|
11126
|
-
await
|
|
11981
|
+
await execAsync14(`docker cp "${seedFile}" ${containerName}:/tmp/seed.sql`);
|
|
11127
11982
|
spinner.text = "Executing seed...";
|
|
11128
11983
|
try {
|
|
11129
|
-
await
|
|
11984
|
+
await execAsync14(`docker exec ${containerName} psql -U postgres -d myn -f /tmp/seed.sql`, {
|
|
11130
11985
|
timeout: 6e5
|
|
11131
11986
|
// 10 minute timeout for large seeds
|
|
11132
11987
|
});
|
|
@@ -11136,22 +11991,22 @@ async function seedCommand(workspaceOrIssue, options) {
|
|
|
11136
11991
|
return;
|
|
11137
11992
|
}
|
|
11138
11993
|
}
|
|
11139
|
-
await
|
|
11994
|
+
await execAsync14(`docker exec ${containerName} rm /tmp/seed.sql`);
|
|
11140
11995
|
spinner.succeed("Database seeded successfully");
|
|
11141
11996
|
try {
|
|
11142
|
-
const { stdout } = await
|
|
11997
|
+
const { stdout } = await execAsync14(
|
|
11143
11998
|
`docker exec ${containerName} psql -U postgres -d myn -c "SELECT version, description FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 3" -t`
|
|
11144
11999
|
);
|
|
11145
|
-
console.log(
|
|
11146
|
-
stdout.trim().split("\n").forEach((line) => console.log(
|
|
12000
|
+
console.log(chalk44.dim("\nRecent migrations:"));
|
|
12001
|
+
stdout.trim().split("\n").forEach((line) => console.log(chalk44.dim(` ${line.trim()}`)));
|
|
11147
12002
|
} catch {
|
|
11148
12003
|
}
|
|
11149
12004
|
} catch (error) {
|
|
11150
12005
|
spinner.fail(`Seed failed: ${error.message}`);
|
|
11151
12006
|
}
|
|
11152
12007
|
}
|
|
11153
|
-
async function
|
|
11154
|
-
const spinner =
|
|
12008
|
+
async function statusCommand4(workspaceOrIssue) {
|
|
12009
|
+
const spinner = ora17("Checking database status...").start();
|
|
11155
12010
|
try {
|
|
11156
12011
|
let containerName;
|
|
11157
12012
|
let projectConfig;
|
|
@@ -11169,7 +12024,7 @@ async function statusCommand3(workspaceOrIssue) {
|
|
|
11169
12024
|
const projects = loadFullProjects();
|
|
11170
12025
|
projectConfig = projects.find((p) => cwd.startsWith(p.path));
|
|
11171
12026
|
if (projectConfig) {
|
|
11172
|
-
const { stdout } = await
|
|
12027
|
+
const { stdout } = await execAsync14(
|
|
11173
12028
|
`docker ps --filter "name=${projectConfig.name?.toLowerCase().replace(/\s+/g, "-")}" --filter "name=postgres" --format "{{.Names}}" | head -1`
|
|
11174
12029
|
);
|
|
11175
12030
|
containerName = stdout.trim();
|
|
@@ -11177,12 +12032,12 @@ async function statusCommand3(workspaceOrIssue) {
|
|
|
11177
12032
|
}
|
|
11178
12033
|
if (!containerName) {
|
|
11179
12034
|
spinner.fail("Could not determine database container");
|
|
11180
|
-
console.log(
|
|
11181
|
-
console.log(
|
|
12035
|
+
console.log(chalk44.dim("\nUsage: pan db status <issue-id>"));
|
|
12036
|
+
console.log(chalk44.dim(" pan db status MIN-123"));
|
|
11182
12037
|
return;
|
|
11183
12038
|
}
|
|
11184
12039
|
spinner.text = `Checking container ${containerName}...`;
|
|
11185
|
-
const { stdout: containerStatus } = await
|
|
12040
|
+
const { stdout: containerStatus } = await execAsync14(
|
|
11186
12041
|
`docker ps --filter "name=${containerName}" --format "{{.Status}}"`
|
|
11187
12042
|
);
|
|
11188
12043
|
if (!containerStatus.trim()) {
|
|
@@ -11190,28 +12045,28 @@ async function statusCommand3(workspaceOrIssue) {
|
|
|
11190
12045
|
return;
|
|
11191
12046
|
}
|
|
11192
12047
|
spinner.succeed(`Container: ${containerName}`);
|
|
11193
|
-
console.log(
|
|
12048
|
+
console.log(chalk44.dim(` Status: ${containerStatus.trim()}`));
|
|
11194
12049
|
try {
|
|
11195
|
-
const { stdout: version } = await
|
|
12050
|
+
const { stdout: version } = await execAsync14(
|
|
11196
12051
|
`docker exec ${containerName} psql -U postgres -d myn -c "SELECT version, description FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 1" -t`
|
|
11197
12052
|
);
|
|
11198
12053
|
const [ver, desc] = version.trim().split("|").map((s) => s.trim());
|
|
11199
|
-
console.log(
|
|
12054
|
+
console.log(chalk44.green(` Flyway: V${ver} - ${desc}`));
|
|
11200
12055
|
} catch {
|
|
11201
|
-
console.log(
|
|
12056
|
+
console.log(chalk44.yellow(" Flyway: Not initialized"));
|
|
11202
12057
|
}
|
|
11203
12058
|
try {
|
|
11204
|
-
const { stdout: tableCount } = await
|
|
12059
|
+
const { stdout: tableCount } = await execAsync14(
|
|
11205
12060
|
`docker exec ${containerName} psql -U postgres -d myn -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'" -t`
|
|
11206
12061
|
);
|
|
11207
|
-
console.log(
|
|
12062
|
+
console.log(chalk44.dim(` Tables: ${tableCount.trim()}`));
|
|
11208
12063
|
} catch {
|
|
11209
12064
|
}
|
|
11210
12065
|
try {
|
|
11211
|
-
const { stdout: dbSize } = await
|
|
12066
|
+
const { stdout: dbSize } = await execAsync14(
|
|
11212
12067
|
`docker exec ${containerName} psql -U postgres -d myn -c "SELECT pg_size_pretty(pg_database_size('myn'))" -t`
|
|
11213
12068
|
);
|
|
11214
|
-
console.log(
|
|
12069
|
+
console.log(chalk44.dim(` Size: ${dbSize.trim()}`));
|
|
11215
12070
|
} catch {
|
|
11216
12071
|
}
|
|
11217
12072
|
} catch (error) {
|
|
@@ -11219,13 +12074,13 @@ async function statusCommand3(workspaceOrIssue) {
|
|
|
11219
12074
|
}
|
|
11220
12075
|
}
|
|
11221
12076
|
async function cleanCommand(file, options) {
|
|
11222
|
-
const spinner =
|
|
12077
|
+
const spinner = ora17("Cleaning database dump file...").start();
|
|
11223
12078
|
try {
|
|
11224
|
-
if (!
|
|
12079
|
+
if (!existsSync44(file)) {
|
|
11225
12080
|
spinner.fail(`File not found: ${file}`);
|
|
11226
12081
|
return;
|
|
11227
12082
|
}
|
|
11228
|
-
const content =
|
|
12083
|
+
const content = readFileSync37(file, "utf-8");
|
|
11229
12084
|
const lines = content.split("\n");
|
|
11230
12085
|
const patternsToRemove = [
|
|
11231
12086
|
/^Defaulted container/,
|
|
@@ -11278,24 +12133,24 @@ async function cleanCommand(file, options) {
|
|
|
11278
12133
|
if (options.dryRun) {
|
|
11279
12134
|
spinner.info(`Would remove ${removedLines} lines`);
|
|
11280
12135
|
if (removedLines > 0) {
|
|
11281
|
-
console.log(
|
|
12136
|
+
console.log(chalk44.dim("\nLines to remove (sample):"));
|
|
11282
12137
|
const removed = lines.filter((line) => patternsToRemove.some((p) => p.test(line))).slice(0, 5);
|
|
11283
|
-
removed.forEach((line) => console.log(
|
|
12138
|
+
removed.forEach((line) => console.log(chalk44.red(` - ${line.slice(0, 80)}...`)));
|
|
11284
12139
|
}
|
|
11285
12140
|
return;
|
|
11286
12141
|
}
|
|
11287
12142
|
const outputPath = options.output || file;
|
|
11288
|
-
|
|
12143
|
+
writeFileSync28(outputPath, cleanedContent);
|
|
11289
12144
|
spinner.succeed(`Cleaned ${removedLines} lines`);
|
|
11290
|
-
console.log(
|
|
11291
|
-
console.log(
|
|
11292
|
-
console.log(
|
|
12145
|
+
console.log(chalk44.dim(` Output: ${outputPath}`));
|
|
12146
|
+
console.log(chalk44.dim(` Original: ${lines.length} lines`));
|
|
12147
|
+
console.log(chalk44.dim(` Cleaned: ${cleanedLines.length} lines`));
|
|
11293
12148
|
} catch (error) {
|
|
11294
12149
|
spinner.fail(`Clean failed: ${error.message}`);
|
|
11295
12150
|
}
|
|
11296
12151
|
}
|
|
11297
12152
|
async function cleanFile(filePath) {
|
|
11298
|
-
const content =
|
|
12153
|
+
const content = readFileSync37(filePath, "utf-8");
|
|
11299
12154
|
const lines = content.split("\n");
|
|
11300
12155
|
let startIndex = 0;
|
|
11301
12156
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -11315,7 +12170,7 @@ async function cleanFile(filePath) {
|
|
|
11315
12170
|
/^error: timed out waiting/
|
|
11316
12171
|
];
|
|
11317
12172
|
const cleanedLines = lines.slice(startIndex).filter((line) => !patternsToRemove.some((p) => p.test(line)));
|
|
11318
|
-
|
|
12173
|
+
writeFileSync28(filePath, cleanedLines.join("\n"));
|
|
11319
12174
|
}
|
|
11320
12175
|
async function configCommand(project2) {
|
|
11321
12176
|
const projects = loadFullProjects();
|
|
@@ -11329,18 +12184,18 @@ async function configCommand(project2) {
|
|
|
11329
12184
|
projectConfig = projects.find((p) => cwd.startsWith(p.path));
|
|
11330
12185
|
}
|
|
11331
12186
|
if (!projectConfig) {
|
|
11332
|
-
console.log(
|
|
11333
|
-
console.log(
|
|
11334
|
-
projects.forEach((p) => console.log(
|
|
12187
|
+
console.log(chalk44.red("Project not found"));
|
|
12188
|
+
console.log(chalk44.dim("\nAvailable projects:"));
|
|
12189
|
+
projects.forEach((p) => console.log(chalk44.dim(` ${p.key} (${p.linear_team || "no team"})`)));
|
|
11335
12190
|
return;
|
|
11336
12191
|
}
|
|
11337
|
-
console.log(
|
|
12192
|
+
console.log(chalk44.bold(`Database Configuration: ${projectConfig.key}`));
|
|
11338
12193
|
console.log("");
|
|
11339
12194
|
const dbConfig = projectConfig.workspace?.database;
|
|
11340
12195
|
if (!dbConfig) {
|
|
11341
|
-
console.log(
|
|
11342
|
-
console.log(
|
|
11343
|
-
console.log(
|
|
12196
|
+
console.log(chalk44.yellow("No database configuration found"));
|
|
12197
|
+
console.log(chalk44.dim("\nAdd to projects.yaml under workspace:"));
|
|
12198
|
+
console.log(chalk44.dim(`
|
|
11344
12199
|
database:
|
|
11345
12200
|
seed_file: /path/to/seed.sql
|
|
11346
12201
|
snapshot_command: "kubectl exec -n prod pod/postgres -- pg_dump -U app db"
|
|
@@ -11349,12 +12204,12 @@ async function configCommand(project2) {
|
|
|
11349
12204
|
return;
|
|
11350
12205
|
}
|
|
11351
12206
|
if (dbConfig.seed_file) {
|
|
11352
|
-
const exists =
|
|
12207
|
+
const exists = existsSync44(dbConfig.seed_file);
|
|
11353
12208
|
console.log(` Seed file: ${dbConfig.seed_file}`);
|
|
11354
|
-
console.log(
|
|
12209
|
+
console.log(chalk44.dim(` Status: ${exists ? chalk44.green("exists") : chalk44.red("not found")}`));
|
|
11355
12210
|
if (exists) {
|
|
11356
12211
|
const stats = statSync6(dbConfig.seed_file);
|
|
11357
|
-
console.log(
|
|
12212
|
+
console.log(chalk44.dim(` Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`));
|
|
11358
12213
|
}
|
|
11359
12214
|
}
|
|
11360
12215
|
if (dbConfig.snapshot_command) {
|
|
@@ -11369,25 +12224,25 @@ async function configCommand(project2) {
|
|
|
11369
12224
|
if (dbConfig.migrations) {
|
|
11370
12225
|
console.log(` Migrations: ${dbConfig.migrations.type}`);
|
|
11371
12226
|
if (dbConfig.migrations.path) {
|
|
11372
|
-
console.log(
|
|
12227
|
+
console.log(chalk44.dim(` Path: ${dbConfig.migrations.path}`));
|
|
11373
12228
|
}
|
|
11374
12229
|
}
|
|
11375
12230
|
}
|
|
11376
12231
|
|
|
11377
12232
|
// src/cli/commands/beads.ts
|
|
11378
|
-
import
|
|
11379
|
-
import
|
|
11380
|
-
import { existsSync as
|
|
11381
|
-
import { join as
|
|
11382
|
-
import { exec as
|
|
11383
|
-
import { promisify as
|
|
12233
|
+
import chalk45 from "chalk";
|
|
12234
|
+
import ora18 from "ora";
|
|
12235
|
+
import { existsSync as existsSync45, readFileSync as readFileSync38 } from "fs";
|
|
12236
|
+
import { join as join46 } from "path";
|
|
12237
|
+
import { exec as exec15, execSync as execSync10 } from "child_process";
|
|
12238
|
+
import { promisify as promisify15 } from "util";
|
|
11384
12239
|
import { platform as platform2 } from "os";
|
|
11385
|
-
var
|
|
12240
|
+
var execAsync15 = promisify15(exec15);
|
|
11386
12241
|
function detectPlatform2() {
|
|
11387
12242
|
const os = platform2();
|
|
11388
12243
|
if (os === "linux") {
|
|
11389
12244
|
try {
|
|
11390
|
-
const release =
|
|
12245
|
+
const release = readFileSync38("/proc/version", "utf8").toLowerCase();
|
|
11391
12246
|
if (release.includes("microsoft") || release.includes("wsl")) {
|
|
11392
12247
|
return "wsl";
|
|
11393
12248
|
}
|
|
@@ -11399,7 +12254,7 @@ function detectPlatform2() {
|
|
|
11399
12254
|
}
|
|
11400
12255
|
async function isBdAvailable() {
|
|
11401
12256
|
try {
|
|
11402
|
-
await
|
|
12257
|
+
await execAsync15("which bd", { encoding: "utf-8" });
|
|
11403
12258
|
return true;
|
|
11404
12259
|
} catch {
|
|
11405
12260
|
return false;
|
|
@@ -11408,7 +12263,7 @@ async function isBdAvailable() {
|
|
|
11408
12263
|
async function getOldClosedCount(cwd, days) {
|
|
11409
12264
|
try {
|
|
11410
12265
|
const seconds = days * 24 * 60 * 60;
|
|
11411
|
-
const { stdout } = await
|
|
12266
|
+
const { stdout } = await execAsync15(
|
|
11412
12267
|
`bd list --status closed --json 2>/dev/null | jq '[.[] | select(.closed_at != null) | select((now - (.closed_at | fromdateiso8601)) > ${seconds})] | length' 2>/dev/null || echo "0"`,
|
|
11413
12268
|
{ cwd, encoding: "utf-8" }
|
|
11414
12269
|
);
|
|
@@ -11421,17 +12276,17 @@ async function compactCommand(options) {
|
|
|
11421
12276
|
const days = options.days || 30;
|
|
11422
12277
|
const cwd = process.cwd();
|
|
11423
12278
|
if (!await isBdAvailable()) {
|
|
11424
|
-
console.error(
|
|
11425
|
-
console.log(
|
|
12279
|
+
console.error(chalk45.red("Error: bd (beads) CLI not found in PATH"));
|
|
12280
|
+
console.log(chalk45.dim("Install beads: https://github.com/steveyegge/beads"));
|
|
11426
12281
|
process.exit(1);
|
|
11427
12282
|
}
|
|
11428
|
-
const beadsDir =
|
|
11429
|
-
if (!
|
|
11430
|
-
console.error(
|
|
11431
|
-
console.log(
|
|
12283
|
+
const beadsDir = join46(cwd, ".beads");
|
|
12284
|
+
if (!existsSync45(beadsDir)) {
|
|
12285
|
+
console.error(chalk45.red("Error: No .beads directory found in current directory"));
|
|
12286
|
+
console.log(chalk45.dim("Run bd init to initialize beads"));
|
|
11432
12287
|
process.exit(1);
|
|
11433
12288
|
}
|
|
11434
|
-
const spinner =
|
|
12289
|
+
const spinner = ora18("Checking for old closed beads...").start();
|
|
11435
12290
|
try {
|
|
11436
12291
|
const count = await getOldClosedCount(cwd, days);
|
|
11437
12292
|
if (count === 0) {
|
|
@@ -11442,28 +12297,28 @@ async function compactCommand(options) {
|
|
|
11442
12297
|
if (options.dryRun) {
|
|
11443
12298
|
spinner.info(`Dry run: Would compact ${count} beads (use without --dry-run to execute)`);
|
|
11444
12299
|
console.log("");
|
|
11445
|
-
console.log(
|
|
12300
|
+
console.log(chalk45.bold("Beads that would be compacted:"));
|
|
11446
12301
|
try {
|
|
11447
|
-
const { stdout: beadsList } = await
|
|
12302
|
+
const { stdout: beadsList } = await execAsync15(
|
|
11448
12303
|
`bd list --status closed --json 2>/dev/null | jq -r '.[] | select(.closed_at != null) | select((now - (.closed_at | fromdateiso8601)) > ${days * 24 * 60 * 60}) | " - \\(.id): \\(.title)"' 2>/dev/null`,
|
|
11449
12304
|
{ cwd, encoding: "utf-8" }
|
|
11450
12305
|
);
|
|
11451
12306
|
console.log(beadsList || " (none)");
|
|
11452
12307
|
} catch {
|
|
11453
|
-
console.log(
|
|
12308
|
+
console.log(chalk45.dim(" (could not list beads)"));
|
|
11454
12309
|
}
|
|
11455
12310
|
return;
|
|
11456
12311
|
}
|
|
11457
12312
|
spinner.text = "Running compaction...";
|
|
11458
|
-
await
|
|
12313
|
+
await execAsync15(`bd admin compact --days ${days}`, { cwd, encoding: "utf-8" });
|
|
11459
12314
|
spinner.succeed(`Compacted ${count} beads older than ${days} days`);
|
|
11460
12315
|
try {
|
|
11461
|
-
await
|
|
11462
|
-
console.log(
|
|
12316
|
+
await execAsync15(`git diff --quiet .beads/`, { cwd, encoding: "utf-8" });
|
|
12317
|
+
console.log(chalk45.dim("No changes to commit (beads already up to date)"));
|
|
11463
12318
|
} catch {
|
|
11464
12319
|
console.log("");
|
|
11465
|
-
console.log(
|
|
11466
|
-
console.log(
|
|
12320
|
+
console.log(chalk45.bold("Changes detected in .beads/"));
|
|
12321
|
+
console.log(chalk45.dim("To commit the compacted beads:"));
|
|
11467
12322
|
console.log("");
|
|
11468
12323
|
console.log(" git add .beads/");
|
|
11469
12324
|
console.log(' git commit -m "chore: compact beads (remove closed issues > ' + days + ' days)"');
|
|
@@ -11475,34 +12330,34 @@ async function compactCommand(options) {
|
|
|
11475
12330
|
}
|
|
11476
12331
|
} catch (error) {
|
|
11477
12332
|
spinner.fail("Compaction failed");
|
|
11478
|
-
console.error(
|
|
12333
|
+
console.error(chalk45.red(error.message));
|
|
11479
12334
|
process.exit(1);
|
|
11480
12335
|
}
|
|
11481
12336
|
}
|
|
11482
12337
|
async function statsCommand() {
|
|
11483
12338
|
const cwd = process.cwd();
|
|
11484
12339
|
if (!await isBdAvailable()) {
|
|
11485
|
-
console.error(
|
|
12340
|
+
console.error(chalk45.red("Error: bd (beads) CLI not found"));
|
|
11486
12341
|
process.exit(1);
|
|
11487
12342
|
}
|
|
11488
|
-
const beadsDir =
|
|
11489
|
-
if (!
|
|
11490
|
-
console.error(
|
|
12343
|
+
const beadsDir = join46(cwd, ".beads");
|
|
12344
|
+
if (!existsSync45(beadsDir)) {
|
|
12345
|
+
console.error(chalk45.red("Error: No .beads directory found"));
|
|
11491
12346
|
process.exit(1);
|
|
11492
12347
|
}
|
|
11493
|
-
const spinner =
|
|
12348
|
+
const spinner = ora18("Gathering beads statistics...").start();
|
|
11494
12349
|
try {
|
|
11495
|
-
const { stdout: totalRaw } = await
|
|
12350
|
+
const { stdout: totalRaw } = await execAsync15(`bd list --limit 0 --json 2>/dev/null | jq 'length'`, {
|
|
11496
12351
|
cwd,
|
|
11497
12352
|
encoding: "utf-8"
|
|
11498
12353
|
});
|
|
11499
12354
|
const total = parseInt(totalRaw.trim(), 10) || 0;
|
|
11500
|
-
const { stdout: openRaw } = await
|
|
12355
|
+
const { stdout: openRaw } = await execAsync15(`bd list --status open --limit 0 --json 2>/dev/null | jq 'length'`, {
|
|
11501
12356
|
cwd,
|
|
11502
12357
|
encoding: "utf-8"
|
|
11503
12358
|
});
|
|
11504
12359
|
const open = parseInt(openRaw.trim(), 10) || 0;
|
|
11505
|
-
const { stdout: closedRaw } = await
|
|
12360
|
+
const { stdout: closedRaw } = await execAsync15(`bd list --status closed --limit 0 --json 2>/dev/null | jq 'length'`, {
|
|
11506
12361
|
cwd,
|
|
11507
12362
|
encoding: "utf-8"
|
|
11508
12363
|
});
|
|
@@ -11510,20 +12365,20 @@ async function statsCommand() {
|
|
|
11510
12365
|
const oldClosed = await getOldClosedCount(cwd, 30);
|
|
11511
12366
|
spinner.stop();
|
|
11512
12367
|
console.log("");
|
|
11513
|
-
console.log(
|
|
12368
|
+
console.log(chalk45.bold("Beads Statistics"));
|
|
11514
12369
|
console.log("");
|
|
11515
|
-
console.log(` Total: ${
|
|
11516
|
-
console.log(` Open: ${
|
|
11517
|
-
console.log(` Closed: ${
|
|
11518
|
-
console.log(` Old (>30d): ${oldClosed > 0 ?
|
|
12370
|
+
console.log(` Total: ${chalk45.cyan(total)}`);
|
|
12371
|
+
console.log(` Open: ${chalk45.green(open)}`);
|
|
12372
|
+
console.log(` Closed: ${chalk45.dim(closed)}`);
|
|
12373
|
+
console.log(` Old (>30d): ${oldClosed > 0 ? chalk45.yellow(oldClosed) : chalk45.dim(oldClosed)}`);
|
|
11519
12374
|
console.log("");
|
|
11520
12375
|
if (oldClosed > 0) {
|
|
11521
|
-
console.log(
|
|
12376
|
+
console.log(chalk45.dim(`Tip: Run 'pan beads compact' to remove old closed beads`));
|
|
11522
12377
|
console.log("");
|
|
11523
12378
|
}
|
|
11524
12379
|
} catch (error) {
|
|
11525
12380
|
spinner.fail("Failed to get statistics");
|
|
11526
|
-
console.error(
|
|
12381
|
+
console.error(chalk45.red(error.message));
|
|
11527
12382
|
process.exit(1);
|
|
11528
12383
|
}
|
|
11529
12384
|
}
|
|
@@ -11544,10 +12399,10 @@ function registerBeadsCommands(program2) {
|
|
|
11544
12399
|
});
|
|
11545
12400
|
}
|
|
11546
12401
|
async function upgradeCommand(checkOnly = false) {
|
|
11547
|
-
console.log(
|
|
12402
|
+
console.log(chalk45.dim("Checking beads version..."));
|
|
11548
12403
|
let currentVersion = "not installed";
|
|
11549
12404
|
try {
|
|
11550
|
-
const { stdout } = await
|
|
12405
|
+
const { stdout } = await execAsync15("bd --version", { encoding: "utf-8" });
|
|
11551
12406
|
const match = stdout.match(/(\d+\.\d+\.\d+)/);
|
|
11552
12407
|
if (match) {
|
|
11553
12408
|
currentVersion = match[1];
|
|
@@ -11556,7 +12411,7 @@ async function upgradeCommand(checkOnly = false) {
|
|
|
11556
12411
|
}
|
|
11557
12412
|
let latestVersion = "unknown";
|
|
11558
12413
|
try {
|
|
11559
|
-
const { stdout } = await
|
|
12414
|
+
const { stdout } = await execAsync15(
|
|
11560
12415
|
"curl -sL https://api.github.com/repos/steveyegge/beads/releases/latest | jq -r .tag_name",
|
|
11561
12416
|
{ encoding: "utf-8" }
|
|
11562
12417
|
);
|
|
@@ -11564,66 +12419,295 @@ async function upgradeCommand(checkOnly = false) {
|
|
|
11564
12419
|
} catch {
|
|
11565
12420
|
}
|
|
11566
12421
|
console.log("");
|
|
11567
|
-
console.log(
|
|
12422
|
+
console.log(chalk45.bold("Beads CLI Version"));
|
|
11568
12423
|
console.log("");
|
|
11569
|
-
console.log(` Current: ${currentVersion === "not installed" ?
|
|
11570
|
-
console.log(` Latest: ${
|
|
12424
|
+
console.log(` Current: ${currentVersion === "not installed" ? chalk45.red(currentVersion) : chalk45.cyan(currentVersion)}`);
|
|
12425
|
+
console.log(` Latest: ${chalk45.green(latestVersion)}`);
|
|
11571
12426
|
console.log("");
|
|
11572
12427
|
if (currentVersion === latestVersion) {
|
|
11573
|
-
console.log(
|
|
12428
|
+
console.log(chalk45.green("\u2713 Already on latest version"));
|
|
11574
12429
|
return;
|
|
11575
12430
|
}
|
|
11576
12431
|
if (checkOnly) {
|
|
11577
12432
|
if (currentVersion !== latestVersion && currentVersion !== "not installed") {
|
|
11578
|
-
console.log(
|
|
11579
|
-
console.log(
|
|
12433
|
+
console.log(chalk45.yellow(`Update available: ${currentVersion} \u2192 ${latestVersion}`));
|
|
12434
|
+
console.log(chalk45.dim(`Run 'pan beads upgrade' to install`));
|
|
11580
12435
|
}
|
|
11581
12436
|
return;
|
|
11582
12437
|
}
|
|
11583
|
-
const spinner =
|
|
12438
|
+
const spinner = ora18("Upgrading beads...").start();
|
|
11584
12439
|
const plat = detectPlatform2();
|
|
11585
12440
|
try {
|
|
11586
12441
|
if (plat === "darwin") {
|
|
11587
12442
|
try {
|
|
11588
|
-
|
|
12443
|
+
execSync10("brew upgrade steveyegge/beads/bd 2>/dev/null || brew install steveyegge/beads/bd", {
|
|
11589
12444
|
stdio: "pipe",
|
|
11590
12445
|
timeout: 12e4
|
|
11591
12446
|
});
|
|
11592
12447
|
spinner.succeed("beads upgraded via Homebrew");
|
|
11593
12448
|
} catch {
|
|
11594
|
-
|
|
12449
|
+
execSync10("curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash", {
|
|
11595
12450
|
stdio: "pipe",
|
|
11596
12451
|
timeout: 12e4
|
|
11597
12452
|
});
|
|
11598
12453
|
spinner.succeed("beads upgraded via install script");
|
|
11599
12454
|
}
|
|
11600
12455
|
} else {
|
|
11601
|
-
|
|
12456
|
+
execSync10("curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash", {
|
|
11602
12457
|
stdio: "pipe",
|
|
11603
12458
|
timeout: 12e4
|
|
11604
12459
|
});
|
|
11605
12460
|
spinner.succeed("beads upgraded via install script");
|
|
11606
12461
|
}
|
|
11607
12462
|
try {
|
|
11608
|
-
const { stdout } = await
|
|
12463
|
+
const { stdout } = await execAsync15("bd --version", { encoding: "utf-8" });
|
|
11609
12464
|
const match = stdout.match(/(\d+\.\d+\.\d+)/);
|
|
11610
12465
|
if (match) {
|
|
11611
|
-
console.log(
|
|
12466
|
+
console.log(chalk45.green(`
|
|
11612
12467
|
\u2713 Now running beads v${match[1]}`));
|
|
11613
12468
|
}
|
|
11614
12469
|
} catch {
|
|
11615
12470
|
}
|
|
11616
12471
|
} catch (error) {
|
|
11617
12472
|
spinner.fail("Upgrade failed");
|
|
11618
|
-
console.error(
|
|
12473
|
+
console.error(chalk45.red(error.message));
|
|
12474
|
+
console.log("");
|
|
12475
|
+
console.log(chalk45.dim("Manual upgrade:"));
|
|
12476
|
+
console.log(chalk45.dim(" curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash"));
|
|
12477
|
+
process.exit(1);
|
|
12478
|
+
}
|
|
12479
|
+
}
|
|
12480
|
+
|
|
12481
|
+
// src/cli/commands/migrate-config.ts
|
|
12482
|
+
import chalk46 from "chalk";
|
|
12483
|
+
import ora19 from "ora";
|
|
12484
|
+
import inquirer5 from "inquirer";
|
|
12485
|
+
|
|
12486
|
+
// src/lib/config-migration.ts
|
|
12487
|
+
import { readFileSync as readFileSync39, writeFileSync as writeFileSync29, existsSync as existsSync46, renameSync } from "fs";
|
|
12488
|
+
import { join as join47 } from "path";
|
|
12489
|
+
import { homedir as homedir19 } from "os";
|
|
12490
|
+
import yaml from "js-yaml";
|
|
12491
|
+
var LEGACY_SETTINGS_PATH = join47(homedir19(), ".panopticon", "settings.json");
|
|
12492
|
+
var NEW_CONFIG_PATH = join47(homedir19(), ".panopticon", "config.yaml");
|
|
12493
|
+
var BACKUP_SETTINGS_PATH = join47(homedir19(), ".panopticon", "settings.json.backup");
|
|
12494
|
+
function needsMigration() {
|
|
12495
|
+
return existsSync46(LEGACY_SETTINGS_PATH) && !existsSync46(NEW_CONFIG_PATH);
|
|
12496
|
+
}
|
|
12497
|
+
function hasLegacySettings() {
|
|
12498
|
+
return existsSync46(LEGACY_SETTINGS_PATH);
|
|
12499
|
+
}
|
|
12500
|
+
function detectEnabledProviders(settings) {
|
|
12501
|
+
return {
|
|
12502
|
+
anthropic: true,
|
|
12503
|
+
// Always enabled
|
|
12504
|
+
openai: !!settings.api_keys.openai,
|
|
12505
|
+
google: !!settings.api_keys.google,
|
|
12506
|
+
zai: !!settings.api_keys.zai,
|
|
12507
|
+
kimi: false
|
|
12508
|
+
// Legacy settings don't have Kimi
|
|
12509
|
+
};
|
|
12510
|
+
}
|
|
12511
|
+
function convertToYamlConfig(settings) {
|
|
12512
|
+
const providers = detectEnabledProviders(settings);
|
|
12513
|
+
const config2 = {
|
|
12514
|
+
models: {
|
|
12515
|
+
providers,
|
|
12516
|
+
overrides: {},
|
|
12517
|
+
// No overrides from legacy
|
|
12518
|
+
gemini_thinking_level: 3
|
|
12519
|
+
},
|
|
12520
|
+
api_keys: settings.api_keys
|
|
12521
|
+
};
|
|
12522
|
+
return config2;
|
|
12523
|
+
}
|
|
12524
|
+
function migrateConfig(options = {}) {
|
|
12525
|
+
const { backup: backup2 = true, deleteLegacy = false, dryRun = false } = options;
|
|
12526
|
+
try {
|
|
12527
|
+
if (!needsMigration()) {
|
|
12528
|
+
if (existsSync46(NEW_CONFIG_PATH)) {
|
|
12529
|
+
return {
|
|
12530
|
+
success: true,
|
|
12531
|
+
overridesCount: 0,
|
|
12532
|
+
providersEnabled: ["anthropic"],
|
|
12533
|
+
message: "Config already migrated (config.yaml exists)"
|
|
12534
|
+
};
|
|
12535
|
+
}
|
|
12536
|
+
return {
|
|
12537
|
+
success: false,
|
|
12538
|
+
overridesCount: 0,
|
|
12539
|
+
providersEnabled: [],
|
|
12540
|
+
message: "No legacy settings.json found to migrate"
|
|
12541
|
+
};
|
|
12542
|
+
}
|
|
12543
|
+
const settings = loadSettings();
|
|
12544
|
+
const yamlConfig = convertToYamlConfig(settings);
|
|
12545
|
+
const yamlContent = yaml.dump(yamlConfig, {
|
|
12546
|
+
indent: 2,
|
|
12547
|
+
lineWidth: 120,
|
|
12548
|
+
noRefs: true
|
|
12549
|
+
});
|
|
12550
|
+
if (dryRun) {
|
|
12551
|
+
const providersEnabled2 = Object.entries(yamlConfig.models?.providers || {}).filter(([_, enabled]) => enabled).map(([name]) => name);
|
|
12552
|
+
return {
|
|
12553
|
+
success: true,
|
|
12554
|
+
overridesCount: Object.keys(yamlConfig.models?.overrides || {}).length,
|
|
12555
|
+
providersEnabled: providersEnabled2,
|
|
12556
|
+
message: `Would migrate to smart selection with ${providersEnabled2.length} providers enabled`
|
|
12557
|
+
};
|
|
12558
|
+
}
|
|
12559
|
+
writeFileSync29(NEW_CONFIG_PATH, yamlContent, "utf-8");
|
|
12560
|
+
if (backup2) {
|
|
12561
|
+
const legacyContent = readFileSync39(LEGACY_SETTINGS_PATH, "utf-8");
|
|
12562
|
+
writeFileSync29(BACKUP_SETTINGS_PATH, legacyContent, "utf-8");
|
|
12563
|
+
}
|
|
12564
|
+
if (deleteLegacy) {
|
|
12565
|
+
renameSync(LEGACY_SETTINGS_PATH, `${LEGACY_SETTINGS_PATH}.migrated`);
|
|
12566
|
+
}
|
|
12567
|
+
const providersEnabled = Object.entries(yamlConfig.models?.providers || {}).filter(([_, enabled]) => enabled).map(([name]) => name);
|
|
12568
|
+
return {
|
|
12569
|
+
success: true,
|
|
12570
|
+
overridesCount: Object.keys(yamlConfig.models?.overrides || {}).length,
|
|
12571
|
+
providersEnabled,
|
|
12572
|
+
message: `Successfully migrated to smart selection with ${providersEnabled.length} providers`
|
|
12573
|
+
};
|
|
12574
|
+
} catch (error) {
|
|
12575
|
+
return {
|
|
12576
|
+
success: false,
|
|
12577
|
+
overridesCount: 0,
|
|
12578
|
+
providersEnabled: [],
|
|
12579
|
+
message: "Migration failed",
|
|
12580
|
+
error: error.message
|
|
12581
|
+
};
|
|
12582
|
+
}
|
|
12583
|
+
}
|
|
12584
|
+
|
|
12585
|
+
// src/cli/commands/migrate-config.ts
|
|
12586
|
+
async function migrateConfigCommand(options = {}) {
|
|
12587
|
+
console.log("");
|
|
12588
|
+
console.log(chalk46.bold.cyan("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
12589
|
+
console.log(chalk46.bold.cyan(" CONFIGURATION MIGRATION"));
|
|
12590
|
+
console.log(chalk46.bold.cyan("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
12591
|
+
console.log("");
|
|
12592
|
+
if (!hasLegacySettings()) {
|
|
12593
|
+
console.log(chalk46.yellow("\u2713 No legacy settings.json found"));
|
|
12594
|
+
console.log(chalk46.dim(" You are already using the new config.yaml format."));
|
|
12595
|
+
console.log("");
|
|
12596
|
+
return;
|
|
12597
|
+
}
|
|
12598
|
+
if (!needsMigration() && !options.force) {
|
|
12599
|
+
console.log(chalk46.green("\u2713 Already migrated to config.yaml"));
|
|
12600
|
+
console.log(chalk46.dim(" Use --force to regenerate config.yaml from settings.json"));
|
|
12601
|
+
console.log("");
|
|
12602
|
+
return;
|
|
12603
|
+
}
|
|
12604
|
+
if (options.preview) {
|
|
12605
|
+
const spinner2 = ora19("Generating migration preview...").start();
|
|
12606
|
+
const preview = migrateConfig({ dryRun: true });
|
|
12607
|
+
if (!preview.success) {
|
|
12608
|
+
spinner2.fail("Preview failed");
|
|
12609
|
+
console.error(chalk46.red(`Error: ${preview.error || preview.message}`));
|
|
12610
|
+
return;
|
|
12611
|
+
}
|
|
12612
|
+
spinner2.succeed("Migration preview generated");
|
|
11619
12613
|
console.log("");
|
|
11620
|
-
console.log(
|
|
11621
|
-
console.log(
|
|
12614
|
+
console.log(chalk46.bold("Migration Summary:"));
|
|
12615
|
+
console.log(` Selection: ${chalk46.cyan("Smart (capability-based)")}`);
|
|
12616
|
+
console.log(` Overrides: ${chalk46.cyan(preview.overridesCount)} work types`);
|
|
12617
|
+
console.log(` Providers: ${chalk46.cyan(preview.providersEnabled.join(", "))}`);
|
|
12618
|
+
console.log("");
|
|
12619
|
+
console.log(chalk46.dim("Note: Legacy presets have been replaced with smart selection."));
|
|
12620
|
+
console.log(chalk46.dim("The system now automatically picks the best model for each task."));
|
|
12621
|
+
console.log("");
|
|
12622
|
+
return;
|
|
12623
|
+
}
|
|
12624
|
+
if (!options.force) {
|
|
12625
|
+
const preview = migrateConfig({ dryRun: true });
|
|
12626
|
+
if (preview.success) {
|
|
12627
|
+
console.log(chalk46.bold("Migration will:"));
|
|
12628
|
+
console.log(` \u2022 Create config.yaml with ${chalk46.cyan("smart (capability-based)")} selection`);
|
|
12629
|
+
console.log(` \u2022 Apply ${chalk46.cyan(preview.overridesCount)} work type overrides`);
|
|
12630
|
+
console.log(` \u2022 Enable providers: ${chalk46.cyan(preview.providersEnabled.join(", "))}`);
|
|
12631
|
+
if (options.backup !== false) {
|
|
12632
|
+
console.log(" \u2022 Back up settings.json to settings.json.backup");
|
|
12633
|
+
}
|
|
12634
|
+
if (options.deleteLegacy) {
|
|
12635
|
+
console.log(" \u2022 Rename settings.json to settings.json.migrated");
|
|
12636
|
+
}
|
|
12637
|
+
console.log("");
|
|
12638
|
+
console.log(chalk46.yellow("Note: Legacy presets (Premium/Balanced/Budget) have been removed."));
|
|
12639
|
+
console.log(chalk46.yellow("The new system automatically selects the best model for each task."));
|
|
12640
|
+
console.log("");
|
|
12641
|
+
}
|
|
12642
|
+
const { confirm: confirm3 } = await inquirer5.prompt([
|
|
12643
|
+
{
|
|
12644
|
+
type: "confirm",
|
|
12645
|
+
name: "confirm",
|
|
12646
|
+
message: "Proceed with migration?",
|
|
12647
|
+
default: true
|
|
12648
|
+
}
|
|
12649
|
+
]);
|
|
12650
|
+
if (!confirm3) {
|
|
12651
|
+
console.log(chalk46.yellow("Migration cancelled"));
|
|
12652
|
+
return;
|
|
12653
|
+
}
|
|
12654
|
+
}
|
|
12655
|
+
const spinner = ora19("Migrating configuration...").start();
|
|
12656
|
+
const migrationOptions = {
|
|
12657
|
+
backup: options.backup !== false,
|
|
12658
|
+
// Default to true
|
|
12659
|
+
deleteLegacy: options.deleteLegacy || false
|
|
12660
|
+
};
|
|
12661
|
+
const result = migrateConfig(migrationOptions);
|
|
12662
|
+
if (!result.success) {
|
|
12663
|
+
spinner.fail("Migration failed");
|
|
12664
|
+
console.error(chalk46.red(`Error: ${result.error || result.message}`));
|
|
11622
12665
|
process.exit(1);
|
|
11623
12666
|
}
|
|
12667
|
+
spinner.succeed("Migration complete!");
|
|
12668
|
+
console.log("");
|
|
12669
|
+
console.log(chalk46.bold.green("\u2713 Configuration migrated successfully"));
|
|
12670
|
+
console.log("");
|
|
12671
|
+
console.log(chalk46.bold("Details:"));
|
|
12672
|
+
console.log(` ${chalk46.dim("Selection:")} ${chalk46.cyan("Smart (capability-based)")}`);
|
|
12673
|
+
console.log(` ${chalk46.dim("Work type overrides:")} ${chalk46.cyan(result.overridesCount)}`);
|
|
12674
|
+
console.log(` ${chalk46.dim("Enabled providers:")} ${chalk46.cyan(result.providersEnabled.join(", "))}`);
|
|
12675
|
+
console.log("");
|
|
12676
|
+
if (migrationOptions.backup) {
|
|
12677
|
+
console.log(chalk46.dim(" Legacy settings.json backed up to settings.json.backup"));
|
|
12678
|
+
}
|
|
12679
|
+
if (migrationOptions.deleteLegacy) {
|
|
12680
|
+
console.log(chalk46.dim(" Legacy settings.json renamed to settings.json.migrated"));
|
|
12681
|
+
}
|
|
12682
|
+
console.log("");
|
|
12683
|
+
console.log(chalk46.bold("Next steps:"));
|
|
12684
|
+
console.log(" 1. Review your new config: " + chalk46.cyan("~/.panopticon/config.yaml"));
|
|
12685
|
+
console.log(" 2. Enable additional providers for more model options");
|
|
12686
|
+
console.log(" 3. Add work type overrides if you prefer specific models for tasks");
|
|
12687
|
+
console.log(" 4. Documentation: " + chalk46.cyan("docs/CONFIGURATION.md"));
|
|
12688
|
+
console.log("");
|
|
11624
12689
|
}
|
|
11625
12690
|
|
|
11626
12691
|
// src/cli/index.ts
|
|
12692
|
+
var PANOPTICON_ENV_FILE = join48(homedir20(), ".panopticon.env");
|
|
12693
|
+
if (existsSync47(PANOPTICON_ENV_FILE)) {
|
|
12694
|
+
try {
|
|
12695
|
+
const envContent = readFileSync40(PANOPTICON_ENV_FILE, "utf-8");
|
|
12696
|
+
for (const line of envContent.split("\n")) {
|
|
12697
|
+
const trimmed = line.trim();
|
|
12698
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
12699
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
12700
|
+
if (match) {
|
|
12701
|
+
const [, key, value] = match;
|
|
12702
|
+
if (process.env[key] === void 0) {
|
|
12703
|
+
process.env[key] = value.trim();
|
|
12704
|
+
}
|
|
12705
|
+
}
|
|
12706
|
+
}
|
|
12707
|
+
} catch (error) {
|
|
12708
|
+
console.warn("Warning: Failed to load ~/.panopticon.env:", error.message);
|
|
12709
|
+
}
|
|
12710
|
+
}
|
|
11627
12711
|
var program = new Command();
|
|
11628
12712
|
program.name("pan").description("Multi-agent orchestration for AI coding assistants").version("0.1.3");
|
|
11629
12713
|
program.command("init").description("Initialize Panopticon (~/.panopticon/)").action(initCommand);
|
|
@@ -11638,90 +12722,93 @@ registerWorkspaceCommands(program);
|
|
|
11638
12722
|
registerTestCommands(program);
|
|
11639
12723
|
registerCloisterCommands(program);
|
|
11640
12724
|
registerSpecialistsCommands(program);
|
|
12725
|
+
registerConvoyCommands(program);
|
|
11641
12726
|
registerSetupCommands(program);
|
|
11642
12727
|
registerInstallCommand(program);
|
|
11643
12728
|
registerDbCommands(program);
|
|
11644
12729
|
registerBeadsCommands(program);
|
|
12730
|
+
program.command("migrate-config").description("Migrate from settings.json to config.yaml").option("--force", "Force migration even if config.yaml exists").option("--preview", "Preview migration without applying changes").option("--no-backup", "Do not back up settings.json").option("--delete-legacy", "Delete settings.json after migration").action(migrateConfigCommand);
|
|
11645
12731
|
program.command("status").description("Show running agents (shorthand for work status)").option("--json", "Output as JSON").action(statusCommand);
|
|
11646
12732
|
program.command("up").description("Start dashboard (and Traefik if enabled)").option("--detach", "Run in background").option("--skip-traefik", "Skip Traefik startup").action(async (options) => {
|
|
11647
|
-
const { spawn, execSync:
|
|
11648
|
-
const { join:
|
|
12733
|
+
const { spawn, execSync: execSync11 } = await import("child_process");
|
|
12734
|
+
const { join: join49, dirname: dirname12 } = await import("path");
|
|
11649
12735
|
const { fileURLToPath: fileURLToPath5 } = await import("url");
|
|
11650
|
-
const { readFileSync:
|
|
12736
|
+
const { readFileSync: readFileSync41, existsSync: existsSync48 } = await import("fs");
|
|
11651
12737
|
const { parse: parse2 } = await import("@iarna/toml");
|
|
11652
12738
|
const __dirname5 = dirname12(fileURLToPath5(import.meta.url));
|
|
11653
|
-
const bundledServer =
|
|
11654
|
-
const srcDashboard =
|
|
11655
|
-
const configFile =
|
|
12739
|
+
const bundledServer = join49(__dirname5, "..", "dashboard", "server.js");
|
|
12740
|
+
const srcDashboard = join49(__dirname5, "..", "..", "src", "dashboard");
|
|
12741
|
+
const configFile = join49(process.env.HOME || "", ".panopticon", "config.toml");
|
|
11656
12742
|
let traefikEnabled = false;
|
|
11657
12743
|
let traefikDomain = "pan.localhost";
|
|
11658
|
-
if (
|
|
12744
|
+
if (existsSync48(configFile)) {
|
|
11659
12745
|
try {
|
|
11660
|
-
const configContent =
|
|
12746
|
+
const configContent = readFileSync41(configFile, "utf-8");
|
|
11661
12747
|
const config2 = parse2(configContent);
|
|
11662
12748
|
traefikEnabled = config2.traefik?.enabled === true;
|
|
11663
12749
|
traefikDomain = config2.traefik?.domain || "pan.localhost";
|
|
11664
12750
|
} catch (error) {
|
|
11665
|
-
console.log(
|
|
12751
|
+
console.log(chalk47.yellow("Warning: Could not read config.toml"));
|
|
11666
12752
|
}
|
|
11667
12753
|
}
|
|
11668
|
-
console.log(
|
|
12754
|
+
console.log(chalk47.bold("Starting Panopticon...\n"));
|
|
11669
12755
|
if (traefikEnabled && !options.skipTraefik) {
|
|
11670
|
-
const traefikDir =
|
|
11671
|
-
if (
|
|
12756
|
+
const traefikDir = join49(process.env.HOME || "", ".panopticon", "traefik");
|
|
12757
|
+
if (existsSync48(traefikDir)) {
|
|
11672
12758
|
try {
|
|
11673
|
-
const composeFile =
|
|
11674
|
-
if (
|
|
11675
|
-
const content =
|
|
12759
|
+
const composeFile = join49(traefikDir, "docker-compose.yml");
|
|
12760
|
+
if (existsSync48(composeFile)) {
|
|
12761
|
+
const content = readFileSync41(composeFile, "utf-8");
|
|
11676
12762
|
if (!content.includes("external: true") && content.includes("panopticon:")) {
|
|
11677
12763
|
const patched = content.replace(
|
|
11678
12764
|
/networks:\s*\n\s*panopticon:\s*\n\s*name: panopticon\s*\n\s*driver: bridge/,
|
|
11679
12765
|
"networks:\n panopticon:\n name: panopticon\n external: true # Network created by 'pan install'"
|
|
11680
12766
|
);
|
|
11681
|
-
const { writeFileSync:
|
|
11682
|
-
|
|
11683
|
-
console.log(
|
|
12767
|
+
const { writeFileSync: writeFileSync30 } = await import("fs");
|
|
12768
|
+
writeFileSync30(composeFile, patched);
|
|
12769
|
+
console.log(chalk47.dim(" (migrated network config)"));
|
|
11684
12770
|
}
|
|
11685
12771
|
}
|
|
11686
|
-
console.log(
|
|
11687
|
-
|
|
12772
|
+
console.log(chalk47.dim("Starting Traefik..."));
|
|
12773
|
+
execSync11("docker-compose up -d", {
|
|
11688
12774
|
cwd: traefikDir,
|
|
11689
12775
|
stdio: "pipe"
|
|
11690
12776
|
});
|
|
11691
|
-
console.log(
|
|
11692
|
-
console.log(
|
|
12777
|
+
console.log(chalk47.green("\u2713 Traefik started"));
|
|
12778
|
+
console.log(chalk47.dim(` Dashboard: https://traefik.${traefikDomain}:8080
|
|
11693
12779
|
`));
|
|
11694
12780
|
} catch (error) {
|
|
11695
|
-
console.log(
|
|
11696
|
-
console.log(
|
|
12781
|
+
console.log(chalk47.yellow("\u26A0 Failed to start Traefik (continuing anyway)"));
|
|
12782
|
+
console.log(chalk47.dim(" Run with --skip-traefik to suppress this message\n"));
|
|
11697
12783
|
}
|
|
11698
12784
|
}
|
|
11699
12785
|
}
|
|
11700
|
-
const isProduction =
|
|
11701
|
-
const isDevelopment =
|
|
12786
|
+
const isProduction = existsSync48(bundledServer);
|
|
12787
|
+
const isDevelopment = existsSync48(srcDashboard);
|
|
11702
12788
|
if (!isProduction && !isDevelopment) {
|
|
11703
|
-
console.error(
|
|
11704
|
-
console.error(
|
|
12789
|
+
console.error(chalk47.red("Error: Dashboard not found"));
|
|
12790
|
+
console.error(chalk47.dim("This may be a corrupted installation. Try reinstalling panopticon-cli."));
|
|
11705
12791
|
process.exit(1);
|
|
11706
12792
|
}
|
|
11707
12793
|
if (isDevelopment && !isProduction) {
|
|
11708
12794
|
try {
|
|
11709
|
-
|
|
12795
|
+
execSync11("npm --version", { stdio: "pipe" });
|
|
11710
12796
|
} catch {
|
|
11711
|
-
console.error(
|
|
11712
|
-
console.error(
|
|
12797
|
+
console.error(chalk47.red("Error: npm not found in PATH"));
|
|
12798
|
+
console.error(chalk47.dim("Make sure Node.js and npm are installed and in your PATH"));
|
|
11713
12799
|
process.exit(1);
|
|
11714
12800
|
}
|
|
11715
12801
|
}
|
|
11716
12802
|
if (isProduction) {
|
|
11717
|
-
console.log(
|
|
12803
|
+
console.log(chalk47.dim("Starting dashboard (bundled mode)..."));
|
|
11718
12804
|
} else {
|
|
11719
|
-
console.log(
|
|
12805
|
+
console.log(chalk47.dim("Starting dashboard (development mode)..."));
|
|
11720
12806
|
}
|
|
11721
12807
|
if (options.detach) {
|
|
11722
12808
|
const child = isProduction ? spawn("node", [bundledServer], {
|
|
11723
12809
|
detached: true,
|
|
11724
|
-
stdio: "ignore"
|
|
12810
|
+
stdio: "ignore",
|
|
12811
|
+
env: { ...process.env, DASHBOARD_PORT: "3010" }
|
|
11725
12812
|
}) : spawn("npm", ["run", "dev"], {
|
|
11726
12813
|
cwd: srcDashboard,
|
|
11727
12814
|
detached: true,
|
|
@@ -11731,7 +12818,7 @@ program.command("up").description("Start dashboard (and Traefik if enabled)").op
|
|
|
11731
12818
|
let hasError = false;
|
|
11732
12819
|
child.on("error", (err) => {
|
|
11733
12820
|
hasError = true;
|
|
11734
|
-
console.error(
|
|
12821
|
+
console.error(chalk47.red("Failed to start dashboard in background:"), err.message);
|
|
11735
12822
|
process.exit(1);
|
|
11736
12823
|
});
|
|
11737
12824
|
setTimeout(() => {
|
|
@@ -11739,72 +12826,78 @@ program.command("up").description("Start dashboard (and Traefik if enabled)").op
|
|
|
11739
12826
|
child.unref();
|
|
11740
12827
|
}
|
|
11741
12828
|
}, 100);
|
|
11742
|
-
console.log(
|
|
12829
|
+
console.log(chalk47.green("\u2713 Dashboard started in background"));
|
|
11743
12830
|
if (traefikEnabled) {
|
|
11744
|
-
console.log(` Frontend: ${
|
|
11745
|
-
console.log(` API: ${
|
|
12831
|
+
console.log(` Frontend: ${chalk47.cyan(`https://${traefikDomain}`)}`);
|
|
12832
|
+
console.log(` API: ${chalk47.cyan(`https://${traefikDomain}/api`)}`);
|
|
12833
|
+
} else if (isProduction) {
|
|
12834
|
+
console.log(` URL: ${chalk47.cyan("http://localhost:3010")}`);
|
|
11746
12835
|
} else {
|
|
11747
|
-
console.log(` Frontend: ${
|
|
11748
|
-
console.log(` API: ${
|
|
12836
|
+
console.log(` Frontend: ${chalk47.cyan("http://localhost:3001")}`);
|
|
12837
|
+
console.log(` API: ${chalk47.cyan("http://localhost:3002")}`);
|
|
11749
12838
|
}
|
|
11750
12839
|
} else {
|
|
11751
12840
|
if (traefikEnabled) {
|
|
11752
|
-
console.log(` Frontend: ${
|
|
11753
|
-
console.log(` API: ${
|
|
12841
|
+
console.log(` Frontend: ${chalk47.cyan(`https://${traefikDomain}`)}`);
|
|
12842
|
+
console.log(` API: ${chalk47.cyan(`https://${traefikDomain}/api`)}`);
|
|
12843
|
+
} else if (isProduction) {
|
|
12844
|
+
console.log(` URL: ${chalk47.cyan("http://localhost:3010")}`);
|
|
11754
12845
|
} else {
|
|
11755
|
-
console.log(` Frontend: ${
|
|
11756
|
-
console.log(` API: ${
|
|
12846
|
+
console.log(` Frontend: ${chalk47.cyan("http://localhost:3001")}`);
|
|
12847
|
+
console.log(` API: ${chalk47.cyan("http://localhost:3002")}`);
|
|
11757
12848
|
}
|
|
11758
|
-
console.log(
|
|
12849
|
+
console.log(chalk47.dim("\nPress Ctrl+C to stop\n"));
|
|
11759
12850
|
const child = isProduction ? spawn("node", [bundledServer], {
|
|
11760
|
-
stdio: "inherit"
|
|
12851
|
+
stdio: "inherit",
|
|
12852
|
+
env: { ...process.env, DASHBOARD_PORT: "3010" }
|
|
11761
12853
|
}) : spawn("npm", ["run", "dev"], {
|
|
11762
12854
|
cwd: srcDashboard,
|
|
11763
12855
|
stdio: "inherit",
|
|
11764
12856
|
shell: true
|
|
11765
12857
|
});
|
|
11766
12858
|
child.on("error", (err) => {
|
|
11767
|
-
console.error(
|
|
12859
|
+
console.error(chalk47.red("Failed to start dashboard:"), err.message);
|
|
11768
12860
|
process.exit(1);
|
|
11769
12861
|
});
|
|
11770
12862
|
}
|
|
11771
12863
|
});
|
|
11772
12864
|
program.command("down").description("Stop dashboard (and Traefik if enabled)").option("--skip-traefik", "Skip Traefik shutdown").action(async (options) => {
|
|
11773
|
-
const { execSync:
|
|
11774
|
-
const { join:
|
|
11775
|
-
const { readFileSync:
|
|
12865
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
12866
|
+
const { join: join49 } = await import("path");
|
|
12867
|
+
const { readFileSync: readFileSync41, existsSync: existsSync48 } = await import("fs");
|
|
11776
12868
|
const { parse: parse2 } = await import("@iarna/toml");
|
|
11777
|
-
console.log(
|
|
11778
|
-
console.log(
|
|
12869
|
+
console.log(chalk47.bold("Stopping Panopticon...\n"));
|
|
12870
|
+
console.log(chalk47.dim("Stopping dashboard..."));
|
|
11779
12871
|
try {
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
12872
|
+
execSync11("lsof -ti:3001 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
|
|
12873
|
+
execSync11("lsof -ti:3002 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
|
|
12874
|
+
execSync11("lsof -ti:3010 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
|
|
12875
|
+
console.log(chalk47.green("\u2713 Dashboard stopped"));
|
|
11783
12876
|
} catch {
|
|
11784
|
-
console.log(
|
|
12877
|
+
console.log(chalk47.dim(" No dashboard processes found"));
|
|
11785
12878
|
}
|
|
11786
|
-
const configFile =
|
|
12879
|
+
const configFile = join49(process.env.HOME || "", ".panopticon", "config.toml");
|
|
11787
12880
|
let traefikEnabled = false;
|
|
11788
|
-
if (
|
|
12881
|
+
if (existsSync48(configFile)) {
|
|
11789
12882
|
try {
|
|
11790
|
-
const configContent =
|
|
12883
|
+
const configContent = readFileSync41(configFile, "utf-8");
|
|
11791
12884
|
const config2 = parse2(configContent);
|
|
11792
12885
|
traefikEnabled = config2.traefik?.enabled === true;
|
|
11793
12886
|
} catch (error) {
|
|
11794
12887
|
}
|
|
11795
12888
|
}
|
|
11796
12889
|
if (traefikEnabled && !options.skipTraefik) {
|
|
11797
|
-
const traefikDir =
|
|
11798
|
-
if (
|
|
11799
|
-
console.log(
|
|
12890
|
+
const traefikDir = join49(process.env.HOME || "", ".panopticon", "traefik");
|
|
12891
|
+
if (existsSync48(traefikDir)) {
|
|
12892
|
+
console.log(chalk47.dim("Stopping Traefik..."));
|
|
11800
12893
|
try {
|
|
11801
|
-
|
|
12894
|
+
execSync11("docker-compose down", {
|
|
11802
12895
|
cwd: traefikDir,
|
|
11803
12896
|
stdio: "pipe"
|
|
11804
12897
|
});
|
|
11805
|
-
console.log(
|
|
12898
|
+
console.log(chalk47.green("\u2713 Traefik stopped"));
|
|
11806
12899
|
} catch (error) {
|
|
11807
|
-
console.log(
|
|
12900
|
+
console.log(chalk47.yellow("\u26A0 Failed to stop Traefik"));
|
|
11808
12901
|
}
|
|
11809
12902
|
}
|
|
11810
12903
|
}
|