@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.
- package/compose/docker-compose.yml +3 -2
- package/dist/index.js +111 -14
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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:")}
|
|
934
|
-
|
|
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
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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")) {
|