codex-plus-patcher 0.7.0 → 0.7.2

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.2",
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,24 @@
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_DEV_INSTANCE_ID,
17
+ DEFAULT_ELECTRON_USER_DATA,
18
+ formatLaunchDevResult,
19
+ formatSyncDevHomeResult,
20
+ launchDevApp,
21
+ syncDevHome,
22
+ } = require("./core/dev-mode");
6
23
  const { patchCodexApp } = require("./core/patch-engine");
7
24
  const { resolveReleasePatchDirectory } = require("./core/release");
8
25
  const { patchSets: builtInPatchSets } = require("./patches");
@@ -18,15 +35,30 @@ function parseArgs(argv) {
18
35
  command: argv.length === 0 ? "help" : "apply",
19
36
  source: "/Applications/Codex.app",
20
37
  target: path.join(os.homedir(), "Applications", "Codex Plus.app"),
38
+ sourceHome: path.join(os.homedir(), ".codex"),
39
+ devHome: DEFAULT_DEV_HOME,
40
+ electronUserDataPath: DEFAULT_ELECTRON_USER_DATA,
21
41
  mode: "builtin",
22
42
  releaseAsset: "codex-plus-patches.tgz",
23
43
  releaseTag: "latest",
24
44
  dryRun: false,
25
45
  json: false,
26
46
  debug: false,
47
+ apply: true,
48
+ launch: true,
49
+ keepOpen: false,
50
+ includeNativeOpenProbes: false,
51
+ noProgress: false,
52
+ quiet: false,
53
+ devInstanceId: DEFAULT_DEV_INSTANCE_ID,
27
54
  };
28
55
  const rest = [...argv];
29
56
  if (rest[0] && !rest[0].startsWith("--")) args.command = rest.shift();
