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 +2 -1
- package/package.json +2 -2
- package/src/cli.js +94 -0
- package/src/core/asar.js +6 -4
- package/src/core/dev-mode.js +339 -0
- package/src/core/plugin-audit.js +1605 -0
- package/src/patches/26.623.41415-4505.js +44 -0
- package/src/patches/26.623.42026-4514.js +44 -0
- package/src/patches/index.js +10 -1
- package/src/patches/lib/common-patches.js +621 -195
- package/src/patches/lib/hooks/message-composer.js +1 -1
- package/src/patches/lib/hooks/project-selector.js +2 -2
- package/src/patches/lib/hooks/review.js +4 -2
- package/src/patches/lib/hooks/settings-commands.js +3 -2
- package/src/patches/lib/hooks/sidebar.js +1 -6
- package/src/patches/lib/project-selector-shortcut-patch.js +141 -2
- package/src/runtime/api/index.js +3 -0
- package/src/runtime/assets.js +4 -4
- package/src/runtime/host/projectSelector.js +5 -1
- package/src/runtime/plugins/mermaidFullscreen.js +19 -6
- package/src/runtime/plugins/nestedRepositories.js +72 -11
- package/src/runtime/plugins/projectColors.js +96 -7
- package/src/runtime/plugins/projectSelectorShortcut.js +67 -12
- package/src/runtime/plugins/sidebarNameBlur.js +1 -1
- package/src/runtime/plugins/userBubbleColors.js +4 -0
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.
|
|
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:
|
|
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
|
+
};
|