svelte-vitals 0.26.0 → 0.27.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 CHANGED
@@ -3,17 +3,17 @@
3
3
  [![npm](https://img.shields.io/npm/v/svelte-vitals)](https://www.npmjs.com/package/svelte-vitals)
4
4
  [![MIT](https://img.shields.io/npm/l/svelte-vitals)](https://opensource.org/licenses/MIT)
5
5
 
6
- > **A SvelteKit SEO checker — not a runtime Web Vitals reporter.**
7
- > Diagnose your project's SEO health by statically analyzing your source code, before it ships. No browser, no build server, no headless Chrome.
6
+ > **A static SvelteKit code-health scanner — not a runtime Web Vitals reporter.**
7
+ > Diagnose your project's SEO, Performance, Correctness, Security, and Architecture health by statically analyzing your source code, before it ships. No browser, no build server, no headless Chrome.
8
8
  >
9
- > **ESM-only** (Node 18+). Ships ES modules only; `require()` is unsupported by design.
9
+ > **ESM-only** (Node 22.13+). Ships ES modules only; `require()` is unsupported by design.
10
10
 
11
11
  ```bash
12
12
  npx svelte-vitals@latest
13
13
  ```
14
14
 
15
- > [!NOTE]
16
- > **Early development.** Currently ships static-mode analysis and the first SEO rule (`<title>` presence). More rules, scoring, and a build-time plugin are on the roadmap. Output may change before `1.0`.
15
+ > [!WARNING]
16
+ > **Pre-1.0.** APIs, rule IDs, scoring, and output formats can change at any time, including breaking changes between minor releases.
17
17
 
18
18
  ## Usage
19
19
 
@@ -25,15 +25,31 @@ npx svelte-vitals@latest ./apps/web # or a specific path
25
25
  ```
26
26
 
27
27
  ```
28
- Svelte Vitals · SEO (static mode)
28
+ Svelte Vitals · static mode
29
29
 
30
- Critical (1)
30
+ Health: 76/100
31
+ SEO Score: 51/100 (route avg 61 · site −10)
32
+ Architecture Score: 100/100 (route avg 100)
33
+
34
+ Critical (2)
31
35
  ────────────────────────
32
36
  ✗ SEO001 Missing <title>
33
- /none
34
- src/routes/none/+page.svelte
37
+ /
38
+ src/routes/+page.svelte
39
+ ✗ SEO002 Missing <meta name="description">
40
+ /
41
+ src/routes/+page.svelte
42
+
43
+ Warnings (10)
44
+ ────────────────────────
45
+ ✗ SEO003 Missing <link rel="canonical">
46
+ /
47
+ src/routes/+page.svelte
48
+ …and 1 more
49
+ …and 1 more rule affected — run with --verbose to see all
35
50
 
36
- Passed (3)
51
+ Passed (11)
52
+ ────────────────────────
37
53
 
38
54
  ↯ = set dynamically (verified at runtime).
39
55
  ```
@@ -44,14 +60,34 @@ On an interactive terminal wide enough for the mascot (20+ columns), a small lin
44
60
 
45
61
  ### Exit codes
46
62
 
47
- | Code | Meaning |
48
- | ---- | --------------------------------------------------------------- |
49
- | `0` | No failing findings |
50
- | `1` | A critical finding is present |
51
- | `2` | Execution error (not a SvelteKit project, internal error, etc.) |
63
+ | Code | Meaning |
64
+ | ---- | --------------------------------------------------------------------------- |
65
+ | `0` | No failing findings |
66
+ | `1` | Critical finding present, or `--fail-on` / `--min-health` threshold reached |
67
+ | `2` | Execution error (not a SvelteKit project, internal error, etc.) |
52
68
 
53
69
  Useful as a CI gate.
54
70
 
71
+ ### Reporters
72
+
73
+ `--reporter <fmt>` selects the output format: `console` (default), `json`, `agent`, `sarif`, `github`, `html` (self-contained, `--out-file`), and `md` (compact, for PR comments/job summaries). `agent` auto-selects inside known AI-agent harnesses (e.g. Claude Code); `github` auto-selects under GitHub Actions.
74
+
75
+ ### Ramping up on an existing project
76
+
77
+ `--diff [ref]` / `--staged` scope findings to changed/staged files; `--baseline <ref>` scopes to findings that are genuinely new versus a ref; `--update-suppressions` records today's findings once so only newly-introduced issues gate the build afterward. `--rules`/`--ignore`/`--category` select which rules run, `--weights` reweights the combined Health score, and `--min-health`/`--score` gate or print just the number. A `svelte-vitals.config.{mjs,js,ts}` file (scaffolded via `svelte-vitals install --client config-file`) can set any of these once instead of repeating flags.
78
+
79
+ ### `svelte-vitals install`
80
+
81
+ An interactive wizard that wires up the [MCP server](https://www.npmjs.com/package/@svelte-vitals/mcp), the [Vite plugin](https://www.npmjs.com/package/@svelte-vitals/vite)'s live dashboard, Agent Skills (`/svelte-vitals`, `/improve-svelte`) for Claude Code, Cursor, and Codex, and a GitHub Actions CI workflow — grouped by category in the picker so it's clear what each target is for:
82
+
83
+ ```bash
84
+ npx svelte-vitals@latest install
85
+ ```
86
+
87
+ ### CI integration
88
+
89
+ `svelte-vitals ci install` scaffolds a GitHub Actions workflow around `@svelte-vitals/action` — inline PR annotations, a job summary, and a sticky PR comment, no YAML to hand-write. The same workflow is also a selectable `ci-workflow` target inside `svelte-vitals install`, so it can be set up in the same pass as everything else. See [CI integration](https://oekazuma.github.io/svelte-vitals/guides/ci/).
90
+
55
91
  ### Agent-native output
56
92
 
57
93
  `svelte-vitals --reporter agent` emits a Markdown remediation document an AI coding agent can act on directly: each failing finding lists its location, a concrete fix (with a code snippet), and an acceptance check.
@@ -64,7 +100,7 @@ svelte-vitals resolves the effective `<head>` of every route by walking the layo
64
100
 
65
101
  A dynamic title such as `<title>{data.title}</title>` — the most common, correct SvelteKit pattern — is **never** flagged as missing; it passes with a `↯` marker. Only genuinely missing or empty metadata is penalized.
66
102
 
67
- See the [project README](https://github.com/oekazuma/svelte-vitals#readme) for the full picture and roadmap.
103
+ See the [full documentation](https://oekazuma.github.io/svelte-vitals/) for every flag, rule, and reporter, or the [project README](https://github.com/oekazuma/svelte-vitals#readme) for the full picture and roadmap.
68
104
 
69
105
  ## License
70
106
 
package/dist/bin.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CONFIG_FILENAMES,
3
4
  buildRulesConfig,
4
5
  findUnknownRuleIds,
5
6
  isReporterName,
@@ -7,7 +8,7 @@ import {
7
8
  readCoreVersion,
8
9
  readPackageVersion,
9
10
  run
10
- } from "./chunk-OCLDCX4Y.js";
11
+ } from "./chunk-NRQKNTAB.js";
11
12
 
12
13
  // src/bin.ts
13
14
  import mri3 from "mri";
@@ -183,7 +184,7 @@ import mri from "mri";
183
184
  import * as p from "@clack/prompts";
184
185
 
185
186
  // src/install/index.ts
186
- import { join as join3 } from "path";
187
+ import { join as join4 } from "path";
187
188
 
188
189
  // src/install/clients.ts
189
190
  import { join } from "path";
@@ -292,21 +293,29 @@ function isViteTargetId(id) {
292
293
  var AGENT_TARGETS = [
293
294
  {
294
295
  id: "claude-skill",
295
- label: "Claude Code skill",
296
- hint: "Teaches the agent svelte-vitals rules + when to run the scanner",
297
- relPath: ".claude/skills/svelte-vitals/SKILL.md"
296
+ label: "Agent skill: svelte-vitals",
297
+ hint: "Teaches the agent svelte-vitals rules + when to run the scanner (Claude Code, Codex, Cursor)",
298
+ relPaths: [
299
+ ".claude/skills/svelte-vitals/SKILL.md",
300
+ ".agents/skills/svelte-vitals/SKILL.md",
301
+ ".cursor/skills/svelte-vitals/SKILL.md"
302
+ ]
298
303
  },
299
304
  {
300
305
  id: "cursor-rules",
301
306
  label: "Cursor rules",
302
307
  hint: "Project rules file so Cursor avoids flagged patterns up front",
303
- relPath: ".cursor/rules/svelte-vitals.mdc"
308
+ relPaths: [".cursor/rules/svelte-vitals.mdc"]
304
309
  },
305
310
  {
306
311
  id: "claude-skill-improve",
307
- label: "Claude Code improve-svelte skill",
308
- hint: "Senior-advisor audit \u2192 implementation plans (read-only), for a project-wide improvement roadmap",
309
- relPath: ".claude/skills/improve-svelte/SKILL.md"
312
+ label: "Agent skill: improve-svelte",
313
+ hint: "Senior-advisor audit \u2192 implementation plans (read-only), for a project-wide improvement roadmap (Claude Code, Codex, Cursor)",
314
+ relPaths: [
315
+ ".claude/skills/improve-svelte/SKILL.md",
316
+ ".agents/skills/improve-svelte/SKILL.md",
317
+ ".cursor/skills/improve-svelte/SKILL.md"
318
+ ]
310
319
  }
311
320
  ];
312
321
  function agentTargetById(id) {
@@ -321,8 +330,7 @@ var CONFIG_TARGETS = [
321
330
  {
322
331
  id: "config-file",
323
332
  label: "Config file",
324
- hint: "Scaffolds svelte-vitals.config.mjs with every option commented out",
325
- relPath: "svelte-vitals.config.mjs"
333
+ hint: "Scaffolds svelte-vitals.config.{mjs,ts} (auto-picks the best one) with every option commented out"
326
334
  }
327
335
  ];
328
336
  function configTargetById(id) {
@@ -332,6 +340,60 @@ function isConfigTargetId(id) {
332
340
  return CONFIG_TARGETS.some((t) => t.id === id);
333
341
  }
334
342
 
343
+ // src/ci/workflow.ts
344
+ var WORKFLOW_PATH = ".github/workflows/svelte-vitals.yml";
345
+ var CHECKOUT_SHA = "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0";
346
+ var CHECKOUT_VERSION = "v7.0.0";
347
+ function planWorkflowWrite(existing, force) {
348
+ if (existing === void 0) return { status: "created" };
349
+ if (!force) return { status: "exists" };
350
+ return { status: "updated" };
351
+ }
352
+ function buildWorkflowYaml(opts) {
353
+ const { actionSha, actionVersion } = opts;
354
+ return [
355
+ "# Generated by svelte-vitals (`ci install` or `install --client ci-workflow`).",
356
+ "# Re-run with --force to regenerate.",
357
+ "name: svelte-vitals",
358
+ "",
359
+ "on:",
360
+ " pull_request:",
361
+ "",
362
+ "permissions:",
363
+ " contents: read",
364
+ " pull-requests: write",
365
+ "",
366
+ "jobs:",
367
+ " svelte-vitals:",
368
+ " runs-on: ubuntu-latest",
369
+ " steps:",
370
+ ` - uses: actions/checkout@${CHECKOUT_SHA} # ${CHECKOUT_VERSION}`,
371
+ " with:",
372
+ " fetch-depth: 0",
373
+ ` - uses: oekazuma/svelte-vitals/packages/action@${actionSha} # @svelte-vitals/action@${actionVersion}`,
374
+ " with:",
375
+ " diff: origin/${{ github.base_ref }}",
376
+ " baseline: origin/${{ github.base_ref }}",
377
+ ""
378
+ ].join("\n");
379
+ }
380
+
381
+ // src/install/ci-targets.ts
382
+ var CI_TARGETS = [
383
+ {
384
+ id: "ci-workflow",
385
+ label: "GitHub Actions CI",
386
+ hint: "Scaffolds a workflow that runs @svelte-vitals/action on pull requests \u2014 inline annotations, job summary, sticky PR comment",
387
+ relPath: WORKFLOW_PATH
388
+ }
389
+ ];
390
+ function ciTargetById(id) {
391
+ return CI_TARGETS.find((t) => t.id === id);
392
+ }
393
+ function isCiTargetId(id) {
394
+ return CI_TARGETS.some((t) => t.id === id);
395
+ }
396
+
335
397
  // src/install/skill-content.ts
336
398
  import { allRules, docsUrlFor } from "@svelte-vitals/core";
337
399
  var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture"];
@@ -722,18 +784,71 @@ measure), say so and suggest the runtime check instead of guessing.
722
784
  }