57
+ if (args.command === "audit-plugins") {
58
+ args.target = DEFAULT_AUDIT_TARGET;
59
+ args.remoteDebuggingPort = DEFAULT_AUDIT_PORT;
60
+ args.devInstanceId = "audit";
61
+ }
30
62
  for (let index = 0; index < rest.length; index += 1) {
31
63
  const arg = rest[index];
32
64
  const next = () => {
@@ -36,6 +68,14 @@ function parseArgs(argv) {
36
68
  };
37
69
  if (arg === "--source") args.source = path.resolve(expandPath(next()));
38
70
  else if (arg === "--target") args.target = path.resolve(expandPath(next()));
71
+ else if (arg === "--source-home") args.sourceHome = path.resolve(expandPath(next()));
72
+ else if (arg === "--dev-home") args.devHome = path.resolve(expandPath(next()));
73
+ else if (arg === "--electron-user-data") args.electronUserDataPath = path.resolve(expandPath(next()));
74
+ else if (arg === "--dev-instance-id") args.devInstanceId = next();
75
+ else if (arg === "--remote-debugging-port" || arg === "--port") {
76
+ const value = next();
77
+ args.remoteDebuggingPort = args.command === "audit-plugins" ? Number(value) : value;
78
+ }
39
79
  else if (arg === "--asar") args.asar = path.resolve(expandPath(next()));
40
80
  else if (arg === "--file") args.file = next();
41
81
  else if (arg === "--contains") args.contains = next();
@@ -45,6 +85,12 @@ function parseArgs(argv) {
45
85
  else if (arg === "--release-tag") args.releaseTag = next();
46
86
  else if (arg === "--release-asset") args.releaseAsset = next();
47
87
  else if (arg === "--dry-run") args.dryRun = true;
88
+ else if (arg === "--no-apply") args.apply = false;
89
+ else if (arg === "--no-launch") args.launch = false;
90
+ else if (arg === "--keep-open") args.keepOpen = true;
91
+ else if (arg === "--include-native-open-probes") args.includeNativeOpenProbes = true;
92
+ else if (arg === "--no-progress") args.noProgress = true;
93
+ else if (arg === "--quiet") args.quiet = true;
48
94
  else if (arg === "--debug") args.debug = true;
49
95
  else if (arg === "--json" || arg === "--format=json") args.json = true;
50
96
  else if (arg === "--format") {
@@ -62,6 +108,9 @@ function helpText() {
62
108
  return `Usage:
63
109
  codex-plus-patcher
64
110
  codex-plus-patcher apply [options]
111
+ codex-plus-patcher audit-plugins [--json] [--quiet] [--no-progress] [--keep-open] [--include-native-open-probes]
112
+ codex-plus-patcher dev-sync [--source-home <path>] [--dev-home <path>] [--json]
113
+ codex-plus-patcher launch-dev --target <path> [--dev-home <path>] [--electron-user-data <path>] [--remote-debugging-port <port>] [--json]
65
114
  codex-plus-patcher menu-diagnostics --asar <path> [--json]
66
115
  codex-plus-patcher asar-list --asar <path> [--contains <text>] [--json]
67
116
  codex-plus-patcher asar-cat --asar <path> --file <asar-path> [--json]
@@ -69,6 +118,13 @@ function helpText() {
69
118
  Options:
70
119
  --source <path> Source Codex.app. Default: /Applications/Codex.app
71
120
  --target <path> Target Codex Plus.app. Default: ~/Applications/Codex Plus.app
121
+ --source-home <path> Original Codex home for dev-sync. Default: ~/.codex
122
+ --dev-home <path> Isolated CODEX_HOME for dev mode. Default: ./work/codex-plus-dev-home
123
+ --electron-user-data <path>
124
+ Isolated Electron userData for launch-dev. Default: ./work/codex-plus-electron-user-data
125
+ --remote-debugging-port <port>
126
+ Remote debugging port passed to launch-dev or audit-plugins
127
+ --dev-instance-id <id> Dev-only bundle identity suffix. Default: dev, or audit for audit-plugins
72
128
  --asar <path> app.asar path for ASAR readback commands
73
129
  --file <asar-path> Packed file path for asar-cat
74
130
  --contains <text> Filter asar-list paths by substring
@@ -78,6 +134,13 @@ Options:
78
134
  --release-tag <tag> Release mode tag. Default: latest
79
135
  --release-asset <name> Release mode asset. Default: codex-plus-patches.tgz
80
136
  --dry-run Select and report the patch without copying/signing
137
+ --no-apply Reuse an existing audit target without applying patches
138
+ --no-launch Attach to an existing audit app instead of launching
139
+ --keep-open Leave the audit-launched app open after probes finish
140
+ --include-native-open-probes
141
+ Also open DevTools and Mermaid viewer windows during audit probes
142
+ --no-progress Suppress audit progress and print only the final summary
143
+ --quiet Print minimal audit output
81
144
  --debug Print stack traces for CLI errors
82
145
  --json Print the machine-readable result
83
146
  `;
@@ -327,6 +390,29 @@ async function main() {
327
390
  process.stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatMenuDiagnosticsResult(result));
328
391
  return;
329
392
  }
393
+ if (args.command === "dev-sync") {
394
+ const result = syncDevHome(args);
395
+ process.stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatSyncDevHomeResult(result));
396
+ return;
397
+ }
398
+ if (args.command === "launch-dev") {
399
+ const result = launchDevApp({
400
+ targetApp: args.target,
401
+ devHome: args.devHome,
402
+ electronUserDataPath: args.electronUserDataPath,
403
+ remoteDebuggingPort: args.remoteDebuggingPort,
404
+ devInstanceId: args.devInstanceId,
405
+ });
406
+ process.stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatLaunchDevResult(result));
407
+ return;
408
+ }
409
+ if (args.command === "audit-plugins") {
410
+ const progress = await createAuditProgress(args);
411
+ const result = await runAudit(args, { progress });
412
+ process.stdout.write(args.json ? formatAuditJson(result) : formatAuditResult(result, args));
413
+ if (!result.ok) process.exitCode = 1;
414
+ return;
415
+ }
330
416
  if (args.command !== "apply") throw new Error(`Unknown command: ${args.command}`);
331
417
 
332
418
  const patchSets = await loadPatchSets(args);
@@ -357,18 +443,26 @@ if (require.main === module) {
357
443
 
358
444
  module.exports = {
359
445
  createApplyProgress,
446
+ createAuditProgress,
360
447
  expandPath,
361
448
  formatAsarCatResult,
362
449
  formatAsarListResult,
450
+ formatAuditJson,
451
+ formatAuditResult,
363
452
  formatError,
453
+ formatLaunchDevResult,
364
454
  formatMenuDiagnosticsResult,
365
455
  formatResult,
456
+ formatSyncDevHomeResult,
366
457
  helpText,
367
458
  listAsarFiles,
368
459
  loadPatchSets,
460
+ launchDevApp,
369
461
  menuDiagnostics,
370
462
  parseArgs,
371
463
  readAsarFile,
372
464
  requirePatchSetModule,
465
+ runAudit,
373
466
  shouldShowApplyProgress,
467
+ syncDevHome,
374
468
  };
package/src/core/asar.js CHANGED
@@ -11,9 +11,10 @@ function sha256File(file) {
11
11
 
12
12
  function readAsar(asarPath) {
13
13
  const buffer = fs.readFileSync(asarPath);
14
+ const headerSize = buffer.readUInt32LE(4);
14
15
  const jsonSize = buffer.readUInt32LE(12);
15
16
  const header = JSON.parse(buffer.subarray(16, 16 + jsonSize).toString("utf8"));
16
- return { buffer, dataStart: 16 + jsonSize, header };
17
+ return { buffer, dataStart: 8 + headerSize, header };
17
18
  }
18
19
 
19
20
  function walkFiles(node, prefix = "", out = []) {
@@ -99,13 +100,14 @@ function patchAsar(asarPath, fileTransforms, transformContext = {}) {
99
100
  }
100
101
 
101
102
  const json = Buffer.from(JSON.stringify(archive.header), "utf8");
103
+ const padding = Buffer.alloc((4 - (json.length % 4)) % 4);
102
104
  const header = Buffer.alloc(16);
103
105
  header.writeUInt32LE(4, 0);
104
- header.writeUInt32LE(json.length + 8, 4);
105
- header.writeUInt32LE(json.length + 4, 8);
106
+ header.writeUInt32LE(json.length + padding.length + 8, 4);
107
+ header.writeUInt32LE(json.length + padding.length + 4, 8);
106
108
  header.writeUInt32LE(json.length, 12);
107
109
 
108
- fs.writeFileSync(asarPath, Buffer.concat([header, json, ...dataBuffers]));
110
+ fs.writeFileSync(asarPath, Buffer.concat([header, json, padding, ...dataBuffers]));
109
111
  return sha256File(asarPath);
110
112
  }
111
113
 
@@ -0,0 +1,339 @@
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 { replacePlistString, 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 DEFAULT_DEV_INSTANCE_ID = "dev";
14
+ const DEV_MODE_WARNING =
15
+ "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.";
16
+
17
+ const COPY_ENTRIES = [
18
+ "config.toml",
19
+ "auth.json",
20
+ ".codex-global-state.json",
21
+ "models_cache.json",
22
+ "version.json",
23
+ "installation_id",
24
+ "history.jsonl",
25
+ "session_index.jsonl",
26
+ "AGENTS.md",
27
+ "rules",
28
+ "skills",
29
+ "plugins",
30
+ "vendor_imports",
31
+ "chrome-native-hosts.json",
32
+ "chrome-native-hosts-v2.json",
33
+ "computer-use/config.json",
34
+ ];
35
+ const SQLITE_SNAPSHOT_ENTRIES = ["state_5.sqlite", "sqlite/state_5.sqlite"];
36
+ const EXCLUDED_DEV_STATE_ENTRIES = [
37
+ "sqlite",
38
+ "cache",
39
+ "log",
40
+ "tmp",
41
+ "process_manager",
42
+ "generated_images",
43
+ "attachments",
44
+ "shell_snapshots",
45
+ ];
46
+
47
+ function isSqlitePath(filePath) {
48
+ const base = path.basename(filePath);
49
+ return base.includes(".sqlite") || base.endsWith(".sqlite-wal") || base.endsWith(".sqlite-shm");
50
+ }
51
+
52
+ function assertSafeDevHome(devHome, sourceHome) {
53
+ const resolvedDevHome = path.resolve(devHome);
54
+ const resolvedSourceHome = path.resolve(sourceHome);
55
+ if (resolvedDevHome === resolvedSourceHome) throw new Error("--dev-home must not be the same as --source-home");
56
+ if (resolvedDevHome === os.homedir() || resolvedDevHome === path.join(os.homedir(), ".codex")) {
57
+ throw new Error("--dev-home must not point at the user's real home or ~/.codex");
58
+ }
59
+ }
60
+
61
+ function copyEntry({ sourceHome, devHome, relativePath, fsImpl = fs }) {
62
+ const source = path.join(sourceHome, relativePath);
63
+ const target = path.join(devHome, relativePath);
64
+ if (!fsImpl.existsSync(source)) return null;
65
+ if (isSqlitePath(source) || relativePath.split(path.sep).includes("sqlite")) return null;
66
+ fsImpl.mkdirSync(path.dirname(target), { recursive: true });
67
+ fsImpl.rmSync(target, { recursive: true, force: true });
68
+ fsImpl.cpSync(source, target, { recursive: true, force: true, dereference: false });
69
+ return relativePath;
70
+ }
71
+
72
+ function scrubDevGlobalState(devHome, fsImpl = fs) {
73
+ const statePath = path.join(devHome, ".codex-global-state.json");
74
+ if (!fsImpl.existsSync(statePath)) return false;
75
+ const state = JSON.parse(fsImpl.readFileSync(statePath, "utf8"));
76
+ const atomState = state["electron-persisted-atom-state"];
77
+ if (atomState == null || typeof atomState !== "object") return false;
78
+ if (!Object.prototype.hasOwnProperty.call(atomState, "composer-prompt-drafts-v1")) return false;
79
+ delete atomState["composer-prompt-drafts-v1"];
80
+ fsImpl.writeFileSync(statePath, `${JSON.stringify(state)}\n`);
81
+ return true;
82
+ }
83
+
84
+ function cleanExcludedDevState(devHome, fsImpl = fs) {
85
+ for (const relativePath of EXCLUDED_DEV_STATE_ENTRIES) {
86
+ fsImpl.rmSync(path.join(devHome, relativePath), { recursive: true, force: true });
87
+ }
88
+ if (!fsImpl.existsSync(devHome)) return;
89
+ for (const entry of fsImpl.readdirSync(devHome)) {
90
+ if (isSqlitePath(entry) || entry.endsWith(".db")) {
91
+ fsImpl.rmSync(path.join(devHome, entry), { recursive: true, force: true });
92
+ }
93
+ }
94
+ }
95
+
96
+ function sqliteLiteral(value) {
97
+ return `'${String(value).replaceAll("'", "''")}'`;
98
+ }
99
+
100
+ function snapshotSqlite({ sourceHome, devHome, relativePath, fsImpl = fs, execFileSync = childProcess.execFileSync }) {
101
+ const source = path.join(sourceHome, relativePath);
102
+ const target = path.join(devHome, relativePath);
103
+ if (!fsImpl.existsSync(source)) return null;
104
+ fsImpl.mkdirSync(path.dirname(target), { recursive: true });
105
+ fsImpl.rmSync(target, { force: true });
106
+ fsImpl.rmSync(`${target}-wal`, { force: true });
107
+ fsImpl.rmSync(`${target}-shm`, { force: true });
108
+ execFileSync("sqlite3", [source, `VACUUM INTO ${sqliteLiteral(target)}`], { stdio: "pipe" });
109
+ return relativePath;
110
+ }
111
+
112
+ function linkSharedDirectory({ sourceHome, devHome, relativePath, fsImpl = fs }) {
113
+ const source = path.join(sourceHome, relativePath);
114
+ const target = path.join(devHome, relativePath);
115
+ fsImpl.rmSync(target, { recursive: true, force: true });
116
+ if (!fsImpl.existsSync(source)) return null;
117
+ fsImpl.symlinkSync(source, target, "dir");
118
+ return { source, target };
119
+ }
120
+
121
+ function syncDevHome({
122
+ sourceHome = path.join(os.homedir(), ".codex"),
123
+ devHome = DEFAULT_DEV_HOME,
124
+ fsImpl = fs,
125
+ execFileSync = childProcess.execFileSync,
126
+ } = {}) {
127
+ const resolvedSourceHome = path.resolve(sourceHome);
128
+ const resolvedDevHome = path.resolve(devHome);
129
+ assertSafeDevHome(resolvedDevHome, resolvedSourceHome);
130
+
131
+ fsImpl.mkdirSync(resolvedDevHome, { recursive: true });
132
+ cleanExcludedDevState(resolvedDevHome, fsImpl);
133
+
134
+ const copied = [];
135
+ for (const relativePath of COPY_ENTRIES) {
136
+ const copiedPath = copyEntry({
137
+ sourceHome: resolvedSourceHome,
138
+ devHome: resolvedDevHome,
139
+ relativePath,
140
+ fsImpl,
141
+ });
142
+ if (copiedPath) copied.push(copiedPath);
143
+ }
144
+ const scrubbedGlobalState = scrubDevGlobalState(resolvedDevHome, fsImpl);
145
+
146
+ const sqliteSnapshots = [];
147
+ for (const relativePath of SQLITE_SNAPSHOT_ENTRIES) {
148
+ const snapshotPath = snapshotSqlite({
149
+ sourceHome: resolvedSourceHome,
150
+ devHome: resolvedDevHome,
151
+ relativePath,
152
+ fsImpl,
153
+ execFileSync,
154
+ });
155
+ if (snapshotPath) sqliteSnapshots.push(snapshotPath);
156
+ }
157
+
158
+ const worktrees = linkSharedDirectory({
159
+ sourceHome: resolvedSourceHome,
160
+ devHome: resolvedDevHome,
161
+ relativePath: "worktrees",
162
+ fsImpl,
163
+ });
164
+ const sessions = linkSharedDirectory({
165
+ sourceHome: resolvedSourceHome,
166
+ devHome: resolvedDevHome,
167
+ relativePath: "sessions",
168
+ fsImpl,
169
+ });
170
+
171
+ return {
172
+ sourceHome: resolvedSourceHome,
173
+ devHome: resolvedDevHome,
174
+ copied,
175
+ scrubbedGlobalState,
176
+ sqliteSnapshots,
177
+ worktrees,
178
+ sessions,
179
+ warning: DEV_MODE_WARNING,
180
+ };
181
+ }
182
+
183
+ function sanitizeDevInstanceId(devInstanceId) {
184
+ if (devInstanceId == null || devInstanceId === "") return null;
185
+ const sanitized = String(devInstanceId)
186
+ .trim()
187
+ .toLowerCase()
188
+ .replace(/[^a-z0-9-]+/g, "-")
189
+ .replace(/^-+|-+$/g, "");
190
+ if (!sanitized) throw new Error("--dev-instance-id must contain at least one letter or number");
191
+ return sanitized;
192
+ }
193
+
194
+ function devBundleIdentity(devInstanceId) {
195
+ const sanitized = sanitizeDevInstanceId(devInstanceId);
196
+ if (!sanitized) return null;
197
+ return {
198
+ id: sanitized,
199
+ bundleIdentifier: `com.openai.codex-plus.${sanitized}`,
200
+ displayName: `Codex Plus (${sanitized})`,
201
+ name: `Codex Plus ${sanitized}`,
202
+ };
203
+ }
204
+
205
+ function buildLaunchDev({
206
+ targetApp,
207
+ devHome = DEFAULT_DEV_HOME,
208
+ electronUserDataPath = DEFAULT_ELECTRON_USER_DATA,
209
+ remoteDebuggingPort,
210
+ devInstanceId = DEFAULT_DEV_INSTANCE_ID,
211
+ } = {}) {
212
+ if (!targetApp) throw new Error("--target is required");
213
+ const appBinary = path.join(path.resolve(targetApp), "Contents/MacOS/Codex");
214
+ const resolvedDevHome = path.resolve(devHome);
215
+ const resolvedElectronUserDataPath = path.resolve(electronUserDataPath);
216
+ const instanceIdentity = devBundleIdentity(devInstanceId);
217
+ const args = [`--user-data-dir=${resolvedElectronUserDataPath}`];
218
+ if (remoteDebuggingPort != null) args.push(`--remote-debugging-port=${remoteDebuggingPort}`);
219
+ return {
220
+ command: appBinary,
221
+ args,
222
+ env: {
223
+ CODEX_HOME: resolvedDevHome,
224
+ CODEX_ELECTRON_USER_DATA_PATH: resolvedElectronUserDataPath,
225
+ },
226
+ instanceIdentity,
227
+ warning: DEV_MODE_WARNING,
228
+ };
229
+ }
230
+
231
+ function markDevRuntimeConfig(targetApp, { patchAsarImpl = patchAsar, setPlistBuddyValueImpl = setPlistBuddyValue } = {}) {
232
+ const target = path.resolve(targetApp);
233
+ const asarPath = path.join(target, ASAR_PATH_IN_BUNDLE);
234
+ const patchedAsarSha = patchAsarImpl(asarPath, [
235
+ [RUNTIME_MANIFEST_FILE, (text) => {
236
+ const match = text.match(/^window\.__CodexPlusRuntimeConfig=({.*?});/);
237
+ if (!match) throw new Error("Could not find Codex Plus runtime config in runtime manifest");
238
+ const config = JSON.parse(match[1]);
239
+ config.devModeStatsigFallback = true;
240
+ return text.replace(match[0], `window.__CodexPlusRuntimeConfig=${JSON.stringify(config)};`);
241
+ }],
242
+ ]);
243
+ setPlistBuddyValueImpl(
244
+ path.join(target, "Contents/Info.plist"),
245
+ ":ElectronAsarIntegrity:Resources/app.asar:hash",
246
+ patchedAsarSha,
247
+ );
248
+ return { asar: asarPath, patchedAsarSha };
249
+ }
250
+
251
+ function signDevApp(targetApp, execFileSync = childProcess.execFileSync) {
252
+ execFileSync("/usr/bin/codesign", ["--force", "--deep", "--sign", "-", path.resolve(targetApp)], { stdio: "pipe" });
253
+ return { signed: true };
254
+ }
255
+
256
+ function markDevBundleIdentity(
257
+ targetApp,
258
+ devInstanceId = DEFAULT_DEV_INSTANCE_ID,
259
+ { replacePlistStringImpl = replacePlistString } = {},
260
+ ) {
261
+ const identity = devBundleIdentity(devInstanceId);
262
+ if (!identity) return null;
263
+ const plistPath = path.join(path.resolve(targetApp), "Contents/Info.plist");
264
+ replacePlistStringImpl(plistPath, "CFBundleIdentifier", identity.bundleIdentifier);
265
+ replacePlistStringImpl(plistPath, "CFBundleDisplayName", identity.displayName);
266
+ replacePlistStringImpl(plistPath, "CFBundleName", identity.name);
267
+ return identity;
268
+ }
269
+
270
+ function launchDevApp({
271
+ spawn = childProcess.spawn,
272
+ env = process.env,
273
+ markDevRuntimeConfigImpl = markDevRuntimeConfig,
274
+ markDevBundleIdentityImpl = markDevBundleIdentity,
275
+ signDevAppImpl = signDevApp,
276
+ ...options
277
+ } = {}) {
278
+ const launch = buildLaunchDev(options);
279
+ fs.mkdirSync(launch.env.CODEX_HOME, { recursive: true });
280
+ fs.mkdirSync(launch.env.CODEX_ELECTRON_USER_DATA_PATH, { recursive: true });
281
+ const devRuntimeConfig = markDevRuntimeConfigImpl(options.targetApp);
282
+ const devBundle = markDevBundleIdentityImpl(options.targetApp, options.devInstanceId);
283
+ const devSignature = signDevAppImpl(options.targetApp);
284
+ const child = spawn(launch.command, launch.args, {
285
+ detached: true,
286
+ env: { ...env, ...launch.env },
287
+ stdio: "ignore",
288
+ });
289
+ child.unref();
290
+ return { ...launch, devRuntimeConfig, devBundle, devSignature, pid: child.pid };
291
+ }
292
+
293
+ function formatSyncDevHomeResult(result) {
294
+ const lines = [
295
+ "Codex Plus dev home synced.",
296
+ `Source home: ${result.sourceHome}`,
297
+ `Dev home: ${result.devHome}`,
298
+ `Copied: ${result.copied.length === 0 ? "(none)" : result.copied.join(", ")}`,
299
+ `Scrubbed writable state: ${result.scrubbedGlobalState ? "composer prompt drafts" : "(none)"}`,
300
+ `SQLite snapshots: ${result.sqliteSnapshots?.length ? result.sqliteSnapshots.join(", ") : "(none)"}`,
301
+ result.worktrees ? `Worktrees: ${result.worktrees.target} -> ${result.worktrees.source}` : "Worktrees: (missing)",
302
+ result.sessions ? `Sessions: ${result.sessions.target} -> ${result.sessions.source}` : "Sessions: (missing)",
303
+ `Warning: ${result.warning}`,
304
+ ];
305
+ return `${lines.join("\n")}\n`;
306
+ }
307
+
308
+ function formatLaunchDevResult(result) {
309
+ const lines = [
310
+ "Codex Plus dev app launched.",
311
+ `Command: ${result.command}`,
312
+ `Args: ${result.args.length === 0 ? "(none)" : result.args.join(" ")}`,
313
+ `CODEX_HOME: ${result.env.CODEX_HOME}`,
314
+ `CODEX_ELECTRON_USER_DATA_PATH: ${result.env.CODEX_ELECTRON_USER_DATA_PATH}`,
315
+ ];
316
+ if (result.pid != null) lines.push(`PID: ${result.pid}`);
317
+ if (result.instanceIdentity) lines.push(`Bundle identity: ${result.instanceIdentity.bundleIdentifier}`);
318
+ lines.push(`Warning: ${result.warning}`);
319
+ return `${lines.join("\n")}\n`;
320
+ }
321
+
322
+ module.exports = {
323
+ COPY_ENTRIES,
324
+ DEFAULT_DEV_HOME,
325
+ DEFAULT_DEV_INSTANCE_ID,
326
+ DEFAULT_ELECTRON_USER_DATA,
327
+ DEV_MODE_WARNING,
328
+ SQLITE_SNAPSHOT_ENTRIES,
329
+ buildLaunchDev,
330
+ devBundleIdentity,
331
+ formatLaunchDevResult,
332
+ formatSyncDevHomeResult,
333
+ launchDevApp,
334
+ markDevBundleIdentity,
335
+ markDevRuntimeConfig,
336
+ sanitizeDevInstanceId,
337
+ signDevApp,
338
+ syncDevHome,
339
+ };