minecodex 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/apps/dashboard/app.js +387 -0
  4. package/apps/dashboard/favicon.svg +18 -0
  5. package/apps/dashboard/index.html +70 -0
  6. package/apps/dashboard/styles.css +297 -0
  7. package/features/images/README.md +106 -0
  8. package/features/images/codex-feature.json +24 -0
  9. package/features/images/src/http-server.mjs +302 -0
  10. package/features/images/src/image-library.mjs +582 -0
  11. package/features/images/src/main.mjs +52 -0
  12. package/features/images/src/prompt-index.mjs +182 -0
  13. package/features/images/src/save-as.mjs +40 -0
  14. package/features/images/src/thread-metadata.mjs +93 -0
  15. package/features/images/web/app.js +1045 -0
  16. package/features/images/web/index.html +162 -0
  17. package/features/images/web/styles.css +994 -0
  18. package/features/model-slider/README.md +30 -0
  19. package/features/model-slider/THIRD_PARTY_NOTICES.md +24 -0
  20. package/features/model-slider/assets/providers/anthropic.svg +1 -0
  21. package/features/model-slider/assets/providers/deepseek.svg +1 -0
  22. package/features/model-slider/assets/providers/gemini.svg +1 -0
  23. package/features/model-slider/assets/providers/grok.svg +1 -0
  24. package/features/model-slider/assets/providers/hunyuan.svg +1 -0
  25. package/features/model-slider/assets/providers/kimi.svg +1 -0
  26. package/features/model-slider/assets/providers/minimax.svg +1 -0
  27. package/features/model-slider/assets/providers/openai.svg +1 -0
  28. package/features/model-slider/assets/providers/qwen.svg +1 -0
  29. package/features/model-slider/assets/providers/xiaomimimo.svg +1 -0
  30. package/features/model-slider/assets/providers/zhipu.svg +1 -0
  31. package/features/model-slider/assets/ui/sparkles.svg +18 -0
  32. package/features/model-slider/assets/ui/star-off.svg +17 -0
  33. package/features/model-slider/assets/ui/star.svg +15 -0
  34. package/features/model-slider/codex-feature.json +56 -0
  35. package/features/model-slider/vendor/cc-switch-LICENSE +21 -0
  36. package/features/model-slider/vendor/lucide-LICENSE +35 -0
  37. package/features/notes/README.md +106 -0
  38. package/features/notes/assets/icons/circle.svg +15 -0
  39. package/features/notes/assets/icons/ellipsis.svg +17 -0
  40. package/features/notes/assets/icons/external-link.svg +17 -0
  41. package/features/notes/assets/icons/list-todo.svg +19 -0
  42. package/features/notes/assets/icons/message-square-quote.svg +17 -0
  43. package/features/notes/assets/icons/paperclip.svg +15 -0
  44. package/features/notes/assets/icons/star-off.svg +17 -0
  45. package/features/notes/assets/icons/star.svg +15 -0
  46. package/features/notes/assets/icons/sticky-note.svg +16 -0
  47. package/features/notes/assets/icons/trash-2.svg +19 -0
  48. package/features/notes/codex-feature.json +90 -0
  49. package/features/notes/src/http-server.mjs +541 -0
  50. package/features/notes/src/main.mjs +54 -0
  51. package/features/notes/src/product.mjs +771 -0
  52. package/features/notes/web/app.js +1495 -0
  53. package/features/notes/web/codex-artifact-document.svg +1 -0
  54. package/features/notes/web/codex-attachment.svg +1 -0
  55. package/features/notes/web/codex-build.svg +1 -0
  56. package/features/notes/web/codex-code.svg +1 -0
  57. package/features/notes/web/codex-cplusplus.svg +1 -0
  58. package/features/notes/web/codex-css.svg +1 -0
  59. package/features/notes/web/codex-document.svg +1 -0
  60. package/features/notes/web/codex-file.svg +1 -0
  61. package/features/notes/web/codex-folder.svg +1 -0
  62. package/features/notes/web/codex-hashes.svg +1 -0
  63. package/features/notes/web/codex-html.svg +1 -0
  64. package/features/notes/web/codex-image.svg +1 -0
  65. package/features/notes/web/codex-java.svg +1 -0
  66. package/features/notes/web/codex-javascript.svg +1 -0
  67. package/features/notes/web/codex-json.svg +1 -0
  68. package/features/notes/web/codex-notebook.svg +1 -0
  69. package/features/notes/web/codex-pdf.svg +1 -0
  70. package/features/notes/web/codex-pencil.svg +3 -0
  71. package/features/notes/web/codex-php.svg +1 -0
  72. package/features/notes/web/codex-plus.svg +3 -0
  73. package/features/notes/web/codex-presentation.svg +1 -0
  74. package/features/notes/web/codex-python.svg +1 -0
  75. package/features/notes/web/codex-react.svg +1 -0
  76. package/features/notes/web/codex-rust.svg +1 -0
  77. package/features/notes/web/codex-shell.svg +1 -0
  78. package/features/notes/web/codex-skill.svg +1 -0
  79. package/features/notes/web/codex-spreadsheet.svg +1 -0
  80. package/features/notes/web/codex-task.svg +3 -0
  81. package/features/notes/web/codex-terminal.svg +1 -0
  82. package/features/notes/web/codex-toml.svg +1 -0
  83. package/features/notes/web/codex-typescript.svg +1 -0
  84. package/features/notes/web/codex-view-all.svg +3 -0
  85. package/features/notes/web/index.html +15 -0
  86. package/features/notes/web/styles.css +1155 -0
  87. package/package.json +67 -0
  88. package/packages/cli/bin/mcx.mjs +7 -0
  89. package/packages/cli/src/browser-exchange.mjs +31 -0
  90. package/packages/cli/src/commands.mjs +389 -0
  91. package/packages/cli/src/config.mjs +101 -0
  92. package/packages/cli/src/control-server.mjs +371 -0
  93. package/packages/cli/src/npm-adapter.mjs +40 -0
  94. package/packages/cli/src/paths.mjs +33 -0
  95. package/packages/cli/src/platform.mjs +368 -0
  96. package/packages/cli/src/runtime-manager.mjs +338 -0
  97. package/packages/runtime-host/README.md +171 -0
  98. package/packages/runtime-host/src/codex-runtime.mjs +4311 -0
  99. package/packages/runtime-host/src/feature-registry.mjs +827 -0
  100. package/packages/runtime-host/src/main.mjs +98 -0
  101. package/packages/runtime-host/src/pid-file.mjs +23 -0
