get-codex-lost-world 1.0.6 → 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.
|
@@ -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() {
|
|
@@ -22,12 +22,16 @@ function pickLatestAssetForTarget(release, target = {}) {
|
|
|
22
22
|
|
|
23
23
|
const candidates = assets
|
|
24
24
|
.filter((asset) => typeof asset?.name === 'string' && asset.name.toLowerCase().endsWith(`.${format}`))
|
|
25
|
-
.sort((a, b) =>
|
|
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
|
+
});
|
|
26
30
|
if (candidates.length === 0) {
|
|
27
31
|
throw new Error(`No .${format} asset found in release assets`);
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
const preferred = candidates.find((asset) => /^CodexIntelMac_
|
|
34
|
+
const preferred = candidates.find((asset) => /^(?:CodexIntelMac_|CodexMacIntel_).+\.dmg$/i.test(asset.name));
|
|
31
35
|
const selected = preferred || candidates[0];
|
|
32
36
|
ensureAssetUrl(selected, `.${format}`);
|
|
33
37
|
return selected;
|