catalyst-core 0.3.0-canary.2 → 0.3.0-canary.3
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/changelog.md +1 -1
- package/dist/errors/registry.js +17 -0
- package/dist/errors-index.json +14 -0
- package/dist/native/buildAndroid/build.js +158 -13
- package/dist/native/buildIos/build.js +118 -73
- package/dist/native/buildIos/config.js +11 -0
- package/dist/native/setupEmulatorIos.js +38 -7
- package/dist/scripts/loadEnvironmentVariables.js +20 -1
- package/mcp_v2/lib/helpers.js +25 -13
- package/package.json +1 -1
package/changelog.md
CHANGED
|
@@ -78,7 +78,7 @@ The app contract validators in `server/utils/validator.js` used to log a bare me
|
|
|
78
78
|
|
|
79
79
|
The route and store checks moved out of the per-request path. `validateGetRoutes` and `validateConfigureStore` now run once when the renderer module loads — at server startup in production, and on the first SSR request in development, where the handler is loaded lazily — rather than once per request inside the handler's `try`/`catch`. The shape of `addMiddlewares` from `server/server.js` is checked during server setup.
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
Separately, `catalyst start`, `build` and `serve` now check `config/config.json` and `package.json` before starting and exit with a coded error when either is missing, a required config key is missing, or a required module alias is missing. An application that satisfies the contract sees no change. The full contract — the files Catalyst imports by fixed path and the required module aliases — is documented under File Conventions.
|
|
82
82
|
The webpack-era keys `WEBPACK_DEV_SERVER_HOSTNAME`, `WEBPACK_DEV_SERVER_PORT` and `ANALYZE_BUNDLE` are no longer required or read; remove them from `config/config.json`.
|
|
83
83
|
|
|
84
84
|
## [0.3.0-beta.5] - 2026-08-18
|
package/dist/errors/registry.js
CHANGED
|
@@ -28,7 +28,9 @@ export const ERROR_CODES = {
|
|
|
28
28
|
|
|
29
29
|
BUNDLE_UPSTREAM_ERROR: "BUNDLE-000",
|
|
30
30
|
IOS_UPSTREAM_ERROR: "IOS-000",
|
|
31
|
+
IOS_SIMULATOR_APP_NOT_FOUND: "IOS-001",
|
|
31
32
|
ANDROID_UPSTREAM_ERROR: "ANDROID-000",
|
|
33
|
+
ANDROID_EMULATOR_BOOT_TIMEOUT: "ANDROID-001",
|
|
32
34
|
|
|
33
35
|
RUNTIME_NATIVE_PERMISSION_DENIED: "RUNTIME-NATIVE-001",
|
|
34
36
|
RUNTIME_NATIVE_PERMISSION_REQUIRED: "RUNTIME-NATIVE-002",
|
|
@@ -231,12 +233,27 @@ export const ERROR_DEFINITIONS = {
|
|
|
231
233
|
defaultDetails: "This wraps an error from Xcode/CocoaPods. See the printed upstream output for the cause.",
|
|
232
234
|
suggestedAction: "Read the upstream Xcode/CocoaPods error printed above and fix the underlying issue.",
|
|
233
235
|
},
|
|
236
|
+
[ERROR_CODES.IOS_SIMULATOR_APP_NOT_FOUND]: {
|
|
237
|
+
category: "IOS",
|
|
238
|
+
defaultMessage: "Could not find an installed iOS Simulator app",
|
|
239
|
+
defaultDetails:
|
|
240
|
+
"Neither DeviceHub.app (Xcode 27+, bundle id com.apple.dt.Devices) nor the legacy Simulator.app (bundle id com.apple.iphonesimulator) is installed under Xcode's Contents/Applications. The simulator itself may still boot headlessly via simctl, but catalyst-core cannot open or focus its window.",
|
|
241
|
+
suggestedAction: "Confirm a full Xcode.app (not just Command Line Tools) is installed and selected via xcode-select -p, then reopen it once from Spotlight so macOS registers its app bundle.",
|
|
242
|
+
},
|
|
234
243
|
[ERROR_CODES.ANDROID_UPSTREAM_ERROR]: {
|
|
235
244
|
category: "ANDROID",
|
|
236
245
|
defaultMessage: "Android build failed in an upstream toolchain step",
|
|
237
246
|
defaultDetails: "This wraps an error from Gradle. See the printed upstream output for the cause.",
|
|
238
247
|
suggestedAction: "Read the upstream Gradle error printed above and fix the underlying issue.",
|
|
239
248
|
},
|
|
249
|
+
[ERROR_CODES.ANDROID_EMULATOR_BOOT_TIMEOUT]: {
|
|
250
|
+
category: "ANDROID",
|
|
251
|
+
defaultMessage: "Timed out waiting for the Android emulator to boot",
|
|
252
|
+
defaultDetails:
|
|
253
|
+
"buildApp:android launched the configured AVD but it did not appear in `adb devices` or did not finish booting (sys.boot_completed=1 and init.svc.bootanim=stopped) within the timeout.",
|
|
254
|
+
suggestedAction:
|
|
255
|
+
"Try booting the AVD manually first (emulator -avd <name>) to see the actual startup error, check available disk space/RAM, or increase the emulator's cold-boot allowance if the device is unusually slow.",
|
|
256
|
+
},
|
|
240
257
|
|
|
241
258
|
[ERROR_CODES.RUNTIME_NATIVE_PERMISSION_DENIED]: {
|
|
242
259
|
category: "RUNTIME-NATIVE",
|
package/dist/errors-index.json
CHANGED
|
@@ -244,6 +244,13 @@
|
|
|
244
244
|
"suggestedAction": "Read the upstream Xcode/CocoaPods error printed above and fix the underlying issue.",
|
|
245
245
|
"docUrl": "https://github.com/tata1mg/catalyst-core/blob/main/errors/IOS/IOS-000.md"
|
|
246
246
|
},
|
|
247
|
+
"IOS-001": {
|
|
248
|
+
"category": "IOS",
|
|
249
|
+
"message": "Could not find an installed iOS Simulator app",
|
|
250
|
+
"details": "Neither DeviceHub.app (Xcode 27+, bundle id com.apple.dt.Devices) nor the legacy Simulator.app (bundle id com.apple.iphonesimulator) is installed under Xcode's Contents/Applications. The simulator itself may still boot headlessly via simctl, but catalyst-core cannot open or focus its window.",
|
|
251
|
+
"suggestedAction": "Confirm a full Xcode.app (not just Command Line Tools) is installed and selected via xcode-select -p, then reopen it once from Spotlight so macOS registers its app bundle.",
|
|
252
|
+
"docUrl": "https://github.com/tata1mg/catalyst-core/blob/main/errors/IOS/IOS-001.md"
|
|
253
|
+
},
|
|
247
254
|
"ANDROID-000": {
|
|
248
255
|
"category": "ANDROID",
|
|
249
256
|
"message": "Android build failed in an upstream toolchain step",
|
|
@@ -251,6 +258,13 @@
|
|
|
251
258
|
"suggestedAction": "Read the upstream Gradle error printed above and fix the underlying issue.",
|
|
252
259
|
"docUrl": "https://github.com/tata1mg/catalyst-core/blob/main/errors/ANDROID/ANDROID-000.md"
|
|
253
260
|
},
|
|
261
|
+
"ANDROID-001": {
|
|
262
|
+
"category": "ANDROID",
|
|
263
|
+
"message": "Timed out waiting for the Android emulator to boot",
|
|
264
|
+
"details": "buildApp:android launched the configured AVD but it did not appear in `adb devices` or did not finish booting (sys.boot_completed=1 and init.svc.bootanim=stopped) within the timeout.",
|
|
265
|
+
"suggestedAction": "Try booting the AVD manually first (emulator -avd <name>) to see the actual startup error, check available disk space/RAM, or increase the emulator's cold-boot allowance if the device is unusually slow.",
|
|
266
|
+
"docUrl": "https://github.com/tata1mg/catalyst-core/blob/main/errors/ANDROID/ANDROID-001.md"
|
|
267
|
+
},
|
|
254
268
|
"RUNTIME-NATIVE-001": {
|
|
255
269
|
"category": "RUNTIME-NATIVE",
|
|
256
270
|
"message": "Permission denied",
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
|
+
const {
|
|
6
|
+
spawn,
|
|
7
|
+
execFileSync
|
|
8
|
+
} = require("child_process");
|
|
5
9
|
const {
|
|
6
10
|
buildAndroidAAB
|
|
7
11
|
} = require("../renameAndroidProject.js");
|
|
@@ -101,36 +105,176 @@ function createBuildPhase(ctx) {
|
|
|
101
105
|
return false;
|
|
102
106
|
}
|
|
103
107
|
}
|
|
108
|
+
|
|
109
|
+
// Returns the serial of an online emulator (adb state "device"), or null if
|
|
110
|
+
// none is online. `devices.includes("emulator")` alone would also match a
|
|
111
|
+
// serial like "emulator-5554" whose state is "offline" or still enumerating —
|
|
112
|
+
// i.e. a device that's connected but not actually ready — so this checks the
|
|
113
|
+
// state column explicitly rather than doing a substring match.
|
|
104
114
|
async function checkEmulator(ADB_PATH) {
|
|
105
115
|
try {
|
|
106
116
|
// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
|
|
107
117
|
const devices = ctx.runCommand(`${ADB_PATH} devices`);
|
|
108
|
-
|
|
118
|
+
const onlineEmulator = devices.split("\n").map(line => line.trim().split(/\s+/)).find(([id, state]) => id?.startsWith("emulator-") && state === "device");
|
|
119
|
+
return onlineEmulator ? onlineEmulator[0] : null;
|
|
109
120
|
} catch (error) {
|
|
110
121
|
progress.log("Error checking emulator status: " + error.message, "error");
|
|
111
|
-
return
|
|
122
|
+
return null;
|
|
112
123
|
}
|
|
113
124
|
}
|
|
125
|
+
|
|
126
|
+
// The Android emulator binary is a long-running server process — it does not
|
|
127
|
+
// exit after boot, it keeps running until the emulator window is closed. Using
|
|
128
|
+
// runInteractiveCommand (which only resolves on the child's `close` event, see
|
|
129
|
+
// utils.js) would block forever waiting for an exit that never happens before
|
|
130
|
+
// the app finishes booting. Spawn it detached instead (same approach as
|
|
131
|
+
// androidSetup.js's startEmulator) and let handleEmulatorSetup poll for
|
|
132
|
+
// readiness separately.
|
|
133
|
+
/**
|
|
134
|
+
* Launch the emulator detached and return once it has actually spawned
|
|
135
|
+
* (not once it exits — see the note above on why runInteractiveCommand
|
|
136
|
+
* can't be used here).
|
|
137
|
+
* @returns {Promise<number>} the spawned process's pid, so a caller whose
|
|
138
|
+
* boot-readiness wait times out can kill it instead of leaving a
|
|
139
|
+
* CPU/RAM-heavy orphaned emulator running past a failed build.
|
|
140
|
+
*/
|
|
114
141
|
async function startEmulator(EMULATOR_PATH, androidConfig) {
|
|
115
142
|
progress.log(`Starting emulator: ${androidConfig.emulatorName}...`, "info");
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - spawn with an argv array (no shell), so androidConfig.emulatorName can't be interpreted as shell syntax even though it comes from a locally-editable config file.
|
|
145
|
+
const child = spawn(EMULATOR_PATH, ["-avd", androidConfig.emulatorName, "-read-only"], {
|
|
146
|
+
detached: true,
|
|
147
|
+
stdio: "ignore"
|
|
148
|
+
});
|
|
149
|
+
child.once("error", error => {
|
|
150
|
+
progress.log("Error starting emulator: " + error.message, "error");
|
|
151
|
+
reject(error);
|
|
152
|
+
});
|
|
153
|
+
// "error" only fires for spawn failures (e.g. bad EMULATOR_PATH); once the
|
|
154
|
+
// process has actually spawned we can detach and let the caller poll adb
|
|
155
|
+
// for boot readiness rather than waiting on this child at all.
|
|
156
|
+
child.once("spawn", () => {
|
|
157
|
+
const {
|
|
158
|
+
pid
|
|
159
|
+
} = child;
|
|
160
|
+
child.unref();
|
|
161
|
+
progress.log("Emulator process started, waiting for it to boot...", "info");
|
|
162
|
+
resolve(pid);
|
|
163
|
+
});
|
|
122
164
|
});
|
|
123
165
|
}
|
|
166
|
+
|
|
167
|
+
// Best-effort cleanup for an emulator process this build spawned but that
|
|
168
|
+
// never became ready — kills the whole detached process group (negative pid)
|
|
169
|
+
// so we don't leave a CPU/RAM-heavy orphaned emulator running after a failed
|
|
170
|
+
// build. Never throws: a failure here shouldn't mask the original timeout error.
|
|
171
|
+
function killOrphanedEmulator(pid, emulatorName) {
|
|
172
|
+
try {
|
|
173
|
+
process.kill(-pid, "SIGTERM");
|
|
174
|
+
progress.log(`Stopped orphaned emulator process (pid ${pid}) after boot failure`, "info");
|
|
175
|
+
} catch (error) {
|
|
176
|
+
progress.log(`Could not stop orphaned emulator "${emulatorName}" (pid ${pid}): ${error.message}. ` + "You may need to quit it manually.", "warning");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function listEmulatorSerials(ADB_PATH) {
|
|
180
|
+
try {
|
|
181
|
+
// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
|
|
182
|
+
const devices = ctx.runCommand(`${ADB_PATH} devices`);
|
|
183
|
+
return devices.split("\n").map(line => line.trim().split(/\s+/)[0]).filter(id => id && id.startsWith("emulator-"));
|
|
184
|
+
} catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Waits for a NEW emulator-XXXX serial to appear in `adb devices` (one not in
|
|
190
|
+
// knownSerials, captured before startEmulator was called), so boot polling can
|
|
191
|
+
// be scoped to the emulator this build just launched rather than whichever
|
|
192
|
+
// device `adb shell` (with no -s) happens to pick when multiple are connected.
|
|
193
|
+
async function waitForNewEmulatorSerial(ADB_PATH, knownSerials, {
|
|
194
|
+
timeoutMs = 30000,
|
|
195
|
+
pollIntervalMs = 1000
|
|
196
|
+
} = {}) {
|
|
197
|
+
const deadline = Date.now() + timeoutMs;
|
|
198
|
+
while (Date.now() < deadline) {
|
|
199
|
+
const current = listEmulatorSerials(ADB_PATH);
|
|
200
|
+
const newSerial = current.find(id => !knownSerials.includes(id));
|
|
201
|
+
if (newSerial) return newSerial;
|
|
202
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Poll a specific emulator (by adb serial, e.g. "emulator-5554") until it
|
|
209
|
+
* reports a completed boot and a stopped boot animation, or the timeout
|
|
210
|
+
* elapses.
|
|
211
|
+
* @param {string} ADB_PATH Path to the adb executable.
|
|
212
|
+
* @param {string} serial The emulator's adb serial to target (adb -s).
|
|
213
|
+
* @param {Object} [options]
|
|
214
|
+
* @param {number} [options.timeoutMs=120000] Overall deadline in milliseconds.
|
|
215
|
+
* @param {number} [options.pollIntervalMs=2000] Delay between readiness checks.
|
|
216
|
+
* @returns {Promise<boolean>} true once both readiness signals are observed;
|
|
217
|
+
* false if the deadline elapses first (a transient/hung adb call counts
|
|
218
|
+
* against the same deadline via the per-call execFileSync timeout below).
|
|
219
|
+
*/
|
|
220
|
+
async function waitForEmulatorBoot(ADB_PATH, serial, {
|
|
221
|
+
timeoutMs = 120000,
|
|
222
|
+
pollIntervalMs = 2000
|
|
223
|
+
} = {}) {
|
|
224
|
+
const deadline = Date.now() + timeoutMs;
|
|
225
|
+
const getprop = prop => {
|
|
226
|
+
const remaining = Math.max(deadline - Date.now(), 1000);
|
|
227
|
+
return execFileSync(ADB_PATH, ["-s", serial, "shell", "getprop", prop], {
|
|
228
|
+
encoding: "utf8",
|
|
229
|
+
timeout: remaining
|
|
230
|
+
}).trim();
|
|
231
|
+
};
|
|
232
|
+
while (Date.now() < deadline) {
|
|
233
|
+
try {
|
|
234
|
+
const bootCompleted = getprop("sys.boot_completed");
|
|
235
|
+
const bootAnimDone = getprop("init.svc.bootanim");
|
|
236
|
+
if (bootCompleted === "1" && bootAnimDone === "stopped") return true;
|
|
237
|
+
} catch {
|
|
238
|
+
// adb not ready yet (device still enumerating), or this call hit its
|
|
239
|
+
// own per-call timeout — either way, keep polling against the outer
|
|
240
|
+
// deadline rather than treating it as fatal.
|
|
241
|
+
}
|
|
242
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Message mirrors errors/registry.js ERROR_DEFINITIONS[ANDROID-001] (see
|
|
248
|
+
// errors/ANDROID/ANDROID-001.md) — this CJS subtree can't import the ESM
|
|
249
|
+
// error registry (see buildErrorFormat.js), so the code is prefixed onto the
|
|
250
|
+
// thrown message directly rather than imported.
|
|
251
|
+
function emulatorBootTimeoutError(detail) {
|
|
252
|
+
return new Error(`[ANDROID-001] Timed out waiting for the Android emulator to boot: ${detail}`);
|
|
253
|
+
}
|
|
124
254
|
async function handleEmulatorSetup(ADB_PATH, EMULATOR_PATH, androidConfig) {
|
|
125
255
|
progress.log("Setting up emulator...", "info");
|
|
126
|
-
|
|
127
|
-
|
|
256
|
+
let serial = await checkEmulator(ADB_PATH);
|
|
257
|
+
let pid = null;
|
|
258
|
+
if (!serial) {
|
|
128
259
|
progress.log("No emulator running, attempting to start one...", "info");
|
|
129
|
-
|
|
130
|
-
|
|
260
|
+
const knownSerials = listEmulatorSerials(ADB_PATH);
|
|
261
|
+
pid = await startEmulator(EMULATOR_PATH, androidConfig);
|
|
262
|
+
serial = await waitForNewEmulatorSerial(ADB_PATH, knownSerials);
|
|
263
|
+
if (!serial) {
|
|
264
|
+
killOrphanedEmulator(pid, androidConfig.emulatorName);
|
|
265
|
+
throw emulatorBootTimeoutError(`emulator "${androidConfig.emulatorName}" never appeared in adb devices`);
|
|
266
|
+
}
|
|
131
267
|
} else {
|
|
132
|
-
progress.log(
|
|
268
|
+
progress.log(`Emulator ${serial} already online, confirming it's fully booted...`, "info");
|
|
269
|
+
}
|
|
270
|
+
// Wait for boot readiness whether the emulator was already online (it may
|
|
271
|
+
// still be finishing its boot animation) or was just launched above.
|
|
272
|
+
const booted = await waitForEmulatorBoot(ADB_PATH, serial);
|
|
273
|
+
if (!booted) {
|
|
274
|
+
if (pid) killOrphanedEmulator(pid, androidConfig.emulatorName);
|
|
275
|
+
throw emulatorBootTimeoutError(`emulator "${androidConfig.emulatorName}" (${serial}) never finished booting`);
|
|
133
276
|
}
|
|
277
|
+
progress.log("Emulator booted successfully", "success");
|
|
134
278
|
return {
|
|
135
279
|
type: "emulator",
|
|
136
280
|
name: androidConfig.emulatorName
|
|
@@ -286,6 +430,7 @@ function createBuildPhase(ctx) {
|
|
|
286
430
|
checkEmulator,
|
|
287
431
|
handleEmulatorSetup,
|
|
288
432
|
startEmulator,
|
|
433
|
+
waitForEmulatorBoot,
|
|
289
434
|
buildApp,
|
|
290
435
|
launchApp,
|
|
291
436
|
createAABConfig,
|
|
@@ -72,6 +72,33 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Xcode 27 replaced the standalone Simulator.app (com.apple.iphonesimulator)
|
|
76
|
+
// with DeviceHub.app (com.apple.dt.Devices); older Xcode installs still ship
|
|
77
|
+
// the former. Resolve whichever is actually installed instead of hardcoding
|
|
78
|
+
// the app name, which no longer resolves via `open -a` on either app.
|
|
79
|
+
function resolveSimulatorAppBundleId() {
|
|
80
|
+
const candidates = ["com.apple.dt.Devices", "com.apple.iphonesimulator"];
|
|
81
|
+
for (const bundleId of candidates) {
|
|
82
|
+
try {
|
|
83
|
+
execSync(`osascript -e 'id of app id "${bundleId}"'`, {
|
|
84
|
+
stdio: "ignore"
|
|
85
|
+
}); // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - bundleId is a fixed internal candidate list, not user input.
|
|
86
|
+
return bundleId;
|
|
87
|
+
} catch {
|
|
88
|
+
// Not installed under this bundle id; try the next candidate.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Text mirrors errors/registry.js ERROR_DEFINITIONS[IOS-001] (see
|
|
95
|
+
// errors/IOS/IOS-001.md) — this CJS subtree can't import the ESM error
|
|
96
|
+
// registry (see buildErrorFormat.js), so the message is duplicated here
|
|
97
|
+
// rather than imported. This condition is recoverable (the simulator can
|
|
98
|
+
// still boot headlessly via simctl) so callers log it as a warning
|
|
99
|
+
// instead of throwing.
|
|
100
|
+
const SIMULATOR_APP_NOT_FOUND_WARNING = "[IOS-001] Could not find an installed iOS Simulator app (checked DeviceHub.app and the legacy Simulator.app). " + "The simulator is booted but its window may not be visible. Suggested action: confirm a full Xcode.app is installed and selected via xcode-select -p, then open it once from Spotlight.";
|
|
101
|
+
|
|
75
102
|
// ─── Clean ────────────────────────────────────────────────────────────────
|
|
76
103
|
|
|
77
104
|
async function cleanBuildArtifacts() {
|
|
@@ -304,7 +331,12 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
304
331
|
}
|
|
305
332
|
async function focusSimulator() {
|
|
306
333
|
console.log("Focusing on Simulator...");
|
|
307
|
-
|
|
334
|
+
const simulatorAppBundleId = resolveSimulatorAppBundleId();
|
|
335
|
+
if (!simulatorAppBundleId) {
|
|
336
|
+
progress.log(SIMULATOR_APP_NOT_FOUND_WARNING, "warning");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
await runCommand(`osascript -e 'tell application id "${simulatorAppBundleId}" to activate'`);
|
|
308
340
|
}
|
|
309
341
|
async function installAndLaunchApp(APP_PATH) {
|
|
310
342
|
progress.start("install");
|
|
@@ -326,6 +358,60 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
326
358
|
|
|
327
359
|
// ─── Physical device ──────────────────────────────────────────────────────
|
|
328
360
|
|
|
361
|
+
// `instruments -s devices` is deprecated and its plain-text output is fragile
|
|
362
|
+
// to parse. `xcrun devicectl list devices --json-output <path>` is the modern,
|
|
363
|
+
// structured replacement (Xcode 15+) and is already used elsewhere in this
|
|
364
|
+
// file for install/launch, so use it as the primary detection source too.
|
|
365
|
+
// Note: devicectl lists simulators as well as physical devices, so filter on
|
|
366
|
+
// hardwareProperties.reality === "physical".
|
|
367
|
+
//
|
|
368
|
+
// Write to a temp file rather than `--json-output -` (stdout): Apple's own
|
|
369
|
+
// scripting guidance is a file path, and stdout support for `-` isn't
|
|
370
|
+
// guaranteed across every Xcode version this needs to run on — a temp file
|
|
371
|
+
// works the same way on every devicectl version. Falls back to
|
|
372
|
+
// xcodebuild -showdestinations below on any failure (old devicectl, no
|
|
373
|
+
// physical device, malformed output), so this only needs to work when it can.
|
|
374
|
+
// Matches the identifier format Apple's own tooling uses (hex digits and
|
|
375
|
+
// hyphens only, e.g. a UUID or the shorter legacy 40-char UDID). device.udid
|
|
376
|
+
// is later interpolated unquoted into shell command strings (xcodebuild
|
|
377
|
+
// -destination, xcrun devicectl --device, see installAndLaunchOnPhysicalDevice
|
|
378
|
+
// and buildProjectForPhysicalDevice) — the old instruments-based parser
|
|
379
|
+
// structurally constrained what could reach that string via its own regex;
|
|
380
|
+
// this is the equivalent guard for the JSON-based devicectl path, so a
|
|
381
|
+
// malformed or hostile identifier in devicectl's output can't inject shell
|
|
382
|
+
// syntax rather than just being rejected as "not a device".
|
|
383
|
+
const VALID_UDID_RE = /^[A-Fa-f0-9-]+$/;
|
|
384
|
+
function tryDevicectl() {
|
|
385
|
+
const tmpPath = path.join(os.tmpdir(), `catalyst-devicectl-${process.pid}-${Date.now()}.json`);
|
|
386
|
+
try {
|
|
387
|
+
execSync(`xcrun devicectl list devices --json-output "${tmpPath}"`); // nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - tmpPath is built from os.tmpdir()/process.pid/Date.now(), not user input.
|
|
388
|
+
const raw = fs.readFileSync(tmpPath, "utf8");
|
|
389
|
+
const parsed = JSON.parse(raw);
|
|
390
|
+
const devices = parsed?.result?.devices ?? [];
|
|
391
|
+
return devices.filter(d => d.hardwareProperties?.reality === "physical")
|
|
392
|
+
// devicectl lists every device it has ever paired with, reachable or
|
|
393
|
+
// not — a device that's unplugged or out of range still shows up with
|
|
394
|
+
// reality: "physical". Only tunnelState !== "unavailable" is actually
|
|
395
|
+
// installable/launchable right now; "disconnected" (paired, USB/Wi-Fi
|
|
396
|
+
// not currently active but developer-mode discoverable) still works for
|
|
397
|
+
// wired installs, so only "unavailable" is excluded here.
|
|
398
|
+
.filter(d => d.connectionProperties?.tunnelState !== "unavailable").map(d => ({
|
|
399
|
+
name: d.deviceProperties?.name ?? "Physical Device",
|
|
400
|
+
version: d.deviceProperties?.osVersionNumber ?? "Unknown",
|
|
401
|
+
udid: d.hardwareProperties?.udid ?? d.identifier,
|
|
402
|
+
type: "physical"
|
|
403
|
+
})).filter(d => typeof d.udid === "string" && VALID_UDID_RE.test(d.udid));
|
|
404
|
+
} catch (error) {
|
|
405
|
+
progress.log(`devicectl detection failed (${error.message}); falling back to xcodebuild destinations`, "warning");
|
|
406
|
+
return [];
|
|
407
|
+
} finally {
|
|
408
|
+
try {
|
|
409
|
+
fs.unlinkSync(tmpPath);
|
|
410
|
+
} catch {
|
|
411
|
+
// Nothing to clean up if the command failed before writing the file.
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
329
415
|
async function detectPhysicalDevices() {
|
|
330
416
|
progress.start("deviceDetection");
|
|
331
417
|
const {
|
|
@@ -337,57 +423,19 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
337
423
|
const configuredUDID = iosConfig.deviceUDID;
|
|
338
424
|
if (configuredUDID) {
|
|
339
425
|
progress.log(`Using configured device UDID: ${configuredUDID}`, "info");
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const nameMatch = deviceLine.match(/^(.+?)\s+\(/);
|
|
346
|
-
const versionMatch = deviceLine.match(/\((\d+\.\d+(?:\.\d+)?)\)/);
|
|
347
|
-
const deviceName = nameMatch ? nameMatch[1].trim() : "Physical Device";
|
|
348
|
-
const deviceVersion = versionMatch ? versionMatch[1] : "Unknown";
|
|
349
|
-
progress.log(`✅ Found configured physical device: ${deviceName} (${deviceVersion})`, "success");
|
|
350
|
-
physicalDevices.push({
|
|
351
|
-
name: deviceName,
|
|
352
|
-
version: deviceVersion,
|
|
353
|
-
udid: configuredUDID,
|
|
354
|
-
type: "physical"
|
|
355
|
-
});
|
|
356
|
-
progress.complete("deviceDetection");
|
|
357
|
-
return physicalDevices[0];
|
|
358
|
-
}
|
|
359
|
-
} else {
|
|
360
|
-
progress.log(`⚠️ Configured device UDID not found in connected devices`, "warning");
|
|
361
|
-
progress.log("Falling back to auto-detection...", "info");
|
|
362
|
-
}
|
|
363
|
-
} catch (error) {
|
|
364
|
-
progress.log(`Error verifying configured device: ${error.message}`, "warning");
|
|
365
|
-
progress.log("Falling back to auto-detection...", "info");
|
|
426
|
+
const configuredDevice = tryDevicectl().find(d => d.udid === configuredUDID);
|
|
427
|
+
if (configuredDevice) {
|
|
428
|
+
progress.log(`✅ Found configured physical device: ${configuredDevice.name} (${configuredDevice.version})`, "success");
|
|
429
|
+
progress.complete("deviceDetection");
|
|
430
|
+
return configuredDevice;
|
|
366
431
|
}
|
|
432
|
+
progress.log(`⚠️ Configured device UDID not found in connected devices`, "warning");
|
|
433
|
+
progress.log("Falling back to auto-detection...", "info");
|
|
367
434
|
} else {
|
|
368
435
|
progress.log("No device UDID configured, using auto-detection", "info");
|
|
369
436
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const out = execSync("instruments -s devices").toString();
|
|
373
|
-
return out.split("\n").filter(line => {
|
|
374
|
-
const m = line.match(/^(.+?)\s+\((\d+\.\d+(?:\.\d+)?)\)\s+\[([A-F0-9-]{36})\](?:\s+\(Simulator\))?$/);
|
|
375
|
-
return m && !line.includes("(Simulator)");
|
|
376
|
-
}).map(line => {
|
|
377
|
-
const [, name, version, udid] = line.match(/^(.+?)\s+\((\d+\.\d+(?:\.\d+)?)\)\s+\[([A-F0-9-]{36})\]/);
|
|
378
|
-
return {
|
|
379
|
-
name: name.trim(),
|
|
380
|
-
version,
|
|
381
|
-
udid,
|
|
382
|
-
type: "physical"
|
|
383
|
-
};
|
|
384
|
-
});
|
|
385
|
-
} catch {
|
|
386
|
-
return [];
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
physicalDevices = tryInstruments();
|
|
390
|
-
if (physicalDevices.length > 0) progress.log(`Found ${physicalDevices.length} physical device(s) via instruments`, "success");
|
|
437
|
+
physicalDevices = tryDevicectl();
|
|
438
|
+
if (physicalDevices.length > 0) progress.log(`Found ${physicalDevices.length} physical device(s) via devicectl`, "success");
|
|
391
439
|
if (physicalDevices.length === 0) {
|
|
392
440
|
try {
|
|
393
441
|
const xcodebuildOutput = execSync(`xcodebuild -scheme "${SCHEME_NAME}" -showdestinations` // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - SCHEME_NAME is generated from trusted iOS project config.
|
|
@@ -417,25 +465,6 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
417
465
|
progress.log(`Error: ${error.message}`, "error");
|
|
418
466
|
}
|
|
419
467
|
}
|
|
420
|
-
if (physicalDevices.length === 0) physicalDevices = tryInstruments();
|
|
421
|
-
if (physicalDevices.length === 0) {
|
|
422
|
-
try {
|
|
423
|
-
for (const line of execSync("xcrun devicectl list devices").toString().split("\n")) {
|
|
424
|
-
if (line.includes("Connected") && !line.includes("Simulator")) {
|
|
425
|
-
const udidMatch = line.match(/([A-F0-9-]{36})/);
|
|
426
|
-
const nameMatch = line.match(/^(.+?)\s+\(/);
|
|
427
|
-
if (udidMatch && nameMatch) physicalDevices.push({
|
|
428
|
-
name: nameMatch[1].trim(),
|
|
429
|
-
version: "Unknown",
|
|
430
|
-
udid: udidMatch[1],
|
|
431
|
-
type: "physical"
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
} catch {
|
|
436
|
-
progress.log("devicectl command not available or failed", "warning");
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
468
|
if (physicalDevices.length > 0) {
|
|
440
469
|
progress.log(`Found ${physicalDevices.length} physical device(s):`, "success");
|
|
441
470
|
physicalDevices.forEach(d => progress.log(` 📱 ${d.name} (${d.version || "Unknown iOS"}) - ${d.udid}`, "info"));
|
|
@@ -561,15 +590,26 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
561
590
|
try {
|
|
562
591
|
progress.log("Launching iOS Simulator...");
|
|
563
592
|
const simulatorsJson = JSON.parse(execSync("xcrun simctl list devices -j").toString());
|
|
593
|
+
// Multiple devices can share a name across runtimes (e.g. leftover devices
|
|
594
|
+
// from an old Xcode install after an upgrade) — simctl marks the ones whose
|
|
595
|
+
// runtime bundle is gone as isAvailable: false, and those fail to boot with
|
|
596
|
+
// "cannot determine the runtime bundle". Don't let iteration order silently
|
|
597
|
+
// pick one of those over the available device: prefer an available match,
|
|
598
|
+
// and among available matches prefer one that's already booted.
|
|
599
|
+
const isDeviceAvailable = device => device.isAvailable !== false;
|
|
564
600
|
let foundSimulator = null,
|
|
565
601
|
foundSimulatorId = null,
|
|
566
602
|
isBooted = false;
|
|
567
603
|
Object.entries(simulatorsJson.devices).forEach(([, devices]) => {
|
|
568
604
|
devices.forEach(device => {
|
|
569
|
-
if (device.name
|
|
605
|
+
if (device.name !== simulatorName) return;
|
|
606
|
+
const available = isDeviceAvailable(device);
|
|
607
|
+
const alreadyBooted = device.state === "Booted";
|
|
608
|
+
const isBetterMatch = !foundSimulator || available && !isDeviceAvailable(foundSimulator) || available === isDeviceAvailable(foundSimulator) && alreadyBooted && !isBooted;
|
|
609
|
+
if (isBetterMatch) {
|
|
570
610
|
foundSimulator = device;
|
|
571
611
|
foundSimulatorId = device.udid;
|
|
572
|
-
isBooted =
|
|
612
|
+
isBooted = alreadyBooted;
|
|
573
613
|
}
|
|
574
614
|
});
|
|
575
615
|
});
|
|
@@ -600,10 +640,15 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
600
640
|
} else {
|
|
601
641
|
progress.log(`Simulator ${simulatorName} is already booted`, "success");
|
|
602
642
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
643
|
+
const simulatorAppBundleId = resolveSimulatorAppBundleId();
|
|
644
|
+
if (simulatorAppBundleId) {
|
|
645
|
+
progress.log("Opening simulator UI...");
|
|
646
|
+
await runCommand(`open -b ${simulatorAppBundleId}`);
|
|
647
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
648
|
+
await runCommand(`osascript -e 'tell application id "${simulatorAppBundleId}" to activate'`);
|
|
649
|
+
} else {
|
|
650
|
+
progress.log(SIMULATOR_APP_NOT_FOUND_WARNING, "warning");
|
|
651
|
+
}
|
|
607
652
|
progress.log("iOS Simulator launched successfully.", "success");
|
|
608
653
|
progress.complete("launchSimulator");
|
|
609
654
|
} catch (error) {
|
|
@@ -161,6 +161,7 @@ public enum ConfigConstants {
|
|
|
161
161
|
for (const [key, value] of Object.entries(iosConfig)) {
|
|
162
162
|
if (addedKeys.has(key)) continue;
|
|
163
163
|
configContent += "\n" + generateSwiftProperty(key, value);
|
|
164
|
+
addedKeys.add(key);
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
if (WEBVIEW_CONFIG.accessControl) {
|
|
@@ -220,6 +221,16 @@ public enum ConfigConstants {
|
|
|
220
221
|
configContent += '\n public static let appInfo = ""';
|
|
221
222
|
addedKeys.add("appInfo");
|
|
222
223
|
}
|
|
224
|
+
|
|
225
|
+
// Ensure cachePattern always exists (default to no patterns, i.e. caching
|
|
226
|
+
// opt-in disabled) — CacheManager.swift references ConfigConstants.cachePattern
|
|
227
|
+
// unconditionally, and it's only emitted above when WEBVIEW_CONFIG.ios.cachePattern
|
|
228
|
+
// is set, which every create-catalyst-app scaffold omits by default (#485).
|
|
229
|
+
if (!addedKeys.has("cachePattern")) {
|
|
230
|
+
progress.log("cachePattern not found in config, adding default (no patterns)", "info");
|
|
231
|
+
configContent += "\n" + generateSwiftProperty("cachePattern", []);
|
|
232
|
+
addedKeys.add("cachePattern");
|
|
233
|
+
}
|
|
223
234
|
configContent += `\n}`;
|
|
224
235
|
fs.writeFileSync(appConfigPath, configContent, "utf8");
|
|
225
236
|
progress.log("Configuration constants generated successfully (SPM Package)", "success");
|
|
@@ -242,6 +242,32 @@ async function configureSimulator(config) {
|
|
|
242
242
|
// }
|
|
243
243
|
// }
|
|
244
244
|
|
|
245
|
+
// Xcode 27 replaced the standalone Simulator.app (com.apple.iphonesimulator)
|
|
246
|
+
// with DeviceHub.app (com.apple.dt.Devices); older Xcode installs still ship
|
|
247
|
+
// the former. Resolve whichever is actually installed instead of hardcoding
|
|
248
|
+
// the app name, which no longer resolves via `open -a` on either app.
|
|
249
|
+
function resolveSimulatorAppBundleId() {
|
|
250
|
+
const candidates = ["com.apple.dt.Devices", "com.apple.iphonesimulator"];
|
|
251
|
+
for (const bundleId of candidates) {
|
|
252
|
+
try {
|
|
253
|
+
(0, _child_process.execSync)(`osascript -e 'id of app id "${bundleId}"'`, {
|
|
254
|
+
stdio: "ignore"
|
|
255
|
+
});
|
|
256
|
+
return bundleId;
|
|
257
|
+
} catch {
|
|
258
|
+
// Not installed under this bundle id; try the next candidate.
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Text mirrors errors/registry.js ERROR_DEFINITIONS[IOS-001] (see
|
|
265
|
+
// errors/IOS/IOS-001.md) — this CJS subtree can't import the ESM error
|
|
266
|
+
// registry (see native/buildErrorFormat.js), so the message is duplicated
|
|
267
|
+
// here rather than imported. This condition is recoverable (the simulator
|
|
268
|
+
// can still boot headlessly via simctl) so callers log it as a warning
|
|
269
|
+
// instead of throwing.
|
|
270
|
+
const SIMULATOR_APP_NOT_FOUND_WARNING = "[IOS-001] Could not find an installed iOS Simulator app (checked DeviceHub.app and the legacy Simulator.app). " + "The simulator is booted but its window may not be visible. Suggested action: confirm a full Xcode.app is installed and selected via xcode-select -p, then open it once from Spotlight.";
|
|
245
271
|
async function launchIOSSimulator(simulatorName) {
|
|
246
272
|
progress.log("Launching iOS Simulator...");
|
|
247
273
|
try {
|
|
@@ -285,15 +311,20 @@ async function launchIOSSimulator(simulatorName) {
|
|
|
285
311
|
console.log(`Simulator ${simulatorName} is already booted`);
|
|
286
312
|
}
|
|
287
313
|
|
|
288
|
-
// Open Simulator.app and focus
|
|
289
|
-
|
|
290
|
-
(
|
|
314
|
+
// Open the simulator UI (DeviceHub on Xcode 27+, Simulator.app on older Xcode) and focus it
|
|
315
|
+
const simulatorAppBundleId = resolveSimulatorAppBundleId();
|
|
316
|
+
if (simulatorAppBundleId) {
|
|
317
|
+
progress.log("Opening simulator UI...");
|
|
318
|
+
(0, _utils.runCommand)(`open -b ${simulatorAppBundleId}`);
|
|
291
319
|
|
|
292
|
-
|
|
293
|
-
|
|
320
|
+
// Give the simulator a moment to open/focus
|
|
321
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
294
322
|
|
|
295
|
-
|
|
296
|
-
|
|
323
|
+
// Activate the simulator UI window to bring it to front
|
|
324
|
+
(0, _utils.runCommand)(`osascript -e 'tell application id "${simulatorAppBundleId}" to activate'`);
|
|
325
|
+
} else {
|
|
326
|
+
progress.log(SIMULATOR_APP_NOT_FOUND_WARNING, "warning");
|
|
327
|
+
}
|
|
297
328
|
console.log("iOS Simulator launched successfully.");
|
|
298
329
|
} catch (error) {
|
|
299
330
|
console.error("Failed to launch iOS Simulator. Error:", error.message);
|
|
@@ -24,7 +24,26 @@ const loadEnvironmentVariables = async () => {
|
|
|
24
24
|
newConfig[key] =
|
|
25
25
|
typeof process.env[key] === "object" ? JSON.stringify(process.env[key]) : process.env[key]
|
|
26
26
|
}
|
|
27
|
-
process.env
|
|
27
|
+
// Merge config.json + filterKeys into the existing process.env instead of
|
|
28
|
+
// replacing it outright. start.js/serve.js forward the full parent
|
|
29
|
+
// process.env into this process (spawnSync(..., { env: { ...process.env, ... } }))
|
|
30
|
+
// specifically so arbitrary env vars (e.g. catalyst-ai's AI_CONFIG, or anything
|
|
31
|
+
// else a consumer/package expects to read from process.env) survive into the
|
|
32
|
+
// server process — a wholesale replace here silently discarded all of them
|
|
33
|
+
// except the small filterKeys allowlist, before any request handler got a
|
|
34
|
+
// chance to read them.
|
|
35
|
+
//
|
|
36
|
+
// Build the merge in a plain object and reassign process.env, rather than
|
|
37
|
+
// Object.assign(process.env, newConfig) directly: writing through the real
|
|
38
|
+
// process.env setter coerces every value to a string (process.env.X = true
|
|
39
|
+
// becomes the string "true"), which would break consumers that compare a
|
|
40
|
+
// config boolean strictly (e.g. expressServer.ts's
|
|
41
|
+
// `process.env.OTEL_ENABLE === true` — see its own comment noting it relies
|
|
42
|
+
// on process.env holding a real boolean here). Reassigning a plain object
|
|
43
|
+
// preserves the primitive types newConfig already carries, same as the
|
|
44
|
+
// original wholesale-replace behavior did for config.json values, while
|
|
45
|
+
// still keeping every pre-existing process.env entry via the spread.
|
|
46
|
+
process.env = { ...process.env, ...newConfig }
|
|
28
47
|
} catch (error) {
|
|
29
48
|
console.error("Error loading environment variables:", error)
|
|
30
49
|
throw error
|
package/mcp_v2/lib/helpers.js
CHANGED
|
@@ -60,10 +60,20 @@ function findCatalystRoot() {
|
|
|
60
60
|
let dir = start
|
|
61
61
|
while (dir !== path.parse(dir).root) {
|
|
62
62
|
const pkgPath = path.join(dir, "package.json")
|
|
63
|
+
// A package.json inside a node_modules segment is an installed copy, not
|
|
64
|
+
// the catalyst-core source repo itself — for the standard install layout
|
|
65
|
+
// (<app>/node_modules/catalyst-core/mcp_v2/mcp.js), walking up from
|
|
66
|
+
// __dirname hits catalyst-core's own installed package.json (name:
|
|
67
|
+
// "catalyst-core") before it ever reaches the consumer app's root. Only
|
|
68
|
+
// the isSourcePackage branch below should match on pkg.name, and only for
|
|
69
|
+
// a real (non-node_modules) source checkout — otherwise keep climbing so
|
|
70
|
+
// the walk finds the consumer's package.json instead, which matches via
|
|
71
|
+
// the deps["catalyst-core"] branch further down.
|
|
72
|
+
const isInstalledCopy = dir.split(path.sep).includes("node_modules")
|
|
63
73
|
if (fs.existsSync(pkgPath)) {
|
|
64
74
|
try {
|
|
65
75
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
|
66
|
-
if (pkg.name === "catalyst-core") {
|
|
76
|
+
if (pkg.name === "catalyst-core" && !isInstalledCopy) {
|
|
67
77
|
const sourceVersion = pkg.version || null
|
|
68
78
|
return {
|
|
69
79
|
dir,
|
|
@@ -86,19 +96,21 @@ function findCatalystRoot() {
|
|
|
86
96
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
87
97
|
const declaredRef = deps["catalyst-core"]
|
|
88
98
|
if (declaredRef) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
99
|
+
// A fixed `<dir>/node_modules/catalyst-core` path only finds a
|
|
100
|
+
// flat install. In a hoisted workspace (npm/yarn workspaces,
|
|
101
|
+
// pnpm with hoisting) the package can live several levels above
|
|
102
|
+
// `dir` instead. require.resolve with `paths` walks node_modules
|
|
103
|
+
// up the tree the same way Node's own module resolution does, so
|
|
104
|
+
// it finds the installed copy regardless of hoisting.
|
|
92
105
|
let installedVersion = null
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
106
|
+
let installed = false
|
|
107
|
+
try {
|
|
108
|
+
const nmPkgPath = require.resolve("catalyst-core/package.json", { paths: [dir] })
|
|
109
|
+
const nmPkg = JSON.parse(fs.readFileSync(nmPkgPath, "utf8"))
|
|
110
|
+
installedVersion = nmPkg.version || null
|
|
111
|
+
installed = true
|
|
112
|
+
} catch {
|
|
113
|
+
/* not installed relative to dir */
|
|
102
114
|
}
|
|
103
115
|
const isGithubRef = declaredRef.startsWith("github:") || declaredRef.includes("#")
|
|
104
116
|
return {
|