@appilots/cli 0.4.0 → 0.11.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-MKT45LRA.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,15 +8442,31 @@ 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(),
8235
8457
  // Reference to a tenant-scoped provider (preferred). Optional at
8236
8458
  // creation so a project can be created and configured later.
8237
8459
  providerId: external_exports.string().min(1).optional(),
8238
- systemPrompt: external_exports.string().max(4096).optional()
8460
+ // 16k chars (~4k tokens): um prompt de CS real não cabe em 4096 — o
8461
+ // primeiro cliente de produção chegou com 6.385 e tomou 'Invalid input'
8462
+ // sem saber por quê. O teto continua existindo porque o systemPrompt
8463
+ // entra em TODO hop do relay: é proteção de TPM, não de gosto.
8464
+ systemPrompt: external_exports.string().max(16e3).optional()
8465
+ });
8466
+ createProjectSchema.partial().extend({
8467
+ providerId: external_exports.string().min(1).nullable().optional(),
8468
+ systemPrompt: external_exports.string().max(16e3).nullable().optional()
8239
8469
  });
8240
- createProjectSchema.partial();
8241
8470
  var providerKindSchema = external_exports.enum(["openai", "anthropic", "custom"]);
8242
8471
  external_exports.object({
8243
8472
  name: external_exports.string().min(1).max(100),
@@ -8264,6 +8493,7 @@ external_exports.object({
8264
8493
  content: external_exports.record(external_exports.unknown())
8265
8494
  });
8266
8495
  var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
8496
+ var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
8267
8497
  external_exports.object({
8268
8498
  name: external_exports.string().min(1).max(100),
8269
8499
  // Required for `sdk` (default), forbidden/ignored for `operator` —
@@ -8271,6 +8501,7 @@ external_exports.object({
8271
8501
  // missing scope still validates as the pre-existing SDK shape.
8272
8502
  projectId: external_exports.string().min(1).optional(),
8273
8503
  scope: apiKeyScopeSchema.default("sdk"),
8504
+ environment: apiKeyEnvironmentSchema.optional(),
8274
8505
  expiresAt: external_exports.string().datetime().optional()
8275
8506
  }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
8276
8507
  message: 'projectId is required for scope "sdk"',
@@ -8278,13 +8509,23 @@ external_exports.object({
8278
8509
  });
8279
8510
  var boundedString = (max) => external_exports.string().max(max);
8280
8511
  var clientPlatformSchema = external_exports.enum(["react-native", "web", "android", "ios"]).or(external_exports.string().min(1).max(40));
8512
+ var identityProvenanceSchema = external_exports.enum(["declared", "derived", "positional"]).or(external_exports.string().min(1).max(40));
8281
8513
  var snapshotInputSchema = external_exports.object({
8282
8514
  id: boundedString(160).optional(),
8515
+ provenance: identityProvenanceSchema.optional(),
8516
+ /**
8517
+ * False when the control is mounted but currently OUTSIDE the window —
8518
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8519
+ * cannot tell (old React Native architecture, or any SDK published
8520
+ * before the field existed), which must never be read as `false`.
8521
+ */
8522
+ onScreen: external_exports.boolean().optional(),
8283
8523
  label: boundedString(300).optional(),
8284
8524
  value: boundedString(4096).optional(),
8285
8525
  placeholder: boundedString(300).optional(),
8286
8526
  editable: external_exports.boolean().optional(),
8287
8527
  secure: external_exports.boolean().optional(),
8528
+ submitsOnReturn: external_exports.boolean().optional(),
8288
8529
  type: boundedString(40).optional(),
8289
8530
  required: external_exports.boolean().optional(),
8290
8531
  invalid: external_exports.boolean().optional(),
@@ -8292,6 +8533,14 @@ var snapshotInputSchema = external_exports.object({
8292
8533
  }).passthrough();
8293
8534
  var snapshotButtonSchema = external_exports.object({
8294
8535
  id: boundedString(160).optional(),
8536
+ provenance: identityProvenanceSchema.optional(),
8537
+ /**
8538
+ * False when the control is mounted but currently OUTSIDE the window —
8539
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8540
+ * cannot tell (old React Native architecture, or any SDK published
8541
+ * before the field existed), which must never be read as `false`.
8542
+ */
8543
+ onScreen: external_exports.boolean().optional(),
8295
8544
  label: boundedString(300).optional(),
8296
8545
  disabled: external_exports.boolean().optional(),
8297
8546
  inModal: external_exports.boolean().optional(),
@@ -8301,12 +8550,28 @@ var snapshotButtonSchema = external_exports.object({
8301
8550
  }).passthrough();
8302
8551
  var snapshotToggleSchema = external_exports.object({
8303
8552
  id: boundedString(160).optional(),
8553
+ provenance: identityProvenanceSchema.optional(),
8554
+ /**
8555
+ * False when the control is mounted but currently OUTSIDE the window —
8556
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8557
+ * cannot tell (old React Native architecture, or any SDK published
8558
+ * before the field existed), which must never be read as `false`.
8559
+ */
8560
+ onScreen: external_exports.boolean().optional(),
8304
8561
  label: boundedString(300).optional(),
8305
8562
  value: external_exports.boolean().optional(),
8306
8563
  inModal: external_exports.boolean().optional()
8307
8564
  }).passthrough();
8308
8565
  var snapshotSliderSchema = external_exports.object({
8309
8566
  id: boundedString(160).optional(),
8567
+ provenance: identityProvenanceSchema.optional(),
8568
+ /**
8569
+ * False when the control is mounted but currently OUTSIDE the window —
8570
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8571
+ * cannot tell (old React Native architecture, or any SDK published
8572
+ * before the field existed), which must never be read as `false`.
8573
+ */
8574
+ onScreen: external_exports.boolean().optional(),
8310
8575
  label: boundedString(300).optional(),
8311
8576
  value: external_exports.number().finite().optional(),
8312
8577
  min: external_exports.number().finite().optional(),
@@ -8347,6 +8612,26 @@ var snapshotListSchema = external_exports.object({
8347
8612
  // should chase, and the relay only reads a known subset.
8348
8613
  items: external_exports.array(external_exports.record(external_exports.unknown())).max(500).optional()
8349
8614
  }).passthrough();
8615
+ var snapshotNativeDialogSchema = external_exports.object({
8616
+ id: boundedString(160),
8617
+ title: boundedString(300).optional(),
8618
+ message: boundedString(2e3).optional(),
8619
+ buttons: external_exports.array(
8620
+ external_exports.object({
8621
+ label: boundedString(160),
8622
+ style: external_exports.enum(["default", "cancel", "destructive"]).optional()
8623
+ }).passthrough()
8624
+ ).max(10)
8625
+ }).passthrough();
8626
+ var snapshotScrollableSchema = external_exports.object({
8627
+ id: boundedString(160),
8628
+ containerType: boundedString(60).optional(),
8629
+ label: boundedString(300).optional(),
8630
+ scrollOffsetY: external_exports.number().finite().optional(),
8631
+ canScrollUp: external_exports.boolean().optional(),
8632
+ canScrollDown: external_exports.boolean().optional(),
8633
+ horizontal: external_exports.boolean().optional()
8634
+ }).passthrough();
8350
8635
  var snapshotChoiceGroupSchema = external_exports.object({
8351
8636
  index: external_exports.number().int().optional(),
8352
8637
  id: boundedString(160).optional(),
@@ -8363,6 +8648,14 @@ var snapshotElementSchema = external_exports.object({
8363
8648
  disabled: external_exports.boolean().optional(),
8364
8649
  selected: external_exports.boolean().optional(),
8365
8650
  source: boundedString(40).optional(),
8651
+ provenance: identityProvenanceSchema.optional(),
8652
+ /**
8653
+ * False when the control is mounted but currently OUTSIDE the window —
8654
+ * below the fold, scrolled off, pushed sideways. Absent means the client
8655
+ * cannot tell (old React Native architecture, or any SDK published
8656
+ * before the field existed), which must never be read as `false`.
8657
+ */
8658
+ onScreen: external_exports.boolean().optional(),
8366
8659
  targetId: boundedString(160).optional(),
8367
8660
  listContext: external_exports.record(external_exports.unknown()).optional(),
8368
8661
  inModal: external_exports.boolean().optional()
@@ -8379,7 +8672,34 @@ var agentSnapshotSchema = external_exports.object({
8379
8672
  modalOpen: external_exports.boolean().optional(),
8380
8673
  lists: external_exports.array(snapshotListSchema).max(100).optional(),
8381
8674
  choiceGroups: external_exports.array(snapshotChoiceGroupSchema).max(100).optional(),
8382
- elements: external_exports.array(snapshotElementSchema).max(1e3).optional()
8675
+ scrollables: external_exports.array(snapshotScrollableSchema).max(20).optional(),
8676
+ nativeDialog: snapshotNativeDialogSchema.optional(),
8677
+ elements: external_exports.array(snapshotElementSchema).max(1e3).optional(),
8678
+ /**
8679
+ * The client clamped this observation to the caps above and lost
8680
+ * data doing it (`clampSnapshotToWireLimits` in
8681
+ * `@appilots/client-core`). Read by the prompt serializer so the
8682
+ * model is told the view is partial — a screen truncated at 500
8683
+ * texts must not be read as a screen that only has 500 things on
8684
+ * it. Optional: older SDKs never clamp and never send it.
8685
+ */
8686
+ truncated: external_exports.boolean().optional(),
8687
+ /**
8688
+ * How many of this screen's controls the client could name, split by
8689
+ * `identityProvenance`. Diagnostic only — the relay never grounds an
8690
+ * action on it — but it is the number that says whether an app needs
8691
+ * annotating, and comparing it between a debug and a release build is
8692
+ * the first direct measurement of what minification costs the agent.
8693
+ */
8694
+ stats: external_exports.object({
8695
+ visitedFibers: external_exports.number().int().nonnegative().optional(),
8696
+ skippedHidden: external_exports.number().int().nonnegative().optional(),
8697
+ identity: external_exports.object({
8698
+ declared: external_exports.number().int().nonnegative().optional(),
8699
+ derived: external_exports.number().int().nonnegative().optional(),
8700
+ positional: external_exports.number().int().nonnegative().optional()
8701
+ }).passthrough().optional()
8702
+ }).passthrough().optional()
8383
8703
  }).passthrough();
8384
8704
  var agentContextSchema = external_exports.object({
8385
8705
  /**
@@ -8429,6 +8749,19 @@ var agentContextSchema = external_exports.object({
8429
8749
  ).max(200).optional(),
8430
8750
  loadingPending: external_exports.boolean().optional()
8431
8751
  }).passthrough();
8752
+ var introspectionFailureReasonSchema = external_exports.enum([
8753
+ "sentinel-missing-internals",
8754
+ "never-mounted",
8755
+ "render-crashed",
8756
+ "names-mangled",
8757
+ "walk-recognized-nothing"
8758
+ ]);
8759
+ var introspectionDiagnosticsSchema = external_exports.object({
8760
+ captured: external_exports.boolean(),
8761
+ failureReason: introspectionFailureReasonSchema.nullish(),
8762
+ reactVersion: boundedString(32).nullish(),
8763
+ failureCount: external_exports.number().int().min(0).max(1e6).optional()
8764
+ });
8432
8765
  external_exports.object({
8433
8766
  content: external_exports.string().min(1).max(4096),
8434
8767
  sessionId: external_exports.string().optional(),
@@ -8438,7 +8771,20 @@ external_exports.object({
8438
8771
  * the active MCP doc and emits `mcp_version_mismatch` telemetry when
8439
8772
  * they differ. Optional — older SDKs simply skip the check.
8440
8773
  */
8441
- mcpVersion: external_exports.string().max(64).optional()
8774
+ mcpVersion: external_exports.string().max(64).optional(),
8775
+ /**
8776
+ * SDK-side introspection health. Only sent when it FAILED — a client
8777
+ * that reached the React tree omits the field entirely, so the happy
8778
+ * path costs nothing on the wire.
8779
+ *
8780
+ * Without this, an app whose introspection is dead sends a snapshot
8781
+ * that is empty but perfectly well-formed, indistinguishable from a
8782
+ * genuinely empty screen. The server emits
8783
+ * `introspection_unavailable` so the failure is visible to us instead
8784
+ * of waiting for a bug report. Optional — older SDKs simply never
8785
+ * send it.
8786
+ */
8787
+ introspection: introspectionDiagnosticsSchema.optional()
8442
8788
  });
8443
8789
  var agentActionTypeSchema = external_exports.enum([
8444
8790
  "navigate",
@@ -8449,11 +8795,22 @@ var agentActionTypeSchema = external_exports.enum([
8449
8795
  "custom"
8450
8796
  ]);
8451
8797
  var navigationPayloadSchema = external_exports.object({
8452
- screenName: external_exports.string().min(1),
8798
+ // Optional ONLY so `goBack` can exist (#398): it returns to whatever
8799
+ // is beneath on the stack and has no destination to name. Every other
8800
+ // action is still required to carry one — enforced by the refine
8801
+ // below rather than by the field, so the error names the real problem
8802
+ // ("navigate needs a screenName") instead of a missing key.
8803
+ screenName: external_exports.string().min(1).optional(),
8453
8804
  params: external_exports.record(external_exports.unknown()).optional(),
8454
8805
  navigationAction: external_exports.enum(["push", "navigate", "replace", "goBack", "reset"]).default("navigate").optional(),
8455
8806
  path: external_exports.array(external_exports.string().min(1)).optional()
8456
- }).passthrough();
8807
+ }).passthrough().refine(
8808
+ (payload) => payload.navigationAction === "goBack" || typeof payload.screenName === "string" && payload.screenName.length > 0,
8809
+ {
8810
+ message: 'screenName is required for every navigationAction except "goBack"',
8811
+ path: ["screenName"]
8812
+ }
8813
+ );
8457
8814
  var formFillFieldSchema = external_exports.object({
8458
8815
  fieldId: external_exports.string().min(1),
8459
8816
  fieldType: external_exports.enum(["text", "select", "toggle", "date", "number", "custom"]).default("text").optional(),
@@ -8787,6 +9144,8 @@ var sandboxObservationSchema = external_exports.object({
8787
9144
  inputs: external_exports.array(
8788
9145
  external_exports.object({
8789
9146
  id: external_exports.string().max(120),
9147
+ provenance: identityProvenanceSchema.optional(),
9148
+ onScreen: external_exports.boolean().optional(),
8790
9149
  label: external_exports.string().max(200).optional(),
8791
9150
  value: external_exports.string().max(2e3).optional(),
8792
9151
  disabled: external_exports.boolean().optional(),
@@ -8800,6 +9159,8 @@ var sandboxObservationSchema = external_exports.object({
8800
9159
  buttons: external_exports.array(
8801
9160
  external_exports.object({
8802
9161
  id: external_exports.string().max(120),
9162
+ provenance: identityProvenanceSchema.optional(),
9163
+ onScreen: external_exports.boolean().optional(),
8803
9164
  label: external_exports.string().max(200).optional(),
8804
9165
  disabled: external_exports.boolean().optional(),
8805
9166
  /** Risk metadata (e.g. "high") the SDK forwards for danger UI. */
@@ -8821,6 +9182,8 @@ var sandboxObservationSchema = external_exports.object({
8821
9182
  sliders: external_exports.array(
8822
9183
  external_exports.object({
8823
9184
  id: external_exports.string().max(120),
9185
+ provenance: identityProvenanceSchema.optional(),
9186
+ onScreen: external_exports.boolean().optional(),
8824
9187
  label: external_exports.string().max(200).optional(),
8825
9188
  value: external_exports.number().finite().optional(),
8826
9189
  min: external_exports.number().finite().optional(),
@@ -8850,6 +9213,44 @@ var sandboxObservationSchema = external_exports.object({
8850
9213
  ).max(200).optional()
8851
9214
  }).passthrough()
8852
9215
  ).max(20).optional(),
9216
+ /**
9217
+ * Toggles exactly as the SDK snapshot reports them (#390).
9218
+ *
9219
+ * Their absence is why `generality-component-control-smarthome`
9220
+ * came back `errored` with a 422 instead of measuring anything:
9221
+ * this schema is `.strict()`, so a scenario that declares a switch
9222
+ * is rejected before the agent is ever asked. Which means the
9223
+ * corpus could not express the single control behind failure 02 of
9224
+ * the functional audit — "disable an agent; the switch never
9225
+ * moves". The one defect the eval most needed to catch was the one
9226
+ * shape it could not describe.
9227
+ */
9228
+ toggles: external_exports.array(
9229
+ external_exports.object({
9230
+ id: external_exports.string().max(120),
9231
+ provenance: identityProvenanceSchema.optional(),
9232
+ onScreen: external_exports.boolean().optional(),
9233
+ label: external_exports.string().max(200).optional(),
9234
+ value: external_exports.boolean().optional(),
9235
+ disabled: external_exports.boolean().optional()
9236
+ })
9237
+ ).max(100).optional(),
9238
+ /**
9239
+ * The interaction graph — the `el:*` ids the prompt tells the model
9240
+ * to prefer over ordinal handles. Passthrough per entry for the
9241
+ * same reason `lists` is: the SDK adds fields faster than this
9242
+ * schema should chase them.
9243
+ */
9244
+ elements: external_exports.array(
9245
+ external_exports.object({
9246
+ id: external_exports.string().max(160),
9247
+ role: external_exports.string().max(40).optional(),
9248
+ label: external_exports.string().max(300).optional()
9249
+ }).passthrough()
9250
+ ).max(500).optional(),
9251
+ /** Screen-level state the relay renders and branches on. */
9252
+ loading: external_exports.boolean().optional(),
9253
+ modalOpen: external_exports.boolean().optional(),
8853
9254
  extra: external_exports.record(external_exports.unknown()).optional()
8854
9255
  }).strict();
8855
9256
  external_exports.object({
@@ -8890,13 +9291,29 @@ external_exports.object({
8890
9291
  * heuristics, forces tool choice when the last user message carries a
8891
9292
  * failure marker). Only meaningful alongside `history`.
8892
9293
  */
8893
- isRecoveryHop: external_exports.boolean().optional()
9294
+ isRecoveryHop: external_exports.boolean().optional(),
9295
+ /**
9296
+ * Run this call against the application structure supplied here
9297
+ * instead of the project's active MCP document.
9298
+ *
9299
+ * Sandbox-only, and the relay honours it only on a `dryRun` call —
9300
+ * see `resolveMcpOverride` in apps/api. It exists because the eval
9301
+ * corpus could otherwise only ever describe ONE app: every scenario
9302
+ * resolves to the same seeded project, so every navigate destination,
9303
+ * every screen title and every bit of domain priming in the prompt
9304
+ * came from the same vehicle-management demo. A suite that can only
9305
+ * pose questions about one app cannot measure whether the agent works
9306
+ * on another one.
9307
+ *
9308
+ * Deliberately loose (`z.record`): the MCP document's shape is owned
9309
+ * by the generator, and pinning it here would turn every additive
9310
+ * generator change into a 422 in the sandbox.
9311
+ */
9312
+ applicationStructure: external_exports.record(external_exports.unknown()).optional()
8894
9313
  }).strict();
8895
9314
  var relayDiagnosticsSchema = external_exports.object({
8896
9315
  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
- ),
9316
+ guardsFired: external_exports.array(external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })),
8900
9317
  repairMode: external_exports.enum(["repair", "strict", "disabled"]),
8901
9318
  promptVersion: external_exports.string(),
8902
9319
  // Defaulted for producers that predate the field: before it existed
@@ -9155,6 +9572,30 @@ external_exports.object({
9155
9572
  name: external_exports.string().min(1).max(200).optional(),
9156
9573
  identifiers: endUserIdentifiersSchema.optional()
9157
9574
  });
9575
+ var MAX_EXTERNAL_USER_ID_LENGTH = 128;
9576
+ function normalizeExternalUserId(value) {
9577
+ let raw;
9578
+ if (typeof value === "string") raw = value;
9579
+ else if (typeof value === "number" && Number.isFinite(value)) raw = String(value);
9580
+ else return void 0;
9581
+ const trimmed = raw.trim();
9582
+ if (!trimmed) return void 0;
9583
+ if (trimmed.length > MAX_EXTERNAL_USER_ID_LENGTH) return void 0;
9584
+ return trimmed;
9585
+ }
9586
+ var MAX_DEVICE_INFO_BYTES = 4096;
9587
+ var sessionDeviceInfoSchema = external_exports.object({
9588
+ platform: external_exports.string().max(64).optional(),
9589
+ osVersion: external_exports.string().max(64).optional(),
9590
+ appVersion: external_exports.string().max(64).optional(),
9591
+ sdkVersion: external_exports.string().max(64).optional()
9592
+ }).passthrough().refine((info) => JSON.stringify(info).length <= MAX_DEVICE_INFO_BYTES, {
9593
+ message: `device_info exceeds ${MAX_DEVICE_INFO_BYTES} bytes`
9594
+ });
9595
+ external_exports.object({
9596
+ userId: external_exports.preprocess(normalizeExternalUserId, external_exports.string().optional()),
9597
+ deviceInfo: sessionDeviceInfoSchema.optional().catch(void 0)
9598
+ });
9158
9599
  external_exports.object({
9159
9600
  projectId: external_exports.string().min(1).max(64),
9160
9601
  externalId: external_exports.string().min(1).max(128),
@@ -9162,6 +9603,27 @@ external_exports.object({
9162
9603
  identifiers: endUserIdentifiersSchema.optional()
9163
9604
  });
9164
9605
 
9606
+ // src/utils/stringify.ts
9607
+ function stringifyUnknown(value) {
9608
+ if (value instanceof Error) {
9609
+ const message = value.message || value.name;
9610
+ const cause = value.cause;
9611
+ if (cause instanceof Error && cause.message && cause.message !== message) {
9612
+ return `${message} (${cause.message})`;
9613
+ }
9614
+ return message;
9615
+ }
9616
+ if (typeof value === "string") return value;
9617
+ if (value === null || value === void 0) return String(value);
9618
+ if (typeof value !== "object") return String(value);
9619
+ try {
9620
+ const json = JSON.stringify(value);
9621
+ if (json && json !== "{}") return json;
9622
+ } catch {
9623
+ }
9624
+ return String(value);
9625
+ }
9626
+
9165
9627
  // src/manifest/loadManifest.ts
9166
9628
  var DEFAULT_MANIFEST_FILENAME = "appilots.manifest.json";
9167
9629
  var manifestSchema = external_exports.object({
@@ -9178,14 +9640,16 @@ async function loadManifest(rootDir, manifestPath) {
9178
9640
  if (error?.code === "ENOENT") {
9179
9641
  return { manifest: null, resolvedPath };
9180
9642
  }
9181
- throw new Error(`Failed to read Appilots manifest at ${resolvedPath}: ${error?.message ?? String(error)}`);
9643
+ throw new Error(
9644
+ `Failed to read Appilots manifest at ${resolvedPath}: ${stringifyUnknown(error)}`
9645
+ );
9182
9646
  }
9183
9647
  let parsedJson;
9184
9648
  try {
9185
9649
  parsedJson = JSON.parse(raw);
9186
9650
  } catch (error) {
9187
9651
  throw new Error(
9188
- `Appilots manifest at ${resolvedPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
9652
+ `Appilots manifest at ${resolvedPath} is not valid JSON: ${stringifyUnknown(error)}`
9189
9653
  );
9190
9654
  }
9191
9655
  const result = manifestSchema.safeParse(parsedJson);
@@ -9199,11 +9663,47 @@ Each entry in "screens" must match the ScreenDescriptor shape (see docs/platform
9199
9663
  }
9200
9664
  return { manifest: result.data, resolvedPath };
9201
9665
  }
9666
+ function mergeScreen(base, declared) {
9667
+ const fields = { ...base };
9668
+ for (const [key, value] of Object.entries(declared)) {
9669
+ if (value === void 0) continue;
9670
+ if (Array.isArray(value) && value.length === 0) continue;
9671
+ fields[key] = value;
9672
+ }
9673
+ const merged = fields;
9674
+ merged.targets = mergeById(base.targets, markManifestOrigin(declared.targets));
9675
+ merged.actions = mergeById(base.actions, declared.actions) ?? [];
9676
+ if (base.agentHints || declared.agentHints) {
9677
+ merged.agentHints = { ...base.agentHints, ...declared.agentHints };
9678
+ }
9679
+ return merged;
9680
+ }
9681
+ function mergeById(base, overlay) {
9682
+ if (!overlay || overlay.length === 0) return base;
9683
+ if (!base || base.length === 0) return overlay;
9684
+ const byId = /* @__PURE__ */ new Map();
9685
+ for (const entry of base) byId.set(entry.id, entry);
9686
+ for (const entry of overlay) {
9687
+ const existing = byId.get(entry.id);
9688
+ byId.set(entry.id, existing ? { ...existing, ...entry } : entry);
9689
+ }
9690
+ return Array.from(byId.values());
9691
+ }
9692
+ function markManifestOrigin(targets) {
9693
+ if (!targets || targets.length === 0) return targets;
9694
+ return targets.map((target) => ({ origin: "manifest", ...target }));
9695
+ }
9202
9696
  function mergeManifestScreens(analyzerScreens, manifestScreens) {
9203
9697
  if (!manifestScreens || manifestScreens.length === 0) return analyzerScreens;
9204
9698
  const byName = /* @__PURE__ */ new Map();
9205
9699
  for (const screen of analyzerScreens) byName.set(screen.name, screen);
9206
- for (const screen of manifestScreens) byName.set(screen.name, screen);
9700
+ for (const declared of manifestScreens) {
9701
+ const base = byName.get(declared.name);
9702
+ byName.set(
9703
+ declared.name,
9704
+ base ? mergeScreen(base, declared) : { ...declared, targets: markManifestOrigin(declared.targets) }
9705
+ );
9706
+ }
9207
9707
  return Array.from(byName.values());
9208
9708
  }
9209
9709
  function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
@@ -9493,7 +9993,12 @@ var MCPGenerator = class _MCPGenerator {
9493
9993
  }
9494
9994
  const mergedScreens = mergeManifestScreens(analyzed.screens, manifest?.screens);
9495
9995
  const navigation = mergeManifestNavigation(analyzed.navigation, manifest?.navigation);
9496
- const agentReadyScreens = mergedScreens.map(enrichScreenForAgent);
9996
+ const agentReadyScreens = mergedScreens.map(
9997
+ (screen) => enrichScreenForAgent({
9998
+ ...screen,
9999
+ filePath: screen.filePath ? path8__namespace.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
10000
+ })
10001
+ );
9497
10002
  const projectInfo = await this.getProjectInfo();
9498
10003
  const projectName = projectInfo.name;
9499
10004
  console.log(`[MCPGenerator] Project name: ${projectName}`);
@@ -9516,10 +10021,7 @@ var MCPGenerator = class _MCPGenerator {
9516
10021
  };
9517
10022
  const serialized = JSON.stringify(document, null, 2);
9518
10023
  const checksum = this.calculateChecksum(serialized);
9519
- const filePath = path8__namespace.default.resolve(
9520
- outputDir,
9521
- `mcp-document.${this.options.format}`
9522
- );
10024
+ const filePath = path8__namespace.default.resolve(outputDir, `mcp-document.${this.options.format}`);
9523
10025
  await fs.writeFile(filePath, serialized, "utf-8");
9524
10026
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
9525
10027
  const checksumFilePath = path8__namespace.default.resolve(outputDir, ".appilots-checksum");
@@ -9574,6 +10076,70 @@ var MCPGenerator = class _MCPGenerator {
9574
10076
  }
9575
10077
  }
9576
10078
  };
10079
+ var DEFAULT_SERVER_URL = "http://localhost:4000";
10080
+ var KNOWN_CONFIG_KEYS = [
10081
+ "apiKey",
10082
+ "projectId",
10083
+ "serverUrl",
10084
+ "outputDir",
10085
+ "include",
10086
+ "exclude",
10087
+ "autoActivate",
10088
+ "strictScreens",
10089
+ "screenPatterns",
10090
+ "navigationInclude",
10091
+ "navigationExclude",
10092
+ "platform",
10093
+ "manifestPath",
10094
+ "eval"
10095
+ ];
10096
+ var KEY_ALIASES = {
10097
+ apiUrl: "serverUrl",
10098
+ // `@appilots/sdk`'s own name for this. One `.appilotsrc` feeds both
10099
+ // packages, so an integrator who wrote the SDK's key should not be told
10100
+ // it is unrecognized.
10101
+ apiBaseUrl: "serverUrl",
10102
+ apiURL: "serverUrl",
10103
+ baseUrl: "serverUrl",
10104
+ url: "serverUrl",
10105
+ host: "serverUrl",
10106
+ endpoint: "serverUrl",
10107
+ server: "serverUrl",
10108
+ key: "apiKey",
10109
+ token: "apiKey",
10110
+ project: "projectId",
10111
+ sourceDir: "include",
10112
+ srcDir: "include",
10113
+ source: "include",
10114
+ outDir: "outputDir"
10115
+ };
10116
+ function editDistance(a, b) {
10117
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
10118
+ for (let i = 1; i <= a.length; i++) {
10119
+ const current = [i];
10120
+ for (let j = 1; j <= b.length; j++) {
10121
+ const deletion = (previous[j] ?? 0) + 1;
10122
+ const insertion = (current[j - 1] ?? 0) + 1;
10123
+ const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
10124
+ current.push(Math.min(deletion, insertion, substitution));
10125
+ }
10126
+ previous = current;
10127
+ }
10128
+ return previous[b.length] ?? 0;
10129
+ }
10130
+ function suggestConfigKey(key) {
10131
+ const alias = KEY_ALIASES[key];
10132
+ if (alias) return alias;
10133
+ const lower = key.toLowerCase();
10134
+ let best;
10135
+ for (const known of KNOWN_CONFIG_KEYS) {
10136
+ const distance = editDistance(lower, known.toLowerCase());
10137
+ if (distance <= 2 && (!best || distance < best.distance)) {
10138
+ best = { key: known, distance };
10139
+ }
10140
+ }
10141
+ return best?.key;
10142
+ }
9577
10143
  function getEnvOverrides(env = process.env) {
9578
10144
  const clean = (value) => {
9579
10145
  const trimmed = value?.trim();
@@ -9585,7 +10151,7 @@ function getEnvOverrides(env = process.env) {
9585
10151
  serverUrl: clean(env.APPILOTS_SERVER_URL)
9586
10152
  };
9587
10153
  }
9588
- function loadConfig() {
10154
+ function loadConfig(onWarn) {
9589
10155
  const configPath = getConfigPath();
9590
10156
  const env = getEnvOverrides();
9591
10157
  let fileConfig = {};
@@ -9593,13 +10159,27 @@ function loadConfig() {
9593
10159
  try {
9594
10160
  fileConfig = JSON.parse(fs$1.readFileSync(configPath, "utf-8"));
9595
10161
  } catch (error) {
9596
- throw new Error(`Failed to parse .appilotsrc at ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
10162
+ throw new Error(
10163
+ `Failed to parse .appilotsrc at ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
10164
+ );
9597
10165
  }
9598
10166
  } else if (!env.apiKey) {
9599
10167
  return null;
9600
10168
  }
10169
+ const legacyWarnings = [];
10170
+ if (fileConfig.serverUrl === void 0) {
10171
+ for (const [alias, canonical] of Object.entries(KEY_ALIASES)) {
10172
+ if (canonical !== "serverUrl" || fileConfig[alias] === void 0) continue;
10173
+ fileConfig.serverUrl = fileConfig[alias];
10174
+ delete fileConfig[alias];
10175
+ legacyWarnings.push(
10176
+ `legacy key "${alias}" \u2014 using it as "serverUrl"; rename it in .appilotsrc`
10177
+ );
10178
+ break;
10179
+ }
10180
+ }
9601
10181
  const merged = {
9602
- serverUrl: "http://localhost:4000",
10182
+ serverUrl: DEFAULT_SERVER_URL,
9603
10183
  outputDir: ".appilots",
9604
10184
  autoActivate: true,
9605
10185
  strictScreens: true,
@@ -9609,6 +10189,26 @@ function loadConfig() {
9609
10189
  ...env.serverUrl ? { serverUrl: env.serverUrl } : {}
9610
10190
  };
9611
10191
  const validation = validateConfig(merged);
10192
+ validation.warnings.unshift(...legacyWarnings);
10193
+ if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
10194
+ for (const key of Object.keys(fileConfig)) {
10195
+ if (KNOWN_CONFIG_KEYS.includes(key)) continue;
10196
+ if (suggestConfigKey(key) !== "serverUrl") continue;
10197
+ validation.warnings = validation.warnings.filter(
10198
+ (warning) => !warning.startsWith(`unknown key "${key}"`)
10199
+ );
10200
+ validation.errors.push(
10201
+ `unknown key "${key}" looks like "serverUrl" \u2014 not falling back to ${DEFAULT_SERVER_URL}; rename the key to "serverUrl"`
10202
+ );
10203
+ validation.valid = false;
10204
+ }
10205
+ }
10206
+ if (onWarn) {
10207
+ const source = configPath ?? "environment variables";
10208
+ for (const warning of validation.warnings) {
10209
+ onWarn(`${source}: ${warning}`);
10210
+ }
10211
+ }
9612
10212
  if (!validation.valid) {
9613
10213
  const source = configPath ?? "environment variables";
9614
10214
  throw new Error(
@@ -9624,7 +10224,7 @@ function saveConfig(dir, config) {
9624
10224
  const mergedConfig = {
9625
10225
  apiKey: config.apiKey || existingConfig?.apiKey || "",
9626
10226
  projectId: config.projectId || existingConfig?.projectId || "",
9627
- serverUrl: config.serverUrl || existingConfig?.serverUrl || "http://localhost:4000",
10227
+ serverUrl: config.serverUrl || existingConfig?.serverUrl || DEFAULT_SERVER_URL,
9628
10228
  outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
9629
10229
  autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
9630
10230
  include: config.include || existingConfig?.include,
@@ -9640,7 +10240,9 @@ function saveConfig(dir, config) {
9640
10240
  try {
9641
10241
  fs$1.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
9642
10242
  } catch (error) {
9643
- throw new Error(`Failed to write .appilotsrc to ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
10243
+ throw new Error(
10244
+ `Failed to write .appilotsrc to ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
10245
+ );
9644
10246
  }
9645
10247
  }
9646
10248
  function getConfigPath() {
@@ -9667,14 +10269,25 @@ function getConfigPath() {
9667
10269
  }
9668
10270
  function validateConfig(config) {
9669
10271
  const errors = [];
10272
+ const warnings = [];
9670
10273
  if (!config || typeof config !== "object") {
9671
10274
  return {
9672
10275
  valid: false,
9673
- errors: ["Configuration must be an object"]
10276
+ errors: ["Configuration must be an object"],
10277
+ warnings
9674
10278
  };
9675
10279
  }
10280
+ for (const key of Object.keys(config)) {
10281
+ if (KNOWN_CONFIG_KEYS.includes(key)) continue;
10282
+ const suggestion = suggestConfigKey(key);
10283
+ warnings.push(
10284
+ `unknown key "${key}" \u2014 ignored` + (suggestion ? ` (did you mean "${suggestion}"?)` : "")
10285
+ );
10286
+ }
9676
10287
  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)");
10288
+ errors.push(
10289
+ "apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)"
10290
+ );
9678
10291
  } else if (!config.apiKey.startsWith("ak_")) {
9679
10292
  errors.push('apiKey must start with "ak_"');
9680
10293
  }
@@ -9736,7 +10349,8 @@ function validateConfig(config) {
9736
10349
  }
9737
10350
  return {
9738
10351
  valid: errors.length === 0,
9739
- errors
10352
+ errors,
10353
+ warnings
9740
10354
  };
9741
10355
  }
9742
10356
 
@@ -9782,19 +10396,76 @@ function formatMetadataWarnings(warnings) {
9782
10396
  );
9783
10397
  }
9784
10398
 
10399
+ // ../shared/dist/chunk-UEMUBXFH.mjs
10400
+ var DEFAULT_API_BASE_URL = "https://api.appilots.com";
10401
+ var APPILOTS_API_PATH_PREFIX = "/api/v1";
10402
+ function normalizeApiBaseUrl(configured) {
10403
+ const raw = (configured ?? DEFAULT_API_BASE_URL).trim().replace(/\/+$/, "");
10404
+ if (!raw) return `${DEFAULT_API_BASE_URL}${APPILOTS_API_PATH_PREFIX}`;
10405
+ if (new RegExp(`${APPILOTS_API_PATH_PREFIX}(/|$)`).test(raw)) return raw;
10406
+ return `${raw}${APPILOTS_API_PATH_PREFIX}`;
10407
+ }
10408
+
9785
10409
  // src/services/api-client.ts
9786
- var DEFAULT_TIMEOUT_MS = 3e4;
10410
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
9787
10411
  var DEFAULT_MAX_RETRIES = 2;
9788
10412
  var RETRY_BASE_DELAY_MS = 500;
10413
+ function describeNetworkError(url, err) {
10414
+ let origin = url;
10415
+ try {
10416
+ origin = new URL(url).origin;
10417
+ } catch {
10418
+ }
10419
+ const cause = err?.cause;
10420
+ const code = typeof cause?.code === "string" ? cause.code : void 0;
10421
+ const detail = code ?? stringifyUnknown(err);
10422
+ return new Error(`Cannot reach Appilots server at ${origin} (${detail})`);
10423
+ }
10424
+ function errorCodeOf(payload) {
10425
+ const error = payload?.error;
10426
+ if (error && typeof error === "object") {
10427
+ const { code } = error;
10428
+ if (typeof code === "string" && code.trim()) return code;
10429
+ }
10430
+ return void 0;
10431
+ }
10432
+ function describeApiError(payload, fallback) {
10433
+ const error = payload?.error;
10434
+ if (typeof error === "string" && error.trim()) return error;
10435
+ if (error && typeof error === "object") {
10436
+ const { code, message: message2 } = error;
10437
+ const text = typeof message2 === "string" && message2.trim() ? message2 : void 0;
10438
+ const tag = typeof code === "string" && code.trim() ? code : void 0;
10439
+ if (text && tag) return `${text} (${tag})`;
10440
+ if (text) return text;
10441
+ if (tag) return tag;
10442
+ }
10443
+ const message = payload?.message;
10444
+ if (typeof message === "string" && message.trim()) return message;
10445
+ return fallback;
10446
+ }
9789
10447
  var AppilotsAPIClient = class {
9790
- serverUrl;
10448
+ /**
10449
+ * `serverUrl` with the API's mount prefix resolved — every path below
10450
+ * is relative to THIS, not to the configured origin.
10451
+ *
10452
+ * It used to be the raw `serverUrl` with `/api/v1` hardcoded into each
10453
+ * path, which made the field mean something different here than in the
10454
+ * SDK. Both read the same `.appilotsrc`: the SDK completes the prefix
10455
+ * when it is missing, so `https://api.appilots.com/api/v1` is correct
10456
+ * there — and here that same value produced
10457
+ * `/api/v1/api/v1/cli/sync`, a 404 whose body reads `Route not found`.
10458
+ * Sharing `normalizeApiBaseUrl` is what makes one file mean one thing:
10459
+ * with or without the prefix now works in both.
10460
+ */
10461
+ baseUrl;
9791
10462
  apiKey;
9792
10463
  timeoutMs;
9793
10464
  maxRetries;
9794
10465
  constructor(config) {
9795
- this.serverUrl = config.serverUrl.replace(/\/$/, "");
10466
+ this.baseUrl = normalizeApiBaseUrl(config.serverUrl);
9796
10467
  this.apiKey = config.apiKey;
9797
- this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10468
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
9798
10469
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
9799
10470
  }
9800
10471
  /**
@@ -9818,7 +10489,7 @@ var AppilotsAPIClient = class {
9818
10489
  }
9819
10490
  return response;
9820
10491
  } catch (err) {
9821
- lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : err;
10492
+ lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : describeNetworkError(url, err);
9822
10493
  } finally {
9823
10494
  clearTimeout(timer);
9824
10495
  }
@@ -9837,7 +10508,7 @@ var AppilotsAPIClient = class {
9837
10508
  */
9838
10509
  async sync(content, version, appVersion) {
9839
10510
  try {
9840
- const response = await this.request(`${this.serverUrl}/api/v1/cli/sync`, {
10511
+ const response = await this.request(`${this.baseUrl}/cli/sync`, {
9841
10512
  method: "POST",
9842
10513
  headers: {
9843
10514
  "Content-Type": "application/json",
@@ -9851,7 +10522,9 @@ var AppilotsAPIClient = class {
9851
10522
  return {
9852
10523
  success: false,
9853
10524
  unchanged: false,
9854
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
10525
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`),
10526
+ errorCode: errorCodeOf(errorData),
10527
+ errorStatus: response.status
9855
10528
  };
9856
10529
  }
9857
10530
  const json = await response.json();
@@ -9864,7 +10537,8 @@ var AppilotsAPIClient = class {
9864
10537
  return {
9865
10538
  success: false,
9866
10539
  unchanged: false,
9867
- error: error instanceof Error ? error.message : "Failed to sync with Appilots API"
10540
+ error: error instanceof Error ? error.message : "Failed to sync with Appilots API",
10541
+ errorStatus: 0
9868
10542
  };
9869
10543
  }
9870
10544
  }
@@ -9875,7 +10549,7 @@ var AppilotsAPIClient = class {
9875
10549
  */
9876
10550
  async status() {
9877
10551
  try {
9878
- const response = await this.request(`${this.serverUrl}/api/v1/cli/status`, {
10552
+ const response = await this.request(`${this.baseUrl}/cli/status`, {
9879
10553
  method: "GET",
9880
10554
  headers: {
9881
10555
  Authorization: `Bearer ${this.apiKey}`
@@ -9884,7 +10558,7 @@ var AppilotsAPIClient = class {
9884
10558
  if (!response.ok) {
9885
10559
  const errorData = await response.json().catch(() => ({}));
9886
10560
  return {
9887
- error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
10561
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
9888
10562
  };
9889
10563
  }
9890
10564
  const json = await response.json();
@@ -9906,7 +10580,7 @@ var AppilotsAPIClient = class {
9906
10580
  */
9907
10581
  async evalRun(scenarios) {
9908
10582
  try {
9909
- const response = await this.request(`${this.serverUrl}/api/v1/cli/eval/run`, {
10583
+ const response = await this.request(`${this.baseUrl}/cli/eval/run`, {
9910
10584
  method: "POST",
9911
10585
  headers: {
9912
10586
  "Content-Type": "application/json",
@@ -9937,7 +10611,7 @@ var AppilotsAPIClient = class {
9937
10611
  */
9938
10612
  async health() {
9939
10613
  try {
9940
- const response = await this.request(`${this.serverUrl}/api/v1/health`, {
10614
+ const response = await this.request(`${this.baseUrl}/health`, {
9941
10615
  method: "GET"
9942
10616
  });
9943
10617
  return response.ok;
@@ -9947,7 +10621,11 @@ var AppilotsAPIClient = class {
9947
10621
  }
9948
10622
  };
9949
10623
 
10624
+ // src/version.ts
10625
+ var CLI_VERSION = "0.11.0";
10626
+
9950
10627
  exports.AppilotsAPIClient = AppilotsAPIClient;
10628
+ exports.CLI_VERSION = CLI_VERSION;
9951
10629
  exports.ComponentAnalyzer = ComponentAnalyzer;
9952
10630
  exports.DEFAULT_MANIFEST_FILENAME = DEFAULT_MANIFEST_FILENAME;
9953
10631
  exports.DEFAULT_WEB_SCREEN_PATTERNS = DEFAULT_WEB_SCREEN_PATTERNS;