@arkhera30/cli 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +494 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command10 } from "commander";
5
- import chalk10 from "chalk";
4
+ import { Command as Command11 } from "commander";
5
+ import chalk11 from "chalk";
6
6
 
7
7
  // src/commands/setup.ts
8
8
  import { Command as Command2 } from "commander";
@@ -570,7 +570,7 @@ Run '${runtime.name} compose logs <service>' from ~/Horus/ to investigate.`
570
570
  Run '${runtime.name} compose logs' from ~/Horus/ to investigate.`
571
571
  );
572
572
  }
573
- await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
573
+ await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
574
574
  }
575
575
  }
576
576
 
@@ -1650,12 +1650,16 @@ var upCommand = new Command3("up").description("Start the Horus stack").option("
1650
1650
  process.exit(1);
1651
1651
  }
1652
1652
  if (opts.pull) {
1653
- const pullSpinner = ora3("Pulling latest images...").start();
1653
+ console.log("");
1654
+ console.log(chalk3.bold("Pulling latest images..."));
1654
1655
  try {
1655
- await composeStreaming(runtime, runtime.name === "podman" ? ["pull"] : ["pull", "--ignore-pull-failures"]);
1656
- pullSpinner.succeed("Images up to date");
1656
+ await composeStreaming(runtime, ["pull"]);
1657
+ console.log("");
1658
+ console.log(chalk3.green("\u2713 Pull complete"));
1657
1659
  } catch {
1658
- pullSpinner.warn("Could not pull images, using cached");
1660
+ console.log("");
1661
+ console.log(chalk3.yellow("\u26A0 Warning: failed to pull one or more images \u2014 using cached versions."));
1662
+ console.log(chalk3.dim(" Run `docker compose pull` to see which services failed."));
1659
1663
  }
1660
1664
  }
1661
1665
  console.log("");
@@ -2642,8 +2646,487 @@ backupCommand.command("restore <file>").description("Restore Horus data from a b
2642
2646
  await restoreBackup(file, opts.yes);
2643
2647
  });
2644
2648
 