723
785
 
724
786
  // src/install/config-content.ts
725
- function buildConfigFileTemplate() {
726
- return `// svelte-vitals config file \u2014 https://oekazuma.github.io/svelte-vitals/guides/configuration/
727
- export default {
728
- // treatDynamicAs: 'pass', // 'pass' | 'warn' | 'fail' \u2014 how {data.title}-style dynamic values are scored
787
+ function buildConfigFileTemplate(opts = {}) {
788
+ const header = "// svelte-vitals config file \u2014 https://oekazuma.github.io/svelte-vitals/guides/configuration/\n";
789
+ const options = ` // treatDynamicAs: 'pass', // 'pass' | 'warn' | 'fail' \u2014 how {data.title}-style dynamic values are scored
729
790
  // metaComponents: ['Seo'], // component names that resolve SEO tags into <head>
730
791
  // rules: {}, // e.g. { SEO001: 'off' } to disable a rule
731
792
  // failOn: 'critical', // 'critical' | 'warning' | 'info'
732
- // weights: {} // e.g. { seo: 2 } \u2014 per-category weight for the combined Health score
793
+ // weights: {} // e.g. { seo: 2 } \u2014 per-category weight for the combined Health score`;
794
+ if (opts.useDefineConfig) {
795
+ return `${header}import { defineConfig } from 'svelte-vitals';
796
+
797
+ export default defineConfig({
798
+ ${options}
799
+ });
800
+ `;
801
+ }
802
+ if (opts.useCommonJs) {
803
+ return `${header}module.exports = {
804
+ ${options}
805
+ };
806
+ `;
807
+ }
808
+ return `${header}export default {
809
+ ${options}
733
810
  };
734
811
  `;
735
812
  }
