@arkhera30/cli 0.1.4 → 0.1.6
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 +5 -5
- package/dist/index.js +169 -69
- package/package.json +1 -1
|
@@ -26,7 +26,7 @@ services:
|
|
|
26
26
|
#
|
|
27
27
|
# start_period covers first-boot GGUF model download (~1-2 GB) + initial embed.
|
|
28
28
|
qmd-daemon:
|
|
29
|
-
image: ghcr.io/
|
|
29
|
+
image: ghcr.io/arjunkhera/horus/qmd-daemon:latest
|
|
30
30
|
environment:
|
|
31
31
|
- QMD_DAEMON_PORT=8181
|
|
32
32
|
volumes:
|
|
@@ -51,7 +51,7 @@ services:
|
|
|
51
51
|
# ── Anvil ──────────────────────────────────────────────────────────────────
|
|
52
52
|
# Notes system and MCP server. Indexes markdown files from the Notes repo.
|
|
53
53
|
anvil:
|
|
54
|
-
image: ghcr.io/
|
|
54
|
+
image: ghcr.io/arjunkhera/horus/anvil:latest
|
|
55
55
|
ports:
|
|
56
56
|
- "${ANVIL_PORT:-8100}:8100"
|
|
57
57
|
volumes:
|
|
@@ -94,7 +94,7 @@ services:
|
|
|
94
94
|
# ── Vault ──────────────────────────────────────────────────────────────────
|
|
95
95
|
# Knowledge service. Semantic search over the knowledge-base repo.
|
|
96
96
|
vault:
|
|
97
|
-
image: ghcr.io/
|
|
97
|
+
image: ghcr.io/arjunkhera/horus/vault:latest
|
|
98
98
|
ports:
|
|
99
99
|
- "${VAULT_PORT:-8000}:8000"
|
|
100
100
|
volumes:
|
|
@@ -140,7 +140,7 @@ services:
|
|
|
140
140
|
# ── Vault MCP ──────────────────────────────────────────────────────────────
|
|
141
141
|
# Thin MCP adapter that translates MCP tool calls to Vault REST API calls.
|
|
142
142
|
vault-mcp:
|
|
143
|
-
image: ghcr.io/
|
|
143
|
+
image: ghcr.io/arjunkhera/horus/vault-mcp:latest
|
|
144
144
|
ports:
|
|
145
145
|
- "${VAULT_MCP_PORT:-8300}:8300"
|
|
146
146
|
environment:
|
|
@@ -172,7 +172,7 @@ services:
|
|
|
172
172
|
# ── Forge ──────────────────────────────────────────────────────────────────
|
|
173
173
|
# Workspace manager and package registry MCP server.
|
|
174
174
|
forge:
|
|
175
|
-
image: ghcr.io/
|
|
175
|
+
image: ghcr.io/arjunkhera/horus/forge:latest
|
|
176
176
|
ports:
|
|
177
177
|
- "${FORGE_PORT:-8200}:8200"
|
|
178
178
|
volumes:
|
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,9 +123,6 @@ function generateEnv(config) {
|
|
|
118
123
|
`VAULT_MCP_PORT=${config.ports.vault_mcp}`,
|
|
119
124
|
`FORGE_PORT=${config.ports.forge}`,
|
|
120
125
|
"",
|
|
121
|
-
"# Auth",
|
|
122
|
-
`GITHUB_TOKEN=${config.github_token}`,
|
|
123
|
-
"",
|
|
124
126
|
"# Repository URLs",
|
|
125
127
|
`ANVIL_REPO_URL=${config.repos.anvil_notes}`,
|
|
126
128
|
`VAULT_KNOWLEDGE_REPO_URL=${config.repos.vault_knowledge}`,
|
|
@@ -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
|
}
|
|
@@ -402,7 +428,7 @@ function installComposeFile() {
|
|
|
402
428
|
}
|
|
403
429
|
|
|
404
430
|
// src/commands/setup.ts
|
|
405
|
-
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) => {
|
|
406
432
|
console.log("");
|
|
407
433
|
console.log(chalk.bold("Horus Setup"));
|
|
408
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"));
|
|
@@ -463,11 +489,18 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
463
489
|
const runtime = await detectRuntime(selectedRuntime);
|
|
464
490
|
let config;
|
|
465
491
|
if (opts.yes) {
|
|
492
|
+
const defaults = defaultConfig();
|
|
466
493
|
config = {
|
|
467
|
-
...
|
|
494
|
+
...defaults,
|
|
468
495
|
runtime: runtime.name,
|
|
469
496
|
data_dir: opts.dataDir || DEFAULT_DATA_DIR,
|
|
470
|
-
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
|
+
}
|
|
471
504
|
};
|
|
472
505
|
} else {
|
|
473
506
|
const data_dir = await input({
|
|
@@ -507,12 +540,43 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
507
540
|
forge: forge ?? DEFAULT_PORTS.forge
|
|
508
541
|
};
|
|
509
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
|
+
const git_host = await input({
|
|
549
|
+
message: "Git server hostname:",
|
|
550
|
+
default: "github.com"
|
|
551
|
+
});
|
|
552
|
+
const examplePrefix = `git@${git_host}:<owner>`;
|
|
553
|
+
console.log("");
|
|
554
|
+
console.log(chalk.dim(` Example: ${examplePrefix}/my-repo.git`));
|
|
555
|
+
console.log("");
|
|
556
|
+
const anvil_notes = await input({
|
|
557
|
+
message: "Anvil notes repo URL (required):",
|
|
558
|
+
validate: (v) => v.trim().length > 0 || "Anvil needs a notes repo to store your data."
|
|
559
|
+
});
|
|
560
|
+
const vault_knowledge = await input({
|
|
561
|
+
message: "Vault knowledge-base repo URL (required):",
|
|
562
|
+
validate: (v) => v.trim().length > 0 || "Vault needs a knowledge-base repo."
|
|
563
|
+
});
|
|
564
|
+
const forge_registry = await input({
|
|
565
|
+
message: "Forge registry repo URL (required):",
|
|
566
|
+
validate: (v) => v.trim().length > 0 || "Forge needs a registry repo."
|
|
567
|
+
});
|
|
510
568
|
config = {
|
|
511
569
|
...defaultConfig(),
|
|
512
570
|
data_dir,
|
|
513
571
|
host_repos_path,
|
|
514
572
|
runtime: runtime.name,
|
|
515
|
-
ports
|
|
573
|
+
ports,
|
|
574
|
+
git_host: git_host.trim(),
|
|
575
|
+
repos: {
|
|
576
|
+
anvil_notes: anvil_notes.trim(),
|
|
577
|
+
vault_knowledge: vault_knowledge.trim(),
|
|
578
|
+
forge_registry: forge_registry.trim()
|
|
579
|
+
}
|
|
516
580
|
};
|
|
517
581
|
}
|
|
518
582
|
const configSpinner = ora("Saving configuration...").start();
|
|
@@ -542,14 +606,50 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
542
606
|
console.error(error.message);
|
|
543
607
|
process.exit(1);
|
|
544
608
|
}
|
|
609
|
+
const dataDir = config.data_dir.startsWith("~") ? join3(process.env.HOME || "", config.data_dir.slice(1)) : config.data_dir;
|
|
610
|
+
const reposToClone = [
|
|
611
|
+
{ url: config.repos.anvil_notes, dest: join3(dataDir, "notes"), label: "Anvil notes" },
|
|
612
|
+
{ url: config.repos.vault_knowledge, dest: join3(dataDir, "knowledge-base"), label: "Vault knowledge-base" },
|
|
613
|
+
{ url: config.repos.forge_registry, dest: join3(dataDir, "registry"), label: "Forge registry" }
|
|
614
|
+
].filter((r) => r.url);
|
|
615
|
+
if (reposToClone.length > 0) {
|
|
616
|
+
console.log("");
|
|
617
|
+
console.log(chalk.bold("Cloning repositories..."));
|
|
618
|
+
mkdirSync2(dataDir, { recursive: true });
|
|
619
|
+
for (const repo of reposToClone) {
|
|
620
|
+
const spinner = ora(`Cloning ${repo.label}...`).start();
|
|
621
|
+
if (existsSync3(join3(repo.dest, ".git"))) {
|
|
622
|
+
spinner.succeed(`${repo.label} already cloned`);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
mkdirSync2(repo.dest, { recursive: true });
|
|
627
|
+
execSync(`git clone "${repo.url}" "${repo.dest}"`, {
|
|
628
|
+
stdio: "pipe",
|
|
629
|
+
timeout: 6e4
|
|
630
|
+
});
|
|
631
|
+
spinner.succeed(`${repo.label} cloned`);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
spinner.fail(`Failed to clone ${repo.label}`);
|
|
634
|
+
const msg = error.message || "";
|
|
635
|
+
if (msg.includes("already exists and is not an empty directory")) {
|
|
636
|
+
console.log(chalk.dim(" Directory exists but has no .git \u2014 check the path."));
|
|
637
|
+
} else {
|
|
638
|
+
console.log(chalk.dim(` ${msg.split("\n")[0]}`));
|
|
639
|
+
}
|
|
640
|
+
console.log(chalk.dim(` URL: ${repo.url}`));
|
|
641
|
+
console.log(chalk.dim(" Ensure you have git access (SSH key or credential helper)."));
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
545
646
|
console.log("");
|
|
546
647
|
console.log(chalk.bold("Pulling container images..."));
|
|
547
648
|
try {
|
|
548
|
-
await composeStreaming(runtime, ["pull"]);
|
|
549
|
-
} catch
|
|
550
|
-
console.log(chalk.
|
|
551
|
-
console.log(chalk.dim(
|
|
552
|
-
process.exit(1);
|
|
649
|
+
await composeStreaming(runtime, ["pull", "--ignore-pull-failures"]);
|
|
650
|
+
} catch {
|
|
651
|
+
console.log(chalk.yellow("Some images could not be pulled."));
|
|
652
|
+
console.log(chalk.dim("Continuing \u2014 services will be built from source if build contexts are available."));
|
|
553
653
|
}
|
|
554
654
|
console.log("");
|
|
555
655
|
console.log(chalk.bold("Starting Horus services..."));
|
|
@@ -796,6 +896,7 @@ var configCommand = new Command5("config").description("View or modify Horus con
|
|
|
796
896
|
console.log(` ${chalk5.bold("data-dir:")} ${config.data_dir}`);
|
|
797
897
|
console.log(` ${chalk5.bold("runtime:")} ${config.runtime}`);
|
|
798
898
|
console.log(` ${chalk5.bold("host-repos-path:")} ${config.host_repos_path || chalk5.dim("(not set)")}`);
|
|
899
|
+
console.log(` ${chalk5.bold("git-host:")} ${config.git_host || chalk5.dim("(not set)")}`);
|
|
799
900
|
console.log(` ${chalk5.bold("github-token:")} ${config.github_token ? maskApiKey(config.github_token) : chalk5.dim("(not set)")}`);
|
|
800
901
|
console.log("");
|
|
801
902
|
console.log(chalk5.bold(" Ports:"));
|
|
@@ -886,23 +987,23 @@ import { Command as Command6 } from "commander";
|
|
|
886
987
|
import chalk6 from "chalk";
|
|
887
988
|
import ora5 from "ora";
|
|
888
989
|
import { checkbox } from "@inquirer/prompts";
|
|
889
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as
|
|
890
|
-
import { join as
|
|
990
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
991
|
+
import { join as join4 } from "path";
|
|
891
992
|
import { homedir as homedir3 } from "os";
|
|
892
993
|
function detectInstalledClients() {
|
|
893
994
|
const detected = [];
|
|
894
995
|
const home = homedir3();
|
|
895
|
-
const claudeDesktopDir =
|
|
896
|
-
if (
|
|
996
|
+
const claudeDesktopDir = join4(home, "Library", "Application Support", "Claude");
|
|
997
|
+
if (existsSync4(claudeDesktopDir)) {
|
|
897
998
|
detected.push("claude-desktop");
|
|
898
999
|
}
|
|
899
|
-
const claudeCodeDir =
|
|
900
|
-
if (
|
|
1000
|
+
const claudeCodeDir = join4(home, ".claude");
|
|
1001
|
+
if (existsSync4(claudeCodeDir)) {
|
|
901
1002
|
detected.push("claude-code");
|
|
902
1003
|
}
|
|
903
|
-
const cursorDir =
|
|
904
|
-
const cursorAppDir =
|
|
905
|
-
if (
|
|
1004
|
+
const cursorDir = join4(home, ".cursor");
|
|
1005
|
+
const cursorAppDir = join4(home, "Library", "Application Support", "Cursor");
|
|
1006
|
+
if (existsSync4(cursorDir) || existsSync4(cursorAppDir)) {
|
|
906
1007
|
detected.push("cursor");
|
|
907
1008
|
}
|
|
908
1009
|
return detected;
|
|
@@ -911,16 +1012,16 @@ function getConfigPath(target) {
|
|
|
911
1012
|
const home = homedir3();
|
|
912
1013
|
switch (target) {
|
|
913
1014
|
case "claude-desktop":
|
|
914
|
-
return
|
|
1015
|
+
return join4(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
915
1016
|
case "claude-code":
|
|
916
|
-
return
|
|
1017
|
+
return join4(home, ".claude", "settings.json");
|
|
917
1018
|
case "cursor":
|
|
918
|
-
return
|
|
1019
|
+
return join4(home, ".cursor", "mcp.json");
|
|
919
1020
|
}
|
|
920
1021
|
}
|
|
921
1022
|
function mergeAndWriteConfig(configPath, mcpServers) {
|
|
922
1023
|
let existing = {};
|
|
923
|
-
if (
|
|
1024
|
+
if (existsSync4(configPath)) {
|
|
924
1025
|
try {
|
|
925
1026
|
const raw = readFileSync3(configPath, "utf-8");
|
|
926
1027
|
existing = JSON.parse(raw);
|
|
@@ -931,19 +1032,19 @@ function mergeAndWriteConfig(configPath, mcpServers) {
|
|
|
931
1032
|
const existingServers = existing.mcpServers ?? {};
|
|
932
1033
|
existing.mcpServers = { ...existingServers, ...mcpServers };
|
|
933
1034
|
const dir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
934
|
-
|
|
1035
|
+
mkdirSync3(dir, { recursive: true });
|
|
935
1036
|
writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
936
1037
|
}
|
|
937
1038
|
async function syncSkills(runtime) {
|
|
938
1039
|
const home = homedir3();
|
|
939
|
-
const skillsBase =
|
|
1040
|
+
const skillsBase = join4(home, ".claude", "skills");
|
|
940
1041
|
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
941
1042
|
const forgeContainer = "horus-forge-1";
|
|
942
1043
|
for (const skill of skills) {
|
|
943
|
-
const destDir =
|
|
944
|
-
|
|
1044
|
+
const destDir = join4(skillsBase, skill);
|
|
1045
|
+
mkdirSync3(destDir, { recursive: true });
|
|
945
1046
|
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
946
|
-
const dest =
|
|
1047
|
+
const dest = join4(destDir, "SKILL.md");
|
|
947
1048
|
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
948
1049
|
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
949
1050
|
writeFileSync3(dest, result.stdout, "utf-8");
|
|
@@ -1061,16 +1162,16 @@ import { Command as Command7 } from "commander";
|
|
|
1061
1162
|
import chalk7 from "chalk";
|
|
1062
1163
|
import ora6 from "ora";
|
|
1063
1164
|
import { select as select2, confirm as confirm3 } from "@inquirer/prompts";
|
|
1064
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as
|
|
1065
|
-
import { join as
|
|
1165
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync, existsSync as existsSync5 } from "fs";
|
|
1166
|
+
import { join as join5 } from "path";
|
|
1066
1167
|
import { createHash } from "crypto";
|
|
1067
1168
|
import { stringify as stringifyYaml2, parse as parseYaml2 } from "yaml";
|
|
1068
|
-
var SNAPSHOTS_DIR =
|
|
1169
|
+
var SNAPSHOTS_DIR = join5(HORUS_DIR, "snapshots");
|
|
1069
1170
|
function ensureSnapshotsDir() {
|
|
1070
|
-
|
|
1171
|
+
mkdirSync4(SNAPSHOTS_DIR, { recursive: true });
|
|
1071
1172
|
}
|
|
1072
1173
|
function composeFileHash() {
|
|
1073
|
-
if (!
|
|
1174
|
+
if (!existsSync5(COMPOSE_PATH)) return "";
|
|
1074
1175
|
const content = readFileSync4(COMPOSE_PATH, "utf-8");
|
|
1075
1176
|
return createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
1076
1177
|
}
|
|
@@ -1100,14 +1201,14 @@ function saveSnapshot(images) {
|
|
|
1100
1201
|
images,
|
|
1101
1202
|
compose_hash: composeFileHash()
|
|
1102
1203
|
};
|
|
1103
|
-
const filePath =
|
|
1204
|
+
const filePath = join5(SNAPSHOTS_DIR, `${timestamp}.yaml`);
|
|
1104
1205
|
writeFileSync4(filePath, stringifyYaml2(snapshot, { lineWidth: 0 }), "utf-8");
|
|
1105
1206
|
return filePath;
|
|
1106
1207
|
}
|
|
1107
1208
|
function listSnapshots() {
|
|
1108
|
-
if (!
|
|
1209
|
+
if (!existsSync5(SNAPSHOTS_DIR)) return [];
|
|
1109
1210
|
return readdirSync(SNAPSHOTS_DIR).filter((f) => f.endsWith(".yaml")).sort().reverse().map((f) => {
|
|
1110
|
-
const file =
|
|
1211
|
+
const file = join5(SNAPSHOTS_DIR, f);
|
|
1111
1212
|
const snapshot = parseYaml2(readFileSync4(file, "utf-8"));
|
|
1112
1213
|
return { file, snapshot };
|
|
1113
1214
|
});
|
|
@@ -1251,11 +1352,10 @@ var updateCommand = new Command7("update").description("Update Horus to the late
|
|
|
1251
1352
|
console.log("");
|
|
1252
1353
|
console.log(chalk7.bold("Pulling latest images..."));
|
|
1253
1354
|
try {
|
|
1254
|
-
await composeStreaming(runtime, ["pull"]);
|
|
1255
|
-
} catch
|
|
1256
|
-
console.log(chalk7.
|
|
1257
|
-
console.log(chalk7.dim(
|
|
1258
|
-
process.exit(1);
|
|
1355
|
+
await composeStreaming(runtime, ["pull", "--ignore-pull-failures"]);
|
|
1356
|
+
} catch {
|
|
1357
|
+
console.log(chalk7.yellow("Some images could not be pulled."));
|
|
1358
|
+
console.log(chalk7.dim("Continuing \u2014 services will be built from source if build contexts are available."));
|
|
1259
1359
|
}
|
|
1260
1360
|
console.log("");
|
|
1261
1361
|
console.log(chalk7.bold("Restarting services..."));
|
|
@@ -1313,9 +1413,9 @@ var updateCommand = new Command7("update").description("Update Horus to the late
|
|
|
1313
1413
|
// src/commands/doctor.ts
|
|
1314
1414
|
import { Command as Command8 } from "commander";
|
|
1315
1415
|
import chalk8 from "chalk";
|
|
1316
|
-
import { execSync } from "child_process";
|
|
1317
|
-
import { existsSync as
|
|
1318
|
-
import { join as
|
|
1416
|
+
import { execSync as execSync2 } from "child_process";
|
|
1417
|
+
import { existsSync as existsSync6, accessSync, statfsSync, constants } from "fs";
|
|
1418
|
+
import { join as join6 } from "path";
|
|
1319
1419
|
function symbol(status) {
|
|
1320
1420
|
switch (status) {
|
|
1321
1421
|
case "pass":
|
|
@@ -1338,11 +1438,11 @@ function colorMessage(status, msg) {
|
|
|
1338
1438
|
}
|
|
1339
1439
|
async function checkRuntime2() {
|
|
1340
1440
|
try {
|
|
1341
|
-
|
|
1441
|
+
execSync2("docker info", { stdio: "ignore" });
|
|
1342
1442
|
return { status: "pass", label: "Runtime", message: "Docker is running" };
|
|
1343
1443
|
} catch {
|
|
1344
1444
|
try {
|
|
1345
|
-
|
|
1445
|
+
execSync2("podman info", { stdio: "ignore" });
|
|
1346
1446
|
return { status: "pass", label: "Runtime", message: "Podman is running" };
|
|
1347
1447
|
} catch {
|
|
1348
1448
|
return {
|
|
@@ -1356,11 +1456,11 @@ async function checkRuntime2() {
|
|
|
1356
1456
|
}
|
|
1357
1457
|
async function checkCompose() {
|
|
1358
1458
|
try {
|
|
1359
|
-
|
|
1459
|
+
execSync2("docker compose version", { stdio: "ignore" });
|
|
1360
1460
|
return { status: "pass", label: "Compose", message: "Compose plugin available" };
|
|
1361
1461
|
} catch {
|
|
1362
1462
|
try {
|
|
1363
|
-
|
|
1463
|
+
execSync2("podman compose version", { stdio: "ignore" });
|
|
1364
1464
|
return { status: "pass", label: "Compose", message: "Compose plugin available (podman)" };
|
|
1365
1465
|
} catch {
|
|
1366
1466
|
return {
|
|
@@ -1384,7 +1484,7 @@ function checkConfig() {
|
|
|
1384
1484
|
};
|
|
1385
1485
|
}
|
|
1386
1486
|
function checkComposeFile() {
|
|
1387
|
-
if (
|
|
1487
|
+
if (existsSync6(COMPOSE_PATH)) {
|
|
1388
1488
|
return { status: "pass", label: "Compose file", message: "Compose file installed (~/.horus/docker-compose.yml)" };
|
|
1389
1489
|
}
|
|
1390
1490
|
return {
|
|
@@ -1396,7 +1496,7 @@ function checkComposeFile() {
|
|
|
1396
1496
|
}
|
|
1397
1497
|
function checkPort(port, serviceName) {
|
|
1398
1498
|
try {
|
|
1399
|
-
const output =
|
|
1499
|
+
const output = execSync2(`lsof -i :${port} -sTCP:LISTEN -t 2>/dev/null || true`, {
|
|
1400
1500
|
encoding: "utf-8"
|
|
1401
1501
|
}).trim();
|
|
1402
1502
|
if (!output) {
|
|
@@ -1405,7 +1505,7 @@ function checkPort(port, serviceName) {
|
|
|
1405
1505
|
const pids = output.split("\n").filter(Boolean);
|
|
1406
1506
|
for (const pid of pids) {
|
|
1407
1507
|
try {
|
|
1408
|
-
const cmdline =
|
|
1508
|
+
const cmdline = execSync2(`ps -p ${pid} -o comm= 2>/dev/null || true`, {
|
|
1409
1509
|
encoding: "utf-8"
|
|
1410
1510
|
}).trim();
|
|
1411
1511
|
if (cmdline.toLowerCase().includes("docker") || cmdline.toLowerCase().includes("podman")) {
|
|
@@ -1425,7 +1525,7 @@ function checkPort(port, serviceName) {
|
|
|
1425
1525
|
}
|
|
1426
1526
|
}
|
|
1427
1527
|
function checkDataDir(dataDir) {
|
|
1428
|
-
if (!
|
|
1528
|
+
if (!existsSync6(dataDir)) {
|
|
1429
1529
|
return {
|
|
1430
1530
|
status: "warn",
|
|
1431
1531
|
label: "Data directory",
|
|
@@ -1446,7 +1546,7 @@ function checkDataDir(dataDir) {
|
|
|
1446
1546
|
}
|
|
1447
1547
|
}
|
|
1448
1548
|
function checkDiskSpace(dataDir) {
|
|
1449
|
-
const checkDir =
|
|
1549
|
+
const checkDir = existsSync6(dataDir) ? dataDir : join6(dataDir, "..");
|
|
1450
1550
|
try {
|
|
1451
1551
|
const stats = statfsSync(checkDir);
|
|
1452
1552
|
const freeBytes = stats.bfree * stats.bsize;
|
|
@@ -1530,7 +1630,7 @@ var doctorCommand = new Command8("doctor").description("Diagnose common Horus is
|
|
|
1530
1630
|
allResults.push(checkComposeFile());
|
|
1531
1631
|
const config = configExists() ? loadConfig() : null;
|
|
1532
1632
|
const ports = config?.ports ?? DEFAULT_PORTS;
|
|
1533
|
-
const dataDir = config?.data_dir ??
|
|
1633
|
+
const dataDir = config?.data_dir ?? join6(process.env.HOME ?? "~", ".horus", "data");
|
|
1534
1634
|
allResults.push(checkPort(ports.anvil, "Anvil"));
|
|
1535
1635
|
allResults.push(checkPort(ports.vault_rest, "Vault"));
|
|
1536
1636
|
allResults.push(checkPort(ports.vault_mcp, "Vault MCP"));
|
|
@@ -1585,13 +1685,13 @@ import { Command as Command9 } from "commander";
|
|
|
1585
1685
|
import chalk9 from "chalk";
|
|
1586
1686
|
import ora7 from "ora";
|
|
1587
1687
|
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
1588
|
-
import { mkdirSync as
|
|
1589
|
-
import { join as
|
|
1590
|
-
import { execSync as
|
|
1688
|
+
import { mkdirSync as mkdirSync5, statSync, existsSync as existsSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1689
|
+
import { join as join7, basename } from "path";
|
|
1690
|
+
import { execSync as execSync3 } from "child_process";
|
|
1591
1691
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
1592
|
-
var BACKUPS_DIR =
|
|
1692
|
+
var BACKUPS_DIR = join7(HORUS_DIR, "backups");
|
|
1593
1693
|
function ensureBackupsDir() {
|
|
1594
|
-
|
|
1694
|
+
mkdirSync5(BACKUPS_DIR, { recursive: true });
|
|
1595
1695
|
}
|
|
1596
1696
|
function formatBytes(bytes) {
|
|
1597
1697
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -1636,11 +1736,11 @@ async function createBackup(yes) {
|
|
|
1636
1736
|
}
|
|
1637
1737
|
ensureBackupsDir();
|
|
1638
1738
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1639
|
-
const tarFile =
|
|
1640
|
-
const metaFile =
|
|
1739
|
+
const tarFile = join7(BACKUPS_DIR, `${timestamp}.tar.gz`);
|
|
1740
|
+
const metaFile = join7(BACKUPS_DIR, `${timestamp}.meta.yaml`);
|
|
1641
1741
|
const backupSpinner = ora7("Creating backup archive...").start();
|
|
1642
1742
|
try {
|
|
1643
|
-
|
|
1743
|
+
execSync3(`tar -czf "${tarFile}" -C "${HORUS_DIR}" data/`, {
|
|
1644
1744
|
stdio: "pipe"
|
|
1645
1745
|
});
|
|
1646
1746
|
backupSpinner.succeed(`Archive created: ${chalk9.dim(tarFile)}`);
|
|
@@ -1686,7 +1786,7 @@ async function restoreBackup(file, yes) {
|
|
|
1686
1786
|
console.log(chalk9.bold("Horus Restore"));
|
|
1687
1787
|
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"));
|
|
1688
1788
|
console.log("");
|
|
1689
|
-
if (!
|
|
1789
|
+
if (!existsSync7(file)) {
|
|
1690
1790
|
console.log(chalk9.red(`Backup file not found: ${file}`));
|
|
1691
1791
|
process.exit(1);
|
|
1692
1792
|
}
|
|
@@ -1724,7 +1824,7 @@ async function restoreBackup(file, yes) {
|
|
|
1724
1824
|
}
|
|
1725
1825
|
const extractSpinner = ora7("Extracting backup...").start();
|
|
1726
1826
|
try {
|
|
1727
|
-
|
|
1827
|
+
execSync3(`tar -xzf "${file}" -C "${HORUS_DIR}/"`, { stdio: "pipe" });
|
|
1728
1828
|
extractSpinner.succeed("Backup extracted");
|
|
1729
1829
|
} catch (error) {
|
|
1730
1830
|
extractSpinner.fail("Failed to extract backup");
|