2649
+ // src/commands/test-env.ts
2650
+ import { Command as Command10 } from "commander";
2651
+ import chalk10 from "chalk";
2652
+ import ora8 from "ora";
2653
+ import { join as join8 } from "path";
2654
+ import { fileURLToPath as fileURLToPath2 } from "url";
2655
+ import { dirname as dirname2 } from "path";
2656
+
2657
+ // src/lib/test-env.ts
2658
+ import {
2659
+ existsSync as existsSync9,
2660
+ mkdirSync as mkdirSync6,
2661
+ readFileSync as readFileSync6,
2662
+ writeFileSync as writeFileSync6,
2663
+ rmSync,
2664
+ readdirSync as readdirSync3,
2665
+ cpSync
2666
+ } from "fs";
2667
+ import { join as join7 } from "path";
2668
+ import { parse as parseYaml3 } from "yaml";
2669
+ import { execa as execa3 } from "execa";
2670
+ function getTestEnvRoot(dataDir) {
2671
+ return join7(dataDir, "test-env");
2672
+ }
2673
+ function getLockPath(dataDir, slot) {
2674
+ return join7(getTestEnvRoot(dataDir), `slot-${slot}.lock`);
2675
+ }
2676
+ function getSlotDataPath(dataDir, slot) {
2677
+ return join7(getTestEnvRoot(dataDir), `slot-${slot}`);
2678
+ }
2679
+ function getTestEnvConfigPath(dataDir) {
2680
+ return join7(dataDir, "config", "test-env.yaml");
2681
+ }
2682
+ var DEFAULT_CONFIG = {
2683
+ max_slots: 1,
2684
+ timeout_minutes: 10,
2685
+ base_port: 9100
2686
+ };
2687
+ var PORT_OFFSETS = {
2688
+ anvil: 0,
2689
+ typesense: 8,
2690
+ vault_svc: 1,
2691
+ vault_router: 50,
2692
+ vault_mcp: 100,
2693
+ forge: 150,
2694
+ ui: 160
2695
+ };
2696
+ function loadTestEnvConfig(dataDir) {
2697
+ const configPath = getTestEnvConfigPath(dataDir);
2698
+ if (!existsSync9(configPath)) {
2699
+ return { ...DEFAULT_CONFIG };
2700
+ }
2701
+ try {
2702
+ const raw = readFileSync6(configPath, "utf-8");
2703
+ const parsed = parseYaml3(raw);
2704
+ const cfg = parsed?.test_env ?? {};
2705
+ return {
2706
+ max_slots: cfg.max_slots ?? DEFAULT_CONFIG.max_slots,
2707
+ timeout_minutes: cfg.timeout_minutes ?? DEFAULT_CONFIG.timeout_minutes,
2708
+ base_port: cfg.base_port ?? DEFAULT_CONFIG.base_port
2709
+ };
2710
+ } catch {
2711
+ return { ...DEFAULT_CONFIG };
2712
+ }
2713
+ }
2714
+ function calcPorts(slot, basePort) {
2715
+ const base = basePort + slot * 300;
2716
+ return {
2717
+ anvil: base + PORT_OFFSETS.anvil,
2718
+ typesense: base + PORT_OFFSETS.typesense,
2719
+ vault_svc: base + PORT_OFFSETS.vault_svc,
2720
+ vault_router: base + PORT_OFFSETS.vault_router,
2721
+ vault_mcp: base + PORT_OFFSETS.vault_mcp,
2722
+ forge: base + PORT_OFFSETS.forge,
2723
+ ui: base + PORT_OFFSETS.ui
2724
+ };
2725
+ }
2726
+ function readLock(dataDir, slot) {
2727
+ const lockPath = getLockPath(dataDir, slot);
2728
+ if (!existsSync9(lockPath)) return null;
2729
+ try {
2730
+ return JSON.parse(readFileSync6(lockPath, "utf-8"));
2731
+ } catch {
2732
+ return null;
2733
+ }
2734
+ }
2735
+ function writeLock(dataDir, lock) {
2736
+ mkdirSync6(getTestEnvRoot(dataDir), { recursive: true });
2737
+ writeFileSync6(getLockPath(dataDir, lock.slot), JSON.stringify(lock, null, 2), "utf-8");
2738
+ }
2739
+ function removeLock(dataDir, slot) {
2740
+ const lockPath = getLockPath(dataDir, slot);
2741
+ if (existsSync9(lockPath)) {
2742
+ rmSync(lockPath);
2743
+ }
2744
+ }
2745
+ function isLockExpired(lock, timeoutMinutes) {
2746
+ const acquired = new Date(lock.acquiredAt).getTime();
2747
+ return Date.now() - acquired > timeoutMinutes * 60 * 1e3;
2748
+ }
2749
+ function getAllSlotStatuses(dataDir, cfg) {
2750
+ return Array.from({ length: cfg.max_slots }, (_, slot) => {
2751
+ const lock = readLock(dataDir, slot);
2752
+ if (!lock) {
2753
+ return { slot, state: "free" };
2754
+ }
2755
+ const expired = isLockExpired(lock, cfg.timeout_minutes);
2756
+ const elapsed = (Date.now() - new Date(lock.acquiredAt).getTime()) / 6e4;
2757
+ return {
2758
+ slot,
2759
+ state: expired ? "expired" : "acquired",
2760
+ lock,
2761
+ ports: lock.ports,
2762
+ dataPath: lock.dataPath,
2763
+ acquiredAt: lock.acquiredAt,
2764
+ elapsedMinutes: Math.round(elapsed)
2765
+ };
2766
+ });
2767
+ }
2768
+ function findFreeSlot(dataDir, cfg) {
2769
+ for (let slot = 0; slot < cfg.max_slots; slot++) {
2770
+ const lock = readLock(dataDir, slot);
2771
+ if (!lock) return slot;
2772
+ if (isLockExpired(lock, cfg.timeout_minutes)) {
2773
+ removeLock(dataDir, slot);
2774
+ return slot;
2775
+ }
2776
+ }
2777
+ return null;
2778
+ }
2779
+ function createSlotDirs(slotDataPath) {
2780
+ const dirs = [
2781
+ "notes",
2782
+ join7("vaults", "personal"),
2783
+ "registry",
2784
+ "workspaces",
2785
+ "sessions",
2786
+ "typesense-data"
2787
+ ];
2788
+ for (const dir of dirs) {
2789
+ mkdirSync6(join7(slotDataPath, dir), { recursive: true });
2790
+ }
2791
+ }
2792
+ function removeSlotDirs(slotDataPath) {
2793
+ if (existsSync9(slotDataPath)) {
2794
+ rmSync(slotDataPath, { recursive: true, force: true });
2795
+ }
2796
+ }
2797
+ function buildComposeEnv(runtime, ports, slotDataPath) {
2798
+ return {
2799
+ ...process.env,
2800
+ HORUS_RUNTIME: runtime.name,
2801
+ TEST_DATA_PATH: slotDataPath,
2802
+ TEST_PORT_ANVIL: String(ports.anvil),
2803
+ TEST_PORT_TYPESENSE: String(ports.typesense),
2804
+ TEST_PORT_VAULT_SVC: String(ports.vault_svc),
2805
+ TEST_PORT_VAULT_ROUTER: String(ports.vault_router),
2806
+ TEST_PORT_VAULT_MCP: String(ports.vault_mcp),
2807
+ TEST_PORT_FORGE: String(ports.forge),
2808
+ TEST_PORT_UI: String(ports.ui)
2809
+ };
2810
+ }
2811
+ async function composeUp(runtime, projectName2, ports, slotDataPath) {
2812
+ const env = buildComposeEnv(runtime, ports, slotDataPath);
2813
+ const result = await execa3(
2814
+ runtime.name,
2815
+ [
2816
+ "compose",
2817
+ "-p",
2818
+ projectName2,
2819
+ "-f",
2820
+ join7(HORUS_DIR, "docker-compose.yml"),
2821
+ "-f",
2822
+ join7(HORUS_DIR, "docker-compose.test.yml"),
2823
+ "up",
2824
+ "-d"
2825
+ ],
2826
+ { cwd: HORUS_DIR, env, reject: false }
2827
+ );
2828
+ if (result.exitCode !== 0) {
2829
+ throw new Error(
2830
+ `Failed to start shadow stack (project ${projectName2}):
2831
+ ${result.stderr}`
2832
+ );
2833
+ }
2834
+ }
2835
+ async function composeDown(runtime, projectName2, ports, slotDataPath) {
2836
+ const env = buildComposeEnv(runtime, ports, slotDataPath);
2837
+ await execa3(
2838
+ runtime.name,
2839
+ ["compose", "-p", projectName2, "down", "--volumes", "--remove-orphans"],
2840
+ { cwd: HORUS_DIR, env, reject: false }
2841
+ );
2842
+ }
2843
+ var HEALTH_SERVICES = ["anvil", "forge", "vault-mcp", "typesense"];
2844
+ async function checkContainerHealthByProject(runtime, projectName2, service) {
2845
+ const candidates = [
2846
+ `${projectName2}-${service}-1`,
2847
+ `${projectName2}_${service}_1`
2848
+ ];
2849
+ for (const name of candidates) {
2850
+ try {
2851
+ const result = await execa3(
2852
+ runtime.name,
2853
+ ["inspect", "--format", "{{.State.Health.Status}}", name],
2854
+ { reject: false }
2855
+ );
2856
+ if (result.exitCode === 0) {
2857
+ const status = result.stdout.toString().trim().toLowerCase();
2858
+ if (status === "healthy") return "healthy";
2859
+ if (status === "unhealthy") return "unhealthy";
2860
+ return "starting";
2861
+ }
2862
+ } catch {
2863
+ continue;
2864
+ }
2865
+ }
2866
+ return "starting";
2867
+ }
2868
+ async function waitForShadowStackHealthy(runtime, projectName2, timeoutMs = 12e4, intervalMs = 3e3, onUpdate) {
2869
+ const start = Date.now();
2870
+ while (true) {
2871
+ const statuses = {};
2872
+ await Promise.all(
2873
+ HEALTH_SERVICES.map(async (svc) => {
2874
+ statuses[svc] = await checkContainerHealthByProject(runtime, projectName2, svc);
2875
+ })
2876
+ );
2877
+ if (onUpdate) onUpdate(statuses);
2878
+ const allHealthy = Object.values(statuses).every((s) => s === "healthy");
2879
+ if (allHealthy) return;
2880
+ const anyUnhealthy = Object.values(statuses).some((s) => s === "unhealthy");
2881
+ if (anyUnhealthy) {
2882
+ const failed = Object.entries(statuses).filter(([, s]) => s === "unhealthy").map(([n]) => n).join(", ");
2883
+ throw new Error(`Shadow stack services failed health check: ${failed}`);
2884
+ }
2885
+ if (Date.now() - start >= timeoutMs) {
2886
+ const notReady = Object.entries(statuses).filter(([, s]) => s !== "healthy").map(([n, s]) => `${n}(${s})`).join(", ");
2887
+ throw new Error(`Timed out after ${timeoutMs / 1e3}s waiting for: ${notReady}`);
2888
+ }
2889
+ await new Promise((r) => setTimeout(r, intervalMs));
2890
+ }
2891
+ }
2892
+ function seedFromFixtures(fixturesPath, slotDataPath) {
2893
+ if (!existsSync9(fixturesPath)) {
2894
+ throw new Error(`Fixtures not found at ${fixturesPath}. Run from the Horus repo root.`);
2895
+ }
2896
+ const dirs = readdirSync3(fixturesPath);
2897
+ for (const dir of dirs) {
2898
+ const src = join7(fixturesPath, dir);
2899
+ const dest = join7(slotDataPath, dir);
2900
+ cpSync(src, dest, { recursive: true });
2901
+ }
2902
+ }
2903
+ function seedFromLive(dataDir, slotDataPath) {
2904
+ const liveDirs = ["notes", "vaults", "registry"];
2905
+ for (const dir of liveDirs) {
2906
+ const src = join7(dataDir, dir);
2907
+ const dest = join7(slotDataPath, dir);
2908
+ if (existsSync9(src)) {
2909
+ cpSync(src, dest, { recursive: true });
2910
+ }
2911
+ }
2912
+ }
2913
+ function projectName(slot) {
2914
+ return `horus-test-${slot}`;
2915
+ }
2916
+
2917
+ // src/commands/test-env.ts
2918
+ var testEnvCommand = new Command10("test-env").description("Manage isolated shadow stacks for integration testing");
2919
+ testEnvCommand.command("acquire").description("Start a shadow stack on alternate ports with isolated data").option("--timeout <seconds>", "Max wait for health checks (default: 120)", "120").action(async (opts) => {
2920
+ const config = loadConfig();
2921
+ const dataDir = config.data_dir;
2922
+ const testCfg = loadTestEnvConfig(dataDir);
2923
+ const spinner = ora8("Detecting runtime...").start();
2924
+ let runtime;
2925
+ try {
2926
+ runtime = await detectRuntime(config.runtime);
2927
+ spinner.succeed(`Using ${chalk10.cyan(runtime.name)}`);
2928
+ } catch (error) {
2929
+ spinner.fail("No container runtime found");
2930
+ console.error(error.message);
2931
+ process.exit(1);
2932
+ }
2933
+ const slot = findFreeSlot(dataDir, testCfg);
2934
+ if (slot === null) {
2935
+ console.error(chalk10.red(
2936
+ `All ${testCfg.max_slots} slot(s) are in use. Run ${chalk10.bold("horus test-env status")} to see active slots, or ${chalk10.bold("horus test-env release")} to free one.`
2937
+ ));
2938
+ process.exit(1);
2939
+ }
2940
+ const ports = calcPorts(slot, testCfg.base_port);
2941
+ const slotDataPath = getSlotDataPath(dataDir, slot);
2942
+ const project = projectName(slot);
2943
+ const dirSpinner = ora8(`Creating slot-${slot} data directories...`).start();
2944
+ createSlotDirs(slotDataPath);
2945
+ dirSpinner.succeed(`Data directory: ${chalk10.dim(slotDataPath)}`);
2946
+ writeLock(dataDir, {
2947
+ slot,
2948
+ pid: process.pid,
2949
+ acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
2950
+ ports,
2951
+ dataPath: slotDataPath
2952
+ });
2953
+ const upSpinner = ora8(`Starting shadow stack (project ${chalk10.cyan(project)})...`).start();
2954
+ try {
2955
+ await composeUp(runtime, project, ports, slotDataPath);
2956
+ upSpinner.succeed(`Shadow stack started`);
2957
+ } catch (error) {
2958
+ upSpinner.fail("Failed to start shadow stack");
2959
+ removeLock(dataDir, slot);
2960
+ removeSlotDirs(slotDataPath);
2961
+ console.error(error.message);
2962
+ process.exit(1);
2963
+ }
2964
+ const healthSpinner = ora8("Waiting for services to be healthy...").start();
2965
+ const timeoutMs = parseInt(opts.timeout, 10) * 1e3;
2966
+ try {
2967
+ await waitForShadowStackHealthy(runtime, project, timeoutMs, 3e3, (statuses) => {
2968
+ const parts = Object.entries(statuses).map(([svc, s]) => `${svc}:${s === "healthy" ? chalk10.green(s) : chalk10.yellow(s)}`).join(" ");
2969
+ healthSpinner.text = `Waiting for services... ${parts}`;
2970
+ });
2971
+ healthSpinner.succeed("All services healthy");
2972
+ } catch (error) {
2973
+ healthSpinner.fail("Health check failed");
2974
+ await composeDown(runtime, project, ports, slotDataPath);
2975
+ removeLock(dataDir, slot);
2976
+ removeSlotDirs(slotDataPath);
2977
+ console.error(error.message);
2978
+ process.exit(1);
2979
+ }
2980
+ console.log("");
2981
+ console.log(chalk10.bold.green(`\u2713 Slot ${slot} acquired`));
2982
+ console.log("");
2983
+ console.log(chalk10.bold("Connection info:"));
2984
+ console.log(` Slot: ${chalk10.cyan(slot)}`);
2985
+ console.log(` Project: ${chalk10.cyan(project)}`);
2986
+ console.log(` Data: ${chalk10.dim(slotDataPath)}`);
2987
+ console.log("");
2988
+ console.log(chalk10.bold("Ports:"));
2989
+ console.log(` Anvil: http://localhost:${chalk10.cyan(ports.anvil)}`);
2990
+ console.log(` Forge: http://localhost:${chalk10.cyan(ports.forge)}`);
2991
+ console.log(` Vault MCP: http://localhost:${chalk10.cyan(ports.vault_mcp)}`);
2992
+ console.log(` Vault Router: http://localhost:${chalk10.cyan(ports.vault_router)}`);
2993
+ console.log(` Typesense: http://localhost:${chalk10.cyan(ports.typesense)}`);
2994
+ console.log(` UI: http://localhost:${chalk10.cyan(ports.ui)}`);
2995
+ console.log("");
2996
+ console.log(chalk10.bold("Environment:"));
2997
+ console.log(` export TEST_SLOT=${slot}`);
2998
+ console.log(` export TEST_ANVIL_URL=http://localhost:${ports.anvil}`);
2999
+ console.log(` export TEST_FORGE_URL=http://localhost:${ports.forge}`);
3000
+ console.log(` export TEST_VAULT_MCP_URL=http://localhost:${ports.vault_mcp}`);
3001
+ console.log(` export TEST_DATA_PATH=${slotDataPath}`);
3002
+ console.log("");
3003
+ console.log(chalk10.dim(`Run ${chalk10.bold(`horus test-env seed --slot ${slot}`)} to populate with fixtures.`));
3004
+ console.log(chalk10.dim(`Run ${chalk10.bold(`horus test-env release --slot ${slot}`)} when done.`));
3005
+ });
3006
+ testEnvCommand.command("release").description("Tear down a shadow stack and remove its data").option("--slot <n>", "Slot number to release (default: auto-detect acquired slot)").action(async (opts) => {
3007
+ const config = loadConfig();
3008
+ const dataDir = config.data_dir;
3009
+ const testCfg = loadTestEnvConfig(dataDir);
3010
+ let slot;
3011
+ if (opts.slot !== void 0) {
3012
+ slot = parseInt(opts.slot, 10);
3013
+ } else {
3014
+ const statuses = getAllSlotStatuses(dataDir, testCfg);
3015
+ const acquired = statuses.find((s) => s.state === "acquired" || s.state === "expired");
3016
+ if (!acquired) {
3017
+ console.log(chalk10.yellow("No active slots found."));
3018
+ return;
3019
+ }
3020
+ slot = acquired.slot;
3021
+ }
3022
+ const lock = readLock(dataDir, slot);
3023
+ const slotDataPath = getSlotDataPath(dataDir, slot);
3024
+ const project = projectName(slot);
3025
+ const ports = lock?.ports ?? calcPorts(slot, testCfg.base_port);
3026
+ const spinner = ora8("Detecting runtime...").start();
3027
+ let runtime;
3028
+ try {
3029
+ runtime = await detectRuntime(config.runtime);
3030
+ spinner.succeed(`Using ${chalk10.cyan(runtime.name)}`);
3031
+ } catch (error) {
3032
+ spinner.fail("No container runtime found");
3033
+ console.error(error.message);
3034
+ process.exit(1);
3035
+ }
3036
+ const downSpinner = ora8(`Stopping ${chalk10.cyan(project)}...`).start();
3037
+ try {
3038
+ await composeDown(runtime, project, ports, slotDataPath);
3039
+ downSpinner.succeed("Shadow stack stopped");
3040
+ } catch {
3041
+ downSpinner.warn("Failed to stop cleanly (continuing cleanup)");
3042
+ }
3043
+ const cleanSpinner = ora8("Removing test data...").start();
3044
+ removeSlotDirs(slotDataPath);
3045
+ removeLock(dataDir, slot);
3046
+ cleanSpinner.succeed("Test data removed");
3047
+ console.log("");
3048
+ console.log(chalk10.bold.green(`\u2713 Slot ${slot} released`));
3049
+ });
3050
+ testEnvCommand.command("status").description("Show active shadow stack slots").action(() => {
3051
+ const config = loadConfig();
3052
+ const dataDir = config.data_dir;
3053
+ const testCfg = loadTestEnvConfig(dataDir);
3054
+ const statuses = getAllSlotStatuses(dataDir, testCfg);
3055
+ const acquiredCount = statuses.filter((s) => s.state === "acquired").length;
3056
+ console.log("");
3057
+ console.log(chalk10.bold("Test Environment Status"));
3058
+ console.log(` Max slots: ${testCfg.max_slots}`);
3059
+ console.log(` In use: ${acquiredCount} / ${testCfg.max_slots}`);
3060
+ console.log(` Base port: ${testCfg.base_port}`);
3061
+ console.log("");
3062
+ if (statuses.every((s) => s.state === "free")) {
3063
+ console.log(chalk10.dim(" No active slots."));
3064
+ console.log("");
3065
+ return;
3066
+ }
3067
+ for (const s of statuses) {
3068
+ if (s.state === "free") continue;
3069
+ const stateLabel = s.state === "expired" ? chalk10.yellow("EXPIRED") : chalk10.green("ACTIVE");
3070
+ console.log(` ${chalk10.bold(`Slot ${s.slot}`)} ${stateLabel}`);
3071
+ if (s.acquiredAt) {
3072
+ console.log(` Acquired: ${s.acquiredAt} (${s.elapsedMinutes}m ago)`);
3073
+ }
3074
+ if (s.ports) {
3075
+ console.log(` Ports: anvil=${s.ports.anvil} forge=${s.ports.forge} vault-mcp=${s.ports.vault_mcp} typesense=${s.ports.typesense}`);
3076
+ }
3077
+ if (s.dataPath) {
3078
+ console.log(` Data: ${chalk10.dim(s.dataPath)}`);
3079
+ }
3080
+ console.log("");
3081
+ }
3082
+ });
3083
+ testEnvCommand.command("seed").description("Populate a slot with test fixtures (or a snapshot of live data)").option("--slot <n>", "Slot to seed (default: auto-detect)").option("--from-live", "Snapshot live data instead of using fixtures").action(async (opts) => {
3084
+ const config = loadConfig();
3085
+ const dataDir = config.data_dir;
3086
+ const testCfg = loadTestEnvConfig(dataDir);
3087
+ let slot;
3088
+ if (opts.slot !== void 0) {
3089
+ slot = parseInt(opts.slot, 10);
3090
+ } else {
3091
+ const statuses = getAllSlotStatuses(dataDir, testCfg);
3092
+ const acquired = statuses.find((s) => s.state === "acquired");
3093
+ if (!acquired) {
3094
+ console.error(chalk10.red("No active slot found. Run `horus test-env acquire` first."));
3095
+ process.exit(1);
3096
+ }
3097
+ slot = acquired.slot;
3098
+ }
3099
+ const slotDataPath = getSlotDataPath(dataDir, slot);
3100
+ if (opts.fromLive) {
3101
+ const spinner = ora8("Snapshotting live data into slot...").start();
3102
+ try {
3103
+ seedFromLive(dataDir, slotDataPath);
3104
+ spinner.succeed("Live data snapshotted");
3105
+ } catch (error) {
3106
+ spinner.fail("Failed to snapshot live data");
3107
+ console.error(error.message);
3108
+ process.exit(1);
3109
+ }
3110
+ } else {
3111
+ const here = dirname2(fileURLToPath2(import.meta.url));
3112
+ const repoRoot = join8(here, "..", "..", "..", "..", "..");
3113
+ const fixturesPath = join8(repoRoot, "test", "fixtures");
3114
+ const spinner = ora8(`Seeding slot-${slot} from fixtures...`).start();
3115
+ try {
3116
+ seedFromFixtures(fixturesPath, slotDataPath);
3117
+ spinner.succeed(`Slot ${slot} seeded from fixtures`);
3118
+ } catch (error) {
3119
+ spinner.fail("Failed to seed fixtures");
3120
+ console.error(error.message);
3121
+ process.exit(1);
3122
+ }
3123
+ }
3124
+ console.log("");
3125
+ console.log(chalk10.dim("Services will re-index automatically. Allow ~10s before running tests."));
3126
+ });
3127
+
2645
3128
  // src/index.ts
