@arkhera30/cli 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +205 -86
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,6 +9,9 @@ import { createRequire } from "module";
|
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import ora from "ora";
|
|
12
|
+
import { execSync } from "child_process";
|
|
13
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
14
|
+
import { join as join3 } from "path";
|
|
12
15
|
import { input, confirm, number, select } from "@inquirer/prompts";
|
|
13
16
|
|
|
14
17
|
// src/lib/config.ts
|
|
@@ -32,8 +35,8 @@ var DEFAULT_PORTS = {
|
|
|
32
35
|
};
|
|
33
36
|
var DEFAULT_REPOS = {
|
|
34
37
|
anvil_notes: "",
|
|
35
|
-
vault_knowledge: "
|
|
36
|
-
forge_registry: "
|
|
38
|
+
vault_knowledge: "",
|
|
39
|
+
forge_registry: ""
|
|
37
40
|
};
|
|
38
41
|
var DEFAULT_DATA_DIR = join(homedir(), ".horus", "data");
|
|
39
42
|
var SERVICES = [
|
|
@@ -52,6 +55,7 @@ function defaultConfig() {
|
|
|
52
55
|
data_dir: DEFAULT_DATA_DIR,
|
|
53
56
|
runtime: "docker",
|
|
54
57
|
ports: { ...DEFAULT_PORTS },
|
|
58
|
+
git_host: "github.com",
|
|
55
59
|
repos: { ...DEFAULT_REPOS },
|
|
56
60
|
host_repos_path: "",
|
|
57
61
|
github_token: ""
|
|
@@ -80,6 +84,7 @@ function loadConfig() {
|
|
|
80
84
|
vault_mcp: parsed.ports?.vault_mcp ?? defaults.ports.vault_mcp,
|
|
81
85
|
forge: parsed.ports?.forge ?? defaults.ports.forge
|
|
82
86
|
},
|
|
87
|
+
git_host: parsed.git_host ?? defaults.git_host,
|
|
83
88
|
repos: {
|
|
84
89
|
anvil_notes: parsed.repos?.anvil_notes ?? defaults.repos.anvil_notes,
|
|
85
90
|
vault_knowledge: parsed.repos?.vault_knowledge ?? defaults.repos.vault_knowledge,
|
|
@@ -118,10 +123,7 @@ function generateEnv(config) {
|
|
|
118
123
|
`VAULT_MCP_PORT=${config.ports.vault_mcp}`,
|
|
119
124
|
`FORGE_PORT=${config.ports.forge}`,
|
|
120
125
|
"",
|
|
121
|
-
"#
|
|
122
|
-
`GITHUB_TOKEN=${config.github_token}`,
|
|
123
|
-
"",
|
|
124
|
-
"# Repository URLs",
|
|
126
|
+
"# Repository URLs (must be HTTPS \u2014 container services do not have SSH keys)",
|
|
125
127
|
`ANVIL_REPO_URL=${config.repos.anvil_notes}`,
|
|
126
128
|
`VAULT_KNOWLEDGE_REPO_URL=${config.repos.vault_knowledge}`,
|
|
127
129
|
`FORGE_REGISTRY_REPO_URL=${config.repos.forge_registry}`,
|
|
@@ -142,7 +144,11 @@ var CONFIG_KEYS = [
|
|
|
142
144
|
"port.vault-rest",
|
|
143
145
|
"port.vault-mcp",
|
|
144
146
|
"port.forge",
|
|
145
|
-
"github-token"
|
|
147
|
+
"github-token",
|
|
148
|
+
"git-host",
|
|
149
|
+
"repo.anvil-notes",
|
|
150
|
+
"repo.vault-knowledge",
|
|
151
|
+
"repo.forge-registry"
|
|
146
152
|
];
|
|
147
153
|
function getConfigValue(config, key) {
|
|
148
154
|
switch (key) {
|
|
@@ -162,6 +168,14 @@ function getConfigValue(config, key) {
|
|
|
162
168
|
return String(config.ports.forge);
|
|
163
169
|
case "github-token":
|
|
164
170
|
return config.github_token;
|
|
171
|
+
case "git-host":
|
|
172
|
+
return config.git_host;
|
|
173
|
+
case "repo.anvil-notes":
|
|
174
|
+
return config.repos.anvil_notes;
|
|
175
|
+
case "repo.vault-knowledge":
|
|
176
|
+
return config.repos.vault_knowledge;
|
|
177
|
+
case "repo.forge-registry":
|
|
178
|
+
return config.repos.forge_registry;
|
|
165
179
|
}
|
|
166
180
|
}
|
|
167
181
|
function setConfigValue(config, key, value) {
|
|
@@ -194,6 +208,18 @@ function setConfigValue(config, key, value) {
|
|
|
194
208
|
case "github-token":
|
|
195
209
|
updated.github_token = value;
|
|
196
210
|
break;
|
|
211
|
+
case "git-host":
|
|
212
|
+
updated.git_host = value;
|
|
213
|
+
break;
|
|
214
|
+
case "repo.anvil-notes":
|
|
215
|
+
updated.repos = { ...updated.repos, anvil_notes: value };
|
|
216
|
+
break;
|
|
217
|
+
case "repo.vault-knowledge":
|
|
218
|
+
updated.repos = { ...updated.repos, vault_knowledge: value };
|
|
219
|
+
break;
|
|
220
|
+
case "repo.forge-registry":
|
|
221
|
+
updated.repos = { ...updated.repos, forge_registry: value };
|
|
222
|
+
break;
|
|
197
223
|
}
|
|
198
224
|
return updated;
|
|
199
225
|
}
|
|
@@ -298,13 +324,6 @@ async function detectRuntime(preferred) {
|
|
|
298
324
|
"No container runtime found.\n\nHorus requires Docker or Podman with the Compose plugin.\n\nInstall one of:\n - Docker Desktop: https://www.docker.com/products/docker-desktop/\n - Podman Desktop: https://podman-desktop.io/\n"
|
|
299
325
|
);
|
|
300
326
|
}
|
|
301
|
-
async function registryLogin(runtime, registry, token, username = "horus") {
|
|
302
|
-
const result = await execa(runtime.name, ["login", registry, "-u", username, "--password-stdin"], {
|
|
303
|
-
input: token,
|
|
304
|
-
reject: false
|
|
305
|
-
});
|
|
306
|
-
return result.exitCode === 0;
|
|
307
|
-
}
|
|
308
327
|
async function composeStreaming(runtime, args) {
|
|
309
328
|
const bin = runtime.name;
|
|
310
329
|
const result = await execa(bin, ["compose", ...args], {
|
|
@@ -409,7 +428,7 @@ function installComposeFile() {
|
|
|
409
428
|
}
|
|
410
429
|
|
|
411
430
|
// src/commands/setup.ts
|
|
412
|
-
var setupCommand = new Command("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").action(async (opts) => {
|
|
431
|
+
var setupCommand = new Command("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").action(async (opts) => {
|
|
413
432
|
console.log("");
|
|
414
433
|
console.log(chalk.bold("Horus Setup"));
|
|
415
434
|
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"));
|
|
@@ -470,11 +489,18 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
470
489
|
const runtime = await detectRuntime(selectedRuntime);
|
|
471
490
|
let config;
|
|
472
491
|
if (opts.yes) {
|
|
492
|
+
const defaults = defaultConfig();
|
|
473
493
|
config = {
|
|
474
|
-
...
|
|
494
|
+
...defaults,
|
|
475
495
|
runtime: runtime.name,
|
|
476
496
|
data_dir: opts.dataDir || DEFAULT_DATA_DIR,
|
|
477
|
-
host_repos_path: opts.reposPath || ""
|
|
497
|
+
host_repos_path: opts.reposPath || "",
|
|
498
|
+
git_host: opts.gitHost || defaults.git_host,
|
|
499
|
+
repos: {
|
|
500
|
+
anvil_notes: opts.anvilRepo || process.env.ANVIL_REPO_URL || defaults.repos.anvil_notes,
|
|
501
|
+
vault_knowledge: opts.vaultRepo || process.env.VAULT_KNOWLEDGE_REPO_URL || defaults.repos.vault_knowledge,
|
|
502
|
+
forge_registry: opts.forgeRepo || process.env.FORGE_REGISTRY_REPO_URL || defaults.repos.forge_registry
|
|
503
|
+
}
|
|
478
504
|
};
|
|
479
505
|
} else {
|
|
480
506
|
const data_dir = await input({
|
|
@@ -514,12 +540,52 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
514
540
|
forge: forge ?? DEFAULT_PORTS.forge
|
|
515
541
|
};
|
|
516
542
|
}
|
|
543
|
+
console.log("");
|
|
544
|
+
console.log(chalk.bold("Repository Configuration"));
|
|
545
|
+
console.log(chalk.dim("Horus stores notes and knowledge in Git repos you own."));
|
|
546
|
+
console.log(chalk.dim("Create empty repos on your Git server, then paste the URLs below."));
|
|
547
|
+
console.log("");
|
|
548
|
+
console.log(chalk.yellow(" Use HTTPS URLs \u2014 container services do not have SSH keys."));
|
|
549
|
+
console.log(chalk.dim(" SSH URLs (git@github.com:...) will fail at runtime inside Docker/Podman."));
|
|
550
|
+
console.log(chalk.dim(" Set GITHUB_TOKEN for private repos."));
|
|
551
|
+
console.log("");
|
|
552
|
+
const git_host = await input({
|
|
553
|
+
message: "Git server hostname:",
|
|
554
|
+
default: "github.com"
|
|
555
|
+
});
|
|
556
|
+
const host = git_host.trim();
|
|
557
|
+
const example = (repo) => chalk.dim(` e.g., https://${host}/<owner>/${repo}`);
|
|
558
|
+
console.log("");
|
|
559
|
+
const anvil_notes = await input({
|
|
560
|
+
message: `Anvil notes repo URL:
|
|
561
|
+
${example("horus-notes")}
|
|
562
|
+
`,
|
|
563
|
+
validate: (v) => v.trim().length > 0 || "Anvil needs a notes repo to store your data."
|
|
564
|
+
});
|
|
565
|
+
const vault_knowledge = await input({
|
|
566
|
+
message: `Vault knowledge-base repo URL:
|
|
567
|
+
${example("knowledge-base")}
|
|
568
|
+
`,
|
|
569
|
+
validate: (v) => v.trim().length > 0 || "Vault needs a knowledge-base repo."
|
|
570
|
+
});
|
|
571
|
+
const forge_registry = await input({
|
|
572
|
+
message: `Forge registry repo URL:
|
|
573
|
+
${example("forge-registry")}
|
|
574
|
+
`,
|
|
575
|
+
validate: (v) => v.trim().length > 0 || "Forge needs a registry repo."
|
|
576
|
+
});
|
|
517
577
|
config = {
|
|
518
578
|
...defaultConfig(),
|
|
519
579
|
data_dir,
|
|
520
580
|
host_repos_path,
|
|
521
581
|
runtime: runtime.name,
|
|
522
|
-
ports
|
|
582
|
+
ports,
|
|
583
|
+
git_host: git_host.trim(),
|
|
584
|
+
repos: {
|
|
585
|
+
anvil_notes: anvil_notes.trim(),
|
|
586
|
+
vault_knowledge: vault_knowledge.trim(),
|
|
587
|
+
forge_registry: forge_registry.trim()
|
|
588
|
+
}
|
|
523
589
|
};
|
|
524
590
|
}
|
|
525
591
|
const configSpinner = ora("Saving configuration...").start();
|
|
@@ -549,14 +615,41 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
549
615
|
console.error(error.message);
|
|
550
616
|
process.exit(1);
|
|
551
617
|
}
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
618
|
+
const dataDir = config.data_dir.startsWith("~") ? join3(process.env.HOME || "", config.data_dir.slice(1)) : config.data_dir;
|
|
619
|
+
const reposToClone = [
|
|
620
|
+
{ url: config.repos.anvil_notes, dest: join3(dataDir, "notes"), label: "Anvil notes" },
|
|
621
|
+
{ url: config.repos.vault_knowledge, dest: join3(dataDir, "knowledge-base"), label: "Vault knowledge-base" },
|
|
622
|
+
{ url: config.repos.forge_registry, dest: join3(dataDir, "registry"), label: "Forge registry" }
|
|
623
|
+
].filter((r) => r.url);
|
|
624
|
+
if (reposToClone.length > 0) {
|
|
625
|
+
console.log("");
|
|
626
|
+
console.log(chalk.bold("Cloning repositories..."));
|
|
627
|
+
mkdirSync2(dataDir, { recursive: true });
|
|
628
|
+
for (const repo of reposToClone) {
|
|
629
|
+
const spinner = ora(`Cloning ${repo.label}...`).start();
|
|
630
|
+
if (existsSync3(join3(repo.dest, ".git"))) {
|
|
631
|
+
spinner.succeed(`${repo.label} already cloned`);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
mkdirSync2(repo.dest, { recursive: true });
|
|
636
|
+
execSync(`git clone "${repo.url}" "${repo.dest}"`, {
|
|
637
|
+
stdio: "pipe",
|
|
638
|
+
timeout: 6e4
|
|
639
|
+
});
|
|
640
|
+
spinner.succeed(`${repo.label} cloned`);
|
|
641
|
+
} catch (error) {
|
|
642
|
+
spinner.fail(`Failed to clone ${repo.label}`);
|
|
643
|
+
const msg = error.message || "";
|
|
644
|
+
if (msg.includes("already exists and is not an empty directory")) {
|
|
645
|
+
console.log(chalk.dim(" Directory exists but has no .git \u2014 check the path."));
|
|
646
|
+
} else {
|
|
647
|
+
console.log(chalk.dim(` ${msg.split("\n")[0]}`));
|
|
648
|
+
}
|
|
649
|
+
console.log(chalk.dim(` URL: ${repo.url}`));
|
|
650
|
+
console.log(chalk.dim(" Ensure you have git access (SSH key or credential helper)."));
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
560
653
|
}
|
|
561
654
|
}
|
|
562
655
|
console.log("");
|
|
@@ -812,6 +905,7 @@ var configCommand = new Command5("config").description("View or modify Horus con
|
|
|
812
905
|
console.log(` ${chalk5.bold("data-dir:")} ${config.data_dir}`);
|
|
813
906
|
console.log(` ${chalk5.bold("runtime:")} ${config.runtime}`);
|
|
814
907
|
console.log(` ${chalk5.bold("host-repos-path:")} ${config.host_repos_path || chalk5.dim("(not set)")}`);
|
|
908
|
+
console.log(` ${chalk5.bold("git-host:")} ${config.git_host || chalk5.dim("(not set)")}`);
|
|
815
909
|
console.log(` ${chalk5.bold("github-token:")} ${config.github_token ? maskApiKey(config.github_token) : chalk5.dim("(not set)")}`);
|
|
816
910
|
console.log("");
|
|
817
911
|
console.log(chalk5.bold(" Ports:"));
|
|
@@ -902,23 +996,23 @@ import { Command as Command6 } from "commander";
|
|
|
902
996
|
import chalk6 from "chalk";
|
|
903
997
|
import ora5 from "ora";
|
|
904
998
|
import { checkbox } from "@inquirer/prompts";
|
|
905
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as
|
|
906
|
-
import { join as
|
|
999
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
1000
|
+
import { join as join4 } from "path";
|
|
907
1001
|
import { homedir as homedir3 } from "os";
|
|
908
1002
|
function detectInstalledClients() {
|
|
909
1003
|
const detected = [];
|
|
910
1004
|
const home = homedir3();
|
|
911
|
-
const claudeDesktopDir =
|
|
912
|
-
if (
|
|
1005
|
+
const claudeDesktopDir = join4(home, "Library", "Application Support", "Claude");
|
|
1006
|
+
if (existsSync4(claudeDesktopDir)) {
|
|
913
1007
|
detected.push("claude-desktop");
|
|
914
1008
|
}
|
|
915
|
-
const claudeCodeDir =
|
|
916
|
-
if (
|
|
1009
|
+
const claudeCodeDir = join4(home, ".claude");
|
|
1010
|
+
if (existsSync4(claudeCodeDir)) {
|
|
917
1011
|
detected.push("claude-code");
|
|
918
1012
|
}
|
|
919
|
-
const cursorDir =
|
|
920
|
-
const cursorAppDir =
|
|
921
|
-
if (
|
|
1013
|
+
const cursorDir = join4(home, ".cursor");
|
|
1014
|
+
const cursorAppDir = join4(home, "Library", "Application Support", "Cursor");
|
|
1015
|
+
if (existsSync4(cursorDir) || existsSync4(cursorAppDir)) {
|
|
922
1016
|
detected.push("cursor");
|
|
923
1017
|
}
|
|
924
1018
|
return detected;
|
|
@@ -927,16 +1021,16 @@ function getConfigPath(target) {
|
|
|
927
1021
|
const home = homedir3();
|
|
928
1022
|
switch (target) {
|
|
929
1023
|
case "claude-desktop":
|
|
930
|
-
return
|
|
1024
|
+
return join4(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
931
1025
|
case "claude-code":
|
|
932
|
-
return
|
|
1026
|
+
return join4(home, ".claude", "settings.json");
|
|
933
1027
|
case "cursor":
|
|
934
|
-
return
|
|
1028
|
+
return join4(home, ".cursor", "mcp.json");
|
|
935
1029
|
}
|
|
936
1030
|
}
|
|
937
1031
|
function mergeAndWriteConfig(configPath, mcpServers) {
|
|
938
1032
|
let existing = {};
|
|
939
|
-
if (
|
|
1033
|
+
if (existsSync4(configPath)) {
|
|
940
1034
|
try {
|
|
941
1035
|
const raw = readFileSync3(configPath, "utf-8");
|
|
942
1036
|
existing = JSON.parse(raw);
|
|
@@ -947,25 +1041,46 @@ function mergeAndWriteConfig(configPath, mcpServers) {
|
|
|
947
1041
|
const existingServers = existing.mcpServers ?? {};
|
|
948
1042
|
existing.mcpServers = { ...existingServers, ...mcpServers };
|
|
949
1043
|
const dir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
950
|
-
|
|
1044
|
+
mkdirSync3(dir, { recursive: true });
|
|
951
1045
|
writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
952
1046
|
}
|
|
953
1047
|
async function syncSkills(runtime) {
|
|
954
1048
|
const home = homedir3();
|
|
955
|
-
const skillsBase =
|
|
1049
|
+
const skillsBase = join4(home, ".claude", "skills");
|
|
956
1050
|
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
957
1051
|
const forgeContainer = "horus-forge-1";
|
|
958
1052
|
for (const skill of skills) {
|
|
959
|
-
const destDir =
|
|
960
|
-
|
|
1053
|
+
const destDir = join4(skillsBase, skill);
|
|
1054
|
+
mkdirSync3(destDir, { recursive: true });
|
|
961
1055
|
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
962
|
-
const dest =
|
|
1056
|
+
const dest = join4(destDir, "SKILL.md");
|
|
963
1057
|
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
964
1058
|
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
965
1059
|
writeFileSync3(dest, result.stdout, "utf-8");
|
|
966
1060
|
}
|
|
967
1061
|
}
|
|
968
1062
|
}
|
|
1063
|
+
async function syncSkillsForCursor(runtime) {
|
|
1064
|
+
const home = homedir3();
|
|
1065
|
+
const rulesDir = join4(home, ".cursor", "rules");
|
|
1066
|
+
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
1067
|
+
const forgeContainer = "horus-forge-1";
|
|
1068
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
1069
|
+
for (const skill of skills) {
|
|
1070
|
+
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
1071
|
+
const dest = join4(rulesDir, `${skill}.mdc`);
|
|
1072
|
+
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
1073
|
+
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
1074
|
+
const frontmatter = `---
|
|
1075
|
+
description: Horus ${skill} reference
|
|
1076
|
+
alwaysApply: true
|
|
1077
|
+
---
|
|
1078
|
+
|
|
1079
|
+
`;
|
|
1080
|
+
writeFileSync3(dest, frontmatter + result.stdout, "utf-8");
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
969
1084
|
function printNextSteps(targets) {
|
|
970
1085
|
console.log("");
|
|
971
1086
|
console.log(chalk6.bold("Next steps:"));
|
|
@@ -978,7 +1093,7 @@ function printNextSteps(targets) {
|
|
|
978
1093
|
console.log(` ${chalk6.cyan("Claude Code")} Start a new Claude Code session`);
|
|
979
1094
|
break;
|
|
980
1095
|
case "cursor":
|
|
981
|
-
console.log(` ${chalk6.cyan("Cursor")} Restart Cursor`);
|
|
1096
|
+
console.log(` ${chalk6.cyan("Cursor")} Restart Cursor to pick up the new MCP configuration and rules`);
|
|
982
1097
|
break;
|
|
983
1098
|
}
|
|
984
1099
|
}
|
|
@@ -1069,6 +1184,16 @@ var connectCommand = new Command6("connect").description("Configure Claude/Curso
|
|
|
1069
1184
|
console.log(chalk6.dim(error.message));
|
|
1070
1185
|
}
|
|
1071
1186
|
}
|
|
1187
|
+
if (targets.includes("cursor")) {
|
|
1188
|
+
const cursorRulesSpinner = ora5("Syncing horus-core rules for Cursor...").start();
|
|
1189
|
+
try {
|
|
1190
|
+
await syncSkillsForCursor(runtime);
|
|
1191
|
+
cursorRulesSpinner.succeed("horus-core rules synced to ~/.cursor/rules/");
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
cursorRulesSpinner.warn("Could not sync Cursor rules (Forge container may not be running)");
|
|
1194
|
+
console.log(chalk6.dim(error.message));
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1072
1197
|
printNextSteps(targets);
|
|
1073
1198
|
});
|
|
1074
1199
|
|
|
@@ -1077,16 +1202,16 @@ import { Command as Command7 } from "commander";
|
|
|
1077
1202
|
import chalk7 from "chalk";
|
|
1078
1203
|
import ora6 from "ora";
|
|
1079
1204
|
import { select as select2, confirm as confirm3 } from "@inquirer/prompts";
|
|
1080
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as
|
|
1081
|
-
import { join as
|
|
1205
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync, existsSync as existsSync5 } from "fs";
|
|
1206
|
+
import { join as join5 } from "path";
|
|
1082
1207
|
import { createHash } from "crypto";
|
|
1083
1208
|
import { stringify as stringifyYaml2, parse as parseYaml2 } from "yaml";
|
|
1084
|
-
var SNAPSHOTS_DIR =
|
|
1209
|
+
var SNAPSHOTS_DIR = join5(HORUS_DIR, "snapshots");
|
|
1085
1210
|
function ensureSnapshotsDir() {
|
|
1086
|
-
|
|
1211
|
+
mkdirSync4(SNAPSHOTS_DIR, { recursive: true });
|
|
1087
1212
|
}
|
|
1088
1213
|
function composeFileHash() {
|
|
1089
|
-
if (!
|
|
1214
|
+
if (!existsSync5(COMPOSE_PATH)) return "";
|
|
1090
1215
|
const content = readFileSync4(COMPOSE_PATH, "utf-8");
|
|
1091
1216
|
return createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
1092
1217
|
}
|
|
@@ -1116,14 +1241,14 @@ function saveSnapshot(images) {
|
|
|
1116
1241
|
images,
|
|
1117
1242
|
compose_hash: composeFileHash()
|
|
1118
1243
|
};
|
|
1119
|
-
const filePath =
|
|
1244
|
+
const filePath = join5(SNAPSHOTS_DIR, `${timestamp}.yaml`);
|
|
1120
1245
|
writeFileSync4(filePath, stringifyYaml2(snapshot, { lineWidth: 0 }), "utf-8");
|
|
1121
1246
|
return filePath;
|
|
1122
1247
|
}
|
|
1123
1248
|
function listSnapshots() {
|
|
1124
|
-
if (!
|
|
1249
|
+
if (!existsSync5(SNAPSHOTS_DIR)) return [];
|
|
1125
1250
|
return readdirSync(SNAPSHOTS_DIR).filter((f) => f.endsWith(".yaml")).sort().reverse().map((f) => {
|
|
1126
|
-
const file =
|
|
1251
|
+
const file = join5(SNAPSHOTS_DIR, f);
|
|
1127
1252
|
const snapshot = parseYaml2(readFileSync4(file, "utf-8"));
|
|
1128
1253
|
return { file, snapshot };
|
|
1129
1254
|
});
|
|
@@ -1245,6 +1370,10 @@ var updateCommand = new Command7("update").description("Update Horus to the late
|
|
|
1245
1370
|
console.log(chalk7.dim(" Could not reach GitHub to check latest version."));
|
|
1246
1371
|
}
|
|
1247
1372
|
console.log("");
|
|
1373
|
+
console.log(chalk7.dim(" Note: this updates the Horus container services only."));
|
|
1374
|
+
console.log(chalk7.dim(" To update the Horus CLI itself, run:"));
|
|
1375
|
+
console.log(` ${chalk7.cyan("npm install -g @arkhera30/cli@latest")}`);
|
|
1376
|
+
console.log("");
|
|
1248
1377
|
if (!opts.yes) {
|
|
1249
1378
|
const confirmed = await confirm3({
|
|
1250
1379
|
message: "Pull latest images and restart services?",
|
|
@@ -1264,16 +1393,6 @@ var updateCommand = new Command7("update").description("Update Horus to the late
|
|
|
1264
1393
|
snapshotSpinner.warn("Could not save snapshot (update will proceed)");
|
|
1265
1394
|
console.log(chalk7.dim(error.message));
|
|
1266
1395
|
}
|
|
1267
|
-
const ghcrToken = config.github_token || process.env.GITHUB_TOKEN || "";
|
|
1268
|
-
if (ghcrToken) {
|
|
1269
|
-
const loginSpinner = ora6("Authenticating with ghcr.io...").start();
|
|
1270
|
-
const ok = await registryLogin(runtime, "ghcr.io", ghcrToken);
|
|
1271
|
-
if (ok) {
|
|
1272
|
-
loginSpinner.succeed("Authenticated with ghcr.io");
|
|
1273
|
-
} else {
|
|
1274
|
-
loginSpinner.warn("GHCR login failed \u2014 private images may not pull");
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
1396
|
console.log("");
|
|
1278
1397
|
console.log(chalk7.bold("Pulling latest images..."));
|
|
1279
1398
|
try {
|
|
@@ -1338,9 +1457,9 @@ var updateCommand = new Command7("update").description("Update Horus to the late
|
|
|
1338
1457
|
// src/commands/doctor.ts
|
|
1339
1458
|
import { Command as Command8 } from "commander";
|
|
1340
1459
|
import chalk8 from "chalk";
|
|
1341
|
-
import { execSync } from "child_process";
|
|
1342
|
-
import { existsSync as
|
|
1343
|
-
import { join as
|
|
1460
|
+
import { execSync as execSync2 } from "child_process";
|
|
1461
|
+
import { existsSync as existsSync6, accessSync, statfsSync, constants } from "fs";
|
|
1462
|
+
import { join as join6 } from "path";
|
|
1344
1463
|
function symbol(status) {
|
|
1345
1464
|
switch (status) {
|
|
1346
1465
|
case "pass":
|
|
@@ -1363,11 +1482,11 @@ function colorMessage(status, msg) {
|
|
|
1363
1482
|
}
|
|
1364
1483
|
async function checkRuntime2() {
|
|
1365
1484
|
try {
|
|
1366
|
-
|
|
1485
|
+
execSync2("docker info", { stdio: "ignore" });
|
|
1367
1486
|
return { status: "pass", label: "Runtime", message: "Docker is running" };
|
|
1368
1487
|
} catch {
|
|
1369
1488
|
try {
|
|
1370
|
-
|
|
1489
|
+
execSync2("podman info", { stdio: "ignore" });
|
|
1371
1490
|
return { status: "pass", label: "Runtime", message: "Podman is running" };
|
|
1372
1491
|
} catch {
|
|
1373
1492
|
return {
|
|
@@ -1381,11 +1500,11 @@ async function checkRuntime2() {
|
|
|
1381
1500
|
}
|
|
1382
1501
|
async function checkCompose() {
|
|
1383
1502
|
try {
|
|
1384
|
-
|
|
1503
|
+
execSync2("docker compose version", { stdio: "ignore" });
|
|
1385
1504
|
return { status: "pass", label: "Compose", message: "Compose plugin available" };
|
|
1386
1505
|
} catch {
|
|
1387
1506
|
try {
|
|
1388
|
-
|
|
1507
|
+
execSync2("podman compose version", { stdio: "ignore" });
|
|
1389
1508
|
return { status: "pass", label: "Compose", message: "Compose plugin available (podman)" };
|
|
1390
1509
|
} catch {
|
|
1391
1510
|
return {
|
|
@@ -1409,7 +1528,7 @@ function checkConfig() {
|
|
|
1409
1528
|
};
|
|
1410
1529
|
}
|
|
1411
1530
|
function checkComposeFile() {
|
|
1412
|
-
if (
|
|
1531
|
+
if (existsSync6(COMPOSE_PATH)) {
|
|
1413
1532
|
return { status: "pass", label: "Compose file", message: "Compose file installed (~/.horus/docker-compose.yml)" };
|
|
1414
1533
|
}
|
|
1415
1534
|
return {
|
|
@@ -1421,7 +1540,7 @@ function checkComposeFile() {
|
|
|
1421
1540
|
}
|
|
1422
1541
|
function checkPort(port, serviceName) {
|
|
1423
1542
|
try {
|
|
1424
|
-
const output =
|
|
1543
|
+
const output = execSync2(`lsof -i :${port} -sTCP:LISTEN -t 2>/dev/null || true`, {
|
|
1425
1544
|
encoding: "utf-8"
|
|
1426
1545
|
}).trim();
|
|
1427
1546
|
if (!output) {
|
|
@@ -1430,7 +1549,7 @@ function checkPort(port, serviceName) {
|
|
|
1430
1549
|
const pids = output.split("\n").filter(Boolean);
|
|
1431
1550
|
for (const pid of pids) {
|
|
1432
1551
|
try {
|
|
1433
|
-
const cmdline =
|
|
1552
|
+
const cmdline = execSync2(`ps -p ${pid} -o comm= 2>/dev/null || true`, {
|
|
1434
1553
|
encoding: "utf-8"
|
|
1435
1554
|
}).trim();
|
|
1436
1555
|
if (cmdline.toLowerCase().includes("docker") || cmdline.toLowerCase().includes("podman")) {
|
|
@@ -1450,7 +1569,7 @@ function checkPort(port, serviceName) {
|
|
|
1450
1569
|
}
|
|
1451
1570
|
}
|
|
1452
1571
|
function checkDataDir(dataDir) {
|
|
1453
|
-
if (!
|
|
1572
|
+
if (!existsSync6(dataDir)) {
|
|
1454
1573
|
return {
|
|
1455
1574
|
status: "warn",
|
|
1456
1575
|
label: "Data directory",
|
|
@@ -1471,7 +1590,7 @@ function checkDataDir(dataDir) {
|
|
|
1471
1590
|
}
|
|
1472
1591
|
}
|
|
1473
1592
|
function checkDiskSpace(dataDir) {
|
|
1474
|
-
const checkDir =
|
|
1593
|
+
const checkDir = existsSync6(dataDir) ? dataDir : join6(dataDir, "..");
|
|
1475
1594
|
try {
|
|
1476
1595
|
const stats = statfsSync(checkDir);
|
|
1477
1596
|
const freeBytes = stats.bfree * stats.bsize;
|
|
@@ -1555,7 +1674,7 @@ var doctorCommand = new Command8("doctor").description("Diagnose common Horus is
|
|
|
1555
1674
|
allResults.push(checkComposeFile());
|
|
1556
1675
|
const config = configExists() ? loadConfig() : null;
|
|
1557
1676
|
const ports = config?.ports ?? DEFAULT_PORTS;
|
|
1558
|
-
const dataDir = config?.data_dir ??
|
|
1677
|
+
const dataDir = config?.data_dir ?? join6(process.env.HOME ?? "~", ".horus", "data");
|
|
1559
1678
|
allResults.push(checkPort(ports.anvil, "Anvil"));
|
|
1560
1679
|
allResults.push(checkPort(ports.vault_rest, "Vault"));
|
|
1561
1680
|
allResults.push(checkPort(ports.vault_mcp, "Vault MCP"));
|
|
@@ -1610,13 +1729,13 @@ import { Command as Command9 } from "commander";
|
|
|
1610
1729
|
import chalk9 from "chalk";
|
|
1611
1730
|
import ora7 from "ora";
|
|
1612
1731
|
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
1613
|
-
import { mkdirSync as
|
|
1614
|
-
import { join as
|
|
1615
|
-
import { execSync as
|
|
1732
|
+
import { mkdirSync as mkdirSync5, statSync, existsSync as existsSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1733
|
+
import { join as join7, basename } from "path";
|
|
1734
|
+
import { execSync as execSync3 } from "child_process";
|
|
1616
1735
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
1617
|
-
var BACKUPS_DIR =
|
|
1736
|
+
var BACKUPS_DIR = join7(HORUS_DIR, "backups");
|
|
1618
1737
|
function ensureBackupsDir() {
|
|
1619
|
-
|
|
1738
|
+
mkdirSync5(BACKUPS_DIR, { recursive: true });
|
|
1620
1739
|
}
|
|
1621
1740
|
function formatBytes(bytes) {
|
|
1622
1741
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -1661,11 +1780,11 @@ async function createBackup(yes) {
|
|
|
1661
1780
|
}
|
|
1662
1781
|
ensureBackupsDir();
|
|
1663
1782
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1664
|
-
const tarFile =
|
|
1665
|
-
const metaFile =
|
|
1783
|
+
const tarFile = join7(BACKUPS_DIR, `${timestamp}.tar.gz`);
|
|
1784
|
+
const metaFile = join7(BACKUPS_DIR, `${timestamp}.meta.yaml`);
|
|
1666
1785
|
const backupSpinner = ora7("Creating backup archive...").start();
|
|
1667
1786
|
try {
|
|
1668
|
-
|
|
1787
|
+
execSync3(`tar -czf "${tarFile}" -C "${HORUS_DIR}" data/`, {
|
|
1669
1788
|
stdio: "pipe"
|
|
1670
1789
|
});
|
|
1671
1790
|
backupSpinner.succeed(`Archive created: ${chalk9.dim(tarFile)}`);
|
|
@@ -1711,7 +1830,7 @@ async function restoreBackup(file, yes) {
|
|
|
1711
1830
|
console.log(chalk9.bold("Horus Restore"));
|
|
1712
1831
|
console.log(chalk9.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"));
|
|
1713
1832
|
console.log("");
|
|
1714
|
-
if (!
|
|
1833
|
+
if (!existsSync7(file)) {
|
|
1715
1834
|
console.log(chalk9.red(`Backup file not found: ${file}`));
|
|
1716
1835
|
process.exit(1);
|
|
1717
1836
|
}
|
|
@@ -1749,7 +1868,7 @@ async function restoreBackup(file, yes) {
|
|
|
1749
1868
|
}
|
|
1750
1869
|
const extractSpinner = ora7("Extracting backup...").start();
|
|
1751
1870
|
try {
|
|
1752
|
-
|
|
1871
|
+
execSync3(`tar -xzf "${file}" -C "${HORUS_DIR}/"`, { stdio: "pipe" });
|
|
1753
1872
|
extractSpinner.succeed("Backup extracted");
|
|
1754
1873
|
} catch (error) {
|
|
1755
1874
|
extractSpinner.fail("Failed to extract backup");
|