@appilots/cli 0.4.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -56,6 +56,45 @@ function hasJsxAttribute(element, name) {
56
56
  }
57
57
 
58
58
  // src/ast/jsx/classify.ts
59
+ var NON_INTERACTION_HANDLERS = /* @__PURE__ */ new Set([
60
+ "onLayout",
61
+ "onScroll",
62
+ "onScrollBeginDrag",
63
+ "onScrollEndDrag",
64
+ "onMomentumScrollBegin",
65
+ "onMomentumScrollEnd",
66
+ "onContentSizeChange",
67
+ "onEndReached",
68
+ "onViewableItemsChanged",
69
+ "onLoad",
70
+ "onLoadStart",
71
+ "onLoadEnd",
72
+ "onError",
73
+ "onProgress",
74
+ "onFocus",
75
+ "onBlur"
76
+ ]);
77
+ var VALUE_HANDLERS = /* @__PURE__ */ new Set([
78
+ "onChange",
79
+ "onChangeText",
80
+ "onValueChange",
81
+ "onSelect",
82
+ "onSelectionChange",
83
+ "onSubmitEditing"
84
+ ]);
85
+ function pressHandlerProp(element) {
86
+ let fallback;
87
+ for (const attr of element.attributes) {
88
+ if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
89
+ const name = attr.name.name;
90
+ if (!/^on[A-Z]/.test(name)) continue;
91
+ if (attr.value?.type !== "JSXExpressionContainer") continue;
92
+ if (name === "onPress") return name;
93
+ if (NON_INTERACTION_HANDLERS.has(name) || VALUE_HANDLERS.has(name)) continue;
94
+ fallback ??= name;
95
+ }
96
+ return fallback;
97
+ }
59
98
  var VIEW_COMPONENTS = /* @__PURE__ */ new Set(["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"]);
60
99
  var INPUT_COMPONENTS = /* @__PURE__ */ new Set(["TextInput", "Input", "HocInput"]);
61
100
  var BUTTON_COMPONENTS = /* @__PURE__ */ new Set(["TouchableOpacity", "Pressable", "Button"]);
@@ -70,17 +109,16 @@ function classifyJsxComponent(name, element) {
70
109
  if (INPUT_COMPONENTS.has(name)) return "input";
71
110
  if (BUTTON_COMPONENTS.has(name)) return "button";
72
111
  if (element) {
73
- const hasOptions = hasJsxAttribute(element, "options");
112
+ const hasOptions = hasJsxAttribute(element, "options") || hasJsxAttribute(element, "segments") || hasJsxAttribute(element, "choices") || hasJsxAttribute(element, "tabs");
74
113
  const hasValue = hasJsxAttribute(element, "value");
75
114
  const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
76
115
  const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange") || hasJsxAttribute(element, "onChangeText");
77
- const hasOnPress = hasJsxAttribute(element, "onPress");
78
116
  if (hasOptions && (hasValue || hasOnChange)) return "select";
79
117
  if (hasChecked && hasOnChange) return "toggle";
80
118
  if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
81
119
  return "input";
82
120
  }
83
- if (hasOnPress) return "button";
121
+ if (pressHandlerProp(element)) return "button";
84
122
  if (getStringAttr(element, "visible") || getExpressionIdentifierAttr(element, "visible")) {
85
123
  return "modal";
86
124
  }
@@ -418,12 +456,17 @@ var ScreenAnalyzer = class {
418
456
  }
419
457
  } catch (error) {
420
458
  if (this.verbose) {
421
- console.warn(`[ScreenAnalyzer] Failed to parse ${file}:`, error instanceof Error ? error.message : error);
459
+ console.warn(
460
+ `[ScreenAnalyzer] Failed to parse ${file}:`,
461
+ error instanceof Error ? error.message : error
462
+ );
422
463
  }
423
464
  }
424
465
  }
425
466
  if (this.verbose && this.strictScreens) {
426
- console.log(`[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`);
467
+ console.log(
468
+ `[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`
469
+ );
427
470
  }
428
471
  return screens;
429
472
  }
