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.
@@ -0,0 +1,103 @@
1
+ import chalk from "chalk";
2
+ import { VITALS } from "./vitals.mjs";
3
+
4
+ export function formatVital(key, value) {
5
+ const info = VITALS[key];
6
+ if (!info || value === undefined || value === null) return null;
7
+ const passed = value <= info.good;
8
+ const formatted =
9
+ info.unit === "ms" ? `${Math.round(value)}ms` : value.toFixed(3);
10
+ const icon = passed ? chalk.green("✓") : chalk.red("✗");
11
+ const color = passed ? chalk.green : chalk.red;
12
+ return (
13
+ ` ${chalk.white(key.padEnd(6))}` +
14
+ `${color(formatted.padStart(9))}` +
15
+ ` ${chalk.dim(`(good: <${info.good}${info.unit})`)} ${icon}`
16
+ );
17
+ }
18
+
19
+ export function printBanner() {
20
+ const lines = [
21
+ "",
22
+ chalk.hex("#FF8C00").bold(
23
+ " ┌─────────────────────────────────────────────────────┐"
24
+ ),
25
+ chalk.hex("#FF8C00").bold(" │") +
26
+ chalk.hex("#FF8C00").bold(
27
+ " T H E G L O L O O P "
28
+ ) +
29
+ chalk.hex("#FF8C00").bold("│"),
30
+ chalk.hex("#FF8C00").bold(" │") +
31
+ chalk.dim(
32
+ " Web Vitals Optimization Engine "
33
+ ) +
34
+ chalk.hex("#FF8C00").bold("│"),
35
+ chalk.hex("#FF8C00").bold(" │") +
36
+ chalk.dim(
37
+ " "
38
+ ) +
39
+ chalk.hex("#FF8C00").bold("│"),
40
+ chalk.hex("#FF8C00").bold(" │") +
41
+ chalk.hex("#4AF626")(" G") +
42
+ chalk.white("ather → ") +
43
+ chalk.dim("run Lighthouse, extract metrics ") +
44
+ chalk.hex("#FF8C00").bold("│"),
45
+ chalk.hex("#FF8C00").bold(" │") +
46
+ chalk.hex("#4AF626")(" L") +
47
+ chalk.white("everage → ") +
48
+ chalk.dim("AI analyzes code + diagnostics ") +
49
+ chalk.hex("#FF8C00").bold("│"),
50
+ chalk.hex("#FF8C00").bold(" │") +
51
+ chalk.hex("#4AF626")(" O") +
52
+ chalk.white("perate → ") +
53
+ chalk.dim("apply fix, re-measure, repeat ") +
54
+ chalk.hex("#FF8C00").bold("│"),
55
+ chalk.hex("#FF8C00").bold(" │") +
56
+ chalk.dim(
57
+ " "
58
+ ) +
59
+ chalk.hex("#FF8C00").bold("│"),
60
+ chalk.hex("#FF8C00").bold(" │") +
61
+ chalk.dim(" ↻ repeat until target met") +
62
+ chalk.dim(
63
+ " "
64
+ ) +
65
+ chalk.hex("#FF8C00").bold("│"),
66
+ chalk.hex("#FF8C00").bold(
67
+ " └─────────────────────────────────────────────────────┘"
68
+ ),
69
+ "",
70
+ ];
71
+ console.log(lines.join("\n"));
72
+ }
73
+
74
+ export function printScore(score) {
75
+ const colored =
76
+ score >= 90
77
+ ? chalk.green(`${score}/100`)
78
+ : score >= 50
79
+ ? chalk.yellow(`${score}/100`)
80
+ : chalk.red(`${score}/100`);
81
+ console.log(chalk.white(` Score `) + chalk.bold(colored));
82
+ }
83
+
84
+ export function printSuggestion(suggestion) {
85
+ for (const line of suggestion.split("\n")) {
86
+ if (
87
+ line.startsWith("DIAGNOSIS:") ||
88
+ line.startsWith("FILE:") ||
89
+ line.startsWith("LINE:") ||
90
+ line.startsWith("WHY:")
91
+ ) {
92
+ const [label, ...rest] = line.split(":");
93
+ console.log(
94
+ chalk.hex("#FF8C00")(` ${label}:`) +
95
+ chalk.white(rest.join(":"))
96
+ );
97
+ } else if (line.startsWith("BEFORE:") || line.startsWith("AFTER:")) {
98
+ console.log(chalk.hex("#FF8C00")(` ${line}`));
99
+ } else {
100
+ console.log(chalk.hex("#89b4fa")(` ${line}`));
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,86 @@
1
+ import { execSync as defaultExecSync } from "node:child_process";
2
+ import { VITALS } from "./vitals.mjs";
3
+
4
+ export function checkLighthouse({ execSync = defaultExecSync } = {}) {
5
+ try {
6
+ execSync("npx -y lighthouse --version", { stdio: "pipe" });
7
+ return true;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ export function runLighthouse(url, { execSync = defaultExecSync } = {}) {
14
+ const cmd = [
15
+ "npx -y lighthouse",
16
+ `"${url}"`,
17
+ "--output=json",
18
+ '--chrome-flags="--headless --no-sandbox"',
19
+ "--only-categories=performance",
20
+ "--quiet",
21
+ ].join(" ");
22
+
23
+ const result = execSync(cmd, {
24
+ maxBuffer: 100 * 1024 * 1024,
25
+ stdio: ["pipe", "pipe", "pipe"],
26
+ });
27
+ return JSON.parse(result.toString());
28
+ }
29
+
30
+ export function extractMetrics(report) {
31
+ const metrics = {};
32
+ for (const [key, info] of Object.entries(VITALS)) {
33
+ const audit = report.audits?.[info.audit];
34
+ if (audit) {
35
+ metrics[key] = {
36
+ value: audit.numericValue,
37
+ display: audit.displayValue,
38
+ score: audit.score,
39
+ };
40
+ }
41
+ }
42
+ metrics.performanceScore = Math.round(
43
+ (report.categories?.performance?.score || 0) * 100
44
+ );
45
+ return metrics;
46
+ }
47
+
48
+ export function extractDiagnostics(report) {
49
+ const relevant = [
50
+ "render-blocking-resources",
51
+ "unused-css-rules",
52
+ "unused-javascript",
53
+ "modern-image-formats",
54
+ "uses-optimized-images",
55
+ "uses-responsive-images",
56
+ "offscreen-images",
57
+ "unminified-css",
58
+ "unminified-javascript",
59
+ "dom-size",
60
+ "critical-request-chains",
61
+ "largest-contentful-paint-element",
62
+ "layout-shift-elements",
63
+ "long-tasks",
64
+ "mainthread-work-breakdown",
65
+ "bootup-time",
66
+ "font-display",
67
+ "uses-text-compression",
68
+ "duplicated-javascript",
69
+ "legacy-javascript",
70
+ "total-byte-weight",
71
+ ];
72
+
73
+ const issues = [];
74
+ for (const id of relevant) {
75
+ const audit = report.audits?.[id];
76
+ if (audit && audit.score !== null && audit.score < 1) {
77
+ issues.push({
78
+ id,
79
+ title: audit.title,
80
+ displayValue: audit.displayValue || "",
81
+ score: audit.score,
82
+ });
83
+ }
84
+ }
85
+ return issues.sort((a, b) => a.score - b.score);
86
+ }
package/lib/model.mjs ADDED
@@ -0,0 +1,21 @@
1
+ import { createAnthropic as defaultCreateAnthropic } from "@ai-sdk/anthropic";
2
+ import { createOpenAI as defaultCreateOpenAI } from "@ai-sdk/openai";
3
+
4
+ export function getModel({
5
+ env = process.env,
6
+ createAnthropic = defaultCreateAnthropic,
7
+ createOpenAI = defaultCreateOpenAI,
8
+ } = {}) {
9
+ if (env.ANTHROPIC_API_KEY) {
10
+ const anthropic = createAnthropic();
11
+ return {
12
+ model: anthropic("claude-sonnet-4-20250514"),
13
+ label: "Claude (Anthropic)",
14
+ };
15
+ }
16
+ if (env.OPENAI_API_KEY) {
17
+ const openai = createOpenAI();
18
+ return { model: openai("gpt-4o-mini"), label: "GPT-4o-mini (OpenAI)" };
19
+ }
20
+ return null;
21
+ }
@@ -0,0 +1,111 @@
1
+ import {
2
+ readFileSync as defaultReadFileSync,
3
+ existsSync as defaultExistsSync,
4
+ } from "node:fs";
5
+ import { join, relative } from "node:path";
6
+
7
+ const MAX_FILE_SIZE = 15_000;
8
+ const MAX_IMPORT_SIZE = 8_000;
9
+
10
+ function pageCandidates(routePath) {
11
+ return [
12
+ // Next.js App Router
13
+ join("app", routePath, "page.tsx"),
14
+ join("app", routePath, "page.jsx"),
15
+ join("app", routePath, "page.js"),
16
+ join("app", routePath, "layout.tsx"),
17
+ join("app", routePath, "layout.jsx"),
18
+ join("app", "layout.tsx"),
19
+ join("app", "layout.jsx"),
20
+ // src/app
21
+ join("src", "app", routePath, "page.tsx"),
22
+ join("src", "app", routePath, "layout.tsx"),
23
+ join("src", "app", "layout.tsx"),
24
+ // Next.js Pages Router
25
+ join("pages", routePath + ".tsx"),
26
+ join("pages", routePath + ".jsx"),
27
+ join("pages", routePath, "index.tsx"),
28
+ join("pages", routePath, "index.jsx"),
29
+ // Config files relevant to performance
30
+ "next.config.ts",
31
+ "next.config.js",
32
+ "next.config.mjs",
33
+ "vite.config.ts",
34
+ "vite.config.js",
35
+ ];
36
+ }
37
+
38
+ function truncate(content, limit) {
39
+ return content.length > limit
40
+ ? content.slice(0, limit) + "\n// ... truncated"
41
+ : content;
42
+ }
43
+
44
+ function safeRead(fullPath, limit, { readFileSync, existsSync }) {
45
+ if (!existsSync(fullPath)) return null;
46
+ try {
47
+ const content = readFileSync(fullPath, "utf8");
48
+ return truncate(content, limit);
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function resolveImports(pageFile, projectRoot, files, { readFileSync, existsSync }) {
55
+ const importRegex = /from\s+["']([./][^"']+)["']/g;
56
+ let match;
57
+ while ((match = importRegex.exec(pageFile.content)) !== null) {
58
+ const importPath = match[1];
59
+ const possiblePaths = [
60
+ importPath + ".tsx",
61
+ importPath + ".jsx",
62
+ importPath + ".ts",
63
+ importPath + ".js",
64
+ join(importPath, "index.tsx"),
65
+ ];
66
+ for (const p of possiblePaths) {
67
+ const resolved = join(
68
+ projectRoot,
69
+ pageFile.path.replace(/[^/]+$/, ""),
70
+ p
71
+ );
72
+ if (existsSync(resolved)) {
73
+ const content = safeRead(resolved, MAX_IMPORT_SIZE, { readFileSync, existsSync });
74
+ if (content !== null) {
75
+ const relPath = relative(projectRoot, resolved);
76
+ if (!files.find((f) => f.path === relPath)) {
77
+ files.push({ path: relPath, content });
78
+ }
79
+ }
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ export function discoverPageFiles(
87
+ projectRoot,
88
+ route,
89
+ { readFileSync = defaultReadFileSync, existsSync = defaultExistsSync } = {}
90
+ ) {
91
+ const routePath = route === "/" ? "" : route.replace(/^\//, "");
92
+ const files = [];
93
+ const deps = { readFileSync, existsSync };
94
+
95
+ const seen = new Set();
96
+ for (const rel of pageCandidates(routePath)) {
97
+ if (seen.has(rel)) continue;
98
+ seen.add(rel);
99
+ const full = join(projectRoot, rel);
100
+ const content = safeRead(full, MAX_FILE_SIZE, deps);
101
+ if (content !== null) {
102
+ files.push({ path: rel, content });
103
+ }
104
+ }
105
+
106
+ if (files.length > 0) {
107
+ resolveImports(files[0], projectRoot, files, deps);
108
+ }
109
+
110
+ return files;
111
+ }
package/lib/vitals.mjs ADDED
@@ -0,0 +1,40 @@
1
+ // Web Vitals constants and thresholds
2
+
3
+ export const VITALS = {
4
+ LCP: {
5
+ good: 2500,
6
+ unit: "ms",
7
+ name: "Largest Contentful Paint",
8
+ audit: "largest-contentful-paint",
9
+ },
10
+ FCP: {
11
+ good: 1800,
12
+ unit: "ms",
13
+ name: "First Contentful Paint",
14
+ audit: "first-contentful-paint",
15
+ },
16
+ CLS: {
17
+ good: 0.1,
18
+ unit: "",
19
+ name: "Cumulative Layout Shift",
20
+ audit: "cumulative-layout-shift",
21
+ },
22
+ TBT: {
23
+ good: 200,
24
+ unit: "ms",
25
+ name: "Total Blocking Time",
26
+ audit: "total-blocking-time",
27
+ },
28
+ SI: {
29
+ good: 3400,
30
+ unit: "ms",
31
+ name: "Speed Index",
32
+ audit: "speed-index",
33
+ },
34
+ TTFB: {
35
+ good: 800,
36
+ unit: "ms",
37
+ name: "Time to First Byte",
38
+ audit: "server-response-time",
39
+ },
40
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "install-glo",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "GLO Loop — AI-powered web vitals optimization engine built with Vercel AI SDK",
5
5
  "main": "index.mjs",
6
6
  "bin": {
@@ -10,7 +10,8 @@
10
10
  "about-glo-loop": "about.mjs"
11
11
  },
12
12
  "scripts": {
13
- "postinstall": "node postinstall.mjs"
13
+ "postinstall": "node postinstall.mjs",
14
+ "test": "node --test __tests__/*.test.mjs"
14
15
  },
15
16
  "type": "module",
16
17
  "keywords": [
@@ -35,6 +36,7 @@
35
36
  "@ai-sdk/openai": "^3.0.47",
36
37
  "ai": "^6.0.134",
37
38
  "boxen": "^8.0.1",
38
- "chalk": "^5.4.1"
39
+ "chalk": "^5.4.1",
40
+ "zod": "^3.24.0"
39
41
  }
40
42
  }