@arkhera30/cli 0.1.8 → 0.1.9

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.
@@ -198,8 +198,9 @@ services:
198
198
  - FORGE_HOST_ANVIL_URL=http://localhost:${ANVIL_PORT:-8100}
199
199
  - FORGE_HOST_VAULT_URL=http://localhost:${VAULT_MCP_PORT:-8300}
200
200
  - FORGE_HOST_FORGE_URL=http://localhost:${FORGE_PORT:-8200}
201
- # Colon-separated list of in-container paths to scan for git repos
202
- - FORGE_SCAN_PATHS=/data/repos
201
+ # Colon-separated list of in-container paths to scan for git repos.
202
+ # Generated by the CLI from host_repos_extra_scan_dirs config — do not edit manually.
203
+ - FORGE_SCAN_PATHS=${FORGE_SCAN_PATHS:-/data/repos}
203
204
  - GITHUB_TOKEN=${GITHUB_TOKEN:-}
204
205
  depends_on:
205
206
  anvil:
package/dist/index.js CHANGED
@@ -58,6 +58,7 @@ function defaultConfig() {
58
58
  git_host: "github.com",
59
59
  repos: { ...DEFAULT_REPOS },
60
60
  host_repos_path: "",
61
+ host_repos_extra_scan_dirs: [],
61
62
  github_token: ""
62
63
  };
63
64
  }
@@ -91,6 +92,7 @@ function loadConfig() {
91
92
  forge_registry: parsed.repos?.forge_registry ?? defaults.repos.forge_registry
92
93
  },
93
94
  host_repos_path: parsed.host_repos_path ?? defaults.host_repos_path,
95
+ host_repos_extra_scan_dirs: parsed.host_repos_extra_scan_dirs ?? defaults.host_repos_extra_scan_dirs,
94
96
  github_token: parsed.github_token ?? defaults.github_token
95
97
  };
96
98
  }
