import ora from 'ora' import { lookupAssets } from '../refs.js' import semver from 'semver' import { downloadAssetZipBytes } from '../client.js' import { getCliClient } from '../cli-client.js' import { fileManifestForRange, packAsset, packPolicyFromInstallMetadata, parsePackDependencies, } from '../pack.js' import { packWarnings, unchangedUploadResult, uploadResult } from '../output.js' import { assetDescriptionSchema, assetNameSchema, semverSchema, MAX_ASSET_DESCRIPTION_LENGTH, type AssetAccess, type AssetType, } from '../schemas.js' export interface UploadCommandOptions { type: AssetType version?: string cwd?: string baseUrl?: string /** npm dependencies, each `name@range` (range defaults to `*`). */ npm?: string[] /** asset dependencies, each `name@range` (range defaults to `*`). */ asset?: string[] /** skill dependencies, each `label=source` passed to `skills add`. */ skill?: string[] /** Requested visibility; omitted lets the server resolve it from entitlement. */ access?: AssetAccess } export async function uploadCommand( name: string, zipFilter: string, description: string, opts: UploadCommandOptions, ): Promise { const spinner = ora({ text: 'Preparing upload', isEnabled: Boolean(process.stderr.isTTY), isSilent: !process.stderr.isTTY, }).start() try { const parsedName = assetNameSchema.parse(name) const parsedVersion = opts.version ? semverSchema.parse(opts.version) : undefined const parsedDescription = parseUploadDescription(description) // Parse dep flags before any network work so a malformed spec fails fast. const dependencies = parsePackDependencies({ npm: opts.npm, asset: opts.asset, skill: opts.skill, }) const cwd = opts.cwd ?? process.cwd() const type = opts.type const { client, baseUrl, authToken } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true, }) const installMetadata = await client.asset.installMetadata() const packed = await packAsset(zipFilter, { cwd, dependencies, policy: packPolicyFromInstallMetadata(installMetadata[type]), fetchFileManifest: fileManifestForRange(client.asset), }) const { zip, npmDependencies, assetDependencies, skillDependencies } = packed // Loud fail-open: what pack could not verify ships in the zip — say so before publishing it. for (const line of packWarnings(packed)) console.warn(line) const profile = await client.user.getProfile() if (!profile) { throw new Error('Not logged in. Run `market login` first.') } const [existing] = await lookupAssets(client.asset, [{ name: parsedName }], { includeUnapproved: true, }) if (existing && existing.ownerId !== profile.id) { throw new Error(`Asset "${parsedName}" already exists and is owned by another user`) } if (existing) { const latestBytes = await downloadAssetZipBytes({ name: parsedName, version: existing.version, baseUrl, authToken, }) if ( existing.description === parsedDescription && depsEqual(existing.npmDependencies, npmDependencies) && depsEqual(existing.assetDependencies, assetDependencies) && depsEqual(existing.skillDependencies, skillDependencies) && bytesEqual(latestBytes, zip) ) { spinner.stop() console.log(unchangedUploadResult(parsedName, existing.version)) return } } const version = parsedVersion ?? nextVersion(existing?.version) spinner.text = `Uploading ${parsedName}@${version}` const uploaded = await client.asset.uploadZip({ name: parsedName, type, version, description: parsedDescription, npmDependencies, assetDependencies, skillDependencies, tags: [], access: opts.access, zip: new File([toArrayBuffer(zip)], `${parsedName}-${version}.zip`, { type: 'application/zip', }), }) spinner.stop() console.log(uploadResult(parsedName, uploaded.version)) } catch (err) { spinner.stop() throw err } } export function parseUploadDescription(description: string): string { try { return assetDescriptionSchema.parse(description) } catch { throw new Error( `Asset description must be ${MAX_ASSET_DESCRIPTION_LENGTH} characters or fewer.`, ) } } /** Structural equality with sorted keys; asset dependency values may be nested `{ version, alias }`. */ function depsEqual(stored: Record, next: Record): boolean { return stableJson(stored) === stableJson(next) } function stableJson(value: unknown): string { if (!isRecord(value)) return JSON.stringify(value) const keys = Object.keys(value).sort() return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}` } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === 'object' && !Array.isArray(value) } function nextVersion(latest?: string): string { if (!latest) return '1.0.0' const version = semver.inc(latest, 'patch') if (!version) throw new Error(`Could not increment version "${latest}"`) return version } function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { if (a.length !== b.length) return false for (let i = 0; i < a.length; i += 1) { if (a[i] !== b[i]) return false } return true } function toArrayBuffer(bytes: Uint8Array): ArrayBuffer { return new Uint8Array(bytes).buffer }