@appilots/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4178 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli/index.ts
27
+ var import_commander7 = require("commander");
28
+
29
+ // src/config/index.ts
30
+ var import_fs = require("fs");
31
+ var import_path = require("path");
32
+ var import_fs2 = require("fs");
33
+ function getEnvOverrides(env = process.env) {
34
+ const clean = (value) => {
35
+ const trimmed = value?.trim();
36
+ return trimmed ? trimmed : void 0;
37
+ };
38
+ return {
39
+ apiKey: clean(env.APPILOTS_API_KEY),
40
+ projectId: clean(env.APPILOTS_PROJECT_ID),
41
+ serverUrl: clean(env.APPILOTS_SERVER_URL)
42
+ };
43
+ }
44
+ function loadConfig() {
45
+ const configPath = getConfigPath();
46
+ const env = getEnvOverrides();
47
+ let fileConfig = {};
48
+ if (configPath) {
49
+ try {
50
+ fileConfig = JSON.parse((0, import_fs.readFileSync)(configPath, "utf-8"));
51
+ } catch (error2) {
52
+ throw new Error(`Failed to parse .appilotsrc at ${configPath}: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
53
+ }
54
+ } else if (!env.apiKey) {
55
+ return null;
56
+ }
57
+ const merged = {
58
+ serverUrl: "http://localhost:4000",
59
+ outputDir: ".appilots",
60
+ autoActivate: true,
61
+ strictScreens: true,
62
+ ...fileConfig,
63
+ ...env.apiKey ? { apiKey: env.apiKey } : {},
64
+ ...env.projectId ? { projectId: env.projectId } : {},
65
+ ...env.serverUrl ? { serverUrl: env.serverUrl } : {}
66
+ };
67
+ const validation = validateConfig(merged);
68
+ if (!validation.valid) {
69
+ const source = configPath ?? "environment variables";
70
+ throw new Error(
71
+ `Invalid Appilots configuration (${source}):
72
+ ` + validation.errors.map((e) => ` - ${e}`).join("\n") + "\nFix .appilotsrc or set APPILOTS_API_KEY / APPILOTS_SERVER_URL."
73
+ );
74
+ }
75
+ return merged;
76
+ }
77
+ function saveConfig(dir, config) {
78
+ const configPath = (0, import_path.join)(dir, ".appilotsrc");
79
+ const existingConfig = getConfigPath() ? loadConfig() : null;
80
+ const mergedConfig = {
81
+ apiKey: config.apiKey || existingConfig?.apiKey || "",
82
+ projectId: config.projectId || existingConfig?.projectId || "",
83
+ serverUrl: config.serverUrl || existingConfig?.serverUrl || "http://localhost:4000",
84
+ outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
85
+ autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
86
+ include: config.include || existingConfig?.include,
87
+ exclude: config.exclude || existingConfig?.exclude,
88
+ strictScreens: config.strictScreens ?? existingConfig?.strictScreens ?? true,
89
+ screenPatterns: config.screenPatterns || existingConfig?.screenPatterns,
90
+ navigationInclude: config.navigationInclude || existingConfig?.navigationInclude,
91
+ navigationExclude: config.navigationExclude || existingConfig?.navigationExclude,
92
+ eval: config.eval || existingConfig?.eval
93
+ };
94
+ try {
95
+ (0, import_fs.writeFileSync)(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
96
+ } catch (error2) {
97
+ throw new Error(`Failed to write .appilotsrc to ${configPath}: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
98
+ }
99
+ }
100
+ function getConfigPath() {
101
+ let currentDir = (0, import_path.resolve)(process.cwd());
102
+ const root = (0, import_path.resolve)("/");
103
+ while (currentDir !== root) {
104
+ const configPath = (0, import_path.join)(currentDir, ".appilotsrc");
105
+ try {
106
+ if ((0, import_fs2.existsSync)(configPath) && (0, import_fs2.statSync)(configPath).isFile()) {
107
+ return configPath;
108
+ }
109
+ } catch {
110
+ }
111
+ currentDir = (0, import_path.resolve)(currentDir, "..");
112
+ }
113
+ const rootConfigPath = (0, import_path.join)(root, ".appilotsrc");
114
+ try {
115
+ if ((0, import_fs2.existsSync)(rootConfigPath) && (0, import_fs2.statSync)(rootConfigPath).isFile()) {
116
+ return rootConfigPath;
117
+ }
118
+ } catch {
119
+ }
120
+ return null;
121
+ }
122
+ function validateConfig(config) {
123
+ const errors = [];
124
+ if (!config || typeof config !== "object") {
125
+ return {
126
+ valid: false,
127
+ errors: ["Configuration must be an object"]
128
+ };
129
+ }
130
+ if (!config.apiKey || typeof config.apiKey !== "string") {
131
+ errors.push("apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)");
132
+ } else if (!config.apiKey.startsWith("ak_")) {
133
+ errors.push('apiKey must start with "ak_"');
134
+ }
135
+ if (config.projectId !== void 0 && config.projectId !== "" && typeof config.projectId !== "string") {
136
+ errors.push("projectId must be a string");
137
+ }
138
+ if (config.serverUrl !== void 0 && typeof config.serverUrl !== "string") {
139
+ errors.push("serverUrl must be a string");
140
+ }
141
+ if (config.outputDir !== void 0 && typeof config.outputDir !== "string") {
142
+ errors.push("outputDir must be a string");
143
+ }
144
+ if (config.include !== void 0 && !Array.isArray(config.include)) {
145
+ errors.push("include must be an array of strings");
146
+ } else if (Array.isArray(config.include) && !config.include.every((item) => typeof item === "string")) {
147
+ errors.push("include must be an array of strings");
148
+ }
149
+ if (config.exclude !== void 0 && !Array.isArray(config.exclude)) {
150
+ errors.push("exclude must be an array of strings");
151
+ } else if (Array.isArray(config.exclude) && !config.exclude.every((item) => typeof item === "string")) {
152
+ errors.push("exclude must be an array of strings");
153
+ }
154
+ if (config.autoActivate !== void 0 && typeof config.autoActivate !== "boolean") {
155
+ errors.push("autoActivate must be a boolean");
156
+ }
157
+ if (config.strictScreens !== void 0 && typeof config.strictScreens !== "boolean") {
158
+ errors.push("strictScreens must be a boolean");
159
+ }
160
+ for (const field of ["screenPatterns", "navigationInclude", "navigationExclude"]) {
161
+ const value = config[field];
162
+ if (value !== void 0 && (!Array.isArray(value) || !value.every((item) => typeof item === "string"))) {
163
+ errors.push(`${field} must be an array of strings`);
164
+ }
165
+ }
166
+ if (config.eval !== void 0) {
167
+ if (typeof config.eval !== "object" || config.eval === null || Array.isArray(config.eval)) {
168
+ errors.push("eval must be an object");
169
+ } else {
170
+ const evalConfig = config.eval;
171
+ if (evalConfig.scenariosDir !== void 0 && typeof evalConfig.scenariosDir !== "string") {
172
+ errors.push("eval.scenariosDir must be a string");
173
+ }
174
+ if (evalConfig.baselinePath !== void 0 && typeof evalConfig.baselinePath !== "string") {
175
+ errors.push("eval.baselinePath must be a string");
176
+ }
177
+ if (evalConfig.minPassRate !== void 0 && (typeof evalConfig.minPassRate !== "number" || evalConfig.minPassRate < 0 || evalConfig.minPassRate > 1)) {
178
+ errors.push("eval.minPassRate must be a number between 0 and 1");
179
+ }
180
+ if (evalConfig.maxTokenRegression !== void 0 && (typeof evalConfig.maxTokenRegression !== "number" || evalConfig.maxTokenRegression < 0)) {
181
+ errors.push("eval.maxTokenRegression must be a non-negative number");
182
+ }
183
+ }
184
+ }
185
+ return {
186
+ valid: errors.length === 0,
187
+ errors
188
+ };
189
+ }
190
+
191
+ // src/cli/utils/logger.ts
192
+ var import_chalk = __toESM(require("chalk"));
193
+ var import_ora = __toESM(require("ora"));
194
+ var mode = "pretty";
195
+ function setLogMode(next) {
196
+ mode = next;
197
+ }
198
+ function jsonOutput(payload) {
199
+ if (mode === "json") {
200
+ console.log(JSON.stringify(payload, null, 2));
201
+ }
202
+ }
203
+ function createSpinner(text) {
204
+ return (0, import_ora.default)({ text, isSilent: mode !== "pretty" });
205
+ }
206
+ function success(msg) {
207
+ if (mode !== "pretty") return;
208
+ console.log(import_chalk.default.green.bold("\u2713"), msg);
209
+ }
210
+ function error(msg) {
211
+ if (mode === "json") {
212
+ console.error(import_chalk.default.red.bold("\u2717"), msg);
213
+ return;
214
+ }
215
+ console.log(import_chalk.default.red.bold("\u2717"), msg);
216
+ }
217
+ function warn(msg) {
218
+ if (mode === "json") {
219
+ console.error(import_chalk.default.yellow.bold("\u26A0"), msg);
220
+ return;
221
+ }
222
+ console.log(import_chalk.default.yellow.bold("\u26A0"), msg);
223
+ }
224
+ function info(msg) {
225
+ if (mode !== "pretty") return;
226
+ console.log(import_chalk.default.blue.bold("\u2139"), msg);
227
+ }
228
+ function dim(msg) {
229
+ if (mode !== "pretty") return;
230
+ console.log(import_chalk.default.dim(msg));
231
+ }
232
+ function banner() {
233
+ if (mode !== "pretty") return;
234
+ const logo = `
235
+ ${import_chalk.default.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")}
236
+ ${import_chalk.default.cyan("\u2551")} ${import_chalk.default.bold.cyan("Appilots MCP Generator")} ${import_chalk.default.cyan("\u2551")}
237
+ ${import_chalk.default.cyan("\u2551")} ${import_chalk.default.dim("v0.1.0")} ${import_chalk.default.cyan("\u2551")}
238
+ ${import_chalk.default.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}
239
+ `;
240
+ console.log(logo);
241
+ }
242
+ function heading(msg) {
243
+ if (mode !== "pretty") return;
244
+ console.log(msg);
245
+ }
246
+ function table(rows) {
247
+ if (mode !== "pretty" || rows.length === 0) {
248
+ return;
249
+ }
250
+ const maxKeyLength = Math.max(...rows.map(([key]) => key.length));
251
+ rows.forEach(([key, value]) => {
252
+ const padding = " ".repeat(maxKeyLength - key.length + 2);
253
+ console.log(` ${import_chalk.default.bold(key)}${padding}${value}`);
254
+ });
255
+ }
256
+
257
+ // src/cli/commands/init.ts
258
+ var import_commander = require("commander");
259
+
260
+ // src/services/api-client.ts
261
+ var DEFAULT_TIMEOUT_MS = 3e4;
262
+ var DEFAULT_MAX_RETRIES = 2;
263
+ var RETRY_BASE_DELAY_MS = 500;
264
+ var AppilotsAPIClient = class {
265
+ serverUrl;
266
+ apiKey;
267
+ timeoutMs;
268
+ maxRetries;
269
+ constructor(config) {
270
+ this.serverUrl = config.serverUrl.replace(/\/$/, "");
271
+ this.apiKey = config.apiKey;
272
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
273
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
274
+ }
275
+ /**
276
+ * fetch with a hard timeout and exponential-backoff retries. Retries
277
+ * only on network failures and 5xx responses — 4xx are the caller's
278
+ * problem and retrying them would just spam the server.
279
+ */
280
+ async request(url, init) {
281
+ let lastError;
282
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
283
+ if (attempt > 0) {
284
+ await new Promise((r) => setTimeout(r, RETRY_BASE_DELAY_MS * 2 ** (attempt - 1)));
285
+ }
286
+ const controller = new AbortController();
287
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
288
+ try {
289
+ const response = await fetch(url, { ...init, signal: controller.signal });
290
+ if (response.status >= 500 && attempt < this.maxRetries) {
291
+ lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);
292
+ continue;
293
+ }
294
+ return response;
295
+ } catch (err) {
296
+ lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : err;
297
+ } finally {
298
+ clearTimeout(timer);
299
+ }
300
+ }
301
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
302
+ }
303
+ /**
304
+ * Syncs content with the Appilots backend
305
+ *
306
+ * @param content The MCP content object to sync
307
+ * @param version Version string for the content
308
+ * @param appVersion Host app version (package.json) — sent as
309
+ * X-App-Version so the backend can associate the MCP
310
+ * document with the app build that produced it
311
+ * @returns SyncResult with success status and metadata
312
+ */
313
+ async sync(content, version, appVersion) {
314
+ try {
315
+ const response = await this.request(`${this.serverUrl}/api/v1/cli/sync`, {
316
+ method: "POST",
317
+ headers: {
318
+ "Content-Type": "application/json",
319
+ Authorization: `Bearer ${this.apiKey}`,
320
+ ...appVersion ? { "X-App-Version": appVersion } : {}
321
+ },
322
+ body: JSON.stringify({ content, version })
323
+ });
324
+ if (!response.ok) {
325
+ const errorData = await response.json().catch(() => ({}));
326
+ return {
327
+ success: false,
328
+ unchanged: false,
329
+ error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
330
+ };
331
+ }
332
+ const json = await response.json();
333
+ const inner = json.data ?? json;
334
+ return {
335
+ success: true,
336
+ ...inner
337
+ };
338
+ } catch (error2) {
339
+ return {
340
+ success: false,
341
+ unchanged: false,
342
+ error: error2 instanceof Error ? error2.message : "Failed to sync with Appilots API"
343
+ };
344
+ }
345
+ }
346
+ /**
347
+ * Gets the status of the Appilots project and active MCP
348
+ *
349
+ * @returns StatusResult with project and active MCP information
350
+ */
351
+ async status() {
352
+ try {
353
+ const response = await this.request(`${this.serverUrl}/api/v1/cli/status`, {
354
+ method: "GET",
355
+ headers: {
356
+ Authorization: `Bearer ${this.apiKey}`
357
+ }
358
+ });
359
+ if (!response.ok) {
360
+ const errorData = await response.json().catch(() => ({}));
361
+ return {
362
+ error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
363
+ };
364
+ }
365
+ const json = await response.json();
366
+ const inner = json.data ?? json;
367
+ return inner;
368
+ } catch (error2) {
369
+ return {
370
+ error: error2 instanceof Error ? error2.message : "Failed to fetch status from Appilots API"
371
+ };
372
+ }
373
+ }
374
+ /**
375
+ * Runs a batch of eval scenarios against the project's MCP document in
376
+ * decision/dry-run mode — no side effects, same relay path as the
377
+ * dashboard's sandbox, authenticated with this client's API key.
378
+ *
379
+ * @param scenarios Scenarios to run, capped server-side at
380
+ * EVAL_MAX_SCENARIOS_PER_RUN (currently 20) as a cost guard.
381
+ */
382
+ async evalRun(scenarios) {
383
+ try {
384
+ const response = await this.request(`${this.serverUrl}/api/v1/cli/eval/run`, {
385
+ method: "POST",
386
+ headers: {
387
+ "Content-Type": "application/json",
388
+ Authorization: `Bearer ${this.apiKey}`
389
+ },
390
+ body: JSON.stringify({ scenarios })
391
+ });
392
+ if (!response.ok) {
393
+ const errorData = await response.json().catch(() => ({}));
394
+ const errObj = errorData?.error;
395
+ return {
396
+ error: errObj?.message || `HTTP ${response.status}: ${response.statusText}`
397
+ };
398
+ }
399
+ const json = await response.json();
400
+ const inner = json.data ?? json;
401
+ return { results: inner.results ?? [] };
402
+ } catch (error2) {
403
+ return {
404
+ error: error2 instanceof Error ? error2.message : "Failed to run eval against Appilots API"
405
+ };
406
+ }
407
+ }
408
+ /**
409
+ * Checks if the Appilots API server is healthy
410
+ *
411
+ * @returns true if server is healthy, false otherwise
412
+ */
413
+ async health() {
414
+ try {
415
+ const response = await this.request(`${this.serverUrl}/api/v1/health`, {
416
+ method: "GET"
417
+ });
418
+ return response.ok;
419
+ } catch {
420
+ return false;
421
+ }
422
+ }
423
+ };
424
+
425
+ // src/cli/commands/init.ts
426
+ var import_fs3 = require("fs");
427
+ var import_path2 = require("path");
428
+ function ensureConfigIgnored(dir) {
429
+ const gitignorePath = (0, import_path2.join)(dir, ".gitignore");
430
+ if (!(0, import_fs3.existsSync)(gitignorePath)) {
431
+ return "no-gitignore";
432
+ }
433
+ const lines = (0, import_fs3.readFileSync)(gitignorePath, "utf-8").split(/\r?\n/);
434
+ const ignored = lines.some((line) => {
435
+ const entry = line.trim();
436
+ return entry === ".appilotsrc" || entry === "/.appilotsrc" || entry === ".appilotsrc/";
437
+ });
438
+ if (ignored) {
439
+ return "already-ignored";
440
+ }
441
+ (0, import_fs3.appendFileSync)(gitignorePath, "\n# Appilots CLI config (contains your API key \u2014 do not commit)\n.appilotsrc\n");
442
+ return "added";
443
+ }
444
+ function initCommand() {
445
+ return new import_commander.Command("init").description("Set up Appilots configuration interactively").option("--api-key <key>", "Appilots API key (ak_... format)").option("--server <url>", "Appilots server URL").action(async (options) => {
446
+ try {
447
+ banner();
448
+ const configPath = getConfigPath();
449
+ if (configPath) {
450
+ warn(`Found existing .appilotsrc at ${configPath}`);
451
+ warn("Use --api-key and --server to override settings");
452
+ }
453
+ const env = getEnvOverrides();
454
+ const apiKey = options.apiKey || env.apiKey;
455
+ if (!apiKey) {
456
+ error("API key is required");
457
+ info("Provide it with: --api-key <your-ak_...key>");
458
+ info("Or set APPILOTS_API_KEY environment variable");
459
+ process.exit(1);
460
+ }
461
+ if (!options.apiKey && env.apiKey) {
462
+ info("Using API key from APPILOTS_API_KEY environment variable");
463
+ }
464
+ const serverUrl = options.server || env.serverUrl || "http://localhost:4000";
465
+ if (!apiKey.startsWith("ak_")) {
466
+ error("Invalid API key format. Must start with ak_");
467
+ process.exit(1);
468
+ }
469
+ info("Validating API key...");
470
+ const apiClient = new AppilotsAPIClient({
471
+ serverUrl,
472
+ apiKey
473
+ });
474
+ const isHealthy = await apiClient.health();
475
+ if (!isHealthy) {
476
+ error(`Cannot reach Appilots server at ${serverUrl}`);
477
+ info("Make sure the server is running and the URL is correct");
478
+ process.exit(1);
479
+ }
480
+ const status = await apiClient.status();
481
+ if (status.error) {
482
+ error(`Failed to validate API key: ${status.error}`);
483
+ process.exit(1);
484
+ }
485
+ if (!status.project) {
486
+ error("Could not retrieve project information");
487
+ process.exit(1);
488
+ }
489
+ success(`\u2713 Successfully connected to ${status.project.name}`);
490
+ success(`\u2713 Project ID: ${status.project.id}`);
491
+ const config = {
492
+ apiKey,
493
+ projectId: status.project.id,
494
+ serverUrl,
495
+ outputDir: ".appilots",
496
+ autoActivate: true
497
+ };
498
+ saveConfig(process.cwd(), config);
499
+ success("Configuration saved to .appilotsrc");
500
+ switch (ensureConfigIgnored(process.cwd())) {
501
+ case "added":
502
+ success("Added .appilotsrc to .gitignore (it contains your API key)");
503
+ break;
504
+ case "no-gitignore":
505
+ warn(".appilotsrc contains your API key \u2014 make sure it is NOT committed");
506
+ info('Add ".appilotsrc" to your .gitignore');
507
+ break;
508
+ case "already-ignored":
509
+ break;
510
+ }
511
+ info("Next steps:");
512
+ dim(" $ appilots sync - Analyze and upload your project");
513
+ dim(" $ appilots watch - Watch for changes and auto-sync");
514
+ } catch (err) {
515
+ error(`Initialization failed: ${err instanceof Error ? err.message : String(err)}`);
516
+ process.exit(1);
517
+ }
518
+ });
519
+ }
520
+
521
+ // src/cli/commands/sync.ts
522
+ var import_commander2 = require("commander");
523
+ var import_promises3 = require("fs/promises");
524
+ var import_node_path2 = require("path");
525
+
526
+ // src/generators/MCPGenerator.ts
527
+ var import_promises2 = require("fs/promises");
528
+ var import_node_crypto = require("crypto");
529
+ var import_node_path = __toESM(require("path"));
530
+
531
+ // src/analyzers/ScreenAnalyzer.ts
532
+ var import_promises = __toESM(require("fs/promises"));
533
+ var import_path3 = __toESM(require("path"));
534
+ var import_traverse4 = __toESM(require("@babel/traverse"));
535
+ var BabelTypes = __toESM(require("@babel/types"));
536
+ var import_fast_glob = __toESM(require("fast-glob"));
537
+
538
+ // src/ast/parse.ts
539
+ var import_parser = require("@babel/parser");
540
+ var DEFAULT_PARSER_PLUGINS = [
541
+ "jsx",
542
+ "typescript",
543
+ ["decorators", { decoratorsBeforeExport: true }],
544
+ "asyncGenerators",
545
+ ["pipelineOperator", { proposal: "minimal" }],
546
+ "partialApplication",
547
+ "logicalAssignment",
548
+ "classPrivateProperties",
549
+ "classPrivateMethods",
550
+ ["recordAndTuple", { syntaxType: "hash" }]
551
+ ];
552
+ function parseSource(source, parserPlugins = []) {
553
+ return (0, import_parser.parse)(source, {
554
+ sourceType: "module",
555
+ plugins: [...DEFAULT_PARSER_PLUGINS, ...parserPlugins]
556
+ });
557
+ }
558
+
559
+ // src/ast/jsx/attributes.ts
560
+ var t = __toESM(require("@babel/types"));
561
+ function findJsxAttribute(element, name) {
562
+ return element.attributes.find(
563
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === name
564
+ );
565
+ }
566
+ function getStringAttr(element, name) {
567
+ const attr = findJsxAttribute(element, name);
568
+ if (!attr || !attr.value) return void 0;
569
+ if (t.isStringLiteral(attr.value)) return attr.value.value;
570
+ if (t.isJSXExpressionContainer(attr.value) && t.isStringLiteral(attr.value.expression)) {
571
+ return attr.value.expression.value;
572
+ }
573
+ return void 0;
574
+ }
575
+ function getExpressionIdentifierAttr(element, name) {
576
+ const attr = findJsxAttribute(element, name);
577
+ if (!attr?.value || !t.isJSXExpressionContainer(attr.value)) return void 0;
578
+ return t.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
579
+ }
580
+ function hasJsxAttribute(element, name) {
581
+ return Boolean(findJsxAttribute(element, name));
582
+ }
583
+
584
+ // src/ast/jsx/classify.ts
585
+ var VIEW_COMPONENTS = /* @__PURE__ */ new Set(["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"]);
586
+ var INPUT_COMPONENTS = /* @__PURE__ */ new Set(["TextInput", "Input", "HocInput"]);
587
+ var BUTTON_COMPONENTS = /* @__PURE__ */ new Set(["TouchableOpacity", "Pressable", "Button"]);
588
+ var LIST_COMPONENTS = /* @__PURE__ */ new Set(["FlatList", "SectionList", "VirtualizedList", "FlashList"]);
589
+ var MODAL_COMPONENTS = /* @__PURE__ */ new Set(["Modal", "BottomSheet"]);
590
+ function classifyJsxComponent(name, element) {
591
+ if (LIST_COMPONENTS.has(name)) return "list";
592
+ if (MODAL_COMPONENTS.has(name)) return "modal";
593
+ if (/date/i.test(name)) return "date";
594
+ if (/(select|picker|dropdown|radio)/i.test(name)) return "select";
595
+ if (/(checkbox|switch|toggle)/i.test(name)) return "toggle";
596
+ if (INPUT_COMPONENTS.has(name)) return "input";
597
+ if (BUTTON_COMPONENTS.has(name)) return "button";
598
+ if (element) {
599
+ const hasOptions = hasJsxAttribute(element, "options");
600
+ const hasValue = hasJsxAttribute(element, "value");
601
+ const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
602
+ const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange") || hasJsxAttribute(element, "onChangeText");
603
+ const hasOnPress = hasJsxAttribute(element, "onPress");
604
+ if (hasOptions && (hasValue || hasOnChange)) return "select";
605
+ if (hasChecked && hasOnChange) return "toggle";
606
+ if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
607
+ return "input";
608
+ }
609
+ if (hasOnPress) return "button";
610
+ if (getStringAttr(element, "visible") || getExpressionIdentifierAttr(element, "visible")) {
611
+ return "modal";
612
+ }
613
+ }
614
+ if (VIEW_COMPONENTS.has(name)) return "view";
615
+ return "custom";
616
+ }
617
+
618
+ // src/ast/functions/collect.ts
619
+ var import_traverse = __toESM(require("@babel/traverse"));
620
+ var t2 = __toESM(require("@babel/types"));
621
+ function collectFunctions(ast) {
622
+ const handlers = /* @__PURE__ */ new Map();
623
+ (0, import_traverse.default)(ast, {
624
+ FunctionDeclaration: (nodePath) => {
625
+ if (nodePath.node.id?.name) handlers.set(nodePath.node.id.name, nodePath.node);
626
+ },
627
+ VariableDeclarator: (nodePath) => {
628
+ if (!t2.isIdentifier(nodePath.node.id)) return;
629
+ const init = nodePath.node.init;
630
+ if (t2.isArrowFunctionExpression(init) || t2.isFunctionExpression(init)) {
631
+ handlers.set(nodePath.node.id.name, init);
632
+ }
633
+ }
634
+ });
635
+ return handlers;
636
+ }
637
+
638
+ // src/ast/functions/analyze-async.ts
639
+ var import_traverse2 = __toESM(require("@babel/traverse"));
640
+ var t3 = __toESM(require("@babel/types"));
641
+ var DESTRUCTIVE_VERB = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
642
+ function analyzeFunctionBehavior(name, fn, handlers, seen = /* @__PURE__ */ new Set()) {
643
+ if (seen.has(name)) {
644
+ return {
645
+ appilotsInferred: { expectedOutcome: "none" },
646
+ destructive: false,
647
+ nativeConfirmationExpected: false
648
+ };
649
+ }
650
+ seen.add(name);
651
+ let hasAwait = fn.async === true;
652
+ let hasThen = false;
653
+ let hasStateSetter = false;
654
+ let nativeConfirmationExpected = false;
655
+ let targetScreen;
656
+ let successSignal;
657
+ let failureSignal;
658
+ let opensModal;
659
+ let destructive = DESTRUCTIVE_VERB.test(name);
660
+ const loadingStateBindings = /* @__PURE__ */ new Set();
661
+ const inspectCall = (node) => {
662
+ if (t3.isMemberExpression(node.callee) && t3.isIdentifier(node.callee.property)) {
663
+ const method = node.callee.property.name;
664
+ if (method === "then" || method === "catch" || method === "finally") hasThen = true;
665
+ if (t3.isIdentifier(node.callee.object) && node.callee.object.name === "navigation" && ["navigate", "push", "replace"].includes(method)) {
666
+ const firstArg = node.arguments[0];
667
+ if (t3.isStringLiteral(firstArg)) {
668
+ targetScreen = firstArg.value;
669
+ successSignal = { type: "navigation", target: firstArg.value };
670
+ }
671
+ }
672
+ if (t3.isIdentifier(node.callee.object) && node.callee.object.name === "navigation" && method === "goBack") {
673
+ successSignal = { type: "goBack", description: "Action returns to the previous screen" };
674
+ }
675
+ if (t3.isIdentifier(node.callee.object) && node.callee.object.name === "Alert" && method === "alert") {
676
+ nativeConfirmationExpected = true;
677
+ }
678
+ if (t3.isIdentifier(node.callee.object) && /(toast|notification|snackbar)/i.test(node.callee.object.name)) {
679
+ const firstArg = node.arguments[0];
680
+ const secondArg = node.arguments[1];
681
+ const text = t3.isStringLiteral(secondArg) ? secondArg.value : t3.isStringLiteral(firstArg) ? firstArg.value : void 0;
682
+ const signal = { type: "toast", description: text };
683
+ if (/(error|danger|fail)/i.test(method) || t3.isStringLiteral(firstArg) && /error/i.test(firstArg.value)) {
684
+ failureSignal = signal;
685
+ } else {
686
+ successSignal = successSignal ?? signal;
687
+ }
688
+ }
689
+ }
690
+ if (t3.isIdentifier(node.callee)) {
691
+ const calleeName = node.callee.name;
692
+ if (/^set[A-Z]/.test(calleeName)) {
693
+ hasStateSetter = true;
694
+ const stateName = setterToStateName(calleeName);
695
+ const firstArg = node.arguments[0];
696
+ if (stateName && t3.isBooleanLiteral(firstArg)) {
697
+ if (/loading|submitting|saving|fetching|refreshing/i.test(stateName)) {
698
+ loadingStateBindings.add(stateName);
699
+ }
700
+ if (firstArg.value === true && !/loading|submitting|saving|fetching|refreshing/i.test(stateName)) {
701
+ opensModal = opensModal ?? stateName;
702
+ }
703
+ }
704
+ }
705
+ if (DESTRUCTIVE_VERB.test(calleeName)) destructive = true;
706
+ const nested = handlers.get(calleeName);
707
+ if (nested) {
708
+ const nestedInfo = analyzeFunctionBehavior(calleeName, nested, handlers, seen);
709
+ hasAwait = hasAwait || nestedInfo.appilotsInferred.isAsyncTrigger === true;
710
+ nativeConfirmationExpected ||= nestedInfo.nativeConfirmationExpected;
711
+ destructive ||= nestedInfo.destructive;
712
+ targetScreen = targetScreen ?? nestedInfo.targetScreen;
713
+ successSignal = successSignal ?? nestedInfo.successSignal;
714
+ failureSignal = failureSignal ?? nestedInfo.failureSignal;
715
+ opensModal = opensModal ?? nestedInfo.opensModal;
716
+ for (const binding of nestedInfo.loadingStateBindings ?? []) {
717
+ loadingStateBindings.add(binding);
718
+ }
719
+ }
720
+ }
721
+ };
722
+ const inspectNode = (node) => {
723
+ if (t3.isAwaitExpression(node)) hasAwait = true;
724
+ if (t3.isCallExpression(node)) inspectCall(node);
725
+ if (t3.isMemberExpression(node) && t3.isIdentifier(node.property) && DESTRUCTIVE_VERB.test(node.property.name)) {
726
+ destructive = true;
727
+ }
728
+ };
729
+ if (fn.body) {
730
+ (0, import_traverse2.default)(fn.body, {
731
+ noScope: true,
732
+ enter: (nodePath) => inspectNode(nodePath.node)
733
+ });
734
+ }
735
+ const expectedOutcome = targetScreen ? "navigation" : successSignal?.type === "goBack" ? "navigation" : hasStateSetter || successSignal?.type === "toast" || opensModal ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
736
+ return {
737
+ appilotsInferred: {
738
+ isAsyncTrigger: hasAwait || hasThen || void 0,
739
+ expectedOutcome,
740
+ ...loadingStateBindings.size > 0 ? { loadingStateBindings: Array.from(loadingStateBindings).sort() } : {}
741
+ },
742
+ destructive,
743
+ nativeConfirmationExpected,
744
+ targetScreen,
745
+ successSignal,
746
+ failureSignal,
747
+ opensModal,
748
+ loadingStateBindings: Array.from(loadingStateBindings).sort()
749
+ };
750
+ }
751
+ function setterToStateName(setterName) {
752
+ const raw = setterName.replace(/^set/, "");
753
+ if (!raw) return void 0;
754
+ return raw.charAt(0).toLowerCase() + raw.slice(1);
755
+ }
756
+
757
+ // src/ast/navigation/calls.ts
758
+ var import_traverse3 = __toESM(require("@babel/traverse"));
759
+ var t4 = __toESM(require("@babel/types"));
760
+ function extractNavigationCalls(ast) {
761
+ const calls = [];
762
+ (0, import_traverse3.default)(ast, {
763
+ noScope: !t4.isFile(ast),
764
+ CallExpression: (nodePath) => {
765
+ const node = nodePath.node;
766
+ if (!t4.isMemberExpression(node.callee) || !t4.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !t4.isIdentifier(node.callee.property)) {
767
+ return;
768
+ }
769
+ const method = node.callee.property.name;
770
+ if (!["navigate", "push", "replace", "goBack"].includes(method)) return;
771
+ const firstArg = node.arguments[0];
772
+ const targetScreen = t4.isStringLiteral(firstArg) ? firstArg.value : void 0;
773
+ calls.push({
774
+ method,
775
+ ...targetScreen ? { targetScreen } : {},
776
+ params: extractNavigationParams(node.arguments[1])
777
+ });
778
+ }
779
+ });
780
+ return calls;
781
+ }
782
+ function extractNavigationParams(arg) {
783
+ const params = {};
784
+ if (!arg || !t4.isObjectExpression(arg)) return params;
785
+ for (const prop of arg.properties) {
786
+ if (!t4.isObjectProperty(prop)) continue;
787
+ const key = t4.isIdentifier(prop.key) ? prop.key.name : t4.isStringLiteral(prop.key) ? prop.key.value : void 0;
788
+ if (!key) continue;
789
+ if (t4.isMemberExpression(prop.value) && t4.isIdentifier(prop.value.object) && t4.isIdentifier(prop.value.property)) {
790
+ params[key] = `${prop.value.object.name}.${prop.value.property.name}`;
791
+ } else if (t4.isIdentifier(prop.value)) {
792
+ params[key] = prop.value.name;
793
+ } else if (t4.isStringLiteral(prop.value)) {
794
+ params[key] = prop.value.value;
795
+ }
796
+ }
797
+ return params;
798
+ }
799
+
800
+ // src/extractors/appilots/jsdoc.ts
801
+ function extractPermissionsFromJsDoc(source) {
802
+ if (!source.includes("@appilots-")) return void 0;
803
+ const result = {};
804
+ let touched = false;
805
+ const permRe = /@appilots-permissions\s+([^\n]+)/g;
806
+ let m;
807
+ while (m = permRe.exec(source)) {
808
+ const rawLine = (m[1] ?? "").replace(/\*\/?\s*$/, "").trim();
809
+ const parsed = parsePermissionLine(rawLine);
810
+ if (parsed) {
811
+ Object.assign(result, parsed);
812
+ touched = true;
813
+ break;
814
+ }
815
+ }
816
+ if (/@appilots-pii\b/.test(source)) {
817
+ result.isPii = true;
818
+ touched = true;
819
+ }
820
+ const blockedRe = /@appilots-blocked-actions\s+([^\n]+)/;
821
+ const blockedMatch = blockedRe.exec(source);
822
+ if (blockedMatch && blockedMatch[1]) {
823
+ const ids = blockedMatch[1].replace(/\*\/?\s*$/, "").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
824
+ if (ids.length > 0) {
825
+ result.blockedActions = ids;
826
+ touched = true;
827
+ }
828
+ }
829
+ return touched ? result : void 0;
830
+ }
831
+ function extractDestructiveJsDocTargets(source) {
832
+ if (!/@appilots-destructive\b/.test(source)) return void 0;
833
+ const out = /* @__PURE__ */ new Set();
834
+ const re = /@appilots-destructive\s*([^\n]*)/g;
835
+ let bareSeen = false;
836
+ let m;
837
+ while (m = re.exec(source)) {
838
+ const rest = (m[1] ?? "").replace(/\*\/?\s*$/, "").trim();
839
+ if (!rest) {
840
+ bareSeen = true;
841
+ continue;
842
+ }
843
+ for (const id of rest.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean)) {
844
+ out.add(id);
845
+ }
846
+ }
847
+ if (out.size === 0) return bareSeen ? "*" : void 0;
848
+ return out;
849
+ }
850
+ function parsePermissionLine(line) {
851
+ const trimmed = line.trim();
852
+ if (!trimmed) return null;
853
+ if (trimmed.startsWith("{")) {
854
+ const candidate = trimmed.replace(/'/g, '"');
855
+ try {
856
+ const obj = JSON.parse(candidate);
857
+ if (obj && typeof obj === "object") {
858
+ return sanitisePermissionsObject(obj);
859
+ }
860
+ } catch {
861
+ }
862
+ }
863
+ const out = {};
864
+ for (const pair of trimmed.split(/[,\s]+/)) {
865
+ const [k, v] = pair.split("=");
866
+ if (!k || v === void 0) continue;
867
+ const key = k.trim();
868
+ const value = v.trim().replace(/^['"]|['"]$/g, "");
869
+ if (value === "true") out[key] = true;
870
+ else if (value === "false") out[key] = false;
871
+ else out[key] = value;
872
+ }
873
+ return Object.keys(out).length > 0 ? sanitisePermissionsObject(out) : null;
874
+ }
875
+ function sanitisePermissionsObject(obj) {
876
+ const out = {};
877
+ if (typeof obj.agentAccess === "string" && (obj.agentAccess === "read" || obj.agentAccess === "write" || obj.agentAccess === "none")) {
878
+ out.agentAccess = obj.agentAccess;
879
+ }
880
+ if (typeof obj.requiresConfirmation === "boolean") {
881
+ out.requiresConfirmation = obj.requiresConfirmation;
882
+ }
883
+ if (typeof obj.isPii === "boolean") {
884
+ out.isPii = obj.isPii;
885
+ }
886
+ if (Array.isArray(obj.blockedActions)) {
887
+ const ids = obj.blockedActions.filter((x) => typeof x === "string" && !!x);
888
+ if (ids.length > 0) out.blockedActions = ids;
889
+ }
890
+ return out;
891
+ }
892
+
893
+ // src/analyzers/ScreenAnalyzer.ts
894
+ var ScreenAnalyzer = class {
895
+ config;
896
+ verbose = process.env.VERBOSE === "true";
897
+ /** When true, only files with registerScreen or matching screen patterns are included */
898
+ strictScreens;
899
+ /** Glob patterns that identify screen files in strict mode */
900
+ screenPatterns;
901
+ /** §D: Count of screens filtered out in strict mode (available after analyze()) */
902
+ screensFilteredOut = 0;
903
+ constructor(config, options) {
904
+ this.config = config;
905
+ this.strictScreens = options?.strictScreens ?? false;
906
+ this.screenPatterns = options?.screenPatterns ?? [
907
+ "**/*Screen.{ts,tsx}",
908
+ "**/screens/**/*.{ts,tsx}"
909
+ ];
910
+ }
911
+ /** Analyze all screens in the project */
912
+ async analyze() {
913
+ const { include = ["src/**/*Screen.{ts,tsx}"], exclude = ["node_modules/**"] } = this.config;
914
+ if (this.verbose) {
915
+ console.log(`[ScreenAnalyzer] Searching with patterns:`, include);
916
+ if (this.strictScreens) {
917
+ console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
918
+ }
919
+ }
920
+ const files = await (0, import_fast_glob.default)(include, {
921
+ cwd: this.config.rootDir,
922
+ ignore: exclude
923
+ });
924
+ if (this.verbose) {
925
+ console.log(`[ScreenAnalyzer] Found ${files.length} files`);
926
+ }
927
+ let screenPatternFiles = null;
928
+ if (this.strictScreens) {
929
+ const matched = await (0, import_fast_glob.default)(this.screenPatterns, {
930
+ cwd: this.config.rootDir,
931
+ ignore: exclude
932
+ });
933
+ screenPatternFiles = new Set(matched.map((f) => import_path3.default.resolve(this.config.rootDir, f)));
934
+ }
935
+ const screens = [];
936
+ this.screensFilteredOut = 0;
937
+ for (const file of files) {
938
+ const filePath = import_path3.default.resolve(this.config.rootDir, file);
939
+ try {
940
+ const descriptor = await this.analyzeFile(filePath);
941
+ if (!descriptor) continue;
942
+ if (this.strictScreens) {
943
+ const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
944
+ const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
945
+ if (!hasRegisterScreen && !matchesPattern) {
946
+ this.screensFilteredOut++;
947
+ if (this.verbose) {
948
+ console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file}`);
949
+ }
950
+ continue;
951
+ }
952
+ }
953
+ screens.push(descriptor);
954
+ if (this.verbose) {
955
+ console.log(`[ScreenAnalyzer] \u2713 Analyzed: ${descriptor.name} (${file})`);
956
+ }
957
+ } catch (error2) {
958
+ if (this.verbose) {
959
+ console.warn(`[ScreenAnalyzer] Failed to parse ${file}:`, error2 instanceof Error ? error2.message : error2);
960
+ }
961
+ }
962
+ }
963
+ if (this.verbose && this.strictScreens) {
964
+ console.log(`[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`);
965
+ }
966
+ return screens;
967
+ }
968
+ /** Analyze a single file. Returns descriptor + whether registerScreen was found. */
969
+ async analyzeFile(filePath) {
970
+ const source = await import_promises.default.readFile(filePath, "utf-8");
971
+ const name = this.extractScreenName(filePath);
972
+ const ast = parseSource(source, this.config.parserPlugins);
973
+ const registerScreenMeta = this.extractRegisterScreenMetadata(ast);
974
+ const hasRegisterScreenCall = this.detectRegisterScreenCall(ast);
975
+ const componentName = this.extractDefaultComponentName(ast);
976
+ const navigationTargets = this.extractNavigationTargets(ast);
977
+ const forms = this.mergeForms(registerScreenMeta?.forms ?? [], this.extractForms(ast));
978
+ const components = this.extractComponents(ast);
979
+ const actions = this.extractActions(ast, registerScreenMeta);
980
+ const collections = this.extractCollections(ast);
981
+ const permissionsFromJsDoc = extractPermissionsFromJsDoc(source);
982
+ const destructiveTags = extractDestructiveJsDocTargets(source);
983
+ if (destructiveTags) {
984
+ for (const action of actions) {
985
+ if (destructiveTags === "*" || destructiveTags.has(action.id)) {
986
+ action.destructive = true;
987
+ }
988
+ }
989
+ }
990
+ const descriptor = {
991
+ name: registerScreenMeta?.name || name,
992
+ filePath,
993
+ title: registerScreenMeta?.title,
994
+ description: registerScreenMeta?.description,
995
+ components,
996
+ forms,
997
+ actions,
998
+ navigationTargets,
999
+ ...collections.length > 0 ? { collections } : {},
1000
+ ...registerScreenMeta?.suggestedPrompts && registerScreenMeta.suggestedPrompts.length > 0 ? { suggestedPrompts: registerScreenMeta.suggestedPrompts } : {},
1001
+ ...permissionsFromJsDoc ? {
1002
+ permissions: permissionsFromJsDoc,
1003
+ ...permissionsFromJsDoc.isPii ? { isPii: true } : {}
1004
+ } : {}
1005
+ };
1006
+ descriptor.__hasRegisterScreen = hasRegisterScreenCall;
1007
+ return descriptor;
1008
+ }
1009
+ /**
1010
+ * §B: Detect actual presence of registerScreen() call in the AST.
1011
+ * This is more reliable than checking for title/description which are optional.
1012
+ */
1013
+ detectRegisterScreenCall(ast) {
1014
+ let found = false;
1015
+ (0, import_traverse4.default)(ast, {
1016
+ CallExpression: (nodePath) => {
1017
+ if (found) return;
1018
+ const callee = nodePath.node.callee;
1019
+ if (BabelTypes.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.property) && callee.property.name === "registerScreen") {
1020
+ found = true;
1021
+ nodePath.stop();
1022
+ }
1023
+ }
1024
+ });
1025
+ return found;
1026
+ }
1027
+ /**
1028
+ * Extract metadata from registerScreen() call
1029
+ */
1030
+ extractRegisterScreenMetadata(ast) {
1031
+ let metadata = null;
1032
+ (0, import_traverse4.default)(ast, {
1033
+ CallExpression: (nodePath) => {
1034
+ const callee = nodePath.node.callee;
1035
+ if (BabelTypes.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.property) && callee.property.name === "registerScreen") {
1036
+ const arg = nodePath.node.arguments[0];
1037
+ if (BabelTypes.isObjectExpression(arg)) {
1038
+ metadata = this.parseRegisterScreenObject(arg);
1039
+ }
1040
+ }
1041
+ }
1042
+ });
1043
+ return metadata;
1044
+ }
1045
+ /**
1046
+ * Parse the object passed to registerScreen()
1047
+ */
1048
+ parseRegisterScreenObject(objExpr) {
1049
+ const result = {
1050
+ name: "",
1051
+ actions: [],
1052
+ forms: [],
1053
+ navigationTargets: [],
1054
+ components: []
1055
+ };
1056
+ for (const prop of objExpr.properties) {
1057
+ if (!BabelTypes.isObjectProperty(prop) && !BabelTypes.isObjectMethod(prop)) {
1058
+ continue;
1059
+ }
1060
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
1061
+ if (!key) continue;
1062
+ if (BabelTypes.isObjectProperty(prop)) {
1063
+ const value = prop.value;
1064
+ if (key === "name" && BabelTypes.isStringLiteral(value)) {
1065
+ result.name = value.value;
1066
+ } else if (key === "title" && BabelTypes.isStringLiteral(value)) {
1067
+ result.title = value.value;
1068
+ } else if (key === "description" && BabelTypes.isStringLiteral(value)) {
1069
+ result.description = value.value;
1070
+ } else if (key === "actions" && BabelTypes.isArrayExpression(value)) {
1071
+ result.actions = this.parseActionsArray(value);
1072
+ } else if (key === "fields" && BabelTypes.isArrayExpression(value)) {
1073
+ const fields = this.parseFieldsArray(value);
1074
+ if (fields.length > 0) {
1075
+ result.forms = [
1076
+ {
1077
+ id: "default",
1078
+ fields
1079
+ }
1080
+ ];
1081
+ }
1082
+ } else if (key === "suggestedPrompts" && BabelTypes.isArrayExpression(value)) {
1083
+ const prompts = [];
1084
+ for (const element of value.elements) {
1085
+ if (BabelTypes.isStringLiteral(element)) {
1086
+ const trimmed = element.value.trim();
1087
+ if (trimmed.length > 0) prompts.push(trimmed);
1088
+ }
1089
+ }
1090
+ if (prompts.length > 0) {
1091
+ result.suggestedPrompts = prompts;
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+ return result;
1097
+ }
1098
+ /**
1099
+ * Parse actions array from registerScreen
1100
+ */
1101
+ parseActionsArray(arr) {
1102
+ const actions = [];
1103
+ for (const element of arr.elements) {
1104
+ if (BabelTypes.isObjectExpression(element)) {
1105
+ const action = this.parseActionObject(element);
1106
+ if (action.id) {
1107
+ actions.push(action);
1108
+ }
1109
+ }
1110
+ }
1111
+ return actions;
1112
+ }
1113
+ /**
1114
+ * Parse a single action object
1115
+ */
1116
+ parseActionObject(obj) {
1117
+ const action = {
1118
+ id: "",
1119
+ type: "custom"
1120
+ };
1121
+ for (const prop of obj.properties) {
1122
+ if (!BabelTypes.isObjectProperty(prop)) continue;
1123
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
1124
+ if (!key) continue;
1125
+ const value = prop.value;
1126
+ if (key === "id" && BabelTypes.isStringLiteral(value)) {
1127
+ action.id = value.value;
1128
+ } else if (key === "label" && BabelTypes.isStringLiteral(value)) {
1129
+ action.label = value.value;
1130
+ } else if (key === "type" && BabelTypes.isStringLiteral(value)) {
1131
+ action.type = value.value || "custom";
1132
+ } else if (key === "targetScreen" && BabelTypes.isStringLiteral(value)) {
1133
+ action.targetScreen = value.value;
1134
+ } else if (key === "description" && BabelTypes.isStringLiteral(value)) {
1135
+ action.description = value.value;
1136
+ } else if (key === "handler" && BabelTypes.isStringLiteral(value)) {
1137
+ action.handler = value.value;
1138
+ } else if (key === "requiresConfirmation" && BabelTypes.isBooleanLiteral(value)) {
1139
+ action.requiresConfirmation = value.value;
1140
+ } else if (key === "destructive" && BabelTypes.isBooleanLiteral(value)) {
1141
+ action.destructive = value.value;
1142
+ } else if (key === "effect" && BabelTypes.isStringLiteral(value)) {
1143
+ action.effect = value.value;
1144
+ } else if (key === "riskLevel" && BabelTypes.isStringLiteral(value)) {
1145
+ action.riskLevel = value.value;
1146
+ } else if (key === "nativeConfirmationExpected" && BabelTypes.isBooleanLiteral(value)) {
1147
+ action.nativeConfirmationExpected = value.value;
1148
+ } else if (key === "appilotsInferred" && BabelTypes.isObjectExpression(value)) {
1149
+ action.appilotsInferred = this.parseAppilotsInferredObject(value);
1150
+ }
1151
+ }
1152
+ return action;
1153
+ }
1154
+ parseAppilotsInferredObject(obj) {
1155
+ const out = {};
1156
+ for (const prop of obj.properties) {
1157
+ if (!BabelTypes.isObjectProperty(prop)) continue;
1158
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
1159
+ if (!key) continue;
1160
+ const value = prop.value;
1161
+ if (key === "isAsyncTrigger" && BabelTypes.isBooleanLiteral(value)) {
1162
+ out.isAsyncTrigger = value.value;
1163
+ } else if (key === "expectedOutcome" && BabelTypes.isStringLiteral(value)) {
1164
+ out.expectedOutcome = value.value;
1165
+ } else if (key === "loadingStateBindings" && BabelTypes.isArrayExpression(value)) {
1166
+ out.loadingStateBindings = value.elements.filter((el) => BabelTypes.isStringLiteral(el)).map((el) => el.value);
1167
+ }
1168
+ }
1169
+ return out;
1170
+ }
1171
+ /**
1172
+ * Parse fields array from registerScreen
1173
+ */
1174
+ parseFieldsArray(arr) {
1175
+ const fields = [];
1176
+ for (const element of arr.elements) {
1177
+ if (BabelTypes.isObjectExpression(element)) {
1178
+ const field = this.parseFieldObject(element);
1179
+ if (field.name) {
1180
+ fields.push(field);
1181
+ }
1182
+ }
1183
+ }
1184
+ return fields;
1185
+ }
1186
+ /**
1187
+ * Parse a single field object
1188
+ */
1189
+ parseFieldObject(obj) {
1190
+ const field = {
1191
+ name: "",
1192
+ type: "text",
1193
+ required: false
1194
+ };
1195
+ for (const prop of obj.properties) {
1196
+ if (!BabelTypes.isObjectProperty(prop)) continue;
1197
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
1198
+ if (!key) continue;
1199
+ const value = prop.value;
1200
+ if (key === "id" && BabelTypes.isStringLiteral(value)) {
1201
+ field.name = value.value;
1202
+ } else if (key === "label" && BabelTypes.isStringLiteral(value)) {
1203
+ field.label = value.value;
1204
+ } else if (key === "type" && BabelTypes.isStringLiteral(value)) {
1205
+ field.type = value.value || "text";
1206
+ } else if (key === "required" && BabelTypes.isBooleanLiteral(value)) {
1207
+ field.required = value.value;
1208
+ } else if (key === "placeholder" && BabelTypes.isStringLiteral(value)) {
1209
+ field.placeholder = value.value;
1210
+ } else if (key === "defaultValue" && BabelTypes.isExpression(value)) {
1211
+ field.defaultValue = this.extractLiteralValue(value);
1212
+ } else if (key === "options" && BabelTypes.isArrayExpression(value)) {
1213
+ field.options = this.parseOptionsArray(value);
1214
+ }
1215
+ }
1216
+ return field;
1217
+ }
1218
+ /**
1219
+ * Parse options array for select fields
1220
+ */
1221
+ parseOptionsArray(arr) {
1222
+ const options = [];
1223
+ for (const element of arr.elements) {
1224
+ if (BabelTypes.isObjectExpression(element)) {
1225
+ let label = "";
1226
+ let value = "";
1227
+ for (const prop of element.properties) {
1228
+ if (!BabelTypes.isObjectProperty(prop)) continue;
1229
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
1230
+ if (!key) continue;
1231
+ if (key === "label" && BabelTypes.isStringLiteral(prop.value)) {
1232
+ label = prop.value.value;
1233
+ } else if (key === "value" && BabelTypes.isStringLiteral(prop.value)) {
1234
+ value = prop.value.value;
1235
+ }
1236
+ }
1237
+ if (label && value) {
1238
+ options.push({ label, value });
1239
+ }
1240
+ }
1241
+ }
1242
+ return options;
1243
+ }
1244
+ mergeForms(primary, secondary) {
1245
+ const out = primary.map((form) => ({
1246
+ ...form,
1247
+ fields: [...form.fields]
1248
+ }));
1249
+ for (const form of secondary) {
1250
+ const existing = out.find((candidate) => candidate.id === form.id) ?? this.findFormWithSharedFields(out, form);
1251
+ if (!existing) {
1252
+ out.push({ ...form, fields: [...form.fields], submitAction: this.namedSubmitAction(form.submitAction) });
1253
+ continue;
1254
+ }
1255
+ for (const field of form.fields) {
1256
+ const existingField = this.findEquivalentField(existing.fields, field);
1257
+ if (existingField) {
1258
+ this.mergeFieldMetadata(existingField, field);
1259
+ } else {
1260
+ existing.fields.push(field);
1261
+ }
1262
+ }
1263
+ existing.submitAction = existing.submitAction ?? this.namedSubmitAction(form.submitAction);
1264
+ existing.validationRules = form.validationRules ? { ...form.validationRules, ...existing.validationRules ?? {} } : existing.validationRules;
1265
+ }
1266
+ return out;
1267
+ }
1268
+ findEquivalentField(fields, incoming) {
1269
+ return fields.find((field) => {
1270
+ if (field.name && incoming.name && field.name === incoming.name) return true;
1271
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
1272
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
1273
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
1274
+ if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
1275
+ return true;
1276
+ }
1277
+ return false;
1278
+ });
1279
+ }
1280
+ namedSubmitAction(submitAction) {
1281
+ return submitAction && submitAction !== "anonymous" ? submitAction : void 0;
1282
+ }
1283
+ mergeFieldMetadata(target, source) {
1284
+ if (this.isWeakInferredFieldName(target.name) && !this.isWeakInferredFieldName(source.name)) {
1285
+ target.name = source.name;
1286
+ }
1287
+ target.label = target.label ?? source.label;
1288
+ target.placeholder = target.placeholder ?? source.placeholder;
1289
+ target.options = target.options ?? source.options;
1290
+ target.locator = target.locator ?? source.locator;
1291
+ if (target.locator && source.locator) {
1292
+ target.locator = this.mergeLocator(source.locator, target.locator);
1293
+ }
1294
+ target.sourceComponent = target.sourceComponent ?? source.sourceComponent;
1295
+ target.valueBinding = target.valueBinding ?? source.valueBinding;
1296
+ target.errorBinding = target.errorBinding ?? source.errorBinding;
1297
+ target.required = target.required || source.required;
1298
+ if (target.type === "text" && source.type !== "text") {
1299
+ target.type = source.type;
1300
+ }
1301
+ }
1302
+ findFormWithSharedFields(forms, incoming) {
1303
+ if (incoming.fields.length === 0) return void 0;
1304
+ let best;
1305
+ for (const form of forms) {
1306
+ const overlap = incoming.fields.filter(
1307
+ (field) => this.findEquivalentField(form.fields, field)
1308
+ ).length;
1309
+ if (overlap > 0 && (!best || overlap > best.overlap)) {
1310
+ best = { form, overlap };
1311
+ }
1312
+ }
1313
+ return best?.form;
1314
+ }
1315
+ /**
1316
+ * Extract the name of the default exported component
1317
+ */
1318
+ extractDefaultComponentName(ast) {
1319
+ let componentName = "";
1320
+ (0, import_traverse4.default)(ast, {
1321
+ ExportDefaultDeclaration: (nodePath) => {
1322
+ const declaration = nodePath.node.declaration;
1323
+ if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id?.name) {
1324
+ componentName = declaration.id.name;
1325
+ } else if (BabelTypes.isArrowFunctionExpression(declaration)) {
1326
+ componentName = "Component";
1327
+ }
1328
+ },
1329
+ VariableDeclarator: (nodePath) => {
1330
+ if (BabelTypes.isIdentifier(nodePath.node.id) && (BabelTypes.isArrowFunctionExpression(nodePath.node.init) || BabelTypes.isFunctionExpression(nodePath.node.init))) {
1331
+ const parent = nodePath.parent;
1332
+ if (BabelTypes.isVariableDeclaration(parent) && parent.declare === true) {
1333
+ componentName = nodePath.node.id.name;
1334
+ }
1335
+ }
1336
+ }
1337
+ });
1338
+ return componentName;
1339
+ }
1340
+ /**
1341
+ * Extract navigation targets from navigation.navigate() calls
1342
+ */
1343
+ extractNavigationTargets(ast) {
1344
+ const targets = /* @__PURE__ */ new Set();
1345
+ (0, import_traverse4.default)(ast, {
1346
+ CallExpression: (nodePath) => {
1347
+ const callee = nodePath.node.callee;
1348
+ if (BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.object) && callee.object.name === "navigation" && BabelTypes.isIdentifier(callee.property) && callee.property.name === "navigate") {
1349
+ const arg = nodePath.node.arguments[0];
1350
+ if (BabelTypes.isStringLiteral(arg)) {
1351
+ targets.add(arg.value);
1352
+ }
1353
+ }
1354
+ }
1355
+ });
1356
+ return Array.from(targets).sort();
1357
+ }
1358
+ /**
1359
+ * Extract form information from JSX
1360
+ */
1361
+ extractForms(ast) {
1362
+ const forms = [];
1363
+ const fields = /* @__PURE__ */ new Map();
1364
+ (0, import_traverse4.default)(ast, {
1365
+ JSXOpeningElement: (nodePath) => {
1366
+ const element = nodePath.node;
1367
+ if (BabelTypes.isJSXIdentifier(element.name)) {
1368
+ const componentName = element.name.name;
1369
+ const role = classifyJsxComponent(componentName, element);
1370
+ if (["input", "select", "toggle", "date"].includes(role)) {
1371
+ const field = this.extractFieldFromInputElement(element);
1372
+ if (field.name) {
1373
+ fields.set(field.name, field);
1374
+ }
1375
+ }
1376
+ }
1377
+ }
1378
+ });
1379
+ if (fields.size > 0) {
1380
+ forms.push({
1381
+ id: "default",
1382
+ fields: Array.from(fields.values())
1383
+ });
1384
+ }
1385
+ return forms;
1386
+ }
1387
+ /**
1388
+ * Extract field metadata from a TextInput/Input element
1389
+ */
1390
+ extractFieldFromInputElement(element) {
1391
+ const field = {
1392
+ name: "",
1393
+ type: "text",
1394
+ required: false
1395
+ };
1396
+ const componentName = BabelTypes.isJSXIdentifier(element.name) ? element.name.name : "";
1397
+ const role = componentName ? classifyJsxComponent(componentName, element) : "custom";
1398
+ field.sourceComponent = componentName || void 0;
1399
+ for (const attr of element.attributes) {
1400
+ if (!BabelTypes.isJSXAttribute(attr)) continue;
1401
+ const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
1402
+ if (!attrName) continue;
1403
+ if (attr.value) {
1404
+ if (BabelTypes.isStringLiteral(attr.value)) {
1405
+ if (attrName === "placeholder") {
1406
+ field.placeholder = attr.value.value;
1407
+ } else if (attrName === "appilotsId") {
1408
+ field.name = attr.value.value;
1409
+ field.locator = this.mergeLocator(field.locator, {
1410
+ id: attr.value.value,
1411
+ appilotsId: attr.value.value,
1412
+ source: "appilotsId"
1413
+ });
1414
+ } else if (attrName === "testID") {
1415
+ if (!field.name) {
1416
+ field.name = attr.value.value.replace(/^(input-|field-|txt-)/, "");
1417
+ }
1418
+ field.locator = this.mergeLocator(field.locator, {
1419
+ id: field.name || attr.value.value,
1420
+ testID: attr.value.value,
1421
+ source: "testID"
1422
+ });
1423
+ } else if (attrName === "label") {
1424
+ field.label = attr.value.value;
1425
+ } else if (attrName === "accessibilityLabel") {
1426
+ if (!field.label) field.label = attr.value.value;
1427
+ field.locator = this.mergeLocator(field.locator, {
1428
+ ...field.locator?.id ? {} : { id: this.slugifyActionId(attr.value.value) },
1429
+ accessibilityLabel: attr.value.value,
1430
+ source: field.locator?.source ?? "accessibilityLabel"
1431
+ });
1432
+ } else if (attrName === "keyboardType") {
1433
+ if (attr.value.value === "email-address" || attr.value.value === "email") field.type = "email";
1434
+ if (attr.value.value === "phone-pad") field.type = "phone";
1435
+ if (attr.value.value === "numeric" || attr.value.value === "number-pad") field.type = "number";
1436
+ }
1437
+ } else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
1438
+ if (attrName === "value" && BabelTypes.isIdentifier(attr.value.expression)) {
1439
+ if (!field.name) {
1440
+ field.name = attr.value.expression.name;
1441
+ }
1442
+ field.valueBinding = attr.value.expression.name;
1443
+ } else if (attrName === "checked" && BabelTypes.isIdentifier(attr.value.expression)) {
1444
+ if (!field.name) field.name = attr.value.expression.name;
1445
+ field.valueBinding = attr.value.expression.name;
1446
+ } else if (attrName === "error") {
1447
+ field.errorBinding = this.expressionToBinding(attr.value.expression);
1448
+ } else if (attrName === "required" && BabelTypes.isBooleanLiteral(attr.value.expression)) {
1449
+ field.required = attr.value.expression.value;
1450
+ }
1451
+ }
1452
+ } else if (attrName === "required") {
1453
+ field.required = true;
1454
+ }
1455
+ }
1456
+ if (role === "select") field.type = "select";
1457
+ if (role === "toggle") field.type = "toggle";
1458
+ if (role === "date") field.type = "date";
1459
+ if (!field.name || this.isWeakInferredFieldName(field.name)) {
1460
+ const label = field.locator?.accessibilityLabel ?? field.label ?? field.placeholder;
1461
+ if (label) field.name = this.slugifyActionId(label);
1462
+ }
1463
+ if (!field.locator && field.name) {
1464
+ field.locator = { id: field.name, label: field.label, source: "inferred" };
1465
+ } else if (field.locator && !field.locator.id && field.name) {
1466
+ field.locator = this.mergeLocator(field.locator, {
1467
+ id: field.name,
1468
+ label: field.label,
1469
+ source: field.locator.source ?? "inferred"
1470
+ });
1471
+ }
1472
+ return field;
1473
+ }
1474
+ isWeakInferredFieldName(name) {
1475
+ return /^(text|value|input|query|search|selected|checked)$/i.test(name);
1476
+ }
1477
+ /**
1478
+ * Extract component structure from JSX
1479
+ */
1480
+ extractComponents(ast) {
1481
+ const components = [];
1482
+ const seen = /* @__PURE__ */ new Set();
1483
+ (0, import_traverse4.default)(ast, {
1484
+ JSXOpeningElement: (nodePath) => {
1485
+ const element = nodePath.node;
1486
+ if (BabelTypes.isJSXIdentifier(element.name)) {
1487
+ const componentName = element.name.name;
1488
+ if (seen.has(componentName)) return;
1489
+ seen.add(componentName);
1490
+ const type = this.inferComponentType(componentName);
1491
+ const component = {
1492
+ name: componentName,
1493
+ type
1494
+ };
1495
+ for (const attr of element.attributes) {
1496
+ if (!BabelTypes.isJSXAttribute(attr)) continue;
1497
+ const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
1498
+ if (!attrName) continue;
1499
+ if (attr.value && BabelTypes.isStringLiteral(attr.value)) {
1500
+ if (attrName === "testID") {
1501
+ component.testID = attr.value.value;
1502
+ } else if (attrName === "accessibilityLabel") {
1503
+ component.accessibilityLabel = attr.value.value;
1504
+ }
1505
+ }
1506
+ }
1507
+ components.push(component);
1508
+ }
1509
+ }
1510
+ });
1511
+ return components;
1512
+ }
1513
+ /**
1514
+ * Infer component type from component name
1515
+ */
1516
+ inferComponentType(name) {
1517
+ const role = classifyJsxComponent(name);
1518
+ if (role === "select" || role === "toggle" || role === "date") {
1519
+ return "input";
1520
+ }
1521
+ if (role === "input") {
1522
+ return "input";
1523
+ }
1524
+ if (role === "button") {
1525
+ return "button";
1526
+ }
1527
+ if (role === "list") {
1528
+ return "list";
1529
+ }
1530
+ if (role === "modal") {
1531
+ return "modal";
1532
+ }
1533
+ if (role === "view") {
1534
+ return "view";
1535
+ }
1536
+ return "custom";
1537
+ }
1538
+ /**
1539
+ * Extract actions from button/touchable elements
1540
+ */
1541
+ extractActions(ast, registerScreenMeta) {
1542
+ const actions = [...registerScreenMeta?.actions || []];
1543
+ const actionIds = new Set(actions.map((a) => a.id));
1544
+ const actionLabels = new Map(
1545
+ actions.filter((a) => a.label).map((a) => [this.normalizeLabel(a.label), a])
1546
+ );
1547
+ (0, import_traverse4.default)(ast, {
1548
+ JSXOpeningElement: (nodePath) => {
1549
+ const element = nodePath.node;
1550
+ if (BabelTypes.isJSXIdentifier(element.name)) {
1551
+ const componentName = element.name.name;
1552
+ if (classifyJsxComponent(componentName, element) === "button") {
1553
+ const action = this.extractActionFromButton(element);
1554
+ const existingByLabel = action.label ? actionLabels.get(this.normalizeLabel(action.label)) : void 0;
1555
+ if (existingByLabel) {
1556
+ this.mergeActionMetadata(existingByLabel, action);
1557
+ return;
1558
+ }
1559
+ if (action.id && !actionIds.has(action.id)) {
1560
+ actions.push(action);
1561
+ actionIds.add(action.id);
1562
+ if (action.label) actionLabels.set(this.normalizeLabel(action.label), action);
1563
+ }
1564
+ }
1565
+ }
1566
+ }
1567
+ });
1568
+ this.enrichActionsFromHandlers(ast, actions);
1569
+ return actions;
1570
+ }
1571
+ normalizeLabel(label) {
1572
+ return label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
1573
+ }
1574
+ mergeActionMetadata(target, source) {
1575
+ target.handler = target.handler ?? source.handler;
1576
+ target.targetScreen = target.targetScreen ?? source.targetScreen;
1577
+ target.description = target.description ?? source.description;
1578
+ target.nativeConfirmationExpected = target.nativeConfirmationExpected || source.nativeConfirmationExpected || void 0;
1579
+ target.requiresConfirmation = target.requiresConfirmation || source.requiresConfirmation || void 0;
1580
+ target.destructive = target.destructive || source.destructive || void 0;
1581
+ target.effect = target.effect ?? source.effect;
1582
+ target.riskLevel = target.riskLevel ?? source.riskLevel;
1583
+ target.appilotsInferred = target.appilotsInferred ?? source.appilotsInferred;
1584
+ }
1585
+ enrichActionsFromHandlers(ast, actions) {
1586
+ const handlerMap = this.collectHandlerInfo(ast);
1587
+ const labelToHandler = this.collectButtonHandlersByLabel(ast);
1588
+ for (const action of actions) {
1589
+ if (!action.handler && action.label) {
1590
+ const handler2 = labelToHandler.get(action.label);
1591
+ if (handler2) action.handler = handler2;
1592
+ }
1593
+ const handler = action.handler ? handlerMap.get(action.handler) : void 0;
1594
+ if (!handler) continue;
1595
+ action.appilotsInferred = {
1596
+ ...action.appilotsInferred ?? {},
1597
+ ...handler.appilotsInferred
1598
+ };
1599
+ if (handler.nativeConfirmationExpected) action.nativeConfirmationExpected = true;
1600
+ if (handler.targetScreen && !action.targetScreen) action.targetScreen = handler.targetScreen;
1601
+ if (handler.successSignal && !action.successSignal) action.successSignal = handler.successSignal;
1602
+ if (handler.failureSignal && !action.failureSignal) action.failureSignal = handler.failureSignal;
1603
+ if (handler.opensModal && !action.opensModal) action.opensModal = handler.opensModal;
1604
+ if (handler.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
1605
+ action.destructive = true;
1606
+ action.effect = action.effect ?? "destructive";
1607
+ action.riskLevel = action.riskLevel ?? "high";
1608
+ action.requiresConfirmation = action.requiresConfirmation ?? true;
1609
+ }
1610
+ }
1611
+ }
1612
+ collectButtonHandlersByLabel(ast) {
1613
+ const out = /* @__PURE__ */ new Map();
1614
+ (0, import_traverse4.default)(ast, {
1615
+ JSXOpeningElement: (nodePath) => {
1616
+ const element = nodePath.node;
1617
+ if (!BabelTypes.isJSXIdentifier(element.name)) return;
1618
+ const componentName = element.name.name;
1619
+ if (classifyJsxComponent(componentName, element) !== "button") return;
1620
+ let label;
1621
+ let handler;
1622
+ for (const attr of element.attributes) {
1623
+ if (!BabelTypes.isJSXAttribute(attr)) continue;
1624
+ const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
1625
+ if (!attrName || !attr.value) continue;
1626
+ if ((attrName === "title" || attrName === "accessibilityLabel" || attrName === "label") && BabelTypes.isStringLiteral(attr.value)) {
1627
+ label = attr.value.value;
1628
+ }
1629
+ if (attrName === "onPress" && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
1630
+ handler = attr.value.expression.name;
1631
+ }
1632
+ }
1633
+ if (label && handler) out.set(label, handler);
1634
+ }
1635
+ });
1636
+ return out;
1637
+ }
1638
+ collectHandlerInfo(ast) {
1639
+ const handlers = collectFunctions(ast);
1640
+ const out = /* @__PURE__ */ new Map();
1641
+ for (const [name, fn] of handlers) {
1642
+ const info2 = analyzeFunctionBehavior(name, fn, handlers);
1643
+ out.set(name, info2);
1644
+ }
1645
+ return out;
1646
+ }
1647
+ analyzeHandlerFunction(name, fn, handlers, seen = /* @__PURE__ */ new Set()) {
1648
+ if (seen.has(name)) {
1649
+ return {
1650
+ appilotsInferred: { expectedOutcome: "none" },
1651
+ destructive: false,
1652
+ nativeConfirmationExpected: false
1653
+ };
1654
+ }
1655
+ seen.add(name);
1656
+ let hasAwait = fn.async === true;
1657
+ let hasThen = false;
1658
+ let hasStateSetter = false;
1659
+ let nativeConfirmationExpected = false;
1660
+ let targetScreen;
1661
+ let destructive = /(delete|destroy|remove|discard|wipe|drop|terminate)/i.test(name);
1662
+ const inspectNode = (node) => {
1663
+ if (BabelTypes.isAwaitExpression(node)) hasAwait = true;
1664
+ if (BabelTypes.isCallExpression(node) && BabelTypes.isMemberExpression(node.callee) && BabelTypes.isIdentifier(node.callee.property)) {
1665
+ const method = node.callee.property.name;
1666
+ if (method === "then" || method === "catch" || method === "finally") hasThen = true;
1667
+ if (BabelTypes.isIdentifier(node.callee.object) && node.callee.object.name === "navigation" && ["navigate", "push", "replace"].includes(method)) {
1668
+ const firstArg = node.arguments[0];
1669
+ if (BabelTypes.isStringLiteral(firstArg)) targetScreen = firstArg.value;
1670
+ }
1671
+ if (BabelTypes.isIdentifier(node.callee.object) && node.callee.object.name === "Alert" && method === "alert") {
1672
+ nativeConfirmationExpected = true;
1673
+ }
1674
+ }
1675
+ if (BabelTypes.isCallExpression(node) && BabelTypes.isIdentifier(node.callee)) {
1676
+ const calleeName = node.callee.name;
1677
+ if (/^set[A-Z]/.test(calleeName)) hasStateSetter = true;
1678
+ if (/(delete|destroy|remove|discard|wipe|drop|terminate)/i.test(calleeName)) {
1679
+ destructive = true;
1680
+ }
1681
+ const nested = handlers.get(calleeName);
1682
+ if (nested) {
1683
+ const nestedInfo = this.analyzeHandlerFunction(calleeName, nested, handlers, seen);
1684
+ hasAwait = hasAwait || nestedInfo.appilotsInferred.isAsyncTrigger === true;
1685
+ nativeConfirmationExpected ||= nestedInfo.nativeConfirmationExpected;
1686
+ destructive ||= nestedInfo.destructive;
1687
+ targetScreen = targetScreen ?? nestedInfo.targetScreen;
1688
+ }
1689
+ }
1690
+ if (BabelTypes.isMemberExpression(node) && BabelTypes.isIdentifier(node.property) && /(delete|destroy|remove|discard|wipe|drop|terminate)/i.test(node.property.name)) {
1691
+ destructive = true;
1692
+ }
1693
+ };
1694
+ if (fn.body) {
1695
+ (0, import_traverse4.default)(fn.body, {
1696
+ noScope: true,
1697
+ enter: (nodePath) => inspectNode(nodePath.node)
1698
+ });
1699
+ }
1700
+ const expectedOutcome = targetScreen ? "navigation" : hasStateSetter ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
1701
+ return {
1702
+ appilotsInferred: {
1703
+ isAsyncTrigger: hasAwait || hasThen || void 0,
1704
+ expectedOutcome
1705
+ },
1706
+ destructive,
1707
+ nativeConfirmationExpected,
1708
+ targetScreen
1709
+ };
1710
+ }
1711
+ /**
1712
+ * BACKLOG 2.2 — heuristic destructive detection.
1713
+ *
1714
+ * Combines several signals:
1715
+ * - JSX `destructive` boolean prop (`<Pressable destructive />`)
1716
+ * - JSX `aria-destructive` attribute
1717
+ * - Handler name contains a generic destructive verb
1718
+ * (delete/destroy/remove/discard/wipe/revoke/etc.). `cancel` is intentionally excluded —
1719
+ * too many "Cancelar" buttons that just dismiss modals.
1720
+ * - testID / id contains a destructive verb
1721
+ *
1722
+ * Default-allow with explicit opt-out via `@appilots-non-destructive`
1723
+ * is more dangerous than the inverse, so we default-deny: the
1724
+ * heuristic must hit OR the JSDoc tag must be present.
1725
+ */
1726
+ isElementDestructive(element, handlerName, actionId) {
1727
+ const DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
1728
+ for (const attr of element.attributes) {
1729
+ if (!BabelTypes.isJSXAttribute(attr)) continue;
1730
+ const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
1731
+ if (attrName === "destructive" || attrName === "aria-destructive") {
1732
+ if (attr.value === null) return true;
1733
+ if (BabelTypes.isJSXExpressionContainer(attr.value)) {
1734
+ const expr = attr.value.expression;
1735
+ if (BabelTypes.isBooleanLiteral(expr)) return expr.value;
1736
+ }
1737
+ if (BabelTypes.isStringLiteral(attr.value)) {
1738
+ return attr.value.value === "true";
1739
+ }
1740
+ }
1741
+ }
1742
+ if (handlerName && DESTRUCTIVE_VERB2.test(handlerName)) return true;
1743
+ if (actionId && DESTRUCTIVE_VERB2.test(actionId)) return true;
1744
+ return false;
1745
+ }
1746
+ /**
1747
+ * Extract action metadata from a button element
1748
+ */
1749
+ extractActionFromButton(element) {
1750
+ const action = {
1751
+ id: "",
1752
+ type: "custom"
1753
+ };
1754
+ const componentName = BabelTypes.isJSXIdentifier(element.name) ? element.name.name : void 0;
1755
+ action.sourceComponent = componentName;
1756
+ let handlerName;
1757
+ for (const attr of element.attributes) {
1758
+ if (!BabelTypes.isJSXAttribute(attr)) continue;
1759
+ const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
1760
+ if (!attrName) continue;
1761
+ if (attr.value) {
1762
+ if (BabelTypes.isStringLiteral(attr.value)) {
1763
+ if (attrName === "appilotsId") {
1764
+ action.id = attr.value.value;
1765
+ action.locator = this.mergeLocator(action.locator, {
1766
+ id: attr.value.value,
1767
+ appilotsId: attr.value.value,
1768
+ source: "appilotsId"
1769
+ });
1770
+ } else if (attrName === "testID") {
1771
+ action.id = attr.value.value;
1772
+ action.locator = this.mergeLocator(action.locator, {
1773
+ id: attr.value.value,
1774
+ testID: attr.value.value,
1775
+ source: "testID"
1776
+ });
1777
+ } else if (attrName === "title" || attrName === "label" || attrName === "accessibilityLabel") {
1778
+ action.label = attr.value.value;
1779
+ if (attrName === "accessibilityLabel") {
1780
+ action.locator = this.mergeLocator(action.locator, {
1781
+ accessibilityLabel: attr.value.value,
1782
+ source: "accessibilityLabel"
1783
+ });
1784
+ }
1785
+ if (!action.id) {
1786
+ action.id = this.slugifyActionId(attr.value.value);
1787
+ }
1788
+ }
1789
+ } else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
1790
+ if (attrName === "onPress" && BabelTypes.isIdentifier(attr.value.expression)) {
1791
+ handlerName = attr.value.expression.name.toLowerCase();
1792
+ action.handler = attr.value.expression.name;
1793
+ if (handlerName.includes("submit")) {
1794
+ action.type = "submit";
1795
+ } else if (handlerName.includes("navigate")) {
1796
+ action.type = "navigation";
1797
+ }
1798
+ } else if (attrName === "onPress") {
1799
+ const inline = this.extractInlineOnPressMetadata(attr.value.expression);
1800
+ if (inline.handler) {
1801
+ handlerName = inline.handler.toLowerCase();
1802
+ action.handler = inline.handler;
1803
+ if (handlerName.includes("submit")) action.type = "submit";
1804
+ }
1805
+ if (inline.targetScreen) {
1806
+ action.type = "navigation";
1807
+ action.targetScreen = inline.targetScreen;
1808
+ }
1809
+ } else if (attrName === "loading" && BabelTypes.isIdentifier(attr.value.expression)) {
1810
+ action.appilotsInferred = {
1811
+ ...action.appilotsInferred ?? {},
1812
+ isAsyncTrigger: true,
1813
+ loadingStateBindings: [attr.value.expression.name]
1814
+ };
1815
+ }
1816
+ }
1817
+ } else if (attrName === "destructive") {
1818
+ action.destructive = true;
1819
+ }
1820
+ }
1821
+ if (!action.locator && action.id) {
1822
+ action.locator = { id: action.id, label: action.label, source: action.label ? "label" : "inferred" };
1823
+ }
1824
+ if (this.isElementDestructive(element, handlerName, action.id)) {
1825
+ action.destructive = true;
1826
+ }
1827
+ return action;
1828
+ }
1829
+ extractInlineOnPressMetadata(expr) {
1830
+ if (BabelTypes.isJSXEmptyExpression(expr)) return {};
1831
+ const navCall = extractNavigationCalls(expr).find((call) => call.targetScreen);
1832
+ const handler = this.extractFirstHandlerName(expr);
1833
+ return {
1834
+ ...handler ? { handler } : {},
1835
+ ...navCall?.targetScreen ? { targetScreen: navCall.targetScreen } : {}
1836
+ };
1837
+ }
1838
+ extractFirstHandlerName(node) {
1839
+ if (BabelTypes.isIdentifier(node)) return node.name;
1840
+ if (BabelTypes.isArrowFunctionExpression(node) || BabelTypes.isFunctionExpression(node)) {
1841
+ return this.extractFirstHandlerName(node.body);
1842
+ }
1843
+ if (BabelTypes.isBlockStatement(node)) {
1844
+ for (const statement of node.body) {
1845
+ const handler = this.extractFirstHandlerName(statement);
1846
+ if (handler) return handler;
1847
+ }
1848
+ return void 0;
1849
+ }
1850
+ if (BabelTypes.isExpressionStatement(node)) {
1851
+ return this.extractFirstHandlerName(node.expression);
1852
+ }
1853
+ if (BabelTypes.isReturnStatement(node)) {
1854
+ return node.argument ? this.extractFirstHandlerName(node.argument) : void 0;
1855
+ }
1856
+ if (BabelTypes.isUnaryExpression(node)) {
1857
+ return this.extractFirstHandlerName(node.argument);
1858
+ }
1859
+ if (BabelTypes.isAwaitExpression(node)) {
1860
+ return this.extractFirstHandlerName(node.argument);
1861
+ }
1862
+ if (BabelTypes.isCallExpression(node)) {
1863
+ if (BabelTypes.isIdentifier(node.callee)) return node.callee.name;
1864
+ return void 0;
1865
+ }
1866
+ return void 0;
1867
+ }
1868
+ mergeLocator(current, next) {
1869
+ return { ...current ?? {}, ...next };
1870
+ }
1871
+ expressionToBinding(expr) {
1872
+ if (BabelTypes.isIdentifier(expr)) return expr.name;
1873
+ if (BabelTypes.isMemberExpression(expr) && BabelTypes.isIdentifier(expr.object) && BabelTypes.isIdentifier(expr.property)) {
1874
+ return `${expr.object.name}.${expr.property.name}`;
1875
+ }
1876
+ return void 0;
1877
+ }
1878
+ slugifyActionId(label) {
1879
+ return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1880
+ }
1881
+ extractCollections(ast) {
1882
+ const renderItemFns = this.collectRenderItemFunctions(ast);
1883
+ const collections = [];
1884
+ (0, import_traverse4.default)(ast, {
1885
+ JSXOpeningElement: (nodePath) => {
1886
+ const element = nodePath.node;
1887
+ if (!BabelTypes.isJSXIdentifier(element.name)) return;
1888
+ const component = element.name.name;
1889
+ if (!["FlatList", "SectionList", "VirtualizedList", "FlashList"].includes(component)) {
1890
+ return;
1891
+ }
1892
+ const dataSource = this.jsxExpressionIdentifier(element, "data");
1893
+ const renderItem = this.jsxExpressionIdentifier(element, "renderItem");
1894
+ const keyField = this.extractKeyField(element);
1895
+ const renderFn = renderItem ? renderItemFns.get(renderItem) : void 0;
1896
+ const rowAction = renderFn ? this.extractRowAction(renderFn) : void 0;
1897
+ const displayFields = renderFn ? this.extractItemFields(renderFn) : [];
1898
+ const itemType = this.inferItemType(dataSource, renderItem, displayFields);
1899
+ const identityFields = this.inferIdentityFields(keyField, displayFields);
1900
+ const searchField = this.inferSearchField(ast, dataSource);
1901
+ const id = dataSource ?? renderItem ?? `${component.toLowerCase()}-${collections.length + 1}`;
1902
+ collections.push({
1903
+ id,
1904
+ component,
1905
+ ...itemType ? { itemType } : {},
1906
+ ...dataSource ? { dataSource } : {},
1907
+ ...keyField ? { keyField } : {},
1908
+ ...renderItem ? { renderItem } : {},
1909
+ ...displayFields.length > 0 ? { displayFields } : {},
1910
+ ...rowAction ? { rowAction } : {},
1911
+ ...rowAction ? { rowActions: [rowAction] } : {},
1912
+ ...identityFields.length > 0 ? { identityFields } : {},
1913
+ ...searchField ? { searchField } : {}
1914
+ });
1915
+ }
1916
+ });
1917
+ return collections;
1918
+ }
1919
+ collectRenderItemFunctions(ast) {
1920
+ const out = /* @__PURE__ */ new Map();
1921
+ (0, import_traverse4.default)(ast, {
1922
+ VariableDeclarator: (nodePath) => {
1923
+ if (!BabelTypes.isIdentifier(nodePath.node.id)) return;
1924
+ const init = nodePath.node.init;
1925
+ if (BabelTypes.isArrowFunctionExpression(init) || BabelTypes.isFunctionExpression(init)) {
1926
+ out.set(nodePath.node.id.name, init);
1927
+ }
1928
+ },
1929
+ FunctionDeclaration: (nodePath) => {
1930
+ if (nodePath.node.id?.name) out.set(nodePath.node.id.name, nodePath.node);
1931
+ }
1932
+ });
1933
+ return out;
1934
+ }
1935
+ jsxExpressionIdentifier(element, attrName) {
1936
+ const attr = element.attributes.find(
1937
+ (candidate) => BabelTypes.isJSXAttribute(candidate) && BabelTypes.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
1938
+ );
1939
+ if (!BabelTypes.isJSXAttribute(attr) || !attr.value) return void 0;
1940
+ if (BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
1941
+ return attr.value.expression.name;
1942
+ }
1943
+ if (BabelTypes.isStringLiteral(attr.value)) return attr.value.value;
1944
+ return void 0;
1945
+ }
1946
+ extractKeyField(element) {
1947
+ const attr = element.attributes.find(
1948
+ (candidate) => BabelTypes.isJSXAttribute(candidate) && BabelTypes.isJSXIdentifier(candidate.name) && candidate.name.name === "keyExtractor"
1949
+ );
1950
+ if (!BabelTypes.isJSXAttribute(attr) || !attr.value || !BabelTypes.isJSXExpressionContainer(attr.value)) {
1951
+ return void 0;
1952
+ }
1953
+ const expr = attr.value.expression;
1954
+ if (!BabelTypes.isArrowFunctionExpression(expr)) return void 0;
1955
+ const param = expr.params[0];
1956
+ if (!BabelTypes.isIdentifier(param)) return void 0;
1957
+ if (BabelTypes.isMemberExpression(expr.body) && BabelTypes.isIdentifier(expr.body.object) && expr.body.object.name === param.name && BabelTypes.isIdentifier(expr.body.property)) {
1958
+ return expr.body.property.name;
1959
+ }
1960
+ return void 0;
1961
+ }
1962
+ extractRowAction(fn) {
1963
+ let action;
1964
+ (0, import_traverse4.default)(fn.body, {
1965
+ noScope: true,
1966
+ CallExpression: (nodePath) => {
1967
+ const node = nodePath.node;
1968
+ if (!BabelTypes.isMemberExpression(node.callee) || !BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !BabelTypes.isIdentifier(node.callee.property) || !["navigate", "push", "replace"].includes(node.callee.property.name)) {
1969
+ return;
1970
+ }
1971
+ const firstArg = node.arguments[0];
1972
+ if (!BabelTypes.isStringLiteral(firstArg)) return;
1973
+ const params = this.extractNavigationParams(node.arguments[1]);
1974
+ action = {
1975
+ type: "navigation",
1976
+ targetScreen: firstArg.value,
1977
+ ...Object.keys(params).length > 0 ? { params } : {},
1978
+ description: `Pressing a row opens ${firstArg.value}`
1979
+ };
1980
+ }
1981
+ });
1982
+ return action;
1983
+ }
1984
+ extractNavigationParams(arg) {
1985
+ const params = {};
1986
+ if (!arg || !BabelTypes.isObjectExpression(arg)) return params;
1987
+ for (const prop of arg.properties) {
1988
+ if (!BabelTypes.isObjectProperty(prop)) continue;
1989
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : void 0;
1990
+ if (!key) continue;
1991
+ if (BabelTypes.isMemberExpression(prop.value) && BabelTypes.isIdentifier(prop.value.object) && BabelTypes.isIdentifier(prop.value.property)) {
1992
+ params[key] = `${prop.value.object.name}.${prop.value.property.name}`;
1993
+ } else if (BabelTypes.isIdentifier(prop.value)) {
1994
+ params[key] = prop.value.name;
1995
+ } else if (BabelTypes.isStringLiteral(prop.value)) {
1996
+ params[key] = prop.value.value;
1997
+ }
1998
+ }
1999
+ return params;
2000
+ }
2001
+ extractItemFields(fn) {
2002
+ const fields = /* @__PURE__ */ new Set();
2003
+ const itemNames = /* @__PURE__ */ new Set(["item"]);
2004
+ const firstParam = fn.params[0];
2005
+ if (BabelTypes.isObjectPattern(firstParam)) {
2006
+ for (const prop of firstParam.properties) {
2007
+ if (BabelTypes.isObjectProperty(prop) && BabelTypes.isIdentifier(prop.key) && prop.key.name === "item" && BabelTypes.isIdentifier(prop.value)) {
2008
+ itemNames.add(prop.value.name);
2009
+ }
2010
+ }
2011
+ } else if (BabelTypes.isIdentifier(firstParam)) {
2012
+ itemNames.add(firstParam.name);
2013
+ }
2014
+ (0, import_traverse4.default)(fn.body, {
2015
+ noScope: true,
2016
+ MemberExpression: (nodePath) => {
2017
+ const node = nodePath.node;
2018
+ if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
2019
+ fields.add(node.property.name);
2020
+ }
2021
+ }
2022
+ });
2023
+ return Array.from(fields).sort();
2024
+ }
2025
+ inferItemType(dataSource, renderItem, displayFields) {
2026
+ const source = dataSource ?? renderItem;
2027
+ if (!source) return void 0;
2028
+ const singular = source.replace(/^render/i, "").replace(/(List|Items|Data|Rows|Sections)$/i, "").replace(/s$/i, "");
2029
+ const candidate = singular.charAt(0).toUpperCase() + singular.slice(1);
2030
+ if (candidate.length > 1) return candidate;
2031
+ if (displayFields.length > 0) return "Item";
2032
+ return void 0;
2033
+ }
2034
+ inferIdentityFields(keyField, displayFields) {
2035
+ const out = /* @__PURE__ */ new Set();
2036
+ if (keyField) out.add(keyField);
2037
+ for (const field of displayFields) {
2038
+ if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
2039
+ }
2040
+ return Array.from(out);
2041
+ }
2042
+ inferSearchField(ast, dataSource) {
2043
+ if (!dataSource) return void 0;
2044
+ let queryBinding;
2045
+ (0, import_traverse4.default)(ast, {
2046
+ CallExpression: (nodePath) => {
2047
+ const node = nodePath.node;
2048
+ if (!BabelTypes.isMemberExpression(node.callee)) return;
2049
+ if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter") return;
2050
+ if (!BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource) return;
2051
+ const fn = node.arguments[0];
2052
+ if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn)) return;
2053
+ (0, import_traverse4.default)(fn.body, {
2054
+ noScope: true,
2055
+ Identifier: (innerPath) => {
2056
+ const name = innerPath.node.name;
2057
+ if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
2058
+ }
2059
+ });
2060
+ }
2061
+ });
2062
+ return queryBinding;
2063
+ }
2064
+ /**
2065
+ * Extract literal values from AST nodes
2066
+ */
2067
+ extractLiteralValue(node) {
2068
+ if (BabelTypes.isStringLiteral(node)) {
2069
+ return node.value;
2070
+ }
2071
+ if (BabelTypes.isNumericLiteral(node)) {
2072
+ return node.value;
2073
+ }
2074
+ if (BabelTypes.isBooleanLiteral(node)) {
2075
+ return node.value;
2076
+ }
2077
+ if (BabelTypes.isNullLiteral(node)) {
2078
+ return null;
2079
+ }
2080
+ return void 0;
2081
+ }
2082
+ /**
2083
+ * Extract screen name from file path
2084
+ * E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
2085
+ */
2086
+ extractScreenName(filePath) {
2087
+ const basename2 = import_path3.default.basename(filePath);
2088
+ return basename2.replace(/\.(tsx?|jsx?)$/, "");
2089
+ }
2090
+ };
2091
+
2092
+ // src/analyzers/NavigationAnalyzer.ts
2093
+ var import_fs4 = require("fs");
2094
+ var import_path4 = __toESM(require("path"));
2095
+ var import_fast_glob2 = __toESM(require("fast-glob"));
2096
+ var parser = __toESM(require("@babel/parser"));
2097
+ var import_traverse5 = __toESM(require("@babel/traverse"));
2098
+ var t5 = __toESM(require("@babel/types"));
2099
+ var NavigationAnalyzer = class {
2100
+ config;
2101
+ /** Extra glob patterns for navigation file discovery (added to defaults) */
2102
+ navigationInclude;
2103
+ /** Extra glob patterns to exclude from navigation analysis */
2104
+ navigationExclude;
2105
+ constructor(config, options) {
2106
+ this.config = config;
2107
+ this.navigationInclude = options?.navigationInclude ?? [];
2108
+ this.navigationExclude = options?.navigationExclude ?? [];
2109
+ }
2110
+ /** Build the full navigation graph */
2111
+ async analyze() {
2112
+ const navigationFiles = await this.findNavigationFiles();
2113
+ const parsedNavigators = [];
2114
+ const typeExports = [];
2115
+ for (const filePath of navigationFiles) {
2116
+ try {
2117
+ const content = await import_fs4.promises.readFile(filePath, "utf-8");
2118
+ const navigators = this.parseNavigators(content, filePath);
2119
+ const types = this.parseParamTypes(content, filePath);
2120
+ parsedNavigators.push(...navigators);
2121
+ typeExports.push(...types);
2122
+ } catch (error2) {
2123
+ console.warn(`Failed to parse ${filePath}:`, error2);
2124
+ }
2125
+ }
2126
+ this.attachParamsToNavigators(parsedNavigators, typeExports);
2127
+ return this.buildNavigationGraph(parsedNavigators);
2128
+ }
2129
+ /** Find all navigation-related files */
2130
+ async findNavigationFiles() {
2131
+ const patterns = [
2132
+ "**/navigation/**/*.{ts,tsx}",
2133
+ "**/navigator*.{ts,tsx}",
2134
+ "**/routes*.{ts,tsx}",
2135
+ // §5: Add user-configured navigation patterns (additive, not replacing)
2136
+ ...this.navigationInclude
2137
+ ];
2138
+ const excludePatterns = [
2139
+ "**/node_modules/**",
2140
+ "**/dist/**",
2141
+ "**/build/**",
2142
+ ...this.config.exclude || [],
2143
+ // §5: Add user-configured navigation excludes
2144
+ ...this.navigationExclude
2145
+ ];
2146
+ const files = await (0, import_fast_glob2.default)(patterns, {
2147
+ cwd: this.config.rootDir,
2148
+ ignore: excludePatterns
2149
+ });
2150
+ return files.map((file) => import_path4.default.join(this.config.rootDir, file));
2151
+ }
2152
+ /** Parse navigator definitions from a file */
2153
+ parseNavigators(content, filePath) {
2154
+ const navigators = [];
2155
+ try {
2156
+ const ast = parser.parse(content, {
2157
+ sourceType: "module",
2158
+ plugins: [
2159
+ "jsx",
2160
+ "typescript",
2161
+ ...this.config.parserPlugins || []
2162
+ ]
2163
+ });
2164
+ const navigatorCalls = /* @__PURE__ */ new Map();
2165
+ const screensByNavigator = /* @__PURE__ */ new Map();
2166
+ (0, import_traverse5.default)(ast, {
2167
+ // Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
2168
+ CallExpression: (nodePath) => {
2169
+ const { node } = nodePath;
2170
+ const callee = node.callee;
2171
+ let navigatorType = null;
2172
+ if (t5.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
2173
+ navigatorType = "stack";
2174
+ } else if (t5.isIdentifier(callee) && callee.name === "createStackNavigator") {
2175
+ navigatorType = "stack";
2176
+ } else if (t5.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
2177
+ navigatorType = "tab";
2178
+ } else if (t5.isIdentifier(callee) && callee.name === "createTabNavigator") {
2179
+ navigatorType = "tab";
2180
+ } else if (t5.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
2181
+ navigatorType = "drawer";
2182
+ }
2183
+ if (navigatorType) {
2184
+ const parent = nodePath.parent;
2185
+ if (t5.isVariableDeclarator(parent) && t5.isIdentifier(parent.id)) {
2186
+ const varName = parent.id.name;
2187
+ navigatorCalls.set(varName, {
2188
+ name: varName,
2189
+ type: navigatorType,
2190
+ screens: []
2191
+ });
2192
+ }
2193
+ }
2194
+ },
2195
+ // Detect Stack.Navigator / Tab.Navigator JSX elements
2196
+ JSXElement: (nodePath) => {
2197
+ const { node } = nodePath;
2198
+ const openingElement = node.openingElement;
2199
+ if (t5.isJSXMemberExpression(openingElement.name) && t5.isJSXIdentifier(openingElement.name.object)) {
2200
+ const objectName = openingElement.name.object.name;
2201
+ const propertyName = t5.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
2202
+ if (propertyName === "Navigator") {
2203
+ const navigator = navigatorCalls.get(objectName);
2204
+ if (navigator) {
2205
+ const initialRouteAttr = openingElement.attributes.find(
2206
+ (attr) => t5.isJSXAttribute(attr) && t5.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
2207
+ );
2208
+ if (t5.isJSXAttribute(initialRouteAttr) && t5.isStringLiteral(initialRouteAttr.value)) {
2209
+ navigator.initialRouteName = initialRouteAttr.value.value;
2210
+ }
2211
+ const screens = this.extractScreensFromNavigator(
2212
+ node,
2213
+ objectName,
2214
+ navigator.type
2215
+ );
2216
+ screensByNavigator.set(objectName, screens);
2217
+ }
2218
+ }
2219
+ }
2220
+ }
2221
+ });
2222
+ navigatorCalls.forEach((navigator) => {
2223
+ const screens = screensByNavigator.get(navigator.name) || [];
2224
+ navigator.screens = screens;
2225
+ navigators.push(navigator);
2226
+ });
2227
+ } catch (error2) {
2228
+ console.warn(`Failed to parse navigators in ${filePath}:`, error2);
2229
+ }
2230
+ return navigators;
2231
+ }
2232
+ /** Extract screens from a navigator JSX element */
2233
+ extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
2234
+ const screens = [];
2235
+ if (!navigatorElement.children) return screens;
2236
+ for (const child of navigatorElement.children) {
2237
+ if (t5.isJSXElement(child) && t5.isJSXMemberExpression(child.openingElement.name)) {
2238
+ const memberExpr = child.openingElement.name;
2239
+ if (t5.isJSXIdentifier(memberExpr.object) && memberExpr.object.name === navigatorVarName && t5.isJSXIdentifier(memberExpr.property) && memberExpr.property.name === "Screen") {
2240
+ const screenName = this.extractAttributeValue(
2241
+ child.openingElement.attributes,
2242
+ "name"
2243
+ );
2244
+ if (screenName) {
2245
+ screens.push({
2246
+ name: screenName,
2247
+ navigatorName: navigatorVarName,
2248
+ navigatorType
2249
+ });
2250
+ }
2251
+ }
2252
+ }
2253
+ }
2254
+ return screens;
2255
+ }
2256
+ /** Extract string attribute value from JSX attributes */
2257
+ extractAttributeValue(attributes, attrName) {
2258
+ const attr = attributes.find(
2259
+ (a) => t5.isJSXAttribute(a) && t5.isJSXIdentifier(a.name) && a.name.name === attrName
2260
+ );
2261
+ if (t5.isJSXAttribute(attr) && t5.isStringLiteral(attr.value)) {
2262
+ return attr.value.value;
2263
+ }
2264
+ return null;
2265
+ }
2266
+ /** Parse TypeScript type exports (ParamList types) */
2267
+ parseParamTypes(content, filePath) {
2268
+ const types = [];
2269
+ try {
2270
+ const ast = parser.parse(content, {
2271
+ sourceType: "module",
2272
+ plugins: [
2273
+ "jsx",
2274
+ "typescript",
2275
+ ...this.config.parserPlugins || []
2276
+ ]
2277
+ });
2278
+ (0, import_traverse5.default)(ast, {
2279
+ TSTypeAliasDeclaration: (nodePath) => {
2280
+ const { node } = nodePath;
2281
+ const typeName = node.id.name;
2282
+ const typeMatch = /^[A-Za-z0-9_]*(Stack|Tab|Drawer)?ParamList$/i.test(typeName);
2283
+ if (typeMatch && node.typeAnnotation?.type === "TSTypeLiteral") {
2284
+ const paramEntries = this.extractParamListEntries(
2285
+ node.typeAnnotation,
2286
+ typeName
2287
+ );
2288
+ const navType = this.inferTypeFromName(typeName);
2289
+ if (navType) {
2290
+ types.push({
2291
+ name: typeName,
2292
+ type: navType.type,
2293
+ paramEntries
2294
+ });
2295
+ }
2296
+ }
2297
+ }
2298
+ });
2299
+ } catch (error2) {
2300
+ console.warn(`Failed to parse types in ${filePath}:`, error2);
2301
+ }
2302
+ return types;
2303
+ }
2304
+ /** Extract param entries from a TypeScript type literal */
2305
+ extractParamListEntries(typeLiteral, typeName) {
2306
+ const entries = /* @__PURE__ */ new Map();
2307
+ for (const member of typeLiteral.members) {
2308
+ if (member.type === "TSPropertySignature" && member.key) {
2309
+ const keyName = member.key.type === "Identifier" ? member.key.name : null;
2310
+ if (keyName && member.typeAnnotation) {
2311
+ const params = this.extractParamsFromType(
2312
+ member.typeAnnotation.typeAnnotation
2313
+ );
2314
+ entries.set(keyName, params);
2315
+ }
2316
+ }
2317
+ }
2318
+ return entries;
2319
+ }
2320
+ /** Extract param descriptors from a TypeScript type */
2321
+ extractParamsFromType(typeAnnotation) {
2322
+ const params = [];
2323
+ if (typeAnnotation.type === "TSUndefinedKeyword" || typeAnnotation.type === "TSNullKeyword") {
2324
+ return params;
2325
+ }
2326
+ if (typeAnnotation.type === "TSTypeLiteral") {
2327
+ const typeLiteral = typeAnnotation;
2328
+ for (const member of typeLiteral.members) {
2329
+ if (member.type === "TSPropertySignature" && member.key) {
2330
+ const keyName = member.key.type === "Identifier" ? member.key.name : null;
2331
+ if (keyName) {
2332
+ const typeStr = member.typeAnnotation ? this.typeToString(member.typeAnnotation.typeAnnotation) : "unknown";
2333
+ const required = !member.optional;
2334
+ params.push({
2335
+ name: keyName,
2336
+ type: typeStr,
2337
+ required
2338
+ });
2339
+ }
2340
+ }
2341
+ }
2342
+ }
2343
+ return params;
2344
+ }
2345
+ /** Convert TypeScript type annotation to string */
2346
+ typeToString(type) {
2347
+ if (type.type === "TSStringKeyword") return "string";
2348
+ if (type.type === "TSNumberKeyword") return "number";
2349
+ if (type.type === "TSBooleanKeyword") return "boolean";
2350
+ if (type.type === "TSUndefinedKeyword") return "undefined";
2351
+ if (type.type === "TSNullKeyword") return "null";
2352
+ if (type.type === "TSUnionType") {
2353
+ return type.types.map((t8) => this.typeToString(t8)).join(" | ");
2354
+ }
2355
+ if (type.type === "TSTypeLiteral") {
2356
+ return "object";
2357
+ }
2358
+ if (type.type === "TSTypeReference" && type.typeName) {
2359
+ const name = type.typeName;
2360
+ return name.type === "Identifier" ? name.name : "unknown";
2361
+ }
2362
+ return "unknown";
2363
+ }
2364
+ /** Infer navigator type from type name */
2365
+ inferTypeFromName(typeName) {
2366
+ if (/stack/i.test(typeName)) return { type: "stack" };
2367
+ if (/tab/i.test(typeName)) return { type: "tab" };
2368
+ if (/drawer/i.test(typeName)) return { type: "drawer" };
2369
+ return null;
2370
+ }
2371
+ /** Attach parsed type params to navigator screens */
2372
+ attachParamsToNavigators(navigators, types) {
2373
+ for (const navigator of navigators) {
2374
+ const matchingType = types.find((t8) => t8.type === navigator.type);
2375
+ if (matchingType) {
2376
+ for (const screen of navigator.screens) {
2377
+ const screenParams = matchingType.paramEntries.get(screen.name);
2378
+ if (screenParams) {
2379
+ screen.params = screenParams;
2380
+ }
2381
+ }
2382
+ }
2383
+ }
2384
+ }
2385
+ /** Build the complete navigation graph */
2386
+ buildNavigationGraph(parsedNavigators) {
2387
+ const screens = {};
2388
+ const navigators = [];
2389
+ let initialScreen = "";
2390
+ for (const navigator of parsedNavigators) {
2391
+ const screenNames = navigator.screens.map((s) => s.name);
2392
+ navigators.push({
2393
+ name: navigator.name,
2394
+ type: navigator.type,
2395
+ screens: screenNames,
2396
+ parentNavigator: navigator.parentNavigator
2397
+ });
2398
+ if (!initialScreen && navigator.initialRouteName && (navigator.name.includes("Root") || screenNames.some((s) => s === "Auth" || s === "Main"))) {
2399
+ initialScreen = navigator.initialRouteName;
2400
+ }
2401
+ }
2402
+ for (const navigator of parsedNavigators) {
2403
+ for (const screen of navigator.screens) {
2404
+ const screenKey = screen.name;
2405
+ const reachableTo = [];
2406
+ const reachableFrom = [];
2407
+ for (const otherScreen of navigator.screens) {
2408
+ if (otherScreen.name !== screen.name) {
2409
+ if (navigator.type === "tab") {
2410
+ reachableTo.push(otherScreen.name);
2411
+ reachableFrom.push(otherScreen.name);
2412
+ } else {
2413
+ reachableTo.push(otherScreen.name);
2414
+ }
2415
+ }
2416
+ }
2417
+ screens[screenKey] = {
2418
+ screenName: screen.name,
2419
+ navigatorType: navigator.type,
2420
+ parentNavigator: navigator.name,
2421
+ reachableFrom,
2422
+ reachableTo,
2423
+ params: screen.params
2424
+ };
2425
+ }
2426
+ }
2427
+ if (!initialScreen && parsedNavigators.length > 0) {
2428
+ const firstNavigator = parsedNavigators[0];
2429
+ const rootNavigator = parsedNavigators.find(
2430
+ (n) => n.name.includes("Root") || firstNavigator && n.name === firstNavigator.name
2431
+ );
2432
+ if (rootNavigator && rootNavigator.screens && rootNavigator.screens.length > 0) {
2433
+ const firstScreen = rootNavigator.screens[0];
2434
+ initialScreen = firstScreen?.name || "";
2435
+ }
2436
+ }
2437
+ return {
2438
+ screens,
2439
+ initialScreen,
2440
+ navigators
2441
+ };
2442
+ }
2443
+ };
2444
+
2445
+ // src/analyzers/ComponentAnalyzer.ts
2446
+ var import_fs5 = require("fs");
2447
+ var parser2 = __toESM(require("@babel/parser"));
2448
+ var import_traverse6 = __toESM(require("@babel/traverse"));
2449
+ var t6 = __toESM(require("@babel/types"));
2450
+ var ComponentAnalyzer = class {
2451
+ config;
2452
+ constructor(config) {
2453
+ this.config = config;
2454
+ }
2455
+ /** Analyze components in a file */
2456
+ async analyzeFile(filePath) {
2457
+ try {
2458
+ const code = (0, import_fs5.readFileSync)(filePath, "utf-8");
2459
+ const ast = parser2.parse(code, {
2460
+ sourceType: "module",
2461
+ plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
2462
+ });
2463
+ const components = [];
2464
+ (0, import_traverse6.default)(ast, {
2465
+ JSXElement: (path5) => {
2466
+ const component = this.extractComponentFromJSXElement(path5.node);
2467
+ if (component) {
2468
+ components.push(component);
2469
+ }
2470
+ }
2471
+ });
2472
+ return components;
2473
+ } catch (error2) {
2474
+ console.error(`Failed to analyze file ${filePath}:`, error2);
2475
+ return [];
2476
+ }
2477
+ }
2478
+ extractComponentFromJSXElement(element) {
2479
+ const elementName = this.getElementName(element.openingElement);
2480
+ if (!elementName) return null;
2481
+ const type = this.determineComponentType(elementName);
2482
+ const props = this.extractProps(element.openingElement);
2483
+ const { testID, accessibilityLabel } = this.extractAccessibilityProps(element.openingElement);
2484
+ const children = this.extractChildren(element.children);
2485
+ return {
2486
+ name: elementName,
2487
+ type,
2488
+ props: Object.keys(props).length > 0 ? props : void 0,
2489
+ children: children.length > 0 ? children : void 0,
2490
+ testID,
2491
+ accessibilityLabel
2492
+ };
2493
+ }
2494
+ getElementName(openingElement) {
2495
+ if (t6.isJSXIdentifier(openingElement.name)) {
2496
+ return openingElement.name.name;
2497
+ }
2498
+ if (t6.isJSXMemberExpression(openingElement.name)) {
2499
+ const parts = [];
2500
+ let current = openingElement.name;
2501
+ while (t6.isJSXMemberExpression(current)) {
2502
+ if (t6.isJSXIdentifier(current.property)) {
2503
+ parts.unshift(current.property.name);
2504
+ }
2505
+ current = current.object;
2506
+ }
2507
+ if (t6.isJSXIdentifier(current)) {
2508
+ parts.unshift(current.name);
2509
+ }
2510
+ return parts.join(".");
2511
+ }
2512
+ return null;
2513
+ }
2514
+ determineComponentType(name) {
2515
+ const viewComponents = ["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"];
2516
+ const inputComponents = ["TextInput", "Input"];
2517
+ const buttonComponents = ["TouchableOpacity", "Pressable", "Button"];
2518
+ const listComponents = ["FlatList", "SectionList"];
2519
+ const modalComponents = ["Modal", "BottomSheet"];
2520
+ if (viewComponents.includes(name)) return "view";
2521
+ if (inputComponents.includes(name)) return "input";
2522
+ if (buttonComponents.includes(name)) return "button";
2523
+ if (listComponents.includes(name)) return "list";
2524
+ if (modalComponents.includes(name)) return "modal";
2525
+ return "custom";
2526
+ }
2527
+ extractProps(openingElement) {
2528
+ const props = {};
2529
+ for (const attr of openingElement.attributes) {
2530
+ if (t6.isJSXAttribute(attr) && t6.isJSXIdentifier(attr.name)) {
2531
+ const propName = attr.name.name;
2532
+ if (attr.value === null) {
2533
+ props[propName] = "true";
2534
+ } else if (t6.isStringLiteral(attr.value)) {
2535
+ props[propName] = attr.value.value;
2536
+ } else if (t6.isJSXExpressionContainer(attr.value) && t6.isStringLiteral(attr.value.expression)) {
2537
+ props[propName] = attr.value.expression.value;
2538
+ }
2539
+ }
2540
+ }
2541
+ return props;
2542
+ }
2543
+ extractAccessibilityProps(openingElement) {
2544
+ const result = {};
2545
+ for (const attr of openingElement.attributes) {
2546
+ if (t6.isJSXAttribute(attr) && t6.isJSXIdentifier(attr.name)) {
2547
+ const propName = attr.name.name;
2548
+ if ((propName === "testID" || propName === "accessibilityLabel") && attr.value) {
2549
+ if (t6.isStringLiteral(attr.value)) {
2550
+ result[propName] = attr.value.value;
2551
+ } else if (t6.isJSXExpressionContainer(attr.value) && t6.isStringLiteral(attr.value.expression)) {
2552
+ result[propName] = attr.value.expression.value;
2553
+ }
2554
+ }
2555
+ }
2556
+ }
2557
+ return result;
2558
+ }
2559
+ extractChildren(children, depth = 0, maxDepth = 3) {
2560
+ if (depth >= maxDepth) return [];
2561
+ const extracted = [];
2562
+ for (const child of children) {
2563
+ if (t6.isJSXElement(child)) {
2564
+ const component = this.extractComponentFromJSXElement(child);
2565
+ if (component) {
2566
+ extracted.push(component);
2567
+ }
2568
+ }
2569
+ }
2570
+ return extracted;
2571
+ }
2572
+ };
2573
+
2574
+ // src/analyzers/FormAnalyzer.ts
2575
+ var import_fs6 = require("fs");
2576
+ var parser3 = __toESM(require("@babel/parser"));
2577
+ var import_traverse7 = __toESM(require("@babel/traverse"));
2578
+ var t7 = __toESM(require("@babel/types"));
2579
+ var path3 = __toESM(require("path"));
2580
+ var FormAnalyzer = class {
2581
+ config;
2582
+ stateVariables = /* @__PURE__ */ new Map();
2583
+ inputElements = [];
2584
+ submitButtons = [];
2585
+ constructor(config) {
2586
+ this.config = config;
2587
+ }
2588
+ /** Analyze forms in a file */
2589
+ async analyzeFile(filePath) {
2590
+ try {
2591
+ const code = (0, import_fs6.readFileSync)(filePath, "utf-8");
2592
+ const ast = parser3.parse(code, {
2593
+ sourceType: "module",
2594
+ plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
2595
+ });
2596
+ this.stateVariables.clear();
2597
+ this.inputElements = [];
2598
+ this.submitButtons = [];
2599
+ (0, import_traverse7.default)(ast, {
2600
+ CallExpression: (path5) => {
2601
+ this.extractStateVariables(path5.node);
2602
+ }
2603
+ });
2604
+ (0, import_traverse7.default)(ast, {
2605
+ JSXElement: (path5) => {
2606
+ this.extractFormElements(path5.node);
2607
+ }
2608
+ });
2609
+ const validationRules = this.extractValidationRules(ast);
2610
+ return this.buildForms(filePath, validationRules);
2611
+ } catch (error2) {
2612
+ console.error(`Failed to analyze forms in file ${filePath}:`, error2);
2613
+ return [];
2614
+ }
2615
+ }
2616
+ extractStateVariables(node) {
2617
+ if (t7.isIdentifier(node.callee) && node.callee.name === "useState" && node.arguments.length > 0) {
2618
+ return;
2619
+ }
2620
+ }
2621
+ extractFormElements(element) {
2622
+ const elementName = this.getElementName(element.openingElement);
2623
+ if (!elementName) return;
2624
+ if (elementName === "TextInput" || elementName === "Input") {
2625
+ const inputInfo = this.extractInputInfo(element.openingElement);
2626
+ this.inputElements.push(inputInfo);
2627
+ }
2628
+ if (["TouchableOpacity", "Pressable", "Button"].includes(elementName)) {
2629
+ const buttonInfo = this.extractButtonInfo(element.openingElement);
2630
+ if (buttonInfo) {
2631
+ this.submitButtons.push(buttonInfo);
2632
+ }
2633
+ }
2634
+ }
2635
+ extractInputInfo(openingElement) {
2636
+ const info2 = { varName: "" };
2637
+ for (const attr of openingElement.attributes) {
2638
+ if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
2639
+ const propName = attr.name.name;
2640
+ const propValue = this.extractAttributeValue(attr.value);
2641
+ switch (propName) {
2642
+ case "label":
2643
+ info2.label = propValue;
2644
+ break;
2645
+ case "placeholder":
2646
+ info2.placeholder = propValue;
2647
+ break;
2648
+ case "keyboardType":
2649
+ info2.keyboardType = propValue;
2650
+ break;
2651
+ case "testID":
2652
+ info2.testID = propValue;
2653
+ break;
2654
+ case "appilotsId":
2655
+ info2.appilotsId = propValue;
2656
+ if (propValue) info2.varName = propValue;
2657
+ break;
2658
+ case "value":
2659
+ if (attr.value && t7.isJSXExpressionContainer(attr.value) && t7.isIdentifier(attr.value.expression)) {
2660
+ info2.varName = attr.value.expression.name;
2661
+ }
2662
+ break;
2663
+ }
2664
+ }
2665
+ }
2666
+ return info2;
2667
+ }
2668
+ extractButtonInfo(openingElement) {
2669
+ const info2 = {};
2670
+ for (const attr of openingElement.attributes) {
2671
+ if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
2672
+ const propName = attr.name.name;
2673
+ if (propName === "title" || propName === "label") {
2674
+ info2.label = this.extractAttributeValue(attr.value);
2675
+ }
2676
+ if (propName === "onPress") {
2677
+ if (attr.value && t7.isJSXExpressionContainer(attr.value)) {
2678
+ if (t7.isIdentifier(attr.value.expression)) {
2679
+ info2.handler = attr.value.expression.name;
2680
+ } else if (t7.isArrowFunctionExpression(attr.value.expression) || t7.isFunctionExpression(attr.value.expression)) {
2681
+ info2.handler = "anonymous";
2682
+ }
2683
+ }
2684
+ }
2685
+ }
2686
+ }
2687
+ return Object.keys(info2).length > 0 ? info2 : null;
2688
+ }
2689
+ extractAttributeValue(value) {
2690
+ if (value === null) return void 0;
2691
+ if (t7.isStringLiteral(value)) return value.value;
2692
+ if (t7.isJSXExpressionContainer(value) && t7.isStringLiteral(value.expression)) {
2693
+ return value.expression.value;
2694
+ }
2695
+ return void 0;
2696
+ }
2697
+ getElementName(openingElement) {
2698
+ if (t7.isJSXIdentifier(openingElement.name)) {
2699
+ return openingElement.name.name;
2700
+ }
2701
+ return null;
2702
+ }
2703
+ extractValidationRules(ast) {
2704
+ const rules = {};
2705
+ (0, import_traverse7.default)(ast, {
2706
+ IfStatement: (path5) => {
2707
+ const test = path5.node.test;
2708
+ const rule = this.extractRuleFromCondition(test);
2709
+ if (rule) {
2710
+ const { field, description } = rule;
2711
+ if (field && description) {
2712
+ rules[field] = description;
2713
+ }
2714
+ }
2715
+ }
2716
+ });
2717
+ return rules;
2718
+ }
2719
+ extractRuleFromCondition(test) {
2720
+ let fieldName = "";
2721
+ let description = "";
2722
+ if (t7.isUnaryExpression(test) && test.operator === "!") {
2723
+ if (t7.isCallExpression(test.argument)) {
2724
+ const callExpr = test.argument;
2725
+ if (t7.isMemberExpression(callExpr.callee)) {
2726
+ const memberExpr = callExpr.callee;
2727
+ if (t7.isIdentifier(memberExpr.object)) {
2728
+ fieldName = memberExpr.object.name;
2729
+ description = `${fieldName} is required`;
2730
+ }
2731
+ }
2732
+ } else if (t7.isIdentifier(test.argument)) {
2733
+ fieldName = test.argument.name;
2734
+ description = `${fieldName} is required`;
2735
+ }
2736
+ }
2737
+ if (t7.isBinaryExpression(test) && (test.operator === "<" || test.operator === "<=")) {
2738
+ if (t7.isMemberExpression(test.left)) {
2739
+ const memberExpr = test.left;
2740
+ if (t7.isIdentifier(memberExpr.object)) {
2741
+ fieldName = memberExpr.object.name;
2742
+ if (t7.isNumericLiteral(test.right)) {
2743
+ description = `${fieldName} must be at least ${test.right.value} characters`;
2744
+ }
2745
+ }
2746
+ }
2747
+ }
2748
+ if (t7.isUnaryExpression(test) && test.operator === "!") {
2749
+ if (t7.isCallExpression(test.argument) && t7.isMemberExpression(test.argument.callee)) {
2750
+ const methodName = t7.isIdentifier(test.argument.callee.property) ? test.argument.callee.property.name : null;
2751
+ if (methodName === "includes" && t7.isIdentifier(test.argument.callee.object)) {
2752
+ fieldName = test.argument.callee.object.name;
2753
+ description = `${fieldName} format is invalid`;
2754
+ }
2755
+ }
2756
+ }
2757
+ return fieldName && description ? { field: fieldName, description } : null;
2758
+ }
2759
+ buildForms(filePath, validationRules) {
2760
+ if (this.inputElements.length === 0) return [];
2761
+ const fileName = path3.basename(filePath, path3.extname(filePath));
2762
+ const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
2763
+ const fields = this.inputElements.map((input) => {
2764
+ const fieldType = this.inferFieldType(input);
2765
+ const required = this.isFieldRequired(input.varName, validationRules);
2766
+ return {
2767
+ name: input.appilotsId || input.varName || input.testID || "field",
2768
+ label: input.label,
2769
+ type: fieldType,
2770
+ required,
2771
+ placeholder: input.placeholder
2772
+ };
2773
+ });
2774
+ const lastButton = this.submitButtons[this.submitButtons.length - 1];
2775
+ const submitAction = lastButton?.handler;
2776
+ return [
2777
+ {
2778
+ id: formId,
2779
+ fields,
2780
+ submitAction,
2781
+ validationRules: Object.keys(validationRules).length > 0 ? validationRules : void 0
2782
+ }
2783
+ ];
2784
+ }
2785
+ inferFieldType(input) {
2786
+ const lowerVarName = input.varName.toLowerCase();
2787
+ const lowerLabel = input.label?.toLowerCase() || "";
2788
+ const lowerPlaceholder = input.placeholder?.toLowerCase() || "";
2789
+ const combined = `${lowerVarName} ${lowerLabel} ${lowerPlaceholder}`;
2790
+ if (input.keyboardType === "email-address" || input.keyboardType === "email") {
2791
+ return "email";
2792
+ }
2793
+ if (input.keyboardType === "phone-pad" || input.keyboardType === "numeric") {
2794
+ return input.keyboardType === "numeric" ? "number" : "phone";
2795
+ }
2796
+ if (combined.includes("email")) return "email";
2797
+ if (combined.includes("phone") || combined.includes("tel")) return "phone";
2798
+ if (combined.includes("password")) return "text";
2799
+ if (combined.includes("number") || combined.includes("numeric")) return "number";
2800
+ if (combined.includes("date")) return "date";
2801
+ if (combined.includes("toggle") || combined.includes("check")) return "toggle";
2802
+ if (combined.includes("select") || combined.includes("choice")) return "select";
2803
+ return "text";
2804
+ }
2805
+ isFieldRequired(fieldName, rules) {
2806
+ const rule = rules[fieldName];
2807
+ return rule !== void 0 && rule.includes("required");
2808
+ }
2809
+ };
2810
+
2811
+ // src/pipeline/enrichment.ts
2812
+ function enrichScreenForAgent(screen) {
2813
+ const targets = mergeTargets([
2814
+ ...targetsFromForms(screen.forms),
2815
+ ...targetsFromActions(screen.actions),
2816
+ ...targetsFromCollections(screen.collections ?? [])
2817
+ ]);
2818
+ const flows = synthesizeFlows(screen, targets);
2819
+ const agentHints = synthesizeAgentHints(screen, targets, flows);
2820
+ return {
2821
+ ...screen,
2822
+ ...targets.length > 0 ? { targets } : {},
2823
+ ...flows.length > 0 ? { flows } : {},
2824
+ ...agentHints ? { agentHints } : {}
2825
+ };
2826
+ }
2827
+ function targetsFromForms(forms) {
2828
+ const targets = [];
2829
+ for (const form of forms) {
2830
+ for (const field of form.fields) {
2831
+ targets.push({
2832
+ id: field.locator?.id ?? field.name,
2833
+ role: fieldToRole(field),
2834
+ label: field.label,
2835
+ fieldName: field.name,
2836
+ locator: field.locator ?? { id: field.name, source: "inferred" },
2837
+ sourceComponent: field.sourceComponent
2838
+ });
2839
+ }
2840
+ if (form.submitAction && form.submitAction !== "anonymous") {
2841
+ targets.push({
2842
+ id: form.submitAction,
2843
+ role: "submit",
2844
+ actionId: form.submitAction,
2845
+ label: form.submitAction
2846
+ });
2847
+ }
2848
+ }
2849
+ return targets;
2850
+ }
2851
+ function fieldToRole(field) {
2852
+ if (field.type === "toggle") return "toggle";
2853
+ if (field.type === "select") return "select";
2854
+ if (field.type === "date") return "date";
2855
+ return "input";
2856
+ }
2857
+ function targetsFromActions(actions) {
2858
+ return actions.filter((action) => action.id).map((action) => ({
2859
+ id: action.locator?.id ?? action.id,
2860
+ role: action.type === "submit" ? "submit" : "button",
2861
+ label: action.label,
2862
+ locator: action.locator ?? { id: action.id, label: action.label, source: "inferred" },
2863
+ actionId: action.id,
2864
+ handler: action.handler,
2865
+ targetScreen: action.targetScreen,
2866
+ destructive: action.destructive,
2867
+ requiresConfirmation: action.requiresConfirmation,
2868
+ opensModal: action.opensModal,
2869
+ opensBottomSheet: action.opensBottomSheet,
2870
+ sourceComponent: action.sourceComponent
2871
+ }));
2872
+ }
2873
+ function targetsFromCollections(collections) {
2874
+ return collections.map((collection) => ({
2875
+ id: collection.id,
2876
+ role: "list",
2877
+ label: collection.itemType ?? collection.dataSource ?? collection.id,
2878
+ locator: { id: collection.id, testID: collection.id, source: "testID" },
2879
+ sourceComponent: collection.component
2880
+ }));
2881
+ }
2882
+ function mergeTargets(targets) {
2883
+ const byId = /* @__PURE__ */ new Map();
2884
+ for (const target of targets) {
2885
+ if (!target.id) continue;
2886
+ const existing = byId.get(target.id);
2887
+ byId.set(target.id, existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target);
2888
+ }
2889
+ return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
2890
+ }
2891
+ function synthesizeFlows(screen, targets) {
2892
+ const flows = [];
2893
+ for (const form of screen.forms) {
2894
+ const submitAction = screen.actions.find((action) => action.id === form.submitAction) ?? screen.actions.find((action) => action.type === "submit");
2895
+ if (!submitAction) continue;
2896
+ flows.push({
2897
+ id: `submit-${form.id}`,
2898
+ title: submitAction.label ? `Submit ${submitAction.label}` : `Submit ${form.id}`,
2899
+ intent: "form_submit",
2900
+ steps: [
2901
+ ...form.fields.map((field) => ({
2902
+ type: field.type === "select" ? "select" : field.type === "toggle" ? "toggle" : "fill",
2903
+ target: field.name,
2904
+ label: field.label,
2905
+ required: field.required,
2906
+ description: field.placeholder
2907
+ })),
2908
+ { type: "press", target: submitAction.id, label: submitAction.label },
2909
+ { type: "wait", description: describeWait(submitAction) }
2910
+ ],
2911
+ waitPolicy: waitPolicyForAction(submitAction),
2912
+ safetyNotes: submitAction.requiresConfirmation || submitAction.destructive ? ["Requires confirmation before execution"] : void 0
2913
+ });
2914
+ }
2915
+ for (const action of screen.actions) {
2916
+ if (action.type === "submit") continue;
2917
+ if (action.targetScreen) {
2918
+ flows.push({
2919
+ id: `navigate-${action.id}`,
2920
+ title: action.label ? `Open ${action.label}` : `Navigate to ${action.targetScreen}`,
2921
+ intent: "navigate",
2922
+ steps: [{ type: "press", target: action.id, label: action.label }],
2923
+ waitPolicy: waitPolicyForAction(action)
2924
+ });
2925
+ } else if (action.destructive || action.requiresConfirmation) {
2926
+ flows.push({
2927
+ id: `confirm-${action.id}`,
2928
+ title: action.label ? `Confirm ${action.label}` : `Confirm ${action.id}`,
2929
+ intent: "destructive_action",
2930
+ steps: [
2931
+ { type: "press", target: action.id, label: action.label },
2932
+ { type: "confirm", description: "Wait for native or custom confirmation before continuing" },
2933
+ { type: "wait", description: describeWait(action) }
2934
+ ],
2935
+ waitPolicy: waitPolicyForAction(action),
2936
+ safetyNotes: ["Destructive or high-risk action"]
2937
+ });
2938
+ }
2939
+ }
2940
+ for (const collection of screen.collections ?? []) {
2941
+ if (collection.rowAction || collection.rowActions && collection.rowActions.length > 0) {
2942
+ flows.push({
2943
+ id: `list-${collection.id}`,
2944
+ title: `Act on an item in ${collection.id}`,
2945
+ intent: "list_action",
2946
+ steps: [
2947
+ { type: "choose-list-item", target: collection.id, description: "Resolve the user reference to a visible or searchable row" },
2948
+ { type: "press", description: collection.rowAction?.description ?? "Open the row action" }
2949
+ ],
2950
+ waitPolicy: { expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback" }
2951
+ });
2952
+ }
2953
+ }
2954
+ return flows.sort((a, b) => a.id.localeCompare(b.id));
2955
+ }
2956
+ function waitPolicyForAction(action) {
2957
+ const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
2958
+ const signals = [action.successSignal, action.failureSignal].filter(Boolean);
2959
+ return {
2960
+ ...expectedOutcome ? { expectedOutcome } : {},
2961
+ ...signals && signals.length > 0 ? { signals } : {},
2962
+ ...action.appilotsInferred?.isAsyncTrigger ? { maxMs: 1e4 } : {}
2963
+ };
2964
+ }
2965
+ function describeWait(action) {
2966
+ if (action.successSignal?.description) return action.successSignal.description;
2967
+ if (action.successSignal?.type === "goBack") return "Wait for the app to return to the previous screen";
2968
+ if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
2969
+ if (action.appilotsInferred?.expectedOutcome) return `Wait for ${action.appilotsInferred.expectedOutcome}`;
2970
+ return "Wait for the UI to settle";
2971
+ }
2972
+ function synthesizeAgentHints(screen, targets, flows) {
2973
+ const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
2974
+ const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
2975
+ const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
2976
+ const firstAsyncAction = screen.actions.find((action) => action.appilotsInferred?.isAsyncTrigger);
2977
+ const hints = {
2978
+ primaryGoal: synthesizePrimaryGoal(screen),
2979
+ commonTasks,
2980
+ preferredTargets,
2981
+ ...firstAsyncAction ? { waitPolicy: waitPolicyForAction(firstAsyncAction) } : {},
2982
+ ...safetyNotes.length > 0 ? { safetyNotes } : {}
2983
+ };
2984
+ return hints.primaryGoal || commonTasks.length > 0 || preferredTargets.length > 0 ? hints : void 0;
2985
+ }
2986
+ function synthesizePrimaryGoal(screen) {
2987
+ const goals = [];
2988
+ const collections = screen.collections ?? [];
2989
+ if (collections.length > 0) {
2990
+ const collectionNames = collections.map((collection) => collection.itemType ?? collection.dataSource ?? collection.id).filter(Boolean).slice(0, 3);
2991
+ goals.push(
2992
+ collectionNames.length > 0 ? `Browse and act on ${collectionNames.join(", ")} list data` : "Browse and act on list data"
2993
+ );
2994
+ }
2995
+ const uniqueFields = /* @__PURE__ */ new Map();
2996
+ for (const form of screen.forms) {
2997
+ for (const field of form.fields) {
2998
+ uniqueFields.set(field.name, field);
2999
+ }
3000
+ }
3001
+ const fieldCount = uniqueFields.size;
3002
+ if (fieldCount > 0) {
3003
+ const requiredCount = Array.from(uniqueFields.values()).filter((field) => field.required).length;
3004
+ const fieldLabel = fieldCount === 1 ? "field" : "fields";
3005
+ goals.push(
3006
+ requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
3007
+ );
3008
+ }
3009
+ const navigationCount = screen.actions.filter((action) => action.targetScreen).length;
3010
+ if (navigationCount > 0) {
3011
+ goals.push(`Open ${navigationCount} related screen${navigationCount === 1 ? "" : "s"}`);
3012
+ }
3013
+ const submitCount = screen.actions.filter((action) => action.type === "submit").length;
3014
+ if (submitCount > 0) {
3015
+ goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
3016
+ }
3017
+ const asyncCount = screen.actions.filter((action) => action.appilotsInferred?.isAsyncTrigger).length;
3018
+ if (asyncCount > 0) {
3019
+ goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
3020
+ }
3021
+ if (goals.length > 0) return goals.join("; ");
3022
+ if (screen.title) return `Screen labeled "${screen.title}"`;
3023
+ return void 0;
3024
+ }
3025
+
3026
+ // src/generators/MCPGenerator.ts
3027
+ var import_fast_glob3 = __toESM(require("fast-glob"));
3028
+ var MCPGenerator = class {
3029
+ analyzerConfig;
3030
+ options;
3031
+ generatorConfig;
3032
+ constructor(config) {
3033
+ this.generatorConfig = config;
3034
+ this.analyzerConfig = {
3035
+ rootDir: config.rootDir,
3036
+ include: config.include ?? ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
3037
+ exclude: config.exclude ?? ["**/node_modules/**", "**/__tests__/**", "**/dist/**"]
3038
+ };
3039
+ this.options = {
3040
+ format: config.format ?? "json",
3041
+ outputDir: config.outputDir ?? ".appilots",
3042
+ includeSourcePaths: config.includeSourcePaths ?? false,
3043
+ splitOutput: config.splitOutput ?? false,
3044
+ version: config.version ?? "1.0"
3045
+ };
3046
+ }
3047
+ /** Generate MCP documents from the project */
3048
+ async generate() {
3049
+ console.log("[MCPGenerator] Starting generation...");
3050
+ const outputDir = this.options.outputDir || ".appilots";
3051
+ await (0, import_promises2.mkdir)(outputDir, { recursive: true });
3052
+ console.log(`[MCPGenerator] Output directory ensured: ${outputDir}`);
3053
+ const screenAnalyzer = new ScreenAnalyzer(this.analyzerConfig, {
3054
+ strictScreens: this.generatorConfig.strictScreens ?? true,
3055
+ screenPatterns: this.generatorConfig.screenPatterns
3056
+ });
3057
+ const navigationAnalyzer = new NavigationAnalyzer(this.analyzerConfig, {
3058
+ navigationInclude: this.generatorConfig.navigationInclude,
3059
+ navigationExclude: this.generatorConfig.navigationExclude
3060
+ });
3061
+ const componentAnalyzer = new ComponentAnalyzer(this.analyzerConfig);
3062
+ const formAnalyzer = new FormAnalyzer(this.analyzerConfig);
3063
+ console.log("[MCPGenerator] Running analyzers...");
3064
+ const [screens, navigation] = await Promise.all([
3065
+ screenAnalyzer.analyze(),
3066
+ navigationAnalyzer.analyze()
3067
+ ]);
3068
+ console.log(
3069
+ `[MCPGenerator] Screen and navigation analysis complete. Found ${screens.length} screens`
3070
+ );
3071
+ const screenFiles = await (0, import_fast_glob3.default)(
3072
+ this.analyzerConfig.include || ["**/*.tsx", "**/*.ts"],
3073
+ {
3074
+ cwd: this.analyzerConfig.rootDir,
3075
+ ignore: this.analyzerConfig.exclude || ["**/node_modules/**"]
3076
+ }
3077
+ );
3078
+ console.log(`[MCPGenerator] Analyzing components and forms from ${screenFiles.length} files...`);
3079
+ const enrichmentPromises = screenFiles.map(async (file) => {
3080
+ const filePath2 = import_node_path.default.resolve(this.analyzerConfig.rootDir, file);
3081
+ try {
3082
+ const [components, forms] = await Promise.all([
3083
+ componentAnalyzer.analyzeFile(filePath2),
3084
+ formAnalyzer.analyzeFile(filePath2)
3085
+ ]);
3086
+ return { filePath: filePath2, components, forms };
3087
+ } catch (error2) {
3088
+ console.warn(`[MCPGenerator] Failed to analyze ${file}:`, error2);
3089
+ return { filePath: filePath2, components: [], forms: [] };
3090
+ }
3091
+ });
3092
+ const enrichmentResults = await Promise.all(enrichmentPromises);
3093
+ const enrichmentMap = /* @__PURE__ */ new Map();
3094
+ for (const result of enrichmentResults) {
3095
+ enrichmentMap.set(result.filePath, {
3096
+ components: result.components,
3097
+ forms: result.forms
3098
+ });
3099
+ }
3100
+ const enrichedScreens = screens.map((screen) => {
3101
+ const enrichment = enrichmentMap.get(screen.filePath);
3102
+ if (enrichment) {
3103
+ const existingComponentNames = new Set(screen.components.map((c) => c.name));
3104
+ const newComponents = enrichment.components.filter(
3105
+ (c) => !existingComponentNames.has(c.name)
3106
+ );
3107
+ const mergedForms = screen.forms.map((form) => ({
3108
+ ...form,
3109
+ fields: [...form.fields]
3110
+ }));
3111
+ for (const newForm of enrichment.forms) {
3112
+ const existingForm = mergedForms.find((form) => form.id === newForm.id) ?? this.findFormWithSharedFields(mergedForms, newForm);
3113
+ if (existingForm) {
3114
+ this.mergeForm(existingForm, newForm);
3115
+ } else {
3116
+ mergedForms.push({
3117
+ ...newForm,
3118
+ fields: [...newForm.fields],
3119
+ submitAction: this.namedSubmitAction(newForm.submitAction)
3120
+ });
3121
+ }
3122
+ }
3123
+ return {
3124
+ ...screen,
3125
+ components: [...screen.components, ...newComponents],
3126
+ forms: mergedForms
3127
+ };
3128
+ }
3129
+ return screen;
3130
+ });
3131
+ const agentReadyScreens = enrichedScreens.map(enrichScreenForAgent);
3132
+ const projectInfo = await this.getProjectInfo();
3133
+ const projectName = projectInfo.name;
3134
+ console.log(`[MCPGenerator] Project name: ${projectName}`);
3135
+ const document = {
3136
+ version: this.options.version,
3137
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3138
+ projectName,
3139
+ screens: agentReadyScreens,
3140
+ navigation,
3141
+ metadata: {
3142
+ generatorVersion: "0.1.0",
3143
+ ...projectInfo.version ? { appVersion: projectInfo.version } : {},
3144
+ totalScreens: agentReadyScreens.length,
3145
+ totalForms: agentReadyScreens.reduce((acc, s) => acc + s.forms.length, 0),
3146
+ totalActions: agentReadyScreens.reduce((acc, s) => acc + s.actions.length, 0),
3147
+ analyzedFiles: screenFiles.length,
3148
+ // §D: Only set when strict mode filters out screens
3149
+ ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
3150
+ }
3151
+ };
3152
+ const serialized = JSON.stringify(document, null, 2);
3153
+ const checksum = this.calculateChecksum(serialized);
3154
+ const filePath = import_node_path.default.resolve(
3155
+ outputDir,
3156
+ `mcp-document.${this.options.format}`
3157
+ );
3158
+ await (0, import_promises2.writeFile)(filePath, serialized, "utf-8");
3159
+ console.log(`[MCPGenerator] Document written to: ${filePath}`);
3160
+ const checksumFilePath = import_node_path.default.resolve(outputDir, ".appilots-checksum");
3161
+ await (0, import_promises2.writeFile)(checksumFilePath, checksum, "utf-8");
3162
+ console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
3163
+ console.log("[MCPGenerator] Generation complete!");
3164
+ return {
3165
+ document,
3166
+ filePath,
3167
+ format: this.options.format,
3168
+ checksum
3169
+ };
3170
+ }
3171
+ /**
3172
+ * Read the previously stored checksum from disk.
3173
+ * Returns null if no checksum file exists (first run).
3174
+ *
3175
+ * §4: This replaces the incomplete `hasChanged` static method.
3176
+ * To detect actual changes, compare this value with `output.checksum`
3177
+ * after calling `generate()`.
3178
+ */
3179
+ static async readPreviousChecksum(outputDir) {
3180
+ const checksumFilePath = import_node_path.default.resolve(outputDir, ".appilots-checksum");
3181
+ try {
3182
+ const content = await (0, import_promises2.readFile)(checksumFilePath, "utf-8");
3183
+ return content.trim() || null;
3184
+ } catch {
3185
+ return null;
3186
+ }
3187
+ }
3188
+ /**
3189
+ * Calculate SHA-256 checksum of content
3190
+ */
3191
+ calculateChecksum(content) {
3192
+ return (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
3193
+ }
3194
+ mergeForm(target, source) {
3195
+ for (const field of source.fields) {
3196
+ const existingField = this.findEquivalentField(target.fields, field);
3197
+ if (existingField) {
3198
+ this.mergeField(existingField, field);
3199
+ } else {
3200
+ target.fields.push(field);
3201
+ }
3202
+ }
3203
+ target.submitAction = target.submitAction ?? this.namedSubmitAction(source.submitAction);
3204
+ target.validationRules = source.validationRules ? { ...source.validationRules, ...target.validationRules ?? {} } : target.validationRules;
3205
+ }
3206
+ findEquivalentField(fields, incoming) {
3207
+ return fields.find((field) => {
3208
+ if (field.name && incoming.name && field.name === incoming.name) return true;
3209
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
3210
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
3211
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
3212
+ if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
3213
+ return true;
3214
+ }
3215
+ return false;
3216
+ });
3217
+ }
3218
+ mergeField(target, source) {
3219
+ if (this.isWeakInferredFieldName(target.name) && !this.isWeakInferredFieldName(source.name)) {
3220
+ target.name = source.name;
3221
+ }
3222
+ target.label = target.label ?? source.label;
3223
+ target.placeholder = target.placeholder ?? source.placeholder;
3224
+ target.options = target.options ?? source.options;
3225
+ target.locator = target.locator ?? source.locator;
3226
+ target.sourceComponent = target.sourceComponent ?? source.sourceComponent;
3227
+ target.valueBinding = target.valueBinding ?? source.valueBinding;
3228
+ target.errorBinding = target.errorBinding ?? source.errorBinding;
3229
+ target.required = target.required || source.required;
3230
+ if (target.type === "text" && source.type !== "text") {
3231
+ target.type = source.type;
3232
+ }
3233
+ }
3234
+ namedSubmitAction(submitAction) {
3235
+ return submitAction && submitAction !== "anonymous" ? submitAction : void 0;
3236
+ }
3237
+ isWeakInferredFieldName(name) {
3238
+ return /^(text|value|input|query|search|selected|checked)$/i.test(name);
3239
+ }
3240
+ findFormWithSharedFields(forms, incoming) {
3241
+ if (incoming.fields.length === 0) return void 0;
3242
+ let best;
3243
+ for (const form of forms) {
3244
+ const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
3245
+ if (overlap > 0 && (!best || overlap > best.overlap)) {
3246
+ best = { form, overlap };
3247
+ }
3248
+ }
3249
+ return best?.form;
3250
+ }
3251
+ /**
3252
+ * Get project name and version from package.json in rootDir
3253
+ */
3254
+ async getProjectInfo() {
3255
+ try {
3256
+ const packageJsonPath = import_node_path.default.resolve(this.analyzerConfig.rootDir, "package.json");
3257
+ const packageJsonContent = await (0, import_promises2.readFile)(packageJsonPath, "utf-8");
3258
+ const packageJson = JSON.parse(packageJsonContent);
3259
+ return {
3260
+ name: packageJson.name || "Unknown Project",
3261
+ version: typeof packageJson.version === "string" ? packageJson.version : void 0
3262
+ };
3263
+ } catch (error2) {
3264
+ console.warn("[MCPGenerator] Failed to read package.json, using default project name");
3265
+ return { name: "Unknown Project" };
3266
+ }
3267
+ }
3268
+ };
3269
+
3270
+ // src/pipeline/metadata-lint.ts
3271
+ var MUTATION_VERB_RE = /\b(delete|destroy|remove|discard|wipe|revoke|cancel|reset|clear|create|add|register|save|submit|update|edit|change|toggle|pay|send|confirm|subscribe|unsubscribe|renew|downgrade|upgrade|transfer)\w*/i;
3272
+ function isMutating(action) {
3273
+ if (action.destructive || action.effect === "destructive") {
3274
+ return { mutating: true, reason: "flagged destructive" };
3275
+ }
3276
+ if (action.type === "submit" || action.type === "state_change") {
3277
+ return { mutating: true, reason: `type "${action.type}"` };
3278
+ }
3279
+ const haystack = `${action.id} ${action.label ?? ""} ${action.handler ?? ""}`;
3280
+ if ((action.type === "api_call" || action.type === "custom") && MUTATION_VERB_RE.test(haystack)) {
3281
+ return { mutating: true, reason: "mutation verb in id/label/handler" };
3282
+ }
3283
+ return { mutating: false, reason: "" };
3284
+ }
3285
+ function lintActionMetadata(document) {
3286
+ const warnings = [];
3287
+ for (const screen of document.screens) {
3288
+ for (const action of screen.actions ?? []) {
3289
+ const { mutating, reason } = isMutating(action);
3290
+ if (!mutating) continue;
3291
+ const missing = [];
3292
+ if (!action.effect) missing.push("effect");
3293
+ if (!action.riskLevel) missing.push("riskLevel");
3294
+ if (missing.length === 0) continue;
3295
+ warnings.push({
3296
+ screen: screen.name,
3297
+ actionId: action.id,
3298
+ actionType: action.type,
3299
+ missing,
3300
+ reason
3301
+ });
3302
+ }
3303
+ }
3304
+ return warnings;
3305
+ }
3306
+ function formatMetadataWarnings(warnings) {
3307
+ return warnings.map(
3308
+ (w) => `${w.screen}.${w.actionId} (${w.actionType}, ${w.reason}): missing ${w.missing.join(", ")}`
3309
+ );
3310
+ }
3311
+
3312
+ // src/cli/utils/metadata-report.ts
3313
+ function reportMetadataLint(document, strict) {
3314
+ const warnings = lintActionMetadata(document);
3315
+ if (warnings.length === 0) {
3316
+ success("Metadata lint: all mutating actions declare effect + riskLevel");
3317
+ return;
3318
+ }
3319
+ warn(`Metadata lint: ${warnings.length} mutating action(s) missing effect/riskLevel`);
3320
+ for (const line of formatMetadataWarnings(warnings)) {
3321
+ dim(` ${line}`);
3322
+ }
3323
+ dim(' Declare them in registerScreen(): actions: [{ id, effect: "write" | "destructive", riskLevel: "low" | "medium" | "high" }]');
3324
+ if (strict) {
3325
+ error("--strict-metadata: failing due to missing action metadata");
3326
+ process.exit(1);
3327
+ }
3328
+ }
3329
+
3330
+ // src/cli/commands/sync.ts
3331
+ function syncCommand() {
3332
+ return new import_commander2.Command("sync").description("Analyze project and sync MCP documents with Appilots").option("--verbose", "Enable verbose logging", false).option("--force", "Force upload even if nothing changed", false).option("--api-key <key>", "Appilots API key (overrides .appilotsrc and APPILOTS_API_KEY)").option("--project-id <id>", "Project ID (overrides .appilotsrc and APPILOTS_PROJECT_ID)").option("--server <url>", "Appilots server URL (overrides .appilotsrc and APPILOTS_SERVER_URL)").option("--json", "Output a machine-readable JSON result (implies --quiet)", false).option("--quiet", "Suppress banner and informational output", false).option("--strict-metadata", "Fail when mutating actions are missing effect/riskLevel metadata", false).action(async (options) => {
3333
+ if (options.json) setLogMode("json");
3334
+ else if (options.quiet) setLogMode("quiet");
3335
+ try {
3336
+ banner();
3337
+ const loaded = loadConfig();
3338
+ const apiKey = options.apiKey || loaded?.apiKey;
3339
+ if (!apiKey) {
3340
+ error("No .appilotsrc configuration found");
3341
+ info("Run: appilots init --api-key <your-key>");
3342
+ info("Or set APPILOTS_API_KEY, or pass --api-key");
3343
+ jsonOutput({ success: false, error: "No configuration found (missing API key)" });
3344
+ process.exit(1);
3345
+ return;
3346
+ }
3347
+ const config = {
3348
+ serverUrl: "http://localhost:4000",
3349
+ outputDir: ".appilots",
3350
+ ...loaded,
3351
+ apiKey,
3352
+ ...options.projectId ? { projectId: options.projectId } : {},
3353
+ ...options.server ? { serverUrl: options.server } : {}
3354
+ };
3355
+ const apiClient = new AppilotsAPIClient({
3356
+ serverUrl: config.serverUrl,
3357
+ apiKey: config.apiKey
3358
+ });
3359
+ const outputDir = config.outputDir || ".appilots";
3360
+ let previousChecksum = "";
3361
+ try {
3362
+ const checksumPath = (0, import_node_path2.join)(outputDir, ".appilots-checksum");
3363
+ previousChecksum = (await (0, import_promises3.readFile)(checksumPath, "utf-8")).trim();
3364
+ } catch {
3365
+ }
3366
+ let spinner = createSpinner("Analyzing project...");
3367
+ spinner.start();
3368
+ const generator = new MCPGenerator({
3369
+ rootDir: process.cwd(),
3370
+ outputDir,
3371
+ format: "json",
3372
+ strictScreens: config.strictScreens,
3373
+ screenPatterns: config.screenPatterns,
3374
+ include: config.include,
3375
+ exclude: config.exclude,
3376
+ navigationInclude: config.navigationInclude,
3377
+ navigationExclude: config.navigationExclude
3378
+ });
3379
+ const output = await generator.generate();
3380
+ spinner.stop();
3381
+ success(
3382
+ `Found ${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
3383
+ );
3384
+ reportMetadataLint(output.document, Boolean(options.strictMetadata));
3385
+ if (!options.force && previousChecksum && previousChecksum === output.checksum) {
3386
+ spinner = createSpinner("Checking remote status...");
3387
+ spinner.start();
3388
+ const status = await apiClient.status();
3389
+ spinner.stop();
3390
+ if (status.error) {
3391
+ warn(`Could not verify remote: ${status.error}`);
3392
+ info("Uploading anyway...");
3393
+ } else if (status.activeMcp) {
3394
+ success("Already up to date (no local or remote changes)");
3395
+ jsonOutput({ success: true, unchanged: true, checksum: output.checksum });
3396
+ return;
3397
+ }
3398
+ }
3399
+ spinner = createSpinner("Uploading to Appilots...");
3400
+ spinner.start();
3401
+ const syncResult = await apiClient.sync(
3402
+ output.document,
3403
+ output.document.version,
3404
+ output.document.metadata.appVersion
3405
+ );
3406
+ spinner.stop();
3407
+ if (!syncResult.success) {
3408
+ error(`Sync failed: ${syncResult.error}`);
3409
+ jsonOutput({ success: false, error: syncResult.error });
3410
+ process.exit(1);
3411
+ }
3412
+ if (syncResult.unchanged && !options.force) {
3413
+ success("Already up to date on server");
3414
+ } else {
3415
+ success("Successfully synced MCP document");
3416
+ info(`Document ID: ${syncResult.id}`);
3417
+ info(`Checksum: ${syncResult.checksum}`);
3418
+ if (config.projectId) {
3419
+ const dashboardUrl = `${config.serverUrl}/dashboard/projects/${config.projectId}`;
3420
+ info(`View on dashboard: ${dashboardUrl}`);
3421
+ }
3422
+ }
3423
+ if (options.verbose) {
3424
+ heading("\nSync Details:");
3425
+ table([
3426
+ ["Screens", String(syncResult.screensCount || 0)],
3427
+ ["Forms", String(syncResult.formsCount || 0)],
3428
+ ["Actions", String(syncResult.actionsCount || 0)],
3429
+ ["Document ID", syncResult.id || "N/A"],
3430
+ ["Previous checksum", previousChecksum || "(first run)"],
3431
+ ["New checksum", output.checksum],
3432
+ ["Changed", String(previousChecksum !== output.checksum)]
3433
+ ]);
3434
+ }
3435
+ jsonOutput({
3436
+ success: true,
3437
+ unchanged: Boolean(syncResult.unchanged),
3438
+ id: syncResult.id,
3439
+ checksum: syncResult.checksum ?? output.checksum,
3440
+ screensCount: syncResult.screensCount ?? output.document.metadata.totalScreens,
3441
+ formsCount: syncResult.formsCount ?? output.document.metadata.totalForms,
3442
+ actionsCount: syncResult.actionsCount ?? output.document.metadata.totalActions
3443
+ });
3444
+ } catch (err) {
3445
+ error(`Sync failed: ${err instanceof Error ? err.message : String(err)}`);
3446
+ jsonOutput({ success: false, error: err instanceof Error ? err.message : String(err) });
3447
+ if (options.verbose) {
3448
+ console.error(err);
3449
+ }
3450
+ process.exit(1);
3451
+ }
3452
+ });
3453
+ }
3454
+
3455
+ // src/cli/commands/watch.ts
3456
+ var import_commander3 = require("commander");
3457
+ var import_node_fs = require("fs");
3458
+ function watchCommand() {
3459
+ return new import_commander3.Command("watch").description("Watch project for changes and auto-sync MCP documents").option("--verbose", "Enable verbose logging", false).action(async (options) => {
3460
+ try {
3461
+ banner();
3462
+ const config = loadConfig();
3463
+ if (!config) {
3464
+ error("No .appilotsrc configuration found");
3465
+ info("Run: appilots init --api-key <your-key>");
3466
+ process.exit(1);
3467
+ }
3468
+ const apiClient = new AppilotsAPIClient({
3469
+ serverUrl: config.serverUrl,
3470
+ apiKey: config.apiKey
3471
+ });
3472
+ info("Performing initial sync...");
3473
+ const generatorConfig = {
3474
+ rootDir: process.cwd(),
3475
+ outputDir: config.outputDir || ".appilots",
3476
+ format: "json",
3477
+ strictScreens: config.strictScreens,
3478
+ screenPatterns: config.screenPatterns,
3479
+ include: config.include,
3480
+ exclude: config.exclude,
3481
+ navigationInclude: config.navigationInclude,
3482
+ navigationExclude: config.navigationExclude
3483
+ };
3484
+ const generator = new MCPGenerator(generatorConfig);
3485
+ const initialOutput = await generator.generate();
3486
+ const initialSync = await apiClient.sync(initialOutput.document, initialOutput.document.version);
3487
+ if (!initialSync.success) {
3488
+ error(`Initial sync failed: ${initialSync.error}`);
3489
+ process.exit(1);
3490
+ }
3491
+ success("Initial sync completed");
3492
+ info(`Watching for changes in ${config.outputDir || ".appilots"}...`);
3493
+ dim("Press Ctrl+C to stop\n");
3494
+ let debounceTimer = null;
3495
+ const DEBOUNCE_MS = 500;
3496
+ const watcher = (0, import_node_fs.watch)(
3497
+ process.cwd(),
3498
+ { recursive: true },
3499
+ async (eventType, filename) => {
3500
+ if (!filename) return;
3501
+ if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
3502
+ return;
3503
+ }
3504
+ if (debounceTimer) {
3505
+ clearTimeout(debounceTimer);
3506
+ }
3507
+ debounceTimer = setTimeout(async () => {
3508
+ try {
3509
+ if (options.verbose) {
3510
+ info(`Change detected: ${filename}`);
3511
+ }
3512
+ const spinner = createSpinner("Re-analyzing project...");
3513
+ spinner.start();
3514
+ const newGenerator = new MCPGenerator(generatorConfig);
3515
+ const output = await newGenerator.generate();
3516
+ spinner.text = "Uploading changes...";
3517
+ const syncResult = await apiClient.sync(output.document, output.document.version);
3518
+ spinner.stop();
3519
+ if (!syncResult.success) {
3520
+ error(`Sync failed: ${syncResult.error}`);
3521
+ } else if (syncResult.unchanged) {
3522
+ info("Changes synced (no content changes detected)");
3523
+ } else {
3524
+ success("Changes synced successfully");
3525
+ info(
3526
+ `${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
3527
+ );
3528
+ }
3529
+ } catch (err) {
3530
+ error(`Watch sync failed: ${err instanceof Error ? err.message : String(err)}`);
3531
+ }
3532
+ }, DEBOUNCE_MS);
3533
+ }
3534
+ );
3535
+ process.on("SIGINT", () => {
3536
+ info("Stopping watcher...");
3537
+ watcher.close();
3538
+ if (debounceTimer) {
3539
+ clearTimeout(debounceTimer);
3540
+ }
3541
+ process.exit(0);
3542
+ });
3543
+ } catch (err) {
3544
+ error(`Watch failed: ${err instanceof Error ? err.message : String(err)}`);
3545
+ process.exit(1);
3546
+ }
3547
+ });
3548
+ }
3549
+
3550
+ // src/cli/commands/generate.ts
3551
+ var import_commander4 = require("commander");
3552
+ function generateCommand() {
3553
+ return new import_commander4.Command("generate").description("Generate MCP documents from project source (local only)").option("-o, --output <path>", "Output directory for MCP documents", ".appilots").option("-f, --format <format>", "Output format (only json supported)", "json").option("--strict-metadata", "Fail when mutating actions are missing effect/riskLevel metadata", false).action(async (options) => {
3554
+ try {
3555
+ banner();
3556
+ if (options.format && options.format !== "json") {
3557
+ error(`Format "${options.format}" is not supported yet. Only "json" is available.`);
3558
+ process.exit(1);
3559
+ }
3560
+ const spinner = createSpinner("Analyzing project...");
3561
+ spinner.start();
3562
+ const generator = new MCPGenerator({
3563
+ rootDir: process.cwd(),
3564
+ outputDir: options.output || ".appilots",
3565
+ format: "json"
3566
+ });
3567
+ const output = await generator.generate();
3568
+ spinner.stop();
3569
+ success(
3570
+ `Generated MCP document with ${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
3571
+ );
3572
+ info(`Written to: ${output.filePath}`);
3573
+ info(`Checksum: ${output.checksum}`);
3574
+ reportMetadataLint(output.document, Boolean(options.strictMetadata));
3575
+ } catch (err) {
3576
+ error(`Generation failed: ${err instanceof Error ? err.message : String(err)}`);
3577
+ process.exit(1);
3578
+ }
3579
+ });
3580
+ }
3581
+
3582
+ // src/cli/commands/status.ts
3583
+ var import_commander5 = require("commander");
3584
+ function statusCommand() {
3585
+ return new import_commander5.Command("status").description("Show current project and MCP document status").option("--json", "Output a machine-readable JSON result (implies --quiet)", false).option("--quiet", "Suppress banner and informational output", false).action(async (options) => {
3586
+ if (options.json) setLogMode("json");
3587
+ else if (options.quiet) setLogMode("quiet");
3588
+ try {
3589
+ banner();
3590
+ const config = loadConfig();
3591
+ if (!config) {
3592
+ error("No .appilotsrc configuration found");
3593
+ info("Run: appilots init --api-key <your-key>");
3594
+ jsonOutput({ success: false, error: "No .appilotsrc configuration found" });
3595
+ process.exit(1);
3596
+ return;
3597
+ }
3598
+ const apiClient = new AppilotsAPIClient({
3599
+ serverUrl: config.serverUrl,
3600
+ apiKey: config.apiKey
3601
+ });
3602
+ const status = await apiClient.status();
3603
+ if (status.error) {
3604
+ error(`Failed to fetch status: ${status.error}`);
3605
+ jsonOutput({ success: false, error: status.error });
3606
+ process.exit(1);
3607
+ }
3608
+ heading("\n\u{1F4CB} Local Configuration:");
3609
+ table([
3610
+ ["API Server", config.serverUrl],
3611
+ ["Project ID", config.projectId || "(resolved from API key)"],
3612
+ ["Output Directory", config.outputDir || ".appilots"],
3613
+ ["Auto Activate", String(config.autoActivate ?? true)]
3614
+ ]);
3615
+ if (status.project) {
3616
+ heading("\n\u{1F4CC} Project Info:");
3617
+ table([["Name", status.project.name], ["ID", status.project.id]]);
3618
+ }
3619
+ if (status.activeMcp) {
3620
+ heading("\n\u2705 Active MCP Document:");
3621
+ table([
3622
+ ["Document ID", status.activeMcp.id],
3623
+ ["Version", status.activeMcp.version],
3624
+ ["Screens", String(status.activeMcp.screensCount)],
3625
+ ["Forms", String(status.activeMcp.formsCount)],
3626
+ ["Actions", String(status.activeMcp.actionsCount)]
3627
+ ]);
3628
+ } else {
3629
+ heading("\n\u26A0\uFE0F No active MCP document");
3630
+ info("Run: appilots sync");
3631
+ }
3632
+ success("Status retrieved successfully");
3633
+ jsonOutput({
3634
+ success: true,
3635
+ local: {
3636
+ serverUrl: config.serverUrl,
3637
+ projectId: config.projectId || null,
3638
+ outputDir: config.outputDir || ".appilots"
3639
+ },
3640
+ project: status.project ?? null,
3641
+ activeMcp: status.activeMcp ?? null
3642
+ });
3643
+ } catch (err) {
3644
+ error(`Status check failed: ${err instanceof Error ? err.message : String(err)}`);
3645
+ jsonOutput({ success: false, error: err instanceof Error ? err.message : String(err) });
3646
+ process.exit(1);
3647
+ }
3648
+ });
3649
+ }
3650
+
3651
+ // src/cli/commands/eval.ts
3652
+ var import_commander6 = require("commander");
3653
+ var import_fs9 = require("fs");
3654
+ var import_path7 = require("path");
3655
+
3656
+ // src/eval/scenario-loader.ts
3657
+ var import_fs7 = require("fs");
3658
+ var import_path5 = require("path");
3659
+ function validateScenarioShape(raw) {
3660
+ if (!raw || typeof raw !== "object") return "scenario must be a JSON object";
3661
+ const s = raw;
3662
+ if (typeof s.name !== "string" || !s.name.trim()) {
3663
+ return '"name" is required and must be a non-empty string';
3664
+ }
3665
+ if (typeof s.when !== "string" || !s.when.trim()) {
3666
+ return '"when" is required and must be a non-empty string (the user prompt)';
3667
+ }
3668
+ if (!s.expect || typeof s.expect !== "object") {
3669
+ return '"expect" is required and must be an object (see docs: Testing your agent between releases)';
3670
+ }
3671
+ return null;
3672
+ }
3673
+ function loadScenarios(dir) {
3674
+ const scenarios = [];
3675
+ const errors = [];
3676
+ let files;
3677
+ try {
3678
+ files = (0, import_fs7.readdirSync)(dir).filter((f) => f.endsWith(".json")).sort();
3679
+ } catch {
3680
+ return { scenarios, errors };
3681
+ }
3682
+ for (const file of files) {
3683
+ const full = (0, import_path5.join)(dir, file);
3684
+ let raw;
3685
+ try {
3686
+ raw = JSON.parse((0, import_fs7.readFileSync)(full, "utf-8"));
3687
+ } catch (err) {
3688
+ errors.push({ file, error: `invalid JSON: ${err instanceof Error ? err.message : String(err)}` });
3689
+ continue;
3690
+ }
3691
+ const shapeError = validateScenarioShape(raw);
3692
+ if (shapeError) {
3693
+ errors.push({ file, error: shapeError });
3694
+ continue;
3695
+ }
3696
+ scenarios.push({ file, scenario: raw });
3697
+ }
3698
+ return { scenarios, errors };
3699
+ }
3700
+
3701
+ // src/eval/matcher.ts
3702
+ function normalizeReply(text) {
3703
+ return text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
3704
+ }
3705
+ function partialMatch(expected, actual) {
3706
+ if (!expected) return true;
3707
+ if (!actual) return false;
3708
+ for (const key of Object.keys(expected)) {
3709
+ const exp = expected[key];
3710
+ const act = actual[key];
3711
+ if (key === "fields" && Array.isArray(exp) && Array.isArray(act)) {
3712
+ for (const expField of exp) {
3713
+ const fieldId = expField.fieldId;
3714
+ const matched = act.find(
3715
+ (f) => f.fieldId === fieldId
3716
+ );
3717
+ if (!matched) return false;
3718
+ if (expField.value !== void 0 && matched.value !== expField.value) {
3719
+ return false;
3720
+ }
3721
+ }
3722
+ continue;
3723
+ }
3724
+ if (typeof exp === "object" && exp !== null && !Array.isArray(exp)) {
3725
+ if (typeof act !== "object" || act === null) return false;
3726
+ if (!partialMatch(exp, act)) {
3727
+ return false;
3728
+ }
3729
+ continue;
3730
+ }
3731
+ if (exp !== act) return false;
3732
+ }
3733
+ return true;
3734
+ }
3735
+ function matchesOne(expected, actual) {
3736
+ if (expected.type !== actual.type) return false;
3737
+ return partialMatch(expected.payload, actual.payload);
3738
+ }
3739
+ function matchActions(expected, actual, reply = "") {
3740
+ if (expected.anyOf && expected.anyOf.length > 0) {
3741
+ const reasons = [];
3742
+ for (const branch of expected.anyOf) {
3743
+ const res = matchActions(branch, actual, reply);
3744
+ if (res.ok) return res;
3745
+ reasons.push(res.reason ?? "no reason");
3746
+ }
3747
+ return {
3748
+ ok: false,
3749
+ reason: `anyOf: no branch matched \u2014 ${reasons.map((r, i) => `[${i}] ${r}`).join(" \xB7 ")}`
3750
+ };
3751
+ }
3752
+ if (expected.forbidActionTypes && expected.forbidActionTypes.length > 0) {
3753
+ const forbidden = actual.find((a) => expected.forbidActionTypes.includes(a.type));
3754
+ if (forbidden) {
3755
+ return {
3756
+ ok: false,
3757
+ reason: `forbidActionTypes: action of type=${forbidden.type} was emitted`
3758
+ };
3759
+ }
3760
+ }
3761
+ if (expected.replyMustNotContain && expected.replyMustNotContain.length > 0) {
3762
+ const hay = normalizeReply(reply);
3763
+ const leaked = expected.replyMustNotContain.find(
3764
+ (needle) => hay.includes(normalizeReply(needle))
3765
+ );
3766
+ if (leaked) {
3767
+ return {
3768
+ ok: false,
3769
+ reason: `replyMustNotContain: "${leaked}" was echoed in the reply "${reply.slice(0, 120)}"`
3770
+ };
3771
+ }
3772
+ }
3773
+ if (expected.replyMustContain && expected.replyMustContain.length > 0) {
3774
+ const hay = normalizeReply(reply);
3775
+ const hit = expected.replyMustContain.some((needle) => hay.includes(normalizeReply(needle)));
3776
+ if (!hit) {
3777
+ return {
3778
+ ok: false,
3779
+ reason: `replyMustContain: none of [${expected.replyMustContain.join(", ")}] found in reply "${reply.slice(0, 120)}"`
3780
+ };
3781
+ }
3782
+ }
3783
+ const mode2 = expected.mode ?? "any";
3784
+ const expectedActions = expected.actions ?? [];
3785
+ if (mode2 === "none") {
3786
+ if (actual.length === 0) return { ok: true };
3787
+ return {
3788
+ ok: false,
3789
+ reason: `none mode: expected 0 actions, got ${actual.length} (${actual.map((a) => a.type).join(", ")})`
3790
+ };
3791
+ }
3792
+ if (mode2 === "exact") {
3793
+ if (actual.length !== expectedActions.length) {
3794
+ return {
3795
+ ok: false,
3796
+ reason: `exact mode: expected ${expectedActions.length} action(s), got ${actual.length}`
3797
+ };
3798
+ }
3799
+ for (let i = 0; i < expectedActions.length; i++) {
3800
+ if (!matchesOne(expectedActions[i], actual[i])) {
3801
+ return {
3802
+ ok: false,
3803
+ reason: `exact mode: action[${i}] mismatch \u2014 expected type=${expectedActions[i].type}, got type=${actual[i].type}`
3804
+ };
3805
+ }
3806
+ }
3807
+ return { ok: true };
3808
+ }
3809
+ if (mode2 === "first") {
3810
+ const first = actual[0];
3811
+ if (!first) {
3812
+ return { ok: false, reason: "first mode: no actions emitted" };
3813
+ }
3814
+ if (!expectedActions[0] || !matchesOne(expectedActions[0], first)) {
3815
+ return {
3816
+ ok: false,
3817
+ reason: `first mode: first action did not match \u2014 expected type=${expectedActions[0]?.type}, got type=${first.type}`
3818
+ };
3819
+ }
3820
+ for (const exp of expectedActions.slice(1)) {
3821
+ if (!actual.some((a) => matchesOne(exp, a))) {
3822
+ return {
3823
+ ok: false,
3824
+ reason: `first mode: expected action of type=${exp.type} not found in remaining actions`
3825
+ };
3826
+ }
3827
+ }
3828
+ return { ok: true };
3829
+ }
3830
+ for (const exp of expectedActions) {
3831
+ if (!actual.some((a) => matchesOne(exp, a))) {
3832
+ return {
3833
+ ok: false,
3834
+ reason: `any mode: expected action of type=${exp.type} not found`
3835
+ };
3836
+ }
3837
+ }
3838
+ return { ok: true };
3839
+ }
3840
+
3841
+ // src/eval/baseline.ts
3842
+ var import_fs8 = require("fs");
3843
+ var import_path6 = require("path");
3844
+ var EMPTY_BASELINE = { generatedAt: null, gitSha: null, perScenario: {} };
3845
+ function loadBaseline(path5) {
3846
+ if (!(0, import_fs8.existsSync)(path5)) return { ...EMPTY_BASELINE, perScenario: {} };
3847
+ try {
3848
+ const raw = JSON.parse((0, import_fs8.readFileSync)(path5, "utf-8"));
3849
+ return {
3850
+ generatedAt: raw.generatedAt ?? null,
3851
+ gitSha: raw.gitSha ?? null,
3852
+ perScenario: raw.perScenario ?? {}
3853
+ };
3854
+ } catch {
3855
+ return { ...EMPTY_BASELINE, perScenario: {} };
3856
+ }
3857
+ }
3858
+ function saveBaseline(path5, baseline) {
3859
+ (0, import_fs8.mkdirSync)((0, import_path6.dirname)(path5), { recursive: true });
3860
+ (0, import_fs8.writeFileSync)(path5, `${JSON.stringify(baseline, null, 2)}
3861
+ `, "utf-8");
3862
+ }
3863
+ function buildBaseline(outcomes, meta) {
3864
+ const perScenario = {};
3865
+ for (const o of outcomes) {
3866
+ perScenario[o.name] = {
3867
+ status: o.status,
3868
+ totalTokens: o.totalTokens,
3869
+ ...o.failureReason ? { failureReason: o.failureReason } : {}
3870
+ };
3871
+ }
3872
+ return { generatedAt: meta.generatedAt, gitSha: meta.gitSha ?? null, perScenario };
3873
+ }
3874
+ function diffAgainstBaseline(outcomes, baseline, maxTokenRegression) {
3875
+ const regressions = [];
3876
+ for (const outcome of outcomes) {
3877
+ const prior = baseline.perScenario[outcome.name];
3878
+ if (!prior) continue;
3879
+ if (outcome.status === "failed" && prior.status === "passed") {
3880
+ regressions.push({
3881
+ name: outcome.name,
3882
+ kind: "newly-failing",
3883
+ detail: outcome.failureReason ?? "scenario now fails (previously passed)"
3884
+ });
3885
+ continue;
3886
+ }
3887
+ if (outcome.status === "passed" && prior.status === "passed" && prior.totalTokens > 0 && outcome.totalTokens > prior.totalTokens * (1 + maxTokenRegression)) {
3888
+ const pct = Math.round((outcome.totalTokens - prior.totalTokens) / prior.totalTokens * 100);
3889
+ regressions.push({
3890
+ name: outcome.name,
3891
+ kind: "token-regression",
3892
+ detail: `tokens ${prior.totalTokens} \u2192 ${outcome.totalTokens} (+${pct}%, threshold +${Math.round(maxTokenRegression * 100)}%)`
3893
+ });
3894
+ }
3895
+ }
3896
+ return regressions;
3897
+ }
3898
+
3899
+ // src/eval/scaffold.ts
3900
+ var EXAMPLE_SCENARIO = {
3901
+ name: "agent-fills-signup-form",
3902
+ description: "The agent should fill the visible signup form fields from a single spoken instruction. Edit `given.observation` to match a real screen in your app, and `when` to a prompt your users would actually say.",
3903
+ given: {
3904
+ screen: "SignupScreen",
3905
+ observation: {
3906
+ route: "SignupScreen",
3907
+ texts: ["Create your account"],
3908
+ inputs: [
3909
+ { id: "email", label: "Email", required: true },
3910
+ { id: "name", label: "Full name", required: true }
3911
+ ],
3912
+ buttons: [{ id: "btn-submit-signup", label: "Sign up" }]
3913
+ }
3914
+ },
3915
+ when: "My name is Jane Doe and my email is jane@example.com",
3916
+ expect: {
3917
+ mode: "any",
3918
+ actions: [
3919
+ {
3920
+ type: "form_fill",
3921
+ payload: {
3922
+ fields: [{ fieldId: "name" }, { fieldId: "email" }]
3923
+ }
3924
+ }
3925
+ ]
3926
+ }
3927
+ };
3928
+
3929
+ // src/cli/commands/eval.ts
3930
+ var DEFAULT_MIN_PASS_RATE = 0.95;
3931
+ var DEFAULT_MAX_TOKEN_REGRESSION = 0.25;
3932
+ function parseRate(raw, fallback, flagName) {
3933
+ if (raw === void 0) return fallback;
3934
+ const n = Number(raw);
3935
+ if (Number.isNaN(n)) {
3936
+ warn(`Ignoring invalid ${flagName} value "${raw}" \u2014 using ${fallback}`);
3937
+ return fallback;
3938
+ }
3939
+ return n;
3940
+ }
3941
+ function evalCommand() {
3942
+ return new import_commander6.Command("eval").description(
3943
+ "Run agent eval scenarios against the current MCP document in dry-run mode and check for regressions vs a committed baseline"
3944
+ ).option("--init", "Scaffold an example scenario in the scenarios directory", false).option("--force", "With --init, overwrite an existing example scenario", false).option("--dir <path>", "Scenarios directory (default: <outputDir>/scenarios)").option("--baseline <path>", "Baseline file path (default: <outputDir>/eval-baseline.json)").option(
3945
+ "--update-baseline",
3946
+ "Record this run as the new baseline (refuses if pass rate is below the threshold)",
3947
+ false
3948
+ ).option("--min-pass-rate <n>", "Minimum fraction of scenarios that must pass, 0-1 (default: 0.95)").option(
3949
+ "--max-token-regression <n>",
3950
+ "Max fractional token increase before a passing scenario is flagged, e.g. 0.25 for +25% (default: 0.25)"
3951
+ ).option("--model <id>", "Model override for scenarios that do not set their own modelOverride").option("--api-key <key>", "Appilots API key (overrides .appilotsrc and APPILOTS_API_KEY)").option("--project-id <id>", "Project ID (overrides .appilotsrc and APPILOTS_PROJECT_ID)").option("--server <url>", "Appilots server URL (overrides .appilotsrc and APPILOTS_SERVER_URL)").option("--json", "Output a machine-readable JSON result (implies --quiet)", false).option("--quiet", "Suppress banner and informational output", false).action(async (options) => {
3952
+ if (options.json) setLogMode("json");
3953
+ else if (options.quiet) setLogMode("quiet");
3954
+ try {
3955
+ banner();
3956
+ const loaded = loadConfig();
3957
+ const outputDir = loaded?.outputDir || ".appilots";
3958
+ const scenariosDir = options.dir || loaded?.eval?.scenariosDir || (0, import_path7.join)(outputDir, "scenarios");
3959
+ if (options.init) {
3960
+ (0, import_fs9.mkdirSync)(scenariosDir, { recursive: true });
3961
+ const examplePath = (0, import_path7.join)(scenariosDir, "example.json");
3962
+ if ((0, import_fs9.existsSync)(examplePath) && !options.force) {
3963
+ warn(`${examplePath} already exists \u2014 pass --force to overwrite`);
3964
+ } else {
3965
+ (0, import_fs9.writeFileSync)(examplePath, `${JSON.stringify(EXAMPLE_SCENARIO, null, 2)}
3966
+ `, "utf-8");
3967
+ success(`Wrote example scenario to ${examplePath}`);
3968
+ }
3969
+ info("Edit it to match a real screen in your app, then:");
3970
+ dim(" $ appilots eval --update-baseline # capture the current (good) behavior");
3971
+ dim(" $ appilots eval # check for regressions on future runs");
3972
+ jsonOutput({ success: true, scenariosDir, examplePath });
3973
+ return;
3974
+ }
3975
+ const apiKey = options.apiKey || loaded?.apiKey;
3976
+ if (!apiKey) {
3977
+ error("No .appilotsrc configuration found");
3978
+ info("Run: appilots init --api-key <your-key>");
3979
+ info("Or set APPILOTS_API_KEY, or pass --api-key");
3980
+ jsonOutput({ success: false, error: "No configuration found (missing API key)" });
3981
+ process.exit(1);
3982
+ return;
3983
+ }
3984
+ const config = {
3985
+ serverUrl: "http://localhost:4000",
3986
+ ...loaded,
3987
+ apiKey,
3988
+ ...options.projectId ? { projectId: options.projectId } : {},
3989
+ ...options.server ? { serverUrl: options.server } : {}
3990
+ };
3991
+ const baselinePath = options.baseline || loaded?.eval?.baselinePath || (0, import_path7.join)(outputDir, "eval-baseline.json");
3992
+ const minPassRate = parseRate(
3993
+ options.minPassRate,
3994
+ loaded?.eval?.minPassRate ?? DEFAULT_MIN_PASS_RATE,
3995
+ "--min-pass-rate"
3996
+ );
3997
+ const maxTokenRegression = parseRate(
3998
+ options.maxTokenRegression,
3999
+ loaded?.eval?.maxTokenRegression ?? DEFAULT_MAX_TOKEN_REGRESSION,
4000
+ "--max-token-regression"
4001
+ );
4002
+ const loadedScenarios = loadScenarios(scenariosDir);
4003
+ if (loadedScenarios.errors.length > 0) {
4004
+ for (const scenarioError of loadedScenarios.errors) {
4005
+ error(`${scenarioError.file}: ${scenarioError.error}`);
4006
+ }
4007
+ jsonOutput({
4008
+ success: false,
4009
+ error: "One or more scenario files failed to load",
4010
+ details: loadedScenarios.errors
4011
+ });
4012
+ process.exit(1);
4013
+ return;
4014
+ }
4015
+ if (loadedScenarios.scenarios.length === 0) {
4016
+ error(`No scenarios found in ${scenariosDir}`);
4017
+ info("Run: appilots eval --init");
4018
+ jsonOutput({ success: false, error: `No scenarios found in ${scenariosDir}` });
4019
+ process.exit(1);
4020
+ return;
4021
+ }
4022
+ const nameCounts = /* @__PURE__ */ new Map();
4023
+ for (const { scenario } of loadedScenarios.scenarios) {
4024
+ nameCounts.set(scenario.name, (nameCounts.get(scenario.name) ?? 0) + 1);
4025
+ }
4026
+ const duplicates = [...nameCounts.entries()].filter(([, count]) => count > 1).map(([name]) => name);
4027
+ if (duplicates.length > 0) {
4028
+ error(
4029
+ `Duplicate scenario name(s) \u2014 "name" must be unique across all scenario files: ${duplicates.join(", ")}`
4030
+ );
4031
+ jsonOutput({ success: false, error: `Duplicate scenario names: ${duplicates.join(", ")}` });
4032
+ process.exit(1);
4033
+ return;
4034
+ }
4035
+ const apiClient = new AppilotsAPIClient({ serverUrl: config.serverUrl, apiKey: config.apiKey });
4036
+ const requests = loadedScenarios.scenarios.map(({ scenario }) => ({
4037
+ id: scenario.name,
4038
+ screen: scenario.given?.screen,
4039
+ observation: scenario.given?.observation,
4040
+ prompt: scenario.when,
4041
+ modelOverride: scenario.modelOverride || options.model
4042
+ }));
4043
+ const spinner = createSpinner(`Running ${requests.length} scenario(s)...`);
4044
+ spinner.start();
4045
+ const runResult = await apiClient.evalRun(requests);
4046
+ spinner.stop();
4047
+ if (runResult.error) {
4048
+ error(`Eval run failed: ${runResult.error}`);
4049
+ jsonOutput({ success: false, error: runResult.error });
4050
+ process.exit(1);
4051
+ return;
4052
+ }
4053
+ const resultsById = new Map((runResult.results ?? []).map((r) => [r.id, r]));
4054
+ const outcomes = loadedScenarios.scenarios.map(({ scenario }) => {
4055
+ const result = resultsById.get(scenario.name);
4056
+ if (!result) {
4057
+ return {
4058
+ name: scenario.name,
4059
+ status: "failed",
4060
+ totalTokens: 0,
4061
+ failureReason: "no result returned by the API for this scenario"
4062
+ };
4063
+ }
4064
+ const match = matchActions(scenario.expect, result.actions, result.content);
4065
+ return {
4066
+ name: scenario.name,
4067
+ status: match.ok ? "passed" : "failed",
4068
+ totalTokens: result.totalTokens,
4069
+ failureReason: match.ok ? void 0 : match.reason
4070
+ };
4071
+ });
4072
+ const passed = outcomes.filter((o) => o.status === "passed").length;
4073
+ const total = outcomes.length;
4074
+ const passRate = total > 0 ? passed / total : 1;
4075
+ heading("\nResults:");
4076
+ table(
4077
+ outcomes.map((o) => [
4078
+ o.status === "passed" ? `\u2713 ${o.name}` : `\u2717 ${o.name}`,
4079
+ o.status === "passed" ? `${o.totalTokens} tokens` : o.failureReason || "failed"
4080
+ ])
4081
+ );
4082
+ const baseline = loadBaseline(baselinePath);
4083
+ let regressions = [];
4084
+ if (!options.updateBaseline) {
4085
+ regressions = diffAgainstBaseline(outcomes, baseline, maxTokenRegression);
4086
+ if (regressions.length > 0) {
4087
+ heading("\nRegressions vs baseline:");
4088
+ table(regressions.map((r) => [r.name, `${r.kind}: ${r.detail}`]));
4089
+ }
4090
+ }
4091
+ const passRatePct = Math.round(passRate * 1e3) / 10;
4092
+ const minPassRatePct = Math.round(minPassRate * 1e3) / 10;
4093
+ heading(`
4094
+ Pass rate: ${passed}/${total} (${passRatePct}%) \u2014 threshold ${minPassRatePct}%`);
4095
+ if (options.updateBaseline) {
4096
+ if (passRate < minPassRate) {
4097
+ error(
4098
+ `Refusing to update baseline: pass rate ${passRatePct}% is below the ${minPassRatePct}% threshold`
4099
+ );
4100
+ info("Fix the failing scenario(s) first, then re-run with --update-baseline.");
4101
+ jsonOutput({ success: false, passRate, minPassRate, total, passed, results: outcomes });
4102
+ process.exit(1);
4103
+ return;
4104
+ }
4105
+ const newBaseline = buildBaseline(outcomes, {
4106
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4107
+ gitSha: process.env.GIT_SHA ?? null
4108
+ });
4109
+ saveBaseline(baselinePath, newBaseline);
4110
+ success(`Updated baseline at ${baselinePath}`);
4111
+ jsonOutput({ success: true, passRate, minPassRate, total, passed, results: outcomes });
4112
+ return;
4113
+ }
4114
+ const ok = passRate >= minPassRate && regressions.length === 0;
4115
+ jsonOutput({
4116
+ success: ok,
4117
+ passRate,
4118
+ minPassRate,
4119
+ total,
4120
+ passed,
4121
+ failed: total - passed,
4122
+ regressions,
4123
+ results: outcomes
4124
+ });
4125
+ if (!ok) {
4126
+ if (passRate < minPassRate) {
4127
+ error(`Pass rate ${passRatePct}% is below the ${minPassRatePct}% threshold`);
4128
+ }
4129
+ if (regressions.length > 0) {
4130
+ error(`${regressions.length} regression(s) vs baseline`);
4131
+ }
4132
+ process.exit(1);
4133
+ } else {
4134
+ success("Eval passed");
4135
+ }
4136
+ } catch (err) {
4137
+ error(`Eval failed: ${err instanceof Error ? err.message : String(err)}`);
4138
+ jsonOutput({ success: false, error: err instanceof Error ? err.message : String(err) });
4139
+ process.exit(1);
4140
+ }
4141
+ });
4142
+ }
4143
+
4144
+ // src/cli/index.ts
4145
+ async function main() {
4146
+ const program = new import_commander7.Command();
4147
+ program.name("appilots").description(
4148
+ "Analyze your React Native app and generate MCP documents for AI-powered navigation.\n\nQuick start:\n $ appilots init --api-key ak_xxx\n $ appilots sync\n $ appilots watch"
4149
+ ).version("0.1.0").option("--verbose", "Enable verbose logging");
4150
+ program.addCommand(initCommand());
4151
+ program.addCommand(syncCommand());
4152
+ program.addCommand(watchCommand());
4153
+ program.addCommand(generateCommand());
4154
+ program.addCommand(statusCommand());
4155
+ program.addCommand(evalCommand());
4156
+ const args = process.argv.slice(2);
4157
+ const firstArg = args[0];
4158
+ const hasCommand = firstArg !== void 0 && args.length > 0 && !firstArg.startsWith("-");
4159
+ if (args.length === 0) {
4160
+ const config = loadConfig();
4161
+ if (config) {
4162
+ process.argv.push("sync");
4163
+ } else {
4164
+ banner();
4165
+ info("Welcome to Appilots! Get started with:");
4166
+ info(" appilots init --api-key <your-ak_key>");
4167
+ console.log("");
4168
+ program.outputHelp();
4169
+ return;
4170
+ }
4171
+ }
4172
+ await program.parseAsync(process.argv);
4173
+ }
4174
+ main().catch((err) => {
4175
+ error(err instanceof Error ? err.message : String(err));
4176
+ process.exit(1);
4177
+ });
4178
+ //# sourceMappingURL=index.js.map