@utopia-ai/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.claude/settings.json +1 -0
  2. package/.claude/settings.local.json +38 -0
  3. package/bin/utopia.js +20 -0
  4. package/package.json +46 -0
  5. package/python/README.md +34 -0
  6. package/python/instrumenter/instrument.py +1148 -0
  7. package/python/pyproject.toml +32 -0
  8. package/python/setup.py +27 -0
  9. package/python/utopia_runtime/__init__.py +30 -0
  10. package/python/utopia_runtime/__pycache__/__init__.cpython-313.pyc +0 -0
  11. package/python/utopia_runtime/__pycache__/client.cpython-313.pyc +0 -0
  12. package/python/utopia_runtime/__pycache__/probe.cpython-313.pyc +0 -0
  13. package/python/utopia_runtime/client.py +31 -0
  14. package/python/utopia_runtime/probe.py +446 -0
  15. package/python/utopia_runtime.egg-info/PKG-INFO +59 -0
  16. package/python/utopia_runtime.egg-info/SOURCES.txt +10 -0
  17. package/python/utopia_runtime.egg-info/dependency_links.txt +1 -0
  18. package/python/utopia_runtime.egg-info/top_level.txt +1 -0
  19. package/scripts/publish-npm.sh +14 -0
  20. package/scripts/publish-pypi.sh +17 -0
  21. package/src/cli/commands/codex.ts +193 -0
  22. package/src/cli/commands/context.ts +188 -0
  23. package/src/cli/commands/destruct.ts +237 -0
  24. package/src/cli/commands/easter-eggs.ts +203 -0
  25. package/src/cli/commands/init.ts +505 -0
  26. package/src/cli/commands/instrument.ts +962 -0
  27. package/src/cli/commands/mcp.ts +16 -0
  28. package/src/cli/commands/serve.ts +194 -0
  29. package/src/cli/commands/status.ts +304 -0
  30. package/src/cli/commands/validate.ts +328 -0
  31. package/src/cli/index.ts +37 -0
  32. package/src/cli/utils/config.ts +54 -0
  33. package/src/graph/index.ts +687 -0
  34. package/src/instrumenter/javascript.ts +1798 -0
  35. package/src/mcp/index.ts +886 -0
  36. package/src/runtime/js/index.ts +518 -0
  37. package/src/runtime/js/package-lock.json +30 -0
  38. package/src/runtime/js/package.json +30 -0
  39. package/src/runtime/js/tsconfig.json +16 -0
  40. package/src/server/db/index.ts +26 -0
  41. package/src/server/db/schema.ts +45 -0
  42. package/src/server/index.ts +79 -0
  43. package/src/server/middleware/auth.ts +74 -0
  44. package/src/server/routes/admin.ts +36 -0
  45. package/src/server/routes/graph.ts +358 -0
  46. package/src/server/routes/probes.ts +286 -0
  47. package/src/types.ts +147 -0
  48. package/src/utopia-mode/index.ts +206 -0
  49. package/tsconfig.json +19 -0
