get-codex-lost-world 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/get-codex-lost-world.js +2 -6
- package/lib/get-codex-lost-world/build.js +1 -7
- package/lib/get-codex-lost-world/github-release-html.js +172 -0
- package/lib/get-codex-lost-world/main.js +26 -34
- package/lib/get-codex-lost-world/release.js +10 -24
- package/lib/get-codex-lost-world/targets.js +2 -4
- package/package.json +3 -5
|
@@ -9,11 +9,9 @@ function printTargetShortcuts() {
|
|
|
9
9
|
'Target shortcuts:',
|
|
10
10
|
' --mac-silicon Build/download for Apple Silicon Mac (default source: original Codex.dmg on macOS arm64 host)',
|
|
11
11
|
' --mac-intel Build/download for Intel Mac (.dmg)',
|
|
12
|
-
' --windows-x64 Build/download for Windows x64 (.zip)',
|
|
13
|
-
' --windows-arm64 Build/download for Windows arm64 (.zip)',
|
|
14
12
|
'',
|
|
15
13
|
'Example:',
|
|
16
|
-
' npx get-codex-lost-world
|
|
14
|
+
' npx get-codex-lost-world -w ~/Downloads',
|
|
17
15
|
].join('\n');
|
|
18
16
|
|
|
19
17
|
process.stdout.write(`${message}\n`);
|
|
@@ -23,8 +21,6 @@ function remapShortcutArgs(argv = []) {
|
|
|
23
21
|
const targetShortcuts = {
|
|
24
22
|
'--mac-silicon': ['--platform', 'mac', '--format', 'dmg'],
|
|
25
23
|
'--mac-intel': ['--platform', 'mac', '--arch', 'x64', '--format', 'dmg'],
|
|
26
|
-
'--windows-x64': ['--platform', 'windows', '--arch', 'x64', '--format', 'zip'],
|
|
27
|
-
'--windows-arm64': ['--platform', 'windows', '--arch', 'arm64', '--format', 'zip'],
|
|
28
24
|
};
|
|
29
25
|
|
|
30
26
|
const selectedShortcuts = argv.filter((arg) => Object.prototype.hasOwnProperty.call(targetShortcuts, arg));
|
|
@@ -59,4 +55,4 @@ main().catch((error) => {
|
|
|
59
55
|
process.stderr.write(`${message}\n\n${usage()}\n`);
|
|
60
56
|
process.stderr.write('Use --target-help to see target shortcuts.\n');
|
|
61
57
|
process.exit(1);
|
|
62
|
-
});
|
|
58
|
+
});
|
|
@@ -11,16 +11,10 @@ function makeOutputName(version) {
|
|
|
11
11
|
return `CodexIntelMac_${sanitizeVersion(version)}.dmg`;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function makeOutputNameForTarget({ version,
|
|
14
|
+
function makeOutputNameForTarget({ version, format }) {
|
|
15
15
|
const safeVersion = sanitizeVersion(version);
|
|
16
|
-
const normalizedPlatform = String(platform || '').trim().toLowerCase();
|
|
17
|
-
const normalizedArch = String(arch || '').trim().toLowerCase();
|
|
18
16
|
const normalizedFormat = String(format || '').trim().toLowerCase();
|
|
19
17
|
|
|
20
|
-
if (normalizedPlatform === 'windows') {
|
|
21
|
-
return `CodexWindows_${normalizedArch || 'x64'}_${safeVersion}.${normalizedFormat || 'zip'}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
return `CodexIntelMac_${safeVersion}.${normalizedFormat || 'dmg'}`;
|
|
25
19
|
}
|
|
26
20
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('node:https');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_HEADERS = {
|
|
6
|
+
'User-Agent': 'get-codex-lost-world',
|
|
7
|
+
Accept: 'text/html,application/xhtml+xml',
|
|
8
|
+
// Avoid having to handle gzip/brotli in Node core without extra deps.
|
|
9
|
+
'Accept-Encoding': 'identity',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function fetchTextOnce(url, { headers = {} } = {}) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const req = https.get(url, { headers: { ...DEFAULT_HEADERS, ...headers } }, (res) => {
|
|
15
|
+
const statusCode = res.statusCode || 0;
|
|
16
|
+
const chunks = [];
|
|
17
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
18
|
+
res.on('end', () => {
|
|
19
|
+
resolve({
|
|
20
|
+
statusCode,
|
|
21
|
+
headers: res.headers || {},
|
|
22
|
+
body: Buffer.concat(chunks).toString('utf8'),
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
res.on('error', reject);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
req.on('error', reject);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function fetchTextWithRedirects(url, { headers = {}, maxRedirects = 10 } = {}) {
|
|
33
|
+
let currentUrl = url;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i <= maxRedirects; i += 1) {
|
|
36
|
+
// eslint-disable-next-line no-await-in-loop
|
|
37
|
+
const res = await fetchTextOnce(currentUrl, { headers });
|
|
38
|
+
|
|
39
|
+
const status = res.statusCode;
|
|
40
|
+
const location = res.headers && res.headers.location;
|
|
41
|
+
if ([301, 302, 303, 307, 308].includes(status) && location) {
|
|
42
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (status < 200 || status >= 300) {
|
|
47
|
+
throw new Error(`Request failed (${status}): ${currentUrl}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
finalUrl: currentUrl,
|
|
52
|
+
statusCode: status,
|
|
53
|
+
body: res.body,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw new Error(`Too many redirects: ${url}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractReleaseTagFromUrl(finalUrl) {
|
|
61
|
+
const input = String(finalUrl || '').trim();
|
|
62
|
+
if (!input) {
|
|
63
|
+
throw new Error('finalUrl is required to extract release tag');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const match = input.match(/\/releases\/tag\/([^/?#]+)/);
|
|
67
|
+
if (!match || !match[1]) {
|
|
68
|
+
throw new Error(`Unable to extract release tag from url: ${finalUrl}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return decodeURIComponent(match[1]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseExpandedAssetsHtml(html, { repo } = {}) {
|
|
75
|
+
const body = String(html || '');
|
|
76
|
+
const assets = [];
|
|
77
|
+
const baseUrl = 'https://github.com/';
|
|
78
|
+
|
|
79
|
+
const liRegex = /<li[^>]*class="Box-row[^"]*"[^>]*>([\s\S]*?)<\/li>/g;
|
|
80
|
+
let liMatch;
|
|
81
|
+
while ((liMatch = liRegex.exec(body))) {
|
|
82
|
+
const block = liMatch[1];
|
|
83
|
+
|
|
84
|
+
const linkMatch = block.match(/<a[^>]*href="([^"]+)"[^>]*class="Truncate"[\s\S]*?<span[^>]*class="Truncate-text text-bold"[^>]*>([^<]+)<\/span>/i);
|
|
85
|
+
if (!linkMatch) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const href = linkMatch[1];
|
|
90
|
+
const name = String(linkMatch[2] || '').trim();
|
|
91
|
+
if (!name) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const timeMatch = block.match(/<relative-time[^>]*datetime="([^"]+)"/i);
|
|
96
|
+
const updated_at = timeMatch ? String(timeMatch[1] || '').trim() : '';
|
|
97
|
+
|
|
98
|
+
const shaMatch = block.match(/sha256:([0-9a-f]{64})/i);
|
|
99
|
+
const sha256 = shaMatch ? `sha256:${shaMatch[1].toLowerCase()}` : '';
|
|
100
|
+
|
|
101
|
+
const sizeMatch = block.match(/>\s*([0-9.]+\s*(?:B|KB|MB|GB|TB))\s*<\/span>\s*<span[^>]*>\s*<relative-time/i);
|
|
102
|
+
const size = sizeMatch ? String(sizeMatch[1] || '').trim() : '';
|
|
103
|
+
|
|
104
|
+
const browser_download_url = new URL(href, baseUrl).toString();
|
|
105
|
+
|
|
106
|
+
assets.push({
|
|
107
|
+
name,
|
|
108
|
+
browser_download_url,
|
|
109
|
+
updated_at,
|
|
110
|
+
size,
|
|
111
|
+
sha256,
|
|
112
|
+
// For compatibility with GitHub API shape
|
|
113
|
+
url: browser_download_url,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If we didn't get any Box-row matches (GitHub markup change), fail loudly.
|
|
118
|
+
if (assets.length === 0) {
|
|
119
|
+
const repoLabel = repo ? ` for ${repo}` : '';
|
|
120
|
+
throw new Error(`No assets parsed from expanded_assets HTML${repoLabel}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return assets;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function maxIsoDatetime(values) {
|
|
127
|
+
let best = '';
|
|
128
|
+
let bestTime = 0;
|
|
129
|
+
|
|
130
|
+
for (const value of values) {
|
|
131
|
+
const time = Date.parse(value);
|
|
132
|
+
if (!Number.isFinite(time)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (time > bestTime) {
|
|
136
|
+
bestTime = time;
|
|
137
|
+
best = value;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return best;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function getLatestReleaseFromGithubHtml({ repo } = {}) {
|
|
145
|
+
const normalizedRepo = String(repo || '').trim();
|
|
146
|
+
if (!normalizedRepo) {
|
|
147
|
+
throw new Error('repo is required');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const latestUrl = `https://github.com/${normalizedRepo}/releases/latest`;
|
|
151
|
+
const latestRes = await fetchTextWithRedirects(latestUrl);
|
|
152
|
+
const tag = extractReleaseTagFromUrl(latestRes.finalUrl);
|
|
153
|
+
|
|
154
|
+
const assetsUrl = `https://github.com/${normalizedRepo}/releases/expanded_assets/${encodeURIComponent(tag)}`;
|
|
155
|
+
const assetsRes = await fetchTextWithRedirects(assetsUrl);
|
|
156
|
+
const assets = parseExpandedAssetsHtml(assetsRes.body, { repo: normalizedRepo });
|
|
157
|
+
|
|
158
|
+
const published_at = maxIsoDatetime(assets.map((asset) => asset.updated_at));
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
tag_name: tag,
|
|
162
|
+
published_at,
|
|
163
|
+
assets,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
fetchTextWithRedirects,
|
|
169
|
+
extractReleaseTagFromUrl,
|
|
170
|
+
parseExpandedAssetsHtml,
|
|
171
|
+
getLatestReleaseFromGithubHtml,
|
|
172
|
+
};
|
|
@@ -8,6 +8,7 @@ const { spawnSync } = require('node:child_process');
|
|
|
8
8
|
const readline = require('node:readline');
|
|
9
9
|
const { parseArgs } = require('./args');
|
|
10
10
|
const { summarizeRelease, pickLatestAssetForTarget } = require('./release');
|
|
11
|
+
const { getLatestReleaseFromGithubHtml } = require('./github-release-html');
|
|
11
12
|
const { createLocalBuilder } = require('./local-builder');
|
|
12
13
|
const { normalizeTarget } = require('./targets');
|
|
13
14
|
|
|
@@ -226,7 +227,22 @@ function getDefaultDeps(overrides = {}) {
|
|
|
226
227
|
headers.Authorization = `Bearer ${githubToken}`;
|
|
227
228
|
}
|
|
228
229
|
|
|
229
|
-
|
|
230
|
+
try {
|
|
231
|
+
return await getJson(url, headers);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
try {
|
|
234
|
+
return await getLatestReleaseFromGithubHtml({ repo: githubRepo });
|
|
235
|
+
} catch (fallbackError) {
|
|
236
|
+
const apiMessage = error instanceof Error ? error.message : String(error);
|
|
237
|
+
const htmlMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
238
|
+
const combined = new Error(
|
|
239
|
+
`GitHub release lookup failed via API and HTML fallback. API error: ${apiMessage}. HTML error: ${htmlMessage}`
|
|
240
|
+
);
|
|
241
|
+
combined.apiError = error;
|
|
242
|
+
combined.htmlError = fallbackError;
|
|
243
|
+
throw combined;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
230
246
|
},
|
|
231
247
|
},
|
|
232
248
|
io,
|
|
@@ -287,7 +303,8 @@ function getDefaultDownloadsLocation(env) {
|
|
|
287
303
|
}
|
|
288
304
|
|
|
289
305
|
function getDefaultBuildLocation(env) {
|
|
290
|
-
|
|
306
|
+
const pwdFromEnv = typeof env?.PWD === 'string' ? env.PWD.trim() : '';
|
|
307
|
+
return pwdFromEnv || process.cwd();
|
|
291
308
|
}
|
|
292
309
|
|
|
293
310
|
function usage() {
|
|
@@ -300,9 +317,9 @@ function usage() {
|
|
|
300
317
|
' -c, --cache Show latest release info and optionally download/sign it',
|
|
301
318
|
' -s, --sign <path> Ad-hoc sign a local app/dmg path',
|
|
302
319
|
' -w, --workdir <path> Build working directory (download source + write Intel DMG)',
|
|
303
|
-
' --platform <p> Target platform: mac
|
|
304
|
-
' --arch <a> Target arch: x64
|
|
305
|
-
' --format <f> Target format: dmg
|
|
320
|
+
' --platform <p> Target platform: mac',
|
|
321
|
+
' --arch <a> Target arch: x64',
|
|
322
|
+
' --format <f> Target format: dmg',
|
|
306
323
|
'',
|
|
307
324
|
'Other options:',
|
|
308
325
|
' -h, --help Show this help message',
|
|
@@ -315,32 +332,16 @@ function usage() {
|
|
|
315
332
|
].join('\n');
|
|
316
333
|
}
|
|
317
334
|
|
|
318
|
-
function
|
|
319
|
-
const normalized = String(hostArch || '').trim().toLowerCase();
|
|
320
|
-
if (normalized === 'arm64') {
|
|
321
|
-
return 'arm64';
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return 'x64';
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function resolveDefaultTargetInput(parsed, env) {
|
|
328
|
-
const runtimePlatform = String((env && env.RUNTIME_PLATFORM) || process.platform || '').trim().toLowerCase();
|
|
329
|
-
const runtimeArch = String((env && env.RUNTIME_ARCH) || process.arch || '').trim().toLowerCase();
|
|
330
|
-
|
|
331
|
-
const inferredPlatform = runtimePlatform === 'win32' ? 'windows' : 'mac';
|
|
335
|
+
function resolveDefaultTargetInput(parsed, _env) {
|
|
332
336
|
const selectedPlatform = typeof parsed.platform === 'string' && parsed.platform.trim() !== ''
|
|
333
337
|
? parsed.platform
|
|
334
|
-
:
|
|
335
|
-
|
|
336
|
-
const normalizedPlatform = String(selectedPlatform).trim().toLowerCase();
|
|
337
|
-
const inferredArch = normalizedPlatform === 'windows' ? mapHostArchToTargetArch(runtimeArch) : 'x64';
|
|
338
|
+
: 'mac';
|
|
338
339
|
const selectedArch = typeof parsed.arch === 'string' && parsed.arch.trim() !== ''
|
|
339
340
|
? parsed.arch
|
|
340
|
-
:
|
|
341
|
+
: 'x64';
|
|
341
342
|
const selectedFormat = typeof parsed.format === 'string' && parsed.format.trim() !== ''
|
|
342
343
|
? parsed.format
|
|
343
|
-
:
|
|
344
|
+
: 'dmg';
|
|
344
345
|
|
|
345
346
|
return {
|
|
346
347
|
platform: selectedPlatform,
|
|
@@ -389,15 +390,6 @@ async function runCacheMode(parsed, deps) {
|
|
|
389
390
|
: (downloadResult && downloadResult.path) || fallbackPath;
|
|
390
391
|
deps.io.log(`Download done: ${downloadedPath}`);
|
|
391
392
|
|
|
392
|
-
if (target.platform === 'windows') {
|
|
393
|
-
return {
|
|
394
|
-
mode: 'cache',
|
|
395
|
-
downloaded: true,
|
|
396
|
-
signed: false,
|
|
397
|
-
path: downloadedPath,
|
|
398
|
-
target,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
393
|
|
|
402
394
|
const signAnswer = await deps.io.prompt('Sign downloaded file? (Y/n)', 'Y');
|
|
403
395
|
let signed = false;
|
|
@@ -18,34 +18,20 @@ function pickLatestAssetForTarget(release, target = {}) {
|
|
|
18
18
|
const assets = Array.isArray(release?.assets) ? release.assets : [];
|
|
19
19
|
const platform = String(target.platform || 'mac').trim().toLowerCase();
|
|
20
20
|
const arch = String(target.arch || 'x64').trim().toLowerCase();
|
|
21
|
-
const format = String(target.format ||
|
|
22
|
-
|
|
23
|
-
const candidates = assets
|
|
21
|
+
const format = String(target.format || 'dmg').trim().toLowerCase();
|
|
22
|
+
|
|
23
|
+
const candidates = assets
|
|
24
|
+
.filter((asset) => typeof asset?.name === 'string' && asset.name.toLowerCase().endsWith(`.${format}`))
|
|
25
|
+
.sort((a, b) => {
|
|
26
|
+
const aTime = Number.isFinite(Date.parse(a.updated_at)) ? Date.parse(a.updated_at) : 0;
|
|
27
|
+
const bTime = Number.isFinite(Date.parse(b.updated_at)) ? Date.parse(b.updated_at) : 0;
|
|
28
|
+
return bTime - aTime;
|
|
29
|
+
});
|
|
24
30
|
if (candidates.length === 0) {
|
|
25
31
|
throw new Error(`No .${format} asset found in release assets`);
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
const strictMatch = candidates.find((asset) => {
|
|
30
|
-
const name = asset.name.toLowerCase();
|
|
31
|
-
return name.includes('windows') && name.includes(arch);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
if (strictMatch) {
|
|
35
|
-
ensureAssetUrl(strictMatch, `.${format}`);
|
|
36
|
-
return strictMatch;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const archMatch = candidates.find((asset) => asset.name.toLowerCase().includes(arch));
|
|
40
|
-
if (archMatch) {
|
|
41
|
-
ensureAssetUrl(archMatch, `.${format}`);
|
|
42
|
-
return archMatch;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
throw new Error(`No .${format} asset found for windows/${arch}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const preferred = candidates.find((asset) => /^CodexIntelMac_.*\.dmg$/i.test(asset.name));
|
|
34
|
+
const preferred = candidates.find((asset) => /^(?:CodexIntelMac_|CodexMacIntel_).+\.dmg$/i.test(asset.name));
|
|
49
35
|
const selected = preferred || candidates[0];
|
|
50
36
|
ensureAssetUrl(selected, `.${format}`);
|
|
51
37
|
return selected;
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const SUPPORTED_PLATFORMS = ['mac'
|
|
3
|
+
const SUPPORTED_PLATFORMS = ['mac'];
|
|
4
4
|
|
|
5
5
|
const SUPPORTED_ARCHES_BY_PLATFORM = {
|
|
6
6
|
mac: ['x64'],
|
|
7
|
-
windows: ['x64', 'arm64'],
|
|
8
7
|
};
|
|
9
8
|
|
|
10
9
|
const SUPPORTED_FORMATS_BY_PLATFORM = {
|
|
11
10
|
mac: ['dmg'],
|
|
12
|
-
windows: ['zip'],
|
|
13
11
|
};
|
|
14
12
|
|
|
15
13
|
function normalizeValue(value) {
|
|
@@ -19,7 +17,7 @@ function normalizeValue(value) {
|
|
|
19
17
|
function normalizeTarget(input = {}) {
|
|
20
18
|
const platform = normalizeValue(input.platform) || 'mac';
|
|
21
19
|
const arch = normalizeValue(input.arch) || 'x64';
|
|
22
|
-
const format = normalizeValue(input.format) ||
|
|
20
|
+
const format = normalizeValue(input.format) || 'dmg';
|
|
23
21
|
const target = { platform, arch, format };
|
|
24
22
|
validateTarget(target);
|
|
25
23
|
return target;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "get-codex-lost-world",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "CLI to download/build Codex Mac Intel DMG
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "CLI to download/build Codex Mac Intel DMG artifacts from upstream Codex.dmg.",
|
|
5
5
|
"main": "lib/get-codex-lost-world/main.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"get-codex-lost-world": "bin/get-codex-lost-world.js"
|
|
@@ -17,9 +17,7 @@
|
|
|
17
17
|
"codex",
|
|
18
18
|
"intel",
|
|
19
19
|
"build",
|
|
20
|
-
"mac"
|
|
21
|
-
"windows",
|
|
22
|
-
"zip"
|
|
20
|
+
"mac"
|
|
23
21
|
],
|
|
24
22
|
"author": "0x0a0d",
|
|
25
23
|
"license": "ISC",
|