@luxkit/cli 1.1.2 → 1.1.4

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/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { program } from "commander";
6
6
  // src/commands/fmt.ts
7
7
  import fs3 from "fs";
8
8
  import path5 from "path";
9
+ import chalk2 from "chalk";
9
10
 
10
11
  // src/presets/fmt/electron-vue.ts
11
12
  var electronVueFmt = {
@@ -112,14 +113,25 @@ trim_trailing_whitespace = false
112
113
  "@stylistic/stylelint-plugin",
113
114
  "postcss-html",
114
115
  "postcss-scss",
115
- "cspell"
116
+ "cspell",
117
+ "husky",
118
+ "lint-staged"
116
119
  ]
117
120
  },
118
121
  scripts: {
119
122
  lint: 'eslint . --cache --cache-location node_modules/.cache/eslint && cspell --cache --cache-location node_modules/.cache/cspell --gitignore "src/**/*" && vue-tsc --noEmit && stylelint "src/**/*.{css,scss,vue}" --cache --cache-strategy content --cache-location node_modules/.cache/stylelint/',
120
123
  "lint:fix": 'eslint . --cache --cache-location node_modules/.cache/eslint --fix && stylelint "src/**/*.{css,scss,vue}" --fix --cache --cache-strategy content --cache-location node_modules/.cache/stylelint/',
121
- format: 'prettier --write "src/**/*.{ts,js,json,vue,css,scss}"'
122
- }
124
+ format: 'prettier --write "src/**/*.{ts,js,json,vue,css,scss}"',
125
+ "lint-staged": "lint-staged"
126
+ },
127
+ lintStaged: () => JSON.stringify(
128
+ {
129
+ "*.{ts,js,vue}": ["eslint --fix", "prettier --write"],
130
+ "*.{css,scss,vue}": ["stylelint --fix", "prettier --write"]
131
+ },
132
+ null,
133
+ 2
134
+ ) + "\n"
123
135
  };
124
136
 
125
137
  // src/presets/fmt/nest.ts
@@ -175,13 +187,21 @@ trim_trailing_whitespace = true
175
187
  trim_trailing_whitespace = false
