install-glo 2.0.0 → 2.0.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/glo-loop.mjs CHANGED
@@ -1,415 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createInterface } from "node:readline/promises";
4
- import { execSync } from "node:child_process";
5
- import { readFileSync, existsSync } from "node:fs";
6
- import { join, relative } from "node:path";
7
- import { generateText } from "ai";
8
- import { createAnthropic } from "@ai-sdk/anthropic";
9
- import { createOpenAI } from "@ai-sdk/openai";
10
4
  import chalk from "chalk";
11
5
 
12
- // ── Provider ───────────────────────────────────────────────────────────
6
+ import { VITALS } from "./lib/vitals.mjs";
7
+ import { getModel } from "./lib/model.mjs";
8
+ import { runLighthouse, extractMetrics, extractDiagnostics } from "./lib/lighthouse.mjs";
9
+ import { discoverPageFiles } from "./lib/source-discovery.mjs";
10
+ import { analyzeWithAI } from "./lib/ai-analysis.mjs";
11
+ import { printBanner, formatVital, printScore, printSuggestion } from "./lib/display.mjs";
13
12
 
14
- function getModel() {
15
- if (process.env.ANTHROPIC_API_KEY) {
16
- const anthropic = createAnthropic();
17
- return {
18
- model: anthropic("claude-sonnet-4-20250514"),
19
- label: "Claude (Anthropic)",
20
- };
21
- }
22
- if (process.env.OPENAI_API_KEY) {
23
- const openai = createOpenAI();
24
- return { model: openai("gpt-4o-mini"), label: "GPT-4o-mini (OpenAI)" };
25
- }
26
- return null;
27
- }
28
-
29
- // ── Web Vitals ─────────────────────────────────────────────────────────
30
-
31
- const VITALS = {
32
- LCP: {
33
- good: 2500,
34
- unit: "ms",
35
- name: "Largest Contentful Paint",
36
- audit: "largest-contentful-paint",
37
- },
38
- FCP: {
39
- good: 1800,
40
- unit: "ms",
41
- name: "First Contentful Paint",
42
- audit: "first-contentful-paint",
43
- },
44
- CLS: {
45
- good: 0.1,
46
- unit: "",
47
- name: "Cumulative Layout Shift",
48
- audit: "cumulative-layout-shift",
49
- },
50
- TBT: {
51
- good: 200,
52
- unit: "ms",
53
- name: "Total Blocking Time",
54
- audit: "total-blocking-time",
55
- },
56
- SI: {
57
- good: 3400,
58
- unit: "ms",
59
- name: "Speed Index",
60
- audit: "speed-index",
61
- },
62
- TTFB: {
63
- good: 800,
64
- unit: "ms",
65
- name: "Time to First Byte",
66
- audit: "server-response-time",
67
- },
68
- };
69
-
70
- // ── Lighthouse ─────────────────────────────────────────────────────────
71
-
72
- function checkLighthouse() {
73
- try {
74
- execSync("npx -y lighthouse --version", { stdio: "pipe" });
75
- return true;
76
- } catch {
77
- return false;
78
- }
79
- }
80
-
81
- function runLighthouse(url) {
82
- const cmd = [
83
- "npx -y lighthouse",
84
- `"${url}"`,
85
- "--output=json",
86
- '--chrome-flags="--headless --no-sandbox"',
87
- "--only-categories=performance",
88
- "--quiet",
89
- ].join(" ");
90
-
91
- const result = execSync(cmd, {
92
- maxBuffer: 100 * 1024 * 1024,
93
- stdio: ["pipe", "pipe", "pipe"],
94
- });
95
- return JSON.parse(result.toString());
96
- }
97
-
98
- function extractMetrics(report) {
99
- const metrics = {};
100
- for (const [key, info] of Object.entries(VITALS)) {
101
- const audit = report.audits?.[info.audit];
102
- if (audit) {
103
- metrics[key] = {
104
- value: audit.numericValue,
105
- display: audit.displayValue,
106
- score: audit.score,
107
- };
108
- }
109
- }
110
- metrics.performanceScore = Math.round(
111
- (report.categories?.performance?.score || 0) * 100
112
- );
113
- return metrics;
114
- }
115
-
116
- function extractDiagnostics(report) {
117
- const relevant = [
118
- "render-blocking-resources",
119
- "unused-css-rules",
120
- "unused-javascript",
121
- "modern-image-formats",
122
- "uses-optimized-images",
123
- "uses-responsive-images",
124
- "offscreen-images",
125
- "unminified-css",
126
- "unminified-javascript",
127
- "dom-size",
128
- "critical-request-chains",
129
- "largest-contentful-paint-element",
130
- "layout-shift-elements",
131
- "long-tasks",
132
- "mainthread-work-breakdown",
133
- "bootup-time",
134
- "font-display",
135
- "uses-text-compression",
136
- "duplicated-javascript",
137
- "legacy-javascript",
138
- "total-byte-weight",
139
- ];
140
-
141
- const issues = [];
142
- for (const id of relevant) {
143
- const audit = report.audits?.[id];
144
- if (audit && audit.score !== null && audit.score < 1) {
145
- issues.push({
146
- id,
147
- title: audit.title,
148
- displayValue: audit.displayValue || "",
149
- score: audit.score,
150
- });
151
- }
152
- }
153
- return issues.sort((a, b) => a.score - b.score); // worst first
154
- }
155
-
156
- // ── Source File Discovery ──────────────────────────────────────────────
157
-
158
- function discoverPageFiles(projectRoot, route) {
159
- const routePath = route === "/" ? "" : route.replace(/^\//, "");
160
- const files = [];
161
-
162
- const candidates = [
163
- // Next.js App Router
164
- join("app", routePath, "page.tsx"),
165
- join("app", routePath, "page.jsx"),
166
- join("app", routePath, "page.js"),
167
- join("app", routePath, "layout.tsx"),
168
- join("app", routePath, "layout.jsx"),
169
- join("app", "layout.tsx"),
170
- join("app", "layout.jsx"),
171
- // src/app
172
- join("src", "app", routePath, "page.tsx"),
173
- join("src", "app", routePath, "layout.tsx"),
174
- join("src", "app", "layout.tsx"),
175
- // Next.js Pages Router
176
- join("pages", routePath + ".tsx"),
177
- join("pages", routePath + ".jsx"),
178
- join("pages", routePath, "index.tsx"),
179
- join("pages", routePath, "index.jsx"),
180
- // Config files relevant to performance
181
- "next.config.ts",
182
- "next.config.js",
183
- "next.config.mjs",
184
- "vite.config.ts",
185
- "vite.config.js",
186
- ];
187
-
188
- for (const rel of candidates) {
189
- const full = join(projectRoot, rel);
190
- if (existsSync(full)) {
191
- try {
192
- const content = readFileSync(full, "utf8");
193
- if (content.length < 15000) {
194
- files.push({ path: rel, content });
195
- } else {
196
- files.push({
197
- path: rel,
198
- content: content.slice(0, 15000) + "\n// ... truncated",
199
- });
200
- }
201
- } catch {}
202
- }
203
- }
204
-
205
- // Scan for component imports from the page file
206
- if (files.length > 0) {
207
- const pageFile = files[0];
208
- const importRegex = /from\s+["']([./][^"']+)["']/g;
209
- let match;
210
- while ((match = importRegex.exec(pageFile.content)) !== null) {
211
- const importPath = match[1];
212
- const possiblePaths = [
213
- importPath + ".tsx",
214
- importPath + ".jsx",
215
- importPath + ".ts",
216
- importPath + ".js",
217
- join(importPath, "index.tsx"),
218
- ];
219
- for (const p of possiblePaths) {
220
- const resolved = join(
221
- projectRoot,
222
- files[0].path.replace(/[^/]+$/, ""),
223
- p
224
- );
225
- if (existsSync(resolved)) {
226
- try {
227
- const content = readFileSync(resolved, "utf8");
228
- const relPath = relative(projectRoot, resolved);
229
- if (!files.find((f) => f.path === relPath)) {
230
- files.push({
231
- path: relPath,
232
- content:
233
- content.length > 8000
234
- ? content.slice(0, 8000) + "\n// ... truncated"
235
- : content,
236
- });
237
- }
238
- } catch {}
239
- break;
240
- }
241
- }
242
- }
243
- }
244
-
245
- return files;
246
- }
247
-
248
- // ── Display Helpers ────────────────────────────────────────────────────
249
-
250
- function formatVital(key, value) {
251
- const info = VITALS[key];
252
- if (!info || value === undefined || value === null) return null;
253
- const passed = key === "CLS" ? value <= info.good : value <= info.good;
254
- const formatted =
255
- info.unit === "ms" ? `${Math.round(value)}ms` : value.toFixed(3);
256
- const icon = passed ? chalk.green("✓") : chalk.red("✗");
257
- const color = passed ? chalk.green : chalk.red;
258
- return (
259
- ` ${chalk.white(key.padEnd(6))}` +
260
- `${color(formatted.padStart(9))}` +
261
- ` ${chalk.dim(`(good: <${info.good}${info.unit})`)} ${icon}`
262
- );
263
- }
264
-
265
- function printBanner() {
266
- const lines = [
267
- "",
268
- chalk.hex("#FF8C00").bold(
269
- " ┌─────────────────────────────────────────────────────┐"
270
- ),
271
- chalk.hex("#FF8C00").bold(" │") +
272
- chalk.hex("#FF8C00").bold(
273
- " T H E G L O L O O P "
274
- ) +
275
- chalk.hex("#FF8C00").bold("│"),
276
- chalk.hex("#FF8C00").bold(" │") +
277
- chalk.dim(
278
- " Web Vitals Optimization Engine "
279
- ) +
280
- chalk.hex("#FF8C00").bold("│"),
281
- chalk.hex("#FF8C00").bold(" │") +
282
- chalk.dim(
283
- " "
284
- ) +
285
- chalk.hex("#FF8C00").bold("│"),
286
- chalk.hex("#FF8C00").bold(" │") +
287
- chalk.hex("#4AF626")(" G") +
288
- chalk.white("ather → ") +
289
- chalk.dim("run Lighthouse, extract metrics ") +
290
- chalk.hex("#FF8C00").bold("│"),
291
- chalk.hex("#FF8C00").bold(" │") +
292
- chalk.hex("#4AF626")(" L") +
293
- chalk.white("everage → ") +
294
- chalk.dim("AI analyzes code + diagnostics ") +
295
- chalk.hex("#FF8C00").bold("│"),
296
- chalk.hex("#FF8C00").bold(" │") +
297
- chalk.hex("#4AF626")(" O") +
298
- chalk.white("perate → ") +
299
- chalk.dim("apply fix, re-measure, repeat ") +
300
- chalk.hex("#FF8C00").bold("│"),
301
- chalk.hex("#FF8C00").bold(" │") +
302
- chalk.dim(
303
- " "
304
- ) +
305
- chalk.hex("#FF8C00").bold("│"),
306
- chalk.hex("#FF8C00").bold(" │") +
307
- chalk.dim(" ↻ repeat until target met") +
308
- chalk.dim(
309
- " "
310
- ) +
311
- chalk.hex("#FF8C00").bold("│"),
312
- chalk.hex("#FF8C00").bold(
313
- " └─────────────────────────────────────────────────────┘"
314
- ),
315
- "",
316
- ];
317
- console.log(lines.join("\n"));
318
- }
319
-
320
- // ── AI Analysis ────────────────────────────────────────────────────────
321
-
322
- async function analyzeWithAI(
323
- model,
324
- targetVital,
325
- metrics,
326
- diagnostics,
327
- sourceFiles,
328
- loopNumber,
329
- previousSuggestions
330
- ) {
331
- const vitalInfo = VITALS[targetVital];
332
-
333
- const sourceContext = sourceFiles
334
- .map((f) => `--- ${f.path} ---\n${f.content}`)
335
- .join("\n\n");
336
-
337
- const diagText = diagnostics
338
- .slice(0, 10)
339
- .map(
340
- (d) =>
341
- `- ${d.title}${d.displayValue ? ` (${d.displayValue})` : ""} [score: ${d.score}]`
342
- )
343
- .join("\n");
344
-
345
- const metricsText = Object.entries(metrics)
346
- .filter(([k]) => k !== "performanceScore")
347
- .map(([k, v]) => {
348
- const info = VITALS[k];
349
- if (!info) return null;
350
- const val = info.unit === "ms" ? `${Math.round(v.value)}ms` : v.value?.toFixed(3);
351
- const status = v.value <= info.good ? "PASS" : "FAIL";
352
- return ` ${k}: ${val} (good: <${info.good}${info.unit}) [${status}]`;
353
- })
354
- .filter(Boolean)
355
- .join("\n");
356
-
357
- const prevContext =
358
- previousSuggestions.length > 0
359
- ? `\n\nPrevious suggestions already applied:\n${previousSuggestions.map((s, i) => `${i + 1}. ${s}`).join("\n")}\n\nDo NOT repeat these. Find the NEXT optimization.`
360
- : "";
361
-
362
- const prompt = `You are a frontend infrastructure engineer optimizing web performance.
363
-
364
- ## Current Metrics (Loop ${loopNumber})
365
- Performance Score: ${metrics.performanceScore}/100
366
- ${metricsText}
367
-
368
- ## Target
369
- Optimize ${targetVital} (${vitalInfo.name}): currently ${metrics[targetVital]?.value !== undefined ? (vitalInfo.unit === "ms" ? Math.round(metrics[targetVital].value) + "ms" : metrics[targetVital].value.toFixed(3)) : "unknown"}, target: <${vitalInfo.good}${vitalInfo.unit}
370
-
371
- ## Lighthouse Diagnostics (sorted by severity)
372
- ${diagText || "No failing diagnostics."}
373
-
374
- ## Source Files
375
- ${sourceContext || "No source files found for this route."}
376
- ${prevContext}
377
-
378
- ## Instructions
379
-
380
- Respond in EXACTLY this format (no markdown fences):
381
-
382
- DIAGNOSIS: One sentence explaining what is causing the ${targetVital} issue.
383
-
384
- FILE: path/to/file.ext
385
- LINE: approximate line number (or "new" if adding to config)
386
-
387
- BEFORE:
388
- <exact code to replace, or "N/A" if adding new code>
389
-
390
- AFTER:
391
- <optimized code>
392
-
393
- WHY: One sentence on expected impact with estimated improvement in ${vitalInfo.unit || "score"}.
394
-
395
- Keep the fix SURGICAL — one change per loop. Prefer high-impact, low-risk changes:
396
- - Image optimization (priority, sizes, lazy/eager, format)
397
- - Font loading (display: swap, preload, subset)
398
- - Bundle optimization (dynamic imports, tree shaking)
399
- - Render-blocking resources (async, defer, preload)
400
- - Layout shift prevention (explicit dimensions, aspect-ratio)
401
- - Server response (caching, compression, CDN)
402
- - Component-level code splitting`;
403
-
404
- const result = await generateText({
405
- model,
406
- prompt,
407
- });
408
-
409
- return result.text;
410
- }
411
-
412
- // ── Main ───────────────────────────────────────────────────────────────
13
+ // ── Main ───────────────────────────────────────────────────────────
413
14
 
414
15
  async function main() {
415
16
  printBanner();
@@ -443,11 +44,9 @@ async function main() {
443
44
  );
444
45
  const targetUrl = url.trim() || "http://localhost:3000";
445
46
 
446
- // Extract route from URL for source file discovery
447
47
  let route = "/";
448
48
  try {
449
- const parsed = new URL(targetUrl);
450
- route = parsed.pathname;
49
+ route = new URL(targetUrl).pathname;
451
50
  } catch {}
452
51
 
453
52
  console.log("");
@@ -463,7 +62,7 @@ async function main() {
463
62
  const vitalInput = await rl.question(
464
63
  chalk.hex("#FF8C00")(" Target vital ") + chalk.dim("(default: LCP): ")
465
64
  );
466
- const targetVital = (vitalInput.trim().toUpperCase() || "LCP");
65
+ const targetVital = vitalInput.trim().toUpperCase() || "LCP";
467
66
  if (!VITALS[targetVital]) {
468
67
  console.log(chalk.red(`\n Unknown vital: ${targetVital}`));
469
68
  console.log(chalk.dim(` Choose from: ${Object.keys(VITALS).join(", ")}\n`));
@@ -488,9 +87,8 @@ async function main() {
488
87
  chalk.hex("#FF8C00")(" Loops: ") + chalk.white(`${maxLoops} max`)
489
88
  );
490
89
 
491
- // Discover source files
492
90
  const projectRoot = process.env.INIT_CWD || process.cwd();
493
- const sourceFiles = discoverPageFiles(projectRoot, route);
91
+ let sourceFiles = discoverPageFiles(projectRoot, route);
494
92
  if (sourceFiles.length > 0) {
495
93
  console.log(
496
94
  chalk.hex("#FF8C00")(" Files: ") +
@@ -514,9 +112,9 @@ async function main() {
514
112
  console.log(chalk.hex("#4AF626").bold("\n GATHER"));
515
113
  console.log(chalk.dim(" Running Lighthouse...\n"));
516
114
 
517
- let report, metrics, diagnostics;
115
+ let metrics, diagnostics;
518
116
  try {
519
- report = runLighthouse(targetUrl);
117
+ const report = runLighthouse(targetUrl);
520
118
  metrics = extractMetrics(report);
521
119
  diagnostics = extractDiagnostics(report);
522
120
  } catch (err) {
@@ -532,17 +130,7 @@ async function main() {
532
130
  break;
533
131
  }
534
132
 
535
- console.log(
536
- chalk.white(` Score `) +
537
- chalk.bold(
538
- metrics.performanceScore >= 90
539
- ? chalk.green(`${metrics.performanceScore}/100`)
540
- : metrics.performanceScore >= 50
541
- ? chalk.yellow(`${metrics.performanceScore}/100`)
542
- : chalk.red(`${metrics.performanceScore}/100`)
543
- )
544
- );
545
-
133
+ printScore(metrics.performanceScore);
546
134
  for (const key of Object.keys(VITALS)) {
547
135
  const line = formatVital(key, metrics[key]?.value);
548
136
  if (line) console.log(line);
@@ -551,22 +139,22 @@ async function main() {
551
139
  // Check if target is already met
552
140
  const currentValue = metrics[targetVital]?.value;
553
141
  if (currentValue !== undefined && currentValue <= VITALS[targetVital].good) {
142
+ const vitalInfo = VITALS[targetVital];
143
+ const display = vitalInfo.unit === "ms"
144
+ ? Math.round(currentValue) + "ms"
145
+ : currentValue.toFixed(3);
554
146
  console.log(
555
147
  chalk.green.bold(
556
- `\n Target met! ${targetVital} = ${VITALS[targetVital].unit === "ms" ? Math.round(currentValue) + "ms" : currentValue.toFixed(3)} (good: <${VITALS[targetVital].good}${VITALS[targetVital].unit})`
148
+ `\n Target met! ${targetVital} = ${display} (good: <${vitalInfo.good}${vitalInfo.unit})`
149
+ )
150
+ );
151
+ console.log(
152
+ chalk.dim(
153
+ loop === 1
154
+ ? ` This page is already performing well for ${targetVital}.`
155
+ : ` Took ${loop - 1} optimization loop(s).`
557
156
  )
558
157
  );
559
- if (loop === 1) {
560
- console.log(
561
- chalk.dim(
562
- " This page is already performing well for " + targetVital + "."
563
- )
564
- );
565
- } else {
566
- console.log(
567
- chalk.dim(` Took ${loop - 1} optimization loop(s).`)
568
- );
569
- }
570
158
  console.log(
571
159
  chalk.dim(
572
160
  "\n Want deeper optimization? " +
@@ -598,25 +186,7 @@ async function main() {
598
186
  break;
599
187
  }
600
188
 
601
- // Display the suggestion
602
- for (const line of suggestion.split("\n")) {
603
- if (
604
- line.startsWith("DIAGNOSIS:") ||
605
- line.startsWith("FILE:") ||
606
- line.startsWith("LINE:") ||
607
- line.startsWith("WHY:")
608
- ) {
609
- const [label, ...rest] = line.split(":");
610
- console.log(
611
- chalk.hex("#FF8C00")(` ${label}:`) +
612
- chalk.white(rest.join(":"))
613
- );
614
- } else if (line.startsWith("BEFORE:") || line.startsWith("AFTER:")) {
615
- console.log(chalk.hex("#FF8C00")(` ${line}`));
616
- } else {
617
- console.log(chalk.hex("#89b4fa")(` ${line}`));
618
- }
619
- }
189
+ printSuggestion(suggestion);
620
190
 
621
191
  // ── OPERATE ─────────────────────────────────────────────────
622
192
 
@@ -633,19 +203,14 @@ async function main() {
633
203
  }
634
204
 
635
205
  if (choice === "y" || choice === "yes") {
636
- previousSuggestions.push(suggestion.split("\n")[0]); // store diagnosis
206
+ previousSuggestions.push(suggestion.split("\n")[0]);
637
207
  console.log(
638
208
  chalk.dim(
639
209
  " Apply the change above to your code, then press Enter to re-measure."
640
210
  )
641
211
  );
642
212
  await rl.question(chalk.dim(" Press Enter when ready..."));
643
- // Re-read source files in case they changed
644
- const updatedFiles = discoverPageFiles(projectRoot, route);
645
- if (updatedFiles.length > 0) {
646
- sourceFiles.length = 0;
647
- sourceFiles.push(...updatedFiles);
648
- }
213
+ sourceFiles = discoverPageFiles(projectRoot, route);
649
214
  } else {
650
215
  previousSuggestions.push(`SKIPPED: ${suggestion.split("\n")[0]}`);
651
216
  console.log(chalk.dim(" Skipped. Moving to next suggestion.\n"));
@@ -0,0 +1,107 @@
1
+ import { generateText as defaultGenerateText } from "ai";
2
+ import { VITALS } from "./vitals.mjs";
3
+
4
+ function buildPrompt(targetVital, metrics, diagnostics, sourceFiles, loopNumber, previousSuggestions) {
5
+ const vitalInfo = VITALS[targetVital];
6
+
7
+ const sourceContext = sourceFiles
8
+ .map((f) => `--- ${f.path} ---\n${f.content}`)
9
+ .join("\n\n");
10
+
11
+ const diagText = diagnostics
12
+ .slice(0, 10)
13
+ .map(
14
+ (d) =>
15
+ `- ${d.title}${d.displayValue ? ` (${d.displayValue})` : ""} [score: ${d.score}]`
16
+ )
17
+ .join("\n");
18
+
19
+ const metricsText = Object.entries(metrics)
20
+ .filter(([k]) => k !== "performanceScore")
21
+ .map(([k, v]) => {
22
+ const info = VITALS[k];
23
+ if (!info) return null;
24
+ const val = info.unit === "ms" ? `${Math.round(v.value)}ms` : v.value?.toFixed(3);
25
+ const status = v.value <= info.good ? "PASS" : "FAIL";
26
+ return ` ${k}: ${val} (good: <${info.good}${info.unit}) [${status}]`;
27
+ })
28
+ .filter(Boolean)
29
+ .join("\n");
30
+
31
+ const prevContext =
32
+ previousSuggestions.length > 0
33
+ ? `\n\nPrevious suggestions already applied:\n${previousSuggestions.map((s, i) => `${i + 1}. ${s}`).join("\n")}\n\nDo NOT repeat these. Find the NEXT optimization.`
34
+ : "";
35
+
36
+ const currentVal = metrics[targetVital]?.value !== undefined
37
+ ? (vitalInfo.unit === "ms" ? Math.round(metrics[targetVital].value) + "ms" : metrics[targetVital].value.toFixed(3))
38
+ : "unknown";
39
+
40
+ return `You are a frontend infrastructure engineer optimizing web performance.
41
+
42
+ ## Current Metrics (Loop ${loopNumber})
43
+ Performance Score: ${metrics.performanceScore}/100
44
+ ${metricsText}
45
+
46
+ ## Target
47
+ Optimize ${targetVital} (${vitalInfo.name}): currently ${currentVal}, target: <${vitalInfo.good}${vitalInfo.unit}
48
+
49
+ ## Lighthouse Diagnostics (sorted by severity)
50
+ ${diagText || "No failing diagnostics."}
51
+
52
+ ## Source Files
53
+ ${sourceContext || "No source files found for this route."}
54
+ ${prevContext}
55
+
56
+ ## Instructions
57
+
58
+ Respond in EXACTLY this format (no markdown fences):
59
+
60
+ DIAGNOSIS: One sentence explaining what is causing the ${targetVital} issue.
61
+
62
+ FILE: path/to/file.ext
63
+ LINE: approximate line number (or "new" if adding to config)
64
+
65
+ BEFORE:
66
+ <exact code to replace, or "N/A" if adding new code>
67
+
68
+ AFTER:
69
+ <optimized code>
70
+
71
+ WHY: One sentence on expected impact with estimated improvement in ${vitalInfo.unit || "score"}.
72
+
73
+ Keep the fix SURGICAL — one change per loop. Prefer high-impact, low-risk changes:
74
+ - Image optimization (priority, sizes, lazy/eager, format)
75
+ - Font loading (display: swap, preload, subset)
76
+ - Bundle optimization (dynamic imports, tree shaking)
77
+ - Render-blocking resources (async, defer, preload)
78
+ - Layout shift prevention (explicit dimensions, aspect-ratio)
79
+ - Server response (caching, compression, CDN)
80
+ - Component-level code splitting`;
81
+ }
82
+
83
+ export async function analyzeWithAI(
84
+ model,
85
+ targetVital,
86
+ metrics,
87
+ diagnostics,
88
+ sourceFiles,
89
+ loopNumber,
90
+ previousSuggestions,
91
+ { generateText = defaultGenerateText } = {}
92
+ ) {
93
+ const prompt = buildPrompt(
94
+ targetVital,
95
+ metrics,
96
+ diagnostics,
97
+ sourceFiles,
98
+ loopNumber,
99
+ previousSuggestions
100
+ );
101
+
102
+ const result = await generateText({ model, prompt });
103
+ return result.text;
104
+ }
105
+
106
+ // Exported for testing
107
+ export { buildPrompt };