@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.
@@ -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 = readFileSync(CONFIG_PATH, "utf-8");
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
- const extraScanPaths = (config.host_repos_extra_scan_dirs ?? []).map((d) => d.trim()).filter(Boolean).map((d) => `${baseScanPath}/${d}`);
115
- const forgeScanPaths = [baseScanPath, ...extraScanPaths].join(":");
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 containerName = `horus-${service}-1`;
361
- try {
362
- const status = await runtime.inspect(containerName, "{{.State.Health.Status}}");
363
- const mappedStatus = mapStatus(status);
364
- return { name: service, status: mappedStatus };
365
- } catch {
366
- return { name: service, status: "stopped" };
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 = 6e5, intervalMs = 5e3) {
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 'docker compose logs <service>' from ~/.horus/ to investigate.`
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 'docker compose logs' from ~/.horus/ to investigate.`
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 readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
420
- import { join as join2, dirname } from "path";
421
- import { fileURLToPath } from "url";
422
- var __filename = fileURLToPath(import.meta.url);
423
- var __dirname = dirname(__filename);
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 = readFileSync2(bundledPath, "utf-8");
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 readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
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 = readFileSync3(configPath, "utf-8");
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 extra_scan_dirs_raw = await input({
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 = config.data_dir.startsWith("~") ? join4(process.env.HOME || "", config.data_dir.slice(1)) : config.data_dir;
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
- const output = result.stdout.trim();
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:")} ${config.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 readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync, existsSync as existsSync5 } from "fs";
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 = readFileSync4(COMPOSE_PATH, "utf-8");
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 readdirSync(SNAPSHOTS_DIR).filter((f) => f.endsWith(".yaml")).sort().reverse().map((f) => {
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(readFileSync4(file, "utf-8"));
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 checkRuntime2() {
1629
- try {
1630
- execSync2("docker info", { stdio: "ignore" });
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("podman info", { stdio: "ignore" });
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
- try {
1648
- execSync2("docker compose version", { stdio: "ignore" });
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("podman compose version", { stdio: "ignore" });
1653
- return { status: "pass", label: "Compose", message: "Compose plugin available (podman)" };
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 lines = psResult.stdout.trim().split("\n").filter(Boolean);
1763
- if (lines.length === 0) {
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
- allResults.push(await checkRuntime2());
1817
- allResults.push(await checkCompose());
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 = statSync(tarFile).size;
2021
+ sizeBytes = statSync2(tarFile).size;
1946
2022
  } catch {
1947
2023
  }
1948
2024
  const meta = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkhera30/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "CLI for managing the Horus AI development stack",
5
5
  "type": "module",
6
6
  "bin": {