@arkhera30/cli 0.1.11 → 0.1.13

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.
Files changed (2) hide show
  1. package/dist/index.js +184 -102
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,26 +3,41 @@
3
3
  // src/index.ts
4
4
  import { Command as Command10 } from "commander";
5
5
  import chalk10 from "chalk";
6
- import { createRequire } from "module";
7
6
 
8
7
  // src/commands/setup.ts
9
8
  import { Command as Command2 } from "commander";
10
9
  import chalk2 from "chalk";
11
10
  import ora2 from "ora";
12
11
  import { execSync } from "child_process";
13
- import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
12
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
14
13
  import { join as join4 } from "path";
15
14
  import { input, confirm, number, select, password } from "@inquirer/prompts";
16
15
 
17
16
  // src/lib/config.ts
18
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
19
- import { resolve } from "path";
17
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2, readdirSync, statSync } from "fs";
18
+ import { resolve, join as pathJoin, relative } from "path";
20
19
  import { homedir as homedir2 } from "os";
21
20
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
22
21
 
23
22
  // src/lib/constants.ts
24
23
  import { homedir } from "os";
25
- import { join } from "path";
24
+ import { join, dirname } from "path";
25
+ import { readFileSync, existsSync } from "fs";
26
+ import { fileURLToPath } from "url";
27
+ function findPackageJson() {
28
+ let dir = dirname(fileURLToPath(import.meta.url));
29
+ while (dir !== dirname(dir)) {
30
+ const candidate = join(dir, "package.json");
31
+ if (existsSync(candidate)) {
32
+ const pkg2 = JSON.parse(readFileSync(candidate, "utf-8"));
33
+ if (pkg2.name === "@arkhera30/cli") return candidate;
34
+ }
35
+ dir = dirname(dir);
36
+ }
37
+ throw new Error("Could not find @arkhera30/cli package.json");
38
+ }
39
+ var pkg = JSON.parse(readFileSync(findPackageJson(), "utf-8"));
40
+ var CLI_VERSION = pkg.version;
26
41
  var HORUS_DIR = join(homedir(), ".horus");
27
42
  var CONFIG_PATH = join(HORUS_DIR, "config.yaml");
28
43
  var ENV_PATH = join(HORUS_DIR, ".env");
@@ -66,13 +81,13 @@ function ensureHorusDir() {
66
81
  mkdirSync(HORUS_DIR, { recursive: true });
67
82
  }
68
83
  function configExists() {
69
- return existsSync(CONFIG_PATH);
84
+ return existsSync2(CONFIG_PATH);
70
85
  }
