@appilots/cli 0.4.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1932 -298
- 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 +829 -151
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +829 -152
- 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.11.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,27 @@ 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
|
}
|
|
130
|
+
const legacyWarnings = [];
|
|
131
|
+
if (fileConfig.serverUrl === void 0) {
|
|
132
|
+
for (const [alias, canonical] of Object.entries(KEY_ALIASES)) {
|
|
133
|
+
if (canonical !== "serverUrl" || fileConfig[alias] === void 0) continue;
|
|
134
|
+
fileConfig.serverUrl = fileConfig[alias];
|
|
135
|
+
delete fileConfig[alias];
|
|
136
|
+
legacyWarnings.push(
|
|
137
|
+
`legacy key "${alias}" \u2014 using it as "serverUrl"; rename it in .appilotsrc`
|
|
138
|
+
);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
61
142
|
const merged = {
|
|
62
|
-
serverUrl:
|
|
143
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
63
144
|
outputDir: ".appilots",
|
|
64
145
|
autoActivate: true,
|
|
65
146
|
strictScreens: true,
|
|
@@ -69,6 +150,26 @@ function loadConfig() {
|
|
|
69
150
|
...env.serverUrl ? { serverUrl: env.serverUrl } : {}
|
|
70
151
|
};
|
|
71
152
|
const validation = validateConfig(merged);
|
|
153
|
+
validation.warnings.unshift(...legacyWarnings);
|
|
154
|
+
if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
|
|
155
|
+
for (const key of Object.keys(fileConfig)) {
|
|
156
|
+
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
157
|
+
if (suggestConfigKey(key) !== "serverUrl") continue;
|
|
158
|
+
validation.warnings = validation.warnings.filter(
|
|
159
|
+
(warning) => !warning.startsWith(`unknown key "${key}"`)
|
|
160
|
+
);
|
|
161
|
+
validation.errors.push(
|
|
162
|
+
`unknown key "${key}" looks like "serverUrl" \u2014 not falling back to ${DEFAULT_SERVER_URL}; rename the key to "serverUrl"`
|
|
163
|
+
);
|
|
164
|
+
validation.valid = false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (onWarn) {
|
|
168
|
+
const source = configPath ?? "environment variables";
|
|
169
|
+
for (const warning of validation.warnings) {
|
|
170
|
+
onWarn(`${source}: ${warning}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
72
173
|
if (!validation.valid) {
|
|
73
174
|
const source = configPath ?? "environment variables";
|
|
74
175
|
throw new Error(
|
|
@@ -84,7 +185,7 @@ function saveConfig(dir, config) {
|
|
|
84
185
|
const mergedConfig = {
|
|
85
186
|
apiKey: config.apiKey || existingConfig?.apiKey || "",
|
|
86
187
|
projectId: config.projectId || existingConfig?.projectId || "",
|
|
87
|
-
serverUrl: config.serverUrl || existingConfig?.serverUrl ||
|
|
188
|
+
serverUrl: config.serverUrl || existingConfig?.serverUrl || DEFAULT_SERVER_URL,
|
|
88
189
|
outputDir: config.outputDir || existingConfig?.outputDir || ".appilots",
|
|
89
190
|
autoActivate: config.autoActivate !== void 0 ? config.autoActivate : existingConfig?.autoActivate ?? true,
|
|
90
191
|
include: config.include || existingConfig?.include,
|
|
@@ -100,7 +201,9 @@ function saveConfig(dir, config) {
|
|
|
100
201
|
try {
|
|
101
202
|
(0, import_fs.writeFileSync)(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
|
|
102
203
|
} catch (error2) {
|
|
103
|
-
throw new Error(
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Failed to write .appilotsrc to ${configPath}: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
206
|
+
);
|
|
104
207
|
}
|
|
105
208
|
}
|
|
106
209
|
function getConfigPath() {
|
|
@@ -127,14 +230,25 @@ function getConfigPath() {
|
|
|
127
230
|
}
|
|
128
231
|
function validateConfig(config) {
|
|
129
232
|
const errors = [];
|
|
233
|
+
const warnings = [];
|
|
130
234
|
if (!config || typeof config !== "object") {
|
|
131
235
|
return {
|
|
132
236
|
valid: false,
|
|
133
|
-
errors: ["Configuration must be an object"]
|
|
237
|
+
errors: ["Configuration must be an object"],
|
|
238
|
+
warnings
|
|
134
239
|
};
|
|
135
240
|
}
|
|
241
|
+
for (const key of Object.keys(config)) {
|
|
242
|
+
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
243
|
+
const suggestion = suggestConfigKey(key);
|
|
244
|
+
warnings.push(
|
|
245
|
+
`unknown key "${key}" \u2014 ignored` + (suggestion ? ` (did you mean "${suggestion}"?)` : "")
|
|
246
|
+
);
|
|
247
|
+
}
|
|
136
248
|
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
137
|
-
errors.push(
|
|
249
|
+
errors.push(
|
|
250
|
+
"apiKey is required and must be a string (set it in .appilotsrc or via APPILOTS_API_KEY)"
|
|
251
|
+
);
|
|
138
252
|
} else if (!config.apiKey.startsWith("ak_")) {
|
|
139
253
|
errors.push('apiKey must start with "ak_"');
|
|
140
254
|
}
|
|
@@ -196,7 +310,8 @@ function validateConfig(config) {
|
|
|
196
310
|
}
|
|
197
311
|
return {
|
|
198
312
|
valid: errors.length === 0,
|
|
199
|
-
errors
|
|
313
|
+
errors,
|
|
314
|
+
warnings
|
|
200
315
|
};
|
|
201
316
|
}
|
|
202
317
|
|
|
@@ -241,14 +356,23 @@ function dim(msg) {
|
|
|
241
356
|
if (mode !== "pretty") return;
|
|
242
357
|
console.log(import_chalk.default.dim(msg));
|
|
243
358
|
}
|
|
359
|
+
var BANNER_WIDTH = 39;
|
|
360
|
+
function bannerRow(plain, colored) {
|
|
361
|
+
return ` ${import_chalk.default.cyan("\u2551")} ${colored}${" ".repeat(Math.max(0, BANNER_WIDTH - 5 - plain.length))}${import_chalk.default.cyan("\u2551")}`;
|
|
362
|
+
}
|
|
244
363
|
function banner() {
|
|
245
364
|
if (mode !== "pretty") return;
|
|
246
|
-
const
|
|
247
|
-
${
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
365
|
+
const title = "Appilots MCP Generator";
|
|
366
|
+
const version = `v${CLI_VERSION}`;
|
|
367
|
+
const border = "\u2550".repeat(BANNER_WIDTH);
|
|
368
|
+
const logo = [
|
|
369
|
+
"",
|
|
370
|
+
` ${import_chalk.default.cyan(`\u2554${border}\u2557`)}`,
|
|
371
|
+
bannerRow(title, import_chalk.default.bold.cyan(title)),
|
|
372
|
+
bannerRow(version, import_chalk.default.dim(version)),
|
|
373
|
+
` ${import_chalk.default.cyan(`\u255A${border}\u255D`)}`,
|
|
374
|
+
" "
|
|
375
|
+
].join("\n");
|
|
252
376
|
console.log(logo);
|
|
253
377
|
}
|
|
254
378
|
function heading(msg) {
|
|
@@ -269,19 +393,105 @@ function table(rows) {
|
|
|
269
393
|
// src/cli/commands/init.ts
|
|
270
394
|
var import_commander = require("commander");
|
|
271
395
|
|
|
396
|
+
// ../shared/dist/chunk-UEMUBXFH.mjs
|
|
397
|
+
var DEFAULT_API_BASE_URL = "https://api.appilots.com";
|
|
398
|
+
var APPILOTS_API_PATH_PREFIX = "/api/v1";
|
|
399
|
+
function normalizeApiBaseUrl(configured) {
|
|
400
|
+
const raw = (configured ?? DEFAULT_API_BASE_URL).trim().replace(/\/+$/, "");
|
|
401
|
+
if (!raw) return `${DEFAULT_API_BASE_URL}${APPILOTS_API_PATH_PREFIX}`;
|
|
402
|
+
if (new RegExp(`${APPILOTS_API_PATH_PREFIX}(/|$)`).test(raw)) return raw;
|
|
403
|
+
return `${raw}${APPILOTS_API_PATH_PREFIX}`;
|
|
404
|
+
}
|
|
405
|
+
var LIMITS = {
|
|
406
|
+
MAX_MCP_DOCUMENT_SIZE_MB: 10,
|
|
407
|
+
MAX_PROJECTS_PER_USER: 20,
|
|
408
|
+
MAX_MESSAGE_LENGTH: 4096,
|
|
409
|
+
MAX_ACTIONS_PER_MESSAGE: 10,
|
|
410
|
+
SESSION_TIMEOUT_MS: 30 * 60 * 1e3
|
|
411
|
+
// 30 minutes
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// src/utils/stringify.ts
|
|
415
|
+
function stringifyUnknown(value) {
|
|
416
|
+
if (value instanceof Error) {
|
|
417
|
+
const message = value.message || value.name;
|
|
418
|
+
const cause = value.cause;
|
|
419
|
+
if (cause instanceof Error && cause.message && cause.message !== message) {
|
|
420
|
+
return `${message} (${cause.message})`;
|
|
421
|
+
}
|
|
422
|
+
return message;
|
|
423
|
+
}
|
|
424
|
+
if (typeof value === "string") return value;
|
|
425
|
+
if (value === null || value === void 0) return String(value);
|
|
426
|
+
if (typeof value !== "object") return String(value);
|
|
427
|
+
try {
|
|
428
|
+
const json = JSON.stringify(value);
|
|
429
|
+
if (json && json !== "{}") return json;
|
|
430
|
+
} catch {
|
|
431
|
+
}
|
|
432
|
+
return String(value);
|
|
433
|
+
}
|
|
434
|
+
|
|
272
435
|
// src/services/api-client.ts
|
|
273
|
-
var
|
|
436
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
274
437
|
var DEFAULT_MAX_RETRIES = 2;
|
|
275
438
|
var RETRY_BASE_DELAY_MS = 500;
|
|
439
|
+
function describeNetworkError(url, err) {
|
|
440
|
+
let origin = url;
|
|
441
|
+
try {
|
|
442
|
+
origin = new URL(url).origin;
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
const cause = err?.cause;
|
|
446
|
+
const code = typeof cause?.code === "string" ? cause.code : void 0;
|
|
447
|
+
const detail = code ?? stringifyUnknown(err);
|
|
448
|
+
return new Error(`Cannot reach Appilots server at ${origin} (${detail})`);
|
|
449
|
+
}
|
|
450
|
+
function errorCodeOf(payload) {
|
|
451
|
+
const error2 = payload?.error;
|
|
452
|
+
if (error2 && typeof error2 === "object") {
|
|
453
|
+
const { code } = error2;
|
|
454
|
+
if (typeof code === "string" && code.trim()) return code;
|
|
455
|
+
}
|
|
456
|
+
return void 0;
|
|
457
|
+
}
|
|
458
|
+
function describeApiError(payload, fallback) {
|
|
459
|
+
const error2 = payload?.error;
|
|
460
|
+
if (typeof error2 === "string" && error2.trim()) return error2;
|
|
461
|
+
if (error2 && typeof error2 === "object") {
|
|
462
|
+
const { code, message: message2 } = error2;
|
|
463
|
+
const text = typeof message2 === "string" && message2.trim() ? message2 : void 0;
|
|
464
|
+
const tag = typeof code === "string" && code.trim() ? code : void 0;
|
|
465
|
+
if (text && tag) return `${text} (${tag})`;
|
|
466
|
+
if (text) return text;
|
|
467
|
+
if (tag) return tag;
|
|
468
|
+
}
|
|
469
|
+
const message = payload?.message;
|
|
470
|
+
if (typeof message === "string" && message.trim()) return message;
|
|
471
|
+
return fallback;
|
|
472
|
+
}
|
|
276
473
|
var AppilotsAPIClient = class {
|
|
277
|
-
|
|
474
|
+
/**
|
|
475
|
+
* `serverUrl` with the API's mount prefix resolved — every path below
|
|
476
|
+
* is relative to THIS, not to the configured origin.
|
|
477
|
+
*
|
|
478
|
+
* It used to be the raw `serverUrl` with `/api/v1` hardcoded into each
|
|
479
|
+
* path, which made the field mean something different here than in the
|
|
480
|
+
* SDK. Both read the same `.appilotsrc`: the SDK completes the prefix
|
|
481
|
+
* when it is missing, so `https://api.appilots.com/api/v1` is correct
|
|
482
|
+
* there — and here that same value produced
|
|
483
|
+
* `/api/v1/api/v1/cli/sync`, a 404 whose body reads `Route not found`.
|
|
484
|
+
* Sharing `normalizeApiBaseUrl` is what makes one file mean one thing:
|
|
485
|
+
* with or without the prefix now works in both.
|
|
486
|
+
*/
|
|
487
|
+
baseUrl;
|
|
278
488
|
apiKey;
|
|
279
489
|
timeoutMs;
|
|
280
490
|
maxRetries;
|
|
281
491
|
constructor(config) {
|
|
282
|
-
this.
|
|
492
|
+
this.baseUrl = normalizeApiBaseUrl(config.serverUrl);
|
|
283
493
|
this.apiKey = config.apiKey;
|
|
284
|
-
this.timeoutMs = config.timeoutMs ??
|
|
494
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
285
495
|
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
286
496
|
}
|
|
287
497
|
/**
|
|
@@ -305,7 +515,7 @@ var AppilotsAPIClient = class {
|
|
|
305
515
|
}
|
|
306
516
|
return response;
|
|
307
517
|
} catch (err) {
|
|
308
|
-
lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : err;
|
|
518
|
+
lastError = controller.signal.aborted ? new Error(`Request timed out after ${this.timeoutMs}ms: ${url}`) : describeNetworkError(url, err);
|
|
309
519
|
} finally {
|
|
310
520
|
clearTimeout(timer);
|
|
311
521
|
}
|
|
@@ -324,7 +534,7 @@ var AppilotsAPIClient = class {
|
|
|
324
534
|
*/
|
|
325
535
|
async sync(content, version, appVersion) {
|
|
326
536
|
try {
|
|
327
|
-
const response = await this.request(`${this.
|
|
537
|
+
const response = await this.request(`${this.baseUrl}/cli/sync`, {
|
|
328
538
|
method: "POST",
|
|
329
539
|
headers: {
|
|
330
540
|
"Content-Type": "application/json",
|
|
@@ -338,7 +548,9 @@ var AppilotsAPIClient = class {
|
|
|
338
548
|
return {
|
|
339
549
|
success: false,
|
|
340
550
|
unchanged: false,
|
|
341
|
-
error: errorData
|
|
551
|
+
error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`),
|
|
552
|
+
errorCode: errorCodeOf(errorData),
|
|
553
|
+
errorStatus: response.status
|
|
342
554
|
};
|
|
343
555
|
}
|
|
344
556
|
const json = await response.json();
|
|
@@ -351,7 +563,8 @@ var AppilotsAPIClient = class {
|
|
|
351
563
|
return {
|
|
352
564
|
success: false,
|
|
353
565
|
unchanged: false,
|
|
354
|
-
error: error2 instanceof Error ? error2.message : "Failed to sync with Appilots API"
|
|
566
|
+
error: error2 instanceof Error ? error2.message : "Failed to sync with Appilots API",
|
|
567
|
+
errorStatus: 0
|
|
355
568
|
};
|
|
356
569
|
}
|
|
357
570
|
}
|
|
@@ -362,7 +575,7 @@ var AppilotsAPIClient = class {
|
|
|
362
575
|
*/
|
|
363
576
|
async status() {
|
|
364
577
|
try {
|
|
365
|
-
const response = await this.request(`${this.
|
|
578
|
+
const response = await this.request(`${this.baseUrl}/cli/status`, {
|
|
366
579
|
method: "GET",
|
|
367
580
|
headers: {
|
|
368
581
|
Authorization: `Bearer ${this.apiKey}`
|
|
@@ -371,7 +584,7 @@ var AppilotsAPIClient = class {
|
|
|
371
584
|
if (!response.ok) {
|
|
372
585
|
const errorData = await response.json().catch(() => ({}));
|
|
373
586
|
return {
|
|
374
|
-
error: errorData
|
|
587
|
+
error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
|
|
375
588
|
};
|
|
376
589
|
}
|
|
377
590
|
const json = await response.json();
|
|
@@ -393,7 +606,7 @@ var AppilotsAPIClient = class {
|
|
|
393
606
|
*/
|
|
394
607
|
async evalRun(scenarios) {
|
|
395
608
|
try {
|
|
396
|
-
const response = await this.request(`${this.
|
|
609
|
+
const response = await this.request(`${this.baseUrl}/cli/eval/run`, {
|
|
397
610
|
method: "POST",
|
|
398
611
|
headers: {
|
|
399
612
|
"Content-Type": "application/json",
|
|
@@ -424,7 +637,7 @@ var AppilotsAPIClient = class {
|
|
|
424
637
|
*/
|
|
425
638
|
async health() {
|
|
426
639
|
try {
|
|
427
|
-
const response = await this.request(`${this.
|
|
640
|
+
const response = await this.request(`${this.baseUrl}/health`, {
|
|
428
641
|
method: "GET"
|
|
429
642
|
});
|
|
430
643
|
return response.ok;
|
|
@@ -437,6 +650,19 @@ var AppilotsAPIClient = class {
|
|
|
437
650
|
// src/cli/commands/init.ts
|
|
438
651
|
var import_fs3 = require("fs");
|
|
439
652
|
var import_path2 = require("path");
|
|
653
|
+
|
|
654
|
+
// src/cli/utils/server-hint.ts
|
|
655
|
+
function hintUnreachableServer(serverUrl, message) {
|
|
656
|
+
if (serverUrl !== DEFAULT_SERVER_URL) return;
|
|
657
|
+
if (typeof message === "string" && !message.includes("Cannot reach Appilots server")) return;
|
|
658
|
+
if (message !== void 0 && typeof message !== "string") return;
|
|
659
|
+
info(`serverUrl is not set, so the CLI is using the built-in default (${DEFAULT_SERVER_URL})`);
|
|
660
|
+
info("For the hosted backend, set it in .appilotsrc:");
|
|
661
|
+
info(' "serverUrl": "https://api.appilots.com"');
|
|
662
|
+
info("Or pass --server <url>, or set APPILOTS_SERVER_URL");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/cli/commands/init.ts
|
|
440
666
|
function ensureConfigIgnored(dir) {
|
|
441
667
|
const gitignorePath = (0, import_path2.join)(dir, ".gitignore");
|
|
442
668
|
if (!(0, import_fs3.existsSync)(gitignorePath)) {
|
|
@@ -450,7 +676,10 @@ function ensureConfigIgnored(dir) {
|
|
|
450
676
|
if (ignored) {
|
|
451
677
|
return "already-ignored";
|
|
452
678
|
}
|
|
453
|
-
(0, import_fs3.appendFileSync)(
|
|
679
|
+
(0, import_fs3.appendFileSync)(
|
|
680
|
+
gitignorePath,
|
|
681
|
+
"\n# Appilots CLI config (contains your API key \u2014 do not commit)\n.appilotsrc\n"
|
|
682
|
+
);
|
|
454
683
|
return "added";
|
|
455
684
|
}
|
|
456
685
|
function initCommand() {
|
|
@@ -473,7 +702,7 @@ function initCommand() {
|
|
|
473
702
|
if (!options.apiKey && env.apiKey) {
|
|
474
703
|
info("Using API key from APPILOTS_API_KEY environment variable");
|
|
475
704
|
}
|
|
476
|
-
const serverUrl = options.server || env.serverUrl ||
|
|
705
|
+
const serverUrl = options.server || env.serverUrl || DEFAULT_SERVER_URL;
|
|
477
706
|
if (!apiKey.startsWith("ak_")) {
|
|
478
707
|
error("Invalid API key format. Must start with ak_");
|
|
479
708
|
process.exit(1);
|
|
@@ -487,6 +716,7 @@ function initCommand() {
|
|
|
487
716
|
if (!isHealthy) {
|
|
488
717
|
error(`Cannot reach Appilots server at ${serverUrl}`);
|
|
489
718
|
info("Make sure the server is running and the URL is correct");
|
|
719
|
+
hintUnreachableServer(serverUrl);
|
|
490
720
|
process.exit(1);
|
|
491
721
|
}
|
|
492
722
|
const status = await apiClient.status();
|
|
@@ -524,7 +754,7 @@ function initCommand() {
|
|
|
524
754
|
dim(" $ appilots sync - Analyze and upload your project");
|
|
525
755
|
dim(" $ appilots watch - Watch for changes and auto-sync");
|
|
526
756
|
} catch (err) {
|
|
527
|
-
error(`Initialization failed: ${
|
|
757
|
+
error(`Initialization failed: ${stringifyUnknown(err)}`);
|
|
528
758
|
process.exit(1);
|
|
529
759
|
}
|
|
530
760
|
});
|
|
@@ -533,7 +763,7 @@ function initCommand() {
|
|
|
533
763
|
// src/cli/commands/sync.ts
|
|
534
764
|
var import_commander2 = require("commander");
|
|
535
765
|
var import_promises5 = require("fs/promises");
|
|
536
|
-
var
|
|
766
|
+
var import_node_path5 = require("path");
|
|
537
767
|
|
|
538
768
|
// src/generators/MCPGenerator.ts
|
|
539
769
|
var import_promises4 = require("fs/promises");
|
|
@@ -813,6 +1043,45 @@ function hasJsxAttribute(element, name) {
|
|
|
813
1043
|
}
|
|
814
1044
|
|
|
815
1045
|
// src/ast/jsx/classify.ts
|
|
1046
|
+
var NON_INTERACTION_HANDLERS = /* @__PURE__ */ new Set([
|
|
1047
|
+
"onLayout",
|
|
1048
|
+
"onScroll",
|
|
1049
|
+
"onScrollBeginDrag",
|
|
1050
|
+
"onScrollEndDrag",
|
|
1051
|
+
"onMomentumScrollBegin",
|
|
1052
|
+
"onMomentumScrollEnd",
|
|
1053
|
+
"onContentSizeChange",
|
|
1054
|
+
"onEndReached",
|
|
1055
|
+
"onViewableItemsChanged",
|
|
1056
|
+
"onLoad",
|
|
1057
|
+
"onLoadStart",
|
|
1058
|
+
"onLoadEnd",
|
|
1059
|
+
"onError",
|
|
1060
|
+
"onProgress",
|
|
1061
|
+
"onFocus",
|
|
1062
|
+
"onBlur"
|
|
1063
|
+
]);
|
|
1064
|
+
var VALUE_HANDLERS = /* @__PURE__ */ new Set([
|
|
1065
|
+
"onChange",
|
|
1066
|
+
"onChangeText",
|
|
1067
|
+
"onValueChange",
|
|
1068
|
+
"onSelect",
|
|
1069
|
+
"onSelectionChange",
|
|
1070
|
+
"onSubmitEditing"
|
|
1071
|
+
]);
|
|
1072
|
+
function pressHandlerProp(element) {
|
|
1073
|
+
let fallback;
|
|
1074
|
+
for (const attr of element.attributes) {
|
|
1075
|
+
if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
|
|
1076
|
+
const name = attr.name.name;
|
|
1077
|
+
if (!/^on[A-Z]/.test(name)) continue;
|
|
1078
|
+
if (attr.value?.type !== "JSXExpressionContainer") continue;
|
|
1079
|
+
if (name === "onPress") return name;
|
|
1080
|
+
if (NON_INTERACTION_HANDLERS.has(name) || VALUE_HANDLERS.has(name)) continue;
|
|
1081
|
+
fallback ??= name;
|
|
1082
|
+
}
|
|
1083
|
+
return fallback;
|
|
1084
|
+
}
|
|
816
1085
|
var VIEW_COMPONENTS = /* @__PURE__ */ new Set(["View", "ScrollView", "SafeAreaView", "KeyboardAvoidingView"]);
|
|
817
1086
|
var INPUT_COMPONENTS = /* @__PURE__ */ new Set(["TextInput", "Input", "HocInput"]);
|
|
818
1087
|
var BUTTON_COMPONENTS = /* @__PURE__ */ new Set(["TouchableOpacity", "Pressable", "Button"]);
|
|
@@ -827,17 +1096,16 @@ function classifyJsxComponent(name, element) {
|
|
|
827
1096
|
if (INPUT_COMPONENTS.has(name)) return "input";
|
|
828
1097
|
if (BUTTON_COMPONENTS.has(name)) return "button";
|
|
829
1098
|
if (element) {
|
|
830
|
-
const hasOptions = hasJsxAttribute(element, "options");
|
|
1099
|
+
const hasOptions = hasJsxAttribute(element, "options") || hasJsxAttribute(element, "segments") || hasJsxAttribute(element, "choices") || hasJsxAttribute(element, "tabs");
|
|
831
1100
|
const hasValue = hasJsxAttribute(element, "value");
|
|
832
1101
|
const hasChecked = hasJsxAttribute(element, "checked") || hasJsxAttribute(element, "selected");
|
|
833
1102
|
const hasOnChange = hasJsxAttribute(element, "onChange") || hasJsxAttribute(element, "onValueChange") || hasJsxAttribute(element, "onChangeText");
|
|
834
|
-
const hasOnPress = hasJsxAttribute(element, "onPress");
|
|
835
1103
|
if (hasOptions && (hasValue || hasOnChange)) return "select";
|
|
836
1104
|
if (hasChecked && hasOnChange) return "toggle";
|
|
837
1105
|
if (hasOnChange && (hasJsxAttribute(element, "label") || hasJsxAttribute(element, "placeholder"))) {
|
|
838
1106
|
return "input";
|
|
839
1107
|
}
|
|
840
|
-
if (
|
|
1108
|
+
if (pressHandlerProp(element)) return "button";
|
|
841
1109
|
if (getStringAttr(element, "visible") || getExpressionIdentifierAttr(element, "visible")) {
|
|
842
1110
|
return "modal";
|
|
843
1111
|
}
|
|
@@ -1165,8 +1433,8 @@ var ScreenAnalyzer = class {
|
|
|
1165
1433
|
}
|
|
1166
1434
|
const screens = [];
|
|
1167
1435
|
this.screensFilteredOut = 0;
|
|
1168
|
-
for (const
|
|
1169
|
-
const filePath = import_path3.default.resolve(this.config.rootDir,
|
|
1436
|
+
for (const file2 of files) {
|
|
1437
|
+
const filePath = import_path3.default.resolve(this.config.rootDir, file2);
|
|
1170
1438
|
try {
|
|
1171
1439
|
const descriptor = await this.analyzeFile(filePath);
|
|
1172
1440
|
if (!descriptor) continue;
|
|
@@ -1176,23 +1444,28 @@ var ScreenAnalyzer = class {
|
|
|
1176
1444
|
if (!hasRegisterScreen && !matchesPattern) {
|
|
1177
1445
|
this.screensFilteredOut++;
|
|
1178
1446
|
if (this.verbose) {
|
|
1179
|
-
console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${
|
|
1447
|
+
console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file2}`);
|
|
1180
1448
|
}
|
|
1181
1449
|
continue;
|
|
1182
1450
|
}
|
|
1183
1451
|
}
|
|
1184
1452
|
screens.push(descriptor);
|
|
1185
1453
|
if (this.verbose) {
|
|
1186
|
-
console.log(`[ScreenAnalyzer] \u2713 Analyzed: ${descriptor.name} (${
|
|
1454
|
+
console.log(`[ScreenAnalyzer] \u2713 Analyzed: ${descriptor.name} (${file2})`);
|
|
1187
1455
|
}
|
|
1188
1456
|
} catch (error2) {
|
|
1189
1457
|
if (this.verbose) {
|
|
1190
|
-
console.warn(
|
|
1458
|
+
console.warn(
|
|
1459
|
+
`[ScreenAnalyzer] Failed to parse ${file2}:`,
|
|
1460
|
+
error2 instanceof Error ? error2.message : error2
|
|
1461
|
+
);
|
|
1191
1462
|
}
|
|
1192
1463
|
}
|
|
1193
1464
|
}
|
|
1194
1465
|
if (this.verbose && this.strictScreens) {
|
|
1195
|
-
console.log(
|
|
1466
|
+
console.log(
|
|
1467
|
+
`[ScreenAnalyzer] Strict mode: ${screens.length} included, ${this.screensFilteredOut} filtered out`
|
|
1468
|
+
);
|
|
1196
1469
|
}
|
|
1197
1470
|
return screens;
|
|
1198
1471
|
}
|
|
@@ -1480,7 +1753,11 @@ var ScreenAnalyzer = class {
|
|
|
1480
1753
|
for (const form of secondary) {
|
|
1481
1754
|
const existing = out.find((candidate) => candidate.id === form.id) ?? this.findFormWithSharedFields(out, form);
|
|
1482
1755
|
if (!existing) {
|
|
1483
|
-
out.push({
|
|
1756
|
+
out.push({
|
|
1757
|
+
...form,
|
|
1758
|
+
fields: [...form.fields],
|
|
1759
|
+
submitAction: this.namedSubmitAction(form.submitAction)
|
|
1760
|
+
});
|
|
1484
1761
|
continue;
|
|
1485
1762
|
}
|
|
1486
1763
|
for (const field of form.fields) {
|
|
@@ -1499,9 +1776,12 @@ var ScreenAnalyzer = class {
|
|
|
1499
1776
|
findEquivalentField(fields, incoming) {
|
|
1500
1777
|
return fields.find((field) => {
|
|
1501
1778
|
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.
|
|
1779
|
+
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
1780
|
+
return true;
|
|
1781
|
+
if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
|
|
1782
|
+
return true;
|
|
1783
|
+
if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
|
|
1784
|
+
return true;
|
|
1505
1785
|
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
1506
1786
|
return true;
|
|
1507
1787
|
}
|
|
@@ -1661,9 +1941,11 @@ var ScreenAnalyzer = class {
|
|
|
1661
1941
|
source: field.locator?.source ?? "accessibilityLabel"
|
|
1662
1942
|
});
|
|
1663
1943
|
} else if (attrName === "keyboardType") {
|
|
1664
|
-
if (attr.value.value === "email-address" || attr.value.value === "email")
|
|
1944
|
+
if (attr.value.value === "email-address" || attr.value.value === "email")
|
|
1945
|
+
field.type = "email";
|
|
1665
1946
|
if (attr.value.value === "phone-pad") field.type = "phone";
|
|
1666
|
-
if (attr.value.value === "numeric" || attr.value.value === "number-pad")
|
|
1947
|
+
if (attr.value.value === "numeric" || attr.value.value === "number-pad")
|
|
1948
|
+
field.type = "number";
|
|
1667
1949
|
}
|
|
1668
1950
|
} else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
|
|
1669
1951
|
if (attrName === "value" && BabelTypes.isIdentifier(attr.value.expression)) {
|
|
@@ -1829,8 +2111,10 @@ var ScreenAnalyzer = class {
|
|
|
1829
2111
|
};
|
|
1830
2112
|
if (handler.nativeConfirmationExpected) action.nativeConfirmationExpected = true;
|
|
1831
2113
|
if (handler.targetScreen && !action.targetScreen) action.targetScreen = handler.targetScreen;
|
|
1832
|
-
if (handler.successSignal && !action.successSignal)
|
|
1833
|
-
|
|
2114
|
+
if (handler.successSignal && !action.successSignal)
|
|
2115
|
+
action.successSignal = handler.successSignal;
|
|
2116
|
+
if (handler.failureSignal && !action.failureSignal)
|
|
2117
|
+
action.failureSignal = handler.failureSignal;
|
|
1834
2118
|
if (handler.opensModal && !action.opensModal) action.opensModal = handler.opensModal;
|
|
1835
2119
|
if (handler.destructive || action.destructive === true || action.requiresConfirmation === true || action.effect === "destructive" || action.riskLevel === "high") {
|
|
1836
2120
|
action.destructive = true;
|
|
@@ -1848,6 +2132,7 @@ var ScreenAnalyzer = class {
|
|
|
1848
2132
|
if (!BabelTypes.isJSXIdentifier(element.name)) return;
|
|
1849
2133
|
const componentName = element.name.name;
|
|
1850
2134
|
if (classifyJsxComponent(componentName, element) !== "button") return;
|
|
2135
|
+
const pressProp = pressHandlerProp(element);
|
|
1851
2136
|
let label;
|
|
1852
2137
|
let handler;
|
|
1853
2138
|
for (const attr of element.attributes) {
|
|
@@ -1857,7 +2142,7 @@ var ScreenAnalyzer = class {
|
|
|
1857
2142
|
if ((attrName === "title" || attrName === "accessibilityLabel" || attrName === "label") && BabelTypes.isStringLiteral(attr.value)) {
|
|
1858
2143
|
label = attr.value.value;
|
|
1859
2144
|
}
|
|
1860
|
-
if (attrName ===
|
|
2145
|
+
if (attrName === pressProp && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
|
|
1861
2146
|
handler = attr.value.expression.name;
|
|
1862
2147
|
}
|
|
1863
2148
|
}
|
|
@@ -1923,10 +2208,13 @@ var ScreenAnalyzer = class {
|
|
|
1923
2208
|
}
|
|
1924
2209
|
};
|
|
1925
2210
|
if (fn.body) {
|
|
1926
|
-
(0, import_traverse4.default)(
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2211
|
+
(0, import_traverse4.default)(
|
|
2212
|
+
fn.body,
|
|
2213
|
+
{
|
|
2214
|
+
noScope: true,
|
|
2215
|
+
enter: (nodePath) => inspectNode(nodePath.node)
|
|
2216
|
+
}
|
|
2217
|
+
);
|
|
1930
2218
|
}
|
|
1931
2219
|
const expectedOutcome = targetScreen ? "navigation" : hasStateSetter ? "inline-feedback" : hasAwait || hasThen ? "mixed" : "none";
|
|
1932
2220
|
return {
|
|
@@ -1984,6 +2272,7 @@ var ScreenAnalyzer = class {
|
|
|
1984
2272
|
};
|
|
1985
2273
|
const componentName = BabelTypes.isJSXIdentifier(element.name) ? element.name.name : void 0;
|
|
1986
2274
|
action.sourceComponent = componentName;
|
|
2275
|
+
const pressProp = pressHandlerProp(element) ?? "onPress";
|
|
1987
2276
|
let handlerName;
|
|
1988
2277
|
for (const attr of element.attributes) {
|
|
1989
2278
|
if (!BabelTypes.isJSXAttribute(attr)) continue;
|
|
@@ -2018,7 +2307,7 @@ var ScreenAnalyzer = class {
|
|
|
2018
2307
|
}
|
|
2019
2308
|
}
|
|
2020
2309
|
} else if (BabelTypes.isJSXExpressionContainer(attr.value)) {
|
|
2021
|
-
if (attrName ===
|
|
2310
|
+
if (attrName === pressProp && BabelTypes.isIdentifier(attr.value.expression)) {
|
|
2022
2311
|
handlerName = attr.value.expression.name.toLowerCase();
|
|
2023
2312
|
action.handler = attr.value.expression.name;
|
|
2024
2313
|
if (handlerName.includes("submit")) {
|
|
@@ -2026,7 +2315,7 @@ var ScreenAnalyzer = class {
|
|
|
2026
2315
|
} else if (handlerName.includes("navigate")) {
|
|
2027
2316
|
action.type = "navigation";
|
|
2028
2317
|
}
|
|
2029
|
-
} else if (attrName ===
|
|
2318
|
+
} else if (attrName === pressProp) {
|
|
2030
2319
|
const inline = this.extractInlineOnPressMetadata(attr.value.expression);
|
|
2031
2320
|
if (inline.handler) {
|
|
2032
2321
|
handlerName = inline.handler.toLowerCase();
|
|
@@ -2050,7 +2339,11 @@ var ScreenAnalyzer = class {
|
|
|
2050
2339
|
}
|
|
2051
2340
|
}
|
|
2052
2341
|
if (!action.locator && action.id) {
|
|
2053
|
-
action.locator = {
|
|
2342
|
+
action.locator = {
|
|
2343
|
+
id: action.id,
|
|
2344
|
+
label: action.label,
|
|
2345
|
+
source: action.label ? "label" : "inferred"
|
|
2346
|
+
};
|
|
2054
2347
|
}
|
|
2055
2348
|
if (this.isElementDestructive(element, handlerName, action.id)) {
|
|
2056
2349
|
action.destructive = true;
|
|
@@ -2192,24 +2485,27 @@ var ScreenAnalyzer = class {
|
|
|
2192
2485
|
}
|
|
2193
2486
|
extractRowAction(fn) {
|
|
2194
2487
|
let action;
|
|
2195
|
-
(0, import_traverse4.default)(
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2488
|
+
(0, import_traverse4.default)(
|
|
2489
|
+
fn.body,
|
|
2490
|
+
{
|
|
2491
|
+
noScope: true,
|
|
2492
|
+
CallExpression: (nodePath) => {
|
|
2493
|
+
const node = nodePath.node;
|
|
2494
|
+
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)) {
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
const firstArg = node.arguments[0];
|
|
2498
|
+
if (!BabelTypes.isStringLiteral(firstArg)) return;
|
|
2499
|
+
const params = this.extractNavigationParams(node.arguments[1]);
|
|
2500
|
+
action = {
|
|
2501
|
+
type: "navigation",
|
|
2502
|
+
targetScreen: firstArg.value,
|
|
2503
|
+
...Object.keys(params).length > 0 ? { params } : {},
|
|
2504
|
+
description: `Pressing a row opens ${firstArg.value}`
|
|
2505
|
+
};
|
|
2201
2506
|
}
|
|
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
2507
|
}
|
|
2212
|
-
|
|
2508
|
+
);
|
|
2213
2509
|
return action;
|
|
2214
2510
|
}
|
|
2215
2511
|
extractNavigationParams(arg) {
|
|
@@ -2242,15 +2538,18 @@ var ScreenAnalyzer = class {
|
|
|
2242
2538
|
} else if (BabelTypes.isIdentifier(firstParam)) {
|
|
2243
2539
|
itemNames.add(firstParam.name);
|
|
2244
2540
|
}
|
|
2245
|
-
(0, import_traverse4.default)(
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2541
|
+
(0, import_traverse4.default)(
|
|
2542
|
+
fn.body,
|
|
2543
|
+
{
|
|
2544
|
+
noScope: true,
|
|
2545
|
+
MemberExpression: (nodePath) => {
|
|
2546
|
+
const node = nodePath.node;
|
|
2547
|
+
if (BabelTypes.isIdentifier(node.object) && itemNames.has(node.object.name) && BabelTypes.isIdentifier(node.property)) {
|
|
2548
|
+
fields.add(node.property.name);
|
|
2549
|
+
}
|
|
2251
2550
|
}
|
|
2252
2551
|
}
|
|
2253
|
-
|
|
2552
|
+
);
|
|
2254
2553
|
return Array.from(fields).sort();
|
|
2255
2554
|
}
|
|
2256
2555
|
inferItemType(dataSource, renderItem, displayFields) {
|
|
@@ -2262,11 +2561,19 @@ var ScreenAnalyzer = class {
|
|
|
2262
2561
|
if (displayFields.length > 0) return "Item";
|
|
2263
2562
|
return void 0;
|
|
2264
2563
|
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Identity comes from how a field is NAMED, not from what the app
|
|
2566
|
+
* sells: `id`/`uuid`/`key`/`slug` are conventions any codebase uses,
|
|
2567
|
+
* `name`/`title`/`email` are how any row introduces itself. `plate`
|
|
2568
|
+
* used to sit in this list and read like one of them — but it is the
|
|
2569
|
+
* example app's schema, and no other tenant ever got its equivalent
|
|
2570
|
+
* (`mrn`, `trackingNumber`, `sku`) added here.
|
|
2571
|
+
*/
|
|
2265
2572
|
inferIdentityFields(keyField, displayFields) {
|
|
2266
2573
|
const out = /* @__PURE__ */ new Set();
|
|
2267
2574
|
if (keyField) out.add(keyField);
|
|
2268
2575
|
for (const field of displayFields) {
|
|
2269
|
-
if (/^(id|uuid|key|name|title|
|
|
2576
|
+
if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
|
|
2270
2577
|
}
|
|
2271
2578
|
return Array.from(out);
|
|
2272
2579
|
}
|
|
@@ -2277,17 +2584,23 @@ var ScreenAnalyzer = class {
|
|
|
2277
2584
|
CallExpression: (nodePath) => {
|
|
2278
2585
|
const node = nodePath.node;
|
|
2279
2586
|
if (!BabelTypes.isMemberExpression(node.callee)) return;
|
|
2280
|
-
if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
|
|
2281
|
-
|
|
2587
|
+
if (!BabelTypes.isIdentifier(node.callee.property) || node.callee.property.name !== "filter")
|
|
2588
|
+
return;
|
|
2589
|
+
if (!BabelTypes.isIdentifier(node.callee.object) || node.callee.object.name !== dataSource)
|
|
2590
|
+
return;
|
|
2282
2591
|
const fn = node.arguments[0];
|
|
2283
|
-
if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2592
|
+
if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
|
|
2593
|
+
return;
|
|
2594
|
+
(0, import_traverse4.default)(
|
|
2595
|
+
fn.body,
|
|
2596
|
+
{
|
|
2597
|
+
noScope: true,
|
|
2598
|
+
Identifier: (innerPath) => {
|
|
2599
|
+
const name = innerPath.node.name;
|
|
2600
|
+
if (/query|search|filter/i.test(name)) queryBinding = queryBinding ?? name;
|
|
2601
|
+
}
|
|
2289
2602
|
}
|
|
2290
|
-
|
|
2603
|
+
);
|
|
2291
2604
|
}
|
|
2292
2605
|
});
|
|
2293
2606
|
return queryBinding;
|
|
@@ -2378,7 +2691,7 @@ var NavigationAnalyzer = class {
|
|
|
2378
2691
|
cwd: this.config.rootDir,
|
|
2379
2692
|
ignore: excludePatterns
|
|
2380
2693
|
});
|
|
2381
|
-
return files.map((
|
|
2694
|
+
return files.map((file2) => import_path4.default.join(this.config.rootDir, file2));
|
|
2382
2695
|
}
|
|
2383
2696
|
/** Parse navigator definitions from a file */
|
|
2384
2697
|
parseNavigators(content, filePath) {
|
|
@@ -2439,11 +2752,7 @@ var NavigationAnalyzer = class {
|
|
|
2439
2752
|
if (t5.isJSXAttribute(initialRouteAttr) && t5.isStringLiteral(initialRouteAttr.value)) {
|
|
2440
2753
|
navigator.initialRouteName = initialRouteAttr.value.value;
|
|
2441
2754
|
}
|
|
2442
|
-
const screens = this.extractScreensFromNavigator(
|
|
2443
|
-
node,
|
|
2444
|
-
objectName,
|
|
2445
|
-
navigator.type
|
|
2446
|
-
);
|
|
2755
|
+
const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
|
|
2447
2756
|
screensByNavigator.set(objectName, screens);
|
|
2448
2757
|
}
|
|
2449
2758
|
}
|
|
@@ -2460,27 +2769,112 @@ var NavigationAnalyzer = class {
|
|
|
2460
2769
|
}
|
|
2461
2770
|
return navigators;
|
|
2462
2771
|
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Is this JSX element `<navigatorVarName.MEMBER …>`?
|
|
2774
|
+
*/
|
|
2775
|
+
// Deliberately NOT a type predicate (`node is t.JSXElement`): a false
|
|
2776
|
+
// result would then narrow `node` to "not a JSXElement", and the very
|
|
2777
|
+
// next check — the same node against a different member — would see
|
|
2778
|
+
// `never`. It is a question about the member name, not about the node
|
|
2779
|
+
// kind.
|
|
2780
|
+
isNavigatorMember(node, navigatorVarName, member) {
|
|
2781
|
+
if (!t5.isJSXElement(node)) return false;
|
|
2782
|
+
const name = node.openingElement.name;
|
|
2783
|
+
return t5.isJSXMemberExpression(name) && t5.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && t5.isJSXIdentifier(name.property) && name.property.name === member;
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Flatten a navigator's JSX children into the `<X.Screen>` elements they
|
|
2787
|
+
* contain, unwrapping every container a real app puts in between.
|
|
2788
|
+
*
|
|
2789
|
+
* The old version compared `t.isJSXElement(child)` against the direct
|
|
2790
|
+
* children only. A ternary is a `JSXExpressionContainer`, so an
|
|
2791
|
+
* auth-gated root — the modal shape of a commercial app, and the shape
|
|
2792
|
+
* of this repo's own `apps/example-app` — contributed ZERO screens to
|
|
2793
|
+
* the graph while `appilots sync` reported success (#396).
|
|
2794
|
+
*
|
|
2795
|
+
* Both branches of a conditional are collected on purpose. The graph is
|
|
2796
|
+
* a design-time map of what routes EXIST, not a prediction of which one
|
|
2797
|
+
* a given session will render; the agent needs the destination name to
|
|
2798
|
+
* be there whether or not the user happens to be logged in right now.
|
|
2799
|
+
*/
|
|
2800
|
+
collectScreenElements(children, navigatorVarName, depth = 0) {
|
|
2801
|
+
if (depth > 12) return [];
|
|
2802
|
+
const found = [];
|
|
2803
|
+
const visit = (node) => {
|
|
2804
|
+
if (!node) return;
|
|
2805
|
+
if (this.isNavigatorMember(node, navigatorVarName, "Screen")) {
|
|
2806
|
+
found.push(node);
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
if (this.isNavigatorMember(node, navigatorVarName, "Group")) {
|
|
2810
|
+
const group = node;
|
|
2811
|
+
found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
if (t5.isJSXElement(node)) {
|
|
2815
|
+
const name = node.openingElement.name;
|
|
2816
|
+
const isForeignNavigator = t5.isJSXMemberExpression(name) && t5.isJSXIdentifier(name.property) && name.property.name === "Navigator";
|
|
2817
|
+
if (isForeignNavigator) return;
|
|
2818
|
+
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
if (t5.isJSXFragment(node)) {
|
|
2822
|
+
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
if (t5.isJSXExpressionContainer(node)) {
|
|
2826
|
+
visit(node.expression);
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
if (t5.isConditionalExpression(node)) {
|
|
2830
|
+
visit(node.consequent);
|
|
2831
|
+
visit(node.alternate);
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
if (t5.isLogicalExpression(node)) {
|
|
2835
|
+
visit(node.left);
|
|
2836
|
+
visit(node.right);
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
if (t5.isCallExpression(node)) {
|
|
2840
|
+
for (const arg of node.arguments) {
|
|
2841
|
+
if (t5.isArrowFunctionExpression(arg) || t5.isFunctionExpression(arg)) {
|
|
2842
|
+
if (t5.isBlockStatement(arg.body)) {
|
|
2843
|
+
for (const stmt of arg.body.body) {
|
|
2844
|
+
if (t5.isReturnStatement(stmt)) visit(stmt.argument);
|
|
2845
|
+
}
|
|
2846
|
+
} else {
|
|
2847
|
+
visit(arg.body);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
if (t5.isArrayExpression(node)) {
|
|
2854
|
+
for (const el of node.elements) visit(el);
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
if (t5.isTSAsExpression(node) || t5.isTSNonNullExpression(node)) {
|
|
2858
|
+
visit(node.expression);
|
|
2859
|
+
}
|
|
2860
|
+
};
|
|
2861
|
+
for (const child of children) visit(child);
|
|
2862
|
+
return found;
|
|
2863
|
+
}
|
|
2463
2864
|
/** Extract screens from a navigator JSX element */
|
|
2464
2865
|
extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
|
|
2465
2866
|
const screens = [];
|
|
2466
2867
|
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
|
-
}
|
|
2868
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2869
|
+
for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
|
|
2870
|
+
const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
|
|
2871
|
+
if (!screenName || seen.has(screenName)) continue;
|
|
2872
|
+
seen.add(screenName);
|
|
2873
|
+
screens.push({
|
|
2874
|
+
name: screenName,
|
|
2875
|
+
navigatorName: navigatorVarName,
|
|
2876
|
+
navigatorType
|
|
2877
|
+
});
|
|
2484
2878
|
}
|
|
2485
2879
|
return screens;
|
|
2486
2880
|
}
|
|
@@ -2539,9 +2933,7 @@ var NavigationAnalyzer = class {
|
|
|
2539
2933
|
if (member.type === "TSPropertySignature" && member.key) {
|
|
2540
2934
|
const keyName = member.key.type === "Identifier" ? member.key.name : null;
|
|
2541
2935
|
if (keyName && member.typeAnnotation) {
|
|
2542
|
-
const params = this.extractParamsFromType(
|
|
2543
|
-
member.typeAnnotation.typeAnnotation
|
|
2544
|
-
);
|
|
2936
|
+
const params = this.extractParamsFromType(member.typeAnnotation.typeAnnotation);
|
|
2545
2937
|
entries.set(keyName, params);
|
|
2546
2938
|
}
|
|
2547
2939
|
}
|
|
@@ -2581,7 +2973,7 @@ var NavigationAnalyzer = class {
|
|
|
2581
2973
|
if (type.type === "TSUndefinedKeyword") return "undefined";
|
|
2582
2974
|
if (type.type === "TSNullKeyword") return "null";
|
|
2583
2975
|
if (type.type === "TSUnionType") {
|
|
2584
|
-
return type.types.map((
|
|
2976
|
+
return type.types.map((t15) => this.typeToString(t15)).join(" | ");
|
|
2585
2977
|
}
|
|
2586
2978
|
if (type.type === "TSTypeLiteral") {
|
|
2587
2979
|
return "object";
|
|
@@ -2602,7 +2994,7 @@ var NavigationAnalyzer = class {
|
|
|
2602
2994
|
/** Attach parsed type params to navigator screens */
|
|
2603
2995
|
attachParamsToNavigators(navigators, types) {
|
|
2604
2996
|
for (const navigator of navigators) {
|
|
2605
|
-
const matchingType = types.find((
|
|
2997
|
+
const matchingType = types.find((t15) => t15.type === navigator.type);
|
|
2606
2998
|
if (matchingType) {
|
|
2607
2999
|
for (const screen of navigator.screens) {
|
|
2608
3000
|
const screenParams = matchingType.paramEntries.get(screen.name);
|
|
@@ -2620,6 +3012,11 @@ var NavigationAnalyzer = class {
|
|
|
2620
3012
|
let initialScreen = "";
|
|
2621
3013
|
for (const navigator of parsedNavigators) {
|
|
2622
3014
|
const screenNames = navigator.screens.map((s) => s.name);
|
|
3015
|
+
if (screenNames.length === 0) {
|
|
3016
|
+
console.warn(
|
|
3017
|
+
`[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.`
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
2623
3020
|
navigators.push({
|
|
2624
3021
|
name: navigator.name,
|
|
2625
3022
|
type: navigator.type,
|
|
@@ -2693,8 +3090,8 @@ var ComponentAnalyzer = class {
|
|
|
2693
3090
|
});
|
|
2694
3091
|
const components = [];
|
|
2695
3092
|
(0, import_traverse6.default)(ast, {
|
|
2696
|
-
JSXElement: (
|
|
2697
|
-
const component = this.extractComponentFromJSXElement(
|
|
3093
|
+
JSXElement: (path11) => {
|
|
3094
|
+
const component = this.extractComponentFromJSXElement(path11.node);
|
|
2698
3095
|
if (component) {
|
|
2699
3096
|
components.push(component);
|
|
2700
3097
|
}
|
|
@@ -2828,13 +3225,13 @@ var FormAnalyzer = class {
|
|
|
2828
3225
|
this.inputElements = [];
|
|
2829
3226
|
this.submitButtons = [];
|
|
2830
3227
|
(0, import_traverse7.default)(ast, {
|
|
2831
|
-
CallExpression: (
|
|
2832
|
-
this.extractStateVariables(
|
|
3228
|
+
CallExpression: (path11) => {
|
|
3229
|
+
this.extractStateVariables(path11.node);
|
|
2833
3230
|
}
|
|
2834
3231
|
});
|
|
2835
3232
|
(0, import_traverse7.default)(ast, {
|
|
2836
|
-
JSXElement: (
|
|
2837
|
-
this.extractFormElements(
|
|
3233
|
+
JSXElement: (path11) => {
|
|
3234
|
+
this.extractFormElements(path11.node);
|
|
2838
3235
|
}
|
|
2839
3236
|
});
|
|
2840
3237
|
const validationRules = this.extractValidationRules(ast);
|
|
@@ -2934,8 +3331,8 @@ var FormAnalyzer = class {
|
|
|
2934
3331
|
extractValidationRules(ast) {
|
|
2935
3332
|
const rules = {};
|
|
2936
3333
|
(0, import_traverse7.default)(ast, {
|
|
2937
|
-
IfStatement: (
|
|
2938
|
-
const test =
|
|
3334
|
+
IfStatement: (path11) => {
|
|
3335
|
+
const test = path11.node.test;
|
|
2939
3336
|
const rule = this.extractRuleFromCondition(test);
|
|
2940
3337
|
if (rule) {
|
|
2941
3338
|
const { field, description } = rule;
|
|
@@ -3068,8 +3465,8 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3068
3465
|
console.log(
|
|
3069
3466
|
`[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
|
|
3070
3467
|
);
|
|
3071
|
-
const enrichmentPromises = screenFiles.map(async (
|
|
3072
|
-
const filePath = import_node_path.default.resolve(config.rootDir,
|
|
3468
|
+
const enrichmentPromises = screenFiles.map(async (file2) => {
|
|
3469
|
+
const filePath = import_node_path.default.resolve(config.rootDir, file2);
|
|
3073
3470
|
try {
|
|
3074
3471
|
const [components, forms] = await Promise.all([
|
|
3075
3472
|
componentAnalyzer.analyzeFile(filePath),
|
|
@@ -3077,7 +3474,7 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3077
3474
|
]);
|
|
3078
3475
|
return { filePath, components, forms };
|
|
3079
3476
|
} catch (error2) {
|
|
3080
|
-
console.warn(`[ReactNativePlatformAnalyzer] Failed to analyze ${
|
|
3477
|
+
console.warn(`[ReactNativePlatformAnalyzer] Failed to analyze ${file2}:`, error2);
|
|
3081
3478
|
return { filePath, components: [], forms: [] };
|
|
3082
3479
|
}
|
|
3083
3480
|
});
|
|
@@ -3345,13 +3742,13 @@ function staticRoutePath(node) {
|
|
|
3345
3742
|
if (!node) return void 0;
|
|
3346
3743
|
if (t9.isStringLiteral(node)) return node.value;
|
|
3347
3744
|
if (t9.isTemplateLiteral(node)) {
|
|
3348
|
-
let
|
|
3745
|
+
let path11 = "";
|
|
3349
3746
|
node.quasis.forEach((quasi, index) => {
|
|
3350
|
-
|
|
3747
|
+
path11 += quasi.value.cooked ?? quasi.value.raw;
|
|
3351
3748
|
const expr = node.expressions[index];
|
|
3352
|
-
if (expr)
|
|
3749
|
+
if (expr) path11 += `:${paramNameOf(expr)}`;
|
|
3353
3750
|
});
|
|
3354
|
-
return
|
|
3751
|
+
return path11;
|
|
3355
3752
|
}
|
|
3356
3753
|
return void 0;
|
|
3357
3754
|
}
|
|
@@ -3436,27 +3833,29 @@ var WebScreenAnalyzer = class {
|
|
|
3436
3833
|
include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
3437
3834
|
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
3438
3835
|
} = this.config;
|
|
3439
|
-
const files = await (0, import_fast_glob4.default)(include, { cwd: this.config.rootDir, ignore: exclude })
|
|
3836
|
+
const files = (await (0, import_fast_glob4.default)(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
|
|
3837
|
+
(file2) => !file2.endsWith(".d.ts")
|
|
3838
|
+
);
|
|
3440
3839
|
const patternMatches = await (0, import_fast_glob4.default)(this.screenPatterns, {
|
|
3441
3840
|
cwd: this.config.rootDir,
|
|
3442
3841
|
ignore: exclude
|
|
3443
3842
|
});
|
|
3444
3843
|
const patternSet = new Set(patternMatches.map((f) => import_path5.default.resolve(this.config.rootDir, f)));
|
|
3445
3844
|
const candidates = [];
|
|
3446
|
-
for (const
|
|
3447
|
-
const filePath = import_path5.default.resolve(this.config.rootDir,
|
|
3845
|
+
for (const file2 of files) {
|
|
3846
|
+
const filePath = import_path5.default.resolve(this.config.rootDir, file2);
|
|
3448
3847
|
try {
|
|
3449
3848
|
const candidate = await this.analyzeFile(filePath);
|
|
3450
3849
|
if (!candidate) continue;
|
|
3451
3850
|
candidate.matchesScreenPattern = patternSet.has(filePath);
|
|
3452
3851
|
candidates.push(candidate);
|
|
3453
3852
|
if (this.verbose) {
|
|
3454
|
-
console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${
|
|
3853
|
+
console.log(`[WebScreenAnalyzer] \u2713 Analyzed: ${candidate.descriptor.name} (${file2})`);
|
|
3455
3854
|
}
|
|
3456
3855
|
} catch (error2) {
|
|
3457
3856
|
if (this.verbose) {
|
|
3458
3857
|
console.warn(
|
|
3459
|
-
`[WebScreenAnalyzer] Failed to parse ${
|
|
3858
|
+
`[WebScreenAnalyzer] Failed to parse ${file2}:`,
|
|
3460
3859
|
error2 instanceof Error ? error2.message : error2
|
|
3461
3860
|
);
|
|
3462
3861
|
}
|
|
@@ -3807,7 +4206,8 @@ var WebScreenAnalyzer = class {
|
|
|
3807
4206
|
if (type === "email") return "email";
|
|
3808
4207
|
if (type === "tel") return "phone";
|
|
3809
4208
|
if (type === "number") return "number";
|
|
3810
|
-
if (type === "date" || type === "datetime-local" || type === "month" || type === "week")
|
|
4209
|
+
if (type === "date" || type === "datetime-local" || type === "month" || type === "week")
|
|
4210
|
+
return "date";
|
|
3811
4211
|
const inputMode = getStringAttr(opening, "inputMode") ?? getStringAttr(opening, "inputmode");
|
|
3812
4212
|
if (inputMode === "email") return "email";
|
|
3813
4213
|
if (inputMode === "tel") return "phone";
|
|
@@ -4121,7 +4521,9 @@ var WebScreenAnalyzer = class {
|
|
|
4121
4521
|
}
|
|
4122
4522
|
const callback = expr.arguments[0];
|
|
4123
4523
|
if (!t10.isArrowFunctionExpression(callback) && !t10.isFunctionExpression(callback)) return;
|
|
4124
|
-
const enclosing = nodePath.findParent(
|
|
4524
|
+
const enclosing = nodePath.findParent(
|
|
4525
|
+
(p) => p.isJSXElement()
|
|
4526
|
+
);
|
|
4125
4527
|
const enclosingName = enclosing ? getJsxElementName(enclosing.node.openingElement) : null;
|
|
4126
4528
|
const enclosingRole = enclosing && enclosingName ? classifyWebJsxComponent(enclosingName, enclosing.node.openingElement) : null;
|
|
4127
4529
|
const returnsListItem = callbackReturnsTag(callback, /* @__PURE__ */ new Set(["li", "tr"]));
|
|
@@ -4223,16 +4625,19 @@ function firstCalledFunctionName(node) {
|
|
|
4223
4625
|
}
|
|
4224
4626
|
function containsWindowConfirm(body) {
|
|
4225
4627
|
let found = false;
|
|
4226
|
-
(0, import_traverse9.default)(
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
found = true;
|
|
4628
|
+
(0, import_traverse9.default)(
|
|
4629
|
+
body,
|
|
4630
|
+
{
|
|
4631
|
+
noScope: true,
|
|
4632
|
+
CallExpression: (nodePath) => {
|
|
4633
|
+
const callee = nodePath.node.callee;
|
|
4634
|
+
if (t10.isIdentifier(callee) && callee.name === "confirm") found = true;
|
|
4635
|
+
if (t10.isMemberExpression(callee) && t10.isIdentifier(callee.object) && callee.object.name === "window" && t10.isIdentifier(callee.property) && callee.property.name === "confirm") {
|
|
4636
|
+
found = true;
|
|
4637
|
+
}
|
|
4233
4638
|
}
|
|
4234
4639
|
}
|
|
4235
|
-
|
|
4640
|
+
);
|
|
4236
4641
|
return found;
|
|
4237
4642
|
}
|
|
4238
4643
|
function mergeLocator(current, next) {
|
|
@@ -4272,32 +4677,38 @@ function collectionItemNames(callback) {
|
|
|
4272
4677
|
function collectionDisplayFields(callback, itemNames) {
|
|
4273
4678
|
const fields = /* @__PURE__ */ new Set();
|
|
4274
4679
|
if (!callback.body) return [];
|
|
4275
|
-
(0, import_traverse9.default)(
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4680
|
+
(0, import_traverse9.default)(
|
|
4681
|
+
callback.body,
|
|
4682
|
+
{
|
|
4683
|
+
noScope: true,
|
|
4684
|
+
MemberExpression: (nodePath) => {
|
|
4685
|
+
const node = nodePath.node;
|
|
4686
|
+
if (t10.isIdentifier(node.object) && itemNames.has(node.object.name) && t10.isIdentifier(node.property)) {
|
|
4687
|
+
fields.add(node.property.name);
|
|
4688
|
+
}
|
|
4281
4689
|
}
|
|
4282
4690
|
}
|
|
4283
|
-
|
|
4691
|
+
);
|
|
4284
4692
|
return Array.from(fields).sort();
|
|
4285
4693
|
}
|
|
4286
4694
|
function collectionKeyField(callback, itemNames) {
|
|
4287
4695
|
let keyField;
|
|
4288
4696
|
if (!callback.body) return void 0;
|
|
4289
|
-
(0, import_traverse9.default)(
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4697
|
+
(0, import_traverse9.default)(
|
|
4698
|
+
callback.body,
|
|
4699
|
+
{
|
|
4700
|
+
noScope: true,
|
|
4701
|
+
JSXAttribute: (nodePath) => {
|
|
4702
|
+
const attr = nodePath.node;
|
|
4703
|
+
if (!t10.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
|
|
4704
|
+
if (!attr.value || !t10.isJSXExpressionContainer(attr.value)) return;
|
|
4705
|
+
const expr = attr.value.expression;
|
|
4706
|
+
if (t10.isMemberExpression(expr) && t10.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t10.isIdentifier(expr.property)) {
|
|
4707
|
+
keyField = keyField ?? expr.property.name;
|
|
4708
|
+
}
|
|
4298
4709
|
}
|
|
4299
4710
|
}
|
|
4300
|
-
|
|
4711
|
+
);
|
|
4301
4712
|
return keyField;
|
|
4302
4713
|
}
|
|
4303
4714
|
function collectionRowAction(callback) {
|
|
@@ -4345,7 +4756,7 @@ function inferIdentityFields(keyField, displayFields) {
|
|
|
4345
4756
|
const out = /* @__PURE__ */ new Set();
|
|
4346
4757
|
if (keyField) out.add(keyField);
|
|
4347
4758
|
for (const field of displayFields) {
|
|
4348
|
-
if (/^(id|uuid|key|name|title|
|
|
4759
|
+
if (/^(id|uuid|key|name|title|email|slug)$/i.test(field)) out.add(field);
|
|
4349
4760
|
}
|
|
4350
4761
|
return Array.from(out);
|
|
4351
4762
|
}
|
|
@@ -4403,7 +4814,7 @@ var WebNavigationAnalyzer = class {
|
|
|
4403
4814
|
...this.navigationExclude
|
|
4404
4815
|
];
|
|
4405
4816
|
const files = await (0, import_fast_glob5.default)(patterns, { cwd: this.config.rootDir, ignore });
|
|
4406
|
-
return files.map((
|
|
4817
|
+
return files.map((file2) => import_path6.default.join(this.config.rootDir, file2));
|
|
4407
4818
|
}
|
|
4408
4819
|
// ── JSX <Route> style ────────────────────────────────────────────
|
|
4409
4820
|
extractJsxRoutes(ast) {
|
|
@@ -4905,8 +5316,8 @@ var ZodParsedType = util.arrayToEnum([
|
|
|
4905
5316
|
"set"
|
|
4906
5317
|
]);
|
|
4907
5318
|
var getParsedType = (data) => {
|
|
4908
|
-
const
|
|
4909
|
-
switch (
|
|
5319
|
+
const t15 = typeof data;
|
|
5320
|
+
switch (t15) {
|
|
4910
5321
|
case "undefined":
|
|
4911
5322
|
return ZodParsedType.undefined;
|
|
4912
5323
|
case "string":
|
|
@@ -5178,8 +5589,8 @@ function getErrorMap() {
|
|
|
5178
5589
|
|
|
5179
5590
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
5180
5591
|
var makeIssue = (params) => {
|
|
5181
|
-
const { data, path:
|
|
5182
|
-
const fullPath = [...
|
|
5592
|
+
const { data, path: path11, errorMaps, issueData } = params;
|
|
5593
|
+
const fullPath = [...path11, ...issueData.path || []];
|
|
5183
5594
|
const fullIssue = {
|
|
5184
5595
|
...issueData,
|
|
5185
5596
|
path: fullPath
|
|
@@ -5295,11 +5706,11 @@ var errorUtil;
|
|
|
5295
5706
|
|
|
5296
5707
|
// ../../node_modules/zod/v3/types.js
|
|
5297
5708
|
var ParseInputLazyPath = class {
|
|
5298
|
-
constructor(parent, value,
|
|
5709
|
+
constructor(parent, value, path11, key) {
|
|
5299
5710
|
this._cachedPath = [];
|
|
5300
5711
|
this.parent = parent;
|
|
5301
5712
|
this.data = value;
|
|
5302
|
-
this._path =
|
|
5713
|
+
this._path = path11;
|
|
5303
5714
|
this._key = key;
|
|
5304
5715
|
}
|
|
5305
5716
|
get path() {
|
|
@@ -8741,7 +9152,7 @@ var coerce = {
|
|
|
8741
9152
|
};
|
|
8742
9153
|
var NEVER = INVALID;
|
|
8743
9154
|
|
|
8744
|
-
// ../shared/dist/chunk-
|
|
9155
|
+
// ../shared/dist/chunk-MKT45LRA.mjs
|
|
8745
9156
|
var locatorSourceSchema = external_exports.enum([
|
|
8746
9157
|
"appilotsId",
|
|
8747
9158
|
"testID",
|
|
@@ -8767,7 +9178,16 @@ var locatorDescriptorSchema = external_exports.object({
|
|
|
8767
9178
|
source: locatorSourceSchema.optional()
|
|
8768
9179
|
}).passthrough();
|
|
8769
9180
|
var signalDescriptorSchema = external_exports.object({
|
|
8770
|
-
type: external_exports.enum([
|
|
9181
|
+
type: external_exports.enum([
|
|
9182
|
+
"navigation",
|
|
9183
|
+
"goBack",
|
|
9184
|
+
"toast",
|
|
9185
|
+
"modal",
|
|
9186
|
+
"inline-error",
|
|
9187
|
+
"data-arrival",
|
|
9188
|
+
"loading",
|
|
9189
|
+
"none"
|
|
9190
|
+
]).or(external_exports.string().min(1).max(60)),
|
|
8771
9191
|
target: external_exports.string().optional(),
|
|
8772
9192
|
description: external_exports.string().optional()
|
|
8773
9193
|
}).passthrough();
|
|
@@ -8781,7 +9201,20 @@ var waitPolicyDescriptorSchema = external_exports.object({
|
|
|
8781
9201
|
signals: external_exports.array(signalDescriptorSchema).optional(),
|
|
8782
9202
|
maxMs: external_exports.number().finite().optional()
|
|
8783
9203
|
}).passthrough();
|
|
8784
|
-
var targetDescriptorRoleSchema = external_exports.enum([
|
|
9204
|
+
var targetDescriptorRoleSchema = external_exports.enum([
|
|
9205
|
+
"button",
|
|
9206
|
+
"submit",
|
|
9207
|
+
"input",
|
|
9208
|
+
"toggle",
|
|
9209
|
+
"select",
|
|
9210
|
+
"date",
|
|
9211
|
+
"list",
|
|
9212
|
+
"row",
|
|
9213
|
+
"menuItem",
|
|
9214
|
+
"modal",
|
|
9215
|
+
"custom"
|
|
9216
|
+
]).or(external_exports.string().min(1).max(60));
|
|
9217
|
+
var targetOriginSchema = external_exports.enum(["analyzer", "manifest"]).or(external_exports.string().min(1).max(40));
|
|
8785
9218
|
var targetDescriptorSchema = external_exports.object({
|
|
8786
9219
|
id: external_exports.string(),
|
|
8787
9220
|
role: targetDescriptorRoleSchema,
|
|
@@ -8795,10 +9228,20 @@ var targetDescriptorSchema = external_exports.object({
|
|
|
8795
9228
|
requiresConfirmation: external_exports.boolean().optional(),
|
|
8796
9229
|
opensModal: external_exports.string().optional(),
|
|
8797
9230
|
opensBottomSheet: external_exports.string().optional(),
|
|
8798
|
-
sourceComponent: external_exports.string().optional()
|
|
9231
|
+
sourceComponent: external_exports.string().optional(),
|
|
9232
|
+
origin: targetOriginSchema.optional()
|
|
8799
9233
|
}).passthrough();
|
|
8800
9234
|
var flowStepDescriptorSchema = external_exports.object({
|
|
8801
|
-
type: external_exports.enum([
|
|
9235
|
+
type: external_exports.enum([
|
|
9236
|
+
"navigate",
|
|
9237
|
+
"fill",
|
|
9238
|
+
"press",
|
|
9239
|
+
"select",
|
|
9240
|
+
"toggle",
|
|
9241
|
+
"wait",
|
|
9242
|
+
"confirm",
|
|
9243
|
+
"choose-list-item"
|
|
9244
|
+
]).or(external_exports.string().min(1).max(60)),
|
|
8802
9245
|
target: external_exports.string().optional(),
|
|
8803
9246
|
label: external_exports.string().optional(),
|
|
8804
9247
|
description: external_exports.string().optional(),
|
|
@@ -9020,15 +9463,31 @@ var mfaRegenerateRecoveryCodesSchema = external_exports.object({
|
|
|
9020
9463
|
password: external_exports.string().min(1, "Password is required"),
|
|
9021
9464
|
code: totpCodeSchema
|
|
9022
9465
|
});
|
|
9466
|
+
var deleteAccountSchema = external_exports.object({
|
|
9467
|
+
password: external_exports.string().min(1, "Password is required"),
|
|
9468
|
+
code: totpCodeSchema.optional(),
|
|
9469
|
+
recoveryCode: recoveryCodeSchema.optional(),
|
|
9470
|
+
confirm: external_exports.literal("DELETE MY ACCOUNT")
|
|
9471
|
+
}).refine((v) => !(v.code && v.recoveryCode), {
|
|
9472
|
+
message: "Provide either a TOTP code or a recovery code, not both",
|
|
9473
|
+
path: ["code"]
|
|
9474
|
+
});
|
|
9023
9475
|
var createProjectSchema = external_exports.object({
|
|
9024
9476
|
name: external_exports.string().min(1).max(100),
|
|
9025
9477
|
description: external_exports.string().max(500).optional(),
|
|
9026
9478
|
// Reference to a tenant-scoped provider (preferred). Optional at
|
|
9027
9479
|
// creation so a project can be created and configured later.
|
|
9028
9480
|
providerId: external_exports.string().min(1).optional(),
|
|
9029
|
-
|
|
9481
|
+
// 16k chars (~4k tokens): um prompt de CS real não cabe em 4096 — o
|
|
9482
|
+
// primeiro cliente de produção chegou com 6.385 e tomou 'Invalid input'
|
|
9483
|
+
// sem saber por quê. O teto continua existindo porque o systemPrompt
|
|
9484
|
+
// entra em TODO hop do relay: é proteção de TPM, não de gosto.
|
|
9485
|
+
systemPrompt: external_exports.string().max(16e3).optional()
|
|
9486
|
+
});
|
|
9487
|
+
var updateProjectSchema = createProjectSchema.partial().extend({
|
|
9488
|
+
providerId: external_exports.string().min(1).nullable().optional(),
|
|
9489
|
+
systemPrompt: external_exports.string().max(16e3).nullable().optional()
|
|
9030
9490
|
});
|
|
9031
|
-
var updateProjectSchema = createProjectSchema.partial();
|
|
9032
9491
|
var providerKindSchema = external_exports.enum(["openai", "anthropic", "custom"]);
|
|
9033
9492
|
var createProviderSchema = external_exports.object({
|
|
9034
9493
|
name: external_exports.string().min(1).max(100),
|
|
@@ -9055,6 +9514,7 @@ var uploadMCPDocumentSchema = external_exports.object({
|
|
|
9055
9514
|
content: external_exports.record(external_exports.unknown())
|
|
9056
9515
|
});
|
|
9057
9516
|
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
|
|
9517
|
+
var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
|
|
9058
9518
|
var createApiKeySchema = external_exports.object({
|
|
9059
9519
|
name: external_exports.string().min(1).max(100),
|
|
9060
9520
|
// Required for `sdk` (default), forbidden/ignored for `operator` —
|
|
@@ -9062,6 +9522,7 @@ var createApiKeySchema = external_exports.object({
|
|
|
9062
9522
|
// missing scope still validates as the pre-existing SDK shape.
|
|
9063
9523
|
projectId: external_exports.string().min(1).optional(),
|
|
9064
9524
|
scope: apiKeyScopeSchema.default("sdk"),
|
|
9525
|
+
environment: apiKeyEnvironmentSchema.optional(),
|
|
9065
9526
|
expiresAt: external_exports.string().datetime().optional()
|
|
9066
9527
|
}).refine((v) => v.scope !== "sdk" || !!v.projectId, {
|
|
9067
9528
|
message: 'projectId is required for scope "sdk"',
|
|
@@ -9069,13 +9530,23 @@ var createApiKeySchema = external_exports.object({
|
|
|
9069
9530
|
});
|
|
9070
9531
|
var boundedString = (max) => external_exports.string().max(max);
|
|
9071
9532
|
var clientPlatformSchema = external_exports.enum(["react-native", "web", "android", "ios"]).or(external_exports.string().min(1).max(40));
|
|
9533
|
+
var identityProvenanceSchema = external_exports.enum(["declared", "derived", "positional"]).or(external_exports.string().min(1).max(40));
|
|
9072
9534
|
var snapshotInputSchema = external_exports.object({
|
|
9073
9535
|
id: boundedString(160).optional(),
|
|
9536
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9537
|
+
/**
|
|
9538
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
9539
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
9540
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
9541
|
+
* before the field existed), which must never be read as `false`.
|
|
9542
|
+
*/
|
|
9543
|
+
onScreen: external_exports.boolean().optional(),
|
|
9074
9544
|
label: boundedString(300).optional(),
|
|
9075
9545
|
value: boundedString(4096).optional(),
|
|
9076
9546
|
placeholder: boundedString(300).optional(),
|
|
9077
9547
|
editable: external_exports.boolean().optional(),
|
|
9078
9548
|
secure: external_exports.boolean().optional(),
|
|
9549
|
+
submitsOnReturn: external_exports.boolean().optional(),
|
|
9079
9550
|
type: boundedString(40).optional(),
|
|
9080
9551
|
required: external_exports.boolean().optional(),
|
|
9081
9552
|
invalid: external_exports.boolean().optional(),
|
|
@@ -9083,6 +9554,14 @@ var snapshotInputSchema = external_exports.object({
|
|
|
9083
9554
|
}).passthrough();
|
|
9084
9555
|
var snapshotButtonSchema = external_exports.object({
|
|
9085
9556
|
id: boundedString(160).optional(),
|
|
9557
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9558
|
+
/**
|
|
9559
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
9560
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
9561
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
9562
|
+
* before the field existed), which must never be read as `false`.
|
|
9563
|
+
*/
|
|
9564
|
+
onScreen: external_exports.boolean().optional(),
|
|
9086
9565
|
label: boundedString(300).optional(),
|
|
9087
9566
|
disabled: external_exports.boolean().optional(),
|
|
9088
9567
|
inModal: external_exports.boolean().optional(),
|
|
@@ -9092,12 +9571,28 @@ var snapshotButtonSchema = external_exports.object({
|
|
|
9092
9571
|
}).passthrough();
|
|
9093
9572
|
var snapshotToggleSchema = external_exports.object({
|
|
9094
9573
|
id: boundedString(160).optional(),
|
|
9574
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9575
|
+
/**
|
|
9576
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
9577
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
9578
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
9579
|
+
* before the field existed), which must never be read as `false`.
|
|
9580
|
+
*/
|
|
9581
|
+
onScreen: external_exports.boolean().optional(),
|
|
9095
9582
|
label: boundedString(300).optional(),
|
|
9096
9583
|
value: external_exports.boolean().optional(),
|
|
9097
9584
|
inModal: external_exports.boolean().optional()
|
|
9098
9585
|
}).passthrough();
|
|
9099
9586
|
var snapshotSliderSchema = external_exports.object({
|
|
9100
9587
|
id: boundedString(160).optional(),
|
|
9588
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9589
|
+
/**
|
|
9590
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
9591
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
9592
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
9593
|
+
* before the field existed), which must never be read as `false`.
|
|
9594
|
+
*/
|
|
9595
|
+
onScreen: external_exports.boolean().optional(),
|
|
9101
9596
|
label: boundedString(300).optional(),
|
|
9102
9597
|
value: external_exports.number().finite().optional(),
|
|
9103
9598
|
min: external_exports.number().finite().optional(),
|
|
@@ -9138,6 +9633,26 @@ var snapshotListSchema = external_exports.object({
|
|
|
9138
9633
|
// should chase, and the relay only reads a known subset.
|
|
9139
9634
|
items: external_exports.array(external_exports.record(external_exports.unknown())).max(500).optional()
|
|
9140
9635
|
}).passthrough();
|
|
9636
|
+
var snapshotNativeDialogSchema = external_exports.object({
|
|
9637
|
+
id: boundedString(160),
|
|
9638
|
+
title: boundedString(300).optional(),
|
|
9639
|
+
message: boundedString(2e3).optional(),
|
|
9640
|
+
buttons: external_exports.array(
|
|
9641
|
+
external_exports.object({
|
|
9642
|
+
label: boundedString(160),
|
|
9643
|
+
style: external_exports.enum(["default", "cancel", "destructive"]).optional()
|
|
9644
|
+
}).passthrough()
|
|
9645
|
+
).max(10)
|
|
9646
|
+
}).passthrough();
|
|
9647
|
+
var snapshotScrollableSchema = external_exports.object({
|
|
9648
|
+
id: boundedString(160),
|
|
9649
|
+
containerType: boundedString(60).optional(),
|
|
9650
|
+
label: boundedString(300).optional(),
|
|
9651
|
+
scrollOffsetY: external_exports.number().finite().optional(),
|
|
9652
|
+
canScrollUp: external_exports.boolean().optional(),
|
|
9653
|
+
canScrollDown: external_exports.boolean().optional(),
|
|
9654
|
+
horizontal: external_exports.boolean().optional()
|
|
9655
|
+
}).passthrough();
|
|
9141
9656
|
var snapshotChoiceGroupSchema = external_exports.object({
|
|
9142
9657
|
index: external_exports.number().int().optional(),
|
|
9143
9658
|
id: boundedString(160).optional(),
|
|
@@ -9154,6 +9669,14 @@ var snapshotElementSchema = external_exports.object({
|
|
|
9154
9669
|
disabled: external_exports.boolean().optional(),
|
|
9155
9670
|
selected: external_exports.boolean().optional(),
|
|
9156
9671
|
source: boundedString(40).optional(),
|
|
9672
|
+
provenance: identityProvenanceSchema.optional(),
|
|
9673
|
+
/**
|
|
9674
|
+
* False when the control is mounted but currently OUTSIDE the window —
|
|
9675
|
+
* below the fold, scrolled off, pushed sideways. Absent means the client
|
|
9676
|
+
* cannot tell (old React Native architecture, or any SDK published
|
|
9677
|
+
* before the field existed), which must never be read as `false`.
|
|
9678
|
+
*/
|
|
9679
|
+
onScreen: external_exports.boolean().optional(),
|
|
9157
9680
|
targetId: boundedString(160).optional(),
|
|
9158
9681
|
listContext: external_exports.record(external_exports.unknown()).optional(),
|
|
9159
9682
|
inModal: external_exports.boolean().optional()
|
|
@@ -9170,7 +9693,34 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
9170
9693
|
modalOpen: external_exports.boolean().optional(),
|
|
9171
9694
|
lists: external_exports.array(snapshotListSchema).max(100).optional(),
|
|
9172
9695
|
choiceGroups: external_exports.array(snapshotChoiceGroupSchema).max(100).optional(),
|
|
9173
|
-
|
|
9696
|
+
scrollables: external_exports.array(snapshotScrollableSchema).max(20).optional(),
|
|
9697
|
+
nativeDialog: snapshotNativeDialogSchema.optional(),
|
|
9698
|
+
elements: external_exports.array(snapshotElementSchema).max(1e3).optional(),
|
|
9699
|
+
/**
|
|
9700
|
+
* The client clamped this observation to the caps above and lost
|
|
9701
|
+
* data doing it (`clampSnapshotToWireLimits` in
|
|
9702
|
+
* `@appilots/client-core`). Read by the prompt serializer so the
|
|
9703
|
+
* model is told the view is partial — a screen truncated at 500
|
|
9704
|
+
* texts must not be read as a screen that only has 500 things on
|
|
9705
|
+
* it. Optional: older SDKs never clamp and never send it.
|
|
9706
|
+
*/
|
|
9707
|
+
truncated: external_exports.boolean().optional(),
|
|
9708
|
+
/**
|
|
9709
|
+
* How many of this screen's controls the client could name, split by
|
|
9710
|
+
* `identityProvenance`. Diagnostic only — the relay never grounds an
|
|
9711
|
+
* action on it — but it is the number that says whether an app needs
|
|
9712
|
+
* annotating, and comparing it between a debug and a release build is
|
|
9713
|
+
* the first direct measurement of what minification costs the agent.
|
|
9714
|
+
*/
|
|
9715
|
+
stats: external_exports.object({
|
|
9716
|
+
visitedFibers: external_exports.number().int().nonnegative().optional(),
|
|
9717
|
+
skippedHidden: external_exports.number().int().nonnegative().optional(),
|
|
9718
|
+
identity: external_exports.object({
|
|
9719
|
+
declared: external_exports.number().int().nonnegative().optional(),
|
|
9720
|
+
derived: external_exports.number().int().nonnegative().optional(),
|
|
9721
|
+
positional: external_exports.number().int().nonnegative().optional()
|
|
9722
|
+
}).passthrough().optional()
|
|
9723
|
+
}).passthrough().optional()
|
|
9174
9724
|
}).passthrough();
|
|
9175
9725
|
var agentContextSchema = external_exports.object({
|
|
9176
9726
|
/**
|
|
@@ -9220,6 +9770,19 @@ var agentContextSchema = external_exports.object({
|
|
|
9220
9770
|
).max(200).optional(),
|
|
9221
9771
|
loadingPending: external_exports.boolean().optional()
|
|
9222
9772
|
}).passthrough();
|
|
9773
|
+
var introspectionFailureReasonSchema = external_exports.enum([
|
|
9774
|
+
"sentinel-missing-internals",
|
|
9775
|
+
"never-mounted",
|
|
9776
|
+
"render-crashed",
|
|
9777
|
+
"names-mangled",
|
|
9778
|
+
"walk-recognized-nothing"
|
|
9779
|
+
]);
|
|
9780
|
+
var introspectionDiagnosticsSchema = external_exports.object({
|
|
9781
|
+
captured: external_exports.boolean(),
|
|
9782
|
+
failureReason: introspectionFailureReasonSchema.nullish(),
|
|
9783
|
+
reactVersion: boundedString(32).nullish(),
|
|
9784
|
+
failureCount: external_exports.number().int().min(0).max(1e6).optional()
|
|
9785
|
+
});
|
|
9223
9786
|
var sendMessageSchema = external_exports.object({
|
|
9224
9787
|
content: external_exports.string().min(1).max(4096),
|
|
9225
9788
|
sessionId: external_exports.string().optional(),
|
|
@@ -9229,7 +9792,20 @@ var sendMessageSchema = external_exports.object({
|
|
|
9229
9792
|
* the active MCP doc and emits `mcp_version_mismatch` telemetry when
|
|
9230
9793
|
* they differ. Optional — older SDKs simply skip the check.
|
|
9231
9794
|
*/
|
|
9232
|
-
mcpVersion: external_exports.string().max(64).optional()
|
|
9795
|
+
mcpVersion: external_exports.string().max(64).optional(),
|
|
9796
|
+
/**
|
|
9797
|
+
* SDK-side introspection health. Only sent when it FAILED — a client
|
|
9798
|
+
* that reached the React tree omits the field entirely, so the happy
|
|
9799
|
+
* path costs nothing on the wire.
|
|
9800
|
+
*
|
|
9801
|
+
* Without this, an app whose introspection is dead sends a snapshot
|
|
9802
|
+
* that is empty but perfectly well-formed, indistinguishable from a
|
|
9803
|
+
* genuinely empty screen. The server emits
|
|
9804
|
+
* `introspection_unavailable` so the failure is visible to us instead
|
|
9805
|
+
* of waiting for a bug report. Optional — older SDKs simply never
|
|
9806
|
+
* send it.
|
|
9807
|
+
*/
|
|
9808
|
+
introspection: introspectionDiagnosticsSchema.optional()
|
|
9233
9809
|
});
|
|
9234
9810
|
var agentActionTypeSchema = external_exports.enum([
|
|
9235
9811
|
"navigate",
|
|
@@ -9240,11 +9816,22 @@ var agentActionTypeSchema = external_exports.enum([
|
|
|
9240
9816
|
"custom"
|
|
9241
9817
|
]);
|
|
9242
9818
|
var navigationPayloadSchema = external_exports.object({
|
|
9243
|
-
|
|
9819
|
+
// Optional ONLY so `goBack` can exist (#398): it returns to whatever
|
|
9820
|
+
// is beneath on the stack and has no destination to name. Every other
|
|
9821
|
+
// action is still required to carry one — enforced by the refine
|
|
9822
|
+
// below rather than by the field, so the error names the real problem
|
|
9823
|
+
// ("navigate needs a screenName") instead of a missing key.
|
|
9824
|
+
screenName: external_exports.string().min(1).optional(),
|
|
9244
9825
|
params: external_exports.record(external_exports.unknown()).optional(),
|
|
9245
9826
|
navigationAction: external_exports.enum(["push", "navigate", "replace", "goBack", "reset"]).default("navigate").optional(),
|
|
9246
9827
|
path: external_exports.array(external_exports.string().min(1)).optional()
|
|
9247
|
-
}).passthrough()
|
|
9828
|
+
}).passthrough().refine(
|
|
9829
|
+
(payload) => payload.navigationAction === "goBack" || typeof payload.screenName === "string" && payload.screenName.length > 0,
|
|
9830
|
+
{
|
|
9831
|
+
message: 'screenName is required for every navigationAction except "goBack"',
|
|
9832
|
+
path: ["screenName"]
|
|
9833
|
+
}
|
|
9834
|
+
);
|
|
9248
9835
|
var formFillFieldSchema = external_exports.object({
|
|
9249
9836
|
fieldId: external_exports.string().min(1),
|
|
9250
9837
|
fieldType: external_exports.enum(["text", "select", "toggle", "date", "number", "custom"]).default("text").optional(),
|
|
@@ -9578,6 +10165,8 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9578
10165
|
inputs: external_exports.array(
|
|
9579
10166
|
external_exports.object({
|
|
9580
10167
|
id: external_exports.string().max(120),
|
|
10168
|
+
provenance: identityProvenanceSchema.optional(),
|
|
10169
|
+
onScreen: external_exports.boolean().optional(),
|
|
9581
10170
|
label: external_exports.string().max(200).optional(),
|
|
9582
10171
|
value: external_exports.string().max(2e3).optional(),
|
|
9583
10172
|
disabled: external_exports.boolean().optional(),
|
|
@@ -9591,6 +10180,8 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9591
10180
|
buttons: external_exports.array(
|
|
9592
10181
|
external_exports.object({
|
|
9593
10182
|
id: external_exports.string().max(120),
|
|
10183
|
+
provenance: identityProvenanceSchema.optional(),
|
|
10184
|
+
onScreen: external_exports.boolean().optional(),
|
|
9594
10185
|
label: external_exports.string().max(200).optional(),
|
|
9595
10186
|
disabled: external_exports.boolean().optional(),
|
|
9596
10187
|
/** Risk metadata (e.g. "high") the SDK forwards for danger UI. */
|
|
@@ -9612,6 +10203,8 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9612
10203
|
sliders: external_exports.array(
|
|
9613
10204
|
external_exports.object({
|
|
9614
10205
|
id: external_exports.string().max(120),
|
|
10206
|
+
provenance: identityProvenanceSchema.optional(),
|
|
10207
|
+
onScreen: external_exports.boolean().optional(),
|
|
9615
10208
|
label: external_exports.string().max(200).optional(),
|
|
9616
10209
|
value: external_exports.number().finite().optional(),
|
|
9617
10210
|
min: external_exports.number().finite().optional(),
|
|
@@ -9641,6 +10234,44 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9641
10234
|
).max(200).optional()
|
|
9642
10235
|
}).passthrough()
|
|
9643
10236
|
).max(20).optional(),
|
|
10237
|
+
/**
|
|
10238
|
+
* Toggles exactly as the SDK snapshot reports them (#390).
|
|
10239
|
+
*
|
|
10240
|
+
* Their absence is why `generality-component-control-smarthome`
|
|
10241
|
+
* came back `errored` with a 422 instead of measuring anything:
|
|
10242
|
+
* this schema is `.strict()`, so a scenario that declares a switch
|
|
10243
|
+
* is rejected before the agent is ever asked. Which means the
|
|
10244
|
+
* corpus could not express the single control behind failure 02 of
|
|
10245
|
+
* the functional audit — "disable an agent; the switch never
|
|
10246
|
+
* moves". The one defect the eval most needed to catch was the one
|
|
10247
|
+
* shape it could not describe.
|
|
10248
|
+
*/
|
|
10249
|
+
toggles: external_exports.array(
|
|
10250
|
+
external_exports.object({
|
|
10251
|
+
id: external_exports.string().max(120),
|
|
10252
|
+
provenance: identityProvenanceSchema.optional(),
|
|
10253
|
+
onScreen: external_exports.boolean().optional(),
|
|
10254
|
+
label: external_exports.string().max(200).optional(),
|
|
10255
|
+
value: external_exports.boolean().optional(),
|
|
10256
|
+
disabled: external_exports.boolean().optional()
|
|
10257
|
+
})
|
|
10258
|
+
).max(100).optional(),
|
|
10259
|
+
/**
|
|
10260
|
+
* The interaction graph — the `el:*` ids the prompt tells the model
|
|
10261
|
+
* to prefer over ordinal handles. Passthrough per entry for the
|
|
10262
|
+
* same reason `lists` is: the SDK adds fields faster than this
|
|
10263
|
+
* schema should chase them.
|
|
10264
|
+
*/
|
|
10265
|
+
elements: external_exports.array(
|
|
10266
|
+
external_exports.object({
|
|
10267
|
+
id: external_exports.string().max(160),
|
|
10268
|
+
role: external_exports.string().max(40).optional(),
|
|
10269
|
+
label: external_exports.string().max(300).optional()
|
|
10270
|
+
}).passthrough()
|
|
10271
|
+
).max(500).optional(),
|
|
10272
|
+
/** Screen-level state the relay renders and branches on. */
|
|
10273
|
+
loading: external_exports.boolean().optional(),
|
|
10274
|
+
modalOpen: external_exports.boolean().optional(),
|
|
9644
10275
|
extra: external_exports.record(external_exports.unknown()).optional()
|
|
9645
10276
|
}).strict();
|
|
9646
10277
|
var sandboxRunRequestSchema = external_exports.object({
|
|
@@ -9681,13 +10312,29 @@ var sandboxRunRequestSchema = external_exports.object({
|
|
|
9681
10312
|
* heuristics, forces tool choice when the last user message carries a
|
|
9682
10313
|
* failure marker). Only meaningful alongside `history`.
|
|
9683
10314
|
*/
|
|
9684
|
-
isRecoveryHop: external_exports.boolean().optional()
|
|
10315
|
+
isRecoveryHop: external_exports.boolean().optional(),
|
|
10316
|
+
/**
|
|
10317
|
+
* Run this call against the application structure supplied here
|
|
10318
|
+
* instead of the project's active MCP document.
|
|
10319
|
+
*
|
|
10320
|
+
* Sandbox-only, and the relay honours it only on a `dryRun` call —
|
|
10321
|
+
* see `resolveMcpOverride` in apps/api. It exists because the eval
|
|
10322
|
+
* corpus could otherwise only ever describe ONE app: every scenario
|
|
10323
|
+
* resolves to the same seeded project, so every navigate destination,
|
|
10324
|
+
* every screen title and every bit of domain priming in the prompt
|
|
10325
|
+
* came from the same vehicle-management demo. A suite that can only
|
|
10326
|
+
* pose questions about one app cannot measure whether the agent works
|
|
10327
|
+
* on another one.
|
|
10328
|
+
*
|
|
10329
|
+
* Deliberately loose (`z.record`): the MCP document's shape is owned
|
|
10330
|
+
* by the generator, and pinning it here would turn every additive
|
|
10331
|
+
* generator change into a 422 in the sandbox.
|
|
10332
|
+
*/
|
|
10333
|
+
applicationStructure: external_exports.record(external_exports.unknown()).optional()
|
|
9685
10334
|
}).strict();
|
|
9686
10335
|
var relayDiagnosticsSchema = external_exports.object({
|
|
9687
10336
|
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
|
-
),
|
|
10337
|
+
guardsFired: external_exports.array(external_exports.object({ guard: external_exports.string(), detail: external_exports.string().optional() })),
|
|
9691
10338
|
repairMode: external_exports.enum(["repair", "strict", "disabled"]),
|
|
9692
10339
|
promptVersion: external_exports.string(),
|
|
9693
10340
|
// Defaulted for producers that predate the field: before it existed
|
|
@@ -9946,6 +10593,30 @@ var identifyUserSchema = external_exports.object({
|
|
|
9946
10593
|
name: external_exports.string().min(1).max(200).optional(),
|
|
9947
10594
|
identifiers: endUserIdentifiersSchema.optional()
|
|
9948
10595
|
});
|
|
10596
|
+
var MAX_EXTERNAL_USER_ID_LENGTH = 128;
|
|
10597
|
+
function normalizeExternalUserId(value) {
|
|
10598
|
+
let raw;
|
|
10599
|
+
if (typeof value === "string") raw = value;
|
|
10600
|
+
else if (typeof value === "number" && Number.isFinite(value)) raw = String(value);
|
|
10601
|
+
else return void 0;
|
|
10602
|
+
const trimmed = raw.trim();
|
|
10603
|
+
if (!trimmed) return void 0;
|
|
10604
|
+
if (trimmed.length > MAX_EXTERNAL_USER_ID_LENGTH) return void 0;
|
|
10605
|
+
return trimmed;
|
|
10606
|
+
}
|
|
10607
|
+
var MAX_DEVICE_INFO_BYTES = 4096;
|
|
10608
|
+
var sessionDeviceInfoSchema = external_exports.object({
|
|
10609
|
+
platform: external_exports.string().max(64).optional(),
|
|
10610
|
+
osVersion: external_exports.string().max(64).optional(),
|
|
10611
|
+
appVersion: external_exports.string().max(64).optional(),
|
|
10612
|
+
sdkVersion: external_exports.string().max(64).optional()
|
|
10613
|
+
}).passthrough().refine((info2) => JSON.stringify(info2).length <= MAX_DEVICE_INFO_BYTES, {
|
|
10614
|
+
message: `device_info exceeds ${MAX_DEVICE_INFO_BYTES} bytes`
|
|
10615
|
+
});
|
|
10616
|
+
var createAgentSessionSchema = external_exports.object({
|
|
10617
|
+
userId: external_exports.preprocess(normalizeExternalUserId, external_exports.string().optional()),
|
|
10618
|
+
deviceInfo: sessionDeviceInfoSchema.optional().catch(void 0)
|
|
10619
|
+
});
|
|
9949
10620
|
var upsertEndUserSchema = external_exports.object({
|
|
9950
10621
|
projectId: external_exports.string().min(1).max(64),
|
|
9951
10622
|
externalId: external_exports.string().min(1).max(128),
|
|
@@ -9969,14 +10640,16 @@ async function loadManifest(rootDir, manifestPath) {
|
|
|
9969
10640
|
if (error2?.code === "ENOENT") {
|
|
9970
10641
|
return { manifest: null, resolvedPath };
|
|
9971
10642
|
}
|
|
9972
|
-
throw new Error(
|
|
10643
|
+
throw new Error(
|
|
10644
|
+
`Failed to read Appilots manifest at ${resolvedPath}: ${stringifyUnknown(error2)}`
|
|
10645
|
+
);
|
|
9973
10646
|
}
|
|
9974
10647
|
let parsedJson;
|
|
9975
10648
|
try {
|
|
9976
10649
|
parsedJson = JSON.parse(raw);
|
|
9977
10650
|
} catch (error2) {
|
|
9978
10651
|
throw new Error(
|
|
9979
|
-
`Appilots manifest at ${resolvedPath} is not valid JSON: ${
|
|
10652
|
+
`Appilots manifest at ${resolvedPath} is not valid JSON: ${stringifyUnknown(error2)}`
|
|
9980
10653
|
);
|
|
9981
10654
|
}
|
|
9982
10655
|
const result = manifestSchema.safeParse(parsedJson);
|
|
@@ -9990,12 +10663,48 @@ Each entry in "screens" must match the ScreenDescriptor shape (see docs/platform
|
|
|
9990
10663
|
}
|
|
9991
10664
|
return { manifest: result.data, resolvedPath };
|
|
9992
10665
|
}
|
|
10666
|
+
function mergeScreen(base, declared) {
|
|
10667
|
+
const fields = { ...base };
|
|
10668
|
+
for (const [key, value] of Object.entries(declared)) {
|
|
10669
|
+
if (value === void 0) continue;
|
|
10670
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
10671
|
+
fields[key] = value;
|
|
10672
|
+
}
|
|
10673
|
+
const merged = fields;
|
|
10674
|
+
merged.targets = mergeById(base.targets, markManifestOrigin(declared.targets));
|
|
10675
|
+
merged.actions = mergeById(base.actions, declared.actions) ?? [];
|
|
10676
|
+
if (base.agentHints || declared.agentHints) {
|
|
10677
|
+
merged.agentHints = { ...base.agentHints, ...declared.agentHints };
|
|
10678
|
+
}
|
|
10679
|
+
return merged;
|
|
10680
|
+
}
|
|
10681
|
+
function mergeById(base, overlay) {
|
|
10682
|
+
if (!overlay || overlay.length === 0) return base;
|
|
10683
|
+
if (!base || base.length === 0) return overlay;
|
|
10684
|
+
const byId = /* @__PURE__ */ new Map();
|
|
10685
|
+
for (const entry of base) byId.set(entry.id, entry);
|
|
10686
|
+
for (const entry of overlay) {
|
|
10687
|
+
const existing = byId.get(entry.id);
|
|
10688
|
+
byId.set(entry.id, existing ? { ...existing, ...entry } : entry);
|
|
10689
|
+
}
|
|
10690
|
+
return Array.from(byId.values());
|
|
10691
|
+
}
|
|
10692
|
+
function markManifestOrigin(targets) {
|
|
10693
|
+
if (!targets || targets.length === 0) return targets;
|
|
10694
|
+
return targets.map((target) => ({ origin: "manifest", ...target }));
|
|
10695
|
+
}
|
|
9993
10696
|
function mergeManifestScreens(analyzerScreens, manifestScreens) {
|
|
9994
10697
|
if (!manifestScreens || manifestScreens.length === 0) return analyzerScreens;
|
|
9995
10698
|
const byName = /* @__PURE__ */ new Map();
|
|
9996
10699
|
for (const screen of analyzerScreens) byName.set(screen.name, screen);
|
|
9997
|
-
for (const
|
|
9998
|
-
|
|
10700
|
+
for (const declared of manifestScreens) {
|
|
10701
|
+
const base = byName.get(declared.name);
|
|
10702
|
+
byName.set(
|
|
10703
|
+
declared.name,
|
|
10704
|
+
base ? mergeScreen(base, declared) : { ...declared, targets: markManifestOrigin(declared.targets) }
|
|
10705
|
+
);
|
|
10706
|
+
}
|
|
10707
|
+
return Array.from(byName.values());
|
|
9999
10708
|
}
|
|
10000
10709
|
function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
|
|
10001
10710
|
if (!manifestNavigation) return analyzerNavigation;
|
|
@@ -10069,7 +10778,12 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10069
10778
|
}
|
|
10070
10779
|
const mergedScreens = mergeManifestScreens(analyzed.screens, manifest?.screens);
|
|
10071
10780
|
const navigation = mergeManifestNavigation(analyzed.navigation, manifest?.navigation);
|
|
10072
|
-
const agentReadyScreens = mergedScreens.map(
|
|
10781
|
+
const agentReadyScreens = mergedScreens.map(
|
|
10782
|
+
(screen) => enrichScreenForAgent({
|
|
10783
|
+
...screen,
|
|
10784
|
+
filePath: screen.filePath ? import_node_path3.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
|
|
10785
|
+
})
|
|
10786
|
+
);
|
|
10073
10787
|
const projectInfo = await this.getProjectInfo();
|
|
10074
10788
|
const projectName = projectInfo.name;
|
|
10075
10789
|
console.log(`[MCPGenerator] Project name: ${projectName}`);
|
|
@@ -10092,10 +10806,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10092
10806
|
};
|
|
10093
10807
|
const serialized = JSON.stringify(document, null, 2);
|
|
10094
10808
|
const checksum = this.calculateChecksum(serialized);
|
|
10095
|
-
const filePath = import_node_path3.default.resolve(
|
|
10096
|
-
outputDir,
|
|
10097
|
-
`mcp-document.${this.options.format}`
|
|
10098
|
-
);
|
|
10809
|
+
const filePath = import_node_path3.default.resolve(outputDir, `mcp-document.${this.options.format}`);
|
|
10099
10810
|
await (0, import_promises4.writeFile)(filePath, serialized, "utf-8");
|
|
10100
10811
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
10101
10812
|
const checksumFilePath = import_node_path3.default.resolve(outputDir, ".appilots-checksum");
|
|
@@ -10211,14 +10922,136 @@ function reportMetadataLint(document, strict) {
|
|
|
10211
10922
|
}
|
|
10212
10923
|
}
|
|
10213
10924
|
|
|
10925
|
+
// src/cli/utils/remote-drift.ts
|
|
10926
|
+
var import_node_fs = require("fs");
|
|
10927
|
+
var import_node_path4 = require("path");
|
|
10928
|
+
var STUB_RATIO = 0.5;
|
|
10929
|
+
function compareWithRemote(local, remote) {
|
|
10930
|
+
if (remote.screensCount === local.screens) return null;
|
|
10931
|
+
return {
|
|
10932
|
+
localScreens: local.screens,
|
|
10933
|
+
remoteScreens: remote.screensCount,
|
|
10934
|
+
remoteId: remote.id,
|
|
10935
|
+
severe: local.screens > 0 && remote.screensCount <= local.screens * STUB_RATIO
|
|
10936
|
+
};
|
|
10937
|
+
}
|
|
10938
|
+
function readLocalDocumentSummary(outputDir) {
|
|
10939
|
+
try {
|
|
10940
|
+
const raw = (0, import_node_fs.readFileSync)((0, import_node_path4.join)(outputDir, "mcp-document.json"), "utf-8");
|
|
10941
|
+
const parsed = JSON.parse(raw);
|
|
10942
|
+
if (typeof parsed?.metadata?.totalScreens !== "number") return null;
|
|
10943
|
+
return { screens: parsed.metadata.totalScreens };
|
|
10944
|
+
} catch {
|
|
10945
|
+
return null;
|
|
10946
|
+
}
|
|
10947
|
+
}
|
|
10948
|
+
function reportRemoteDrift(drift, options = {}) {
|
|
10949
|
+
if (!drift.severe) {
|
|
10950
|
+
info(
|
|
10951
|
+
`Server's active document differs from what was just generated (active: ${drift.remoteId}, ${drift.remoteScreens} screens).`
|
|
10952
|
+
);
|
|
10953
|
+
return;
|
|
10954
|
+
}
|
|
10955
|
+
warn(
|
|
10956
|
+
`The agent is ${options.uploadFailed ? "still " : ""}running on a ${drift.remoteScreens}-screen document while this app has ${drift.localScreens} screens.`
|
|
10957
|
+
);
|
|
10958
|
+
info(` Active on server: ${drift.remoteId} (${drift.remoteScreens} screens)`);
|
|
10959
|
+
info(` Just generated: ${drift.localScreens} screens`);
|
|
10960
|
+
info("");
|
|
10961
|
+
info("While a document that small is active the agent cannot see the other");
|
|
10962
|
+
info("screens, and missions that need them fail in ways that read as model");
|
|
10963
|
+
info("errors. The usual causes: an older version activated from the");
|
|
10964
|
+
info("dashboard, a sync that went to a different server, or a database");
|
|
10965
|
+
info("seeded before the seed learned to refuse partial documents.");
|
|
10966
|
+
info("");
|
|
10967
|
+
if (options.uploadFailed) {
|
|
10968
|
+
info("Nothing was uploaded just now, so this is unchanged. Fix the error");
|
|
10969
|
+
info("above and run sync again \u2014 e2e runs before that will fail on");
|
|
10970
|
+
info("navigation, not on anything the model did.");
|
|
10971
|
+
} else {
|
|
10972
|
+
info("Uploading now replaces it.");
|
|
10973
|
+
}
|
|
10974
|
+
}
|
|
10975
|
+
|
|
10976
|
+
// src/cli/utils/upload-failure.ts
|
|
10977
|
+
function explainUploadFailure(failure, serverUrl) {
|
|
10978
|
+
const status = failure.errorStatus ?? 0;
|
|
10979
|
+
if (status === 401) {
|
|
10980
|
+
info("");
|
|
10981
|
+
info("`sync` authenticates with the project API key (`ak_\u2026`) \u2014 the same key");
|
|
10982
|
+
info("the SDK uses. Check that it is the key for THIS project, is still");
|
|
10983
|
+
info("active, and reaches the server you meant:");
|
|
10984
|
+
info(` Server: ${serverUrl ?? "(default)"}`);
|
|
10985
|
+
info("Set it in .appilotsrc (`apiKey`), via APPILOTS_API_KEY, or --api-key.");
|
|
10986
|
+
info("");
|
|
10987
|
+
info("A dashboard login token will not work here and is not needed: the CLI");
|
|
10988
|
+
info("talks to /cli/sync, not the dashboard's /mcp-documents.");
|
|
10989
|
+
return;
|
|
10990
|
+
}
|
|
10991
|
+
if (failure.errorCode === "WORKSPACE_INACTIVE") {
|
|
10992
|
+
info("");
|
|
10993
|
+
info("The workspace is deactivated. Re-activate it in the dashboard");
|
|
10994
|
+
info("(Settings \u2192 Workspace) and run sync again.");
|
|
10995
|
+
return;
|
|
10996
|
+
}
|
|
10997
|
+
if (status === 404) {
|
|
10998
|
+
info("");
|
|
10999
|
+
info(`No /cli/sync at ${serverUrl ?? "(default)"} \u2014 check serverUrl points at`);
|
|
11000
|
+
info("an Appilots API (and one recent enough to serve the CLI routes).");
|
|
11001
|
+
return;
|
|
11002
|
+
}
|
|
11003
|
+
if (status === 422) {
|
|
11004
|
+
info("");
|
|
11005
|
+
info("The server rejected the document itself. Inspect what was generated:");
|
|
11006
|
+
info(" .appilots/mcp-document.json");
|
|
11007
|
+
return;
|
|
11008
|
+
}
|
|
11009
|
+
if (status >= 500) {
|
|
11010
|
+
info("");
|
|
11011
|
+
info("The server failed to process the upload. It was retried already \u2014");
|
|
11012
|
+
info("if it persists, the API logs hold the matching request id.");
|
|
11013
|
+
}
|
|
11014
|
+
}
|
|
11015
|
+
|
|
11016
|
+
// src/cli/utils/empty-document.ts
|
|
11017
|
+
function assertDocumentHasScreens(document, options = {}) {
|
|
11018
|
+
if ((document.metadata?.totalScreens ?? 0) > 0) return;
|
|
11019
|
+
if (options.allowEmpty) {
|
|
11020
|
+
info("No screens found, continuing because --allow-empty was passed.");
|
|
11021
|
+
return;
|
|
11022
|
+
}
|
|
11023
|
+
error("No screens found \u2014 this application map would give the agent nothing to navigate.");
|
|
11024
|
+
info("");
|
|
11025
|
+
info("The analyzer looks for React Navigation: a <NavigationContainer> and");
|
|
11026
|
+
info("<Stack.Screen> / <Tab.Screen> declarations. It found none.");
|
|
11027
|
+
info("");
|
|
11028
|
+
info("The two usual causes:");
|
|
11029
|
+
info(" \u2022 The app uses Expo Router (or another file-based router). It is the");
|
|
11030
|
+
info(" default for `npx create-expo-app` and the analyzer cannot read it.");
|
|
11031
|
+
info(" \u2022 The screens live outside the analyzed paths \u2014 check `include`,");
|
|
11032
|
+
info(" `exclude` and `screenPatterns` in your Appilots config.");
|
|
11033
|
+
info("");
|
|
11034
|
+
info("The escape hatch, which works for any router: declare your screens in");
|
|
11035
|
+
info("an `appilots.manifest.json` at the project root. Its screens are merged");
|
|
11036
|
+
info("on top of whatever the analyzer found, so a fully declared app needs no");
|
|
11037
|
+
info("React Navigation at all. See the CLI docs for the shape.");
|
|
11038
|
+
info("");
|
|
11039
|
+
info("If an empty map really is what you want, pass --allow-empty.");
|
|
11040
|
+
process.exit(1);
|
|
11041
|
+
}
|
|
11042
|
+
|
|
10214
11043
|
// src/cli/commands/sync.ts
|
|
10215
11044
|
function syncCommand() {
|
|
10216
|
-
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(
|
|
11045
|
+
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(
|
|
11046
|
+
"--strict-metadata",
|
|
11047
|
+
"Fail when mutating actions are missing effect/riskLevel metadata",
|
|
11048
|
+
false
|
|
11049
|
+
).option("--allow-empty", "Do not fail when the application map has zero screens", false).action(async (options) => {
|
|
10217
11050
|
if (options.json) setLogMode("json");
|
|
10218
11051
|
else if (options.quiet) setLogMode("quiet");
|
|
10219
11052
|
try {
|
|
10220
11053
|
banner();
|
|
10221
|
-
const loaded = loadConfig();
|
|
11054
|
+
const loaded = loadConfig(warn);
|
|
10222
11055
|
const apiKey = options.apiKey || loaded?.apiKey;
|
|
10223
11056
|
if (!apiKey) {
|
|
10224
11057
|
error("No .appilotsrc configuration found");
|
|
@@ -10229,7 +11062,7 @@ function syncCommand() {
|
|
|
10229
11062
|
return;
|
|
10230
11063
|
}
|
|
10231
11064
|
const config = {
|
|
10232
|
-
serverUrl:
|
|
11065
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
10233
11066
|
outputDir: ".appilots",
|
|
10234
11067
|
...loaded,
|
|
10235
11068
|
apiKey,
|
|
@@ -10243,7 +11076,7 @@ function syncCommand() {
|
|
|
10243
11076
|
const outputDir = config.outputDir || ".appilots";
|
|
10244
11077
|
let previousChecksum = "";
|
|
10245
11078
|
try {
|
|
10246
|
-
const checksumPath = (0,
|
|
11079
|
+
const checksumPath = (0, import_node_path5.join)(outputDir, ".appilots-checksum");
|
|
10247
11080
|
previousChecksum = (await (0, import_promises5.readFile)(checksumPath, "utf-8")).trim();
|
|
10248
11081
|
} catch {
|
|
10249
11082
|
}
|
|
@@ -10264,22 +11097,32 @@ function syncCommand() {
|
|
|
10264
11097
|
});
|
|
10265
11098
|
const output = await generator.generate();
|
|
10266
11099
|
spinner.stop();
|
|
11100
|
+
assertDocumentHasScreens(output.document, {
|
|
11101
|
+
rootDir: process.cwd(),
|
|
11102
|
+
allowEmpty: Boolean(options.allowEmpty)
|
|
11103
|
+
});
|
|
10267
11104
|
success(
|
|
10268
11105
|
`Found ${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
|
|
10269
11106
|
);
|
|
10270
11107
|
reportMetadataLint(output.document, Boolean(options.strictMetadata));
|
|
11108
|
+
spinner = createSpinner("Checking remote status...");
|
|
11109
|
+
spinner.start();
|
|
11110
|
+
const remote = await apiClient.status();
|
|
11111
|
+
spinner.stop();
|
|
11112
|
+
const remoteActive = remote.error ? null : remote.activeMcp ?? null;
|
|
11113
|
+
const drift = remoteActive ? compareWithRemote({ screens: output.document.metadata.totalScreens }, remoteActive) : null;
|
|
10271
11114
|
if (!options.force && previousChecksum && previousChecksum === output.checksum) {
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
10277
|
-
|
|
11115
|
+
if (remote.error) {
|
|
11116
|
+
warn(`Could not verify remote: ${remote.error}`);
|
|
11117
|
+
info("Uploading anyway...");
|
|
11118
|
+
} else if (remoteActive) {
|
|
11119
|
+
if (!drift) {
|
|
11120
|
+
success("Already up to date (no local or remote changes)");
|
|
11121
|
+
jsonOutput({ success: true, unchanged: true, checksum: output.checksum });
|
|
11122
|
+
return;
|
|
11123
|
+
}
|
|
11124
|
+
reportRemoteDrift(drift);
|
|
10278
11125
|
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
11126
|
}
|
|
10284
11127
|
}
|
|
10285
11128
|
spinner = createSpinner("Uploading to Appilots...");
|
|
@@ -10292,13 +11135,33 @@ function syncCommand() {
|
|
|
10292
11135
|
spinner.stop();
|
|
10293
11136
|
if (!syncResult.success) {
|
|
10294
11137
|
error(`Sync failed: ${syncResult.error}`);
|
|
10295
|
-
|
|
11138
|
+
hintUnreachableServer(config.serverUrl, syncResult.error);
|
|
11139
|
+
explainUploadFailure(syncResult, config.serverUrl);
|
|
11140
|
+
if (drift?.severe) {
|
|
11141
|
+
reportRemoteDrift(drift, { uploadFailed: true });
|
|
11142
|
+
}
|
|
11143
|
+
jsonOutput({
|
|
11144
|
+
success: false,
|
|
11145
|
+
error: syncResult.error,
|
|
11146
|
+
errorCode: syncResult.errorCode,
|
|
11147
|
+
errorStatus: syncResult.errorStatus,
|
|
11148
|
+
activeDocument: remoteActive,
|
|
11149
|
+
drift
|
|
11150
|
+
});
|
|
10296
11151
|
process.exit(1);
|
|
10297
11152
|
}
|
|
10298
11153
|
if (syncResult.unchanged && !options.force) {
|
|
10299
11154
|
success("Already up to date on server");
|
|
11155
|
+
if (syncResult.activated) {
|
|
11156
|
+
info(`Re-activated document ${syncResult.id} (it was stored but not active).`);
|
|
11157
|
+
}
|
|
10300
11158
|
} else {
|
|
10301
11159
|
success("Successfully synced MCP document");
|
|
11160
|
+
if (drift?.severe) {
|
|
11161
|
+
info(
|
|
11162
|
+
`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.`
|
|
11163
|
+
);
|
|
11164
|
+
}
|
|
10302
11165
|
info(`Document ID: ${syncResult.id}`);
|
|
10303
11166
|
info(`Checksum: ${syncResult.checksum}`);
|
|
10304
11167
|
if (config.projectId) {
|
|
@@ -10328,8 +11191,8 @@ function syncCommand() {
|
|
|
10328
11191
|
actionsCount: syncResult.actionsCount ?? output.document.metadata.totalActions
|
|
10329
11192
|
});
|
|
10330
11193
|
} catch (err) {
|
|
10331
|
-
error(`Sync failed: ${
|
|
10332
|
-
jsonOutput({ success: false, error:
|
|
11194
|
+
error(`Sync failed: ${stringifyUnknown(err)}`);
|
|
11195
|
+
jsonOutput({ success: false, error: stringifyUnknown(err) });
|
|
10333
11196
|
if (options.verbose) {
|
|
10334
11197
|
console.error(err);
|
|
10335
11198
|
}
|
|
@@ -10340,12 +11203,12 @@ function syncCommand() {
|
|
|
10340
11203
|
|
|
10341
11204
|
// src/cli/commands/watch.ts
|
|
10342
11205
|
var import_commander3 = require("commander");
|
|
10343
|
-
var
|
|
11206
|
+
var import_node_fs2 = require("fs");
|
|
10344
11207
|
function watchCommand() {
|
|
10345
11208
|
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
11209
|
try {
|
|
10347
11210
|
banner();
|
|
10348
|
-
const config = loadConfig();
|
|
11211
|
+
const config = loadConfig(warn);
|
|
10349
11212
|
if (!config) {
|
|
10350
11213
|
error("No .appilotsrc configuration found");
|
|
10351
11214
|
info("Run: appilots init --api-key <your-key>");
|
|
@@ -10374,7 +11237,10 @@ function watchCommand() {
|
|
|
10374
11237
|
};
|
|
10375
11238
|
const generator = new MCPGenerator(generatorConfig);
|
|
10376
11239
|
const initialOutput = await generator.generate();
|
|
10377
|
-
const initialSync = await apiClient.sync(
|
|
11240
|
+
const initialSync = await apiClient.sync(
|
|
11241
|
+
initialOutput.document,
|
|
11242
|
+
initialOutput.document.version
|
|
11243
|
+
);
|
|
10378
11244
|
if (!initialSync.success) {
|
|
10379
11245
|
error(`Initial sync failed: ${initialSync.error}`);
|
|
10380
11246
|
process.exit(1);
|
|
@@ -10384,45 +11250,41 @@ function watchCommand() {
|
|
|
10384
11250
|
dim("Press Ctrl+C to stop\n");
|
|
10385
11251
|
let debounceTimer = null;
|
|
10386
11252
|
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);
|
|
11253
|
+
const watcher = (0, import_node_fs2.watch)(process.cwd(), { recursive: true }, async (eventType, filename) => {
|
|
11254
|
+
if (!filename) return;
|
|
11255
|
+
if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
|
|
11256
|
+
return;
|
|
10424
11257
|
}
|
|
10425
|
-
|
|
11258
|
+
if (debounceTimer) {
|
|
11259
|
+
clearTimeout(debounceTimer);
|
|
11260
|
+
}
|
|
11261
|
+
debounceTimer = setTimeout(async () => {
|
|
11262
|
+
try {
|
|
11263
|
+
if (options.verbose) {
|
|
11264
|
+
info(`Change detected: ${filename}`);
|
|
11265
|
+
}
|
|
11266
|
+
const spinner = createSpinner("Re-analyzing project...");
|
|
11267
|
+
spinner.start();
|
|
11268
|
+
const newGenerator = new MCPGenerator(generatorConfig);
|
|
11269
|
+
const output = await newGenerator.generate();
|
|
11270
|
+
spinner.text = "Uploading changes...";
|
|
11271
|
+
const syncResult = await apiClient.sync(output.document, output.document.version);
|
|
11272
|
+
spinner.stop();
|
|
11273
|
+
if (!syncResult.success) {
|
|
11274
|
+
error(`Sync failed: ${syncResult.error}`);
|
|
11275
|
+
} else if (syncResult.unchanged) {
|
|
11276
|
+
info("Changes synced (no content changes detected)");
|
|
11277
|
+
} else {
|
|
11278
|
+
success("Changes synced successfully");
|
|
11279
|
+
info(
|
|
11280
|
+
`${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
|
|
11281
|
+
);
|
|
11282
|
+
}
|
|
11283
|
+
} catch (err) {
|
|
11284
|
+
error(`Watch sync failed: ${stringifyUnknown(err)}`);
|
|
11285
|
+
}
|
|
11286
|
+
}, DEBOUNCE_MS);
|
|
11287
|
+
});
|
|
10426
11288
|
process.on("SIGINT", () => {
|
|
10427
11289
|
info("Stopping watcher...");
|
|
10428
11290
|
watcher.close();
|
|
@@ -10432,7 +11294,7 @@ function watchCommand() {
|
|
|
10432
11294
|
process.exit(0);
|
|
10433
11295
|
});
|
|
10434
11296
|
} catch (err) {
|
|
10435
|
-
error(`Watch failed: ${
|
|
11297
|
+
error(`Watch failed: ${stringifyUnknown(err)}`);
|
|
10436
11298
|
process.exit(1);
|
|
10437
11299
|
}
|
|
10438
11300
|
});
|
|
@@ -10441,7 +11303,11 @@ function watchCommand() {
|
|
|
10441
11303
|
// src/cli/commands/generate.ts
|
|
10442
11304
|
var import_commander4 = require("commander");
|
|
10443
11305
|
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(
|
|
11306
|
+
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(
|
|
11307
|
+
"--strict-metadata",
|
|
11308
|
+
"Fail when mutating actions are missing effect/riskLevel metadata",
|
|
11309
|
+
false
|
|
11310
|
+
).option("--allow-empty", "Do not fail when the application map has zero screens", false).action(async (options) => {
|
|
10445
11311
|
try {
|
|
10446
11312
|
banner();
|
|
10447
11313
|
if (options.format && options.format !== "json") {
|
|
@@ -10452,7 +11318,7 @@ function generateCommand() {
|
|
|
10452
11318
|
spinner.start();
|
|
10453
11319
|
let loaded = null;
|
|
10454
11320
|
try {
|
|
10455
|
-
loaded = loadConfig();
|
|
11321
|
+
loaded = loadConfig(warn);
|
|
10456
11322
|
} catch {
|
|
10457
11323
|
loaded = null;
|
|
10458
11324
|
}
|
|
@@ -10465,6 +11331,10 @@ function generateCommand() {
|
|
|
10465
11331
|
});
|
|
10466
11332
|
const output = await generator.generate();
|
|
10467
11333
|
spinner.stop();
|
|
11334
|
+
assertDocumentHasScreens(output.document, {
|
|
11335
|
+
rootDir: process.cwd(),
|
|
11336
|
+
allowEmpty: Boolean(options.allowEmpty)
|
|
11337
|
+
});
|
|
10468
11338
|
success(
|
|
10469
11339
|
`Generated MCP document with ${output.document.metadata.totalScreens} screens, ${output.document.metadata.totalForms} forms, ${output.document.metadata.totalActions} actions`
|
|
10470
11340
|
);
|
|
@@ -10472,21 +11342,761 @@ function generateCommand() {
|
|
|
10472
11342
|
info(`Checksum: ${output.checksum}`);
|
|
10473
11343
|
reportMetadataLint(output.document, Boolean(options.strictMetadata));
|
|
10474
11344
|
} catch (err) {
|
|
10475
|
-
error(`Generation failed: ${
|
|
11345
|
+
error(`Generation failed: ${stringifyUnknown(err)}`);
|
|
10476
11346
|
process.exit(1);
|
|
10477
11347
|
}
|
|
10478
11348
|
});
|
|
10479
11349
|
}
|
|
10480
11350
|
|
|
10481
|
-
// src/cli/commands/
|
|
11351
|
+
// src/cli/commands/annotate.ts
|
|
11352
|
+
var import_promises6 = require("fs/promises");
|
|
11353
|
+
var import_node_path7 = __toESM(require("path"));
|
|
10482
11354
|
var import_commander5 = require("commander");
|
|
11355
|
+
var import_fast_glob6 = __toESM(require("fast-glob"));
|
|
11356
|
+
|
|
11357
|
+
// src/annotate/sites.ts
|
|
11358
|
+
var import_traverse12 = __toESM(require("@babel/traverse"));
|
|
11359
|
+
var t13 = __toESM(require("@babel/types"));
|
|
11360
|
+
|
|
11361
|
+
// src/annotate/forwarding.ts
|
|
11362
|
+
var import_node_path6 = __toESM(require("path"));
|
|
11363
|
+
var import_node_fs3 = require("fs");
|
|
11364
|
+
var import_traverse11 = __toESM(require("@babel/traverse"));
|
|
11365
|
+
var t12 = __toESM(require("@babel/types"));
|
|
11366
|
+
var traverse11 = import_traverse11.default.default ?? import_traverse11.default;
|
|
11367
|
+
var RN_TESTID_CARRIERS = /* @__PURE__ */ new Set([
|
|
11368
|
+
"View",
|
|
11369
|
+
"Text",
|
|
11370
|
+
"TextInput",
|
|
11371
|
+
"Image",
|
|
11372
|
+
"ImageBackground",
|
|
11373
|
+
"ScrollView",
|
|
11374
|
+
"FlatList",
|
|
11375
|
+
"SectionList",
|
|
11376
|
+
"VirtualizedList",
|
|
11377
|
+
"TouchableOpacity",
|
|
11378
|
+
"TouchableHighlight",
|
|
11379
|
+
"TouchableWithoutFeedback",
|
|
11380
|
+
"TouchableNativeFeedback",
|
|
11381
|
+
"Pressable",
|
|
11382
|
+
"Button",
|
|
11383
|
+
"Switch",
|
|
11384
|
+
"Modal",
|
|
11385
|
+
"RefreshControl",
|
|
11386
|
+
"ActivityIndicator",
|
|
11387
|
+
"SafeAreaView",
|
|
11388
|
+
"KeyboardAvoidingView",
|
|
11389
|
+
"Slider"
|
|
11390
|
+
]);
|
|
11391
|
+
function collectImports(ast) {
|
|
11392
|
+
const out = /* @__PURE__ */ new Map();
|
|
11393
|
+
traverse11(ast, {
|
|
11394
|
+
ImportDeclaration(nodePath) {
|
|
11395
|
+
const source = nodePath.node.source.value;
|
|
11396
|
+
for (const spec of nodePath.node.specifiers) {
|
|
11397
|
+
if (t12.isImportSpecifier(spec)) {
|
|
11398
|
+
const imported = t12.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
11399
|
+
out.set(spec.local.name, { source, local: spec.local.name, imported });
|
|
11400
|
+
} else if (t12.isImportDefaultSpecifier(spec)) {
|
|
11401
|
+
out.set(spec.local.name, { source, local: spec.local.name, imported: "default" });
|
|
11402
|
+
}
|
|
11403
|
+
}
|
|
11404
|
+
}
|
|
11405
|
+
});
|
|
11406
|
+
return out;
|
|
11407
|
+
}
|
|
11408
|
+
var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
11409
|
+
function resolveRelativeImport(fromFile, source) {
|
|
11410
|
+
if (!source.startsWith(".")) return void 0;
|
|
11411
|
+
const base = import_node_path6.default.resolve(import_node_path6.default.dirname(fromFile), source);
|
|
11412
|
+
for (const ext of EXTENSIONS) {
|
|
11413
|
+
const candidate = `${base}${ext}`;
|
|
11414
|
+
if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
|
|
11415
|
+
}
|
|
11416
|
+
for (const ext of EXTENSIONS) {
|
|
11417
|
+
const candidate = import_node_path6.default.join(base, `index${ext}`);
|
|
11418
|
+
if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
|
|
11419
|
+
}
|
|
11420
|
+
return void 0;
|
|
11421
|
+
}
|
|
11422
|
+
var IDENTITY_HOOKS = /* @__PURE__ */ new Set([
|
|
11423
|
+
"useAppilotsTarget",
|
|
11424
|
+
"useAppilotsField",
|
|
11425
|
+
"useAppilotsToggle",
|
|
11426
|
+
"useAppilotsSlider"
|
|
11427
|
+
]);
|
|
11428
|
+
function inspectModule(file2, exportName, depth = 0) {
|
|
11429
|
+
const none = { forwards: false, derives: false };
|
|
11430
|
+
if (depth > 2) return none;
|
|
11431
|
+
let source;
|
|
11432
|
+
try {
|
|
11433
|
+
source = (0, import_node_fs3.readFileSync)(file2, "utf-8");
|
|
11434
|
+
} catch {
|
|
11435
|
+
return none;
|
|
11436
|
+
}
|
|
11437
|
+
const mentionsIdentity = source.includes("testID") || source.includes("accessibilityLabel") || source.includes("useAppilots") || /\{\s*\.\.\./.test(source);
|
|
11438
|
+
if (!mentionsIdentity && !/export\s*\{[^}]*\}\s*from/.test(source)) return none;
|
|
11439
|
+
let ast;
|
|
11440
|
+
try {
|
|
11441
|
+
ast = parseSource(source);
|
|
11442
|
+
} catch {
|
|
11443
|
+
return none;
|
|
11444
|
+
}
|
|
11445
|
+
let forwards = false;
|
|
11446
|
+
let derives = false;
|
|
11447
|
+
let barrelTarget;
|
|
11448
|
+
traverse11(ast, {
|
|
11449
|
+
// `export { Card } from './Card'` — follow one hop.
|
|
11450
|
+
ExportNamedDeclaration(nodePath) {
|
|
11451
|
+
const from = nodePath.node.source?.value;
|
|
11452
|
+
if (!from) return;
|
|
11453
|
+
const named = nodePath.node.specifiers.some(
|
|
11454
|
+
(spec) => t12.isExportSpecifier(spec) && (t12.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === exportName
|
|
11455
|
+
);
|
|
11456
|
+
if (named) barrelTarget = from;
|
|
11457
|
+
},
|
|
11458
|
+
ExportAllDeclaration(nodePath) {
|
|
11459
|
+
barrelTarget ??= nodePath.node.source.value;
|
|
11460
|
+
},
|
|
11461
|
+
// `interface CardProps { testID?: string }` / `type Props = { testID… }`
|
|
11462
|
+
TSPropertySignature(nodePath) {
|
|
11463
|
+
if (t12.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
|
|
11464
|
+
},
|
|
11465
|
+
// `function Card({ testID }) {}` — the destructured parameter.
|
|
11466
|
+
ObjectProperty(nodePath) {
|
|
11467
|
+
if (t12.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
|
|
11468
|
+
},
|
|
11469
|
+
// `<View {...props} />` — anything spread onto JSX carries it along.
|
|
11470
|
+
JSXSpreadAttribute() {
|
|
11471
|
+
forwards = true;
|
|
11472
|
+
},
|
|
11473
|
+
// `testID={`toggle-${id}`}` / `accessibilityLabel={label}` — the
|
|
11474
|
+
// component naming itself from a prop. A STRING literal does not count:
|
|
11475
|
+
// that is one hardcoded name shared by every instance, which is the
|
|
11476
|
+
// duplicate-id problem rather than a solution to it.
|
|
11477
|
+
JSXAttribute(nodePath) {
|
|
11478
|
+
const name = t12.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
|
|
11479
|
+
if (name !== "testID" && name !== "accessibilityLabel") return;
|
|
11480
|
+
const value = nodePath.node.value;
|
|
11481
|
+
if (!t12.isJSXExpressionContainer(value)) return;
|
|
11482
|
+
if (t12.isStringLiteral(value.expression)) return;
|
|
11483
|
+
derives = true;
|
|
11484
|
+
},
|
|
11485
|
+
// `useAppilotsTarget(`${groupId}-${option.value}`, …)`
|
|
11486
|
+
CallExpression(nodePath) {
|
|
11487
|
+
const callee = nodePath.node.callee;
|
|
11488
|
+
if (t12.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
|
|
11489
|
+
}
|
|
11490
|
+
});
|
|
11491
|
+
if (forwards || derives) return { forwards, derives };
|
|
11492
|
+
if (barrelTarget) {
|
|
11493
|
+
const next = resolveRelativeImport(file2, barrelTarget);
|
|
11494
|
+
if (next) return inspectModule(next, exportName, depth + 1);
|
|
11495
|
+
}
|
|
11496
|
+
return none;
|
|
11497
|
+
}
|
|
11498
|
+
var verdictCache = /* @__PURE__ */ new Map();
|
|
11499
|
+
function canCarryTestId(componentName, fromFile, imports) {
|
|
11500
|
+
const origin = imports.get(componentName);
|
|
11501
|
+
if (origin?.source === "react-native") {
|
|
11502
|
+
return RN_TESTID_CARRIERS.has(origin.imported) ? "rn-primitive" : "unverifiable";
|
|
11503
|
+
}
|
|
11504
|
+
const target = origin ? resolveRelativeImport(fromFile, origin.source) : fromFile;
|
|
11505
|
+
if (!target) return "unverifiable";
|
|
11506
|
+
const exportName = origin?.imported ?? componentName;
|
|
11507
|
+
const key = `${target}#${exportName}`;
|
|
11508
|
+
let identity = verdictCache.get(key);
|
|
11509
|
+
if (identity === void 0) {
|
|
11510
|
+
identity = inspectModule(target, exportName);
|
|
11511
|
+
verdictCache.set(key, identity);
|
|
11512
|
+
}
|
|
11513
|
+
if (identity.forwards) return "forwards";
|
|
11514
|
+
if (identity.derives) return "derives";
|
|
11515
|
+
return "swallows";
|
|
11516
|
+
}
|
|
11517
|
+
function resetForwardingCache() {
|
|
11518
|
+
verdictCache.clear();
|
|
11519
|
+
}
|
|
11520
|
+
|
|
11521
|
+
// src/annotate/sites.ts
|
|
11522
|
+
var traverse12 = import_traverse12.default.default ?? import_traverse12.default;
|
|
11523
|
+
var LABEL_PROPS = ["title", "label", "placeholder"];
|
|
11524
|
+
var IDENTITY_PROPS = ["testID", "appilotsId", "accessibilityLabel"];
|
|
11525
|
+
function kebab(text) {
|
|
11526
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
11527
|
+
}
|
|
11528
|
+
function verbOf(name) {
|
|
11529
|
+
return kebab(name.replace(/^(handle|on)(?=[A-Z])/, ""));
|
|
11530
|
+
}
|
|
11531
|
+
function scopeSlugForFile(file2) {
|
|
11532
|
+
const base = file2.replace(/\\/g, "/").split("/").pop() ?? file2;
|
|
11533
|
+
const name = base.replace(/\.[jt]sx?$/, "");
|
|
11534
|
+
if (name.toLowerCase() === "index") {
|
|
11535
|
+
const dir = file2.replace(/\\/g, "/").split("/").slice(-2, -1)[0] ?? "";
|
|
11536
|
+
return kebab(dir);
|
|
11537
|
+
}
|
|
11538
|
+
return kebab(name.replace(/Screen$/, ""));
|
|
11539
|
+
}
|
|
11540
|
+
function collapseRepeats(slug) {
|
|
11541
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11542
|
+
const out = [];
|
|
11543
|
+
for (const token of slug.split("-")) {
|
|
11544
|
+
if (!token || seen.has(token)) continue;
|
|
11545
|
+
seen.add(token);
|
|
11546
|
+
out.push(token);
|
|
11547
|
+
}
|
|
11548
|
+
return out.join("-");
|
|
11549
|
+
}
|
|
11550
|
+
function deriveTestId(site) {
|
|
11551
|
+
const base = [site.scope, kebab(site.component)].filter(Boolean).join("-");
|
|
11552
|
+
if (site.pressProp && site.pressProp !== "onPress") {
|
|
11553
|
+
return collapseRepeats(`${base}-${verbOf(site.pressProp)}`);
|
|
11554
|
+
}
|
|
11555
|
+
if (site.handler) {
|
|
11556
|
+
const verb = verbOf(site.handler);
|
|
11557
|
+
return collapseRepeats(verb ? `${base}-${verb}` : base);
|
|
11558
|
+
}
|
|
11559
|
+
return collapseRepeats(`${base}-${site.ordinal}`);
|
|
11560
|
+
}
|
|
11561
|
+
function hasAnyProp(element, names) {
|
|
11562
|
+
return names.some((name) => findJsxAttribute(element, name) !== void 0);
|
|
11563
|
+
}
|
|
11564
|
+
function hasLabelText(element) {
|
|
11565
|
+
return LABEL_PROPS.some((name) => {
|
|
11566
|
+
const value = getStringAttr(element, name);
|
|
11567
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
11568
|
+
});
|
|
11569
|
+
}
|
|
11570
|
+
function namedHandler(element, prop) {
|
|
11571
|
+
if (!prop) return void 0;
|
|
11572
|
+
const attr = findJsxAttribute(element, prop);
|
|
11573
|
+
if (!attr || !t13.isJSXExpressionContainer(attr.value)) return void 0;
|
|
11574
|
+
return t13.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
|
|
11575
|
+
}
|
|
11576
|
+
var ANNOTATABLE_ROLES = /* @__PURE__ */ new Set(["button", "input", "select", "toggle", "date", "list"]);
|
|
11577
|
+
function isPerItemRender(nodePath) {
|
|
11578
|
+
let current = nodePath.parentPath;
|
|
11579
|
+
while (current) {
|
|
11580
|
+
const node = current.node;
|
|
11581
|
+
if (t13.isCallExpression(node) && t13.isMemberExpression(node.callee) && t13.isIdentifier(node.callee.property) && (node.callee.property.name === "map" || node.callee.property.name === "flatMap")) {
|
|
11582
|
+
return true;
|
|
11583
|
+
}
|
|
11584
|
+
if (t13.isJSXAttribute(node) && t13.isJSXIdentifier(node.name) && /^render[A-Z]|^ListHeaderComponent$|^ListFooterComponent$/.test(node.name.name) && node.name.name !== "renderScrollComponent") {
|
|
11585
|
+
return /^renderItem$|^renderSectionHeader$|^renderSectionFooter$/.test(node.name.name);
|
|
11586
|
+
}
|
|
11587
|
+
current = current.parentPath;
|
|
11588
|
+
}
|
|
11589
|
+
return false;
|
|
11590
|
+
}
|
|
11591
|
+
function findAnnotationSites(source, file2) {
|
|
11592
|
+
let ast;
|
|
11593
|
+
try {
|
|
11594
|
+
ast = parseSource(source);
|
|
11595
|
+
} catch {
|
|
11596
|
+
return [];
|
|
11597
|
+
}
|
|
11598
|
+
const imports = collectImports(ast);
|
|
11599
|
+
const scope = scopeSlugForFile(file2);
|
|
11600
|
+
const sites = [];
|
|
11601
|
+
const perComponent = /* @__PURE__ */ new Map();
|
|
11602
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
11603
|
+
traverse12(ast, {
|
|
11604
|
+
JSXOpeningElement(nodePath) {
|
|
11605
|
+
for (const prop of IDENTITY_PROPS) {
|
|
11606
|
+
const existing = getStringAttr(nodePath.node, prop);
|
|
11607
|
+
if (existing) usedIds.add(existing);
|
|
11608
|
+
}
|
|
11609
|
+
}
|
|
11610
|
+
});
|
|
11611
|
+
traverse12(ast, {
|
|
11612
|
+
JSXOpeningElement(nodePath) {
|
|
11613
|
+
const element = nodePath.node;
|
|
11614
|
+
if (!t13.isJSXIdentifier(element.name)) return;
|
|
11615
|
+
const component = element.name.name;
|
|
11616
|
+
const role = classifyJsxComponent(component, element);
|
|
11617
|
+
if (!ANNOTATABLE_ROLES.has(role)) return;
|
|
11618
|
+
if (hasAnyProp(element, IDENTITY_PROPS)) return;
|
|
11619
|
+
if (role === "button" && hasLabelText(element)) return;
|
|
11620
|
+
const carrier = canCarryTestId(component, file2, imports);
|
|
11621
|
+
if (carrier === "derives") return;
|
|
11622
|
+
if (isPerItemRender(nodePath)) return;
|
|
11623
|
+
const pressProp = pressHandlerProp(element) ?? void 0;
|
|
11624
|
+
const handler = namedHandler(element, pressProp);
|
|
11625
|
+
const ordinal = (perComponent.get(component) ?? 0) + 1;
|
|
11626
|
+
perComponent.set(component, ordinal);
|
|
11627
|
+
let testId = deriveTestId({ scope, component, pressProp, handler, ordinal });
|
|
11628
|
+
if (usedIds.has(testId)) {
|
|
11629
|
+
let suffix = 2;
|
|
11630
|
+
while (usedIds.has(`${testId}-${suffix}`)) suffix += 1;
|
|
11631
|
+
testId = `${testId}-${suffix}`;
|
|
11632
|
+
}
|
|
11633
|
+
usedIds.add(testId);
|
|
11634
|
+
const line = element.loc?.start.line ?? 0;
|
|
11635
|
+
const nameEndLine = element.name.loc?.end.line ?? line;
|
|
11636
|
+
const firstAttr = element.attributes[0];
|
|
11637
|
+
const firstAttrLoc = firstAttr?.loc?.start;
|
|
11638
|
+
const isMultiline = !!firstAttrLoc && firstAttrLoc.line !== nameEndLine;
|
|
11639
|
+
const insertText = isMultiline ? `
|
|
11640
|
+
${" ".repeat(firstAttrLoc.column)}testID="${testId}"` : ` testID="${testId}"`;
|
|
11641
|
+
sites.push({
|
|
11642
|
+
file: file2,
|
|
11643
|
+
line,
|
|
11644
|
+
column: element.loc?.start.column ?? 0,
|
|
11645
|
+
component,
|
|
11646
|
+
role,
|
|
11647
|
+
pressProp,
|
|
11648
|
+
handler,
|
|
11649
|
+
testId,
|
|
11650
|
+
carrier,
|
|
11651
|
+
// Just past the element name — before any existing attribute, so a
|
|
11652
|
+
// multi-line element keeps its formatting and the new prop reads
|
|
11653
|
+
// first in the diff.
|
|
11654
|
+
insertAt: element.name.end ?? element.start ?? 0,
|
|
11655
|
+
insertText
|
|
11656
|
+
});
|
|
11657
|
+
}
|
|
11658
|
+
});
|
|
11659
|
+
return sites;
|
|
11660
|
+
}
|
|
11661
|
+
|
|
11662
|
+
// src/annotate/apply.ts
|
|
11663
|
+
var import_magic_string = __toESM(require("magic-string"));
|
|
11664
|
+
function planSite(site) {
|
|
11665
|
+
if (site.carrier === "rn-primitive" || site.carrier === "forwards") {
|
|
11666
|
+
return { ...site, outcome: "annotated" };
|
|
11667
|
+
}
|
|
11668
|
+
if (site.carrier === "swallows") {
|
|
11669
|
+
return {
|
|
11670
|
+
...site,
|
|
11671
|
+
outcome: "blocked-swallows",
|
|
11672
|
+
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.`
|
|
11673
|
+
};
|
|
11674
|
+
}
|
|
11675
|
+
return {
|
|
11676
|
+
...site,
|
|
11677
|
+
outcome: "blocked-unverifiable",
|
|
11678
|
+
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.`
|
|
11679
|
+
};
|
|
11680
|
+
}
|
|
11681
|
+
function applyToFile(source, sites) {
|
|
11682
|
+
const planned = sites.map(planSite);
|
|
11683
|
+
const written = planned.filter((site) => site.outcome === "annotated");
|
|
11684
|
+
const blocked = planned.filter((site) => site.outcome !== "annotated");
|
|
11685
|
+
if (written.length === 0) {
|
|
11686
|
+
return { file: sites[0]?.file ?? "", content: source, written, blocked };
|
|
11687
|
+
}
|
|
11688
|
+
const magic = new import_magic_string.default(source);
|
|
11689
|
+
for (const site of written) {
|
|
11690
|
+
magic.appendLeft(site.insertAt, site.insertText);
|
|
11691
|
+
}
|
|
11692
|
+
return { file: written[0].file, content: magic.toString(), written, blocked };
|
|
11693
|
+
}
|
|
11694
|
+
|
|
11695
|
+
// src/annotate/forward.ts
|
|
11696
|
+
var import_node_fs4 = require("fs");
|
|
11697
|
+
var import_magic_string2 = __toESM(require("magic-string"));
|
|
11698
|
+
var import_traverse13 = __toESM(require("@babel/traverse"));
|
|
11699
|
+
var t14 = __toESM(require("@babel/types"));
|
|
11700
|
+
var traverse13 = import_traverse13.default.default ?? import_traverse13.default;
|
|
11701
|
+
function findComponent(ast, name) {
|
|
11702
|
+
let found;
|
|
11703
|
+
traverse13(ast, {
|
|
11704
|
+
FunctionDeclaration(nodePath) {
|
|
11705
|
+
if (nodePath.node.id?.name === name) {
|
|
11706
|
+
found ??= { params: nodePath.node.params, body: nodePath.node.body };
|
|
11707
|
+
}
|
|
11708
|
+
},
|
|
11709
|
+
VariableDeclarator(nodePath) {
|
|
11710
|
+
if (!t14.isIdentifier(nodePath.node.id) || nodePath.node.id.name !== name) return;
|
|
11711
|
+
const init = nodePath.node.init;
|
|
11712
|
+
if (t14.isArrowFunctionExpression(init) || t14.isFunctionExpression(init)) {
|
|
11713
|
+
found ??= { params: init.params, body: init.body };
|
|
11714
|
+
}
|
|
11715
|
+
}
|
|
11716
|
+
});
|
|
11717
|
+
return found;
|
|
11718
|
+
}
|
|
11719
|
+
function findReturnedRoots(body) {
|
|
11720
|
+
const roots = [];
|
|
11721
|
+
let unsupported = false;
|
|
11722
|
+
const record = (node) => {
|
|
11723
|
+
if (!node) return;
|
|
11724
|
+
if (t14.isParenthesizedExpression(node)) return record(node.expression);
|
|
11725
|
+
if (t14.isJSXElement(node)) {
|
|
11726
|
+
roots.push(node.openingElement);
|
|
11727
|
+
return;
|
|
11728
|
+
}
|
|
11729
|
+
if (t14.isConditionalExpression(node)) {
|
|
11730
|
+
record(node.consequent);
|
|
11731
|
+
record(node.alternate);
|
|
11732
|
+
return;
|
|
11733
|
+
}
|
|
11734
|
+
if (t14.isJSXFragment(node)) {
|
|
11735
|
+
unsupported = true;
|
|
11736
|
+
return;
|
|
11737
|
+
}
|
|
11738
|
+
if (t14.isNullLiteral(node)) return;
|
|
11739
|
+
unsupported = true;
|
|
11740
|
+
};
|
|
11741
|
+
if (t14.isBlockStatement(body)) {
|
|
11742
|
+
traverse13(
|
|
11743
|
+
t14.file(t14.program([t14.expressionStatement(t14.functionExpression(null, [], body))])),
|
|
11744
|
+
{
|
|
11745
|
+
ReturnStatement(nodePath) {
|
|
11746
|
+
record(nodePath.node.argument);
|
|
11747
|
+
}
|
|
11748
|
+
}
|
|
11749
|
+
);
|
|
11750
|
+
} else {
|
|
11751
|
+
record(body);
|
|
11752
|
+
}
|
|
11753
|
+
if (unsupported || roots.length === 0) return void 0;
|
|
11754
|
+
return roots;
|
|
11755
|
+
}
|
|
11756
|
+
function isCarrierRoot(element, imports) {
|
|
11757
|
+
if (!t14.isJSXIdentifier(element.name)) return false;
|
|
11758
|
+
const name = element.name.name;
|
|
11759
|
+
const origin = imports.get(name);
|
|
11760
|
+
if (origin?.source !== "react-native") return false;
|
|
11761
|
+
return RN_TESTID_CARRIERS.has(origin.imported);
|
|
11762
|
+
}
|
|
11763
|
+
function hasProp(element, name) {
|
|
11764
|
+
return element.attributes.some(
|
|
11765
|
+
(attr) => t14.isJSXAttribute(attr) && t14.isJSXIdentifier(attr.name) && attr.name.name === name
|
|
11766
|
+
);
|
|
11767
|
+
}
|
|
11768
|
+
function indentOf(source, offset) {
|
|
11769
|
+
const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
|
|
11770
|
+
const match = /^[ \t]*/.exec(source.slice(lineStart, offset));
|
|
11771
|
+
return match?.[0] ?? "";
|
|
11772
|
+
}
|
|
11773
|
+
function insertAfterLastMember(source, members, text, closingBrace, separator) {
|
|
11774
|
+
const last = members[members.length - 1];
|
|
11775
|
+
if (!last || typeof last.end !== "number" || typeof last.start !== "number") {
|
|
11776
|
+
return { at: closingBrace, text };
|
|
11777
|
+
}
|
|
11778
|
+
const trailing = /^\s*[,;]/.exec(source.slice(last.end, closingBrace));
|
|
11779
|
+
const at = last.end + (trailing?.[0].length ?? 0);
|
|
11780
|
+
const endsWithSeparator = source.slice(0, last.end).trimEnd().endsWith(separator);
|
|
11781
|
+
const lead = trailing || endsWithSeparator ? "" : separator;
|
|
11782
|
+
const openLine = source.lastIndexOf("\n", last.start);
|
|
11783
|
+
const multiline = openLine !== -1 && openLine > source.lastIndexOf("{", last.start);
|
|
11784
|
+
if (!multiline) return { at, text: `${lead} ${text}` };
|
|
11785
|
+
const indent = indentOf(source, last.start);
|
|
11786
|
+
return { at, text: `${lead}
|
|
11787
|
+
${indent}${text}` };
|
|
11788
|
+
}
|
|
11789
|
+
function followBarrel(file2, ast, component) {
|
|
11790
|
+
let target;
|
|
11791
|
+
traverse13(ast, {
|
|
11792
|
+
ExportNamedDeclaration(nodePath) {
|
|
11793
|
+
const from = nodePath.node.source?.value;
|
|
11794
|
+
if (!from) return;
|
|
11795
|
+
const named = nodePath.node.specifiers.some(
|
|
11796
|
+
(spec) => t14.isExportSpecifier(spec) && (t14.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === component
|
|
11797
|
+
);
|
|
11798
|
+
if (named) target ??= from;
|
|
11799
|
+
}
|
|
11800
|
+
});
|
|
11801
|
+
return target ? resolveRelativeImport(file2, target) : void 0;
|
|
11802
|
+
}
|
|
11803
|
+
function planForward(file2, component, depth = 0) {
|
|
11804
|
+
const base = { file: file2, component, edits: [], roots: 0 };
|
|
11805
|
+
let source;
|
|
11806
|
+
try {
|
|
11807
|
+
source = (0, import_node_fs4.readFileSync)(file2, "utf-8");
|
|
11808
|
+
} catch {
|
|
11809
|
+
return { ...base, refusal: `cannot read ${file2}` };
|
|
11810
|
+
}
|
|
11811
|
+
let ast;
|
|
11812
|
+
try {
|
|
11813
|
+
ast = parseSource(source);
|
|
11814
|
+
} catch {
|
|
11815
|
+
return { ...base, refusal: `cannot parse ${file2}` };
|
|
11816
|
+
}
|
|
11817
|
+
const found = findComponent(ast, component);
|
|
11818
|
+
if (!found) {
|
|
11819
|
+
const barrel = depth < 2 ? followBarrel(file2, ast, component) : void 0;
|
|
11820
|
+
if (barrel) return planForward(barrel, component, depth + 1);
|
|
11821
|
+
return { ...base, refusal: `no function named ${component} in this file` };
|
|
11822
|
+
}
|
|
11823
|
+
const param = found.params[0];
|
|
11824
|
+
if (!t14.isObjectPattern(param)) {
|
|
11825
|
+
return {
|
|
11826
|
+
...base,
|
|
11827
|
+
refusal: `${component} does not destructure its props, so there is no safe place to add one without knowing what the parameter is called downstream`
|
|
11828
|
+
};
|
|
11829
|
+
}
|
|
11830
|
+
const roots = findReturnedRoots(found.body);
|
|
11831
|
+
if (!roots) {
|
|
11832
|
+
return {
|
|
11833
|
+
...base,
|
|
11834
|
+
refusal: `${component} returns something other than a JSX element (a fragment, or a value this cannot follow)`
|
|
11835
|
+
};
|
|
11836
|
+
}
|
|
11837
|
+
const imports = collectImports(ast);
|
|
11838
|
+
const nonCarrier = roots.find((root) => !isCarrierRoot(root, imports));
|
|
11839
|
+
if (nonCarrier) {
|
|
11840
|
+
const name = t14.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
|
|
11841
|
+
return {
|
|
11842
|
+
...base,
|
|
11843
|
+
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`
|
|
11844
|
+
};
|
|
11845
|
+
}
|
|
11846
|
+
const edits = [];
|
|
11847
|
+
const alreadyDestructured = param.properties.some(
|
|
11848
|
+
(prop) => t14.isObjectProperty(prop) && t14.isIdentifier(prop.key) && prop.key.name === "testID"
|
|
11849
|
+
);
|
|
11850
|
+
if (!alreadyDestructured) {
|
|
11851
|
+
const insert = insertAfterLastMember(source, param.properties, "testID", param.end ?? 0, ",");
|
|
11852
|
+
edits.push({ ...insert, what: "parameter" });
|
|
11853
|
+
}
|
|
11854
|
+
const annotation = param.typeAnnotation;
|
|
11855
|
+
if (t14.isTSTypeAnnotation(annotation)) {
|
|
11856
|
+
const declared = annotation.typeAnnotation;
|
|
11857
|
+
let body;
|
|
11858
|
+
let closing = 0;
|
|
11859
|
+
if (t14.isTSTypeLiteral(declared)) {
|
|
11860
|
+
body = declared.members;
|
|
11861
|
+
closing = declared.end ?? 0;
|
|
11862
|
+
} else if (t14.isTSTypeReference(declared) && t14.isIdentifier(declared.typeName)) {
|
|
11863
|
+
const typeName = declared.typeName.name;
|
|
11864
|
+
traverse13(ast, {
|
|
11865
|
+
TSInterfaceDeclaration(nodePath) {
|
|
11866
|
+
if (nodePath.node.id.name !== typeName) return;
|
|
11867
|
+
body = nodePath.node.body.body;
|
|
11868
|
+
closing = nodePath.node.body.end ?? 0;
|
|
11869
|
+
},
|
|
11870
|
+
TSTypeAliasDeclaration(nodePath) {
|
|
11871
|
+
if (nodePath.node.id.name !== typeName || !t14.isTSTypeLiteral(nodePath.node.typeAnnotation))
|
|
11872
|
+
return;
|
|
11873
|
+
body = nodePath.node.typeAnnotation.members;
|
|
11874
|
+
closing = nodePath.node.typeAnnotation.end ?? 0;
|
|
11875
|
+
}
|
|
11876
|
+
});
|
|
11877
|
+
if (!body) {
|
|
11878
|
+
return {
|
|
11879
|
+
...base,
|
|
11880
|
+
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`
|
|
11881
|
+
};
|
|
11882
|
+
}
|
|
11883
|
+
}
|
|
11884
|
+
if (body) {
|
|
11885
|
+
const declaresTestId = body.some(
|
|
11886
|
+
(member) => t14.isTSPropertySignature(member) && t14.isIdentifier(member.key) && member.key.name === "testID"
|
|
11887
|
+
);
|
|
11888
|
+
if (!declaresTestId) {
|
|
11889
|
+
const insert = insertAfterLastMember(source, body, "testID?: string;", closing, ";");
|
|
11890
|
+
edits.push({ ...insert, what: "props-type" });
|
|
11891
|
+
}
|
|
11892
|
+
}
|
|
11893
|
+
}
|
|
11894
|
+
let delivered = 0;
|
|
11895
|
+
for (const root of roots) {
|
|
11896
|
+
if (hasProp(root, "testID")) continue;
|
|
11897
|
+
edits.push({
|
|
11898
|
+
at: root.name.end ?? root.start ?? 0,
|
|
11899
|
+
text: " testID={testID}",
|
|
11900
|
+
what: "root"
|
|
11901
|
+
});
|
|
11902
|
+
delivered += 1;
|
|
11903
|
+
}
|
|
11904
|
+
if (edits.length === 0) {
|
|
11905
|
+
return { ...base, refusal: `${component} already forwards testID` };
|
|
11906
|
+
}
|
|
11907
|
+
return { file: file2, component, edits, roots: delivered };
|
|
11908
|
+
}
|
|
11909
|
+
function applyForward(source, plan) {
|
|
11910
|
+
if (plan.refusal || plan.edits.length === 0) return source;
|
|
11911
|
+
const magic = new import_magic_string2.default(source);
|
|
11912
|
+
for (const edit of plan.edits) magic.appendLeft(edit.at, edit.text);
|
|
11913
|
+
return magic.toString();
|
|
11914
|
+
}
|
|
11915
|
+
|
|
11916
|
+
// src/cli/commands/annotate.ts
|
|
11917
|
+
var DEFAULT_INCLUDE = ["src/**/*Screen.{tsx,jsx}", "src/**/screens/**/*.{tsx,jsx}"];
|
|
11918
|
+
var DEFAULT_EXCLUDE = ["**/node_modules/**", "**/dist/**", "**/*.test.*", "**/__tests__/**"];
|
|
11919
|
+
function annotateCommand() {
|
|
11920
|
+
return new import_commander5.Command("annotate").description(
|
|
11921
|
+
"Add missing testIDs to interactive elements so the agent can address them (dry run by default)"
|
|
11922
|
+
).option("--write", "Actually modify the files (default: report only)", false).option(
|
|
11923
|
+
"--forward",
|
|
11924
|
+
"Also teach a shared component to accept and pass through testID, when its call sites are blocked only by that",
|
|
11925
|
+
false
|
|
11926
|
+
).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) => {
|
|
11927
|
+
try {
|
|
11928
|
+
if (options.json) setLogMode("json");
|
|
11929
|
+
else if (options.quiet) setLogMode("quiet");
|
|
11930
|
+
banner();
|
|
11931
|
+
let loaded = null;
|
|
11932
|
+
try {
|
|
11933
|
+
loaded = loadConfig(warn);
|
|
11934
|
+
} catch {
|
|
11935
|
+
loaded = null;
|
|
11936
|
+
}
|
|
11937
|
+
const rootDir = process.cwd();
|
|
11938
|
+
const include = options.include ?? DEFAULT_INCLUDE;
|
|
11939
|
+
const exclude = loaded?.exclude ?? DEFAULT_EXCLUDE;
|
|
11940
|
+
const spinner = createSpinner("Scanning for unaddressable controls...");
|
|
11941
|
+
spinner.start();
|
|
11942
|
+
resetForwardingCache();
|
|
11943
|
+
const files = await (0, import_fast_glob6.default)(include, { cwd: rootDir, ignore: exclude, absolute: true });
|
|
11944
|
+
const scan = async () => {
|
|
11945
|
+
const out = [];
|
|
11946
|
+
for (const file2 of files) {
|
|
11947
|
+
const source = await (0, import_promises6.readFile)(file2, "utf-8");
|
|
11948
|
+
const sites = findAnnotationSites(source, file2);
|
|
11949
|
+
if (sites.length === 0) continue;
|
|
11950
|
+
out.push(applyToFile(source, sites));
|
|
11951
|
+
}
|
|
11952
|
+
return out;
|
|
11953
|
+
};
|
|
11954
|
+
let edits = await scan();
|
|
11955
|
+
let forwards = [];
|
|
11956
|
+
if (options.forward) {
|
|
11957
|
+
forwards = await planForwards(edits, rootDir);
|
|
11958
|
+
const applied = forwards.filter((plan) => !plan.refusal);
|
|
11959
|
+
if (options.write && applied.length > 0) {
|
|
11960
|
+
for (const plan of applied) {
|
|
11961
|
+
const before = await (0, import_promises6.readFile)(plan.file, "utf-8");
|
|
11962
|
+
await (0, import_promises6.writeFile)(plan.file, applyForward(before, plan), "utf-8");
|
|
11963
|
+
}
|
|
11964
|
+
resetForwardingCache();
|
|
11965
|
+
edits = await scan();
|
|
11966
|
+
}
|
|
11967
|
+
}
|
|
11968
|
+
spinner.stop();
|
|
11969
|
+
const written = edits.flatMap((edit) => edit.written);
|
|
11970
|
+
const blocked = edits.flatMap((edit) => edit.blocked);
|
|
11971
|
+
const touched = edits.filter((edit) => edit.written.length > 0);
|
|
11972
|
+
if (written.length === 0 && blocked.length === 0) {
|
|
11973
|
+
success("Every interactive control already has something to address it by.");
|
|
11974
|
+
if (options.json) {
|
|
11975
|
+
jsonOutput({ ok: true, scanned: files.length, annotated: 0, blocked: 0, sites: [] });
|
|
11976
|
+
}
|
|
11977
|
+
return;
|
|
11978
|
+
}
|
|
11979
|
+
if (options.write) {
|
|
11980
|
+
for (const edit of touched) {
|
|
11981
|
+
await (0, import_promises6.writeFile)(edit.file, edit.content, "utf-8");
|
|
11982
|
+
}
|
|
11983
|
+
}
|
|
11984
|
+
reportSites(rootDir, touched, blocked, options.write === true, forwards);
|
|
11985
|
+
if (options.json) {
|
|
11986
|
+
jsonOutput({
|
|
11987
|
+
ok: true,
|
|
11988
|
+
scanned: files.length,
|
|
11989
|
+
written: options.write === true,
|
|
11990
|
+
annotated: written.length,
|
|
11991
|
+
blocked: blocked.length,
|
|
11992
|
+
files: touched.map((edit) => import_node_path7.default.relative(rootDir, edit.file)),
|
|
11993
|
+
sites: [...written, ...blocked].map((site) => ({
|
|
11994
|
+
file: import_node_path7.default.relative(rootDir, site.file),
|
|
11995
|
+
line: site.line,
|
|
11996
|
+
component: site.component,
|
|
11997
|
+
role: site.role,
|
|
11998
|
+
testId: site.testId,
|
|
11999
|
+
outcome: site.outcome,
|
|
12000
|
+
reason: site.blockedReason
|
|
12001
|
+
}))
|
|
12002
|
+
});
|
|
12003
|
+
}
|
|
12004
|
+
} catch (err) {
|
|
12005
|
+
error(`Annotate failed: ${stringifyUnknown(err)}`);
|
|
12006
|
+
process.exit(1);
|
|
12007
|
+
}
|
|
12008
|
+
});
|
|
12009
|
+
}
|
|
12010
|
+
async function planForwards(edits, rootDir) {
|
|
12011
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12012
|
+
const plans = [];
|
|
12013
|
+
for (const edit of edits) {
|
|
12014
|
+
for (const site of edit.blocked) {
|
|
12015
|
+
if (site.outcome !== "blocked-swallows") continue;
|
|
12016
|
+
if (seen.has(site.component)) continue;
|
|
12017
|
+
seen.add(site.component);
|
|
12018
|
+
const source = await (0, import_promises6.readFile)(site.file, "utf-8");
|
|
12019
|
+
const origin = findImportSource(source, site.component);
|
|
12020
|
+
const target = origin ? resolveRelativeImport(site.file, origin) : site.file;
|
|
12021
|
+
if (!target) {
|
|
12022
|
+
plans.push({
|
|
12023
|
+
file: site.file,
|
|
12024
|
+
component: site.component,
|
|
12025
|
+
edits: [],
|
|
12026
|
+
roots: 0,
|
|
12027
|
+
refusal: `cannot locate the source of <${site.component}>`
|
|
12028
|
+
});
|
|
12029
|
+
continue;
|
|
12030
|
+
}
|
|
12031
|
+
plans.push(planForward(target, site.component));
|
|
12032
|
+
}
|
|
12033
|
+
}
|
|
12034
|
+
void rootDir;
|
|
12035
|
+
return plans;
|
|
12036
|
+
}
|
|
12037
|
+
function findImportSource(source, name) {
|
|
12038
|
+
const pattern = new RegExp(
|
|
12039
|
+
String.raw`import\s*\{[^}]*\b${name}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
|
|
12040
|
+
);
|
|
12041
|
+
return pattern.exec(source)?.[1];
|
|
12042
|
+
}
|
|
12043
|
+
function reportSites(rootDir, touched, blocked, didWrite, forwards) {
|
|
12044
|
+
const written = touched.flatMap((edit) => edit.written);
|
|
12045
|
+
if (written.length > 0) {
|
|
12046
|
+
heading(didWrite ? "Added" : "Would add");
|
|
12047
|
+
for (const edit of touched) {
|
|
12048
|
+
info(import_node_path7.default.relative(rootDir, edit.file));
|
|
12049
|
+
for (const site of edit.written) {
|
|
12050
|
+
dim(` ${String(site.line).padStart(4)} <${site.component}> testID="${site.testId}"`);
|
|
12051
|
+
}
|
|
12052
|
+
}
|
|
12053
|
+
}
|
|
12054
|
+
const teachable = new Map(
|
|
12055
|
+
forwards.filter((plan) => !plan.refusal).map((plan) => [plan.component, plan])
|
|
12056
|
+
);
|
|
12057
|
+
if (teachable.size > 0) {
|
|
12058
|
+
heading(didWrite ? "Taught to carry testID" : "Would teach to carry testID");
|
|
12059
|
+
for (const plan of teachable.values()) {
|
|
12060
|
+
info(`<${plan.component}> ${import_node_path7.default.relative(rootDir, plan.file)}`);
|
|
12061
|
+
for (const edit of plan.edits) {
|
|
12062
|
+
dim(` ${edit.what}: ${edit.text.trim()}`);
|
|
12063
|
+
}
|
|
12064
|
+
}
|
|
12065
|
+
}
|
|
12066
|
+
if (blocked.length > 0) {
|
|
12067
|
+
heading("Skipped \u2014 the prop would not survive to the runtime");
|
|
12068
|
+
const byComponent = /* @__PURE__ */ new Map();
|
|
12069
|
+
for (const site of blocked) {
|
|
12070
|
+
const group = byComponent.get(site.component) ?? [];
|
|
12071
|
+
group.push(site);
|
|
12072
|
+
byComponent.set(site.component, group);
|
|
12073
|
+
}
|
|
12074
|
+
for (const [component, group] of byComponent) {
|
|
12075
|
+
if (teachable.has(component)) continue;
|
|
12076
|
+
warn(`<${component}> \u2014 ${group.length} site${group.length === 1 ? "" : "s"}`);
|
|
12077
|
+
const refusal = forwards.find((plan) => plan.component === component)?.refusal;
|
|
12078
|
+
dim(` ${group[0].blockedReason}`);
|
|
12079
|
+
if (refusal) dim(` --forward cannot do it either: ${refusal}`);
|
|
12080
|
+
}
|
|
12081
|
+
}
|
|
12082
|
+
if (!didWrite && written.length > 0) {
|
|
12083
|
+
info("");
|
|
12084
|
+
info(`Nothing was written. Re-run with --write to apply ${written.length} change(s).`);
|
|
12085
|
+
} else if (didWrite) {
|
|
12086
|
+
success(`Wrote ${written.length} testID(s) across ${touched.length} file(s).`);
|
|
12087
|
+
info("Re-run `appilots generate` so the document picks them up.");
|
|
12088
|
+
}
|
|
12089
|
+
}
|
|
12090
|
+
|
|
12091
|
+
// src/cli/commands/status.ts
|
|
12092
|
+
var import_commander6 = require("commander");
|
|
10483
12093
|
function statusCommand() {
|
|
10484
|
-
return new
|
|
12094
|
+
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
12095
|
if (options.json) setLogMode("json");
|
|
10486
12096
|
else if (options.quiet) setLogMode("quiet");
|
|
10487
12097
|
try {
|
|
10488
12098
|
banner();
|
|
10489
|
-
const config = loadConfig();
|
|
12099
|
+
const config = loadConfig(warn);
|
|
10490
12100
|
if (!config) {
|
|
10491
12101
|
error("No .appilotsrc configuration found");
|
|
10492
12102
|
info("Run: appilots init --api-key <your-key>");
|
|
@@ -10513,8 +12123,13 @@ function statusCommand() {
|
|
|
10513
12123
|
]);
|
|
10514
12124
|
if (status.project) {
|
|
10515
12125
|
heading("\n\u{1F4CC} Project Info:");
|
|
10516
|
-
table([
|
|
12126
|
+
table([
|
|
12127
|
+
["Name", status.project.name],
|
|
12128
|
+
["ID", status.project.id]
|
|
12129
|
+
]);
|
|
10517
12130
|
}
|
|
12131
|
+
const outputDir = config.outputDir || ".appilots";
|
|
12132
|
+
let drift = null;
|
|
10518
12133
|
if (status.activeMcp) {
|
|
10519
12134
|
heading("\n\u2705 Active MCP Document:");
|
|
10520
12135
|
table([
|
|
@@ -10524,6 +12139,14 @@ function statusCommand() {
|
|
|
10524
12139
|
["Forms", String(status.activeMcp.formsCount)],
|
|
10525
12140
|
["Actions", String(status.activeMcp.actionsCount)]
|
|
10526
12141
|
]);
|
|
12142
|
+
const local = readLocalDocumentSummary(outputDir);
|
|
12143
|
+
if (local) {
|
|
12144
|
+
drift = compareWithRemote(local, status.activeMcp);
|
|
12145
|
+
if (drift) {
|
|
12146
|
+
heading("");
|
|
12147
|
+
reportRemoteDrift(drift);
|
|
12148
|
+
}
|
|
12149
|
+
}
|
|
10527
12150
|
} else {
|
|
10528
12151
|
heading("\n\u26A0\uFE0F No active MCP document");
|
|
10529
12152
|
info("Run: appilots sync");
|
|
@@ -10534,21 +12157,22 @@ function statusCommand() {
|
|
|
10534
12157
|
local: {
|
|
10535
12158
|
serverUrl: config.serverUrl,
|
|
10536
12159
|
projectId: config.projectId || null,
|
|
10537
|
-
outputDir
|
|
12160
|
+
outputDir
|
|
10538
12161
|
},
|
|
10539
12162
|
project: status.project ?? null,
|
|
10540
|
-
activeMcp: status.activeMcp ?? null
|
|
12163
|
+
activeMcp: status.activeMcp ?? null,
|
|
12164
|
+
drift
|
|
10541
12165
|
});
|
|
10542
12166
|
} catch (err) {
|
|
10543
|
-
error(`Status check failed: ${
|
|
10544
|
-
jsonOutput({ success: false, error:
|
|
12167
|
+
error(`Status check failed: ${stringifyUnknown(err)}`);
|
|
12168
|
+
jsonOutput({ success: false, error: stringifyUnknown(err) });
|
|
10545
12169
|
process.exit(1);
|
|
10546
12170
|
}
|
|
10547
12171
|
});
|
|
10548
12172
|
}
|
|
10549
12173
|
|
|
10550
12174
|
// src/cli/commands/eval.ts
|
|
10551
|
-
var
|
|
12175
|
+
var import_commander7 = require("commander");
|
|
10552
12176
|
var import_fs10 = require("fs");
|
|
10553
12177
|
var import_path9 = require("path");
|
|
10554
12178
|
|
|
@@ -10578,21 +12202,21 @@ function loadScenarios(dir) {
|
|
|
10578
12202
|
} catch {
|
|
10579
12203
|
return { scenarios, errors };
|
|
10580
12204
|
}
|
|
10581
|
-
for (const
|
|
10582
|
-
const full = (0, import_path7.join)(dir,
|
|
12205
|
+
for (const file2 of files) {
|
|
12206
|
+
const full = (0, import_path7.join)(dir, file2);
|
|
10583
12207
|
let raw;
|
|
10584
12208
|
try {
|
|
10585
12209
|
raw = JSON.parse((0, import_fs8.readFileSync)(full, "utf-8"));
|
|
10586
12210
|
} catch (err) {
|
|
10587
|
-
errors.push({ file, error: `invalid JSON: ${
|
|
12211
|
+
errors.push({ file: file2, error: `invalid JSON: ${stringifyUnknown(err)}` });
|
|
10588
12212
|
continue;
|
|
10589
12213
|
}
|
|
10590
12214
|
const shapeError = validateScenarioShape(raw);
|
|
10591
12215
|
if (shapeError) {
|
|
10592
|
-
errors.push({ file, error: shapeError });
|
|
12216
|
+
errors.push({ file: file2, error: shapeError });
|
|
10593
12217
|
continue;
|
|
10594
12218
|
}
|
|
10595
|
-
scenarios.push({ file, scenario: raw });
|
|
12219
|
+
scenarios.push({ file: file2, scenario: raw });
|
|
10596
12220
|
}
|
|
10597
12221
|
return { scenarios, errors };
|
|
10598
12222
|
}
|
|
@@ -10741,10 +12365,10 @@ function matchActions(expected, actual, reply = "") {
|
|
|
10741
12365
|
var import_fs9 = require("fs");
|
|
10742
12366
|
var import_path8 = require("path");
|
|
10743
12367
|
var EMPTY_BASELINE = { generatedAt: null, gitSha: null, perScenario: {} };
|
|
10744
|
-
function loadBaseline(
|
|
10745
|
-
if (!(0, import_fs9.existsSync)(
|
|
12368
|
+
function loadBaseline(path11) {
|
|
12369
|
+
if (!(0, import_fs9.existsSync)(path11)) return { ...EMPTY_BASELINE, perScenario: {} };
|
|
10746
12370
|
try {
|
|
10747
|
-
const raw = JSON.parse((0, import_fs9.readFileSync)(
|
|
12371
|
+
const raw = JSON.parse((0, import_fs9.readFileSync)(path11, "utf-8"));
|
|
10748
12372
|
return {
|
|
10749
12373
|
generatedAt: raw.generatedAt ?? null,
|
|
10750
12374
|
gitSha: raw.gitSha ?? null,
|
|
@@ -10754,9 +12378,9 @@ function loadBaseline(path9) {
|
|
|
10754
12378
|
return { ...EMPTY_BASELINE, perScenario: {} };
|
|
10755
12379
|
}
|
|
10756
12380
|
}
|
|
10757
|
-
function saveBaseline(
|
|
10758
|
-
(0, import_fs9.mkdirSync)((0, import_path8.dirname)(
|
|
10759
|
-
(0, import_fs9.writeFileSync)(
|
|
12381
|
+
function saveBaseline(path11, baseline) {
|
|
12382
|
+
(0, import_fs9.mkdirSync)((0, import_path8.dirname)(path11), { recursive: true });
|
|
12383
|
+
(0, import_fs9.writeFileSync)(path11, `${JSON.stringify(baseline, null, 2)}
|
|
10760
12384
|
`, "utf-8");
|
|
10761
12385
|
}
|
|
10762
12386
|
function buildBaseline(outcomes, meta) {
|
|
@@ -10838,13 +12462,16 @@ function parseRate(raw, fallback, flagName) {
|
|
|
10838
12462
|
return n;
|
|
10839
12463
|
}
|
|
10840
12464
|
function evalCommand() {
|
|
10841
|
-
return new
|
|
12465
|
+
return new import_commander7.Command("eval").description(
|
|
10842
12466
|
"Run agent eval scenarios against the current MCP document in dry-run mode and check for regressions vs a committed baseline"
|
|
10843
12467
|
).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
12468
|
"--update-baseline",
|
|
10845
12469
|
"Record this run as the new baseline (refuses if pass rate is below the threshold)",
|
|
10846
12470
|
false
|
|
10847
|
-
).option(
|
|
12471
|
+
).option(
|
|
12472
|
+
"--min-pass-rate <n>",
|
|
12473
|
+
"Minimum fraction of scenarios that must pass, 0-1 (default: 0.95)"
|
|
12474
|
+
).option(
|
|
10848
12475
|
"--max-token-regression <n>",
|
|
10849
12476
|
"Max fractional token increase before a passing scenario is flagged, e.g. 0.25 for +25% (default: 0.25)"
|
|
10850
12477
|
).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 +12479,7 @@ function evalCommand() {
|
|
|
10852
12479
|
else if (options.quiet) setLogMode("quiet");
|
|
10853
12480
|
try {
|
|
10854
12481
|
banner();
|
|
10855
|
-
const loaded = loadConfig();
|
|
12482
|
+
const loaded = loadConfig(warn);
|
|
10856
12483
|
const outputDir = loaded?.outputDir || ".appilots";
|
|
10857
12484
|
const scenariosDir = options.dir || loaded?.eval?.scenariosDir || (0, import_path9.join)(outputDir, "scenarios");
|
|
10858
12485
|
if (options.init) {
|
|
@@ -10881,7 +12508,7 @@ function evalCommand() {
|
|
|
10881
12508
|
return;
|
|
10882
12509
|
}
|
|
10883
12510
|
const config = {
|
|
10884
|
-
serverUrl:
|
|
12511
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
10885
12512
|
...loaded,
|
|
10886
12513
|
apiKey,
|
|
10887
12514
|
...options.projectId ? { projectId: options.projectId } : {},
|
|
@@ -10927,11 +12554,17 @@ function evalCommand() {
|
|
|
10927
12554
|
error(
|
|
10928
12555
|
`Duplicate scenario name(s) \u2014 "name" must be unique across all scenario files: ${duplicates.join(", ")}`
|
|
10929
12556
|
);
|
|
10930
|
-
jsonOutput({
|
|
12557
|
+
jsonOutput({
|
|
12558
|
+
success: false,
|
|
12559
|
+
error: `Duplicate scenario names: ${duplicates.join(", ")}`
|
|
12560
|
+
});
|
|
10931
12561
|
process.exit(1);
|
|
10932
12562
|
return;
|
|
10933
12563
|
}
|
|
10934
|
-
const apiClient = new AppilotsAPIClient({
|
|
12564
|
+
const apiClient = new AppilotsAPIClient({
|
|
12565
|
+
serverUrl: config.serverUrl,
|
|
12566
|
+
apiKey: config.apiKey
|
|
12567
|
+
});
|
|
10935
12568
|
const requests = loadedScenarios.scenarios.map(({ scenario }) => ({
|
|
10936
12569
|
id: scenario.name,
|
|
10937
12570
|
screen: scenario.given?.screen,
|
|
@@ -11033,8 +12666,8 @@ Pass rate: ${passed}/${total} (${passRatePct}%) \u2014 threshold ${minPassRatePc
|
|
|
11033
12666
|
success("Eval passed");
|
|
11034
12667
|
}
|
|
11035
12668
|
} catch (err) {
|
|
11036
|
-
error(`Eval failed: ${
|
|
11037
|
-
jsonOutput({ success: false, error:
|
|
12669
|
+
error(`Eval failed: ${stringifyUnknown(err)}`);
|
|
12670
|
+
jsonOutput({ success: false, error: stringifyUnknown(err) });
|
|
11038
12671
|
process.exit(1);
|
|
11039
12672
|
}
|
|
11040
12673
|
});
|
|
@@ -11042,16 +12675,17 @@ Pass rate: ${passed}/${total} (${passRatePct}%) \u2014 threshold ${minPassRatePc
|
|
|
11042
12675
|
|
|
11043
12676
|
// src/cli/index.ts
|
|
11044
12677
|
async function main() {
|
|
11045
|
-
const
|
|
11046
|
-
|
|
12678
|
+
const program2 = new import_commander8.Command();
|
|
12679
|
+
program2.name("appilots").description(
|
|
11047
12680
|
"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
|
-
|
|
12681
|
+
).version(CLI_VERSION).option("--verbose", "Enable verbose logging");
|
|
12682
|
+
program2.addCommand(initCommand());
|
|
12683
|
+
program2.addCommand(syncCommand());
|
|
12684
|
+
program2.addCommand(watchCommand());
|
|
12685
|
+
program2.addCommand(generateCommand());
|
|
12686
|
+
program2.addCommand(annotateCommand());
|
|
12687
|
+
program2.addCommand(statusCommand());
|
|
12688
|
+
program2.addCommand(evalCommand());
|
|
11055
12689
|
const args = process.argv.slice(2);
|
|
11056
12690
|
const firstArg = args[0];
|
|
11057
12691
|
const hasCommand = firstArg !== void 0 && args.length > 0 && !firstArg.startsWith("-");
|
|
@@ -11064,14 +12698,14 @@ async function main() {
|
|
|
11064
12698
|
info("Welcome to Appilots! Get started with:");
|
|
11065
12699
|
info(" appilots init --api-key <your-ak_key>");
|
|
11066
12700
|
console.log("");
|
|
11067
|
-
|
|
12701
|
+
program2.outputHelp();
|
|
11068
12702
|
return;
|
|
11069
12703
|
}
|
|
11070
12704
|
}
|
|
11071
|
-
await
|
|
12705
|
+
await program2.parseAsync(process.argv);
|
|
11072
12706
|
}
|
|
11073
12707
|
main().catch((err) => {
|
|
11074
|
-
error(
|
|
12708
|
+
error(stringifyUnknown(err));
|
|
11075
12709
|
process.exit(1);
|
|
11076
12710
|
});
|
|
11077
12711
|
//# sourceMappingURL=index.js.map
|