@@ -108,6 +110,9 @@ function resolvePath(p) {
108
110
  function generateEnv(config) {
109
111
  const dataDir = resolvePath(config.data_dir);
110
112
  const hostReposPath = config.host_repos_path ? resolvePath(config.host_repos_path) : "";
113
+ 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(":");
111
116
  const lines = [
112
117
  "# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
113
118
  "# Horus \u2014 Generated .env file",
@@ -116,6 +121,7 @@ function generateEnv(config) {
116
121
  "",
117
122
  `HORUS_DATA_PATH=${dataDir}`,
118
123
  `HOST_REPOS_PATH=${hostReposPath}`,
124
+ `FORGE_SCAN_PATHS=${forgeScanPaths}`,
119
125
  "",
120
126
  "# Ports",
121
127
  `ANVIL_PORT=${config.ports.anvil}`,
@@ -142,6 +148,7 @@ function writeEnvFile(config) {
142
148
  var CONFIG_KEYS = [
143
149
  "data-dir",
144
150
  "host-repos-path",
151
+ "host-repos-extra-scan-dirs",
145
152
  "runtime",
146
153
  "port.anvil",
147
154
  "port.vault-rest",
@@ -159,6 +166,8 @@ function getConfigValue(config, key) {
159
166
  return config.data_dir;
160
167
  case "host-repos-path":
161
168
  return config.host_repos_path;
169
+ case "host-repos-extra-scan-dirs":
170
+ return (config.host_repos_extra_scan_dirs ?? []).join(", ");
162
171
  case "runtime":
163
172
  return config.runtime;
164
173
  case "port.anvil":
@@ -190,6 +199,9 @@ function setConfigValue(config, key, value) {
190
199
  case "host-repos-path":
191
200
  updated.host_repos_path = value;
192
201
  break;
202
+ case "host-repos-extra-scan-dirs":
203
+ updated.host_repos_extra_scan_dirs = value.split(",").map((d) => d.trim()).filter(Boolean);
204
+ break;
193
205
  case "runtime":
194
206
  if (value !== "docker" && value !== "podman") {
195
207
  throw new Error(`Invalid runtime: ${value}. Must be "docker" or "podman".`);
@@ -258,11 +270,13 @@ async function commandExists(command) {
258
270
  }
259
271
  function createRuntime(name) {
260
272
  const bin = name;
273
+ const composeEnv = { ...process.env, HORUS_RUNTIME: name };
261
274
  return {
262
275
  name,
263
276
  async compose(...args) {
264
277
  const result = await execa(bin, ["compose", ...args], {
265
278
  cwd: HORUS_DIR,
279
+ env: composeEnv,
266
280
  reject: false
267
281
  });
268
282
  if (result.exitCode !== 0) {
@@ -290,6 +304,7 @@ function createRuntime(name) {
290
304
  try {
291
305
  const result = await execa(bin, ["compose", "ps", "--format", "json"], {
292
306
  cwd: HORUS_DIR,
307
+ env: composeEnv,
293
308
  reject: false
294
309
  });
295
310
  return result.exitCode === 0 && result.stdout.toString().trim().length > 0;
@@ -331,6 +346,7 @@ async function composeStreaming(runtime, args) {
331
346
  const bin = runtime.name;
332
347
  const result = await execa(bin, ["compose", ...args], {
333
348
  cwd: HORUS_DIR,
349
+ env: { ...process.env, HORUS_RUNTIME: runtime.name },
334
350
  stdio: "inherit",
335
351
  reject: false
336
352
  });
@@ -420,13 +436,22 @@ function getBundledComposePath() {
420
436
  Searched: ${candidates.join(", ")}`
421
437
  );
422
438
  }
439
+ function applyPodmanUserOverride(compose) {
440
+ return compose.replace(
441
+ /^( image: .+)$/gm,
442
+ '$1\n user: "0:0"'
443
+ );
444
+ }
423
445
  function composeFileExists() {
424
446
  return existsSync2(COMPOSE_PATH);
425
447
  }
426
- function installComposeFile() {
448
+ function installComposeFile(runtime) {
427
449
  ensureHorusDir();
428
450
  const bundledPath = getBundledComposePath();
429
- const content = readFileSync2(bundledPath, "utf-8");
451
+ let content = readFileSync2(bundledPath, "utf-8");
452
+ if (runtime === "podman") {
453
+ content = applyPodmanUserOverride(content);
454
+ }
430
455
  writeFileSync2(COMPOSE_PATH, content, "utf-8");
431
456
  }
432
457
 
@@ -526,6 +551,11 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
526
551
  message: "Host repos path (for Forge repo scanning, leave empty to skip):",
527
552
  default: ""
528
553
  });
554
+ const extra_scan_dirs_raw = await input({
555
+ message: "Extra subdirectories to scan within repos path (comma-separated, e.g. ArjunKhera \u2014 leave empty to skip):",
556
+ default: ""
557
+ });
558
+ const host_repos_extra_scan_dirs = extra_scan_dirs_raw.split(",").map((d) => d.trim()).filter(Boolean);
529
559
  const customize_ports = await confirm({
530
560
  message: "Customize port assignments?",
531
561
  default: false
@@ -600,6 +630,7 @@ ${example("forge-registry")}
600
630
  ...defaultConfig(),
601
631
  data_dir,
602
632
  host_repos_path,
633
+ host_repos_extra_scan_dirs,
603
634
  runtime: runtime.name,
604
635
  ports,
605
636
  git_host: git_host.trim(),
@@ -631,7 +662,7 @@ ${example("forge-registry")}
631
662
  }
632
663
  const composeSpinner = ora("Installing docker-compose.yml...").start();
633
664
  try {
634
- installComposeFile();
665
+ installComposeFile(runtime.name);
635
666
  composeSpinner.succeed("Compose file installed to ~/.horus/docker-compose.yml");
636
667
  } catch (error) {
637
668
  composeSpinner.fail("Failed to install compose file");
@@ -741,7 +772,7 @@ ${example("forge-registry")}
741
772
  import { Command as Command2 } from "commander";
742
773
  import chalk2 from "chalk";
743
774
  import ora2 from "ora";
744
- var upCommand = new Command2("up").description("Start the Horus stack").action(async () => {
775
+ var upCommand = new Command2("up").description("Start the Horus stack").option("--no-pull", "Skip pulling latest images before starting").action(async (opts) => {
745
776
  if (!configExists() || !composeFileExists()) {
746
777
  console.log(chalk2.red("Horus is not set up yet."));
747
778
  console.log(chalk2.dim("Run `horus setup` first."));
@@ -758,6 +789,15 @@ var upCommand = new Command2("up").description("Start the Horus stack").action(a
758
789
  console.log(error.message);
759
790
  process.exit(1);
760
791
  }
792
+ if (opts.pull) {
793
+ const pullSpinner = ora2("Pulling latest images...").start();
794
+ try {
795
+ await composeStreaming(runtime, ["pull", "--ignore-pull-failures"]);
796
+ pullSpinner.succeed("Images up to date");
797
+ } catch {
798
+ pullSpinner.warn("Could not pull images, using cached");
799
+ }
800
+ }
761
801
  console.log("");
762
802
  console.log(chalk2.bold("Starting Horus services..."));
763
803
  try {
@@ -930,8 +970,10 @@ var configCommand = new Command5("config").description("View or modify Horus con
930
970
  console.log(` ${chalk5.bold("version:")} ${config.version}`);
931
971
  console.log(` ${chalk5.bold("data-dir:")} ${config.data_dir}`);
932
972
  console.log(` ${chalk5.bold("runtime:")} ${config.runtime}`);
933
- console.log(` ${chalk5.bold("host-repos-path:")} ${config.host_repos_path || chalk5.dim("(not set)")}`);
934
- console.log(` ${chalk5.bold("git-host:")} ${config.git_host || chalk5.dim("(not set)")}`);
973
+ console.log(` ${chalk5.bold("host-repos-path:")} ${config.host_repos_path || chalk5.dim("(not set)")}`);
974
+ const extraDirs = (config.host_repos_extra_scan_dirs ?? []).join(", ");
975
+ console.log(` ${chalk5.bold("host-repos-extra-scan-dirs:")} ${extraDirs || chalk5.dim("(not set)")}`);
976
+ console.log(` ${chalk5.bold("git-host:")} ${config.git_host || chalk5.dim("(not set)")}`);
935
977
  console.log(` ${chalk5.bold("github-token:")} ${config.github_token ? maskApiKey(config.github_token) : chalk5.dim("(not set)")}`);
936
978
  console.log("");
937
979
  console.log(chalk5.bold(" Ports:"));
@@ -992,6 +1034,7 @@ configCommand.command("set <key> <value>").description("Set a configuration valu
992
1034
  const needsRestart = [
993
1035
  "data-dir",
994
1036
  "host-repos-path",
1037
+ "host-repos-extra-scan-dirs",
995
1038
  "runtime",
996
1039
  "port.anvil",
997
1040
  "port.vault-rest",
@@ -1025,6 +1068,7 @@ import { checkbox } from "@inquirer/prompts";
1025
1068
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
1026
1069
  import { join as join4 } from "path";
1027
1070
  import { homedir as homedir3 } from "os";
1071
+ import { execa as execa2 } from "execa";
1028
1072
  function detectInstalledClients() {
1029
1073
  const detected = [];
1030
1074
  const home = homedir3();
@@ -1070,6 +1114,32 @@ function mergeAndWriteConfig(configPath, mcpServers) {
1070
1114
  mkdirSync3(dir, { recursive: true });
1071
1115
  writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
1072
1116
  }
1117
+ async function isClaudeCliAvailable() {
1118
+ try {
1119
+ const result = await execa2("claude", ["--version"], { reject: false });
1120
+ return result.exitCode === 0;
1121
+ } catch {
1122
+ return false;
1123
+ }
1124
+ }
1125
+ async function registerWithClaudeCode(mcpServers) {
1126
+ const registered = [];
1127
+ const failed = [];
1128
+ for (const [name, entry] of Object.entries(mcpServers)) {
1129
+ const baseUrl = entry.url.replace(/\/sse$/, "");
1130
+ const result = await execa2(
1131
+ "claude",
1132
+ ["mcp", "add", "--transport", "http", "--scope", "user", name, baseUrl],
1133
+ { reject: false }
1134
+ );
1135
+ if (result.exitCode === 0) {
1136
+ registered.push(name);
1137
+ } else {
1138
+ failed.push(name);
1139
+ }
1140
+ }
1141
+ return { registered, failed };
1142
+ }
1073
1143
  async function syncSkills(runtime) {
1074
1144
  const home = homedir3();
1075
1145
  const skillsBase = join4(home, ".claude", "skills");
@@ -1190,14 +1260,41 @@ var connectCommand = new Command6("connect").description("Configure Claude/Curso
1190
1260
  forge: { url: `http://${host}:${config.ports.forge}/sse` }
1191
1261
  };
1192
1262
  for (const target of targets) {
1193
- const configPath = getConfigPath(target);
1194
- const writeSpinner = ora5(`Configuring ${chalk6.cyan(target)}...`).start();
1195
- try {
1196
- mergeAndWriteConfig(configPath, mcpServers);
1197
- writeSpinner.succeed(`Configured ${chalk6.cyan(target)} \u2014 ${chalk6.dim(configPath)}`);
1198
- } catch (error) {
1199
- writeSpinner.fail(`Failed to configure ${target}`);
1200
- console.log(chalk6.dim(error.message));
1263
+ if (target === "claude-code") {
1264
+ const cliSpinner = ora5("Registering MCP servers with Claude Code CLI...").start();
1265
+ const cliAvailable = await isClaudeCliAvailable();
1266
+ if (cliAvailable) {
1267
+ const { registered, failed } = await registerWithClaudeCode(mcpServers);
1268
+ if (failed.length === 0) {
1269
+ cliSpinner.succeed(
1270
+ `Registered with Claude Code: ${registered.map((n) => chalk6.cyan(n)).join(", ")}`
1271
+ );
1272
+ } else if (registered.length > 0) {
1273
+ cliSpinner.warn(
1274
+ `Partially registered \u2014 ok: ${registered.join(", ")}, failed: ${failed.join(", ")}`
1275
+ );
1276
+ } else {
1277
+ cliSpinner.fail("Failed to register MCP servers with Claude Code CLI");
1278
+ }
1279
+ } else {
1280
+ cliSpinner.warn("claude CLI not found on PATH \u2014 register manually:");
1281
+ for (const [name, entry] of Object.entries(mcpServers)) {
1282
+ const baseUrl = entry.url.replace(/\/sse$/, "");
1283
+ console.log(
1284
+ chalk6.dim(` claude mcp add --transport http --scope user ${name} ${baseUrl}`)
1285
+ );
1286
+ }
1287
+ }
1288
+ } else {
1289
+ const configPath = getConfigPath(target);
1290
+ const writeSpinner = ora5(`Configuring ${chalk6.cyan(target)}...`).start();
1291
+ try {
1292
+ mergeAndWriteConfig(configPath, mcpServers);
1293
+ writeSpinner.succeed(`Configured ${chalk6.cyan(target)} \u2014 ${chalk6.dim(configPath)}`);
1294
+ } catch (error) {
1295
+ writeSpinner.fail(`Failed to configure ${target}`);
1296
+ console.log(chalk6.dim(error.message));
1297
+ }
1201
1298
  }
1202
1299
  }
1203
1300
  if (targets.includes("claude-code")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkhera30/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "CLI for managing the Horus AI development stack",
5
5
  "type": "module",
6
6
  "bin": {