@savvy-web/lint-staged 1.1.0 → 1.2.1

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.
Files changed (4) hide show
  1. package/841.js +111 -164
  2. package/README.md +28 -28
  3. package/index.d.ts +15 -8
  4. package/package.json +5 -5
package/841.js CHANGED
@@ -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, SectionDefinition, ShellSectionDefinition, SyncResult, ToolDefinition, ToolDiscovery, ToolDiscoveryLive } from "@savvy-web/silk-effects";
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: "SAVVY-LINT"
303
+ toolName: "savvy-lint"
307
304
  });
308
- function generateManagedContent(configPath) {
309
- return `# DO NOT EDIT between these markers - managed by savvy-lint
310
- # Skip in CI environment
311
- if ! { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; }; then
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 generateShellScriptsManagedContent() {
351
- return `# DO NOT EDIT between these markers - managed by savvy-lint
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 = "\u2713";
551
- const CROSS_MARK = "\u2717";
552
- const WARNING = "\u26A0";
553
- const BULLET = "\u2022";
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.15";
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 section = yield* ManagedSection;
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 managedStatus = {
701
- isUpToDate: false,
702
- configPath: null,
703
- needsUpdate: false,
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
- managedStatus = yield* checkManagedSectionStatus(section, HUSKY_HOOK_PATH);
708
- if (managedStatus.found && managedStatus.needsUpdate) warnings.push(`${WARNING} Your ${HUSKY_HOOK_PATH} managed section is outdated.\n Run 'savvy-lint init' to update it (preserves your custom hooks).`);
709
- else if (!managedStatus.found) warnings.push(`${WARNING} Your ${HUSKY_HOOK_PATH} does not have a savvy-lint managed section.\n Run 'savvy-lint init' to add it.`);
710
- } else warnings.push(`${WARNING} No husky pre-commit hook found.\n Run 'savvy-lint init' to create it.`);
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
- ...status
643
+ found: false,
644
+ isUpToDate: false
724
645
  });
725
- if (status.found && status.needsUpdate) warnings.push(`${WARNING} Your ${hookPath} managed section is outdated.\n Run 'savvy-lint init' to update it (preserves your custom hooks).`);
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) if (managedStatus.found) if (managedStatus.isUpToDate) yield* Effect.log(`${CHECK_MARK} Managed section: up-to-date`);
758
- else yield* Effect.log(`${WARNING} Managed section: outdated (run 'savvy-lint init' to update)`);
759
- else yield* Effect.log(`${BULLET} Managed section: not found (run 'savvy-lint init' to add)`);
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 || !managedStatus.found || managedStatus.needsUpdate || hasShellHookIssues || hasMarkdownlintIssues || hasBiomeSchemaIssues;
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 = "\u2713";
991
- const init_WARNING = "\u26A0";
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.15";
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 entire hook file (not just managed section)"), Options.withDefault(false));
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 writeHook(fs, section, hookPath, block, comment, force) {
1026
+ function ensureHookFile(path, header) {
1081
1027
  return Effect.gen(function*() {
1082
- const hookExists = yield* fs.exists(hookPath);
1083
- const header = `#!/usr/bin/env sh\n# ${comment}\n# Custom hooks can go above or below the managed section\n`;
1084
- if (!hookExists || force) {
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 section = yield* ManagedSection;
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* writeHook(fs, section, HUSKY_HOOK_PATH, preCommitBlock(config), "Pre-commit hook with savvy-lint managed section", force);
1111
- if (presetIncludesShellScripts(preset)) {
1112
- const shellBlock = shellScriptsBlock();
1113
- yield* writeHook(fs, section, POST_CHECKOUT_HOOK_PATH, shellBlock, "Post-checkout hook with savvy-lint managed section", force);
1114
- yield* writeHook(fs, section, POST_MERGE_HOOK_PATH, shellBlock, "Post-merge hook with savvy-lint managed section", force);
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.1.0"
1086
+ version: "1.2.1"
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
- [![npm version](https://img.shields.io/npm/v/@savvy-web/lint-staged)](https://www.npmjs.com/package/@savvy-web/lint-staged)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Node.js](https://img.shields.io/badge/Node.js-24%2B-green)](https://nodejs.org/)
3
+ [![npm](https://img.shields.io/npm/v/@savvy-web%2Flint-staged?label=npm&color=cb3837)](https://www.npmjs.com/package/@savvy-web/lint-staged)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js %3E%3D24](https://img.shields.io/badge/Node.js-%3E%3D24-5fa04e.svg)](https://nodejs.org/)
6
+ [![TypeScript 6.0](https://img.shields.io/badge/TypeScript-6.0-3178c6.svg)](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
- ## Installation
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 Start
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 Handlers
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 # Bootstrap hooks, config, and tooling
90
- savvy-lint init --preset silk --force # Overwrite with silk preset
91
- savvy-lint check # Validate current configuration
92
- savvy-lint check --quiet # Warnings only (for postinstall)
93
- savvy-lint fmt package-json # Sort package.json fields
94
- savvy-lint fmt yaml # Format YAML files with Prettier
95
- savvy-lint fmt pnpm-workspace # Sort and format pnpm-workspace.yaml
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
- ## Claude Code Plugin
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
- A companion Claude Code plugin is available that automatically injects code
101
- quality context (Biome, markdownlint, and TypeScript conventions) at session
102
- start.
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 Configuration](../docs/handlers.md) -- Detailed options for each handler
115
- - [Configuration API](../docs/configuration.md) -- createConfig and Preset APIs
116
- - [CLI Reference](../docs/cli.md) -- `savvy-lint init`, `check`, and `fmt`
117
- - [Utilities](../docs/utilities.md) -- Command, Filter, and advanced utilities
118
- - [Migration Guide](../docs/migration.md) -- Migrating from raw lint-staged configs
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! See [CONTRIBUTING.md](../CONTRIBUTING.md) for setup
123
- and guidelines.
123
+ Contributions welcome. See [CONTRIBUTING.md](../CONTRIBUTING.md) for setup and guidelines.
124
124
 
125
125
  ## License
126
126
 
127
- [MIT](./LICENSE)
127
+ [MIT](LICENSE)
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
- * Creates the necessary configuration files for lint-staged:
550
- * - `.husky/pre-commit` hook with managed section
551
- * - `.husky/post-checkout` and `.husky/post-merge` hooks (when preset includes ShellScripts)
552
- * - `.markdownlint-cli2.jsonc` config (when preset includes Markdown)
553
- * - lint-staged config at the specified path
554
- *
555
- * The managed section feature allows users to add custom hooks above/below
556
- * the savvy-lint section without them being overwritten on updates.
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/lint-staged",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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": [
@@ -44,24 +44,24 @@
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.3.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.0.0",
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.15",
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
60
  "husky": "^9.1.7",
61
61
  "lint-staged": "^17.0.5",
62
62
  "markdownlint-cli2": "^0.22.1",
63
63
  "markdownlint-cli2-formatter-codequality": "^0.0.7",
64
- "turbo": "^2.9.14",
64
+ "turbo": "^2.9.16",
65
65
  "typescript": "^6.0.0"
66
66
  },
67
67
  "peerDependenciesMeta": {