diff --git a/skillhub-cli/.npmignore b/skillhub-cli/.npmignore index f8d3a0da..fe3e5ff2 100644 --- a/skillhub-cli/.npmignore +++ b/skillhub-cli/.npmignore @@ -5,6 +5,7 @@ unbuild.config.ts tsconfig.json *.test.ts src/ +.agents/ .claude/ .omc/ AGENTS.md diff --git a/skillhub-cli/package.json b/skillhub-cli/package.json index 9d16fcdd..ef491bf7 100644 --- a/skillhub-cli/package.json +++ b/skillhub-cli/package.json @@ -1,6 +1,6 @@ { "name": "motovis-skillhub", - "version": "1.1.3", + "version": "1.2.0", "type": "module", "description": "SkillHub CLI - 企业级 Agent Skill 管理工具,支持命名空间", "bin": { diff --git a/skillhub-cli/src/cli.ts b/skillhub-cli/src/cli.ts index 4e4c736f..5367e392 100644 --- a/skillhub-cli/src/cli.ts +++ b/skillhub-cli/src/cli.ts @@ -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 ", 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 ", desc: "Set configuration (e.g., registry URL)" }, + { cmd: "config get ", 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 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 --help' for command-specific options.")); sections.push(""); sections.push(bold("Global Options")); @@ -133,7 +146,7 @@ export async function createCli(): Promise { .name("skillhub") .description("CLI for SkillHub — publish, search, and manage agent skills") .version(version) - .option("--registry ", "Registry API base URL", "http://localhost:8080") + .option("--registry ", "Registry API base URL") .option("--json", "Output results as JSON"); const customHelp = buildTopLevelHelp(version); @@ -171,6 +184,7 @@ export async function createCli(): Promise { { registerExplore }, { registerTransfer }, { registerHide }, + { registerConfig }, ] = await Promise.all([ import("./commands/login.js"), import("./commands/logout.js"), @@ -199,6 +213,7 @@ export async function createCli(): Promise { 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 { registerExplore(program); registerTransfer(program); registerHide(program); + registerConfig(program); return program; } diff --git a/skillhub-cli/src/commands/config.ts b/skillhub-cli/src/commands/config.ts new file mode 100644 index 00000000..e518a707 --- /dev/null +++ b/skillhub-cli/src/commands/config.ts @@ -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 ") + .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 = {}; + 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} `)); + } else { + error(`Unknown config key: ${key}. Supported keys: registry`); + process.exitCode = 1; + } + }); + + configCmd + .command("get ") + .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://:"`)}`); + info(` ${dim("# Example: export SKILLHUB_REGISTRY=\"http://192.168.1.100:8080\"")}\n`); + + info(` ${green("Windows CMD:")}`); + info(` ${cyan(`set SKILLHUB_REGISTRY=http://:`)}`); + info(` ${dim("# Example: set SKILLHUB_REGISTRY=http://192.168.1.100:8080")}\n`); + + info(` ${green("Windows PowerShell:")}`); + info(` ${cyan(`$env:SKILLHUB_REGISTRY="http://:"`)}`); + 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://:"' >> ~/.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://:"`)}`); + info(` ${dim("# Restart terminal after running this command")}\n`); + + info(` ${green("PowerShell (User profile):")}`); + info(` ${cyan(`[System.Environment]::SetEnvironmentVariable('SKILLHUB_REGISTRY', 'http://:', '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`); + }); +} diff --git a/skillhub-cli/src/commands/explore.ts b/skillhub-cli/src/commands/explore.ts index cd60d57f..b9e44d84 100644 --- a/skillhub-cli/src/commands/explore.ts +++ b/skillhub-cli/src/commands/explore.ts @@ -221,15 +221,28 @@ export function registerExplore(program: Command) { .argument("[query]", "Search query for finding skills") .option("-n, --limit ", "Max results", "20") .option("-s, --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 = { 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."); diff --git a/skillhub-cli/src/commands/login.ts b/skillhub-cli/src/commands/login.ts index a236b19f..4e72f621 100644 --- a/skillhub-cli/src/commands/login.ts +++ b/skillhub-cli/src/commands/login.ts @@ -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(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; } }); diff --git a/skillhub-cli/src/core/api-client.ts b/skillhub-cli/src/core/api-client.ts index bf94edc9..cd5850a8 100644 --- a/skillhub-cli/src/core/api-client.ts +++ b/skillhub-cli/src/core/api-client.ts @@ -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 "; + } + super(detail); } }