minecodex 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -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/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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecodex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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() {
|