@savvy-web/lint-staged 0.2.2 → 0.3.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.
package/376.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { Command, Options } from "@effect/cli";
2
2
  import { NodeContext, NodeRuntime } from "@effect/platform-node";
3
3
  import { Effect } from "effect";
4
+ import { isDeepStrictEqual } from "node:util";
4
5
  import { FileSystem } from "@effect/platform";
6
+ import { applyEdits, modify, parse } from "jsonc-parser";
5
7
  import { execSync } from "node:child_process";
6
8
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
9
  import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
@@ -921,17 +923,201 @@ class TypeScript {
921
923
  };
922
924
  }
923
925
  }
926
+ const MARKDOWNLINT_TEMPLATE = {
927
+ $schema: "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.20.0/schema/markdownlint-cli2-config-schema.json",
928
+ globs: [
929
+ "**/*.{md,mdx}"
930
+ ],
931
+ fix: true,
932
+ gitignore: true,
933
+ noBanner: true,
934
+ ignores: [
935
+ "**/node_modules",
936
+ "**/.cache",
937
+ "**/coverage",
938
+ "**/.coverage",
939
+ "**/dist",
940
+ "**/CHANGELOG.md",
941
+ "**/.claude/plans"
942
+ ],
943
+ config: {
944
+ default: true,
945
+ MD001: true,
946
+ MD002: true,
947
+ MD003: true,
948
+ MD004: true,
949
+ MD005: true,
950
+ MD006: true,
951
+ MD007: true,
952
+ MD008: true,
953
+ MD009: true,
954
+ MD010: true,
955
+ MD011: true,
956
+ MD012: true,
957
+ MD013: false,
958
+ MD014: true,
959
+ MD015: true,
960
+ MD016: true,
961
+ MD017: true,
962
+ MD018: true,
963
+ MD019: true,
964
+ MD020: true,
965
+ MD021: true,
966
+ MD022: true,
967
+ MD023: true,
968
+ MD024: {
969
+ siblings_only: true
970
+ },
971
+ MD025: true,
972
+ MD026: true,
973
+ MD027: true,
974
+ MD028: true,
975
+ MD029: true,
976
+ MD030: true,
977
+ MD031: true,
978
+ MD032: true,
979
+ MD033: {
980
+ allowed_elements: [
981
+ "br",
982
+ "details",
983
+ "summary",
984
+ "img",
985
+ "sup",
986
+ "sub"
987
+ ]
988
+ },
989
+ MD034: true,
990
+ MD035: true,
991
+ MD036: true,
992
+ MD037: true,
993
+ MD038: true,
994
+ MD039: true,
995
+ MD040: true,
996
+ MD041: true,
997
+ MD042: true,
998
+ MD043: false,
999
+ MD044: false,
1000
+ MD045: true,
1001
+ MD046: true,
1002
+ MD047: true,
1003
+ MD048: true,
1004
+ MD049: true,
1005
+ MD050: true,
1006
+ MD051: true,
1007
+ MD052: true,
1008
+ MD053: true,
1009
+ MD054: true,
1010
+ MD055: true,
1011
+ MD056: true,
1012
+ MD057: true,
1013
+ MD058: true,
1014
+ MD059: true,
1015
+ MD060: {
1016
+ style: "compact"
1017
+ }
1018
+ }
1019
+ };
1020
+ const MARKDOWNLINT_SCHEMA = "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.20.0/schema/markdownlint-cli2-config-schema.json";
1021
+ const MARKDOWNLINT_CONFIG = {
1022
+ default: true,
1023
+ MD001: true,
1024
+ MD002: true,
1025
+ MD003: true,
1026
+ MD004: true,
1027
+ MD005: true,
1028
+ MD006: true,
1029
+ MD007: true,
1030
+ MD008: true,
1031
+ MD009: true,
1032
+ MD010: true,
1033
+ MD011: true,
1034
+ MD012: true,
1035
+ MD013: false,
1036
+ MD014: true,
1037
+ MD015: true,
1038
+ MD016: true,
1039
+ MD017: true,
1040
+ MD018: true,
1041
+ MD019: true,
1042
+ MD020: true,
1043
+ MD021: true,
1044
+ MD022: true,
1045
+ MD023: true,
1046
+ MD024: {
1047
+ siblings_only: true
1048
+ },
1049
+ MD025: true,
1050
+ MD026: true,
1051
+ MD027: true,
1052
+ MD028: true,
1053
+ MD029: true,
1054
+ MD030: true,
1055
+ MD031: true,
1056
+ MD032: true,
1057
+ MD033: {
1058
+ allowed_elements: [
1059
+ "br",
1060
+ "details",
1061
+ "summary",
1062
+ "img",
1063
+ "sup",
1064
+ "sub"
1065
+ ]
1066
+ },
1067
+ MD034: true,
1068
+ MD035: true,
1069
+ MD036: true,
1070
+ MD037: true,
1071
+ MD038: true,
1072
+ MD039: true,
1073
+ MD040: true,
1074
+ MD041: true,
1075
+ MD042: true,
1076
+ MD043: false,
1077
+ MD044: false,
1078
+ MD045: true,
1079
+ MD046: true,
1080
+ MD047: true,
1081
+ MD048: true,
1082
+ MD049: true,
1083
+ MD050: true,
1084
+ MD051: true,
1085
+ MD052: true,
1086
+ MD053: true,
1087
+ MD054: true,
1088
+ MD055: true,
1089
+ MD056: true,
1090
+ MD057: true,
1091
+ MD058: true,
1092
+ MD059: true,
1093
+ MD060: {
1094
+ style: "compact"
1095
+ }
1096
+ };
924
1097
  const CHECK_MARK = "\u2713";
