codex-plus-patcher 0.7.0 → 0.7.1

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
@@ -40,7 +40,8 @@ The generated app includes a readable Codex Plus runtime under
40
40
  built-in plugins, and the small Codex core hooks those plugins use. See
41
41
  [Runtime Plugin Support](docs/plugin-support.md) for the currently supported
42
42
  plugin interfaces and [Plugin Architecture](docs/plugin-architecture.md) for
43
- the layer rules.
43
+ the layer rules. Use [Plugin Debugging](docs/plugin-debugging.md) for the
44
+ side-by-side dev launch and live proof workflow.
44
45
 
45
46
  ## How It Works
46
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plus-patcher",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
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": {
@@ -20,7 +20,7 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "scripts": {
23
- "test": "node --test",
23
+ "test": "node --test tests/*.test.js",
24
24
  "check": "node scripts/check-syntax.js",
25
25
  "check:pr": "node scripts/safe-automerge-pr.js --check",
26
26
  "pr:automerge": "node scripts/safe-automerge-pr.js"
package/src/cli.js CHANGED
@@ -2,7 +2,23 @@
2
2
  const os = require("node:os");
3
3
  const path = require("node:path");
4
4
 
5
+ const {
6
+ createAuditProgress,
7
+ DEFAULT_PORT: DEFAULT_AUDIT_PORT,
8
+ DEFAULT_TARGET: DEFAULT_AUDIT_TARGET,
9
+ formatAuditJson,
10
+ formatAuditResult,
11
+ runAudit,
12
+ } = require("./core/plugin-audit");
5
13
  const { readAsar, walkFiles } = require("./core/asar");
14
+ const {
15
+ DEFAULT_DEV_HOME,
16
+ DEFAULT_ELECTRON_USER_DATA,
17
+ formatLaunchDevResult,
18
+ formatSyncDevHomeResult,
19
+ launchDevApp,
20
+ syncDevHome,
21
+ } = require("./core/dev-mode");
6
22
  const { patchCodexApp } = require("./core/patch-engine");
7
23
  const { resolveReleasePatchDirectory } = require("./core/release");
8
24
  const { patchSets: builtInPatchSets } = require("./patches");
@@ -18,15 +34,28 @@ function parseArgs(argv) {
18
34
  command: argv.length === 0 ? "help" : "apply",
19
35
  source: "/Applications/Codex.app",
20
36
  target: path.join(os.homedir(), "Applications", "Codex Plus.app"),
37
+ sourceHome: path.join(os.homedir(), ".codex"),
38
+ devHome: DEFAULT_DEV_HOME,
39
+ electronUserDataPath: DEFAULT_ELECTRON_USER_DATA,
21
40
  mode: "builtin",
22
41
  releaseAsset: "codex-plus-patches.tgz",
23
42
  releaseTag: "latest",
24
43
  dryRun: false,
25
44
  json: false,
26
45
  debug: false,
46
+ apply: true,
47
+ launch: true,
48
+ keepOpen: false,
49
+ includeNativeOpenProbes: false,
50
+ noProgress: false,
51
+ quiet: false,
27
52
  };
28
53
  const rest = [...argv];
29
54
  if (rest[0] && !rest[0].startsWith("--")) args.command = rest.shift();
55
+ if (args.command === "audit-plugins") {
56
+ args.target = DEFAULT_AUDIT_TARGET;
57
+ args.remoteDebuggingPort = DEFAULT_AUDIT_PORT;
58
+ }
30
59
  for (let index = 0; index < rest.length; index += 1) {
31
60
  const arg = rest[index];
32
61
  const next = () => {
@@ -36,6 +65,13 @@ function parseArgs(argv) {
36
65
  };
37
66
  if (arg === "--source") args.source = path.resolve(expandPath(next()));
38
67
  else if (arg === "--target") args.target = path.resolve(expandPath(next()));
68
+ else if (arg === "--source-home") args.sourceHome = path.resolve(expandPath(next()));
69
+ else if (arg === "--dev-home") args.devHome = path.resolve(expandPath(next()));
70
+ else if (arg === "--electron-user-data") args.electronUserDataPath = path.resolve(expandPath(next()));
71
+ else if (arg === "--remote-debugging-port" || arg === "--port") {
72
+ const value = next();
73
+ args.remoteDebuggingPort = args.command === "audit-plugins" ? Number(value) : value;
74
+ }
39
75
  else if (arg === "--asar") args.asar = path.resolve(expandPath(next()));
40
76
  else if (arg === "--file") args.file = next();
41
77
  else if (arg === "--contains") args.contains = next();
@@ -45,6 +81,12 @@ function parseArgs(argv) {
45
81
  else if (arg === "--release-tag") args.releaseTag = next();
46
82
  else if (arg === "--release-asset") args.releaseAsset = next();
47
83
  else if (arg === "--dry-run") args.dryRun = true;
84
+ else if (arg === "--no-apply") args.apply = false;
85
+ else if (arg === "--no-launch") args.launch = false;
86
+ else if (arg === "--keep-open") args.keepOpen = true;
87
+ else if (arg === "--include-native-open-probes") args.includeNativeOpenProbes = true;
88
+ else if (arg === "--no-progress") args.noProgress = true;
89
+ else if (arg === "--quiet") args.quiet = true;
48
90
  else if (arg === "--debug") args.debug = true;
49
91
  else if (arg === "--json" || arg === "--format=json") args.json = true;
50
92
  else if (arg === "--format") {
@@ -62,6 +104,9 @@ function helpText() {
62
104
  return `Usage:
63
105
  codex-plus-patcher
64
106
  codex-plus-patcher apply [options]
107
+ codex-plus-patcher audit-plugins [--json] [--quiet] [--no-progress] [--keep-open] [--include-native-open-probes]
108
+ codex-plus-patcher dev-sync [--source-home <path>] [--dev-home <path>] [--json]
109
+ codex-plus-patcher launch-dev --target <path> [--dev-home <path>] [--electron-user-data <path>] [--remote-debugging-port <port>] [--json]
65
110
  codex-plus-patcher menu-diagnostics --asar <path> [--json]
66
111
  codex-plus-patcher asar-list --asar <path> [--contains <text>] [--json]
67
112
  codex-plus-patcher asar-cat --asar <path> --file <asar-path> [--json]
@@ -69,6 +114,12 @@ function helpText() {
69
114
  Options:
70
115
  --source <path> Source Codex.app. Default: /Applications/Codex.app
71
116
  --target <path> Target Codex Plus.app. Default: ~/Applications/Codex Plus.app
117
+ --source-home <path> Original Codex home for dev-sync. Default: ~/.codex
118
+ --dev-home <path> Isolated CODEX_HOME for dev mode. Default: ./work/codex-plus-dev-home
119
+ --electron-user-data <path>
120
+ Isolated Electron userData for launch-dev. Default: ./work/codex-plus-electron-user-data
121
+ --remote-debugging-port <port>
122
+ Remote debugging port passed to launch-dev or audit-plugins
72
123
  --asar <path> app.asar path for ASAR readback commands
73
124
  --file <asar-path> Packed file path for asar-cat
74
125
  --contains <text> Filter asar-list paths by substring
@@ -78,6 +129,13 @@ Options:
78
129
  --release-tag <tag> Release mode tag. Default: latest
79
130
  --release-asset <name> Release mode asset. Default: codex-plus-patches.tgz
80
131
  --dry-run Select and report the patch without copying/signing
132
+ --no-apply Reuse an existing audit target without applying patches
133
+ --no-launch Attach to an existing audit app instead of launching
134
+ --keep-open Leave the audit-launched app open after probes finish
135
+ --include-native-open-probes
136
+ Also open DevTools and Mermaid viewer windows during audit probes
137
+ --no-progress Suppress audit progress and print only the final summary
138
+ --quiet Print minimal audit output
81
139
  --debug Print stack traces for CLI errors
82
140
  --json Print the machine-readable result
83
141
  `;
@@ -327,6 +385,28 @@ async function main() {
327
385
  process.stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatMenuDiagnosticsResult(result));
328
386
  return;
329
387
  }
388
+ if (args.command === "dev-sync") {
389
+ const result = syncDevHome(args);
390
+ process.stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatSyncDevHomeResult(result));
391
+ return;
392
+ }
393
+ if (args.command === "launch-dev") {
394
+ const result = launchDevApp({
395
+ targetApp: args.target,
396
+ devHome: args.devHome,
397
+ electronUserDataPath: args.electronUserDataPath,
398
+ remoteDebuggingPort: args.remoteDebuggingPort,
399
+ });
400
+ process.stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatLaunchDevResult(result));
401
+ return;
402
+ }
403
+ if (args.command === "audit-plugins") {
404
+ const progress = await createAuditProgress(args);
405
+ const result = await runAudit(args, { progress });
406
+ process.stdout.write(args.json ? formatAuditJson(result) : formatAuditResult(result, args));
407
+ if (!result.ok) process.exitCode = 1;
408
+ return;
409
+ }
330
410
  if (args.command !== "apply") throw new Error(`Unknown command: ${args.command}`);
331
411
 
332
412
  const patchSets = await loadPatchSets(args);
@@ -357,18 +437,26 @@ if (require.main === module) {
357
437
 
358
438
  module.exports = {
359
439
  createApplyProgress,
440
+ createAuditProgress,
360
441
  expandPath,
361
442
  formatAsarCatResult,
362
443
  formatAsarListResult,
444
+ formatAuditJson,
445
+ formatAuditResult,
363
446
  formatError,
447
+ formatLaunchDevResult,
364
448
  formatMenuDiagnosticsResult,
365
449
  formatResult,
450
+ formatSyncDevHomeResult,
366
451
  helpText,
367
452
  listAsarFiles,
368
453
  loadPatchSets,
454
+ launchDevApp,
369
455
  menuDiagnostics,
370
456
  parseArgs,
371
457
  readAsarFile,
372
458
  requirePatchSetModule,
459
+ runAudit,
373
460
  shouldShowApplyProgress,
461
+ syncDevHome,
374
462
  };
@@ -0,0 +1,274 @@
1
+ const childProcess = require("node:child_process");
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+
6
+ const { patchAsar } = require("./asar");
7
+ const { setPlistBuddyValue } = require("./plist");
8
+
9
+ const ASAR_PATH_IN_BUNDLE = "Contents/Resources/app.asar";
10
+ const RUNTIME_MANIFEST_FILE = "webview/assets/codex-plus/runtime-manifest.js";
11
+ const DEFAULT_DEV_HOME = path.resolve("work/codex-plus-dev-home");
12
+ const DEFAULT_ELECTRON_USER_DATA = path.resolve("work/codex-plus-electron-user-data");
13
+ const DEV_MODE_WARNING =
14
+ "Dev mode shares the original Codex worktrees. Use it for UI/plugin validation; do not edit the same checkout from regular Codex and Codex Plus at the same time.";
15
+
16
+ const COPY_ENTRIES = [
17
+ "config.toml",
18
+ "auth.json",
19
+ ".codex-global-state.json",
20
+ "models_cache.json",
21
+ "version.json",
22
+ "installation_id",
23
+ "history.jsonl",
24
+ "session_index.jsonl",
25
+ "AGENTS.md",
26
+ "rules",
27
+ "skills",
28
+ "plugins",
29
+ "vendor_imports",
30
+ "chrome-native-hosts.json",
31
+ "chrome-native-hosts-v2.json",
32
+ "computer-use/config.json",
33
+ ];
34
+ const SQLITE_SNAPSHOT_ENTRIES = ["state_5.sqlite", "sqlite/state_5.sqlite"];
35
+ const EXCLUDED_DEV_STATE_ENTRIES = [
36
+ "sqlite",
37
+ "cache",
38
+ "log",
39
+ "tmp",
40
+ "process_manager",
41
+ "generated_images",
42
+ "attachments",
43
+ "shell_snapshots",
44
+ ];
45
+
46
+ function isSqlitePath(filePath) {
47
+ const base = path.basename(filePath);
48
+ return base.includes(".sqlite") || base.endsWith(".sqlite-wal") || base.endsWith(".sqlite-shm");
49
+ }
50
+
51
+ function assertSafeDevHome(devHome, sourceHome) {
52
+ const resolvedDevHome = path.resolve(devHome);
53
+ const resolvedSourceHome = path.resolve(sourceHome);
54
+ if (resolvedDevHome === resolvedSourceHome) throw new Error("--dev-home must not be the same as --source-home");
55
+ if (resolvedDevHome === os.homedir() || resolvedDevHome === path.join(os.homedir(), ".codex")) {
56
+ throw new Error("--dev-home must not point at the user's real home or ~/.codex");
57
+ }
58
+ }
59
+
60
+ function copyEntry({ sourceHome, devHome, relativePath, fsImpl = fs }) {
61
+ const source = path.join(sourceHome, relativePath);
62
+ const target = path.join(devHome, relativePath);
63
+ if (!fsImpl.existsSync(source)) return null;
64
+ if (isSqlitePath(source) || relativePath.split(path.sep).includes("sqlite")) return null;
65
+ fsImpl.mkdirSync(path.dirname(target), { recursive: true });
66
+ fsImpl.rmSync(target, { recursive: true, force: true });
67
+ fsImpl.cpSync(source, target, { recursive: true, force: true, dereference: false });
68
+ return relativePath;
69
+ }
70
+
71
+ function scrubDevGlobalState(devHome, fsImpl = fs) {
72
+ const statePath = path.join(devHome, ".codex-global-state.json");
73
+ if (!fsImpl.existsSync(statePath)) return false;
74
+ const state = JSON.parse(fsImpl.readFileSync(statePath, "utf8"));
75
+ const atomState = state["electron-persisted-atom-state"];
76
+ if (atomState == null || typeof atomState !== "object") return false;
77
+ if (!Object.prototype.hasOwnProperty.call(atomState, "composer-prompt-drafts-v1")) return false;
78
+ delete atomState["composer-prompt-drafts-v1"];
79
+ fsImpl.writeFileSync(statePath, `${JSON.stringify(state)}\n`);
80
+ return true;
81
+ }
82
+
83
+ function cleanExcludedDevState(devHome, fsImpl = fs) {
84
+ for (const relativePath of EXCLUDED_DEV_STATE_ENTRIES) {
85
+ fsImpl.rmSync(path.join(devHome, relativePath), { recursive: true, force: true });
86
+ }
87
+ if (!fsImpl.existsSync(devHome)) return;
88
+ for (const entry of fsImpl.readdirSync(devHome)) {
89
+ if (isSqlitePath(entry) || entry.endsWith(".db")) {
90
+ fsImpl.rmSync(path.join(devHome, entry), { recursive: true, force: true });
91
+ }
92
+ }
93
+ }
94
+
95
+ function sqliteLiteral(value) {
96
+ return `'${String(value).replaceAll("'", "''")}'`;
97
+ }
98
+
99
+ function snapshotSqlite({ sourceHome, devHome, relativePath, fsImpl = fs, execFileSync = childProcess.execFileSync }) {
100
+ const source = path.join(sourceHome, relativePath);
101
+ const target = path.join(devHome, relativePath);
102
+ if (!fsImpl.existsSync(source)) return null;
103
+ fsImpl.mkdirSync(path.dirname(target), { recursive: true });
104
+ fsImpl.rmSync(target, { force: true });
105
+ fsImpl.rmSync(`${target}-wal`, { force: true });
106
+ fsImpl.rmSync(`${target}-shm`, { force: true });
107
+ execFileSync("sqlite3", [source, `VACUUM INTO ${sqliteLiteral(target)}`], { stdio: "pipe" });
108
+ return relativePath;
109
+ }
110
+
111
+ function linkSharedDirectory({ sourceHome, devHome, relativePath, fsImpl = fs }) {
112
+ const source = path.join(sourceHome, relativePath);
113
+ const target = path.join(devHome, relativePath);
114
+ fsImpl.rmSync(target, { recursive: true, force: true });
115
+ if (!fsImpl.existsSync(source)) return null;
116
+ fsImpl.symlinkSync(source, target, "dir");
117
+ return { source, target };
118
+ }
119
+
120
+ function syncDevHome({
121
+ sourceHome = path.join(os.homedir(), ".codex"),
122
+ devHome = DEFAULT_DEV_HOME,
123
+ fsImpl = fs,
124
+ execFileSync = childProcess.execFileSync,
125
+ } = {}) {
126
+ const resolvedSourceHome = path.resolve(sourceHome);
127
+ const resolvedDevHome = path.resolve(devHome);
128
+ assertSafeDevHome(resolvedDevHome, resolvedSourceHome);
129
+
130
+ fsImpl.mkdirSync(resolvedDevHome, { recursive: true });
131
+ cleanExcludedDevState(resolvedDevHome, fsImpl);
132
+
133
+ const copied = [];
134
+ for (const relativePath of COPY_ENTRIES) {
135
+ const copiedPath = copyEntry({
136
+ sourceHome: resolvedSourceHome,
137
+ devHome: resolvedDevHome,
138
+ relativePath,
139
+ fsImpl,
140
+ });
141
+ if (copiedPath) copied.push(copiedPath);
142
+ }
143
+ const scrubbedGlobalState = scrubDevGlobalState(resolvedDevHome, fsImpl);
144
+
145
+ const sqliteSnapshots = [];
146
+ for (const relativePath of SQLITE_SNAPSHOT_ENTRIES) {
147
+ const snapshotPath = snapshotSqlite({
148
+ sourceHome: resolvedSourceHome,
149
+ devHome: resolvedDevHome,
150
+ relativePath,
151
+ fsImpl,
152
+ execFileSync,
153
+ });
154
+ if (snapshotPath) sqliteSnapshots.push(snapshotPath);
155
+ }
156
+
157
+ const worktrees = linkSharedDirectory({
158
+ sourceHome: resolvedSourceHome,
159
+ devHome: resolvedDevHome,
160
+ relativePath: "worktrees",
161
+ fsImpl,
162
+ });
163
+ const sessions = linkSharedDirectory({
164
+ sourceHome: resolvedSourceHome,
165
+ devHome: resolvedDevHome,
166
+ relativePath: "sessions",
167
+ fsImpl,
168
+ });
169
+
170
+ return {
171
+ sourceHome: resolvedSourceHome,
172
+ devHome: resolvedDevHome,
173
+ copied,
174
+ scrubbedGlobalState,
175
+ sqliteSnapshots,
176
+ worktrees,
177
+ sessions,
178
+ warning: DEV_MODE_WARNING,
179
+ };
180
+ }
181
+
182
+ function buildLaunchDev({ targetApp, devHome = DEFAULT_DEV_HOME, electronUserDataPath = DEFAULT_ELECTRON_USER_DATA, remoteDebuggingPort } = {}) {
183
+ if (!targetApp) throw new Error("--target is required");
184
+ const appBinary = path.join(path.resolve(targetApp), "Contents/MacOS/Codex");
185
+ const resolvedDevHome = path.resolve(devHome);
186
+ const resolvedElectronUserDataPath = path.resolve(electronUserDataPath);
187
+ const args = [`--user-data-dir=${resolvedElectronUserDataPath}`];
188
+ if (remoteDebuggingPort != null) args.push(`--remote-debugging-port=${remoteDebuggingPort}`);
189
+ return {
190
+ command: appBinary,
191
+ args,
192
+ env: {
193
+ CODEX_HOME: resolvedDevHome,
194
+ CODEX_ELECTRON_USER_DATA_PATH: resolvedElectronUserDataPath,
195
+ },
196
+ warning: DEV_MODE_WARNING,
197
+ };
198
+ }
199
+
200
+ function markDevRuntimeConfig(targetApp, { patchAsarImpl = patchAsar, setPlistBuddyValueImpl = setPlistBuddyValue } = {}) {
201
+ const target = path.resolve(targetApp);
202
+ const asarPath = path.join(target, ASAR_PATH_IN_BUNDLE);
203
+ const patchedAsarSha = patchAsarImpl(asarPath, [
204
+ [RUNTIME_MANIFEST_FILE, (text) => {
205
+ const match = text.match(/^window\.__CodexPlusRuntimeConfig=({.*?});/);
206
+ if (!match) throw new Error("Could not find Codex Plus runtime config in runtime manifest");
207
+ const config = JSON.parse(match[1]);
208
+ config.devModeStatsigFallback = true;
209
+ return text.replace(match[0], `window.__CodexPlusRuntimeConfig=${JSON.stringify(config)};`);
210
+ }],
211
+ ]);
212
+ setPlistBuddyValueImpl(
213
+ path.join(target, "Contents/Info.plist"),
214
+ ":ElectronAsarIntegrity:Resources/app.asar:hash",
215
+ patchedAsarSha,
216
+ );
217
+ return { asar: asarPath, patchedAsarSha };
218
+ }
219
+
220
+ function launchDevApp({ spawn = childProcess.spawn, env = process.env, markDevRuntimeConfigImpl = markDevRuntimeConfig, ...options } = {}) {
221
+ const launch = buildLaunchDev(options);
222
+ fs.mkdirSync(launch.env.CODEX_HOME, { recursive: true });
223
+ fs.mkdirSync(launch.env.CODEX_ELECTRON_USER_DATA_PATH, { recursive: true });
224
+ const devRuntimeConfig = markDevRuntimeConfigImpl(options.targetApp);
225
+ const child = spawn(launch.command, launch.args, {
226
+ detached: true,
227
+ env: { ...env, ...launch.env },
228
+ stdio: "ignore",
229
+ });
230
+ child.unref();
231
+ return { ...launch, devRuntimeConfig, pid: child.pid };
232
+ }
233
+
234
+ function formatSyncDevHomeResult(result) {
235
+ const lines = [
236
+ "Codex Plus dev home synced.",
237
+ `Source home: ${result.sourceHome}`,
238
+ `Dev home: ${result.devHome}`,
239
+ `Copied: ${result.copied.length === 0 ? "(none)" : result.copied.join(", ")}`,
240
+ `Scrubbed writable state: ${result.scrubbedGlobalState ? "composer prompt drafts" : "(none)"}`,
241
+ `SQLite snapshots: ${result.sqliteSnapshots?.length ? result.sqliteSnapshots.join(", ") : "(none)"}`,
242
+ result.worktrees ? `Worktrees: ${result.worktrees.target} -> ${result.worktrees.source}` : "Worktrees: (missing)",
243
+ result.sessions ? `Sessions: ${result.sessions.target} -> ${result.sessions.source}` : "Sessions: (missing)",
244
+ `Warning: ${result.warning}`,
245
+ ];
246
+ return `${lines.join("\n")}\n`;
247
+ }
248
+
249
+ function formatLaunchDevResult(result) {
250
+ const lines = [
251
+ "Codex Plus dev app launched.",
252
+ `Command: ${result.command}`,
253
+ `Args: ${result.args.length === 0 ? "(none)" : result.args.join(" ")}`,
254
+ `CODEX_HOME: ${result.env.CODEX_HOME}`,
255
+ `CODEX_ELECTRON_USER_DATA_PATH: ${result.env.CODEX_ELECTRON_USER_DATA_PATH}`,
256
+ ];
257
+ if (result.pid != null) lines.push(`PID: ${result.pid}`);
258
+ lines.push(`Warning: ${result.warning}`);
259
+ return `${lines.join("\n")}\n`;
260
+ }
261
+
262
+ module.exports = {
263
+ COPY_ENTRIES,
264
+ DEFAULT_DEV_HOME,
265
+ DEFAULT_ELECTRON_USER_DATA,
266
+ DEV_MODE_WARNING,
267
+ SQLITE_SNAPSHOT_ENTRIES,
268
+ buildLaunchDev,
269
+ formatLaunchDevResult,
270
+ formatSyncDevHomeResult,
271
+ launchDevApp,
272
+ markDevRuntimeConfig,
273
+ syncDevHome,
274
+ };