skillhub/skillhub-cli/src/commands/resolve.ts
chenbaowang 2543cd52e7 fix(cli): make --registry parameter work for all commands
Fixed the issue where the --registry command-line parameter was defined
but not actually used by any commands.

Changes:
- Added loadConfigFromProgram() helper in config.ts to read registry from program.opts()
- Updated all 21 command files to use loadConfigFromProgram() instead of loadConfig()
- Ensured correct priority: CLI args > env vars > config file > defaults

Priority order:
1. --registry <url> (command-line flag) - highest priority
2. SKILLHUB_REGISTRY (environment variable)
3. ~/.skillhub/config.json (config file)
4. http://localhost:8080 (default value)

This allows users to temporarily override the registry without modifying
environment variables or config files, providing better flexibility for:
- Testing against different registries
- Multi-project/multi-environment setups
- CI/CD automation scripts

Updated commands:
- install, download, update, check, sync, uninstall
- explore, search
- publish, delete, archive, versions
- inspect, resolve, rating, rate, star, report, reviews
- whoami, me, namespaces, notifications
- transfer, hide, unhide

Fixes issue where `npx motovis-skillhub install <skill> --registry <url>`
would ignore the --registry parameter and use env vars or defaults instead.
2026-04-21 00:07:15 +08:00

178 lines
6 KiB
TypeScript

import { Command } from "commander";
import { ApiClient } from "../core/api-client.js";
import { loadConfig, loadConfigFromProgram } from "../core/config.js";
import { readToken } from "../core/auth-token.js";
import { success, error, info, dim } from "../utils/logger.js";
import { parseSkillName } from "../core/skill-name.js";
import { runInteractiveSearch, searchSkills } from "../core/interactive-search.js";
export interface ResolveResponse {
skillId: number;
namespace: string;
slug: string;
version: string;
versionId: number;
fingerprint: string;
matched: string;
downloadUrl: string;
}
interface VersionSearchResult {
namespace: string;
name: string;
exists: boolean;
}
async function resolveWithVersion(
client: ApiClient,
namespace: string,
slug: string,
version: string
): Promise<ResolveResponse | null> {
try {
const result = await client.get<ResolveResponse>(
`/api/v1/skills/${namespace}/${slug}/resolve?version=${version}`
);
return result;
} catch (e: any) {
const status = e.status || e.statusCode;
if (status === 404 || status === 400) return null;
throw e;
}
}
export function registerResolve(program: Command) {
program
.command("resolve <slug>")
.description("Resolve the latest version of a skill")
.option("-v, --skill-version <ver>", "Specific version")
.option("--tag <tag>", "Tag to resolve (default: latest, ignored if --skill-version)")
.option("--hash <hash>", "Content hash")
.action(async (slug: string, opts: Record<string, string>) => {
try {
const { namespace, slug: skillSlug } = parseSkillName(slug);
const config = loadConfigFromProgram(program);
const token = await readToken();
const client = new ApiClient({ baseUrl: config.registry, token: token || undefined });
let targetNamespace = namespace;
let targetSlug = skillSlug;
// Strip 'v' prefix if present (both 1.0.0 and v1.0.0 should work)
const specifiedVersion = opts.skillVersion?.replace(/^v/, "");
// Case 1: User specified a version
if (specifiedVersion) {
if (namespace && namespace !== "global") {
const result = await resolveWithVersion(client, namespace, skillSlug, specifiedVersion);
if (result) {
printResolveResult(result);
return;
}
error(`Version ${specifiedVersion} not found for ${namespace}/${skillSlug}`);
error(`Please check if the version number is correct.`);
process.exit(1);
}
const results = await searchSkills(client, skillSlug, 50);
const seen = new Set<string>();
const uniqueResults = results.filter((r) => {
const key = `${r.namespace}/${r.name}`;
if (!seen.has(key)) {
seen.add(key);
return true;
}
return false;
});
if (uniqueResults.length === 0) {
error(`Skill not found: ${skillSlug}`);
process.exit(1);
}
const resolvePromises = uniqueResults.map(async (r) => ({
...r,
result: await resolveWithVersion(client, r.namespace, r.name, specifiedVersion),
}));
const resolvedResults = await Promise.all(resolvePromises);
const matches = resolvedResults.filter((r) => r.result !== null);
if (matches.length === 0) {
error(`Version ${specifiedVersion} not found for ${skillSlug}`);
error(`Please check if the version number is correct.`);
process.exit(1);
}
if (matches.length === 1) {
// Only one match, auto-select
printResolveResult(matches[0].result!);
return;
}
// Multiple matches, list them for user to choose manually
info(`Found multiple skills with version ${specifiedVersion}:`);
for (const m of matches) {
console.log(` ${m.namespace}/${m.name}`);
}
dim(`\nUse: resolve <namespace>/<skill> --skill-version ${specifiedVersion}`);
process.exit(1);
}
// Case 2: No version specified (original behavior)
if (namespace === "global") {
const results = await searchSkills(client, skillSlug, 50);
const seen = new Set<string>();
const uniqueResults = results.filter((r) => {
const key = `${r.namespace}/${r.name}`;
if (!seen.has(key)) {
seen.add(key);
return true;
}
return false;
});
if (uniqueResults.length === 0) {
error(`Skill not found: ${skillSlug}`);
process.exit(1);
}
if (uniqueResults.length === 1) {
targetNamespace = uniqueResults[0].namespace;
targetSlug = uniqueResults[0].name;
} else {
const selected = await runInteractiveSearch(client, skillSlug);
if (!selected) {
info("Cancelled.");
return;
}
const [ns, name] = selected.split("/", 2);
targetNamespace = ns;
targetSlug = name;
}
}
const params = new URLSearchParams();
if (opts.tag) {
params.set("tag", opts.tag);
}
if (opts.hash) params.set("hash", opts.hash);
const qs = params.toString();
const path = `/api/v1/skills/${targetNamespace}/${targetSlug}/resolve${qs ? "?" + qs : ""}`;
const result = await client.get<ResolveResponse>(path);
printResolveResult(result);
} catch (e: any) {
error(`Failed: ${e.message}`);
process.exit(1);
}
});
}
function printResolveResult(result: ResolveResponse) {
info(`${result.slug}@${result.version}`);
dim(`Namespace: ${result.namespace}`);
dim(`Version ID: ${result.versionId}`);
dim(`Fingerprint: ${result.fingerprint}`);
dim(`Matched: ${result.matched}`);
dim(`Download URL: ${result.downloadUrl}`);
}