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.
@@ -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 --windows-x64 --workdir /tmp/codex',
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, platform, arch, format }) {
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
- 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() {
@@ -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 | windows',
304
- ' --arch <a> Target arch: x64 | arm64 (windows)',
305
- ' --format <f> Target format: dmg | zip',
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 mapHostArchToTargetArch(hostArch) {
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
- : inferredPlatform;
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
- : inferredArch;
341
+ : 'x64';
341
342
  const selectedFormat = typeof parsed.format === 'string' && parsed.format.trim() !== ''
342
343
  ? parsed.format
343
- : (normalizedPlatform === 'windows' ? 'zip' : 'dmg');
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 || (platform === 'windows' ? 'zip' : 'dmg')).trim().toLowerCase();
22
-
23
- const candidates = assets.filter((asset) => typeof asset?.name === 'string' && asset.name.toLowerCase().endsWith(`.${format}`));
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
- if (platform === 'windows') {
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', 'windows'];
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) || (platform === 'windows' ? 'zip' : 'dmg');
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.5",
4
- "description": "CLI to download/build Codex Mac Intel DMG and Windows ZIP artifacts from upstream Codex.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",