@@ -0,0 +1,1798 @@
1
+ // Utopia JS/TS AST Instrumenter
2
+ // Uses Babel to parse, traverse, and transform JS/TS source files,
3
+ // injecting lightweight probes for error tracking, database monitoring,
4
+ // API call tracing, infrastructure reporting, and function profiling.
5
+
6
+ import * as parser from '@babel/parser';
7
+ import _traverse from '@babel/traverse';
8
+ import _generate from '@babel/generator';
9
+ import * as t from '@babel/types';
10
+ import { readFile, writeFile } from 'node:fs/promises';
11
+ import { resolve, relative, extname, basename, dirname, join } from 'node:path';
12
+ import { readdir, stat } from 'node:fs/promises';
13
+
14
+ // Handle default export interop for CommonJS/ESM compatibility
15
+ const traverse = (_traverse as any).default || _traverse;
16
+ const generate = (_generate as any).default || _generate;
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ interface InstrumentOptions {
23
+ probeTypes: ('error' | 'database' | 'api' | 'infra' | 'function')[];
24
+ utopiaMode: boolean;
25
+ dryRun: boolean;
26
+ entryPoints?: string[];
27
+ }
28
+
29
+ interface InstrumentResult {
30
+ file: string;
31
+ probesAdded: { type: string; line: number; functionName: string }[];
32
+ success: boolean;
33
+ error?: string;
34
+ }
35
+
36
+ interface ValidationResult {
37
+ valid: boolean;
38
+ errors: string[];
39
+ warnings: string[];
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Constants
44
+ // ---------------------------------------------------------------------------
45
+
46
+ const SKIP_DIRS = new Set([
47
+ 'node_modules',
48
+ 'dist',
49
+ 'build',
50
+ '.next',
51
+ '__tests__',
52
+ '__mocks__',
53
+ '.git',
54
+ '.utopia',
55
+ 'coverage',
56
+ ]);
57
+
58
+ const VALID_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx']);
59
+
60
+ const ENTRY_POINT_BASENAMES = new Set([
61
+ 'index.ts',
62
+ 'index.js',
63
+ 'index.tsx',
64
+ 'index.jsx',
65
+ 'main.ts',
66
+ 'main.js',
67
+ 'server.ts',
68
+ 'server.js',
69
+ 'app.ts',
70
+ 'app.js',
71
+ ]);
72
+
73
+ const PRISMA_METHODS = new Set([
74
+ 'findMany',
75
+ 'findUnique',
76
+ 'findFirst',
77
+ 'findUniqueOrThrow',
78
+ 'findFirstOrThrow',
79
+ 'create',
80
+ 'createMany',
81
+ 'update',
82
+ 'updateMany',
83
+ 'upsert',
84
+ 'delete',
85
+ 'deleteMany',
86
+ 'count',
87
+ 'aggregate',
88
+ 'groupBy',
89
+ ]);
90
+
91
+ const MONGOOSE_METHODS = new Set([
92
+ 'find',
93
+ 'findOne',
94
+ 'findById',
95
+ 'findOneAndUpdate',
96
+ 'findOneAndDelete',
97
+ 'findOneAndReplace',
98
+ 'findByIdAndUpdate',
99
+ 'findByIdAndDelete',
100
+ 'create',
101
+ 'insertMany',
102
+ 'updateOne',
103
+ 'updateMany',
104
+ 'deleteOne',
105
+ 'deleteMany',
106
+ 'countDocuments',
107
+ 'estimatedDocumentCount',
108
+ 'aggregate',
109
+ ]);
110
+
111
+ const KNEX_QUERY_METHODS = new Set([
112
+ 'select',
113
+ 'insert',
114
+ 'update',
115
+ 'delete',
116
+ 'del',
117
+ 'where',
118
+ 'from',
119
+ 'into',
120
+ 'raw',
121
+ ]);
122
+
123
+ const BABEL_PLUGINS: parser.ParserPlugin[] = [
124
+ 'typescript',
125
+ 'jsx',
126
+ 'decorators-legacy',
127
+ 'classProperties',
128
+ 'optionalChaining',
129
+ 'nullishCoalescingOperator',
130
+ 'dynamicImport',
131
+ 'exportDefaultFrom',
132
+ 'importMeta',
133
+ ];
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Helpers
137
+ // ---------------------------------------------------------------------------
138
+
139
+ /**
140
+ * Detect Babel parser plugins to use based on file extension.
141
+ */
142
+ function getParserPlugins(filePath: string): parser.ParserPlugin[] {
143
+ const ext = extname(filePath).toLowerCase();
144
+ // All files get the full plugin set; Babel is fine with unused plugins
145
+ return [...BABEL_PLUGINS];
146
+ }
147
+
148
+ /**
149
+ * Determine the source type for the parser.
150
+ */
151
+ function getSourceType(filePath: string): 'module' | 'script' {
152
+ // Default to module for all TS/modern JS
153
+ return 'module';
154
+ }
155
+
156
+ /**
157
+ * Get the enclosing function name from a Babel traversal path.
158
+ */
159
+ function getEnclosingFunctionName(path: any): string {
160
+ let current = path;
161
+ while (current) {
162
+ if (current.isFunctionDeclaration() && current.node.id) {
163
+ return current.node.id.name;
164
+ }
165
+ if (current.isFunctionExpression() && current.node.id) {
166
+ return current.node.id.name;
167
+ }
168
+ if (current.isClassMethod() || current.isObjectMethod()) {
169
+ const key = current.node.key;
170
+ if (t.isIdentifier(key)) return key.name;
171
+ if (t.isStringLiteral(key)) return key.value;
172
+ }
173
+ if (
174
+ current.isVariableDeclarator &&
175
+ current.isVariableDeclarator() &&
176
+ t.isIdentifier(current.node.id)
177
+ ) {
178
+ return current.node.id.name;
179
+ }
180
+ // Arrow function assigned to variable
181
+ if (
182
+ current.parentPath &&
183
+ current.parentPath.isVariableDeclarator() &&
184
+ t.isIdentifier(current.parentPath.node.id)
185
+ ) {
186
+ return current.parentPath.node.id.name;
187
+ }
188
+ // Object property
189
+ if (
190
+ current.parentPath &&
191
+ current.parentPath.isObjectProperty() &&
192
+ t.isIdentifier(current.parentPath.node.key)
193
+ ) {
194
+ return current.parentPath.node.key.name;
195
+ }
196
+ // Export default
197
+ if (current.parentPath && current.parentPath.isExportDefaultDeclaration()) {
198
+ return 'default';
199
+ }
200
+ current = current.parentPath;
201
+ }
202
+ return '<anonymous>';
203
+ }
204
+
205
+ /**
206
+ * Given a CallExpression path, get enclosing function name by walking up.
207
+ */
208
+ function getEnclosingFnName(path: any): string {
209
+ let current = path.parentPath;
210
+ while (current) {
211
+ if (current.isFunctionDeclaration() && current.node.id) {
212
+ return current.node.id.name;
213
+ }
214
+ if (current.isFunctionExpression() && current.node.id) {
215
+ return current.node.id.name;
216
+ }
217
+ if (current.isArrowFunctionExpression()) {
218
+ // Check if assigned to a variable
219
+ if (
220
+ current.parentPath &&
221
+ current.parentPath.isVariableDeclarator() &&
222
+ t.isIdentifier(current.parentPath.node.id)
223
+ ) {
224
+ return current.parentPath.node.id.name;
225
+ }
226
+ if (
227
+ current.parentPath &&
228
+ current.parentPath.isObjectProperty() &&
229
+ t.isIdentifier(current.parentPath.node.key)
230
+ ) {
231
+ return current.parentPath.node.key.name;
232
+ }
233
+ }
234
+ if (current.isClassMethod() || current.isObjectMethod()) {
235
+ const key = current.node.key;
236
+ if (t.isIdentifier(key)) return key.name;
237
+ if (t.isStringLiteral(key)) return key.value;
238
+ }
239
+ current = current.parentPath;
240
+ }
241
+ return '<anonymous>';
242
+ }
243
+
244
+ /**
245
+ * Read a specific line from source text. Returns empty string if out of range.
246
+ */
247
+ function getSourceLine(source: string, lineNumber: number): string {
248
+ const lines = source.split('\n');
249
+ if (lineNumber < 1 || lineNumber > lines.length) return '';
250
+ return lines[lineNumber - 1].trim();
251
+ }
252
+
253
+ /**
254
+ * Get the function name from a function-like path node.
255
+ */
256
+ function getFunctionName(path: any): string {
257
+ const node = path.node;
258
+
259
+ // FunctionDeclaration
260
+ if (t.isFunctionDeclaration(node) && node.id) {
261
+ return node.id.name;
262
+ }
263
+
264
+ // ClassMethod / ObjectMethod
265
+ if (t.isClassMethod(node) || t.isObjectMethod(node)) {
266
+ if (t.isIdentifier(node.key)) return node.key.name;
267
+ if (t.isStringLiteral(node.key)) return node.key.value;
268
+ return '<computed>';
269
+ }
270
+
271
+ // FunctionExpression with name
272
+ if (t.isFunctionExpression(node) && node.id) {
273
+ return node.id.name;
274
+ }
275
+
276
+ // Variable assignment: const foo = () => {} or const foo = function() {}
277
+ if (
278
+ path.parentPath &&
279
+ path.parentPath.isVariableDeclarator() &&
280
+ t.isIdentifier(path.parentPath.node.id)
281
+ ) {
282
+ return path.parentPath.node.id.name;
283
+ }
284
+
285
+ // Object property: { foo: () => {} }
286
+ if (
287
+ path.parentPath &&
288
+ path.parentPath.isObjectProperty() &&
289
+ t.isIdentifier(path.parentPath.node.key)
290
+ ) {
291
+ return path.parentPath.node.key.name;
292
+ }
293
+
294
+ // Export default
295
+ if (path.parentPath && path.parentPath.isExportDefaultDeclaration()) {
296
+ return 'default';
297
+ }
298
+
299
+ // Assignment expression: module.exports = function() {}
300
+ if (
301
+ path.parentPath &&
302
+ path.parentPath.isAssignmentExpression() &&
303
+ t.isMemberExpression(path.parentPath.node.left)
304
+ ) {
305
+ const left = path.parentPath.node.left;
306
+ if (t.isIdentifier(left.property)) return left.property.name;
307
+ }
308
+
309
+ return '<anonymous>';
310
+ }
311
+
312
+ /**
313
+ * Check if a function body is already wrapped in a utopia try/catch.
314
+ */
315
+ function isAlreadyWrapped(body: t.BlockStatement): boolean {
316
+ if (body.body.length === 0) return false;
317
+ const first = body.body[0];
318
+ if (!t.isTryStatement(first)) return false;
319
+ // Check if there is a leading comment indicating utopia instrumentation
320
+ const leadingComments = first.leadingComments;
321
+ if (leadingComments && leadingComments.some((c: t.Comment) => c.value.includes('utopia:probe'))) {
322
+ return true;
323
+ }
324
+ // Also check the try block covers the whole body
325
+ if (body.body.length === 1 && t.isTryStatement(first)) {
326
+ const catchClause = first.handler;
327
+ if (catchClause && catchClause.param && t.isIdentifier(catchClause.param)) {
328
+ if (catchClause.param.name === '__utopia_err') return true;
329
+ }
330
+ }
331
+ return false;
332
+ }
333
+
334
+ /**
335
+ * Check if a call expression is already wrapped in a utopia probe IIFE.
336
+ */
337
+ function isInsideUtopiaIIFE(path: any): boolean {
338
+ let current = path.parentPath;
339
+ let depth = 0;
340
+ while (current && depth < 10) {
341
+ if (current.isCallExpression()) {
342
+ const callee = current.node.callee;
343
+ // Check for IIFE pattern: (async () => { ... })()
344
+ if (
345
+ t.isArrowFunctionExpression(callee) ||
346
+ t.isFunctionExpression(callee)
347
+ ) {
348
+ // Check for utopia variable names in the body
349
+ const body = callee.body;
350
+ if (t.isBlockStatement(body)) {
351
+ const bodySource = body.body.some(
352
+ (stmt: t.Statement) =>
353
+ t.isVariableDeclaration(stmt) &&
354
+ stmt.declarations.some(
355
+ (d: t.VariableDeclarator) =>
356
+ t.isIdentifier(d.id) &&
357
+ (d.id.name.startsWith('__utopia_db_') ||
358
+ d.id.name.startsWith('__utopia_api_'))
359
+ )
360
+ );
361
+ if (bodySource) return true;
362
+ }
363
+ }
364
+ }
365
+ current = current.parentPath;
366
+ depth++;
367
+ }
368
+ return false;
369
+ }
370
+
371
+ /**
372
+ * Convert a CallExpression node to a readable string representation.
373
+ */
374
+ function callExpressionToString(node: t.CallExpression): string {
375
+ try {
376
+ const result = generate(node, { concise: true });
377
+ // Truncate very long call strings
378
+ const str = result.code;
379
+ if (str.length > 200) return str.slice(0, 200) + '...';
380
+ return str;
381
+ } catch {
382
+ return '<unknown call>';
383
+ }
384
+ }
385
+
386
+ // ---------------------------------------------------------------------------
387
+ // Detection helpers for DB / API patterns
388
+ // ---------------------------------------------------------------------------
389
+
390
+ interface DetectedDbCall {
391
+ library: string;
392
+ operation: string;
393
+ table: string;
394
+ }
395
+
396
+ /**
397
+ * Detect if a CallExpression is a database operation. Returns info or null.
398
+ */
399
+ function detectDbCall(node: t.CallExpression): DetectedDbCall | null {
400
+ const callee = node.callee;
401
+
402
+ // Pattern: prisma.<model>.<method>()
403
+ // e.g. prisma.user.findMany()
404
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
405
+ const method = callee.property.name;
406
+ if (PRISMA_METHODS.has(method)) {
407
+ const obj = callee.object;
408
+ if (t.isMemberExpression(obj) && t.isIdentifier(obj.property) && t.isIdentifier(obj.object)) {
409
+ if (obj.object.name === 'prisma' || obj.object.name === 'db') {
410
+ return { library: 'prisma', operation: method, table: obj.property.name };
411
+ }
412
+ }
413
+ }
414
+ }
415
+
416
+ // Pattern: db.query(...), pool.query(...), connection.query(...)
417
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
418
+ const method = callee.property.name;
419
+ if (method === 'query' || method === 'execute') {
420
+ const obj = callee.object;
421
+ if (t.isIdentifier(obj)) {
422
+ const name = obj.name.toLowerCase();
423
+ if (
424
+ name === 'db' ||
425
+ name === 'pool' ||
426
+ name === 'connection' ||
427
+ name === 'client' ||
428
+ name === 'conn' ||
429
+ name === 'database' ||
430
+ name === 'pg' ||
431
+ name === 'mysql'
432
+ ) {
433
+ return { library: 'sql', operation: method, table: '<query>' };
434
+ }
435
+ }
436
+ }
437
+ }
438
+
439
+ // Pattern: Model.find(...), Model.findOne(...), Model.create(...) (Mongoose)
440
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
441
+ const method = callee.property.name;
442
+ if (MONGOOSE_METHODS.has(method)) {
443
+ const obj = callee.object;
444
+ if (t.isIdentifier(obj)) {
445
+ // Mongoose models are typically PascalCase
446
+ const name = obj.name;
447
+ if (name.length > 0 && name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase()) {
448
+ return { library: 'mongoose', operation: method, table: name };
449
+ }
450
+ }
451
+ }
452
+ }
453
+
454
+ // Pattern: knex('table').<method>() or knex.select()/knex.insert() etc.
455
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
456
+ const method = callee.property.name;
457
+ if (KNEX_QUERY_METHODS.has(method)) {
458
+ const obj = callee.object;
459
+ // knex('table').select()
460
+ if (t.isCallExpression(obj) && t.isIdentifier(obj.callee)) {
461
+ if (obj.callee.name === 'knex') {
462
+ let table = '<table>';
463
+ if (obj.arguments.length > 0 && t.isStringLiteral(obj.arguments[0])) {
464
+ table = obj.arguments[0].value;
465
+ }
466
+ return { library: 'knex', operation: method, table };
467
+ }
468
+ }
469
+ // knex.select()
470
+ if (t.isIdentifier(obj) && obj.name === 'knex') {
471
+ return { library: 'knex', operation: method, table: '<query>' };
472
+ }
473
+ }
474
+ }
475
+
476
+ return null;
477
+ }
478
+
479
+ interface DetectedApiCall {
480
+ library: string;
481
+ method: string;
482
+ }
483
+
484
+ /**
485
+ * Detect if a CallExpression is an HTTP API call. Returns info or null.
486
+ */
487
+ function detectApiCall(node: t.CallExpression): DetectedApiCall | null {
488
+ const callee = node.callee;
489
+
490
+ // Pattern: fetch(url, opts)
491
+ if (t.isIdentifier(callee) && callee.name === 'fetch') {
492
+ return { library: 'fetch', method: 'GET' };
493
+ }
494
+
495
+ // Pattern: axios.get(), axios.post(), axios.put(), axios.delete(), axios.patch()
496
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property) && t.isIdentifier(callee.object)) {
497
+ if (callee.object.name === 'axios') {
498
+ const method = callee.property.name.toUpperCase();
499
+ if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'REQUEST'].includes(method)) {
500
+ return { library: 'axios', method: method === 'REQUEST' ? 'UNKNOWN' : method };
501
+ }
502
+ }
503
+ }
504
+
505
+ // Pattern: axios(config)
506
+ if (t.isIdentifier(callee) && callee.name === 'axios') {
507
+ return { library: 'axios', method: 'UNKNOWN' };
508
+ }
509
+
510
+ return null;
511
+ }
512
+
513
+ // ---------------------------------------------------------------------------
514
+ // AST builder helpers
515
+ // ---------------------------------------------------------------------------
516
+
517
+ /**
518
+ * Build the utopia:probe comment node.
519
+ */
520
+ function makeProbeComment(): t.CommentLine {
521
+ return { type: 'CommentLine', value: ' utopia:probe' } as t.CommentLine;
522
+ }
523
+
524
+ /**
525
+ * Add a leading comment to a node.
526
+ */
527
+ function addProbeComment(node: t.Node): void {
528
+ if (!node.leadingComments) {
529
+ node.leadingComments = [];
530
+ }
531
+ node.leadingComments.push(makeProbeComment());
532
+ }
533
+
534
+ /**
535
+ * Build the error probe try/catch wrapper.
536
+ */
537
+ function buildErrorTryCatch(
538
+ originalBody: t.Statement[],
539
+ filePath: string,
540
+ line: number,
541
+ functionName: string,
542
+ codeLine: string
543
+ ): t.TryStatement {
544
+ const catchParam = t.identifier('__utopia_err');
545
+
546
+ const reportCall = t.expressionStatement(
547
+ t.callExpression(
548
+ t.memberExpression(t.identifier('__utopia'), t.identifier('reportError')),
549
+ [
550
+ t.objectExpression([
551
+ t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
552
+ t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
553
+ t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
554
+ t.objectProperty(
555
+ t.identifier('errorType'),
556
+ t.logicalExpression(
557
+ '||',
558
+ t.optionalMemberExpression(
559
+ t.optionalMemberExpression(
560
+ t.identifier('__utopia_err'),
561
+ t.identifier('constructor'),
562
+ false,
563
+ true
564
+ ),
565
+ t.identifier('name'),
566
+ false,
567
+ true
568
+ ),
569
+ t.stringLiteral('Error')
570
+ )
571
+ ),
572
+ t.objectProperty(
573
+ t.identifier('message'),
574
+ t.logicalExpression(
575
+ '||',
576
+ t.optionalMemberExpression(
577
+ t.identifier('__utopia_err'),
578
+ t.identifier('message'),
579
+ false,
580
+ true
581
+ ),
582
+ t.callExpression(t.identifier('String'), [t.identifier('__utopia_err')])
583
+ )
584
+ ),
585
+ t.objectProperty(
586
+ t.identifier('stack'),
587
+ t.logicalExpression(
588
+ '||',
589
+ t.optionalMemberExpression(
590
+ t.identifier('__utopia_err'),
591
+ t.identifier('stack'),
592
+ false,
593
+ true
594
+ ),
595
+ t.stringLiteral('')
596
+ )
597
+ ),
598
+ t.objectProperty(t.identifier('inputData'), t.objectExpression([])),
599
+ t.objectProperty(t.identifier('codeLine'), t.stringLiteral(codeLine)),
600
+ ]),
601
+ ]
602
+ )
603
+ );
604
+
605
+ const rethrow = t.throwStatement(t.identifier('__utopia_err'));
606
+
607
+ const catchBlock = t.blockStatement([reportCall, rethrow]);
608
+ const catchClause = t.catchClause(catchParam, catchBlock);
609
+
610
+ const tryBlock = t.blockStatement([...originalBody]);
611
+ const tryStatement = t.tryStatement(tryBlock, catchClause);
612
+
613
+ addProbeComment(tryStatement);
614
+ return tryStatement;
615
+ }
616
+
617
+ /**
618
+ * Build a database probe wrapper IIFE for a call expression.
619
+ */
620
+ function buildDbProbeIIFE(
621
+ originalCall: t.Expression,
622
+ filePath: string,
623
+ line: number,
624
+ functionName: string,
625
+ operation: string,
626
+ callString: string,
627
+ table: string,
628
+ library: string
629
+ ): t.CallExpression {
630
+ const startVar = t.variableDeclaration('const', [
631
+ t.variableDeclarator(
632
+ t.identifier('__utopia_db_start'),
633
+ t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), [])
634
+ ),
635
+ ]);
636
+
637
+ const resultVar = t.variableDeclaration('const', [
638
+ t.variableDeclarator(
639
+ t.identifier('__utopia_db_result'),
640
+ t.awaitExpression(originalCall)
641
+ ),
642
+ ]);
643
+
644
+ const durationExpr = t.binaryExpression(
645
+ '-',
646
+ t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), []),
647
+ t.identifier('__utopia_db_start')
648
+ );
649
+
650
+ const successReport = t.expressionStatement(
651
+ t.callExpression(
652
+ t.memberExpression(t.identifier('__utopia'), t.identifier('reportDb')),
653
+ [
654
+ t.objectExpression([
655
+ t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
656
+ t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
657
+ t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
658
+ t.objectProperty(t.identifier('operation'), t.stringLiteral(operation)),
659
+ t.objectProperty(t.identifier('query'), t.stringLiteral(callString)),
660
+ t.objectProperty(t.identifier('table'), t.stringLiteral(table)),
661
+ t.objectProperty(t.identifier('duration'), durationExpr),
662
+ t.objectProperty(
663
+ t.identifier('rowCount'),
664
+ t.conditionalExpression(
665
+ t.callExpression(
666
+ t.memberExpression(t.identifier('Array'), t.identifier('isArray')),
667
+ [t.identifier('__utopia_db_result')]
668
+ ),
669
+ t.memberExpression(t.identifier('__utopia_db_result'), t.identifier('length')),
670
+ t.identifier('undefined')
671
+ )
672
+ ),
673
+ t.objectProperty(
674
+ t.identifier('connectionInfo'),
675
+ t.objectExpression([
676
+ t.objectProperty(t.identifier('type'), t.stringLiteral(library)),
677
+ ])
678
+ ),
679
+ ]),
680
+ ]
681
+ )
682
+ );
683
+
684
+ const returnResult = t.returnStatement(t.identifier('__utopia_db_result'));
685
+
686
+ const catchParam = t.identifier('__utopia_db_err');
687
+
688
+ const errorDurationExpr = t.binaryExpression(
689
+ '-',
690
+ t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), []),
691
+ t.identifier('__utopia_db_start')
692
+ );
693
+
694
+ const errorReport = t.expressionStatement(
695
+ t.callExpression(
696
+ t.memberExpression(t.identifier('__utopia'), t.identifier('reportDb')),
697
+ [
698
+ t.objectExpression([
699
+ t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
700
+ t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
701
+ t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
702
+ t.objectProperty(t.identifier('operation'), t.stringLiteral(operation)),
703
+ t.objectProperty(t.identifier('query'), t.stringLiteral(callString)),
704
+ t.objectProperty(t.identifier('duration'), errorDurationExpr),
705
+ t.objectProperty(
706
+ t.identifier('connectionInfo'),
707
+ t.objectExpression([
708
+ t.objectProperty(t.identifier('type'), t.stringLiteral(library)),
709
+ ])
710
+ ),
711
+ t.objectProperty(
712
+ t.identifier('error'),
713
+ t.optionalMemberExpression(
714
+ t.identifier('__utopia_db_err'),
715
+ t.identifier('message'),
716
+ false,
717
+ true
718
+ )
719
+ ),
720
+ ]),
721
+ ]
722
+ )
723
+ );
724
+
725
+ const rethrow = t.throwStatement(t.identifier('__utopia_db_err'));
726
+
727
+ const tryStatement = t.tryStatement(
728
+ t.blockStatement([resultVar, successReport, returnResult]),
729
+ t.catchClause(catchParam, t.blockStatement([errorReport, rethrow]))
730
+ );
731
+
732
+ const arrowBody = t.blockStatement([startVar, tryStatement]);
733
+ const arrow = t.arrowFunctionExpression([], arrowBody, true);
734
+
735
+ const iife = t.callExpression(arrow, []);
736
+ const awaitedIife = t.awaitExpression(iife);
737
+
738
+ // We return the call expression (the IIFE call), the await is handled by placement
739
+ return iife;
740
+ }
741
+
742
+ /**
743
+ * Build an API probe wrapper IIFE for a call expression.
744
+ */
745
+ function buildApiProbeIIFE(
746
+ originalCall: t.Expression,
747
+ filePath: string,
748
+ line: number,
749
+ functionName: string,
750
+ method: string,
751
+ library: string
752
+ ): t.CallExpression {
753
+ const startVar = t.variableDeclaration('const', [
754
+ t.variableDeclarator(
755
+ t.identifier('__utopia_api_start'),
756
+ t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), [])
757
+ ),
758
+ ]);
759
+
760
+ const resultVar = t.variableDeclaration('const', [
761
+ t.variableDeclarator(
762
+ t.identifier('__utopia_api_result'),
763
+ t.awaitExpression(originalCall)
764
+ ),
765
+ ]);
766
+
767
+ const durationExpr = t.binaryExpression(
768
+ '-',
769
+ t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), []),
770
+ t.identifier('__utopia_api_start')
771
+ );
772
+
773
+ // Build URL extraction depending on library
774
+ let urlExpr: t.Expression;
775
+ let methodExpr: t.Expression;
776
+ let statusCodeExpr: t.Expression;
777
+
778
+ if (library === 'fetch') {
779
+ // For fetch: first arg is the URL, response has .status
780
+ urlExpr = t.callExpression(t.identifier('String'), [
781
+ t.logicalExpression(
782
+ '||',
783
+ t.optionalMemberExpression(
784
+ t.identifier('__utopia_api_result'),
785
+ t.identifier('url'),
786
+ false,
787
+ true
788
+ ),
789
+ t.stringLiteral('')
790
+ ),
791
+ ]);
792
+ statusCodeExpr = t.logicalExpression(
793
+ '||',
794
+ t.optionalMemberExpression(
795
+ t.identifier('__utopia_api_result'),
796
+ t.identifier('status'),
797
+ false,
798
+ true
799
+ ),
800
+ t.numericLiteral(0)
801
+ );
802
+ methodExpr = t.stringLiteral(method);
803
+ } else {
804
+ // For axios: response has .status, .config.url, .config.method
805
+ urlExpr = t.logicalExpression(
806
+ '||',
807
+ t.optionalMemberExpression(
808
+ t.optionalMemberExpression(
809
+ t.identifier('__utopia_api_result'),
810
+ t.identifier('config'),
811
+ false,
812
+ true
813
+ ),
814
+ t.identifier('url'),
815
+ false,
816
+ true
817
+ ),
818
+ t.stringLiteral('')
819
+ );
820
+ statusCodeExpr = t.logicalExpression(
821
+ '||',
822
+ t.optionalMemberExpression(
823
+ t.identifier('__utopia_api_result'),
824
+ t.identifier('status'),
825
+ false,
826
+ true
827
+ ),
828
+ t.numericLiteral(0)
829
+ );
830
+ methodExpr = t.logicalExpression(
831
+ '||',
832
+ t.optionalMemberExpression(
833
+ t.optionalMemberExpression(
834
+ t.identifier('__utopia_api_result'),
835
+ t.identifier('config'),
836
+ false,
837
+ true
838
+ ),
839
+ t.identifier('method'),
840
+ false,
841
+ true
842
+ ),
843
+ t.stringLiteral(method)
844
+ );
845
+ }
846
+
847
+ const successReport = t.expressionStatement(
848
+ t.callExpression(
849
+ t.memberExpression(t.identifier('__utopia'), t.identifier('reportApi')),
850
+ [
851
+ t.objectExpression([
852
+ t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
853
+ t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
854
+ t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
855
+ t.objectProperty(t.identifier('method'), methodExpr),
856
+ t.objectProperty(t.identifier('url'), urlExpr),
857
+ t.objectProperty(t.identifier('statusCode'), statusCodeExpr),
858
+ t.objectProperty(t.identifier('duration'), durationExpr),
859
+ ]),
860
+ ]
861
+ )
862
+ );
863
+
864
+ const returnResult = t.returnStatement(t.identifier('__utopia_api_result'));
865
+
866
+ const catchParam = t.identifier('__utopia_api_err');
867
+
868
+ const errorDurationExpr = t.binaryExpression(
869
+ '-',
870
+ t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), []),
871
+ t.identifier('__utopia_api_start')
872
+ );
873
+
874
+ const errorReport = t.expressionStatement(
875
+ t.callExpression(
876
+ t.memberExpression(t.identifier('__utopia'), t.identifier('reportApi')),
877
+ [
878
+ t.objectExpression([
879
+ t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
880
+ t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
881
+ t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
882
+ t.objectProperty(t.identifier('method'), t.stringLiteral(method)),
883
+ t.objectProperty(t.identifier('url'), t.stringLiteral('')),
884
+ t.objectProperty(t.identifier('statusCode'), t.numericLiteral(0)),
885
+ t.objectProperty(t.identifier('duration'), errorDurationExpr),
886
+ t.objectProperty(
887
+ t.identifier('error'),
888
+ t.optionalMemberExpression(
889
+ t.identifier('__utopia_api_err'),
890
+ t.identifier('message'),
891
+ false,
892
+ true
893
+ )
894
+ ),
895
+ ]),
896
+ ]
897
+ )
898
+ );
899
+
900
+ const rethrow = t.throwStatement(t.identifier('__utopia_api_err'));
901
+
902
+ const tryStatement = t.tryStatement(
903
+ t.blockStatement([resultVar, successReport, returnResult]),
904
+ t.catchClause(catchParam, t.blockStatement([errorReport, rethrow]))
905
+ );
906
+
907
+ const arrowBody = t.blockStatement([startVar, tryStatement]);
908
+ const arrow = t.arrowFunctionExpression([], arrowBody, true);
909
+
910
+ return t.callExpression(arrow, []);
911
+ }
912
+
913
+ /**
914
+ * Build the infra probe report statement for entry point files.
915
+ */
916
+ function buildInfraProbeStatement(filePath: string): t.ExpressionStatement {
917
+ // Build: process.env.AWS_REGION ? 'aws' : process.env.GOOGLE_CLOUD_PROJECT ? 'gcp' : process.env.VERCEL ? 'vercel' : 'other'
918
+ const envAccess = (key: string) =>
919
+ t.memberExpression(
920
+ t.memberExpression(t.identifier('process'), t.identifier('env')),
921
+ t.identifier(key)
922
+ );
923
+
924
+ const providerExpr = t.conditionalExpression(
925
+ envAccess('AWS_REGION'),
926
+ t.stringLiteral('aws'),
927
+ t.conditionalExpression(
928
+ envAccess('GOOGLE_CLOUD_PROJECT'),
929
+ t.stringLiteral('gcp'),
930
+ t.conditionalExpression(
931
+ envAccess('VERCEL'),
932
+ t.stringLiteral('vercel'),
933
+ t.stringLiteral('other')
934
+ )
935
+ )
936
+ );
937
+
938
+ const regionExpr = t.logicalExpression(
939
+ '||',
940
+ t.logicalExpression(
941
+ '||',
942
+ t.logicalExpression(
943
+ '||',
944
+ envAccess('AWS_REGION'),
945
+ envAccess('GOOGLE_CLOUD_REGION')
946
+ ),
947
+ envAccess('VERCEL_REGION')
948
+ ),
949
+ t.identifier('undefined')
950
+ );
951
+
952
+ const serviceTypeExpr = t.conditionalExpression(
953
+ envAccess('AWS_LAMBDA_FUNCTION_NAME'),
954
+ t.stringLiteral('lambda'),
955
+ t.conditionalExpression(
956
+ envAccess('K_SERVICE'),
957
+ t.stringLiteral('cloud-run'),
958
+ t.conditionalExpression(
959
+ envAccess('VERCEL'),
960
+ t.stringLiteral('vercel'),
961
+ t.identifier('undefined')
962
+ )
963
+ )
964
+ );
965
+
966
+ const instanceIdExpr = t.logicalExpression(
967
+ '||',
968
+ envAccess('HOSTNAME'),
969
+ t.identifier('undefined')
970
+ );
971
+
972
+ // Filter env vars to exclude secrets:
973
+ // Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.includes('KEY') && ...))
974
+ const filterExpr = t.callExpression(
975
+ t.memberExpression(t.identifier('Object'), t.identifier('fromEntries')),
976
+ [
977
+ t.callExpression(
978
+ t.memberExpression(
979
+ t.callExpression(
980
+ t.memberExpression(t.identifier('Object'), t.identifier('entries')),
981
+ [t.memberExpression(t.identifier('process'), t.identifier('env'))]
982
+ ),
983
+ t.identifier('filter')
984
+ ),
985
+ [
986
+ t.arrowFunctionExpression(
987
+ [t.arrayPattern([t.identifier('k')])],
988
+ t.logicalExpression(
989
+ '&&',
990
+ t.logicalExpression(
991
+ '&&',
992
+ t.logicalExpression(
993
+ '&&',
994
+ t.unaryExpression(
995
+ '!',
996
+ t.callExpression(
997
+ t.memberExpression(t.identifier('k'), t.identifier('includes')),
998
+ [t.stringLiteral('KEY')]
999
+ )
1000
+ ),
1001
+ t.unaryExpression(
1002
+ '!',
1003
+ t.callExpression(
1004
+ t.memberExpression(t.identifier('k'), t.identifier('includes')),
1005
+ [t.stringLiteral('SECRET')]
1006
+ )
1007
+ )
1008
+ ),
1009
+ t.unaryExpression(
1010
+ '!',
1011
+ t.callExpression(
1012
+ t.memberExpression(t.identifier('k'), t.identifier('includes')),
1013
+ [t.stringLiteral('TOKEN')]
1014
+ )
1015
+ )
1016
+ ),
1017
+ t.unaryExpression(
1018
+ '!',
1019
+ t.callExpression(
1020
+ t.memberExpression(t.identifier('k'), t.identifier('includes')),
1021
+ [t.stringLiteral('PASSWORD')]
1022
+ )
1023
+ )
1024
+ )
1025
+ ),
1026
+ ]
1027
+ ),
1028
+ ]
1029
+ );
1030
+
1031
+ // process.memoryUsage?.()?.heapUsed || 0
1032
+ const memoryExpr = t.logicalExpression(
1033
+ '||',
1034
+ t.optionalMemberExpression(
1035
+ t.optionalCallExpression(
1036
+ t.optionalMemberExpression(
1037
+ t.identifier('process'),
1038
+ t.identifier('memoryUsage'),
1039
+ false,
1040
+ true
1041
+ ),
1042
+ [],
1043
+ true
1044
+ ),
1045
+ t.identifier('heapUsed'),
1046
+ false,
1047
+ true
1048
+ ),
1049
+ t.numericLiteral(0)
1050
+ );
1051
+
1052
+ const reportCall = t.expressionStatement(
1053
+ t.callExpression(
1054
+ t.memberExpression(t.identifier('__utopia'), t.identifier('reportInfra')),
1055
+ [
1056
+ t.objectExpression([
1057
+ t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
1058
+ t.objectProperty(t.identifier('line'), t.numericLiteral(1)),
1059
+ t.objectProperty(t.identifier('provider'), providerExpr),
1060
+ t.objectProperty(t.identifier('region'), regionExpr),
1061
+ t.objectProperty(t.identifier('serviceType'), serviceTypeExpr),
1062
+ t.objectProperty(t.identifier('instanceId'), instanceIdExpr),
1063
+ t.objectProperty(t.identifier('envVars'), filterExpr),
1064
+ t.objectProperty(t.identifier('memoryUsage'), memoryExpr),
1065
+ ]),
1066
+ ]
1067
+ )
1068
+ );
1069
+
1070
+ addProbeComment(reportCall);
1071
+ return reportCall;
1072
+ }
1073
+
1074
+ /**
1075
+ * Build a function probe wrapper for Utopia mode.
1076
+ * Wraps function body with timing + arg capture + reportFunction call.
1077
+ * For Utopia mode, also calls reportLlmContext with function source context.
1078
+ */
1079
+ function buildFunctionProbeWrapper(
1080
+ originalBody: t.Statement[],
1081
+ filePath: string,
1082
+ line: number,
1083
+ functionName: string,
1084
+ paramNames: string[],
1085
+ utopiaMode: boolean
1086
+ ): t.Statement[] {
1087
+ // const __utopia_fn_start = Date.now();
1088
+ const startDecl = t.variableDeclaration('const', [
1089
+ t.variableDeclarator(
1090
+ t.identifier('__utopia_fn_start'),
1091
+ t.callExpression(
1092
+ t.memberExpression(t.identifier('Date'), t.identifier('now')),
1093
+ []
1094
+ )
1095
+ ),
1096
+ ]);
1097
+ addProbeComment(startDecl);
1098
+
1099
+ // Build args capture: { param1: param1, param2: param2 }
1100
+ const argsCapture = t.arrayExpression(
1101
+ paramNames.map(name => {
1102
+ try {
1103
+ return t.identifier(name);
1104
+ } catch {
1105
+ return t.stringLiteral(`<${name}>`);
1106
+ }
1107
+ })
1108
+ );
1109
+
1110
+ // __utopia.reportFunction({ file, line, functionName, args, duration, callStack })
1111
+ const reportFnCall = t.expressionStatement(
1112
+ t.callExpression(
1113
+ t.memberExpression(t.identifier('__utopia'), t.identifier('reportFunction')),
1114
+ [
1115
+ t.objectExpression([
1116
+ t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
1117
+ t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
1118
+ t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
1119
+ t.objectProperty(t.identifier('args'), argsCapture),
1120
+ t.objectProperty(
1121
+ t.identifier('returnValue'),
1122
+ t.identifier('__utopia_fn_result')
1123
+ ),
1124
+ t.objectProperty(
1125
+ t.identifier('duration'),
1126
+ t.binaryExpression(
1127
+ '-',
1128
+ t.callExpression(
1129
+ t.memberExpression(t.identifier('Date'), t.identifier('now')),
1130
+ []
1131
+ ),
1132
+ t.identifier('__utopia_fn_start')
1133
+ )
1134
+ ),
1135
+ t.objectProperty(
1136
+ t.identifier('callStack'),
1137
+ t.callExpression(
1138
+ t.memberExpression(
1139
+ t.logicalExpression(
1140
+ '||',
1141
+ t.optionalMemberExpression(
1142
+ t.newExpression(t.identifier('Error'), []),
1143
+ t.identifier('stack'),
1144
+ false,
1145
+ true
1146
+ ),
1147
+ t.stringLiteral('')
1148
+ ),
1149
+ t.identifier('split')
1150
+ ),
1151
+ [t.stringLiteral('\n')]
1152
+ )
1153
+ ),
1154
+ ]),
1155
+ ]
1156
+ )
1157
+ );
1158
+
1159
+ // Build the utopia mode LLM context call if enabled
1160
+ const llmReportStmts: t.Statement[] = [];
1161
+ if (utopiaMode) {
1162
+ // __utopia.reportLlmContext({ file, line, functionName, context: JSON.stringify({ functionName, args, returnValue, duration }) })
1163
+ const llmCall = t.expressionStatement(
1164
+ t.callExpression(
1165
+ t.memberExpression(t.identifier('__utopia'), t.identifier('reportLlmContext')),
1166
+ [
1167
+ t.objectExpression([
1168
+ t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
1169
+ t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
1170
+ t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
1171
+ t.objectProperty(
1172
+ t.identifier('context'),
1173
+ t.callExpression(
1174
+ t.memberExpression(t.identifier('JSON'), t.identifier('stringify')),
1175
+ [
1176
+ t.objectExpression([
1177
+ t.objectProperty(t.identifier('function'), t.stringLiteral(functionName)),
1178
+ t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
1179
+ t.objectProperty(t.identifier('args'), argsCapture),
1180
+ t.objectProperty(t.identifier('returnValue'), t.identifier('__utopia_fn_result')),
1181
+ t.objectProperty(
1182
+ t.identifier('duration'),
1183
+ t.binaryExpression(
1184
+ '-',
1185
+ t.callExpression(
1186
+ t.memberExpression(t.identifier('Date'), t.identifier('now')),
1187
+ []
1188
+ ),
1189
+ t.identifier('__utopia_fn_start')
1190
+ )
1191
+ ),
1192
+ ]),
1193
+ ]
1194
+ )
1195
+ ),
1196
+ ]),
1197
+ ]
1198
+ )
1199
+ );
1200
+ llmReportStmts.push(llmCall);
1201
+ }
1202
+
1203
+ // let __utopia_fn_result;
1204
+ const resultDecl = t.variableDeclaration('let', [
1205
+ t.variableDeclarator(t.identifier('__utopia_fn_result'), t.identifier('undefined')),
1206
+ ]);
1207
+
1208
+ // try { <original body with result capture> } finally { report }
1209
+ // We need to capture the return value. Wrap in try/finally:
1210
+ // try { __utopia_fn_result = (() => { <original body> })(); } finally { reportFunction(); }
1211
+ // Simpler approach: just wrap and report in finally block
1212
+ const finallyBlock = t.blockStatement([reportFnCall, ...llmReportStmts]);
1213
+
1214
+ const tryStatement = t.tryStatement(
1215
+ t.blockStatement([...originalBody]),
1216
+ null,
1217
+ finallyBlock
1218
+ );
1219
+
1220
+ return [startDecl, resultDecl, tryStatement];
1221
+ }
1222
+
1223
+ /**
1224
+ * Build the utopia runtime import declaration.
1225
+ */
1226
+ function buildUtopiaImport(): t.ImportDeclaration {
1227
+ return t.importDeclaration(
1228
+ [t.importSpecifier(t.identifier('__utopia'), t.identifier('__utopia'))],
1229
+ t.stringLiteral('utopia-runtime')
1230
+ );
1231
+ }
1232
+
1233
+ /**
1234
+ * Check if the AST already has an import from 'utopia-runtime'.
1235
+ */
1236
+ function hasUtopiaImport(ast: t.File): boolean {
1237
+ for (const node of ast.program.body) {
1238
+ if (
1239
+ t.isImportDeclaration(node) &&
1240
+ node.source.value === 'utopia-runtime'
1241
+ ) {
1242
+ return true;
1243
+ }
1244
+ }
1245
+ return false;
1246
+ }
1247
+
1248
+ // ---------------------------------------------------------------------------
1249
+ // Core instrumenter
1250
+ // ---------------------------------------------------------------------------
1251
+
1252
+ /**
1253
+ * Instrument a single JavaScript/TypeScript file.
1254
+ */
1255
+ export async function instrumentFile(
1256
+ filePath: string,
1257
+ options: InstrumentOptions
1258
+ ): Promise<InstrumentResult> {
1259
+ const absolutePath = resolve(filePath);
1260
+ const probesAdded: { type: string; line: number; functionName: string }[] = [];
1261
+
1262
+ try {
1263
+ const source = await readFile(absolutePath, 'utf-8');
1264
+ const relPath = relative(process.cwd(), absolutePath);
1265
+ const plugins = getParserPlugins(absolutePath);
1266
+
1267
+ let ast: t.File;
1268
+ try {
1269
+ ast = parser.parse(source, {
1270
+ sourceType: getSourceType(absolutePath),
1271
+ plugins,
1272
+ errorRecovery: true,
1273
+ });
1274
+ } catch (parseError: any) {
1275
+ return {
1276
+ file: relPath,
1277
+ probesAdded: [],
1278
+ success: false,
1279
+ error: `Parse error: ${parseError.message}`,
1280
+ };
1281
+ }
1282
+
1283
+ const shouldInstrument = {
1284
+ error: options.probeTypes.includes('error'),
1285
+ database: options.probeTypes.includes('database'),
1286
+ api: options.probeTypes.includes('api'),
1287
+ infra: options.probeTypes.includes('infra'),
1288
+ function: options.probeTypes.includes('function'),
1289
+ };
1290
+
1291
+ const isEntry = isEntryPoint(absolutePath, options.entryPoints);
1292
+
1293
+ // -----------------------------------------------------------------------
1294
+ // Traverse the AST and inject probes
1295
+ // -----------------------------------------------------------------------
1296
+
1297
+ traverse(ast, {
1298
+ // ---- Error probes: wrap function bodies in try/catch ----
1299
+ 'FunctionDeclaration|ArrowFunctionExpression|FunctionExpression|ClassMethod'(
1300
+ path: any
1301
+ ) {
1302
+ if (!shouldInstrument.error) return;
1303
+
1304
+ const node = path.node;
1305
+ let body: t.BlockStatement | null = null;
1306
+
1307
+ if (t.isArrowFunctionExpression(node)) {
1308
+ if (t.isBlockStatement(node.body)) {
1309
+ body = node.body;
1310
+ } else {
1311
+ // Expression body arrow: () => expr
1312
+ // Convert to block: () => { return expr; }
1313
+ const returnStmt = t.returnStatement(node.body as t.Expression);
1314
+ body = t.blockStatement([returnStmt]);
1315
+ node.body = body;
1316
+ }
1317
+ } else if (
1318
+ t.isFunctionDeclaration(node) ||
1319
+ t.isFunctionExpression(node) ||
1320
+ t.isClassMethod(node)
1321
+ ) {
1322
+ body = node.body;
1323
+ }
1324
+
1325
+ if (!body || body.body.length === 0) return;
1326
+ if (isAlreadyWrapped(body)) return;
1327
+
1328
+ const fnName = getFunctionName(path);
1329
+ const line = node.loc?.start?.line ?? 0;
1330
+ const codeLine = getSourceLine(source, line);
1331
+
1332
+ const tryCatch = buildErrorTryCatch(
1333
+ [...body.body],
1334
+ relPath,
1335
+ line,
1336
+ fnName,
1337
+ codeLine
1338
+ );
1339
+
1340
+ body.body = [tryCatch];
1341
+
1342
+ probesAdded.push({ type: 'error', line, functionName: fnName });
1343
+ },
1344
+
1345
+ // ---- Database and API probes: wrap call expressions ----
1346
+ CallExpression(path: any) {
1347
+ if (!shouldInstrument.database && !shouldInstrument.api) return;
1348
+
1349
+ const node = path.node as t.CallExpression;
1350
+ const line = node.loc?.start?.line ?? 0;
1351
+
1352
+ // Skip if already inside a utopia IIFE
1353
+ if (isInsideUtopiaIIFE(path)) return;
1354
+
1355
+ // ---- Database probes ----
1356
+ if (shouldInstrument.database) {
1357
+ const dbInfo = detectDbCall(node);
1358
+ if (dbInfo) {
1359
+ const fnName = getEnclosingFnName(path);
1360
+ const callStr = callExpressionToString(node);
1361
+
1362
+ const iife = buildDbProbeIIFE(
1363
+ t.cloneNode(node, true),
1364
+ relPath,
1365
+ line,
1366
+ fnName,
1367
+ dbInfo.operation,
1368
+ callStr,
1369
+ dbInfo.table,
1370
+ dbInfo.library
1371
+ );
1372
+
1373
+ // Replace the call expression with an awaited IIFE
1374
+ path.replaceWith(t.awaitExpression(iife));
1375
+ path.skip(); // Don't re-traverse the replacement
1376
+
1377
+ probesAdded.push({
1378
+ type: 'database',
1379
+ line,
1380
+ functionName: fnName,
1381
+ });
1382
+ return;
1383
+ }
1384
+ }
1385
+
1386
+ // ---- API probes ----
1387
+ if (shouldInstrument.api) {
1388
+ const apiInfo = detectApiCall(node);
1389
+ if (apiInfo) {
1390
+ const fnName = getEnclosingFnName(path);
1391
+
1392
+ const iife = buildApiProbeIIFE(
1393
+ t.cloneNode(node, true),
1394
+ relPath,
1395
+ line,
1396
+ fnName,
1397
+ apiInfo.method,
1398
+ apiInfo.library
1399
+ );
1400
+
1401
+ path.replaceWith(t.awaitExpression(iife));
1402
+ path.skip();
1403
+
1404
+ probesAdded.push({
1405
+ type: 'api',
1406
+ line,
1407
+ functionName: fnName,
1408
+ });
1409
+ return;
1410
+ }
1411
+ }
1412
+ },
1413
+ });
1414
+
1415
+ // ---- Function probes (Utopia mode): wrap functions with timing + reporting ----
1416
+ if (shouldInstrument.function) {
1417
+ traverse(ast, {
1418
+ 'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ClassMethod'(
1419
+ path: any
1420
+ ) {
1421
+ const node = path.node;
1422
+ let body: t.BlockStatement | null = null;
1423
+
1424
+ if (t.isArrowFunctionExpression(node)) {
1425
+ if (t.isBlockStatement(node.body)) {
1426
+ body = node.body;
1427
+ } else {
1428
+ const returnStmt = t.returnStatement(node.body as t.Expression);
1429
+ body = t.blockStatement([returnStmt]);
1430
+ node.body = body;
1431
+ }
1432
+ } else if (
1433
+ t.isFunctionDeclaration(node) ||
1434
+ t.isFunctionExpression(node) ||
1435
+ t.isClassMethod(node)
1436
+ ) {
1437
+ body = node.body;
1438
+ }
1439
+
1440
+ if (!body || body.body.length === 0) return;
1441
+
1442
+ // Skip if already has function probe (check for __utopia_fn_start)
1443
+ const hasProbe = body.body.some(
1444
+ (s: t.Statement) =>
1445
+ t.isVariableDeclaration(s) &&
1446
+ s.declarations.some(
1447
+ (d: t.VariableDeclarator) =>
1448
+ t.isIdentifier(d.id) && d.id.name === '__utopia_fn_start'
1449
+ )
1450
+ );
1451
+ if (hasProbe) return;
1452
+
1453
+ // Only instrument "interesting" functions for now:
1454
+ // - DB call handlers, API handlers, exported functions, named functions > 3 statements
1455
+ const fnName = getFunctionName(path);
1456
+ if (!fnName || fnName === '<anonymous>') return;
1457
+ if (body.body.length < 2) return; // Skip trivial functions
1458
+
1459
+ const line = node.loc?.start?.line ?? 0;
1460
+
1461
+ // Extract parameter names
1462
+ const params: string[] = (node.params || [])
1463
+ .map((p: t.Node) => {
1464
+ if (t.isIdentifier(p)) return p.name;
1465
+ if (t.isAssignmentPattern(p) && t.isIdentifier(p.left)) return p.left.name;
1466
+ if (t.isRestElement(p) && t.isIdentifier(p.argument)) return p.argument.name;
1467
+ return null;
1468
+ })
1469
+ .filter(Boolean) as string[];
1470
+
1471
+ const wrappedBody = buildFunctionProbeWrapper(
1472
+ [...body.body],
1473
+ relPath,
1474
+ line,
1475
+ fnName,
1476
+ params,
1477
+ options.utopiaMode
1478
+ );
1479
+
1480
+ body.body = wrappedBody;
1481
+ probesAdded.push({ type: 'function', line, functionName: fnName });
1482
+ },
1483
+ });
1484
+ }
1485
+
1486
+ // ---- Infra probe: add at module level for entry points ----
1487
+ if (shouldInstrument.infra && isEntry) {
1488
+ const infraStmt = buildInfraProbeStatement(relPath);
1489
+
1490
+ // Insert after all imports
1491
+ let insertIndex = 0;
1492
+ for (let i = 0; i < ast.program.body.length; i++) {
1493
+ const stmt = ast.program.body[i];
1494
+ if (t.isImportDeclaration(stmt)) {
1495
+ insertIndex = i + 1;
1496
+ }
1497
+ }
1498
+
1499
+ ast.program.body.splice(insertIndex, 0, infraStmt);
1500
+ probesAdded.push({ type: 'infra', line: 1, functionName: '<module>' });
1501
+ }
1502
+
1503
+ // ---- Add utopia runtime import if probes were added ----
1504
+ if (probesAdded.length > 0 && !hasUtopiaImport(ast)) {
1505
+ const importDecl = buildUtopiaImport();
1506
+ // Insert at very top (before other imports)
1507
+ ast.program.body.unshift(importDecl);
1508
+ }
1509
+
1510
+ // ---- Generate output ----
1511
+ const output = generate(ast, {
1512
+ retainLines: true,
1513
+ comments: true,
1514
+ });
1515
+
1516
+ if (!options.dryRun) {
1517
+ await writeFile(absolutePath, output.code, 'utf-8');
1518
+ }
1519
+
1520
+ return {
1521
+ file: relPath,
1522
+ probesAdded,
1523
+ success: true,
1524
+ };
1525
+ } catch (error: any) {
1526
+ return {
1527
+ file: relative(process.cwd(), absolutePath),
1528
+ probesAdded: [],
1529
+ success: false,
1530
+ error: error.message,
1531
+ };
1532
+ }
1533
+ }
1534
+
1535
+ // ---------------------------------------------------------------------------
1536
+ // Directory instrumenter
1537
+ // ---------------------------------------------------------------------------
1538
+
1539
+ /**
1540
+ * Recursively collect all instrumentable files in a directory.
1541
+ */
1542
+ async function collectFiles(dirPath: string): Promise<string[]> {
1543
+ const results: string[] = [];
1544
+
1545
+ async function walk(dir: string): Promise<void> {
1546
+ let entries: string[];
1547
+ try {
1548
+ entries = await readdir(dir);
1549
+ } catch {
1550
+ return;
1551
+ }
1552
+
1553
+ for (const entry of entries) {
1554
+ const fullPath = join(dir, entry);
1555
+
1556
+ // Skip hidden directories and known non-source dirs
1557
+ if (entry.startsWith('.') || SKIP_DIRS.has(entry)) continue;
1558
+
1559
+ let stats;
1560
+ try {
1561
+ stats = await stat(fullPath);
1562
+ } catch {
1563
+ continue;
1564
+ }
1565
+
1566
+ if (stats.isDirectory()) {
1567
+ await walk(fullPath);
1568
+ } else if (stats.isFile()) {
1569
+ const ext = extname(entry).toLowerCase();
1570
+ if (!VALID_EXTENSIONS.has(ext)) continue;
1571
+
1572
+ // Skip test files, spec files, and declaration files
1573
+ const name = entry.toLowerCase();
1574
+ if (name.endsWith('.test.ts') || name.endsWith('.test.js')) continue;
1575
+ if (name.endsWith('.test.tsx') || name.endsWith('.test.jsx')) continue;
1576
+ if (name.endsWith('.spec.ts') || name.endsWith('.spec.js')) continue;
1577
+ if (name.endsWith('.spec.tsx') || name.endsWith('.spec.jsx')) continue;
1578
+ if (name.endsWith('.d.ts')) continue;
1579
+
1580
+ results.push(fullPath);
1581
+ }
1582
+ }
1583
+ }
1584
+
1585
+ await walk(dirPath);
1586
+ return results;
1587
+ }
1588
+
1589
+ /**
1590
+ * Instrument all eligible files in a directory recursively.
1591
+ */
1592
+ export async function instrumentDirectory(
1593
+ dirPath: string,
1594
+ options: InstrumentOptions
1595
+ ): Promise<InstrumentResult[]> {
1596
+ const absoluteDir = resolve(dirPath);
1597
+ const files = await collectFiles(absoluteDir);
1598
+ const results: InstrumentResult[] = [];
1599
+
1600
+ for (const file of files) {
1601
+ const result = await instrumentFile(file, options);
1602
+ results.push(result);
1603
+ }
1604
+
1605
+ return results;
1606
+ }
1607
+
1608
+ // ---------------------------------------------------------------------------
1609
+ // Validation
1610
+ // ---------------------------------------------------------------------------
1611
+
1612
+ /**
1613
+ * Validate that instrumentation was applied correctly to a file.
1614
+ * Parses the file, checks for syntax errors, and verifies probe markers.
1615
+ */
1616
+ export async function validateInstrumentation(
1617
+ filePath: string
1618
+ ): Promise<ValidationResult> {
1619
+ const absolutePath = resolve(filePath);
1620
+ const errors: string[] = [];
1621
+ const warnings: string[] = [];
1622
+
1623
+ try {
1624
+ const source = await readFile(absolutePath, 'utf-8');
1625
+ const plugins = getParserPlugins(absolutePath);
1626
+
1627
+ let ast: t.File;
1628
+ try {
1629
+ ast = parser.parse(source, {
1630
+ sourceType: getSourceType(absolutePath),
1631
+ plugins,
1632
+ errorRecovery: true,
1633
+ });
1634
+ } catch (parseError: any) {
1635
+ return {
1636
+ valid: false,
1637
+ errors: [`Syntax error after instrumentation: ${parseError.message}`],
1638
+ warnings: [],
1639
+ };
1640
+ }
1641
+
1642
+ // Check for parser errors stored in the AST
1643
+ const astAny = ast as any;
1644
+ if (astAny.errors && astAny.errors.length > 0) {
1645
+ for (const err of astAny.errors) {
1646
+ errors.push(`Parser error: ${err?.message || String(err)}`);
1647
+ }
1648
+ }
1649
+
1650
+ // Check that utopia:probe comments exist and are followed by valid code
1651
+ let probeCommentCount = 0;
1652
+ let validProbeCount = 0;
1653
+
1654
+ traverse(ast, {
1655
+ enter(path: any) {
1656
+ const node = path.node;
1657
+ if (!node.leadingComments) return;
1658
+
1659
+ for (const comment of node.leadingComments) {
1660
+ if (comment.value.includes('utopia:probe')) {
1661
+ probeCommentCount++;
1662
+
1663
+ // Verify the node after the comment is a valid probe construct
1664
+ if (t.isTryStatement(node)) {
1665
+ // Error probe: check catch param is __utopia_err
1666
+ if (
1667
+ node.handler &&
1668
+ node.handler.param &&
1669
+ t.isIdentifier(node.handler.param) &&
1670
+ node.handler.param.name === '__utopia_err'
1671
+ ) {
1672
+ validProbeCount++;
1673
+ } else {
1674
+ warnings.push(
1675
+ `Probe comment at line ${node.loc?.start?.line ?? '?'} followed by try/catch without expected __utopia_err parameter`
1676
+ );
1677
+ }
1678
+ } else if (t.isExpressionStatement(node)) {
1679
+ // Could be infra probe or other report call
1680
+ const expr = node.expression;
1681
+ if (
1682
+ t.isCallExpression(expr) &&
1683
+ t.isMemberExpression(expr.callee) &&
1684
+ t.isIdentifier(expr.callee.object) &&
1685
+ expr.callee.object.name === '__utopia'
1686
+ ) {
1687
+ validProbeCount++;
1688
+ } else {
1689
+ warnings.push(
1690
+ `Probe comment at line ${node.loc?.start?.line ?? '?'} not followed by expected __utopia call`
1691
+ );
1692
+ }
1693
+ } else {
1694
+ warnings.push(
1695
+ `Probe comment at line ${node.loc?.start?.line ?? '?'} followed by unexpected node type: ${node.type}`
1696
+ );
1697
+ }
1698
+ }
1699
+ }
1700
+ },
1701
+ });
1702
+
1703
+ if (probeCommentCount === 0) {
1704
+ warnings.push('No utopia:probe markers found in file');
1705
+ }
1706
+
1707
+ // Verify utopia-runtime import exists if probes are present
1708
+ if (probeCommentCount > 0 && !hasUtopiaImport(ast)) {
1709
+ errors.push(
1710
+ 'File has utopia:probe markers but is missing "utopia-runtime" import'
1711
+ );
1712
+ }
1713
+
1714
+ // Check for unmatched __utopia references (should all be guarded by import)
1715
+ let hasUtopiaRef = false;
1716
+ traverse(ast, {
1717
+ Identifier(path: any) {
1718
+ if (path.node.name === '__utopia') {
1719
+ hasUtopiaRef = true;
1720
+ path.stop();
1721
+ }
1722
+ },
1723
+ });
1724
+
1725
+ if (hasUtopiaRef && !hasUtopiaImport(ast)) {
1726
+ errors.push(
1727
+ 'File references __utopia but does not import from utopia-runtime'
1728
+ );
1729
+ }
1730
+
1731
+ return {
1732
+ valid: errors.length === 0,
1733
+ errors,
1734
+ warnings,
1735
+ };
1736
+ } catch (error: any) {
1737
+ return {
1738
+ valid: false,
1739
+ errors: [`Validation failed: ${error.message}`],
1740
+ warnings: [],
1741
+ };
1742
+ }
1743
+ }
1744
+
1745
+ // ---------------------------------------------------------------------------
1746
+ // Entry point detection
1747
+ // ---------------------------------------------------------------------------
1748
+
1749
+ /**
1750
+ * Determine if a file is considered an entry point.
1751
+ */
1752
+ export function isEntryPoint(
1753
+ filePath: string,
1754
+ customEntryPoints?: string[]
1755
+ ): boolean {
1756
+ const absolutePath = resolve(filePath);
1757
+ const base = basename(absolutePath);
1758
+ const normalizedPath = absolutePath.replace(/\\/g, '/');
1759
+
1760
+ // Check against well-known entry point basenames
1761
+ if (ENTRY_POINT_BASENAMES.has(base)) {
1762
+ return true;
1763
+ }
1764
+
1765
+ // Check Next.js API route patterns
1766
+ if (
1767
+ normalizedPath.includes('/pages/api/') ||
1768
+ normalizedPath.includes('/src/pages/api/') ||
1769
+ normalizedPath.includes('/app/api/')
1770
+ ) {
1771
+ return true;
1772
+ }
1773
+
1774
+ // Check custom entry points
1775
+ if (customEntryPoints) {
1776
+ for (const pattern of customEntryPoints) {
1777
+ const resolvedPattern = resolve(pattern);
1778
+ if (absolutePath === resolvedPattern) {
1779
+ return true;
1780
+ }
1781
+ // Support glob-like suffix matching: if pattern ends with **, treat as directory prefix
1782
+ if (pattern.endsWith('**')) {
1783
+ const prefix = resolve(pattern.slice(0, -2));
1784
+ if (absolutePath.startsWith(prefix)) {
1785
+ return true;
1786
+ }
1787
+ }
1788
+ }
1789
+ }
1790
+
1791
+ return false;
1792
+ }
1793
+
1794
+ // ---------------------------------------------------------------------------
1795
+ // Exports
1796
+ // ---------------------------------------------------------------------------
1797
+
1798
+ export type { InstrumentOptions, InstrumentResult, ValidationResult };