mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* fix(release): extend standalone runtime download retries (#15672) Co-authored-by: Qwen-Coder <[email protected]> * docs(release): state retry-budget evidence without claiming the failing leg (#15672) The v0.26.2-preview.0 archive step aborted and passed on an unchanged re-run, but its job log was never retrievable, so raising the download retry budget from three to five attempts is defence-in-depth, not a confirmed fix for the download leg. Record that in the source comment and drop the same unconfirmed attribution from the retry test. * fix(release): correct retry-budget comment with attempt-1 log evidence The comment claimed the v0.26.2-preview.0 archive-step log was not retrievable and the failing leg unpinned. The job log is available via the Actions API and shows three consecutive 504s from the GitHub Releases CDN downloading the bun darwin-x64 runtime, so state the pinned leg directly. --------- Co-authored-by: Qwen Autofix <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: 易良 <[email protected]>
640 lines
19 KiB
JavaScript
640 lines
19 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2025 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { Readable } from 'node:stream';
|
|
import { pipeline } from 'node:stream/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { parse as parseYaml } from 'yaml';
|
|
import { stageOpenTuiPackages } from './opentui-package-assets.js';
|
|
import {
|
|
TARGET_CLIPBOARD_PACKAGE,
|
|
standaloneArchiveName,
|
|
writeSha256Sums,
|
|
} from './create-standalone-package.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const rootDir = path.resolve(__dirname, '..');
|
|
|
|
const RELEASE_TARGETS = [
|
|
{
|
|
qwenTarget: 'darwin-arm64',
|
|
nodeTarget: 'darwin-arm64',
|
|
nodeArchiveExtension: 'tar.gz',
|
|
bunAsset: 'bun-darwin-aarch64',
|
|
},
|
|
{
|
|
qwenTarget: 'darwin-x64',
|
|
nodeTarget: 'darwin-x64',
|
|
nodeArchiveExtension: 'tar.gz',
|
|
bunAsset: 'bun-darwin-x64',
|
|
},
|
|
{
|
|
qwenTarget: 'linux-arm64',
|
|
nodeTarget: 'linux-arm64',
|
|
nodeArchiveExtension: 'tar.xz',
|
|
bunAsset: 'bun-linux-aarch64',
|
|
},
|
|
{
|
|
qwenTarget: 'linux-x64',
|
|
nodeTarget: 'linux-x64',
|
|
nodeArchiveExtension: 'tar.xz',
|
|
bunAsset: 'bun-linux-x64',
|
|
},
|
|
{
|
|
qwenTarget: 'win-x64',
|
|
nodeTarget: 'win-x64',
|
|
nodeArchiveExtension: 'zip',
|
|
bunAsset: 'bun-windows-x64',
|
|
},
|
|
];
|
|
// Classic Node.js packaging stays the default. --runtime=bun opts into the
|
|
// temporary OpenTUI preview (the renderer needs bun:ffi; Node without FFI
|
|
// falls back to ink). --include-opentui-preview adds the bun flavor's
|
|
// archives (suffixed -opentui-preview) to the same release directory.
|
|
const DEFAULT_RUNTIME = 'node';
|
|
const DEFAULT_BUN_VERSION = '1.4.4';
|
|
const BUN_RELEASE_BASE_URL = 'https://github.com/oven-sh/bun/releases/download';
|
|
// The runtime downloads are the publish job's least reliable leg: one
|
|
// transient download failure aborted the v0.23.4 publish while the
|
|
// unchanged re-run passed. The v0.26.2-preview.0 archive step aborted the
|
|
// same way: its job log shows three consecutive 504s from the GitHub
|
|
// Releases CDN while downloading the bun darwin-x64 runtime, and the
|
|
// unchanged re-run passed. Five attempts back off to ~75s to ride out
|
|
// longer transient bursts.
|
|
const MAX_DOWNLOAD_ATTEMPTS = 5;
|
|
const INITIAL_DOWNLOAD_BACKOFF_MS = 5_000;
|
|
const DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
|
|
// Temporary OpenTUI preview: the bundled OpenTUI backend resolves its native
|
|
// render library at runtime via `import('@opentui/core-<platform>-<arch>')`,
|
|
// so each standalone archive must ship the matching platform package(s).
|
|
// Stage every platform variant (like the clipboard addons) because release
|
|
// packaging cross-builds all targets from a single host. Linux is glibc-only
|
|
// on purpose: RELEASE_TARGETS bundles glibc-linked Bun binaries that cannot
|
|
// start on musl hosts, so the -musl render packages would be dead weight
|
|
// claiming support the archive cannot deliver.
|
|
const OPENTUI_PLATFORM_PACKAGES = [
|
|
'@opentui/core-darwin-arm64',
|
|
'@opentui/core-darwin-x64',
|
|
'@opentui/core-linux-arm64',
|
|
'@opentui/core-linux-x64',
|
|
'@opentui/core-win32-arm64',
|
|
'@opentui/core-win32-x64',
|
|
];
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help) {
|
|
printUsage();
|
|
return;
|
|
}
|
|
|
|
const runtime = args.runtime || DEFAULT_RUNTIME;
|
|
if (runtime !== 'node' && runtime !== 'bun') {
|
|
fail('--runtime must be either "node" or "bun"');
|
|
}
|
|
// The bun flavor is additive: with --include-opentui-preview the release
|
|
// directory carries both the classic Node.js archives and the bun/OpenTUI
|
|
// preview archives; every downstream check derives its expected set from
|
|
// this list.
|
|
const flavors =
|
|
args.includeOpentuiPreview && runtime !== 'bun'
|
|
? ['node', 'bun']
|
|
: [runtime];
|
|
|
|
const nodeVersion = args.nodeVersion || process.versions.node;
|
|
const bunVersion = args.bunVersion || DEFAULT_BUN_VERSION;
|
|
const outDir = path.resolve(
|
|
args.outDir || path.join(rootDir, 'dist', 'standalone'),
|
|
);
|
|
const runtimeParent = path.resolve(
|
|
args.runtimeDir || process.env.RUNNER_TEMP || os.tmpdir(),
|
|
);
|
|
fs.mkdirSync(runtimeParent, { recursive: true });
|
|
const runtimeDir = fs.mkdtempSync(
|
|
path.join(runtimeParent, `qwen-${runtime}-runtime-`),
|
|
);
|
|
const nodeDistUrl = `https://nodejs.org/dist/v${nodeVersion}`;
|
|
const bunDistUrl = `${BUN_RELEASE_BASE_URL}/bun-v${bunVersion}`;
|
|
|
|
try {
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
// Each flavor verifies its runtime archive against its own publisher's
|
|
// checksum list (Node.js SHASUMS256.txt vs Bun's), so fetch one per flavor.
|
|
const checksums = {};
|
|
for (const flavor of flavors) {
|
|
checksums[flavor] = await downloadRuntimeChecksums({
|
|
runtime: flavor,
|
|
distUrl: flavor === 'bun' ? bunDistUrl : nodeDistUrl,
|
|
checksumsPath: path.join(runtimeDir, `${flavor}-SHASUMS256.txt`),
|
|
expectedArchives: RELEASE_TARGETS.map((target) =>
|
|
runtimeArchiveName({ ...target, runtime: flavor, nodeVersion }),
|
|
),
|
|
});
|
|
}
|
|
const nativeModulesDir = stageNativeModules(runtimeDir);
|
|
// Only the bun runtime consumes the staged OpenTUI packages; the classic
|
|
// Node packaging must not install them (nor fail on a missing lockfile
|
|
// entry) at all.
|
|
const opentuiModulesDir = flavors.includes('bun')
|
|
? stageOpenTuiPackages(rootDir, runtimeDir, OPENTUI_PLATFORM_PACKAGES)
|
|
: undefined;
|
|
|
|
for (const flavor of flavors) {
|
|
for (const target of RELEASE_TARGETS) {
|
|
await packageTarget({
|
|
...target,
|
|
runtime: flavor,
|
|
bunDistUrl,
|
|
nodeDistUrl,
|
|
nodeVersion,
|
|
outDir,
|
|
releaseVersion: args.version,
|
|
runtimeDir,
|
|
checksums: checksums[flavor],
|
|
nativeModulesDir,
|
|
opentuiModulesDir,
|
|
});
|
|
}
|
|
}
|
|
|
|
await writeSha256Sums(outDir);
|
|
assertStandaloneOutput(outDir, flavors);
|
|
} finally {
|
|
fs.rmSync(runtimeDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function isMainModule() {
|
|
return process.argv[1] && path.resolve(process.argv[1]) === __filename;
|
|
}
|
|
|
|
function runtimeArchiveName({
|
|
runtime,
|
|
nodeVersion,
|
|
nodeTarget,
|
|
nodeArchiveExtension,
|
|
bunAsset,
|
|
}) {
|
|
return runtime === 'bun'
|
|
? `${bunAsset}.zip`
|
|
: `node-v${nodeVersion}-${nodeTarget}.${nodeArchiveExtension}`;
|
|
}
|
|
|
|
function runtimeLabel(runtime) {
|
|
return runtime === 'bun' ? 'Bun' : 'Node.js';
|
|
}
|
|
|
|
async function packageTarget({
|
|
qwenTarget,
|
|
nodeTarget,
|
|
nodeArchiveExtension,
|
|
bunAsset,
|
|
runtime,
|
|
bunDistUrl,
|
|
nodeDistUrl,
|
|
nodeVersion,
|
|
outDir,
|
|
releaseVersion,
|
|
runtimeDir,
|
|
checksums,
|
|
nativeModulesDir,
|
|
opentuiModulesDir,
|
|
}) {
|
|
const archiveName = runtimeArchiveName({
|
|
runtime,
|
|
nodeVersion,
|
|
nodeTarget,
|
|
nodeArchiveExtension,
|
|
bunAsset,
|
|
});
|
|
const archiveUrlBase = runtime === 'bun' ? bunDistUrl : nodeDistUrl;
|
|
const archivePath = path.join(runtimeDir, archiveName);
|
|
|
|
await downloadRuntimeArchive({
|
|
archiveUrl: `${archiveUrlBase}/${archiveName}`,
|
|
archivePath,
|
|
archiveName,
|
|
checksums,
|
|
label: runtimeLabel(runtime),
|
|
});
|
|
|
|
const args = [
|
|
'scripts/create-standalone-package.js',
|
|
'--target',
|
|
qwenTarget,
|
|
'--node-archive',
|
|
archivePath,
|
|
'--native-modules-dir',
|
|
nativeModulesDir,
|
|
'--out-dir',
|
|
outDir,
|
|
'--skip-checksums',
|
|
];
|
|
if (runtime === 'bun') {
|
|
args.push('--runtime', 'bun');
|
|
args.push('--opentui-modules-dir', opentuiModulesDir);
|
|
}
|
|
if (releaseVersion) {
|
|
args.push('--version', releaseVersion);
|
|
}
|
|
|
|
execFileSync(process.execPath, args, {
|
|
cwd: rootDir,
|
|
stdio: 'inherit',
|
|
});
|
|
}
|
|
|
|
// The release installs from pnpm-lock.yaml, so that is where every target's
|
|
// native package version comes from. Only the build host's own platform
|
|
// package is installed, so the installed tree cannot answer for the others.
|
|
let pnpmLockedKeys;
|
|
function readPnpmLockedVersion(packageName) {
|
|
pnpmLockedKeys ??= Object.keys(
|
|
parseYaml(fs.readFileSync(path.join(rootDir, 'pnpm-lock.yaml'), 'utf8'))
|
|
?.packages ?? {},
|
|
);
|
|
const versions = pnpmLockedKeys
|
|
.filter((key) => key.startsWith(`${packageName}@`))
|
|
.map((key) => key.slice(packageName.length + 1));
|
|
// Two locked versions would leave the staged one ambiguous; fail instead.
|
|
return versions.length === 1 ? versions[0] : undefined;
|
|
}
|
|
|
|
function readClipboardPackageSpecs() {
|
|
const cliPackage = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(rootDir, 'packages', 'cli', 'package.json'),
|
|
'utf8',
|
|
),
|
|
);
|
|
const packageNames = [
|
|
'@teddyzhu/clipboard',
|
|
...new Set(TARGET_CLIPBOARD_PACKAGE.values()),
|
|
];
|
|
|
|
return packageNames.map((packageName) => {
|
|
const version = readPnpmLockedVersion(packageName);
|
|
const declaredVersion = cliPackage.optionalDependencies?.[packageName];
|
|
if (!version || ![version, `^${version}`].includes(declaredVersion)) {
|
|
fail(`Clipboard package version is not locked for ${packageName}`);
|
|
}
|
|
return `${packageName}@${version}`;
|
|
});
|
|
}
|
|
|
|
// node-pty pins live in the root package.json optionalDependencies (mirrored
|
|
// in packages/core). The wrapper plus every pinned platform package must be
|
|
// staged because release packaging cross-builds all targets from a single
|
|
// host — the host's own node_modules only ever carries one platform's
|
|
// prebuild, and getPty() resolves the platform package from node_modules at
|
|
// runtime (#11872). Deriving the list from the root manifest keeps it in sync
|
|
// when a platform pin is added (e.g. linux-arm64).
|
|
function readNodePtyPackageSpecs() {
|
|
const rootPackage = JSON.parse(
|
|
fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'),
|
|
);
|
|
const packageNames = Object.keys(
|
|
rootPackage.optionalDependencies ?? {},
|
|
).filter((packageName) => packageName.startsWith('@lydell/node-pty'));
|
|
|
|
return packageNames.map((packageName) => {
|
|
const version = readPnpmLockedVersion(packageName);
|
|
if (!version) {
|
|
fail(`node-pty package version is not locked for ${packageName}`);
|
|
}
|
|
return `${packageName}@${version}`;
|
|
});
|
|
}
|
|
|
|
function stageNativeModules(runtimeDir) {
|
|
const installDir = path.join(runtimeDir, 'clipboard-modules');
|
|
fs.mkdirSync(installDir, { recursive: true });
|
|
console.log('Staging standalone native packages (clipboard, node-pty)');
|
|
const npmExecPath = process.env.npm_execpath;
|
|
if (!npmExecPath) {
|
|
fail('npm_execpath is unavailable; run package:standalone:release via npm');
|
|
}
|
|
execFileSync(
|
|
process.execPath,
|
|
[
|
|
npmExecPath,
|
|
'install',
|
|
'--prefix',
|
|
installDir,
|
|
'--package-lock=false',
|
|
'--no-save',
|
|
'--ignore-scripts',
|
|
'--force',
|
|
'--no-audit',
|
|
'--no-fund',
|
|
...readClipboardPackageSpecs(),
|
|
...readNodePtyPackageSpecs(),
|
|
],
|
|
{
|
|
cwd: rootDir,
|
|
stdio: 'inherit',
|
|
},
|
|
);
|
|
return path.join(installDir, 'node_modules');
|
|
}
|
|
|
|
async function downloadFile(url, destination, { fetchImpl = fetch } = {}) {
|
|
console.log(`Downloading ${url}`);
|
|
const response = await fetchImpl(url, {
|
|
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
});
|
|
if (!response.ok) {
|
|
fail(
|
|
`Failed to download ${url}: ${response.status} ${response.statusText}`,
|
|
);
|
|
}
|
|
if (!response.body) {
|
|
fail(`Failed to download ${url}: response body was empty`);
|
|
}
|
|
await pipeline(
|
|
Readable.fromWeb(response.body),
|
|
fs.createWriteStream(destination),
|
|
);
|
|
}
|
|
|
|
// verify runs after each attempt: an integrity failure means the bytes on
|
|
// disk are bad, so the retry re-downloads instead of reusing them.
|
|
async function downloadWithRetry(
|
|
url,
|
|
destination,
|
|
{
|
|
verify,
|
|
fetchImpl = fetch,
|
|
sleepImpl = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
} = {},
|
|
) {
|
|
for (let attempt = 1; attempt <= MAX_DOWNLOAD_ATTEMPTS; attempt += 1) {
|
|
try {
|
|
await downloadFile(url, destination, { fetchImpl });
|
|
await verify?.();
|
|
return;
|
|
} catch (error) {
|
|
if (attempt >= MAX_DOWNLOAD_ATTEMPTS) {
|
|
throw error;
|
|
}
|
|
const delayMs = INITIAL_DOWNLOAD_BACKOFF_MS * 2 ** (attempt - 1);
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
// A real undici network failure reads only "fetch failed"; the
|
|
// discriminating detail (ECONNRESET, ENOTFOUND, ...) is on the cause.
|
|
const cause =
|
|
error instanceof Error && error.cause instanceof Error
|
|
? error.cause.message
|
|
: undefined;
|
|
console.warn(
|
|
`Download attempt ${attempt}/${MAX_DOWNLOAD_ATTEMPTS} failed for ${url}: ${message}${
|
|
cause ? ` (cause: ${cause})` : ''
|
|
}, retrying in ${delayMs / 1000}s...`,
|
|
);
|
|
await sleepImpl(delayMs);
|
|
}
|
|
}
|
|
}
|
|
|
|
// The archive leg's download-and-verify wiring, lifted out of packageTarget so
|
|
// tests pin it behaviourally (a rejected download must be re-fetched) instead
|
|
// of matching its source spelling.
|
|
async function downloadRuntimeArchive({
|
|
archiveUrl,
|
|
archivePath,
|
|
archiveName,
|
|
checksums,
|
|
label,
|
|
fetchImpl,
|
|
sleepImpl,
|
|
}) {
|
|
await downloadWithRetry(archiveUrl, archivePath, {
|
|
verify: () => verifyNodeArchive(archivePath, archiveName, checksums, label),
|
|
fetchImpl,
|
|
sleepImpl,
|
|
});
|
|
}
|
|
|
|
// The checksum list is the one download the archive legs cannot re-fetch, so
|
|
// verify it inside the retry: a corrupt or truncated list is downloaded again
|
|
// instead of poisoning every archive check that shares the parsed map.
|
|
async function downloadRuntimeChecksums({
|
|
runtime,
|
|
distUrl,
|
|
checksumsPath,
|
|
expectedArchives,
|
|
fetchImpl,
|
|
sleepImpl,
|
|
}) {
|
|
let checksums;
|
|
await downloadWithRetry(`${distUrl}/SHASUMS256.txt`, checksumsPath, {
|
|
verify: () => {
|
|
checksums = parseChecksums(fs.readFileSync(checksumsPath, 'utf8'));
|
|
const missing = expectedArchives.filter(
|
|
(archiveName) => !checksums.has(archiveName),
|
|
);
|
|
if (missing.length > 0) {
|
|
fail(
|
|
`${runtimeLabel(runtime)} SHASUMS256.txt does not list ${missing.join(', ')}`,
|
|
);
|
|
}
|
|
},
|
|
fetchImpl,
|
|
sleepImpl,
|
|
});
|
|
return checksums;
|
|
}
|
|
|
|
function parseChecksums(content) {
|
|
const checksums = new Map();
|
|
for (const line of content.split(/\r?\n/)) {
|
|
const [hash, fileName] = line.trim().split(/\s+/, 2);
|
|
if (hash && fileName) {
|
|
checksums.set(fileName.replace(/^\*/, ''), hash);
|
|
}
|
|
}
|
|
return checksums;
|
|
}
|
|
|
|
async function verifyNodeArchive(archivePath, archiveName, checksums, label) {
|
|
const displayLabel = label || 'Node.js';
|
|
const expected = checksums.get(archiveName);
|
|
if (!expected) {
|
|
fail(`${displayLabel} SHASUMS256.txt does not list ${archiveName}`);
|
|
}
|
|
|
|
const actual = await sha256File(archivePath);
|
|
if (actual !== expected) {
|
|
fail(`Checksum verification failed for ${archiveName}`);
|
|
}
|
|
|
|
console.log(`Verified ${displayLabel} runtime checksum for ${archiveName}`);
|
|
}
|
|
|
|
async function sha256File(filePath) {
|
|
const hash = crypto.createHash('sha256');
|
|
await pipeline(fs.createReadStream(filePath), hash);
|
|
return hash.digest('hex');
|
|
}
|
|
|
|
function assertStandaloneOutput(outDir, runtimes = ['node']) {
|
|
const checksumPath = path.join(outDir, 'SHA256SUMS');
|
|
if (!fs.existsSync(checksumPath)) {
|
|
fail(`Standalone SHA256SUMS was not created at ${checksumPath}`);
|
|
}
|
|
|
|
const expectedArchiveNames = RELEASE_TARGETS.flatMap(({ qwenTarget }) =>
|
|
runtimes.map((runtime) => standaloneArchiveName(qwenTarget, runtime)),
|
|
).sort();
|
|
const archiveNames = fs
|
|
.readFileSync(checksumPath, 'utf8')
|
|
.split(/\r?\n/)
|
|
.filter((line) => /^[0-9a-f]{64}\s+/.test(line))
|
|
.map((line) => line.trim().split(/\s+/, 2)[1]?.replace(/^\*/, ''))
|
|
.filter(Boolean)
|
|
.sort();
|
|
const missing = expectedArchiveNames.filter(
|
|
(archiveName) => !archiveNames.includes(archiveName),
|
|
);
|
|
const extra = archiveNames.filter(
|
|
(archiveName) => !expectedArchiveNames.includes(archiveName),
|
|
);
|
|
|
|
if (
|
|
archiveNames.length !== expectedArchiveNames.length ||
|
|
missing.length > 0 ||
|
|
extra.length > 0
|
|
) {
|
|
fail(
|
|
[
|
|
`Expected standalone checksums for ${expectedArchiveNames.join(', ')}`,
|
|
`found ${archiveNames.join(', ') || 'none'}.`,
|
|
missing.length > 0 ? `Missing: ${missing.join(', ')}.` : '',
|
|
extra.length > 0 ? `Extra: ${extra.join(', ')}.` : '',
|
|
]
|
|
.filter(Boolean)
|
|
.join(' '),
|
|
);
|
|
}
|
|
|
|
console.log(`Verified ${archiveNames.length} standalone release checksums.`);
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = {
|
|
help: false,
|
|
includeOpentuiPreview: false,
|
|
nodeVersion: undefined,
|
|
bunVersion: undefined,
|
|
runtime: undefined,
|
|
outDir: undefined,
|
|
runtimeDir: undefined,
|
|
version: undefined,
|
|
};
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
switch (arg) {
|
|
case '--help':
|
|
case '-h':
|
|
args.help = true;
|
|
break;
|
|
case '--include-opentui-preview':
|
|
args.includeOpentuiPreview = true;
|
|
break;
|
|
case '--node-version':
|
|
args.nodeVersion = readOptionValue(argv, index, arg);
|
|
index += 1;
|
|
break;
|
|
case '--bun-version':
|
|
args.bunVersion = readOptionValue(argv, index, arg);
|
|
index += 1;
|
|
break;
|
|
case '--runtime':
|
|
args.runtime = readOptionValue(argv, index, arg);
|
|
index += 1;
|
|
break;
|
|
case '--out-dir':
|
|
args.outDir = readOptionValue(argv, index, arg);
|
|
index += 1;
|
|
break;
|
|
case '--runtime-dir':
|
|
args.runtimeDir = readOptionValue(argv, index, arg);
|
|
index += 1;
|
|
break;
|
|
case '--version':
|
|
args.version = readOptionValue(argv, index, arg);
|
|
index += 1;
|
|
break;
|
|
default:
|
|
fail(`Unknown option: ${arg}`);
|
|
}
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
function readOptionValue(argv, index, optionName) {
|
|
const value = argv[index + 1];
|
|
if (!value || value.startsWith('-')) {
|
|
fail(`${optionName} requires a value`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function printUsage() {
|
|
console.log(`
|
|
Usage:
|
|
npm run package:standalone:release -- [OPTIONS]
|
|
|
|
Options:
|
|
--version VERSION Release version written to standalone manifests.
|
|
--out-dir PATH Output directory. Defaults to dist/standalone.
|
|
--runtime-dir PATH Temporary Node.js runtime download directory.
|
|
--node-version VERSION Node.js version to download. Defaults to current Node.
|
|
--runtime RUNTIME Runtime to bundle: "node" (classic packaging) or
|
|
"bun" (temporary OpenTUI preview).
|
|
--include-opentui-preview
|
|
Also build the bun/OpenTUI preview archives
|
|
(qwen-code-*-opentui-preview.*) into the same output
|
|
directory, alongside the classic archives.
|
|
--bun-version VERSION Bun version to download. Defaults to ${DEFAULT_BUN_VERSION}.
|
|
`);
|
|
}
|
|
|
|
function fail(message) {
|
|
throw new Error(`ERROR: ${message}`);
|
|
}
|
|
|
|
export {
|
|
assertStandaloneOutput,
|
|
downloadRuntimeArchive,
|
|
downloadRuntimeChecksums,
|
|
downloadWithRetry,
|
|
parseChecksums,
|
|
readClipboardPackageSpecs,
|
|
readNodePtyPackageSpecs,
|
|
RELEASE_TARGETS,
|
|
runtimeArchiveName,
|
|
};
|
|
|
|
if (isMainModule()) {
|
|
try {
|
|
await main();
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|