fix(cli): handle 302 redirect in download and fix --skill-version option parsing

- undici does not auto-follow HTTP redirects, manually follow 302/307/308
  to MinIO presigned URL before piping response body
- Commander.js parses --skill-version as skillVersion (camelCase), not
  skill-version (kebab-case), fix option access in install and download
  commands
This commit is contained in:
chenbaowang 2026-04-16 10:16:33 +08:00
parent 8c87cc5fe4
commit af6ebf3058
2 changed files with 26 additions and 7 deletions

View file

@ -28,18 +28,27 @@ export function registerDownload(program: Command) {
const spinner = ora(`Downloading ${skillSlug} from ${namespace}`).start();
let downloadUrl = `${ApiRoutes.skillDownload.replace("{namespace}", namespace).replace("{slug}", skillSlug)}`;
if (opts["skill-version"]) {
downloadUrl = `/api/v1/skills/${namespace}/${skillSlug}/versions/${opts["skill-version"]}/download`;
if (opts.skillVersion) {
downloadUrl = `/api/v1/skills/${namespace}/${skillSlug}/versions/${opts.skillVersion}/download`;
} else if (opts.tag) {
downloadUrl = `/api/v1/skills/${namespace}/${skillSlug}/tags/${opts.tag}/download`;
}
const { request } = await import("undici");
const url = new URL(downloadUrl, config.registry);
const { statusCode, body } = await request(url.toString(), {
let response = await request(url.toString(), {
method: "GET",
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
if (response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308) {
const location = response.headers.location;
if (!location) {
spinner.fail(`Redirect response has no Location header`);
process.exit(1);
}
response = await request(location, { method: "GET" });
}
const { statusCode, body } = response;
if (statusCode >= 400) {
spinner.fail(`Download failed: HTTP ${statusCode}`);

View file

@ -240,9 +240,8 @@ async function installFromRegistry(slug: string, opts: Record<string, string | s
// Present version selection
let selectedVersion: string = "latest";
if (opts.yes && opts["skill-version"]) {
// Non-interactive: use command-line version if provided
selectedVersion = opts["skill-version"] as string;
if (opts.yes && opts.skillVersion) {
selectedVersion = String(opts.skillVersion);
} else if (opts.yes && opts.tag) {
// Non-interactive: resolve tag to version
for (const [vid, tags] of versionTagsMap) {
@ -285,10 +284,21 @@ async function installFromRegistry(slug: string, opts: Record<string, string | s
spinner.text = "Downloading";
const { request } = await import("undici");
const { statusCode, body } = await request(downloadUrl, {
let response = await request(downloadUrl, {
method: "GET",
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
// undici 不自动跟随 redirect手动处理 302/307/308
if (response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308) {
const location = response.headers.location;
if (!location) {
spinner.fail(`Redirect response has no Location header`);
await rm(tmpDir, { recursive: true, force: true });
process.exit(1);
}
response = await request(location, { method: "GET" });
}
const { statusCode, body } = response;
if (statusCode >= 400) {
spinner.fail(`Skill not found: ${ns}/${actualSlug}`);