@arkhera30/cli 0.1.10 → 0.1.12
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 +4 -0
- package/dist/index.js +157 -81
- package/package.json +1 -1
|
@@ -29,6 +29,7 @@ services:
|
|
|
29
29
|
image: ghcr.io/arjunkhera/horus/qmd-daemon:latest
|
|
30
30
|
environment:
|
|
31
31
|
- QMD_DAEMON_PORT=8181
|
|
32
|
+
- HORUS_RUNTIME=${HORUS_RUNTIME:-docker}
|
|
32
33
|
volumes:
|
|
33
34
|
- qmd-daemon-data:/home/qmd/.cache/qmd
|
|
34
35
|
networks:
|
|
@@ -60,6 +61,7 @@ services:
|
|
|
60
61
|
# Shared QMD database + model cache (same volume as qmd-daemon).
|
|
61
62
|
- qmd-daemon-data:/home/anvil/.cache/qmd
|
|
62
63
|
environment:
|
|
64
|
+
- HORUS_RUNTIME=${HORUS_RUNTIME:-docker}
|
|
63
65
|
- ANVIL_TRANSPORT=http
|
|
64
66
|
- ANVIL_PORT=8100
|
|
65
67
|
- ANVIL_HOST=0.0.0.0
|
|
@@ -105,6 +107,7 @@ services:
|
|
|
105
107
|
# Shared QMD database + model cache (same volume as qmd-daemon).
|
|
106
108
|
- qmd-daemon-data:/home/appuser/.cache/qmd
|
|
107
109
|
environment:
|
|
110
|
+
- HORUS_RUNTIME=${HORUS_RUNTIME:-docker}
|
|
108
111
|
- KNOWLEDGE_REPO_PATH=/data/knowledge-repo
|
|
109
112
|
- WORKSPACE_PATH=/data/workspace
|
|
110
113
|
- VAULT_KNOWLEDGE_REPO_URL=${VAULT_KNOWLEDGE_REPO_URL:-}
|
|
@@ -183,6 +186,7 @@ services:
|
|
|
183
186
|
# Local repos — read-only; lets Forge scan host repos for the index
|
|
184
187
|
- ${HOST_REPOS_PATH}:/data/repos:ro
|
|
185
188
|
environment:
|
|
189
|
+
- HORUS_RUNTIME=${HORUS_RUNTIME:-docker}
|
|
186
190
|
- FORGE_PORT=8200
|
|
187
191
|
- FORGE_HOST=0.0.0.0
|
|
188
192
|
- FORGE_REGISTRY_PATH=/data/registry
|
package/dist/index.js
CHANGED
|
@@ -15,14 +15,20 @@ import { join as join4 } from "path";
|
|
|
15
15
|
import { input, confirm, number, select, password } from "@inquirer/prompts";
|
|
16
16
|
|
|
17
17
|
// src/lib/config.ts
|
|
18
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
19
|
-
import { resolve } from "path";
|
|
18
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from "fs";
|
|
19
|
+
import { resolve, join as pathJoin, relative } from "path";
|
|
20
20
|
import { homedir as homedir2 } from "os";
|
|
21
21
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
22
22
|
|
|
23
23
|
// src/lib/constants.ts
|
|
24
24
|
import { homedir } from "os";
|
|
25
|
-
import { join } from "path";
|
|
25
|
+
import { join, dirname } from "path";
|
|
26
|
+
import { readFileSync } from "fs";
|
|
27
|
+
import { fileURLToPath } from "url";
|
|
28
|
+
var __pkg_dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
var pkgPath = join(__pkg_dirname, "..", "..", "package.json");
|
|
30
|
+
var pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
31
|
+
var CLI_VERSION = pkg.version;
|
|
26
32
|
var HORUS_DIR = join(homedir(), ".horus");
|
|
27
33
|
var CONFIG_PATH = join(HORUS_DIR, "config.yaml");
|
|
28
34
|
var ENV_PATH = join(HORUS_DIR, ".env");
|
|
@@ -72,7 +78,7 @@ function loadConfig() {
|
|
|
72
78
|
if (!existsSync(CONFIG_PATH)) {
|
|
73
79
|
return defaultConfig();
|
|
74
80
|
}
|
|
75
|
-
const raw =
|
|
81
|
+
const raw = readFileSync2(CONFIG_PATH, "utf-8");
|
|
76
82
|
const parsed = parseYaml(raw);
|
|
77
83
|
const defaults = defaultConfig();
|
|
78
84
|
return {
|
|
@@ -107,18 +113,61 @@ function resolvePath(p) {
|
|
|
107
113
|
}
|
|
108
114
|
return resolve(p);
|
|
109
115
|
}
|
|
116
|
+
function discoverRepoDirs(rootDir, maxDepth = 4) {
|
|
117
|
+
const repoDirs = /* @__PURE__ */ new Set();
|
|
118
|
+
function walk(dir, depth) {
|
|
119
|
+
if (depth > maxDepth) return;
|
|
120
|
+
let entries;
|
|
121
|
+
try {
|
|
122
|
+
entries = readdirSync(dir);
|
|
123
|
+
} catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (entry === "node_modules" || entry === ".git") continue;
|
|
128
|
+
const full = pathJoin(dir, entry);
|
|
129
|
+
try {
|
|
130
|
+
if (!statSync(full).isDirectory()) continue;
|
|
131
|
+
} catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (existsSync(pathJoin(full, ".git"))) {
|
|
135
|
+
repoDirs.add(dir);
|
|
136
|
+
}
|
|
137
|
+
walk(full, depth + 1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (existsSync(rootDir)) {
|
|
141
|
+
walk(rootDir, 0);
|
|
142
|
+
}
|
|
143
|
+
return [...repoDirs];
|
|
144
|
+
}
|
|
110
145
|
function generateEnv(config) {
|
|
111
146
|
const dataDir = resolvePath(config.data_dir);
|
|
112
147
|
const hostReposPath = config.host_repos_path ? resolvePath(config.host_repos_path) : "";
|
|
113
148
|
const baseScanPath = "/data/repos";
|
|
114
|
-
|
|
115
|
-
|
|
149
|
+
let forgeScanPaths;
|
|
150
|
+
if (hostReposPath) {
|
|
151
|
+
const discoveredDirs = discoverRepoDirs(hostReposPath);
|
|
152
|
+
const containerPaths = discoveredDirs.map((dir) => {
|
|
153
|
+
const rel = relative(hostReposPath, dir);
|
|
154
|
+
return rel ? `${baseScanPath}/${rel}` : baseScanPath;
|
|
155
|
+
});
|
|
156
|
+
const allPaths = [baseScanPath, ...containerPaths];
|
|
157
|
+
const extraScanPaths = (config.host_repos_extra_scan_dirs ?? []).map((d) => d.trim()).filter(Boolean).map((d) => `${baseScanPath}/${d}`);
|
|
158
|
+
const uniquePaths = [.../* @__PURE__ */ new Set([...allPaths, ...extraScanPaths])];
|
|
159
|
+
forgeScanPaths = uniquePaths.join(":");
|
|
160
|
+
} else {
|
|
161
|
+
const extraScanPaths = (config.host_repos_extra_scan_dirs ?? []).map((d) => d.trim()).filter(Boolean).map((d) => `${baseScanPath}/${d}`);
|
|
162
|
+
forgeScanPaths = [baseScanPath, ...extraScanPaths].join(":");
|
|
163
|
+
}
|
|
116
164
|
const lines = [
|
|
117
165
|
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
118
166
|
"# Horus \u2014 Generated .env file",
|
|
119
167
|
"# Do not edit manually. Use `horus config set <key> <value>` instead.",
|
|
120
168
|
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
121
169
|
"",
|
|
170
|
+
`HORUS_RUNTIME=${config.runtime}`,
|
|
122
171
|
`HORUS_DATA_PATH=${dataDir}`,
|
|
123
172
|
`HOST_REPOS_PATH=${hostReposPath}`,
|
|
124
173
|
`FORGE_SCAN_PATHS=${forgeScanPaths}`,
|
|
@@ -298,6 +347,9 @@ function createRuntime(name) {
|
|
|
298
347
|
const result = await execa(bin, ["inspect", "--format", format, container], {
|
|
299
348
|
reject: false
|
|
300
349
|
});
|
|
350
|
+
if (result.exitCode !== 0) {
|
|
351
|
+
throw new Error(`inspect failed: ${result.stderr}`);
|
|
352
|
+
}
|
|
301
353
|
return result.stdout?.toString().trim() ?? "";
|
|
302
354
|
},
|
|
303
355
|
async isRunning() {
|
|
@@ -314,6 +366,24 @@ function createRuntime(name) {
|
|
|
314
366
|
}
|
|
315
367
|
};
|
|
316
368
|
}
|
|
369
|
+
function parseComposeJson(output) {
|
|
370
|
+
const trimmed = output.trim();
|
|
371
|
+
if (!trimmed) return [];
|
|
372
|
+
if (trimmed.startsWith("[")) {
|
|
373
|
+
try {
|
|
374
|
+
const parsed = JSON.parse(trimmed);
|
|
375
|
+
if (Array.isArray(parsed)) return parsed;
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return trimmed.split("\n").filter((line) => line.trim()).map((line) => {
|
|
380
|
+
try {
|
|
381
|
+
return JSON.parse(line);
|
|
382
|
+
} catch {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}).filter((item) => item !== null);
|
|
386
|
+
}
|
|
317
387
|
async function checkRuntime(name) {
|
|
318
388
|
return tryCommand(name, ["compose", "version"]);
|
|
319
389
|
}
|
|
@@ -357,14 +427,22 @@ async function composeStreaming(runtime, args) {
|
|
|
357
427
|
|
|
358
428
|
// src/lib/health.ts
|
|
359
429
|
async function checkContainerHealth(runtime, service) {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
430
|
+
const candidates = [`horus-${service}-1`, `horus_${service}_1`];
|
|
431
|
+
for (const containerName of candidates) {
|
|
432
|
+
try {
|
|
433
|
+
const healthStatus = await runtime.inspect(containerName, "{{.State.Health.Status}}");
|
|
434
|
+
if (healthStatus && !healthStatus.includes("<nil>") && healthStatus.trim() !== "") {
|
|
435
|
+
return { name: service, status: mapStatus(healthStatus) };
|
|
436
|
+
}
|
|
437
|
+
const stateStatus = await runtime.inspect(containerName, "{{.State.Status}}");
|
|
438
|
+
if (stateStatus && stateStatus.trim() !== "") {
|
|
439
|
+
return { name: service, status: mapStateStatus(stateStatus) };
|
|
440
|
+
}
|
|
441
|
+
} catch {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
367
444
|
}
|
|
445
|
+
return { name: service, status: "stopped" };
|
|
368
446
|
}
|
|
369
447
|
function mapStatus(raw) {
|
|
370
448
|
switch (raw.trim().toLowerCase()) {
|
|
@@ -378,13 +456,28 @@ function mapStatus(raw) {
|
|
|
378
456
|
return "unknown";
|
|
379
457
|
}
|
|
380
458
|
}
|
|
459
|
+
function mapStateStatus(raw) {
|
|
460
|
+
switch (raw.trim().toLowerCase()) {
|
|
461
|
+
case "running":
|
|
462
|
+
return "healthy";
|
|
463
|
+
case "created":
|
|
464
|
+
case "restarting":
|
|
465
|
+
return "starting";
|
|
466
|
+
case "exited":
|
|
467
|
+
case "dead":
|
|
468
|
+
case "removing":
|
|
469
|
+
return "unhealthy";
|
|
470
|
+
default:
|
|
471
|
+
return "unknown";
|
|
472
|
+
}
|
|
473
|
+
}
|
|
381
474
|
async function checkAllHealth(runtime) {
|
|
382
475
|
const results = await Promise.all(
|
|
383
476
|
SERVICES.map((service) => checkContainerHealth(runtime, service))
|
|
384
477
|
);
|
|
385
478
|
return results;
|
|
386
479
|
}
|
|
387
|
-
async function pollUntilHealthy(runtime, onUpdate, timeoutMs =
|
|
480
|
+
async function pollUntilHealthy(runtime, onUpdate, timeoutMs = 3e5, intervalMs = 5e3) {
|
|
388
481
|
const startTime = Date.now();
|
|
389
482
|
while (true) {
|
|
390
483
|
const states = await checkAllHealth(runtime);
|
|
@@ -400,7 +493,7 @@ async function pollUntilHealthy(runtime, onUpdate, timeoutMs = 6e5, intervalMs =
|
|
|
400
493
|
const unhealthyServices = states.filter((s) => s.status === "unhealthy").map((s) => s.name).join(", ");
|
|
401
494
|
throw new Error(
|
|
402
495
|
`Services failed health check: ${unhealthyServices}
|
|
403
|
-
Run '
|
|
496
|
+
Run '${runtime.name} compose logs <service>' from ~/.horus/ to investigate.`
|
|
404
497
|
);
|
|
405
498
|
}
|
|
406
499
|
const elapsed = Date.now() - startTime;
|
|
@@ -408,7 +501,7 @@ Run 'docker compose logs <service>' from ~/.horus/ to investigate.`
|
|
|
408
501
|
const notReady = states.filter((s) => s.status !== "healthy").map((s) => `${s.name} (${s.status})`).join(", ");
|
|
409
502
|
throw new Error(
|
|
410
503
|
`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for services: ${notReady}
|
|
411
|
-
Run '
|
|
504
|
+
Run '${runtime.name} compose logs' from ~/.horus/ to investigate.`
|
|
412
505
|
);
|
|
413
506
|
}
|
|
414
507
|
await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
|
|
@@ -416,11 +509,11 @@ Run 'docker compose logs' from ~/.horus/ to investigate.`
|
|
|
416
509
|
}
|
|
417
510
|
|
|
418
511
|
// src/lib/compose.ts
|
|
419
|
-
import { readFileSync as
|
|
420
|
-
import { join as join2, dirname } from "path";
|
|
421
|
-
import { fileURLToPath } from "url";
|
|
422
|
-
var __filename =
|
|
423
|
-
var __dirname =
|
|
512
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
513
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
514
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
515
|
+
var __filename = fileURLToPath2(import.meta.url);
|
|
516
|
+
var __dirname = dirname2(__filename);
|
|
424
517
|
function getBundledComposePath() {
|
|
425
518
|
const candidates = [
|
|
426
519
|
join2(__dirname, "..", "..", "compose", "docker-compose.yml"),
|
|
@@ -448,7 +541,7 @@ function composeFileExists() {
|
|
|
448
541
|
function installComposeFile(runtime) {
|
|
449
542
|
ensureHorusDir();
|
|
450
543
|
const bundledPath = getBundledComposePath();
|
|
451
|
-
let content =
|
|
544
|
+
let content = readFileSync3(bundledPath, "utf-8");
|
|
452
545
|
if (runtime === "podman") {
|
|
453
546
|
content = applyPodmanUserOverride(content);
|
|
454
547
|
}
|
|
@@ -460,7 +553,7 @@ import { Command } from "commander";
|
|
|
460
553
|
import chalk from "chalk";
|
|
461
554
|
import ora from "ora";
|
|
462
555
|
import { checkbox } from "@inquirer/prompts";
|
|
463
|
-
import { readFileSync as
|
|
556
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
464
557
|
import { join as join3 } from "path";
|
|
465
558
|
import { homedir as homedir3 } from "os";
|
|
466
559
|
import { execa as execa2 } from "execa";
|
|
@@ -497,7 +590,7 @@ function mergeAndWriteConfig(configPath, mcpServers) {
|
|
|
497
590
|
let existing = {};
|
|
498
591
|
if (existsSync3(configPath)) {
|
|
499
592
|
try {
|
|
500
|
-
const raw =
|
|
593
|
+
const raw = readFileSync4(configPath, "utf-8");
|
|
501
594
|
existing = JSON.parse(raw);
|
|
502
595
|
} catch {
|
|
503
596
|
existing = {};
|
|
@@ -820,11 +913,7 @@ var setupCommand = new Command2("setup").description("Interactive first-run setu
|
|
|
820
913
|
message: "Host repos path (for Forge repo scanning, leave empty to skip):",
|
|
821
914
|
default: ""
|
|
822
915
|
});
|
|
823
|
-
const
|
|
824
|
-
message: "Extra subdirectories to scan within repos path (comma-separated, e.g. ArjunKhera \u2014 leave empty to skip):",
|
|
825
|
-
default: ""
|
|
826
|
-
});
|
|
827
|
-
const host_repos_extra_scan_dirs = extra_scan_dirs_raw.split(",").map((d) => d.trim()).filter(Boolean);
|
|
916
|
+
const host_repos_extra_scan_dirs = [];
|
|
828
917
|
const customize_ports = await confirm({
|
|
829
918
|
message: "Customize port assignments?",
|
|
830
919
|
default: false
|
|
@@ -938,7 +1027,7 @@ ${example("forge-registry")}
|
|
|
938
1027
|
console.error(error.message);
|
|
939
1028
|
process.exit(1);
|
|
940
1029
|
}
|
|
941
|
-
const dataDir =
|
|
1030
|
+
const dataDir = resolvePath(config.data_dir);
|
|
942
1031
|
const reposToClone = [
|
|
943
1032
|
{ url: config.repos.anvil_notes, dest: join4(dataDir, "notes"), label: "Anvil notes" },
|
|
944
1033
|
{ url: config.repos.vault_knowledge, dest: join4(dataDir, "knowledge-base"), label: "Vault knowledge-base" },
|
|
@@ -1172,17 +1261,14 @@ var statusCommand = new Command5("status").description("Show status of Horus ser
|
|
|
1172
1261
|
let containers = [];
|
|
1173
1262
|
try {
|
|
1174
1263
|
const result = await runtime.compose("ps", "--format", "json");
|
|
1175
|
-
|
|
1176
|
-
if (output) {
|
|
1177
|
-
containers = output.split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
|
|
1178
|
-
}
|
|
1264
|
+
containers = parseComposeJson(result.stdout);
|
|
1179
1265
|
} catch {
|
|
1180
1266
|
}
|
|
1181
1267
|
spinner.stop();
|
|
1182
1268
|
console.log("");
|
|
1183
1269
|
console.log(chalk5.bold("Horus Status"));
|
|
1184
1270
|
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"));
|
|
1185
|
-
console.log(` ${chalk5.bold("Version:")} ${
|
|
1271
|
+
console.log(` ${chalk5.bold("Version:")} ${CLI_VERSION}`);
|
|
1186
1272
|
console.log(` ${chalk5.bold("Runtime:")} ${runtime.name}`);
|
|
1187
1273
|
console.log(` ${chalk5.bold("Config:")} ~/.horus/config.yaml`);
|
|
1188
1274
|
console.log("");
|
|
@@ -1347,7 +1433,7 @@ import { Command as Command7 } from "commander";
|
|
|
1347
1433
|
import chalk7 from "chalk";
|
|
1348
1434
|
import ora6 from "ora";
|
|
1349
1435
|
import { select as select2, confirm as confirm3 } from "@inquirer/prompts";
|
|
1350
|
-
import { readFileSync as
|
|
1436
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync2, existsSync as existsSync5 } from "fs";
|
|
1351
1437
|
import { join as join5 } from "path";
|
|
1352
1438
|
import { createHash } from "crypto";
|
|
1353
1439
|
import { stringify as stringifyYaml2, parse as parseYaml2 } from "yaml";
|
|
@@ -1357,7 +1443,7 @@ function ensureSnapshotsDir() {
|
|
|
1357
1443
|
}
|
|
1358
1444
|
function composeFileHash() {
|
|
1359
1445
|
if (!existsSync5(COMPOSE_PATH)) return "";
|
|
1360
|
-
const content =
|
|
1446
|
+
const content = readFileSync5(COMPOSE_PATH, "utf-8");
|
|
1361
1447
|
return createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
1362
1448
|
}
|
|
1363
1449
|
async function captureCurrentImages(runtime) {
|
|
@@ -1392,9 +1478,9 @@ function saveSnapshot(images) {
|
|
|
1392
1478
|
}
|
|
1393
1479
|
function listSnapshots() {
|
|
1394
1480
|
if (!existsSync5(SNAPSHOTS_DIR)) return [];
|
|
1395
|
-
return
|
|
1481
|
+
return readdirSync2(SNAPSHOTS_DIR).filter((f) => f.endsWith(".yaml")).sort().reverse().map((f) => {
|
|
1396
1482
|
const file = join5(SNAPSHOTS_DIR, f);
|
|
1397
|
-
const snapshot = parseYaml2(
|
|
1483
|
+
const snapshot = parseYaml2(readFileSync5(file, "utf-8"));
|
|
1398
1484
|
return { file, snapshot };
|
|
1399
1485
|
});
|
|
1400
1486
|
}
|
|
@@ -1625,41 +1711,38 @@ function colorMessage(status, msg) {
|
|
|
1625
1711
|
return chalk8.red(msg);
|
|
1626
1712
|
}
|
|
1627
1713
|
}
|
|
1628
|
-
async function
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
return { status: "pass", label: "Runtime", message: "Docker is running" };
|
|
1632
|
-
} catch {
|
|
1714
|
+
async function checkRuntimeAvailability(preferred) {
|
|
1715
|
+
const order = preferred === "podman" ? ["podman", "docker"] : ["docker", "podman"];
|
|
1716
|
+
for (const rt of order) {
|
|
1633
1717
|
try {
|
|
1634
|
-
execSync2(
|
|
1635
|
-
return { status: "pass", label: "Runtime", message: "Podman is running
|
|
1718
|
+
execSync2(`${rt} info`, { stdio: "ignore" });
|
|
1719
|
+
return { status: "pass", label: "Runtime", message: `${rt === "docker" ? "Docker" : "Podman"} is running` };
|
|
1636
1720
|
} catch {
|
|
1637
|
-
return {
|
|
1638
|
-
status: "fail",
|
|
1639
|
-
label: "Runtime",
|
|
1640
|
-
message: "Docker/Podman is not running",
|
|
1641
|
-
hint: "Start Docker Desktop or Podman Desktop"
|
|
1642
|
-
};
|
|
1643
1721
|
}
|
|
1644
1722
|
}
|
|
1723
|
+
return {
|
|
1724
|
+
status: "fail",
|
|
1725
|
+
label: "Runtime",
|
|
1726
|
+
message: "Docker/Podman is not running",
|
|
1727
|
+
hint: "Start Docker Desktop or Podman Desktop"
|
|
1728
|
+
};
|
|
1645
1729
|
}
|
|
1646
|
-
async function checkCompose() {
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
return { status: "pass", label: "Compose", message: "Compose plugin available" };
|
|
1650
|
-
} catch {
|
|
1730
|
+
async function checkCompose(preferred) {
|
|
1731
|
+
const order = preferred === "podman" ? ["podman", "docker"] : ["docker", "podman"];
|
|
1732
|
+
for (const rt of order) {
|
|
1651
1733
|
try {
|
|
1652
|
-
execSync2(
|
|
1653
|
-
|
|
1734
|
+
execSync2(`${rt} compose version`, { stdio: "ignore" });
|
|
1735
|
+
const label = rt === "podman" ? "Compose plugin available (podman)" : "Compose plugin available";
|
|
1736
|
+
return { status: "pass", label: "Compose", message: label };
|
|
1654
1737
|
} catch {
|
|
1655
|
-
return {
|
|
1656
|
-
status: "fail",
|
|
1657
|
-
label: "Compose",
|
|
1658
|
-
message: "Compose plugin not found",
|
|
1659
|
-
hint: "Install Docker Compose plugin: https://docs.docker.com/compose/install/"
|
|
1660
|
-
};
|
|
1661
1738
|
}
|
|
1662
1739
|
}
|
|
1740
|
+
return {
|
|
1741
|
+
status: "fail",
|
|
1742
|
+
label: "Compose",
|
|
1743
|
+
message: "Compose plugin not found",
|
|
1744
|
+
hint: "Install Docker Compose plugin or podman-compose"
|
|
1745
|
+
};
|
|
1663
1746
|
}
|
|
1664
1747
|
function checkConfig() {
|
|
1665
1748
|
if (configExists()) {
|
|
@@ -1759,8 +1842,8 @@ async function checkServices(runtime) {
|
|
|
1759
1842
|
const results = [];
|
|
1760
1843
|
try {
|
|
1761
1844
|
const psResult = await runtime.compose("ps", "--format", "json");
|
|
1762
|
-
const
|
|
1763
|
-
if (
|
|
1845
|
+
const containers = parseComposeJson(psResult.stdout);
|
|
1846
|
+
if (containers.length === 0) {
|
|
1764
1847
|
return [
|
|
1765
1848
|
{
|
|
1766
1849
|
status: "warn",
|
|
@@ -1770,17 +1853,10 @@ async function checkServices(runtime) {
|
|
|
1770
1853
|
}
|
|
1771
1854
|
];
|
|
1772
1855
|
}
|
|
1773
|
-
const containers = lines.map((l) => {
|
|
1774
|
-
try {
|
|
1775
|
-
return JSON.parse(l);
|
|
1776
|
-
} catch {
|
|
1777
|
-
return null;
|
|
1778
|
-
}
|
|
1779
|
-
}).filter((c) => c !== null);
|
|
1780
1856
|
for (const c of containers) {
|
|
1781
|
-
const name = c.Service ?? "unknown";
|
|
1857
|
+
const name = c.Service ?? c.Name ?? "unknown";
|
|
1782
1858
|
const health = (c.Health || c.State || "unknown").toLowerCase();
|
|
1783
|
-
if (health === "healthy" || health === "running") {
|
|
1859
|
+
if (health === "healthy" || health === "running" || health === "up") {
|
|
1784
1860
|
results.push({ status: "pass", label: `Service: ${name}`, message: `${name} is ${health}` });
|
|
1785
1861
|
} else if (health === "starting") {
|
|
1786
1862
|
results.push({
|
|
@@ -1813,11 +1889,11 @@ var doctorCommand = new Command8("doctor").description("Diagnose common Horus is
|
|
|
1813
1889
|
console.log(chalk8.bold("Horus Doctor"));
|
|
1814
1890
|
console.log(chalk8.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"));
|
|
1815
1891
|
const allResults = [];
|
|
1816
|
-
|
|
1817
|
-
allResults.push(await
|
|
1892
|
+
const config = configExists() ? loadConfig() : null;
|
|
1893
|
+
allResults.push(await checkRuntimeAvailability(config?.runtime));
|
|
1894
|
+
allResults.push(await checkCompose(config?.runtime));
|
|
1818
1895
|
allResults.push(checkConfig());
|
|
1819
1896
|
allResults.push(checkComposeFile());
|
|
1820
|
-
const config = configExists() ? loadConfig() : null;
|
|
1821
1897
|
const ports = config?.ports ?? DEFAULT_PORTS;
|
|
1822
1898
|
const dataDir = config?.data_dir ?? join6(process.env.HOME ?? "~", ".horus", "data");
|
|
1823
1899
|
allResults.push(checkPort(ports.anvil, "Anvil"));
|
|
@@ -1874,7 +1950,7 @@ import { Command as Command9 } from "commander";
|
|
|
1874
1950
|
import chalk9 from "chalk";
|
|
1875
1951
|
import ora7 from "ora";
|
|
1876
1952
|
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
1877
|
-
import { mkdirSync as mkdirSync5, statSync, existsSync as existsSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1953
|
+
import { mkdirSync as mkdirSync5, statSync as statSync2, existsSync as existsSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1878
1954
|
import { join as join7, basename } from "path";
|
|
1879
1955
|
import { execSync as execSync3 } from "child_process";
|
|
1880
1956
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
@@ -1942,7 +2018,7 @@ async function createBackup(yes) {
|
|
|
1942
2018
|
}
|
|
1943
2019
|
let sizeBytes = 0;
|
|
1944
2020
|
try {
|
|
1945
|
-
sizeBytes =
|
|
2021
|
+
sizeBytes = statSync2(tarFile).size;
|
|
1946
2022
|
} catch {
|
|
1947
2023
|
}
|
|
1948
2024
|
const meta = {
|