export interface ParsedAssetSpec { id: string; kind: 'image' | 'video' | 'audio' | 'subtitle' | 'other'; path: string; sceneIndex?: number; backend?: string; } const ALLOWED_KINDS = new Set(['image', 'video', 'audio', 'subtitle', 'other']); // A URI/URL scheme prefix (http://, https://, Asset://, gobananas://, gs://, s3://, …) // — its internal `://` and any `:port` would be shredded by a naive split(':'). const SCHEME_RE = /^[a-zA-Z][a-zA-Z0-9+.\-]*:\/\//; // For a scheme path, peel an optional trailing `:sceneIndex[:backend]` from the END // so the URL (which may itself contain colons) is preserved intact. const TRAILING_RE = /^(.*?)(?::(\d+)(?::([^:]+))?)?$/; /** * Parse a `--asset kind:path[:sceneIndex][:backend]` spec. The path may be a * local file path OR a URL / `Asset://` URI whose internal colons must survive. * Local paths keep the original positional split (byte-identical behavior); * scheme paths peel the trailing `:sceneIndex[:backend]` from the end instead. */ export function parseAssetSpec(raw: string): ParsedAssetSpec { const firstColon = raw.indexOf(':'); const kindRaw = firstColon >= 0 ? raw.slice(0, firstColon) : ''; const remainder = firstColon >= 0 ? raw.slice(firstColon + 1) : ''; if (!kindRaw || !remainder) { throw new Error(`Invalid --asset value: "${raw}". Expected kind:path[:sceneIndex][:backend]`); } let path: string; let sceneIndexRaw: string | undefined; let backend: string | undefined; if (SCHEME_RE.test(remainder)) { const match = TRAILING_RE.exec(remainder); path = match?.[1] ?? remainder; sceneIndexRaw = match?.[2]; backend = match?.[3]; } else { [path, sceneIndexRaw, backend] = remainder.split(':'); } const kind = (ALLOWED_KINDS.has(kindRaw) ? kindRaw : 'other') as ParsedAssetSpec['kind']; const sceneIndex = sceneIndexRaw !== undefined && sceneIndexRaw !== '' ? Number(sceneIndexRaw) : undefined; return { id: `${kind}-${path}`, kind, path, ...(Number.isFinite(sceneIndex) ? { sceneIndex } : {}), ...(backend ? { backend } : {}), }; }