svelte-vitals 0.19.0 → 0.21.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/dist/bin.js CHANGED
@@ -4,12 +4,14 @@ import {
4
4
  findUnknownRuleIds,
5
5
  isReporterName,
6
6
  knownRuleIds,
7
+ readCoreVersion,
7
8
  readPackageVersion,
8
9
  run
9
- } from "./chunk-ZE3M3T6U.js";
10
+ } from "./chunk-B3RVLTZY.js";
10
11
 
11
12
  // src/bin.ts
12
- import mri2 from "mri";
13
+ import mri3 from "mri";
14
+ import * as p2 from "@clack/prompts";
13
15
 
14
16
  // src/resolve-args.ts
15
17
  var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture"];
@@ -55,6 +57,26 @@ function parseWeights(raw, errors) {
55
57
  }
56
58
  return weights;
57
59
  }
60
+ function parseCategories(raw, errors) {
61
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
62
+ const categories = [];
63
+ const unknownCategories = [];
64
+ for (const entry of raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)) {
65
+ if (!CATEGORIES.includes(entry)) {
66
+ unknownCategories.push(entry);
67
+ continue;
68
+ }
69
+ if (!categories.includes(entry)) categories.push(entry);
70
+ }
71
+ if (unknownCategories.length > 0) {
72
+ errors.push(`svelte-vitals: unknown category(ies) in --category: ${unknownCategories.join(", ")}`);
73
+ errors.push(`Known categories: ${CATEGORIES.join(", ")}`);
74
+ }
75
+ if (unknownCategories.length === 0 && categories.length === 0) {
76
+ errors.push("svelte-vitals: --category was passed but contains no categories.");
77
+ }
78
+ return categories;
79
+ }
58
80
  var toList = (v) => typeof v === "string" ? v.split(",").map((s) => s.trim()).filter(Boolean) : [];
