@arkhera30/cli 0.1.7 → 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
@@ -12,7 +12,7 @@ import ora from "ora";
12
12
  import { execSync } from "child_process";
13
13
  import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
14
14
  import { join as join3 } from "path";
15
- import { input, confirm, number, select } from "@inquirer/prompts";
15
+ import { input, confirm, number, select, password } from "@inquirer/prompts";
16
16
 
17
17
  // src/lib/config.ts
18
18
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
@@ -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}`,
@@ -127,6 +133,9 @@ function generateEnv(config) {
127
133
  `ANVIL_REPO_URL=${config.repos.anvil_notes}`,
128
134
  `VAULT_KNOWLEDGE_REPO_URL=${config.repos.vault_knowledge}`,
129
135
  `FORGE_REGISTRY_REPO_URL=${config.repos.forge_registry}`,
136
+ "",
137
+ "# Authentication",
138
+ `GITHUB_TOKEN=${config.github_token}`,
130
139
  ""
131
140
  ];
132
141
  return lines.join("\n");
@@ -139,6 +148,7 @@ function writeEnvFile(config) {
139
148
  var CONFIG_KEYS = [
140
149
  "data-dir",
141
150
  "host-repos-path",
151
+ "host-repos-extra-scan-dirs",
142
152
  "runtime",
143
153
  "port.anvil",
144
154
  "port.vault-rest",
@@ -156,6 +166,8 @@ function getConfigValue(config, key) {
156
166
  return config.data_dir;
157
167
  case "host-repos-path":
158
168
  return config.host_repos_path;
169
+ case "host-repos-extra-scan-dirs":
170
+ return (config.host_repos_extra_scan_dirs ?? []).join(", ");
159
171
  case "runtime":
160
172
  return config.runtime;
161
173
  case "port.anvil":
@@ -187,6 +199,9 @@ function setConfigValue(config, key, value) {
187
199
  case "host-repos-path":
188
200
  updated.host_repos_path = value;
189
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;
190
205
  case "runtime":
191
206
  if (value !== "docker" && value !== "podman") {
192
207
  throw new Error(`Invalid runtime: ${value}. Must be "docker" or "podman".`);
@@ -255,11 +270,13 @@ async function commandExists(command) {
255
270
  }
256
271
  function createRuntime(name) {
257
272
  const bin = name;
273
+ const composeEnv = { ...process.env, HORUS_RUNTIME: name };
258
274
  return {
259
275
  name,
260
276
  async compose(...args) {
261
277
  const result = await execa(bin, ["compose", ...args], {
262
278
  cwd: HORUS_DIR,
279
+ env: composeEnv,
263
280
  reject: false
264
281
  });
265
282
  if (result.exitCode !== 0) {
@@ -287,6 +304,7 @@ function createRuntime(name) {
287
304
  try {
288
305
  const result = await execa(bin, ["compose", "ps", "--format", "json"], {
289
306
  cwd: HORUS_DIR,
307
+ env: composeEnv,
290
308
  reject: false
291
309
  });
292
310
  return result.exitCode === 0 && result.stdout.toString().trim().length > 0;
@@ -328,6 +346,7 @@ async function composeStreaming(runtime, args) {
328
346
  const bin = runtime.name;
329
347
  const result = await execa(bin, ["compose", ...args], {
330
348
  cwd: HORUS_DIR,
349
+ env: { ...process.env, HORUS_RUNTIME: runtime.name },
331
350
  stdio: "inherit",
332
351
  reject: false
333
352
  });
@@ -417,18 +436,38 @@ function getBundledComposePath() {
417
436
  Searched: ${candidates.join(", ")}`
418
437
  );
419
438
  }
439
+ function applyPodmanUserOverride(compose) {
440
+ return compose.replace(
441
+ /^( image: .+)$/gm,
442
+ '$1\n user: "0:0"'
443
+ );
444
+ }
420
445
  function composeFileExists() {
421
446
  return existsSync2(COMPOSE_PATH);
422
447
  }
