catalyst-core 1.0.2-canary.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/catalyst.js +105 -1
- package/changelog.md +34 -0
- package/dist/cli/README.md +119 -0
- package/dist/cli/appConfig.js +51 -0
- package/dist/cli/buildErrors.js +170 -0
- package/dist/cli/buildWarnings.js +28 -0
- package/dist/cli/devOutput.js +524 -0
- package/dist/cli/diagnostic.js +388 -0
- package/dist/cli/hints.js +69 -0
- package/dist/cli/package.json +3 -0
- package/dist/cli/spinner.js +141 -0
- package/dist/cli/theme.js +155 -0
- package/dist/errors/index.js +7 -3
- package/dist/logger.js +61 -6
- package/dist/native/androidProject/app/build.gradle.kts +2 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +2 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +9 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +65 -34
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt +109 -0
- package/dist/native/androidSetup.js +46 -57
- package/dist/native/buildAndroid/assets.js +22 -13
- package/dist/native/buildAndroid/build.js +98 -26
- package/dist/native/buildAndroid/config.js +43 -12
- package/dist/native/buildAndroid/index.js +49 -191
- package/dist/native/buildAppAndroid.js +19 -8
- package/dist/native/buildAppIos.js +35 -16
- package/dist/native/buildIos/assets.js +1 -6
- package/dist/native/buildIos/build.js +66 -95
- package/dist/native/buildIos/config.js +26 -8
- package/dist/native/buildIos/index.js +143 -64
- package/dist/native/buildIos/plugins.js +12 -15
- package/dist/native/internalPluginUtils.js +0 -1
- package/dist/native/iosnativeWebView/CoreLogic/Sources/CatalystCoreLogic/Constants/ConfigConstants.swift +2 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashHTMLView.swift +31 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +17 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +41 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +2 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +3 -0
- package/dist/native/pluginComposerAndroid.js +2 -3
- package/dist/native/renameAndroidProject.js +42 -20
- package/dist/native/setupEmulatorIos.js +28 -49
- package/dist/native/setupServer.js +12 -15
- package/dist/native/terminalProgress.js +186 -147
- package/dist/native/utils.js +340 -54
- package/dist/otel.js +15 -9
- package/dist/scripts/build.js +313 -42
- package/dist/scripts/generateOfflineManifest.js +11 -1
- package/dist/scripts/loadEnvironmentVariables.js +24 -2
- package/dist/scripts/preServerInit.js +32 -10
- package/dist/scripts/preflight.js +35 -11
- package/dist/scripts/scriptUtils.js +34 -0
- package/dist/scripts/serve.js +64 -15
- package/dist/scripts/start.js +58 -21
- package/dist/server/expressServer.js +167 -32
- package/dist/server/renderer/handler.jsx +87 -11
- package/dist/server/utils/validator.js +14 -11
- package/dist/vite/manifest-categorization-plugin.js +7 -5
- package/dist/vite/resolveDevServerConfig.js +9 -2
- package/dist/vite/vite.config.client.js +18 -0
- package/dist/vite/vite.config.js +73 -13
- package/dist/vite/vite.config.server.js +21 -0
- package/dist/web-router/components/MetaTag.jsx +15 -2
- package/dist/web-router/components/MetaTag.test.jsx +71 -2
- package/dist/webmcp/WebMcpProvider.jsx +3 -2
- package/dist/webmcp/useTool.js +4 -1
- package/mcp_v2/conversion-tasks.json +6 -5
- package/mcp_v2/knowledge-base.json +124 -7
- package/mcp_v2/test/config.test.ts +59 -0
- package/mcp_v2/tools/build.js +6 -6
- package/mcp_v2/tools/config.js +35 -18
- package/mcp_v2/tools/conversion.js +7 -13
- package/package.json +5 -5
- package/test/cli/buildErrors.test.ts +173 -0
- package/test/cli/buildWarnings.test.cjs +62 -0
- package/test/cli/devOutput.test.ts +549 -0
- package/test/cli/diagnostic.test.ts +434 -0
- package/test/cli/output.test.cjs +90 -0
- package/test/cli/spinner.test.ts +177 -0
- package/test/cli/theme.test.ts +163 -0
- package/test/native/redactSecrets.test.ts +72 -0
- package/test/native/utils.test.ts +532 -0
- package/test/scripts/build.test.ts +275 -0
- package/test/scripts/serve.test.ts +102 -0
- package/test/server/fixtures/template/src/js/routes/index.jsx +8 -0
- package/test/server/handler.test.ts +17 -0
- package/test/server/logger.test.ts +185 -0
- package/test/server/otel.test.ts +556 -0
- package/test/server/validator.test.ts +19 -8
- package/dist/scripts/checkVersion.js +0 -3
- package/dist/scripts/cleanCache.js +0 -30
package/dist/native/utils.js
CHANGED
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
exports.ANDROID_SDK_HINT = void 0;
|
|
6
7
|
exports.commandExists = commandExists;
|
|
8
|
+
exports.detectAndroidSdk = detectAndroidSdk;
|
|
9
|
+
exports.ensureLocalProperties = ensureLocalProperties;
|
|
10
|
+
exports.expandHome = expandHome;
|
|
11
|
+
exports.listAvds = listAvds;
|
|
7
12
|
exports.promptUser = promptUser;
|
|
13
|
+
exports.redactError = redactError;
|
|
14
|
+
exports.redactSecrets = redactSecrets;
|
|
8
15
|
exports.runCommand = runCommand;
|
|
9
16
|
exports.runInteractiveCommand = runInteractiveCommand;
|
|
10
17
|
exports.runSdkManagerCommand = runSdkManagerCommand;
|
|
@@ -12,19 +19,88 @@ exports.validateAndCompleteConfig = validateAndCompleteConfig;
|
|
|
12
19
|
var _child_process = require("child_process");
|
|
13
20
|
var _readline = _interopRequireDefault(require("readline"));
|
|
14
21
|
var _fs = _interopRequireDefault(require("fs"));
|
|
22
|
+
var _os = _interopRequireDefault(require("os"));
|
|
23
|
+
var _path = _interopRequireDefault(require("path"));
|
|
24
|
+
var _theme = _interopRequireDefault(require("../cli/theme.js"));
|
|
25
|
+
var _spinner = _interopRequireDefault(require("../cli/spinner.js"));
|
|
15
26
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
27
|
+
const {
|
|
28
|
+
GUTTER,
|
|
29
|
+
t
|
|
30
|
+
} = _theme.default;
|
|
31
|
+
const {
|
|
32
|
+
withSpinnersPaused
|
|
33
|
+
} = _spinner.default;
|
|
34
|
+
|
|
35
|
+
// Retain only the tail of a child's output: it is used for diagnostics, and a
|
|
36
|
+
// long gradle build would otherwise hold every byte it ever printed in memory.
|
|
37
|
+
const MAX_OUTPUT_BUFFER_BYTES = 1024 * 1024;
|
|
38
|
+
|
|
39
|
+
// Prompts are short, so only a small trailing window is ever worth matching.
|
|
40
|
+
// This is load-bearing, not belt-and-braces: handlePrompts only trims the
|
|
41
|
+
// buffer when something matches, and a normal build never prompts at all --
|
|
42
|
+
// so without the cap the match buffer accumulates the entire build output and
|
|
43
|
+
// indexOf rescans all of it on every chunk.
|
|
44
|
+
const MAX_PROMPT_MATCH_BYTES = 8 * 1024;
|
|
45
|
+
|
|
46
|
+
// Signing commands carry keystore passwords on the command line, and this is
|
|
47
|
+
// the one place every native command is echoed on failure. Redact here so a
|
|
48
|
+
// wrong password cannot put the real one in the terminal or in CI logs.
|
|
49
|
+
const SECRET_ARG_PATTERNS = [/(-storepass\s+)("[^"]*"|\S+)/gi, /(-keypass\s+)("[^"]*"|\S+)/gi, /(signing\.store\.password=)("[^"]*"|\S+)/gi, /(signing\.key\.password=)("[^"]*"|\S+)/gi, /(--password[=\s]+)("[^"]*"|\S+)/gi];
|
|
50
|
+
function redactSecrets(text) {
|
|
51
|
+
if (typeof text !== "string") return text;
|
|
52
|
+
return SECRET_ARG_PATTERNS.reduce((acc, pattern) => acc.replace(pattern, "$1***"), text);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Redacts by pattern, and also every secret value the command carries, in case a tool echoes one back without its flag. */
|
|
56
|
+
function secretScrubber(command) {
|
|
57
|
+
const secrets = SECRET_ARG_PATTERNS.flatMap(pattern => [...String(command).matchAll(pattern)].map(match => match[2].replace(/^"|"$/g, ""))).filter(Boolean);
|
|
58
|
+
return text => typeof text === "string" ? secrets.reduce((acc, secret) => acc.split(secret).join("***"), redactSecrets(text)) : text;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Scrub a failed command's error before anyone sees it. execSync puts the
|
|
63
|
+
* whole command line in error.message ("Command failed: ...") and the child's
|
|
64
|
+
* output in stdout/stderr, and callers rethrow error.message wrapped in their
|
|
65
|
+
* own text -- so the passwords are removed here, at the source.
|
|
66
|
+
*/
|
|
67
|
+
function redactError(error, command) {
|
|
68
|
+
if (!error || typeof error !== "object") return error;
|
|
69
|
+
const scrub = secretScrubber(command);
|
|
70
|
+
for (const key of ["message", "stack", "cmd", "stdout", "stderr"]) {
|
|
71
|
+
if (typeof error[key] === "string") error[key] = scrub(error[key]);
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(error.output)) error.output = error.output.map(scrub);
|
|
74
|
+
return error;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} command
|
|
79
|
+
* @param {object} [options]
|
|
80
|
+
* @param {number} [options.timeout] - ms before the command is killed. Device
|
|
81
|
+
* probes (adb, devicectl) take one: a wedged adb server otherwise hangs the
|
|
82
|
+
* build forever behind a spinner that keeps saying it is working.
|
|
83
|
+
*/
|
|
84
|
+
function runCommand(command, {
|
|
85
|
+
timeout
|
|
86
|
+
} = {}) {
|
|
87
|
+
// Redacted before it reaches the log file too -- the log is a build
|
|
88
|
+
// artifact that gets attached to bug reports and uploaded by CI.
|
|
21
89
|
try {
|
|
90
|
+
// stderr is captured, not inherited. Many tools (java -version, for
|
|
91
|
+
// one) write to stderr on success, and an inherited fd puts that text
|
|
92
|
+
// at column 0 -- straight through whatever the spinner is drawing.
|
|
93
|
+
// The caller decides what, if anything, is worth showing.
|
|
22
94
|
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Native setup/build uses fixed internal command strings; preserve existing shell behavior.
|
|
23
95
|
return (0, _child_process.execSync)(command, {
|
|
24
|
-
encoding: "utf8"
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
98
|
+
timeout
|
|
25
99
|
});
|
|
26
100
|
} catch (error) {
|
|
27
|
-
|
|
101
|
+
if (timeout && isTimeout(error)) throw toolTimeoutError(command, timeout);
|
|
102
|
+
redactError(error, command);
|
|
103
|
+
console.error(`Error executing command: ${secretScrubber(command)(command)}`);
|
|
28
104
|
console.error(`Error message: ${error.message}`);
|
|
29
105
|
throw error;
|
|
30
106
|
}
|
|
@@ -40,49 +116,241 @@ function commandExists(command) {
|
|
|
40
116
|
return false;
|
|
41
117
|
}
|
|
42
118
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
119
|
+
|
|
120
|
+
/** execSync reports a timeout as a killed child, not with a code of its own. */
|
|
121
|
+
function isTimeout(error) {
|
|
122
|
+
return error?.code === "ETIMEDOUT" || error?.signal === "SIGTERM" && error?.status === null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A device tool that stopped answering. adb in particular wedges: its server
|
|
127
|
+
* keeps the port open and accepts connections but never replies, so every
|
|
128
|
+
* later call hangs too. Name the tool and the reset that clears it.
|
|
129
|
+
*/
|
|
130
|
+
function toolTimeoutError(command, timeout) {
|
|
131
|
+
const tool = _path.default.basename(String(command).trim().split(/\s+/)[0].replace(/^"|"$/g, ""));
|
|
132
|
+
const seconds = Math.round(timeout / 1000);
|
|
133
|
+
const error = new Error(`${tool} did not respond within ${seconds}s`);
|
|
134
|
+
error.code = "ETOOLTIMEOUT";
|
|
135
|
+
error.hint = tool === "adb" ? "The adb server is stuck. Restart it with `adb kill-server && adb start-server`, then retry." : tool === "xcrun" ? "Xcode's device tools are not responding. Quit Xcode and Simulator, then retry." : `Check that ${tool} works on its own, then retry.`;
|
|
136
|
+
return error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** A required value with no terminal to ask for it on, e.g. in CI. */
|
|
140
|
+
function missingConfigError(question) {
|
|
141
|
+
const label = question.replace(/[:\s]+$/, "");
|
|
142
|
+
const error = new Error(`Missing configuration, and no terminal to ask on: ${label}`);
|
|
143
|
+
error.code = "ENOTTY";
|
|
144
|
+
error.hint = "Set it in config/config.json (WEBVIEW_CONFIG), or run the command in an interactive terminal.";
|
|
145
|
+
return error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Ask a question on the terminal.
|
|
150
|
+
*
|
|
151
|
+
* Every live spinner is stood down first: the native steps run under one, and
|
|
152
|
+
* its next frame used to erase the question, so a missing config field looked
|
|
153
|
+
* like a hang that never ended. The interface is created per question and
|
|
154
|
+
* closed after it -- one held open at module load kept stdin referenced, so
|
|
155
|
+
* every command that imported this file had to process.exit() to finish.
|
|
156
|
+
*/
|
|
157
|
+
async function promptUser(question, {
|
|
158
|
+
defaultValue
|
|
159
|
+
} = {}) {
|
|
160
|
+
if (!process.stdin.isTTY) {
|
|
161
|
+
if (defaultValue !== undefined) return defaultValue;
|
|
162
|
+
throw missingConfigError(question);
|
|
163
|
+
}
|
|
164
|
+
return withSpinnersPaused(() => new Promise(resolve => {
|
|
165
|
+
const rl = _readline.default.createInterface({
|
|
166
|
+
input: process.stdin,
|
|
167
|
+
output: process.stdout
|
|
168
|
+
});
|
|
169
|
+
const label = question.replace(/[:\s]+$/, "");
|
|
170
|
+
const suffix = defaultValue ? t.dim(` (${defaultValue})`) : "";
|
|
171
|
+
rl.question(`${GUTTER}${t.warn("?")} ${label}${suffix} `, answer => {
|
|
172
|
+
rl.close();
|
|
173
|
+
const value = answer.trim() || defaultValue || "";
|
|
174
|
+
// Settle the question into a record of what was chosen,
|
|
175
|
+
// in place: one line up, cleared, rewritten.
|
|
176
|
+
const esc = String.fromCharCode(27);
|
|
177
|
+
process.stdout.write(`${esc}[1A\r${esc}[2K${GUTTER}${t.ok("✓")} ${label} ${t.dim(value)}\n`);
|
|
178
|
+
resolve(value);
|
|
179
|
+
});
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Where the Android SDK most likely lives: the environment first, then the
|
|
185
|
+
* locations Android Studio installs to. Returns null when none has adb, so a
|
|
186
|
+
* stray empty directory is not mistaken for an SDK.
|
|
187
|
+
*/
|
|
188
|
+
function detectAndroidSdk(env = process.env) {
|
|
189
|
+
const home = _os.default.homedir();
|
|
190
|
+
const candidates = [env.ANDROID_HOME, env.ANDROID_SDK_ROOT, _path.default.join(home, "Library", "Android", "sdk"), _path.default.join(home, "Android", "Sdk"),
|
|
191
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Paths come from env vars, the user's own SDK location or the project dir, never remote input.
|
|
192
|
+
env.LOCALAPPDATA && _path.default.join(env.LOCALAPPDATA, "Android", "Sdk")].filter(Boolean);
|
|
193
|
+
const adb = process.platform === "win32" ? "adb.exe" : "adb";
|
|
194
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Paths come from env vars, the user's own SDK location or the project dir, never remote input.
|
|
195
|
+
return candidates.find(dir => _fs.default.existsSync(_path.default.join(dir, "platform-tools", adb))) || null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** AVD names known to the SDK's emulator, or [] when it cannot be asked. */
|
|
199
|
+
function listAvds(sdkPath) {
|
|
200
|
+
if (!sdkPath) return [];
|
|
201
|
+
try {
|
|
202
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process, javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - sdkPath is a local SDK directory from config or auto-detection, never remote input.
|
|
203
|
+
const out = (0, _child_process.execSync)(`"${_path.default.join(expandHome(sdkPath), "emulator", "emulator")}" -list-avds`, {
|
|
204
|
+
encoding: "utf8",
|
|
205
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
206
|
+
timeout: 15000
|
|
47
207
|
});
|
|
48
|
-
|
|
208
|
+
return out.split("\n").map(line => line.trim()).filter(line => line && !line.startsWith("INFO"));
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Expand a leading `~` (as in the SDK hint below); nothing else in the chain does. */
|
|
215
|
+
function expandHome(dir) {
|
|
216
|
+
if (typeof dir !== "string") return dir;
|
|
217
|
+
if (dir === "~") return _os.default.homedir();
|
|
218
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Paths come from env vars, the user's own SDK location or the project dir, never remote input.
|
|
219
|
+
return dir.startsWith("~/") || dir.startsWith("~\\") ? _path.default.join(_os.default.homedir(), dir.slice(2)) : dir;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The hint for a missing or wrong SDK path, naming the one field to set. */
|
|
223
|
+
const ANDROID_SDK_HINT = exports.ANDROID_SDK_HINT = 'Install Android Studio, or set WEBVIEW_CONFIG.android.sdkPath in config/config.json (e.g. "~/Library/Android/sdk").';
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Write the SDK location gradle reads. `setupEmulator:android` used to be the
|
|
227
|
+
* only thing that wrote it, so a `buildApp:android` without a prior setup died
|
|
228
|
+
* in gradle with "SDK location not found" despite a configured sdkPath.
|
|
229
|
+
*/
|
|
230
|
+
function ensureLocalProperties(projectDir, sdkPath) {
|
|
231
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Paths come from env vars, the user's own SDK location or the project dir, never remote input.
|
|
232
|
+
const file = _path.default.join(projectDir, "local.properties");
|
|
233
|
+
const line = `sdk.dir=${expandHome(sdkPath).replace(/\\/g, "\\\\")}`;
|
|
234
|
+
let content = "";
|
|
235
|
+
try {
|
|
236
|
+
content = _fs.default.readFileSync(file, "utf8");
|
|
237
|
+
} catch (error) {
|
|
238
|
+
// Missing is the case being fixed.
|
|
239
|
+
}
|
|
240
|
+
if (content.split("\n").some(existing => existing.trim() === line)) return false;
|
|
241
|
+
const kept = content.split("\n").filter(existing => existing.trim() && !existing.startsWith("sdk.dir="));
|
|
242
|
+
_fs.default.writeFileSync(file, [...kept, line].join("\n") + "\n");
|
|
243
|
+
return true;
|
|
49
244
|
}
|
|
50
|
-
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Run a command, keeping its output rather than printing it.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} command
|
|
250
|
+
* @param {string[]} args
|
|
251
|
+
* @param {object} [promptResponses] - text to answer prompts with
|
|
252
|
+
* @param {object} [options]
|
|
253
|
+
* @param {(line: string) => void} [options.onLine] - called per output line.
|
|
254
|
+
* Without it the output is captured silently. Gradle and xcodebuild emit
|
|
255
|
+
* hundreds of lines that nobody reads on success and that are redundant on
|
|
256
|
+
* failure -- the error path mines `error.output`, which is retained either
|
|
257
|
+
* way. Callers that want progress pass a handler that summarises, rather
|
|
258
|
+
* than letting the raw stream scroll.
|
|
259
|
+
*/
|
|
260
|
+
async function runInteractiveCommand(command, args, promptResponses = {}, {
|
|
261
|
+
onLine
|
|
262
|
+
} = {}) {
|
|
51
263
|
return new Promise((resolve, reject) => {
|
|
264
|
+
// Named `child`, not `process`: the previous name shadowed the global
|
|
265
|
+
// `process` for the whole function body.
|
|
52
266
|
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Native build commands are fixed internal commands passed with argv arrays.
|
|
53
|
-
const
|
|
267
|
+
const child = (0, _child_process.spawn)(command, args, {
|
|
54
268
|
stdio: ["pipe", "pipe", "pipe"]
|
|
55
269
|
});
|
|
56
|
-
let
|
|
270
|
+
let promptBuffer = "";
|
|
57
271
|
let outputBuffer = "";
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
272
|
+
const partial = {
|
|
273
|
+
stdout: "",
|
|
274
|
+
stderr: ""
|
|
275
|
+
};
|
|
276
|
+
const consume = (data, stream) => {
|
|
277
|
+
const text = data.toString();
|
|
278
|
+
|
|
279
|
+
// Hold back a trailing partial line until its newline arrives.
|
|
280
|
+
// Printing raw chunks splits lines mid-write and injects blanks.
|
|
281
|
+
const lines = (partial[stream] + text).split("\n");
|
|
282
|
+
partial[stream] = lines.pop();
|
|
283
|
+
if (onLine) {
|
|
284
|
+
for (const line of lines) onLine(line);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Previously unbounded: a long gradle build retained every byte.
|
|
288
|
+
outputBuffer += text;
|
|
289
|
+
if (outputBuffer.length > MAX_OUTPUT_BUFFER_BYTES) {
|
|
290
|
+
outputBuffer = outputBuffer.slice(-MAX_OUTPUT_BUFFER_BYTES);
|
|
291
|
+
}
|
|
292
|
+
promptBuffer = handlePrompts(child, promptBuffer + text, promptResponses);
|
|
293
|
+
if (promptBuffer.length > MAX_PROMPT_MATCH_BYTES) {
|
|
294
|
+
promptBuffer = promptBuffer.slice(-MAX_PROMPT_MATCH_BYTES);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
child.stdout.on("data", data => consume(data, "stdout"));
|
|
298
|
+
child.stderr.on("data", data => consume(data, "stderr"));
|
|
299
|
+
|
|
300
|
+
// Without this the promise never settles when the binary is missing or
|
|
301
|
+
// not executable, so a missing gradle/adb hangs the CLI indefinitely.
|
|
302
|
+
child.on("error", error => {
|
|
303
|
+
if (error.code === "ENOENT") {
|
|
304
|
+
reject(new Error(`Command not found: ${command}`, {
|
|
305
|
+
cause: error
|
|
306
|
+
}));
|
|
307
|
+
} else {
|
|
308
|
+
reject(new Error(`Failed to start ${command}: ${error.message}`, {
|
|
309
|
+
cause: error
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
69
312
|
});
|
|
70
|
-
|
|
313
|
+
child.on("close", code => {
|
|
314
|
+
// Flush whatever never got its closing newline.
|
|
315
|
+
for (const stream of ["stdout", "stderr"]) {
|
|
316
|
+
if (partial[stream]) {
|
|
317
|
+
if (onLine) onLine(partial[stream]);
|
|
318
|
+
partial[stream] = "";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
71
321
|
if (code === 0) {
|
|
72
322
|
resolve(outputBuffer);
|
|
73
|
-
|
|
74
|
-
reject(new Error(`Command failed with exit code ${code}`));
|
|
323
|
+
return;
|
|
75
324
|
}
|
|
325
|
+
|
|
326
|
+
// Carry the output on the error. Without it the caller only has
|
|
327
|
+
// "exit code 1" and the compiler error that actually broke the
|
|
328
|
+
// build is lost -- which is why failures used to mean opening
|
|
329
|
+
// Android Studio to find out what happened.
|
|
330
|
+
const failure = new Error(`Command failed with exit code ${code}`);
|
|
331
|
+
failure.code = code;
|
|
332
|
+
failure.output = outputBuffer;
|
|
333
|
+
reject(failure);
|
|
76
334
|
});
|
|
77
335
|
});
|
|
78
336
|
}
|
|
79
|
-
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Answer a prompt found in the accumulated output.
|
|
340
|
+
*
|
|
341
|
+
* Returns the buffer to keep looking in. The match must be consumed: the buffer
|
|
342
|
+
* grows with every chunk, so leaving a matched prompt in it re-answers that
|
|
343
|
+
* prompt on every subsequent chunk, spraying `y\n` into the child's stdin.
|
|
344
|
+
*/
|
|
345
|
+
function handlePrompts(child, buffer, promptResponses) {
|
|
80
346
|
for (const [prompt, response] of Object.entries(promptResponses)) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
347
|
+
const index = buffer.indexOf(prompt);
|
|
348
|
+
if (index !== -1) {
|
|
349
|
+
child.stdin.write(response + "\n");
|
|
350
|
+
return buffer.slice(index + prompt.length);
|
|
84
351
|
}
|
|
85
352
|
}
|
|
353
|
+
return buffer;
|
|
86
354
|
}
|
|
87
355
|
async function runSdkManagerCommand(sdkManagerPath, args) {
|
|
88
356
|
const promptResponses = {
|
|
@@ -110,22 +378,35 @@ async function validateAndCompleteConfig(platform, configPath) {
|
|
|
110
378
|
// Check if WEBVIEW_CONFIG already has all required fields
|
|
111
379
|
const webviewConfig = config.WEBVIEW_CONFIG;
|
|
112
380
|
const commonFields = {
|
|
113
|
-
port: "
|
|
381
|
+
port: "Port the app is served on"
|
|
114
382
|
};
|
|
383
|
+
|
|
384
|
+
// cachePattern is optional -- the iOS build emits an empty pattern list
|
|
385
|
+
// when it is absent and Android never required it -- so asking for it on
|
|
386
|
+
// every first setup only stalled people on a question they could not answer.
|
|
115
387
|
const platformConfigs = {
|
|
116
388
|
android: {
|
|
117
|
-
buildType: "
|
|
118
|
-
sdkPath: "
|
|
119
|
-
emulatorName: "
|
|
120
|
-
cachePattern: "Enter Cache pattern (e.g., *.css): "
|
|
389
|
+
buildType: "Android build type (debug/release)",
|
|
390
|
+
sdkPath: "Android SDK path",
|
|
391
|
+
emulatorName: "Android emulator (AVD) name"
|
|
121
392
|
},
|
|
122
393
|
ios: {
|
|
123
|
-
buildType: "
|
|
124
|
-
appBundleId: "
|
|
125
|
-
simulatorName: "
|
|
126
|
-
cachePattern: "Enter Cache pattern (e.g., *.css): "
|
|
394
|
+
buildType: "iOS build type (debug/release)",
|
|
395
|
+
appBundleId: "iOS bundle ID (e.g. com.example.app)",
|
|
396
|
+
simulatorName: "iOS simulator name (e.g. iPhone 16 Pro)"
|
|
127
397
|
}
|
|
128
398
|
};
|
|
399
|
+
|
|
400
|
+
// Answers we can work out are filled in rather than asked; the rest are
|
|
401
|
+
// asked with the best guess offered as the default.
|
|
402
|
+
const sdkGuess = platform === "android" ? webviewConfig.android?.sdkPath || detectAndroidSdk() : null;
|
|
403
|
+
const defaults = {
|
|
404
|
+
port: config.NODE_SERVER_PORT ? String(config.NODE_SERVER_PORT) : undefined,
|
|
405
|
+
buildType: "debug",
|
|
406
|
+
sdkPath: sdkGuess || undefined,
|
|
407
|
+
emulatorName: platform === "android" ? listAvds(sdkGuess)[0] : undefined
|
|
408
|
+
};
|
|
409
|
+
const filled = [];
|
|
129
410
|
if (!platformConfigs[platform]) {
|
|
130
411
|
throw new Error('Invalid platform. Must be either "android" or "ios"');
|
|
131
412
|
}
|
|
@@ -162,24 +443,29 @@ async function validateAndCompleteConfig(platform, configPath) {
|
|
|
162
443
|
};
|
|
163
444
|
}
|
|
164
445
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
446
|
+
// Fields with a sure answer (detected or conventional) are filled without
|
|
447
|
+
// asking; only genuinely unknown ones reach the prompt.
|
|
448
|
+
const autoFill = new Set(["port", "buildType", "sdkPath"]);
|
|
449
|
+
const fill = async (target, key, prompt) => {
|
|
450
|
+
if (target[key]) return;
|
|
451
|
+
const guess = defaults[key];
|
|
452
|
+
target[key] = guess && autoFill.has(key) ? guess : await promptUser(prompt, {
|
|
453
|
+
defaultValue: guess
|
|
454
|
+
});
|
|
455
|
+
filled.push(key);
|
|
456
|
+
};
|
|
457
|
+
for (const [key, prompt] of Object.entries(commonFields)) await fill(webviewConfig, key, prompt);
|
|
173
458
|
for (const [key, prompt] of Object.entries(platformConfigs[platform])) {
|
|
174
|
-
|
|
175
|
-
webviewConfig[platform][key] = await promptUser(prompt);
|
|
176
|
-
}
|
|
459
|
+
await fill(webviewConfig[platform], key, prompt);
|
|
177
460
|
}
|
|
178
461
|
|
|
179
462
|
// Save updated config back to file
|
|
180
463
|
try {
|
|
181
464
|
_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
182
|
-
|
|
465
|
+
if (filled.length) {
|
|
466
|
+
// Under a live spinner a plain log lands on the spinner's own row.
|
|
467
|
+
await withSpinnersPaused(async () => console.log(`${GUTTER}${t.accent("›")} Saved ${filled.join(", ")} to ${t.accent("config/config.json")}`));
|
|
468
|
+
}
|
|
183
469
|
} catch (error) {
|
|
184
470
|
console.error(`Error saving config file: ${error.message}`);
|
|
185
471
|
throw error;
|
package/dist/otel.js
CHANGED
|
@@ -16,6 +16,12 @@ export const IS_BOT_KEY = createContextKey("catalyst.is_bot")
|
|
|
16
16
|
import semanticConventions from "@opentelemetry/semantic-conventions"
|
|
17
17
|
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, ATTR_DEPLOYMENT_ENVIRONMENT } = semanticConventions
|
|
18
18
|
|
|
19
|
+
// The app-wide logger only exists once the app calls configureLogger() from
|
|
20
|
+
// catalyst-core/logger, which nothing in core does for it. Resolve it per call,
|
|
21
|
+
// so a logger configured after this module loads is still picked up, and fall
|
|
22
|
+
// back to console so init() never throws a ReferenceError on a bare `logger`.
|
|
23
|
+
const getLogger = () => global.logger ?? console
|
|
24
|
+
|
|
19
25
|
function init(config = {}) {
|
|
20
26
|
// OpenTelemetry is opt-in — bail out unless explicitly enabled. Mirrors the
|
|
21
27
|
// OTEL_ENABLE guard in server/expressServer.js and server/renderer/handler.jsx
|
|
@@ -84,7 +90,7 @@ function init(config = {}) {
|
|
|
84
90
|
const sdk = new NodeSDK(sdkConfig)
|
|
85
91
|
|
|
86
92
|
sdk.start()
|
|
87
|
-
|
|
93
|
+
getLogger().info("✅ OpenTelemetry started successfully")
|
|
88
94
|
|
|
89
95
|
// Initialize custom metrics only if metrics are enabled
|
|
90
96
|
let meter = null
|
|
@@ -93,11 +99,11 @@ function init(config = {}) {
|
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
const gracefulShutdown = (signal) => {
|
|
96
|
-
|
|
102
|
+
getLogger().info(`📡 Received ${signal}, shutting down OpenTelemetry gracefully...`)
|
|
97
103
|
sdk.shutdown()
|
|
98
|
-
.then(() =>
|
|
104
|
+
.then(() => getLogger().info("✅ OpenTelemetry shutdown completed"))
|
|
99
105
|
.catch((error) => {
|
|
100
|
-
|
|
106
|
+
getLogger().error("❌ Error terminating OpenTelemetry:", error)
|
|
101
107
|
})
|
|
102
108
|
.finally(() => process.exit())
|
|
103
109
|
}
|
|
@@ -107,7 +113,7 @@ function init(config = {}) {
|
|
|
107
113
|
|
|
108
114
|
return { sdk, meter }
|
|
109
115
|
} catch (error) {
|
|
110
|
-
|
|
116
|
+
getLogger().error("❌ Failed to initialize OpenTelemetry:", error)
|
|
111
117
|
throw error
|
|
112
118
|
}
|
|
113
119
|
}
|
|
@@ -122,13 +128,13 @@ function init(config = {}) {
|
|
|
122
128
|
*/
|
|
123
129
|
function createTraceExporter(protocol, url, headers = {}, grpcCredentials) {
|
|
124
130
|
if (protocol.toLowerCase() === "http") {
|
|
125
|
-
|
|
131
|
+
getLogger().info(`📡 Creating HTTP trace exporter for URL: ${url}`)
|
|
126
132
|
return new OTLPTraceExporterHTTP({
|
|
127
133
|
url: url,
|
|
128
134
|
headers: headers,
|
|
129
135
|
})
|
|
130
136
|
} else if (protocol.toLowerCase() === "grpc") {
|
|
131
|
-
|
|
137
|
+
getLogger().info(`📡 Creating gRPC trace exporter for URL: ${url}`)
|
|
132
138
|
return new OTLPTraceExporter({
|
|
133
139
|
url: url,
|
|
134
140
|
headers: headers,
|
|
@@ -151,13 +157,13 @@ function createTraceExporter(protocol, url, headers = {}, grpcCredentials) {
|
|
|
151
157
|
*/
|
|
152
158
|
function createMetricExporter(protocol, url, headers = {}, grpcCredentials) {
|
|
153
159
|
if (protocol.toLowerCase() === "http") {
|
|
154
|
-
|
|
160
|
+
getLogger().info(`📊 Creating HTTP metric exporter for URL: ${url}`)
|
|
155
161
|
return new OTLPMetricExporterHTTP({
|
|
156
162
|
url: url,
|
|
157
163
|
headers: headers,
|
|
158
164
|
})
|
|
159
165
|
} else if (protocol.toLowerCase() === "grpc") {
|
|
160
|
-
|
|
166
|
+
getLogger().info(`📊 Creating gRPC metric exporter for URL: ${url}`)
|
|
161
167
|
return new OTLPMetricExporter({
|
|
162
168
|
url: url,
|
|
163
169
|
headers: headers,
|