@arkhera30/cli 0.1.16 → 0.2.0
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 +2 -1
- package/dist/index.js +559 -163
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
# NOTE: This file is superseded by dynamic compose generation in cli/src/lib/compose.ts
|
|
1
2
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
# Horus — Production Docker Compose
|
|
3
|
+
# Horus — Production Docker Compose (DEPRECATED static template)
|
|
3
4
|
# Managed by @arkhera30/cli. Do not edit manually.
|
|
4
5
|
#
|
|
5
6
|
# This file uses pre-built images from ghcr.io. Once CI pipelines are set up,
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import chalk2 from "chalk";
|
|
|
10
10
|
import ora2 from "ora";
|
|
11
11
|
import { execSync } from "child_process";
|
|
12
12
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
13
|
-
import { join as
|
|
13
|
+
import { join as join3 } from "path";
|
|
14
14
|
import { input, confirm, number, select, password } from "@inquirer/prompts";
|
|
15
15
|
|
|
16
16
|
// src/lib/config.ts
|
|
@@ -38,26 +38,26 @@ function findPackageJson() {
|
|
|
38
38
|
}
|
|
39
39
|
var pkg = JSON.parse(readFileSync(findPackageJson(), "utf-8"));
|
|
40
40
|
var CLI_VERSION = pkg.version;
|
|
41
|
-
var HORUS_DIR = join(homedir(), "
|
|
41
|
+
var HORUS_DIR = join(homedir(), "Horus");
|
|
42
|
+
var LEGACY_HORUS_DIR = join(homedir(), ".horus");
|
|
42
43
|
var CONFIG_PATH = join(HORUS_DIR, "config.yaml");
|
|
43
44
|
var ENV_PATH = join(HORUS_DIR, ".env");
|
|
44
45
|
var COMPOSE_PATH = join(HORUS_DIR, "docker-compose.yml");
|
|
45
46
|
var DEFAULT_PORTS = {
|
|
46
47
|
anvil: 8100,
|
|
47
48
|
vault_rest: 8e3,
|
|
49
|
+
// keep for individual vault instances
|
|
48
50
|
vault_mcp: 8300,
|
|
51
|
+
vault_router: 8400,
|
|
52
|
+
// new
|
|
49
53
|
forge: 8200
|
|
50
54
|
};
|
|
51
|
-
var
|
|
52
|
-
anvil_notes: "",
|
|
53
|
-
vault_knowledge: "",
|
|
54
|
-
forge_registry: ""
|
|
55
|
-
};
|
|
56
|
-
var DEFAULT_DATA_DIR = join(homedir(), ".horus", "data");
|
|
55
|
+
var DEFAULT_DATA_DIR = join(homedir(), "Horus", "data");
|
|
57
56
|
var SERVICES = [
|
|
58
57
|
"qmd-daemon",
|
|
59
58
|
"anvil",
|
|
60
|
-
"vault",
|
|
59
|
+
"vault-router",
|
|
60
|
+
// replaces 'vault'
|
|
61
61
|
"vault-mcp",
|
|
62
62
|
"forge"
|
|
63
63
|
];
|
|
@@ -70,11 +70,14 @@ function defaultConfig() {
|
|
|
70
70
|
data_dir: DEFAULT_DATA_DIR,
|
|
71
71
|
runtime: "docker",
|
|
72
72
|
ports: { ...DEFAULT_PORTS },
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
repos: {
|
|
74
|
+
anvil_notes: "",
|
|
75
|
+
forge_registry: ""
|
|
76
|
+
},
|
|
77
|
+
vaults: {},
|
|
78
|
+
github_hosts: {},
|
|
75
79
|
host_repos_path: "",
|
|
76
|
-
host_repos_extra_scan_dirs: []
|
|
77
|
-
github_token: ""
|
|
80
|
+
host_repos_extra_scan_dirs: []
|
|
78
81
|
};
|
|
79
82
|
}
|
|
80
83
|
function ensureHorusDir() {
|
|
@@ -85,30 +88,52 @@ function configExists() {
|
|
|
85
88
|
}
|
|
86
89
|
function loadConfig() {
|
|
87
90
|
if (!existsSync2(CONFIG_PATH)) {
|
|
91
|
+
const legacyConfigPath = pathJoin(LEGACY_HORUS_DIR, "config.yaml");
|
|
92
|
+
if (existsSync2(legacyConfigPath)) {
|
|
93
|
+
console.warn(
|
|
94
|
+
`
|
|
95
|
+
Warning: Horus config found at ~/.horus/config.yaml (legacy location).
|
|
96
|
+
The new default is ~/Horus. Run \`horus setup\` to migrate.
|
|
97
|
+
`
|
|
98
|
+
);
|
|
99
|
+
const raw2 = readFileSync2(legacyConfigPath, "utf-8");
|
|
100
|
+
const parsed2 = parseYaml(raw2);
|
|
101
|
+
return buildConfigFromParsed(parsed2);
|
|
102
|
+
}
|
|
88
103
|
return defaultConfig();
|
|
89
104
|
}
|
|
90
105
|
const raw = readFileSync2(CONFIG_PATH, "utf-8");
|
|
91
106
|
const parsed = parseYaml(raw);
|
|
107
|
+
return buildConfigFromParsed(parsed);
|
|
108
|
+
}
|
|
109
|
+
function buildConfigFromParsed(parsed) {
|
|
110
|
+
const repos = parsed.repos;
|
|
111
|
+
if (repos && "vault_knowledge" in repos) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"config.yaml uses the old single-vault format (repos.vault_knowledge). This version requires the new multi-vault format. Please delete ~/Horus/config.yaml and run `horus setup` to reconfigure."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
92
116
|
const defaults = defaultConfig();
|
|
117
|
+
const parsedPorts = parsed.ports;
|
|
93
118
|
return {
|
|
94
119
|
version: parsed.version ?? defaults.version,
|
|
95
120
|
data_dir: parsed.data_dir ?? defaults.data_dir,
|
|
96
121
|
runtime: parsed.runtime ?? defaults.runtime,
|
|
97
122
|
ports: {
|
|
98
|
-
anvil:
|
|
99
|
-
vault_rest:
|
|
100
|
-
vault_mcp:
|
|
101
|
-
|
|
123
|
+
anvil: parsedPorts?.anvil ?? defaults.ports.anvil,
|
|
124
|
+
vault_rest: parsedPorts?.vault_rest ?? defaults.ports.vault_rest,
|
|
125
|
+
vault_mcp: parsedPorts?.vault_mcp ?? defaults.ports.vault_mcp,
|
|
126
|
+
vault_router: parsedPorts?.vault_router ?? defaults.ports.vault_router,
|
|
127
|
+
forge: parsedPorts?.forge ?? defaults.ports.forge
|
|
102
128
|
},
|
|
103
|
-
git_host: parsed.git_host ?? defaults.git_host,
|
|
104
129
|
repos: {
|
|
105
|
-
anvil_notes:
|
|
106
|
-
|
|
107
|
-
forge_registry: parsed.repos?.forge_registry ?? defaults.repos.forge_registry
|
|
130
|
+
anvil_notes: repos?.anvil_notes ?? defaults.repos.anvil_notes,
|
|
131
|
+
forge_registry: repos?.forge_registry ?? defaults.repos.forge_registry
|
|
108
132
|
},
|
|
133
|
+
vaults: parsed.vaults ?? defaults.vaults,
|
|
134
|
+
github_hosts: parsed.github_hosts ?? defaults.github_hosts,
|
|
109
135
|
host_repos_path: parsed.host_repos_path ?? defaults.host_repos_path,
|
|
110
|
-
host_repos_extra_scan_dirs: parsed.host_repos_extra_scan_dirs ?? defaults.host_repos_extra_scan_dirs
|
|
111
|
-
github_token: parsed.github_token ?? defaults.github_token
|
|
136
|
+
host_repos_extra_scan_dirs: parsed.host_repos_extra_scan_dirs ?? defaults.host_repos_extra_scan_dirs
|
|
112
137
|
};
|
|
113
138
|
}
|
|
114
139
|
function saveConfig(config) {
|
|
@@ -116,6 +141,14 @@ function saveConfig(config) {
|
|
|
116
141
|
const yaml = stringifyYaml(config, { lineWidth: 0 });
|
|
117
142
|
writeFileSync(CONFIG_PATH, yaml, "utf-8");
|
|
118
143
|
}
|
|
144
|
+
function resolveGitHubHost(repoUrl, github_hosts) {
|
|
145
|
+
try {
|
|
146
|
+
const hostname = new URL(repoUrl).hostname;
|
|
147
|
+
return Object.values(github_hosts).find((h) => h.host === hostname);
|
|
148
|
+
} catch {
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
119
152
|
function resolvePath(p) {
|
|
120
153
|
if (p.startsWith("~")) {
|
|
121
154
|
return resolve(homedir2(), p.slice(2));
|
|
@@ -185,15 +218,12 @@ function generateEnv(config) {
|
|
|
185
218
|
`ANVIL_PORT=${config.ports.anvil}`,
|
|
186
219
|
`VAULT_PORT=${config.ports.vault_rest}`,
|
|
187
220
|
`VAULT_MCP_PORT=${config.ports.vault_mcp}`,
|
|
221
|
+
`VAULT_ROUTER_PORT=${config.ports.vault_router}`,
|
|
188
222
|
`FORGE_PORT=${config.ports.forge}`,
|
|
189
223
|
"",
|
|
190
224
|
"# Repository URLs (must be HTTPS \u2014 container services do not have SSH keys)",
|
|
191
225
|
`ANVIL_REPO_URL=${config.repos.anvil_notes}`,
|
|
192
|
-
`VAULT_KNOWLEDGE_REPO_URL=${config.repos.vault_knowledge}`,
|
|
193
226
|
`FORGE_REGISTRY_REPO_URL=${config.repos.forge_registry}`,
|
|
194
|
-
"",
|
|
195
|
-
"# Authentication",
|
|
196
|
-
`GITHUB_TOKEN=${config.github_token}`,
|
|
197
227
|
""
|
|
198
228
|
];
|
|
199
229
|
return lines.join("\n");
|
|
@@ -211,11 +241,9 @@ var CONFIG_KEYS = [
|
|
|
211
241
|
"port.anvil",
|
|
212
242
|
"port.vault-rest",
|
|
213
243
|
"port.vault-mcp",
|
|
244
|
+
"port.vault-router",
|
|
214
245
|
"port.forge",
|
|
215
|
-
"github-token",
|
|
216
|
-
"git-host",
|
|
217
246
|
"repo.anvil-notes",
|
|
218
|
-
"repo.vault-knowledge",
|
|
219
247
|
"repo.forge-registry"
|
|
220
248
|
];
|
|
221
249
|
function getConfigValue(config, key) {
|
|
@@ -234,16 +262,12 @@ function getConfigValue(config, key) {
|
|
|
234
262
|
return String(config.ports.vault_rest);
|
|
235
263
|
case "port.vault-mcp":
|
|
236
264
|
return String(config.ports.vault_mcp);
|
|
265
|
+
case "port.vault-router":
|
|
266
|
+
return String(config.ports.vault_router);
|
|
237
267
|
case "port.forge":
|
|
238
268
|
return String(config.ports.forge);
|
|
239
|
-
case "github-token":
|
|
240
|
-
return config.github_token;
|
|
241
|
-
case "git-host":
|
|
242
|
-
return config.git_host;
|
|
243
269
|
case "repo.anvil-notes":
|
|
244
270
|
return config.repos.anvil_notes;
|
|
245
|
-
case "repo.vault-knowledge":
|
|
246
|
-
return config.repos.vault_knowledge;
|
|
247
271
|
case "repo.forge-registry":
|
|
248
272
|
return config.repos.forge_registry;
|
|
249
273
|
}
|
|
@@ -275,21 +299,15 @@ function setConfigValue(config, key, value) {
|
|
|
275
299
|
case "port.vault-mcp":
|
|
276
300
|
updated.ports = { ...updated.ports, vault_mcp: parseInt(value, 10) };
|
|
277
301
|
break;
|
|
302
|
+
case "port.vault-router":
|
|
303
|
+
updated.ports = { ...updated.ports, vault_router: parseInt(value, 10) };
|
|
304
|
+
break;
|
|
278
305
|
case "port.forge":
|
|
279
306
|
updated.ports = { ...updated.ports, forge: parseInt(value, 10) };
|
|
280
307
|
break;
|
|
281
|
-
case "github-token":
|
|
282
|
-
updated.github_token = value;
|
|
283
|
-
break;
|
|
284
|
-
case "git-host":
|
|
285
|
-
updated.git_host = value;
|
|
286
|
-
break;
|
|
287
308
|
case "repo.anvil-notes":
|
|
288
309
|
updated.repos = { ...updated.repos, anvil_notes: value };
|
|
289
310
|
break;
|
|
290
|
-
case "repo.vault-knowledge":
|
|
291
|
-
updated.repos = { ...updated.repos, vault_knowledge: value };
|
|
292
|
-
break;
|
|
293
311
|
case "repo.forge-registry":
|
|
294
312
|
updated.repos = { ...updated.repos, forge_registry: value };
|
|
295
313
|
break;
|
|
@@ -502,7 +520,7 @@ async function pollUntilHealthy(runtime, onUpdate, timeoutMs = 3e5, intervalMs =
|
|
|
502
520
|
const unhealthyServices = states.filter((s) => s.status === "unhealthy").map((s) => s.name).join(", ");
|
|
503
521
|
throw new Error(
|
|
504
522
|
`Services failed health check: ${unhealthyServices}
|
|
505
|
-
Run '${runtime.name} compose logs <service>' from
|
|
523
|
+
Run '${runtime.name} compose logs <service>' from ~/Horus/ to investigate.`
|
|
506
524
|
);
|
|
507
525
|
}
|
|
508
526
|
const elapsed = Date.now() - startTime;
|
|
@@ -510,7 +528,7 @@ Run '${runtime.name} compose logs <service>' from ~/.horus/ to investigate.`
|
|
|
510
528
|
const notReady = states.filter((s) => s.status !== "healthy").map((s) => `${s.name} (${s.status})`).join(", ");
|
|
511
529
|
throw new Error(
|
|
512
530
|
`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for services: ${notReady}
|
|
513
|
-
Run '${runtime.name} compose logs' from
|
|
531
|
+
Run '${runtime.name} compose logs' from ~/Horus/ to investigate.`
|
|
514
532
|
);
|
|
515
533
|
}
|
|
516
534
|
await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
|
|
@@ -519,41 +537,280 @@ Run '${runtime.name} compose logs' from ~/.horus/ to investigate.`
|
|
|
519
537
|
|
|
520
538
|
// src/lib/compose.ts
|
|
521
539
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
|
|
522
|
-
import { join as join2, dirname as dirname2 } from "path";
|
|
523
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
524
|
-
var __filename = fileURLToPath2(import.meta.url);
|
|
525
|
-
var __dirname = dirname2(__filename);
|
|
526
|
-
function getBundledComposePath() {
|
|
527
|
-
const candidates = [
|
|
528
|
-
join2(__dirname, "..", "..", "compose", "docker-compose.yml"),
|
|
529
|
-
join2(__dirname, "..", "compose", "docker-compose.yml")
|
|
530
|
-
];
|
|
531
|
-
for (const candidate of candidates) {
|
|
532
|
-
if (existsSync3(candidate)) {
|
|
533
|
-
return candidate;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
throw new Error(
|
|
537
|
-
`Bundled docker-compose.yml not found. The CLI package may be corrupted.
|
|
538
|
-
Searched: ${candidates.join(", ")}`
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
540
|
function applyPodmanUserOverride(compose) {
|
|
542
541
|
return compose.replace(
|
|
543
542
|
/^( image: .+)$/gm,
|
|
544
543
|
'$1\n user: "0:0"'
|
|
545
544
|
);
|
|
546
545
|
}
|
|
546
|
+
var QMD_DAEMON_SERVICE = ` # \u2500\u2500 QMD Daemon \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
547
|
+
# Shared QMD MCP HTTP server. Keeps GGUF models warm in memory so Anvil and
|
|
548
|
+
# Vault pay the model-load cost only once.
|
|
549
|
+
qmd-daemon:
|
|
550
|
+
image: ghcr.io/arjunkhera/horus/qmd-daemon:latest
|
|
551
|
+
environment:
|
|
552
|
+
- QMD_DAEMON_PORT=8181
|
|
553
|
+
- HORUS_RUNTIME=\${HORUS_RUNTIME:-docker}
|
|
554
|
+
volumes:
|
|
555
|
+
- qmd-daemon-data:/home/qmd/.cache/qmd
|
|
556
|
+
networks:
|
|
557
|
+
- horus-net
|
|
558
|
+
restart: unless-stopped
|
|
559
|
+
stop_grace_period: 15s
|
|
560
|
+
deploy:
|
|
561
|
+
resources:
|
|
562
|
+
limits:
|
|
563
|
+
memory: 4g
|
|
564
|
+
reservations:
|
|
565
|
+
memory: 512m
|
|
566
|
+
healthcheck:
|
|
567
|
+
test: ["CMD", "curl", "-f", "http://localhost:8181/health"]
|
|
568
|
+
interval: 30s
|
|
569
|
+
timeout: 10s
|
|
570
|
+
start_period: 600s
|
|
571
|
+
retries: 3`;
|
|
572
|
+
var ANVIL_SERVICE = ` # \u2500\u2500 Anvil \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
573
|
+
# Notes system and MCP server. Indexes markdown files from the Notes repo.
|
|
574
|
+
anvil:
|
|
575
|
+
image: ghcr.io/arjunkhera/horus/anvil:latest
|
|
576
|
+
ports:
|
|
577
|
+
- "\${ANVIL_PORT:-8100}:8100"
|
|
578
|
+
volumes:
|
|
579
|
+
- \${HORUS_DATA_PATH}/notes:/data/notes:rw
|
|
580
|
+
- qmd-daemon-data:/home/anvil/.cache/qmd
|
|
581
|
+
environment:
|
|
582
|
+
- HORUS_RUNTIME=\${HORUS_RUNTIME:-docker}
|
|
583
|
+
- ANVIL_TRANSPORT=http
|
|
584
|
+
- ANVIL_PORT=8100
|
|
585
|
+
- ANVIL_HOST=0.0.0.0
|
|
586
|
+
- ANVIL_NOTES_PATH=/data/notes
|
|
587
|
+
- ANVIL_REPO_URL=\${ANVIL_REPO_URL:-}
|
|
588
|
+
- ANVIL_QMD_COLLECTION=\${ANVIL_QMD_COLLECTION:-anvil}
|
|
589
|
+
- ANVIL_SYNC_INTERVAL=\${ANVIL_SYNC_INTERVAL:-300}
|
|
590
|
+
- ANVIL_DEBOUNCE_SECONDS=\${ANVIL_DEBOUNCE_SECONDS:-5}
|
|
591
|
+
- GITHUB_TOKEN=\${GITHUB_TOKEN:-}
|
|
592
|
+
- QMD_DAEMON_URL=http://qmd-daemon:8181
|
|
593
|
+
depends_on:
|
|
594
|
+
qmd-daemon:
|
|
595
|
+
condition: service_healthy
|
|
596
|
+
networks:
|
|
597
|
+
- horus-net
|
|
598
|
+
restart: unless-stopped
|
|
599
|
+
stop_grace_period: 15s
|
|
600
|
+
deploy:
|
|
601
|
+
resources:
|
|
602
|
+
limits:
|
|
603
|
+
memory: 512m
|
|
604
|
+
reservations:
|
|
605
|
+
memory: 256m
|
|
606
|
+
healthcheck:
|
|
607
|
+
test: ["CMD", "curl", "-f", "http://localhost:8100/health"]
|
|
608
|
+
interval: 30s
|
|
609
|
+
timeout: 5s
|
|
610
|
+
start_period: 600s
|
|
611
|
+
retries: 3`;
|
|
612
|
+
var FORGE_SERVICE = ` # \u2500\u2500 Forge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
613
|
+
# Workspace manager and package registry MCP server.
|
|
614
|
+
forge:
|
|
615
|
+
image: ghcr.io/arjunkhera/horus/forge:latest
|
|
616
|
+
ports:
|
|
617
|
+
- "\${FORGE_PORT:-8200}:8200"
|
|
618
|
+
volumes:
|
|
619
|
+
- \${HORUS_DATA_PATH}/registry:/data/registry:rw
|
|
620
|
+
- \${HORUS_DATA_PATH}/workspaces:/data/workspaces:rw
|
|
621
|
+
- \${HOST_REPOS_PATH}:/data/repos:ro
|
|
622
|
+
environment:
|
|
623
|
+
- HORUS_RUNTIME=\${HORUS_RUNTIME:-docker}
|
|
624
|
+
- FORGE_PORT=8200
|
|
625
|
+
- FORGE_HOST=0.0.0.0
|
|
626
|
+
- FORGE_REGISTRY_PATH=/data/registry
|
|
627
|
+
- FORGE_WORKSPACES_PATH=/data/workspaces
|
|
628
|
+
- FORGE_REGISTRY_REPO_URL=\${FORGE_REGISTRY_REPO_URL:-}
|
|
629
|
+
- FORGE_SYNC_INTERVAL=\${FORGE_SYNC_INTERVAL:-300}
|
|
630
|
+
- FORGE_ANVIL_URL=http://anvil:8100
|
|
631
|
+
- FORGE_VAULT_URL=http://vault-mcp:8300
|
|
632
|
+
- FORGE_HOST_WORKSPACES_PATH=\${HORUS_DATA_PATH}/workspaces
|
|
633
|
+
- FORGE_HOST_REPOS_PATH=\${HOST_REPOS_PATH}
|
|
634
|
+
- FORGE_HOST_ANVIL_URL=http://localhost:\${ANVIL_PORT:-8100}
|
|
635
|
+
- FORGE_HOST_VAULT_URL=http://localhost:\${VAULT_MCP_PORT:-8300}
|
|
636
|
+
- FORGE_HOST_FORGE_URL=http://localhost:\${FORGE_PORT:-8200}
|
|
637
|
+
- FORGE_SCAN_PATHS=\${FORGE_SCAN_PATHS:-/data/repos}
|
|
638
|
+
- GITHUB_TOKEN=\${GITHUB_TOKEN:-}
|
|
639
|
+
depends_on:
|
|
640
|
+
anvil:
|
|
641
|
+
condition: service_healthy
|
|
642
|
+
vault-router:
|
|
643
|
+
condition: service_healthy
|
|
644
|
+
networks:
|
|
645
|
+
- horus-net
|
|
646
|
+
restart: unless-stopped
|
|
647
|
+
stop_grace_period: 15s
|
|
648
|
+
deploy:
|
|
649
|
+
resources:
|
|
650
|
+
limits:
|
|
651
|
+
memory: 512m
|
|
652
|
+
reservations:
|
|
653
|
+
memory: 128m
|
|
654
|
+
healthcheck:
|
|
655
|
+
test: ["CMD", "curl", "-f", "http://localhost:8200/health"]
|
|
656
|
+
interval: 30s
|
|
657
|
+
timeout: 5s
|
|
658
|
+
start_period: 60s
|
|
659
|
+
retries: 3`;
|
|
660
|
+
function generateComposeFile(config, runtime) {
|
|
661
|
+
const vaultEntries = Object.entries(config.vaults).sort(([a], [b]) => a.localeCompare(b));
|
|
662
|
+
const vaultServices = vaultEntries.map(([name, vault], index) => {
|
|
663
|
+
const hostPort = `800${index + 1}`;
|
|
664
|
+
const envVarName = `VAULT_REST_PORT_${name.toUpperCase().replace(/-/g, "_")}`;
|
|
665
|
+
const githubHost = resolveGitHubHost(vault.repo, config.github_hosts);
|
|
666
|
+
const token = githubHost?.token ?? "";
|
|
667
|
+
const apiHost = githubHost?.host ?? "github.com";
|
|
668
|
+
return ` # \u2500\u2500 Vault: ${name} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
669
|
+
vault-${name}:
|
|
670
|
+
image: ghcr.io/arjunkhera/horus/vault:latest
|
|
671
|
+
ports:
|
|
672
|
+
- "\${${envVarName}:-${hostPort}}:8000"
|
|
673
|
+
volumes:
|
|
674
|
+
- \${HORUS_DATA_PATH}/vaults/${name}:/data/knowledge-repo:rw
|
|
675
|
+
- vault-${name}-workspace:/data/workspace
|
|
676
|
+
- qmd-daemon-data:/home/appuser/.cache/qmd
|
|
677
|
+
environment:
|
|
678
|
+
- HORUS_RUNTIME=\${HORUS_RUNTIME:-docker}
|
|
679
|
+
- KNOWLEDGE_REPO_PATH=/data/knowledge-repo
|
|
680
|
+
- WORKSPACE_PATH=/data/workspace
|
|
681
|
+
- VAULT_KNOWLEDGE_REPO_URL=${vault.repo}
|
|
682
|
+
- QMD_INDEX_NAME=vault-${name}
|
|
683
|
+
- SYNC_INTERVAL=\${VAULT_SYNC_INTERVAL:-300}
|
|
684
|
+
- VAULT_SYNC_INTERVAL=\${VAULT_SYNC_INTERVAL:-300}
|
|
685
|
+
- LOG_LEVEL=\${LOG_LEVEL:-info}
|
|
686
|
+
- HOST=0.0.0.0
|
|
687
|
+
- PORT=8000
|
|
688
|
+
- GITHUB_TOKEN=${token}
|
|
689
|
+
- GITHUB_API_HOST=${apiHost}
|
|
690
|
+
- QMD_DAEMON_URL=http://qmd-daemon:8181
|
|
691
|
+
depends_on:
|
|
692
|
+
qmd-daemon:
|
|
693
|
+
condition: service_healthy
|
|
694
|
+
networks:
|
|
695
|
+
- horus-net
|
|
696
|
+
restart: unless-stopped
|
|
697
|
+
deploy:
|
|
698
|
+
resources:
|
|
699
|
+
limits:
|
|
700
|
+
memory: 512m
|
|
701
|
+
reservations:
|
|
702
|
+
memory: 256m
|
|
703
|
+
healthcheck:
|
|
704
|
+
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
705
|
+
interval: 30s
|
|
706
|
+
timeout: 10s
|
|
707
|
+
start_period: 600s
|
|
708
|
+
retries: 3`;
|
|
709
|
+
});
|
|
710
|
+
const defaultVaultEntry = vaultEntries.find(([, v]) => v.default);
|
|
711
|
+
const defaultVaultName = defaultVaultEntry ? defaultVaultEntry[0] : vaultEntries[0]?.[0] ?? "";
|
|
712
|
+
const vaultEndpoints = vaultEntries.map(([name]) => `${name}=http://vault-${name}:8000`).join(",");
|
|
713
|
+
const vaultRouterDependsOn = vaultEntries.map(([name]) => ` vault-${name}:
|
|
714
|
+
condition: service_healthy`).join("\n");
|
|
715
|
+
const vaultRouterService = ` # \u2500\u2500 Vault Router \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
716
|
+
# Routes requests to the appropriate vault instance by name.
|
|
717
|
+
vault-router:
|
|
718
|
+
image: ghcr.io/arjunkhera/horus/vault-router:latest
|
|
719
|
+
ports:
|
|
720
|
+
- "\${VAULT_ROUTER_PORT:-8400}:8400"
|
|
721
|
+
environment:
|
|
722
|
+
- VAULT_ENDPOINTS=${vaultEndpoints}
|
|
723
|
+
- VAULT_DEFAULT=${defaultVaultName}
|
|
724
|
+
depends_on:
|
|
725
|
+
${vaultRouterDependsOn}
|
|
726
|
+
networks:
|
|
727
|
+
- horus-net
|
|
728
|
+
restart: unless-stopped
|
|
729
|
+
deploy:
|
|
730
|
+
resources:
|
|
731
|
+
limits:
|
|
732
|
+
memory: 256m
|
|
733
|
+
reservations:
|
|
734
|
+
memory: 64m
|
|
735
|
+
healthcheck:
|
|
736
|
+
test: ["CMD", "curl", "-f", "http://localhost:8400/health"]
|
|
737
|
+
interval: 30s
|
|
738
|
+
timeout: 10s
|
|
739
|
+
start_period: 30s
|
|
740
|
+
retries: 3`;
|
|
741
|
+
const vaultMcpService = ` # \u2500\u2500 Vault MCP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
742
|
+
# Thin MCP adapter that translates MCP tool calls to Vault REST API calls.
|
|
743
|
+
vault-mcp:
|
|
744
|
+
image: ghcr.io/arjunkhera/horus/vault-mcp:latest
|
|
745
|
+
ports:
|
|
746
|
+
- "\${VAULT_MCP_PORT:-8300}:8300"
|
|
747
|
+
environment:
|
|
748
|
+
- VAULT_MCP_HTTP=true
|
|
749
|
+
- VAULT_MCP_PORT=8300
|
|
750
|
+
- VAULT_MCP_HOST=0.0.0.0
|
|
751
|
+
- KNOWLEDGE_SERVICE_URL=http://vault-router:8400
|
|
752
|
+
depends_on:
|
|
753
|
+
vault-router:
|
|
754
|
+
condition: service_healthy
|
|
755
|
+
networks:
|
|
756
|
+
- horus-net
|
|
757
|
+
restart: unless-stopped
|
|
758
|
+
stop_grace_period: 15s
|
|
759
|
+
deploy:
|
|
760
|
+
resources:
|
|
761
|
+
limits:
|
|
762
|
+
memory: 256m
|
|
763
|
+
reservations:
|
|
764
|
+
memory: 64m
|
|
765
|
+
healthcheck:
|
|
766
|
+
test: ["CMD", "curl", "-f", "http://localhost:8300/health"]
|
|
767
|
+
interval: 30s
|
|
768
|
+
timeout: 5s
|
|
769
|
+
start_period: 30s
|
|
770
|
+
retries: 3`;
|
|
771
|
+
const vaultVolumeEntries = vaultEntries.map(([name]) => ` vault-${name}-workspace:`).join("\n");
|
|
772
|
+
const sections = [
|
|
773
|
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
774
|
+
"# Horus \u2014 Generated Docker Compose",
|
|
775
|
+
"# Managed by @arkhera30/cli. Do not edit manually.",
|
|
776
|
+
"# Generated dynamically from ~/Horus/config.yaml by `horus setup`.",
|
|
777
|
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
778
|
+
"",
|
|
779
|
+
"services:",
|
|
780
|
+
"",
|
|
781
|
+
QMD_DAEMON_SERVICE,
|
|
782
|
+
"",
|
|
783
|
+
ANVIL_SERVICE,
|
|
784
|
+
"",
|
|
785
|
+
...vaultServices.map((s) => s + "\n"),
|
|
786
|
+
vaultRouterService,
|
|
787
|
+
"",
|
|
788
|
+
vaultMcpService,
|
|
789
|
+
"",
|
|
790
|
+
FORGE_SERVICE,
|
|
791
|
+
"",
|
|
792
|
+
"# \u2500\u2500 Networks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
793
|
+
"networks:",
|
|
794
|
+
" horus-net:",
|
|
795
|
+
" driver: bridge",
|
|
796
|
+
"",
|
|
797
|
+
"# \u2500\u2500 Volumes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
798
|
+
"volumes:",
|
|
799
|
+
" qmd-daemon-data:",
|
|
800
|
+
vaultVolumeEntries
|
|
801
|
+
];
|
|
802
|
+
let content = sections.join("\n");
|
|
803
|
+
if (runtime === "podman") {
|
|
804
|
+
content = applyPodmanUserOverride(content);
|
|
805
|
+
}
|
|
806
|
+
return content;
|
|
807
|
+
}
|
|
547
808
|
function composeFileExists() {
|
|
548
809
|
return existsSync3(COMPOSE_PATH);
|
|
549
810
|
}
|
|
550
|
-
function installComposeFile(runtime) {
|
|
811
|
+
function installComposeFile(config, runtime) {
|
|
551
812
|
ensureHorusDir();
|
|
552
|
-
const
|
|
553
|
-
let content = readFileSync3(bundledPath, "utf-8");
|
|
554
|
-
if (runtime === "podman") {
|
|
555
|
-
content = applyPodmanUserOverride(content);
|
|
556
|
-
}
|
|
813
|
+
const content = generateComposeFile(config, runtime);
|
|
557
814
|
writeFileSync2(COMPOSE_PATH, content, "utf-8");
|
|
558
815
|
}
|
|
559
816
|
|
|
@@ -563,22 +820,22 @@ import chalk from "chalk";
|
|
|
563
820
|
import ora from "ora";
|
|
564
821
|
import { checkbox } from "@inquirer/prompts";
|
|
565
822
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
|
|
566
|
-
import { join as
|
|
823
|
+
import { join as join2 } from "path";
|
|
567
824
|
import { homedir as homedir3 } from "os";
|
|
568
825
|
import { execa as execa2 } from "execa";
|
|
569
826
|
function detectInstalledClients() {
|
|
570
827
|
const detected = [];
|
|
571
828
|
const home = homedir3();
|
|
572
|
-
const claudeDesktopDir =
|
|
829
|
+
const claudeDesktopDir = join2(home, "Library", "Application Support", "Claude");
|
|
573
830
|
if (existsSync4(claudeDesktopDir)) {
|
|
574
831
|
detected.push("claude-desktop");
|
|
575
832
|
}
|
|
576
|
-
const claudeCodeDir =
|
|
833
|
+
const claudeCodeDir = join2(home, ".claude");
|
|
577
834
|
if (existsSync4(claudeCodeDir)) {
|
|
578
835
|
detected.push("claude-code");
|
|
579
836
|
}
|
|
580
|
-
const cursorDir =
|
|
581
|
-
const cursorAppDir =
|
|
837
|
+
const cursorDir = join2(home, ".cursor");
|
|
838
|
+
const cursorAppDir = join2(home, "Library", "Application Support", "Cursor");
|
|
582
839
|
if (existsSync4(cursorDir) || existsSync4(cursorAppDir)) {
|
|
583
840
|
detected.push("cursor");
|
|
584
841
|
}
|
|
@@ -588,11 +845,11 @@ function getConfigPath(target) {
|
|
|
588
845
|
const home = homedir3();
|
|
589
846
|
switch (target) {
|
|
590
847
|
case "claude-desktop":
|
|
591
|
-
return
|
|
848
|
+
return join2(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
592
849
|
case "claude-code":
|
|
593
|
-
return
|
|
850
|
+
return join2(home, ".claude", "settings.json");
|
|
594
851
|
case "cursor":
|
|
595
|
-
return
|
|
852
|
+
return join2(home, ".cursor", "mcp.json");
|
|
596
853
|
}
|
|
597
854
|
}
|
|
598
855
|
function mergeAndWriteConfig(configPath, mcpServers) {
|
|
@@ -612,13 +869,13 @@ function mergeAndWriteConfig(configPath, mcpServers) {
|
|
|
612
869
|
writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
613
870
|
}
|
|
614
871
|
function getMcpRemoteWrapperPath() {
|
|
615
|
-
return
|
|
872
|
+
return join2(homedir3(), ".forge", "bin", "mcp-remote-wrapper");
|
|
616
873
|
}
|
|
617
874
|
function buildStdioServers(config, wrapperPath, host) {
|
|
618
875
|
return {
|
|
619
|
-
anvil: { command: wrapperPath, args: [`http://${host}:${config.ports.anvil}/mcp
|
|
620
|
-
vault: { command: wrapperPath, args: [`http://${host}:${config.ports.vault_mcp}/mcp
|
|
621
|
-
forge: { command: wrapperPath, args: [`http://${host}:${config.ports.forge}/mcp
|
|
876
|
+
anvil: { command: wrapperPath, args: [`http://${host}:${config.ports.anvil}/mcp`, "--transport", "http-only"] },
|
|
877
|
+
vault: { command: wrapperPath, args: [`http://${host}:${config.ports.vault_mcp}/mcp`, "--transport", "http-only"] },
|
|
878
|
+
forge: { command: wrapperPath, args: [`http://${host}:${config.ports.forge}/mcp`, "--transport", "http-only"] }
|
|
622
879
|
};
|
|
623
880
|
}
|
|
624
881
|
async function isClaudeCliAvailable() {
|
|
@@ -650,14 +907,14 @@ async function registerWithClaudeCode(mcpServers) {
|
|
|
650
907
|
}
|
|
651
908
|
async function syncSkills(runtime) {
|
|
652
909
|
const home = homedir3();
|
|
653
|
-
const skillsBase =
|
|
910
|
+
const skillsBase = join2(home, ".claude", "skills");
|
|
654
911
|
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
655
912
|
const forgeContainer = "horus-forge-1";
|
|
656
913
|
for (const skill of skills) {
|
|
657
|
-
const destDir =
|
|
914
|
+
const destDir = join2(skillsBase, skill);
|
|
658
915
|
mkdirSync2(destDir, { recursive: true });
|
|
659
916
|
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
660
|
-
const dest =
|
|
917
|
+
const dest = join2(destDir, "SKILL.md");
|
|
661
918
|
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
662
919
|
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
663
920
|
writeFileSync3(dest, result.stdout, "utf-8");
|
|
@@ -666,8 +923,8 @@ async function syncSkills(runtime) {
|
|
|
666
923
|
}
|
|
667
924
|
async function syncSkillsForCursor(runtime) {
|
|
668
925
|
const home = homedir3();
|
|
669
|
-
const rulesDir =
|
|
670
|
-
const skillsBase =
|
|
926
|
+
const rulesDir = join2(home, ".cursor", "rules");
|
|
927
|
+
const skillsBase = join2(home, ".cursor", "skills-cursor");
|
|
671
928
|
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
672
929
|
const forgeContainer = "horus-forge-1";
|
|
673
930
|
mkdirSync2(rulesDir, { recursive: true });
|
|
@@ -675,7 +932,7 @@ async function syncSkillsForCursor(runtime) {
|
|
|
675
932
|
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
676
933
|
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
677
934
|
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
678
|
-
const ruleDest =
|
|
935
|
+
const ruleDest = join2(rulesDir, `${skill}.mdc`);
|
|
679
936
|
const frontmatter = `---
|
|
680
937
|
description: Horus ${skill} reference
|
|
681
938
|
alwaysApply: true
|
|
@@ -683,9 +940,9 @@ alwaysApply: true
|
|
|
683
940
|
|
|
684
941
|
`;
|
|
685
942
|
writeFileSync3(ruleDest, frontmatter + result.stdout, "utf-8");
|
|
686
|
-
const skillDir =
|
|
943
|
+
const skillDir = join2(skillsBase, skill);
|
|
687
944
|
mkdirSync2(skillDir, { recursive: true });
|
|
688
|
-
writeFileSync3(
|
|
945
|
+
writeFileSync3(join2(skillDir, "SKILL.md"), result.stdout, "utf-8");
|
|
689
946
|
}
|
|
690
947
|
}
|
|
691
948
|
}
|
|
@@ -873,7 +1130,14 @@ function injectToken(url, token) {
|
|
|
873
1130
|
return url;
|
|
874
1131
|
}
|
|
875
1132
|
}
|
|
876
|
-
|
|
1133
|
+
function extractHostname(url) {
|
|
1134
|
+
try {
|
|
1135
|
+
return new URL(url).hostname;
|
|
1136
|
+
} catch {
|
|
1137
|
+
return "github.com";
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
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("--anvil-repo <url>", "Anvil notes repository URL").option("--vault-name <name>", "Vault name (can be specified multiple times)").option("--vault-repo <url>", "Vault knowledge-base repository URL (matches positionally with --vault-name)").option("--forge-repo <url>", "Forge registry repository URL").option("--github-token <token>", "GitHub personal access token for private repos (primary host)").action(async (opts) => {
|
|
877
1141
|
console.log("");
|
|
878
1142
|
console.log(chalk2.bold("Horus Setup"));
|
|
879
1143
|
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"));
|
|
@@ -935,18 +1199,47 @@ var setupCommand = new Command2("setup").description("Interactive first-run setu
|
|
|
935
1199
|
let config;
|
|
936
1200
|
if (opts.yes) {
|
|
937
1201
|
const defaults = defaultConfig();
|
|
1202
|
+
const vaultNames = opts.vaultName ? Array.isArray(opts.vaultName) ? opts.vaultName : [opts.vaultName] : ["default"];
|
|
1203
|
+
const vaultRepos = opts.vaultRepo ? Array.isArray(opts.vaultRepo) ? opts.vaultRepo : [opts.vaultRepo] : [process.env.VAULT_KNOWLEDGE_REPO_URL ?? ""];
|
|
1204
|
+
const vaults = {};
|
|
1205
|
+
vaultNames.forEach((name, i) => {
|
|
1206
|
+
vaults[name] = {
|
|
1207
|
+
repo: vaultRepos[i] ?? vaultRepos[0] ?? "",
|
|
1208
|
+
default: i === 0
|
|
1209
|
+
};
|
|
1210
|
+
});
|
|
1211
|
+
const primaryToken = opts.githubToken || process.env.GITHUB_TOKEN || "";
|
|
1212
|
+
const anvilRepo = opts.anvilRepo || process.env.ANVIL_REPO_URL || defaults.repos.anvil_notes;
|
|
1213
|
+
const allRepoUrls = [anvilRepo, ...Object.values(vaults).map((v) => v.repo)].filter(Boolean);
|
|
1214
|
+
const seenHosts = /* @__PURE__ */ new Set();
|
|
1215
|
+
const github_hosts = {};
|
|
1216
|
+
let hostIndex = 0;
|
|
1217
|
+
for (const url of allRepoUrls) {
|
|
1218
|
+
const hostname = extractHostname(url);
|
|
1219
|
+
if (!seenHosts.has(hostname)) {
|
|
1220
|
+
seenHosts.add(hostname);
|
|
1221
|
+
const hostKey = hostIndex === 0 ? "default" : hostname;
|
|
1222
|
+
github_hosts[hostKey] = {
|
|
1223
|
+
host: hostname,
|
|
1224
|
+
token: primaryToken
|
|
1225
|
+
};
|
|
1226
|
+
hostIndex++;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (Object.keys(github_hosts).length === 0) {
|
|
1230
|
+
github_hosts["default"] = { host: "github.com", token: primaryToken };
|
|
1231
|
+
}
|
|
938
1232
|
config = {
|
|
939
1233
|
...defaults,
|
|
940
1234
|
runtime: runtime.name,
|
|
941
1235
|
data_dir: opts.dataDir || DEFAULT_DATA_DIR,
|
|
942
1236
|
host_repos_path: opts.reposPath || "",
|
|
943
|
-
git_host: opts.gitHost || defaults.git_host,
|
|
944
1237
|
repos: {
|
|
945
|
-
anvil_notes:
|
|
946
|
-
vault_knowledge: opts.vaultRepo || process.env.VAULT_KNOWLEDGE_REPO_URL || defaults.repos.vault_knowledge,
|
|
1238
|
+
anvil_notes: anvilRepo,
|
|
947
1239
|
forge_registry: opts.forgeRepo || process.env.FORGE_REGISTRY_REPO_URL || defaults.repos.forge_registry
|
|
948
1240
|
},
|
|
949
|
-
|
|
1241
|
+
vaults,
|
|
1242
|
+
github_hosts
|
|
950
1243
|
};
|
|
951
1244
|
} else {
|
|
952
1245
|
const data_dir = await input({
|
|
@@ -969,13 +1262,17 @@ var setupCommand = new Command2("setup").description("Interactive first-run setu
|
|
|
969
1262
|
default: DEFAULT_PORTS.anvil
|
|
970
1263
|
});
|
|
971
1264
|
const vault_rest = await number({
|
|
972
|
-
message: "Vault REST port:",
|
|
1265
|
+
message: "Vault REST port (per-vault instances):",
|
|
973
1266
|
default: DEFAULT_PORTS.vault_rest
|
|
974
1267
|
});
|
|
975
1268
|
const vault_mcp = await number({
|
|
976
1269
|
message: "Vault MCP port:",
|
|
977
1270
|
default: DEFAULT_PORTS.vault_mcp
|
|
978
1271
|
});
|
|
1272
|
+
const vault_router = await number({
|
|
1273
|
+
message: "Vault Router port:",
|
|
1274
|
+
default: DEFAULT_PORTS.vault_router
|
|
1275
|
+
});
|
|
979
1276
|
const forge = await number({
|
|
980
1277
|
message: "Forge port:",
|
|
981
1278
|
default: DEFAULT_PORTS.forge
|
|
@@ -984,6 +1281,7 @@ var setupCommand = new Command2("setup").description("Interactive first-run setu
|
|
|
984
1281
|
anvil: anvil ?? DEFAULT_PORTS.anvil,
|
|
985
1282
|
vault_rest: vault_rest ?? DEFAULT_PORTS.vault_rest,
|
|
986
1283
|
vault_mcp: vault_mcp ?? DEFAULT_PORTS.vault_mcp,
|
|
1284
|
+
vault_router: vault_router ?? DEFAULT_PORTS.vault_router,
|
|
987
1285
|
forge: forge ?? DEFAULT_PORTS.forge
|
|
988
1286
|
};
|
|
989
1287
|
}
|
|
@@ -995,12 +1293,11 @@ var setupCommand = new Command2("setup").description("Interactive first-run setu
|
|
|
995
1293
|
console.log(chalk2.yellow(" Use HTTPS URLs \u2014 container services do not have SSH keys."));
|
|
996
1294
|
console.log(chalk2.dim(" SSH URLs (git@github.com:...) will fail at runtime inside Docker/Podman."));
|
|
997
1295
|
console.log("");
|
|
998
|
-
const
|
|
999
|
-
message: "Git server hostname:",
|
|
1296
|
+
const primaryHost = await input({
|
|
1297
|
+
message: "Primary Git server hostname:",
|
|
1000
1298
|
default: "github.com"
|
|
1001
1299
|
});
|
|
1002
|
-
const
|
|
1003
|
-
const example = (repo) => chalk2.dim(` e.g., https://${host}/<owner>/${repo}`);
|
|
1300
|
+
const example = (repo) => chalk2.dim(` e.g., https://${primaryHost}/<owner>/${repo}`);
|
|
1004
1301
|
console.log("");
|
|
1005
1302
|
const anvil_notes = await input({
|
|
1006
1303
|
message: `Anvil notes repo URL:
|
|
@@ -1008,12 +1305,6 @@ ${example("horus-notes")}
|
|
|
1008
1305
|
`,
|
|
1009
1306
|
validate: (v) => v.trim().length > 0 || "Anvil needs a notes repo to store your data."
|
|
1010
1307
|
});
|
|
1011
|
-
const vault_knowledge = await input({
|
|
1012
|
-
message: `Vault knowledge-base repo URL:
|
|
1013
|
-
${example("knowledge-base")}
|
|
1014
|
-
`,
|
|
1015
|
-
validate: (v) => v.trim().length > 0 || "Vault needs a knowledge-base repo."
|
|
1016
|
-
});
|
|
1017
1308
|
const forge_registry = await input({
|
|
1018
1309
|
message: `Forge registry repo URL:
|
|
1019
1310
|
${example("forge-registry")}
|
|
@@ -1021,13 +1312,75 @@ ${example("forge-registry")}
|
|
|
1021
1312
|
validate: (v) => v.trim().length > 0 || "Forge needs a registry repo."
|
|
1022
1313
|
});
|
|
1023
1314
|
console.log("");
|
|
1315
|
+
console.log(chalk2.bold("Vault Configuration"));
|
|
1316
|
+
console.log(chalk2.dim("Add one or more knowledge-base vaults. Each vault is a separate Git repo."));
|
|
1317
|
+
console.log("");
|
|
1318
|
+
const vaults = {};
|
|
1319
|
+
let addingVaults = true;
|
|
1320
|
+
let isFirstVault = true;
|
|
1321
|
+
while (addingVaults) {
|
|
1322
|
+
const vaultName = await input({
|
|
1323
|
+
message: "Add vault name (e.g., personal):",
|
|
1324
|
+
validate: (v) => {
|
|
1325
|
+
const trimmed = v.trim();
|
|
1326
|
+
if (!trimmed) return "Vault name cannot be empty.";
|
|
1327
|
+
if (!/^[a-z0-9-]+$/.test(trimmed)) return "Vault name must be lowercase alphanumeric with hyphens only.";
|
|
1328
|
+
if (trimmed in vaults) return `Vault "${trimmed}" already added.`;
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
const vaultRepo = await input({
|
|
1333
|
+
message: `Vault repo URL:
|
|
1334
|
+
${example(`${vaultName.trim()}-knowledge`)}
|
|
1335
|
+
`,
|
|
1336
|
+
validate: (v) => v.trim().length > 0 || "Vault repo URL cannot be empty."
|
|
1337
|
+
});
|
|
1338
|
+
let isDefault = isFirstVault;
|
|
1339
|
+
if (!isFirstVault) {
|
|
1340
|
+
isDefault = await confirm({
|
|
1341
|
+
message: `Is "${vaultName.trim()}" the default vault?`,
|
|
1342
|
+
default: false
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
if (isDefault) {
|
|
1346
|
+
for (const v of Object.values(vaults)) {
|
|
1347
|
+
v.default = false;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
vaults[vaultName.trim()] = {
|
|
1351
|
+
repo: vaultRepo.trim(),
|
|
1352
|
+
default: isDefault || isFirstVault
|
|
1353
|
+
};
|
|
1354
|
+
isFirstVault = false;
|
|
1355
|
+
addingVaults = await confirm({
|
|
1356
|
+
message: "Add another vault?",
|
|
1357
|
+
default: false
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
const defaultCount = Object.values(vaults).filter((v) => v.default).length;
|
|
1361
|
+
if (defaultCount === 0 && Object.keys(vaults).length > 0) {
|
|
1362
|
+
const firstKey = Object.keys(vaults)[0];
|
|
1363
|
+
vaults[firstKey].default = true;
|
|
1364
|
+
}
|
|
1365
|
+
console.log("");
|
|
1024
1366
|
console.log(chalk2.bold("Authentication"));
|
|
1025
|
-
console.log(chalk2.dim("A personal access token is required for private repositories."));
|
|
1367
|
+
console.log(chalk2.dim("A personal access token is required per Git server for private repositories."));
|
|
1026
1368
|
console.log("");
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1369
|
+
const allRepoUrls = [anvil_notes.trim(), ...Object.values(vaults).map((v) => v.repo)].filter(Boolean);
|
|
1370
|
+
const uniqueHostnames = [...new Set(allRepoUrls.map(extractHostname))];
|
|
1371
|
+
const github_hosts = {};
|
|
1372
|
+
for (let i = 0; i < uniqueHostnames.length; i++) {
|
|
1373
|
+
const hostname = uniqueHostnames[i];
|
|
1374
|
+
const token = await password({
|
|
1375
|
+
message: `GitHub token for ${chalk2.cyan(hostname)} (leave empty to skip):`,
|
|
1376
|
+
mask: "*"
|
|
1377
|
+
});
|
|
1378
|
+
const hostKey = i === 0 ? "default" : hostname;
|
|
1379
|
+
github_hosts[hostKey] = {
|
|
1380
|
+
host: hostname,
|
|
1381
|
+
token: token.trim()
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1031
1384
|
config = {
|
|
1032
1385
|
...defaultConfig(),
|
|
1033
1386
|
data_dir,
|
|
@@ -1035,19 +1388,18 @@ ${example("forge-registry")}
|
|
|
1035
1388
|
host_repos_extra_scan_dirs,
|
|
1036
1389
|
runtime: runtime.name,
|
|
1037
1390
|
ports,
|
|
1038
|
-
git_host: git_host.trim(),
|
|
1039
1391
|
repos: {
|
|
1040
1392
|
anvil_notes: anvil_notes.trim(),
|
|
1041
|
-
vault_knowledge: vault_knowledge.trim(),
|
|
1042
1393
|
forge_registry: forge_registry.trim()
|
|
1043
1394
|
},
|
|
1044
|
-
|
|
1395
|
+
vaults,
|
|
1396
|
+
github_hosts
|
|
1045
1397
|
};
|
|
1046
1398
|
}
|
|
1047
1399
|
const configSpinner = ora2("Saving configuration...").start();
|
|
1048
1400
|
try {
|
|
1049
1401
|
saveConfig(config);
|
|
1050
|
-
configSpinner.succeed("Configuration saved to
|
|
1402
|
+
configSpinner.succeed("Configuration saved to ~/Horus/config.yaml");
|
|
1051
1403
|
} catch (error) {
|
|
1052
1404
|
configSpinner.fail("Failed to save configuration");
|
|
1053
1405
|
console.error(error.message);
|
|
@@ -1056,7 +1408,7 @@ ${example("forge-registry")}
|
|
|
1056
1408
|
const envSpinner = ora2("Generating .env file...").start();
|
|
1057
1409
|
try {
|
|
1058
1410
|
writeEnvFile(config);
|
|
1059
|
-
envSpinner.succeed("Environment file written to
|
|
1411
|
+
envSpinner.succeed("Environment file written to ~/Horus/.env");
|
|
1060
1412
|
} catch (error) {
|
|
1061
1413
|
envSpinner.fail("Failed to generate .env");
|
|
1062
1414
|
console.error(error.message);
|
|
@@ -1064,32 +1416,54 @@ ${example("forge-registry")}
|
|
|
1064
1416
|
}
|
|
1065
1417
|
const composeSpinner = ora2("Installing docker-compose.yml...").start();
|
|
1066
1418
|
try {
|
|
1067
|
-
installComposeFile(runtime.name);
|
|
1068
|
-
composeSpinner.succeed("Compose file installed to
|
|
1419
|
+
installComposeFile(config, runtime.name);
|
|
1420
|
+
composeSpinner.succeed("Compose file installed to ~/Horus/docker-compose.yml");
|
|
1069
1421
|
} catch (error) {
|
|
1070
1422
|
composeSpinner.fail("Failed to install compose file");
|
|
1071
1423
|
console.error(error.message);
|
|
1072
1424
|
process.exit(1);
|
|
1073
1425
|
}
|
|
1074
1426
|
const dataDir = resolvePath(config.data_dir);
|
|
1427
|
+
const anvilToken = resolveGitHubHost(config.repos.anvil_notes, config.github_hosts)?.token ?? "";
|
|
1428
|
+
const forgeToken = resolveGitHubHost(config.repos.forge_registry, config.github_hosts)?.token ?? "";
|
|
1075
1429
|
const reposToClone = [
|
|
1076
|
-
{
|
|
1077
|
-
|
|
1078
|
-
|
|
1430
|
+
{
|
|
1431
|
+
url: config.repos.anvil_notes,
|
|
1432
|
+
dest: join3(dataDir, "notes"),
|
|
1433
|
+
label: "Anvil notes",
|
|
1434
|
+
token: anvilToken
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
url: config.repos.forge_registry,
|
|
1438
|
+
dest: join3(dataDir, "registry"),
|
|
1439
|
+
label: "Forge registry",
|
|
1440
|
+
token: forgeToken
|
|
1441
|
+
}
|
|
1079
1442
|
].filter((r) => r.url);
|
|
1443
|
+
for (const [name, vault] of Object.entries(config.vaults)) {
|
|
1444
|
+
if (vault.repo) {
|
|
1445
|
+
const vaultToken = resolveGitHubHost(vault.repo, config.github_hosts)?.token ?? "";
|
|
1446
|
+
reposToClone.push({
|
|
1447
|
+
url: vault.repo,
|
|
1448
|
+
dest: join3(dataDir, "vaults", name),
|
|
1449
|
+
label: `Vault: ${name}`,
|
|
1450
|
+
token: vaultToken
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1080
1454
|
if (reposToClone.length > 0) {
|
|
1081
1455
|
console.log("");
|
|
1082
1456
|
console.log(chalk2.bold("Cloning repositories..."));
|
|
1083
1457
|
mkdirSync3(dataDir, { recursive: true });
|
|
1084
1458
|
for (const repo of reposToClone) {
|
|
1085
1459
|
const spinner = ora2(`Cloning ${repo.label}...`).start();
|
|
1086
|
-
if (existsSync5(
|
|
1460
|
+
if (existsSync5(join3(repo.dest, ".git"))) {
|
|
1087
1461
|
spinner.succeed(`${repo.label} already cloned`);
|
|
1088
1462
|
continue;
|
|
1089
1463
|
}
|
|
1090
1464
|
try {
|
|
1091
1465
|
mkdirSync3(repo.dest, { recursive: true });
|
|
1092
|
-
const cloneUrl = injectToken(repo.url,
|
|
1466
|
+
const cloneUrl = injectToken(repo.url, repo.token);
|
|
1093
1467
|
execSync(`git clone "${cloneUrl}" "${repo.dest}"`, {
|
|
1094
1468
|
stdio: "pipe",
|
|
1095
1469
|
timeout: 6e4
|
|
@@ -1104,7 +1478,7 @@ ${example("forge-registry")}
|
|
|
1104
1478
|
console.log(chalk2.dim(` ${msg.split("\n")[0]}`));
|
|
1105
1479
|
}
|
|
1106
1480
|
console.log(chalk2.dim(` URL: ${repo.url}`));
|
|
1107
|
-
if (!
|
|
1481
|
+
if (!repo.token) {
|
|
1108
1482
|
console.log(chalk2.dim(" Tip: Re-run setup and provide a GitHub token if the repo is private."));
|
|
1109
1483
|
}
|
|
1110
1484
|
process.exit(1);
|
|
@@ -1151,7 +1525,7 @@ ${example("forge-registry")}
|
|
|
1151
1525
|
healthSpinner.fail("Some services did not become healthy");
|
|
1152
1526
|
console.log(chalk2.dim(error.message));
|
|
1153
1527
|
console.log("");
|
|
1154
|
-
console.log(chalk2.dim("Tip: Check logs with `docker compose logs` from
|
|
1528
|
+
console.log(chalk2.dim("Tip: Check logs with `docker compose logs` from ~/Horus/"));
|
|
1155
1529
|
process.exit(1);
|
|
1156
1530
|
}
|
|
1157
1531
|
console.log("");
|
|
@@ -1172,15 +1546,23 @@ ${example("forge-registry")}
|
|
|
1172
1546
|
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"));
|
|
1173
1547
|
console.log("");
|
|
1174
1548
|
console.log(` ${chalk2.bold("Runtime:")} ${runtime.name}`);
|
|
1175
|
-
console.log(` ${chalk2.bold("Config:")}
|
|
1549
|
+
console.log(` ${chalk2.bold("Config:")} ~/Horus/config.yaml`);
|
|
1176
1550
|
console.log(` ${chalk2.bold("Data:")} ${config.data_dir}`);
|
|
1177
1551
|
console.log("");
|
|
1178
1552
|
console.log(chalk2.bold(" Service URLs:"));
|
|
1179
|
-
console.log(` Anvil:
|
|
1180
|
-
console.log(` Vault
|
|
1181
|
-
console.log(` Vault MCP:
|
|
1182
|
-
console.log(` Forge:
|
|
1553
|
+
console.log(` Anvil: http://localhost:${config.ports.anvil}`);
|
|
1554
|
+
console.log(` Vault Router: http://localhost:${config.ports.vault_router}`);
|
|
1555
|
+
console.log(` Vault MCP: http://localhost:${config.ports.vault_mcp}`);
|
|
1556
|
+
console.log(` Forge: http://localhost:${config.ports.forge}`);
|
|
1183
1557
|
console.log("");
|
|
1558
|
+
console.log(chalk2.bold(" Vault instances:"));
|
|
1559
|
+
Object.entries(config.vaults).sort(([a], [b]) => a.localeCompare(b)).forEach(([name, vault], index) => {
|
|
1560
|
+
const port = `800${index + 1}`;
|
|
1561
|
+
const defaultLabel = vault.default ? chalk2.dim(" (default)") : "";
|
|
1562
|
+
console.log(` ${name}${defaultLabel}: http://localhost:${port}`);
|
|
1563
|
+
});
|
|
1564
|
+
console.log("");
|
|
1565
|
+
void lastStates;
|
|
1184
1566
|
});
|
|
1185
1567
|
|
|
1186
1568
|
// src/commands/up.ts
|
|
@@ -1314,7 +1696,7 @@ var statusCommand = new Command5("status").description("Show status of Horus ser
|
|
|
1314
1696
|
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"));
|
|
1315
1697
|
console.log(` ${chalk5.bold("Version:")} ${CLI_VERSION}`);
|
|
1316
1698
|
console.log(` ${chalk5.bold("Runtime:")} ${runtime.name}`);
|
|
1317
|
-
console.log(` ${chalk5.bold("Config:")}
|
|
1699
|
+
console.log(` ${chalk5.bold("Config:")} ~/Horus/config.yaml`);
|
|
1318
1700
|
console.log("");
|
|
1319
1701
|
if (containers.length === 0) {
|
|
1320
1702
|
console.log(chalk5.yellow(" No services are running."));
|
|
@@ -1385,21 +1767,38 @@ var configCommand = new Command6("config").description("View or modify Horus con
|
|
|
1385
1767
|
console.log(` ${chalk6.bold("host-repos-path:")} ${config.host_repos_path || chalk6.dim("(not set)")}`);
|
|
1386
1768
|
const extraDirs = (config.host_repos_extra_scan_dirs ?? []).join(", ");
|
|
1387
1769
|
console.log(` ${chalk6.bold("host-repos-extra-scan-dirs:")} ${extraDirs || chalk6.dim("(not set)")}`);
|
|
1388
|
-
console.log(` ${chalk6.bold("git-host:")} ${config.git_host || chalk6.dim("(not set)")}`);
|
|
1389
|
-
console.log(` ${chalk6.bold("github-token:")} ${config.github_token ? maskApiKey(config.github_token) : chalk6.dim("(not set)")}`);
|
|
1390
1770
|
console.log("");
|
|
1391
1771
|
console.log(chalk6.bold(" Ports:"));
|
|
1392
|
-
console.log(` ${chalk6.bold("anvil:")}
|
|
1393
|
-
console.log(` ${chalk6.bold("vault-rest:")}
|
|
1394
|
-
console.log(` ${chalk6.bold("vault-mcp:")}
|
|
1395
|
-
console.log(` ${chalk6.bold("
|
|
1772
|
+
console.log(` ${chalk6.bold("anvil:")} ${config.ports.anvil}`);
|
|
1773
|
+
console.log(` ${chalk6.bold("vault-rest:")} ${config.ports.vault_rest}`);
|
|
1774
|
+
console.log(` ${chalk6.bold("vault-mcp:")} ${config.ports.vault_mcp}`);
|
|
1775
|
+
console.log(` ${chalk6.bold("vault-router:")} ${config.ports.vault_router}`);
|
|
1776
|
+
console.log(` ${chalk6.bold("forge:")} ${config.ports.forge}`);
|
|
1396
1777
|
console.log("");
|
|
1397
1778
|
console.log(chalk6.bold(" Repos:"));
|
|
1398
|
-
console.log(` ${chalk6.bold("anvil-notes:")}
|
|
1399
|
-
console.log(` ${chalk6.bold("
|
|
1400
|
-
console.log(
|
|
1779
|
+
console.log(` ${chalk6.bold("anvil-notes:")} ${config.repos.anvil_notes || chalk6.dim("(not set)")}`);
|
|
1780
|
+
console.log(` ${chalk6.bold("forge-registry:")} ${config.repos.forge_registry || chalk6.dim("(not set)")}`);
|
|
1781
|
+
console.log("");
|
|
1782
|
+
console.log(chalk6.bold(" Vaults:"));
|
|
1783
|
+
if (Object.keys(config.vaults ?? {}).length === 0) {
|
|
1784
|
+
console.log(chalk6.dim(" (none configured)"));
|
|
1785
|
+
} else {
|
|
1786
|
+
for (const [name, vault] of Object.entries(config.vaults)) {
|
|
1787
|
+
const defaultLabel = vault.default ? chalk6.dim(" (default)") : "";
|
|
1788
|
+
console.log(` ${chalk6.bold(name)}${defaultLabel}: ${vault.repo || chalk6.dim("(no repo)")}`);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
console.log("");
|
|
1792
|
+
console.log(chalk6.bold(" GitHub Hosts:"));
|
|
1793
|
+
if (Object.keys(config.github_hosts ?? {}).length === 0) {
|
|
1794
|
+
console.log(chalk6.dim(" (none configured)"));
|
|
1795
|
+
} else {
|
|
1796
|
+
for (const [key, gh] of Object.entries(config.github_hosts)) {
|
|
1797
|
+
console.log(` ${chalk6.bold(key)}: ${gh.host} token: ${gh.token ? maskApiKey(gh.token) : chalk6.dim("(not set)")}`);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1401
1800
|
console.log("");
|
|
1402
|
-
console.log(chalk6.dim(` Config file:
|
|
1801
|
+
console.log(chalk6.dim(` Config file: ~/Horus/config.yaml`));
|
|
1403
1802
|
console.log(chalk6.dim(` Use 'horus config get <key>' or 'horus config set <key> <value>'`));
|
|
1404
1803
|
console.log("");
|
|
1405
1804
|
});
|
|
@@ -1416,11 +1815,7 @@ configCommand.command("get <key>").description("Get a configuration value").acti
|
|
|
1416
1815
|
}
|
|
1417
1816
|
const config = loadConfig();
|
|
1418
1817
|
const value = getConfigValue(config, key);
|
|
1419
|
-
|
|
1420
|
-
console.log(maskApiKey(value));
|
|
1421
|
-
} else {
|
|
1422
|
-
console.log(value || "");
|
|
1423
|
-
}
|
|
1818
|
+
console.log(value || "");
|
|
1424
1819
|
});
|
|
1425
1820
|
configCommand.command("set <key> <value>").description("Set a configuration value").action(async (key, value) => {
|
|
1426
1821
|
if (!configExists()) {
|
|
@@ -1451,6 +1846,7 @@ configCommand.command("set <key> <value>").description("Set a configuration valu
|
|
|
1451
1846
|
"port.anvil",
|
|
1452
1847
|
"port.vault-rest",
|
|
1453
1848
|
"port.vault-mcp",
|
|
1849
|
+
"port.vault-router",
|
|
1454
1850
|
"port.forge"
|
|
1455
1851
|
];
|
|
1456
1852
|
if (needsRestart.includes(key)) {
|
|
@@ -1478,10 +1874,10 @@ import chalk7 from "chalk";
|
|
|
1478
1874
|
import ora6 from "ora";
|
|
1479
1875
|
import { select as select2, confirm as confirm3 } from "@inquirer/prompts";
|
|
1480
1876
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync2, existsSync as existsSync6 } from "fs";
|
|
1481
|
-
import { join as
|
|
1877
|
+
import { join as join4 } from "path";
|
|
1482
1878
|
import { createHash } from "crypto";
|
|
1483
1879
|
import { stringify as stringifyYaml2, parse as parseYaml2 } from "yaml";
|
|
1484
|
-
var SNAPSHOTS_DIR =
|
|
1880
|
+
var SNAPSHOTS_DIR = join4(HORUS_DIR, "snapshots");
|
|
1485
1881
|
function ensureSnapshotsDir() {
|
|
1486
1882
|
mkdirSync4(SNAPSHOTS_DIR, { recursive: true });
|
|
1487
1883
|
}
|
|
@@ -1516,14 +1912,14 @@ function saveSnapshot(images) {
|
|
|
1516
1912
|
images,
|
|
1517
1913
|
compose_hash: composeFileHash()
|
|
1518
1914
|
};
|
|
1519
|
-
const filePath =
|
|
1915
|
+
const filePath = join4(SNAPSHOTS_DIR, `${timestamp}.yaml`);
|
|
1520
1916
|
writeFileSync4(filePath, stringifyYaml2(snapshot, { lineWidth: 0 }), "utf-8");
|
|
1521
1917
|
return filePath;
|
|
1522
1918
|
}
|
|
1523
1919
|
function listSnapshots() {
|
|
1524
1920
|
if (!existsSync6(SNAPSHOTS_DIR)) return [];
|
|
1525
1921
|
return readdirSync2(SNAPSHOTS_DIR).filter((f) => f.endsWith(".yaml")).sort().reverse().map((f) => {
|
|
1526
|
-
const file =
|
|
1922
|
+
const file = join4(SNAPSHOTS_DIR, f);
|
|
1527
1923
|
const snapshot = parseYaml2(readFileSync5(file, "utf-8"));
|
|
1528
1924
|
return { file, snapshot };
|
|
1529
1925
|
});
|
|
@@ -1734,7 +2130,7 @@ import { Command as Command8 } from "commander";
|
|
|
1734
2130
|
import chalk8 from "chalk";
|
|
1735
2131
|
import { execSync as execSync2 } from "child_process";
|
|
1736
2132
|
import { existsSync as existsSync7, accessSync, statfsSync, constants } from "fs";
|
|
1737
|
-
import { join as
|
|
2133
|
+
import { join as join5 } from "path";
|
|
1738
2134
|
function symbol(status) {
|
|
1739
2135
|
switch (status) {
|
|
1740
2136
|
case "pass":
|
|
@@ -1790,23 +2186,23 @@ async function checkCompose(preferred) {
|
|
|
1790
2186
|
}
|
|
1791
2187
|
function checkConfig() {
|
|
1792
2188
|
if (configExists()) {
|
|
1793
|
-
return { status: "pass", label: "Config", message: "Configuration file exists (
|
|
2189
|
+
return { status: "pass", label: "Config", message: "Configuration file exists (~/Horus/config.yaml)" };
|
|
1794
2190
|
}
|
|
1795
2191
|
return {
|
|
1796
2192
|
status: "fail",
|
|
1797
2193
|
label: "Config",
|
|
1798
|
-
message: "Configuration file missing (
|
|
2194
|
+
message: "Configuration file missing (~/Horus/config.yaml)",
|
|
1799
2195
|
hint: "Run `horus setup` to create the configuration"
|
|
1800
2196
|
};
|
|
1801
2197
|
}
|
|
1802
2198
|
function checkComposeFile() {
|
|
1803
2199
|
if (existsSync7(COMPOSE_PATH)) {
|
|
1804
|
-
return { status: "pass", label: "Compose file", message: "Compose file installed (
|
|
2200
|
+
return { status: "pass", label: "Compose file", message: "Compose file installed (~/Horus/docker-compose.yml)" };
|
|
1805
2201
|
}
|
|
1806
2202
|
return {
|
|
1807
2203
|
status: "fail",
|
|
1808
2204
|
label: "Compose file",
|
|
1809
|
-
message: "Compose file missing (
|
|
2205
|
+
message: "Compose file missing (~/Horus/docker-compose.yml)",
|
|
1810
2206
|
hint: "Run `horus setup` to install the compose file"
|
|
1811
2207
|
};
|
|
1812
2208
|
}
|
|
@@ -1862,7 +2258,7 @@ function checkDataDir(dataDir) {
|
|
|
1862
2258
|
}
|
|
1863
2259
|
}
|
|
1864
2260
|
function checkDiskSpace(dataDir) {
|
|
1865
|
-
const checkDir = existsSync7(dataDir) ? dataDir :
|
|
2261
|
+
const checkDir = existsSync7(dataDir) ? dataDir : join5(dataDir, "..");
|
|
1866
2262
|
try {
|
|
1867
2263
|
const stats = statfsSync(checkDir);
|
|
1868
2264
|
const freeBytes = stats.bfree * stats.bsize;
|
|
@@ -1939,7 +2335,7 @@ var doctorCommand = new Command8("doctor").description("Diagnose common Horus is
|
|
|
1939
2335
|
allResults.push(checkConfig());
|
|
1940
2336
|
allResults.push(checkComposeFile());
|
|
1941
2337
|
const ports = config?.ports ?? DEFAULT_PORTS;
|
|
1942
|
-
const dataDir = config?.data_dir ??
|
|
2338
|
+
const dataDir = config?.data_dir ?? DEFAULT_DATA_DIR;
|
|
1943
2339
|
allResults.push(checkPort(ports.anvil, "Anvil"));
|
|
1944
2340
|
allResults.push(checkPort(ports.vault_rest, "Vault"));
|
|
1945
2341
|
allResults.push(checkPort(ports.vault_mcp, "Vault MCP"));
|
|
@@ -1995,10 +2391,10 @@ import chalk9 from "chalk";
|
|
|
1995
2391
|
import ora7 from "ora";
|
|
1996
2392
|
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
1997
2393
|
import { mkdirSync as mkdirSync5, statSync as statSync2, existsSync as existsSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
1998
|
-
import { join as
|
|
2394
|
+
import { join as join6, basename } from "path";
|
|
1999
2395
|
import { execSync as execSync3 } from "child_process";
|
|
2000
2396
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
2001
|
-
var BACKUPS_DIR =
|
|
2397
|
+
var BACKUPS_DIR = join6(HORUS_DIR, "backups");
|
|
2002
2398
|
function ensureBackupsDir() {
|
|
2003
2399
|
mkdirSync5(BACKUPS_DIR, { recursive: true });
|
|
2004
2400
|
}
|
|
@@ -2045,8 +2441,8 @@ async function createBackup(yes) {
|
|
|
2045
2441
|
}
|
|
2046
2442
|
ensureBackupsDir();
|
|
2047
2443
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2048
|
-
const tarFile =
|
|
2049
|
-
const metaFile =
|
|
2444
|
+
const tarFile = join6(BACKUPS_DIR, `${timestamp}.tar.gz`);
|
|
2445
|
+
const metaFile = join6(BACKUPS_DIR, `${timestamp}.meta.yaml`);
|
|
2050
2446
|
const backupSpinner = ora7("Creating backup archive...").start();
|
|
2051
2447
|
try {
|
|
2052
2448
|
execSync3(`tar -czf "${tarFile}" -C "${HORUS_DIR}" data/`, {
|