176
188
  `,
177
189
  dependencies: {
178
- dev: ["prettier", "cspell"]
190
+ dev: ["prettier", "cspell", "husky", "lint-staged"]
179
191
  },
180
192
  scripts: {
181
193
  lint: 'eslint "{src,apps,libs,test}/**/*.ts" --cache --cache-location node_modules/.cache/eslint && cspell --cache --cache-location node_modules/.cache/cspell --gitignore "src/**/*" && tsc --noEmit',
182
194
  "lint:fix": 'eslint "{src,apps,libs,test}/**/*.ts" --cache --cache-location node_modules/.cache/eslint --fix',
183
- format: 'prettier --write "src/**/*.{ts,js,json}"'
184
- }
195
+ format: 'prettier --write "src/**/*.{ts,js,json}"',
196
+ "lint-staged": "lint-staged"
197
+ },
198
+ lintStaged: () => JSON.stringify(
199
+ {
200
+ "*.{ts,js}": ["eslint --fix", "prettier --write"]
201
+ },
202
+ null,
203
+ 2
204
+ ) + "\n"
185
205
  };
186
206
 
187
207
  // src/presets/fmt/node.ts
@@ -286,14 +306,24 @@ trim_trailing_whitespace = false
286
306
  "eslint-plugin-prettier",
287
307
  "eslint-config-prettier",
288
308
  "prettier",
289
- "cspell"
309
+ "cspell",
310
+ "husky",
311
+ "lint-staged"
290
312
  ]
291
313
  },
292
314
  scripts: {
293
315
  lint: 'eslint . --cache --cache-location node_modules/.cache/eslint && cspell --cache --cache-location node_modules/.cache/cspell --gitignore "src/**/*" && tsc --noEmit',
294
316
  "lint:fix": "eslint . --cache --cache-location node_modules/.cache/eslint --fix",
295
- format: 'prettier --write "src/**/*.{ts,js,json}"'
296
- }
317
+ format: 'prettier --write "src/**/*.{ts,js,json}"',
318
+ "lint-staged": "lint-staged"
319
+ },
320
+ lintStaged: () => JSON.stringify(
321
+ {
322
+ "*.{ts,js}": ["eslint --fix", "prettier --write"]
323
+ },
324
+ null,
325
+ 2
326
+ ) + "\n"
297
327
  };
298
328
 
299
329
  // src/presets/fmt/uniapp.ts
@@ -399,14 +429,25 @@ trim_trailing_whitespace = false
399
429
  "@stylistic/stylelint-plugin",
400
430
  "postcss-html",
401
431
  "postcss-scss",
402
- "cspell"
432
+ "cspell",
433
+ "husky",
434
+ "lint-staged"
403
435
  ]
404
436
  },
405
437
  scripts: {
406
438
  lint: 'eslint . --cache --cache-location node_modules/.cache/eslint && cspell --cache --cache-location node_modules/.cache/cspell --gitignore "src/**/*" && vue-tsc --noEmit && stylelint "src/**/*.{css,scss,vue}" --cache --cache-strategy content --cache-location node_modules/.cache/stylelint/',
407
439
  "lint:fix": 'eslint . --cache --cache-location node_modules/.cache/eslint --fix && stylelint "src/**/*.{css,scss,vue}" --fix --cache --cache-strategy content --cache-location node_modules/.cache/stylelint/',
408
- format: 'prettier --write "src/**/*.{ts,js,json,vue,css,scss}"'
409
- }
440
+ format: 'prettier --write "src/**/*.{ts,js,json,vue,css,scss}"',
441
+ "lint-staged": "lint-staged"
442
+ },
443
+ lintStaged: () => JSON.stringify(
444
+ {
445
+ "*.{ts,js,vue}": ["eslint --fix", "prettier --write"],
446
+ "*.{css,scss,vue}": ["stylelint --fix", "prettier --write"]
447
+ },
448
+ null,
449
+ 2
450
+ ) + "\n"
410
451
  };
411
452
 
412
453
  // src/presets/fmt/web-react.ts
@@ -519,14 +560,25 @@ trim_trailing_whitespace = false
519
560
  "stylelint-scss",
520
561
  "@stylistic/stylelint-plugin",
521
562
  "postcss-scss",
522
- "cspell"
563
+ "cspell",
564
+ "husky",
565
+ "lint-staged"
523
566
  ]
524
567
  },
525
568
  scripts: {
526
569
  lint: 'eslint . --cache --cache-location node_modules/.cache/eslint && cspell --cache --cache-location node_modules/.cache/cspell --gitignore "src/**/*" && tsc --noEmit && stylelint "src/**/*.{css,scss}" --cache --cache-strategy content --cache-location node_modules/.cache/stylelint/',
527
570
  "lint:fix": 'eslint . --cache --cache-location node_modules/.cache/eslint --fix && stylelint "src/**/*.{css,scss}" --fix --cache --cache-strategy content --cache-location node_modules/.cache/stylelint/',
528
- format: 'prettier --write "src/**/*.{ts,js,json,jsx,tsx,css,scss}"'
529
- }
571
+ format: 'prettier --write "src/**/*.{ts,js,json,jsx,tsx,css,scss}"',
572
+ "lint-staged": "lint-staged"
573
+ },
574
+ lintStaged: () => JSON.stringify(
575
+ {
576
+ "*.{ts,tsx,js,jsx}": ["eslint --fix", "prettier --write"],
577
+ "*.{css,scss}": ["stylelint --fix", "prettier --write"]
578
+ },
579
+ null,
580
+ 2
581
+ ) + "\n"
530
582
  };
531
583
 
532
584
  // src/presets/fmt/web-vue.ts
@@ -630,14 +682,25 @@ trim_trailing_whitespace = false
630
682
  "@stylistic/stylelint-plugin",
631
683
  "postcss-html",
632
684
  "postcss-scss",
633
- "cspell"
685
+ "cspell",
686
+ "husky",
687
+ "lint-staged"
634
688
  ]
635
689
  },
636
690
  scripts: {
637
691
  lint: 'eslint . --cache --cache-location node_modules/.cache/eslint && cspell --cache --cache-location node_modules/.cache/cspell --gitignore "src/**/*" && vue-tsc --noEmit && stylelint "src/**/*.{css,scss,vue}" --cache --cache-strategy content --cache-location node_modules/.cache/stylelint/',
638
692
  "lint:fix": 'eslint . --cache --cache-location node_modules/.cache/eslint --fix && stylelint "src/**/*.{css,scss,vue}" --fix --cache --cache-strategy content --cache-location node_modules/.cache/stylelint/',
639
- format: 'prettier --write "src/**/*.{ts,js,json,vue,css,scss}"'
640
- }
693
+ format: 'prettier --write "src/**/*.{ts,js,json,vue,css,scss}"',
694
+ "lint-staged": "lint-staged"
695
+ },
696
+ lintStaged: () => JSON.stringify(
697
+ {
698
+ "*.{ts,js,vue}": ["eslint --fix", "prettier --write"],
699
+ "*.{css,scss,vue}": ["stylelint --fix", "prettier --write"]
700
+ },
701
+ null,
702
+ 2
703
+ ) + "\n"
641
704
  };
642
705
 
643
706
  // src/presets/fmt/index.ts
@@ -668,7 +731,6 @@ var logger = {
668
731
  };
669
732
 
670
733
  // src/utils/errors.ts
671
- import chalk2 from "chalk";
672
734
  var CliError = class extends Error {
673
735
  code;
674
736
  suggestion;
@@ -714,14 +776,6 @@ function levenshtein(a, b) {
714
776
  }
715
777
  function resolvePreset(presets, name) {
716
778
  const found = presets.find((p) => p.name === name);
717
- if (!found) {
718
- const err = new PresetNotFoundError(
719
- name,
720
- presets.map((p) => p.name)
721
- );
722
- console.error(chalk2.red(err.message));
723
- process.exit(1);
724
- }
725
779
  return found;
726
780
  }
727
781
 
@@ -784,7 +838,8 @@ var CONFIG_FILES = [
784
838
  { filename: "stylelint.config.mjs", getContent: (p) => p.stylelint?.() },
785
839
  { filename: ".stylelintignore", getContent: (p) => p.stylelintIgnore?.() },
786
840
  { filename: "cspell.json", getContent: (p) => p.cspell?.() },
787
- { filename: ".editorconfig", getContent: (p) => p.editorconfig?.() }
841
+ { filename: ".editorconfig", getContent: (p) => p.editorconfig?.() },
842
+ { filename: ".lintstagedrc.json", getContent: (p) => p.lintStaged?.() }
788
843
  ];
789
844
  function generateConfigFile(preset, filename, content, opts) {
790
845
  const filepath = path2.join(opts.cwd, filename);
@@ -807,6 +862,8 @@ function generateAllFmt(preset, opts) {
807
862
  for (const { filename, getContent } of CONFIG_FILES) {
808
863
  if (opts.noStylelint && filename.includes("stylelint")) continue;
809
864
  if (opts.noEditorconfig && filename === ".editorconfig") continue;
865
+ if (opts.noCspell && filename.includes("cspell")) continue;
866
+ if (opts.noLintStaged && filename === ".lintstagedrc.json") continue;
810
867
  const content = getContent(preset);
811
868
  if (content === void 0) continue;
812
869
  const action = generateConfigFile(preset, filename, content, opts);
@@ -999,10 +1056,13 @@ var CONFIG_GETTERS = [
999
1056
  { filename: "stylelint.config.mjs", getContent: (p) => p.stylelint?.() },
1000
1057
  { filename: ".stylelintignore", getContent: (p) => p.stylelintIgnore?.() },
1001
1058
  { filename: "cspell.json", getContent: (p) => p.cspell?.() },
1002
- { filename: ".editorconfig", getContent: (p) => p.editorconfig?.() }
1059
+ { filename: ".editorconfig", getContent: (p) => p.editorconfig?.() },
1060
+ { filename: ".lintstagedrc.json", getContent: (p) => p.lintStaged?.() }
1003
1061
  ];
1004
1062
  var STYLELINT_FILES = /* @__PURE__ */ new Set(["stylelint.config.mjs", ".stylelintignore"]);
1005
1063
  var EDITORCONFIG_FILE = ".editorconfig";
1064
+ var CSPELL_FILE = "cspell.json";
1065
+ var LINTSTAGED_FILE = ".lintstagedrc.json";
1006
1066
  var STYLELINT_SETTINGS_PREFIXES = [
1007
1067
  "stylelint.",
1008
1068
  "css.validate",
@@ -1019,6 +1079,8 @@ var STYLELINT_DEPS = /* @__PURE__ */ new Set([
1019
1079
  "postcss-scss"
1020
1080
  ]);
1021
1081
  var STYLELINT_EXTENSION = "stylelint.vscode-stylelint";
1082
+ var HUSKY_DEPS = /* @__PURE__ */ new Set(["husky"]);
1083
+ var LINTSTAGED_DEPS = /* @__PURE__ */ new Set(["lint-staged"]);
1022
1084
  function getLuxDir() {
1023
1085
  return process.env.LUX_HOME || path4.join(os.homedir(), ".lux");
1024
1086
  }
@@ -1031,6 +1093,28 @@ function getLocalPresetDir(type, presetName) {
1031
1093
  function isValidPresetName(name) {
1032
1094
  return name.length > 0 && !name.includes("/") && !name.includes("\\") && !name.includes("..");
1033
1095
  }
1096
+ function listCustomPresets() {
1097
+ const fmtDir = path4.join(getLuxDir(), "preset", "fmt");
1098
+ if (!fs2.existsSync(fmtDir)) return [];
1099
+ const entries = fs2.readdirSync(fmtDir, { withFileTypes: true });
1100
+ const result = [];
1101
+ for (const entry of entries) {
1102
+ if (!entry.isDirectory()) continue;
1103
+ if (!isValidPresetName(entry.name)) continue;
1104
+ const pkgPath = path4.join(fmtDir, entry.name, "package.json");
1105
+ if (fs2.existsSync(pkgPath)) {
1106
+ result.push(entry.name);
1107
+ }
1108
+ }
1109
+ return result;
1110
+ }
1111
+ function isValidCustomPreset(name) {
1112
+ if (!isValidPresetName(name)) return false;
1113
+ const presetDir = path4.join(getLuxDir(), "preset", "fmt", name);
1114
+ if (!fs2.existsSync(presetDir)) return false;
1115
+ const pkgPath = path4.join(presetDir, "package.json");
1116
+ return fs2.existsSync(pkgPath);
1117
+ }
1034
1118
  function localPresetExists(type, presetName) {
1035
1119
  const dir = getLocalPresetDir(type, presetName);
1036
1120
  return fs2.existsSync(dir);
@@ -1115,6 +1199,8 @@ function applyLocalFmtPreset(cwd, presetName, opts) {
1115
1199
  for (const filename of entries) {
1116
1200
  if (opts.noStylelint && STYLELINT_FILES.has(filename)) continue;
1117
1201
  if (opts.noEditorconfig && filename === EDITORCONFIG_FILE) continue;
1202
+ if (opts.noCspell && filename === CSPELL_FILE) continue;
1203
+ if (opts.noLintStaged && filename === LINTSTAGED_FILE) continue;
1118
1204
  const destPath = path4.join(cwd, filename);
1119
1205
  const exists = fileExists(destPath);
1120
1206
  if (exists && !opts.force) {
@@ -1235,6 +1321,9 @@ function mergeTemplateIntoProject(templatePkg, projectPkg, pm, opts, result) {
1235
1321
  for (const [dep, version] of Object.entries(templatePkg.devDependencies)) {
1236
1322
  if (opts.noStylelint && STYLELINT_DEPS.has(dep)) continue;
1237
1323
  if (opts.noEditorconfig && dep.includes("editorconfig")) continue;
1324
+ if (opts.noCspell && dep === "cspell") continue;
1325
+ if (opts.noHusky && HUSKY_DEPS.has(dep)) continue;
1326
+ if (opts.noLintStaged && LINTSTAGED_DEPS.has(dep)) continue;
1238
1327
  if (existingDeps[dep] === void 0 && version !== "<latest>") {
1239
1328
  newDeps[dep] = version;
1240
1329
  }
@@ -1244,11 +1333,15 @@ function mergeTemplateIntoProject(templatePkg, projectPkg, pm, opts, result) {
1244
1333
  if (templatePkg.scripts) {
1245
1334
  const existingScripts = merged.scripts ?? {};
1246
1335
  const newScripts = { ...existingScripts };
1247
- for (const [key, value] of Object.entries(templatePkg.scripts)) {
1248
- let resolved = value.replace(/<pm>/g, prefix);
1249
- if (opts.noStylelint) {
1250
- resolved = resolved.replace(/\s*&&\s*stylelint\s+"[^"]*".*/g, "");
1251
- }
1336
+ const filteredScripts = filterScripts(
1337
+ templatePkg.scripts,
1338
+ opts.noStylelint,
1339
+ opts.noEditorconfig,
1340
+ opts.noCspell,
1341
+ opts.noLintStaged
1342
+ );
1343
+ for (const [key, value] of Object.entries(filteredScripts)) {
1344
+ const resolved = value.replace(/<pm>/g, prefix);
1252
1345
  if (existingScripts[key] !== void 0 && !opts.force) {
1253
1346
  result.scriptsSkipped++;
1254
1347
  if (opts.dryRun) {
@@ -1283,6 +1376,57 @@ function filterStylelintSettings(settings) {
1283
1376
  }
1284
1377
  return filtered;
1285
1378
  }
1379
+ function filterScripts(scripts, noStylelint, noEditorconfig, noCspell, noLintStaged = false) {
1380
+ const filtered = {};
1381
+ for (const [key, value] of Object.entries(scripts)) {
1382
+ if (noStylelint && key.includes("stylelint")) continue;
1383
+ if (noEditorconfig && key.includes("editorconfig")) continue;
1384
+ if (noCspell && key.includes("cspell")) continue;
1385
+ if (noLintStaged && key.includes("lint-staged")) continue;
1386
+ let resolved = value;
1387
+ if (noStylelint) {
1388
+ resolved = resolved.replace(/\s*&&\s*stylelint\s+"[^"]*".*/g, "");
1389
+ }
1390
+ if (noCspell) {
1391
+ resolved = resolved.replace(/\s*&&\s*cspell\s+[^&]*/g, "");
1392
+ resolved = resolved.replace(/(\S)&&/g, "$1 &&");
1393
+ }
1394
+ filtered[key] = resolved;
1395
+ }
1396
+ return filtered;
1397
+ }
1398
+ function detectPresetCapabilities(presetName) {
1399
+ const presetDir = path4.join(getLuxDir(), "preset", "fmt", presetName);
1400
+ const entries = fs2.readdirSync(presetDir);
1401
+ const hasStylelintFile = entries.some((f) => STYLELINT_FILES.has(f));
1402
+ const pkg = readJson(
1403
+ path4.join(presetDir, "package.json")
1404
+ );
1405
+ const hasStylelintDep = pkg?.devDependencies ? Object.keys(pkg.devDependencies).some((d) => isNotStylelintDep(d) === false) : false;
1406
+ const hasEditorconfigFile = entries.includes(EDITORCONFIG_FILE);
1407
+ const hasEditorconfigDep = pkg?.devDependencies ? Object.keys(pkg.devDependencies).some((d) => !isNotEditorconfigDep(d)) : false;
1408
+ const hasCspellFile = entries.includes(CSPELL_FILE);
1409
+ const hasCspellDep = pkg?.devDependencies ? Object.keys(pkg.devDependencies).some((d) => d === "cspell") : false;
1410
+ const hasLintStagedFile = entries.includes(LINTSTAGED_FILE);
1411
+ const hasLintStagedDep = pkg?.devDependencies ? Object.keys(pkg.devDependencies).some((d) => isNotLintStagedDep(d) === false) : false;
1412
+ return {
1413
+ hasStylelint: hasStylelintFile || hasStylelintDep,
1414
+ hasEditorconfig: hasEditorconfigFile || hasEditorconfigDep,
1415
+ hasCspell: hasCspellFile || hasCspellDep,
1416
+ hasLintStaged: hasLintStagedFile || hasLintStagedDep
1417
+ };
1418
+ }
1419
+ function isNotStylelintDep(dep) {
1420
+ if (dep.includes("stylelint")) return false;
1421
+ if (dep === "postcss-html" || dep === "postcss-scss") return false;
1422
+ return true;
1423
+ }
1424
+ function isNotEditorconfigDep(dep) {
1425
+ return !dep.includes("editorconfig");
1426
+ }
1427
+ function isNotLintStagedDep(dep) {
1428
+ return dep !== "lint-staged";
1429
+ }
1286
1430
  function resolveLocalDeps(deps) {
1287
1431
  const packages = [];
1288
1432
  for (const [name, version] of Object.entries(deps)) {
@@ -1296,27 +1440,33 @@ function resolveLocalDeps(deps) {
1296
1440
  }
1297
1441
 
1298
1442
  // src/commands/fmt.ts
1299
- function filterStylelintScripts(scripts) {
1300
- const filtered = {};
1301
- for (const [key, value] of Object.entries(scripts)) {
1302
- filtered[key] = value.replace(/\s*&&\s*stylelint\s+"[^"]*".*/g, "");
1303
- }
1304
- return filtered;
1305
- }
1306
- function isNotStylelintDep(dep) {
1443
+ function isNotStylelintDep2(dep) {
1307
1444
  if (dep.includes("stylelint")) return false;
1308
1445
  if (dep === "postcss-html" || dep === "postcss-scss") return false;
1309
1446
  return true;
1310
1447
  }
1311
- function isNotEditorconfigDep(dep) {
1448
+ function isNotEditorconfigDep2(dep) {
1312
1449
  return !dep.includes("editorconfig");
1313
1450
  }
1451
+ function isNotCspellDep(dep) {
1452
+ return dep !== "cspell";
1453
+ }
1454
+ function isNotHuskyDep(dep) {
1455
+ return dep !== "husky";
1456
+ }
1457
+ function isNotLintStagedDep2(dep) {
1458
+ return dep !== "lint-staged";
1459
+ }
1314
1460
  function registerFmtCommand(program2) {
1315
1461
  const fmt = program2.command("fmt").description("Initialize formatting config with preset");
1316
- fmt.argument("<preset>").option("-F, --force", "Force overwrite existing files").option("--no-install", "Skip dependency installation").option("--dry-run", "Preview without writing files").option("--stylelint", "Include Stylelint config generation").option("--editorconfig", "Include EditorConfig config generation").option("--reset", "Reset local preset and re-materialize from built-in").action(
1462
+ fmt.argument("<preset>").option("-F, --force", "Force overwrite existing files").option("--no-install", "Skip dependency installation").option("--dry-run", "Preview without writing files").option("--stylelint", "Include Stylelint config generation").option("--editorconfig", "Include EditorConfig config generation").option("--cspell", "Include CSpell config generation").option("--husky", "Initialize husky for Git hooks").option("--lint-staged", "Set up lint-staged (implies --husky)").option("--reset", "Reset local preset and re-materialize from built-in").action(
1317
1463
  async (presetName, options) => {
1318
- const preset = resolvePreset(FMT_PRESETS, presetName);
1319
- if (!preset) return;
1464
+ const builtinPreset = FMT_PRESETS.find((p) => p.name === presetName);
1465
+ const isBuiltin = builtinPreset !== void 0;
1466
+ if (options.reset && !isBuiltin) {
1467
+ logger.warn(`"${presetName}" is a custom preset, --reset has no builtin to restore`);
1468
+ return;
1469
+ }
1320
1470
  const cwd = process.cwd();
1321
1471
  const pkgPath = path5.join(cwd, "package.json");
1322
1472
  if (fileExists(pkgPath)) {
@@ -1329,31 +1479,73 @@ function registerFmtCommand(program2) {
1329
1479
  return;
1330
1480
  }
1331
1481
  }
1332
- if (options.reset) {
1333
- resetLocalPreset("fmt", presetName);
1334
- }
1335
- const useLocal = localPresetExists("fmt", presetName);
1336
- if (useLocal) {
1482
+ if (isBuiltin) {
1483
+ if (options.reset) {
1484
+ resetLocalPreset("fmt", presetName);
1485
+ }
1486
+ const useLocal = localPresetExists("fmt", presetName);
1487
+ if (useLocal) {
1488
+ await executeLocalPath(cwd, presetName, options);
1489
+ } else {
1490
+ await executeBuiltinPath(cwd, presetName, builtinPreset, options);
1491
+ }
1492
+ } else if (isValidCustomPreset(presetName)) {
1337
1493
  await executeLocalPath(cwd, presetName, options);
1338
1494
  } else {
1339
- await executeBuiltinPath(cwd, presetName, preset, options);
1495
+ const builtinNames = new Set(FMT_PRESETS.map((p) => p.name));
1496
+ const customNames = listCustomPresets().filter((n) => !builtinNames.has(n));
1497
+ const allNames = [...builtinNames, ...customNames];
1498
+ const err = new PresetNotFoundError(presetName, allNames);
1499
+ logger.error(err.message);
1500
+ process.exitCode = 1;
1340
1501
  }
1341
1502
  }
1342
1503
  );
1343
1504
  fmt.command("list").description("List available fmt presets").action(() => {
1505
+ const builtinNames = new Set(FMT_PRESETS.map((p) => p.name));
1344
1506
  for (const p of FMT_PRESETS) {
1345
1507
  console.log(`${p.name.padEnd(12)} ${p.description}`);
1346
1508
  }
1509
+ const customs = listCustomPresets().filter((name) => !builtinNames.has(name));
1510
+ for (const name of customs) {
1511
+ console.log(`${name.padEnd(12)} ${chalk2.yellow("(custom)")}`);
1512
+ }
1347
1513
  });
1348
1514
  }
1349
1515
  async function executeLocalPath(cwd, presetName, options) {
1350
1516
  logger.log("Using local custom preset");
1517
+ const caps = detectPresetCapabilities(presetName);
1518
+ if (options.stylelint && !caps.hasStylelint) {
1519
+ logger.warn(
1520
+ "--stylelint has no effect: this custom preset has no stylelint config or dependencies"
1521
+ );
1522
+ }
1523
+ if (options.editorconfig && !caps.hasEditorconfig) {
1524
+ logger.warn(
1525
+ "--editorconfig has no effect: this custom preset has no editorconfig config or dependencies"
1526
+ );
1527
+ }
1528
+ if (options.cspell && !caps.hasCspell) {
1529
+ logger.warn(
1530
+ "--cspell has no effect: this custom preset has no cspell config or dependencies"
1531
+ );
1532
+ }
1533
+ if (options.lintStaged && !caps.hasLintStaged) {
1534
+ logger.warn(
1535
+ "--lint-staged has no effect: this custom preset has no lint-staged config or dependencies"
1536
+ );
1537
+ }
1538
+ const noHusky = options.husky !== true && options.lintStaged !== true;
1539
+ const noLintStaged = options.lintStaged !== true;
1351
1540
  const opts = {
1352
1541
  cwd,
1353
1542
  force: options.force ?? false,
1354
1543
  dryRun: options.dryRun ?? false,
1355
1544
  noStylelint: options.stylelint !== true,
1356
- noEditorconfig: options.editorconfig !== true
1545
+ noEditorconfig: options.editorconfig !== true,
1546
+ noCspell: options.cspell !== true,
1547
+ noHusky,
1548
+ noLintStaged
1357
1549
  };
1358
1550
  let result;
1359
1551
  try {
@@ -1384,7 +1576,10 @@ async function executeLocalPath(cwd, presetName, options) {
1384
1576
  const depsToInstall = filterDeps(
1385
1577
  Object.keys(templatePkg.devDependencies),
1386
1578
  opts.noStylelint,
1387
- opts.noEditorconfig
1579
+ opts.noEditorconfig,
1580
+ opts.noCspell,
1581
+ opts.noHusky,
1582
+ opts.noLintStaged
1388
1583
  );
1389
1584
  const projectPkgPath = path5.join(cwd, "package.json");
1390
1585
  const projectPkg = readJson(projectPkgPath);
@@ -1394,6 +1589,9 @@ async function executeLocalPath(cwd, presetName, options) {
1394
1589
  if (missing.length === 0) return;
1395
1590
  if (opts.dryRun) {
1396
1591
  logger.log(`[dry-run] Would add to package.json: ${missing.join(", ")}`);
1592
+ if (!opts.noHusky) {
1593
+ await initHusky(cwd, pm, opts);
1594
+ }
1397
1595
  return;
1398
1596
  }
1399
1597
  if (options.install === false) {
@@ -1412,6 +1610,9 @@ async function executeLocalPath(cwd, presetName, options) {
1412
1610
  const message = error instanceof Error ? error.message : String(error);
1413
1611
  logger.warn(`Failed to fetch versions: ${message}. You can add dependencies manually.`);
1414
1612
  }
1613
+ if (!opts.noHusky) {
1614
+ await initHusky(cwd, pm, opts);
1615
+ }
1415
1616
  return;
1416
1617
  }
1417
1618
  try {
@@ -1427,15 +1628,23 @@ async function executeLocalPath(cwd, presetName, options) {
1427
1628
  const message = error instanceof Error ? error.message : String(error);
1428
1629
  logger.warn(`Dependency installation failed: ${message}. You can install manually.`);
1429
1630
  }
1631
+ if (!opts.noHusky) {
1632
+ await initHusky(cwd, pm, opts);
1633
+ }
1430
1634
  }
1431
1635
  async function executeBuiltinPath(cwd, presetName, preset, options) {
1432
1636
  const pm = fileExists(path5.join(cwd, "package.json")) ? detectPackageManager(cwd) : void 0;
1637
+ const noHusky = options.husky !== true && options.lintStaged !== true;
1638
+ const noLintStaged = options.lintStaged !== true;
1433
1639
  const opts = {
1434
1640
  cwd,
1435
1641
  force: options.force ?? false,
1436
1642
  dryRun: options.dryRun ?? false,
1437
1643
  noStylelint: options.stylelint !== true,
1438
1644
  noEditorconfig: options.editorconfig !== true,
1645
+ noCspell: options.cspell !== true,
1646
+ noHusky,
1647
+ noLintStaged,
1439
1648
  lockfile: pm ? getLockfileName(pm) : void 0
1440
1649
  };
1441
1650
  const result = generateAllFmt(preset, opts);
@@ -1452,15 +1661,27 @@ async function executeBuiltinPath(cwd, presetName, preset, options) {
1452
1661
  warnMissingPackageJson(preset, options.install !== false);
1453
1662
  return;
1454
1663
  }
1455
- const scripts = opts.noStylelint && preset.scripts ? filterStylelintScripts(preset.scripts) : preset.scripts;
1664
+ const scripts = preset.scripts ? filterScripts(
1665
+ preset.scripts,
1666
+ opts.noStylelint,
1667
+ opts.noEditorconfig,
1668
+ opts.noCspell,
1669
+ opts.noLintStaged
1670
+ ) : void 0;
1456
1671
  if (scripts) {
1457
1672
  await injectScripts(scripts, opts, pm);
1458
1673
  }
1459
1674
  if (!preset.dependencies?.dev) return;
1460
- const devDeps = opts.noStylelint ? preset.dependencies.dev.filter(isNotStylelintDep) : preset.dependencies.dev;
1461
- const finalDeps = opts.noEditorconfig ? devDeps.filter(isNotEditorconfigDep) : devDeps;
1675
+ const devDeps = opts.noStylelint ? preset.dependencies.dev.filter(isNotStylelintDep2) : preset.dependencies.dev;
1676
+ const noEditorconfigDeps = opts.noEditorconfig ? devDeps.filter(isNotEditorconfigDep2) : devDeps;
1677
+ const noCspellDeps = opts.noCspell ? noEditorconfigDeps.filter(isNotCspellDep) : noEditorconfigDeps;
1678
+ const noHuskyDeps = opts.noHusky ? noCspellDeps.filter(isNotHuskyDep) : noCspellDeps;
1679
+ const finalDeps = opts.noLintStaged ? noHuskyDeps.filter(isNotLintStagedDep2) : noHuskyDeps;
1462
1680
  if (opts.dryRun) {
1463
1681
  logger.log(`[dry-run] Would add to package.json: ${finalDeps.join(", ")}`);
1682
+ if (!opts.noHusky) {
1683
+ await initHusky(cwd, pm, opts);
1684
+ }
1464
1685
  return;
1465
1686
  }
1466
1687
  if (options.install === false) {
@@ -1475,6 +1696,9 @@ async function executeBuiltinPath(cwd, presetName, preset, options) {
1475
1696
  const message = error instanceof Error ? error.message : String(error);
1476
1697
  logger.warn(`Failed to fetch versions: ${message}. You can add dependencies manually.`);
1477
1698
  }
1699
+ if (!opts.noHusky) {
1700
+ await initHusky(cwd, pm, opts);
1701
+ }
1478
1702
  return;
1479
1703
  }
1480
1704
  try {
@@ -1485,11 +1709,17 @@ async function executeBuiltinPath(cwd, presetName, preset, options) {
1485
1709
  const message = error instanceof Error ? error.message : String(error);
1486
1710
  logger.warn(`Dependency installation failed: ${message}. You can install manually.`);
1487
1711
  }
1712
+ if (!opts.noHusky) {
1713
+ await initHusky(cwd, pm, opts);
1714
+ }
1488
1715
  }
1489
- function filterDeps(deps, noStylelint, noEditorconfig) {
1716
+ function filterDeps(deps, noStylelint, noEditorconfig, noCspell, noHusky, noLintStaged) {
1490
1717
  let filtered = deps;
1491
- if (noStylelint) filtered = filtered.filter(isNotStylelintDep);
1492
- if (noEditorconfig) filtered = filtered.filter(isNotEditorconfigDep);
1718
+ if (noStylelint) filtered = filtered.filter(isNotStylelintDep2);
1719
+ if (noEditorconfig) filtered = filtered.filter(isNotEditorconfigDep2);
1720
+ if (noCspell) filtered = filtered.filter(isNotCspellDep);
1721
+ if (noHusky) filtered = filtered.filter(isNotHuskyDep);
1722
+ if (noLintStaged) filtered = filtered.filter(isNotLintStagedDep2);
1493
1723
  return filtered;
1494
1724
  }
1495
1725
  function logGenerationResult(result, dryRun) {
@@ -1552,6 +1782,8 @@ function summarizeFiles(filenames) {
1552
1782
  else if (name.includes("stylelint")) categories.add("stylelint");
1553
1783
  else if (name.includes("cspell")) categories.add("cspell");
1554
1784
  else if (name.includes("editorconfig")) categories.add("editorconfig");
1785
+ else if (name.includes("husky")) categories.add("husky");
1786
+ else if (name.includes("lintstagedrc")) categories.add("lint-staged");
1555
1787
  }
1556
1788
  return [...categories].join(", ");
1557
1789
  }
@@ -1590,6 +1822,58 @@ async function injectScripts(scripts, opts, pm) {
1590
1822
  );
1591
1823
  }
1592
1824
  }
1825
+ async function initHusky(cwd, pm, opts) {
1826
+ const pkgPath = path5.join(cwd, "package.json");
1827
+ const pkg = readJson(pkgPath);
1828
+ if (!pkg) {
1829
+ logger.warn("package.json not found, skipping husky setup");
1830
+ return;
1831
+ }
1832
+ const prefix = getRunPrefix(pm);
1833
+ const isYarn = pm === "yarn";
1834
+ const initScriptName = isYarn ? "postinstall" : "prepare";
1835
+ const hookCommand = opts.noLintStaged ? `${prefix} lint` : `${prefix} lint-staged`;
1836
+ const huskyDir = path5.join(cwd, ".husky");
1837
+ const preCommitPath = path5.join(huskyDir, "pre-commit");
1838
+ if (opts.dryRun) {
1839
+ logger.log(`[dry-run] Would create .husky/pre-commit with: ${hookCommand}`);
1840
+ logger.log(`[dry-run] Would inject "${initScriptName}": "husky" script`);
1841
+ logger.log(`[dry-run] Would run ${prefix} ${initScriptName}`);
1842
+ return;
1843
+ }
1844
+ if (fileExists(preCommitPath) && !opts.force) {
1845
+ logger.log("Skipped .husky/pre-commit (already exists)");
1846
+ } else {
1847
+ ensureDir(huskyDir);
1848
+ writeFile(preCommitPath, `${hookCommand}
1849
+ `);
1850
+ fs3.chmodSync(preCommitPath, 493);
1851
+ }
1852
+ const scripts = pkg.scripts ?? {};
1853
+ if (scripts[initScriptName] !== void 0 && !opts.force) {
1854
+ logger.log(`Skipped script "${initScriptName}" (already exists)`);
1855
+ } else {
1856
+ scripts[initScriptName] = "husky";
1857
+ pkg.scripts = scripts;
1858
+ writeJson(pkgPath, pkg);
1859
+ logger.log(`Injected "${initScriptName}" script for husky`);
1860
+ }
1861
+ logger.log(`Running ${prefix} ${initScriptName} to initialize git hooks...`);
1862
+ try {
1863
+ const args = isYarn ? ["postinstall"] : ["run", initScriptName];
1864
+ const { exitCode } = await execFileNoThrow(pm, args, { cwd });
1865
+ if (exitCode === 0) {
1866
+ logger.success("Husky initialized successfully");
1867
+ } else {
1868
+ logger.warn(`Husky init script exited with code ${exitCode}`);
1869
+ }
1870
+ } catch (error) {
1871
+ const message = error instanceof Error ? error.message : String(error);
1872
+ logger.warn(
1873
+ `Husky init failed: ${message}. You can run "${prefix} ${initScriptName}" manually.`
1874
+ );
1875
+ }
1876
+ }
1593
1877
 
1594
1878
  // src/commands/init.ts
1595
1879
  import { select, isCancel, cancel, outro } from "@clack/prompts";
@@ -2448,7 +2732,10 @@ function materializeAllPresets() {
2448
2732
  force: false,
2449
2733
  dryRun: false,
2450
2734
  noStylelint: false,
2451
- noEditorconfig: false
2735
+ noEditorconfig: false,
2736
+ noCspell: false,
2737
+ noHusky: false,
2738
+ noLintStaged: false
2452
2739
  };
2453
2740
  for (const preset of FMT_PRESETS) {
2454
2741
  materializeFmtPreset(preset.name, preset, opts);
@@ -2676,7 +2963,15 @@ function registerVscodeCommand(program2) {
2676
2963
  vscode.argument("<preset>").option("-F, --force", "Force overwrite existing files").option("--dry-run", "Preview without writing files").option("--stylelint", "Include Stylelint settings and extension").option("--reset", "Reset local preset and re-materialize from built-in").action(
2677
2964
  async (presetName, options) => {
2678
2965
  const preset = resolvePreset(VSCODE_PRESETS, presetName);
2679
- if (!preset) return;
2966
+ if (!preset) {
2967
+ const err = new PresetNotFoundError(
2968
+ presetName,
2969
+ VSCODE_PRESETS.map((p) => p.name)
2970
+ );
2971
+ logger.error(err.message);
2972
+ process.exitCode = 1;
2973
+ return;
2974
+ }
2680
2975
  const cwd = process.cwd();
2681
2976
  if (options.reset) {
2682
2977
  resetLocalPreset("vscode", presetName);
@@ -2702,7 +2997,10 @@ function executeVscodeLocalPath(cwd, presetName, options) {
2702
2997
  force: options.force ?? false,
2703
2998
  dryRun: options.dryRun ?? false,
2704
2999
  noStylelint: options.stylelint !== true,
2705
- noEditorconfig: false
3000
+ noEditorconfig: false,
3001
+ noCspell: false,
3002
+ noHusky: true,
3003
+ noLintStaged: true
2706
3004
  };
2707
3005
  const result = applyLocalVscodePreset(cwd, presetName, opts);
2708
3006
  const files = [...result.created, ...result.overwritten];
@@ -2722,7 +3020,10 @@ function executeVscodeBuiltinPath(cwd, presetName, preset, options) {
2722
3020
  force: options.force ?? false,
2723
3021
  dryRun: options.dryRun ?? false,
2724
3022
  noStylelint: options.stylelint !== true,
2725
- noEditorconfig: false
3023
+ noEditorconfig: false,
3024
+ noCspell: false,
3025
+ noHusky: true,
3026
+ noLintStaged: true
2726
3027
  };
2727
3028
  const result = generateAllVscode(preset, opts);
2728
3029
  const files = [...result.created, ...result.overwritten];