@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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var fs = require('fs/promises');
4
- var path6 = require('path');
4
+ var path8 = require('path');
5
5
  var traverse4 = require('@babel/traverse');
6
6
  var BabelTypes = require('@babel/types');
7
7
  var glob = require('fast-glob');
@@ -30,7 +30,7 @@ function _interopNamespace(e) {
30
30
  }
31
31
 
32
32
  var fs__default = /*#__PURE__*/_interopDefault(fs);
33
- var path6__namespace = /*#__PURE__*/_interopNamespace(path6);
33
+ var path8__namespace = /*#__PURE__*/_interopNamespace(path8);
34
34
  var traverse4__default = /*#__PURE__*/_interopDefault(traverse4);
35
35
  var BabelTypes__namespace = /*#__PURE__*/_interopNamespace(BabelTypes);
36
36
  var glob__default = /*#__PURE__*/_interopDefault(glob);
@@ -83,6 +83,45 @@ function hasJsxAttribute(element, name) {
83
83
  }
84
84
 
85
85
  // src/ast/jsx/classify.ts
86
+ var NON_INTERACTION_HANDLERS = /* @__PURE__ */ new Set([
87
+ "onLayout",
88
+ "onScroll",
89
+ "onScrollBeginDrag",
90
+ "onScrollEndDrag",
91
+ "onMomentumScrollBegin",
92
+ "onMomentumScrollEnd",
93
+ "onContentSizeChange",
94
+ "onEndReached",
95
+ "onViewableItemsChanged",
96
+ "onLoad",
97
+ "onLoadStart",
98
+ "onLoadEnd",
99
+ "onError",
100
+ "onProgress",
101
+ "onFocus",
102
+ "onBlur"
103
+ ]);
104
+ var VALUE_HANDLERS = /* @__PURE__ */ new Set([
105
+ "onChange",
106
+ "onChangeText",
107
+ "onValueChange",
108
+ "onSelect",
109
+ "onSelectionChange",
110
+ "onSubmitEditing"
111
+ ]);
112
+ function pressHandlerProp(element) {
113
+ let fallback;
114
+ for (const attr of element.attributes) {
115
+ if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
116
+ const name = attr.name.name;
117
+ if (!/^on[A-Z]/.test(name)) continue;
118
+ if (attr.value?.type !== "JSXExpressionContainer") continue;
119
+ if (name === "onPress") return name;
120
+ if (NON_INTERACTION_HANDLERS.has(name) || VALUE_HANDLERS.has(name)) continue;
121
+ fallback ??= name;
122
+ }
123
+ return fallback;
124
+ }
86
125
  var VIEW_COMPONENTS = /* @__PURE__ */ new Set(["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"]);
87
126
  var INPUT_COMPONENTS = /* @__PURE__ */ new Set(["TextInput", "Input", "HocInput"]);
88
127
  var BUTTON_COMPONENTS = /* @__PURE__ */ new Set(["TouchableOpacity", "Pressable", "Button"]);
@@ -97,17 +136,16 @@ function classifyJsxComponent(name, element) {
97
136
  if (INPUT_COMPONENTS.has(name)) return "input";
98
137
  if (BUTTON_COMPONENTS.has(name)) return "button";
99
138
  if (element) {
100
- const hasOptions = hasJsxAttribute(element, "options");
139
+ const hasOptions = hasJsxAttribute(element, "options") || hasJsxAttribute(element, "segments") || hasJsxAttribute(element, "choices") || hasJsxAttribute(element, "tabs");
101
140
  const hasValue = hasJsxAttribute(element, "value");
102
141
  const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
103
142
  const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange") || hasJsxAttribute(element, "onChangeText");
104
- const hasOnPress = hasJsxAttribute(element, "onPress");
105
143
  if (hasOptions && (hasValue || hasOnChange)) return "select";
106
144
  if (hasChecked && hasOnChange) return "toggle";
107
145
  if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
108
146
  return "input";
109
147
  }
110
- if (hasOnPress) return "button";
148
+ if (pressHandlerProp(element)) return "button";
111
149
  if (getStringAttr(element, "visible") || getExpressionIdentifierAttr(element, "visible")) {
112
150
  return "modal";
113
151
  }
@@ -419,12 +457,12 @@ var ScreenAnalyzer = class {
419
457
  cwd: this.config.rootDir,
420
458
  ignore: exclude
421
459
  });
422
- screenPatternFiles = new Set(matched.map((f) => path6__namespace.default.resolve(this.config.rootDir, f)));
460
+ screenPatternFiles = new Set(matched.map((f) => path8__namespace.default.resolve(this.config.rootDir, f)));
423
461
  }
424
462
  const screens = [];
425
463
  this.screensFilteredOut = 0;
426
464
  for (const file of files) {
427
- const filePath = path6__namespace.default.resolve(this.config.rootDir, file);
465
+ const filePath = path8__namespace.default.resolve(this.config.rootDir, file);
428
466
  try {
429
467
  const descriptor = await this.analyzeFile(filePath);
430
468
  if (!descriptor) continue;
@@ -445,12 +483,17 @@ var ScreenAnalyzer = class {
445
483
  }
446
484
  } catch (error) {
447
485
  if (this.verbose) {
448
- console.warn(`[ScreenAnalyzer] Failed to parse ${file}:`, error instanceof Error ? error.message : error);
486
+ console.warn(
487
+ `[ScreenAnalyzer] Failed to parse ${file}:`,
488
+ error instanceof Error ? error.message : error
489
+ );
449
490
  }
450
491
  }
451
492
  }
452
493
  if (this.verbose && this.strictScreens) {
453
- console.log(`[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`);
494
+ console.log(
495
+ `[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`
496
+ );
454
497
  }
455
498
  return screens;
456
499
  }
