@appilots/cli 0.3.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/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs, { readFile, mkdir, writeFile } from 'fs/promises';
2
- import * as path6 from 'path';
3
- import path6__default, { join, resolve } from 'path';
2
+ import * as path8 from 'path';
3
+ import path8__default, { join, resolve } from 'path';
4
4
  import traverse4 from '@babel/traverse';
5
5
  import * as BabelTypes from '@babel/types';
6
6
  import glob from 'fast-glob';
@@ -56,6 +56,45 @@ function hasJsxAttribute(element, name) {
56
56
  }
57
57
 
58
58
  // src/ast/jsx/classify.ts
59
+ var NON_INTERACTION_HANDLERS = /* @__PURE__ */ new Set([
60
+ "onLayout",
61
+ "onScroll",
62
+ "onScrollBeginDrag",
63
+ "onScrollEndDrag",
64
+ "onMomentumScrollBegin",
65
+ "onMomentumScrollEnd",
66
+ "onContentSizeChange",
67
+ "onEndReached",
68
+ "onViewableItemsChanged",
69
+ "onLoad",
70
+ "onLoadStart",
71
+ "onLoadEnd",
72
+ "onError",
73
+ "onProgress",
74
+ "onFocus",
75
+ "onBlur"
76
+ ]);
77
+ var VALUE_HANDLERS = /* @__PURE__ */ new Set([
78
+ "onChange",
79
+ "onChangeText",
80
+ "onValueChange",
81
+ "onSelect",
82
+ "onSelectionChange",
83
+ "onSubmitEditing"
84
+ ]);
85
+ function pressHandlerProp(element) {
86
+ let fallback;
87
+ for (const attr of element.attributes) {
88
+ if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
89
+ const name = attr.name.name;
90
+ if (!/^on[A-Z]/.test(name)) continue;
91
+ if (attr.value?.type !== "JSXExpressionContainer") continue;
92
+ if (name === "onPress") return name;
93
+ if (NON_INTERACTION_HANDLERS.has(name) || VALUE_HANDLERS.has(name)) continue;
94
+ fallback ??= name;
95
+ }
96
+ return fallback;
97
+ }
59
98
  var VIEW_COMPONENTS = /* @__PURE__ */ new Set(["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"]);
60
99
  var INPUT_COMPONENTS = /* @__PURE__ */ new Set(["TextInput", "Input", "HocInput"]);
61
100
  var BUTTON_COMPONENTS = /* @__PURE__ */ new Set(["TouchableOpacity", "Pressable", "Button"]);
@@ -70,17 +109,16 @@ function classifyJsxComponent(name, element) {
70
109
  if (INPUT_COMPONENTS.has(name)) return "input";
71
110
  if (BUTTON_COMPONENTS.has(name)) return "button";
72
111
  if (element) {
73
- const hasOptions = hasJsxAttribute(element, "options");
112
+ const hasOptions = hasJsxAttribute(element, "options") || hasJsxAttribute(element, "segments") || hasJsxAttribute(element, "choices") || hasJsxAttribute(element, "tabs");
74
113
  const hasValue = hasJsxAttribute(element, "value");
75
114
  const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
76
115
  const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange") || hasJsxAttribute(element, "onChangeText");
77
- const hasOnPress = hasJsxAttribute(element, "onPress");
78
116
  if (hasOptions && (hasValue || hasOnChange)) return "select";
79
117
  if (hasChecked && hasOnChange) return "toggle";
80
118
  if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
81
119
  return "input";
82
120
  }
83
- if (hasOnPress) return "button";
121
+ if (pressHandlerProp(element)) return "button";
84
122
  if (getStringAttr(element, "visible") || getExpressionIdentifierAttr(element, "visible")) {
85
123
  return "modal";
86
124
  }
@@ -392,12 +430,12 @@ var ScreenAnalyzer = class {
392
430
  cwd: this.config.rootDir,
393
431
  ignore: exclude
394
432
  });
395
- screenPatternFiles = new Set(matched.map((f) => path6__default.resolve(this.config.rootDir, f)));
433
+ screenPatternFiles = new Set(matched.map((f) => path8__default.resolve(this.config.rootDir, f)));
396
434
  }
397
435
  const screens = [];
398
436
  this.screensFilteredOut = 0;
399
437
  for (const file of files) {
400
- const filePath = path6__default.resolve(this.config.rootDir, file);
438
+ const filePath = path8__default.resolve(this.config.rootDir, file);
401
439
  try {
402
440
  const descriptor = await this.analyzeFile(filePath);
403
441
  if (!descriptor) continue;
@@ -418,12 +456,17 @@ var ScreenAnalyzer = class {
418
456
  }
419
457
  } catch (error) {
420
458
  if (this.verbose) {
421
- console.warn(`[ScreenAnalyzer] Failed to parse ${file}:`, error instanceof Error ? error.message : error);
459
+ console.warn(
460
+ `[ScreenAnalyzer] Failed to parse ${file}:`,
461
+ error instanceof Error ? error.message : error
462
+ );
422
463
  }
423
464
  }
424
465
  }
425
466
  if (this.verbose && this.strictScreens) {
426
- console.log(`[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`);
467
+ console.log(
468
+ `[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`
469
+ );
427
470
  }
428
471
  return screens;
429
472
  }