@@ -0,0 +1,368 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+
7
+ const defaultExecFile = promisify(execFileCallback);
8
+ const defaultFileSystem = Object.freeze({ mkdir, readFile, stat, unlink, writeFile });
9
+
10
+ export const SERVICE_LABEL = "com.vontean.minecodex";
11
+ export const CODEX_APP_PATH = "/Applications/ChatGPT.app";
12
+ export const CODEX_EXECUTABLE = `${CODEX_APP_PATH}/Contents/MacOS/ChatGPT`;
13
+ export const MIN_NODE_VERSION = Object.freeze({ major: 22, minor: 5, patch: 0 });
14
+
15
+ function xml(value) {
16
+ return String(value)
17
+ .replaceAll("&", "&")
18
+ .replaceAll("<", "&lt;")
19
+ .replaceAll(">", "&gt;")
20
+ .replaceAll('"', "&quot;")
21
+ .replaceAll("'", "&apos;");
22
+ }
23
+
24
+ export function buildLaunchAgent({ nodePath, cliPath, paths }) {
25
+ return `<?xml version="1.0" encoding="UTF-8"?>
26
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
27
+ <plist version="1.0">
28
+ <dict>
29
+ <key>Label</key>
30
+ <string>${SERVICE_LABEL}</string>
31
+ <key>ProgramArguments</key>
32
+ <array>
33
+ <string>${xml(nodePath)}</string>
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>
48
+ <string>${xml(paths.serviceLogPath)}</string>
49
+ <key>StandardErrorPath</key>
50
+ <string>${xml(paths.serviceErrorLogPath)}</string>
51
+ <key>EnvironmentVariables</key>
52
+ <dict>
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>
56
+ </dict>
57
+ </plist>
58
+ `;
59
+ }
60
+
61
+ export function assertNodeSupported(nodeVersion = process.versions.node) {
62
+ const [major, minor, patch = 0] = String(nodeVersion).split(".").map((part) => Number.parseInt(part, 10));
63
+ const supported = Number.isInteger(major)
64
+ && (major > MIN_NODE_VERSION.major
65
+ || (major === MIN_NODE_VERSION.major && Number.isInteger(minor) && minor >= MIN_NODE_VERSION.minor));
66
+ if (!supported) {
67
+ throw new Error(
68
+ `MineCodex requires Node.js ${MIN_NODE_VERSION.major}.${MIN_NODE_VERSION.minor}.0 or newer; `
69
+ + `current version is ${nodeVersion}. Upgrade Node.js before running mcx install.`,
70
+ );
71
+ }
72
+ return { major, minor, patch };
73
+ }
74
+
75
+ export function supportedCodexAppPaths(homeDir = os.homedir()) {
76
+ return Object.freeze([
77
+ CODEX_APP_PATH,
78
+ path.join(homeDir, "Applications", "ChatGPT.app"),
79
+ ]);
80
+ }
81
+
82
+ export async function resolveCodexInstallation({
83
+ homeDir = os.homedir(),
84
+ fileExists = async (filePath) => {
85
+ try {
86
+ await readFile(filePath);
87
+ return true;
88
+ } catch (error) {
89
+ if (error.code === "ENOENT") return false;
90
+ throw error;
91
+ }
92
+ },
93
+ } = {}) {
94
+ for (const appPath of supportedCodexAppPaths(homeDir)) {
95
+ const infoPath = path.join(appPath, "Contents", "Info.plist");
96
+ if (await fileExists(infoPath)) {
97
+ return Object.freeze({
98
+ appPath,
99
+ executable: path.join(appPath, "Contents", "MacOS", "ChatGPT"),
100
+ });
101
+ }
102
+ }
103
+ throw new Error(
104
+ `Codex Desktop was not found in a supported installation path: ${supportedCodexAppPaths(homeDir).join(", ")}`,
105
+ );
106
+ }
107
+
108
+ export function parseProcessList(output) {
109
+ return String(output).split("\n").flatMap((line) => {
110
+ const match = line.match(/^\s*(\d+)\s+(.+)$/);
111
+ return match ? [{ pid: Number(match[1]), command: match[2] }] : [];
112
+ });
113
+ }
114
+
115
+ export function isExactCodexProcess(command, installation) {
116
+ const value = String(command).trim();
117
+ return value === installation?.executable || value.startsWith(`${installation?.executable} `);
118
+ }
119
+
120
+ function hasManagedProfile(command, profileDir) {
121
+ const value = String(command);
122
+ const marker = `--user-data-dir=${profileDir}`;
123
+ let offset = value.indexOf(marker);
124
+ while (offset >= 0) {
125
+ const before = offset === 0 || /\s/.test(value[offset - 1]);
126
+ const end = offset + marker.length;
127
+ const after = end === value.length || /\s/.test(value[end]);
128
+ if (before && after) return true;
129
+ offset = value.indexOf(marker, offset + 1);
130
+ }
131
+ return false;
132
+ }
133
+
134
+ export function serviceIsAbsent(error) {
135
+ return /could not find service|service not found|no such process|domain does not support specified action/i.test(
136
+ `${error?.stderr ?? ""}\n${error?.message ?? ""}`,
137
+ );
138
+ }
139
+
140
+ async function waitForProcessExit(pid, { processApi, timeoutMs = 8_000, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) }) {
141
+ const deadline = Date.now() + timeoutMs;
142
+ while (Date.now() < deadline) {
143
+ try {
144
+ processApi.kill(pid, 0);
145
+ } catch (error) {
146
+ if (error.code === "ESRCH") return;
147
+ throw error;
148
+ }
149
+ await sleep(100);
150
+ }
151
+ throw new Error(`Process ${pid} did not exit in time`);
152
+ }
153
+
154
+ function serviceTarget(uid) {
155
+ return `gui/${uid}/${SERVICE_LABEL}`;
156
+ }
157
+
158
+ export class MacPlatformAdapter {
159
+ constructor({
160
+ uid = process.getuid?.() ?? os.userInfo().uid,
161
+ platform = process.platform,
162
+ homeDir = os.homedir(),
163
+ execFile = defaultExecFile,
164
+ fileSystem = defaultFileSystem,
165
+ processApi = process,
166
+ sleep,
167
+ codexInstallation,
168
+ } = {}) {
169
+ this.uid = uid;
170
+ this.platform = platform;
171
+ this.homeDir = homeDir;
172
+ this.execFile = execFile;
173
+ this.fileSystem = fileSystem;
174
+ this.processApi = processApi;
175
+ this.sleep = sleep;
176
+ this.codexInstallation = codexInstallation;
177
+ }
178
+
179
+ assertInstallSupported() {
180
+ if (this.platform !== "darwin") {
181
+ throw new Error("MineCodex v1 installation currently supports macOS only.");
182
+ }
183
+ }
184
+
185
+ async assertCodexInstalled() {
186
+ this.codexInstallation = await resolveCodexInstallation({
187
+ homeDir: this.homeDir,
188
+ fileExists: async (filePath) => {
189
+ try {
190
+ await this.fileSystem.readFile(filePath);
191
+ return true;
192
+ } catch (error) {
193
+ if (error.code === "ENOENT") return false;
194
+ throw error;
195
+ }
196
+ },
197
+ });
198
+ return this.codexInstallation;
199
+ }
200
+
201
+ async codex() {
202
+ return this.codexInstallation ?? this.assertCodexInstalled();
203
+ }
204
+
205
+ async isServiceRunning() {
206
+ try {
207
+ await this.execFile("/bin/launchctl", ["print", serviceTarget(this.uid)]);
208
+ return true;
209
+ } catch (error) {
210
+ if (serviceIsAbsent(error)) return false;
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ async snapshotServiceState(paths) {
216
+ let plist;
217
+ try {
218
+ const metadataPromise = this.fileSystem.stat
219
+ ? this.fileSystem.stat(paths.launchAgentPath).catch(() => null)
220
+ : Promise.resolve(null);
221
+ const [content, metadata] = await Promise.all([
222
+ this.fileSystem.readFile(paths.launchAgentPath, "utf8"),
223
+ metadataPromise,
224
+ ]);
225
+ plist = { exists: true, content, mode: metadata?.mode };
226
+ } catch (error) {
227
+ if (error.code !== "ENOENT") throw error;
228
+ plist = { exists: false, content: null, mode: null };
229
+ }
230
+ return { plist, running: await this.isServiceRunning() };
231
+ }
232
+
233
+ async installService({ cliPath, paths, start = true }) {
234
+ await this.fileSystem.mkdir(path.dirname(paths.launchAgentPath), { recursive: true });
235
+ await this.fileSystem.mkdir(paths.logsDir, { recursive: true, mode: 0o700 });
236
+ const plist = buildLaunchAgent({ nodePath: process.execPath, cliPath, paths });
237
+ await this.fileSystem.writeFile(paths.launchAgentPath, plist, { mode: 0o600 });
238
+ await this.stopService({ ignoreErrors: true });
239
+ await this.execFile("/bin/launchctl", ["bootstrap", `gui/${this.uid}`, paths.launchAgentPath]);
240
+ if (start) {
241
+ await this.execFile("/bin/launchctl", ["kickstart", "-k", serviceTarget(this.uid)]);
242
+ } else {
243
+ await this.stopService({ ignoreErrors: true });
244
+ }
245
+ }
246
+
247
+ async stopService({ ignoreErrors = false } = {}) {
248
+ try {
249
+ await this.execFile("/bin/launchctl", ["bootout", serviceTarget(this.uid)]);
250
+ } catch (error) {
251
+ if (!ignoreErrors || !serviceIsAbsent(error)) throw error;
252
+ }
253
+ }
254
+
255
+ async startService(paths) {
256
+ if (!(await this.isServiceRunning())) {
257
+ await this.execFile("/bin/launchctl", ["bootstrap", `gui/${this.uid}`, paths.launchAgentPath]);
258
+ }
259
+ await this.execFile("/bin/launchctl", ["kickstart", "-k", serviceTarget(this.uid)]);
260
+ }
261
+
262
+ async assertServiceStopped() {
263
+ if (await this.isServiceRunning()) throw new Error("MineCodex launchd service is still running.");
264
+ }
265
+
266
+ async restoreServiceState(snapshot, { cliPath, paths } = {}) {
267
+ await this.stopService({ ignoreErrors: true });
268
+ await this.assertServiceStopped();
269
+ if (snapshot?.plist?.exists) {
270
+ await this.fileSystem.mkdir(path.dirname(paths.launchAgentPath), { recursive: true });
271
+ await this.fileSystem.writeFile(paths.launchAgentPath, snapshot.plist.content, {
272
+ mode: snapshot.plist.mode ? snapshot.plist.mode & 0o777 : 0o600,
273
+ });
274
+ } else {
275
+ await this.fileSystem.unlink(paths.launchAgentPath).catch((error) => {
276
+ if (error.code !== "ENOENT") throw error;
277
+ });
278
+ }
279
+ if (snapshot?.running) {
280
+ if (!snapshot?.plist?.exists) throw new Error("Cannot restore a running service without its plist.");
281
+ await this.execFile("/bin/launchctl", ["bootstrap", `gui/${this.uid}`, paths.launchAgentPath]);
282
+ await this.execFile("/bin/launchctl", ["kickstart", "-k", serviceTarget(this.uid)]);
283
+ }
284
+ return snapshot;
285
+ }
286
+
287
+ async removeService(paths) {
288
+ await this.stopService({ ignoreErrors: true });
289
+ await this.assertServiceStopped();
290
+ await this.fileSystem.unlink(paths.launchAgentPath).catch((error) => {
291
+ if (error.code !== "ENOENT") throw error;
292
+ });
293
+ }
294
+
295
+ async listCodexProcesses() {
296
+ const installation = await this.codex();
297
+ const { stdout } = await this.execFile("/bin/ps", ["-axo", "pid=,command="]);
298
+ return parseProcessList(stdout).filter(({ command }) => isExactCodexProcess(command, installation));
299
+ }
300
+
301
+ async ownedCodexProcesses(paths) {
302
+ const processes = await this.listCodexProcesses();
303
+ return processes.filter(({ command }) => hasManagedProfile(command, paths.profileDir));
304
+ }
305
+
306
+ async assertOwnedCodexStopped(paths) {
307
+ const processes = await this.ownedCodexProcesses(paths);
308
+ if (processes.length) {
309
+ throw new Error(`MineCodex-owned Codex processes are still running: ${processes.map(({ pid }) => pid).join(", ")}`);
310
+ }
311
+ }
312
+
313
+ async snapshotNativeCodexState() {
314
+ const processes = (await this.listCodexProcesses()).filter(({ command }) => !command.includes("--user-data-dir="));
315
+ return { count: processes.length, processes };
316
+ }
317
+
318
+ async restoreNativeCodexState(snapshot) {
319
+ if (!snapshot?.count) return 0;
320
+ const current = (await this.listCodexProcesses()).filter(({ command }) => !command.includes("--user-data-dir="));
321
+ if (current.length) return current.length;
322
+ await this.openNativeCodex();
323
+ return snapshot.count;
324
+ }
325
+
326
+ async terminateOwnedCodex(paths) {
327
+ let pid;
328
+ try {
329
+ pid = Number((await this.fileSystem.readFile(paths.codexPidPath, "utf8")).trim());
330
+ } catch (error) {
331
+ if (error.code === "ENOENT") {
332
+ await this.assertOwnedCodexStopped(paths);
333
+ return false;
334
+ }
335
+ throw error;
336
+ }
337
+ if (!Number.isInteger(pid) || pid <= 1) {
338
+ await this.assertOwnedCodexStopped(paths);
339
+ return false;
340
+ }
341
+ const processInfo = (await this.ownedCodexProcesses(paths)).find((candidate) => candidate.pid === pid);
342
+ if (!processInfo) {
343
+ await this.assertOwnedCodexStopped(paths);
344
+ return false;
345
+ }
346
+ this.processApi.kill(pid, "SIGTERM");
347
+ await waitForProcessExit(pid, { processApi: this.processApi, sleep: this.sleep });
348
+ await this.fileSystem.unlink(paths.codexPidPath).catch(() => {});
349
+ await this.assertOwnedCodexStopped(paths);
350
+ return true;
351
+ }
352
+
353
+ async terminateNativeCodex() {
354
+ const processes = (await this.listCodexProcesses()).filter(({ command }) => !command.includes("--user-data-dir="));
355
+ for (const { pid } of processes) this.processApi.kill(pid, "SIGTERM");
356
+ await Promise.all(processes.map(({ pid }) => waitForProcessExit(pid, { processApi: this.processApi, sleep: this.sleep })));
357
+ return processes.length;
358
+ }
359
+
360
+ async openBrowser(url) {
361
+ await this.execFile("/usr/bin/open", [url]);
362
+ }
363
+
364
+ async openNativeCodex() {
365
+ const installation = await this.codex();
366
+ await this.execFile("/usr/bin/open", ["-a", installation.appPath]);
367
+ }
368
+ }
@@ -0,0 +1,338 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
+ import { readConfig, FEATURE_IDS } from "./config.mjs";
4
+
5
+ export const RUNTIME_PLUGIN_STATES = Object.freeze([
6
+ "configured",
7
+ "launching",
8
+ "ready",
9
+ "injected",
10
+ "failed",
11
+ "stopped",
12
+ ]);
13
+
14
+ function enabledFeatureIds(config) {
15
+ return FEATURE_IDS.filter((id) => config.features[id]);
16
+ }
17
+
18
+ function hasExited(child) {
19
+ return Boolean(child && (child.exitCode != null || child.signalCode != null));
20
+ }
21
+
22
+ function normalizedFailure(failure) {
23
+ if (!failure || typeof failure !== "object") return failure ?? null;
24
+ return {
25
+ code: failure.code ?? "RUNTIME_PLUGIN_FAILED",
26
+ message: failure.message ?? "MineCodex plugin failed.",
27
+ phase: failure.phase ?? "runtime",
28
+ };
29
+ }
30
+
31
+ export function normalizeRuntimeReady(ready = {}) {
32
+ const sourcePlugins = Array.isArray(ready.plugins) ? ready.plugins : [];
33
+ const plugins = sourcePlugins.flatMap((plugin) => {
34
+ if (!plugin || typeof plugin.featureId !== "string") return [];
35
+ const state = RUNTIME_PLUGIN_STATES.includes(plugin.state) ? plugin.state : "failed";
36
+ return [{
37
+ ...plugin,
38
+ featureId: plugin.featureId,
39
+ state,
40
+ owned: plugin.owned === true,
41
+ health: plugin.health ?? null,
42
+ failure: normalizedFailure(plugin.failure),
43
+ }];
44
+ });
45
+ const legacyFeatures = Array.isArray(ready.features)
46
+ ? ready.features.filter((id) => typeof id === "string")
47
+ : [];
48
+ const features = [...new Set([
49
+ ...legacyFeatures,
50
+ ...plugins.filter(({ state }) => ["configured", "launching", "ready", "injected"].includes(state)).map(({ featureId }) => featureId),
51
+ ])];
52
+ const renderer = ready.renderer && typeof ready.renderer === "object"
53
+ ? {
54
+ ...ready.renderer,
55
+ discovered: ready.renderer.discovered === true,
56
+ injectedTargets: Number.isInteger(ready.renderer.injectedTargets) ? ready.renderer.injectedTargets : 0,
57
+ active: ready.renderer.active === true,
58
+ runtimeSessionId: ready.renderer.runtimeSessionId ?? null,
59
+ }
60
+ : null;
61
+ return {
62
+ ...ready,
63
+ runtimeSessionId: ready.runtimeSessionId ?? null,
64
+ features,
65
+ plugins,
66
+ failures: Array.isArray(ready.failures) ? ready.failures : [],
67
+ renderer,
68
+ };
69
+ }
70
+
71
+ function isReadyPayload(ready, child) {
72
+ return ready?.pid === child.pid
73
+ && (Array.isArray(ready.features) || Array.isArray(ready.plugins));
74
+ }
75
+
76
+ function rendererUnavailableReady(ready) {
77
+ const normalized = normalizeRuntimeReady(ready);
78
+ return normalizeRuntimeReady({
79
+ ...normalized,
80
+ plugins: normalized.plugins.map((plugin) => plugin.state === "injected"
81
+ ? { ...plugin, state: "ready", health: null }
82
+ : plugin),
83
+ renderer: {
84
+ ...(normalized.renderer ?? {}),
85
+ discovered: false,
86
+ injectedTargets: 0,
87
+ active: false,
88
+ runtimeSessionId: normalized.runtimeSessionId,
89
+ },
90
+ });
91
+ }
92
+
93
+ async function waitForReady(readyPath, child, timeoutMs = 25_000) {
94
+ const deadline = Date.now() + timeoutMs;
95
+ while (Date.now() < deadline) {
96
+ try {
97
+ const ready = JSON.parse(await readFile(readyPath, "utf8"));
98
+ if (isReadyPayload(ready, child)) {
99
+ child.readyPayloadSeen = true;
100
+ return normalizeRuntimeReady(ready);
101
+ }
102
+ } catch (error) {
103
+ if (error.code !== "ENOENT" && !(error instanceof SyntaxError)) throw error;
104
+ }
105
+ if (hasExited(child)) {
106
+ throw new Error(`RuntimeHost exited before becoming ready (${child.signalCode ?? child.exitCode ?? "unknown"})`);
107
+ }
108
+ await new Promise((resolve) => setTimeout(resolve, 100));
109
+ }
110
+ throw new Error("RuntimeHost did not report ready in time");
111
+ }
112
+
113
+ export class RuntimeManager {
114
+ constructor({
115
+ paths,
116
+ platform,
117
+ logger = console,
118
+ spawnProcess = spawn,
119
+ waitForReady: readyWaiter = waitForReady,
120
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
121
+ stopTimeoutMs = 5_000,
122
+ }) {
123
+ this.paths = paths;
124
+ this.platform = platform;
125
+ this.logger = logger;
126
+ this.spawnProcess = spawnProcess;
127
+ this.waitForReady = readyWaiter;
128
+ this.sleep = sleep;
129
+ this.stopTimeoutMs = stopTimeoutMs;
130
+ this.child = null;
131
+ this.appliedFeatures = [];
132
+ this.plugins = [];
133
+ this.ready = null;
134
+ this.stopping = false;
135
+ this.lifecycleQueue = Promise.resolve();
136
+ this.restartInFlight = null;
137
+ }
138
+
139
+ enqueue(operation) {
140
+ const next = this.lifecycleQueue.catch(() => {}).then(operation);
141
+ this.lifecycleQueue = next.catch(() => {});
142
+ return next;
143
+ }
144
+
145
+ async startInternal() {
146
+ if (this.child && !hasExited(this.child)) return this.status();
147
+ this.clearChildState();
148
+ this.stopping = false;
149
+ const config = await readConfig(this.paths.configPath);
150
+ const features = enabledFeatureIds(config);
151
+ const codexInstallation = typeof this.platform?.codex === "function"
152
+ ? await this.platform.codex()
153
+ : null;
154
+ const codexExecutable = codexInstallation?.executable
155
+ ?? process.env.MINECODEX_CODEX_EXECUTABLE;
156
+ await mkdir(this.paths.supportDir, { recursive: true, mode: 0o700 });
157
+ await unlink(this.paths.runtimeReadyPath).catch((error) => {
158
+ if (error.code !== "ENOENT") throw error;
159
+ });
160
+ const child = this.spawnProcess(process.execPath, [this.paths.runtimeEntry], {
161
+ cwd: this.paths.packageRoot,
162
+ env: {
163
+ ...process.env,
164
+ CODEX_FEATURES_ROOT: this.paths.featuresRoot,
165
+ CODEX_RUNTIME_PROFILE_DIR: this.paths.profileDir,
166
+ CODEX_RUNTIME_CDP_PORT: process.env.CODEX_RUNTIME_CDP_PORT ?? "9231",
167
+ CODEX_IMAGE_HOST_DATA_DIR: this.paths.imagesDataDir,
168
+ CODEX_NOTES_DATA_DIR: this.paths.notesDataDir,
169
+ MINECODEX_ENABLED_FEATURES: features.join(","),
170
+ MINECODEX_CODEX_PID_FILE: this.paths.codexPidPath,
171
+ MINECODEX_RUNTIME_READY_FILE: this.paths.runtimeReadyPath,
172
+ ...(codexExecutable ? { MINECODEX_CODEX_EXECUTABLE: codexExecutable } : {}),
173
+ },
174
+ stdio: "inherit",
175
+ });
176
+ this.child = child;
177
+ if (hasExited(child)) {
178
+ this.clearChildState();
179
+ throw new Error(`RuntimeHost exited immediately (${child.signalCode ?? child.exitCode})`);
180
+ }
181
+ child.once("exit", (code, signal) => {
182
+ if (this.child === child) this.clearChildState();
183
+ if (!this.stopping && (code !== 0 || signal !== null)) {
184
+ this.logger.error(`MineCodex RuntimeHost exited (${signal ?? code ?? "unknown"})`);
185
+ }
186
+ });
187
+ try {
188
+ const ready = await this.waitForReady(this.paths.runtimeReadyPath, child);
189
+ this.ready = normalizeRuntimeReady(ready);
190
+ this.appliedFeatures = [...this.ready.features];
191
+ this.plugins = [...this.ready.plugins];
192
+ } catch (startupError) {
193
+ let cleanupError = null;
194
+ this.stopping = true;
195
+ try {
196
+ if (hasExited(child)) {
197
+ this.clearChildState();
198
+ } else {
199
+ const exited = await this.waitForExactExit(child, "SIGTERM", this.stopTimeoutMs);
200
+ const confirmed = exited || await this.waitForExactExit(child, "SIGKILL", this.stopTimeoutMs);
201
+ if (confirmed) {
202
+ this.clearChildState();
203
+ } else {
204
+ cleanupError = new Error(
205
+ `RuntimeHost pid ${child.pid} did not exit after SIGKILL within ${this.stopTimeoutMs}ms`,
206
+ );
207
+ }
208
+ }
209
+ } catch (failure) {
210
+ cleanupError = failure;
211
+ } finally {
212
+ this.stopping = false;
213
+ }
214
+ if (cleanupError) {
215
+ throw new Error(`${startupError.message}; cleanup also failed: ${cleanupError.message}`, { cause: startupError });
216
+ }
217
+ throw startupError;
218
+ }
219
+ return this.status();
220
+ }
221
+
222
+ clearChildState() {
223
+ this.child = null;
224
+ this.ready = null;
225
+ this.appliedFeatures = [];
226
+ this.plugins = [];
227
+ }
228
+
229
+ start() {
230
+ return this.enqueue(() => this.startInternal());
231
+ }
232
+
233
+ async stopInternal() {
234
+ const child = this.child;
235
+ if (!child || hasExited(child)) {
236
+ this.clearChildState();
237
+ return;
238
+ }
239
+ this.stopping = true;
240
+ let confirmed = false;
241
+ try {
242
+ const exited = await this.waitForExactExit(child, "SIGTERM", this.stopTimeoutMs);
243
+ confirmed = exited || await this.waitForExactExit(child, "SIGKILL", this.stopTimeoutMs);
244
+ } finally {
245
+ this.stopping = false;
246
+ }
247
+ if (!confirmed) {
248
+ throw new Error(`RuntimeHost pid ${child.pid} did not exit after SIGKILL within ${this.stopTimeoutMs}ms`);
249
+ }
250
+ this.clearChildState();
251
+ }
252
+
253
+ async waitForExactExit(child, signal, timeoutMs) {
254
+ if (hasExited(child)) return true;
255
+ let exited = false;
256
+ const onExit = () => { exited = true; };
257
+ child.once("exit", onExit);
258
+ try {
259
+ try {
260
+ child.kill(signal);
261
+ } catch (error) {
262
+ if (error?.code === "ESRCH") return true;
263
+ throw error;
264
+ }
265
+ const deadline = Date.now() + timeoutMs;
266
+ while (Date.now() < deadline) {
267
+ if (exited || hasExited(child)) return true;
268
+ await this.sleep(50);
269
+ }
270
+ return exited || hasExited(child);
271
+ } finally {
272
+ child.removeListener?.("exit", onExit);
273
+ }
274
+ }
275
+
276
+ stop() {
277
+ return this.enqueue(() => this.stopInternal());
278
+ }
279
+
280
+ async refreshStatus() {
281
+ const child = this.child;
282
+ if (!child || hasExited(child)) return this.status();
283
+ try {
284
+ const ready = normalizeRuntimeReady(JSON.parse(await readFile(this.paths.runtimeReadyPath, "utf8")));
285
+ const expectedSession = this.ready?.runtimeSessionId ?? null;
286
+ const currentSession = ready.runtimeSessionId ?? null;
287
+ if (!isReadyPayload(ready, child) || (expectedSession && currentSession !== expectedSession)) {
288
+ this.ready = rendererUnavailableReady(this.ready ?? ready);
289
+ } else {
290
+ this.ready = ready;
291
+ this.appliedFeatures = [...ready.features];
292
+ this.plugins = [...ready.plugins];
293
+ }
294
+ } catch (error) {
295
+ if (error.code !== "ENOENT" && !(error instanceof SyntaxError)) throw error;
296
+ if (this.ready) this.ready = rendererUnavailableReady(this.ready);
297
+ }
298
+ if (this.ready) {
299
+ this.appliedFeatures = [...this.ready.features];
300
+ this.plugins = [...this.ready.plugins];
301
+ }
302
+ return this.status();
303
+ }
304
+
305
+ restart() {
306
+ if (this.restartInFlight) return this.restartInFlight;
307
+ const operation = this.enqueue(async () => {
308
+ await this.stopInternal();
309
+ await this.platform.terminateOwnedCodex(this.paths);
310
+ return this.startInternal();
311
+ });
312
+ this.restartInFlight = operation;
313
+ const shared = this.restartInFlight.finally(() => {
314
+ if (this.restartInFlight === operation) this.restartInFlight = null;
315
+ });
316
+ return shared;
317
+ }
318
+
319
+ status() {
320
+ const running = Boolean(this.child && !hasExited(this.child));
321
+ return {
322
+ running,
323
+ pid: this.child?.pid ?? null,
324
+ appliedFeatures: running ? [...this.appliedFeatures] : [],
325
+ plugins: running ? [...this.plugins] : [],
326
+ pluginStates: running ? [...this.plugins] : [],
327
+ ready: running ? this.ready : null,
328
+ runtimeSessionId: running ? this.ready?.runtimeSessionId ?? null : null,
329
+ renderer: running ? this.ready?.renderer ?? null : null,
330
+ failures: running ? [...(this.ready?.failures ?? [])] : [],
331
+ };
332
+ }
333
+ }
334
+
335
+ export async function writeServicePid(pidPath) {
336
+ await writeFile(pidPath, `${process.pid}\n`, { mode: 0o600 });
337
+ return async () => unlink(pidPath).catch(() => {});
338
+ }