@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.
- package/dist/index.js +184 -102
- 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
|
|
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
|
|
84
|
+
return existsSync2(CONFIG_PATH);
|
|
70
85
|
}
|
|
71
86
|
function loadConfig() {
|
|
72
|
-
if (!
|
|
87
|
+
if (!existsSync2(CONFIG_PATH)) {
|
|
73
88
|
return defaultConfig();
|
|
74
89
|
}
|
|
75
|
-
const raw =
|
|
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
|
-
|
|
115
|
-
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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 =
|
|
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 '
|
|
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 '
|
|
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
|
|
421
|
-
import { join as join2, dirname } from "path";
|
|
422
|
-
import { fileURLToPath } from "url";
|
|
423
|
-
var __filename =
|
|
424
|
-
var __dirname =
|
|
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 (
|
|
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
|
|
548
|
+
return existsSync3(COMPOSE_PATH);
|
|
448
549
|
}
|
|
449
550
|
function installComposeFile(runtime) {
|
|
450
551
|
ensureHorusDir();
|
|
451
552
|
const bundledPath = getBundledComposePath();
|
|
452
|
-
let content =
|
|
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
|
|
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 (
|
|
573
|
+
if (existsSync4(claudeDesktopDir)) {
|
|
473
574
|
detected.push("claude-desktop");
|
|
474
575
|
}
|
|
475
576
|
const claudeCodeDir = join3(home, ".claude");
|
|
476
|
-
if (
|
|
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 (
|
|
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 (
|
|
600
|
+
if (existsSync4(configPath)) {
|
|
500
601
|
try {
|
|
501
|
-
const raw =
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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:")} ${
|
|
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
|
|
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 (!
|
|
1361
|
-
const content =
|
|
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 (!
|
|
1396
|
-
return
|
|
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(
|
|
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
|
|
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
|
|
1630
|
-
|
|
1631
|
-
|
|
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(
|
|
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
|
-
|
|
1649
|
-
|
|
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(
|
|
1654
|
-
|
|
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 (
|
|
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 (!
|
|
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 =
|
|
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
|
|
1764
|
-
if (
|
|
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
|
-
|
|
1818
|
-
allResults.push(await
|
|
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
|
|
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 =
|
|
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 (!
|
|
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(
|
|
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);
|