@@ -711,7 +754,11 @@ var ScreenAnalyzer = class {
711
754
  for (const form of secondary) {
712
755
  const existing = out.find((candidate) => candidate.id === form.id) ?? this.findFormWithSharedFields(out, form);
713
756
  if (!existing) {
714
- out.push({ ...form, fields: [...form.fields], submitAction: this.namedSubmitAction(form.submitAction) });
757
+ out.push({
758
+ ...form,
759
+ fields: [...form.fields],
760
+ submitAction: this.namedSubmitAction(form.submitAction)
761
+ });
715
762
  continue;
716
763
  }
717
764
  for (const field of form.fields) {
@@ -730,9 +777,12 @@ var ScreenAnalyzer = class {
730
777
  findEquivalentField(fields, incoming) {
731
778
  return fields.find((field) => {
732
779
  if (field.name && incoming.name && field.name === incoming.name) return true;
733
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
734
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
735
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
780
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
781
+ return true;
782
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
783
+ return true;
784
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
785
+ return true;
736
786
  if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
737
787
  return true;
738
788
  }
@@ -892,9 +942,11 @@ var ScreenAnalyzer = class {
892
942
  source: field.locator?.source ?? "accessibilityLabel"
893
943
  });
894
944
  } else if (attrName === "keyboardType") {
895
- if (attr.value.value === "email-address" || attr.value.value === "email") field.type = "email";
945
+ if (attr.value.value === "email-address" || attr.value.value === "email")
946
+ field.type = "email";
896
947
  if (attr.value.value === "phone-pad") field.type = "phone";
897
- if (attr.value.value === "numeric" || attr.value.value === "number-pad") field.type = "number";
948
+ if (attr.value.value === "numeric" || attr.value.value === "number-pad")
949
+ field.type = "number";
898
950
  }
899
951
  } else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
900
952
  if (attrName === "value" && BabelTypes.isIdentifier(attr.value.expression)) {
@@ -1060,8 +1112,10 @@ var ScreenAnalyzer = class {
1060
1112
  };
1061
1113
  if (handler.nativeConfirmationExpected) action.nativeConfirmationExpected = true;
1062
1114
  if (handler.targetScreen && !action.targetScreen) action.targetScreen = handler.targetScreen;
1063
- if (handler.successSignal && !action.successSignal) action.successSignal = handler.successSignal;
1064
- if (handler.failureSignal && !action.failureSignal) action.failureSignal = handler.failureSignal;
1115
+ if (handler.successSignal && !action.successSignal)
1116
+ action.successSignal = handler.successSignal;
1117
+ if (handler.failureSignal && !action.failureSignal)
1118
+ action.failureSignal = handler.failureSignal;
1065
1119
  if (handler.opensModal && !action.opensModal) action.opensModal = handler.opensModal;
1066
1120
  if (handler.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
1067
1121
  action.destructive = true;
@@ -1079,6 +1133,7 @@ var ScreenAnalyzer = class {
1079
1133
  if (!BabelTypes.isJSXIdentifier(element.name)) return;
1080
1134
  const componentName = element.name.name;
1081
1135
  if (classifyJsxComponent(componentName, element) !== "button") return;
1136
+ const pressProp = pressHandlerProp(element);
1082
1137
  let label;
1083
1138
  let handler;
1084
1139
  for (const attr of element.attributes) {
@@ -1088,7 +1143,7 @@ var ScreenAnalyzer = class {
1088
1143
  if ((attrName === "title" || attrName === "accessibilityLabel" || attrName === "label") && BabelTypes.isStringLiteral(attr.value)) {
1089
1144
  label = attr.value.value;
1090
1145
  }
1091
- if (attrName === "onPress" && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
1146
+ if (attrName === pressProp && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
1092
1147
  handler = attr.value.expression.name;
1093
1148
  }
1094
1149
  }
@@ -1154,10 +1209,13 @@ var ScreenAnalyzer = class {
1154
1209
  }
1155
1210
  };
1156
1211
  if (fn.body) {
1157
- traverse4(fn.body, {
1158
- noScope: true,
1159
- enter: (nodePath) => inspectNode(nodePath.node)
1160
- });
1212
+ traverse4(
1213
+ fn.body,
1214
+ {
1215
+ noScope: true,
1216
+ enter: (nodePath) => inspectNode(nodePath.node)
1217
+ }
1218
+ );
1161
1219
  }
1162
1220
  const expectedOutcome = targetScreen ? "navigation" : hasStateSetter ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
1163
1221
  return {
@@ -1186,7 +1244,7 @@ var ScreenAnalyzer = class {
1186
1244
  * heuristic must hit OR the JSDoc tag must be present.
1187
1245
  */
1188
1246
  isElementDestructive(element, handlerName, actionId) {
1189
- const DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
1247
+ const DESTRUCTIVE_VERB3 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
1190
1248
  for (const attr of element.attributes) {
1191
1249
  if (!BabelTypes.isJSXAttribute(attr)) continue;
1192
1250
  const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
@@ -1201,8 +1259,8 @@ var ScreenAnalyzer = class {
1201
1259
  }
1202
1260
  }
1203
1261
  }
1204
- if (handlerName && DESTRUCTIVE_VERB2.test(handlerName)) return true;
1205
- if (actionId && DESTRUCTIVE_VERB2.test(actionId)) return true;
1262
+ if (handlerName && DESTRUCTIVE_VERB3.test(handlerName)) return true;
1263
+ if (actionId && DESTRUCTIVE_VERB3.test(actionId)) return true;
1206
1264
  return false;
1207
1265
  }
1208
1266
  /**
@@ -1215,6 +1273,7 @@ var ScreenAnalyzer = class {
1215
1273
  };
1216
1274
  const componentName = BabelTypes.isJSXIdentifier(element.name) ? element.name.name : void 0;
1217
1275
  action.sourceComponent = componentName;
1276
+ const pressProp = pressHandlerProp(element) ?? "onPress";
1218
1277
  let handlerName;
1219
1278
  for (const attr of element.attributes) {
1220
1279
  if (!BabelTypes.isJSXAttribute(attr)) continue;
@@ -1249,7 +1308,7 @@ var ScreenAnalyzer = class {
1249
1308
  }
1250
1309
  }
1251
1310
  } else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
1252
- if (attrName === "onPress" && BabelTypes.isIdentifier(attr.value.expression)) {
1311
+ if (attrName === pressProp && BabelTypes.isIdentifier(attr.value.expression)) {
1253
1312
  handlerName = attr.value.expression.name.toLowerCase();
1254
1313
  action.handler = attr.value.expression.name;
1255
1314
  if (handlerName.includes("submit")) {
@@ -1257,7 +1316,7 @@ var ScreenAnalyzer = class {
1257
1316
  } else if (handlerName.includes("navigate")) {
1258
1317
  action.type = "navigation";
1259
1318
  }
1260
- } else if (attrName === "onPress") {
1319
+ } else if (attrName === pressProp) {
1261
1320
  const inline = this.extractInlineOnPressMetadata(attr.value.expression);
1262
1321
  if (inline.handler) {
1263
1322
  handlerName = inline.handler.toLowerCase();
@@ -1281,7 +1340,11 @@ var ScreenAnalyzer = class {
1281
1340
  }
1282
1341
  }
1283
1342
  if (!action.locator && action.id) {
1284
- action.locator = { id: action.id, label: action.label, source: action.label ? "label" : "inferred" };
1343
+ action.locator = {
1344
+ id: action.id,
1345
+ label: action.label,
1346
+ source: action.label ? "label" : "inferred"
1347
+ };
1285
1348
  }
1286
1349
  if (this.isElementDestructive(element, handlerName, action.id)) {
1287
1350
  action.destructive = true;
@@ -1423,24 +1486,27 @@ var ScreenAnalyzer = class {
1423
1486
  }
1424
1487
  extractRowAction(fn) {
1425
1488
  let action;
1426
- traverse4(fn.body, {
1427
- noScope: true,
1428
- CallExpression: (nodePath) => {
1429
- const node = nodePath.node;
1430
- 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)) {
1431
- return;
1489
+ traverse4(
1490
+ fn.body,
1491
+ {
1492
+ noScope: true,
1493
+ CallExpression: (nodePath) => {
1494
+ const node = nodePath.node;
1495
+ 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)) {
1496
+ return;
1497
+ }
1498
+ const firstArg = node.arguments[0];
1499
+ if (!BabelTypes.isStringLiteral(firstArg)) return;
1500
+ const params = this.extractNavigationParams(node.arguments[1]);
1501
+ action = {
1502
+ type: "navigation",
1503
+ targetScreen: firstArg.value,
1504
+ ...Object.keys(params).length > 0 ? { params } : {},
1505
+ description: `Pressing a row opens ${firstArg.value}`
1506
+ };
1432
1507
  }
1433
- const firstArg = node.arguments[0];
1434
- if (!BabelTypes.isStringLiteral(firstArg)) return;
1435
- const params = this.extractNavigationParams(node.arguments[1]);
1436
- action = {
1437
- type: "navigation",
1438
- targetScreen: firstArg.value,
1439
- ...Object.keys(params).length > 0 ? { params } : {},
1440
- description: `Pressing a row opens ${firstArg.value}`
1441
- };
1442
1508
  }
1443
- });
1509
+ );
1444
1510
  return action;
1445
1511
  }
1446
1512
  extractNavigationParams(arg) {
@@ -1473,15 +1539,18 @@ var ScreenAnalyzer = class {
1473
1539
  } else if (BabelTypes.isIdentifier(firstParam)) {
1474
1540
  itemNames.add(firstParam.name);
1475
1541
  }
1476
- traverse4(fn.body, {
1477
- noScope: true,
1478
- MemberExpression: (nodePath) => {
1479
- const node = nodePath.node;
1480
- if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
1481
- fields.add(node.property.name);
1542
+ traverse4(
1543
+ fn.body,
1544
+ {
1545
+ noScope: true,
1546
+ MemberExpression: (nodePath) => {
1547
+ const node = nodePath.node;
1548
+ if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
1549
+ fields.add(node.property.name);
1550
+ }
1482
1551
  }
1483
1552
  }
1484
- });
1553
+ );
1485
1554
  return Array.from(fields).sort();
1486
1555
  }
1487
1556
  inferItemType(dataSource, renderItem, displayFields) {
@@ -1493,11 +1562,19 @@ var ScreenAnalyzer = class {
1493
1562
  if (displayFields.length > 0) return "Item";
1494
1563
  return void 0;
1495
1564
  }
1565
+ /**
1566
+ * Identity comes from how a field is NAMED, not from what the app
1567
+ * sells: `id`/`uuid`/`key`/`slug` are conventions any codebase uses,
1568
+ * `name`/`title`/`email` are how any row introduces itself. `plate`
1569
+ * used to sit in this list and read like one of them — but it is the
1570
+ * example app's schema, and no other tenant ever got its equivalent
1571
+ * (`mrn`, `trackingNumber`, `sku`) added here.
1572
+ */
1496
1573
  inferIdentityFields(keyField, displayFields) {
1497
1574
  const out = /* @__PURE__ */ new Set();
1498
1575
  if (keyField) out.add(keyField);
1499
1576
  for (const field of displayFields) {
1500
- if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
1577
+ if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
1501
1578
  }
1502
1579
  return Array.from(out);
1503
1580
  }
@@ -1508,17 +1585,23 @@ var ScreenAnalyzer = class {
1508
1585
  CallExpression: (nodePath) => {
1509
1586
  const node = nodePath.node;
1510
1587
  if (!BabelTypes.isMemberExpression(node.callee)) return;
1511
- if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter") return;
1512
- if (!BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource) return;
1588
+ if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
1589
+ return;
1590
+ if (!BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource)
1591
+ return;
1513
1592
  const fn = node.arguments[0];
1514
- if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn)) return;
1515
- traverse4(fn.body, {
1516
- noScope: true,
1517
- Identifier: (innerPath) => {
1518
- const name = innerPath.node.name;
1519
- if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
1593
+ if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
1594
+ return;
1595
+ traverse4(
1596
+ fn.body,
1597
+ {
1598
+ noScope: true,
1599
+ Identifier: (innerPath) => {
1600
+ const name = innerPath.node.name;
1601
+ if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
1602
+ }
1520
1603
  }
1521
- });
1604
+ );
1522
1605
  }
1523
1606
  });
1524
1607
  return queryBinding;
@@ -1546,7 +1629,7 @@ var ScreenAnalyzer = class {
1546
1629
  * E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
1547
1630
  */
1548
1631
  extractScreenName(filePath) {
1549
- const basename2 = path6__default.basename(filePath);
1632
+ const basename2 = path8__default.basename(filePath);
1550
1633
  return basename2.replace(/\.(tsx?|jsx?)$/, "");
1551
1634
  }
1552
1635
  };
@@ -1601,7 +1684,7 @@ var NavigationAnalyzer = class {
1601
1684
  cwd: this.config.rootDir,
1602
1685
  ignore: excludePatterns
1603
1686
  });
1604
- return files.map((file) => path6__default.join(this.config.rootDir, file));
1687
+ return files.map((file) => path8__default.join(this.config.rootDir, file));
1605
1688
  }
1606
1689
  /** Parse navigator definitions from a file */
1607
1690
  parseNavigators(content, filePath) {
@@ -1662,11 +1745,7 @@ var NavigationAnalyzer = class {
1662
1745
  if (BabelTypes.isJSXAttribute(initialRouteAttr) && BabelTypes.isStringLiteral(initialRouteAttr.value)) {
1663
1746
  navigator.initialRouteName = initialRouteAttr.value.value;
1664
1747
  }
1665
- const screens = this.extractScreensFromNavigator(
1666
- node,
1667
- objectName,
1668
- navigator.type
1669
- );
1748
+ const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
1670
1749
  screensByNavigator.set(objectName, screens);
1671
1750
  }
1672
1751
  }
@@ -1683,27 +1762,112 @@ var NavigationAnalyzer = class {
1683
1762
  }
1684
1763
  return navigators;
1685
1764
  }
1765
+ /**
1766
+ * Is this JSX element `<navigatorVarName.MEMBER …>`?
1767
+ */
1768
+ // Deliberately NOT a type predicate (`node is t.JSXElement`): a false
1769
+ // result would then narrow `node` to "not a JSXElement", and the very
1770
+ // next check — the same node against a different member — would see
1771
+ // `never`. It is a question about the member name, not about the node
1772
+ // kind.
1773
+ isNavigatorMember(node, navigatorVarName, member) {
1774
+ if (!BabelTypes.isJSXElement(node)) return false;
1775
+ const name = node.openingElement.name;
1776
+ return BabelTypes.isJSXMemberExpression(name) && BabelTypes.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && BabelTypes.isJSXIdentifier(name.property) && name.property.name === member;
1777
+ }
1778
+ /**
1779
+ * Flatten a navigator's JSX children into the `<X.Screen>` elements they
1780
+ * contain, unwrapping every container a real app puts in between.
1781
+ *
1782
+ * The old version compared `t.isJSXElement(child)` against the direct
1783
+ * children only. A ternary is a `JSXExpressionContainer`, so an
1784
+ * auth-gated root — the modal shape of a commercial app, and the shape
1785
+ * of this repo's own `apps/example-app` — contributed ZERO screens to
1786
+ * the graph while `appilots sync` reported success (#396).
1787
+ *
1788
+ * Both branches of a conditional are collected on purpose. The graph is
1789
+ * a design-time map of what routes EXIST, not a prediction of which one
1790
+ * a given session will render; the agent needs the destination name to
1791
+ * be there whether or not the user happens to be logged in right now.
1792
+ */
1793
+ collectScreenElements(children, navigatorVarName, depth = 0) {
1794
+ if (depth > 12) return [];
1795
+ const found = [];
1796
+ const visit = (node) => {
1797
+ if (!node) return;
1798
+ if (this.isNavigatorMember(node, navigatorVarName, "Screen")) {
1799
+ found.push(node);
1800
+ return;
1801
+ }
1802
+ if (this.isNavigatorMember(node, navigatorVarName, "Group")) {
1803
+ const group = node;
1804
+ found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
1805
+ return;
1806
+ }
1807
+ if (BabelTypes.isJSXElement(node)) {
1808
+ const name = node.openingElement.name;
1809
+ const isForeignNavigator = BabelTypes.isJSXMemberExpression(name) && BabelTypes.isJSXIdentifier(name.property) && name.property.name === "Navigator";
1810
+ if (isForeignNavigator) return;
1811
+ found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
1812
+ return;
1813
+ }
1814
+ if (BabelTypes.isJSXFragment(node)) {
1815
+ found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
1816
+ return;
1817
+ }
1818
+ if (BabelTypes.isJSXExpressionContainer(node)) {
1819
+ visit(node.expression);
1820
+ return;
1821
+ }
1822
+ if (BabelTypes.isConditionalExpression(node)) {
1823
+ visit(node.consequent);
1824
+ visit(node.alternate);
1825
+ return;
1826
+ }
1827
+ if (BabelTypes.isLogicalExpression(node)) {
1828
+ visit(node.left);
1829
+ visit(node.right);
1830
+ return;
1831
+ }
1832
+ if (BabelTypes.isCallExpression(node)) {
1833
+ for (const arg of node.arguments) {
1834
+ if (BabelTypes.isArrowFunctionExpression(arg) || BabelTypes.isFunctionExpression(arg)) {
1835
+ if (BabelTypes.isBlockStatement(arg.body)) {
1836
+ for (const stmt of arg.body.body) {
1837
+ if (BabelTypes.isReturnStatement(stmt)) visit(stmt.argument);
1838
+ }
1839
+ } else {
1840
+ visit(arg.body);
1841
+ }
1842
+ }
1843
+ }
1844
+ return;
1845
+ }
1846
+ if (BabelTypes.isArrayExpression(node)) {
1847
+ for (const el of node.elements) visit(el);
1848
+ return;
1849
+ }
1850
+ if (BabelTypes.isTSAsExpression(node) || BabelTypes.isTSNonNullExpression(node)) {
1851
+ visit(node.expression);
1852
+ }
1853
+ };
1854
+ for (const child of children) visit(child);
1855
+ return found;
1856
+ }
1686
1857
  /** Extract screens from a navigator JSX element */
1687
1858
  extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
1688
1859
  const screens = [];
1689
1860
  if (!navigatorElement.children) return screens;
1690
- for (const child of navigatorElement.children) {
1691
- if (BabelTypes.isJSXElement(child) && BabelTypes.isJSXMemberExpression(child.openingElement.name)) {
1692
- const memberExpr = child.openingElement.name;
1693
- if (BabelTypes.isJSXIdentifier(memberExpr.object) && memberExpr.object.name === navigatorVarName && BabelTypes.isJSXIdentifier(memberExpr.property) && memberExpr.property.name === "Screen") {
1694
- const screenName = this.extractAttributeValue(
1695
- child.openingElement.attributes,
1696
- "name"
1697
- );
1698
- if (screenName) {
1699
- screens.push({
1700
- name: screenName,
1701
- navigatorName: navigatorVarName,
1702
- navigatorType
1703
- });
1704
- }
1705
- }
1706
- }
1861
+ const seen = /* @__PURE__ */ new Set();
1862
+ for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
1863
+ const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
1864
+ if (!screenName || seen.has(screenName)) continue;
1865
+ seen.add(screenName);
1866
+ screens.push({
1867
+ name: screenName,
1868
+ navigatorName: navigatorVarName,
1869
+ navigatorType
1870
+ });
1707
1871
  }
1708
1872
  return screens;
1709
1873
  }
@@ -1762,9 +1926,7 @@ var NavigationAnalyzer = class {
1762
1926
  if (member.type === "TSPropertySignature" && member.key) {
1763
1927
  const keyName = member.key.type === "Identifier" ? member.key.name : null;
1764
1928
  if (keyName && member.typeAnnotation) {
1765
- const params = this.extractParamsFromType(
1766
- member.typeAnnotation.typeAnnotation
1767
- );
1929
+ const params = this.extractParamsFromType(member.typeAnnotation.typeAnnotation);
1768
1930
  entries.set(keyName, params);
1769
1931
  }
1770
1932
  }
@@ -1804,7 +1966,7 @@ var NavigationAnalyzer = class {
1804
1966
  if (type.type === "TSUndefinedKeyword") return "undefined";
1805
1967
  if (type.type === "TSNullKeyword") return "null";
1806
1968
  if (type.type === "TSUnionType") {
1807
- return type.types.map((t8) => this.typeToString(t8)).join(" | ");
1969
+ return type.types.map((t12) => this.typeToString(t12)).join(" | ");
1808
1970
  }
1809
1971
  if (type.type === "TSTypeLiteral") {
1810
1972
  return "object";
@@ -1825,7 +1987,7 @@ var NavigationAnalyzer = class {
1825
1987
  /** Attach parsed type params to navigator screens */
1826
1988
  attachParamsToNavigators(navigators, types) {
1827
1989
  for (const navigator of navigators) {
1828
- const matchingType = types.find((t8) => t8.type === navigator.type);
1990
+ const matchingType = types.find((t12) => t12.type === navigator.type);
1829
1991
  if (matchingType) {
1830
1992
  for (const screen of navigator.screens) {
1831
1993
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -1843,6 +2005,11 @@ var NavigationAnalyzer = class {
1843
2005
  let initialScreen = "";
1844
2006
  for (const navigator of parsedNavigators) {
1845
2007
  const screenNames = navigator.screens.map((s) => s.name);
2008
+ if (screenNames.length === 0) {
2009
+ console.warn(
2010
+ `[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.`
2011
+ );
2012
+ }
1846
2013
  navigators.push({
1847
2014
  name: navigator.name,
1848
2015
  type: navigator.type,
@@ -1910,8 +2077,8 @@ var ComponentAnalyzer = class {
1910
2077
  });
1911
2078
  const components = [];
1912
2079
  traverse4(ast, {
1913
- JSXElement: (path7) => {
1914
- const component = this.extractComponentFromJSXElement(path7.node);
2080
+ JSXElement: (path9) => {
2081
+ const component = this.extractComponentFromJSXElement(path9.node);
1915
2082
  if (component) {
1916
2083
  components.push(component);
1917
2084
  }
@@ -2038,13 +2205,13 @@ var FormAnalyzer = class {
2038
2205
  this.inputElements = [];
2039
2206
  this.submitButtons = [];
2040
2207
  traverse4(ast, {
2041
- CallExpression: (path7) => {
2042
- this.extractStateVariables(path7.node);
2208
+ CallExpression: (path9) => {
2209
+ this.extractStateVariables(path9.node);
2043
2210
  }
2044
2211
  });
2045
2212
  traverse4(ast, {
2046
- JSXElement: (path7) => {
2047
- this.extractFormElements(path7.node);
2213
+ JSXElement: (path9) => {
2214
+ this.extractFormElements(path9.node);
2048
2215
  }
2049
2216
  });
2050
2217
  const validationRules = this.extractValidationRules(ast);
@@ -2144,8 +2311,8 @@ var FormAnalyzer = class {
2144
2311
  extractValidationRules(ast) {
2145
2312
  const rules = {};
2146
2313
  traverse4(ast, {
2147
- IfStatement: (path7) => {
2148
- const test = path7.node.test;
2314
+ IfStatement: (path9) => {
2315
+ const test = path9.node.test;
2149
2316
  const rule = this.extractRuleFromCondition(test);
2150
2317
  if (rule) {
2151
2318
  const { field, description } = rule;
@@ -2199,7 +2366,7 @@ var FormAnalyzer = class {
2199
2366
  }
2200
2367
  buildForms(filePath, validationRules) {
2201
2368
  if (this.inputElements.length === 0) return [];
2202
- const fileName = path6.basename(filePath, path6.extname(filePath));
2369
+ const fileName = path8.basename(filePath, path8.extname(filePath));
2203
2370
  const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
2204
2371
  const fields = this.inputElements.map((input) => {
2205
2372
  const fieldType = this.inferFieldType(input);
@@ -2277,7 +2444,7 @@ var ReactNativePlatformAnalyzer = class {
2277
2444
  `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
2278
2445
  );
2279
2446
  const enrichmentPromises = screenFiles.map(async (file) => {
2280
- const filePath = path6__default.resolve(config.rootDir, file);
2447
+ const filePath = path8__default.resolve(config.rootDir, file);
2281
2448
  try {
2282
2449
  const [components, forms] = await Promise.all([
2283
2450
  componentAnalyzer.analyzeFile(filePath),
@@ -2320,91 +2487,1579 @@ var ReactNativePlatformAnalyzer = class {
2320
2487
  });
2321
2488
  }
2322
2489
  }
2323
- return {
2324
- ...screen,
2325
- components: [...screen.components, ...newComponents],
2326
- forms: mergedForms
2327
- };
2490
+ return {
2491
+ ...screen,
2492
+ components: [...screen.components, ...newComponents],
2493
+ forms: mergedForms
2494
+ };
2495
+ }
2496
+ return screen;
2497
+ });
2498
+ return {
2499
+ screens: enrichedScreens,
2500
+ navigation,
2501
+ analyzedFiles: screenFiles.length,
2502
+ ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
2503
+ };
2504
+ }
2505
+ mergeForm(target, source) {
2506
+ for (const field of source.fields) {
2507
+ const existingField = this.findEquivalentField(target.fields, field);
2508
+ if (existingField) {
2509
+ this.mergeField(existingField, field);
2510
+ } else {
2511
+ target.fields.push(field);
2512
+ }
2513
+ }
2514
+ target.submitAction = target.submitAction ?? this.namedSubmitAction(source.submitAction);
2515
+ target.validationRules = source.validationRules ? { ...source.validationRules, ...target.validationRules ?? {} } : target.validationRules;
2516
+ }
2517
+ findEquivalentField(fields, incoming) {
2518
+ return fields.find((field) => {
2519
+ if (field.name && incoming.name && field.name === incoming.name) return true;
2520
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
2521
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
2522
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
2523
+ if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
2524
+ return true;
2525
+ }
2526
+ return false;
2527
+ });
2528
+ }
2529
+ mergeField(target, source) {
2530
+ if (this.isWeakInferredFieldName(target.name) && !this.isWeakInferredFieldName(source.name)) {
2531
+ target.name = source.name;
2532
+ }
2533
+ target.label = target.label ?? source.label;
2534
+ target.placeholder = target.placeholder ?? source.placeholder;
2535
+ target.options = target.options ?? source.options;
2536
+ target.locator = target.locator ?? source.locator;
2537
+ target.sourceComponent = target.sourceComponent ?? source.sourceComponent;
2538
+ target.valueBinding = target.valueBinding ?? source.valueBinding;
2539
+ target.errorBinding = target.errorBinding ?? source.errorBinding;
2540
+ target.required = target.required || source.required;
2541
+ if (target.type === "text" && source.type !== "text") {
2542
+ target.type = source.type;
2543
+ }
2544
+ }
2545
+ namedSubmitAction(submitAction) {
2546
+ return submitAction && submitAction !== "anonymous" ? submitAction : void 0;
2547
+ }
2548
+ isWeakInferredFieldName(name) {
2549
+ return /^(text|value|input|query|search|selected|checked)$/i.test(name);
2550
+ }
2551
+ findFormWithSharedFields(forms, incoming) {
2552
+ if (incoming.fields.length === 0) return void 0;
2553
+ let best;
2554
+ for (const form of forms) {
2555
+ const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
2556
+ if (overlap > 0 && (!best || overlap > best.overlap)) {
2557
+ best = { form, overlap };
2558
+ }
2559
+ }
2560
+ return best?.form;
2561
+ }
2562
+ };
2563
+
2564
+ // src/analyzers/GenericPlatformAnalyzer.ts
2565
+ var GenericPlatformAnalyzer = class {
2566
+ constructor(platform) {
2567
+ this.platform = platform;
2568
+ }
2569
+ platform;
2570
+ async analyze(_config, _options) {
2571
+ return {
2572
+ screens: [],
2573
+ navigation: { screens: {}, initialScreen: "", navigators: [] },
2574
+ analyzedFiles: 0
2575
+ };
2576
+ }
2577
+ };
2578
+
2579
+ // src/ast/jsx/web/classify.ts
2580
+ var VIEW_TAGS = /* @__PURE__ */ new Set([
2581
+ "div",
2582
+ "span",
2583
+ "section",
2584
+ "main",
2585
+ "article",
2586
+ "aside",
2587
+ "header",
2588
+ "footer",
2589
+ "nav",
2590
+ "fieldset",
2591
+ "form",
2592
+ "p"
2593
+ ]);
2594
+ var LIST_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
2595
+ var BUTTON_TAGS = /* @__PURE__ */ new Set(["button"]);
2596
+ var INPUT_TAGS = /* @__PURE__ */ new Set(["input", "textarea"]);
2597
+ var MODAL_TAGS = /* @__PURE__ */ new Set(["dialog"]);
2598
+ var BUTTON_COMPONENTS2 = /* @__PURE__ */ new Set(["Button", "IconButton", "Link", "NavLink"]);
2599
+ var INPUT_COMPONENTS2 = /* @__PURE__ */ new Set(["Input", "TextField", "TextArea", "Textarea"]);
2600
+ var LIST_COMPONENTS2 = /* @__PURE__ */ new Set(["List", "Table", "DataGrid", "DataTable"]);
2601
+ var MODAL_COMPONENTS2 = /* @__PURE__ */ new Set(["Modal", "Dialog", "Drawer", "Popover", "BottomSheet"]);
2602
+ var ARIA_ROLE_TO_SEMANTIC = {
2603
+ button: "button",
2604
+ link: "button",
2605
+ tab: "button",
2606
+ menuitem: "button",
2607
+ textbox: "input",
2608
+ searchbox: "input",
2609
+ spinbutton: "input",
2610
+ listbox: "select",
2611
+ combobox: "select",
2612
+ radiogroup: "select",
2613
+ radio: "select",
2614
+ option: "select",
2615
+ checkbox: "toggle",
2616
+ switch: "toggle",
2617
+ dialog: "modal",
2618
+ alertdialog: "modal",
2619
+ list: "list",
2620
+ table: "list",
2621
+ grid: "list",
2622
+ form: "view"
2623
+ };
2624
+ var INPUT_TYPE_TO_SEMANTIC = {
2625
+ checkbox: "toggle",
2626
+ radio: "select",
2627
+ date: "date",
2628
+ "datetime-local": "date",
2629
+ month: "date",
2630
+ week: "date",
2631
+ time: "date",
2632
+ submit: "button",
2633
+ button: "button",
2634
+ reset: "button",
2635
+ image: "button",
2636
+ hidden: "custom",
2637
+ range: "input",
2638
+ file: "input",
2639
+ color: "input"
2640
+ };
2641
+ function classifyWebJsxComponent(name, element) {
2642
+ if (element) {
2643
+ const role = getStringAttr(element, "role");
2644
+ if (role && ARIA_ROLE_TO_SEMANTIC[role]) return ARIA_ROLE_TO_SEMANTIC[role];
2645
+ }
2646
+ if (/^[a-z]/.test(name)) {
2647
+ if (name === "input") {
2648
+ const type = element ? getStringAttr(element, "type") : void 0;
2649
+ if (type && INPUT_TYPE_TO_SEMANTIC[type]) return INPUT_TYPE_TO_SEMANTIC[type];
2650
+ return "input";
2651
+ }
2652
+ if (INPUT_TAGS.has(name)) return "input";
2653
+ if (name === "select") return "select";
2654
+ if (BUTTON_TAGS.has(name)) return "button";
2655
+ if (name === "a") {
2656
+ if (element && (hasJsxAttribute(element, "href") || hasJsxAttribute(element, "onClick"))) {
2657
+ return "button";
2658
+ }
2659
+ return "view";
2660
+ }
2661
+ if (LIST_TAGS.has(name)) return "list";
2662
+ if (MODAL_TAGS.has(name)) return "modal";
2663
+ if (VIEW_TAGS.has(name)) return "view";
2664
+ return "custom";
2665
+ }
2666
+ if (LIST_COMPONENTS2.has(name)) return "list";
2667
+ if (MODAL_COMPONENTS2.has(name)) return "modal";
2668
+ if (/date/i.test(name)) return "date";
2669
+ if (/(select|picker|dropdown|radio)/i.test(name)) return "select";
2670
+ if (/(checkbox|switch|toggle)/i.test(name)) return "toggle";
2671
+ if (INPUT_COMPONENTS2.has(name)) return "input";
2672
+ if (BUTTON_COMPONENTS2.has(name)) return "button";
2673
+ if (element) {
2674
+ const hasOptions = hasJsxAttribute(element, "options");
2675
+ const hasValue = hasJsxAttribute(element, "value");
2676
+ const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
2677
+ const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange");
2678
+ const hasOnClick = hasJsxAttribute(element, "onClick");
2679
+ if (hasOptions && (hasValue || hasOnChange)) return "select";
2680
+ if (hasChecked && hasOnChange) return "toggle";
2681
+ if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
2682
+ return "input";
2683
+ }
2684
+ if (hasOnClick) return "button";
2685
+ if (getStringAttr(element, "open") || getExpressionIdentifierAttr(element, "open") || getExpressionIdentifierAttr(element, "isOpen")) {
2686
+ return "modal";
2687
+ }
2688
+ }
2689
+ return "custom";
2690
+ }
2691
+ function getJsxElementName(openingElement) {
2692
+ return getJsxName(openingElement.name);
2693
+ }
2694
+ function getJsxName(name) {
2695
+ if (BabelTypes.isJSXIdentifier(name)) return name.name;
2696
+ if (BabelTypes.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
2697
+ if (BabelTypes.isJSXMemberExpression(name)) {
2698
+ const objectName = getJsxName(name.object);
2699
+ const propertyName = BabelTypes.isJSXIdentifier(name.property) ? name.property.name : null;
2700
+ return objectName && propertyName ? `${objectName}.${propertyName}` : null;
2701
+ }
2702
+ return null;
2703
+ }
2704
+ var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
2705
+ function staticRoutePath(node) {
2706
+ if (!node) return void 0;
2707
+ if (BabelTypes.isStringLiteral(node)) return node.value;
2708
+ if (BabelTypes.isTemplateLiteral(node)) {
2709
+ let path9 = "";
2710
+ node.quasis.forEach((quasi, index) => {
2711
+ path9 += quasi.value.cooked ?? quasi.value.raw;
2712
+ const expr = node.expressions[index];
2713
+ if (expr) path9 += `:${paramNameOf(expr)}`;
2714
+ });
2715
+ return path9;
2716
+ }
2717
+ return void 0;
2718
+ }
2719
+ function paramNameOf(expr) {
2720
+ if (BabelTypes.isIdentifier(expr)) return expr.name;
2721
+ if (BabelTypes.isMemberExpression(expr) && BabelTypes.isIdentifier(expr.property)) return expr.property.name;
2722
+ return "param";
2723
+ }
2724
+ function extractWebNavigationCalls(ast) {
2725
+ const calls = [];
2726
+ const inspect = (node) => {
2727
+ if (BabelTypes.isCallExpression(node)) {
2728
+ if (BabelTypes.isIdentifier(node.callee) && node.callee.name === "navigate") {
2729
+ const first = node.arguments[0];
2730
+ const targetPath = staticRoutePath(first);
2731
+ if (targetPath !== void 0) {
2732
+ calls.push({
2733
+ method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
2734
+ targetPath
2735
+ });
2736
+ } else if (BabelTypes.isNumericLiteral(first) || BabelTypes.isUnaryExpression(first) && first.operator === "-") {
2737
+ calls.push({ method: "goBack" });
2738
+ }
2739
+ return;
2740
+ }
2741
+ if (BabelTypes.isMemberExpression(node.callee) && BabelTypes.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && BabelTypes.isIdentifier(node.callee.property)) {
2742
+ const method = node.callee.property.name;
2743
+ const targetPath = staticRoutePath(node.arguments[0]);
2744
+ if ((method === "push" || method === "navigate") && targetPath !== void 0) {
2745
+ calls.push({ method: "navigate", targetPath });
2746
+ } else if (method === "replace" && targetPath !== void 0) {
2747
+ calls.push({ method: "replace", targetPath });
2748
+ } else if (method === "back" || method === "goBack") {
2749
+ calls.push({ method: "goBack" });
2750
+ }
2751
+ }
2752
+ return;
2753
+ }
2754
+ if (BabelTypes.isAssignmentExpression(node) && BabelTypes.isMemberExpression(node.left) && BabelTypes.isIdentifier(node.left.property) && node.left.property.name === "href" && isLocationExpression(node.left.object) && BabelTypes.isStringLiteral(node.right) && node.right.value.startsWith("/")) {
2755
+ calls.push({ method: "navigate", targetPath: node.right.value });
2756
+ }
2757
+ };
2758
+ traverse4(ast, {
2759
+ noScope: !BabelTypes.isFile(ast),
2760
+ enter: (nodePath) => inspect(nodePath.node)
2761
+ });
2762
+ return calls;
2763
+ }
2764
+ function hasReplaceOption(arg) {
2765
+ if (!arg || !BabelTypes.isObjectExpression(arg)) return false;
2766
+ return arg.properties.some(
2767
+ (prop) => BabelTypes.isObjectProperty(prop) && BabelTypes.isIdentifier(prop.key) && prop.key.name === "replace" && BabelTypes.isBooleanLiteral(prop.value) && prop.value.value === true
2768
+ );
2769
+ }
2770
+ function isLocationExpression(node) {
2771
+ if (BabelTypes.isIdentifier(node)) return node.name === "location";
2772
+ return BabelTypes.isMemberExpression(node) && BabelTypes.isIdentifier(node.object) && node.object.name === "window" && BabelTypes.isIdentifier(node.property) && node.property.name === "location";
2773
+ }
2774
+
2775
+ // src/analyzers/web/WebScreenAnalyzer.ts
2776
+ var DEFAULT_WEB_SCREEN_PATTERNS = [
2777
+ "**/pages/**/*.{ts,tsx,js,jsx}",
2778
+ "**/routes/**/*.{ts,tsx,js,jsx}",
2779
+ "**/views/**/*.{ts,tsx,js,jsx}",
2780
+ "**/app/**/*.{ts,tsx,js,jsx}",
2781
+ "**/*Page.{ts,tsx,js,jsx}",
2782
+ "**/*Screen.{ts,tsx,js,jsx}",
2783
+ "**/*View.{ts,tsx,js,jsx}"
2784
+ ];
2785
+ var DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
2786
+ var LISTISH_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
2787
+ var WebScreenAnalyzer = class {
2788
+ config;
2789
+ verbose = process.env.VERBOSE === "true";
2790
+ screenPatterns;
2791
+ constructor(config, options) {
2792
+ this.config = config;
2793
+ this.screenPatterns = options?.screenPatterns ?? DEFAULT_WEB_SCREEN_PATTERNS;
2794
+ }
2795
+ async analyze() {
2796
+ const {
2797
+ include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
2798
+ exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
2799
+ } = this.config;
2800
+ const files = (await glob(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
2801
+ (file) => !file.endsWith(".d.ts")
2802
+ );
2803
+ const patternMatches = await glob(this.screenPatterns, {
2804
+ cwd: this.config.rootDir,
2805
+ ignore: exclude
2806
+ });
2807
+ const patternSet = new Set(patternMatches.map((f) => path8__default.resolve(this.config.rootDir, f)));
2808
+ const candidates = [];
2809
+ for (const file of files) {
2810
+ const filePath = path8__default.resolve(this.config.rootDir, file);
2811
+ try {
2812
+ const candidate = await this.analyzeFile(filePath);
2813
+ if (!candidate) continue;
2814
+ candidate.matchesScreenPattern = patternSet.has(filePath);
2815
+ candidates.push(candidate);
2816
+ if (this.verbose) {
2817
+ console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file})`);
2818
+ }
2819
+ } catch (error) {
2820
+ if (this.verbose) {
2821
+ console.warn(
2822
+ `[WebScreenAnalyzer] Failed to parse ${file}:`,
2823
+ error instanceof Error ? error.message : error
2824
+ );
2825
+ }
2826
+ }
2827
+ }
2828
+ return { candidates, analyzedFiles: files.length };
2829
+ }
2830
+ async analyzeFile(filePath) {
2831
+ const source = await fs.readFile(filePath, "utf-8");
2832
+ const ast = parseSource(source, this.config.parserPlugins);
2833
+ const registerScreenMeta = this.extractRegisterScreenMetadata(ast);
2834
+ const componentName = this.extractComponentName(ast);
2835
+ const labelsByHtmlFor = this.collectHtmlForLabels(ast);
2836
+ const handlerBehaviors = this.collectWebHandlerBehaviors(ast);
2837
+ const forms = this.mergeForms(
2838
+ registerScreenMeta?.forms ?? [],
2839
+ this.extractForms(ast, labelsByHtmlFor)
2840
+ );
2841
+ const actions = this.extractActions(ast, registerScreenMeta, handlerBehaviors);
2842
+ const components = this.extractComponents(ast);
2843
+ const collections = this.extractCollections(ast);
2844
+ const navigationTargets = this.extractNavigationTargets(ast);
2845
+ const permissionsFromJsDoc = extractPermissionsFromJsDoc(source);
2846
+ const destructiveTags = extractDestructiveJsDocTargets(source);
2847
+ if (destructiveTags) {
2848
+ for (const action of actions) {
2849
+ if (destructiveTags === "*" || destructiveTags.has(action.id)) {
2850
+ action.destructive = true;
2851
+ }
2852
+ }
2853
+ }
2854
+ const name = registerScreenMeta?.name || componentName || path8__default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
2855
+ const descriptor = {
2856
+ name,
2857
+ filePath,
2858
+ title: registerScreenMeta?.title,
2859
+ description: registerScreenMeta?.description,
2860
+ components,
2861
+ forms,
2862
+ actions,
2863
+ navigationTargets,
2864
+ ...collections.length > 0 ? { collections } : {},
2865
+ ...registerScreenMeta?.suggestedPrompts && registerScreenMeta.suggestedPrompts.length > 0 ? { suggestedPrompts: registerScreenMeta.suggestedPrompts } : {},
2866
+ ...permissionsFromJsDoc ? {
2867
+ permissions: permissionsFromJsDoc,
2868
+ ...permissionsFromJsDoc.isPii ? { isPii: true } : {}
2869
+ } : {}
2870
+ };
2871
+ return {
2872
+ descriptor,
2873
+ hasRegisterScreen: registerScreenMeta !== null || this.detectRegisterScreenCall(ast),
2874
+ matchesScreenPattern: false
2875
+ };
2876
+ }
2877
+ // ── registerScreen ────────────────────────────────────────────────
2878
+ detectRegisterScreenCall(ast) {
2879
+ let found = false;
2880
+ traverse4(ast, {
2881
+ CallExpression: (nodePath) => {
2882
+ if (found) return;
2883
+ if (isRegisterScreenCallee(nodePath.node.callee)) {
2884
+ found = true;
2885
+ nodePath.stop();
2886
+ }
2887
+ }
2888
+ });
2889
+ return found;
2890
+ }
2891
+ extractRegisterScreenMetadata(ast) {
2892
+ let plain = null;
2893
+ traverse4(ast, {
2894
+ CallExpression: (nodePath) => {
2895
+ if (!isRegisterScreenCallee(nodePath.node.callee)) return;
2896
+ const arg = nodePath.node.arguments[0];
2897
+ if (BabelTypes.isObjectExpression(arg)) {
2898
+ plain = literalToPlain(arg);
2899
+ }
2900
+ }
2901
+ });
2902
+ if (!plain) return null;
2903
+ const meta = plain;
2904
+ const result = {
2905
+ name: typeof meta.name === "string" ? meta.name : "",
2906
+ actions: [],
2907
+ forms: [],
2908
+ navigationTargets: [],
2909
+ components: []
2910
+ };
2911
+ if (typeof meta.title === "string") result.title = meta.title;
2912
+ if (typeof meta.description === "string") result.description = meta.description;
2913
+ if (Array.isArray(meta.suggestedPrompts)) {
2914
+ const prompts = meta.suggestedPrompts.filter((p) => typeof p === "string").map((p) => p.trim()).filter((p) => p.length > 0);
2915
+ if (prompts.length > 0) result.suggestedPrompts = prompts;
2916
+ }
2917
+ if (Array.isArray(meta.actions)) {
2918
+ result.actions = meta.actions.filter((a) => typeof a === "object" && a !== null).filter((a) => typeof a.id === "string" && a.id.length > 0).map((a) => ({ type: "custom", ...a }));
2919
+ }
2920
+ if (Array.isArray(meta.fields)) {
2921
+ const fields = meta.fields.filter((f) => typeof f === "object" && f !== null).filter((f) => typeof f.id === "string" && f.id.length > 0).map(
2922
+ (f) => ({
2923
+ name: f.id,
2924
+ type: typeof f.type === "string" ? f.type : "text",
2925
+ required: f.required === true,
2926
+ ...typeof f.label === "string" ? { label: f.label } : {},
2927
+ ...typeof f.placeholder === "string" ? { placeholder: f.placeholder } : {},
2928
+ ...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
2929
+ ...Array.isArray(f.options) ? { options: f.options } : {}
2930
+ })
2931
+ );
2932
+ if (fields.length > 0) result.forms = [{ id: "default", fields }];
2933
+ }
2934
+ return result;
2935
+ }
2936
+ // ── Component name / structure ────────────────────────────────────
2937
+ /** Default-exported component name, else the first exported capitalized function. */
2938
+ extractComponentName(ast) {
2939
+ let defaultName = "";
2940
+ let firstExported = "";
2941
+ traverse4(ast, {
2942
+ ExportDefaultDeclaration: (nodePath) => {
2943
+ const declaration = nodePath.node.declaration;
2944
+ if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id?.name) {
2945
+ defaultName = declaration.id.name;
2946
+ } else if (BabelTypes.isIdentifier(declaration)) {
2947
+ defaultName = declaration.name;
2948
+ }
2949
+ },
2950
+ ExportNamedDeclaration: (nodePath) => {
2951
+ if (firstExported) return;
2952
+ const declaration = nodePath.node.declaration;
2953
+ if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
2954
+ firstExported = declaration.id.name;
2955
+ } else if (BabelTypes.isVariableDeclaration(declaration)) {
2956
+ for (const declarator of declaration.declarations) {
2957
+ if (BabelTypes.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (BabelTypes.isArrowFunctionExpression(declarator.init) || BabelTypes.isFunctionExpression(declarator.init))) {
2958
+ firstExported = declarator.id.name;
2959
+ break;
2960
+ }
2961
+ }
2962
+ }
2963
+ }
2964
+ });
2965
+ return defaultName || firstExported;
2966
+ }
2967
+ extractComponents(ast) {
2968
+ const components = [];
2969
+ const seen = /* @__PURE__ */ new Set();
2970
+ traverse4(ast, {
2971
+ JSXOpeningElement: (nodePath) => {
2972
+ const element = nodePath.node;
2973
+ const name = getJsxElementName(element);
2974
+ if (!name || seen.has(name)) return;
2975
+ seen.add(name);
2976
+ const role = classifyWebJsxComponent(name, element);
2977
+ const type = role === "select" || role === "toggle" || role === "date" ? "input" : role === "input" || role === "button" || role === "list" || role === "modal" || role === "view" ? role : "custom";
2978
+ const component = { name, type };
2979
+ const testId = getStringAttr(element, "data-testid") ?? getStringAttr(element, "testID");
2980
+ const ariaLabel = getStringAttr(element, "aria-label");
2981
+ if (testId) component.testID = testId;
2982
+ if (ariaLabel) component.accessibilityLabel = ariaLabel;
2983
+ components.push(component);
2984
+ }
2985
+ });
2986
+ return components;
2987
+ }
2988
+ // ── <label htmlFor> association ───────────────────────────────────
2989
+ collectHtmlForLabels(ast) {
2990
+ const labels = /* @__PURE__ */ new Map();
2991
+ traverse4(ast, {
2992
+ JSXElement: (nodePath) => {
2993
+ const element = nodePath.node;
2994
+ if (getJsxElementName(element.openingElement) !== "label") return;
2995
+ const htmlFor = getStringAttr(element.openingElement, "htmlFor");
2996
+ if (!htmlFor) return;
2997
+ const text = jsxTextContent(element);
2998
+ if (text) labels.set(htmlFor, text);
2999
+ }
3000
+ });
3001
+ return labels;
3002
+ }
3003
+ // ── Forms ─────────────────────────────────────────────────────────
3004
+ extractForms(ast, labelsByHtmlFor) {
3005
+ const formBuckets = /* @__PURE__ */ new Map();
3006
+ const usedIds = /* @__PURE__ */ new Set();
3007
+ let formCount = 0;
3008
+ const uniqueId = (preferred) => {
3009
+ if (!usedIds.has(preferred)) {
3010
+ usedIds.add(preferred);
3011
+ return preferred;
3012
+ }
3013
+ let suffix = 2;
3014
+ while (usedIds.has(`${preferred}-${suffix}`)) suffix += 1;
3015
+ const id = `${preferred}-${suffix}`;
3016
+ usedIds.add(id);
3017
+ return id;
3018
+ };
3019
+ const bucketFor = (formElement) => {
3020
+ let bucket = formBuckets.get(formElement);
3021
+ if (bucket) return bucket;
3022
+ formCount += 1;
3023
+ let preferred = "default";
3024
+ let submitAction;
3025
+ if (formElement) {
3026
+ const opening = formElement.openingElement;
3027
+ preferred = getStringAttr(opening, "id") ?? getStringAttr(opening, "name") ?? getStringAttr(opening, "data-testid") ?? (formCount === 1 ? "default" : `form-${formCount}`);
3028
+ submitAction = this.handlerNameFromAttr(opening, "onSubmit");
3029
+ }
3030
+ bucket = { id: uniqueId(preferred), fields: /* @__PURE__ */ new Map(), submitAction };
3031
+ formBuckets.set(formElement, bucket);
3032
+ return bucket;
3033
+ };
3034
+ traverse4(ast, {
3035
+ JSXElement: (nodePath) => {
3036
+ const element = nodePath.node;
3037
+ const name = getJsxElementName(element.openingElement);
3038
+ if (!name) return;
3039
+ const role = classifyWebJsxComponent(name, element.openingElement);
3040
+ if (!["input", "select", "toggle", "date"].includes(role)) return;
3041
+ if (name === "option") return;
3042
+ const field = this.extractField(element, role, labelsByHtmlFor);
3043
+ if (!field.name) return;
3044
+ const formParent = nodePath.findParent(
3045
+ (p) => p.isJSXElement() && (getJsxElementName(p.node.openingElement) === "form" || getStringAttr(p.node.openingElement, "role") === "form")
3046
+ );
3047
+ const bucket = bucketFor(formParent ? formParent.node : null);
3048
+ if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
3049
+ }
3050
+ });
3051
+ traverse4(ast, {
3052
+ JSXElement: (nodePath) => {
3053
+ const element = nodePath.node;
3054
+ const name = getJsxElementName(element.openingElement);
3055
+ if (!name) return;
3056
+ if (!this.isSubmitElement(name, element.openingElement)) return;
3057
+ const formParent = nodePath.findParent(
3058
+ (p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
3059
+ );
3060
+ if (!formParent) return;
3061
+ const bucket = formBuckets.get(formParent.node);
3062
+ if (!bucket) return;
3063
+ const handler = this.handlerNameFromAttr(element.openingElement, "onClick");
3064
+ if (!bucket.submitAction && handler) bucket.submitAction = handler;
3065
+ }
3066
+ });
3067
+ return Array.from(formBuckets.values()).filter((bucket) => bucket.fields.size > 0).map((bucket) => ({
3068
+ id: bucket.id,
3069
+ fields: Array.from(bucket.fields.values()),
3070
+ ...bucket.submitAction && bucket.submitAction !== "anonymous" ? { submitAction: bucket.submitAction } : {}
3071
+ }));
3072
+ }
3073
+ isSubmitElement(name, opening) {
3074
+ const type = getStringAttr(opening, "type");
3075
+ if (name === "button") return type === "submit" || type === void 0;
3076
+ if (name === "input") return type === "submit";
3077
+ return false;
3078
+ }
3079
+ extractField(element, role, labelsByHtmlFor) {
3080
+ const opening = element.openingElement;
3081
+ const componentName = getJsxElementName(opening) ?? void 0;
3082
+ const field = {
3083
+ name: "",
3084
+ type: "text",
3085
+ required: false,
3086
+ sourceComponent: componentName
3087
+ };
3088
+ const appilotsId = getStringAttr(opening, "appilotsId");
3089
+ const dataTestId = getStringAttr(opening, "data-testid");
3090
+ const domId = getStringAttr(opening, "id");
3091
+ const nameAttr = getStringAttr(opening, "name");
3092
+ const ariaLabel = getStringAttr(opening, "aria-label");
3093
+ const placeholder = getStringAttr(opening, "placeholder");
3094
+ if (placeholder) field.placeholder = placeholder;
3095
+ if (ariaLabel) field.label = ariaLabel;
3096
+ if (domId && labelsByHtmlFor.has(domId)) field.label = labelsByHtmlFor.get(domId);
3097
+ if (appilotsId) {
3098
+ field.name = appilotsId;
3099
+ field.locator = mergeLocator(field.locator, {
3100
+ id: appilotsId,
3101
+ appilotsId,
3102
+ source: "appilotsId"
3103
+ });
3104
+ }
3105
+ if (dataTestId) {
3106
+ if (!field.name) field.name = dataTestId.replace(/^(input-|field-|txt-)/, "");
3107
+ field.locator = mergeLocator(field.locator, {
3108
+ ...field.locator?.id ? {} : { id: dataTestId },
3109
+ testID: dataTestId,
3110
+ source: field.locator?.source ?? "data-testid"
3111
+ });
3112
+ }
3113
+ if (nameAttr && !field.name) field.name = nameAttr;
3114
+ if (domId) {
3115
+ if (!field.name) field.name = domId;
3116
+ field.locator = mergeLocator(field.locator, {
3117
+ ...field.locator?.id ? {} : { id: domId },
3118
+ source: field.locator?.source ?? "id"
3119
+ });
3120
+ }
3121
+ if (ariaLabel) {
3122
+ field.locator = mergeLocator(field.locator, {
3123
+ ...field.locator?.id ? {} : { id: slugify(ariaLabel) },
3124
+ accessibilityLabel: ariaLabel,
3125
+ source: field.locator?.source ?? "aria-label"
3126
+ });
3127
+ }
3128
+ for (const attr of opening.attributes) {
3129
+ if (!BabelTypes.isJSXAttribute(attr) || !BabelTypes.isJSXIdentifier(attr.name)) continue;
3130
+ const attrName = attr.name.name;
3131
+ if (attrName === "required") {
3132
+ if (attr.value === null) field.required = true;
3133
+ else if (BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isBooleanLiteral(attr.value.expression)) {
3134
+ field.required = attr.value.expression.value;
3135
+ }
3136
+ }
3137
+ if ((attrName === "value" || attrName === "checked") && attr.value && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
3138
+ field.valueBinding = attr.value.expression.name;
3139
+ if (!field.name) field.name = attr.value.expression.name;
3140
+ }
3141
+ }
3142
+ if (getStringAttr(opening, "aria-required") === "true") field.required = true;
3143
+ if (role === "select") field.type = "select";
3144
+ else if (role === "toggle") field.type = "toggle";
3145
+ else if (role === "date") field.type = "date";
3146
+ else {
3147
+ field.type = this.inferInputType(opening, field);
3148
+ }
3149
+ if (componentName === "select") {
3150
+ const options = this.extractSelectOptions(element);
3151
+ if (options.length > 0) field.options = options;
3152
+ }
3153
+ if (!field.name || isWeakInferredFieldName(field.name)) {
3154
+ const labelish = field.label ?? field.placeholder;
3155
+ if (labelish) field.name = slugify(labelish);
3156
+ }
3157
+ if (!field.locator && field.name) {
3158
+ field.locator = { id: field.name, label: field.label, source: "inferred" };
3159
+ } else if (field.locator && !field.locator.id && field.name) {
3160
+ field.locator = mergeLocator(field.locator, {
3161
+ id: field.name,
3162
+ label: field.label,
3163
+ source: field.locator.source ?? "inferred"
3164
+ });
3165
+ }
3166
+ return field;
3167
+ }
3168
+ inferInputType(opening, field) {
3169
+ const type = getStringAttr(opening, "type");
3170
+ if (type === "email") return "email";
3171
+ if (type === "tel") return "phone";
3172
+ if (type === "number") return "number";
3173
+ if (type === "date" || type === "datetime-local" || type === "month" || type === "week")
3174
+ return "date";
3175
+ const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
3176
+ if (inputMode === "email") return "email";
3177
+ if (inputMode === "tel") return "phone";
3178
+ if (inputMode === "numeric" || inputMode === "decimal") return "number";
3179
+ const combined = `${field.name} ${field.label ?? ""} ${field.placeholder ?? ""}`.toLowerCase();
3180
+ if (combined.includes("email")) return "email";
3181
+ if (combined.includes("phone") || combined.includes("tel")) return "phone";
3182
+ return "text";
3183
+ }
3184
+ extractSelectOptions(selectElement) {
3185
+ const options = [];
3186
+ for (const child of selectElement.children) {
3187
+ if (!BabelTypes.isJSXElement(child)) continue;
3188
+ if (getJsxElementName(child.openingElement) !== "option") continue;
3189
+ const value = getStringAttr(child.openingElement, "value");
3190
+ const label = jsxTextContent(child) || value || "";
3191
+ if (label && value) options.push({ label, value });
3192
+ }
3193
+ return options;
3194
+ }
3195
+ mergeForms(primary, secondary) {
3196
+ const out = primary.map((form) => ({ ...form, fields: [...form.fields] }));
3197
+ for (const form of secondary) {
3198
+ const existing = out.find((candidate) => candidate.id === form.id);
3199
+ if (!existing) {
3200
+ out.push({ ...form, fields: [...form.fields] });
3201
+ continue;
3202
+ }
3203
+ for (const field of form.fields) {
3204
+ const existingField = existing.fields.find((candidate) => candidate.name === field.name);
3205
+ if (!existingField) {
3206
+ existing.fields.push(field);
3207
+ continue;
3208
+ }
3209
+ existingField.label = existingField.label ?? field.label;
3210
+ existingField.placeholder = existingField.placeholder ?? field.placeholder;
3211
+ existingField.options = existingField.options ?? field.options;
3212
+ existingField.locator = existingField.locator ?? field.locator;
3213
+ existingField.sourceComponent = existingField.sourceComponent ?? field.sourceComponent;
3214
+ existingField.valueBinding = existingField.valueBinding ?? field.valueBinding;
3215
+ existingField.required = existingField.required || field.required;
3216
+ if (existingField.type === "text" && field.type !== "text") existingField.type = field.type;
3217
+ }
3218
+ existing.submitAction = existing.submitAction ?? form.submitAction;
3219
+ }
3220
+ return out;
3221
+ }
3222
+ // ── Actions ───────────────────────────────────────────────────────
3223
+ extractActions(ast, registerScreenMeta, handlerBehaviors) {
3224
+ const actions = [...registerScreenMeta?.actions ?? []];
3225
+ const actionIds = new Set(actions.map((a) => a.id));
3226
+ const actionLabels = new Map(
3227
+ actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
3228
+ );
3229
+ traverse4(ast, {
3230
+ JSXElement: (nodePath) => {
3231
+ const element = nodePath.node;
3232
+ const name = getJsxElementName(element.openingElement);
3233
+ if (!name) return;
3234
+ if (classifyWebJsxComponent(name, element.openingElement) !== "button") return;
3235
+ const action = this.extractActionFromElement(element, nodePath);
3236
+ if (!action) return;
3237
+ const existingByLabel = action.label ? actionLabels.get(normalizeLabel(action.label)) : void 0;
3238
+ if (existingByLabel) {
3239
+ this.mergeActionMetadata(existingByLabel, action);
3240
+ return;
3241
+ }
3242
+ if (action.id && !actionIds.has(action.id)) {
3243
+ actions.push(action);
3244
+ actionIds.add(action.id);
3245
+ if (action.label) actionLabels.set(normalizeLabel(action.label), action);
3246
+ }
3247
+ }
3248
+ });
3249
+ this.enrichActionsFromHandlers(actions, handlerBehaviors);
3250
+ return actions;
3251
+ }
3252
+ extractActionFromElement(element, nodePath) {
3253
+ const opening = element.openingElement;
3254
+ const componentName = getJsxElementName(opening) ?? void 0;
3255
+ const action = { id: "", type: "custom", sourceComponent: componentName };
3256
+ const ariaLabel = getStringAttr(opening, "aria-label");
3257
+ const label = ariaLabel ?? jsxTextContent(element) ?? getStringAttr(opening, "value") ?? getStringAttr(opening, "title");
3258
+ if (label) action.label = label;
3259
+ if (ariaLabel) {
3260
+ action.locator = mergeLocator(action.locator, {
3261
+ accessibilityLabel: ariaLabel,
3262
+ source: "aria-label"
3263
+ });
3264
+ }
3265
+ const appilotsId = getStringAttr(opening, "appilotsId");
3266
+ const dataTestId = getStringAttr(opening, "data-testid");
3267
+ const domId = getStringAttr(opening, "id");
3268
+ if (appilotsId) {
3269
+ action.id = appilotsId;
3270
+ action.locator = mergeLocator(action.locator, {
3271
+ id: appilotsId,
3272
+ appilotsId,
3273
+ source: "appilotsId"
3274
+ });
3275
+ } else if (dataTestId) {
3276
+ action.id = dataTestId;
3277
+ action.locator = mergeLocator(action.locator, {
3278
+ id: dataTestId,
3279
+ testID: dataTestId,
3280
+ source: "data-testid"
3281
+ });
3282
+ } else if (domId) {
3283
+ action.id = domId;
3284
+ action.locator = mergeLocator(action.locator, { id: domId, source: "id" });
3285
+ } else if (action.label) {
3286
+ action.id = slugify(action.label);
3287
+ }
3288
+ if (!action.id) return null;
3289
+ const to = routePathAttr(opening, "to") ?? routePathAttr(opening, "href");
3290
+ if (to && to.startsWith("/")) {
3291
+ action.type = "navigation";
3292
+ action.targetScreen = to;
3293
+ } else if (to && !to.startsWith("/") && !hasJsxAttribute(opening, "onClick")) {
3294
+ return null;
3295
+ }
3296
+ let handlerName;
3297
+ const onClickAttr = opening.attributes.find(
3298
+ (attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
3299
+ );
3300
+ if (onClickAttr?.value && BabelTypes.isJSXExpressionContainer(onClickAttr.value)) {
3301
+ const expr = onClickAttr.value.expression;
3302
+ if (BabelTypes.isIdentifier(expr)) {
3303
+ handlerName = expr.name;
3304
+ action.handler = expr.name;
3305
+ } else if (!BabelTypes.isJSXEmptyExpression(expr)) {
3306
+ const inlineNavCalls = extractWebNavigationCalls(expr);
3307
+ const inlineNav = inlineNavCalls.find((call) => call.targetPath);
3308
+ if (inlineNav?.targetPath) {
3309
+ action.type = "navigation";
3310
+ action.targetScreen = inlineNav.targetPath;
3311
+ } else if (inlineNavCalls.some((call) => call.method === "goBack")) {
3312
+ action.type = "navigation";
3313
+ action.successSignal = {
3314
+ type: "goBack",
3315
+ description: "Action returns to the previous page"
3316
+ };
3317
+ action.appilotsInferred = {
3318
+ ...action.appilotsInferred ?? {},
3319
+ expectedOutcome: "navigation"
3320
+ };
3321
+ }
3322
+ const inlineHandler = firstCalledFunctionName(expr);
3323
+ if (inlineHandler) {
3324
+ handlerName = inlineHandler;
3325
+ action.handler = inlineHandler;
3326
+ }
3327
+ }
3328
+ }
3329
+ if (handlerName) {
3330
+ const lower = handlerName.toLowerCase();
3331
+ if (lower.includes("submit")) action.type = "submit";
3332
+ else if (lower.includes("navigate") && action.type === "custom") action.type = "navigation";
3333
+ }
3334
+ if (componentName && this.isSubmitElement(componentName, opening)) {
3335
+ const formParent = nodePath.findParent(
3336
+ (p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
3337
+ );
3338
+ if (formParent) {
3339
+ action.type = "submit";
3340
+ if (!action.handler) {
3341
+ const formHandler = this.handlerNameFromAttr(formParent.node.openingElement, "onSubmit");
3342
+ if (formHandler && formHandler !== "anonymous") {
3343
+ action.handler = formHandler;
3344
+ handlerName = formHandler;
3345
+ }
3346
+ }
3347
+ }
3348
+ }
3349
+ if (hasJsxAttribute(opening, "destructive") || hasJsxAttribute(opening, "aria-destructive")) {
3350
+ const value = getStringAttr(opening, "destructive") ?? getStringAttr(opening, "aria-destructive");
3351
+ action.destructive = value !== "false";
3352
+ } else if (handlerName && DESTRUCTIVE_VERB2.test(handlerName) || DESTRUCTIVE_VERB2.test(action.id)) {
3353
+ action.destructive = true;
3354
+ }
3355
+ if (!action.locator && action.id) {
3356
+ action.locator = {
3357
+ id: action.id,
3358
+ label: action.label,
3359
+ source: action.label ? "label" : "inferred"
3360
+ };
3361
+ }
3362
+ return action;
3363
+ }
3364
+ mergeActionMetadata(target, source) {
3365
+ target.handler = target.handler ?? source.handler;
3366
+ target.targetScreen = target.targetScreen ?? source.targetScreen;
3367
+ target.description = target.description ?? source.description;
3368
+ target.locator = target.locator ?? source.locator;
3369
+ target.nativeConfirmationExpected = target.nativeConfirmationExpected || source.nativeConfirmationExpected || void 0;
3370
+ target.requiresConfirmation = target.requiresConfirmation || source.requiresConfirmation || void 0;
3371
+ target.destructive = target.destructive || source.destructive || void 0;
3372
+ target.effect = target.effect ?? source.effect;
3373
+ target.riskLevel = target.riskLevel ?? source.riskLevel;
3374
+ target.appilotsInferred = target.appilotsInferred ?? source.appilotsInferred;
3375
+ }
3376
+ enrichActionsFromHandlers(actions, behaviors) {
3377
+ for (const action of actions) {
3378
+ const behavior = action.handler ? behaviors.get(action.handler) : void 0;
3379
+ if (!behavior) continue;
3380
+ action.appilotsInferred = {
3381
+ ...action.appilotsInferred ?? {},
3382
+ ...behavior.base.appilotsInferred
3383
+ };
3384
+ if (behavior.nativeConfirmationExpected || behavior.base.nativeConfirmationExpected) {
3385
+ action.nativeConfirmationExpected = true;
3386
+ }
3387
+ if (behavior.targetPath && !action.targetScreen) {
3388
+ action.targetScreen = behavior.targetPath;
3389
+ if (action.type === "custom") action.type = "navigation";
3390
+ action.appilotsInferred = {
3391
+ ...action.appilotsInferred ?? {},
3392
+ expectedOutcome: "navigation"
3393
+ };
3394
+ }
3395
+ if (behavior.base.successSignal && !action.successSignal) {
3396
+ action.successSignal = behavior.base.successSignal;
3397
+ }
3398
+ if (behavior.base.failureSignal && !action.failureSignal) {
3399
+ action.failureSignal = behavior.base.failureSignal;
3400
+ }
3401
+ if (behavior.base.opensModal && !action.opensModal) {
3402
+ action.opensModal = behavior.base.opensModal;
3403
+ }
3404
+ if (behavior.base.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
3405
+ action.destructive = true;
3406
+ action.effect = action.effect ?? "destructive";
3407
+ action.riskLevel = action.riskLevel ?? "high";
3408
+ action.requiresConfirmation = action.requiresConfirmation ?? true;
3409
+ }
3410
+ }
3411
+ }
3412
+ /**
3413
+ * Handler behavior via the shared, platform-neutral analyzer
3414
+ * (async/await, `.then`, state setters, toasts, destructive verbs)
3415
+ * plus the web-only signals: React Router navigation targets and
3416
+ * `window.confirm(...)` as the native confirmation dialog.
3417
+ */
3418
+ collectWebHandlerBehaviors(ast) {
3419
+ const handlers = collectFunctions(ast);
3420
+ const out = /* @__PURE__ */ new Map();
3421
+ for (const [name, fn] of handlers) {
3422
+ const base = analyzeFunctionBehavior(name, fn, handlers);
3423
+ const navCalls = fn.body ? extractWebNavigationCalls(fn.body) : [];
3424
+ const firstNav = navCalls.find((call) => call.targetPath);
3425
+ const goesBack = navCalls.some((call) => call.method === "goBack");
3426
+ out.set(name, {
3427
+ base: {
3428
+ ...base,
3429
+ ...goesBack && !base.successSignal ? {
3430
+ successSignal: {
3431
+ type: "goBack",
3432
+ description: "Action returns to the previous page"
3433
+ }
3434
+ } : {}
3435
+ },
3436
+ targetPath: firstNav?.targetPath,
3437
+ nativeConfirmationExpected: fn.body ? containsWindowConfirm(fn.body) : false
3438
+ });
3439
+ }
3440
+ return out;
3441
+ }
3442
+ handlerNameFromAttr(opening, attrName) {
3443
+ const attr = opening.attributes.find(
3444
+ (candidate) => BabelTypes.isJSXAttribute(candidate) && BabelTypes.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
3445
+ );
3446
+ if (!attr?.value || !BabelTypes.isJSXExpressionContainer(attr.value)) return void 0;
3447
+ const expr = attr.value.expression;
3448
+ if (BabelTypes.isIdentifier(expr)) return expr.name;
3449
+ if (BabelTypes.isArrowFunctionExpression(expr) || BabelTypes.isFunctionExpression(expr)) {
3450
+ return firstCalledFunctionName(expr) ?? "anonymous";
3451
+ }
3452
+ return void 0;
3453
+ }
3454
+ // ── Navigation targets (route paths — resolved by the orchestrator) ─
3455
+ extractNavigationTargets(ast) {
3456
+ const targets = /* @__PURE__ */ new Set();
3457
+ for (const call of extractWebNavigationCalls(ast)) {
3458
+ if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
3459
+ }
3460
+ traverse4(ast, {
3461
+ JSXOpeningElement: (nodePath) => {
3462
+ const element = nodePath.node;
3463
+ const name = getJsxElementName(element);
3464
+ if (name === "Link" || name === "NavLink" || name === "Navigate") {
3465
+ const to = routePathAttr(element, "to");
3466
+ if (to && to.startsWith("/")) targets.add(to);
3467
+ }
3468
+ if (name === "a") {
3469
+ const href = routePathAttr(element, "href");
3470
+ if (href && href.startsWith("/")) targets.add(href);
3471
+ }
3472
+ }
3473
+ });
3474
+ return Array.from(targets).sort();
3475
+ }
3476
+ // ── Collections ───────────────────────────────────────────────────
3477
+ extractCollections(ast) {
3478
+ const collections = [];
3479
+ const seen = /* @__PURE__ */ new Set();
3480
+ traverse4(ast, {
3481
+ JSXExpressionContainer: (nodePath) => {
3482
+ const expr = nodePath.node.expression;
3483
+ if (!BabelTypes.isCallExpression(expr) || !BabelTypes.isMemberExpression(expr.callee) || !BabelTypes.isIdentifier(expr.callee.object) || !BabelTypes.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
3484
+ return;
3485
+ }
3486
+ const callback = expr.arguments[0];
3487
+ if (!BabelTypes.isArrowFunctionExpression(callback) && !BabelTypes.isFunctionExpression(callback)) return;
3488
+ const enclosing = nodePath.findParent(
3489
+ (p) => p.isJSXElement()
3490
+ );
3491
+ const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
3492
+ const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
3493
+ const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
3494
+ if (enclosingName === "select") return;
3495
+ const isListContext = enclosingName !== null && LISTISH_TAGS.has(enclosingName) || enclosingRole === "list" || returnsListItem;
3496
+ if (!isListContext) return;
3497
+ const dataSource = expr.callee.object.name;
3498
+ if (seen.has(dataSource)) return;
3499
+ seen.add(dataSource);
3500
+ const itemNames = collectionItemNames(callback);
3501
+ const displayFields = collectionDisplayFields(callback, itemNames);
3502
+ const keyField = collectionKeyField(callback, itemNames);
3503
+ const rowAction = collectionRowAction(callback);
3504
+ const itemType = inferItemType(dataSource, displayFields);
3505
+ const identityFields = inferIdentityFields(keyField, displayFields);
3506
+ collections.push({
3507
+ id: dataSource,
3508
+ component: enclosingName ?? "list",
3509
+ ...itemType ? { itemType } : {},
3510
+ dataSource,
3511
+ ...keyField ? { keyField } : {},
3512
+ ...displayFields.length > 0 ? { displayFields } : {},
3513
+ ...rowAction ? { rowAction, rowActions: [rowAction] } : {},
3514
+ ...identityFields.length > 0 ? { identityFields } : {}
3515
+ });
3516
+ }
3517
+ });
3518
+ return collections;
3519
+ }
3520
+ };
3521
+ function isRegisterScreenCallee(callee) {
3522
+ return BabelTypes.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.property) && callee.property.name === "registerScreen";
3523
+ }
3524
+ function literalToPlain(node) {
3525
+ if (BabelTypes.isStringLiteral(node) || BabelTypes.isNumericLiteral(node) || BabelTypes.isBooleanLiteral(node)) {
3526
+ return node.value;
3527
+ }
3528
+ if (BabelTypes.isNullLiteral(node)) return null;
3529
+ if (BabelTypes.isArrayExpression(node)) {
3530
+ return node.elements.filter((el) => el !== null && BabelTypes.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
3531
+ }
3532
+ if (BabelTypes.isObjectExpression(node)) {
3533
+ const out = {};
3534
+ for (const prop of node.properties) {
3535
+ if (!BabelTypes.isObjectProperty(prop)) continue;
3536
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : void 0;
3537
+ if (!key || !BabelTypes.isExpression(prop.value)) continue;
3538
+ const value = literalToPlain(prop.value);
3539
+ if (value !== void 0) out[key] = value;
3540
+ }
3541
+ return out;
3542
+ }
3543
+ return void 0;
3544
+ }
3545
+ function jsxTextContent(element) {
3546
+ const parts = [];
3547
+ const walk = (children) => {
3548
+ for (const child of children) {
3549
+ if (BabelTypes.isJSXText(child)) {
3550
+ const trimmed = child.value.replace(/\s+/g, " ").trim();
3551
+ if (trimmed) parts.push(trimmed);
3552
+ } else if (BabelTypes.isJSXExpressionContainer(child) && BabelTypes.isStringLiteral(child.expression)) {
3553
+ parts.push(child.expression.value);
3554
+ } else if (BabelTypes.isJSXElement(child)) {
3555
+ walk(child.children);
3556
+ }
3557
+ }
3558
+ };
3559
+ walk(element.children);
3560
+ const text = parts.join(" ").trim();
3561
+ return text.length > 0 ? text : void 0;
3562
+ }
3563
+ function firstCalledFunctionName(node) {
3564
+ if (BabelTypes.isIdentifier(node)) return node.name;
3565
+ if (BabelTypes.isArrowFunctionExpression(node) || BabelTypes.isFunctionExpression(node)) {
3566
+ return firstCalledFunctionName(node.body);
3567
+ }
3568
+ if (BabelTypes.isBlockStatement(node)) {
3569
+ for (const statement of node.body) {
3570
+ const handler = firstCalledFunctionName(statement);
3571
+ if (handler) return handler;
3572
+ }
3573
+ return void 0;
3574
+ }
3575
+ if (BabelTypes.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
3576
+ if (BabelTypes.isReturnStatement(node)) {
3577
+ return node.argument ? firstCalledFunctionName(node.argument) : void 0;
3578
+ }
3579
+ if (BabelTypes.isAwaitExpression(node) || BabelTypes.isUnaryExpression(node)) {
3580
+ return firstCalledFunctionName(node.argument);
3581
+ }
3582
+ if (BabelTypes.isCallExpression(node)) {
3583
+ if (BabelTypes.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
3584
+ return node.callee.name;
3585
+ }
3586
+ return void 0;
3587
+ }
3588
+ return void 0;
3589
+ }
3590
+ function containsWindowConfirm(body) {
3591
+ let found = false;
3592
+ traverse4(
3593
+ body,
3594
+ {
3595
+ noScope: true,
3596
+ CallExpression: (nodePath) => {
3597
+ const callee = nodePath.node.callee;
3598
+ if (BabelTypes.isIdentifier(callee) && callee.name === "confirm") found = true;
3599
+ if (BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.object) && callee.object.name === "window" && BabelTypes.isIdentifier(callee.property) && callee.property.name === "confirm") {
3600
+ found = true;
3601
+ }
3602
+ }
3603
+ }
3604
+ );
3605
+ return found;
3606
+ }
3607
+ function mergeLocator(current, next) {
3608
+ return { ...current ?? {}, ...next };
3609
+ }
3610
+ function routePathAttr(opening, attrName) {
3611
+ const literal = getStringAttr(opening, attrName);
3612
+ if (literal) return literal;
3613
+ const attr = opening.attributes.find(
3614
+ (candidate) => BabelTypes.isJSXAttribute(candidate) && BabelTypes.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
3615
+ );
3616
+ if (!attr?.value || !BabelTypes.isJSXExpressionContainer(attr.value)) return void 0;
3617
+ return staticRoutePath(attr.value.expression);
3618
+ }
3619
+ function slugify(label) {
3620
+ return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3621
+ }
3622
+ function normalizeLabel(label) {
3623
+ return label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
3624
+ }
3625
+ function isWeakInferredFieldName(name) {
3626
+ return /^(text|value|input|query|search|selected|checked)$/i.test(name);
3627
+ }
3628
+ function collectionItemNames(callback) {
3629
+ const names = /* @__PURE__ */ new Set(["item"]);
3630
+ const firstParam = callback.params[0];
3631
+ if (BabelTypes.isIdentifier(firstParam)) names.add(firstParam.name);
3632
+ if (BabelTypes.isObjectPattern(firstParam)) {
3633
+ for (const prop of firstParam.properties) {
3634
+ if (BabelTypes.isObjectProperty(prop) && BabelTypes.isIdentifier(prop.key) && BabelTypes.isIdentifier(prop.value)) {
3635
+ names.add(prop.value.name);
3636
+ }
3637
+ }
3638
+ }
3639
+ return names;
3640
+ }
3641
+ function collectionDisplayFields(callback, itemNames) {
3642
+ const fields = /* @__PURE__ */ new Set();
3643
+ if (!callback.body) return [];
3644
+ traverse4(
3645
+ callback.body,
3646
+ {
3647
+ noScope: true,
3648
+ MemberExpression: (nodePath) => {
3649
+ const node = nodePath.node;
3650
+ if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
3651
+ fields.add(node.property.name);
3652
+ }
3653
+ }
3654
+ }
3655
+ );
3656
+ return Array.from(fields).sort();
3657
+ }
3658
+ function collectionKeyField(callback, itemNames) {
3659
+ let keyField;
3660
+ if (!callback.body) return void 0;
3661
+ traverse4(
3662
+ callback.body,
3663
+ {
3664
+ noScope: true,
3665
+ JSXAttribute: (nodePath) => {
3666
+ const attr = nodePath.node;
3667
+ if (!BabelTypes.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
3668
+ if (!attr.value || !BabelTypes.isJSXExpressionContainer(attr.value)) return;
3669
+ const expr = attr.value.expression;
3670
+ if (BabelTypes.isMemberExpression(expr) && BabelTypes.isIdentifier(expr.object) && itemNames.has(expr.object.name) && BabelTypes.isIdentifier(expr.property)) {
3671
+ keyField = keyField ?? expr.property.name;
3672
+ }
3673
+ }
3674
+ }
3675
+ );
3676
+ return keyField;
3677
+ }
3678
+ function collectionRowAction(callback) {
3679
+ if (!callback.body) return void 0;
3680
+ const navCall = extractWebNavigationCalls(callback.body).find((call) => call.targetPath);
3681
+ if (!navCall?.targetPath) return void 0;
3682
+ return {
3683
+ type: "navigation",
3684
+ // Route path — the orchestrator resolves it to a screen name.
3685
+ targetScreen: navCall.targetPath,
3686
+ description: `Clicking a row opens ${navCall.targetPath}`
3687
+ };
3688
+ }
3689
+ function callbackReturnsTag(callback, tags) {
3690
+ let found = false;
3691
+ const inspect = (node) => {
3692
+ if (!node || found) return;
3693
+ if (BabelTypes.isJSXElement(node)) {
3694
+ const name = getJsxElementName(node.openingElement);
3695
+ if (name && tags.has(name)) found = true;
3696
+ return;
3697
+ }
3698
+ if (BabelTypes.isBlockStatement(node)) {
3699
+ for (const statement of node.body) {
3700
+ if (BabelTypes.isReturnStatement(statement)) inspect(statement.argument);
3701
+ }
3702
+ }
3703
+ if (BabelTypes.isParenthesizedExpression(node)) inspect(node.expression);
3704
+ if (BabelTypes.isConditionalExpression(node)) {
3705
+ inspect(node.consequent);
3706
+ inspect(node.alternate);
3707
+ }
3708
+ };
3709
+ inspect(callback.body);
3710
+ return found;
3711
+ }
3712
+ function inferItemType(dataSource, displayFields) {
3713
+ const singular = dataSource.replace(/^render/i, "").replace(/(List|Items|Data|Rows|Sections)$/i, "").replace(/s$/i, "");
3714
+ const candidate = singular.charAt(0).toUpperCase() + singular.slice(1);
3715
+ if (candidate.length > 1) return candidate;
3716
+ if (displayFields.length > 0) return "Item";
3717
+ return void 0;
3718
+ }
3719
+ function inferIdentityFields(keyField, displayFields) {
3720
+ const out = /* @__PURE__ */ new Set();
3721
+ if (keyField) out.add(keyField);
3722
+ for (const field of displayFields) {
3723
+ if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
3724
+ }
3725
+ return Array.from(out);
3726
+ }
3727
+ var WEB_NAVIGATOR_TYPE = "route";
3728
+ var WebNavigationAnalyzer = class {
3729
+ config;
3730
+ navigationInclude;
3731
+ navigationExclude;
3732
+ constructor(config, options) {
3733
+ this.config = config;
3734
+ this.navigationInclude = options?.navigationInclude ?? [];
3735
+ this.navigationExclude = options?.navigationExclude ?? [];
3736
+ }
3737
+ async analyze() {
3738
+ const files = await this.findRouteFiles();
3739
+ const routes = [];
3740
+ for (const filePath of files) {
3741
+ try {
3742
+ const content = await promises.readFile(filePath, "utf-8");
3743
+ if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
3744
+ continue;
3745
+ }
3746
+ const ast = parseSource(content, this.config.parserPlugins);
3747
+ routes.push(...this.extractJsxRoutes(ast));
3748
+ routes.push(...this.extractObjectRoutes(ast));
3749
+ } catch (error) {
3750
+ console.warn(`[WebNavigationAnalyzer] Failed to parse ${filePath}:`, error);
3751
+ }
3752
+ }
3753
+ const deduped = this.dedupeRoutes(routes);
3754
+ return { graph: this.buildGraph(deduped), routes: deduped };
3755
+ }
3756
+ /** Files likely to contain route configuration. */
3757
+ async findRouteFiles() {
3758
+ const patterns = [
3759
+ "**/*{router,routes,Router,Routes}*.{ts,tsx,js,jsx}",
3760
+ "**/App.{ts,tsx,js,jsx}",
3761
+ "**/app.{ts,tsx,js,jsx}",
3762
+ "**/main.{ts,tsx,js,jsx}",
3763
+ "**/index.{ts,tsx,js,jsx}",
3764
+ ...this.navigationInclude
3765
+ ];
3766
+ const ignore = [
3767
+ "**/node_modules/**",
3768
+ "**/dist/**",
3769
+ "**/build/**",
3770
+ ...this.config.exclude || [],
3771
+ ...this.navigationExclude
3772
+ ];
3773
+ const files = await glob(patterns, { cwd: this.config.rootDir, ignore });
3774
+ return files.map((file) => path8__default.join(this.config.rootDir, file));
3775
+ }
3776
+ // ── JSX <Route> style ────────────────────────────────────────────
3777
+ extractJsxRoutes(ast) {
3778
+ const routes = [];
3779
+ const visitRoute = (element, parentPath) => {
3780
+ const opening = element.openingElement;
3781
+ const name = getJsxElementName(opening);
3782
+ if (name !== "Route") {
3783
+ for (const child of element.children) {
3784
+ if (BabelTypes.isJSXElement(child)) visitRoute(child, parentPath);
3785
+ }
3786
+ return;
3787
+ }
3788
+ const segment = getStringAttr(opening, "path");
3789
+ const isIndex = hasJsxAttribute(opening, "index") && !segment;
3790
+ const fullPath = this.joinPaths(parentPath, segment, isIndex);
3791
+ const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
3792
+ const isLeaf = !element.children.some(
3793
+ (child) => BabelTypes.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
3794
+ );
3795
+ if ((segment || isIndex) && (componentName || isLeaf)) {
3796
+ routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
3797
+ }
3798
+ for (const child of element.children) {
3799
+ if (BabelTypes.isJSXElement(child)) visitRoute(child, fullPath);
3800
+ }
3801
+ };
3802
+ traverse4(ast, {
3803
+ JSXElement: (nodePath) => {
3804
+ const name = getJsxElementName(nodePath.node.openingElement);
3805
+ if (name !== "Routes" && name !== "Route") return;
3806
+ if (nodePath.findParent((p) => {
3807
+ if (!p.isJSXElement()) return false;
3808
+ const parentName = getJsxElementName(p.node.openingElement);
3809
+ return parentName === "Routes" || parentName === "Route";
3810
+ })) {
3811
+ return;
3812
+ }
3813
+ visitRoute(nodePath.node, "");
2328
3814
  }
2329
- return screen;
2330
3815
  });
2331
- return {
2332
- screens: enrichedScreens,
2333
- navigation,
2334
- analyzedFiles: screenFiles.length,
2335
- ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
2336
- };
3816
+ return routes;
2337
3817
  }
2338
- mergeForm(target, source) {
2339
- for (const field of source.fields) {
2340
- const existingField = this.findEquivalentField(target.fields, field);
2341
- if (existingField) {
2342
- this.mergeField(existingField, field);
2343
- } else {
2344
- target.fields.push(field);
3818
+ /** `element={<VehicleList/>}` or `Component={VehicleList}`. */
3819
+ componentNameFromElementAttr(opening) {
3820
+ for (const attr of opening.attributes) {
3821
+ if (!BabelTypes.isJSXAttribute(attr) || !BabelTypes.isJSXIdentifier(attr.name)) continue;
3822
+ if (attr.name.name === "element" && BabelTypes.isJSXExpressionContainer(attr.value)) {
3823
+ const expr = attr.value.expression;
3824
+ if (BabelTypes.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
3825
+ }
3826
+ if (attr.name.name === "Component" && BabelTypes.isJSXExpressionContainer(attr.value)) {
3827
+ if (BabelTypes.isIdentifier(attr.value.expression)) return attr.value.expression.name;
2345
3828
  }
2346
3829
  }
2347
- target.submitAction = target.submitAction ?? this.namedSubmitAction(source.submitAction);
2348
- target.validationRules = source.validationRules ? { ...source.validationRules, ...target.validationRules ?? {} } : target.validationRules;
3830
+ return null;
2349
3831
  }
2350
- findEquivalentField(fields, incoming) {
2351
- return fields.find((field) => {
2352
- if (field.name && incoming.name && field.name === incoming.name) return true;
2353
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
2354
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
2355
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
2356
- if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
2357
- return true;
3832
+ // ── createBrowserRouter([...]) / useRoutes([...]) style ──────────
3833
+ extractObjectRoutes(ast) {
3834
+ const routes = [];
3835
+ const ROUTER_FACTORIES = /* @__PURE__ */ new Set([
3836
+ "createBrowserRouter",
3837
+ "createHashRouter",
3838
+ "createMemoryRouter",
3839
+ "useRoutes"
3840
+ ]);
3841
+ traverse4(ast, {
3842
+ CallExpression: (nodePath) => {
3843
+ const callee = nodePath.node.callee;
3844
+ if (!BabelTypes.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
3845
+ const first = nodePath.node.arguments[0];
3846
+ if (!BabelTypes.isArrayExpression(first)) return;
3847
+ this.visitRouteObjects(first, "", routes);
2358
3848
  }
2359
- return false;
2360
3849
  });
3850
+ return routes;
2361
3851
  }
2362
- mergeField(target, source) {
2363
- if (this.isWeakInferredFieldName(target.name) && !this.isWeakInferredFieldName(source.name)) {
2364
- target.name = source.name;
2365
- }
2366
- target.label = target.label ?? source.label;
2367
- target.placeholder = target.placeholder ?? source.placeholder;
2368
- target.options = target.options ?? source.options;
2369
- target.locator = target.locator ?? source.locator;
2370
- target.sourceComponent = target.sourceComponent ?? source.sourceComponent;
2371
- target.valueBinding = target.valueBinding ?? source.valueBinding;
2372
- target.errorBinding = target.errorBinding ?? source.errorBinding;
2373
- target.required = target.required || source.required;
2374
- if (target.type === "text" && source.type !== "text") {
2375
- target.type = source.type;
3852
+ visitRouteObjects(arr, parentPath, out) {
3853
+ for (const element of arr.elements) {
3854
+ if (!BabelTypes.isObjectExpression(element)) continue;
3855
+ let segment;
3856
+ let isIndex = false;
3857
+ let componentName;
3858
+ let children;
3859
+ for (const prop of element.properties) {
3860
+ if (!BabelTypes.isObjectProperty(prop) || !BabelTypes.isIdentifier(prop.key)) continue;
3861
+ const key = prop.key.name;
3862
+ if (key === "path" && BabelTypes.isStringLiteral(prop.value)) segment = prop.value.value;
3863
+ if (key === "index" && BabelTypes.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
3864
+ if (key === "element" && BabelTypes.isJSXElement(prop.value)) {
3865
+ componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
3866
+ }
3867
+ if (key === "Component" && BabelTypes.isIdentifier(prop.value)) componentName = prop.value.name;
3868
+ if (key === "children" && BabelTypes.isArrayExpression(prop.value)) children = prop.value;
3869
+ }
3870
+ const fullPath = this.joinPaths(parentPath, segment, isIndex);
3871
+ if ((segment !== void 0 || isIndex) && (componentName || !children)) {
3872
+ out.push(this.buildRoute(fullPath, componentName, isIndex, Boolean(children)));
3873
+ }
3874
+ if (children) this.visitRouteObjects(children, fullPath, out);
2376
3875
  }
2377
3876
  }
2378
- namedSubmitAction(submitAction) {
2379
- return submitAction && submitAction !== "anonymous" ? submitAction : void 0;
3877
+ // ── Shared route building ────────────────────────────────────────
3878
+ joinPaths(parent, segment, isIndex) {
3879
+ if (isIndex || segment === void 0) return parent || "/";
3880
+ if (segment.startsWith("/")) return this.normalizePath(segment);
3881
+ return this.normalizePath(`${parent === "/" ? "" : parent}/${segment}`);
2380
3882
  }
2381
- isWeakInferredFieldName(name) {
2382
- return /^(text|value|input|query|search|selected|checked)$/i.test(name);
3883
+ normalizePath(p) {
3884
+ const cleaned = `/${p}`.replace(/\/+/g, "/");
3885
+ return cleaned.length > 1 ? cleaned.replace(/\/$/, "") : cleaned;
2383
3886
  }
2384
- findFormWithSharedFields(forms, incoming) {
2385
- if (incoming.fields.length === 0) return void 0;
2386
- let best;
2387
- for (const form of forms) {
2388
- const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
2389
- if (overlap > 0 && (!best || overlap > best.overlap)) {
2390
- best = { form, overlap };
3887
+ buildRoute(fullPath, componentName, isIndex, isLayout) {
3888
+ const params = this.paramsFromPath(fullPath);
3889
+ return {
3890
+ path: fullPath,
3891
+ screenName: componentName ?? screenNameFromPath(fullPath),
3892
+ ...params.length > 0 ? { params } : {},
3893
+ ...isIndex ? { index: true } : {},
3894
+ ...isLayout ? { layout: true } : {}
3895
+ };
3896
+ }
3897
+ paramsFromPath(routePath) {
3898
+ const params = [];
3899
+ for (const segment of routePath.split("/")) {
3900
+ if (!segment.startsWith(":")) continue;
3901
+ const optional = segment.endsWith("?");
3902
+ const name = segment.slice(1, optional ? -1 : void 0);
3903
+ if (name) params.push({ name, type: "string", required: !optional });
3904
+ }
3905
+ return params;
3906
+ }
3907
+ /**
3908
+ * One entry per path. When several declarations resolve to the same
3909
+ * path, keep the one that best describes what the user lands on: a
3910
+ * page beats a layout wrapper (an `index` child and its parent layout
3911
+ * share a path), and a resolved component name beats a name derived
3912
+ * from the path.
3913
+ */
3914
+ dedupeRoutes(routes) {
3915
+ const byPath = /* @__PURE__ */ new Map();
3916
+ for (const route of routes) {
3917
+ const existing = byPath.get(route.path);
3918
+ if (!existing || routeScore(route) > routeScore(existing)) {
3919
+ byPath.set(route.path, route);
2391
3920
  }
2392
3921
  }
2393
- return best?.form;
3922
+ return Array.from(byPath.values());
3923
+ }
3924
+ buildGraph(routes) {
3925
+ const screens = {};
3926
+ const navigatorName = "router";
3927
+ const screenNames = routes.map((r) => r.screenName);
3928
+ for (const route of routes) {
3929
+ const others = screenNames.filter((name) => name !== route.screenName);
3930
+ screens[route.screenName] = {
3931
+ screenName: route.screenName,
3932
+ // Open-union value — web routes, not a RN stack/tab/drawer.
3933
+ navigatorType: WEB_NAVIGATOR_TYPE,
3934
+ parentNavigator: navigatorName,
3935
+ // Any route is one URL away from any other — both directions,
3936
+ // like the RN analyzer models tab navigators.
3937
+ reachableFrom: others,
3938
+ reachableTo: others,
3939
+ ...route.params ? { params: route.params } : {}
3940
+ };
3941
+ }
3942
+ const initialRoute = routes.find((r) => r.path === "/") ?? routes.find((r) => r.index) ?? routes[0];
3943
+ return {
3944
+ screens,
3945
+ initialScreen: initialRoute?.screenName ?? "",
3946
+ navigators: routes.length > 0 ? [{
3947
+ name: navigatorName,
3948
+ type: WEB_NAVIGATOR_TYPE,
3949
+ screens: screenNames
3950
+ }] : []
3951
+ };
2394
3952
  }
2395
3953
  };
3954
+ function screenNameFromPath(routePath) {
3955
+ if (routePath === "/" || routePath === "") return "Home";
3956
+ return routePath.split("/").filter(Boolean).map((segment) => segment.replace(/^:/, "").replace(/\?$/, "")).map(
3957
+ (segment) => segment.split(/[-_.]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("")
3958
+ ).join("");
3959
+ }
3960
+ function routeScore(route) {
3961
+ const isPage = route.layout ? 0 : 2;
3962
+ const hasRealName = route.screenName === screenNameFromPath(route.path) ? 0 : 1;
3963
+ return isPage + hasRealName;
3964
+ }
3965
+ function resolvePathToScreen(routes, target) {
3966
+ const normalized = `/${target}`.replace(/\/+/g, "/").replace(/\?.*$/, "").replace(/#.*$/, "");
3967
+ const cleaned = normalized.length > 1 ? normalized.replace(/\/$/, "") : normalized;
3968
+ const exact = routes.find((r) => r.path === cleaned);
3969
+ if (exact) return exact.screenName;
3970
+ const targetSegments = cleaned.split("/").filter(Boolean);
3971
+ for (const route of routes) {
3972
+ const routeSegments = route.path.split("/").filter(Boolean);
3973
+ if (routeSegments.length !== targetSegments.length) continue;
3974
+ const matches = routeSegments.every(
3975
+ (seg, i) => seg.startsWith(":") || seg === "*" || seg === targetSegments[i]
3976
+ );
3977
+ if (matches) return route.screenName;
3978
+ }
3979
+ return void 0;
3980
+ }
2396
3981
 
2397
- // src/analyzers/GenericPlatformAnalyzer.ts
2398
- var GenericPlatformAnalyzer = class {
2399
- constructor(platform) {
2400
- this.platform = platform;
3982
+ // src/analyzers/web/ReactWebPlatformAnalyzer.ts
3983
+ var ReactWebPlatformAnalyzer = class {
3984
+ platform = "web";
3985
+ async analyze(config, options) {
3986
+ const screenAnalyzer = new WebScreenAnalyzer(config, {
3987
+ screenPatterns: options.screenPatterns
3988
+ });
3989
+ const navigationAnalyzer = new WebNavigationAnalyzer(config, {
3990
+ navigationInclude: options.navigationInclude,
3991
+ navigationExclude: options.navigationExclude
3992
+ });
3993
+ console.log("[ReactWebPlatformAnalyzer] Running analyzers...");
3994
+ const [screenAnalysis, navigationResult] = await Promise.all([
3995
+ screenAnalyzer.analyze(),
3996
+ navigationAnalyzer.analyze()
3997
+ ]);
3998
+ const routeScreenNames = new Set(navigationResult.routes.map((route) => route.screenName));
3999
+ const strictScreens = options.strictScreens ?? true;
4000
+ let screensFilteredOut = 0;
4001
+ const included = [];
4002
+ for (const candidate of screenAnalysis.candidates) {
4003
+ if (!strictScreens || this.isScreen(candidate, routeScreenNames)) {
4004
+ included.push(candidate);
4005
+ } else {
4006
+ screensFilteredOut++;
4007
+ }
4008
+ }
4009
+ const screens = included.map(
4010
+ (candidate) => this.resolveRoutePaths(candidate.descriptor, navigationResult.routes)
4011
+ );
4012
+ console.log(
4013
+ `[ReactWebPlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens, ${navigationResult.routes.length} routes`
4014
+ );
4015
+ return {
4016
+ screens,
4017
+ navigation: navigationResult.graph,
4018
+ analyzedFiles: screenAnalysis.analyzedFiles,
4019
+ ...screensFilteredOut > 0 ? { screensFilteredOut } : {}
4020
+ };
2401
4021
  }
2402
- platform;
2403
- async analyze(_config, _options) {
4022
+ isScreen(candidate, routeScreenNames) {
4023
+ return candidate.hasRegisterScreen || candidate.matchesScreenPattern || routeScreenNames.has(candidate.descriptor.name);
4024
+ }
4025
+ /** Replace route-path references with screen names where the route table resolves them. */
4026
+ resolveRoutePaths(screen, routes) {
4027
+ const resolve2 = (target) => {
4028
+ if (!target || !target.startsWith("/")) return target;
4029
+ return resolvePathToScreen(routes, target) ?? target;
4030
+ };
4031
+ const navigationTargets = Array.from(
4032
+ new Set(
4033
+ screen.navigationTargets.map((target) => resolve2(target)).filter((target) => target !== screen.name)
4034
+ )
4035
+ ).sort();
4036
+ const actions = screen.actions.map((action) => {
4037
+ const resolved = resolve2(action.targetScreen);
4038
+ return resolved === action.targetScreen ? action : { ...action, targetScreen: resolved };
4039
+ });
4040
+ const collections = screen.collections?.map((collection) => {
4041
+ const resolveRow = (row) => {
4042
+ if (!row?.targetScreen) return row;
4043
+ const resolved = resolve2(row.targetScreen);
4044
+ if (resolved === row.targetScreen) return row;
4045
+ return {
4046
+ ...row,
4047
+ targetScreen: resolved,
4048
+ ...row.description ? { description: `Clicking a row opens ${resolved}` } : {}
4049
+ };
4050
+ };
4051
+ const rowAction = resolveRow(collection.rowAction);
4052
+ return {
4053
+ ...collection,
4054
+ ...rowAction ? { rowAction } : {},
4055
+ ...collection.rowActions ? { rowActions: collection.rowActions.map((row) => resolveRow(row)) } : {}
4056
+ };
4057
+ });
2404
4058
  return {
2405
- screens: [],
2406
- navigation: { screens: {}, initialScreen: "", navigators: [] },
2407
- analyzedFiles: 0
4059
+ ...screen,
4060
+ navigationTargets,
4061
+ actions,
4062
+ ...collections ? { collections } : {}
2408
4063
  };
2409
4064
  }
2410
4065
  };
@@ -2614,8 +4269,8 @@ var ZodParsedType = util.arrayToEnum([
2614
4269
  "set"
2615
4270
  ]);
2616
4271
  var getParsedType = (data) => {
2617
- const t8 = typeof data;
2618
- switch (t8) {
4272
+ const t12 = typeof data;
4273
+ switch (t12) {
2619
4274
  case "undefined":
2620
4275
  return ZodParsedType.undefined;
2621
4276
  case "string":
@@ -2887,8 +4542,8 @@ function getErrorMap() {
2887
4542
 
2888
4543
  // ../../node_modules/zod/v3/helpers/parseUtil.js
2889
4544
  var makeIssue = (params) => {
2890
- const { data, path: path7, errorMaps, issueData } = params;
2891
- const fullPath = [...path7, ...issueData.path || []];
4545
+ const { data, path: path9, errorMaps, issueData } = params;
4546
+ const fullPath = [...path9, ...issueData.path || []];
2892
4547
  const fullIssue = {
2893
4548
  ...issueData,
2894
4549
  path: fullPath
@@ -3004,11 +4659,11 @@ var errorUtil;
3004
4659
 
3005
4660
  // ../../node_modules/zod/v3/types.js
3006
4661
  var ParseInputLazyPath = class {
3007
- constructor(parent, value, path7, key) {
4662
+ constructor(parent, value, path9, key) {
3008
4663
  this._cachedPath = [];
3009
4664
  this.parent = parent;
3010
4665
  this.data = value;
3011
- this._path = path7;
4666
+ this._path = path9;
3012
4667
  this._key = key;
3013
4668
  }
3014
4669
  get path() {
@@ -6449,7 +8104,7 @@ var coerce = {
6449
8104
  };
6450
8105
  var NEVER = INVALID;
6451
8106
 
6452
- // ../shared/dist/chunk-XLOYG3DH.mjs
8107
+ // ../shared/dist/chunk-PKS3VDNB.mjs
6453
8108
  var locatorSourceSchema = external_exports.enum([
6454
8109
  "appilotsId",
6455
8110
  "testID",
@@ -6475,7 +8130,16 @@ var locatorDescriptorSchema = external_exports.object({
6475
8130
  source: locatorSourceSchema.optional()
6476
8131
  }).passthrough();
6477
8132
  var signalDescriptorSchema = external_exports.object({
6478
- type: external_exports.enum(["navigation", "goBack", "toast", "modal", "inline-error", "data-arrival", "loading", "none"]).or(external_exports.string().min(1).max(60)),
8133
+ type: external_exports.enum([
8134
+ "navigation",
8135
+ "goBack",
8136
+ "toast",
8137
+ "modal",
8138
+ "inline-error",
8139
+ "data-arrival",
8140
+ "loading",
8141
+ "none"
8142
+ ]).or(external_exports.string().min(1).max(60)),
6479
8143
  target: external_exports.string().optional(),
6480
8144
  description: external_exports.string().optional()
6481
8145
  }).passthrough();
@@ -6489,7 +8153,20 @@ var waitPolicyDescriptorSchema = external_exports.object({
6489
8153
  signals: external_exports.array(signalDescriptorSchema).optional(),
6490
8154
  maxMs: external_exports.number().finite().optional()
6491
8155
  }).passthrough();
6492
- var targetDescriptorRoleSchema = external_exports.enum(["button", "submit", "input", "toggle", "select", "date", "list", "row", "menuItem", "modal", "custom"]).or(external_exports.string().min(1).max(60));
8156
+ var targetDescriptorRoleSchema = external_exports.enum([
8157
+ "button",
8158
+ "submit",
8159
+ "input",
8160
+ "toggle",
8161
+ "select",
8162
+ "date",
8163
+ "list",
8164
+ "row",
8165
+ "menuItem",
8166
+ "modal",
8167
+ "custom"
8168
+ ]).or(external_exports.string().min(1).max(60));
8169
+ var targetOriginSchema = external_exports.enum(["analyzer", "manifest"]).or(external_exports.string().min(1).max(40));
6493
8170
  var targetDescriptorSchema = external_exports.object({
6494
8171
  id: external_exports.string(),
6495
8172
  role: targetDescriptorRoleSchema,
@@ -6503,10 +8180,20 @@ var targetDescriptorSchema = external_exports.object({
6503
8180
  requiresConfirmation: external_exports.boolean().optional(),
6504
8181
  opensModal: external_exports.string().optional(),
6505
8182
  opensBottomSheet: external_exports.string().optional(),
6506
- sourceComponent: external_exports.string().optional()
8183
+ sourceComponent: external_exports.string().optional(),
8184
+ origin: targetOriginSchema.optional()
6507
8185
  }).passthrough();
6508
8186
  var flowStepDescriptorSchema = external_exports.object({
6509
- type: external_exports.enum(["navigate", "fill", "press", "select", "toggle", "wait", "confirm", "choose-list-item"]).or(external_exports.string().min(1).max(60)),
8187
+ type: external_exports.enum([
8188
+ "navigate",
8189
+ "fill",
8190
+ "press",
8191
+ "select",
8192
+ "toggle",
8193
+ "wait",
8194
+ "confirm",
8195
+ "choose-list-item"
8196
+ ]).or(external_exports.string().min(1).max(60)),
6510
8197
  target: external_exports.string().optional(),
6511
8198
  label: external_exports.string().optional(),
6512
8199
  description: external_exports.string().optional(),
@@ -6634,7 +8321,19 @@ var navigationNodeSchema = external_exports.object({
6634
8321
  parentNavigator: external_exports.string().optional(),
6635
8322
  reachableFrom: external_exports.array(external_exports.string()).default([]),
6636
8323
  reachableTo: external_exports.array(external_exports.string()).default([]),
6637
- params: external_exports.array(paramDescriptorSchema).optional()
8324
+ params: external_exports.array(paramDescriptorSchema).optional(),
8325
+ /**
8326
+ * Web clients only (documented convention, additive — previously
8327
+ * round-tripped via `.passthrough()`): the screen's URL route
8328
+ * template, e.g. `/users/:id`. Param segments use `:name` (the
8329
+ * relay also accepts `[name]` / `{name}`). When present and the
8330
+ * request's `context.platform` is `'web'`, the relay resolves
8331
+ * `navigate` targets against these templates and injects the
8332
+ * concrete URL segments as the navigate payload's `path` — the web
8333
+ * equivalent of RN's nested-navigation path injection. See
8334
+ * docs/agent-contract.md ("Web clients" section).
8335
+ */
8336
+ path: external_exports.string().max(500).optional()
6638
8337
  }).passthrough();
6639
8338
  var navigatorDescriptorSchema = external_exports.object({
6640
8339
  name: external_exports.string(),
@@ -6681,6 +8380,50 @@ var loginSchema = external_exports.object({
6681
8380
  loginSchema.extend({
6682
8381
  name: external_exports.string().min(2, "Name must be at least 2 characters").max(100)
6683
8382
  });
8383
+ external_exports.object({
8384
+ name: external_exports.string().min(2, "Name must be at least 2 characters").max(100).optional(),
8385
+ avatarUrl: external_exports.string().url("Invalid URL").max(2048).nullable().optional()
8386
+ }).refine((v) => v.name !== void 0 || v.avatarUrl !== void 0, {
8387
+ message: "At least one field must be provided"
8388
+ });
8389
+ external_exports.object({
8390
+ currentPassword: external_exports.string().min(1, "Current password is required"),
8391
+ newPassword: external_exports.string().min(8, "Password must be at least 8 characters")
8392
+ });
8393
+ var totpCodeSchema = external_exports.string().transform((v) => v.replace(/\s/g, "")).pipe(external_exports.string().regex(/^\d{6}$/, "Code must be 6 digits"));
8394
+ var recoveryCodeSchema = external_exports.string().transform((v) => v.toUpperCase().replace(/[^A-Z0-9]/g, "")).pipe(external_exports.string().regex(/^[23456789BCDFGHJKMNPQRSTVWXYZ]{10}$/, "Invalid recovery code"));
8395
+ external_exports.object({
8396
+ code: totpCodeSchema
8397
+ });
8398
+ external_exports.object({
8399
+ challengeToken: external_exports.string().min(1, "Challenge token is required"),
8400
+ code: totpCodeSchema.optional(),
8401
+ recoveryCode: recoveryCodeSchema.optional()
8402
+ }).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
8403
+ message: "Provide either a TOTP code or a recovery code",
8404
+ path: ["code"]
8405
+ });
8406
+ external_exports.object({
8407
+ password: external_exports.string().min(1, "Password is required"),
8408
+ code: totpCodeSchema.optional(),
8409
+ recoveryCode: recoveryCodeSchema.optional()
8410
+ }).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
8411
+ message: "Provide either a TOTP code or a recovery code",
8412
+ path: ["code"]
8413
+ });
8414
+ external_exports.object({
8415
+ password: external_exports.string().min(1, "Password is required"),
8416
+ code: totpCodeSchema
8417
+ });
8418
+ external_exports.object({
8419
+ password: external_exports.string().min(1, "Password is required"),
8420
+ code: totpCodeSchema.optional(),
8421
+ recoveryCode: recoveryCodeSchema.optional(),
8422
+ confirm: external_exports.literal("DELETE MY ACCOUNT")
8423
+ }).refine((v) => !(v.code && v.recoveryCode), {
8424
+ message: "Provide either a TOTP code or a recovery code, not both",
8425
+ path: ["code"]
8426
+ });
6684
8427
  var createProjectSchema = external_exports.object({
6685
8428
  name: external_exports.string().min(1).max(100),
6686
8429
  description: external_exports.string().max(500).optional(),
@@ -6716,6 +8459,7 @@ external_exports.object({
6716
8459
  content: external_exports.record(external_exports.unknown())
6717
8460
  });
6718
8461
  var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
8462
+ var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
6719
8463
  external_exports.object({
6720
8464
  name: external_exports.string().min(1).max(100),
6721
8465
  // Required for `sdk` (default), forbidden/ignored for `operator` —
@@ -6723,6 +8467,7 @@ external_exports.object({
6723
8467
  // missing scope still validates as the pre-existing SDK shape.
6724
8468
  projectId: external_exports.string().min(1).optional(),
6725
8469
  scope: apiKeyScopeSchema.default("sdk"),
8470
+ environment: apiKeyEnvironmentSchema.optional(),
6726
8471
  expiresAt: external_exports.string().datetime().optional()
6727
8472
  }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
6728
8473
  message: 'projectId is required for scope "sdk"',
@@ -6730,13 +8475,23 @@ external_exports.object({
6730
8475
  });
6731
8476
  var boundedString = (max) => external_exports.string().max(max);
6732
8477
  var clientPlatformSchema = external_exports.enum(["react-native", "web", "android", "ios"]).or(external_exports.string().min(1).max(40));
8478
+ var identityProvenanceSchema = external_exports.enum(["declared", "derived", "positional"]).or(external_exports.string().min(1).max(40));
6733
8479
  var snapshotInputSchema = external_exports.object({
6734
8480
  id: boundedString(160).optional(),
8481
+ provenance: identityProvenanceSchema.optional(),
8482
+ /**
8483
+ * False when the control is mounted but currently OUTSIDE the window —
8484
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8485
+ * cannot tell (old React Native architecture, or any SDK published
8486
+ * before the field existed), which must never be read as `false`.
8487
+ */
8488
+ onScreen: external_exports.boolean().optional(),
6735
8489
  label: boundedString(300).optional(),
6736
8490
  value: boundedString(4096).optional(),
6737
8491
  placeholder: boundedString(300).optional(),
6738
8492
  editable: external_exports.boolean().optional(),
6739
8493
  secure: external_exports.boolean().optional(),
8494
+ submitsOnReturn: external_exports.boolean().optional(),
6740
8495
  type: boundedString(40).optional(),
6741
8496
  required: external_exports.boolean().optional(),
6742
8497
  invalid: external_exports.boolean().optional(),
@@ -6744,6 +8499,14 @@ var snapshotInputSchema = external_exports.object({
6744
8499
  }).passthrough();
6745
8500
  var snapshotButtonSchema = external_exports.object({
6746
8501
  id: boundedString(160).optional(),
8502
+ provenance: identityProvenanceSchema.optional(),
8503
+ /**
8504
+ * False when the control is mounted but currently OUTSIDE the window —
8505
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8506
+ * cannot tell (old React Native architecture, or any SDK published
8507
+ * before the field existed), which must never be read as `false`.
8508
+ */
8509
+ onScreen: external_exports.boolean().optional(),
6747
8510
  label: boundedString(300).optional(),
6748
8511
  disabled: external_exports.boolean().optional(),
6749
8512
  inModal: external_exports.boolean().optional(),
@@ -6753,12 +8516,28 @@ var snapshotButtonSchema = external_exports.object({
6753
8516
  }).passthrough();
6754
8517
  var snapshotToggleSchema = external_exports.object({
6755
8518
  id: boundedString(160).optional(),
8519
+ provenance: identityProvenanceSchema.optional(),
8520
+ /**
8521
+ * False when the control is mounted but currently OUTSIDE the window —
8522
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8523
+ * cannot tell (old React Native architecture, or any SDK published
8524
+ * before the field existed), which must never be read as `false`.
8525
+ */
8526
+ onScreen: external_exports.boolean().optional(),
6756
8527
  label: boundedString(300).optional(),
6757
8528
  value: external_exports.boolean().optional(),
6758
8529
  inModal: external_exports.boolean().optional()
6759
8530
  }).passthrough();
6760
8531
  var snapshotSliderSchema = external_exports.object({
6761
8532
  id: boundedString(160).optional(),
8533
+ provenance: identityProvenanceSchema.optional(),
8534
+ /**
8535
+ * False when the control is mounted but currently OUTSIDE the window —
8536
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8537
+ * cannot tell (old React Native architecture, or any SDK published
8538
+ * before the field existed), which must never be read as `false`.
8539
+ */
8540
+ onScreen: external_exports.boolean().optional(),
6762
8541
  label: boundedString(300).optional(),
6763
8542
  value: external_exports.number().finite().optional(),
6764
8543
  min: external_exports.number().finite().optional(),
@@ -6799,6 +8578,26 @@ var snapshotListSchema = external_exports.object({
6799
8578
  // should chase, and the relay only reads a known subset.
6800
8579
  items: external_exports.array(external_exports.record(external_exports.unknown())).max(500).optional()
6801
8580
  }).passthrough();
8581
+ var snapshotNativeDialogSchema = external_exports.object({
8582
+ id: boundedString(160),
8583
+ title: boundedString(300).optional(),
8584
+ message: boundedString(2e3).optional(),
8585
+ buttons: external_exports.array(
8586
+ external_exports.object({
8587
+ label: boundedString(160),
8588
+ style: external_exports.enum(["default", "cancel", "destructive"]).optional()
8589
+ }).passthrough()
8590
+ ).max(10)
8591
+ }).passthrough();
8592
+ var snapshotScrollableSchema = external_exports.object({
8593
+ id: boundedString(160),
8594
+ containerType: boundedString(60).optional(),
8595
+ label: boundedString(300).optional(),
8596
+ scrollOffsetY: external_exports.number().finite().optional(),
8597
+ canScrollUp: external_exports.boolean().optional(),
8598
+ canScrollDown: external_exports.boolean().optional(),
8599
+ horizontal: external_exports.boolean().optional()
8600
+ }).passthrough();
6802
8601
  var snapshotChoiceGroupSchema = external_exports.object({
6803
8602
  index: external_exports.number().int().optional(),
6804
8603
  id: boundedString(160).optional(),
@@ -6815,6 +8614,14 @@ var snapshotElementSchema = external_exports.object({
6815
8614
  disabled: external_exports.boolean().optional(),
6816
8615
  selected: external_exports.boolean().optional(),
6817
8616
  source: boundedString(40).optional(),
8617
+ provenance: identityProvenanceSchema.optional(),
8618
+ /**
8619
+ * False when the control is mounted but currently OUTSIDE the window —
8620
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8621
+ * cannot tell (old React Native architecture, or any SDK published
8622
+ * before the field existed), which must never be read as `false`.
8623
+ */
8624
+ onScreen: external_exports.boolean().optional(),
6818
8625
  targetId: boundedString(160).optional(),
6819
8626
  listContext: external_exports.record(external_exports.unknown()).optional(),
6820
8627
  inModal: external_exports.boolean().optional()
@@ -6831,7 +8638,34 @@ var agentSnapshotSchema = external_exports.object({
6831
8638
  modalOpen: external_exports.boolean().optional(),
6832
8639
  lists: external_exports.array(snapshotListSchema).max(100).optional(),
6833
8640
  choiceGroups: external_exports.array(snapshotChoiceGroupSchema).max(100).optional(),
6834
- elements: external_exports.array(snapshotElementSchema).max(1e3).optional()
8641
+ scrollables: external_exports.array(snapshotScrollableSchema).max(20).optional(),
8642
+ nativeDialog: snapshotNativeDialogSchema.optional(),
8643
+ elements: external_exports.array(snapshotElementSchema).max(1e3).optional(),
8644
+ /**
8645
+ * The client clamped this observation to the caps above and lost
8646
+ * data doing it (`clampSnapshotToWireLimits` in
8647
+ * `@appilots/client-core`). Read by the prompt serializer so the
8648
+ * model is told the view is partial — a screen truncated at 500
8649
+ * texts must not be read as a screen that only has 500 things on
8650
+ * it. Optional: older SDKs never clamp and never send it.
8651
+ */
8652
+ truncated: external_exports.boolean().optional(),
8653
+ /**
8654
+ * How many of this screen's controls the client could name, split by
8655
+ * `identityProvenance`. Diagnostic only — the relay never grounds an
8656
+ * action on it — but it is the number that says whether an app needs
8657
+ * annotating, and comparing it between a debug and a release build is
8658
+ * the first direct measurement of what minification costs the agent.
8659
+ */
8660
+ stats: external_exports.object({
8661
+ visitedFibers: external_exports.number().int().nonnegative().optional(),
8662
+ skippedHidden: external_exports.number().int().nonnegative().optional(),
8663
+ identity: external_exports.object({
8664
+ declared: external_exports.number().int().nonnegative().optional(),
8665
+ derived: external_exports.number().int().nonnegative().optional(),
8666
+ positional: external_exports.number().int().nonnegative().optional()
8667
+ }).passthrough().optional()
8668
+ }).passthrough().optional()
6835
8669
  }).passthrough();
6836
8670
  var agentContextSchema = external_exports.object({
6837
8671
  /**
@@ -6881,6 +8715,19 @@ var agentContextSchema = external_exports.object({
6881
8715
  ).max(200).optional(),
6882
8716
  loadingPending: external_exports.boolean().optional()
6883
8717
  }).passthrough();
8718
+ var introspectionFailureReasonSchema = external_exports.enum([
8719
+ "sentinel-missing-internals",
8720
+ "never-mounted",
8721
+ "render-crashed",
8722
+ "names-mangled",
8723
+ "walk-recognized-nothing"
8724
+ ]);
8725
+ var introspectionDiagnosticsSchema = external_exports.object({
8726
+ captured: external_exports.boolean(),
8727
+ failureReason: introspectionFailureReasonSchema.nullish(),
8728
+ reactVersion: boundedString(32).nullish(),
8729
+ failureCount: external_exports.number().int().min(0).max(1e6).optional()
8730
+ });
6884
8731
  external_exports.object({
6885
8732
  content: external_exports.string().min(1).max(4096),
6886
8733
  sessionId: external_exports.string().optional(),
@@ -6890,7 +8737,20 @@ external_exports.object({
6890
8737
  * the active MCP doc and emits `mcp_version_mismatch` telemetry when
6891
8738
  * they differ. Optional — older SDKs simply skip the check.
6892
8739
  */
6893
- mcpVersion: external_exports.string().max(64).optional()
8740
+ mcpVersion: external_exports.string().max(64).optional(),
8741
+ /**
8742
+ * SDK-side introspection health. Only sent when it FAILED — a client
8743
+ * that reached the React tree omits the field entirely, so the happy
8744
+ * path costs nothing on the wire.
8745
+ *
8746
+ * Without this, an app whose introspection is dead sends a snapshot
8747
+ * that is empty but perfectly well-formed, indistinguishable from a
8748
+ * genuinely empty screen. The server emits
8749
+ * `introspection_unavailable` so the failure is visible to us instead
8750
+ * of waiting for a bug report. Optional — older SDKs simply never
8751
+ * send it.
8752
+ */
8753
+ introspection: introspectionDiagnosticsSchema.optional()
6894
8754
  });
6895
8755
  var agentActionTypeSchema = external_exports.enum([
6896
8756
  "navigate",
@@ -6901,11 +8761,22 @@ var agentActionTypeSchema = external_exports.enum([
6901
8761
  "custom"
6902
8762
  ]);
6903
8763
  var navigationPayloadSchema = external_exports.object({
6904
- screenName: external_exports.string().min(1),
8764
+ // Optional ONLY so `goBack` can exist (#398): it returns to whatever
8765
+ // is beneath on the stack and has no destination to name. Every other
8766
+ // action is still required to carry one — enforced by the refine
8767
+ // below rather than by the field, so the error names the real problem
8768
+ // ("navigate needs a screenName") instead of a missing key.
8769
+ screenName: external_exports.string().min(1).optional(),
6905
8770
  params: external_exports.record(external_exports.unknown()).optional(),
6906
8771
  navigationAction: external_exports.enum(["push", "navigate", "replace", "goBack", "reset"]).default("navigate").optional(),
6907
8772
  path: external_exports.array(external_exports.string().min(1)).optional()
6908
- }).passthrough();
8773
+ }).passthrough().refine(
8774
+ (payload) => payload.navigationAction === "goBack" || typeof payload.screenName === "string" && payload.screenName.length > 0,
8775
+ {
8776
+ message: 'screenName is required for every navigationAction except "goBack"',
8777
+ path: ["screenName"]
8778
+ }
8779
+ );
6909
8780
  var formFillFieldSchema = external_exports.object({
6910
8781
  fieldId: external_exports.string().min(1),
6911
8782
  fieldType: external_exports.enum(["text", "select", "toggle", "date", "number", "custom"]).default("text").optional(),
@@ -7239,6 +9110,8 @@ var sandboxObservationSchema = external_exports.object({
7239
9110
  inputs: external_exports.array(
7240
9111
  external_exports.object({
7241
9112
  id: external_exports.string().max(120),
9113
+ provenance: identityProvenanceSchema.optional(),
9114
+ onScreen: external_exports.boolean().optional(),
7242
9115
  label: external_exports.string().max(200).optional(),
7243
9116
  value: external_exports.string().max(2e3).optional(),
7244
9117
  disabled: external_exports.boolean().optional(),
@@ -7252,6 +9125,8 @@ var sandboxObservationSchema = external_exports.object({
7252
9125
  buttons: external_exports.array(
7253
9126
  external_exports.object({
7254
9127
  id: external_exports.string().max(120),
9128
+ provenance: identityProvenanceSchema.optional(),
9129
+ onScreen: external_exports.boolean().optional(),
7255
9130
  label: external_exports.string().max(200).optional(),
7256
9131
  disabled: external_exports.boolean().optional(),
7257
9132
  /** Risk metadata (e.g. "high") the SDK forwards for danger UI. */
@@ -7273,6 +9148,8 @@ var sandboxObservationSchema = external_exports.object({
7273
9148
  sliders: external_exports.array(
7274
9149
  external_exports.object({
7275
9150
  id: external_exports.string().max(120),
9151
+ provenance: identityProvenanceSchema.optional(),
9152
+ onScreen: external_exports.boolean().optional(),
7276
9153
  label: external_exports.string().max(200).optional(),
7277
9154
  value: external_exports.number().finite().optional(),
7278
9155
  min: external_exports.number().finite().optional(),
@@ -7302,6 +9179,44 @@ var sandboxObservationSchema = external_exports.object({
7302
9179
  ).max(200).optional()
7303
9180
  }).passthrough()
7304
9181
  ).max(20).optional(),
9182
+ /**
9183
+ * Toggles exactly as the SDK snapshot reports them (#390).
9184
+ *
9185
+ * Their absence is why `generality-component-control-smarthome`
9186
+ * came back `errored` with a 422 instead of measuring anything:
9187
+ * this schema is `.strict()`, so a scenario that declares a switch
9188
+ * is rejected before the agent is ever asked. Which means the
9189
+ * corpus could not express the single control behind failure 02 of
9190
+ * the functional audit — "disable an agent; the switch never
9191
+ * moves". The one defect the eval most needed to catch was the one
9192
+ * shape it could not describe.
9193
+ */
9194
+ toggles: external_exports.array(
9195
+ external_exports.object({
9196
+ id: external_exports.string().max(120),
9197
+ provenance: identityProvenanceSchema.optional(),
9198
+ onScreen: external_exports.boolean().optional(),
9199
+ label: external_exports.string().max(200).optional(),
9200
+ value: external_exports.boolean().optional(),
9201
+ disabled: external_exports.boolean().optional()
9202
+ })
9203
+ ).max(100).optional(),
9204
+ /**
9205
+ * The interaction graph — the `el:*` ids the prompt tells the model
9206
+ * to prefer over ordinal handles. Passthrough per entry for the
9207
+ * same reason `lists` is: the SDK adds fields faster than this
9208
+ * schema should chase them.
9209
+ */
9210
+ elements: external_exports.array(
9211
+ external_exports.object({
9212
+ id: external_exports.string().max(160),
9213
+ role: external_exports.string().max(40).optional(),
9214
+ label: external_exports.string().max(300).optional()
9215
+ }).passthrough()
9216
+ ).max(500).optional(),
9217
+ /** Screen-level state the relay renders and branches on. */
9218
+ loading: external_exports.boolean().optional(),
9219
+ modalOpen: external_exports.boolean().optional(),
7305
9220
  extra: external_exports.record(external_exports.unknown()).optional()
7306
9221
  }).strict();
7307
9222
  external_exports.object({
@@ -7342,8 +9257,36 @@ external_exports.object({
7342
9257
  * heuristics, forces tool choice when the last user message carries a
7343
9258
  * failure marker). Only meaningful alongside `history`.
7344
9259
  */
7345
- isRecoveryHop: external_exports.boolean().optional()
9260
+ isRecoveryHop: external_exports.boolean().optional(),
9261
+ /**
9262
+ * Run this call against the application structure supplied here
9263
+ * instead of the project's active MCP document.
9264
+ *
9265
+ * Sandbox-only, and the relay honours it only on a `dryRun` call —
9266
+ * see `resolveMcpOverride` in apps/api. It exists because the eval
9267
+ * corpus could otherwise only ever describe ONE app: every scenario
9268
+ * resolves to the same seeded project, so every navigate destination,
9269
+ * every screen title and every bit of domain priming in the prompt
9270
+ * came from the same vehicle-management demo. A suite that can only
9271
+ * pose questions about one app cannot measure whether the agent works
9272
+ * on another one.
9273
+ *
9274
+ * Deliberately loose (`z.record`): the MCP document's shape is owned
9275
+ * by the generator, and pinning it here would turn every additive
9276
+ * generator change into a 422 in the sandbox.
9277
+ */
9278
+ applicationStructure: external_exports.record(external_exports.unknown()).optional()
7346
9279
  }).strict();
9280
+ var relayDiagnosticsSchema = external_exports.object({
9281
+ unverifiedTargets: external_exports.array(external_exports.string()),
9282
+ guardsFired: external_exports.array(external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })),
9283
+ repairMode: external_exports.enum(["repair", "strict", "disabled"]),
9284
+ promptVersion: external_exports.string(),
9285
+ // Defaulted for producers that predate the field: before it existed
9286
+ // every response had reached the model, so `true` is the correct
9287
+ // backfill.
9288
+ modelInvoked: external_exports.boolean().default(true)
9289
+ });
7347
9290
  var sandboxTraceHopSchema = external_exports.object({
7348
9291
  hop: external_exports.number().int().nonnegative(),
7349
9292
  inputPreview: external_exports.string(),
@@ -7382,7 +9325,16 @@ external_exports.object({
7382
9325
  /** Hop-by-hop trace. Always at least one entry. */
7383
9326
  trace: external_exports.array(sandboxTraceHopSchema),
7384
9327
  /** True when the run was a no-op (e.g. project has no MCP yet). */
7385
- warning: external_exports.string().optional()
9328
+ warning: external_exports.string().optional(),
9329
+ /**
9330
+ * SPEC-046 — guard / hallucination telemetry for the run.
9331
+ *
9332
+ * ADDITIVE AND OPTIONAL, permanently: SDK and dashboard consumers
9333
+ * predate it, and older API deployments will not send it. Consumers
9334
+ * must treat an absent block as "no information", never as "no guards
9335
+ * fired".
9336
+ */
9337
+ diagnostics: relayDiagnosticsSchema.optional()
7386
9338
  });
7387
9339
  external_exports.object({
7388
9340
  name: external_exports.string().min(1).max(120),
@@ -7586,6 +9538,30 @@ external_exports.object({
7586
9538
  name: external_exports.string().min(1).max(200).optional(),
7587
9539
  identifiers: endUserIdentifiersSchema.optional()
7588
9540
  });
9541
+ var MAX_EXTERNAL_USER_ID_LENGTH = 128;
9542
+ function normalizeExternalUserId(value) {
9543
+ let raw;
9544
+ if (typeof value === "string") raw = value;
9545
+ else if (typeof value === "number" && Number.isFinite(value)) raw = String(value);
9546
+ else return void 0;
9547
+ const trimmed = raw.trim();
9548
+ if (!trimmed) return void 0;
9549
+ if (trimmed.length > MAX_EXTERNAL_USER_ID_LENGTH) return void 0;
9550
+ return trimmed;
9551
+ }
9552
+ var MAX_DEVICE_INFO_BYTES = 4096;
9553
+ var sessionDeviceInfoSchema = external_exports.object({
9554
+ platform: external_exports.string().max(64).optional(),
9555
+ osVersion: external_exports.string().max(64).optional(),
9556
+ appVersion: external_exports.string().max(64).optional(),
9557
+ sdkVersion: external_exports.string().max(64).optional()
9558
+ }).passthrough().refine((info) => JSON.stringify(info).length <= MAX_DEVICE_INFO_BYTES, {
9559
+ message: `device_info exceeds ${MAX_DEVICE_INFO_BYTES} bytes`
9560
+ });
9561
+ external_exports.object({
9562
+ userId: external_exports.preprocess(normalizeExternalUserId, external_exports.string().optional()),
9563
+ deviceInfo: sessionDeviceInfoSchema.optional().catch(void 0)
9564
+ });
7589
9565
  external_exports.object({
7590
9566
  projectId: external_exports.string().min(1).max(64),
7591
9567
  externalId: external_exports.string().min(1).max(128),
@@ -7593,6 +9569,27 @@ external_exports.object({
7593
9569
  identifiers: endUserIdentifiersSchema.optional()
7594
9570
  });
7595
9571
 
9572
+ // src/utils/stringify.ts
9573
+ function stringifyUnknown(value) {
9574
+ if (value instanceof Error) {
9575
+ const message = value.message || value.name;
9576
+ const cause = value.cause;
9577
+ if (cause instanceof Error && cause.message && cause.message !== message) {
9578
+ return `${message} (${cause.message})`;
9579
+ }
9580
+ return message;
9581
+ }
9582
+ if (typeof value === "string") return value;
9583
+ if (value === null || value === void 0) return String(value);
9584
+ if (typeof value !== "object") return String(value);
9585
+ try {
9586
+ const json = JSON.stringify(value);
9587
+ if (json && json !== "{}") return json;
9588
+ } catch {
9589
+ }
9590
+ return String(value);
9591
+ }
9592
+
7596
9593
  // src/manifest/loadManifest.ts
7597
9594
  var DEFAULT_MANIFEST_FILENAME = "appilots.manifest.json";
7598
9595
  var manifestSchema = external_exports.object({
@@ -7601,7 +9598,7 @@ var manifestSchema = external_exports.object({
7601
9598
  navigation: navigationGraphSchema.partial().optional()
7602
9599
  }).passthrough();
7603
9600
  async function loadManifest(rootDir, manifestPath) {
7604
- const resolvedPath = path6__default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
9601
+ const resolvedPath = path8__default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
7605
9602
  let raw;
7606
9603
  try {
7607
9604
  raw = await readFile(resolvedPath, "utf-8");
@@ -7609,14 +9606,16 @@ async function loadManifest(rootDir, manifestPath) {
7609
9606
  if (error?.code === "ENOENT") {
7610
9607
  return { manifest: null, resolvedPath };
7611
9608
  }
7612
- throw new Error(`Failed to read Appilots manifest at ${resolvedPath}: ${error?.message ?? String(error)}`);
9609
+ throw new Error(
9610
+ `Failed to read Appilots manifest at ${resolvedPath}: ${stringifyUnknown(error)}`
9611
+ );
7613
9612
  }
7614
9613
  let parsedJson;
7615
9614
  try {
7616
9615
  parsedJson = JSON.parse(raw);
7617
9616
  } catch (error) {
7618
9617
  throw new Error(
7619
- `Appilots manifest at ${resolvedPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
9618
+ `Appilots manifest at ${resolvedPath} is not valid JSON: ${stringifyUnknown(error)}`
7620
9619
  );
7621
9620
  }
7622
9621
  const result = manifestSchema.safeParse(parsedJson);
@@ -7630,11 +9629,47 @@ Each entry in "screens" must match the ScreenDescriptor shape (see docs/platform
7630
9629
  }
7631
9630
  return { manifest: result.data, resolvedPath };
7632
9631
  }
9632
+ function mergeScreen(base, declared) {
9633
+ const fields = { ...base };
9634
+ for (const [key, value] of Object.entries(declared)) {
9635
+ if (value === void 0) continue;
9636
+ if (Array.isArray(value) && value.length === 0) continue;
9637
+ fields[key] = value;
9638
+ }
9639
+ const merged = fields;
9640
+ merged.targets = mergeById(base.targets, markManifestOrigin(declared.targets));
9641
+ merged.actions = mergeById(base.actions, declared.actions) ?? [];
9642
+ if (base.agentHints || declared.agentHints) {
9643
+ merged.agentHints = { ...base.agentHints, ...declared.agentHints };
9644
+ }
9645
+ return merged;
9646
+ }
9647
+ function mergeById(base, overlay) {
9648
+ if (!overlay || overlay.length === 0) return base;
9649
+ if (!base || base.length === 0) return overlay;
9650
+ const byId = /* @__PURE__ */ new Map();
9651
+ for (const entry of base) byId.set(entry.id, entry);
9652
+ for (const entry of overlay) {
9653
+ const existing = byId.get(entry.id);
9654
+ byId.set(entry.id, existing ? { ...existing, ...entry } : entry);
9655
+ }
9656
+ return Array.from(byId.values());
9657
+ }
9658
+ function markManifestOrigin(targets) {
9659
+ if (!targets || targets.length === 0) return targets;
9660
+ return targets.map((target) => ({ origin: "manifest", ...target }));
9661
+ }
7633
9662
  function mergeManifestScreens(analyzerScreens, manifestScreens) {
7634
9663
  if (!manifestScreens || manifestScreens.length === 0) return analyzerScreens;
7635
9664
  const byName = /* @__PURE__ */ new Map();
7636
9665
  for (const screen of analyzerScreens) byName.set(screen.name, screen);
7637
- for (const screen of manifestScreens) byName.set(screen.name, screen);
9666
+ for (const declared of manifestScreens) {
9667
+ const base = byName.get(declared.name);
9668
+ byName.set(
9669
+ declared.name,
9670
+ base ? mergeScreen(base, declared) : { ...declared, targets: markManifestOrigin(declared.targets) }
9671
+ );
9672
+ }
7638
9673
  return Array.from(byName.values());
7639
9674
  }
7640
9675
  function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
@@ -7886,12 +9921,15 @@ var MCPGenerator = class _MCPGenerator {
7886
9921
  /**
7887
9922
  * Select the `PlatformAnalyzer` implementation for a `.appilotsrc`
7888
9923
  * `platform` value. `'react-native'` (or unset — the existing default)
7889
- * gets the real Babel/JSX pipeline; anything else gets the no-op
7890
- * generic analyzer, relying entirely on a declared manifest.
9924
+ * gets the RN Babel/JSX pipeline; `'web'` gets the React web (DOM +
9925
+ * React Router) pipeline; anything else gets the no-op generic
9926
+ * analyzer, relying entirely on a declared manifest. The manifest
9927
+ * still merges on top of every analyzer's output either way.
7891
9928
  */
7892
9929
  static createPlatformAnalyzer(platform) {
7893
9930
  const resolved = platform ?? "react-native";
7894
9931
  if (resolved === "react-native") return new ReactNativePlatformAnalyzer();
9932
+ if (resolved === "web") return new ReactWebPlatformAnalyzer();
7895
9933
  return new GenericPlatformAnalyzer(resolved);
7896
9934
  }
7897
9935
  /** Generate MCP documents from the project */
@@ -7921,7 +9959,12 @@ var MCPGenerator = class _MCPGenerator {
7921
9959
  }
7922
9960
  const mergedScreens = mergeManifestScreens(analyzed.screens, manifest?.screens);
7923
9961
  const navigation = mergeManifestNavigation(analyzed.navigation, manifest?.navigation);
7924
- const agentReadyScreens = mergedScreens.map(enrichScreenForAgent);
9962
+ const agentReadyScreens = mergedScreens.map(
9963
+ (screen) => enrichScreenForAgent({
9964
+ ...screen,
9965
+ filePath: screen.filePath ? path8__default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
9966
+ })
9967
+ );
7925
9968
  const projectInfo = await this.getProjectInfo();
7926
9969
  const projectName = projectInfo.name;
7927
9970
  console.log(`[MCPGenerator] Project name: ${projectName}`);
@@ -7944,13 +9987,10 @@ var MCPGenerator = class _MCPGenerator {
7944
9987
  };
7945
9988
  const serialized = JSON.stringify(document, null, 2);
7946
9989
  const checksum = this.calculateChecksum(serialized);
7947
- const filePath = path6__default.resolve(
7948
- outputDir,
7949
- `mcp-document.${this.options.format}`
7950
- );
9990
+ const filePath = path8__default.resolve(outputDir, `mcp-document.${this.options.format}`);
7951
9991
  await writeFile(filePath, serialized, "utf-8");
7952
9992
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
7953
- const checksumFilePath = path6__default.resolve(outputDir, ".appilots-checksum");
9993
+ const checksumFilePath = path8__default.resolve(outputDir, ".appilots-checksum");
7954
9994
  await writeFile(checksumFilePath, checksum, "utf-8");
7955
9995
  console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
7956
9996
  console.log("[MCPGenerator] Generation complete!");
@@ -7970,7 +10010,7 @@ var MCPGenerator = class _MCPGenerator {
7970
10010
  * after calling `generate()`.
7971
10011
  */
7972
10012
  static async readPreviousChecksum(outputDir) {
7973
- const checksumFilePath = path6__default.resolve(outputDir, ".appilots-checksum");
10013
+ const checksumFilePath = path8__default.resolve(outputDir, ".appilots-checksum");
7974
10014
  try {
7975
10015
  const content = await readFile(checksumFilePath, "utf-8");
7976
10016
  return content.trim() || null;
@@ -7989,7 +10029,7 @@ var MCPGenerator = class _MCPGenerator {
7989
10029
  */
7990
10030
  async getProjectInfo() {
7991
10031
  try {
7992
- const packageJsonPath = path6__default.resolve(this.analyzerConfig.rootDir, "package.json");
10032
+ const packageJsonPath = path8__default.resolve(this.analyzerConfig.rootDir, "package.json");
7993
10033
  const packageJsonContent = await readFile(packageJsonPath, "utf-8");
7994
10034
  const packageJson = JSON.parse(packageJsonContent);
7995
10035
  return {
@@ -8002,6 +10042,70 @@ var MCPGenerator = class _MCPGenerator {
8002
10042
  }
8003
10043
  }
8004
10044
  };
10045
+ var DEFAULT_SERVER_URL = "http://localhost:4000";
10046
+ var KNOWN_CONFIG_KEYS = [
10047
+ "apiKey",
10048
+ "projectId",
10049
+ "serverUrl",
10050
+ "outputDir",
10051
+ "include",
10052
+ "exclude",
10053
+ "autoActivate",
10054
+ "strictScreens",
10055
+ "screenPatterns",
10056
+ "navigationInclude",
10057
+ "navigationExclude",
10058
+ "platform",
10059
+ "manifestPath",
10060
+ "eval"
10061
+ ];
10062
+ var KEY_ALIASES = {
10063
+ apiUrl: "serverUrl",
10064
+ // `@appilots/sdk`'s own name for this. One `.appilotsrc` feeds both
10065
+ // packages, so an integrator who wrote the SDK's key should not be told
10066
+ // it is unrecognized.
10067
+ apiBaseUrl: "serverUrl",
10068
+ apiURL: "serverUrl",
10069
+ baseUrl: "serverUrl",
10070
+ url: "serverUrl",
10071
+ host: "serverUrl",
10072
+ endpoint: "serverUrl",
10073
+ server: "serverUrl",
10074
+ key: "apiKey",
10075
+ token: "apiKey",
10076
+ project: "projectId",
10077
+ sourceDir: "include",
10078
+ srcDir: "include",
10079
+ source: "include",
10080
+ outDir: "outputDir"
10081
+ };
10082
+ function editDistance(a, b) {
10083
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
10084
+ for (let i = 1; i <= a.length; i++) {
10085
+ const current = [i];
10086
+ for (let j = 1; j <= b.length; j++) {
10087
+ const deletion = (previous[j] ?? 0) + 1;
10088
+ const insertion = (current[j - 1] ?? 0) + 1;
10089
+ const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
10090
+ current.push(Math.min(deletion, insertion, substitution));
10091
+ }
10092
+ previous = current;
10093
+ }
10094
+ return previous[b.length] ?? 0;
10095
+ }
10096
+ function suggestConfigKey(key) {
10097
+ const alias = KEY_ALIASES[key];
10098
+ if (alias) return alias;
10099
+ const lower = key.toLowerCase();
10100
+ let best;
10101
+ for (const known of KNOWN_CONFIG_KEYS) {
10102
+ const distance = editDistance(lower, known.toLowerCase());
10103
+ if (distance <= 2 && (!best || distance < best.distance)) {
10104
+ best = { key: known, distance };
10105
+ }
10106
+ }
10107
+ return best?.key;
10108
+ }
8005
10109
  function getEnvOverrides(env = process.env) {
8006
10110
  const clean = (value) => {
8007
10111
  const trimmed = value?.trim();
@@ -8013,7 +10117,7 @@ function getEnvOverrides(env = process.env) {
8013
10117
  serverUrl: clean(env.APPILOTS_SERVER_URL)
8014
10118
  };
8015
10119
  }
8016
- function loadConfig() {
10120
+ function loadConfig(onWarn) {
8017
10121
  const configPath = getConfigPath();
8018
10122
  const env = getEnvOverrides();
8019
10123
  let fileConfig = {};
@@ -8021,13 +10125,15 @@ function loadConfig() {
8021
10125
  try {
8022
10126
  fileConfig = JSON.parse(readFileSync(configPath, "utf-8"));
8023
10127
  } catch (error) {
8024
- throw new Error(`Failed to parse .appilotsrc at ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
10128
+ throw new Error(
10129
+ `Failed to parse .appilotsrc at ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
10130
+ );
8025
10131
  }
8026
10132
  } else if (!env.apiKey) {
8027
10133
  return null;
8028
10134
  }
8029
10135
  const merged = {
8030
- serverUrl: "http://localhost:4000",
10136
+ serverUrl: DEFAULT_SERVER_URL,
8031
10137
  outputDir: ".appilots",
8032
10138
  autoActivate: true,
8033
10139
  strictScreens: true,
@@ -8037,6 +10143,12 @@ function loadConfig() {
8037
10143
  ...env.serverUrl ? { serverUrl: env.serverUrl } : {}
8038
10144
  };
8039
10145
  const validation = validateConfig(merged);
10146
+ if (onWarn) {
10147
+ const source = configPath ?? "environment variables";
10148
+ for (const warning of validation.warnings) {
10149
+ onWarn(`${source}: ${warning}`);
10150
+ }
10151
+ }
8040
10152
  if (!validation.valid) {
8041
10153
  const source = configPath ?? "environment variables";
8042
10154
  throw new Error(
@@ -8052,7 +10164,7 @@ function saveConfig(dir, config) {
8052
10164
  const mergedConfig = {
8053
10165
  apiKey: config.apiKey || existingConfig?.apiKey || "",
8054
10166
  projectId: config.projectId || existingConfig?.projectId || "",
8055
- serverUrl: config.serverUrl || existingConfig?.serverUrl || "http://localhost:4000",
10167
+ serverUrl: config.serverUrl || existingConfig?.serverUrl || DEFAULT_SERVER_URL,
8056
10168
  outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
8057
10169
  autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
8058
10170
  include: config.include || existingConfig?.include,
@@ -8068,7 +10180,9 @@ function saveConfig(dir, config) {
8068
10180
  try {
8069
10181
  writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
8070
10182
  } catch (error) {
8071
- throw new Error(`Failed to write .appilotsrc to ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
10183
+ throw new Error(
10184
+ `Failed to write .appilotsrc to ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
10185
+ );
8072
10186
  }
8073
10187
  }
8074
10188
  function getConfigPath() {
@@ -8095,14 +10209,25 @@ function getConfigPath() {
8095
10209
  }
8096
10210
  function validateConfig(config) {
8097
10211
  const errors = [];
10212
+ const warnings = [];
8098
10213
  if (!config || typeof config !== "object") {
8099
10214
  return {
8100
10215
  valid: false,
8101
- errors: ["Configuration must be an object"]
10216
+ errors: ["Configuration must be an object"],
10217
+ warnings
8102
10218
  };
8103
10219
  }
10220
+ for (const key of Object.keys(config)) {
10221
+ if (KNOWN_CONFIG_KEYS.includes(key)) continue;
10222
+ const suggestion = suggestConfigKey(key);
10223
+ warnings.push(
10224
+ `unknown key "${key}" \u2014 ignored` + (suggestion ? ` (did you mean "${suggestion}"?)` : "")
10225
+ );
10226
+ }
8104
10227
  if (!config.apiKey || typeof config.apiKey !== "string") {
8105
- errors.push("apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)");
10228
+ errors.push(
10229
+ "apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)"
10230
+ );
8106
10231
  } else if (!config.apiKey.startsWith("ak_")) {
8107
10232
  errors.push('apiKey must start with "ak_"');
8108
10233
  }
@@ -8164,7 +10289,8 @@ function validateConfig(config) {
8164
10289
  }
8165
10290
  return {
8166
10291
  valid: errors.length === 0,
8167
- errors
10292
+ errors,
10293
+ warnings
8168
10294
  };
8169
10295
  }
8170
10296
 
@@ -8210,19 +10336,76 @@ function formatMetadataWarnings(warnings) {
8210
10336
  );
8211
10337
  }
8212
10338
 
10339
+ // ../shared/dist/chunk-UEMUBXFH.mjs
10340
+ var DEFAULT_API_BASE_URL = "https://api.appilots.com";
10341
+ var APPILOTS_API_PATH_PREFIX = "/api/v1";
10342
+ function normalizeApiBaseUrl(configured) {
10343
+ const raw = (configured ?? DEFAULT_API_BASE_URL).trim().replace(/\/+$/, "");
10344
+ if (!raw) return `${DEFAULT_API_BASE_URL}${APPILOTS_API_PATH_PREFIX}`;
10345
+ if (new RegExp(`${APPILOTS_API_PATH_PREFIX}(/|$)`).test(raw)) return raw;
10346
+ return `${raw}${APPILOTS_API_PATH_PREFIX}`;
10347
+ }
10348
+
8213
10349
  // src/services/api-client.ts
8214
- var DEFAULT_TIMEOUT_MS = 3e4;
10350
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
8215
10351
  var DEFAULT_MAX_RETRIES = 2;
8216
10352
  var RETRY_BASE_DELAY_MS = 500;
10353
+ function describeNetworkError(url, err) {
10354
+ let origin = url;
10355
+ try {
10356
+ origin = new URL(url).origin;
10357
+ } catch {
10358
+ }
10359
+ const cause = err?.cause;
10360
+ const code = typeof cause?.code === "string" ? cause.code : void 0;
10361
+ const detail = code ?? stringifyUnknown(err);
10362
+ return new Error(`Cannot reach Appilots server at ${origin} (${detail})`);
10363
+ }
10364
+ function errorCodeOf(payload) {
10365
+ const error = payload?.error;
10366
+ if (error && typeof error === "object") {
10367
+ const { code } = error;
10368
+ if (typeof code === "string" && code.trim()) return code;
10369
+ }
10370
+ return void 0;
10371
+ }
10372
+ function describeApiError(payload, fallback) {
10373
+ const error = payload?.error;
10374
+ if (typeof error === "string" && error.trim()) return error;
10375
+ if (error && typeof error === "object") {
10376
+ const { code, message: message2 } = error;
10377
+ const text = typeof message2 === "string" && message2.trim() ? message2 : void 0;
10378
+ const tag = typeof code === "string" && code.trim() ? code : void 0;
10379
+ if (text && tag) return `${text} (${tag})`;
10380
+ if (text) return text;
10381
+ if (tag) return tag;
10382
+ }
10383
+ const message = payload?.message;
10384
+ if (typeof message === "string" && message.trim()) return message;
10385
+ return fallback;
10386
+ }
8217
10387
  var AppilotsAPIClient = class {
8218
- serverUrl;
10388
+ /**
10389
+ * `serverUrl` with the API's mount prefix resolved — every path below
10390
+ * is relative to THIS, not to the configured origin.
10391
+ *
10392
+ * It used to be the raw `serverUrl` with `/api/v1` hardcoded into each
10393
+ * path, which made the field mean something different here than in the
10394
+ * SDK. Both read the same `.appilotsrc`: the SDK completes the prefix
10395
+ * when it is missing, so `https://api.appilots.com/api/v1` is correct
10396
+ * there — and here that same value produced
10397
+ * `/api/v1/api/v1/cli/sync`, a 404 whose body reads `Route not found`.
10398
+ * Sharing `normalizeApiBaseUrl` is what makes one file mean one thing:
10399
+ * with or without the prefix now works in both.
10400
+ */
10401
+ baseUrl;
8219
10402
  apiKey;
8220
10403
  timeoutMs;
8221
10404
  maxRetries;
8222
10405
  constructor(config) {
8223
- this.serverUrl = config.serverUrl.replace(/\/$/, "");
10406
+ this.baseUrl = normalizeApiBaseUrl(config.serverUrl);
8224
10407
  this.apiKey = config.apiKey;
8225
- this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10408
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
8226
10409
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
8227
10410
  }
8228
10411
  /**
@@ -8246,7 +10429,7 @@ var AppilotsAPIClient = class {
8246
10429
  }
8247
10430
  return response;
8248
10431
  } catch (err) {
8249
- lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : err;
10432
+ lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : describeNetworkError(url, err);
8250
10433
  } finally {
8251
10434
  clearTimeout(timer);
8252
10435
  }
@@ -8265,7 +10448,7 @@ var AppilotsAPIClient = class {
8265
10448
  */
8266
10449
  async sync(content, version, appVersion) {
8267
10450
  try {
8268
- const response = await this.request(`${this.serverUrl}/api/v1/cli/sync`, {
10451
+ const response = await this.request(`${this.baseUrl}/cli/sync`, {
8269
10452
  method: "POST",
8270
10453
  headers: {
8271
10454
  "Content-Type": "application/json",
@@ -8279,7 +10462,9 @@ var AppilotsAPIClient = class {
8279
10462
  return {
8280
10463
  success: false,
8281
10464
  unchanged: false,
8282
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
10465
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`),
10466
+ errorCode: errorCodeOf(errorData),
10467
+ errorStatus: response.status
8283
10468
  };
8284
10469
  }
8285
10470
  const json = await response.json();
@@ -8292,7 +10477,8 @@ var AppilotsAPIClient = class {
8292
10477
  return {
8293
10478
  success: false,
8294
10479
  unchanged: false,
8295
- error: error instanceof Error ? error.message : "Failed to sync with Appilots API"
10480
+ error: error instanceof Error ? error.message : "Failed to sync with Appilots API",
10481
+ errorStatus: 0
8296
10482
  };
8297
10483
  }
8298
10484
  }
@@ -8303,7 +10489,7 @@ var AppilotsAPIClient = class {
8303
10489
  */
8304
10490
  async status() {
8305
10491
  try {
8306
- const response = await this.request(`${this.serverUrl}/api/v1/cli/status`, {
10492
+ const response = await this.request(`${this.baseUrl}/cli/status`, {
8307
10493
  method: "GET",
8308
10494
  headers: {
8309
10495
  Authorization: `Bearer ${this.apiKey}`
@@ -8312,7 +10498,7 @@ var AppilotsAPIClient = class {
8312
10498
  if (!response.ok) {
8313
10499
  const errorData = await response.json().catch(() => ({}));
8314
10500
  return {
8315
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
10501
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
8316
10502
  };
8317
10503
  }
8318
10504
  const json = await response.json();
@@ -8334,7 +10520,7 @@ var AppilotsAPIClient = class {
8334
10520
  */
8335
10521
  async evalRun(scenarios) {
8336
10522
  try {
8337
- const response = await this.request(`${this.serverUrl}/api/v1/cli/eval/run`, {
10523
+ const response = await this.request(`${this.baseUrl}/cli/eval/run`, {
8338
10524
  method: "POST",
8339
10525
  headers: {
8340
10526
  "Content-Type": "application/json",
@@ -8365,7 +10551,7 @@ var AppilotsAPIClient = class {
8365
10551
  */
8366
10552
  async health() {
8367
10553
  try {
8368
- const response = await this.request(`${this.serverUrl}/api/v1/health`, {
10554
+ const response = await this.request(`${this.baseUrl}/health`, {
8369
10555
  method: "GET"
8370
10556
  });
8371
10557
  return response.ok;
@@ -8375,6 +10561,9 @@ var AppilotsAPIClient = class {
8375
10561
  }
8376
10562
  };
8377
10563
 
8378
- export { AppilotsAPIClient, ComponentAnalyzer, DEFAULT_MANIFEST_FILENAME, FormAnalyzer, GenericPlatformAnalyzer, MCPGenerator, NavigationAnalyzer, ReactNativePlatformAnalyzer, ScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, loadManifest, mergeManifestNavigation, mergeManifestScreens, saveConfig, validateConfig };
10564
+ // src/version.ts
10565
+ var CLI_VERSION = "0.10.0";
10566
+
10567
+ export { AppilotsAPIClient, CLI_VERSION, ComponentAnalyzer, DEFAULT_MANIFEST_FILENAME, DEFAULT_WEB_SCREEN_PATTERNS, FormAnalyzer, GenericPlatformAnalyzer, MCPGenerator, NavigationAnalyzer, ReactNativePlatformAnalyzer, ReactWebPlatformAnalyzer, ScreenAnalyzer, WebNavigationAnalyzer, WebScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, loadManifest, mergeManifestNavigation, mergeManifestScreens, resolvePathToScreen, saveConfig, screenNameFromPath, validateConfig };
8379
10568
  //# sourceMappingURL=index.mjs.map
8380
10569
  //# sourceMappingURL=index.mjs.map