panopticon-cli 0.3.4 → 0.3.7
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 +24 -5
- package/dist/cli/index.js +784 -493
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/public/assets/index-CUoYoWX_.css +32 -0
- package/dist/dashboard/public/assets/index-CY0Ew5B9.js +423 -0
- package/dist/dashboard/public/index.html +13 -0
- package/dist/dashboard/server.js +102757 -0
- package/package.json +8 -3
- package/templates/traefik/docker-compose.yml +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
|
|
47
47
|
// src/cli/index.ts
|
|
48
48
|
import { Command } from "commander";
|
|
49
|
-
import
|
|
49
|
+
import chalk34 from "chalk";
|
|
50
50
|
|
|
51
51
|
// src/cli/commands/init.ts
|
|
52
52
|
import { existsSync, mkdirSync, readdirSync, cpSync } from "fs";
|
|
@@ -606,13 +606,13 @@ function sendMail(toAgentId, from, message, priority = "normal") {
|
|
|
606
606
|
JSON.stringify(mailItem, null, 2)
|
|
607
607
|
);
|
|
608
608
|
}
|
|
609
|
-
function
|
|
609
|
+
function generateFixedPointPrompt(agentId) {
|
|
610
610
|
const { hasWork, urgentCount, items } = checkHook(agentId);
|
|
611
611
|
if (!hasWork) return null;
|
|
612
612
|
const lines = [
|
|
613
|
-
"#
|
|
613
|
+
"# FPP: Work Found on Your Hook",
|
|
614
614
|
"",
|
|
615
|
-
'> "
|
|
615
|
+
'> "Any runnable action is a fixed point and must resolve before the system can rest."',
|
|
616
616
|
""
|
|
617
617
|
];
|
|
618
618
|
if (urgentCount > 0) {
|
|
@@ -820,9 +820,9 @@ function spawnAgent(options) {
|
|
|
820
820
|
let prompt = options.prompt || "";
|
|
821
821
|
const { hasWork, items } = checkHook(agentId);
|
|
822
822
|
if (hasWork) {
|
|
823
|
-
const
|
|
824
|
-
if (
|
|
825
|
-
prompt =
|
|
823
|
+
const fixedPointPrompt = generateFixedPointPrompt(agentId);
|
|
824
|
+
if (fixedPointPrompt) {
|
|
825
|
+
prompt = fixedPointPrompt + "\n\n---\n\n" + prompt;
|
|
826
826
|
}
|
|
827
827
|
}
|
|
828
828
|
const promptFile = join5(getAgentDir(agentId), "initial-prompt.md");
|
|
@@ -959,17 +959,17 @@ function generateRecoveryPrompt(state) {
|
|
|
959
959
|
"3. Check hook for pending work: `pan work hook check`",
|
|
960
960
|
"4. Resume from last known state",
|
|
961
961
|
"",
|
|
962
|
-
"##
|
|
963
|
-
'> "
|
|
962
|
+
"## FPP Reminder",
|
|
963
|
+
'> "Any runnable action is a fixed point and must resolve before the system can rest."',
|
|
964
964
|
""
|
|
965
965
|
];
|
|
966
966
|
const { hasWork } = checkHook(state.id);
|
|
967
967
|
if (hasWork) {
|
|
968
|
-
const
|
|
969
|
-
if (
|
|
968
|
+
const fixedPointPrompt = generateFixedPointPrompt(state.id);
|
|
969
|
+
if (fixedPointPrompt) {
|
|
970
970
|
lines.push("---");
|
|
971
971
|
lines.push("");
|
|
972
|
-
lines.push(
|
|
972
|
+
lines.push(fixedPointPrompt);
|
|
973
973
|
}
|
|
974
974
|
}
|
|
975
975
|
return lines.join("\n");
|
|
@@ -1328,12 +1328,12 @@ import chalk9 from "chalk";
|
|
|
1328
1328
|
async function killCommand(id, options) {
|
|
1329
1329
|
const agentId = id.startsWith("agent-") ? id : `agent-${id.toLowerCase()}`;
|
|
1330
1330
|
const state = getAgentState(agentId);
|
|
1331
|
-
const
|
|
1332
|
-
if (!state && !
|
|
1331
|
+
const isRunning3 = sessionExists(agentId);
|
|
1332
|
+
if (!state && !isRunning3) {
|
|
1333
1333
|
console.log(chalk9.yellow(`Agent ${agentId} not found.`));
|
|
1334
1334
|
return;
|
|
1335
1335
|
}
|
|
1336
|
-
if (!options.force &&
|
|
1336
|
+
if (!options.force && isRunning3) {
|
|
1337
1337
|
}
|
|
1338
1338
|
try {
|
|
1339
1339
|
stopAgent(agentId);
|
|
@@ -1420,7 +1420,7 @@ function findPRForBranch(workspace) {
|
|
|
1420
1420
|
}
|
|
1421
1421
|
function mergePR(workspace, prNumber) {
|
|
1422
1422
|
try {
|
|
1423
|
-
execSync3(`gh pr merge ${prNumber} --squash
|
|
1423
|
+
execSync3(`gh pr merge ${prNumber} --squash`, {
|
|
1424
1424
|
cwd: workspace,
|
|
1425
1425
|
encoding: "utf-8",
|
|
1426
1426
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1432,8 +1432,8 @@ function mergePR(workspace, prNumber) {
|
|
|
1432
1432
|
}
|
|
1433
1433
|
async function updateLinearStatus(apiKey, issueIdentifier) {
|
|
1434
1434
|
try {
|
|
1435
|
-
const { LinearClient } = await import("@linear/sdk");
|
|
1436
|
-
const client = new
|
|
1435
|
+
const { LinearClient: LinearClient2 } = await import("@linear/sdk");
|
|
1436
|
+
const client = new LinearClient2({ apiKey });
|
|
1437
1437
|
const me = await client.viewer;
|
|
1438
1438
|
const teams = await me.teams();
|
|
1439
1439
|
const team = teams.nodes[0];
|
|
@@ -1938,8 +1938,8 @@ async function planCommand(id, options = {}) {
|
|
|
1938
1938
|
process.exit(1);
|
|
1939
1939
|
}
|
|
1940
1940
|
spinner.text = "Fetching issue from Linear...";
|
|
1941
|
-
const { LinearClient } = await import("@linear/sdk");
|
|
1942
|
-
const client = new
|
|
1941
|
+
const { LinearClient: LinearClient2 } = await import("@linear/sdk");
|
|
1942
|
+
const client = new LinearClient2({ apiKey });
|
|
1943
1943
|
const me = await client.viewer;
|
|
1944
1944
|
const teams = await me.teams();
|
|
1945
1945
|
const team = teams.nodes[0];
|
|
@@ -2519,10 +2519,10 @@ async function hookCommand(action, idOrMessage, options = {}) {
|
|
|
2519
2519
|
console.log(chalk15.green(`\u2713 Mail sent to ${targetAgent}`));
|
|
2520
2520
|
break;
|
|
2521
2521
|
}
|
|
2522
|
-
case "
|
|
2523
|
-
const prompt =
|
|
2522
|
+
case "fpp": {
|
|
2523
|
+
const prompt = generateFixedPointPrompt(idOrMessage || agentId);
|
|
2524
2524
|
if (!prompt) {
|
|
2525
|
-
console.log(chalk15.green("No
|
|
2525
|
+
console.log(chalk15.green("No fixed point work found"));
|
|
2526
2526
|
return;
|
|
2527
2527
|
}
|
|
2528
2528
|
console.log(prompt);
|
|
@@ -2536,7 +2536,7 @@ async function hookCommand(action, idOrMessage, options = {}) {
|
|
|
2536
2536
|
console.log(` ${chalk15.cyan("pan work hook pop <item-id>")} - Remove completed item`);
|
|
2537
2537
|
console.log(` ${chalk15.cyan("pan work hook clear [agent-id]")} - Clear all hook items`);
|
|
2538
2538
|
console.log(` ${chalk15.cyan("pan work hook mail <agent-id> <msg>")} - Send mail to agent`);
|
|
2539
|
-
console.log(` ${chalk15.cyan("pan work hook
|
|
2539
|
+
console.log(` ${chalk15.cyan("pan work hook fpp [agent-id]")} - Generate FPP prompt`);
|
|
2540
2540
|
}
|
|
2541
2541
|
}
|
|
2542
2542
|
|
|
@@ -3431,6 +3431,178 @@ async function healthCommand(action, arg, options = {}) {
|
|
|
3431
3431
|
}
|
|
3432
3432
|
}
|
|
3433
3433
|
|
|
3434
|
+
// src/cli/commands/work/reopen.ts
|
|
3435
|
+
import chalk20 from "chalk";
|
|
3436
|
+
import ora10 from "ora";
|
|
3437
|
+
import inquirer3 from "inquirer";
|
|
3438
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
3439
|
+
import { join as join13 } from "path";
|
|
3440
|
+
import { homedir as homedir5 } from "os";
|
|
3441
|
+
import { LinearClient } from "@linear/sdk";
|
|
3442
|
+
function getLinearApiKey3() {
|
|
3443
|
+
const envFile = join13(homedir5(), ".panopticon.env");
|
|
3444
|
+
if (existsSync14(envFile)) {
|
|
3445
|
+
const content = readFileSync13(envFile, "utf-8");
|
|
3446
|
+
const match = content.match(/LINEAR_API_KEY=(.+)/);
|
|
3447
|
+
if (match) return match[1].trim();
|
|
3448
|
+
}
|
|
3449
|
+
return process.env.LINEAR_API_KEY || null;
|
|
3450
|
+
}
|
|
3451
|
+
async function fetchIssueWithComments(client, issueId) {
|
|
3452
|
+
const results = await client.searchIssues(issueId, { first: 1 });
|
|
3453
|
+
if (results.nodes.length === 0) {
|
|
3454
|
+
throw new Error(`Issue not found: ${issueId}`);
|
|
3455
|
+
}
|
|
3456
|
+
const linearIssue = results.nodes[0];
|
|
3457
|
+
const state = await linearIssue.state;
|
|
3458
|
+
const commentsData = await linearIssue.comments();
|
|
3459
|
+
const comments = [];
|
|
3460
|
+
for (const comment of commentsData.nodes) {
|
|
3461
|
+
const user = await comment.user;
|
|
3462
|
+
comments.push({
|
|
3463
|
+
id: comment.id,
|
|
3464
|
+
body: comment.body,
|
|
3465
|
+
author: user?.name ?? "Unknown",
|
|
3466
|
+
createdAt: comment.createdAt.toISOString()
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
return {
|
|
3470
|
+
issue: {
|
|
3471
|
+
id: linearIssue.id,
|
|
3472
|
+
identifier: linearIssue.identifier,
|
|
3473
|
+
title: linearIssue.title,
|
|
3474
|
+
description: linearIssue.description || void 0,
|
|
3475
|
+
state: state?.name || "Unknown",
|
|
3476
|
+
url: linearIssue.url
|
|
3477
|
+
},
|
|
3478
|
+
comments
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3481
|
+
async function reopenIssue(client, issueId) {
|
|
3482
|
+
const results = await client.searchIssues(issueId, { first: 1 });
|
|
3483
|
+
if (results.nodes.length === 0) {
|
|
3484
|
+
throw new Error(`Issue not found: ${issueId}`);
|
|
3485
|
+
}
|
|
3486
|
+
const linearIssue = results.nodes[0];
|
|
3487
|
+
const team = await linearIssue.team;
|
|
3488
|
+
if (!team) {
|
|
3489
|
+
throw new Error("Could not determine issue team");
|
|
3490
|
+
}
|
|
3491
|
+
const states = await team.states();
|
|
3492
|
+
const backlogState = states.nodes.find((s) => s.type === "backlog");
|
|
3493
|
+
const unstartedState = states.nodes.find((s) => s.type === "unstarted");
|
|
3494
|
+
const targetState = backlogState || unstartedState;
|
|
3495
|
+
if (!targetState) {
|
|
3496
|
+
throw new Error("No backlog or unstarted state found");
|
|
3497
|
+
}
|
|
3498
|
+
await client.updateIssue(linearIssue.id, {
|
|
3499
|
+
stateId: targetState.id
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3502
|
+
function formatComments(comments) {
|
|
3503
|
+
if (comments.length === 0) {
|
|
3504
|
+
return "No comments";
|
|
3505
|
+
}
|
|
3506
|
+
return comments.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()).map((c) => {
|
|
3507
|
+
const date = new Date(c.createdAt).toLocaleString();
|
|
3508
|
+
const truncatedBody = c.body.length > 200 ? c.body.slice(0, 200) + "..." : c.body;
|
|
3509
|
+
return ` [${date}] ${c.author}:
|
|
3510
|
+
${truncatedBody.replace(/\n/g, "\n ")}`;
|
|
3511
|
+
}).join("\n\n");
|
|
3512
|
+
}
|
|
3513
|
+
async function reopenCommand(id, options = {}) {
|
|
3514
|
+
const spinner = ora10(`Fetching issue ${id}...`).start();
|
|
3515
|
+
try {
|
|
3516
|
+
const apiKey = getLinearApiKey3();
|
|
3517
|
+
if (!apiKey) {
|
|
3518
|
+
spinner.fail("LINEAR_API_KEY not found");
|
|
3519
|
+
console.log("");
|
|
3520
|
+
console.log(chalk20.dim("Set it in ~/.panopticon.env:"));
|
|
3521
|
+
console.log(" LINEAR_API_KEY=lin_api_xxxxx");
|
|
3522
|
+
process.exit(1);
|
|
3523
|
+
}
|
|
3524
|
+
const client = new LinearClient({ apiKey });
|
|
3525
|
+
spinner.text = "Fetching issue and comments...";
|
|
3526
|
+
const { issue, comments } = await fetchIssueWithComments(client, id);
|
|
3527
|
+
spinner.stop();
|
|
3528
|
+
console.log("");
|
|
3529
|
+
console.log(chalk20.bold("\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"));
|
|
3530
|
+
console.log(chalk20.bold(` Reopen: ${issue.identifier}`));
|
|
3531
|
+
console.log(chalk20.bold("\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"));
|
|
3532
|
+
console.log("");
|
|
3533
|
+
console.log(chalk20.bold("Title:"), issue.title);
|
|
3534
|
+
console.log(chalk20.bold("Current State:"), issue.state);
|
|
3535
|
+
console.log(chalk20.bold("URL:"), issue.url);
|
|
3536
|
+
console.log("");
|
|
3537
|
+
if (issue.description) {
|
|
3538
|
+
console.log(chalk20.bold("Description:"));
|
|
3539
|
+
const descPreview = issue.description.length > 300 ? issue.description.slice(0, 300) + "..." : issue.description;
|
|
3540
|
+
console.log(chalk20.dim(descPreview));
|
|
3541
|
+
console.log("");
|
|
3542
|
+
}
|
|
3543
|
+
console.log(chalk20.bold(`Comments (${comments.length}):`));
|
|
3544
|
+
if (comments.length > 0) {
|
|
3545
|
+
console.log(formatComments(comments));
|
|
3546
|
+
} else {
|
|
3547
|
+
console.log(chalk20.dim(" No comments"));
|
|
3548
|
+
}
|
|
3549
|
+
console.log("");
|
|
3550
|
+
if (options.json) {
|
|
3551
|
+
console.log(
|
|
3552
|
+
JSON.stringify(
|
|
3553
|
+
{
|
|
3554
|
+
issue,
|
|
3555
|
+
comments
|
|
3556
|
+
},
|
|
3557
|
+
null,
|
|
3558
|
+
2
|
|
3559
|
+
)
|
|
3560
|
+
);
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
if (!options.force) {
|
|
3564
|
+
const confirm2 = await inquirer3.prompt([
|
|
3565
|
+
{
|
|
3566
|
+
type: "confirm",
|
|
3567
|
+
name: "proceed",
|
|
3568
|
+
message: `Reopen ${issue.identifier} and run planning?`,
|
|
3569
|
+
default: true
|
|
3570
|
+
}
|
|
3571
|
+
]);
|
|
3572
|
+
if (!confirm2.proceed) {
|
|
3573
|
+
console.log(chalk20.yellow("Cancelled"));
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
const reopenSpinner = ora10("Transitioning issue to backlog...").start();
|
|
3578
|
+
await reopenIssue(client, id);
|
|
3579
|
+
reopenSpinner.succeed(`Issue ${issue.identifier} reopened`);
|
|
3580
|
+
const commentSpinner = ora10("Adding reopen comment...").start();
|
|
3581
|
+
await client.createComment({
|
|
3582
|
+
issueId: issue.id,
|
|
3583
|
+
body: `Issue reopened for re-planning via Panopticon.
|
|
3584
|
+
|
|
3585
|
+
Previous state: ${issue.state}`
|
|
3586
|
+
});
|
|
3587
|
+
commentSpinner.succeed("Added reopen comment");
|
|
3588
|
+
if (!options.skipPlan) {
|
|
3589
|
+
console.log("");
|
|
3590
|
+
console.log(chalk20.cyan("Running planning workflow..."));
|
|
3591
|
+
console.log("");
|
|
3592
|
+
await planCommand(id, { force: true });
|
|
3593
|
+
} else {
|
|
3594
|
+
console.log("");
|
|
3595
|
+
console.log(chalk20.green("Issue reopened. Run planning manually:"));
|
|
3596
|
+
console.log(` pan work plan ${id}`);
|
|
3597
|
+
console.log("");
|
|
3598
|
+
}
|
|
3599
|
+
} catch (error) {
|
|
3600
|
+
if (spinner.isSpinning) spinner.fail();
|
|
3601
|
+
console.error(chalk20.red(`Error: ${error.message}`));
|
|
3602
|
+
process.exit(1);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3434
3606
|
// src/cli/commands/work/index.ts
|
|
3435
3607
|
function registerWorkCommands(program2) {
|
|
3436
3608
|
const work = program2.command("work").description("Agent and work management");
|
|
@@ -3443,7 +3615,7 @@ function registerWorkCommands(program2) {
|
|
|
3443
3615
|
work.command("plan <id>").description("Create execution plan before spawning").option("-o, --output <path>", "Output file path").option("--json", "Output as JSON").option("--skip-discovery", "Skip interactive discovery phase").option("--force", "Force planning even for simple issues").action(planCommand);
|
|
3444
3616
|
work.command("list").description("List issues from configured trackers").option("--all", "Include closed issues").option("--mine", "Show only my assigned issues").option("--json", "Output as JSON").option("--tracker <type>", "Query specific tracker (linear/github/gitlab)").option("--all-trackers", "Query all configured trackers").action(listCommand);
|
|
3445
3617
|
work.command("triage [id]").description("Triage secondary tracker issues").option("--create", "Create primary issue from secondary").option("--dismiss <reason>", "Dismiss from triage").action(triageCommand);
|
|
3446
|
-
work.command("hook [action] [idOrMessage...]").description("
|
|
3618
|
+
work.command("hook [action] [idOrMessage...]").description("FPP hooks: check, push, pop, clear, mail, fpp").option("--json", "Output as JSON").action((action, idOrMessage, options) => {
|
|
3447
3619
|
hookCommand(action || "help", idOrMessage?.join(" "), options);
|
|
3448
3620
|
});
|
|
3449
3621
|
work.command("recover [id]").description("Recover crashed agents").option("--all", "Auto-recover all crashed agents").option("--json", "Output as JSON").action(recoverCommand);
|
|
@@ -3457,13 +3629,14 @@ function registerWorkCommands(program2) {
|
|
|
3457
3629
|
interval: parseInt(options.interval, 10)
|
|
3458
3630
|
});
|
|
3459
3631
|
});
|
|
3632
|
+
work.command("reopen <id>").description("Reopen a closed/done issue and re-run planning").option("--skip-plan", "Skip automatic planning after reopen").option("--json", "Output as JSON").option("--force", "Skip confirmation").action(reopenCommand);
|
|
3460
3633
|
}
|
|
3461
3634
|
|
|
3462
3635
|
// src/cli/commands/workspace.ts
|
|
3463
|
-
import
|
|
3464
|
-
import
|
|
3465
|
-
import { existsSync as
|
|
3466
|
-
import { join as
|
|
3636
|
+
import chalk21 from "chalk";
|
|
3637
|
+
import ora11 from "ora";
|
|
3638
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
|
|
3639
|
+
import { join as join16, basename as basename2 } from "path";
|
|
3467
3640
|
|
|
3468
3641
|
// src/lib/worktree.ts
|
|
3469
3642
|
import { execSync as execSync6 } from "child_process";
|
|
@@ -3516,13 +3689,13 @@ function removeWorktree(repoPath, worktreePath) {
|
|
|
3516
3689
|
}
|
|
3517
3690
|
|
|
3518
3691
|
// src/lib/template.ts
|
|
3519
|
-
import { readFileSync as
|
|
3520
|
-
import { join as
|
|
3692
|
+
import { readFileSync as readFileSync14, existsSync as existsSync15, readdirSync as readdirSync9 } from "fs";
|
|
3693
|
+
import { join as join14 } from "path";
|
|
3521
3694
|
function loadTemplate(templatePath) {
|
|
3522
|
-
if (!
|
|
3695
|
+
if (!existsSync15(templatePath)) {
|
|
3523
3696
|
throw new Error(`Template not found: ${templatePath}`);
|
|
3524
3697
|
}
|
|
3525
|
-
return
|
|
3698
|
+
return readFileSync14(templatePath, "utf8");
|
|
3526
3699
|
}
|
|
3527
3700
|
function substituteVariables(template, variables) {
|
|
3528
3701
|
let result = template;
|
|
@@ -3543,17 +3716,17 @@ function generateClaudeMd(projectPath, variables) {
|
|
|
3543
3716
|
"warnings.md"
|
|
3544
3717
|
];
|
|
3545
3718
|
for (const section of defaultOrder) {
|
|
3546
|
-
const sectionPath =
|
|
3547
|
-
if (
|
|
3719
|
+
const sectionPath = join14(CLAUDE_MD_TEMPLATES, section);
|
|
3720
|
+
if (existsSync15(sectionPath)) {
|
|
3548
3721
|
const content = loadTemplate(sectionPath);
|
|
3549
3722
|
sections.push(substituteVariables(content, variables));
|
|
3550
3723
|
}
|
|
3551
3724
|
}
|
|
3552
|
-
const projectSections =
|
|
3553
|
-
if (
|
|
3725
|
+
const projectSections = join14(projectPath, ".panopticon", "claude-md", "sections");
|
|
3726
|
+
if (existsSync15(projectSections)) {
|
|
3554
3727
|
const projectFiles = readdirSync9(projectSections).filter((f) => f.endsWith(".md")).sort();
|
|
3555
3728
|
for (const file of projectFiles) {
|
|
3556
|
-
const content = loadTemplate(
|
|
3729
|
+
const content = loadTemplate(join14(projectSections, file));
|
|
3557
3730
|
sections.push(substituteVariables(content, variables));
|
|
3558
3731
|
}
|
|
3559
3732
|
}
|
|
@@ -3574,7 +3747,7 @@ This workspace was created by Panopticon. Use \`bd\` commands to track your work
|
|
|
3574
3747
|
|
|
3575
3748
|
// src/lib/skills-merge.ts
|
|
3576
3749
|
import {
|
|
3577
|
-
existsSync as
|
|
3750
|
+
existsSync as existsSync16,
|
|
3578
3751
|
readdirSync as readdirSync10,
|
|
3579
3752
|
lstatSync,
|
|
3580
3753
|
readlinkSync,
|
|
@@ -3582,7 +3755,7 @@ import {
|
|
|
3582
3755
|
mkdirSync as mkdirSync10,
|
|
3583
3756
|
appendFileSync as appendFileSync2
|
|
3584
3757
|
} from "fs";
|
|
3585
|
-
import { join as
|
|
3758
|
+
import { join as join15 } from "path";
|
|
3586
3759
|
import { execSync as execSync7 } from "child_process";
|
|
3587
3760
|
function detectContentOrigin(path, repoPath) {
|
|
3588
3761
|
try {
|
|
@@ -3607,21 +3780,21 @@ function detectContentOrigin(path, repoPath) {
|
|
|
3607
3780
|
}
|
|
3608
3781
|
}
|
|
3609
3782
|
function mergeSkillsIntoWorkspace(workspacePath) {
|
|
3610
|
-
const skillsTarget =
|
|
3783
|
+
const skillsTarget = join15(workspacePath, ".claude", "skills");
|
|
3611
3784
|
const added = [];
|
|
3612
3785
|
const skipped = [];
|
|
3613
3786
|
mkdirSync10(skillsTarget, { recursive: true });
|
|
3614
3787
|
const existingSkills = /* @__PURE__ */ new Set();
|
|
3615
|
-
if (
|
|
3788
|
+
if (existsSync16(skillsTarget)) {
|
|
3616
3789
|
for (const item of readdirSync10(skillsTarget)) {
|
|
3617
3790
|
existingSkills.add(item);
|
|
3618
3791
|
}
|
|
3619
3792
|
}
|
|
3620
|
-
if (!
|
|
3793
|
+
if (!existsSync16(SKILLS_DIR)) return { added, skipped };
|
|
3621
3794
|
const panopticonSkills = readdirSync10(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
3622
3795
|
for (const skill of panopticonSkills) {
|
|
3623
|
-
const targetPath =
|
|
3624
|
-
const sourcePath =
|
|
3796
|
+
const targetPath = join15(skillsTarget, skill);
|
|
3797
|
+
const sourcePath = join15(SKILLS_DIR, skill);
|
|
3625
3798
|
if (existingSkills.has(skill)) {
|
|
3626
3799
|
const origin = detectContentOrigin(targetPath, workspacePath);
|
|
3627
3800
|
if (origin === "git-tracked") {
|
|
@@ -3647,7 +3820,7 @@ function mergeSkillsIntoWorkspace(workspacePath) {
|
|
|
3647
3820
|
return { added, skipped };
|
|
3648
3821
|
}
|
|
3649
3822
|
function updateGitignore(skillsDir, skills) {
|
|
3650
|
-
const gitignorePath =
|
|
3823
|
+
const gitignorePath = join15(skillsDir, ".gitignore");
|
|
3651
3824
|
const content = `# Panopticon-managed symlinks (not committed)
|
|
3652
3825
|
${skills.join("\n")}
|
|
3653
3826
|
`;
|
|
@@ -3665,7 +3838,7 @@ function registerWorkspaceCommands(program2) {
|
|
|
3665
3838
|
workspace.command("destroy <issueId>").description("Destroy workspace").option("--force", "Force removal even with uncommitted changes").option("--project <path>", "Explicit project path (overrides registry)").action(destroyCommand);
|
|
3666
3839
|
}
|
|
3667
3840
|
async function createCommand(issueId, options) {
|
|
3668
|
-
const spinner =
|
|
3841
|
+
const spinner = ora11("Creating workspace...").start();
|
|
3669
3842
|
try {
|
|
3670
3843
|
const normalizedId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
3671
3844
|
const branchName = `feature/${normalizedId}`;
|
|
@@ -3689,29 +3862,29 @@ async function createCommand(issueId, options) {
|
|
|
3689
3862
|
projectRoot = process.cwd();
|
|
3690
3863
|
}
|
|
3691
3864
|
}
|
|
3692
|
-
const workspacesDir =
|
|
3693
|
-
const workspacePath =
|
|
3865
|
+
const workspacesDir = join16(projectRoot, "workspaces");
|
|
3866
|
+
const workspacePath = join16(workspacesDir, folderName);
|
|
3694
3867
|
if (options.dryRun) {
|
|
3695
3868
|
spinner.info("Dry run mode");
|
|
3696
3869
|
console.log("");
|
|
3697
|
-
console.log(
|
|
3870
|
+
console.log(chalk21.bold("Would create:"));
|
|
3698
3871
|
if (projectName) {
|
|
3699
|
-
console.log(` Project: ${
|
|
3872
|
+
console.log(` Project: ${chalk21.green(projectName)}`);
|
|
3700
3873
|
}
|
|
3701
|
-
console.log(` Root: ${
|
|
3702
|
-
console.log(` Workspace: ${
|
|
3703
|
-
console.log(` Branch: ${
|
|
3704
|
-
console.log(` CLAUDE.md: ${
|
|
3874
|
+
console.log(` Root: ${chalk21.dim(projectRoot)}`);
|
|
3875
|
+
console.log(` Workspace: ${chalk21.cyan(workspacePath)}`);
|
|
3876
|
+
console.log(` Branch: ${chalk21.cyan(branchName)}`);
|
|
3877
|
+
console.log(` CLAUDE.md: ${chalk21.dim(join16(workspacePath, "CLAUDE.md"))}`);
|
|
3705
3878
|
if (options.skills !== false) {
|
|
3706
|
-
console.log(` Skills: ${
|
|
3879
|
+
console.log(` Skills: ${chalk21.dim(join16(workspacePath, ".claude", "skills"))}`);
|
|
3707
3880
|
}
|
|
3708
3881
|
return;
|
|
3709
3882
|
}
|
|
3710
|
-
if (
|
|
3883
|
+
if (existsSync17(workspacePath)) {
|
|
3711
3884
|
spinner.fail(`Workspace already exists: ${workspacePath}`);
|
|
3712
3885
|
process.exit(1);
|
|
3713
3886
|
}
|
|
3714
|
-
if (!
|
|
3887
|
+
if (!existsSync17(join16(projectRoot, ".git"))) {
|
|
3715
3888
|
spinner.fail("Not a git repository. Run this from the project root.");
|
|
3716
3889
|
process.exit(1);
|
|
3717
3890
|
}
|
|
@@ -3728,31 +3901,31 @@ async function createCommand(issueId, options) {
|
|
|
3728
3901
|
PROJECT_NAME: projectName
|
|
3729
3902
|
};
|
|
3730
3903
|
const claudeMd = generateClaudeMd(projectRoot, variables);
|
|
3731
|
-
writeFileSync10(
|
|
3904
|
+
writeFileSync10(join16(workspacePath, "CLAUDE.md"), claudeMd);
|
|
3732
3905
|
let skillsResult = { added: [], skipped: [] };
|
|
3733
3906
|
if (options.skills !== false) {
|
|
3734
3907
|
spinner.text = "Merging skills...";
|
|
3735
|
-
mkdirSync11(
|
|
3908
|
+
mkdirSync11(join16(workspacePath, ".claude", "skills"), { recursive: true });
|
|
3736
3909
|
skillsResult = mergeSkillsIntoWorkspace(workspacePath);
|
|
3737
3910
|
}
|
|
3738
3911
|
spinner.succeed("Workspace created!");
|
|
3739
3912
|
console.log("");
|
|
3740
|
-
console.log(
|
|
3913
|
+
console.log(chalk21.bold("Workspace Details:"));
|
|
3741
3914
|
if (projectName) {
|
|
3742
|
-
console.log(` Project: ${
|
|
3915
|
+
console.log(` Project: ${chalk21.green(projectName)}`);
|
|
3743
3916
|
}
|
|
3744
|
-
console.log(` Path: ${
|
|
3745
|
-
console.log(` Branch: ${
|
|
3917
|
+
console.log(` Path: ${chalk21.cyan(workspacePath)}`);
|
|
3918
|
+
console.log(` Branch: ${chalk21.dim(branchName)}`);
|
|
3746
3919
|
console.log("");
|
|
3747
3920
|
if (options.skills !== false) {
|
|
3748
|
-
console.log(
|
|
3921
|
+
console.log(chalk21.bold("Skills:"));
|
|
3749
3922
|
console.log(` Added: ${skillsResult.added.length} Panopticon skills`);
|
|
3750
3923
|
if (skillsResult.skipped.length > 0) {
|
|
3751
|
-
console.log(` Skipped: ${
|
|
3924
|
+
console.log(` Skipped: ${chalk21.dim(skillsResult.skipped.join(", "))}`);
|
|
3752
3925
|
}
|
|
3753
3926
|
console.log("");
|
|
3754
3927
|
}
|
|
3755
|
-
console.log(
|
|
3928
|
+
console.log(chalk21.dim(`Next: cd ${workspacePath}`));
|
|
3756
3929
|
} catch (error) {
|
|
3757
3930
|
spinner.fail(error.message);
|
|
3758
3931
|
process.exit(1);
|
|
@@ -3764,7 +3937,7 @@ async function listCommand2(options) {
|
|
|
3764
3937
|
if (projects.length > 0 && options.all) {
|
|
3765
3938
|
const allWorkspaces = [];
|
|
3766
3939
|
for (const { key, config } of projects) {
|
|
3767
|
-
if (!
|
|
3940
|
+
if (!existsSync17(join16(config.path, ".git"))) {
|
|
3768
3941
|
continue;
|
|
3769
3942
|
}
|
|
3770
3943
|
const worktrees2 = listWorktrees(config.path);
|
|
@@ -3784,29 +3957,29 @@ async function listCommand2(options) {
|
|
|
3784
3957
|
return;
|
|
3785
3958
|
}
|
|
3786
3959
|
if (allWorkspaces.length === 0) {
|
|
3787
|
-
console.log(
|
|
3788
|
-
console.log(
|
|
3960
|
+
console.log(chalk21.dim("No workspaces found in any registered project."));
|
|
3961
|
+
console.log(chalk21.dim("Create one with: pan workspace create <issue-id>"));
|
|
3789
3962
|
return;
|
|
3790
3963
|
}
|
|
3791
3964
|
for (const proj of allWorkspaces) {
|
|
3792
|
-
console.log(
|
|
3965
|
+
console.log(chalk21.bold(`
|
|
3793
3966
|
${proj.projectName}
|
|
3794
3967
|
`));
|
|
3795
3968
|
for (const ws of proj.workspaces) {
|
|
3796
3969
|
const name = basename2(ws.path);
|
|
3797
|
-
const status = ws.prunable ?
|
|
3798
|
-
console.log(` ${
|
|
3799
|
-
console.log(` Branch: ${ws.branch ||
|
|
3800
|
-
console.log(` Path: ${
|
|
3970
|
+
const status = ws.prunable ? chalk21.yellow(" (prunable)") : "";
|
|
3971
|
+
console.log(` ${chalk21.cyan(name)}${status}`);
|
|
3972
|
+
console.log(` Branch: ${ws.branch || chalk21.dim("(detached)")}`);
|
|
3973
|
+
console.log(` Path: ${chalk21.dim(ws.path)}`);
|
|
3801
3974
|
}
|
|
3802
3975
|
}
|
|
3803
3976
|
return;
|
|
3804
3977
|
}
|
|
3805
3978
|
const projectRoot = process.cwd();
|
|
3806
|
-
if (!
|
|
3807
|
-
console.error(
|
|
3979
|
+
if (!existsSync17(join16(projectRoot, ".git"))) {
|
|
3980
|
+
console.error(chalk21.red("Not a git repository."));
|
|
3808
3981
|
if (projects.length > 0) {
|
|
3809
|
-
console.log(
|
|
3982
|
+
console.log(chalk21.dim("Tip: Use --all to list workspaces across all registered projects."));
|
|
3810
3983
|
}
|
|
3811
3984
|
process.exit(1);
|
|
3812
3985
|
}
|
|
@@ -3819,25 +3992,25 @@ ${proj.projectName}
|
|
|
3819
3992
|
return;
|
|
3820
3993
|
}
|
|
3821
3994
|
if (workspaces.length === 0) {
|
|
3822
|
-
console.log(
|
|
3823
|
-
console.log(
|
|
3995
|
+
console.log(chalk21.dim("No workspaces found."));
|
|
3996
|
+
console.log(chalk21.dim("Create one with: pan workspace create <issue-id>"));
|
|
3824
3997
|
if (projects.length > 0) {
|
|
3825
|
-
console.log(
|
|
3998
|
+
console.log(chalk21.dim("Tip: Use --all to list workspaces across all registered projects."));
|
|
3826
3999
|
}
|
|
3827
4000
|
return;
|
|
3828
4001
|
}
|
|
3829
|
-
console.log(
|
|
4002
|
+
console.log(chalk21.bold("\nWorkspaces\n"));
|
|
3830
4003
|
for (const ws of workspaces) {
|
|
3831
4004
|
const name = basename2(ws.path);
|
|
3832
|
-
const status = ws.prunable ?
|
|
3833
|
-
console.log(`${
|
|
3834
|
-
console.log(` Branch: ${ws.branch ||
|
|
3835
|
-
console.log(` Path: ${
|
|
4005
|
+
const status = ws.prunable ? chalk21.yellow(" (prunable)") : "";
|
|
4006
|
+
console.log(`${chalk21.cyan(name)}${status}`);
|
|
4007
|
+
console.log(` Branch: ${ws.branch || chalk21.dim("(detached)")}`);
|
|
4008
|
+
console.log(` Path: ${chalk21.dim(ws.path)}`);
|
|
3836
4009
|
console.log("");
|
|
3837
4010
|
}
|
|
3838
4011
|
}
|
|
3839
4012
|
async function destroyCommand(issueId, options) {
|
|
3840
|
-
const spinner =
|
|
4013
|
+
const spinner = ora11("Destroying workspace...").start();
|
|
3841
4014
|
try {
|
|
3842
4015
|
const normalizedId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
3843
4016
|
const folderName = `feature-${normalizedId}`;
|
|
@@ -3852,36 +4025,36 @@ async function destroyCommand(issueId, options) {
|
|
|
3852
4025
|
projectRoot = process.cwd();
|
|
3853
4026
|
}
|
|
3854
4027
|
}
|
|
3855
|
-
const workspacePath =
|
|
3856
|
-
if (!
|
|
3857
|
-
const cwdPath =
|
|
3858
|
-
if (projectRoot !== process.cwd() &&
|
|
4028
|
+
const workspacePath = join16(projectRoot, "workspaces", folderName);
|
|
4029
|
+
if (!existsSync17(workspacePath)) {
|
|
4030
|
+
const cwdPath = join16(process.cwd(), "workspaces", folderName);
|
|
4031
|
+
if (projectRoot !== process.cwd() && existsSync17(cwdPath)) {
|
|
3859
4032
|
projectRoot = process.cwd();
|
|
3860
4033
|
} else {
|
|
3861
4034
|
spinner.fail(`Workspace not found: ${workspacePath}`);
|
|
3862
4035
|
process.exit(1);
|
|
3863
4036
|
}
|
|
3864
4037
|
}
|
|
3865
|
-
const finalWorkspacePath =
|
|
4038
|
+
const finalWorkspacePath = join16(projectRoot, "workspaces", folderName);
|
|
3866
4039
|
spinner.text = "Removing git worktree...";
|
|
3867
4040
|
removeWorktree(projectRoot, finalWorkspacePath);
|
|
3868
4041
|
spinner.succeed(`Workspace destroyed: ${folderName}`);
|
|
3869
4042
|
} catch (error) {
|
|
3870
4043
|
spinner.fail(error.message);
|
|
3871
4044
|
if (!options.force) {
|
|
3872
|
-
console.log(
|
|
4045
|
+
console.log(chalk21.dim("Tip: Use --force to remove even with uncommitted changes"));
|
|
3873
4046
|
}
|
|
3874
4047
|
process.exit(1);
|
|
3875
4048
|
}
|
|
3876
4049
|
}
|
|
3877
4050
|
|
|
3878
4051
|
// src/cli/commands/install.ts
|
|
3879
|
-
import
|
|
3880
|
-
import
|
|
4052
|
+
import chalk22 from "chalk";
|
|
4053
|
+
import ora12 from "ora";
|
|
3881
4054
|
import { execSync as execSync8 } from "child_process";
|
|
3882
|
-
import { existsSync as
|
|
3883
|
-
import { join as
|
|
3884
|
-
import { homedir as
|
|
4055
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync12, writeFileSync as writeFileSync11, readFileSync as readFileSync15, copyFileSync, readdirSync as readdirSync11, statSync } from "fs";
|
|
4056
|
+
import { join as join17 } from "path";
|
|
4057
|
+
import { homedir as homedir6, platform } from "os";
|
|
3885
4058
|
function registerInstallCommand(program2) {
|
|
3886
4059
|
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").action(installCommand);
|
|
3887
4060
|
}
|
|
@@ -3889,7 +4062,7 @@ function detectPlatform() {
|
|
|
3889
4062
|
const os = platform();
|
|
3890
4063
|
if (os === "linux") {
|
|
3891
4064
|
try {
|
|
3892
|
-
const release =
|
|
4065
|
+
const release = readFileSync15("/proc/version", "utf8").toLowerCase();
|
|
3893
4066
|
if (release.includes("microsoft") || release.includes("wsl")) {
|
|
3894
4067
|
return "wsl";
|
|
3895
4068
|
}
|
|
@@ -3900,14 +4073,14 @@ function detectPlatform() {
|
|
|
3900
4073
|
return os;
|
|
3901
4074
|
}
|
|
3902
4075
|
function copyDirectoryRecursive(source, dest) {
|
|
3903
|
-
if (!
|
|
4076
|
+
if (!existsSync18(source)) {
|
|
3904
4077
|
throw new Error(`Source directory not found: ${source}`);
|
|
3905
4078
|
}
|
|
3906
4079
|
mkdirSync12(dest, { recursive: true });
|
|
3907
4080
|
const entries = readdirSync11(source);
|
|
3908
4081
|
for (const entry of entries) {
|
|
3909
|
-
const sourcePath =
|
|
3910
|
-
const destPath =
|
|
4082
|
+
const sourcePath = join17(source, entry);
|
|
4083
|
+
const destPath = join17(dest, entry);
|
|
3911
4084
|
const stat = statSync(sourcePath);
|
|
3912
4085
|
if (stat.isDirectory()) {
|
|
3913
4086
|
copyDirectoryRecursive(sourcePath, destPath);
|
|
@@ -3977,7 +4150,7 @@ function checkPrerequisites() {
|
|
|
3977
4150
|
message: hasBeads ? "installed" : "not found",
|
|
3978
4151
|
fix: "cargo install beads-cli"
|
|
3979
4152
|
});
|
|
3980
|
-
const hasTtyd = checkCommand("ttyd") ||
|
|
4153
|
+
const hasTtyd = checkCommand("ttyd") || existsSync18(join17(homedir6(), "bin", "ttyd"));
|
|
3981
4154
|
results.push({
|
|
3982
4155
|
name: "ttyd",
|
|
3983
4156
|
passed: hasTtyd,
|
|
@@ -3990,21 +4163,21 @@ function checkPrerequisites() {
|
|
|
3990
4163
|
};
|
|
3991
4164
|
}
|
|
3992
4165
|
function printPrereqStatus(prereqs) {
|
|
3993
|
-
console.log(
|
|
4166
|
+
console.log(chalk22.bold("Prerequisites:\n"));
|
|
3994
4167
|
for (const result of prereqs.results) {
|
|
3995
|
-
const icon = result.passed ?
|
|
3996
|
-
const msg = result.passed ?
|
|
4168
|
+
const icon = result.passed ? chalk22.green("\u2713") : chalk22.red("\u2717");
|
|
4169
|
+
const msg = result.passed ? chalk22.dim(result.message) : chalk22.yellow(result.message);
|
|
3997
4170
|
console.log(` ${icon} ${result.name}: ${msg}`);
|
|
3998
4171
|
if (!result.passed && result.fix) {
|
|
3999
|
-
console.log(` ${
|
|
4172
|
+
console.log(` ${chalk22.dim("\u2192 " + result.fix)}`);
|
|
4000
4173
|
}
|
|
4001
4174
|
}
|
|
4002
4175
|
console.log("");
|
|
4003
4176
|
}
|
|
4004
4177
|
async function installCommand(options) {
|
|
4005
|
-
console.log(
|
|
4178
|
+
console.log(chalk22.bold("\nPanopticon Installation\n"));
|
|
4006
4179
|
const plat = detectPlatform();
|
|
4007
|
-
console.log(`Platform: ${
|
|
4180
|
+
console.log(`Platform: ${chalk22.cyan(plat)}
|
|
4008
4181
|
`);
|
|
4009
4182
|
const prereqs = checkPrerequisites();
|
|
4010
4183
|
if (options.check) {
|
|
@@ -4013,11 +4186,11 @@ async function installCommand(options) {
|
|
|
4013
4186
|
}
|
|
4014
4187
|
printPrereqStatus(prereqs);
|
|
4015
4188
|
if (!prereqs.allPassed) {
|
|
4016
|
-
console.log(
|
|
4017
|
-
console.log(
|
|
4189
|
+
console.log(chalk22.red("Fix prerequisites above before continuing."));
|
|
4190
|
+
console.log(chalk22.dim("Tip: Run with --minimal to skip optional components"));
|
|
4018
4191
|
process.exit(1);
|
|
4019
4192
|
}
|
|
4020
|
-
const spinner =
|
|
4193
|
+
const spinner = ora12("Initializing Panopticon directories...").start();
|
|
4021
4194
|
for (const dir of INIT_DIRS) {
|
|
4022
4195
|
mkdirSync12(dir, { recursive: true });
|
|
4023
4196
|
}
|
|
@@ -4039,14 +4212,14 @@ async function installCommand(options) {
|
|
|
4039
4212
|
execSync8("mkcert -install", { stdio: "pipe" });
|
|
4040
4213
|
spinner.succeed("mkcert CA installed");
|
|
4041
4214
|
spinner.start("Generating wildcard certificates...");
|
|
4042
|
-
const traefikCertFile =
|
|
4043
|
-
const traefikKeyFile =
|
|
4215
|
+
const traefikCertFile = join17(TRAEFIK_CERTS_DIR, "_wildcard.pan.localhost.pem");
|
|
4216
|
+
const traefikKeyFile = join17(TRAEFIK_CERTS_DIR, "_wildcard.pan.localhost-key.pem");
|
|
4044
4217
|
execSync8(
|
|
4045
4218
|
`mkcert -cert-file "${traefikCertFile}" -key-file "${traefikKeyFile}" "*.pan.localhost" "*.localhost" localhost 127.0.0.1 ::1`,
|
|
4046
4219
|
{ stdio: "pipe" }
|
|
4047
4220
|
);
|
|
4048
|
-
const legacyCertFile =
|
|
4049
|
-
const legacyKeyFile =
|
|
4221
|
+
const legacyCertFile = join17(CERTS_DIR, "localhost.pem");
|
|
4222
|
+
const legacyKeyFile = join17(CERTS_DIR, "localhost-key.pem");
|
|
4050
4223
|
copyFileSync(traefikCertFile, legacyCertFile);
|
|
4051
4224
|
copyFileSync(traefikKeyFile, legacyKeyFile);
|
|
4052
4225
|
spinner.succeed("Wildcard certificates generated (*.pan.localhost, *.localhost)");
|
|
@@ -4057,13 +4230,13 @@ async function installCommand(options) {
|
|
|
4057
4230
|
spinner.info("Skipping mkcert (not installed)");
|
|
4058
4231
|
}
|
|
4059
4232
|
}
|
|
4060
|
-
const hasTtyd = checkCommand("ttyd") ||
|
|
4233
|
+
const hasTtyd = checkCommand("ttyd") || existsSync18(join17(homedir6(), "bin", "ttyd"));
|
|
4061
4234
|
if (!hasTtyd) {
|
|
4062
4235
|
spinner.start("Installing ttyd (web terminal)...");
|
|
4063
4236
|
try {
|
|
4064
|
-
const binDir =
|
|
4237
|
+
const binDir = join17(homedir6(), "bin");
|
|
4065
4238
|
mkdirSync12(binDir, { recursive: true });
|
|
4066
|
-
const ttydPath =
|
|
4239
|
+
const ttydPath = join17(binDir, "ttyd");
|
|
4067
4240
|
const plat2 = detectPlatform();
|
|
4068
4241
|
let downloadUrl = "";
|
|
4069
4242
|
if (plat2 === "darwin") {
|
|
@@ -4094,19 +4267,31 @@ async function installCommand(options) {
|
|
|
4094
4267
|
if (!options.minimal) {
|
|
4095
4268
|
spinner.start("Setting up Traefik configuration...");
|
|
4096
4269
|
try {
|
|
4097
|
-
if (!
|
|
4270
|
+
if (!existsSync18(join17(TRAEFIK_DIR, "docker-compose.yml"))) {
|
|
4098
4271
|
copyDirectoryRecursive(SOURCE_TRAEFIK_TEMPLATES, TRAEFIK_DIR);
|
|
4099
4272
|
spinner.succeed("Traefik configuration created from templates");
|
|
4100
4273
|
} else {
|
|
4101
4274
|
spinner.info("Traefik configuration already exists (skipping)");
|
|
4102
4275
|
}
|
|
4276
|
+
const existingCompose = join17(TRAEFIK_DIR, "docker-compose.yml");
|
|
4277
|
+
if (existsSync18(existingCompose)) {
|
|
4278
|
+
const content = readFileSync15(existingCompose, "utf-8");
|
|
4279
|
+
if (content.includes("panopticon:") && !content.includes("external: true")) {
|
|
4280
|
+
const patched = content.replace(
|
|
4281
|
+
/networks:\s*\n\s*panopticon:\s*\n\s*name: panopticon\s*\n\s*driver: bridge/,
|
|
4282
|
+
"networks:\n panopticon:\n name: panopticon\n external: true # Network created by 'pan install'"
|
|
4283
|
+
);
|
|
4284
|
+
writeFileSync11(existingCompose, patched);
|
|
4285
|
+
spinner.info("Migrated Traefik config (added external: true to network)");
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4103
4288
|
} catch (error) {
|
|
4104
4289
|
spinner.fail(`Failed to set up Traefik configuration: ${error}`);
|
|
4105
|
-
console.log(
|
|
4290
|
+
console.log(chalk22.yellow("You can set up Traefik manually later"));
|
|
4106
4291
|
}
|
|
4107
4292
|
}
|
|
4108
|
-
const configFile =
|
|
4109
|
-
if (!
|
|
4293
|
+
const configFile = join17(PANOPTICON_HOME, "config.toml");
|
|
4294
|
+
if (!existsSync18(configFile)) {
|
|
4110
4295
|
spinner.start("Creating default config...");
|
|
4111
4296
|
const config = getDefaultConfig();
|
|
4112
4297
|
if (options.minimal) {
|
|
@@ -4124,30 +4309,30 @@ async function installCommand(options) {
|
|
|
4124
4309
|
spinner.succeed("Config created");
|
|
4125
4310
|
}
|
|
4126
4311
|
console.log("");
|
|
4127
|
-
console.log(
|
|
4312
|
+
console.log(chalk22.green.bold("Installation complete!"));
|
|
4128
4313
|
console.log("");
|
|
4129
|
-
console.log(
|
|
4130
|
-
console.log(` 1. Run ${
|
|
4314
|
+
console.log(chalk22.bold("Next steps:"));
|
|
4315
|
+
console.log(` 1. Run ${chalk22.cyan("pan sync")} to sync skills to ~/.claude/`);
|
|
4131
4316
|
if (!options.minimal) {
|
|
4132
|
-
console.log(` 2. Add to ${
|
|
4133
|
-
console.log(` 3. Run ${
|
|
4134
|
-
console.log(` 4. Access dashboard at ${
|
|
4317
|
+
console.log(` 2. Add to ${chalk22.yellow("/etc/hosts")}: ${chalk22.cyan("127.0.0.1 pan.localhost")}`);
|
|
4318
|
+
console.log(` 3. Run ${chalk22.cyan("pan up")} to start Traefik and dashboard`);
|
|
4319
|
+
console.log(` 4. Access dashboard at ${chalk22.cyan("https://pan.localhost")}`);
|
|
4135
4320
|
} else {
|
|
4136
|
-
console.log(` 2. Run ${
|
|
4137
|
-
console.log(` 3. Access dashboard at ${
|
|
4321
|
+
console.log(` 2. Run ${chalk22.cyan("pan up")} to start the dashboard`);
|
|
4322
|
+
console.log(` 3. Access dashboard at ${chalk22.cyan("http://localhost:3001")}`);
|
|
4138
4323
|
}
|
|
4139
|
-
console.log(` ${!options.minimal ? "5" : "4"}. Create a workspace with ${
|
|
4324
|
+
console.log(` ${!options.minimal ? "5" : "4"}. Create a workspace with ${chalk22.cyan("pan workspace create <issue-id>")}`);
|
|
4140
4325
|
console.log("");
|
|
4141
4326
|
}
|
|
4142
4327
|
|
|
4143
4328
|
// src/cli/commands/cloister/status.ts
|
|
4144
|
-
import
|
|
4329
|
+
import chalk23 from "chalk";
|
|
4145
4330
|
|
|
4146
4331
|
// src/lib/cloister/config.ts
|
|
4147
|
-
import { readFileSync as
|
|
4332
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, existsSync as existsSync19, mkdirSync as mkdirSync13 } from "fs";
|
|
4148
4333
|
import { parse, stringify } from "@iarna/toml";
|
|
4149
|
-
import { join as
|
|
4150
|
-
var CLOISTER_CONFIG_FILE =
|
|
4334
|
+
import { join as join18 } from "path";
|
|
4335
|
+
var CLOISTER_CONFIG_FILE = join18(PANOPTICON_HOME, "cloister.toml");
|
|
4151
4336
|
var DEFAULT_CLOISTER_CONFIG = {
|
|
4152
4337
|
startup: {
|
|
4153
4338
|
auto_start: true
|
|
@@ -4250,15 +4435,15 @@ function deepMerge(defaults, overrides) {
|
|
|
4250
4435
|
return result;
|
|
4251
4436
|
}
|
|
4252
4437
|
function loadCloisterConfig() {
|
|
4253
|
-
if (!
|
|
4438
|
+
if (!existsSync19(PANOPTICON_HOME)) {
|
|
4254
4439
|
mkdirSync13(PANOPTICON_HOME, { recursive: true });
|
|
4255
4440
|
}
|
|
4256
|
-
if (!
|
|
4441
|
+
if (!existsSync19(CLOISTER_CONFIG_FILE)) {
|
|
4257
4442
|
saveCloisterConfig(DEFAULT_CLOISTER_CONFIG);
|
|
4258
4443
|
return DEFAULT_CLOISTER_CONFIG;
|
|
4259
4444
|
}
|
|
4260
4445
|
try {
|
|
4261
|
-
const content =
|
|
4446
|
+
const content = readFileSync16(CLOISTER_CONFIG_FILE, "utf-8");
|
|
4262
4447
|
const parsed = parse(content);
|
|
4263
4448
|
return deepMerge(DEFAULT_CLOISTER_CONFIG, parsed);
|
|
4264
4449
|
} catch (error) {
|
|
@@ -4268,7 +4453,7 @@ function loadCloisterConfig() {
|
|
|
4268
4453
|
}
|
|
4269
4454
|
}
|
|
4270
4455
|
function saveCloisterConfig(config) {
|
|
4271
|
-
if (!
|
|
4456
|
+
if (!existsSync19(PANOPTICON_HOME)) {
|
|
4272
4457
|
mkdirSync13(PANOPTICON_HOME, { recursive: true });
|
|
4273
4458
|
}
|
|
4274
4459
|
try {
|
|
@@ -4302,8 +4487,8 @@ function evaluateHealthState(timeSinceActivityMs, thresholds) {
|
|
|
4302
4487
|
}
|
|
4303
4488
|
function getAgentHealth2(agentId, runtime, thresholds) {
|
|
4304
4489
|
const thresholdsMs = thresholds || getHealthThresholdsMs();
|
|
4305
|
-
const
|
|
4306
|
-
if (!
|
|
4490
|
+
const isRunning3 = runtime.isRunning(agentId);
|
|
4491
|
+
if (!isRunning3) {
|
|
4307
4492
|
return {
|
|
4308
4493
|
agentId,
|
|
4309
4494
|
state: "stuck",
|
|
@@ -4383,13 +4568,13 @@ function getHealthLabel(state) {
|
|
|
4383
4568
|
|
|
4384
4569
|
// src/lib/cloister/database.ts
|
|
4385
4570
|
import Database from "better-sqlite3";
|
|
4386
|
-
import { join as
|
|
4387
|
-
import { existsSync as
|
|
4388
|
-
var CLOISTER_DB_PATH =
|
|
4571
|
+
import { join as join19 } from "path";
|
|
4572
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync14 } from "fs";
|
|
4573
|
+
var CLOISTER_DB_PATH = join19(PANOPTICON_HOME, "cloister.db");
|
|
4389
4574
|
var RETENTION_DAYS = 7;
|
|
4390
4575
|
var db = null;
|
|
4391
4576
|
function initHealthDatabase() {
|
|
4392
|
-
if (!
|
|
4577
|
+
if (!existsSync20(PANOPTICON_HOME)) {
|
|
4393
4578
|
mkdirSync14(PANOPTICON_HOME, { recursive: true });
|
|
4394
4579
|
}
|
|
4395
4580
|
db = new Database(CLOISTER_DB_PATH);
|
|
@@ -4455,17 +4640,17 @@ function cleanupOldEvents(database = getHealthDatabase(), retentionDays = RETENT
|
|
|
4455
4640
|
}
|
|
4456
4641
|
|
|
4457
4642
|
// src/lib/cloister/specialists.ts
|
|
4458
|
-
import { readFileSync as
|
|
4459
|
-
import { join as
|
|
4643
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync22, mkdirSync as mkdirSync15, readdirSync as readdirSync13, unlinkSync as unlinkSync2, appendFileSync as appendFileSync3 } from "fs";
|
|
4644
|
+
import { join as join22, basename as basename4 } from "path";
|
|
4460
4645
|
import { execSync as execSync9 } from "child_process";
|
|
4461
4646
|
|
|
4462
4647
|
// src/lib/cost-parsers/jsonl-parser.ts
|
|
4463
|
-
import { existsSync as
|
|
4464
|
-
import { join as
|
|
4465
|
-
import { homedir as
|
|
4648
|
+
import { existsSync as existsSync21, readFileSync as readFileSync17, readdirSync as readdirSync12, statSync as statSync2 } from "fs";
|
|
4649
|
+
import { join as join21, basename as basename3 } from "path";
|
|
4650
|
+
import { homedir as homedir7 } from "os";
|
|
4466
4651
|
|
|
4467
4652
|
// src/lib/cost.ts
|
|
4468
|
-
import { join as
|
|
4653
|
+
import { join as join20 } from "path";
|
|
4469
4654
|
var DEFAULT_PRICING = [
|
|
4470
4655
|
// Anthropic
|
|
4471
4656
|
{ provider: "anthropic", model: "claude-opus-4", inputPer1k: 0.015, outputPer1k: 0.075, cacheReadPer1k: 175e-5, cacheWritePer1k: 0.01875, currency: "USD" },
|
|
@@ -4502,15 +4687,15 @@ function getPricing(provider, model) {
|
|
|
4502
4687
|
}
|
|
4503
4688
|
return pricing || null;
|
|
4504
4689
|
}
|
|
4505
|
-
var BUDGETS_FILE =
|
|
4690
|
+
var BUDGETS_FILE = join20(COSTS_DIR, "budgets.json");
|
|
4506
4691
|
|
|
4507
4692
|
// src/lib/cost-parsers/jsonl-parser.ts
|
|
4508
|
-
var CLAUDE_PROJECTS_DIR =
|
|
4693
|
+
var CLAUDE_PROJECTS_DIR = join21(homedir7(), ".claude", "projects");
|
|
4509
4694
|
function getProjectDirs() {
|
|
4510
|
-
if (!
|
|
4695
|
+
if (!existsSync21(CLAUDE_PROJECTS_DIR)) {
|
|
4511
4696
|
return [];
|
|
4512
4697
|
}
|
|
4513
|
-
return readdirSync12(CLAUDE_PROJECTS_DIR).map((name) =>
|
|
4698
|
+
return readdirSync12(CLAUDE_PROJECTS_DIR).map((name) => join21(CLAUDE_PROJECTS_DIR, name)).filter((path) => {
|
|
4514
4699
|
try {
|
|
4515
4700
|
return statSync2(path).isDirectory();
|
|
4516
4701
|
} catch {
|
|
@@ -4519,10 +4704,10 @@ function getProjectDirs() {
|
|
|
4519
4704
|
});
|
|
4520
4705
|
}
|
|
4521
4706
|
function getSessionFiles(projectDir) {
|
|
4522
|
-
if (!
|
|
4707
|
+
if (!existsSync21(projectDir)) {
|
|
4523
4708
|
return [];
|
|
4524
4709
|
}
|
|
4525
|
-
return readdirSync12(projectDir).filter((name) => name.endsWith(".jsonl")).map((name) =>
|
|
4710
|
+
return readdirSync12(projectDir).filter((name) => name.endsWith(".jsonl")).map((name) => join21(projectDir, name)).sort((a, b) => {
|
|
4526
4711
|
try {
|
|
4527
4712
|
return statSync2(b).mtime.getTime() - statSync2(a).mtime.getTime();
|
|
4528
4713
|
} catch {
|
|
@@ -4566,10 +4751,10 @@ function normalizeModelName(model) {
|
|
|
4566
4751
|
return { provider: "anthropic", model: "claude-sonnet-4" };
|
|
4567
4752
|
}
|
|
4568
4753
|
function parseClaudeSession(sessionFile) {
|
|
4569
|
-
if (!
|
|
4754
|
+
if (!existsSync21(sessionFile)) {
|
|
4570
4755
|
return null;
|
|
4571
4756
|
}
|
|
4572
|
-
const content =
|
|
4757
|
+
const content = readFileSync17(sessionFile, "utf-8");
|
|
4573
4758
|
const lines = content.split("\n").filter((line) => line.trim());
|
|
4574
4759
|
let sessionId = "";
|
|
4575
4760
|
let startTime = "";
|
|
@@ -4636,8 +4821,8 @@ function parseClaudeSession(sessionFile) {
|
|
|
4636
4821
|
}
|
|
4637
4822
|
|
|
4638
4823
|
// src/lib/cloister/specialists.ts
|
|
4639
|
-
var SPECIALISTS_DIR =
|
|
4640
|
-
var REGISTRY_FILE =
|
|
4824
|
+
var SPECIALISTS_DIR = join22(PANOPTICON_HOME, "specialists");
|
|
4825
|
+
var REGISTRY_FILE = join22(SPECIALISTS_DIR, "registry.json");
|
|
4641
4826
|
var DEFAULT_SPECIALISTS = [
|
|
4642
4827
|
{
|
|
4643
4828
|
name: "merge-agent",
|
|
@@ -4662,10 +4847,10 @@ var DEFAULT_SPECIALISTS = [
|
|
|
4662
4847
|
}
|
|
4663
4848
|
];
|
|
4664
4849
|
function initSpecialistsDirectory() {
|
|
4665
|
-
if (!
|
|
4850
|
+
if (!existsSync22(SPECIALISTS_DIR)) {
|
|
4666
4851
|
mkdirSync15(SPECIALISTS_DIR, { recursive: true });
|
|
4667
4852
|
}
|
|
4668
|
-
if (!
|
|
4853
|
+
if (!existsSync22(REGISTRY_FILE)) {
|
|
4669
4854
|
const registry = {
|
|
4670
4855
|
version: "1.0",
|
|
4671
4856
|
specialists: DEFAULT_SPECIALISTS,
|
|
@@ -4677,7 +4862,7 @@ function initSpecialistsDirectory() {
|
|
|
4677
4862
|
function loadRegistry() {
|
|
4678
4863
|
initSpecialistsDirectory();
|
|
4679
4864
|
try {
|
|
4680
|
-
const content =
|
|
4865
|
+
const content = readFileSync18(REGISTRY_FILE, "utf-8");
|
|
4681
4866
|
return JSON.parse(content);
|
|
4682
4867
|
} catch (error) {
|
|
4683
4868
|
console.error("Failed to load specialist registry:", error);
|
|
@@ -4689,7 +4874,7 @@ function loadRegistry() {
|
|
|
4689
4874
|
}
|
|
4690
4875
|
}
|
|
4691
4876
|
function saveRegistry(registry) {
|
|
4692
|
-
if (!
|
|
4877
|
+
if (!existsSync22(SPECIALISTS_DIR)) {
|
|
4693
4878
|
mkdirSync15(SPECIALISTS_DIR, { recursive: true });
|
|
4694
4879
|
}
|
|
4695
4880
|
registry.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -4702,15 +4887,15 @@ function saveRegistry(registry) {
|
|
|
4702
4887
|
}
|
|
4703
4888
|
}
|
|
4704
4889
|
function getSessionFilePath(name) {
|
|
4705
|
-
return
|
|
4890
|
+
return join22(SPECIALISTS_DIR, `${name}.session`);
|
|
4706
4891
|
}
|
|
4707
4892
|
function getSessionId(name) {
|
|
4708
4893
|
const sessionFile = getSessionFilePath(name);
|
|
4709
|
-
if (!
|
|
4894
|
+
if (!existsSync22(sessionFile)) {
|
|
4710
4895
|
return null;
|
|
4711
4896
|
}
|
|
4712
4897
|
try {
|
|
4713
|
-
return
|
|
4898
|
+
return readFileSync18(sessionFile, "utf-8").trim();
|
|
4714
4899
|
} catch (error) {
|
|
4715
4900
|
console.error(`Failed to read session file for ${name}:`, error);
|
|
4716
4901
|
return null;
|
|
@@ -4718,7 +4903,7 @@ function getSessionId(name) {
|
|
|
4718
4903
|
}
|
|
4719
4904
|
function clearSessionId(name) {
|
|
4720
4905
|
const sessionFile = getSessionFilePath(name);
|
|
4721
|
-
if (!
|
|
4906
|
+
if (!existsSync22(sessionFile)) {
|
|
4722
4907
|
return false;
|
|
4723
4908
|
}
|
|
4724
4909
|
try {
|
|
@@ -4906,15 +5091,72 @@ async function initializeEnabledSpecialists() {
|
|
|
4906
5091
|
}
|
|
4907
5092
|
return results;
|
|
4908
5093
|
}
|
|
5094
|
+
async function wakeSpecialist(name, taskPrompt, options = {}) {
|
|
5095
|
+
const { waitForReady = true, startIfNotRunning = true } = options;
|
|
5096
|
+
const tmuxSession = getTmuxSessionName(name);
|
|
5097
|
+
const sessionId = getSessionId(name);
|
|
5098
|
+
const wasAlreadyRunning = isRunning(name);
|
|
5099
|
+
if (!wasAlreadyRunning) {
|
|
5100
|
+
if (!startIfNotRunning) {
|
|
5101
|
+
return {
|
|
5102
|
+
success: false,
|
|
5103
|
+
message: `Specialist ${name} is not running`,
|
|
5104
|
+
wasAlreadyRunning: false,
|
|
5105
|
+
error: "not_running"
|
|
5106
|
+
};
|
|
5107
|
+
}
|
|
5108
|
+
const cwd = process.env.HOME || "/home/eltmon";
|
|
5109
|
+
try {
|
|
5110
|
+
const claudeCmd = sessionId ? `claude --resume "${sessionId}" --dangerously-skip-permissions` : `claude --dangerously-skip-permissions`;
|
|
5111
|
+
execSync9(
|
|
5112
|
+
`tmux new-session -d -s "${tmuxSession}" -c "${cwd}" "${claudeCmd}"`,
|
|
5113
|
+
{ encoding: "utf-8" }
|
|
5114
|
+
);
|
|
5115
|
+
if (waitForReady) {
|
|
5116
|
+
await new Promise((resolve2) => setTimeout(resolve2, 3e3));
|
|
5117
|
+
}
|
|
5118
|
+
} catch (error) {
|
|
5119
|
+
return {
|
|
5120
|
+
success: false,
|
|
5121
|
+
message: `Failed to start specialist ${name}: ${error.message}`,
|
|
5122
|
+
wasAlreadyRunning: false,
|
|
5123
|
+
error: error.message
|
|
5124
|
+
};
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
try {
|
|
5128
|
+
const escapedPrompt = taskPrompt.replace(/'/g, "'\\''");
|
|
5129
|
+
execSync9(`tmux send-keys -t "${tmuxSession}" '${escapedPrompt}'`, { encoding: "utf-8" });
|
|
5130
|
+
await new Promise((resolve2) => setTimeout(resolve2, 200));
|
|
5131
|
+
execSync9(`tmux send-keys -t "${tmuxSession}" C-m`, { encoding: "utf-8" });
|
|
5132
|
+
recordWake(name, sessionId || void 0);
|
|
5133
|
+
return {
|
|
5134
|
+
success: true,
|
|
5135
|
+
message: wasAlreadyRunning ? `Sent task to running specialist ${name}` : `Started specialist ${name} and sent task`,
|
|
5136
|
+
tmuxSession,
|
|
5137
|
+
wasAlreadyRunning
|
|
5138
|
+
};
|
|
5139
|
+
} catch (error) {
|
|
5140
|
+
return {
|
|
5141
|
+
success: false,
|
|
5142
|
+
message: `Failed to send task to specialist ${name}: ${error.message}`,
|
|
5143
|
+
tmuxSession,
|
|
5144
|
+
wasAlreadyRunning,
|
|
5145
|
+
error: error.message
|
|
5146
|
+
};
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
4909
5149
|
function checkSpecialistQueue(specialistName) {
|
|
4910
5150
|
return checkHook(specialistName);
|
|
4911
5151
|
}
|
|
5152
|
+
var FEEDBACK_DIR = join22(PANOPTICON_HOME, "specialists", "feedback");
|
|
5153
|
+
var FEEDBACK_LOG = join22(FEEDBACK_DIR, "feedback.jsonl");
|
|
4912
5154
|
|
|
4913
5155
|
// src/lib/runtimes/claude-code.ts
|
|
4914
|
-
import { existsSync as
|
|
4915
|
-
import { join as
|
|
4916
|
-
import { homedir as
|
|
4917
|
-
var CLAUDE_PROJECTS_DIR2 =
|
|
5156
|
+
import { existsSync as existsSync23, readFileSync as readFileSync19, statSync as statSync3, mkdirSync as mkdirSync16, writeFileSync as writeFileSync14 } from "fs";
|
|
5157
|
+
import { join as join23 } from "path";
|
|
5158
|
+
import { homedir as homedir8 } from "os";
|
|
5159
|
+
var CLAUDE_PROJECTS_DIR2 = join23(homedir8(), ".claude", "projects");
|
|
4918
5160
|
var ClaudeCodeRuntime = class {
|
|
4919
5161
|
name = "claude-code";
|
|
4920
5162
|
/**
|
|
@@ -4924,15 +5166,15 @@ var ClaudeCodeRuntime = class {
|
|
|
4924
5166
|
* We need to find the project directory that contains sessions for this workspace.
|
|
4925
5167
|
*/
|
|
4926
5168
|
getProjectDirForWorkspace(workspace) {
|
|
4927
|
-
if (!
|
|
5169
|
+
if (!existsSync23(CLAUDE_PROJECTS_DIR2)) {
|
|
4928
5170
|
return null;
|
|
4929
5171
|
}
|
|
4930
5172
|
const projectDirs = getProjectDirs();
|
|
4931
5173
|
for (const projectDir of projectDirs) {
|
|
4932
|
-
const indexPath =
|
|
4933
|
-
if (
|
|
5174
|
+
const indexPath = join23(projectDir, "sessions-index.json");
|
|
5175
|
+
if (existsSync23(indexPath)) {
|
|
4934
5176
|
try {
|
|
4935
|
-
const indexContent =
|
|
5177
|
+
const indexContent = readFileSync19(indexPath, "utf-8");
|
|
4936
5178
|
if (indexContent.includes(workspace)) {
|
|
4937
5179
|
return projectDir;
|
|
4938
5180
|
}
|
|
@@ -4946,12 +5188,12 @@ var ClaudeCodeRuntime = class {
|
|
|
4946
5188
|
* Get the active session ID for an agent from the sessions index
|
|
4947
5189
|
*/
|
|
4948
5190
|
getActiveSessionId(projectDir) {
|
|
4949
|
-
const indexPath =
|
|
4950
|
-
if (!
|
|
5191
|
+
const indexPath = join23(projectDir, "sessions-index.json");
|
|
5192
|
+
if (!existsSync23(indexPath)) {
|
|
4951
5193
|
return null;
|
|
4952
5194
|
}
|
|
4953
5195
|
try {
|
|
4954
|
-
const indexContent =
|
|
5196
|
+
const indexContent = readFileSync19(indexPath, "utf-8");
|
|
4955
5197
|
const index = JSON.parse(indexContent);
|
|
4956
5198
|
if (index.sessions && Array.isArray(index.sessions)) {
|
|
4957
5199
|
const sessions = index.sessions;
|
|
@@ -4986,8 +5228,8 @@ var ClaudeCodeRuntime = class {
|
|
|
4986
5228
|
}
|
|
4987
5229
|
const sessionId = this.getActiveSessionId(projectDir);
|
|
4988
5230
|
if (sessionId) {
|
|
4989
|
-
const sessionPath =
|
|
4990
|
-
if (
|
|
5231
|
+
const sessionPath = join23(projectDir, `${sessionId}.jsonl`);
|
|
5232
|
+
if (existsSync23(sessionPath)) {
|
|
4991
5233
|
return sessionPath;
|
|
4992
5234
|
}
|
|
4993
5235
|
}
|
|
@@ -5000,7 +5242,7 @@ var ClaudeCodeRuntime = class {
|
|
|
5000
5242
|
*/
|
|
5001
5243
|
getLastActivity(agentId) {
|
|
5002
5244
|
const sessionPath = this.getSessionPath(agentId);
|
|
5003
|
-
if (!sessionPath || !
|
|
5245
|
+
if (!sessionPath || !existsSync23(sessionPath)) {
|
|
5004
5246
|
return null;
|
|
5005
5247
|
}
|
|
5006
5248
|
try {
|
|
@@ -5014,12 +5256,12 @@ var ClaudeCodeRuntime = class {
|
|
|
5014
5256
|
* Read active heartbeat file if it exists
|
|
5015
5257
|
*/
|
|
5016
5258
|
getActiveHeartbeat(agentId) {
|
|
5017
|
-
const heartbeatPath =
|
|
5018
|
-
if (!
|
|
5259
|
+
const heartbeatPath = join23(homedir8(), ".panopticon", "heartbeats", `${agentId}.json`);
|
|
5260
|
+
if (!existsSync23(heartbeatPath)) {
|
|
5019
5261
|
return null;
|
|
5020
5262
|
}
|
|
5021
5263
|
try {
|
|
5022
|
-
const content =
|
|
5264
|
+
const content = readFileSync19(heartbeatPath, "utf-8");
|
|
5023
5265
|
const data = JSON.parse(content);
|
|
5024
5266
|
const timestamp = new Date(data.timestamp);
|
|
5025
5267
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -5123,11 +5365,11 @@ var ClaudeCodeRuntime = class {
|
|
|
5123
5365
|
throw new Error(`Agent ${agentId} is not running`);
|
|
5124
5366
|
}
|
|
5125
5367
|
sendKeys(agentId, message);
|
|
5126
|
-
const mailDir =
|
|
5368
|
+
const mailDir = join23(getAgentDir(agentId), "mail");
|
|
5127
5369
|
mkdirSync16(mailDir, { recursive: true });
|
|
5128
5370
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5129
5371
|
writeFileSync14(
|
|
5130
|
-
|
|
5372
|
+
join23(mailDir, `${timestamp}.md`),
|
|
5131
5373
|
`# Message
|
|
5132
5374
|
|
|
5133
5375
|
${message}
|
|
@@ -5293,8 +5535,8 @@ function getRuntimeForAgent(agentId) {
|
|
|
5293
5535
|
}
|
|
5294
5536
|
|
|
5295
5537
|
// src/lib/cloister/triggers.ts
|
|
5296
|
-
import { existsSync as
|
|
5297
|
-
import { join as
|
|
5538
|
+
import { existsSync as existsSync24 } from "fs";
|
|
5539
|
+
import { join as join24 } from "path";
|
|
5298
5540
|
import { execSync as execSync10 } from "child_process";
|
|
5299
5541
|
function checkStuckEscalation(health, currentModel, config) {
|
|
5300
5542
|
const conf = config || loadCloisterConfig();
|
|
@@ -5384,8 +5626,8 @@ function checkPlanningComplete(agentId, workspace, issueId, config) {
|
|
|
5384
5626
|
}
|
|
5385
5627
|
} catch (error) {
|
|
5386
5628
|
}
|
|
5387
|
-
const prdPath =
|
|
5388
|
-
if (
|
|
5629
|
+
const prdPath = join24(workspace, `docs/prds/active/${issueId.toLowerCase()}-plan.md`);
|
|
5630
|
+
if (existsSync24(prdPath)) {
|
|
5389
5631
|
signals.push("PRD file exists");
|
|
5390
5632
|
signalCount++;
|
|
5391
5633
|
}
|
|
@@ -5528,11 +5770,11 @@ function checkAllTriggers(agentId, workspace, issueId, currentModel, health, con
|
|
|
5528
5770
|
|
|
5529
5771
|
// src/lib/cloister/handoff.ts
|
|
5530
5772
|
import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync17 } from "fs";
|
|
5531
|
-
import { join as
|
|
5773
|
+
import { join as join26 } from "path";
|
|
5532
5774
|
|
|
5533
5775
|
// src/lib/cloister/handoff-context.ts
|
|
5534
|
-
import { existsSync as
|
|
5535
|
-
import { join as
|
|
5776
|
+
import { existsSync as existsSync25, readFileSync as readFileSync20 } from "fs";
|
|
5777
|
+
import { join as join25 } from "path";
|
|
5536
5778
|
import { execSync as execSync11 } from "child_process";
|
|
5537
5779
|
async function captureHandoffContext(agentState, targetModel, reason) {
|
|
5538
5780
|
const context = {
|
|
@@ -5554,13 +5796,13 @@ async function captureHandoffContext(agentState, targetModel, reason) {
|
|
|
5554
5796
|
}
|
|
5555
5797
|
async function captureFiles(context, workspace) {
|
|
5556
5798
|
try {
|
|
5557
|
-
const stateFile =
|
|
5558
|
-
if (
|
|
5559
|
-
context.stateFile =
|
|
5799
|
+
const stateFile = join25(workspace, ".planning/STATE.md");
|
|
5800
|
+
if (existsSync25(stateFile)) {
|
|
5801
|
+
context.stateFile = readFileSync20(stateFile, "utf-8");
|
|
5560
5802
|
}
|
|
5561
|
-
const claudeMd =
|
|
5562
|
-
if (
|
|
5563
|
-
context.claudeMd =
|
|
5803
|
+
const claudeMd = join25(workspace, "CLAUDE.md");
|
|
5804
|
+
if (existsSync25(claudeMd)) {
|
|
5805
|
+
context.claudeMd = readFileSync20(claudeMd, "utf-8");
|
|
5564
5806
|
}
|
|
5565
5807
|
} catch (error) {
|
|
5566
5808
|
console.error("Error capturing files:", error);
|
|
@@ -5749,9 +5991,9 @@ async function performKillAndSpawn(state, options) {
|
|
|
5749
5991
|
const context = await captureHandoffContext(state, options.targetModel, options.reason);
|
|
5750
5992
|
stopAgent(state.id);
|
|
5751
5993
|
const prompt = buildHandoffPrompt(context, options.additionalInstructions);
|
|
5752
|
-
const handoffDir =
|
|
5994
|
+
const handoffDir = join26(getAgentDir(state.id), "handoffs");
|
|
5753
5995
|
mkdirSync17(handoffDir, { recursive: true });
|
|
5754
|
-
const handoffFile =
|
|
5996
|
+
const handoffFile = join26(handoffDir, `handoff-${Date.now()}.md`);
|
|
5755
5997
|
writeFileSync15(handoffFile, prompt);
|
|
5756
5998
|
const newState = spawnAgent({
|
|
5757
5999
|
issueId: state.issueId,
|
|
@@ -5790,8 +6032,25 @@ async function performSpecialistWake(state, options) {
|
|
|
5790
6032
|
error: "Could not determine specialist name from agent ID"
|
|
5791
6033
|
};
|
|
5792
6034
|
}
|
|
5793
|
-
|
|
5794
|
-
|
|
6035
|
+
const sessionId = getSessionId(specialistName);
|
|
6036
|
+
const tmuxSession = getTmuxSessionName(specialistName);
|
|
6037
|
+
console.log(`[handoff] Waking specialist ${specialistName} (session: ${sessionId || "none"})`);
|
|
6038
|
+
const wakeResult = await wakeSpecialist(specialistName, prompt, {
|
|
6039
|
+
waitForReady: true,
|
|
6040
|
+
startIfNotRunning: true
|
|
6041
|
+
});
|
|
6042
|
+
if (!wakeResult.success) {
|
|
6043
|
+
console.error(`[handoff] Failed to wake specialist: ${wakeResult.error}`);
|
|
6044
|
+
console.warn(`[handoff] Falling back to kill-spawn`);
|
|
6045
|
+
return await performKillAndSpawn(state, options);
|
|
6046
|
+
}
|
|
6047
|
+
console.log(`[handoff] Successfully woke specialist ${specialistName}`);
|
|
6048
|
+
return {
|
|
6049
|
+
success: true,
|
|
6050
|
+
method: "specialist-wake",
|
|
6051
|
+
newSessionId: sessionId || void 0,
|
|
6052
|
+
context
|
|
6053
|
+
};
|
|
5795
6054
|
} catch (error) {
|
|
5796
6055
|
return {
|
|
5797
6056
|
success: false,
|
|
@@ -5824,19 +6083,19 @@ function sleep(ms) {
|
|
|
5824
6083
|
}
|
|
5825
6084
|
|
|
5826
6085
|
// src/lib/cloister/handoff-logger.ts
|
|
5827
|
-
import { existsSync as
|
|
5828
|
-
import { join as
|
|
5829
|
-
var HANDOFF_LOG_FILE =
|
|
6086
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync18, appendFileSync as appendFileSync4, readFileSync as readFileSync21, writeFileSync as writeFileSync16 } from "fs";
|
|
6087
|
+
import { join as join27 } from "path";
|
|
6088
|
+
var HANDOFF_LOG_FILE = join27(PANOPTICON_HOME, "logs", "handoffs.jsonl");
|
|
5830
6089
|
function ensureLogDir() {
|
|
5831
|
-
const logDir =
|
|
5832
|
-
if (!
|
|
6090
|
+
const logDir = join27(PANOPTICON_HOME, "logs");
|
|
6091
|
+
if (!existsSync27(logDir)) {
|
|
5833
6092
|
mkdirSync18(logDir, { recursive: true });
|
|
5834
6093
|
}
|
|
5835
6094
|
}
|
|
5836
6095
|
function logHandoffEvent(event) {
|
|
5837
6096
|
ensureLogDir();
|
|
5838
6097
|
const line = JSON.stringify(event) + "\n";
|
|
5839
|
-
|
|
6098
|
+
appendFileSync4(HANDOFF_LOG_FILE, line, "utf-8");
|
|
5840
6099
|
}
|
|
5841
6100
|
function createHandoffEvent(agentId, issueId, context, trigger, success, errorMessage) {
|
|
5842
6101
|
let stuckMinutes;
|
|
@@ -6262,8 +6521,8 @@ async function statusCommand2(options) {
|
|
|
6262
6521
|
console.log(JSON.stringify(status, null, 2));
|
|
6263
6522
|
return;
|
|
6264
6523
|
}
|
|
6265
|
-
console.log(
|
|
6266
|
-
const runningStatus = status.running ?
|
|
6524
|
+
console.log(chalk23.bold("\n\u{1F514} Cloister Agent Watchdog\n"));
|
|
6525
|
+
const runningStatus = status.running ? chalk23.green("Running") : chalk23.red("Stopped");
|
|
6267
6526
|
console.log(`Status: ${runningStatus}`);
|
|
6268
6527
|
if (status.lastCheck) {
|
|
6269
6528
|
const lastCheck = new Date(status.lastCheck);
|
|
@@ -6271,69 +6530,69 @@ async function statusCommand2(options) {
|
|
|
6271
6530
|
console.log(`Last check: ${timeSince}s ago`);
|
|
6272
6531
|
}
|
|
6273
6532
|
console.log("");
|
|
6274
|
-
console.log(
|
|
6275
|
-
console.log(` \u{1F7E2} Active: ${
|
|
6276
|
-
console.log(` \u{1F7E1} Stale: ${
|
|
6277
|
-
console.log(` \u{1F7E0} Warning: ${
|
|
6278
|
-
console.log(` \u{1F534} Stuck: ${
|
|
6533
|
+
console.log(chalk23.bold("Agent Health Summary:"));
|
|
6534
|
+
console.log(` \u{1F7E2} Active: ${chalk23.green(status.summary.active)}`);
|
|
6535
|
+
console.log(` \u{1F7E1} Stale: ${chalk23.yellow(status.summary.stale)}`);
|
|
6536
|
+
console.log(` \u{1F7E0} Warning: ${chalk23.hex("#FFA500")(status.summary.warning)}`);
|
|
6537
|
+
console.log(` \u{1F534} Stuck: ${chalk23.red(status.summary.stuck)}`);
|
|
6279
6538
|
console.log(` Total: ${status.summary.total}`);
|
|
6280
6539
|
console.log("");
|
|
6281
6540
|
if (status.agentsNeedingAttention.length > 0) {
|
|
6282
|
-
console.log(
|
|
6541
|
+
console.log(chalk23.bold("\u26A0\uFE0F Agents Needing Attention:"));
|
|
6283
6542
|
for (const agentId of status.agentsNeedingAttention) {
|
|
6284
6543
|
const health = service.getAgentHealth(agentId);
|
|
6285
6544
|
if (health) {
|
|
6286
6545
|
const emoji = getHealthEmoji(health.state);
|
|
6287
6546
|
const label = getHealthLabel(health.state);
|
|
6288
|
-
const color = health.state === "warning" ?
|
|
6547
|
+
const color = health.state === "warning" ? chalk23.hex("#FFA500") : chalk23.red;
|
|
6289
6548
|
console.log(` ${emoji} ${color(agentId)} - ${label}`);
|
|
6290
6549
|
}
|
|
6291
6550
|
}
|
|
6292
6551
|
console.log("");
|
|
6293
6552
|
}
|
|
6294
|
-
console.log(
|
|
6553
|
+
console.log(chalk23.bold("Configuration:"));
|
|
6295
6554
|
console.log(` Auto-start: ${status.config.startup.auto_start ? "enabled" : "disabled"}`);
|
|
6296
6555
|
console.log(` Thresholds: stale=${status.config.thresholds.stale}m, warning=${status.config.thresholds.warning}m, stuck=${status.config.thresholds.stuck}m`);
|
|
6297
6556
|
console.log(` Auto-actions:`);
|
|
6298
6557
|
console.log(` - Poke on warning: ${status.config.auto_actions.poke_on_warning ? "enabled" : "disabled"}`);
|
|
6299
|
-
console.log(` - Kill on stuck: ${status.config.auto_actions.kill_on_stuck ?
|
|
6558
|
+
console.log(` - Kill on stuck: ${status.config.auto_actions.kill_on_stuck ? chalk23.red("enabled") : "disabled"}`);
|
|
6300
6559
|
console.log("");
|
|
6301
6560
|
}
|
|
6302
6561
|
|
|
6303
6562
|
// src/cli/commands/cloister/start.ts
|
|
6304
|
-
import
|
|
6563
|
+
import chalk24 from "chalk";
|
|
6305
6564
|
async function startCommand() {
|
|
6306
6565
|
const service = getCloisterService();
|
|
6307
6566
|
if (service.isRunning()) {
|
|
6308
|
-
console.log(
|
|
6567
|
+
console.log(chalk24.yellow("\u26A0\uFE0F Cloister is already running"));
|
|
6309
6568
|
return;
|
|
6310
6569
|
}
|
|
6311
6570
|
await service.start();
|
|
6312
|
-
console.log(
|
|
6313
|
-
console.log(
|
|
6571
|
+
console.log(chalk24.green("\u2713 Cloister started"));
|
|
6572
|
+
console.log(chalk24.dim(" Monitoring all running agents..."));
|
|
6314
6573
|
}
|
|
6315
6574
|
|
|
6316
6575
|
// src/cli/commands/cloister/stop.ts
|
|
6317
|
-
import
|
|
6576
|
+
import chalk25 from "chalk";
|
|
6318
6577
|
async function stopCommand(options) {
|
|
6319
6578
|
const service = getCloisterService();
|
|
6320
6579
|
if (!service.isRunning() && !options.emergency) {
|
|
6321
|
-
console.log(
|
|
6580
|
+
console.log(chalk25.yellow("\u26A0\uFE0F Cloister is not running"));
|
|
6322
6581
|
return;
|
|
6323
6582
|
}
|
|
6324
6583
|
if (options.emergency) {
|
|
6325
|
-
console.log(
|
|
6326
|
-
console.log(
|
|
6584
|
+
console.log(chalk25.red.bold("\u{1F6A8} EMERGENCY STOP - Killing all agents"));
|
|
6585
|
+
console.log(chalk25.dim(" This will terminate all running agent sessions"));
|
|
6327
6586
|
const killedAgents = service.emergencyStop();
|
|
6328
6587
|
console.log("");
|
|
6329
|
-
console.log(
|
|
6588
|
+
console.log(chalk25.green(`\u2713 Killed ${killedAgents.length} agent(s):`));
|
|
6330
6589
|
for (const agentId of killedAgents) {
|
|
6331
|
-
console.log(
|
|
6590
|
+
console.log(chalk25.dim(` - ${agentId}`));
|
|
6332
6591
|
}
|
|
6333
6592
|
} else {
|
|
6334
6593
|
service.stop();
|
|
6335
|
-
console.log(
|
|
6336
|
-
console.log(
|
|
6594
|
+
console.log(chalk25.green("\u2713 Cloister stopped"));
|
|
6595
|
+
console.log(chalk25.dim(" Agents are still running"));
|
|
6337
6596
|
}
|
|
6338
6597
|
}
|
|
6339
6598
|
|
|
@@ -6347,11 +6606,11 @@ function registerCloisterCommands(program2) {
|
|
|
6347
6606
|
}
|
|
6348
6607
|
|
|
6349
6608
|
// src/cli/commands/setup/hooks.ts
|
|
6350
|
-
import
|
|
6351
|
-
import { readFileSync as
|
|
6352
|
-
import { join as
|
|
6609
|
+
import chalk26 from "chalk";
|
|
6610
|
+
import { readFileSync as readFileSync22, writeFileSync as writeFileSync17, existsSync as existsSync28, mkdirSync as mkdirSync19, copyFileSync as copyFileSync2, chmodSync as chmodSync2 } from "fs";
|
|
6611
|
+
import { join as join28 } from "path";
|
|
6353
6612
|
import { execSync as execSync12 } from "child_process";
|
|
6354
|
-
import { homedir as
|
|
6613
|
+
import { homedir as homedir9 } from "os";
|
|
6355
6614
|
function checkJqInstalled() {
|
|
6356
6615
|
try {
|
|
6357
6616
|
execSync12("which jq", { stdio: "pipe" });
|
|
@@ -6361,38 +6620,38 @@ function checkJqInstalled() {
|
|
|
6361
6620
|
}
|
|
6362
6621
|
}
|
|
6363
6622
|
function installJq() {
|
|
6364
|
-
console.log(
|
|
6623
|
+
console.log(chalk26.yellow("Installing jq dependency..."));
|
|
6365
6624
|
try {
|
|
6366
6625
|
const platform2 = process.platform;
|
|
6367
6626
|
if (platform2 === "darwin") {
|
|
6368
6627
|
try {
|
|
6369
6628
|
execSync12("brew --version", { stdio: "pipe" });
|
|
6370
6629
|
execSync12("brew install jq", { stdio: "inherit" });
|
|
6371
|
-
console.log(
|
|
6630
|
+
console.log(chalk26.green("\u2713 jq installed via Homebrew"));
|
|
6372
6631
|
return true;
|
|
6373
6632
|
} catch {
|
|
6374
|
-
console.log(
|
|
6633
|
+
console.log(chalk26.yellow("\u26A0 Homebrew not found"));
|
|
6375
6634
|
}
|
|
6376
6635
|
} else if (platform2 === "linux") {
|
|
6377
6636
|
try {
|
|
6378
6637
|
execSync12("apt-get --version", { stdio: "pipe" });
|
|
6379
6638
|
execSync12("sudo apt-get update && sudo apt-get install -y jq", { stdio: "inherit" });
|
|
6380
|
-
console.log(
|
|
6639
|
+
console.log(chalk26.green("\u2713 jq installed via apt"));
|
|
6381
6640
|
return true;
|
|
6382
6641
|
} catch {
|
|
6383
6642
|
try {
|
|
6384
6643
|
execSync12("yum --version", { stdio: "pipe" });
|
|
6385
6644
|
execSync12("sudo yum install -y jq", { stdio: "inherit" });
|
|
6386
|
-
console.log(
|
|
6645
|
+
console.log(chalk26.green("\u2713 jq installed via yum"));
|
|
6387
6646
|
return true;
|
|
6388
6647
|
} catch {
|
|
6389
|
-
console.log(
|
|
6648
|
+
console.log(chalk26.yellow("\u26A0 No supported package manager found (apt/yum)"));
|
|
6390
6649
|
}
|
|
6391
6650
|
}
|
|
6392
6651
|
}
|
|
6393
6652
|
return false;
|
|
6394
6653
|
} catch (error) {
|
|
6395
|
-
console.log(
|
|
6654
|
+
console.log(chalk26.red("\u2717 Failed to install jq automatically"));
|
|
6396
6655
|
return false;
|
|
6397
6656
|
}
|
|
6398
6657
|
}
|
|
@@ -6405,73 +6664,73 @@ function hookAlreadyConfigured(settings, hookPath) {
|
|
|
6405
6664
|
);
|
|
6406
6665
|
}
|
|
6407
6666
|
async function setupHooksCommand() {
|
|
6408
|
-
console.log(
|
|
6667
|
+
console.log(chalk26.bold("Setting up Panopticon heartbeat hooks\n"));
|
|
6409
6668
|
if (!checkJqInstalled()) {
|
|
6410
|
-
console.log(
|
|
6669
|
+
console.log(chalk26.yellow("\u26A0 jq is required for heartbeat hooks"));
|
|
6411
6670
|
const installed = installJq();
|
|
6412
6671
|
if (!installed) {
|
|
6413
|
-
console.log(
|
|
6414
|
-
console.log(
|
|
6415
|
-
console.log(
|
|
6416
|
-
console.log(
|
|
6417
|
-
console.log(
|
|
6672
|
+
console.log(chalk26.red("\n\u2717 Setup failed: jq dependency missing"));
|
|
6673
|
+
console.log(chalk26.dim("\nPlease install jq manually:"));
|
|
6674
|
+
console.log(chalk26.dim(" macOS: brew install jq"));
|
|
6675
|
+
console.log(chalk26.dim(" Ubuntu: sudo apt-get install jq"));
|
|
6676
|
+
console.log(chalk26.dim(" CentOS: sudo yum install jq\n"));
|
|
6418
6677
|
process.exit(1);
|
|
6419
6678
|
}
|
|
6420
6679
|
} else {
|
|
6421
|
-
console.log(
|
|
6680
|
+
console.log(chalk26.green("\u2713 jq is installed"));
|
|
6422
6681
|
}
|
|
6423
|
-
const panopticonHome =
|
|
6424
|
-
const binDir =
|
|
6425
|
-
const heartbeatsDir =
|
|
6426
|
-
if (!
|
|
6682
|
+
const panopticonHome = join28(homedir9(), ".panopticon");
|
|
6683
|
+
const binDir = join28(panopticonHome, "bin");
|
|
6684
|
+
const heartbeatsDir = join28(panopticonHome, "heartbeats");
|
|
6685
|
+
if (!existsSync28(binDir)) {
|
|
6427
6686
|
mkdirSync19(binDir, { recursive: true });
|
|
6428
|
-
console.log(
|
|
6687
|
+
console.log(chalk26.green("\u2713 Created ~/.panopticon/bin/"));
|
|
6429
6688
|
}
|
|
6430
|
-
if (!
|
|
6689
|
+
if (!existsSync28(heartbeatsDir)) {
|
|
6431
6690
|
mkdirSync19(heartbeatsDir, { recursive: true });
|
|
6432
|
-
console.log(
|
|
6691
|
+
console.log(chalk26.green("\u2713 Created ~/.panopticon/heartbeats/"));
|
|
6433
6692
|
}
|
|
6434
|
-
const scriptSource =
|
|
6435
|
-
const scriptDest =
|
|
6693
|
+
const scriptSource = join28(process.cwd(), "scripts", "heartbeat-hook");
|
|
6694
|
+
const scriptDest = join28(binDir, "heartbeat-hook");
|
|
6436
6695
|
let sourcePath = scriptSource;
|
|
6437
|
-
if (!
|
|
6696
|
+
if (!existsSync28(sourcePath)) {
|
|
6438
6697
|
const { fileURLToPath: fileURLToPath3 } = await import("url");
|
|
6439
6698
|
const { dirname: dirname6 } = await import("path");
|
|
6440
6699
|
const __dirname3 = dirname6(fileURLToPath3(import.meta.url));
|
|
6441
|
-
const installedSource =
|
|
6442
|
-
if (
|
|
6700
|
+
const installedSource = join28(__dirname3, "..", "..", "..", "scripts", "heartbeat-hook");
|
|
6701
|
+
if (existsSync28(installedSource)) {
|
|
6443
6702
|
sourcePath = installedSource;
|
|
6444
6703
|
} else {
|
|
6445
|
-
console.log(
|
|
6446
|
-
console.log(
|
|
6447
|
-
console.log(
|
|
6704
|
+
console.log(chalk26.red("\u2717 Could not find heartbeat-hook script"));
|
|
6705
|
+
console.log(chalk26.dim(` Checked: ${scriptSource}`));
|
|
6706
|
+
console.log(chalk26.dim(` Checked: ${installedSource}`));
|
|
6448
6707
|
process.exit(1);
|
|
6449
6708
|
}
|
|
6450
6709
|
}
|
|
6451
6710
|
copyFileSync2(sourcePath, scriptDest);
|
|
6452
6711
|
chmodSync2(scriptDest, 493);
|
|
6453
|
-
console.log(
|
|
6454
|
-
const claudeDir =
|
|
6455
|
-
const settingsPath =
|
|
6712
|
+
console.log(chalk26.green("\u2713 Installed heartbeat-hook script"));
|
|
6713
|
+
const claudeDir = join28(homedir9(), ".claude");
|
|
6714
|
+
const settingsPath = join28(claudeDir, "settings.json");
|
|
6456
6715
|
let settings = {};
|
|
6457
|
-
if (
|
|
6716
|
+
if (existsSync28(settingsPath)) {
|
|
6458
6717
|
try {
|
|
6459
|
-
const settingsContent =
|
|
6718
|
+
const settingsContent = readFileSync22(settingsPath, "utf-8");
|
|
6460
6719
|
settings = JSON.parse(settingsContent);
|
|
6461
|
-
console.log(
|
|
6720
|
+
console.log(chalk26.green("\u2713 Read existing Claude Code settings"));
|
|
6462
6721
|
} catch (error) {
|
|
6463
|
-
console.log(
|
|
6722
|
+
console.log(chalk26.yellow("\u26A0 Could not parse settings.json, creating new file"));
|
|
6464
6723
|
settings = {};
|
|
6465
6724
|
}
|
|
6466
6725
|
} else {
|
|
6467
|
-
console.log(
|
|
6468
|
-
if (!
|
|
6726
|
+
console.log(chalk26.dim("No existing settings.json found, creating new file"));
|
|
6727
|
+
if (!existsSync28(claudeDir)) {
|
|
6469
6728
|
mkdirSync19(claudeDir, { recursive: true });
|
|
6470
6729
|
}
|
|
6471
6730
|
}
|
|
6472
6731
|
if (hookAlreadyConfigured(settings, scriptDest)) {
|
|
6473
|
-
console.log(
|
|
6474
|
-
console.log(
|
|
6732
|
+
console.log(chalk26.cyan("\n\u2713 Panopticon heartbeat hook already configured"));
|
|
6733
|
+
console.log(chalk26.dim(" No changes needed\n"));
|
|
6475
6734
|
return;
|
|
6476
6735
|
}
|
|
6477
6736
|
if (!settings.hooks) {
|
|
@@ -6489,12 +6748,12 @@ async function setupHooksCommand() {
|
|
|
6489
6748
|
}
|
|
6490
6749
|
]
|
|
6491
6750
|
});
|
|
6492
|
-
|
|
6493
|
-
console.log(
|
|
6494
|
-
console.log(
|
|
6495
|
-
console.log(
|
|
6496
|
-
console.log(
|
|
6497
|
-
console.log(
|
|
6751
|
+
writeFileSync17(settingsPath, JSON.stringify(settings, null, 2));
|
|
6752
|
+
console.log(chalk26.green("\u2713 Updated Claude Code settings.json"));
|
|
6753
|
+
console.log(chalk26.green.bold("\n\u2713 Setup complete!\n"));
|
|
6754
|
+
console.log(chalk26.dim("Heartbeat hooks are now active. When you run agents via"));
|
|
6755
|
+
console.log(chalk26.dim("`pan work issue`, they will send real-time activity updates"));
|
|
6756
|
+
console.log(chalk26.dim("to the Panopticon dashboard.\n"));
|
|
6498
6757
|
}
|
|
6499
6758
|
|
|
6500
6759
|
// src/cli/commands/setup/index.ts
|
|
@@ -6504,16 +6763,16 @@ function registerSetupCommands(program2) {
|
|
|
6504
6763
|
}
|
|
6505
6764
|
|
|
6506
6765
|
// src/cli/commands/specialists/list.ts
|
|
6507
|
-
import
|
|
6766
|
+
import chalk27 from "chalk";
|
|
6508
6767
|
function listCommand3(options) {
|
|
6509
6768
|
const specialists = getAllSpecialistStatus();
|
|
6510
6769
|
if (options.json) {
|
|
6511
6770
|
console.log(JSON.stringify(specialists, null, 2));
|
|
6512
6771
|
return;
|
|
6513
6772
|
}
|
|
6514
|
-
console.log(
|
|
6773
|
+
console.log(chalk27.bold("\nSpecialist Agents:\n"));
|
|
6515
6774
|
if (specialists.length === 0) {
|
|
6516
|
-
console.log(
|
|
6775
|
+
console.log(chalk27.dim("No specialists configured."));
|
|
6517
6776
|
console.log("");
|
|
6518
6777
|
return;
|
|
6519
6778
|
}
|
|
@@ -6525,38 +6784,38 @@ function listCommand3(options) {
|
|
|
6525
6784
|
function displaySpecialist(specialist) {
|
|
6526
6785
|
const statusIcon = getStatusIcon(specialist);
|
|
6527
6786
|
const statusColor = getStatusColor(specialist);
|
|
6528
|
-
const enabledBadge = specialist.enabled ?
|
|
6529
|
-
console.log(`${statusIcon} ${
|
|
6530
|
-
console.log(` ${
|
|
6787
|
+
const enabledBadge = specialist.enabled ? chalk27.green("enabled") : chalk27.dim("disabled");
|
|
6788
|
+
console.log(`${statusIcon} ${chalk27.bold(specialist.displayName)} (${enabledBadge})`);
|
|
6789
|
+
console.log(` ${chalk27.dim(specialist.description)}`);
|
|
6531
6790
|
console.log(` Status: ${statusColor(specialist.state)}`);
|
|
6532
6791
|
if (specialist.isRunning) {
|
|
6533
|
-
console.log(` Running: ${
|
|
6792
|
+
console.log(` Running: ${chalk27.cyan(specialist.tmuxSession)}`);
|
|
6534
6793
|
}
|
|
6535
6794
|
if (specialist.sessionId) {
|
|
6536
6795
|
const shortId = specialist.sessionId.substring(0, 8);
|
|
6537
|
-
console.log(` Session: ${
|
|
6796
|
+
console.log(` Session: ${chalk27.dim(shortId + "...")}`);
|
|
6538
6797
|
}
|
|
6539
6798
|
if (specialist.contextTokens) {
|
|
6540
6799
|
const tokensFormatted = specialist.contextTokens.toLocaleString();
|
|
6541
|
-
console.log(` Context: ${
|
|
6800
|
+
console.log(` Context: ${chalk27.dim(tokensFormatted + " tokens")}`);
|
|
6542
6801
|
}
|
|
6543
6802
|
if (specialist.lastWake) {
|
|
6544
6803
|
const lastWake = new Date(specialist.lastWake);
|
|
6545
6804
|
const relative = getRelativeTime(lastWake);
|
|
6546
|
-
console.log(` Last wake: ${
|
|
6805
|
+
console.log(` Last wake: ${chalk27.dim(relative)}`);
|
|
6547
6806
|
}
|
|
6548
6807
|
console.log("");
|
|
6549
6808
|
}
|
|
6550
6809
|
function getStatusIcon(specialist) {
|
|
6551
|
-
if (!specialist.enabled) return
|
|
6552
|
-
if (specialist.isRunning) return
|
|
6553
|
-
if (specialist.state === "sleeping") return
|
|
6554
|
-
return
|
|
6810
|
+
if (!specialist.enabled) return chalk27.dim("\u25CB");
|
|
6811
|
+
if (specialist.isRunning) return chalk27.green("\u25CF");
|
|
6812
|
+
if (specialist.state === "sleeping") return chalk27.yellow("\u25CF");
|
|
6813
|
+
return chalk27.dim("\u25CB");
|
|
6555
6814
|
}
|
|
6556
6815
|
function getStatusColor(specialist) {
|
|
6557
|
-
if (specialist.isRunning) return
|
|
6558
|
-
if (specialist.state === "sleeping") return
|
|
6559
|
-
return
|
|
6816
|
+
if (specialist.isRunning) return chalk27.green;
|
|
6817
|
+
if (specialist.state === "sleeping") return chalk27.yellow;
|
|
6818
|
+
return chalk27.dim;
|
|
6560
6819
|
}
|
|
6561
6820
|
function getRelativeTime(date) {
|
|
6562
6821
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -6572,12 +6831,12 @@ function getRelativeTime(date) {
|
|
|
6572
6831
|
}
|
|
6573
6832
|
|
|
6574
6833
|
// src/cli/commands/specialists/wake.ts
|
|
6575
|
-
import
|
|
6834
|
+
import chalk28 from "chalk";
|
|
6576
6835
|
import { execSync as execSync13 } from "child_process";
|
|
6577
6836
|
function wakeCommand(name, options) {
|
|
6578
6837
|
const validNames = ["merge-agent", "review-agent", "test-agent"];
|
|
6579
6838
|
if (!validNames.includes(name)) {
|
|
6580
|
-
console.log(
|
|
6839
|
+
console.log(chalk28.red(`
|
|
6581
6840
|
Error: Unknown specialist '${name}'`));
|
|
6582
6841
|
console.log(`Valid specialists: ${validNames.join(", ")}
|
|
6583
6842
|
`);
|
|
@@ -6585,29 +6844,29 @@ Error: Unknown specialist '${name}'`));
|
|
|
6585
6844
|
}
|
|
6586
6845
|
const specialistName = name;
|
|
6587
6846
|
const status = getSpecialistStatus(specialistName);
|
|
6588
|
-
console.log(
|
|
6847
|
+
console.log(chalk28.bold(`
|
|
6589
6848
|
Waking ${status.displayName}...
|
|
6590
6849
|
`));
|
|
6591
6850
|
if (status.isRunning) {
|
|
6592
|
-
console.log(
|
|
6851
|
+
console.log(chalk28.yellow(`Specialist is already running in tmux session: ${status.tmuxSession}`));
|
|
6593
6852
|
if (options.task) {
|
|
6594
|
-
console.log(
|
|
6853
|
+
console.log(chalk28.dim("\nSending task message..."));
|
|
6595
6854
|
try {
|
|
6596
6855
|
const escapedTask = options.task.replace(/'/g, "'\\''");
|
|
6597
6856
|
execSync13(`tmux send-keys -t "${status.tmuxSession}" '${escapedTask}' C-m`, { encoding: "utf-8" });
|
|
6598
|
-
console.log(
|
|
6857
|
+
console.log(chalk28.green("\u2713 Task message sent"));
|
|
6599
6858
|
} catch (error) {
|
|
6600
|
-
console.log(
|
|
6859
|
+
console.log(chalk28.red(`Failed to send message: ${error.message}`));
|
|
6601
6860
|
}
|
|
6602
6861
|
} else {
|
|
6603
|
-
console.log(
|
|
6862
|
+
console.log(chalk28.dim("Use --task to send a message to the running specialist"));
|
|
6604
6863
|
}
|
|
6605
6864
|
console.log("");
|
|
6606
6865
|
return;
|
|
6607
6866
|
}
|
|
6608
6867
|
if (!status.enabled) {
|
|
6609
|
-
console.log(
|
|
6610
|
-
console.log(
|
|
6868
|
+
console.log(chalk28.yellow(`Warning: Specialist '${specialistName}' is disabled in registry`));
|
|
6869
|
+
console.log(chalk28.dim("You can still wake it manually, but it won't auto-wake\n"));
|
|
6611
6870
|
}
|
|
6612
6871
|
const sessionId = getSessionId(specialistName);
|
|
6613
6872
|
const tmuxSession = getTmuxSessionName(specialistName);
|
|
@@ -6616,28 +6875,28 @@ Waking ${status.displayName}...
|
|
|
6616
6875
|
let claudeCmd = "claude --dangerously-skip-permissions";
|
|
6617
6876
|
if (sessionId) {
|
|
6618
6877
|
claudeCmd += ` --resume ${sessionId}`;
|
|
6619
|
-
console.log(
|
|
6878
|
+
console.log(chalk28.dim(`Resuming session: ${sessionId.substring(0, 8)}...`));
|
|
6620
6879
|
} else {
|
|
6621
|
-
console.log(
|
|
6880
|
+
console.log(chalk28.dim("Starting fresh session (no previous session found)"));
|
|
6622
6881
|
}
|
|
6623
|
-
console.log(
|
|
6882
|
+
console.log(chalk28.dim(`Creating tmux session: ${tmuxSession}`));
|
|
6624
6883
|
execSync13(
|
|
6625
6884
|
`tmux new-session -d -s "${tmuxSession}" -c "${cwd}" "${claudeCmd}"`,
|
|
6626
6885
|
{ encoding: "utf-8" }
|
|
6627
6886
|
);
|
|
6628
6887
|
execSync13("sleep 2", { encoding: "utf-8" });
|
|
6629
6888
|
if (options.task) {
|
|
6630
|
-
console.log(
|
|
6889
|
+
console.log(chalk28.dim("Sending task message..."));
|
|
6631
6890
|
const escapedTask = options.task.replace(/'/g, "'\\''");
|
|
6632
6891
|
execSync13(`tmux send-keys -t "${tmuxSession}" '${escapedTask}' C-m`, { encoding: "utf-8" });
|
|
6633
6892
|
}
|
|
6634
6893
|
recordWake(specialistName);
|
|
6635
|
-
console.log(
|
|
6636
|
-
console.log(
|
|
6637
|
-
console.log(
|
|
6894
|
+
console.log(chalk28.green(`\u2713 Specialist ${specialistName} woken up successfully`));
|
|
6895
|
+
console.log(chalk28.dim(` Tmux session: ${tmuxSession}`));
|
|
6896
|
+
console.log(chalk28.dim(` Attach with: tmux attach -t ${tmuxSession}`));
|
|
6638
6897
|
console.log("");
|
|
6639
6898
|
} catch (error) {
|
|
6640
|
-
console.log(
|
|
6899
|
+
console.log(chalk28.red(`
|
|
6641
6900
|
Failed to wake specialist: ${error.message}
|
|
6642
6901
|
`));
|
|
6643
6902
|
process.exit(1);
|
|
@@ -6645,11 +6904,11 @@ Failed to wake specialist: ${error.message}
|
|
|
6645
6904
|
}
|
|
6646
6905
|
|
|
6647
6906
|
// src/cli/commands/specialists/queue.ts
|
|
6648
|
-
import
|
|
6907
|
+
import chalk29 from "chalk";
|
|
6649
6908
|
function queueCommand(name, options) {
|
|
6650
6909
|
const validNames = ["merge-agent", "review-agent", "test-agent"];
|
|
6651
6910
|
if (!validNames.includes(name)) {
|
|
6652
|
-
console.log(
|
|
6911
|
+
console.log(chalk29.red(`
|
|
6653
6912
|
Error: Unknown specialist '${name}'`));
|
|
6654
6913
|
console.log(`Valid specialists: ${validNames.join(", ")}
|
|
6655
6914
|
`);
|
|
@@ -6658,7 +6917,7 @@ Error: Unknown specialist '${name}'`));
|
|
|
6658
6917
|
const specialistName = name;
|
|
6659
6918
|
const metadata = getSpecialistMetadata(specialistName);
|
|
6660
6919
|
if (!metadata) {
|
|
6661
|
-
console.log(
|
|
6920
|
+
console.log(chalk29.red(`
|
|
6662
6921
|
Error: Specialist '${specialistName}' not found in registry
|
|
6663
6922
|
`));
|
|
6664
6923
|
process.exit(1);
|
|
@@ -6668,17 +6927,17 @@ Error: Specialist '${specialistName}' not found in registry
|
|
|
6668
6927
|
console.log(JSON.stringify(queueStatus, null, 2));
|
|
6669
6928
|
return;
|
|
6670
6929
|
}
|
|
6671
|
-
console.log(
|
|
6930
|
+
console.log(chalk29.bold(`
|
|
6672
6931
|
${metadata.displayName} Queue:
|
|
6673
6932
|
`));
|
|
6674
6933
|
if (!queueStatus.hasWork) {
|
|
6675
|
-
console.log(
|
|
6934
|
+
console.log(chalk29.dim("Queue is empty - no pending work"));
|
|
6676
6935
|
console.log("");
|
|
6677
6936
|
return;
|
|
6678
6937
|
}
|
|
6679
|
-
console.log(`Total items: ${
|
|
6938
|
+
console.log(`Total items: ${chalk29.bold(queueStatus.items.length.toString())}`);
|
|
6680
6939
|
if (queueStatus.urgentCount > 0) {
|
|
6681
|
-
console.log(`Urgent items: ${
|
|
6940
|
+
console.log(`Urgent items: ${chalk29.red.bold(queueStatus.urgentCount.toString())}`);
|
|
6682
6941
|
}
|
|
6683
6942
|
console.log("");
|
|
6684
6943
|
for (let i = 0; i < queueStatus.items.length; i++) {
|
|
@@ -6686,45 +6945,45 @@ ${metadata.displayName} Queue:
|
|
|
6686
6945
|
const position = i + 1;
|
|
6687
6946
|
const priorityColor = getPriorityColor(item.priority);
|
|
6688
6947
|
console.log(`${position}. ${priorityColor(item.priority.toUpperCase())}`);
|
|
6689
|
-
console.log(` ID: ${
|
|
6690
|
-
console.log(` Source: ${
|
|
6948
|
+
console.log(` ID: ${chalk29.dim(item.id)}`);
|
|
6949
|
+
console.log(` Source: ${chalk29.dim(item.source)}`);
|
|
6691
6950
|
if (item.payload) {
|
|
6692
6951
|
const payload = item.payload;
|
|
6693
6952
|
if (payload.issueId) {
|
|
6694
|
-
console.log(` Issue: ${
|
|
6953
|
+
console.log(` Issue: ${chalk29.cyan(payload.issueId)}`);
|
|
6695
6954
|
}
|
|
6696
6955
|
if (payload.prUrl) {
|
|
6697
|
-
console.log(` PR: ${
|
|
6956
|
+
console.log(` PR: ${chalk29.dim(payload.prUrl)}`);
|
|
6698
6957
|
}
|
|
6699
6958
|
if (payload.workspace) {
|
|
6700
|
-
console.log(` Workspace: ${
|
|
6959
|
+
console.log(` Workspace: ${chalk29.dim(payload.workspace)}`);
|
|
6701
6960
|
}
|
|
6702
6961
|
if (payload.branch) {
|
|
6703
|
-
console.log(` Branch: ${
|
|
6962
|
+
console.log(` Branch: ${chalk29.dim(payload.branch)}`);
|
|
6704
6963
|
}
|
|
6705
6964
|
if (payload.filesChanged && Array.isArray(payload.filesChanged)) {
|
|
6706
6965
|
const fileCount = payload.filesChanged.length;
|
|
6707
|
-
console.log(` Files: ${
|
|
6966
|
+
console.log(` Files: ${chalk29.dim(fileCount + " changed")}`);
|
|
6708
6967
|
}
|
|
6709
6968
|
}
|
|
6710
6969
|
const createdAt = new Date(item.createdAt);
|
|
6711
6970
|
const age = getAge(createdAt);
|
|
6712
|
-
console.log(` Age: ${
|
|
6971
|
+
console.log(` Age: ${chalk29.dim(age)}`);
|
|
6713
6972
|
console.log("");
|
|
6714
6973
|
}
|
|
6715
6974
|
}
|
|
6716
6975
|
function getPriorityColor(priority) {
|
|
6717
6976
|
switch (priority.toLowerCase()) {
|
|
6718
6977
|
case "urgent":
|
|
6719
|
-
return
|
|
6978
|
+
return chalk29.red.bold;
|
|
6720
6979
|
case "high":
|
|
6721
|
-
return
|
|
6980
|
+
return chalk29.yellow;
|
|
6722
6981
|
case "normal":
|
|
6723
|
-
return
|
|
6982
|
+
return chalk29.white;
|
|
6724
6983
|
case "low":
|
|
6725
|
-
return
|
|
6984
|
+
return chalk29.dim;
|
|
6726
6985
|
default:
|
|
6727
|
-
return
|
|
6986
|
+
return chalk29.white;
|
|
6728
6987
|
}
|
|
6729
6988
|
}
|
|
6730
6989
|
function getAge(date) {
|
|
@@ -6741,13 +7000,13 @@ function getAge(date) {
|
|
|
6741
7000
|
}
|
|
6742
7001
|
|
|
6743
7002
|
// src/cli/commands/specialists/reset.ts
|
|
6744
|
-
import
|
|
7003
|
+
import chalk30 from "chalk";
|
|
6745
7004
|
import { execSync as execSync14 } from "child_process";
|
|
6746
7005
|
import * as readline from "readline";
|
|
6747
7006
|
async function resetCommand(name, options) {
|
|
6748
7007
|
const validNames = ["merge-agent", "review-agent", "test-agent"];
|
|
6749
7008
|
if (!validNames.includes(name)) {
|
|
6750
|
-
console.log(
|
|
7009
|
+
console.log(chalk30.red(`
|
|
6751
7010
|
Error: Unknown specialist '${name}'`));
|
|
6752
7011
|
console.log(`Valid specialists: ${validNames.join(", ")}
|
|
6753
7012
|
`);
|
|
@@ -6755,13 +7014,13 @@ Error: Unknown specialist '${name}'`));
|
|
|
6755
7014
|
}
|
|
6756
7015
|
const specialistName = name;
|
|
6757
7016
|
const status = getSpecialistStatus(specialistName);
|
|
6758
|
-
console.log(
|
|
7017
|
+
console.log(chalk30.bold(`
|
|
6759
7018
|
Resetting ${status.displayName}...
|
|
6760
7019
|
`));
|
|
6761
|
-
console.log(
|
|
7020
|
+
console.log(chalk30.dim("Current state:"));
|
|
6762
7021
|
console.log(` Status: ${status.state}`);
|
|
6763
7022
|
if (status.isRunning) {
|
|
6764
|
-
console.log(` Running: ${
|
|
7023
|
+
console.log(` Running: ${chalk30.yellow("yes")} (tmux: ${status.tmuxSession})`);
|
|
6765
7024
|
}
|
|
6766
7025
|
if (status.sessionId) {
|
|
6767
7026
|
console.log(` Session: ${status.sessionId.substring(0, 8)}...`);
|
|
@@ -6775,29 +7034,29 @@ Resetting ${status.displayName}...
|
|
|
6775
7034
|
"This will clear the session and all context. Continue?"
|
|
6776
7035
|
);
|
|
6777
7036
|
if (!confirmed) {
|
|
6778
|
-
console.log(
|
|
7037
|
+
console.log(chalk30.dim("Reset cancelled\n"));
|
|
6779
7038
|
return;
|
|
6780
7039
|
}
|
|
6781
7040
|
}
|
|
6782
7041
|
if (status.isRunning) {
|
|
6783
|
-
console.log(
|
|
7042
|
+
console.log(chalk30.dim("Stopping tmux session..."));
|
|
6784
7043
|
try {
|
|
6785
7044
|
execSync14(`tmux kill-session -t "${status.tmuxSession}"`, { encoding: "utf-8", stdio: "ignore" });
|
|
6786
|
-
console.log(
|
|
7045
|
+
console.log(chalk30.green("\u2713 Tmux session stopped"));
|
|
6787
7046
|
} catch (error) {
|
|
6788
|
-
console.log(
|
|
7047
|
+
console.log(chalk30.yellow("\u26A0 Failed to stop tmux session (may not be running)"));
|
|
6789
7048
|
}
|
|
6790
7049
|
}
|
|
6791
|
-
console.log(
|
|
7050
|
+
console.log(chalk30.dim("Clearing session file..."));
|
|
6792
7051
|
const cleared = clearSessionId(specialistName);
|
|
6793
7052
|
if (cleared) {
|
|
6794
|
-
console.log(
|
|
7053
|
+
console.log(chalk30.green("\u2713 Session file cleared"));
|
|
6795
7054
|
} else {
|
|
6796
|
-
console.log(
|
|
7055
|
+
console.log(chalk30.dim("No session file to clear"));
|
|
6797
7056
|
}
|
|
6798
|
-
console.log(
|
|
7057
|
+
console.log(chalk30.green(`
|
|
6799
7058
|
\u2713 Specialist ${specialistName} has been reset`));
|
|
6800
|
-
console.log(
|
|
7059
|
+
console.log(chalk30.dim("Next wake will start a fresh session\n"));
|
|
6801
7060
|
}
|
|
6802
7061
|
function confirm(question) {
|
|
6803
7062
|
const rl = readline.createInterface({
|
|
@@ -6805,7 +7064,7 @@ function confirm(question) {
|
|
|
6805
7064
|
output: process.stdout
|
|
6806
7065
|
});
|
|
6807
7066
|
return new Promise((resolve2) => {
|
|
6808
|
-
rl.question(
|
|
7067
|
+
rl.question(chalk30.yellow(`${question} [y/N] `), (answer) => {
|
|
6809
7068
|
rl.close();
|
|
6810
7069
|
resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
6811
7070
|
});
|
|
@@ -6822,29 +7081,29 @@ function registerSpecialistsCommands(program2) {
|
|
|
6822
7081
|
}
|
|
6823
7082
|
|
|
6824
7083
|
// src/cli/commands/project.ts
|
|
6825
|
-
import
|
|
6826
|
-
import { existsSync as
|
|
6827
|
-
import { join as
|
|
7084
|
+
import chalk31 from "chalk";
|
|
7085
|
+
import { existsSync as existsSync29, readFileSync as readFileSync23 } from "fs";
|
|
7086
|
+
import { join as join29, resolve } from "path";
|
|
6828
7087
|
async function projectAddCommand(projectPath, options = {}) {
|
|
6829
7088
|
const fullPath = resolve(projectPath);
|
|
6830
|
-
if (!
|
|
6831
|
-
console.log(
|
|
7089
|
+
if (!existsSync29(fullPath)) {
|
|
7090
|
+
console.log(chalk31.red(`Path does not exist: ${fullPath}`));
|
|
6832
7091
|
return;
|
|
6833
7092
|
}
|
|
6834
7093
|
const name = options.name || fullPath.split("/").pop() || "unknown";
|
|
6835
7094
|
const key = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
6836
7095
|
const existing = getProject(key);
|
|
6837
7096
|
if (existing) {
|
|
6838
|
-
console.log(
|
|
6839
|
-
console.log(
|
|
6840
|
-
console.log(
|
|
7097
|
+
console.log(chalk31.yellow(`Project already registered with key: ${key}`));
|
|
7098
|
+
console.log(chalk31.dim(`Existing path: ${existing.path}`));
|
|
7099
|
+
console.log(chalk31.dim(`To update, first run: pan project remove ${key}`));
|
|
6841
7100
|
return;
|
|
6842
7101
|
}
|
|
6843
7102
|
let linearTeam = options.linearTeam;
|
|
6844
7103
|
if (!linearTeam) {
|
|
6845
|
-
const projectToml =
|
|
6846
|
-
if (
|
|
6847
|
-
const content =
|
|
7104
|
+
const projectToml = join29(fullPath, ".panopticon", "project.toml");
|
|
7105
|
+
if (existsSync29(projectToml)) {
|
|
7106
|
+
const content = readFileSync23(projectToml, "utf-8");
|
|
6848
7107
|
const match = content.match(/team\s*=\s*"([^"]+)"/);
|
|
6849
7108
|
if (match) linearTeam = match[1];
|
|
6850
7109
|
}
|
|
@@ -6857,21 +7116,21 @@ async function projectAddCommand(projectPath, options = {}) {
|
|
|
6857
7116
|
projectConfig.linear_team = linearTeam.toUpperCase();
|
|
6858
7117
|
}
|
|
6859
7118
|
registerProject(key, projectConfig);
|
|
6860
|
-
console.log(
|
|
6861
|
-
console.log(
|
|
6862
|
-
console.log(
|
|
7119
|
+
console.log(chalk31.green(`\u2713 Added project: ${name}`));
|
|
7120
|
+
console.log(chalk31.dim(` Key: ${key}`));
|
|
7121
|
+
console.log(chalk31.dim(` Path: ${fullPath}`));
|
|
6863
7122
|
if (linearTeam) {
|
|
6864
|
-
console.log(
|
|
7123
|
+
console.log(chalk31.dim(` Linear team: ${linearTeam}`));
|
|
6865
7124
|
}
|
|
6866
7125
|
console.log("");
|
|
6867
|
-
console.log(
|
|
7126
|
+
console.log(chalk31.dim(`Edit ${PROJECTS_CONFIG_FILE} to add issue routing rules.`));
|
|
6868
7127
|
}
|
|
6869
7128
|
async function projectListCommand(options = {}) {
|
|
6870
7129
|
const projects = listProjects();
|
|
6871
7130
|
if (projects.length === 0) {
|
|
6872
|
-
console.log(
|
|
6873
|
-
console.log(
|
|
6874
|
-
console.log(
|
|
7131
|
+
console.log(chalk31.dim("No projects registered."));
|
|
7132
|
+
console.log(chalk31.dim("Add one with: pan project add <path> --linear-team <TEAM>"));
|
|
7133
|
+
console.log(chalk31.dim(`Or edit: ${PROJECTS_CONFIG_FILE}`));
|
|
6875
7134
|
return;
|
|
6876
7135
|
}
|
|
6877
7136
|
if (options.json) {
|
|
@@ -6882,51 +7141,51 @@ async function projectListCommand(options = {}) {
|
|
|
6882
7141
|
console.log(JSON.stringify(output, null, 2));
|
|
6883
7142
|
return;
|
|
6884
7143
|
}
|
|
6885
|
-
console.log(
|
|
7144
|
+
console.log(chalk31.bold("\nRegistered Projects:\n"));
|
|
6886
7145
|
for (const { key, config } of projects) {
|
|
6887
|
-
const exists =
|
|
6888
|
-
const statusIcon = exists ?
|
|
6889
|
-
console.log(`${statusIcon} ${
|
|
6890
|
-
console.log(` ${
|
|
7146
|
+
const exists = existsSync29(config.path);
|
|
7147
|
+
const statusIcon = exists ? chalk31.green("\u2713") : chalk31.red("\u2717");
|
|
7148
|
+
console.log(`${statusIcon} ${chalk31.bold(config.name)} ${chalk31.dim(`(${key})`)}`);
|
|
7149
|
+
console.log(` ${chalk31.dim(config.path)}`);
|
|
6891
7150
|
if (config.linear_team) {
|
|
6892
|
-
console.log(` ${
|
|
7151
|
+
console.log(` ${chalk31.cyan(`Linear: ${config.linear_team}`)}`);
|
|
6893
7152
|
}
|
|
6894
7153
|
if (config.issue_routing && config.issue_routing.length > 0) {
|
|
6895
|
-
console.log(` ${
|
|
7154
|
+
console.log(` ${chalk31.dim(`Routes: ${config.issue_routing.length} rules`)}`);
|
|
6896
7155
|
}
|
|
6897
7156
|
console.log("");
|
|
6898
7157
|
}
|
|
6899
|
-
console.log(
|
|
7158
|
+
console.log(chalk31.dim(`Config: ${PROJECTS_CONFIG_FILE}`));
|
|
6900
7159
|
}
|
|
6901
7160
|
async function projectRemoveCommand(nameOrPath) {
|
|
6902
7161
|
const projects = listProjects();
|
|
6903
7162
|
if (unregisterProject(nameOrPath)) {
|
|
6904
|
-
console.log(
|
|
7163
|
+
console.log(chalk31.green(`\u2713 Removed project: ${nameOrPath}`));
|
|
6905
7164
|
return;
|
|
6906
7165
|
}
|
|
6907
7166
|
for (const { key, config } of projects) {
|
|
6908
7167
|
if (config.name === nameOrPath || config.path === resolve(nameOrPath)) {
|
|
6909
7168
|
unregisterProject(key);
|
|
6910
|
-
console.log(
|
|
7169
|
+
console.log(chalk31.green(`\u2713 Removed project: ${config.name}`));
|
|
6911
7170
|
return;
|
|
6912
7171
|
}
|
|
6913
7172
|
}
|
|
6914
|
-
console.log(
|
|
6915
|
-
console.log(
|
|
7173
|
+
console.log(chalk31.red(`Project not found: ${nameOrPath}`));
|
|
7174
|
+
console.log(chalk31.dim(`Use 'pan project list' to see registered projects.`));
|
|
6916
7175
|
}
|
|
6917
7176
|
async function projectInitCommand() {
|
|
6918
|
-
if (
|
|
6919
|
-
console.log(
|
|
7177
|
+
if (existsSync29(PROJECTS_CONFIG_FILE)) {
|
|
7178
|
+
console.log(chalk31.yellow(`Config already exists: ${PROJECTS_CONFIG_FILE}`));
|
|
6920
7179
|
return;
|
|
6921
7180
|
}
|
|
6922
7181
|
initializeProjectsConfig();
|
|
6923
|
-
console.log(
|
|
7182
|
+
console.log(chalk31.green("\u2713 Projects config initialized"));
|
|
6924
7183
|
console.log("");
|
|
6925
|
-
console.log(
|
|
7184
|
+
console.log(chalk31.dim(`Edit ${PROJECTS_CONFIG_FILE} to add your projects.`));
|
|
6926
7185
|
console.log("");
|
|
6927
|
-
console.log(
|
|
7186
|
+
console.log(chalk31.bold("Quick start:"));
|
|
6928
7187
|
console.log(
|
|
6929
|
-
|
|
7188
|
+
chalk31.dim(
|
|
6930
7189
|
' pan project add /path/to/project --name "My Project" --linear-team MIN'
|
|
6931
7190
|
)
|
|
6932
7191
|
);
|
|
@@ -6945,13 +7204,13 @@ async function projectShowCommand(keyOrName) {
|
|
|
6945
7204
|
}
|
|
6946
7205
|
}
|
|
6947
7206
|
if (!found) {
|
|
6948
|
-
console.error(
|
|
6949
|
-
console.log(
|
|
7207
|
+
console.error(chalk31.red(`Project not found: ${keyOrName}`));
|
|
7208
|
+
console.log(chalk31.dim(`Use 'pan project list' to see registered projects.`));
|
|
6950
7209
|
process.exit(1);
|
|
6951
7210
|
}
|
|
6952
|
-
const pathExists =
|
|
6953
|
-
const pathStatus = pathExists ?
|
|
6954
|
-
console.log(
|
|
7211
|
+
const pathExists = existsSync29(found.path);
|
|
7212
|
+
const pathStatus = pathExists ? chalk31.green("\u2713") : chalk31.red("\u2717");
|
|
7213
|
+
console.log(chalk31.bold(`
|
|
6955
7214
|
Project: ${foundKey}
|
|
6956
7215
|
`));
|
|
6957
7216
|
console.log(` Name: ${found.name}`);
|
|
@@ -6960,7 +7219,7 @@ Project: ${foundKey}
|
|
|
6960
7219
|
console.log(` Team: ${found.linear_team}`);
|
|
6961
7220
|
}
|
|
6962
7221
|
if (found.issue_routing && found.issue_routing.length > 0) {
|
|
6963
|
-
console.log("\n " +
|
|
7222
|
+
console.log("\n " + chalk31.bold("Routing Rules:"));
|
|
6964
7223
|
for (const rule of found.issue_routing) {
|
|
6965
7224
|
if (rule.labels) {
|
|
6966
7225
|
console.log(` Labels: ${rule.labels.join(", ")}`);
|
|
@@ -6975,11 +7234,11 @@ Project: ${foundKey}
|
|
|
6975
7234
|
}
|
|
6976
7235
|
|
|
6977
7236
|
// src/cli/commands/doctor.ts
|
|
6978
|
-
import
|
|
6979
|
-
import { existsSync as
|
|
7237
|
+
import chalk32 from "chalk";
|
|
7238
|
+
import { existsSync as existsSync30, readdirSync as readdirSync15, readFileSync as readFileSync24 } from "fs";
|
|
6980
7239
|
import { execSync as execSync15 } from "child_process";
|
|
6981
|
-
import { homedir as
|
|
6982
|
-
import { join as
|
|
7240
|
+
import { homedir as homedir10 } from "os";
|
|
7241
|
+
import { join as join30 } from "path";
|
|
6983
7242
|
function checkCommand2(cmd) {
|
|
6984
7243
|
try {
|
|
6985
7244
|
execSync15(`which ${cmd}`, { encoding: "utf-8", stdio: "pipe" });
|
|
@@ -6989,10 +7248,10 @@ function checkCommand2(cmd) {
|
|
|
6989
7248
|
}
|
|
6990
7249
|
}
|
|
6991
7250
|
function checkDirectory(path) {
|
|
6992
|
-
return
|
|
7251
|
+
return existsSync30(path);
|
|
6993
7252
|
}
|
|
6994
7253
|
function countItems(path) {
|
|
6995
|
-
if (!
|
|
7254
|
+
if (!existsSync30(path)) return 0;
|
|
6996
7255
|
try {
|
|
6997
7256
|
return readdirSync15(path).length;
|
|
6998
7257
|
} catch {
|
|
@@ -7000,8 +7259,8 @@ function countItems(path) {
|
|
|
7000
7259
|
}
|
|
7001
7260
|
}
|
|
7002
7261
|
async function doctorCommand() {
|
|
7003
|
-
console.log(
|
|
7004
|
-
console.log(
|
|
7262
|
+
console.log(chalk32.bold("\nPanopticon Doctor\n"));
|
|
7263
|
+
console.log(chalk32.dim("Checking system health...\n"));
|
|
7005
7264
|
const checks = [];
|
|
7006
7265
|
const requiredCommands = [
|
|
7007
7266
|
{ cmd: "git", name: "Git", fix: "Install git" },
|
|
@@ -7043,8 +7302,8 @@ async function doctorCommand() {
|
|
|
7043
7302
|
}
|
|
7044
7303
|
}
|
|
7045
7304
|
if (checkDirectory(CLAUDE_DIR)) {
|
|
7046
|
-
const skillsCount = countItems(
|
|
7047
|
-
const commandsCount = countItems(
|
|
7305
|
+
const skillsCount = countItems(join30(CLAUDE_DIR, "skills"));
|
|
7306
|
+
const commandsCount = countItems(join30(CLAUDE_DIR, "commands"));
|
|
7048
7307
|
checks.push({
|
|
7049
7308
|
name: "Claude Code Skills",
|
|
7050
7309
|
status: skillsCount > 0 ? "ok" : "warn",
|
|
@@ -7065,8 +7324,8 @@ async function doctorCommand() {
|
|
|
7065
7324
|
fix: "Install Claude Code first"
|
|
7066
7325
|
});
|
|
7067
7326
|
}
|
|
7068
|
-
const envFile =
|
|
7069
|
-
if (
|
|
7327
|
+
const envFile = join30(homedir10(), ".panopticon.env");
|
|
7328
|
+
if (existsSync30(envFile)) {
|
|
7070
7329
|
checks.push({ name: "Config File", status: "ok", message: "~/.panopticon.env exists" });
|
|
7071
7330
|
} else {
|
|
7072
7331
|
checks.push({
|
|
@@ -7078,8 +7337,8 @@ async function doctorCommand() {
|
|
|
7078
7337
|
}
|
|
7079
7338
|
if (process.env.LINEAR_API_KEY) {
|
|
7080
7339
|
checks.push({ name: "LINEAR_API_KEY", status: "ok", message: "Set in environment" });
|
|
7081
|
-
} else if (
|
|
7082
|
-
const content =
|
|
7340
|
+
} else if (existsSync30(envFile)) {
|
|
7341
|
+
const content = readFileSync24(envFile, "utf-8");
|
|
7083
7342
|
if (content.includes("LINEAR_API_KEY")) {
|
|
7084
7343
|
checks.push({ name: "LINEAR_API_KEY", status: "ok", message: "Set in config file" });
|
|
7085
7344
|
} else {
|
|
@@ -7114,46 +7373,46 @@ async function doctorCommand() {
|
|
|
7114
7373
|
});
|
|
7115
7374
|
}
|
|
7116
7375
|
const icons = {
|
|
7117
|
-
ok:
|
|
7118
|
-
warn:
|
|
7119
|
-
error:
|
|
7376
|
+
ok: chalk32.green("\u2713"),
|
|
7377
|
+
warn: chalk32.yellow("\u26A0"),
|
|
7378
|
+
error: chalk32.red("\u2717")
|
|
7120
7379
|
};
|
|
7121
7380
|
let hasErrors = false;
|
|
7122
7381
|
let hasWarnings = false;
|
|
7123
7382
|
for (const check of checks) {
|
|
7124
7383
|
const icon = icons[check.status];
|
|
7125
|
-
const message = check.status === "error" ?
|
|
7384
|
+
const message = check.status === "error" ? chalk32.red(check.message) : check.status === "warn" ? chalk32.yellow(check.message) : chalk32.dim(check.message);
|
|
7126
7385
|
console.log(`${icon} ${check.name}: ${message}`);
|
|
7127
7386
|
if (check.fix && check.status !== "ok") {
|
|
7128
|
-
console.log(
|
|
7387
|
+
console.log(chalk32.dim(` Fix: ${check.fix}`));
|
|
7129
7388
|
}
|
|
7130
7389
|
if (check.status === "error") hasErrors = true;
|
|
7131
7390
|
if (check.status === "warn") hasWarnings = true;
|
|
7132
7391
|
}
|
|
7133
7392
|
console.log("");
|
|
7134
7393
|
if (hasErrors) {
|
|
7135
|
-
console.log(
|
|
7136
|
-
console.log(
|
|
7394
|
+
console.log(chalk32.red("Some required components are missing."));
|
|
7395
|
+
console.log(chalk32.dim("Fix the errors above before using Panopticon."));
|
|
7137
7396
|
} else if (hasWarnings) {
|
|
7138
|
-
console.log(
|
|
7397
|
+
console.log(chalk32.yellow("System is functional with some optional features missing."));
|
|
7139
7398
|
} else {
|
|
7140
|
-
console.log(
|
|
7399
|
+
console.log(chalk32.green("All systems operational!"));
|
|
7141
7400
|
}
|
|
7142
7401
|
console.log("");
|
|
7143
7402
|
}
|
|
7144
7403
|
|
|
7145
7404
|
// src/cli/commands/update.ts
|
|
7146
7405
|
import { execSync as execSync16 } from "child_process";
|
|
7147
|
-
import
|
|
7148
|
-
import { readFileSync as
|
|
7406
|
+
import chalk33 from "chalk";
|
|
7407
|
+
import { readFileSync as readFileSync25 } from "fs";
|
|
7149
7408
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7150
|
-
import { dirname as dirname5, join as
|
|
7409
|
+
import { dirname as dirname5, join as join31 } from "path";
|
|
7151
7410
|
function getCurrentVersion() {
|
|
7152
7411
|
try {
|
|
7153
7412
|
const __filename3 = fileURLToPath2(import.meta.url);
|
|
7154
7413
|
const __dirname3 = dirname5(__filename3);
|
|
7155
|
-
const pkgPath =
|
|
7156
|
-
const pkg = JSON.parse(
|
|
7414
|
+
const pkgPath = join31(__dirname3, "..", "..", "..", "package.json");
|
|
7415
|
+
const pkg = JSON.parse(readFileSync25(pkgPath, "utf-8"));
|
|
7157
7416
|
return pkg.version;
|
|
7158
7417
|
} catch {
|
|
7159
7418
|
return "unknown";
|
|
@@ -7186,49 +7445,49 @@ function isNewer(latest, current) {
|
|
|
7186
7445
|
return l.patch > c.patch;
|
|
7187
7446
|
}
|
|
7188
7447
|
async function updateCommand(options) {
|
|
7189
|
-
console.log(
|
|
7448
|
+
console.log(chalk33.bold("Panopticon Update\n"));
|
|
7190
7449
|
const currentVersion = getCurrentVersion();
|
|
7191
|
-
console.log(`Current version: ${
|
|
7450
|
+
console.log(`Current version: ${chalk33.cyan(currentVersion)}`);
|
|
7192
7451
|
let latestVersion;
|
|
7193
7452
|
try {
|
|
7194
|
-
console.log(
|
|
7453
|
+
console.log(chalk33.dim("Checking npm for latest version..."));
|
|
7195
7454
|
latestVersion = await getLatestVersion();
|
|
7196
|
-
console.log(`Latest version: ${
|
|
7455
|
+
console.log(`Latest version: ${chalk33.cyan(latestVersion)}`);
|
|
7197
7456
|
} catch (error) {
|
|
7198
|
-
console.error(
|
|
7199
|
-
console.error(
|
|
7457
|
+
console.error(chalk33.red("Failed to check for updates"));
|
|
7458
|
+
console.error(chalk33.dim("Make sure you have internet connectivity"));
|
|
7200
7459
|
process.exit(1);
|
|
7201
7460
|
}
|
|
7202
7461
|
const needsUpdate = isNewer(latestVersion, currentVersion);
|
|
7203
7462
|
if (!needsUpdate) {
|
|
7204
|
-
console.log(
|
|
7463
|
+
console.log(chalk33.green("\n\u2713 You are on the latest version"));
|
|
7205
7464
|
return;
|
|
7206
7465
|
}
|
|
7207
7466
|
console.log(
|
|
7208
|
-
|
|
7467
|
+
chalk33.yellow(`
|
|
7209
7468
|
\u2191 Update available: ${currentVersion} \u2192 ${latestVersion}`)
|
|
7210
7469
|
);
|
|
7211
7470
|
if (options.check) {
|
|
7212
|
-
console.log(
|
|
7471
|
+
console.log(chalk33.dim("\nRun `pan update` to install"));
|
|
7213
7472
|
return;
|
|
7214
7473
|
}
|
|
7215
|
-
console.log(
|
|
7474
|
+
console.log(chalk33.dim("\nUpdating Panopticon..."));
|
|
7216
7475
|
try {
|
|
7217
7476
|
execSync16("npm install -g panopticon-cli@latest", {
|
|
7218
7477
|
stdio: "inherit"
|
|
7219
7478
|
});
|
|
7220
|
-
console.log(
|
|
7479
|
+
console.log(chalk33.green(`
|
|
7221
7480
|
\u2713 Updated to ${latestVersion}`));
|
|
7222
7481
|
const config = loadConfig();
|
|
7223
7482
|
if (config.sync.auto_sync) {
|
|
7224
|
-
console.log(
|
|
7483
|
+
console.log(chalk33.dim("\nRunning auto-sync..."));
|
|
7225
7484
|
await syncCommand({});
|
|
7226
7485
|
}
|
|
7227
|
-
console.log(
|
|
7486
|
+
console.log(chalk33.dim("\nRestart any running agents to use the new version."));
|
|
7228
7487
|
} catch (error) {
|
|
7229
|
-
console.error(
|
|
7488
|
+
console.error(chalk33.red("\nUpdate failed"));
|
|
7230
7489
|
console.error(
|
|
7231
|
-
|
|
7490
|
+
chalk33.dim("Try running with sudo: sudo npm install -g panopticon-cli@latest")
|
|
7232
7491
|
);
|
|
7233
7492
|
process.exit(1);
|
|
7234
7493
|
}
|
|
@@ -7253,55 +7512,85 @@ registerInstallCommand(program);
|
|
|
7253
7512
|
program.command("status").description("Show running agents (shorthand for work status)").option("--json", "Output as JSON").action(statusCommand);
|
|
7254
7513
|
program.command("up").description("Start dashboard (and Traefik if enabled)").option("--detach", "Run in background").option("--skip-traefik", "Skip Traefik startup").action(async (options) => {
|
|
7255
7514
|
const { spawn, execSync: execSync17 } = await import("child_process");
|
|
7256
|
-
const { join:
|
|
7515
|
+
const { join: join32, dirname: dirname6 } = await import("path");
|
|
7257
7516
|
const { fileURLToPath: fileURLToPath3 } = await import("url");
|
|
7258
|
-
const { readFileSync:
|
|
7517
|
+
const { readFileSync: readFileSync26, existsSync: existsSync31 } = await import("fs");
|
|
7259
7518
|
const { parse: parse2 } = await import("@iarna/toml");
|
|
7260
7519
|
const __dirname3 = dirname6(fileURLToPath3(import.meta.url));
|
|
7261
|
-
const
|
|
7262
|
-
const
|
|
7520
|
+
const bundledServer = join32(__dirname3, "..", "dashboard", "server.js");
|
|
7521
|
+
const srcDashboard = join32(__dirname3, "..", "..", "src", "dashboard");
|
|
7522
|
+
const configFile = join32(process.env.HOME || "", ".panopticon", "config.toml");
|
|
7263
7523
|
let traefikEnabled = false;
|
|
7264
7524
|
let traefikDomain = "pan.localhost";
|
|
7265
|
-
if (
|
|
7525
|
+
if (existsSync31(configFile)) {
|
|
7266
7526
|
try {
|
|
7267
|
-
const configContent =
|
|
7527
|
+
const configContent = readFileSync26(configFile, "utf-8");
|
|
7268
7528
|
const config = parse2(configContent);
|
|
7269
7529
|
traefikEnabled = config.traefik?.enabled === true;
|
|
7270
7530
|
traefikDomain = config.traefik?.domain || "pan.localhost";
|
|
7271
7531
|
} catch (error) {
|
|
7272
|
-
console.log(
|
|
7532
|
+
console.log(chalk34.yellow("Warning: Could not read config.toml"));
|
|
7273
7533
|
}
|
|
7274
7534
|
}
|
|
7275
|
-
console.log(
|
|
7535
|
+
console.log(chalk34.bold("Starting Panopticon...\n"));
|
|
7276
7536
|
if (traefikEnabled && !options.skipTraefik) {
|
|
7277
|
-
const traefikDir =
|
|
7278
|
-
if (
|
|
7537
|
+
const traefikDir = join32(process.env.HOME || "", ".panopticon", "traefik");
|
|
7538
|
+
if (existsSync31(traefikDir)) {
|
|
7279
7539
|
try {
|
|
7280
|
-
|
|
7540
|
+
const composeFile = join32(traefikDir, "docker-compose.yml");
|
|
7541
|
+
if (existsSync31(composeFile)) {
|
|
7542
|
+
const content = readFileSync26(composeFile, "utf-8");
|
|
7543
|
+
if (!content.includes("external: true") && content.includes("panopticon:")) {
|
|
7544
|
+
const patched = content.replace(
|
|
7545
|
+
/networks:\s*\n\s*panopticon:\s*\n\s*name: panopticon\s*\n\s*driver: bridge/,
|
|
7546
|
+
"networks:\n panopticon:\n name: panopticon\n external: true # Network created by 'pan install'"
|
|
7547
|
+
);
|
|
7548
|
+
const { writeFileSync: writeFileSync18 } = await import("fs");
|
|
7549
|
+
writeFileSync18(composeFile, patched);
|
|
7550
|
+
console.log(chalk34.dim(" (migrated network config)"));
|
|
7551
|
+
}
|
|
7552
|
+
}
|
|
7553
|
+
console.log(chalk34.dim("Starting Traefik..."));
|
|
7281
7554
|
execSync17("docker-compose up -d", {
|
|
7282
7555
|
cwd: traefikDir,
|
|
7283
7556
|
stdio: "pipe"
|
|
7284
7557
|
});
|
|
7285
|
-
console.log(
|
|
7286
|
-
console.log(
|
|
7558
|
+
console.log(chalk34.green("\u2713 Traefik started"));
|
|
7559
|
+
console.log(chalk34.dim(` Dashboard: https://traefik.${traefikDomain}:8080
|
|
7287
7560
|
`));
|
|
7288
7561
|
} catch (error) {
|
|
7289
|
-
console.log(
|
|
7290
|
-
console.log(
|
|
7562
|
+
console.log(chalk34.yellow("\u26A0 Failed to start Traefik (continuing anyway)"));
|
|
7563
|
+
console.log(chalk34.dim(" Run with --skip-traefik to suppress this message\n"));
|
|
7291
7564
|
}
|
|
7292
7565
|
}
|
|
7293
7566
|
}
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
console.error(
|
|
7298
|
-
console.error(
|
|
7567
|
+
const isProduction = existsSync31(bundledServer);
|
|
7568
|
+
const isDevelopment = existsSync31(srcDashboard);
|
|
7569
|
+
if (!isProduction && !isDevelopment) {
|
|
7570
|
+
console.error(chalk34.red("Error: Dashboard not found"));
|
|
7571
|
+
console.error(chalk34.dim("This may be a corrupted installation. Try reinstalling panopticon-cli."));
|
|
7299
7572
|
process.exit(1);
|
|
7300
7573
|
}
|
|
7301
|
-
|
|
7574
|
+
if (isDevelopment && !isProduction) {
|
|
7575
|
+
try {
|
|
7576
|
+
execSync17("npm --version", { stdio: "pipe" });
|
|
7577
|
+
} catch {
|
|
7578
|
+
console.error(chalk34.red("Error: npm not found in PATH"));
|
|
7579
|
+
console.error(chalk34.dim("Make sure Node.js and npm are installed and in your PATH"));
|
|
7580
|
+
process.exit(1);
|
|
7581
|
+
}
|
|
7582
|
+
}
|
|
7583
|
+
if (isProduction) {
|
|
7584
|
+
console.log(chalk34.dim("Starting dashboard (bundled mode)..."));
|
|
7585
|
+
} else {
|
|
7586
|
+
console.log(chalk34.dim("Starting dashboard (development mode)..."));
|
|
7587
|
+
}
|
|
7302
7588
|
if (options.detach) {
|
|
7303
|
-
const child = spawn("
|
|
7304
|
-
|
|
7589
|
+
const child = isProduction ? spawn("node", [bundledServer], {
|
|
7590
|
+
detached: true,
|
|
7591
|
+
stdio: "ignore"
|
|
7592
|
+
}) : spawn("npm", ["run", "dev"], {
|
|
7593
|
+
cwd: srcDashboard,
|
|
7305
7594
|
detached: true,
|
|
7306
7595
|
stdio: "ignore",
|
|
7307
7596
|
shell: true
|
|
@@ -7309,7 +7598,7 @@ program.command("up").description("Start dashboard (and Traefik if enabled)").op
|
|
|
7309
7598
|
let hasError = false;
|
|
7310
7599
|
child.on("error", (err) => {
|
|
7311
7600
|
hasError = true;
|
|
7312
|
-
console.error(
|
|
7601
|
+
console.error(chalk34.red("Failed to start dashboard in background:"), err.message);
|
|
7313
7602
|
process.exit(1);
|
|
7314
7603
|
});
|
|
7315
7604
|
setTimeout(() => {
|
|
@@ -7317,70 +7606,72 @@ program.command("up").description("Start dashboard (and Traefik if enabled)").op
|
|
|
7317
7606
|
child.unref();
|
|
7318
7607
|
}
|
|
7319
7608
|
}, 100);
|
|
7320
|
-
console.log(
|
|
7609
|
+
console.log(chalk34.green("\u2713 Dashboard started in background"));
|
|
7321
7610
|
if (traefikEnabled) {
|
|
7322
|
-
console.log(` Frontend: ${
|
|
7323
|
-
console.log(` API: ${
|
|
7611
|
+
console.log(` Frontend: ${chalk34.cyan(`https://${traefikDomain}`)}`);
|
|
7612
|
+
console.log(` API: ${chalk34.cyan(`https://${traefikDomain}/api`)}`);
|
|
7324
7613
|
} else {
|
|
7325
|
-
console.log(` Frontend: ${
|
|
7326
|
-
console.log(` API: ${
|
|
7614
|
+
console.log(` Frontend: ${chalk34.cyan("http://localhost:3001")}`);
|
|
7615
|
+
console.log(` API: ${chalk34.cyan("http://localhost:3002")}`);
|
|
7327
7616
|
}
|
|
7328
7617
|
} else {
|
|
7329
7618
|
if (traefikEnabled) {
|
|
7330
|
-
console.log(` Frontend: ${
|
|
7331
|
-
console.log(` API: ${
|
|
7619
|
+
console.log(` Frontend: ${chalk34.cyan(`https://${traefikDomain}`)}`);
|
|
7620
|
+
console.log(` API: ${chalk34.cyan(`https://${traefikDomain}/api`)}`);
|
|
7332
7621
|
} else {
|
|
7333
|
-
console.log(` Frontend: ${
|
|
7334
|
-
console.log(` API: ${
|
|
7622
|
+
console.log(` Frontend: ${chalk34.cyan("http://localhost:3001")}`);
|
|
7623
|
+
console.log(` API: ${chalk34.cyan("http://localhost:3002")}`);
|
|
7335
7624
|
}
|
|
7336
|
-
console.log(
|
|
7337
|
-
const child = spawn("
|
|
7338
|
-
|
|
7625
|
+
console.log(chalk34.dim("\nPress Ctrl+C to stop\n"));
|
|
7626
|
+
const child = isProduction ? spawn("node", [bundledServer], {
|
|
7627
|
+
stdio: "inherit"
|
|
7628
|
+
}) : spawn("npm", ["run", "dev"], {
|
|
7629
|
+
cwd: srcDashboard,
|
|
7339
7630
|
stdio: "inherit",
|
|
7340
7631
|
shell: true
|
|
7341
7632
|
});
|
|
7342
7633
|
child.on("error", (err) => {
|
|
7343
|
-
console.error(
|
|
7634
|
+
console.error(chalk34.red("Failed to start dashboard:"), err.message);
|
|
7344
7635
|
process.exit(1);
|
|
7345
7636
|
});
|
|
7346
7637
|
}
|
|
7347
7638
|
});
|
|
7348
7639
|
program.command("down").description("Stop dashboard (and Traefik if enabled)").option("--skip-traefik", "Skip Traefik shutdown").action(async (options) => {
|
|
7349
7640
|
const { execSync: execSync17 } = await import("child_process");
|
|
7350
|
-
const { join:
|
|
7351
|
-
const { readFileSync:
|
|
7641
|
+
const { join: join32 } = await import("path");
|
|
7642
|
+
const { readFileSync: readFileSync26, existsSync: existsSync31 } = await import("fs");
|
|
7352
7643
|
const { parse: parse2 } = await import("@iarna/toml");
|
|
7353
|
-
console.log(
|
|
7354
|
-
console.log(
|
|
7644
|
+
console.log(chalk34.bold("Stopping Panopticon...\n"));
|
|
7645
|
+
console.log(chalk34.dim("Stopping dashboard..."));
|
|
7355
7646
|
try {
|
|
7356
7647
|
execSync17("lsof -ti:3001 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
|
|
7357
7648
|
execSync17("lsof -ti:3002 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
|
|
7358
|
-
console.log(
|
|
7649
|
+
console.log(chalk34.green("\u2713 Dashboard stopped"));
|
|
7359
7650
|
} catch {
|
|
7360
|
-
console.log(
|
|
7651
|
+
console.log(chalk34.dim(" No dashboard processes found"));
|
|
7361
7652
|
}
|
|
7362
|
-
const configFile =
|
|
7653
|
+
const configFile = join32(process.env.HOME || "", ".panopticon", "config.toml");
|
|
7363
7654
|
let traefikEnabled = false;
|
|
7364
|
-
if (
|
|
7655
|
+
if (existsSync31(configFile)) {
|
|
7365
7656
|
try {
|
|
7366
|
-
const configContent =
|
|
7657
|
+
const configContent = readFileSync26(configFile, "utf-8");
|
|
7367
7658
|
const config = parse2(configContent);
|
|
7368
7659
|
traefikEnabled = config.traefik?.enabled === true;
|
|
7369
7660
|
} catch (error) {
|
|
7370
7661
|
}
|
|
7371
7662
|
}
|
|
7372
7663
|
if (traefikEnabled && !options.skipTraefik) {
|
|
7373
|
-
const traefikDir =
|
|
7374
|
-
if (
|
|
7375
|
-
console.log(
|
|
7664
|
+
const traefikDir = join32(process.env.HOME || "", ".panopticon", "traefik");
|
|
7665
|
+
if (existsSync31(traefikDir)) {
|
|
7666
|
+
console.log(chalk34.dim("Stopping Traefik..."));
|
|
7376
7667
|
try {
|
|
7377
7668
|
execSync17("docker-compose down", {
|
|
7378
7669
|
cwd: traefikDir,
|
|
7379
7670
|
stdio: "pipe"
|
|
7380
7671
|
});
|
|
7381
|
-
console.log(
|
|
7672
|
+
console.log(chalk34.green("\u2713 Traefik stopped"));
|
|
7382
7673
|
} catch (error) {
|
|
7383
|
-
console.log(
|
|
7674
|
+
console.log(chalk34.yellow("\u26A0 Failed to stop Traefik"));
|
|
7384
7675
|
}
|
|
7385
7676
|
}
|
|
7386
7677
|
}
|