@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.js CHANGED
@@ -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
  }
@@ -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 {
@@ -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;
@@ -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
  }
@@ -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,
@@ -2657,7 +2824,9 @@ var WebScreenAnalyzer = class {
2657
2824
  include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
2658
2825
  exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
2659
2826
  } = this.config;
2660
- const files = await glob__default.default(include, { cwd: this.config.rootDir, ignore: exclude });
2827
+ const files = (await glob__default.default(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
2828
+ (file) => !file.endsWith(".d.ts")
2829
+ );
2661
2830
  const patternMatches = await glob__default.default(this.screenPatterns, {
2662
2831
  cwd: this.config.rootDir,
2663
2832
  ignore: exclude
@@ -3028,7 +3197,8 @@ var WebScreenAnalyzer = class {
3028
3197
  if (type === "email") return "email";
3029
3198
  if (type === "tel") return "phone";
3030
3199
  if (type === "number") return "number";
3031
- if (type === "date" || type === "datetime-local" || type === "month" || type === "week") return "date";
3200
+ if (type === "date" || type === "datetime-local" || type === "month" || type === "week")
3201
+ return "date";
3032
3202
  const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
3033
3203
  if (inputMode === "email") return "email";
3034
3204
  if (inputMode === "tel") return "phone";
@@ -3342,7 +3512,9 @@ var WebScreenAnalyzer = class {
3342
3512
  }
3343
3513
  const callback = expr.arguments[0];
3344
3514
  if (!BabelTypes__namespace.isArrowFunctionExpression(callback) && !BabelTypes__namespace.isFunctionExpression(callback)) return;
3345
- const enclosing = nodePath.findParent((p) => p.isJSXElement());
3515
+ const enclosing = nodePath.findParent(
3516
+ (p) => p.isJSXElement()
3517
+ );
3346
3518
  const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
3347
3519
  const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
3348
3520
  const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
@@ -3444,16 +3616,19 @@ function firstCalledFunctionName(node) {
3444
3616
  }
3445
3617
  function containsWindowConfirm(body) {
3446
3618
  let found = false;
3447
- traverse4__default.default(body, {
3448
- noScope: true,
3449
- CallExpression: (nodePath) => {
3450
- const callee = nodePath.node.callee;
3451
- if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "confirm") found = true;
3452
- if (BabelTypes__namespace.isMemberExpression(callee) && BabelTypes__namespace.isIdentifier(callee.object) && callee.object.name === "window" && BabelTypes__namespace.isIdentifier(callee.property) && callee.property.name === "confirm") {
3453
- found = true;
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
+ }
3454
3629
  }
3455
3630
  }
3456
- });
3631
+ );
3457
3632
  return found;
3458
3633
  }
3459
3634
  function mergeLocator(current, next) {
@@ -3493,32 +3668,38 @@ function collectionItemNames(callback) {
3493
3668
  function collectionDisplayFields(callback, itemNames) {
3494
3669
  const fields = /* @__PURE__ */ new Set();
3495
3670
  if (!callback.body) return [];
3496
- traverse4__default.default(callback.body, {
3497
- noScope: true,
3498
- MemberExpression: (nodePath) => {
3499
- const node = nodePath.node;
3500
- if (BabelTypes__namespace.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes__namespace.isIdentifier(node.property)) {
3501
- fields.add(node.property.name);
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
+ }
3502
3680
  }
3503
3681
  }
3504
- });
3682
+ );
3505
3683
  return Array.from(fields).sort();
3506
3684
  }
3507
3685
  function collectionKeyField(callback, itemNames) {
3508
3686
  let keyField;
3509
3687
  if (!callback.body) return void 0;
3510
- traverse4__default.default(callback.body, {
3511
- noScope: true,
3512
- JSXAttribute: (nodePath) => {
3513
- const attr = nodePath.node;
3514
- if (!BabelTypes__namespace.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
3515
- if (!attr.value || !BabelTypes__namespace.isJSXExpressionContainer(attr.value)) return;
3516
- const expr = attr.value.expression;
3517
- if (BabelTypes__namespace.isMemberExpression(expr) && BabelTypes__namespace.isIdentifier(expr.object) && itemNames.has(expr.object.name) && BabelTypes__namespace.isIdentifier(expr.property)) {
3518
- keyField = keyField ?? expr.property.name;
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
+ }
3519
3700
  }
3520
3701
  }
3521
- });
3702
+ );
3522
3703
  return keyField;
3523
3704
  }