@@ -711,7 +754,11 @@ var ScreenAnalyzer = class {
711
754
  for (const form of secondary) {
712
755
  const existing = out.find((candidate) => candidate.id === form.id) ?? this.findFormWithSharedFields(out, form);
713
756
  if (!existing) {
714
- out.push({ ...form, fields: [...form.fields], submitAction: this.namedSubmitAction(form.submitAction) });
757
+ out.push({
758
+ ...form,
759
+ fields: [...form.fields],
760
+ submitAction: this.namedSubmitAction(form.submitAction)
761
+ });
715
762
  continue;
716
763
  }
717
764
  for (const field of form.fields) {
@@ -730,9 +777,12 @@ var ScreenAnalyzer = class {
730
777
  findEquivalentField(fields, incoming) {
731
778
  return fields.find((field) => {
732
779
  if (field.name && incoming.name && field.name === incoming.name) return true;
733
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
734
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
735
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
780
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
781
+ return true;
782
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
783
+ return true;
784
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
785
+ return true;
736
786
  if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
737
787
  return true;
738
788
  }
@@ -892,9 +942,11 @@ var ScreenAnalyzer = class {
892
942
  source: field.locator?.source ?? "accessibilityLabel"
893
943
  });
894
944
  } else if (attrName === "keyboardType") {
895
- if (attr.value.value === "email-address" || attr.value.value === "email") field.type = "email";
945
+ if (attr.value.value === "email-address" || attr.value.value === "email")
946
+ field.type = "email";
896
947
  if (attr.value.value === "phone-pad") field.type = "phone";
897
- if (attr.value.value === "numeric" || attr.value.value === "number-pad") field.type = "number";
948
+ if (attr.value.value === "numeric" || attr.value.value === "number-pad")
949
+ field.type = "number";
898
950
  }
899
951
  } else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
900
952
  if (attrName === "value" && BabelTypes.isIdentifier(attr.value.expression)) {
@@ -1060,8 +1112,10 @@ var ScreenAnalyzer = class {
1060
1112
  };
1061
1113
  if (handler.nativeConfirmationExpected) action.nativeConfirmationExpected = true;
1062
1114
  if (handler.targetScreen && !action.targetScreen) action.targetScreen = handler.targetScreen;
1063
- if (handler.successSignal && !action.successSignal) action.successSignal = handler.successSignal;
1064
- if (handler.failureSignal && !action.failureSignal) action.failureSignal = handler.failureSignal;
1115
+ if (handler.successSignal && !action.successSignal)
1116
+ action.successSignal = handler.successSignal;
1117
+ if (handler.failureSignal && !action.failureSignal)
1118
+ action.failureSignal = handler.failureSignal;
1065
1119
  if (handler.opensModal && !action.opensModal) action.opensModal = handler.opensModal;
1066
1120
  if (handler.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
1067
1121
  action.destructive = true;
@@ -1079,6 +1133,7 @@ var ScreenAnalyzer = class {
1079
1133
  if (!BabelTypes.isJSXIdentifier(element.name)) return;
1080
1134
  const componentName = element.name.name;
1081
1135
  if (classifyJsxComponent(componentName, element) !== "button") return;
1136
+ const pressProp = pressHandlerProp(element);
1082
1137
  let label;
1083
1138
  let handler;
1084
1139
  for (const attr of element.attributes) {
@@ -1088,7 +1143,7 @@ var ScreenAnalyzer = class {
1088
1143
  if ((attrName === "title" || attrName === "accessibilityLabel" || attrName === "label") && BabelTypes.isStringLiteral(attr.value)) {
1089
1144
  label = attr.value.value;
1090
1145
  }
1091
- if (attrName === "onPress" && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
1146
+ if (attrName === pressProp && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
1092
1147
  handler = attr.value.expression.name;
1093
1148
  }
1094
1149
  }
@@ -1154,10 +1209,13 @@ var ScreenAnalyzer = class {
1154
1209
  }
1155
1210
  };
1156
1211
  if (fn.body) {
1157
- traverse4(fn.body, {
1158
- noScope: true,
1159
- enter: (nodePath) => inspectNode(nodePath.node)
1160
- });
1212
+ traverse4(
1213
+ fn.body,
1214
+ {
1215
+ noScope: true,
1216
+ enter: (nodePath) => inspectNode(nodePath.node)
1217
+ }
1218
+ );
1161
1219
  }
1162
1220
  const expectedOutcome = targetScreen ? "navigation" : hasStateSetter ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
1163
1221
  return {
@@ -1215,6 +1273,7 @@ var ScreenAnalyzer = class {
1215
1273
  };
1216
1274
  const componentName = BabelTypes.isJSXIdentifier(element.name) ? element.name.name : void 0;
1217
1275
  action.sourceComponent = componentName;
1276
+ const pressProp = pressHandlerProp(element) ?? "onPress";
1218
1277
  let handlerName;
1219
1278
  for (const attr of element.attributes) {
1220
1279
  if (!BabelTypes.isJSXAttribute(attr)) continue;
@@ -1249,7 +1308,7 @@ var ScreenAnalyzer = class {
1249
1308
  }
1250
1309
  }
1251
1310
  } else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
1252
- if (attrName === "onPress" && BabelTypes.isIdentifier(attr.value.expression)) {
1311
+ if (attrName === pressProp && BabelTypes.isIdentifier(attr.value.expression)) {
1253
1312
  handlerName = attr.value.expression.name.toLowerCase();
1254
1313
  action.handler = attr.value.expression.name;
1255
1314
  if (handlerName.includes("submit")) {
@@ -1257,7 +1316,7 @@ var ScreenAnalyzer = class {
1257
1316
  } else if (handlerName.includes("navigate")) {
1258
1317
  action.type = "navigation";
1259
1318
  }
1260
- } else if (attrName === "onPress") {
1319
+ } else if (attrName === pressProp) {
1261
1320
  const inline = this.extractInlineOnPressMetadata(attr.value.expression);
1262
1321
  if (inline.handler) {
1263
1322
  handlerName = inline.handler.toLowerCase();
@@ -1281,7 +1340,11 @@ var ScreenAnalyzer = class {
1281
1340
  }
1282
1341
  }
1283
1342
  if (!action.locator && action.id) {
1284
- action.locator = { id: action.id, label: action.label, source: action.label ? "label" : "inferred" };
1343
+ action.locator = {
1344
+ id: action.id,
1345
+ label: action.label,
1346
+ source: action.label ? "label" : "inferred"
1347
+ };
1285
1348
  }
1286
1349
  if (this.isElementDestructive(element, handlerName, action.id)) {
1287
1350
  action.destructive = true;
@@ -1423,24 +1486,27 @@ var ScreenAnalyzer = class {
1423
1486
  }
1424
1487
  extractRowAction(fn) {
1425
1488
  let action;
1426
- traverse4(fn.body, {
1427
- noScope: true,
1428
- CallExpression: (nodePath) => {
1429
- const node = nodePath.node;
1430
- if (!BabelTypes.isMemberExpression(node.callee) || !BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !BabelTypes.isIdentifier(node.callee.property) || !["navigate", "push", "replace"].includes(node.callee.property.name)) {
1431
- return;
1489
+ traverse4(
1490
+ fn.body,
1491
+ {
1492
+ noScope: true,
1493
+ CallExpression: (nodePath) => {
1494
+ const node = nodePath.node;
1495
+ if (!BabelTypes.isMemberExpression(node.callee) || !BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !BabelTypes.isIdentifier(node.callee.property) || !["navigate", "push", "replace"].includes(node.callee.property.name)) {
1496
+ return;
1497
+ }
1498
+ const firstArg = node.arguments[0];
1499
+ if (!BabelTypes.isStringLiteral(firstArg)) return;
1500
+ const params = this.extractNavigationParams(node.arguments[1]);
1501
+ action = {
1502
+ type: "navigation",
1503
+ targetScreen: firstArg.value,
1504
+ ...Object.keys(params).length > 0 ? { params } : {},
1505
+ description: `Pressing a row opens ${firstArg.value}`
1506
+ };
1432
1507
  }
1433
- const firstArg = node.arguments[0];
1434
- if (!BabelTypes.isStringLiteral(firstArg)) return;
1435
- const params = this.extractNavigationParams(node.arguments[1]);
1436
- action = {
1437
- type: "navigation",
1438
- targetScreen: firstArg.value,
1439
- ...Object.keys(params).length > 0 ? { params } : {},
1440
- description: `Pressing a row opens ${firstArg.value}`
1441
- };
1442
1508
  }
1443
- });
1509
+ );
1444
1510
  return action;
1445
1511
  }
1446
1512
  extractNavigationParams(arg) {
@@ -1473,15 +1539,18 @@ var ScreenAnalyzer = class {
1473
1539
  } else if (BabelTypes.isIdentifier(firstParam)) {
1474
1540
  itemNames.add(firstParam.name);
1475
1541
  }
1476
- traverse4(fn.body, {
1477
- noScope: true,
1478
- MemberExpression: (nodePath) => {
1479
- const node = nodePath.node;
1480
- if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
1481
- fields.add(node.property.name);
1542
+ traverse4(
1543
+ fn.body,
1544
+ {
1545
+ noScope: true,
1546
+ MemberExpression: (nodePath) => {
1547
+ const node = nodePath.node;
1548
+ if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
1549
+ fields.add(node.property.name);
1550
+ }
1482
1551
  }
1483
1552
  }
1484
- });
1553
+ );
1485
1554
  return Array.from(fields).sort();
1486
1555
  }
1487
1556
  inferItemType(dataSource, renderItem, displayFields) {
@@ -1493,11 +1562,19 @@ var ScreenAnalyzer = class {
1493
1562
  if (displayFields.length > 0) return "Item";
1494
1563
  return void 0;
1495
1564
  }
1565
+ /**
1566
+ * Identity comes from how a field is NAMED, not from what the app
1567
+ * sells: `id`/`uuid`/`key`/`slug` are conventions any codebase uses,
1568
+ * `name`/`title`/`email` are how any row introduces itself. `plate`
1569
+ * used to sit in this list and read like one of them — but it is the
1570
+ * example app's schema, and no other tenant ever got its equivalent
1571
+ * (`mrn`, `trackingNumber`, `sku`) added here.
1572
+ */
1496
1573
  inferIdentityFields(keyField, displayFields) {
1497
1574
  const out = /* @__PURE__ */ new Set();
1498
1575
  if (keyField) out.add(keyField);
1499
1576
  for (const field of displayFields) {
1500
- if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
1577
+ if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
1501
1578
  }
1502
1579
  return Array.from(out);
1503
1580
  }
@@ -1508,17 +1585,23 @@ var ScreenAnalyzer = class {
1508
1585
  CallExpression: (nodePath) => {
1509
1586
  const node = nodePath.node;
1510
1587
  if (!BabelTypes.isMemberExpression(node.callee)) return;
1511
- if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter") return;
1512
- if (!BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource) return;
1588
+ if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
1589
+ return;
1590
+ if (!BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource)
1591
+ return;
1513
1592
  const fn = node.arguments[0];
1514
- if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn)) return;
1515
- traverse4(fn.body, {
1516
- noScope: true,
1517
- Identifier: (innerPath) => {
1518
- const name = innerPath.node.name;
1519
- if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
1593
+ if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
1594
+ return;
1595
+ traverse4(
1596
+ fn.body,
1597
+ {
1598
+ noScope: true,
1599
+ Identifier: (innerPath) => {
1600
+ const name = innerPath.node.name;
1601
+ if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
1602
+ }
1520
1603
  }
1521
- });
1604
+ );
1522
1605
  }
1523
1606
  });
1524
1607
  return queryBinding;
@@ -1662,11 +1745,7 @@ var NavigationAnalyzer = class {
1662
1745
  if (BabelTypes.isJSXAttribute(initialRouteAttr) && BabelTypes.isStringLiteral(initialRouteAttr.value)) {
1663
1746
  navigator.initialRouteName = initialRouteAttr.value.value;
1664
1747
  }
1665
- const screens = this.extractScreensFromNavigator(
1666
- node,
1667
- objectName,
1668
- navigator.type
1669
- );
1748
+ const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
1670
1749
  screensByNavigator.set(objectName, screens);
1671
1750
  }
1672
1751
  }
@@ -1683,27 +1762,112 @@ var NavigationAnalyzer = class {
1683
1762
  }
1684
1763
  return navigators;
1685
1764
  }
1765
+ /**
1766
+ * Is this JSX element `<navigatorVarName.MEMBER …>`?
1767
+ */
1768
+ // Deliberately NOT a type predicate (`node is t.JSXElement`): a false
1769
+ // result would then narrow `node` to "not a JSXElement", and the very
1770
+ // next check — the same node against a different member — would see
1771
+ // `never`. It is a question about the member name, not about the node
1772
+ // kind.
1773
+ isNavigatorMember(node, navigatorVarName, member) {
1774
+ if (!BabelTypes.isJSXElement(node)) return false;
1775
+ const name = node.openingElement.name;
1776
+ return BabelTypes.isJSXMemberExpression(name) && BabelTypes.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && BabelTypes.isJSXIdentifier(name.property) && name.property.name === member;
1777
+ }
1778
+ /**
1779
+ * Flatten a navigator's JSX children into the `<X.Screen>` elements they
1780
+ * contain, unwrapping every container a real app puts in between.
1781
+ *
1782
+ * The old version compared `t.isJSXElement(child)` against the direct
1783
+ * children only. A ternary is a `JSXExpressionContainer`, so an
1784
+ * auth-gated root — the modal shape of a commercial app, and the shape
1785
+ * of this repo's own `apps/example-app` — contributed ZERO screens to
1786
+ * the graph while `appilots sync` reported success (#396).
1787
+ *
1788
+ * Both branches of a conditional are collected on purpose. The graph is
1789
+ * a design-time map of what routes EXIST, not a prediction of which one
1790
+ * a given session will render; the agent needs the destination name to
1791
+ * be there whether or not the user happens to be logged in right now.
1792
+ */
1793
+ collectScreenElements(children, navigatorVarName, depth = 0) {
1794
+ if (depth > 12) return [];
1795
+ const found = [];
1796
+ const visit = (node) => {
1797
+ if (!node) return;
1798
+ if (this.isNavigatorMember(node, navigatorVarName, "Screen")) {
1799
+ found.push(node);
1800
+ return;
1801
+ }
1802
+ if (this.isNavigatorMember(node, navigatorVarName, "Group")) {
1803
+ const group = node;
1804
+ found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
1805
+ return;
1806
+ }
1807
+ if (BabelTypes.isJSXElement(node)) {
1808
+ const name = node.openingElement.name;
1809
+ const isForeignNavigator = BabelTypes.isJSXMemberExpression(name) && BabelTypes.isJSXIdentifier(name.property) && name.property.name === "Navigator";
1810
+ if (isForeignNavigator) return;
1811
+ found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
1812
+ return;
1813
+ }
1814
+ if (BabelTypes.isJSXFragment(node)) {
1815
+ found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
1816
+ return;
1817
+ }
1818
+ if (BabelTypes.isJSXExpressionContainer(node)) {
1819
+ visit(node.expression);
1820
+ return;
1821
+ }
1822
+ if (BabelTypes.isConditionalExpression(node)) {
1823
+ visit(node.consequent);
1824
+ visit(node.alternate);
1825
+ return;
1826
+ }
1827
+ if (BabelTypes.isLogicalExpression(node)) {
1828
+ visit(node.left);
1829
+ visit(node.right);
1830
+ return;
1831
+ }
1832
+ if (BabelTypes.isCallExpression(node)) {
1833
+ for (const arg of node.arguments) {
1834
+ if (BabelTypes.isArrowFunctionExpression(arg) || BabelTypes.isFunctionExpression(arg)) {
1835
+ if (BabelTypes.isBlockStatement(arg.body)) {
1836
+ for (const stmt of arg.body.body) {
1837
+ if (BabelTypes.isReturnStatement(stmt)) visit(stmt.argument);
1838
+ }
1839
+ } else {
1840
+ visit(arg.body);
1841
+ }
1842
+ }
1843
+ }
1844
+ return;
1845
+ }
1846
+ if (BabelTypes.isArrayExpression(node)) {
1847
+ for (const el of node.elements) visit(el);
1848
+ return;
1849
+ }
1850
+ if (BabelTypes.isTSAsExpression(node) || BabelTypes.isTSNonNullExpression(node)) {
1851
+ visit(node.expression);
1852
+ }
1853
+ };
1854
+ for (const child of children) visit(child);
1855
+ return found;
1856
+ }
1686
1857
  /** Extract screens from a navigator JSX element */
1687
1858
  extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
1688
1859
  const screens = [];
1689
1860
  if (!navigatorElement.children) return screens;
1690
- for (const child of navigatorElement.children) {
1691
- if (BabelTypes.isJSXElement(child) && BabelTypes.isJSXMemberExpression(child.openingElement.name)) {
1692
- const memberExpr = child.openingElement.name;
1693
- if (BabelTypes.isJSXIdentifier(memberExpr.object) && memberExpr.object.name === navigatorVarName && BabelTypes.isJSXIdentifier(memberExpr.property) && memberExpr.property.name === "Screen") {
1694
- const screenName = this.extractAttributeValue(
1695
- child.openingElement.attributes,
1696
- "name"
1697
- );
1698
- if (screenName) {
1699
- screens.push({
1700
- name: screenName,
1701
- navigatorName: navigatorVarName,
1702
- navigatorType
1703
- });
1704
- }
1705
- }
1706
- }
1861
+ const seen = /* @__PURE__ */ new Set();
1862
+ for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
1863
+ const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
1864
+ if (!screenName || seen.has(screenName)) continue;
1865
+ seen.add(screenName);
1866
+ screens.push({
1867
+ name: screenName,
1868
+ navigatorName: navigatorVarName,
1869
+ navigatorType
1870
+ });
1707
1871
  }
1708
1872
  return screens;
1709
1873
  }
@@ -1762,9 +1926,7 @@ var NavigationAnalyzer = class {
1762
1926
  if (member.type === "TSPropertySignature" && member.key) {
1763
1927
  const keyName = member.key.type === "Identifier" ? member.key.name : null;
1764
1928
  if (keyName && member.typeAnnotation) {
1765
- const params = this.extractParamsFromType(
1766
- member.typeAnnotation.typeAnnotation
1767
- );
1929
+ const params = this.extractParamsFromType(member.typeAnnotation.typeAnnotation);
1768
1930
  entries.set(keyName, params);
1769
1931
  }
1770
1932
  }
@@ -1843,6 +2005,11 @@ var NavigationAnalyzer = class {
1843
2005
  let initialScreen = "";
1844
2006
  for (const navigator of parsedNavigators) {
1845
2007
  const screenNames = navigator.screens.map((s) => s.name);
2008
+ if (screenNames.length === 0) {
2009
+ console.warn(
2010
+ `[MCPGenerator] navigator "${navigator.name}" (${navigator.type}) declared no reachable screens. Its routes will be missing from the app map, so the agent will have no destination name for them. If the screens are there in the source, this is a parser gap \u2014 please report it with the file.`
2011
+ );
2012
+ }
1846
2013
  navigators.push({
1847
2014
  name: navigator.name,
1848
2015
  type: navigator.type,
@@ -2630,7 +2797,9 @@ var WebScreenAnalyzer = class {
2630
2797
  include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
2631
2798
  exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
2632
2799
  } = this.config;
2633
- const files = await glob(include, { cwd: this.config.rootDir, ignore: exclude });
2800
+ const files = (await glob(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
2801
+ (file) => !file.endsWith(".d.ts")
2802
+ );
2634
2803
  const patternMatches = await glob(this.screenPatterns, {
2635
2804
  cwd: this.config.rootDir,
2636
2805
  ignore: exclude
@@ -3001,7 +3170,8 @@ var WebScreenAnalyzer = class {
3001
3170
  if (type === "email") return "email";
3002
3171
  if (type === "tel") return "phone";
3003
3172
  if (type === "number") return "number";
3004
- if (type === "date" || type === "datetime-local" || type === "month" || type === "week") return "date";
3173
+ if (type === "date" || type === "datetime-local" || type === "month" || type === "week")
3174
+ return "date";
3005
3175
  const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
3006
3176
  if (inputMode === "email") return "email";
3007
3177
  if (inputMode === "tel") return "phone";
@@ -3315,7 +3485,9 @@ var WebScreenAnalyzer = class {
3315
3485
  }
3316
3486
  const callback = expr.arguments[0];
3317
3487
  if (!BabelTypes.isArrowFunctionExpression(callback) && !BabelTypes.isFunctionExpression(callback)) return;
3318
- const enclosing = nodePath.findParent((p) => p.isJSXElement());
3488
+ const enclosing = nodePath.findParent(
3489
+ (p) => p.isJSXElement()
3490
+ );
3319
3491
  const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
3320
3492
  const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
3321
3493
  const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
@@ -3417,16 +3589,19 @@ function firstCalledFunctionName(node) {
3417
3589
  }
3418
3590
  function containsWindowConfirm(body) {
3419
3591
  let found = false;
3420
- traverse4(body, {
3421
- noScope: true,
3422
- CallExpression: (nodePath) => {
3423
- const callee = nodePath.node.callee;
3424
- if (BabelTypes.isIdentifier(callee) && callee.name === "confirm") found = true;
3425
- if (BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.object) && callee.object.name === "window" && BabelTypes.isIdentifier(callee.property) && callee.property.name === "confirm") {
3426
- found = true;
3592
+ traverse4(
3593
+ body,
3594
+ {
3595
+ noScope: true,
3596
+ CallExpression: (nodePath) => {
3597
+ const callee = nodePath.node.callee;
3598
+ if (BabelTypes.isIdentifier(callee) && callee.name === "confirm") found = true;
3599
+ if (BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.object) && callee.object.name === "window" && BabelTypes.isIdentifier(callee.property) && callee.property.name === "confirm") {
3600
+ found = true;
3601
+ }
3427
3602
  }
3428
3603
  }
3429
- });
3604
+ );
3430
3605
  return found;
3431
3606
  }
3432
3607
  function mergeLocator(current, next) {
@@ -3466,32 +3641,38 @@ function collectionItemNames(callback) {
3466
3641
  function collectionDisplayFields(callback, itemNames) {
3467
3642
  const fields = /* @__PURE__ */ new Set();
3468
3643
  if (!callback.body) return [];
3469
- traverse4(callback.body, {
3470
- noScope: true,
3471
- MemberExpression: (nodePath) => {
3472
- const node = nodePath.node;
3473
- if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
3474
- fields.add(node.property.name);
3644
+ traverse4(
3645
+ callback.body,
3646
+ {
3647
+ noScope: true,
3648
+ MemberExpression: (nodePath) => {
3649
+ const node = nodePath.node;
3650
+ if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
3651
+ fields.add(node.property.name);
3652
+ }
3475
3653
  }
3476
3654
  }
3477
- });
3655
+ );
3478
3656
  return Array.from(fields).sort();
3479
3657
  }
3480
3658
  function collectionKeyField(callback, itemNames) {
3481
3659
  let keyField;
3482
3660
  if (!callback.body) return void 0;
3483
- traverse4(callback.body, {
3484
- noScope: true,
3485
- JSXAttribute: (nodePath) => {
3486
- const attr = nodePath.node;
3487
- if (!BabelTypes.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
3488
- if (!attr.value || !BabelTypes.isJSXExpressionContainer(attr.value)) return;
3489
- const expr = attr.value.expression;
3490
- if (BabelTypes.isMemberExpression(expr) && BabelTypes.isIdentifier(expr.object) && itemNames.has(expr.object.name) && BabelTypes.isIdentifier(expr.property)) {
3491
- keyField = keyField ?? expr.property.name;
3661
+ traverse4(
3662
+ callback.body,
3663
+ {
3664
+ noScope: true,
3665
+ JSXAttribute: (nodePath) => {
3666
+ const attr = nodePath.node;
3667
+ if (!BabelTypes.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
3668
+ if (!attr.value || !BabelTypes.isJSXExpressionContainer(attr.value)) return;
3669
+ const expr = attr.value.expression;
3670
+ if (BabelTypes.isMemberExpression(expr) && BabelTypes.isIdentifier(expr.object) && itemNames.has(expr.object.name) && BabelTypes.isIdentifier(expr.property)) {
3671
+ keyField = keyField ?? expr.property.name;
3672
+ }
3492
3673
  }
3493
3674
  }
3494
- });
3675
+ );
3495
3676
  return keyField;
3496
3677
  }
3497
3678
  function collectionRowAction(callback) {
@@ -3539,7 +3720,7 @@ function inferIdentityFields(keyField, displayFields) {
3539
3720
  const out = /* @__PURE__ */ new Set();
3540
3721
  if (keyField) out.add(keyField);
3541
3722
  for (const field of displayFields) {
3542
- if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
3723
+ if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
3543
3724
  }
3544
3725
  return Array.from(out);
3545
3726
  }
@@ -7923,7 +8104,7 @@ var coerce = {
7923
8104
  };
7924
8105
  var NEVER = INVALID;
7925
8106
 
7926
- // ../shared/dist/chunk-MSOZJNXB.mjs
8107
+ // ../shared/dist/chunk-PKS3VDNB.mjs
7927
8108
  var locatorSourceSchema = external_exports.enum([
7928
8109
  "appilotsId",
7929
8110
  "testID",
@@ -7949,7 +8130,16 @@ var locatorDescriptorSchema = external_exports.object({
7949
8130
  source: locatorSourceSchema.optional()
7950
8131
  }).passthrough();
7951
8132
  var signalDescriptorSchema = external_exports.object({
7952
- type: external_exports.enum(["navigation", "goBack", "toast", "modal", "inline-error", "data-arrival", "loading", "none"]).or(external_exports.string().min(1).max(60)),
8133
+ type: external_exports.enum([
8134
+ "navigation",
8135
+ "goBack",
8136
+ "toast",
8137
+ "modal",
8138
+ "inline-error",
8139
+ "data-arrival",
8140
+ "loading",
8141
+ "none"
8142
+ ]).or(external_exports.string().min(1).max(60)),
7953
8143
  target: external_exports.string().optional(),
7954
8144
  description: external_exports.string().optional()
7955
8145
  }).passthrough();
@@ -7963,7 +8153,20 @@ var waitPolicyDescriptorSchema = external_exports.object({
7963
8153
  signals: external_exports.array(signalDescriptorSchema).optional(),
7964
8154
  maxMs: external_exports.number().finite().optional()
7965
8155
  }).passthrough();
7966
- var targetDescriptorRoleSchema = external_exports.enum(["button", "submit", "input", "toggle", "select", "date", "list", "row", "menuItem", "modal", "custom"]).or(external_exports.string().min(1).max(60));
8156
+ var targetDescriptorRoleSchema = external_exports.enum([
8157
+ "button",
8158
+ "submit",
8159
+ "input",
8160
+ "toggle",
8161
+ "select",
8162
+ "date",
8163
+ "list",
8164
+ "row",
8165
+ "menuItem",
8166
+ "modal",
8167
+ "custom"
8168
+ ]).or(external_exports.string().min(1).max(60));
8169
+ var targetOriginSchema = external_exports.enum(["analyzer", "manifest"]).or(external_exports.string().min(1).max(40));
7967
8170
  var targetDescriptorSchema = external_exports.object({
7968
8171
  id: external_exports.string(),
7969
8172
  role: targetDescriptorRoleSchema,
@@ -7977,10 +8180,20 @@ var targetDescriptorSchema = external_exports.object({
7977
8180
  requiresConfirmation: external_exports.boolean().optional(),
7978
8181
  opensModal: external_exports.string().optional(),
7979
8182
  opensBottomSheet: external_exports.string().optional(),
7980
- sourceComponent: external_exports.string().optional()
8183
+ sourceComponent: external_exports.string().optional(),
8184
+ origin: targetOriginSchema.optional()
7981
8185
  }).passthrough();
7982
8186
  var flowStepDescriptorSchema = external_exports.object({
7983
- type: external_exports.enum(["navigate", "fill", "press", "select", "toggle", "wait", "confirm", "choose-list-item"]).or(external_exports.string().min(1).max(60)),
8187
+ type: external_exports.enum([
8188
+ "navigate",
8189
+ "fill",
8190
+ "press",
8191
+ "select",
8192
+ "toggle",
8193
+ "wait",
8194
+ "confirm",
8195
+ "choose-list-item"
8196
+ ]).or(external_exports.string().min(1).max(60)),
7984
8197
  target: external_exports.string().optional(),
7985
8198
  label: external_exports.string().optional(),
7986
8199
  description: external_exports.string().optional(),
@@ -8202,6 +8415,15 @@ external_exports.object({
8202
8415
  password: external_exports.string().min(1, "Password is required"),
8203
8416
  code: totpCodeSchema
8204
8417
  });
8418
+ external_exports.object({
8419
+ password: external_exports.string().min(1, "Password is required"),
8420
+ code: totpCodeSchema.optional(),
8421
+ recoveryCode: recoveryCodeSchema.optional(),
8422
+ confirm: external_exports.literal("DELETE MY ACCOUNT")
8423
+ }).refine((v) => !(v.code && v.recoveryCode), {
8424
+ message: "Provide either a TOTP code or a recovery code, not both",
8425
+ path: ["code"]
8426
+ });
8205
8427
  var createProjectSchema = external_exports.object({
8206
8428
  name: external_exports.string().min(1).max(100),
8207
8429
  description: external_exports.string().max(500).optional(),
@@ -8237,6 +8459,7 @@ external_exports.object({
8237
8459
  content: external_exports.record(external_exports.unknown())
8238
8460
  });
8239
8461
  var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
8462
+ var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
8240
8463
  external_exports.object({
8241
8464
  name: external_exports.string().min(1).max(100),
8242
8465
  // Required for `sdk` (default), forbidden/ignored for `operator` —
@@ -8244,6 +8467,7 @@ external_exports.object({
8244
8467
  // missing scope still validates as the pre-existing SDK shape.
8245
8468
  projectId: external_exports.string().min(1).optional(),
8246
8469
  scope: apiKeyScopeSchema.default("sdk"),
8470
+ environment: apiKeyEnvironmentSchema.optional(),
8247
8471
  expiresAt: external_exports.string().datetime().optional()
8248
8472
  }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
8249
8473
  message: 'projectId is required for scope "sdk"',
@@ -8251,13 +8475,23 @@ external_exports.object({
8251
8475
  });
8252
8476
  var boundedString = (max) => external_exports.string().max(max);
8253
8477
  var clientPlatformSchema = external_exports.enum(["react-native", "web", "android", "ios"]).or(external_exports.string().min(1).max(40));
8478
+ var identityProvenanceSchema = external_exports.enum(["declared", "derived", "positional"]).or(external_exports.string().min(1).max(40));
8254
8479
  var snapshotInputSchema = external_exports.object({
8255
8480
  id: boundedString(160).optional(),
8481
+ provenance: identityProvenanceSchema.optional(),
8482
+ /**
8483
+ * False when the control is mounted but currently OUTSIDE the window —
8484
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8485
+ * cannot tell (old React Native architecture, or any SDK published
8486
+ * before the field existed), which must never be read as `false`.
8487
+ */
8488
+ onScreen: external_exports.boolean().optional(),
8256
8489
  label: boundedString(300).optional(),
8257
8490
  value: boundedString(4096).optional(),
8258
8491
  placeholder: boundedString(300).optional(),
8259
8492
  editable: external_exports.boolean().optional(),
8260
8493
  secure: external_exports.boolean().optional(),
8494
+ submitsOnReturn: external_exports.boolean().optional(),
8261
8495
  type: boundedString(40).optional(),
8262
8496
  required: external_exports.boolean().optional(),
8263
8497
  invalid: external_exports.boolean().optional(),
@@ -8265,6 +8499,14 @@ var snapshotInputSchema = external_exports.object({
8265
8499
  }).passthrough();
8266
8500
  var snapshotButtonSchema = external_exports.object({
8267
8501
  id: boundedString(160).optional(),
8502
+ provenance: identityProvenanceSchema.optional(),
8503
+ /**
8504
+ * False when the control is mounted but currently OUTSIDE the window —
8505
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8506
+ * cannot tell (old React Native architecture, or any SDK published
8507
+ * before the field existed), which must never be read as `false`.
8508
+ */
8509
+ onScreen: external_exports.boolean().optional(),
8268
8510
  label: boundedString(300).optional(),
8269
8511
  disabled: external_exports.boolean().optional(),
8270
8512
  inModal: external_exports.boolean().optional(),
@@ -8274,12 +8516,28 @@ var snapshotButtonSchema = external_exports.object({
8274
8516
  }).passthrough();
8275
8517
  var snapshotToggleSchema = external_exports.object({
8276
8518
  id: boundedString(160).optional(),
8519
+ provenance: identityProvenanceSchema.optional(),
8520
+ /**
8521
+ * False when the control is mounted but currently OUTSIDE the window —
8522
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8523
+ * cannot tell (old React Native architecture, or any SDK published
8524
+ * before the field existed), which must never be read as `false`.
8525
+ */
8526
+ onScreen: external_exports.boolean().optional(),
8277
8527
  label: boundedString(300).optional(),
8278
8528
  value: external_exports.boolean().optional(),
8279
8529
  inModal: external_exports.boolean().optional()
8280
8530
  }).passthrough();
8281
8531
  var snapshotSliderSchema = external_exports.object({
8282
8532
  id: boundedString(160).optional(),
8533
+ provenance: identityProvenanceSchema.optional(),
8534
+ /**
8535
+ * False when the control is mounted but currently OUTSIDE the window —
8536
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8537
+ * cannot tell (old React Native architecture, or any SDK published
8538
+ * before the field existed), which must never be read as `false`.
8539
+ */
8540
+ onScreen: external_exports.boolean().optional(),
8283
8541
  label: boundedString(300).optional(),
8284
8542
  value: external_exports.number().finite().optional(),
8285
8543
  min: external_exports.number().finite().optional(),
@@ -8320,6 +8578,26 @@ var snapshotListSchema = external_exports.object({
8320
8578
  // should chase, and the relay only reads a known subset.
8321
8579
  items: external_exports.array(external_exports.record(external_exports.unknown())).max(500).optional()
8322
8580
  }).passthrough();
8581
+ var snapshotNativeDialogSchema = external_exports.object({
8582
+ id: boundedString(160),
8583
+ title: boundedString(300).optional(),
8584
+ message: boundedString(2e3).optional(),
8585
+ buttons: external_exports.array(
8586
+ external_exports.object({
8587
+ label: boundedString(160),
8588
+ style: external_exports.enum(["default", "cancel", "destructive"]).optional()
8589
+ }).passthrough()
8590
+ ).max(10)
8591
+ }).passthrough();
8592
+ var snapshotScrollableSchema = external_exports.object({
8593
+ id: boundedString(160),
8594
+ containerType: boundedString(60).optional(),
8595
+ label: boundedString(300).optional(),
8596
+ scrollOffsetY: external_exports.number().finite().optional(),
8597
+ canScrollUp: external_exports.boolean().optional(),
8598
+ canScrollDown: external_exports.boolean().optional(),
8599
+ horizontal: external_exports.boolean().optional()
8600
+ }).passthrough();
8323
8601
  var snapshotChoiceGroupSchema = external_exports.object({
8324
8602
  index: external_exports.number().int().optional(),
8325
8603
  id: boundedString(160).optional(),
@@ -8336,6 +8614,14 @@ var snapshotElementSchema = external_exports.object({
8336
8614
  disabled: external_exports.boolean().optional(),
8337
8615
  selected: external_exports.boolean().optional(),
8338
8616
  source: boundedString(40).optional(),
8617
+ provenance: identityProvenanceSchema.optional(),
8618
+ /**
8619
+ * False when the control is mounted but currently OUTSIDE the window —
8620
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8621
+ * cannot tell (old React Native architecture, or any SDK published
8622
+ * before the field existed), which must never be read as `false`.
8623
+ */
8624
+ onScreen: external_exports.boolean().optional(),
8339
8625
  targetId: boundedString(160).optional(),
8340
8626
  listContext: external_exports.record(external_exports.unknown()).optional(),
8341
8627
  inModal: external_exports.boolean().optional()
@@ -8352,7 +8638,34 @@ var agentSnapshotSchema = external_exports.object({
8352
8638
  modalOpen: external_exports.boolean().optional(),
8353
8639
  lists: external_exports.array(snapshotListSchema).max(100).optional(),
8354
8640
  choiceGroups: external_exports.array(snapshotChoiceGroupSchema).max(100).optional(),
8355
- elements: external_exports.array(snapshotElementSchema).max(1e3).optional()
8641
+ scrollables: external_exports.array(snapshotScrollableSchema).max(20).optional(),
8642
+ nativeDialog: snapshotNativeDialogSchema.optional(),
8643
+ elements: external_exports.array(snapshotElementSchema).max(1e3).optional(),
8644
+ /**
8645
+ * The client clamped this observation to the caps above and lost
8646
+ * data doing it (`clampSnapshotToWireLimits` in
8647
+ * `@appilots/client-core`). Read by the prompt serializer so the
8648
+ * model is told the view is partial — a screen truncated at 500
8649
+ * texts must not be read as a screen that only has 500 things on
8650
+ * it. Optional: older SDKs never clamp and never send it.
8651
+ */
8652
+ truncated: external_exports.boolean().optional(),
8653
+ /**
8654
+ * How many of this screen's controls the client could name, split by
8655
+ * `identityProvenance`. Diagnostic only — the relay never grounds an
8656
+ * action on it — but it is the number that says whether an app needs
8657
+ * annotating, and comparing it between a debug and a release build is
8658
+ * the first direct measurement of what minification costs the agent.
8659
+ */
8660
+ stats: external_exports.object({
8661
+ visitedFibers: external_exports.number().int().nonnegative().optional(),
8662
+ skippedHidden: external_exports.number().int().nonnegative().optional(),
8663
+ identity: external_exports.object({
8664
+ declared: external_exports.number().int().nonnegative().optional(),
8665
+ derived: external_exports.number().int().nonnegative().optional(),
8666
+ positional: external_exports.number().int().nonnegative().optional()
8667
+ }).passthrough().optional()
8668
+ }).passthrough().optional()
8356
8669
  }).passthrough();
8357
8670
  var agentContextSchema = external_exports.object({
8358
8671
  /**
@@ -8402,6 +8715,19 @@ var agentContextSchema = external_exports.object({
8402
8715
  ).max(200).optional(),
8403
8716
  loadingPending: external_exports.boolean().optional()
8404
8717
  }).passthrough();
8718
+ var introspectionFailureReasonSchema = external_exports.enum([
8719
+ "sentinel-missing-internals",
8720
+ "never-mounted",
8721
+ "render-crashed",
8722
+ "names-mangled",
8723
+ "walk-recognized-nothing"
8724
+ ]);
8725
+ var introspectionDiagnosticsSchema = external_exports.object({
8726
+ captured: external_exports.boolean(),
8727
+ failureReason: introspectionFailureReasonSchema.nullish(),
8728
+ reactVersion: boundedString(32).nullish(),
8729
+ failureCount: external_exports.number().int().min(0).max(1e6).optional()
8730
+ });
8405
8731
  external_exports.object({
8406
8732
  content: external_exports.string().min(1).max(4096),
8407
8733
  sessionId: external_exports.string().optional(),
@@ -8411,7 +8737,20 @@ external_exports.object({
8411
8737
  * the active MCP doc and emits `mcp_version_mismatch` telemetry when
8412
8738
  * they differ. Optional — older SDKs simply skip the check.
8413
8739
  */
8414
- mcpVersion: external_exports.string().max(64).optional()
8740
+ mcpVersion: external_exports.string().max(64).optional(),
8741
+ /**
8742
+ * SDK-side introspection health. Only sent when it FAILED — a client
8743
+ * that reached the React tree omits the field entirely, so the happy
8744
+ * path costs nothing on the wire.
8745
+ *
8746
+ * Without this, an app whose introspection is dead sends a snapshot
8747
+ * that is empty but perfectly well-formed, indistinguishable from a
8748
+ * genuinely empty screen. The server emits
8749
+ * `introspection_unavailable` so the failure is visible to us instead
8750
+ * of waiting for a bug report. Optional — older SDKs simply never
8751
+ * send it.
8752
+ */
8753
+ introspection: introspectionDiagnosticsSchema.optional()
8415
8754
  });
8416
8755
  var agentActionTypeSchema = external_exports.enum([
8417
8756
  "navigate",
@@ -8422,11 +8761,22 @@ var agentActionTypeSchema = external_exports.enum([
8422
8761
  "custom"
8423
8762
  ]);
8424
8763
  var navigationPayloadSchema = external_exports.object({
8425
- screenName: external_exports.string().min(1),
8764
+ // Optional ONLY so `goBack` can exist (#398): it returns to whatever
8765
+ // is beneath on the stack and has no destination to name. Every other
8766
+ // action is still required to carry one — enforced by the refine
8767
+ // below rather than by the field, so the error names the real problem
8768
+ // ("navigate needs a screenName") instead of a missing key.
8769
+ screenName: external_exports.string().min(1).optional(),
8426
8770
  params: external_exports.record(external_exports.unknown()).optional(),
8427
8771
  navigationAction: external_exports.enum(["push", "navigate", "replace", "goBack", "reset"]).default("navigate").optional(),
8428
8772
  path: external_exports.array(external_exports.string().min(1)).optional()
8429
- }).passthrough();
8773
+ }).passthrough().refine(
8774
+ (payload) => payload.navigationAction === "goBack" || typeof payload.screenName === "string" && payload.screenName.length > 0,
8775
+ {
8776
+ message: 'screenName is required for every navigationAction except "goBack"',
8777
+ path: ["screenName"]
8778
+ }
8779
+ );
8430
8780
  var formFillFieldSchema = external_exports.object({
8431
8781
  fieldId: external_exports.string().min(1),
8432
8782
  fieldType: external_exports.enum(["text", "select", "toggle", "date", "number", "custom"]).default("text").optional(),
@@ -8760,6 +9110,8 @@ var sandboxObservationSchema = external_exports.object({
8760
9110
  inputs: external_exports.array(
8761
9111
  external_exports.object({
8762
9112
  id: external_exports.string().max(120),
9113
+ provenance: identityProvenanceSchema.optional(),
9114
+ onScreen: external_exports.boolean().optional(),
8763
9115
  label: external_exports.string().max(200).optional(),
8764
9116
  value: external_exports.string().max(2e3).optional(),
8765
9117
  disabled: external_exports.boolean().optional(),
@@ -8773,6 +9125,8 @@ var sandboxObservationSchema = external_exports.object({
8773
9125
  buttons: external_exports.array(
8774
9126
  external_exports.object({
8775
9127
  id: external_exports.string().max(120),
9128
+ provenance: identityProvenanceSchema.optional(),
9129
+ onScreen: external_exports.boolean().optional(),
8776
9130
  label: external_exports.string().max(200).optional(),
8777
9131
  disabled: external_exports.boolean().optional(),
8778
9132
  /** Risk metadata (e.g. "high") the SDK forwards for danger UI. */
@@ -8794,6 +9148,8 @@ var sandboxObservationSchema = external_exports.object({
8794
9148
  sliders: external_exports.array(
8795
9149
  external_exports.object({
8796
9150
  id: external_exports.string().max(120),
9151
+ provenance: identityProvenanceSchema.optional(),
9152
+ onScreen: external_exports.boolean().optional(),
8797
9153
  label: external_exports.string().max(200).optional(),
8798
9154
  value: external_exports.number().finite().optional(),
8799
9155
  min: external_exports.number().finite().optional(),
@@ -8823,6 +9179,44 @@ var sandboxObservationSchema = external_exports.object({
8823
9179
  ).max(200).optional()
8824
9180
  }).passthrough()
8825
9181
  ).max(20).optional(),
9182
+ /**
9183
+ * Toggles exactly as the SDK snapshot reports them (#390).
9184
+ *
9185
+ * Their absence is why `generality-component-control-smarthome`
9186
+ * came back `errored` with a 422 instead of measuring anything:
9187
+ * this schema is `.strict()`, so a scenario that declares a switch
9188
+ * is rejected before the agent is ever asked. Which means the
9189
+ * corpus could not express the single control behind failure 02 of
9190
+ * the functional audit — "disable an agent; the switch never
9191
+ * moves". The one defect the eval most needed to catch was the one
9192
+ * shape it could not describe.
9193
+ */
9194
+ toggles: external_exports.array(
9195
+ external_exports.object({
9196
+ id: external_exports.string().max(120),
9197
+ provenance: identityProvenanceSchema.optional(),
9198
+ onScreen: external_exports.boolean().optional(),
9199
+ label: external_exports.string().max(200).optional(),
9200
+ value: external_exports.boolean().optional(),
9201
+ disabled: external_exports.boolean().optional()
9202
+ })
9203
+ ).max(100).optional(),
9204
+ /**
9205
+ * The interaction graph — the `el:*` ids the prompt tells the model
9206
+ * to prefer over ordinal handles. Passthrough per entry for the
9207
+ * same reason `lists` is: the SDK adds fields faster than this
9208
+ * schema should chase them.
9209
+ */
9210
+ elements: external_exports.array(
9211
+ external_exports.object({
9212
+ id: external_exports.string().max(160),
9213
+ role: external_exports.string().max(40).optional(),
9214
+ label: external_exports.string().max(300).optional()
9215
+ }).passthrough()
9216
+ ).max(500).optional(),
9217
+ /** Screen-level state the relay renders and branches on. */
9218
+ loading: external_exports.boolean().optional(),
9219
+ modalOpen: external_exports.boolean().optional(),
8826
9220
  extra: external_exports.record(external_exports.unknown()).optional()
8827
9221
  }).strict();
8828
9222
  external_exports.object({
@@ -8863,13 +9257,29 @@ external_exports.object({
8863
9257
  * heuristics, forces tool choice when the last user message carries a
8864
9258
  * failure marker). Only meaningful alongside `history`.
8865
9259
  */
8866
- isRecoveryHop: external_exports.boolean().optional()
9260
+ isRecoveryHop: external_exports.boolean().optional(),
9261
+ /**
9262
+ * Run this call against the application structure supplied here
9263
+ * instead of the project's active MCP document.
9264
+ *
9265
+ * Sandbox-only, and the relay honours it only on a `dryRun` call —
9266
+ * see `resolveMcpOverride` in apps/api. It exists because the eval
9267
+ * corpus could otherwise only ever describe ONE app: every scenario
9268
+ * resolves to the same seeded project, so every navigate destination,
9269
+ * every screen title and every bit of domain priming in the prompt
9270
+ * came from the same vehicle-management demo. A suite that can only
9271
+ * pose questions about one app cannot measure whether the agent works
9272
+ * on another one.
9273
+ *
9274
+ * Deliberately loose (`z.record`): the MCP document's shape is owned
9275
+ * by the generator, and pinning it here would turn every additive
9276
+ * generator change into a 422 in the sandbox.
9277
+ */
9278
+ applicationStructure: external_exports.record(external_exports.unknown()).optional()
8867
9279
  }).strict();
8868
9280
  var relayDiagnosticsSchema = external_exports.object({
8869
9281
  unverifiedTargets: external_exports.array(external_exports.string()),
8870
- guardsFired: external_exports.array(
8871
- external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })
8872
- ),
9282
+ guardsFired: external_exports.array(external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })),
8873
9283
  repairMode: external_exports.enum(["repair", "strict", "disabled"]),
8874
9284
  promptVersion: external_exports.string(),
8875
9285
  // Defaulted for producers that predate the field: before it existed
@@ -9128,6 +9538,30 @@ external_exports.object({
9128
9538
  name: external_exports.string().min(1).max(200).optional(),
9129
9539
  identifiers: endUserIdentifiersSchema.optional()
9130
9540
  });
9541
+ var MAX_EXTERNAL_USER_ID_LENGTH = 128;
9542
+ function normalizeExternalUserId(value) {
9543
+ let raw;
9544
+ if (typeof value === "string") raw = value;
9545
+ else if (typeof value === "number" && Number.isFinite(value)) raw = String(value);
9546
+ else return void 0;
9547
+ const trimmed = raw.trim();
9548
+ if (!trimmed) return void 0;
9549
+ if (trimmed.length > MAX_EXTERNAL_USER_ID_LENGTH) return void 0;
9550
+ return trimmed;
9551
+ }
9552
+ var MAX_DEVICE_INFO_BYTES = 4096;
9553
+ var sessionDeviceInfoSchema = external_exports.object({
9554
+ platform: external_exports.string().max(64).optional(),
9555
+ osVersion: external_exports.string().max(64).optional(),
9556
+ appVersion: external_exports.string().max(64).optional(),
9557
+ sdkVersion: external_exports.string().max(64).optional()
9558
+ }).passthrough().refine((info) => JSON.stringify(info).length <= MAX_DEVICE_INFO_BYTES, {
9559
+ message: `device_info exceeds ${MAX_DEVICE_INFO_BYTES} bytes`
9560
+ });
9561
+ external_exports.object({
9562
+ userId: external_exports.preprocess(normalizeExternalUserId, external_exports.string().optional()),
9563
+ deviceInfo: sessionDeviceInfoSchema.optional().catch(void 0)
9564
+ });
9131
9565
  external_exports.object({
9132
9566
  projectId: external_exports.string().min(1).max(64),
9133
9567
  externalId: external_exports.string().min(1).max(128),
@@ -9135,6 +9569,27 @@ external_exports.object({
9135
9569
  identifiers: endUserIdentifiersSchema.optional()
9136
9570
  });
9137
9571
 
9572
+ // src/utils/stringify.ts
9573
+ function stringifyUnknown(value) {
9574
+ if (value instanceof Error) {
9575
+ const message = value.message || value.name;
9576
+ const cause = value.cause;
9577
+ if (cause instanceof Error && cause.message && cause.message !== message) {
9578
+ return `${message} (${cause.message})`;
9579
+ }
9580
+ return message;
9581
+ }
9582
+ if (typeof value === "string") return value;
9583
+ if (value === null || value === void 0) return String(value);
9584
+ if (typeof value !== "object") return String(value);
9585
+ try {
9586
+ const json = JSON.stringify(value);
9587
+ if (json && json !== "{}") return json;
9588
+ } catch {
9589
+ }
9590
+ return String(value);
9591
+ }
9592
+
9138
9593
  // src/manifest/loadManifest.ts
9139
9594
  var DEFAULT_MANIFEST_FILENAME = "appilots.manifest.json";
9140
9595
  var manifestSchema = external_exports.object({
@@ -9151,14 +9606,16 @@ async function loadManifest(rootDir, manifestPath) {
9151
9606
  if (error?.code === "ENOENT") {
9152
9607
  return { manifest: null, resolvedPath };
9153
9608
  }
9154
- throw new Error(`Failed to read Appilots manifest at ${resolvedPath}: ${error?.message ?? String(error)}`);
9609
+ throw new Error(
9610
+ `Failed to read Appilots manifest at ${resolvedPath}: ${stringifyUnknown(error)}`
9611
+ );
9155
9612
  }
9156
9613
  let parsedJson;
9157
9614
  try {
9158
9615
  parsedJson = JSON.parse(raw);
9159
9616
  } catch (error) {
9160
9617
  throw new Error(
9161
- `Appilots manifest at ${resolvedPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
9618
+ `Appilots manifest at ${resolvedPath} is not valid JSON: ${stringifyUnknown(error)}`
9162
9619
  );
9163
9620
  }
9164
9621
  const result = manifestSchema.safeParse(parsedJson);
@@ -9172,11 +9629,47 @@ Each entry in "screens" must match the ScreenDescriptor shape (see docs/platform
9172
9629
  }
9173
9630
  return { manifest: result.data, resolvedPath };
9174
9631
  }
9632
+ function mergeScreen(base, declared) {
9633
+ const fields = { ...base };
9634
+ for (const [key, value] of Object.entries(declared)) {
9635
+ if (value === void 0) continue;
9636
+ if (Array.isArray(value) && value.length === 0) continue;
9637
+ fields[key] = value;
9638
+ }
9639
+ const merged = fields;
9640
+ merged.targets = mergeById(base.targets, markManifestOrigin(declared.targets));
9641
+ merged.actions = mergeById(base.actions, declared.actions) ?? [];
9642
+ if (base.agentHints || declared.agentHints) {
9643
+ merged.agentHints = { ...base.agentHints, ...declared.agentHints };
9644
+ }
9645
+ return merged;
9646
+ }
9647
+ function mergeById(base, overlay) {
9648
+ if (!overlay || overlay.length === 0) return base;
9649
+ if (!base || base.length === 0) return overlay;
9650
+ const byId = /* @__PURE__ */ new Map();
9651
+ for (const entry of base) byId.set(entry.id, entry);
9652
+ for (const entry of overlay) {
9653
+ const existing = byId.get(entry.id);
9654
+ byId.set(entry.id, existing ? { ...existing, ...entry } : entry);
9655
+ }
9656
+ return Array.from(byId.values());
9657
+ }
9658
+ function markManifestOrigin(targets) {
9659
+ if (!targets || targets.length === 0) return targets;
9660
+ return targets.map((target) => ({ origin: "manifest", ...target }));
9661
+ }
9175
9662
  function mergeManifestScreens(analyzerScreens, manifestScreens) {
9176
9663
  if (!manifestScreens || manifestScreens.length === 0) return analyzerScreens;
9177
9664
  const byName = /* @__PURE__ */ new Map();
9178
9665
  for (const screen of analyzerScreens) byName.set(screen.name, screen);
9179
- for (const screen of manifestScreens) byName.set(screen.name, screen);
9666
+ for (const declared of manifestScreens) {
9667
+ const base = byName.get(declared.name);
9668
+ byName.set(
9669
+ declared.name,
9670
+ base ? mergeScreen(base, declared) : { ...declared, targets: markManifestOrigin(declared.targets) }
9671
+ );
9672
+ }
9180
9673
  return Array.from(byName.values());
9181
9674
  }
9182
9675
  function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
@@ -9466,7 +9959,12 @@ var MCPGenerator = class _MCPGenerator {
9466
9959
  }
9467
9960
  const mergedScreens = mergeManifestScreens(analyzed.screens, manifest?.screens);
9468
9961
  const navigation = mergeManifestNavigation(analyzed.navigation, manifest?.navigation);
9469
- const agentReadyScreens = mergedScreens.map(enrichScreenForAgent);
9962
+ const agentReadyScreens = mergedScreens.map(
9963
+ (screen) => enrichScreenForAgent({
9964
+ ...screen,
9965
+ filePath: screen.filePath ? path8__default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
9966
+ })
9967
+ );
9470
9968
  const projectInfo = await this.getProjectInfo();
9471
9969
  const projectName = projectInfo.name;
9472
9970
  console.log(`[MCPGenerator] Project name: ${projectName}`);
@@ -9489,10 +9987,7 @@ var MCPGenerator = class _MCPGenerator {
9489
9987
  };
9490
9988
  const serialized = JSON.stringify(document, null, 2);
9491
9989
  const checksum = this.calculateChecksum(serialized);
9492
- const filePath = path8__default.resolve(
9493
- outputDir,
9494
- `mcp-document.${this.options.format}`
9495
- );
9990
+ const filePath = path8__default.resolve(outputDir, `mcp-document.${this.options.format}`);
9496
9991
  await writeFile(filePath, serialized, "utf-8");
9497
9992
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
9498
9993
  const checksumFilePath = path8__default.resolve(outputDir, ".appilots-checksum");
@@ -9547,6 +10042,70 @@ var MCPGenerator = class _MCPGenerator {
9547
10042
  }
9548
10043
  }
9549
10044
  };
10045
+ var DEFAULT_SERVER_URL = "http://localhost:4000";
10046
+ var KNOWN_CONFIG_KEYS = [
10047
+ "apiKey",
10048
+ "projectId",
10049
+ "serverUrl",
10050
+ "outputDir",
10051
+ "include",
10052
+ "exclude",
10053
+ "autoActivate",
10054
+ "strictScreens",
10055
+ "screenPatterns",
10056
+ "navigationInclude",
10057
+ "navigationExclude",
10058
+ "platform",
10059
+ "manifestPath",
10060
+ "eval"
10061
+ ];
10062
+ var KEY_ALIASES = {
10063
+ apiUrl: "serverUrl",
10064
+ // `@appilots/sdk`'s own name for this. One `.appilotsrc` feeds both
10065
+ // packages, so an integrator who wrote the SDK's key should not be told
10066
+ // it is unrecognized.
10067
+ apiBaseUrl: "serverUrl",
10068
+ apiURL: "serverUrl",
10069
+ baseUrl: "serverUrl",
10070
+ url: "serverUrl",
10071
+ host: "serverUrl",
10072
+ endpoint: "serverUrl",
10073
+ server: "serverUrl",
10074
+ key: "apiKey",
10075
+ token: "apiKey",
10076
+ project: "projectId",
10077
+ sourceDir: "include",
10078
+ srcDir: "include",
10079
+ source: "include",
10080
+ outDir: "outputDir"
10081
+ };
10082
+ function editDistance(a, b) {
10083
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
10084
+ for (let i = 1; i <= a.length; i++) {
10085
+ const current = [i];
10086
+ for (let j = 1; j <= b.length; j++) {
10087
+ const deletion = (previous[j] ?? 0) + 1;
10088
+ const insertion = (current[j - 1] ?? 0) + 1;
10089
+ const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
10090
+ current.push(Math.min(deletion, insertion, substitution));
10091
+ }
10092
+ previous = current;
10093
+ }
10094
+ return previous[b.length] ?? 0;
10095
+ }
10096
+ function suggestConfigKey(key) {
10097
+ const alias = KEY_ALIASES[key];
10098
+ if (alias) return alias;
10099
+ const lower = key.toLowerCase();
10100
+ let best;
10101
+ for (const known of KNOWN_CONFIG_KEYS) {
10102
+ const distance = editDistance(lower, known.toLowerCase());
10103
+ if (distance <= 2 && (!best || distance < best.distance)) {
10104
+ best = { key: known, distance };
10105
+ }
10106
+ }
10107
+ return best?.key;
10108
+ }
9550
10109
  function getEnvOverrides(env = process.env) {
9551
10110
  const clean = (value) => {
9552
10111
  const trimmed = value?.trim();
@@ -9558,7 +10117,7 @@ function getEnvOverrides(env = process.env) {
9558
10117
  serverUrl: clean(env.APPILOTS_SERVER_URL)
9559
10118
  };
9560
10119
  }
9561
- function loadConfig() {
10120
+ function loadConfig(onWarn) {
9562
10121
  const configPath = getConfigPath();
9563
10122
  const env = getEnvOverrides();
9564
10123
  let fileConfig = {};
@@ -9566,13 +10125,15 @@ function loadConfig() {
9566
10125
  try {
9567
10126
  fileConfig = JSON.parse(readFileSync(configPath, "utf-8"));
9568
10127
  } catch (error) {
9569
- throw new Error(`Failed to parse .appilotsrc at ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
10128
+ throw new Error(
10129
+ `Failed to parse .appilotsrc at ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
10130
+ );
9570
10131
  }
9571
10132
  } else if (!env.apiKey) {
9572
10133
  return null;
9573
10134
  }
9574
10135
  const merged = {
9575
- serverUrl: "http://localhost:4000",
10136
+ serverUrl: DEFAULT_SERVER_URL,
9576
10137
  outputDir: ".appilots",
9577
10138
  autoActivate: true,
9578
10139
  strictScreens: true,
@@ -9582,6 +10143,12 @@ function loadConfig() {
9582
10143
  ...env.serverUrl ? { serverUrl: env.serverUrl } : {}
9583
10144
  };
9584
10145
  const validation = validateConfig(merged);
10146
+ if (onWarn) {
10147
+ const source = configPath ?? "environment variables";
10148
+ for (const warning of validation.warnings) {
10149
+ onWarn(`${source}: ${warning}`);
10150
+ }
10151
+ }
9585
10152
  if (!validation.valid) {
9586
10153
  const source = configPath ?? "environment variables";
9587
10154
  throw new Error(
@@ -9597,7 +10164,7 @@ function saveConfig(dir, config) {
9597
10164
  const mergedConfig = {
9598
10165
  apiKey: config.apiKey || existingConfig?.apiKey || "",
9599
10166
  projectId: config.projectId || existingConfig?.projectId || "",
9600
- serverUrl: config.serverUrl || existingConfig?.serverUrl || "http://localhost:4000",
10167
+ serverUrl: config.serverUrl || existingConfig?.serverUrl || DEFAULT_SERVER_URL,
9601
10168
  outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
9602
10169
  autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
9603
10170
  include: config.include || existingConfig?.include,
@@ -9613,7 +10180,9 @@ function saveConfig(dir, config) {
9613
10180
  try {
9614
10181
  writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
9615
10182
  } catch (error) {
9616
- throw new Error(`Failed to write .appilotsrc to ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
10183
+ throw new Error(
10184
+ `Failed to write .appilotsrc to ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
10185
+ );
9617
10186
  }
9618
10187
  }
9619
10188
  function getConfigPath() {
@@ -9640,14 +10209,25 @@ function getConfigPath() {
9640
10209
  }
9641
10210
  function validateConfig(config) {
9642
10211
  const errors = [];
10212
+ const warnings = [];
9643
10213
  if (!config || typeof config !== "object") {
9644
10214
  return {
9645
10215
  valid: false,
9646
- errors: ["Configuration must be an object"]
10216
+ errors: ["Configuration must be an object"],
10217
+ warnings
9647
10218
  };
9648
10219
  }
10220
+ for (const key of Object.keys(config)) {
10221
+ if (KNOWN_CONFIG_KEYS.includes(key)) continue;
10222
+ const suggestion = suggestConfigKey(key);
10223
+ warnings.push(
10224
+ `unknown key "${key}" \u2014 ignored` + (suggestion ? ` (did you mean "${suggestion}"?)` : "")
10225
+ );
10226
+ }
9649
10227
  if (!config.apiKey || typeof config.apiKey !== "string") {
9650
- errors.push("apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)");
10228
+ errors.push(
10229
+ "apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)"
10230
+ );
9651
10231
  } else if (!config.apiKey.startsWith("ak_")) {
9652
10232
  errors.push('apiKey must start with "ak_"');
9653
10233
  }
@@ -9709,7 +10289,8 @@ function validateConfig(config) {
9709
10289
  }
9710
10290
  return {
9711
10291
  valid: errors.length === 0,
9712
- errors
10292
+ errors,
10293
+ warnings
9713
10294
  };
9714
10295
  }
9715
10296
 
@@ -9755,19 +10336,76 @@ function formatMetadataWarnings(warnings) {
9755
10336
  );
9756
10337
  }
9757
10338
 
10339
+ // ../shared/dist/chunk-UEMUBXFH.mjs
10340
+ var DEFAULT_API_BASE_URL = "https://api.appilots.com";
10341
+ var APPILOTS_API_PATH_PREFIX = "/api/v1";
10342
+ function normalizeApiBaseUrl(configured) {
10343
+ const raw = (configured ?? DEFAULT_API_BASE_URL).trim().replace(/\/+$/, "");
10344
+ if (!raw) return `${DEFAULT_API_BASE_URL}${APPILOTS_API_PATH_PREFIX}`;
10345
+ if (new RegExp(`${APPILOTS_API_PATH_PREFIX}(/|$)`).test(raw)) return raw;
10346
+ return `${raw}${APPILOTS_API_PATH_PREFIX}`;
10347
+ }
10348
+
9758
10349
  // src/services/api-client.ts
9759
- var DEFAULT_TIMEOUT_MS = 3e4;
10350
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
9760
10351
  var DEFAULT_MAX_RETRIES = 2;
9761
10352
  var RETRY_BASE_DELAY_MS = 500;
10353
+ function describeNetworkError(url, err) {
10354
+ let origin = url;
10355
+ try {
10356
+ origin = new URL(url).origin;
10357
+ } catch {
10358
+ }
10359
+ const cause = err?.cause;
10360
+ const code = typeof cause?.code === "string" ? cause.code : void 0;
10361
+ const detail = code ?? stringifyUnknown(err);
10362
+ return new Error(`Cannot reach Appilots server at ${origin} (${detail})`);
10363
+ }
10364
+ function errorCodeOf(payload) {
10365
+ const error = payload?.error;
10366
+ if (error && typeof error === "object") {
10367
+ const { code } = error;
10368
+ if (typeof code === "string" && code.trim()) return code;
10369
+ }
10370
+ return void 0;
10371
+ }
10372
+ function describeApiError(payload, fallback) {
10373
+ const error = payload?.error;
10374
+ if (typeof error === "string" && error.trim()) return error;
10375
+ if (error && typeof error === "object") {
10376
+ const { code, message: message2 } = error;
10377
+ const text = typeof message2 === "string" && message2.trim() ? message2 : void 0;
10378
+ const tag = typeof code === "string" && code.trim() ? code : void 0;
10379
+ if (text && tag) return `${text} (${tag})`;
10380
+ if (text) return text;
10381
+ if (tag) return tag;
10382
+ }
10383
+ const message = payload?.message;
10384
+ if (typeof message === "string" && message.trim()) return message;
10385
+ return fallback;
10386
+ }
9762
10387
  var AppilotsAPIClient = class {
9763
- serverUrl;
10388
+ /**
10389
+ * `serverUrl` with the API's mount prefix resolved — every path below
10390
+ * is relative to THIS, not to the configured origin.
10391
+ *
10392
+ * It used to be the raw `serverUrl` with `/api/v1` hardcoded into each
10393
+ * path, which made the field mean something different here than in the
10394
+ * SDK. Both read the same `.appilotsrc`: the SDK completes the prefix
10395
+ * when it is missing, so `https://api.appilots.com/api/v1` is correct
10396
+ * there — and here that same value produced
10397
+ * `/api/v1/api/v1/cli/sync`, a 404 whose body reads `Route not found`.
10398
+ * Sharing `normalizeApiBaseUrl` is what makes one file mean one thing:
10399
+ * with or without the prefix now works in both.
10400
+ */
10401
+ baseUrl;
9764
10402
  apiKey;
9765
10403
  timeoutMs;
9766
10404
  maxRetries;
9767
10405
  constructor(config) {
9768
- this.serverUrl = config.serverUrl.replace(/\/$/, "");
10406
+ this.baseUrl = normalizeApiBaseUrl(config.serverUrl);
9769
10407
  this.apiKey = config.apiKey;
9770
- this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10408
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
9771
10409
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
9772
10410
  }
9773
10411
  /**
@@ -9791,7 +10429,7 @@ var AppilotsAPIClient = class {
9791
10429
  }
9792
10430
  return response;
9793
10431
  } catch (err) {
9794
- lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : err;
10432
+ lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : describeNetworkError(url, err);
9795
10433
  } finally {
9796
10434
  clearTimeout(timer);
9797
10435
  }
@@ -9810,7 +10448,7 @@ var AppilotsAPIClient = class {
9810
10448
  */
9811
10449
  async sync(content, version, appVersion) {
9812
10450
  try {
9813
- const response = await this.request(`${this.serverUrl}/api/v1/cli/sync`, {
10451
+ const response = await this.request(`${this.baseUrl}/cli/sync`, {
9814
10452
  method: "POST",
9815
10453
  headers: {
9816
10454
  "Content-Type": "application/json",
@@ -9824,7 +10462,9 @@ var AppilotsAPIClient = class {
9824
10462
  return {
9825
10463
  success: false,
9826
10464
  unchanged: false,
9827
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
10465
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`),
10466
+ errorCode: errorCodeOf(errorData),
10467
+ errorStatus: response.status
9828
10468
  };
9829
10469
  }
9830
10470
  const json = await response.json();
@@ -9837,7 +10477,8 @@ var AppilotsAPIClient = class {
9837
10477
  return {
9838
10478
  success: false,
9839
10479
  unchanged: false,
9840
- error: error instanceof Error ? error.message : "Failed to sync with Appilots API"
10480
+ error: error instanceof Error ? error.message : "Failed to sync with Appilots API",
10481
+ errorStatus: 0
9841
10482
  };
9842
10483
  }
9843
10484
  }
@@ -9848,7 +10489,7 @@ var AppilotsAPIClient = class {
9848
10489
  */
9849
10490
  async status() {
9850
10491
  try {
9851
- const response = await this.request(`${this.serverUrl}/api/v1/cli/status`, {
10492
+ const response = await this.request(`${this.baseUrl}/cli/status`, {
9852
10493
  method: "GET",
9853
10494
  headers: {
9854
10495
  Authorization: `Bearer ${this.apiKey}`
@@ -9857,7 +10498,7 @@ var AppilotsAPIClient = class {
9857
10498
  if (!response.ok) {
9858
10499
  const errorData = await response.json().catch(() => ({}));
9859
10500
  return {
9860
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
10501
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
9861
10502
  };
9862
10503
  }
9863
10504
  const json = await response.json();
@@ -9879,7 +10520,7 @@ var AppilotsAPIClient = class {
9879
10520
  */
9880
10521
  async evalRun(scenarios) {
9881
10522
  try {
9882
- const response = await this.request(`${this.serverUrl}/api/v1/cli/eval/run`, {
10523
+ const response = await this.request(`${this.baseUrl}/cli/eval/run`, {
9883
10524
  method: "POST",
9884
10525
  headers: {
9885
10526
  "Content-Type": "application/json",
@@ -9910,7 +10551,7 @@ var AppilotsAPIClient = class {
9910
10551
  */
9911
10552
  async health() {
9912
10553
  try {
9913
- const response = await this.request(`${this.serverUrl}/api/v1/health`, {
10554
+ const response = await this.request(`${this.baseUrl}/health`, {
9914
10555
  method: "GET"
9915
10556
  });
9916
10557
  return response.ok;
@@ -9920,6 +10561,9 @@ var AppilotsAPIClient = class {
9920
10561
  }
9921
10562
  };
9922
10563
 
9923
- export { AppilotsAPIClient, ComponentAnalyzer, DEFAULT_MANIFEST_FILENAME, DEFAULT_WEB_SCREEN_PATTERNS, FormAnalyzer, GenericPlatformAnalyzer, MCPGenerator, NavigationAnalyzer, ReactNativePlatformAnalyzer, ReactWebPlatformAnalyzer, ScreenAnalyzer, WebNavigationAnalyzer, WebScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, loadManifest, mergeManifestNavigation, mergeManifestScreens, resolvePathToScreen, saveConfig, screenNameFromPath, validateConfig };
10564
+ // src/version.ts
10565
+ var CLI_VERSION = "0.10.0";
10566
+
10567
+ export { AppilotsAPIClient, CLI_VERSION, ComponentAnalyzer, DEFAULT_MANIFEST_FILENAME, DEFAULT_WEB_SCREEN_PATTERNS, FormAnalyzer, GenericPlatformAnalyzer, MCPGenerator, NavigationAnalyzer, ReactNativePlatformAnalyzer, ReactWebPlatformAnalyzer, ScreenAnalyzer, WebNavigationAnalyzer, WebScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, loadManifest, mergeManifestNavigation, mergeManifestScreens, resolvePathToScreen, saveConfig, screenNameFromPath, validateConfig };
9924
10568
  //# sourceMappingURL=index.mjs.map
9925
10569
  //# sourceMappingURL=index.mjs.map