@@ -738,7 +781,11 @@ var ScreenAnalyzer = class {
738
781
  for (const form of secondary) {
739
782
  const existing = out.find((candidate) => candidate.id === form.id) ?? this.findFormWithSharedFields(out, form);
740
783
  if (!existing) {
741
- out.push({ ...form, fields: [...form.fields], submitAction: this.namedSubmitAction(form.submitAction) });
784
+ out.push({
785
+ ...form,
786
+ fields: [...form.fields],
787
+ submitAction: this.namedSubmitAction(form.submitAction)
788
+ });
742
789
  continue;
743
790
  }
744
791
  for (const field of form.fields) {
@@ -757,9 +804,12 @@ var ScreenAnalyzer = class {
757
804
  findEquivalentField(fields, incoming) {
758
805
  return fields.find((field) => {
759
806
  if (field.name && incoming.name && field.name === incoming.name) return true;
760
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
761
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
762
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
807
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
808
+ return true;
809
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
810
+ return true;
811
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
812
+ return true;
763
813
  if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
764
814
  return true;
765
815
  }
@@ -919,9 +969,11 @@ var ScreenAnalyzer = class {
919
969
  source: field.locator?.source ?? "accessibilityLabel"
920
970
  });
921
971
  } else if (attrName === "keyboardType") {
922
- if (attr.value.value === "email-address" || attr.value.value === "email") field.type = "email";
972
+ if (attr.value.value === "email-address" || attr.value.value === "email")
973
+ field.type = "email";
923
974
  if (attr.value.value === "phone-pad") field.type = "phone";
924
- if (attr.value.value === "numeric" || attr.value.value === "number-pad") field.type = "number";
975
+ if (attr.value.value === "numeric" || attr.value.value === "number-pad")
976
+ field.type = "number";
925
977
  }
926
978
  } else if (BabelTypes__namespace.isJSXExpressionContainer(attr.value)) {
927
979
  if (attrName === "value" && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
@@ -1087,8 +1139,10 @@ var ScreenAnalyzer = class {
1087
1139
  };
1088
1140
  if (handler.nativeConfirmationExpected) action.nativeConfirmationExpected = true;
1089
1141
  if (handler.targetScreen && !action.targetScreen) action.targetScreen = handler.targetScreen;
1090
- if (handler.successSignal && !action.successSignal) action.successSignal = handler.successSignal;
1091
- if (handler.failureSignal && !action.failureSignal) action.failureSignal = handler.failureSignal;
1142
+ if (handler.successSignal && !action.successSignal)
1143
+ action.successSignal = handler.successSignal;
1144
+ if (handler.failureSignal && !action.failureSignal)
1145
+ action.failureSignal = handler.failureSignal;
1092
1146
  if (handler.opensModal && !action.opensModal) action.opensModal = handler.opensModal;
1093
1147
  if (handler.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
1094
1148
  action.destructive = true;
@@ -1106,6 +1160,7 @@ var ScreenAnalyzer = class {
1106
1160
  if (!BabelTypes__namespace.isJSXIdentifier(element.name)) return;
1107
1161
  const componentName = element.name.name;
1108
1162
  if (classifyJsxComponent(componentName, element) !== "button") return;
1163
+ const pressProp = pressHandlerProp(element);
1109
1164
  let label;
1110
1165
  let handler;
1111
1166
  for (const attr of element.attributes) {
@@ -1115,7 +1170,7 @@ var ScreenAnalyzer = class {
1115
1170
  if ((attrName === "title" || attrName === "accessibilityLabel" || attrName === "label") && BabelTypes__namespace.isStringLiteral(attr.value)) {
1116
1171
  label = attr.value.value;
1117
1172
  }
1118
- if (attrName === "onPress" && BabelTypes__namespace.isJSXExpressionContainer(attr.value) && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
1173
+ if (attrName === pressProp && BabelTypes__namespace.isJSXExpressionContainer(attr.value) && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
1119
1174
  handler = attr.value.expression.name;
1120
1175
  }
1121
1176
  }
@@ -1181,10 +1236,13 @@ var ScreenAnalyzer = class {
1181
1236
  }
1182
1237
  };
1183
1238
  if (fn.body) {
1184
- traverse4__default.default(fn.body, {
1185
- noScope: true,
1186
- enter: (nodePath) => inspectNode(nodePath.node)
1187
- });
1239
+ traverse4__default.default(
1240
+ fn.body,
1241
+ {
1242
+ noScope: true,
1243
+ enter: (nodePath) => inspectNode(nodePath.node)
1244
+ }
1245
+ );
1188
1246
  }
1189
1247
  const expectedOutcome = targetScreen ? "navigation" : hasStateSetter ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
1190
1248
  return {
@@ -1213,7 +1271,7 @@ var ScreenAnalyzer = class {
1213
1271
  * heuristic must hit OR the JSDoc tag must be present.
1214
1272
  */
1215
1273
  isElementDestructive(element, handlerName, actionId) {
1216
- const DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
1274
+ const DESTRUCTIVE_VERB3 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
1217
1275
  for (const attr of element.attributes) {
1218
1276
  if (!BabelTypes__namespace.isJSXAttribute(attr)) continue;
1219
1277
  const attrName = BabelTypes__namespace.isJSXIdentifier(attr.name) ? attr.name.name : null;
@@ -1228,8 +1286,8 @@ var ScreenAnalyzer = class {
1228
1286
  }
1229
1287
  }
1230
1288
  }
1231
- if (handlerName && DESTRUCTIVE_VERB2.test(handlerName)) return true;
1232
- if (actionId && DESTRUCTIVE_VERB2.test(actionId)) return true;
1289
+ if (handlerName && DESTRUCTIVE_VERB3.test(handlerName)) return true;
1290
+ if (actionId && DESTRUCTIVE_VERB3.test(actionId)) return true;
1233
1291
  return false;
1234
1292
  }
1235
1293
  /**
@@ -1242,6 +1300,7 @@ var ScreenAnalyzer = class {
1242
1300
  };
1243
1301
  const componentName = BabelTypes__namespace.isJSXIdentifier(element.name) ? element.name.name : void 0;
1244
1302
  action.sourceComponent = componentName;
1303
+ const pressProp = pressHandlerProp(element) ?? "onPress";
1245
1304
  let handlerName;
1246
1305
  for (const attr of element.attributes) {
1247
1306
  if (!BabelTypes__namespace.isJSXAttribute(attr)) continue;
@@ -1276,7 +1335,7 @@ var ScreenAnalyzer = class {
1276
1335
  }
1277
1336
  }
1278
1337
  } else if (BabelTypes__namespace.isJSXExpressionContainer(attr.value)) {
1279
- if (attrName === "onPress" && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
1338
+ if (attrName === pressProp && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
1280
1339
  handlerName = attr.value.expression.name.toLowerCase();
1281
1340
  action.handler = attr.value.expression.name;
1282
1341
  if (handlerName.includes("submit")) {
@@ -1284,7 +1343,7 @@ var ScreenAnalyzer = class {
1284
1343
  } else if (handlerName.includes("navigate")) {
1285
1344
  action.type = "navigation";
1286
1345
  }
1287
- } else if (attrName === "onPress") {
1346
+ } else if (attrName === pressProp) {
1288
1347
  const inline = this.extractInlineOnPressMetadata(attr.value.expression);
1289
1348
  if (inline.handler) {
1290
1349
  handlerName = inline.handler.toLowerCase();
@@ -1308,7 +1367,11 @@ var ScreenAnalyzer = class {
1308
1367
  }
1309
1368
  }
1310
1369
  if (!action.locator && action.id) {
1311
- action.locator = { id: action.id, label: action.label, source: action.label ? "label" : "inferred" };
1370
+ action.locator = {
1371
+ id: action.id,
1372
+ label: action.label,
1373
+ source: action.label ? "label" : "inferred"
1374
+ };
1312
1375
  }
1313
1376
  if (this.isElementDestructive(element, handlerName, action.id)) {
1314
1377
  action.destructive = true;
@@ -1450,24 +1513,27 @@ var ScreenAnalyzer = class {
1450
1513
  }
1451
1514
  extractRowAction(fn) {
1452
1515
  let action;
1453
- traverse4__default.default(fn.body, {
1454
- noScope: true,
1455
- CallExpression: (nodePath) => {
1456
- const node = nodePath.node;
1457
- if (!BabelTypes__namespace.isMemberExpression(node.callee) || !BabelTypes__namespace.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !BabelTypes__namespace.isIdentifier(node.callee.property) || !["navigate", "push", "replace"].includes(node.callee.property.name)) {
1458
- return;
1516
+ traverse4__default.default(
1517
+ fn.body,
1518
+ {
1519
+ noScope: true,
1520
+ CallExpression: (nodePath) => {
1521
+ const node = nodePath.node;
1522
+ if (!BabelTypes__namespace.isMemberExpression(node.callee) || !BabelTypes__namespace.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !BabelTypes__namespace.isIdentifier(node.callee.property) || !["navigate", "push", "replace"].includes(node.callee.property.name)) {
1523
+ return;
1524
+ }
1525
+ const firstArg = node.arguments[0];
1526
+ if (!BabelTypes__namespace.isStringLiteral(firstArg)) return;
1527
+ const params = this.extractNavigationParams(node.arguments[1]);
1528
+ action = {
1529
+ type: "navigation",
1530
+ targetScreen: firstArg.value,
1531
+ ...Object.keys(params).length > 0 ? { params } : {},
1532
+ description: `Pressing a row opens ${firstArg.value}`
1533
+ };
1459
1534
  }
1460
- const firstArg = node.arguments[0];
1461
- if (!BabelTypes__namespace.isStringLiteral(firstArg)) return;
1462
- const params = this.extractNavigationParams(node.arguments[1]);
1463
- action = {
1464
- type: "navigation",
1465
- targetScreen: firstArg.value,
1466
- ...Object.keys(params).length > 0 ? { params } : {},
1467
- description: `Pressing a row opens ${firstArg.value}`
1468
- };
1469
1535
  }
1470
- });
1536
+ );
1471
1537
  return action;
1472
1538
  }
1473
1539
  extractNavigationParams(arg) {
@@ -1500,15 +1566,18 @@ var ScreenAnalyzer = class {
1500
1566
  } else if (BabelTypes__namespace.isIdentifier(firstParam)) {
1501
1567
  itemNames.add(firstParam.name);
1502
1568
  }
1503
- traverse4__default.default(fn.body, {
1504
- noScope: true,
1505
- MemberExpression: (nodePath) => {
1506
- const node = nodePath.node;
1507
- if (BabelTypes__namespace.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes__namespace.isIdentifier(node.property)) {
1508
- fields.add(node.property.name);
1569
+ traverse4__default.default(
1570
+ fn.body,
1571
+ {
1572
+ noScope: true,
1573
+ MemberExpression: (nodePath) => {
1574
+ const node = nodePath.node;
1575
+ if (BabelTypes__namespace.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes__namespace.isIdentifier(node.property)) {
1576
+ fields.add(node.property.name);
1577
+ }
1509
1578
  }
1510
1579
  }
1511
- });
1580
+ );
1512
1581
  return Array.from(fields).sort();
1513
1582
  }
1514
1583
  inferItemType(dataSource, renderItem, displayFields) {
@@ -1520,11 +1589,19 @@ var ScreenAnalyzer = class {
1520
1589
  if (displayFields.length > 0) return "Item";
1521
1590
  return void 0;
1522
1591
  }
1592
+ /**
1593
+ * Identity comes from how a field is NAMED, not from what the app
1594
+ * sells: `id`/`uuid`/`key`/`slug` are conventions any codebase uses,
1595
+ * `name`/`title`/`email` are how any row introduces itself. `plate`
1596
+ * used to sit in this list and read like one of them — but it is the
1597
+ * example app's schema, and no other tenant ever got its equivalent
1598
+ * (`mrn`, `trackingNumber`, `sku`) added here.
1599
+ */
1523
1600
  inferIdentityFields(keyField, displayFields) {
1524
1601
  const out = /* @__PURE__ */ new Set();
1525
1602
  if (keyField) out.add(keyField);
1526
1603
  for (const field of displayFields) {
1527
- if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
1604
+ if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
1528
1605
  }
1529
1606
  return Array.from(out);
1530
1607
  }
@@ -1535,17 +1612,23 @@ var ScreenAnalyzer = class {
1535
1612
  CallExpression: (nodePath) => {
1536
1613
  const node = nodePath.node;
1537
1614
  if (!BabelTypes__namespace.isMemberExpression(node.callee)) return;
1538
- if (!BabelTypes__namespace.isIdentifier(node.callee.property) || node.callee.property.name !== "filter") return;
1539
- if (!BabelTypes__namespace.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource) return;
1615
+ if (!BabelTypes__namespace.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
1616
+ return;
1617
+ if (!BabelTypes__namespace.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource)
1618
+ return;
1540
1619
  const fn = node.arguments[0];
1541
- if (!BabelTypes__namespace.isArrowFunctionExpression(fn) && !BabelTypes__namespace.isFunctionExpression(fn)) return;
1542
- traverse4__default.default(fn.body, {
1543
- noScope: true,
1544
- Identifier: (innerPath) => {
1545
- const name = innerPath.node.name;
1546
- if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
1620
+ if (!BabelTypes__namespace.isArrowFunctionExpression(fn) && !BabelTypes__namespace.isFunctionExpression(fn))
1621
+ return;
1622
+ traverse4__default.default(
1623
+ fn.body,
1624
+ {
1625
+ noScope: true,
1626
+ Identifier: (innerPath) => {
1627
+ const name = innerPath.node.name;
1628
+ if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
1629
+ }
1547
1630
  }
1548
- });
1631
+ );
1549
1632
  }
1550
1633
  });
1551
1634
  return queryBinding;
@@ -1573,7 +1656,7 @@ var ScreenAnalyzer = class {
1573
1656
  * E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
1574
1657
  */
1575
1658
  extractScreenName(filePath) {
1576
- const basename2 = path6__namespace.default.basename(filePath);
1659
+ const basename2 = path8__namespace.default.basename(filePath);
1577
1660
  return basename2.replace(/\.(tsx?|jsx?)$/, "");
1578
1661
  }
1579
1662
  };
@@ -1628,7 +1711,7 @@ var NavigationAnalyzer = class {
1628
1711
  cwd: this.config.rootDir,
1629
1712
  ignore: excludePatterns
1630
1713
  });
1631
- return files.map((file) => path6__namespace.default.join(this.config.rootDir, file));
1714
+ return files.map((file) => path8__namespace.default.join(this.config.rootDir, file));
1632
1715
  }
1633
1716
  /** Parse navigator definitions from a file */
1634
1717
  parseNavigators(content, filePath) {
@@ -1689,11 +1772,7 @@ var NavigationAnalyzer = class {
1689
1772
  if (BabelTypes__namespace.isJSXAttribute(initialRouteAttr) && BabelTypes__namespace.isStringLiteral(initialRouteAttr.value)) {
1690
1773
  navigator.initialRouteName = initialRouteAttr.value.value;
1691
1774
  }
1692
- const screens = this.extractScreensFromNavigator(
1693
- node,
1694
- objectName,
1695
- navigator.type
1696
- );
1775
+ const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
1697
1776
  screensByNavigator.set(objectName, screens);
1698
1777
  }
1699
1778
  }
@@ -1710,27 +1789,112 @@ var NavigationAnalyzer = class {
1710
1789
  }
1711
1790
  return navigators;
1712
1791
  }
1792
+ /**
1793
+ * Is this JSX element `<navigatorVarName.MEMBER …>`?
1794
+ */
1795
+ // Deliberately NOT a type predicate (`node is t.JSXElement`): a false
1796
+ // result would then narrow `node` to "not a JSXElement", and the very
1797
+ // next check — the same node against a different member — would see
1798
+ // `never`. It is a question about the member name, not about the node
1799
+ // kind.
1800
+ isNavigatorMember(node, navigatorVarName, member) {
1801
+ if (!BabelTypes__namespace.isJSXElement(node)) return false;
1802
+ const name = node.openingElement.name;
1803
+ return BabelTypes__namespace.isJSXMemberExpression(name) && BabelTypes__namespace.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && BabelTypes__namespace.isJSXIdentifier(name.property) && name.property.name === member;
1804
+ }
1805
+ /**
1806
+ * Flatten a navigator's JSX children into the `<X.Screen>` elements they
1807
+ * contain, unwrapping every container a real app puts in between.
1808
+ *
1809
+ * The old version compared `t.isJSXElement(child)` against the direct
1810
+ * children only. A ternary is a `JSXExpressionContainer`, so an
1811
+ * auth-gated root — the modal shape of a commercial app, and the shape
1812
+ * of this repo's own `apps/example-app` — contributed ZERO screens to
1813
+ * the graph while `appilots sync` reported success (#396).
1814
+ *
1815
+ * Both branches of a conditional are collected on purpose. The graph is
1816
+ * a design-time map of what routes EXIST, not a prediction of which one
1817
+ * a given session will render; the agent needs the destination name to
1818
+ * be there whether or not the user happens to be logged in right now.
1819
+ */
1820
+ collectScreenElements(children, navigatorVarName, depth = 0) {
1821
+ if (depth > 12) return [];
1822
+ const found = [];
1823
+ const visit = (node) => {
1824
+ if (!node) return;
1825
+ if (this.isNavigatorMember(node, navigatorVarName, "Screen")) {
1826
+ found.push(node);
1827
+ return;
1828
+ }
1829
+ if (this.isNavigatorMember(node, navigatorVarName, "Group")) {
1830
+ const group = node;
1831
+ found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
1832
+ return;
1833
+ }
1834
+ if (BabelTypes__namespace.isJSXElement(node)) {
1835
+ const name = node.openingElement.name;
1836
+ const isForeignNavigator = BabelTypes__namespace.isJSXMemberExpression(name) && BabelTypes__namespace.isJSXIdentifier(name.property) && name.property.name === "Navigator";
1837
+ if (isForeignNavigator) return;
1838
+ found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
1839
+ return;
1840
+ }
1841
+ if (BabelTypes__namespace.isJSXFragment(node)) {
1842
+ found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
1843
+ return;
1844
+ }
1845
+ if (BabelTypes__namespace.isJSXExpressionContainer(node)) {
1846
+ visit(node.expression);
1847
+ return;
1848
+ }
1849
+ if (BabelTypes__namespace.isConditionalExpression(node)) {
1850
+ visit(node.consequent);
1851
+ visit(node.alternate);
1852
+ return;
1853
+ }
1854
+ if (BabelTypes__namespace.isLogicalExpression(node)) {
1855
+ visit(node.left);
1856
+ visit(node.right);
1857
+ return;
1858
+ }
1859
+ if (BabelTypes__namespace.isCallExpression(node)) {
1860
+ for (const arg of node.arguments) {
1861
+ if (BabelTypes__namespace.isArrowFunctionExpression(arg) || BabelTypes__namespace.isFunctionExpression(arg)) {
1862
+ if (BabelTypes__namespace.isBlockStatement(arg.body)) {
1863
+ for (const stmt of arg.body.body) {
1864
+ if (BabelTypes__namespace.isReturnStatement(stmt)) visit(stmt.argument);
1865
+ }
1866
+ } else {
1867
+ visit(arg.body);
1868
+ }
1869
+ }
1870
+ }
1871
+ return;
1872
+ }
1873
+ if (BabelTypes__namespace.isArrayExpression(node)) {
1874
+ for (const el of node.elements) visit(el);
1875
+ return;
1876
+ }
1877
+ if (BabelTypes__namespace.isTSAsExpression(node) || BabelTypes__namespace.isTSNonNullExpression(node)) {
1878
+ visit(node.expression);
1879
+ }
1880
+ };
1881
+ for (const child of children) visit(child);
1882
+ return found;
1883
+ }
1713
1884
  /** Extract screens from a navigator JSX element */
1714
1885
  extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
1715
1886
  const screens = [];
1716
1887
  if (!navigatorElement.children) return screens;
1717
- for (const child of navigatorElement.children) {
1718
- if (BabelTypes__namespace.isJSXElement(child) && BabelTypes__namespace.isJSXMemberExpression(child.openingElement.name)) {
1719
- const memberExpr = child.openingElement.name;
1720
- if (BabelTypes__namespace.isJSXIdentifier(memberExpr.object) && memberExpr.object.name === navigatorVarName && BabelTypes__namespace.isJSXIdentifier(memberExpr.property) && memberExpr.property.name === "Screen") {
1721
- const screenName = this.extractAttributeValue(
1722
- child.openingElement.attributes,
1723
- "name"
1724
- );
1725
- if (screenName) {
1726
- screens.push({
1727
- name: screenName,
1728
- navigatorName: navigatorVarName,
1729
- navigatorType
1730
- });
1731
- }
1732
- }
1733
- }
1888
+ const seen = /* @__PURE__ */ new Set();
1889
+ for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
1890
+ const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
1891
+ if (!screenName || seen.has(screenName)) continue;
1892
+ seen.add(screenName);
1893
+ screens.push({
1894
+ name: screenName,
1895
+ navigatorName: navigatorVarName,
1896
+ navigatorType
1897
+ });
1734
1898
  }
1735
1899
  return screens;
1736
1900
  }
@@ -1789,9 +1953,7 @@ var NavigationAnalyzer = class {
1789
1953
  if (member.type === "TSPropertySignature" && member.key) {
1790
1954
  const keyName = member.key.type === "Identifier" ? member.key.name : null;
1791
1955
  if (keyName && member.typeAnnotation) {
1792
- const params = this.extractParamsFromType(
1793
- member.typeAnnotation.typeAnnotation
1794
- );
1956
+ const params = this.extractParamsFromType(member.typeAnnotation.typeAnnotation);
1795
1957
  entries.set(keyName, params);
1796
1958
  }
1797
1959
  }
@@ -1831,7 +1993,7 @@ var NavigationAnalyzer = class {
1831
1993
  if (type.type === "TSUndefinedKeyword") return "undefined";
1832
1994
  if (type.type === "TSNullKeyword") return "null";
1833
1995
  if (type.type === "TSUnionType") {
1834
- return type.types.map((t8) => this.typeToString(t8)).join(" | ");
1996
+ return type.types.map((t12) => this.typeToString(t12)).join(" | ");
1835
1997
  }
1836
1998
  if (type.type === "TSTypeLiteral") {
1837
1999
  return "object";
@@ -1852,7 +2014,7 @@ var NavigationAnalyzer = class {
1852
2014
  /** Attach parsed type params to navigator screens */
1853
2015
  attachParamsToNavigators(navigators, types) {
1854
2016
  for (const navigator of navigators) {
1855
- const matchingType = types.find((t8) => t8.type === navigator.type);
2017
+ const matchingType = types.find((t12) => t12.type === navigator.type);
1856
2018
  if (matchingType) {
1857
2019
  for (const screen of navigator.screens) {
1858
2020
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -1870,6 +2032,11 @@ var NavigationAnalyzer = class {
1870
2032
  let initialScreen = "";
1871
2033
  for (const navigator of parsedNavigators) {
1872
2034
  const screenNames = navigator.screens.map((s) => s.name);
2035
+ if (screenNames.length === 0) {
2036
+ console.warn(
2037
+ `[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.`
2038
+ );
2039
+ }
1873
2040
  navigators.push({
1874
2041
  name: navigator.name,
1875
2042
  type: navigator.type,
@@ -1937,8 +2104,8 @@ var ComponentAnalyzer = class {
1937
2104
  });
1938
2105
  const components = [];
1939
2106
  traverse4__default.default(ast, {
1940
- JSXElement: (path7) => {
1941
- const component = this.extractComponentFromJSXElement(path7.node);
2107
+ JSXElement: (path9) => {
2108
+ const component = this.extractComponentFromJSXElement(path9.node);
1942
2109
  if (component) {
1943
2110
  components.push(component);
1944
2111
  }
@@ -2065,13 +2232,13 @@ var FormAnalyzer = class {
2065
2232
  this.inputElements = [];
2066
2233
  this.submitButtons = [];
2067
2234
  traverse4__default.default(ast, {
2068
- CallExpression: (path7) => {
2069
- this.extractStateVariables(path7.node);
2235
+ CallExpression: (path9) => {
2236
+ this.extractStateVariables(path9.node);
2070
2237
  }
2071
2238
  });
2072
2239
  traverse4__default.default(ast, {
2073
- JSXElement: (path7) => {
2074
- this.extractFormElements(path7.node);
2240
+ JSXElement: (path9) => {
2241
+ this.extractFormElements(path9.node);
2075
2242
  }
2076
2243
  });
2077
2244
  const validationRules = this.extractValidationRules(ast);
@@ -2171,8 +2338,8 @@ var FormAnalyzer = class {
2171
2338
  extractValidationRules(ast) {
2172
2339
  const rules = {};
2173
2340
  traverse4__default.default(ast, {
2174
- IfStatement: (path7) => {
2175
- const test = path7.node.test;
2341
+ IfStatement: (path9) => {
2342
+ const test = path9.node.test;
2176
2343
  const rule = this.extractRuleFromCondition(test);
2177
2344
  if (rule) {
2178
2345
  const { field, description } = rule;
@@ -2226,7 +2393,7 @@ var FormAnalyzer = class {
2226
2393
  }
2227
2394
  buildForms(filePath, validationRules) {
2228
2395
  if (this.inputElements.length === 0) return [];
2229
- const fileName = path6__namespace.basename(filePath, path6__namespace.extname(filePath));
2396
+ const fileName = path8__namespace.basename(filePath, path8__namespace.extname(filePath));
2230
2397
  const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
2231
2398
  const fields = this.inputElements.map((input) => {
2232
2399
  const fieldType = this.inferFieldType(input);
@@ -2304,7 +2471,7 @@ var ReactNativePlatformAnalyzer = class {
2304
2471
  `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
2305
2472
  );
2306
2473
  const enrichmentPromises = screenFiles.map(async (file) => {
2307
- const filePath = path6__namespace.default.resolve(config.rootDir, file);
2474
+ const filePath = path8__namespace.default.resolve(config.rootDir, file);
2308
2475
  try {
2309
2476
  const [components, forms] = await Promise.all([
2310
2477
  componentAnalyzer.analyzeFile(filePath),
@@ -2347,91 +2514,1579 @@ var ReactNativePlatformAnalyzer = class {
2347
2514
  });
2348
2515
  }
2349
2516
  }
2350
- return {
2351
- ...screen,
2352
- components: [...screen.components, ...newComponents],
2353
- forms: mergedForms
2354
- };
2517
+ return {
2518
+ ...screen,
2519
+ components: [...screen.components, ...newComponents],
2520
+ forms: mergedForms
2521
+ };
2522
+ }
2523
+ return screen;
2524
+ });
2525
+ return {
2526
+ screens: enrichedScreens,
2527
+ navigation,
2528
+ analyzedFiles: screenFiles.length,
2529
+ ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
2530
+ };
2531
+ }
2532
+ mergeForm(target, source) {
2533
+ for (const field of source.fields) {
2534
+ const existingField = this.findEquivalentField(target.fields, field);
2535
+ if (existingField) {
2536
+ this.mergeField(existingField, field);
2537
+ } else {
2538
+ target.fields.push(field);
2539
+ }
2540
+ }
2541
+ target.submitAction = target.submitAction ?? this.namedSubmitAction(source.submitAction);
2542
+ target.validationRules = source.validationRules ? { ...source.validationRules, ...target.validationRules ?? {} } : target.validationRules;
2543
+ }
2544
+ findEquivalentField(fields, incoming) {
2545
+ return fields.find((field) => {
2546
+ if (field.name && incoming.name && field.name === incoming.name) return true;
2547
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
2548
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
2549
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
2550
+ if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
2551
+ return true;
2552
+ }
2553
+ return false;
2554
+ });
2555
+ }
2556
+ mergeField(target, source) {
2557
+ if (this.isWeakInferredFieldName(target.name) && !this.isWeakInferredFieldName(source.name)) {
2558
+ target.name = source.name;
2559
+ }
2560
+ target.label = target.label ?? source.label;
2561
+ target.placeholder = target.placeholder ?? source.placeholder;
2562
+ target.options = target.options ?? source.options;
2563
+ target.locator = target.locator ?? source.locator;
2564
+ target.sourceComponent = target.sourceComponent ?? source.sourceComponent;
2565
+ target.valueBinding = target.valueBinding ?? source.valueBinding;
2566
+ target.errorBinding = target.errorBinding ?? source.errorBinding;
2567
+ target.required = target.required || source.required;
2568
+ if (target.type === "text" && source.type !== "text") {
2569
+ target.type = source.type;
2570
+ }
2571
+ }
2572
+ namedSubmitAction(submitAction) {
2573
+ return submitAction && submitAction !== "anonymous" ? submitAction : void 0;
2574
+ }
2575
+ isWeakInferredFieldName(name) {
2576
+ return /^(text|value|input|query|search|selected|checked)$/i.test(name);
2577
+ }
2578
+ findFormWithSharedFields(forms, incoming) {
2579
+ if (incoming.fields.length === 0) return void 0;
2580
+ let best;
2581
+ for (const form of forms) {
2582
+ const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
2583
+ if (overlap > 0 && (!best || overlap > best.overlap)) {
2584
+ best = { form, overlap };
2585
+ }
2586
+ }
2587
+ return best?.form;
2588
+ }
2589
+ };
2590
+
2591
+ // src/analyzers/GenericPlatformAnalyzer.ts
2592
+ var GenericPlatformAnalyzer = class {
2593
+ constructor(platform) {
2594
+ this.platform = platform;
2595
+ }
2596
+ platform;
2597
+ async analyze(_config, _options) {
2598
+ return {
2599
+ screens: [],
2600
+ navigation: { screens: {}, initialScreen: "", navigators: [] },
2601
+ analyzedFiles: 0
2602
+ };
2603
+ }
2604
+ };
2605
+
2606
+ // src/ast/jsx/web/classify.ts
2607
+ var VIEW_TAGS = /* @__PURE__ */ new Set([
2608
+ "div",
2609
+ "span",
2610
+ "section",
2611
+ "main",
2612
+ "article",
2613
+ "aside",
2614
+ "header",
2615
+ "footer",
2616
+ "nav",
2617
+ "fieldset",
2618
+ "form",
2619
+ "p"
2620
+ ]);
2621
+ var LIST_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
2622
+ var BUTTON_TAGS = /* @__PURE__ */ new Set(["button"]);
2623
+ var INPUT_TAGS = /* @__PURE__ */ new Set(["input", "textarea"]);
2624
+ var MODAL_TAGS = /* @__PURE__ */ new Set(["dialog"]);
2625
+ var BUTTON_COMPONENTS2 = /* @__PURE__ */ new Set(["Button", "IconButton", "Link", "NavLink"]);
2626
+ var INPUT_COMPONENTS2 = /* @__PURE__ */ new Set(["Input", "TextField", "TextArea", "Textarea"]);
2627
+ var LIST_COMPONENTS2 = /* @__PURE__ */ new Set(["List", "Table", "DataGrid", "DataTable"]);
2628
+ var MODAL_COMPONENTS2 = /* @__PURE__ */ new Set(["Modal", "Dialog", "Drawer", "Popover", "BottomSheet"]);
2629
+ var ARIA_ROLE_TO_SEMANTIC = {
2630
+ button: "button",
2631
+ link: "button",
2632
+ tab: "button",
2633
+ menuitem: "button",
2634
+ textbox: "input",
2635
+ searchbox: "input",
2636
+ spinbutton: "input",
2637
+ listbox: "select",
2638
+ combobox: "select",
2639
+ radiogroup: "select",
2640
+ radio: "select",
2641
+ option: "select",
2642
+ checkbox: "toggle",
2643
+ switch: "toggle",
2644
+ dialog: "modal",
2645
+ alertdialog: "modal",
2646
+ list: "list",
2647
+ table: "list",
2648
+ grid: "list",
2649
+ form: "view"
2650
+ };
2651
+ var INPUT_TYPE_TO_SEMANTIC = {
2652
+ checkbox: "toggle",
2653
+ radio: "select",
2654
+ date: "date",
2655
+ "datetime-local": "date",
2656
+ month: "date",
2657
+ week: "date",
2658
+ time: "date",
2659
+ submit: "button",
2660
+ button: "button",
2661
+ reset: "button",
2662
+ image: "button",
2663
+ hidden: "custom",
2664
+ range: "input",
2665
+ file: "input",
2666
+ color: "input"
2667
+ };
2668
+ function classifyWebJsxComponent(name, element) {
2669
+ if (element) {
2670
+ const role = getStringAttr(element, "role");
2671
+ if (role && ARIA_ROLE_TO_SEMANTIC[role]) return ARIA_ROLE_TO_SEMANTIC[role];
2672
+ }
2673
+ if (/^[a-z]/.test(name)) {
2674
+ if (name === "input") {
2675
+ const type = element ? getStringAttr(element, "type") : void 0;
2676
+ if (type && INPUT_TYPE_TO_SEMANTIC[type]) return INPUT_TYPE_TO_SEMANTIC[type];
2677
+ return "input";
2678
+ }
2679
+ if (INPUT_TAGS.has(name)) return "input";
2680
+ if (name === "select") return "select";
2681
+ if (BUTTON_TAGS.has(name)) return "button";
2682
+ if (name === "a") {
2683
+ if (element && (hasJsxAttribute(element, "href") || hasJsxAttribute(element, "onClick"))) {
2684
+ return "button";
2685
+ }
2686
+ return "view";
2687
+ }
2688
+ if (LIST_TAGS.has(name)) return "list";
2689
+ if (MODAL_TAGS.has(name)) return "modal";
2690
+ if (VIEW_TAGS.has(name)) return "view";
2691
+ return "custom";
2692
+ }
2693
+ if (LIST_COMPONENTS2.has(name)) return "list";
2694
+ if (MODAL_COMPONENTS2.has(name)) return "modal";
2695
+ if (/date/i.test(name)) return "date";
2696
+ if (/(select|picker|dropdown|radio)/i.test(name)) return "select";
2697
+ if (/(checkbox|switch|toggle)/i.test(name)) return "toggle";
2698
+ if (INPUT_COMPONENTS2.has(name)) return "input";
2699
+ if (BUTTON_COMPONENTS2.has(name)) return "button";
2700
+ if (element) {
2701
+ const hasOptions = hasJsxAttribute(element, "options");
2702
+ const hasValue = hasJsxAttribute(element, "value");
2703
+ const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
2704
+ const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange");
2705
+ const hasOnClick = hasJsxAttribute(element, "onClick");
2706
+ if (hasOptions && (hasValue || hasOnChange)) return "select";
2707
+ if (hasChecked && hasOnChange) return "toggle";
2708
+ if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
2709
+ return "input";
2710
+ }
2711
+ if (hasOnClick) return "button";
2712
+ if (getStringAttr(element, "open") || getExpressionIdentifierAttr(element, "open") || getExpressionIdentifierAttr(element, "isOpen")) {
2713
+ return "modal";
2714
+ }
2715
+ }
2716
+ return "custom";
2717
+ }
2718
+ function getJsxElementName(openingElement) {
2719
+ return getJsxName(openingElement.name);
2720
+ }
2721
+ function getJsxName(name) {
2722
+ if (BabelTypes__namespace.isJSXIdentifier(name)) return name.name;
2723
+ if (BabelTypes__namespace.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
2724
+ if (BabelTypes__namespace.isJSXMemberExpression(name)) {
2725
+ const objectName = getJsxName(name.object);
2726
+ const propertyName = BabelTypes__namespace.isJSXIdentifier(name.property) ? name.property.name : null;
2727
+ return objectName && propertyName ? `${objectName}.${propertyName}` : null;
2728
+ }
2729
+ return null;
2730
+ }
2731
+ var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
2732
+ function staticRoutePath(node) {
2733
+ if (!node) return void 0;
2734
+ if (BabelTypes__namespace.isStringLiteral(node)) return node.value;
2735
+ if (BabelTypes__namespace.isTemplateLiteral(node)) {
2736
+ let path9 = "";
2737
+ node.quasis.forEach((quasi, index) => {
2738
+ path9 += quasi.value.cooked ?? quasi.value.raw;
2739
+ const expr = node.expressions[index];
2740
+ if (expr) path9 += `:${paramNameOf(expr)}`;
2741
+ });
2742
+ return path9;
2743
+ }
2744
+ return void 0;
2745
+ }
2746
+ function paramNameOf(expr) {
2747
+ if (BabelTypes__namespace.isIdentifier(expr)) return expr.name;
2748
+ if (BabelTypes__namespace.isMemberExpression(expr) && BabelTypes__namespace.isIdentifier(expr.property)) return expr.property.name;
2749
+ return "param";
2750
+ }
2751
+ function extractWebNavigationCalls(ast) {
2752
+ const calls = [];
2753
+ const inspect = (node) => {
2754
+ if (BabelTypes__namespace.isCallExpression(node)) {
2755
+ if (BabelTypes__namespace.isIdentifier(node.callee) && node.callee.name === "navigate") {
2756
+ const first = node.arguments[0];
2757
+ const targetPath = staticRoutePath(first);
2758
+ if (targetPath !== void 0) {
2759
+ calls.push({
2760
+ method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
2761
+ targetPath
2762
+ });
2763
+ } else if (BabelTypes__namespace.isNumericLiteral(first) || BabelTypes__namespace.isUnaryExpression(first) && first.operator === "-") {
2764
+ calls.push({ method: "goBack" });
2765
+ }
2766
+ return;
2767
+ }
2768
+ if (BabelTypes__namespace.isMemberExpression(node.callee) && BabelTypes__namespace.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && BabelTypes__namespace.isIdentifier(node.callee.property)) {
2769
+ const method = node.callee.property.name;
2770
+ const targetPath = staticRoutePath(node.arguments[0]);
2771
+ if ((method === "push" || method === "navigate") && targetPath !== void 0) {
2772
+ calls.push({ method: "navigate", targetPath });
2773
+ } else if (method === "replace" && targetPath !== void 0) {
2774
+ calls.push({ method: "replace", targetPath });
2775
+ } else if (method === "back" || method === "goBack") {
2776
+ calls.push({ method: "goBack" });
2777
+ }
2778
+ }
2779
+ return;
2780
+ }
2781
+ if (BabelTypes__namespace.isAssignmentExpression(node) && BabelTypes__namespace.isMemberExpression(node.left) && BabelTypes__namespace.isIdentifier(node.left.property) && node.left.property.name === "href" && isLocationExpression(node.left.object) && BabelTypes__namespace.isStringLiteral(node.right) && node.right.value.startsWith("/")) {
2782
+ calls.push({ method: "navigate", targetPath: node.right.value });
2783
+ }
2784
+ };
2785
+ traverse4__default.default(ast, {
2786
+ noScope: !BabelTypes__namespace.isFile(ast),
2787
+ enter: (nodePath) => inspect(nodePath.node)
2788
+ });
2789
+ return calls;
2790
+ }
2791
+ function hasReplaceOption(arg) {
2792
+ if (!arg || !BabelTypes__namespace.isObjectExpression(arg)) return false;
2793
+ return arg.properties.some(
2794
+ (prop) => BabelTypes__namespace.isObjectProperty(prop) && BabelTypes__namespace.isIdentifier(prop.key) && prop.key.name === "replace" && BabelTypes__namespace.isBooleanLiteral(prop.value) && prop.value.value === true
2795
+ );
2796
+ }
2797
+ function isLocationExpression(node) {
2798
+ if (BabelTypes__namespace.isIdentifier(node)) return node.name === "location";
2799
+ return BabelTypes__namespace.isMemberExpression(node) && BabelTypes__namespace.isIdentifier(node.object) && node.object.name === "window" && BabelTypes__namespace.isIdentifier(node.property) && node.property.name === "location";
2800
+ }
2801
+
2802
+ // src/analyzers/web/WebScreenAnalyzer.ts
2803
+ var DEFAULT_WEB_SCREEN_PATTERNS = [
2804
+ "**/pages/**/*.{ts,tsx,js,jsx}",
2805
+ "**/routes/**/*.{ts,tsx,js,jsx}",
2806
+ "**/views/**/*.{ts,tsx,js,jsx}",
2807
+ "**/app/**/*.{ts,tsx,js,jsx}",
2808
+ "**/*Page.{ts,tsx,js,jsx}",
2809
+ "**/*Screen.{ts,tsx,js,jsx}",
2810
+ "**/*View.{ts,tsx,js,jsx}"
2811
+ ];
2812
+ var DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
2813
+ var LISTISH_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "dl", "table", "tbody"]);
2814
+ var WebScreenAnalyzer = class {
2815
+ config;
2816
+ verbose = process.env.VERBOSE === "true";
2817
+ screenPatterns;
2818
+ constructor(config, options) {
2819
+ this.config = config;
2820
+ this.screenPatterns = options?.screenPatterns ?? DEFAULT_WEB_SCREEN_PATTERNS;
2821
+ }
2822
+ async analyze() {
2823
+ const {
2824
+ include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
2825
+ exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
2826
+ } = this.config;
2827
+ const files = (await glob__default.default(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
2828
+ (file) => !file.endsWith(".d.ts")
2829
+ );
2830
+ const patternMatches = await glob__default.default(this.screenPatterns, {
2831
+ cwd: this.config.rootDir,
2832
+ ignore: exclude
2833
+ });
2834
+ const patternSet = new Set(patternMatches.map((f) => path8__namespace.default.resolve(this.config.rootDir, f)));
2835
+ const candidates = [];
2836
+ for (const file of files) {
2837
+ const filePath = path8__namespace.default.resolve(this.config.rootDir, file);
2838
+ try {
2839
+ const candidate = await this.analyzeFile(filePath);
2840
+ if (!candidate) continue;
2841
+ candidate.matchesScreenPattern = patternSet.has(filePath);
2842
+ candidates.push(candidate);
2843
+ if (this.verbose) {
2844
+ console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file})`);
2845
+ }
2846
+ } catch (error) {
2847
+ if (this.verbose) {
2848
+ console.warn(
2849
+ `[WebScreenAnalyzer] Failed to parse ${file}:`,
2850
+ error instanceof Error ? error.message : error
2851
+ );
2852
+ }
2853
+ }
2854
+ }
2855
+ return { candidates, analyzedFiles: files.length };
2856
+ }
2857
+ async analyzeFile(filePath) {
2858
+ const source = await fs__default.default.readFile(filePath, "utf-8");
2859
+ const ast = parseSource(source, this.config.parserPlugins);
2860
+ const registerScreenMeta = this.extractRegisterScreenMetadata(ast);
2861
+ const componentName = this.extractComponentName(ast);
2862
+ const labelsByHtmlFor = this.collectHtmlForLabels(ast);
2863
+ const handlerBehaviors = this.collectWebHandlerBehaviors(ast);
2864
+ const forms = this.mergeForms(
2865
+ registerScreenMeta?.forms ?? [],
2866
+ this.extractForms(ast, labelsByHtmlFor)
2867
+ );
2868
+ const actions = this.extractActions(ast, registerScreenMeta, handlerBehaviors);
2869
+ const components = this.extractComponents(ast);
2870
+ const collections = this.extractCollections(ast);
2871
+ const navigationTargets = this.extractNavigationTargets(ast);
2872
+ const permissionsFromJsDoc = extractPermissionsFromJsDoc(source);
2873
+ const destructiveTags = extractDestructiveJsDocTargets(source);
2874
+ if (destructiveTags) {
2875
+ for (const action of actions) {
2876
+ if (destructiveTags === "*" || destructiveTags.has(action.id)) {
2877
+ action.destructive = true;
2878
+ }
2879
+ }
2880
+ }
2881
+ const name = registerScreenMeta?.name || componentName || path8__namespace.default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
2882
+ const descriptor = {
2883
+ name,
2884
+ filePath,
2885
+ title: registerScreenMeta?.title,
2886
+ description: registerScreenMeta?.description,
2887
+ components,
2888
+ forms,
2889
+ actions,
2890
+ navigationTargets,
2891
+ ...collections.length > 0 ? { collections } : {},
2892
+ ...registerScreenMeta?.suggestedPrompts && registerScreenMeta.suggestedPrompts.length > 0 ? { suggestedPrompts: registerScreenMeta.suggestedPrompts } : {},
2893
+ ...permissionsFromJsDoc ? {
2894
+ permissions: permissionsFromJsDoc,
2895
+ ...permissionsFromJsDoc.isPii ? { isPii: true } : {}
2896
+ } : {}
2897
+ };
2898
+ return {
2899
+ descriptor,
2900
+ hasRegisterScreen: registerScreenMeta !== null || this.detectRegisterScreenCall(ast),
2901
+ matchesScreenPattern: false
2902
+ };
2903
+ }
2904
+ // ── registerScreen ────────────────────────────────────────────────
2905
+ detectRegisterScreenCall(ast) {
2906
+ let found = false;
2907
+ traverse4__default.default(ast, {
2908
+ CallExpression: (nodePath) => {
2909
+ if (found) return;
2910
+ if (isRegisterScreenCallee(nodePath.node.callee)) {
2911
+ found = true;
2912
+ nodePath.stop();
2913
+ }
2914
+ }
2915
+ });
2916
+ return found;
2917
+ }
2918
+ extractRegisterScreenMetadata(ast) {
2919
+ let plain = null;
2920
+ traverse4__default.default(ast, {
2921
+ CallExpression: (nodePath) => {
2922
+ if (!isRegisterScreenCallee(nodePath.node.callee)) return;
2923
+ const arg = nodePath.node.arguments[0];
2924
+ if (BabelTypes__namespace.isObjectExpression(arg)) {
2925
+ plain = literalToPlain(arg);
2926
+ }
2927
+ }
2928
+ });
2929
+ if (!plain) return null;
2930
+ const meta = plain;
2931
+ const result = {
2932
+ name: typeof meta.name === "string" ? meta.name : "",
2933
+ actions: [],
2934
+ forms: [],
2935
+ navigationTargets: [],
2936
+ components: []
2937
+ };
2938
+ if (typeof meta.title === "string") result.title = meta.title;
2939
+ if (typeof meta.description === "string") result.description = meta.description;
2940
+ if (Array.isArray(meta.suggestedPrompts)) {
2941
+ const prompts = meta.suggestedPrompts.filter((p) => typeof p === "string").map((p) => p.trim()).filter((p) => p.length > 0);
2942
+ if (prompts.length > 0) result.suggestedPrompts = prompts;
2943
+ }
2944
+ if (Array.isArray(meta.actions)) {
2945
+ 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 }));
2946
+ }
2947
+ if (Array.isArray(meta.fields)) {
2948
+ const fields = meta.fields.filter((f) => typeof f === "object" && f !== null).filter((f) => typeof f.id === "string" && f.id.length > 0).map(
2949
+ (f) => ({
2950
+ name: f.id,
2951
+ type: typeof f.type === "string" ? f.type : "text",
2952
+ required: f.required === true,
2953
+ ...typeof f.label === "string" ? { label: f.label } : {},
2954
+ ...typeof f.placeholder === "string" ? { placeholder: f.placeholder } : {},
2955
+ ...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
2956
+ ...Array.isArray(f.options) ? { options: f.options } : {}
2957
+ })
2958
+ );
2959
+ if (fields.length > 0) result.forms = [{ id: "default", fields }];
2960
+ }
2961
+ return result;
2962
+ }
2963
+ // ── Component name / structure ────────────────────────────────────
2964
+ /** Default-exported component name, else the first exported capitalized function. */
2965
+ extractComponentName(ast) {
2966
+ let defaultName = "";
2967
+ let firstExported = "";
2968
+ traverse4__default.default(ast, {
2969
+ ExportDefaultDeclaration: (nodePath) => {
2970
+ const declaration = nodePath.node.declaration;
2971
+ if (BabelTypes__namespace.isFunctionDeclaration(declaration) && declaration.id?.name) {
2972
+ defaultName = declaration.id.name;
2973
+ } else if (BabelTypes__namespace.isIdentifier(declaration)) {
2974
+ defaultName = declaration.name;
2975
+ }
2976
+ },
2977
+ ExportNamedDeclaration: (nodePath) => {
2978
+ if (firstExported) return;
2979
+ const declaration = nodePath.node.declaration;
2980
+ if (BabelTypes__namespace.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
2981
+ firstExported = declaration.id.name;
2982
+ } else if (BabelTypes__namespace.isVariableDeclaration(declaration)) {
2983
+ for (const declarator of declaration.declarations) {
2984
+ if (BabelTypes__namespace.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (BabelTypes__namespace.isArrowFunctionExpression(declarator.init) || BabelTypes__namespace.isFunctionExpression(declarator.init))) {
2985
+ firstExported = declarator.id.name;
2986
+ break;
2987
+ }
2988
+ }
2989
+ }
2990
+ }
2991
+ });
2992
+ return defaultName || firstExported;
2993
+ }
2994
+ extractComponents(ast) {
2995
+ const components = [];
2996
+ const seen = /* @__PURE__ */ new Set();
2997
+ traverse4__default.default(ast, {
2998
+ JSXOpeningElement: (nodePath) => {
2999
+ const element = nodePath.node;
3000
+ const name = getJsxElementName(element);
3001
+ if (!name || seen.has(name)) return;
3002
+ seen.add(name);
3003
+ const role = classifyWebJsxComponent(name, element);
3004
+ const type = role === "select" || role === "toggle" || role === "date" ? "input" : role === "input" || role === "button" || role === "list" || role === "modal" || role === "view" ? role : "custom";
3005
+ const component = { name, type };
3006
+ const testId = getStringAttr(element, "data-testid") ?? getStringAttr(element, "testID");
3007
+ const ariaLabel = getStringAttr(element, "aria-label");
3008
+ if (testId) component.testID = testId;
3009
+ if (ariaLabel) component.accessibilityLabel = ariaLabel;
3010
+ components.push(component);
3011
+ }
3012
+ });
3013
+ return components;
3014
+ }
3015
+ // ── <label htmlFor> association ───────────────────────────────────
3016
+ collectHtmlForLabels(ast) {
3017
+ const labels = /* @__PURE__ */ new Map();
3018
+ traverse4__default.default(ast, {
3019
+ JSXElement: (nodePath) => {
3020
+ const element = nodePath.node;
3021
+ if (getJsxElementName(element.openingElement) !== "label") return;
3022
+ const htmlFor = getStringAttr(element.openingElement, "htmlFor");
3023
+ if (!htmlFor) return;
3024
+ const text = jsxTextContent(element);
3025
+ if (text) labels.set(htmlFor, text);
3026
+ }
3027
+ });
3028
+ return labels;
3029
+ }
3030
+ // ── Forms ─────────────────────────────────────────────────────────
3031
+ extractForms(ast, labelsByHtmlFor) {
3032
+ const formBuckets = /* @__PURE__ */ new Map();
3033
+ const usedIds = /* @__PURE__ */ new Set();
3034
+ let formCount = 0;
3035
+ const uniqueId = (preferred) => {
3036
+ if (!usedIds.has(preferred)) {
3037
+ usedIds.add(preferred);
3038
+ return preferred;
3039
+ }
3040
+ let suffix = 2;
3041
+ while (usedIds.has(`${preferred}-${suffix}`)) suffix += 1;
3042
+ const id = `${preferred}-${suffix}`;
3043
+ usedIds.add(id);
3044
+ return id;
3045
+ };
3046
+ const bucketFor = (formElement) => {
3047
+ let bucket = formBuckets.get(formElement);
3048
+ if (bucket) return bucket;
3049
+ formCount += 1;
3050
+ let preferred = "default";
3051
+ let submitAction;
3052
+ if (formElement) {
3053
+ const opening = formElement.openingElement;
3054
+ preferred = getStringAttr(opening, "id") ?? getStringAttr(opening, "name") ?? getStringAttr(opening, "data-testid") ?? (formCount === 1 ? "default" : `form-${formCount}`);
3055
+ submitAction = this.handlerNameFromAttr(opening, "onSubmit");
3056
+ }
3057
+ bucket = { id: uniqueId(preferred), fields: /* @__PURE__ */ new Map(), submitAction };
3058
+ formBuckets.set(formElement, bucket);
3059
+ return bucket;
3060
+ };
3061
+ traverse4__default.default(ast, {
3062
+ JSXElement: (nodePath) => {
3063
+ const element = nodePath.node;
3064
+ const name = getJsxElementName(element.openingElement);
3065
+ if (!name) return;
3066
+ const role = classifyWebJsxComponent(name, element.openingElement);
3067
+ if (!["input", "select", "toggle", "date"].includes(role)) return;
3068
+ if (name === "option") return;
3069
+ const field = this.extractField(element, role, labelsByHtmlFor);
3070
+ if (!field.name) return;
3071
+ const formParent = nodePath.findParent(
3072
+ (p) => p.isJSXElement() && (getJsxElementName(p.node.openingElement) === "form" || getStringAttr(p.node.openingElement, "role") === "form")
3073
+ );
3074
+ const bucket = bucketFor(formParent ? formParent.node : null);
3075
+ if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
3076
+ }
3077
+ });
3078
+ traverse4__default.default(ast, {
3079
+ JSXElement: (nodePath) => {
3080
+ const element = nodePath.node;
3081
+ const name = getJsxElementName(element.openingElement);
3082
+ if (!name) return;
3083
+ if (!this.isSubmitElement(name, element.openingElement)) return;
3084
+ const formParent = nodePath.findParent(
3085
+ (p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
3086
+ );
3087
+ if (!formParent) return;
3088
+ const bucket = formBuckets.get(formParent.node);
3089
+ if (!bucket) return;
3090
+ const handler = this.handlerNameFromAttr(element.openingElement, "onClick");
3091
+ if (!bucket.submitAction && handler) bucket.submitAction = handler;
3092
+ }
3093
+ });
3094
+ return Array.from(formBuckets.values()).filter((bucket) => bucket.fields.size > 0).map((bucket) => ({
3095
+ id: bucket.id,
3096
+ fields: Array.from(bucket.fields.values()),
3097
+ ...bucket.submitAction && bucket.submitAction !== "anonymous" ? { submitAction: bucket.submitAction } : {}
3098
+ }));
3099
+ }
3100
+ isSubmitElement(name, opening) {
3101
+ const type = getStringAttr(opening, "type");
3102
+ if (name === "button") return type === "submit" || type === void 0;
3103
+ if (name === "input") return type === "submit";
3104
+ return false;
3105
+ }
3106
+ extractField(element, role, labelsByHtmlFor) {
3107
+ const opening = element.openingElement;
3108
+ const componentName = getJsxElementName(opening) ?? void 0;
3109
+ const field = {
3110
+ name: "",
3111
+ type: "text",
3112
+ required: false,
3113
+ sourceComponent: componentName
3114
+ };
3115
+ const appilotsId = getStringAttr(opening, "appilotsId");
3116
+ const dataTestId = getStringAttr(opening, "data-testid");
3117
+ const domId = getStringAttr(opening, "id");
3118
+ const nameAttr = getStringAttr(opening, "name");
3119
+ const ariaLabel = getStringAttr(opening, "aria-label");
3120
+ const placeholder = getStringAttr(opening, "placeholder");
3121
+ if (placeholder) field.placeholder = placeholder;
3122
+ if (ariaLabel) field.label = ariaLabel;
3123
+ if (domId && labelsByHtmlFor.has(domId)) field.label = labelsByHtmlFor.get(domId);
3124
+ if (appilotsId) {
3125
+ field.name = appilotsId;
3126
+ field.locator = mergeLocator(field.locator, {
3127
+ id: appilotsId,
3128
+ appilotsId,
3129
+ source: "appilotsId"
3130
+ });
3131
+ }
3132
+ if (dataTestId) {
3133
+ if (!field.name) field.name = dataTestId.replace(/^(input-|field-|txt-)/, "");
3134
+ field.locator = mergeLocator(field.locator, {
3135
+ ...field.locator?.id ? {} : { id: dataTestId },
3136
+ testID: dataTestId,
3137
+ source: field.locator?.source ?? "data-testid"
3138
+ });
3139
+ }
3140
+ if (nameAttr && !field.name) field.name = nameAttr;
3141
+ if (domId) {
3142
+ if (!field.name) field.name = domId;
3143
+ field.locator = mergeLocator(field.locator, {
3144
+ ...field.locator?.id ? {} : { id: domId },
3145
+ source: field.locator?.source ?? "id"
3146
+ });
3147
+ }
3148
+ if (ariaLabel) {
3149
+ field.locator = mergeLocator(field.locator, {
3150
+ ...field.locator?.id ? {} : { id: slugify(ariaLabel) },
3151
+ accessibilityLabel: ariaLabel,
3152
+ source: field.locator?.source ?? "aria-label"
3153
+ });
3154
+ }
3155
+ for (const attr of opening.attributes) {
3156
+ if (!BabelTypes__namespace.isJSXAttribute(attr) || !BabelTypes__namespace.isJSXIdentifier(attr.name)) continue;
3157
+ const attrName = attr.name.name;
3158
+ if (attrName === "required") {
3159
+ if (attr.value === null) field.required = true;
3160
+ else if (BabelTypes__namespace.isJSXExpressionContainer(attr.value) && BabelTypes__namespace.isBooleanLiteral(attr.value.expression)) {
3161
+ field.required = attr.value.expression.value;
3162
+ }
3163
+ }
3164
+ if ((attrName === "value" || attrName === "checked") && attr.value && BabelTypes__namespace.isJSXExpressionContainer(attr.value) && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
3165
+ field.valueBinding = attr.value.expression.name;
3166
+ if (!field.name) field.name = attr.value.expression.name;
3167
+ }
3168
+ }
3169
+ if (getStringAttr(opening, "aria-required") === "true") field.required = true;
3170
+ if (role === "select") field.type = "select";
3171
+ else if (role === "toggle") field.type = "toggle";
3172
+ else if (role === "date") field.type = "date";
3173
+ else {
3174
+ field.type = this.inferInputType(opening, field);
3175
+ }
3176
+ if (componentName === "select") {
3177
+ const options = this.extractSelectOptions(element);
3178
+ if (options.length > 0) field.options = options;
3179
+ }
3180
+ if (!field.name || isWeakInferredFieldName(field.name)) {
3181
+ const labelish = field.label ?? field.placeholder;
3182
+ if (labelish) field.name = slugify(labelish);
3183
+ }
3184
+ if (!field.locator && field.name) {
3185
+ field.locator = { id: field.name, label: field.label, source: "inferred" };
3186
+ } else if (field.locator && !field.locator.id && field.name) {
3187
+ field.locator = mergeLocator(field.locator, {
3188
+ id: field.name,
3189
+ label: field.label,
3190
+ source: field.locator.source ?? "inferred"
3191
+ });
3192
+ }
3193
+ return field;
3194
+ }
3195
+ inferInputType(opening, field) {
3196
+ const type = getStringAttr(opening, "type");
3197
+ if (type === "email") return "email";
3198
+ if (type === "tel") return "phone";
3199
+ if (type === "number") return "number";
3200
+ if (type === "date" || type === "datetime-local" || type === "month" || type === "week")
3201
+ return "date";
3202
+ const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
3203
+ if (inputMode === "email") return "email";
3204
+ if (inputMode === "tel") return "phone";
3205
+ if (inputMode === "numeric" || inputMode === "decimal") return "number";
3206
+ const combined = `${field.name} ${field.label ?? ""} ${field.placeholder ?? ""}`.toLowerCase();
3207
+ if (combined.includes("email")) return "email";
3208
+ if (combined.includes("phone") || combined.includes("tel")) return "phone";
3209
+ return "text";
3210
+ }
3211
+ extractSelectOptions(selectElement) {
3212
+ const options = [];
3213
+ for (const child of selectElement.children) {
3214
+ if (!BabelTypes__namespace.isJSXElement(child)) continue;
3215
+ if (getJsxElementName(child.openingElement) !== "option") continue;
3216
+ const value = getStringAttr(child.openingElement, "value");
3217
+ const label = jsxTextContent(child) || value || "";
3218
+ if (label && value) options.push({ label, value });
3219
+ }
3220
+ return options;
3221
+ }
3222
+ mergeForms(primary, secondary) {
3223
+ const out = primary.map((form) => ({ ...form, fields: [...form.fields] }));
3224
+ for (const form of secondary) {
3225
+ const existing = out.find((candidate) => candidate.id === form.id);
3226
+ if (!existing) {
3227
+ out.push({ ...form, fields: [...form.fields] });
3228
+ continue;
3229
+ }
3230
+ for (const field of form.fields) {
3231
+ const existingField = existing.fields.find((candidate) => candidate.name === field.name);
3232
+ if (!existingField) {
3233
+ existing.fields.push(field);
3234
+ continue;
3235
+ }
3236
+ existingField.label = existingField.label ?? field.label;
3237
+ existingField.placeholder = existingField.placeholder ?? field.placeholder;
3238
+ existingField.options = existingField.options ?? field.options;
3239
+ existingField.locator = existingField.locator ?? field.locator;
3240
+ existingField.sourceComponent = existingField.sourceComponent ?? field.sourceComponent;
3241
+ existingField.valueBinding = existingField.valueBinding ?? field.valueBinding;
3242
+ existingField.required = existingField.required || field.required;
3243
+ if (existingField.type === "text" && field.type !== "text") existingField.type = field.type;
3244
+ }
3245
+ existing.submitAction = existing.submitAction ?? form.submitAction;
3246
+ }
3247
+ return out;
3248
+ }
3249
+ // ── Actions ───────────────────────────────────────────────────────
3250
+ extractActions(ast, registerScreenMeta, handlerBehaviors) {
3251
+ const actions = [...registerScreenMeta?.actions ?? []];
3252
+ const actionIds = new Set(actions.map((a) => a.id));
3253
+ const actionLabels = new Map(
3254
+ actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
3255
+ );
3256
+ traverse4__default.default(ast, {
3257
+ JSXElement: (nodePath) => {
3258
+ const element = nodePath.node;
3259
+ const name = getJsxElementName(element.openingElement);
3260
+ if (!name) return;
3261
+ if (classifyWebJsxComponent(name, element.openingElement) !== "button") return;
3262
+ const action = this.extractActionFromElement(element, nodePath);
3263
+ if (!action) return;
3264
+ const existingByLabel = action.label ? actionLabels.get(normalizeLabel(action.label)) : void 0;
3265
+ if (existingByLabel) {
3266
+ this.mergeActionMetadata(existingByLabel, action);
3267
+ return;
3268
+ }
3269
+ if (action.id && !actionIds.has(action.id)) {
3270
+ actions.push(action);
3271
+ actionIds.add(action.id);
3272
+ if (action.label) actionLabels.set(normalizeLabel(action.label), action);
3273
+ }
3274
+ }
3275
+ });
3276
+ this.enrichActionsFromHandlers(actions, handlerBehaviors);
3277
+ return actions;
3278
+ }
3279
+ extractActionFromElement(element, nodePath) {
3280
+ const opening = element.openingElement;
3281
+ const componentName = getJsxElementName(opening) ?? void 0;
3282
+ const action = { id: "", type: "custom", sourceComponent: componentName };
3283
+ const ariaLabel = getStringAttr(opening, "aria-label");
3284
+ const label = ariaLabel ?? jsxTextContent(element) ?? getStringAttr(opening, "value") ?? getStringAttr(opening, "title");
3285
+ if (label) action.label = label;
3286
+ if (ariaLabel) {
3287
+ action.locator = mergeLocator(action.locator, {
3288
+ accessibilityLabel: ariaLabel,
3289
+ source: "aria-label"
3290
+ });
3291
+ }
3292
+ const appilotsId = getStringAttr(opening, "appilotsId");
3293
+ const dataTestId = getStringAttr(opening, "data-testid");
3294
+ const domId = getStringAttr(opening, "id");
3295
+ if (appilotsId) {
3296
+ action.id = appilotsId;
3297
+ action.locator = mergeLocator(action.locator, {
3298
+ id: appilotsId,
3299
+ appilotsId,
3300
+ source: "appilotsId"
3301
+ });
3302
+ } else if (dataTestId) {
3303
+ action.id = dataTestId;
3304
+ action.locator = mergeLocator(action.locator, {
3305
+ id: dataTestId,
3306
+ testID: dataTestId,
3307
+ source: "data-testid"
3308
+ });
3309
+ } else if (domId) {
3310
+ action.id = domId;
3311
+ action.locator = mergeLocator(action.locator, { id: domId, source: "id" });
3312
+ } else if (action.label) {
3313
+ action.id = slugify(action.label);
3314
+ }
3315
+ if (!action.id) return null;
3316
+ const to = routePathAttr(opening, "to") ?? routePathAttr(opening, "href");
3317
+ if (to && to.startsWith("/")) {
3318
+ action.type = "navigation";
3319
+ action.targetScreen = to;
3320
+ } else if (to && !to.startsWith("/") && !hasJsxAttribute(opening, "onClick")) {
3321
+ return null;
3322
+ }
3323
+ let handlerName;
3324
+ const onClickAttr = opening.attributes.find(
3325
+ (attr) => BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
3326
+ );
3327
+ if (onClickAttr?.value && BabelTypes__namespace.isJSXExpressionContainer(onClickAttr.value)) {
3328
+ const expr = onClickAttr.value.expression;
3329
+ if (BabelTypes__namespace.isIdentifier(expr)) {
3330
+ handlerName = expr.name;
3331
+ action.handler = expr.name;
3332
+ } else if (!BabelTypes__namespace.isJSXEmptyExpression(expr)) {
3333
+ const inlineNavCalls = extractWebNavigationCalls(expr);
3334
+ const inlineNav = inlineNavCalls.find((call) => call.targetPath);
3335
+ if (inlineNav?.targetPath) {
3336
+ action.type = "navigation";
3337
+ action.targetScreen = inlineNav.targetPath;
3338
+ } else if (inlineNavCalls.some((call) => call.method === "goBack")) {
3339
+ action.type = "navigation";
3340
+ action.successSignal = {
3341
+ type: "goBack",
3342
+ description: "Action returns to the previous page"
3343
+ };
3344
+ action.appilotsInferred = {
3345
+ ...action.appilotsInferred ?? {},
3346
+ expectedOutcome: "navigation"
3347
+ };
3348
+ }
3349
+ const inlineHandler = firstCalledFunctionName(expr);
3350
+ if (inlineHandler) {
3351
+ handlerName = inlineHandler;
3352
+ action.handler = inlineHandler;
3353
+ }
3354
+ }
3355
+ }
3356
+ if (handlerName) {
3357
+ const lower = handlerName.toLowerCase();
3358
+ if (lower.includes("submit")) action.type = "submit";
3359
+ else if (lower.includes("navigate") && action.type === "custom") action.type = "navigation";
3360
+ }
3361
+ if (componentName && this.isSubmitElement(componentName, opening)) {
3362
+ const formParent = nodePath.findParent(
3363
+ (p) => p.isJSXElement() && getJsxElementName(p.node.openingElement) === "form"
3364
+ );
3365
+ if (formParent) {
3366
+ action.type = "submit";
3367
+ if (!action.handler) {
3368
+ const formHandler = this.handlerNameFromAttr(formParent.node.openingElement, "onSubmit");
3369
+ if (formHandler && formHandler !== "anonymous") {
3370
+ action.handler = formHandler;
3371
+ handlerName = formHandler;
3372
+ }
3373
+ }
3374
+ }
3375
+ }
3376
+ if (hasJsxAttribute(opening, "destructive") || hasJsxAttribute(opening, "aria-destructive")) {
3377
+ const value = getStringAttr(opening, "destructive") ?? getStringAttr(opening, "aria-destructive");
3378
+ action.destructive = value !== "false";
3379
+ } else if (handlerName && DESTRUCTIVE_VERB2.test(handlerName) || DESTRUCTIVE_VERB2.test(action.id)) {
3380
+ action.destructive = true;
3381
+ }
3382
+ if (!action.locator && action.id) {
3383
+ action.locator = {
3384
+ id: action.id,
3385
+ label: action.label,
3386
+ source: action.label ? "label" : "inferred"
3387
+ };
3388
+ }
3389
+ return action;
3390
+ }
3391
+ mergeActionMetadata(target, source) {
3392
+ target.handler = target.handler ?? source.handler;
3393
+ target.targetScreen = target.targetScreen ?? source.targetScreen;
3394
+ target.description = target.description ?? source.description;
3395
+ target.locator = target.locator ?? source.locator;
3396
+ target.nativeConfirmationExpected = target.nativeConfirmationExpected || source.nativeConfirmationExpected || void 0;
3397
+ target.requiresConfirmation = target.requiresConfirmation || source.requiresConfirmation || void 0;
3398
+ target.destructive = target.destructive || source.destructive || void 0;
3399
+ target.effect = target.effect ?? source.effect;
3400
+ target.riskLevel = target.riskLevel ?? source.riskLevel;
3401
+ target.appilotsInferred = target.appilotsInferred ?? source.appilotsInferred;
3402
+ }
3403
+ enrichActionsFromHandlers(actions, behaviors) {
3404
+ for (const action of actions) {
3405
+ const behavior = action.handler ? behaviors.get(action.handler) : void 0;
3406
+ if (!behavior) continue;
3407
+ action.appilotsInferred = {
3408
+ ...action.appilotsInferred ?? {},
3409
+ ...behavior.base.appilotsInferred
3410
+ };
3411
+ if (behavior.nativeConfirmationExpected || behavior.base.nativeConfirmationExpected) {
3412
+ action.nativeConfirmationExpected = true;
3413
+ }
3414
+ if (behavior.targetPath && !action.targetScreen) {
3415
+ action.targetScreen = behavior.targetPath;
3416
+ if (action.type === "custom") action.type = "navigation";
3417
+ action.appilotsInferred = {
3418
+ ...action.appilotsInferred ?? {},
3419
+ expectedOutcome: "navigation"
3420
+ };
3421
+ }
3422
+ if (behavior.base.successSignal && !action.successSignal) {
3423
+ action.successSignal = behavior.base.successSignal;
3424
+ }
3425
+ if (behavior.base.failureSignal && !action.failureSignal) {
3426
+ action.failureSignal = behavior.base.failureSignal;
3427
+ }
3428
+ if (behavior.base.opensModal && !action.opensModal) {
3429
+ action.opensModal = behavior.base.opensModal;
3430
+ }
3431
+ if (behavior.base.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
3432
+ action.destructive = true;
3433
+ action.effect = action.effect ?? "destructive";
3434
+ action.riskLevel = action.riskLevel ?? "high";
3435
+ action.requiresConfirmation = action.requiresConfirmation ?? true;
3436
+ }
3437
+ }
3438
+ }
3439
+ /**
3440
+ * Handler behavior via the shared, platform-neutral analyzer
3441
+ * (async/await, `.then`, state setters, toasts, destructive verbs)
3442
+ * plus the web-only signals: React Router navigation targets and
3443
+ * `window.confirm(...)` as the native confirmation dialog.
3444
+ */
3445
+ collectWebHandlerBehaviors(ast) {
3446
+ const handlers = collectFunctions(ast);
3447
+ const out = /* @__PURE__ */ new Map();
3448
+ for (const [name, fn] of handlers) {
3449
+ const base = analyzeFunctionBehavior(name, fn, handlers);
3450
+ const navCalls = fn.body ? extractWebNavigationCalls(fn.body) : [];
3451
+ const firstNav = navCalls.find((call) => call.targetPath);
3452
+ const goesBack = navCalls.some((call) => call.method === "goBack");
3453
+ out.set(name, {
3454
+ base: {
3455
+ ...base,
3456
+ ...goesBack && !base.successSignal ? {
3457
+ successSignal: {
3458
+ type: "goBack",
3459
+ description: "Action returns to the previous page"
3460
+ }
3461
+ } : {}
3462
+ },
3463
+ targetPath: firstNav?.targetPath,
3464
+ nativeConfirmationExpected: fn.body ? containsWindowConfirm(fn.body) : false
3465
+ });
3466
+ }
3467
+ return out;
3468
+ }
3469
+ handlerNameFromAttr(opening, attrName) {
3470
+ const attr = opening.attributes.find(
3471
+ (candidate) => BabelTypes__namespace.isJSXAttribute(candidate) && BabelTypes__namespace.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
3472
+ );
3473
+ if (!attr?.value || !BabelTypes__namespace.isJSXExpressionContainer(attr.value)) return void 0;
3474
+ const expr = attr.value.expression;
3475
+ if (BabelTypes__namespace.isIdentifier(expr)) return expr.name;
3476
+ if (BabelTypes__namespace.isArrowFunctionExpression(expr) || BabelTypes__namespace.isFunctionExpression(expr)) {
3477
+ return firstCalledFunctionName(expr) ?? "anonymous";
3478
+ }
3479
+ return void 0;
3480
+ }
3481
+ // ── Navigation targets (route paths — resolved by the orchestrator) ─
3482
+ extractNavigationTargets(ast) {
3483
+ const targets = /* @__PURE__ */ new Set();
3484
+ for (const call of extractWebNavigationCalls(ast)) {
3485
+ if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
3486
+ }
3487
+ traverse4__default.default(ast, {
3488
+ JSXOpeningElement: (nodePath) => {
3489
+ const element = nodePath.node;
3490
+ const name = getJsxElementName(element);
3491
+ if (name === "Link" || name === "NavLink" || name === "Navigate") {
3492
+ const to = routePathAttr(element, "to");
3493
+ if (to && to.startsWith("/")) targets.add(to);
3494
+ }
3495
+ if (name === "a") {
3496
+ const href = routePathAttr(element, "href");
3497
+ if (href && href.startsWith("/")) targets.add(href);
3498
+ }
3499
+ }
3500
+ });
3501
+ return Array.from(targets).sort();
3502
+ }
3503
+ // ── Collections ───────────────────────────────────────────────────
3504
+ extractCollections(ast) {
3505
+ const collections = [];
3506
+ const seen = /* @__PURE__ */ new Set();
3507
+ traverse4__default.default(ast, {
3508
+ JSXExpressionContainer: (nodePath) => {
3509
+ const expr = nodePath.node.expression;
3510
+ if (!BabelTypes__namespace.isCallExpression(expr) || !BabelTypes__namespace.isMemberExpression(expr.callee) || !BabelTypes__namespace.isIdentifier(expr.callee.object) || !BabelTypes__namespace.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
3511
+ return;
3512
+ }
3513
+ const callback = expr.arguments[0];
3514
+ if (!BabelTypes__namespace.isArrowFunctionExpression(callback) && !BabelTypes__namespace.isFunctionExpression(callback)) return;
3515
+ const enclosing = nodePath.findParent(
3516
+ (p) => p.isJSXElement()
3517
+ );
3518
+ const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
3519
+ const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
3520
+ const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
3521
+ if (enclosingName === "select") return;
3522
+ const isListContext = enclosingName !== null && LISTISH_TAGS.has(enclosingName) || enclosingRole === "list" || returnsListItem;
3523
+ if (!isListContext) return;
3524
+ const dataSource = expr.callee.object.name;
3525
+ if (seen.has(dataSource)) return;
3526
+ seen.add(dataSource);
3527
+ const itemNames = collectionItemNames(callback);
3528
+ const displayFields = collectionDisplayFields(callback, itemNames);
3529
+ const keyField = collectionKeyField(callback, itemNames);
3530
+ const rowAction = collectionRowAction(callback);
3531
+ const itemType = inferItemType(dataSource, displayFields);
3532
+ const identityFields = inferIdentityFields(keyField, displayFields);
3533
+ collections.push({
3534
+ id: dataSource,
3535
+ component: enclosingName ?? "list",
3536
+ ...itemType ? { itemType } : {},
3537
+ dataSource,
3538
+ ...keyField ? { keyField } : {},
3539
+ ...displayFields.length > 0 ? { displayFields } : {},
3540
+ ...rowAction ? { rowAction, rowActions: [rowAction] } : {},
3541
+ ...identityFields.length > 0 ? { identityFields } : {}
3542
+ });
3543
+ }
3544
+ });
3545
+ return collections;
3546
+ }
3547
+ };
3548
+ function isRegisterScreenCallee(callee) {
3549
+ return BabelTypes__namespace.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes__namespace.isMemberExpression(callee) && BabelTypes__namespace.isIdentifier(callee.property) && callee.property.name === "registerScreen";
3550
+ }
3551
+ function literalToPlain(node) {
3552
+ if (BabelTypes__namespace.isStringLiteral(node) || BabelTypes__namespace.isNumericLiteral(node) || BabelTypes__namespace.isBooleanLiteral(node)) {
3553
+ return node.value;
3554
+ }
3555
+ if (BabelTypes__namespace.isNullLiteral(node)) return null;
3556
+ if (BabelTypes__namespace.isArrayExpression(node)) {
3557
+ return node.elements.filter((el) => el !== null && BabelTypes__namespace.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
3558
+ }
3559
+ if (BabelTypes__namespace.isObjectExpression(node)) {
3560
+ const out = {};
3561
+ for (const prop of node.properties) {
3562
+ if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
3563
+ const key = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : BabelTypes__namespace.isStringLiteral(prop.key) ? prop.key.value : void 0;
3564
+ if (!key || !BabelTypes__namespace.isExpression(prop.value)) continue;
3565
+ const value = literalToPlain(prop.value);
3566
+ if (value !== void 0) out[key] = value;
3567
+ }
3568
+ return out;
3569
+ }
3570
+ return void 0;
3571
+ }
3572
+ function jsxTextContent(element) {
3573
+ const parts = [];
3574
+ const walk = (children) => {
3575
+ for (const child of children) {
3576
+ if (BabelTypes__namespace.isJSXText(child)) {
3577
+ const trimmed = child.value.replace(/\s+/g, " ").trim();
3578
+ if (trimmed) parts.push(trimmed);
3579
+ } else if (BabelTypes__namespace.isJSXExpressionContainer(child) && BabelTypes__namespace.isStringLiteral(child.expression)) {
3580
+ parts.push(child.expression.value);
3581
+ } else if (BabelTypes__namespace.isJSXElement(child)) {
3582
+ walk(child.children);
3583
+ }
3584
+ }
3585
+ };
3586
+ walk(element.children);
3587
+ const text = parts.join(" ").trim();
3588
+ return text.length > 0 ? text : void 0;
3589
+ }
3590
+ function firstCalledFunctionName(node) {
3591
+ if (BabelTypes__namespace.isIdentifier(node)) return node.name;
3592
+ if (BabelTypes__namespace.isArrowFunctionExpression(node) || BabelTypes__namespace.isFunctionExpression(node)) {
3593
+ return firstCalledFunctionName(node.body);
3594
+ }
3595
+ if (BabelTypes__namespace.isBlockStatement(node)) {
3596
+ for (const statement of node.body) {
3597
+ const handler = firstCalledFunctionName(statement);
3598
+ if (handler) return handler;
3599
+ }
3600
+ return void 0;
3601
+ }
3602
+ if (BabelTypes__namespace.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
3603
+ if (BabelTypes__namespace.isReturnStatement(node)) {
3604
+ return node.argument ? firstCalledFunctionName(node.argument) : void 0;
3605
+ }
3606
+ if (BabelTypes__namespace.isAwaitExpression(node) || BabelTypes__namespace.isUnaryExpression(node)) {
3607
+ return firstCalledFunctionName(node.argument);
3608
+ }
3609
+ if (BabelTypes__namespace.isCallExpression(node)) {
3610
+ if (BabelTypes__namespace.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
3611
+ return node.callee.name;
3612
+ }
3613
+ return void 0;
3614
+ }
3615
+ return void 0;
3616
+ }
3617
+ function containsWindowConfirm(body) {
3618
+ let found = false;
3619
+ traverse4__default.default(
3620
+ body,
3621
+ {
3622
+ noScope: true,
3623
+ CallExpression: (nodePath) => {
3624
+ const callee = nodePath.node.callee;
3625
+ if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "confirm") found = true;
3626
+ if (BabelTypes__namespace.isMemberExpression(callee) && BabelTypes__namespace.isIdentifier(callee.object) && callee.object.name === "window" && BabelTypes__namespace.isIdentifier(callee.property) && callee.property.name === "confirm") {
3627
+ found = true;
3628
+ }
3629
+ }
3630
+ }
3631
+ );
3632
+ return found;
3633
+ }
3634
+ function mergeLocator(current, next) {
3635
+ return { ...current ?? {}, ...next };
3636
+ }
3637
+ function routePathAttr(opening, attrName) {
3638
+ const literal = getStringAttr(opening, attrName);
3639
+ if (literal) return literal;
3640
+ const attr = opening.attributes.find(
3641
+ (candidate) => BabelTypes__namespace.isJSXAttribute(candidate) && BabelTypes__namespace.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
3642
+ );
3643
+ if (!attr?.value || !BabelTypes__namespace.isJSXExpressionContainer(attr.value)) return void 0;
3644
+ return staticRoutePath(attr.value.expression);
3645
+ }
3646
+ function slugify(label) {
3647
+ return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3648
+ }
3649
+ function normalizeLabel(label) {
3650
+ return label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
3651
+ }
3652
+ function isWeakInferredFieldName(name) {
3653
+ return /^(text|value|input|query|search|selected|checked)$/i.test(name);
3654
+ }
3655
+ function collectionItemNames(callback) {
3656
+ const names = /* @__PURE__ */ new Set(["item"]);
3657
+ const firstParam = callback.params[0];
3658
+ if (BabelTypes__namespace.isIdentifier(firstParam)) names.add(firstParam.name);
3659
+ if (BabelTypes__namespace.isObjectPattern(firstParam)) {
3660
+ for (const prop of firstParam.properties) {
3661
+ if (BabelTypes__namespace.isObjectProperty(prop) && BabelTypes__namespace.isIdentifier(prop.key) && BabelTypes__namespace.isIdentifier(prop.value)) {
3662
+ names.add(prop.value.name);
3663
+ }
3664
+ }
3665
+ }
3666
+ return names;
3667
+ }
3668
+ function collectionDisplayFields(callback, itemNames) {
3669
+ const fields = /* @__PURE__ */ new Set();
3670
+ if (!callback.body) return [];
3671
+ traverse4__default.default(
3672
+ callback.body,
3673
+ {
3674
+ noScope: true,
3675
+ MemberExpression: (nodePath) => {
3676
+ const node = nodePath.node;
3677
+ if (BabelTypes__namespace.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes__namespace.isIdentifier(node.property)) {
3678
+ fields.add(node.property.name);
3679
+ }
3680
+ }
3681
+ }
3682
+ );
3683
+ return Array.from(fields).sort();
3684
+ }
3685
+ function collectionKeyField(callback, itemNames) {
3686
+ let keyField;
3687
+ if (!callback.body) return void 0;
3688
+ traverse4__default.default(
3689
+ callback.body,
3690
+ {
3691
+ noScope: true,
3692
+ JSXAttribute: (nodePath) => {
3693
+ const attr = nodePath.node;
3694
+ if (!BabelTypes__namespace.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
3695
+ if (!attr.value || !BabelTypes__namespace.isJSXExpressionContainer(attr.value)) return;
3696
+ const expr = attr.value.expression;
3697
+ if (BabelTypes__namespace.isMemberExpression(expr) && BabelTypes__namespace.isIdentifier(expr.object) && itemNames.has(expr.object.name) && BabelTypes__namespace.isIdentifier(expr.property)) {
3698
+ keyField = keyField ?? expr.property.name;
3699
+ }
3700
+ }
3701
+ }
3702
+ );
3703
+ return keyField;
3704
+ }
3705
+ function collectionRowAction(callback) {
3706
+ if (!callback.body) return void 0;
3707
+ const navCall = extractWebNavigationCalls(callback.body).find((call) => call.targetPath);
3708
+ if (!navCall?.targetPath) return void 0;
3709
+ return {
3710
+ type: "navigation",
3711
+ // Route path — the orchestrator resolves it to a screen name.
3712
+ targetScreen: navCall.targetPath,
3713
+ description: `Clicking a row opens ${navCall.targetPath}`
3714
+ };
3715
+ }
3716
+ function callbackReturnsTag(callback, tags) {
3717
+ let found = false;
3718
+ const inspect = (node) => {
3719
+ if (!node || found) return;
3720
+ if (BabelTypes__namespace.isJSXElement(node)) {
3721
+ const name = getJsxElementName(node.openingElement);
3722
+ if (name && tags.has(name)) found = true;
3723
+ return;
3724
+ }
3725
+ if (BabelTypes__namespace.isBlockStatement(node)) {
3726
+ for (const statement of node.body) {
3727
+ if (BabelTypes__namespace.isReturnStatement(statement)) inspect(statement.argument);
3728
+ }
3729
+ }
3730
+ if (BabelTypes__namespace.isParenthesizedExpression(node)) inspect(node.expression);
3731
+ if (BabelTypes__namespace.isConditionalExpression(node)) {
3732
+ inspect(node.consequent);
3733
+ inspect(node.alternate);
3734
+ }
3735
+ };
3736
+ inspect(callback.body);
3737
+ return found;
3738
+ }
3739
+ function inferItemType(dataSource, displayFields) {
3740
+ const singular = dataSource.replace(/^render/i, "").replace(/(List|Items|Data|Rows|Sections)$/i, "").replace(/s$/i, "");
3741
+ const candidate = singular.charAt(0).toUpperCase() + singular.slice(1);
3742
+ if (candidate.length > 1) return candidate;
3743
+ if (displayFields.length > 0) return "Item";
3744
+ return void 0;
3745
+ }
3746
+ function inferIdentityFields(keyField, displayFields) {
3747
+ const out = /* @__PURE__ */ new Set();
3748
+ if (keyField) out.add(keyField);
3749
+ for (const field of displayFields) {
3750
+ if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
3751
+ }
3752
+ return Array.from(out);
3753
+ }
3754
+ var WEB_NAVIGATOR_TYPE = "route";
3755
+ var WebNavigationAnalyzer = class {
3756
+ config;
3757
+ navigationInclude;
3758
+ navigationExclude;
3759
+ constructor(config, options) {
3760
+ this.config = config;
3761
+ this.navigationInclude = options?.navigationInclude ?? [];
3762
+ this.navigationExclude = options?.navigationExclude ?? [];
3763
+ }
3764
+ async analyze() {
3765
+ const files = await this.findRouteFiles();
3766
+ const routes = [];
3767
+ for (const filePath of files) {
3768
+ try {
3769
+ const content = await fs$1.promises.readFile(filePath, "utf-8");
3770
+ if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
3771
+ continue;
3772
+ }
3773
+ const ast = parseSource(content, this.config.parserPlugins);
3774
+ routes.push(...this.extractJsxRoutes(ast));
3775
+ routes.push(...this.extractObjectRoutes(ast));
3776
+ } catch (error) {
3777
+ console.warn(`[WebNavigationAnalyzer] Failed to parse ${filePath}:`, error);
3778
+ }
3779
+ }
3780
+ const deduped = this.dedupeRoutes(routes);
3781
+ return { graph: this.buildGraph(deduped), routes: deduped };
3782
+ }
3783
+ /** Files likely to contain route configuration. */
3784
+ async findRouteFiles() {
3785
+ const patterns = [
3786
+ "**/*{router,routes,Router,Routes}*.{ts,tsx,js,jsx}",
3787
+ "**/App.{ts,tsx,js,jsx}",
3788
+ "**/app.{ts,tsx,js,jsx}",
3789
+ "**/main.{ts,tsx,js,jsx}",
3790
+ "**/index.{ts,tsx,js,jsx}",
3791
+ ...this.navigationInclude
3792
+ ];
3793
+ const ignore = [
3794
+ "**/node_modules/**",
3795
+ "**/dist/**",
3796
+ "**/build/**",
3797
+ ...this.config.exclude || [],
3798
+ ...this.navigationExclude
3799
+ ];
3800
+ const files = await glob__default.default(patterns, { cwd: this.config.rootDir, ignore });
3801
+ return files.map((file) => path8__namespace.default.join(this.config.rootDir, file));
3802
+ }
3803
+ // ── JSX <Route> style ────────────────────────────────────────────
3804
+ extractJsxRoutes(ast) {
3805
+ const routes = [];
3806
+ const visitRoute = (element, parentPath) => {
3807
+ const opening = element.openingElement;
3808
+ const name = getJsxElementName(opening);
3809
+ if (name !== "Route") {
3810
+ for (const child of element.children) {
3811
+ if (BabelTypes__namespace.isJSXElement(child)) visitRoute(child, parentPath);
3812
+ }
3813
+ return;
3814
+ }
3815
+ const segment = getStringAttr(opening, "path");
3816
+ const isIndex = hasJsxAttribute(opening, "index") && !segment;
3817
+ const fullPath = this.joinPaths(parentPath, segment, isIndex);
3818
+ const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
3819
+ const isLeaf = !element.children.some(
3820
+ (child) => BabelTypes__namespace.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
3821
+ );
3822
+ if ((segment || isIndex) && (componentName || isLeaf)) {
3823
+ routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
3824
+ }
3825
+ for (const child of element.children) {
3826
+ if (BabelTypes__namespace.isJSXElement(child)) visitRoute(child, fullPath);
3827
+ }
3828
+ };
3829
+ traverse4__default.default(ast, {
3830
+ JSXElement: (nodePath) => {
3831
+ const name = getJsxElementName(nodePath.node.openingElement);
3832
+ if (name !== "Routes" && name !== "Route") return;
3833
+ if (nodePath.findParent((p) => {
3834
+ if (!p.isJSXElement()) return false;
3835
+ const parentName = getJsxElementName(p.node.openingElement);
3836
+ return parentName === "Routes" || parentName === "Route";
3837
+ })) {
3838
+ return;
3839
+ }
3840
+ visitRoute(nodePath.node, "");
2355
3841
  }
2356
- return screen;
2357
3842
  });
2358
- return {
2359
- screens: enrichedScreens,
2360
- navigation,
2361
- analyzedFiles: screenFiles.length,
2362
- ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
2363
- };
3843
+ return routes;
2364
3844
  }
2365
- mergeForm(target, source) {
2366
- for (const field of source.fields) {
2367
- const existingField = this.findEquivalentField(target.fields, field);
2368
- if (existingField) {
2369
- this.mergeField(existingField, field);
2370
- } else {
2371
- target.fields.push(field);
3845
+ /** `element={<VehicleList/>}` or `Component={VehicleList}`. */
3846
+ componentNameFromElementAttr(opening) {
3847
+ for (const attr of opening.attributes) {
3848
+ if (!BabelTypes__namespace.isJSXAttribute(attr) || !BabelTypes__namespace.isJSXIdentifier(attr.name)) continue;
3849
+ if (attr.name.name === "element" && BabelTypes__namespace.isJSXExpressionContainer(attr.value)) {
3850
+ const expr = attr.value.expression;
3851
+ if (BabelTypes__namespace.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
3852
+ }
3853
+ if (attr.name.name === "Component" && BabelTypes__namespace.isJSXExpressionContainer(attr.value)) {
3854
+ if (BabelTypes__namespace.isIdentifier(attr.value.expression)) return attr.value.expression.name;
2372
3855
  }
2373
3856
  }
2374
- target.submitAction = target.submitAction ?? this.namedSubmitAction(source.submitAction);
2375
- target.validationRules = source.validationRules ? { ...source.validationRules, ...target.validationRules ?? {} } : target.validationRules;
3857
+ return null;
2376
3858
  }
2377
- findEquivalentField(fields, incoming) {
2378
- return fields.find((field) => {
2379
- if (field.name && incoming.name && field.name === incoming.name) return true;
2380
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
2381
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
2382
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
2383
- if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
2384
- return true;
3859
+ // ── createBrowserRouter([...]) / useRoutes([...]) style ──────────
3860
+ extractObjectRoutes(ast) {
3861
+ const routes = [];
3862
+ const ROUTER_FACTORIES = /* @__PURE__ */ new Set([
3863
+ "createBrowserRouter",
3864
+ "createHashRouter",
3865
+ "createMemoryRouter",
3866
+ "useRoutes"
3867
+ ]);
3868
+ traverse4__default.default(ast, {
3869
+ CallExpression: (nodePath) => {
3870
+ const callee = nodePath.node.callee;
3871
+ if (!BabelTypes__namespace.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
3872
+ const first = nodePath.node.arguments[0];
3873
+ if (!BabelTypes__namespace.isArrayExpression(first)) return;
3874
+ this.visitRouteObjects(first, "", routes);
2385
3875
  }
2386
- return false;
2387
3876
  });
3877
+ return routes;
2388
3878
  }
2389
- mergeField(target, source) {
2390
- if (this.isWeakInferredFieldName(target.name) && !this.isWeakInferredFieldName(source.name)) {
2391
- target.name = source.name;
2392
- }
2393
- target.label = target.label ?? source.label;
2394
- target.placeholder = target.placeholder ?? source.placeholder;
2395
- target.options = target.options ?? source.options;
2396
- target.locator = target.locator ?? source.locator;
2397
- target.sourceComponent = target.sourceComponent ?? source.sourceComponent;
2398
- target.valueBinding = target.valueBinding ?? source.valueBinding;
2399
- target.errorBinding = target.errorBinding ?? source.errorBinding;
2400
- target.required = target.required || source.required;
2401
- if (target.type === "text" && source.type !== "text") {
2402
- target.type = source.type;
3879
+ visitRouteObjects(arr, parentPath, out) {
3880
+ for (const element of arr.elements) {
3881
+ if (!BabelTypes__namespace.isObjectExpression(element)) continue;
3882
+ let segment;
3883
+ let isIndex = false;
3884
+ let componentName;
3885
+ let children;
3886
+ for (const prop of element.properties) {
3887
+ if (!BabelTypes__namespace.isObjectProperty(prop) || !BabelTypes__namespace.isIdentifier(prop.key)) continue;
3888
+ const key = prop.key.name;
3889
+ if (key === "path" && BabelTypes__namespace.isStringLiteral(prop.value)) segment = prop.value.value;
3890
+ if (key === "index" && BabelTypes__namespace.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
3891
+ if (key === "element" && BabelTypes__namespace.isJSXElement(prop.value)) {
3892
+ componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
3893
+ }
3894
+ if (key === "Component" && BabelTypes__namespace.isIdentifier(prop.value)) componentName = prop.value.name;
3895
+ if (key === "children" && BabelTypes__namespace.isArrayExpression(prop.value)) children = prop.value;
3896
+ }
3897
+ const fullPath = this.joinPaths(parentPath, segment, isIndex);
3898
+ if ((segment !== void 0 || isIndex) && (componentName || !children)) {
3899
+ out.push(this.buildRoute(fullPath, componentName, isIndex, Boolean(children)));
3900
+ }
3901
+ if (children) this.visitRouteObjects(children, fullPath, out);
2403
3902
  }
2404
3903
  }
2405
- namedSubmitAction(submitAction) {
2406
- return submitAction && submitAction !== "anonymous" ? submitAction : void 0;
3904
+ // ── Shared route building ────────────────────────────────────────
3905
+ joinPaths(parent, segment, isIndex) {
3906
+ if (isIndex || segment === void 0) return parent || "/";
3907
+ if (segment.startsWith("/")) return this.normalizePath(segment);
3908
+ return this.normalizePath(`${parent === "/" ? "" : parent}/${segment}`);
2407
3909
  }
2408
- isWeakInferredFieldName(name) {
2409
- return /^(text|value|input|query|search|selected|checked)$/i.test(name);
3910
+ normalizePath(p) {
3911
+ const cleaned = `/${p}`.replace(/\/+/g, "/");
3912
+ return cleaned.length > 1 ? cleaned.replace(/\/$/, "") : cleaned;
2410
3913
  }
2411
- findFormWithSharedFields(forms, incoming) {
2412
- if (incoming.fields.length === 0) return void 0;
2413
- let best;
2414
- for (const form of forms) {
2415
- const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
2416
- if (overlap > 0 && (!best || overlap > best.overlap)) {
2417
- best = { form, overlap };
3914
+ buildRoute(fullPath, componentName, isIndex, isLayout) {
3915
+ const params = this.paramsFromPath(fullPath);
3916
+ return {
3917
+ path: fullPath,
3918
+ screenName: componentName ?? screenNameFromPath(fullPath),
3919
+ ...params.length > 0 ? { params } : {},
3920
+ ...isIndex ? { index: true } : {},
3921
+ ...isLayout ? { layout: true } : {}
3922
+ };
3923
+ }
3924
+ paramsFromPath(routePath) {
3925
+ const params = [];
3926
+ for (const segment of routePath.split("/")) {
3927
+ if (!segment.startsWith(":")) continue;
3928
+ const optional = segment.endsWith("?");
3929
+ const name = segment.slice(1, optional ? -1 : void 0);
3930
+ if (name) params.push({ name, type: "string", required: !optional });
3931
+ }
3932
+ return params;
3933
+ }
3934
+ /**
3935
+ * One entry per path. When several declarations resolve to the same
3936
+ * path, keep the one that best describes what the user lands on: a
3937
+ * page beats a layout wrapper (an `index` child and its parent layout
3938
+ * share a path), and a resolved component name beats a name derived
3939
+ * from the path.
3940
+ */
3941
+ dedupeRoutes(routes) {
3942
+ const byPath = /* @__PURE__ */ new Map();
3943
+ for (const route of routes) {
3944
+ const existing = byPath.get(route.path);
3945
+ if (!existing || routeScore(route) > routeScore(existing)) {
3946
+ byPath.set(route.path, route);
2418
3947
  }
2419
3948
  }
2420
- return best?.form;
3949
+ return Array.from(byPath.values());
3950
+ }
3951
+ buildGraph(routes) {
3952
+ const screens = {};
3953
+ const navigatorName = "router";
3954
+ const screenNames = routes.map((r) => r.screenName);
3955
+ for (const route of routes) {
3956
+ const others = screenNames.filter((name) => name !== route.screenName);
3957
+ screens[route.screenName] = {
3958
+ screenName: route.screenName,
3959
+ // Open-union value — web routes, not a RN stack/tab/drawer.
3960
+ navigatorType: WEB_NAVIGATOR_TYPE,
3961
+ parentNavigator: navigatorName,
3962
+ // Any route is one URL away from any other — both directions,
3963
+ // like the RN analyzer models tab navigators.
3964
+ reachableFrom: others,
3965
+ reachableTo: others,
3966
+ ...route.params ? { params: route.params } : {}
3967
+ };
3968
+ }
3969
+ const initialRoute = routes.find((r) => r.path === "/") ?? routes.find((r) => r.index) ?? routes[0];
3970
+ return {
3971
+ screens,
3972
+ initialScreen: initialRoute?.screenName ?? "",
3973
+ navigators: routes.length > 0 ? [{
3974
+ name: navigatorName,
3975
+ type: WEB_NAVIGATOR_TYPE,
3976
+ screens: screenNames
3977
+ }] : []
3978
+ };
2421
3979
  }
2422
3980
  };
3981
+ function screenNameFromPath(routePath) {
3982
+ if (routePath === "/" || routePath === "") return "Home";
3983
+ return routePath.split("/").filter(Boolean).map((segment) => segment.replace(/^:/, "").replace(/\?$/, "")).map(
3984
+ (segment) => segment.split(/[-_.]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("")
3985
+ ).join("");
3986
+ }
3987
+ function routeScore(route) {
3988
+ const isPage = route.layout ? 0 : 2;
3989
+ const hasRealName = route.screenName === screenNameFromPath(route.path) ? 0 : 1;
3990
+ return isPage + hasRealName;
3991
+ }
3992
+ function resolvePathToScreen(routes, target) {
3993
+ const normalized = `/${target}`.replace(/\/+/g, "/").replace(/\?.*$/, "").replace(/#.*$/, "");
3994
+ const cleaned = normalized.length > 1 ? normalized.replace(/\/$/, "") : normalized;
3995
+ const exact = routes.find((r) => r.path === cleaned);
3996
+ if (exact) return exact.screenName;
3997
+ const targetSegments = cleaned.split("/").filter(Boolean);
3998
+ for (const route of routes) {
3999
+ const routeSegments = route.path.split("/").filter(Boolean);
4000
+ if (routeSegments.length !== targetSegments.length) continue;
4001
+ const matches = routeSegments.every(
4002
+ (seg, i) => seg.startsWith(":") || seg === "*" || seg === targetSegments[i]
4003
+ );
4004
+ if (matches) return route.screenName;
4005
+ }
4006
+ return void 0;
4007
+ }
2423
4008
 
2424
- // src/analyzers/GenericPlatformAnalyzer.ts
2425
- var GenericPlatformAnalyzer = class {
2426
- constructor(platform) {
2427
- this.platform = platform;
4009
+ // src/analyzers/web/ReactWebPlatformAnalyzer.ts
4010
+ var ReactWebPlatformAnalyzer = class {
4011
+ platform = "web";
4012
+ async analyze(config, options) {
4013
+ const screenAnalyzer = new WebScreenAnalyzer(config, {
4014
+ screenPatterns: options.screenPatterns
4015
+ });
4016
+ const navigationAnalyzer = new WebNavigationAnalyzer(config, {
4017
+ navigationInclude: options.navigationInclude,
4018
+ navigationExclude: options.navigationExclude
4019
+ });
4020
+ console.log("[ReactWebPlatformAnalyzer] Running analyzers...");
4021
+ const [screenAnalysis, navigationResult] = await Promise.all([
4022
+ screenAnalyzer.analyze(),
4023
+ navigationAnalyzer.analyze()
4024
+ ]);
4025
+ const routeScreenNames = new Set(navigationResult.routes.map((route) => route.screenName));
4026
+ const strictScreens = options.strictScreens ?? true;
4027
+ let screensFilteredOut = 0;
4028
+ const included = [];
4029
+ for (const candidate of screenAnalysis.candidates) {
4030
+ if (!strictScreens || this.isScreen(candidate, routeScreenNames)) {
4031
+ included.push(candidate);
4032
+ } else {
4033
+ screensFilteredOut++;
4034
+ }
4035
+ }
4036
+ const screens = included.map(
4037
+ (candidate) => this.resolveRoutePaths(candidate.descriptor, navigationResult.routes)
4038
+ );
4039
+ console.log(
4040
+ `[ReactWebPlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens, ${navigationResult.routes.length} routes`
4041
+ );
4042
+ return {
4043
+ screens,
4044
+ navigation: navigationResult.graph,
4045
+ analyzedFiles: screenAnalysis.analyzedFiles,
4046
+ ...screensFilteredOut > 0 ? { screensFilteredOut } : {}
4047
+ };
2428
4048
  }
2429
- platform;
2430
- async analyze(_config, _options) {
4049
+ isScreen(candidate, routeScreenNames) {
4050
+ return candidate.hasRegisterScreen || candidate.matchesScreenPattern || routeScreenNames.has(candidate.descriptor.name);
4051
+ }
4052
+ /** Replace route-path references with screen names where the route table resolves them. */
4053
+ resolveRoutePaths(screen, routes) {
4054
+ const resolve2 = (target) => {
4055
+ if (!target || !target.startsWith("/")) return target;
4056
+ return resolvePathToScreen(routes, target) ?? target;
4057
+ };
4058
+ const navigationTargets = Array.from(
4059
+ new Set(
4060
+ screen.navigationTargets.map((target) => resolve2(target)).filter((target) => target !== screen.name)
4061
+ )
4062
+ ).sort();
4063
+ const actions = screen.actions.map((action) => {
4064
+ const resolved = resolve2(action.targetScreen);
4065
+ return resolved === action.targetScreen ? action : { ...action, targetScreen: resolved };
4066
+ });
4067
+ const collections = screen.collections?.map((collection) => {
4068
+ const resolveRow = (row) => {
4069
+ if (!row?.targetScreen) return row;
4070
+ const resolved = resolve2(row.targetScreen);
4071
+ if (resolved === row.targetScreen) return row;
4072
+ return {
4073
+ ...row,
4074
+ targetScreen: resolved,
4075
+ ...row.description ? { description: `Clicking a row opens ${resolved}` } : {}
4076
+ };
4077
+ };
4078
+ const rowAction = resolveRow(collection.rowAction);
4079
+ return {
4080
+ ...collection,
4081
+ ...rowAction ? { rowAction } : {},
4082
+ ...collection.rowActions ? { rowActions: collection.rowActions.map((row) => resolveRow(row)) } : {}
4083
+ };
4084
+ });
2431
4085
  return {
2432
- screens: [],
2433
- navigation: { screens: {}, initialScreen: "", navigators: [] },
2434
- analyzedFiles: 0
4086
+ ...screen,
4087
+ navigationTargets,
4088
+ actions,
4089
+ ...collections ? { collections } : {}
2435
4090
  };
2436
4091
  }
2437
4092
  };
@@ -2641,8 +4296,8 @@ var ZodParsedType = util.arrayToEnum([
2641
4296
  "set"
2642
4297
  ]);
2643
4298
  var getParsedType = (data) => {
2644
- const t8 = typeof data;
2645
- switch (t8) {
4299
+ const t12 = typeof data;
4300
+ switch (t12) {
2646
4301
  case "undefined":
2647
4302
  return ZodParsedType.undefined;
2648
4303
  case "string":
@@ -2914,8 +4569,8 @@ function getErrorMap() {
2914
4569
 
2915
4570
  // ../../node_modules/zod/v3/helpers/parseUtil.js
2916
4571
  var makeIssue = (params) => {
2917
- const { data, path: path7, errorMaps, issueData } = params;
2918
- const fullPath = [...path7, ...issueData.path || []];
4572
+ const { data, path: path9, errorMaps, issueData } = params;
4573
+ const fullPath = [...path9, ...issueData.path || []];
2919
4574
  const fullIssue = {
2920
4575
  ...issueData,
2921
4576
  path: fullPath
@@ -3031,11 +4686,11 @@ var errorUtil;
3031
4686
 
3032
4687
  // ../../node_modules/zod/v3/types.js
3033
4688
  var ParseInputLazyPath = class {
3034
- constructor(parent, value, path7, key) {
4689
+ constructor(parent, value, path9, key) {
3035
4690
  this._cachedPath = [];
3036
4691
  this.parent = parent;
3037
4692
  this.data = value;
3038
- this._path = path7;
4693
+ this._path = path9;
3039
4694
  this._key = key;
3040
4695
  }
3041
4696
  get path() {
@@ -6476,7 +8131,7 @@ var coerce = {
6476
8131
  };
6477
8132
  var NEVER = INVALID;
6478
8133
 
6479
- // ../shared/dist/chunk-XLOYG3DH.mjs
8134
+ // ../shared/dist/chunk-PKS3VDNB.mjs
6480
8135
  var locatorSourceSchema = external_exports.enum([
6481
8136
  "appilotsId",
6482
8137
  "testID",
@@ -6502,7 +8157,16 @@ var locatorDescriptorSchema = external_exports.object({
6502
8157
  source: locatorSourceSchema.optional()
6503
8158
  }).passthrough();
6504
8159
  var signalDescriptorSchema = external_exports.object({
6505
- type: external_exports.enum(["navigation", "goBack", "toast", "modal", "inline-error", "data-arrival", "loading", "none"]).or(external_exports.string().min(1).max(60)),
8160
+ type: external_exports.enum([
8161
+ "navigation",
8162
+ "goBack",
8163
+ "toast",
8164
+ "modal",
8165
+ "inline-error",
8166
+ "data-arrival",
8167
+ "loading",
8168
+ "none"
8169
+ ]).or(external_exports.string().min(1).max(60)),
6506
8170
  target: external_exports.string().optional(),
6507
8171
  description: external_exports.string().optional()
6508
8172
  }).passthrough();
@@ -6516,7 +8180,20 @@ var waitPolicyDescriptorSchema = external_exports.object({
6516
8180
  signals: external_exports.array(signalDescriptorSchema).optional(),
6517
8181
  maxMs: external_exports.number().finite().optional()
6518
8182
  }).passthrough();
6519
- var targetDescriptorRoleSchema = external_exports.enum(["button", "submit", "input", "toggle", "select", "date", "list", "row", "menuItem", "modal", "custom"]).or(external_exports.string().min(1).max(60));
8183
+ var targetDescriptorRoleSchema = external_exports.enum([
8184
+ "button",
8185
+ "submit",
8186
+ "input",
8187
+ "toggle",
8188
+ "select",
8189
+ "date",
8190
+ "list",
8191
+ "row",
8192
+ "menuItem",
8193
+ "modal",
8194
+ "custom"
8195
+ ]).or(external_exports.string().min(1).max(60));
8196
+ var targetOriginSchema = external_exports.enum(["analyzer", "manifest"]).or(external_exports.string().min(1).max(40));
6520
8197
  var targetDescriptorSchema = external_exports.object({
6521
8198
  id: external_exports.string(),
6522
8199
  role: targetDescriptorRoleSchema,
@@ -6530,10 +8207,20 @@ var targetDescriptorSchema = external_exports.object({
6530
8207
  requiresConfirmation: external_exports.boolean().optional(),
6531
8208
  opensModal: external_exports.string().optional(),
6532
8209
  opensBottomSheet: external_exports.string().optional(),
6533
- sourceComponent: external_exports.string().optional()
8210
+ sourceComponent: external_exports.string().optional(),
8211
+ origin: targetOriginSchema.optional()
6534
8212
  }).passthrough();
6535
8213
  var flowStepDescriptorSchema = external_exports.object({
6536
- type: external_exports.enum(["navigate", "fill", "press", "select", "toggle", "wait", "confirm", "choose-list-item"]).or(external_exports.string().min(1).max(60)),
8214
+ type: external_exports.enum([
8215
+ "navigate",
8216
+ "fill",
8217
+ "press",
8218
+ "select",
8219
+ "toggle",
8220
+ "wait",
8221
+ "confirm",
8222
+ "choose-list-item"
8223
+ ]).or(external_exports.string().min(1).max(60)),
6537
8224
  target: external_exports.string().optional(),
6538
8225
  label: external_exports.string().optional(),
6539
8226
  description: external_exports.string().optional(),
@@ -6661,7 +8348,19 @@ var navigationNodeSchema = external_exports.object({
6661
8348
  parentNavigator: external_exports.string().optional(),
6662
8349
  reachableFrom: external_exports.array(external_exports.string()).default([]),
6663
8350
  reachableTo: external_exports.array(external_exports.string()).default([]),
6664
- params: external_exports.array(paramDescriptorSchema).optional()
8351
+ params: external_exports.array(paramDescriptorSchema).optional(),
8352
+ /**
8353
+ * Web clients only (documented convention, additive — previously
8354
+ * round-tripped via `.passthrough()`): the screen's URL route
8355
+ * template, e.g. `/users/:id`. Param segments use `:name` (the
8356
+ * relay also accepts `[name]` / `{name}`). When present and the
8357
+ * request's `context.platform` is `'web'`, the relay resolves
8358
+ * `navigate` targets against these templates and injects the
8359
+ * concrete URL segments as the navigate payload's `path` — the web
8360
+ * equivalent of RN's nested-navigation path injection. See
8361
+ * docs/agent-contract.md ("Web clients" section).
8362
+ */
8363
+ path: external_exports.string().max(500).optional()
6665
8364
  }).passthrough();
6666
8365
  var navigatorDescriptorSchema = external_exports.object({
6667
8366
  name: external_exports.string(),
@@ -6708,6 +8407,50 @@ var loginSchema = external_exports.object({
6708
8407
  loginSchema.extend({
6709
8408
  name: external_exports.string().min(2, "Name must be at least 2 characters").max(100)
6710
8409
  });
8410
+ external_exports.object({
8411
+ name: external_exports.string().min(2, "Name must be at least 2 characters").max(100).optional(),
8412
+ avatarUrl: external_exports.string().url("Invalid URL").max(2048).nullable().optional()
8413
+ }).refine((v) => v.name !== void 0 || v.avatarUrl !== void 0, {
8414
+ message: "At least one field must be provided"
8415
+ });
8416
+ external_exports.object({
8417
+ currentPassword: external_exports.string().min(1, "Current password is required"),
8418
+ newPassword: external_exports.string().min(8, "Password must be at least 8 characters")
8419
+ });
8420
+ var totpCodeSchema = external_exports.string().transform((v) => v.replace(/\s/g, "")).pipe(external_exports.string().regex(/^\d{6}$/, "Code must be 6 digits"));
8421
+ var recoveryCodeSchema = external_exports.string().transform((v) => v.toUpperCase().replace(/[^A-Z0-9]/g, "")).pipe(external_exports.string().regex(/^[23456789BCDFGHJKMNPQRSTVWXYZ]{10}$/, "Invalid recovery code"));
8422
+ external_exports.object({
8423
+ code: totpCodeSchema
8424
+ });
8425
+ external_exports.object({
8426
+ challengeToken: external_exports.string().min(1, "Challenge token is required"),
8427
+ code: totpCodeSchema.optional(),
8428
+ recoveryCode: recoveryCodeSchema.optional()
8429
+ }).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
8430
+ message: "Provide either a TOTP code or a recovery code",
8431
+ path: ["code"]
8432
+ });
8433
+ external_exports.object({
8434
+ password: external_exports.string().min(1, "Password is required"),
8435
+ code: totpCodeSchema.optional(),
8436
+ recoveryCode: recoveryCodeSchema.optional()
8437
+ }).refine((v) => Boolean(v.code) !== Boolean(v.recoveryCode), {
8438
+ message: "Provide either a TOTP code or a recovery code",
8439
+ path: ["code"]
8440
+ });
8441
+ external_exports.object({
8442
+ password: external_exports.string().min(1, "Password is required"),
8443
+ code: totpCodeSchema
8444
+ });
8445
+ external_exports.object({
8446
+ password: external_exports.string().min(1, "Password is required"),
8447
+ code: totpCodeSchema.optional(),
8448
+ recoveryCode: recoveryCodeSchema.optional(),
8449
+ confirm: external_exports.literal("DELETE MY ACCOUNT")
8450
+ }).refine((v) => !(v.code && v.recoveryCode), {
8451
+ message: "Provide either a TOTP code or a recovery code, not both",
8452
+ path: ["code"]
8453
+ });
6711
8454
  var createProjectSchema = external_exports.object({
6712
8455
  name: external_exports.string().min(1).max(100),
6713
8456
  description: external_exports.string().max(500).optional(),
@@ -6743,6 +8486,7 @@ external_exports.object({
6743
8486
  content: external_exports.record(external_exports.unknown())
6744
8487
  });
6745
8488
  var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
8489
+ var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
6746
8490
  external_exports.object({
6747
8491
  name: external_exports.string().min(1).max(100),
6748
8492
  // Required for `sdk` (default), forbidden/ignored for `operator` —
@@ -6750,6 +8494,7 @@ external_exports.object({
6750
8494
  // missing scope still validates as the pre-existing SDK shape.
6751
8495
  projectId: external_exports.string().min(1).optional(),
6752
8496
  scope: apiKeyScopeSchema.default("sdk"),
8497
+ environment: apiKeyEnvironmentSchema.optional(),
6753
8498
  expiresAt: external_exports.string().datetime().optional()
6754
8499
  }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
6755
8500
  message: 'projectId is required for scope "sdk"',
@@ -6757,13 +8502,23 @@ external_exports.object({
6757
8502
  });
6758
8503
  var boundedString = (max) => external_exports.string().max(max);
6759
8504
  var clientPlatformSchema = external_exports.enum(["react-native", "web", "android", "ios"]).or(external_exports.string().min(1).max(40));
8505
+ var identityProvenanceSchema = external_exports.enum(["declared", "derived", "positional"]).or(external_exports.string().min(1).max(40));
6760
8506
  var snapshotInputSchema = external_exports.object({
6761
8507
  id: boundedString(160).optional(),
8508
+ provenance: identityProvenanceSchema.optional(),
8509
+ /**
8510
+ * False when the control is mounted but currently OUTSIDE the window —
8511
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8512
+ * cannot tell (old React Native architecture, or any SDK published
8513
+ * before the field existed), which must never be read as `false`.
8514
+ */
8515
+ onScreen: external_exports.boolean().optional(),
6762
8516
  label: boundedString(300).optional(),
6763
8517
  value: boundedString(4096).optional(),
6764
8518
  placeholder: boundedString(300).optional(),
6765
8519
  editable: external_exports.boolean().optional(),
6766
8520
  secure: external_exports.boolean().optional(),
8521
+ submitsOnReturn: external_exports.boolean().optional(),
6767
8522
  type: boundedString(40).optional(),
6768
8523
  required: external_exports.boolean().optional(),
6769
8524
  invalid: external_exports.boolean().optional(),
@@ -6771,6 +8526,14 @@ var snapshotInputSchema = external_exports.object({
6771
8526
  }).passthrough();
6772
8527
  var snapshotButtonSchema = external_exports.object({
6773
8528
  id: boundedString(160).optional(),
8529
+ provenance: identityProvenanceSchema.optional(),
8530
+ /**
8531
+ * False when the control is mounted but currently OUTSIDE the window —
8532
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8533
+ * cannot tell (old React Native architecture, or any SDK published
8534
+ * before the field existed), which must never be read as `false`.
8535
+ */
8536
+ onScreen: external_exports.boolean().optional(),
6774
8537
  label: boundedString(300).optional(),
6775
8538
  disabled: external_exports.boolean().optional(),
6776
8539
  inModal: external_exports.boolean().optional(),
@@ -6780,12 +8543,28 @@ var snapshotButtonSchema = external_exports.object({
6780
8543
  }).passthrough();
6781
8544
  var snapshotToggleSchema = external_exports.object({
6782
8545
  id: boundedString(160).optional(),
8546
+ provenance: identityProvenanceSchema.optional(),
8547
+ /**
8548
+ * False when the control is mounted but currently OUTSIDE the window —
8549
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8550
+ * cannot tell (old React Native architecture, or any SDK published
8551
+ * before the field existed), which must never be read as `false`.
8552
+ */
8553
+ onScreen: external_exports.boolean().optional(),
6783
8554
  label: boundedString(300).optional(),
6784
8555
  value: external_exports.boolean().optional(),
6785
8556
  inModal: external_exports.boolean().optional()
6786
8557
  }).passthrough();
6787
8558
  var snapshotSliderSchema = external_exports.object({
6788
8559
  id: boundedString(160).optional(),
8560
+ provenance: identityProvenanceSchema.optional(),
8561
+ /**
8562
+ * False when the control is mounted but currently OUTSIDE the window —
8563
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8564
+ * cannot tell (old React Native architecture, or any SDK published
8565
+ * before the field existed), which must never be read as `false`.
8566
+ */
8567
+ onScreen: external_exports.boolean().optional(),
6789
8568
  label: boundedString(300).optional(),
6790
8569
  value: external_exports.number().finite().optional(),
6791
8570
  min: external_exports.number().finite().optional(),
@@ -6826,6 +8605,26 @@ var snapshotListSchema = external_exports.object({
6826
8605
  // should chase, and the relay only reads a known subset.
6827
8606
  items: external_exports.array(external_exports.record(external_exports.unknown())).max(500).optional()
6828
8607
  }).passthrough();
8608
+ var snapshotNativeDialogSchema = external_exports.object({
8609
+ id: boundedString(160),
8610
+ title: boundedString(300).optional(),
8611
+ message: boundedString(2e3).optional(),
8612
+ buttons: external_exports.array(
8613
+ external_exports.object({
8614
+ label: boundedString(160),
8615
+ style: external_exports.enum(["default", "cancel", "destructive"]).optional()
8616
+ }).passthrough()
8617
+ ).max(10)
8618
+ }).passthrough();
8619
+ var snapshotScrollableSchema = external_exports.object({
8620
+ id: boundedString(160),
8621
+ containerType: boundedString(60).optional(),
8622
+ label: boundedString(300).optional(),
8623
+ scrollOffsetY: external_exports.number().finite().optional(),
8624
+ canScrollUp: external_exports.boolean().optional(),
8625
+ canScrollDown: external_exports.boolean().optional(),
8626
+ horizontal: external_exports.boolean().optional()
8627
+ }).passthrough();
6829
8628
  var snapshotChoiceGroupSchema = external_exports.object({
6830
8629
  index: external_exports.number().int().optional(),
6831
8630
  id: boundedString(160).optional(),
@@ -6842,6 +8641,14 @@ var snapshotElementSchema = external_exports.object({
6842
8641
  disabled: external_exports.boolean().optional(),
6843
8642
  selected: external_exports.boolean().optional(),
6844
8643
  source: boundedString(40).optional(),
8644
+ provenance: identityProvenanceSchema.optional(),
8645
+ /**
8646
+ * False when the control is mounted but currently OUTSIDE the window —
8647
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8648
+ * cannot tell (old React Native architecture, or any SDK published
8649
+ * before the field existed), which must never be read as `false`.
8650
+ */
8651
+ onScreen: external_exports.boolean().optional(),
6845
8652
  targetId: boundedString(160).optional(),
6846
8653
  listContext: external_exports.record(external_exports.unknown()).optional(),
6847
8654
  inModal: external_exports.boolean().optional()
@@ -6858,7 +8665,34 @@ var agentSnapshotSchema = external_exports.object({
6858
8665
  modalOpen: external_exports.boolean().optional(),
6859
8666
  lists: external_exports.array(snapshotListSchema).max(100).optional(),
6860
8667
  choiceGroups: external_exports.array(snapshotChoiceGroupSchema).max(100).optional(),
6861
- elements: external_exports.array(snapshotElementSchema).max(1e3).optional()
8668
+ scrollables: external_exports.array(snapshotScrollableSchema).max(20).optional(),
8669
+ nativeDialog: snapshotNativeDialogSchema.optional(),
8670
+ elements: external_exports.array(snapshotElementSchema).max(1e3).optional(),
8671
+ /**
8672
+ * The client clamped this observation to the caps above and lost
8673
+ * data doing it (`clampSnapshotToWireLimits` in
8674
+ * `@appilots/client-core`). Read by the prompt serializer so the
8675
+ * model is told the view is partial — a screen truncated at 500
8676
+ * texts must not be read as a screen that only has 500 things on
8677
+ * it. Optional: older SDKs never clamp and never send it.
8678
+ */
8679
+ truncated: external_exports.boolean().optional(),
8680
+ /**
8681
+ * How many of this screen's controls the client could name, split by
8682
+ * `identityProvenance`. Diagnostic only — the relay never grounds an
8683
+ * action on it — but it is the number that says whether an app needs
8684
+ * annotating, and comparing it between a debug and a release build is
8685
+ * the first direct measurement of what minification costs the agent.
8686
+ */
8687
+ stats: external_exports.object({
8688
+ visitedFibers: external_exports.number().int().nonnegative().optional(),
8689
+ skippedHidden: external_exports.number().int().nonnegative().optional(),
8690
+ identity: external_exports.object({
8691
+ declared: external_exports.number().int().nonnegative().optional(),
8692
+ derived: external_exports.number().int().nonnegative().optional(),
8693
+ positional: external_exports.number().int().nonnegative().optional()
8694
+ }).passthrough().optional()
8695
+ }).passthrough().optional()
6862
8696
  }).passthrough();
6863
8697
  var agentContextSchema = external_exports.object({
6864
8698
  /**
@@ -6908,6 +8742,19 @@ var agentContextSchema = external_exports.object({
6908
8742
  ).max(200).optional(),
6909
8743
  loadingPending: external_exports.boolean().optional()
6910
8744
  }).passthrough();
8745
+ var introspectionFailureReasonSchema = external_exports.enum([
8746
+ "sentinel-missing-internals",
8747
+ "never-mounted",
8748
+ "render-crashed",
8749
+ "names-mangled",
8750
+ "walk-recognized-nothing"
8751
+ ]);
8752
+ var introspectionDiagnosticsSchema = external_exports.object({
8753
+ captured: external_exports.boolean(),
8754
+ failureReason: introspectionFailureReasonSchema.nullish(),
8755
+ reactVersion: boundedString(32).nullish(),
8756
+ failureCount: external_exports.number().int().min(0).max(1e6).optional()
8757
+ });
6911
8758
  external_exports.object({
6912
8759
  content: external_exports.string().min(1).max(4096),
6913
8760
  sessionId: external_exports.string().optional(),
@@ -6917,7 +8764,20 @@ external_exports.object({
6917
8764
  * the active MCP doc and emits `mcp_version_mismatch` telemetry when
6918
8765
  * they differ. Optional — older SDKs simply skip the check.
6919
8766
  */
6920
- mcpVersion: external_exports.string().max(64).optional()
8767
+ mcpVersion: external_exports.string().max(64).optional(),
8768
+ /**
8769
+ * SDK-side introspection health. Only sent when it FAILED — a client
8770
+ * that reached the React tree omits the field entirely, so the happy
8771
+ * path costs nothing on the wire.
8772
+ *
8773
+ * Without this, an app whose introspection is dead sends a snapshot
8774
+ * that is empty but perfectly well-formed, indistinguishable from a
8775
+ * genuinely empty screen. The server emits
8776
+ * `introspection_unavailable` so the failure is visible to us instead
8777
+ * of waiting for a bug report. Optional — older SDKs simply never
8778
+ * send it.
8779
+ */
8780
+ introspection: introspectionDiagnosticsSchema.optional()
6921
8781
  });
6922
8782
  var agentActionTypeSchema = external_exports.enum([
6923
8783
  "navigate",
@@ -6928,11 +8788,22 @@ var agentActionTypeSchema = external_exports.enum([
6928
8788
  "custom"
6929
8789
  ]);
6930
8790
  var navigationPayloadSchema = external_exports.object({
6931
- screenName: external_exports.string().min(1),
8791
+ // Optional ONLY so `goBack` can exist (#398): it returns to whatever
8792
+ // is beneath on the stack and has no destination to name. Every other
8793
+ // action is still required to carry one — enforced by the refine
8794
+ // below rather than by the field, so the error names the real problem
8795
+ // ("navigate needs a screenName") instead of a missing key.
8796
+ screenName: external_exports.string().min(1).optional(),
6932
8797
  params: external_exports.record(external_exports.unknown()).optional(),
6933
8798
  navigationAction: external_exports.enum(["push", "navigate", "replace", "goBack", "reset"]).default("navigate").optional(),
6934
8799
  path: external_exports.array(external_exports.string().min(1)).optional()
6935
- }).passthrough();
8800
+ }).passthrough().refine(
8801
+ (payload) => payload.navigationAction === "goBack" || typeof payload.screenName === "string" && payload.screenName.length > 0,
8802
+ {
8803
+ message: 'screenName is required for every navigationAction except "goBack"',
8804
+ path: ["screenName"]
8805
+ }
8806
+ );
6936
8807
  var formFillFieldSchema = external_exports.object({
6937
8808
  fieldId: external_exports.string().min(1),
6938
8809
  fieldType: external_exports.enum(["text", "select", "toggle", "date", "number", "custom"]).default("text").optional(),
@@ -7266,6 +9137,8 @@ var sandboxObservationSchema = external_exports.object({
7266
9137
  inputs: external_exports.array(
7267
9138
  external_exports.object({
7268
9139
  id: external_exports.string().max(120),
9140
+ provenance: identityProvenanceSchema.optional(),
9141
+ onScreen: external_exports.boolean().optional(),
7269
9142
  label: external_exports.string().max(200).optional(),
7270
9143
  value: external_exports.string().max(2e3).optional(),
7271
9144
  disabled: external_exports.boolean().optional(),
@@ -7279,6 +9152,8 @@ var sandboxObservationSchema = external_exports.object({
7279
9152
  buttons: external_exports.array(
7280
9153
  external_exports.object({
7281
9154
  id: external_exports.string().max(120),
9155
+ provenance: identityProvenanceSchema.optional(),
9156
+ onScreen: external_exports.boolean().optional(),
7282
9157
  label: external_exports.string().max(200).optional(),
7283
9158
  disabled: external_exports.boolean().optional(),
7284
9159
  /** Risk metadata (e.g. "high") the SDK forwards for danger UI. */
@@ -7300,6 +9175,8 @@ var sandboxObservationSchema = external_exports.object({
7300
9175
  sliders: external_exports.array(
7301
9176
  external_exports.object({
7302
9177
  id: external_exports.string().max(120),
9178
+ provenance: identityProvenanceSchema.optional(),
9179
+ onScreen: external_exports.boolean().optional(),
7303
9180
  label: external_exports.string().max(200).optional(),
7304
9181
  value: external_exports.number().finite().optional(),
7305
9182
  min: external_exports.number().finite().optional(),
@@ -7329,6 +9206,44 @@ var sandboxObservationSchema = external_exports.object({
7329
9206
  ).max(200).optional()
7330
9207
  }).passthrough()
7331
9208
  ).max(20).optional(),
9209
+ /**
9210
+ * Toggles exactly as the SDK snapshot reports them (#390).
9211
+ *
9212
+ * Their absence is why `generality-component-control-smarthome`
9213
+ * came back `errored` with a 422 instead of measuring anything:
9214
+ * this schema is `.strict()`, so a scenario that declares a switch
9215
+ * is rejected before the agent is ever asked. Which means the
9216
+ * corpus could not express the single control behind failure 02 of
9217
+ * the functional audit — "disable an agent; the switch never
9218
+ * moves". The one defect the eval most needed to catch was the one
9219
+ * shape it could not describe.
9220
+ */
9221
+ toggles: external_exports.array(
9222
+ external_exports.object({
9223
+ id: external_exports.string().max(120),
9224
+ provenance: identityProvenanceSchema.optional(),
9225
+ onScreen: external_exports.boolean().optional(),
9226
+ label: external_exports.string().max(200).optional(),
9227
+ value: external_exports.boolean().optional(),
9228
+ disabled: external_exports.boolean().optional()
9229
+ })
9230
+ ).max(100).optional(),
9231
+ /**
9232
+ * The interaction graph — the `el:*` ids the prompt tells the model
9233
+ * to prefer over ordinal handles. Passthrough per entry for the
9234
+ * same reason `lists` is: the SDK adds fields faster than this
9235
+ * schema should chase them.
9236
+ */
9237
+ elements: external_exports.array(
9238
+ external_exports.object({
9239
+ id: external_exports.string().max(160),
9240
+ role: external_exports.string().max(40).optional(),
9241
+ label: external_exports.string().max(300).optional()
9242
+ }).passthrough()
9243
+ ).max(500).optional(),
9244
+ /** Screen-level state the relay renders and branches on. */
9245
+ loading: external_exports.boolean().optional(),
9246
+ modalOpen: external_exports.boolean().optional(),
7332
9247
  extra: external_exports.record(external_exports.unknown()).optional()
7333
9248
  }).strict();
7334
9249
  external_exports.object({
@@ -7369,8 +9284,36 @@ external_exports.object({
7369
9284
  * heuristics, forces tool choice when the last user message carries a
7370
9285
  * failure marker). Only meaningful alongside `history`.
7371
9286
  */
7372
- isRecoveryHop: external_exports.boolean().optional()
9287
+ isRecoveryHop: external_exports.boolean().optional(),
9288
+ /**
9289
+ * Run this call against the application structure supplied here
9290
+ * instead of the project's active MCP document.
9291
+ *
9292
+ * Sandbox-only, and the relay honours it only on a `dryRun` call —
9293
+ * see `resolveMcpOverride` in apps/api. It exists because the eval
9294
+ * corpus could otherwise only ever describe ONE app: every scenario
9295
+ * resolves to the same seeded project, so every navigate destination,
9296
+ * every screen title and every bit of domain priming in the prompt
9297
+ * came from the same vehicle-management demo. A suite that can only
9298
+ * pose questions about one app cannot measure whether the agent works
9299
+ * on another one.
9300
+ *
9301
+ * Deliberately loose (`z.record`): the MCP document's shape is owned
9302
+ * by the generator, and pinning it here would turn every additive
9303
+ * generator change into a 422 in the sandbox.
9304
+ */
9305
+ applicationStructure: external_exports.record(external_exports.unknown()).optional()
7373
9306
  }).strict();
9307
+ var relayDiagnosticsSchema = external_exports.object({
9308
+ unverifiedTargets: external_exports.array(external_exports.string()),
9309
+ guardsFired: external_exports.array(external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })),
9310
+ repairMode: external_exports.enum(["repair", "strict", "disabled"]),
9311
+ promptVersion: external_exports.string(),
9312
+ // Defaulted for producers that predate the field: before it existed
9313
+ // every response had reached the model, so `true` is the correct
9314
+ // backfill.
9315
+ modelInvoked: external_exports.boolean().default(true)
9316
+ });
7374
9317
  var sandboxTraceHopSchema = external_exports.object({
7375
9318
  hop: external_exports.number().int().nonnegative(),
7376
9319
  inputPreview: external_exports.string(),
@@ -7409,7 +9352,16 @@ external_exports.object({
7409
9352
  /** Hop-by-hop trace. Always at least one entry. */
7410
9353
  trace: external_exports.array(sandboxTraceHopSchema),
7411
9354
  /** True when the run was a no-op (e.g. project has no MCP yet). */
7412
- warning: external_exports.string().optional()
9355
+ warning: external_exports.string().optional(),
9356
+ /**
9357
+ * SPEC-046 — guard / hallucination telemetry for the run.
9358
+ *
9359
+ * ADDITIVE AND OPTIONAL, permanently: SDK and dashboard consumers
9360
+ * predate it, and older API deployments will not send it. Consumers
9361
+ * must treat an absent block as "no information", never as "no guards
9362
+ * fired".
9363
+ */
9364
+ diagnostics: relayDiagnosticsSchema.optional()
7413
9365
  });
7414
9366
  external_exports.object({
7415
9367
  name: external_exports.string().min(1).max(120),
@@ -7613,6 +9565,30 @@ external_exports.object({
7613
9565
  name: external_exports.string().min(1).max(200).optional(),
7614
9566
  identifiers: endUserIdentifiersSchema.optional()
7615
9567
  });
9568
+ var MAX_EXTERNAL_USER_ID_LENGTH = 128;
9569
+ function normalizeExternalUserId(value) {
9570
+ let raw;
9571
+ if (typeof value === "string") raw = value;
9572
+ else if (typeof value === "number" && Number.isFinite(value)) raw = String(value);
9573
+ else return void 0;
9574
+ const trimmed = raw.trim();
9575
+ if (!trimmed) return void 0;
9576
+ if (trimmed.length > MAX_EXTERNAL_USER_ID_LENGTH) return void 0;
9577
+ return trimmed;
9578
+ }
9579
+ var MAX_DEVICE_INFO_BYTES = 4096;
9580
+ var sessionDeviceInfoSchema = external_exports.object({
9581
+ platform: external_exports.string().max(64).optional(),
9582
+ osVersion: external_exports.string().max(64).optional(),
9583
+ appVersion: external_exports.string().max(64).optional(),
9584
+ sdkVersion: external_exports.string().max(64).optional()
9585
+ }).passthrough().refine((info) => JSON.stringify(info).length <= MAX_DEVICE_INFO_BYTES, {
9586
+ message: `device_info exceeds ${MAX_DEVICE_INFO_BYTES} bytes`
9587
+ });
9588
+ external_exports.object({
9589
+ userId: external_exports.preprocess(normalizeExternalUserId, external_exports.string().optional()),
9590
+ deviceInfo: sessionDeviceInfoSchema.optional().catch(void 0)
9591
+ });
7616
9592
  external_exports.object({
7617
9593
  projectId: external_exports.string().min(1).max(64),
7618
9594
  externalId: external_exports.string().min(1).max(128),
@@ -7620,6 +9596,27 @@ external_exports.object({
7620
9596
  identifiers: endUserIdentifiersSchema.optional()
7621
9597
  });
7622
9598
 
9599
+ // src/utils/stringify.ts
9600
+ function stringifyUnknown(value) {
9601
+ if (value instanceof Error) {
9602
+ const message = value.message || value.name;
9603
+ const cause = value.cause;
9604
+ if (cause instanceof Error && cause.message && cause.message !== message) {
9605
+ return `${message} (${cause.message})`;
9606
+ }
9607
+ return message;
9608
+ }
9609
+ if (typeof value === "string") return value;
9610
+ if (value === null || value === void 0) return String(value);
9611
+ if (typeof value !== "object") return String(value);
9612
+ try {
9613
+ const json = JSON.stringify(value);
9614
+ if (json && json !== "{}") return json;
9615
+ } catch {
9616
+ }
9617
+ return String(value);
9618
+ }
9619
+
7623
9620
  // src/manifest/loadManifest.ts
7624
9621
  var DEFAULT_MANIFEST_FILENAME = "appilots.manifest.json";
7625
9622
  var manifestSchema = external_exports.object({
@@ -7628,7 +9625,7 @@ var manifestSchema = external_exports.object({
7628
9625
  navigation: navigationGraphSchema.partial().optional()
7629
9626
  }).passthrough();
7630
9627
  async function loadManifest(rootDir, manifestPath) {
7631
- const resolvedPath = path6__namespace.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
9628
+ const resolvedPath = path8__namespace.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
7632
9629
  let raw;
7633
9630
  try {
7634
9631
  raw = await fs.readFile(resolvedPath, "utf-8");
@@ -7636,14 +9633,16 @@ async function loadManifest(rootDir, manifestPath) {
7636
9633
  if (error?.code === "ENOENT") {
7637
9634
  return { manifest: null, resolvedPath };
7638
9635
  }
7639
- throw new Error(`Failed to read Appilots manifest at ${resolvedPath}: ${error?.message ?? String(error)}`);
9636
+ throw new Error(
9637
+ `Failed to read Appilots manifest at ${resolvedPath}: ${stringifyUnknown(error)}`
9638
+ );
7640
9639
  }
7641
9640
  let parsedJson;
7642
9641
  try {
7643
9642
  parsedJson = JSON.parse(raw);
7644
9643
  } catch (error) {
7645
9644
  throw new Error(
7646
- `Appilots manifest at ${resolvedPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
9645
+ `Appilots manifest at ${resolvedPath} is not valid JSON: ${stringifyUnknown(error)}`
7647
9646
  );
7648
9647
  }
7649
9648
  const result = manifestSchema.safeParse(parsedJson);
@@ -7657,11 +9656,47 @@ Each entry in "screens" must match the ScreenDescriptor shape (see docs/platform
7657
9656
  }
7658
9657
  return { manifest: result.data, resolvedPath };
7659
9658
  }
9659
+ function mergeScreen(base, declared) {
9660
+ const fields = { ...base };
9661
+ for (const [key, value] of Object.entries(declared)) {
9662
+ if (value === void 0) continue;
9663
+ if (Array.isArray(value) && value.length === 0) continue;
9664
+ fields[key] = value;
9665
+ }
9666
+ const merged = fields;
9667
+ merged.targets = mergeById(base.targets, markManifestOrigin(declared.targets));
9668
+ merged.actions = mergeById(base.actions, declared.actions) ?? [];
9669
+ if (base.agentHints || declared.agentHints) {
9670
+ merged.agentHints = { ...base.agentHints, ...declared.agentHints };
9671
+ }
9672
+ return merged;
9673
+ }
9674
+ function mergeById(base, overlay) {
9675
+ if (!overlay || overlay.length === 0) return base;
9676
+ if (!base || base.length === 0) return overlay;
9677
+ const byId = /* @__PURE__ */ new Map();
9678
+ for (const entry of base) byId.set(entry.id, entry);
9679
+ for (const entry of overlay) {
9680
+ const existing = byId.get(entry.id);
9681
+ byId.set(entry.id, existing ? { ...existing, ...entry } : entry);
9682
+ }
9683
+ return Array.from(byId.values());
9684
+ }
9685
+ function markManifestOrigin(targets) {
9686
+ if (!targets || targets.length === 0) return targets;
9687
+ return targets.map((target) => ({ origin: "manifest", ...target }));
9688
+ }
7660
9689
  function mergeManifestScreens(analyzerScreens, manifestScreens) {
7661
9690
  if (!manifestScreens || manifestScreens.length === 0) return analyzerScreens;
7662
9691
  const byName = /* @__PURE__ */ new Map();
7663
9692
  for (const screen of analyzerScreens) byName.set(screen.name, screen);
7664
- for (const screen of manifestScreens) byName.set(screen.name, screen);
9693
+ for (const declared of manifestScreens) {
9694
+ const base = byName.get(declared.name);
9695
+ byName.set(
9696
+ declared.name,
9697
+ base ? mergeScreen(base, declared) : { ...declared, targets: markManifestOrigin(declared.targets) }
9698
+ );
9699
+ }
7665
9700
  return Array.from(byName.values());
7666
9701
  }
7667
9702
  function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
@@ -7913,12 +9948,15 @@ var MCPGenerator = class _MCPGenerator {
7913
9948
  /**
7914
9949
  * Select the `PlatformAnalyzer` implementation for a `.appilotsrc`
7915
9950
  * `platform` value. `'react-native'` (or unset — the existing default)
7916
- * gets the real Babel/JSX pipeline; anything else gets the no-op
7917
- * generic analyzer, relying entirely on a declared manifest.
9951
+ * gets the RN Babel/JSX pipeline; `'web'` gets the React web (DOM +
9952
+ * React Router) pipeline; anything else gets the no-op generic
9953
+ * analyzer, relying entirely on a declared manifest. The manifest
9954
+ * still merges on top of every analyzer's output either way.
7918
9955
  */
7919
9956
  static createPlatformAnalyzer(platform) {
7920
9957
  const resolved = platform ?? "react-native";
7921
9958
  if (resolved === "react-native") return new ReactNativePlatformAnalyzer();
9959
+ if (resolved === "web") return new ReactWebPlatformAnalyzer();
7922
9960
  return new GenericPlatformAnalyzer(resolved);
7923
9961
  }
7924
9962
  /** Generate MCP documents from the project */
@@ -7948,7 +9986,12 @@ var MCPGenerator = class _MCPGenerator {
7948
9986
  }
7949
9987
  const mergedScreens = mergeManifestScreens(analyzed.screens, manifest?.screens);
7950
9988
  const navigation = mergeManifestNavigation(analyzed.navigation, manifest?.navigation);
7951
- const agentReadyScreens = mergedScreens.map(enrichScreenForAgent);
9989
+ const agentReadyScreens = mergedScreens.map(
9990
+ (screen) => enrichScreenForAgent({
9991
+ ...screen,
9992
+ filePath: screen.filePath ? path8__namespace.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
9993
+ })
9994
+ );
7952
9995
  const projectInfo = await this.getProjectInfo();
7953
9996
  const projectName = projectInfo.name;
7954
9997
  console.log(`[MCPGenerator] Project name: ${projectName}`);
@@ -7971,13 +10014,10 @@ var MCPGenerator = class _MCPGenerator {
7971
10014
  };
7972
10015
  const serialized = JSON.stringify(document, null, 2);
7973
10016
  const checksum = this.calculateChecksum(serialized);
7974
- const filePath = path6__namespace.default.resolve(
7975
- outputDir,
7976
- `mcp-document.${this.options.format}`
7977
- );
10017
+ const filePath = path8__namespace.default.resolve(outputDir, `mcp-document.${this.options.format}`);
7978
10018
  await fs.writeFile(filePath, serialized, "utf-8");
7979
10019
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
7980
- const checksumFilePath = path6__namespace.default.resolve(outputDir, ".appilots-checksum");
10020
+ const checksumFilePath = path8__namespace.default.resolve(outputDir, ".appilots-checksum");
7981
10021
  await fs.writeFile(checksumFilePath, checksum, "utf-8");
7982
10022
  console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
7983
10023
  console.log("[MCPGenerator] Generation complete!");
@@ -7997,7 +10037,7 @@ var MCPGenerator = class _MCPGenerator {
7997
10037
  * after calling `generate()`.
7998
10038
  */
7999
10039
  static async readPreviousChecksum(outputDir) {
8000
- const checksumFilePath = path6__namespace.default.resolve(outputDir, ".appilots-checksum");
10040
+ const checksumFilePath = path8__namespace.default.resolve(outputDir, ".appilots-checksum");
8001
10041
  try {
8002
10042
  const content = await fs.readFile(checksumFilePath, "utf-8");
8003
10043
  return content.trim() || null;
@@ -8016,7 +10056,7 @@ var MCPGenerator = class _MCPGenerator {
8016
10056
  */
8017
10057
  async getProjectInfo() {
8018
10058
  try {
8019
- const packageJsonPath = path6__namespace.default.resolve(this.analyzerConfig.rootDir, "package.json");
10059
+ const packageJsonPath = path8__namespace.default.resolve(this.analyzerConfig.rootDir, "package.json");
8020
10060
  const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
8021
10061
  const packageJson = JSON.parse(packageJsonContent);
8022
10062
  return {
@@ -8029,6 +10069,70 @@ var MCPGenerator = class _MCPGenerator {
8029
10069
  }
8030
10070
  }
8031
10071
  };
10072
+ var DEFAULT_SERVER_URL = "http://localhost:4000";
10073
+ var KNOWN_CONFIG_KEYS = [
10074
+ "apiKey",
10075
+ "projectId",
10076
+ "serverUrl",
10077
+ "outputDir",
10078
+ "include",
10079
+ "exclude",
10080
+ "autoActivate",
10081
+ "strictScreens",
10082
+ "screenPatterns",
10083
+ "navigationInclude",
10084
+ "navigationExclude",
10085
+ "platform",
10086
+ "manifestPath",
10087
+ "eval"
10088
+ ];
10089
+ var KEY_ALIASES = {
10090
+ apiUrl: "serverUrl",
10091
+ // `@appilots/sdk`'s own name for this. One `.appilotsrc` feeds both
10092
+ // packages, so an integrator who wrote the SDK's key should not be told
10093
+ // it is unrecognized.
10094
+ apiBaseUrl: "serverUrl",
10095
+ apiURL: "serverUrl",
10096
+ baseUrl: "serverUrl",
10097
+ url: "serverUrl",
10098
+ host: "serverUrl",
10099
+ endpoint: "serverUrl",
10100
+ server: "serverUrl",
10101
+ key: "apiKey",
10102
+ token: "apiKey",
10103
+ project: "projectId",
10104
+ sourceDir: "include",
10105
+ srcDir: "include",
10106
+ source: "include",
10107
+ outDir: "outputDir"
10108
+ };
10109
+ function editDistance(a, b) {
10110
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
10111
+ for (let i = 1; i <= a.length; i++) {
10112
+ const current = [i];
10113
+ for (let j = 1; j <= b.length; j++) {
10114
+ const deletion = (previous[j] ?? 0) + 1;
10115
+ const insertion = (current[j - 1] ?? 0) + 1;
10116
+ const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
10117
+ current.push(Math.min(deletion, insertion, substitution));
10118
+ }
10119
+ previous = current;
10120
+ }
10121
+ return previous[b.length] ?? 0;
10122
+ }
10123
+ function suggestConfigKey(key) {
10124
+ const alias = KEY_ALIASES[key];
10125
+ if (alias) return alias;
10126
+ const lower = key.toLowerCase();
10127
+ let best;
10128
+ for (const known of KNOWN_CONFIG_KEYS) {
10129
+ const distance = editDistance(lower, known.toLowerCase());
10130
+ if (distance <= 2 && (!best || distance < best.distance)) {
10131
+ best = { key: known, distance };
10132
+ }
10133
+ }
10134
+ return best?.key;
10135
+ }
8032
10136
  function getEnvOverrides(env = process.env) {
8033
10137
  const clean = (value) => {
8034
10138
  const trimmed = value?.trim();
@@ -8040,7 +10144,7 @@ function getEnvOverrides(env = process.env) {
8040
10144
  serverUrl: clean(env.APPILOTS_SERVER_URL)
8041
10145
  };
8042
10146
  }
8043
- function loadConfig() {
10147
+ function loadConfig(onWarn) {
8044
10148
  const configPath = getConfigPath();
8045
10149
  const env = getEnvOverrides();
8046
10150
  let fileConfig = {};
@@ -8048,13 +10152,15 @@ function loadConfig() {
8048
10152
  try {
8049
10153
  fileConfig = JSON.parse(fs$1.readFileSync(configPath, "utf-8"));
8050
10154
  } catch (error) {
8051
- throw new Error(`Failed to parse .appilotsrc at ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
10155
+ throw new Error(
10156
+ `Failed to parse .appilotsrc at ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
10157
+ );
8052
10158
  }
8053
10159
  } else if (!env.apiKey) {
8054
10160
  return null;
8055
10161
  }
8056
10162
  const merged = {
8057
- serverUrl: "http://localhost:4000",
10163
+ serverUrl: DEFAULT_SERVER_URL,
8058
10164
  outputDir: ".appilots",
8059
10165
  autoActivate: true,
8060
10166
  strictScreens: true,
@@ -8064,6 +10170,12 @@ function loadConfig() {
8064
10170
  ...env.serverUrl ? { serverUrl: env.serverUrl } : {}
8065
10171
  };
8066
10172
  const validation = validateConfig(merged);
10173
+ if (onWarn) {
10174
+ const source = configPath ?? "environment variables";
10175
+ for (const warning of validation.warnings) {
10176
+ onWarn(`${source}: ${warning}`);
10177
+ }
10178
+ }
8067
10179
  if (!validation.valid) {
8068
10180
  const source = configPath ?? "environment variables";
8069
10181
  throw new Error(
@@ -8074,12 +10186,12 @@ function loadConfig() {
8074
10186
  return merged;
8075
10187
  }
8076
10188
  function saveConfig(dir, config) {
8077
- const configPath = path6.join(dir, ".appilotsrc");
10189
+ const configPath = path8.join(dir, ".appilotsrc");
8078
10190
  const existingConfig = getConfigPath() ? loadConfig() : null;
8079
10191
  const mergedConfig = {
8080
10192
  apiKey: config.apiKey || existingConfig?.apiKey || "",
8081
10193
  projectId: config.projectId || existingConfig?.projectId || "",
8082
- serverUrl: config.serverUrl || existingConfig?.serverUrl || "http://localhost:4000",
10194
+ serverUrl: config.serverUrl || existingConfig?.serverUrl || DEFAULT_SERVER_URL,
8083
10195
  outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
8084
10196
  autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
8085
10197
  include: config.include || existingConfig?.include,
@@ -8095,23 +10207,25 @@ function saveConfig(dir, config) {
8095
10207
  try {
8096
10208
  fs$1.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
8097
10209
  } catch (error) {
8098
- throw new Error(`Failed to write .appilotsrc to ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
10210
+ throw new Error(
10211
+ `Failed to write .appilotsrc to ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
10212
+ );
8099
10213
  }
8100
10214
  }
8101
10215
  function getConfigPath() {
8102
- let currentDir = path6.resolve(process.cwd());
8103
- const root = path6.resolve("/");
10216
+ let currentDir = path8.resolve(process.cwd());
10217
+ const root = path8.resolve("/");
8104
10218
  while (currentDir !== root) {
8105
- const configPath = path6.join(currentDir, ".appilotsrc");
10219
+ const configPath = path8.join(currentDir, ".appilotsrc");
8106
10220
  try {
8107
10221
  if (fs$1.existsSync(configPath) && fs$1.statSync(configPath).isFile()) {
8108
10222
  return configPath;
8109
10223
  }
8110
10224
  } catch {
8111
10225
  }
8112
- currentDir = path6.resolve(currentDir, "..");
10226
+ currentDir = path8.resolve(currentDir, "..");
8113
10227
  }
8114
- const rootConfigPath = path6.join(root, ".appilotsrc");
10228
+ const rootConfigPath = path8.join(root, ".appilotsrc");
8115
10229
  try {
8116
10230
  if (fs$1.existsSync(rootConfigPath) && fs$1.statSync(rootConfigPath).isFile()) {
8117
10231
  return rootConfigPath;
@@ -8122,14 +10236,25 @@ function getConfigPath() {
8122
10236
  }
8123
10237
  function validateConfig(config) {
8124
10238
  const errors = [];
10239
+ const warnings = [];
8125
10240
  if (!config || typeof config !== "object") {
8126
10241
  return {
8127
10242
  valid: false,
8128
- errors: ["Configuration must be an object"]
10243
+ errors: ["Configuration must be an object"],
10244
+ warnings
8129
10245
  };
8130
10246
  }
10247
+ for (const key of Object.keys(config)) {
10248
+ if (KNOWN_CONFIG_KEYS.includes(key)) continue;
10249
+ const suggestion = suggestConfigKey(key);
10250
+ warnings.push(
10251
+ `unknown key "${key}" \u2014 ignored` + (suggestion ? ` (did you mean "${suggestion}"?)` : "")
10252
+ );
10253
+ }
8131
10254
  if (!config.apiKey || typeof config.apiKey !== "string") {
8132
- errors.push("apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)");
10255
+ errors.push(
10256
+ "apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)"
10257
+ );
8133
10258
  } else if (!config.apiKey.startsWith("ak_")) {
8134
10259
  errors.push('apiKey must start with "ak_"');
8135
10260
  }
@@ -8191,7 +10316,8 @@ function validateConfig(config) {
8191
10316
  }
8192
10317
  return {
8193
10318
  valid: errors.length === 0,
8194
- errors
10319
+ errors,
10320
+ warnings
8195
10321
  };
8196
10322
  }
8197
10323
 
@@ -8237,19 +10363,76 @@ function formatMetadataWarnings(warnings) {
8237
10363
  );
8238
10364
  }
8239
10365
 
10366
+ // ../shared/dist/chunk-UEMUBXFH.mjs
10367
+ var DEFAULT_API_BASE_URL = "https://api.appilots.com";
10368
+ var APPILOTS_API_PATH_PREFIX = "/api/v1";
10369
+ function normalizeApiBaseUrl(configured) {
10370
+ const raw = (configured ?? DEFAULT_API_BASE_URL).trim().replace(/\/+$/, "");
10371
+ if (!raw) return `${DEFAULT_API_BASE_URL}${APPILOTS_API_PATH_PREFIX}`;
10372
+ if (new RegExp(`${APPILOTS_API_PATH_PREFIX}(/|$)`).test(raw)) return raw;
10373
+ return `${raw}${APPILOTS_API_PATH_PREFIX}`;
10374
+ }
10375
+
8240
10376
  // src/services/api-client.ts
8241
- var DEFAULT_TIMEOUT_MS = 3e4;
10377
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
8242
10378
  var DEFAULT_MAX_RETRIES = 2;
8243
10379
  var RETRY_BASE_DELAY_MS = 500;
10380
+ function describeNetworkError(url, err) {
10381
+ let origin = url;
10382
+ try {
10383
+ origin = new URL(url).origin;
10384
+ } catch {
10385
+ }
10386
+ const cause = err?.cause;
10387
+ const code = typeof cause?.code === "string" ? cause.code : void 0;
10388
+ const detail = code ?? stringifyUnknown(err);
10389
+ return new Error(`Cannot reach Appilots server at ${origin} (${detail})`);
10390
+ }
10391
+ function errorCodeOf(payload) {
10392
+ const error = payload?.error;
10393
+ if (error && typeof error === "object") {
10394
+ const { code } = error;
10395
+ if (typeof code === "string" && code.trim()) return code;
10396
+ }
10397
+ return void 0;
10398
+ }
10399
+ function describeApiError(payload, fallback) {
10400
+ const error = payload?.error;
10401
+ if (typeof error === "string" && error.trim()) return error;
10402
+ if (error && typeof error === "object") {
10403
+ const { code, message: message2 } = error;
10404
+ const text = typeof message2 === "string" && message2.trim() ? message2 : void 0;
10405
+ const tag = typeof code === "string" && code.trim() ? code : void 0;
10406
+ if (text && tag) return `${text} (${tag})`;
10407
+ if (text) return text;
10408
+ if (tag) return tag;
10409
+ }
10410
+ const message = payload?.message;
10411
+ if (typeof message === "string" && message.trim()) return message;
10412
+ return fallback;
10413
+ }
8244
10414
  var AppilotsAPIClient = class {
8245
- serverUrl;
10415
+ /**
10416
+ * `serverUrl` with the API's mount prefix resolved — every path below
10417
+ * is relative to THIS, not to the configured origin.
10418
+ *
10419
+ * It used to be the raw `serverUrl` with `/api/v1` hardcoded into each
10420
+ * path, which made the field mean something different here than in the
10421
+ * SDK. Both read the same `.appilotsrc`: the SDK completes the prefix
10422
+ * when it is missing, so `https://api.appilots.com/api/v1` is correct
10423
+ * there — and here that same value produced
10424
+ * `/api/v1/api/v1/cli/sync`, a 404 whose body reads `Route not found`.
10425
+ * Sharing `normalizeApiBaseUrl` is what makes one file mean one thing:
10426
+ * with or without the prefix now works in both.
10427
+ */
10428
+ baseUrl;
8246
10429
  apiKey;
8247
10430
  timeoutMs;
8248
10431
  maxRetries;
8249
10432
  constructor(config) {
8250
- this.serverUrl = config.serverUrl.replace(/\/$/, "");
10433
+ this.baseUrl = normalizeApiBaseUrl(config.serverUrl);
8251
10434
  this.apiKey = config.apiKey;
8252
- this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10435
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
8253
10436
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
8254
10437
  }
8255
10438
  /**
@@ -8273,7 +10456,7 @@ var AppilotsAPIClient = class {
8273
10456
  }
8274
10457
  return response;
8275
10458
  } catch (err) {
8276
- lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : err;
10459
+ lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : describeNetworkError(url, err);
8277
10460
  } finally {
8278
10461
  clearTimeout(timer);
8279
10462
  }
@@ -8292,7 +10475,7 @@ var AppilotsAPIClient = class {
8292
10475
  */
8293
10476
  async sync(content, version, appVersion) {
8294
10477
  try {
8295
- const response = await this.request(`${this.serverUrl}/api/v1/cli/sync`, {
10478
+ const response = await this.request(`${this.baseUrl}/cli/sync`, {
8296
10479
  method: "POST",
8297
10480
  headers: {
8298
10481
  "Content-Type": "application/json",
@@ -8306,7 +10489,9 @@ var AppilotsAPIClient = class {
8306
10489
  return {
8307
10490
  success: false,
8308
10491
  unchanged: false,
8309
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
10492
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`),
10493
+ errorCode: errorCodeOf(errorData),
10494
+ errorStatus: response.status
8310
10495
  };
8311
10496
  }
8312
10497
  const json = await response.json();
@@ -8319,7 +10504,8 @@ var AppilotsAPIClient = class {
8319
10504
  return {
8320
10505
  success: false,
8321
10506
  unchanged: false,
8322
- error: error instanceof Error ? error.message : "Failed to sync with Appilots API"
10507
+ error: error instanceof Error ? error.message : "Failed to sync with Appilots API",
10508
+ errorStatus: 0
8323
10509
  };
8324
10510
  }
8325
10511
  }
@@ -8330,7 +10516,7 @@ var AppilotsAPIClient = class {
8330
10516
  */
8331
10517
  async status() {
8332
10518
  try {
8333
- const response = await this.request(`${this.serverUrl}/api/v1/cli/status`, {
10519
+ const response = await this.request(`${this.baseUrl}/cli/status`, {
8334
10520
  method: "GET",
8335
10521
  headers: {
8336
10522
  Authorization: `Bearer ${this.apiKey}`
@@ -8339,7 +10525,7 @@ var AppilotsAPIClient = class {
8339
10525
  if (!response.ok) {
8340
10526
  const errorData = await response.json().catch(() => ({}));
8341
10527
  return {
8342
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
10528
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
8343
10529
  };
8344
10530
  }
8345
10531
  const json = await response.json();
@@ -8361,7 +10547,7 @@ var AppilotsAPIClient = class {
8361
10547
  */
8362
10548
  async evalRun(scenarios) {
8363
10549
  try {
8364
- const response = await this.request(`${this.serverUrl}/api/v1/cli/eval/run`, {
10550
+ const response = await this.request(`${this.baseUrl}/cli/eval/run`, {
8365
10551
  method: "POST",
8366
10552
  headers: {
8367
10553
  "Content-Type": "application/json",
@@ -8392,7 +10578,7 @@ var AppilotsAPIClient = class {
8392
10578
  */
8393
10579
  async health() {
8394
10580
  try {
8395
- const response = await this.request(`${this.serverUrl}/api/v1/health`, {
10581
+ const response = await this.request(`${this.baseUrl}/health`, {
8396
10582
  method: "GET"
8397
10583
  });
8398
10584
  return response.ok;
@@ -8402,15 +10588,23 @@ var AppilotsAPIClient = class {
8402
10588
  }
8403
10589
  };
8404
10590
 
10591
+ // src/version.ts
10592
+ var CLI_VERSION = "0.10.0";
10593
+
8405
10594
  exports.AppilotsAPIClient = AppilotsAPIClient;
10595
+ exports.CLI_VERSION = CLI_VERSION;
8406
10596
  exports.ComponentAnalyzer = ComponentAnalyzer;
8407
10597
  exports.DEFAULT_MANIFEST_FILENAME = DEFAULT_MANIFEST_FILENAME;
10598
+ exports.DEFAULT_WEB_SCREEN_PATTERNS = DEFAULT_WEB_SCREEN_PATTERNS;
8408
10599
  exports.FormAnalyzer = FormAnalyzer;
8409
10600
  exports.GenericPlatformAnalyzer = GenericPlatformAnalyzer;
8410
10601
  exports.MCPGenerator = MCPGenerator;
8411
10602
  exports.NavigationAnalyzer = NavigationAnalyzer;
8412
10603
  exports.ReactNativePlatformAnalyzer = ReactNativePlatformAnalyzer;
10604
+ exports.ReactWebPlatformAnalyzer = ReactWebPlatformAnalyzer;
8413
10605
  exports.ScreenAnalyzer = ScreenAnalyzer;
10606
+ exports.WebNavigationAnalyzer = WebNavigationAnalyzer;
10607
+ exports.WebScreenAnalyzer = WebScreenAnalyzer;
8414
10608
  exports.formatMetadataWarnings = formatMetadataWarnings;
8415
10609
  exports.getConfigPath = getConfigPath;
8416
10610
  exports.getEnvOverrides = getEnvOverrides;
@@ -8419,7 +10613,9 @@ exports.loadConfig = loadConfig;
8419
10613
  exports.loadManifest = loadManifest;
8420
10614
  exports.mergeManifestNavigation = mergeManifestNavigation;
8421
10615
  exports.mergeManifestScreens = mergeManifestScreens;
10616
+ exports.resolvePathToScreen = resolvePathToScreen;
8422
10617
  exports.saveConfig = saveConfig;
10618
+ exports.screenNameFromPath = screenNameFromPath;
8423
10619
  exports.validateConfig = validateConfig;
8424
10620
  //# sourceMappingURL=index.js.map
8425
10621
  //# sourceMappingURL=index.js.map