/** TWS source releases use three numeric components, without tags or suffixes. */ export function scriptVersionError(value: unknown): string { if (typeof value !== 'string' || !value.trim()) return 'A script release version is required.'; if (value.length > 80 || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value.trim())) return 'Use MAJOR.MINOR.PATCH for the script release version, such as 1.2.0.'; return ''; } /** Ignore non-script components (for example native plugins) in a TWS manifest. */ export function parseScriptVersions(text: string): Record { let data: unknown; try { data = JSON.parse(text); } catch { throw new Error('Choose a valid versions.json file.'); } if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error('versions.json must map script filenames to release versions.'); const entries = Object.entries(data).filter(([name]) => name.endsWith('.gsc')); if (!entries.length) throw new Error('versions.json contains no .gsc filenames.'); for (const [name, value] of entries) { if (name.includes('/') || name.includes('\\')) throw new Error(`Use an exact filename in versions.json: ${name}`); const error = scriptVersionError(value); if (error) throw new Error(`${name}: ${error}`); } return Object.fromEntries(entries.map(([name, value]) => [name, (value as string).trim()])); } /** Uploaded filenames must never resolve inherited object properties. */ export function manifestVersion(versions: Record, filename: string): string { return Object.hasOwn(versions, filename) ? versions[filename] : ''; }