59
81
  function resolveArgs(argv) {
60
82
  const warnings = [];
@@ -71,6 +93,14 @@ function resolveArgs(argv) {
71
93
  const route = typeof argv.route === "string" ? argv.route : void 0;
72
94
  const diffBase = typeof argv.diff === "string" ? argv.diff || "HEAD" : void 0;
73
95
  const staged = Boolean(argv.staged);
96
+ let baselineRef;
97
+ if (typeof argv.baseline === "string") {
98
+ if (argv.baseline.trim() === "") {
99
+ errors.push("svelte-vitals: --baseline requires a git ref (e.g. --baseline origin/main).");
100
+ } else {
101
+ baselineRef = argv.baseline;
102
+ }
103
+ }
74
104
  const allow = toList(argv.rules);
75
105
  const ignore = toList(argv.ignore);
76
106
  const unknown = findUnknownRuleIds([...allow, ...ignore]);
@@ -84,7 +114,7 @@ function resolveArgs(argv) {
84
114
  } else if (typeof argv.reporter === "string") {
85
115
  if (!isReporterName(argv.reporter)) {
86
116
  errors.push(
87
- `svelte-vitals: unknown reporter '${argv.reporter}'. Valid values: console, json, agent, sarif, github, html.`
117
+ `svelte-vitals: unknown reporter '${argv.reporter}'. Valid values: console, json, agent, sarif, github, html, md.`
88
118
  );
89
119
  } else {
90
120
  reporter = argv.reporter;
@@ -99,12 +129,20 @@ function resolveArgs(argv) {
99
129
  }
100
130
  const failOn = argv["fail-on-warning"] ? "warning" : failOnValid ? failOnRaw : void 0;
101
131
  const weights = parseWeights(argv.weights, errors);
132
+ const categories = parseCategories(argv.category, errors);
133
+ const score = Boolean(argv.score);
134
+ if (score && (argv.json || typeof argv.reporter === "string")) {
135
+ warnings.push("svelte-vitals: --score overrides --reporter; reporter output suppressed.");
136
+ }
102
137
  const rulesConfig = buildRulesConfig(allow, ignore);
103
138
  const rules = Object.keys(rulesConfig).length > 0 ? rulesConfig : void 0;
104
139
  if (errors.length > 0) return { options: null, warnings, errors };
105
140
  return {
106
141
  options: {
107
142
  cwd: positional ?? process.cwd(),
143
+ // Never reinterpret an explicit target (design doc 2026-07-08-monorepo-app-picker-design.md,
144
+ // decision 1): the monorepo picker in run() only triggers when this is false.
145
+ explicitPath: positional !== void 0,
108
146
  metaComponents,
109
147
  treatDynamicAs,
110
148
  route,
@@ -114,8 +152,11 @@ function resolveArgs(argv) {
114
152
  failOn,
115
153
  rules,
116
154
  ...weights !== void 0 ? { weights } : {},
155
+ ...categories !== void 0 ? { categories } : {},
156
+ ...score ? { score } : {},
117
157
  ...diffBase !== void 0 ? { diffBase } : {},
118
- ...staged ? { staged } : {}
158
+ ...staged ? { staged } : {},
159
+ ...baselineRef !== void 0 ? { baseline: baselineRef } : {}
119
160
  },
120
161
  warnings,
121
162
  errors
@@ -236,6 +277,93 @@ function isViteTargetId(id) {
236
277
  return VITE_TARGETS.some((t) => t.id === id);
237
278
  }
238
279
 
280
+ // src/install/agent-targets.ts
281
+ var AGENT_TARGETS = [
282
+ {
283
+ id: "claude-skill",
284
+ label: "Claude Code skill",
285
+ hint: "Teaches the agent svelte-vitals rules + when to run the scanner",
286
+ relPath: ".claude/skills/svelte-vitals/SKILL.md"
287
+ },
288
+ {
289
+ id: "cursor-rules",
290
+ label: "Cursor rules",
291
+ hint: "Project rules file so Cursor avoids flagged patterns up front",
292
+ relPath: ".cursor/rules/svelte-vitals.mdc"
293
+ }
294
+ ];
295
+ function agentTargetById(id) {
296
+ return AGENT_TARGETS.find((t) => t.id === id);
297
+ }
298
+ function isAgentTargetId(id) {
299
+ return AGENT_TARGETS.some((t) => t.id === id);
300
+ }
301
+
302
+ // src/install/skill-content.ts
303
+ import { allRules, docsUrlFor } from "@svelte-vitals/core";
304
+ var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture"];
305
+ var CATEGORY_LABELS = {
306
+ seo: "SEO",
307
+ performance: "Performance",
308
+ correctness: "Correctness",
309
+ security: "Security",
310
+ architecture: "Architecture"
311
+ };
312
+ function oneLine(text) {
313
+ return text.replace(/\r?\n+/g, " ").trim();
314
+ }
315
+ function ruleLine(rule) {
316
+ const fixPart = rule.fix?.description ? ` Fix: ${oneLine(rule.fix.description)}` : "";
317
+ return `- **${rule.id} \u2014 ${oneLine(rule.title)}** (${rule.severity}): ${oneLine(rule.rationale)}${fixPart} ([docs](${docsUrlFor(rule.id)}))`;
318
+ }
319
+ function ruleDigest() {
320
+ return CATEGORY_ORDER.map((category) => {
321
+ const lines = allRules.filter((r) => r.category === category).map(ruleLine).join("\n");
322
+ return `### ${CATEGORY_LABELS[category]}
323
+
324
+ ${lines}`;
325
+ }).join("\n\n");
326
+ }
327
+ function sharedBody(version) {
328
+ return `<!-- Generated by \`svelte-vitals install\` (svelte-vitals ${version}). Re-run with --force to refresh. -->
329
+
330
+ # svelte-vitals
331
+
332
+ ## When to use
333
+
334
+ Use this whenever you are writing or reviewing SvelteKit route files (\`+page.svelte\`, \`+layout.svelte\`) or components in this project \u2014 svelte-vitals statically checks SEO, performance, correctness, security, and architecture patterns.
335
+
336
+ ## Playbook
337
+
338
+ 1. After writing or editing code, run \`npx svelte-vitals . --diff --reporter agent\` and fix any findings it reports.
339
+ 2. Before committing, run \`npx svelte-vitals . --staged\` as a pre-commit gate.
340
+ 3. For a rule's full rationale and fix examples, use the \`explain_rule\` MCP tool (svelte-vitals MCP server) or open its docs link below.
341
+
342
+ ## Rule digest
343
+
344
+ ${ruleDigest()}
345
+ `;
346
+ }
347
+ function buildSkillMarkdown(version) {
348
+ const frontmatter = `---
349
+ name: svelte-vitals
350
+ description: Use when writing or reviewing SvelteKit routes/components \u2014 svelte-vitals rule knowledge (SEO, performance, correctness, security, architecture) and how to run the scanner.
351
+ ---`;
352
+ return `${frontmatter}
353
+
354
+ ${sharedBody(version)}`;
355
+ }
356
+ function buildCursorRules(version) {
357
+ const frontmatter = `---
358
+ description: svelte-vitals code-health rules for SvelteKit (SEO, performance, correctness, security, architecture)
359
+ globs: ["**/*.svelte", "src/routes/**"]
360
+ alwaysApply: false
361
+ ---`;
362
+ return `${frontmatter}
363
+
364
+ ${sharedBody(version)}`;
365
+ }
366
+
239
367
  // src/install/codemod-vite-config.ts
240
368
  import { parseModule, generateCode, builders, MagicastError } from "magicast";
241
369
  var MANUAL_SNIPPET = `import { svelteVitals } from '@svelte-vitals/vite';
@@ -252,7 +380,7 @@ function codemodViteConfig(existing) {
252
380
  return { status: "manual", snippet: MANUAL_SNIPPET };
253
381
  }
254
382
  const already = configObj.plugins.find(
255
- (p2) => p2?.$type === "function-call" && p2?.$callee === "svelteVitals"
383
+ (p3) => p3?.$type === "function-call" && p3?.$callee === "svelteVitals"
256
384
  );
257
385
  if (already !== void 0) {
258
386
  return { status: "exists" };
@@ -369,6 +497,15 @@ function installCommand(pm) {
369
497
  const action = pm === "npm" ? "install" : "add";
370
498
  return { command: pm, args: [action, "-D", "@svelte-vitals/vite"] };
371
499
  }
500
+ function readInstalledViteVersion(io) {
501
+ const raw = io.readFile(join2(io.cwd, "node_modules/@svelte-vitals/vite/package.json"));
502
+ if (raw === void 0) return void 0;
503
+ try {
504
+ return JSON.parse(raw).version;
505
+ } catch {
506
+ return void 0;
507
+ }
508
+ }
372
509
 
373
510
  // src/install/index.ts
374
511
  function planForClient(client, scope, io, force) {
@@ -395,6 +532,13 @@ function planForDevOverlay(io) {
395
532
  const result = codemodHooksServer(content);
396
533
  return { id: "vite-dev-overlay", label: viteTargetById("vite-dev-overlay").label, path, ...result };
397
534
  }
535
+ function planForAgentTarget(target, io, force, version) {
536
+ const path = join3(io.cwd, target.relPath);
537
+ const existing = io.readFile(path);
538
+ const content = target.id === "claude-skill" ? buildSkillMarkdown(version) : buildCursorRules(version);
539
+ const status = existing === void 0 ? "created" : force ? "updated" : "exists";
540
+ return { id: target.id, label: target.label, path, status, content };
541
+ }
398
542
  function indent(text) {
399
543
  return text.split("\n").map((l) => ` ${l}`).join("\n");
400
544
  }
@@ -403,7 +547,7 @@ function rowLine(r) {
403
547
  return r.status === "manual" && r.snippet ? `${head}
404
548
  ${indent(r.snippet)}` : head;
405
549
  }
406
- async function runInstall(flags, io, prompts) {
550
+ async function runInstall(flags, io, prompts, version = "0.0.0") {
407
551
  let ids;
408
552
  if (flags.client && flags.client.length > 0) {
409
553
  ids = flags.client;
@@ -421,10 +565,21 @@ async function runInstall(flags, io, prompts) {
421
565
  const viteConfigExists = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].some(
422
566
  (f) => configExists(join3(io.cwd, f))
423
567
  );
424
- const detected = [...detectedClients, ...viteConfigExists ? VITE_TARGETS.map((t) => t.id) : []];
568
+ const claudeSkillDetected = configExists(join3(io.cwd, ".claude", "settings.json"));
569
+ const cursorRulesDetected = configExists(join3(io.cwd, ".cursor", "mcp.json"));
570
+ const detectedAgents = [
571
+ ...claudeSkillDetected ? ["claude-skill"] : [],
572
+ ...cursorRulesDetected ? ["cursor-rules"] : []
573
+ ];
574
+ const detected = [
575
+ ...detectedClients,
576
+ ...viteConfigExists ? VITE_TARGETS.map((t) => t.id) : [],
577
+ ...detectedAgents
578
+ ];
425
579
  const options = [
426
580
  ...CLIENTS.map((c) => ({ id: c.id, label: c.label })),
427
- ...VITE_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
581
+ ...VITE_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
582
+ ...AGENT_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
428
583
  ];
429
584
  const picked = await prompts.selectClients(options, detected);
430
585
  if (picked === null) {
@@ -434,13 +589,14 @@ async function runInstall(flags, io, prompts) {
434
589
  ids = picked;
435
590
  } else {
436
591
  io.errorLog(
437
- "svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-dev-overlay> to install non-interactively."
592
+ "svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-dev-overlay,claude-skill,cursor-rules> to install non-interactively."
438
593
  );
439
594
  return 2;
440
595
  }
441
596
  const clients = ids.map(clientById).filter((c) => c !== void 0);
442
597
  const viteIds = ids.filter(isViteTargetId);
443
- if (clients.length === 0 && viteIds.length === 0) {
598
+ const agentIds = ids.filter(isAgentTargetId);
599
+ if (clients.length === 0 && viteIds.length === 0 && agentIds.length === 0) {
444
600
  io.errorLog("svelte-vitals: no valid clients or targets selected.");
445
601
  return 2;
446
602
  }
@@ -474,6 +630,10 @@ async function runInstall(flags, io, prompts) {
474
630
  for (const viteId of viteIds) {
475
631
  rows.push(viteId === "vite-plugin" ? planForVitePlugin(io) : planForDevOverlay(io));
476
632
  }
633
+ for (const agentId of agentIds) {
634
+ const target = agentTargetById(agentId);
635
+ rows.push(planForAgentTarget(target, io, flags.force ?? false, version));
636
+ }
477
637
  const planText = rows.map(rowLine).join("\n");
478
638
  io.log("Plan:");
479
639
  io.log(planText);
@@ -519,6 +679,11 @@ ${indent(r.snippet ?? "")}`);
519
679
  io.errorLog(
520
680
  `svelte-vitals: failed to install @svelte-vitals/vite (${command} ${args.join(" ")} exited ${code}). Install it manually.`
521
681
  );
682
+ } else {
683
+ const installedVersion = readInstalledViteVersion(io);
684
+ io.log(
685
+ installedVersion ? `svelte-vitals: installed @svelte-vitals/vite@${installedVersion} \u2014 compare against \`svelte-vitals --version\`'s core number if findings ever seem out of sync.` : "svelte-vitals: installed @svelte-vitals/vite (could not read the installed version from node_modules)."
686
+ );
522
687
  }
523
688
  }
524
689
  if (hadFailure) return 2;
@@ -530,7 +695,11 @@ ${indent(r.snippet ?? "")}`);
530
695
  }
531
696
 
532
697
  // src/install/args.ts
533
- var VALID_TARGETS = [...CLIENTS.map((c) => c.id), ...VITE_TARGETS.map((t) => t.id)];
698
+ var VALID_TARGETS = [
699
+ ...CLIENTS.map((c) => c.id),
700
+ ...VITE_TARGETS.map((t) => t.id),
701
+ ...AGENT_TARGETS.map((t) => t.id)
702
+ ];
534
703
  var EXPECTED_TARGETS = VALID_TARGETS.join("|");
535
704
  function resolveInstallArgs(argv) {
536
705
  const warnings = [];
@@ -568,16 +737,20 @@ function resolveInstallArgs(argv) {
568
737
  }
569
738
 
570
739
  // src/install/cli.ts
571
- var INSTALL_HELP = `svelte-vitals install \u2014 set up the svelte-vitals MCP server for your AI-agent clients
740
+ var INSTALL_HELP = `svelte-vitals install \u2014 set up the svelte-vitals MCP server, Vite integration, and agent skills/rules
572
741
 
573
742
  Usage:
574
743
  svelte-vitals install [options]
575
744
 
576
745
  Options:
577
- --client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-dev-overlay (skips the interactive picker)
746
+ --client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-dev-overlay,claude-skill,cursor-rules
747
+ (skips the interactive picker)
578
748
  vite-plugin registers the build-mode plugin in vite.config.{ts,js,mjs}; vite-dev-overlay
579
749
  wires up the dev-overlay hook in src/hooks.server.{ts,js}. --force does not apply
580
- to either \u2014 an existing registration is always left as-is.
750
+ to either of these two \u2014 an existing registration is always left as-is.
751
+ claude-skill writes a Claude Code skill (.claude/skills/svelte-vitals/SKILL.md); cursor-rules
752
+ writes a Cursor rules file (.cursor/rules/svelte-vitals.mdc). Both are generated from the
753
+ current rule set and support --force to regenerate.
581
754
  --scope <scope> project | global (applies to all selected clients; codex is always global)
582
755
  --yes, -y Skip the confirmation prompt
583
756
  --dry-run Print the planned changes and exit without writing
@@ -661,7 +834,143 @@ async function runInstallCli(args) {
661
834
  for (const w of warnings) console.error(w);
662
835
  for (const e of errors) console.error(e);
663
836
  if (!flags) return 2;
664
- return runInstall(flags, realIO(), clackPrompts());
837
+ return runInstall(flags, realIO(), clackPrompts(), readPackageVersion());
838
+ }
839
+
840
+ // src/ci/cli.ts
841
+ import { join as join4 } from "path";
842
+ import mri2 from "mri";
843
+
844
+ // src/ci/workflow.ts
845
+ var WORKFLOW_PATH = ".github/workflows/svelte-vitals.yml";
846
+ function planWorkflowWrite(existing, force) {
847
+ if (existing === void 0) return { status: "created" };
848
+ if (!force) return { status: "exists" };
849
+ return { status: "updated" };
850
+ }
851
+ function buildWorkflowYaml(opts) {
852
+ const v = opts.version;
853
+ return [
854
+ `# Generated by \`svelte-vitals ci install\` (svelte-vitals ${v}).`,
855
+ "# Re-run with --force to regenerate.",
856
+ "name: svelte-vitals",
857
+ "",
858
+ "on:",
859
+ " pull_request:",
860
+ "",
861
+ "permissions:",
862
+ " contents: read",
863
+ " pull-requests: write",
864
+ "",
865
+ "jobs:",
866
+ " svelte-vitals:",
867
+ " runs-on: ubuntu-latest",
868
+ " steps:",
869
+ " - uses: actions/checkout@v4",
870
+ " with:",
871
+ " fetch-depth: 0",
872
+ " - uses: actions/setup-node@v4",
873
+ " with:",
874
+ " node-version: 24",
875
+ " - name: Scan (inline annotations + gate)",
876
+ " id: scan",
877
+ " continue-on-error: true",
878
+ " run: >",
879
+ ` npx -y svelte-vitals@${v} .`,
880
+ " --diff origin/${{ github.base_ref }}",
881
+ " --baseline origin/${{ github.base_ref }}",
882
+ " --reporter github",
883
+ " - name: Markdown summary",
884
+ " run: >",
885
+ ` npx -y svelte-vitals@${v} .`,
886
+ " --diff origin/${{ github.base_ref }}",
887
+ " --baseline origin/${{ github.base_ref }}",
888
+ " --reporter md > svelte-vitals-report.md || true",
889
+ " - name: Job summary",
890
+ ' run: cat svelte-vitals-report.md >> "$GITHUB_STEP_SUMMARY"',
891
+ " - name: PR comment (sticky)",
892
+ " if: github.event.pull_request.head.repo.full_name == github.repository",
893
+ " continue-on-error: true",
894
+ " uses: actions/github-script@v7",
895
+ " with:",
896
+ " script: |",
897
+ " const fs = require('fs');",
898
+ " const marker = '<!-- svelte-vitals-report -->';",
899
+ " const body = marker + '\\n' + fs.readFileSync('svelte-vitals-report.md', 'utf8');",
900
+ " const { data: comments } = await github.rest.issues.listComments({",
901
+ " ...context.repo, issue_number: context.issue.number, per_page: 100",
902
+ " });",
903
+ " const mine = comments.find(c => c.body && c.body.startsWith(marker));",
904
+ " if (mine) {",
905
+ " await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body });",
906
+ " } else {",
907
+ " await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body });",
908
+ " }",
909
+ " - name: Gate",
910
+ " if: steps.scan.outcome == 'failure'",
911
+ " run: |",
912
+ ' echo "svelte-vitals found blocking issues (see annotations above)."',
913
+ " exit 1",
914
+ ""
915
+ ].join("\n");
916
+ }
917
+
918
+ // src/ci/cli.ts
919
+ var CI_HELP = `svelte-vitals ci \u2014 scaffold CI integration
920
+
921
+ Usage:
922
+ svelte-vitals ci install [options]
923
+
924
+ Adds a GitHub Actions workflow (${WORKFLOW_PATH}) that scans pull requests, posts inline
925
+ annotations + a job summary, and maintains a sticky PR comment with the findings.
926
+
927
+ Options:
928
+ --force Overwrite an existing workflow file
929
+ --dry-run Print the plan and exit without writing
930
+ -h, --help Show this help`;
931
+ async function runCiCli(args, io = realIO()) {
932
+ const sub = args[0];
933
+ if (sub === "--help" || sub === "-h") {
934
+ io.log(CI_HELP);
935
+ return 0;
936
+ }
937
+ if (sub !== "install") {
938
+ io.log(CI_HELP);
939
+ return 2;
940
+ }
941
+ const argv = mri2(args.slice(1), {
942
+ boolean: ["force", "dry-run", "help"],
943
+ alias: { h: "help" }
944
+ });
945
+ if (argv.help) {
946
+ io.log(CI_HELP);
947
+ return 0;
948
+ }
949
+ const path = join4(io.cwd, WORKFLOW_PATH);
950
+ const existing = io.readFile(path);
951
+ const plan = planWorkflowWrite(existing, Boolean(argv.force));
952
+ io.log("Plan:");
953
+ io.log(` ${WORKFLOW_PATH} [${plan.status}]`);
954
+ if (argv["dry-run"]) {
955
+ io.log("Dry run \u2014 no files written.");
956
+ return 0;
957
+ }
958
+ if (plan.status === "exists") {
959
+ io.log(`= already installed (${WORKFLOW_PATH}) \u2014 use --force to regenerate.`);
960
+ } else {
961
+ const version = readPackageVersion();
962
+ try {
963
+ io.writeFile(path, buildWorkflowYaml({ version }));
964
+ io.log(`\u2713 ${plan.status} ${WORKFLOW_PATH}`);
965
+ } catch (err) {
966
+ io.errorLog(
967
+ `svelte-vitals: failed to write ${WORKFLOW_PATH}: ${err instanceof Error ? err.message : String(err)}`
968
+ );
969
+ return 2;
970
+ }
971
+ }
972
+ io.log("Done. Commit the workflow file and open a PR to see it in action.");
973
+ return 0;
665
974
  }
666
975
 
667
976
  // src/bin.ts
@@ -669,7 +978,8 @@ var HELP = `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (
669
978
 
670
979
  Usage:
671
980
  svelte-vitals [path] [options]
672
- svelte-vitals install Set up the MCP server for Claude Code / Cursor / Codex
981
+ svelte-vitals install Set up the MCP server, Vite integration, or agent skills/rules
982
+ svelte-vitals ci install Add a GitHub Actions PR gate (annotations + summary comment)
673
983
 
674
984
  Options:
675
985
  --meta-components <names> Comma-separated component names that emit head metadata
@@ -677,8 +987,9 @@ Options:
677
987
  --route <glob> Only analyze routes matching this glob
678
988
  --diff [ref] Report only findings in files changed vs ref (default HEAD; e.g. --diff main)
679
989
  --staged Report only findings in files staged for commit (pre-commit gate)
990
+ --baseline <ref> Report only findings not present at ref (compare against e.g. origin/main)
680
991
  --by-route Show per-route score breakdown in console output
681
- --reporter <fmt> console | json | agent | sarif | github | html (auto: agent under AI-agent envs, github under GitHub Actions)
992
+ --reporter <fmt> console | json | agent | sarif | github | html | md (auto: agent under AI-agent envs, github under GitHub Actions)
682
993
  --out-file <path> Output path for --reporter html (default: svelte-vitals-report.html; '-' for stdout)
683
994
  --json Alias for --reporter=json
684
995
  --fail-on <severity> Fail (exit 1) when any finding reaches this severity: critical | warning | info
@@ -686,7 +997,9 @@ Options:
686
997
  --min-health <0-100> Fail (exit 1) when the combined Health score is below this value
687
998
  --rules <ids> Comma-separated rule ids to enable (all others disabled)
688
999
  --ignore <ids> Comma-separated rule ids to disable
1000
+ --category <cats> Comma-separated categories to analyze: seo | performance | correctness | security | architecture
689
1001
  --weights <pairs> Per-category Health weight overrides, e.g. seo=2,performance=1 (unlisted categories default to 1)
1002
+ --score Print only the combined Health score (works with --min-health for gating)
690
1003
  --no-color Disable ANSI color in console output
691
1004
  -h, --help Show this help
692
1005
  -v, --version Show version
@@ -699,15 +1012,26 @@ Exit codes:
699
1012
  1 critical finding present (or --fail-on threshold reached)
700
1013
  2 execution error (not a SvelteKit project / internal error)`;
701
1014
  var VERSION = readPackageVersion();
1015
+ async function selectApp(apps) {
1016
+ const res = await p2.select({
1017
+ message: "Multiple SvelteKit apps found \u2014 which one should svelte-vitals analyze?",
1018
+ options: apps.map((a) => ({ value: a, label: a }))
1019
+ });
1020
+ return p2.isCancel(res) ? null : res;
1021
+ }
702
1022
  async function main() {
703
1023
  const rawArgs = process.argv.slice(2);
704
1024
  if (rawArgs[0] === "install") {
705
1025
  const code2 = await runInstallCli(rawArgs.slice(1));
706
1026
  process.exit(code2);
707
1027
  }
708
- const argv = mri2(process.argv.slice(2), {
1028
+ if (rawArgs[0] === "ci") {
1029
+ const code2 = await runCiCli(rawArgs.slice(1));
1030
+ process.exit(code2);
1031
+ }
1032
+ const argv = mri3(process.argv.slice(2), {
709
1033
  alias: { h: "help", v: "version" },
710
- boolean: ["by-route", "json", "fail-on-warning", "staged", "no-color"],
1034
+ boolean: ["by-route", "json", "fail-on-warning", "staged", "no-color", "score"],
711
1035
  string: [
712
1036
  "meta-components",
713
1037
  "treat-dynamic-as",
@@ -719,7 +1043,9 @@ async function main() {
719
1043
  "min-health",
720
1044
  "out-file",
721
1045
  "diff",
722
- "weights"
1046
+ "baseline",
1047
+ "weights",
1048
+ "category"
723
1049
  ]
724
1050
  });
725
1051
  if (argv.help) {
@@ -727,7 +1053,7 @@ async function main() {
727
1053
  process.exit(0);
728
1054
  }
729
1055
  if (argv.version) {
730
- console.log(VERSION);
1056
+ console.log(`${VERSION} (core ${readCoreVersion()})`);
731
1057
  process.exit(0);
732
1058
  }
733
1059
  const { options, warnings, errors } = resolveArgs(argv);
@@ -744,7 +1070,7 @@ async function main() {
744
1070
  }
745
1071
  minHealth = n;
746
1072
  }
747
- const code = await run({ ...options, minHealth, noColor: argv["no-color"] });
1073
+ const code = await run({ ...options, minHealth, noColor: argv["no-color"], selectApp });
748
1074
  process.exit(code);
749
1075
  }
750
1076
  void main();
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import { mkdirSync, writeFileSync } from "fs";
3
- import { dirname } from "path";
3
+ import { dirname as dirname2, join as join5 } from "path";
4
4
  import {
5
5
  allRules as allRules2,
6
6
  runRules,
@@ -10,6 +10,7 @@ import {
10
10
  formatSarifReport,
11
11
  formatGithubReport,
12
12
  formatHtmlReport,
13
+ formatMarkdownReport,
13
14
  summarize,
14
15
  hasFailureAtOrAbove,
15
16
  computeHealth,
@@ -73,7 +74,7 @@ async function detectProject(rt, cwd) {
73
74
  const hasRoutes = await rt.exists(rt.join(cwd, ROUTES_DIR));
74
75
  if (hasKitDep || hasConfig && hasRoutes) return;
75
76
  throw new ProjectError(
76
- "No SvelteKit project found in the current directory. Run this inside a SvelteKit app, or pass --config."
77
+ "No SvelteKit project found in the current directory. Run this inside a SvelteKit app, or pass a path (e.g. npx svelte-vitals apps/web)."
77
78
  );
78
79
  }
79
80
  async function enumerateRoutePages(rt, cwd) {
@@ -650,6 +651,23 @@ async function collectRoutes(rt, cwd, config = defaultConfig) {
650
651
  // src/providers/source/components.ts
651
652
  import { collectComponentFacts } from "@svelte-vitals/core";
652
653
 
654
+ // src/discover-apps.ts
655
+ import { existsSync } from "fs";
656
+ import { join as join2, dirname } from "path";
657
+ import { glob } from "tinyglobby";
658
+ async function discoverApps(cwd) {
659
+ const configs = await glob("**/svelte.config.{js,ts}", {
660
+ cwd,
661
+ dot: false,
662
+ deep: 4,
663
+ ignore: ["**/node_modules/**", "**/.svelte-kit/**", "**/build/**", "**/dist/**", "**/.git/**"]
664
+ });
665
+ const dirs = [...new Set(configs.map((c) => dirname(c)))].filter(
666
+ (d) => d !== "." && existsSync(join2(cwd, d, "src", "routes"))
667
+ );
668
+ return dirs.sort();
669
+ }
670
+
653
671
  // src/version.ts
654
672
  import { readFileSync } from "fs";
655
673
  function readPackageVersion() {
@@ -660,11 +678,20 @@ function readPackageVersion() {
660
678
  return "0.0.0";
661
679
  }
662
680
  }
681
+ function readCoreVersion() {
682
+ try {
683
+ const entry = import.meta.resolve("@svelte-vitals/core");
684
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", entry), "utf8"));
685
+ return pkg.version ?? "0.0.0";
686
+ } catch {
687
+ return "0.0.0";
688
+ }
689
+ }
663
690
 
664
691
  // src/reporter-resolve.ts
665
692
  var AGENT_ENV_VARS = ["CLAUDECODE", "SVELTE_VITALS_AGENT"];
666
693
  function isReporterName(value) {
667
- return value === "console" || value === "json" || value === "agent" || value === "sarif" || value === "github" || value === "html";
694
+ return value === "console" || value === "json" || value === "agent" || value === "sarif" || value === "github" || value === "html" || value === "md";
668
695
  }
669
696
  function isAgentEnv(env = process.env) {
670
697
  return AGENT_ENV_VARS.some((key) => {
@@ -711,6 +738,58 @@ function filterToChangedFiles(results, changed) {
711
738
  return results.filter((r) => r.location !== void 0 && changed.has(r.location));
712
739
  }
713
740
 
741
+ // src/baseline.ts
742
+ import { execFileSync as execFileSync2 } from "child_process";
743
+ import { mkdtempSync, rmSync } from "fs";
744
+ import { tmpdir } from "os";
745
+ import { join as join3 } from "path";
746
+ function git2(args, cwd) {
747
+ return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
748
+ }
749
+ function findingKey(r) {
750
+ return `${r.id}::${r.route ?? ""}::${r.location ?? ""}`;
751
+ }
752
+ function checkoutBaseline(cwd, ref) {
753
+ let tmp;
754
+ try {
755
+ const repoRoot = git2(["rev-parse", "--show-toplevel"], cwd).trim();
756
+ const showPrefix = git2(["rev-parse", "--show-prefix"], cwd).trim().replace(/\/+$/, "");
757
+ tmp = mkdtempSync(join3(tmpdir(), "svelte-vitals-baseline-"));
758
+ const wt = join3(tmp, "wt");
759
+ git2(["worktree", "add", "--detach", wt, ref], repoRoot);
760
+ const analyzeCwd = showPrefix ? join3(wt, showPrefix) : wt;
761
+ const tmpDir = tmp;
762
+ const cleanup = () => {
763
+ try {
764
+ execFileSync2("git", ["worktree", "remove", "--force", wt], {
765
+ cwd: repoRoot,
766
+ encoding: "utf8",
767
+ stdio: ["ignore", "pipe", "ignore"]
768
+ });
769
+ } catch {
770
+ try {
771
+ execFileSync2("git", ["worktree", "prune"], {
772
+ cwd: repoRoot,
773
+ encoding: "utf8",
774
+ stdio: ["ignore", "pipe", "ignore"]
775
+ });
776
+ } catch {
777
+ }
778
+ } finally {
779
+ rmSync(tmpDir, { recursive: true, force: true });
780
+ }
781
+ };
782
+ return { analyzeCwd, cleanup };
783
+ } catch {
784
+ if (tmp !== void 0) rmSync(tmp, { recursive: true, force: true });
785
+ return void 0;
786
+ }
787
+ }
788
+ function filterToNewFindings(results, baselineResults) {
789
+ const baselineKeys = new Set(baselineResults.map(findingKey));
790
+ return results.filter((r) => !baselineKeys.has(findingKey(r)));
791
+ }
792
+
714
793
  // src/color.ts
715
794
  import { noColorPalette } from "@svelte-vitals/core";
716
795
  var wrap = (open, close = 0) => (s) => `\x1B[${open}m${s}\x1B[${close}m`;
@@ -754,8 +833,8 @@ function startSpinner(text, opts) {
754
833
  }
755
834
 
756
835
  // src/config-file.ts
757
- import { existsSync } from "fs";
758
- import { join as join2 } from "path";
836
+ import { existsSync as existsSync2 } from "fs";
837
+ import { join as join4 } from "path";
759
838
  import { pathToFileURL } from "url";
760
839
 
761
840
  // src/rules-config.ts
@@ -852,7 +931,7 @@ function validateConfigFile(raw, path) {
852
931
  return { config, warnings };
853
932
  }
854
933
  async function loadConfigFile(cwd) {
855
- const found = CONFIG_FILENAMES.map((name) => join2(cwd, name)).find((path) => existsSync(path));
934
+ const found = CONFIG_FILENAMES.map((name) => join4(cwd, name)).find((path) => existsSync2(path));
856
935
  if (!found) return void 0;
857
936
  let mod;
858
937
  try {
@@ -879,9 +958,9 @@ import { defineConfig as defineConfig2 } from "@svelte-vitals/core";
879
958
  function spinnerEnabled(opts) {
880
959
  return opts.reporter === "console" && opts.stderrIsTTY && !isAutoDetectedAgent(opts.rawReporter, opts.env) && colorEnabled({ reporter: opts.reporter, isTTY: opts.stderrIsTTY, env: opts.env, noColorFlag: opts.noColorFlag });
881
960
  }
882
- function routeMatcher(glob) {
883
- if (!glob) return () => true;
884
- const body = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, " ").replace(/\*/g, "[^/]*").replace(/\/ $/g, "(?:/.*)?").replace(/^ \//g, "(?:.*/)?").replace(/ \//g, "(?:.*/)?").replace(/\/ /g, "(?:/.*)?").replace(/ /g, ".*");
961
+ function routeMatcher(glob2) {
962
+ if (!glob2) return () => true;
963
+ const body = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, " ").replace(/\*/g, "[^/]*").replace(/\/ $/g, "(?:/.*)?").replace(/^ \//g, "(?:.*/)?").replace(/ \//g, "(?:.*/)?").replace(/\/ /g, "(?:/.*)?").replace(/ /g, ".*");
885
964
  const re = new RegExp(`^${body}$`);
886
965
  return (route) => re.test(route.replace(/^\//, ""));
887
966
  }
@@ -907,7 +986,8 @@ async function analyzeProject(opts = {}) {
907
986
  const headings = collected.headings.filter((h) => matches(h.route));
908
987
  const project = await collectProjectFacts(rt, cwd);
909
988
  const components = opts.route ? [] : await collectComponentFacts(rt, cwd);
910
- const rules = selectRules(allRules2, config);
989
+ const selected = selectRules(allRules2, config);
990
+ const rules = opts.categories ? selected.filter((r) => opts.categories.includes(r.category)) : selected;
911
991
  const results = applyRuleSeverities(
912
992
  await runRules(rules, { heads, images, headings, components, project, config }),
913
993
  config
@@ -924,7 +1004,7 @@ async function run(opts = {}) {
924
1004
  const env = opts.env ?? process.env;
925
1005
  const reporter = resolveReporter(opts.reporter, env);
926
1006
  const spinner = startSpinner("Analyzing\u2026", {
927
- enabled: spinnerEnabled({
1007
+ enabled: !opts.score && spinnerEnabled({
928
1008
  reporter,
929
1009
  rawReporter: opts.reporter,
930
1010
  stderrIsTTY: opts.stderrIsTTY ?? !!process.stderr.isTTY,
@@ -932,25 +1012,75 @@ async function run(opts = {}) {
932
1012
  noColorFlag: opts.noColor
933
1013
  })
934
1014
  });
1015
+ let cwd = opts.cwd ?? process.cwd();
935
1016
  let analysis;
936
1017
  try {
937
1018
  analysis = await analyzeProject({
938
- cwd: opts.cwd ?? process.cwd(),
1019
+ cwd,
939
1020
  metaComponents: opts.metaComponents,
940
1021
  treatDynamicAs: opts.treatDynamicAs,
941
1022
  route: opts.route,
942
1023
  failOn: opts.failOn,
943
1024
  rules: opts.rules,
944
- weights: opts.weights
1025
+ weights: opts.weights,
1026
+ categories: opts.categories
945
1027
  });
946
1028
  } catch (err) {
947
1029
  spinner.stop();
948
1030
  if (err instanceof ProjectError) {
949
- errorLog(err.message);
1031
+ if (opts.explicitPath) {
1032
+ errorLog(err.message);
1033
+ return 2;
1034
+ }
1035
+ const apps = await discoverApps(cwd);
1036
+ if (apps.length === 0) {
1037
+ errorLog(err.message);
1038
+ return 2;
1039
+ }
1040
+ let chosen;
1041
+ if (apps.length === 1) {
1042
+ errorLog(`svelte-vitals: detected SvelteKit app at ${apps[0]}; analyzing it.`);
1043
+ chosen = apps[0];
1044
+ } else if (
1045
+ // clack reads from stdin and renders to stdout, so both must be interactive —
1046
+ // a piped/redirected stdin would leave the prompt hanging for input that never comes.
1047
+ (opts.stdinIsTTY ?? !!process.stdin.isTTY) && (opts.stdoutIsTTY ?? !!process.stdout.isTTY) && opts.selectApp
1048
+ ) {
1049
+ const selection = await opts.selectApp(apps);
1050
+ if (selection === null) {
1051
+ log("Cancelled.");
1052
+ return 0;
1053
+ }
1054
+ chosen = selection;
1055
+ } else {
1056
+ errorLog(`svelte-vitals: multiple SvelteKit apps found: ${apps.join(", ")}.`);
1057
+ errorLog(`svelte-vitals: pass one as a path, e.g. \`npx svelte-vitals ${apps[0]}\`.`);
1058
+ return 2;
1059
+ }
1060
+ cwd = join5(cwd, chosen);
1061
+ try {
1062
+ analysis = await analyzeProject({
1063
+ cwd,
1064
+ metaComponents: opts.metaComponents,
1065
+ treatDynamicAs: opts.treatDynamicAs,
1066
+ route: opts.route,
1067
+ failOn: opts.failOn,
1068
+ rules: opts.rules,
1069
+ weights: opts.weights,
1070
+ categories: opts.categories
1071
+ });
1072
+ } catch (err2) {
1073
+ if (err2 instanceof ProjectError) {
1074
+ errorLog(err2.message);
1075
+ return 2;
1076
+ }
1077
+ errorLog(`svelte-vitals: ${err2 instanceof Error ? err2.message : String(err2)}`);
1078
+ return 2;
1079
+ }
1080
+ } else {
1081
+ errorLog(`svelte-vitals: ${err instanceof Error ? err.message : String(err)}`);
950
1082
  return 2;
951
1083
  }
952
- errorLog(`svelte-vitals: ${err instanceof Error ? err.message : String(err)}`);
953
- return 2;
954
1084
  }
955
1085
  spinner.stop();
956
1086
  for (const w of analysis.warnings) errorLog(`svelte-vitals: ${w}`);
@@ -958,7 +1088,6 @@ async function run(opts = {}) {
958
1088
  const { config, version } = analysis;
959
1089
  let results = analysis.results;
960
1090
  if (opts.staged || opts.diffBase !== void 0) {
961
- const cwd = opts.cwd ?? process.cwd();
962
1091
  const changed = opts.staged ? getChangedFiles(cwd, { staged: true }) : getChangedFiles(cwd, { base: opts.diffBase });
963
1092
  if (changed === void 0) {
964
1093
  errorLog(
@@ -968,46 +1097,78 @@ async function run(opts = {}) {
968
1097
  results = filterToChangedFiles(results, changed);
969
1098
  }
970
1099
  }
971
- if (reporter === "agent" && isAutoDetectedAgent(opts.reporter, env)) {
972
- errorLog(
973
- "svelte-vitals: agent reporter auto-selected (AI-agent env detected); override with --reporter console|json."
974
- );
975
- }
976
- if (reporter === "github" && isAutoDetectedGithub(opts.reporter, env)) {
977
- errorLog(
978
- "svelte-vitals: github reporter auto-selected (GitHub Actions detected); override with --reporter console|json|sarif."
979
- );
1100
+ if (opts.baseline !== void 0) {
1101
+ const checkout = checkoutBaseline(cwd, opts.baseline);
1102
+ if (checkout === void 0) {
1103
+ errorLog(
1104
+ `svelte-vitals: could not analyze baseline '${opts.baseline}' (not a git repo, git unavailable, or bad ref); reporting all findings.`
1105
+ );
1106
+ } else {
1107
+ try {
1108
+ const base = await analyzeProject({
1109
+ cwd: checkout.analyzeCwd,
1110
+ metaComponents: opts.metaComponents,
1111
+ treatDynamicAs: opts.treatDynamicAs,
1112
+ route: opts.route,
1113
+ failOn: opts.failOn,
1114
+ rules: opts.rules,
1115
+ weights: opts.weights,
1116
+ categories: opts.categories
1117
+ });
1118
+ results = filterToNewFindings(results, base.results);
1119
+ } catch {
1120
+ errorLog(`svelte-vitals: baseline analysis of '${opts.baseline}' failed; reporting all findings.`);
1121
+ } finally {
1122
+ checkout.cleanup();
1123
+ }
1124
+ }
980
1125
  }
981
- if (reporter === "json") {
982
- log(formatJsonReport(results, config, { version }));
983
- } else if (reporter === "agent") {
984
- log(formatAgentReport(results, config));
985
- } else if (reporter === "sarif") {
986
- log(formatSarifReport(results, config, { version }));
987
- } else if (reporter === "github") {
988
- const output = formatGithubReport(results, config);
989
- if (output) log(output);
990
- } else if (reporter === "html") {
991
- const html = formatHtmlReport(results, config, { version });
992
- if (opts.outFile === "-") {
993
- log(html);
1126
+ if (opts.score) {
1127
+ log(String(computeHealth(results, config).health));
1128
+ } else {
1129
+ if (reporter === "agent" && isAutoDetectedAgent(opts.reporter, env)) {
1130
+ errorLog(
1131
+ "svelte-vitals: agent reporter auto-selected (AI-agent env detected); override with --reporter console|json."
1132
+ );
1133
+ }
1134
+ if (reporter === "github" && isAutoDetectedGithub(opts.reporter, env)) {
1135
+ errorLog(
1136
+ "svelte-vitals: github reporter auto-selected (GitHub Actions detected); override with --reporter console|json|sarif."
1137
+ );
1138
+ }
1139
+ if (reporter === "json") {
1140
+ log(formatJsonReport(results, config, { version }));
1141
+ } else if (reporter === "agent") {
1142
+ log(formatAgentReport(results, config));
1143
+ } else if (reporter === "sarif") {
1144
+ log(formatSarifReport(results, config, { version }));
1145
+ } else if (reporter === "github") {
1146
+ const output = formatGithubReport(results, config);
1147
+ if (output) log(output);
1148
+ } else if (reporter === "html") {
1149
+ const html = formatHtmlReport(results, config, { version, coreVersion: readCoreVersion() });
1150
+ if (opts.outFile === "-") {
1151
+ log(html);
1152
+ } else {
1153
+ const path = opts.outFile || "svelte-vitals-report.html";
1154
+ const write = opts.writeFile ?? ((p, c) => {
1155
+ mkdirSync(dirname2(p), { recursive: true });
1156
+ writeFileSync(p, c);
1157
+ });
1158
+ write(path, html);
1159
+ errorLog(`svelte-vitals: wrote report to ${path}`);
1160
+ }
1161
+ } else if (reporter === "md") {
1162
+ log(formatMarkdownReport(results, config, { version }));
994
1163
  } else {
995
- const path = opts.outFile || "svelte-vitals-report.html";
996
- const write = opts.writeFile ?? ((p, c) => {
997
- mkdirSync(dirname(p), { recursive: true });
998
- writeFileSync(p, c);
1164
+ const colorOn = colorEnabled({
1165
+ reporter,
1166
+ isTTY: opts.stdoutIsTTY ?? !!process.stdout.isTTY,
1167
+ env,
1168
+ noColorFlag: opts.noColor
999
1169
  });
1000
- write(path, html);
1001
- errorLog(`svelte-vitals: wrote report to ${path}`);
1170
+ log(formatConsoleReport(results, config, { byRoute: opts.byRoute ?? false, palette: paletteFor(colorOn) }));
1002
1171
  }
1003
- } else {
1004
- const colorOn = colorEnabled({
1005
- reporter,
1006
- isTTY: opts.stdoutIsTTY ?? !!process.stdout.isTTY,
1007
- env,
1008
- noColorFlag: opts.noColor
1009
- });
1010
- log(formatConsoleReport(results, config, { byRoute: opts.byRoute ?? false, palette: paletteFor(colorOn) }));
1011
1172
  }
1012
1173
  const summary = summarize(results, config);
1013
1174
  const failBySeverity = hasFailureAtOrAbove(summary, config.failOn);
@@ -1022,6 +1183,7 @@ async function run(opts = {}) {
1022
1183
  export {
1023
1184
  ProjectError,
1024
1185
  readPackageVersion,
1186
+ readCoreVersion,
1025
1187
  isReporterName,
1026
1188
  findUnknownRuleIds,
1027
1189
  knownRuleIds,
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { RuleSetting, Config, Severity, Category, Result } from '@svelte-vitals/core';
2
2
  export { defineConfig } from '@svelte-vitals/core';
3
3
 
4
- type ReporterName = 'console' | 'json' | 'agent' | 'sarif' | 'github' | 'html';
4
+ type ReporterName = 'console' | 'json' | 'agent' | 'sarif' | 'github' | 'html' | 'md';
5
5
 
6
6
  /** Thrown when the target directory is not a SvelteKit project (CLI maps to exit 2). */
7
7
  declare class ProjectError extends Error {
@@ -59,10 +59,14 @@ interface RunOptions {
59
59
  rules?: Record<string, RuleSetting>;
60
60
  /** Per-category weights for the combined Health score (flag > config file > default 1 each). */
61
61
  weights?: Partial<Record<Category, number>>;
62
+ /** Restrict analysis to rules in these categories (applied after rules/ignore selection). */
63
+ categories?: Category[];
62
64
  /** Override process.env for reporter auto-detection (mainly useful in tests). */
63
65
  env?: NodeJS.ProcessEnv;
64
66
  /** Fail (exit 1) when the combined Health score is below this value (0–100). */
65
67
  minHealth?: number;
68
+ /** Print only the combined Health score (integer) to stdout. */
69
+ score?: boolean;
66
70
  /** Output path for --reporter html (default 'svelte-vitals-report.html'; '-' = stdout). */
67
71
  outFile?: string;
68
72
  /** Injected file writer for --reporter html (defaults to node:fs writeFileSync). Mainly for tests. */
@@ -71,12 +75,20 @@ interface RunOptions {
71
75
  diffBase?: string;
72
76
  /** Report only findings in files staged for commit. Takes precedence over `diffBase`. */
73
77
  staged?: boolean;
78
+ /** Report only findings not present when analyzing this git ref (e.g. the PR base). */
79
+ baseline?: string;
74
80
  /** Disable ANSI color in console output. */
75
81
  noColor?: boolean;
76
82
  /** Override stdout TTY detection (tests). */
77
83
  stdoutIsTTY?: boolean;
78
84
  /** Override stderr TTY detection (tests). */
79
85
  stderrIsTTY?: boolean;
86
+ /** Override stdin TTY detection (tests). */
87
+ stdinIsTTY?: boolean;
88
+ /** True when the user passed a path argument — discovery must not run (design: never reinterpret an explicit target). */
89
+ explicitPath?: boolean;
90
+ /** Injected picker for the monorepo app selector (bin.ts wires a clack implementation; null = cancelled). */
91
+ selectApp?: (apps: string[]) => Promise<string | null>;
80
92
  }
81
93
  /**
82
94
  * Whether the "Analyzing…" spinner should run. Unlike color, the spinner animates
@@ -103,6 +115,8 @@ interface AnalyzeOptions {
103
115
  rules?: Record<string, RuleSetting>;
104
116
  /** Per-category weights for the combined Health score (flag > config file > default 1 each). */
105
117
  weights?: Partial<Record<Category, number>>;
118
+ /** Restrict analysis to rules in these categories (applied after rules/ignore selection). */
119
+ categories?: Category[];
106
120
  }
107
121
  interface AnalyzeResult {
108
122
  results: Result[];
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  routeMatcher,
10
10
  run,
11
11
  spinnerEnabled
12
- } from "./chunk-ZE3M3T6U.js";
12
+ } from "./chunk-B3RVLTZY.js";
13
13
  export {
14
14
  ProjectError,
15
15
  analyzeProject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "A SvelteKit SEO checker — not a runtime Web Vitals reporter. Static analysis of your routes' head metadata.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "smol-toml": "^1.7.0",
46
46
  "svelte": "^5.56.4",
47
47
  "tinyglobby": "^0.2.17",
48
- "@svelte-vitals/core": "0.20.0"
48
+ "@svelte-vitals/core": "0.22.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^24.13.2"