423
- function installComposeFile() {
448
+ function installComposeFile(runtime) {
424
449
  ensureHorusDir();
425
450
  const bundledPath = getBundledComposePath();
426
- const content = readFileSync2(bundledPath, "utf-8");
451
+ let content = readFileSync2(bundledPath, "utf-8");
452
+ if (runtime === "podman") {
453
+ content = applyPodmanUserOverride(content);
454
+ }
427
455
  writeFileSync2(COMPOSE_PATH, content, "utf-8");
428
456
  }
429
457
 
430
458
  // src/commands/setup.ts
431
- var setupCommand = new Command("setup").description("Interactive first-run setup for Horus").option("-y, --yes", "Non-interactive mode (use defaults + env vars)").option("--runtime <runtime>", "Container runtime to use: docker or podman (non-interactive only)").option("--data-dir <path>", "Data directory path").option("--repos-path <path>", "Host repos path for Forge scanning").option("--git-host <host>", "Git server hostname (e.g., github.com, gitlab.corp.com)").option("--anvil-repo <url>", "Anvil notes repository URL").option("--vault-repo <url>", "Vault knowledge-base repository URL").option("--forge-repo <url>", "Forge registry repository URL").action(async (opts) => {
459
+ function injectToken(url, token) {
460
+ if (!token) return url;
461
+ try {
462
+ const parsed = new URL(url);
463
+ parsed.username = "oauth2";
464
+ parsed.password = token;
465
+ return parsed.toString();
466
+ } catch {
467
+ return url;
468
+ }
469
+ }
470
+ var setupCommand = new Command("setup").description("Interactive first-run setup for Horus").option("-y, --yes", "Non-interactive mode (use defaults + env vars)").option("--runtime <runtime>", "Container runtime to use: docker or podman (non-interactive only)").option("--data-dir <path>", "Data directory path").option("--repos-path <path>", "Host repos path for Forge scanning").option("--git-host <host>", "Git server hostname (e.g., github.com, gitlab.corp.com)").option("--anvil-repo <url>", "Anvil notes repository URL").option("--vault-repo <url>", "Vault knowledge-base repository URL").option("--forge-repo <url>", "Forge registry repository URL").option("--github-token <token>", "GitHub personal access token for private repos").action(async (opts) => {
432
471
  console.log("");
433
472
  console.log(chalk.bold("Horus Setup"));
434
473
  console.log(chalk.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"));
@@ -500,7 +539,8 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
500
539
  anvil_notes: opts.anvilRepo || process.env.ANVIL_REPO_URL || defaults.repos.anvil_notes,
501
540
  vault_knowledge: opts.vaultRepo || process.env.VAULT_KNOWLEDGE_REPO_URL || defaults.repos.vault_knowledge,
502
541
  forge_registry: opts.forgeRepo || process.env.FORGE_REGISTRY_REPO_URL || defaults.repos.forge_registry
503
- }
542
+ },
543
+ github_token: opts.githubToken || process.env.GITHUB_TOKEN || ""
504
544
  };
505
545
  } else {
506
546
  const data_dir = await input({
@@ -511,6 +551,11 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
511
551
  message: "Host repos path (for Forge repo scanning, leave empty to skip):",
512
552
  default: ""
513
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);
514
559
  const customize_ports = await confirm({
515
560
  message: "Customize port assignments?",
516
561
  default: false
@@ -547,7 +592,6 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
547
592
  console.log("");
548
593
  console.log(chalk.yellow(" Use HTTPS URLs \u2014 container services do not have SSH keys."));
549
594
  console.log(chalk.dim(" SSH URLs (git@github.com:...) will fail at runtime inside Docker/Podman."));
550
- console.log(chalk.dim(" Set GITHUB_TOKEN for private repos."));
551
595
  console.log("");
552
596
  const git_host = await input({
553
597
  message: "Git server hostname:",
@@ -574,10 +618,19 @@ ${example("forge-registry")}
574
618
  `,
575
619
  validate: (v) => v.trim().length > 0 || "Forge needs a registry repo."
576
620
  });
621
+ console.log("");
622
+ console.log(chalk.bold("Authentication"));
623
+ console.log(chalk.dim("A personal access token is required for private repositories."));
624
+ console.log("");
625
+ const github_token = await password({
626
+ message: "GitHub personal access token (leave empty to skip):",
627
+ mask: "*"
628
+ });
577
629
  config = {
578
630
  ...defaultConfig(),
579
631
  data_dir,
580
632
  host_repos_path,
633
+ host_repos_extra_scan_dirs,
581
634
  runtime: runtime.name,
582
635
  ports,
583
636
  git_host: git_host.trim(),
@@ -585,7 +638,8 @@ ${example("forge-registry")}
585
638
  anvil_notes: anvil_notes.trim(),
586
639
  vault_knowledge: vault_knowledge.trim(),
587
640
  forge_registry: forge_registry.trim()
588
- }
641
+ },
642
+ github_token: github_token.trim()
589
643
  };
590
644
  }
591
645
  const configSpinner = ora("Saving configuration...").start();
@@ -608,7 +662,7 @@ ${example("forge-registry")}
608
662
  }
609
663
  const composeSpinner = ora("Installing docker-compose.yml...").start();
610
664
  try {
611
- installComposeFile();
665
+ installComposeFile(runtime.name);
612
666
  composeSpinner.succeed("Compose file installed to ~/.horus/docker-compose.yml");
613
667
  } catch (error) {
614
668
  composeSpinner.fail("Failed to install compose file");
@@ -633,7 +687,8 @@ ${example("forge-registry")}
633
687
  }
634
688
  try {
635
689
  mkdirSync2(repo.dest, { recursive: true });
636
- execSync(`git clone "${repo.url}" "${repo.dest}"`, {
690
+ const cloneUrl = injectToken(repo.url, config.github_token);
691
+ execSync(`git clone "${cloneUrl}" "${repo.dest}"`, {
637
692
  stdio: "pipe",
638
693
  timeout: 6e4
639
694
  });
@@ -647,7 +702,9 @@ ${example("forge-registry")}
647
702
  console.log(chalk.dim(` ${msg.split("\n")[0]}`));
648
703
  }
649
704
  console.log(chalk.dim(` URL: ${repo.url}`));
650
- console.log(chalk.dim(" Ensure you have git access (SSH key or credential helper)."));
705
+ if (!config.github_token) {
706
+ console.log(chalk.dim(" Tip: Re-run setup and provide a GitHub token if the repo is private."));
707
+ }
651
708
  process.exit(1);
652
709
  }
653
710
  }
@@ -715,7 +772,7 @@ ${example("forge-registry")}
715
772
  import { Command as Command2 } from "commander";
716
773
  import chalk2 from "chalk";
717
774
  import ora2 from "ora";
718
- 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) => {
719
776
  if (!configExists() || !composeFileExists()) {
720
777
  console.log(chalk2.red("Horus is not set up yet."));
721
778
  console.log(chalk2.dim("Run `horus setup` first."));
@@ -732,6 +789,15 @@ var upCommand = new Command2("up").description("Start the Horus stack").action(a
732
789
  console.log(error.message);
733
790
  process.exit(1);
734
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
+ }
735
801
  console.log("");
736
802
  console.log(chalk2.bold("Starting Horus services..."));
737
803
  try {
@@ -904,8 +970,10 @@ var configCommand = new Command5("config").description("View or modify Horus con
904
970
  console.log(` ${chalk5.bold("version:")} ${config.version}`);
905
971
  console.log(` ${chalk5.bold("data-dir:")} ${config.data_dir}`);
906
972
  console.log(` ${chalk5.bold("runtime:")} ${config.runtime}`);
907
- console.log(` ${chalk5.bold("host-repos-path:")} ${config.host_repos_path || chalk5.dim("(not set)")}`);
908
- 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)")}`);
909
977
  console.log(` ${chalk5.bold("github-token:")} ${config.github_token ? maskApiKey(config.github_token) : chalk5.dim("(not set)")}`);
910
978
  console.log("");
911
979
  console.log(chalk5.bold(" Ports:"));
@@ -966,6 +1034,7 @@ configCommand.command("set <key> <value>").description("Set a configuration valu
966
1034
  const needsRestart = [
967
1035
  "data-dir",
968
1036
  "host-repos-path",
1037
+ "host-repos-extra-scan-dirs",
969
1038
  "runtime",
970
1039
  "port.anvil",
971
1040
  "port.vault-rest",
@@ -999,6 +1068,7 @@ import { checkbox } from "@inquirer/prompts";
999
1068
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
1000
1069
  import { join as join4 } from "path";
1001
1070
  import { homedir as homedir3 } from "os";
1071
+ import { execa as execa2 } from "execa";
1002
1072
  function detectInstalledClients() {
1003
1073
  const detected = [];
1004
1074
  const home = homedir3();
@@ -1044,6 +1114,32 @@ function mergeAndWriteConfig(configPath, mcpServers) {
1044
1114
  mkdirSync3(dir, { recursive: true });
1045
1115
  writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
1046
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
+ }
1047
1143
  async function syncSkills(runtime) {
1048
1144
  const home = homedir3();
1049
1145
  const skillsBase = join4(home, ".claude", "skills");
@@ -1164,14 +1260,41 @@ var connectCommand = new Command6("connect").description("Configure Claude/Curso
1164
1260
  forge: { url: `http://${host}:${config.ports.forge}/sse` }
1165
1261
  };
1166
1262
  for (const target of targets) {
1167
- const configPath = getConfigPath(target);
1168
- const writeSpinner = ora5(`Configuring ${chalk6.cyan(target)}...`).start();
1169
- try {
1170
- mergeAndWriteConfig(configPath, mcpServers);
1171
- writeSpinner.succeed(`Configured ${chalk6.cyan(target)} \u2014 ${chalk6.dim(configPath)}`);
1172
- } catch (error) {
1173
- writeSpinner.fail(`Failed to configure ${target}`);
1174
- 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
+ }
1175
1298
  }
1176
1299
  }
1177
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.7",
3
+ "version": "0.1.9",
4
4
  "description": "CLI for managing the Horus AI development stack",
5
5
  "type": "module",
6
6
  "bin": {