agendex-cli 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +603 -264
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4473,6 +4473,158 @@ function spawnBrowser(command, args, options = {}) {
|
|
|
4473
4473
|
child.unref();
|
|
4474
4474
|
}
|
|
4475
4475
|
|
|
4476
|
+
// src/help.ts
|
|
4477
|
+
var COMMAND_WIDTH = 38;
|
|
4478
|
+
var FLAG_WIDTH = 18;
|
|
4479
|
+
var ANSI = {
|
|
4480
|
+
reset: "\x1B[0m",
|
|
4481
|
+
bold: "\x1B[1m",
|
|
4482
|
+
gray: "\x1B[90m",
|
|
4483
|
+
cyan: "\x1B[36m",
|
|
4484
|
+
green: "\x1B[32m",
|
|
4485
|
+
yellow: "\x1B[33m"
|
|
4486
|
+
};
|
|
4487
|
+
var HELP_GROUPS = [
|
|
4488
|
+
{
|
|
4489
|
+
title: "Quick start",
|
|
4490
|
+
commands: [
|
|
4491
|
+
{
|
|
4492
|
+
command: "start",
|
|
4493
|
+
description: "Start the background sync daemon",
|
|
4494
|
+
examples: ["agendex", "agendex start"]
|
|
4495
|
+
},
|
|
4496
|
+
{ command: "status", description: "Show local/cloud health and recommended next steps" },
|
|
4497
|
+
{
|
|
4498
|
+
command: "open",
|
|
4499
|
+
description: "Open the Agendex dashboard in your browser",
|
|
4500
|
+
examples: ["agendex open --url <self-hosted-url>"]
|
|
4501
|
+
}
|
|
4502
|
+
]
|
|
4503
|
+
},
|
|
4504
|
+
{
|
|
4505
|
+
title: "Cloud account",
|
|
4506
|
+
commands: [
|
|
4507
|
+
{
|
|
4508
|
+
command: "login",
|
|
4509
|
+
description: "Authenticate via browser OAuth",
|
|
4510
|
+
examples: ["agendex login --url <self-hosted-url>"]
|
|
4511
|
+
},
|
|
4512
|
+
{ command: "logout", description: "Clear the stored cloud token" },
|
|
4513
|
+
{ command: "view <url>", description: "Open a shared plan URL in your browser" }
|
|
4514
|
+
]
|
|
4515
|
+
},
|
|
4516
|
+
{
|
|
4517
|
+
title: "Plan sources",
|
|
4518
|
+
commands: [
|
|
4519
|
+
{ command: "configure", description: "Select which agents/adapters to index" },
|
|
4520
|
+
{
|
|
4521
|
+
command: "add-dir <path>",
|
|
4522
|
+
description: "Add a custom directory to scan for plans",
|
|
4523
|
+
examples: ["agendex add-dir ~/plans --live"]
|
|
4524
|
+
},
|
|
4525
|
+
{ command: "remove-dir <path>", description: "Remove a custom plan directory" },
|
|
4526
|
+
{ command: "list-dirs", description: "List configured custom plan directories" }
|
|
4527
|
+
]
|
|
4528
|
+
},
|
|
4529
|
+
{
|
|
4530
|
+
title: "Sync & upload",
|
|
4531
|
+
commands: [
|
|
4532
|
+
{
|
|
4533
|
+
command: "sync",
|
|
4534
|
+
description: "Run a one-shot scan and cloud sync",
|
|
4535
|
+
examples: ["agendex sync --force"]
|
|
4536
|
+
},
|
|
4537
|
+
{
|
|
4538
|
+
command: "upload <path>",
|
|
4539
|
+
description: "Upload a single Markdown plan file",
|
|
4540
|
+
examples: ["agendex upload plan.md --agent claude-code --open"]
|
|
4541
|
+
}
|
|
4542
|
+
]
|
|
4543
|
+
},
|
|
4544
|
+
{
|
|
4545
|
+
title: "Hooks & review",
|
|
4546
|
+
commands: [
|
|
4547
|
+
{ command: "hooks status", description: "Show Claude Code, Codex, and Pi hook status" },
|
|
4548
|
+
{
|
|
4549
|
+
command: "hooks install <agent|all>",
|
|
4550
|
+
description: "Install hook integration",
|
|
4551
|
+
examples: ["agendex hooks install claude-code --preview"]
|
|
4552
|
+
},
|
|
4553
|
+
{ command: "hooks uninstall <agent|all>", description: "Remove managed hook entries" },
|
|
4554
|
+
{ command: "review-plan --hook --agent <agent>", description: "Run hook-native plan review" }
|
|
4555
|
+
]
|
|
4556
|
+
},
|
|
4557
|
+
{
|
|
4558
|
+
title: "Maintenance",
|
|
4559
|
+
commands: [
|
|
4560
|
+
{ command: "cleanup", description: "Interactively remove cloud daemon records" },
|
|
4561
|
+
{ command: "cleanup --stale", description: "Auto-remove stale cloud daemon records" },
|
|
4562
|
+
{ command: "upgrade", description: "Upgrade the globally installed CLI" },
|
|
4563
|
+
{ command: "upgrade --force", description: "Reinstall latest even when up to date" },
|
|
4564
|
+
{ command: "help", description: "Show this help message" },
|
|
4565
|
+
{ command: "--version, -v", description: "Print CLI version" }
|
|
4566
|
+
]
|
|
4567
|
+
}
|
|
4568
|
+
];
|
|
4569
|
+
var FLAGS = [
|
|
4570
|
+
{ flag: "--dev", description: "Use the dev environment (~/.agendex-dev config dir)" }
|
|
4571
|
+
];
|
|
4572
|
+
function supportsColor() {
|
|
4573
|
+
if (process.env.NO_COLOR)
|
|
4574
|
+
return false;
|
|
4575
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0")
|
|
4576
|
+
return true;
|
|
4577
|
+
return Boolean(process.stdout.isTTY);
|
|
4578
|
+
}
|
|
4579
|
+
function paint(enabled, code, text) {
|
|
4580
|
+
return enabled ? `${code}${text}${ANSI.reset}` : text;
|
|
4581
|
+
}
|
|
4582
|
+
function createStyles(color) {
|
|
4583
|
+
return {
|
|
4584
|
+
title: (text) => paint(color, `${ANSI.bold}${ANSI.cyan}`, text),
|
|
4585
|
+
section: (text) => paint(color, ANSI.yellow, text),
|
|
4586
|
+
command: (text) => paint(color, ANSI.green, text),
|
|
4587
|
+
muted: (text) => paint(color, ANSI.gray, text)
|
|
4588
|
+
};
|
|
4589
|
+
}
|
|
4590
|
+
function commandLine(styles, entry) {
|
|
4591
|
+
return ` ${styles.command(entry.command.padEnd(COMMAND_WIDTH))}${entry.description}`;
|
|
4592
|
+
}
|
|
4593
|
+
function exampleLine(styles, examples) {
|
|
4594
|
+
return ` ${styles.muted(examples.join(" | "))}`;
|
|
4595
|
+
}
|
|
4596
|
+
function flagLine(styles, flag) {
|
|
4597
|
+
return ` ${styles.command(flag.flag.padEnd(FLAG_WIDTH))}${flag.description}`;
|
|
4598
|
+
}
|
|
4599
|
+
function renderHelp(options) {
|
|
4600
|
+
const styles = createStyles(options.color ?? supportsColor());
|
|
4601
|
+
const lines = [];
|
|
4602
|
+
lines.push(styles.title("Agendex: local coding-agent plan sync"));
|
|
4603
|
+
lines.push(styles.muted(`CLI v${options.cliVersion}`));
|
|
4604
|
+
lines.push("");
|
|
4605
|
+
lines.push(`Usage: ${styles.command("agendex")} ${styles.muted("[OPTIONS] [COMMAND]")}`);
|
|
4606
|
+
lines.push("");
|
|
4607
|
+
lines.push(styles.section("Commands:"));
|
|
4608
|
+
for (const group of HELP_GROUPS) {
|
|
4609
|
+
lines.push(` ${styles.section(`${group.title}:`)}`);
|
|
4610
|
+
for (const command of group.commands) {
|
|
4611
|
+
lines.push(commandLine(styles, command));
|
|
4612
|
+
if (command.examples && command.examples.length > 0) {
|
|
4613
|
+
lines.push(exampleLine(styles, command.examples));
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
lines.push("");
|
|
4617
|
+
}
|
|
4618
|
+
lines.push(styles.section("Flags:"));
|
|
4619
|
+
for (const flag of FLAGS)
|
|
4620
|
+
lines.push(flagLine(styles, flag));
|
|
4621
|
+
lines.push("");
|
|
4622
|
+
lines.push(styles.section("Tip:"));
|
|
4623
|
+
lines.push(` ${styles.muted("Run")} ${styles.command("agendex status")} ${styles.muted("to see daemon health, cloud sync state, and next steps.")}`);
|
|
4624
|
+
return lines.join(`
|
|
4625
|
+
`);
|
|
4626
|
+
}
|
|
4627
|
+
|
|
4476
4628
|
// src/daemon.ts
|
|
4477
4629
|
import { spawn as spawn2 } from "node:child_process";
|
|
4478
4630
|
import { hostname as osHostname2 } from "node:os";
|
|
@@ -5676,6 +5828,254 @@ async function runHookReviewCommand(args) {
|
|
|
5676
5828
|
return 1;
|
|
5677
5829
|
}
|
|
5678
5830
|
|
|
5831
|
+
// src/status.ts
|
|
5832
|
+
var LABEL_WIDTH = 18;
|
|
5833
|
+
var ACTION_WIDTH = 26;
|
|
5834
|
+
var ANSI2 = {
|
|
5835
|
+
reset: "\x1B[0m",
|
|
5836
|
+
bold: "\x1B[1m",
|
|
5837
|
+
dim: "\x1B[2m",
|
|
5838
|
+
gray: "\x1B[90m",
|
|
5839
|
+
cyan: "\x1B[36m",
|
|
5840
|
+
green: "\x1B[32m",
|
|
5841
|
+
yellow: "\x1B[33m",
|
|
5842
|
+
red: "\x1B[31m",
|
|
5843
|
+
blue: "\x1B[34m"
|
|
5844
|
+
};
|
|
5845
|
+
function supportsColor2() {
|
|
5846
|
+
if (process.env.NO_COLOR)
|
|
5847
|
+
return false;
|
|
5848
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0")
|
|
5849
|
+
return true;
|
|
5850
|
+
return Boolean(process.stdout.isTTY);
|
|
5851
|
+
}
|
|
5852
|
+
function paint2(enabled, code, text) {
|
|
5853
|
+
return enabled ? `${code}${text}${ANSI2.reset}` : text;
|
|
5854
|
+
}
|
|
5855
|
+
function createStyles2(color) {
|
|
5856
|
+
return {
|
|
5857
|
+
title: (text) => paint2(color, `${ANSI2.bold}${ANSI2.cyan}`, text),
|
|
5858
|
+
section: (text) => paint2(color, ANSI2.yellow, text),
|
|
5859
|
+
key: (text) => paint2(color, ANSI2.green, text),
|
|
5860
|
+
value: (text) => text,
|
|
5861
|
+
muted: (text) => paint2(color, ANSI2.gray, text),
|
|
5862
|
+
status(kind, text) {
|
|
5863
|
+
if (kind === "success")
|
|
5864
|
+
return paint2(color, ANSI2.green, text);
|
|
5865
|
+
if (kind === "warning")
|
|
5866
|
+
return paint2(color, ANSI2.yellow, text);
|
|
5867
|
+
if (kind === "danger")
|
|
5868
|
+
return paint2(color, ANSI2.red, text);
|
|
5869
|
+
if (kind === "info")
|
|
5870
|
+
return paint2(color, ANSI2.blue, text);
|
|
5871
|
+
return paint2(color, ANSI2.gray, text);
|
|
5872
|
+
}
|
|
5873
|
+
};
|
|
5874
|
+
}
|
|
5875
|
+
function badge(styles, kind, label) {
|
|
5876
|
+
let icon = "•";
|
|
5877
|
+
if (kind === "success")
|
|
5878
|
+
icon = "✓";
|
|
5879
|
+
else if (kind === "warning")
|
|
5880
|
+
icon = "!";
|
|
5881
|
+
else if (kind === "danger")
|
|
5882
|
+
icon = "×";
|
|
5883
|
+
return styles.status(kind, `${icon} ${label}`);
|
|
5884
|
+
}
|
|
5885
|
+
function isPresent(value) {
|
|
5886
|
+
return value !== null && value !== undefined;
|
|
5887
|
+
}
|
|
5888
|
+
function row(styles, label, status, detail) {
|
|
5889
|
+
const labelCell = styles.key(label.padEnd(LABEL_WIDTH));
|
|
5890
|
+
const suffix = detail ? ` ${styles.muted(detail)}` : "";
|
|
5891
|
+
return ` ${labelCell}${status}${suffix}`;
|
|
5892
|
+
}
|
|
5893
|
+
function actionRow(styles, command, description) {
|
|
5894
|
+
return ` ${styles.key(command.padEnd(ACTION_WIDTH))}${description}`;
|
|
5895
|
+
}
|
|
5896
|
+
function listItem(styles, value) {
|
|
5897
|
+
return ` ${styles.muted("•")} ${value}`;
|
|
5898
|
+
}
|
|
5899
|
+
function summarizeList(items, maxItems = 4) {
|
|
5900
|
+
if (items.length === 0)
|
|
5901
|
+
return "";
|
|
5902
|
+
if (items.length <= maxItems)
|
|
5903
|
+
return items.join(", ");
|
|
5904
|
+
const visible = items.slice(0, maxItems).join(", ");
|
|
5905
|
+
return `${visible} +${items.length - maxItems} more`;
|
|
5906
|
+
}
|
|
5907
|
+
function formatDuration(ms) {
|
|
5908
|
+
const safeMs = Number.isFinite(ms) ? Math.max(0, Math.floor(ms)) : 0;
|
|
5909
|
+
const seconds = Math.floor(safeMs / 1000);
|
|
5910
|
+
if (seconds < 60)
|
|
5911
|
+
return `${seconds}s`;
|
|
5912
|
+
const minutes = Math.floor(seconds / 60);
|
|
5913
|
+
const remainingSeconds = seconds % 60;
|
|
5914
|
+
if (minutes < 60)
|
|
5915
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
5916
|
+
const hours = Math.floor(minutes / 60);
|
|
5917
|
+
const remainingMinutes = minutes % 60;
|
|
5918
|
+
if (hours < 24)
|
|
5919
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
5920
|
+
const days = Math.floor(hours / 24);
|
|
5921
|
+
const remainingHours = hours % 24;
|
|
5922
|
+
return `${days}d ${remainingHours}h`;
|
|
5923
|
+
}
|
|
5924
|
+
function localDaemonDetail(options, now) {
|
|
5925
|
+
if (!options.running)
|
|
5926
|
+
return "run `agendex start` to begin background sync";
|
|
5927
|
+
const parts = [];
|
|
5928
|
+
if (isPresent(options.pidInfo?.pid))
|
|
5929
|
+
parts.push(`PID ${options.pidInfo.pid}`);
|
|
5930
|
+
if (isPresent(options.pidInfo?.startedAtMs)) {
|
|
5931
|
+
parts.push(`up ${formatDuration(now - options.pidInfo.startedAtMs)}`);
|
|
5932
|
+
} else {
|
|
5933
|
+
parts.push("uptime unknown");
|
|
5934
|
+
}
|
|
5935
|
+
if (options.pidInfo?.hostname)
|
|
5936
|
+
parts.push(`host ${options.pidInfo.hostname}`);
|
|
5937
|
+
else
|
|
5938
|
+
parts.push("host unknown");
|
|
5939
|
+
return parts.join(" • ");
|
|
5940
|
+
}
|
|
5941
|
+
function isDeviceAlive(device, now) {
|
|
5942
|
+
if (!isPresent(device.lastSeenAt))
|
|
5943
|
+
return false;
|
|
5944
|
+
return now - device.lastSeenAt < CLI_DAEMON_STALE_AFTER_MS;
|
|
5945
|
+
}
|
|
5946
|
+
function sortDevices(devices, localDeviceId, now) {
|
|
5947
|
+
return [...devices].sort((a, b) => {
|
|
5948
|
+
const localDiff = Number(b.deviceId === localDeviceId) - Number(a.deviceId === localDeviceId);
|
|
5949
|
+
if (localDiff !== 0)
|
|
5950
|
+
return localDiff;
|
|
5951
|
+
const aliveDiff = Number(isDeviceAlive(b, now)) - Number(isDeviceAlive(a, now));
|
|
5952
|
+
if (aliveDiff !== 0)
|
|
5953
|
+
return aliveDiff;
|
|
5954
|
+
return (a.hostname ?? "").localeCompare(b.hostname ?? "");
|
|
5955
|
+
});
|
|
5956
|
+
}
|
|
5957
|
+
function deviceLines({ styles, device, localDeviceId, now }) {
|
|
5958
|
+
const alive = isDeviceAlive(device, now);
|
|
5959
|
+
const statusText = alive ? "✓ alive" : "! stale";
|
|
5960
|
+
const statusCell = styles.status(alive ? "success" : "warning", statusText.padEnd(12));
|
|
5961
|
+
const hostname2 = device.hostname ?? "unknown host";
|
|
5962
|
+
const isLocalDevice = localDeviceId !== undefined && device.deviceId === localDeviceId;
|
|
5963
|
+
const localMarker = isLocalDevice ? " (this machine)" : "";
|
|
5964
|
+
const pid = isPresent(device.pid) ? `PID ${device.pid}` : "PID unknown";
|
|
5965
|
+
const uptime = isPresent(device.startedAtMs) ? `up ${formatDuration(now - device.startedAtMs)}` : "uptime unknown";
|
|
5966
|
+
const seen = isPresent(device.lastSeenAt) ? `seen ${formatDuration(now - device.lastSeenAt)} ago` : "last seen unknown";
|
|
5967
|
+
const ip = device.ipAddress ?? "IP unknown";
|
|
5968
|
+
return [
|
|
5969
|
+
` ${statusCell}${styles.value(hostname2)}${styles.muted(localMarker)}`,
|
|
5970
|
+
` ${styles.muted([pid, uptime, seen, ip].join(" • "))}`
|
|
5971
|
+
];
|
|
5972
|
+
}
|
|
5973
|
+
function addCloudDaemonLines({ lines, styles, options, cloudReady, now }) {
|
|
5974
|
+
if (!cloudReady) {
|
|
5975
|
+
lines.push(row(styles, "Daemon registry", badge(styles, "warning", "offline"), "login required"));
|
|
5976
|
+
return;
|
|
5977
|
+
}
|
|
5978
|
+
if (options.cloudDaemonError?.kind === "auth-expired") {
|
|
5979
|
+
lines.push(row(styles, "Daemon registry", badge(styles, "danger", "token expired"), "run `agendex login`"));
|
|
5980
|
+
return;
|
|
5981
|
+
}
|
|
5982
|
+
if (options.cloudDaemonError?.kind === "unavailable") {
|
|
5983
|
+
lines.push(row(styles, "Daemon registry", badge(styles, "warning", "unavailable"), options.cloudDaemonError.message ?? "will retry on the next status check"));
|
|
5984
|
+
return;
|
|
5985
|
+
}
|
|
5986
|
+
const devices = options.devices ?? [];
|
|
5987
|
+
if (devices.length === 0) {
|
|
5988
|
+
lines.push(row(styles, "Daemon registry", badge(styles, "info", "empty"), "no cloud daemons reported"));
|
|
5989
|
+
return;
|
|
5990
|
+
}
|
|
5991
|
+
const aliveCount = devices.filter((device) => isDeviceAlive(device, now)).length;
|
|
5992
|
+
const staleCount = devices.length - aliveCount;
|
|
5993
|
+
lines.push(row(styles, "Daemon registry", badge(styles, "success", `${devices.length} device${devices.length === 1 ? "" : "s"}`), `${aliveCount} alive • ${staleCount} stale`));
|
|
5994
|
+
const localDeviceId = options.config?.deviceId;
|
|
5995
|
+
for (const device of sortDevices(devices, localDeviceId, now)) {
|
|
5996
|
+
lines.push(...deviceLines({ styles, device, localDeviceId, now }));
|
|
5997
|
+
}
|
|
5998
|
+
}
|
|
5999
|
+
function nextActions(options, now) {
|
|
6000
|
+
const config = options.config;
|
|
6001
|
+
const cloudReady = Boolean(config?.cloudToken && config.convexUrl);
|
|
6002
|
+
const adapters = config?.enabledAdapters ?? [];
|
|
6003
|
+
const devices = options.devices ?? [];
|
|
6004
|
+
const hasStaleDevices = devices.some((device) => !isDeviceAlive(device, now));
|
|
6005
|
+
const authExpired = options.cloudDaemonError?.kind === "auth-expired";
|
|
6006
|
+
const actions = [];
|
|
6007
|
+
if (!options.running) {
|
|
6008
|
+
actions.push({ command: "agendex start", description: "Start the background sync daemon" });
|
|
6009
|
+
}
|
|
6010
|
+
if (!cloudReady || authExpired) {
|
|
6011
|
+
actions.push({ command: "agendex login", description: "Connect or refresh cloud sync" });
|
|
6012
|
+
}
|
|
6013
|
+
if (adapters.length === 0) {
|
|
6014
|
+
actions.push({
|
|
6015
|
+
command: "agendex configure",
|
|
6016
|
+
description: "Choose which agent plan sources to index"
|
|
6017
|
+
});
|
|
6018
|
+
}
|
|
6019
|
+
if (hasStaleDevices) {
|
|
6020
|
+
actions.push({
|
|
6021
|
+
command: "agendex cleanup --stale",
|
|
6022
|
+
description: "Remove stale cloud daemon records"
|
|
6023
|
+
});
|
|
6024
|
+
}
|
|
6025
|
+
if (cloudReady) {
|
|
6026
|
+
actions.push({
|
|
6027
|
+
command: "agendex sync",
|
|
6028
|
+
description: "Run a one-shot scan and cloud sync now"
|
|
6029
|
+
});
|
|
6030
|
+
}
|
|
6031
|
+
actions.push({ command: "agendex open", description: "Open the Agendex dashboard" });
|
|
6032
|
+
const seen = new Set;
|
|
6033
|
+
return actions.filter((action) => {
|
|
6034
|
+
if (seen.has(action.command))
|
|
6035
|
+
return false;
|
|
6036
|
+
seen.add(action.command);
|
|
6037
|
+
return true;
|
|
6038
|
+
});
|
|
6039
|
+
}
|
|
6040
|
+
function renderStatus(options) {
|
|
6041
|
+
const styles = createStyles2(options.color ?? supportsColor2());
|
|
6042
|
+
const now = options.now ?? Date.now();
|
|
6043
|
+
const config = options.config;
|
|
6044
|
+
const adapters = config?.enabledAdapters ?? [];
|
|
6045
|
+
const customDirs = config?.customPlanDirs ?? [];
|
|
6046
|
+
const cloudReady = Boolean(config?.cloudToken && config.convexUrl);
|
|
6047
|
+
const lines = [];
|
|
6048
|
+
lines.push(styles.title("Agendex status"));
|
|
6049
|
+
lines.push(styles.muted(`Config: ${options.configPath}`));
|
|
6050
|
+
lines.push("");
|
|
6051
|
+
lines.push(styles.section("Local:"));
|
|
6052
|
+
lines.push(row(styles, "Daemon", options.running ? badge(styles, "success", "running") : badge(styles, "warning", "not running"), localDaemonDetail(options, now)));
|
|
6053
|
+
lines.push(row(styles, "Config file", config ? badge(styles, "success", "found") : badge(styles, "warning", "missing"), config ? `v${config.configVersion}` : "created on first start/configure"));
|
|
6054
|
+
lines.push(row(styles, "Local API token", config?.token ? badge(styles, "success", "set") : badge(styles, "warning", "missing"), config?.token ? "ready for OSS API auth" : "generated when the local app starts"));
|
|
6055
|
+
lines.push(row(styles, "CLI version", styles.value(`v${options.cliVersion}`)));
|
|
6056
|
+
lines.push("");
|
|
6057
|
+
lines.push(styles.section("Cloud:"));
|
|
6058
|
+
lines.push(row(styles, "Account", cloudReady ? badge(styles, "success", "logged in") : badge(styles, "warning", "not logged in"), config?.convexUrl ?? "run `agendex login` to enable cloud sync"));
|
|
6059
|
+
if (config?.siteUrl) {
|
|
6060
|
+
lines.push(row(styles, "Web app", styles.value(config.siteUrl)));
|
|
6061
|
+
}
|
|
6062
|
+
lines.push(row(styles, "Device ID", config?.deviceId ? badge(styles, "success", "registered") : badge(styles, "warning", "missing"), config?.deviceId ?? "created on the next daemon heartbeat"));
|
|
6063
|
+
addCloudDaemonLines({ lines, styles, options, cloudReady, now });
|
|
6064
|
+
lines.push("");
|
|
6065
|
+
lines.push(styles.section("Plan sources:"));
|
|
6066
|
+
lines.push(row(styles, "Adapters", adapters.length > 0 ? badge(styles, "success", `${adapters.length} enabled`) : badge(styles, "warning", "none enabled"), adapters.length > 0 ? summarizeList(adapters) : "run `agendex configure`"));
|
|
6067
|
+
lines.push(row(styles, "Custom dirs", customDirs.length > 0 ? badge(styles, "info", `${customDirs.length} configured`) : badge(styles, "info", "none"), customDirs.length > 0 ? "additional scan roots" : "using built-in agent directories"));
|
|
6068
|
+
for (const dir of customDirs)
|
|
6069
|
+
lines.push(listItem(styles, dir));
|
|
6070
|
+
lines.push("");
|
|
6071
|
+
lines.push(styles.section("Next steps:"));
|
|
6072
|
+
for (const action of nextActions(options, now)) {
|
|
6073
|
+
lines.push(actionRow(styles, action.command, action.description));
|
|
6074
|
+
}
|
|
6075
|
+
return lines.join(`
|
|
6076
|
+
`);
|
|
6077
|
+
}
|
|
6078
|
+
|
|
5679
6079
|
// src/sync.ts
|
|
5680
6080
|
import { hostname as osHostname3 } from "node:os";
|
|
5681
6081
|
async function syncAll(force = false) {
|
|
@@ -5744,7 +6144,7 @@ import { join as join15 } from "node:path";
|
|
|
5744
6144
|
// package.json
|
|
5745
6145
|
var package_default = {
|
|
5746
6146
|
name: "agendex-cli",
|
|
5747
|
-
version: "2.0.
|
|
6147
|
+
version: "2.0.1",
|
|
5748
6148
|
description: "Agendex CLI for login, sync, and daemon workflows",
|
|
5749
6149
|
homepage: "https://github.com/Tyru5/Agendex#readme",
|
|
5750
6150
|
bugs: {
|
|
@@ -6304,18 +6704,15 @@ async function main() {
|
|
|
6304
6704
|
return 0;
|
|
6305
6705
|
}
|
|
6306
6706
|
process.kill(pid, "SIGTERM");
|
|
6307
|
-
const
|
|
6308
|
-
|
|
6309
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
6310
|
-
}
|
|
6311
|
-
if (isRunning(pid)) {
|
|
6707
|
+
const stopped = await waitForProcessExit(pid, 5000);
|
|
6708
|
+
if (!stopped) {
|
|
6312
6709
|
writeStderr("[agendex] daemon did not stop in time");
|
|
6313
|
-
|
|
6314
|
-
removePid();
|
|
6315
|
-
await sendShutdown();
|
|
6316
|
-
writeStdout(`[agendex] daemon stopped (PID ${pid})`);
|
|
6710
|
+
return 1;
|
|
6317
6711
|
}
|
|
6318
|
-
|
|
6712
|
+
removePid();
|
|
6713
|
+
await sendShutdown();
|
|
6714
|
+
writeStdout(`[agendex] daemon stopped (PID ${pid})`);
|
|
6715
|
+
return 0;
|
|
6319
6716
|
}
|
|
6320
6717
|
case "login": {
|
|
6321
6718
|
const urlIdx = args.indexOf("--url");
|
|
@@ -6338,160 +6735,19 @@ async function main() {
|
|
|
6338
6735
|
return 0;
|
|
6339
6736
|
}
|
|
6340
6737
|
case "upload": {
|
|
6341
|
-
return
|
|
6738
|
+
return runUpload(args);
|
|
6342
6739
|
}
|
|
6343
6740
|
case "hooks": {
|
|
6344
|
-
return
|
|
6741
|
+
return runHooksCommand(args, cliEntry);
|
|
6345
6742
|
}
|
|
6346
6743
|
case "review-plan": {
|
|
6347
|
-
return
|
|
6744
|
+
return runHookReviewCommand(args);
|
|
6348
6745
|
}
|
|
6349
6746
|
case "cleanup": {
|
|
6350
|
-
|
|
6351
|
-
if (!config?.cloudToken || !config?.convexUrl) {
|
|
6352
|
-
writeStderr("[agendex] not logged in. Run `agendex login` first.");
|
|
6353
|
-
return 1;
|
|
6354
|
-
}
|
|
6355
|
-
let allDevices;
|
|
6356
|
-
try {
|
|
6357
|
-
allDevices = await fetchDevices();
|
|
6358
|
-
} catch (err) {
|
|
6359
|
-
if (err instanceof AuthExpiredError) {
|
|
6360
|
-
writeStderr("[agendex] cloud token expired. Run `agendex login` to re-authenticate.");
|
|
6361
|
-
return 1;
|
|
6362
|
-
}
|
|
6363
|
-
throw err;
|
|
6364
|
-
}
|
|
6365
|
-
if (allDevices.length === 0) {
|
|
6366
|
-
writeStdout("[agendex] no daemons found");
|
|
6367
|
-
return 0;
|
|
6368
|
-
}
|
|
6369
|
-
const now = Date.now();
|
|
6370
|
-
const staleDevices = allDevices.filter((d2) => {
|
|
6371
|
-
const age = d2.lastSeenAt ? now - d2.lastSeenAt : Number.POSITIVE_INFINITY;
|
|
6372
|
-
return age >= CLI_DAEMON_STALE_AFTER_MS;
|
|
6373
|
-
});
|
|
6374
|
-
if (args.includes("--stale")) {
|
|
6375
|
-
if (staleDevices.length === 0) {
|
|
6376
|
-
writeStdout("[agendex] no stale daemons to remove");
|
|
6377
|
-
return 0;
|
|
6378
|
-
}
|
|
6379
|
-
const staleIds = staleDevices.map((d2) => d2.deviceId).filter((id) => id != null);
|
|
6380
|
-
if (staleIds.length === 0) {
|
|
6381
|
-
writeStdout("[agendex] stale daemons have no device IDs and cannot be removed");
|
|
6382
|
-
return 0;
|
|
6383
|
-
}
|
|
6384
|
-
const result2 = await deleteDaemons(staleIds);
|
|
6385
|
-
if (result2.ok) {
|
|
6386
|
-
writeStdout(`[agendex] removed ${result2.deleted} stale daemon(s)`);
|
|
6387
|
-
} else {
|
|
6388
|
-
writeStderr("[agendex] failed to remove stale daemons");
|
|
6389
|
-
return 1;
|
|
6390
|
-
}
|
|
6391
|
-
return 0;
|
|
6392
|
-
}
|
|
6393
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
6394
|
-
writeStderr("[agendex] interactive cleanup requires a TTY. Use --stale to auto-remove stale daemons.");
|
|
6395
|
-
return 1;
|
|
6396
|
-
}
|
|
6397
|
-
const { promptForDaemonCleanup: promptForDaemonCleanup2 } = await Promise.resolve().then(() => (init_cleanup(), exports_cleanup));
|
|
6398
|
-
const deviceIds = allDevices.filter((d2) => d2.deviceId != null).map((d2) => {
|
|
6399
|
-
const age = d2.lastSeenAt ? now - d2.lastSeenAt : Number.POSITIVE_INFINITY;
|
|
6400
|
-
const status = age < CLI_DAEMON_STALE_AFTER_MS ? "alive" : "stale";
|
|
6401
|
-
return {
|
|
6402
|
-
deviceId: d2.deviceId,
|
|
6403
|
-
hostname: d2.hostname ?? "unknown",
|
|
6404
|
-
pid: d2.pid,
|
|
6405
|
-
status
|
|
6406
|
-
};
|
|
6407
|
-
});
|
|
6408
|
-
if (deviceIds.length === 0) {
|
|
6409
|
-
writeStdout("[agendex] no daemons with device IDs to remove");
|
|
6410
|
-
return 0;
|
|
6411
|
-
}
|
|
6412
|
-
const selected = await promptForDaemonCleanup2(deviceIds);
|
|
6413
|
-
if (!selected)
|
|
6414
|
-
return 0;
|
|
6415
|
-
const result = await deleteDaemons(selected);
|
|
6416
|
-
if (result.ok) {
|
|
6417
|
-
writeStdout(`[agendex] removed ${result.deleted} daemon(s)`);
|
|
6418
|
-
} else {
|
|
6419
|
-
writeStderr("[agendex] failed to remove daemons");
|
|
6420
|
-
return 1;
|
|
6421
|
-
}
|
|
6422
|
-
return 0;
|
|
6747
|
+
return runCleanupCommand(args);
|
|
6423
6748
|
}
|
|
6424
6749
|
case "add-dir": {
|
|
6425
|
-
|
|
6426
|
-
if (!dirPath || !dirPath.trim()) {
|
|
6427
|
-
writeStderr("[agendex] usage: agendex add-dir <path>");
|
|
6428
|
-
return 1;
|
|
6429
|
-
}
|
|
6430
|
-
const resolved = resolveCustomPlanDirPath(dirPath);
|
|
6431
|
-
if (!existsSync13(resolved)) {
|
|
6432
|
-
writeStderr(`[agendex] path does not exist: ${resolved}`);
|
|
6433
|
-
return 1;
|
|
6434
|
-
}
|
|
6435
|
-
if (!statSync2(resolved).isDirectory()) {
|
|
6436
|
-
writeStderr(`[agendex] path is not a directory: ${resolved}`);
|
|
6437
|
-
return 1;
|
|
6438
|
-
}
|
|
6439
|
-
if (args.includes("--live")) {
|
|
6440
|
-
const cfg = loadConfig();
|
|
6441
|
-
const token = cfg?.token;
|
|
6442
|
-
if (!token) {
|
|
6443
|
-
writeStderr("[agendex] no local token found in config — is the server running?");
|
|
6444
|
-
return 1;
|
|
6445
|
-
}
|
|
6446
|
-
const port = process.env.PORT ?? "4890";
|
|
6447
|
-
const { request } = await import("node:http");
|
|
6448
|
-
const body = JSON.stringify({ path: resolved });
|
|
6449
|
-
try {
|
|
6450
|
-
const res = await new Promise((resolve12, reject) => {
|
|
6451
|
-
const req = request(`http://localhost:${port}/api/v1/plan-sources`, {
|
|
6452
|
-
method: "POST",
|
|
6453
|
-
headers: {
|
|
6454
|
-
Authorization: `Bearer ${token}`,
|
|
6455
|
-
"Content-Type": "application/json",
|
|
6456
|
-
"Content-Length": String(Buffer.byteLength(body))
|
|
6457
|
-
}
|
|
6458
|
-
}, (res2) => {
|
|
6459
|
-
let data = "";
|
|
6460
|
-
res2.setEncoding("utf8");
|
|
6461
|
-
res2.on("data", (chunk) => {
|
|
6462
|
-
data += chunk;
|
|
6463
|
-
});
|
|
6464
|
-
res2.on("end", () => resolve12({ status: res2.statusCode ?? 0, body: data }));
|
|
6465
|
-
res2.on("error", reject);
|
|
6466
|
-
});
|
|
6467
|
-
req.on("error", reject);
|
|
6468
|
-
req.write(body);
|
|
6469
|
-
req.end();
|
|
6470
|
-
});
|
|
6471
|
-
if (res.status >= 200 && res.status < 300) {
|
|
6472
|
-
writeStdout(`[agendex] added custom plan dir: ${resolved}`);
|
|
6473
|
-
writeStdout(`[agendex] server notified — scanning + watching now`);
|
|
6474
|
-
} else {
|
|
6475
|
-
writeStderr(`[agendex] server returned ${res.status}: ${res.body}`);
|
|
6476
|
-
return 1;
|
|
6477
|
-
}
|
|
6478
|
-
} catch (err) {
|
|
6479
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
6480
|
-
writeStderr(`[agendex] could not reach local server on port ${port}: ${msg}`);
|
|
6481
|
-
return 1;
|
|
6482
|
-
}
|
|
6483
|
-
} else {
|
|
6484
|
-
const cfg = loadConfig();
|
|
6485
|
-
const currentDirs = cfg?.customPlanDirs ?? [];
|
|
6486
|
-
const updated = normalizeCustomPlanDirs([...currentDirs, resolved]);
|
|
6487
|
-
saveConfig({
|
|
6488
|
-
...cfg ?? { configVersion: 3, enabledAdapters: [] },
|
|
6489
|
-
customPlanDirs: updated
|
|
6490
|
-
});
|
|
6491
|
-
writeStdout(`[agendex] added custom plan dir: ${resolved}`);
|
|
6492
|
-
writeStdout(`[agendex] daemon will pick up the change automatically`);
|
|
6493
|
-
}
|
|
6494
|
-
return 0;
|
|
6750
|
+
return runAddDirCommand(args);
|
|
6495
6751
|
}
|
|
6496
6752
|
case "remove-dir": {
|
|
6497
6753
|
const dirPath = args.find((a) => a !== "remove-dir" && a !== "--dev" && !a.startsWith("--"));
|
|
@@ -6532,110 +6788,40 @@ async function main() {
|
|
|
6532
6788
|
const pidInfo = readPidInfo();
|
|
6533
6789
|
const pid = pidInfo?.pid ?? null;
|
|
6534
6790
|
const running = pid ? isRunning(pid) : false;
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
writeStdout(` - ${dir}`);
|
|
6544
|
-
}
|
|
6545
|
-
writeStdout(`[agendex] Daemon: ${running ? `running (PID ${pid})` : "not running"}`);
|
|
6546
|
-
if (running && pidInfo?.startedAtMs) {
|
|
6547
|
-
writeStdout(`[agendex] Uptime: ${formatDuration(Date.now() - pidInfo.startedAtMs)}`);
|
|
6548
|
-
} else if (running) {
|
|
6549
|
-
writeStdout(`[agendex] Uptime: unknown (restart daemon to populate)`);
|
|
6550
|
-
} else {
|
|
6551
|
-
writeStdout(`[agendex] Uptime: n/a`);
|
|
6552
|
-
}
|
|
6553
|
-
if (running && pidInfo?.hostname) {
|
|
6554
|
-
writeStdout(`[agendex] Hostname: ${pidInfo.hostname}`);
|
|
6555
|
-
} else if (running) {
|
|
6556
|
-
writeStdout(`[agendex] Hostname: unknown (restart daemon to populate)`);
|
|
6557
|
-
} else {
|
|
6558
|
-
writeStdout(`[agendex] Hostname: n/a`);
|
|
6559
|
-
}
|
|
6560
|
-
writeStdout(`[agendex] CLI version: ${CLI_VERSION}`);
|
|
6561
|
-
try {
|
|
6562
|
-
if (config?.cloudToken && config?.convexUrl) {
|
|
6563
|
-
const allDevices = await fetchDevices();
|
|
6564
|
-
if (allDevices.length > 0) {
|
|
6565
|
-
const now = Date.now();
|
|
6566
|
-
const localDeviceId = config.deviceId;
|
|
6567
|
-
writeStdout(`[agendex] All daemons:`);
|
|
6568
|
-
for (const device of allDevices) {
|
|
6569
|
-
const age = device.lastSeenAt ? now - device.lastSeenAt : Number.POSITIVE_INFINITY;
|
|
6570
|
-
const status = age < CLI_DAEMON_STALE_AFTER_MS ? "alive" : "stale";
|
|
6571
|
-
const uptimeStr = device.startedAtMs != null ? formatDuration(now - device.startedAtMs) : "~";
|
|
6572
|
-
const pidStr = device.pid != null ? String(device.pid) : "~";
|
|
6573
|
-
const hostnameStr = device.hostname ?? "~";
|
|
6574
|
-
const ipStr = device.ipAddress ?? "~";
|
|
6575
|
-
const isLocal = localDeviceId && device.deviceId === localDeviceId;
|
|
6576
|
-
writeStdout(`- hostname: ${hostnameStr}${isLocal ? " (this machine)" : ""}
|
|
6577
|
-
ip: ${ipStr}
|
|
6578
|
-
pid: ${pidStr}
|
|
6579
|
-
uptime: ${uptimeStr}
|
|
6580
|
-
status: ${status}`);
|
|
6581
|
-
}
|
|
6791
|
+
let devices = null;
|
|
6792
|
+
let cloudDaemonError = null;
|
|
6793
|
+
if (config?.cloudToken && config?.convexUrl) {
|
|
6794
|
+
try {
|
|
6795
|
+
devices = await fetchDevices();
|
|
6796
|
+
} catch (err) {
|
|
6797
|
+
if (err instanceof AuthExpiredError) {
|
|
6798
|
+
cloudDaemonError = { kind: "auth-expired" };
|
|
6582
6799
|
} else {
|
|
6583
|
-
|
|
6800
|
+
cloudDaemonError = {
|
|
6801
|
+
kind: "unavailable",
|
|
6802
|
+
message: err instanceof Error ? err.message : String(err)
|
|
6803
|
+
};
|
|
6584
6804
|
}
|
|
6585
6805
|
}
|
|
6586
|
-
} catch (err) {
|
|
6587
|
-
if (err instanceof AuthExpiredError) {
|
|
6588
|
-
writeStderr(`[agendex] Cloud token expired — cloud sync and daemon tracking are inactive.`);
|
|
6589
|
-
writeStderr(`[agendex] Run \`agendex login\` to re-authenticate.`);
|
|
6590
|
-
}
|
|
6591
6806
|
}
|
|
6807
|
+
writeStdout(renderStatus({
|
|
6808
|
+
config,
|
|
6809
|
+
configPath: getConfigPath(),
|
|
6810
|
+
pidInfo,
|
|
6811
|
+
running,
|
|
6812
|
+
cliVersion: CLI_VERSION,
|
|
6813
|
+
devices,
|
|
6814
|
+
cloudDaemonError
|
|
6815
|
+
}));
|
|
6592
6816
|
return 0;
|
|
6593
6817
|
}
|
|
6594
6818
|
case "upgrade": {
|
|
6595
|
-
return
|
|
6819
|
+
return runUpgrade({ force: args.includes("--force") });
|
|
6596
6820
|
}
|
|
6597
6821
|
case "help":
|
|
6598
6822
|
case "--help":
|
|
6599
6823
|
case "-h": {
|
|
6600
|
-
writeStdout(
|
|
6601
|
-
agendex - CLI for syncing local agent plans to the cloud
|
|
6602
|
-
|
|
6603
|
-
Usage:
|
|
6604
|
-
agendex Start daemon (default, backgrounds itself)
|
|
6605
|
-
agendex start Start daemon (backgrounds itself)
|
|
6606
|
-
agendex stop Stop the running daemon
|
|
6607
|
-
agendex login Authenticate via browser OAuth (agendex.dev)
|
|
6608
|
-
agendex login --url <url> Login to a self-hosted instance
|
|
6609
|
-
agendex open Open the Agendex web app in your browser
|
|
6610
|
-
agendex open --url <url> Open a self-hosted instance
|
|
6611
|
-
agendex view <url> Open a shared plan URL in your browser
|
|
6612
|
-
agendex logout Clear stored cloud token
|
|
6613
|
-
agendex configure Select which agents/adapters to index
|
|
6614
|
-
agendex hooks status Show Claude Code, Codex, and Pi hook status
|
|
6615
|
-
agendex hooks install <agent|all> Install hook integration (--preview required for claude-code)
|
|
6616
|
-
agendex hooks uninstall <agent|all> Remove managed Agendex hook entries
|
|
6617
|
-
agendex review-plan --hook --agent <agent> Hook-native plan review command
|
|
6618
|
-
agendex add-dir <path> Add a custom directory to scan for plans
|
|
6619
|
-
agendex add-dir <path> --live Add dir and notify running server immediately
|
|
6620
|
-
agendex remove-dir <path> Remove a custom directory
|
|
6621
|
-
agendex list-dirs List custom plan directories
|
|
6622
|
-
agendex sync One-shot scan + sync to cloud (skips unchanged plans)
|
|
6623
|
-
agendex sync --force Re-sync all plans, ignoring cache
|
|
6624
|
-
agendex upload <path> Upload a single Markdown plan file to the cloud
|
|
6625
|
-
agendex upload <path> --agent <name> Override the uploaded plan's agent label
|
|
6626
|
-
agendex upload <path> --open Open the uploaded plan in the browser after upload
|
|
6627
|
-
agendex cleanup Interactively remove cloud daemons
|
|
6628
|
-
agendex cleanup --stale Auto-remove all stale daemons
|
|
6629
|
-
agendex status Show current config state + daemon status
|
|
6630
|
-
agendex upgrade Upgrade the globally installed CLI to the latest version
|
|
6631
|
-
agendex upgrade --force Reinstall latest even if already up to date
|
|
6632
|
-
agendex help Show this help message
|
|
6633
|
-
agendex --version Print CLI version
|
|
6634
|
-
agendex -v Print CLI version
|
|
6635
|
-
|
|
6636
|
-
Flags:
|
|
6637
|
-
--dev Use dev environment (~/.agendex-dev/ config dir)
|
|
6638
|
-
`.trim());
|
|
6824
|
+
writeStdout(renderHelp({ cliVersion: CLI_VERSION }));
|
|
6639
6825
|
return 0;
|
|
6640
6826
|
}
|
|
6641
6827
|
default: {
|
|
@@ -6645,6 +6831,173 @@ Flags:
|
|
|
6645
6831
|
}
|
|
6646
6832
|
}
|
|
6647
6833
|
}
|
|
6834
|
+
function waitForProcessExit(pid, timeoutMs) {
|
|
6835
|
+
return new Promise((resolve12) => {
|
|
6836
|
+
const deadline = Date.now() + timeoutMs;
|
|
6837
|
+
const interval = setInterval(() => {
|
|
6838
|
+
if (!isRunning(pid)) {
|
|
6839
|
+
clearInterval(interval);
|
|
6840
|
+
resolve12(true);
|
|
6841
|
+
return;
|
|
6842
|
+
}
|
|
6843
|
+
if (Date.now() >= deadline) {
|
|
6844
|
+
clearInterval(interval);
|
|
6845
|
+
resolve12(false);
|
|
6846
|
+
}
|
|
6847
|
+
}, 200);
|
|
6848
|
+
});
|
|
6849
|
+
}
|
|
6850
|
+
async function runCleanupCommand(commandArgs) {
|
|
6851
|
+
const config = loadConfig();
|
|
6852
|
+
if (!config?.cloudToken || !config?.convexUrl) {
|
|
6853
|
+
writeStderr("[agendex] not logged in. Run `agendex login` first.");
|
|
6854
|
+
return 1;
|
|
6855
|
+
}
|
|
6856
|
+
let allDevices;
|
|
6857
|
+
try {
|
|
6858
|
+
allDevices = await fetchDevices();
|
|
6859
|
+
} catch (err) {
|
|
6860
|
+
if (err instanceof AuthExpiredError) {
|
|
6861
|
+
writeStderr("[agendex] cloud token expired. Run `agendex login` to re-authenticate.");
|
|
6862
|
+
return 1;
|
|
6863
|
+
}
|
|
6864
|
+
throw err;
|
|
6865
|
+
}
|
|
6866
|
+
if (allDevices.length === 0) {
|
|
6867
|
+
writeStdout("[agendex] no daemons found");
|
|
6868
|
+
return 0;
|
|
6869
|
+
}
|
|
6870
|
+
const now = Date.now();
|
|
6871
|
+
const staleDevices = allDevices.filter((device) => {
|
|
6872
|
+
const age = device.lastSeenAt !== null ? now - device.lastSeenAt : Number.POSITIVE_INFINITY;
|
|
6873
|
+
return age >= CLI_DAEMON_STALE_AFTER_MS;
|
|
6874
|
+
});
|
|
6875
|
+
if (commandArgs.includes("--stale")) {
|
|
6876
|
+
if (staleDevices.length === 0) {
|
|
6877
|
+
writeStdout("[agendex] no stale daemons to remove");
|
|
6878
|
+
return 0;
|
|
6879
|
+
}
|
|
6880
|
+
const staleIds = staleDevices.flatMap((device) => device.deviceId === null ? [] : [device.deviceId]);
|
|
6881
|
+
if (staleIds.length === 0) {
|
|
6882
|
+
writeStdout("[agendex] stale daemons have no device IDs and cannot be removed");
|
|
6883
|
+
return 0;
|
|
6884
|
+
}
|
|
6885
|
+
const result2 = await deleteDaemons(staleIds);
|
|
6886
|
+
if (result2.ok) {
|
|
6887
|
+
writeStdout(`[agendex] removed ${result2.deleted} stale daemon(s)`);
|
|
6888
|
+
} else {
|
|
6889
|
+
writeStderr("[agendex] failed to remove stale daemons");
|
|
6890
|
+
return 1;
|
|
6891
|
+
}
|
|
6892
|
+
return 0;
|
|
6893
|
+
}
|
|
6894
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
6895
|
+
writeStderr("[agendex] interactive cleanup requires a TTY. Use --stale to auto-remove stale daemons.");
|
|
6896
|
+
return 1;
|
|
6897
|
+
}
|
|
6898
|
+
const { promptForDaemonCleanup: promptForDaemonCleanup2 } = await Promise.resolve().then(() => (init_cleanup(), exports_cleanup));
|
|
6899
|
+
const deviceIds = allDevices.flatMap((device) => {
|
|
6900
|
+
if (device.deviceId === null)
|
|
6901
|
+
return [];
|
|
6902
|
+
const age = device.lastSeenAt !== null ? now - device.lastSeenAt : Number.POSITIVE_INFINITY;
|
|
6903
|
+
const status = age < CLI_DAEMON_STALE_AFTER_MS ? "alive" : "stale";
|
|
6904
|
+
return [
|
|
6905
|
+
{
|
|
6906
|
+
deviceId: device.deviceId,
|
|
6907
|
+
hostname: device.hostname ?? "unknown",
|
|
6908
|
+
pid: device.pid,
|
|
6909
|
+
status
|
|
6910
|
+
}
|
|
6911
|
+
];
|
|
6912
|
+
});
|
|
6913
|
+
if (deviceIds.length === 0) {
|
|
6914
|
+
writeStdout("[agendex] no daemons with device IDs to remove");
|
|
6915
|
+
return 0;
|
|
6916
|
+
}
|
|
6917
|
+
const selected = await promptForDaemonCleanup2(deviceIds);
|
|
6918
|
+
if (!selected)
|
|
6919
|
+
return 0;
|
|
6920
|
+
const result = await deleteDaemons(selected);
|
|
6921
|
+
if (result.ok) {
|
|
6922
|
+
writeStdout(`[agendex] removed ${result.deleted} daemon(s)`);
|
|
6923
|
+
} else {
|
|
6924
|
+
writeStderr("[agendex] failed to remove daemons");
|
|
6925
|
+
return 1;
|
|
6926
|
+
}
|
|
6927
|
+
return 0;
|
|
6928
|
+
}
|
|
6929
|
+
async function runAddDirCommand(commandArgs) {
|
|
6930
|
+
const dirPath = commandArgs.find((arg) => arg !== "add-dir" && arg !== "--dev" && !arg.startsWith("--"));
|
|
6931
|
+
if (dirPath === undefined || dirPath.trim() === "") {
|
|
6932
|
+
writeStderr("[agendex] usage: agendex add-dir <path>");
|
|
6933
|
+
return 1;
|
|
6934
|
+
}
|
|
6935
|
+
const resolved = resolveCustomPlanDirPath(dirPath);
|
|
6936
|
+
if (!existsSync13(resolved)) {
|
|
6937
|
+
writeStderr(`[agendex] path does not exist: ${resolved}`);
|
|
6938
|
+
return 1;
|
|
6939
|
+
}
|
|
6940
|
+
if (!statSync2(resolved).isDirectory()) {
|
|
6941
|
+
writeStderr(`[agendex] path is not a directory: ${resolved}`);
|
|
6942
|
+
return 1;
|
|
6943
|
+
}
|
|
6944
|
+
if (commandArgs.includes("--live")) {
|
|
6945
|
+
const cfg = loadConfig();
|
|
6946
|
+
const token = cfg?.token;
|
|
6947
|
+
if (!token) {
|
|
6948
|
+
writeStderr("[agendex] no local token found in config — is the server running?");
|
|
6949
|
+
return 1;
|
|
6950
|
+
}
|
|
6951
|
+
const port = process.env.PORT ?? "4890";
|
|
6952
|
+
const { request } = await import("node:http");
|
|
6953
|
+
const body = JSON.stringify({ path: resolved });
|
|
6954
|
+
try {
|
|
6955
|
+
const res = await new Promise((resolve12, reject) => {
|
|
6956
|
+
const req = request(`http://localhost:${port}/api/v1/plan-sources`, {
|
|
6957
|
+
method: "POST",
|
|
6958
|
+
headers: {
|
|
6959
|
+
Authorization: `Bearer ${token}`,
|
|
6960
|
+
"Content-Type": "application/json",
|
|
6961
|
+
"Content-Length": String(Buffer.byteLength(body))
|
|
6962
|
+
}
|
|
6963
|
+
}, (res2) => {
|
|
6964
|
+
let data = "";
|
|
6965
|
+
res2.setEncoding("utf8");
|
|
6966
|
+
res2.on("data", (chunk) => {
|
|
6967
|
+
data += chunk;
|
|
6968
|
+
});
|
|
6969
|
+
res2.on("end", () => resolve12({ status: res2.statusCode ?? 0, body: data }));
|
|
6970
|
+
res2.on("error", reject);
|
|
6971
|
+
});
|
|
6972
|
+
req.on("error", reject);
|
|
6973
|
+
req.write(body);
|
|
6974
|
+
req.end();
|
|
6975
|
+
});
|
|
6976
|
+
if (res.status >= 200 && res.status < 300) {
|
|
6977
|
+
writeStdout(`[agendex] added custom plan dir: ${resolved}`);
|
|
6978
|
+
writeStdout(`[agendex] server notified — scanning + watching now`);
|
|
6979
|
+
} else {
|
|
6980
|
+
writeStderr(`[agendex] server returned ${res.status}: ${res.body}`);
|
|
6981
|
+
return 1;
|
|
6982
|
+
}
|
|
6983
|
+
} catch (err) {
|
|
6984
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6985
|
+
writeStderr(`[agendex] could not reach local server on port ${port}: ${msg}`);
|
|
6986
|
+
return 1;
|
|
6987
|
+
}
|
|
6988
|
+
} else {
|
|
6989
|
+
const cfg = loadConfig();
|
|
6990
|
+
const currentDirs = cfg?.customPlanDirs ?? [];
|
|
6991
|
+
const updated = normalizeCustomPlanDirs([...currentDirs, resolved]);
|
|
6992
|
+
saveConfig({
|
|
6993
|
+
...cfg ?? { configVersion: 3, enabledAdapters: [] },
|
|
6994
|
+
customPlanDirs: updated
|
|
6995
|
+
});
|
|
6996
|
+
writeStdout(`[agendex] added custom plan dir: ${resolved}`);
|
|
6997
|
+
writeStdout(`[agendex] daemon will pick up the change automatically`);
|
|
6998
|
+
}
|
|
6999
|
+
return 0;
|
|
7000
|
+
}
|
|
6648
7001
|
var exitCode = await main().catch((err) => {
|
|
6649
7002
|
writeStderr(`[agendex] ${err instanceof Error ? err.message : err}`);
|
|
6650
7003
|
return 1;
|
|
@@ -6675,17 +7028,3 @@ function writeStderr(message) {
|
|
|
6675
7028
|
writeSync(process.stderr.fd, `${message}
|
|
6676
7029
|
`);
|
|
6677
7030
|
}
|
|
6678
|
-
function formatDuration(ms) {
|
|
6679
|
-
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
6680
|
-
const days = Math.floor(totalSeconds / 86400);
|
|
6681
|
-
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
6682
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
6683
|
-
const seconds = totalSeconds % 60;
|
|
6684
|
-
if (days > 0)
|
|
6685
|
-
return `${days}d ${hours}h ${minutes}m ${seconds}s`;
|
|
6686
|
-
if (hours > 0)
|
|
6687
|
-
return `${hours}h ${minutes}m ${seconds}s`;
|
|
6688
|
-
if (minutes > 0)
|
|
6689
|
-
return `${minutes}m ${seconds}s`;
|
|
6690
|
-
return `${seconds}s`;
|
|
6691
|
-
}
|