@utils-code/front-end 1.0.0 → 1.0.3

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/README.md CHANGED
@@ -1,9 +1,30 @@
1
1
  # @utils-code/front-end
2
2
 
3
- This package installs the archive segments. To create the original ZIP:
3
+ This package contains the archive segments for Codex Windows 26.915.31029. It can assemble the original verified MSIX or extract a portable copy that starts without Microsoft Store/AppX registration. No install lifecycle script runs automatically.
4
4
 
5
- ```bash
6
- npx utils-code-front-end assemble ./code.zip
5
+ ## Portable installation on Windows
6
+
7
+ Fully exit any installed Microsoft Store/MSIX copy of Codex, then run from Command Prompt. A short path is recommended:
8
+
9
+ ```cmd
10
+ npx utils-code-front-end install-portable C:\CodexOffline
11
+ C:\CodexOffline\Codex.cmd
12
+ ```
13
+
14
+ The installer does not call `Add-AppxPackage`, does not require Developer Mode, and does not modify the official `ChatGPT.exe` or `app.asar`. Its launcher disables the package-only updater and points the desktop app at the bundled Codex CLI. The legacy `install-offline` command remains as an alias for `install-portable`.
15
+
16
+ Use `--force` only to replace the selected portable directory. “Offline” here means Store-independent installation and startup; Codex model requests still require network access.
17
+
18
+ Before publishing, the same checkout can be tested directly on Windows:
19
+
20
+ ```cmd
21
+ node front-end\bin\assemble.mjs install-portable C:\CodexOffline --force --launch
7
22
  ```
8
23
 
9
- No install lifecycle script runs automatically.
24
+ If no Windows machine is available, run the repository's **Windows portable smoke test** workflow from GitHub Actions. It downloads the pinned official MSIX, verifies its exact size and SHA-256, extracts it with this installer, and requires Electron to create a renderer page and remain running. Startup diagnostics are uploaded as a workflow artifact even when the check fails.
25
+
26
+ ## Assemble the original MSIX
27
+
28
+ ```cmd
29
+ npx utils-code-front-end assemble .\OpenAI.Codex.msix
30
+ ```
package/bin/assemble.mjs CHANGED
@@ -1,39 +1,388 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn, spawnSync } from 'node:child_process';
2
3
  import { createHash } from 'node:crypto';
3
- import { createReadStream, createWriteStream } from 'node:fs';
4
- import { access, mkdir, readFile, rename, rm } from 'node:fs/promises';
4
+ import { createReadStream, createWriteStream, realpathSync } from 'node:fs';
5
+ import {
6
+ access,
7
+ mkdir,
8
+ readFile,
9
+ readdir,
10
+ rename,
11
+ rm,
12
+ stat,
13
+ writeFile
14
+ } from 'node:fs/promises';
5
15
  import { createRequire } from 'node:module';
6
- import { dirname, resolve } from 'node:path';
16
+ import { dirname, join, resolve } from 'node:path';
7
17
  import { finished } from 'node:stream/promises';
18
+ import { fileURLToPath } from 'node:url';
8
19
 
9
20
  const require = createRequire(import.meta.url);
10
- const output = resolve(process.argv[3] ?? process.argv[2] ?? './code.zip');
11
- if (process.argv[2] && process.argv[2] !== 'assemble') {
12
- console.error('Usage: utils-code-front-end assemble [output.zip]');
13
- process.exit(1);
14
- }
15
- try {
16
- await access(output);
17
- throw new Error(`Refusing to overwrite ${output}`);
18
- } catch (error) {
19
- if (String(error.message).startsWith('Refusing')) throw error;
20
- }
21
-
22
- const manifest = JSON.parse(await readFile(new URL('../manifest.json', import.meta.url)));
23
- await mkdir(dirname(output), { recursive: true });
24
- const temporary = `${output}.partial`;
25
- await rm(temporary, { force: true });
26
- const target = createWriteStream(temporary, { flags: 'wx' });
27
- const hash = createHash('sha256');
28
- for (const part of manifest.parts) {
29
- const chunkNumber = String(Math.floor(Number(part.file.slice(-4)) / 10) + 1).padStart(3, '0');
30
- const packageRoot = dirname(require.resolve(`@utils-code/front-end-parts-${chunkNumber}/package.json`));
31
- const source = createReadStream(resolve(packageRoot, 'parts', part.file));
32
- source.on('data', (buffer) => hash.update(buffer));
33
- source.pipe(target, { end: false });
34
- await finished(source);
35
- }
36
- target.end();
37
- await finished(target);
38
- await rename(temporary, output);
39
- console.log(`Created ${output} (${hash.digest('hex')}).`);
21
+ const manifestPath = fileURLToPath(new URL('../manifest.json', import.meta.url));
22
+ const entryPackageRoot = dirname(manifestPath);
23
+ const manifest = JSON.parse(await readFile(manifestPath));
24
+ const supportedEncodedComponents = new Map([
25
+ ['%40', '@'],
26
+ ['%24', '$'],
27
+ ['%2B', '+']
28
+ ]);
29
+ const runtimeMarkers = ['CODEX_SPARKLE_ENABLED', 'CODEX_CLI_PATH'];
30
+
31
+ function usage() {
32
+ console.log(`Usage:
33
+ utils-code-front-end assemble [output.msix]
34
+ utils-code-front-end install-portable [install-directory] [--force] [--launch]
35
+ utils-code-front-end install-portable-msix <source.msix> [install-directory] [--force] [--launch]
36
+ utils-code-front-end install-offline [install-directory] [--force] [--launch]
37
+ utils-code-front-end launch [install-directory]
38
+
39
+ The portable installer extracts the verified official MSIX without registering
40
+ an AppX package. The default directory is
41
+ %LOCALAPPDATA%\\Programs\\OpenAI.Codex.Portable.`);
42
+ }
43
+
44
+ async function exists(path) {
45
+ try {
46
+ await access(path);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ async function sha256(path) {
54
+ const hash = createHash('sha256');
55
+ const input = createReadStream(path);
56
+ input.on('data', (buffer) => hash.update(buffer));
57
+ await finished(input);
58
+ return hash.digest('hex');
59
+ }
60
+
61
+ async function resolvePartPackageRoot(chunkNumber) {
62
+ const sibling = resolve(entryPackageRoot, '..', `front-end-parts-${chunkNumber}`);
63
+ if (await exists(join(sibling, 'package.json'))) return sibling;
64
+ return dirname(require.resolve(`@utils-code/front-end-parts-${chunkNumber}/package.json`));
65
+ }
66
+
67
+ async function assemble(outputArgument = './OpenAI.Codex.msix') {
68
+ const output = resolve(outputArgument);
69
+ if (await exists(output)) throw new Error(`Refusing to overwrite ${output}`);
70
+
71
+ await mkdir(dirname(output), { recursive: true });
72
+ const temporary = `${output}.partial`;
73
+ await rm(temporary, { force: true });
74
+ const target = createWriteStream(temporary, { flags: 'wx' });
75
+ const hash = createHash('sha256');
76
+
77
+ try {
78
+ for (const part of manifest.parts) {
79
+ const chunkNumber = String(Math.floor(Number(part.file.slice(-4)) / 10) + 1).padStart(3, '0');
80
+ const packageRoot = await resolvePartPackageRoot(chunkNumber);
81
+ const source = createReadStream(resolve(packageRoot, 'parts', part.file));
82
+ source.on('data', (buffer) => hash.update(buffer));
83
+ source.pipe(target, { end: false });
84
+ await finished(source);
85
+ }
86
+ target.end();
87
+ await finished(target);
88
+
89
+ const actualHash = hash.digest('hex');
90
+ if (actualHash !== manifest.source.sha256) {
91
+ throw new Error(`Archive checksum mismatch: expected ${manifest.source.sha256}, got ${actualHash}`);
92
+ }
93
+
94
+ await rename(temporary, output);
95
+ console.log(`Created ${output}`);
96
+ console.log(`SHA-256 ${actualHash}`);
97
+ return output;
98
+ } catch (error) {
99
+ target.destroy();
100
+ await rm(temporary, { force: true });
101
+ throw error;
102
+ }
103
+ }
104
+
105
+ function extractArchiveWithWindowsTar(archive, destination) {
106
+ const result = spawnSync(
107
+ 'tar.exe',
108
+ ['-xf', archive, '-C', destination],
109
+ {
110
+ encoding: 'utf8',
111
+ stdio: 'inherit'
112
+ }
113
+ );
114
+
115
+ if (result.error) throw result.error;
116
+ if (result.status !== 0) throw new Error(`Windows tar extraction failed with exit code ${result.status}`);
117
+ }
118
+
119
+ function assertWindows() {
120
+ if (process.platform !== 'win32') {
121
+ throw new Error('Portable installation and launch are only supported on Windows.');
122
+ }
123
+ }
124
+
125
+ function defaultInstallDirectory(directoryArgument) {
126
+ if (directoryArgument) return resolve(directoryArgument);
127
+ if (!process.env.LOCALAPPDATA) {
128
+ throw new Error('LOCALAPPDATA is unavailable; pass an absolute install directory.');
129
+ }
130
+ return resolve(process.env.LOCALAPPDATA, 'Programs', 'OpenAI.Codex.Portable');
131
+ }
132
+
133
+ function decodeSupportedComponent(name) {
134
+ let decoded = name;
135
+ for (const [encoded, value] of supportedEncodedComponents) {
136
+ decoded = decoded.replaceAll(new RegExp(encoded, 'gi'), value);
137
+ }
138
+ return decoded;
139
+ }
140
+
141
+ async function collectEncodedPaths(root, result = []) {
142
+ for (const entry of await readdir(root, { withFileTypes: true })) {
143
+ const path = join(root, entry.name);
144
+ if (entry.isDirectory()) await collectEncodedPaths(path, result);
145
+
146
+ const encodedTokens = entry.name.match(/%[0-9a-f]{2}/gi) ?? [];
147
+ const unsupported = encodedTokens.filter(
148
+ (token) => !supportedEncodedComponents.has(token.toUpperCase())
149
+ );
150
+ if (unsupported.length > 0) {
151
+ throw new Error(`Unsupported encoded path component ${entry.name} at ${path}`);
152
+ }
153
+ if (encodedTokens.length > 0) result.push(path);
154
+ }
155
+ return result;
156
+ }
157
+
158
+ export async function repairEncodedPaths(root) {
159
+ const encodedPaths = await collectEncodedPaths(root);
160
+ encodedPaths.sort((left, right) => right.length - left.length);
161
+
162
+ for (const source of encodedPaths) {
163
+ const parent = dirname(source);
164
+ const name = source.slice(parent.length + 1);
165
+ const destination = join(parent, decodeSupportedComponent(name));
166
+ if (source === destination) continue;
167
+ if (await exists(destination)) {
168
+ throw new Error(`Cannot repair encoded path because the destination exists: ${destination}`);
169
+ }
170
+ await rename(source, destination);
171
+ }
172
+ return encodedPaths.length;
173
+ }
174
+
175
+ async function assertFile(path, label) {
176
+ if (!(await exists(path)) || !(await stat(path)).isFile()) {
177
+ throw new Error(`${label} was not found: ${path}`);
178
+ }
179
+ }
180
+
181
+ export async function findAsciiMarkers(path, markers) {
182
+ const remaining = new Set(markers);
183
+ const longest = Math.max(...markers.map((marker) => marker.length));
184
+ let carry = '';
185
+ const input = createReadStream(path);
186
+ for await (const chunk of input) {
187
+ const text = carry + chunk.toString('latin1');
188
+ for (const marker of remaining) {
189
+ if (text.includes(marker)) remaining.delete(marker);
190
+ }
191
+ if (remaining.size === 0) break;
192
+ carry = text.slice(-(longest - 1));
193
+ }
194
+ input.destroy();
195
+ return [...remaining];
196
+ }
197
+
198
+ async function assertPortableRuntime(appRoot) {
199
+ const executable = join(appRoot, 'ChatGPT.exe');
200
+ const cli = join(appRoot, 'resources', 'codex.exe');
201
+ const asar = join(appRoot, 'resources', 'app.asar');
202
+ await assertFile(executable, 'Codex desktop executable');
203
+ await assertFile(cli, 'Bundled Codex CLI');
204
+ await assertFile(asar, 'Electron ASAR');
205
+
206
+ const missingMarkers = await findAsciiMarkers(asar, runtimeMarkers);
207
+ if (missingMarkers.length > 0) {
208
+ throw new Error(
209
+ `Codex ${manifest.source.codexVersion} does not expose the required portable runtime hooks: ${missingMarkers.join(', ')}`
210
+ );
211
+ }
212
+ }
213
+
214
+ export function launcherContents() {
215
+ return [
216
+ '@echo off',
217
+ 'setlocal',
218
+ 'set "CODEX_SPARKLE_ENABLED=false"',
219
+ 'set "CODEX_CLI_PATH=%~dp0app\\resources\\codex.exe"',
220
+ 'set "CODEX_ELECTRON_ENABLE_WINDOWS_COMPUTER_USE=1"',
221
+ 'start "" /D "%~dp0app" "%~dp0app\\ChatGPT.exe" %*',
222
+ ''
223
+ ].join('\r\n');
224
+ }
225
+
226
+ function parseInstallArguments(arguments_) {
227
+ const knownFlags = new Set(['--force', '--launch']);
228
+ const unknownFlag = arguments_.find((argument) => argument.startsWith('-') && !knownFlags.has(argument));
229
+ if (unknownFlag) throw new Error(`Unknown option: ${unknownFlag}`);
230
+ const positional = arguments_.filter((argument) => !knownFlags.has(argument));
231
+ if (positional.length > 1) throw new Error('Pass at most one install directory.');
232
+ return {
233
+ directory: positional[0],
234
+ force: arguments_.includes('--force'),
235
+ launch: arguments_.includes('--launch')
236
+ };
237
+ }
238
+
239
+ function parseMsixInstallArguments(arguments_) {
240
+ const options = parseInstallArguments(arguments_.slice(1));
241
+ const archive = arguments_[0];
242
+ if (!archive || archive.startsWith('-')) {
243
+ throw new Error('Pass the verified source MSIX as the first argument.');
244
+ }
245
+ return { ...options, archive: resolve(archive) };
246
+ }
247
+
248
+ async function validateSourceArchive(archive) {
249
+ await assertFile(archive, 'Source MSIX');
250
+ const archiveStat = await stat(archive);
251
+ if (archiveStat.size !== manifest.source.bytes) {
252
+ throw new Error(`Source MSIX size mismatch: expected ${manifest.source.bytes}, got ${archiveStat.size}`);
253
+ }
254
+ const actualHash = await sha256(archive);
255
+ if (actualHash !== manifest.source.sha256) {
256
+ throw new Error(`Source MSIX checksum mismatch: expected ${manifest.source.sha256}, got ${actualHash}`);
257
+ }
258
+ console.log(`Verified source MSIX SHA-256 ${actualHash}`);
259
+ }
260
+
261
+ function assertShortInstallPath(installDirectory) {
262
+ if (installDirectory.length > 80) {
263
+ throw new Error(`Install path is too long (${installDirectory.length} characters). Use a short path such as C:\\CodexOffline.`);
264
+ }
265
+ }
266
+
267
+ async function extractPortableArchive(archive, installDirectory, options) {
268
+ assertShortInstallPath(installDirectory);
269
+ if ((await exists(installDirectory)) && !options.force) {
270
+ throw new Error(`Install directory already exists: ${installDirectory}. Pass --force to replace it.`);
271
+ }
272
+
273
+ const stagingDirectory = `${installDirectory}.partial-${process.pid}`;
274
+ const parentDirectory = dirname(installDirectory);
275
+ if (!(await exists(parentDirectory))) await mkdir(parentDirectory, { recursive: true });
276
+ await rm(stagingDirectory, { recursive: true, force: true });
277
+
278
+ try {
279
+ await mkdir(stagingDirectory, { recursive: true });
280
+ extractArchiveWithWindowsTar(archive, stagingDirectory);
281
+
282
+ const appRoot = join(stagingDirectory, 'app');
283
+ const repairedPaths = await repairEncodedPaths(appRoot);
284
+ await assertPortableRuntime(appRoot);
285
+ await writeFile(join(stagingDirectory, 'Codex.cmd'), launcherContents(), 'ascii');
286
+ await writeFile(
287
+ join(stagingDirectory, '.utils-code-portable.json'),
288
+ `${JSON.stringify({
289
+ installedAt: new Date().toISOString(),
290
+ npmVersion: manifest.version,
291
+ codexVersion: manifest.source.codexVersion,
292
+ backendVersion: manifest.source.backendVersion,
293
+ windowsPackageVersion: manifest.source.windowsPackageVersion,
294
+ sourceSha256: manifest.source.sha256,
295
+ mode: 'portable',
296
+ repairedEncodedPaths: repairedPaths
297
+ }, null, 2)}\n`
298
+ );
299
+
300
+ if (await exists(installDirectory)) await rm(installDirectory, { recursive: true, force: true });
301
+ await rename(stagingDirectory, installDirectory);
302
+ console.log(`Installed portable Codex ${manifest.source.codexVersion} at ${installDirectory}`);
303
+ console.log(`Repaired ${repairedPaths} encoded archive paths.`);
304
+ console.log(`Run ${join(installDirectory, 'Codex.cmd')}`);
305
+
306
+ if (options.launch) await launchPortable(installDirectory);
307
+ } catch (error) {
308
+ await rm(stagingDirectory, { recursive: true, force: true });
309
+ throw error;
310
+ }
311
+ }
312
+
313
+ async function installPortable(arguments_) {
314
+ assertWindows();
315
+ const options = parseInstallArguments(arguments_);
316
+ const installDirectory = defaultInstallDirectory(options.directory);
317
+ const archive = `${installDirectory}.source-${process.pid}.zip`;
318
+ await rm(archive, { force: true });
319
+
320
+ try {
321
+ await assemble(archive);
322
+ await extractPortableArchive(archive, installDirectory, options);
323
+ } finally {
324
+ await rm(archive, { force: true });
325
+ }
326
+ }
327
+
328
+ async function installPortableMsix(arguments_) {
329
+ assertWindows();
330
+ const options = parseMsixInstallArguments(arguments_);
331
+ const installDirectory = defaultInstallDirectory(options.directory);
332
+ await validateSourceArchive(options.archive);
333
+ await extractPortableArchive(options.archive, installDirectory, options);
334
+ }
335
+
336
+ async function launchPortable(directoryArgument) {
337
+ assertWindows();
338
+ const installDirectory = defaultInstallDirectory(directoryArgument);
339
+ const executable = join(installDirectory, 'app', 'ChatGPT.exe');
340
+ const cli = join(installDirectory, 'app', 'resources', 'codex.exe');
341
+ await assertFile(executable, 'Portable Codex executable');
342
+ await assertFile(cli, 'Portable Codex CLI');
343
+ const child = spawn(executable, [], {
344
+ cwd: join(installDirectory, 'app'),
345
+ detached: true,
346
+ stdio: 'ignore',
347
+ env: {
348
+ ...process.env,
349
+ CODEX_SPARKLE_ENABLED: 'false',
350
+ CODEX_CLI_PATH: cli,
351
+ CODEX_ELECTRON_ENABLE_WINDOWS_COMPUTER_USE: '1'
352
+ }
353
+ });
354
+ await new Promise((resolveSpawn, rejectSpawn) => {
355
+ child.once('spawn', resolveSpawn);
356
+ child.once('error', rejectSpawn);
357
+ });
358
+ child.unref();
359
+ console.log(`Launched portable Codex from ${executable}`);
360
+ }
361
+
362
+ function isMainModule() {
363
+ if (!process.argv[1] || process.argv[1] === '-') return false;
364
+ try {
365
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
366
+ } catch {
367
+ return false;
368
+ }
369
+ }
370
+
371
+ if (isMainModule()) {
372
+ const [command = 'assemble', ...commandArguments] = process.argv.slice(2);
373
+
374
+ if (command === 'assemble') {
375
+ await assemble(commandArguments[0]);
376
+ } else if (command === 'install-portable' || command === 'install-offline') {
377
+ await installPortable(commandArguments);
378
+ } else if (command === 'install-portable-msix') {
379
+ await installPortableMsix(commandArguments);
380
+ } else if (command === 'launch') {
381
+ await launchPortable(commandArguments[0]);
382
+ } else if (command === '--help' || command === '-h' || command === 'help') {
383
+ usage();
384
+ } else {
385
+ usage();
386
+ process.exitCode = 1;
387
+ }
388
+ }