736
813
 
814
+ // src/install/config-file-format.ts
815
+ import { join as join2 } from "path";
816
+ function nodeSupportsNativeTypeScript(version) {
817
+ const match = /^v?(\d+)\.(\d+)/.exec(version);
818
+ if (!match) return false;
819
+ const major = Number(match[1]);
820
+ const minor = Number(match[2]);
821
+ return major > 23 || major === 23 && minor >= 6 || major === 22 && minor >= 18;
822
+ }
823
+ function findExistingConfigFile(readFile, cwd) {
824
+ return CONFIG_FILENAMES.find((rel) => readFile(join2(cwd, rel)) !== void 0);
825
+ }
826
+ function hasSvelteVitalsDependency(readFile, cwd) {
827
+ const raw = readFile(join2(cwd, "package.json"));
828
+ if (raw === void 0) return false;
829
+ try {
830
+ const pkg = JSON.parse(raw);
831
+ return Boolean(pkg.dependencies?.["svelte-vitals"] ?? pkg.devDependencies?.["svelte-vitals"]);
832
+ } catch {
833
+ return false;
834
+ }
835
+ }
836
+ function isEsmProject(readFile, cwd) {
837
+ const raw = readFile(join2(cwd, "package.json"));
838
+ if (raw === void 0) return false;
839
+ try {
840
+ return JSON.parse(raw).type === "module";
841
+ } catch {
842
+ return false;
843
+ }
844
+ }
845
+ function detectBestConfigExtension(opts) {
846
+ if (!nodeSupportsNativeTypeScript(opts.nodeVersion)) return "mjs";
847
+ const looksTypeScript = opts.readFile(join2(opts.cwd, "tsconfig.json")) !== void 0 || opts.readFile(join2(opts.cwd, "vite.config.ts")) !== void 0;
848
+ if (!looksTypeScript) return "mjs";
849
+ return hasSvelteVitalsDependency(opts.readFile, opts.cwd) ? "ts" : "mjs";
850
+ }
851
+
737
852
  // src/install/codemod-vite-config.ts
