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
- return getJson(url, headers);
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
- return getDefaultDownloadsLocation(env);
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) => new Date(b.updated_at) - new Date(a.updated_at));
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_.*\.dmg$/i.test(asset.name));
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "get-codex-lost-world",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
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": {