925
1098
  const WARNING = "\u26A0";
926
1099
  const EXECUTABLE_MODE = 493;
927
1100
  const HUSKY_HOOK_PATH = ".husky/pre-commit";
1101
+ const POST_CHECKOUT_HOOK_PATH = ".husky/post-checkout";
1102
+ const POST_MERGE_HOOK_PATH = ".husky/post-merge";
928
1103
  const DEFAULT_CONFIG_PATH = "lib/configs/lint-staged.config.ts";
1104
+ const MARKDOWNLINT_CONFIG_PATH = "lib/configs/.markdownlint-cli2.jsonc";
1105
+ const JSONC_FORMAT = {
1106
+ tabSize: 1,
1107
+ insertSpaces: false
1108
+ };
929
1109
  const BEGIN_MARKER = "# --- BEGIN SAVVY-LINT MANAGED SECTION ---";
930
1110
  const END_MARKER = "# --- END SAVVY-LINT MANAGED SECTION ---";
1111
+ function presetIncludesShellScripts(preset) {
1112
+ return "minimal" !== preset;
1113
+ }
1114
+ function presetIncludesMarkdown(preset) {
1115
+ return "minimal" !== preset;
1116
+ }
931
1117
  function generateManagedContent(configPath) {
932
1118
  return `# DO NOT EDIT between these markers - managed by savvy-lint
933
1119
  # Skip in CI environment
934
- { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; } && exit 0
1120
+ if ! { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; }; then
935
1121
 
936
1122
  # Get repo root directory
937
1123
  ROOT=$(git rev-parse --show-toplevel)
@@ -959,26 +1145,36 @@ detect_pm() {
959
1145
  fi
960
1146
  }
961
1147
 
962
- # Get the exec command for the detected package manager
1148
+ # Run lint-staged with the detected package manager
963
1149
  PM=$(detect_pm)
964
1150
  case "$PM" in
965
- pnpm) CMD="pnpm exec" ;;
966
- yarn) CMD="yarn exec" ;;
967
- bun) CMD="bunx" ;;
968
- *) CMD="npx --no --" ;;
1151
+ pnpm) pnpm exec lint-staged --config "$ROOT/${configPath}" ;;
1152
+ yarn) yarn exec lint-staged --config "$ROOT/${configPath}" ;;
1153
+ bun) bunx lint-staged --config "$ROOT/${configPath}" ;;
1154
+ *) npx --no -- lint-staged --config "$ROOT/${configPath}" ;;
969
1155
  esac
970
1156
 
971
- $CMD lint-staged --config "$ROOT/${configPath}"`;
1157
+ fi`;
972
1158
  }
973
- function generateFullHookContent(configPath) {
974
- return `#!/usr/bin/env sh
975
- # Pre-commit hook with savvy-lint managed section
976
- # Custom hooks can go above or below the managed section
1159
+ function generateShellScriptsManagedContent() {
1160
+ return `# DO NOT EDIT between these markers - managed by savvy-lint
1161
+ if ! { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; }; then
977
1162
 
978
- ${BEGIN_MARKER}
979
- ${generateManagedContent(configPath)}
980
- ${END_MARKER}
981
- `;
1163
+ # Configure git to ignore executable bit changes
1164
+ # This ensures hook scripts can be made executable locally without git tracking the change
1165
+ git config core.fileMode false
1166
+
1167
+ # Ensure all shell scripts tracked by git are executable
1168
+ git ls-files -z '*.sh' | xargs -0 -r chmod +x 2>/dev/null || true
1169
+
1170
+ fi`;
1171
+ }
1172
+ function updateManagedSectionWithContent(existingContent, managedContent) {
1173
+ const { beforeSection, afterSection, found } = extractManagedSection(existingContent);
1174
+ const newManagedSection = `${BEGIN_MARKER}\n${managedContent}\n${END_MARKER}`;
1175
+ if (found) return `${beforeSection}${newManagedSection}${afterSection}`;
1176
+ const trimmedContent = existingContent.trimEnd();
1177
+ return `${trimmedContent}\n\n${newManagedSection}\n`;
982
1178
  }
983
1179
  function extractManagedSection(content) {
984
1180
  const beginIndex = content.indexOf(BEGIN_MARKER);
@@ -996,24 +1192,73 @@ function extractManagedSection(content) {
996
1192
  found: true
997
1193
  };
998
1194
  }
999
- function updateManagedSection(existingContent, configPath) {
1000
- const { beforeSection, afterSection, found } = extractManagedSection(existingContent);
1001
- const newManagedSection = `${BEGIN_MARKER}\n${generateManagedContent(configPath)}\n${END_MARKER}`;
1002
- if (found) return `${beforeSection}${newManagedSection}${afterSection}`;
1003
- const trimmedContent = existingContent.trimEnd();
1004
- return `${trimmedContent}\n\n${newManagedSection}\n`;
1195
+ function generateFullHookContentFromManaged(comment, managedContent) {
1196
+ return `#!/usr/bin/env sh
1197
+ # ${comment}
1198
+ # Custom hooks can go above or below the managed section
1199
+
1200
+ ${BEGIN_MARKER}
1201
+ ${managedContent}
1202
+ ${END_MARKER}
1203
+ `;
1005
1204
  }
1006
1205
  function generateConfigContent(preset) {
1007
1206
  return `/**
1008
1207
  * lint-staged configuration
1009
1208
  * Generated by savvy-lint init
1010
1209
  */
1011
- import type { Configuration } from "lint-staged";
1012
1210
  import { Preset } from "@savvy-web/lint-staged";
1013
1211
 
1014
- export default Preset.${preset}() satisfies Configuration;
1212
+ export default Preset.${preset}();
1015
1213
  `;
1016
1214
  }
1215
+ function writeMarkdownlintConfig(fs, preset, force) {
1216
+ return Effect.gen(function*() {
1217
+ const configExists = yield* fs.exists(MARKDOWNLINT_CONFIG_PATH);
1218
+ const fullTemplate = JSON.stringify(MARKDOWNLINT_TEMPLATE, null, "\t");
1219
+ if (!configExists) {
1220
+ yield* fs.makeDirectory("lib/configs", {
1221
+ recursive: true
1222
+ });
1223
+ yield* fs.writeFileString(MARKDOWNLINT_CONFIG_PATH, `${fullTemplate}\n`);
1224
+ yield* Effect.log(`${CHECK_MARK} Created ${MARKDOWNLINT_CONFIG_PATH}`);
1225
+ return;
1226
+ }
1227
+ if ("silk" !== preset) return void (yield* Effect.log(`${CHECK_MARK} ${MARKDOWNLINT_CONFIG_PATH}: exists (not managed by ${preset} preset)`));
1228
+ if (force) {
1229
+ yield* fs.writeFileString(MARKDOWNLINT_CONFIG_PATH, `${fullTemplate}\n`);
1230
+ yield* Effect.log(`${CHECK_MARK} Replaced ${MARKDOWNLINT_CONFIG_PATH} (--force)`);
1231
+ return;
1232
+ }
1233
+ const existingText = yield* fs.readFileString(MARKDOWNLINT_CONFIG_PATH);
1234
+ const existingParsed = parse(existingText);
1235
+ let updatedText = existingText;
1236
+ let schemaUpdated = false;
1237
+ if (existingParsed.$schema !== MARKDOWNLINT_SCHEMA) {
1238
+ const edits = modify(updatedText, [
1239
+ "$schema"
1240
+ ], MARKDOWNLINT_SCHEMA, {
1241
+ formattingOptions: JSONC_FORMAT
1242
+ });
1243
+ updatedText = applyEdits(updatedText, edits);
1244
+ schemaUpdated = true;
1245
+ }
1246
+ const existingConfig = existingParsed.config;
1247
+ const configMatches = void 0 !== existingConfig && isDeepStrictEqual(existingConfig, MARKDOWNLINT_CONFIG);
1248
+ if (!configMatches) {
1249
+ yield* Effect.log(`${WARNING} ${MARKDOWNLINT_CONFIG_PATH}: config rules differ from template (use --force to overwrite)`);
1250
+ if (schemaUpdated) {
1251
+ yield* fs.writeFileString(MARKDOWNLINT_CONFIG_PATH, updatedText);
1252
+ yield* Effect.log(`${CHECK_MARK} Updated $schema in ${MARKDOWNLINT_CONFIG_PATH}`);
1253
+ }
1254
+ return;
1255
+ }
1256
+ if (schemaUpdated) {
1257
+ yield* fs.writeFileString(MARKDOWNLINT_CONFIG_PATH, updatedText);
1258
+ yield* Effect.log(`${CHECK_MARK} Updated $schema in ${MARKDOWNLINT_CONFIG_PATH}`);
1259
+ } else yield* Effect.log(`${CHECK_MARK} ${MARKDOWNLINT_CONFIG_PATH}: up-to-date`);
1260
+ });
1261
+ }
1017
1262
  const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite entire hook file (not just managed section)"), Options.withDefault(false));
1018
1263
  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));
1019
1264
  const presetOption = Options.choice("preset", [
@@ -1024,6 +1269,31 @@ const presetOption = Options.choice("preset", [
1024
1269
  function makeExecutable(path) {
1025
1270
  return Effect.tryPromise(()=>import("node:fs/promises").then((fs)=>fs.chmod(path, EXECUTABLE_MODE)));
1026
1271
  }
1272
+ function writeHook(fs, hookPath, managedContent, comment, force) {
1273
+ return Effect.gen(function*() {
1274
+ const hookExists = yield* fs.exists(hookPath);
1275
+ if (hookExists && !force) {
1276
+ const existingContent = yield* fs.readFileString(hookPath);
1277
+ const { found } = extractManagedSection(existingContent);
1278
+ const updatedContent = updateManagedSectionWithContent(existingContent, managedContent);
1279
+ yield* fs.writeFileString(hookPath, updatedContent);
1280
+ yield* makeExecutable(hookPath);
1281
+ if (found) yield* Effect.log(`${CHECK_MARK} Updated managed section in ${hookPath}`);
1282
+ else yield* Effect.log(`${CHECK_MARK} Added managed section to ${hookPath}`);
1283
+ } else if (hookExists && force) {
1284
+ yield* fs.writeFileString(hookPath, generateFullHookContentFromManaged(comment, managedContent));
1285
+ yield* makeExecutable(hookPath);
1286
+ yield* Effect.log(`${CHECK_MARK} Replaced ${hookPath} (--force)`);
1287
+ } else {
1288
+ yield* fs.makeDirectory(".husky", {
1289
+ recursive: true
1290
+ });
1291
+ yield* fs.writeFileString(hookPath, generateFullHookContentFromManaged(comment, managedContent));
1292
+ yield* makeExecutable(hookPath);
1293
+ yield* Effect.log(`${CHECK_MARK} Created ${hookPath}`);
1294
+ }
1295
+ });
1296
+ }
1027
1297
  const initCommand = Command.make("init", {
1028
1298
  force: forceOption,
1029
1299
  config: configOption,
@@ -1032,27 +1302,13 @@ const initCommand = Command.make("init", {
1032
1302
  const fs = yield* FileSystem.FileSystem;
1033
1303
  if (config.startsWith("/")) yield* Effect.fail(new Error("Config path must be relative to repository root, not absolute"));
1034
1304
  yield* Effect.log("Initializing lint-staged configuration...\n");
1035
- const huskyExists = yield* fs.exists(HUSKY_HOOK_PATH);
1036
- if (huskyExists && !force) {
1037
- const existingContent = yield* fs.readFileString(HUSKY_HOOK_PATH);
1038
- const { found } = extractManagedSection(existingContent);
1039
- const updatedContent = updateManagedSection(existingContent, config);
1040
- yield* fs.writeFileString(HUSKY_HOOK_PATH, updatedContent);
1041
- yield* makeExecutable(HUSKY_HOOK_PATH);
1042
- if (found) yield* Effect.log(`${CHECK_MARK} Updated managed section in ${HUSKY_HOOK_PATH}`);
1043
- else yield* Effect.log(`${CHECK_MARK} Added managed section to ${HUSKY_HOOK_PATH}`);
1044
- } else if (huskyExists && force) {
1045
- yield* fs.writeFileString(HUSKY_HOOK_PATH, generateFullHookContent(config));
1046
- yield* makeExecutable(HUSKY_HOOK_PATH);
1047
- yield* Effect.log(`${CHECK_MARK} Replaced ${HUSKY_HOOK_PATH} (--force)`);
1048
- } else {
1049
- yield* fs.makeDirectory(".husky", {
1050
- recursive: true
1051
- });
1052
- yield* fs.writeFileString(HUSKY_HOOK_PATH, generateFullHookContent(config));
1053
- yield* makeExecutable(HUSKY_HOOK_PATH);
1054
- yield* Effect.log(`${CHECK_MARK} Created ${HUSKY_HOOK_PATH}`);
1305
+ yield* writeHook(fs, HUSKY_HOOK_PATH, generateManagedContent(config), "Pre-commit hook with savvy-lint managed section", force);
1306
+ if (presetIncludesShellScripts(preset)) {
1307
+ const shellContent = generateShellScriptsManagedContent();
1308
+ yield* writeHook(fs, POST_CHECKOUT_HOOK_PATH, shellContent, "Post-checkout hook with savvy-lint managed section", force);
1309
+ yield* writeHook(fs, POST_MERGE_HOOK_PATH, shellContent, "Post-merge hook with savvy-lint managed section", force);
1055
1310
  }
1311
+ if (presetIncludesMarkdown(preset)) yield* writeMarkdownlintConfig(fs, preset, force);
1056
1312
  const configExists = yield* fs.exists(config);
1057
1313
  if (configExists && !force) yield* Effect.log(`${WARNING} ${config} already exists (use --force to overwrite)`);
1058
1314
  else {
@@ -1098,6 +1354,23 @@ function extractConfigPathFromManaged(managedContent) {
1098
1354
  const match = managedContent.match(/lint-staged --config "\$ROOT\/([^"]+)"/);
1099
1355
  return match ? match[1] : null;
1100
1356
  }
1357
+ function checkHookManagedSection(hookContent, expectedManagedContent) {
1358
+ const { managedSection, found } = extractManagedSection(hookContent);
1359
+ if (!found) return {
1360
+ found: false,
1361
+ isUpToDate: false,
1362
+ needsUpdate: false
1363
+ };
1364
+ const expectedSection = `${BEGIN_MARKER}\n${expectedManagedContent}\n${END_MARKER}`;
1365
+ const normalizedExisting = managedSection.trim().replace(/\s+/g, " ");
1366
+ const normalizedExpected = expectedSection.trim().replace(/\s+/g, " ");
1367
+ const isUpToDate = normalizedExisting === normalizedExpected;
1368
+ return {
1369
+ found: true,
1370
+ isUpToDate,
1371
+ needsUpdate: !isUpToDate
1372
+ };
1373
+ }
1101
1374
  function checkManagedSectionStatus(existingManaged) {
1102
1375
  const configPath = extractConfigPathFromManaged(existingManaged);
1103
1376
  if (!configPath) return {
@@ -1115,6 +1388,18 @@ function checkManagedSectionStatus(existingManaged) {
1115
1388
  needsUpdate: !isUpToDate
1116
1389
  };
1117
1390
  }
1391
+ function checkMarkdownlintConfig(content) {
1392
+ const parsed = parse(content);
1393
+ const schemaMatches = parsed.$schema === MARKDOWNLINT_SCHEMA;
1394
+ const existingConfig = parsed.config;
1395
+ const configMatches = void 0 !== existingConfig && isDeepStrictEqual(existingConfig, MARKDOWNLINT_CONFIG);
1396
+ return {
1397
+ exists: true,
1398
+ schemaMatches,
1399
+ configMatches,
1400
+ isUpToDate: schemaMatches && configMatches
1401
+ };
1402
+ }
1118
1403
  const quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Only output warnings (for postinstall usage)"), Options.withDefault(false));
1119
1404
  const checkCommand = Command.make("check", {
1120
1405
  quiet: quietOption
@@ -1150,6 +1435,36 @@ const checkCommand = Command.make("check", {
1150
1435
  }
1151
1436
  } else warnings.push(`${check_WARNING} No husky pre-commit hook found.\n Run 'savvy-lint init' to create it.`);
1152
1437
  if (!foundConfig) warnings.push(`${check_WARNING} No lint-staged config file found.\n Run 'savvy-lint init' to create one.`);
1438
+ const shellHookPaths = [
1439
+ POST_CHECKOUT_HOOK_PATH,
1440
+ POST_MERGE_HOOK_PATH
1441
+ ];
1442
+ const shellHookStatuses = [];
1443
+ for (const hookPath of shellHookPaths){
1444
+ const hookExists = yield* fs.exists(hookPath);
1445
+ if (hookExists) {
1446
+ const hookContent = yield* fs.readFileString(hookPath);
1447
+ const status = checkHookManagedSection(hookContent, generateShellScriptsManagedContent());
1448
+ shellHookStatuses.push({
1449
+ path: hookPath,
1450
+ ...status
1451
+ });
1452
+ if (status.found && status.needsUpdate) warnings.push(`${check_WARNING} Your ${hookPath} managed section is outdated.\n Run 'savvy-lint init' to update it (preserves your custom hooks).`);
1453
+ }
1454
+ }
1455
+ const hasMarkdownlintConfig = yield* fs.exists(MARKDOWNLINT_CONFIG_PATH);
1456
+ let markdownlintStatus = {
1457
+ exists: false,
1458
+ schemaMatches: false,
1459
+ configMatches: false,
1460
+ isUpToDate: false
1461
+ };
1462
+ if (hasMarkdownlintConfig) {
1463
+ const mdContent = yield* fs.readFileString(MARKDOWNLINT_CONFIG_PATH);
1464
+ markdownlintStatus = checkMarkdownlintConfig(mdContent);
1465
+ if (!markdownlintStatus.schemaMatches) warnings.push(`${check_WARNING} ${MARKDOWNLINT_CONFIG_PATH}: $schema differs from template.\n Run 'savvy-lint init' to update it.`);
1466
+ if (!markdownlintStatus.configMatches) warnings.push(`${check_WARNING} ${MARKDOWNLINT_CONFIG_PATH}: config rules differ from template.\n Run 'savvy-lint init --force' to overwrite.`);
1467
+ }
1153
1468
  if (quiet) {
1154
1469
  if (warnings.length > 0) for (const warning of warnings)yield* Effect.log(warning);
1155
1470
  return;
@@ -1162,6 +1477,8 @@ const checkCommand = Command.make("check", {
1162
1477
  if (hasHuskyHook) if (managedStatus.found) if (managedStatus.isUpToDate) yield* Effect.log(`${check_CHECK_MARK} Managed section: up-to-date`);
1163
1478
  else yield* Effect.log(`${check_WARNING} Managed section: outdated (run 'savvy-lint init' to update)`);
1164
1479
  else yield* Effect.log(`${BULLET} Managed section: not found (run 'savvy-lint init' to add)`);
1480
+ for (const status of shellHookStatuses)if (status.found) if (status.isUpToDate) yield* Effect.log(`${check_CHECK_MARK} ${status.path}: up-to-date`);
1481
+ else yield* Effect.log(`${check_WARNING} ${status.path}: outdated (run 'savvy-lint init' to update)`);
1165
1482
  yield* Effect.log("\nTool availability:");
1166
1483
  const biomeAvailable = Biome.isAvailable();
1167
1484
  const biomeConfig = Biome.findConfig();
@@ -1183,8 +1500,18 @@ const checkCommand = Command.make("check", {
1183
1500
  const tsdocAvailable = TypeScript.isTsdocAvailable();
1184
1501
  if (tsdocAvailable) yield* Effect.log(` ${check_CHECK_MARK} TSDoc (tsdoc.json found)`);
1185
1502
  else yield* Effect.log(` ${BULLET} TSDoc: no tsdoc.json found`);
1503
+ if (hasMarkdownlintConfig) if (markdownlintStatus.isUpToDate) yield* Effect.log(` ${check_CHECK_MARK} ${MARKDOWNLINT_CONFIG_PATH}: up-to-date`);
1504
+ else {
1505
+ const issues = [];
1506
+ if (!markdownlintStatus.schemaMatches) issues.push("$schema");
1507
+ if (!markdownlintStatus.configMatches) issues.push("config");
1508
+ yield* Effect.log(` ${check_WARNING} ${MARKDOWNLINT_CONFIG_PATH}: ${issues.join(", ")} differ from template`);
1509
+ }
1510
+ else yield* Effect.log(` ${BULLET} ${MARKDOWNLINT_CONFIG_PATH}: not found`);
1186
1511
  yield* Effect.log("");
1187
- const hasIssues = !foundConfig || !hasHuskyHook || !managedStatus.found || managedStatus.needsUpdate;
1512
+ const hasShellHookIssues = shellHookStatuses.some((s)=>s.found && s.needsUpdate);
1513
+ const hasMarkdownlintIssues = hasMarkdownlintConfig && !markdownlintStatus.isUpToDate;
1514
+ const hasIssues = !foundConfig || !hasHuskyHook || !managedStatus.found || managedStatus.needsUpdate || hasShellHookIssues || hasMarkdownlintIssues;
1188
1515
  if (hasIssues) yield* Effect.log(`${check_WARNING} Some issues found. Run 'savvy-lint init' to fix.`);
1189
1516
  else yield* Effect.log(`${check_CHECK_MARK} Lint-staged is configured correctly.`);
1190
1517
  })).pipe(Command.withDescription("Check current lint-staged configuration and tool availability"));
@@ -1194,7 +1521,7 @@ const rootCommand = Command.make("savvy-lint").pipe(Command.withSubcommands([
1194
1521
  ]));
1195
1522
  const cli = Command.run(rootCommand, {
1196
1523
  name: "savvy-lint",
1197
- version: "0.2.2"
1524
+ version: "0.3.1"
1198
1525
  });
1199
1526
  function runCli() {
1200
1527
  const main = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(NodeContext.layer));
package/README.md CHANGED
@@ -2,17 +2,19 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@savvy-web/lint-staged)](https://www.npmjs.com/package/@savvy-web/lint-staged)
4
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/)
5
6
 
6
7
  Composable, configurable lint-staged handlers for pre-commit hooks. Stop
7
- duplicating lint-staged configs across projects - use reusable handlers with
8
+ duplicating lint-staged configs across projects -- use reusable handlers with
8
9
  sensible defaults and easy customization.
9
10
 
10
11
  ## Features
11
12
 
12
13
  - Composable handlers for Biome, Markdown, YAML, TypeScript, and more
13
14
  - Zero-config presets for instant setup
15
+ - CLI tool (`savvy-lint`) to bootstrap and validate your configuration
14
16
  - Workspace-aware TSDoc validation for public APIs
15
- - Bundled dependencies for fast, offline-capable execution
17
+ - Shareable Biome configuration via `@savvy-web/lint-staged/biome/silk.jsonc`
16
18
  - Static class API with excellent TypeScript and TSDoc support
17
19
 
18
20
  ## Installation
@@ -30,7 +32,13 @@ npm install -D markdownlint-cli2
30
32
 
31
33
  ## Quick Start
32
34
 
33
- Use a preset for instant setup:
35
+ Use the CLI to bootstrap your configuration:
36
+
37
+ ```bash
38
+ npx savvy-lint init --preset standard
39
+ ```
40
+
41
+ Or configure manually with a preset:
34
42
 
35
43
  ```typescript
36
44
  // lint-staged.config.ts
@@ -61,17 +69,6 @@ export default {
61
69
  | `standard()` | + Markdown, Yaml, PnpmWorkspace, ShellScripts |
62
70
  | `silk()` | + TypeScript |
63
71
 
64
- Extend any preset with options:
65
-
66
- ```typescript
67
- import { Preset } from '@savvy-web/lint-staged';
68
-
69
- export default Preset.standard({
70
- biome: { exclude: ['vendor/'] },
71
- typescript: {}, // Enable TypeScript in standard
72
- });
73
- ```
74
-
75
72
  ## Available Handlers
76
73
 
77
74
  | Handler | Files | Description |
@@ -82,14 +79,26 @@ export default Preset.standard({
82
79
  | `Yaml` | `**/*.{yml,yaml}` | Format and validate |
83
80
  | `PnpmWorkspace` | `pnpm-workspace.yaml` | Sort and format |
84
81
  | `ShellScripts` | `**/*.sh` | Manage permissions |
85
- | `TypeScript` | `*.{ts,tsx}` | TSDoc validation + typecheck |
82
+ | `TypeScript` | `*.{ts,cts,mts,tsx}` | TSDoc validation + typecheck |
83
+
84
+ ## CLI
85
+
86
+ The `savvy-lint` CLI helps bootstrap and validate your setup:
87
+
88
+ ```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
+ ```
86
94
 
87
95
  ## Documentation
88
96
 
89
- - [Handler Configuration](./docs/handlers.md) - Detailed options for each handler
90
- - [Utilities](./docs/utilities.md) - Command, Filter, and advanced utilities
91
- - [Configuration API](./docs/configuration.md) - createConfig and Preset APIs
92
- - [Migration Guide](./docs/migration.md) - Migrating from raw lint-staged configs
97
+ - [Handler Configuration](./docs/handlers.md) -- Detailed options for each handler
98
+ - [Configuration API](./docs/configuration.md) -- createConfig and Preset APIs
99
+ - [CLI Reference](./docs/cli.md) -- `savvy-lint init` and `savvy-lint check`
100
+ - [Utilities](./docs/utilities.md) -- Command, Filter, and advanced utilities
101
+ - [Migration Guide](./docs/migration.md) -- Migrating from raw lint-staged configs
93
102
 
94
103
  ## Contributing
95
104
 
@@ -0,0 +1,126 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
3
+ "assist": {
4
+ "actions": {
5
+ "source": {
6
+ "organizeImports": {
7
+ "level": "on",
8
+ "options": {
9
+ "identifierOrder": "lexicographic"
10
+ }
11
+ }
12
+ }
13
+ },
14
+ "enabled": true
15
+ },
16
+ "formatter": {
17
+ "enabled": true,
18
+ "formatWithErrors": true,
19
+ "indentStyle": "tab",
20
+ "indentWidth": 2,
21
+ "lineWidth": 120
22
+ },
23
+ "json": {
24
+ "assist": {
25
+ "enabled": true
26
+ },
27
+ "formatter": {
28
+ "enabled": true,
29
+ "expand": "auto"
30
+ },
31
+ "linter": {
32
+ "enabled": true
33
+ }
34
+ },
35
+ "css": {
36
+ "parser": {
37
+ "cssModules": true
38
+ },
39
+ "linter": {
40
+ "enabled": true
41
+ }
42
+ },
43
+ "linter": {
44
+ "domains": {
45
+ "project": "all"
46
+ },
47
+ "enabled": true,
48
+ "rules": {
49
+ "correctness": {
50
+ "noUnusedVariables": {
51
+ "level": "error",
52
+ "options": {
53
+ "ignoreRestSiblings": true
54
+ }
55
+ },
56
+ "useImportExtensions": {
57
+ "level": "error",
58
+ "options": {
59
+ "forceJsExtensions": true
60
+ }
61
+ }
62
+ },
63
+ "nursery": {
64
+ "noImportCycles": "error",
65
+ "useExplicitType": "off"
66
+ },
67
+ "recommended": true,
68
+ "style": {
69
+ "useConsistentTypeDefinitions": "error",
70
+ "useImportType": {
71
+ "level": "error",
72
+ "options": {
73
+ "style": "separatedType"
74
+ }
75
+ },
76
+ "useNodejsImportProtocol": "error"
77
+ },
78
+ "suspicious": {
79
+ "noBiomeFirstException": "error",
80
+ "noDuplicateObjectKeys": "error",
81
+ "noQuickfixBiome": "error",
82
+ "useBiomeIgnoreFolder": "error"
83
+ }
84
+ }
85
+ },
86
+ "overrides": [
87
+ {
88
+ "includes": ["package.json"],
89
+ "json": {
90
+ "formatter": {
91
+ "expand": "auto"
92
+ }
93
+ }
94
+ },
95
+ {
96
+ "assist": {
97
+ "actions": {
98
+ "source": {
99
+ "useSortedKeys": "on"
100
+ }
101
+ }
102
+ },
103
+ "includes": ["**/turbo.json", "**/tsconfig.json", "**/tsconfig.*.json"]
104
+ }
105
+ ],
106
+ "root": true,
107
+ "vcs": {
108
+ "clientKind": "git",
109
+ "defaultBranch": "main",
110
+ "enabled": true,
111
+ "useIgnoreFile": true
112
+ },
113
+ "files": {
114
+ "includes": [
115
+ "**",
116
+ "!**/node_modules",
117
+ "!**/dist",
118
+ "!**/.turbo",
119
+ "!**/.git",
120
+ "!**/.rslib",
121
+ "!**/.vitest",
122
+ "!**/.coverage",
123
+ "!coverage"
124
+ ]
125
+ }
126
+ }
package/index.d.ts CHANGED
@@ -798,6 +798,8 @@ export declare interface ImportGraphResult {
798
798
  * @remarks
799
799
  * Creates the necessary configuration files for lint-staged:
800
800
  * - `.husky/pre-commit` hook with managed section
801
+ * - `.husky/post-checkout` and `.husky/post-merge` hooks (when preset includes ShellScripts)
802
+ * - `.markdownlint-cli2.jsonc` config (when preset includes Markdown)
801
803
  * - lint-staged config at the specified path
802
804
  *
803
805
  * The managed section feature allows users to add custom hooks above/below
package/index.js CHANGED
@@ -23,7 +23,7 @@ class PackageJson {
23
23
  }
24
24
  const files = Filter.shellEscape(filtered);
25
25
  const biomeCmd = options.biomeConfig ? `biome check --write --max-diagnostics=none --config-path=${options.biomeConfig} ${files}` : `biome check --write --max-diagnostics=none ${files}`;
26
- return `${biomeCmd} && git add ${files}`;
26
+ return biomeCmd;
27
27
  };
28
28
  }
29
29
  }
@@ -76,7 +76,6 @@ class PnpmWorkspace {
76
76
  if (!skipSort || !skipFormat) {
77
77
  const formatted = stringify(parsed, DEFAULT_STRINGIFY_OPTIONS);
78
78
  writeFileSync(filepath, formatted, "utf-8");
79
- return `git add ${filepath}`;
80
79
  }
81
80
  return [];
82
81
  };
@@ -141,7 +140,6 @@ class Yaml {
141
140
  } catch (error) {
142
141
  throw new Error(`Invalid YAML in ${filepath}: ${error instanceof Error ? error.message : String(error)}`);
143
142
  }
144
- if (!skipFormat && filtered.length > 0) return `git add ${Filter.shellEscape(filtered)}`;
145
143
  return [];
146
144
  };
147
145
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/lint-staged",
3
- "version": "0.2.2",
3
+ "version": "0.3.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": [
@@ -33,7 +33,8 @@
33
33
  ".": {
34
34
  "types": "./index.d.ts",
35
35
  "import": "./index.js"
36
- }
36
+ },
37
+ "./biome/silk.jsonc": "./biome/silk.jsonc"
37
38
  },
