zelune 1.5.7 → 1.6.2

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/mcp/server.mjs ADDED
@@ -0,0 +1,2228 @@
1
+ import fsSync from "node:fs";
2
+ import path from "node:path";
3
+ import readline from "node:readline";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import fs from "node:fs/promises";
6
+ import { parse } from "@babel/parser";
7
+ import _traverse from "@babel/traverse";
8
+ import _generate from "@babel/generator";
9
+ import * as t from "@babel/types";
10
+ _traverse.default ?? _traverse;
11
+ _generate.default ?? _generate;
12
+ function getJsxTagName(nameNode) {
13
+ if (t.isJSXIdentifier(nameNode)) {
14
+ return nameNode.name;
15
+ }
16
+ if (t.isJSXMemberExpression(nameNode)) {
17
+ return `${getJsxTagName(nameNode.object)}.${nameNode.property.name}`;
18
+ }
19
+ if (t.isJSXNamespacedName(nameNode)) {
20
+ return `${nameNode.namespace.name}:${nameNode.name.name}`;
21
+ }
22
+ return "Unknown";
23
+ }
24
+ const traverse = _traverse.default ?? _traverse;
25
+ const generate = _generate.default ?? _generate;
26
+ const MAX_HISTORY = 50;
27
+ const undoStack = [];
28
+ const redoStack = [];
29
+ const fileWriteLocks = /* @__PURE__ */ new Map();
30
+ function withFileLock(filePath, task) {
31
+ const prev = fileWriteLocks.get(filePath) ?? Promise.resolve();
32
+ const next = prev.then(task, task);
33
+ fileWriteLocks.set(
34
+ filePath,
35
+ next.catch(() => void 0)
36
+ );
37
+ return next;
38
+ }
39
+ const SCREEN_DIR_CANDIDATES = ["src/screens", "screens", "app", "src/app"];
40
+ const IGNORED_SCAN_DIRS = /* @__PURE__ */ new Set([
41
+ "node_modules",
42
+ ".git",
43
+ "dist",
44
+ "build",
45
+ ".next",
46
+ ".expo",
47
+ ".turbo",
48
+ "ios",
49
+ "android",
50
+ "vendor",
51
+ "coverage",
52
+ ".agents",
53
+ ".gemini"
54
+ ]);
55
+ function countScreenFilesInDirectory(screensDir, maxDepth = 4) {
56
+ let count = 0;
57
+ function walk(curr, depth) {
58
+ if (depth > maxDepth) return;
59
+ let entries = [];
60
+ try {
61
+ entries = fsSync.readdirSync(curr, { withFileTypes: true });
62
+ } catch {
63
+ return;
64
+ }
65
+ for (const entry of entries) {
66
+ if (entry.name.startsWith(".")) continue;
67
+ if (entry.isDirectory()) {
68
+ if (entry.name === "node_modules" || entry.name === "__tests__" || entry.name === "components") {
69
+ continue;
70
+ }
71
+ walk(path.join(curr, entry.name), depth + 1);
72
+ } else if (entry.isFile()) {
73
+ const f = entry.name;
74
+ if (f.startsWith("_") || f.startsWith("+")) continue;
75
+ if (f === "index.ts" || f === "index.js") continue;
76
+ if (f.includes(".test.") || f.includes(".spec.") || f.endsWith(".d.ts")) continue;
77
+ if (f.endsWith(".tsx") || f.endsWith(".jsx")) {
78
+ count++;
79
+ }
80
+ }
81
+ }
82
+ }
83
+ walk(screensDir, 0);
84
+ return count;
85
+ }
86
+ function hasDirectScreenFiles(dirPath) {
87
+ for (const rel of SCREEN_DIR_CANDIDATES) {
88
+ const full = path.join(dirPath, rel);
89
+ try {
90
+ if (fsSync.existsSync(full) && fsSync.statSync(full).isDirectory()) {
91
+ const count = countScreenFilesInDirectory(full);
92
+ if (count > 0) {
93
+ return { found: true, screenCount: count };
94
+ }
95
+ }
96
+ } catch {
97
+ }
98
+ }
99
+ return { found: false, screenCount: 0 };
100
+ }
101
+ function scoreMobileCandidate(dirPath, screenCount) {
102
+ let score = screenCount;
103
+ const folderName = path.basename(dirPath).toLowerCase();
104
+ if (/mobile|native|expo|app|client/.test(folderName)) {
105
+ score += 10;
106
+ }
107
+ if (fsSync.existsSync(path.join(dirPath, "app.json")) || fsSync.existsSync(path.join(dirPath, "eas.json"))) {
108
+ score += 25;
109
+ }
110
+ try {
111
+ const pkgPath = path.join(dirPath, "package.json");
112
+ if (fsSync.existsSync(pkgPath)) {
113
+ const pkgRaw = fsSync.readFileSync(pkgPath, "utf-8");
114
+ if (pkgRaw.includes('"react-native"') || pkgRaw.includes('"expo"')) {
115
+ score += 20;
116
+ }
117
+ }
118
+ } catch {
119
+ }
120
+ return score;
121
+ }
122
+ function findMobileAppRoot(rawRoot) {
123
+ const normalizedRoot = path.resolve(rawRoot);
124
+ if (!fsSync.existsSync(normalizedRoot)) {
125
+ return normalizedRoot;
126
+ }
127
+ const direct = hasDirectScreenFiles(normalizedRoot);
128
+ if (direct.found) {
129
+ return normalizedRoot;
130
+ }
131
+ let bestCandidate = null;
132
+ let bestScore = -1;
133
+ function scanSubdirs(parentDir, depth) {
134
+ if (depth > 2) return;
135
+ let entries = [];
136
+ try {
137
+ entries = fsSync.readdirSync(parentDir, { withFileTypes: true });
138
+ } catch {
139
+ return;
140
+ }
141
+ for (const entry of entries) {
142
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
143
+ if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
144
+ const subPath = path.join(parentDir, entry.name);
145
+ const check = hasDirectScreenFiles(subPath);
146
+ if (check.found) {
147
+ const score = scoreMobileCandidate(subPath, check.screenCount);
148
+ if (score > bestScore) {
149
+ bestScore = score;
150
+ bestCandidate = subPath;
151
+ }
152
+ } else if (depth < 2) {
153
+ scanSubdirs(subPath, depth + 1);
154
+ }
155
+ }
156
+ }
157
+ scanSubdirs(normalizedRoot, 1);
158
+ return bestCandidate ?? normalizedRoot;
159
+ }
160
+ function getActiveProjectRoot(fallbackRoot) {
161
+ const envTarget = process.env.ANTIGRAVITY_TARGET_PROJECT;
162
+ const baseRoot = envTarget && fsSync.existsSync(envTarget) ? path.resolve(envTarget) : path.resolve(fallbackRoot);
163
+ return findMobileAppRoot(baseRoot);
164
+ }
165
+ function resolveScreensDirectory(projectRoot) {
166
+ const normalizedRoot = findMobileAppRoot(projectRoot);
167
+ for (const rel of SCREEN_DIR_CANDIDATES) {
168
+ const full = path.join(normalizedRoot, rel);
169
+ if (fsSync.existsSync(full) && fsSync.statSync(full).isDirectory() && countScreenFilesInDirectory(full) > 0) {
170
+ return { dirPath: full, relDir: rel };
171
+ }
172
+ }
173
+ for (const rel of SCREEN_DIR_CANDIDATES) {
174
+ const full = path.join(normalizedRoot, rel);
175
+ if (fsSync.existsSync(full) && fsSync.statSync(full).isDirectory()) {
176
+ return { dirPath: full, relDir: rel };
177
+ }
178
+ }
179
+ return {
180
+ dirPath: path.join(normalizedRoot, "src/screens"),
181
+ relDir: "src/screens"
182
+ };
183
+ }
184
+ const FORBIDDEN_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
185
+ function safeRealpath(targetPath) {
186
+ try {
187
+ return fsSync.realpathSync(targetPath);
188
+ } catch {
189
+ return path.resolve(targetPath);
190
+ }
191
+ }
192
+ function resolveSafeScreenPath(workspaceRoot, relativeFile) {
193
+ const normalizedRoot = safeRealpath(getActiveProjectRoot(workspaceRoot));
194
+ const cleanRel = String(relativeFile ?? "").replace(/^[/\\]+/, "");
195
+ const resolved = path.resolve(normalizedRoot, cleanRel);
196
+ const allowedDirs = ["src/screens", "screens", "app", "src/app"].map((d) => {
197
+ const candidate = path.resolve(normalizedRoot, d);
198
+ return fsSync.existsSync(candidate) ? safeRealpath(candidate) : candidate;
199
+ });
200
+ const isLexicallyAllowed = allowedDirs.some(
201
+ (dir) => resolved.startsWith(dir + path.sep) || resolved === dir
202
+ );
203
+ if (!isLexicallyAllowed) {
204
+ throw new Error(
205
+ `Security violation: File path "${relativeFile}" is outside allowed screen directories (src/screens, screens, app)`
206
+ );
207
+ }
208
+ if (!resolved.endsWith(".tsx") && !resolved.endsWith(".jsx")) {
209
+ throw new Error(`Invalid file extension for "${relativeFile}". Only .tsx and .jsx are allowed.`);
210
+ }
211
+ if (fsSync.existsSync(resolved)) {
212
+ const stat = fsSync.lstatSync(resolved);
213
+ if (stat.isSymbolicLink()) {
214
+ throw new Error(
215
+ `Security violation: Symbolic link "${relativeFile}" is not permitted inside screen directories`
216
+ );
217
+ }
218
+ const realTarget = safeRealpath(resolved);
219
+ const isRealInsideAllowed = allowedDirs.some(
220
+ (dir) => realTarget.startsWith(dir + path.sep) || realTarget === dir
221
+ );
222
+ if (!isRealInsideAllowed) {
223
+ throw new Error(
224
+ `Security violation: Canonical path for "${relativeFile}" resolves outside allowed screen directories`
225
+ );
226
+ }
227
+ return realTarget;
228
+ }
229
+ return resolved;
230
+ }
231
+ function normalizeRouteTarget(rawTarget) {
232
+ const trimmed = rawTarget.replace(/^\/+/, "").replace(/\([^)]+\)\/?/g, "").replace(/\[\.\.\.([^\]]+)\]/g, "CatchAll_$1").replace(/\[([^\]]+)\]/g, "By_$1").replace(/Screen$/, "").replace(/\/index$/, "");
233
+ if (!trimmed || trimmed === "index") return "Home";
234
+ if (trimmed.includes("-") || trimmed.includes("/") || trimmed.includes("_") || trimmed === trimmed.toLowerCase()) {
235
+ return trimmed.split(/[-_/]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
236
+ }
237
+ return trimmed;
238
+ }
239
+ function extractLiteralValue(node) {
240
+ if (!node) return void 0;
241
+ if (t.isStringLiteral(node)) return node.value;
242
+ if (t.isNumericLiteral(node)) return node.value;
243
+ if (t.isUnaryExpression(node) && node.operator === "-" && t.isNumericLiteral(node.argument)) {
244
+ return -node.argument.value;
245
+ }
246
+ return void 0;
247
+ }
248
+ function collectStyleSheetMap(ast) {
249
+ let identifierName = "styles";
250
+ const styleObjects = /* @__PURE__ */ new Map();
251
+ const valuesMap = /* @__PURE__ */ new Map();
252
+ traverse(ast, {
253
+ VariableDeclarator(nodePath) {
254
+ const { id, init } = nodePath.node;
255
+ if (!t.isIdentifier(id) || !init) return;
256
+ const isStyleSheetCreate = t.isCallExpression(init) && t.isMemberExpression(init.callee) && t.isIdentifier(init.callee.object, { name: "StyleSheet" }) && t.isIdentifier(init.callee.property, { name: "create" }) && init.arguments.length > 0 && t.isObjectExpression(init.arguments[0]);
257
+ if (!isStyleSheetCreate) return;
258
+ identifierName = id.name;
259
+ const rootObj = init.arguments[0];
260
+ for (const prop of rootObj.properties) {
261
+ if (t.isObjectProperty(prop) && t.isObjectExpression(prop.value)) {
262
+ const keyName = t.isIdentifier(prop.key) ? prop.key.name : t.isStringLiteral(prop.key) ? prop.key.value : null;
263
+ if (!keyName) continue;
264
+ styleObjects.set(keyName, prop.value);
265
+ const styleRecord = {};
266
+ for (const rule of prop.value.properties) {
267
+ if (t.isObjectProperty(rule)) {
268
+ const ruleKey = t.isIdentifier(rule.key) ? rule.key.name : t.isStringLiteral(rule.key) ? rule.key.value : null;
269
+ const val = extractLiteralValue(rule.value);
270
+ if (ruleKey && val !== void 0) {
271
+ styleRecord[ruleKey] = val;
272
+ }
273
+ }
274
+ }
275
+ valuesMap.set(keyName, styleRecord);
276
+ }
277
+ }
278
+ }
279
+ });
280
+ return { identifierName, styleObjects, valuesMap };
281
+ }
282
+ function inspectJsxTextChildren(jsxElement) {
283
+ const tagName = getJsxTagName(jsxElement.openingElement.name);
284
+ const isTextComponent = tagName === "Text" || tagName.endsWith(".Text");
285
+ let hasDynamicText = false;
286
+ const textParts = [];
287
+ let hasChildElements = false;
288
+ for (const child of jsxElement.children) {
289
+ if (t.isJSXText(child)) {
290
+ const cleaned = child.value.replace(/\s+/g, " ").trim();
291
+ if (cleaned) textParts.push(cleaned);
292
+ } else if (t.isJSXExpressionContainer(child)) {
293
+ if (t.isStringLiteral(child.expression)) {
294
+ textParts.push(child.expression.value);
295
+ } else if (t.isNumericLiteral(child.expression)) {
296
+ textParts.push(String(child.expression.value));
297
+ } else if (!t.isJSXEmptyExpression(child.expression)) {
298
+ hasDynamicText = true;
299
+ }
300
+ } else if (t.isJSXElement(child) || t.isJSXFragment(child)) {
301
+ hasChildElements = true;
302
+ }
303
+ }
304
+ if (hasChildElements && !isTextComponent) {
305
+ return {
306
+ textContent: null,
307
+ hasDynamicText: false,
308
+ isTextEditable: false
309
+ };
310
+ }
311
+ const joined = textParts.join(" ").trim();
312
+ const isTextEditable = (isTextComponent || joined.length > 0) && !hasDynamicText && !hasChildElements;
313
+ return {
314
+ textContent: joined.length > 0 || isTextComponent ? joined : null,
315
+ hasDynamicText,
316
+ isTextEditable
317
+ };
318
+ }
319
+ function extractElementNavigationTarget(openingElement) {
320
+ for (const attr of openingElement.attributes) {
321
+ if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && ["onPress", "href", "to"].includes(attr.name.name)) {
322
+ if (t.isStringLiteral(attr.value)) {
323
+ return normalizeRouteTarget(attr.value.value);
324
+ }
325
+ if (t.isJSXExpressionContainer(attr.value)) {
326
+ const expr = attr.value.expression;
327
+ const codeStr = generate(expr).code;
328
+ const match = codeStr.match(/(?:navigate|push|replace|jumpTo)\(\s*['"`]([^'"`$]+)['"`]/);
329
+ if (match) return normalizeRouteTarget(match[1]);
330
+ const objMatch = codeStr.match(/(?:pathname|screen|route|name)\s*:\s*['"`]([^'"`$]+)['"`]/);
331
+ if (objMatch) return normalizeRouteTarget(objMatch[1]);
332
+ }
333
+ }
334
+ }
335
+ return null;
336
+ }
337
+ function resolveElementStyles(openingElement, styleSheetInfo) {
338
+ let styleSource = "none";
339
+ let styleSheetKey = null;
340
+ let inlineObject = null;
341
+ const styles = {};
342
+ const styleAttr = openingElement.attributes.find(
343
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: "style" })
344
+ );
345
+ if (!styleAttr || !t.isJSXExpressionContainer(styleAttr.value)) {
346
+ return { styleSource, styleSheetKey, inlineObject, styles };
347
+ }
348
+ const expr = styleAttr.value.expression;
349
+ const processExpression = (node) => {
350
+ if (t.isMemberExpression(node) && t.isIdentifier(node.object, { name: styleSheetInfo.identifierName }) && t.isIdentifier(node.property)) {
351
+ const key = node.property.name;
352
+ styleSheetKey = key;
353
+ styleSource = "stylesheet";
354
+ const sheetStyles = styleSheetInfo.valuesMap.get(key);
355
+ if (sheetStyles) {
356
+ Object.assign(styles, sheetStyles);
357
+ }
358
+ } else if (t.isObjectExpression(node)) {
359
+ inlineObject = node;
360
+ if (styleSource === "none") {
361
+ styleSource = "inline";
362
+ }
363
+ for (const prop of node.properties) {
364
+ if (t.isObjectProperty(prop)) {
365
+ const propKey = t.isIdentifier(prop.key) ? prop.key.name : t.isStringLiteral(prop.key) ? prop.key.value : null;
366
+ const val = extractLiteralValue(prop.value);
367
+ if (propKey && val !== void 0) {
368
+ styles[propKey] = val;
369
+ }
370
+ }
371
+ }
372
+ } else if (t.isArrayExpression(node)) {
373
+ for (const el of node.elements) {
374
+ if (el && (t.isExpression(el) || t.isSpreadElement(el))) {
375
+ processExpression(el);
376
+ }
377
+ }
378
+ }
379
+ };
380
+ processExpression(expr);
381
+ return { styleSource, styleSheetKey, inlineObject, styles };
382
+ }
383
+ function discoverScreenFilesSync(workspaceRoot) {
384
+ const activeRoot = getActiveProjectRoot(workspaceRoot);
385
+ const { dirPath: screensDir, relDir } = resolveScreensDirectory(activeRoot);
386
+ const collectedRelFromScreensDir = [];
387
+ function walk(currDir, relPrefix, depth) {
388
+ if (depth > 4) return;
389
+ let dirents = [];
390
+ try {
391
+ dirents = fsSync.readdirSync(currDir, { withFileTypes: true });
392
+ } catch {
393
+ return;
394
+ }
395
+ for (const entry of dirents) {
396
+ if (entry.name.startsWith(".")) continue;
397
+ if (entry.isDirectory()) {
398
+ if (entry.name === "node_modules" || entry.name === "__tests__" || entry.name === "components") {
399
+ continue;
400
+ }
401
+ walk(
402
+ path.join(currDir, entry.name),
403
+ relPrefix ? `${relPrefix}/${entry.name}` : entry.name,
404
+ depth + 1
405
+ );
406
+ } else if (entry.isFile()) {
407
+ const f = entry.name;
408
+ if (f.startsWith("_") || f.startsWith("+")) continue;
409
+ if (f === "index.ts" || f === "index.js") continue;
410
+ if (f.includes(".test.") || f.includes(".spec.") || f.endsWith(".d.ts")) continue;
411
+ if (!f.endsWith(".tsx") && !f.endsWith(".jsx")) continue;
412
+ collectedRelFromScreensDir.push(relPrefix ? `${relPrefix}/${f}` : f);
413
+ }
414
+ }
415
+ }
416
+ walk(screensDir, "", 0);
417
+ collectedRelFromScreensDir.sort();
418
+ const seenIds = /* @__PURE__ */ new Set();
419
+ const files = [];
420
+ for (const subRel of collectedRelFromScreensDir) {
421
+ const relPath = `${relDir}/${subRel}`.replace(/\\/g, "/");
422
+ const fullPath = path.join(screensDir, subRel);
423
+ const noExt = subRel.replace(/\.(tsx|jsx)$/, "");
424
+ const baseFileName = path.basename(noExt);
425
+ let routeName = normalizeRouteTarget(noExt);
426
+ if (seenIds.has(routeName)) {
427
+ routeName = normalizeRouteTarget(noExt.replace(/[()]/g, ""));
428
+ }
429
+ if (seenIds.has(routeName)) {
430
+ let suffix = 2;
431
+ while (seenIds.has(`${routeName}${suffix}`)) {
432
+ suffix++;
433
+ }
434
+ routeName = `${routeName}${suffix}`;
435
+ }
436
+ seenIds.add(routeName);
437
+ const displayName = baseFileName.endsWith("Screen") ? baseFileName : `${routeName}Screen`;
438
+ files.push({
439
+ id: routeName,
440
+ name: displayName,
441
+ routeName,
442
+ relPath,
443
+ fullPath
444
+ });
445
+ }
446
+ return files;
447
+ }
448
+ function computeScreenStem(idOrName) {
449
+ return idOrName.toLowerCase().replace(/(?:screen|page|view|tab|modal|stack|details|detail|list)$/i, "").replace(/[-_/\s]+/g, "").replace(/s$/, "");
450
+ }
451
+ function unwrapAstNode(node) {
452
+ let curr = node ?? null;
453
+ while (curr) {
454
+ if (t.isTSAsExpression(curr) || t.isTSTypeAssertion(curr) || t.isTSNonNullExpression(curr) || t.isTSSatisfiesExpression(curr) || t.isParenthesizedExpression(curr)) {
455
+ curr = curr.expression;
456
+ } else {
457
+ break;
458
+ }
459
+ }
460
+ return curr;
461
+ }
462
+ function extractRouteStringsFromAstNode(node, depth = 0) {
463
+ if (depth > 4) return [];
464
+ const unwrapped = unwrapAstNode(node);
465
+ if (!unwrapped) return [];
466
+ if (t.isStringLiteral(unwrapped)) {
467
+ return [unwrapped.value];
468
+ }
469
+ if (t.isTemplateLiteral(unwrapped)) {
470
+ const joined = unwrapped.quasis.map((q) => q.value.cooked ?? q.value.raw).join("id");
471
+ return joined ? [joined] : [];
472
+ }
473
+ if (t.isConditionalExpression(unwrapped)) {
474
+ return [
475
+ ...extractRouteStringsFromAstNode(unwrapped.consequent, depth + 1),
476
+ ...extractRouteStringsFromAstNode(unwrapped.alternate, depth + 1)
477
+ ];
478
+ }
479
+ if (t.isLogicalExpression(unwrapped)) {
480
+ return [
481
+ ...extractRouteStringsFromAstNode(unwrapped.left, depth + 1),
482
+ ...extractRouteStringsFromAstNode(unwrapped.right, depth + 1)
483
+ ];
484
+ }
485
+ if (t.isArrayExpression(unwrapped)) {
486
+ const out = [];
487
+ for (const el of unwrapped.elements) {
488
+ if (el) out.push(...extractRouteStringsFromAstNode(el, depth + 1));
489
+ }
490
+ return out;
491
+ }
492
+ if (t.isObjectExpression(unwrapped)) {
493
+ const out = [];
494
+ const routeKeys = /* @__PURE__ */ new Set([
495
+ "screen",
496
+ "pathname",
497
+ "route",
498
+ "href",
499
+ "name",
500
+ "path",
501
+ "target",
502
+ "to",
503
+ "destination",
504
+ "params",
505
+ "routes"
506
+ ]);
507
+ for (const prop of unwrapped.properties) {
508
+ if (t.isObjectProperty(prop)) {
509
+ const keyName = t.isIdentifier(prop.key) ? prop.key.name : t.isStringLiteral(prop.key) ? prop.key.value : "";
510
+ if (routeKeys.has(keyName)) {
511
+ out.push(...extractRouteStringsFromAstNode(prop.value, depth + 1));
512
+ }
513
+ }
514
+ }
515
+ return out;
516
+ }
517
+ return [];
518
+ }
519
+ function resolveCandidateToScreenId(rawCandidate, screenFiles, currentScreenId, allowUnmatchedFallback = false) {
520
+ const trimmed = rawCandidate.trim();
521
+ if (!trimmed || trimmed.length > 80) return null;
522
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("mailto:") || trimmed.startsWith("tel:") || trimmed.startsWith("#") || /\.(png|jpg|jpeg|svg|webp|gif|json|css)$/i.test(trimmed)) {
523
+ return null;
524
+ }
525
+ const normalized = normalizeRouteTarget(trimmed);
526
+ if (!normalized || normalized.toLowerCase() === currentScreenId.toLowerCase()) {
527
+ return null;
528
+ }
529
+ for (const s of screenFiles) {
530
+ if (s.id.toLowerCase() === currentScreenId.toLowerCase()) continue;
531
+ const sIdLower = s.id.toLowerCase();
532
+ const sNameLower = s.name.toLowerCase();
533
+ const normLower = normalized.toLowerCase();
534
+ if (sIdLower === normLower || sNameLower === normLower || sNameLower === `${normLower}screen`) {
535
+ return s.id;
536
+ }
537
+ }
538
+ const segments = trimmed.split(/[/.]/).map((seg) => seg.trim()).filter(
539
+ (seg) => seg.length > 0 && !seg.startsWith("(") && !seg.startsWith("[") && seg.toLowerCase() !== "id" && seg.toLowerCase() !== "index" && !/^\d+$/.test(seg)
540
+ );
541
+ for (let i = segments.length - 1; i >= 0; i--) {
542
+ const segNorm = normalizeRouteTarget(segments[i]).toLowerCase();
543
+ for (const s of screenFiles) {
544
+ if (s.id.toLowerCase() === currentScreenId.toLowerCase()) continue;
545
+ if (s.id.toLowerCase() === segNorm || s.name.toLowerCase() === `${segNorm}screen`) {
546
+ return s.id;
547
+ }
548
+ }
549
+ }
550
+ const candidateStems = [
551
+ computeScreenStem(normalized),
552
+ ...segments.map((seg) => computeScreenStem(seg))
553
+ ].filter((st) => st.length >= 4);
554
+ for (const candStem of candidateStems) {
555
+ for (const s of screenFiles) {
556
+ if (s.id.toLowerCase() === currentScreenId.toLowerCase()) continue;
557
+ const targetStem = computeScreenStem(s.id);
558
+ if (targetStem.length < 3) continue;
559
+ if (targetStem === candStem || candStem.length >= 4 && s.id.toLowerCase().startsWith(candStem) || targetStem.length >= 4 && candStem.endsWith(targetStem)) {
560
+ return s.id;
561
+ }
562
+ }
563
+ }
564
+ return allowUnmatchedFallback ? normalized : null;
565
+ }
566
+ async function discoverScreens(workspaceRoot) {
567
+ const screenFiles = discoverScreenFilesSync(workspaceRoot);
568
+ const results = [];
569
+ const backNavigationMeta = /* @__PURE__ */ new Map();
570
+ for (const item of screenFiles) {
571
+ const { id: routeName, name, relPath, fullPath } = item;
572
+ try {
573
+ const code = await fs.readFile(fullPath, "utf-8");
574
+ const ast = parse(code, {
575
+ sourceType: "module",
576
+ plugins: ["typescript", "jsx"]
577
+ });
578
+ let elementCount = 0;
579
+ const outgoingFlows = [];
580
+ let hasBackNavigation = false;
581
+ let backLine = 1;
582
+ const addFlowEdge = (targetScreenId, line, customLabel) => {
583
+ if (!targetScreenId || targetScreenId === routeName) return;
584
+ if (outgoingFlows.some((e) => e.fromScreen === routeName && e.toScreen === targetScreenId)) {
585
+ return;
586
+ }
587
+ outgoingFlows.push({
588
+ id: `${routeName}->${targetScreenId}:${line}`,
589
+ fromScreen: routeName,
590
+ toScreen: targetScreenId,
591
+ triggerLabel: customLabel || `Navigate to ${targetScreenId}`,
592
+ line
593
+ });
594
+ };
595
+ const extractTriggerLabelFromAncestry = (nodePath, fallbackTarget) => {
596
+ const jsxParent = nodePath.findParent((p) => p.isJSXElement());
597
+ if (jsxParent) {
598
+ const rawCode = generate(jsxParent.node).code;
599
+ const textMatches = Array.from(rawCode.matchAll(/>([^<>{}\n]+)</g)).map((m) => m[1].trim()).filter(Boolean);
600
+ if (textMatches.length > 0) {
601
+ return textMatches.slice(0, 2).join(" · ");
602
+ }
603
+ }
604
+ return `Navigate to ${fallbackTarget}`;
605
+ };
606
+ traverse(ast, {
607
+ JSXOpeningElement(nodePath) {
608
+ var _a;
609
+ const tagName = getJsxTagName(nodePath.node.name);
610
+ if (tagName !== "Fragment" && tagName !== "React.Fragment" && tagName !== "") {
611
+ elementCount++;
612
+ }
613
+ const line = ((_a = nodePath.node.loc) == null ? void 0 : _a.start.line) ?? 1;
614
+ for (const attr of nodePath.node.attributes) {
615
+ if (!t.isJSXAttribute(attr) || !t.isJSXIdentifier(attr.name)) continue;
616
+ const attrName = attr.name.name;
617
+ if (["href", "to", "route", "screen", "target", "pathname"].includes(attrName)) {
618
+ const valNode = t.isJSXExpressionContainer(attr.value) ? attr.value.expression : attr.value;
619
+ const candidates = extractRouteStringsFromAstNode(valNode);
620
+ for (const raw of candidates) {
621
+ const resolved = resolveCandidateToScreenId(raw, screenFiles, routeName, false);
622
+ if (resolved) {
623
+ addFlowEdge(resolved, line, extractTriggerLabelFromAncestry(nodePath, resolved));
624
+ }
625
+ }
626
+ }
627
+ if (attrName === "onPress" && t.isJSXExpressionContainer(attr.value)) {
628
+ const exprCode = generate(attr.value.expression).code;
629
+ if (/\b(?:goBack|back|pop|popToTop)\s*\(/.test(exprCode)) {
630
+ hasBackNavigation = true;
631
+ backLine = line;
632
+ }
633
+ if (/\b(?:signOut|logout|logOut)\b/i.test(exprCode)) {
634
+ const loginScreen2 = screenFiles.find((s) => s.id.toLowerCase() === "login");
635
+ if (loginScreen2 && loginScreen2.id !== routeName) {
636
+ addFlowEdge(loginScreen2.id, line, "Sign Out");
637
+ }
638
+ }
639
+ for (const s of screenFiles) {
640
+ if (s.id === routeName) continue;
641
+ const stem = computeScreenStem(s.id);
642
+ if (stem.length >= 4) {
643
+ const actionRegex = new RegExp(
644
+ `(?:on|handle|goTo|navigateTo|open|view|show|select)${s.id}|${s.id}(?:Press|Click|Action|Screen|Route)`,
645
+ "i"
646
+ );
647
+ if (actionRegex.test(exprCode)) {
648
+ addFlowEdge(s.id, line, extractTriggerLabelFromAncestry(nodePath, s.id));
649
+ }
650
+ }
651
+ }
652
+ }
653
+ }
654
+ },
655
+ CallExpression(nodePath) {
656
+ var _a;
657
+ const { callee, arguments: args } = nodePath.node;
658
+ const line = ((_a = nodePath.node.loc) == null ? void 0 : _a.start.line) ?? 1;
659
+ let methodName = "";
660
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
661
+ methodName = callee.property.name;
662
+ } else if (t.isIdentifier(callee)) {
663
+ methodName = callee.name;
664
+ }
665
+ if (["goBack", "back", "pop", "popToTop"].includes(methodName)) {
666
+ hasBackNavigation = true;
667
+ backLine = line;
668
+ return;
669
+ }
670
+ if (/^(?:signOut|logout|logOut)$/i.test(methodName)) {
671
+ const loginScreen2 = screenFiles.find((s) => s.id.toLowerCase() === "login");
672
+ if (loginScreen2 && loginScreen2.id !== routeName) {
673
+ addFlowEdge(loginScreen2.id, line, "Sign Out");
674
+ }
675
+ return;
676
+ }
677
+ const isDirectNavMethod = ["navigate", "push", "replace", "jumpTo", "reset"].includes(methodName);
678
+ const isHelperNavCall = /^(?:navigateTo|goTo|openScreen|handleNavigate|onNavigate)$/i.test(methodName);
679
+ if (!isDirectNavMethod && !isHelperNavCall) return;
680
+ const extractedCandidates = [];
681
+ for (const arg of args) {
682
+ extractedCandidates.push(...extractRouteStringsFromAstNode(arg));
683
+ }
684
+ let matchedAny = false;
685
+ for (const raw of extractedCandidates) {
686
+ const resolved = resolveCandidateToScreenId(raw, screenFiles, routeName, false);
687
+ if (resolved) {
688
+ matchedAny = true;
689
+ addFlowEdge(resolved, line, extractTriggerLabelFromAncestry(nodePath, resolved));
690
+ }
691
+ }
692
+ if (!matchedAny && isDirectNavMethod && extractedCandidates.length > 0) {
693
+ const fallbackTarget = normalizeRouteTarget(extractedCandidates[0]);
694
+ if (fallbackTarget && fallbackTarget !== routeName) {
695
+ addFlowEdge(fallbackTarget, line, extractTriggerLabelFromAncestry(nodePath, fallbackTarget));
696
+ }
697
+ }
698
+ },
699
+ ObjectProperty(nodePath) {
700
+ var _a;
701
+ const { key, value } = nodePath.node;
702
+ const keyName = t.isIdentifier(key) ? key.name : t.isStringLiteral(key) ? key.value : "";
703
+ if (!keyName) return;
704
+ const isExplicitRouteProp = [
705
+ "route",
706
+ "screen",
707
+ "href",
708
+ "pathname",
709
+ "path",
710
+ "target",
711
+ "destination",
712
+ "screenName",
713
+ "nav",
714
+ "tab"
715
+ ].includes(keyName);
716
+ const isItemActionProp = ["id", "key", "slug", "action"].includes(keyName);
717
+ if (!isExplicitRouteProp && !isItemActionProp) return;
718
+ const candidates = extractRouteStringsFromAstNode(value);
719
+ const line = ((_a = nodePath.node.loc) == null ? void 0 : _a.start.line) ?? 1;
720
+ for (const raw of candidates) {
721
+ if (isExplicitRouteProp) {
722
+ const resolved = resolveCandidateToScreenId(raw, screenFiles, routeName, false);
723
+ if (resolved) {
724
+ addFlowEdge(resolved, line, `Navigate to ${resolved}`);
725
+ }
726
+ } else if (isItemActionProp) {
727
+ const norm = normalizeRouteTarget(raw);
728
+ const exactScreen = screenFiles.find(
729
+ (s) => s.id !== routeName && s.id.length >= 4 && s.id.toLowerCase() === norm.toLowerCase()
730
+ );
731
+ if (exactScreen) {
732
+ addFlowEdge(exactScreen.id, line, `Open ${exactScreen.id}`);
733
+ }
734
+ }
735
+ }
736
+ },
737
+ JSXText(nodePath) {
738
+ var _a;
739
+ const txt = nodePath.node.value.trim();
740
+ if (/^(?:←\s*)?Back$/i.test(txt)) {
741
+ hasBackNavigation = true;
742
+ backLine = ((_a = nodePath.node.loc) == null ? void 0 : _a.start.line) ?? 1;
743
+ }
744
+ }
745
+ });
746
+ backNavigationMeta.set(routeName, { hasBack: hasBackNavigation, line: backLine });
747
+ results.push({
748
+ id: routeName,
749
+ name,
750
+ routeName,
751
+ filePath: relPath,
752
+ elementCount,
753
+ outgoingFlows
754
+ });
755
+ } catch (err) {
756
+ const msg = err instanceof Error ? err.message : String(err);
757
+ results.push({
758
+ id: routeName,
759
+ name,
760
+ routeName,
761
+ filePath: relPath,
762
+ elementCount: 0,
763
+ outgoingFlows: [],
764
+ parseError: msg
765
+ });
766
+ }
767
+ }
768
+ const screenMap = new Map(results.map((s) => [s.id, s]));
769
+ const homeScreen = results.find((s) => s.id.toLowerCase() === "home") ?? results.find((s) => ["dashboard", "main", "index", "feed"].includes(s.id.toLowerCase()));
770
+ const loginScreen = results.find((s) => ["login", "signin", "auth"].includes(s.id.toLowerCase()));
771
+ const signupScreen = results.find((s) => ["signup", "register", "createaccount"].includes(s.id.toLowerCase()));
772
+ const forgotScreen = results.find((s) => s.id.toLowerCase().includes("forgot"));
773
+ const profileScreen = results.find((s) => s.id.toLowerCase() === "profile");
774
+ const ensureEdge = (fromId, toId, label, isReturn = false, line = 1) => {
775
+ if (!fromId || !toId || fromId === toId) return;
776
+ const fromScr = screenMap.get(fromId);
777
+ if (!fromScr || !screenMap.has(toId)) return;
778
+ if (fromScr.outgoingFlows.some((e) => e.toScreen === toId)) return;
779
+ fromScr.outgoingFlows.push({
780
+ id: `${fromId}->${toId}${isReturn ? ":return" : ""}:${line}`,
781
+ fromScreen: fromId,
782
+ toScreen: toId,
783
+ triggerLabel: label,
784
+ line
785
+ });
786
+ };
787
+ if (loginScreen && homeScreen) {
788
+ ensureEdge(loginScreen.id, homeScreen.id, `Sign In → ${homeScreen.id}`);
789
+ }
790
+ if (signupScreen && loginScreen) {
791
+ ensureEdge(signupScreen.id, loginScreen.id, `Back to ${loginScreen.id}`, true);
792
+ }
793
+ if (signupScreen && homeScreen) {
794
+ ensureEdge(signupScreen.id, homeScreen.id, `Continue → ${homeScreen.id}`);
795
+ }
796
+ if (forgotScreen && loginScreen) {
797
+ ensureEdge(forgotScreen.id, loginScreen.id, `Back to ${loginScreen.id}`, true);
798
+ }
799
+ for (const scr of results) {
800
+ const lower = scr.id.toLowerCase();
801
+ if (profileScreen && (lower === "settings" || lower === "wallet" || lower === "editprofile")) {
802
+ ensureEdge(profileScreen.id, scr.id, `Open ${scr.id}`);
803
+ }
804
+ if (lower.includes("dealer") || lower.includes("listing") || lower.includes("vehicle")) {
805
+ const marketScreen = results.find(
806
+ (s) => ["marketplace", "catalog", "shop", "explore", "search"].includes(s.id.toLowerCase())
807
+ );
808
+ if (marketScreen && marketScreen.id !== scr.id) {
809
+ ensureEdge(marketScreen.id, scr.id, `View ${scr.id}`);
810
+ }
811
+ }
812
+ }
813
+ if (homeScreen) {
814
+ for (const scr of results) {
815
+ if (scr.id === homeScreen.id) continue;
816
+ if (scr.id === (loginScreen == null ? void 0 : loginScreen.id) || scr.id === (signupScreen == null ? void 0 : signupScreen.id) || scr.id === (forgotScreen == null ? void 0 : forgotScreen.id)) {
817
+ continue;
818
+ }
819
+ const hasIncoming = results.some(
820
+ (other) => other.outgoingFlows.some((e) => e.toScreen === scr.id)
821
+ );
822
+ if (!hasIncoming) {
823
+ ensureEdge(homeScreen.id, scr.id, `${homeScreen.id} → ${scr.id}`);
824
+ }
825
+ }
826
+ }
827
+ const forwardSnapshot = results.map((s) => ({
828
+ id: s.id,
829
+ targets: s.outgoingFlows.map((e) => e.toScreen)
830
+ }));
831
+ for (const scr of results) {
832
+ const backMeta = backNavigationMeta.get(scr.id);
833
+ if (!(backMeta == null ? void 0 : backMeta.hasBack)) continue;
834
+ const callers = forwardSnapshot.filter((caller) => caller.id !== scr.id && caller.targets.includes(scr.id)).map((caller) => caller.id);
835
+ if (callers.length > 0) {
836
+ const primaryCaller = callers.find((c) => c !== (homeScreen == null ? void 0 : homeScreen.id)) ?? callers[0];
837
+ ensureEdge(
838
+ scr.id,
839
+ primaryCaller,
840
+ `Back to ${primaryCaller}`,
841
+ true,
842
+ backMeta.line
843
+ );
844
+ } else if (homeScreen && scr.id !== homeScreen.id) {
845
+ ensureEdge(scr.id, homeScreen.id, `Back to ${homeScreen.id}`, true, backMeta.line);
846
+ }
847
+ }
848
+ return results;
849
+ }
850
+ async function inspectElement(workspaceRoot, params) {
851
+ var _a, _b;
852
+ const fullPath = resolveSafeScreenPath(workspaceRoot, params.file);
853
+ const code = await fs.readFile(fullPath, "utf-8");
854
+ const ast = parse(code, {
855
+ sourceType: "module",
856
+ plugins: ["typescript", "jsx"]
857
+ });
858
+ const styleSheetInfo = collectStyleSheetMap(ast);
859
+ let matchedElement = null;
860
+ let matchedIndex = -1;
861
+ let currentIndex = 0;
862
+ const targetIndex = Number.parseInt(params.astPath, 10);
863
+ traverse(ast, {
864
+ JSXOpeningElement(nodePath) {
865
+ var _a2;
866
+ const tagName2 = getJsxTagName(nodePath.node.name);
867
+ if (tagName2 === "Fragment" || tagName2 === "React.Fragment" || tagName2 === "") {
868
+ return;
869
+ }
870
+ const idx = currentIndex++;
871
+ const parentJsx = nodePath.parentPath.isJSXElement() ? nodePath.parentPath.node : null;
872
+ if (!parentJsx) return;
873
+ if (!Number.isNaN(targetIndex) && idx === targetIndex) {
874
+ matchedElement = parentJsx;
875
+ matchedIndex = idx;
876
+ } else if (!matchedElement && params.line && ((_a2 = nodePath.node.loc) == null ? void 0 : _a2.start.line) === params.line) {
877
+ matchedElement = parentJsx;
878
+ matchedIndex = idx;
879
+ }
880
+ }
881
+ });
882
+ if (!matchedElement) {
883
+ throw new Error(`Element at astPath=${params.astPath} (line=${params.line ?? "?"}) not found in ${params.file}`);
884
+ }
885
+ const element = matchedElement;
886
+ const opening = element.openingElement;
887
+ const tagName = getJsxTagName(opening.name);
888
+ const line = ((_a = opening.loc) == null ? void 0 : _a.start.line) ?? 1;
889
+ const col = ((_b = opening.loc) == null ? void 0 : _b.start.column) ?? 0;
890
+ const textInfo = inspectJsxTextChildren(element);
891
+ const styleInfo = resolveElementStyles(opening, styleSheetInfo);
892
+ const navigationTarget = extractElementNavigationTarget(opening);
893
+ const snippetRaw = generate(element, { concise: false }).code;
894
+ const codeSnippet = snippetRaw.length > 600 ? `${snippetRaw.slice(0, 600)}
895
+ /* ... */` : snippetRaw;
896
+ return {
897
+ file: params.file,
898
+ astPath: String(matchedIndex),
899
+ tagName,
900
+ line,
901
+ col,
902
+ textContent: textInfo.textContent,
903
+ hasDynamicText: textInfo.hasDynamicText,
904
+ isTextEditable: textInfo.isTextEditable,
905
+ styleSource: styleInfo.styleSource,
906
+ styleSheetKey: styleInfo.styleSheetKey,
907
+ styles: styleInfo.styles,
908
+ codeSnippet,
909
+ navigationTarget
910
+ };
911
+ }
912
+ function createSafeJsxTextChild(text) {
913
+ if (/[{}<>]|[\r\n]|^\s|\s$/.test(text)) {
914
+ return t.jsxExpressionContainer(t.stringLiteral(text));
915
+ }
916
+ return t.jsxText(text);
917
+ }
918
+ function upsertObjectProperty(objExpr, key, val) {
919
+ if (!key || FORBIDDEN_OBJECT_KEYS.has(key) || key.length > 80) {
920
+ throw new Error(`Security violation: Invalid style property name "${key}"`);
921
+ }
922
+ const existingIndex = objExpr.properties.findIndex((prop) => {
923
+ if (!t.isObjectProperty(prop)) return false;
924
+ const k = t.isIdentifier(prop.key) ? prop.key.name : t.isStringLiteral(prop.key) ? prop.key.value : null;
925
+ return k === key;
926
+ });
927
+ if (val === null || val === void 0 || val === "") {
928
+ if (existingIndex !== -1) {
929
+ objExpr.properties.splice(existingIndex, 1);
930
+ }
931
+ return;
932
+ }
933
+ if (typeof val !== "number" && typeof val !== "string") {
934
+ throw new Error(`Security violation: Style value for "${key}" must be a string or finite number`);
935
+ }
936
+ if (typeof val === "number" && !Number.isFinite(val)) {
937
+ throw new Error(`Security violation: Style value for "${key}" must be a finite number`);
938
+ }
939
+ if (typeof val === "string" && val.length > 500) {
940
+ throw new Error(`Security violation: Style value for "${key}" exceeds maximum length`);
941
+ }
942
+ const valueNode = typeof val === "number" ? val < 0 ? t.unaryExpression("-", t.numericLiteral(Math.abs(val))) : t.numericLiteral(val) : t.stringLiteral(val);
943
+ if (existingIndex !== -1) {
944
+ const existingProp = objExpr.properties[existingIndex];
945
+ existingProp.value = valueNode;
946
+ } else {
947
+ const keyNode = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? t.identifier(key) : t.stringLiteral(key);
948
+ objExpr.properties.push(t.objectProperty(keyNode, valueNode));
949
+ }
950
+ }
951
+ async function patchElement(workspaceRoot, payload) {
952
+ const fullPath = resolveSafeScreenPath(workspaceRoot, payload.file);
953
+ return withFileLock(fullPath, async () => {
954
+ const previousContent = await fs.readFile(fullPath, "utf-8");
955
+ const ast = parse(previousContent, {
956
+ sourceType: "module",
957
+ plugins: ["typescript", "jsx"]
958
+ });
959
+ const styleSheetInfo = collectStyleSheetMap(ast);
960
+ const targetIndex = Number.parseInt(payload.astPath, 10);
961
+ let matchedElement = null;
962
+ let currentIndex = 0;
963
+ traverse(ast, {
964
+ JSXOpeningElement(nodePath) {
965
+ var _a;
966
+ const tagName = getJsxTagName(nodePath.node.name);
967
+ if (tagName === "Fragment" || tagName === "React.Fragment" || tagName === "") {
968
+ return;
969
+ }
970
+ const idx = currentIndex++;
971
+ const parentJsx = nodePath.parentPath.isJSXElement() ? nodePath.parentPath.node : null;
972
+ if (!parentJsx) return;
973
+ if (!Number.isNaN(targetIndex) && idx === targetIndex) {
974
+ matchedElement = parentJsx;
975
+ } else if (!matchedElement && payload.line && ((_a = nodePath.node.loc) == null ? void 0 : _a.start.line) === payload.line) {
976
+ matchedElement = parentJsx;
977
+ }
978
+ }
979
+ });
980
+ if (!matchedElement) {
981
+ throw new Error(`Target JSX element (astPath=${payload.astPath}) not found in ${payload.file}`);
982
+ }
983
+ const element = matchedElement;
984
+ const opening = element.openingElement;
985
+ if (payload.text !== void 0) {
986
+ if (typeof payload.text !== "string") {
987
+ throw new Error("Invalid text payload: must be a string.");
988
+ }
989
+ const textCheck = inspectJsxTextChildren(element);
990
+ if (textCheck.hasDynamicText) {
991
+ throw new Error(
992
+ 'Cannot overwrite dynamic JSX expression inside <Text>. Use "Ask Antigravity" for structural/expression edits.'
993
+ );
994
+ }
995
+ element.children = [createSafeJsxTextChild(payload.text)];
996
+ }
997
+ if (payload.styles && Object.keys(payload.styles).length > 0) {
998
+ const styleInfo = resolveElementStyles(opening, styleSheetInfo);
999
+ let targetObject = null;
1000
+ if (styleInfo.styleSheetKey && styleSheetInfo.styleObjects.has(styleInfo.styleSheetKey)) {
1001
+ targetObject = styleSheetInfo.styleObjects.get(styleInfo.styleSheetKey);
1002
+ } else if (styleInfo.inlineObject) {
1003
+ targetObject = styleInfo.inlineObject;
1004
+ } else {
1005
+ const newObj = t.objectExpression([]);
1006
+ const existingStyleAttrIndex = opening.attributes.findIndex(
1007
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: "style" })
1008
+ );
1009
+ if (existingStyleAttrIndex !== -1) {
1010
+ const existingAttr = opening.attributes[existingStyleAttrIndex];
1011
+ if (t.isJSXExpressionContainer(existingAttr.value) && t.isExpression(existingAttr.value.expression)) {
1012
+ existingAttr.value = t.jsxExpressionContainer(
1013
+ t.arrayExpression([existingAttr.value.expression, newObj])
1014
+ );
1015
+ }
1016
+ } else {
1017
+ opening.attributes.push(
1018
+ t.jsxAttribute(t.jsxIdentifier("style"), t.jsxExpressionContainer(newObj))
1019
+ );
1020
+ }
1021
+ targetObject = newObj;
1022
+ }
1023
+ for (const [styleKey, styleVal] of Object.entries(payload.styles)) {
1024
+ upsertObjectProperty(targetObject, styleKey, styleVal);
1025
+ }
1026
+ }
1027
+ const output = generate(ast, {
1028
+ retainLines: false,
1029
+ comments: true,
1030
+ jsescOption: { minimal: true }
1031
+ });
1032
+ const nextContent = output.code + "\n";
1033
+ if (nextContent !== previousContent) {
1034
+ await fs.writeFile(fullPath, nextContent, "utf-8");
1035
+ undoStack.push({
1036
+ file: payload.file,
1037
+ previousContent,
1038
+ nextContent,
1039
+ description: `Updated ${getJsxTagName(opening.name)} in ${path.basename(payload.file)}`,
1040
+ timestamp: Date.now()
1041
+ });
1042
+ if (undoStack.length > MAX_HISTORY) {
1043
+ undoStack.shift();
1044
+ }
1045
+ redoStack.length = 0;
1046
+ }
1047
+ const updatedElement = await inspectElement(workspaceRoot, {
1048
+ file: payload.file,
1049
+ astPath: payload.astPath
1050
+ });
1051
+ return {
1052
+ element: updatedElement,
1053
+ undoAvailable: undoStack.length,
1054
+ redoAvailable: redoStack.length
1055
+ };
1056
+ });
1057
+ }
1058
+ async function reorderElement(workspaceRoot, payload) {
1059
+ const fullPath = resolveSafeScreenPath(workspaceRoot, payload.file);
1060
+ return withFileLock(fullPath, async () => {
1061
+ const previousContent = await fs.readFile(fullPath, "utf-8");
1062
+ const ast = parse(previousContent, {
1063
+ sourceType: "module",
1064
+ plugins: ["typescript", "jsx"]
1065
+ });
1066
+ const byIndex = /* @__PURE__ */ new Map();
1067
+ const parentByElement = /* @__PURE__ */ new Map();
1068
+ let currentIndex = 0;
1069
+ traverse(ast, {
1070
+ JSXOpeningElement(nodePath) {
1071
+ var _a;
1072
+ const tagName = getJsxTagName(nodePath.node.name);
1073
+ if (tagName === "Fragment" || tagName === "React.Fragment" || tagName === "") {
1074
+ return;
1075
+ }
1076
+ const idx = currentIndex++;
1077
+ const jsxElPath = nodePath.parentPath;
1078
+ if (!jsxElPath.isJSXElement()) return;
1079
+ const element = jsxElPath.node;
1080
+ const parentNode = (_a = jsxElPath.parentPath) == null ? void 0 : _a.node;
1081
+ const parent = t.isJSXElement(parentNode) || t.isJSXFragment(parentNode) ? parentNode : null;
1082
+ byIndex.set(idx, { index: idx, element, parent });
1083
+ parentByElement.set(element, parent);
1084
+ }
1085
+ });
1086
+ const sourceIdx = Number.parseInt(payload.astPath, 10);
1087
+ const sourceInfo = byIndex.get(sourceIdx);
1088
+ if (!sourceInfo || !sourceInfo.parent) {
1089
+ throw new Error(`Element at astPath=${payload.astPath} cannot be reordered (no JSX parent container).`);
1090
+ }
1091
+ const parentContainer = sourceInfo.parent;
1092
+ const sourceElement = sourceInfo.element;
1093
+ const jsxSiblings = parentContainer.children.filter(
1094
+ (c) => t.isJSXElement(c)
1095
+ );
1096
+ if (jsxSiblings.length < 2) {
1097
+ throw new Error("Element has no sibling elements inside its parent container to reorder with.");
1098
+ }
1099
+ let targetSibling = null;
1100
+ let placement = payload.placement === "after" ? "after" : "before";
1101
+ if (payload.direction === "up" || payload.direction === "down") {
1102
+ const currPos = jsxSiblings.indexOf(sourceElement);
1103
+ if (currPos === -1) {
1104
+ throw new Error("Could not locate element inside parent children list.");
1105
+ }
1106
+ if (payload.direction === "up") {
1107
+ if (currPos === 0) {
1108
+ throw new Error("Element is already first in its container.");
1109
+ }
1110
+ targetSibling = jsxSiblings[currPos - 1];
1111
+ placement = "before";
1112
+ } else {
1113
+ if (currPos >= jsxSiblings.length - 1) {
1114
+ throw new Error("Element is already last in its container.");
1115
+ }
1116
+ targetSibling = jsxSiblings[currPos + 1];
1117
+ placement = "after";
1118
+ }
1119
+ } else if (payload.targetAstPath !== void 0) {
1120
+ const targetIdx = Number.parseInt(payload.targetAstPath, 10);
1121
+ const rawTargetInfo = byIndex.get(targetIdx);
1122
+ if (!rawTargetInfo) {
1123
+ throw new Error(`Target sibling at astPath=${payload.targetAstPath} not found.`);
1124
+ }
1125
+ let candidate = rawTargetInfo.element;
1126
+ while (candidate && parentByElement.get(candidate) !== parentContainer) {
1127
+ const p = parentByElement.get(candidate);
1128
+ candidate = p && t.isJSXElement(p) ? p : null;
1129
+ }
1130
+ if (!candidate || candidate === sourceElement) {
1131
+ throw new Error("Target drop element is not a sibling inside the same Flexbox container.");
1132
+ }
1133
+ targetSibling = candidate;
1134
+ }
1135
+ if (!targetSibling || targetSibling === sourceElement) {
1136
+ throw new Error("Invalid reorder target sibling.");
1137
+ }
1138
+ const meaningfulChildren = parentContainer.children.filter(
1139
+ (child) => !(t.isJSXText(child) && child.value.trim() === "")
1140
+ );
1141
+ const withoutSource = meaningfulChildren.filter((child) => child !== sourceElement);
1142
+ const targetPos = withoutSource.indexOf(targetSibling);
1143
+ if (targetPos === -1) {
1144
+ throw new Error("Target sibling not found in parent children.");
1145
+ }
1146
+ const insertIdx = placement === "after" ? targetPos + 1 : targetPos;
1147
+ withoutSource.splice(insertIdx, 0, sourceElement);
1148
+ parentContainer.children = withoutSource;
1149
+ let newSourceAstPath = payload.astPath;
1150
+ let reindexCounter = 0;
1151
+ traverse(ast, {
1152
+ JSXOpeningElement(nodePath) {
1153
+ const tagName = getJsxTagName(nodePath.node.name);
1154
+ if (tagName === "Fragment" || tagName === "React.Fragment" || tagName === "") {
1155
+ return;
1156
+ }
1157
+ const idx = reindexCounter++;
1158
+ if (nodePath.parentPath.isJSXElement() && nodePath.parentPath.node === sourceElement) {
1159
+ newSourceAstPath = String(idx);
1160
+ }
1161
+ }
1162
+ });
1163
+ const output = generate(ast, {
1164
+ retainLines: false,
1165
+ comments: true,
1166
+ jsescOption: { minimal: true }
1167
+ });
1168
+ const nextContent = output.code + "\n";
1169
+ if (nextContent !== previousContent) {
1170
+ await fs.writeFile(fullPath, nextContent, "utf-8");
1171
+ undoStack.push({
1172
+ file: payload.file,
1173
+ previousContent,
1174
+ nextContent,
1175
+ description: `Reordered ${getJsxTagName(sourceElement.openingElement.name)} in ${path.basename(payload.file)}`,
1176
+ timestamp: Date.now()
1177
+ });
1178
+ if (undoStack.length > MAX_HISTORY) {
1179
+ undoStack.shift();
1180
+ }
1181
+ redoStack.length = 0;
1182
+ }
1183
+ const updatedElement = await inspectElement(workspaceRoot, {
1184
+ file: payload.file,
1185
+ astPath: newSourceAstPath
1186
+ });
1187
+ return {
1188
+ element: updatedElement,
1189
+ undoAvailable: undoStack.length,
1190
+ redoAvailable: redoStack.length
1191
+ };
1192
+ });
1193
+ }
1194
+ const CONTAINER_TAG_NAMES = /* @__PURE__ */ new Set([
1195
+ "View",
1196
+ "ScrollView",
1197
+ "SafeAreaView",
1198
+ "KeyboardAvoidingView",
1199
+ "Pressable",
1200
+ "TouchableOpacity",
1201
+ "ImageBackground"
1202
+ ]);
1203
+ function buildStyleObjectExpression(styles) {
1204
+ return t.objectExpression(
1205
+ Object.entries(styles).map(
1206
+ ([k, v]) => t.objectProperty(
1207
+ t.identifier(k),
1208
+ typeof v === "number" ? t.numericLiteral(v) : t.stringLiteral(v)
1209
+ )
1210
+ )
1211
+ );
1212
+ }
1213
+ function buildInlineStyleAttr(styles) {
1214
+ return t.jsxAttribute(
1215
+ t.jsxIdentifier("style"),
1216
+ t.jsxExpressionContainer(buildStyleObjectExpression(styles))
1217
+ );
1218
+ }
1219
+ function ensureReactNativeImports(ast, requiredComponents) {
1220
+ const needed = new Set(requiredComponents);
1221
+ if (needed.size === 0) return;
1222
+ let rnImportDecl = null;
1223
+ for (const stmt of ast.program.body) {
1224
+ if (t.isImportDeclaration(stmt) && stmt.source.value === "react-native" && stmt.importKind !== "type") {
1225
+ rnImportDecl = stmt;
1226
+ break;
1227
+ }
1228
+ }
1229
+ if (rnImportDecl) {
1230
+ const existing = /* @__PURE__ */ new Set();
1231
+ for (const spec of rnImportDecl.specifiers) {
1232
+ if (t.isImportSpecifier(spec)) {
1233
+ const importedName = t.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
1234
+ existing.add(importedName);
1235
+ existing.add(spec.local.name);
1236
+ }
1237
+ }
1238
+ for (const comp of needed) {
1239
+ if (!existing.has(comp)) {
1240
+ rnImportDecl.specifiers.push(
1241
+ t.importSpecifier(t.identifier(comp), t.identifier(comp))
1242
+ );
1243
+ }
1244
+ }
1245
+ } else {
1246
+ const specifiers = Array.from(needed).map(
1247
+ (comp) => t.importSpecifier(t.identifier(comp), t.identifier(comp))
1248
+ );
1249
+ ast.program.body.unshift(
1250
+ t.importDeclaration(specifiers, t.stringLiteral("react-native"))
1251
+ );
1252
+ }
1253
+ }
1254
+ function buildPresetJsxElement(preset, options) {
1255
+ const cleanLabel = (options.label ?? "").trim();
1256
+ const cleanNav = (options.navigateTo ?? "").trim();
1257
+ switch (preset) {
1258
+ case "button": {
1259
+ const btnText = cleanLabel || (cleanNav ? `Go to ${cleanNav}` : "Continue");
1260
+ const attrs = [
1261
+ t.jsxAttribute(
1262
+ t.jsxIdentifier("activeOpacity"),
1263
+ t.jsxExpressionContainer(t.numericLiteral(0.8))
1264
+ ),
1265
+ buildInlineStyleAttr({
1266
+ backgroundColor: "#0F3460",
1267
+ paddingVertical: 14,
1268
+ paddingHorizontal: 20,
1269
+ borderRadius: 12,
1270
+ alignItems: "center",
1271
+ justifyContent: "center",
1272
+ marginTop: 12
1273
+ })
1274
+ ];
1275
+ if (cleanNav) {
1276
+ if (options.hasNavigationInScope) {
1277
+ attrs.push(
1278
+ t.jsxAttribute(
1279
+ t.jsxIdentifier("onPress"),
1280
+ t.jsxExpressionContainer(
1281
+ t.arrowFunctionExpression(
1282
+ [],
1283
+ t.callExpression(
1284
+ t.memberExpression(t.identifier("navigation"), t.identifier("navigate")),
1285
+ [t.stringLiteral(cleanNav)]
1286
+ )
1287
+ )
1288
+ )
1289
+ )
1290
+ );
1291
+ } else if (options.hasRouterInScope) {
1292
+ attrs.push(
1293
+ t.jsxAttribute(
1294
+ t.jsxIdentifier("onPress"),
1295
+ t.jsxExpressionContainer(
1296
+ t.arrowFunctionExpression(
1297
+ [],
1298
+ t.callExpression(
1299
+ t.memberExpression(t.identifier("router"), t.identifier("push")),
1300
+ [t.stringLiteral(`/${cleanNav.toLowerCase()}`)]
1301
+ )
1302
+ )
1303
+ )
1304
+ )
1305
+ );
1306
+ } else {
1307
+ attrs.push(t.jsxAttribute(t.jsxIdentifier("route"), t.stringLiteral(cleanNav)));
1308
+ }
1309
+ }
1310
+ const textChild = t.jsxElement(
1311
+ t.jsxOpeningElement(
1312
+ t.jsxIdentifier("Text"),
1313
+ [
1314
+ buildInlineStyleAttr({
1315
+ color: "#FFFFFF",
1316
+ fontSize: 14,
1317
+ fontWeight: "700"
1318
+ })
1319
+ ],
1320
+ false
1321
+ ),
1322
+ t.jsxClosingElement(t.jsxIdentifier("Text")),
1323
+ [t.jsxText(btnText)],
1324
+ false
1325
+ );
1326
+ return {
1327
+ element: t.jsxElement(
1328
+ t.jsxOpeningElement(t.jsxIdentifier("TouchableOpacity"), attrs, false),
1329
+ t.jsxClosingElement(t.jsxIdentifier("TouchableOpacity")),
1330
+ [textChild],
1331
+ false
1332
+ ),
1333
+ requiredImports: ["TouchableOpacity", "Text"]
1334
+ };
1335
+ }
1336
+ case "section": {
1337
+ const sectionTitle = cleanLabel || "Featured Section";
1338
+ const titleNode = t.jsxElement(
1339
+ t.jsxOpeningElement(
1340
+ t.jsxIdentifier("Text"),
1341
+ [
1342
+ buildInlineStyleAttr({
1343
+ fontSize: 16,
1344
+ fontWeight: "700",
1345
+ color: "#0F172A",
1346
+ marginBottom: 4
1347
+ })
1348
+ ],
1349
+ false
1350
+ ),
1351
+ t.jsxClosingElement(t.jsxIdentifier("Text")),
1352
+ [t.jsxText(sectionTitle)],
1353
+ false
1354
+ );
1355
+ const subtitleNode = t.jsxElement(
1356
+ t.jsxOpeningElement(
1357
+ t.jsxIdentifier("Text"),
1358
+ [
1359
+ buildInlineStyleAttr({
1360
+ fontSize: 13,
1361
+ lineHeight: 19,
1362
+ color: "#64748B"
1363
+ })
1364
+ ],
1365
+ false
1366
+ ),
1367
+ t.jsxClosingElement(t.jsxIdentifier("Text")),
1368
+ [t.jsxText("Add details, metrics, or actions inside this section card.")],
1369
+ false
1370
+ );
1371
+ return {
1372
+ element: t.jsxElement(
1373
+ t.jsxOpeningElement(
1374
+ t.jsxIdentifier("View"),
1375
+ [
1376
+ buildInlineStyleAttr({
1377
+ backgroundColor: "#F8FAFC",
1378
+ borderRadius: 16,
1379
+ padding: 16,
1380
+ marginTop: 12,
1381
+ borderWidth: 1,
1382
+ borderColor: "#E2E8F0"
1383
+ })
1384
+ ],
1385
+ false
1386
+ ),
1387
+ t.jsxClosingElement(t.jsxIdentifier("View")),
1388
+ [titleNode, subtitleNode],
1389
+ false
1390
+ ),
1391
+ requiredImports: ["View", "Text"]
1392
+ };
1393
+ }
1394
+ case "row": {
1395
+ const leftLabel = cleanLabel || "Row Item Title";
1396
+ const leftText = t.jsxElement(
1397
+ t.jsxOpeningElement(
1398
+ t.jsxIdentifier("Text"),
1399
+ [
1400
+ buildInlineStyleAttr({
1401
+ fontSize: 14,
1402
+ fontWeight: "600",
1403
+ color: "#0F172A"
1404
+ })
1405
+ ],
1406
+ false
1407
+ ),
1408
+ t.jsxClosingElement(t.jsxIdentifier("Text")),
1409
+ [t.jsxText(leftLabel)],
1410
+ false
1411
+ );
1412
+ const rightText = t.jsxElement(
1413
+ t.jsxOpeningElement(
1414
+ t.jsxIdentifier("Text"),
1415
+ [
1416
+ buildInlineStyleAttr({
1417
+ fontSize: 13,
1418
+ fontWeight: "600",
1419
+ color: "#6366F1"
1420
+ })
1421
+ ],
1422
+ false
1423
+ ),
1424
+ t.jsxClosingElement(t.jsxIdentifier("Text")),
1425
+ [t.jsxText("Details →")],
1426
+ false
1427
+ );
1428
+ return {
1429
+ element: t.jsxElement(
1430
+ t.jsxOpeningElement(
1431
+ t.jsxIdentifier("View"),
1432
+ [
1433
+ buildInlineStyleAttr({
1434
+ flexDirection: "row",
1435
+ alignItems: "center",
1436
+ justifyContent: "space-between",
1437
+ backgroundColor: "#F8FAFC",
1438
+ paddingVertical: 12,
1439
+ paddingHorizontal: 14,
1440
+ borderRadius: 12,
1441
+ marginTop: 8,
1442
+ borderWidth: 1,
1443
+ borderColor: "#E2E8F0"
1444
+ })
1445
+ ],
1446
+ false
1447
+ ),
1448
+ t.jsxClosingElement(t.jsxIdentifier("View")),
1449
+ [leftText, rightText],
1450
+ false
1451
+ ),
1452
+ requiredImports: ["View", "Text"]
1453
+ };
1454
+ }
1455
+ case "heading": {
1456
+ const headingText = cleanLabel || "New Heading";
1457
+ return {
1458
+ element: t.jsxElement(
1459
+ t.jsxOpeningElement(
1460
+ t.jsxIdentifier("Text"),
1461
+ [
1462
+ buildInlineStyleAttr({
1463
+ fontSize: 20,
1464
+ fontWeight: "700",
1465
+ color: "#0F172A",
1466
+ marginTop: 12,
1467
+ marginBottom: 4
1468
+ })
1469
+ ],
1470
+ false
1471
+ ),
1472
+ t.jsxClosingElement(t.jsxIdentifier("Text")),
1473
+ [t.jsxText(headingText)],
1474
+ false
1475
+ ),
1476
+ requiredImports: ["Text"]
1477
+ };
1478
+ }
1479
+ case "text": {
1480
+ const bodyCopy = cleanLabel || "Add your supporting paragraph description here.";
1481
+ return {
1482
+ element: t.jsxElement(
1483
+ t.jsxOpeningElement(
1484
+ t.jsxIdentifier("Text"),
1485
+ [
1486
+ buildInlineStyleAttr({
1487
+ fontSize: 14,
1488
+ lineHeight: 20,
1489
+ color: "#475569",
1490
+ marginTop: 6
1491
+ })
1492
+ ],
1493
+ false
1494
+ ),
1495
+ t.jsxClosingElement(t.jsxIdentifier("Text")),
1496
+ [t.jsxText(bodyCopy)],
1497
+ false
1498
+ ),
1499
+ requiredImports: ["Text"]
1500
+ };
1501
+ }
1502
+ case "input": {
1503
+ const placeholderText = cleanLabel || "Enter value...";
1504
+ return {
1505
+ element: t.jsxElement(
1506
+ t.jsxOpeningElement(
1507
+ t.jsxIdentifier("TextInput"),
1508
+ [
1509
+ t.jsxAttribute(t.jsxIdentifier("placeholder"), t.stringLiteral(placeholderText)),
1510
+ t.jsxAttribute(t.jsxIdentifier("placeholderTextColor"), t.stringLiteral("#94A3B8")),
1511
+ buildInlineStyleAttr({
1512
+ backgroundColor: "#F1F5F9",
1513
+ borderWidth: 1,
1514
+ borderColor: "#CBD5E1",
1515
+ borderRadius: 10,
1516
+ paddingHorizontal: 14,
1517
+ paddingVertical: 12,
1518
+ fontSize: 14,
1519
+ color: "#0F172A",
1520
+ marginTop: 10
1521
+ })
1522
+ ],
1523
+ true
1524
+ ),
1525
+ null,
1526
+ [],
1527
+ true
1528
+ ),
1529
+ requiredImports: ["TextInput"]
1530
+ };
1531
+ }
1532
+ case "badge": {
1533
+ const badgeLabel = cleanLabel || "NEW BADGE";
1534
+ const badgeText = t.jsxElement(
1535
+ t.jsxOpeningElement(
1536
+ t.jsxIdentifier("Text"),
1537
+ [
1538
+ buildInlineStyleAttr({
1539
+ fontSize: 11,
1540
+ fontWeight: "700",
1541
+ color: "#4F46E5",
1542
+ letterSpacing: 0.4
1543
+ })
1544
+ ],
1545
+ false
1546
+ ),
1547
+ t.jsxClosingElement(t.jsxIdentifier("Text")),
1548
+ [t.jsxText(badgeLabel)],
1549
+ false
1550
+ );
1551
+ return {
1552
+ element: t.jsxElement(
1553
+ t.jsxOpeningElement(
1554
+ t.jsxIdentifier("View"),
1555
+ [
1556
+ buildInlineStyleAttr({
1557
+ alignSelf: "flex-start",
1558
+ backgroundColor: "#EEF2FF",
1559
+ paddingHorizontal: 10,
1560
+ paddingVertical: 4,
1561
+ borderRadius: 999,
1562
+ marginTop: 8
1563
+ })
1564
+ ],
1565
+ false
1566
+ ),
1567
+ t.jsxClosingElement(t.jsxIdentifier("View")),
1568
+ [badgeText],
1569
+ false
1570
+ ),
1571
+ requiredImports: ["View", "Text"]
1572
+ };
1573
+ }
1574
+ case "divider":
1575
+ default: {
1576
+ return {
1577
+ element: t.jsxElement(
1578
+ t.jsxOpeningElement(
1579
+ t.jsxIdentifier("View"),
1580
+ [
1581
+ buildInlineStyleAttr({
1582
+ height: 1,
1583
+ backgroundColor: "#E2E8F0",
1584
+ marginVertical: 14,
1585
+ width: "100%"
1586
+ })
1587
+ ],
1588
+ true
1589
+ ),
1590
+ null,
1591
+ [],
1592
+ true
1593
+ ),
1594
+ requiredImports: ["View"]
1595
+ };
1596
+ }
1597
+ }
1598
+ }
1599
+ async function insertElement(workspaceRoot, payload) {
1600
+ const fullPath = resolveSafeScreenPath(workspaceRoot, payload.file);
1601
+ return withFileLock(fullPath, async () => {
1602
+ const previousContent = await fs.readFile(fullPath, "utf-8");
1603
+ const ast = parse(previousContent, {
1604
+ sourceType: "module",
1605
+ plugins: ["typescript", "jsx"]
1606
+ });
1607
+ const byIndex = /* @__PURE__ */ new Map();
1608
+ let currentIndex = 0;
1609
+ let hasNavigationInScope = false;
1610
+ let hasRouterInScope = false;
1611
+ traverse(ast, {
1612
+ Identifier(nodePath) {
1613
+ if (nodePath.node.name === "navigation") hasNavigationInScope = true;
1614
+ if (nodePath.node.name === "router") hasRouterInScope = true;
1615
+ },
1616
+ JSXOpeningElement(nodePath) {
1617
+ var _a;
1618
+ const tagName = getJsxTagName(nodePath.node.name);
1619
+ if (tagName === "Fragment" || tagName === "React.Fragment" || tagName === "") {
1620
+ return;
1621
+ }
1622
+ const idx = currentIndex++;
1623
+ const jsxElPath = nodePath.parentPath;
1624
+ if (!jsxElPath.isJSXElement()) return;
1625
+ const element = jsxElPath.node;
1626
+ const parentNode = (_a = jsxElPath.parentPath) == null ? void 0 : _a.node;
1627
+ const parent = t.isJSXElement(parentNode) || t.isJSXFragment(parentNode) ? parentNode : null;
1628
+ byIndex.set(idx, { index: idx, tagName, element, parent });
1629
+ }
1630
+ });
1631
+ if (byIndex.size === 0) {
1632
+ throw new Error(`No JSX elements found in ${payload.file} to insert into.`);
1633
+ }
1634
+ const { element: newElement, requiredImports } = buildPresetJsxElement(payload.preset, {
1635
+ label: payload.label,
1636
+ navigateTo: payload.navigateTo,
1637
+ hasNavigationInScope,
1638
+ hasRouterInScope
1639
+ });
1640
+ ensureReactNativeImports(ast, requiredImports);
1641
+ const ensureOpenContainer = (container) => {
1642
+ if (container.openingElement.selfClosing) {
1643
+ container.openingElement.selfClosing = false;
1644
+ container.selfClosing = false;
1645
+ container.closingElement = t.jsxClosingElement(
1646
+ t.cloneNode(container.openingElement.name, true)
1647
+ );
1648
+ }
1649
+ };
1650
+ const targetIdx = payload.astPath !== void 0 && payload.astPath !== "" ? Number.parseInt(payload.astPath, 10) : -1;
1651
+ const targetInfo = targetIdx >= 0 ? byIndex.get(targetIdx) : void 0;
1652
+ const requestedPlacement = payload.placement ?? "after";
1653
+ if (targetInfo) {
1654
+ const baseTag = targetInfo.tagName.split(".").pop() ?? targetInfo.tagName;
1655
+ const isContainer = CONTAINER_TAG_NAMES.has(baseTag);
1656
+ if (requestedPlacement === "inside" && isContainer || !targetInfo.parent) {
1657
+ ensureOpenContainer(targetInfo.element);
1658
+ targetInfo.element.children.push(newElement);
1659
+ } else {
1660
+ const parentContainer = targetInfo.parent;
1661
+ const pos = parentContainer.children.indexOf(targetInfo.element);
1662
+ if (pos === -1) {
1663
+ parentContainer.children.push(newElement);
1664
+ } else {
1665
+ const insertAt = requestedPlacement === "before" ? pos : pos + 1;
1666
+ parentContainer.children.splice(insertAt, 0, newElement);
1667
+ }
1668
+ }
1669
+ } else {
1670
+ let defaultContainer = byIndex.get(0).element;
1671
+ for (const info of byIndex.values()) {
1672
+ const base = info.tagName.split(".").pop() ?? info.tagName;
1673
+ if (base === "ScrollView") {
1674
+ defaultContainer = info.element;
1675
+ break;
1676
+ }
1677
+ }
1678
+ ensureOpenContainer(defaultContainer);
1679
+ defaultContainer.children.push(newElement);
1680
+ }
1681
+ let insertedAstPath = "0";
1682
+ let reindexCounter = 0;
1683
+ traverse(ast, {
1684
+ JSXOpeningElement(nodePath) {
1685
+ const tagName = getJsxTagName(nodePath.node.name);
1686
+ if (tagName === "Fragment" || tagName === "React.Fragment" || tagName === "") {
1687
+ return;
1688
+ }
1689
+ const idx = reindexCounter++;
1690
+ if (nodePath.parentPath.isJSXElement() && nodePath.parentPath.node === newElement) {
1691
+ insertedAstPath = String(idx);
1692
+ }
1693
+ }
1694
+ });
1695
+ const output = generate(ast, {
1696
+ retainLines: false,
1697
+ comments: true,
1698
+ jsescOption: { minimal: true }
1699
+ });
1700
+ const nextContent = output.code + "\n";
1701
+ await fs.writeFile(fullPath, nextContent, "utf-8");
1702
+ undoStack.push({
1703
+ file: payload.file,
1704
+ previousContent,
1705
+ nextContent,
1706
+ description: `Inserted ${payload.preset} in ${path.basename(payload.file)}`,
1707
+ timestamp: Date.now()
1708
+ });
1709
+ if (undoStack.length > MAX_HISTORY) {
1710
+ undoStack.shift();
1711
+ }
1712
+ redoStack.length = 0;
1713
+ const inspected = await inspectElement(workspaceRoot, {
1714
+ file: payload.file,
1715
+ astPath: insertedAstPath
1716
+ });
1717
+ return {
1718
+ element: inspected,
1719
+ undoAvailable: undoStack.length,
1720
+ redoAvailable: redoStack.length
1721
+ };
1722
+ });
1723
+ }
1724
+ async function duplicateElement(workspaceRoot, payload) {
1725
+ const fullPath = resolveSafeScreenPath(workspaceRoot, payload.file);
1726
+ return withFileLock(fullPath, async () => {
1727
+ const previousContent = await fs.readFile(fullPath, "utf-8");
1728
+ const ast = parse(previousContent, {
1729
+ sourceType: "module",
1730
+ plugins: ["typescript", "jsx"]
1731
+ });
1732
+ let targetElement = null;
1733
+ let parentContainer = null;
1734
+ let currentIndex = 0;
1735
+ const requestedIdx = Number.parseInt(payload.astPath, 10);
1736
+ traverse(ast, {
1737
+ JSXOpeningElement(nodePath) {
1738
+ var _a;
1739
+ const tagName = getJsxTagName(nodePath.node.name);
1740
+ if (tagName === "Fragment" || tagName === "React.Fragment" || tagName === "") {
1741
+ return;
1742
+ }
1743
+ const idx = currentIndex++;
1744
+ if (idx === requestedIdx && nodePath.parentPath.isJSXElement()) {
1745
+ targetElement = nodePath.parentPath.node;
1746
+ const p = (_a = nodePath.parentPath.parentPath) == null ? void 0 : _a.node;
1747
+ parentContainer = t.isJSXElement(p) || t.isJSXFragment(p) ? p : null;
1748
+ }
1749
+ }
1750
+ });
1751
+ if (!targetElement || !parentContainer) {
1752
+ throw new Error("Cannot duplicate the root screen container.");
1753
+ }
1754
+ const cloned = t.cloneNode(targetElement, true);
1755
+ const siblings = parentContainer.children;
1756
+ const pos = siblings.indexOf(targetElement);
1757
+ siblings.splice(pos + 1, 0, cloned);
1758
+ let clonedAstPath = payload.astPath;
1759
+ let reindexCounter = 0;
1760
+ traverse(ast, {
1761
+ JSXOpeningElement(nodePath) {
1762
+ const tagName = getJsxTagName(nodePath.node.name);
1763
+ if (tagName === "Fragment" || tagName === "React.Fragment" || tagName === "") {
1764
+ return;
1765
+ }
1766
+ const idx = reindexCounter++;
1767
+ if (nodePath.parentPath.isJSXElement() && nodePath.parentPath.node === cloned) {
1768
+ clonedAstPath = String(idx);
1769
+ }
1770
+ }
1771
+ });
1772
+ const output = generate(ast, {
1773
+ retainLines: false,
1774
+ comments: true,
1775
+ jsescOption: { minimal: true }
1776
+ });
1777
+ const nextContent = output.code + "\n";
1778
+ await fs.writeFile(fullPath, nextContent, "utf-8");
1779
+ undoStack.push({
1780
+ file: payload.file,
1781
+ previousContent,
1782
+ nextContent,
1783
+ description: `Duplicated <${getJsxTagName(targetElement.openingElement.name)}> in ${path.basename(payload.file)}`,
1784
+ timestamp: Date.now()
1785
+ });
1786
+ if (undoStack.length > MAX_HISTORY) {
1787
+ undoStack.shift();
1788
+ }
1789
+ redoStack.length = 0;
1790
+ const inspected = await inspectElement(workspaceRoot, {
1791
+ file: payload.file,
1792
+ astPath: clonedAstPath
1793
+ });
1794
+ return {
1795
+ element: inspected,
1796
+ undoAvailable: undoStack.length,
1797
+ redoAvailable: redoStack.length
1798
+ };
1799
+ });
1800
+ }
1801
+ async function deleteElement(workspaceRoot, payload) {
1802
+ const fullPath = resolveSafeScreenPath(workspaceRoot, payload.file);
1803
+ return withFileLock(fullPath, async () => {
1804
+ const previousContent = await fs.readFile(fullPath, "utf-8");
1805
+ const ast = parse(previousContent, {
1806
+ sourceType: "module",
1807
+ plugins: ["typescript", "jsx"]
1808
+ });
1809
+ let targetElement = null;
1810
+ let parentContainer = null;
1811
+ let currentIndex = 0;
1812
+ const requestedIdx = Number.parseInt(payload.astPath, 10);
1813
+ if (requestedIdx === 0) {
1814
+ throw new Error("Cannot delete the root screen container element.");
1815
+ }
1816
+ traverse(ast, {
1817
+ JSXOpeningElement(nodePath) {
1818
+ var _a;
1819
+ const tagName = getJsxTagName(nodePath.node.name);
1820
+ if (tagName === "Fragment" || tagName === "React.Fragment" || tagName === "") {
1821
+ return;
1822
+ }
1823
+ const idx = currentIndex++;
1824
+ if (idx === requestedIdx && nodePath.parentPath.isJSXElement()) {
1825
+ targetElement = nodePath.parentPath.node;
1826
+ const p = (_a = nodePath.parentPath.parentPath) == null ? void 0 : _a.node;
1827
+ parentContainer = t.isJSXElement(p) || t.isJSXFragment(p) ? p : null;
1828
+ }
1829
+ }
1830
+ });
1831
+ if (!targetElement || !parentContainer) {
1832
+ throw new Error(`Element at astPath=${payload.astPath} cannot be deleted.`);
1833
+ }
1834
+ const siblings = parentContainer.children;
1835
+ const pos = siblings.indexOf(targetElement);
1836
+ if (pos === -1) {
1837
+ throw new Error("Could not find element inside parent children.");
1838
+ }
1839
+ siblings.splice(pos, 1);
1840
+ const output = generate(ast, {
1841
+ retainLines: false,
1842
+ comments: true,
1843
+ jsescOption: { minimal: true }
1844
+ });
1845
+ const nextContent = output.code + "\n";
1846
+ await fs.writeFile(fullPath, nextContent, "utf-8");
1847
+ undoStack.push({
1848
+ file: payload.file,
1849
+ previousContent,
1850
+ nextContent,
1851
+ description: `Deleted <${getJsxTagName(targetElement.openingElement.name)}> in ${path.basename(payload.file)}`,
1852
+ timestamp: Date.now()
1853
+ });
1854
+ if (undoStack.length > MAX_HISTORY) {
1855
+ undoStack.shift();
1856
+ }
1857
+ redoStack.length = 0;
1858
+ return {
1859
+ deletedAstPath: payload.astPath,
1860
+ undoAvailable: undoStack.length,
1861
+ redoAvailable: redoStack.length
1862
+ };
1863
+ });
1864
+ }
1865
+ const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
1866
+ const DEFAULT_WORKSPACE_ROOT = path.resolve(__dirname$1, "..");
1867
+ const MCP_TOOLS = [
1868
+ {
1869
+ name: "list_canvas_screens",
1870
+ description: "Lists all discovered React Native screens on the visual canvas along with their element counts and outgoing navigation flow edges.",
1871
+ inputSchema: {
1872
+ type: "object",
1873
+ properties: {}
1874
+ }
1875
+ },
1876
+ {
1877
+ name: "inspect_screen_element",
1878
+ description: "Inspects a specific JSX element inside a React Native screen by its relative file path and AST index path.",
1879
+ inputSchema: {
1880
+ type: "object",
1881
+ properties: {
1882
+ file: {
1883
+ type: "string",
1884
+ description: "Relative path to the screen file (e.g. src/screens/HomeScreen.tsx)"
1885
+ },
1886
+ astPath: {
1887
+ type: "string",
1888
+ description: 'Preorder AST index of the JSX element (e.g. "0", "4")'
1889
+ }
1890
+ },
1891
+ required: ["file", "astPath"]
1892
+ }
1893
+ },
1894
+ {
1895
+ name: "patch_screen_element",
1896
+ description: "Surgically patches the text copy or StyleSheet properties of a React Native JSX element.",
1897
+ inputSchema: {
1898
+ type: "object",
1899
+ properties: {
1900
+ file: {
1901
+ type: "string",
1902
+ description: "Relative path to the screen file (e.g. src/screens/HomeScreen.tsx)"
1903
+ },
1904
+ astPath: {
1905
+ type: "string",
1906
+ description: "Preorder AST index of the JSX element"
1907
+ },
1908
+ text: {
1909
+ type: "string",
1910
+ description: "Optional new static text content for <Text> elements"
1911
+ },
1912
+ styles: {
1913
+ type: "object",
1914
+ description: "Optional map of React Native style properties to update"
1915
+ }
1916
+ },
1917
+ required: ["file", "astPath"]
1918
+ }
1919
+ },
1920
+ {
1921
+ name: "reorder_screen_element",
1922
+ description: "Surgically reorders a React Native JSX element among its Flexbox siblings in the .tsx AST.",
1923
+ inputSchema: {
1924
+ type: "object",
1925
+ properties: {
1926
+ file: {
1927
+ type: "string",
1928
+ description: "Relative path to the screen file (e.g. src/screens/HomeScreen.tsx)"
1929
+ },
1930
+ astPath: {
1931
+ type: "string",
1932
+ description: "Preorder AST index of the JSX element to move"
1933
+ },
1934
+ direction: {
1935
+ type: "string",
1936
+ enum: ["up", "down"],
1937
+ description: 'Optional relative direction ("up" or "down") among siblings'
1938
+ },
1939
+ targetAstPath: {
1940
+ type: "string",
1941
+ description: "Optional AST index of the sibling to place before/after"
1942
+ },
1943
+ placement: {
1944
+ type: "string",
1945
+ enum: ["before", "after"],
1946
+ description: "Whether to insert before or after targetAstPath"
1947
+ }
1948
+ },
1949
+ required: ["file", "astPath"]
1950
+ }
1951
+ },
1952
+ {
1953
+ name: "insert_screen_element",
1954
+ description: "Surgically inserts a new React Native component/section preset (button, section, row, heading, text, input, badge, divider) into a screen .tsx AST and auto-injects required react-native imports.",
1955
+ inputSchema: {
1956
+ type: "object",
1957
+ properties: {
1958
+ file: {
1959
+ type: "string",
1960
+ description: "Relative path to the screen file (e.g. src/screens/HomeScreen.tsx)"
1961
+ },
1962
+ astPath: {
1963
+ type: "string",
1964
+ description: "Optional AST index of the reference JSX element"
1965
+ },
1966
+ placement: {
1967
+ type: "string",
1968
+ enum: ["inside", "after", "before"],
1969
+ description: 'Placement relative to astPath (defaults to "after" or "inside")'
1970
+ },
1971
+ preset: {
1972
+ type: "string",
1973
+ enum: ["button", "section", "row", "heading", "text", "input", "badge", "divider"],
1974
+ description: "Component preset to insert"
1975
+ },
1976
+ label: {
1977
+ type: "string",
1978
+ description: "Optional custom label/text/placeholder for the inserted component"
1979
+ },
1980
+ navigateTo: {
1981
+ type: "string",
1982
+ description: "Optional target screen ID if inserting a navigation button"
1983
+ }
1984
+ },
1985
+ required: ["file", "preset"]
1986
+ }
1987
+ },
1988
+ {
1989
+ name: "duplicate_screen_element",
1990
+ description: "Duplicates a JSX element right after itself in the screen .tsx AST.",
1991
+ inputSchema: {
1992
+ type: "object",
1993
+ properties: {
1994
+ file: { type: "string" },
1995
+ astPath: { type: "string" }
1996
+ },
1997
+ required: ["file", "astPath"]
1998
+ }
1999
+ },
2000
+ {
2001
+ name: "delete_screen_element",
2002
+ description: "Deletes a non-root JSX element from the screen .tsx AST.",
2003
+ inputSchema: {
2004
+ type: "object",
2005
+ properties: {
2006
+ file: { type: "string" },
2007
+ astPath: { type: "string" }
2008
+ },
2009
+ required: ["file", "astPath"]
2010
+ }
2011
+ }
2012
+ ];
2013
+ async function executeMcpTool(workspaceRoot, name, args) {
2014
+ if (name === "list_canvas_screens") {
2015
+ const screens = await discoverScreens(workspaceRoot);
2016
+ return {
2017
+ content: [{ type: "text", text: JSON.stringify(screens, null, 2) }]
2018
+ };
2019
+ }
2020
+ if (name === "inspect_screen_element") {
2021
+ const file = String((args == null ? void 0 : args.file) ?? "");
2022
+ const astPath = String((args == null ? void 0 : args.astPath) ?? "0");
2023
+ const element = await inspectElement(workspaceRoot, { file, astPath });
2024
+ return {
2025
+ content: [{ type: "text", text: JSON.stringify(element, null, 2) }]
2026
+ };
2027
+ }
2028
+ if (name === "patch_screen_element") {
2029
+ const file = String((args == null ? void 0 : args.file) ?? "");
2030
+ const astPath = String((args == null ? void 0 : args.astPath) ?? "0");
2031
+ const text = typeof (args == null ? void 0 : args.text) === "string" ? args.text : void 0;
2032
+ const styles = (args == null ? void 0 : args.styles) ?? void 0;
2033
+ const result = await patchElement(workspaceRoot, {
2034
+ file,
2035
+ astPath,
2036
+ text,
2037
+ styles
2038
+ });
2039
+ return {
2040
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2041
+ };
2042
+ }
2043
+ if (name === "reorder_screen_element") {
2044
+ const file = String((args == null ? void 0 : args.file) ?? "");
2045
+ const astPath = String((args == null ? void 0 : args.astPath) ?? "0");
2046
+ const direction = (args == null ? void 0 : args.direction) === "up" || (args == null ? void 0 : args.direction) === "down" ? args.direction : void 0;
2047
+ const targetAstPath = typeof (args == null ? void 0 : args.targetAstPath) === "string" ? args.targetAstPath : void 0;
2048
+ const placement = (args == null ? void 0 : args.placement) === "before" || (args == null ? void 0 : args.placement) === "after" ? args.placement : void 0;
2049
+ const result = await reorderElement(workspaceRoot, {
2050
+ file,
2051
+ astPath,
2052
+ direction,
2053
+ targetAstPath,
2054
+ placement
2055
+ });
2056
+ return {
2057
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2058
+ };
2059
+ }
2060
+ if (name === "insert_screen_element") {
2061
+ const file = String((args == null ? void 0 : args.file) ?? "");
2062
+ const preset = String((args == null ? void 0 : args.preset) ?? "button");
2063
+ const astPath = typeof (args == null ? void 0 : args.astPath) === "string" ? args.astPath : void 0;
2064
+ const placement = (args == null ? void 0 : args.placement) === "inside" || (args == null ? void 0 : args.placement) === "after" || (args == null ? void 0 : args.placement) === "before" ? args.placement : void 0;
2065
+ const label = typeof (args == null ? void 0 : args.label) === "string" ? args.label : void 0;
2066
+ const navigateTo = typeof (args == null ? void 0 : args.navigateTo) === "string" ? args.navigateTo : void 0;
2067
+ const result = await insertElement(workspaceRoot, {
2068
+ file,
2069
+ astPath,
2070
+ placement,
2071
+ preset,
2072
+ label,
2073
+ navigateTo
2074
+ });
2075
+ return {
2076
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2077
+ };
2078
+ }
2079
+ if (name === "duplicate_screen_element") {
2080
+ const file = String((args == null ? void 0 : args.file) ?? "");
2081
+ const astPath = String((args == null ? void 0 : args.astPath) ?? "1");
2082
+ const result = await duplicateElement(workspaceRoot, { file, astPath });
2083
+ return {
2084
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2085
+ };
2086
+ }
2087
+ if (name === "delete_screen_element") {
2088
+ const file = String((args == null ? void 0 : args.file) ?? "");
2089
+ const astPath = String((args == null ? void 0 : args.astPath) ?? "1");
2090
+ const result = await deleteElement(workspaceRoot, { file, astPath });
2091
+ return {
2092
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2093
+ };
2094
+ }
2095
+ throw new Error(`Unknown MCP tool: ${name}`);
2096
+ }
2097
+ async function handleMcpJsonRpcRequest(request, customWorkspaceRoot) {
2098
+ var _a, _b, _c;
2099
+ if (request.id === void 0) {
2100
+ return null;
2101
+ }
2102
+ const workspaceRoot = customWorkspaceRoot || process.env.ANTIGRAVITY_TARGET_PROJECT || process.env.ANTIGRAVITY_WORKSPACE_ROOT || DEFAULT_WORKSPACE_ROOT;
2103
+ const method = request.method ?? "";
2104
+ try {
2105
+ if (method === "initialize") {
2106
+ const requestedProtocol = typeof ((_a = request.params) == null ? void 0 : _a.protocolVersion) === "string" ? request.params.protocolVersion : "2024-11-05";
2107
+ return {
2108
+ jsonrpc: "2.0",
2109
+ id: request.id,
2110
+ result: {
2111
+ protocolVersion: requestedProtocol,
2112
+ capabilities: {
2113
+ tools: {}
2114
+ },
2115
+ serverInfo: {
2116
+ name: "zelune",
2117
+ version: "1.6.1"
2118
+ }
2119
+ }
2120
+ };
2121
+ }
2122
+ if (method === "ping") {
2123
+ return {
2124
+ jsonrpc: "2.0",
2125
+ id: request.id,
2126
+ result: {}
2127
+ };
2128
+ }
2129
+ if (method === "tools/list") {
2130
+ return {
2131
+ jsonrpc: "2.0",
2132
+ id: request.id,
2133
+ result: {
2134
+ tools: MCP_TOOLS
2135
+ }
2136
+ };
2137
+ }
2138
+ if (method === "resources/list") {
2139
+ return {
2140
+ jsonrpc: "2.0",
2141
+ id: request.id,
2142
+ result: {
2143
+ resources: []
2144
+ }
2145
+ };
2146
+ }
2147
+ if (method === "prompts/list") {
2148
+ return {
2149
+ jsonrpc: "2.0",
2150
+ id: request.id,
2151
+ result: {
2152
+ prompts: []
2153
+ }
2154
+ };
2155
+ }
2156
+ if (method === "tools/call") {
2157
+ const toolName = String(((_b = request.params) == null ? void 0 : _b.name) ?? "");
2158
+ const toolArgs = ((_c = request.params) == null ? void 0 : _c.arguments) ?? void 0;
2159
+ const toolResult = await executeMcpTool(workspaceRoot, toolName, toolArgs);
2160
+ return {
2161
+ jsonrpc: "2.0",
2162
+ id: request.id,
2163
+ result: toolResult
2164
+ };
2165
+ }
2166
+ return {
2167
+ jsonrpc: "2.0",
2168
+ id: request.id,
2169
+ error: {
2170
+ code: -32601,
2171
+ message: `Method not found: ${method}`
2172
+ }
2173
+ };
2174
+ } catch (err) {
2175
+ const message = err instanceof Error ? err.message : String(err);
2176
+ return {
2177
+ jsonrpc: "2.0",
2178
+ id: request.id,
2179
+ error: {
2180
+ code: -32603,
2181
+ message
2182
+ }
2183
+ };
2184
+ }
2185
+ }
2186
+ function startMcpServer(customWorkspaceRoot) {
2187
+ const rl = readline.createInterface({
2188
+ input: process.stdin,
2189
+ terminal: false
2190
+ });
2191
+ rl.on("line", async (line) => {
2192
+ const trimmed = line.trim();
2193
+ if (!trimmed) return;
2194
+ let parsed;
2195
+ try {
2196
+ parsed = JSON.parse(trimmed);
2197
+ } catch {
2198
+ const parseErr = {
2199
+ jsonrpc: "2.0",
2200
+ id: null,
2201
+ error: { code: -32700, message: "Parse error" }
2202
+ };
2203
+ process.stdout.write(`${JSON.stringify(parseErr)}
2204
+ `);
2205
+ return;
2206
+ }
2207
+ const response = await handleMcpJsonRpcRequest(parsed, customWorkspaceRoot);
2208
+ if (response) {
2209
+ process.stdout.write(`${JSON.stringify(response)}
2210
+ `);
2211
+ }
2212
+ });
2213
+ }
2214
+ const isDirectExecution = Boolean(process.argv[1]) && (() => {
2215
+ try {
2216
+ return import.meta.url === pathToFileURL(fsSync.realpathSync(process.argv[1])).href;
2217
+ } catch {
2218
+ return false;
2219
+ }
2220
+ })();
2221
+ if (isDirectExecution) {
2222
+ startMcpServer();
2223
+ }
2224
+ export {
2225
+ MCP_TOOLS,
2226
+ handleMcpJsonRpcRequest,
2227
+ startMcpServer
2228
+ };