@savvy-web/lint-staged 1.0.1 → 1.2.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/{878.js → 841.js} +111 -164
- package/README.md +28 -28
- package/bin/savvy-lint.js +1 -1
- package/index.d.ts +15 -8
- package/index.js +2 -2
- package/package.json +10 -10
package/{878.js → 841.js}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Command, Options } from "@effect/cli";
|
|
2
2
|
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
3
|
-
import { BiomeSchemaSync, BiomeSchemaSyncLive, CheckResult, ConfigDiscovery, ConfigDiscoveryLive, ManagedSection, ManagedSectionLive,
|
|
3
|
+
import { BiomeSchemaSync, BiomeSchemaSyncLive, CheckResult, ConfigDiscovery, ConfigDiscoveryLive, ManagedSection, ManagedSectionLive, SavvyBaseSection, SavvyHooksSection, SectionDefinition, ToolDefinition, ToolDiscovery, ToolDiscoveryLive, savvyBasePreamble, savvyHooksHygiene, savvyToolSection } from "@savvy-web/silk-effects";
|
|
4
4
|
import { Effect, Layer } from "effect";
|
|
5
5
|
import { WorkspacesLive, findWorkspaceRootSync, getWorkspacePackagesSync } from "workspaces-effect";
|
|
6
6
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -299,69 +299,18 @@ const POST_CHECKOUT_HOOK_PATH = ".husky/post-checkout";
|
|
|
299
299
|
const POST_MERGE_HOOK_PATH = ".husky/post-merge";
|
|
300
300
|
const DEFAULT_CONFIG_PATH = "lib/configs/lint-staged.config.ts";
|
|
301
301
|
const MARKDOWNLINT_CONFIG_PATH = "lib/configs/.markdownlint-cli2.jsonc";
|
|
302
|
-
const SavvyLintSection = ShellSectionDefinition.make({
|
|
303
|
-
toolName: "SAVVY-LINT"
|
|
304
|
-
});
|
|
305
302
|
const SavvyLintSectionDef = SectionDefinition.make({
|
|
306
|
-
toolName: "
|
|
303
|
+
toolName: "savvy-lint"
|
|
307
304
|
});
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
# Get repo root directory
|
|
314
|
-
ROOT=$(git rev-parse --show-toplevel)
|
|
315
|
-
|
|
316
|
-
# Detect package manager from package.json or lockfiles
|
|
317
|
-
detect_pm() {
|
|
318
|
-
# Check packageManager field in package.json (e.g., "pnpm@9.0.0")
|
|
319
|
-
if [ -f "$ROOT/package.json" ]; then
|
|
320
|
-
pm=$(jq -r '.packageManager // empty' "$ROOT/package.json" 2>/dev/null | cut -d'@' -f1)
|
|
321
|
-
if [ -n "$pm" ]; then
|
|
322
|
-
echo "$pm"
|
|
323
|
-
return
|
|
324
|
-
fi
|
|
325
|
-
fi
|
|
326
|
-
|
|
327
|
-
# Fallback to lockfile detection
|
|
328
|
-
if [ -f "$ROOT/pnpm-lock.yaml" ]; then
|
|
329
|
-
echo "pnpm"
|
|
330
|
-
elif [ -f "$ROOT/yarn.lock" ]; then
|
|
331
|
-
echo "yarn"
|
|
332
|
-
elif [ -f "$ROOT/bun.lock" ]; then
|
|
333
|
-
echo "bun"
|
|
334
|
-
else
|
|
335
|
-
echo "npm"
|
|
336
|
-
fi
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
# Run lint-staged with the detected package manager
|
|
340
|
-
PM=$(detect_pm)
|
|
341
|
-
case "$PM" in
|
|
342
|
-
pnpm) pnpm exec lint-staged --config "$ROOT/${configPath}" ;;
|
|
343
|
-
yarn) yarn exec lint-staged --config "$ROOT/${configPath}" ;;
|
|
344
|
-
bun) bunx lint-staged --config "$ROOT/${configPath}" ;;
|
|
345
|
-
*) npx --no -- lint-staged --config "$ROOT/${configPath}" ;;
|
|
346
|
-
esac
|
|
347
|
-
|
|
348
|
-
fi`;
|
|
305
|
+
const LegacySavvyLintHygieneDef = SectionDefinition.make({
|
|
306
|
+
toolName: "savvy-lint"
|
|
307
|
+
});
|
|
308
|
+
function lintStagedCommand(configPath) {
|
|
309
|
+
return `lint-staged --config "$ROOT/${configPath}"`;
|
|
349
310
|
}
|
|
350
|
-
function
|
|
351
|
-
return
|
|
352
|
-
if ! { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; }; then
|
|
353
|
-
|
|
354
|
-
# Configure git to ignore executable bit changes
|
|
355
|
-
# This ensures hook scripts can be made executable locally without git tracking the change
|
|
356
|
-
git config core.fileMode false
|
|
357
|
-
|
|
358
|
-
# Ensure all shell scripts tracked by git are executable
|
|
359
|
-
git ls-files -z '*.sh' | xargs -0 -r chmod +x 2>/dev/null || true
|
|
360
|
-
|
|
361
|
-
fi`;
|
|
311
|
+
function savvyLintBlock(configPath) {
|
|
312
|
+
return savvyToolSection("savvy-lint", lintStagedCommand(configPath));
|
|
362
313
|
}
|
|
363
|
-
const preCommitBlock = SavvyLintSection.generate(generateManagedContent);
|
|
364
|
-
const shellScriptsBlock = ()=>SavvyLintSection.block(generateShellScriptsManagedContent());
|
|
365
314
|
const MARKDOWNLINT_TEMPLATE = {
|
|
366
315
|
$schema: "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.22.0/schema/markdownlint-cli2-config-schema.json",
|
|
367
316
|
globs: [
|
|
@@ -547,10 +496,10 @@ const MARKDOWNLINT_CONFIG = {
|
|
|
547
496
|
"changeset-uncategorized-content": false,
|
|
548
497
|
"changeset-dependency-table-format": false
|
|
549
498
|
};
|
|
550
|
-
const CHECK_MARK = "
|
|
551
|
-
const CROSS_MARK = "
|
|
552
|
-
const WARNING = "
|
|
553
|
-
const BULLET = "
|
|
499
|
+
const CHECK_MARK = "✓";
|
|
500
|
+
const CROSS_MARK = "✗";
|
|
501
|
+
const WARNING = "⚠";
|
|
502
|
+
const BULLET = "•";
|
|
554
503
|
const CONFIG_FILES = [
|
|
555
504
|
"lint-staged.config.ts",
|
|
556
505
|
"lint-staged.config.js",
|
|
@@ -588,56 +537,6 @@ function extractConfigPathFromManaged(managedContent) {
|
|
|
588
537
|
const match = managedContent.match(/lint-staged --config "\$ROOT\/([^"]+)"/);
|
|
589
538
|
return match ? match[1] : null;
|
|
590
539
|
}
|
|
591
|
-
function checkHookManagedSection(section, hookPath, block) {
|
|
592
|
-
return Effect.gen(function*() {
|
|
593
|
-
const result = yield* section.check(hookPath, block);
|
|
594
|
-
return CheckResult.$match(result, {
|
|
595
|
-
Found: ({ isUpToDate })=>({
|
|
596
|
-
found: true,
|
|
597
|
-
isUpToDate,
|
|
598
|
-
needsUpdate: !isUpToDate
|
|
599
|
-
}),
|
|
600
|
-
NotFound: ()=>({
|
|
601
|
-
found: false,
|
|
602
|
-
isUpToDate: false,
|
|
603
|
-
needsUpdate: false
|
|
604
|
-
})
|
|
605
|
-
});
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
function checkManagedSectionStatus(section, hookPath) {
|
|
609
|
-
return Effect.gen(function*() {
|
|
610
|
-
const existing = yield* section.read(hookPath, SavvyLintSectionDef);
|
|
611
|
-
if (null === existing) return {
|
|
612
|
-
isUpToDate: false,
|
|
613
|
-
configPath: null,
|
|
614
|
-
needsUpdate: false,
|
|
615
|
-
found: false
|
|
616
|
-
};
|
|
617
|
-
const configPath = extractConfigPathFromManaged(existing.text);
|
|
618
|
-
if (!configPath) return {
|
|
619
|
-
isUpToDate: false,
|
|
620
|
-
configPath: null,
|
|
621
|
-
needsUpdate: true,
|
|
622
|
-
found: true
|
|
623
|
-
};
|
|
624
|
-
const result = yield* section.check(hookPath, preCommitBlock(configPath));
|
|
625
|
-
return CheckResult.$match(result, {
|
|
626
|
-
Found: ({ isUpToDate })=>({
|
|
627
|
-
isUpToDate,
|
|
628
|
-
configPath: configPath,
|
|
629
|
-
needsUpdate: !isUpToDate,
|
|
630
|
-
found: true
|
|
631
|
-
}),
|
|
632
|
-
NotFound: ()=>({
|
|
633
|
-
isUpToDate: false,
|
|
634
|
-
configPath: null,
|
|
635
|
-
needsUpdate: false,
|
|
636
|
-
found: false
|
|
637
|
-
})
|
|
638
|
-
});
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
540
|
function checkMarkdownlintConfig(content) {
|
|
642
541
|
return Effect.gen(function*() {
|
|
643
542
|
const parsed = yield* parse(content);
|
|
@@ -654,7 +553,7 @@ function checkMarkdownlintConfig(content) {
|
|
|
654
553
|
}
|
|
655
554
|
function checkBiomeSchemas() {
|
|
656
555
|
return Effect.gen(function*() {
|
|
657
|
-
const version = "2.4.
|
|
556
|
+
const version = "2.4.16";
|
|
658
557
|
const statuses = [];
|
|
659
558
|
if (!version) return {
|
|
660
559
|
statuses,
|
|
@@ -691,23 +590,45 @@ const checkCommand = Command.make("check", {
|
|
|
691
590
|
quiet: quietOption
|
|
692
591
|
}, ({ quiet })=>Effect.gen(function*() {
|
|
693
592
|
const fs = yield* FileSystem.FileSystem;
|
|
694
|
-
const
|
|
593
|
+
const ms = yield* ManagedSection;
|
|
695
594
|
const td = yield* ToolDiscovery;
|
|
696
595
|
const discovery = yield* ConfigDiscovery;
|
|
697
596
|
const warnings = [];
|
|
698
597
|
const foundConfig = yield* findConfigFile(fs);
|
|
699
598
|
const hasHuskyHook = yield* fs.exists(HUSKY_HOOK_PATH);
|
|
700
|
-
let
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
found: false
|
|
705
|
-
};
|
|
599
|
+
let sectionsHealthy = true;
|
|
600
|
+
let baseStatusLabel = "missing";
|
|
601
|
+
let lintStatusLabel = "missing";
|
|
602
|
+
let detectedConfigPath = null;
|
|
706
603
|
if (hasHuskyHook) {
|
|
707
|
-
|
|
708
|
-
if (
|
|
709
|
-
|
|
710
|
-
|
|
604
|
+
const baseResult = yield* ms.check(HUSKY_HOOK_PATH, SavvyBaseSection.block(savvyBasePreamble()));
|
|
605
|
+
if (CheckResult.$is("Found")(baseResult)) {
|
|
606
|
+
baseStatusLabel = baseResult.isUpToDate ? "up-to-date" : "outdated";
|
|
607
|
+
if (!baseResult.isUpToDate) sectionsHealthy = false;
|
|
608
|
+
} else sectionsHealthy = false;
|
|
609
|
+
const existing = yield* ms.read(HUSKY_HOOK_PATH, SavvyLintSectionDef);
|
|
610
|
+
if (existing) {
|
|
611
|
+
const configPath = extractConfigPathFromManaged(existing.content);
|
|
612
|
+
detectedConfigPath = configPath;
|
|
613
|
+
if (configPath) {
|
|
614
|
+
const lintResult = yield* ms.check(HUSKY_HOOK_PATH, savvyLintBlock(configPath));
|
|
615
|
+
if (CheckResult.$is("Found")(lintResult)) {
|
|
616
|
+
lintStatusLabel = lintResult.isUpToDate ? "up-to-date" : "outdated";
|
|
617
|
+
if (!lintResult.isUpToDate) sectionsHealthy = false;
|
|
618
|
+
} else {
|
|
619
|
+
lintStatusLabel = "outdated";
|
|
620
|
+
sectionsHealthy = false;
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
lintStatusLabel = "outdated";
|
|
624
|
+
sectionsHealthy = false;
|
|
625
|
+
}
|
|
626
|
+
} else sectionsHealthy = false;
|
|
627
|
+
if ("up-to-date" !== baseStatusLabel || "up-to-date" !== lintStatusLabel) warnings.push(`${WARNING} Your ${HUSKY_HOOK_PATH} managed sections are out of date.\n Run 'savvy-lint init' to update (preserves your custom hooks).`);
|
|
628
|
+
} else {
|
|
629
|
+
sectionsHealthy = false;
|
|
630
|
+
warnings.push(`${WARNING} No husky pre-commit hook found.\n Run 'savvy-lint init' to create it.`);
|
|
631
|
+
}
|
|
711
632
|
if (!foundConfig) warnings.push(`${WARNING} No lint-staged config file found.\n Run 'savvy-lint init' to create one.`);
|
|
712
633
|
const shellHookPaths = [
|
|
713
634
|
POST_CHECKOUT_HOOK_PATH,
|
|
@@ -716,13 +637,30 @@ const checkCommand = Command.make("check", {
|
|
|
716
637
|
const shellHookStatuses = [];
|
|
717
638
|
for (const hookPath of shellHookPaths){
|
|
718
639
|
const hookExists = yield* fs.exists(hookPath);
|
|
719
|
-
if (hookExists) {
|
|
720
|
-
const status = yield* checkHookManagedSection(section, hookPath, shellScriptsBlock());
|
|
640
|
+
if (!hookExists) {
|
|
721
641
|
shellHookStatuses.push({
|
|
722
642
|
path: hookPath,
|
|
723
|
-
|
|
643
|
+
found: false,
|
|
644
|
+
isUpToDate: false
|
|
724
645
|
});
|
|
725
|
-
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const hygieneResult = yield* ms.check(hookPath, SavvyHooksSection.block(savvyHooksHygiene()));
|
|
649
|
+
const found = CheckResult.$is("Found")(hygieneResult);
|
|
650
|
+
const isUpToDate = CheckResult.$is("Found")(hygieneResult) && hygieneResult.isUpToDate;
|
|
651
|
+
shellHookStatuses.push({
|
|
652
|
+
path: hookPath,
|
|
653
|
+
found,
|
|
654
|
+
isUpToDate
|
|
655
|
+
});
|
|
656
|
+
if (found) {
|
|
657
|
+
if (!isUpToDate) {
|
|
658
|
+
sectionsHealthy = false;
|
|
659
|
+
warnings.push(`${WARNING} ${hookPath} savvy-hooks section is outdated.\n Run 'savvy-lint init' to update.`);
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
sectionsHealthy = false;
|
|
663
|
+
warnings.push(`${WARNING} ${hookPath} has no savvy-hooks section.\n Run 'savvy-lint init' to add it.`);
|
|
726
664
|
}
|
|
727
665
|
}
|
|
728
666
|
const biomeSchemaStatus = yield* checkBiomeSchemas().pipe(Effect.catchAll(()=>Effect.succeed({
|
|
@@ -754,11 +692,18 @@ const checkCommand = Command.make("check", {
|
|
|
754
692
|
else yield* Effect.log(`${CROSS_MARK} No lint-staged config file found`);
|
|
755
693
|
if (hasHuskyHook) yield* Effect.log(`${CHECK_MARK} Husky hook: ${HUSKY_HOOK_PATH}`);
|
|
756
694
|
else yield* Effect.log(`${CROSS_MARK} No husky pre-commit hook found`);
|
|
757
|
-
if (hasHuskyHook)
|
|
758
|
-
|
|
759
|
-
|
|
695
|
+
if (hasHuskyHook) {
|
|
696
|
+
if ("up-to-date" === baseStatusLabel) yield* Effect.log(`${CHECK_MARK} Base section: up-to-date`);
|
|
697
|
+
else if ("outdated" === baseStatusLabel) yield* Effect.log(`${WARNING} Base section: outdated (run 'savvy-lint init' to update)`);
|
|
698
|
+
else yield* Effect.log(`${BULLET} Base section: not found (run 'savvy-lint init' to add)`);
|
|
699
|
+
const lintLabel = detectedConfigPath ? ` (config: ${detectedConfigPath})` : "";
|
|
700
|
+
if ("up-to-date" === lintStatusLabel) yield* Effect.log(`${CHECK_MARK} Lint section: up-to-date${lintLabel}`);
|
|
701
|
+
else if ("outdated" === lintStatusLabel) yield* Effect.log(`${WARNING} Lint section: outdated (run 'savvy-lint init' to update)`);
|
|
702
|
+
else yield* Effect.log(`${BULLET} Lint section: not found (run 'savvy-lint init' to add)`);
|
|
703
|
+
}
|
|
760
704
|
for (const status of shellHookStatuses)if (status.found) if (status.isUpToDate) yield* Effect.log(`${CHECK_MARK} ${status.path}: up-to-date`);
|
|
761
705
|
else yield* Effect.log(`${WARNING} ${status.path}: outdated (run 'savvy-lint init' to update)`);
|
|
706
|
+
else yield* Effect.log(`${BULLET} ${status.path}: savvy-hooks section not found`);
|
|
762
707
|
yield* Effect.log("\nTool availability:");
|
|
763
708
|
const biomeAvailable = yield* td.isAvailable(ToolDefinition.make({
|
|
764
709
|
name: "biome"
|
|
@@ -807,10 +752,9 @@ const checkCommand = Command.make("check", {
|
|
|
807
752
|
for (const status of biomeSchemaStatus.statuses)if (status.matches) yield* Effect.log(` ${CHECK_MARK} ${status.path}: biome $schema up-to-date`);
|
|
808
753
|
else yield* Effect.log(` ${WARNING} ${status.path}: biome $schema outdated (run 'savvy-lint init' to update)`);
|
|
809
754
|
yield* Effect.log("");
|
|
810
|
-
const hasShellHookIssues = shellHookStatuses.some((s)=>s.found && s.needsUpdate);
|
|
811
755
|
const hasMarkdownlintIssues = hasMarkdownlintConfig && !markdownlintStatus.isUpToDate;
|
|
812
756
|
const hasBiomeSchemaIssues = biomeSchemaStatus.statuses.some((s)=>!s.matches);
|
|
813
|
-
const hasIssues = !foundConfig || !hasHuskyHook || !
|
|
757
|
+
const hasIssues = !foundConfig || !hasHuskyHook || !sectionsHealthy || hasMarkdownlintIssues || hasBiomeSchemaIssues;
|
|
814
758
|
if (hasIssues) yield* Effect.log(`${WARNING} Some issues found. Run 'savvy-lint init' to fix.`);
|
|
815
759
|
else yield* Effect.log(`${CHECK_MARK} Lint-staged is configured correctly.`);
|
|
816
760
|
})).pipe(Command.withDescription("Check current lint-staged configuration and tool availability"));
|
|
@@ -987,13 +931,15 @@ const fmtCommand = Command.make("fmt").pipe(Command.withSubcommands([
|
|
|
987
931
|
pnpmWorkspaceCommand,
|
|
988
932
|
yamlCommand
|
|
989
933
|
]));
|
|
990
|
-
const init_CHECK_MARK = "
|
|
991
|
-
const init_WARNING = "
|
|
934
|
+
const init_CHECK_MARK = "✓";
|
|
935
|
+
const init_WARNING = "⚠";
|
|
992
936
|
const EXECUTABLE_MODE = 493;
|
|
993
937
|
const JSONC_FORMAT = {
|
|
994
938
|
tabSize: 1,
|
|
995
939
|
insertSpaces: false
|
|
996
940
|
};
|
|
941
|
+
const PRE_COMMIT_HEADER = "#!/usr/bin/env sh\n# Pre-commit hook with savvy managed sections\n# Custom hooks can go above, below, or between the managed sections\n\n";
|
|
942
|
+
const HYGIENE_HEADER = "#!/usr/bin/env sh\n# Managed by savvy-hooks\n# Custom hooks can go above or below the managed section\n\n";
|
|
997
943
|
function presetIncludesShellScripts(preset) {
|
|
998
944
|
return "minimal" !== preset;
|
|
999
945
|
}
|
|
@@ -1059,7 +1005,7 @@ function writeMarkdownlintConfig(fs, preset, force) {
|
|
|
1059
1005
|
}
|
|
1060
1006
|
function syncBiomeSchemas() {
|
|
1061
1007
|
return Effect.gen(function*() {
|
|
1062
|
-
const version = "2.4.
|
|
1008
|
+
const version = "2.4.16";
|
|
1063
1009
|
if (!version) return;
|
|
1064
1010
|
const syncer = yield* BiomeSchemaSync;
|
|
1065
1011
|
const result = yield* syncer.sync(version);
|
|
@@ -1067,7 +1013,7 @@ function syncBiomeSchemas() {
|
|
|
1067
1013
|
for (const configPath of result.updated)yield* Effect.log(`${init_CHECK_MARK} Updated $schema in ${configPath}`);
|
|
1068
1014
|
});
|
|
1069
1015
|
}
|
|
1070
|
-
const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite
|
|
1016
|
+
const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite the pre-commit hook and config file entirely (managed sections in post-checkout/post-merge are never force-reset)"), Options.withDefault(false));
|
|
1071
1017
|
const configOption = Options.text("config").pipe(Options.withAlias("c"), Options.withDescription("Relative path for the lint-staged config file (from repo root)"), Options.withDefault(DEFAULT_CONFIG_PATH));
|
|
1072
1018
|
const presetOption = Options.choice("preset", [
|
|
1073
1019
|
"minimal",
|
|
@@ -1077,25 +1023,11 @@ const presetOption = Options.choice("preset", [
|
|
|
1077
1023
|
function makeExecutable(path) {
|
|
1078
1024
|
return Effect.tryPromise(()=>import("node:fs/promises").then((fs)=>fs.chmod(path, EXECUTABLE_MODE)));
|
|
1079
1025
|
}
|
|
1080
|
-
function
|
|
1026
|
+
function ensureHookFile(path, header) {
|
|
1081
1027
|
return Effect.gen(function*() {
|
|
1082
|
-
const
|
|
1083
|
-
const
|
|
1084
|
-
if (!
|
|
1085
|
-
if (!hookExists) yield* fs.makeDirectory(".husky", {
|
|
1086
|
-
recursive: true
|
|
1087
|
-
});
|
|
1088
|
-
yield* fs.writeFileString(hookPath, header);
|
|
1089
|
-
}
|
|
1090
|
-
const result = yield* section.sync(hookPath, block);
|
|
1091
|
-
yield* makeExecutable(hookPath);
|
|
1092
|
-
if (hookExists) if (force) yield* Effect.log(`${init_CHECK_MARK} Replaced ${hookPath} (--force)`);
|
|
1093
|
-
else yield* SyncResult.$match(result, {
|
|
1094
|
-
Created: ()=>Effect.log(`${init_CHECK_MARK} Added managed section to ${hookPath}`),
|
|
1095
|
-
Updated: ()=>Effect.log(`${init_CHECK_MARK} Updated managed section in ${hookPath}`),
|
|
1096
|
-
Unchanged: ()=>Effect.log(`${init_CHECK_MARK} ${hookPath}: up-to-date`)
|
|
1097
|
-
});
|
|
1098
|
-
else yield* Effect.log(`${init_CHECK_MARK} Created ${hookPath}`);
|
|
1028
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1029
|
+
const exists = yield* fs.exists(path);
|
|
1030
|
+
if (!exists) yield* fs.writeFileString(path, header);
|
|
1099
1031
|
});
|
|
1100
1032
|
}
|
|
1101
1033
|
const initCommand = Command.make("init", {
|
|
@@ -1104,14 +1036,29 @@ const initCommand = Command.make("init", {
|
|
|
1104
1036
|
preset: presetOption
|
|
1105
1037
|
}, ({ force, config, preset })=>Effect.gen(function*() {
|
|
1106
1038
|
const fs = yield* FileSystem.FileSystem;
|
|
1107
|
-
const
|
|
1039
|
+
const ms = yield* ManagedSection;
|
|
1108
1040
|
if (config.startsWith("/")) yield* Effect.fail(new Error("Config path must be relative to repository root, not absolute"));
|
|
1109
1041
|
yield* Effect.log("Initializing lint-staged configuration...\n");
|
|
1110
|
-
yield*
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1042
|
+
yield* fs.makeDirectory(".husky", {
|
|
1043
|
+
recursive: true
|
|
1044
|
+
});
|
|
1045
|
+
if (force) yield* fs.writeFileString(HUSKY_HOOK_PATH, PRE_COMMIT_HEADER);
|
|
1046
|
+
else yield* ensureHookFile(HUSKY_HOOK_PATH, PRE_COMMIT_HEADER);
|
|
1047
|
+
const preCommitResults = yield* ms.syncMany(HUSKY_HOOK_PATH, [
|
|
1048
|
+
SavvyBaseSection.block(savvyBasePreamble()),
|
|
1049
|
+
savvyLintBlock(config)
|
|
1050
|
+
]);
|
|
1051
|
+
yield* makeExecutable(HUSKY_HOOK_PATH);
|
|
1052
|
+
yield* Effect.log(`${init_CHECK_MARK} ${force ? "Replaced" : "Synced"} ${HUSKY_HOOK_PATH} (${preCommitResults.map((r)=>r._tag).join(", ")})`);
|
|
1053
|
+
if (presetIncludesShellScripts(preset)) for (const hookPath of [
|
|
1054
|
+
POST_CHECKOUT_HOOK_PATH,
|
|
1055
|
+
POST_MERGE_HOOK_PATH
|
|
1056
|
+
]){
|
|
1057
|
+
yield* ensureHookFile(hookPath, HYGIENE_HEADER);
|
|
1058
|
+
yield* ms.remove(hookPath, LegacySavvyLintHygieneDef);
|
|
1059
|
+
yield* ms.sync(hookPath, SavvyHooksSection.block(savvyHooksHygiene()));
|
|
1060
|
+
yield* makeExecutable(hookPath);
|
|
1061
|
+
yield* Effect.log(`${init_CHECK_MARK} Synced ${hookPath}`);
|
|
1115
1062
|
}
|
|
1116
1063
|
if (presetIncludesMarkdown(preset)) yield* writeMarkdownlintConfig(fs, preset, force);
|
|
1117
1064
|
yield* syncBiomeSchemas().pipe(Effect.catchTag("BiomeSyncError", (e)=>Effect.log(`${init_WARNING} Could not sync biome $schema: ${e.message}`)));
|
|
@@ -1136,7 +1083,7 @@ const rootCommand = Command.make("savvy-lint").pipe(Command.withSubcommands([
|
|
|
1136
1083
|
]));
|
|
1137
1084
|
const cli = Command.run(rootCommand, {
|
|
1138
1085
|
name: "savvy-lint",
|
|
1139
|
-
version: "1.0
|
|
1086
|
+
version: "1.2.0"
|
|
1140
1087
|
});
|
|
1141
1088
|
function runCli() {
|
|
1142
1089
|
const main = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(AppLayer));
|
package/README.md
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# @savvy-web/lint-staged
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@savvy-web/lint-staged)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
6
7
|
|
|
7
|
-
Composable, configurable lint-staged handlers for pre-commit hooks. Stop
|
|
8
|
-
duplicating lint-staged configs across projects -- use reusable handlers with
|
|
9
|
-
sensible defaults and easy customization.
|
|
8
|
+
Composable, configurable lint-staged handlers for pre-commit hooks. Stop duplicating lint-staged configs across projects — reuse handlers with sensible defaults and easy customization.
|
|
10
9
|
|
|
11
10
|
## Features
|
|
12
11
|
|
|
@@ -17,7 +16,7 @@ sensible defaults and easy customization.
|
|
|
17
16
|
- Shareable Biome configuration via `@savvy-web/lint-staged/biome/silk.jsonc`
|
|
18
17
|
- Static class API with excellent TypeScript support
|
|
19
18
|
|
|
20
|
-
##
|
|
19
|
+
## Install
|
|
21
20
|
|
|
22
21
|
```bash
|
|
23
22
|
# Install the package and required peer dependencies
|
|
@@ -30,7 +29,7 @@ npm install -D @biomejs/biome
|
|
|
30
29
|
npm install -D markdownlint-cli2
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
## Quick
|
|
32
|
+
## Quick start
|
|
34
33
|
|
|
35
34
|
Use the CLI to bootstrap your configuration:
|
|
36
35
|
|
|
@@ -69,7 +68,7 @@ export default {
|
|
|
69
68
|
| `standard()` | + Markdown, Yaml, PnpmWorkspace, ShellScripts |
|
|
70
69
|
| `silk()` | + TypeScript |
|
|
71
70
|
|
|
72
|
-
## Available
|
|
71
|
+
## Available handlers
|
|
73
72
|
|
|
74
73
|
| Handler | Files | Description |
|
|
75
74
|
| --- | --- | --- |
|
|
@@ -86,20 +85,22 @@ export default {
|
|
|
86
85
|
The `savvy-lint` CLI helps bootstrap, validate, and format your setup:
|
|
87
86
|
|
|
88
87
|
```bash
|
|
89
|
-
savvy-lint init
|
|
90
|
-
savvy-lint init --preset silk --force #
|
|
91
|
-
savvy-lint check
|
|
92
|
-
savvy-lint check --quiet
|
|
93
|
-
savvy-lint fmt package-json
|
|
94
|
-
savvy-lint fmt yaml
|
|
95
|
-
savvy-lint fmt pnpm-workspace
|
|
88
|
+
savvy-lint init # Bootstrap hooks, config, and tooling
|
|
89
|
+
savvy-lint init --preset silk --force # Reset pre-commit and config file
|
|
90
|
+
savvy-lint check # Validate current configuration
|
|
91
|
+
savvy-lint check --quiet # Warnings only (for postinstall)
|
|
92
|
+
savvy-lint fmt package-json # Sort package.json fields
|
|
93
|
+
savvy-lint fmt yaml # Format YAML files with Prettier
|
|
94
|
+
savvy-lint fmt pnpm-workspace # Sort and format pnpm-workspace.yaml
|
|
96
95
|
```
|
|
97
96
|
|
|
98
|
-
|
|
97
|
+
`init` writes two managed sections into `.husky/pre-commit` — a shared `SAVVY-BASE` preamble that defines `pm_exec` and a `SAVVY-LINT` one-liner that runs lint-staged. It also reconciles a co-owned `SAVVY-HOOKS` hygiene section in `.husky/post-checkout` and `.husky/post-merge` that is shared with `@savvy-web/commitlint`. `--force` resets only the pre-commit hook and the lint-staged config file; the hygiene hooks are always reconciled in place because they are co-owned across the Silk Suite tools.
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
`check` validates each managed section independently and reports a per-section status; a stale or missing section degrades the overall verdict even when the hook file is present.
|
|
100
|
+
|
|
101
|
+
## Claude Code plugin
|
|
102
|
+
|
|
103
|
+
A companion Claude Code plugin is available that automatically injects code quality context (Biome, markdownlint, and TypeScript conventions) at session start.
|
|
103
104
|
|
|
104
105
|
```bash
|
|
105
106
|
# Add the Savvy Web plugin marketplace (one-time setup)
|
|
@@ -111,17 +112,16 @@ start.
|
|
|
111
112
|
|
|
112
113
|
## Documentation
|
|
113
114
|
|
|
114
|
-
- [Handler
|
|
115
|
-
- [Configuration API](../docs/configuration.md)
|
|
116
|
-
- [CLI
|
|
117
|
-
- [Utilities](../docs/utilities.md)
|
|
118
|
-
- [Migration
|
|
115
|
+
- [Handler configuration](../docs/handlers.md) — Detailed options for each handler.
|
|
116
|
+
- [Configuration API](../docs/configuration.md) — `createConfig` and `Preset` APIs.
|
|
117
|
+
- [CLI reference](../docs/cli.md) — `savvy-lint init`, `check`, and `fmt`.
|
|
118
|
+
- [Utilities](../docs/utilities.md) — `Command`, `Filter`, and advanced utilities.
|
|
119
|
+
- [Migration guide](../docs/migration.md) — Migrating from raw lint-staged configs.
|
|
119
120
|
|
|
120
121
|
## Contributing
|
|
121
122
|
|
|
122
|
-
Contributions welcome
|
|
123
|
-
and guidelines.
|
|
123
|
+
Contributions welcome. See [CONTRIBUTING.md](../CONTRIBUTING.md) for setup and guidelines.
|
|
124
124
|
|
|
125
125
|
## License
|
|
126
126
|
|
|
127
|
-
[MIT](
|
|
127
|
+
[MIT](LICENSE)
|
package/bin/savvy-lint.js
CHANGED
package/index.d.ts
CHANGED
|
@@ -169,6 +169,9 @@ export declare interface BiomeOptions extends BaseHandlerOptions {
|
|
|
169
169
|
*
|
|
170
170
|
* @remarks
|
|
171
171
|
* Validates the current lint-staged setup and displays detected settings.
|
|
172
|
+
* Validates the `savvy-base` and `savvy-lint` sections in the pre-commit hook and the
|
|
173
|
+
* co-owned `savvy-hooks` hygiene section in `post-checkout` / `post-merge`. Section
|
|
174
|
+
* health degrades the overall verdict even if files exist.
|
|
172
175
|
* With --quiet flag, only outputs warnings (for postinstall usage).
|
|
173
176
|
*/
|
|
174
177
|
export declare const checkCommand: Command_2.Command<"check", ConfigDiscovery | FileSystem.FileSystem | ManagedSection | ToolDiscovery, JsoncParseError | SectionParseError | PlatformError, {
|
|
@@ -546,14 +549,18 @@ export declare abstract class Handler {
|
|
|
546
549
|
* Init command implementation.
|
|
547
550
|
*
|
|
548
551
|
* @remarks
|
|
549
|
-
*
|
|
550
|
-
* - `.husky/pre-commit`
|
|
551
|
-
*
|
|
552
|
-
* - `.
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
552
|
+
* Writes:
|
|
553
|
+
* - `.husky/pre-commit` — `savvy-base` preamble + `savvy-lint` tool section, in order
|
|
554
|
+
* (via `ManagedSection.syncMany`).
|
|
555
|
+
* - `.husky/post-checkout` and `.husky/post-merge` — co-owned `savvy-hooks` hygiene
|
|
556
|
+
* (idempotent, shared with `@savvy-web/commitlint`). Migrates legacy `SAVVY-LINT`
|
|
557
|
+
* hygiene blocks by removing them before writing the new section.
|
|
558
|
+
* - `.markdownlint-cli2.jsonc` config (when preset includes Markdown).
|
|
559
|
+
* - lint-staged config at the specified path.
|
|
560
|
+
*
|
|
561
|
+
* Users may add custom commands above, below, or between the managed sections.
|
|
562
|
+
* `--force` resets only the pre-commit hook and the config file; the hygiene sections
|
|
563
|
+
* are always reconciled with `sync`.
|
|
557
564
|
*/
|
|
558
565
|
export declare const initCommand: Command_2.Command<"init", BiomeSchemaSync | FileSystem.FileSystem | ManagedSection, Error | JsoncModificationError | JsoncParseError | SectionWriteError | PlatformError, {
|
|
559
566
|
readonly force: boolean;
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import sort_package_json from "sort-package-json";
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { Filter, Command as Command_Command, isWorkspacePackagePath, Biome, PnpmWorkspace, Yaml, getWorkspaceRoot } from "./
|
|
4
|
+
import { Filter, Command as Command_Command, isWorkspacePackagePath, Biome, PnpmWorkspace, Yaml, getWorkspaceRoot } from "./841.js";
|
|
5
5
|
class Markdown {
|
|
6
6
|
static glob = "**/*.{md,mdx}";
|
|
7
7
|
static defaultExcludes = [];
|
|
@@ -294,6 +294,6 @@ class Handler {
|
|
|
294
294
|
throw new Error("Handler.create() must be implemented by subclass");
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
|
-
export { Biome, Command, Filter, PnpmWorkspace, Yaml, checkCommand, fmtCommand, getWorkspacePackagePaths, getWorkspacePackages, getWorkspaceRoot, initCommand, isWorkspacePackagePath, resetWorkspaceCache, rootCommand, runCli } from "./
|
|
297
|
+
export { Biome, Command, Filter, PnpmWorkspace, Yaml, checkCommand, fmtCommand, getWorkspacePackagePaths, getWorkspacePackages, getWorkspaceRoot, initCommand, isWorkspacePackagePath, resetWorkspaceCache, rootCommand, runCli } from "./841.js";
|
|
298
298
|
export { ConfigDiscovery, ConfigDiscoveryLive } from "@savvy-web/silk-effects";
|
|
299
299
|
export { Handler, Markdown, PackageJson, Preset, ShellScripts, TypeScript, createConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvy-web/lint-staged",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Composable, configurable lint-staged handlers for pre-commit hooks. Provides reusable handlers for Biome, Markdown, YAML, TypeScript, and more.",
|
|
6
6
|
"keywords": [
|
|
@@ -38,30 +38,30 @@
|
|
|
38
38
|
"./biome/silk.jsonc": "./biome/silk.jsonc"
|
|
39
39
|
},
|
|
40
40
|
"bin": {
|
|
41
|
-
"savvy-lint": "
|
|
41
|
+
"savvy-lint": "bin/savvy-lint.js"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@effect/cli": "^0.75.1",
|
|
45
45
|
"@effect/platform": "^0.96.1",
|
|
46
46
|
"@effect/platform-node": "^0.106.0",
|
|
47
|
-
"@savvy-web/silk-effects": "^0.
|
|
47
|
+
"@savvy-web/silk-effects": "^0.5.0",
|
|
48
48
|
"effect": "^3.21.2",
|
|
49
49
|
"jsonc-effect": "^0.2.1",
|
|
50
50
|
"prettier": "^3.8.3",
|
|
51
51
|
"sort-package-json": "^3.6.1",
|
|
52
|
-
"workspaces-effect": "^1.
|
|
52
|
+
"workspaces-effect": "^1.1.0",
|
|
53
53
|
"yaml": "^2.9.0",
|
|
54
54
|
"yaml-lint": "^1.7.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@biomejs/biome": "2.4.
|
|
57
|
+
"@biomejs/biome": "2.4.16",
|
|
58
58
|
"@types/node": "^25.6.0",
|
|
59
59
|
"@typescript/native-preview": "^7.0.0-dev.20260513.1",
|
|
60
|
-
"husky": "^9.1.
|
|
61
|
-
"lint-staged": "^
|
|
62
|
-
"markdownlint-cli2": "^0.22.
|
|
60
|
+
"husky": "^9.1.7",
|
|
61
|
+
"lint-staged": "^17.0.5",
|
|
62
|
+
"markdownlint-cli2": "^0.22.1",
|
|
63
63
|
"markdownlint-cli2-formatter-codequality": "^0.0.7",
|
|
64
|
-
"turbo": "^2.9.
|
|
64
|
+
"turbo": "^2.9.15",
|
|
65
65
|
"typescript": "^6.0.0"
|
|
66
66
|
},
|
|
67
67
|
"peerDependenciesMeta": {
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"!lint-staged.api.json",
|
|
101
101
|
"!tsconfig.json",
|
|
102
102
|
"!tsdoc.json",
|
|
103
|
-
"
|
|
103
|
+
"841.js",
|
|
104
104
|
"LICENSE",
|
|
105
105
|
"README.md",
|
|
106
106
|
"bin/savvy-lint.js",
|