mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-15 23:31:10 +00:00
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:
parent
d96ef86fac
commit
d4ea485662
7 changed files with 215 additions and 14 deletions
|
|
@ -5,6 +5,7 @@ unbuild.config.ts
|
|||
tsconfig.json
|
||||
*.test.ts
|
||||
src/
|
||||
.agents/
|
||||
.claude/
|
||||
.omc/
|
||||
AGENTS.md
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "motovis-skillhub",
|
||||
"version": "1.1.3",
|
||||
"version": "1.2.0",
|
||||
"type": "module",
|
||||
"description": "SkillHub CLI - 企业级 Agent Skill 管理工具,支持命名空间",
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
155
skillhub-cli/src/commands/config.ts
Normal file
155
skillhub-cli/src/commands/config.ts
Normal 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`);
|
||||
});
|
||||
}
|
||||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue