@appilots/cli 0.4.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1901 -300
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +77 -9
- package/dist/index.d.ts +77 -9
- package/dist/index.js +794 -149
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +794 -150
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
package/dist/cli/index.js
CHANGED
|
@@ -28,12 +28,79 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
));
|
|
29
29
|
|
|
30
30
|
// src/cli/index.ts
|
|
31
|
-
var
|
|
31
|
+
var import_commander8 = require("commander");
|
|
32
|
+
|
|
33
|
+
// src/version.ts
|
|
34
|
+
var CLI_VERSION = "0.10.0";
|
|
32
35
|
|
|
33
36
|
// src/config/index.ts
|
|
34
37
|
var import_fs = require("fs");
|
|
35
38
|
var import_path = require("path");
|
|
36
39
|
var import_fs2 = require("fs");
|
|
40
|
+
var DEFAULT_SERVER_URL = "http://localhost:4000";
|
|
41
|
+
var KNOWN_CONFIG_KEYS = [
|
|
42
|
+
"apiKey",
|
|
43
|
+
"projectId",
|
|
44
|
+
"serverUrl",
|
|
45
|
+
"outputDir",
|
|
46
|
+
"include",
|
|
47
|
+
"exclude",
|
|
48
|
+
"autoActivate",
|
|
49
|
+
"strictScreens",
|
|
50
|
+
"screenPatterns",
|
|
51
|
+
"navigationInclude",
|
|
52
|
+
"navigationExclude",
|
|
53
|
+
"platform",
|
|
54
|
+
"manifestPath",
|
|
55
|
+
"eval"
|
|
56
|
+
];
|
|
57
|
+
var KEY_ALIASES = {
|
|
58
|
+
apiUrl: "serverUrl",
|
|
59
|
+
// `@appilots/sdk`'s own name for this. One `.appilotsrc` feeds both
|
|
60
|
+
// packages, so an integrator who wrote the SDK's key should not be told
|
|
61
|
+
// it is unrecognized.
|
|
62
|
+
apiBaseUrl: "serverUrl",
|
|
63
|
+
apiURL: "serverUrl",
|
|
64
|
+
baseUrl: "serverUrl",
|
|
65
|
+
url: "serverUrl",
|
|
66
|
+
host: "serverUrl",
|
|
67
|
+
endpoint: "serverUrl",
|
|
68
|
+
server: "serverUrl",
|
|
69
|
+
key: "apiKey",
|
|
70
|
+
token: "apiKey",
|
|
71
|
+
project: "projectId",
|
|
72
|
+
sourceDir: "include",
|
|
73
|
+
srcDir: "include",
|
|
74
|
+
source: "include",
|
|
75
|
+
outDir: "outputDir"
|
|
76
|
+
};
|
|
77
|
+
function editDistance(a, b) {
|
|
78
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
79
|
+
for (let i = 1; i <= a.length; i++) {
|
|
80
|
+
const current = [i];
|
|
81
|
+
for (let j = 1; j <= b.length; j++) {
|
|
82
|
+
const deletion = (previous[j] ?? 0) + 1;
|
|
83
|
+
const insertion = (current[j - 1] ?? 0) + 1;
|
|
84
|
+
const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
85
|
+
current.push(Math.min(deletion, insertion, substitution));
|
|
86
|
+
}
|
|
87
|
+
previous = current;
|
|
88
|
+
}
|
|
89
|
+
return previous[b.length] ?? 0;
|
|
90
|
+
}
|
|
91
|
+
function suggestConfigKey(key) {
|
|
92
|
+
const alias = KEY_ALIASES[key];
|
|
93
|
+
if (alias) return alias;
|
|
94
|
+
const lower = key.toLowerCase();
|
|
95
|
+
let best;
|
|
96
|
+
for (const known of KNOWN_CONFIG_KEYS) {
|
|
97
|
+
const distance = editDistance(lower, known.toLowerCase());
|
|
98
|
+
if (distance <= 2 && (!best || distance < best.distance)) {
|
|
99
|
+
best = { key: known, distance };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return best?.key;
|
|
103
|
+
}
|
|
37
104
|
function getEnvOverrides(env = process.env) {
|
|
38
105
|
const clean = (value) => {
|
|
39
106
|
const trimmed = value?.trim();
|
|
@@ -45,7 +112,7 @@ function getEnvOverrides(env = process.env) {
|
|
|
45
112
|
serverUrl: clean(env.APPILOTS_SERVER_URL)
|
|
46
113
|
};
|
|
47
114
|
}
|
|
48
|
-
function loadConfig() {
|
|
115
|
+
function loadConfig(onWarn) {
|
|
49
116
|
const configPath = getConfigPath();
|
|
50
117
|
const env = getEnvOverrides();
|
|
51
118
|
let fileConfig = {};
|
|
@@ -53,13 +120,15 @@ function loadConfig() {
|
|
|
53
120
|
try {
|
|
54
121
|
fileConfig = JSON.parse((0, import_fs.readFileSync)(configPath, "utf-8"));
|
|
55
122
|
} catch (error2) {
|
|
56
|
-
throw new Error(
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Failed to parse .appilotsrc at ${configPath}: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
125
|
+
);
|
|
57
126
|
}
|
|
58
127
|
} else if (!env.apiKey) {
|
|
59
128
|
return null;
|
|
60
129
|
}
|
|
61
130
|
const merged = {
|
|
62
|
-
serverUrl:
|
|
131
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
63
132
|
outputDir: ".appilots",
|
|
64
133
|
autoActivate: true,
|
|
65
134
|
strictScreens: true,
|
|
@@ -69,6 +138,12 @@ function loadConfig() {
|
|
|
69
138
|
...env.serverUrl ? { serverUrl: env.serverUrl } : {}
|
|
70
139
|
};
|
|
71
140
|
const validation = validateConfig(merged);
|
|
141
|
+
if (onWarn) {
|
|
142
|
+
const source = configPath ?? "environment variables";
|
|
143
|
+
for (const warning of validation.warnings) {
|
|
144
|
+
onWarn(`${source}: ${warning}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
72
147
|
if (!validation.valid) {
|
|
73
148
|
const source = configPath ?? "environment variables";
|
|
74
149
|
throw new Error(
|
|
@@ -84,7 +159,7 @@ function saveConfig(dir, config) {
|
|
|
84
159
|
const mergedConfig = {
|
|
85
160
|
apiKey: config.apiKey || existingConfig?.apiKey || "",
|
|
86
161
|
projectId: config.projectId || existingConfig?.projectId || "",
|
|
87
|
-
serverUrl: config.serverUrl || existingConfig?.serverUrl ||
|
|
162
|
+
serverUrl: config.serverUrl || existingConfig?.serverUrl || DEFAULT_SERVER_URL,
|
|
88
163
|
outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
|
|
89
164
|
autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
|
|
90
165
|
include: config.include || existingConfig?.include,
|
|
@@ -100,7 +175,9 @@ function saveConfig(dir, config) {
|
|
|
100
175
|
try {
|
|
101
176
|
(0, import_fs.writeFileSync)(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
|
|
102
177
|
} catch (error2) {
|
|
103
|
-
throw new Error(
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Failed to write .appilotsrc to ${configPath}: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
180
|
+
);
|
|
104
181
|
}
|
|
105
182
|
}
|
|
106
183
|
function getConfigPath() {
|
|
@@ -127,14 +204,25 @@ function getConfigPath() {
|
|
|
127
204
|
}
|
|
128
205
|
function validateConfig(config) {
|
|
129
206
|
const errors = [];
|
|
207
|
+
const warnings = [];
|
|
130
208
|
if (!config || typeof config !== "object") {
|
|
131
209
|
return {
|
|
132
210
|
valid: false,
|
|
133
|
-
errors: ["Configuration must be an object"]
|
|
211
|
+
errors: ["Configuration must be an object"],
|
|
212
|
+
warnings
|
|
134
213
|
};
|
|
135
214
|
}
|
|
215
|
+
for (const key of Object.keys(config)) {
|
|
216
|
+
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
217
|
+
const suggestion = suggestConfigKey(key);
|
|
218
|
+
warnings.push(
|
|
219
|
+
`unknown key "${key}" \u2014 ignored` + (suggestion ? ` (did you mean "${suggestion}"?)` : "")
|
|
220
|
+
);
|
|
221
|
+
}
|
|
136
222
|
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
137
|
-
errors.push(
|
|
223
|
+
errors.push(
|
|
224
|
+
"apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)"
|
|
225
|
+
);
|
|
138
226
|
} else if (!config.apiKey.startsWith("ak_")) {
|
|
139
227
|
errors.push('apiKey must start with "ak_"');
|
|
140
228
|
}
|
|
@@ -196,7 +284,8 @@ function validateConfig(config) {
|
|
|
196
284
|
}
|
|
197
285
|
return {
|
|
198
286
|
valid: errors.length === 0,
|
|
199
|
-
errors
|
|
287
|
+
errors,
|
|
288
|
+
warnings
|
|
200
289
|
};
|
|
201
290
|
}
|
|
202
291
|
|
|
@@ -241,14 +330,23 @@ function dim(msg) {
|
|
|
241
330
|
if (mode !== "pretty") return;
|
|
242
331
|
console.log(import_chalk.default.dim(msg));
|
|
243
332
|
}
|
|
333
|
+
var BANNER_WIDTH = 39;
|
|
334
|
+
function bannerRow(plain, colored) {
|
|
335
|
+
return ` ${import_chalk.default.cyan("\u2551")} ${colored}${" ".repeat(Math.max(0, BANNER_WIDTH - 5 - plain.length))}${import_chalk.default.cyan("\u2551")}`;
|
|
336
|
+
}
|
|
244
337
|
function banner() {
|
|
245
338
|
if (mode !== "pretty") return;
|
|
246
|
-
const
|
|
247
|
-
${
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
339
|
+
const title = "Appilots MCP Generator";
|
|
340
|
+
const version = `v${CLI_VERSION}`;
|
|
341
|
+
const border = "\u2550".repeat(BANNER_WIDTH);
|
|
342
|
+
const logo = [
|
|
343
|
+
"",
|
|
344
|
+
` ${import_chalk.default.cyan(`\u2554${border}\u2557`)}`,
|
|
345
|
+
bannerRow(title, import_chalk.default.bold.cyan(title)),
|
|
346
|
+
bannerRow(version, import_chalk.default.dim(version)),
|
|
347
|
+
` ${import_chalk.default.cyan(`\u255A${border}\u255D`)}`,
|
|
348
|
+
" "
|
|
349
|
+
].join("\n");
|
|
252
350
|
console.log(logo);
|
|
253
351
|
}
|
|
254
352
|
function heading(msg) {
|
|
@@ -269,19 +367,105 @@ function table(rows) {
|
|
|
269
367
|
// src/cli/commands/init.ts
|
|
270
368
|
var import_commander = require("commander");
|
|
271
369
|
|
|
370
|
+
// ../shared/dist/chunk-UEMUBXFH.mjs
|
|
371
|
+
var DEFAULT_API_BASE_URL = "https://api.appilots.com";
|
|
372
|
+
var APPILOTS_API_PATH_PREFIX = "/api/v1";
|
|
373
|
+
function normalizeApiBaseUrl(configured) {
|
|
374
|
+
const raw = (configured ?? DEFAULT_API_BASE_URL).trim().replace(/\/+$/, "");
|
|
375
|
+
if (!raw) return `${DEFAULT_API_BASE_URL}${APPILOTS_API_PATH_PREFIX}`;
|
|
376
|
+
if (new RegExp(`${APPILOTS_API_PATH_PREFIX}(/|$)`).test(raw)) return raw;
|
|
377
|
+
return `${raw}${APPILOTS_API_PATH_PREFIX}`;
|
|
378
|
+
}
|
|
379
|
+
var LIMITS = {
|
|
380
|
+
MAX_MCP_DOCUMENT_SIZE_MB: 10,
|
|
381
|
+
MAX_PROJECTS_PER_USER: 20,
|
|
382
|
+
MAX_MESSAGE_LENGTH: 4096,
|
|
383
|
+
MAX_ACTIONS_PER_MESSAGE: 10,
|
|
384
|
+
SESSION_TIMEOUT_MS: 30 * 60 * 1e3
|
|
385
|
+
// 30 minutes
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// src/utils/stringify.ts
|
|
389
|
+
function stringifyUnknown(value) {
|
|
390
|
+
if (value instanceof Error) {
|
|
391
|
+
const message = value.message || value.name;
|
|
392
|
+
const cause = value.cause;
|
|
393
|
+
if (cause instanceof Error && cause.message && cause.message !== message) {
|
|
394
|
+
return `${message} (${cause.message})`;
|
|
395
|
+
}
|
|
396
|
+
return message;
|
|
397
|
+
}
|
|
398
|
+
if (typeof value === "string") return value;
|
|
399
|
+
if (value === null || value === void 0) return String(value);
|
|
400
|
+
if (typeof value !== "object") return String(value);
|
|
401
|
+
try {
|
|
402
|
+
const json = JSON.stringify(value);
|
|
403
|
+
if (json && json !== "{}") return json;
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
return String(value);
|
|
407
|
+
}
|
|
408
|
+
|
|
272
409
|
// src/services/api-client.ts
|
|
273
|
-
var
|
|
410
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
274
411
|
var DEFAULT_MAX_RETRIES = 2;
|
|
275
412
|
var RETRY_BASE_DELAY_MS = 500;
|
|
413
|
+
function describeNetworkError(url, err) {
|
|
414
|
+
let origin = url;
|
|
415
|
+
try {
|
|
416
|
+
origin = new URL(url).origin;
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
const cause = err?.cause;
|
|
420
|
+
const code = typeof cause?.code === "string" ? cause.code : void 0;
|
|
421
|
+
const detail = code ?? stringifyUnknown(err);
|
|
422
|
+
return new Error(`Cannot reach Appilots server at ${origin} (${detail})`);
|
|
423
|
+
}
|
|
424
|
+
function errorCodeOf(payload) {
|
|
425
|
+
const error2 = payload?.error;
|
|
426
|
+
if (error2 && typeof error2 === "object") {
|
|
427
|
+
const { code } = error2;
|
|
428
|
+
if (typeof code === "string" && code.trim()) return code;
|
|
429
|
+
}
|
|
430
|
+
return void 0;
|
|
431
|
+
}
|
|
432
|
+
function describeApiError(payload, fallback) {
|
|
433
|
+
const error2 = payload?.error;
|
|
434
|
+
if (typeof error2 === "string" && error2.trim()) return error2;
|
|
435
|
+
if (error2 && typeof error2 === "object") {
|
|
436
|
+
const { code, message: message2 } = error2;
|
|
437
|
+
const text = typeof message2 === "string" && message2.trim() ? message2 : void 0;
|
|
438
|
+
const tag = typeof code === "string" && code.trim() ? code : void 0;
|
|
439
|
+
if (text && tag) return `${text} (${tag})`;
|
|
440
|
+
if (text) return text;
|
|
441
|
+
if (tag) return tag;
|
|
442
|
+
}
|
|
443
|
+
const message = payload?.message;
|
|
444
|
+
if (typeof message === "string" && message.trim()) return message;
|
|
445
|
+
return fallback;
|
|
446
|
+
}
|
|
276
447
|
var AppilotsAPIClient = class {
|
|
277
|
-
|
|
448
|
+
/**
|
|
449
|
+
* `serverUrl` with the API's mount prefix resolved — every path below
|
|
450
|
+
* is relative to THIS, not to the configured origin.
|
|
451
|
+
*
|
|
452
|
+
* It used to be the raw `serverUrl` with `/api/v1` hardcoded into each
|
|
453
|
+
* path, which made the field mean something different here than in the
|
|
454
|
+
* SDK. Both read the same `.appilotsrc`: the SDK completes the prefix
|
|
455
|
+
* when it is missing, so `https://api.appilots.com/api/v1` is correct
|
|
456
|
+
* there — and here that same value produced
|
|
457
|
+
* `/api/v1/api/v1/cli/sync`, a 404 whose body reads `Route not found`.
|
|
458
|
+
* Sharing `normalizeApiBaseUrl` is what makes one file mean one thing:
|
|
459
|
+
* with or without the prefix now works in both.
|
|
460
|
+
*/
|
|
461
|
+
baseUrl;
|
|
278
462
|
apiKey;
|
|
279
463
|
timeoutMs;
|
|
280
464
|
maxRetries;
|
|
281
465
|
constructor(config) {
|
|
282
|
-
this.
|
|
466
|
+
this.baseUrl = normalizeApiBaseUrl(config.serverUrl);
|
|
283
467
|
this.apiKey = config.apiKey;
|
|
284
|
-
this.timeoutMs = config.timeoutMs ??
|
|
468
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
285
469
|
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
286
470
|
}
|
|
287
471
|
/**
|
|
@@ -305,7 +489,7 @@ var AppilotsAPIClient = class {
|
|
|
305
489
|
}
|
|
306
490
|
return response;
|
|
307
491
|
} catch (err) {
|
|
308
|
-
lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : err;
|
|
492
|
+
lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : describeNetworkError(url, err);
|
|
309
493
|
} finally {
|
|
310
494
|
clearTimeout(timer);
|
|
311
495
|
}
|
|
@@ -324,7 +508,7 @@ var AppilotsAPIClient = class {
|
|
|
324
508
|
*/
|
|
325
509
|
async sync(content, version, appVersion) {
|
|
326
510
|
try {
|
|
327
|
-
const response = await this.request(`${this.
|
|
511
|
+
const response = await this.request(`${this.baseUrl}/cli/sync`, {
|
|
328
512
|
method: "POST",
|
|
329
513
|
headers: {
|
|
330
514
|
"Content-Type": "application/json",
|
|
@@ -338,7 +522,9 @@ var AppilotsAPIClient = class {
|
|
|
338
522
|
return {
|
|
339
523
|
success: false,
|
|
340
524
|
unchanged: false,
|
|
341
|
-
error: errorData
|
|
525
|
+
error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`),
|
|
526
|
+
errorCode: errorCodeOf(errorData),
|
|
527
|
+
errorStatus: response.status
|
|
342
528
|
};
|
|
343
529
|
}
|
|
344
530
|
const json = await response.json();
|
|
@@ -351,7 +537,8 @@ var AppilotsAPIClient = class {
|
|
|
351
537
|
return {
|
|
352
538
|
success: false,
|
|
353
539
|
unchanged: false,
|
|
354
|
-
error: error2 instanceof Error ? error2.message : "Failed to sync with Appilots API"
|
|
540
|
+
error: error2 instanceof Error ? error2.message : "Failed to sync with Appilots API",
|
|
541
|
+
errorStatus: 0
|
|
355
542
|
};
|
|
356
543
|
}
|
|
357
544
|
}
|
|
@@ -362,7 +549,7 @@ var AppilotsAPIClient = class {
|
|
|
362
549
|
*/
|
|
363
550
|
async status() {
|
|
364
551
|
try {
|
|
365
|
-
const response = await this.request(`${this.
|
|
552
|
+
const response = await this.request(`${this.baseUrl}/cli/status`, {
|
|
366
553
|
method: "GET",
|
|
367
554
|
headers: {
|
|
368
555
|
Authorization: `Bearer ${this.apiKey}`
|
|
@@ -371,7 +558,7 @@ var AppilotsAPIClient = class {
|
|
|
371
558
|
if (!response.ok) {
|
|
372
559
|
const errorData = await response.json().catch(() => ({}));
|
|
373
560
|
return {
|
|
374
|
-
error: errorData
|
|
561
|
+
error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
|
|
375
562
|
};
|
|
376
563
|
}
|
|
377
564
|
const json = await response.json();
|
|
@@ -393,7 +580,7 @@ var AppilotsAPIClient = class {
|
|
|
393
580
|
*/
|
|
394
581
|
async evalRun(scenarios) {
|
|
395
582
|
try {
|
|
396
|
-
const response = await this.request(`${this.
|
|
583
|
+
const response = await this.request(`${this.baseUrl}/cli/eval/run`, {
|
|
397
584
|
method: "POST",
|
|
398
585
|
headers: {
|
|
399
586
|
"Content-Type": "application/json",
|
|
@@ -424,7 +611,7 @@ var AppilotsAPIClient = class {
|
|
|
424
611
|
*/
|
|
425
612
|
async health() {
|
|
426
613
|
try {
|
|
427
|
-
const response = await this.request(`${this.
|
|
614
|
+
const response = await this.request(`${this.baseUrl}/health`, {
|
|
428
615
|
method: "GET"
|
|
429
616
|
});
|
|
430
617
|
return response.ok;
|
|
@@ -437,6 +624,19 @@ var AppilotsAPIClient = class {
|
|
|
437
624
|
// src/cli/commands/init.ts
|
|
438
625
|
var import_fs3 = require("fs");
|
|
439
626
|
var import_path2 = require("path");
|
|
627
|
+
|
|
628
|
+
// src/cli/utils/server-hint.ts
|
|
629
|
+
function hintUnreachableServer(serverUrl, message) {
|
|
630
|
+
if (serverUrl !== DEFAULT_SERVER_URL) return;
|
|
631
|
+
if (typeof message === "string" && !message.includes("Cannot reach Appilots server")) return;
|
|
632
|
+
if (message !== void 0 && typeof message !== "string") return;
|
|
633
|
+
info(`serverUrl is not set, so the CLI is using the built-in default (${DEFAULT_SERVER_URL})`);
|
|
634
|
+
info("For the hosted backend, set it in .appilotsrc:");
|
|
635
|
+
info(' "serverUrl": "https://api.appilots.com"');
|
|
636
|
+
info("Or pass --server <url>, or set APPILOTS_SERVER_URL");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/cli/commands/init.ts
|
|
440
640
|
function ensureConfigIgnored(dir) {
|
|
441
641
|
const gitignorePath = (0, import_path2.join)(dir, ".gitignore");
|
|
442
642
|
if (!(0, import_fs3.existsSync)(gitignorePath)) {
|
|
@@ -450,7 +650,10 @@ function ensureConfigIgnored(dir) {
|
|
|
450
650
|
if (ignored) {
|
|
451
651
|
return "already-ignored";
|
|
452
652
|
}
|
|
453
|
-
(0, import_fs3.appendFileSync)(
|
|
653
|
+
(0, import_fs3.appendFileSync)(
|
|
654
|
+
gitignorePath,
|
|
655
|
+
"\n# Appilots CLI config (contains your API key \u2014 do not commit)\n.appilotsrc\n"
|
|
656
|
+
);
|
|
454
657
|
return "added";
|
|
455
658
|
}
|
|
456
659
|
function initCommand() {
|
|
@@ -473,7 +676,7 @@ function initCommand() {
|
|
|
473
676
|
if (!options.apiKey && env.apiKey) {
|
|
474
677
|
info("Using API key from APPILOTS_API_KEY environment variable");
|
|
475
678
|
}
|
|
476
|
-
const serverUrl = options.server || env.serverUrl ||
|
|
679
|
+
const serverUrl = options.server || env.serverUrl || DEFAULT_SERVER_URL;
|
|
477
680
|
if (!apiKey.startsWith("ak_")) {
|
|
478
681
|
error("Invalid API key format. Must start with ak_");
|
|
479
682
|
process.exit(1);
|
|
@@ -487,6 +690,7 @@ function initCommand() {
|
|
|
487
690
|
if (!isHealthy) {
|
|
488
691
|
error(`Cannot reach Appilots server at ${serverUrl}`);
|
|
489
692
|
info("Make sure the server is running and the URL is correct");
|
|
693
|
+
hintUnreachableServer(serverUrl);
|
|
490
694
|
process.exit(1);
|
|
491
695
|
}
|
|
492
696
|
const status = await apiClient.status();
|
|
@@ -524,7 +728,7 @@ function initCommand() {
|
|
|
524
728
|
dim(" $ appilots sync - Analyze and upload your project");
|
|
525
729
|
dim(" $ appilots watch - Watch for changes and auto-sync");
|
|
526
730
|
} catch (err) {
|
|
527
|
-
error(`Initialization failed: ${
|
|
731
|
+
error(`Initialization failed: ${stringifyUnknown(err)}`);
|
|
528
732
|
process.exit(1);
|
|
529
733
|
}
|
|
530
734
|
});
|
|
@@ -533,7 +737,7 @@ function initCommand() {
|
|
|
533
737
|
// src/cli/commands/sync.ts
|
|
534
738
|
var import_commander2 = require("commander");
|
|
535
739
|
var import_promises5 = require("fs/promises");
|
|
536
|
-
var
|
|
740
|
+
var import_node_path5 = require("path");
|
|
537
741
|
|
|
538
742
|
// src/generators/MCPGenerator.ts
|
|
539
743
|
var import_promises4 = require("fs/promises");
|
|
@@ -813,6 +1017,45 @@ function hasJsxAttribute(element, name) {
|
|
|
813
1017
|
}
|
|
814
1018
|
|
|
815
1019
|
// src/ast/jsx/classify.ts
|
|
1020
|
+
var NON_INTERACTION_HANDLERS = /* @__PURE__ */ new Set([
|
|
1021
|
+
"onLayout",
|
|
1022
|
+
"onScroll",
|
|
1023
|
+
"onScrollBeginDrag",
|
|
1024
|
+
"onScrollEndDrag",
|
|
1025
|
+
"onMomentumScrollBegin",
|
|
1026
|
+
"onMomentumScrollEnd",
|
|
1027
|
+
"onContentSizeChange",
|
|
1028
|
+
"onEndReached",
|
|
1029
|
+
"onViewableItemsChanged",
|
|
1030
|
+
"onLoad",
|
|
1031
|
+
"onLoadStart",
|
|
1032
|
+
"onLoadEnd",
|
|
1033
|
+
"onError",
|
|
1034
|
+
"onProgress",
|
|
1035
|
+
"onFocus",
|
|
1036
|
+
"onBlur"
|
|
1037
|
+
]);
|
|
1038
|
+
var VALUE_HANDLERS = /* @__PURE__ */ new Set([
|
|
1039
|
+
"onChange",
|
|
1040
|
+
"onChangeText",
|
|
1041
|
+
"onValueChange",
|
|
1042
|
+
"onSelect",
|
|
1043
|
+
"onSelectionChange",
|
|
1044
|
+
"onSubmitEditing"
|
|
1045
|
+
]);
|
|
1046
|
+
function pressHandlerProp(element) {
|
|
1047
|
+
let fallback;
|
|
1048
|
+
for (const attr of element.attributes) {
|
|
1049
|
+
if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
|
|
1050
|
+
const name = attr.name.name;
|
|
1051
|
+
if (!/^on[A-Z]/.test(name)) continue;
|
|
1052
|
+
if (attr.value?.type !== "JSXExpressionContainer") continue;
|
|
1053
|
+
if (name === "onPress") return name;
|
|
1054
|
+
if (NON_INTERACTION_HANDLERS.has(name) || VALUE_HANDLERS.has(name)) continue;
|
|
1055
|
+
fallback ??= name;
|
|
1056
|
+
}
|
|
1057
|
+
return fallback;
|
|
1058
|
+
}
|
|
816
1059
|
var VIEW_COMPONENTS = /* @__PURE__ */ new Set(["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"]);
|
|
817
1060
|
var INPUT_COMPONENTS = /* @__PURE__ */ new Set(["TextInput", "Input", "HocInput"]);
|
|
818
1061
|
var BUTTON_COMPONENTS = /* @__PURE__ */ new Set(["TouchableOpacity", "Pressable", "Button"]);
|
|
@@ -827,17 +1070,16 @@ function classifyJsxComponent(name, element) {
|
|
|
827
1070
|
if (INPUT_COMPONENTS.has(name)) return "input";
|
|
828
1071
|
if (BUTTON_COMPONENTS.has(name)) return "button";
|
|
829
1072
|
if (element) {
|
|
830
|
-
const hasOptions = hasJsxAttribute(element, "options");
|
|
1073
|
+
const hasOptions = hasJsxAttribute(element, "options") || hasJsxAttribute(element, "segments") || hasJsxAttribute(element, "choices") || hasJsxAttribute(element, "tabs");
|
|
831
1074
|
const hasValue = hasJsxAttribute(element, "value");
|
|
832
1075
|
const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
|
|
833
1076
|
const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange") || hasJsxAttribute(element, "onChangeText");
|
|
834
|
-
const hasOnPress = hasJsxAttribute(element, "onPress");
|
|
835
1077
|
if (hasOptions && (hasValue || hasOnChange)) return "select";
|
|
836
1078
|
if (hasChecked && hasOnChange) return "toggle";
|
|
837
1079
|
if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
|
|
838
1080
|
return "input";
|
|
839
1081
|
}
|
|
840
|
-
if (
|
|
1082
|
+
if (pressHandlerProp(element)) return "button";
|
|
841
1083
|
if (getStringAttr(element, "visible") || getExpressionIdentifierAttr(element, "visible")) {
|
|
842
1084
|
return "modal";
|
|
843
1085
|
}
|
|
@@ -1165,8 +1407,8 @@ var ScreenAnalyzer = class {
|
|
|
1165
1407
|
}
|
|
1166
1408
|
const screens = [];
|
|
1167
1409
|
this.screensFilteredOut = 0;
|
|
1168
|
-
for (const
|
|
1169
|
-
const filePath = import_path3.default.resolve(this.config.rootDir,
|
|
1410
|
+
for (const file2 of files) {
|
|
1411
|
+
const filePath = import_path3.default.resolve(this.config.rootDir, file2);
|
|
1170
1412
|
try {
|
|
1171
1413
|
const descriptor = await this.analyzeFile(filePath);
|
|
1172
1414
|
if (!descriptor) continue;
|
|
@@ -1176,23 +1418,28 @@ var ScreenAnalyzer = class {
|
|
|
1176
1418
|
if (!hasRegisterScreen && !matchesPattern) {
|
|
1177
1419
|
this.screensFilteredOut++;
|
|
1178
1420
|
if (this.verbose) {
|
|
1179
|
-
console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${
|
|
1421
|
+
console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file2}`);
|
|
1180
1422
|
}
|
|
1181
1423
|
continue;
|
|
1182
1424
|
}
|
|
1183
1425
|
}
|
|
1184
1426
|
screens.push(descriptor);
|
|
1185
1427
|
if (this.verbose) {
|
|
1186
|
-
console.log(`[ScreenAnalyzer] \u2713 Analyzed: ${descriptor.name} (${
|
|
1428
|
+
console.log(`[ScreenAnalyzer] \u2713 Analyzed: ${descriptor.name} (${file2})`);
|
|
1187
1429
|
}
|
|
1188
1430
|
} catch (error2) {
|
|
1189
1431
|
if (this.verbose) {
|
|
1190
|
-
console.warn(
|
|
1432
|
+
console.warn(
|
|
1433
|
+
`[ScreenAnalyzer] Failed to parse ${file2}:`,
|
|
1434
|
+
error2 instanceof Error ? error2.message : error2
|
|
1435
|
+
);
|
|
1191
1436
|
}
|
|
1192
1437
|
}
|
|
1193
1438
|
}
|
|
1194
1439
|
if (this.verbose && this.strictScreens) {
|
|
1195
|
-
console.log(
|
|
1440
|
+
console.log(
|
|
1441
|
+
`[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`
|
|
1442
|
+
);
|
|
1196
1443
|
}
|
|
1197
1444
|
return screens;
|
|
1198
1445
|
}
|
|
@@ -1480,7 +1727,11 @@ var ScreenAnalyzer = class {
|
|
|
1480
1727
|
for (const form of secondary) {
|
|
1481
1728
|
const existing = out.find((candidate) => candidate.id === form.id) ?? this.findFormWithSharedFields(out, form);
|
|
1482
1729
|
if (!existing) {
|
|
1483
|
-
out.push({
|
|
1730
|
+
out.push({
|
|
1731
|
+
...form,
|
|
1732
|
+
fields: [...form.fields],
|
|
1733
|
+
submitAction: this.namedSubmitAction(form.submitAction)
|
|
1734
|
+
});
|
|
1484
1735
|
continue;
|
|
1485
1736
|
}
|
|
1486
1737
|
for (const field of form.fields) {
|
|
@@ -1499,9 +1750,12 @@ var ScreenAnalyzer = class {
|
|
|
1499
1750
|
findEquivalentField(fields, incoming) {
|
|
1500
1751
|
return fields.find((field) => {
|
|
1501
1752
|
if (field.name && incoming.name && field.name === incoming.name) return true;
|
|
1502
|
-
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
1503
|
-
|
|
1504
|
-
if (field.
|
|
1753
|
+
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
1754
|
+
return true;
|
|
1755
|
+
if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
|
|
1756
|
+
return true;
|
|
1757
|
+
if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
|
|
1758
|
+
return true;
|
|
1505
1759
|
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
1506
1760
|
return true;
|
|
1507
1761
|
}
|
|
@@ -1661,9 +1915,11 @@ var ScreenAnalyzer = class {
|
|
|
1661
1915
|
source: field.locator?.source ?? "accessibilityLabel"
|
|
1662
1916
|
});
|
|
1663
1917
|
} else if (attrName === "keyboardType") {
|
|
1664
|
-
if (attr.value.value === "email-address" || attr.value.value === "email")
|
|
1918
|
+
if (attr.value.value === "email-address" || attr.value.value === "email")
|
|
1919
|
+
field.type = "email";
|
|
1665
1920
|
if (attr.value.value === "phone-pad") field.type = "phone";
|
|
1666
|
-
if (attr.value.value === "numeric" || attr.value.value === "number-pad")
|
|
1921
|
+
if (attr.value.value === "numeric" || attr.value.value === "number-pad")
|
|
1922
|
+
field.type = "number";
|
|
1667
1923
|
}
|
|
1668
1924
|
} else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
|
|
1669
1925
|
if (attrName === "value" && BabelTypes.isIdentifier(attr.value.expression)) {
|
|
@@ -1829,8 +2085,10 @@ var ScreenAnalyzer = class {
|
|
|
1829
2085
|
};
|
|
1830
2086
|
if (handler.nativeConfirmationExpected) action.nativeConfirmationExpected = true;
|
|
1831
2087
|
if (handler.targetScreen && !action.targetScreen) action.targetScreen = handler.targetScreen;
|
|
1832
|
-
if (handler.successSignal && !action.successSignal)
|
|
1833
|
-
|
|
2088
|
+
if (handler.successSignal && !action.successSignal)
|
|
2089
|
+
action.successSignal = handler.successSignal;
|
|
2090
|
+
if (handler.failureSignal && !action.failureSignal)
|
|
2091
|
+
action.failureSignal = handler.failureSignal;
|
|
1834
2092
|
if (handler.opensModal && !action.opensModal) action.opensModal = handler.opensModal;
|
|
1835
2093
|
if (handler.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
|
|
1836
2094
|
action.destructive = true;
|
|
@@ -1848,6 +2106,7 @@ var ScreenAnalyzer = class {
|
|
|
1848
2106
|
if (!BabelTypes.isJSXIdentifier(element.name)) return;
|
|
1849
2107
|
const componentName = element.name.name;
|
|
1850
2108
|
if (classifyJsxComponent(componentName, element) !== "button") return;
|
|
2109
|
+
const pressProp = pressHandlerProp(element);
|
|
1851
2110
|
let label;
|
|
1852
2111
|
let handler;
|
|
1853
2112
|
for (const attr of element.attributes) {
|
|
@@ -1857,7 +2116,7 @@ var ScreenAnalyzer = class {
|
|
|
1857
2116
|
if ((attrName === "title" || attrName === "accessibilityLabel" || attrName === "label") && BabelTypes.isStringLiteral(attr.value)) {
|
|
1858
2117
|
label = attr.value.value;
|
|
1859
2118
|
}
|
|
1860
|
-
if (attrName ===
|
|
2119
|
+
if (attrName === pressProp && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
|
|
1861
2120
|
handler = attr.value.expression.name;
|
|
1862
2121
|
}
|
|
1863
2122
|
}
|
|
@@ -1923,10 +2182,13 @@ var ScreenAnalyzer = class {
|
|
|
1923
2182
|
}
|
|
1924
2183
|
};
|
|
1925
2184
|
if (fn.body) {
|
|
1926
|
-
(0, import_traverse4.default)(
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2185
|
+
(0, import_traverse4.default)(
|
|
2186
|
+
fn.body,
|
|
2187
|
+
{
|
|
2188
|
+
noScope: true,
|
|
2189
|
+
enter: (nodePath) => inspectNode(nodePath.node)
|
|
2190
|
+
}
|
|
2191
|
+
);
|
|
1930
2192
|
}
|
|
1931
2193
|
const expectedOutcome = targetScreen ? "navigation" : hasStateSetter ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
|
|
1932
2194
|
return {
|
|
@@ -1984,6 +2246,7 @@ var ScreenAnalyzer = class {
|
|
|
1984
2246
|
};
|
|
1985
2247
|
const componentName = BabelTypes.isJSXIdentifier(element.name) ? element.name.name : void 0;
|
|
1986
2248
|
action.sourceComponent = componentName;
|
|
2249
|
+
const pressProp = pressHandlerProp(element) ?? "onPress";
|
|
1987
2250
|
let handlerName;
|
|
1988
2251
|
for (const attr of element.attributes) {
|
|
1989
2252
|
if (!BabelTypes.isJSXAttribute(attr)) continue;
|
|
@@ -2018,7 +2281,7 @@ var ScreenAnalyzer = class {
|
|
|
2018
2281
|
}
|
|
2019
2282
|
}
|
|
2020
2283
|
} else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
|
|
2021
|
-
if (attrName ===
|
|
2284
|
+
if (attrName === pressProp && BabelTypes.isIdentifier(attr.value.expression)) {
|
|
2022
2285
|
handlerName = attr.value.expression.name.toLowerCase();
|
|
2023
2286
|
action.handler = attr.value.expression.name;
|
|
2024
2287
|
if (handlerName.includes("submit")) {
|
|
@@ -2026,7 +2289,7 @@ var ScreenAnalyzer = class {
|
|
|
2026
2289
|
} else if (handlerName.includes("navigate")) {
|
|
2027
2290
|
action.type = "navigation";
|
|
2028
2291
|
}
|
|
2029
|
-
} else if (attrName ===
|
|
2292
|
+
} else if (attrName === pressProp) {
|
|
2030
2293
|
const inline = this.extractInlineOnPressMetadata(attr.value.expression);
|
|
2031
2294
|
if (inline.handler) {
|
|
2032
2295
|
handlerName = inline.handler.toLowerCase();
|
|
@@ -2050,7 +2313,11 @@ var ScreenAnalyzer = class {
|
|
|
2050
2313
|
}
|
|
2051
2314
|
}
|
|
2052
2315
|
if (!action.locator && action.id) {
|
|
2053
|
-
action.locator = {
|
|
2316
|
+
action.locator = {
|
|
2317
|
+
id: action.id,
|
|
2318
|
+
label: action.label,
|
|
2319
|
+
source: action.label ? "label" : "inferred"
|
|
2320
|
+
};
|
|
2054
2321
|
}
|
|
2055
2322
|
if (this.isElementDestructive(element, handlerName, action.id)) {
|
|
2056
2323
|
action.destructive = true;
|
|
@@ -2192,24 +2459,27 @@ var ScreenAnalyzer = class {
|
|
|
2192
2459
|
}
|
|
2193
2460
|
extractRowAction(fn) {
|
|
2194
2461
|
let action;
|
|
2195
|
-
(0, import_traverse4.default)(
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2462
|
+
(0, import_traverse4.default)(
|
|
2463
|
+
fn.body,
|
|
2464
|
+
{
|
|
2465
|
+
noScope: true,
|
|
2466
|
+
CallExpression: (nodePath) => {
|
|
2467
|
+
const node = nodePath.node;
|
|
2468
|
+
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)) {
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
const firstArg = node.arguments[0];
|
|
2472
|
+
if (!BabelTypes.isStringLiteral(firstArg)) return;
|
|
2473
|
+
const params = this.extractNavigationParams(node.arguments[1]);
|
|
2474
|
+
action = {
|
|
2475
|
+
type: "navigation",
|
|
2476
|
+
targetScreen: firstArg.value,
|
|
2477
|
+
...Object.keys(params).length > 0 ? { params } : {},
|
|
2478
|
+
description: `Pressing a row opens ${firstArg.value}`
|
|
2479
|
+
};
|
|
2201
2480
|
}
|
|
2202
|
-
const firstArg = node.arguments[0];
|
|
2203
|
-
if (!BabelTypes.isStringLiteral(firstArg)) return;
|
|
2204
|
-
const params = this.extractNavigationParams(node.arguments[1]);
|
|
2205
|
-
action = {
|
|
2206
|
-
type: "navigation",
|
|
2207
|
-
targetScreen: firstArg.value,
|
|
2208
|
-
...Object.keys(params).length > 0 ? { params } : {},
|
|
2209
|
-
description: `Pressing a row opens ${firstArg.value}`
|
|
2210
|
-
};
|
|
2211
2481
|
}
|
|
2212
|
-
|
|
2482
|
+
);
|
|
2213
2483
|
return action;
|
|
2214
2484
|
}
|
|
2215
2485
|
extractNavigationParams(arg) {
|
|
@@ -2242,15 +2512,18 @@ var ScreenAnalyzer = class {
|
|
|
2242
2512
|
} else if (BabelTypes.isIdentifier(firstParam)) {
|
|
2243
2513
|
itemNames.add(firstParam.name);
|
|
2244
2514
|
}
|
|
2245
|
-
(0, import_traverse4.default)(
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2515
|
+
(0, import_traverse4.default)(
|
|
2516
|
+
fn.body,
|
|
2517
|
+
{
|
|
2518
|
+
noScope: true,
|
|
2519
|
+
MemberExpression: (nodePath) => {
|
|
2520
|
+
const node = nodePath.node;
|
|
2521
|
+
if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
|
|
2522
|
+
fields.add(node.property.name);
|
|
2523
|
+
}
|
|
2251
2524
|
}
|
|
2252
2525
|
}
|
|
2253
|
-
|
|
2526
|
+
);
|
|
2254
2527
|
return Array.from(fields).sort();
|
|
2255
2528
|
}
|
|
2256
2529
|
inferItemType(dataSource, renderItem, displayFields) {
|
|
@@ -2262,11 +2535,19 @@ var ScreenAnalyzer = class {
|
|
|
2262
2535
|
if (displayFields.length > 0) return "Item";
|
|
2263
2536
|
return void 0;
|
|
2264
2537
|
}
|
|
2538
|
+
/**
|
|
2539
|
+
* Identity comes from how a field is NAMED, not from what the app
|
|
2540
|
+
* sells: `id`/`uuid`/`key`/`slug` are conventions any codebase uses,
|
|
2541
|
+
* `name`/`title`/`email` are how any row introduces itself. `plate`
|
|
2542
|
+
* used to sit in this list and read like one of them — but it is the
|
|
2543
|
+
* example app's schema, and no other tenant ever got its equivalent
|
|
2544
|
+
* (`mrn`, `trackingNumber`, `sku`) added here.
|
|
2545
|
+
*/
|
|
2265
2546
|
inferIdentityFields(keyField, displayFields) {
|
|
2266
2547
|
const out = /* @__PURE__ */ new Set();
|
|
2267
2548
|
if (keyField) out.add(keyField);
|
|
2268
2549
|
for (const field of displayFields) {
|
|
2269
|
-
if (/^(id|uuid|key|name|title|
|
|
2550
|
+
if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
|
|
2270
2551
|
}
|
|
2271
2552
|
return Array.from(out);
|
|
2272
2553
|
}
|
|
@@ -2277,17 +2558,23 @@ var ScreenAnalyzer = class {
|
|
|
2277
2558
|
CallExpression: (nodePath) => {
|
|
2278
2559
|
const node = nodePath.node;
|
|
2279
2560
|
if (!BabelTypes.isMemberExpression(node.callee)) return;
|
|
2280
|
-
if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
|
|
2281
|
-
|
|
2561
|
+
if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
|
|
2562
|
+
return;
|
|
2563
|
+
if (!BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource)
|
|
2564
|
+
return;
|
|
2282
2565
|
const fn = node.arguments[0];
|
|
2283
|
-
if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2566
|
+
if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
|
|
2567
|
+
return;
|
|
2568
|
+
(0, import_traverse4.default)(
|
|
2569
|
+
fn.body,
|
|
2570
|
+
{
|
|
2571
|
+
noScope: true,
|
|
2572
|
+
Identifier: (innerPath) => {
|
|
2573
|
+
const name = innerPath.node.name;
|
|
2574
|
+
if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
|
|
2575
|
+
}
|
|
2289
2576
|
}
|
|
2290
|
-
|
|
2577
|
+
);
|
|
2291
2578
|
}
|
|
2292
2579
|
});
|
|
2293
2580
|
return queryBinding;
|
|
@@ -2378,7 +2665,7 @@ var NavigationAnalyzer = class {
|
|
|
2378
2665
|
cwd: this.config.rootDir,
|
|
2379
2666
|
ignore: excludePatterns
|
|
2380
2667
|
});
|
|
2381
|
-
return files.map((
|
|
2668
|
+
return files.map((file2) => import_path4.default.join(this.config.rootDir, file2));
|
|
2382
2669
|
}
|
|
2383
2670
|
/** Parse navigator definitions from a file */
|
|
2384
2671
|
parseNavigators(content, filePath) {
|
|
@@ -2439,11 +2726,7 @@ var NavigationAnalyzer = class {
|
|
|
2439
2726
|
if (t5.isJSXAttribute(initialRouteAttr) && t5.isStringLiteral(initialRouteAttr.value)) {
|
|
2440
2727
|
navigator.initialRouteName = initialRouteAttr.value.value;
|
|
2441
2728
|
}
|
|
2442
|
-
const screens = this.extractScreensFromNavigator(
|
|
2443
|
-
node,
|
|
2444
|
-
objectName,
|
|
2445
|
-
navigator.type
|
|
2446
|
-
);
|
|
2729
|
+
const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
|
|
2447
2730
|
screensByNavigator.set(objectName, screens);
|
|
2448
2731
|
}
|
|
2449
2732
|
}
|
|
@@ -2460,27 +2743,112 @@ var NavigationAnalyzer = class {
|
|
|
2460
2743
|
}
|
|
2461
2744
|
return navigators;
|
|
2462
2745
|
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Is this JSX element `<navigatorVarName.MEMBER …>`?
|
|
2748
|
+
*/
|
|
2749
|
+
// Deliberately NOT a type predicate (`node is t.JSXElement`): a false
|
|
2750
|
+
// result would then narrow `node` to "not a JSXElement", and the very
|
|
2751
|
+
// next check — the same node against a different member — would see
|
|
2752
|
+
// `never`. It is a question about the member name, not about the node
|
|
2753
|
+
// kind.
|
|
2754
|
+
isNavigatorMember(node, navigatorVarName, member) {
|
|
2755
|
+
if (!t5.isJSXElement(node)) return false;
|
|
2756
|
+
const name = node.openingElement.name;
|
|
2757
|
+
return t5.isJSXMemberExpression(name) && t5.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && t5.isJSXIdentifier(name.property) && name.property.name === member;
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* Flatten a navigator's JSX children into the `<X.Screen>` elements they
|
|
2761
|
+
* contain, unwrapping every container a real app puts in between.
|
|
2762
|
+
*
|
|
2763
|
+
* The old version compared `t.isJSXElement(child)` against the direct
|
|
2764
|
+
* children only. A ternary is a `JSXExpressionContainer`, so an
|
|
2765
|
+
* auth-gated root — the modal shape of a commercial app, and the shape
|
|
2766
|
+
* of this repo's own `apps/example-app` — contributed ZERO screens to
|
|
2767
|
+
* the graph while `appilots sync` reported success (#396).
|
|
2768
|
+
*
|
|
2769
|
+
* Both branches of a conditional are collected on purpose. The graph is
|
|
2770
|
+
* a design-time map of what routes EXIST, not a prediction of which one
|
|
2771
|
+
* a given session will render; the agent needs the destination name to
|
|
2772
|
+
* be there whether or not the user happens to be logged in right now.
|
|
2773
|
+
*/
|
|
2774
|
+
collectScreenElements(children, navigatorVarName, depth = 0) {
|
|
2775
|
+
if (depth > 12) return [];
|
|
2776
|
+
const found = [];
|
|
2777
|
+
const visit = (node) => {
|
|
2778
|
+
if (!node) return;
|
|
2779
|
+
if (this.isNavigatorMember(node, navigatorVarName, "Screen")) {
|
|
2780
|
+
found.push(node);
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
if (this.isNavigatorMember(node, navigatorVarName, "Group")) {
|
|
2784
|
+
const group = node;
|
|
2785
|
+
found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
if (t5.isJSXElement(node)) {
|
|
2789
|
+
const name = node.openingElement.name;
|
|
2790
|
+
const isForeignNavigator = t5.isJSXMemberExpression(name) && t5.isJSXIdentifier(name.property) && name.property.name === "Navigator";
|
|
2791
|
+
if (isForeignNavigator) return;
|
|
2792
|
+
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
if (t5.isJSXFragment(node)) {
|
|
2796
|
+
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
if (t5.isJSXExpressionContainer(node)) {
|
|
2800
|
+
visit(node.expression);
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
if (t5.isConditionalExpression(node)) {
|
|
2804
|
+
visit(node.consequent);
|
|
2805
|
+
visit(node.alternate);
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
if (t5.isLogicalExpression(node)) {
|
|
2809
|
+
visit(node.left);
|
|
2810
|
+
visit(node.right);
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
if (t5.isCallExpression(node)) {
|
|
2814
|
+
for (const arg of node.arguments) {
|
|
2815
|
+
if (t5.isArrowFunctionExpression(arg) || t5.isFunctionExpression(arg)) {
|
|
2816
|
+
if (t5.isBlockStatement(arg.body)) {
|
|
2817
|
+
for (const stmt of arg.body.body) {
|
|
2818
|
+
if (t5.isReturnStatement(stmt)) visit(stmt.argument);
|
|
2819
|
+
}
|
|
2820
|
+
} else {
|
|
2821
|
+
visit(arg.body);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
if (t5.isArrayExpression(node)) {
|
|
2828
|
+
for (const el of node.elements) visit(el);
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
if (t5.isTSAsExpression(node) || t5.isTSNonNullExpression(node)) {
|
|
2832
|
+
visit(node.expression);
|
|
2833
|
+
}
|
|
2834
|
+
};
|
|
2835
|
+
for (const child of children) visit(child);
|
|
2836
|
+
return found;
|
|
2837
|
+
}
|
|
2463
2838
|
/** Extract screens from a navigator JSX element */
|
|
2464
2839
|
extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
|
|
2465
2840
|
const screens = [];
|
|
2466
2841
|
if (!navigatorElement.children) return screens;
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
name: screenName,
|
|
2478
|
-
navigatorName: navigatorVarName,
|
|
2479
|
-
navigatorType
|
|
2480
|
-
});
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
}
|
|
2842
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2843
|
+
for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
|
|
2844
|
+
const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
|
|
2845
|
+
if (!screenName || seen.has(screenName)) continue;
|
|
2846
|
+
seen.add(screenName);
|
|
2847
|
+
screens.push({
|
|
2848
|
+
name: screenName,
|
|
2849
|
+
navigatorName: navigatorVarName,
|
|
2850
|
+
navigatorType
|
|
2851
|
+
});
|
|
2484
2852
|
}
|
|
2485
2853
|
return screens;
|
|
2486
2854
|
}
|
|
@@ -2539,9 +2907,7 @@ var NavigationAnalyzer = class {
|
|
|
2539
2907
|
if (member.type === "TSPropertySignature" && member.key) {
|
|
2540
2908
|
const keyName = member.key.type === "Identifier" ? member.key.name : null;
|
|
2541
2909
|
if (keyName && member.typeAnnotation) {
|
|
2542
|
-
const params = this.extractParamsFromType(
|
|
2543
|
-
member.typeAnnotation.typeAnnotation
|
|
2544
|
-
);
|
|
2910
|
+
const params = this.extractParamsFromType(member.typeAnnotation.typeAnnotation);
|
|
2545
2911
|
entries.set(keyName, params);
|
|
2546
2912
|
}
|
|
2547
2913
|
}
|
|
@@ -2581,7 +2947,7 @@ var NavigationAnalyzer = class {
|
|
|
2581
2947
|
if (type.type === "TSUndefinedKeyword") return "undefined";
|
|
2582
2948
|
if (type.type === "TSNullKeyword") return "null";
|
|
2583
2949
|
if (type.type === "TSUnionType") {
|
|
2584
|
-
return type.types.map((
|
|
2950
|
+
return type.types.map((t15) => this.typeToString(t15)).join(" | ");
|
|
2585
2951
|
}
|
|
2586
2952
|
if (type.type === "TSTypeLiteral") {
|
|
2587
2953
|
return "object";
|
|
@@ -2602,7 +2968,7 @@ var NavigationAnalyzer = class {
|
|
|
2602
2968
|
/** Attach parsed type params to navigator screens */
|
|
2603
2969
|
attachParamsToNavigators(navigators, types) {
|
|
2604
2970
|
for (const navigator of navigators) {
|
|
2605
|
-
const matchingType = types.find((
|
|
2971
|
+
const matchingType = types.find((t15) => t15.type === navigator.type);
|
|
2606
2972
|
if (matchingType) {
|
|
2607
2973
|
for (const screen of navigator.screens) {
|
|
2608
2974
|
const screenParams = matchingType.paramEntries.get(screen.name);
|
|
@@ -2620,6 +2986,11 @@ var NavigationAnalyzer = class {
|
|
|
2620
2986
|
let initialScreen = "";
|
|
2621
2987
|
for (const navigator of parsedNavigators) {
|
|
2622
2988
|
const screenNames = navigator.screens.map((s) => s.name);
|
|
2989
|
+
if (screenNames.length === 0) {
|
|
2990
|
+
console.warn(
|
|
2991
|
+
`[MCPGenerator] navigator "${navigator.name}" (${navigator.type}) declared no reachable screens. Its routes will be missing from the app map, so the agent will have no destination name for them. If the screens are there in the source, this is a parser gap \u2014 please report it with the file.`
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
2623
2994
|
navigators.push({
|
|
2624
2995
|
name: navigator.name,
|
|
2625
2996
|
type: navigator.type,
|
|
@@ -2693,8 +3064,8 @@ var ComponentAnalyzer = class {
|
|
|
2693
3064
|
});
|
|
2694
3065
|
const components = [];
|
|
2695
3066
|
(0, import_traverse6.default)(ast, {
|
|
2696
|
-
JSXElement: (
|
|
2697
|
-
const component = this.extractComponentFromJSXElement(
|
|
3067
|
+
JSXElement: (path11) => {
|
|
3068
|
+
const component = this.extractComponentFromJSXElement(path11.node);
|
|
2698
3069
|
if (component) {
|
|
2699
3070
|
components.push(component);
|
|
2700
3071
|
}
|
|
@@ -2828,13 +3199,13 @@ var FormAnalyzer = class {
|
|
|
2828
3199
|
this.inputElements = [];
|
|
2829
3200
|
this.submitButtons = [];
|
|
2830
3201
|
(0, import_traverse7.default)(ast, {
|
|
2831
|
-
CallExpression: (
|
|
2832
|
-
this.extractStateVariables(
|
|
3202
|
+
CallExpression: (path11) => {
|
|
3203
|
+
this.extractStateVariables(path11.node);
|
|
2833
3204
|
}
|
|
2834
3205
|
});
|
|
2835
3206
|
(0, import_traverse7.default)(ast, {
|
|
2836
|
-
JSXElement: (
|
|
2837
|
-
this.extractFormElements(
|
|
3207
|
+
JSXElement: (path11) => {
|
|
3208
|
+
this.extractFormElements(path11.node);
|
|
2838
3209
|
}
|
|
2839
3210
|
});
|
|
2840
3211
|
const validationRules = this.extractValidationRules(ast);
|
|
@@ -2934,8 +3305,8 @@ var FormAnalyzer = class {
|
|
|
2934
3305
|
extractValidationRules(ast) {
|
|
2935
3306
|
const rules = {};
|
|
2936
3307
|
(0, import_traverse7.default)(ast, {
|
|
2937
|
-
IfStatement: (
|
|
2938
|
-
const test =
|
|
3308
|
+
IfStatement: (path11) => {
|
|
3309
|
+
const test = path11.node.test;
|
|
2939
3310
|
const rule = this.extractRuleFromCondition(test);
|
|
2940
3311
|
if (rule) {
|
|
2941
3312
|
const { field, description } = rule;
|
|
@@ -3068,8 +3439,8 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3068
3439
|
console.log(
|
|
3069
3440
|
`[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
|
|
3070
3441
|
);
|
|
3071
|
-
const enrichmentPromises = screenFiles.map(async (
|
|
3072
|
-
const filePath = import_node_path.default.resolve(config.rootDir,
|
|
3442
|
+
const enrichmentPromises = screenFiles.map(async (file2) => {
|
|
3443
|
+
const filePath = import_node_path.default.resolve(config.rootDir, file2);
|
|
3073
3444
|
try {
|
|
3074
3445
|
const [components, forms] = await Promise.all([
|
|
3075
3446
|
componentAnalyzer.analyzeFile(filePath),
|
|
@@ -3077,7 +3448,7 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3077
3448
|
]);
|
|
3078
3449
|
return { filePath, components, forms };
|
|
3079
3450
|
} catch (error2) {
|
|
3080
|
-
console.warn(`[ReactNativePlatformAnalyzer] Failed to analyze ${
|
|
3451
|
+
console.warn(`[ReactNativePlatformAnalyzer] Failed to analyze ${file2}:`, error2);
|
|
3081
3452
|
return { filePath, components: [], forms: [] };
|
|
3082
3453
|
}
|
|
3083
3454
|
});
|
|
@@ -3345,13 +3716,13 @@ function staticRoutePath(node) {
|
|
|
3345
3716
|
if (!node) return void 0;
|
|
3346
3717
|
if (t9.isStringLiteral(node)) return node.value;
|
|
3347
3718
|
if (t9.isTemplateLiteral(node)) {
|
|
3348
|
-
let
|
|
3719
|
+
let path11 = "";
|
|
3349
3720
|
node.quasis.forEach((quasi, index) => {
|
|
3350
|
-
|
|
3721
|
+
path11 += quasi.value.cooked ?? quasi.value.raw;
|
|
3351
3722
|
const expr = node.expressions[index];
|
|
3352
|
-
if (expr)
|
|
3723
|
+
if (expr) path11 += `:${paramNameOf(expr)}`;
|
|
3353
3724
|
});
|
|
3354
|
-
return
|
|
3725
|
+
return path11;
|
|
3355
3726
|
}
|
|
3356
3727
|
return void 0;
|
|
3357
3728
|
}
|
|
@@ -3436,27 +3807,29 @@ var WebScreenAnalyzer = class {
|
|
|
3436
3807
|
include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
3437
3808
|
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
3438
3809
|
} = this.config;
|
|
3439
|
-
const files = await (0, import_fast_glob4.default)(include, { cwd: this.config.rootDir, ignore: exclude })
|
|
3810
|
+
const files = (await (0, import_fast_glob4.default)(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
|
|
3811
|
+
(file2) => !file2.endsWith(".d.ts")
|
|
3812
|
+
);
|
|
3440
3813
|
const patternMatches = await (0, import_fast_glob4.default)(this.screenPatterns, {
|
|
3441
3814
|
cwd: this.config.rootDir,
|
|
3442
3815
|
ignore: exclude
|
|
3443
3816
|
});
|
|
3444
3817
|
const patternSet = new Set(patternMatches.map((f) => import_path5.default.resolve(this.config.rootDir, f)));
|
|
3445
3818
|
const candidates = [];
|
|
3446
|
-
for (const
|
|
3447
|
-
const filePath = import_path5.default.resolve(this.config.rootDir,
|
|
3819
|
+
for (const file2 of files) {
|
|
3820
|
+
const filePath = import_path5.default.resolve(this.config.rootDir, file2);
|
|
3448
3821
|
try {
|
|
3449
3822
|
const candidate = await this.analyzeFile(filePath);
|
|
3450
3823
|
if (!candidate) continue;
|
|
3451
3824
|
candidate.matchesScreenPattern = patternSet.has(filePath);
|
|
3452
3825
|
candidates.push(candidate);
|
|
3453
3826
|
if (this.verbose) {
|
|
3454
|
-
console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${
|
|
3827
|
+
console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file2})`);
|
|
3455
3828
|
}
|
|
3456
3829
|
} catch (error2) {
|
|
3457
3830
|
if (this.verbose) {
|
|
3458
3831
|
console.warn(
|
|
3459
|
-
`[WebScreenAnalyzer] Failed to parse ${
|
|
3832
|
+
`[WebScreenAnalyzer] Failed to parse ${file2}:`,
|
|
3460
3833
|
error2 instanceof Error ? error2.message : error2
|
|
3461
3834
|
);
|
|
3462
3835
|
}
|
|
@@ -3807,7 +4180,8 @@ var WebScreenAnalyzer = class {
|
|
|
3807
4180
|
if (type === "email") return "email";
|
|
3808
4181
|
if (type === "tel") return "phone";
|
|
3809
4182
|
if (type === "number") return "number";
|
|
3810
|
-
if (type === "date" || type === "datetime-local" || type === "month" || type === "week")
|
|
4183
|
+
if (type === "date" || type === "datetime-local" || type === "month" || type === "week")
|
|
4184
|
+
return "date";
|
|
3811
4185
|
const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
|
|
3812
4186
|
if (inputMode === "email") return "email";
|
|
3813
4187
|
if (inputMode === "tel") return "phone";
|
|
@@ -4121,7 +4495,9 @@ var WebScreenAnalyzer = class {
|
|
|
4121
4495
|
}
|
|
4122
4496
|
const callback = expr.arguments[0];
|
|
4123
4497
|
if (!t10.isArrowFunctionExpression(callback) && !t10.isFunctionExpression(callback)) return;
|
|
4124
|
-
const enclosing = nodePath.findParent(
|
|
4498
|
+
const enclosing = nodePath.findParent(
|
|
4499
|
+
(p) => p.isJSXElement()
|
|
4500
|
+
);
|
|
4125
4501
|
const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
|
|
4126
4502
|
const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
|
|
4127
4503
|
const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
|
|
@@ -4223,16 +4599,19 @@ function firstCalledFunctionName(node) {
|
|
|
4223
4599
|
}
|
|
4224
4600
|
function containsWindowConfirm(body) {
|
|
4225
4601
|
let found = false;
|
|
4226
|
-
(0, import_traverse9.default)(
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
found = true;
|
|
4602
|
+
(0, import_traverse9.default)(
|
|
4603
|
+
body,
|
|
4604
|
+
{
|
|
4605
|
+
noScope: true,
|
|
4606
|
+
CallExpression: (nodePath) => {
|
|
4607
|
+
const callee = nodePath.node.callee;
|
|
4608
|
+
if (t10.isIdentifier(callee) && callee.name === "confirm") found = true;
|
|
4609
|
+
if (t10.isMemberExpression(callee) && t10.isIdentifier(callee.object) && callee.object.name === "window" && t10.isIdentifier(callee.property) && callee.property.name === "confirm") {
|
|
4610
|
+
found = true;
|
|
4611
|
+
}
|
|
4233
4612
|
}
|
|
4234
4613
|
}
|
|
4235
|
-
|
|
4614
|
+
);
|
|
4236
4615
|
return found;
|
|
4237
4616
|
}
|
|
4238
4617
|
function mergeLocator(current, next) {
|
|
@@ -4272,32 +4651,38 @@ function collectionItemNames(callback) {
|
|
|
4272
4651
|
function collectionDisplayFields(callback, itemNames) {
|
|
4273
4652
|
const fields = /* @__PURE__ */ new Set();
|
|
4274
4653
|
if (!callback.body) return [];
|
|
4275
|
-
(0, import_traverse9.default)(
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4654
|
+
(0, import_traverse9.default)(
|
|
4655
|
+
callback.body,
|
|
4656
|
+
{
|
|
4657
|
+
noScope: true,
|
|
4658
|
+
MemberExpression: (nodePath) => {
|
|
4659
|
+
const node = nodePath.node;
|
|
4660
|
+
if (t10.isIdentifier(node.object) && itemNames.has(node.object.name) && t10.isIdentifier(node.property)) {
|
|
4661
|
+
fields.add(node.property.name);
|
|
4662
|
+
}
|
|
4281
4663
|
}
|
|
4282
4664
|
}
|
|
4283
|
-
|
|
4665
|
+
);
|
|
4284
4666
|
return Array.from(fields).sort();
|
|
4285
4667
|
}
|
|
4286
4668
|
function collectionKeyField(callback, itemNames) {
|
|
4287
4669
|
let keyField;
|
|
4288
4670
|
if (!callback.body) return void 0;
|
|
4289
|
-
(0, import_traverse9.default)(
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4671
|
+
(0, import_traverse9.default)(
|
|
4672
|
+
callback.body,
|
|
4673
|
+
{
|
|
4674
|
+
noScope: true,
|
|
4675
|
+
JSXAttribute: (nodePath) => {
|
|
4676
|
+
const attr = nodePath.node;
|
|
4677
|
+
if (!t10.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
|
|
4678
|
+
if (!attr.value || !t10.isJSXExpressionContainer(attr.value)) return;
|
|
4679
|
+
const expr = attr.value.expression;
|
|
4680
|
+
if (t10.isMemberExpression(expr) && t10.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t10.isIdentifier(expr.property)) {
|
|
4681
|
+
keyField = keyField ?? expr.property.name;
|
|
4682
|
+
}
|
|
4298
4683
|
}
|
|
4299
4684
|
}
|
|
4300
|
-
|
|
4685
|
+
);
|
|
4301
4686
|
return keyField;
|
|
4302
4687
|
}
|
|
4303
4688
|
function collectionRowAction(callback) {
|
|
@@ -4345,7 +4730,7 @@ function inferIdentityFields(keyField, displayFields) {
|
|
|
4345
4730
|
const out = /* @__PURE__ */ new Set();
|
|
4346
4731
|
if (keyField) out.add(keyField);
|
|
4347
4732
|
for (const field of displayFields) {
|
|
4348
|
-
if (/^(id|uuid|key|name|title|
|
|
4733
|
+
if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
|
|
4349
4734
|
}
|
|
4350
4735
|
return Array.from(out);
|
|
4351
4736
|
}
|
|
@@ -4403,7 +4788,7 @@ var WebNavigationAnalyzer = class {
|
|
|
4403
4788
|
...this.navigationExclude
|
|
4404
4789
|
];
|
|
4405
4790
|
const files = await (0, import_fast_glob5.default)(patterns, { cwd: this.config.rootDir, ignore });
|
|
4406
|
-
return files.map((
|
|
4791
|
+
return files.map((file2) => import_path6.default.join(this.config.rootDir, file2));
|
|
4407
4792
|
}
|
|
4408
4793
|
// ── JSX <Route> style ────────────────────────────────────────────
|
|
4409
4794
|
extractJsxRoutes(ast) {
|
|
@@ -4905,8 +5290,8 @@ var ZodParsedType = util.arrayToEnum([
|
|
|
4905
5290
|
"set"
|
|
4906
5291
|
]);
|
|
4907
5292
|
var getParsedType = (data) => {
|
|
4908
|
-
const
|
|
4909
|
-
switch (
|
|
5293
|
+
const t15 = typeof data;
|
|
5294
|
+
switch (t15) {
|
|
4910
5295
|
case "undefined":
|
|
4911
5296
|
return ZodParsedType.undefined;
|
|
4912
5297
|
case "string":
|
|
@@ -5178,8 +5563,8 @@ function getErrorMap() {
|
|
|
5178
5563
|
|
|
5179
5564
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
5180
5565
|
var makeIssue = (params) => {
|
|
5181
|
-
const { data, path:
|
|
5182
|
-
const fullPath = [...
|
|
5566
|
+
const { data, path: path11, errorMaps, issueData } = params;
|
|
5567
|
+
const fullPath = [...path11, ...issueData.path || []];
|
|
5183
5568
|
const fullIssue = {
|
|
5184
5569
|
...issueData,
|
|
5185
5570
|
path: fullPath
|
|
@@ -5295,11 +5680,11 @@ var errorUtil;
|
|
|
5295
5680
|
|
|
5296
5681
|
// ../../node_modules/zod/v3/types.js
|
|
5297
5682
|
var ParseInputLazyPath = class {
|
|
5298
|
-
constructor(parent, value,
|
|
5683
|
+
constructor(parent, value, path11, key) {
|
|
5299
5684
|
this._cachedPath = [];
|
|
5300
5685
|
this.parent = parent;
|
|
5301
5686
|
this.data = value;
|
|
5302
|
-
this._path =
|
|
5687
|
+
this._path = path11;
|
|
5303
5688
|
this._key = key;
|
|
5304
5689
|
}
|
|
5305
5690
|
get path() {
|
|
@@ -8741,7 +9126,7 @@ var coerce = {
|
|
|
8741
9126
|
};
|
|
8742
9127
|
var NEVER = INVALID;
|
|
8743
9128
|
|
|
8744
|
-
// ../shared/dist/chunk-
|
|
9129
|
+
// ../shared/dist/chunk-PKS3VDNB.mjs
|
|
8745
9130
|
var locatorSourceSchema = external_exports.enum([
|
|
8746
9131
|
"appilotsId",
|
|
8747
9132
|
"testID",
|
|
@@ -8767,7 +9152,16 @@ var locatorDescriptorSchema = external_exports.object({
|
|
|
8767
9152
|
source: locatorSourceSchema.optional()
|
|
8768
9153
|
}).passthrough();
|
|
8769
9154
|
var signalDescriptorSchema = external_exports.object({
|
|
8770
|
-
type: external_exports.enum([
|
|
9155
|
+
type: external_exports.enum([
|
|
9156
|
+
"navigation",
|
|
9157
|
+
"goBack",
|
|
9158
|
+
"toast",
|
|
9159
|
+
"modal",
|
|
9160
|
+
"inline-error",
|
|
9161
|
+
"data-arrival",
|
|
9162
|
+
"loading",
|
|
9163
|
+
"none"
|
|
9164
|
+
]).or(external_exports.string().min(1).max(60)),
|
|
8771
9165
|
target: external_exports.string().optional(),
|
|
8772
9166
|
description: external_exports.string().optional()
|
|
8773
9167
|
}).passthrough();
|
|
@@ -8781,7 +9175,20 @@ var waitPolicyDescriptorSchema = external_exports.object({
|
|
|
8781
9175
|
signals: external_exports.array(signalDescriptorSchema).optional(),
|
|
8782
9176
|
maxMs: external_exports.number().finite().optional()
|
|
8783
9177
|
}).passthrough();
|
|
8784
|
-
var targetDescriptorRoleSchema = external_exports.enum([
|
|
9178
|
+
var targetDescriptorRoleSchema = external_exports.enum([
|
|
9179
|
+
"button",
|
|
9180
|
+
"submit",
|
|
9181
|
+
"input",
|
|
9182
|
+
"toggle",
|
|
9183
|
+
"select",
|
|
9184
|
+
"date",
|
|
9185
|
+
"list",
|
|
9186
|
+
"row",
|
|
9187
|
+
"menuItem",
|
|
9188
|
+
"modal",
|
|
9189
|
+
"custom"
|
|
9190
|
+
]).or(external_exports.string().min(1).max(60));
|
|
9191
|
+
var targetOriginSchema = external_exports.enum(["analyzer", "manifest"]).or(external_exports.string().min(1).max(40));
|
|
8785
9192
|
var targetDescriptorSchema = external_exports.object({
|
|
8786
9193
|
id: external_exports.string(),
|
|
8787
9194
|
role: targetDescriptorRoleSchema,
|
|
@@ -8795,10 +9202,20 @@ var targetDescriptorSchema = external_exports.object({
|
|
|
8795
9202
|
requiresConfirmation: external_exports.boolean().optional(),
|
|
8796
9203
|
opensModal: external_exports.string().optional(),
|
|
8797
9204
|
opensBottomSheet: external_exports.string().optional(),
|
|
8798
|
-
sourceComponent: external_exports.string().optional()
|
|
9205
|
+
sourceComponent: external_exports.string().optional(),
|
|
9206
|
+
origin: targetOriginSchema.optional()
|
|
8799
9207
|
}).passthrough();
|
|
8800
9208
|
var flowStepDescriptorSchema = external_exports.object({
|
|
8801
|
-
type: external_exports.enum([
|
|
9209
|
+
type: external_exports.enum([
|
|
9210
|
+
"navigate",
|
|
9211
|
+
"fill",
|
|
9212
|
+
"press",
|
|
9213
|
+
"select",
|
|
9214
|
+
"toggle",
|
|
9215
|
+
"wait",
|
|
9216
|
+
"confirm",
|
|
9217
|
+
"choose-list-item"
|
|
9218
|
+
]).or(external_exports.string().min(1).max(60)),
|
|
8802
9219
|
target: external_exports.string().optional(),
|
|
8803
9220
|
label: external_exports.string().optional(),
|
|
8804
9221
|
description: external_exports.string().optional(),
|
|
@@ -9020,6 +9437,15 @@ var mfaRegenerateRecoveryCodesSchema = external_exports.object({
|
|
|
9020
9437
|
password: external_exports.string().min(1, "Password is required"),
|
|
9021
9438
|
code: totpCodeSchema
|
|
9022
9439
|
});
|
|
9440
|
+
var deleteAccountSchema = external_exports.object({
|
|
9441
|
+
password: external_exports.string().min(1, "Password is required"),
|
|
9442
|
+
code: totpCodeSchema.optional(),
|
|
9443
|
+
recoveryCode: recoveryCodeSchema.optional(),
|
|
9444
|
+
confirm: external_exports.literal("DELETE MY ACCOUNT")
|
|
9445
|
+
}).refine((v) => !(v.code && v.recoveryCode), {
|
|
9446
|
+
message: "Provide either a TOTP code or a recovery code, not both",
|
|
9447
|
+
path: ["code"]
|
|
9448
|
+
});
|
|
9023
9449
|
var createProjectSchema = external_exports.object({
|
|
9024
9450
|
name: external_exports.string().min(1).max(100),
|
|
9025
9451
|
description: external_exports.string().max(500).optional(),
|
|
@@ -9055,6 +9481,7 @@ var uploadMCPDocumentSchema = external_exports.object({
|
|
|
9055
9481
|
content: external_exports.record(external_exports.unknown())
|
|
9056
9482
|
});
|
|
9057
9483
|
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
|
|
9484
|
+
var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
|
|
9058
9485
|
var createApiKeySchema = external_exports.object({
|
|
9059
9486
|
name: external_exports.string().min(1).max(100),
|
|
9060
9487
|
// Required for `sdk` (default), forbidden/ignored for `operator` —
|
|
@@ -9062,6 +9489,7 @@ var createApiKeySchema = external_exports.object({
|
|
|
9062
9489
|
// missing scope still validates as the pre-existing SDK shape.
|
|
9063
9490
|
projectId: external_exports.string().min(1).optional(),
|
|
9064
9491
|
scope: apiKeyScopeSchema.default("sdk"),
|
|
9492
|
+
environment: apiKeyEnvironmentSchema.optional(),
|
|
9065
9493
|
expiresAt: external_exports.string().datetime().optional()
|
|
9066
9494
|
}).refine((v) => v.scope !== "sdk" || !!v.projectId, {
|
|
9067
9495
|
message: 'projectId is required for scope "sdk"',
|
|
@@ -9069,13 +9497,23 @@ var createApiKeySchema = external_exports.object({
|
|
|
9069
9497
|
});
|
|
9070
9498
|
var boundedString = (max) => external_exports.string().max(max);
|
|
9071
9499
|
var clientPlatformSchema = external_exports.enum(["react-native", "web", "android", "ios"]).or(external_exports.string().min(1).max(40));
|
|
9500
|
+
var identityProvenanceSchema = external_exports.enum(["declared", "derived", "positional"]).or(external_exports.string().min(1).max(40));
|
|
9072
9501
|
var snapshotInputSchema = external_exports.object({
|
|
9073
9502
|
id: boundedString(160).optional(),
|
|
9503
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9504
|
+
/**
|
|
9505
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
9506
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
9507
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
9508
|
+
* before the field existed), which must never be read as `false`.
|
|
9509
|
+
*/
|
|
9510
|
+
onScreen: external_exports.boolean().optional(),
|
|
9074
9511
|
label: boundedString(300).optional(),
|
|
9075
9512
|
value: boundedString(4096).optional(),
|
|
9076
9513
|
placeholder: boundedString(300).optional(),
|
|
9077
9514
|
editable: external_exports.boolean().optional(),
|
|
9078
9515
|
secure: external_exports.boolean().optional(),
|
|
9516
|
+
submitsOnReturn: external_exports.boolean().optional(),
|
|
9079
9517
|
type: boundedString(40).optional(),
|
|
9080
9518
|
required: external_exports.boolean().optional(),
|
|
9081
9519
|
invalid: external_exports.boolean().optional(),
|
|
@@ -9083,6 +9521,14 @@ var snapshotInputSchema = external_exports.object({
|
|
|
9083
9521
|
}).passthrough();
|
|
9084
9522
|
var snapshotButtonSchema = external_exports.object({
|
|
9085
9523
|
id: boundedString(160).optional(),
|
|
9524
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9525
|
+
/**
|
|
9526
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
9527
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
9528
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
9529
|
+
* before the field existed), which must never be read as `false`.
|
|
9530
|
+
*/
|
|
9531
|
+
onScreen: external_exports.boolean().optional(),
|
|
9086
9532
|
label: boundedString(300).optional(),
|
|
9087
9533
|
disabled: external_exports.boolean().optional(),
|
|
9088
9534
|
inModal: external_exports.boolean().optional(),
|
|
@@ -9092,12 +9538,28 @@ var snapshotButtonSchema = external_exports.object({
|
|
|
9092
9538
|
}).passthrough();
|
|
9093
9539
|
var snapshotToggleSchema = external_exports.object({
|
|
9094
9540
|
id: boundedString(160).optional(),
|
|
9541
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9542
|
+
/**
|
|
9543
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
9544
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
9545
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
9546
|
+
* before the field existed), which must never be read as `false`.
|
|
9547
|
+
*/
|
|
9548
|
+
onScreen: external_exports.boolean().optional(),
|
|
9095
9549
|
label: boundedString(300).optional(),
|
|
9096
9550
|
value: external_exports.boolean().optional(),
|
|
9097
9551
|
inModal: external_exports.boolean().optional()
|
|
9098
9552
|
}).passthrough();
|
|
9099
9553
|
var snapshotSliderSchema = external_exports.object({
|
|
9100
9554
|
id: boundedString(160).optional(),
|
|
9555
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9556
|
+
/**
|
|
9557
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
9558
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
9559
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
9560
|
+
* before the field existed), which must never be read as `false`.
|
|
9561
|
+
*/
|
|
9562
|
+
onScreen: external_exports.boolean().optional(),
|
|
9101
9563
|
label: boundedString(300).optional(),
|
|
9102
9564
|
value: external_exports.number().finite().optional(),
|
|
9103
9565
|
min: external_exports.number().finite().optional(),
|
|
@@ -9138,6 +9600,26 @@ var snapshotListSchema = external_exports.object({
|
|
|
9138
9600
|
// should chase, and the relay only reads a known subset.
|
|
9139
9601
|
items: external_exports.array(external_exports.record(external_exports.unknown())).max(500).optional()
|
|
9140
9602
|
}).passthrough();
|
|
9603
|
+
var snapshotNativeDialogSchema = external_exports.object({
|
|
9604
|
+
id: boundedString(160),
|
|
9605
|
+
title: boundedString(300).optional(),
|
|
9606
|
+
message: boundedString(2e3).optional(),
|
|
9607
|
+
buttons: external_exports.array(
|
|
9608
|
+
external_exports.object({
|
|
9609
|
+
label: boundedString(160),
|
|
9610
|
+
style: external_exports.enum(["default", "cancel", "destructive"]).optional()
|
|
9611
|
+
}).passthrough()
|
|
9612
|
+
).max(10)
|
|
9613
|
+
}).passthrough();
|
|
9614
|
+
var snapshotScrollableSchema = external_exports.object({
|
|
9615
|
+
id: boundedString(160),
|
|
9616
|
+
containerType: boundedString(60).optional(),
|
|
9617
|
+
label: boundedString(300).optional(),
|
|
9618
|
+
scrollOffsetY: external_exports.number().finite().optional(),
|
|
9619
|
+
canScrollUp: external_exports.boolean().optional(),
|
|
9620
|
+
canScrollDown: external_exports.boolean().optional(),
|
|
9621
|
+
horizontal: external_exports.boolean().optional()
|
|
9622
|
+
}).passthrough();
|
|
9141
9623
|
var snapshotChoiceGroupSchema = external_exports.object({
|
|
9142
9624
|
index: external_exports.number().int().optional(),
|
|
9143
9625
|
id: boundedString(160).optional(),
|
|
@@ -9154,6 +9636,14 @@ var snapshotElementSchema = external_exports.object({
|
|
|
9154
9636
|
disabled: external_exports.boolean().optional(),
|
|
9155
9637
|
selected: external_exports.boolean().optional(),
|
|
9156
9638
|
source: boundedString(40).optional(),
|
|
9639
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9640
|
+
/**
|
|
9641
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
9642
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
9643
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
9644
|
+
* before the field existed), which must never be read as `false`.
|
|
9645
|
+
*/
|
|
9646
|
+
onScreen: external_exports.boolean().optional(),
|
|
9157
9647
|
targetId: boundedString(160).optional(),
|
|
9158
9648
|
listContext: external_exports.record(external_exports.unknown()).optional(),
|
|
9159
9649
|
inModal: external_exports.boolean().optional()
|
|
@@ -9170,7 +9660,34 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
9170
9660
|
modalOpen: external_exports.boolean().optional(),
|
|
9171
9661
|
lists: external_exports.array(snapshotListSchema).max(100).optional(),
|
|
9172
9662
|
choiceGroups: external_exports.array(snapshotChoiceGroupSchema).max(100).optional(),
|
|
9173
|
-
|
|
9663
|
+
scrollables: external_exports.array(snapshotScrollableSchema).max(20).optional(),
|
|
9664
|
+
nativeDialog: snapshotNativeDialogSchema.optional(),
|
|
9665
|
+
elements: external_exports.array(snapshotElementSchema).max(1e3).optional(),
|
|
9666
|
+
/**
|
|
9667
|
+
* The client clamped this observation to the caps above and lost
|
|
9668
|
+
* data doing it (`clampSnapshotToWireLimits` in
|
|
9669
|
+
* `@appilots/client-core`). Read by the prompt serializer so the
|
|
9670
|
+
* model is told the view is partial — a screen truncated at 500
|
|
9671
|
+
* texts must not be read as a screen that only has 500 things on
|
|
9672
|
+
* it. Optional: older SDKs never clamp and never send it.
|
|
9673
|
+
*/
|
|
9674
|
+
truncated: external_exports.boolean().optional(),
|
|
9675
|
+
/**
|
|
9676
|
+
* How many of this screen's controls the client could name, split by
|
|
9677
|
+
* `identityProvenance`. Diagnostic only — the relay never grounds an
|
|
9678
|
+
* action on it — but it is the number that says whether an app needs
|
|
9679
|
+
* annotating, and comparing it between a debug and a release build is
|
|
9680
|
+
* the first direct measurement of what minification costs the agent.
|
|
9681
|
+
*/
|
|
9682
|
+
stats: external_exports.object({
|
|
9683
|
+
visitedFibers: external_exports.number().int().nonnegative().optional(),
|
|
9684
|
+
skippedHidden: external_exports.number().int().nonnegative().optional(),
|
|
9685
|
+
identity: external_exports.object({
|
|
9686
|
+
declared: external_exports.number().int().nonnegative().optional(),
|
|
9687
|
+
derived: external_exports.number().int().nonnegative().optional(),
|
|
9688
|
+
positional: external_exports.number().int().nonnegative().optional()
|
|
9689
|
+
}).passthrough().optional()
|
|
9690
|
+
}).passthrough().optional()
|
|
9174
9691
|
}).passthrough();
|
|
9175
9692
|
var agentContextSchema = external_exports.object({
|
|
9176
9693
|
/**
|
|
@@ -9220,6 +9737,19 @@ var agentContextSchema = external_exports.object({
|
|
|
9220
9737
|
).max(200).optional(),
|
|
9221
9738
|
loadingPending: external_exports.boolean().optional()
|
|
9222
9739
|
}).passthrough();
|
|
9740
|
+
var introspectionFailureReasonSchema = external_exports.enum([
|
|
9741
|
+
"sentinel-missing-internals",
|
|
9742
|
+
"never-mounted",
|
|
9743
|
+
"render-crashed",
|
|
9744
|
+
"names-mangled",
|
|
9745
|
+
"walk-recognized-nothing"
|
|
9746
|
+
]);
|
|
9747
|
+
var introspectionDiagnosticsSchema = external_exports.object({
|
|
9748
|
+
captured: external_exports.boolean(),
|
|
9749
|
+
failureReason: introspectionFailureReasonSchema.nullish(),
|
|
9750
|
+
reactVersion: boundedString(32).nullish(),
|
|
9751
|
+
failureCount: external_exports.number().int().min(0).max(1e6).optional()
|
|
9752
|
+
});
|
|
9223
9753
|
var sendMessageSchema = external_exports.object({
|
|
9224
9754
|
content: external_exports.string().min(1).max(4096),
|
|
9225
9755
|
sessionId: external_exports.string().optional(),
|
|
@@ -9229,7 +9759,20 @@ var sendMessageSchema = external_exports.object({
|
|
|
9229
9759
|
* the active MCP doc and emits `mcp_version_mismatch` telemetry when
|
|
9230
9760
|
* they differ. Optional — older SDKs simply skip the check.
|
|
9231
9761
|
*/
|
|
9232
|
-
mcpVersion: external_exports.string().max(64).optional()
|
|
9762
|
+
mcpVersion: external_exports.string().max(64).optional(),
|
|
9763
|
+
/**
|
|
9764
|
+
* SDK-side introspection health. Only sent when it FAILED — a client
|
|
9765
|
+
* that reached the React tree omits the field entirely, so the happy
|
|
9766
|
+
* path costs nothing on the wire.
|
|
9767
|
+
*
|
|
9768
|
+
* Without this, an app whose introspection is dead sends a snapshot
|
|
9769
|
+
* that is empty but perfectly well-formed, indistinguishable from a
|
|
9770
|
+
* genuinely empty screen. The server emits
|
|
9771
|
+
* `introspection_unavailable` so the failure is visible to us instead
|
|
9772
|
+
* of waiting for a bug report. Optional — older SDKs simply never
|
|
9773
|
+
* send it.
|
|
9774
|
+
*/
|
|
9775
|
+
introspection: introspectionDiagnosticsSchema.optional()
|
|
9233
9776
|
});
|
|
9234
9777
|
var agentActionTypeSchema = external_exports.enum([
|
|
9235
9778
|
"navigate",
|
|
@@ -9240,11 +9783,22 @@ var agentActionTypeSchema = external_exports.enum([
|
|
|
9240
9783
|
"custom"
|
|
9241
9784
|
]);
|
|
9242
9785
|
var navigationPayloadSchema = external_exports.object({
|
|
9243
|
-
|
|
9786
|
+
// Optional ONLY so `goBack` can exist (#398): it returns to whatever
|
|
9787
|
+
// is beneath on the stack and has no destination to name. Every other
|
|
9788
|
+
// action is still required to carry one — enforced by the refine
|
|
9789
|
+
// below rather than by the field, so the error names the real problem
|
|
9790
|
+
// ("navigate needs a screenName") instead of a missing key.
|
|
9791
|
+
screenName: external_exports.string().min(1).optional(),
|
|
9244
9792
|
params: external_exports.record(external_exports.unknown()).optional(),
|
|
9245
9793
|
navigationAction: external_exports.enum(["push", "navigate", "replace", "goBack", "reset"]).default("navigate").optional(),
|
|
9246
9794
|
path: external_exports.array(external_exports.string().min(1)).optional()
|
|
9247
|
-
}).passthrough()
|
|
9795
|
+
}).passthrough().refine(
|
|
9796
|
+
(payload) => payload.navigationAction === "goBack" || typeof payload.screenName === "string" && payload.screenName.length > 0,
|
|
9797
|
+
{
|
|
9798
|
+
message: 'screenName is required for every navigationAction except "goBack"',
|
|
9799
|
+
path: ["screenName"]
|
|
9800
|
+
}
|
|
9801
|
+
);
|
|
9248
9802
|
var formFillFieldSchema = external_exports.object({
|
|
9249
9803
|
fieldId: external_exports.string().min(1),
|
|
9250
9804
|
fieldType: external_exports.enum(["text", "select", "toggle", "date", "number", "custom"]).default("text").optional(),
|
|
@@ -9578,6 +10132,8 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9578
10132
|
inputs: external_exports.array(
|
|
9579
10133
|
external_exports.object({
|
|
9580
10134
|
id: external_exports.string().max(120),
|
|
10135
|
+
provenance: identityProvenanceSchema.optional(),
|
|
10136
|
+
onScreen: external_exports.boolean().optional(),
|
|
9581
10137
|
label: external_exports.string().max(200).optional(),
|
|
9582
10138
|
value: external_exports.string().max(2e3).optional(),
|
|
9583
10139
|
disabled: external_exports.boolean().optional(),
|
|
@@ -9591,6 +10147,8 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9591
10147
|
buttons: external_exports.array(
|
|
9592
10148
|
external_exports.object({
|
|
9593
10149
|
id: external_exports.string().max(120),
|
|
10150
|
+
provenance: identityProvenanceSchema.optional(),
|
|
10151
|
+
onScreen: external_exports.boolean().optional(),
|
|
9594
10152
|
label: external_exports.string().max(200).optional(),
|
|
9595
10153
|
disabled: external_exports.boolean().optional(),
|
|
9596
10154
|
/** Risk metadata (e.g. "high") the SDK forwards for danger UI. */
|
|
@@ -9612,6 +10170,8 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9612
10170
|
sliders: external_exports.array(
|
|
9613
10171
|
external_exports.object({
|
|
9614
10172
|
id: external_exports.string().max(120),
|
|
10173
|
+
provenance: identityProvenanceSchema.optional(),
|
|
10174
|
+
onScreen: external_exports.boolean().optional(),
|
|
9615
10175
|
label: external_exports.string().max(200).optional(),
|
|
9616
10176
|
value: external_exports.number().finite().optional(),
|
|
9617
10177
|
min: external_exports.number().finite().optional(),
|
|
@@ -9641,6 +10201,44 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9641
10201
|
).max(200).optional()
|
|
9642
10202
|
}).passthrough()
|
|
9643
10203
|
).max(20).optional(),
|
|
10204
|
+
/**
|
|
10205
|
+
* Toggles exactly as the SDK snapshot reports them (#390).
|
|
10206
|
+
*
|
|
10207
|
+
* Their absence is why `generality-component-control-smarthome`
|
|
10208
|
+
* came back `errored` with a 422 instead of measuring anything:
|
|
10209
|
+
* this schema is `.strict()`, so a scenario that declares a switch
|
|
10210
|
+
* is rejected before the agent is ever asked. Which means the
|
|
10211
|
+
* corpus could not express the single control behind failure 02 of
|
|
10212
|
+
* the functional audit — "disable an agent; the switch never
|
|
10213
|
+
* moves". The one defect the eval most needed to catch was the one
|
|
10214
|
+
* shape it could not describe.
|
|
10215
|
+
*/
|
|
10216
|
+
toggles: external_exports.array(
|
|
10217
|
+
external_exports.object({
|
|
10218
|
+
id: external_exports.string().max(120),
|
|
10219
|
+
provenance: identityProvenanceSchema.optional(),
|
|
10220
|
+
onScreen: external_exports.boolean().optional(),
|
|
10221
|
+
label: external_exports.string().max(200).optional(),
|
|
10222
|
+
value: external_exports.boolean().optional(),
|
|
10223
|
+
disabled: external_exports.boolean().optional()
|
|
10224
|
+
})
|
|
10225
|
+
).max(100).optional(),
|
|
10226
|
+
/**
|
|
10227
|
+
* The interaction graph — the `el:*` ids the prompt tells the model
|
|
10228
|
+
* to prefer over ordinal handles. Passthrough per entry for the
|
|
10229
|
+
* same reason `lists` is: the SDK adds fields faster than this
|
|
10230
|
+
* schema should chase them.
|
|
10231
|
+
*/
|
|
10232
|
+
elements: external_exports.array(
|
|
10233
|
+
external_exports.object({
|
|
10234
|
+
id: external_exports.string().max(160),
|
|
10235
|
+
role: external_exports.string().max(40).optional(),
|
|
10236
|
+
label: external_exports.string().max(300).optional()
|
|
10237
|
+
}).passthrough()
|
|
10238
|
+
).max(500).optional(),
|
|
10239
|
+
/** Screen-level state the relay renders and branches on. */
|
|
10240
|
+
loading: external_exports.boolean().optional(),
|
|
10241
|
+
modalOpen: external_exports.boolean().optional(),
|
|
9644
10242
|
extra: external_exports.record(external_exports.unknown()).optional()
|
|
9645
10243
|
}).strict();
|
|
9646
10244
|
var sandboxRunRequestSchema = external_exports.object({
|
|
@@ -9681,13 +10279,29 @@ var sandboxRunRequestSchema = external_exports.object({
|
|
|
9681
10279
|
* heuristics, forces tool choice when the last user message carries a
|
|
9682
10280
|
* failure marker). Only meaningful alongside `history`.
|
|
9683
10281
|
*/
|
|
9684
|
-
isRecoveryHop: external_exports.boolean().optional()
|
|
10282
|
+
isRecoveryHop: external_exports.boolean().optional(),
|
|
10283
|
+
/**
|
|
10284
|
+
* Run this call against the application structure supplied here
|
|
10285
|
+
* instead of the project's active MCP document.
|
|
10286
|
+
*
|
|
10287
|
+
* Sandbox-only, and the relay honours it only on a `dryRun` call —
|
|
10288
|
+
* see `resolveMcpOverride` in apps/api. It exists because the eval
|
|
10289
|
+
* corpus could otherwise only ever describe ONE app: every scenario
|
|
10290
|
+
* resolves to the same seeded project, so every navigate destination,
|
|
10291
|
+
* every screen title and every bit of domain priming in the prompt
|
|
10292
|
+
* came from the same vehicle-management demo. A suite that can only
|
|
10293
|
+
* pose questions about one app cannot measure whether the agent works
|
|
10294
|
+
* on another one.
|
|
10295
|
+
*
|
|
10296
|
+
* Deliberately loose (`z.record`): the MCP document's shape is owned
|
|
10297
|
+
* by the generator, and pinning it here would turn every additive
|
|
10298
|
+
* generator change into a 422 in the sandbox.
|
|
10299
|
+
*/
|
|
10300
|
+
applicationStructure: external_exports.record(external_exports.unknown()).optional()
|
|
9685
10301
|
}).strict();
|
|
9686
10302
|
var relayDiagnosticsSchema = external_exports.object({
|
|
9687
10303
|
unverifiedTargets: external_exports.array(external_exports.string()),
|
|
9688
|
-
guardsFired: external_exports.array(
|
|
9689
|
-
external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })
|
|
9690
|
-
),
|
|
10304
|
+
guardsFired: external_exports.array(external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })),
|
|
9691
10305
|
repairMode: external_exports.enum(["repair", "strict", "disabled"]),
|
|
9692
10306
|
promptVersion: external_exports.string(),
|
|
9693
10307
|
// Defaulted for producers that predate the field: before it existed
|
|
@@ -9946,6 +10560,30 @@ var identifyUserSchema = external_exports.object({
|
|
|
9946
10560
|
name: external_exports.string().min(1).max(200).optional(),
|
|
9947
10561
|
identifiers: endUserIdentifiersSchema.optional()
|
|
9948
10562
|
});
|
|
10563
|
+
var MAX_EXTERNAL_USER_ID_LENGTH = 128;
|
|
10564
|
+
function normalizeExternalUserId(value) {
|
|
10565
|
+
let raw;
|
|
10566
|
+
if (typeof value === "string") raw = value;
|
|
10567
|
+
else if (typeof value === "number" && Number.isFinite(value)) raw = String(value);
|
|
10568
|
+
else return void 0;
|
|
10569
|
+
const trimmed = raw.trim();
|
|
10570
|
+
if (!trimmed) return void 0;
|
|
10571
|
+
if (trimmed.length > MAX_EXTERNAL_USER_ID_LENGTH) return void 0;
|
|
10572
|
+
return trimmed;
|
|
10573
|
+
}
|
|
10574
|
+
var MAX_DEVICE_INFO_BYTES = 4096;
|
|
10575
|
+
var sessionDeviceInfoSchema = external_exports.object({
|
|
10576
|
+
platform: external_exports.string().max(64).optional(),
|
|
10577
|
+
osVersion: external_exports.string().max(64).optional(),
|
|
10578
|
+
appVersion: external_exports.string().max(64).optional(),
|
|
10579
|
+
sdkVersion: external_exports.string().max(64).optional()
|
|
10580
|
+
}).passthrough().refine((info2) => JSON.stringify(info2).length <= MAX_DEVICE_INFO_BYTES, {
|
|
10581
|
+
message: `device_info exceeds ${MAX_DEVICE_INFO_BYTES} bytes`
|
|
10582
|
+
});
|
|
10583
|
+
var createAgentSessionSchema = external_exports.object({
|
|
10584
|
+
userId: external_exports.preprocess(normalizeExternalUserId, external_exports.string().optional()),
|
|
10585
|
+
deviceInfo: sessionDeviceInfoSchema.optional().catch(void 0)
|
|
10586
|
+
});
|
|
9949
10587
|
var upsertEndUserSchema = external_exports.object({
|
|
9950
10588
|
projectId: external_exports.string().min(1).max(64),
|
|
9951
10589
|
externalId: external_exports.string().min(1).max(128),
|
|
@@ -9969,14 +10607,16 @@ async function loadManifest(rootDir, manifestPath) {
|
|
|
9969
10607
|
if (error2?.code === "ENOENT") {
|
|
9970
10608
|
return { manifest: null, resolvedPath };
|
|
9971
10609
|
}
|
|
9972
|
-
throw new Error(
|
|
10610
|
+
throw new Error(
|
|
10611
|
+
`Failed to read Appilots manifest at ${resolvedPath}: ${stringifyUnknown(error2)}`
|
|
10612
|
+
);
|
|
9973
10613
|
}
|
|
9974
10614
|
let parsedJson;
|
|
9975
10615
|
try {
|
|
9976
10616
|
parsedJson = JSON.parse(raw);
|
|
9977
10617
|
} catch (error2) {
|
|
9978
10618
|
throw new Error(
|
|
9979
|
-
`Appilots manifest at ${resolvedPath} is not valid JSON: ${
|
|
10619
|
+
`Appilots manifest at ${resolvedPath} is not valid JSON: ${stringifyUnknown(error2)}`
|
|
9980
10620
|
);
|
|
9981
10621
|
}
|
|
9982
10622
|
const result = manifestSchema.safeParse(parsedJson);
|
|
@@ -9990,11 +10630,47 @@ Each entry in "screens" must match the ScreenDescriptor shape (see docs/platform
|
|
|
9990
10630
|
}
|
|
9991
10631
|
return { manifest: result.data, resolvedPath };
|
|
9992
10632
|
}
|
|
10633
|
+
function mergeScreen(base, declared) {
|
|
10634
|
+
const fields = { ...base };
|
|
10635
|
+
for (const [key, value] of Object.entries(declared)) {
|
|
10636
|
+
if (value === void 0) continue;
|
|
10637
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
10638
|
+
fields[key] = value;
|
|
10639
|
+
}
|
|
10640
|
+
const merged = fields;
|
|
10641
|
+
merged.targets = mergeById(base.targets, markManifestOrigin(declared.targets));
|
|
10642
|
+
merged.actions = mergeById(base.actions, declared.actions) ?? [];
|
|
10643
|
+
if (base.agentHints || declared.agentHints) {
|
|
10644
|
+
merged.agentHints = { ...base.agentHints, ...declared.agentHints };
|
|
10645
|
+
}
|
|
10646
|
+
return merged;
|
|
10647
|
+
}
|
|
10648
|
+
function mergeById(base, overlay) {
|
|
10649
|
+
if (!overlay || overlay.length === 0) return base;
|
|
10650
|
+
if (!base || base.length === 0) return overlay;
|
|
10651
|
+
const byId = /* @__PURE__ */ new Map();
|
|
10652
|
+
for (const entry of base) byId.set(entry.id, entry);
|
|
10653
|
+
for (const entry of overlay) {
|
|
10654
|
+
const existing = byId.get(entry.id);
|
|
10655
|
+
byId.set(entry.id, existing ? { ...existing, ...entry } : entry);
|
|
10656
|
+
}
|
|
10657
|
+
return Array.from(byId.values());
|
|
10658
|
+
}
|
|
10659
|
+
function markManifestOrigin(targets) {
|
|
10660
|
+
if (!targets || targets.length === 0) return targets;
|
|
10661
|
+
return targets.map((target) => ({ origin: "manifest", ...target }));
|
|
10662
|
+
}
|
|
9993
10663
|
function mergeManifestScreens(analyzerScreens, manifestScreens) {
|
|
9994
10664
|
if (!manifestScreens || manifestScreens.length === 0) return analyzerScreens;
|
|
9995
10665
|
const byName = /* @__PURE__ */ new Map();
|
|
9996
10666
|
for (const screen of analyzerScreens) byName.set(screen.name, screen);
|
|
9997
|
-
for (const
|
|
10667
|
+
for (const declared of manifestScreens) {
|
|
10668
|
+
const base = byName.get(declared.name);
|
|
10669
|
+
byName.set(
|
|
10670
|
+
declared.name,
|
|
10671
|
+
base ? mergeScreen(base, declared) : { ...declared, targets: markManifestOrigin(declared.targets) }
|
|
10672
|
+
);
|
|
10673
|
+
}
|
|
9998
10674
|
return Array.from(byName.values());
|
|
9999
10675
|
}
|
|
10000
10676
|
function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
|
|
@@ -10069,7 +10745,12 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10069
10745
|
}
|
|
10070
10746
|
const mergedScreens = mergeManifestScreens(analyzed.screens, manifest?.screens);
|
|
10071
10747
|
const navigation = mergeManifestNavigation(analyzed.navigation, manifest?.navigation);
|
|
10072
|
-
const agentReadyScreens = mergedScreens.map(
|
|
10748
|
+
const agentReadyScreens = mergedScreens.map(
|
|
10749
|
+
(screen) => enrichScreenForAgent({
|
|
10750
|
+
...screen,
|
|
10751
|
+
filePath: screen.filePath ? import_node_path3.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
|
|
10752
|
+
})
|
|
10753
|
+
);
|
|
10073
10754
|
const projectInfo = await this.getProjectInfo();
|
|
10074
10755
|
const projectName = projectInfo.name;
|
|
10075
10756
|
console.log(`[MCPGenerator] Project name: ${projectName}`);
|
|
@@ -10092,10 +10773,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10092
10773
|
};
|
|
10093
10774
|
const serialized = JSON.stringify(document, null, 2);
|
|
10094
10775
|
const checksum = this.calculateChecksum(serialized);
|
|
10095
|
-
const filePath = import_node_path3.default.resolve(
|
|
10096
|
-
outputDir,
|
|
10097
|
-
`mcp-document.${this.options.format}`
|
|
10098
|
-
);
|
|
10776
|
+
const filePath = import_node_path3.default.resolve(outputDir, `mcp-document.${this.options.format}`);
|
|
10099
10777
|
await (0, import_promises4.writeFile)(filePath, serialized, "utf-8");
|
|
10100
10778
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
10101
10779
|
const checksumFilePath = import_node_path3.default.resolve(outputDir, ".appilots-checksum");
|
|
@@ -10211,14 +10889,136 @@ function reportMetadataLint(document, strict) {
|
|
|
10211
10889
|
}
|
|
10212
10890
|
}
|
|
10213
10891
|
|
|
10214
|
-
// src/cli/
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10892
|
+
// src/cli/utils/remote-drift.ts
|
|
10893
|
+
var import_node_fs = require("fs");
|
|
10894
|
+
var import_node_path4 = require("path");
|
|
10895
|
+
var STUB_RATIO = 0.5;
|
|
10896
|
+
function compareWithRemote(local, remote) {
|
|
10897
|
+
if (remote.screensCount === local.screens) return null;
|
|
10898
|
+
return {
|
|
10899
|
+
localScreens: local.screens,
|
|
10900
|
+
remoteScreens: remote.screensCount,
|
|
10901
|
+
remoteId: remote.id,
|
|
10902
|
+
severe: local.screens > 0 && remote.screensCount <= local.screens * STUB_RATIO
|
|
10903
|
+
};
|
|
10904
|
+
}
|
|
10905
|
+
function readLocalDocumentSummary(outputDir) {
|
|
10906
|
+
try {
|
|
10907
|
+
const raw = (0, import_node_fs.readFileSync)((0, import_node_path4.join)(outputDir, "mcp-document.json"), "utf-8");
|
|
10908
|
+
const parsed = JSON.parse(raw);
|
|
10909
|
+
if (typeof parsed?.metadata?.totalScreens !== "number") return null;
|
|
10910
|
+
return { screens: parsed.metadata.totalScreens };
|
|
10911
|
+
} catch {
|
|
10912
|
+
return null;
|
|
10913
|
+
}
|
|
10914
|
+
}
|
|
10915
|
+
function reportRemoteDrift(drift, options = {}) {
|
|
10916
|
+
if (!drift.severe) {
|
|
10917
|
+
info(
|
|
10918
|
+
`Server's active document differs from what was just generated (active: ${drift.remoteId}, ${drift.remoteScreens} screens).`
|
|
10919
|
+
);
|
|
10920
|
+
return;
|
|
10921
|
+
}
|
|
10922
|
+
warn(
|
|
10923
|
+
`The agent is ${options.uploadFailed ? "still " : ""}running on a ${drift.remoteScreens}-screen document while this app has ${drift.localScreens} screens.`
|
|
10924
|
+
);
|
|
10925
|
+
info(` Active on server: ${drift.remoteId} (${drift.remoteScreens} screens)`);
|
|
10926
|
+
info(` Just generated: ${drift.localScreens} screens`);
|
|
10927
|
+
info("");
|
|
10928
|
+
info("While a document that small is active the agent cannot see the other");
|
|
10929
|
+
info("screens, and missions that need them fail in ways that read as model");
|
|
10930
|
+
info("errors. The usual causes: an older version activated from the");
|
|
10931
|
+
info("dashboard, a sync that went to a different server, or a database");
|
|
10932
|
+
info("seeded before the seed learned to refuse partial documents.");
|
|
10933
|
+
info("");
|
|
10934
|
+
if (options.uploadFailed) {
|
|
10935
|
+
info("Nothing was uploaded just now, so this is unchanged. Fix the error");
|
|
10936
|
+
info("above and run sync again \u2014 e2e runs before that will fail on");
|
|
10937
|
+
info("navigation, not on anything the model did.");
|
|
10938
|
+
} else {
|
|
10939
|
+
info("Uploading now replaces it.");
|
|
10940
|
+
}
|
|
10941
|
+
}
|
|
10942
|
+
|
|
10943
|
+
// src/cli/utils/upload-failure.ts
|
|
10944
|
+
function explainUploadFailure(failure, serverUrl) {
|
|
10945
|
+
const status = failure.errorStatus ?? 0;
|
|
10946
|
+
if (status === 401) {
|
|
10947
|
+
info("");
|
|
10948
|
+
info("`sync` authenticates with the project API key (`ak_\u2026`) \u2014 the same key");
|
|
10949
|
+
info("the SDK uses. Check that it is the key for THIS project, is still");
|
|
10950
|
+
info("active, and reaches the server you meant:");
|
|
10951
|
+
info(` Server: ${serverUrl ?? "(default)"}`);
|
|
10952
|
+
info("Set it in .appilotsrc (`apiKey`), via APPILOTS_API_KEY, or --api-key.");
|
|
10953
|
+
info("");
|
|
10954
|
+
info("A dashboard login token will not work here and is not needed: the CLI");
|
|
10955
|
+
info("talks to /cli/sync, not the dashboard's /mcp-documents.");
|
|
10956
|
+
return;
|
|
10957
|
+
}
|
|
10958
|
+
if (failure.errorCode === "WORKSPACE_INACTIVE") {
|
|
10959
|
+
info("");
|
|
10960
|
+
info("The workspace is deactivated. Re-activate it in the dashboard");
|
|
10961
|
+
info("(Settings \u2192 Workspace) and run sync again.");
|
|
10962
|
+
return;
|
|
10963
|
+
}
|
|
10964
|
+
if (status === 404) {
|
|
10965
|
+
info("");
|
|
10966
|
+
info(`No /cli/sync at ${serverUrl ?? "(default)"} \u2014 check serverUrl points at`);
|
|
10967
|
+
info("an Appilots API (and one recent enough to serve the CLI routes).");
|
|
10968
|
+
return;
|
|
10969
|
+
}
|
|
10970
|
+
if (status === 422) {
|
|
10971
|
+
info("");
|
|
10972
|
+
info("The server rejected the document itself. Inspect what was generated:");
|
|
10973
|
+
info(" .appilots/mcp-document.json");
|
|
10974
|
+
return;
|
|
10975
|
+
}
|
|
10976
|
+
if (status >= 500) {
|
|
10977
|
+
info("");
|
|
10978
|
+
info("The server failed to process the upload. It was retried already \u2014");
|
|
10979
|
+
info("if it persists, the API logs hold the matching request id.");
|
|
10980
|
+
}
|
|
10981
|
+
}
|
|
10982
|
+
|
|
10983
|
+
// src/cli/utils/empty-document.ts
|
|
10984
|
+
function assertDocumentHasScreens(document, options = {}) {
|
|
10985
|
+
if ((document.metadata?.totalScreens ?? 0) > 0) return;
|
|
10986
|
+
if (options.allowEmpty) {
|
|
10987
|
+
info("No screens found, continuing because --allow-empty was passed.");
|
|
10988
|
+
return;
|
|
10989
|
+
}
|
|
10990
|
+
error("No screens found \u2014 this application map would give the agent nothing to navigate.");
|
|
10991
|
+
info("");
|
|
10992
|
+
info("The analyzer looks for React Navigation: a <NavigationContainer> and");
|
|
10993
|
+
info("<Stack.Screen> / <Tab.Screen> declarations. It found none.");
|
|
10994
|
+
info("");
|
|
10995
|
+
info("The two usual causes:");
|
|
10996
|
+
info(" \u2022 The app uses Expo Router (or another file-based router). It is the");
|
|
10997
|
+
info(" default for `npx create-expo-app` and the analyzer cannot read it.");
|
|
10998
|
+
info(" \u2022 The screens live outside the analyzed paths \u2014 check `include`,");
|
|
10999
|
+
info(" `exclude` and `screenPatterns` in your Appilots config.");
|
|
11000
|
+
info("");
|
|
11001
|
+
info("The escape hatch, which works for any router: declare your screens in");
|
|
11002
|
+
info("an `appilots.manifest.json` at the project root. Its screens are merged");
|
|
11003
|
+
info("on top of whatever the analyzer found, so a fully declared app needs no");
|
|
11004
|
+
info("React Navigation at all. See the CLI docs for the shape.");
|
|
11005
|
+
info("");
|
|
11006
|
+
info("If an empty map really is what you want, pass --allow-empty.");
|
|
11007
|
+
process.exit(1);
|
|
11008
|
+
}
|
|
11009
|
+
|
|
11010
|
+
// src/cli/commands/sync.ts
|
|
11011
|
+
function syncCommand() {
|
|
11012
|
+
return new import_commander2.Command("sync").description("Analyze project and sync MCP documents with Appilots").option("--verbose", "Enable verbose logging", false).option("--force", "Force upload even if nothing changed", false).option("--api-key <key>", "Appilots API key (overrides .appilotsrc and APPILOTS_API_KEY)").option("--project-id <id>", "Project ID (overrides .appilotsrc and APPILOTS_PROJECT_ID)").option("--server <url>", "Appilots server URL (overrides .appilotsrc and APPILOTS_SERVER_URL)").option("--json", "Output a machine-readable JSON result (implies --quiet)", false).option("--quiet", "Suppress banner and informational output", false).option(
|
|
11013
|
+
"--strict-metadata",
|
|
11014
|
+
"Fail when mutating actions are missing effect/riskLevel metadata",
|
|
11015
|
+
false
|
|
11016
|
+
).option("--allow-empty", "Do not fail when the application map has zero screens", false).action(async (options) => {
|
|
11017
|
+
if (options.json) setLogMode("json");
|
|
11018
|
+
else if (options.quiet) setLogMode("quiet");
|
|
11019
|
+
try {
|
|
10220
11020
|
banner();
|
|
10221
|
-
const loaded = loadConfig();
|
|
11021
|
+
const loaded = loadConfig(warn);
|
|
10222
11022
|
const apiKey = options.apiKey || loaded?.apiKey;
|
|
10223
11023
|
if (!apiKey) {
|
|
10224
11024
|
error("No .appilotsrc configuration found");
|
|
@@ -10229,7 +11029,7 @@ function syncCommand() {
|
|
|
10229
11029
|
return;
|
|
10230
11030
|
}
|
|
10231
11031
|
const config = {
|
|
10232
|
-
serverUrl:
|
|
11032
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
10233
11033
|
outputDir: ".appilots",
|
|
10234
11034
|
...loaded,
|
|
10235
11035
|
apiKey,
|
|
@@ -10243,7 +11043,7 @@ function syncCommand() {
|
|
|
10243
11043
|
const outputDir = config.outputDir || ".appilots";
|
|
10244
11044
|
let previousChecksum = "";
|
|
10245
11045
|
try {
|
|
10246
|
-
const checksumPath = (0,
|
|
11046
|
+
const checksumPath = (0, import_node_path5.join)(outputDir, ".appilots-checksum");
|
|
10247
11047
|
previousChecksum = (await (0, import_promises5.readFile)(checksumPath, "utf-8")).trim();
|
|
10248
11048
|
} catch {
|
|
10249
11049
|
}
|
|
@@ -10264,22 +11064,32 @@ function syncCommand() {
|
|
|
10264
11064
|
});
|
|
10265
11065
|
const output = await generator.generate();
|
|
10266
11066
|
spinner.stop();
|
|
11067
|
+
assertDocumentHasScreens(output.document, {
|
|
11068
|
+
rootDir: process.cwd(),
|
|
11069
|
+
allowEmpty: Boolean(options.allowEmpty)
|
|
11070
|
+
});
|
|
10267
11071
|
success(
|
|
10268
11072
|
`Found ${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
|
|
10269
11073
|
);
|
|
10270
11074
|
reportMetadataLint(output.document, Boolean(options.strictMetadata));
|
|
11075
|
+
spinner = createSpinner("Checking remote status...");
|
|
11076
|
+
spinner.start();
|
|
11077
|
+
const remote = await apiClient.status();
|
|
11078
|
+
spinner.stop();
|
|
11079
|
+
const remoteActive = remote.error ? null : remote.activeMcp ?? null;
|
|
11080
|
+
const drift = remoteActive ? compareWithRemote({ screens: output.document.metadata.totalScreens }, remoteActive) : null;
|
|
10271
11081
|
if (!options.force && previousChecksum && previousChecksum === output.checksum) {
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
10277
|
-
|
|
11082
|
+
if (remote.error) {
|
|
11083
|
+
warn(`Could not verify remote: ${remote.error}`);
|
|
11084
|
+
info("Uploading anyway...");
|
|
11085
|
+
} else if (remoteActive) {
|
|
11086
|
+
if (!drift) {
|
|
11087
|
+
success("Already up to date (no local or remote changes)");
|
|
11088
|
+
jsonOutput({ success: true, unchanged: true, checksum: output.checksum });
|
|
11089
|
+
return;
|
|
11090
|
+
}
|
|
11091
|
+
reportRemoteDrift(drift);
|
|
10278
11092
|
info("Uploading anyway...");
|
|
10279
|
-
} else if (status.activeMcp) {
|
|
10280
|
-
success("Already up to date (no local or remote changes)");
|
|
10281
|
-
jsonOutput({ success: true, unchanged: true, checksum: output.checksum });
|
|
10282
|
-
return;
|
|
10283
11093
|
}
|
|
10284
11094
|
}
|
|
10285
11095
|
spinner = createSpinner("Uploading to Appilots...");
|
|
@@ -10292,13 +11102,33 @@ function syncCommand() {
|
|
|
10292
11102
|
spinner.stop();
|
|
10293
11103
|
if (!syncResult.success) {
|
|
10294
11104
|
error(`Sync failed: ${syncResult.error}`);
|
|
10295
|
-
|
|
11105
|
+
hintUnreachableServer(config.serverUrl, syncResult.error);
|
|
11106
|
+
explainUploadFailure(syncResult, config.serverUrl);
|
|
11107
|
+
if (drift?.severe) {
|
|
11108
|
+
reportRemoteDrift(drift, { uploadFailed: true });
|
|
11109
|
+
}
|
|
11110
|
+
jsonOutput({
|
|
11111
|
+
success: false,
|
|
11112
|
+
error: syncResult.error,
|
|
11113
|
+
errorCode: syncResult.errorCode,
|
|
11114
|
+
errorStatus: syncResult.errorStatus,
|
|
11115
|
+
activeDocument: remoteActive,
|
|
11116
|
+
drift
|
|
11117
|
+
});
|
|
10296
11118
|
process.exit(1);
|
|
10297
11119
|
}
|
|
10298
11120
|
if (syncResult.unchanged && !options.force) {
|
|
10299
11121
|
success("Already up to date on server");
|
|
11122
|
+
if (syncResult.activated) {
|
|
11123
|
+
info(`Re-activated document ${syncResult.id} (it was stored but not active).`);
|
|
11124
|
+
}
|
|
10300
11125
|
} else {
|
|
10301
11126
|
success("Successfully synced MCP document");
|
|
11127
|
+
if (drift?.severe) {
|
|
11128
|
+
info(
|
|
11129
|
+
`Replaced the ${drift.remoteScreens}-screen document that was active (${drift.remoteId}) \u2014 the agent could not see the other ${drift.localScreens - drift.remoteScreens} screens until now.`
|
|
11130
|
+
);
|
|
11131
|
+
}
|
|
10302
11132
|
info(`Document ID: ${syncResult.id}`);
|
|
10303
11133
|
info(`Checksum: ${syncResult.checksum}`);
|
|
10304
11134
|
if (config.projectId) {
|
|
@@ -10328,8 +11158,8 @@ function syncCommand() {
|
|
|
10328
11158
|
actionsCount: syncResult.actionsCount ?? output.document.metadata.totalActions
|
|
10329
11159
|
});
|
|
10330
11160
|
} catch (err) {
|
|
10331
|
-
error(`Sync failed: ${
|
|
10332
|
-
jsonOutput({ success: false, error:
|
|
11161
|
+
error(`Sync failed: ${stringifyUnknown(err)}`);
|
|
11162
|
+
jsonOutput({ success: false, error: stringifyUnknown(err) });
|
|
10333
11163
|
if (options.verbose) {
|
|
10334
11164
|
console.error(err);
|
|
10335
11165
|
}
|
|
@@ -10340,12 +11170,12 @@ function syncCommand() {
|
|
|
10340
11170
|
|
|
10341
11171
|
// src/cli/commands/watch.ts
|
|
10342
11172
|
var import_commander3 = require("commander");
|
|
10343
|
-
var
|
|
11173
|
+
var import_node_fs2 = require("fs");
|
|
10344
11174
|
function watchCommand() {
|
|
10345
11175
|
return new import_commander3.Command("watch").description("Watch project for changes and auto-sync MCP documents").option("--verbose", "Enable verbose logging", false).action(async (options) => {
|
|
10346
11176
|
try {
|
|
10347
11177
|
banner();
|
|
10348
|
-
const config = loadConfig();
|
|
11178
|
+
const config = loadConfig(warn);
|
|
10349
11179
|
if (!config) {
|
|
10350
11180
|
error("No .appilotsrc configuration found");
|
|
10351
11181
|
info("Run: appilots init --api-key <your-key>");
|
|
@@ -10374,7 +11204,10 @@ function watchCommand() {
|
|
|
10374
11204
|
};
|
|
10375
11205
|
const generator = new MCPGenerator(generatorConfig);
|
|
10376
11206
|
const initialOutput = await generator.generate();
|
|
10377
|
-
const initialSync = await apiClient.sync(
|
|
11207
|
+
const initialSync = await apiClient.sync(
|
|
11208
|
+
initialOutput.document,
|
|
11209
|
+
initialOutput.document.version
|
|
11210
|
+
);
|
|
10378
11211
|
if (!initialSync.success) {
|
|
10379
11212
|
error(`Initial sync failed: ${initialSync.error}`);
|
|
10380
11213
|
process.exit(1);
|
|
@@ -10384,45 +11217,41 @@ function watchCommand() {
|
|
|
10384
11217
|
dim("Press Ctrl+C to stop\n");
|
|
10385
11218
|
let debounceTimer = null;
|
|
10386
11219
|
const DEBOUNCE_MS = 500;
|
|
10387
|
-
const watcher = (0,
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
if (!filename) return;
|
|
10392
|
-
if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
|
|
10393
|
-
return;
|
|
10394
|
-
}
|
|
10395
|
-
if (debounceTimer) {
|
|
10396
|
-
clearTimeout(debounceTimer);
|
|
10397
|
-
}
|
|
10398
|
-
debounceTimer = setTimeout(async () => {
|
|
10399
|
-
try {
|
|
10400
|
-
if (options.verbose) {
|
|
10401
|
-
info(`Change detected: ${filename}`);
|
|
10402
|
-
}
|
|
10403
|
-
const spinner = createSpinner("Re-analyzing project...");
|
|
10404
|
-
spinner.start();
|
|
10405
|
-
const newGenerator = new MCPGenerator(generatorConfig);
|
|
10406
|
-
const output = await newGenerator.generate();
|
|
10407
|
-
spinner.text = "Uploading changes...";
|
|
10408
|
-
const syncResult = await apiClient.sync(output.document, output.document.version);
|
|
10409
|
-
spinner.stop();
|
|
10410
|
-
if (!syncResult.success) {
|
|
10411
|
-
error(`Sync failed: ${syncResult.error}`);
|
|
10412
|
-
} else if (syncResult.unchanged) {
|
|
10413
|
-
info("Changes synced (no content changes detected)");
|
|
10414
|
-
} else {
|
|
10415
|
-
success("Changes synced successfully");
|
|
10416
|
-
info(
|
|
10417
|
-
`${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
|
|
10418
|
-
);
|
|
10419
|
-
}
|
|
10420
|
-
} catch (err) {
|
|
10421
|
-
error(`Watch sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
10422
|
-
}
|
|
10423
|
-
}, DEBOUNCE_MS);
|
|
11220
|
+
const watcher = (0, import_node_fs2.watch)(process.cwd(), { recursive: true }, async (eventType, filename) => {
|
|
11221
|
+
if (!filename) return;
|
|
11222
|
+
if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
|
|
11223
|
+
return;
|
|
10424
11224
|
}
|
|
10425
|
-
|
|
11225
|
+
if (debounceTimer) {
|
|
11226
|
+
clearTimeout(debounceTimer);
|
|
11227
|
+
}
|
|
11228
|
+
debounceTimer = setTimeout(async () => {
|
|
11229
|
+
try {
|
|
11230
|
+
if (options.verbose) {
|
|
11231
|
+
info(`Change detected: ${filename}`);
|
|
11232
|
+
}
|
|
11233
|
+
const spinner = createSpinner("Re-analyzing project...");
|
|
11234
|
+
spinner.start();
|
|
11235
|
+
const newGenerator = new MCPGenerator(generatorConfig);
|
|
11236
|
+
const output = await newGenerator.generate();
|
|
11237
|
+
spinner.text = "Uploading changes...";
|
|
11238
|
+
const syncResult = await apiClient.sync(output.document, output.document.version);
|
|
11239
|
+
spinner.stop();
|
|
11240
|
+
if (!syncResult.success) {
|
|
11241
|
+
error(`Sync failed: ${syncResult.error}`);
|
|
11242
|
+
} else if (syncResult.unchanged) {
|
|
11243
|
+
info("Changes synced (no content changes detected)");
|
|
11244
|
+
} else {
|
|
11245
|
+
success("Changes synced successfully");
|
|
11246
|
+
info(
|
|
11247
|
+
`${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
|
|
11248
|
+
);
|
|
11249
|
+
}
|
|
11250
|
+
} catch (err) {
|
|
11251
|
+
error(`Watch sync failed: ${stringifyUnknown(err)}`);
|
|
11252
|
+
}
|
|
11253
|
+
}, DEBOUNCE_MS);
|
|
11254
|
+
});
|
|
10426
11255
|
process.on("SIGINT", () => {
|
|
10427
11256
|
info("Stopping watcher...");
|
|
10428
11257
|
watcher.close();
|
|
@@ -10432,7 +11261,7 @@ function watchCommand() {
|
|
|
10432
11261
|
process.exit(0);
|
|
10433
11262
|
});
|
|
10434
11263
|
} catch (err) {
|
|
10435
|
-
error(`Watch failed: ${
|
|
11264
|
+
error(`Watch failed: ${stringifyUnknown(err)}`);
|
|
10436
11265
|
process.exit(1);
|
|
10437
11266
|
}
|
|
10438
11267
|
});
|
|
@@ -10441,7 +11270,11 @@ function watchCommand() {
|
|
|
10441
11270
|
// src/cli/commands/generate.ts
|
|
10442
11271
|
var import_commander4 = require("commander");
|
|
10443
11272
|
function generateCommand() {
|
|
10444
|
-
return new import_commander4.Command("generate").description("Generate MCP documents from project source (local only)").option("-o, --output <path>", "Output directory for MCP documents", ".appilots").option("-f, --format <format>", "Output format (only json supported)", "json").option(
|
|
11273
|
+
return new import_commander4.Command("generate").description("Generate MCP documents from project source (local only)").option("-o, --output <path>", "Output directory for MCP documents", ".appilots").option("-f, --format <format>", "Output format (only json supported)", "json").option(
|
|
11274
|
+
"--strict-metadata",
|
|
11275
|
+
"Fail when mutating actions are missing effect/riskLevel metadata",
|
|
11276
|
+
false
|
|
11277
|
+
).option("--allow-empty", "Do not fail when the application map has zero screens", false).action(async (options) => {
|
|
10445
11278
|
try {
|
|
10446
11279
|
banner();
|
|
10447
11280
|
if (options.format && options.format !== "json") {
|
|
@@ -10452,7 +11285,7 @@ function generateCommand() {
|
|
|
10452
11285
|
spinner.start();
|
|
10453
11286
|
let loaded = null;
|
|
10454
11287
|
try {
|
|
10455
|
-
loaded = loadConfig();
|
|
11288
|
+
loaded = loadConfig(warn);
|
|
10456
11289
|
} catch {
|
|
10457
11290
|
loaded = null;
|
|
10458
11291
|
}
|
|
@@ -10465,6 +11298,10 @@ function generateCommand() {
|
|
|
10465
11298
|
});
|
|
10466
11299
|
const output = await generator.generate();
|
|
10467
11300
|
spinner.stop();
|
|
11301
|
+
assertDocumentHasScreens(output.document, {
|
|
11302
|
+
rootDir: process.cwd(),
|
|
11303
|
+
allowEmpty: Boolean(options.allowEmpty)
|
|
11304
|
+
});
|
|
10468
11305
|
success(
|
|
10469
11306
|
`Generated MCP document with ${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
|
|
10470
11307
|
);
|
|
@@ -10472,21 +11309,761 @@ function generateCommand() {
|
|
|
10472
11309
|
info(`Checksum: ${output.checksum}`);
|
|
10473
11310
|
reportMetadataLint(output.document, Boolean(options.strictMetadata));
|
|
10474
11311
|
} catch (err) {
|
|
10475
|
-
error(`Generation failed: ${
|
|
11312
|
+
error(`Generation failed: ${stringifyUnknown(err)}`);
|
|
10476
11313
|
process.exit(1);
|
|
10477
11314
|
}
|
|
10478
11315
|
});
|
|
10479
11316
|
}
|
|
10480
11317
|
|
|
10481
|
-
// src/cli/commands/
|
|
11318
|
+
// src/cli/commands/annotate.ts
|
|
11319
|
+
var import_promises6 = require("fs/promises");
|
|
11320
|
+
var import_node_path7 = __toESM(require("path"));
|
|
10482
11321
|
var import_commander5 = require("commander");
|
|
11322
|
+
var import_fast_glob6 = __toESM(require("fast-glob"));
|
|
11323
|
+
|
|
11324
|
+
// src/annotate/sites.ts
|
|
11325
|
+
var import_traverse12 = __toESM(require("@babel/traverse"));
|
|
11326
|
+
var t13 = __toESM(require("@babel/types"));
|
|
11327
|
+
|
|
11328
|
+
// src/annotate/forwarding.ts
|
|
11329
|
+
var import_node_path6 = __toESM(require("path"));
|
|
11330
|
+
var import_node_fs3 = require("fs");
|
|
11331
|
+
var import_traverse11 = __toESM(require("@babel/traverse"));
|
|
11332
|
+
var t12 = __toESM(require("@babel/types"));
|
|
11333
|
+
var traverse11 = import_traverse11.default.default ?? import_traverse11.default;
|
|
11334
|
+
var RN_TESTID_CARRIERS = /* @__PURE__ */ new Set([
|
|
11335
|
+
"View",
|
|
11336
|
+
"Text",
|
|
11337
|
+
"TextInput",
|
|
11338
|
+
"Image",
|
|
11339
|
+
"ImageBackground",
|
|
11340
|
+
"ScrollView",
|
|
11341
|
+
"FlatList",
|
|
11342
|
+
"SectionList",
|
|
11343
|
+
"VirtualizedList",
|
|
11344
|
+
"TouchableOpacity",
|
|
11345
|
+
"TouchableHighlight",
|
|
11346
|
+
"TouchableWithoutFeedback",
|
|
11347
|
+
"TouchableNativeFeedback",
|
|
11348
|
+
"Pressable",
|
|
11349
|
+
"Button",
|
|
11350
|
+
"Switch",
|
|
11351
|
+
"Modal",
|
|
11352
|
+
"RefreshControl",
|
|
11353
|
+
"ActivityIndicator",
|
|
11354
|
+
"SafeAreaView",
|
|
11355
|
+
"KeyboardAvoidingView",
|
|
11356
|
+
"Slider"
|
|
11357
|
+
]);
|
|
11358
|
+
function collectImports(ast) {
|
|
11359
|
+
const out = /* @__PURE__ */ new Map();
|
|
11360
|
+
traverse11(ast, {
|
|
11361
|
+
ImportDeclaration(nodePath) {
|
|
11362
|
+
const source = nodePath.node.source.value;
|
|
11363
|
+
for (const spec of nodePath.node.specifiers) {
|
|
11364
|
+
if (t12.isImportSpecifier(spec)) {
|
|
11365
|
+
const imported = t12.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
11366
|
+
out.set(spec.local.name, { source, local: spec.local.name, imported });
|
|
11367
|
+
} else if (t12.isImportDefaultSpecifier(spec)) {
|
|
11368
|
+
out.set(spec.local.name, { source, local: spec.local.name, imported: "default" });
|
|
11369
|
+
}
|
|
11370
|
+
}
|
|
11371
|
+
}
|
|
11372
|
+
});
|
|
11373
|
+
return out;
|
|
11374
|
+
}
|
|
11375
|
+
var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
11376
|
+
function resolveRelativeImport(fromFile, source) {
|
|
11377
|
+
if (!source.startsWith(".")) return void 0;
|
|
11378
|
+
const base = import_node_path6.default.resolve(import_node_path6.default.dirname(fromFile), source);
|
|
11379
|
+
for (const ext of EXTENSIONS) {
|
|
11380
|
+
const candidate = `${base}${ext}`;
|
|
11381
|
+
if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
|
|
11382
|
+
}
|
|
11383
|
+
for (const ext of EXTENSIONS) {
|
|
11384
|
+
const candidate = import_node_path6.default.join(base, `index${ext}`);
|
|
11385
|
+
if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
|
|
11386
|
+
}
|
|
11387
|
+
return void 0;
|
|
11388
|
+
}
|
|
11389
|
+
var IDENTITY_HOOKS = /* @__PURE__ */ new Set([
|
|
11390
|
+
"useAppilotsTarget",
|
|
11391
|
+
"useAppilotsField",
|
|
11392
|
+
"useAppilotsToggle",
|
|
11393
|
+
"useAppilotsSlider"
|
|
11394
|
+
]);
|
|
11395
|
+
function inspectModule(file2, exportName, depth = 0) {
|
|
11396
|
+
const none = { forwards: false, derives: false };
|
|
11397
|
+
if (depth > 2) return none;
|
|
11398
|
+
let source;
|
|
11399
|
+
try {
|
|
11400
|
+
source = (0, import_node_fs3.readFileSync)(file2, "utf-8");
|
|
11401
|
+
} catch {
|
|
11402
|
+
return none;
|
|
11403
|
+
}
|
|
11404
|
+
const mentionsIdentity = source.includes("testID") || source.includes("accessibilityLabel") || source.includes("useAppilots") || /\{\s*\.\.\./.test(source);
|
|
11405
|
+
if (!mentionsIdentity && !/export\s*\{[^}]*\}\s*from/.test(source)) return none;
|
|
11406
|
+
let ast;
|
|
11407
|
+
try {
|
|
11408
|
+
ast = parseSource(source);
|
|
11409
|
+
} catch {
|
|
11410
|
+
return none;
|
|
11411
|
+
}
|
|
11412
|
+
let forwards = false;
|
|
11413
|
+
let derives = false;
|
|
11414
|
+
let barrelTarget;
|
|
11415
|
+
traverse11(ast, {
|
|
11416
|
+
// `export { Card } from './Card'` — follow one hop.
|
|
11417
|
+
ExportNamedDeclaration(nodePath) {
|
|
11418
|
+
const from = nodePath.node.source?.value;
|
|
11419
|
+
if (!from) return;
|
|
11420
|
+
const named = nodePath.node.specifiers.some(
|
|
11421
|
+
(spec) => t12.isExportSpecifier(spec) && (t12.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === exportName
|
|
11422
|
+
);
|
|
11423
|
+
if (named) barrelTarget = from;
|
|
11424
|
+
},
|
|
11425
|
+
ExportAllDeclaration(nodePath) {
|
|
11426
|
+
barrelTarget ??= nodePath.node.source.value;
|
|
11427
|
+
},
|
|
11428
|
+
// `interface CardProps { testID?: string }` / `type Props = { testID… }`
|
|
11429
|
+
TSPropertySignature(nodePath) {
|
|
11430
|
+
if (t12.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
|
|
11431
|
+
},
|
|
11432
|
+
// `function Card({ testID }) {}` — the destructured parameter.
|
|
11433
|
+
ObjectProperty(nodePath) {
|
|
11434
|
+
if (t12.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
|
|
11435
|
+
},
|
|
11436
|
+
// `<View {...props} />` — anything spread onto JSX carries it along.
|
|
11437
|
+
JSXSpreadAttribute() {
|
|
11438
|
+
forwards = true;
|
|
11439
|
+
},
|
|
11440
|
+
// `testID={`toggle-${id}`}` / `accessibilityLabel={label}` — the
|
|
11441
|
+
// component naming itself from a prop. A STRING literal does not count:
|
|
11442
|
+
// that is one hardcoded name shared by every instance, which is the
|
|
11443
|
+
// duplicate-id problem rather than a solution to it.
|
|
11444
|
+
JSXAttribute(nodePath) {
|
|
11445
|
+
const name = t12.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
|
|
11446
|
+
if (name !== "testID" && name !== "accessibilityLabel") return;
|
|
11447
|
+
const value = nodePath.node.value;
|
|
11448
|
+
if (!t12.isJSXExpressionContainer(value)) return;
|
|
11449
|
+
if (t12.isStringLiteral(value.expression)) return;
|
|
11450
|
+
derives = true;
|
|
11451
|
+
},
|
|
11452
|
+
// `useAppilotsTarget(`${groupId}-${option.value}`, …)`
|
|
11453
|
+
CallExpression(nodePath) {
|
|
11454
|
+
const callee = nodePath.node.callee;
|
|
11455
|
+
if (t12.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
|
|
11456
|
+
}
|
|
11457
|
+
});
|
|
11458
|
+
if (forwards || derives) return { forwards, derives };
|
|
11459
|
+
if (barrelTarget) {
|
|
11460
|
+
const next = resolveRelativeImport(file2, barrelTarget);
|
|
11461
|
+
if (next) return inspectModule(next, exportName, depth + 1);
|
|
11462
|
+
}
|
|
11463
|
+
return none;
|
|
11464
|
+
}
|
|
11465
|
+
var verdictCache = /* @__PURE__ */ new Map();
|
|
11466
|
+
function canCarryTestId(componentName, fromFile, imports) {
|
|
11467
|
+
const origin = imports.get(componentName);
|
|
11468
|
+
if (origin?.source === "react-native") {
|
|
11469
|
+
return RN_TESTID_CARRIERS.has(origin.imported) ? "rn-primitive" : "unverifiable";
|
|
11470
|
+
}
|
|
11471
|
+
const target = origin ? resolveRelativeImport(fromFile, origin.source) : fromFile;
|
|
11472
|
+
if (!target) return "unverifiable";
|
|
11473
|
+
const exportName = origin?.imported ?? componentName;
|
|
11474
|
+
const key = `${target}#${exportName}`;
|
|
11475
|
+
let identity = verdictCache.get(key);
|
|
11476
|
+
if (identity === void 0) {
|
|
11477
|
+
identity = inspectModule(target, exportName);
|
|
11478
|
+
verdictCache.set(key, identity);
|
|
11479
|
+
}
|
|
11480
|
+
if (identity.forwards) return "forwards";
|
|
11481
|
+
if (identity.derives) return "derives";
|
|
11482
|
+
return "swallows";
|
|
11483
|
+
}
|
|
11484
|
+
function resetForwardingCache() {
|
|
11485
|
+
verdictCache.clear();
|
|
11486
|
+
}
|
|
11487
|
+
|
|
11488
|
+
// src/annotate/sites.ts
|
|
11489
|
+
var traverse12 = import_traverse12.default.default ?? import_traverse12.default;
|
|
11490
|
+
var LABEL_PROPS = ["title", "label", "placeholder"];
|
|
11491
|
+
var IDENTITY_PROPS = ["testID", "appilotsId", "accessibilityLabel"];
|
|
11492
|
+
function kebab(text) {
|
|
11493
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
11494
|
+
}
|
|
11495
|
+
function verbOf(name) {
|
|
11496
|
+
return kebab(name.replace(/^(handle|on)(?=[A-Z])/, ""));
|
|
11497
|
+
}
|
|
11498
|
+
function scopeSlugForFile(file2) {
|
|
11499
|
+
const base = file2.replace(/\\/g, "/").split("/").pop() ?? file2;
|
|
11500
|
+
const name = base.replace(/\.[jt]sx?$/, "");
|
|
11501
|
+
if (name.toLowerCase() === "index") {
|
|
11502
|
+
const dir = file2.replace(/\\/g, "/").split("/").slice(-2, -1)[0] ?? "";
|
|
11503
|
+
return kebab(dir);
|
|
11504
|
+
}
|
|
11505
|
+
return kebab(name.replace(/Screen$/, ""));
|
|
11506
|
+
}
|
|
11507
|
+
function collapseRepeats(slug) {
|
|
11508
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11509
|
+
const out = [];
|
|
11510
|
+
for (const token of slug.split("-")) {
|
|
11511
|
+
if (!token || seen.has(token)) continue;
|
|
11512
|
+
seen.add(token);
|
|
11513
|
+
out.push(token);
|
|
11514
|
+
}
|
|
11515
|
+
return out.join("-");
|
|
11516
|
+
}
|
|
11517
|
+
function deriveTestId(site) {
|
|
11518
|
+
const base = [site.scope, kebab(site.component)].filter(Boolean).join("-");
|
|
11519
|
+
if (site.pressProp && site.pressProp !== "onPress") {
|
|
11520
|
+
return collapseRepeats(`${base}-${verbOf(site.pressProp)}`);
|
|
11521
|
+
}
|
|
11522
|
+
if (site.handler) {
|
|
11523
|
+
const verb = verbOf(site.handler);
|
|
11524
|
+
return collapseRepeats(verb ? `${base}-${verb}` : base);
|
|
11525
|
+
}
|
|
11526
|
+
return collapseRepeats(`${base}-${site.ordinal}`);
|
|
11527
|
+
}
|
|
11528
|
+
function hasAnyProp(element, names) {
|
|
11529
|
+
return names.some((name) => findJsxAttribute(element, name) !== void 0);
|
|
11530
|
+
}
|
|
11531
|
+
function hasLabelText(element) {
|
|
11532
|
+
return LABEL_PROPS.some((name) => {
|
|
11533
|
+
const value = getStringAttr(element, name);
|
|
11534
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
11535
|
+
});
|
|
11536
|
+
}
|
|
11537
|
+
function namedHandler(element, prop) {
|
|
11538
|
+
if (!prop) return void 0;
|
|
11539
|
+
const attr = findJsxAttribute(element, prop);
|
|
11540
|
+
if (!attr || !t13.isJSXExpressionContainer(attr.value)) return void 0;
|
|
11541
|
+
return t13.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
|
|
11542
|
+
}
|
|
11543
|
+
var ANNOTATABLE_ROLES = /* @__PURE__ */ new Set(["button", "input", "select", "toggle", "date", "list"]);
|
|
11544
|
+
function isPerItemRender(nodePath) {
|
|
11545
|
+
let current = nodePath.parentPath;
|
|
11546
|
+
while (current) {
|
|
11547
|
+
const node = current.node;
|
|
11548
|
+
if (t13.isCallExpression(node) && t13.isMemberExpression(node.callee) && t13.isIdentifier(node.callee.property) && (node.callee.property.name === "map" || node.callee.property.name === "flatMap")) {
|
|
11549
|
+
return true;
|
|
11550
|
+
}
|
|
11551
|
+
if (t13.isJSXAttribute(node) && t13.isJSXIdentifier(node.name) && /^render[A-Z]|^ListHeaderComponent$|^ListFooterComponent$/.test(node.name.name) && node.name.name !== "renderScrollComponent") {
|
|
11552
|
+
return /^renderItem$|^renderSectionHeader$|^renderSectionFooter$/.test(node.name.name);
|
|
11553
|
+
}
|
|
11554
|
+
current = current.parentPath;
|
|
11555
|
+
}
|
|
11556
|
+
return false;
|
|
11557
|
+
}
|
|
11558
|
+
function findAnnotationSites(source, file2) {
|
|
11559
|
+
let ast;
|
|
11560
|
+
try {
|
|
11561
|
+
ast = parseSource(source);
|
|
11562
|
+
} catch {
|
|
11563
|
+
return [];
|
|
11564
|
+
}
|
|
11565
|
+
const imports = collectImports(ast);
|
|
11566
|
+
const scope = scopeSlugForFile(file2);
|
|
11567
|
+
const sites = [];
|
|
11568
|
+
const perComponent = /* @__PURE__ */ new Map();
|
|
11569
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
11570
|
+
traverse12(ast, {
|
|
11571
|
+
JSXOpeningElement(nodePath) {
|
|
11572
|
+
for (const prop of IDENTITY_PROPS) {
|
|
11573
|
+
const existing = getStringAttr(nodePath.node, prop);
|
|
11574
|
+
if (existing) usedIds.add(existing);
|
|
11575
|
+
}
|
|
11576
|
+
}
|
|
11577
|
+
});
|
|
11578
|
+
traverse12(ast, {
|
|
11579
|
+
JSXOpeningElement(nodePath) {
|
|
11580
|
+
const element = nodePath.node;
|
|
11581
|
+
if (!t13.isJSXIdentifier(element.name)) return;
|
|
11582
|
+
const component = element.name.name;
|
|
11583
|
+
const role = classifyJsxComponent(component, element);
|
|
11584
|
+
if (!ANNOTATABLE_ROLES.has(role)) return;
|
|
11585
|
+
if (hasAnyProp(element, IDENTITY_PROPS)) return;
|
|
11586
|
+
if (role === "button" && hasLabelText(element)) return;
|
|
11587
|
+
const carrier = canCarryTestId(component, file2, imports);
|
|
11588
|
+
if (carrier === "derives") return;
|
|
11589
|
+
if (isPerItemRender(nodePath)) return;
|
|
11590
|
+
const pressProp = pressHandlerProp(element) ?? void 0;
|
|
11591
|
+
const handler = namedHandler(element, pressProp);
|
|
11592
|
+
const ordinal = (perComponent.get(component) ?? 0) + 1;
|
|
11593
|
+
perComponent.set(component, ordinal);
|
|
11594
|
+
let testId = deriveTestId({ scope, component, pressProp, handler, ordinal });
|
|
11595
|
+
if (usedIds.has(testId)) {
|
|
11596
|
+
let suffix = 2;
|
|
11597
|
+
while (usedIds.has(`${testId}-${suffix}`)) suffix += 1;
|
|
11598
|
+
testId = `${testId}-${suffix}`;
|
|
11599
|
+
}
|
|
11600
|
+
usedIds.add(testId);
|
|
11601
|
+
const line = element.loc?.start.line ?? 0;
|
|
11602
|
+
const nameEndLine = element.name.loc?.end.line ?? line;
|
|
11603
|
+
const firstAttr = element.attributes[0];
|
|
11604
|
+
const firstAttrLoc = firstAttr?.loc?.start;
|
|
11605
|
+
const isMultiline = !!firstAttrLoc && firstAttrLoc.line !== nameEndLine;
|
|
11606
|
+
const insertText = isMultiline ? `
|
|
11607
|
+
${" ".repeat(firstAttrLoc.column)}testID="${testId}"` : ` testID="${testId}"`;
|
|
11608
|
+
sites.push({
|
|
11609
|
+
file: file2,
|
|
11610
|
+
line,
|
|
11611
|
+
column: element.loc?.start.column ?? 0,
|
|
11612
|
+
component,
|
|
11613
|
+
role,
|
|
11614
|
+
pressProp,
|
|
11615
|
+
handler,
|
|
11616
|
+
testId,
|
|
11617
|
+
carrier,
|
|
11618
|
+
// Just past the element name — before any existing attribute, so a
|
|
11619
|
+
// multi-line element keeps its formatting and the new prop reads
|
|
11620
|
+
// first in the diff.
|
|
11621
|
+
insertAt: element.name.end ?? element.start ?? 0,
|
|
11622
|
+
insertText
|
|
11623
|
+
});
|
|
11624
|
+
}
|
|
11625
|
+
});
|
|
11626
|
+
return sites;
|
|
11627
|
+
}
|
|
11628
|
+
|
|
11629
|
+
// src/annotate/apply.ts
|
|
11630
|
+
var import_magic_string = __toESM(require("magic-string"));
|
|
11631
|
+
function planSite(site) {
|
|
11632
|
+
if (site.carrier === "rn-primitive" || site.carrier === "forwards") {
|
|
11633
|
+
return { ...site, outcome: "annotated" };
|
|
11634
|
+
}
|
|
11635
|
+
if (site.carrier === "swallows") {
|
|
11636
|
+
return {
|
|
11637
|
+
...site,
|
|
11638
|
+
outcome: "blocked-swallows",
|
|
11639
|
+
blockedReason: `<${site.component}> does not accept testID, so React would drop the prop and the agent would be told about an element that never reaches the screen. Add testID?: string to its props and pass it to the element it renders, then re-run.`
|
|
11640
|
+
};
|
|
11641
|
+
}
|
|
11642
|
+
return {
|
|
11643
|
+
...site,
|
|
11644
|
+
outcome: "blocked-unverifiable",
|
|
11645
|
+
blockedReason: `<${site.component}> comes from a package whose source is not readable from here, so whether it forwards testID cannot be checked. Annotate it by hand if it does.`
|
|
11646
|
+
};
|
|
11647
|
+
}
|
|
11648
|
+
function applyToFile(source, sites) {
|
|
11649
|
+
const planned = sites.map(planSite);
|
|
11650
|
+
const written = planned.filter((site) => site.outcome === "annotated");
|
|
11651
|
+
const blocked = planned.filter((site) => site.outcome !== "annotated");
|
|
11652
|
+
if (written.length === 0) {
|
|
11653
|
+
return { file: sites[0]?.file ?? "", content: source, written, blocked };
|
|
11654
|
+
}
|
|
11655
|
+
const magic = new import_magic_string.default(source);
|
|
11656
|
+
for (const site of written) {
|
|
11657
|
+
magic.appendLeft(site.insertAt, site.insertText);
|
|
11658
|
+
}
|
|
11659
|
+
return { file: written[0].file, content: magic.toString(), written, blocked };
|
|
11660
|
+
}
|
|
11661
|
+
|
|
11662
|
+
// src/annotate/forward.ts
|
|
11663
|
+
var import_node_fs4 = require("fs");
|
|
11664
|
+
var import_magic_string2 = __toESM(require("magic-string"));
|
|
11665
|
+
var import_traverse13 = __toESM(require("@babel/traverse"));
|
|
11666
|
+
var t14 = __toESM(require("@babel/types"));
|
|
11667
|
+
var traverse13 = import_traverse13.default.default ?? import_traverse13.default;
|
|
11668
|
+
function findComponent(ast, name) {
|
|
11669
|
+
let found;
|
|
11670
|
+
traverse13(ast, {
|
|
11671
|
+
FunctionDeclaration(nodePath) {
|
|
11672
|
+
if (nodePath.node.id?.name === name) {
|
|
11673
|
+
found ??= { params: nodePath.node.params, body: nodePath.node.body };
|
|
11674
|
+
}
|
|
11675
|
+
},
|
|
11676
|
+
VariableDeclarator(nodePath) {
|
|
11677
|
+
if (!t14.isIdentifier(nodePath.node.id) || nodePath.node.id.name !== name) return;
|
|
11678
|
+
const init = nodePath.node.init;
|
|
11679
|
+
if (t14.isArrowFunctionExpression(init) || t14.isFunctionExpression(init)) {
|
|
11680
|
+
found ??= { params: init.params, body: init.body };
|
|
11681
|
+
}
|
|
11682
|
+
}
|
|
11683
|
+
});
|
|
11684
|
+
return found;
|
|
11685
|
+
}
|
|
11686
|
+
function findReturnedRoots(body) {
|
|
11687
|
+
const roots = [];
|
|
11688
|
+
let unsupported = false;
|
|
11689
|
+
const record = (node) => {
|
|
11690
|
+
if (!node) return;
|
|
11691
|
+
if (t14.isParenthesizedExpression(node)) return record(node.expression);
|
|
11692
|
+
if (t14.isJSXElement(node)) {
|
|
11693
|
+
roots.push(node.openingElement);
|
|
11694
|
+
return;
|
|
11695
|
+
}
|
|
11696
|
+
if (t14.isConditionalExpression(node)) {
|
|
11697
|
+
record(node.consequent);
|
|
11698
|
+
record(node.alternate);
|
|
11699
|
+
return;
|
|
11700
|
+
}
|
|
11701
|
+
if (t14.isJSXFragment(node)) {
|
|
11702
|
+
unsupported = true;
|
|
11703
|
+
return;
|
|
11704
|
+
}
|
|
11705
|
+
if (t14.isNullLiteral(node)) return;
|
|
11706
|
+
unsupported = true;
|
|
11707
|
+
};
|
|
11708
|
+
if (t14.isBlockStatement(body)) {
|
|
11709
|
+
traverse13(
|
|
11710
|
+
t14.file(t14.program([t14.expressionStatement(t14.functionExpression(null, [], body))])),
|
|
11711
|
+
{
|
|
11712
|
+
ReturnStatement(nodePath) {
|
|
11713
|
+
record(nodePath.node.argument);
|
|
11714
|
+
}
|
|
11715
|
+
}
|
|
11716
|
+
);
|
|
11717
|
+
} else {
|
|
11718
|
+
record(body);
|
|
11719
|
+
}
|
|
11720
|
+
if (unsupported || roots.length === 0) return void 0;
|
|
11721
|
+
return roots;
|
|
11722
|
+
}
|
|
11723
|
+
function isCarrierRoot(element, imports) {
|
|
11724
|
+
if (!t14.isJSXIdentifier(element.name)) return false;
|
|
11725
|
+
const name = element.name.name;
|
|
11726
|
+
const origin = imports.get(name);
|
|
11727
|
+
if (origin?.source !== "react-native") return false;
|
|
11728
|
+
return RN_TESTID_CARRIERS.has(origin.imported);
|
|
11729
|
+
}
|
|
11730
|
+
function hasProp(element, name) {
|
|
11731
|
+
return element.attributes.some(
|
|
11732
|
+
(attr) => t14.isJSXAttribute(attr) && t14.isJSXIdentifier(attr.name) && attr.name.name === name
|
|
11733
|
+
);
|
|
11734
|
+
}
|
|
11735
|
+
function indentOf(source, offset) {
|
|
11736
|
+
const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
|
|
11737
|
+
const match = /^[ \t]*/.exec(source.slice(lineStart, offset));
|
|
11738
|
+
return match?.[0] ?? "";
|
|
11739
|
+
}
|
|
11740
|
+
function insertAfterLastMember(source, members, text, closingBrace, separator) {
|
|
11741
|
+
const last = members[members.length - 1];
|
|
11742
|
+
if (!last || typeof last.end !== "number" || typeof last.start !== "number") {
|
|
11743
|
+
return { at: closingBrace, text };
|
|
11744
|
+
}
|
|
11745
|
+
const trailing = /^\s*[,;]/.exec(source.slice(last.end, closingBrace));
|
|
11746
|
+
const at = last.end + (trailing?.[0].length ?? 0);
|
|
11747
|
+
const endsWithSeparator = source.slice(0, last.end).trimEnd().endsWith(separator);
|
|
11748
|
+
const lead = trailing || endsWithSeparator ? "" : separator;
|
|
11749
|
+
const openLine = source.lastIndexOf("\n", last.start);
|
|
11750
|
+
const multiline = openLine !== -1 && openLine > source.lastIndexOf("{", last.start);
|
|
11751
|
+
if (!multiline) return { at, text: `${lead} ${text}` };
|
|
11752
|
+
const indent = indentOf(source, last.start);
|
|
11753
|
+
return { at, text: `${lead}
|
|
11754
|
+
${indent}${text}` };
|
|
11755
|
+
}
|
|
11756
|
+
function followBarrel(file2, ast, component) {
|
|
11757
|
+
let target;
|
|
11758
|
+
traverse13(ast, {
|
|
11759
|
+
ExportNamedDeclaration(nodePath) {
|
|
11760
|
+
const from = nodePath.node.source?.value;
|
|
11761
|
+
if (!from) return;
|
|
11762
|
+
const named = nodePath.node.specifiers.some(
|
|
11763
|
+
(spec) => t14.isExportSpecifier(spec) && (t14.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === component
|
|
11764
|
+
);
|
|
11765
|
+
if (named) target ??= from;
|
|
11766
|
+
}
|
|
11767
|
+
});
|
|
11768
|
+
return target ? resolveRelativeImport(file2, target) : void 0;
|
|
11769
|
+
}
|
|
11770
|
+
function planForward(file2, component, depth = 0) {
|
|
11771
|
+
const base = { file: file2, component, edits: [], roots: 0 };
|
|
11772
|
+
let source;
|
|
11773
|
+
try {
|
|
11774
|
+
source = (0, import_node_fs4.readFileSync)(file2, "utf-8");
|
|
11775
|
+
} catch {
|
|
11776
|
+
return { ...base, refusal: `cannot read ${file2}` };
|
|
11777
|
+
}
|
|
11778
|
+
let ast;
|
|
11779
|
+
try {
|
|
11780
|
+
ast = parseSource(source);
|
|
11781
|
+
} catch {
|
|
11782
|
+
return { ...base, refusal: `cannot parse ${file2}` };
|
|
11783
|
+
}
|
|
11784
|
+
const found = findComponent(ast, component);
|
|
11785
|
+
if (!found) {
|
|
11786
|
+
const barrel = depth < 2 ? followBarrel(file2, ast, component) : void 0;
|
|
11787
|
+
if (barrel) return planForward(barrel, component, depth + 1);
|
|
11788
|
+
return { ...base, refusal: `no function named ${component} in this file` };
|
|
11789
|
+
}
|
|
11790
|
+
const param = found.params[0];
|
|
11791
|
+
if (!t14.isObjectPattern(param)) {
|
|
11792
|
+
return {
|
|
11793
|
+
...base,
|
|
11794
|
+
refusal: `${component} does not destructure its props, so there is no safe place to add one without knowing what the parameter is called downstream`
|
|
11795
|
+
};
|
|
11796
|
+
}
|
|
11797
|
+
const roots = findReturnedRoots(found.body);
|
|
11798
|
+
if (!roots) {
|
|
11799
|
+
return {
|
|
11800
|
+
...base,
|
|
11801
|
+
refusal: `${component} returns something other than a JSX element (a fragment, or a value this cannot follow)`
|
|
11802
|
+
};
|
|
11803
|
+
}
|
|
11804
|
+
const imports = collectImports(ast);
|
|
11805
|
+
const nonCarrier = roots.find((root) => !isCarrierRoot(root, imports));
|
|
11806
|
+
if (nonCarrier) {
|
|
11807
|
+
const name = t14.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
|
|
11808
|
+
return {
|
|
11809
|
+
...base,
|
|
11810
|
+
refusal: `${component} renders <${name}> at its root, which is not a React Native element known to accept testID. Forwarding into it would move the problem down a level, not fix it`
|
|
11811
|
+
};
|
|
11812
|
+
}
|
|
11813
|
+
const edits = [];
|
|
11814
|
+
const alreadyDestructured = param.properties.some(
|
|
11815
|
+
(prop) => t14.isObjectProperty(prop) && t14.isIdentifier(prop.key) && prop.key.name === "testID"
|
|
11816
|
+
);
|
|
11817
|
+
if (!alreadyDestructured) {
|
|
11818
|
+
const insert = insertAfterLastMember(source, param.properties, "testID", param.end ?? 0, ",");
|
|
11819
|
+
edits.push({ ...insert, what: "parameter" });
|
|
11820
|
+
}
|
|
11821
|
+
const annotation = param.typeAnnotation;
|
|
11822
|
+
if (t14.isTSTypeAnnotation(annotation)) {
|
|
11823
|
+
const declared = annotation.typeAnnotation;
|
|
11824
|
+
let body;
|
|
11825
|
+
let closing = 0;
|
|
11826
|
+
if (t14.isTSTypeLiteral(declared)) {
|
|
11827
|
+
body = declared.members;
|
|
11828
|
+
closing = declared.end ?? 0;
|
|
11829
|
+
} else if (t14.isTSTypeReference(declared) && t14.isIdentifier(declared.typeName)) {
|
|
11830
|
+
const typeName = declared.typeName.name;
|
|
11831
|
+
traverse13(ast, {
|
|
11832
|
+
TSInterfaceDeclaration(nodePath) {
|
|
11833
|
+
if (nodePath.node.id.name !== typeName) return;
|
|
11834
|
+
body = nodePath.node.body.body;
|
|
11835
|
+
closing = nodePath.node.body.end ?? 0;
|
|
11836
|
+
},
|
|
11837
|
+
TSTypeAliasDeclaration(nodePath) {
|
|
11838
|
+
if (nodePath.node.id.name !== typeName || !t14.isTSTypeLiteral(nodePath.node.typeAnnotation))
|
|
11839
|
+
return;
|
|
11840
|
+
body = nodePath.node.typeAnnotation.members;
|
|
11841
|
+
closing = nodePath.node.typeAnnotation.end ?? 0;
|
|
11842
|
+
}
|
|
11843
|
+
});
|
|
11844
|
+
if (!body) {
|
|
11845
|
+
return {
|
|
11846
|
+
...base,
|
|
11847
|
+
refusal: `${component}'s props type \`${typeName}\` is not declared in this file, so adding a field to it would edit a contract shared with code this cannot see`
|
|
11848
|
+
};
|
|
11849
|
+
}
|
|
11850
|
+
}
|
|
11851
|
+
if (body) {
|
|
11852
|
+
const declaresTestId = body.some(
|
|
11853
|
+
(member) => t14.isTSPropertySignature(member) && t14.isIdentifier(member.key) && member.key.name === "testID"
|
|
11854
|
+
);
|
|
11855
|
+
if (!declaresTestId) {
|
|
11856
|
+
const insert = insertAfterLastMember(source, body, "testID?: string;", closing, ";");
|
|
11857
|
+
edits.push({ ...insert, what: "props-type" });
|
|
11858
|
+
}
|
|
11859
|
+
}
|
|
11860
|
+
}
|
|
11861
|
+
let delivered = 0;
|
|
11862
|
+
for (const root of roots) {
|
|
11863
|
+
if (hasProp(root, "testID")) continue;
|
|
11864
|
+
edits.push({
|
|
11865
|
+
at: root.name.end ?? root.start ?? 0,
|
|
11866
|
+
text: " testID={testID}",
|
|
11867
|
+
what: "root"
|
|
11868
|
+
});
|
|
11869
|
+
delivered += 1;
|
|
11870
|
+
}
|
|
11871
|
+
if (edits.length === 0) {
|
|
11872
|
+
return { ...base, refusal: `${component} already forwards testID` };
|
|
11873
|
+
}
|
|
11874
|
+
return { file: file2, component, edits, roots: delivered };
|
|
11875
|
+
}
|
|
11876
|
+
function applyForward(source, plan) {
|
|
11877
|
+
if (plan.refusal || plan.edits.length === 0) return source;
|
|
11878
|
+
const magic = new import_magic_string2.default(source);
|
|
11879
|
+
for (const edit of plan.edits) magic.appendLeft(edit.at, edit.text);
|
|
11880
|
+
return magic.toString();
|
|
11881
|
+
}
|
|
11882
|
+
|
|
11883
|
+
// src/cli/commands/annotate.ts
|
|
11884
|
+
var DEFAULT_INCLUDE = ["src/**/*Screen.{tsx,jsx}", "src/**/screens/**/*.{tsx,jsx}"];
|
|
11885
|
+
var DEFAULT_EXCLUDE = ["**/node_modules/**", "**/dist/**", "**/*.test.*", "**/__tests__/**"];
|
|
11886
|
+
function annotateCommand() {
|
|
11887
|
+
return new import_commander5.Command("annotate").description(
|
|
11888
|
+
"Add missing testIDs to interactive elements so the agent can address them (dry run by default)"
|
|
11889
|
+
).option("--write", "Actually modify the files (default: report only)", false).option(
|
|
11890
|
+
"--forward",
|
|
11891
|
+
"Also teach a shared component to accept and pass through testID, when its call sites are blocked only by that",
|
|
11892
|
+
false
|
|
11893
|
+
).option("-i, --include <globs...>", "Glob patterns to scan", void 0).option("--json", "Emit a machine-readable report", false).option("--quiet", "Only warnings and errors", false).action(async (options) => {
|
|
11894
|
+
try {
|
|
11895
|
+
if (options.json) setLogMode("json");
|
|
11896
|
+
else if (options.quiet) setLogMode("quiet");
|
|
11897
|
+
banner();
|
|
11898
|
+
let loaded = null;
|
|
11899
|
+
try {
|
|
11900
|
+
loaded = loadConfig(warn);
|
|
11901
|
+
} catch {
|
|
11902
|
+
loaded = null;
|
|
11903
|
+
}
|
|
11904
|
+
const rootDir = process.cwd();
|
|
11905
|
+
const include = options.include ?? DEFAULT_INCLUDE;
|
|
11906
|
+
const exclude = loaded?.exclude ?? DEFAULT_EXCLUDE;
|
|
11907
|
+
const spinner = createSpinner("Scanning for unaddressable controls...");
|
|
11908
|
+
spinner.start();
|
|
11909
|
+
resetForwardingCache();
|
|
11910
|
+
const files = await (0, import_fast_glob6.default)(include, { cwd: rootDir, ignore: exclude, absolute: true });
|
|
11911
|
+
const scan = async () => {
|
|
11912
|
+
const out = [];
|
|
11913
|
+
for (const file2 of files) {
|
|
11914
|
+
const source = await (0, import_promises6.readFile)(file2, "utf-8");
|
|
11915
|
+
const sites = findAnnotationSites(source, file2);
|
|
11916
|
+
if (sites.length === 0) continue;
|
|
11917
|
+
out.push(applyToFile(source, sites));
|
|
11918
|
+
}
|
|
11919
|
+
return out;
|
|
11920
|
+
};
|
|
11921
|
+
let edits = await scan();
|
|
11922
|
+
let forwards = [];
|
|
11923
|
+
if (options.forward) {
|
|
11924
|
+
forwards = await planForwards(edits, rootDir);
|
|
11925
|
+
const applied = forwards.filter((plan) => !plan.refusal);
|
|
11926
|
+
if (options.write && applied.length > 0) {
|
|
11927
|
+
for (const plan of applied) {
|
|
11928
|
+
const before = await (0, import_promises6.readFile)(plan.file, "utf-8");
|
|
11929
|
+
await (0, import_promises6.writeFile)(plan.file, applyForward(before, plan), "utf-8");
|
|
11930
|
+
}
|
|
11931
|
+
resetForwardingCache();
|
|
11932
|
+
edits = await scan();
|
|
11933
|
+
}
|
|
11934
|
+
}
|
|
11935
|
+
spinner.stop();
|
|
11936
|
+
const written = edits.flatMap((edit) => edit.written);
|
|
11937
|
+
const blocked = edits.flatMap((edit) => edit.blocked);
|
|
11938
|
+
const touched = edits.filter((edit) => edit.written.length > 0);
|
|
11939
|
+
if (written.length === 0 && blocked.length === 0) {
|
|
11940
|
+
success("Every interactive control already has something to address it by.");
|
|
11941
|
+
if (options.json) {
|
|
11942
|
+
jsonOutput({ ok: true, scanned: files.length, annotated: 0, blocked: 0, sites: [] });
|
|
11943
|
+
}
|
|
11944
|
+
return;
|
|
11945
|
+
}
|
|
11946
|
+
if (options.write) {
|
|
11947
|
+
for (const edit of touched) {
|
|
11948
|
+
await (0, import_promises6.writeFile)(edit.file, edit.content, "utf-8");
|
|
11949
|
+
}
|
|
11950
|
+
}
|
|
11951
|
+
reportSites(rootDir, touched, blocked, options.write === true, forwards);
|
|
11952
|
+
if (options.json) {
|
|
11953
|
+
jsonOutput({
|
|
11954
|
+
ok: true,
|
|
11955
|
+
scanned: files.length,
|
|
11956
|
+
written: options.write === true,
|
|
11957
|
+
annotated: written.length,
|
|
11958
|
+
blocked: blocked.length,
|
|
11959
|
+
files: touched.map((edit) => import_node_path7.default.relative(rootDir, edit.file)),
|
|
11960
|
+
sites: [...written, ...blocked].map((site) => ({
|
|
11961
|
+
file: import_node_path7.default.relative(rootDir, site.file),
|
|
11962
|
+
line: site.line,
|
|
11963
|
+
component: site.component,
|
|
11964
|
+
role: site.role,
|
|
11965
|
+
testId: site.testId,
|
|
11966
|
+
outcome: site.outcome,
|
|
11967
|
+
reason: site.blockedReason
|
|
11968
|
+
}))
|
|
11969
|
+
});
|
|
11970
|
+
}
|
|
11971
|
+
} catch (err) {
|
|
11972
|
+
error(`Annotate failed: ${stringifyUnknown(err)}`);
|
|
11973
|
+
process.exit(1);
|
|
11974
|
+
}
|
|
11975
|
+
});
|
|
11976
|
+
}
|
|
11977
|
+
async function planForwards(edits, rootDir) {
|
|
11978
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11979
|
+
const plans = [];
|
|
11980
|
+
for (const edit of edits) {
|
|
11981
|
+
for (const site of edit.blocked) {
|
|
11982
|
+
if (site.outcome !== "blocked-swallows") continue;
|
|
11983
|
+
if (seen.has(site.component)) continue;
|
|
11984
|
+
seen.add(site.component);
|
|
11985
|
+
const source = await (0, import_promises6.readFile)(site.file, "utf-8");
|
|
11986
|
+
const origin = findImportSource(source, site.component);
|
|
11987
|
+
const target = origin ? resolveRelativeImport(site.file, origin) : site.file;
|
|
11988
|
+
if (!target) {
|
|
11989
|
+
plans.push({
|
|
11990
|
+
file: site.file,
|
|
11991
|
+
component: site.component,
|
|
11992
|
+
edits: [],
|
|
11993
|
+
roots: 0,
|
|
11994
|
+
refusal: `cannot locate the source of <${site.component}>`
|
|
11995
|
+
});
|
|
11996
|
+
continue;
|
|
11997
|
+
}
|
|
11998
|
+
plans.push(planForward(target, site.component));
|
|
11999
|
+
}
|
|
12000
|
+
}
|
|
12001
|
+
void rootDir;
|
|
12002
|
+
return plans;
|
|
12003
|
+
}
|
|
12004
|
+
function findImportSource(source, name) {
|
|
12005
|
+
const pattern = new RegExp(
|
|
12006
|
+
String.raw`import\s*\{[^}]*\b${name}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
|
|
12007
|
+
);
|
|
12008
|
+
return pattern.exec(source)?.[1];
|
|
12009
|
+
}
|
|
12010
|
+
function reportSites(rootDir, touched, blocked, didWrite, forwards) {
|
|
12011
|
+
const written = touched.flatMap((edit) => edit.written);
|
|
12012
|
+
if (written.length > 0) {
|
|
12013
|
+
heading(didWrite ? "Added" : "Would add");
|
|
12014
|
+
for (const edit of touched) {
|
|
12015
|
+
info(import_node_path7.default.relative(rootDir, edit.file));
|
|
12016
|
+
for (const site of edit.written) {
|
|
12017
|
+
dim(` ${String(site.line).padStart(4)} <${site.component}> testID="${site.testId}"`);
|
|
12018
|
+
}
|
|
12019
|
+
}
|
|
12020
|
+
}
|
|
12021
|
+
const teachable = new Map(
|
|
12022
|
+
forwards.filter((plan) => !plan.refusal).map((plan) => [plan.component, plan])
|
|
12023
|
+
);
|
|
12024
|
+
if (teachable.size > 0) {
|
|
12025
|
+
heading(didWrite ? "Taught to carry testID" : "Would teach to carry testID");
|
|
12026
|
+
for (const plan of teachable.values()) {
|
|
12027
|
+
info(`<${plan.component}> ${import_node_path7.default.relative(rootDir, plan.file)}`);
|
|
12028
|
+
for (const edit of plan.edits) {
|
|
12029
|
+
dim(` ${edit.what}: ${edit.text.trim()}`);
|
|
12030
|
+
}
|
|
12031
|
+
}
|
|
12032
|
+
}
|
|
12033
|
+
if (blocked.length > 0) {
|
|
12034
|
+
heading("Skipped \u2014 the prop would not survive to the runtime");
|
|
12035
|
+
const byComponent = /* @__PURE__ */ new Map();
|
|
12036
|
+
for (const site of blocked) {
|
|
12037
|
+
const group = byComponent.get(site.component) ?? [];
|
|
12038
|
+
group.push(site);
|
|
12039
|
+
byComponent.set(site.component, group);
|
|
12040
|
+
}
|
|
12041
|
+
for (const [component, group] of byComponent) {
|
|
12042
|
+
if (teachable.has(component)) continue;
|
|
12043
|
+
warn(`<${component}> \u2014 ${group.length} site${group.length === 1 ? "" : "s"}`);
|
|
12044
|
+
const refusal = forwards.find((plan) => plan.component === component)?.refusal;
|
|
12045
|
+
dim(` ${group[0].blockedReason}`);
|
|
12046
|
+
if (refusal) dim(` --forward cannot do it either: ${refusal}`);
|
|
12047
|
+
}
|
|
12048
|
+
}
|
|
12049
|
+
if (!didWrite && written.length > 0) {
|
|
12050
|
+
info("");
|
|
12051
|
+
info(`Nothing was written. Re-run with --write to apply ${written.length} change(s).`);
|
|
12052
|
+
} else if (didWrite) {
|
|
12053
|
+
success(`Wrote ${written.length} testID(s) across ${touched.length} file(s).`);
|
|
12054
|
+
info("Re-run `appilots generate` so the document picks them up.");
|
|
12055
|
+
}
|
|
12056
|
+
}
|
|
12057
|
+
|
|
12058
|
+
// src/cli/commands/status.ts
|
|
12059
|
+
var import_commander6 = require("commander");
|
|
10483
12060
|
function statusCommand() {
|
|
10484
|
-
return new
|
|
12061
|
+
return new import_commander6.Command("status").description("Show current project and MCP document status").option("--json", "Output a machine-readable JSON result (implies --quiet)", false).option("--quiet", "Suppress banner and informational output", false).action(async (options) => {
|
|
10485
12062
|
if (options.json) setLogMode("json");
|
|
10486
12063
|
else if (options.quiet) setLogMode("quiet");
|
|
10487
12064
|
try {
|
|
10488
12065
|
banner();
|
|
10489
|
-
const config = loadConfig();
|
|
12066
|
+
const config = loadConfig(warn);
|
|
10490
12067
|
if (!config) {
|
|
10491
12068
|
error("No .appilotsrc configuration found");
|
|
10492
12069
|
info("Run: appilots init --api-key <your-key>");
|
|
@@ -10513,8 +12090,13 @@ function statusCommand() {
|
|
|
10513
12090
|
]);
|
|
10514
12091
|
if (status.project) {
|
|
10515
12092
|
heading("\n\u{1F4CC} Project Info:");
|
|
10516
|
-
table([
|
|
12093
|
+
table([
|
|
12094
|
+
["Name", status.project.name],
|
|
12095
|
+
["ID", status.project.id]
|
|
12096
|
+
]);
|
|
10517
12097
|
}
|
|
12098
|
+
const outputDir = config.outputDir || ".appilots";
|
|
12099
|
+
let drift = null;
|
|
10518
12100
|
if (status.activeMcp) {
|
|
10519
12101
|
heading("\n\u2705 Active MCP Document:");
|
|
10520
12102
|
table([
|
|
@@ -10524,6 +12106,14 @@ function statusCommand() {
|
|
|
10524
12106
|
["Forms", String(status.activeMcp.formsCount)],
|
|
10525
12107
|
["Actions", String(status.activeMcp.actionsCount)]
|
|
10526
12108
|
]);
|
|
12109
|
+
const local = readLocalDocumentSummary(outputDir);
|
|
12110
|
+
if (local) {
|
|
12111
|
+
drift = compareWithRemote(local, status.activeMcp);
|
|
12112
|
+
if (drift) {
|
|
12113
|
+
heading("");
|
|
12114
|
+
reportRemoteDrift(drift);
|
|
12115
|
+
}
|
|
12116
|
+
}
|
|
10527
12117
|
} else {
|
|
10528
12118
|
heading("\n\u26A0\uFE0F No active MCP document");
|
|
10529
12119
|
info("Run: appilots sync");
|
|
@@ -10534,21 +12124,22 @@ function statusCommand() {
|
|
|
10534
12124
|
local: {
|
|
10535
12125
|
serverUrl: config.serverUrl,
|
|
10536
12126
|
projectId: config.projectId || null,
|
|
10537
|
-
outputDir
|
|
12127
|
+
outputDir
|
|
10538
12128
|
},
|
|
10539
12129
|
project: status.project ?? null,
|
|
10540
|
-
activeMcp: status.activeMcp ?? null
|
|
12130
|
+
activeMcp: status.activeMcp ?? null,
|
|
12131
|
+
drift
|
|
10541
12132
|
});
|
|
10542
12133
|
} catch (err) {
|
|
10543
|
-
error(`Status check failed: ${
|
|
10544
|
-
jsonOutput({ success: false, error:
|
|
12134
|
+
error(`Status check failed: ${stringifyUnknown(err)}`);
|
|
12135
|
+
jsonOutput({ success: false, error: stringifyUnknown(err) });
|
|
10545
12136
|
process.exit(1);
|
|
10546
12137
|
}
|
|
10547
12138
|
});
|
|
10548
12139
|
}
|
|
10549
12140
|
|
|
10550
12141
|
// src/cli/commands/eval.ts
|
|
10551
|
-
var
|
|
12142
|
+
var import_commander7 = require("commander");
|
|
10552
12143
|
var import_fs10 = require("fs");
|
|
10553
12144
|
var import_path9 = require("path");
|
|
10554
12145
|
|
|
@@ -10578,21 +12169,21 @@ function loadScenarios(dir) {
|
|
|
10578
12169
|
} catch {
|
|
10579
12170
|
return { scenarios, errors };
|
|
10580
12171
|
}
|
|
10581
|
-
for (const
|
|
10582
|
-
const full = (0, import_path7.join)(dir,
|
|
12172
|
+
for (const file2 of files) {
|
|
12173
|
+
const full = (0, import_path7.join)(dir, file2);
|
|
10583
12174
|
let raw;
|
|
10584
12175
|
try {
|
|
10585
12176
|
raw = JSON.parse((0, import_fs8.readFileSync)(full, "utf-8"));
|
|
10586
12177
|
} catch (err) {
|
|
10587
|
-
errors.push({ file, error: `invalid JSON: ${
|
|
12178
|
+
errors.push({ file: file2, error: `invalid JSON: ${stringifyUnknown(err)}` });
|
|
10588
12179
|
continue;
|
|
10589
12180
|
}
|
|
10590
12181
|
const shapeError = validateScenarioShape(raw);
|
|
10591
12182
|
if (shapeError) {
|
|
10592
|
-
errors.push({ file, error: shapeError });
|
|
12183
|
+
errors.push({ file: file2, error: shapeError });
|
|
10593
12184
|
continue;
|
|
10594
12185
|
}
|
|
10595
|
-
scenarios.push({ file, scenario: raw });
|
|
12186
|
+
scenarios.push({ file: file2, scenario: raw });
|
|
10596
12187
|
}
|
|
10597
12188
|
return { scenarios, errors };
|
|
10598
12189
|
}
|
|
@@ -10741,10 +12332,10 @@ function matchActions(expected, actual, reply = "") {
|
|
|
10741
12332
|
var import_fs9 = require("fs");
|
|
10742
12333
|
var import_path8 = require("path");
|
|
10743
12334
|
var EMPTY_BASELINE = { generatedAt: null, gitSha: null, perScenario: {} };
|
|
10744
|
-
function loadBaseline(
|
|
10745
|
-
if (!(0, import_fs9.existsSync)(
|
|
12335
|
+
function loadBaseline(path11) {
|
|
12336
|
+
if (!(0, import_fs9.existsSync)(path11)) return { ...EMPTY_BASELINE, perScenario: {} };
|
|
10746
12337
|
try {
|
|
10747
|
-
const raw = JSON.parse((0, import_fs9.readFileSync)(
|
|
12338
|
+
const raw = JSON.parse((0, import_fs9.readFileSync)(path11, "utf-8"));
|
|
10748
12339
|
return {
|
|
10749
12340
|
generatedAt: raw.generatedAt ?? null,
|
|
10750
12341
|
gitSha: raw.gitSha ?? null,
|
|
@@ -10754,9 +12345,9 @@ function loadBaseline(path9) {
|
|
|
10754
12345
|
return { ...EMPTY_BASELINE, perScenario: {} };
|
|
10755
12346
|
}
|
|
10756
12347
|
}
|
|
10757
|
-
function saveBaseline(
|
|
10758
|
-
(0, import_fs9.mkdirSync)((0, import_path8.dirname)(
|
|
10759
|
-
(0, import_fs9.writeFileSync)(
|
|
12348
|
+
function saveBaseline(path11, baseline) {
|
|
12349
|
+
(0, import_fs9.mkdirSync)((0, import_path8.dirname)(path11), { recursive: true });
|
|
12350
|
+
(0, import_fs9.writeFileSync)(path11, `${JSON.stringify(baseline, null, 2)}
|
|
10760
12351
|
`, "utf-8");
|
|
10761
12352
|
}
|
|
10762
12353
|
function buildBaseline(outcomes, meta) {
|
|
@@ -10838,13 +12429,16 @@ function parseRate(raw, fallback, flagName) {
|
|
|
10838
12429
|
return n;
|
|
10839
12430
|
}
|
|
10840
12431
|
function evalCommand() {
|
|
10841
|
-
return new
|
|
12432
|
+
return new import_commander7.Command("eval").description(
|
|
10842
12433
|
"Run agent eval scenarios against the current MCP document in dry-run mode and check for regressions vs a committed baseline"
|
|
10843
12434
|
).option("--init", "Scaffold an example scenario in the scenarios directory", false).option("--force", "With --init, overwrite an existing example scenario", false).option("--dir <path>", "Scenarios directory (default: <outputDir>/scenarios)").option("--baseline <path>", "Baseline file path (default: <outputDir>/eval-baseline.json)").option(
|
|
10844
12435
|
"--update-baseline",
|
|
10845
12436
|
"Record this run as the new baseline (refuses if pass rate is below the threshold)",
|
|
10846
12437
|
false
|
|
10847
|
-
).option(
|
|
12438
|
+
).option(
|
|
12439
|
+
"--min-pass-rate <n>",
|
|
12440
|
+
"Minimum fraction of scenarios that must pass, 0-1 (default: 0.95)"
|
|
12441
|
+
).option(
|
|
10848
12442
|
"--max-token-regression <n>",
|
|
10849
12443
|
"Max fractional token increase before a passing scenario is flagged, e.g. 0.25 for +25% (default: 0.25)"
|
|
10850
12444
|
).option("--model <id>", "Model override for scenarios that do not set their own modelOverride").option("--api-key <key>", "Appilots API key (overrides .appilotsrc and APPILOTS_API_KEY)").option("--project-id <id>", "Project ID (overrides .appilotsrc and APPILOTS_PROJECT_ID)").option("--server <url>", "Appilots server URL (overrides .appilotsrc and APPILOTS_SERVER_URL)").option("--json", "Output a machine-readable JSON result (implies --quiet)", false).option("--quiet", "Suppress banner and informational output", false).action(async (options) => {
|
|
@@ -10852,7 +12446,7 @@ function evalCommand() {
|
|
|
10852
12446
|
else if (options.quiet) setLogMode("quiet");
|
|
10853
12447
|
try {
|
|
10854
12448
|
banner();
|
|
10855
|
-
const loaded = loadConfig();
|
|
12449
|
+
const loaded = loadConfig(warn);
|
|
10856
12450
|
const outputDir = loaded?.outputDir || ".appilots";
|
|
10857
12451
|
const scenariosDir = options.dir || loaded?.eval?.scenariosDir || (0, import_path9.join)(outputDir, "scenarios");
|
|
10858
12452
|
if (options.init) {
|
|
@@ -10881,7 +12475,7 @@ function evalCommand() {
|
|
|
10881
12475
|
return;
|
|
10882
12476
|
}
|
|
10883
12477
|
const config = {
|
|
10884
|
-
serverUrl:
|
|
12478
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
10885
12479
|
...loaded,
|
|
10886
12480
|
apiKey,
|
|
10887
12481
|
...options.projectId ? { projectId: options.projectId } : {},
|
|
@@ -10927,11 +12521,17 @@ function evalCommand() {
|
|
|
10927
12521
|
error(
|
|
10928
12522
|
`Duplicate scenario name(s) \u2014 "name" must be unique across all scenario files: ${duplicates.join(", ")}`
|
|
10929
12523
|
);
|
|
10930
|
-
jsonOutput({
|
|
12524
|
+
jsonOutput({
|
|
12525
|
+
success: false,
|
|
12526
|
+
error: `Duplicate scenario names: ${duplicates.join(", ")}`
|
|
12527
|
+
});
|
|
10931
12528
|
process.exit(1);
|
|
10932
12529
|
return;
|
|
10933
12530
|
}
|
|
10934
|
-
const apiClient = new AppilotsAPIClient({
|
|
12531
|
+
const apiClient = new AppilotsAPIClient({
|
|
12532
|
+
serverUrl: config.serverUrl,
|
|
12533
|
+
apiKey: config.apiKey
|
|
12534
|
+
});
|
|
10935
12535
|
const requests = loadedScenarios.scenarios.map(({ scenario }) => ({
|
|
10936
12536
|
id: scenario.name,
|
|
10937
12537
|
screen: scenario.given?.screen,
|
|
@@ -11033,8 +12633,8 @@ Pass rate: ${passed}/${total} (${passRatePct}%) \u2014 threshold ${minPassRatePc
|
|
|
11033
12633
|
success("Eval passed");
|
|
11034
12634
|
}
|
|
11035
12635
|
} catch (err) {
|
|
11036
|
-
error(`Eval failed: ${
|
|
11037
|
-
jsonOutput({ success: false, error:
|
|
12636
|
+
error(`Eval failed: ${stringifyUnknown(err)}`);
|
|
12637
|
+
jsonOutput({ success: false, error: stringifyUnknown(err) });
|
|
11038
12638
|
process.exit(1);
|
|
11039
12639
|
}
|
|
11040
12640
|
});
|
|
@@ -11042,16 +12642,17 @@ Pass rate: ${passed}/${total} (${passRatePct}%) \u2014 threshold ${minPassRatePc
|
|
|
11042
12642
|
|
|
11043
12643
|
// src/cli/index.ts
|
|
11044
12644
|
async function main() {
|
|
11045
|
-
const
|
|
11046
|
-
|
|
12645
|
+
const program2 = new import_commander8.Command();
|
|
12646
|
+
program2.name("appilots").description(
|
|
11047
12647
|
"Analyze your React Native app and generate MCP documents for AI-powered navigation.\n\nQuick start:\n $ appilots init --api-key ak_xxx\n $ appilots sync\n $ appilots watch"
|
|
11048
|
-
).version(
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
12648
|
+
).version(CLI_VERSION).option("--verbose", "Enable verbose logging");
|
|
12649
|
+
program2.addCommand(initCommand());
|
|
12650
|
+
program2.addCommand(syncCommand());
|
|
12651
|
+
program2.addCommand(watchCommand());
|
|
12652
|
+
program2.addCommand(generateCommand());
|
|
12653
|
+
program2.addCommand(annotateCommand());
|
|
12654
|
+
program2.addCommand(statusCommand());
|
|
12655
|
+
program2.addCommand(evalCommand());
|
|
11055
12656
|
const args = process.argv.slice(2);
|
|
11056
12657
|
const firstArg = args[0];
|
|
11057
12658
|
const hasCommand = firstArg !== void 0 && args.length > 0 && !firstArg.startsWith("-");
|
|
@@ -11064,14 +12665,14 @@ async function main() {
|
|
|
11064
12665
|
info("Welcome to Appilots! Get started with:");
|
|
11065
12666
|
info(" appilots init --api-key <your-ak_key>");
|
|
11066
12667
|
console.log("");
|
|
11067
|
-
|
|
12668
|
+
program2.outputHelp();
|
|
11068
12669
|
return;
|
|
11069
12670
|
}
|
|
11070
12671
|
}
|
|
11071
|
-
await
|
|
12672
|
+
await program2.parseAsync(process.argv);
|
|
11072
12673
|
}
|
|
11073
12674
|
main().catch((err) => {
|
|
11074
|
-
error(
|
|
12675
|
+
error(stringifyUnknown(err));
|
|
11075
12676
|
process.exit(1);
|
|
11076
12677
|
});
|
|
11077
12678
|
//# sourceMappingURL=index.js.map
|