@arkhera30/cli 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +424 -402
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,12 +6,12 @@ import chalk10 from "chalk";
|
|
|
6
6
|
import { createRequire } from "module";
|
|
7
7
|
|
|
8
8
|
// src/commands/setup.ts
|
|
9
|
-
import { Command } from "commander";
|
|
10
|
-
import
|
|
11
|
-
import
|
|
9
|
+
import { Command as Command2 } from "commander";
|
|
10
|
+
import chalk2 from "chalk";
|
|
11
|
+
import ora2 from "ora";
|
|
12
12
|
import { execSync } from "child_process";
|
|
13
|
-
import { existsSync as
|
|
14
|
-
import { join as
|
|
13
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
14
|
+
import { join as join4 } from "path";
|
|
15
15
|
import { input, confirm, number, select, password } from "@inquirer/prompts";
|
|
16
16
|
|
|
17
17
|
// src/lib/config.ts
|
|
@@ -455,6 +455,275 @@ function installComposeFile(runtime) {
|
|
|
455
455
|
writeFileSync2(COMPOSE_PATH, content, "utf-8");
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
// src/commands/connect.ts
|
|
459
|
+
import { Command } from "commander";
|
|
460
|
+
import chalk from "chalk";
|
|
461
|
+
import ora from "ora";
|
|
462
|
+
import { checkbox } from "@inquirer/prompts";
|
|
463
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
464
|
+
import { join as join3 } from "path";
|
|
465
|
+
import { homedir as homedir3 } from "os";
|
|
466
|
+
import { execa as execa2 } from "execa";
|
|
467
|
+
function detectInstalledClients() {
|
|
468
|
+
const detected = [];
|
|
469
|
+
const home = homedir3();
|
|
470
|
+
const claudeDesktopDir = join3(home, "Library", "Application Support", "Claude");
|
|
471
|
+
if (existsSync3(claudeDesktopDir)) {
|
|
472
|
+
detected.push("claude-desktop");
|
|
473
|
+
}
|
|
474
|
+
const claudeCodeDir = join3(home, ".claude");
|
|
475
|
+
if (existsSync3(claudeCodeDir)) {
|
|
476
|
+
detected.push("claude-code");
|
|
477
|
+
}
|
|
478
|
+
const cursorDir = join3(home, ".cursor");
|
|
479
|
+
const cursorAppDir = join3(home, "Library", "Application Support", "Cursor");
|
|
480
|
+
if (existsSync3(cursorDir) || existsSync3(cursorAppDir)) {
|
|
481
|
+
detected.push("cursor");
|
|
482
|
+
}
|
|
483
|
+
return detected;
|
|
484
|
+
}
|
|
485
|
+
function getConfigPath(target) {
|
|
486
|
+
const home = homedir3();
|
|
487
|
+
switch (target) {
|
|
488
|
+
case "claude-desktop":
|
|
489
|
+
return join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
490
|
+
case "claude-code":
|
|
491
|
+
return join3(home, ".claude", "settings.json");
|
|
492
|
+
case "cursor":
|
|
493
|
+
return join3(home, ".cursor", "mcp.json");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function mergeAndWriteConfig(configPath, mcpServers) {
|
|
497
|
+
let existing = {};
|
|
498
|
+
if (existsSync3(configPath)) {
|
|
499
|
+
try {
|
|
500
|
+
const raw = readFileSync3(configPath, "utf-8");
|
|
501
|
+
existing = JSON.parse(raw);
|
|
502
|
+
} catch {
|
|
503
|
+
existing = {};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const existingServers = existing.mcpServers ?? {};
|
|
507
|
+
existing.mcpServers = { ...existingServers, ...mcpServers };
|
|
508
|
+
const dir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
509
|
+
mkdirSync2(dir, { recursive: true });
|
|
510
|
+
writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
511
|
+
}
|
|
512
|
+
async function isClaudeCliAvailable() {
|
|
513
|
+
try {
|
|
514
|
+
const result = await execa2("claude", ["--version"], { reject: false });
|
|
515
|
+
return result.exitCode === 0;
|
|
516
|
+
} catch {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function registerWithClaudeCode(mcpServers) {
|
|
521
|
+
const registered = [];
|
|
522
|
+
const failed = [];
|
|
523
|
+
for (const [name, entry] of Object.entries(mcpServers)) {
|
|
524
|
+
const baseUrl = entry.url.replace(/\/sse$/, "");
|
|
525
|
+
const result = await execa2(
|
|
526
|
+
"claude",
|
|
527
|
+
["mcp", "add", "--transport", "http", "--scope", "user", name, baseUrl],
|
|
528
|
+
{ reject: false }
|
|
529
|
+
);
|
|
530
|
+
if (result.exitCode === 0) {
|
|
531
|
+
registered.push(name);
|
|
532
|
+
} else {
|
|
533
|
+
failed.push(name);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return { registered, failed };
|
|
537
|
+
}
|
|
538
|
+
async function syncSkills(runtime) {
|
|
539
|
+
const home = homedir3();
|
|
540
|
+
const skillsBase = join3(home, ".claude", "skills");
|
|
541
|
+
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
542
|
+
const forgeContainer = "horus-forge-1";
|
|
543
|
+
for (const skill of skills) {
|
|
544
|
+
const destDir = join3(skillsBase, skill);
|
|
545
|
+
mkdirSync2(destDir, { recursive: true });
|
|
546
|
+
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
547
|
+
const dest = join3(destDir, "SKILL.md");
|
|
548
|
+
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
549
|
+
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
550
|
+
writeFileSync3(dest, result.stdout, "utf-8");
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async function syncSkillsForCursor(runtime) {
|
|
555
|
+
const home = homedir3();
|
|
556
|
+
const rulesDir = join3(home, ".cursor", "rules");
|
|
557
|
+
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
558
|
+
const forgeContainer = "horus-forge-1";
|
|
559
|
+
mkdirSync2(rulesDir, { recursive: true });
|
|
560
|
+
for (const skill of skills) {
|
|
561
|
+
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
562
|
+
const dest = join3(rulesDir, `${skill}.mdc`);
|
|
563
|
+
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
564
|
+
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
565
|
+
const frontmatter = `---
|
|
566
|
+
description: Horus ${skill} reference
|
|
567
|
+
alwaysApply: true
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
`;
|
|
571
|
+
writeFileSync3(dest, frontmatter + result.stdout, "utf-8");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function printNextSteps(targets) {
|
|
576
|
+
console.log("");
|
|
577
|
+
console.log(chalk.bold("Next steps:"));
|
|
578
|
+
for (const target of targets) {
|
|
579
|
+
switch (target) {
|
|
580
|
+
case "claude-desktop":
|
|
581
|
+
console.log(` ${chalk.cyan("Claude Desktop")} Restart Claude Desktop to pick up the new MCP configuration`);
|
|
582
|
+
break;
|
|
583
|
+
case "claude-code":
|
|
584
|
+
console.log(` ${chalk.cyan("Claude Code")} Start a new Claude Code session`);
|
|
585
|
+
break;
|
|
586
|
+
case "cursor":
|
|
587
|
+
console.log(` ${chalk.cyan("Cursor")} Restart Cursor to pick up the new MCP configuration and rules`);
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
console.log("");
|
|
592
|
+
}
|
|
593
|
+
async function runConnect(config, runtime, targets, host = "localhost") {
|
|
594
|
+
const mcpServers = {
|
|
595
|
+
anvil: { url: `http://${host}:${config.ports.anvil}/sse` },
|
|
596
|
+
vault: { url: `http://${host}:${config.ports.vault_mcp}/sse` },
|
|
597
|
+
forge: { url: `http://${host}:${config.ports.forge}/sse` }
|
|
598
|
+
};
|
|
599
|
+
const configured = [];
|
|
600
|
+
for (const target of targets) {
|
|
601
|
+
if (target === "claude-code") {
|
|
602
|
+
const cliSpinner = ora("Registering MCP servers with Claude Code CLI...").start();
|
|
603
|
+
const cliAvailable = await isClaudeCliAvailable();
|
|
604
|
+
if (cliAvailable) {
|
|
605
|
+
const { registered, failed } = await registerWithClaudeCode(mcpServers);
|
|
606
|
+
if (failed.length === 0) {
|
|
607
|
+
cliSpinner.succeed(
|
|
608
|
+
`Registered with Claude Code: ${registered.map((n) => chalk.cyan(n)).join(", ")}`
|
|
609
|
+
);
|
|
610
|
+
configured.push(target);
|
|
611
|
+
} else if (registered.length > 0) {
|
|
612
|
+
cliSpinner.warn(
|
|
613
|
+
`Partially registered \u2014 ok: ${registered.join(", ")}, failed: ${failed.join(", ")}`
|
|
614
|
+
);
|
|
615
|
+
configured.push(target);
|
|
616
|
+
} else {
|
|
617
|
+
cliSpinner.fail("Failed to register MCP servers with Claude Code CLI");
|
|
618
|
+
}
|
|
619
|
+
} else {
|
|
620
|
+
cliSpinner.warn("claude CLI not found on PATH \u2014 register manually:");
|
|
621
|
+
for (const [name, entry] of Object.entries(mcpServers)) {
|
|
622
|
+
const baseUrl = entry.url.replace(/\/sse$/, "");
|
|
623
|
+
console.log(
|
|
624
|
+
chalk.dim(` claude mcp add --transport http --scope user ${name} ${baseUrl}`)
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
const configPath = getConfigPath(target);
|
|
630
|
+
const writeSpinner = ora(`Configuring ${chalk.cyan(target)}...`).start();
|
|
631
|
+
try {
|
|
632
|
+
mergeAndWriteConfig(configPath, mcpServers);
|
|
633
|
+
writeSpinner.succeed(`Configured ${chalk.cyan(target)} \u2014 ${chalk.dim(configPath)}`);
|
|
634
|
+
configured.push(target);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
writeSpinner.fail(`Failed to configure ${target}`);
|
|
637
|
+
console.log(chalk.dim(error.message));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (targets.includes("claude-code")) {
|
|
642
|
+
const skillsSpinner = ora("Syncing horus-core skills...").start();
|
|
643
|
+
try {
|
|
644
|
+
await syncSkills(runtime);
|
|
645
|
+
skillsSpinner.succeed("horus-core skills synced to ~/.claude/skills/");
|
|
646
|
+
} catch (error) {
|
|
647
|
+
skillsSpinner.warn("Could not sync skills (Forge container may not be running)");
|
|
648
|
+
console.log(chalk.dim(error.message));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (targets.includes("cursor")) {
|
|
652
|
+
const cursorRulesSpinner = ora("Syncing horus-core rules for Cursor...").start();
|
|
653
|
+
try {
|
|
654
|
+
await syncSkillsForCursor(runtime);
|
|
655
|
+
cursorRulesSpinner.succeed("horus-core rules synced to ~/.cursor/rules/");
|
|
656
|
+
} catch (error) {
|
|
657
|
+
cursorRulesSpinner.warn("Could not sync Cursor rules (Forge container may not be running)");
|
|
658
|
+
console.log(chalk.dim(error.message));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (configured.length > 0) {
|
|
662
|
+
printNextSteps(configured);
|
|
663
|
+
}
|
|
664
|
+
return configured;
|
|
665
|
+
}
|
|
666
|
+
var connectCommand = new Command("connect").description("Configure Claude/Cursor MCP integration").option("--target <client>", "Client to configure: claude-desktop, claude-code, cursor, all (default: auto-detect)").option("--host <host>", "MCP host (default: localhost)", "localhost").option("-y, --yes", "Skip confirmation prompts").action(async (opts) => {
|
|
667
|
+
console.log("");
|
|
668
|
+
console.log(chalk.bold("Horus Connect"));
|
|
669
|
+
console.log(chalk.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
670
|
+
console.log("");
|
|
671
|
+
const config = loadConfig();
|
|
672
|
+
const runtimeSpinner = ora("Detecting runtime...").start();
|
|
673
|
+
let runtime;
|
|
674
|
+
try {
|
|
675
|
+
runtime = await detectRuntime(config.runtime);
|
|
676
|
+
runtimeSpinner.succeed(`Using ${chalk.cyan(runtime.name)}`);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
runtimeSpinner.fail("No container runtime found");
|
|
679
|
+
console.log(error.message);
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
const runningSpinner = ora("Checking Horus status...").start();
|
|
683
|
+
const running = await runtime.isRunning();
|
|
684
|
+
if (!running) {
|
|
685
|
+
runningSpinner.fail("Horus is not running");
|
|
686
|
+
console.log(chalk.dim("Run `horus up` first, then re-run `horus connect`."));
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
runningSpinner.succeed("Horus is running");
|
|
690
|
+
let targets = [];
|
|
691
|
+
if (opts.target === "all") {
|
|
692
|
+
targets = ["claude-desktop", "claude-code", "cursor"];
|
|
693
|
+
} else if (opts.target) {
|
|
694
|
+
const valid = ["claude-desktop", "claude-code", "cursor"];
|
|
695
|
+
if (!valid.includes(opts.target)) {
|
|
696
|
+
console.log(chalk.red(`Invalid target: ${opts.target}`));
|
|
697
|
+
console.log(chalk.dim("Valid targets: claude-desktop, claude-code, cursor, all"));
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
targets = [opts.target];
|
|
701
|
+
} else {
|
|
702
|
+
const detected = detectInstalledClients();
|
|
703
|
+
if (detected.length === 0) {
|
|
704
|
+
console.log(chalk.yellow("No supported clients detected (Claude Desktop, Claude Code, or Cursor)."));
|
|
705
|
+
console.log(chalk.dim("Use --target to specify a client manually."));
|
|
706
|
+
process.exit(1);
|
|
707
|
+
}
|
|
708
|
+
if (opts.yes) {
|
|
709
|
+
targets = detected;
|
|
710
|
+
console.log(`Detected clients: ${detected.map((t) => chalk.cyan(t)).join(", ")}`);
|
|
711
|
+
} else {
|
|
712
|
+
const chosen = await checkbox({
|
|
713
|
+
message: "Select clients to configure:",
|
|
714
|
+
choices: detected.map((t) => ({ name: t, value: t, checked: true })),
|
|
715
|
+
validate: (input2) => input2.length > 0 ? true : "Select at least one client."
|
|
716
|
+
});
|
|
717
|
+
targets = chosen;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (targets.length === 0) {
|
|
721
|
+
console.log(chalk.yellow("No clients selected. Exiting."));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
await runConnect(config, runtime, targets, opts.host);
|
|
725
|
+
});
|
|
726
|
+
|
|
458
727
|
// src/commands/setup.ts
|
|
459
728
|
function injectToken(url, token) {
|
|
460
729
|
if (!token) return url;
|
|
@@ -467,26 +736,26 @@ function injectToken(url, token) {
|
|
|
467
736
|
return url;
|
|
468
737
|
}
|
|
469
738
|
}
|
|
470
|
-
var setupCommand = new
|
|
739
|
+
var setupCommand = new Command2("setup").description("Interactive first-run setup for Horus").option("-y, --yes", "Non-interactive mode (use defaults + env vars)").option("--runtime <runtime>", "Container runtime to use: docker or podman (non-interactive only)").option("--data-dir <path>", "Data directory path").option("--repos-path <path>", "Host repos path for Forge scanning").option("--git-host <host>", "Git server hostname (e.g., github.com, gitlab.corp.com)").option("--anvil-repo <url>", "Anvil notes repository URL").option("--vault-repo <url>", "Vault knowledge-base repository URL").option("--forge-repo <url>", "Forge registry repository URL").option("--github-token <token>", "GitHub personal access token for private repos").action(async (opts) => {
|
|
471
740
|
console.log("");
|
|
472
|
-
console.log(
|
|
473
|
-
console.log(
|
|
741
|
+
console.log(chalk2.bold("Horus Setup"));
|
|
742
|
+
console.log(chalk2.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
474
743
|
console.log("");
|
|
475
744
|
if (configExists()) {
|
|
476
745
|
if (opts.yes) {
|
|
477
|
-
console.log(
|
|
746
|
+
console.log(chalk2.yellow("Existing configuration found. Overwriting in non-interactive mode."));
|
|
478
747
|
} else {
|
|
479
748
|
const proceed = await confirm({
|
|
480
749
|
message: "Horus is already configured. Reconfigure?",
|
|
481
750
|
default: false
|
|
482
751
|
});
|
|
483
752
|
if (!proceed) {
|
|
484
|
-
console.log(
|
|
753
|
+
console.log(chalk2.dim("Setup cancelled."));
|
|
485
754
|
return;
|
|
486
755
|
}
|
|
487
756
|
}
|
|
488
757
|
}
|
|
489
|
-
const checkSpinner =
|
|
758
|
+
const checkSpinner = ora2("Checking for container runtimes...").start();
|
|
490
759
|
const [hasDocker, hasPodman] = await Promise.all([
|
|
491
760
|
checkRuntime("docker"),
|
|
492
761
|
checkRuntime("podman")
|
|
@@ -497,7 +766,7 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
497
766
|
...hasPodman ? ["podman"] : []
|
|
498
767
|
];
|
|
499
768
|
if (available.length === 0) {
|
|
500
|
-
console.log(
|
|
769
|
+
console.log(chalk2.red("No container runtime found."));
|
|
501
770
|
console.log("");
|
|
502
771
|
console.log("Horus requires Docker or Podman with the Compose plugin.");
|
|
503
772
|
console.log("");
|
|
@@ -510,12 +779,12 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
510
779
|
if (opts.yes) {
|
|
511
780
|
const requested = opts.runtime;
|
|
512
781
|
if (requested && !available.includes(requested)) {
|
|
513
|
-
console.log(
|
|
514
|
-
console.log(
|
|
782
|
+
console.log(chalk2.red(`Requested runtime "${requested}" is not installed.`));
|
|
783
|
+
console.log(chalk2.dim(`Available: ${available.join(", ")}`));
|
|
515
784
|
process.exit(1);
|
|
516
785
|
}
|
|
517
786
|
selectedRuntime = requested ?? available[0];
|
|
518
|
-
console.log(`Using ${
|
|
787
|
+
console.log(`Using ${chalk2.cyan(selectedRuntime)}`);
|
|
519
788
|
} else {
|
|
520
789
|
selectedRuntime = await select({
|
|
521
790
|
message: "Which container runtime would you like to use?",
|
|
@@ -586,19 +855,19 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
586
855
|
};
|
|
587
856
|
}
|
|
588
857
|
console.log("");
|
|
589
|
-
console.log(
|
|
590
|
-
console.log(
|
|
591
|
-
console.log(
|
|
858
|
+
console.log(chalk2.bold("Repository Configuration"));
|
|
859
|
+
console.log(chalk2.dim("Horus stores notes and knowledge in Git repos you own."));
|
|
860
|
+
console.log(chalk2.dim("Create empty repos on your Git server, then paste the URLs below."));
|
|
592
861
|
console.log("");
|
|
593
|
-
console.log(
|
|
594
|
-
console.log(
|
|
862
|
+
console.log(chalk2.yellow(" Use HTTPS URLs \u2014 container services do not have SSH keys."));
|
|
863
|
+
console.log(chalk2.dim(" SSH URLs (git@github.com:...) will fail at runtime inside Docker/Podman."));
|
|
595
864
|
console.log("");
|
|
596
865
|
const git_host = await input({
|
|
597
866
|
message: "Git server hostname:",
|
|
598
867
|
default: "github.com"
|
|
599
868
|
});
|
|
600
869
|
const host = git_host.trim();
|
|
601
|
-
const example = (repo) =>
|
|
870
|
+
const example = (repo) => chalk2.dim(` e.g., https://${host}/<owner>/${repo}`);
|
|
602
871
|
console.log("");
|
|
603
872
|
const anvil_notes = await input({
|
|
604
873
|
message: `Anvil notes repo URL:
|
|
@@ -619,8 +888,8 @@ ${example("forge-registry")}
|
|
|
619
888
|
validate: (v) => v.trim().length > 0 || "Forge needs a registry repo."
|
|
620
889
|
});
|
|
621
890
|
console.log("");
|
|
622
|
-
console.log(
|
|
623
|
-
console.log(
|
|
891
|
+
console.log(chalk2.bold("Authentication"));
|
|
892
|
+
console.log(chalk2.dim("A personal access token is required for private repositories."));
|
|
624
893
|
console.log("");
|
|
625
894
|
const github_token = await password({
|
|
626
895
|
message: "GitHub personal access token (leave empty to skip):",
|
|
@@ -642,7 +911,7 @@ ${example("forge-registry")}
|
|
|
642
911
|
github_token: github_token.trim()
|
|
643
912
|
};
|
|
644
913
|
}
|
|
645
|
-
const configSpinner =
|
|
914
|
+
const configSpinner = ora2("Saving configuration...").start();
|
|
646
915
|
try {
|
|
647
916
|
saveConfig(config);
|
|
648
917
|
configSpinner.succeed("Configuration saved to ~/.horus/config.yaml");
|
|
@@ -651,7 +920,7 @@ ${example("forge-registry")}
|
|
|
651
920
|
console.error(error.message);
|
|
652
921
|
process.exit(1);
|
|
653
922
|
}
|
|
654
|
-
const envSpinner =
|
|
923
|
+
const envSpinner = ora2("Generating .env file...").start();
|
|
655
924
|
try {
|
|
656
925
|
writeEnvFile(config);
|
|
657
926
|
envSpinner.succeed("Environment file written to ~/.horus/.env");
|
|
@@ -660,7 +929,7 @@ ${example("forge-registry")}
|
|
|
660
929
|
console.error(error.message);
|
|
661
930
|
process.exit(1);
|
|
662
931
|
}
|
|
663
|
-
const composeSpinner =
|
|
932
|
+
const composeSpinner = ora2("Installing docker-compose.yml...").start();
|
|
664
933
|
try {
|
|
665
934
|
installComposeFile(runtime.name);
|
|
666
935
|
composeSpinner.succeed("Compose file installed to ~/.horus/docker-compose.yml");
|
|
@@ -669,24 +938,24 @@ ${example("forge-registry")}
|
|
|
669
938
|
console.error(error.message);
|
|
670
939
|
process.exit(1);
|
|
671
940
|
}
|
|
672
|
-
const dataDir = config.data_dir.startsWith("~") ?
|
|
941
|
+
const dataDir = config.data_dir.startsWith("~") ? join4(process.env.HOME || "", config.data_dir.slice(1)) : config.data_dir;
|
|
673
942
|
const reposToClone = [
|
|
674
|
-
{ url: config.repos.anvil_notes, dest:
|
|
675
|
-
{ url: config.repos.vault_knowledge, dest:
|
|
676
|
-
{ url: config.repos.forge_registry, dest:
|
|
943
|
+
{ url: config.repos.anvil_notes, dest: join4(dataDir, "notes"), label: "Anvil notes" },
|
|
944
|
+
{ url: config.repos.vault_knowledge, dest: join4(dataDir, "knowledge-base"), label: "Vault knowledge-base" },
|
|
945
|
+
{ url: config.repos.forge_registry, dest: join4(dataDir, "registry"), label: "Forge registry" }
|
|
677
946
|
].filter((r) => r.url);
|
|
678
947
|
if (reposToClone.length > 0) {
|
|
679
948
|
console.log("");
|
|
680
|
-
console.log(
|
|
681
|
-
|
|
949
|
+
console.log(chalk2.bold("Cloning repositories..."));
|
|
950
|
+
mkdirSync3(dataDir, { recursive: true });
|
|
682
951
|
for (const repo of reposToClone) {
|
|
683
|
-
const spinner =
|
|
684
|
-
if (
|
|
952
|
+
const spinner = ora2(`Cloning ${repo.label}...`).start();
|
|
953
|
+
if (existsSync4(join4(repo.dest, ".git"))) {
|
|
685
954
|
spinner.succeed(`${repo.label} already cloned`);
|
|
686
955
|
continue;
|
|
687
956
|
}
|
|
688
957
|
try {
|
|
689
|
-
|
|
958
|
+
mkdirSync3(repo.dest, { recursive: true });
|
|
690
959
|
const cloneUrl = injectToken(repo.url, config.github_token);
|
|
691
960
|
execSync(`git clone "${cloneUrl}" "${repo.dest}"`, {
|
|
692
961
|
stdio: "pipe",
|
|
@@ -697,37 +966,37 @@ ${example("forge-registry")}
|
|
|
697
966
|
spinner.fail(`Failed to clone ${repo.label}`);
|
|
698
967
|
const msg = error.message || "";
|
|
699
968
|
if (msg.includes("already exists and is not an empty directory")) {
|
|
700
|
-
console.log(
|
|
969
|
+
console.log(chalk2.dim(" Directory exists but has no .git \u2014 check the path."));
|
|
701
970
|
} else {
|
|
702
|
-
console.log(
|
|
971
|
+
console.log(chalk2.dim(` ${msg.split("\n")[0]}`));
|
|
703
972
|
}
|
|
704
|
-
console.log(
|
|
973
|
+
console.log(chalk2.dim(` URL: ${repo.url}`));
|
|
705
974
|
if (!config.github_token) {
|
|
706
|
-
console.log(
|
|
975
|
+
console.log(chalk2.dim(" Tip: Re-run setup and provide a GitHub token if the repo is private."));
|
|
707
976
|
}
|
|
708
977
|
process.exit(1);
|
|
709
978
|
}
|
|
710
979
|
}
|
|
711
980
|
}
|
|
712
981
|
console.log("");
|
|
713
|
-
console.log(
|
|
982
|
+
console.log(chalk2.bold("Pulling container images..."));
|
|
714
983
|
try {
|
|
715
984
|
await composeStreaming(runtime, ["pull", "--ignore-pull-failures"]);
|
|
716
985
|
} catch {
|
|
717
|
-
console.log(
|
|
718
|
-
console.log(
|
|
986
|
+
console.log(chalk2.yellow("Some images could not be pulled."));
|
|
987
|
+
console.log(chalk2.dim("Continuing \u2014 services will be built from source if build contexts are available."));
|
|
719
988
|
}
|
|
720
989
|
console.log("");
|
|
721
|
-
console.log(
|
|
990
|
+
console.log(chalk2.bold("Starting Horus services..."));
|
|
722
991
|
try {
|
|
723
992
|
await composeStreaming(runtime, ["up", "-d"]);
|
|
724
993
|
} catch (error) {
|
|
725
|
-
console.log(
|
|
726
|
-
console.log(
|
|
994
|
+
console.log(chalk2.red("Failed to start services."));
|
|
995
|
+
console.log(chalk2.dim(error.message));
|
|
727
996
|
process.exit(1);
|
|
728
997
|
}
|
|
729
998
|
console.log("");
|
|
730
|
-
const healthSpinner =
|
|
999
|
+
const healthSpinner = ora2("Waiting for services to become healthy...").start();
|
|
731
1000
|
let lastStates = [];
|
|
732
1001
|
try {
|
|
733
1002
|
const states = await pollUntilHealthy(
|
|
@@ -735,7 +1004,7 @@ ${example("forge-registry")}
|
|
|
735
1004
|
(current) => {
|
|
736
1005
|
lastStates = current;
|
|
737
1006
|
const summary = current.map((s) => {
|
|
738
|
-
const icon = s.status === "healthy" ?
|
|
1007
|
+
const icon = s.status === "healthy" ? chalk2.green("*") : s.status === "starting" ? chalk2.yellow("~") : chalk2.red("x");
|
|
739
1008
|
return `${icon} ${s.name}`;
|
|
740
1009
|
}).join(" ");
|
|
741
1010
|
healthSpinner.text = `Waiting for services... ${summary}`;
|
|
@@ -747,20 +1016,33 @@ ${example("forge-registry")}
|
|
|
747
1016
|
lastStates = states;
|
|
748
1017
|
} catch (error) {
|
|
749
1018
|
healthSpinner.fail("Some services did not become healthy");
|
|
750
|
-
console.log(
|
|
1019
|
+
console.log(chalk2.dim(error.message));
|
|
751
1020
|
console.log("");
|
|
752
|
-
console.log(
|
|
1021
|
+
console.log(chalk2.dim("Tip: Check logs with `docker compose logs` from ~/.horus/"));
|
|
753
1022
|
process.exit(1);
|
|
754
1023
|
}
|
|
755
1024
|
console.log("");
|
|
756
|
-
|
|
757
|
-
|
|
1025
|
+
const detectedClients = detectInstalledClients();
|
|
1026
|
+
if (detectedClients.length > 0) {
|
|
1027
|
+
console.log(chalk2.bold("Configuring AI clients..."));
|
|
1028
|
+
try {
|
|
1029
|
+
await runConnect(config, runtime, detectedClients, "localhost");
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
console.log(chalk2.yellow("Could not configure AI clients automatically."));
|
|
1032
|
+
console.log(chalk2.dim(`Run ${chalk2.cyan("horus connect")} to configure them manually.`));
|
|
1033
|
+
}
|
|
1034
|
+
} else {
|
|
1035
|
+
console.log(chalk2.dim(`No AI clients detected. Run ${chalk2.cyan("horus connect")} after installing Claude Desktop, Claude Code, or Cursor.`));
|
|
1036
|
+
}
|
|
758
1037
|
console.log("");
|
|
759
|
-
console.log(
|
|
760
|
-
console.log(
|
|
761
|
-
console.log(` ${chalk.bold("Data:")} ${config.data_dir}`);
|
|
1038
|
+
console.log(chalk2.bold.green("Setup complete!"));
|
|
1039
|
+
console.log(chalk2.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
762
1040
|
console.log("");
|
|
763
|
-
console.log(
|
|
1041
|
+
console.log(` ${chalk2.bold("Runtime:")} ${runtime.name}`);
|
|
1042
|
+
console.log(` ${chalk2.bold("Config:")} ~/.horus/config.yaml`);
|
|
1043
|
+
console.log(` ${chalk2.bold("Data:")} ${config.data_dir}`);
|
|
1044
|
+
console.log("");
|
|
1045
|
+
console.log(chalk2.bold(" Service URLs:"));
|
|
764
1046
|
console.log(` Anvil: http://localhost:${config.ports.anvil}`);
|
|
765
1047
|
console.log(` Vault REST: http://localhost:${config.ports.vault_rest}`);
|
|
766
1048
|
console.log(` Vault MCP: http://localhost:${config.ports.vault_mcp}`);
|
|
@@ -769,28 +1051,28 @@ ${example("forge-registry")}
|
|
|
769
1051
|
});
|
|
770
1052
|
|
|
771
1053
|
// src/commands/up.ts
|
|
772
|
-
import { Command as
|
|
773
|
-
import
|
|
774
|
-
import
|
|
775
|
-
var upCommand = new
|
|
1054
|
+
import { Command as Command3 } from "commander";
|
|
1055
|
+
import chalk3 from "chalk";
|
|
1056
|
+
import ora3 from "ora";
|
|
1057
|
+
var upCommand = new Command3("up").description("Start the Horus stack").option("--no-pull", "Skip pulling latest images before starting").action(async (opts) => {
|
|
776
1058
|
if (!configExists() || !composeFileExists()) {
|
|
777
|
-
console.log(
|
|
778
|
-
console.log(
|
|
1059
|
+
console.log(chalk3.red("Horus is not set up yet."));
|
|
1060
|
+
console.log(chalk3.dim("Run `horus setup` first."));
|
|
779
1061
|
process.exit(1);
|
|
780
1062
|
}
|
|
781
1063
|
const config = loadConfig();
|
|
782
|
-
const spinner =
|
|
1064
|
+
const spinner = ora3("Detecting runtime...").start();
|
|
783
1065
|
let runtime;
|
|
784
1066
|
try {
|
|
785
1067
|
runtime = await detectRuntime(config.runtime);
|
|
786
|
-
spinner.succeed(`Using ${
|
|
1068
|
+
spinner.succeed(`Using ${chalk3.cyan(runtime.name)}`);
|
|
787
1069
|
} catch (error) {
|
|
788
1070
|
spinner.fail("No container runtime found");
|
|
789
1071
|
console.log(error.message);
|
|
790
1072
|
process.exit(1);
|
|
791
1073
|
}
|
|
792
1074
|
if (opts.pull) {
|
|
793
|
-
const pullSpinner =
|
|
1075
|
+
const pullSpinner = ora3("Pulling latest images...").start();
|
|
794
1076
|
try {
|
|
795
1077
|
await composeStreaming(runtime, ["pull", "--ignore-pull-failures"]);
|
|
796
1078
|
pullSpinner.succeed("Images up to date");
|
|
@@ -799,29 +1081,29 @@ var upCommand = new Command2("up").description("Start the Horus stack").option("
|
|
|
799
1081
|
}
|
|
800
1082
|
}
|
|
801
1083
|
console.log("");
|
|
802
|
-
console.log(
|
|
1084
|
+
console.log(chalk3.bold("Starting Horus services..."));
|
|
803
1085
|
try {
|
|
804
1086
|
await composeStreaming(runtime, ["up", "-d"]);
|
|
805
1087
|
} catch (error) {
|
|
806
|
-
console.log(
|
|
807
|
-
console.log(
|
|
1088
|
+
console.log(chalk3.red("Failed to start services."));
|
|
1089
|
+
console.log(chalk3.dim(error.message));
|
|
808
1090
|
process.exit(1);
|
|
809
1091
|
}
|
|
810
1092
|
console.log("");
|
|
811
|
-
const statusSpinner =
|
|
1093
|
+
const statusSpinner = ora3("Checking service status...").start();
|
|
812
1094
|
try {
|
|
813
1095
|
const states = await checkAllHealth(runtime);
|
|
814
1096
|
statusSpinner.stop();
|
|
815
|
-
console.log(
|
|
1097
|
+
console.log(chalk3.bold("Service Status:"));
|
|
816
1098
|
for (const s of states) {
|
|
817
|
-
const color = s.status === "healthy" ?
|
|
1099
|
+
const color = s.status === "healthy" ? chalk3.green : s.status === "starting" ? chalk3.yellow : chalk3.red;
|
|
818
1100
|
console.log(` ${color(s.status.padEnd(10))} ${s.name}`);
|
|
819
1101
|
}
|
|
820
1102
|
const allHealthy = states.every((s) => s.status === "healthy");
|
|
821
1103
|
if (!allHealthy) {
|
|
822
1104
|
console.log("");
|
|
823
1105
|
console.log(
|
|
824
|
-
|
|
1106
|
+
chalk3.yellow("Some services are still starting. Run `horus status` to check progress.")
|
|
825
1107
|
);
|
|
826
1108
|
}
|
|
827
1109
|
} catch {
|
|
@@ -831,53 +1113,53 @@ var upCommand = new Command2("up").description("Start the Horus stack").option("
|
|
|
831
1113
|
});
|
|
832
1114
|
|
|
833
1115
|
// src/commands/down.ts
|
|
834
|
-
import { Command as
|
|
835
|
-
import
|
|
836
|
-
import
|
|
837
|
-
var downCommand = new
|
|
1116
|
+
import { Command as Command4 } from "commander";
|
|
1117
|
+
import chalk4 from "chalk";
|
|
1118
|
+
import ora4 from "ora";
|
|
1119
|
+
var downCommand = new Command4("down").description("Stop the Horus stack").action(async () => {
|
|
838
1120
|
if (!configExists() || !composeFileExists()) {
|
|
839
|
-
console.log(
|
|
840
|
-
console.log(
|
|
1121
|
+
console.log(chalk4.red("Horus is not set up yet."));
|
|
1122
|
+
console.log(chalk4.dim("Run `horus setup` first."));
|
|
841
1123
|
process.exit(1);
|
|
842
1124
|
}
|
|
843
1125
|
const config = loadConfig();
|
|
844
|
-
const spinner =
|
|
1126
|
+
const spinner = ora4("Detecting runtime...").start();
|
|
845
1127
|
let runtime;
|
|
846
1128
|
try {
|
|
847
1129
|
runtime = await detectRuntime(config.runtime);
|
|
848
|
-
spinner.succeed(`Using ${
|
|
1130
|
+
spinner.succeed(`Using ${chalk4.cyan(runtime.name)}`);
|
|
849
1131
|
} catch (error) {
|
|
850
1132
|
spinner.fail("No container runtime found");
|
|
851
1133
|
console.log(error.message);
|
|
852
1134
|
process.exit(1);
|
|
853
1135
|
}
|
|
854
1136
|
console.log("");
|
|
855
|
-
console.log(
|
|
1137
|
+
console.log(chalk4.bold("Stopping Horus services..."));
|
|
856
1138
|
try {
|
|
857
1139
|
await composeStreaming(runtime, ["down"]);
|
|
858
1140
|
} catch (error) {
|
|
859
|
-
console.log(
|
|
860
|
-
console.log(
|
|
1141
|
+
console.log(chalk4.red("Failed to stop services."));
|
|
1142
|
+
console.log(chalk4.dim(error.message));
|
|
861
1143
|
process.exit(1);
|
|
862
1144
|
}
|
|
863
1145
|
console.log("");
|
|
864
|
-
console.log(
|
|
865
|
-
console.log(
|
|
1146
|
+
console.log(chalk4.green("All services stopped."));
|
|
1147
|
+
console.log(chalk4.dim("Data volumes have been preserved. Run `horus up` to restart."));
|
|
866
1148
|
console.log("");
|
|
867
1149
|
});
|
|
868
1150
|
|
|
869
1151
|
// src/commands/status.ts
|
|
870
|
-
import { Command as
|
|
871
|
-
import
|
|
872
|
-
import
|
|
873
|
-
var statusCommand = new
|
|
1152
|
+
import { Command as Command5 } from "commander";
|
|
1153
|
+
import chalk5 from "chalk";
|
|
1154
|
+
import ora5 from "ora";
|
|
1155
|
+
var statusCommand = new Command5("status").description("Show status of Horus services").action(async () => {
|
|
874
1156
|
if (!configExists() || !composeFileExists()) {
|
|
875
|
-
console.log(
|
|
876
|
-
console.log(
|
|
1157
|
+
console.log(chalk5.red("Horus is not set up yet."));
|
|
1158
|
+
console.log(chalk5.dim("Run `horus setup` first."));
|
|
877
1159
|
process.exit(1);
|
|
878
1160
|
}
|
|
879
1161
|
const config = loadConfig();
|
|
880
|
-
const spinner =
|
|
1162
|
+
const spinner = ora5("Checking services...").start();
|
|
881
1163
|
let runtime;
|
|
882
1164
|
try {
|
|
883
1165
|
runtime = await detectRuntime(config.runtime);
|
|
@@ -898,28 +1180,28 @@ var statusCommand = new Command4("status").description("Show status of Horus ser
|
|
|
898
1180
|
}
|
|
899
1181
|
spinner.stop();
|
|
900
1182
|
console.log("");
|
|
901
|
-
console.log(
|
|
902
|
-
console.log(
|
|
903
|
-
console.log(` ${
|
|
904
|
-
console.log(` ${
|
|
905
|
-
console.log(` ${
|
|
1183
|
+
console.log(chalk5.bold("Horus Status"));
|
|
1184
|
+
console.log(chalk5.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1185
|
+
console.log(` ${chalk5.bold("Version:")} ${config.version}`);
|
|
1186
|
+
console.log(` ${chalk5.bold("Runtime:")} ${runtime.name}`);
|
|
1187
|
+
console.log(` ${chalk5.bold("Config:")} ~/.horus/config.yaml`);
|
|
906
1188
|
console.log("");
|
|
907
1189
|
if (containers.length === 0) {
|
|
908
|
-
console.log(
|
|
909
|
-
console.log(
|
|
1190
|
+
console.log(chalk5.yellow(" No services are running."));
|
|
1191
|
+
console.log(chalk5.dim(" Run `horus up` to start the stack."));
|
|
910
1192
|
console.log("");
|
|
911
1193
|
return;
|
|
912
1194
|
}
|
|
913
1195
|
const header = ` ${pad("SERVICE", 14)} ${pad("STATUS", 12)} ${pad("PORTS", 20)} ${pad("UPTIME", 20)}`;
|
|
914
|
-
console.log(
|
|
915
|
-
console.log(
|
|
1196
|
+
console.log(chalk5.bold(header));
|
|
1197
|
+
console.log(chalk5.dim(" " + "\u2500".repeat(66)));
|
|
916
1198
|
for (const service of SERVICES) {
|
|
917
1199
|
const container = containers.find(
|
|
918
1200
|
(c) => c.Service === service || c.Name?.includes(service)
|
|
919
1201
|
);
|
|
920
1202
|
if (!container) {
|
|
921
1203
|
console.log(
|
|
922
|
-
` ${pad(service, 14)} ${
|
|
1204
|
+
` ${pad(service, 14)} ${chalk5.red(pad("stopped", 12))} ${pad("-", 20)} ${pad("-", 20)}`
|
|
923
1205
|
);
|
|
924
1206
|
continue;
|
|
925
1207
|
}
|
|
@@ -937,9 +1219,9 @@ function pad(str, width) {
|
|
|
937
1219
|
}
|
|
938
1220
|
function getStatusColor(status) {
|
|
939
1221
|
const lower = status.toLowerCase();
|
|
940
|
-
if (lower === "healthy" || lower === "running") return
|
|
941
|
-
if (lower === "starting") return
|
|
942
|
-
return
|
|
1222
|
+
if (lower === "healthy" || lower === "running") return chalk5.green;
|
|
1223
|
+
if (lower === "starting") return chalk5.yellow;
|
|
1224
|
+
return chalk5.red;
|
|
943
1225
|
}
|
|
944
1226
|
function formatPorts(publishers) {
|
|
945
1227
|
if (!publishers || publishers.length === 0) return "-";
|
|
@@ -954,52 +1236,52 @@ function extractUptime(status) {
|
|
|
954
1236
|
}
|
|
955
1237
|
|
|
956
1238
|
// src/commands/config.ts
|
|
957
|
-
import { Command as
|
|
958
|
-
import
|
|
1239
|
+
import { Command as Command6 } from "commander";
|
|
1240
|
+
import chalk6 from "chalk";
|
|
959
1241
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
960
|
-
var configCommand = new
|
|
1242
|
+
var configCommand = new Command6("config").description("View or modify Horus configuration").action(async () => {
|
|
961
1243
|
if (!configExists()) {
|
|
962
|
-
console.log(
|
|
963
|
-
console.log(
|
|
1244
|
+
console.log(chalk6.red("Horus is not configured yet."));
|
|
1245
|
+
console.log(chalk6.dim("Run `horus setup` first."));
|
|
964
1246
|
process.exit(1);
|
|
965
1247
|
}
|
|
966
1248
|
const config = loadConfig();
|
|
967
1249
|
console.log("");
|
|
968
|
-
console.log(
|
|
969
|
-
console.log(
|
|
970
|
-
console.log(` ${
|
|
971
|
-
console.log(` ${
|
|
972
|
-
console.log(` ${
|
|
973
|
-
console.log(` ${
|
|
1250
|
+
console.log(chalk6.bold("Horus Configuration"));
|
|
1251
|
+
console.log(chalk6.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1252
|
+
console.log(` ${chalk6.bold("version:")} ${config.version}`);
|
|
1253
|
+
console.log(` ${chalk6.bold("data-dir:")} ${config.data_dir}`);
|
|
1254
|
+
console.log(` ${chalk6.bold("runtime:")} ${config.runtime}`);
|
|
1255
|
+
console.log(` ${chalk6.bold("host-repos-path:")} ${config.host_repos_path || chalk6.dim("(not set)")}`);
|
|
974
1256
|
const extraDirs = (config.host_repos_extra_scan_dirs ?? []).join(", ");
|
|
975
|
-
console.log(` ${
|
|
976
|
-
console.log(` ${
|
|
977
|
-
console.log(` ${
|
|
1257
|
+
console.log(` ${chalk6.bold("host-repos-extra-scan-dirs:")} ${extraDirs || chalk6.dim("(not set)")}`);
|
|
1258
|
+
console.log(` ${chalk6.bold("git-host:")} ${config.git_host || chalk6.dim("(not set)")}`);
|
|
1259
|
+
console.log(` ${chalk6.bold("github-token:")} ${config.github_token ? maskApiKey(config.github_token) : chalk6.dim("(not set)")}`);
|
|
978
1260
|
console.log("");
|
|
979
|
-
console.log(
|
|
980
|
-
console.log(` ${
|
|
981
|
-
console.log(` ${
|
|
982
|
-
console.log(` ${
|
|
983
|
-
console.log(` ${
|
|
1261
|
+
console.log(chalk6.bold(" Ports:"));
|
|
1262
|
+
console.log(` ${chalk6.bold("anvil:")} ${config.ports.anvil}`);
|
|
1263
|
+
console.log(` ${chalk6.bold("vault-rest:")} ${config.ports.vault_rest}`);
|
|
1264
|
+
console.log(` ${chalk6.bold("vault-mcp:")} ${config.ports.vault_mcp}`);
|
|
1265
|
+
console.log(` ${chalk6.bold("forge:")} ${config.ports.forge}`);
|
|
984
1266
|
console.log("");
|
|
985
|
-
console.log(
|
|
986
|
-
console.log(` ${
|
|
987
|
-
console.log(` ${
|
|
988
|
-
console.log(` ${
|
|
1267
|
+
console.log(chalk6.bold(" Repos:"));
|
|
1268
|
+
console.log(` ${chalk6.bold("anvil-notes:")} ${config.repos.anvil_notes || chalk6.dim("(not set)")}`);
|
|
1269
|
+
console.log(` ${chalk6.bold("vault-knowledge:")} ${config.repos.vault_knowledge || chalk6.dim("(not set)")}`);
|
|
1270
|
+
console.log(` ${chalk6.bold("forge-registry:")} ${config.repos.forge_registry || chalk6.dim("(not set)")}`);
|
|
989
1271
|
console.log("");
|
|
990
|
-
console.log(
|
|
991
|
-
console.log(
|
|
1272
|
+
console.log(chalk6.dim(` Config file: ~/.horus/config.yaml`));
|
|
1273
|
+
console.log(chalk6.dim(` Use 'horus config get <key>' or 'horus config set <key> <value>'`));
|
|
992
1274
|
console.log("");
|
|
993
1275
|
});
|
|
994
1276
|
configCommand.command("get <key>").description("Get a configuration value").action(async (key) => {
|
|
995
1277
|
if (!configExists()) {
|
|
996
|
-
console.log(
|
|
997
|
-
console.log(
|
|
1278
|
+
console.log(chalk6.red("Horus is not configured yet."));
|
|
1279
|
+
console.log(chalk6.dim("Run `horus setup` first."));
|
|
998
1280
|
process.exit(1);
|
|
999
1281
|
}
|
|
1000
1282
|
if (!isValidKey(key)) {
|
|
1001
|
-
console.log(
|
|
1002
|
-
console.log(
|
|
1283
|
+
console.log(chalk6.red(`Unknown config key: ${key}`));
|
|
1284
|
+
console.log(chalk6.dim(`Valid keys: ${CONFIG_KEYS.join(", ")}`));
|
|
1003
1285
|
process.exit(1);
|
|
1004
1286
|
}
|
|
1005
1287
|
const config = loadConfig();
|
|
@@ -1012,25 +1294,25 @@ configCommand.command("get <key>").description("Get a configuration value").acti
|
|
|
1012
1294
|
});
|
|
1013
1295
|
configCommand.command("set <key> <value>").description("Set a configuration value").action(async (key, value) => {
|
|
1014
1296
|
if (!configExists()) {
|
|
1015
|
-
console.log(
|
|
1016
|
-
console.log(
|
|
1297
|
+
console.log(chalk6.red("Horus is not configured yet."));
|
|
1298
|
+
console.log(chalk6.dim("Run `horus setup` first."));
|
|
1017
1299
|
process.exit(1);
|
|
1018
1300
|
}
|
|
1019
1301
|
if (!isValidKey(key)) {
|
|
1020
|
-
console.log(
|
|
1021
|
-
console.log(
|
|
1302
|
+
console.log(chalk6.red(`Unknown config key: ${key}`));
|
|
1303
|
+
console.log(chalk6.dim(`Valid keys: ${CONFIG_KEYS.join(", ")}`));
|
|
1022
1304
|
process.exit(1);
|
|
1023
1305
|
}
|
|
1024
1306
|
let config = loadConfig();
|
|
1025
1307
|
try {
|
|
1026
1308
|
config = setConfigValue(config, key, value);
|
|
1027
1309
|
} catch (error) {
|
|
1028
|
-
console.log(
|
|
1310
|
+
console.log(chalk6.red(error.message));
|
|
1029
1311
|
process.exit(1);
|
|
1030
1312
|
}
|
|
1031
1313
|
saveConfig(config);
|
|
1032
1314
|
writeEnvFile(config);
|
|
1033
|
-
console.log(
|
|
1315
|
+
console.log(chalk6.green(`Set ${key} and regenerated .env file.`));
|
|
1034
1316
|
const needsRestart = [
|
|
1035
1317
|
"data-dir",
|
|
1036
1318
|
"host-repos-path",
|
|
@@ -1042,17 +1324,17 @@ configCommand.command("set <key> <value>").description("Set a configuration valu
|
|
|
1042
1324
|
"port.forge"
|
|
1043
1325
|
];
|
|
1044
1326
|
if (needsRestart.includes(key)) {
|
|
1045
|
-
console.log(
|
|
1327
|
+
console.log(chalk6.yellow("Restart required for changes to take effect."));
|
|
1046
1328
|
if (process.stdin.isTTY) {
|
|
1047
1329
|
const restart = await confirm2({
|
|
1048
1330
|
message: "Restart Horus now?",
|
|
1049
1331
|
default: false
|
|
1050
1332
|
});
|
|
1051
1333
|
if (restart) {
|
|
1052
|
-
console.log(
|
|
1334
|
+
console.log(chalk6.dim("Run `horus down && horus up` to restart."));
|
|
1053
1335
|
}
|
|
1054
1336
|
} else {
|
|
1055
|
-
console.log(
|
|
1337
|
+
console.log(chalk6.dim("Run `horus down && horus up` to restart."));
|
|
1056
1338
|
}
|
|
1057
1339
|
}
|
|
1058
1340
|
});
|
|
@@ -1060,266 +1342,6 @@ function isValidKey(key) {
|
|
|
1060
1342
|
return CONFIG_KEYS.includes(key);
|
|
1061
1343
|
}
|
|
1062
1344
|
|
|
1063
|
-
// src/commands/connect.ts
|
|
1064
|
-
import { Command as Command6 } from "commander";
|
|
1065
|
-
import chalk6 from "chalk";
|
|
1066
|
-
import ora5 from "ora";
|
|
1067
|
-
import { checkbox } from "@inquirer/prompts";
|
|
1068
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
1069
|
-
import { join as join4 } from "path";
|
|
1070
|
-
import { homedir as homedir3 } from "os";
|
|
1071
|
-
import { execa as execa2 } from "execa";
|
|
1072
|
-
function detectInstalledClients() {
|
|
1073
|
-
const detected = [];
|
|
1074
|
-
const home = homedir3();
|
|
1075
|
-
const claudeDesktopDir = join4(home, "Library", "Application Support", "Claude");
|
|
1076
|
-
if (existsSync4(claudeDesktopDir)) {
|
|
1077
|
-
detected.push("claude-desktop");
|
|
1078
|
-
}
|
|
1079
|
-
const claudeCodeDir = join4(home, ".claude");
|
|
1080
|
-
if (existsSync4(claudeCodeDir)) {
|
|
1081
|
-
detected.push("claude-code");
|
|
1082
|
-
}
|
|
1083
|
-
const cursorDir = join4(home, ".cursor");
|
|
1084
|
-
const cursorAppDir = join4(home, "Library", "Application Support", "Cursor");
|
|
1085
|
-
if (existsSync4(cursorDir) || existsSync4(cursorAppDir)) {
|
|
1086
|
-
detected.push("cursor");
|
|
1087
|
-
}
|
|
1088
|
-
return detected;
|
|
1089
|
-
}
|
|
1090
|
-
function getConfigPath(target) {
|
|
1091
|
-
const home = homedir3();
|
|
1092
|
-
switch (target) {
|
|
1093
|
-
case "claude-desktop":
|
|
1094
|
-
return join4(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1095
|
-
case "claude-code":
|
|
1096
|
-
return join4(home, ".claude", "settings.json");
|
|
1097
|
-
case "cursor":
|
|
1098
|
-
return join4(home, ".cursor", "mcp.json");
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
function mergeAndWriteConfig(configPath, mcpServers) {
|
|
1102
|
-
let existing = {};
|
|
1103
|
-
if (existsSync4(configPath)) {
|
|
1104
|
-
try {
|
|
1105
|
-
const raw = readFileSync3(configPath, "utf-8");
|
|
1106
|
-
existing = JSON.parse(raw);
|
|
1107
|
-
} catch {
|
|
1108
|
-
existing = {};
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
const existingServers = existing.mcpServers ?? {};
|
|
1112
|
-
existing.mcpServers = { ...existingServers, ...mcpServers };
|
|
1113
|
-
const dir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
1114
|
-
mkdirSync3(dir, { recursive: true });
|
|
1115
|
-
writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
1116
|
-
}
|
|
1117
|
-
async function isClaudeCliAvailable() {
|
|
1118
|
-
try {
|
|
1119
|
-
const result = await execa2("claude", ["--version"], { reject: false });
|
|
1120
|
-
return result.exitCode === 0;
|
|
1121
|
-
} catch {
|
|
1122
|
-
return false;
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
async function registerWithClaudeCode(mcpServers) {
|
|
1126
|
-
const registered = [];
|
|
1127
|
-
const failed = [];
|
|
1128
|
-
for (const [name, entry] of Object.entries(mcpServers)) {
|
|
1129
|
-
const baseUrl = entry.url.replace(/\/sse$/, "");
|
|
1130
|
-
const result = await execa2(
|
|
1131
|
-
"claude",
|
|
1132
|
-
["mcp", "add", "--transport", "http", "--scope", "user", name, baseUrl],
|
|
1133
|
-
{ reject: false }
|
|
1134
|
-
);
|
|
1135
|
-
if (result.exitCode === 0) {
|
|
1136
|
-
registered.push(name);
|
|
1137
|
-
} else {
|
|
1138
|
-
failed.push(name);
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
return { registered, failed };
|
|
1142
|
-
}
|
|
1143
|
-
async function syncSkills(runtime) {
|
|
1144
|
-
const home = homedir3();
|
|
1145
|
-
const skillsBase = join4(home, ".claude", "skills");
|
|
1146
|
-
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
1147
|
-
const forgeContainer = "horus-forge-1";
|
|
1148
|
-
for (const skill of skills) {
|
|
1149
|
-
const destDir = join4(skillsBase, skill);
|
|
1150
|
-
mkdirSync3(destDir, { recursive: true });
|
|
1151
|
-
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
1152
|
-
const dest = join4(destDir, "SKILL.md");
|
|
1153
|
-
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
1154
|
-
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
1155
|
-
writeFileSync3(dest, result.stdout, "utf-8");
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
async function syncSkillsForCursor(runtime) {
|
|
1160
|
-
const home = homedir3();
|
|
1161
|
-
const rulesDir = join4(home, ".cursor", "rules");
|
|
1162
|
-
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
1163
|
-
const forgeContainer = "horus-forge-1";
|
|
1164
|
-
mkdirSync3(rulesDir, { recursive: true });
|
|
1165
|
-
for (const skill of skills) {
|
|
1166
|
-
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
1167
|
-
const dest = join4(rulesDir, `${skill}.mdc`);
|
|
1168
|
-
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
1169
|
-
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
1170
|
-
const frontmatter = `---
|
|
1171
|
-
description: Horus ${skill} reference
|
|
1172
|
-
alwaysApply: true
|
|
1173
|
-
---
|
|
1174
|
-
|
|
1175
|
-
`;
|
|
1176
|
-
writeFileSync3(dest, frontmatter + result.stdout, "utf-8");
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
function printNextSteps(targets) {
|
|
1181
|
-
console.log("");
|
|
1182
|
-
console.log(chalk6.bold("Next steps:"));
|
|
1183
|
-
for (const target of targets) {
|
|
1184
|
-
switch (target) {
|
|
1185
|
-
case "claude-desktop":
|
|
1186
|
-
console.log(` ${chalk6.cyan("Claude Desktop")} Restart Claude Desktop to pick up the new MCP configuration`);
|
|
1187
|
-
break;
|
|
1188
|
-
case "claude-code":
|
|
1189
|
-
console.log(` ${chalk6.cyan("Claude Code")} Start a new Claude Code session`);
|
|
1190
|
-
break;
|
|
1191
|
-
case "cursor":
|
|
1192
|
-
console.log(` ${chalk6.cyan("Cursor")} Restart Cursor to pick up the new MCP configuration and rules`);
|
|
1193
|
-
break;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
console.log("");
|
|
1197
|
-
}
|
|
1198
|
-
var connectCommand = new Command6("connect").description("Configure Claude/Cursor MCP integration").option("--target <client>", "Client to configure: claude-desktop, claude-code, cursor, all (default: auto-detect)").option("--host <host>", "MCP host (default: localhost)", "localhost").option("-y, --yes", "Skip confirmation prompts").action(async (opts) => {
|
|
1199
|
-
console.log("");
|
|
1200
|
-
console.log(chalk6.bold("Horus Connect"));
|
|
1201
|
-
console.log(chalk6.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1202
|
-
console.log("");
|
|
1203
|
-
const config = loadConfig();
|
|
1204
|
-
const runtimeSpinner = ora5("Detecting runtime...").start();
|
|
1205
|
-
let runtime;
|
|
1206
|
-
try {
|
|
1207
|
-
runtime = await detectRuntime(config.runtime);
|
|
1208
|
-
runtimeSpinner.succeed(`Using ${chalk6.cyan(runtime.name)}`);
|
|
1209
|
-
} catch (error) {
|
|
1210
|
-
runtimeSpinner.fail("No container runtime found");
|
|
1211
|
-
console.log(error.message);
|
|
1212
|
-
process.exit(1);
|
|
1213
|
-
}
|
|
1214
|
-
const runningSpinner = ora5("Checking Horus status...").start();
|
|
1215
|
-
const running = await runtime.isRunning();
|
|
1216
|
-
if (!running) {
|
|
1217
|
-
runningSpinner.fail("Horus is not running");
|
|
1218
|
-
console.log(chalk6.dim("Run `horus up` first, then re-run `horus connect`."));
|
|
1219
|
-
process.exit(1);
|
|
1220
|
-
}
|
|
1221
|
-
runningSpinner.succeed("Horus is running");
|
|
1222
|
-
let targets = [];
|
|
1223
|
-
if (opts.target === "all") {
|
|
1224
|
-
targets = ["claude-desktop", "claude-code", "cursor"];
|
|
1225
|
-
} else if (opts.target) {
|
|
1226
|
-
const valid = ["claude-desktop", "claude-code", "cursor"];
|
|
1227
|
-
if (!valid.includes(opts.target)) {
|
|
1228
|
-
console.log(chalk6.red(`Invalid target: ${opts.target}`));
|
|
1229
|
-
console.log(chalk6.dim("Valid targets: claude-desktop, claude-code, cursor, all"));
|
|
1230
|
-
process.exit(1);
|
|
1231
|
-
}
|
|
1232
|
-
targets = [opts.target];
|
|
1233
|
-
} else {
|
|
1234
|
-
const detected = detectInstalledClients();
|
|
1235
|
-
if (detected.length === 0) {
|
|
1236
|
-
console.log(chalk6.yellow("No supported clients detected (Claude Desktop, Claude Code, or Cursor)."));
|
|
1237
|
-
console.log(chalk6.dim("Use --target to specify a client manually."));
|
|
1238
|
-
process.exit(1);
|
|
1239
|
-
}
|
|
1240
|
-
if (opts.yes) {
|
|
1241
|
-
targets = detected;
|
|
1242
|
-
console.log(`Detected clients: ${detected.map((t) => chalk6.cyan(t)).join(", ")}`);
|
|
1243
|
-
} else {
|
|
1244
|
-
const chosen = await checkbox({
|
|
1245
|
-
message: "Select clients to configure:",
|
|
1246
|
-
choices: detected.map((t) => ({ name: t, value: t, checked: true })),
|
|
1247
|
-
validate: (input2) => input2.length > 0 ? true : "Select at least one client."
|
|
1248
|
-
});
|
|
1249
|
-
targets = chosen;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
if (targets.length === 0) {
|
|
1253
|
-
console.log(chalk6.yellow("No clients selected. Exiting."));
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
const host = opts.host;
|
|
1257
|
-
const mcpServers = {
|
|
1258
|
-
anvil: { url: `http://${host}:${config.ports.anvil}/sse` },
|
|
1259
|
-
vault: { url: `http://${host}:${config.ports.vault_mcp}/sse` },
|
|
1260
|
-
forge: { url: `http://${host}:${config.ports.forge}/sse` }
|
|
1261
|
-
};
|
|
1262
|
-
for (const target of targets) {
|
|
1263
|
-
if (target === "claude-code") {
|
|
1264
|
-
const cliSpinner = ora5("Registering MCP servers with Claude Code CLI...").start();
|
|
1265
|
-
const cliAvailable = await isClaudeCliAvailable();
|
|
1266
|
-
if (cliAvailable) {
|
|
1267
|
-
const { registered, failed } = await registerWithClaudeCode(mcpServers);
|
|
1268
|
-
if (failed.length === 0) {
|
|
1269
|
-
cliSpinner.succeed(
|
|
1270
|
-
`Registered with Claude Code: ${registered.map((n) => chalk6.cyan(n)).join(", ")}`
|
|
1271
|
-
);
|
|
1272
|
-
} else if (registered.length > 0) {
|
|
1273
|
-
cliSpinner.warn(
|
|
1274
|
-
`Partially registered \u2014 ok: ${registered.join(", ")}, failed: ${failed.join(", ")}`
|
|
1275
|
-
);
|
|
1276
|
-
} else {
|
|
1277
|
-
cliSpinner.fail("Failed to register MCP servers with Claude Code CLI");
|
|
1278
|
-
}
|
|
1279
|
-
} else {
|
|
1280
|
-
cliSpinner.warn("claude CLI not found on PATH \u2014 register manually:");
|
|
1281
|
-
for (const [name, entry] of Object.entries(mcpServers)) {
|
|
1282
|
-
const baseUrl = entry.url.replace(/\/sse$/, "");
|
|
1283
|
-
console.log(
|
|
1284
|
-
chalk6.dim(` claude mcp add --transport http --scope user ${name} ${baseUrl}`)
|
|
1285
|
-
);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
} else {
|
|
1289
|
-
const configPath = getConfigPath(target);
|
|
1290
|
-
const writeSpinner = ora5(`Configuring ${chalk6.cyan(target)}...`).start();
|
|
1291
|
-
try {
|
|
1292
|
-
mergeAndWriteConfig(configPath, mcpServers);
|
|
1293
|
-
writeSpinner.succeed(`Configured ${chalk6.cyan(target)} \u2014 ${chalk6.dim(configPath)}`);
|
|
1294
|
-
} catch (error) {
|
|
1295
|
-
writeSpinner.fail(`Failed to configure ${target}`);
|
|
1296
|
-
console.log(chalk6.dim(error.message));
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
if (targets.includes("claude-code")) {
|
|
1301
|
-
const skillsSpinner = ora5("Syncing horus-core skills...").start();
|
|
1302
|
-
try {
|
|
1303
|
-
await syncSkills(runtime);
|
|
1304
|
-
skillsSpinner.succeed("horus-core skills synced to ~/.claude/skills/");
|
|
1305
|
-
} catch (error) {
|
|
1306
|
-
skillsSpinner.warn("Could not sync skills (Forge container may not be running)");
|
|
1307
|
-
console.log(chalk6.dim(error.message));
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
if (targets.includes("cursor")) {
|
|
1311
|
-
const cursorRulesSpinner = ora5("Syncing horus-core rules for Cursor...").start();
|
|
1312
|
-
try {
|
|
1313
|
-
await syncSkillsForCursor(runtime);
|
|
1314
|
-
cursorRulesSpinner.succeed("horus-core rules synced to ~/.cursor/rules/");
|
|
1315
|
-
} catch (error) {
|
|
1316
|
-
cursorRulesSpinner.warn("Could not sync Cursor rules (Forge container may not be running)");
|
|
1317
|
-
console.log(chalk6.dim(error.message));
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
printNextSteps(targets);
|
|
1321
|
-
});
|
|
1322
|
-
|
|
1323
1345
|
// src/commands/update.ts
|
|
1324
1346
|
import { Command as Command7 } from "commander";
|
|
1325
1347
|
import chalk7 from "chalk";
|