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