3524
3705
  function collectionRowAction(callback) {
@@ -3566,7 +3747,7 @@ function inferIdentityFields(keyField, displayFields) {
3566
3747
  const out = /* @__PURE__ */ new Set();
3567
3748
  if (keyField) out.add(keyField);
3568
3749
  for (const field of displayFields) {
3569
- if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
3750
+ if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
3570
3751
  }
3571
3752
  return Array.from(out);
3572
3753
  }
@@ -7950,7 +8131,7 @@ var coerce = {
7950
8131
  };
7951
8132
  var NEVER = INVALID;
7952
8133
 
7953
- // ../shared/dist/chunk-MSOZJNXB.mjs
8134
+ // ../shared/dist/chunk-PKS3VDNB.mjs
7954
8135
  var locatorSourceSchema = external_exports.enum([
7955
8136
  "appilotsId",
7956
8137
  "testID",
@@ -7976,7 +8157,16 @@ var locatorDescriptorSchema = external_exports.object({
7976
8157
  source: locatorSourceSchema.optional()
7977
8158
  }).passthrough();
7978
8159
  var signalDescriptorSchema = external_exports.object({
7979
- 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)),
7980
8170
  target: external_exports.string().optional(),
7981
8171
  description: external_exports.string().optional()
7982
8172
  }).passthrough();
@@ -7990,7 +8180,20 @@ var waitPolicyDescriptorSchema = external_exports.object({
7990
8180
  signals: external_exports.array(signalDescriptorSchema).optional(),
7991
8181
  maxMs: external_exports.number().finite().optional()
7992
8182
  }).passthrough();
7993
- 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));
7994
8197
  var targetDescriptorSchema = external_exports.object({
7995
8198
  id: external_exports.string(),
7996
8199
  role: targetDescriptorRoleSchema,
@@ -8004,10 +8207,20 @@ var targetDescriptorSchema = external_exports.object({
8004
8207
  requiresConfirmation: external_exports.boolean().optional(),
8005
8208
  opensModal: external_exports.string().optional(),
8006
8209
  opensBottomSheet: external_exports.string().optional(),
8007
- sourceComponent: external_exports.string().optional()
8210
+ sourceComponent: external_exports.string().optional(),
8211
+ origin: targetOriginSchema.optional()
8008
8212
  }).passthrough();
8009
8213
  var flowStepDescriptorSchema = external_exports.object({
8010
- 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)),
8011
8224
  target: external_exports.string().optional(),
8012
8225
  label: external_exports.string().optional(),
8013
8226
  description: external_exports.string().optional(),
@@ -8229,6 +8442,15 @@ external_exports.object({
8229
8442
  password: external_exports.string().min(1, "Password is required"),
8230
8443
  code: totpCodeSchema
8231
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
+ });
8232
8454
  var createProjectSchema = external_exports.object({
8233
8455
  name: external_exports.string().min(1).max(100),
8234
8456
  description: external_exports.string().max(500).optional(),
@@ -8264,6 +8486,7 @@ external_exports.object({
8264
8486
  content: external_exports.record(external_exports.unknown())
8265
8487
  });
8266
8488
  var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
8489
+ var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
8267
8490
  external_exports.object({
8268
8491
  name: external_exports.string().min(1).max(100),
8269
8492
  // Required for `sdk` (default), forbidden/ignored for `operator` —
@@ -8271,6 +8494,7 @@ external_exports.object({
8271
8494
  // missing scope still validates as the pre-existing SDK shape.
8272
8495
  projectId: external_exports.string().min(1).optional(),
8273
8496
  scope: apiKeyScopeSchema.default("sdk"),
8497
+ environment: apiKeyEnvironmentSchema.optional(),
8274
8498
  expiresAt: external_exports.string().datetime().optional()
8275
8499
  }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
8276
8500
  message: 'projectId is required for scope "sdk"',
@@ -8278,13 +8502,23 @@ external_exports.object({
8278
8502
  });
8279
8503
  var boundedString = (max) => external_exports.string().max(max);
8280
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));
8281
8506
  var snapshotInputSchema = external_exports.object({
8282
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(),
8283
8516
  label: boundedString(300).optional(),
8284
8517
  value: boundedString(4096).optional(),
8285
8518
  placeholder: boundedString(300).optional(),
8286
8519
  editable: external_exports.boolean().optional(),
8287
8520
  secure: external_exports.boolean().optional(),
8521
+ submitsOnReturn: external_exports.boolean().optional(),
8288
8522
  type: boundedString(40).optional(),
8289
8523
  required: external_exports.boolean().optional(),
8290
8524
  invalid: external_exports.boolean().optional(),
@@ -8292,6 +8526,14 @@ var snapshotInputSchema = external_exports.object({
8292
8526
  }).passthrough();
8293
8527
  var snapshotButtonSchema = external_exports.object({
8294
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(),
8295
8537
  label: boundedString(300).optional(),
8296
8538
  disabled: external_exports.boolean().optional(),
8297
8539
  inModal: external_exports.boolean().optional(),
@@ -8301,12 +8543,28 @@ var snapshotButtonSchema = external_exports.object({
8301
8543
  }).passthrough();
8302
8544
  var snapshotToggleSchema = external_exports.object({
8303
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(),
8304
8554
  label: boundedString(300).optional(),
8305
8555
  value: external_exports.boolean().optional(),
8306
8556
  inModal: external_exports.boolean().optional()
8307
8557
  }).passthrough();
8308
8558
  var snapshotSliderSchema = external_exports.object({
8309
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(),
8310
8568
  label: boundedString(300).optional(),
8311
8569
  value: external_exports.number().finite().optional(),
8312
8570
  min: external_exports.number().finite().optional(),
@@ -8347,6 +8605,26 @@ var snapshotListSchema = external_exports.object({
8347
8605
  // should chase, and the relay only reads a known subset.
8348
8606
  items: external_exports.array(external_exports.record(external_exports.unknown())).max(500).optional()
8349
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();
8350
8628
  var snapshotChoiceGroupSchema = external_exports.object({
8351
8629
  index: external_exports.number().int().optional(),
8352
8630
  id: boundedString(160).optional(),
@@ -8363,6 +8641,14 @@ var snapshotElementSchema = external_exports.object({
8363
8641
  disabled: external_exports.boolean().optional(),
8364
8642
  selected: external_exports.boolean().optional(),
8365
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(),
8366
8652
  targetId: boundedString(160).optional(),
8367
8653
  listContext: external_exports.record(external_exports.unknown()).optional(),
8368
8654
  inModal: external_exports.boolean().optional()
@@ -8379,7 +8665,34 @@ var agentSnapshotSchema = external_exports.object({
8379
8665
  modalOpen: external_exports.boolean().optional(),
8380
8666
  lists: external_exports.array(snapshotListSchema).max(100).optional(),
8381
8667
  choiceGroups: external_exports.array(snapshotChoiceGroupSchema).max(100).optional(),
8382
- 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()
8383
8696
  }).passthrough();
8384
8697
  var agentContextSchema = external_exports.object({
8385
8698
  /**
@@ -8429,6 +8742,19 @@ var agentContextSchema = external_exports.object({
8429
8742
  ).max(200).optional(),
8430
8743
  loadingPending: external_exports.boolean().optional()
8431
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
+ });
8432
8758
  external_exports.object({
8433
8759
  content: external_exports.string().min(1).max(4096),
8434
8760
  sessionId: external_exports.string().optional(),
@@ -8438,7 +8764,20 @@ external_exports.object({
8438
8764
  * the active MCP doc and emits `mcp_version_mismatch` telemetry when
8439
8765
  * they differ. Optional — older SDKs simply skip the check.
8440
8766
  */
8441
- 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()
8442
8781
  });
8443
8782
  var agentActionTypeSchema = external_exports.enum([
8444
8783
  "navigate",
@@ -8449,11 +8788,22 @@ var agentActionTypeSchema = external_exports.enum([
8449
8788
  "custom"
8450
8789
  ]);
8451
8790
  var navigationPayloadSchema = external_exports.object({
8452
- 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(),
8453
8797
  params: external_exports.record(external_exports.unknown()).optional(),
8454
8798
  navigationAction: external_exports.enum(["push", "navigate", "replace", "goBack", "reset"]).default("navigate").optional(),
8455
8799
  path: external_exports.array(external_exports.string().min(1)).optional()
8456
- }).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
+ );
8457
8807
  var formFillFieldSchema = external_exports.object({
8458
8808
  fieldId: external_exports.string().min(1),
8459
8809
  fieldType: external_exports.enum(["text", "select", "toggle", "date", "number", "custom"]).default("text").optional(),
@@ -8787,6 +9137,8 @@ var sandboxObservationSchema = external_exports.object({
8787
9137
  inputs: external_exports.array(
8788
9138
  external_exports.object({
8789
9139
  id: external_exports.string().max(120),
9140
+ provenance: identityProvenanceSchema.optional(),
9141
+ onScreen: external_exports.boolean().optional(),
8790
9142
  label: external_exports.string().max(200).optional(),
8791
9143
  value: external_exports.string().max(2e3).optional(),
8792
9144
  disabled: external_exports.boolean().optional(),
@@ -8800,6 +9152,8 @@ var sandboxObservationSchema = external_exports.object({
8800
9152
  buttons: external_exports.array(
8801
9153
  external_exports.object({
8802
9154
  id: external_exports.string().max(120),
9155
+ provenance: identityProvenanceSchema.optional(),
9156
+ onScreen: external_exports.boolean().optional(),
8803
9157
  label: external_exports.string().max(200).optional(),
8804
9158
  disabled: external_exports.boolean().optional(),
8805
9159
  /** Risk metadata (e.g. "high") the SDK forwards for danger UI. */
@@ -8821,6 +9175,8 @@ var sandboxObservationSchema = external_exports.object({
8821
9175
  sliders: external_exports.array(
8822
9176
  external_exports.object({
8823
9177
  id: external_exports.string().max(120),
9178
+ provenance: identityProvenanceSchema.optional(),
9179
+ onScreen: external_exports.boolean().optional(),
8824
9180
  label: external_exports.string().max(200).optional(),
8825
9181
  value: external_exports.number().finite().optional(),
8826
9182
  min: external_exports.number().finite().optional(),
@@ -8850,6 +9206,44 @@ var sandboxObservationSchema = external_exports.object({
8850
9206
  ).max(200).optional()
8851
9207
  }).passthrough()
8852
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(),
8853
9247
  extra: external_exports.record(external_exports.unknown()).optional()
8854
9248
  }).strict();
8855
9249
  external_exports.object({
@@ -8890,13 +9284,29 @@ external_exports.object({
8890
9284
  * heuristics, forces tool choice when the last user message carries a
8891
9285
  * failure marker). Only meaningful alongside `history`.
8892
9286
  */
8893
- 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()
8894
9306
  }).strict();
8895
9307
  var relayDiagnosticsSchema = external_exports.object({
8896
9308
  unverifiedTargets: external_exports.array(external_exports.string()),
8897
- guardsFired: external_exports.array(
8898
- external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })
8899
- ),
9309
+ guardsFired: external_exports.array(external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })),
8900
9310
  repairMode: external_exports.enum(["repair", "strict", "disabled"]),
8901
9311
  promptVersion: external_exports.string(),
8902
9312
  // Defaulted for producers that predate the field: before it existed
@@ -9155,6 +9565,30 @@ external_exports.object({
9155
9565
  name: external_exports.string().min(1).max(200).optional(),
9156
9566
  identifiers: endUserIdentifiersSchema.optional()
9157
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
+ });
9158
9592
  external_exports.object({
9159
9593
  projectId: external_exports.string().min(1).max(64),
9160
9594
  externalId: external_exports.string().min(1).max(128),
@@ -9162,6 +9596,27 @@ external_exports.object({
9162
9596
  identifiers: endUserIdentifiersSchema.optional()
9163
9597
  });
9164
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
+
9165
9620
  // src/manifest/loadManifest.ts
9166
9621
  var DEFAULT_MANIFEST_FILENAME = "appilots.manifest.json";
9167
9622
  var manifestSchema = external_exports.object({
@@ -9178,14 +9633,16 @@ async function loadManifest(rootDir, manifestPath) {
9178
9633
  if (error?.code === "ENOENT") {
9179
9634
  return { manifest: null, resolvedPath };
9180
9635
  }
9181
- 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
+ );
9182
9639
  }
9183
9640
  let parsedJson;
9184
9641
  try {
9185
9642
  parsedJson = JSON.parse(raw);
9186
9643
  } catch (error) {
9187
9644
  throw new Error(
9188
- `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)}`
9189
9646
  );
9190
9647
  }
9191
9648
  const result = manifestSchema.safeParse(parsedJson);
@@ -9199,11 +9656,47 @@ Each entry in "screens" must match the ScreenDescriptor shape (see docs/platform
9199
9656
  }
9200
9657
  return { manifest: result.data, resolvedPath };
9201
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
+ }
9202
9689
  function mergeManifestScreens(analyzerScreens, manifestScreens) {
9203
9690
  if (!manifestScreens || manifestScreens.length === 0) return analyzerScreens;
9204
9691
  const byName = /* @__PURE__ */ new Map();
9205
9692
  for (const screen of analyzerScreens) byName.set(screen.name, screen);
9206
- 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
+ }
9207
9700
  return Array.from(byName.values());
9208
9701
  }
9209
9702
  function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
@@ -9493,7 +9986,12 @@ var MCPGenerator = class _MCPGenerator {
9493
9986
  }
9494
9987
  const mergedScreens = mergeManifestScreens(analyzed.screens, manifest?.screens);
9495
9988
  const navigation = mergeManifestNavigation(analyzed.navigation, manifest?.navigation);
9496
- 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
+ );
9497
9995
  const projectInfo = await this.getProjectInfo();
9498
9996
  const projectName = projectInfo.name;
9499
9997
  console.log(`[MCPGenerator] Project name: ${projectName}`);
@@ -9516,10 +10014,7 @@ var MCPGenerator = class _MCPGenerator {
9516
10014
  };
9517
10015
  const serialized = JSON.stringify(document, null, 2);
9518
10016
  const checksum = this.calculateChecksum(serialized);
9519
- const filePath = path8__namespace.default.resolve(
9520
- outputDir,
9521
- `mcp-document.${this.options.format}`
9522
- );
10017
+ const filePath = path8__namespace.default.resolve(outputDir, `mcp-document.${this.options.format}`);
9523
10018
  await fs.writeFile(filePath, serialized, "utf-8");
9524
10019
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
9525
10020
  const checksumFilePath = path8__namespace.default.resolve(outputDir, ".appilots-checksum");
@@ -9574,6 +10069,70 @@ var MCPGenerator = class _MCPGenerator {
9574
10069
  }
9575
10070
  }
9576
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
+ }
9577
10136
  function getEnvOverrides(env = process.env) {
9578
10137
  const clean = (value) => {
9579
10138
  const trimmed = value?.trim();
@@ -9585,7 +10144,7 @@ function getEnvOverrides(env = process.env) {
9585
10144
  serverUrl: clean(env.APPILOTS_SERVER_URL)
9586
10145
  };
9587
10146
  }
9588
- function loadConfig() {
10147
+ function loadConfig(onWarn) {
9589
10148
  const configPath = getConfigPath();
9590
10149
  const env = getEnvOverrides();
9591
10150
  let fileConfig = {};
@@ -9593,13 +10152,15 @@ function loadConfig() {
9593
10152
  try {
9594
10153
  fileConfig = JSON.parse(fs$1.readFileSync(configPath, "utf-8"));
9595
10154
  } catch (error) {
9596
- 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
+ );
9597
10158
  }
9598
10159
  } else if (!env.apiKey) {
9599
10160
  return null;
9600
10161
  }
9601
10162
  const merged = {
9602
- serverUrl: "http://localhost:4000",
10163
+ serverUrl: DEFAULT_SERVER_URL,
9603
10164
  outputDir: ".appilots",
9604
10165
  autoActivate: true,
9605
10166
  strictScreens: true,
@@ -9609,6 +10170,12 @@ function loadConfig() {
9609
10170
  ...env.serverUrl ? { serverUrl: env.serverUrl } : {}
9610
10171
  };
9611
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
+ }
9612
10179
  if (!validation.valid) {
9613
10180
  const source = configPath ?? "environment variables";
9614
10181
  throw new Error(
@@ -9624,7 +10191,7 @@ function saveConfig(dir, config) {
9624
10191
  const mergedConfig = {
9625
10192
  apiKey: config.apiKey || existingConfig?.apiKey || "",
9626
10193
  projectId: config.projectId || existingConfig?.projectId || "",
9627
- serverUrl: config.serverUrl || existingConfig?.serverUrl || "http://localhost:4000",
10194
+ serverUrl: config.serverUrl || existingConfig?.serverUrl || DEFAULT_SERVER_URL,
9628
10195
  outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
9629
10196
  autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
9630
10197
  include: config.include || existingConfig?.include,
@@ -9640,7 +10207,9 @@ function saveConfig(dir, config) {
9640
10207
  try {
9641
10208
  fs$1.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
9642
10209
  } catch (error) {
9643
- 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
+ );
9644
10213
  }
9645
10214
  }
9646
10215
  function getConfigPath() {
@@ -9667,14 +10236,25 @@ function getConfigPath() {
9667
10236
  }
9668
10237
  function validateConfig(config) {
9669
10238
  const errors = [];
10239
+ const warnings = [];
9670
10240
  if (!config || typeof config !== "object") {
9671
10241
  return {
9672
10242
  valid: false,
9673
- errors: ["Configuration must be an object"]
10243
+ errors: ["Configuration must be an object"],
10244
+ warnings
9674
10245
  };
9675
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
+ }
9676
10254
  if (!config.apiKey || typeof config.apiKey !== "string") {
9677
- 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
+ );
9678
10258
  } else if (!config.apiKey.startsWith("ak_")) {
9679
10259
  errors.push('apiKey must start with "ak_"');
9680
10260
  }
@@ -9736,7 +10316,8 @@ function validateConfig(config) {
9736
10316
  }
9737
10317
  return {
9738
10318
  valid: errors.length === 0,
9739
- errors
10319
+ errors,
10320
+ warnings
9740
10321
  };
9741
10322
  }
9742
10323
 
@@ -9782,19 +10363,76 @@ function formatMetadataWarnings(warnings) {
9782
10363
  );
9783
10364
  }
9784
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
+
9785
10376
  // src/services/api-client.ts
9786
- var DEFAULT_TIMEOUT_MS = 3e4;
10377
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
9787
10378
  var DEFAULT_MAX_RETRIES = 2;
9788
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
+ }
9789
10414
  var AppilotsAPIClient = class {
9790
- 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;
9791
10429
  apiKey;
9792
10430
  timeoutMs;
9793
10431
  maxRetries;
9794
10432
  constructor(config) {
9795
- this.serverUrl = config.serverUrl.replace(/\/$/, "");
10433
+ this.baseUrl = normalizeApiBaseUrl(config.serverUrl);
9796
10434
  this.apiKey = config.apiKey;
9797
- this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10435
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
9798
10436
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
9799
10437
  }
9800
10438
  /**
@@ -9818,7 +10456,7 @@ var AppilotsAPIClient = class {
9818
10456
  }
9819
10457
  return response;
9820
10458
  } catch (err) {
9821
- 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);
9822
10460
  } finally {
9823
10461
  clearTimeout(timer);
9824
10462
  }
@@ -9837,7 +10475,7 @@ var AppilotsAPIClient = class {
9837
10475
  */
9838
10476
  async sync(content, version, appVersion) {
9839
10477
  try {
9840
- const response = await this.request(`${this.serverUrl}/api/v1/cli/sync`, {
10478
+ const response = await this.request(`${this.baseUrl}/cli/sync`, {
9841
10479
  method: "POST",
9842
10480
  headers: {
9843
10481
  "Content-Type": "application/json",
@@ -9851,7 +10489,9 @@ var AppilotsAPIClient = class {
9851
10489
  return {
9852
10490
  success: false,
9853
10491
  unchanged: false,
9854
- 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
9855
10495
  };
9856
10496
  }
9857
10497
  const json = await response.json();
@@ -9864,7 +10504,8 @@ var AppilotsAPIClient = class {
9864
10504
  return {
9865
10505
  success: false,
9866
10506
  unchanged: false,
9867
- 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
9868
10509
  };
9869
10510
  }
9870
10511
  }
@@ -9875,7 +10516,7 @@ var AppilotsAPIClient = class {
9875
10516
  */
9876
10517
  async status() {
9877
10518
  try {
9878
- const response = await this.request(`${this.serverUrl}/api/v1/cli/status`, {
10519
+ const response = await this.request(`${this.baseUrl}/cli/status`, {
9879
10520
  method: "GET",
9880
10521
  headers: {
9881
10522
  Authorization: `Bearer ${this.apiKey}`
@@ -9884,7 +10525,7 @@ var AppilotsAPIClient = class {
9884
10525
  if (!response.ok) {
9885
10526
  const errorData = await response.json().catch(() => ({}));
9886
10527
  return {
9887
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
10528
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
9888
10529
  };
9889
10530
  }
9890
10531
  const json = await response.json();
@@ -9906,7 +10547,7 @@ var AppilotsAPIClient = class {
9906
10547
  */
9907
10548
  async evalRun(scenarios) {
9908
10549
  try {
9909
- const response = await this.request(`${this.serverUrl}/api/v1/cli/eval/run`, {
10550
+ const response = await this.request(`${this.baseUrl}/cli/eval/run`, {
9910
10551
  method: "POST",
9911
10552
  headers: {
9912
10553
  "Content-Type": "application/json",
@@ -9937,7 +10578,7 @@ var AppilotsAPIClient = class {
9937
10578
  */
9938
10579
  async health() {
9939
10580
  try {
9940
- const response = await this.request(`${this.serverUrl}/api/v1/health`, {
10581
+ const response = await this.request(`${this.baseUrl}/health`, {
9941
10582
  method: "GET"
9942
10583
  });
9943
10584
  return response.ok;
@@ -9947,7 +10588,11 @@ var AppilotsAPIClient = class {
9947
10588
  }
9948
10589
  };
9949
10590
 
10591
+ // src/version.ts
10592
+ var CLI_VERSION = "0.10.0";
10593
+
9950
10594
  exports.AppilotsAPIClient = AppilotsAPIClient;
10595
+ exports.CLI_VERSION = CLI_VERSION;
9951
10596
  exports.ComponentAnalyzer = ComponentAnalyzer;
9952
10597
  exports.DEFAULT_MANIFEST_FILENAME = DEFAULT_MANIFEST_FILENAME;
9953
10598
  exports.DEFAULT_WEB_SCREEN_PATTERNS = DEFAULT_WEB_SCREEN_PATTERNS;