@arkhera30/cli 0.1.5 → 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/dist/index.js +159 -84
- 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,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
|
}
|
|
@@ -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,43 @@ 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
|
+
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
|
+
});
|
|
517
568
|
config = {
|
|
518
569
|
...defaultConfig(),
|
|
519
570
|
data_dir,
|
|
520
571
|
host_repos_path,
|
|
521
572
|
runtime: runtime.name,
|
|
522
|
-
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
|
+
}
|
|
523
580
|
};
|
|
524
581
|
}
|
|
525
582
|
const configSpinner = ora("Saving configuration...").start();
|
|
@@ -549,14 +606,41 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
|
|
|
549
606
|
console.error(error.message);
|
|
550
607
|
process.exit(1);
|
|
551
608
|
}
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
+
}
|
|
560
644
|
}
|
|
561
645
|
}
|
|
562
646
|
console.log("");
|
|
@@ -812,6 +896,7 @@ var configCommand = new Command5("config").description("View or modify Horus con
|
|
|
812
896
|
console.log(` ${chalk5.bold("data-dir:")} ${config.data_dir}`);
|
|
813
897
|
console.log(` ${chalk5.bold("runtime:")} ${config.runtime}`);
|
|
814
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)")}`);
|
|
815
900
|
console.log(` ${chalk5.bold("github-token:")} ${config.github_token ? maskApiKey(config.github_token) : chalk5.dim("(not set)")}`);
|
|
816
901
|
console.log("");
|
|
817
902
|
console.log(chalk5.bold(" Ports:"));
|
|
@@ -902,23 +987,23 @@ import { Command as Command6 } from "commander";
|
|
|
902
987
|
import chalk6 from "chalk";
|
|
903
988
|
import ora5 from "ora";
|
|
904
989
|
import { checkbox } from "@inquirer/prompts";
|
|
905
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as
|
|
906
|
-
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";
|
|
907
992
|
import { homedir as homedir3 } from "os";
|
|
908
993
|
function detectInstalledClients() {
|
|
909
994
|
const detected = [];
|
|
910
995
|
const home = homedir3();
|
|
911
|
-
const claudeDesktopDir =
|
|
912
|
-
if (
|
|
996
|
+
const claudeDesktopDir = join4(home, "Library", "Application Support", "Claude");
|
|
997
|
+
if (existsSync4(claudeDesktopDir)) {
|
|
913
998
|
detected.push("claude-desktop");
|
|
914
999
|
}
|
|
915
|
-
const claudeCodeDir =
|
|
916
|
-
if (
|
|
1000
|
+
const claudeCodeDir = join4(home, ".claude");
|
|
1001
|
+
if (existsSync4(claudeCodeDir)) {
|
|
917
1002
|
detected.push("claude-code");
|
|
918
1003
|
}
|
|
919
|
-
const cursorDir =
|
|
920
|
-
const cursorAppDir =
|
|
921
|
-
if (
|
|
1004
|
+
const cursorDir = join4(home, ".cursor");
|
|
1005
|
+
const cursorAppDir = join4(home, "Library", "Application Support", "Cursor");
|
|
1006
|
+
if (existsSync4(cursorDir) || existsSync4(cursorAppDir)) {
|
|
922
1007
|
detected.push("cursor");
|
|
923
1008
|
}
|
|
924
1009
|
return detected;
|
|
@@ -927,16 +1012,16 @@ function getConfigPath(target) {
|
|
|
927
1012
|
const home = homedir3();
|
|
928
1013
|
switch (target) {
|
|
929
1014
|
case "claude-desktop":
|
|
930
|
-
return
|
|
1015
|
+
return join4(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
931
1016
|
case "claude-code":
|
|
932
|
-
return
|
|
1017
|
+
return join4(home, ".claude", "settings.json");
|
|
933
1018
|
case "cursor":
|
|
934
|
-
return
|
|
1019
|
+
return join4(home, ".cursor", "mcp.json");
|
|
935
1020
|
}
|
|
936
1021
|
}
|
|
937
1022
|
function mergeAndWriteConfig(configPath, mcpServers) {
|
|
938
1023
|
let existing = {};
|
|
939
|
-
if (
|
|
1024
|
+
if (existsSync4(configPath)) {
|
|
940
1025
|
try {
|
|
941
1026
|
const raw = readFileSync3(configPath, "utf-8");
|
|
942
1027
|
existing = JSON.parse(raw);
|
|
@@ -947,19 +1032,19 @@ function mergeAndWriteConfig(configPath, mcpServers) {
|
|
|
947
1032
|
const existingServers = existing.mcpServers ?? {};
|
|
948
1033
|
existing.mcpServers = { ...existingServers, ...mcpServers };
|
|
949
1034
|
const dir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
950
|
-
|
|
1035
|
+
mkdirSync3(dir, { recursive: true });
|
|
951
1036
|
writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
952
1037
|
}
|
|
953
1038
|
async function syncSkills(runtime) {
|
|
954
1039
|
const home = homedir3();
|
|
955
|
-
const skillsBase =
|
|
1040
|
+
const skillsBase = join4(home, ".claude", "skills");
|
|
956
1041
|
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
957
1042
|
const forgeContainer = "horus-forge-1";
|
|
958
1043
|
for (const skill of skills) {
|
|
959
|
-
const destDir =
|
|
960
|
-
|
|
1044
|
+
const destDir = join4(skillsBase, skill);
|
|
1045
|
+
mkdirSync3(destDir, { recursive: true });
|
|
961
1046
|
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
962
|
-
const dest =
|
|
1047
|
+
const dest = join4(destDir, "SKILL.md");
|
|
963
1048
|
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
964
1049
|
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
965
1050
|
writeFileSync3(dest, result.stdout, "utf-8");
|
|
@@ -1077,16 +1162,16 @@ import { Command as Command7 } from "commander";
|
|
|
1077
1162
|
import chalk7 from "chalk";
|
|
1078
1163
|
import ora6 from "ora";
|
|
1079
1164
|
import { select as select2, confirm as confirm3 } from "@inquirer/prompts";
|
|
1080
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as
|
|
1081
|
-
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";
|
|
1082
1167
|
import { createHash } from "crypto";
|
|
1083
1168
|
import { stringify as stringifyYaml2, parse as parseYaml2 } from "yaml";
|
|
1084
|
-
var SNAPSHOTS_DIR =
|
|
1169
|
+
var SNAPSHOTS_DIR = join5(HORUS_DIR, "snapshots");
|
|
1085
1170
|
function ensureSnapshotsDir() {
|
|
1086
|
-
|
|
1171
|
+
mkdirSync4(SNAPSHOTS_DIR, { recursive: true });
|
|
1087
1172
|
}
|
|
1088
1173
|
function composeFileHash() {
|
|
1089
|
-
if (!
|
|
1174
|
+
if (!existsSync5(COMPOSE_PATH)) return "";
|
|
1090
1175
|
const content = readFileSync4(COMPOSE_PATH, "utf-8");
|
|
1091
1176
|
return createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
1092
1177
|
}
|
|
@@ -1116,14 +1201,14 @@ function saveSnapshot(images) {
|
|
|
1116
1201
|
images,
|
|
1117
1202
|
compose_hash: composeFileHash()
|
|
1118
1203
|
};
|
|
1119
|
-
const filePath =
|
|
1204
|
+
const filePath = join5(SNAPSHOTS_DIR, `${timestamp}.yaml`);
|
|
1120
1205
|
writeFileSync4(filePath, stringifyYaml2(snapshot, { lineWidth: 0 }), "utf-8");
|
|
1121
1206
|
return filePath;
|
|
1122
1207
|
}
|
|
1123
1208
|
function listSnapshots() {
|
|
1124
|
-
if (!
|
|
1209
|
+
if (!existsSync5(SNAPSHOTS_DIR)) return [];
|
|
1125
1210
|
return readdirSync(SNAPSHOTS_DIR).filter((f) => f.endsWith(".yaml")).sort().reverse().map((f) => {
|
|
1126
|
-
const file =
|
|
1211
|
+
const file = join5(SNAPSHOTS_DIR, f);
|
|
1127
1212
|
const snapshot = parseYaml2(readFileSync4(file, "utf-8"));
|
|
1128
1213
|
return { file, snapshot };
|
|
1129
1214
|
});
|
|
@@ -1264,16 +1349,6 @@ var updateCommand = new Command7("update").description("Update Horus to the late
|
|
|
1264
1349
|
snapshotSpinner.warn("Could not save snapshot (update will proceed)");
|
|
1265
1350
|
console.log(chalk7.dim(error.message));
|
|
1266
1351
|
}
|
|
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
1352
|
console.log("");
|
|
1278
1353
|
console.log(chalk7.bold("Pulling latest images..."));
|
|
1279
1354
|
try {
|
|
@@ -1338,9 +1413,9 @@ var updateCommand = new Command7("update").description("Update Horus to the late
|
|
|
1338
1413
|
// src/commands/doctor.ts
|
|
1339
1414
|
import { Command as Command8 } from "commander";
|
|
1340
1415
|
import chalk8 from "chalk";
|
|
1341
|
-
import { execSync } from "child_process";
|
|
1342
|
-
import { existsSync as
|
|
1343
|
-
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";
|
|
1344
1419
|
function symbol(status) {
|
|
1345
1420
|
switch (status) {
|
|
1346
1421
|
case "pass":
|
|
@@ -1363,11 +1438,11 @@ function colorMessage(status, msg) {
|
|
|
1363
1438
|
}
|
|
1364
1439
|
async function checkRuntime2() {
|
|
1365
1440
|
try {
|
|
1366
|
-
|
|
1441
|
+
execSync2("docker info", { stdio: "ignore" });
|
|
1367
1442
|
return { status: "pass", label: "Runtime", message: "Docker is running" };
|
|
1368
1443
|
} catch {
|
|
1369
1444
|
try {
|
|
1370
|
-
|
|
1445
|
+
execSync2("podman info", { stdio: "ignore" });
|
|
1371
1446
|
return { status: "pass", label: "Runtime", message: "Podman is running" };
|
|
1372
1447
|
} catch {
|
|
1373
1448
|
return {
|
|
@@ -1381,11 +1456,11 @@ async function checkRuntime2() {
|
|
|
1381
1456
|
}
|
|
1382
1457
|
async function checkCompose() {
|
|
1383
1458
|
try {
|
|
1384
|
-
|
|
1459
|
+
execSync2("docker compose version", { stdio: "ignore" });
|
|
1385
1460
|
return { status: "pass", label: "Compose", message: "Compose plugin available" };
|
|
1386
1461
|
} catch {
|
|
1387
1462
|
try {
|
|
1388
|
-
|
|
1463
|
+
execSync2("podman compose version", { stdio: "ignore" });
|
|
1389
1464
|
return { status: "pass", label: "Compose", message: "Compose plugin available (podman)" };
|
|
1390
1465
|
} catch {
|
|
1391
1466
|
return {
|
|
@@ -1409,7 +1484,7 @@ function checkConfig() {
|
|
|
1409
1484
|
};
|
|
1410
1485
|
}
|
|
1411
1486
|
function checkComposeFile() {
|
|
1412
|
-
if (
|
|
1487
|
+
if (existsSync6(COMPOSE_PATH)) {
|
|
1413
1488
|
return { status: "pass", label: "Compose file", message: "Compose file installed (~/.horus/docker-compose.yml)" };
|
|
1414
1489
|
}
|
|
1415
1490
|
return {
|
|
@@ -1421,7 +1496,7 @@ function checkComposeFile() {
|
|
|
1421
1496
|
}
|
|
1422
1497
|
function checkPort(port, serviceName) {
|
|
1423
1498
|
try {
|
|
1424
|
-
const output =
|
|
1499
|
+
const output = execSync2(`lsof -i :${port} -sTCP:LISTEN -t 2>/dev/null || true`, {
|
|
1425
1500
|
encoding: "utf-8"
|
|
1426
1501
|
}).trim();
|
|
1427
1502
|
if (!output) {
|
|
@@ -1430,7 +1505,7 @@ function checkPort(port, serviceName) {
|
|
|
1430
1505
|
const pids = output.split("\n").filter(Boolean);
|
|
1431
1506
|
for (const pid of pids) {
|
|
1432
1507
|
try {
|
|
1433
|
-
const cmdline =
|
|
1508
|
+
const cmdline = execSync2(`ps -p ${pid} -o comm= 2>/dev/null || true`, {
|
|
1434
1509
|
encoding: "utf-8"
|
|
1435
1510
|
}).trim();
|
|
1436
1511
|
if (cmdline.toLowerCase().includes("docker") || cmdline.toLowerCase().includes("podman")) {
|
|
@@ -1450,7 +1525,7 @@ function checkPort(port, serviceName) {
|
|
|
1450
1525
|
}
|
|
1451
1526
|
}
|
|
1452
1527
|
function checkDataDir(dataDir) {
|
|
1453
|
-
if (!
|
|
1528
|
+
if (!existsSync6(dataDir)) {
|
|
1454
1529
|
return {
|
|
1455
1530
|
status: "warn",
|
|
1456
1531
|
label: "Data directory",
|
|
@@ -1471,7 +1546,7 @@ function checkDataDir(dataDir) {
|
|
|
1471
1546
|
}
|
|
1472
1547
|
}
|
|
1473
1548
|
function checkDiskSpace(dataDir) {
|
|
1474
|
-
const checkDir =
|
|
1549
|
+
const checkDir = existsSync6(dataDir) ? dataDir : join6(dataDir, "..");
|
|
1475
1550
|
try {
|
|
1476
1551
|
const stats = statfsSync(checkDir);
|
|
1477
1552
|
const freeBytes = stats.bfree * stats.bsize;
|
|
@@ -1555,7 +1630,7 @@ var doctorCommand = new Command8("doctor").description("Diagnose common Horus is
|
|
|
1555
1630
|
allResults.push(checkComposeFile());
|
|
1556
1631
|
const config = configExists() ? loadConfig() : null;
|
|
1557
1632
|
const ports = config?.ports ?? DEFAULT_PORTS;
|
|
1558
|
-
const dataDir = config?.data_dir ??
|
|
1633
|
+
const dataDir = config?.data_dir ?? join6(process.env.HOME ?? "~", ".horus", "data");
|
|
1559
1634
|
allResults.push(checkPort(ports.anvil, "Anvil"));
|
|
1560
1635
|
allResults.push(checkPort(ports.vault_rest, "Vault"));
|
|
1561
1636
|
allResults.push(checkPort(ports.vault_mcp, "Vault MCP"));
|
|
@@ -1610,13 +1685,13 @@ import { Command as Command9 } from "commander";
|
|
|
1610
1685
|
import chalk9 from "chalk";
|
|
1611
1686
|
import ora7 from "ora";
|
|
1612
1687
|
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
1613
|
-
import { mkdirSync as
|
|
1614
|
-
import { join as
|
|
1615
|
-
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";
|
|
1616
1691
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
1617
|
-
var BACKUPS_DIR =
|
|
1692
|
+
var BACKUPS_DIR = join7(HORUS_DIR, "backups");
|
|
1618
1693
|
function ensureBackupsDir() {
|
|
1619
|
-
|
|
1694
|
+
mkdirSync5(BACKUPS_DIR, { recursive: true });
|
|
1620
1695
|
}
|
|
1621
1696
|
function formatBytes(bytes) {
|
|
1622
1697
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -1661,11 +1736,11 @@ async function createBackup(yes) {
|
|
|
1661
1736
|
}
|
|
1662
1737
|
ensureBackupsDir();
|
|
1663
1738
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1664
|
-
const tarFile =
|
|
1665
|
-
const metaFile =
|
|
1739
|
+
const tarFile = join7(BACKUPS_DIR, `${timestamp}.tar.gz`);
|
|
1740
|
+
const metaFile = join7(BACKUPS_DIR, `${timestamp}.meta.yaml`);
|
|
1666
1741
|
const backupSpinner = ora7("Creating backup archive...").start();
|
|
1667
1742
|
try {
|
|
1668
|
-
|
|
1743
|
+
execSync3(`tar -czf "${tarFile}" -C "${HORUS_DIR}" data/`, {
|
|
1669
1744
|
stdio: "pipe"
|
|
1670
1745
|
});
|
|
1671
1746
|
backupSpinner.succeed(`Archive created: ${chalk9.dim(tarFile)}`);
|
|
@@ -1711,7 +1786,7 @@ async function restoreBackup(file, yes) {
|
|
|
1711
1786
|
console.log(chalk9.bold("Horus Restore"));
|
|
1712
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"));
|
|
1713
1788
|
console.log("");
|
|
1714
|
-
if (!
|
|
1789
|
+
if (!existsSync7(file)) {
|
|
1715
1790
|
console.log(chalk9.red(`Backup file not found: ${file}`));
|
|
1716
1791
|
process.exit(1);
|
|
1717
1792
|
}
|
|
@@ -1749,7 +1824,7 @@ async function restoreBackup(file, yes) {
|
|
|
1749
1824
|
}
|
|
1750
1825
|
const extractSpinner = ora7("Extracting backup...").start();
|
|
1751
1826
|
try {
|
|
1752
|
-
|
|
1827
|
+
execSync3(`tar -xzf "${file}" -C "${HORUS_DIR}/"`, { stdio: "pipe" });
|
|
1753
1828
|
extractSpinner.succeed("Backup extracted");
|
|
1754
1829
|
} catch (error) {
|
|
1755
1830
|
extractSpinner.fail("Failed to extract backup");
|