@appilots/cli 0.4.0 → 0.10.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.10.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,15 @@ 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
  }
61
130
  const merged = {
62
- serverUrl: "http://localhost:4000",
131
+ serverUrl: DEFAULT_SERVER_URL,
63
132
  outputDir: ".appilots",
64
133
  autoActivate: true,
65
134
  strictScreens: true,
@@ -69,6 +138,12 @@ function loadConfig() {
69
138
  ...env.serverUrl ? { serverUrl: env.serverUrl } : {}
70
139
  };
71
140
  const validation = validateConfig(merged);
141
+ if (onWarn) {
142
+ const source = configPath ?? "environment variables";
143
+ for (const warning of validation.warnings) {
144
+ onWarn(`${source}: ${warning}`);
145
+ }
146
+ }
72
147
  if (!validation.valid) {
73
148
  const source = configPath ?? "environment variables";
74
149
  throw new Error(
@@ -84,7 +159,7 @@ function saveConfig(dir, config) {
84
159
  const mergedConfig = {
85
160
  apiKey: config.apiKey || existingConfig?.apiKey || "",
86
161
  projectId: config.projectId || existingConfig?.projectId || "",
87
- serverUrl: config.serverUrl || existingConfig?.serverUrl || "http://localhost:4000",
162
+ serverUrl: config.serverUrl || existingConfig?.serverUrl || DEFAULT_SERVER_URL,
88
163
  outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
89
164
  autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
90
165
  include: config.include || existingConfig?.include,
@@ -100,7 +175,9 @@ function saveConfig(dir, config) {
100
175
  try {
101
176
  (0, import_fs.writeFileSync)(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
102
177
  } catch (error2) {
103
- throw new Error(`Failed to write .appilotsrc to ${configPath}: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
178
+ throw new Error(
179
+ `Failed to write .appilotsrc to ${configPath}: ${error2 instanceof Error ? error2.message : "Unknown error"}`
180
+ );
104
181
  }
105
182
  }
106
183
  function getConfigPath() {
@@ -127,14 +204,25 @@ function getConfigPath() {
127
204
  }
128
205
  function validateConfig(config) {
129
206
  const errors = [];
207
+ const warnings = [];
130
208
  if (!config || typeof config !== "object") {
131
209
  return {
132
210
  valid: false,
133
- errors: ["Configuration must be an object"]
211
+ errors: ["Configuration must be an object"],
212
+ warnings
134
213
  };
135
214
  }
215
+ for (const key of Object.keys(config)) {
216
+ if (KNOWN_CONFIG_KEYS.includes(key)) continue;
217
+ const suggestion = suggestConfigKey(key);
218
+ warnings.push(
219
+ `unknown key "${key}" \u2014 ignored` + (suggestion ? ` (did you mean "${suggestion}"?)` : "")
220
+ );
221
+ }
136
222
  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)");
223
+ errors.push(
224
+ "apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)"
225
+ );
138
226
  } else if (!config.apiKey.startsWith("ak_")) {
139
227
  errors.push('apiKey must start with "ak_"');
140
228
  }
@@ -196,7 +284,8 @@ function validateConfig(config) {
196
284
  }
197
285
  return {
198
286
  valid: errors.length === 0,
199
- errors
287
+ errors,
288
+ warnings
200
289
  };
201
290
  }
202
291
 
@@ -241,14 +330,23 @@ function dim(msg) {
241
330
  if (mode !== "pretty") return;
242
331
  console.log(import_chalk.default.dim(msg));
243
332
  }
333
+ var BANNER_WIDTH = 39;
334
+ function bannerRow(plain, colored) {
335
+ return ` ${import_chalk.default.cyan("\u2551")} ${colored}${" ".repeat(Math.max(0, BANNER_WIDTH - 5 - plain.length))}${import_chalk.default.cyan("\u2551")}`;
336
+ }
244
337
  function banner() {
245
338
  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
- `;
339
+ const title = "Appilots MCP Generator";
340
+ const version = `v${CLI_VERSION}`;
341
+ const border = "\u2550".repeat(BANNER_WIDTH);
342
+ const logo = [
343
+ "",
344
+ ` ${import_chalk.default.cyan(`\u2554${border}\u2557`)}`,
345
+ bannerRow(title, import_chalk.default.bold.cyan(title)),
346
+ bannerRow(version, import_chalk.default.dim(version)),
347
+ ` ${import_chalk.default.cyan(`\u255A${border}\u255D`)}`,
348
+ " "
349
+ ].join("\n");
252
350
  console.log(logo);
253
351
  }
254
352
  function heading(msg) {
@@ -269,19 +367,105 @@ function table(rows) {
269
367
  // src/cli/commands/init.ts
270
368
  var import_commander = require("commander");
271
369
 
370
+ // ../shared/dist/chunk-UEMUBXFH.mjs
371
+ var DEFAULT_API_BASE_URL = "https://api.appilots.com";
372
+ var APPILOTS_API_PATH_PREFIX = "/api/v1";
373
+ function normalizeApiBaseUrl(configured) {
374
+ const raw = (configured ?? DEFAULT_API_BASE_URL).trim().replace(/\/+$/, "");
375
+ if (!raw) return `${DEFAULT_API_BASE_URL}${APPILOTS_API_PATH_PREFIX}`;
376
+ if (new RegExp(`${APPILOTS_API_PATH_PREFIX}(/|$)`).test(raw)) return raw;
377
+ return `${raw}${APPILOTS_API_PATH_PREFIX}`;
378
+ }
379
+ var LIMITS = {
380
+ MAX_MCP_DOCUMENT_SIZE_MB: 10,
381
+ MAX_PROJECTS_PER_USER: 20,
382
+ MAX_MESSAGE_LENGTH: 4096,
383
+ MAX_ACTIONS_PER_MESSAGE: 10,
384
+ SESSION_TIMEOUT_MS: 30 * 60 * 1e3
385
+ // 30 minutes
386
+ };
387
+
388
+ // src/utils/stringify.ts
389
+ function stringifyUnknown(value) {
390
+ if (value instanceof Error) {
391
+ const message = value.message || value.name;
392
+ const cause = value.cause;
393
+ if (cause instanceof Error && cause.message && cause.message !== message) {
394
+ return `${message} (${cause.message})`;
395
+ }
396
+ return message;
397
+ }
398
+ if (typeof value === "string") return value;
399
+ if (value === null || value === void 0) return String(value);
400
+ if (typeof value !== "object") return String(value);
401
+ try {
402
+ const json = JSON.stringify(value);
403
+ if (json && json !== "{}") return json;
404
+ } catch {
405
+ }
406
+ return String(value);
407
+ }
408
+
272
409
  // src/services/api-client.ts
273
- var DEFAULT_TIMEOUT_MS = 3e4;
410
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
274
411
  var DEFAULT_MAX_RETRIES = 2;
275
412
  var RETRY_BASE_DELAY_MS = 500;
413
+ function describeNetworkError(url, err) {
414
+ let origin = url;
415
+ try {
416
+ origin = new URL(url).origin;
417
+ } catch {
418
+ }
419
+ const cause = err?.cause;
420
+ const code = typeof cause?.code === "string" ? cause.code : void 0;
421
+ const detail = code ?? stringifyUnknown(err);
422
+ return new Error(`Cannot reach Appilots server at ${origin} (${detail})`);
423
+ }
424
+ function errorCodeOf(payload) {
425
+ const error2 = payload?.error;
426
+ if (error2 && typeof error2 === "object") {
427
+ const { code } = error2;
428
+ if (typeof code === "string" && code.trim()) return code;
429
+ }
430
+ return void 0;
431
+ }
432
+ function describeApiError(payload, fallback) {
433
+ const error2 = payload?.error;
434
+ if (typeof error2 === "string" && error2.trim()) return error2;
435
+ if (error2 && typeof error2 === "object") {
436
+ const { code, message: message2 } = error2;
437
+ const text = typeof message2 === "string" && message2.trim() ? message2 : void 0;
438
+ const tag = typeof code === "string" && code.trim() ? code : void 0;
439
+ if (text && tag) return `${text} (${tag})`;
440
+ if (text) return text;
441
+ if (tag) return tag;
442
+ }
443
+ const message = payload?.message;
444
+ if (typeof message === "string" && message.trim()) return message;
445
+ return fallback;
446
+ }
276
447
  var AppilotsAPIClient = class {
277
- serverUrl;
448
+ /**
449
+ * `serverUrl` with the API's mount prefix resolved — every path below
450
+ * is relative to THIS, not to the configured origin.
451
+ *
452
+ * It used to be the raw `serverUrl` with `/api/v1` hardcoded into each
453
+ * path, which made the field mean something different here than in the
454
+ * SDK. Both read the same `.appilotsrc`: the SDK completes the prefix
455
+ * when it is missing, so `https://api.appilots.com/api/v1` is correct
456
+ * there — and here that same value produced
457
+ * `/api/v1/api/v1/cli/sync`, a 404 whose body reads `Route not found`.
458
+ * Sharing `normalizeApiBaseUrl` is what makes one file mean one thing:
459
+ * with or without the prefix now works in both.
460
+ */
461
+ baseUrl;
278
462
  apiKey;
279
463
  timeoutMs;
280
464
  maxRetries;
281
465
  constructor(config) {
282
- this.serverUrl = config.serverUrl.replace(/\/$/, "");
466
+ this.baseUrl = normalizeApiBaseUrl(config.serverUrl);
283
467
  this.apiKey = config.apiKey;
284
- this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
468
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
285
469
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
286
470
  }
287
471
  /**
@@ -305,7 +489,7 @@ var AppilotsAPIClient = class {
305
489
  }
306
490
  return response;
307
491
  } catch (err) {
308
- lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : err;
492
+ lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : describeNetworkError(url, err);
309
493
  } finally {
310
494
  clearTimeout(timer);
311
495
  }
@@ -324,7 +508,7 @@ var AppilotsAPIClient = class {
324
508
  */
325
509
  async sync(content, version, appVersion) {
326
510
  try {
327
- const response = await this.request(`${this.serverUrl}/api/v1/cli/sync`, {
511
+ const response = await this.request(`${this.baseUrl}/cli/sync`, {
328
512
  method: "POST",
329
513
  headers: {
330
514
  "Content-Type": "application/json",
@@ -338,7 +522,9 @@ var AppilotsAPIClient = class {
338
522
  return {
339
523
  success: false,
340
524
  unchanged: false,
341
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
525
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`),
526
+ errorCode: errorCodeOf(errorData),
527
+ errorStatus: response.status
342
528
  };
343
529
  }
344
530
  const json = await response.json();
@@ -351,7 +537,8 @@ var AppilotsAPIClient = class {
351
537
  return {
352
538
  success: false,
353
539
  unchanged: false,
354
- error: error2 instanceof Error ? error2.message : "Failed to sync with Appilots API"
540
+ error: error2 instanceof Error ? error2.message : "Failed to sync with Appilots API",
541
+ errorStatus: 0
355
542
  };
356
543
  }
357
544
  }
@@ -362,7 +549,7 @@ var AppilotsAPIClient = class {
362
549
  */
363
550
  async status() {
364
551
  try {
365
- const response = await this.request(`${this.serverUrl}/api/v1/cli/status`, {
552
+ const response = await this.request(`${this.baseUrl}/cli/status`, {
366
553
  method: "GET",
367
554
  headers: {
368
555
  Authorization: `Bearer ${this.apiKey}`
@@ -371,7 +558,7 @@ var AppilotsAPIClient = class {
371
558
  if (!response.ok) {
372
559
  const errorData = await response.json().catch(() => ({}));
373
560
  return {
374
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
561
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
375
562
  };
376
563
  }
377
564
  const json = await response.json();
@@ -393,7 +580,7 @@ var AppilotsAPIClient = class {
393
580
  */
394
581
  async evalRun(scenarios) {
395
582
  try {
396
- const response = await this.request(`${this.serverUrl}/api/v1/cli/eval/run`, {
583
+ const response = await this.request(`${this.baseUrl}/cli/eval/run`, {
397
584
  method: "POST",
398
585
  headers: {
399
586
  "Content-Type": "application/json",
@@ -424,7 +611,7 @@ var AppilotsAPIClient = class {
424
611
  */
425
612
  async health() {
426
613
  try {
427
- const response = await this.request(`${this.serverUrl}/api/v1/health`, {
614
+ const response = await this.request(`${this.baseUrl}/health`, {
428
615
  method: "GET"
429
616
  });
430
617
  return response.ok;
@@ -437,6 +624,19 @@ var AppilotsAPIClient = class {
437
624
  // src/cli/commands/init.ts
438
625
  var import_fs3 = require("fs");
439
626
  var import_path2 = require("path");
627
+
628
+ // src/cli/utils/server-hint.ts
629
+ function hintUnreachableServer(serverUrl, message) {
630
+ if (serverUrl !== DEFAULT_SERVER_URL) return;
631
+ if (typeof message === "string" && !message.includes("Cannot reach Appilots server")) return;
632
+ if (message !== void 0 && typeof message !== "string") return;
633
+ info(`serverUrl is not set, so the CLI is using the built-in default (${DEFAULT_SERVER_URL})`);
634
+ info("For the hosted backend, set it in .appilotsrc:");
635
+ info(' "serverUrl": "https://api.appilots.com"');
636
+ info("Or pass --server <url>, or set APPILOTS_SERVER_URL");
637
+ }
638
+
639
+ // src/cli/commands/init.ts
440
640
  function ensureConfigIgnored(dir) {
441
641
  const gitignorePath = (0, import_path2.join)(dir, ".gitignore");
442
642
  if (!(0, import_fs3.existsSync)(gitignorePath)) {
@@ -450,7 +650,10 @@ function ensureConfigIgnored(dir) {
450
650
  if (ignored) {
451
651
  return "already-ignored";
452
652
  }
453
- (0, import_fs3.appendFileSync)(gitignorePath, "\n# Appilots CLI config (contains your API key \u2014 do not commit)\n.appilotsrc\n");
653
+ (0, import_fs3.appendFileSync)(
654
+ gitignorePath,
655
+ "\n# Appilots CLI config (contains your API key \u2014 do not commit)\n.appilotsrc\n"
656
+ );
454
657
  return "added";
455
658
  }
456
659
  function initCommand() {
@@ -473,7 +676,7 @@ function initCommand() {
473
676
  if (!options.apiKey && env.apiKey) {
474
677
  info("Using API key from APPILOTS_API_KEY environment variable");
475
678
  }
476
- const serverUrl = options.server || env.serverUrl || "http://localhost:4000";
679
+ const serverUrl = options.server || env.serverUrl || DEFAULT_SERVER_URL;
477
680
  if (!apiKey.startsWith("ak_")) {
478
681
  error("Invalid API key format. Must start with ak_");
479
682
  process.exit(1);
@@ -487,6 +690,7 @@ function initCommand() {
487
690
  if (!isHealthy) {
488
691
  error(`Cannot reach Appilots server at ${serverUrl}`);
489
692
  info("Make sure the server is running and the URL is correct");
693
+ hintUnreachableServer(serverUrl);
490
694
  process.exit(1);
491
695
  }
492
696
  const status = await apiClient.status();
@@ -524,7 +728,7 @@ function initCommand() {
524
728
  dim(" $ appilots sync - Analyze and upload your project");
525
729
  dim(" $ appilots watch - Watch for changes and auto-sync");
526
730
  } catch (err) {
527
- error(`Initialization failed: ${err instanceof Error ? err.message : String(err)}`);
731
+ error(`Initialization failed: ${stringifyUnknown(err)}`);
528
732
  process.exit(1);
529
733
  }
530
734
  });
@@ -533,7 +737,7 @@ function initCommand() {
533
737
  // src/cli/commands/sync.ts
534
738
  var import_commander2 = require("commander");
535
739
  var import_promises5 = require("fs/promises");
536
- var import_node_path4 = require("path");
740
+ var import_node_path5 = require("path");
537
741
 
538
742
  // src/generators/MCPGenerator.ts
539
743
  var import_promises4 = require("fs/promises");
@@ -813,6 +1017,45 @@ function hasJsxAttribute(element, name) {
813
1017
  }
814
1018
 
815
1019
  // src/ast/jsx/classify.ts
1020
+ var NON_INTERACTION_HANDLERS = /* @__PURE__ */ new Set([
1021
+ "onLayout",
1022
+ "onScroll",
1023
+ "onScrollBeginDrag",
1024
+ "onScrollEndDrag",
1025
+ "onMomentumScrollBegin",
1026
+ "onMomentumScrollEnd",
1027
+ "onContentSizeChange",
1028
+ "onEndReached",
1029
+ "onViewableItemsChanged",
1030
+ "onLoad",
1031
+ "onLoadStart",
1032
+ "onLoadEnd",
1033
+ "onError",
1034
+ "onProgress",
1035
+ "onFocus",
1036
+ "onBlur"
1037
+ ]);
1038
+ var VALUE_HANDLERS = /* @__PURE__ */ new Set([
1039
+ "onChange",
1040
+ "onChangeText",
1041
+ "onValueChange",
1042
+ "onSelect",
1043
+ "onSelectionChange",
1044
+ "onSubmitEditing"
1045
+ ]);
1046
+ function pressHandlerProp(element) {
1047
+ let fallback;
1048
+ for (const attr of element.attributes) {
1049
+ if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
1050
+ const name = attr.name.name;
1051
+ if (!/^on[A-Z]/.test(name)) continue;
1052
+ if (attr.value?.type !== "JSXExpressionContainer") continue;
1053
+ if (name === "onPress") return name;
1054
+ if (NON_INTERACTION_HANDLERS.has(name) || VALUE_HANDLERS.has(name)) continue;
1055
+ fallback ??= name;
1056
+ }
1057
+ return fallback;
1058
+ }
816
1059
  var VIEW_COMPONENTS = /* @__PURE__ */ new Set(["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"]);
817
1060
  var INPUT_COMPONENTS = /* @__PURE__ */ new Set(["TextInput", "Input", "HocInput"]);
818
1061
  var BUTTON_COMPONENTS = /* @__PURE__ */ new Set(["TouchableOpacity", "Pressable", "Button"]);
@@ -827,17 +1070,16 @@ function classifyJsxComponent(name, element) {
827
1070
  if (INPUT_COMPONENTS.has(name)) return "input";
828
1071
  if (BUTTON_COMPONENTS.has(name)) return "button";
829
1072
  if (element) {
830
- const hasOptions = hasJsxAttribute(element, "options");
1073
+ const hasOptions = hasJsxAttribute(element, "options") || hasJsxAttribute(element, "segments") || hasJsxAttribute(element, "choices") || hasJsxAttribute(element, "tabs");
831
1074
  const hasValue = hasJsxAttribute(element, "value");
832
1075
  const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
833
1076
  const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange") || hasJsxAttribute(element, "onChangeText");
834
- const hasOnPress = hasJsxAttribute(element, "onPress");
835
1077
  if (hasOptions && (hasValue || hasOnChange)) return "select";
836
1078
  if (hasChecked && hasOnChange) return "toggle";
837
1079
  if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
838
1080
  return "input";
839
1081
  }
840
- if (hasOnPress) return "button";
1082
+ if (pressHandlerProp(element)) return "button";
841
1083
  if (getStringAttr(element, "visible") || getExpressionIdentifierAttr(element, "visible")) {
842
1084
  return "modal";
843
1085
  }
@@ -1165,8 +1407,8 @@ var ScreenAnalyzer = class {
1165
1407
  }
1166
1408
  const screens = [];
1167
1409
  this.screensFilteredOut = 0;
1168
- for (const file of files) {
1169
- const filePath = import_path3.default.resolve(this.config.rootDir, file);
1410
+ for (const file2 of files) {
1411
+ const filePath = import_path3.default.resolve(this.config.rootDir, file2);
1170
1412
  try {
1171
1413
  const descriptor = await this.analyzeFile(filePath);
1172
1414
  if (!descriptor) continue;
@@ -1176,23 +1418,28 @@ var ScreenAnalyzer = class {
1176
1418
  if (!hasRegisterScreen && !matchesPattern) {
1177
1419
  this.screensFilteredOut++;
1178
1420
  if (this.verbose) {
1179
- console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file}`);
1421
+ console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file2}`);
1180
1422
  }
1181
1423
  continue;
1182
1424
  }
1183
1425
  }
1184
1426
  screens.push(descriptor);
1185
1427
  if (this.verbose) {
1186
- console.log(`[ScreenAnalyzer] \u2713 Analyzed: ${descriptor.name} (${file})`);
1428
+ console.log(`[ScreenAnalyzer] \u2713 Analyzed: ${descriptor.name} (${file2})`);
1187
1429
  }
1188
1430
  } catch (error2) {
1189
1431
  if (this.verbose) {
1190
- console.warn(`[ScreenAnalyzer] Failed to parse ${file}:`, error2 instanceof Error ? error2.message : error2);
1432
+ console.warn(
1433
+ `[ScreenAnalyzer] Failed to parse ${file2}:`,
1434
+ error2 instanceof Error ? error2.message : error2
1435
+ );
1191
1436
  }
1192
1437
  }
1193
1438
  }
1194
1439
  if (this.verbose && this.strictScreens) {
1195
- console.log(`[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`);
1440
+ console.log(
1441
+ `[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`
1442
+ );
1196
1443
  }
1197
1444
  return screens;
1198
1445
  }
@@ -1480,7 +1727,11 @@ var ScreenAnalyzer = class {
1480
1727
  for (const form of secondary) {
1481
1728
  const existing = out.find((candidate) => candidate.id === form.id) ?? this.findFormWithSharedFields(out, form);
1482
1729
  if (!existing) {
1483
- out.push({ ...form, fields: [...form.fields], submitAction: this.namedSubmitAction(form.submitAction) });
1730
+ out.push({
1731
+ ...form,
1732
+ fields: [...form.fields],
1733
+ submitAction: this.namedSubmitAction(form.submitAction)
1734
+ });
1484
1735
  continue;
1485
1736
  }
1486
1737
  for (const field of form.fields) {
@@ -1499,9 +1750,12 @@ var ScreenAnalyzer = class {
1499
1750
  findEquivalentField(fields, incoming) {
1500
1751
  return fields.find((field) => {
1501
1752
  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;
1753
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
1754
+ return true;
1755
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
1756
+ return true;
1757
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
1758
+ return true;
1505
1759
  if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
1506
1760
  return true;
1507
1761
  }
@@ -1661,9 +1915,11 @@ var ScreenAnalyzer = class {
1661
1915
  source: field.locator?.source ?? "accessibilityLabel"
1662
1916
  });
1663
1917
  } else if (attrName === "keyboardType") {
1664
- if (attr.value.value === "email-address" || attr.value.value === "email") field.type = "email";
1918
+ if (attr.value.value === "email-address" || attr.value.value === "email")
1919
+ field.type = "email";
1665
1920
  if (attr.value.value === "phone-pad") field.type = "phone";
1666
- if (attr.value.value === "numeric" || attr.value.value === "number-pad") field.type = "number";
1921
+ if (attr.value.value === "numeric" || attr.value.value === "number-pad")
1922
+ field.type = "number";
1667
1923
  }
1668
1924
  } else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
1669
1925
  if (attrName === "value" && BabelTypes.isIdentifier(attr.value.expression)) {
@@ -1829,8 +2085,10 @@ var ScreenAnalyzer = class {
1829
2085
  };
1830
2086
  if (handler.nativeConfirmationExpected) action.nativeConfirmationExpected = true;
1831
2087
  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;
2088
+ if (handler.successSignal && !action.successSignal)
2089
+ action.successSignal = handler.successSignal;
2090
+ if (handler.failureSignal && !action.failureSignal)
2091
+ action.failureSignal = handler.failureSignal;
1834
2092
  if (handler.opensModal && !action.opensModal) action.opensModal = handler.opensModal;
1835
2093
  if (handler.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
1836
2094
  action.destructive = true;
@@ -1848,6 +2106,7 @@ var ScreenAnalyzer = class {
1848
2106
  if (!BabelTypes.isJSXIdentifier(element.name)) return;
1849
2107
  const componentName = element.name.name;
1850
2108
  if (classifyJsxComponent(componentName, element) !== "button") return;
2109
+ const pressProp = pressHandlerProp(element);
1851
2110
  let label;
1852
2111
  let handler;
1853
2112
  for (const attr of element.attributes) {
@@ -1857,7 +2116,7 @@ var ScreenAnalyzer = class {
1857
2116
  if ((attrName === "title" || attrName === "accessibilityLabel" || attrName === "label") && BabelTypes.isStringLiteral(attr.value)) {
1858
2117
  label = attr.value.value;
1859
2118
  }
1860
- if (attrName === "onPress" && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
2119
+ if (attrName === pressProp && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
1861
2120
  handler = attr.value.expression.name;
1862
2121
  }
1863
2122
  }
@@ -1923,10 +2182,13 @@ var ScreenAnalyzer = class {
1923
2182
  }
1924
2183
  };
1925
2184
  if (fn.body) {
1926
- (0, import_traverse4.default)(fn.body, {
1927
- noScope: true,
1928
- enter: (nodePath) => inspectNode(nodePath.node)
1929
- });
2185
+ (0, import_traverse4.default)(
2186
+ fn.body,
2187
+ {
2188
+ noScope: true,
2189
+ enter: (nodePath) => inspectNode(nodePath.node)
2190
+ }
2191
+ );
1930
2192
  }
1931
2193
  const expectedOutcome = targetScreen ? "navigation" : hasStateSetter ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
1932
2194
  return {
@@ -1984,6 +2246,7 @@ var ScreenAnalyzer = class {
1984
2246
  };
1985
2247
  const componentName = BabelTypes.isJSXIdentifier(element.name) ? element.name.name : void 0;
1986
2248
  action.sourceComponent = componentName;
2249
+ const pressProp = pressHandlerProp(element) ?? "onPress";
1987
2250
  let handlerName;
1988
2251
  for (const attr of element.attributes) {
1989
2252
  if (!BabelTypes.isJSXAttribute(attr)) continue;
@@ -2018,7 +2281,7 @@ var ScreenAnalyzer = class {
2018
2281
  }
2019
2282
  }
2020
2283
  } else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
2021
- if (attrName === "onPress" && BabelTypes.isIdentifier(attr.value.expression)) {
2284
+ if (attrName === pressProp && BabelTypes.isIdentifier(attr.value.expression)) {
2022
2285
  handlerName = attr.value.expression.name.toLowerCase();
2023
2286
  action.handler = attr.value.expression.name;
2024
2287
  if (handlerName.includes("submit")) {
@@ -2026,7 +2289,7 @@ var ScreenAnalyzer = class {
2026
2289
  } else if (handlerName.includes("navigate")) {
2027
2290
  action.type = "navigation";
2028
2291
  }
2029
- } else if (attrName === "onPress") {
2292
+ } else if (attrName === pressProp) {
2030
2293
  const inline = this.extractInlineOnPressMetadata(attr.value.expression);
2031
2294
  if (inline.handler) {
2032
2295
  handlerName = inline.handler.toLowerCase();
@@ -2050,7 +2313,11 @@ var ScreenAnalyzer = class {
2050
2313
  }
2051
2314
  }
2052
2315
  if (!action.locator && action.id) {
2053
- action.locator = { id: action.id, label: action.label, source: action.label ? "label" : "inferred" };
2316
+ action.locator = {
2317
+ id: action.id,
2318
+ label: action.label,
2319
+ source: action.label ? "label" : "inferred"
2320
+ };
2054
2321
  }
2055
2322
  if (this.isElementDestructive(element, handlerName, action.id)) {
2056
2323
  action.destructive = true;
@@ -2192,24 +2459,27 @@ var ScreenAnalyzer = class {
2192
2459
  }
2193
2460
  extractRowAction(fn) {
2194
2461
  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;
2462
+ (0, import_traverse4.default)(
2463
+ fn.body,
2464
+ {
2465
+ noScope: true,
2466
+ CallExpression: (nodePath) => {
2467
+ const node = nodePath.node;
2468
+ 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)) {
2469
+ return;
2470
+ }
2471
+ const firstArg = node.arguments[0];
2472
+ if (!BabelTypes.isStringLiteral(firstArg)) return;
2473
+ const params = this.extractNavigationParams(node.arguments[1]);
2474
+ action = {
2475
+ type: "navigation",
2476
+ targetScreen: firstArg.value,
2477
+ ...Object.keys(params).length > 0 ? { params } : {},
2478
+ description: `Pressing a row opens ${firstArg.value}`
2479
+ };
2201
2480
  }
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
2481
  }
2212
- });
2482
+ );
2213
2483
  return action;
2214
2484
  }
2215
2485
  extractNavigationParams(arg) {
@@ -2242,15 +2512,18 @@ var ScreenAnalyzer = class {
2242
2512
  } else if (BabelTypes.isIdentifier(firstParam)) {
2243
2513
  itemNames.add(firstParam.name);
2244
2514
  }
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);
2515
+ (0, import_traverse4.default)(
2516
+ fn.body,
2517
+ {
2518
+ noScope: true,
2519
+ MemberExpression: (nodePath) => {
2520
+ const node = nodePath.node;
2521
+ if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
2522
+ fields.add(node.property.name);
2523
+ }
2251
2524
  }
2252
2525
  }
2253
- });
2526
+ );
2254
2527
  return Array.from(fields).sort();
2255
2528
  }
2256
2529
  inferItemType(dataSource, renderItem, displayFields) {
@@ -2262,11 +2535,19 @@ var ScreenAnalyzer = class {
2262
2535
  if (displayFields.length > 0) return "Item";
2263
2536
  return void 0;
2264
2537
  }
2538
+ /**
2539
+ * Identity comes from how a field is NAMED, not from what the app
2540
+ * sells: `id`/`uuid`/`key`/`slug` are conventions any codebase uses,
2541
+ * `name`/`title`/`email` are how any row introduces itself. `plate`
2542
+ * used to sit in this list and read like one of them — but it is the
2543
+ * example app's schema, and no other tenant ever got its equivalent
2544
+ * (`mrn`, `trackingNumber`, `sku`) added here.
2545
+ */
2265
2546
  inferIdentityFields(keyField, displayFields) {
2266
2547
  const out = /* @__PURE__ */ new Set();
2267
2548
  if (keyField) out.add(keyField);
2268
2549
  for (const field of displayFields) {
2269
- if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
2550
+ if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
2270
2551
  }
2271
2552
  return Array.from(out);
2272
2553
  }
@@ -2277,17 +2558,23 @@ var ScreenAnalyzer = class {
2277
2558
  CallExpression: (nodePath) => {
2278
2559
  const node = nodePath.node;
2279
2560
  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;
2561
+ if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
2562
+ return;
2563
+ if (!BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource)
2564
+ return;
2282
2565
  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;
2566
+ if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
2567
+ return;
2568
+ (0, import_traverse4.default)(
2569
+ fn.body,
2570
+ {
2571
+ noScope: true,
2572
+ Identifier: (innerPath) => {
2573
+ const name = innerPath.node.name;
2574
+ if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
2575
+ }
2289
2576
  }
2290
- });
2577
+ );
2291
2578
  }
2292
2579
  });
2293
2580
  return queryBinding;
@@ -2378,7 +2665,7 @@ var NavigationAnalyzer = class {
2378
2665
  cwd: this.config.rootDir,
2379
2666
  ignore: excludePatterns
2380
2667
  });
2381
- return files.map((file) => import_path4.default.join(this.config.rootDir, file));
2668
+ return files.map((file2) => import_path4.default.join(this.config.rootDir, file2));
2382
2669
  }
2383
2670
  /** Parse navigator definitions from a file */
2384
2671
  parseNavigators(content, filePath) {
@@ -2439,11 +2726,7 @@ var NavigationAnalyzer = class {
2439
2726
  if (t5.isJSXAttribute(initialRouteAttr) && t5.isStringLiteral(initialRouteAttr.value)) {
2440
2727
  navigator.initialRouteName = initialRouteAttr.value.value;
2441
2728
  }
2442
- const screens = this.extractScreensFromNavigator(
2443
- node,
2444
- objectName,
2445
- navigator.type
2446
- );
2729
+ const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
2447
2730
  screensByNavigator.set(objectName, screens);
2448
2731
  }
2449
2732
  }
@@ -2460,27 +2743,112 @@ var NavigationAnalyzer = class {
2460
2743
  }
2461
2744
  return navigators;
2462
2745
  }
2746
+ /**
2747
+ * Is this JSX element `<navigatorVarName.MEMBER …>`?
2748
+ */
2749
+ // Deliberately NOT a type predicate (`node is t.JSXElement`): a false
2750
+ // result would then narrow `node` to "not a JSXElement", and the very
2751
+ // next check — the same node against a different member — would see
2752
+ // `never`. It is a question about the member name, not about the node
2753
+ // kind.
2754
+ isNavigatorMember(node, navigatorVarName, member) {
2755
+ if (!t5.isJSXElement(node)) return false;
2756
+ const name = node.openingElement.name;
2757
+ return t5.isJSXMemberExpression(name) && t5.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && t5.isJSXIdentifier(name.property) && name.property.name === member;
2758
+ }
2759
+ /**
2760
+ * Flatten a navigator's JSX children into the `<X.Screen>` elements they
2761
+ * contain, unwrapping every container a real app puts in between.
2762
+ *
2763
+ * The old version compared `t.isJSXElement(child)` against the direct
2764
+ * children only. A ternary is a `JSXExpressionContainer`, so an
2765
+ * auth-gated root — the modal shape of a commercial app, and the shape
2766
+ * of this repo's own `apps/example-app` — contributed ZERO screens to
2767
+ * the graph while `appilots sync` reported success (#396).
2768
+ *
2769
+ * Both branches of a conditional are collected on purpose. The graph is
2770
+ * a design-time map of what routes EXIST, not a prediction of which one
2771
+ * a given session will render; the agent needs the destination name to
2772
+ * be there whether or not the user happens to be logged in right now.
2773
+ */
2774
+ collectScreenElements(children, navigatorVarName, depth = 0) {
2775
+ if (depth > 12) return [];
2776
+ const found = [];
2777
+ const visit = (node) => {
2778
+ if (!node) return;
2779
+ if (this.isNavigatorMember(node, navigatorVarName, "Screen")) {
2780
+ found.push(node);
2781
+ return;
2782
+ }
2783
+ if (this.isNavigatorMember(node, navigatorVarName, "Group")) {
2784
+ const group = node;
2785
+ found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
2786
+ return;
2787
+ }
2788
+ if (t5.isJSXElement(node)) {
2789
+ const name = node.openingElement.name;
2790
+ const isForeignNavigator = t5.isJSXMemberExpression(name) && t5.isJSXIdentifier(name.property) && name.property.name === "Navigator";
2791
+ if (isForeignNavigator) return;
2792
+ found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
2793
+ return;
2794
+ }
2795
+ if (t5.isJSXFragment(node)) {
2796
+ found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
2797
+ return;
2798
+ }
2799
+ if (t5.isJSXExpressionContainer(node)) {
2800
+ visit(node.expression);
2801
+ return;
2802
+ }
2803
+ if (t5.isConditionalExpression(node)) {
2804
+ visit(node.consequent);
2805
+ visit(node.alternate);
2806
+ return;
2807
+ }
2808
+ if (t5.isLogicalExpression(node)) {
2809
+ visit(node.left);
2810
+ visit(node.right);
2811
+ return;
2812
+ }
2813
+ if (t5.isCallExpression(node)) {
2814
+ for (const arg of node.arguments) {
2815
+ if (t5.isArrowFunctionExpression(arg) || t5.isFunctionExpression(arg)) {
2816
+ if (t5.isBlockStatement(arg.body)) {
2817
+ for (const stmt of arg.body.body) {
2818
+ if (t5.isReturnStatement(stmt)) visit(stmt.argument);
2819
+ }
2820
+ } else {
2821
+ visit(arg.body);
2822
+ }
2823
+ }
2824
+ }
2825
+ return;
2826
+ }
2827
+ if (t5.isArrayExpression(node)) {
2828
+ for (const el of node.elements) visit(el);
2829
+ return;
2830
+ }
2831
+ if (t5.isTSAsExpression(node) || t5.isTSNonNullExpression(node)) {
2832
+ visit(node.expression);
2833
+ }
2834
+ };
2835
+ for (const child of children) visit(child);
2836
+ return found;
2837
+ }
2463
2838
  /** Extract screens from a navigator JSX element */
2464
2839
  extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
2465
2840
  const screens = [];
2466
2841
  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
- }
2842
+ const seen = /* @__PURE__ */ new Set();
2843
+ for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
2844
+ const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
2845
+ if (!screenName || seen.has(screenName)) continue;
2846
+ seen.add(screenName);
2847
+ screens.push({
2848
+ name: screenName,
2849
+ navigatorName: navigatorVarName,
2850
+ navigatorType
2851
+ });
2484
2852
  }
2485
2853
  return screens;
2486
2854
  }
@@ -2539,9 +2907,7 @@ var NavigationAnalyzer = class {
2539
2907
  if (member.type === "TSPropertySignature" && member.key) {
2540
2908
  const keyName = member.key.type === "Identifier" ? member.key.name : null;
2541
2909
  if (keyName && member.typeAnnotation) {
2542
- const params = this.extractParamsFromType(
2543
- member.typeAnnotation.typeAnnotation
2544
- );
2910
+ const params = this.extractParamsFromType(member.typeAnnotation.typeAnnotation);
2545
2911
  entries.set(keyName, params);
2546
2912
  }
2547
2913
  }
@@ -2581,7 +2947,7 @@ var NavigationAnalyzer = class {
2581
2947
  if (type.type === "TSUndefinedKeyword") return "undefined";
2582
2948
  if (type.type === "TSNullKeyword") return "null";
2583
2949
  if (type.type === "TSUnionType") {
2584
- return type.types.map((t12) => this.typeToString(t12)).join(" | ");
2950
+ return type.types.map((t15) => this.typeToString(t15)).join(" | ");
2585
2951
  }
2586
2952
  if (type.type === "TSTypeLiteral") {
2587
2953
  return "object";
@@ -2602,7 +2968,7 @@ var NavigationAnalyzer = class {
2602
2968
  /** Attach parsed type params to navigator screens */
2603
2969
  attachParamsToNavigators(navigators, types) {
2604
2970
  for (const navigator of navigators) {
2605
- const matchingType = types.find((t12) => t12.type === navigator.type);
2971
+ const matchingType = types.find((t15) => t15.type === navigator.type);
2606
2972
  if (matchingType) {
2607
2973
  for (const screen of navigator.screens) {
2608
2974
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -2620,6 +2986,11 @@ var NavigationAnalyzer = class {
2620
2986
  let initialScreen = "";
2621
2987
  for (const navigator of parsedNavigators) {
2622
2988
  const screenNames = navigator.screens.map((s) => s.name);
2989
+ if (screenNames.length === 0) {
2990
+ console.warn(
2991
+ `[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.`
2992
+ );
2993
+ }
2623
2994
  navigators.push({
2624
2995
  name: navigator.name,
2625
2996
  type: navigator.type,
@@ -2693,8 +3064,8 @@ var ComponentAnalyzer = class {
2693
3064
  });
2694
3065
  const components = [];
2695
3066
  (0, import_traverse6.default)(ast, {
2696
- JSXElement: (path9) => {
2697
- const component = this.extractComponentFromJSXElement(path9.node);
3067
+ JSXElement: (path11) => {
3068
+ const component = this.extractComponentFromJSXElement(path11.node);
2698
3069
  if (component) {
2699
3070
  components.push(component);
2700
3071
  }
@@ -2828,13 +3199,13 @@ var FormAnalyzer = class {
2828
3199
  this.inputElements = [];
2829
3200
  this.submitButtons = [];
2830
3201
  (0, import_traverse7.default)(ast, {
2831
- CallExpression: (path9) => {
2832
- this.extractStateVariables(path9.node);
3202
+ CallExpression: (path11) => {
3203
+ this.extractStateVariables(path11.node);
2833
3204
  }
2834
3205
  });
2835
3206
  (0, import_traverse7.default)(ast, {
2836
- JSXElement: (path9) => {
2837
- this.extractFormElements(path9.node);
3207
+ JSXElement: (path11) => {
3208
+ this.extractFormElements(path11.node);
2838
3209
  }
2839
3210
  });
2840
3211
  const validationRules = this.extractValidationRules(ast);
@@ -2934,8 +3305,8 @@ var FormAnalyzer = class {
2934
3305
  extractValidationRules(ast) {
2935
3306
  const rules = {};
2936
3307
  (0, import_traverse7.default)(ast, {
2937
- IfStatement: (path9) => {
2938
- const test = path9.node.test;
3308
+ IfStatement: (path11) => {
3309
+ const test = path11.node.test;
2939
3310
  const rule = this.extractRuleFromCondition(test);
2940
3311
  if (rule) {
2941
3312
  const { field, description } = rule;
@@ -3068,8 +3439,8 @@ var ReactNativePlatformAnalyzer = class {
3068
3439
  console.log(
3069
3440
  `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
3070
3441
  );
3071
- const enrichmentPromises = screenFiles.map(async (file) => {
3072
- const filePath = import_node_path.default.resolve(config.rootDir, file);
3442
+ const enrichmentPromises = screenFiles.map(async (file2) => {
3443
+ const filePath = import_node_path.default.resolve(config.rootDir, file2);
3073
3444
  try {
3074
3445
  const [components, forms] = await Promise.all([
3075
3446
  componentAnalyzer.analyzeFile(filePath),
@@ -3077,7 +3448,7 @@ var ReactNativePlatformAnalyzer = class {
3077
3448
  ]);
3078
3449
  return { filePath, components, forms };
3079
3450
  } catch (error2) {
3080
- console.warn(`[ReactNativePlatformAnalyzer] Failed to analyze ${file}:`, error2);
3451
+ console.warn(`[ReactNativePlatformAnalyzer] Failed to analyze ${file2}:`, error2);
3081
3452
  return { filePath, components: [], forms: [] };
3082
3453
  }
3083
3454
  });
@@ -3345,13 +3716,13 @@ function staticRoutePath(node) {
3345
3716
  if (!node) return void 0;
3346
3717
  if (t9.isStringLiteral(node)) return node.value;
3347
3718
  if (t9.isTemplateLiteral(node)) {
3348
- let path9 = "";
3719
+ let path11 = "";
3349
3720
  node.quasis.forEach((quasi, index) => {
3350
- path9 += quasi.value.cooked ?? quasi.value.raw;
3721
+ path11 += quasi.value.cooked ?? quasi.value.raw;
3351
3722
  const expr = node.expressions[index];
3352
- if (expr) path9 += `:${paramNameOf(expr)}`;
3723
+ if (expr) path11 += `:${paramNameOf(expr)}`;
3353
3724
  });
3354
- return path9;
3725
+ return path11;
3355
3726
  }
3356
3727
  return void 0;
3357
3728
  }
@@ -3436,27 +3807,29 @@ var WebScreenAnalyzer = class {
3436
3807
  include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
3437
3808
  exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
3438
3809
  } = this.config;
3439
- const files = await (0, import_fast_glob4.default)(include, { cwd: this.config.rootDir, ignore: exclude });
3810
+ const files = (await (0, import_fast_glob4.default)(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
3811
+ (file2) => !file2.endsWith(".d.ts")
3812
+ );
3440
3813
  const patternMatches = await (0, import_fast_glob4.default)(this.screenPatterns, {
3441
3814
  cwd: this.config.rootDir,
3442
3815
  ignore: exclude
3443
3816
  });
3444
3817
  const patternSet = new Set(patternMatches.map((f) => import_path5.default.resolve(this.config.rootDir, f)));
3445
3818
  const candidates = [];
3446
- for (const file of files) {
3447
- const filePath = import_path5.default.resolve(this.config.rootDir, file);
3819
+ for (const file2 of files) {
3820
+ const filePath = import_path5.default.resolve(this.config.rootDir, file2);
3448
3821
  try {
3449
3822
  const candidate = await this.analyzeFile(filePath);
3450
3823
  if (!candidate) continue;
3451
3824
  candidate.matchesScreenPattern = patternSet.has(filePath);
3452
3825
  candidates.push(candidate);
3453
3826
  if (this.verbose) {
3454
- console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file})`);
3827
+ console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file2})`);
3455
3828
  }
3456
3829
  } catch (error2) {
3457
3830
  if (this.verbose) {
3458
3831
  console.warn(
3459
- `[WebScreenAnalyzer] Failed to parse ${file}:`,
3832
+ `[WebScreenAnalyzer] Failed to parse ${file2}:`,
3460
3833
  error2 instanceof Error ? error2.message : error2
3461
3834
  );
3462
3835
  }
@@ -3807,7 +4180,8 @@ var WebScreenAnalyzer = class {
3807
4180
  if (type === "email") return "email";
3808
4181
  if (type === "tel") return "phone";
3809
4182
  if (type === "number") return "number";
3810
- if (type === "date" || type === "datetime-local" || type === "month" || type === "week") return "date";
4183
+ if (type === "date" || type === "datetime-local" || type === "month" || type === "week")
4184
+ return "date";
3811
4185
  const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
3812
4186
  if (inputMode === "email") return "email";
3813
4187
  if (inputMode === "tel") return "phone";
@@ -4121,7 +4495,9 @@ var WebScreenAnalyzer = class {
4121
4495
  }
4122
4496
  const callback = expr.arguments[0];
4123
4497
  if (!t10.isArrowFunctionExpression(callback) && !t10.isFunctionExpression(callback)) return;
4124
- const enclosing = nodePath.findParent((p) => p.isJSXElement());
4498
+ const enclosing = nodePath.findParent(
4499
+ (p) => p.isJSXElement()
4500
+ );
4125
4501
  const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
4126
4502
  const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
4127
4503
  const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
@@ -4223,16 +4599,19 @@ function firstCalledFunctionName(node) {
4223
4599
  }
4224
4600
  function containsWindowConfirm(body) {
4225
4601
  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;
4602
+ (0, import_traverse9.default)(
4603
+ body,
4604
+ {
4605
+ noScope: true,
4606
+ CallExpression: (nodePath) => {
4607
+ const callee = nodePath.node.callee;
4608
+ if (t10.isIdentifier(callee) && callee.name === "confirm") found = true;
4609
+ if (t10.isMemberExpression(callee) && t10.isIdentifier(callee.object) && callee.object.name === "window" && t10.isIdentifier(callee.property) && callee.property.name === "confirm") {
4610
+ found = true;
4611
+ }
4233
4612
  }
4234
4613
  }
4235
- });
4614
+ );
4236
4615
  return found;
4237
4616
  }
4238
4617
  function mergeLocator(current, next) {
@@ -4272,32 +4651,38 @@ function collectionItemNames(callback) {
4272
4651
  function collectionDisplayFields(callback, itemNames) {
4273
4652
  const fields = /* @__PURE__ */ new Set();
4274
4653
  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);
4654
+ (0, import_traverse9.default)(
4655
+ callback.body,
4656
+ {
4657
+ noScope: true,
4658
+ MemberExpression: (nodePath) => {
4659
+ const node = nodePath.node;
4660
+ if (t10.isIdentifier(node.object) && itemNames.has(node.object.name) && t10.isIdentifier(node.property)) {
4661
+ fields.add(node.property.name);
4662
+ }
4281
4663
  }
4282
4664
  }
4283
- });
4665
+ );
4284
4666
  return Array.from(fields).sort();
4285
4667
  }
4286
4668
  function collectionKeyField(callback, itemNames) {
4287
4669
  let keyField;
4288
4670
  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;
4671
+ (0, import_traverse9.default)(
4672
+ callback.body,
4673
+ {
4674
+ noScope: true,
4675
+ JSXAttribute: (nodePath) => {
4676
+ const attr = nodePath.node;
4677
+ if (!t10.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
4678
+ if (!attr.value || !t10.isJSXExpressionContainer(attr.value)) return;
4679
+ const expr = attr.value.expression;
4680
+ if (t10.isMemberExpression(expr) && t10.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t10.isIdentifier(expr.property)) {
4681
+ keyField = keyField ?? expr.property.name;
4682
+ }
4298
4683
  }
4299
4684
  }
4300
- });
4685
+ );
4301
4686
  return keyField;
4302
4687
  }
4303
4688
  function collectionRowAction(callback) {
@@ -4345,7 +4730,7 @@ function inferIdentityFields(keyField, displayFields) {
4345
4730
  const out = /* @__PURE__ */ new Set();
4346
4731
  if (keyField) out.add(keyField);
4347
4732
  for (const field of displayFields) {
4348
- if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
4733
+ if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
4349
4734
  }
4350
4735
  return Array.from(out);
4351
4736
  }
@@ -4403,7 +4788,7 @@ var WebNavigationAnalyzer = class {
4403
4788
  ...this.navigationExclude
4404
4789
  ];
4405
4790
  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));
4791
+ return files.map((file2) => import_path6.default.join(this.config.rootDir, file2));
4407
4792
  }
4408
4793
  // ── JSX <Route> style ────────────────────────────────────────────
4409
4794
  extractJsxRoutes(ast) {
@@ -4905,8 +5290,8 @@ var ZodParsedType = util.arrayToEnum([
4905
5290
  "set"
4906
5291
  ]);
4907
5292
  var getParsedType = (data) => {
4908
- const t12 = typeof data;
4909
- switch (t12) {
5293
+ const t15 = typeof data;
5294
+ switch (t15) {
4910
5295
  case "undefined":
4911
5296
  return ZodParsedType.undefined;
4912
5297
  case "string":
@@ -5178,8 +5563,8 @@ function getErrorMap() {
5178
5563
 
5179
5564
  // ../../node_modules/zod/v3/helpers/parseUtil.js
5180
5565
  var makeIssue = (params) => {
5181
- const { data, path: path9, errorMaps, issueData } = params;
5182
- const fullPath = [...path9, ...issueData.path || []];
5566
+ const { data, path: path11, errorMaps, issueData } = params;
5567
+ const fullPath = [...path11, ...issueData.path || []];
5183
5568
  const fullIssue = {
5184
5569
  ...issueData,
5185
5570
  path: fullPath
@@ -5295,11 +5680,11 @@ var errorUtil;
5295
5680
 
5296
5681
  // ../../node_modules/zod/v3/types.js
5297
5682
  var ParseInputLazyPath = class {
5298
- constructor(parent, value, path9, key) {
5683
+ constructor(parent, value, path11, key) {
5299
5684
  this._cachedPath = [];
5300
5685
  this.parent = parent;
5301
5686
  this.data = value;
5302
- this._path = path9;
5687
+ this._path = path11;
5303
5688
  this._key = key;
5304
5689
  }
5305
5690
  get path() {
@@ -8741,7 +9126,7 @@ var coerce = {
8741
9126
  };
8742
9127
  var NEVER = INVALID;
8743
9128
 
8744
- // ../shared/dist/chunk-MSOZJNXB.mjs
9129
+ // ../shared/dist/chunk-PKS3VDNB.mjs
8745
9130
  var locatorSourceSchema = external_exports.enum([
8746
9131
  "appilotsId",
8747
9132
  "testID",
@@ -8767,7 +9152,16 @@ var locatorDescriptorSchema = external_exports.object({
8767
9152
  source: locatorSourceSchema.optional()
8768
9153
  }).passthrough();
8769
9154
  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)),
9155
+ type: external_exports.enum([
9156
+ "navigation",
9157
+ "goBack",
9158
+ "toast",
9159
+ "modal",
9160
+ "inline-error",
9161
+ "data-arrival",
9162
+ "loading",
9163
+ "none"
9164
+ ]).or(external_exports.string().min(1).max(60)),
8771
9165
  target: external_exports.string().optional(),
8772
9166
  description: external_exports.string().optional()
8773
9167
  }).passthrough();
@@ -8781,7 +9175,20 @@ var waitPolicyDescriptorSchema = external_exports.object({
8781
9175
  signals: external_exports.array(signalDescriptorSchema).optional(),
8782
9176
  maxMs: external_exports.number().finite().optional()
8783
9177
  }).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));
9178
+ var targetDescriptorRoleSchema = external_exports.enum([
9179
+ "button",
9180
+ "submit",
9181
+ "input",
9182
+ "toggle",
9183
+ "select",
9184
+ "date",
9185
+ "list",
9186
+ "row",
9187
+ "menuItem",
9188
+ "modal",
9189
+ "custom"
9190
+ ]).or(external_exports.string().min(1).max(60));
9191
+ var targetOriginSchema = external_exports.enum(["analyzer", "manifest"]).or(external_exports.string().min(1).max(40));
8785
9192
  var targetDescriptorSchema = external_exports.object({
8786
9193
  id: external_exports.string(),
8787
9194
  role: targetDescriptorRoleSchema,
@@ -8795,10 +9202,20 @@ var targetDescriptorSchema = external_exports.object({
8795
9202
  requiresConfirmation: external_exports.boolean().optional(),
8796
9203
  opensModal: external_exports.string().optional(),
8797
9204
  opensBottomSheet: external_exports.string().optional(),
8798
- sourceComponent: external_exports.string().optional()
9205
+ sourceComponent: external_exports.string().optional(),
9206
+ origin: targetOriginSchema.optional()
8799
9207
  }).passthrough();
8800
9208
  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)),
9209
+ type: external_exports.enum([
9210
+ "navigate",
9211
+ "fill",
9212
+ "press",
9213
+ "select",
9214
+ "toggle",
9215
+ "wait",
9216
+ "confirm",
9217
+ "choose-list-item"
9218
+ ]).or(external_exports.string().min(1).max(60)),
8802
9219
  target: external_exports.string().optional(),
8803
9220
  label: external_exports.string().optional(),
8804
9221
  description: external_exports.string().optional(),
@@ -9020,6 +9437,15 @@ var mfaRegenerateRecoveryCodesSchema = external_exports.object({
9020
9437
  password: external_exports.string().min(1, "Password is required"),
9021
9438
  code: totpCodeSchema
9022
9439
  });
9440
+ var deleteAccountSchema = external_exports.object({
9441
+ password: external_exports.string().min(1, "Password is required"),
9442
+ code: totpCodeSchema.optional(),
9443
+ recoveryCode: recoveryCodeSchema.optional(),
9444
+ confirm: external_exports.literal("DELETE MY ACCOUNT")
9445
+ }).refine((v) => !(v.code && v.recoveryCode), {
9446
+ message: "Provide either a TOTP code or a recovery code, not both",
9447
+ path: ["code"]
9448
+ });
9023
9449
  var createProjectSchema = external_exports.object({
9024
9450
  name: external_exports.string().min(1).max(100),
9025
9451
  description: external_exports.string().max(500).optional(),
@@ -9055,6 +9481,7 @@ var uploadMCPDocumentSchema = external_exports.object({
9055
9481
  content: external_exports.record(external_exports.unknown())
9056
9482
  });
9057
9483
  var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
9484
+ var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
9058
9485
  var createApiKeySchema = external_exports.object({
9059
9486
  name: external_exports.string().min(1).max(100),
9060
9487
  // Required for `sdk` (default), forbidden/ignored for `operator` —
@@ -9062,6 +9489,7 @@ var createApiKeySchema = external_exports.object({
9062
9489
  // missing scope still validates as the pre-existing SDK shape.
9063
9490
  projectId: external_exports.string().min(1).optional(),
9064
9491
  scope: apiKeyScopeSchema.default("sdk"),
9492
+ environment: apiKeyEnvironmentSchema.optional(),
9065
9493
  expiresAt: external_exports.string().datetime().optional()
9066
9494
  }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
9067
9495
  message: 'projectId is required for scope "sdk"',
@@ -9069,13 +9497,23 @@ var createApiKeySchema = external_exports.object({
9069
9497
  });
9070
9498
  var boundedString = (max) => external_exports.string().max(max);
9071
9499
  var clientPlatformSchema = external_exports.enum(["react-native", "web", "android", "ios"]).or(external_exports.string().min(1).max(40));
9500
+ var identityProvenanceSchema = external_exports.enum(["declared", "derived", "positional"]).or(external_exports.string().min(1).max(40));
9072
9501
  var snapshotInputSchema = external_exports.object({
9073
9502
  id: boundedString(160).optional(),
9503
+ provenance: identityProvenanceSchema.optional(),
9504
+ /**
9505
+ * False when the control is mounted but currently OUTSIDE the window —
9506
+ * below the fold, scrolled off, pushed sideways. Absent means the client
9507
+ * cannot tell (old React Native architecture, or any SDK published
9508
+ * before the field existed), which must never be read as `false`.
9509
+ */
9510
+ onScreen: external_exports.boolean().optional(),
9074
9511
  label: boundedString(300).optional(),
9075
9512
  value: boundedString(4096).optional(),
9076
9513
  placeholder: boundedString(300).optional(),
9077
9514
  editable: external_exports.boolean().optional(),
9078
9515
  secure: external_exports.boolean().optional(),
9516
+ submitsOnReturn: external_exports.boolean().optional(),
9079
9517
  type: boundedString(40).optional(),
9080
9518
  required: external_exports.boolean().optional(),
9081
9519
  invalid: external_exports.boolean().optional(),
@@ -9083,6 +9521,14 @@ var snapshotInputSchema = external_exports.object({
9083
9521
  }).passthrough();
9084
9522
  var snapshotButtonSchema = external_exports.object({
9085
9523
  id: boundedString(160).optional(),
9524
+ provenance: identityProvenanceSchema.optional(),
9525
+ /**
9526
+ * False when the control is mounted but currently OUTSIDE the window —
9527
+ * below the fold, scrolled off, pushed sideways. Absent means the client
9528
+ * cannot tell (old React Native architecture, or any SDK published
9529
+ * before the field existed), which must never be read as `false`.
9530
+ */
9531
+ onScreen: external_exports.boolean().optional(),
9086
9532
  label: boundedString(300).optional(),
9087
9533
  disabled: external_exports.boolean().optional(),
9088
9534
  inModal: external_exports.boolean().optional(),
@@ -9092,12 +9538,28 @@ var snapshotButtonSchema = external_exports.object({
9092
9538
  }).passthrough();
9093
9539
  var snapshotToggleSchema = external_exports.object({
9094
9540
  id: boundedString(160).optional(),
9541
+ provenance: identityProvenanceSchema.optional(),
9542
+ /**
9543
+ * False when the control is mounted but currently OUTSIDE the window —
9544
+ * below the fold, scrolled off, pushed sideways. Absent means the client
9545
+ * cannot tell (old React Native architecture, or any SDK published
9546
+ * before the field existed), which must never be read as `false`.
9547
+ */
9548
+ onScreen: external_exports.boolean().optional(),
9095
9549
  label: boundedString(300).optional(),
9096
9550
  value: external_exports.boolean().optional(),
9097
9551
  inModal: external_exports.boolean().optional()
9098
9552
  }).passthrough();
9099
9553
  var snapshotSliderSchema = external_exports.object({
9100
9554
  id: boundedString(160).optional(),
9555
+ provenance: identityProvenanceSchema.optional(),
9556
+ /**
9557
+ * False when the control is mounted but currently OUTSIDE the window —
9558
+ * below the fold, scrolled off, pushed sideways. Absent means the client
9559
+ * cannot tell (old React Native architecture, or any SDK published
9560
+ * before the field existed), which must never be read as `false`.
9561
+ */
9562
+ onScreen: external_exports.boolean().optional(),
9101
9563
  label: boundedString(300).optional(),
9102
9564
  value: external_exports.number().finite().optional(),
9103
9565
  min: external_exports.number().finite().optional(),
@@ -9138,6 +9600,26 @@ var snapshotListSchema = external_exports.object({
9138
9600
  // should chase, and the relay only reads a known subset.
9139
9601
  items: external_exports.array(external_exports.record(external_exports.unknown())).max(500).optional()
9140
9602
  }).passthrough();
9603
+ var snapshotNativeDialogSchema = external_exports.object({
9604
+ id: boundedString(160),
9605
+ title: boundedString(300).optional(),
9606
+ message: boundedString(2e3).optional(),
9607
+ buttons: external_exports.array(
9608
+ external_exports.object({
9609
+ label: boundedString(160),
9610
+ style: external_exports.enum(["default", "cancel", "destructive"]).optional()
9611
+ }).passthrough()
9612
+ ).max(10)
9613
+ }).passthrough();
9614
+ var snapshotScrollableSchema = external_exports.object({
9615
+ id: boundedString(160),
9616
+ containerType: boundedString(60).optional(),
9617
+ label: boundedString(300).optional(),
9618
+ scrollOffsetY: external_exports.number().finite().optional(),
9619
+ canScrollUp: external_exports.boolean().optional(),
9620
+ canScrollDown: external_exports.boolean().optional(),
9621
+ horizontal: external_exports.boolean().optional()
9622
+ }).passthrough();
9141
9623
  var snapshotChoiceGroupSchema = external_exports.object({
9142
9624
  index: external_exports.number().int().optional(),
9143
9625
  id: boundedString(160).optional(),
@@ -9154,6 +9636,14 @@ var snapshotElementSchema = external_exports.object({
9154
9636
  disabled: external_exports.boolean().optional(),
9155
9637
  selected: external_exports.boolean().optional(),
9156
9638
  source: boundedString(40).optional(),
9639
+ provenance: identityProvenanceSchema.optional(),
9640
+ /**
9641
+ * False when the control is mounted but currently OUTSIDE the window —
9642
+ * below the fold, scrolled off, pushed sideways. Absent means the client
9643
+ * cannot tell (old React Native architecture, or any SDK published
9644
+ * before the field existed), which must never be read as `false`.
9645
+ */
9646
+ onScreen: external_exports.boolean().optional(),
9157
9647
  targetId: boundedString(160).optional(),
9158
9648
  listContext: external_exports.record(external_exports.unknown()).optional(),
9159
9649
  inModal: external_exports.boolean().optional()
@@ -9170,7 +9660,34 @@ var agentSnapshotSchema = external_exports.object({
9170
9660
  modalOpen: external_exports.boolean().optional(),
9171
9661
  lists: external_exports.array(snapshotListSchema).max(100).optional(),
9172
9662
  choiceGroups: external_exports.array(snapshotChoiceGroupSchema).max(100).optional(),
9173
- elements: external_exports.array(snapshotElementSchema).max(1e3).optional()
9663
+ scrollables: external_exports.array(snapshotScrollableSchema).max(20).optional(),
9664
+ nativeDialog: snapshotNativeDialogSchema.optional(),
9665
+ elements: external_exports.array(snapshotElementSchema).max(1e3).optional(),
9666
+ /**
9667
+ * The client clamped this observation to the caps above and lost
9668
+ * data doing it (`clampSnapshotToWireLimits` in
9669
+ * `@appilots/client-core`). Read by the prompt serializer so the
9670
+ * model is told the view is partial — a screen truncated at 500
9671
+ * texts must not be read as a screen that only has 500 things on
9672
+ * it. Optional: older SDKs never clamp and never send it.
9673
+ */
9674
+ truncated: external_exports.boolean().optional(),
9675
+ /**
9676
+ * How many of this screen's controls the client could name, split by
9677
+ * `identityProvenance`. Diagnostic only — the relay never grounds an
9678
+ * action on it — but it is the number that says whether an app needs
9679
+ * annotating, and comparing it between a debug and a release build is
9680
+ * the first direct measurement of what minification costs the agent.
9681
+ */
9682
+ stats: external_exports.object({
9683
+ visitedFibers: external_exports.number().int().nonnegative().optional(),
9684
+ skippedHidden: external_exports.number().int().nonnegative().optional(),
9685
+ identity: external_exports.object({
9686
+ declared: external_exports.number().int().nonnegative().optional(),
9687
+ derived: external_exports.number().int().nonnegative().optional(),
9688
+ positional: external_exports.number().int().nonnegative().optional()
9689
+ }).passthrough().optional()
9690
+ }).passthrough().optional()
9174
9691
  }).passthrough();
9175
9692
  var agentContextSchema = external_exports.object({
9176
9693
  /**
@@ -9220,6 +9737,19 @@ var agentContextSchema = external_exports.object({
9220
9737
  ).max(200).optional(),
9221
9738
  loadingPending: external_exports.boolean().optional()
9222
9739
  }).passthrough();
9740
+ var introspectionFailureReasonSchema = external_exports.enum([
9741
+ "sentinel-missing-internals",
9742
+ "never-mounted",
9743
+ "render-crashed",
9744
+ "names-mangled",
9745
+ "walk-recognized-nothing"
9746
+ ]);
9747
+ var introspectionDiagnosticsSchema = external_exports.object({
9748
+ captured: external_exports.boolean(),
9749
+ failureReason: introspectionFailureReasonSchema.nullish(),
9750
+ reactVersion: boundedString(32).nullish(),
9751
+ failureCount: external_exports.number().int().min(0).max(1e6).optional()
9752
+ });
9223
9753
  var sendMessageSchema = external_exports.object({
9224
9754
  content: external_exports.string().min(1).max(4096),
9225
9755
  sessionId: external_exports.string().optional(),
@@ -9229,7 +9759,20 @@ var sendMessageSchema = external_exports.object({
9229
9759
  * the active MCP doc and emits `mcp_version_mismatch` telemetry when
9230
9760
  * they differ. Optional — older SDKs simply skip the check.
9231
9761
  */
9232
- mcpVersion: external_exports.string().max(64).optional()
9762
+ mcpVersion: external_exports.string().max(64).optional(),
9763
+ /**
9764
+ * SDK-side introspection health. Only sent when it FAILED — a client
9765
+ * that reached the React tree omits the field entirely, so the happy
9766
+ * path costs nothing on the wire.
9767
+ *
9768
+ * Without this, an app whose introspection is dead sends a snapshot
9769
+ * that is empty but perfectly well-formed, indistinguishable from a
9770
+ * genuinely empty screen. The server emits
9771
+ * `introspection_unavailable` so the failure is visible to us instead
9772
+ * of waiting for a bug report. Optional — older SDKs simply never
9773
+ * send it.
9774
+ */
9775
+ introspection: introspectionDiagnosticsSchema.optional()
9233
9776
  });
9234
9777
  var agentActionTypeSchema = external_exports.enum([
9235
9778
  "navigate",
@@ -9240,11 +9783,22 @@ var agentActionTypeSchema = external_exports.enum([
9240
9783
  "custom"
9241
9784
  ]);
9242
9785
  var navigationPayloadSchema = external_exports.object({
9243
- screenName: external_exports.string().min(1),
9786
+ // Optional ONLY so `goBack` can exist (#398): it returns to whatever
9787
+ // is beneath on the stack and has no destination to name. Every other
9788
+ // action is still required to carry one — enforced by the refine
9789
+ // below rather than by the field, so the error names the real problem
9790
+ // ("navigate needs a screenName") instead of a missing key.
9791
+ screenName: external_exports.string().min(1).optional(),
9244
9792
  params: external_exports.record(external_exports.unknown()).optional(),
9245
9793
  navigationAction: external_exports.enum(["push", "navigate", "replace", "goBack", "reset"]).default("navigate").optional(),
9246
9794
  path: external_exports.array(external_exports.string().min(1)).optional()
9247
- }).passthrough();
9795
+ }).passthrough().refine(
9796
+ (payload) => payload.navigationAction === "goBack" || typeof payload.screenName === "string" && payload.screenName.length > 0,
9797
+ {
9798
+ message: 'screenName is required for every navigationAction except "goBack"',
9799
+ path: ["screenName"]
9800
+ }
9801
+ );
9248
9802
  var formFillFieldSchema = external_exports.object({
9249
9803
  fieldId: external_exports.string().min(1),
9250
9804
  fieldType: external_exports.enum(["text", "select", "toggle", "date", "number", "custom"]).default("text").optional(),
@@ -9578,6 +10132,8 @@ var sandboxObservationSchema = external_exports.object({
9578
10132
  inputs: external_exports.array(
9579
10133
  external_exports.object({
9580
10134
  id: external_exports.string().max(120),
10135
+ provenance: identityProvenanceSchema.optional(),
10136
+ onScreen: external_exports.boolean().optional(),
9581
10137
  label: external_exports.string().max(200).optional(),
9582
10138
  value: external_exports.string().max(2e3).optional(),
9583
10139
  disabled: external_exports.boolean().optional(),
@@ -9591,6 +10147,8 @@ var sandboxObservationSchema = external_exports.object({
9591
10147
  buttons: external_exports.array(
9592
10148
  external_exports.object({
9593
10149
  id: external_exports.string().max(120),
10150
+ provenance: identityProvenanceSchema.optional(),
10151
+ onScreen: external_exports.boolean().optional(),
9594
10152
  label: external_exports.string().max(200).optional(),
9595
10153
  disabled: external_exports.boolean().optional(),
9596
10154
  /** Risk metadata (e.g. "high") the SDK forwards for danger UI. */
@@ -9612,6 +10170,8 @@ var sandboxObservationSchema = external_exports.object({
9612
10170
  sliders: external_exports.array(
9613
10171
  external_exports.object({
9614
10172
  id: external_exports.string().max(120),
10173
+ provenance: identityProvenanceSchema.optional(),
10174
+ onScreen: external_exports.boolean().optional(),
9615
10175
  label: external_exports.string().max(200).optional(),
9616
10176
  value: external_exports.number().finite().optional(),
9617
10177
  min: external_exports.number().finite().optional(),
@@ -9641,6 +10201,44 @@ var sandboxObservationSchema = external_exports.object({
9641
10201
  ).max(200).optional()
9642
10202
  }).passthrough()
9643
10203
  ).max(20).optional(),
10204
+ /**
10205
+ * Toggles exactly as the SDK snapshot reports them (#390).
10206
+ *
10207
+ * Their absence is why `generality-component-control-smarthome`
10208
+ * came back `errored` with a 422 instead of measuring anything:
10209
+ * this schema is `.strict()`, so a scenario that declares a switch
10210
+ * is rejected before the agent is ever asked. Which means the
10211
+ * corpus could not express the single control behind failure 02 of
10212
+ * the functional audit — "disable an agent; the switch never
10213
+ * moves". The one defect the eval most needed to catch was the one
10214
+ * shape it could not describe.
10215
+ */
10216
+ toggles: external_exports.array(
10217
+ external_exports.object({
10218
+ id: external_exports.string().max(120),
10219
+ provenance: identityProvenanceSchema.optional(),
10220
+ onScreen: external_exports.boolean().optional(),
10221
+ label: external_exports.string().max(200).optional(),
10222
+ value: external_exports.boolean().optional(),
10223
+ disabled: external_exports.boolean().optional()
10224
+ })
10225
+ ).max(100).optional(),
10226
+ /**
10227
+ * The interaction graph — the `el:*` ids the prompt tells the model
10228
+ * to prefer over ordinal handles. Passthrough per entry for the
10229
+ * same reason `lists` is: the SDK adds fields faster than this
10230
+ * schema should chase them.
10231
+ */
10232
+ elements: external_exports.array(
10233
+ external_exports.object({
10234
+ id: external_exports.string().max(160),
10235
+ role: external_exports.string().max(40).optional(),
10236
+ label: external_exports.string().max(300).optional()
10237
+ }).passthrough()
10238
+ ).max(500).optional(),
10239
+ /** Screen-level state the relay renders and branches on. */
10240
+ loading: external_exports.boolean().optional(),
10241
+ modalOpen: external_exports.boolean().optional(),
9644
10242
  extra: external_exports.record(external_exports.unknown()).optional()
9645
10243
  }).strict();
9646
10244
  var sandboxRunRequestSchema = external_exports.object({
@@ -9681,13 +10279,29 @@ var sandboxRunRequestSchema = external_exports.object({
9681
10279
  * heuristics, forces tool choice when the last user message carries a
9682
10280
  * failure marker). Only meaningful alongside `history`.
9683
10281
  */
9684
- isRecoveryHop: external_exports.boolean().optional()
10282
+ isRecoveryHop: external_exports.boolean().optional(),
10283
+ /**
10284
+ * Run this call against the application structure supplied here
10285
+ * instead of the project's active MCP document.
10286
+ *
10287
+ * Sandbox-only, and the relay honours it only on a `dryRun` call —
10288
+ * see `resolveMcpOverride` in apps/api. It exists because the eval
10289
+ * corpus could otherwise only ever describe ONE app: every scenario
10290
+ * resolves to the same seeded project, so every navigate destination,
10291
+ * every screen title and every bit of domain priming in the prompt
10292
+ * came from the same vehicle-management demo. A suite that can only
10293
+ * pose questions about one app cannot measure whether the agent works
10294
+ * on another one.
10295
+ *
10296
+ * Deliberately loose (`z.record`): the MCP document's shape is owned
10297
+ * by the generator, and pinning it here would turn every additive
10298
+ * generator change into a 422 in the sandbox.
10299
+ */
10300
+ applicationStructure: external_exports.record(external_exports.unknown()).optional()
9685
10301
  }).strict();
9686
10302
  var relayDiagnosticsSchema = external_exports.object({
9687
10303
  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
- ),
10304
+ guardsFired: external_exports.array(external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })),
9691
10305
  repairMode: external_exports.enum(["repair", "strict", "disabled"]),
9692
10306
  promptVersion: external_exports.string(),
9693
10307
  // Defaulted for producers that predate the field: before it existed
@@ -9946,6 +10560,30 @@ var identifyUserSchema = external_exports.object({
9946
10560
  name: external_exports.string().min(1).max(200).optional(),
9947
10561
  identifiers: endUserIdentifiersSchema.optional()
9948
10562
  });
10563
+ var MAX_EXTERNAL_USER_ID_LENGTH = 128;
10564
+ function normalizeExternalUserId(value) {
10565
+ let raw;
10566
+ if (typeof value === "string") raw = value;
10567
+ else if (typeof value === "number" && Number.isFinite(value)) raw = String(value);
10568
+ else return void 0;
10569
+ const trimmed = raw.trim();
10570
+ if (!trimmed) return void 0;
10571
+ if (trimmed.length > MAX_EXTERNAL_USER_ID_LENGTH) return void 0;
10572
+ return trimmed;
10573
+ }
10574
+ var MAX_DEVICE_INFO_BYTES = 4096;
10575
+ var sessionDeviceInfoSchema = external_exports.object({
10576
+ platform: external_exports.string().max(64).optional(),
10577
+ osVersion: external_exports.string().max(64).optional(),
10578
+ appVersion: external_exports.string().max(64).optional(),
10579
+ sdkVersion: external_exports.string().max(64).optional()
10580
+ }).passthrough().refine((info2) => JSON.stringify(info2).length <= MAX_DEVICE_INFO_BYTES, {
10581
+ message: `device_info exceeds ${MAX_DEVICE_INFO_BYTES} bytes`
10582
+ });
10583
+ var createAgentSessionSchema = external_exports.object({
10584
+ userId: external_exports.preprocess(normalizeExternalUserId, external_exports.string().optional()),
10585
+ deviceInfo: sessionDeviceInfoSchema.optional().catch(void 0)
10586
+ });
9949
10587
  var upsertEndUserSchema = external_exports.object({
9950
10588
  projectId: external_exports.string().min(1).max(64),
9951
10589
  externalId: external_exports.string().min(1).max(128),
@@ -9969,14 +10607,16 @@ async function loadManifest(rootDir, manifestPath) {
9969
10607
  if (error2?.code === "ENOENT") {
9970
10608
  return { manifest: null, resolvedPath };
9971
10609
  }
9972
- throw new Error(`Failed to read Appilots manifest at ${resolvedPath}: ${error2?.message ?? String(error2)}`);
10610
+ throw new Error(
10611
+ `Failed to read Appilots manifest at ${resolvedPath}: ${stringifyUnknown(error2)}`
10612
+ );
9973
10613
  }
9974
10614
  let parsedJson;
9975
10615
  try {
9976
10616
  parsedJson = JSON.parse(raw);
9977
10617
  } catch (error2) {
9978
10618
  throw new Error(
9979
- `Appilots manifest at ${resolvedPath} is not valid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
10619
+ `Appilots manifest at ${resolvedPath} is not valid JSON: ${stringifyUnknown(error2)}`
9980
10620
  );
9981
10621
  }
9982
10622
  const result = manifestSchema.safeParse(parsedJson);
@@ -9990,11 +10630,47 @@ Each entry in "screens" must match the ScreenDescriptor shape (see docs/platform
9990
10630
  }
9991
10631
  return { manifest: result.data, resolvedPath };
9992
10632
  }
10633
+ function mergeScreen(base, declared) {
10634
+ const fields = { ...base };
10635
+ for (const [key, value] of Object.entries(declared)) {
10636
+ if (value === void 0) continue;
10637
+ if (Array.isArray(value) && value.length === 0) continue;
10638
+ fields[key] = value;
10639
+ }
10640
+ const merged = fields;
10641
+ merged.targets = mergeById(base.targets, markManifestOrigin(declared.targets));
10642
+ merged.actions = mergeById(base.actions, declared.actions) ?? [];
10643
+ if (base.agentHints || declared.agentHints) {
10644
+ merged.agentHints = { ...base.agentHints, ...declared.agentHints };
10645
+ }
10646
+ return merged;
10647
+ }
10648
+ function mergeById(base, overlay) {
10649
+ if (!overlay || overlay.length === 0) return base;
10650
+ if (!base || base.length === 0) return overlay;
10651
+ const byId = /* @__PURE__ */ new Map();
10652
+ for (const entry of base) byId.set(entry.id, entry);
10653
+ for (const entry of overlay) {
10654
+ const existing = byId.get(entry.id);
10655
+ byId.set(entry.id, existing ? { ...existing, ...entry } : entry);
10656
+ }
10657
+ return Array.from(byId.values());
10658
+ }
10659
+ function markManifestOrigin(targets) {
10660
+ if (!targets || targets.length === 0) return targets;
10661
+ return targets.map((target) => ({ origin: "manifest", ...target }));
10662
+ }
9993
10663
  function mergeManifestScreens(analyzerScreens, manifestScreens) {
9994
10664
  if (!manifestScreens || manifestScreens.length === 0) return analyzerScreens;
9995
10665
  const byName = /* @__PURE__ */ new Map();
9996
10666
  for (const screen of analyzerScreens) byName.set(screen.name, screen);
9997
- for (const screen of manifestScreens) byName.set(screen.name, screen);
10667
+ for (const declared of manifestScreens) {
10668
+ const base = byName.get(declared.name);
10669
+ byName.set(
10670
+ declared.name,
10671
+ base ? mergeScreen(base, declared) : { ...declared, targets: markManifestOrigin(declared.targets) }
10672
+ );
10673
+ }
9998
10674
  return Array.from(byName.values());
9999
10675
  }
10000
10676
  function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
@@ -10069,7 +10745,12 @@ var MCPGenerator = class _MCPGenerator {
10069
10745
  }
10070
10746
  const mergedScreens = mergeManifestScreens(analyzed.screens, manifest?.screens);
10071
10747
  const navigation = mergeManifestNavigation(analyzed.navigation, manifest?.navigation);
10072
- const agentReadyScreens = mergedScreens.map(enrichScreenForAgent);
10748
+ const agentReadyScreens = mergedScreens.map(
10749
+ (screen) => enrichScreenForAgent({
10750
+ ...screen,
10751
+ filePath: screen.filePath ? import_node_path3.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
10752
+ })
10753
+ );
10073
10754
  const projectInfo = await this.getProjectInfo();
10074
10755
  const projectName = projectInfo.name;
10075
10756
  console.log(`[MCPGenerator] Project name: ${projectName}`);
@@ -10092,10 +10773,7 @@ var MCPGenerator = class _MCPGenerator {
10092
10773
  };
10093
10774
  const serialized = JSON.stringify(document, null, 2);
10094
10775
  const checksum = this.calculateChecksum(serialized);
10095
- const filePath = import_node_path3.default.resolve(
10096
- outputDir,
10097
- `mcp-document.${this.options.format}`
10098
- );
10776
+ const filePath = import_node_path3.default.resolve(outputDir, `mcp-document.${this.options.format}`);
10099
10777
  await (0, import_promises4.writeFile)(filePath, serialized, "utf-8");
10100
10778
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
10101
10779
  const checksumFilePath = import_node_path3.default.resolve(outputDir, ".appilots-checksum");
@@ -10211,14 +10889,136 @@ function reportMetadataLint(document, strict) {
10211
10889
  }
10212
10890
  }
10213
10891
 
10214
- // src/cli/commands/sync.ts
10215
- 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) => {
10217
- if (options.json) setLogMode("json");
10218
- else if (options.quiet) setLogMode("quiet");
10219
- try {
10892
+ // src/cli/utils/remote-drift.ts
10893
+ var import_node_fs = require("fs");
10894
+ var import_node_path4 = require("path");
10895
+ var STUB_RATIO = 0.5;
10896
+ function compareWithRemote(local, remote) {
10897
+ if (remote.screensCount === local.screens) return null;
10898
+ return {
10899
+ localScreens: local.screens,
10900
+ remoteScreens: remote.screensCount,
10901
+ remoteId: remote.id,
10902
+ severe: local.screens > 0 && remote.screensCount <= local.screens * STUB_RATIO
10903
+ };
10904
+ }
10905
+ function readLocalDocumentSummary(outputDir) {
10906
+ try {
10907
+ const raw = (0, import_node_fs.readFileSync)((0, import_node_path4.join)(outputDir, "mcp-document.json"), "utf-8");
10908
+ const parsed = JSON.parse(raw);
10909
+ if (typeof parsed?.metadata?.totalScreens !== "number") return null;
10910
+ return { screens: parsed.metadata.totalScreens };
10911
+ } catch {
10912
+ return null;
10913
+ }
10914
+ }
10915
+ function reportRemoteDrift(drift, options = {}) {
10916
+ if (!drift.severe) {
10917
+ info(
10918
+ `Server's active document differs from what was just generated (active: ${drift.remoteId}, ${drift.remoteScreens} screens).`
10919
+ );
10920
+ return;
10921
+ }
10922
+ warn(
10923
+ `The agent is ${options.uploadFailed ? "still " : ""}running on a ${drift.remoteScreens}-screen document while this app has ${drift.localScreens} screens.`
10924
+ );
10925
+ info(` Active on server: ${drift.remoteId} (${drift.remoteScreens} screens)`);
10926
+ info(` Just generated: ${drift.localScreens} screens`);
10927
+ info("");
10928
+ info("While a document that small is active the agent cannot see the other");
10929
+ info("screens, and missions that need them fail in ways that read as model");
10930
+ info("errors. The usual causes: an older version activated from the");
10931
+ info("dashboard, a sync that went to a different server, or a database");
10932
+ info("seeded before the seed learned to refuse partial documents.");
10933
+ info("");
10934
+ if (options.uploadFailed) {
10935
+ info("Nothing was uploaded just now, so this is unchanged. Fix the error");
10936
+ info("above and run sync again \u2014 e2e runs before that will fail on");
10937
+ info("navigation, not on anything the model did.");
10938
+ } else {
10939
+ info("Uploading now replaces it.");
10940
+ }
10941
+ }
10942
+
10943
+ // src/cli/utils/upload-failure.ts
10944
+ function explainUploadFailure(failure, serverUrl) {
10945
+ const status = failure.errorStatus ?? 0;
10946
+ if (status === 401) {
10947
+ info("");
10948
+ info("`sync` authenticates with the project API key (`ak_\u2026`) \u2014 the same key");
10949
+ info("the SDK uses. Check that it is the key for THIS project, is still");
10950
+ info("active, and reaches the server you meant:");
10951
+ info(` Server: ${serverUrl ?? "(default)"}`);
10952
+ info("Set it in .appilotsrc (`apiKey`), via APPILOTS_API_KEY, or --api-key.");
10953
+ info("");
10954
+ info("A dashboard login token will not work here and is not needed: the CLI");
10955
+ info("talks to /cli/sync, not the dashboard's /mcp-documents.");
10956
+ return;
10957
+ }
10958
+ if (failure.errorCode === "WORKSPACE_INACTIVE") {
10959
+ info("");
10960
+ info("The workspace is deactivated. Re-activate it in the dashboard");
10961
+ info("(Settings \u2192 Workspace) and run sync again.");
10962
+ return;
10963
+ }
10964
+ if (status === 404) {
10965
+ info("");
10966
+ info(`No /cli/sync at ${serverUrl ?? "(default)"} \u2014 check serverUrl points at`);
10967
+ info("an Appilots API (and one recent enough to serve the CLI routes).");
10968
+ return;
10969
+ }
10970
+ if (status === 422) {
10971
+ info("");
10972
+ info("The server rejected the document itself. Inspect what was generated:");
10973
+ info(" .appilots/mcp-document.json");
10974
+ return;
10975
+ }
10976
+ if (status >= 500) {
10977
+ info("");
10978
+ info("The server failed to process the upload. It was retried already \u2014");
10979
+ info("if it persists, the API logs hold the matching request id.");
10980
+ }
10981
+ }
10982
+
10983
+ // src/cli/utils/empty-document.ts
10984
+ function assertDocumentHasScreens(document, options = {}) {
10985
+ if ((document.metadata?.totalScreens ?? 0) > 0) return;
10986
+ if (options.allowEmpty) {
10987
+ info("No screens found, continuing because --allow-empty was passed.");
10988
+ return;
10989
+ }
10990
+ error("No screens found \u2014 this application map would give the agent nothing to navigate.");
10991
+ info("");
10992
+ info("The analyzer looks for React Navigation: a <NavigationContainer> and");
10993
+ info("<Stack.Screen> / <Tab.Screen> declarations. It found none.");
10994
+ info("");
10995
+ info("The two usual causes:");
10996
+ info(" \u2022 The app uses Expo Router (or another file-based router). It is the");
10997
+ info(" default for `npx create-expo-app` and the analyzer cannot read it.");
10998
+ info(" \u2022 The screens live outside the analyzed paths \u2014 check `include`,");
10999
+ info(" `exclude` and `screenPatterns` in your Appilots config.");
11000
+ info("");
11001
+ info("The escape hatch, which works for any router: declare your screens in");
11002
+ info("an `appilots.manifest.json` at the project root. Its screens are merged");
11003
+ info("on top of whatever the analyzer found, so a fully declared app needs no");
11004
+ info("React Navigation at all. See the CLI docs for the shape.");
11005
+ info("");
11006
+ info("If an empty map really is what you want, pass --allow-empty.");
11007
+ process.exit(1);
11008
+ }
11009
+
11010
+ // src/cli/commands/sync.ts
11011
+ function syncCommand() {
11012
+ 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(
11013
+ "--strict-metadata",
11014
+ "Fail when mutating actions are missing effect/riskLevel metadata",
11015
+ false
11016
+ ).option("--allow-empty", "Do not fail when the application map has zero screens", false).action(async (options) => {
11017
+ if (options.json) setLogMode("json");
11018
+ else if (options.quiet) setLogMode("quiet");
11019
+ try {
10220
11020
  banner();
10221
- const loaded = loadConfig();
11021
+ const loaded = loadConfig(warn);
10222
11022
  const apiKey = options.apiKey || loaded?.apiKey;
10223
11023
  if (!apiKey) {
10224
11024
  error("No .appilotsrc configuration found");
@@ -10229,7 +11029,7 @@ function syncCommand() {
10229
11029
  return;
10230
11030
  }
10231
11031
  const config = {
10232
- serverUrl: "http://localhost:4000",
11032
+ serverUrl: DEFAULT_SERVER_URL,
10233
11033
  outputDir: ".appilots",
10234
11034
  ...loaded,
10235
11035
  apiKey,
@@ -10243,7 +11043,7 @@ function syncCommand() {
10243
11043
  const outputDir = config.outputDir || ".appilots";
10244
11044
  let previousChecksum = "";
10245
11045
  try {
10246
- const checksumPath = (0, import_node_path4.join)(outputDir, ".appilots-checksum");
11046
+ const checksumPath = (0, import_node_path5.join)(outputDir, ".appilots-checksum");
10247
11047
  previousChecksum = (await (0, import_promises5.readFile)(checksumPath, "utf-8")).trim();
10248
11048
  } catch {
10249
11049
  }
@@ -10264,22 +11064,32 @@ function syncCommand() {
10264
11064
  });
10265
11065
  const output = await generator.generate();
10266
11066
  spinner.stop();
11067
+ assertDocumentHasScreens(output.document, {
11068
+ rootDir: process.cwd(),
11069
+ allowEmpty: Boolean(options.allowEmpty)
11070
+ });
10267
11071
  success(
10268
11072
  `Found ${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
10269
11073
  );
10270
11074
  reportMetadataLint(output.document, Boolean(options.strictMetadata));
11075
+ spinner = createSpinner("Checking remote status...");
11076
+ spinner.start();
11077
+ const remote = await apiClient.status();
11078
+ spinner.stop();
11079
+ const remoteActive = remote.error ? null : remote.activeMcp ?? null;
11080
+ const drift = remoteActive ? compareWithRemote({ screens: output.document.metadata.totalScreens }, remoteActive) : null;
10271
11081
  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}`);
11082
+ if (remote.error) {
11083
+ warn(`Could not verify remote: ${remote.error}`);
11084
+ info("Uploading anyway...");
11085
+ } else if (remoteActive) {
11086
+ if (!drift) {
11087
+ success("Already up to date (no local or remote changes)");
11088
+ jsonOutput({ success: true, unchanged: true, checksum: output.checksum });
11089
+ return;
11090
+ }
11091
+ reportRemoteDrift(drift);
10278
11092
  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
11093
  }
10284
11094
  }
10285
11095
  spinner = createSpinner("Uploading to Appilots...");
@@ -10292,13 +11102,33 @@ function syncCommand() {
10292
11102
  spinner.stop();
10293
11103
  if (!syncResult.success) {
10294
11104
  error(`Sync failed: ${syncResult.error}`);
10295
- jsonOutput({ success: false, error: syncResult.error });
11105
+ hintUnreachableServer(config.serverUrl, syncResult.error);
11106
+ explainUploadFailure(syncResult, config.serverUrl);
11107
+ if (drift?.severe) {
11108
+ reportRemoteDrift(drift, { uploadFailed: true });
11109
+ }
11110
+ jsonOutput({
11111
+ success: false,
11112
+ error: syncResult.error,
11113
+ errorCode: syncResult.errorCode,
11114
+ errorStatus: syncResult.errorStatus,
11115
+ activeDocument: remoteActive,
11116
+ drift
11117
+ });
10296
11118
  process.exit(1);
10297
11119
  }
10298
11120
  if (syncResult.unchanged && !options.force) {
10299
11121
  success("Already up to date on server");
11122
+ if (syncResult.activated) {
11123
+ info(`Re-activated document ${syncResult.id} (it was stored but not active).`);
11124
+ }
10300
11125
  } else {
10301
11126
  success("Successfully synced MCP document");
11127
+ if (drift?.severe) {
11128
+ info(
11129
+ `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.`
11130
+ );
11131
+ }
10302
11132
  info(`Document ID: ${syncResult.id}`);
10303
11133
  info(`Checksum: ${syncResult.checksum}`);
10304
11134
  if (config.projectId) {
@@ -10328,8 +11158,8 @@ function syncCommand() {
10328
11158
  actionsCount: syncResult.actionsCount ?? output.document.metadata.totalActions
10329
11159
  });
10330
11160
  } 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) });
11161
+ error(`Sync failed: ${stringifyUnknown(err)}`);
11162
+ jsonOutput({ success: false, error: stringifyUnknown(err) });
10333
11163
  if (options.verbose) {
10334
11164
  console.error(err);
10335
11165
  }
@@ -10340,12 +11170,12 @@ function syncCommand() {
10340
11170
 
10341
11171
  // src/cli/commands/watch.ts
10342
11172
  var import_commander3 = require("commander");
10343
- var import_node_fs = require("fs");
11173
+ var import_node_fs2 = require("fs");
10344
11174
  function watchCommand() {
10345
11175
  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
11176
  try {
10347
11177
  banner();
10348
- const config = loadConfig();
11178
+ const config = loadConfig(warn);
10349
11179
  if (!config) {
10350
11180
  error("No .appilotsrc configuration found");
10351
11181
  info("Run: appilots init --api-key <your-key>");
@@ -10374,7 +11204,10 @@ function watchCommand() {
10374
11204
  };
10375
11205
  const generator = new MCPGenerator(generatorConfig);
10376
11206
  const initialOutput = await generator.generate();
10377
- const initialSync = await apiClient.sync(initialOutput.document, initialOutput.document.version);
11207
+ const initialSync = await apiClient.sync(
11208
+ initialOutput.document,
11209
+ initialOutput.document.version
11210
+ );
10378
11211
  if (!initialSync.success) {
10379
11212
  error(`Initial sync failed: ${initialSync.error}`);
10380
11213
  process.exit(1);
@@ -10384,45 +11217,41 @@ function watchCommand() {
10384
11217
  dim("Press Ctrl+C to stop\n");
10385
11218
  let debounceTimer = null;
10386
11219
  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);
11220
+ const watcher = (0, import_node_fs2.watch)(process.cwd(), { recursive: true }, async (eventType, filename) => {
11221
+ if (!filename) return;
11222
+ if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
11223
+ return;
10424
11224
  }
10425
- );
11225
+ if (debounceTimer) {
11226
+ clearTimeout(debounceTimer);
11227
+ }
11228
+ debounceTimer = setTimeout(async () => {
11229
+ try {
11230
+ if (options.verbose) {
11231
+ info(`Change detected: ${filename}`);
11232
+ }
11233
+ const spinner = createSpinner("Re-analyzing project...");
11234
+ spinner.start();
11235
+ const newGenerator = new MCPGenerator(generatorConfig);
11236
+ const output = await newGenerator.generate();
11237
+ spinner.text = "Uploading changes...";
11238
+ const syncResult = await apiClient.sync(output.document, output.document.version);
11239
+ spinner.stop();
11240
+ if (!syncResult.success) {
11241
+ error(`Sync failed: ${syncResult.error}`);
11242
+ } else if (syncResult.unchanged) {
11243
+ info("Changes synced (no content changes detected)");
11244
+ } else {
11245
+ success("Changes synced successfully");
11246
+ info(
11247
+ `${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
11248
+ );
11249
+ }
11250
+ } catch (err) {
11251
+ error(`Watch sync failed: ${stringifyUnknown(err)}`);
11252
+ }
11253
+ }, DEBOUNCE_MS);
11254
+ });
10426
11255
  process.on("SIGINT", () => {
10427
11256
  info("Stopping watcher...");
10428
11257
  watcher.close();
@@ -10432,7 +11261,7 @@ function watchCommand() {
10432
11261
  process.exit(0);
10433
11262
  });
10434
11263
  } catch (err) {
10435
- error(`Watch failed: ${err instanceof Error ? err.message : String(err)}`);
11264
+ error(`Watch failed: ${stringifyUnknown(err)}`);
10436
11265
  process.exit(1);
10437
11266
  }
10438
11267
  });
@@ -10441,7 +11270,11 @@ function watchCommand() {
10441
11270
  // src/cli/commands/generate.ts
10442
11271
  var import_commander4 = require("commander");
10443
11272
  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) => {
11273
+ 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(
11274
+ "--strict-metadata",
11275
+ "Fail when mutating actions are missing effect/riskLevel metadata",
11276
+ false
11277
+ ).option("--allow-empty", "Do not fail when the application map has zero screens", false).action(async (options) => {
10445
11278
  try {
10446
11279
  banner();
10447
11280
  if (options.format && options.format !== "json") {
@@ -10452,7 +11285,7 @@ function generateCommand() {
10452
11285
  spinner.start();
10453
11286
  let loaded = null;
10454
11287
  try {
10455
- loaded = loadConfig();
11288
+ loaded = loadConfig(warn);
10456
11289
  } catch {
10457
11290
  loaded = null;
10458
11291
  }
@@ -10465,6 +11298,10 @@ function generateCommand() {
10465
11298
  });
10466
11299
  const output = await generator.generate();
10467
11300
  spinner.stop();
11301
+ assertDocumentHasScreens(output.document, {
11302
+ rootDir: process.cwd(),
11303
+ allowEmpty: Boolean(options.allowEmpty)
11304
+ });
10468
11305
  success(
10469
11306
  `Generated MCP document with ${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
10470
11307
  );
@@ -10472,21 +11309,761 @@ function generateCommand() {
10472
11309
  info(`Checksum: ${output.checksum}`);
10473
11310
  reportMetadataLint(output.document, Boolean(options.strictMetadata));
10474
11311
  } catch (err) {
10475
- error(`Generation failed: ${err instanceof Error ? err.message : String(err)}`);
11312
+ error(`Generation failed: ${stringifyUnknown(err)}`);
10476
11313
  process.exit(1);
10477
11314
  }
10478
11315
  });
10479
11316
  }
10480
11317
 
10481
- // src/cli/commands/status.ts
11318
+ // src/cli/commands/annotate.ts
11319
+ var import_promises6 = require("fs/promises");
11320
+ var import_node_path7 = __toESM(require("path"));
10482
11321
  var import_commander5 = require("commander");
11322
+ var import_fast_glob6 = __toESM(require("fast-glob"));
11323
+
11324
+ // src/annotate/sites.ts
11325
+ var import_traverse12 = __toESM(require("@babel/traverse"));
11326
+ var t13 = __toESM(require("@babel/types"));
11327
+
11328
+ // src/annotate/forwarding.ts
11329
+ var import_node_path6 = __toESM(require("path"));
11330
+ var import_node_fs3 = require("fs");
11331
+ var import_traverse11 = __toESM(require("@babel/traverse"));
11332
+ var t12 = __toESM(require("@babel/types"));
11333
+ var traverse11 = import_traverse11.default.default ?? import_traverse11.default;
11334
+ var RN_TESTID_CARRIERS = /* @__PURE__ */ new Set([
11335
+ "View",
11336
+ "Text",
11337
+ "TextInput",
11338
+ "Image",
11339
+ "ImageBackground",
11340
+ "ScrollView",
11341
+ "FlatList",
11342
+ "SectionList",
11343
+ "VirtualizedList",
11344
+ "TouchableOpacity",
11345
+ "TouchableHighlight",
11346
+ "TouchableWithoutFeedback",
11347
+ "TouchableNativeFeedback",
11348
+ "Pressable",
11349
+ "Button",
11350
+ "Switch",
11351
+ "Modal",
11352
+ "RefreshControl",
11353
+ "ActivityIndicator",
11354
+ "SafeAreaView",
11355
+ "KeyboardAvoidingView",
11356
+ "Slider"
11357
+ ]);
11358
+ function collectImports(ast) {
11359
+ const out = /* @__PURE__ */ new Map();
11360
+ traverse11(ast, {
11361
+ ImportDeclaration(nodePath) {
11362
+ const source = nodePath.node.source.value;
11363
+ for (const spec of nodePath.node.specifiers) {
11364
+ if (t12.isImportSpecifier(spec)) {
11365
+ const imported = t12.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
11366
+ out.set(spec.local.name, { source, local: spec.local.name, imported });
11367
+ } else if (t12.isImportDefaultSpecifier(spec)) {
11368
+ out.set(spec.local.name, { source, local: spec.local.name, imported: "default" });
11369
+ }
11370
+ }
11371
+ }
11372
+ });
11373
+ return out;
11374
+ }
11375
+ var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
11376
+ function resolveRelativeImport(fromFile, source) {
11377
+ if (!source.startsWith(".")) return void 0;
11378
+ const base = import_node_path6.default.resolve(import_node_path6.default.dirname(fromFile), source);
11379
+ for (const ext of EXTENSIONS) {
11380
+ const candidate = `${base}${ext}`;
11381
+ if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
11382
+ }
11383
+ for (const ext of EXTENSIONS) {
11384
+ const candidate = import_node_path6.default.join(base, `index${ext}`);
11385
+ if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
11386
+ }
11387
+ return void 0;
11388
+ }
11389
+ var IDENTITY_HOOKS = /* @__PURE__ */ new Set([
11390
+ "useAppilotsTarget",
11391
+ "useAppilotsField",
11392
+ "useAppilotsToggle",
11393
+ "useAppilotsSlider"
11394
+ ]);
11395
+ function inspectModule(file2, exportName, depth = 0) {
11396
+ const none = { forwards: false, derives: false };
11397
+ if (depth > 2) return none;
11398
+ let source;
11399
+ try {
11400
+ source = (0, import_node_fs3.readFileSync)(file2, "utf-8");
11401
+ } catch {
11402
+ return none;
11403
+ }
11404
+ const mentionsIdentity = source.includes("testID") || source.includes("accessibilityLabel") || source.includes("useAppilots") || /\{\s*\.\.\./.test(source);
11405
+ if (!mentionsIdentity && !/export\s*\{[^}]*\}\s*from/.test(source)) return none;
11406
+ let ast;
11407
+ try {
11408
+ ast = parseSource(source);
11409
+ } catch {
11410
+ return none;
11411
+ }
11412
+ let forwards = false;
11413
+ let derives = false;
11414
+ let barrelTarget;
11415
+ traverse11(ast, {
11416
+ // `export { Card } from './Card'` — follow one hop.
11417
+ ExportNamedDeclaration(nodePath) {
11418
+ const from = nodePath.node.source?.value;
11419
+ if (!from) return;
11420
+ const named = nodePath.node.specifiers.some(
11421
+ (spec) => t12.isExportSpecifier(spec) && (t12.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === exportName
11422
+ );
11423
+ if (named) barrelTarget = from;
11424
+ },
11425
+ ExportAllDeclaration(nodePath) {
11426
+ barrelTarget ??= nodePath.node.source.value;
11427
+ },
11428
+ // `interface CardProps { testID?: string }` / `type Props = { testID… }`
11429
+ TSPropertySignature(nodePath) {
11430
+ if (t12.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
11431
+ },
11432
+ // `function Card({ testID }) {}` — the destructured parameter.
11433
+ ObjectProperty(nodePath) {
11434
+ if (t12.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
11435
+ },
11436
+ // `<View {...props} />` — anything spread onto JSX carries it along.
11437
+ JSXSpreadAttribute() {
11438
+ forwards = true;
11439
+ },
11440
+ // `testID={`toggle-${id}`}` / `accessibilityLabel={label}` — the
11441
+ // component naming itself from a prop. A STRING literal does not count:
11442
+ // that is one hardcoded name shared by every instance, which is the
11443
+ // duplicate-id problem rather than a solution to it.
11444
+ JSXAttribute(nodePath) {
11445
+ const name = t12.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
11446
+ if (name !== "testID" && name !== "accessibilityLabel") return;
11447
+ const value = nodePath.node.value;
11448
+ if (!t12.isJSXExpressionContainer(value)) return;
11449
+ if (t12.isStringLiteral(value.expression)) return;
11450
+ derives = true;
11451
+ },
11452
+ // `useAppilotsTarget(`${groupId}-${option.value}`, …)`
11453
+ CallExpression(nodePath) {
11454
+ const callee = nodePath.node.callee;
11455
+ if (t12.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
11456
+ }
11457
+ });
11458
+ if (forwards || derives) return { forwards, derives };
11459
+ if (barrelTarget) {
11460
+ const next = resolveRelativeImport(file2, barrelTarget);
11461
+ if (next) return inspectModule(next, exportName, depth + 1);
11462
+ }
11463
+ return none;
11464
+ }
11465
+ var verdictCache = /* @__PURE__ */ new Map();
11466
+ function canCarryTestId(componentName, fromFile, imports) {
11467
+ const origin = imports.get(componentName);
11468
+ if (origin?.source === "react-native") {
11469
+ return RN_TESTID_CARRIERS.has(origin.imported) ? "rn-primitive" : "unverifiable";
11470
+ }
11471
+ const target = origin ? resolveRelativeImport(fromFile, origin.source) : fromFile;
11472
+ if (!target) return "unverifiable";
11473
+ const exportName = origin?.imported ?? componentName;
11474
+ const key = `${target}#${exportName}`;
11475
+ let identity = verdictCache.get(key);
11476
+ if (identity === void 0) {
11477
+ identity = inspectModule(target, exportName);
11478
+ verdictCache.set(key, identity);
11479
+ }
11480
+ if (identity.forwards) return "forwards";
11481
+ if (identity.derives) return "derives";
11482
+ return "swallows";
11483
+ }
11484
+ function resetForwardingCache() {
11485
+ verdictCache.clear();
11486
+ }
11487
+
11488
+ // src/annotate/sites.ts
11489
+ var traverse12 = import_traverse12.default.default ?? import_traverse12.default;
11490
+ var LABEL_PROPS = ["title", "label", "placeholder"];
11491
+ var IDENTITY_PROPS = ["testID", "appilotsId", "accessibilityLabel"];
11492
+ function kebab(text) {
11493
+ return text.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
11494
+ }
11495
+ function verbOf(name) {
11496
+ return kebab(name.replace(/^(handle|on)(?=[A-Z])/, ""));
11497
+ }
11498
+ function scopeSlugForFile(file2) {
11499
+ const base = file2.replace(/\\/g, "/").split("/").pop() ?? file2;
11500
+ const name = base.replace(/\.[jt]sx?$/, "");
11501
+ if (name.toLowerCase() === "index") {
11502
+ const dir = file2.replace(/\\/g, "/").split("/").slice(-2, -1)[0] ?? "";
11503
+ return kebab(dir);
11504
+ }
11505
+ return kebab(name.replace(/Screen$/, ""));
11506
+ }
11507
+ function collapseRepeats(slug) {
11508
+ const seen = /* @__PURE__ */ new Set();
11509
+ const out = [];
11510
+ for (const token of slug.split("-")) {
11511
+ if (!token || seen.has(token)) continue;
11512
+ seen.add(token);
11513
+ out.push(token);
11514
+ }
11515
+ return out.join("-");
11516
+ }
11517
+ function deriveTestId(site) {
11518
+ const base = [site.scope, kebab(site.component)].filter(Boolean).join("-");
11519
+ if (site.pressProp && site.pressProp !== "onPress") {
11520
+ return collapseRepeats(`${base}-${verbOf(site.pressProp)}`);
11521
+ }
11522
+ if (site.handler) {
11523
+ const verb = verbOf(site.handler);
11524
+ return collapseRepeats(verb ? `${base}-${verb}` : base);
11525
+ }
11526
+ return collapseRepeats(`${base}-${site.ordinal}`);
11527
+ }
11528
+ function hasAnyProp(element, names) {
11529
+ return names.some((name) => findJsxAttribute(element, name) !== void 0);
11530
+ }
11531
+ function hasLabelText(element) {
11532
+ return LABEL_PROPS.some((name) => {
11533
+ const value = getStringAttr(element, name);
11534
+ return typeof value === "string" && value.trim().length > 0;
11535
+ });
11536
+ }
11537
+ function namedHandler(element, prop) {
11538
+ if (!prop) return void 0;
11539
+ const attr = findJsxAttribute(element, prop);
11540
+ if (!attr || !t13.isJSXExpressionContainer(attr.value)) return void 0;
11541
+ return t13.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
11542
+ }
11543
+ var ANNOTATABLE_ROLES = /* @__PURE__ */ new Set(["button", "input", "select", "toggle", "date", "list"]);
11544
+ function isPerItemRender(nodePath) {
11545
+ let current = nodePath.parentPath;
11546
+ while (current) {
11547
+ const node = current.node;
11548
+ if (t13.isCallExpression(node) && t13.isMemberExpression(node.callee) && t13.isIdentifier(node.callee.property) && (node.callee.property.name === "map" || node.callee.property.name === "flatMap")) {
11549
+ return true;
11550
+ }
11551
+ if (t13.isJSXAttribute(node) && t13.isJSXIdentifier(node.name) && /^render[A-Z]|^ListHeaderComponent$|^ListFooterComponent$/.test(node.name.name) && node.name.name !== "renderScrollComponent") {
11552
+ return /^renderItem$|^renderSectionHeader$|^renderSectionFooter$/.test(node.name.name);
11553
+ }
11554
+ current = current.parentPath;
11555
+ }
11556
+ return false;
11557
+ }
11558
+ function findAnnotationSites(source, file2) {
11559
+ let ast;
11560
+ try {
11561
+ ast = parseSource(source);
11562
+ } catch {
11563
+ return [];
11564
+ }
11565
+ const imports = collectImports(ast);
11566
+ const scope = scopeSlugForFile(file2);
11567
+ const sites = [];
11568
+ const perComponent = /* @__PURE__ */ new Map();
11569
+ const usedIds = /* @__PURE__ */ new Set();
11570
+ traverse12(ast, {
11571
+ JSXOpeningElement(nodePath) {
11572
+ for (const prop of IDENTITY_PROPS) {
11573
+ const existing = getStringAttr(nodePath.node, prop);
11574
+ if (existing) usedIds.add(existing);
11575
+ }
11576
+ }
11577
+ });
11578
+ traverse12(ast, {
11579
+ JSXOpeningElement(nodePath) {
11580
+ const element = nodePath.node;
11581
+ if (!t13.isJSXIdentifier(element.name)) return;
11582
+ const component = element.name.name;
11583
+ const role = classifyJsxComponent(component, element);
11584
+ if (!ANNOTATABLE_ROLES.has(role)) return;
11585
+ if (hasAnyProp(element, IDENTITY_PROPS)) return;
11586
+ if (role === "button" && hasLabelText(element)) return;
11587
+ const carrier = canCarryTestId(component, file2, imports);
11588
+ if (carrier === "derives") return;
11589
+ if (isPerItemRender(nodePath)) return;
11590
+ const pressProp = pressHandlerProp(element) ?? void 0;
11591
+ const handler = namedHandler(element, pressProp);
11592
+ const ordinal = (perComponent.get(component) ?? 0) + 1;
11593
+ perComponent.set(component, ordinal);
11594
+ let testId = deriveTestId({ scope, component, pressProp, handler, ordinal });
11595
+ if (usedIds.has(testId)) {
11596
+ let suffix = 2;
11597
+ while (usedIds.has(`${testId}-${suffix}`)) suffix += 1;
11598
+ testId = `${testId}-${suffix}`;
11599
+ }
11600
+ usedIds.add(testId);
11601
+ const line = element.loc?.start.line ?? 0;
11602
+ const nameEndLine = element.name.loc?.end.line ?? line;
11603
+ const firstAttr = element.attributes[0];
11604
+ const firstAttrLoc = firstAttr?.loc?.start;
11605
+ const isMultiline = !!firstAttrLoc && firstAttrLoc.line !== nameEndLine;
11606
+ const insertText = isMultiline ? `
11607
+ ${" ".repeat(firstAttrLoc.column)}testID="${testId}"` : ` testID="${testId}"`;
11608
+ sites.push({
11609
+ file: file2,
11610
+ line,
11611
+ column: element.loc?.start.column ?? 0,
11612
+ component,
11613
+ role,
11614
+ pressProp,
11615
+ handler,
11616
+ testId,
11617
+ carrier,
11618
+ // Just past the element name — before any existing attribute, so a
11619
+ // multi-line element keeps its formatting and the new prop reads
11620
+ // first in the diff.
11621
+ insertAt: element.name.end ?? element.start ?? 0,
11622
+ insertText
11623
+ });
11624
+ }
11625
+ });
11626
+ return sites;
11627
+ }
11628
+
11629
+ // src/annotate/apply.ts
11630
+ var import_magic_string = __toESM(require("magic-string"));
11631
+ function planSite(site) {
11632
+ if (site.carrier === "rn-primitive" || site.carrier === "forwards") {
11633
+ return { ...site, outcome: "annotated" };
11634
+ }
11635
+ if (site.carrier === "swallows") {
11636
+ return {
11637
+ ...site,
11638
+ outcome: "blocked-swallows",
11639
+ 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.`
11640
+ };
11641
+ }
11642
+ return {
11643
+ ...site,
11644
+ outcome: "blocked-unverifiable",
11645
+ 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.`
11646
+ };
11647
+ }
11648
+ function applyToFile(source, sites) {
11649
+ const planned = sites.map(planSite);
11650
+ const written = planned.filter((site) => site.outcome === "annotated");
11651
+ const blocked = planned.filter((site) => site.outcome !== "annotated");
11652
+ if (written.length === 0) {
11653
+ return { file: sites[0]?.file ?? "", content: source, written, blocked };
11654
+ }
11655
+ const magic = new import_magic_string.default(source);
11656
+ for (const site of written) {
11657
+ magic.appendLeft(site.insertAt, site.insertText);
11658
+ }
11659
+ return { file: written[0].file, content: magic.toString(), written, blocked };
11660
+ }
11661
+
11662
+ // src/annotate/forward.ts
11663
+ var import_node_fs4 = require("fs");
11664
+ var import_magic_string2 = __toESM(require("magic-string"));
11665
+ var import_traverse13 = __toESM(require("@babel/traverse"));
11666
+ var t14 = __toESM(require("@babel/types"));
11667
+ var traverse13 = import_traverse13.default.default ?? import_traverse13.default;
11668
+ function findComponent(ast, name) {
11669
+ let found;
11670
+ traverse13(ast, {
11671
+ FunctionDeclaration(nodePath) {
11672
+ if (nodePath.node.id?.name === name) {
11673
+ found ??= { params: nodePath.node.params, body: nodePath.node.body };
11674
+ }
11675
+ },
11676
+ VariableDeclarator(nodePath) {
11677
+ if (!t14.isIdentifier(nodePath.node.id) || nodePath.node.id.name !== name) return;
11678
+ const init = nodePath.node.init;
11679
+ if (t14.isArrowFunctionExpression(init) || t14.isFunctionExpression(init)) {
11680
+ found ??= { params: init.params, body: init.body };
11681
+ }
11682
+ }
11683
+ });
11684
+ return found;
11685
+ }
11686
+ function findReturnedRoots(body) {
11687
+ const roots = [];
11688
+ let unsupported = false;
11689
+ const record = (node) => {
11690
+ if (!node) return;
11691
+ if (t14.isParenthesizedExpression(node)) return record(node.expression);
11692
+ if (t14.isJSXElement(node)) {
11693
+ roots.push(node.openingElement);
11694
+ return;
11695
+ }
11696
+ if (t14.isConditionalExpression(node)) {
11697
+ record(node.consequent);
11698
+ record(node.alternate);
11699
+ return;
11700
+ }
11701
+ if (t14.isJSXFragment(node)) {
11702
+ unsupported = true;
11703
+ return;
11704
+ }
11705
+ if (t14.isNullLiteral(node)) return;
11706
+ unsupported = true;
11707
+ };
11708
+ if (t14.isBlockStatement(body)) {
11709
+ traverse13(
11710
+ t14.file(t14.program([t14.expressionStatement(t14.functionExpression(null, [], body))])),
11711
+ {
11712
+ ReturnStatement(nodePath) {
11713
+ record(nodePath.node.argument);
11714
+ }
11715
+ }
11716
+ );
11717
+ } else {
11718
+ record(body);
11719
+ }
11720
+ if (unsupported || roots.length === 0) return void 0;
11721
+ return roots;
11722
+ }
11723
+ function isCarrierRoot(element, imports) {
11724
+ if (!t14.isJSXIdentifier(element.name)) return false;
11725
+ const name = element.name.name;
11726
+ const origin = imports.get(name);
11727
+ if (origin?.source !== "react-native") return false;
11728
+ return RN_TESTID_CARRIERS.has(origin.imported);
11729
+ }
11730
+ function hasProp(element, name) {
11731
+ return element.attributes.some(
11732
+ (attr) => t14.isJSXAttribute(attr) && t14.isJSXIdentifier(attr.name) && attr.name.name === name
11733
+ );
11734
+ }
11735
+ function indentOf(source, offset) {
11736
+ const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
11737
+ const match = /^[ \t]*/.exec(source.slice(lineStart, offset));
11738
+ return match?.[0] ?? "";
11739
+ }
11740
+ function insertAfterLastMember(source, members, text, closingBrace, separator) {
11741
+ const last = members[members.length - 1];
11742
+ if (!last || typeof last.end !== "number" || typeof last.start !== "number") {
11743
+ return { at: closingBrace, text };
11744
+ }
11745
+ const trailing = /^\s*[,;]/.exec(source.slice(last.end, closingBrace));
11746
+ const at = last.end + (trailing?.[0].length ?? 0);
11747
+ const endsWithSeparator = source.slice(0, last.end).trimEnd().endsWith(separator);
11748
+ const lead = trailing || endsWithSeparator ? "" : separator;
11749
+ const openLine = source.lastIndexOf("\n", last.start);
11750
+ const multiline = openLine !== -1 && openLine > source.lastIndexOf("{", last.start);
11751
+ if (!multiline) return { at, text: `${lead} ${text}` };
11752
+ const indent = indentOf(source, last.start);
11753
+ return { at, text: `${lead}
11754
+ ${indent}${text}` };
11755
+ }
11756
+ function followBarrel(file2, ast, component) {
11757
+ let target;
11758
+ traverse13(ast, {
11759
+ ExportNamedDeclaration(nodePath) {
11760
+ const from = nodePath.node.source?.value;
11761
+ if (!from) return;
11762
+ const named = nodePath.node.specifiers.some(
11763
+ (spec) => t14.isExportSpecifier(spec) && (t14.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === component
11764
+ );
11765
+ if (named) target ??= from;
11766
+ }
11767
+ });
11768
+ return target ? resolveRelativeImport(file2, target) : void 0;
11769
+ }
11770
+ function planForward(file2, component, depth = 0) {
11771
+ const base = { file: file2, component, edits: [], roots: 0 };
11772
+ let source;
11773
+ try {
11774
+ source = (0, import_node_fs4.readFileSync)(file2, "utf-8");
11775
+ } catch {
11776
+ return { ...base, refusal: `cannot read ${file2}` };
11777
+ }
11778
+ let ast;
11779
+ try {
11780
+ ast = parseSource(source);
11781
+ } catch {
11782
+ return { ...base, refusal: `cannot parse ${file2}` };
11783
+ }
11784
+ const found = findComponent(ast, component);
11785
+ if (!found) {
11786
+ const barrel = depth < 2 ? followBarrel(file2, ast, component) : void 0;
11787
+ if (barrel) return planForward(barrel, component, depth + 1);
11788
+ return { ...base, refusal: `no function named ${component} in this file` };
11789
+ }
11790
+ const param = found.params[0];
11791
+ if (!t14.isObjectPattern(param)) {
11792
+ return {
11793
+ ...base,
11794
+ refusal: `${component} does not destructure its props, so there is no safe place to add one without knowing what the parameter is called downstream`
11795
+ };
11796
+ }
11797
+ const roots = findReturnedRoots(found.body);
11798
+ if (!roots) {
11799
+ return {
11800
+ ...base,
11801
+ refusal: `${component} returns something other than a JSX element (a fragment, or a value this cannot follow)`
11802
+ };
11803
+ }
11804
+ const imports = collectImports(ast);
11805
+ const nonCarrier = roots.find((root) => !isCarrierRoot(root, imports));
11806
+ if (nonCarrier) {
11807
+ const name = t14.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
11808
+ return {
11809
+ ...base,
11810
+ 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`
11811
+ };
11812
+ }
11813
+ const edits = [];
11814
+ const alreadyDestructured = param.properties.some(
11815
+ (prop) => t14.isObjectProperty(prop) && t14.isIdentifier(prop.key) && prop.key.name === "testID"
11816
+ );
11817
+ if (!alreadyDestructured) {
11818
+ const insert = insertAfterLastMember(source, param.properties, "testID", param.end ?? 0, ",");
11819
+ edits.push({ ...insert, what: "parameter" });
11820
+ }
11821
+ const annotation = param.typeAnnotation;
11822
+ if (t14.isTSTypeAnnotation(annotation)) {
11823
+ const declared = annotation.typeAnnotation;
11824
+ let body;
11825
+ let closing = 0;
11826
+ if (t14.isTSTypeLiteral(declared)) {
11827
+ body = declared.members;
11828
+ closing = declared.end ?? 0;
11829
+ } else if (t14.isTSTypeReference(declared) && t14.isIdentifier(declared.typeName)) {
11830
+ const typeName = declared.typeName.name;
11831
+ traverse13(ast, {
11832
+ TSInterfaceDeclaration(nodePath) {
11833
+ if (nodePath.node.id.name !== typeName) return;
11834
+ body = nodePath.node.body.body;
11835
+ closing = nodePath.node.body.end ?? 0;
11836
+ },
11837
+ TSTypeAliasDeclaration(nodePath) {
11838
+ if (nodePath.node.id.name !== typeName || !t14.isTSTypeLiteral(nodePath.node.typeAnnotation))
11839
+ return;
11840
+ body = nodePath.node.typeAnnotation.members;
11841
+ closing = nodePath.node.typeAnnotation.end ?? 0;
11842
+ }
11843
+ });
11844
+ if (!body) {
11845
+ return {
11846
+ ...base,
11847
+ 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`
11848
+ };
11849
+ }
11850
+ }
11851
+ if (body) {
11852
+ const declaresTestId = body.some(
11853
+ (member) => t14.isTSPropertySignature(member) && t14.isIdentifier(member.key) && member.key.name === "testID"
11854
+ );
11855
+ if (!declaresTestId) {
11856
+ const insert = insertAfterLastMember(source, body, "testID?: string;", closing, ";");
11857
+ edits.push({ ...insert, what: "props-type" });
11858
+ }
11859
+ }
11860
+ }
11861
+ let delivered = 0;
11862
+ for (const root of roots) {
11863
+ if (hasProp(root, "testID")) continue;
11864
+ edits.push({
11865
+ at: root.name.end ?? root.start ?? 0,
11866
+ text: " testID={testID}",
11867
+ what: "root"
11868
+ });
11869
+ delivered += 1;
11870
+ }
11871
+ if (edits.length === 0) {
11872
+ return { ...base, refusal: `${component} already forwards testID` };
11873
+ }
11874
+ return { file: file2, component, edits, roots: delivered };
11875
+ }
11876
+ function applyForward(source, plan) {
11877
+ if (plan.refusal || plan.edits.length === 0) return source;
11878
+ const magic = new import_magic_string2.default(source);
11879
+ for (const edit of plan.edits) magic.appendLeft(edit.at, edit.text);
11880
+ return magic.toString();
11881
+ }
11882
+
11883
+ // src/cli/commands/annotate.ts
11884
+ var DEFAULT_INCLUDE = ["src/**/*Screen.{tsx,jsx}", "src/**/screens/**/*.{tsx,jsx}"];
11885
+ var DEFAULT_EXCLUDE = ["**/node_modules/**", "**/dist/**", "**/*.test.*", "**/__tests__/**"];
11886
+ function annotateCommand() {
11887
+ return new import_commander5.Command("annotate").description(
11888
+ "Add missing testIDs to interactive elements so the agent can address them (dry run by default)"
11889
+ ).option("--write", "Actually modify the files (default: report only)", false).option(
11890
+ "--forward",
11891
+ "Also teach a shared component to accept and pass through testID, when its call sites are blocked only by that",
11892
+ false
11893
+ ).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) => {
11894
+ try {
11895
+ if (options.json) setLogMode("json");
11896
+ else if (options.quiet) setLogMode("quiet");
11897
+ banner();
11898
+ let loaded = null;
11899
+ try {
11900
+ loaded = loadConfig(warn);
11901
+ } catch {
11902
+ loaded = null;
11903
+ }
11904
+ const rootDir = process.cwd();
11905
+ const include = options.include ?? DEFAULT_INCLUDE;
11906
+ const exclude = loaded?.exclude ?? DEFAULT_EXCLUDE;
11907
+ const spinner = createSpinner("Scanning for unaddressable controls...");
11908
+ spinner.start();
11909
+ resetForwardingCache();
11910
+ const files = await (0, import_fast_glob6.default)(include, { cwd: rootDir, ignore: exclude, absolute: true });
11911
+ const scan = async () => {
11912
+ const out = [];
11913
+ for (const file2 of files) {
11914
+ const source = await (0, import_promises6.readFile)(file2, "utf-8");
11915
+ const sites = findAnnotationSites(source, file2);
11916
+ if (sites.length === 0) continue;
11917
+ out.push(applyToFile(source, sites));
11918
+ }
11919
+ return out;
11920
+ };
11921
+ let edits = await scan();
11922
+ let forwards = [];
11923
+ if (options.forward) {
11924
+ forwards = await planForwards(edits, rootDir);
11925
+ const applied = forwards.filter((plan) => !plan.refusal);
11926
+ if (options.write && applied.length > 0) {
11927
+ for (const plan of applied) {
11928
+ const before = await (0, import_promises6.readFile)(plan.file, "utf-8");
11929
+ await (0, import_promises6.writeFile)(plan.file, applyForward(before, plan), "utf-8");
11930
+ }
11931
+ resetForwardingCache();
11932
+ edits = await scan();
11933
+ }
11934
+ }
11935
+ spinner.stop();
11936
+ const written = edits.flatMap((edit) => edit.written);
11937
+ const blocked = edits.flatMap((edit) => edit.blocked);
11938
+ const touched = edits.filter((edit) => edit.written.length > 0);
11939
+ if (written.length === 0 && blocked.length === 0) {
11940
+ success("Every interactive control already has something to address it by.");
11941
+ if (options.json) {
11942
+ jsonOutput({ ok: true, scanned: files.length, annotated: 0, blocked: 0, sites: [] });
11943
+ }
11944
+ return;
11945
+ }
11946
+ if (options.write) {
11947
+ for (const edit of touched) {
11948
+ await (0, import_promises6.writeFile)(edit.file, edit.content, "utf-8");
11949
+ }
11950
+ }
11951
+ reportSites(rootDir, touched, blocked, options.write === true, forwards);
11952
+ if (options.json) {
11953
+ jsonOutput({
11954
+ ok: true,
11955
+ scanned: files.length,
11956
+ written: options.write === true,
11957
+ annotated: written.length,
11958
+ blocked: blocked.length,
11959
+ files: touched.map((edit) => import_node_path7.default.relative(rootDir, edit.file)),
11960
+ sites: [...written, ...blocked].map((site) => ({
11961
+ file: import_node_path7.default.relative(rootDir, site.file),
11962
+ line: site.line,
11963
+ component: site.component,
11964
+ role: site.role,
11965
+ testId: site.testId,
11966
+ outcome: site.outcome,
11967
+ reason: site.blockedReason
11968
+ }))
11969
+ });
11970
+ }
11971
+ } catch (err) {
11972
+ error(`Annotate failed: ${stringifyUnknown(err)}`);
11973
+ process.exit(1);
11974
+ }
11975
+ });
11976
+ }
11977
+ async function planForwards(edits, rootDir) {
11978
+ const seen = /* @__PURE__ */ new Set();
11979
+ const plans = [];
11980
+ for (const edit of edits) {
11981
+ for (const site of edit.blocked) {
11982
+ if (site.outcome !== "blocked-swallows") continue;
11983
+ if (seen.has(site.component)) continue;
11984
+ seen.add(site.component);
11985
+ const source = await (0, import_promises6.readFile)(site.file, "utf-8");
11986
+ const origin = findImportSource(source, site.component);
11987
+ const target = origin ? resolveRelativeImport(site.file, origin) : site.file;
11988
+ if (!target) {
11989
+ plans.push({
11990
+ file: site.file,
11991
+ component: site.component,
11992
+ edits: [],
11993
+ roots: 0,
11994
+ refusal: `cannot locate the source of <${site.component}>`
11995
+ });
11996
+ continue;
11997
+ }
11998
+ plans.push(planForward(target, site.component));
11999
+ }
12000
+ }
12001
+ void rootDir;
12002
+ return plans;
12003
+ }
12004
+ function findImportSource(source, name) {
12005
+ const pattern = new RegExp(
12006
+ String.raw`import\s*\{[^}]*\b${name}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
12007
+ );
12008
+ return pattern.exec(source)?.[1];
12009
+ }
12010
+ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
12011
+ const written = touched.flatMap((edit) => edit.written);
12012
+ if (written.length > 0) {
12013
+ heading(didWrite ? "Added" : "Would add");
12014
+ for (const edit of touched) {
12015
+ info(import_node_path7.default.relative(rootDir, edit.file));
12016
+ for (const site of edit.written) {
12017
+ dim(` ${String(site.line).padStart(4)} <${site.component}> testID="${site.testId}"`);
12018
+ }
12019
+ }
12020
+ }
12021
+ const teachable = new Map(
12022
+ forwards.filter((plan) => !plan.refusal).map((plan) => [plan.component, plan])
12023
+ );
12024
+ if (teachable.size > 0) {
12025
+ heading(didWrite ? "Taught to carry testID" : "Would teach to carry testID");
12026
+ for (const plan of teachable.values()) {
12027
+ info(`<${plan.component}> ${import_node_path7.default.relative(rootDir, plan.file)}`);
12028
+ for (const edit of plan.edits) {
12029
+ dim(` ${edit.what}: ${edit.text.trim()}`);
12030
+ }
12031
+ }
12032
+ }
12033
+ if (blocked.length > 0) {
12034
+ heading("Skipped \u2014 the prop would not survive to the runtime");
12035
+ const byComponent = /* @__PURE__ */ new Map();
12036
+ for (const site of blocked) {
12037
+ const group = byComponent.get(site.component) ?? [];
12038
+ group.push(site);
12039
+ byComponent.set(site.component, group);
12040
+ }
12041
+ for (const [component, group] of byComponent) {
12042
+ if (teachable.has(component)) continue;
12043
+ warn(`<${component}> \u2014 ${group.length} site${group.length === 1 ? "" : "s"}`);
12044
+ const refusal = forwards.find((plan) => plan.component === component)?.refusal;
12045
+ dim(` ${group[0].blockedReason}`);
12046
+ if (refusal) dim(` --forward cannot do it either: ${refusal}`);
12047
+ }
12048
+ }
12049
+ if (!didWrite && written.length > 0) {
12050
+ info("");
12051
+ info(`Nothing was written. Re-run with --write to apply ${written.length} change(s).`);
12052
+ } else if (didWrite) {
12053
+ success(`Wrote ${written.length} testID(s) across ${touched.length} file(s).`);
12054
+ info("Re-run `appilots generate` so the document picks them up.");
12055
+ }
12056
+ }
12057
+
12058
+ // src/cli/commands/status.ts
12059
+ var import_commander6 = require("commander");
10483
12060
  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) => {
12061
+ 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
12062
  if (options.json) setLogMode("json");
10486
12063
  else if (options.quiet) setLogMode("quiet");
10487
12064
  try {
10488
12065
  banner();
10489
- const config = loadConfig();
12066
+ const config = loadConfig(warn);
10490
12067
  if (!config) {
10491
12068
  error("No .appilotsrc configuration found");
10492
12069
  info("Run: appilots init --api-key <your-key>");
@@ -10513,8 +12090,13 @@ function statusCommand() {
10513
12090
  ]);
10514
12091
  if (status.project) {
10515
12092
  heading("\n\u{1F4CC} Project Info:");
10516
- table([["Name", status.project.name], ["ID", status.project.id]]);
12093
+ table([
12094
+ ["Name", status.project.name],
12095
+ ["ID", status.project.id]
12096
+ ]);
10517
12097
  }
12098
+ const outputDir = config.outputDir || ".appilots";
12099
+ let drift = null;
10518
12100
  if (status.activeMcp) {
10519
12101
  heading("\n\u2705 Active MCP Document:");
10520
12102
  table([
@@ -10524,6 +12106,14 @@ function statusCommand() {
10524
12106
  ["Forms", String(status.activeMcp.formsCount)],
10525
12107
  ["Actions", String(status.activeMcp.actionsCount)]
10526
12108
  ]);
12109
+ const local = readLocalDocumentSummary(outputDir);
12110
+ if (local) {
12111
+ drift = compareWithRemote(local, status.activeMcp);
12112
+ if (drift) {
12113
+ heading("");
12114
+ reportRemoteDrift(drift);
12115
+ }
12116
+ }
10527
12117
  } else {
10528
12118
  heading("\n\u26A0\uFE0F No active MCP document");
10529
12119
  info("Run: appilots sync");
@@ -10534,21 +12124,22 @@ function statusCommand() {
10534
12124
  local: {
10535
12125
  serverUrl: config.serverUrl,
10536
12126
  projectId: config.projectId || null,
10537
- outputDir: config.outputDir || ".appilots"
12127
+ outputDir
10538
12128
  },
10539
12129
  project: status.project ?? null,
10540
- activeMcp: status.activeMcp ?? null
12130
+ activeMcp: status.activeMcp ?? null,
12131
+ drift
10541
12132
  });
10542
12133
  } 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) });
12134
+ error(`Status check failed: ${stringifyUnknown(err)}`);
12135
+ jsonOutput({ success: false, error: stringifyUnknown(err) });
10545
12136
  process.exit(1);
10546
12137
  }
10547
12138
  });
10548
12139
  }
10549
12140
 
10550
12141
  // src/cli/commands/eval.ts
10551
- var import_commander6 = require("commander");
12142
+ var import_commander7 = require("commander");
10552
12143
  var import_fs10 = require("fs");
10553
12144
  var import_path9 = require("path");
10554
12145
 
@@ -10578,21 +12169,21 @@ function loadScenarios(dir) {
10578
12169
  } catch {
10579
12170
  return { scenarios, errors };
10580
12171
  }
10581
- for (const file of files) {
10582
- const full = (0, import_path7.join)(dir, file);
12172
+ for (const file2 of files) {
12173
+ const full = (0, import_path7.join)(dir, file2);
10583
12174
  let raw;
10584
12175
  try {
10585
12176
  raw = JSON.parse((0, import_fs8.readFileSync)(full, "utf-8"));
10586
12177
  } catch (err) {
10587
- errors.push({ file, error: `invalid JSON: ${err instanceof Error ? err.message : String(err)}` });
12178
+ errors.push({ file: file2, error: `invalid JSON: ${stringifyUnknown(err)}` });
10588
12179
  continue;
10589
12180
  }
10590
12181
  const shapeError = validateScenarioShape(raw);
10591
12182
  if (shapeError) {
10592
- errors.push({ file, error: shapeError });
12183
+ errors.push({ file: file2, error: shapeError });
10593
12184
  continue;
10594
12185
  }
10595
- scenarios.push({ file, scenario: raw });
12186
+ scenarios.push({ file: file2, scenario: raw });
10596
12187
  }
10597
12188
  return { scenarios, errors };
10598
12189
  }
@@ -10741,10 +12332,10 @@ function matchActions(expected, actual, reply = "") {
10741
12332
  var import_fs9 = require("fs");
10742
12333
  var import_path8 = require("path");
10743
12334
  var EMPTY_BASELINE = { generatedAt: null, gitSha: null, perScenario: {} };
10744
- function loadBaseline(path9) {
10745
- if (!(0, import_fs9.existsSync)(path9)) return { ...EMPTY_BASELINE, perScenario: {} };
12335
+ function loadBaseline(path11) {
12336
+ if (!(0, import_fs9.existsSync)(path11)) return { ...EMPTY_BASELINE, perScenario: {} };
10746
12337
  try {
10747
- const raw = JSON.parse((0, import_fs9.readFileSync)(path9, "utf-8"));
12338
+ const raw = JSON.parse((0, import_fs9.readFileSync)(path11, "utf-8"));
10748
12339
  return {
10749
12340
  generatedAt: raw.generatedAt ?? null,
10750
12341
  gitSha: raw.gitSha ?? null,
@@ -10754,9 +12345,9 @@ function loadBaseline(path9) {
10754
12345
  return { ...EMPTY_BASELINE, perScenario: {} };
10755
12346
  }
10756
12347
  }
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)}
12348
+ function saveBaseline(path11, baseline) {
12349
+ (0, import_fs9.mkdirSync)((0, import_path8.dirname)(path11), { recursive: true });
12350
+ (0, import_fs9.writeFileSync)(path11, `${JSON.stringify(baseline, null, 2)}
10760
12351
  `, "utf-8");
10761
12352
  }
10762
12353
  function buildBaseline(outcomes, meta) {
@@ -10838,13 +12429,16 @@ function parseRate(raw, fallback, flagName) {
10838
12429
  return n;
10839
12430
  }
10840
12431
  function evalCommand() {
10841
- return new import_commander6.Command("eval").description(
12432
+ return new import_commander7.Command("eval").description(
10842
12433
  "Run agent eval scenarios against the current MCP document in dry-run mode and check for regressions vs a committed baseline"
10843
12434
  ).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
12435
  "--update-baseline",
10845
12436
  "Record this run as the new baseline (refuses if pass rate is below the threshold)",
10846
12437
  false
10847
- ).option("--min-pass-rate <n>", "Minimum fraction of scenarios that must pass, 0-1 (default: 0.95)").option(
12438
+ ).option(
12439
+ "--min-pass-rate <n>",
12440
+ "Minimum fraction of scenarios that must pass, 0-1 (default: 0.95)"
12441
+ ).option(
10848
12442
  "--max-token-regression <n>",
10849
12443
  "Max fractional token increase before a passing scenario is flagged, e.g. 0.25 for +25% (default: 0.25)"
10850
12444
  ).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 +12446,7 @@ function evalCommand() {
10852
12446
  else if (options.quiet) setLogMode("quiet");
10853
12447
  try {
10854
12448
  banner();
10855
- const loaded = loadConfig();
12449
+ const loaded = loadConfig(warn);
10856
12450
  const outputDir = loaded?.outputDir || ".appilots";
10857
12451
  const scenariosDir = options.dir || loaded?.eval?.scenariosDir || (0, import_path9.join)(outputDir, "scenarios");
10858
12452
  if (options.init) {
@@ -10881,7 +12475,7 @@ function evalCommand() {
10881
12475
  return;
10882
12476
  }
10883
12477
  const config = {
10884
- serverUrl: "http://localhost:4000",
12478
+ serverUrl: DEFAULT_SERVER_URL,
10885
12479
  ...loaded,
10886
12480
  apiKey,
10887
12481
  ...options.projectId ? { projectId: options.projectId } : {},
@@ -10927,11 +12521,17 @@ function evalCommand() {
10927
12521
  error(
10928
12522
  `Duplicate scenario name(s) \u2014 "name" must be unique across all scenario files: ${duplicates.join(", ")}`
10929
12523
  );
10930
- jsonOutput({ success: false, error: `Duplicate scenario names: ${duplicates.join(", ")}` });
12524
+ jsonOutput({
12525
+ success: false,
12526
+ error: `Duplicate scenario names: ${duplicates.join(", ")}`
12527
+ });
10931
12528
  process.exit(1);
10932
12529
  return;
10933
12530
  }
10934
- const apiClient = new AppilotsAPIClient({ serverUrl: config.serverUrl, apiKey: config.apiKey });
12531
+ const apiClient = new AppilotsAPIClient({
12532
+ serverUrl: config.serverUrl,
12533
+ apiKey: config.apiKey
12534
+ });
10935
12535
  const requests = loadedScenarios.scenarios.map(({ scenario }) => ({
10936
12536
  id: scenario.name,
10937
12537
  screen: scenario.given?.screen,
@@ -11033,8 +12633,8 @@ Pass rate: ${passed}/${total} (${passRatePct}%) \u2014 threshold ${minPassRatePc
11033
12633
  success("Eval passed");
11034
12634
  }
11035
12635
  } 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) });
12636
+ error(`Eval failed: ${stringifyUnknown(err)}`);
12637
+ jsonOutput({ success: false, error: stringifyUnknown(err) });
11038
12638
  process.exit(1);
11039
12639
  }
11040
12640
  });
@@ -11042,16 +12642,17 @@ Pass rate: ${passed}/${total} (${passRatePct}%) \u2014 threshold ${minPassRatePc
11042
12642
 
11043
12643
  // src/cli/index.ts
11044
12644
  async function main() {
11045
- const program = new import_commander7.Command();
11046
- program.name("appilots").description(
12645
+ const program2 = new import_commander8.Command();
12646
+ program2.name("appilots").description(
11047
12647
  "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());
12648
+ ).version(CLI_VERSION).option("--verbose", "Enable verbose logging");
12649
+ program2.addCommand(initCommand());
12650
+ program2.addCommand(syncCommand());
12651
+ program2.addCommand(watchCommand());
12652
+ program2.addCommand(generateCommand());
12653
+ program2.addCommand(annotateCommand());
12654
+ program2.addCommand(statusCommand());
12655
+ program2.addCommand(evalCommand());
11055
12656
  const args = process.argv.slice(2);
11056
12657
  const firstArg = args[0];
11057
12658
  const hasCommand = firstArg !== void 0 && args.length > 0 && !firstArg.startsWith("-");
@@ -11064,14 +12665,14 @@ async function main() {
11064
12665
  info("Welcome to Appilots! Get started with:");
11065
12666
  info(" appilots init --api-key <your-ak_key>");
11066
12667
  console.log("");
11067
- program.outputHelp();
12668
+ program2.outputHelp();
11068
12669
  return;
11069
12670
  }
11070
12671
  }
11071
- await program.parseAsync(process.argv);
12672
+ await program2.parseAsync(process.argv);
11072
12673
  }
11073
12674
  main().catch((err) => {
11074
- error(err instanceof Error ? err.message : String(err));
12675
+ error(stringifyUnknown(err));
11075
12676
  process.exit(1);
11076
12677
  });
11077
12678
  //# sourceMappingURL=index.js.map