|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + * Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | + *--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import { spawnSync } from 'node:child_process'; |
| 7 | + |
| 8 | +const feedUrl = 'https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/npm/registry'; |
| 9 | +const azureDevOpsResource = '499b84ac-1321-427f-aa17-267ca6975798'; |
| 10 | +const codexAliasPrefix = 'npm:@openai/codex@'; |
| 11 | + |
| 12 | +type OutputFormat = 'text' | 'raw' | 'json'; |
| 13 | + |
| 14 | +interface Options { |
| 15 | + format: OutputFormat; |
| 16 | + requestedVersion: string | undefined; |
| 17 | +} |
| 18 | + |
| 19 | +interface CodexVersionMetadata { |
| 20 | + optionalDependencies?: Record<string, string>; |
| 21 | +} |
| 22 | + |
| 23 | +interface CodexPackument { |
| 24 | + versions: Record<string, CodexVersionMetadata>; |
| 25 | +} |
| 26 | + |
| 27 | +function usage(): void { |
| 28 | + console.error('Usage: node --experimental-strip-types latest-private-version.ts [--raw | --json] [--version <x.y.z>]'); |
| 29 | +} |
| 30 | + |
| 31 | +function fail(message: string): never { |
| 32 | + console.error(`Error: ${message}`); |
| 33 | + process.exit(1); |
| 34 | +} |
| 35 | + |
| 36 | +function parseArgs(args: string[]): Options { |
| 37 | + let format: OutputFormat = 'text'; |
| 38 | + let requestedVersion: string | undefined; |
| 39 | + |
| 40 | + for (let index = 0; index < args.length; index++) { |
| 41 | + const arg = args[index]; |
| 42 | + if (arg === '--raw') { |
| 43 | + format = 'raw'; |
| 44 | + } else if (arg === '--json') { |
| 45 | + format = 'json'; |
| 46 | + } else if (arg === '--version') { |
| 47 | + requestedVersion = args[++index]; |
| 48 | + if (!requestedVersion) { |
| 49 | + usage(); |
| 50 | + process.exit(2); |
| 51 | + } |
| 52 | + } else { |
| 53 | + usage(); |
| 54 | + process.exit(2); |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + if (requestedVersion && !/^\d+\.\d+\.\d+$/.test(requestedVersion)) { |
| 59 | + fail(`--version must be a stable x.y.z version, got ${requestedVersion}`); |
| 60 | + } |
| 61 | + |
| 62 | + return { format, requestedVersion }; |
| 63 | +} |
| 64 | + |
| 65 | +function getAccessToken(): string { |
| 66 | + const azureCli = process.platform === 'win32' ? 'az.cmd' : 'az'; |
| 67 | + const result = spawnSync(azureCli, [ |
| 68 | + 'account', |
| 69 | + 'get-access-token', |
| 70 | + '--resource', |
| 71 | + azureDevOpsResource, |
| 72 | + '--query', |
| 73 | + 'accessToken', |
| 74 | + '--output', |
| 75 | + 'tsv', |
| 76 | + ], { encoding: 'utf8' }); |
| 77 | + |
| 78 | + if (result.error) { |
| 79 | + fail(`could not run Azure CLI (${result.error.message}). Install it and sign in to the monacotools organization.`); |
| 80 | + } |
| 81 | + if (result.status !== 0) { |
| 82 | + const detail = result.stderr?.trim(); |
| 83 | + fail(`Azure CLI could not obtain an Azure DevOps token${detail ? `: ${detail}` : ''}`); |
| 84 | + } |
| 85 | + |
| 86 | + const token = result.stdout?.trim(); |
| 87 | + if (!token) { |
| 88 | + fail('Azure CLI returned an empty Azure DevOps token. Sign in and try again.'); |
| 89 | + } |
| 90 | + return token; |
| 91 | +} |
| 92 | + |
| 93 | +function parseStableVersion(version: string): number[] | undefined { |
| 94 | + const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version); |
| 95 | + return match ? match.slice(1).map(Number) : undefined; |
| 96 | +} |
| 97 | + |
| 98 | +function compareVersions(left: string, right: string): number { |
| 99 | + const leftParts = parseStableVersion(left); |
| 100 | + const rightParts = parseStableVersion(right); |
| 101 | + if (!leftParts || !rightParts) { |
| 102 | + throw new Error('compareVersions only accepts stable versions'); |
| 103 | + } |
| 104 | + for (let index = 0; index < 3; index++) { |
| 105 | + const difference = leftParts[index] - rightParts[index]; |
| 106 | + if (difference !== 0) { |
| 107 | + return difference; |
| 108 | + } |
| 109 | + } |
| 110 | + return 0; |
| 111 | +} |
| 112 | + |
| 113 | +function binaryVersions(packument: CodexPackument, version: string): string[] { |
| 114 | + const optionalDependencies = packument.versions[version]?.optionalDependencies; |
| 115 | + if (!optionalDependencies || typeof optionalDependencies !== 'object') { |
| 116 | + return []; |
| 117 | + } |
| 118 | + |
| 119 | + return Object.values(optionalDependencies) |
| 120 | + .filter(value => typeof value === 'string' && value.startsWith(codexAliasPrefix)) |
| 121 | + .map(value => value.slice(codexAliasPrefix.length)); |
| 122 | +} |
| 123 | + |
| 124 | +function requiredVersions(packument: CodexPackument, version: string): string[] { |
| 125 | + return [version, ...binaryVersions(packument, version)]; |
| 126 | +} |
| 127 | + |
| 128 | +const { format, requestedVersion } = parseArgs(process.argv.slice(2)); |
| 129 | +const accessToken = getAccessToken(); |
| 130 | +const response = await fetch(`${feedUrl}/@openai%2Fcodex`, { |
| 131 | + headers: { Authorization: `Bearer ${accessToken}` }, |
| 132 | +}); |
| 133 | + |
| 134 | +if (!response.ok) { |
| 135 | + fail(`private VS Code feed returned HTTP ${response.status} ${response.statusText}`); |
| 136 | +} |
| 137 | + |
| 138 | +const packument = await response.json() as CodexPackument; |
| 139 | +if (!packument || typeof packument !== 'object' || !packument.versions || typeof packument.versions !== 'object') { |
| 140 | + fail('private VS Code feed returned an unexpected @openai/codex response'); |
| 141 | +} |
| 142 | + |
| 143 | +const availableVersions = new Set(Object.keys(packument.versions)); |
| 144 | +const completeStableVersions = [...availableVersions] |
| 145 | + .filter(version => parseStableVersion(version)) |
| 146 | + .filter(version => binaryVersions(packument, version).length > 0) |
| 147 | + .filter(version => requiredVersions(packument, version).every(required => availableVersions.has(required))) |
| 148 | + .sort(compareVersions); |
| 149 | + |
| 150 | +const latestVersion = completeStableVersions.at(-1); |
| 151 | +if (!latestVersion) { |
| 152 | + fail('the private VS Code feed contains no stable Codex release with all platform binaries'); |
| 153 | +} |
| 154 | + |
| 155 | +const checkedVersion = requestedVersion ?? latestVersion; |
| 156 | +if (requestedVersion && availableVersions.has(requestedVersion) && binaryVersions(packument, requestedVersion).length === 0) { |
| 157 | + fail(`Codex ${requestedVersion} metadata declares no platform binary aliases, so its availability cannot be verified`); |
| 158 | +} |
| 159 | +const missingVersions = requiredVersions(packument, checkedVersion).filter(version => !availableVersions.has(version)); |
| 160 | +const result = { |
| 161 | + feed: feedUrl, |
| 162 | + latestVersion, |
| 163 | + checkedVersion, |
| 164 | + available: missingVersions.length === 0, |
| 165 | + missingVersions, |
| 166 | +}; |
| 167 | + |
| 168 | +if (format === 'json') { |
| 169 | + console.log(JSON.stringify(result, undefined, 2)); |
| 170 | +} else if (format === 'raw') { |
| 171 | + console.log(latestVersion); |
| 172 | +} else if (requestedVersion) { |
| 173 | + if (result.available) { |
| 174 | + console.log(`Codex ${requestedVersion} is fully available from the private VS Code feed.`); |
| 175 | + } else { |
| 176 | + console.log(`Codex ${requestedVersion} is not fully available from the private VS Code feed.`); |
| 177 | + console.log(`Missing: ${missingVersions.join(', ')}`); |
| 178 | + console.log(`Latest fully available stable version: ${latestVersion}`); |
| 179 | + } |
| 180 | +} else { |
| 181 | + console.log(`Latest Codex stable fully available from the private VS Code feed: ${latestVersion}`); |
| 182 | +} |
| 183 | + |
| 184 | +if (requestedVersion && !result.available) { |
| 185 | + process.exitCode = 1; |
| 186 | +} |
0 commit comments