738
853
  import { parseModule, generateCode, builders, MagicastError } from "magicast";
739
854
  var MANUAL_SNIPPET = `import { svelteVitals } from '@svelte-vitals/vite';
@@ -840,7 +955,7 @@ function codemodHooksServer(existing) {
840
955
  }
841
956
 
842
957
  // src/install/package-manager.ts
843
- import { join as join2 } from "path";
958
+ import { join as join3 } from "path";
844
959
  var LOCKFILE_TO_PM = {
845
960
  "pnpm-lock.yaml": "pnpm",
846
961
  "yarn.lock": "yarn",
@@ -849,12 +964,12 @@ var LOCKFILE_TO_PM = {
849
964
  };
850
965
  function detectPackageManager(io) {
851
966
  for (const [file, pm] of Object.entries(LOCKFILE_TO_PM)) {
852
- if (io.readFile(join2(io.cwd, file)) !== void 0) return pm;
967
+ if (io.readFile(join3(io.cwd, file)) !== void 0) return pm;
853
968
  }
854
969
  return "npm";
855
970
  }
856
971
  function hasVitePackage(io) {
857
- const raw = io.readFile(join2(io.cwd, "package.json"));
972
+ const raw = io.readFile(join3(io.cwd, "package.json"));
858
973
  if (raw === void 0) return false;
859
974
  try {
860
975
  const pkg = JSON.parse(raw);
@@ -868,7 +983,7 @@ function installCommand(pm) {
868
983
  return { command: pm, args: [action, "-D", "@svelte-vitals/vite"] };
869
984
  }
870
985
  function readInstalledViteVersion(io) {
871
- const raw = io.readFile(join2(io.cwd, "node_modules/@svelte-vitals/vite/package.json"));
986
+ const raw = io.readFile(join3(io.cwd, "node_modules/@svelte-vitals/vite/package.json"));
872
987
  if (raw === void 0) return void 0;
873
988
  try {
874
989
  return JSON.parse(raw).version;
@@ -877,6 +992,10 @@ function readInstalledViteVersion(io) {
877
992
  }
878
993
  }
879
994
 
995
+ // src/ci/action-pin.generated.ts
996
+ var ACTION_SHA = "3cf64af159479578663195757c5740629e32ac94";
997
+ var ACTION_VERSION = "0.3.2";
998
+
880
999
  // src/install/index.ts
881
1000
  function planForClient(client, scope, io, force) {
882
1001
  const path = client.resolvePath(scope, io.cwd, io.home);
@@ -886,11 +1005,11 @@ function planForClient(client, scope, io, force) {
886
1005
  }
887
1006
  function resolveCandidate(io, candidates) {
888
1007
  for (const rel of candidates) {
889
- const path = join3(io.cwd, rel);
1008
+ const path = join4(io.cwd, rel);
890
1009
  const content = io.readFile(path);
891
1010
  if (content !== void 0) return { path, content };
892
1011
  }
893
- return { path: join3(io.cwd, candidates[0]), content: void 0 };
1012
+ return { path: join4(io.cwd, candidates[0]), content: void 0 };
894
1013
  }
895
1014
  function planForVitePlugin(io) {
896
1015
  const { path, content } = resolveCandidate(io, ["vite.config.ts", "vite.config.js", "vite.config.mjs"]);
@@ -917,18 +1036,40 @@ function agentTargetContent(id, version) {
917
1036
  }
918
1037
  }
919
1038
  function planForAgentTarget(target, io, force, version) {
920
- const path = join3(io.cwd, target.relPath);
921
- const existing = io.readFile(path);
922
1039
  const content = agentTargetContent(target.id, version);
923
- const status = existing === void 0 ? "created" : force ? "updated" : "exists";
924
- return { id: target.id, label: target.label, path, status, content };
1040
+ return target.relPaths.map((relPath) => {
1041
+ const path = join4(io.cwd, relPath);
1042
+ const existing = io.readFile(path);
1043
+ const status = existing === void 0 ? "created" : force ? "updated" : "exists";
1044
+ return { id: target.id, label: target.label, path, status, content };
1045
+ });
925
1046
  }
926
1047
  function planForConfigTarget(target, io, force) {
927
- const path = join3(io.cwd, target.relPath);
1048
+ const existingRel = findExistingConfigFile(io.readFile, io.cwd);
1049
+ if (existingRel !== void 0) {
1050
+ const path2 = join4(io.cwd, existingRel);
1051
+ const status = force ? "updated" : "exists";
1052
+ const content2 = force ? buildConfigFileTemplate({
1053
+ useDefineConfig: existingRel.endsWith(".ts") && hasSvelteVitalsDependency(io.readFile, io.cwd),
1054
+ useCommonJs: existingRel.endsWith(".js") && !isEsmProject(io.readFile, io.cwd)
1055
+ }) : void 0;
1056
+ return { id: target.id, label: target.label, path: path2, status, content: content2 };
1057
+ }
1058
+ const ext = detectBestConfigExtension({
1059
+ readFile: io.readFile,
1060
+ cwd: io.cwd,
1061
+ nodeVersion: io.nodeVersion ?? process.version
1062
+ });
1063
+ const path = join4(io.cwd, `svelte-vitals.config.${ext}`);
1064
+ const content = buildConfigFileTemplate({ useDefineConfig: ext === "ts" });
1065
+ return { id: target.id, label: target.label, path, status: "created", content };
1066
+ }
1067
+ function planForCiTarget(target, io, force) {
1068
+ const path = join4(io.cwd, target.relPath);
928
1069
  const existing = io.readFile(path);
929
- const content = buildConfigFileTemplate();
930
- const status = existing === void 0 ? "created" : force ? "updated" : "exists";
931
- return { id: target.id, label: target.label, path, status, content };
1070
+ const plan = planWorkflowWrite(existing, force);
1071
+ const content = plan.status === "exists" ? void 0 : buildWorkflowYaml({ actionSha: ACTION_SHA, actionVersion: ACTION_VERSION });
1072
+ return { id: target.id, label: target.label, path, status: plan.status, content };
932
1073
  }
933
1074
  function indent(text) {
934
1075
  return text.split("\n").map((l) => ` ${l}`).join("\n");
@@ -942,19 +1083,16 @@ async function runRefresh(io, flags, version) {
942
1083
  let hadFailure = false;
943
1084
  const rows = [];
944
1085
  for (const target of AGENT_TARGETS) {
945
- const path = join3(io.cwd, target.relPath);
946
- try {
947
- if (io.readFile(path) === void 0) continue;
948
- rows.push(planForAgentTarget(
949
- target,
950
- io,
951
- /* force */
952
- true,
953
- version
954
- ));
955
- } catch (err) {
956
- hadFailure = true;
957
- io.errorLog(`svelte-vitals: failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`);
1086
+ const content = agentTargetContent(target.id, version);
1087
+ for (const relPath of target.relPaths) {
1088
+ const path = join4(io.cwd, relPath);
1089
+ try {
1090
+ if (io.readFile(path) === void 0) continue;
1091
+ rows.push({ id: target.id, label: target.label, path, status: "updated", content });
1092
+ } catch (err) {
1093
+ hadFailure = true;
1094
+ io.errorLog(`svelte-vitals: failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`);
1095
+ }
958
1096
  }
959
1097
  }
960
1098
  if (rows.length === 0) {
@@ -1004,26 +1142,31 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
1004
1142
  (c) => c.scopes.some((s) => configExists(c.resolvePath(s, io.cwd, io.home)))
1005
1143
  ).map((c) => c.id);
1006
1144
  const viteConfigExists = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].some(
1007
- (f) => configExists(join3(io.cwd, f))
1145
+ (f) => configExists(join4(io.cwd, f))
1008
1146
  );
1009
- const claudeSkillDetected = configExists(join3(io.cwd, ".claude", "settings.json"));
1010
- const cursorRulesDetected = configExists(join3(io.cwd, ".cursor", "mcp.json"));
1147
+ const claudeSkillDetected = configExists(join4(io.cwd, ".claude", "settings.json"));
1148
+ const cursorRulesDetected = configExists(join4(io.cwd, ".cursor", "mcp.json"));
1011
1149
  const detectedAgents = [
1012
1150
  ...claudeSkillDetected ? ["claude-skill"] : [],
1013
1151
  ...cursorRulesDetected ? ["cursor-rules"] : []
1014
1152
  ];
1153
+ const ciWorkflowDetected = configExists(join4(io.cwd, CI_TARGETS[0].relPath));
1154
+ const configFileDetected = findExistingConfigFile((p3) => configExists(p3) ? "" : void 0, io.cwd) !== void 0;
1015
1155
  const detected = [
1016
1156
  ...detectedClients,
1017
1157
  ...viteConfigExists ? VITE_TARGETS.map((t) => t.id) : [],
1018
- ...detectedAgents
1019
- ];
1020
- const options = [
1021
- ...CLIENTS.map((c) => ({ id: c.id, label: c.label })),
1022
- ...VITE_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
1023
- ...AGENT_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
1024
- ...CONFIG_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
1158
+ ...detectedAgents,
1159
+ ...ciWorkflowDetected ? CI_TARGETS.map((t) => t.id) : [],
1160
+ ...configFileDetected ? CONFIG_TARGETS.map((t) => t.id) : []
1025
1161
  ];
1026
- const picked = await prompts.selectClients(options, detected);
1162
+ const groups = {
1163
+ "MCP server": CLIENTS.map((c) => ({ id: c.id, label: c.label })),
1164
+ "Vite integration": VITE_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
1165
+ "Agent Skills & rules": AGENT_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
1166
+ "CI (GitHub Actions)": CI_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
1167
+ "Config file": CONFIG_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
1168
+ };
1169
+ const picked = await prompts.selectClients(groups, detected);
1027
1170
  if (picked === null) {
1028
1171
  io.log("Cancelled.");
1029
1172
  return 0;
@@ -1031,7 +1174,7 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
1031
1174
  ids = picked;
1032
1175
  } else {
1033
1176
  io.errorLog(
1034
- "svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-hooks,claude-skill,cursor-rules,claude-skill-improve,config-file> to install non-interactively."
1177
+ "svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-hooks,claude-skill,cursor-rules,claude-skill-improve,config-file,ci-workflow> to install non-interactively."
1035
1178
  );
1036
1179
  return 2;
1037
1180
  }
@@ -1039,7 +1182,8 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
1039
1182
  const viteIds = ids.filter(isViteTargetId);
1040
1183
  const agentIds = ids.filter(isAgentTargetId);
1041
1184
  const configIds = ids.filter(isConfigTargetId);
1042
- if (clients.length === 0 && viteIds.length === 0 && agentIds.length === 0 && configIds.length === 0) {
1185
+ const ciIds = ids.filter(isCiTargetId);
1186
+ if (clients.length === 0 && viteIds.length === 0 && agentIds.length === 0 && configIds.length === 0 && ciIds.length === 0) {
1043
1187
  io.errorLog("svelte-vitals: no valid clients or targets selected.");
1044
1188
  return 2;
1045
1189
  }
@@ -1075,11 +1219,36 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
1075
1219
  }
1076
1220
  for (const agentId of agentIds) {
1077
1221
  const target = agentTargetById(agentId);
1078
- rows.push(planForAgentTarget(target, io, flags.force ?? false, version));
1222
+ try {
1223
+ rows.push(...planForAgentTarget(target, io, flags.force ?? false, version));
1224
+ } catch (err) {
1225
+ io.errorLog(
1226
+ `svelte-vitals: could not check existing agent target ${target.id}: ${err instanceof Error ? err.message : String(err)}`
1227
+ );
1228
+ return 2;
1229
+ }
1079
1230
  }
1080
1231
  for (const configId of configIds) {
1081
1232
  const target = configTargetById(configId);
1082
- rows.push(planForConfigTarget(target, io, flags.force ?? false));
1233
+ try {
1234
+ rows.push(planForConfigTarget(target, io, flags.force ?? false));
1235
+ } catch (err) {
1236
+ io.errorLog(
1237
+ `svelte-vitals: could not check existing config file: ${err instanceof Error ? err.message : String(err)}`
1238
+ );
1239
+ return 2;
1240
+ }
1241
+ }
1242
+ for (const ciId of ciIds) {
1243
+ const target = ciTargetById(ciId);
1244
+ try {
1245
+ rows.push(planForCiTarget(target, io, flags.force ?? false));
1246
+ } catch (err) {
1247
+ io.errorLog(
1248
+ `svelte-vitals: could not check existing workflow at ${join4(io.cwd, target.relPath)}: ${err instanceof Error ? err.message : String(err)}`
1249
+ );
1250
+ return 2;
1251
+ }
1083
1252
  }
1084
1253
  const planText = rows.map(rowLine).join("\n");
1085
1254
  io.log("Plan:");
@@ -1112,6 +1281,11 @@ ${indent(r.snippet ?? "")}`);
1112
1281
  io.writeFile(r.path, r.content ?? "");
