minecodex 0.1.0 → 0.1.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 +6 -1
- package/features/images/README.md +1 -1
- package/features/images/codex-feature.json +1 -1
- package/features/model-slider/codex-feature.json +1 -1
- package/features/notes/codex-feature.json +1 -1
- package/package.json +4 -2
- package/packages/cli/native/MineCodex.app/Contents/Info.plist +28 -0
- package/packages/cli/native/MineCodex.app/Contents/Library/LaunchAgents/com.vontean.minecodex.background.plist +19 -0
- package/packages/cli/native/MineCodex.app/Contents/MacOS/MineCodexServiceManager +0 -0
- package/packages/cli/native/MineCodex.app/Contents/Resources/MineCodexServiceHost +0 -0
- package/packages/cli/native/MineCodex.app/Contents/_CodeSignature/CodeResources +135 -0
- package/packages/cli/native/README.md +18 -0
- package/packages/cli/src/commands.mjs +13 -7
- package/packages/cli/src/paths.mjs +8 -0
- package/packages/cli/src/platform.mjs +183 -50
- package/packages/runtime-host/README.md +3 -0
- package/packages/runtime-host/src/codex-runtime.mjs +24 -1
package/README.md
CHANGED
|
@@ -17,6 +17,10 @@ npm install -g minecodex
|
|
|
17
17
|
mcx install
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
`mcx install` 会关闭当前正在运行的 Codex 窗口,再以 MineCodex 托管方式重新打开;
|
|
21
|
+
请先保存正在编辑的内容,并让安装命令完整运行到成功提示。安装后,macOS 的
|
|
22
|
+
“登录项与扩展”中会显示名为 **MineCodex** 的后台项目,用于在登录后恢复插件。
|
|
23
|
+
|
|
20
24
|
安装后三个插件默认开启。打开本地控制台:
|
|
21
25
|
|
|
22
26
|
```bash
|
|
@@ -34,7 +38,8 @@ mcx status [--json]
|
|
|
34
38
|
mcx restart
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
关闭插件只会停用对应功能,不会删除数据;保存开关后运行 `mcx restart` 应用变更。
|
|
42
|
+
`mcx uninstall` 会移除名为 MineCodex 的后台项目,并默认保留 MineCodex 设置、
|
|
38
43
|
Notes 数据和 Images 索引;只有显式传入 `--purge` 才会删除 MineCodex 自有数据。
|
|
39
44
|
MineCodex 永远不会删除 `~/.codex/generated_images` 中的原图或 Notes 引用的源文件。
|
|
40
45
|
|
|
@@ -87,7 +87,7 @@ HTTP + Images UI
|
|
|
87
87
|
- Images 服务需要保持运行,才会持续监听新图片;侧边栏注册由 `CodexRuntimeHost` 统一维持。
|
|
88
88
|
- 每次服务启动都会先开放 health,再在后台补扫停机期间新增的图片并刷新来源名称;初始补扫完成后,运行期间的新图片由文件监听自动加入。手动 Refresh 会再次补扫来源和 Archive 状态,但不会重置筛选、图板模式、详情、滚动位置或列数。刷新 ring 至少显示 1 秒。
|
|
89
89
|
- 侧边栏使用 Codex 当前未公开的 CDP / DOM 能力,Codex UI 更新后只需调整统一宿主;独立 Web UI 和路径索引不受影响。
|
|
90
|
-
- 当前 Codex 的 CSP 会屏蔽普通本地 iframe
|
|
90
|
+
- 当前 Codex 的 CSP 会屏蔽普通本地 iframe。统一宿主会先启用 CSP bypass 并注册 document-start 脚本;尚未完成该初始化的首次或旧版 document 会受控 reload 一次。完成初始化后会留下 document 标记,后续 RuntimeHost 重启只替换旧的 MineCodex runtime,不再刷新 Codex 页面(Issue #1)。
|
|
91
91
|
- 详情打开时会从本机 Session JSONL 按需读取该 ImageGen item 的真实 `revised_prompt` 并缓存到索引;历史记录缺少该事件时明确显示 Prompt 不可用,不从附近文本猜测。
|
|
92
92
|
- 当前没有发现 Codex 会因运行时间增长而自动清理 `generated_images` 的证据。如果未来观察到真实清理,再决定是否增加可选备份。
|
|
93
93
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecodex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Lightweight, local-first plugins for the Codex desktop app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"LICENSE",
|
|
17
17
|
"packages/cli/bin",
|
|
18
18
|
"packages/cli/src",
|
|
19
|
+
"packages/cli/native/MineCodex.app",
|
|
19
20
|
"packages/runtime-host/src",
|
|
20
21
|
"features/model-slider/assets",
|
|
21
22
|
"features/model-slider/codex-feature.json",
|
|
@@ -39,10 +40,11 @@
|
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
42
|
"start": "node packages/cli/bin/mcx.mjs serve",
|
|
42
|
-
"test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs",
|
|
43
|
+
"test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs apps/dashboard/test/*.test.mjs",
|
|
43
44
|
"test:cli": "node --test packages/cli/test/*.test.mjs",
|
|
44
45
|
"test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
|
|
45
46
|
"check": "node --check packages/cli/bin/mcx.mjs && node --check packages/cli/src/*.mjs && node --check packages/runtime-host/src/*.mjs",
|
|
47
|
+
"build:service": "scripts/build-service-app.zsh",
|
|
46
48
|
"pack:check": "npm pack --dry-run",
|
|
47
49
|
"pack:audit": "node scripts/audit-package.mjs",
|
|
48
50
|
"prepublishOnly": "npm test && npm run check && npm run pack:audit"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
+
<string>en</string>
|
|
7
|
+
<key>CFBundleDisplayName</key>
|
|
8
|
+
<string>MineCodex</string>
|
|
9
|
+
<key>CFBundleExecutable</key>
|
|
10
|
+
<string>MineCodexServiceManager</string>
|
|
11
|
+
<key>CFBundleIdentifier</key>
|
|
12
|
+
<string>com.vontean.minecodex.background-service-manager</string>
|
|
13
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
14
|
+
<string>6.0</string>
|
|
15
|
+
<key>CFBundleName</key>
|
|
16
|
+
<string>MineCodex</string>
|
|
17
|
+
<key>CFBundlePackageType</key>
|
|
18
|
+
<string>APPL</string>
|
|
19
|
+
<key>CFBundleShortVersionString</key>
|
|
20
|
+
<string>1.0.0</string>
|
|
21
|
+
<key>CFBundleVersion</key>
|
|
22
|
+
<string>1</string>
|
|
23
|
+
<key>LSBackgroundOnly</key>
|
|
24
|
+
<true/>
|
|
25
|
+
<key>LSMinimumSystemVersion</key>
|
|
26
|
+
<string>13.0</string>
|
|
27
|
+
</dict>
|
|
28
|
+
</plist>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.vontean.minecodex.background</string>
|
|
7
|
+
<key>BundleProgram</key>
|
|
8
|
+
<string>Contents/Resources/MineCodexServiceHost</string>
|
|
9
|
+
<key>RunAtLoad</key>
|
|
10
|
+
<true/>
|
|
11
|
+
<key>KeepAlive</key>
|
|
12
|
+
<dict>
|
|
13
|
+
<key>SuccessfulExit</key>
|
|
14
|
+
<false/>
|
|
15
|
+
</dict>
|
|
16
|
+
<key>ProcessType</key>
|
|
17
|
+
<string>Interactive</string>
|
|
18
|
+
</dict>
|
|
19
|
+
</plist>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>files</key>
|
|
6
|
+
<dict>
|
|
7
|
+
<key>Resources/MineCodexServiceHost</key>
|
|
8
|
+
<data>
|
|
9
|
+
kB0iYBcULzqcS1yOgYpY3LZjeq4=
|
|
10
|
+
</data>
|
|
11
|
+
</dict>
|
|
12
|
+
<key>files2</key>
|
|
13
|
+
<dict>
|
|
14
|
+
<key>Library/LaunchAgents/com.vontean.minecodex.background.plist</key>
|
|
15
|
+
<dict>
|
|
16
|
+
<key>hash2</key>
|
|
17
|
+
<data>
|
|
18
|
+
mGWkf+X17Pau3uy8xUUB1s+JXofeKYKNRzFPk3FXeaA=
|
|
19
|
+
</data>
|
|
20
|
+
</dict>
|
|
21
|
+
<key>Resources/MineCodexServiceHost</key>
|
|
22
|
+
<dict>
|
|
23
|
+
<key>hash2</key>
|
|
24
|
+
<data>
|
|
25
|
+
b9bnzjgASA30QyXiczJMxHbOYKm7WG+j24ZfZ1povzc=
|
|
26
|
+
</data>
|
|
27
|
+
</dict>
|
|
28
|
+
</dict>
|
|
29
|
+
<key>rules</key>
|
|
30
|
+
<dict>
|
|
31
|
+
<key>^Resources/</key>
|
|
32
|
+
<true/>
|
|
33
|
+
<key>^Resources/.*\.lproj/</key>
|
|
34
|
+
<dict>
|
|
35
|
+
<key>optional</key>
|
|
36
|
+
<true/>
|
|
37
|
+
<key>weight</key>
|
|
38
|
+
<real>1000</real>
|
|
39
|
+
</dict>
|
|
40
|
+
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
|
41
|
+
<dict>
|
|
42
|
+
<key>omit</key>
|
|
43
|
+
<true/>
|
|
44
|
+
<key>weight</key>
|
|
45
|
+
<real>1100</real>
|
|
46
|
+
</dict>
|
|
47
|
+
<key>^Resources/Base\.lproj/</key>
|
|
48
|
+
<dict>
|
|
49
|
+
<key>weight</key>
|
|
50
|
+
<real>1010</real>
|
|
51
|
+
</dict>
|
|
52
|
+
<key>^version.plist$</key>
|
|
53
|
+
<true/>
|
|
54
|
+
</dict>
|
|
55
|
+
<key>rules2</key>
|
|
56
|
+
<dict>
|
|
57
|
+
<key>.*\.dSYM($|/)</key>
|
|
58
|
+
<dict>
|
|
59
|
+
<key>weight</key>
|
|
60
|
+
<real>11</real>
|
|
61
|
+
</dict>
|
|
62
|
+
<key>^(.*/)?\.DS_Store$</key>
|
|
63
|
+
<dict>
|
|
64
|
+
<key>omit</key>
|
|
65
|
+
<true/>
|
|
66
|
+
<key>weight</key>
|
|
67
|
+
<real>2000</real>
|
|
68
|
+
</dict>
|
|
69
|
+
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
|
|
70
|
+
<dict>
|
|
71
|
+
<key>nested</key>
|
|
72
|
+
<true/>
|
|
73
|
+
<key>weight</key>
|
|
74
|
+
<real>10</real>
|
|
75
|
+
</dict>
|
|
76
|
+
<key>^.*</key>
|
|
77
|
+
<true/>
|
|
78
|
+
<key>^Info\.plist$</key>
|
|
79
|
+
<dict>
|
|
80
|
+
<key>omit</key>
|
|
81
|
+
<true/>
|
|
82
|
+
<key>weight</key>
|
|
83
|
+
<real>20</real>
|
|
84
|
+
</dict>
|
|
85
|
+
<key>^PkgInfo$</key>
|
|
86
|
+
<dict>
|
|
87
|
+
<key>omit</key>
|
|
88
|
+
<true/>
|
|
89
|
+
<key>weight</key>
|
|
90
|
+
<real>20</real>
|
|
91
|
+
</dict>
|
|
92
|
+
<key>^Resources/</key>
|
|
93
|
+
<dict>
|
|
94
|
+
<key>weight</key>
|
|
95
|
+
<real>20</real>
|
|
96
|
+
</dict>
|
|
97
|
+
<key>^Resources/.*\.lproj/</key>
|
|
98
|
+
<dict>
|
|
99
|
+
<key>optional</key>
|
|
100
|
+
<true/>
|
|
101
|
+
<key>weight</key>
|
|
102
|
+
<real>1000</real>
|
|
103
|
+
</dict>
|
|
104
|
+
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
|
105
|
+
<dict>
|
|
106
|
+
<key>omit</key>
|
|
107
|
+
<true/>
|
|
108
|
+
<key>weight</key>
|
|
109
|
+
<real>1100</real>
|
|
110
|
+
</dict>
|
|
111
|
+
<key>^Resources/Base\.lproj/</key>
|
|
112
|
+
<dict>
|
|
113
|
+
<key>weight</key>
|
|
114
|
+
<real>1010</real>
|
|
115
|
+
</dict>
|
|
116
|
+
<key>^[^/]+$</key>
|
|
117
|
+
<dict>
|
|
118
|
+
<key>nested</key>
|
|
119
|
+
<true/>
|
|
120
|
+
<key>weight</key>
|
|
121
|
+
<real>10</real>
|
|
122
|
+
</dict>
|
|
123
|
+
<key>^embedded\.provisionprofile$</key>
|
|
124
|
+
<dict>
|
|
125
|
+
<key>weight</key>
|
|
126
|
+
<real>20</real>
|
|
127
|
+
</dict>
|
|
128
|
+
<key>^version\.plist$</key>
|
|
129
|
+
<dict>
|
|
130
|
+
<key>weight</key>
|
|
131
|
+
<real>20</real>
|
|
132
|
+
</dict>
|
|
133
|
+
</dict>
|
|
134
|
+
</dict>
|
|
135
|
+
</plist>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Stable MineCodex background host
|
|
2
|
+
|
|
3
|
+
`MineCodex.app` is the small, universal native owner of MineCodex's macOS
|
|
4
|
+
background item. The npm package ships the built app only; native source and the
|
|
5
|
+
build script remain development inputs.
|
|
6
|
+
|
|
7
|
+
The production app bundle is intentionally versioned independently from the npm
|
|
8
|
+
package. Its ad-hoc code identity must stay byte-for-byte stable across ordinary
|
|
9
|
+
JavaScript releases because macOS records the helper's designated requirement.
|
|
10
|
+
Do not rebuild or change the bundle for an npm version bump. If native behavior
|
|
11
|
+
must change, plan an explicit signed-helper migration or introduce a new service
|
|
12
|
+
identifier and verify the macOS Background Items upgrade path before release.
|
|
13
|
+
|
|
14
|
+
Rebuild deliberately with:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm run build:service
|
|
18
|
+
```
|
|
@@ -23,6 +23,12 @@ async function exists(filePath) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
async function serviceInstalled(platform, paths) {
|
|
27
|
+
if (platform.isServiceInstalled) return platform.isServiceInstalled(paths);
|
|
28
|
+
return (paths.serviceManagerPath ? exists(paths.serviceManagerPath) : false)
|
|
29
|
+
|| (paths.launchAgentPath ? exists(paths.launchAgentPath) : false);
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
async function waitForService(port, { fetchImpl = fetch, timeoutMs = 20_000 } = {}) {
|
|
27
33
|
const deadline = Date.now() + timeoutMs;
|
|
28
34
|
while (Date.now() < deadline) {
|
|
@@ -151,7 +157,7 @@ async function installCommand({ paths, platform, cliPath, output, serviceProbe,
|
|
|
151
157
|
async function openCommand({ paths, platform, serviceProbe, fetchImpl }) {
|
|
152
158
|
const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
|
|
153
159
|
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl, timeoutMs: 750 }))) {
|
|
154
|
-
if (!(await
|
|
160
|
+
if (!(await serviceInstalled(platform, paths))) throw new Error("MineCodex is not installed. Run `mcx install` first.");
|
|
155
161
|
await platform.startService(paths);
|
|
156
162
|
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))) throw new Error("MineCodex service could not be started.");
|
|
157
163
|
}
|
|
@@ -181,16 +187,16 @@ function featureStatusText(feature) {
|
|
|
181
187
|
return `${copy[state] ?? state}${failure}`;
|
|
182
188
|
}
|
|
183
189
|
|
|
184
|
-
async function statusCommand({ paths, jsonOutput, output, fetchImpl }) {
|
|
190
|
+
async function statusCommand({ paths, platform, jsonOutput, output, fetchImpl }) {
|
|
185
191
|
let status;
|
|
186
192
|
try {
|
|
187
193
|
const response = await authorizedFetch(paths, "/api/status", {}, { fetchImpl });
|
|
188
194
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
189
|
-
status = { installed: await
|
|
195
|
+
status = { installed: await serviceInstalled(platform, paths), service: { running: true }, ...await response.json() };
|
|
190
196
|
} catch {
|
|
191
197
|
const config = await readConfig(paths.configPath).catch(() => null);
|
|
192
198
|
status = {
|
|
193
|
-
installed: await
|
|
199
|
+
installed: await serviceInstalled(platform, paths),
|
|
194
200
|
version: packageMetadata.version,
|
|
195
201
|
service: { running: false },
|
|
196
202
|
runtime: { running: false },
|
|
@@ -267,7 +273,7 @@ async function updateCommand({ paths, platform, cliPath, output, npm = createNpm
|
|
|
267
273
|
output(`MineCodex ${packageMetadata.version} is already up to date.`);
|
|
268
274
|
return;
|
|
269
275
|
}
|
|
270
|
-
const wasInstalled = await
|
|
276
|
+
const wasInstalled = await serviceInstalled(platform, paths);
|
|
271
277
|
const previousService = wasInstalled
|
|
272
278
|
? (await serviceSnapshot(platform, paths) ?? { running: true, plist: { exists: true } })
|
|
273
279
|
: null;
|
|
@@ -276,7 +282,7 @@ async function updateCommand({ paths, platform, cliPath, output, npm = createNpm
|
|
|
276
282
|
if (!rollbackPath) throw new Error("Could not create a rollback archive for the installed MineCodex version.");
|
|
277
283
|
try {
|
|
278
284
|
if (wasInstalled && previousService?.running) {
|
|
279
|
-
await platform.stopService({ ignoreErrors: false });
|
|
285
|
+
await platform.stopService(paths, { ignoreErrors: false });
|
|
280
286
|
await platform.assertServiceStopped?.();
|
|
281
287
|
}
|
|
282
288
|
await npm.installGlobal("minecodex@latest");
|
|
@@ -378,7 +384,7 @@ export async function runCli(argv, {
|
|
|
378
384
|
if (["--version", "-v"].includes(command)) return output(packageMetadata.version);
|
|
379
385
|
if (command === "install") return installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, nodeVersion });
|
|
380
386
|
if (command === "open" || command === "gui") return openCommand({ paths, platform, serviceProbe, fetchImpl });
|
|
381
|
-
if (command === "status") return statusCommand({ paths, jsonOutput: args.includes("--json"), output, fetchImpl });
|
|
387
|
+
if (command === "status") return statusCommand({ paths, platform, jsonOutput: args.includes("--json"), output, fetchImpl });
|
|
382
388
|
if (command === "restart") return restartCommand({ paths, output, fetchImpl });
|
|
383
389
|
if (command === "uninstall" || command === "remove") {
|
|
384
390
|
return uninstallCommand({ paths, platform, purge: args.includes("--purge"), output, cliPath });
|
|
@@ -10,6 +10,8 @@ export const PACKAGE_ROOT = path.resolve(
|
|
|
10
10
|
export function resolvePaths({ homeDir = os.homedir() } = {}) {
|
|
11
11
|
const supportDir = path.join(homeDir, "Library", "Application Support", "MineCodex");
|
|
12
12
|
const logsDir = path.join(homeDir, "Library", "Logs", "MineCodex");
|
|
13
|
+
const serviceDir = path.join(supportDir, "service");
|
|
14
|
+
const serviceAppPath = path.join(serviceDir, "MineCodex.app");
|
|
13
15
|
return Object.freeze({
|
|
14
16
|
homeDir,
|
|
15
17
|
packageRoot: PACKAGE_ROOT,
|
|
@@ -18,12 +20,18 @@ export function resolvePaths({ homeDir = os.homedir() } = {}) {
|
|
|
18
20
|
runtimeEntry: path.join(PACKAGE_ROOT, "packages", "runtime-host", "src", "main.mjs"),
|
|
19
21
|
supportDir,
|
|
20
22
|
logsDir,
|
|
23
|
+
serviceDir,
|
|
24
|
+
serviceBundleSource: path.join(PACKAGE_ROOT, "packages", "cli", "native", "MineCodex.app"),
|
|
25
|
+
serviceAppPath,
|
|
26
|
+
serviceManagerPath: path.join(serviceAppPath, "Contents", "MacOS", "MineCodexServiceManager"),
|
|
27
|
+
serviceRuntimeConfigPath: path.join(supportDir, "service-runtime.plist"),
|
|
21
28
|
configPath: path.join(supportDir, "config.json"),
|
|
22
29
|
tokenPath: path.join(supportDir, "control-token"),
|
|
23
30
|
pidPath: path.join(supportDir, "service.pid"),
|
|
24
31
|
codexPidPath: path.join(supportDir, "codex.pid"),
|
|
25
32
|
runtimeReadyPath: path.join(supportDir, "runtime-ready.json"),
|
|
26
33
|
profileDir: path.join(supportDir, "codex-profile"),
|
|
34
|
+
// v0.1.0 used a directly installed plist. Keep the path only for migration and cleanup.
|
|
27
35
|
launchAgentPath: path.join(homeDir, "Library", "LaunchAgents", "com.vontean.minecodex.plist"),
|
|
28
36
|
serviceLogPath: path.join(logsDir, "runtime.log"),
|
|
29
37
|
serviceErrorLogPath: path.join(logsDir, "runtime-error.log"),
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
-
import { mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
|
|
7
7
|
const defaultExecFile = promisify(execFileCallback);
|
|
8
|
-
const defaultFileSystem = Object.freeze({ mkdir, readFile, stat, unlink, writeFile });
|
|
8
|
+
const defaultFileSystem = Object.freeze({ mkdir, readFile, rename, rm, stat, unlink, writeFile });
|
|
9
9
|
|
|
10
|
-
export const SERVICE_LABEL = "com.vontean.minecodex";
|
|
10
|
+
export const SERVICE_LABEL = "com.vontean.minecodex.background";
|
|
11
|
+
export const LEGACY_SERVICE_LABEL = "com.vontean.minecodex";
|
|
11
12
|
export const CODEX_APP_PATH = "/Applications/ChatGPT.app";
|
|
12
13
|
export const CODEX_EXECUTABLE = `${CODEX_APP_PATH}/Contents/MacOS/ChatGPT`;
|
|
13
14
|
export const MIN_NODE_VERSION = Object.freeze({ major: 22, minor: 5, patch: 0 });
|
|
15
|
+
export const MIN_MACOS_VERSION = Object.freeze({ major: 13, darwinMajor: 22 });
|
|
14
16
|
|
|
15
17
|
function xml(value) {
|
|
16
18
|
return String(value)
|
|
@@ -21,38 +23,21 @@ function xml(value) {
|
|
|
21
23
|
.replaceAll("'", "'");
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
export function
|
|
26
|
+
export function buildServiceRuntimeConfig({ nodePath, cliPath, paths }) {
|
|
25
27
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
26
28
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
27
29
|
<plist version="1.0">
|
|
28
30
|
<dict>
|
|
29
|
-
<key>
|
|
30
|
-
<string>${
|
|
31
|
-
<key>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
<string>${xml(cliPath)}</string>
|
|
35
|
-
<string>serve</string>
|
|
36
|
-
<string>--service</string>
|
|
37
|
-
</array>
|
|
38
|
-
<key>RunAtLoad</key>
|
|
39
|
-
<true/>
|
|
40
|
-
<key>KeepAlive</key>
|
|
41
|
-
<dict>
|
|
42
|
-
<key>SuccessfulExit</key>
|
|
43
|
-
<false/>
|
|
44
|
-
</dict>
|
|
45
|
-
<key>ProcessType</key>
|
|
46
|
-
<string>Interactive</string>
|
|
47
|
-
<key>StandardOutPath</key>
|
|
31
|
+
<key>NodePath</key>
|
|
32
|
+
<string>${xml(nodePath)}</string>
|
|
33
|
+
<key>CLIPath</key>
|
|
34
|
+
<string>${xml(cliPath)}</string>
|
|
35
|
+
<key>StandardOutputPath</key>
|
|
48
36
|
<string>${xml(paths.serviceLogPath)}</string>
|
|
49
37
|
<key>StandardErrorPath</key>
|
|
50
38
|
<string>${xml(paths.serviceErrorLogPath)}</string>
|
|
51
|
-
<key>
|
|
52
|
-
<
|
|
53
|
-
<key>PATH</key>
|
|
54
|
-
<string>${xml(path.dirname(nodePath))}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
55
|
-
</dict>
|
|
39
|
+
<key>Path</key>
|
|
40
|
+
<string>${xml(path.dirname(nodePath))}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
56
41
|
</dict>
|
|
57
42
|
</plist>
|
|
58
43
|
`;
|
|
@@ -155,10 +140,31 @@ function serviceTarget(uid) {
|
|
|
155
140
|
return `gui/${uid}/${SERVICE_LABEL}`;
|
|
156
141
|
}
|
|
157
142
|
|
|
143
|
+
function legacyServiceTarget(uid) {
|
|
144
|
+
return `gui/${uid}/${LEGACY_SERVICE_LABEL}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function appServiceStatus(output) {
|
|
148
|
+
const match = String(output).match(/(?:^|\n)status=([a-z-]+)(?:\n|$)/);
|
|
149
|
+
return match?.[1] ?? "unknown";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function assertOwnedServicePaths(paths) {
|
|
153
|
+
const root = path.parse(paths.serviceDir ?? "").root;
|
|
154
|
+
if (!path.isAbsolute(paths.serviceDir)
|
|
155
|
+
|| paths.serviceDir === root
|
|
156
|
+
|| path.dirname(paths.serviceDir) === root
|
|
157
|
+
|| path.basename(paths.serviceDir) !== "service"
|
|
158
|
+
|| paths.serviceAppPath !== path.join(paths.serviceDir, "MineCodex.app")) {
|
|
159
|
+
throw new Error(`Refusing unsafe MineCodex service path: ${paths.serviceAppPath}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
158
163
|
export class MacPlatformAdapter {
|
|
159
164
|
constructor({
|
|
160
165
|
uid = process.getuid?.() ?? os.userInfo().uid,
|
|
161
166
|
platform = process.platform,
|
|
167
|
+
osRelease = os.release(),
|
|
162
168
|
homeDir = os.homedir(),
|
|
163
169
|
execFile = defaultExecFile,
|
|
164
170
|
fileSystem = defaultFileSystem,
|
|
@@ -168,6 +174,7 @@ export class MacPlatformAdapter {
|
|
|
168
174
|
} = {}) {
|
|
169
175
|
this.uid = uid;
|
|
170
176
|
this.platform = platform;
|
|
177
|
+
this.osRelease = osRelease;
|
|
171
178
|
this.homeDir = homeDir;
|
|
172
179
|
this.execFile = execFile;
|
|
173
180
|
this.fileSystem = fileSystem;
|
|
@@ -180,6 +187,10 @@ export class MacPlatformAdapter {
|
|
|
180
187
|
if (this.platform !== "darwin") {
|
|
181
188
|
throw new Error("MineCodex v1 installation currently supports macOS only.");
|
|
182
189
|
}
|
|
190
|
+
const darwinMajor = Number.parseInt(this.osRelease.split(".")[0], 10);
|
|
191
|
+
if (!Number.isInteger(darwinMajor) || darwinMajor < MIN_MACOS_VERSION.darwinMajor) {
|
|
192
|
+
throw new Error(`MineCodex requires macOS ${MIN_MACOS_VERSION.major} or newer.`);
|
|
193
|
+
}
|
|
183
194
|
}
|
|
184
195
|
|
|
185
196
|
async assertCodexInstalled() {
|
|
@@ -202,9 +213,9 @@ export class MacPlatformAdapter {
|
|
|
202
213
|
return this.codexInstallation ?? this.assertCodexInstalled();
|
|
203
214
|
}
|
|
204
215
|
|
|
205
|
-
async
|
|
216
|
+
async isLaunchdServiceRunning(target) {
|
|
206
217
|
try {
|
|
207
|
-
await this.execFile("/bin/launchctl", ["print",
|
|
218
|
+
await this.execFile("/bin/launchctl", ["print", target]);
|
|
208
219
|
return true;
|
|
209
220
|
} catch (error) {
|
|
210
221
|
if (serviceIsAbsent(error)) return false;
|
|
@@ -212,6 +223,41 @@ export class MacPlatformAdapter {
|
|
|
212
223
|
}
|
|
213
224
|
}
|
|
214
225
|
|
|
226
|
+
async isServiceRunning() {
|
|
227
|
+
return this.isLaunchdServiceRunning(serviceTarget(this.uid));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async pathExists(filePath) {
|
|
231
|
+
try {
|
|
232
|
+
if (this.fileSystem.stat) await this.fileSystem.stat(filePath);
|
|
233
|
+
else await this.fileSystem.readFile(filePath);
|
|
234
|
+
return true;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error.code === "ENOENT") return false;
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async serviceManagerStatus(paths) {
|
|
242
|
+
if (!(await this.pathExists(paths.serviceManagerPath))) return "not-found";
|
|
243
|
+
const { stdout } = await this.execFile(paths.serviceManagerPath, ["status"]);
|
|
244
|
+
return appServiceStatus(stdout);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async isServiceInstalled(paths) {
|
|
248
|
+
return (await this.pathExists(paths.serviceManagerPath))
|
|
249
|
+
|| (await this.pathExists(paths.launchAgentPath));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async writeServiceRuntimeConfig(paths, content, mode = 0o600) {
|
|
253
|
+
const temporaryPath = `${paths.serviceRuntimeConfigPath}.installing`;
|
|
254
|
+
await this.fileSystem.unlink(temporaryPath).catch((error) => {
|
|
255
|
+
if (error.code !== "ENOENT") throw error;
|
|
256
|
+
});
|
|
257
|
+
await this.fileSystem.writeFile(temporaryPath, content, { mode });
|
|
258
|
+
await this.fileSystem.rename(temporaryPath, paths.serviceRuntimeConfigPath);
|
|
259
|
+
}
|
|
260
|
+
|
|
215
261
|
async snapshotServiceState(paths) {
|
|
216
262
|
let plist;
|
|
217
263
|
try {
|
|
@@ -227,45 +273,123 @@ export class MacPlatformAdapter {
|
|
|
227
273
|
if (error.code !== "ENOENT") throw error;
|
|
228
274
|
plist = { exists: false, content: null, mode: null };
|
|
229
275
|
}
|
|
230
|
-
|
|
276
|
+
const appExists = await this.pathExists(paths.serviceManagerPath);
|
|
277
|
+
let runtimeConfig = { exists: false, content: null, mode: null };
|
|
278
|
+
if (appExists && paths.serviceRuntimeConfigPath) {
|
|
279
|
+
try {
|
|
280
|
+
const metadataPromise = this.fileSystem.stat
|
|
281
|
+
? this.fileSystem.stat(paths.serviceRuntimeConfigPath).catch(() => null)
|
|
282
|
+
: Promise.resolve(null);
|
|
283
|
+
const [content, metadata] = await Promise.all([
|
|
284
|
+
this.fileSystem.readFile(paths.serviceRuntimeConfigPath, "utf8"),
|
|
285
|
+
metadataPromise,
|
|
286
|
+
]);
|
|
287
|
+
runtimeConfig = { exists: true, content, mode: metadata?.mode };
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error.code !== "ENOENT") throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const running = await this.isServiceRunning()
|
|
293
|
+
|| (plist.exists && await this.isLaunchdServiceRunning(legacyServiceTarget(this.uid)));
|
|
294
|
+
return {
|
|
295
|
+
app: {
|
|
296
|
+
exists: appExists,
|
|
297
|
+
status: appExists ? await this.serviceManagerStatus(paths) : "not-found",
|
|
298
|
+
runtimeConfig,
|
|
299
|
+
},
|
|
300
|
+
plist,
|
|
301
|
+
running,
|
|
302
|
+
};
|
|
231
303
|
}
|
|
232
304
|
|
|
233
305
|
async installService({ cliPath, paths, start = true }) {
|
|
234
|
-
|
|
306
|
+
const stagingAppPath = path.join(paths.serviceDir, ".MineCodex.app.installing");
|
|
307
|
+
assertOwnedServicePaths(paths);
|
|
308
|
+
await this.stopService(paths, { ignoreErrors: false });
|
|
309
|
+
await this.fileSystem.mkdir(paths.serviceDir, { recursive: true, mode: 0o700 });
|
|
235
310
|
await this.fileSystem.mkdir(paths.logsDir, { recursive: true, mode: 0o700 });
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
await this.
|
|
311
|
+
await this.writeServiceRuntimeConfig(
|
|
312
|
+
paths,
|
|
313
|
+
buildServiceRuntimeConfig({ nodePath: process.execPath, cliPath, paths }),
|
|
314
|
+
);
|
|
315
|
+
await this.fileSystem.rm(stagingAppPath, { recursive: true, force: true });
|
|
316
|
+
try {
|
|
317
|
+
await this.execFile("/usr/bin/ditto", [paths.serviceBundleSource, stagingAppPath]);
|
|
318
|
+
await this.execFile("/usr/bin/codesign", ["--verify", "--deep", "--strict", stagingAppPath]);
|
|
319
|
+
await this.fileSystem.rm(paths.serviceAppPath, { recursive: true, force: true });
|
|
320
|
+
await this.fileSystem.rename(stagingAppPath, paths.serviceAppPath);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
await this.fileSystem.rm(stagingAppPath, { recursive: true, force: true }).catch(() => {});
|
|
323
|
+
throw error;
|
|
244
324
|
}
|
|
325
|
+
await this.fileSystem.unlink(paths.launchAgentPath).catch((error) => {
|
|
326
|
+
if (error.code !== "ENOENT") throw error;
|
|
327
|
+
});
|
|
328
|
+
if (start) await this.startService(paths);
|
|
245
329
|
}
|
|
246
330
|
|
|
247
|
-
async stopService({ ignoreErrors = false } = {}) {
|
|
331
|
+
async stopService(paths, { ignoreErrors = false } = {}) {
|
|
332
|
+
let managerError = null;
|
|
333
|
+
if (paths?.serviceManagerPath && await this.pathExists(paths.serviceManagerPath)) {
|
|
334
|
+
try {
|
|
335
|
+
await this.execFile(paths.serviceManagerPath, ["unregister"]);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
managerError = error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
248
340
|
try {
|
|
249
341
|
await this.execFile("/bin/launchctl", ["bootout", serviceTarget(this.uid)]);
|
|
250
342
|
} catch (error) {
|
|
251
|
-
if (!
|
|
343
|
+
if (!serviceIsAbsent(error)) throw error;
|
|
252
344
|
}
|
|
345
|
+
try {
|
|
346
|
+
await this.execFile("/bin/launchctl", ["bootout", legacyServiceTarget(this.uid)]);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (!serviceIsAbsent(error)) throw error;
|
|
349
|
+
}
|
|
350
|
+
if (managerError && !ignoreErrors) throw managerError;
|
|
253
351
|
}
|
|
254
352
|
|
|
255
353
|
async startService(paths) {
|
|
256
|
-
|
|
257
|
-
|
|
354
|
+
const status = await this.serviceManagerStatus(paths);
|
|
355
|
+
if (status === "requires-approval") {
|
|
356
|
+
throw new Error(
|
|
357
|
+
"MineCodex background service is disabled. Enable MineCodex in System Settings > General > Login Items & Extensions, then run `mcx install` again.",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (status !== "enabled") {
|
|
361
|
+
const { stdout } = await this.execFile(paths.serviceManagerPath, ["register"]);
|
|
362
|
+
const registeredStatus = appServiceStatus(stdout);
|
|
363
|
+
if (registeredStatus !== "enabled") {
|
|
364
|
+
throw new Error(`MineCodex background service registration requires attention: ${registeredStatus}`);
|
|
365
|
+
}
|
|
258
366
|
}
|
|
259
367
|
await this.execFile("/bin/launchctl", ["kickstart", "-k", serviceTarget(this.uid)]);
|
|
260
368
|
}
|
|
261
369
|
|
|
262
370
|
async assertServiceStopped() {
|
|
263
|
-
|
|
371
|
+
const [current, legacy] = await Promise.all([
|
|
372
|
+
this.isServiceRunning(),
|
|
373
|
+
this.isLaunchdServiceRunning(legacyServiceTarget(this.uid)),
|
|
374
|
+
]);
|
|
375
|
+
if (current || legacy) throw new Error("MineCodex launchd service is still running.");
|
|
264
376
|
}
|
|
265
377
|
|
|
266
378
|
async restoreServiceState(snapshot, { cliPath, paths } = {}) {
|
|
267
|
-
await this.
|
|
379
|
+
await this.removeService(paths);
|
|
268
380
|
await this.assertServiceStopped();
|
|
381
|
+
if (snapshot?.app?.exists) {
|
|
382
|
+
await this.installService({ cliPath, paths, start: false });
|
|
383
|
+
if (snapshot.app.runtimeConfig?.exists) {
|
|
384
|
+
await this.writeServiceRuntimeConfig(
|
|
385
|
+
paths,
|
|
386
|
+
snapshot.app.runtimeConfig.content,
|
|
387
|
+
snapshot.app.runtimeConfig.mode ? snapshot.app.runtimeConfig.mode & 0o777 : 0o600,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
if (snapshot.app.status === "enabled") await this.startService(paths);
|
|
391
|
+
return snapshot;
|
|
392
|
+
}
|
|
269
393
|
if (snapshot?.plist?.exists) {
|
|
270
394
|
await this.fileSystem.mkdir(path.dirname(paths.launchAgentPath), { recursive: true });
|
|
271
395
|
await this.fileSystem.writeFile(paths.launchAgentPath, snapshot.plist.content, {
|
|
@@ -276,20 +400,29 @@ export class MacPlatformAdapter {
|
|
|
276
400
|
if (error.code !== "ENOENT") throw error;
|
|
277
401
|
});
|
|
278
402
|
}
|
|
279
|
-
if (snapshot?.running) {
|
|
280
|
-
if (!snapshot?.plist?.exists) throw new Error("Cannot restore a running service without its plist.");
|
|
403
|
+
if (snapshot?.running && snapshot?.plist?.exists) {
|
|
281
404
|
await this.execFile("/bin/launchctl", ["bootstrap", `gui/${this.uid}`, paths.launchAgentPath]);
|
|
282
|
-
await this.execFile("/bin/launchctl", ["kickstart", "-k",
|
|
405
|
+
await this.execFile("/bin/launchctl", ["kickstart", "-k", legacyServiceTarget(this.uid)]);
|
|
283
406
|
}
|
|
284
407
|
return snapshot;
|
|
285
408
|
}
|
|
286
409
|
|
|
287
410
|
async removeService(paths) {
|
|
288
|
-
|
|
411
|
+
assertOwnedServicePaths(paths);
|
|
412
|
+
await this.stopService(paths, { ignoreErrors: false });
|
|
289
413
|
await this.assertServiceStopped();
|
|
290
414
|
await this.fileSystem.unlink(paths.launchAgentPath).catch((error) => {
|
|
291
415
|
if (error.code !== "ENOENT") throw error;
|
|
292
416
|
});
|
|
417
|
+
if (paths.serviceRuntimeConfigPath) {
|
|
418
|
+
await this.fileSystem.unlink(paths.serviceRuntimeConfigPath).catch((error) => {
|
|
419
|
+
if (error.code !== "ENOENT") throw error;
|
|
420
|
+
});
|
|
421
|
+
await this.fileSystem.unlink(`${paths.serviceRuntimeConfigPath}.installing`).catch((error) => {
|
|
422
|
+
if (error.code !== "ENOENT") throw error;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
await this.fileSystem.rm(paths.serviceDir, { recursive: true, force: true });
|
|
293
426
|
}
|
|
294
427
|
|
|
295
428
|
async listCodexProcesses() {
|
|
@@ -141,6 +141,9 @@ Host 会再次发送当前状态。功能页面可据此暂停隐藏状态下的
|
|
|
141
141
|
|
|
142
142
|
- 只在 top frame 安装 Runtime,绝不向业务 iframe 注入 binding token。
|
|
143
143
|
- Watch / refresh 串行;已连接 Renderer 不重复连接,关闭 target 会清理状态。
|
|
144
|
+
- 本地 Surface 注入前先启用 CSP bypass 并注册 document-start 脚本;缺少初始化标记的
|
|
145
|
+
首次或旧版 document 只受控 reload 一次。标记存在时,新的 RuntimeHost 会话直接
|
|
146
|
+
替换旧 Runtime,不刷新当前 Codex 页面。
|
|
144
147
|
- 入口由 Renderer 内的 MutationObserver 幂等挂载,React 重绘不会产生重复入口。
|
|
145
148
|
- Summary 根据对话主区域宽度连续派生 `overlay / shift / gutter`:小于 1096px
|
|
146
149
|
使用临时 Popover;1096–1535px 预留 316px 并把对话内容左移 158px;更宽时
|
|
@@ -5,6 +5,8 @@ import path from "node:path";
|
|
|
5
5
|
|
|
6
6
|
const RUNTIME_VERSION = 99;
|
|
7
7
|
const HOST_BINDING_NAME = "__codexPersonalHostAction";
|
|
8
|
+
const CSP_BOOTSTRAP_VERSION = 1;
|
|
9
|
+
const CSP_BOOTSTRAP_KEY = "__mineCodexCspBootstrapVersion";
|
|
8
10
|
|
|
9
11
|
export const RESPONSIVE_SUMMARY_LAYOUT = Object.freeze({
|
|
10
12
|
contentBaseWidth: 736,
|
|
@@ -3564,6 +3566,14 @@ export function createRuntimeReadyContract({
|
|
|
3564
3566
|
};
|
|
3565
3567
|
}
|
|
3566
3568
|
|
|
3569
|
+
function createDocumentBootstrapSource(source) {
|
|
3570
|
+
return `window[${JSON.stringify(CSP_BOOTSTRAP_KEY)}] = ${CSP_BOOTSTRAP_VERSION};\n${source}`;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
function documentBootstrapExpression() {
|
|
3574
|
+
return `window[${JSON.stringify(CSP_BOOTSTRAP_KEY)}] === ${CSP_BOOTSTRAP_VERSION}`;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3567
3577
|
async function connect(url) {
|
|
3568
3578
|
const socket = new WebSocket(url);
|
|
3569
3579
|
const pending = new Map();
|
|
@@ -4250,7 +4260,20 @@ export class CodexRuntime {
|
|
|
4250
4260
|
bindingToken,
|
|
4251
4261
|
runtimeSessionId: this.runtimeSessionId,
|
|
4252
4262
|
});
|
|
4253
|
-
const
|
|
4263
|
+
const bootstrapExpression = documentBootstrapExpression();
|
|
4264
|
+
const script = await client.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
4265
|
+
source: createDocumentBootstrapSource(source),
|
|
4266
|
+
});
|
|
4267
|
+
const documentWasBootstrapped = Boolean(evaluationValue(await client.send("Runtime.evaluate", {
|
|
4268
|
+
expression: bootstrapExpression,
|
|
4269
|
+
returnByValue: true,
|
|
4270
|
+
})));
|
|
4271
|
+
if (!documentWasBootstrapped) {
|
|
4272
|
+
const pageLoaded = client.waitFor("Page.loadEventFired", 20_000);
|
|
4273
|
+
await client.send("Page.reload");
|
|
4274
|
+
await pageLoaded;
|
|
4275
|
+
await waitForExpression(client, bootstrapExpression);
|
|
4276
|
+
}
|
|
4254
4277
|
await waitForExpression(
|
|
4255
4278
|
client,
|
|
4256
4279
|
`document.readyState === "interactive" || document.readyState === "complete"`,
|