frontend-guardian 0.1.15 → 0.1.17
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 +4 -1
- package/dist/cli.js +67 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ npx frontend-guardian . # scan current dir
|
|
|
9
9
|
npx frontend-guardian ./src # scan ./src
|
|
10
10
|
npx frontend-guardian . --fail-on-score 70 # exit 1 if score < 70 (for CI)
|
|
11
11
|
npx frontend-guardian . --json # output JSON
|
|
12
|
+
npx frontend-guardian . --suggest # AI fix suggestions (hosted API or GEMINI_API_KEY)
|
|
12
13
|
npx frontend-guardian init # add .github/workflows/frontend-guardian.yml to repo
|
|
13
14
|
```
|
|
14
15
|
|
|
@@ -23,10 +24,12 @@ node packages/cli/dist/cli.js .
|
|
|
23
24
|
|
|
24
25
|
## Publish to npm
|
|
25
26
|
|
|
27
|
+
From repo root:
|
|
28
|
+
|
|
26
29
|
```bash
|
|
27
30
|
pnpm run build:packages
|
|
28
31
|
cd packages/core && npm publish --access public
|
|
29
|
-
cd
|
|
32
|
+
cd packages/cli && npm publish --access public
|
|
30
33
|
```
|
|
31
34
|
|
|
32
35
|
## Output
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as path from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import ora from "ora";
|
|
7
|
+
import { GoogleGenAI } from "@google/genai";
|
|
7
8
|
import { scanFiles, scanZip } from "@justinmoto/frontend-guardian-core";
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const EXT = [".js", ".jsx", ".ts", ".tsx", ".css"];
|
|
@@ -321,14 +322,17 @@ function parseArgs() {
|
|
|
321
322
|
const argv = process.argv.slice(2);
|
|
322
323
|
const pathArg = argv[0];
|
|
323
324
|
if (!pathArg || pathArg.startsWith("-")) {
|
|
324
|
-
return { path: "", json: false, failOnScore: null };
|
|
325
|
+
return { path: "", json: false, failOnScore: null, suggest: false };
|
|
325
326
|
}
|
|
326
327
|
let json = false;
|
|
327
328
|
let failOnScore = null;
|
|
329
|
+
let suggest = false;
|
|
328
330
|
for (let i = 1; i < argv.length; i++) {
|
|
329
331
|
const a = argv[i];
|
|
330
332
|
if (a === "--json")
|
|
331
333
|
json = true;
|
|
334
|
+
else if (a === "--suggest")
|
|
335
|
+
suggest = true;
|
|
332
336
|
else if (a === "--fail-on-score" && argv[i + 1] != null) {
|
|
333
337
|
const n = parseInt(argv[i + 1], 10);
|
|
334
338
|
if (!Number.isNaN(n) && n >= 0 && n <= 100)
|
|
@@ -341,7 +345,29 @@ function parseArgs() {
|
|
|
341
345
|
failOnScore = n;
|
|
342
346
|
}
|
|
343
347
|
}
|
|
344
|
-
return { path: pathArg, json, failOnScore };
|
|
348
|
+
return { path: pathArg, json, failOnScore, suggest };
|
|
349
|
+
}
|
|
350
|
+
const DEFAULT_SUGGEST_API = "https://frontend-guardian.vercel.app/api/suggest";
|
|
351
|
+
async function getGeminiSuggestion(apiKey, issueTitle, detected) {
|
|
352
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
353
|
+
const prompt = `Frontend Guardian found this issue: "${issueTitle}". Detected: ${detected.slice(0, 500)}. Give a brief concrete fix suggestion in 1-2 sentences. No preamble.`;
|
|
354
|
+
const res = await ai.models.generateContent({ model: "gemini-1.5-flash", contents: prompt });
|
|
355
|
+
const text = res.text;
|
|
356
|
+
return (text ?? "").trim();
|
|
357
|
+
}
|
|
358
|
+
async function getSuggestFromBackend(issueTitle, detected) {
|
|
359
|
+
const url = process.env.FRONTEND_GUARDIAN_SUGGEST_URL || DEFAULT_SUGGEST_API;
|
|
360
|
+
const res = await fetch(url, {
|
|
361
|
+
method: "POST",
|
|
362
|
+
headers: { "Content-Type": "application/json" },
|
|
363
|
+
body: JSON.stringify({ issueTitle, detected: detected.slice(0, 500) }),
|
|
364
|
+
});
|
|
365
|
+
if (!res.ok) {
|
|
366
|
+
const err = await res.json().catch(() => ({}));
|
|
367
|
+
throw new Error(err?.error || `API ${res.status}`);
|
|
368
|
+
}
|
|
369
|
+
const data = (await res.json());
|
|
370
|
+
return (data.suggestion ?? "").trim();
|
|
345
371
|
}
|
|
346
372
|
async function main() {
|
|
347
373
|
const argv = process.argv.slice(2);
|
|
@@ -349,14 +375,15 @@ async function main() {
|
|
|
349
375
|
initWorkflow(process.cwd());
|
|
350
376
|
return;
|
|
351
377
|
}
|
|
352
|
-
const { path: pathArg, json: outputJson, failOnScore } = parseArgs();
|
|
378
|
+
const { path: pathArg, json: outputJson, failOnScore, suggest: wantSuggest } = parseArgs();
|
|
353
379
|
if (!pathArg) {
|
|
354
380
|
console.error("Usage: frontend-guardian <path> [options]");
|
|
355
381
|
console.error(" frontend-guardian init # add GitHub Actions workflow");
|
|
356
382
|
console.error(" npx frontend-guardian .");
|
|
357
383
|
console.error(" npx frontend-guardian . --fail-on-score 70");
|
|
358
384
|
console.error(" npx frontend-guardian . --json");
|
|
359
|
-
console.error("
|
|
385
|
+
console.error(" npx frontend-guardian . --suggest # AI fix suggestions (uses hosted API or your GEMINI_API_KEY)");
|
|
386
|
+
console.error("Options: --json Output as JSON. --fail-on-score <0-100> Exit 1 if below. --suggest Gemini AI suggestions.");
|
|
360
387
|
process.exit(1);
|
|
361
388
|
}
|
|
362
389
|
const resolved = path.resolve(process.cwd(), pathArg);
|
|
@@ -414,6 +441,42 @@ async function main() {
|
|
|
414
441
|
else {
|
|
415
442
|
printResult(projectName, result);
|
|
416
443
|
}
|
|
444
|
+
if (wantSuggest && result.warnings.length + result.duplicates.length > 0) {
|
|
445
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
446
|
+
const byTitle = new Map();
|
|
447
|
+
for (const w of result.warnings) {
|
|
448
|
+
const { title, detected } = issueType(w);
|
|
449
|
+
const key = title;
|
|
450
|
+
if (!byTitle.has(key))
|
|
451
|
+
byTitle.set(key, detected);
|
|
452
|
+
else
|
|
453
|
+
byTitle.set(key, byTitle.get(key) + "; " + detected);
|
|
454
|
+
}
|
|
455
|
+
for (const d of result.duplicates) {
|
|
456
|
+
const key = "Duplicate Components";
|
|
457
|
+
const detected = d.files.join(", ");
|
|
458
|
+
if (!byTitle.has(key))
|
|
459
|
+
byTitle.set(key, detected);
|
|
460
|
+
else
|
|
461
|
+
byTitle.set(key, byTitle.get(key) + "; " + detected);
|
|
462
|
+
}
|
|
463
|
+
console.log(chalk.cyan("\n 🤖 AI suggestions\n"));
|
|
464
|
+
const spinner = ora("Getting suggestions...").start();
|
|
465
|
+
try {
|
|
466
|
+
for (const [title, detected] of byTitle.entries()) {
|
|
467
|
+
const suggestion = apiKey
|
|
468
|
+
? await getGeminiSuggestion(apiKey, title, detected)
|
|
469
|
+
: await getSuggestFromBackend(title, detected);
|
|
470
|
+
spinner.stop();
|
|
471
|
+
console.log(" " + chalk.bold(title) + "\n " + chalk.gray(suggestion) + "\n");
|
|
472
|
+
spinner.start("Getting suggestions...");
|
|
473
|
+
}
|
|
474
|
+
spinner.succeed("Done.");
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
spinner.fail("AI suggestions not available at the moment.");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
417
480
|
if (failOnScore != null && result.score < failOnScore) {
|
|
418
481
|
const msg = `Score ${result.score} is below required ${failOnScore}. Failing.`;
|
|
419
482
|
if (outputJson)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontend-guardian",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Scan frontend projects for Tailwind & component consistency (Phase 1 CLI)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"build": "tsc"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"@google/genai": "^1.0.0",
|
|
14
15
|
"@justinmoto/frontend-guardian-core": "^0.1.15",
|
|
15
16
|
"chalk": "^5.3.0",
|
|
16
17
|
"ora": "^8.0.1"
|