1113
1282
  io.log(`\u2713 ${r.label}: ${r.status} ${r.path}`);
1114
1283
  if (isViteTargetId(r.id)) viteWasWritten = true;
1284
+ if (isConfigTargetId(r.id) && r.path.endsWith(".ts")) {
1285
+ io.log(
1286
+ "svelte-vitals: note \u2014 a .ts config needs Node 22.18+ (or 23.6+) everywhere svelte-vitals runs, CI included; rename to .mjs if that is not guaranteed."
1287
+ );
1288
+ }
1115
1289
  } catch (err) {
1116
1290
  hadFailure = true;
1117
1291
  io.errorLog(`svelte-vitals: failed to write ${r.path}: ${err instanceof Error ? err.message : String(err)}`);
@@ -1145,7 +1319,9 @@ ${indent(r.snippet ?? "")}`);
1145
1319
  var VALID_TARGETS = [
1146
1320
  ...CLIENTS.map((c) => c.id),
1147
1321
  ...VITE_TARGETS.map((t) => t.id),
1148
- ...AGENT_TARGETS.map((t) => t.id)
1322
+ ...AGENT_TARGETS.map((t) => t.id),
1323
+ ...CONFIG_TARGETS.map((t) => t.id),
1324
+ ...CI_TARGETS.map((t) => t.id)
1149
1325
  ];
1150
1326
  var EXPECTED_TARGETS = VALID_TARGETS.join("|");
1151
1327
  function resolveInstallArgs(argv) {
@@ -1192,26 +1368,37 @@ function resolveInstallArgs(argv) {
1192
1368
  }
1193
1369
 
1194
1370
  // src/install/cli.ts
1195
- var INSTALL_HELP = `svelte-vitals install \u2014 set up the svelte-vitals MCP server, Vite integration, and agent skills/rules
1371
+ var INSTALL_HELP = `svelte-vitals install \u2014 set up the svelte-vitals MCP server, Vite integration, agent skills/rules, and CI
1196
1372
 
1197
1373
  Usage:
1198
1374
  svelte-vitals install [options]
1199
1375
 
1200
1376
  Options:
1201
- --client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-hooks,claude-skill,cursor-rules,claude-skill-improve,config-file
1202
- (skips the interactive picker)
1377
+ --client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-hooks,claude-skill,cursor-rules,claude-skill-improve,config-file,ci-workflow
1378
+ (skips the interactive picker; the picker groups these by category \u2014
1379
+ MCP server, Vite integration, Agent Skills & rules, CI, Config file)
1203
1380
  vite-plugin registers the build-mode plugin in vite.config.{ts,js,mjs}; vite-hooks
1204
1381
  wires up the svelteVitalsHandle hook in src/hooks.server.{ts,js}, which improves the
1205
1382
  live dashboard's per-route accuracy as you browse. --force does not apply
1206
1383
  to either of these two \u2014 an existing registration is always left as-is.
1207
- claude-skill writes a Claude Code skill (.claude/skills/svelte-vitals/SKILL.md); cursor-rules
1208
- writes a Cursor rules file (.cursor/rules/svelte-vitals.mdc). Both are generated from the
1209
- current rule set and support --force to regenerate.
1210
- claude-skill-improve writes a second, read-only Claude Code skill
1211
- (.claude/skills/improve-svelte/SKILL.md) that audits the whole project and writes
1212
- implementation plans instead of a run-after-every-edit playbook; also supports --force.
1213
- config-file scaffolds svelte-vitals.config.mjs with every option commented out;
1214
- supports --force to regenerate.
1384
+ claude-skill writes an agent skill (Claude Code, Codex, and Cursor \u2014
1385
+ .claude/skills/, .agents/skills/, and .cursor/skills/ under svelte-vitals/);
1386
+ cursor-rules writes a Cursor rules file (.cursor/rules/svelte-vitals.mdc).
1387
+ Both are generated from the current rule set and support --force to regenerate.
1388
+ claude-skill-improve writes a second, read-only agent skill (same three
1389
+ locations, under improve-svelte/) that audits the whole project and writes
1390
+ implementation plans instead of a run-after-every-edit playbook; also
1391
+ supports --force.
1392
+ config-file scaffolds svelte-vitals.config.{mjs,ts} with every option commented
1393
+ out, auto-picking .ts (with defineConfig) when the current Node supports it, the
1394
+ project looks TypeScript-oriented (tsconfig.json or vite.config.ts present), and
1395
+ svelte-vitals is a declared dependency (defineConfig's import resolves at load
1396
+ time); else the safe .mjs default. Supports --force to regenerate the file
1397
+ that's already there (its extension never changes on --force).
1398
+ ci-workflow scaffolds .github/workflows/svelte-vitals.yml, the same file
1399
+ \`svelte-vitals ci install\` writes standalone \u2014 pick it here to set it up in
1400
+ the same pass as everything else; supports --force to regenerate. \`svelte-vitals
1401
+ ci upgrade\` remains the way to bump an existing workflow's pinned action version.
1215
1402
  --scope <scope> project | global (applies to all selected clients; codex is always global)
1216
1403
  --yes, -y Skip the confirmation prompt
1217
1404
  --dry-run Print the planned changes and exit without writing
@@ -1237,6 +1424,7 @@ function realIO() {
1237
1424
  cwd: process.cwd(),
1238
1425
  home: homedir(),
1239
1426
  isTTY: Boolean(process.stdout.isTTY),
1427
+ nodeVersion: process.version,
1240
1428
  log: (line) => console.log(line),
1241
1429
  errorLog: (line) => console.error(line),
1242
1430
  runCommand: (command, args, cwd) => {
@@ -1260,10 +1448,15 @@ function realIO() {
1260
1448
  }
1261
1449
  function clackPrompts() {
1262
1450
  return {
1263
- selectClients: async (all, defaults) => {
1264
- const res = await p.multiselect({
1451
+ selectClients: async (groups, defaults) => {
1452
+ const res = await p.groupMultiselect({
1265
1453
  message: "Which clients/targets should svelte-vitals be installed for?",
1266
- options: all.map((o) => ({ value: o.id, label: o.label, hint: o.hint })),
1454
+ options: Object.fromEntries(
1455
+ Object.entries(groups).map(([group, opts]) => [
1456
+ group,
1457
+ opts.map((o) => ({ value: o.id, label: o.label, hint: o.hint }))
1458
+ ])
1459
+ ),
1267
1460
  initialValues: defaults,
1268
1461
  required: true
1269
1462
  });
@@ -1302,47 +1495,9 @@ async function runInstallCli(args) {
1302
1495
  }
1303
1496
 
1304
1497
  // src/ci/cli.ts
1305
- import { join as join4 } from "path";
1498
+ import { join as join5 } from "path";
1306
1499
  import mri2 from "mri";
1307
1500
 
1308
- // src/ci/workflow.ts
1309
- var WORKFLOW_PATH = ".github/workflows/svelte-vitals.yml";
1310
- var CHECKOUT_SHA = "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0";
1311
- var CHECKOUT_VERSION = "v7.0.0";
1312
- function planWorkflowWrite(existing, force) {
1313
- if (existing === void 0) return { status: "created" };
1314
- if (!force) return { status: "exists" };
1315
- return { status: "updated" };
1316
- }
1317
- function buildWorkflowYaml(opts) {
1318
- const { actionSha, actionVersion } = opts;
1319
- return [
1320
- "# Generated by `svelte-vitals ci install`.",
1321
- "# Re-run with --force to regenerate.",
1322
- "name: svelte-vitals",
1323
- "",
1324
- "on:",
1325
- " pull_request:",
1326
- "",
1327
- "permissions:",
1328
- " contents: read",
1329
- " pull-requests: write",
1330
- "",
1331
- "jobs:",
1332
- " svelte-vitals:",
1333
- " runs-on: ubuntu-latest",
1334
- " steps:",
1335
- ` - uses: actions/checkout@${CHECKOUT_SHA} # ${CHECKOUT_VERSION}`,
1336
- " with:",
1337
- " fetch-depth: 0",
1338
- ` - uses: oekazuma/svelte-vitals/packages/action@${actionSha} # @svelte-vitals/action@${actionVersion}`,
1339
- " with:",
1340
- " diff: origin/${{ github.base_ref }}",
1341
- " baseline: origin/${{ github.base_ref }}",
1342
- ""
1343
- ].join("\n");
1344
- }
1345
-
1346
1501
  // src/ci/upgrade.ts
