@arkhera30/cli 0.1.17 → 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 +528 -165
- 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
|
|
@@ -46,19 +46,18 @@ var COMPOSE_PATH = join(HORUS_DIR, "docker-compose.yml");
|
|
|
46
46
|
var DEFAULT_PORTS = {
|
|
47
47
|
anvil: 8100,
|
|
48
48
|
vault_rest: 8e3,
|
|
49
|
+
// keep for individual vault instances
|
|
49
50
|
vault_mcp: 8300,
|
|
51
|
+
vault_router: 8400,
|
|
52
|
+
// new
|
|
50
53
|
forge: 8200
|
|
51
54
|
};
|
|
52
|
-
var DEFAULT_REPOS = {
|
|
53
|
-
anvil_notes: "",
|
|
54
|
-
vault_knowledge: "",
|
|
55
|
-
forge_registry: ""
|
|
56
|
-
};
|
|
57
55
|
var DEFAULT_DATA_DIR = join(homedir(), "Horus", "data");
|
|
58
56
|
var SERVICES = [
|
|
59
57
|
"qmd-daemon",
|
|
60
58
|
"anvil",
|
|
61
|
-
"vault",
|
|
59
|
+
"vault-router",
|
|
60
|
+
// replaces 'vault'
|
|
62
61
|
"vault-mcp",
|
|
63
62
|
"forge"
|
|
64
63
|
];
|
|
@@ -71,11 +70,14 @@ function defaultConfig() {
|
|
|
71
70
|
data_dir: DEFAULT_DATA_DIR,
|
|
72
71
|
runtime: "docker",
|
|
73
72
|
ports: { ...DEFAULT_PORTS },
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
repos: {
|
|
74
|
+
anvil_notes: "",
|
|
75
|
+
forge_registry: ""
|
|
76
|
+
},
|
|
77
|
+
vaults: {},
|
|
78
|
+
github_hosts: {},
|
|
76
79
|
host_repos_path: "",
|
|
77
|
-
host_repos_extra_scan_dirs: []
|
|
78
|
-
github_token: ""
|
|
80
|
+
host_repos_extra_scan_dirs: []
|
|
79
81
|
};
|
|
80
82
|
}
|
|
81
83
|
function ensureHorusDir() {
|
|
@@ -96,52 +98,42 @@ The new default is ~/Horus. Run \`horus setup\` to migrate.
|
|
|
96
98
|
);
|
|
97
99
|
const raw2 = readFileSync2(legacyConfigPath, "utf-8");
|
|
98
100
|
const parsed2 = parseYaml(raw2);
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
version: parsed2.version ?? defaults2.version,
|
|
102
|
-
data_dir: parsed2.data_dir ?? defaults2.data_dir,
|
|
103
|
-
runtime: parsed2.runtime ?? defaults2.runtime,
|
|
104
|
-
ports: {
|
|
105
|
-
anvil: parsed2.ports?.anvil ?? defaults2.ports.anvil,
|
|
106
|
-
vault_rest: parsed2.ports?.vault_rest ?? defaults2.ports.vault_rest,
|
|
107
|
-
vault_mcp: parsed2.ports?.vault_mcp ?? defaults2.ports.vault_mcp,
|
|
108
|
-
forge: parsed2.ports?.forge ?? defaults2.ports.forge
|
|
109
|
-
},
|
|
110
|
-
git_host: parsed2.git_host ?? defaults2.git_host,
|
|
111
|
-
repos: {
|
|
112
|
-
anvil_notes: parsed2.repos?.anvil_notes ?? defaults2.repos.anvil_notes,
|
|
113
|
-
vault_knowledge: parsed2.repos?.vault_knowledge ?? defaults2.repos.vault_knowledge,
|
|
114
|
-
forge_registry: parsed2.repos?.forge_registry ?? defaults2.repos.forge_registry
|
|
115
|
-
},
|
|
116
|
-
host_repos_path: parsed2.host_repos_path ?? defaults2.host_repos_path,
|
|
117
|
-
host_repos_extra_scan_dirs: parsed2.host_repos_extra_scan_dirs ?? defaults2.host_repos_extra_scan_dirs,
|
|
118
|
-
github_token: parsed2.github_token ?? defaults2.github_token
|
|
119
|
-
};
|
|
101
|
+
return buildConfigFromParsed(parsed2);
|
|
120
102
|
}
|
|
121
103
|
return defaultConfig();
|
|
122
104
|
}
|
|
123
105
|
const raw = readFileSync2(CONFIG_PATH, "utf-8");
|
|
124
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
|
+
}
|
|
125
116
|
const defaults = defaultConfig();
|
|
117
|
+
const parsedPorts = parsed.ports;
|
|
126
118
|
return {
|
|
127
119
|
version: parsed.version ?? defaults.version,
|
|
128
120
|
data_dir: parsed.data_dir ?? defaults.data_dir,
|
|
129
121
|
runtime: parsed.runtime ?? defaults.runtime,
|
|
130
122
|
ports: {
|
|
131
|
-
anvil:
|
|
132
|
-
vault_rest:
|
|
133
|
-
vault_mcp:
|
|
134
|
-
|
|
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
|
|
135
128
|
},
|
|
136
|
-
git_host: parsed.git_host ?? defaults.git_host,
|
|
137
129
|
repos: {
|
|
138
|
-
anvil_notes:
|
|
139
|
-
|
|
140
|
-
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
|
|
141
132
|
},
|
|
133
|
+
vaults: parsed.vaults ?? defaults.vaults,
|
|
134
|
+
github_hosts: parsed.github_hosts ?? defaults.github_hosts,
|
|
142
135
|
host_repos_path: parsed.host_repos_path ?? defaults.host_repos_path,
|
|
143
|
-
host_repos_extra_scan_dirs: parsed.host_repos_extra_scan_dirs ?? defaults.host_repos_extra_scan_dirs
|
|
144
|
-
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
|
|
145
137
|
};
|
|
146
138
|
}
|
|
147
139
|
function saveConfig(config) {
|
|
@@ -149,6 +141,14 @@ function saveConfig(config) {
|
|
|
149
141
|
const yaml = stringifyYaml(config, { lineWidth: 0 });
|
|
150
142
|
writeFileSync(CONFIG_PATH, yaml, "utf-8");
|
|
151
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
|
+
}
|
|
152
152
|
function resolvePath(p) {
|
|
153
153
|
if (p.startsWith("~")) {
|
|
154
154
|
return resolve(homedir2(), p.slice(2));
|
|
@@ -218,15 +218,12 @@ function generateEnv(config) {
|
|
|
218
218
|
`ANVIL_PORT=${config.ports.anvil}`,
|
|
219
219
|
`VAULT_PORT=${config.ports.vault_rest}`,
|
|
220
220
|
`VAULT_MCP_PORT=${config.ports.vault_mcp}`,
|
|
221
|
+
`VAULT_ROUTER_PORT=${config.ports.vault_router}`,
|
|
221
222
|
`FORGE_PORT=${config.ports.forge}`,
|
|
222
223
|
"",
|
|
223
224
|
"# Repository URLs (must be HTTPS \u2014 container services do not have SSH keys)",
|
|
224
225
|
`ANVIL_REPO_URL=${config.repos.anvil_notes}`,
|
|
225
|
-
`VAULT_KNOWLEDGE_REPO_URL=${config.repos.vault_knowledge}`,
|
|
226
226
|
`FORGE_REGISTRY_REPO_URL=${config.repos.forge_registry}`,
|
|
227
|
-
"",
|
|
228
|
-
"# Authentication",
|
|
229
|
-
`GITHUB_TOKEN=${config.github_token}`,
|
|
230
227
|
""
|
|
231
228
|
];
|
|
232
229
|
return lines.join("\n");
|
|
@@ -244,11 +241,9 @@ var CONFIG_KEYS = [
|
|
|
244
241
|
"port.anvil",
|
|
245
242
|
"port.vault-rest",
|
|
246
243
|
"port.vault-mcp",
|
|
244
|
+
"port.vault-router",
|
|
247
245
|
"port.forge",
|
|
248
|
-
"github-token",
|
|
249
|
-
"git-host",
|
|
250
246
|
"repo.anvil-notes",
|
|
251
|
-
"repo.vault-knowledge",
|
|
252
247
|
"repo.forge-registry"
|
|
253
248
|
];
|
|
254
249
|
function getConfigValue(config, key) {
|
|
@@ -267,16 +262,12 @@ function getConfigValue(config, key) {
|
|
|
267
262
|
return String(config.ports.vault_rest);
|
|
268
263
|
case "port.vault-mcp":
|
|
269
264
|
return String(config.ports.vault_mcp);
|
|
265
|
+
case "port.vault-router":
|
|
266
|
+
return String(config.ports.vault_router);
|
|
270
267
|
case "port.forge":
|
|
271
268
|
return String(config.ports.forge);
|
|
272
|
-
case "github-token":
|
|
273
|
-
return config.github_token;
|
|
274
|
-
case "git-host":
|
|
275
|
-
return config.git_host;
|
|
276
269
|
case "repo.anvil-notes":
|
|
277
270
|
return config.repos.anvil_notes;
|
|
278
|
-
case "repo.vault-knowledge":
|
|
279
|
-
return config.repos.vault_knowledge;
|
|
280
271
|
case "repo.forge-registry":
|
|
281
272
|
return config.repos.forge_registry;
|
|
282
273
|
}
|
|
@@ -308,21 +299,15 @@ function setConfigValue(config, key, value) {
|
|
|
308
299
|
case "port.vault-mcp":
|
|
309
300
|
updated.ports = { ...updated.ports, vault_mcp: parseInt(value, 10) };
|
|
310
301
|
break;
|
|
302
|
+
case "port.vault-router":
|
|
303
|
+
updated.ports = { ...updated.ports, vault_router: parseInt(value, 10) };
|
|
304
|
+
break;
|
|
311
305
|
case "port.forge":
|
|
312
306
|
updated.ports = { ...updated.ports, forge: parseInt(value, 10) };
|
|
313
307
|
break;
|
|
314
|
-
case "github-token":
|
|
315
|
-
updated.github_token = value;
|
|
316
|
-
break;
|
|
317
|
-
case "git-host":
|
|
318
|
-
updated.git_host = value;
|
|
319
|
-
break;
|
|
320
308
|
case "repo.anvil-notes":
|
|
321
309
|
updated.repos = { ...updated.repos, anvil_notes: value };
|
|
322
310
|
break;
|
|
323
|
-
case "repo.vault-knowledge":
|
|
324
|
-
updated.repos = { ...updated.repos, vault_knowledge: value };
|
|
325
|
-
break;
|
|
326
311
|
case "repo.forge-registry":
|
|
327
312
|
updated.repos = { ...updated.repos, forge_registry: value };
|
|
328
313
|
break;
|
|
@@ -552,41 +537,280 @@ Run '${runtime.name} compose logs' from ~/Horus/ to investigate.`
|
|
|
552
537
|
|
|
553
538
|
// src/lib/compose.ts
|
|
554
539
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
|
|
555
|
-
import { join as join2, dirname as dirname2 } from "path";
|
|
556
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
557
|
-
var __filename = fileURLToPath2(import.meta.url);
|
|
558
|
-
var __dirname = dirname2(__filename);
|
|
559
|
-
function getBundledComposePath() {
|
|
560
|
-
const candidates = [
|
|
561
|
-
join2(__dirname, "..", "..", "compose", "docker-compose.yml"),
|
|
562
|
-
join2(__dirname, "..", "compose", "docker-compose.yml")
|
|
563
|
-
];
|
|
564
|
-
for (const candidate of candidates) {
|
|
565
|
-
if (existsSync3(candidate)) {
|
|
566
|
-
return candidate;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
throw new Error(
|
|
570
|
-
`Bundled docker-compose.yml not found. The CLI package may be corrupted.
|
|
571
|
-
Searched: ${candidates.join(", ")}`
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
540
|
function applyPodmanUserOverride(compose) {
|
|
575
541
|
return compose.replace(
|
|
576
542
|
/^( image: .+)$/gm,
|
|
577
543
|
'$1\n user: "0:0"'
|
|
578
544
|
);
|
|
579
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
|
+
}
|
|
580
808
|
function composeFileExists() {
|
|
581
809
|
return existsSync3(COMPOSE_PATH);
|
|
582
810
|
}
|
|
583
|
-
function installComposeFile(runtime) {
|
|
811
|
+
function installComposeFile(config, runtime) {
|
|
584
812
|
ensureHorusDir();
|
|
585
|
-
const
|
|
586
|
-
let content = readFileSync3(bundledPath, "utf-8");
|
|
587
|
-
if (runtime === "podman") {
|
|
588
|
-
content = applyPodmanUserOverride(content);
|
|
589
|
-
}
|
|
813
|
+
const content = generateComposeFile(config, runtime);
|
|
590
814
|
writeFileSync2(COMPOSE_PATH, content, "utf-8");
|
|
591
815
|
}
|
|
592
816
|
|
|
@@ -596,22 +820,22 @@ import chalk from "chalk";
|
|
|
596
820
|
import ora from "ora";
|
|
597
821
|
import { checkbox } from "@inquirer/prompts";
|
|
598
822
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
|
|
599
|
-
import { join as
|
|
823
|
+
import { join as join2 } from "path";
|
|
600
824
|
import { homedir as homedir3 } from "os";
|
|
601
825
|
import { execa as execa2 } from "execa";
|
|
602
826
|
function detectInstalledClients() {
|
|
603
827
|
const detected = [];
|
|
604
828
|
const home = homedir3();
|
|
605
|
-
const claudeDesktopDir =
|
|
829
|
+
const claudeDesktopDir = join2(home, "Library", "Application Support", "Claude");
|
|
606
830
|
if (existsSync4(claudeDesktopDir)) {
|
|
607
831
|
detected.push("claude-desktop");
|
|
608
832
|
}
|
|
609
|
-
const claudeCodeDir =
|
|
833
|
+
const claudeCodeDir = join2(home, ".claude");
|
|
610
834
|
if (existsSync4(claudeCodeDir)) {
|
|
611
835
|
detected.push("claude-code");
|
|
612
836
|
}
|
|
613
|
-
const cursorDir =
|
|
614
|
-
const cursorAppDir =
|
|
837
|
+
const cursorDir = join2(home, ".cursor");
|
|
838
|
+
const cursorAppDir = join2(home, "Library", "Application Support", "Cursor");
|
|
615
839
|
if (existsSync4(cursorDir) || existsSync4(cursorAppDir)) {
|
|
616
840
|
detected.push("cursor");
|
|
617
841
|
}
|
|
@@ -621,11 +845,11 @@ function getConfigPath(target) {
|
|
|
621
845
|
const home = homedir3();
|
|
622
846
|
switch (target) {
|
|
623
847
|
case "claude-desktop":
|
|
624
|
-
return
|
|
848
|
+
return join2(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
625
849
|
case "claude-code":
|
|
626
|
-
return
|
|
850
|
+
return join2(home, ".claude", "settings.json");
|
|
627
851
|
case "cursor":
|
|
628
|
-
return
|
|
852
|
+
return join2(home, ".cursor", "mcp.json");
|
|
629
853
|
}
|
|
630
854
|
}
|
|
631
855
|
function mergeAndWriteConfig(configPath, mcpServers) {
|
|
@@ -645,7 +869,7 @@ function mergeAndWriteConfig(configPath, mcpServers) {
|
|
|
645
869
|
writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
646
870
|
}
|
|
647
871
|
function getMcpRemoteWrapperPath() {
|
|
648
|
-
return
|
|
872
|
+
return join2(homedir3(), ".forge", "bin", "mcp-remote-wrapper");
|
|
649
873
|
}
|
|
650
874
|
function buildStdioServers(config, wrapperPath, host) {
|
|
651
875
|
return {
|
|
@@ -683,14 +907,14 @@ async function registerWithClaudeCode(mcpServers) {
|
|
|
683
907
|
}
|
|
684
908
|
async function syncSkills(runtime) {
|
|
685
909
|
const home = homedir3();
|
|
686
|
-
const skillsBase =
|
|
910
|
+
const skillsBase = join2(home, ".claude", "skills");
|
|
687
911
|
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
688
912
|
const forgeContainer = "horus-forge-1";
|
|
689
913
|
for (const skill of skills) {
|
|
690
|
-
const destDir =
|
|
914
|
+
const destDir = join2(skillsBase, skill);
|
|
691
915
|
mkdirSync2(destDir, { recursive: true });
|
|
692
916
|
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
693
|
-
const dest =
|
|
917
|
+
const dest = join2(destDir, "SKILL.md");
|
|
694
918
|
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
695
919
|
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
696
920
|
writeFileSync3(dest, result.stdout, "utf-8");
|
|
@@ -699,8 +923,8 @@ async function syncSkills(runtime) {
|
|
|
699
923
|
}
|
|
700
924
|
async function syncSkillsForCursor(runtime) {
|
|
701
925
|
const home = homedir3();
|
|
702
|
-
const rulesDir =
|
|
703
|
-
const skillsBase =
|
|
926
|
+
const rulesDir = join2(home, ".cursor", "rules");
|
|
927
|
+
const skillsBase = join2(home, ".cursor", "skills-cursor");
|
|
704
928
|
const skills = ["horus-anvil", "horus-vault", "horus-forge"];
|
|
705
929
|
const forgeContainer = "horus-forge-1";
|
|
706
930
|
mkdirSync2(rulesDir, { recursive: true });
|
|
@@ -708,7 +932,7 @@ async function syncSkillsForCursor(runtime) {
|
|
|
708
932
|
const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
|
|
709
933
|
const result = await runtime.exec(forgeContainer, "cat", src);
|
|
710
934
|
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
711
|
-
const ruleDest =
|
|
935
|
+
const ruleDest = join2(rulesDir, `${skill}.mdc`);
|
|
712
936
|
const frontmatter = `---
|
|
713
937
|
description: Horus ${skill} reference
|
|
714
938
|
alwaysApply: true
|
|
@@ -716,9 +940,9 @@ alwaysApply: true
|
|
|
716
940
|
|
|
717
941
|
`;
|
|
718
942
|
writeFileSync3(ruleDest, frontmatter + result.stdout, "utf-8");
|
|
719
|
-
const skillDir =
|
|
943
|
+
const skillDir = join2(skillsBase, skill);
|
|
720
944
|
mkdirSync2(skillDir, { recursive: true });
|
|
721
|
-
writeFileSync3(
|
|
945
|
+
writeFileSync3(join2(skillDir, "SKILL.md"), result.stdout, "utf-8");
|
|
722
946
|
}
|
|
723
947
|
}
|
|
724
948
|
}
|
|
@@ -906,7 +1130,14 @@ function injectToken(url, token) {
|
|
|
906
1130
|
return url;
|
|
907
1131
|
}
|
|
908
1132
|
}
|
|
909
|
-
|
|
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) => {
|
|
910
1141
|
console.log("");
|
|
911
1142
|
console.log(chalk2.bold("Horus Setup"));
|
|
912
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"));
|
|
@@ -968,18 +1199,47 @@ var setupCommand = new Command2("setup").description("Interactive first-run setu
|
|
|
968
1199
|
let config;
|
|
969
1200
|
if (opts.yes) {
|
|
970
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
|
+
}
|
|
971
1232
|
config = {
|
|
972
1233
|
...defaults,
|
|
973
1234
|
runtime: runtime.name,
|
|
974
1235
|
data_dir: opts.dataDir || DEFAULT_DATA_DIR,
|
|
975
1236
|
host_repos_path: opts.reposPath || "",
|
|
976
|
-
git_host: opts.gitHost || defaults.git_host,
|
|
977
1237
|
repos: {
|
|
978
|
-
anvil_notes:
|
|
979
|
-
vault_knowledge: opts.vaultRepo || process.env.VAULT_KNOWLEDGE_REPO_URL || defaults.repos.vault_knowledge,
|
|
1238
|
+
anvil_notes: anvilRepo,
|
|
980
1239
|
forge_registry: opts.forgeRepo || process.env.FORGE_REGISTRY_REPO_URL || defaults.repos.forge_registry
|
|
981
1240
|
},
|
|
982
|
-
|
|
1241
|
+
vaults,
|
|
1242
|
+
github_hosts
|
|
983
1243
|
};
|
|
984
1244
|
} else {
|
|
985
1245
|
const data_dir = await input({
|
|
@@ -1002,13 +1262,17 @@ var setupCommand = new Command2("setup").description("Interactive first-run setu
|
|
|
1002
1262
|
default: DEFAULT_PORTS.anvil
|
|
1003
1263
|
});
|
|
1004
1264
|
const vault_rest = await number({
|
|
1005
|
-
message: "Vault REST port:",
|
|
1265
|
+
message: "Vault REST port (per-vault instances):",
|
|
1006
1266
|
default: DEFAULT_PORTS.vault_rest
|
|
1007
1267
|
});
|
|
1008
1268
|
const vault_mcp = await number({
|
|
1009
1269
|
message: "Vault MCP port:",
|
|
1010
1270
|
default: DEFAULT_PORTS.vault_mcp
|
|
1011
1271
|
});
|
|
1272
|
+
const vault_router = await number({
|
|
1273
|
+
message: "Vault Router port:",
|
|
1274
|
+
default: DEFAULT_PORTS.vault_router
|
|
1275
|
+
});
|
|
1012
1276
|
const forge = await number({
|
|
1013
1277
|
message: "Forge port:",
|
|
1014
1278
|
default: DEFAULT_PORTS.forge
|
|
@@ -1017,6 +1281,7 @@ var setupCommand = new Command2("setup").description("Interactive first-run setu
|
|
|
1017
1281
|
anvil: anvil ?? DEFAULT_PORTS.anvil,
|
|
1018
1282
|
vault_rest: vault_rest ?? DEFAULT_PORTS.vault_rest,
|
|
1019
1283
|
vault_mcp: vault_mcp ?? DEFAULT_PORTS.vault_mcp,
|
|
1284
|
+
vault_router: vault_router ?? DEFAULT_PORTS.vault_router,
|
|
1020
1285
|
forge: forge ?? DEFAULT_PORTS.forge
|
|
1021
1286
|
};
|
|
1022
1287
|
}
|
|
@@ -1028,12 +1293,11 @@ var setupCommand = new Command2("setup").description("Interactive first-run setu
|
|
|
1028
1293
|
console.log(chalk2.yellow(" Use HTTPS URLs \u2014 container services do not have SSH keys."));
|
|
1029
1294
|
console.log(chalk2.dim(" SSH URLs (git@github.com:...) will fail at runtime inside Docker/Podman."));
|
|
1030
1295
|
console.log("");
|
|
1031
|
-
const
|
|
1032
|
-
message: "Git server hostname:",
|
|
1296
|
+
const primaryHost = await input({
|
|
1297
|
+
message: "Primary Git server hostname:",
|
|
1033
1298
|
default: "github.com"
|
|
1034
1299
|
});
|
|
1035
|
-
const
|
|
1036
|
-
const example = (repo) => chalk2.dim(` e.g., https://${host}/<owner>/${repo}`);
|
|
1300
|
+
const example = (repo) => chalk2.dim(` e.g., https://${primaryHost}/<owner>/${repo}`);
|
|
1037
1301
|
console.log("");
|
|
1038
1302
|
const anvil_notes = await input({
|
|
1039
1303
|
message: `Anvil notes repo URL:
|
|
@@ -1041,12 +1305,6 @@ ${example("horus-notes")}
|
|
|
1041
1305
|
`,
|
|
1042
1306
|
validate: (v) => v.trim().length > 0 || "Anvil needs a notes repo to store your data."
|
|
1043
1307
|
});
|
|
1044
|
-
const vault_knowledge = await input({
|
|
1045
|
-
message: `Vault knowledge-base repo URL:
|
|
1046
|
-
${example("knowledge-base")}
|
|
1047
|
-
`,
|
|
1048
|
-
validate: (v) => v.trim().length > 0 || "Vault needs a knowledge-base repo."
|
|
1049
|
-
});
|
|
1050
1308
|
const forge_registry = await input({
|
|
1051
1309
|
message: `Forge registry repo URL:
|
|
1052
1310
|
${example("forge-registry")}
|
|
@@ -1054,13 +1312,75 @@ ${example("forge-registry")}
|
|
|
1054
1312
|
validate: (v) => v.trim().length > 0 || "Forge needs a registry repo."
|
|
1055
1313
|
});
|
|
1056
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("");
|
|
1057
1366
|
console.log(chalk2.bold("Authentication"));
|
|
1058
|
-
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."));
|
|
1059
1368
|
console.log("");
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
+
}
|
|
1064
1384
|
config = {
|
|
1065
1385
|
...defaultConfig(),
|
|
1066
1386
|
data_dir,
|
|
@@ -1068,13 +1388,12 @@ ${example("forge-registry")}
|
|
|
1068
1388
|
host_repos_extra_scan_dirs,
|
|
1069
1389
|
runtime: runtime.name,
|
|
1070
1390
|
ports,
|
|
1071
|
-
git_host: git_host.trim(),
|
|
1072
1391
|
repos: {
|
|
1073
1392
|
anvil_notes: anvil_notes.trim(),
|
|
1074
|
-
vault_knowledge: vault_knowledge.trim(),
|
|
1075
1393
|
forge_registry: forge_registry.trim()
|
|
1076
1394
|
},
|
|
1077
|
-
|
|
1395
|
+
vaults,
|
|
1396
|
+
github_hosts
|
|
1078
1397
|
};
|
|
1079
1398
|
}
|
|
1080
1399
|
const configSpinner = ora2("Saving configuration...").start();
|
|
@@ -1097,7 +1416,7 @@ ${example("forge-registry")}
|
|
|
1097
1416
|
}
|
|
1098
1417
|
const composeSpinner = ora2("Installing docker-compose.yml...").start();
|
|
1099
1418
|
try {
|
|
1100
|
-
installComposeFile(runtime.name);
|
|
1419
|
+
installComposeFile(config, runtime.name);
|
|
1101
1420
|
composeSpinner.succeed("Compose file installed to ~/Horus/docker-compose.yml");
|
|
1102
1421
|
} catch (error) {
|
|
1103
1422
|
composeSpinner.fail("Failed to install compose file");
|
|
@@ -1105,24 +1424,46 @@ ${example("forge-registry")}
|
|
|
1105
1424
|
process.exit(1);
|
|
1106
1425
|
}
|
|
1107
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 ?? "";
|
|
1108
1429
|
const reposToClone = [
|
|
1109
|
-
{
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
+
}
|
|
1112
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
|
+
}
|
|
1113
1454
|
if (reposToClone.length > 0) {
|
|
1114
1455
|
console.log("");
|
|
1115
1456
|
console.log(chalk2.bold("Cloning repositories..."));
|
|
1116
1457
|
mkdirSync3(dataDir, { recursive: true });
|
|
1117
1458
|
for (const repo of reposToClone) {
|
|
1118
1459
|
const spinner = ora2(`Cloning ${repo.label}...`).start();
|
|
1119
|
-
if (existsSync5(
|
|
1460
|
+
if (existsSync5(join3(repo.dest, ".git"))) {
|
|
1120
1461
|
spinner.succeed(`${repo.label} already cloned`);
|
|
1121
1462
|
continue;
|
|
1122
1463
|
}
|
|
1123
1464
|
try {
|
|
1124
1465
|
mkdirSync3(repo.dest, { recursive: true });
|
|
1125
|
-
const cloneUrl = injectToken(repo.url,
|
|
1466
|
+
const cloneUrl = injectToken(repo.url, repo.token);
|
|
1126
1467
|
execSync(`git clone "${cloneUrl}" "${repo.dest}"`, {
|
|
1127
1468
|
stdio: "pipe",
|
|
1128
1469
|
timeout: 6e4
|
|
@@ -1137,7 +1478,7 @@ ${example("forge-registry")}
|
|
|
1137
1478
|
console.log(chalk2.dim(` ${msg.split("\n")[0]}`));
|
|
1138
1479
|
}
|
|
1139
1480
|
console.log(chalk2.dim(` URL: ${repo.url}`));
|
|
1140
|
-
if (!
|
|
1481
|
+
if (!repo.token) {
|
|
1141
1482
|
console.log(chalk2.dim(" Tip: Re-run setup and provide a GitHub token if the repo is private."));
|
|
1142
1483
|
}
|
|
1143
1484
|
process.exit(1);
|
|
@@ -1209,11 +1550,19 @@ ${example("forge-registry")}
|
|
|
1209
1550
|
console.log(` ${chalk2.bold("Data:")} ${config.data_dir}`);
|
|
1210
1551
|
console.log("");
|
|
1211
1552
|
console.log(chalk2.bold(" Service URLs:"));
|
|
1212
|
-
console.log(` Anvil:
|
|
1213
|
-
console.log(` Vault
|
|
1214
|
-
console.log(` Vault MCP:
|
|
1215
|
-
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}`);
|
|
1216
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;
|
|
1217
1566
|
});
|
|
1218
1567
|
|
|
1219
1568
|
// src/commands/up.ts
|
|
@@ -1418,19 +1767,36 @@ var configCommand = new Command6("config").description("View or modify Horus con
|
|
|
1418
1767
|
console.log(` ${chalk6.bold("host-repos-path:")} ${config.host_repos_path || chalk6.dim("(not set)")}`);
|
|
1419
1768
|
const extraDirs = (config.host_repos_extra_scan_dirs ?? []).join(", ");
|
|
1420
1769
|
console.log(` ${chalk6.bold("host-repos-extra-scan-dirs:")} ${extraDirs || chalk6.dim("(not set)")}`);
|
|
1421
|
-
console.log(` ${chalk6.bold("git-host:")} ${config.git_host || chalk6.dim("(not set)")}`);
|
|
1422
|
-
console.log(` ${chalk6.bold("github-token:")} ${config.github_token ? maskApiKey(config.github_token) : chalk6.dim("(not set)")}`);
|
|
1423
1770
|
console.log("");
|
|
1424
1771
|
console.log(chalk6.bold(" Ports:"));
|
|
1425
|
-
console.log(` ${chalk6.bold("anvil:")}
|
|
1426
|
-
console.log(` ${chalk6.bold("vault-rest:")}
|
|
1427
|
-
console.log(` ${chalk6.bold("vault-mcp:")}
|
|
1428
|
-
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}`);
|
|
1429
1777
|
console.log("");
|
|
1430
1778
|
console.log(chalk6.bold(" Repos:"));
|
|
1431
|
-
console.log(` ${chalk6.bold("anvil-notes:")}
|
|
1432
|
-
console.log(` ${chalk6.bold("
|
|
1433
|
-
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
|
+
}
|
|
1434
1800
|
console.log("");
|
|
1435
1801
|
console.log(chalk6.dim(` Config file: ~/Horus/config.yaml`));
|
|
1436
1802
|
console.log(chalk6.dim(` Use 'horus config get <key>' or 'horus config set <key> <value>'`));
|
|
@@ -1449,11 +1815,7 @@ configCommand.command("get <key>").description("Get a configuration value").acti
|
|
|
1449
1815
|
}
|
|
1450
1816
|
const config = loadConfig();
|
|
1451
1817
|
const value = getConfigValue(config, key);
|
|
1452
|
-
|
|
1453
|
-
console.log(maskApiKey(value));
|
|
1454
|
-
} else {
|
|
1455
|
-
console.log(value || "");
|
|
1456
|
-
}
|
|
1818
|
+
console.log(value || "");
|
|
1457
1819
|
});
|
|
1458
1820
|
configCommand.command("set <key> <value>").description("Set a configuration value").action(async (key, value) => {
|
|
1459
1821
|
if (!configExists()) {
|
|
@@ -1484,6 +1846,7 @@ configCommand.command("set <key> <value>").description("Set a configuration valu
|
|
|
1484
1846
|
"port.anvil",
|
|
1485
1847
|
"port.vault-rest",
|
|
1486
1848
|
"port.vault-mcp",
|
|
1849
|
+
"port.vault-router",
|
|
1487
1850
|
"port.forge"
|
|
1488
1851
|
];
|
|
1489
1852
|
if (needsRestart.includes(key)) {
|
|
@@ -1511,10 +1874,10 @@ import chalk7 from "chalk";
|
|
|
1511
1874
|
import ora6 from "ora";
|
|
1512
1875
|
import { select as select2, confirm as confirm3 } from "@inquirer/prompts";
|
|
1513
1876
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync2, existsSync as existsSync6 } from "fs";
|
|
1514
|
-
import { join as
|
|
1877
|
+
import { join as join4 } from "path";
|
|
1515
1878
|
import { createHash } from "crypto";
|
|
1516
1879
|
import { stringify as stringifyYaml2, parse as parseYaml2 } from "yaml";
|
|
1517
|
-
var SNAPSHOTS_DIR =
|
|
1880
|
+
var SNAPSHOTS_DIR = join4(HORUS_DIR, "snapshots");
|
|
1518
1881
|
function ensureSnapshotsDir() {
|
|
1519
1882
|
mkdirSync4(SNAPSHOTS_DIR, { recursive: true });
|
|
1520
1883
|
}
|
|
@@ -1549,14 +1912,14 @@ function saveSnapshot(images) {
|
|
|
1549
1912
|
images,
|
|
1550
1913
|
compose_hash: composeFileHash()
|
|
1551
1914
|
};
|
|
1552
|
-
const filePath =
|
|
1915
|
+
const filePath = join4(SNAPSHOTS_DIR, `${timestamp}.yaml`);
|
|
1553
1916
|
writeFileSync4(filePath, stringifyYaml2(snapshot, { lineWidth: 0 }), "utf-8");
|
|
1554
1917
|
return filePath;
|
|
1555
1918
|
}
|
|
1556
1919
|
function listSnapshots() {
|
|
1557
1920
|
if (!existsSync6(SNAPSHOTS_DIR)) return [];
|
|
1558
1921
|
return readdirSync2(SNAPSHOTS_DIR).filter((f) => f.endsWith(".yaml")).sort().reverse().map((f) => {
|
|
1559
|
-
const file =
|
|
1922
|
+
const file = join4(SNAPSHOTS_DIR, f);
|
|
1560
1923
|
const snapshot = parseYaml2(readFileSync5(file, "utf-8"));
|
|
1561
1924
|
return { file, snapshot };
|
|
1562
1925
|
});
|
|
@@ -1767,7 +2130,7 @@ import { Command as Command8 } from "commander";
|
|
|
1767
2130
|
import chalk8 from "chalk";
|
|
1768
2131
|
import { execSync as execSync2 } from "child_process";
|
|
1769
2132
|
import { existsSync as existsSync7, accessSync, statfsSync, constants } from "fs";
|
|
1770
|
-
import { join as
|
|
2133
|
+
import { join as join5 } from "path";
|
|
1771
2134
|
function symbol(status) {
|
|
1772
2135
|
switch (status) {
|
|
1773
2136
|
case "pass":
|
|
@@ -1895,7 +2258,7 @@ function checkDataDir(dataDir) {
|
|
|
1895
2258
|
}
|
|
1896
2259
|
}
|
|
1897
2260
|
function checkDiskSpace(dataDir) {
|
|
1898
|
-
const checkDir = existsSync7(dataDir) ? dataDir :
|
|
2261
|
+
const checkDir = existsSync7(dataDir) ? dataDir : join5(dataDir, "..");
|
|
1899
2262
|
try {
|
|
1900
2263
|
const stats = statfsSync(checkDir);
|
|
1901
2264
|
const freeBytes = stats.bfree * stats.bsize;
|
|
@@ -2028,10 +2391,10 @@ import chalk9 from "chalk";
|
|
|
2028
2391
|
import ora7 from "ora";
|
|
2029
2392
|
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
2030
2393
|
import { mkdirSync as mkdirSync5, statSync as statSync2, existsSync as existsSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
2031
|
-
import { join as
|
|
2394
|
+
import { join as join6, basename } from "path";
|
|
2032
2395
|
import { execSync as execSync3 } from "child_process";
|
|
2033
2396
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
2034
|
-
var BACKUPS_DIR =
|
|
2397
|
+
var BACKUPS_DIR = join6(HORUS_DIR, "backups");
|
|
2035
2398
|
function ensureBackupsDir() {
|
|
2036
2399
|
mkdirSync5(BACKUPS_DIR, { recursive: true });
|
|
2037
2400
|
}
|
|
@@ -2078,8 +2441,8 @@ async function createBackup(yes) {
|
|
|
2078
2441
|
}
|
|
2079
2442
|
ensureBackupsDir();
|
|
2080
2443
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2081
|
-
const tarFile =
|
|
2082
|
-
const metaFile =
|
|
2444
|
+
const tarFile = join6(BACKUPS_DIR, `${timestamp}.tar.gz`);
|
|
2445
|
+
const metaFile = join6(BACKUPS_DIR, `${timestamp}.meta.yaml`);
|
|
2083
2446
|
const backupSpinner = ora7("Creating backup archive...").start();
|
|
2084
2447
|
try {
|
|
2085
2448
|
execSync3(`tar -czf "${tarFile}" -C "${HORUS_DIR}" data/`, {
|