2646
- var program = new Command10();
3129
+ var program = new Command11();
2647
3130
  program.name("horus").description("CLI for managing the Horus Docker Compose stack").version(CLI_VERSION);
2648
3131
  program.addCommand(setupCommand);
2649
3132
  program.addCommand(upCommand);
@@ -2654,6 +3137,7 @@ program.addCommand(connectCommand);
2654
3137
  program.addCommand(updateCommand);
2655
3138
  program.addCommand(doctorCommand);
2656
3139
  program.addCommand(backupCommand);
3140
+ program.addCommand(testEnvCommand);
2657
3141
  program.exitOverride();
2658
3142
  try {
2659
3143
  await program.parseAsync(process.argv);
@@ -2662,9 +3146,9 @@ try {
2662
3146
  process.exit(0);
2663
3147
  }
2664
3148
  if (error instanceof Error) {
2665
- console.error(chalk10.red(`Error: ${error.message}`));
3149
+ console.error(chalk11.red(`Error: ${error.message}`));
2666
3150
  } else {
2667
- console.error(chalk10.red("An unexpected error occurred."));
3151
+ console.error(chalk11.red("An unexpected error occurred."));
2668
3152
  }
2669
3153
  process.exit(1);
2670
3154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkhera30/cli",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for managing the Horus AI development stack",
5
5
  "type": "module",
6
6
  "bin": {