fix(cli): fix registry config priority for login and all commands

- Remove hardcoded default from Commander --registry option that
  overrode config file settings in ALL commands
- Login command now uses loadConfigFromProgram() like other commands
  instead of hardcoded http://localhost:8080
- Improved login error messages with registry URL context
This commit is contained in:
chenbaowang 2026-04-21 14:57:00 +08:00
parent d96ef86fac
commit d4ea485662
7 changed files with 215 additions and 14 deletions

View file

@ -5,6 +5,7 @@ unbuild.config.ts
tsconfig.json
*.test.ts
src/
.agents/
.claude/
.omc/
AGENTS.md

View file

@ -1,6 +1,6 @@
{
"name": "motovis-skillhub",
"version": "1.1.3",
"version": "1.2.0",
"type": "module",
"description": "SkillHub CLI - 企业级 Agent Skill 管理工具,支持命名空间",
"bin": {

View file

@ -58,7 +58,6 @@ function buildTopLevelHelp(version: string): string {
sections.push(formatSection("Discover", [
{ cmd: "explore", desc: "Browse or search skills from the registry", alias: "find, find-skills, search" },
{ cmd: "search <query>", desc: dim("[Deprecated: use 'explore' instead]") },
]));
sections.push("");
@ -108,12 +107,26 @@ function buildTopLevelHelp(version: string): string {
]));
sections.push("");
sections.push(formatSection("Configuration", [
{ cmd: "config list", desc: "Show current registry configuration" },
{ cmd: "config set <key> <value>", desc: "Set configuration (e.g., registry URL)" },
{ cmd: "config get <key>", desc: "Get configuration value" },
{ cmd: "config show-env-instructions", desc: "Show environment variable setup guide" },
]));
sections.push("");
sections.push(bold("Examples"));
sections.push(dim(" skillhub install vision2group/fork-workflow Install a skill"));
sections.push(dim(" skillhub explore Browse available skills"));
sections.push(dim(" skillhub publish Publish current directory"));
sections.push(dim(" skillhub me skills List your published skills"));
sections.push(dim(" skillhub update --global Update all global skills"));
sections.push(dim(" skillhub install vision2group/fork-workflow Install a skill from registry"));
sections.push(dim(" skillhub install find-skills --from https://... Install from GitHub or local path"));
sections.push(dim(" skillhub explore Interactive skill search"));
sections.push(dim(" skillhub explore --hot Browse popular skills"));
sections.push(dim(" skillhub config list Show current configuration"));
sections.push(dim(" skillhub --registry <url> explore One-time registry override"));
sections.push(dim(" skillhub publish Publish current directory"));
sections.push(dim(" skillhub me skills List your published skills"));
sections.push(dim(" skillhub update Update installed skills"));
sections.push("");
sections.push(dim("Run 'skillhub <command> --help' for command-specific options."));
sections.push("");
sections.push(bold("Global Options"));
@ -133,7 +146,7 @@ export async function createCli(): Promise<Command> {
.name("skillhub")
.description("CLI for SkillHub — publish, search, and manage agent skills")
.version(version)
.option("--registry <url>", "Registry API base URL", "http://localhost:8080")
.option("--registry <url>", "Registry API base URL")
.option("--json", "Output results as JSON");
const customHelp = buildTopLevelHelp(version);
@ -171,6 +184,7 @@ export async function createCli(): Promise<Command> {
{ registerExplore },
{ registerTransfer },
{ registerHide },
{ registerConfig },
] = await Promise.all([
import("./commands/login.js"),
import("./commands/logout.js"),
@ -199,6 +213,7 @@ export async function createCli(): Promise<Command> {
import("./commands/explore.js"),
import("./commands/transfer.js"),
import("./commands/hide.js"),
import("./commands/config.js"),
]);
registerLogin(program);
@ -229,6 +244,7 @@ export async function createCli(): Promise<Command> {
registerExplore(program);
registerTransfer(program);
registerHide(program);
registerConfig(program);
return program;
}

View file

@ -0,0 +1,155 @@
import { Command } from "commander";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { success, error, info, dim } from "../utils/logger.js";
const CONFIG_DIR = join(homedir(), ".skillhub");
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
export function registerConfig(program: Command) {
const configCmd = program
.command("config")
.description("Manage SkillHub CLI configuration")
.addHelpCommand(false);
configCmd
.command("list")
.description("List current configuration")
.action(() => {
const env = process.env.SKILLHUB_REGISTRY;
let fileConfig: { registry?: string } = {};
if (existsSync(CONFIG_FILE)) {
try {
fileConfig = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
} catch {
// Invalid config file, ignore
}
}
info("Current configuration:\n");
info(` ${cyan("Environment")}`);
info(` SKILLHUB_REGISTRY: ${env || dim("not set")}`);
info(`\n ${cyan("Config file")}`);
info(` ~/.skillhub/config.json: ${fileConfig.registry || dim("not set")}`);
info(`\n ${cyan("Default")}`);
info(` http://localhost:8080\n`);
const active = env || fileConfig.registry || "http://localhost:8080";
const source = env
? green("environment variable")
: fileConfig.registry
? yellow("config file")
: dim("default");
success(`Active registry: ${active}`);
info(`Source: ${source}`);
});
configCmd
.command("set <key> <value>")
.description("Set a configuration value (stored in ~/.skillhub/config.json)")
.action((key: string, value: string) => {
if (key === "registry") {
if (!existsSync(CONFIG_DIR)) {
mkdirSync(CONFIG_DIR, { recursive: true });
}
let config: Record<string, string> = {};
if (existsSync(CONFIG_FILE)) {
try {
config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
} catch {
// Invalid config file, start fresh
}
}
config.registry = value;
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
success(`Registry set to: ${value}`);
info(`Config file: ${CONFIG_FILE}`);
info(`\n💡 You can also use environment variable for current session:`);
info(` ` + cyan(`export SKILLHUB_REGISTRY="${value}"`));
info(`\n💡 Or use --registry flag for one-time override:`);
info(` ` + cyan(`skillhub --registry ${value} <command>`));
} else {
error(`Unknown config key: ${key}. Supported keys: registry`);
process.exitCode = 1;
}
});
configCmd
.command("get <key>")
.description("Get a configuration value")
.action((key: string) => {
if (key === "registry") {
const value = process.env.SKILLHUB_REGISTRY ||
(existsSync(CONFIG_FILE) ? (() => {
try {
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8")).registry;
} catch {
return null;
}
})() : null) ||
"http://localhost:8080";
success(value);
} else {
error(`Unknown config key: ${key}. Supported keys: registry`);
process.exitCode = 1;
}
});
configCmd
.command("show-env-instructions")
.description("Show how to set SKILLHUB_REGISTRY environment variable")
.action(() => {
info(`${yellow("Environment variable setup for SKILLHUB_REGISTRY:\n")}`);
info(`${cyan("🔹 Temporary (current session only):")}\n`);
info(` ${green("Linux/macOS:")}`);
info(` ${cyan(`export SKILLHUB_REGISTRY="http://<skillhub-ip>:<backend-port>"`)}`);
info(` ${dim("# Example: export SKILLHUB_REGISTRY=\"http://192.168.1.100:8080\"")}\n`);
info(` ${green("Windows CMD:")}`);
info(` ${cyan(`set SKILLHUB_REGISTRY=http://<skillhub-ip>:<backend-port>`)}`);
info(` ${dim("# Example: set SKILLHUB_REGISTRY=http://192.168.1.100:8080")}\n`);
info(` ${green("Windows PowerShell:")}`);
info(` ${cyan(`$env:SKILLHUB_REGISTRY="http://<skillhub-ip>:<backend-port>"`)}`);
info(` ${dim("# Example: $env:SKILLHUB_REGISTRY='http://192.168.1.100:8080'")}\n`);
info(`${cyan("🔹 Permanent (survives terminal restart):")}\n`);
info(` ${green("Linux/macOS (~/.bashrc or ~/.zshrc):")}`);
info(` ${cyan(`echo 'export SKILLHUB_REGISTRY="http://<ip>:<port>"' >> ~/.bashrc`)}`);
info(` ${cyan(`source ~/.bashrc`)}`);
info(` ${dim("# Add to ~/.bashrc for bash, ~/.zshrc for zsh")}\n`);
info(` ${green("Windows (User environment variable):")}`);
info(` ${cyan(`setx SKILLHUB_REGISTRY "http://<skillhub-ip>:<backend-port>"`)}`);
info(` ${dim("# Restart terminal after running this command")}\n`);
info(` ${green("PowerShell (User profile):")}`);
info(` ${cyan(`[System.Environment]::SetEnvironmentVariable('SKILLHUB_REGISTRY', 'http://<ip>:<port>', 'User')`)}`);
info(` ${dim("# Restart PowerShell after running this command")}\n`);
info(`${cyan("📋 Configuration priority (highest to lowest):")}`);
info(` 1. ${green("--registry flag")} (one-time, per command)`);
info(` 2. ${green("SKILLHUB_REGISTRY")} (environment variable)`);
info(` 3. ${green("~/.skillhub/config.json")} (config file)`);
info(` 4. ${dim("http://localhost:8080")} (default)\n`);
info(`${cyan("💡 Quick examples:")}`);
info(` skillhub config set registry http://192.168.1.100:8080`);
info(` skillhub --registry http://192.168.1.100:8080 explore`);
info(` skillhub config list\n`);
});
}

View file

@ -221,15 +221,28 @@ export function registerExplore(program: Command) {
.argument("[query]", "Search query for finding skills")
.option("-n, --limit <n>", "Max results", "20")
.option("-s, --sort <sort>", "Sort by: hot, newest, downloads (default: interactive mode)")
.action(async (query: string | undefined, opts: { limit: string; sort?: string }) => {
.option("--hot", "Sort by popularity (shorthand for --sort hot)")
.option("--newest", "Sort by newest first (shorthand for --sort newest)")
.option("--downloads", "Sort by download count (shorthand for --sort downloads)")
.action(async (query: string | undefined, opts: { limit: string; sort?: string; hot?: boolean; newest?: boolean; downloads?: boolean }) => {
const config = loadConfigFromProgram(program);
const token = await readToken();
const client = new ApiClient({ baseUrl: config.registry, token: token || undefined });
const sortMap: Record<string, string> = { hot: "rating", newest: "newest", downloads: "downloads" };
const apiSort = sortMap[opts.sort || "newest"] || "newest";
// Resolve sort priority: explicit --sort > shorthand flags > default
let effectiveSort = opts.sort;
if (!effectiveSort) {
if (opts.hot) effectiveSort = "hot";
else if (opts.newest) effectiveSort = "newest";
else if (opts.downloads) effectiveSort = "downloads";
}
const apiSort = sortMap[effectiveSort || "newest"] || "newest";
try {
if (!query && !opts.sort) {
// Enter interactive mode only if no query AND no sort option (explicit or shorthand)
const hasSortOption = opts.sort || opts.hot || opts.newest || opts.downloads;
if (!query && !hasSortOption) {
const selected = await runInteractiveSearch(client, "", apiSort);
if (!selected) {
console.log("\nCancelled.");

View file

@ -4,6 +4,7 @@ import { stdin, stdout } from "node:process";
import { writeToken } from "../core/auth-token.js";
import { ApiClient } from "../core/api-client.js";
import { ApiRoutes, WhoamiResponse } from "../schema/routes.js";
import { loadConfigFromProgram } from "../core/config.js";
import { success, error, info } from "../utils/logger.js";
export function registerLogin(program: Command) {
@ -19,15 +20,22 @@ export function registerLogin(program: Command) {
const token = opts.token || (await ask("Enter your SkillHub token: "));
rl.close();
const registry = opts.registry || "http://localhost:8080";
const client = new ApiClient({ baseUrl: registry, token });
const config = loadConfigFromProgram(program);
const registry = opts.registry || config.registry;
try {
const client = new ApiClient({ baseUrl: registry, token });
const resp = await client.get<WhoamiResponse>(ApiRoutes.whoami);
await writeToken(token);
success(`Authenticated as ${resp.user.displayName} (@${resp.user.handle})`);
} catch (e: any) {
error(`Authentication failed: ${e.message}`);
const detail = e.message || e.code || e.constructor?.name || String(e);
error(`Authentication failed: ${detail}`);
if (e.statusCode) {
info(`Registry: ${registry} (HTTP ${e.statusCode})`);
} else {
info(`Registry: ${registry} — connection or network error`);
}
process.exitCode = 1;
}
});

View file

@ -146,6 +146,14 @@ export class ApiError extends Error {
detail += "\nRun `skillhub login` to authenticate.";
}
// Enhanced error messages for connection issues
if (statusCode === 0 || detail.includes("ECONNREFUSED") || detail.includes("ENOTFOUND")) {
detail += "\n\n💡 Connection failed. Check your registry configuration:\n";
detail += " - Run 'skillhub config list' to see current configuration\n";
detail += " - Run 'skillhub config show-env-instructions' for setup guide\n";
detail += " - Or use: skillhub --registry <url> <command>";
}
super(detail);
}
}