@arkhera30/cli 0.1.8 → 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/compose/docker-compose.yml +3 -2
- package/dist/index.js +468 -349
- package/package.json +1 -1
|
@@ -198,8 +198,9 @@ services:
|
|
|
198
198
|
- FORGE_HOST_ANVIL_URL=http://localhost:${ANVIL_PORT:-8100}
|
|
199
199
|
- FORGE_HOST_VAULT_URL=http://localhost:${VAULT_MCP_PORT:-8300}
|
|
200
200
|
- FORGE_HOST_FORGE_URL=http://localhost:${FORGE_PORT:-8200}
|
|
201
|
-
# Colon-separated list of in-container paths to scan for git repos
|
|
202
|
-
|
|
201
|
+
# Colon-separated list of in-container paths to scan for git repos.
|
|
202
|
+
# Generated by the CLI from host_repos_extra_scan_dirs config — do not edit manually.
|
|
203
|
+
- FORGE_SCAN_PATHS=${FORGE_SCAN_PATHS:-/data/repos}
|
|
203
204
|
- GITHUB_TOKEN=${GITHUB_TOKEN:-}
|
|
204
205
|
depends_on:
|
|
205
206
|
anvil:
|
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
|
|
@@ -58,6 +58,7 @@ function defaultConfig() {
|
|
|
58
58
|
git_host: "github.com",
|
|
59
59
|
repos: { ...DEFAULT_REPOS },
|
|
60
60
|
host_repos_path: "",
|
|
61
|
+
host_repos_extra_scan_dirs: [],
|
|
61
62
|
github_token: ""
|
|
62
63
|
};
|
|
63
64
|
}
|
|
@@ -91,6 +92,7 @@ function loadConfig() {
|
|
|
91
92
|
forge_registry: parsed.repos?.forge_registry ?? defaults.repos.forge_registry
|
|
92
93
|
},
|
|
93
94
|
host_repos_path: parsed.host_repos_path ?? defaults.host_repos_path,
|
|
95
|
+
host_repos_extra_scan_dirs: parsed.host_repos_extra_scan_dirs ?? defaults.host_repos_extra_scan_dirs,
|
|
94
96
|
github_token: parsed.github_token ?? defaults.github_token
|
|
95
97
|
};
|
|
96
98
|
}
|
|
@@ -108,6 +110,9 @@ function resolvePath(p) {
|
|
|
108
110
|
function generateEnv(config) {
|
|
109
111
|
const dataDir = resolvePath(config.data_dir);
|
|
110
112
|
const hostReposPath = config.host_repos_path ? resolvePath(config.host_repos_path) : "";
|
|
113
|
+
const baseScanPath = "/data/repos";
|
|
114
|
+
const extraScanPaths = (config.host_repos_extra_scan_dirs ?? []).map((d) => d.trim()).filter(Boolean).map((d) => `${baseScanPath}/${d}`);
|
|
115
|
+
const forgeScanPaths = [baseScanPath, ...extraScanPaths].join(":");
|
|
111
116
|
const lines = [
|
|
112
117
|
"# \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",
|
|
113
118
|
"# Horus \u2014 Generated .env file",
|
|
@@ -116,6 +121,7 @@ function generateEnv(config) {
|
|
|
116
121
|
"",
|
|
117
122
|
`HORUS_DATA_PATH=${dataDir}`,
|
|
118
123
|
`HOST_REPOS_PATH=${hostReposPath}`,
|
|
124
|
+
`FORGE_SCAN_PATHS=${forgeScanPaths}`,
|
|
119
125
|
"",
|
|
120
126
|
"# Ports",
|
|
121
127
|
`ANVIL_PORT=${config.ports.anvil}`,
|
|
@@ -142,6 +148,7 @@ function writeEnvFile(config) {
|
|
|
142
148
|
var CONFIG_KEYS = [
|
|
143
149
|
"data-dir",
|
|
144
150
|
"host-repos-path",
|
|
151
|
+
"host-repos-extra-scan-dirs",
|
|
145
152
|
"runtime",
|
|
146
153
|
"port.anvil",
|
|
147
154
|
"port.vault-rest",
|
|
@@ -159,6 +166,8 @@ function getConfigValue(config, key) {
|
|
|
159
166
|
return config.data_dir;
|
|
160
167
|
case "host-repos-path":
|
|
161
168
|
return config.host_repos_path;
|
|
169
|
+
case "host-repos-extra-scan-dirs":
|
|
170
|
+
return (config.host_repos_extra_scan_dirs ?? []).join(", ");
|
|
162
171
|
case "runtime":
|
|
163
172
|
return config.runtime;
|
|
164
173
|
case "port.anvil":
|
|
@@ -190,6 +199,9 @@ function setConfigValue(config, key, value) {
|
|
|
190
199
|
case "host-repos-path":
|
|
191
200
|
updated.host_repos_path = value;
|
|
192
201
|
break;
|
|
202
|
+
case "host-repos-extra-scan-dirs":
|
|
203
|
+
updated.host_repos_extra_scan_dirs = value.split(",").map((d) => d.trim()).filter(Boolean);
|
|
204
|
+
break;
|
|
193
205
|
case "runtime":
|
|
194
206
|
if (value !== "docker" && value !== "podman") {
|
|
195
207
|
throw new Error(`Invalid runtime: ${value}. Must be "docker" or "podman".`);
|
|
@@ -258,11 +270,13 @@ async function commandExists(command) {
|
|
|
258
270
|
}
|
|
259
271
|
function createRuntime(name) {
|
|
260
272
|
const bin = name;
|
|
273
|
+
const composeEnv = { ...process.env, HORUS_RUNTIME: name };
|
|
261
274
|
return {
|
|
262
275
|
name,
|
|
263
276
|
async compose(...args) {
|
|
264
277
|
const result = await execa(bin, ["compose", ...args], {
|
|
265
278
|
cwd: HORUS_DIR,
|
|
279
|
+
env: composeEnv,
|
|
266
280
|
reject: false
|
|
267
281
|
});
|
|
268
282
|
if (result.exitCode !== 0) {
|
|
@@ -290,6 +304,7 @@ function createRuntime(name) {
|
|
|
290
304
|
try {
|
|
291
305
|
const result = await execa(bin, ["compose", "ps", "--format", "json"], {
|
|
292
306
|
cwd: HORUS_DIR,
|
|
307
|
+
env: composeEnv,
|
|
293
308
|
reject: false
|
|
294
309
|
});
|
|
295
310
|
return result.exitCode === 0 && result.stdout.toString().trim().length > 0;
|
|
@@ -331,6 +346,7 @@ async function composeStreaming(runtime, args) {
|
|
|
331
346
|
const bin = runtime.name;
|
|
332
347
|
const result = await execa(bin, ["compose", ...args], {
|
|
333
348
|
cwd: HORUS_DIR,
|
|
349
|
+
env: { ...process.env, HORUS_RUNTIME: runtime.name },
|
|
334
350
|
stdio: "inherit",
|
|
335
351
|
reject: false
|
|
336
352
|
});
|
|
@@ -420,16 +436,294 @@ function getBundledComposePath() {
|
|
|
420
436
|
Searched: ${candidates.join(", ")}`
|
|
421
437
|
);
|
|
422
438
|
}
|
|
439
|
+
function applyPodmanUserOverride(compose) {
|
|
440
|
+
return compose.replace(
|
|
441
|
+
/^( image: .+)$/gm,
|
|
442
|
+
'$1\n user: "0:0"'
|
|
443
|
+
);
|
|
444
|
+
}
|
|
423
445
|
function composeFileExists() {
|
|
424
446
|
return existsSync2(COMPOSE_PATH);
|
|
425
447
|
}
|
|
426
|
-
function installComposeFile() {
|
|
448
|
+
function installComposeFile(runtime) {
|
|
427
449
|
ensureHorusDir();
|
|
428
450
|
const bundledPath = getBundledComposePath();
|
|
429
|
-
|
|
451
|
+
let content = readFileSync2(bundledPath, "utf-8");
|
|
452
|
+
if (runtime === "podman") {
|
|
453
|
+
content = applyPodmanUserOverride(content);
|
|
454
|
+
}
|
|
430
455
|
writeFileSync2(COMPOSE_PATH, content, "utf-8");
|
|
431
456
|
}
|
|
432
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
|
+
|
|
433
727
|
// src/commands/setup.ts
|
|
434
728
|
function injectToken(url, token) {
|
|
435
729
|
if (!token) return url;
|
|
@@ -442,26 +736,26 @@ function injectToken(url, token) {
|
|
|
442
736
|
return url;
|
|
443
737
|
}
|
|
444
738
|
}
|
|
445
|
-
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) => {
|
|
446
740
|
console.log("");
|
|
447
|
-
console.log(
|
|
448
|
-
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"));
|
|
449
743
|
console.log("");
|
|
450
744
|
if (configExists()) {
|
|
451
745
|
if (opts.yes) {
|
|
452
|
-
console.log(
|
|
746
|
+
console.log(chalk2.yellow("Existing configuration found. Overwriting in non-interactive mode."));
|
|
453
747
|
} else {
|
|
454
748
|
const proceed = await confirm({
|
|
455
749
|
message: "Horus is already configured. Reconfigure?",
|
|
456
750
|
default: false
|
|
457
751
|
});
|
|
458
752
|
if (!proceed) {
|
|
459
|
-
console.log(
|
|
753
|
+
console.log(chalk2.dim("Setup cancelled."));
|
|
460
754
|
return;
|
|
461
755
|
}
|
|
462
756
|
}
|
|
463
757
|
}
|
|
464
|
-
const checkSpinner =
|
|
758
|
+
const checkSpinner = ora2("Checking for container runtimes...").start();
|
|
465
759
|
const [hasDocker, hasPodman] = await Promise.all([
|
|
466
760
|
checkRuntime("docker"),
|
|
467
761
|
checkRuntime("podman")
|
|
@@ -472,7 +766,7 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
472
766
|
...hasPodman ? ["podman"] : []
|
|
473
767
|
];
|
|
474
768
|
if (available.length === 0) {
|
|
475
|
-
console.log(
|
|
769
|
+
console.log(chalk2.red("No container runtime found."));
|
|
476
770
|
console.log("");
|
|
477
771
|
console.log("Horus requires Docker or Podman with the Compose plugin.");
|
|
478
772
|
console.log("");
|
|
@@ -485,12 +779,12 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
485
779
|
if (opts.yes) {
|
|
486
780
|
const requested = opts.runtime;
|
|
487
781
|
if (requested && !available.includes(requested)) {
|
|
488
|
-
console.log(
|
|
489
|
-
console.log(
|
|
782
|
+
console.log(chalk2.red(`Requested runtime "${requested}" is not installed.`));
|
|
783
|
+
console.log(chalk2.dim(`Available: ${available.join(", ")}`));
|
|
490
784
|
process.exit(1);
|
|
491
785
|
}
|
|
492
786
|
selectedRuntime = requested ?? available[0];
|
|
493
|
-
console.log(`Using ${
|
|
787
|
+
console.log(`Using ${chalk2.cyan(selectedRuntime)}`);
|
|
494
788
|
} else {
|
|
495
789
|
selectedRuntime = await select({
|
|
496
790
|
message: "Which container runtime would you like to use?",
|
|
@@ -526,6 +820,11 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
526
820
|
message: "Host repos path (for Forge repo scanning, leave empty to skip):",
|
|
527
821
|
default: ""
|
|
528
822
|
});
|
|
823
|
+
const extra_scan_dirs_raw = await input({
|
|
824
|
+
message: "Extra subdirectories to scan within repos path (comma-separated, e.g. ArjunKhera \u2014 leave empty to skip):",
|
|
825
|
+
default: ""
|
|
826
|
+
});
|
|
827
|
+
const host_repos_extra_scan_dirs = extra_scan_dirs_raw.split(",").map((d) => d.trim()).filter(Boolean);
|
|
529
828
|
const customize_ports = await confirm({
|
|
530
829
|
message: "Customize port assignments?",
|
|
531
830
|
default: false
|
|
@@ -556,19 +855,19 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
556
855
|
};
|
|
557
856
|
}
|
|
558
857
|
console.log("");
|
|
559
|
-
console.log(
|
|
560
|
-
console.log(
|
|
561
|
-
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."));
|
|
562
861
|
console.log("");
|
|
563
|
-
console.log(
|
|
564
|
-
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."));
|
|
565
864
|
console.log("");
|
|
566
865
|
const git_host = await input({
|
|
567
866
|
message: "Git server hostname:",
|
|
568
867
|
default: "github.com"
|
|
569
868
|
});
|
|
570
869
|
const host = git_host.trim();
|
|
571
|
-
const example = (repo) =>
|
|
870
|
+
const example = (repo) => chalk2.dim(` e.g., https://${host}/<owner>/${repo}`);
|
|
572
871
|
console.log("");
|
|
573
872
|
const anvil_notes = await input({
|
|
574
873
|
message: `Anvil notes repo URL:
|
|
@@ -589,8 +888,8 @@ ${example("forge-registry")}
|
|
|
589
888
|
validate: (v) => v.trim().length > 0 || "Forge needs a registry repo."
|
|
590
889
|
});
|
|
591
890
|
console.log("");
|
|
592
|
-
console.log(
|
|
593
|
-
console.log(
|
|
891
|
+
console.log(chalk2.bold("Authentication"));
|
|
892
|
+
console.log(chalk2.dim("A personal access token is required for private repositories."));
|
|
594
893
|
console.log("");
|
|
595
894
|
const github_token = await password({
|
|
596
895
|
message: "GitHub personal access token (leave empty to skip):",
|
|
@@ -600,6 +899,7 @@ ${example("forge-registry")}
|
|
|
600
899
|
...defaultConfig(),
|
|
601
900
|
data_dir,
|
|
602
901
|
host_repos_path,
|
|
902
|
+
host_repos_extra_scan_dirs,
|
|
603
903
|
runtime: runtime.name,
|
|
604
904
|
ports,
|
|
605
905
|
git_host: git_host.trim(),
|
|
@@ -611,7 +911,7 @@ ${example("forge-registry")}
|
|
|
611
911
|
github_token: github_token.trim()
|
|
612
912
|
};
|
|
613
913
|
}
|
|
614
|
-
const configSpinner =
|
|
914
|
+
const configSpinner = ora2("Saving configuration...").start();
|
|
615
915
|
try {
|
|
616
916
|
saveConfig(config);
|
|
617
917
|
configSpinner.succeed("Configuration saved to ~/.horus/config.yaml");
|
|
@@ -620,7 +920,7 @@ ${example("forge-registry")}
|
|
|
620
920
|
console.error(error.message);
|
|
621
921
|
process.exit(1);
|
|
622
922
|
}
|
|
623
|
-
const envSpinner =
|
|
923
|
+
const envSpinner = ora2("Generating .env file...").start();
|
|
624
924
|
try {
|
|
625
925
|
writeEnvFile(config);
|
|
626
926
|
envSpinner.succeed("Environment file written to ~/.horus/.env");
|
|
@@ -629,33 +929,33 @@ ${example("forge-registry")}
|
|
|
629
929
|
console.error(error.message);
|
|
630
930
|
process.exit(1);
|
|
631
931
|
}
|
|
632
|
-
const composeSpinner =
|
|
932
|
+
const composeSpinner = ora2("Installing docker-compose.yml...").start();
|
|
633
933
|
try {
|
|
634
|
-
installComposeFile();
|
|
934
|
+
installComposeFile(runtime.name);
|
|
635
935
|
composeSpinner.succeed("Compose file installed to ~/.horus/docker-compose.yml");
|
|
636
936
|
} catch (error) {
|
|
637
937
|
composeSpinner.fail("Failed to install compose file");
|
|
638
938
|
console.error(error.message);
|
|
639
939
|
process.exit(1);
|
|
640
940
|
}
|
|
641
|
-
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;
|
|
642
942
|
const reposToClone = [
|
|
643
|
-
{ url: config.repos.anvil_notes, dest:
|
|
644
|
-
{ url: config.repos.vault_knowledge, dest:
|
|
645
|
-
{ 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" }
|
|
646
946
|
].filter((r) => r.url);
|
|
647
947
|
if (reposToClone.length > 0) {
|
|
648
948
|
console.log("");
|
|
649
|
-
console.log(
|
|
650
|
-
|
|
949
|
+
console.log(chalk2.bold("Cloning repositories..."));
|
|
950
|
+
mkdirSync3(dataDir, { recursive: true });
|
|
651
951
|
for (const repo of reposToClone) {
|
|
652
|
-
const spinner =
|
|
653
|
-
if (
|
|
952
|
+
const spinner = ora2(`Cloning ${repo.label}...`).start();
|
|
953
|
+
if (existsSync4(join4(repo.dest, ".git"))) {
|
|
654
954
|
spinner.succeed(`${repo.label} already cloned`);
|
|
655
955
|
continue;
|
|
656
956
|
}
|
|
657
957
|
try {
|
|
658
|
-
|
|
958
|
+
mkdirSync3(repo.dest, { recursive: true });
|
|
659
959
|
const cloneUrl = injectToken(repo.url, config.github_token);
|
|
660
960
|
execSync(`git clone "${cloneUrl}" "${repo.dest}"`, {
|
|
661
961
|
stdio: "pipe",
|
|
@@ -666,37 +966,37 @@ ${example("forge-registry")}
|
|
|
666
966
|
spinner.fail(`Failed to clone ${repo.label}`);
|
|
667
967
|
const msg = error.message || "";
|
|
668
968
|
if (msg.includes("already exists and is not an empty directory")) {
|
|
669
|
-
console.log(
|
|
969
|
+
console.log(chalk2.dim(" Directory exists but has no .git \u2014 check the path."));
|
|
670
970
|
} else {
|
|
671
|
-
console.log(
|
|
971
|
+
console.log(chalk2.dim(` ${msg.split("\n")[0]}`));
|
|
672
972
|
}
|
|
673
|
-
console.log(
|
|
973
|
+
console.log(chalk2.dim(` URL: ${repo.url}`));
|
|
674
974
|
if (!config.github_token) {
|
|
675
|
-
console.log(
|
|
975
|
+
console.log(chalk2.dim(" Tip: Re-run setup and provide a GitHub token if the repo is private."));
|
|
676
976
|
}
|
|
677
977
|
process.exit(1);
|
|
678
978
|
}
|
|
679
979
|
}
|
|
680
980
|
}
|
|
681
981
|
console.log("");
|
|
682
|
-
console.log(
|
|
982
|
+
console.log(chalk2.bold("Pulling container images..."));
|
|
683
983
|
try {
|
|
684
984
|
await composeStreaming(runtime, ["pull", "--ignore-pull-failures"]);
|
|
685
985
|
} catch {
|
|
686
|
-
console.log(
|
|
687
|
-
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."));
|
|
688
988
|
}
|
|
689
989
|
console.log("");
|
|
690
|
-
console.log(
|
|
990
|
+
console.log(chalk2.bold("Starting Horus services..."));
|
|
691
991
|
try {
|
|
692
992
|
await composeStreaming(runtime, ["up", "-d"]);
|
|
693
993
|
} catch (error) {
|
|
694
|
-
console.log(
|
|
695
|
-
console.log(
|
|
994
|
+
console.log(chalk2.red("Failed to start services."));
|
|
995
|
+
console.log(chalk2.dim(error.message));
|
|
696
996
|
process.exit(1);
|
|
697
997
|
}
|
|
698
998
|
console.log("");
|
|
699
|
-
const healthSpinner =
|
|
999
|
+
const healthSpinner = ora2("Waiting for services to become healthy...").start();
|
|
700
1000
|
let lastStates = [];
|
|
701
1001
|
try {
|
|
702
1002
|
const states = await pollUntilHealthy(
|
|
@@ -704,7 +1004,7 @@ ${example("forge-registry")}
|
|
|
704
1004
|
(current) => {
|
|
705
1005
|
lastStates = current;
|
|
706
1006
|
const summary = current.map((s) => {
|
|
707
|
-
const icon = s.status === "healthy" ?
|
|
1007
|
+
const icon = s.status === "healthy" ? chalk2.green("*") : s.status === "starting" ? chalk2.yellow("~") : chalk2.red("x");
|
|
708
1008
|
return `${icon} ${s.name}`;
|
|
709
1009
|
}).join(" ");
|
|
710
1010
|
healthSpinner.text = `Waiting for services... ${summary}`;
|
|
@@ -716,20 +1016,33 @@ ${example("forge-registry")}
|
|
|
716
1016
|
lastStates = states;
|
|
717
1017
|
} catch (error) {
|
|
718
1018
|
healthSpinner.fail("Some services did not become healthy");
|
|
719
|
-
console.log(
|
|
1019
|
+
console.log(chalk2.dim(error.message));
|
|
720
1020
|
console.log("");
|
|
721
|
-
console.log(
|
|
1021
|
+
console.log(chalk2.dim("Tip: Check logs with `docker compose logs` from ~/.horus/"));
|
|
722
1022
|
process.exit(1);
|
|
723
1023
|
}
|
|
724
1024
|
console.log("");
|
|
725
|
-
|
|
726
|
-
|
|
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
|
+
}
|
|
727
1037
|
console.log("");
|
|
728
|
-
console.log(
|
|
729
|
-
console.log(
|
|
730
|
-
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"));
|
|
731
1040
|
console.log("");
|
|
732
|
-
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:"));
|
|
733
1046
|
console.log(` Anvil: http://localhost:${config.ports.anvil}`);
|
|
734
1047
|
console.log(` Vault REST: http://localhost:${config.ports.vault_rest}`);
|
|
735
1048
|
console.log(` Vault MCP: http://localhost:${config.ports.vault_mcp}`);
|
|
@@ -738,50 +1051,59 @@ ${example("forge-registry")}
|
|
|
738
1051
|
});
|
|
739
1052
|
|
|
740
1053
|
// src/commands/up.ts
|
|
741
|
-
import { Command as
|
|
742
|
-
import
|
|
743
|
-
import
|
|
744
|
-
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) => {
|
|
745
1058
|
if (!configExists() || !composeFileExists()) {
|
|
746
|
-
console.log(
|
|
747
|
-
console.log(
|
|
1059
|
+
console.log(chalk3.red("Horus is not set up yet."));
|
|
1060
|
+
console.log(chalk3.dim("Run `horus setup` first."));
|
|
748
1061
|
process.exit(1);
|
|
749
1062
|
}
|
|
750
1063
|
const config = loadConfig();
|
|
751
|
-
const spinner =
|
|
1064
|
+
const spinner = ora3("Detecting runtime...").start();
|
|
752
1065
|
let runtime;
|
|
753
1066
|
try {
|
|
754
1067
|
runtime = await detectRuntime(config.runtime);
|
|
755
|
-
spinner.succeed(`Using ${
|
|
1068
|
+
spinner.succeed(`Using ${chalk3.cyan(runtime.name)}`);
|
|
756
1069
|
} catch (error) {
|
|
757
1070
|
spinner.fail("No container runtime found");
|
|
758
1071
|
console.log(error.message);
|
|
759
1072
|
process.exit(1);
|
|
760
1073
|
}
|
|
1074
|
+
if (opts.pull) {
|
|
1075
|
+
const pullSpinner = ora3("Pulling latest images...").start();
|
|
1076
|
+
try {
|
|
1077
|
+
await composeStreaming(runtime, ["pull", "--ignore-pull-failures"]);
|
|
1078
|
+
pullSpinner.succeed("Images up to date");
|
|
1079
|
+
} catch {
|
|
1080
|
+
pullSpinner.warn("Could not pull images, using cached");
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
761
1083
|
console.log("");
|
|
762
|
-
console.log(
|
|
1084
|
+
console.log(chalk3.bold("Starting Horus services..."));
|
|
763
1085
|
try {
|
|
764
1086
|
await composeStreaming(runtime, ["up", "-d"]);
|
|
765
1087
|
} catch (error) {
|
|
766
|
-
console.log(
|
|
767
|
-
console.log(
|
|
1088
|
+
console.log(chalk3.red("Failed to start services."));
|
|
1089
|
+
console.log(chalk3.dim(error.message));
|
|
768
1090
|
process.exit(1);
|
|
769
1091
|
}
|
|
770
1092
|
console.log("");
|
|
771
|
-
const statusSpinner =
|
|
1093
|
+
const statusSpinner = ora3("Checking service status...").start();
|
|
772
1094
|
try {
|
|
773
1095
|
const states = await checkAllHealth(runtime);
|
|
774
1096
|
statusSpinner.stop();
|
|
775
|
-
console.log(
|
|
1097
|
+
console.log(chalk3.bold("Service Status:"));
|
|
776
1098
|
for (const s of states) {
|
|
777
|
-
const color = s.status === "healthy" ?
|
|
1099
|
+
const color = s.status === "healthy" ? chalk3.green : s.status === "starting" ? chalk3.yellow : chalk3.red;
|
|
778
1100
|
console.log(` ${color(s.status.padEnd(10))} ${s.name}`);
|
|
779
1101
|
}
|
|
780
1102
|
const allHealthy = states.every((s) => s.status === "healthy");
|
|
781
1103
|
if (!allHealthy) {
|
|
782
1104
|
console.log("");
|
|
783
1105
|
console.log(
|
|
784
|
-
|
|
1106
|
+
chalk3.yellow("Some services are still starting. Run `horus status` to check progress.")
|
|
785
1107
|
);
|
|
786
1108
|
}
|
|
787
1109
|
} catch {
|
|
@@ -791,53 +1113,53 @@ var upCommand = new Command2("up").description("Start the Horus stack").action(a
|
|
|
791
1113
|
});
|
|
792
1114
|
|
|
793
1115
|
// src/commands/down.ts
|
|
794
|
-
import { Command as
|
|
795
|
-
import
|
|
796
|
-
import
|
|
797
|
-
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 () => {
|
|
798
1120
|
if (!configExists() || !composeFileExists()) {
|
|
799
|
-
console.log(
|
|
800
|
-
console.log(
|
|
1121
|
+
console.log(chalk4.red("Horus is not set up yet."));
|
|
1122
|
+
console.log(chalk4.dim("Run `horus setup` first."));
|
|
801
1123
|
process.exit(1);
|
|
802
1124
|
}
|
|
803
1125
|
const config = loadConfig();
|
|
804
|
-
const spinner =
|
|
1126
|
+
const spinner = ora4("Detecting runtime...").start();
|
|
805
1127
|
let runtime;
|
|
806
1128
|
try {
|
|
807
1129
|
runtime = await detectRuntime(config.runtime);
|
|
808
|
-
spinner.succeed(`Using ${
|
|
1130
|
+
spinner.succeed(`Using ${chalk4.cyan(runtime.name)}`);
|
|
809
1131
|
} catch (error) {
|
|
810
1132
|
spinner.fail("No container runtime found");
|
|
811
1133
|
console.log(error.message);
|
|
812
1134
|
process.exit(1);
|
|
813
1135
|
}
|
|
814
1136
|
console.log("");
|
|
815
|
-
console.log(
|
|
1137
|
+
console.log(chalk4.bold("Stopping Horus services..."));
|
|
816
1138
|
try {
|
|
817
1139
|
await composeStreaming(runtime, ["down"]);
|
|
818
1140
|
} catch (error) {
|
|
819
|
-
console.log(
|
|
820
|
-
console.log(
|
|
1141
|
+
console.log(chalk4.red("Failed to stop services."));
|
|
1142
|
+
console.log(chalk4.dim(error.message));
|
|
821
1143
|
process.exit(1);
|
|
822
1144
|
}
|
|
823
1145
|
console.log("");
|
|
824
|
-
console.log(
|
|
825
|
-
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."));
|
|
826
1148
|
console.log("");
|
|
827
1149
|
});
|
|
828
1150
|
|
|
829
1151
|
// src/commands/status.ts
|
|
830
|
-
import { Command as
|
|
831
|
-
import
|
|
832
|
-
import
|
|
833
|
-
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 () => {
|
|
834
1156
|
if (!configExists() || !composeFileExists()) {
|
|
835
|
-
console.log(
|
|
836
|
-
console.log(
|
|
1157
|
+
console.log(chalk5.red("Horus is not set up yet."));
|
|
1158
|
+
console.log(chalk5.dim("Run `horus setup` first."));
|
|
837
1159
|
process.exit(1);
|
|
838
1160
|
}
|
|
839
1161
|
const config = loadConfig();
|
|
840
|
-
const spinner =
|
|
1162
|
+
const spinner = ora5("Checking services...").start();
|
|
841
1163
|
let runtime;
|
|
842
1164
|
try {
|
|
843
1165
|
runtime = await detectRuntime(config.runtime);
|
|
@@ -858,28 +1180,28 @@ var statusCommand = new Command4("status").description("Show status of Horus ser
|
|
|
858
1180
|
}
|
|
859
1181
|
spinner.stop();
|
|
860
1182
|
console.log("");
|
|
861
|
-
console.log(
|
|
862
|
-
console.log(
|
|
863
|
-
console.log(` ${
|
|
864
|
-
console.log(` ${
|
|
865
|
-
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`);
|
|
866
1188
|
console.log("");
|
|
867
1189
|
if (containers.length === 0) {
|
|
868
|
-
console.log(
|
|
869
|
-
console.log(
|
|
1190
|
+
console.log(chalk5.yellow(" No services are running."));
|
|
1191
|
+
console.log(chalk5.dim(" Run `horus up` to start the stack."));
|
|
870
1192
|
console.log("");
|
|
871
1193
|
return;
|
|
872
1194
|
}
|
|
873
1195
|
const header = ` ${pad("SERVICE", 14)} ${pad("STATUS", 12)} ${pad("PORTS", 20)} ${pad("UPTIME", 20)}`;
|
|
874
|
-
console.log(
|
|
875
|
-
console.log(
|
|
1196
|
+
console.log(chalk5.bold(header));
|
|
1197
|
+
console.log(chalk5.dim(" " + "\u2500".repeat(66)));
|
|
876
1198
|
for (const service of SERVICES) {
|
|
877
1199
|
const container = containers.find(
|
|
878
1200
|
(c) => c.Service === service || c.Name?.includes(service)
|
|
879
1201
|
);
|
|
880
1202
|
if (!container) {
|
|
881
1203
|
console.log(
|
|
882
|
-
` ${pad(service, 14)} ${
|
|
1204
|
+
` ${pad(service, 14)} ${chalk5.red(pad("stopped", 12))} ${pad("-", 20)} ${pad("-", 20)}`
|
|
883
1205
|
);
|
|
884
1206
|
continue;
|
|
885
1207
|
}
|
|
@@ -897,9 +1219,9 @@ function pad(str, width) {
|
|
|
897
1219
|
}
|
|
898
1220
|
function getStatusColor(status) {
|
|
899
1221
|
const lower = status.toLowerCase();
|
|
900
|
-
if (lower === "healthy" || lower === "running") return
|
|
901
|
-
if (lower === "starting") return
|
|
902
|
-
return
|
|
1222
|
+
if (lower === "healthy" || lower === "running") return chalk5.green;
|
|
1223
|
+
if (lower === "starting") return chalk5.yellow;
|
|
1224
|
+
return chalk5.red;
|
|
903
1225
|
}
|
|
904
1226
|
function formatPorts(publishers) {
|
|
905
1227
|
if (!publishers || publishers.length === 0) return "-";
|
|
@@ -914,50 +1236,52 @@ function extractUptime(status) {
|
|
|
914
1236
|
}
|
|
915
1237
|
|
|
916
1238
|
// src/commands/config.ts
|
|
917
|
-
import { Command as
|
|
918
|
-
import
|
|
1239
|
+
import { Command as Command6 } from "commander";
|
|
1240
|
+
import chalk6 from "chalk";
|
|
919
1241
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
920
|
-
var configCommand = new
|
|
1242
|
+
var configCommand = new Command6("config").description("View or modify Horus configuration").action(async () => {
|
|
921
1243
|
if (!configExists()) {
|
|
922
|
-
console.log(
|
|
923
|
-
console.log(
|
|
1244
|
+
console.log(chalk6.red("Horus is not configured yet."));
|
|
1245
|
+
console.log(chalk6.dim("Run `horus setup` first."));
|
|
924
1246
|
process.exit(1);
|
|
925
1247
|
}
|
|
926
1248
|
const config = loadConfig();
|
|
927
1249
|
console.log("");
|
|
928
|
-
console.log(
|
|
929
|
-
console.log(
|
|
930
|
-
console.log(` ${
|
|
931
|
-
console.log(` ${
|
|
932
|
-
console.log(` ${
|
|
933
|
-
console.log(` ${
|
|
934
|
-
|
|
935
|
-
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)")}`);
|
|
1256
|
+
const extraDirs = (config.host_repos_extra_scan_dirs ?? []).join(", ");
|
|
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)")}`);
|
|
936
1260
|
console.log("");
|
|
937
|
-
console.log(
|
|
938
|
-
console.log(` ${
|
|
939
|
-
console.log(` ${
|
|
940
|
-
console.log(` ${
|
|
941
|
-
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}`);
|
|
942
1266
|
console.log("");
|
|
943
|
-
console.log(
|
|
944
|
-
console.log(` ${
|
|
945
|
-
console.log(` ${
|
|
946
|
-
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)")}`);
|
|
947
1271
|
console.log("");
|
|
948
|
-
console.log(
|
|
949
|
-
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>'`));
|
|
950
1274
|
console.log("");
|
|
951
1275
|
});
|
|
952
1276
|
configCommand.command("get <key>").description("Get a configuration value").action(async (key) => {
|
|
953
1277
|
if (!configExists()) {
|
|
954
|
-
console.log(
|
|
955
|
-
console.log(
|
|
1278
|
+
console.log(chalk6.red("Horus is not configured yet."));
|
|
1279
|
+
console.log(chalk6.dim("Run `horus setup` first."));
|
|
956
1280
|
process.exit(1);
|
|
957
1281
|
}
|
|
958
1282
|
if (!isValidKey(key)) {
|
|
959
|
-
console.log(
|
|
960
|
-
console.log(
|
|
1283
|
+
console.log(chalk6.red(`Unknown config key: ${key}`));
|
|
1284
|
+
console.log(chalk6.dim(`Valid keys: ${CONFIG_KEYS.join(", ")}`));
|
|
961
1285
|
process.exit(1);
|
|
962
1286
|
}
|
|
963
1287
|
const config = loadConfig();
|
|
@@ -970,28 +1294,29 @@ configCommand.command("get <key>").description("Get a configuration value").acti
|
|
|
970
1294
|
});
|
|
971
1295
|
configCommand.command("set <key> <value>").description("Set a configuration value").action(async (key, value) => {
|
|
972
1296
|
if (!configExists()) {
|
|
973
|
-
console.log(
|
|
974
|
-
console.log(
|
|
1297
|
+
console.log(chalk6.red("Horus is not configured yet."));
|
|
1298
|
+
console.log(chalk6.dim("Run `horus setup` first."));
|
|
975
1299
|
process.exit(1);
|
|
976
1300
|
}
|
|
977
1301
|
if (!isValidKey(key)) {
|
|
978
|
-
console.log(
|
|
979
|
-
console.log(
|
|
1302
|
+
console.log(chalk6.red(`Unknown config key: ${key}`));
|
|
1303
|
+
console.log(chalk6.dim(`Valid keys: ${CONFIG_KEYS.join(", ")}`));
|
|
980
1304
|
process.exit(1);
|
|
981
1305
|
}
|
|
982
1306
|
let config = loadConfig();
|
|
983
1307
|
try {
|
|
984
1308
|
config = setConfigValue(config, key, value);
|
|
985
1309
|
} catch (error) {
|
|
986
|
-
console.log(
|
|
1310
|
+
console.log(chalk6.red(error.message));
|
|
987
1311
|
process.exit(1);
|
|
988
1312
|
}
|
|
989
1313
|
saveConfig(config);
|
|
990
1314
|
writeEnvFile(config);
|
|
991
|
-
console.log(
|
|
1315
|
+
console.log(chalk6.green(`Set ${key} and regenerated .env file.`));
|
|
992
1316
|
const needsRestart = [
|
|
993
1317
|
"data-dir",
|
|
994
1318
|
"host-repos-path",
|
|
1319
|
+
"host-repos-extra-scan-dirs",
|
|
995
1320
|
"runtime",
|
|
996
1321
|
"port.anvil",
|
|
997
1322
|
"port.vault-rest",
|
|
@@ -999,17 +1324,17 @@ configCommand.command("set <key> <value>").description("Set a configuration valu
|
|
|
999
1324
|
"port.forge"
|
|
1000
1325
|
];
|
|
1001
1326
|
if (needsRestart.includes(key)) {
|
|
1002
|
-
console.log(
|
|
1327
|
+
console.log(chalk6.yellow("Restart required for changes to take effect."));
|
|
1003
1328
|
if (process.stdin.isTTY) {
|
|
1004
1329
|
const restart = await confirm2({
|
|
1005
1330
|
message: "Restart Horus now?",
|
|
1006
1331
|
default: false
|
|
1007
1332
|
});
|
|
1008
1333
|
if (restart) {
|
|
1009
|
-
console.log(
|
|
1334
|
+
console.log(chalk6.dim("Run `horus down && horus up` to restart."));
|
|
1010
1335
|
}
|
|
1011
1336
|
} else {
|
|
1012
|
-
console.log(
|
|
1337
|
+
console.log(chalk6.dim("Run `horus down && horus up` to restart."));
|
|
1013
1338
|
}
|
|
1014
1339
|
}
|
|
1015
1340
|
});
|
|
@@ -1017,212 +1342,6 @@ function isValidKey(key) {
|
|
|
1017
1342
|
return CONFIG_KEYS.includes(key);
|
|
1018
1343
|
}
|
|
1019
1344
|
|
|
1020
|
-
// src/commands/connect.ts
|
|
1021
|
-
import { Command as Command6 } from "commander";
|
|
1022
|
-
import chalk6 from "chalk";
|
|
1023
|
-
import ora5 from "ora";
|
|
1024
|
-
import { checkbox } from "@inquirer/prompts";
|
|
1025
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
1026
|
-
import { join as join4 } from "path";
|
|
1027
|
-
import { homedir as homedir3 } from "os";
|
|
1028
|
-
function detectInstalledClients() {
|
|
1029
|
-
const detected = [];
|
|
1030
|
-
const home = homedir3();
|
|
1031
|
-
const claudeDesktopDir = join4(home, "Library", "Application Support", "Claude");
|
|
1032
|
-
if (existsSync4(claudeDesktopDir)) {
|
|
1033
|
-
detected.push("claude-desktop");
|
|
1034
|
-
}
|
|
1035
|
-
const claudeCodeDir = join4(home, ".claude");
|
|
1036
|
-
if (existsSync4(claudeCodeDir)) {
|
|
1037
|
-
detected.push("claude-code");
|
|
1038
|
-
}
|
|
1039
|
-
const cursorDir = join4(home, ".cursor");
|
|
1040
|
-
const cursorAppDir = join4(home, "Library", "Application Support", "Cursor");
|
|
1041
|
-
if (existsSync4(cursorDir) || existsSync4(cursorAppDir)) {
|
|
1042
|
-
detected.push("cursor");
|
|
1043
|
-
}
|
|
1044
|
-
return detected;
|
|
1045
|
-
}
|
|
1046
|
-
function getConfigPath(target) {
|
|
1047
|
-
const home = homedir3();
|
|
1048
|
-
switch (target) {
|
|
1049
|
-
case "claude-desktop":
|
|
1050
|
-
return join4(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1051
|
-
case "claude-code":
|
|
1052
|
-
return join4(home, ".claude", "settings.json");
|
|
1053
|
-
case "cursor":
|
|
1054
|
-
return join4(home, ".cursor", "mcp.json");
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
function mergeAndWriteConfig(configPath, mcpServers) {
|
|
1058
|
-
let existing = {};
|
|
1059
|
-
if (existsSync4(configPath)) {
|
|
1060
|
-
try {
|
|
1061
|
-
const raw = readFileSync3(configPath, "utf-8");
|
|
1062
|
-
existing = JSON.parse(raw);
|
|
1063
|
-
} catch {
|
|
1064
|
-
existing = {};
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
const existingServers = existing.mcpServers ?? {};
|
|
1068
|
-
existing.mcpServers = { ...existingServers, ...mcpServers };
|
|
1069
|
-
const dir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
1070
|
-
mkdirSync3(dir, { recursive: true });
|
|
1071
|
-
writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
1072
|
-
}
|
|
1073
|
-
async function syncSkills(runtime) {
|
|
1074
|
-
const home = homedir3();
|
|
1075
|
-
const skillsBase = join4(home, ".claude", "skills");
|
|
1076
|
-
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
1077
|
-
const forgeContainer = "horus-forge-1";
|
|
1078
|
-
for (const skill of skills) {
|
|
1079
|
-
const destDir = join4(skillsBase, skill);
|
|
1080
|
-
mkdirSync3(destDir, { recursive: true });
|
|
1081
|
-
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
1082
|
-
const dest = join4(destDir, "SKILL.md");
|
|
1083
|
-
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
1084
|
-
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
1085
|
-
writeFileSync3(dest, result.stdout, "utf-8");
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
async function syncSkillsForCursor(runtime) {
|
|
1090
|
-
const home = homedir3();
|
|
1091
|
-
const rulesDir = join4(home, ".cursor", "rules");
|
|
1092
|
-
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
1093
|
-
const forgeContainer = "horus-forge-1";
|
|
1094
|
-
mkdirSync3(rulesDir, { recursive: true });
|
|
1095
|
-
for (const skill of skills) {
|
|
1096
|
-
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
1097
|
-
const dest = join4(rulesDir, `${skill}.mdc`);
|
|
1098
|
-
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
1099
|
-
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
1100
|
-
const frontmatter = `---
|
|
1101
|
-
description: Horus ${skill} reference
|
|
1102
|
-
alwaysApply: true
|
|
1103
|
-
---
|
|
1104
|
-
|
|
1105
|
-
`;
|
|
1106
|
-
writeFileSync3(dest, frontmatter + result.stdout, "utf-8");
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
function printNextSteps(targets) {
|
|
1111
|
-
console.log("");
|
|
1112
|
-
console.log(chalk6.bold("Next steps:"));
|
|
1113
|
-
for (const target of targets) {
|
|
1114
|
-
switch (target) {
|
|
1115
|
-
case "claude-desktop":
|
|
1116
|
-
console.log(` ${chalk6.cyan("Claude Desktop")} Restart Claude Desktop to pick up the new MCP configuration`);
|
|
1117
|
-
break;
|
|
1118
|
-
case "claude-code":
|
|
1119
|
-
console.log(` ${chalk6.cyan("Claude Code")} Start a new Claude Code session`);
|
|
1120
|
-
break;
|
|
1121
|
-
case "cursor":
|
|
1122
|
-
console.log(` ${chalk6.cyan("Cursor")} Restart Cursor to pick up the new MCP configuration and rules`);
|
|
1123
|
-
break;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
console.log("");
|
|
1127
|
-
}
|
|
1128
|
-
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) => {
|
|
1129
|
-
console.log("");
|
|
1130
|
-
console.log(chalk6.bold("Horus Connect"));
|
|
1131
|
-
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"));
|
|
1132
|
-
console.log("");
|
|
1133
|
-
const config = loadConfig();
|
|
1134
|
-
const runtimeSpinner = ora5("Detecting runtime...").start();
|
|
1135
|
-
let runtime;
|
|
1136
|
-
try {
|
|
1137
|
-
runtime = await detectRuntime(config.runtime);
|
|
1138
|
-
runtimeSpinner.succeed(`Using ${chalk6.cyan(runtime.name)}`);
|
|
1139
|
-
} catch (error) {
|
|
1140
|
-
runtimeSpinner.fail("No container runtime found");
|
|
1141
|
-
console.log(error.message);
|
|
1142
|
-
process.exit(1);
|
|
1143
|
-
}
|
|
1144
|
-
const runningSpinner = ora5("Checking Horus status...").start();
|
|
1145
|
-
const running = await runtime.isRunning();
|
|
1146
|
-
if (!running) {
|
|
1147
|
-
runningSpinner.fail("Horus is not running");
|
|
1148
|
-
console.log(chalk6.dim("Run `horus up` first, then re-run `horus connect`."));
|
|
1149
|
-
process.exit(1);
|
|
1150
|
-
}
|
|
1151
|
-
runningSpinner.succeed("Horus is running");
|
|
1152
|
-
let targets = [];
|
|
1153
|
-
if (opts.target === "all") {
|
|
1154
|
-
targets = ["claude-desktop", "claude-code", "cursor"];
|
|
1155
|
-
} else if (opts.target) {
|
|
1156
|
-
const valid = ["claude-desktop", "claude-code", "cursor"];
|
|
1157
|
-
if (!valid.includes(opts.target)) {
|
|
1158
|
-
console.log(chalk6.red(`Invalid target: ${opts.target}`));
|
|
1159
|
-
console.log(chalk6.dim("Valid targets: claude-desktop, claude-code, cursor, all"));
|
|
1160
|
-
process.exit(1);
|
|
1161
|
-
}
|
|
1162
|
-
targets = [opts.target];
|
|
1163
|
-
} else {
|
|
1164
|
-
const detected = detectInstalledClients();
|
|
1165
|
-
if (detected.length === 0) {
|
|
1166
|
-
console.log(chalk6.yellow("No supported clients detected (Claude Desktop, Claude Code, or Cursor)."));
|
|
1167
|
-
console.log(chalk6.dim("Use --target to specify a client manually."));
|
|
1168
|
-
process.exit(1);
|
|
1169
|
-
}
|
|
1170
|
-
if (opts.yes) {
|
|
1171
|
-
targets = detected;
|
|
1172
|
-
console.log(`Detected clients: ${detected.map((t) => chalk6.cyan(t)).join(", ")}`);
|
|
1173
|
-
} else {
|
|
1174
|
-
const chosen = await checkbox({
|
|
1175
|
-
message: "Select clients to configure:",
|
|
1176
|
-
choices: detected.map((t) => ({ name: t, value: t, checked: true })),
|
|
1177
|
-
validate: (input2) => input2.length > 0 ? true : "Select at least one client."
|
|
1178
|
-
});
|
|
1179
|
-
targets = chosen;
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
if (targets.length === 0) {
|
|
1183
|
-
console.log(chalk6.yellow("No clients selected. Exiting."));
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
const host = opts.host;
|
|
1187
|
-
const mcpServers = {
|
|
1188
|
-
anvil: { url: `http://${host}:${config.ports.anvil}/sse` },
|
|
1189
|
-
vault: { url: `http://${host}:${config.ports.vault_mcp}/sse` },
|
|
1190
|
-
forge: { url: `http://${host}:${config.ports.forge}/sse` }
|
|
1191
|
-
};
|
|
1192
|
-
for (const target of targets) {
|
|
1193
|
-
const configPath = getConfigPath(target);
|
|
1194
|
-
const writeSpinner = ora5(`Configuring ${chalk6.cyan(target)}...`).start();
|
|
1195
|
-
try {
|
|
1196
|
-
mergeAndWriteConfig(configPath, mcpServers);
|
|
1197
|
-
writeSpinner.succeed(`Configured ${chalk6.cyan(target)} \u2014 ${chalk6.dim(configPath)}`);
|
|
1198
|
-
} catch (error) {
|
|
1199
|
-
writeSpinner.fail(`Failed to configure ${target}`);
|
|
1200
|
-
console.log(chalk6.dim(error.message));
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
if (targets.includes("claude-code")) {
|
|
1204
|
-
const skillsSpinner = ora5("Syncing horus-core skills...").start();
|
|
1205
|
-
try {
|
|
1206
|
-
await syncSkills(runtime);
|
|
1207
|
-
skillsSpinner.succeed("horus-core skills synced to ~/.claude/skills/");
|
|
1208
|
-
} catch (error) {
|
|
1209
|
-
skillsSpinner.warn("Could not sync skills (Forge container may not be running)");
|
|
1210
|
-
console.log(chalk6.dim(error.message));
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
if (targets.includes("cursor")) {
|
|
1214
|
-
const cursorRulesSpinner = ora5("Syncing horus-core rules for Cursor...").start();
|
|
1215
|
-
try {
|
|
1216
|
-
await syncSkillsForCursor(runtime);
|
|
1217
|
-
cursorRulesSpinner.succeed("horus-core rules synced to ~/.cursor/rules/");
|
|
1218
|
-
} catch (error) {
|
|
1219
|
-
cursorRulesSpinner.warn("Could not sync Cursor rules (Forge container may not be running)");
|
|
1220
|
-
console.log(chalk6.dim(error.message));
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
printNextSteps(targets);
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
1345
|
// src/commands/update.ts
|
|
1227
1346
|
import { Command as Command7 } from "commander";
|
|
1228
1347
|
import chalk7 from "chalk";
|