codex-plus-patcher 0.2.1 → 0.3.0
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 +15 -2
- package/package.json +2 -2
- package/src/cli.js +53 -0
- package/src/core/asar.js +29 -0
- package/src/core/patch-engine.js +12 -1
- package/src/patches/26.616.41845-4198.js +31 -670
- package/src/patches/26.616.51431-4212.js +31 -742
- package/src/patches/26.616.71553-4265.js +31 -742
- package/src/patches/lib/common-patches.js +606 -0
- package/src/patches/lib/host-hooks.js +24 -0
- package/src/patches/lib/make-patch-set.js +14 -0
- package/src/patches/lib/replace.js +17 -0
- package/src/runtime/assets.js +27 -0
- package/src/runtime/plugins/aboutMetadata.js +93 -0
- package/src/runtime/plugins/diagnosticErrors.js +43 -0
- package/src/runtime/plugins/nestedRepositories.js +448 -0
- package/src/runtime/plugins/projectColors.js +153 -0
- package/src/runtime/plugins/sidebarNameBlur.js +28 -0
- package/src/runtime/plugins/userBubbleColors.js +134 -0
- package/src/runtime/runtime.js +363 -0
- package/src/runtime/worker.js +228 -0
package/README.md
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
Codex Plus is an experimental local demonstrator for changes that can be layered onto an installed Codex desktop app without redistributing the app itself. The patcher is meant for technically curious users who want to inspect, test, or iterate on small binary patch sets against their own local copy.
|
|
23
23
|
|
|
24
|
-
The current built-in
|
|
24
|
+
The current built-in Codex Plus features are delivered by versioned ASAR
|
|
25
|
+
patches plus readable runtime plugins:
|
|
25
26
|
|
|
26
27
|
- rename the copied app and add patch provenance to the About dialog
|
|
27
28
|
- expose nested repositories in the Review pane
|
|
@@ -30,11 +31,21 @@ The current built-in patches:
|
|
|
30
31
|
- add adaptive project colors for sidebar projects, grouped threads, pinned threads, user-message accents, and the composer
|
|
31
32
|
- add the `Toggle sidebar blur` command palette entry to blur sidebar chat and project names for the current session
|
|
32
33
|
|
|
34
|
+
The generated app includes a readable Codex Plus runtime under
|
|
35
|
+
`webview/assets/codex-plus/`. Versioned ASAR patches install the runtime,
|
|
36
|
+
built-in plugins, and the small Codex core hooks those plugins use. See
|
|
37
|
+
[Runtime Plugin Support](docs/plugin-support.md) for the currently supported
|
|
38
|
+
plugin interfaces.
|
|
39
|
+
|
|
33
40
|
## How It Works
|
|
34
41
|
|
|
35
42
|
The patcher reads the installed `Codex.app`, verifies the exact Codex version, bundle version, and original `Contents/Resources/app.asar` SHA-256, then selects the matching patch queue. Unsupported app versions fail closed so a patch written for one bundle is not applied to a different bundle by accident.
|
|
36
43
|
|
|
37
|
-
When applying patches, the tool copies `Codex.app` to the target `Codex Plus.app`, rewrites selected packed ASAR files with ordered text transforms, updates bundle metadata such as the app name and identifier, refreshes Electron ASAR integrity metadata, and signs the copied app ad hoc. The source app is not modified.
|
|
44
|
+
When applying patches, the tool copies `Codex.app` to the target `Codex Plus.app`, rewrites selected packed ASAR files with ordered text transforms, adds Codex Plus runtime/plugin assets, updates bundle metadata such as the app name and identifier, refreshes Electron ASAR integrity metadata, and signs the copied app ad hoc. The source app is not modified.
|
|
45
|
+
|
|
46
|
+
The patch transforms should stay small where possible: their job is increasingly
|
|
47
|
+
to add reusable plugin interfaces and hook those interfaces into Codex core
|
|
48
|
+
surfaces, while feature behavior lives in readable runtime/plugin files.
|
|
38
49
|
|
|
39
50
|
## Disclaimer
|
|
40
51
|
|
|
@@ -115,6 +126,8 @@ Patch queues export `patchSets` from `index.js`. Each patch set declares:
|
|
|
115
126
|
- an ordered `patches` array with stable patch IDs
|
|
116
127
|
- optional bundle metadata updates for the copied app
|
|
117
128
|
- ordered file transforms applied to packed ASAR files
|
|
129
|
+
- optional `assetFiles` entries added to the packed ASAR, used for runtime code,
|
|
130
|
+
built-in plugins, and plugin interface assets
|
|
118
131
|
|
|
119
132
|
Unsupported app versions fail closed.
|
|
120
133
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-plus-patcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Patch queue tool for building a local Codex Plus.app from an installed Codex.app.",
|
|
6
6
|
"repository": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"test": "node --test",
|
|
24
|
-
"check": "node
|
|
24
|
+
"check": "node scripts/check-syntax.js"
|
|
25
25
|
},
|
|
26
26
|
"engines": {
|
|
27
27
|
"node": ">=20"
|
package/src/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const os = require("node:os");
|
|
3
3
|
const path = require("node:path");
|
|
4
4
|
|
|
5
|
+
const { readAsar, walkFiles } = require("./core/asar");
|
|
5
6
|
const { patchCodexApp } = require("./core/patch-engine");
|
|
6
7
|
const { resolveReleasePatchDirectory } = require("./core/release");
|
|
7
8
|
const { patchSets: builtInPatchSets } = require("./patches");
|
|
@@ -35,6 +36,9 @@ function parseArgs(argv) {
|
|
|
35
36
|
};
|
|
36
37
|
if (arg === "--source") args.source = path.resolve(expandPath(next()));
|
|
37
38
|
else if (arg === "--target") args.target = path.resolve(expandPath(next()));
|
|
39
|
+
else if (arg === "--asar") args.asar = path.resolve(expandPath(next()));
|
|
40
|
+
else if (arg === "--file") args.file = next();
|
|
41
|
+
else if (arg === "--contains") args.contains = next();
|
|
38
42
|
else if (arg === "--mode") args.mode = next();
|
|
39
43
|
else if (arg === "--patch-dir") args.patchDir = path.resolve(expandPath(next()));
|
|
40
44
|
else if (arg === "--github-repo") args.githubRepo = next();
|
|
@@ -58,10 +62,15 @@ function helpText() {
|
|
|
58
62
|
return `Usage:
|
|
59
63
|
codex-plus-patcher
|
|
60
64
|
codex-plus-patcher apply [options]
|
|
65
|
+
codex-plus-patcher asar-list --asar <path> [--contains <text>] [--json]
|
|
66
|
+
codex-plus-patcher asar-cat --asar <path> --file <asar-path> [--json]
|
|
61
67
|
|
|
62
68
|
Options:
|
|
63
69
|
--source <path> Source Codex.app. Default: /Applications/Codex.app
|
|
64
70
|
--target <path> Target Codex Plus.app. Default: ~/Applications/Codex Plus.app
|
|
71
|
+
--asar <path> app.asar path for ASAR readback commands
|
|
72
|
+
--file <asar-path> Packed file path for asar-cat
|
|
73
|
+
--contains <text> Filter asar-list paths by substring
|
|
65
74
|
--mode <builtin|dev|release>
|
|
66
75
|
--patch-dir <path> Dev mode patch directory containing index.js
|
|
67
76
|
--github-repo <owner/repo>
|
|
@@ -125,6 +134,36 @@ function formatResult(result) {
|
|
|
125
134
|
return `${lines.join("\n")}\n`;
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
function listAsarFiles({ asar, contains }) {
|
|
138
|
+
if (!asar) throw new Error("--asar is required");
|
|
139
|
+
const archive = readAsar(asar);
|
|
140
|
+
const files = walkFiles(archive.header)
|
|
141
|
+
.map(([file]) => file)
|
|
142
|
+
.filter((file) => contains == null || file.includes(contains));
|
|
143
|
+
return { asar, files };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readAsarFile({ asar, file }) {
|
|
147
|
+
if (!asar) throw new Error("--asar is required");
|
|
148
|
+
if (!file) throw new Error("--file is required");
|
|
149
|
+
const archive = readAsar(asar);
|
|
150
|
+
const node = new Map(walkFiles(archive.header)).get(file);
|
|
151
|
+
if (!node) throw new Error(`Could not find ${file} in ${asar}`);
|
|
152
|
+
if (node.unpacked) throw new Error(`Cannot read unpacked ASAR file ${file} from ${asar}`);
|
|
153
|
+
const size = Number(node.size || 0);
|
|
154
|
+
const start = archive.dataStart + Number(node.offset || 0);
|
|
155
|
+
const content = archive.buffer.subarray(start, start + size).toString("utf8");
|
|
156
|
+
return { asar, file, size, content };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatAsarListResult(result) {
|
|
160
|
+
return result.files.length > 0 ? `${result.files.join("\n")}\n` : "";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function formatAsarCatResult(result) {
|
|
164
|
+
return result.content;
|
|
165
|
+
}
|
|
166
|
+
|
|
128
167
|
function formatError(error, { debug = false } = {}) {
|
|
129
168
|
if (debug || process.env.CODEX_PLUS_PATCHER_DEBUG === "1") return error.stack || error.message || String(error);
|
|
130
169
|
return `Error: ${error.message || String(error)}`;
|
|
@@ -183,6 +222,16 @@ async function main() {
|
|
|
183
222
|
printHelp();
|
|
184
223
|
return;
|
|
185
224
|
}
|
|
225
|
+
if (args.command === "asar-list") {
|
|
226
|
+
const result = listAsarFiles(args);
|
|
227
|
+
process.stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatAsarListResult(result));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (args.command === "asar-cat") {
|
|
231
|
+
const result = readAsarFile(args);
|
|
232
|
+
process.stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatAsarCatResult(result));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
186
235
|
if (args.command !== "apply") throw new Error(`Unknown command: ${args.command}`);
|
|
187
236
|
|
|
188
237
|
const patchSets = await loadPatchSets(args);
|
|
@@ -214,11 +263,15 @@ if (require.main === module) {
|
|
|
214
263
|
module.exports = {
|
|
215
264
|
createApplyProgress,
|
|
216
265
|
expandPath,
|
|
266
|
+
formatAsarCatResult,
|
|
267
|
+
formatAsarListResult,
|
|
217
268
|
formatError,
|
|
218
269
|
formatResult,
|
|
219
270
|
helpText,
|
|
271
|
+
listAsarFiles,
|
|
220
272
|
loadPatchSets,
|
|
221
273
|
parseArgs,
|
|
274
|
+
readAsarFile,
|
|
222
275
|
requirePatchSetModule,
|
|
223
276
|
shouldShowApplyProgress,
|
|
224
277
|
};
|
package/src/core/asar.js
CHANGED
|
@@ -26,6 +26,26 @@ function walkFiles(node, prefix = "", out = []) {
|
|
|
26
26
|
return out;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function ensureFileEntry(header, filePath) {
|
|
30
|
+
const parts = filePath.split("/").filter(Boolean);
|
|
31
|
+
if (parts.length === 0) throw new Error("Cannot add an empty ASAR path");
|
|
32
|
+
|
|
33
|
+
let node = header;
|
|
34
|
+
for (const part of parts.slice(0, -1)) {
|
|
35
|
+
node.files ||= {};
|
|
36
|
+
node.files[part] ||= { files: {} };
|
|
37
|
+
if (!node.files[part].files) throw new Error(`Cannot add ${filePath}: ${part} is already a file`);
|
|
38
|
+
node = node.files[part];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
node.files ||= {};
|
|
42
|
+
const fileName = parts.at(-1);
|
|
43
|
+
const existing = node.files[fileName];
|
|
44
|
+
if (existing?.files) throw new Error(`Cannot add ${filePath}: path is already a directory`);
|
|
45
|
+
node.files[fileName] = existing || {};
|
|
46
|
+
return node.files[fileName];
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
function fileIntegrity(buffer) {
|
|
30
50
|
const blockSize = 4 * 1024 * 1024;
|
|
31
51
|
const blocks = [];
|
|
@@ -42,6 +62,9 @@ function fileIntegrity(buffer) {
|
|
|
42
62
|
|
|
43
63
|
function patchAsar(asarPath, fileTransforms, transformContext = {}) {
|
|
44
64
|
const archive = readAsar(asarPath);
|
|
65
|
+
const assetFiles = transformContext.assetFiles || [];
|
|
66
|
+
for (const [filePath] of assetFiles) ensureFileEntry(archive.header, filePath);
|
|
67
|
+
|
|
45
68
|
const entries = walkFiles(archive.header);
|
|
46
69
|
const contents = new Map();
|
|
47
70
|
|
|
@@ -58,6 +81,11 @@ function patchAsar(asarPath, fileTransforms, transformContext = {}) {
|
|
|
58
81
|
contents.set(filePath, Buffer.from(patched, "utf8"));
|
|
59
82
|
}
|
|
60
83
|
|
|
84
|
+
for (const [filePath, content] of assetFiles) {
|
|
85
|
+
if (!contents.has(filePath)) throw new Error(`Could not add ${filePath} to app.asar`);
|
|
86
|
+
contents.set(filePath, Buffer.isBuffer(content) ? content : Buffer.from(String(content), "utf8"));
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
let dataOffset = 0;
|
|
62
90
|
const dataBuffers = [];
|
|
63
91
|
for (const [filePath, node] of entries) {
|
|
@@ -82,6 +110,7 @@ function patchAsar(asarPath, fileTransforms, transformContext = {}) {
|
|
|
82
110
|
}
|
|
83
111
|
|
|
84
112
|
module.exports = {
|
|
113
|
+
ensureFileEntry,
|
|
85
114
|
patchAsar,
|
|
86
115
|
readAsar,
|
|
87
116
|
sha256,
|
package/src/core/patch-engine.js
CHANGED
|
@@ -89,6 +89,13 @@ function collectFileTransforms(patchSet) {
|
|
|
89
89
|
return collectPatchQueue(patchSet).flatMap((patch) => patch.fileTransforms || []);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function collectAssetFiles(patchSet) {
|
|
93
|
+
return [
|
|
94
|
+
...(patchSet.assetFiles || []),
|
|
95
|
+
...collectPatchQueue(patchSet).flatMap((patch) => patch.assetFiles || []),
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
function collectInfoPlistStrings(patchSet) {
|
|
93
100
|
return Object.assign(
|
|
94
101
|
{},
|
|
@@ -139,6 +146,7 @@ async function applyPatchSet({
|
|
|
139
146
|
const setPlistBuddyStringValue = operations.setPlistBuddyValue || setPlistBuddyValue;
|
|
140
147
|
const patchQueue = collectPatchQueue(patchSet);
|
|
141
148
|
const fileTransforms = collectFileTransforms(patchSet);
|
|
149
|
+
const assetFiles = collectAssetFiles(patchSet);
|
|
142
150
|
if (dryRun) {
|
|
143
151
|
return {
|
|
144
152
|
sourceApp,
|
|
@@ -146,6 +154,7 @@ async function applyPatchSet({
|
|
|
146
154
|
patchSet: patchSet.id,
|
|
147
155
|
patches: patchQueue.map((patch) => patch.id),
|
|
148
156
|
patchedFiles: fileTransforms.map(([filePath]) => filePath),
|
|
157
|
+
addedFiles: assetFiles.map(([filePath]) => filePath),
|
|
149
158
|
dryRun: true,
|
|
150
159
|
};
|
|
151
160
|
}
|
|
@@ -162,7 +171,7 @@ async function applyPatchSet({
|
|
|
162
171
|
const targetAsar = path.join(targetApp, ASAR_PATH_IN_BUNDLE);
|
|
163
172
|
const patchContext = buildPatchContext(patchSet, patchQueue, operations);
|
|
164
173
|
const patchedAsarSha = await withProgress(progress, progressOffset + 3, progressTotal, "Patch app.asar", () =>
|
|
165
|
-
patchAsarFile(targetAsar, fileTransforms, patchContext),
|
|
174
|
+
patchAsarFile(targetAsar, fileTransforms, { ...patchContext, assetFiles }),
|
|
166
175
|
);
|
|
167
176
|
|
|
168
177
|
const plistPath = path.join(targetApp, "Contents/Info.plist");
|
|
@@ -185,6 +194,7 @@ async function applyPatchSet({
|
|
|
185
194
|
patchSet: patchSet.id,
|
|
186
195
|
patches: patchQueue.map((patch) => patch.id),
|
|
187
196
|
patchedFiles: fileTransforms.map(([filePath]) => filePath),
|
|
197
|
+
addedFiles: assetFiles.map(([filePath]) => filePath),
|
|
188
198
|
patchedAsarSha,
|
|
189
199
|
dryRun: false,
|
|
190
200
|
};
|
|
@@ -210,6 +220,7 @@ module.exports = {
|
|
|
210
220
|
ASAR_PATH_IN_BUNDLE,
|
|
211
221
|
applyPatchSet,
|
|
212
222
|
collectFileTransforms,
|
|
223
|
+
collectAssetFiles,
|
|
213
224
|
collectInfoPlistStrings,
|
|
214
225
|
collectPatchQueue,
|
|
215
226
|
getPatcherGitSha,
|