1347
1502
  var ACTION_USES_LINE = /^(?<indent>\s*-\s*uses:\s*(?:&\S+\s+)?oekazuma\/svelte-vitals\/packages\/action@)(?<ref>[^\s#]+)(?<comment>\s*#.*)?$/;
1348
1503
  function upgradeActionPin(content, sha, version) {
@@ -1371,10 +1526,6 @@ function upgradeActionPin(content, sha, version) {
1371
1526
  return { status: "upgraded", content: next.join("\n"), replaced, from };
1372
1527
  }
1373
1528
 
1374
- // src/ci/action-pin.generated.ts
1375
- var ACTION_SHA = "e30438c8facc8aabac6d4ce00455e29ec02ad9af";
1376
- var ACTION_VERSION = "0.3.1";
1377
-
1378
1529
  // src/ci/cli.ts
1379
1530
  var CI_HELP = `svelte-vitals ci \u2014 scaffold CI integration
1380
1531
 
@@ -1416,7 +1567,7 @@ async function runCiCli(args, io = realIO()) {
1416
1567
  io.log(CI_HELP);
1417
1568
  return 0;
1418
1569
  }
1419
- const path = join4(io.cwd, WORKFLOW_PATH);
1570
+ const path = join5(io.cwd, WORKFLOW_PATH);
1420
1571
  const existing = io.readFile(path);
1421
1572
  const plan = planWorkflowWrite(existing, Boolean(argv.force));
1422
1573
  io.log("Plan:");
@@ -1450,7 +1601,7 @@ async function runCiUpgrade(args, io) {
1450
1601
  io.log(CI_HELP);
1451
1602
  return 0;
1452
1603
  }
1453
- const path = join4(io.cwd, WORKFLOW_PATH);
1604
+ const path = join5(io.cwd, WORKFLOW_PATH);
1454
1605
  const existing = io.readFile(path);
1455
1606
  if (existing === void 0) {
1456
1607
  io.errorLog(`svelte-vitals: no ${WORKFLOW_PATH} found \u2014 run \`svelte-vitals ci install\` first.`);
@@ -1552,6 +1552,7 @@ export {
1552
1552
  findUnknownRuleIds,
1553
1553
  knownRuleIds,
1554
1554
  buildRulesConfig,
1555
+ CONFIG_FILENAMES,
1555
1556
  loadConfigFile,
1556
1557
  spinnerEnabled,
1557
1558
  routeMatcher,
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  routeMatcher,
11
11
  run,
12
12
  spinnerEnabled
13
- } from "./chunk-OCLDCX4Y.js";
13
+ } from "./chunk-NRQKNTAB.js";
14
14
  export {
15
15
  ProjectError,
16
16
  analyzeProject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.26.0",
3
+ "version": "0.27.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",