@appilots/cli 0.4.0 → 0.11.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.
package/dist/cli/index.js CHANGED
@@ -28,12 +28,79 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  ));
29
29
 
30
30
  // src/cli/index.ts
31
- var import_commander7 = require("commander");
31
+ var import_commander8 = require("commander");
32
+
33
+ // src/version.ts
34
+ var CLI_VERSION = "0.11.0";
32
35
 
33
36
  // src/config/index.ts
34
37
  var import_fs = require("fs");
35
38
  var import_path = require("path");
36
39
  var import_fs2 = require("fs");
40
+ var DEFAULT_SERVER_URL = "http://localhost:4000";
41
+ var KNOWN_CONFIG_KEYS = [
42
+ "apiKey",
43
+ "projectId",
44
+ "serverUrl",
45
+ "outputDir",
46
+ "include",
47
+ "exclude",
48
+ "autoActivate",
49
+ "strictScreens",
50
+ "screenPatterns",
51
+ "navigationInclude",
52
+ "navigationExclude",
53
+ "platform",
54
+ "manifestPath",
55
+ "eval"
56
+ ];
57
+ var KEY_ALIASES = {
58
+ apiUrl: "serverUrl",
59
+ // `@appilots/sdk`'s own name for this. One `.appilotsrc` feeds both
60
+ // packages, so an integrator who wrote the SDK's key should not be told
61
+ // it is unrecognized.
62
+ apiBaseUrl: "serverUrl",
63
+ apiURL: "serverUrl",
64
+ baseUrl: "serverUrl",
65
+ url: "serverUrl",
66
+ host: "serverUrl",
67
+ endpoint: "serverUrl",
68
+ server: "serverUrl",
69
+ key: "apiKey",
70
+ token: "apiKey",
71
+ project: "projectId",
72
+ sourceDir: "include",
73
+ srcDir: "include",
74
+ source: "include",
75
+ outDir: "outputDir"
76
+ };
77
+ function editDistance(a, b) {
78
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
79
+ for (let i = 1; i <= a.length; i++) {
80
+ const current = [i];
81
+ for (let j = 1; j <= b.length; j++) {
82
+ const deletion = (previous[j] ?? 0) + 1;
83
+ const insertion = (current[j - 1] ?? 0) + 1;
84
+ const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
85
+ current.push(Math.min(deletion, insertion, substitution));
86
+ }
87
+ previous = current;
88
+ }
89
+ return previous[b.length] ?? 0;
90
+ }
91
+ function suggestConfigKey(key) {
92
+ const alias = KEY_ALIASES[key];
93
+ if (alias) return alias;
94
+ const lower = key.toLowerCase();
95
+ let best;
96
+ for (const known of KNOWN_CONFIG_KEYS) {
97
+ const distance = editDistance(lower, known.toLowerCase());
98
+ if (distance <= 2 && (!best || distance < best.distance)) {
99
+ best = { key: known, distance };
100
+ }
101
+ }
102
+ return best?.key;
103
+ }
37
104
  function getEnvOverrides(env = process.env) {
38
105
  const clean = (value) => {
39
106
  const trimmed = value?.trim();
@@ -45,7 +112,7 @@ function getEnvOverrides(env = process.env) {
45
112
  serverUrl: clean(env.APPILOTS_SERVER_URL)
46
113
  };
47
114
  }
48
- function loadConfig() {
115
+ function loadConfig(onWarn) {
49
116
  const configPath = getConfigPath();
50
117
  const env = getEnvOverrides();
51
118
  let fileConfig = {};
@@ -53,13 +120,27 @@ function loadConfig() {
53
120
  try {
54
121
  fileConfig = JSON.parse((0, import_fs.readFileSync)(configPath, "utf-8"));
55
122
  } catch (error2) {
56
- throw new Error(`Failed to parse .appilotsrc at ${configPath}: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
123
+ throw new Error(
124
+ `Failed to parse .appilotsrc at ${configPath}: ${error2 instanceof Error ? error2.message : "Unknown error"}`
125
+ );
57
126
  }
58
127
  } else if (!env.apiKey) {
59
128
  return null;
60
129
  }
130
+ const legacyWarnings = [];
131
+ if (fileConfig.serverUrl === void 0) {
132
+ for (const [alias, canonical] of Object.entries(KEY_ALIASES)) {
133
+ if (canonical !== "serverUrl" || fileConfig[alias] === void 0) continue;
134
+ fileConfig.serverUrl = fileConfig[alias];
135
+ delete fileConfig[alias];
136
+ legacyWarnings.push(
137
+ `legacy key "${alias}" \u2014 using it as "serverUrl"; rename it in .appilotsrc`
138
+ );
139
+ break;
140
+ }
141
+ }
61
142
  const merged = {
62
- serverUrl: "http://localhost:4000",
143
+ serverUrl: DEFAULT_SERVER_URL,
63
144
  outputDir: ".appilots",
64
145
  autoActivate: true,
65
146
  strictScreens: true,
@@ -69,6 +150,26 @@ function loadConfig() {
69
150
  ...env.serverUrl ? { serverUrl: env.serverUrl } : {}
70
151
  };
71
152
  const validation = validateConfig(merged);
153
+ validation.warnings.unshift(...legacyWarnings);
154
+ if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
155
+ for (const key of Object.keys(fileConfig)) {
156
+ if (KNOWN_CONFIG_KEYS.includes(key)) continue;
157
+ if (suggestConfigKey(key) !== "serverUrl") continue;
158
+ validation.warnings = validation.warnings.filter(
159
+ (warning) => !warning.startsWith(`unknown key "${key}"`)
160
+ );
161
+ validation.errors.push(
162
+ `unknown key "${key}" looks like "serverUrl" \u2014 not falling back to ${DEFAULT_SERVER_URL}; rename the key to "serverUrl"`
163
+ );
164
+ validation.valid = false;
165
+ }
166
+ }
167
+ if (onWarn) {
168
+ const source = configPath ?? "environment variables";
169
+ for (const warning of validation.warnings) {
170
+ onWarn(`${source}: ${warning}`);
171
+ }
172
+ }
72
173
  if (!validation.valid) {
73
174
  const source = configPath ?? "environment variables";
74
175
  throw new Error(
@@ -84,7 +185,7 @@ function saveConfig(dir, config) {
84
185
  const mergedConfig = {
85
186
  apiKey: config.apiKey || existingConfig?.apiKey || "",
86
187
  projectId: config.projectId || existingConfig?.projectId || "",
87
- serverUrl: config.serverUrl || existingConfig?.serverUrl || "http://localhost:4000",
188
+ serverUrl: config.serverUrl || existingConfig?.serverUrl || DEFAULT_SERVER_URL,
88
189
  outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
89
190
  autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
90
191
  include: config.include || existingConfig?.include,
@@ -100,7 +201,9 @@ function saveConfig(dir, config) {
100
201
  try {
101
202
  (0, import_fs.writeFileSync)(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
102
203
  } catch (error2) {
103
- throw new Error(`Failed to write .appilotsrc to ${configPath}: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
204
+ throw new Error(
205
+ `Failed to write .appilotsrc to ${configPath}: ${error2 instanceof Error ? error2.message : "Unknown error"}`
206
+ );
104
207
  }
105
208
  }
106
209
  function getConfigPath() {
@@ -127,14 +230,25 @@ function getConfigPath() {
127
230
  }
128
231
  function validateConfig(config) {
129
232
  const errors = [];
233
+ const warnings = [];
130
234
  if (!config || typeof config !== "object") {
131
235
  return {
132
236
  valid: false,
133
- errors: ["Configuration must be an object"]
237
+ errors: ["Configuration must be an object"],
238
+ warnings
134
239
  };
135
240
  }
241
+ for (const key of Object.keys(config)) {
242
+ if (KNOWN_CONFIG_KEYS.includes(key)) continue;
243
+ const suggestion = suggestConfigKey(key);
244
+ warnings.push(
245
+ `unknown key "${key}" \u2014 ignored` + (suggestion ? ` (did you mean "${suggestion}"?)` : "")
246
+ );
247
+ }
136
248
  if (!config.apiKey || typeof config.apiKey !== "string") {
137
- errors.push("apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)");
249
+ errors.push(
250
+ "apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)"
251
+ );
138
252
  } else if (!config.apiKey.startsWith("ak_")) {
139
253
  errors.push('apiKey must start with "ak_"');
140
254
  }
@@ -196,7 +310,8 @@ function validateConfig(config) {
196
310
  }
197
311
  return {
198
312
  valid: errors.length === 0,
199
- errors
313
+ errors,
314
+ warnings
200
315
  };
201
316
  }
202
317
 
@@ -241,14 +356,23 @@ function dim(msg) {
241
356
  if (mode !== "pretty") return;
242
357
  console.log(import_chalk.default.dim(msg));
243
358
  }
359
+ var BANNER_WIDTH = 39;
360
+ function bannerRow(plain, colored) {
361
+ return ` ${import_chalk.default.cyan("\u2551")} ${colored}${" ".repeat(Math.max(0, BANNER_WIDTH - 5 - plain.length))}${import_chalk.default.cyan("\u2551")}`;
362
+ }
244
363
  function banner() {
245
364
  if (mode !== "pretty") return;
246
- const logo = `
247
- ${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")}
248
- ${import_chalk.default.cyan("\u2551")} ${import_chalk.default.bold.cyan("Appilots MCP Generator")} ${import_chalk.default.cyan("\u2551")}
249
- ${import_chalk.default.cyan("\u2551")} ${import_chalk.default.dim("v0.1.0")} ${import_chalk.default.cyan("\u2551")}
250
- ${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")}
251
- `;
365
+ const title = "Appilots MCP Generator";
366
+ const version = `v${CLI_VERSION}`;
367
+ const border = "\u2550".repeat(BANNER_WIDTH);
368
+ const logo = [
369
+ "",
370
+ ` ${import_chalk.default.cyan(`\u2554${border}\u2557`)}`,
371
+ bannerRow(title, import_chalk.default.bold.cyan(title)),
372
+ bannerRow(version, import_chalk.default.dim(version)),
373
+ ` ${import_chalk.default.cyan(`\u255A${border}\u255D`)}`,
374
+ " "
375
+ ].join("\n");
252
376
  console.log(logo);
253
377
  }
254
378
  function heading(msg) {
@@ -269,19 +393,105 @@ function table(rows) {
269
393
  // src/cli/commands/init.ts
270
394
  var import_commander = require("commander");
271
395
 
396
+ // ../shared/dist/chunk-UEMUBXFH.mjs
397
+ var DEFAULT_API_BASE_URL = "https://api.appilots.com";
398
+ var APPILOTS_API_PATH_PREFIX = "/api/v1";
399
+ function normalizeApiBaseUrl(configured) {
400
+ const raw = (configured ?? DEFAULT_API_BASE_URL).trim().replace(/\/+$/, "");
401
+ if (!raw) return `${DEFAULT_API_BASE_URL}${APPILOTS_API_PATH_PREFIX}`;
402
+ if (new RegExp(`${APPILOTS_API_PATH_PREFIX}(/|$)`).test(raw)) return raw;
403
+ return `${raw}${APPILOTS_API_PATH_PREFIX}`;
404
+ }
405
+ var LIMITS = {
406
+ MAX_MCP_DOCUMENT_SIZE_MB: 10,
407
+ MAX_PROJECTS_PER_USER: 20,
408
+ MAX_MESSAGE_LENGTH: 4096,
409
+ MAX_ACTIONS_PER_MESSAGE: 10,
410
+ SESSION_TIMEOUT_MS: 30 * 60 * 1e3
411
+ // 30 minutes
412
+ };
413
+
414
+ // src/utils/stringify.ts
415
+ function stringifyUnknown(value) {
416
+ if (value instanceof Error) {
417
+ const message = value.message || value.name;
418
+ const cause = value.cause;
419
+ if (cause instanceof Error && cause.message && cause.message !== message) {
420
+ return `${message} (${cause.message})`;
421
+ }
422
+ return message;
423
+ }
424
+ if (typeof value === "string") return value;
425
+ if (value === null || value === void 0) return String(value);
426
+ if (typeof value !== "object") return String(value);
427
+ try {
428
+ const json = JSON.stringify(value);
429
+ if (json && json !== "{}") return json;
430
+ } catch {
431
+ }
432
+ return String(value);
433
+ }
434
+
272
435
  // src/services/api-client.ts
273
- var DEFAULT_TIMEOUT_MS = 3e4;
436
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
274
437
  var DEFAULT_MAX_RETRIES = 2;
275
438
  var RETRY_BASE_DELAY_MS = 500;
439
+ function describeNetworkError(url, err) {
440
+ let origin = url;
441
+ try {
442
+ origin = new URL(url).origin;
443
+ } catch {
444
+ }
445
+ const cause = err?.cause;
446
+ const code = typeof cause?.code === "string" ? cause.code : void 0;
447
+ const detail = code ?? stringifyUnknown(err);
448
+ return new Error(`Cannot reach Appilots server at ${origin} (${detail})`);
449
+ }
450
+ function errorCodeOf(payload) {
451
+ const error2 = payload?.error;
452
+ if (error2 && typeof error2 === "object") {
453
+ const { code } = error2;
454
+ if (typeof code === "string" && code.trim()) return code;
455
+ }
456
+ return void 0;
457
+ }
458
+ function describeApiError(payload, fallback) {
459
+ const error2 = payload?.error;
460
+ if (typeof error2 === "string" && error2.trim()) return error2;
461
+ if (error2 && typeof error2 === "object") {
462
+ const { code, message: message2 } = error2;
463
+ const text = typeof message2 === "string" && message2.trim() ? message2 : void 0;
464
+ const tag = typeof code === "string" && code.trim() ? code : void 0;
465
+ if (text && tag) return `${text} (${tag})`;
466
+ if (text) return text;
467
+ if (tag) return tag;
468
+ }
469
+ const message = payload?.message;
470
+ if (typeof message === "string" && message.trim()) return message;
471
+ return fallback;
472
+ }
276
473
  var AppilotsAPIClient = class {
277
- serverUrl;
474
+ /**
475
+ * `serverUrl` with the API's mount prefix resolved — every path below
476
+ * is relative to THIS, not to the configured origin.
477
+ *
478
+ * It used to be the raw `serverUrl` with `/api/v1` hardcoded into each
479
+ * path, which made the field mean something different here than in the
480
+ * SDK. Both read the same `.appilotsrc`: the SDK completes the prefix
481
+ * when it is missing, so `https://api.appilots.com/api/v1` is correct
482
+ * there — and here that same value produced
483
+ * `/api/v1/api/v1/cli/sync`, a 404 whose body reads `Route not found`.
484
+ * Sharing `normalizeApiBaseUrl` is what makes one file mean one thing:
485
+ * with or without the prefix now works in both.
486
+ */
487
+ baseUrl;
278
488
  apiKey;
279
489
  timeoutMs;
280
490
  maxRetries;
281
491
  constructor(config) {
282
- this.serverUrl = config.serverUrl.replace(/\/$/, "");
492
+ this.baseUrl = normalizeApiBaseUrl(config.serverUrl);
283
493
  this.apiKey = config.apiKey;
284
- this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
494
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
285
495
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
286
496
  }
287
497
  /**
@@ -305,7 +515,7 @@ var AppilotsAPIClient = class {
305
515
  }
306
516
  return response;
307
517
  } catch (err) {
308
- lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : err;
518
+ lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : describeNetworkError(url, err);
309
519
  } finally {
310
520
  clearTimeout(timer);
311
521
  }
@@ -324,7 +534,7 @@ var AppilotsAPIClient = class {
324
534
  */
325
535
  async sync(content, version, appVersion) {
326
536
  try {
327
- const response = await this.request(`${this.serverUrl}/api/v1/cli/sync`, {
537
+ const response = await this.request(`${this.baseUrl}/cli/sync`, {
328
538
  method: "POST",
329
539
  headers: {
330
540
  "Content-Type": "application/json",
@@ -338,7 +548,9 @@ var AppilotsAPIClient = class {
338
548
  return {
339
549
  success: false,
340
550
  unchanged: false,
341
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
551
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`),
552
+ errorCode: errorCodeOf(errorData),
553
+ errorStatus: response.status
342
554
  };
343
555
  }
344
556
  const json = await response.json();
@@ -351,7 +563,8 @@ var AppilotsAPIClient = class {
351
563
  return {
352
564
  success: false,
353
565
  unchanged: false,
354
- error: error2 instanceof Error ? error2.message : "Failed to sync with Appilots API"
566
+ error: error2 instanceof Error ? error2.message : "Failed to sync with Appilots API",
567
+ errorStatus: 0
355
568
  };
356
569
  }
357
570
  }
@@ -362,7 +575,7 @@ var AppilotsAPIClient = class {
362
575
  */
363
576
  async status() {
364
577
  try {
365
- const response = await this.request(`${this.serverUrl}/api/v1/cli/status`, {
578
+ const response = await this.request(`${this.baseUrl}/cli/status`, {
366
579
  method: "GET",
367
580
  headers: {
368
581
  Authorization: `Bearer ${this.apiKey}`
@@ -371,7 +584,7 @@ var AppilotsAPIClient = class {
371
584
  if (!response.ok) {
372
585
  const errorData = await response.json().catch(() => ({}));
373
586
  return {
374
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
587
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
375
588
  };
376
589
  }
377
590
  const json = await response.json();
@@ -393,7 +606,7 @@ var AppilotsAPIClient = class {
393
606
  */
394
607
  async evalRun(scenarios) {
395
608
  try {
396
- const response = await this.request(`${this.serverUrl}/api/v1/cli/eval/run`, {
609
+ const response = await this.request(`${this.baseUrl}/cli/eval/run`, {
397
610
  method: "POST",
398
611
  headers: {
399
612
  "Content-Type": "application/json",
@@ -424,7 +637,7 @@ var AppilotsAPIClient = class {
424
637
  */
425
638
  async health() {
426
639
  try {
427
- const response = await this.request(`${this.serverUrl}/api/v1/health`, {
640
+ const response = await this.request(`${this.baseUrl}/health`, {
428
641
  method: "GET"
429
642
  });
430
643
  return response.ok;
@@ -437,6 +650,19 @@ var AppilotsAPIClient = class {
437
650
  // src/cli/commands/init.ts
438
651
  var import_fs3 = require("fs");
439
652
  var import_path2 = require("path");
653
+
654
+ // src/cli/utils/server-hint.ts
655
+ function hintUnreachableServer(serverUrl, message) {
656
+ if (serverUrl !== DEFAULT_SERVER_URL) return;
657
+ if (typeof message === "string" && !message.includes("Cannot reach Appilots server")) return;
658
+ if (message !== void 0 && typeof message !== "string") return;
659
+ info(`serverUrl is not set, so the CLI is using the built-in default (${DEFAULT_SERVER_URL})`);
660
+ info("For the hosted backend, set it in .appilotsrc:");
661
+ info(' "serverUrl": "https://api.appilots.com"');
662
+ info("Or pass --server <url>, or set APPILOTS_SERVER_URL");
663
+ }
664
+
665
+ // src/cli/commands/init.ts
440
666
  function ensureConfigIgnored(dir) {
441
667
  const gitignorePath = (0, import_path2.join)(dir, ".gitignore");
442
668
  if (!(0, import_fs3.existsSync)(gitignorePath)) {
@@ -450,7 +676,10 @@ function ensureConfigIgnored(dir) {
450
676
  if (ignored) {
451
677
  return "already-ignored";
452
678
  }
453
- (0, import_fs3.appendFileSync)(gitignorePath, "\n# Appilots CLI config (contains your API key \u2014 do not commit)\n.appilotsrc\n");
679
+ (0, import_fs3.appendFileSync)(
680
+ gitignorePath,
681
+ "\n# Appilots CLI config (contains your API key \u2014 do not commit)\n.appilotsrc\n"
682
+ );
454
683
  return "added";
455
684
  }
456
685
  function initCommand() {
@@ -473,7 +702,7 @@ function initCommand() {
473
702
  if (!options.apiKey && env.apiKey) {
474
703
  info("Using API key from APPILOTS_API_KEY environment variable");
475
704
  }
476
- const serverUrl = options.server || env.serverUrl || "http://localhost:4000";
705
+ const serverUrl = options.server || env.serverUrl || DEFAULT_SERVER_URL;
477
706
  if (!apiKey.startsWith("ak_")) {
478
707
  error("Invalid API key format. Must start with ak_");
479
708
  process.exit(1);
@@ -487,6 +716,7 @@ function initCommand() {
487
716
  if (!isHealthy) {
488
717
  error(`Cannot reach Appilots server at ${serverUrl}`);
489
718
  info("Make sure the server is running and the URL is correct");
719
+ hintUnreachableServer(serverUrl);
490
720
  process.exit(1);
491
721
  }
492
722
  const status = await apiClient.status();
@@ -524,7 +754,7 @@ function initCommand() {
524
754
  dim(" $ appilots sync - Analyze and upload your project");
525
755
  dim(" $ appilots watch - Watch for changes and auto-sync");
526
756
  } catch (err) {
527
- error(`Initialization failed: ${err instanceof Error ? err.message : String(err)}`);
757
+ error(`Initialization failed: ${stringifyUnknown(err)}`);
528
758
  process.exit(1);
529
759
  }
530
760
  });
@@ -533,7 +763,7 @@ function initCommand() {
533
763
  // src/cli/commands/sync.ts
534
764
  var import_commander2 = require("commander");
535
765
  var import_promises5 = require("fs/promises");
536
- var import_node_path4 = require("path");
766
+ var import_node_path5 = require("path");
537
767
 
538
768
  // src/generators/MCPGenerator.ts
539
769
  var import_promises4 = require("fs/promises");
@@ -813,6 +1043,45 @@ function hasJsxAttribute(element, name) {
813
1043
  }
814
1044
 
815
1045
  // src/ast/jsx/classify.ts
1046
+ var NON_INTERACTION_HANDLERS = /* @__PURE__ */ new Set([
1047
+ "onLayout",
1048
+ "onScroll",
1049
+ "onScrollBeginDrag",
1050
+ "onScrollEndDrag",
1051
+ "onMomentumScrollBegin",
1052
+ "onMomentumScrollEnd",
1053
+ "onContentSizeChange",
1054
+ "onEndReached",
1055
+ "onViewableItemsChanged",
1056
+ "onLoad",
1057
+ "onLoadStart",
1058
+ "onLoadEnd",
1059
+ "onError",
1060
+ "onProgress",
1061
+ "onFocus",
1062
+ "onBlur"
1063
+ ]);
1064
+ var VALUE_HANDLERS = /* @__PURE__ */ new Set([
1065
+ "onChange",
1066
+ "onChangeText",
1067
+ "onValueChange",
1068
+ "onSelect",
1069
+ "onSelectionChange",
1070
+ "onSubmitEditing"
1071
+ ]);
1072
+ function pressHandlerProp(element) {
1073
+ let fallback;
1074
+ for (const attr of element.attributes) {
1075
+ if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
1076
+ const name = attr.name.name;
1077
+ if (!/^on[A-Z]/.test(name)) continue;
1078
+ if (attr.value?.type !== "JSXExpressionContainer") continue;
1079
+ if (name === "onPress") return name;
1080
+ if (NON_INTERACTION_HANDLERS.has(name) || VALUE_HANDLERS.has(name)) continue;
1081
+ fallback ??= name;
1082
+ }
1083
+ return fallback;
1084
+ }
816
1085
  var VIEW_COMPONENTS = /* @__PURE__ */ new Set(["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"]);
817
1086
  var INPUT_COMPONENTS = /* @__PURE__ */ new Set(["TextInput", "Input", "HocInput"]);
818
1087
  var BUTTON_COMPONENTS = /* @__PURE__ */ new Set(["TouchableOpacity", "Pressable", "Button"]);
@@ -827,17 +1096,16 @@ function classifyJsxComponent(name, element) {
827
1096
  if (INPUT_COMPONENTS.has(name)) return "input";
828
1097
  if (BUTTON_COMPONENTS.has(name)) return "button";
829
1098
  if (element) {
830
- const hasOptions = hasJsxAttribute(element, "options");
1099
+ const hasOptions = hasJsxAttribute(element, "options") || hasJsxAttribute(element, "segments") || hasJsxAttribute(element, "choices") || hasJsxAttribute(element, "tabs");
831
1100
  const hasValue = hasJsxAttribute(element, "value");
832
1101
  const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
833
1102
  const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange") || hasJsxAttribute(element, "onChangeText");
834
- const hasOnPress = hasJsxAttribute(element, "onPress");
835
1103
  if (hasOptions && (hasValue || hasOnChange)) return "select";
836
1104
  if (hasChecked && hasOnChange) return "toggle";
837
1105
  if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
838
1106
  return "input";
839
1107
  }
840
- if (hasOnPress) return "button";
1108
+ if (pressHandlerProp(element)) return "button";
841
1109
  if (getStringAttr(element, "visible") || getExpressionIdentifierAttr(element, "visible")) {
842
1110
  return "modal";
843
1111
  }
@@ -1165,8 +1433,8 @@ var ScreenAnalyzer = class {
1165
1433
  }
1166
1434
  const screens = [];
1167
1435
  this.screensFilteredOut = 0;
1168
- for (const file of files) {
1169
- const filePath = import_path3.default.resolve(this.config.rootDir, file);
1436
+ for (const file2 of files) {
1437
+ const filePath = import_path3.default.resolve(this.config.rootDir, file2);
1170
1438
  try {
1171
1439
  const descriptor = await this.analyzeFile(filePath);
1172
1440
  if (!descriptor) continue;
@@ -1176,23 +1444,28 @@ var ScreenAnalyzer = class {
1176
1444
  if (!hasRegisterScreen && !matchesPattern) {
1177
1445
  this.screensFilteredOut++;
1178
1446
  if (this.verbose) {
1179
- console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file}`);
1447
+ console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file2}`);
1180
1448
  }
1181
1449
  continue;
1182
1450
  }
1183
1451
  }
1184
1452
  screens.push(descriptor);
1185
1453
  if (this.verbose) {
1186
- console.log(`[ScreenAnalyzer] \u2713 Analyzed: ${descriptor.name} (${file})`);
1454
+ console.log(`[ScreenAnalyzer] \u2713 Analyzed: ${descriptor.name} (${file2})`);
1187
1455
  }
1188
1456
  } catch (error2) {
1189
1457
  if (this.verbose) {
1190
- console.warn(`[ScreenAnalyzer] Failed to parse ${file}:`, error2 instanceof Error ? error2.message : error2);
1458
+ console.warn(
1459
+ `[ScreenAnalyzer] Failed to parse ${file2}:`,
1460
+ error2 instanceof Error ? error2.message : error2
1461
+ );
1191
1462
  }
1192
1463
  }
1193
1464
  }
1194
1465
  if (this.verbose && this.strictScreens) {
1195
- console.log(`[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`);
1466
+ console.log(
1467
+ `[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`
1468
+ );
1196
1469
  }
1197
1470
  return screens;
1198
1471
  }
@@ -1480,7 +1753,11 @@ var ScreenAnalyzer = class {
1480
1753
  for (const form of secondary) {
1481
1754
  const existing = out.find((candidate) => candidate.id === form.id) ?? this.findFormWithSharedFields(out, form);
1482
1755
  if (!existing) {
1483
- out.push({ ...form, fields: [...form.fields], submitAction: this.namedSubmitAction(form.submitAction) });
1756
+ out.push({
1757
+ ...form,
1758
+ fields: [...form.fields],
1759
+ submitAction: this.namedSubmitAction(form.submitAction)
1760
+ });
1484
1761
  continue;
1485
1762
  }
1486
1763
  for (const field of form.fields) {
@@ -1499,9 +1776,12 @@ var ScreenAnalyzer = class {
1499
1776
  findEquivalentField(fields, incoming) {
1500
1777
  return fields.find((field) => {
1501
1778
  if (field.name && incoming.name && field.name === incoming.name) return true;
1502
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
1503
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
1504
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
1779
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
1780
+ return true;
1781
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
1782
+ return true;
1783
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
1784
+ return true;
1505
1785
  if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
1506
1786
  return true;
1507
1787
  }
@@ -1661,9 +1941,11 @@ var ScreenAnalyzer = class {
1661
1941
  source: field.locator?.source ?? "accessibilityLabel"
1662
1942
  });
1663
1943
  } else if (attrName === "keyboardType") {
1664
- if (attr.value.value === "email-address" || attr.value.value === "email") field.type = "email";
1944
+ if (attr.value.value === "email-address" || attr.value.value === "email")
1945
+ field.type = "email";
1665
1946
  if (attr.value.value === "phone-pad") field.type = "phone";
1666
- if (attr.value.value === "numeric" || attr.value.value === "number-pad") field.type = "number";
1947
+ if (attr.value.value === "numeric" || attr.value.value === "number-pad")
1948
+ field.type = "number";
1667
1949
  }
1668
1950
  } else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
1669
1951
  if (attrName === "value" && BabelTypes.isIdentifier(attr.value.expression)) {
@@ -1829,8 +2111,10 @@ var ScreenAnalyzer = class {
1829
2111
  };
1830
2112
  if (handler.nativeConfirmationExpected) action.nativeConfirmationExpected = true;
1831
2113
  if (handler.targetScreen && !action.targetScreen) action.targetScreen = handler.targetScreen;
1832
- if (handler.successSignal && !action.successSignal) action.successSignal = handler.successSignal;
1833
- if (handler.failureSignal && !action.failureSignal) action.failureSignal = handler.failureSignal;
2114
+ if (handler.successSignal && !action.successSignal)
2115
+ action.successSignal = handler.successSignal;
2116
+ if (handler.failureSignal && !action.failureSignal)
2117
+ action.failureSignal = handler.failureSignal;
1834
2118
  if (handler.opensModal && !action.opensModal) action.opensModal = handler.opensModal;
1835
2119
  if (handler.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
1836
2120
  action.destructive = true;
@@ -1848,6 +2132,7 @@ var ScreenAnalyzer = class {
1848
2132
  if (!BabelTypes.isJSXIdentifier(element.name)) return;
1849
2133
  const componentName = element.name.name;
1850
2134
  if (classifyJsxComponent(componentName, element) !== "button") return;
2135
+ const pressProp = pressHandlerProp(element);
1851
2136
  let label;
1852
2137
  let handler;
1853
2138
  for (const attr of element.attributes) {
@@ -1857,7 +2142,7 @@ var ScreenAnalyzer = class {
1857
2142
  if ((attrName === "title" || attrName === "accessibilityLabel" || attrName === "label") && BabelTypes.isStringLiteral(attr.value)) {
1858
2143
  label = attr.value.value;
1859
2144
  }
1860
- if (attrName === "onPress" && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
2145
+ if (attrName === pressProp && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
1861
2146
  handler = attr.value.expression.name;
1862
2147
  }
1863
2148
  }
@@ -1923,10 +2208,13 @@ var ScreenAnalyzer = class {
1923
2208
  }
1924
2209
  };
1925
2210
  if (fn.body) {
1926
- (0, import_traverse4.default)(fn.body, {
1927
- noScope: true,
1928
- enter: (nodePath) => inspectNode(nodePath.node)
1929
- });
2211
+ (0, import_traverse4.default)(
2212
+ fn.body,
2213
+ {
2214
+ noScope: true,
2215
+ enter: (nodePath) => inspectNode(nodePath.node)
2216
+ }
2217
+ );
1930
2218
  }
1931
2219
  const expectedOutcome = targetScreen ? "navigation" : hasStateSetter ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
1932
2220
  return {
@@ -1984,6 +2272,7 @@ var ScreenAnalyzer = class {
1984
2272
  };
1985
2273
  const componentName = BabelTypes.isJSXIdentifier(element.name) ? element.name.name : void 0;
1986
2274
  action.sourceComponent = componentName;
2275
+ const pressProp = pressHandlerProp(element) ?? "onPress";
1987
2276
  let handlerName;
1988
2277
  for (const attr of element.attributes) {
1989
2278
  if (!BabelTypes.isJSXAttribute(attr)) continue;
@@ -2018,7 +2307,7 @@ var ScreenAnalyzer = class {
2018
2307
  }
2019
2308
  }
2020
2309
  } else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
2021
- if (attrName === "onPress" && BabelTypes.isIdentifier(attr.value.expression)) {
2310
+ if (attrName === pressProp && BabelTypes.isIdentifier(attr.value.expression)) {
2022
2311
  handlerName = attr.value.expression.name.toLowerCase();
2023
2312
  action.handler = attr.value.expression.name;
2024
2313
  if (handlerName.includes("submit")) {
@@ -2026,7 +2315,7 @@ var ScreenAnalyzer = class {
2026
2315
  } else if (handlerName.includes("navigate")) {
2027
2316
  action.type = "navigation";
2028
2317
  }
2029
- } else if (attrName === "onPress") {
2318
+ } else if (attrName === pressProp) {
2030
2319
  const inline = this.extractInlineOnPressMetadata(attr.value.expression);
2031
2320
  if (inline.handler) {
2032
2321
  handlerName = inline.handler.toLowerCase();
@@ -2050,7 +2339,11 @@ var ScreenAnalyzer = class {
2050
2339
  }
2051
2340
  }
2052
2341
  if (!action.locator && action.id) {
2053
- action.locator = { id: action.id, label: action.label, source: action.label ? "label" : "inferred" };
2342
+ action.locator = {
2343
+ id: action.id,
2344
+ label: action.label,
2345
+ source: action.label ? "label" : "inferred"
2346
+ };
2054
2347
  }
2055
2348
  if (this.isElementDestructive(element, handlerName, action.id)) {
2056
2349
  action.destructive = true;
@@ -2192,24 +2485,27 @@ var ScreenAnalyzer = class {
2192
2485
  }
2193
2486
  extractRowAction(fn) {
2194
2487
  let action;
2195
- (0, import_traverse4.default)(fn.body, {
2196
- noScope: true,
2197
- CallExpression: (nodePath) => {
2198
- const node = nodePath.node;
2199
- 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)) {
2200
- return;
2488
+ (0, import_traverse4.default)(
2489
+ fn.body,
2490
+ {
2491
+ noScope: true,
2492
+ CallExpression: (nodePath) => {
2493
+ const node = nodePath.node;
2494
+ 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)) {
2495
+ return;
2496
+ }
2497
+ const firstArg = node.arguments[0];
2498
+ if (!BabelTypes.isStringLiteral(firstArg)) return;
2499
+ const params = this.extractNavigationParams(node.arguments[1]);
2500
+ action = {
2501
+ type: "navigation",
2502
+ targetScreen: firstArg.value,
2503
+ ...Object.keys(params).length > 0 ? { params } : {},
2504
+ description: `Pressing a row opens ${firstArg.value}`
2505
+ };
2201
2506
  }
2202
- const firstArg = node.arguments[0];
2203
- if (!BabelTypes.isStringLiteral(firstArg)) return;
2204
- const params = this.extractNavigationParams(node.arguments[1]);
2205
- action = {
2206
- type: "navigation",
2207
- targetScreen: firstArg.value,
2208
- ...Object.keys(params).length > 0 ? { params } : {},
2209
- description: `Pressing a row opens ${firstArg.value}`
2210
- };
2211
2507
  }
2212
- });
2508
+ );
2213
2509
  return action;
2214
2510
  }
2215
2511
  extractNavigationParams(arg) {
@@ -2242,15 +2538,18 @@ var ScreenAnalyzer = class {
2242
2538
  } else if (BabelTypes.isIdentifier(firstParam)) {
2243
2539
  itemNames.add(firstParam.name);
2244
2540
  }
2245
- (0, import_traverse4.default)(fn.body, {
2246
- noScope: true,
2247
- MemberExpression: (nodePath) => {
2248
- const node = nodePath.node;
2249
- if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
2250
- fields.add(node.property.name);
2541
+ (0, import_traverse4.default)(
2542
+ fn.body,
2543
+ {
2544
+ noScope: true,
2545
+ MemberExpression: (nodePath) => {
2546
+ const node = nodePath.node;
2547
+ if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
2548
+ fields.add(node.property.name);
2549
+ }
2251
2550
  }
2252
2551
  }
2253
- });
2552
+ );
2254
2553
  return Array.from(fields).sort();
2255
2554
  }
2256
2555
  inferItemType(dataSource, renderItem, displayFields) {
@@ -2262,11 +2561,19 @@ var ScreenAnalyzer = class {
2262
2561
  if (displayFields.length > 0) return "Item";
2263
2562
  return void 0;
2264
2563
  }
2564
+ /**
2565
+ * Identity comes from how a field is NAMED, not from what the app
2566
+ * sells: `id`/`uuid`/`key`/`slug` are conventions any codebase uses,
2567
+ * `name`/`title`/`email` are how any row introduces itself. `plate`
2568
+ * used to sit in this list and read like one of them — but it is the
2569
+ * example app's schema, and no other tenant ever got its equivalent
2570
+ * (`mrn`, `trackingNumber`, `sku`) added here.
2571
+ */
2265
2572
  inferIdentityFields(keyField, displayFields) {
2266
2573
  const out = /* @__PURE__ */ new Set();
2267
2574
  if (keyField) out.add(keyField);
2268
2575
  for (const field of displayFields) {
2269
- if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
2576
+ if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
2270
2577
  }
2271
2578
  return Array.from(out);
2272
2579
  }
@@ -2277,17 +2584,23 @@ var ScreenAnalyzer = class {
2277
2584
  CallExpression: (nodePath) => {
2278
2585
  const node = nodePath.node;
2279
2586
  if (!BabelTypes.isMemberExpression(node.callee)) return;
2280
- if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter") return;
2281
- if (!BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource) return;
2587
+ if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
2588
+ return;
2589
+ if (!BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource)
2590
+ return;
2282
2591
  const fn = node.arguments[0];
2283
- if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn)) return;
2284
- (0, import_traverse4.default)(fn.body, {
2285
- noScope: true,
2286
- Identifier: (innerPath) => {
2287
- const name = innerPath.node.name;
2288
- if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
2592
+ if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
2593
+ return;
2594
+ (0, import_traverse4.default)(
2595
+ fn.body,
2596
+ {
2597
+ noScope: true,
2598
+ Identifier: (innerPath) => {
2599
+ const name = innerPath.node.name;
2600
+ if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
2601
+ }
2289
2602
  }
2290
- });
2603
+ );
2291
2604
  }
2292
2605
  });
2293
2606
  return queryBinding;
@@ -2378,7 +2691,7 @@ var NavigationAnalyzer = class {
2378
2691
  cwd: this.config.rootDir,
2379
2692
  ignore: excludePatterns
2380
2693
  });
2381
- return files.map((file) => import_path4.default.join(this.config.rootDir, file));
2694
+ return files.map((file2) => import_path4.default.join(this.config.rootDir, file2));
2382
2695
  }
2383
2696
  /** Parse navigator definitions from a file */
2384
2697
  parseNavigators(content, filePath) {
@@ -2439,11 +2752,7 @@ var NavigationAnalyzer = class {
2439
2752
  if (t5.isJSXAttribute(initialRouteAttr) && t5.isStringLiteral(initialRouteAttr.value)) {
2440
2753
  navigator.initialRouteName = initialRouteAttr.value.value;
2441
2754
  }
2442
- const screens = this.extractScreensFromNavigator(
2443
- node,
2444
- objectName,
2445
- navigator.type
2446
- );
2755
+ const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
2447
2756
  screensByNavigator.set(objectName, screens);
2448
2757
  }
2449
2758
  }
@@ -2460,27 +2769,112 @@ var NavigationAnalyzer = class {
2460
2769
  }
2461
2770
  return navigators;
2462
2771
  }
2772
+ /**
2773
+ * Is this JSX element `<navigatorVarName.MEMBER …>`?
2774
+ */
2775
+ // Deliberately NOT a type predicate (`node is t.JSXElement`): a false
2776
+ // result would then narrow `node` to "not a JSXElement", and the very
2777
+ // next check — the same node against a different member — would see
2778
+ // `never`. It is a question about the member name, not about the node
2779
+ // kind.
2780
+ isNavigatorMember(node, navigatorVarName, member) {
2781
+ if (!t5.isJSXElement(node)) return false;
2782
+ const name = node.openingElement.name;
2783
+ return t5.isJSXMemberExpression(name) && t5.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && t5.isJSXIdentifier(name.property) && name.property.name === member;
2784
+ }
2785
+ /**
2786
+ * Flatten a navigator's JSX children into the `<X.Screen>` elements they
2787
+ * contain, unwrapping every container a real app puts in between.
2788
+ *
2789
+ * The old version compared `t.isJSXElement(child)` against the direct
2790
+ * children only. A ternary is a `JSXExpressionContainer`, so an
2791
+ * auth-gated root — the modal shape of a commercial app, and the shape
2792
+ * of this repo's own `apps/example-app` — contributed ZERO screens to
2793
+ * the graph while `appilots sync` reported success (#396).
2794
+ *
2795
+ * Both branches of a conditional are collected on purpose. The graph is
2796
+ * a design-time map of what routes EXIST, not a prediction of which one
2797
+ * a given session will render; the agent needs the destination name to
2798
+ * be there whether or not the user happens to be logged in right now.
2799
+ */
2800
+ collectScreenElements(children, navigatorVarName, depth = 0) {
2801
+ if (depth > 12) return [];
2802
+ const found = [];
2803
+ const visit = (node) => {
2804
+ if (!node) return;
2805
+ if (this.isNavigatorMember(node, navigatorVarName, "Screen")) {
2806
+ found.push(node);
2807
+ return;
2808
+ }
2809
+ if (this.isNavigatorMember(node, navigatorVarName, "Group")) {
2810
+ const group = node;
2811
+ found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
2812
+ return;
2813
+ }
2814
+ if (t5.isJSXElement(node)) {
2815
+ const name = node.openingElement.name;
2816
+ const isForeignNavigator = t5.isJSXMemberExpression(name) && t5.isJSXIdentifier(name.property) && name.property.name === "Navigator";
2817
+ if (isForeignNavigator) return;
2818
+ found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
2819
+ return;
2820
+ }
2821
+ if (t5.isJSXFragment(node)) {
2822
+ found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
2823
+ return;
2824
+ }
2825
+ if (t5.isJSXExpressionContainer(node)) {
2826
+ visit(node.expression);
2827
+ return;
2828
+ }
2829
+ if (t5.isConditionalExpression(node)) {
2830
+ visit(node.consequent);
2831
+ visit(node.alternate);
2832
+ return;
2833
+ }
2834
+ if (t5.isLogicalExpression(node)) {
2835
+ visit(node.left);
2836
+ visit(node.right);
2837
+ return;
2838
+ }
2839
+ if (t5.isCallExpression(node)) {
2840
+ for (const arg of node.arguments) {
2841
+ if (t5.isArrowFunctionExpression(arg) || t5.isFunctionExpression(arg)) {
2842
+ if (t5.isBlockStatement(arg.body)) {
2843
+ for (const stmt of arg.body.body) {
2844
+ if (t5.isReturnStatement(stmt)) visit(stmt.argument);
2845
+ }
2846
+ } else {
2847
+ visit(arg.body);
2848
+ }
2849
+ }
2850
+ }
2851
+ return;
2852
+ }
2853
+ if (t5.isArrayExpression(node)) {
2854
+ for (const el of node.elements) visit(el);
2855
+ return;
2856
+ }
2857
+ if (t5.isTSAsExpression(node) || t5.isTSNonNullExpression(node)) {
2858
+ visit(node.expression);
2859
+ }
2860
+ };
2861
+ for (const child of children) visit(child);
2862
+ return found;
2863
+ }
2463
2864
  /** Extract screens from a navigator JSX element */
2464
2865
  extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
2465
2866
  const screens = [];
2466
2867
  if (!navigatorElement.children) return screens;
2467
- for (const child of navigatorElement.children) {
2468
- if (t5.isJSXElement(child) && t5.isJSXMemberExpression(child.openingElement.name)) {
2469
- const memberExpr = child.openingElement.name;
2470
- if (t5.isJSXIdentifier(memberExpr.object) && memberExpr.object.name === navigatorVarName && t5.isJSXIdentifier(memberExpr.property) && memberExpr.property.name === "Screen") {
2471
- const screenName = this.extractAttributeValue(
2472
- child.openingElement.attributes,
2473
- "name"
2474
- );
2475
- if (screenName) {
2476
- screens.push({
2477
- name: screenName,
2478
- navigatorName: navigatorVarName,
2479
- navigatorType
2480
- });
2481
- }
2482
- }
2483
- }
2868
+ const seen = /* @__PURE__ */ new Set();
2869
+ for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
2870
+ const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
2871
+ if (!screenName || seen.has(screenName)) continue;
2872
+ seen.add(screenName);
2873
+ screens.push({
2874
+ name: screenName,
2875
+ navigatorName: navigatorVarName,
2876
+ navigatorType
2877
+ });
2484
2878
  }
2485
2879
  return screens;
2486
2880
  }
@@ -2539,9 +2933,7 @@ var NavigationAnalyzer = class {
2539
2933
  if (member.type === "TSPropertySignature" && member.key) {
2540
2934
  const keyName = member.key.type === "Identifier" ? member.key.name : null;
2541
2935
  if (keyName && member.typeAnnotation) {
2542
- const params = this.extractParamsFromType(
2543
- member.typeAnnotation.typeAnnotation
2544
- );
2936
+ const params = this.extractParamsFromType(member.typeAnnotation.typeAnnotation);
2545
2937
  entries.set(keyName, params);
2546
2938
  }
2547
2939
  }
@@ -2581,7 +2973,7 @@ var NavigationAnalyzer = class {
2581
2973
  if (type.type === "TSUndefinedKeyword") return "undefined";
2582
2974
  if (type.type === "TSNullKeyword") return "null";
2583
2975
  if (type.type === "TSUnionType") {
2584
- return type.types.map((t12) => this.typeToString(t12)).join(" | ");
2976
+ return type.types.map((t15) => this.typeToString(t15)).join(" | ");
2585
2977
  }
2586
2978
  if (type.type === "TSTypeLiteral") {
2587
2979
  return "object";
@@ -2602,7 +2994,7 @@ var NavigationAnalyzer = class {
2602
2994
  /** Attach parsed type params to navigator screens */
2603
2995
  attachParamsToNavigators(navigators, types) {
2604
2996
  for (const navigator of navigators) {
2605
- const matchingType = types.find((t12) => t12.type === navigator.type);
2997
+ const matchingType = types.find((t15) => t15.type === navigator.type);
2606
2998
  if (matchingType) {
2607
2999
  for (const screen of navigator.screens) {
2608
3000
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -2620,6 +3012,11 @@ var NavigationAnalyzer = class {
2620
3012
  let initialScreen = "";
2621
3013
  for (const navigator of parsedNavigators) {
2622
3014
  const screenNames = navigator.screens.map((s) => s.name);
3015
+ if (screenNames.length === 0) {
3016
+ console.warn(
3017
+ `[MCPGenerator] navigator "${navigator.name}" (${navigator.type}) declared no reachable screens. Its routes will be missing from the app map, so the agent will have no destination name for them. If the screens are there in the source, this is a parser gap \u2014 please report it with the file.`
3018
+ );
3019
+ }
2623
3020
  navigators.push({
2624
3021
  name: navigator.name,
2625
3022
  type: navigator.type,
@@ -2693,8 +3090,8 @@ var ComponentAnalyzer = class {
2693
3090
  });
2694
3091
  const components = [];
2695
3092
  (0, import_traverse6.default)(ast, {
2696
- JSXElement: (path9) => {
2697
- const component = this.extractComponentFromJSXElement(path9.node);
3093
+ JSXElement: (path11) => {
3094
+ const component = this.extractComponentFromJSXElement(path11.node);
2698
3095
  if (component) {
2699
3096
  components.push(component);
2700
3097
  }
@@ -2828,13 +3225,13 @@ var FormAnalyzer = class {
2828
3225
  this.inputElements = [];
2829
3226
  this.submitButtons = [];
2830
3227
  (0, import_traverse7.default)(ast, {
2831
- CallExpression: (path9) => {
2832
- this.extractStateVariables(path9.node);
3228
+ CallExpression: (path11) => {
3229
+ this.extractStateVariables(path11.node);
2833
3230
  }
2834
3231
  });
2835
3232
  (0, import_traverse7.default)(ast, {
2836
- JSXElement: (path9) => {
2837
- this.extractFormElements(path9.node);
3233
+ JSXElement: (path11) => {
3234
+ this.extractFormElements(path11.node);
2838
3235
  }
2839
3236
  });
2840
3237
  const validationRules = this.extractValidationRules(ast);
@@ -2934,8 +3331,8 @@ var FormAnalyzer = class {
2934
3331
  extractValidationRules(ast) {
2935
3332
  const rules = {};
2936
3333
  (0, import_traverse7.default)(ast, {
2937
- IfStatement: (path9) => {
2938
- const test = path9.node.test;
3334
+ IfStatement: (path11) => {
3335
+ const test = path11.node.test;
2939
3336
  const rule = this.extractRuleFromCondition(test);
2940
3337
  if (rule) {
2941
3338
  const { field, description } = rule;
@@ -3068,8 +3465,8 @@ var ReactNativePlatformAnalyzer = class {
3068
3465
  console.log(
3069
3466
  `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
3070
3467
  );
3071
- const enrichmentPromises = screenFiles.map(async (file) => {
3072
- const filePath = import_node_path.default.resolve(config.rootDir, file);
3468
+ const enrichmentPromises = screenFiles.map(async (file2) => {
3469
+ const filePath = import_node_path.default.resolve(config.rootDir, file2);
3073
3470
  try {
3074
3471
  const [components, forms] = await Promise.all([
3075
3472
  componentAnalyzer.analyzeFile(filePath),
@@ -3077,7 +3474,7 @@ var ReactNativePlatformAnalyzer = class {
3077
3474
  ]);
3078
3475
  return { filePath, components, forms };
3079
3476
  } catch (error2) {
3080
- console.warn(`[ReactNativePlatformAnalyzer] Failed to analyze ${file}:`, error2);
3477
+ console.warn(`[ReactNativePlatformAnalyzer] Failed to analyze ${file2}:`, error2);
3081
3478
  return { filePath, components: [], forms: [] };
3082
3479
  }
3083
3480
  });
@@ -3345,13 +3742,13 @@ function staticRoutePath(node) {
3345
3742
  if (!node) return void 0;
3346
3743
  if (t9.isStringLiteral(node)) return node.value;
3347
3744
  if (t9.isTemplateLiteral(node)) {
3348
- let path9 = "";
3745
+ let path11 = "";
3349
3746
  node.quasis.forEach((quasi, index) => {
3350
- path9 += quasi.value.cooked ?? quasi.value.raw;
3747
+ path11 += quasi.value.cooked ?? quasi.value.raw;
3351
3748
  const expr = node.expressions[index];
3352
- if (expr) path9 += `:${paramNameOf(expr)}`;
3749
+ if (expr) path11 += `:${paramNameOf(expr)}`;
3353
3750
  });
3354
- return path9;
3751
+ return path11;
3355
3752
  }
3356
3753
  return void 0;
3357
3754
  }
@@ -3436,27 +3833,29 @@ var WebScreenAnalyzer = class {
3436
3833
  include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
3437
3834
  exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
3438
3835
  } = this.config;
3439
- const files = await (0, import_fast_glob4.default)(include, { cwd: this.config.rootDir, ignore: exclude });
3836
+ const files = (await (0, import_fast_glob4.default)(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
3837
+ (file2) => !file2.endsWith(".d.ts")
3838
+ );
3440
3839
  const patternMatches = await (0, import_fast_glob4.default)(this.screenPatterns, {
3441
3840
  cwd: this.config.rootDir,
3442
3841
  ignore: exclude
3443
3842
  });
3444
3843
  const patternSet = new Set(patternMatches.map((f) => import_path5.default.resolve(this.config.rootDir, f)));
3445
3844
  const candidates = [];
3446
- for (const file of files) {
3447
- const filePath = import_path5.default.resolve(this.config.rootDir, file);
3845
+ for (const file2 of files) {
3846
+ const filePath = import_path5.default.resolve(this.config.rootDir, file2);
3448
3847
  try {
3449
3848
  const candidate = await this.analyzeFile(filePath);
3450
3849
  if (!candidate) continue;
3451
3850
  candidate.matchesScreenPattern = patternSet.has(filePath);
3452
3851
  candidates.push(candidate);
3453
3852
  if (this.verbose) {
3454
- console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file})`);
3853
+ console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file2})`);
3455
3854
  }
3456
3855
  } catch (error2) {
3457
3856
  if (this.verbose) {
3458
3857
  console.warn(
3459
- `[WebScreenAnalyzer] Failed to parse ${file}:`,
3858
+ `[WebScreenAnalyzer] Failed to parse ${file2}:`,
3460
3859
  error2 instanceof Error ? error2.message : error2
3461
3860
  );
3462
3861
  }
@@ -3807,7 +4206,8 @@ var WebScreenAnalyzer = class {
3807
4206
  if (type === "email") return "email";
3808
4207
  if (type === "tel") return "phone";
3809
4208
  if (type === "number") return "number";
3810
- if (type === "date" || type === "datetime-local" || type === "month" || type === "week") return "date";
4209
+ if (type === "date" || type === "datetime-local" || type === "month" || type === "week")
4210
+ return "date";
3811
4211
  const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
3812
4212
  if (inputMode === "email") return "email";
3813
4213
  if (inputMode === "tel") return "phone";
@@ -4121,7 +4521,9 @@ var WebScreenAnalyzer = class {
4121
4521
  }
4122
4522
  const callback = expr.arguments[0];
4123
4523
  if (!t10.isArrowFunctionExpression(callback) && !t10.isFunctionExpression(callback)) return;
4124
- const enclosing = nodePath.findParent((p) => p.isJSXElement());
4524
+ const enclosing = nodePath.findParent(
4525
+ (p) => p.isJSXElement()
4526
+ );
4125
4527
  const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
4126
4528
  const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
4127
4529
  const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
@@ -4223,16 +4625,19 @@ function firstCalledFunctionName(node) {
4223
4625
  }
4224
4626
  function containsWindowConfirm(body) {
4225
4627
  let found = false;
4226
- (0, import_traverse9.default)(body, {
4227
- noScope: true,
4228
- CallExpression: (nodePath) => {
4229
- const callee = nodePath.node.callee;
4230
- if (t10.isIdentifier(callee) && callee.name === "confirm") found = true;
4231
- if (t10.isMemberExpression(callee) && t10.isIdentifier(callee.object) && callee.object.name === "window" && t10.isIdentifier(callee.property) && callee.property.name === "confirm") {
4232
- found = true;
4628
+ (0, import_traverse9.default)(
4629
+ body,
4630
+ {
4631
+ noScope: true,
4632
+ CallExpression: (nodePath) => {
4633
+ const callee = nodePath.node.callee;
4634
+ if (t10.isIdentifier(callee) && callee.name === "confirm") found = true;
4635
+ if (t10.isMemberExpression(callee) && t10.isIdentifier(callee.object) && callee.object.name === "window" && t10.isIdentifier(callee.property) && callee.property.name === "confirm") {
4636
+ found = true;
4637
+ }
4233
4638
  }
4234
4639
  }
4235
- });
4640
+ );
4236
4641
  return found;
4237
4642
  }
4238
4643
  function mergeLocator(current, next) {
@@ -4272,32 +4677,38 @@ function collectionItemNames(callback) {
4272
4677
  function collectionDisplayFields(callback, itemNames) {
4273
4678
  const fields = /* @__PURE__ */ new Set();
4274
4679
  if (!callback.body) return [];
4275
- (0, import_traverse9.default)(callback.body, {
4276
- noScope: true,
4277
- MemberExpression: (nodePath) => {
4278
- const node = nodePath.node;
4279
- if (t10.isIdentifier(node.object) && itemNames.has(node.object.name) && t10.isIdentifier(node.property)) {
4280
- fields.add(node.property.name);
4680
+ (0, import_traverse9.default)(
4681
+ callback.body,
4682
+ {
4683
+ noScope: true,
4684
+ MemberExpression: (nodePath) => {
4685
+ const node = nodePath.node;
4686
+ if (t10.isIdentifier(node.object) && itemNames.has(node.object.name) && t10.isIdentifier(node.property)) {
4687
+ fields.add(node.property.name);
4688
+ }
4281
4689
  }
4282
4690
  }
4283
- });
4691
+ );
4284
4692
  return Array.from(fields).sort();
4285
4693
  }
4286
4694
  function collectionKeyField(callback, itemNames) {
4287
4695
  let keyField;
4288
4696
  if (!callback.body) return void 0;
4289
- (0, import_traverse9.default)(callback.body, {
4290
- noScope: true,
4291
- JSXAttribute: (nodePath) => {
4292
- const attr = nodePath.node;
4293
- if (!t10.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
4294
- if (!attr.value || !t10.isJSXExpressionContainer(attr.value)) return;
4295
- const expr = attr.value.expression;
4296
- if (t10.isMemberExpression(expr) && t10.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t10.isIdentifier(expr.property)) {
4297
- keyField = keyField ?? expr.property.name;
4697
+ (0, import_traverse9.default)(
4698
+ callback.body,
4699
+ {
4700
+ noScope: true,
4701
+ JSXAttribute: (nodePath) => {
4702
+ const attr = nodePath.node;
4703
+ if (!t10.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
4704
+ if (!attr.value || !t10.isJSXExpressionContainer(attr.value)) return;
4705
+ const expr = attr.value.expression;
4706
+ if (t10.isMemberExpression(expr) && t10.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t10.isIdentifier(expr.property)) {
4707
+ keyField = keyField ?? expr.property.name;
4708
+ }
4298
4709
  }
4299
4710
  }
4300
- });
4711
+ );
4301
4712
  return keyField;
4302
4713
  }
4303
4714
  function collectionRowAction(callback) {
@@ -4345,7 +4756,7 @@ function inferIdentityFields(keyField, displayFields) {
4345
4756
  const out = /* @__PURE__ */ new Set();
4346
4757
  if (keyField) out.add(keyField);
4347
4758
  for (const field of displayFields) {
4348
- if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
4759
+ if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
4349
4760
  }
4350
4761
  return Array.from(out);
4351
4762
  }
@@ -4403,7 +4814,7 @@ var WebNavigationAnalyzer = class {
4403
4814
  ...this.navigationExclude
4404
4815
  ];
4405
4816
  const files = await (0, import_fast_glob5.default)(patterns, { cwd: this.config.rootDir, ignore });
4406
- return files.map((file) => import_path6.default.join(this.config.rootDir, file));
4817
+ return files.map((file2) => import_path6.default.join(this.config.rootDir, file2));
4407
4818
  }
4408
4819
  // ── JSX <Route> style ────────────────────────────────────────────
4409
4820
  extractJsxRoutes(ast) {
@@ -4905,8 +5316,8 @@ var ZodParsedType = util.arrayToEnum([
4905
5316
  "set"
4906
5317
  ]);
4907
5318
  var getParsedType = (data) => {
4908
- const t12 = typeof data;
4909
- switch (t12) {
5319
+ const t15 = typeof data;
5320
+ switch (t15) {
4910
5321
  case "undefined":
4911
5322
  return ZodParsedType.undefined;
4912
5323
  case "string":
@@ -5178,8 +5589,8 @@ function getErrorMap() {
5178
5589
 
5179
5590
  // ../../node_modules/zod/v3/helpers/parseUtil.js
5180
5591
  var makeIssue = (params) => {
5181
- const { data, path: path9, errorMaps, issueData } = params;
5182
- const fullPath = [...path9, ...issueData.path || []];
5592
+ const { data, path: path11, errorMaps, issueData } = params;
5593
+ const fullPath = [...path11, ...issueData.path || []];
5183
5594
  const fullIssue = {
5184
5595
  ...issueData,
5185
5596
  path: fullPath
@@ -5295,11 +5706,11 @@ var errorUtil;
5295
5706
 
5296
5707
  // ../../node_modules/zod/v3/types.js
5297
5708
  var ParseInputLazyPath = class {
5298
- constructor(parent, value, path9, key) {
5709
+ constructor(parent, value, path11, key) {
5299
5710
  this._cachedPath = [];
5300
5711
  this.parent = parent;
5301
5712
  this.data = value;
5302
- this._path = path9;
5713
+ this._path = path11;
5303
5714
  this._key = key;
5304
5715
  }
5305
5716
  get path() {
@@ -8741,7 +9152,7 @@ var coerce = {
8741
9152
  };
8742
9153
  var NEVER = INVALID;
8743
9154
 
8744
- // ../shared/dist/chunk-MSOZJNXB.mjs
9155
+ // ../shared/dist/chunk-MKT45LRA.mjs
8745
9156
  var locatorSourceSchema = external_exports.enum([
8746
9157
  "appilotsId",
8747
9158
  "testID",
@@ -8767,7 +9178,16 @@ var locatorDescriptorSchema = external_exports.object({
8767
9178
  source: locatorSourceSchema.optional()
8768
9179
  }).passthrough();
8769
9180
  var signalDescriptorSchema = external_exports.object({
8770
- type: external_exports.enum(["navigation", "goBack", "toast", "modal", "inline-error", "data-arrival", "loading", "none"]).or(external_exports.string().min(1).max(60)),
9181
+ type: external_exports.enum([
9182
+ "navigation",
9183
+ "goBack",
9184
+ "toast",
9185
+ "modal",
9186
+ "inline-error",
9187
+ "data-arrival",
9188
+ "loading",
9189
+ "none"
9190
+ ]).or(external_exports.string().min(1).max(60)),
8771
9191
  target: external_exports.string().optional(),
8772
9192
  description: external_exports.string().optional()
8773
9193
  }).passthrough();
@@ -8781,7 +9201,20 @@ var waitPolicyDescriptorSchema = external_exports.object({
8781
9201
  signals: external_exports.array(signalDescriptorSchema).optional(),
8782
9202
  maxMs: external_exports.number().finite().optional()
8783
9203
  }).passthrough();
8784
- var targetDescriptorRoleSchema = external_exports.enum(["button", "submit", "input", "toggle", "select", "date", "list", "row", "menuItem", "modal", "custom"]).or(external_exports.string().min(1).max(60));
9204
+ var targetDescriptorRoleSchema = external_exports.enum([
9205
+ "button",
9206
+ "submit",
9207
+ "input",
9208
+ "toggle",
9209
+ "select",
9210
+ "date",
9211
+ "list",
9212
+ "row",
9213
+ "menuItem",
9214
+ "modal",
9215
+ "custom"
9216
+ ]).or(external_exports.string().min(1).max(60));
9217
+ var targetOriginSchema = external_exports.enum(["analyzer", "manifest"]).or(external_exports.string().min(1).max(40));
8785
9218
  var targetDescriptorSchema = external_exports.object({
8786
9219
  id: external_exports.string(),
8787
9220
  role: targetDescriptorRoleSchema,
@@ -8795,10 +9228,20 @@ var targetDescriptorSchema = external_exports.object({
8795
9228
  requiresConfirmation: external_exports.boolean().optional(),
8796
9229
  opensModal: external_exports.string().optional(),
8797
9230
  opensBottomSheet: external_exports.string().optional(),
8798
- sourceComponent: external_exports.string().optional()
9231
+ sourceComponent: external_exports.string().optional(),
9232
+ origin: targetOriginSchema.optional()
8799
9233
  }).passthrough();
8800
9234
  var flowStepDescriptorSchema = external_exports.object({
8801
- type: external_exports.enum(["navigate", "fill", "press", "select", "toggle", "wait", "confirm", "choose-list-item"]).or(external_exports.string().min(1).max(60)),
9235
+ type: external_exports.enum([
9236
+ "navigate",
9237
+ "fill",
9238
+ "press",
9239
+ "select",
9240
+ "toggle",
9241
+ "wait",
9242
+ "confirm",
9243
+ "choose-list-item"
9244
+ ]).or(external_exports.string().min(1).max(60)),
8802
9245
  target: external_exports.string().optional(),
8803
9246
  label: external_exports.string().optional(),
8804
9247
  description: external_exports.string().optional(),
@@ -9020,15 +9463,31 @@ var mfaRegenerateRecoveryCodesSchema = external_exports.object({
9020
9463
  password: external_exports.string().min(1, "Password is required"),
9021
9464
  code: totpCodeSchema
9022
9465
  });
9466
+ var deleteAccountSchema = external_exports.object({
9467
+ password: external_exports.string().min(1, "Password is required"),
9468
+ code: totpCodeSchema.optional(),
9469
+ recoveryCode: recoveryCodeSchema.optional(),
9470
+ confirm: external_exports.literal("DELETE MY ACCOUNT")
9471
+ }).refine((v) => !(v.code && v.recoveryCode), {
9472
+ message: "Provide either a TOTP code or a recovery code, not both",
9473
+ path: ["code"]
9474
+ });
9023
9475
  var createProjectSchema = external_exports.object({
9024
9476
  name: external_exports.string().min(1).max(100),
9025
9477
  description: external_exports.string().max(500).optional(),
9026
9478
  // Reference to a tenant-scoped provider (preferred). Optional at
9027
9479
  // creation so a project can be created and configured later.
9028
9480
  providerId: external_exports.string().min(1).optional(),
9029
- systemPrompt: external_exports.string().max(4096).optional()
9481
+ // 16k chars (~4k tokens): um prompt de CS real não cabe em 4096 — o
9482
+ // primeiro cliente de produção chegou com 6.385 e tomou 'Invalid input'
9483
+ // sem saber por quê. O teto continua existindo porque o systemPrompt
9484
+ // entra em TODO hop do relay: é proteção de TPM, não de gosto.
9485
+ systemPrompt: external_exports.string().max(16e3).optional()
9486
+ });
9487
+ var updateProjectSchema = createProjectSchema.partial().extend({
9488
+ providerId: external_exports.string().min(1).nullable().optional(),
9489
+ systemPrompt: external_exports.string().max(16e3).nullable().optional()
9030
9490
  });
9031
- var updateProjectSchema = createProjectSchema.partial();
9032
9491
  var providerKindSchema = external_exports.enum(["openai", "anthropic", "custom"]);
9033
9492
  var createProviderSchema = external_exports.object({
9034
9493
  name: external_exports.string().min(1).max(100),
@@ -9055,6 +9514,7 @@ var uploadMCPDocumentSchema = external_exports.object({
9055
9514
  content: external_exports.record(external_exports.unknown())
9056
9515
  });
9057
9516
  var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
9517
+ var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
9058
9518
  var createApiKeySchema = external_exports.object({
9059
9519
  name: external_exports.string().min(1).max(100),
9060
9520
  // Required for `sdk` (default), forbidden/ignored for `operator` —
@@ -9062,6 +9522,7 @@ var createApiKeySchema = external_exports.object({
9062
9522
  // missing scope still validates as the pre-existing SDK shape.
9063
9523
  projectId: external_exports.string().min(1).optional(),
9064
9524
  scope: apiKeyScopeSchema.default("sdk"),
9525
+ environment: apiKeyEnvironmentSchema.optional(),
9065
9526
  expiresAt: external_exports.string().datetime().optional()
9066
9527
  }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
9067
9528
  message: 'projectId is required for scope "sdk"',
@@ -9069,13 +9530,23 @@ var createApiKeySchema = external_exports.object({
9069
9530
  });
9070
9531
  var boundedString = (max) => external_exports.string().max(max);
9071
9532
  var clientPlatformSchema = external_exports.enum(["react-native", "web", "android", "ios"]).or(external_exports.string().min(1).max(40));
9533
+ var identityProvenanceSchema = external_exports.enum(["declared", "derived", "positional"]).or(external_exports.string().min(1).max(40));
9072
9534
  var snapshotInputSchema = external_exports.object({
9073
9535
  id: boundedString(160).optional(),
9536
+ provenance: identityProvenanceSchema.optional(),
9537
+ /**
9538
+ * False when the control is mounted but currently OUTSIDE the window —
9539
+ * below the fold, scrolled off, pushed sideways. Absent means the client
9540
+ * cannot tell (old React Native architecture, or any SDK published
9541
+ * before the field existed), which must never be read as `false`.
9542
+ */
9543
+ onScreen: external_exports.boolean().optional(),
9074
9544
  label: boundedString(300).optional(),
9075
9545
  value: boundedString(4096).optional(),
9076
9546
  placeholder: boundedString(300).optional(),
9077
9547
  editable: external_exports.boolean().optional(),
9078
9548
  secure: external_exports.boolean().optional(),
9549
+ submitsOnReturn: external_exports.boolean().optional(),
9079
9550
  type: boundedString(40).optional(),
9080
9551
  required: external_exports.boolean().optional(),
9081
9552
  invalid: external_exports.boolean().optional(),
@@ -9083,6 +9554,14 @@ var snapshotInputSchema = external_exports.object({
9083
9554
  }).passthrough();
9084
9555
  var snapshotButtonSchema = external_exports.object({
9085
9556
  id: boundedString(160).optional(),
9557
+ provenance: identityProvenanceSchema.optional(),
9558
+ /**
9559
+ * False when the control is mounted but currently OUTSIDE the window —
9560
+ * below the fold, scrolled off, pushed sideways. Absent means the client
9561
+ * cannot tell (old React Native architecture, or any SDK published
9562
+ * before the field existed), which must never be read as `false`.
9563
+ */
9564
+ onScreen: external_exports.boolean().optional(),
9086
9565
  label: boundedString(300).optional(),
9087
9566
  disabled: external_exports.boolean().optional(),
9088
9567
  inModal: external_exports.boolean().optional(),
@@ -9092,12 +9571,28 @@ var snapshotButtonSchema = external_exports.object({
9092
9571
  }).passthrough();
9093
9572
  var snapshotToggleSchema = external_exports.object({
9094
9573
  id: boundedString(160).optional(),
9574
+ provenance: identityProvenanceSchema.optional(),
9575
+ /**
9576
+ * False when the control is mounted but currently OUTSIDE the window —
9577
+ * below the fold, scrolled off, pushed sideways. Absent means the client
9578
+ * cannot tell (old React Native architecture, or any SDK published
9579
+ * before the field existed), which must never be read as `false`.
9580
+ */
9581
+ onScreen: external_exports.boolean().optional(),
9095
9582
  label: boundedString(300).optional(),
9096
9583
  value: external_exports.boolean().optional(),
9097
9584
  inModal: external_exports.boolean().optional()
9098
9585
  }).passthrough();
9099
9586
  var snapshotSliderSchema = external_exports.object({
9100
9587
  id: boundedString(160).optional(),
9588
+ provenance: identityProvenanceSchema.optional(),
9589
+ /**
9590
+ * False when the control is mounted but currently OUTSIDE the window —
9591
+ * below the fold, scrolled off, pushed sideways. Absent means the client
9592
+ * cannot tell (old React Native architecture, or any SDK published
9593
+ * before the field existed), which must never be read as `false`.
9594
+ */
9595
+ onScreen: external_exports.boolean().optional(),
9101
9596
  label: boundedString(300).optional(),
9102
9597
  value: external_exports.number().finite().optional(),
9103
9598
  min: external_exports.number().finite().optional(),
@@ -9138,6 +9633,26 @@ var snapshotListSchema = external_exports.object({
9138
9633
  // should chase, and the relay only reads a known subset.
9139
9634
  items: external_exports.array(external_exports.record(external_exports.unknown())).max(500).optional()
9140
9635
  }).passthrough();
9636
+ var snapshotNativeDialogSchema = external_exports.object({
9637
+ id: boundedString(160),
9638
+ title: boundedString(300).optional(),
9639
+ message: boundedString(2e3).optional(),
9640
+ buttons: external_exports.array(
9641
+ external_exports.object({
9642
+ label: boundedString(160),
9643
+ style: external_exports.enum(["default", "cancel", "destructive"]).optional()
9644
+ }).passthrough()
9645
+ ).max(10)
9646
+ }).passthrough();
9647
+ var snapshotScrollableSchema = external_exports.object({
9648
+ id: boundedString(160),
9649
+ containerType: boundedString(60).optional(),
9650
+ label: boundedString(300).optional(),
9651
+ scrollOffsetY: external_exports.number().finite().optional(),
9652
+ canScrollUp: external_exports.boolean().optional(),
9653
+ canScrollDown: external_exports.boolean().optional(),
9654
+ horizontal: external_exports.boolean().optional()
9655
+ }).passthrough();
9141
9656
  var snapshotChoiceGroupSchema = external_exports.object({
9142
9657
  index: external_exports.number().int().optional(),
9143
9658
  id: boundedString(160).optional(),
@@ -9154,6 +9669,14 @@ var snapshotElementSchema = external_exports.object({
9154
9669
  disabled: external_exports.boolean().optional(),
9155
9670
  selected: external_exports.boolean().optional(),
9156
9671
  source: boundedString(40).optional(),
9672
+ provenance: identityProvenanceSchema.optional(),
9673
+ /**
9674
+ * False when the control is mounted but currently OUTSIDE the window —
9675
+ * below the fold, scrolled off, pushed sideways. Absent means the client
9676
+ * cannot tell (old React Native architecture, or any SDK published
9677
+ * before the field existed), which must never be read as `false`.
9678
+ */
9679
+ onScreen: external_exports.boolean().optional(),
9157
9680
  targetId: boundedString(160).optional(),
9158
9681
  listContext: external_exports.record(external_exports.unknown()).optional(),
9159
9682
  inModal: external_exports.boolean().optional()
@@ -9170,7 +9693,34 @@ var agentSnapshotSchema = external_exports.object({
9170
9693
  modalOpen: external_exports.boolean().optional(),
9171
9694
  lists: external_exports.array(snapshotListSchema).max(100).optional(),
9172
9695
  choiceGroups: external_exports.array(snapshotChoiceGroupSchema).max(100).optional(),
9173
- elements: external_exports.array(snapshotElementSchema).max(1e3).optional()
9696
+ scrollables: external_exports.array(snapshotScrollableSchema).max(20).optional(),
9697
+ nativeDialog: snapshotNativeDialogSchema.optional(),
9698
+ elements: external_exports.array(snapshotElementSchema).max(1e3).optional(),
9699
+ /**
9700
+ * The client clamped this observation to the caps above and lost
9701
+ * data doing it (`clampSnapshotToWireLimits` in
9702
+ * `@appilots/client-core`). Read by the prompt serializer so the
9703
+ * model is told the view is partial — a screen truncated at 500
9704
+ * texts must not be read as a screen that only has 500 things on
9705
+ * it. Optional: older SDKs never clamp and never send it.
9706
+ */
9707
+ truncated: external_exports.boolean().optional(),
9708
+ /**
9709
+ * How many of this screen's controls the client could name, split by
9710
+ * `identityProvenance`. Diagnostic only — the relay never grounds an
9711
+ * action on it — but it is the number that says whether an app needs
9712
+ * annotating, and comparing it between a debug and a release build is
9713
+ * the first direct measurement of what minification costs the agent.
9714
+ */
9715
+ stats: external_exports.object({
9716
+ visitedFibers: external_exports.number().int().nonnegative().optional(),
9717
+ skippedHidden: external_exports.number().int().nonnegative().optional(),
9718
+ identity: external_exports.object({
9719
+ declared: external_exports.number().int().nonnegative().optional(),
9720
+ derived: external_exports.number().int().nonnegative().optional(),
9721
+ positional: external_exports.number().int().nonnegative().optional()
9722
+ }).passthrough().optional()
9723
+ }).passthrough().optional()
9174
9724
  }).passthrough();
9175
9725
  var agentContextSchema = external_exports.object({
9176
9726
  /**
@@ -9220,6 +9770,19 @@ var agentContextSchema = external_exports.object({
9220
9770
  ).max(200).optional(),
9221
9771
  loadingPending: external_exports.boolean().optional()
9222
9772
  }).passthrough();
9773
+ var introspectionFailureReasonSchema = external_exports.enum([
9774
+ "sentinel-missing-internals",
9775
+ "never-mounted",
9776
+ "render-crashed",
9777
+ "names-mangled",
9778
+ "walk-recognized-nothing"
9779
+ ]);
9780
+ var introspectionDiagnosticsSchema = external_exports.object({
9781
+ captured: external_exports.boolean(),
9782
+ failureReason: introspectionFailureReasonSchema.nullish(),
9783
+ reactVersion: boundedString(32).nullish(),
9784
+ failureCount: external_exports.number().int().min(0).max(1e6).optional()
9785
+ });
9223
9786
  var sendMessageSchema = external_exports.object({
9224
9787
  content: external_exports.string().min(1).max(4096),
9225
9788
  sessionId: external_exports.string().optional(),
@@ -9229,7 +9792,20 @@ var sendMessageSchema = external_exports.object({
9229
9792
  * the active MCP doc and emits `mcp_version_mismatch` telemetry when
9230
9793
  * they differ. Optional — older SDKs simply skip the check.
9231
9794
  */
9232
- mcpVersion: external_exports.string().max(64).optional()
9795
+ mcpVersion: external_exports.string().max(64).optional(),
9796
+ /**
9797
+ * SDK-side introspection health. Only sent when it FAILED — a client
9798
+ * that reached the React tree omits the field entirely, so the happy
9799
+ * path costs nothing on the wire.
9800
+ *
9801
+ * Without this, an app whose introspection is dead sends a snapshot
9802
+ * that is empty but perfectly well-formed, indistinguishable from a
9803
+ * genuinely empty screen. The server emits
9804
+ * `introspection_unavailable` so the failure is visible to us instead
9805
+ * of waiting for a bug report. Optional — older SDKs simply never
9806
+ * send it.
9807
+ */
9808
+ introspection: introspectionDiagnosticsSchema.optional()
9233
9809
  });
9234
9810
  var agentActionTypeSchema = external_exports.enum([
9235
9811
  "navigate",
@@ -9240,11 +9816,22 @@ var agentActionTypeSchema = external_exports.enum([
9240
9816
  "custom"
9241
9817
  ]);
9242
9818
  var navigationPayloadSchema = external_exports.object({
9243
- screenName: external_exports.string().min(1),
9819
+ // Optional ONLY so `goBack` can exist (#398): it returns to whatever
9820
+ // is beneath on the stack and has no destination to name. Every other
9821
+ // action is still required to carry one — enforced by the refine
9822
+ // below rather than by the field, so the error names the real problem
9823
+ // ("navigate needs a screenName") instead of a missing key.
9824
+ screenName: external_exports.string().min(1).optional(),
9244
9825
  params: external_exports.record(external_exports.unknown()).optional(),
9245
9826
  navigationAction: external_exports.enum(["push", "navigate", "replace", "goBack", "reset"]).default("navigate").optional(),
9246
9827
  path: external_exports.array(external_exports.string().min(1)).optional()
9247
- }).passthrough();
9828
+ }).passthrough().refine(
9829
+ (payload) => payload.navigationAction === "goBack" || typeof payload.screenName === "string" && payload.screenName.length > 0,
9830
+ {
9831
+ message: 'screenName is required for every navigationAction except "goBack"',
9832
+ path: ["screenName"]
9833
+ }
9834
+ );
9248
9835
  var formFillFieldSchema = external_exports.object({
9249
9836
  fieldId: external_exports.string().min(1),
9250
9837
  fieldType: external_exports.enum(["text", "select", "toggle", "date", "number", "custom"]).default("text").optional(),
@@ -9578,6 +10165,8 @@ var sandboxObservationSchema = external_exports.object({
9578
10165
  inputs: external_exports.array(
9579
10166
  external_exports.object({
9580
10167
  id: external_exports.string().max(120),
10168
+ provenance: identityProvenanceSchema.optional(),
10169
+ onScreen: external_exports.boolean().optional(),
9581
10170
  label: external_exports.string().max(200).optional(),
9582
10171
  value: external_exports.string().max(2e3).optional(),
9583
10172
  disabled: external_exports.boolean().optional(),
@@ -9591,6 +10180,8 @@ var sandboxObservationSchema = external_exports.object({
9591
10180
  buttons: external_exports.array(
9592
10181
  external_exports.object({
9593
10182
  id: external_exports.string().max(120),
10183
+ provenance: identityProvenanceSchema.optional(),
10184
+ onScreen: external_exports.boolean().optional(),
9594
10185
  label: external_exports.string().max(200).optional(),
9595
10186
  disabled: external_exports.boolean().optional(),
9596
10187
  /** Risk metadata (e.g. "high") the SDK forwards for danger UI. */
@@ -9612,6 +10203,8 @@ var sandboxObservationSchema = external_exports.object({
9612
10203
  sliders: external_exports.array(
9613
10204
  external_exports.object({
9614
10205
  id: external_exports.string().max(120),
10206
+ provenance: identityProvenanceSchema.optional(),
10207
+ onScreen: external_exports.boolean().optional(),
9615
10208
  label: external_exports.string().max(200).optional(),
9616
10209
  value: external_exports.number().finite().optional(),
9617
10210
  min: external_exports.number().finite().optional(),
@@ -9641,6 +10234,44 @@ var sandboxObservationSchema = external_exports.object({
9641
10234
  ).max(200).optional()
9642
10235
  }).passthrough()
9643
10236
  ).max(20).optional(),
10237
+ /**
10238
+ * Toggles exactly as the SDK snapshot reports them (#390).
10239
+ *
10240
+ * Their absence is why `generality-component-control-smarthome`
10241
+ * came back `errored` with a 422 instead of measuring anything:
10242
+ * this schema is `.strict()`, so a scenario that declares a switch
10243
+ * is rejected before the agent is ever asked. Which means the
10244
+ * corpus could not express the single control behind failure 02 of
10245
+ * the functional audit — "disable an agent; the switch never
10246
+ * moves". The one defect the eval most needed to catch was the one
10247
+ * shape it could not describe.
10248
+ */
10249
+ toggles: external_exports.array(
10250
+ external_exports.object({
10251
+ id: external_exports.string().max(120),
10252
+ provenance: identityProvenanceSchema.optional(),
10253
+ onScreen: external_exports.boolean().optional(),
10254
+ label: external_exports.string().max(200).optional(),
10255
+ value: external_exports.boolean().optional(),
10256
+ disabled: external_exports.boolean().optional()
10257
+ })
10258
+ ).max(100).optional(),
10259
+ /**
10260
+ * The interaction graph — the `el:*` ids the prompt tells the model
10261
+ * to prefer over ordinal handles. Passthrough per entry for the
10262
+ * same reason `lists` is: the SDK adds fields faster than this
10263
+ * schema should chase them.
10264
+ */
10265
+ elements: external_exports.array(
10266
+ external_exports.object({
10267
+ id: external_exports.string().max(160),
10268
+ role: external_exports.string().max(40).optional(),
10269
+ label: external_exports.string().max(300).optional()
10270
+ }).passthrough()
10271
+ ).max(500).optional(),
10272
+ /** Screen-level state the relay renders and branches on. */
10273
+ loading: external_exports.boolean().optional(),
10274
+ modalOpen: external_exports.boolean().optional(),
9644
10275
  extra: external_exports.record(external_exports.unknown()).optional()
9645
10276
  }).strict();
9646
10277
  var sandboxRunRequestSchema = external_exports.object({
@@ -9681,13 +10312,29 @@ var sandboxRunRequestSchema = external_exports.object({
9681
10312
  * heuristics, forces tool choice when the last user message carries a
9682
10313
  * failure marker). Only meaningful alongside `history`.
9683
10314
  */
9684
- isRecoveryHop: external_exports.boolean().optional()
10315
+ isRecoveryHop: external_exports.boolean().optional(),
10316
+ /**
10317
+ * Run this call against the application structure supplied here
10318
+ * instead of the project's active MCP document.
10319
+ *
10320
+ * Sandbox-only, and the relay honours it only on a `dryRun` call —
10321
+ * see `resolveMcpOverride` in apps/api. It exists because the eval
10322
+ * corpus could otherwise only ever describe ONE app: every scenario
10323
+ * resolves to the same seeded project, so every navigate destination,
10324
+ * every screen title and every bit of domain priming in the prompt
10325
+ * came from the same vehicle-management demo. A suite that can only
10326
+ * pose questions about one app cannot measure whether the agent works
10327
+ * on another one.
10328
+ *
10329
+ * Deliberately loose (`z.record`): the MCP document's shape is owned
10330
+ * by the generator, and pinning it here would turn every additive
10331
+ * generator change into a 422 in the sandbox.
10332
+ */
10333
+ applicationStructure: external_exports.record(external_exports.unknown()).optional()
9685
10334
  }).strict();
9686
10335
  var relayDiagnosticsSchema = external_exports.object({
9687
10336
  unverifiedTargets: external_exports.array(external_exports.string()),
9688
- guardsFired: external_exports.array(
9689
- external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })
9690
- ),
10337
+ guardsFired: external_exports.array(external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })),
9691
10338
  repairMode: external_exports.enum(["repair", "strict", "disabled"]),
9692
10339
  promptVersion: external_exports.string(),
9693
10340
  // Defaulted for producers that predate the field: before it existed
@@ -9946,6 +10593,30 @@ var identifyUserSchema = external_exports.object({
9946
10593
  name: external_exports.string().min(1).max(200).optional(),
9947
10594
  identifiers: endUserIdentifiersSchema.optional()
9948
10595
  });
10596
+ var MAX_EXTERNAL_USER_ID_LENGTH = 128;
10597
+ function normalizeExternalUserId(value) {
10598
+ let raw;
10599
+ if (typeof value === "string") raw = value;
10600
+ else if (typeof value === "number" && Number.isFinite(value)) raw = String(value);
10601
+ else return void 0;
10602
+ const trimmed = raw.trim();
10603
+ if (!trimmed) return void 0;
10604
+ if (trimmed.length > MAX_EXTERNAL_USER_ID_LENGTH) return void 0;
10605
+ return trimmed;
10606
+ }
10607
+ var MAX_DEVICE_INFO_BYTES = 4096;
10608
+ var sessionDeviceInfoSchema = external_exports.object({
10609
+ platform: external_exports.string().max(64).optional(),
10610
+ osVersion: external_exports.string().max(64).optional(),
10611
+ appVersion: external_exports.string().max(64).optional(),
10612
+ sdkVersion: external_exports.string().max(64).optional()
10613
+ }).passthrough().refine((info2) => JSON.stringify(info2).length <= MAX_DEVICE_INFO_BYTES, {
10614
+ message: `device_info exceeds ${MAX_DEVICE_INFO_BYTES} bytes`
10615
+ });
10616
+ var createAgentSessionSchema = external_exports.object({
10617
+ userId: external_exports.preprocess(normalizeExternalUserId, external_exports.string().optional()),
10618
+ deviceInfo: sessionDeviceInfoSchema.optional().catch(void 0)
10619
+ });
9949
10620
  var upsertEndUserSchema = external_exports.object({
9950
10621
  projectId: external_exports.string().min(1).max(64),
9951
10622
  externalId: external_exports.string().min(1).max(128),
@@ -9969,14 +10640,16 @@ async function loadManifest(rootDir, manifestPath) {
9969
10640
  if (error2?.code === "ENOENT") {
9970
10641
  return { manifest: null, resolvedPath };
9971
10642
  }
9972
- throw new Error(`Failed to read Appilots manifest at ${resolvedPath}: ${error2?.message ?? String(error2)}`);
10643
+ throw new Error(
10644
+ `Failed to read Appilots manifest at ${resolvedPath}: ${stringifyUnknown(error2)}`
10645
+ );
9973
10646
  }
9974
10647
  let parsedJson;
9975
10648
  try {
9976
10649
  parsedJson = JSON.parse(raw);
9977
10650
  } catch (error2) {
9978
10651
  throw new Error(
9979
- `Appilots manifest at ${resolvedPath} is not valid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
10652
+ `Appilots manifest at ${resolvedPath} is not valid JSON: ${stringifyUnknown(error2)}`
9980
10653
  );
9981
10654
  }
9982
10655
  const result = manifestSchema.safeParse(parsedJson);
@@ -9990,12 +10663,48 @@ Each entry in "screens" must match the ScreenDescriptor shape (see docs/platform
9990
10663
  }
9991
10664
  return { manifest: result.data, resolvedPath };
9992
10665
  }
10666
+ function mergeScreen(base, declared) {
10667
+ const fields = { ...base };
10668
+ for (const [key, value] of Object.entries(declared)) {
10669
+ if (value === void 0) continue;
10670
+ if (Array.isArray(value) && value.length === 0) continue;
10671
+ fields[key] = value;
10672
+ }
10673
+ const merged = fields;
10674
+ merged.targets = mergeById(base.targets, markManifestOrigin(declared.targets));
10675
+ merged.actions = mergeById(base.actions, declared.actions) ?? [];
10676
+ if (base.agentHints || declared.agentHints) {
10677
+ merged.agentHints = { ...base.agentHints, ...declared.agentHints };
10678
+ }
10679
+ return merged;
10680
+ }
10681
+ function mergeById(base, overlay) {
10682
+ if (!overlay || overlay.length === 0) return base;
10683
+ if (!base || base.length === 0) return overlay;
10684
+ const byId = /* @__PURE__ */ new Map();
10685
+ for (const entry of base) byId.set(entry.id, entry);
10686
+ for (const entry of overlay) {
10687
+ const existing = byId.get(entry.id);
10688
+ byId.set(entry.id, existing ? { ...existing, ...entry } : entry);
10689
+ }
10690
+ return Array.from(byId.values());
10691
+ }
10692
+ function markManifestOrigin(targets) {
10693
+ if (!targets || targets.length === 0) return targets;
10694
+ return targets.map((target) => ({ origin: "manifest", ...target }));
10695
+ }
9993
10696
  function mergeManifestScreens(analyzerScreens, manifestScreens) {
9994
10697
  if (!manifestScreens || manifestScreens.length === 0) return analyzerScreens;
9995
10698
  const byName = /* @__PURE__ */ new Map();
9996
10699
  for (const screen of analyzerScreens) byName.set(screen.name, screen);
9997
- for (const screen of manifestScreens) byName.set(screen.name, screen);
9998
- return Array.from(byName.values());
10700
+ for (const declared of manifestScreens) {
10701
+ const base = byName.get(declared.name);
10702
+ byName.set(
10703
+ declared.name,
10704
+ base ? mergeScreen(base, declared) : { ...declared, targets: markManifestOrigin(declared.targets) }
10705
+ );
10706
+ }
10707
+ return Array.from(byName.values());
9999
10708
  }
10000
10709
  function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
10001
10710
  if (!manifestNavigation) return analyzerNavigation;
@@ -10069,7 +10778,12 @@ var MCPGenerator = class _MCPGenerator {
10069
10778
  }
10070
10779
  const mergedScreens = mergeManifestScreens(analyzed.screens, manifest?.screens);
10071
10780
  const navigation = mergeManifestNavigation(analyzed.navigation, manifest?.navigation);
10072
- const agentReadyScreens = mergedScreens.map(enrichScreenForAgent);
10781
+ const agentReadyScreens = mergedScreens.map(
10782
+ (screen) => enrichScreenForAgent({
10783
+ ...screen,
10784
+ filePath: screen.filePath ? import_node_path3.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
10785
+ })
10786
+ );
10073
10787
  const projectInfo = await this.getProjectInfo();
10074
10788
  const projectName = projectInfo.name;
10075
10789
  console.log(`[MCPGenerator] Project name: ${projectName}`);
@@ -10092,10 +10806,7 @@ var MCPGenerator = class _MCPGenerator {
10092
10806
  };
10093
10807
  const serialized = JSON.stringify(document, null, 2);
10094
10808
  const checksum = this.calculateChecksum(serialized);
10095
- const filePath = import_node_path3.default.resolve(
10096
- outputDir,
10097
- `mcp-document.${this.options.format}`
10098
- );
10809
+ const filePath = import_node_path3.default.resolve(outputDir, `mcp-document.${this.options.format}`);
10099
10810
  await (0, import_promises4.writeFile)(filePath, serialized, "utf-8");
10100
10811
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
10101
10812
  const checksumFilePath = import_node_path3.default.resolve(outputDir, ".appilots-checksum");
@@ -10211,14 +10922,136 @@ function reportMetadataLint(document, strict) {
10211
10922
  }
10212
10923
  }
10213
10924
 
10925
+ // src/cli/utils/remote-drift.ts
10926
+ var import_node_fs = require("fs");
10927
+ var import_node_path4 = require("path");
10928
+ var STUB_RATIO = 0.5;
10929
+ function compareWithRemote(local, remote) {
10930
+ if (remote.screensCount === local.screens) return null;
10931
+ return {
10932
+ localScreens: local.screens,
10933
+ remoteScreens: remote.screensCount,
10934
+ remoteId: remote.id,
10935
+ severe: local.screens > 0 && remote.screensCount <= local.screens * STUB_RATIO
10936
+ };
10937
+ }
10938
+ function readLocalDocumentSummary(outputDir) {
10939
+ try {
10940
+ const raw = (0, import_node_fs.readFileSync)((0, import_node_path4.join)(outputDir, "mcp-document.json"), "utf-8");
10941
+ const parsed = JSON.parse(raw);
10942
+ if (typeof parsed?.metadata?.totalScreens !== "number") return null;
10943
+ return { screens: parsed.metadata.totalScreens };
10944
+ } catch {
10945
+ return null;
10946
+ }
10947
+ }
10948
+ function reportRemoteDrift(drift, options = {}) {
10949
+ if (!drift.severe) {
10950
+ info(
10951
+ `Server's active document differs from what was just generated (active: ${drift.remoteId}, ${drift.remoteScreens} screens).`
10952
+ );
10953
+ return;
10954
+ }
10955
+ warn(
10956
+ `The agent is ${options.uploadFailed ? "still " : ""}running on a ${drift.remoteScreens}-screen document while this app has ${drift.localScreens} screens.`
10957
+ );
10958
+ info(` Active on server: ${drift.remoteId} (${drift.remoteScreens} screens)`);
10959
+ info(` Just generated: ${drift.localScreens} screens`);
10960
+ info("");
10961
+ info("While a document that small is active the agent cannot see the other");
10962
+ info("screens, and missions that need them fail in ways that read as model");
10963
+ info("errors. The usual causes: an older version activated from the");
10964
+ info("dashboard, a sync that went to a different server, or a database");
10965
+ info("seeded before the seed learned to refuse partial documents.");
10966
+ info("");
10967
+ if (options.uploadFailed) {
10968
+ info("Nothing was uploaded just now, so this is unchanged. Fix the error");
10969
+ info("above and run sync again \u2014 e2e runs before that will fail on");
10970
+ info("navigation, not on anything the model did.");
10971
+ } else {
10972
+ info("Uploading now replaces it.");
10973
+ }
10974
+ }
10975
+
10976
+ // src/cli/utils/upload-failure.ts
10977
+ function explainUploadFailure(failure, serverUrl) {
10978
+ const status = failure.errorStatus ?? 0;
10979
+ if (status === 401) {
10980
+ info("");
10981
+ info("`sync` authenticates with the project API key (`ak_\u2026`) \u2014 the same key");
10982
+ info("the SDK uses. Check that it is the key for THIS project, is still");
10983
+ info("active, and reaches the server you meant:");
10984
+ info(` Server: ${serverUrl ?? "(default)"}`);
10985
+ info("Set it in .appilotsrc (`apiKey`), via APPILOTS_API_KEY, or --api-key.");
10986
+ info("");
10987
+ info("A dashboard login token will not work here and is not needed: the CLI");
10988
+ info("talks to /cli/sync, not the dashboard's /mcp-documents.");
10989
+ return;
10990
+ }
10991
+ if (failure.errorCode === "WORKSPACE_INACTIVE") {
10992
+ info("");
10993
+ info("The workspace is deactivated. Re-activate it in the dashboard");
10994
+ info("(Settings \u2192 Workspace) and run sync again.");
10995
+ return;
10996
+ }
10997
+ if (status === 404) {
10998
+ info("");
10999
+ info(`No /cli/sync at ${serverUrl ?? "(default)"} \u2014 check serverUrl points at`);
11000
+ info("an Appilots API (and one recent enough to serve the CLI routes).");
11001
+ return;
11002
+ }
11003
+ if (status === 422) {
11004
+ info("");
11005
+ info("The server rejected the document itself. Inspect what was generated:");
11006
+ info(" .appilots/mcp-document.json");
11007
+ return;
11008
+ }
11009
+ if (status >= 500) {
11010
+ info("");
11011
+ info("The server failed to process the upload. It was retried already \u2014");
11012
+ info("if it persists, the API logs hold the matching request id.");
11013
+ }
11014
+ }
11015
+
11016
+ // src/cli/utils/empty-document.ts
11017
+ function assertDocumentHasScreens(document, options = {}) {
11018
+ if ((document.metadata?.totalScreens ?? 0) > 0) return;
11019
+ if (options.allowEmpty) {
11020
+ info("No screens found, continuing because --allow-empty was passed.");
11021
+ return;
11022
+ }
11023
+ error("No screens found \u2014 this application map would give the agent nothing to navigate.");
11024
+ info("");
11025
+ info("The analyzer looks for React Navigation: a <NavigationContainer> and");
11026
+ info("<Stack.Screen> / <Tab.Screen> declarations. It found none.");
11027
+ info("");
11028
+ info("The two usual causes:");
11029
+ info(" \u2022 The app uses Expo Router (or another file-based router). It is the");
11030
+ info(" default for `npx create-expo-app` and the analyzer cannot read it.");
11031
+ info(" \u2022 The screens live outside the analyzed paths \u2014 check `include`,");
11032
+ info(" `exclude` and `screenPatterns` in your Appilots config.");
11033
+ info("");
11034
+ info("The escape hatch, which works for any router: declare your screens in");
11035
+ info("an `appilots.manifest.json` at the project root. Its screens are merged");
11036
+ info("on top of whatever the analyzer found, so a fully declared app needs no");
11037
+ info("React Navigation at all. See the CLI docs for the shape.");
11038
+ info("");
11039
+ info("If an empty map really is what you want, pass --allow-empty.");
11040
+ process.exit(1);
11041
+ }
11042
+
10214
11043
  // src/cli/commands/sync.ts
10215
11044
  function syncCommand() {
10216
- 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) => {
11045
+ 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(
11046
+ "--strict-metadata",
11047
+ "Fail when mutating actions are missing effect/riskLevel metadata",
11048
+ false
11049
+ ).option("--allow-empty", "Do not fail when the application map has zero screens", false).action(async (options) => {
10217
11050
  if (options.json) setLogMode("json");
10218
11051
  else if (options.quiet) setLogMode("quiet");
10219
11052
  try {
10220
11053
  banner();
10221
- const loaded = loadConfig();
11054
+ const loaded = loadConfig(warn);
10222
11055
  const apiKey = options.apiKey || loaded?.apiKey;
10223
11056
  if (!apiKey) {
10224
11057
  error("No .appilotsrc configuration found");
@@ -10229,7 +11062,7 @@ function syncCommand() {
10229
11062
  return;
10230
11063
  }
10231
11064
  const config = {
10232
- serverUrl: "http://localhost:4000",
11065
+ serverUrl: DEFAULT_SERVER_URL,
10233
11066
  outputDir: ".appilots",
10234
11067
  ...loaded,
10235
11068
  apiKey,
@@ -10243,7 +11076,7 @@ function syncCommand() {
10243
11076
  const outputDir = config.outputDir || ".appilots";
10244
11077
  let previousChecksum = "";
10245
11078
  try {
10246
- const checksumPath = (0, import_node_path4.join)(outputDir, ".appilots-checksum");
11079
+ const checksumPath = (0, import_node_path5.join)(outputDir, ".appilots-checksum");
10247
11080
  previousChecksum = (await (0, import_promises5.readFile)(checksumPath, "utf-8")).trim();
10248
11081
  } catch {
10249
11082
  }
@@ -10264,22 +11097,32 @@ function syncCommand() {
10264
11097
  });
10265
11098
  const output = await generator.generate();
10266
11099
  spinner.stop();
11100
+ assertDocumentHasScreens(output.document, {
11101
+ rootDir: process.cwd(),
11102
+ allowEmpty: Boolean(options.allowEmpty)
11103
+ });
10267
11104
  success(
10268
11105
  `Found ${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
10269
11106
  );
10270
11107
  reportMetadataLint(output.document, Boolean(options.strictMetadata));
11108
+ spinner = createSpinner("Checking remote status...");
11109
+ spinner.start();
11110
+ const remote = await apiClient.status();
11111
+ spinner.stop();
11112
+ const remoteActive = remote.error ? null : remote.activeMcp ?? null;
11113
+ const drift = remoteActive ? compareWithRemote({ screens: output.document.metadata.totalScreens }, remoteActive) : null;
10271
11114
  if (!options.force && previousChecksum && previousChecksum === output.checksum) {
10272
- spinner = createSpinner("Checking remote status...");
10273
- spinner.start();
10274
- const status = await apiClient.status();
10275
- spinner.stop();
10276
- if (status.error) {
10277
- warn(`Could not verify remote: ${status.error}`);
11115
+ if (remote.error) {
11116
+ warn(`Could not verify remote: ${remote.error}`);
11117
+ info("Uploading anyway...");
11118
+ } else if (remoteActive) {
11119
+ if (!drift) {
11120
+ success("Already up to date (no local or remote changes)");
11121
+ jsonOutput({ success: true, unchanged: true, checksum: output.checksum });
11122
+ return;
11123
+ }
11124
+ reportRemoteDrift(drift);
10278
11125
  info("Uploading anyway...");
10279
- } else if (status.activeMcp) {
10280
- success("Already up to date (no local or remote changes)");
10281
- jsonOutput({ success: true, unchanged: true, checksum: output.checksum });
10282
- return;
10283
11126
  }
10284
11127
  }
10285
11128
  spinner = createSpinner("Uploading to Appilots...");
@@ -10292,13 +11135,33 @@ function syncCommand() {
10292
11135
  spinner.stop();
10293
11136
  if (!syncResult.success) {
10294
11137
  error(`Sync failed: ${syncResult.error}`);
10295
- jsonOutput({ success: false, error: syncResult.error });
11138
+ hintUnreachableServer(config.serverUrl, syncResult.error);
11139
+ explainUploadFailure(syncResult, config.serverUrl);
11140
+ if (drift?.severe) {
11141
+ reportRemoteDrift(drift, { uploadFailed: true });
11142
+ }
11143
+ jsonOutput({
11144
+ success: false,
11145
+ error: syncResult.error,
11146
+ errorCode: syncResult.errorCode,
11147
+ errorStatus: syncResult.errorStatus,
11148
+ activeDocument: remoteActive,
11149
+ drift
11150
+ });
10296
11151
  process.exit(1);
10297
11152
  }
10298
11153
  if (syncResult.unchanged && !options.force) {
10299
11154
  success("Already up to date on server");
11155
+ if (syncResult.activated) {
11156
+ info(`Re-activated document ${syncResult.id} (it was stored but not active).`);
11157
+ }
10300
11158
  } else {
10301
11159
  success("Successfully synced MCP document");
11160
+ if (drift?.severe) {
11161
+ info(
11162
+ `Replaced the ${drift.remoteScreens}-screen document that was active (${drift.remoteId}) \u2014 the agent could not see the other ${drift.localScreens - drift.remoteScreens} screens until now.`
11163
+ );
11164
+ }
10302
11165
  info(`Document ID: ${syncResult.id}`);
10303
11166
  info(`Checksum: ${syncResult.checksum}`);
10304
11167
  if (config.projectId) {
@@ -10328,8 +11191,8 @@ function syncCommand() {
10328
11191
  actionsCount: syncResult.actionsCount ?? output.document.metadata.totalActions
10329
11192
  });
10330
11193
  } catch (err) {
10331
- error(`Sync failed: ${err instanceof Error ? err.message : String(err)}`);
10332
- jsonOutput({ success: false, error: err instanceof Error ? err.message : String(err) });
11194
+ error(`Sync failed: ${stringifyUnknown(err)}`);
11195
+ jsonOutput({ success: false, error: stringifyUnknown(err) });
10333
11196
  if (options.verbose) {
10334
11197
  console.error(err);
10335
11198
  }
@@ -10340,12 +11203,12 @@ function syncCommand() {
10340
11203
 
10341
11204
  // src/cli/commands/watch.ts
10342
11205
  var import_commander3 = require("commander");
10343
- var import_node_fs = require("fs");
11206
+ var import_node_fs2 = require("fs");
10344
11207
  function watchCommand() {
10345
11208
  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) => {
10346
11209
  try {
10347
11210
  banner();
10348
- const config = loadConfig();
11211
+ const config = loadConfig(warn);
10349
11212
  if (!config) {
10350
11213
  error("No .appilotsrc configuration found");
10351
11214
  info("Run: appilots init --api-key <your-key>");
@@ -10374,7 +11237,10 @@ function watchCommand() {
10374
11237
  };
10375
11238
  const generator = new MCPGenerator(generatorConfig);
10376
11239
  const initialOutput = await generator.generate();
10377
- const initialSync = await apiClient.sync(initialOutput.document, initialOutput.document.version);
11240
+ const initialSync = await apiClient.sync(
11241
+ initialOutput.document,
11242
+ initialOutput.document.version
11243
+ );
10378
11244
  if (!initialSync.success) {
10379
11245
  error(`Initial sync failed: ${initialSync.error}`);
10380
11246
  process.exit(1);
@@ -10384,45 +11250,41 @@ function watchCommand() {
10384
11250
  dim("Press Ctrl+C to stop\n");
10385
11251
  let debounceTimer = null;
10386
11252
  const DEBOUNCE_MS = 500;
10387
- const watcher = (0, import_node_fs.watch)(
10388
- process.cwd(),
10389
- { recursive: true },
10390
- async (eventType, filename) => {
10391
- if (!filename) return;
10392
- if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
10393
- return;
10394
- }
10395
- if (debounceTimer) {
10396
- clearTimeout(debounceTimer);
10397
- }
10398
- debounceTimer = setTimeout(async () => {
10399
- try {
10400
- if (options.verbose) {
10401
- info(`Change detected: ${filename}`);
10402
- }
10403
- const spinner = createSpinner("Re-analyzing project...");
10404
- spinner.start();
10405
- const newGenerator = new MCPGenerator(generatorConfig);
10406
- const output = await newGenerator.generate();
10407
- spinner.text = "Uploading changes...";
10408
- const syncResult = await apiClient.sync(output.document, output.document.version);
10409
- spinner.stop();
10410
- if (!syncResult.success) {
10411
- error(`Sync failed: ${syncResult.error}`);
10412
- } else if (syncResult.unchanged) {
10413
- info("Changes synced (no content changes detected)");
10414
- } else {
10415
- success("Changes synced successfully");
10416
- info(
10417
- `${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
10418
- );
10419
- }
10420
- } catch (err) {
10421
- error(`Watch sync failed: ${err instanceof Error ? err.message : String(err)}`);
10422
- }
10423
- }, DEBOUNCE_MS);
11253
+ const watcher = (0, import_node_fs2.watch)(process.cwd(), { recursive: true }, async (eventType, filename) => {
11254
+ if (!filename) return;
11255
+ if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
11256
+ return;
10424
11257
  }
10425
- );
11258
+ if (debounceTimer) {
11259
+ clearTimeout(debounceTimer);
11260
+ }
11261
+ debounceTimer = setTimeout(async () => {
11262
+ try {
11263
+ if (options.verbose) {
11264
+ info(`Change detected: ${filename}`);
11265
+ }
11266
+ const spinner = createSpinner("Re-analyzing project...");
11267
+ spinner.start();
11268
+ const newGenerator = new MCPGenerator(generatorConfig);
11269
+ const output = await newGenerator.generate();
11270
+ spinner.text = "Uploading changes...";
11271
+ const syncResult = await apiClient.sync(output.document, output.document.version);
11272
+ spinner.stop();
11273
+ if (!syncResult.success) {
11274
+ error(`Sync failed: ${syncResult.error}`);
11275
+ } else if (syncResult.unchanged) {
11276
+ info("Changes synced (no content changes detected)");
11277
+ } else {
11278
+ success("Changes synced successfully");
11279
+ info(
11280
+ `${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
11281
+ );
11282
+ }
11283
+ } catch (err) {
11284
+ error(`Watch sync failed: ${stringifyUnknown(err)}`);
11285
+ }
11286
+ }, DEBOUNCE_MS);
11287
+ });
10426
11288
  process.on("SIGINT", () => {
10427
11289
  info("Stopping watcher...");
10428
11290
  watcher.close();
@@ -10432,7 +11294,7 @@ function watchCommand() {
10432
11294
  process.exit(0);
10433
11295
  });
10434
11296
  } catch (err) {
10435
- error(`Watch failed: ${err instanceof Error ? err.message : String(err)}`);
11297
+ error(`Watch failed: ${stringifyUnknown(err)}`);
10436
11298
  process.exit(1);
10437
11299
  }
10438
11300
  });
@@ -10441,7 +11303,11 @@ function watchCommand() {
10441
11303
  // src/cli/commands/generate.ts
10442
11304
  var import_commander4 = require("commander");
10443
11305
  function generateCommand() {
10444
- 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) => {
11306
+ 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(
11307
+ "--strict-metadata",
11308
+ "Fail when mutating actions are missing effect/riskLevel metadata",
11309
+ false
11310
+ ).option("--allow-empty", "Do not fail when the application map has zero screens", false).action(async (options) => {
10445
11311
  try {
10446
11312
  banner();
10447
11313
  if (options.format && options.format !== "json") {
@@ -10452,7 +11318,7 @@ function generateCommand() {
10452
11318
  spinner.start();
10453
11319
  let loaded = null;
10454
11320
  try {
10455
- loaded = loadConfig();
11321
+ loaded = loadConfig(warn);
10456
11322
  } catch {
10457
11323
  loaded = null;
10458
11324
  }
@@ -10465,6 +11331,10 @@ function generateCommand() {
10465
11331
  });
10466
11332
  const output = await generator.generate();
10467
11333
  spinner.stop();
11334
+ assertDocumentHasScreens(output.document, {
11335
+ rootDir: process.cwd(),
11336
+ allowEmpty: Boolean(options.allowEmpty)
11337
+ });
10468
11338
  success(
10469
11339
  `Generated MCP document with ${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
10470
11340
  );
@@ -10472,21 +11342,761 @@ function generateCommand() {
10472
11342
  info(`Checksum: ${output.checksum}`);
10473
11343
  reportMetadataLint(output.document, Boolean(options.strictMetadata));
10474
11344
  } catch (err) {
10475
- error(`Generation failed: ${err instanceof Error ? err.message : String(err)}`);
11345
+ error(`Generation failed: ${stringifyUnknown(err)}`);
10476
11346
  process.exit(1);
10477
11347
  }
10478
11348
  });
10479
11349
  }
10480
11350
 
10481
- // src/cli/commands/status.ts
11351
+ // src/cli/commands/annotate.ts
11352
+ var import_promises6 = require("fs/promises");
11353
+ var import_node_path7 = __toESM(require("path"));
10482
11354
  var import_commander5 = require("commander");
11355
+ var import_fast_glob6 = __toESM(require("fast-glob"));
11356
+
11357
+ // src/annotate/sites.ts
11358
+ var import_traverse12 = __toESM(require("@babel/traverse"));
11359
+ var t13 = __toESM(require("@babel/types"));
11360
+
11361
+ // src/annotate/forwarding.ts
11362
+ var import_node_path6 = __toESM(require("path"));
11363
+ var import_node_fs3 = require("fs");
11364
+ var import_traverse11 = __toESM(require("@babel/traverse"));
11365
+ var t12 = __toESM(require("@babel/types"));
11366
+ var traverse11 = import_traverse11.default.default ?? import_traverse11.default;
11367
+ var RN_TESTID_CARRIERS = /* @__PURE__ */ new Set([
11368
+ "View",
11369
+ "Text",
11370
+ "TextInput",
11371
+ "Image",
11372
+ "ImageBackground",
11373
+ "ScrollView",
11374
+ "FlatList",
11375
+ "SectionList",
11376
+ "VirtualizedList",
11377
+ "TouchableOpacity",
11378
+ "TouchableHighlight",
11379
+ "TouchableWithoutFeedback",
11380
+ "TouchableNativeFeedback",
11381
+ "Pressable",
11382
+ "Button",
11383
+ "Switch",
11384
+ "Modal",
11385
+ "RefreshControl",
11386
+ "ActivityIndicator",
11387
+ "SafeAreaView",
11388
+ "KeyboardAvoidingView",
11389
+ "Slider"
11390
+ ]);
11391
+ function collectImports(ast) {
11392
+ const out = /* @__PURE__ */ new Map();
11393
+ traverse11(ast, {
11394
+ ImportDeclaration(nodePath) {
11395
+ const source = nodePath.node.source.value;
11396
+ for (const spec of nodePath.node.specifiers) {
11397
+ if (t12.isImportSpecifier(spec)) {
11398
+ const imported = t12.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
11399
+ out.set(spec.local.name, { source, local: spec.local.name, imported });
11400
+ } else if (t12.isImportDefaultSpecifier(spec)) {
11401
+ out.set(spec.local.name, { source, local: spec.local.name, imported: "default" });
11402
+ }
11403
+ }
11404
+ }
11405
+ });
11406
+ return out;
11407
+ }
11408
+ var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
11409
+ function resolveRelativeImport(fromFile, source) {
11410
+ if (!source.startsWith(".")) return void 0;
11411
+ const base = import_node_path6.default.resolve(import_node_path6.default.dirname(fromFile), source);
11412
+ for (const ext of EXTENSIONS) {
11413
+ const candidate = `${base}${ext}`;
11414
+ if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
11415
+ }
11416
+ for (const ext of EXTENSIONS) {
11417
+ const candidate = import_node_path6.default.join(base, `index${ext}`);
11418
+ if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
11419
+ }
11420
+ return void 0;
11421
+ }
11422
+ var IDENTITY_HOOKS = /* @__PURE__ */ new Set([
11423
+ "useAppilotsTarget",
11424
+ "useAppilotsField",
11425
+ "useAppilotsToggle",
11426
+ "useAppilotsSlider"
11427
+ ]);
11428
+ function inspectModule(file2, exportName, depth = 0) {
11429
+ const none = { forwards: false, derives: false };
11430
+ if (depth > 2) return none;
11431
+ let source;
11432
+ try {
11433
+ source = (0, import_node_fs3.readFileSync)(file2, "utf-8");
11434
+ } catch {
11435
+ return none;
11436
+ }
11437
+ const mentionsIdentity = source.includes("testID") || source.includes("accessibilityLabel") || source.includes("useAppilots") || /\{\s*\.\.\./.test(source);
11438
+ if (!mentionsIdentity && !/export\s*\{[^}]*\}\s*from/.test(source)) return none;
11439
+ let ast;
11440
+ try {
11441
+ ast = parseSource(source);
11442
+ } catch {
11443
+ return none;
11444
+ }
11445
+ let forwards = false;
11446
+ let derives = false;
11447
+ let barrelTarget;
11448
+ traverse11(ast, {
11449
+ // `export { Card } from './Card'` — follow one hop.
11450
+ ExportNamedDeclaration(nodePath) {
11451
+ const from = nodePath.node.source?.value;
11452
+ if (!from) return;
11453
+ const named = nodePath.node.specifiers.some(
11454
+ (spec) => t12.isExportSpecifier(spec) && (t12.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === exportName
11455
+ );
11456
+ if (named) barrelTarget = from;
11457
+ },
11458
+ ExportAllDeclaration(nodePath) {
11459
+ barrelTarget ??= nodePath.node.source.value;
11460
+ },
11461
+ // `interface CardProps { testID?: string }` / `type Props = { testID… }`
11462
+ TSPropertySignature(nodePath) {
11463
+ if (t12.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
11464
+ },
11465
+ // `function Card({ testID }) {}` — the destructured parameter.
11466
+ ObjectProperty(nodePath) {
11467
+ if (t12.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
11468
+ },
11469
+ // `<View {...props} />` — anything spread onto JSX carries it along.
11470
+ JSXSpreadAttribute() {
11471
+ forwards = true;
11472
+ },
11473
+ // `testID={`toggle-${id}`}` / `accessibilityLabel={label}` — the
11474
+ // component naming itself from a prop. A STRING literal does not count:
11475
+ // that is one hardcoded name shared by every instance, which is the
11476
+ // duplicate-id problem rather than a solution to it.
11477
+ JSXAttribute(nodePath) {
11478
+ const name = t12.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
11479
+ if (name !== "testID" && name !== "accessibilityLabel") return;
11480
+ const value = nodePath.node.value;
11481
+ if (!t12.isJSXExpressionContainer(value)) return;
11482
+ if (t12.isStringLiteral(value.expression)) return;
11483
+ derives = true;
11484
+ },
11485
+ // `useAppilotsTarget(`${groupId}-${option.value}`, …)`
11486
+ CallExpression(nodePath) {
11487
+ const callee = nodePath.node.callee;
11488
+ if (t12.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
11489
+ }
11490
+ });
11491
+ if (forwards || derives) return { forwards, derives };
11492
+ if (barrelTarget) {
11493
+ const next = resolveRelativeImport(file2, barrelTarget);
11494
+ if (next) return inspectModule(next, exportName, depth + 1);
11495
+ }
11496
+ return none;
11497
+ }
11498
+ var verdictCache = /* @__PURE__ */ new Map();
11499
+ function canCarryTestId(componentName, fromFile, imports) {
11500
+ const origin = imports.get(componentName);
11501
+ if (origin?.source === "react-native") {
11502
+ return RN_TESTID_CARRIERS.has(origin.imported) ? "rn-primitive" : "unverifiable";
11503
+ }
11504
+ const target = origin ? resolveRelativeImport(fromFile, origin.source) : fromFile;
11505
+ if (!target) return "unverifiable";
11506
+ const exportName = origin?.imported ?? componentName;
11507
+ const key = `${target}#${exportName}`;
11508
+ let identity = verdictCache.get(key);
11509
+ if (identity === void 0) {
11510
+ identity = inspectModule(target, exportName);
11511
+ verdictCache.set(key, identity);
11512
+ }
11513
+ if (identity.forwards) return "forwards";
11514
+ if (identity.derives) return "derives";
11515
+ return "swallows";
11516
+ }
11517
+ function resetForwardingCache() {
11518
+ verdictCache.clear();
11519
+ }
11520
+
11521
+ // src/annotate/sites.ts
11522
+ var traverse12 = import_traverse12.default.default ?? import_traverse12.default;
11523
+ var LABEL_PROPS = ["title", "label", "placeholder"];
11524
+ var IDENTITY_PROPS = ["testID", "appilotsId", "accessibilityLabel"];
11525
+ function kebab(text) {
11526
+ return text.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
11527
+ }
11528
+ function verbOf(name) {
11529
+ return kebab(name.replace(/^(handle|on)(?=[A-Z])/, ""));
11530
+ }
11531
+ function scopeSlugForFile(file2) {
11532
+ const base = file2.replace(/\\/g, "/").split("/").pop() ?? file2;
11533
+ const name = base.replace(/\.[jt]sx?$/, "");
11534
+ if (name.toLowerCase() === "index") {
11535
+ const dir = file2.replace(/\\/g, "/").split("/").slice(-2, -1)[0] ?? "";
11536
+ return kebab(dir);
11537
+ }
11538
+ return kebab(name.replace(/Screen$/, ""));
11539
+ }
11540
+ function collapseRepeats(slug) {
11541
+ const seen = /* @__PURE__ */ new Set();
11542
+ const out = [];
11543
+ for (const token of slug.split("-")) {
11544
+ if (!token || seen.has(token)) continue;
11545
+ seen.add(token);
11546
+ out.push(token);
11547
+ }
11548
+ return out.join("-");
11549
+ }
11550
+ function deriveTestId(site) {
11551
+ const base = [site.scope, kebab(site.component)].filter(Boolean).join("-");
11552
+ if (site.pressProp && site.pressProp !== "onPress") {
11553
+ return collapseRepeats(`${base}-${verbOf(site.pressProp)}`);
11554
+ }
11555
+ if (site.handler) {
11556
+ const verb = verbOf(site.handler);
11557
+ return collapseRepeats(verb ? `${base}-${verb}` : base);
11558
+ }
11559
+ return collapseRepeats(`${base}-${site.ordinal}`);
11560
+ }
11561
+ function hasAnyProp(element, names) {
11562
+ return names.some((name) => findJsxAttribute(element, name) !== void 0);
11563
+ }
11564
+ function hasLabelText(element) {
11565
+ return LABEL_PROPS.some((name) => {
11566
+ const value = getStringAttr(element, name);
11567
+ return typeof value === "string" && value.trim().length > 0;
11568
+ });
11569
+ }
11570
+ function namedHandler(element, prop) {
11571
+ if (!prop) return void 0;
11572
+ const attr = findJsxAttribute(element, prop);
11573
+ if (!attr || !t13.isJSXExpressionContainer(attr.value)) return void 0;
11574
+ return t13.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
11575
+ }
11576
+ var ANNOTATABLE_ROLES = /* @__PURE__ */ new Set(["button", "input", "select", "toggle", "date", "list"]);
11577
+ function isPerItemRender(nodePath) {
11578
+ let current = nodePath.parentPath;
11579
+ while (current) {
11580
+ const node = current.node;
11581
+ if (t13.isCallExpression(node) && t13.isMemberExpression(node.callee) && t13.isIdentifier(node.callee.property) && (node.callee.property.name === "map" || node.callee.property.name === "flatMap")) {
11582
+ return true;
11583
+ }
11584
+ if (t13.isJSXAttribute(node) && t13.isJSXIdentifier(node.name) && /^render[A-Z]|^ListHeaderComponent$|^ListFooterComponent$/.test(node.name.name) && node.name.name !== "renderScrollComponent") {
11585
+ return /^renderItem$|^renderSectionHeader$|^renderSectionFooter$/.test(node.name.name);
11586
+ }
11587
+ current = current.parentPath;
11588
+ }
11589
+ return false;
11590
+ }
11591
+ function findAnnotationSites(source, file2) {
11592
+ let ast;
11593
+ try {
11594
+ ast = parseSource(source);
11595
+ } catch {
11596
+ return [];
11597
+ }
11598
+ const imports = collectImports(ast);
11599
+ const scope = scopeSlugForFile(file2);
11600
+ const sites = [];
11601
+ const perComponent = /* @__PURE__ */ new Map();
11602
+ const usedIds = /* @__PURE__ */ new Set();
11603
+ traverse12(ast, {
11604
+ JSXOpeningElement(nodePath) {
11605
+ for (const prop of IDENTITY_PROPS) {
11606
+ const existing = getStringAttr(nodePath.node, prop);
11607
+ if (existing) usedIds.add(existing);
11608
+ }
11609
+ }
11610
+ });
11611
+ traverse12(ast, {
11612
+ JSXOpeningElement(nodePath) {
11613
+ const element = nodePath.node;
11614
+ if (!t13.isJSXIdentifier(element.name)) return;
11615
+ const component = element.name.name;
11616
+ const role = classifyJsxComponent(component, element);
11617
+ if (!ANNOTATABLE_ROLES.has(role)) return;
11618
+ if (hasAnyProp(element, IDENTITY_PROPS)) return;
11619
+ if (role === "button" && hasLabelText(element)) return;
11620
+ const carrier = canCarryTestId(component, file2, imports);
11621
+ if (carrier === "derives") return;
11622
+ if (isPerItemRender(nodePath)) return;
11623
+ const pressProp = pressHandlerProp(element) ?? void 0;
11624
+ const handler = namedHandler(element, pressProp);
11625
+ const ordinal = (perComponent.get(component) ?? 0) + 1;
11626
+ perComponent.set(component, ordinal);
11627
+ let testId = deriveTestId({ scope, component, pressProp, handler, ordinal });
11628
+ if (usedIds.has(testId)) {
11629
+ let suffix = 2;
11630
+ while (usedIds.has(`${testId}-${suffix}`)) suffix += 1;
11631
+ testId = `${testId}-${suffix}`;
11632
+ }
11633
+ usedIds.add(testId);
11634
+ const line = element.loc?.start.line ?? 0;
11635
+ const nameEndLine = element.name.loc?.end.line ?? line;
11636
+ const firstAttr = element.attributes[0];
11637
+ const firstAttrLoc = firstAttr?.loc?.start;
11638
+ const isMultiline = !!firstAttrLoc && firstAttrLoc.line !== nameEndLine;
11639
+ const insertText = isMultiline ? `
11640
+ ${" ".repeat(firstAttrLoc.column)}testID="${testId}"` : ` testID="${testId}"`;
11641
+ sites.push({
11642
+ file: file2,
11643
+ line,
11644
+ column: element.loc?.start.column ?? 0,
11645
+ component,
11646
+ role,
11647
+ pressProp,
11648
+ handler,
11649
+ testId,
11650
+ carrier,
11651
+ // Just past the element name — before any existing attribute, so a
11652
+ // multi-line element keeps its formatting and the new prop reads
11653
+ // first in the diff.
11654
+ insertAt: element.name.end ?? element.start ?? 0,
11655
+ insertText
11656
+ });
11657
+ }
11658
+ });
11659
+ return sites;
11660
+ }
11661
+
11662
+ // src/annotate/apply.ts
11663
+ var import_magic_string = __toESM(require("magic-string"));
11664
+ function planSite(site) {
11665
+ if (site.carrier === "rn-primitive" || site.carrier === "forwards") {
11666
+ return { ...site, outcome: "annotated" };
11667
+ }
11668
+ if (site.carrier === "swallows") {
11669
+ return {
11670
+ ...site,
11671
+ outcome: "blocked-swallows",
11672
+ blockedReason: `<${site.component}> does not accept testID, so React would drop the prop and the agent would be told about an element that never reaches the screen. Add testID?: string to its props and pass it to the element it renders, then re-run.`
11673
+ };
11674
+ }
11675
+ return {
11676
+ ...site,
11677
+ outcome: "blocked-unverifiable",
11678
+ blockedReason: `<${site.component}> comes from a package whose source is not readable from here, so whether it forwards testID cannot be checked. Annotate it by hand if it does.`
11679
+ };
11680
+ }
11681
+ function applyToFile(source, sites) {
11682
+ const planned = sites.map(planSite);
11683
+ const written = planned.filter((site) => site.outcome === "annotated");
11684
+ const blocked = planned.filter((site) => site.outcome !== "annotated");
11685
+ if (written.length === 0) {
11686
+ return { file: sites[0]?.file ?? "", content: source, written, blocked };
11687
+ }
11688
+ const magic = new import_magic_string.default(source);
11689
+ for (const site of written) {
11690
+ magic.appendLeft(site.insertAt, site.insertText);
11691
+ }
11692
+ return { file: written[0].file, content: magic.toString(), written, blocked };
11693
+ }
11694
+
11695
+ // src/annotate/forward.ts
11696
+ var import_node_fs4 = require("fs");
11697
+ var import_magic_string2 = __toESM(require("magic-string"));
11698
+ var import_traverse13 = __toESM(require("@babel/traverse"));
11699
+ var t14 = __toESM(require("@babel/types"));
11700
+ var traverse13 = import_traverse13.default.default ?? import_traverse13.default;
11701
+ function findComponent(ast, name) {
11702
+ let found;
11703
+ traverse13(ast, {
11704
+ FunctionDeclaration(nodePath) {
11705
+ if (nodePath.node.id?.name === name) {
11706
+ found ??= { params: nodePath.node.params, body: nodePath.node.body };
11707
+ }
11708
+ },
11709
+ VariableDeclarator(nodePath) {
11710
+ if (!t14.isIdentifier(nodePath.node.id) || nodePath.node.id.name !== name) return;
11711
+ const init = nodePath.node.init;
11712
+ if (t14.isArrowFunctionExpression(init) || t14.isFunctionExpression(init)) {
11713
+ found ??= { params: init.params, body: init.body };
11714
+ }
11715
+ }
11716
+ });
11717
+ return found;
11718
+ }
11719
+ function findReturnedRoots(body) {
11720
+ const roots = [];
11721
+ let unsupported = false;
11722
+ const record = (node) => {
11723
+ if (!node) return;
11724
+ if (t14.isParenthesizedExpression(node)) return record(node.expression);
11725
+ if (t14.isJSXElement(node)) {
11726
+ roots.push(node.openingElement);
11727
+ return;
11728
+ }
11729
+ if (t14.isConditionalExpression(node)) {
11730
+ record(node.consequent);
11731
+ record(node.alternate);
11732
+ return;
11733
+ }
11734
+ if (t14.isJSXFragment(node)) {
11735
+ unsupported = true;
11736
+ return;
11737
+ }
11738
+ if (t14.isNullLiteral(node)) return;
11739
+ unsupported = true;
11740
+ };
11741
+ if (t14.isBlockStatement(body)) {
11742
+ traverse13(
11743
+ t14.file(t14.program([t14.expressionStatement(t14.functionExpression(null, [], body))])),
11744
+ {
11745
+ ReturnStatement(nodePath) {
11746
+ record(nodePath.node.argument);
11747
+ }
11748
+ }
11749
+ );
11750
+ } else {
11751
+ record(body);
11752
+ }
11753
+ if (unsupported || roots.length === 0) return void 0;
11754
+ return roots;
11755
+ }
11756
+ function isCarrierRoot(element, imports) {
11757
+ if (!t14.isJSXIdentifier(element.name)) return false;
11758
+ const name = element.name.name;
11759
+ const origin = imports.get(name);
11760
+ if (origin?.source !== "react-native") return false;
11761
+ return RN_TESTID_CARRIERS.has(origin.imported);
11762
+ }
11763
+ function hasProp(element, name) {
11764
+ return element.attributes.some(
11765
+ (attr) => t14.isJSXAttribute(attr) && t14.isJSXIdentifier(attr.name) && attr.name.name === name
11766
+ );
11767
+ }
11768
+ function indentOf(source, offset) {
11769
+ const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
11770
+ const match = /^[ \t]*/.exec(source.slice(lineStart, offset));
11771
+ return match?.[0] ?? "";
11772
+ }
11773
+ function insertAfterLastMember(source, members, text, closingBrace, separator) {
11774
+ const last = members[members.length - 1];
11775
+ if (!last || typeof last.end !== "number" || typeof last.start !== "number") {
11776
+ return { at: closingBrace, text };
11777
+ }
11778
+ const trailing = /^\s*[,;]/.exec(source.slice(last.end, closingBrace));
11779
+ const at = last.end + (trailing?.[0].length ?? 0);
11780
+ const endsWithSeparator = source.slice(0, last.end).trimEnd().endsWith(separator);
11781
+ const lead = trailing || endsWithSeparator ? "" : separator;
11782
+ const openLine = source.lastIndexOf("\n", last.start);
11783
+ const multiline = openLine !== -1 && openLine > source.lastIndexOf("{", last.start);
11784
+ if (!multiline) return { at, text: `${lead} ${text}` };
11785
+ const indent = indentOf(source, last.start);
11786
+ return { at, text: `${lead}
11787
+ ${indent}${text}` };
11788
+ }
11789
+ function followBarrel(file2, ast, component) {
11790
+ let target;
11791
+ traverse13(ast, {
11792
+ ExportNamedDeclaration(nodePath) {
11793
+ const from = nodePath.node.source?.value;
11794
+ if (!from) return;
11795
+ const named = nodePath.node.specifiers.some(
11796
+ (spec) => t14.isExportSpecifier(spec) && (t14.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === component
11797
+ );
11798
+ if (named) target ??= from;
11799
+ }
11800
+ });
11801
+ return target ? resolveRelativeImport(file2, target) : void 0;
11802
+ }
11803
+ function planForward(file2, component, depth = 0) {
11804
+ const base = { file: file2, component, edits: [], roots: 0 };
11805
+ let source;
11806
+ try {
11807
+ source = (0, import_node_fs4.readFileSync)(file2, "utf-8");
11808
+ } catch {
11809
+ return { ...base, refusal: `cannot read ${file2}` };
11810
+ }
11811
+ let ast;
11812
+ try {
11813
+ ast = parseSource(source);
11814
+ } catch {
11815
+ return { ...base, refusal: `cannot parse ${file2}` };
11816
+ }
11817
+ const found = findComponent(ast, component);
11818
+ if (!found) {
11819
+ const barrel = depth < 2 ? followBarrel(file2, ast, component) : void 0;
11820
+ if (barrel) return planForward(barrel, component, depth + 1);
11821
+ return { ...base, refusal: `no function named ${component} in this file` };
11822
+ }
11823
+ const param = found.params[0];
11824
+ if (!t14.isObjectPattern(param)) {
11825
+ return {
11826
+ ...base,
11827
+ refusal: `${component} does not destructure its props, so there is no safe place to add one without knowing what the parameter is called downstream`
11828
+ };
11829
+ }
11830
+ const roots = findReturnedRoots(found.body);
11831
+ if (!roots) {
11832
+ return {
11833
+ ...base,
11834
+ refusal: `${component} returns something other than a JSX element (a fragment, or a value this cannot follow)`
11835
+ };
11836
+ }
11837
+ const imports = collectImports(ast);
11838
+ const nonCarrier = roots.find((root) => !isCarrierRoot(root, imports));
11839
+ if (nonCarrier) {
11840
+ const name = t14.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
11841
+ return {
11842
+ ...base,
11843
+ refusal: `${component} renders <${name}> at its root, which is not a React Native element known to accept testID. Forwarding into it would move the problem down a level, not fix it`
11844
+ };
11845
+ }
11846
+ const edits = [];
11847
+ const alreadyDestructured = param.properties.some(
11848
+ (prop) => t14.isObjectProperty(prop) && t14.isIdentifier(prop.key) && prop.key.name === "testID"
11849
+ );
11850
+ if (!alreadyDestructured) {
11851
+ const insert = insertAfterLastMember(source, param.properties, "testID", param.end ?? 0, ",");
11852
+ edits.push({ ...insert, what: "parameter" });
11853
+ }
11854
+ const annotation = param.typeAnnotation;
11855
+ if (t14.isTSTypeAnnotation(annotation)) {
11856
+ const declared = annotation.typeAnnotation;
11857
+ let body;
11858
+ let closing = 0;
11859
+ if (t14.isTSTypeLiteral(declared)) {
11860
+ body = declared.members;
11861
+ closing = declared.end ?? 0;
11862
+ } else if (t14.isTSTypeReference(declared) && t14.isIdentifier(declared.typeName)) {
11863
+ const typeName = declared.typeName.name;
11864
+ traverse13(ast, {
11865
+ TSInterfaceDeclaration(nodePath) {
11866
+ if (nodePath.node.id.name !== typeName) return;
11867
+ body = nodePath.node.body.body;
11868
+ closing = nodePath.node.body.end ?? 0;
11869
+ },
11870
+ TSTypeAliasDeclaration(nodePath) {
11871
+ if (nodePath.node.id.name !== typeName || !t14.isTSTypeLiteral(nodePath.node.typeAnnotation))
11872
+ return;
11873
+ body = nodePath.node.typeAnnotation.members;
11874
+ closing = nodePath.node.typeAnnotation.end ?? 0;
11875
+ }
11876
+ });
11877
+ if (!body) {
11878
+ return {
11879
+ ...base,
11880
+ refusal: `${component}'s props type \`${typeName}\` is not declared in this file, so adding a field to it would edit a contract shared with code this cannot see`
11881
+ };
11882
+ }
11883
+ }
11884
+ if (body) {
11885
+ const declaresTestId = body.some(
11886
+ (member) => t14.isTSPropertySignature(member) && t14.isIdentifier(member.key) && member.key.name === "testID"
11887
+ );
11888
+ if (!declaresTestId) {
11889
+ const insert = insertAfterLastMember(source, body, "testID?: string;", closing, ";");
11890
+ edits.push({ ...insert, what: "props-type" });
11891
+ }
11892
+ }
11893
+ }
11894
+ let delivered = 0;
11895
+ for (const root of roots) {
11896
+ if (hasProp(root, "testID")) continue;
11897
+ edits.push({
11898
+ at: root.name.end ?? root.start ?? 0,
11899
+ text: " testID={testID}",
11900
+ what: "root"
11901
+ });
11902
+ delivered += 1;
11903
+ }
11904
+ if (edits.length === 0) {
11905
+ return { ...base, refusal: `${component} already forwards testID` };
11906
+ }
11907
+ return { file: file2, component, edits, roots: delivered };
11908
+ }
11909
+ function applyForward(source, plan) {
11910
+ if (plan.refusal || plan.edits.length === 0) return source;
11911
+ const magic = new import_magic_string2.default(source);
11912
+ for (const edit of plan.edits) magic.appendLeft(edit.at, edit.text);
11913
+ return magic.toString();
11914
+ }
11915
+
11916
+ // src/cli/commands/annotate.ts
11917
+ var DEFAULT_INCLUDE = ["src/**/*Screen.{tsx,jsx}", "src/**/screens/**/*.{tsx,jsx}"];
11918
+ var DEFAULT_EXCLUDE = ["**/node_modules/**", "**/dist/**", "**/*.test.*", "**/__tests__/**"];
11919
+ function annotateCommand() {
11920
+ return new import_commander5.Command("annotate").description(
11921
+ "Add missing testIDs to interactive elements so the agent can address them (dry run by default)"
11922
+ ).option("--write", "Actually modify the files (default: report only)", false).option(
11923
+ "--forward",
11924
+ "Also teach a shared component to accept and pass through testID, when its call sites are blocked only by that",
11925
+ false
11926
+ ).option("-i, --include <globs...>", "Glob patterns to scan", void 0).option("--json", "Emit a machine-readable report", false).option("--quiet", "Only warnings and errors", false).action(async (options) => {
11927
+ try {
11928
+ if (options.json) setLogMode("json");
11929
+ else if (options.quiet) setLogMode("quiet");
11930
+ banner();
11931
+ let loaded = null;
11932
+ try {
11933
+ loaded = loadConfig(warn);
11934
+ } catch {
11935
+ loaded = null;
11936
+ }
11937
+ const rootDir = process.cwd();
11938
+ const include = options.include ?? DEFAULT_INCLUDE;
11939
+ const exclude = loaded?.exclude ?? DEFAULT_EXCLUDE;
11940
+ const spinner = createSpinner("Scanning for unaddressable controls...");
11941
+ spinner.start();
11942
+ resetForwardingCache();
11943
+ const files = await (0, import_fast_glob6.default)(include, { cwd: rootDir, ignore: exclude, absolute: true });
11944
+ const scan = async () => {
11945
+ const out = [];
11946
+ for (const file2 of files) {
11947
+ const source = await (0, import_promises6.readFile)(file2, "utf-8");
11948
+ const sites = findAnnotationSites(source, file2);
11949
+ if (sites.length === 0) continue;
11950
+ out.push(applyToFile(source, sites));
11951
+ }
11952
+ return out;
11953
+ };
11954
+ let edits = await scan();
11955
+ let forwards = [];
11956
+ if (options.forward) {
11957
+ forwards = await planForwards(edits, rootDir);
11958
+ const applied = forwards.filter((plan) => !plan.refusal);
11959
+ if (options.write && applied.length > 0) {
11960
+ for (const plan of applied) {
11961
+ const before = await (0, import_promises6.readFile)(plan.file, "utf-8");
11962
+ await (0, import_promises6.writeFile)(plan.file, applyForward(before, plan), "utf-8");
11963
+ }
11964
+ resetForwardingCache();
11965
+ edits = await scan();
11966
+ }
11967
+ }
11968
+ spinner.stop();
11969
+ const written = edits.flatMap((edit) => edit.written);
11970
+ const blocked = edits.flatMap((edit) => edit.blocked);
11971
+ const touched = edits.filter((edit) => edit.written.length > 0);
11972
+ if (written.length === 0 && blocked.length === 0) {
11973
+ success("Every interactive control already has something to address it by.");
11974
+ if (options.json) {
11975
+ jsonOutput({ ok: true, scanned: files.length, annotated: 0, blocked: 0, sites: [] });
11976
+ }
11977
+ return;
11978
+ }
11979
+ if (options.write) {
11980
+ for (const edit of touched) {
11981
+ await (0, import_promises6.writeFile)(edit.file, edit.content, "utf-8");
11982
+ }
11983
+ }
11984
+ reportSites(rootDir, touched, blocked, options.write === true, forwards);
11985
+ if (options.json) {
11986
+ jsonOutput({
11987
+ ok: true,
11988
+ scanned: files.length,
11989
+ written: options.write === true,
11990
+ annotated: written.length,
11991
+ blocked: blocked.length,
11992
+ files: touched.map((edit) => import_node_path7.default.relative(rootDir, edit.file)),
11993
+ sites: [...written, ...blocked].map((site) => ({
11994
+ file: import_node_path7.default.relative(rootDir, site.file),
11995
+ line: site.line,
11996
+ component: site.component,
11997
+ role: site.role,
11998
+ testId: site.testId,
11999
+ outcome: site.outcome,
12000
+ reason: site.blockedReason
12001
+ }))
12002
+ });
12003
+ }
12004
+ } catch (err) {
12005
+ error(`Annotate failed: ${stringifyUnknown(err)}`);
12006
+ process.exit(1);
12007
+ }
12008
+ });
12009
+ }
12010
+ async function planForwards(edits, rootDir) {
12011
+ const seen = /* @__PURE__ */ new Set();
12012
+ const plans = [];
12013
+ for (const edit of edits) {
12014
+ for (const site of edit.blocked) {
12015
+ if (site.outcome !== "blocked-swallows") continue;
12016
+ if (seen.has(site.component)) continue;
12017
+ seen.add(site.component);
12018
+ const source = await (0, import_promises6.readFile)(site.file, "utf-8");
12019
+ const origin = findImportSource(source, site.component);
12020
+ const target = origin ? resolveRelativeImport(site.file, origin) : site.file;
12021
+ if (!target) {
12022
+ plans.push({
12023
+ file: site.file,
12024
+ component: site.component,
12025
+ edits: [],
12026
+ roots: 0,
12027
+ refusal: `cannot locate the source of <${site.component}>`
12028
+ });
12029
+ continue;
12030
+ }
12031
+ plans.push(planForward(target, site.component));
12032
+ }
12033
+ }
12034
+ void rootDir;
12035
+ return plans;
12036
+ }
12037
+ function findImportSource(source, name) {
12038
+ const pattern = new RegExp(
12039
+ String.raw`import\s*\{[^}]*\b${name}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
12040
+ );
12041
+ return pattern.exec(source)?.[1];
12042
+ }
12043
+ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
12044
+ const written = touched.flatMap((edit) => edit.written);
12045
+ if (written.length > 0) {
12046
+ heading(didWrite ? "Added" : "Would add");
12047
+ for (const edit of touched) {
12048
+ info(import_node_path7.default.relative(rootDir, edit.file));
12049
+ for (const site of edit.written) {
12050
+ dim(` ${String(site.line).padStart(4)} <${site.component}> testID="${site.testId}"`);
12051
+ }
12052
+ }
12053
+ }
12054
+ const teachable = new Map(
12055
+ forwards.filter((plan) => !plan.refusal).map((plan) => [plan.component, plan])
12056
+ );
12057
+ if (teachable.size > 0) {
12058
+ heading(didWrite ? "Taught to carry testID" : "Would teach to carry testID");
12059
+ for (const plan of teachable.values()) {
12060
+ info(`<${plan.component}> ${import_node_path7.default.relative(rootDir, plan.file)}`);
12061
+ for (const edit of plan.edits) {
12062
+ dim(` ${edit.what}: ${edit.text.trim()}`);
12063
+ }
12064
+ }
12065
+ }
12066
+ if (blocked.length > 0) {
12067
+ heading("Skipped \u2014 the prop would not survive to the runtime");
12068
+ const byComponent = /* @__PURE__ */ new Map();
12069
+ for (const site of blocked) {
12070
+ const group = byComponent.get(site.component) ?? [];
12071
+ group.push(site);
12072
+ byComponent.set(site.component, group);
12073
+ }
12074
+ for (const [component, group] of byComponent) {
12075
+ if (teachable.has(component)) continue;
12076
+ warn(`<${component}> \u2014 ${group.length} site${group.length === 1 ? "" : "s"}`);
12077
+ const refusal = forwards.find((plan) => plan.component === component)?.refusal;
12078
+ dim(` ${group[0].blockedReason}`);
12079
+ if (refusal) dim(` --forward cannot do it either: ${refusal}`);
12080
+ }
12081
+ }
12082
+ if (!didWrite && written.length > 0) {
12083
+ info("");
12084
+ info(`Nothing was written. Re-run with --write to apply ${written.length} change(s).`);
12085
+ } else if (didWrite) {
12086
+ success(`Wrote ${written.length} testID(s) across ${touched.length} file(s).`);
12087
+ info("Re-run `appilots generate` so the document picks them up.");
12088
+ }
12089
+ }
12090
+
12091
+ // src/cli/commands/status.ts
12092
+ var import_commander6 = require("commander");
10483
12093
  function statusCommand() {
10484
- 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) => {
12094
+ return new import_commander6.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) => {
10485
12095
  if (options.json) setLogMode("json");
10486
12096
  else if (options.quiet) setLogMode("quiet");
10487
12097
  try {
10488
12098
  banner();
10489
- const config = loadConfig();
12099
+ const config = loadConfig(warn);
10490
12100
  if (!config) {
10491
12101
  error("No .appilotsrc configuration found");
10492
12102
  info("Run: appilots init --api-key <your-key>");
@@ -10513,8 +12123,13 @@ function statusCommand() {
10513
12123
  ]);
10514
12124
  if (status.project) {
10515
12125
  heading("\n\u{1F4CC} Project Info:");
10516
- table([["Name", status.project.name], ["ID", status.project.id]]);
12126
+ table([
12127
+ ["Name", status.project.name],
12128
+ ["ID", status.project.id]
12129
+ ]);
10517
12130
  }
12131
+ const outputDir = config.outputDir || ".appilots";
12132
+ let drift = null;
10518
12133
  if (status.activeMcp) {
10519
12134
  heading("\n\u2705 Active MCP Document:");
10520
12135
  table([
@@ -10524,6 +12139,14 @@ function statusCommand() {
10524
12139
  ["Forms", String(status.activeMcp.formsCount)],
10525
12140
  ["Actions", String(status.activeMcp.actionsCount)]
10526
12141
  ]);
12142
+ const local = readLocalDocumentSummary(outputDir);
12143
+ if (local) {
12144
+ drift = compareWithRemote(local, status.activeMcp);
12145
+ if (drift) {
12146
+ heading("");
12147
+ reportRemoteDrift(drift);
12148
+ }
12149
+ }
10527
12150
  } else {
10528
12151
  heading("\n\u26A0\uFE0F No active MCP document");
10529
12152
  info("Run: appilots sync");
@@ -10534,21 +12157,22 @@ function statusCommand() {
10534
12157
  local: {
10535
12158
  serverUrl: config.serverUrl,
10536
12159
  projectId: config.projectId || null,
10537
- outputDir: config.outputDir || ".appilots"
12160
+ outputDir
10538
12161
  },
10539
12162
  project: status.project ?? null,
10540
- activeMcp: status.activeMcp ?? null
12163
+ activeMcp: status.activeMcp ?? null,
12164
+ drift
10541
12165
  });
10542
12166
  } catch (err) {
10543
- error(`Status check failed: ${err instanceof Error ? err.message : String(err)}`);
10544
- jsonOutput({ success: false, error: err instanceof Error ? err.message : String(err) });
12167
+ error(`Status check failed: ${stringifyUnknown(err)}`);
12168
+ jsonOutput({ success: false, error: stringifyUnknown(err) });
10545
12169
  process.exit(1);
10546
12170
  }
10547
12171
  });
10548
12172
  }
10549
12173
 
10550
12174
  // src/cli/commands/eval.ts
10551
- var import_commander6 = require("commander");
12175
+ var import_commander7 = require("commander");
10552
12176
  var import_fs10 = require("fs");
10553
12177
  var import_path9 = require("path");
10554
12178
 
@@ -10578,21 +12202,21 @@ function loadScenarios(dir) {
10578
12202
  } catch {
10579
12203
  return { scenarios, errors };
10580
12204
  }
10581
- for (const file of files) {
10582
- const full = (0, import_path7.join)(dir, file);
12205
+ for (const file2 of files) {
12206
+ const full = (0, import_path7.join)(dir, file2);
10583
12207
  let raw;
10584
12208
  try {
10585
12209
  raw = JSON.parse((0, import_fs8.readFileSync)(full, "utf-8"));
10586
12210
  } catch (err) {
10587
- errors.push({ file, error: `invalid JSON: ${err instanceof Error ? err.message : String(err)}` });
12211
+ errors.push({ file: file2, error: `invalid JSON: ${stringifyUnknown(err)}` });
10588
12212
  continue;
10589
12213
  }
10590
12214
  const shapeError = validateScenarioShape(raw);
10591
12215
  if (shapeError) {
10592
- errors.push({ file, error: shapeError });
12216
+ errors.push({ file: file2, error: shapeError });
10593
12217
  continue;
10594
12218
  }
10595
- scenarios.push({ file, scenario: raw });
12219
+ scenarios.push({ file: file2, scenario: raw });
10596
12220
  }
10597
12221
  return { scenarios, errors };
10598
12222
  }
@@ -10741,10 +12365,10 @@ function matchActions(expected, actual, reply = "") {
10741
12365
  var import_fs9 = require("fs");
10742
12366
  var import_path8 = require("path");
10743
12367
  var EMPTY_BASELINE = { generatedAt: null, gitSha: null, perScenario: {} };
10744
- function loadBaseline(path9) {
10745
- if (!(0, import_fs9.existsSync)(path9)) return { ...EMPTY_BASELINE, perScenario: {} };
12368
+ function loadBaseline(path11) {
12369
+ if (!(0, import_fs9.existsSync)(path11)) return { ...EMPTY_BASELINE, perScenario: {} };
10746
12370
  try {
10747
- const raw = JSON.parse((0, import_fs9.readFileSync)(path9, "utf-8"));
12371
+ const raw = JSON.parse((0, import_fs9.readFileSync)(path11, "utf-8"));
10748
12372
  return {
10749
12373
  generatedAt: raw.generatedAt ?? null,
10750
12374
  gitSha: raw.gitSha ?? null,
@@ -10754,9 +12378,9 @@ function loadBaseline(path9) {
10754
12378
  return { ...EMPTY_BASELINE, perScenario: {} };
10755
12379
  }
10756
12380
  }
10757
- function saveBaseline(path9, baseline) {
10758
- (0, import_fs9.mkdirSync)((0, import_path8.dirname)(path9), { recursive: true });
10759
- (0, import_fs9.writeFileSync)(path9, `${JSON.stringify(baseline, null, 2)}
12381
+ function saveBaseline(path11, baseline) {
12382
+ (0, import_fs9.mkdirSync)((0, import_path8.dirname)(path11), { recursive: true });
12383
+ (0, import_fs9.writeFileSync)(path11, `${JSON.stringify(baseline, null, 2)}
10760
12384
  `, "utf-8");
10761
12385
  }
10762
12386
  function buildBaseline(outcomes, meta) {
@@ -10838,13 +12462,16 @@ function parseRate(raw, fallback, flagName) {
10838
12462
  return n;
10839
12463
  }
10840
12464
  function evalCommand() {
10841
- return new import_commander6.Command("eval").description(
12465
+ return new import_commander7.Command("eval").description(
10842
12466
  "Run agent eval scenarios against the current MCP document in dry-run mode and check for regressions vs a committed baseline"
10843
12467
  ).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(
10844
12468
  "--update-baseline",
10845
12469
  "Record this run as the new baseline (refuses if pass rate is below the threshold)",
10846
12470
  false
10847
- ).option("--min-pass-rate <n>", "Minimum fraction of scenarios that must pass, 0-1 (default: 0.95)").option(
12471
+ ).option(
12472
+ "--min-pass-rate <n>",
12473
+ "Minimum fraction of scenarios that must pass, 0-1 (default: 0.95)"
12474
+ ).option(
10848
12475
  "--max-token-regression <n>",
10849
12476
  "Max fractional token increase before a passing scenario is flagged, e.g. 0.25 for +25% (default: 0.25)"
10850
12477
  ).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) => {
@@ -10852,7 +12479,7 @@ function evalCommand() {
10852
12479
  else if (options.quiet) setLogMode("quiet");
10853
12480
  try {
10854
12481
  banner();
10855
- const loaded = loadConfig();
12482
+ const loaded = loadConfig(warn);
10856
12483
  const outputDir = loaded?.outputDir || ".appilots";
10857
12484
  const scenariosDir = options.dir || loaded?.eval?.scenariosDir || (0, import_path9.join)(outputDir, "scenarios");
10858
12485
  if (options.init) {
@@ -10881,7 +12508,7 @@ function evalCommand() {
10881
12508
  return;
10882
12509
  }
10883
12510
  const config = {
10884
- serverUrl: "http://localhost:4000",
12511
+ serverUrl: DEFAULT_SERVER_URL,
10885
12512
  ...loaded,
10886
12513
  apiKey,
10887
12514
  ...options.projectId ? { projectId: options.projectId } : {},
@@ -10927,11 +12554,17 @@ function evalCommand() {
10927
12554
  error(
10928
12555
  `Duplicate scenario name(s) \u2014 "name" must be unique across all scenario files: ${duplicates.join(", ")}`
10929
12556
  );
10930
- jsonOutput({ success: false, error: `Duplicate scenario names: ${duplicates.join(", ")}` });
12557
+ jsonOutput({
12558
+ success: false,
12559
+ error: `Duplicate scenario names: ${duplicates.join(", ")}`
12560
+ });
10931
12561
  process.exit(1);
10932
12562
  return;
10933
12563
  }
10934
- const apiClient = new AppilotsAPIClient({ serverUrl: config.serverUrl, apiKey: config.apiKey });
12564
+ const apiClient = new AppilotsAPIClient({
12565
+ serverUrl: config.serverUrl,
12566
+ apiKey: config.apiKey
12567
+ });
10935
12568
  const requests = loadedScenarios.scenarios.map(({ scenario }) => ({
10936
12569
  id: scenario.name,
10937
12570
  screen: scenario.given?.screen,
@@ -11033,8 +12666,8 @@ Pass rate: ${passed}/${total} (${passRatePct}%) \u2014 threshold ${minPassRatePc
11033
12666
  success("Eval passed");
11034
12667
  }
11035
12668
  } catch (err) {
11036
- error(`Eval failed: ${err instanceof Error ? err.message : String(err)}`);
11037
- jsonOutput({ success: false, error: err instanceof Error ? err.message : String(err) });
12669
+ error(`Eval failed: ${stringifyUnknown(err)}`);
12670
+ jsonOutput({ success: false, error: stringifyUnknown(err) });
11038
12671
  process.exit(1);
11039
12672
  }
11040
12673
  });
@@ -11042,16 +12675,17 @@ Pass rate: ${passed}/${total} (${passRatePct}%) \u2014 threshold ${minPassRatePc
11042
12675
 
11043
12676
  // src/cli/index.ts
11044
12677
  async function main() {
11045
- const program = new import_commander7.Command();
11046
- program.name("appilots").description(
12678
+ const program2 = new import_commander8.Command();
12679
+ program2.name("appilots").description(
11047
12680
  "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"
11048
- ).version("0.1.0").option("--verbose", "Enable verbose logging");
11049
- program.addCommand(initCommand());
11050
- program.addCommand(syncCommand());
11051
- program.addCommand(watchCommand());
11052
- program.addCommand(generateCommand());
11053
- program.addCommand(statusCommand());
11054
- program.addCommand(evalCommand());
12681
+ ).version(CLI_VERSION).option("--verbose", "Enable verbose logging");
12682
+ program2.addCommand(initCommand());
12683
+ program2.addCommand(syncCommand());
12684
+ program2.addCommand(watchCommand());
12685
+ program2.addCommand(generateCommand());
12686
+ program2.addCommand(annotateCommand());
12687
+ program2.addCommand(statusCommand());
12688
+ program2.addCommand(evalCommand());
11055
12689
  const args = process.argv.slice(2);
11056
12690
  const firstArg = args[0];
11057
12691
  const hasCommand = firstArg !== void 0 && args.length > 0 && !firstArg.startsWith("-");
@@ -11064,14 +12698,14 @@ async function main() {
11064
12698
  info("Welcome to Appilots! Get started with:");
11065
12699
  info(" appilots init --api-key <your-ak_key>");
11066
12700
  console.log("");
11067
- program.outputHelp();
12701
+ program2.outputHelp();
11068
12702
  return;
11069
12703
  }
11070
12704
  }
11071
- await program.parseAsync(process.argv);
12705
+ await program2.parseAsync(process.argv);
11072
12706
  }
11073
12707
  main().catch((err) => {
11074
- error(err instanceof Error ? err.message : String(err));
12708
+ error(stringifyUnknown(err));
11075
12709
  process.exit(1);
11076
12710
  });
11077
12711
  //# sourceMappingURL=index.js.map