@foxlight/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.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @foxlight/cli
2
+
3
+ Command-line interface for [Foxlight](https://github.com/josegabrielcruz/foxlight) — the open-source front-end intelligence platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @foxlight/cli
9
+ ```
10
+
11
+ Or use via npx:
12
+
13
+ ```bash
14
+ npx @foxlight/cli <command>
15
+ ```
16
+
17
+ ## Commands
18
+
19
+ ### `foxlight init`
20
+
21
+ Initialize Foxlight in your project. Creates a `.foxlight.json` configuration file.
22
+
23
+ ```bash
24
+ foxlight init
25
+ ```
26
+
27
+ ### `foxlight analyze`
28
+
29
+ Scan your project and discover components, imports, and dependencies.
30
+
31
+ ```bash
32
+ foxlight analyze
33
+ foxlight analyze --json # Output as JSON
34
+ foxlight analyze --root ./my-app # Specify project root
35
+ ```
36
+
37
+ ### `foxlight health`
38
+
39
+ Show the component health dashboard with scores for bundle size, test coverage, accessibility, freshness, and performance.
40
+
41
+ ```bash
42
+ foxlight health
43
+ foxlight health --component Button # Filter to one component
44
+ foxlight health --json # Output as JSON
45
+ ```
46
+
47
+ ### `foxlight cost`
48
+
49
+ Estimate hosting costs based on your bundle sizes across different providers.
50
+
51
+ ```bash
52
+ foxlight cost
53
+ foxlight cost --provider vercel # Specific provider
54
+ foxlight cost --json # Output as JSON
55
+ ```
56
+
57
+ ### `foxlight upgrade <package>`
58
+
59
+ Analyze the impact of upgrading a dependency.
60
+
61
+ ```bash
62
+ foxlight upgrade react
63
+ foxlight upgrade react --to 19.0.0 # Target specific version
64
+ foxlight upgrade --json # Output as JSON
65
+ ```
66
+
67
+ ## Global Options
68
+
69
+ | Option | Description |
70
+ | -------------- | ------------------------------------- |
71
+ | `--root <dir>` | Project root directory (default: `.`) |
72
+ | `--json` | Output results as JSON |
73
+ | `--help` | Show help message |
74
+ | `--version` | Show version number |
75
+
76
+ ## Configuration
77
+
78
+ Create a `.foxlight.json` in your project root:
79
+
80
+ ```json
81
+ {
82
+ "include": ["src/**/*.{ts,tsx,js,jsx,vue,svelte}"],
83
+ "exclude": ["**/*.test.*", "**/*.spec.*"],
84
+ "framework": "react"
85
+ }
86
+ ```
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,857 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { resolve as resolve4 } from "path";
5
+
6
+ // src/commands/analyze.ts
7
+ import { analyzeProject } from "@foxlight/analyzer";
8
+
9
+ // src/utils/output.ts
10
+ var COLORS = {
11
+ reset: "\x1B[0m",
12
+ bold: "\x1B[1m",
13
+ dim: "\x1B[2m",
14
+ red: "\x1B[31m",
15
+ green: "\x1B[32m",
16
+ yellow: "\x1B[33m",
17
+ blue: "\x1B[34m",
18
+ magenta: "\x1B[35m",
19
+ cyan: "\x1B[36m",
20
+ gray: "\x1B[90m"
21
+ };
22
+ function color(text, ...codes) {
23
+ const prefix = codes.map((c) => COLORS[c]).join("");
24
+ return `${prefix}${text}${COLORS.reset}`;
25
+ }
26
+ var ui = {
27
+ /** Print the Foxlight banner / header. */
28
+ banner() {
29
+ console.log("");
30
+ console.log(color(" \u{1F98A} Foxlight", "bold", "cyan"));
31
+ console.log(color(" Front-End Intelligence Platform", "dim"));
32
+ console.log("");
33
+ },
34
+ /** Print a section header. */
35
+ heading(text) {
36
+ console.log(color(`
37
+ ${text}`, "bold"));
38
+ console.log(color(" " + "\u2500".repeat(48), "dim"));
39
+ },
40
+ /** Print an info line. */
41
+ info(label, value) {
42
+ console.log(` ${color(label, "dim")} ${value}`);
43
+ },
44
+ /** Print a success message. */
45
+ success(text) {
46
+ console.log(` ${color("\u2713", "green")} ${text}`);
47
+ },
48
+ /** Print a warning message. */
49
+ warn(text) {
50
+ console.log(` ${color("\u26A0", "yellow")} ${text}`);
51
+ },
52
+ /** Print an error message. */
53
+ error(text) {
54
+ console.log(` ${color("\u2717", "red")} ${text}`);
55
+ },
56
+ /** Print a table row. */
57
+ row(columns, widths) {
58
+ const formatted = columns.map((col, i) => col.padEnd(widths[i] ?? 20));
59
+ console.log(` ${formatted.join(" ")}`);
60
+ },
61
+ /** Print a table header. */
62
+ tableHeader(columns, widths) {
63
+ const formatted = columns.map((col, i) => color(col.padEnd(widths[i] ?? 20), "dim", "bold"));
64
+ console.log(` ${formatted.join(" ")}`);
65
+ },
66
+ /** Print a component health score with color coding. */
67
+ healthScore(score) {
68
+ if (score >= 80) return color(`${score}`, "green", "bold");
69
+ if (score >= 50) return color(`${score}`, "yellow", "bold");
70
+ return color(`${score}`, "red", "bold");
71
+ },
72
+ /** Print a size delta with color. */
73
+ sizeDelta(delta) {
74
+ if (delta > 0) return color(`+${delta}`, "red");
75
+ if (delta < 0) return color(`${delta}`, "green");
76
+ return color("0", "dim");
77
+ },
78
+ /** Print a spinner/progress message. */
79
+ progress(text) {
80
+ process.stdout.write(` ${color("\u25CC", "cyan")} ${text}...`);
81
+ },
82
+ /** Clear the progress line and replace with a result. */
83
+ progressDone(text) {
84
+ process.stdout.write(`\r ${color("\u25CF", "cyan")} ${text}
85
+ `);
86
+ },
87
+ /** Blank line. */
88
+ gap() {
89
+ console.log("");
90
+ }
91
+ };
92
+
93
+ // src/commands/analyze.ts
94
+ async function runAnalyze(options) {
95
+ const { rootDir, json } = options;
96
+ ui.progress("Scanning project");
97
+ const result = await analyzeProject(rootDir);
98
+ ui.progressDone(
99
+ `Scanned ${result.stats.filesScanned} files in ${result.stats.duration.toFixed(0)}ms`
100
+ );
101
+ if (json) {
102
+ const snapshot = result.registry.createSnapshot("local", "local");
103
+ console.log(JSON.stringify(snapshot, null, 2));
104
+ return;
105
+ }
106
+ ui.heading("Analysis Results");
107
+ ui.info("Files scanned:", String(result.stats.filesScanned));
108
+ ui.info("Components found:", String(result.stats.componentsFound));
109
+ ui.info("Imports tracked:", String(result.stats.importsTracked));
110
+ ui.info("Framework:", result.config.framework ?? "auto-detected");
111
+ const components = result.registry.getAllComponents();
112
+ if (components.length > 0) {
113
+ ui.heading("Components");
114
+ const widths = [30, 12, 10, 8];
115
+ ui.tableHeader(["Name", "Framework", "Props", "Children"], widths);
116
+ for (const comp of components) {
117
+ ui.row(
118
+ [comp.name, comp.framework, String(comp.props.length), String(comp.children.length)],
119
+ widths
120
+ );
121
+ }
122
+ }
123
+ const cycles = result.graph.detectCycles();
124
+ if (cycles.length > 0) {
125
+ ui.heading("Circular Dependencies");
126
+ ui.warn(`Found ${cycles.length} circular dependency chain(s)`);
127
+ for (const cycle of cycles.slice(0, 5)) {
128
+ ui.info(" Cycle:", cycle.join(" \u2192 "));
129
+ }
130
+ }
131
+ const roots = result.registry.getRootComponents();
132
+ if (roots.length > 0) {
133
+ ui.heading("Top-Level Components (no parents)");
134
+ for (const root of roots) {
135
+ const subtree = result.registry.getSubtree(root.id);
136
+ ui.info(` ${root.name}`, `(${subtree.length} components in subtree)`);
137
+ }
138
+ }
139
+ ui.gap();
140
+ }
141
+
142
+ // src/commands/health.ts
143
+ import { analyzeProject as analyzeProject2 } from "@foxlight/analyzer";
144
+ import { computeComponentHealth } from "@foxlight/core";
145
+ async function runHealth(options) {
146
+ const { rootDir, json, component } = options;
147
+ ui.progress("Analyzing project health");
148
+ const result = await analyzeProject2(rootDir);
149
+ ui.progressDone("Analysis complete");
150
+ const components = result.registry.getAllComponents();
151
+ if (components.length === 0) {
152
+ ui.warn("No components found. Run `foxlight analyze` to check your config.");
153
+ return;
154
+ }
155
+ const healthResults = components.map((comp) => {
156
+ const bundleInfo = result.registry.getBundleInfo(comp.id);
157
+ const input = {
158
+ component: comp,
159
+ bundleInfo: bundleInfo ?? void 0
160
+ // Test coverage, accessibility, freshness, performance, and reliability
161
+ // require external data sources. They will show as "not available"
162
+ // until integrations are configured.
163
+ };
164
+ return computeComponentHealth(input);
165
+ });
166
+ if (json) {
167
+ console.log(JSON.stringify(healthResults, null, 2));
168
+ return;
169
+ }
170
+ const display = component ? healthResults.filter((h) => {
171
+ const comp = result.registry.getComponent(h.componentId);
172
+ return comp?.name.toLowerCase() === component.toLowerCase() || h.componentId.toLowerCase().includes(component.toLowerCase());
173
+ }) : healthResults;
174
+ if (display.length === 0) {
175
+ ui.error(`Component "${component}" not found.`);
176
+ return;
177
+ }
178
+ ui.heading("Component Health Dashboard");
179
+ const widths = [22, 8, 10, 10, 10, 10, 10];
180
+ ui.tableHeader(["Component", "Score", "Bundle", "Tests", "A11y", "Fresh", "Perf"], widths);
181
+ const sorted = [...display].sort((a, b) => a.score - b.score);
182
+ for (const h of sorted) {
183
+ const comp = result.registry.getComponent(h.componentId);
184
+ const name = comp?.name ?? h.componentId;
185
+ ui.row(
186
+ [
187
+ name,
188
+ ui.healthScore(h.score),
189
+ ui.healthScore(h.metrics.bundleSize.score),
190
+ ui.healthScore(h.metrics.testCoverage.score),
191
+ ui.healthScore(h.metrics.accessibility.score),
192
+ ui.healthScore(h.metrics.freshness.score),
193
+ ui.healthScore(h.metrics.performance.score)
194
+ ],
195
+ widths
196
+ );
197
+ }
198
+ const avgScore = display.reduce((sum, h) => sum + h.score, 0) / display.length;
199
+ const critical = display.filter((h) => h.score < 50).length;
200
+ const warning = display.filter((h) => h.score >= 50 && h.score < 80).length;
201
+ const healthy = display.filter((h) => h.score >= 80).length;
202
+ ui.heading("Summary");
203
+ ui.info("Average health score:", ui.healthScore(Math.round(avgScore)));
204
+ ui.success(`${healthy} healthy`);
205
+ if (warning > 0) ui.warn(`${warning} need attention`);
206
+ if (critical > 0) ui.error(`${critical} critical`);
207
+ ui.gap();
208
+ ui.info("Tip:", "Integrate test coverage, accessibility, and performance data for full scoring.");
209
+ ui.gap();
210
+ }
211
+
212
+ // src/commands/init.ts
213
+ import { existsSync } from "fs";
214
+ import { writeFile } from "fs/promises";
215
+ import { join } from "path";
216
+ import { detectFramework } from "@foxlight/core";
217
+ async function runInit(options) {
218
+ const { rootDir } = options;
219
+ const configPath = join(rootDir, "foxlight.config.ts");
220
+ if (existsSync(configPath)) {
221
+ ui.warn("foxlight.config.ts already exists. Skipping initialization.");
222
+ return;
223
+ }
224
+ ui.progress("Detecting project settings");
225
+ const framework = await detectFramework(rootDir);
226
+ ui.progressDone(`Detected framework: ${framework}`);
227
+ const configContent = generateConfig(framework);
228
+ await writeFile(configPath, configContent, "utf-8");
229
+ ui.success(`Created ${configPath}`);
230
+ ui.gap();
231
+ ui.info("Next steps:", "");
232
+ ui.info(" 1.", "Review foxlight.config.ts and adjust settings");
233
+ ui.info(" 2.", "Run `foxlight analyze` to scan your project");
234
+ ui.info(" 3.", "Run `foxlight health` to see component health scores");
235
+ ui.gap();
236
+ }
237
+ function generateConfig(framework) {
238
+ return `import type { FoxlightConfig } from "@foxlight/core";
239
+
240
+ const config: FoxlightConfig = {
241
+ rootDir: ".",
242
+
243
+ // Source files to analyze
244
+ include: [
245
+ "src/**/*.{tsx,jsx,vue,svelte}",
246
+ "components/**/*.{tsx,jsx,vue,svelte}",
247
+ "app/**/*.{tsx,jsx,vue,svelte}",
248
+ ],
249
+
250
+ // Files to exclude
251
+ exclude: [
252
+ "**/node_modules/**",
253
+ "**/dist/**",
254
+ "**/*.test.*",
255
+ "**/*.spec.*",
256
+ "**/*.stories.*",
257
+ ],
258
+
259
+ // Auto-detected framework: "${framework}"
260
+ // Uncomment to override:
261
+ // framework: "${framework}",
262
+
263
+ // Cost model (uncomment and configure for cost analysis)
264
+ // costModel: {
265
+ // provider: "vercel",
266
+ // invocationCostPer1M: 0.60,
267
+ // bandwidthCostPerGB: 0.15,
268
+ // storageCostPerGB: 0.023,
269
+ // baseCost: 0,
270
+ // },
271
+
272
+ // Baseline storage for visual regression (uncomment to configure)
273
+ // baselines: {
274
+ // provider: "s3",
275
+ // bucket: "my-foxlight-baselines",
276
+ // prefix: "visual",
277
+ // },
278
+ };
279
+
280
+ export default config;
281
+ `;
282
+ }
283
+
284
+ // src/commands/cost.ts
285
+ import { analyzeProject as analyzeProject3 } from "@foxlight/analyzer";
286
+ import {
287
+ estimateMonthlyCost,
288
+ COST_MODELS,
289
+ DEFAULT_TRAFFIC
290
+ } from "@foxlight/core";
291
+ import { formatBytes } from "@foxlight/bundle";
292
+ async function runCost(options) {
293
+ const { rootDir, json, provider, pageViews } = options;
294
+ ui.progress("Analyzing project for cost estimation");
295
+ const result = await analyzeProject3(rootDir);
296
+ ui.progressDone("Analysis complete");
297
+ const components = result.registry.getAllComponents();
298
+ if (components.length === 0) {
299
+ ui.warn("No components found. Run `foxlight analyze` to check your config.");
300
+ return;
301
+ }
302
+ const bundleInfos = components.map((comp) => result.registry.getBundleInfo(comp.id)).filter((info) => info !== void 0);
303
+ const hasBundleData = bundleInfos.length > 0;
304
+ const traffic = {
305
+ ...DEFAULT_TRAFFIC,
306
+ ...pageViews ? { monthlyPageViews: pageViews } : {}
307
+ };
308
+ const providers = provider ? { [provider]: COST_MODELS[provider] } : COST_MODELS;
309
+ if (provider && !COST_MODELS[provider]) {
310
+ ui.error(`Unknown provider: ${provider}`);
311
+ ui.info("Available providers:", Object.keys(COST_MODELS).join(", "));
312
+ return;
313
+ }
314
+ const estimates = {};
315
+ for (const [name, model] of Object.entries(providers)) {
316
+ if (!model) continue;
317
+ estimates[name] = estimateMonthlyCost(bundleInfos, model, traffic);
318
+ }
319
+ if (json) {
320
+ console.log(
321
+ JSON.stringify(
322
+ {
323
+ components: components.length,
324
+ hasBundleData,
325
+ traffic,
326
+ estimates
327
+ },
328
+ null,
329
+ 2
330
+ )
331
+ );
332
+ return;
333
+ }
334
+ ui.heading("Cost Estimation");
335
+ ui.info("Components:", String(components.length));
336
+ ui.info(
337
+ "Bundle data:",
338
+ hasBundleData ? "available" : "not yet available \u2014 run a build with the Foxlight plugin first"
339
+ );
340
+ ui.info("Traffic assumption:", `${formatNumber(traffic.monthlyPageViews)} page views/month`);
341
+ if (bundleInfos.length > 0) {
342
+ const totalRaw = bundleInfos.reduce((sum, b) => sum + b.selfSize.raw, 0);
343
+ const totalGzip = bundleInfos.reduce((sum, b) => sum + b.selfSize.gzip, 0);
344
+ ui.info("Total bundle size:", `${formatBytes(totalRaw)} (${formatBytes(totalGzip)} gzip)`);
345
+ }
346
+ ui.heading("Estimated Monthly Cost by Provider");
347
+ const widths = [16, 14];
348
+ ui.tableHeader(["Provider", "Monthly Cost"], widths);
349
+ for (const [name, cost] of Object.entries(estimates)) {
350
+ ui.row([capitalize(name), formatCurrency(cost)], widths);
351
+ }
352
+ ui.gap();
353
+ ui.info(
354
+ "Note:",
355
+ "Estimates are based on bundle size and traffic assumptions. Actual costs vary."
356
+ );
357
+ ui.gap();
358
+ }
359
+ function formatCurrency(amount) {
360
+ if (amount < 0.01) return "< $0.01";
361
+ return `$${amount.toFixed(2)}`;
362
+ }
363
+ function formatNumber(n) {
364
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
365
+ if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
366
+ return String(Math.round(n));
367
+ }
368
+ function capitalize(s) {
369
+ return s.charAt(0).toUpperCase() + s.slice(1);
370
+ }
371
+
372
+ // src/commands/upgrade.ts
373
+ import { analyzeProject as analyzeProject4 } from "@foxlight/analyzer";
374
+ import { analyzeUpgrade } from "@foxlight/core";
375
+ async function runUpgrade(options) {
376
+ const { rootDir, packageName, targetVersion, json } = options;
377
+ ui.progress(`Analyzing upgrade impact for ${packageName}`);
378
+ const result = await analyzeProject4(rootDir);
379
+ ui.progressDone("Analysis complete");
380
+ const components = result.registry.getAllComponents();
381
+ const affectedComponents = components.filter((c) => c.dependencies.includes(packageName));
382
+ ui.progress(`Checking ${packageName} upgrade`);
383
+ const preview = await analyzeUpgrade({
384
+ rootDir,
385
+ packageName,
386
+ targetVersion,
387
+ affectedComponents
388
+ });
389
+ ui.progressDone("Upgrade analysis complete");
390
+ if (json) {
391
+ console.log(JSON.stringify(preview, null, 2));
392
+ return;
393
+ }
394
+ ui.heading(`Upgrade Preview: ${packageName}`);
395
+ ui.info("Current version:", preview.fromVersion);
396
+ ui.info("Target version:", preview.toVersion);
397
+ ui.info("Risk level:", formatRisk(preview.risk));
398
+ ui.heading("Checks");
399
+ for (const check of preview.checks) {
400
+ const icon = check.status === "pass" ? "\u2713" : check.status === "warn" ? "\u26A0" : "\u2717";
401
+ const colorFn = check.status === "pass" ? ui.success : check.status === "warn" ? ui.warn : ui.error;
402
+ colorFn(`${icon} ${check.name}: ${check.summary}`);
403
+ if (check.details) {
404
+ ui.info(" ", check.details);
405
+ }
406
+ }
407
+ if (affectedComponents.length > 0) {
408
+ ui.heading("Affected Components");
409
+ for (const comp of affectedComponents.slice(0, 20)) {
410
+ ui.info(" \u2022", comp.name);
411
+ }
412
+ if (affectedComponents.length > 20) {
413
+ ui.info(" ", `...and ${affectedComponents.length - 20} more`);
414
+ }
415
+ }
416
+ ui.gap();
417
+ }
418
+ function formatRisk(risk) {
419
+ switch (risk) {
420
+ case "low":
421
+ return "\u{1F7E2} Low";
422
+ case "medium":
423
+ return "\u{1F7E1} Medium";
424
+ case "high":
425
+ return "\u{1F534} High";
426
+ }
427
+ }
428
+
429
+ // src/commands/ci.ts
430
+ import {
431
+ compareSnapshots,
432
+ hasSignificantChanges,
433
+ postPRComment,
434
+ createCheckRun,
435
+ detectGitHubEnv,
436
+ postMRComment,
437
+ detectGitLabEnv
438
+ } from "@foxlight/ci";
439
+ async function runCI(options) {
440
+ const { rootDir, json } = options;
441
+ const basePath = options.basePath ?? ".foxlight/snapshot.json";
442
+ const outputPath = options.outputPath ?? ".foxlight/snapshot.json";
443
+ const commitSha = process.env["GITHUB_SHA"] ?? process.env["CI_COMMIT_SHA"] ?? "local";
444
+ const branch = process.env["GITHUB_HEAD_REF"] ?? process.env["CI_COMMIT_BRANCH"] ?? process.env["CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"] ?? "unknown";
445
+ ui.progress("Analyzing project");
446
+ const comparison = await compareSnapshots({
447
+ rootDir,
448
+ basePath,
449
+ outputPath,
450
+ commitSha,
451
+ branch
452
+ });
453
+ ui.progressDone("Analysis complete");
454
+ const { diff, head } = comparison;
455
+ const significant = hasSignificantChanges(diff);
456
+ if (json) {
457
+ console.log(JSON.stringify({ diff, significant }, null, 2));
458
+ return;
459
+ }
460
+ ui.heading("CI Analysis Summary");
461
+ ui.info("Commit:", commitSha.slice(0, 8));
462
+ ui.info("Branch:", branch);
463
+ ui.info("Components:", String(head.components.length));
464
+ ui.info("Significant changes:", significant ? "yes" : "no");
465
+ if (diff.components.added.length > 0) {
466
+ ui.success(`${diff.components.added.length} component(s) added`);
467
+ }
468
+ if (diff.components.removed.length > 0) {
469
+ ui.warn(`${diff.components.removed.length} component(s) removed`);
470
+ }
471
+ if (diff.components.modified.length > 0) {
472
+ ui.info("Modified:", `${diff.components.modified.length} component(s)`);
473
+ }
474
+ const githubEnv = detectGitHubEnv();
475
+ if (githubEnv.token && githubEnv.owner && githubEnv.repo && githubEnv.prNumber) {
476
+ ui.progress("Posting GitHub PR comment");
477
+ try {
478
+ const config = githubEnv;
479
+ await postPRComment(config, diff);
480
+ ui.progressDone("PR comment posted");
481
+ await createCheckRun(config, diff, {
482
+ name: "Foxlight Analysis",
483
+ headSha: commitSha
484
+ });
485
+ ui.success("Check run created");
486
+ } catch (err) {
487
+ ui.warn(`GitHub integration failed: ${err instanceof Error ? err.message : String(err)}`);
488
+ }
489
+ }
490
+ const gitlabEnv = detectGitLabEnv();
491
+ const hasGitLab = !!(gitlabEnv.token && gitlabEnv.projectId && gitlabEnv.mergeRequestIid);
492
+ if (hasGitLab) {
493
+ ui.progress("Posting GitLab MR note");
494
+ try {
495
+ await postMRComment(gitlabEnv, diff);
496
+ ui.progressDone("MR note posted");
497
+ } catch (err) {
498
+ ui.warn(`GitLab integration failed: ${err instanceof Error ? err.message : String(err)}`);
499
+ }
500
+ }
501
+ if (!githubEnv.token && !hasGitLab) {
502
+ ui.info("CI platform:", "not detected (running locally)");
503
+ ui.info("Snapshot saved to:", outputPath);
504
+ }
505
+ ui.gap();
506
+ }
507
+
508
+ // src/commands/coverage.ts
509
+ import { resolve } from "path";
510
+ import {
511
+ loadCoverageData,
512
+ mapCoverageToComponents,
513
+ findUncoveredComponents,
514
+ findLowCoverageComponents,
515
+ summarizeCoverage
516
+ } from "@foxlight/core";
517
+ async function runCoverage(options) {
518
+ const projectRoot = resolve(options.root || ".");
519
+ const coverageData = await loadCoverageData(projectRoot, options.coveragePath);
520
+ if (Object.keys(coverageData).length === 0) {
521
+ console.log("\u274C No coverage data found.");
522
+ console.log("Make sure you have generated coverage with a tool like:");
523
+ console.log(" npm test -- --coverage (Jest/Vitest)");
524
+ console.log(" npx nyc npm test");
525
+ process.exit(1);
526
+ }
527
+ const componentFilePaths = new Set(Object.keys(coverageData));
528
+ const coverage = mapCoverageToComponents(coverageData, componentFilePaths);
529
+ if (options.json) {
530
+ const output = {
531
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
532
+ overall: coverage.overallPercentage,
533
+ components: Array.from(coverage.components.values()).map((c) => ({
534
+ filePath: c.filePath,
535
+ coverage: c.percentage,
536
+ statements: {
537
+ covered: c.statementsCovered,
538
+ total: c.statementsTotal
539
+ },
540
+ functions: {
541
+ covered: c.functionsCovered,
542
+ total: c.functionsTotal
543
+ }
544
+ }))
545
+ };
546
+ console.log(JSON.stringify(output, null, 2));
547
+ return;
548
+ }
549
+ console.log("\n\u{1F4CA} Test Coverage Report\n");
550
+ console.log(summarizeCoverage(coverage));
551
+ const uncovered = findUncoveredComponents(coverage);
552
+ if (uncovered.length > 0) {
553
+ console.log(`
554
+ \u274C Uncovered Components (${uncovered.length}):`);
555
+ for (const comp of uncovered.slice(0, 10)) {
556
+ console.log(` - ${comp.filePath}`);
557
+ }
558
+ if (uncovered.length > 10) {
559
+ console.log(` ... and ${uncovered.length - 10} more`);
560
+ }
561
+ }
562
+ const threshold = options.threshold || 50;
563
+ const lowCoverage = findLowCoverageComponents(coverage, threshold);
564
+ if (lowCoverage.length > 0) {
565
+ console.log(`
566
+ \u26A0\uFE0F Low Coverage (<${threshold}%) (${lowCoverage.length}):`);
567
+ for (const comp of lowCoverage.slice(0, 15)) {
568
+ console.log(` ${comp.filePath}: ${comp.percentage}%`);
569
+ }
570
+ if (lowCoverage.length > 15) {
571
+ console.log(` ... and ${lowCoverage.length - 15} more
572
+ `);
573
+ }
574
+ }
575
+ if (coverage.overallPercentage < threshold) {
576
+ console.log(`
577
+ \u274C Coverage ${coverage.overallPercentage}% is below threshold ${threshold}%`);
578
+ process.exit(1);
579
+ }
580
+ console.log("");
581
+ }
582
+
583
+ // src/commands/dead-code.ts
584
+ import { resolve as resolve2 } from "path";
585
+ import { detectDeadCode, formatDeadCodeReport } from "@foxlight/core";
586
+ import { analyzeProject as analyzeProject5 } from "@foxlight/analyzer";
587
+ async function runDeadCodeDetection(options) {
588
+ const projectRoot = resolve2(options.root || ".");
589
+ console.log("\u{1F50D} Analyzing codebase for dead code...\n");
590
+ const analysis = await analyzeProject5(projectRoot);
591
+ const report = detectDeadCode(analysis.registry);
592
+ if (options.json) {
593
+ const output = {
594
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
595
+ summary: {
596
+ unusedComponents: report.unusedComponents.length,
597
+ orphanedComponents: report.orphanedComponents.length,
598
+ unusedExports: report.unusedExports.length,
599
+ potentialSavingsBytes: report.totalPotentialBytes
600
+ },
601
+ unusedComponents: report.unusedComponents.map((c) => ({
602
+ id: c.id,
603
+ name: c.name,
604
+ filePath: c.filePath,
605
+ reason: c.reason,
606
+ potentialSavings: c.potentialSavings
607
+ })),
608
+ orphanedComponents: report.orphanedComponents.map((c) => ({
609
+ id: c.id,
610
+ name: c.name,
611
+ filePath: c.filePath
612
+ })),
613
+ unusedExports: report.unusedExports.map((e) => ({
614
+ filePath: e.filePath,
615
+ exportName: e.exportName,
616
+ reason: e.reason
617
+ }))
618
+ };
619
+ console.log(JSON.stringify(output, null, 2));
620
+ return;
621
+ }
622
+ console.log(formatDeadCodeReport(report));
623
+ if (report.unusedComponents.length > 0) {
624
+ console.log("\n\u{1F4A1} Tip: These components can likely be safely removed.");
625
+ console.log("Run with --json for detailed analysis.");
626
+ }
627
+ const totalIssues = report.unusedComponents.length + report.orphanedComponents.length + report.unusedExports.length;
628
+ if (totalIssues === 0) {
629
+ console.log("\n\u2705 No dead code detected!");
630
+ } else {
631
+ console.log(`
632
+ \u26A0\uFE0F Found ${totalIssues} potential issues to address.
633
+ `);
634
+ }
635
+ }
636
+
637
+ // src/commands/api-check.ts
638
+ import { resolve as resolve3 } from "path";
639
+ import { existsSync as existsSync2, readFileSync } from "fs";
640
+ import { writeFile as writeFile2 } from "fs/promises";
641
+ import { join as join2 } from "path";
642
+ import {
643
+ createAPISnapshot,
644
+ snapshotToJSON,
645
+ snapshotFromJSON,
646
+ compareSnapshots as compareSnapshots2,
647
+ formatAPIChangeSummary
648
+ } from "@foxlight/core";
649
+ import { analyzeProject as analyzeProject6 } from "@foxlight/analyzer";
650
+ var DEFAULT_SNAPSHOT_PATH = ".foxlight/api-baseline.json";
651
+ async function runAPICheck(options) {
652
+ const projectRoot = resolve3(options.root || ".");
653
+ const snapshotPath = options.baseline || join2(projectRoot, DEFAULT_SNAPSHOT_PATH);
654
+ console.log("\u{1F50D} Checking for API breaking changes...\n");
655
+ const analysis = await analyzeProject6(projectRoot);
656
+ const currentComponents = analysis.registry.getAllComponents();
657
+ const currentSnapshot = createAPISnapshot(currentComponents);
658
+ let baselineSnapshot;
659
+ if (existsSync2(snapshotPath)) {
660
+ try {
661
+ const baselineJson = readFileSync(snapshotPath, "utf-8");
662
+ baselineSnapshot = snapshotFromJSON(baselineJson);
663
+ } catch {
664
+ console.log(`\u26A0\uFE0F Could not load baseline snapshot from ${snapshotPath}`);
665
+ baselineSnapshot = null;
666
+ }
667
+ }
668
+ if (!baselineSnapshot) {
669
+ if (options.save) {
670
+ console.log("\u{1F4F8} Creating initial API baseline...");
671
+ await writeFile2(snapshotPath, snapshotToJSON(currentSnapshot));
672
+ console.log(`\u2705 API baseline saved to ${snapshotPath}
673
+ `);
674
+ return;
675
+ } else {
676
+ console.log("\u2139\uFE0F No baseline found. Run with --save to create one.");
677
+ console.log(`Expected baseline at: ${snapshotPath}
678
+ `);
679
+ return;
680
+ }
681
+ }
682
+ const summary = compareSnapshots2(baselineSnapshot, currentSnapshot);
683
+ if (options.json) {
684
+ const output = {
685
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
686
+ breakingChanges: summary.breaking.map((c) => ({
687
+ component: c.componentName,
688
+ type: c.changeType,
689
+ severity: c.severity,
690
+ description: c.description
691
+ })),
692
+ addedComponents: summary.addedComponents.map((c) => c.name),
693
+ removedComponents: summary.removedComponents.map((c) => c.name),
694
+ totalBreakingChanges: summary.breaking.length
695
+ };
696
+ console.log(JSON.stringify(output, null, 2));
697
+ if (summary.breaking.length > 0) {
698
+ process.exit(1);
699
+ }
700
+ return;
701
+ }
702
+ console.log(formatAPIChangeSummary(summary));
703
+ if (options.save) {
704
+ console.log("\n\u{1F4F8} Updating API baseline...");
705
+ await writeFile2(snapshotPath, snapshotToJSON(currentSnapshot));
706
+ console.log(`\u2705 API baseline updated
707
+ `);
708
+ }
709
+ if (summary.breaking.length > 0) {
710
+ console.log(
711
+ `
712
+ \u274C Found ${summary.breaking.length} breaking change(s). Please review before merging.
713
+ `
714
+ );
715
+ process.exit(1);
716
+ } else {
717
+ console.log("\n\u2705 No breaking changes detected.\n");
718
+ }
719
+ }
720
+
721
+ // src/index.ts
722
+ async function main() {
723
+ const args = process.argv.slice(2);
724
+ const command = args[0];
725
+ const flags = /* @__PURE__ */ new Map();
726
+ for (let i = 1; i < args.length; i++) {
727
+ const arg = args[i];
728
+ if (arg.startsWith("--")) {
729
+ const key = arg.slice(2);
730
+ const nextArg = args[i + 1];
731
+ const value = nextArg && !nextArg.startsWith("--") ? nextArg : "true";
732
+ flags.set(key, value);
733
+ if (value !== "true") i++;
734
+ }
735
+ }
736
+ const rootDir = resolve4(flags.get("root") ?? flags.get("dir") ?? ".");
737
+ const json = flags.has("json");
738
+ ui.banner();
739
+ switch (command) {
740
+ case "init":
741
+ await runInit({ rootDir });
742
+ break;
743
+ case "analyze":
744
+ case "scan":
745
+ await runAnalyze({ rootDir, json });
746
+ break;
747
+ case "health":
748
+ case "dashboard":
749
+ await runHealth({
750
+ rootDir,
751
+ json,
752
+ component: flags.get("component") ?? flags.get("c")
753
+ });
754
+ break;
755
+ case "cost":
756
+ await runCost({
757
+ rootDir,
758
+ json,
759
+ provider: flags.get("provider") ?? flags.get("p"),
760
+ pageViews: flags.has("page-views") ? parseInt(flags.get("page-views"), 10) : void 0
761
+ });
762
+ break;
763
+ case "upgrade": {
764
+ const packageName = args[1];
765
+ if (!packageName || packageName.startsWith("--")) {
766
+ ui.error("Please specify a package name: foxlight upgrade <package>");
767
+ process.exitCode = 1;
768
+ break;
769
+ }
770
+ await runUpgrade({
771
+ rootDir,
772
+ json,
773
+ packageName,
774
+ targetVersion: flags.get("to") ?? flags.get("target")
775
+ });
776
+ break;
777
+ }
778
+ case "ci":
779
+ await runCI({
780
+ rootDir,
781
+ json,
782
+ basePath: flags.get("base"),
783
+ outputPath: flags.get("output")
784
+ });
785
+ break;
786
+ case "coverage":
787
+ await runCoverage({
788
+ root: rootDir,
789
+ json,
790
+ threshold: flags.has("threshold") ? parseInt(flags.get("threshold"), 10) : void 0,
791
+ coveragePath: flags.get("coverage")
792
+ });
793
+ break;
794
+ case "dead-code":
795
+ await runDeadCodeDetection({
796
+ root: rootDir,
797
+ json,
798
+ threshold: flags.has("threshold") ? parseInt(flags.get("threshold"), 10) : void 0
799
+ });
800
+ break;
801
+ case "api-check":
802
+ await runAPICheck({
803
+ root: rootDir,
804
+ json,
805
+ save: flags.has("save"),
806
+ baseline: flags.get("baseline")
807
+ });
808
+ break;
809
+ case "help":
810
+ case "--help":
811
+ case "-h":
812
+ case void 0:
813
+ printHelp();
814
+ break;
815
+ case "version":
816
+ case "--version":
817
+ case "-v":
818
+ console.log(" foxlight v0.1.0");
819
+ break;
820
+ default:
821
+ ui.error(`Unknown command: ${command}`);
822
+ ui.gap();
823
+ printHelp();
824
+ process.exitCode = 1;
825
+ }
826
+ }
827
+ function printHelp() {
828
+ console.log(" Usage: foxlight <command> [options]");
829
+ console.log("");
830
+ console.log(" Commands:");
831
+ console.log(" init Initialize Foxlight in your project");
832
+ console.log(" analyze Scan project and discover components");
833
+ console.log(" health Show component health dashboard");
834
+ console.log(" cost Estimate hosting costs by provider");
835
+ console.log(" upgrade <pkg> Analyze dependency upgrade impact");
836
+ console.log(" coverage Show test coverage by component");
837
+ console.log(" dead-code Find unused components and exports");
838
+ console.log(" api-check Detect breaking changes in component APIs");
839
+ console.log(" ci Run CI analysis and post results");
840
+ console.log("");
841
+ console.log(" Options:");
842
+ console.log(" --root <dir> Project root directory (default: .)");
843
+ console.log(" --json Output results as JSON");
844
+ console.log(" --component <name> Filter health to a specific component");
845
+ console.log(" --provider <name> Cost provider (vercel, netlify, aws, cloudflare)");
846
+ console.log(" --to <version> Target version for upgrade command");
847
+ console.log(" --threshold <num> Coverage/dead-code threshold");
848
+ console.log(" --coverage <path> Path to coverage JSON file");
849
+ console.log(" --save Save current state as baseline (api-check)");
850
+ console.log(" --help Show this help message");
851
+ console.log(" --version Show version number");
852
+ console.log("");
853
+ }
854
+ main().catch((error) => {
855
+ ui.error(error instanceof Error ? error.message : String(error));
856
+ process.exitCode = 1;
857
+ });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@foxlight/cli",
3
+ "version": "0.1.0",
4
+ "description": "Command-line interface for Foxlight — analyze components, check health, run upgrades.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "foxlight": "./dist/index.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "dev": "tsup --watch"
20
+ },
21
+ "dependencies": {
22
+ "@foxlight/core": "*",
23
+ "@foxlight/analyzer": "*",
24
+ "@foxlight/bundle": "*",
25
+ "@foxlight/ci": "*"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "license": "MIT",
31
+ "author": "Jose Cruz",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/josegabrielcruz/foxlight.git",
35
+ "directory": "packages/cli"
36
+ },
37
+ "homepage": "https://github.com/josegabrielcruz/foxlight/tree/master/packages/cli#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/josegabrielcruz/foxlight/issues"
40
+ },
41
+ "keywords": [
42
+ "foxlight",
43
+ "cli",
44
+ "component-health",
45
+ "frontend-tools"
46
+ ]
47
+ }