38
39
  "bin": {
39
40
  "savvy-lint": "./bin/savvy-lint.js"
@@ -47,12 +48,13 @@
47
48
  "effect": "^3.19.16",
48
49
  "eslint": "^9.39.2",
49
50
  "eslint-plugin-tsdoc": "^0.5.0",
51
+ "jsonc-parser": "^3.3.1",
50
52
  "sort-package-json": "^3.6.1",
51
53
  "workspace-tools": "^0.41.0",
52
54
  "yaml": "^2.8.2"
53
55
  },
54
56
  "peerDependencies": {
55
- "@biomejs/biome": "^2.3.12",
57
+ "@biomejs/biome": "2.3.14",
56
58
  "husky": "^9.1.7",
57
59
  "lint-staged": "^16.2.7",
58
60
  "markdownlint-cli2": "^0.20.0",
@@ -75,20 +77,6 @@
75
77
  "optional": false
76
78
  }
77
79
  },
78
- "devEngines": {
79
- "packageManager": {
80
- "name": "pnpm",
81
- "version": "10.28.2",
82
- "onFail": "ignore"
83
- },
84
- "runtime": [
85
- {
86
- "name": "node",
87
- "version": "24.11.0",
88
- "onFail": "ignore"
89
- }
90
- ]
91
- },
92
80
  "scripts": {
93
81
  "postinstall": "savvy-lint check --quiet || true"
94
82
  },
@@ -100,6 +88,7 @@
100
88
  "LICENSE",
101
89
  "README.md",
102
90
  "bin/savvy-lint.js",
91
+ "biome/silk.jsonc",
103
92
  "index.d.ts",
104
93
  "index.js",
105
94
  "package.json",
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.56.0"
8
+ "packageVersion": "7.56.2"
9
9
  }
10
10
  ]
11
11
  }