@appilots/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,3070 @@
1
+ import fs, { mkdir, writeFile, readFile } from 'fs/promises';
2
+ import * as path4 from 'path';
3
+ import path4__default, { join, resolve } from 'path';
4
+ import traverse4 from '@babel/traverse';
5
+ import * as BabelTypes from '@babel/types';
6
+ import glob from 'fast-glob';
7
+ import * as parser from '@babel/parser';
8
+ import { parse } from '@babel/parser';
9
+ import { promises, readFileSync, writeFileSync, existsSync, statSync } from 'fs';
10
+ import { createHash } from 'crypto';
11
+
12
+ // src/analyzers/ScreenAnalyzer.ts
13
+ var DEFAULT_PARSER_PLUGINS = [
14
+ "jsx",
15
+ "typescript",
16
+ ["decorators", { decoratorsBeforeExport: true }],
17
+ "asyncGenerators",
18
+ ["pipelineOperator", { proposal: "minimal" }],
19
+ "partialApplication",
20
+ "logicalAssignment",
21
+ "classPrivateProperties",
22
+ "classPrivateMethods",
23
+ ["recordAndTuple", { syntaxType: "hash" }]
24
+ ];
25
+ function parseSource(source, parserPlugins = []) {
26
+ return parse(source, {
27
+ sourceType: "module",
28
+ plugins: [...DEFAULT_PARSER_PLUGINS, ...parserPlugins]
29
+ });
30
+ }
31
+ function findJsxAttribute(element, name) {
32
+ return element.attributes.find(
33
+ (attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === name
34
+ );
35
+ }
36
+ function getStringAttr(element, name) {
37
+ const attr = findJsxAttribute(element, name);
38
+ if (!attr || !attr.value) return void 0;
39
+ if (BabelTypes.isStringLiteral(attr.value)) return attr.value.value;
40
+ if (BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isStringLiteral(attr.value.expression)) {
41
+ return attr.value.expression.value;
42
+ }
43
+ return void 0;
44
+ }
45
+ function getExpressionIdentifierAttr(element, name) {
46
+ const attr = findJsxAttribute(element, name);
47
+ if (!attr?.value || !BabelTypes.isJSXExpressionContainer(attr.value)) return void 0;
48
+ return BabelTypes.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
49
+ }
50
+ function hasJsxAttribute(element, name) {
51
+ return Boolean(findJsxAttribute(element, name));
52
+ }
53
+
54
+ // src/ast/jsx/classify.ts
55
+ var VIEW_COMPONENTS = /* @__PURE__ */ new Set(["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"]);
56
+ var INPUT_COMPONENTS = /* @__PURE__ */ new Set(["TextInput", "Input", "HocInput"]);
57
+ var BUTTON_COMPONENTS = /* @__PURE__ */ new Set(["TouchableOpacity", "Pressable", "Button"]);
58
+ var LIST_COMPONENTS = /* @__PURE__ */ new Set(["FlatList", "SectionList", "VirtualizedList", "FlashList"]);
59
+ var MODAL_COMPONENTS = /* @__PURE__ */ new Set(["Modal", "BottomSheet"]);
60
+ function classifyJsxComponent(name, element) {
61
+ if (LIST_COMPONENTS.has(name)) return "list";
62
+ if (MODAL_COMPONENTS.has(name)) return "modal";
63
+ if (/date/i.test(name)) return "date";
64
+ if (/(select|picker|dropdown|radio)/i.test(name)) return "select";
65
+ if (/(checkbox|switch|toggle)/i.test(name)) return "toggle";
66
+ if (INPUT_COMPONENTS.has(name)) return "input";
67
+ if (BUTTON_COMPONENTS.has(name)) return "button";
68
+ if (element) {
69
+ const hasOptions = hasJsxAttribute(element, "options");
70
+ const hasValue = hasJsxAttribute(element, "value");
71
+ const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
72
+ const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange") || hasJsxAttribute(element, "onChangeText");
73
+ const hasOnPress = hasJsxAttribute(element, "onPress");
74
+ if (hasOptions && (hasValue || hasOnChange)) return "select";
75
+ if (hasChecked && hasOnChange) return "toggle";
76
+ if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
77
+ return "input";
78
+ }
79
+ if (hasOnPress) return "button";
80
+ if (getStringAttr(element, "visible") || getExpressionIdentifierAttr(element, "visible")) {
81
+ return "modal";
82
+ }
83
+ }
84
+ if (VIEW_COMPONENTS.has(name)) return "view";
85
+ return "custom";
86
+ }
87
+ function collectFunctions(ast) {
88
+ const handlers = /* @__PURE__ */ new Map();
89
+ traverse4(ast, {
90
+ FunctionDeclaration: (nodePath) => {
91
+ if (nodePath.node.id?.name) handlers.set(nodePath.node.id.name, nodePath.node);
92
+ },
93
+ VariableDeclarator: (nodePath) => {
94
+ if (!BabelTypes.isIdentifier(nodePath.node.id)) return;
95
+ const init = nodePath.node.init;
96
+ if (BabelTypes.isArrowFunctionExpression(init) || BabelTypes.isFunctionExpression(init)) {
97
+ handlers.set(nodePath.node.id.name, init);
98
+ }
99
+ }
100
+ });
101
+ return handlers;
102
+ }
103
+ var DESTRUCTIVE_VERB = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
104
+ function analyzeFunctionBehavior(name, fn, handlers, seen = /* @__PURE__ */ new Set()) {
105
+ if (seen.has(name)) {
106
+ return {
107
+ appilotsInferred: { expectedOutcome: "none" },
108
+ destructive: false,
109
+ nativeConfirmationExpected: false
110
+ };
111
+ }
112
+ seen.add(name);
113
+ let hasAwait = fn.async === true;
114
+ let hasThen = false;
115
+ let hasStateSetter = false;
116
+ let nativeConfirmationExpected = false;
117
+ let targetScreen;
118
+ let successSignal;
119
+ let failureSignal;
120
+ let opensModal;
121
+ let destructive = DESTRUCTIVE_VERB.test(name);
122
+ const loadingStateBindings = /* @__PURE__ */ new Set();
123
+ const inspectCall = (node) => {
124
+ if (BabelTypes.isMemberExpression(node.callee) && BabelTypes.isIdentifier(node.callee.property)) {
125
+ const method = node.callee.property.name;
126
+ if (method === "then" || method === "catch" || method === "finally") hasThen = true;
127
+ if (BabelTypes.isIdentifier(node.callee.object) && node.callee.object.name === "navigation" && ["navigate", "push", "replace"].includes(method)) {
128
+ const firstArg = node.arguments[0];
129
+ if (BabelTypes.isStringLiteral(firstArg)) {
130
+ targetScreen = firstArg.value;
131
+ successSignal = { type: "navigation", target: firstArg.value };
132
+ }
133
+ }
134
+ if (BabelTypes.isIdentifier(node.callee.object) && node.callee.object.name === "navigation" && method === "goBack") {
135
+ successSignal = { type: "goBack", description: "Action returns to the previous screen" };
136
+ }
137
+ if (BabelTypes.isIdentifier(node.callee.object) && node.callee.object.name === "Alert" && method === "alert") {
138
+ nativeConfirmationExpected = true;
139
+ }
140
+ if (BabelTypes.isIdentifier(node.callee.object) && /(toast|notification|snackbar)/i.test(node.callee.object.name)) {
141
+ const firstArg = node.arguments[0];
142
+ const secondArg = node.arguments[1];
143
+ const text = BabelTypes.isStringLiteral(secondArg) ? secondArg.value : BabelTypes.isStringLiteral(firstArg) ? firstArg.value : void 0;
144
+ const signal = { type: "toast", description: text };
145
+ if (/(error|danger|fail)/i.test(method) || BabelTypes.isStringLiteral(firstArg) && /error/i.test(firstArg.value)) {
146
+ failureSignal = signal;
147
+ } else {
148
+ successSignal = successSignal ?? signal;
149
+ }
150
+ }
151
+ }
152
+ if (BabelTypes.isIdentifier(node.callee)) {
153
+ const calleeName = node.callee.name;
154
+ if (/^set[A-Z]/.test(calleeName)) {
155
+ hasStateSetter = true;
156
+ const stateName = setterToStateName(calleeName);
157
+ const firstArg = node.arguments[0];
158
+ if (stateName && BabelTypes.isBooleanLiteral(firstArg)) {
159
+ if (/loading|submitting|saving|fetching|refreshing/i.test(stateName)) {
160
+ loadingStateBindings.add(stateName);
161
+ }
162
+ if (firstArg.value === true && !/loading|submitting|saving|fetching|refreshing/i.test(stateName)) {
163
+ opensModal = opensModal ?? stateName;
164
+ }
165
+ }
166
+ }
167
+ if (DESTRUCTIVE_VERB.test(calleeName)) destructive = true;
168
+ const nested = handlers.get(calleeName);
169
+ if (nested) {
170
+ const nestedInfo = analyzeFunctionBehavior(calleeName, nested, handlers, seen);
171
+ hasAwait = hasAwait || nestedInfo.appilotsInferred.isAsyncTrigger === true;
172
+ nativeConfirmationExpected ||= nestedInfo.nativeConfirmationExpected;
173
+ destructive ||= nestedInfo.destructive;
174
+ targetScreen = targetScreen ?? nestedInfo.targetScreen;
175
+ successSignal = successSignal ?? nestedInfo.successSignal;
176
+ failureSignal = failureSignal ?? nestedInfo.failureSignal;
177
+ opensModal = opensModal ?? nestedInfo.opensModal;
178
+ for (const binding of nestedInfo.loadingStateBindings ?? []) {
179
+ loadingStateBindings.add(binding);
180
+ }
181
+ }
182
+ }
183
+ };
184
+ const inspectNode = (node) => {
185
+ if (BabelTypes.isAwaitExpression(node)) hasAwait = true;
186
+ if (BabelTypes.isCallExpression(node)) inspectCall(node);
187
+ if (BabelTypes.isMemberExpression(node) && BabelTypes.isIdentifier(node.property) && DESTRUCTIVE_VERB.test(node.property.name)) {
188
+ destructive = true;
189
+ }
190
+ };
191
+ if (fn.body) {
192
+ traverse4(fn.body, {
193
+ noScope: true,
194
+ enter: (nodePath) => inspectNode(nodePath.node)
195
+ });
196
+ }
197
+ const expectedOutcome = targetScreen ? "navigation" : successSignal?.type === "goBack" ? "navigation" : hasStateSetter || successSignal?.type === "toast" || opensModal ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
198
+ return {
199
+ appilotsInferred: {
200
+ isAsyncTrigger: hasAwait || hasThen || void 0,
201
+ expectedOutcome,
202
+ ...loadingStateBindings.size > 0 ? { loadingStateBindings: Array.from(loadingStateBindings).sort() } : {}
203
+ },
204
+ destructive,
205
+ nativeConfirmationExpected,
206
+ targetScreen,
207
+ successSignal,
208
+ failureSignal,
209
+ opensModal,
210
+ loadingStateBindings: Array.from(loadingStateBindings).sort()
211
+ };
212
+ }
213
+ function setterToStateName(setterName) {
214
+ const raw = setterName.replace(/^set/, "");
215
+ if (!raw) return void 0;
216
+ return raw.charAt(0).toLowerCase() + raw.slice(1);
217
+ }
218
+ function extractNavigationCalls(ast) {
219
+ const calls = [];
220
+ traverse4(ast, {
221
+ noScope: !BabelTypes.isFile(ast),
222
+ CallExpression: (nodePath) => {
223
+ const node = nodePath.node;
224
+ if (!BabelTypes.isMemberExpression(node.callee) || !BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !BabelTypes.isIdentifier(node.callee.property)) {
225
+ return;
226
+ }
227
+ const method = node.callee.property.name;
228
+ if (!["navigate", "push", "replace", "goBack"].includes(method)) return;
229
+ const firstArg = node.arguments[0];
230
+ const targetScreen = BabelTypes.isStringLiteral(firstArg) ? firstArg.value : void 0;
231
+ calls.push({
232
+ method,
233
+ ...targetScreen ? { targetScreen } : {},
234
+ params: extractNavigationParams(node.arguments[1])
235
+ });
236
+ }
237
+ });
238
+ return calls;
239
+ }
240
+ function extractNavigationParams(arg) {
241
+ const params = {};
242
+ if (!arg || !BabelTypes.isObjectExpression(arg)) return params;
243
+ for (const prop of arg.properties) {
244
+ if (!BabelTypes.isObjectProperty(prop)) continue;
245
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : void 0;
246
+ if (!key) continue;
247
+ if (BabelTypes.isMemberExpression(prop.value) && BabelTypes.isIdentifier(prop.value.object) && BabelTypes.isIdentifier(prop.value.property)) {
248
+ params[key] = `${prop.value.object.name}.${prop.value.property.name}`;
249
+ } else if (BabelTypes.isIdentifier(prop.value)) {
250
+ params[key] = prop.value.name;
251
+ } else if (BabelTypes.isStringLiteral(prop.value)) {
252
+ params[key] = prop.value.value;
253
+ }
254
+ }
255
+ return params;
256
+ }
257
+
258
+ // src/extractors/appilots/jsdoc.ts
259
+ function extractPermissionsFromJsDoc(source) {
260
+ if (!source.includes("@appilots-")) return void 0;
261
+ const result = {};
262
+ let touched = false;
263
+ const permRe = /@appilots-permissions\s+([^\n]+)/g;
264
+ let m;
265
+ while (m = permRe.exec(source)) {
266
+ const rawLine = (m[1] ?? "").replace(/\*\/?\s*$/, "").trim();
267
+ const parsed = parsePermissionLine(rawLine);
268
+ if (parsed) {
269
+ Object.assign(result, parsed);
270
+ touched = true;
271
+ break;
272
+ }
273
+ }
274
+ if (/@appilots-pii\b/.test(source)) {
275
+ result.isPii = true;
276
+ touched = true;
277
+ }
278
+ const blockedRe = /@appilots-blocked-actions\s+([^\n]+)/;
279
+ const blockedMatch = blockedRe.exec(source);
280
+ if (blockedMatch && blockedMatch[1]) {
281
+ const ids = blockedMatch[1].replace(/\*\/?\s*$/, "").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
282
+ if (ids.length > 0) {
283
+ result.blockedActions = ids;
284
+ touched = true;
285
+ }
286
+ }
287
+ return touched ? result : void 0;
288
+ }
289
+ function extractDestructiveJsDocTargets(source) {
290
+ if (!/@appilots-destructive\b/.test(source)) return void 0;
291
+ const out = /* @__PURE__ */ new Set();
292
+ const re = /@appilots-destructive\s*([^\n]*)/g;
293
+ let bareSeen = false;
294
+ let m;
295
+ while (m = re.exec(source)) {
296
+ const rest = (m[1] ?? "").replace(/\*\/?\s*$/, "").trim();
297
+ if (!rest) {
298
+ bareSeen = true;
299
+ continue;
300
+ }
301
+ for (const id of rest.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean)) {
302
+ out.add(id);
303
+ }
304
+ }
305
+ if (out.size === 0) return bareSeen ? "*" : void 0;
306
+ return out;
307
+ }
308
+ function parsePermissionLine(line) {
309
+ const trimmed = line.trim();
310
+ if (!trimmed) return null;
311
+ if (trimmed.startsWith("{")) {
312
+ const candidate = trimmed.replace(/'/g, '"');
313
+ try {
314
+ const obj = JSON.parse(candidate);
315
+ if (obj && typeof obj === "object") {
316
+ return sanitisePermissionsObject(obj);
317
+ }
318
+ } catch {
319
+ }
320
+ }
321
+ const out = {};
322
+ for (const pair of trimmed.split(/[,\s]+/)) {
323
+ const [k, v] = pair.split("=");
324
+ if (!k || v === void 0) continue;
325
+ const key = k.trim();
326
+ const value = v.trim().replace(/^['"]|['"]$/g, "");
327
+ if (value === "true") out[key] = true;
328
+ else if (value === "false") out[key] = false;
329
+ else out[key] = value;
330
+ }
331
+ return Object.keys(out).length > 0 ? sanitisePermissionsObject(out) : null;
332
+ }
333
+ function sanitisePermissionsObject(obj) {
334
+ const out = {};
335
+ if (typeof obj.agentAccess === "string" && (obj.agentAccess === "read" || obj.agentAccess === "write" || obj.agentAccess === "none")) {
336
+ out.agentAccess = obj.agentAccess;
337
+ }
338
+ if (typeof obj.requiresConfirmation === "boolean") {
339
+ out.requiresConfirmation = obj.requiresConfirmation;
340
+ }
341
+ if (typeof obj.isPii === "boolean") {
342
+ out.isPii = obj.isPii;
343
+ }
344
+ if (Array.isArray(obj.blockedActions)) {
345
+ const ids = obj.blockedActions.filter((x) => typeof x === "string" && !!x);
346
+ if (ids.length > 0) out.blockedActions = ids;
347
+ }
348
+ return out;
349
+ }
350
+
351
+ // src/analyzers/ScreenAnalyzer.ts
352
+ var ScreenAnalyzer = class {
353
+ config;
354
+ verbose = process.env.VERBOSE === "true";
355
+ /** When true, only files with registerScreen or matching screen patterns are included */
356
+ strictScreens;
357
+ /** Glob patterns that identify screen files in strict mode */
358
+ screenPatterns;
359
+ /** §D: Count of screens filtered out in strict mode (available after analyze()) */
360
+ screensFilteredOut = 0;
361
+ constructor(config, options) {
362
+ this.config = config;
363
+ this.strictScreens = options?.strictScreens ?? false;
364
+ this.screenPatterns = options?.screenPatterns ?? [
365
+ "**/*Screen.{ts,tsx}",
366
+ "**/screens/**/*.{ts,tsx}"
367
+ ];
368
+ }
369
+ /** Analyze all screens in the project */
370
+ async analyze() {
371
+ const { include = ["src/**/*Screen.{ts,tsx}"], exclude = ["node_modules/**"] } = this.config;
372
+ if (this.verbose) {
373
+ console.log(`[ScreenAnalyzer] Searching with patterns:`, include);
374
+ if (this.strictScreens) {
375
+ console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
376
+ }
377
+ }
378
+ const files = await glob(include, {
379
+ cwd: this.config.rootDir,
380
+ ignore: exclude
381
+ });
382
+ if (this.verbose) {
383
+ console.log(`[ScreenAnalyzer] Found ${files.length} files`);
384
+ }
385
+ let screenPatternFiles = null;
386
+ if (this.strictScreens) {
387
+ const matched = await glob(this.screenPatterns, {
388
+ cwd: this.config.rootDir,
389
+ ignore: exclude
390
+ });
391
+ screenPatternFiles = new Set(matched.map((f) => path4__default.resolve(this.config.rootDir, f)));
392
+ }
393
+ const screens = [];
394
+ this.screensFilteredOut = 0;
395
+ for (const file of files) {
396
+ const filePath = path4__default.resolve(this.config.rootDir, file);
397
+ try {
398
+ const descriptor = await this.analyzeFile(filePath);
399
+ if (!descriptor) continue;
400
+ if (this.strictScreens) {
401
+ const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
402
+ const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
403
+ if (!hasRegisterScreen && !matchesPattern) {
404
+ this.screensFilteredOut++;
405
+ if (this.verbose) {
406
+ console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file}`);
407
+ }
408
+ continue;
409
+ }
410
+ }
411
+ screens.push(descriptor);
412
+ if (this.verbose) {
413
+ console.log(`[ScreenAnalyzer] \u2713 Analyzed: ${descriptor.name} (${file})`);
414
+ }
415
+ } catch (error) {
416
+ if (this.verbose) {
417
+ console.warn(`[ScreenAnalyzer] Failed to parse ${file}:`, error instanceof Error ? error.message : error);
418
+ }
419
+ }
420
+ }
421
+ if (this.verbose && this.strictScreens) {
422
+ console.log(`[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`);
423
+ }
424
+ return screens;
425
+ }
426
+ /** Analyze a single file. Returns descriptor + whether registerScreen was found. */
427
+ async analyzeFile(filePath) {
428
+ const source = await fs.readFile(filePath, "utf-8");
429
+ const name = this.extractScreenName(filePath);
430
+ const ast = parseSource(source, this.config.parserPlugins);
431
+ const registerScreenMeta = this.extractRegisterScreenMetadata(ast);
432
+ const hasRegisterScreenCall = this.detectRegisterScreenCall(ast);
433
+ this.extractDefaultComponentName(ast);
434
+ const navigationTargets = this.extractNavigationTargets(ast);
435
+ const forms = this.mergeForms(registerScreenMeta?.forms ?? [], this.extractForms(ast));
436
+ const components = this.extractComponents(ast);
437
+ const actions = this.extractActions(ast, registerScreenMeta);
438
+ const collections = this.extractCollections(ast);
439
+ const permissionsFromJsDoc = extractPermissionsFromJsDoc(source);
440
+ const destructiveTags = extractDestructiveJsDocTargets(source);
441
+ if (destructiveTags) {
442
+ for (const action of actions) {
443
+ if (destructiveTags === "*" || destructiveTags.has(action.id)) {
444
+ action.destructive = true;
445
+ }
446
+ }
447
+ }
448
+ const descriptor = {
449
+ name: registerScreenMeta?.name || name,
450
+ filePath,
451
+ title: registerScreenMeta?.title,
452
+ description: registerScreenMeta?.description,
453
+ components,
454
+ forms,
455
+ actions,
456
+ navigationTargets,
457
+ ...collections.length > 0 ? { collections } : {},
458
+ ...registerScreenMeta?.suggestedPrompts && registerScreenMeta.suggestedPrompts.length > 0 ? { suggestedPrompts: registerScreenMeta.suggestedPrompts } : {},
459
+ ...permissionsFromJsDoc ? {
460
+ permissions: permissionsFromJsDoc,
461
+ ...permissionsFromJsDoc.isPii ? { isPii: true } : {}
462
+ } : {}
463
+ };
464
+ descriptor.__hasRegisterScreen = hasRegisterScreenCall;
465
+ return descriptor;
466
+ }
467
+ /**
468
+ * §B: Detect actual presence of registerScreen() call in the AST.
469
+ * This is more reliable than checking for title/description which are optional.
470
+ */
471
+ detectRegisterScreenCall(ast) {
472
+ let found = false;
473
+ traverse4(ast, {
474
+ CallExpression: (nodePath) => {
475
+ if (found) return;
476
+ const callee = nodePath.node.callee;
477
+ if (BabelTypes.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.property) && callee.property.name === "registerScreen") {
478
+ found = true;
479
+ nodePath.stop();
480
+ }
481
+ }
482
+ });
483
+ return found;
484
+ }
485
+ /**
486
+ * Extract metadata from registerScreen() call
487
+ */
488
+ extractRegisterScreenMetadata(ast) {
489
+ let metadata = null;
490
+ traverse4(ast, {
491
+ CallExpression: (nodePath) => {
492
+ const callee = nodePath.node.callee;
493
+ if (BabelTypes.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.property) && callee.property.name === "registerScreen") {
494
+ const arg = nodePath.node.arguments[0];
495
+ if (BabelTypes.isObjectExpression(arg)) {
496
+ metadata = this.parseRegisterScreenObject(arg);
497
+ }
498
+ }
499
+ }
500
+ });
501
+ return metadata;
502
+ }
503
+ /**
504
+ * Parse the object passed to registerScreen()
505
+ */
506
+ parseRegisterScreenObject(objExpr) {
507
+ const result = {
508
+ name: "",
509
+ actions: [],
510
+ forms: [],
511
+ navigationTargets: [],
512
+ components: []
513
+ };
514
+ for (const prop of objExpr.properties) {
515
+ if (!BabelTypes.isObjectProperty(prop) && !BabelTypes.isObjectMethod(prop)) {
516
+ continue;
517
+ }
518
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
519
+ if (!key) continue;
520
+ if (BabelTypes.isObjectProperty(prop)) {
521
+ const value = prop.value;
522
+ if (key === "name" && BabelTypes.isStringLiteral(value)) {
523
+ result.name = value.value;
524
+ } else if (key === "title" && BabelTypes.isStringLiteral(value)) {
525
+ result.title = value.value;
526
+ } else if (key === "description" && BabelTypes.isStringLiteral(value)) {
527
+ result.description = value.value;
528
+ } else if (key === "actions" && BabelTypes.isArrayExpression(value)) {
529
+ result.actions = this.parseActionsArray(value);
530
+ } else if (key === "fields" && BabelTypes.isArrayExpression(value)) {
531
+ const fields = this.parseFieldsArray(value);
532
+ if (fields.length > 0) {
533
+ result.forms = [
534
+ {
535
+ id: "default",
536
+ fields
537
+ }
538
+ ];
539
+ }
540
+ } else if (key === "suggestedPrompts" && BabelTypes.isArrayExpression(value)) {
541
+ const prompts = [];
542
+ for (const element of value.elements) {
543
+ if (BabelTypes.isStringLiteral(element)) {
544
+ const trimmed = element.value.trim();
545
+ if (trimmed.length > 0) prompts.push(trimmed);
546
+ }
547
+ }
548
+ if (prompts.length > 0) {
549
+ result.suggestedPrompts = prompts;
550
+ }
551
+ }
552
+ }
553
+ }
554
+ return result;
555
+ }
556
+ /**
557
+ * Parse actions array from registerScreen
558
+ */
559
+ parseActionsArray(arr) {
560
+ const actions = [];
561
+ for (const element of arr.elements) {
562
+ if (BabelTypes.isObjectExpression(element)) {
563
+ const action = this.parseActionObject(element);
564
+ if (action.id) {
565
+ actions.push(action);
566
+ }
567
+ }
568
+ }
569
+ return actions;
570
+ }
571
+ /**
572
+ * Parse a single action object
573
+ */
574
+ parseActionObject(obj) {
575
+ const action = {
576
+ id: "",
577
+ type: "custom"
578
+ };
579
+ for (const prop of obj.properties) {
580
+ if (!BabelTypes.isObjectProperty(prop)) continue;
581
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
582
+ if (!key) continue;
583
+ const value = prop.value;
584
+ if (key === "id" && BabelTypes.isStringLiteral(value)) {
585
+ action.id = value.value;
586
+ } else if (key === "label" && BabelTypes.isStringLiteral(value)) {
587
+ action.label = value.value;
588
+ } else if (key === "type" && BabelTypes.isStringLiteral(value)) {
589
+ action.type = value.value || "custom";
590
+ } else if (key === "targetScreen" && BabelTypes.isStringLiteral(value)) {
591
+ action.targetScreen = value.value;
592
+ } else if (key === "description" && BabelTypes.isStringLiteral(value)) {
593
+ action.description = value.value;
594
+ } else if (key === "handler" && BabelTypes.isStringLiteral(value)) {
595
+ action.handler = value.value;
596
+ } else if (key === "requiresConfirmation" && BabelTypes.isBooleanLiteral(value)) {
597
+ action.requiresConfirmation = value.value;
598
+ } else if (key === "destructive" && BabelTypes.isBooleanLiteral(value)) {
599
+ action.destructive = value.value;
600
+ } else if (key === "effect" && BabelTypes.isStringLiteral(value)) {
601
+ action.effect = value.value;
602
+ } else if (key === "riskLevel" && BabelTypes.isStringLiteral(value)) {
603
+ action.riskLevel = value.value;
604
+ } else if (key === "nativeConfirmationExpected" && BabelTypes.isBooleanLiteral(value)) {
605
+ action.nativeConfirmationExpected = value.value;
606
+ } else if (key === "appilotsInferred" && BabelTypes.isObjectExpression(value)) {
607
+ action.appilotsInferred = this.parseAppilotsInferredObject(value);
608
+ }
609
+ }
610
+ return action;
611
+ }
612
+ parseAppilotsInferredObject(obj) {
613
+ const out = {};
614
+ for (const prop of obj.properties) {
615
+ if (!BabelTypes.isObjectProperty(prop)) continue;
616
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
617
+ if (!key) continue;
618
+ const value = prop.value;
619
+ if (key === "isAsyncTrigger" && BabelTypes.isBooleanLiteral(value)) {
620
+ out.isAsyncTrigger = value.value;
621
+ } else if (key === "expectedOutcome" && BabelTypes.isStringLiteral(value)) {
622
+ out.expectedOutcome = value.value;
623
+ } else if (key === "loadingStateBindings" && BabelTypes.isArrayExpression(value)) {
624
+ out.loadingStateBindings = value.elements.filter((el) => BabelTypes.isStringLiteral(el)).map((el) => el.value);
625
+ }
626
+ }
627
+ return out;
628
+ }
629
+ /**
630
+ * Parse fields array from registerScreen
631
+ */
632
+ parseFieldsArray(arr) {
633
+ const fields = [];
634
+ for (const element of arr.elements) {
635
+ if (BabelTypes.isObjectExpression(element)) {
636
+ const field = this.parseFieldObject(element);
637
+ if (field.name) {
638
+ fields.push(field);
639
+ }
640
+ }
641
+ }
642
+ return fields;
643
+ }
644
+ /**
645
+ * Parse a single field object
646
+ */
647
+ parseFieldObject(obj) {
648
+ const field = {
649
+ name: "",
650
+ type: "text",
651
+ required: false
652
+ };
653
+ for (const prop of obj.properties) {
654
+ if (!BabelTypes.isObjectProperty(prop)) continue;
655
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
656
+ if (!key) continue;
657
+ const value = prop.value;
658
+ if (key === "id" && BabelTypes.isStringLiteral(value)) {
659
+ field.name = value.value;
660
+ } else if (key === "label" && BabelTypes.isStringLiteral(value)) {
661
+ field.label = value.value;
662
+ } else if (key === "type" && BabelTypes.isStringLiteral(value)) {
663
+ field.type = value.value || "text";
664
+ } else if (key === "required" && BabelTypes.isBooleanLiteral(value)) {
665
+ field.required = value.value;
666
+ } else if (key === "placeholder" && BabelTypes.isStringLiteral(value)) {
667
+ field.placeholder = value.value;
668
+ } else if (key === "defaultValue" && BabelTypes.isExpression(value)) {
669
+ field.defaultValue = this.extractLiteralValue(value);
670
+ } else if (key === "options" && BabelTypes.isArrayExpression(value)) {
671
+ field.options = this.parseOptionsArray(value);
672
+ }
673
+ }
674
+ return field;
675
+ }
676
+ /**
677
+ * Parse options array for select fields
678
+ */
679
+ parseOptionsArray(arr) {
680
+ const options = [];
681
+ for (const element of arr.elements) {
682
+ if (BabelTypes.isObjectExpression(element)) {
683
+ let label = "";
684
+ let value = "";
685
+ for (const prop of element.properties) {
686
+ if (!BabelTypes.isObjectProperty(prop)) continue;
687
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
688
+ if (!key) continue;
689
+ if (key === "label" && BabelTypes.isStringLiteral(prop.value)) {
690
+ label = prop.value.value;
691
+ } else if (key === "value" && BabelTypes.isStringLiteral(prop.value)) {
692
+ value = prop.value.value;
693
+ }
694
+ }
695
+ if (label && value) {
696
+ options.push({ label, value });
697
+ }
698
+ }
699
+ }
700
+ return options;
701
+ }
702
+ mergeForms(primary, secondary) {
703
+ const out = primary.map((form) => ({
704
+ ...form,
705
+ fields: [...form.fields]
706
+ }));
707
+ for (const form of secondary) {
708
+ const existing = out.find((candidate) => candidate.id === form.id) ?? this.findFormWithSharedFields(out, form);
709
+ if (!existing) {
710
+ out.push({ ...form, fields: [...form.fields], submitAction: this.namedSubmitAction(form.submitAction) });
711
+ continue;
712
+ }
713
+ for (const field of form.fields) {
714
+ const existingField = this.findEquivalentField(existing.fields, field);
715
+ if (existingField) {
716
+ this.mergeFieldMetadata(existingField, field);
717
+ } else {
718
+ existing.fields.push(field);
719
+ }
720
+ }
721
+ existing.submitAction = existing.submitAction ?? this.namedSubmitAction(form.submitAction);
722
+ existing.validationRules = form.validationRules ? { ...form.validationRules, ...existing.validationRules ?? {} } : existing.validationRules;
723
+ }
724
+ return out;
725
+ }
726
+ findEquivalentField(fields, incoming) {
727
+ return fields.find((field) => {
728
+ if (field.name && incoming.name && field.name === incoming.name) return true;
729
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
730
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
731
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
732
+ if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
733
+ return true;
734
+ }
735
+ return false;
736
+ });
737
+ }
738
+ namedSubmitAction(submitAction) {
739
+ return submitAction && submitAction !== "anonymous" ? submitAction : void 0;
740
+ }
741
+ mergeFieldMetadata(target, source) {
742
+ if (this.isWeakInferredFieldName(target.name) && !this.isWeakInferredFieldName(source.name)) {
743
+ target.name = source.name;
744
+ }
745
+ target.label = target.label ?? source.label;
746
+ target.placeholder = target.placeholder ?? source.placeholder;
747
+ target.options = target.options ?? source.options;
748
+ target.locator = target.locator ?? source.locator;
749
+ if (target.locator && source.locator) {
750
+ target.locator = this.mergeLocator(source.locator, target.locator);
751
+ }
752
+ target.sourceComponent = target.sourceComponent ?? source.sourceComponent;
753
+ target.valueBinding = target.valueBinding ?? source.valueBinding;
754
+ target.errorBinding = target.errorBinding ?? source.errorBinding;
755
+ target.required = target.required || source.required;
756
+ if (target.type === "text" && source.type !== "text") {
757
+ target.type = source.type;
758
+ }
759
+ }
760
+ findFormWithSharedFields(forms, incoming) {
761
+ if (incoming.fields.length === 0) return void 0;
762
+ let best;
763
+ for (const form of forms) {
764
+ const overlap = incoming.fields.filter(
765
+ (field) => this.findEquivalentField(form.fields, field)
766
+ ).length;
767
+ if (overlap > 0 && (!best || overlap > best.overlap)) {
768
+ best = { form, overlap };
769
+ }
770
+ }
771
+ return best?.form;
772
+ }
773
+ /**
774
+ * Extract the name of the default exported component
775
+ */
776
+ extractDefaultComponentName(ast) {
777
+ let componentName = "";
778
+ traverse4(ast, {
779
+ ExportDefaultDeclaration: (nodePath) => {
780
+ const declaration = nodePath.node.declaration;
781
+ if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id?.name) {
782
+ componentName = declaration.id.name;
783
+ } else if (BabelTypes.isArrowFunctionExpression(declaration)) {
784
+ componentName = "Component";
785
+ }
786
+ },
787
+ VariableDeclarator: (nodePath) => {
788
+ if (BabelTypes.isIdentifier(nodePath.node.id) && (BabelTypes.isArrowFunctionExpression(nodePath.node.init) || BabelTypes.isFunctionExpression(nodePath.node.init))) {
789
+ const parent = nodePath.parent;
790
+ if (BabelTypes.isVariableDeclaration(parent) && parent.declare === true) {
791
+ componentName = nodePath.node.id.name;
792
+ }
793
+ }
794
+ }
795
+ });
796
+ return componentName;
797
+ }
798
+ /**
799
+ * Extract navigation targets from navigation.navigate() calls
800
+ */
801
+ extractNavigationTargets(ast) {
802
+ const targets = /* @__PURE__ */ new Set();
803
+ traverse4(ast, {
804
+ CallExpression: (nodePath) => {
805
+ const callee = nodePath.node.callee;
806
+ if (BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.object) && callee.object.name === "navigation" && BabelTypes.isIdentifier(callee.property) && callee.property.name === "navigate") {
807
+ const arg = nodePath.node.arguments[0];
808
+ if (BabelTypes.isStringLiteral(arg)) {
809
+ targets.add(arg.value);
810
+ }
811
+ }
812
+ }
813
+ });
814
+ return Array.from(targets).sort();
815
+ }
816
+ /**
817
+ * Extract form information from JSX
818
+ */
819
+ extractForms(ast) {
820
+ const forms = [];
821
+ const fields = /* @__PURE__ */ new Map();
822
+ traverse4(ast, {
823
+ JSXOpeningElement: (nodePath) => {
824
+ const element = nodePath.node;
825
+ if (BabelTypes.isJSXIdentifier(element.name)) {
826
+ const componentName = element.name.name;
827
+ const role = classifyJsxComponent(componentName, element);
828
+ if (["input", "select", "toggle", "date"].includes(role)) {
829
+ const field = this.extractFieldFromInputElement(element);
830
+ if (field.name) {
831
+ fields.set(field.name, field);
832
+ }
833
+ }
834
+ }
835
+ }
836
+ });
837
+ if (fields.size > 0) {
838
+ forms.push({
839
+ id: "default",
840
+ fields: Array.from(fields.values())
841
+ });
842
+ }
843
+ return forms;
844
+ }
845
+ /**
846
+ * Extract field metadata from a TextInput/Input element
847
+ */
848
+ extractFieldFromInputElement(element) {
849
+ const field = {
850
+ name: "",
851
+ type: "text",
852
+ required: false
853
+ };
854
+ const componentName = BabelTypes.isJSXIdentifier(element.name) ? element.name.name : "";
855
+ const role = componentName ? classifyJsxComponent(componentName, element) : "custom";
856
+ field.sourceComponent = componentName || void 0;
857
+ for (const attr of element.attributes) {
858
+ if (!BabelTypes.isJSXAttribute(attr)) continue;
859
+ const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
860
+ if (!attrName) continue;
861
+ if (attr.value) {
862
+ if (BabelTypes.isStringLiteral(attr.value)) {
863
+ if (attrName === "placeholder") {
864
+ field.placeholder = attr.value.value;
865
+ } else if (attrName === "appilotsId") {
866
+ field.name = attr.value.value;
867
+ field.locator = this.mergeLocator(field.locator, {
868
+ id: attr.value.value,
869
+ appilotsId: attr.value.value,
870
+ source: "appilotsId"
871
+ });
872
+ } else if (attrName === "testID") {
873
+ if (!field.name) {
874
+ field.name = attr.value.value.replace(/^(input-|field-|txt-)/, "");
875
+ }
876
+ field.locator = this.mergeLocator(field.locator, {
877
+ id: field.name || attr.value.value,
878
+ testID: attr.value.value,
879
+ source: "testID"
880
+ });
881
+ } else if (attrName === "label") {
882
+ field.label = attr.value.value;
883
+ } else if (attrName === "accessibilityLabel") {
884
+ if (!field.label) field.label = attr.value.value;
885
+ field.locator = this.mergeLocator(field.locator, {
886
+ ...field.locator?.id ? {} : { id: this.slugifyActionId(attr.value.value) },
887
+ accessibilityLabel: attr.value.value,
888
+ source: field.locator?.source ?? "accessibilityLabel"
889
+ });
890
+ } else if (attrName === "keyboardType") {
891
+ if (attr.value.value === "email-address" || attr.value.value === "email") field.type = "email";
892
+ if (attr.value.value === "phone-pad") field.type = "phone";
893
+ if (attr.value.value === "numeric" || attr.value.value === "number-pad") field.type = "number";
894
+ }
895
+ } else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
896
+ if (attrName === "value" && BabelTypes.isIdentifier(attr.value.expression)) {
897
+ if (!field.name) {
898
+ field.name = attr.value.expression.name;
899
+ }
900
+ field.valueBinding = attr.value.expression.name;
901
+ } else if (attrName === "checked" && BabelTypes.isIdentifier(attr.value.expression)) {
902
+ if (!field.name) field.name = attr.value.expression.name;
903
+ field.valueBinding = attr.value.expression.name;
904
+ } else if (attrName === "error") {
905
+ field.errorBinding = this.expressionToBinding(attr.value.expression);
906
+ } else if (attrName === "required" && BabelTypes.isBooleanLiteral(attr.value.expression)) {
907
+ field.required = attr.value.expression.value;
908
+ }
909
+ }
910
+ } else if (attrName === "required") {
911
+ field.required = true;
912
+ }
913
+ }
914
+ if (role === "select") field.type = "select";
915
+ if (role === "toggle") field.type = "toggle";
916
+ if (role === "date") field.type = "date";
917
+ if (!field.name || this.isWeakInferredFieldName(field.name)) {
918
+ const label = field.locator?.accessibilityLabel ?? field.label ?? field.placeholder;
919
+ if (label) field.name = this.slugifyActionId(label);
920
+ }
921
+ if (!field.locator && field.name) {
922
+ field.locator = { id: field.name, label: field.label, source: "inferred" };
923
+ } else if (field.locator && !field.locator.id && field.name) {
924
+ field.locator = this.mergeLocator(field.locator, {
925
+ id: field.name,
926
+ label: field.label,
927
+ source: field.locator.source ?? "inferred"
928
+ });
929
+ }
930
+ return field;
931
+ }
932
+ isWeakInferredFieldName(name) {
933
+ return /^(text|value|input|query|search|selected|checked)$/i.test(name);
934
+ }
935
+ /**
936
+ * Extract component structure from JSX
937
+ */
938
+ extractComponents(ast) {
939
+ const components = [];
940
+ const seen = /* @__PURE__ */ new Set();
941
+ traverse4(ast, {
942
+ JSXOpeningElement: (nodePath) => {
943
+ const element = nodePath.node;
944
+ if (BabelTypes.isJSXIdentifier(element.name)) {
945
+ const componentName = element.name.name;
946
+ if (seen.has(componentName)) return;
947
+ seen.add(componentName);
948
+ const type = this.inferComponentType(componentName);
949
+ const component = {
950
+ name: componentName,
951
+ type
952
+ };
953
+ for (const attr of element.attributes) {
954
+ if (!BabelTypes.isJSXAttribute(attr)) continue;
955
+ const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
956
+ if (!attrName) continue;
957
+ if (attr.value && BabelTypes.isStringLiteral(attr.value)) {
958
+ if (attrName === "testID") {
959
+ component.testID = attr.value.value;
960
+ } else if (attrName === "accessibilityLabel") {
961
+ component.accessibilityLabel = attr.value.value;
962
+ }
963
+ }
964
+ }
965
+ components.push(component);
966
+ }
967
+ }
968
+ });
969
+ return components;
970
+ }
971
+ /**
972
+ * Infer component type from component name
973
+ */
974
+ inferComponentType(name) {
975
+ const role = classifyJsxComponent(name);
976
+ if (role === "select" || role === "toggle" || role === "date") {
977
+ return "input";
978
+ }
979
+ if (role === "input") {
980
+ return "input";
981
+ }
982
+ if (role === "button") {
983
+ return "button";
984
+ }
985
+ if (role === "list") {
986
+ return "list";
987
+ }
988
+ if (role === "modal") {
989
+ return "modal";
990
+ }
991
+ if (role === "view") {
992
+ return "view";
993
+ }
994
+ return "custom";
995
+ }
996
+ /**
997
+ * Extract actions from button/touchable elements
998
+ */
999
+ extractActions(ast, registerScreenMeta) {
1000
+ const actions = [...registerScreenMeta?.actions || []];
1001
+ const actionIds = new Set(actions.map((a) => a.id));
1002
+ const actionLabels = new Map(
1003
+ actions.filter((a) => a.label).map((a) => [this.normalizeLabel(a.label), a])
1004
+ );
1005
+ traverse4(ast, {
1006
+ JSXOpeningElement: (nodePath) => {
1007
+ const element = nodePath.node;
1008
+ if (BabelTypes.isJSXIdentifier(element.name)) {
1009
+ const componentName = element.name.name;
1010
+ if (classifyJsxComponent(componentName, element) === "button") {
1011
+ const action = this.extractActionFromButton(element);
1012
+ const existingByLabel = action.label ? actionLabels.get(this.normalizeLabel(action.label)) : void 0;
1013
+ if (existingByLabel) {
1014
+ this.mergeActionMetadata(existingByLabel, action);
1015
+ return;
1016
+ }
1017
+ if (action.id && !actionIds.has(action.id)) {
1018
+ actions.push(action);
1019
+ actionIds.add(action.id);
1020
+ if (action.label) actionLabels.set(this.normalizeLabel(action.label), action);
1021
+ }
1022
+ }
1023
+ }
1024
+ }
1025
+ });
1026
+ this.enrichActionsFromHandlers(ast, actions);
1027
+ return actions;
1028
+ }
1029
+ normalizeLabel(label) {
1030
+ return label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
1031
+ }
1032
+ mergeActionMetadata(target, source) {
1033
+ target.handler = target.handler ?? source.handler;
1034
+ target.targetScreen = target.targetScreen ?? source.targetScreen;
1035
+ target.description = target.description ?? source.description;
1036
+ target.nativeConfirmationExpected = target.nativeConfirmationExpected || source.nativeConfirmationExpected || void 0;
1037
+ target.requiresConfirmation = target.requiresConfirmation || source.requiresConfirmation || void 0;
1038
+ target.destructive = target.destructive || source.destructive || void 0;
1039
+ target.effect = target.effect ?? source.effect;
1040
+ target.riskLevel = target.riskLevel ?? source.riskLevel;
1041
+ target.appilotsInferred = target.appilotsInferred ?? source.appilotsInferred;
1042
+ }
1043
+ enrichActionsFromHandlers(ast, actions) {
1044
+ const handlerMap = this.collectHandlerInfo(ast);
1045
+ const labelToHandler = this.collectButtonHandlersByLabel(ast);
1046
+ for (const action of actions) {
1047
+ if (!action.handler && action.label) {
1048
+ const handler2 = labelToHandler.get(action.label);
1049
+ if (handler2) action.handler = handler2;
1050
+ }
1051
+ const handler = action.handler ? handlerMap.get(action.handler) : void 0;
1052
+ if (!handler) continue;
1053
+ action.appilotsInferred = {
1054
+ ...action.appilotsInferred ?? {},
1055
+ ...handler.appilotsInferred
1056
+ };
1057
+ if (handler.nativeConfirmationExpected) action.nativeConfirmationExpected = true;
1058
+ if (handler.targetScreen && !action.targetScreen) action.targetScreen = handler.targetScreen;
1059
+ if (handler.successSignal && !action.successSignal) action.successSignal = handler.successSignal;
1060
+ if (handler.failureSignal && !action.failureSignal) action.failureSignal = handler.failureSignal;
1061
+ if (handler.opensModal && !action.opensModal) action.opensModal = handler.opensModal;
1062
+ if (handler.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
1063
+ action.destructive = true;
1064
+ action.effect = action.effect ?? "destructive";
1065
+ action.riskLevel = action.riskLevel ?? "high";
1066
+ action.requiresConfirmation = action.requiresConfirmation ?? true;
1067
+ }
1068
+ }
1069
+ }
1070
+ collectButtonHandlersByLabel(ast) {
1071
+ const out = /* @__PURE__ */ new Map();
1072
+ traverse4(ast, {
1073
+ JSXOpeningElement: (nodePath) => {
1074
+ const element = nodePath.node;
1075
+ if (!BabelTypes.isJSXIdentifier(element.name)) return;
1076
+ const componentName = element.name.name;
1077
+ if (classifyJsxComponent(componentName, element) !== "button") return;
1078
+ let label;
1079
+ let handler;
1080
+ for (const attr of element.attributes) {
1081
+ if (!BabelTypes.isJSXAttribute(attr)) continue;
1082
+ const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
1083
+ if (!attrName || !attr.value) continue;
1084
+ if ((attrName === "title" || attrName === "accessibilityLabel" || attrName === "label") && BabelTypes.isStringLiteral(attr.value)) {
1085
+ label = attr.value.value;
1086
+ }
1087
+ if (attrName === "onPress" && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
1088
+ handler = attr.value.expression.name;
1089
+ }
1090
+ }
1091
+ if (label && handler) out.set(label, handler);
1092
+ }
1093
+ });
1094
+ return out;
1095
+ }
1096
+ collectHandlerInfo(ast) {
1097
+ const handlers = collectFunctions(ast);
1098
+ const out = /* @__PURE__ */ new Map();
1099
+ for (const [name, fn] of handlers) {
1100
+ const info = analyzeFunctionBehavior(name, fn, handlers);
1101
+ out.set(name, info);
1102
+ }
1103
+ return out;
1104
+ }
1105
+ analyzeHandlerFunction(name, fn, handlers, seen = /* @__PURE__ */ new Set()) {
1106
+ if (seen.has(name)) {
1107
+ return {
1108
+ appilotsInferred: { expectedOutcome: "none" },
1109
+ destructive: false,
1110
+ nativeConfirmationExpected: false
1111
+ };
1112
+ }
1113
+ seen.add(name);
1114
+ let hasAwait = fn.async === true;
1115
+ let hasThen = false;
1116
+ let hasStateSetter = false;
1117
+ let nativeConfirmationExpected = false;
1118
+ let targetScreen;
1119
+ let destructive = /(delete|destroy|remove|discard|wipe|drop|terminate)/i.test(name);
1120
+ const inspectNode = (node) => {
1121
+ if (BabelTypes.isAwaitExpression(node)) hasAwait = true;
1122
+ if (BabelTypes.isCallExpression(node) && BabelTypes.isMemberExpression(node.callee) && BabelTypes.isIdentifier(node.callee.property)) {
1123
+ const method = node.callee.property.name;
1124
+ if (method === "then" || method === "catch" || method === "finally") hasThen = true;
1125
+ if (BabelTypes.isIdentifier(node.callee.object) && node.callee.object.name === "navigation" && ["navigate", "push", "replace"].includes(method)) {
1126
+ const firstArg = node.arguments[0];
1127
+ if (BabelTypes.isStringLiteral(firstArg)) targetScreen = firstArg.value;
1128
+ }
1129
+ if (BabelTypes.isIdentifier(node.callee.object) && node.callee.object.name === "Alert" && method === "alert") {
1130
+ nativeConfirmationExpected = true;
1131
+ }
1132
+ }
1133
+ if (BabelTypes.isCallExpression(node) && BabelTypes.isIdentifier(node.callee)) {
1134
+ const calleeName = node.callee.name;
1135
+ if (/^set[A-Z]/.test(calleeName)) hasStateSetter = true;
1136
+ if (/(delete|destroy|remove|discard|wipe|drop|terminate)/i.test(calleeName)) {
1137
+ destructive = true;
1138
+ }
1139
+ const nested = handlers.get(calleeName);
1140
+ if (nested) {
1141
+ const nestedInfo = this.analyzeHandlerFunction(calleeName, nested, handlers, seen);
1142
+ hasAwait = hasAwait || nestedInfo.appilotsInferred.isAsyncTrigger === true;
1143
+ nativeConfirmationExpected ||= nestedInfo.nativeConfirmationExpected;
1144
+ destructive ||= nestedInfo.destructive;
1145
+ targetScreen = targetScreen ?? nestedInfo.targetScreen;
1146
+ }
1147
+ }
1148
+ if (BabelTypes.isMemberExpression(node) && BabelTypes.isIdentifier(node.property) && /(delete|destroy|remove|discard|wipe|drop|terminate)/i.test(node.property.name)) {
1149
+ destructive = true;
1150
+ }
1151
+ };
1152
+ if (fn.body) {
1153
+ traverse4(fn.body, {
1154
+ noScope: true,
1155
+ enter: (nodePath) => inspectNode(nodePath.node)
1156
+ });
1157
+ }
1158
+ const expectedOutcome = targetScreen ? "navigation" : hasStateSetter ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
1159
+ return {
1160
+ appilotsInferred: {
1161
+ isAsyncTrigger: hasAwait || hasThen || void 0,
1162
+ expectedOutcome
1163
+ },
1164
+ destructive,
1165
+ nativeConfirmationExpected,
1166
+ targetScreen
1167
+ };
1168
+ }
1169
+ /**
1170
+ * BACKLOG 2.2 — heuristic destructive detection.
1171
+ *
1172
+ * Combines several signals:
1173
+ * - JSX `destructive` boolean prop (`<Pressable destructive />`)
1174
+ * - JSX `aria-destructive` attribute
1175
+ * - Handler name contains a generic destructive verb
1176
+ * (delete/destroy/remove/discard/wipe/revoke/etc.). `cancel` is intentionally excluded —
1177
+ * too many "Cancelar" buttons that just dismiss modals.
1178
+ * - testID / id contains a destructive verb
1179
+ *
1180
+ * Default-allow with explicit opt-out via `@appilots-non-destructive`
1181
+ * is more dangerous than the inverse, so we default-deny: the
1182
+ * heuristic must hit OR the JSDoc tag must be present.
1183
+ */
1184
+ isElementDestructive(element, handlerName, actionId) {
1185
+ const DESTRUCTIVE_VERB2 = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
1186
+ for (const attr of element.attributes) {
1187
+ if (!BabelTypes.isJSXAttribute(attr)) continue;
1188
+ const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
1189
+ if (attrName === "destructive" || attrName === "aria-destructive") {
1190
+ if (attr.value === null) return true;
1191
+ if (BabelTypes.isJSXExpressionContainer(attr.value)) {
1192
+ const expr = attr.value.expression;
1193
+ if (BabelTypes.isBooleanLiteral(expr)) return expr.value;
1194
+ }
1195
+ if (BabelTypes.isStringLiteral(attr.value)) {
1196
+ return attr.value.value === "true";
1197
+ }
1198
+ }
1199
+ }
1200
+ if (handlerName && DESTRUCTIVE_VERB2.test(handlerName)) return true;
1201
+ if (actionId && DESTRUCTIVE_VERB2.test(actionId)) return true;
1202
+ return false;
1203
+ }
1204
+ /**
1205
+ * Extract action metadata from a button element
1206
+ */
1207
+ extractActionFromButton(element) {
1208
+ const action = {
1209
+ id: "",
1210
+ type: "custom"
1211
+ };
1212
+ const componentName = BabelTypes.isJSXIdentifier(element.name) ? element.name.name : void 0;
1213
+ action.sourceComponent = componentName;
1214
+ let handlerName;
1215
+ for (const attr of element.attributes) {
1216
+ if (!BabelTypes.isJSXAttribute(attr)) continue;
1217
+ const attrName = BabelTypes.isJSXIdentifier(attr.name) ? attr.name.name : null;
1218
+ if (!attrName) continue;
1219
+ if (attr.value) {
1220
+ if (BabelTypes.isStringLiteral(attr.value)) {
1221
+ if (attrName === "appilotsId") {
1222
+ action.id = attr.value.value;
1223
+ action.locator = this.mergeLocator(action.locator, {
1224
+ id: attr.value.value,
1225
+ appilotsId: attr.value.value,
1226
+ source: "appilotsId"
1227
+ });
1228
+ } else if (attrName === "testID") {
1229
+ action.id = attr.value.value;
1230
+ action.locator = this.mergeLocator(action.locator, {
1231
+ id: attr.value.value,
1232
+ testID: attr.value.value,
1233
+ source: "testID"
1234
+ });
1235
+ } else if (attrName === "title" || attrName === "label" || attrName === "accessibilityLabel") {
1236
+ action.label = attr.value.value;
1237
+ if (attrName === "accessibilityLabel") {
1238
+ action.locator = this.mergeLocator(action.locator, {
1239
+ accessibilityLabel: attr.value.value,
1240
+ source: "accessibilityLabel"
1241
+ });
1242
+ }
1243
+ if (!action.id) {
1244
+ action.id = this.slugifyActionId(attr.value.value);
1245
+ }
1246
+ }
1247
+ } else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
1248
+ if (attrName === "onPress" && BabelTypes.isIdentifier(attr.value.expression)) {
1249
+ handlerName = attr.value.expression.name.toLowerCase();
1250
+ action.handler = attr.value.expression.name;
1251
+ if (handlerName.includes("submit")) {
1252
+ action.type = "submit";
1253
+ } else if (handlerName.includes("navigate")) {
1254
+ action.type = "navigation";
1255
+ }
1256
+ } else if (attrName === "onPress") {
1257
+ const inline = this.extractInlineOnPressMetadata(attr.value.expression);
1258
+ if (inline.handler) {
1259
+ handlerName = inline.handler.toLowerCase();
1260
+ action.handler = inline.handler;
1261
+ if (handlerName.includes("submit")) action.type = "submit";
1262
+ }
1263
+ if (inline.targetScreen) {
1264
+ action.type = "navigation";
1265
+ action.targetScreen = inline.targetScreen;
1266
+ }
1267
+ } else if (attrName === "loading" && BabelTypes.isIdentifier(attr.value.expression)) {
1268
+ action.appilotsInferred = {
1269
+ ...action.appilotsInferred ?? {},
1270
+ isAsyncTrigger: true,
1271
+ loadingStateBindings: [attr.value.expression.name]
1272
+ };
1273
+ }
1274
+ }
1275
+ } else if (attrName === "destructive") {
1276
+ action.destructive = true;
1277
+ }
1278
+ }
1279
+ if (!action.locator && action.id) {
1280
+ action.locator = { id: action.id, label: action.label, source: action.label ? "label" : "inferred" };
1281
+ }
1282
+ if (this.isElementDestructive(element, handlerName, action.id)) {
1283
+ action.destructive = true;
1284
+ }
1285
+ return action;
1286
+ }
1287
+ extractInlineOnPressMetadata(expr) {
1288
+ if (BabelTypes.isJSXEmptyExpression(expr)) return {};
1289
+ const navCall = extractNavigationCalls(expr).find((call) => call.targetScreen);
1290
+ const handler = this.extractFirstHandlerName(expr);
1291
+ return {
1292
+ ...handler ? { handler } : {},
1293
+ ...navCall?.targetScreen ? { targetScreen: navCall.targetScreen } : {}
1294
+ };
1295
+ }
1296
+ extractFirstHandlerName(node) {
1297
+ if (BabelTypes.isIdentifier(node)) return node.name;
1298
+ if (BabelTypes.isArrowFunctionExpression(node) || BabelTypes.isFunctionExpression(node)) {
1299
+ return this.extractFirstHandlerName(node.body);
1300
+ }
1301
+ if (BabelTypes.isBlockStatement(node)) {
1302
+ for (const statement of node.body) {
1303
+ const handler = this.extractFirstHandlerName(statement);
1304
+ if (handler) return handler;
1305
+ }
1306
+ return void 0;
1307
+ }
1308
+ if (BabelTypes.isExpressionStatement(node)) {
1309
+ return this.extractFirstHandlerName(node.expression);
1310
+ }
1311
+ if (BabelTypes.isReturnStatement(node)) {
1312
+ return node.argument ? this.extractFirstHandlerName(node.argument) : void 0;
1313
+ }
1314
+ if (BabelTypes.isUnaryExpression(node)) {
1315
+ return this.extractFirstHandlerName(node.argument);
1316
+ }
1317
+ if (BabelTypes.isAwaitExpression(node)) {
1318
+ return this.extractFirstHandlerName(node.argument);
1319
+ }
1320
+ if (BabelTypes.isCallExpression(node)) {
1321
+ if (BabelTypes.isIdentifier(node.callee)) return node.callee.name;
1322
+ return void 0;
1323
+ }
1324
+ return void 0;
1325
+ }
1326
+ mergeLocator(current, next) {
1327
+ return { ...current ?? {}, ...next };
1328
+ }
1329
+ expressionToBinding(expr) {
1330
+ if (BabelTypes.isIdentifier(expr)) return expr.name;
1331
+ if (BabelTypes.isMemberExpression(expr) && BabelTypes.isIdentifier(expr.object) && BabelTypes.isIdentifier(expr.property)) {
1332
+ return `${expr.object.name}.${expr.property.name}`;
1333
+ }
1334
+ return void 0;
1335
+ }
1336
+ slugifyActionId(label) {
1337
+ return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1338
+ }
1339
+ extractCollections(ast) {
1340
+ const renderItemFns = this.collectRenderItemFunctions(ast);
1341
+ const collections = [];
1342
+ traverse4(ast, {
1343
+ JSXOpeningElement: (nodePath) => {
1344
+ const element = nodePath.node;
1345
+ if (!BabelTypes.isJSXIdentifier(element.name)) return;
1346
+ const component = element.name.name;
1347
+ if (!["FlatList", "SectionList", "VirtualizedList", "FlashList"].includes(component)) {
1348
+ return;
1349
+ }
1350
+ const dataSource = this.jsxExpressionIdentifier(element, "data");
1351
+ const renderItem = this.jsxExpressionIdentifier(element, "renderItem");
1352
+ const keyField = this.extractKeyField(element);
1353
+ const renderFn = renderItem ? renderItemFns.get(renderItem) : void 0;
1354
+ const rowAction = renderFn ? this.extractRowAction(renderFn) : void 0;
1355
+ const displayFields = renderFn ? this.extractItemFields(renderFn) : [];
1356
+ const itemType = this.inferItemType(dataSource, renderItem, displayFields);
1357
+ const identityFields = this.inferIdentityFields(keyField, displayFields);
1358
+ const searchField = this.inferSearchField(ast, dataSource);
1359
+ const id = dataSource ?? renderItem ?? `${component.toLowerCase()}-${collections.length + 1}`;
1360
+ collections.push({
1361
+ id,
1362
+ component,
1363
+ ...itemType ? { itemType } : {},
1364
+ ...dataSource ? { dataSource } : {},
1365
+ ...keyField ? { keyField } : {},
1366
+ ...renderItem ? { renderItem } : {},
1367
+ ...displayFields.length > 0 ? { displayFields } : {},
1368
+ ...rowAction ? { rowAction } : {},
1369
+ ...rowAction ? { rowActions: [rowAction] } : {},
1370
+ ...identityFields.length > 0 ? { identityFields } : {},
1371
+ ...searchField ? { searchField } : {}
1372
+ });
1373
+ }
1374
+ });
1375
+ return collections;
1376
+ }
1377
+ collectRenderItemFunctions(ast) {
1378
+ const out = /* @__PURE__ */ new Map();
1379
+ traverse4(ast, {
1380
+ VariableDeclarator: (nodePath) => {
1381
+ if (!BabelTypes.isIdentifier(nodePath.node.id)) return;
1382
+ const init = nodePath.node.init;
1383
+ if (BabelTypes.isArrowFunctionExpression(init) || BabelTypes.isFunctionExpression(init)) {
1384
+ out.set(nodePath.node.id.name, init);
1385
+ }
1386
+ },
1387
+ FunctionDeclaration: (nodePath) => {
1388
+ if (nodePath.node.id?.name) out.set(nodePath.node.id.name, nodePath.node);
1389
+ }
1390
+ });
1391
+ return out;
1392
+ }
1393
+ jsxExpressionIdentifier(element, attrName) {
1394
+ const attr = element.attributes.find(
1395
+ (candidate) => BabelTypes.isJSXAttribute(candidate) && BabelTypes.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
1396
+ );
1397
+ if (!BabelTypes.isJSXAttribute(attr) || !attr.value) return void 0;
1398
+ if (BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
1399
+ return attr.value.expression.name;
1400
+ }
1401
+ if (BabelTypes.isStringLiteral(attr.value)) return attr.value.value;
1402
+ return void 0;
1403
+ }
1404
+ extractKeyField(element) {
1405
+ const attr = element.attributes.find(
1406
+ (candidate) => BabelTypes.isJSXAttribute(candidate) && BabelTypes.isJSXIdentifier(candidate.name) && candidate.name.name === "keyExtractor"
1407
+ );
1408
+ if (!BabelTypes.isJSXAttribute(attr) || !attr.value || !BabelTypes.isJSXExpressionContainer(attr.value)) {
1409
+ return void 0;
1410
+ }
1411
+ const expr = attr.value.expression;
1412
+ if (!BabelTypes.isArrowFunctionExpression(expr)) return void 0;
1413
+ const param = expr.params[0];
1414
+ if (!BabelTypes.isIdentifier(param)) return void 0;
1415
+ if (BabelTypes.isMemberExpression(expr.body) && BabelTypes.isIdentifier(expr.body.object) && expr.body.object.name === param.name && BabelTypes.isIdentifier(expr.body.property)) {
1416
+ return expr.body.property.name;
1417
+ }
1418
+ return void 0;
1419
+ }
1420
+ extractRowAction(fn) {
1421
+ let action;
1422
+ traverse4(fn.body, {
1423
+ noScope: true,
1424
+ CallExpression: (nodePath) => {
1425
+ const node = nodePath.node;
1426
+ if (!BabelTypes.isMemberExpression(node.callee) || !BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !BabelTypes.isIdentifier(node.callee.property) || !["navigate", "push", "replace"].includes(node.callee.property.name)) {
1427
+ return;
1428
+ }
1429
+ const firstArg = node.arguments[0];
1430
+ if (!BabelTypes.isStringLiteral(firstArg)) return;
1431
+ const params = this.extractNavigationParams(node.arguments[1]);
1432
+ action = {
1433
+ type: "navigation",
1434
+ targetScreen: firstArg.value,
1435
+ ...Object.keys(params).length > 0 ? { params } : {},
1436
+ description: `Pressing a row opens ${firstArg.value}`
1437
+ };
1438
+ }
1439
+ });
1440
+ return action;
1441
+ }
1442
+ extractNavigationParams(arg) {
1443
+ const params = {};
1444
+ if (!arg || !BabelTypes.isObjectExpression(arg)) return params;
1445
+ for (const prop of arg.properties) {
1446
+ if (!BabelTypes.isObjectProperty(prop)) continue;
1447
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : void 0;
1448
+ if (!key) continue;
1449
+ if (BabelTypes.isMemberExpression(prop.value) && BabelTypes.isIdentifier(prop.value.object) && BabelTypes.isIdentifier(prop.value.property)) {
1450
+ params[key] = `${prop.value.object.name}.${prop.value.property.name}`;
1451
+ } else if (BabelTypes.isIdentifier(prop.value)) {
1452
+ params[key] = prop.value.name;
1453
+ } else if (BabelTypes.isStringLiteral(prop.value)) {
1454
+ params[key] = prop.value.value;
1455
+ }
1456
+ }
1457
+ return params;
1458
+ }
1459
+ extractItemFields(fn) {
1460
+ const fields = /* @__PURE__ */ new Set();
1461
+ const itemNames = /* @__PURE__ */ new Set(["item"]);
1462
+ const firstParam = fn.params[0];
1463
+ if (BabelTypes.isObjectPattern(firstParam)) {
1464
+ for (const prop of firstParam.properties) {
1465
+ if (BabelTypes.isObjectProperty(prop) && BabelTypes.isIdentifier(prop.key) && prop.key.name === "item" && BabelTypes.isIdentifier(prop.value)) {
1466
+ itemNames.add(prop.value.name);
1467
+ }
1468
+ }
1469
+ } else if (BabelTypes.isIdentifier(firstParam)) {
1470
+ itemNames.add(firstParam.name);
1471
+ }
1472
+ traverse4(fn.body, {
1473
+ noScope: true,
1474
+ MemberExpression: (nodePath) => {
1475
+ const node = nodePath.node;
1476
+ if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
1477
+ fields.add(node.property.name);
1478
+ }
1479
+ }
1480
+ });
1481
+ return Array.from(fields).sort();
1482
+ }
1483
+ inferItemType(dataSource, renderItem, displayFields) {
1484
+ const source = dataSource ?? renderItem;
1485
+ if (!source) return void 0;
1486
+ const singular = source.replace(/^render/i, "").replace(/(List|Items|Data|Rows|Sections)$/i, "").replace(/s$/i, "");
1487
+ const candidate = singular.charAt(0).toUpperCase() + singular.slice(1);
1488
+ if (candidate.length > 1) return candidate;
1489
+ if (displayFields.length > 0) return "Item";
1490
+ return void 0;
1491
+ }
1492
+ inferIdentityFields(keyField, displayFields) {
1493
+ const out = /* @__PURE__ */ new Set();
1494
+ if (keyField) out.add(keyField);
1495
+ for (const field of displayFields) {
1496
+ if (/^(id|uuid|key|name|title|plate|email|slug)$/i.test(field)) out.add(field);
1497
+ }
1498
+ return Array.from(out);
1499
+ }
1500
+ inferSearchField(ast, dataSource) {
1501
+ if (!dataSource) return void 0;
1502
+ let queryBinding;
1503
+ traverse4(ast, {
1504
+ CallExpression: (nodePath) => {
1505
+ const node = nodePath.node;
1506
+ if (!BabelTypes.isMemberExpression(node.callee)) return;
1507
+ if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter") return;
1508
+ if (!BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource) return;
1509
+ const fn = node.arguments[0];
1510
+ if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn)) return;
1511
+ traverse4(fn.body, {
1512
+ noScope: true,
1513
+ Identifier: (innerPath) => {
1514
+ const name = innerPath.node.name;
1515
+ if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
1516
+ }
1517
+ });
1518
+ }
1519
+ });
1520
+ return queryBinding;
1521
+ }
1522
+ /**
1523
+ * Extract literal values from AST nodes
1524
+ */
1525
+ extractLiteralValue(node) {
1526
+ if (BabelTypes.isStringLiteral(node)) {
1527
+ return node.value;
1528
+ }
1529
+ if (BabelTypes.isNumericLiteral(node)) {
1530
+ return node.value;
1531
+ }
1532
+ if (BabelTypes.isBooleanLiteral(node)) {
1533
+ return node.value;
1534
+ }
1535
+ if (BabelTypes.isNullLiteral(node)) {
1536
+ return null;
1537
+ }
1538
+ return void 0;
1539
+ }
1540
+ /**
1541
+ * Extract screen name from file path
1542
+ * E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
1543
+ */
1544
+ extractScreenName(filePath) {
1545
+ const basename2 = path4__default.basename(filePath);
1546
+ return basename2.replace(/\.(tsx?|jsx?)$/, "");
1547
+ }
1548
+ };
1549
+ var NavigationAnalyzer = class {
1550
+ config;
1551
+ /** Extra glob patterns for navigation file discovery (added to defaults) */
1552
+ navigationInclude;
1553
+ /** Extra glob patterns to exclude from navigation analysis */
1554
+ navigationExclude;
1555
+ constructor(config, options) {
1556
+ this.config = config;
1557
+ this.navigationInclude = options?.navigationInclude ?? [];
1558
+ this.navigationExclude = options?.navigationExclude ?? [];
1559
+ }
1560
+ /** Build the full navigation graph */
1561
+ async analyze() {
1562
+ const navigationFiles = await this.findNavigationFiles();
1563
+ const parsedNavigators = [];
1564
+ const typeExports = [];
1565
+ for (const filePath of navigationFiles) {
1566
+ try {
1567
+ const content = await promises.readFile(filePath, "utf-8");
1568
+ const navigators = this.parseNavigators(content, filePath);
1569
+ const types = this.parseParamTypes(content, filePath);
1570
+ parsedNavigators.push(...navigators);
1571
+ typeExports.push(...types);
1572
+ } catch (error) {
1573
+ console.warn(`Failed to parse ${filePath}:`, error);
1574
+ }
1575
+ }
1576
+ this.attachParamsToNavigators(parsedNavigators, typeExports);
1577
+ return this.buildNavigationGraph(parsedNavigators);
1578
+ }
1579
+ /** Find all navigation-related files */
1580
+ async findNavigationFiles() {
1581
+ const patterns = [
1582
+ "**/navigation/**/*.{ts,tsx}",
1583
+ "**/navigator*.{ts,tsx}",
1584
+ "**/routes*.{ts,tsx}",
1585
+ // §5: Add user-configured navigation patterns (additive, not replacing)
1586
+ ...this.navigationInclude
1587
+ ];
1588
+ const excludePatterns = [
1589
+ "**/node_modules/**",
1590
+ "**/dist/**",
1591
+ "**/build/**",
1592
+ ...this.config.exclude || [],
1593
+ // §5: Add user-configured navigation excludes
1594
+ ...this.navigationExclude
1595
+ ];
1596
+ const files = await glob(patterns, {
1597
+ cwd: this.config.rootDir,
1598
+ ignore: excludePatterns
1599
+ });
1600
+ return files.map((file) => path4__default.join(this.config.rootDir, file));
1601
+ }
1602
+ /** Parse navigator definitions from a file */
1603
+ parseNavigators(content, filePath) {
1604
+ const navigators = [];
1605
+ try {
1606
+ const ast = parser.parse(content, {
1607
+ sourceType: "module",
1608
+ plugins: [
1609
+ "jsx",
1610
+ "typescript",
1611
+ ...this.config.parserPlugins || []
1612
+ ]
1613
+ });
1614
+ const navigatorCalls = /* @__PURE__ */ new Map();
1615
+ const screensByNavigator = /* @__PURE__ */ new Map();
1616
+ traverse4(ast, {
1617
+ // Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
1618
+ CallExpression: (nodePath) => {
1619
+ const { node } = nodePath;
1620
+ const callee = node.callee;
1621
+ let navigatorType = null;
1622
+ if (BabelTypes.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
1623
+ navigatorType = "stack";
1624
+ } else if (BabelTypes.isIdentifier(callee) && callee.name === "createStackNavigator") {
1625
+ navigatorType = "stack";
1626
+ } else if (BabelTypes.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
1627
+ navigatorType = "tab";
1628
+ } else if (BabelTypes.isIdentifier(callee) && callee.name === "createTabNavigator") {
1629
+ navigatorType = "tab";
1630
+ } else if (BabelTypes.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
1631
+ navigatorType = "drawer";
1632
+ }
1633
+ if (navigatorType) {
1634
+ const parent = nodePath.parent;
1635
+ if (BabelTypes.isVariableDeclarator(parent) && BabelTypes.isIdentifier(parent.id)) {
1636
+ const varName = parent.id.name;
1637
+ navigatorCalls.set(varName, {
1638
+ name: varName,
1639
+ type: navigatorType,
1640
+ screens: []
1641
+ });
1642
+ }
1643
+ }
1644
+ },
1645
+ // Detect Stack.Navigator / Tab.Navigator JSX elements
1646
+ JSXElement: (nodePath) => {
1647
+ const { node } = nodePath;
1648
+ const openingElement = node.openingElement;
1649
+ if (BabelTypes.isJSXMemberExpression(openingElement.name) && BabelTypes.isJSXIdentifier(openingElement.name.object)) {
1650
+ const objectName = openingElement.name.object.name;
1651
+ const propertyName = BabelTypes.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
1652
+ if (propertyName === "Navigator") {
1653
+ const navigator = navigatorCalls.get(objectName);
1654
+ if (navigator) {
1655
+ const initialRouteAttr = openingElement.attributes.find(
1656
+ (attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
1657
+ );
1658
+ if (BabelTypes.isJSXAttribute(initialRouteAttr) && BabelTypes.isStringLiteral(initialRouteAttr.value)) {
1659
+ navigator.initialRouteName = initialRouteAttr.value.value;
1660
+ }
1661
+ const screens = this.extractScreensFromNavigator(
1662
+ node,
1663
+ objectName,
1664
+ navigator.type
1665
+ );
1666
+ screensByNavigator.set(objectName, screens);
1667
+ }
1668
+ }
1669
+ }
1670
+ }
1671
+ });
1672
+ navigatorCalls.forEach((navigator) => {
1673
+ const screens = screensByNavigator.get(navigator.name) || [];
1674
+ navigator.screens = screens;
1675
+ navigators.push(navigator);
1676
+ });
1677
+ } catch (error) {
1678
+ console.warn(`Failed to parse navigators in ${filePath}:`, error);
1679
+ }
1680
+ return navigators;
1681
+ }
1682
+ /** Extract screens from a navigator JSX element */
1683
+ extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
1684
+ const screens = [];
1685
+ if (!navigatorElement.children) return screens;
1686
+ for (const child of navigatorElement.children) {
1687
+ if (BabelTypes.isJSXElement(child) && BabelTypes.isJSXMemberExpression(child.openingElement.name)) {
1688
+ const memberExpr = child.openingElement.name;
1689
+ if (BabelTypes.isJSXIdentifier(memberExpr.object) && memberExpr.object.name === navigatorVarName && BabelTypes.isJSXIdentifier(memberExpr.property) && memberExpr.property.name === "Screen") {
1690
+ const screenName = this.extractAttributeValue(
1691
+ child.openingElement.attributes,
1692
+ "name"
1693
+ );
1694
+ if (screenName) {
1695
+ screens.push({
1696
+ name: screenName,
1697
+ navigatorName: navigatorVarName,
1698
+ navigatorType
1699
+ });
1700
+ }
1701
+ }
1702
+ }
1703
+ }
1704
+ return screens;
1705
+ }
1706
+ /** Extract string attribute value from JSX attributes */
1707
+ extractAttributeValue(attributes, attrName) {
1708
+ const attr = attributes.find(
1709
+ (a) => BabelTypes.isJSXAttribute(a) && BabelTypes.isJSXIdentifier(a.name) && a.name.name === attrName
1710
+ );
1711
+ if (BabelTypes.isJSXAttribute(attr) && BabelTypes.isStringLiteral(attr.value)) {
1712
+ return attr.value.value;
1713
+ }
1714
+ return null;
1715
+ }
1716
+ /** Parse TypeScript type exports (ParamList types) */
1717
+ parseParamTypes(content, filePath) {
1718
+ const types = [];
1719
+ try {
1720
+ const ast = parser.parse(content, {
1721
+ sourceType: "module",
1722
+ plugins: [
1723
+ "jsx",
1724
+ "typescript",
1725
+ ...this.config.parserPlugins || []
1726
+ ]
1727
+ });
1728
+ traverse4(ast, {
1729
+ TSTypeAliasDeclaration: (nodePath) => {
1730
+ const { node } = nodePath;
1731
+ const typeName = node.id.name;
1732
+ const typeMatch = /^[A-Za-z0-9_]*(Stack|Tab|Drawer)?ParamList$/i.test(typeName);
1733
+ if (typeMatch && node.typeAnnotation?.type === "TSTypeLiteral") {
1734
+ const paramEntries = this.extractParamListEntries(
1735
+ node.typeAnnotation,
1736
+ typeName
1737
+ );
1738
+ const navType = this.inferTypeFromName(typeName);
1739
+ if (navType) {
1740
+ types.push({
1741
+ name: typeName,
1742
+ type: navType.type,
1743
+ paramEntries
1744
+ });
1745
+ }
1746
+ }
1747
+ }
1748
+ });
1749
+ } catch (error) {
1750
+ console.warn(`Failed to parse types in ${filePath}:`, error);
1751
+ }
1752
+ return types;
1753
+ }
1754
+ /** Extract param entries from a TypeScript type literal */
1755
+ extractParamListEntries(typeLiteral, typeName) {
1756
+ const entries = /* @__PURE__ */ new Map();
1757
+ for (const member of typeLiteral.members) {
1758
+ if (member.type === "TSPropertySignature" && member.key) {
1759
+ const keyName = member.key.type === "Identifier" ? member.key.name : null;
1760
+ if (keyName && member.typeAnnotation) {
1761
+ const params = this.extractParamsFromType(
1762
+ member.typeAnnotation.typeAnnotation
1763
+ );
1764
+ entries.set(keyName, params);
1765
+ }
1766
+ }
1767
+ }
1768
+ return entries;
1769
+ }
1770
+ /** Extract param descriptors from a TypeScript type */
1771
+ extractParamsFromType(typeAnnotation) {
1772
+ const params = [];
1773
+ if (typeAnnotation.type === "TSUndefinedKeyword" || typeAnnotation.type === "TSNullKeyword") {
1774
+ return params;
1775
+ }
1776
+ if (typeAnnotation.type === "TSTypeLiteral") {
1777
+ const typeLiteral = typeAnnotation;
1778
+ for (const member of typeLiteral.members) {
1779
+ if (member.type === "TSPropertySignature" && member.key) {
1780
+ const keyName = member.key.type === "Identifier" ? member.key.name : null;
1781
+ if (keyName) {
1782
+ const typeStr = member.typeAnnotation ? this.typeToString(member.typeAnnotation.typeAnnotation) : "unknown";
1783
+ const required = !member.optional;
1784
+ params.push({
1785
+ name: keyName,
1786
+ type: typeStr,
1787
+ required
1788
+ });
1789
+ }
1790
+ }
1791
+ }
1792
+ }
1793
+ return params;
1794
+ }
1795
+ /** Convert TypeScript type annotation to string */
1796
+ typeToString(type) {
1797
+ if (type.type === "TSStringKeyword") return "string";
1798
+ if (type.type === "TSNumberKeyword") return "number";
1799
+ if (type.type === "TSBooleanKeyword") return "boolean";
1800
+ if (type.type === "TSUndefinedKeyword") return "undefined";
1801
+ if (type.type === "TSNullKeyword") return "null";
1802
+ if (type.type === "TSUnionType") {
1803
+ return type.types.map((t8) => this.typeToString(t8)).join(" | ");
1804
+ }
1805
+ if (type.type === "TSTypeLiteral") {
1806
+ return "object";
1807
+ }
1808
+ if (type.type === "TSTypeReference" && type.typeName) {
1809
+ const name = type.typeName;
1810
+ return name.type === "Identifier" ? name.name : "unknown";
1811
+ }
1812
+ return "unknown";
1813
+ }
1814
+ /** Infer navigator type from type name */
1815
+ inferTypeFromName(typeName) {
1816
+ if (/stack/i.test(typeName)) return { type: "stack" };
1817
+ if (/tab/i.test(typeName)) return { type: "tab" };
1818
+ if (/drawer/i.test(typeName)) return { type: "drawer" };
1819
+ return null;
1820
+ }
1821
+ /** Attach parsed type params to navigator screens */
1822
+ attachParamsToNavigators(navigators, types) {
1823
+ for (const navigator of navigators) {
1824
+ const matchingType = types.find((t8) => t8.type === navigator.type);
1825
+ if (matchingType) {
1826
+ for (const screen of navigator.screens) {
1827
+ const screenParams = matchingType.paramEntries.get(screen.name);
1828
+ if (screenParams) {
1829
+ screen.params = screenParams;
1830
+ }
1831
+ }
1832
+ }
1833
+ }
1834
+ }
1835
+ /** Build the complete navigation graph */
1836
+ buildNavigationGraph(parsedNavigators) {
1837
+ const screens = {};
1838
+ const navigators = [];
1839
+ let initialScreen = "";
1840
+ for (const navigator of parsedNavigators) {
1841
+ const screenNames = navigator.screens.map((s) => s.name);
1842
+ navigators.push({
1843
+ name: navigator.name,
1844
+ type: navigator.type,
1845
+ screens: screenNames,
1846
+ parentNavigator: navigator.parentNavigator
1847
+ });
1848
+ if (!initialScreen && navigator.initialRouteName && (navigator.name.includes("Root") || screenNames.some((s) => s === "Auth" || s === "Main"))) {
1849
+ initialScreen = navigator.initialRouteName;
1850
+ }
1851
+ }
1852
+ for (const navigator of parsedNavigators) {
1853
+ for (const screen of navigator.screens) {
1854
+ const screenKey = screen.name;
1855
+ const reachableTo = [];
1856
+ const reachableFrom = [];
1857
+ for (const otherScreen of navigator.screens) {
1858
+ if (otherScreen.name !== screen.name) {
1859
+ if (navigator.type === "tab") {
1860
+ reachableTo.push(otherScreen.name);
1861
+ reachableFrom.push(otherScreen.name);
1862
+ } else {
1863
+ reachableTo.push(otherScreen.name);
1864
+ }
1865
+ }
1866
+ }
1867
+ screens[screenKey] = {
1868
+ screenName: screen.name,
1869
+ navigatorType: navigator.type,
1870
+ parentNavigator: navigator.name,
1871
+ reachableFrom,
1872
+ reachableTo,
1873
+ params: screen.params
1874
+ };
1875
+ }
1876
+ }
1877
+ if (!initialScreen && parsedNavigators.length > 0) {
1878
+ const firstNavigator = parsedNavigators[0];
1879
+ const rootNavigator = parsedNavigators.find(
1880
+ (n) => n.name.includes("Root") || firstNavigator && n.name === firstNavigator.name
1881
+ );
1882
+ if (rootNavigator && rootNavigator.screens && rootNavigator.screens.length > 0) {
1883
+ const firstScreen = rootNavigator.screens[0];
1884
+ initialScreen = firstScreen?.name || "";
1885
+ }
1886
+ }
1887
+ return {
1888
+ screens,
1889
+ initialScreen,
1890
+ navigators
1891
+ };
1892
+ }
1893
+ };
1894
+ var ComponentAnalyzer = class {
1895
+ config;
1896
+ constructor(config) {
1897
+ this.config = config;
1898
+ }
1899
+ /** Analyze components in a file */
1900
+ async analyzeFile(filePath) {
1901
+ try {
1902
+ const code = readFileSync(filePath, "utf-8");
1903
+ const ast = parser.parse(code, {
1904
+ sourceType: "module",
1905
+ plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
1906
+ });
1907
+ const components = [];
1908
+ traverse4(ast, {
1909
+ JSXElement: (path5) => {
1910
+ const component = this.extractComponentFromJSXElement(path5.node);
1911
+ if (component) {
1912
+ components.push(component);
1913
+ }
1914
+ }
1915
+ });
1916
+ return components;
1917
+ } catch (error) {
1918
+ console.error(`Failed to analyze file ${filePath}:`, error);
1919
+ return [];
1920
+ }
1921
+ }
1922
+ extractComponentFromJSXElement(element) {
1923
+ const elementName = this.getElementName(element.openingElement);
1924
+ if (!elementName) return null;
1925
+ const type = this.determineComponentType(elementName);
1926
+ const props = this.extractProps(element.openingElement);
1927
+ const { testID, accessibilityLabel } = this.extractAccessibilityProps(element.openingElement);
1928
+ const children = this.extractChildren(element.children);
1929
+ return {
1930
+ name: elementName,
1931
+ type,
1932
+ props: Object.keys(props).length > 0 ? props : void 0,
1933
+ children: children.length > 0 ? children : void 0,
1934
+ testID,
1935
+ accessibilityLabel
1936
+ };
1937
+ }
1938
+ getElementName(openingElement) {
1939
+ if (BabelTypes.isJSXIdentifier(openingElement.name)) {
1940
+ return openingElement.name.name;
1941
+ }
1942
+ if (BabelTypes.isJSXMemberExpression(openingElement.name)) {
1943
+ const parts = [];
1944
+ let current = openingElement.name;
1945
+ while (BabelTypes.isJSXMemberExpression(current)) {
1946
+ if (BabelTypes.isJSXIdentifier(current.property)) {
1947
+ parts.unshift(current.property.name);
1948
+ }
1949
+ current = current.object;
1950
+ }
1951
+ if (BabelTypes.isJSXIdentifier(current)) {
1952
+ parts.unshift(current.name);
1953
+ }
1954
+ return parts.join(".");
1955
+ }
1956
+ return null;
1957
+ }
1958
+ determineComponentType(name) {
1959
+ const viewComponents = ["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"];
1960
+ const inputComponents = ["TextInput", "Input"];
1961
+ const buttonComponents = ["TouchableOpacity", "Pressable", "Button"];
1962
+ const listComponents = ["FlatList", "SectionList"];
1963
+ const modalComponents = ["Modal", "BottomSheet"];
1964
+ if (viewComponents.includes(name)) return "view";
1965
+ if (inputComponents.includes(name)) return "input";
1966
+ if (buttonComponents.includes(name)) return "button";
1967
+ if (listComponents.includes(name)) return "list";
1968
+ if (modalComponents.includes(name)) return "modal";
1969
+ return "custom";
1970
+ }
1971
+ extractProps(openingElement) {
1972
+ const props = {};
1973
+ for (const attr of openingElement.attributes) {
1974
+ if (BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name)) {
1975
+ const propName = attr.name.name;
1976
+ if (attr.value === null) {
1977
+ props[propName] = "true";
1978
+ } else if (BabelTypes.isStringLiteral(attr.value)) {
1979
+ props[propName] = attr.value.value;
1980
+ } else if (BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isStringLiteral(attr.value.expression)) {
1981
+ props[propName] = attr.value.expression.value;
1982
+ }
1983
+ }
1984
+ }
1985
+ return props;
1986
+ }
1987
+ extractAccessibilityProps(openingElement) {
1988
+ const result = {};
1989
+ for (const attr of openingElement.attributes) {
1990
+ if (BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name)) {
1991
+ const propName = attr.name.name;
1992
+ if ((propName === "testID" || propName === "accessibilityLabel") && attr.value) {
1993
+ if (BabelTypes.isStringLiteral(attr.value)) {
1994
+ result[propName] = attr.value.value;
1995
+ } else if (BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isStringLiteral(attr.value.expression)) {
1996
+ result[propName] = attr.value.expression.value;
1997
+ }
1998
+ }
1999
+ }
2000
+ }
2001
+ return result;
2002
+ }
2003
+ extractChildren(children, depth = 0, maxDepth = 3) {
2004
+ if (depth >= maxDepth) return [];
2005
+ const extracted = [];
2006
+ for (const child of children) {
2007
+ if (BabelTypes.isJSXElement(child)) {
2008
+ const component = this.extractComponentFromJSXElement(child);
2009
+ if (component) {
2010
+ extracted.push(component);
2011
+ }
2012
+ }
2013
+ }
2014
+ return extracted;
2015
+ }
2016
+ };
2017
+ var FormAnalyzer = class {
2018
+ config;
2019
+ stateVariables = /* @__PURE__ */ new Map();
2020
+ inputElements = [];
2021
+ submitButtons = [];
2022
+ constructor(config) {
2023
+ this.config = config;
2024
+ }
2025
+ /** Analyze forms in a file */
2026
+ async analyzeFile(filePath) {
2027
+ try {
2028
+ const code = readFileSync(filePath, "utf-8");
2029
+ const ast = parser.parse(code, {
2030
+ sourceType: "module",
2031
+ plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
2032
+ });
2033
+ this.stateVariables.clear();
2034
+ this.inputElements = [];
2035
+ this.submitButtons = [];
2036
+ traverse4(ast, {
2037
+ CallExpression: (path5) => {
2038
+ this.extractStateVariables(path5.node);
2039
+ }
2040
+ });
2041
+ traverse4(ast, {
2042
+ JSXElement: (path5) => {
2043
+ this.extractFormElements(path5.node);
2044
+ }
2045
+ });
2046
+ const validationRules = this.extractValidationRules(ast);
2047
+ return this.buildForms(filePath, validationRules);
2048
+ } catch (error) {
2049
+ console.error(`Failed to analyze forms in file ${filePath}:`, error);
2050
+ return [];
2051
+ }
2052
+ }
2053
+ extractStateVariables(node) {
2054
+ if (BabelTypes.isIdentifier(node.callee) && node.callee.name === "useState" && node.arguments.length > 0) {
2055
+ return;
2056
+ }
2057
+ }
2058
+ extractFormElements(element) {
2059
+ const elementName = this.getElementName(element.openingElement);
2060
+ if (!elementName) return;
2061
+ if (elementName === "TextInput" || elementName === "Input") {
2062
+ const inputInfo = this.extractInputInfo(element.openingElement);
2063
+ this.inputElements.push(inputInfo);
2064
+ }
2065
+ if (["TouchableOpacity", "Pressable", "Button"].includes(elementName)) {
2066
+ const buttonInfo = this.extractButtonInfo(element.openingElement);
2067
+ if (buttonInfo) {
2068
+ this.submitButtons.push(buttonInfo);
2069
+ }
2070
+ }
2071
+ }
2072
+ extractInputInfo(openingElement) {
2073
+ const info = { varName: "" };
2074
+ for (const attr of openingElement.attributes) {
2075
+ if (BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name)) {
2076
+ const propName = attr.name.name;
2077
+ const propValue = this.extractAttributeValue(attr.value);
2078
+ switch (propName) {
2079
+ case "label":
2080
+ info.label = propValue;
2081
+ break;
2082
+ case "placeholder":
2083
+ info.placeholder = propValue;
2084
+ break;
2085
+ case "keyboardType":
2086
+ info.keyboardType = propValue;
2087
+ break;
2088
+ case "testID":
2089
+ info.testID = propValue;
2090
+ break;
2091
+ case "appilotsId":
2092
+ info.appilotsId = propValue;
2093
+ if (propValue) info.varName = propValue;
2094
+ break;
2095
+ case "value":
2096
+ if (attr.value && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
2097
+ info.varName = attr.value.expression.name;
2098
+ }
2099
+ break;
2100
+ }
2101
+ }
2102
+ }
2103
+ return info;
2104
+ }
2105
+ extractButtonInfo(openingElement) {
2106
+ const info = {};
2107
+ for (const attr of openingElement.attributes) {
2108
+ if (BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name)) {
2109
+ const propName = attr.name.name;
2110
+ if (propName === "title" || propName === "label") {
2111
+ info.label = this.extractAttributeValue(attr.value);
2112
+ }
2113
+ if (propName === "onPress") {
2114
+ if (attr.value && BabelTypes.isJSXExpressionContainer(attr.value)) {
2115
+ if (BabelTypes.isIdentifier(attr.value.expression)) {
2116
+ info.handler = attr.value.expression.name;
2117
+ } else if (BabelTypes.isArrowFunctionExpression(attr.value.expression) || BabelTypes.isFunctionExpression(attr.value.expression)) {
2118
+ info.handler = "anonymous";
2119
+ }
2120
+ }
2121
+ }
2122
+ }
2123
+ }
2124
+ return Object.keys(info).length > 0 ? info : null;
2125
+ }
2126
+ extractAttributeValue(value) {
2127
+ if (value === null) return void 0;
2128
+ if (BabelTypes.isStringLiteral(value)) return value.value;
2129
+ if (BabelTypes.isJSXExpressionContainer(value) && BabelTypes.isStringLiteral(value.expression)) {
2130
+ return value.expression.value;
2131
+ }
2132
+ return void 0;
2133
+ }
2134
+ getElementName(openingElement) {
2135
+ if (BabelTypes.isJSXIdentifier(openingElement.name)) {
2136
+ return openingElement.name.name;
2137
+ }
2138
+ return null;
2139
+ }
2140
+ extractValidationRules(ast) {
2141
+ const rules = {};
2142
+ traverse4(ast, {
2143
+ IfStatement: (path5) => {
2144
+ const test = path5.node.test;
2145
+ const rule = this.extractRuleFromCondition(test);
2146
+ if (rule) {
2147
+ const { field, description } = rule;
2148
+ if (field && description) {
2149
+ rules[field] = description;
2150
+ }
2151
+ }
2152
+ }
2153
+ });
2154
+ return rules;
2155
+ }
2156
+ extractRuleFromCondition(test) {
2157
+ let fieldName = "";
2158
+ let description = "";
2159
+ if (BabelTypes.isUnaryExpression(test) && test.operator === "!") {
2160
+ if (BabelTypes.isCallExpression(test.argument)) {
2161
+ const callExpr = test.argument;
2162
+ if (BabelTypes.isMemberExpression(callExpr.callee)) {
2163
+ const memberExpr = callExpr.callee;
2164
+ if (BabelTypes.isIdentifier(memberExpr.object)) {
2165
+ fieldName = memberExpr.object.name;
2166
+ description = `${fieldName} is required`;
2167
+ }
2168
+ }
2169
+ } else if (BabelTypes.isIdentifier(test.argument)) {
2170
+ fieldName = test.argument.name;
2171
+ description = `${fieldName} is required`;
2172
+ }
2173
+ }
2174
+ if (BabelTypes.isBinaryExpression(test) && (test.operator === "<" || test.operator === "<=")) {
2175
+ if (BabelTypes.isMemberExpression(test.left)) {
2176
+ const memberExpr = test.left;
2177
+ if (BabelTypes.isIdentifier(memberExpr.object)) {
2178
+ fieldName = memberExpr.object.name;
2179
+ if (BabelTypes.isNumericLiteral(test.right)) {
2180
+ description = `${fieldName} must be at least ${test.right.value} characters`;
2181
+ }
2182
+ }
2183
+ }
2184
+ }
2185
+ if (BabelTypes.isUnaryExpression(test) && test.operator === "!") {
2186
+ if (BabelTypes.isCallExpression(test.argument) && BabelTypes.isMemberExpression(test.argument.callee)) {
2187
+ const methodName = BabelTypes.isIdentifier(test.argument.callee.property) ? test.argument.callee.property.name : null;
2188
+ if (methodName === "includes" && BabelTypes.isIdentifier(test.argument.callee.object)) {
2189
+ fieldName = test.argument.callee.object.name;
2190
+ description = `${fieldName} format is invalid`;
2191
+ }
2192
+ }
2193
+ }
2194
+ return fieldName && description ? { field: fieldName, description } : null;
2195
+ }
2196
+ buildForms(filePath, validationRules) {
2197
+ if (this.inputElements.length === 0) return [];
2198
+ const fileName = path4.basename(filePath, path4.extname(filePath));
2199
+ const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
2200
+ const fields = this.inputElements.map((input) => {
2201
+ const fieldType = this.inferFieldType(input);
2202
+ const required = this.isFieldRequired(input.varName, validationRules);
2203
+ return {
2204
+ name: input.appilotsId || input.varName || input.testID || "field",
2205
+ label: input.label,
2206
+ type: fieldType,
2207
+ required,
2208
+ placeholder: input.placeholder
2209
+ };
2210
+ });
2211
+ const lastButton = this.submitButtons[this.submitButtons.length - 1];
2212
+ const submitAction = lastButton?.handler;
2213
+ return [
2214
+ {
2215
+ id: formId,
2216
+ fields,
2217
+ submitAction,
2218
+ validationRules: Object.keys(validationRules).length > 0 ? validationRules : void 0
2219
+ }
2220
+ ];
2221
+ }
2222
+ inferFieldType(input) {
2223
+ const lowerVarName = input.varName.toLowerCase();
2224
+ const lowerLabel = input.label?.toLowerCase() || "";
2225
+ const lowerPlaceholder = input.placeholder?.toLowerCase() || "";
2226
+ const combined = `${lowerVarName} ${lowerLabel} ${lowerPlaceholder}`;
2227
+ if (input.keyboardType === "email-address" || input.keyboardType === "email") {
2228
+ return "email";
2229
+ }
2230
+ if (input.keyboardType === "phone-pad" || input.keyboardType === "numeric") {
2231
+ return input.keyboardType === "numeric" ? "number" : "phone";
2232
+ }
2233
+ if (combined.includes("email")) return "email";
2234
+ if (combined.includes("phone") || combined.includes("tel")) return "phone";
2235
+ if (combined.includes("password")) return "text";
2236
+ if (combined.includes("number") || combined.includes("numeric")) return "number";
2237
+ if (combined.includes("date")) return "date";
2238
+ if (combined.includes("toggle") || combined.includes("check")) return "toggle";
2239
+ if (combined.includes("select") || combined.includes("choice")) return "select";
2240
+ return "text";
2241
+ }
2242
+ isFieldRequired(fieldName, rules) {
2243
+ const rule = rules[fieldName];
2244
+ return rule !== void 0 && rule.includes("required");
2245
+ }
2246
+ };
2247
+
2248
+ // src/pipeline/enrichment.ts
2249
+ function enrichScreenForAgent(screen) {
2250
+ const targets = mergeTargets([
2251
+ ...targetsFromForms(screen.forms),
2252
+ ...targetsFromActions(screen.actions),
2253
+ ...targetsFromCollections(screen.collections ?? [])
2254
+ ]);
2255
+ const flows = synthesizeFlows(screen);
2256
+ const agentHints = synthesizeAgentHints(screen, targets, flows);
2257
+ return {
2258
+ ...screen,
2259
+ ...targets.length > 0 ? { targets } : {},
2260
+ ...flows.length > 0 ? { flows } : {},
2261
+ ...agentHints ? { agentHints } : {}
2262
+ };
2263
+ }
2264
+ function targetsFromForms(forms) {
2265
+ const targets = [];
2266
+ for (const form of forms) {
2267
+ for (const field of form.fields) {
2268
+ targets.push({
2269
+ id: field.locator?.id ?? field.name,
2270
+ role: fieldToRole(field),
2271
+ label: field.label,
2272
+ fieldName: field.name,
2273
+ locator: field.locator ?? { id: field.name, source: "inferred" },
2274
+ sourceComponent: field.sourceComponent
2275
+ });
2276
+ }
2277
+ if (form.submitAction && form.submitAction !== "anonymous") {
2278
+ targets.push({
2279
+ id: form.submitAction,
2280
+ role: "submit",
2281
+ actionId: form.submitAction,
2282
+ label: form.submitAction
2283
+ });
2284
+ }
2285
+ }
2286
+ return targets;
2287
+ }
2288
+ function fieldToRole(field) {
2289
+ if (field.type === "toggle") return "toggle";
2290
+ if (field.type === "select") return "select";
2291
+ if (field.type === "date") return "date";
2292
+ return "input";
2293
+ }
2294
+ function targetsFromActions(actions) {
2295
+ return actions.filter((action) => action.id).map((action) => ({
2296
+ id: action.locator?.id ?? action.id,
2297
+ role: action.type === "submit" ? "submit" : "button",
2298
+ label: action.label,
2299
+ locator: action.locator ?? { id: action.id, label: action.label, source: "inferred" },
2300
+ actionId: action.id,
2301
+ handler: action.handler,
2302
+ targetScreen: action.targetScreen,
2303
+ destructive: action.destructive,
2304
+ requiresConfirmation: action.requiresConfirmation,
2305
+ opensModal: action.opensModal,
2306
+ opensBottomSheet: action.opensBottomSheet,
2307
+ sourceComponent: action.sourceComponent
2308
+ }));
2309
+ }
2310
+ function targetsFromCollections(collections) {
2311
+ return collections.map((collection) => ({
2312
+ id: collection.id,
2313
+ role: "list",
2314
+ label: collection.itemType ?? collection.dataSource ?? collection.id,
2315
+ locator: { id: collection.id, testID: collection.id, source: "testID" },
2316
+ sourceComponent: collection.component
2317
+ }));
2318
+ }
2319
+ function mergeTargets(targets) {
2320
+ const byId = /* @__PURE__ */ new Map();
2321
+ for (const target of targets) {
2322
+ if (!target.id) continue;
2323
+ const existing = byId.get(target.id);
2324
+ byId.set(target.id, existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target);
2325
+ }
2326
+ return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
2327
+ }
2328
+ function synthesizeFlows(screen, targets) {
2329
+ const flows = [];
2330
+ for (const form of screen.forms) {
2331
+ const submitAction = screen.actions.find((action) => action.id === form.submitAction) ?? screen.actions.find((action) => action.type === "submit");
2332
+ if (!submitAction) continue;
2333
+ flows.push({
2334
+ id: `submit-${form.id}`,
2335
+ title: submitAction.label ? `Submit ${submitAction.label}` : `Submit ${form.id}`,
2336
+ intent: "form_submit",
2337
+ steps: [
2338
+ ...form.fields.map((field) => ({
2339
+ type: field.type === "select" ? "select" : field.type === "toggle" ? "toggle" : "fill",
2340
+ target: field.name,
2341
+ label: field.label,
2342
+ required: field.required,
2343
+ description: field.placeholder
2344
+ })),
2345
+ { type: "press", target: submitAction.id, label: submitAction.label },
2346
+ { type: "wait", description: describeWait(submitAction) }
2347
+ ],
2348
+ waitPolicy: waitPolicyForAction(submitAction),
2349
+ safetyNotes: submitAction.requiresConfirmation || submitAction.destructive ? ["Requires confirmation before execution"] : void 0
2350
+ });
2351
+ }
2352
+ for (const action of screen.actions) {
2353
+ if (action.type === "submit") continue;
2354
+ if (action.targetScreen) {
2355
+ flows.push({
2356
+ id: `navigate-${action.id}`,
2357
+ title: action.label ? `Open ${action.label}` : `Navigate to ${action.targetScreen}`,
2358
+ intent: "navigate",
2359
+ steps: [{ type: "press", target: action.id, label: action.label }],
2360
+ waitPolicy: waitPolicyForAction(action)
2361
+ });
2362
+ } else if (action.destructive || action.requiresConfirmation) {
2363
+ flows.push({
2364
+ id: `confirm-${action.id}`,
2365
+ title: action.label ? `Confirm ${action.label}` : `Confirm ${action.id}`,
2366
+ intent: "destructive_action",
2367
+ steps: [
2368
+ { type: "press", target: action.id, label: action.label },
2369
+ { type: "confirm", description: "Wait for native or custom confirmation before continuing" },
2370
+ { type: "wait", description: describeWait(action) }
2371
+ ],
2372
+ waitPolicy: waitPolicyForAction(action),
2373
+ safetyNotes: ["Destructive or high-risk action"]
2374
+ });
2375
+ }
2376
+ }
2377
+ for (const collection of screen.collections ?? []) {
2378
+ if (collection.rowAction || collection.rowActions && collection.rowActions.length > 0) {
2379
+ flows.push({
2380
+ id: `list-${collection.id}`,
2381
+ title: `Act on an item in ${collection.id}`,
2382
+ intent: "list_action",
2383
+ steps: [
2384
+ { type: "choose-list-item", target: collection.id, description: "Resolve the user reference to a visible or searchable row" },
2385
+ { type: "press", description: collection.rowAction?.description ?? "Open the row action" }
2386
+ ],
2387
+ waitPolicy: { expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback" }
2388
+ });
2389
+ }
2390
+ }
2391
+ return flows.sort((a, b) => a.id.localeCompare(b.id));
2392
+ }
2393
+ function waitPolicyForAction(action) {
2394
+ const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
2395
+ const signals = [action.successSignal, action.failureSignal].filter(Boolean);
2396
+ return {
2397
+ ...expectedOutcome ? { expectedOutcome } : {},
2398
+ ...signals && signals.length > 0 ? { signals } : {},
2399
+ ...action.appilotsInferred?.isAsyncTrigger ? { maxMs: 1e4 } : {}
2400
+ };
2401
+ }
2402
+ function describeWait(action) {
2403
+ if (action.successSignal?.description) return action.successSignal.description;
2404
+ if (action.successSignal?.type === "goBack") return "Wait for the app to return to the previous screen";
2405
+ if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
2406
+ if (action.appilotsInferred?.expectedOutcome) return `Wait for ${action.appilotsInferred.expectedOutcome}`;
2407
+ return "Wait for the UI to settle";
2408
+ }
2409
+ function synthesizeAgentHints(screen, targets, flows) {
2410
+ const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
2411
+ const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
2412
+ const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
2413
+ const firstAsyncAction = screen.actions.find((action) => action.appilotsInferred?.isAsyncTrigger);
2414
+ const hints = {
2415
+ primaryGoal: synthesizePrimaryGoal(screen),
2416
+ commonTasks,
2417
+ preferredTargets,
2418
+ ...firstAsyncAction ? { waitPolicy: waitPolicyForAction(firstAsyncAction) } : {},
2419
+ ...safetyNotes.length > 0 ? { safetyNotes } : {}
2420
+ };
2421
+ return hints.primaryGoal || commonTasks.length > 0 || preferredTargets.length > 0 ? hints : void 0;
2422
+ }
2423
+ function synthesizePrimaryGoal(screen) {
2424
+ const goals = [];
2425
+ const collections = screen.collections ?? [];
2426
+ if (collections.length > 0) {
2427
+ const collectionNames = collections.map((collection) => collection.itemType ?? collection.dataSource ?? collection.id).filter(Boolean).slice(0, 3);
2428
+ goals.push(
2429
+ collectionNames.length > 0 ? `Browse and act on ${collectionNames.join(", ")} list data` : "Browse and act on list data"
2430
+ );
2431
+ }
2432
+ const uniqueFields = /* @__PURE__ */ new Map();
2433
+ for (const form of screen.forms) {
2434
+ for (const field of form.fields) {
2435
+ uniqueFields.set(field.name, field);
2436
+ }
2437
+ }
2438
+ const fieldCount = uniqueFields.size;
2439
+ if (fieldCount > 0) {
2440
+ const requiredCount = Array.from(uniqueFields.values()).filter((field) => field.required).length;
2441
+ const fieldLabel = fieldCount === 1 ? "field" : "fields";
2442
+ goals.push(
2443
+ requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
2444
+ );
2445
+ }
2446
+ const navigationCount = screen.actions.filter((action) => action.targetScreen).length;
2447
+ if (navigationCount > 0) {
2448
+ goals.push(`Open ${navigationCount} related screen${navigationCount === 1 ? "" : "s"}`);
2449
+ }
2450
+ const submitCount = screen.actions.filter((action) => action.type === "submit").length;
2451
+ if (submitCount > 0) {
2452
+ goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
2453
+ }
2454
+ const asyncCount = screen.actions.filter((action) => action.appilotsInferred?.isAsyncTrigger).length;
2455
+ if (asyncCount > 0) {
2456
+ goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
2457
+ }
2458
+ if (goals.length > 0) return goals.join("; ");
2459
+ if (screen.title) return `Screen labeled "${screen.title}"`;
2460
+ return void 0;
2461
+ }
2462
+ var MCPGenerator = class {
2463
+ analyzerConfig;
2464
+ options;
2465
+ generatorConfig;
2466
+ constructor(config) {
2467
+ this.generatorConfig = config;
2468
+ this.analyzerConfig = {
2469
+ rootDir: config.rootDir,
2470
+ include: config.include ?? ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
2471
+ exclude: config.exclude ?? ["**/node_modules/**", "**/__tests__/**", "**/dist/**"]
2472
+ };
2473
+ this.options = {
2474
+ format: config.format ?? "json",
2475
+ outputDir: config.outputDir ?? ".appilots",
2476
+ includeSourcePaths: config.includeSourcePaths ?? false,
2477
+ splitOutput: config.splitOutput ?? false,
2478
+ version: config.version ?? "1.0"
2479
+ };
2480
+ }
2481
+ /** Generate MCP documents from the project */
2482
+ async generate() {
2483
+ console.log("[MCPGenerator] Starting generation...");
2484
+ const outputDir = this.options.outputDir || ".appilots";
2485
+ await mkdir(outputDir, { recursive: true });
2486
+ console.log(`[MCPGenerator] Output directory ensured: ${outputDir}`);
2487
+ const screenAnalyzer = new ScreenAnalyzer(this.analyzerConfig, {
2488
+ strictScreens: this.generatorConfig.strictScreens ?? true,
2489
+ screenPatterns: this.generatorConfig.screenPatterns
2490
+ });
2491
+ const navigationAnalyzer = new NavigationAnalyzer(this.analyzerConfig, {
2492
+ navigationInclude: this.generatorConfig.navigationInclude,
2493
+ navigationExclude: this.generatorConfig.navigationExclude
2494
+ });
2495
+ const componentAnalyzer = new ComponentAnalyzer(this.analyzerConfig);
2496
+ const formAnalyzer = new FormAnalyzer(this.analyzerConfig);
2497
+ console.log("[MCPGenerator] Running analyzers...");
2498
+ const [screens, navigation] = await Promise.all([
2499
+ screenAnalyzer.analyze(),
2500
+ navigationAnalyzer.analyze()
2501
+ ]);
2502
+ console.log(
2503
+ `[MCPGenerator] Screen and navigation analysis complete. Found ${screens.length} screens`
2504
+ );
2505
+ const screenFiles = await glob(
2506
+ this.analyzerConfig.include || ["**/*.tsx", "**/*.ts"],
2507
+ {
2508
+ cwd: this.analyzerConfig.rootDir,
2509
+ ignore: this.analyzerConfig.exclude || ["**/node_modules/**"]
2510
+ }
2511
+ );
2512
+ console.log(`[MCPGenerator] Analyzing components and forms from ${screenFiles.length} files...`);
2513
+ const enrichmentPromises = screenFiles.map(async (file) => {
2514
+ const filePath2 = path4__default.resolve(this.analyzerConfig.rootDir, file);
2515
+ try {
2516
+ const [components, forms] = await Promise.all([
2517
+ componentAnalyzer.analyzeFile(filePath2),
2518
+ formAnalyzer.analyzeFile(filePath2)
2519
+ ]);
2520
+ return { filePath: filePath2, components, forms };
2521
+ } catch (error) {
2522
+ console.warn(`[MCPGenerator] Failed to analyze ${file}:`, error);
2523
+ return { filePath: filePath2, components: [], forms: [] };
2524
+ }
2525
+ });
2526
+ const enrichmentResults = await Promise.all(enrichmentPromises);
2527
+ const enrichmentMap = /* @__PURE__ */ new Map();
2528
+ for (const result of enrichmentResults) {
2529
+ enrichmentMap.set(result.filePath, {
2530
+ components: result.components,
2531
+ forms: result.forms
2532
+ });
2533
+ }
2534
+ const enrichedScreens = screens.map((screen) => {
2535
+ const enrichment = enrichmentMap.get(screen.filePath);
2536
+ if (enrichment) {
2537
+ const existingComponentNames = new Set(screen.components.map((c) => c.name));
2538
+ const newComponents = enrichment.components.filter(
2539
+ (c) => !existingComponentNames.has(c.name)
2540
+ );
2541
+ const mergedForms = screen.forms.map((form) => ({
2542
+ ...form,
2543
+ fields: [...form.fields]
2544
+ }));
2545
+ for (const newForm of enrichment.forms) {
2546
+ const existingForm = mergedForms.find((form) => form.id === newForm.id) ?? this.findFormWithSharedFields(mergedForms, newForm);
2547
+ if (existingForm) {
2548
+ this.mergeForm(existingForm, newForm);
2549
+ } else {
2550
+ mergedForms.push({
2551
+ ...newForm,
2552
+ fields: [...newForm.fields],
2553
+ submitAction: this.namedSubmitAction(newForm.submitAction)
2554
+ });
2555
+ }
2556
+ }
2557
+ return {
2558
+ ...screen,
2559
+ components: [...screen.components, ...newComponents],
2560
+ forms: mergedForms
2561
+ };
2562
+ }
2563
+ return screen;
2564
+ });
2565
+ const agentReadyScreens = enrichedScreens.map(enrichScreenForAgent);
2566
+ const projectInfo = await this.getProjectInfo();
2567
+ const projectName = projectInfo.name;
2568
+ console.log(`[MCPGenerator] Project name: ${projectName}`);
2569
+ const document = {
2570
+ version: this.options.version,
2571
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2572
+ projectName,
2573
+ screens: agentReadyScreens,
2574
+ navigation,
2575
+ metadata: {
2576
+ generatorVersion: "0.1.0",
2577
+ ...projectInfo.version ? { appVersion: projectInfo.version } : {},
2578
+ totalScreens: agentReadyScreens.length,
2579
+ totalForms: agentReadyScreens.reduce((acc, s) => acc + s.forms.length, 0),
2580
+ totalActions: agentReadyScreens.reduce((acc, s) => acc + s.actions.length, 0),
2581
+ analyzedFiles: screenFiles.length,
2582
+ // §D: Only set when strict mode filters out screens
2583
+ ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
2584
+ }
2585
+ };
2586
+ const serialized = JSON.stringify(document, null, 2);
2587
+ const checksum = this.calculateChecksum(serialized);
2588
+ const filePath = path4__default.resolve(
2589
+ outputDir,
2590
+ `mcp-document.${this.options.format}`
2591
+ );
2592
+ await writeFile(filePath, serialized, "utf-8");
2593
+ console.log(`[MCPGenerator] Document written to: ${filePath}`);
2594
+ const checksumFilePath = path4__default.resolve(outputDir, ".appilots-checksum");
2595
+ await writeFile(checksumFilePath, checksum, "utf-8");
2596
+ console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
2597
+ console.log("[MCPGenerator] Generation complete!");
2598
+ return {
2599
+ document,
2600
+ filePath,
2601
+ format: this.options.format,
2602
+ checksum
2603
+ };
2604
+ }
2605
+ /**
2606
+ * Read the previously stored checksum from disk.
2607
+ * Returns null if no checksum file exists (first run).
2608
+ *
2609
+ * §4: This replaces the incomplete `hasChanged` static method.
2610
+ * To detect actual changes, compare this value with `output.checksum`
2611
+ * after calling `generate()`.
2612
+ */
2613
+ static async readPreviousChecksum(outputDir) {
2614
+ const checksumFilePath = path4__default.resolve(outputDir, ".appilots-checksum");
2615
+ try {
2616
+ const content = await readFile(checksumFilePath, "utf-8");
2617
+ return content.trim() || null;
2618
+ } catch {
2619
+ return null;
2620
+ }
2621
+ }
2622
+ /**
2623
+ * Calculate SHA-256 checksum of content
2624
+ */
2625
+ calculateChecksum(content) {
2626
+ return createHash("sha256").update(content).digest("hex");
2627
+ }
2628
+ mergeForm(target, source) {
2629
+ for (const field of source.fields) {
2630
+ const existingField = this.findEquivalentField(target.fields, field);
2631
+ if (existingField) {
2632
+ this.mergeField(existingField, field);
2633
+ } else {
2634
+ target.fields.push(field);
2635
+ }
2636
+ }
2637
+ target.submitAction = target.submitAction ?? this.namedSubmitAction(source.submitAction);
2638
+ target.validationRules = source.validationRules ? { ...source.validationRules, ...target.validationRules ?? {} } : target.validationRules;
2639
+ }
2640
+ findEquivalentField(fields, incoming) {
2641
+ return fields.find((field) => {
2642
+ if (field.name && incoming.name && field.name === incoming.name) return true;
2643
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
2644
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
2645
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
2646
+ if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
2647
+ return true;
2648
+ }
2649
+ return false;
2650
+ });
2651
+ }
2652
+ mergeField(target, source) {
2653
+ if (this.isWeakInferredFieldName(target.name) && !this.isWeakInferredFieldName(source.name)) {
2654
+ target.name = source.name;
2655
+ }
2656
+ target.label = target.label ?? source.label;
2657
+ target.placeholder = target.placeholder ?? source.placeholder;
2658
+ target.options = target.options ?? source.options;
2659
+ target.locator = target.locator ?? source.locator;
2660
+ target.sourceComponent = target.sourceComponent ?? source.sourceComponent;
2661
+ target.valueBinding = target.valueBinding ?? source.valueBinding;
2662
+ target.errorBinding = target.errorBinding ?? source.errorBinding;
2663
+ target.required = target.required || source.required;
2664
+ if (target.type === "text" && source.type !== "text") {
2665
+ target.type = source.type;
2666
+ }
2667
+ }
2668
+ namedSubmitAction(submitAction) {
2669
+ return submitAction && submitAction !== "anonymous" ? submitAction : void 0;
2670
+ }
2671
+ isWeakInferredFieldName(name) {
2672
+ return /^(text|value|input|query|search|selected|checked)$/i.test(name);
2673
+ }
2674
+ findFormWithSharedFields(forms, incoming) {
2675
+ if (incoming.fields.length === 0) return void 0;
2676
+ let best;
2677
+ for (const form of forms) {
2678
+ const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
2679
+ if (overlap > 0 && (!best || overlap > best.overlap)) {
2680
+ best = { form, overlap };
2681
+ }
2682
+ }
2683
+ return best?.form;
2684
+ }
2685
+ /**
2686
+ * Get project name and version from package.json in rootDir
2687
+ */
2688
+ async getProjectInfo() {
2689
+ try {
2690
+ const packageJsonPath = path4__default.resolve(this.analyzerConfig.rootDir, "package.json");
2691
+ const packageJsonContent = await readFile(packageJsonPath, "utf-8");
2692
+ const packageJson = JSON.parse(packageJsonContent);
2693
+ return {
2694
+ name: packageJson.name || "Unknown Project",
2695
+ version: typeof packageJson.version === "string" ? packageJson.version : void 0
2696
+ };
2697
+ } catch (error) {
2698
+ console.warn("[MCPGenerator] Failed to read package.json, using default project name");
2699
+ return { name: "Unknown Project" };
2700
+ }
2701
+ }
2702
+ };
2703
+ function getEnvOverrides(env = process.env) {
2704
+ const clean = (value) => {
2705
+ const trimmed = value?.trim();
2706
+ return trimmed ? trimmed : void 0;
2707
+ };
2708
+ return {
2709
+ apiKey: clean(env.APPILOTS_API_KEY),
2710
+ projectId: clean(env.APPILOTS_PROJECT_ID),
2711
+ serverUrl: clean(env.APPILOTS_SERVER_URL)
2712
+ };
2713
+ }
2714
+ function loadConfig() {
2715
+ const configPath = getConfigPath();
2716
+ const env = getEnvOverrides();
2717
+ let fileConfig = {};
2718
+ if (configPath) {
2719
+ try {
2720
+ fileConfig = JSON.parse(readFileSync(configPath, "utf-8"));
2721
+ } catch (error) {
2722
+ throw new Error(`Failed to parse .appilotsrc at ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
2723
+ }
2724
+ } else if (!env.apiKey) {
2725
+ return null;
2726
+ }
2727
+ const merged = {
2728
+ serverUrl: "http://localhost:4000",
2729
+ outputDir: ".appilots",
2730
+ autoActivate: true,
2731
+ strictScreens: true,
2732
+ ...fileConfig,
2733
+ ...env.apiKey ? { apiKey: env.apiKey } : {},
2734
+ ...env.projectId ? { projectId: env.projectId } : {},
2735
+ ...env.serverUrl ? { serverUrl: env.serverUrl } : {}
2736
+ };
2737
+ const validation = validateConfig(merged);
2738
+ if (!validation.valid) {
2739
+ const source = configPath ?? "environment variables";
2740
+ throw new Error(
2741
+ `Invalid Appilots configuration (${source}):
2742
+ ` + validation.errors.map((e) => ` - ${e}`).join("\n") + "\nFix .appilotsrc or set APPILOTS_API_KEY / APPILOTS_SERVER_URL."
2743
+ );
2744
+ }
2745
+ return merged;
2746
+ }
2747
+ function saveConfig(dir, config) {
2748
+ const configPath = join(dir, ".appilotsrc");
2749
+ const existingConfig = getConfigPath() ? loadConfig() : null;
2750
+ const mergedConfig = {
2751
+ apiKey: config.apiKey || existingConfig?.apiKey || "",
2752
+ projectId: config.projectId || existingConfig?.projectId || "",
2753
+ serverUrl: config.serverUrl || existingConfig?.serverUrl || "http://localhost:4000",
2754
+ outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
2755
+ autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
2756
+ include: config.include || existingConfig?.include,
2757
+ exclude: config.exclude || existingConfig?.exclude,
2758
+ strictScreens: config.strictScreens ?? existingConfig?.strictScreens ?? true,
2759
+ screenPatterns: config.screenPatterns || existingConfig?.screenPatterns,
2760
+ navigationInclude: config.navigationInclude || existingConfig?.navigationInclude,
2761
+ navigationExclude: config.navigationExclude || existingConfig?.navigationExclude,
2762
+ eval: config.eval || existingConfig?.eval
2763
+ };
2764
+ try {
2765
+ writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
2766
+ } catch (error) {
2767
+ throw new Error(`Failed to write .appilotsrc to ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
2768
+ }
2769
+ }
2770
+ function getConfigPath() {
2771
+ let currentDir = resolve(process.cwd());
2772
+ const root = resolve("/");
2773
+ while (currentDir !== root) {
2774
+ const configPath = join(currentDir, ".appilotsrc");
2775
+ try {
2776
+ if (existsSync(configPath) && statSync(configPath).isFile()) {
2777
+ return configPath;
2778
+ }
2779
+ } catch {
2780
+ }
2781
+ currentDir = resolve(currentDir, "..");
2782
+ }
2783
+ const rootConfigPath = join(root, ".appilotsrc");
2784
+ try {
2785
+ if (existsSync(rootConfigPath) && statSync(rootConfigPath).isFile()) {
2786
+ return rootConfigPath;
2787
+ }
2788
+ } catch {
2789
+ }
2790
+ return null;
2791
+ }
2792
+ function validateConfig(config) {
2793
+ const errors = [];
2794
+ if (!config || typeof config !== "object") {
2795
+ return {
2796
+ valid: false,
2797
+ errors: ["Configuration must be an object"]
2798
+ };
2799
+ }
2800
+ if (!config.apiKey || typeof config.apiKey !== "string") {
2801
+ errors.push("apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)");
2802
+ } else if (!config.apiKey.startsWith("ak_")) {
2803
+ errors.push('apiKey must start with "ak_"');
2804
+ }
2805
+ if (config.projectId !== void 0 && config.projectId !== "" && typeof config.projectId !== "string") {
2806
+ errors.push("projectId must be a string");
2807
+ }
2808
+ if (config.serverUrl !== void 0 && typeof config.serverUrl !== "string") {
2809
+ errors.push("serverUrl must be a string");
2810
+ }
2811
+ if (config.outputDir !== void 0 && typeof config.outputDir !== "string") {
2812
+ errors.push("outputDir must be a string");
2813
+ }
2814
+ if (config.include !== void 0 && !Array.isArray(config.include)) {
2815
+ errors.push("include must be an array of strings");
2816
+ } else if (Array.isArray(config.include) && !config.include.every((item) => typeof item === "string")) {
2817
+ errors.push("include must be an array of strings");
2818
+ }
2819
+ if (config.exclude !== void 0 && !Array.isArray(config.exclude)) {
2820
+ errors.push("exclude must be an array of strings");
2821
+ } else if (Array.isArray(config.exclude) && !config.exclude.every((item) => typeof item === "string")) {
2822
+ errors.push("exclude must be an array of strings");
2823
+ }
2824
+ if (config.autoActivate !== void 0 && typeof config.autoActivate !== "boolean") {
2825
+ errors.push("autoActivate must be a boolean");
2826
+ }
2827
+ if (config.strictScreens !== void 0 && typeof config.strictScreens !== "boolean") {
2828
+ errors.push("strictScreens must be a boolean");
2829
+ }
2830
+ for (const field of ["screenPatterns", "navigationInclude", "navigationExclude"]) {
2831
+ const value = config[field];
2832
+ if (value !== void 0 && (!Array.isArray(value) || !value.every((item) => typeof item === "string"))) {
2833
+ errors.push(`${field} must be an array of strings`);
2834
+ }
2835
+ }
2836
+ if (config.eval !== void 0) {
2837
+ if (typeof config.eval !== "object" || config.eval === null || Array.isArray(config.eval)) {
2838
+ errors.push("eval must be an object");
2839
+ } else {
2840
+ const evalConfig = config.eval;
2841
+ if (evalConfig.scenariosDir !== void 0 && typeof evalConfig.scenariosDir !== "string") {
2842
+ errors.push("eval.scenariosDir must be a string");
2843
+ }
2844
+ if (evalConfig.baselinePath !== void 0 && typeof evalConfig.baselinePath !== "string") {
2845
+ errors.push("eval.baselinePath must be a string");
2846
+ }
2847
+ if (evalConfig.minPassRate !== void 0 && (typeof evalConfig.minPassRate !== "number" || evalConfig.minPassRate < 0 || evalConfig.minPassRate > 1)) {
2848
+ errors.push("eval.minPassRate must be a number between 0 and 1");
2849
+ }
2850
+ if (evalConfig.maxTokenRegression !== void 0 && (typeof evalConfig.maxTokenRegression !== "number" || evalConfig.maxTokenRegression < 0)) {
2851
+ errors.push("eval.maxTokenRegression must be a non-negative number");
2852
+ }
2853
+ }
2854
+ }
2855
+ return {
2856
+ valid: errors.length === 0,
2857
+ errors
2858
+ };
2859
+ }
2860
+
2861
+ // src/pipeline/metadata-lint.ts
2862
+ var MUTATION_VERB_RE = /\b(delete|destroy|remove|discard|wipe|revoke|cancel|reset|clear|create|add|register|save|submit|update|edit|change|toggle|pay|send|confirm|subscribe|unsubscribe|renew|downgrade|upgrade|transfer)\w*/i;
2863
+ function isMutating(action) {
2864
+ if (action.destructive || action.effect === "destructive") {
2865
+ return { mutating: true, reason: "flagged destructive" };
2866
+ }
2867
+ if (action.type === "submit" || action.type === "state_change") {
2868
+ return { mutating: true, reason: `type "${action.type}"` };
2869
+ }
2870
+ const haystack = `${action.id} ${action.label ?? ""} ${action.handler ?? ""}`;
2871
+ if ((action.type === "api_call" || action.type === "custom") && MUTATION_VERB_RE.test(haystack)) {
2872
+ return { mutating: true, reason: "mutation verb in id/label/handler" };
2873
+ }
2874
+ return { mutating: false, reason: "" };
2875
+ }
2876
+ function lintActionMetadata(document) {
2877
+ const warnings = [];
2878
+ for (const screen of document.screens) {
2879
+ for (const action of screen.actions ?? []) {
2880
+ const { mutating, reason } = isMutating(action);
2881
+ if (!mutating) continue;
2882
+ const missing = [];
2883
+ if (!action.effect) missing.push("effect");
2884
+ if (!action.riskLevel) missing.push("riskLevel");
2885
+ if (missing.length === 0) continue;
2886
+ warnings.push({
2887
+ screen: screen.name,
2888
+ actionId: action.id,
2889
+ actionType: action.type,
2890
+ missing,
2891
+ reason
2892
+ });
2893
+ }
2894
+ }
2895
+ return warnings;
2896
+ }
2897
+ function formatMetadataWarnings(warnings) {
2898
+ return warnings.map(
2899
+ (w) => `${w.screen}.${w.actionId} (${w.actionType}, ${w.reason}): missing ${w.missing.join(", ")}`
2900
+ );
2901
+ }
2902
+
2903
+ // src/services/api-client.ts
2904
+ var DEFAULT_TIMEOUT_MS = 3e4;
2905
+ var DEFAULT_MAX_RETRIES = 2;
2906
+ var RETRY_BASE_DELAY_MS = 500;
2907
+ var AppilotsAPIClient = class {
2908
+ serverUrl;
2909
+ apiKey;
2910
+ timeoutMs;
2911
+ maxRetries;
2912
+ constructor(config) {
2913
+ this.serverUrl = config.serverUrl.replace(/\/$/, "");
2914
+ this.apiKey = config.apiKey;
2915
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2916
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
2917
+ }
2918
+ /**
2919
+ * fetch with a hard timeout and exponential-backoff retries. Retries
2920
+ * only on network failures and 5xx responses — 4xx are the caller's
2921
+ * problem and retrying them would just spam the server.
2922
+ */
2923
+ async request(url, init) {
2924
+ let lastError;
2925
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
2926
+ if (attempt > 0) {
2927
+ await new Promise((r) => setTimeout(r, RETRY_BASE_DELAY_MS * 2 ** (attempt - 1)));
2928
+ }
2929
+ const controller = new AbortController();
2930
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
2931
+ try {
2932
+ const response = await fetch(url, { ...init, signal: controller.signal });
2933
+ if (response.status >= 500 && attempt < this.maxRetries) {
2934
+ lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);
2935
+ continue;
2936
+ }
2937
+ return response;
2938
+ } catch (err) {
2939
+ lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : err;
2940
+ } finally {
2941
+ clearTimeout(timer);
2942
+ }
2943
+ }
2944
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
2945
+ }
2946
+ /**
2947
+ * Syncs content with the Appilots backend
2948
+ *
2949
+ * @param content The MCP content object to sync
2950
+ * @param version Version string for the content
2951
+ * @param appVersion Host app version (package.json) — sent as
2952
+ * X-App-Version so the backend can associate the MCP
2953
+ * document with the app build that produced it
2954
+ * @returns SyncResult with success status and metadata
2955
+ */
2956
+ async sync(content, version, appVersion) {
2957
+ try {
2958
+ const response = await this.request(`${this.serverUrl}/api/v1/cli/sync`, {
2959
+ method: "POST",
2960
+ headers: {
2961
+ "Content-Type": "application/json",
2962
+ Authorization: `Bearer ${this.apiKey}`,
2963
+ ...appVersion ? { "X-App-Version": appVersion } : {}
2964
+ },
2965
+ body: JSON.stringify({ content, version })
2966
+ });
2967
+ if (!response.ok) {
2968
+ const errorData = await response.json().catch(() => ({}));
2969
+ return {
2970
+ success: false,
2971
+ unchanged: false,
2972
+ error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
2973
+ };
2974
+ }
2975
+ const json = await response.json();
2976
+ const inner = json.data ?? json;
2977
+ return {
2978
+ success: true,
2979
+ ...inner
2980
+ };
2981
+ } catch (error) {
2982
+ return {
2983
+ success: false,
2984
+ unchanged: false,
2985
+ error: error instanceof Error ? error.message : "Failed to sync with Appilots API"
2986
+ };
2987
+ }
2988
+ }
2989
+ /**
2990
+ * Gets the status of the Appilots project and active MCP
2991
+ *
2992
+ * @returns StatusResult with project and active MCP information
2993
+ */
2994
+ async status() {
2995
+ try {
2996
+ const response = await this.request(`${this.serverUrl}/api/v1/cli/status`, {
2997
+ method: "GET",
2998
+ headers: {
2999
+ Authorization: `Bearer ${this.apiKey}`
3000
+ }
3001
+ });
3002
+ if (!response.ok) {
3003
+ const errorData = await response.json().catch(() => ({}));
3004
+ return {
3005
+ error: errorData?.error || `HTTP ${response.status}: ${response.statusText}`
3006
+ };
3007
+ }
3008
+ const json = await response.json();
3009
+ const inner = json.data ?? json;
3010
+ return inner;
3011
+ } catch (error) {
3012
+ return {
3013
+ error: error instanceof Error ? error.message : "Failed to fetch status from Appilots API"
3014
+ };
3015
+ }
3016
+ }
3017
+ /**
3018
+ * Runs a batch of eval scenarios against the project's MCP document in
3019
+ * decision/dry-run mode — no side effects, same relay path as the
3020
+ * dashboard's sandbox, authenticated with this client's API key.
3021
+ *
3022
+ * @param scenarios Scenarios to run, capped server-side at
3023
+ * EVAL_MAX_SCENARIOS_PER_RUN (currently 20) as a cost guard.
3024
+ */
3025
+ async evalRun(scenarios) {
3026
+ try {
3027
+ const response = await this.request(`${this.serverUrl}/api/v1/cli/eval/run`, {
3028
+ method: "POST",
3029
+ headers: {
3030
+ "Content-Type": "application/json",
3031
+ Authorization: `Bearer ${this.apiKey}`
3032
+ },
3033
+ body: JSON.stringify({ scenarios })
3034
+ });
3035
+ if (!response.ok) {
3036
+ const errorData = await response.json().catch(() => ({}));
3037
+ const errObj = errorData?.error;
3038
+ return {
3039
+ error: errObj?.message || `HTTP ${response.status}: ${response.statusText}`
3040
+ };
3041
+ }
3042
+ const json = await response.json();
3043
+ const inner = json.data ?? json;
3044
+ return { results: inner.results ?? [] };
3045
+ } catch (error) {
3046
+ return {
3047
+ error: error instanceof Error ? error.message : "Failed to run eval against Appilots API"
3048
+ };
3049
+ }
3050
+ }
3051
+ /**
3052
+ * Checks if the Appilots API server is healthy
3053
+ *
3054
+ * @returns true if server is healthy, false otherwise
3055
+ */
3056
+ async health() {
3057
+ try {
3058
+ const response = await this.request(`${this.serverUrl}/api/v1/health`, {
3059
+ method: "GET"
3060
+ });
3061
+ return response.ok;
3062
+ } catch {
3063
+ return false;
3064
+ }
3065
+ }
3066
+ };
3067
+
3068
+ export { AppilotsAPIClient, ComponentAnalyzer, FormAnalyzer, MCPGenerator, NavigationAnalyzer, ScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, saveConfig, validateConfig };
3069
+ //# sourceMappingURL=index.mjs.map
3070
+ //# sourceMappingURL=index.mjs.map