71
86
  function loadConfig() {
72
- if (!existsSync(CONFIG_PATH)) {
87
+ if (!existsSync2(CONFIG_PATH)) {
73
88
  return defaultConfig();
74
89
  }
75
- const raw = readFileSync(CONFIG_PATH, "utf-8");
90
+ const raw = readFileSync2(CONFIG_PATH, "utf-8");
76
91
  const parsed = parseYaml(raw);
77
92
  const defaults = defaultConfig();
78
93
  return {
@@ -107,12 +122,54 @@ function resolvePath(p) {
107
122
  }
108
123
  return resolve(p);
109
124
  }
125
+ function discoverRepoDirs(rootDir, maxDepth = 4) {
126
+ const repoDirs = /* @__PURE__ */ new Set();
127
+ function walk(dir, depth) {
128
+ if (depth > maxDepth) return;
129
+ let entries;
130
+ try {
131
+ entries = readdirSync(dir);
132
+ } catch {
133
+ return;
134
+ }
135
+ for (const entry of entries) {
136
+ if (entry === "node_modules" || entry === ".git") continue;
137
+ const full = pathJoin(dir, entry);
138
+ try {
139
+ if (!statSync(full).isDirectory()) continue;
140
+ } catch {
141
+ continue;
142
+ }
143
+ if (existsSync2(pathJoin(full, ".git"))) {
144
+ repoDirs.add(dir);
145
+ }
146
+ walk(full, depth + 1);
147
+ }
148
+ }
149
+ if (existsSync2(rootDir)) {
150
+ walk(rootDir, 0);
151
+ }
152
+ return [...repoDirs];
153
+ }
110
154
  function generateEnv(config) {
111
155
  const dataDir = resolvePath(config.data_dir);
112
156
  const hostReposPath = config.host_repos_path ? resolvePath(config.host_repos_path) : "";
113
157
  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(":");
158
+ let forgeScanPaths;
159
+ if (hostReposPath) {
160
+ const discoveredDirs = discoverRepoDirs(hostReposPath);
161
+ const containerPaths = discoveredDirs.map((dir) => {
162
+ const rel = relative(hostReposPath, dir);
163
+ return rel ? `${baseScanPath}/${rel}` : baseScanPath;
164
+ });
165
+ const allPaths = [baseScanPath, ...containerPaths];
166
+ const extraScanPaths = (config.host_repos_extra_scan_dirs ?? []).map((d) => d.trim()).filter(Boolean).map((d) => `${baseScanPath}/${d}`);
167
+ const uniquePaths = [.../* @__PURE__ */ new Set([...allPaths, ...extraScanPaths])];
168
+ forgeScanPaths = uniquePaths.join(":");
169
+ } else {
170
+ const extraScanPaths = (config.host_repos_extra_scan_dirs ?? []).map((d) => d.trim()).filter(Boolean).map((d) => `${baseScanPath}/${d}`);
171
+ forgeScanPaths = [baseScanPath, ...extraScanPaths].join(":");
172
+ }
116
173
  const lines = [
117
174
  "# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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
175
  "# Horus \u2014 Generated .env file",
@@ -299,6 +356,9 @@ function createRuntime(name) {
299
356
  const result = await execa(bin, ["inspect", "--format", format, container], {
300
357
  reject: false
301
358
  });
359
+ if (result.exitCode !== 0) {
360
+ throw new Error(`inspect failed: ${result.stderr}`);
361
+ }
302
362
  return result.stdout?.toString().trim() ?? "";
303
363
  },
304
364
  async isRunning() {
@@ -315,6 +375,24 @@ function createRuntime(name) {
315
375
  }
316
376
  };
317
377
  }
378
+ function parseComposeJson(output) {
379
+ const trimmed = output.trim();
380
+ if (!trimmed) return [];
381
+ if (trimmed.startsWith("[")) {
382
+ try {
383
+ const parsed = JSON.parse(trimmed);
384
+ if (Array.isArray(parsed)) return parsed;
385
+ } catch {
386
+ }
387
+ }
388
+ return trimmed.split("\n").filter((line) => line.trim()).map((line) => {
389
+ try {
390
+ return JSON.parse(line);
391
+ } catch {
392
+ return null;
393
+ }
394
+ }).filter((item) => item !== null);
395
+ }
318
396
  async function checkRuntime(name) {
319
397
  return tryCommand(name, ["compose", "version"]);
320
398
  }
@@ -358,14 +436,22 @@ async function composeStreaming(runtime, args) {
358
436
 
359
437
  // src/lib/health.ts
360
438
  async function checkContainerHealth(runtime, service) {
361
- const containerName = `horus-${service}-1`;
362
- try {
363
- const status = await runtime.inspect(containerName, "{{.State.Health.Status}}");
364
- const mappedStatus = mapStatus(status);
365
- return { name: service, status: mappedStatus };
366
- } catch {
367
- return { name: service, status: "stopped" };
439
+ const candidates = [`horus-${service}-1`, `horus_${service}_1`];
440
+ for (const containerName of candidates) {
441
+ try {
442
+ const healthStatus = await runtime.inspect(containerName, "{{.State.Health.Status}}");
443
+ if (healthStatus && !healthStatus.includes("<nil>") && healthStatus.trim() !== "") {
444
+ return { name: service, status: mapStatus(healthStatus) };
445
+ }
446
+ const stateStatus = await runtime.inspect(containerName, "{{.State.Status}}");
447
+ if (stateStatus && stateStatus.trim() !== "") {
448
+ return { name: service, status: mapStateStatus(stateStatus) };
449
+ }
450
+ } catch {
451
+ continue;
452
+ }
368
453
  }
454
+ return { name: service, status: "stopped" };
369
455
  }
370
456
  function mapStatus(raw) {
371
457
  switch (raw.trim().toLowerCase()) {
@@ -379,13 +465,28 @@ function mapStatus(raw) {
379
465
  return "unknown";
380
466
  }
381
467
  }
468
+ function mapStateStatus(raw) {
469
+ switch (raw.trim().toLowerCase()) {
470
+ case "running":
471
+ return "healthy";
472
+ case "created":
473
+ case "restarting":
474
+ return "starting";
475
+ case "exited":
476
+ case "dead":
477
+ case "removing":
478
+ return "unhealthy";
479
+ default:
480
+ return "unknown";
481
+ }
482
+ }
382
483
  async function checkAllHealth(runtime) {
383
484
  const results = await Promise.all(
384
485
  SERVICES.map((service) => checkContainerHealth(runtime, service))
385
486
  );
386
487
  return results;
387
488
  }
388
- async function pollUntilHealthy(runtime, onUpdate, timeoutMs = 6e5, intervalMs = 5e3) {
489
+ async function pollUntilHealthy(runtime, onUpdate, timeoutMs = 3e5, intervalMs = 5e3) {
389
490
  const startTime = Date.now();
390
491
  while (true) {
391
492
  const states = await checkAllHealth(runtime);
@@ -401,7 +502,7 @@ async function pollUntilHealthy(runtime, onUpdate, timeoutMs = 6e5, intervalMs =
401
502
  const unhealthyServices = states.filter((s) => s.status === "unhealthy").map((s) => s.name).join(", ");
402
503
  throw new Error(
403
504
  `Services failed health check: ${unhealthyServices}
404
- Run 'docker compose logs <service>' from ~/.horus/ to investigate.`
505
+ Run '${runtime.name} compose logs <service>' from ~/.horus/ to investigate.`
405
506
  );
406
507
  }
407
508
  const elapsed = Date.now() - startTime;
@@ -409,7 +510,7 @@ Run 'docker compose logs <service>' from ~/.horus/ to investigate.`
409
510
  const notReady = states.filter((s) => s.status !== "healthy").map((s) => `${s.name} (${s.status})`).join(", ");
410
511
  throw new Error(
411
512
  `Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for services: ${notReady}
412
- Run 'docker compose logs' from ~/.horus/ to investigate.`
513
+ Run '${runtime.name} compose logs' from ~/.horus/ to investigate.`
413
514
  );
414
515
  }
415
516
  await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
@@ -417,18 +518,18 @@ Run 'docker compose logs' from ~/.horus/ to investigate.`
417
518
  }
418
519
 
419
520
  // src/lib/compose.ts
420
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
421
- import { join as join2, dirname } from "path";
422
- import { fileURLToPath } from "url";
423
- var __filename = fileURLToPath(import.meta.url);
424
- var __dirname = dirname(__filename);
521
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
522
+ import { join as join2, dirname as dirname2 } from "path";
523
+ import { fileURLToPath as fileURLToPath2 } from "url";
524
+ var __filename = fileURLToPath2(import.meta.url);
525
+ var __dirname = dirname2(__filename);
425
526
  function getBundledComposePath() {
426
527
  const candidates = [
427
528
  join2(__dirname, "..", "..", "compose", "docker-compose.yml"),
428
529
  join2(__dirname, "..", "compose", "docker-compose.yml")
429
530
  ];
430
531
  for (const candidate of candidates) {
431
- if (existsSync2(candidate)) {
532
+ if (existsSync3(candidate)) {
432
533
  return candidate;
433
534
  }
434
535
  }
@@ -444,12 +545,12 @@ function applyPodmanUserOverride(compose) {
444
545
  );
445
546
  }
446
547
  function composeFileExists() {
447
- return existsSync2(COMPOSE_PATH);
548
+ return existsSync3(COMPOSE_PATH);
448
549
  }
449
550
  function installComposeFile(runtime) {
450
551
  ensureHorusDir();
451
552
  const bundledPath = getBundledComposePath();
452
- let content = readFileSync2(bundledPath, "utf-8");
553
+ let content = readFileSync3(bundledPath, "utf-8");
453
554
  if (runtime === "podman") {
454
555
  content = applyPodmanUserOverride(content);
455
556
  }
@@ -461,7 +562,7 @@ import { Command } from "commander";
461
562
  import chalk from "chalk";
462
563
  import ora from "ora";
463
564
  import { checkbox } from "@inquirer/prompts";
464
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
565
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
465
566
  import { join as join3 } from "path";
466
567
  import { homedir as homedir3 } from "os";
467
568
  import { execa as execa2 } from "execa";
@@ -469,16 +570,16 @@ function detectInstalledClients() {
469
570
  const detected = [];
470
571
  const home = homedir3();
471
572
  const claudeDesktopDir = join3(home, "Library", "Application Support", "Claude");
472
- if (existsSync3(claudeDesktopDir)) {
573
+ if (existsSync4(claudeDesktopDir)) {
473
574
  detected.push("claude-desktop");
474
575
  }
475
576
  const claudeCodeDir = join3(home, ".claude");
476
- if (existsSync3(claudeCodeDir)) {
577
+ if (existsSync4(claudeCodeDir)) {
477
578
  detected.push("claude-code");
478
579
  }
479
580
  const cursorDir = join3(home, ".cursor");
480
581
  const cursorAppDir = join3(home, "Library", "Application Support", "Cursor");
481
- if (existsSync3(cursorDir) || existsSync3(cursorAppDir)) {
582
+ if (existsSync4(cursorDir) || existsSync4(cursorAppDir)) {
482
583
  detected.push("cursor");
483
584
  }
484
585
  return detected;
@@ -496,9 +597,9 @@ function getConfigPath(target) {
496
597
  }
497
598
  function mergeAndWriteConfig(configPath, mcpServers) {
498
599
  let existing = {};
499
- if (existsSync3(configPath)) {
600
+ if (existsSync4(configPath)) {
500
601
  try {
501
- const raw = readFileSync3(configPath, "utf-8");
602
+ const raw = readFileSync4(configPath, "utf-8");
502
603
  existing = JSON.parse(raw);
503
604
  } catch {
504
605
  existing = {};
@@ -821,11 +922,7 @@ var setupCommand = new Command2("setup").description("Interactive first-run setu
821
922
  message: "Host repos path (for Forge repo scanning, leave empty to skip):",
822
923
  default: ""
823
924
  });
824
- const extra_scan_dirs_raw = await input({
825
- message: "Extra subdirectories to scan within repos path (comma-separated, e.g. ArjunKhera \u2014 leave empty to skip):",
826
- default: ""
827
- });
828
- const host_repos_extra_scan_dirs = extra_scan_dirs_raw.split(",").map((d) => d.trim()).filter(Boolean);
925
+ const host_repos_extra_scan_dirs = [];
829
926
  const customize_ports = await confirm({
830
927
  message: "Customize port assignments?",
831
928
  default: false
@@ -939,7 +1036,7 @@ ${example("forge-registry")}
939
1036
  console.error(error.message);
940
1037
  process.exit(1);
941
1038
  }
942
- const dataDir = config.data_dir.startsWith("~") ? join4(process.env.HOME || "", config.data_dir.slice(1)) : config.data_dir;
1039
+ const dataDir = resolvePath(config.data_dir);
943
1040
  const reposToClone = [
944
1041
  { url: config.repos.anvil_notes, dest: join4(dataDir, "notes"), label: "Anvil notes" },
945
1042
  { url: config.repos.vault_knowledge, dest: join4(dataDir, "knowledge-base"), label: "Vault knowledge-base" },
@@ -951,7 +1048,7 @@ ${example("forge-registry")}
951
1048
  mkdirSync3(dataDir, { recursive: true });
952
1049
  for (const repo of reposToClone) {
953
1050
  const spinner = ora2(`Cloning ${repo.label}...`).start();
954
- if (existsSync4(join4(repo.dest, ".git"))) {
1051
+ if (existsSync5(join4(repo.dest, ".git"))) {
955
1052
  spinner.succeed(`${repo.label} already cloned`);
956
1053
  continue;
957
1054
  }
@@ -1173,17 +1270,14 @@ var statusCommand = new Command5("status").description("Show status of Horus ser
1173
1270
  let containers = [];
1174
1271
  try {
1175
1272
  const result = await runtime.compose("ps", "--format", "json");
1176
- const output = result.stdout.trim();
1177
- if (output) {
1178
- containers = output.split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
1179
- }
1273
+ containers = parseComposeJson(result.stdout);
1180
1274
  } catch {
1181
1275
  }
1182
1276
  spinner.stop();
1183
1277
  console.log("");
1184
1278
  console.log(chalk5.bold("Horus Status"));
1185
1279
  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"));
1186
- console.log(` ${chalk5.bold("Version:")} ${config.version}`);
1280
+ console.log(` ${chalk5.bold("Version:")} ${CLI_VERSION}`);
1187
1281
  console.log(` ${chalk5.bold("Runtime:")} ${runtime.name}`);
1188
1282
  console.log(` ${chalk5.bold("Config:")} ~/.horus/config.yaml`);
1189
1283
  console.log("");
@@ -1348,7 +1442,7 @@ import { Command as Command7 } from "commander";
1348
1442
  import chalk7 from "chalk";
1349
1443
  import ora6 from "ora";
1350
1444
  import { select as select2, confirm as confirm3 } from "@inquirer/prompts";
1351
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync, existsSync as existsSync5 } from "fs";
1445
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync2, existsSync as existsSync6 } from "fs";
1352
1446
  import { join as join5 } from "path";
1353
1447
  import { createHash } from "crypto";
1354
1448
  import { stringify as stringifyYaml2, parse as parseYaml2 } from "yaml";
@@ -1357,8 +1451,8 @@ function ensureSnapshotsDir() {
1357
1451
  mkdirSync4(SNAPSHOTS_DIR, { recursive: true });
1358
1452
  }
1359
1453
  function composeFileHash() {
1360
- if (!existsSync5(COMPOSE_PATH)) return "";
1361
- const content = readFileSync4(COMPOSE_PATH, "utf-8");
1454
+ if (!existsSync6(COMPOSE_PATH)) return "";
1455
+ const content = readFileSync5(COMPOSE_PATH, "utf-8");
1362
1456
  return createHash("sha256").update(content).digest("hex").slice(0, 12);
1363
1457
  }
1364
1458
  async function captureCurrentImages(runtime) {
@@ -1392,10 +1486,10 @@ function saveSnapshot(images) {
1392
1486
  return filePath;
1393
1487
  }
1394
1488
  function listSnapshots() {
1395
- if (!existsSync5(SNAPSHOTS_DIR)) return [];
1396
- return readdirSync(SNAPSHOTS_DIR).filter((f) => f.endsWith(".yaml")).sort().reverse().map((f) => {
1489
+ if (!existsSync6(SNAPSHOTS_DIR)) return [];
1490
+ return readdirSync2(SNAPSHOTS_DIR).filter((f) => f.endsWith(".yaml")).sort().reverse().map((f) => {
1397
1491
  const file = join5(SNAPSHOTS_DIR, f);
1398
- const snapshot = parseYaml2(readFileSync4(file, "utf-8"));
1492
+ const snapshot = parseYaml2(readFileSync5(file, "utf-8"));
1399
1493
  return { file, snapshot };
1400
1494
  });
1401
1495
  }
@@ -1604,7 +1698,7 @@ var updateCommand = new Command7("update").description("Update Horus to the late
1604
1698
  import { Command as Command8 } from "commander";
1605
1699
  import chalk8 from "chalk";
1606
1700
  import { execSync as execSync2 } from "child_process";
1607
- import { existsSync as existsSync6, accessSync, statfsSync, constants } from "fs";
1701
+ import { existsSync as existsSync7, accessSync, statfsSync, constants } from "fs";
1608
1702
  import { join as join6 } from "path";
1609
1703
  function symbol(status) {
1610
1704
  switch (status) {
@@ -1626,41 +1720,38 @@ function colorMessage(status, msg) {
1626
1720
  return chalk8.red(msg);
1627
1721
  }
1628
1722
  }
1629
- async function checkRuntime2() {
1630
- try {
1631
- execSync2("docker info", { stdio: "ignore" });
1632
- return { status: "pass", label: "Runtime", message: "Docker is running" };
1633
- } catch {
1723
+ async function checkRuntimeAvailability(preferred) {
1724
+ const order = preferred === "podman" ? ["podman", "docker"] : ["docker", "podman"];
1725
+ for (const rt of order) {
1634
1726
  try {
1635
- execSync2("podman info", { stdio: "ignore" });
1636
- return { status: "pass", label: "Runtime", message: "Podman is running" };
1727
+ execSync2(`${rt} info`, { stdio: "ignore" });
1728
+ return { status: "pass", label: "Runtime", message: `${rt === "docker" ? "Docker" : "Podman"} is running` };
1637
1729
  } catch {
1638
- return {
1639
- status: "fail",
1640
- label: "Runtime",
1641
- message: "Docker/Podman is not running",
1642
- hint: "Start Docker Desktop or Podman Desktop"
1643
- };
1644
1730
  }
1645
1731
  }
1732
+ return {
1733
+ status: "fail",
1734
+ label: "Runtime",
1735
+ message: "Docker/Podman is not running",
1736
+ hint: "Start Docker Desktop or Podman Desktop"
1737
+ };
1646
1738
  }
1647
- async function checkCompose() {
1648
- try {
1649
- execSync2("docker compose version", { stdio: "ignore" });
1650
- return { status: "pass", label: "Compose", message: "Compose plugin available" };
1651
- } catch {
1739
+ async function checkCompose(preferred) {
1740
+ const order = preferred === "podman" ? ["podman", "docker"] : ["docker", "podman"];
1741
+ for (const rt of order) {
1652
1742
  try {
1653
- execSync2("podman compose version", { stdio: "ignore" });
1654
- return { status: "pass", label: "Compose", message: "Compose plugin available (podman)" };
1743
+ execSync2(`${rt} compose version`, { stdio: "ignore" });
1744
+ const label = rt === "podman" ? "Compose plugin available (podman)" : "Compose plugin available";
1745
+ return { status: "pass", label: "Compose", message: label };
1655
1746
  } catch {
1656
- return {
1657
- status: "fail",
1658
- label: "Compose",
1659
- message: "Compose plugin not found",
1660
- hint: "Install Docker Compose plugin: https://docs.docker.com/compose/install/"
1661
- };
1662
1747
  }
1663
1748
  }
1749
+ return {
1750
+ status: "fail",
1751
+ label: "Compose",
1752
+ message: "Compose plugin not found",
1753
+ hint: "Install Docker Compose plugin or podman-compose"
1754
+ };
1664
1755
  }
1665
1756
  function checkConfig() {
1666
1757
  if (configExists()) {
@@ -1674,7 +1765,7 @@ function checkConfig() {
1674
1765
  };
1675
1766
  }
1676
1767
  function checkComposeFile() {
1677
- if (existsSync6(COMPOSE_PATH)) {
1768
+ if (existsSync7(COMPOSE_PATH)) {
1678
1769
  return { status: "pass", label: "Compose file", message: "Compose file installed (~/.horus/docker-compose.yml)" };
1679
1770
  }
1680
1771
  return {
@@ -1715,7 +1806,7 @@ function checkPort(port, serviceName) {
1715
1806
  }
1716
1807
  }
1717
1808
  function checkDataDir(dataDir) {
1718
- if (!existsSync6(dataDir)) {
1809
+ if (!existsSync7(dataDir)) {
1719
1810
  return {
1720
1811
  status: "warn",
1721
1812
  label: "Data directory",
@@ -1736,7 +1827,7 @@ function checkDataDir(dataDir) {
1736
1827
  }
1737
1828
  }
1738
1829
  function checkDiskSpace(dataDir) {
1739
- const checkDir = existsSync6(dataDir) ? dataDir : join6(dataDir, "..");
1830
+ const checkDir = existsSync7(dataDir) ? dataDir : join6(dataDir, "..");
1740
1831
  try {
1741
1832
  const stats = statfsSync(checkDir);
1742
1833
  const freeBytes = stats.bfree * stats.bsize;
@@ -1760,8 +1851,8 @@ async function checkServices(runtime) {
1760
1851
  const results = [];
1761
1852
  try {
1762
1853
  const psResult = await runtime.compose("ps", "--format", "json");
1763
- const lines = psResult.stdout.trim().split("\n").filter(Boolean);
1764
- if (lines.length === 0) {
1854
+ const containers = parseComposeJson(psResult.stdout);
1855
+ if (containers.length === 0) {
1765
1856
  return [
1766
1857
  {
1767
1858
  status: "warn",
@@ -1771,17 +1862,10 @@ async function checkServices(runtime) {
1771
1862
  }
1772
1863
  ];
1773
1864
  }
1774
- const containers = lines.map((l) => {
1775
- try {
1776
- return JSON.parse(l);
1777
- } catch {
1778
- return null;
1779
- }
1780
- }).filter((c) => c !== null);
1781
1865
  for (const c of containers) {
1782
- const name = c.Service ?? "unknown";
1866
+ const name = c.Service ?? c.Name ?? "unknown";
1783
1867
  const health = (c.Health || c.State || "unknown").toLowerCase();
1784
- if (health === "healthy" || health === "running") {
1868
+ if (health === "healthy" || health === "running" || health === "up") {
1785
1869
  results.push({ status: "pass", label: `Service: ${name}`, message: `${name} is ${health}` });
1786
1870
  } else if (health === "starting") {
1787
1871
  results.push({
@@ -1814,11 +1898,11 @@ var doctorCommand = new Command8("doctor").description("Diagnose common Horus is
1814
1898
  console.log(chalk8.bold("Horus Doctor"));
1815
1899
  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"));
1816
1900
  const allResults = [];
1817
- allResults.push(await checkRuntime2());
1818
- allResults.push(await checkCompose());
1901
+ const config = configExists() ? loadConfig() : null;
1902
+ allResults.push(await checkRuntimeAvailability(config?.runtime));
1903
+ allResults.push(await checkCompose(config?.runtime));
1819
1904
  allResults.push(checkConfig());
1820
1905
  allResults.push(checkComposeFile());
1821
- const config = configExists() ? loadConfig() : null;
1822
1906
  const ports = config?.ports ?? DEFAULT_PORTS;
1823
1907
  const dataDir = config?.data_dir ?? join6(process.env.HOME ?? "~", ".horus", "data");
1824
1908
  allResults.push(checkPort(ports.anvil, "Anvil"));
@@ -1875,7 +1959,7 @@ import { Command as Command9 } from "commander";
1875
1959
  import chalk9 from "chalk";
1876
1960
  import ora7 from "ora";
1877
1961
  import { confirm as confirm4 } from "@inquirer/prompts";
1878
- import { mkdirSync as mkdirSync5, statSync, existsSync as existsSync7, writeFileSync as writeFileSync5 } from "fs";
1962
+ import { mkdirSync as mkdirSync5, statSync as statSync2, existsSync as existsSync8, writeFileSync as writeFileSync5 } from "fs";
1879
1963
  import { join as join7, basename } from "path";
1880
1964
  import { execSync as execSync3 } from "child_process";
1881
1965
  import { stringify as stringifyYaml3 } from "yaml";
@@ -1943,7 +2027,7 @@ async function createBackup(yes) {
1943
2027
  }
1944
2028
  let sizeBytes = 0;
1945
2029
  try {
1946
- sizeBytes = statSync(tarFile).size;
2030
+ sizeBytes = statSync2(tarFile).size;
1947
2031
  } catch {
1948
2032
  }
1949
2033
  const meta = {
@@ -1976,7 +2060,7 @@ async function restoreBackup(file, yes) {
1976
2060
  console.log(chalk9.bold("Horus Restore"));
1977
2061
  console.log(chalk9.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1978
2062
  console.log("");
1979
- if (!existsSync7(file)) {
2063
+ if (!existsSync8(file)) {
1980
2064
  console.log(chalk9.red(`Backup file not found: ${file}`));
1981
2065
  process.exit(1);
1982
2066
  }
@@ -2065,10 +2149,8 @@ backupCommand.command("restore <file>").description("Restore Horus data from a b
2065
2149
  });
2066
2150
 
2067
2151
  // src/index.ts
2068
- var require2 = createRequire(import.meta.url);
2069
- var { version } = require2("../package.json");
2070
2152
  var program = new Command10();
2071
- program.name("horus").description("CLI for managing the Horus Docker Compose stack").version(version);
2153
+ program.name("horus").description("CLI for managing the Horus Docker Compose stack").version(CLI_VERSION);
2072
2154
  program.addCommand(setupCommand);
2073
2155
  program.addCommand(upCommand);
2074
2156
  program.addCommand(downCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkhera30/cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "CLI for managing the Horus AI development stack",
5
5
  "type": "module",
6
6
  "bin": {