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 +52 -16
- package/dist/bin.js +269 -118
- package/dist/{chunk-OCLDCX4Y.js → chunk-NRQKNTAB.js} +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/svelte-vitals)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
> **A SvelteKit
|
|
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
|
|
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
|
-
> [!
|
|
16
|
-
> **
|
|
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 ·
|
|
28
|
+
Svelte Vitals · static mode
|
|
29
29
|
|
|
30
|
-
|
|
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
|
-
/
|
|
34
|
-
src/routes
|
|
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 (
|
|
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` |
|
|
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-
|
|
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
|
|
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: "
|
|
296
|
-
hint: "Teaches the agent svelte-vitals rules + when to run the scanner",
|
|
297
|
-
|
|
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
|
-
|
|
308
|
+
relPaths: [".cursor/rules/svelte-vitals.mdc"]
|
|
304
309
|
},
|
|
305
310
|
{
|
|
306
311
|
id: "claude-skill-improve",
|
|
307
|
-
label: "
|
|
308
|
-
hint: "Senior-advisor audit \u2192 implementation plans (read-only), for a project-wide improvement roadmap",
|
|
309
|
-
|
|
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
|
-
|
|
727
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
924
|
-
|
|
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
|
|
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
|
|
930
|
-
const
|
|
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
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
true
|
|
953
|
-
|
|
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(
|
|
1145
|
+
(f) => configExists(join4(io.cwd, f))
|
|
1008
1146
|
);
|
|
1009
|
-
const claudeSkillDetected = configExists(
|
|
1010
|
-
const cursorRulesDetected = configExists(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
supports --force
|
|
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 (
|
|
1264
|
-
const res = await p.
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
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.`);
|
package/dist/index.js
CHANGED
package/package.json
CHANGED