@savvy-web/lint-staged 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/376.js ADDED
@@ -0,0 +1,1198 @@
1
+ import { Command, Options } from "@effect/cli";
2
+ import { NodeContext, NodeRuntime } from "@effect/platform-node";
3
+ import { Effect } from "effect";
4
+ import { FileSystem } from "@effect/platform";
5
+ import { execSync } from "node:child_process";
6
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
8
+ import { cosmiconfigSync, defaultLoaders } from "cosmiconfig";
9
+ import parser from "@typescript-eslint/parser";
10
+ import { ESLint } from "eslint";
11
+ import eslint_plugin_tsdoc from "eslint-plugin-tsdoc";
12
+ import { getWorkspaceInfos } from "workspace-tools";
13
+ import typescript from "typescript";
14
+ const VALID_COMMAND_PATTERN = /^[\w@/-]+$/;
15
+ function validateCommandName(name) {
16
+ if (!VALID_COMMAND_PATTERN.test(name)) throw new Error(`Invalid command name: "${name}". Only alphanumeric characters, hyphens, underscores, @ and / are allowed.`);
17
+ }
18
+ class Command_Command {
19
+ static cachedPackageManager = null;
20
+ static detectPackageManager(cwd = process.cwd()) {
21
+ if (null !== Command_Command.cachedPackageManager) return Command_Command.cachedPackageManager;
22
+ const packageJsonPath = join(cwd, "package.json");
23
+ if (!existsSync(packageJsonPath)) {
24
+ Command_Command.cachedPackageManager = "npm";
25
+ return "npm";
26
+ }
27
+ try {
28
+ const content = readFileSync(packageJsonPath, "utf-8");
29
+ const pkg = JSON.parse(content);
30
+ if (pkg.packageManager) {
31
+ const match = pkg.packageManager.match(/^(npm|pnpm|yarn|bun)@/);
32
+ if (match) {
33
+ Command_Command.cachedPackageManager = match[1];
34
+ return Command_Command.cachedPackageManager;
35
+ }
36
+ }
37
+ } catch {}
38
+ Command_Command.cachedPackageManager = "npm";
39
+ return "npm";
40
+ }
41
+ static getExecPrefix(packageManager) {
42
+ switch(packageManager){
43
+ case "pnpm":
44
+ return [
45
+ "pnpm",
46
+ "exec"
47
+ ];
48
+ case "yarn":
49
+ return [
50
+ "yarn",
51
+ "exec"
52
+ ];
53
+ case "bun":
54
+ return [
55
+ "bunx"
56
+ ];
57
+ default:
58
+ return [
59
+ "npx",
60
+ "--no"
61
+ ];
62
+ }
63
+ }
64
+ static clearCache() {
65
+ Command_Command.cachedPackageManager = null;
66
+ }
67
+ static isAvailable(command) {
68
+ validateCommandName(command);
69
+ try {
70
+ execSync(`command -v ${command}`, {
71
+ stdio: "ignore"
72
+ });
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+ static findTool(tool) {
79
+ validateCommandName(tool);
80
+ if (Command_Command.isAvailable(tool)) return {
81
+ available: true,
82
+ command: tool,
83
+ source: "global"
84
+ };
85
+ const pm = Command_Command.detectPackageManager();
86
+ const prefix = Command_Command.getExecPrefix(pm);
87
+ const execCmd = [
88
+ ...prefix,
89
+ tool
90
+ ].join(" ");
91
+ try {
92
+ execSync(`${execCmd} --version`, {
93
+ stdio: "ignore"
94
+ });
95
+ return {
96
+ available: true,
97
+ command: execCmd,
98
+ source: pm
99
+ };
100
+ } catch {}
101
+ return {
102
+ available: false,
103
+ command: void 0,
104
+ source: void 0
105
+ };
106
+ }
107
+ static requireTool(tool, errorMessage) {
108
+ const result = Command_Command.findTool(tool);
109
+ if (!result.available || !result.command) throw new Error(errorMessage ?? `Required tool '${tool}' is not available. Install it globally or add it as a dev dependency.`);
110
+ return result.command;
111
+ }
112
+ static exec(command) {
113
+ return execSync(command, {
114
+ encoding: "utf-8"
115
+ }).trim();
116
+ }
117
+ static execSilent(command) {
118
+ try {
119
+ execSync(command, {
120
+ stdio: "ignore"
121
+ });
122
+ return true;
123
+ } catch {
124
+ return false;
125
+ }
126
+ }
127
+ }
128
+ const TOOL_CONFIGS = {
129
+ markdownlint: {
130
+ moduleName: "markdownlint-cli2",
131
+ libConfigFiles: [
132
+ ".markdownlint-cli2.jsonc",
133
+ ".markdownlint-cli2.json",
134
+ ".markdownlint-cli2.yaml",
135
+ ".markdownlint-cli2.cjs",
136
+ ".markdownlint.jsonc",
137
+ ".markdownlint.json",
138
+ ".markdownlint.yaml"
139
+ ],
140
+ standardPlaces: [
141
+ ".markdownlint-cli2.jsonc",
142
+ ".markdownlint-cli2.json",
143
+ ".markdownlint-cli2.yaml",
144
+ ".markdownlint-cli2.cjs",
145
+ ".markdownlint.jsonc",
146
+ ".markdownlint.json",
147
+ ".markdownlint.yaml"
148
+ ]
149
+ },
150
+ biome: {
151
+ moduleName: "biome",
152
+ libConfigFiles: [
153
+ "biome.jsonc",
154
+ "biome.json"
155
+ ],
156
+ standardPlaces: [
157
+ "biome.jsonc",
158
+ "biome.json"
159
+ ]
160
+ },
161
+ eslint: {
162
+ moduleName: "eslint",
163
+ libConfigFiles: [
164
+ "eslint.config.ts",
165
+ "eslint.config.js",
166
+ "eslint.config.mjs"
167
+ ],
168
+ standardPlaces: [
169
+ "eslint.config.ts",
170
+ "eslint.config.js",
171
+ "eslint.config.mjs"
172
+ ]
173
+ },
174
+ prettier: {
175
+ moduleName: "prettier",
176
+ libConfigFiles: [
177
+ ".prettierrc",
178
+ ".prettierrc.json",
179
+ ".prettierrc.yaml",
180
+ ".prettierrc.js",
181
+ "prettier.config.js"
182
+ ],
183
+ standardPlaces: [
184
+ ".prettierrc",
185
+ ".prettierrc.json",
186
+ ".prettierrc.yaml",
187
+ ".prettierrc.js",
188
+ "prettier.config.js",
189
+ "package.json"
190
+ ]
191
+ }
192
+ };
193
+ class ConfigSearch {
194
+ static libConfigDir = "lib/configs";
195
+ static find(tool, options = {}) {
196
+ const config = TOOL_CONFIGS[tool];
197
+ if (!config) return {
198
+ filepath: void 0,
199
+ found: false
200
+ };
201
+ return ConfigSearch.findFile(config.moduleName, {
202
+ libConfigFiles: config.libConfigFiles,
203
+ standardPlaces: config.standardPlaces,
204
+ ...options
205
+ });
206
+ }
207
+ static findFile(moduleName, options = {}) {
208
+ const { searchFrom = process.cwd(), stopDir, libConfigFiles = [], standardPlaces = [] } = options;
209
+ const loaders = {
210
+ ".jsonc": defaultLoaders[".json"],
211
+ ".yaml": defaultLoaders[".yaml"],
212
+ ".yml": defaultLoaders[".yaml"]
213
+ };
214
+ const libConfigDir = join(searchFrom, ConfigSearch.libConfigDir);
215
+ for (const file of libConfigFiles){
216
+ const filepath = join(libConfigDir, file);
217
+ if (existsSync(filepath)) return {
218
+ filepath,
219
+ found: true
220
+ };
221
+ }
222
+ if (0 === standardPlaces.length) return {
223
+ filepath: void 0,
224
+ found: false
225
+ };
226
+ try {
227
+ const explorer = cosmiconfigSync(moduleName, {
228
+ searchPlaces: standardPlaces,
229
+ loaders,
230
+ ...void 0 !== stopDir && {
231
+ stopDir
232
+ }
233
+ });
234
+ const result = explorer.search(searchFrom);
235
+ if (result?.filepath) return {
236
+ filepath: result.filepath,
237
+ found: true
238
+ };
239
+ } catch {}
240
+ return {
241
+ filepath: void 0,
242
+ found: false
243
+ };
244
+ }
245
+ static exists(filepath) {
246
+ return existsSync(filepath);
247
+ }
248
+ static resolve(filename, fallback) {
249
+ const libPath = `${ConfigSearch.libConfigDir}/${filename}`;
250
+ if (ConfigSearch.exists(libPath)) return libPath;
251
+ return fallback;
252
+ }
253
+ }
254
+ class Filter {
255
+ static exclude(filenames, patterns) {
256
+ if (0 === patterns.length) return [
257
+ ...filenames
258
+ ];
259
+ return filenames.filter((file)=>!patterns.some((pattern)=>file.includes(pattern)));
260
+ }
261
+ static include(filenames, patterns) {
262
+ if (0 === patterns.length) return [];
263
+ return filenames.filter((file)=>patterns.some((pattern)=>file.includes(pattern)));
264
+ }
265
+ static apply(filenames, options) {
266
+ let result = [
267
+ ...filenames
268
+ ];
269
+ if (options.include && options.include.length > 0) result = Filter.include(result, options.include);
270
+ if (options.exclude && options.exclude.length > 0) result = Filter.exclude(result, options.exclude);
271
+ return result;
272
+ }
273
+ }
274
+ class Biome {
275
+ static glob = "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}";
276
+ static defaultExcludes = [
277
+ "package-lock.json",
278
+ "__fixtures__"
279
+ ];
280
+ static handler = Biome.create();
281
+ static findBiome() {
282
+ const result = Command_Command.findTool("biome");
283
+ return result.command;
284
+ }
285
+ static isAvailable() {
286
+ return Command_Command.findTool("biome").available;
287
+ }
288
+ static findConfig() {
289
+ const result = ConfigSearch.find("biome");
290
+ return result.filepath;
291
+ }
292
+ static create(options = {}) {
293
+ const excludes = options.exclude ?? [
294
+ ...Biome.defaultExcludes
295
+ ];
296
+ const config = options.config ?? Biome.findConfig();
297
+ return (filenames)=>{
298
+ const filtered = Filter.exclude(filenames, excludes);
299
+ if (0 === filtered.length) return [];
300
+ const biomeCmd = Command_Command.requireTool("biome", "Biome is not available. Install it globally (recommended) or add @biomejs/biome as a dev dependency.");
301
+ const files = filtered.join(" ");
302
+ const flags = options.flags ?? [];
303
+ const configFlag = config ? `--config-path=${config}` : "";
304
+ const cmd = [
305
+ `${biomeCmd} check --write --no-errors-on-unmatched`,
306
+ configFlag,
307
+ ...flags,
308
+ files
309
+ ].filter(Boolean).join(" ");
310
+ return cmd;
311
+ };
312
+ }
313
+ }
314
+ class Markdown {
315
+ static glob = "**/*.{md,mdx}";
316
+ static defaultExcludes = [];
317
+ static handler = Markdown.create();
318
+ static findMarkdownlint() {
319
+ const result = Command_Command.findTool("markdownlint-cli2");
320
+ return result.command;
321
+ }
322
+ static isAvailable() {
323
+ return Command_Command.findTool("markdownlint-cli2").available;
324
+ }
325
+ static findConfig() {
326
+ const result = ConfigSearch.find("markdownlint");
327
+ return result.filepath;
328
+ }
329
+ static create(options = {}) {
330
+ const excludes = options.exclude ?? [
331
+ ...Markdown.defaultExcludes
332
+ ];
333
+ const noFix = options.noFix ?? false;
334
+ const config = options.config ?? Markdown.findConfig();
335
+ return (filenames)=>{
336
+ const filtered = Filter.exclude(filenames, excludes);
337
+ if (0 === filtered.length) return [];
338
+ const mdlintCmd = Command_Command.requireTool("markdownlint-cli2", "markdownlint-cli2 is not available. Install it globally or add it as a dev dependency.");
339
+ const files = filtered.join(" ");
340
+ const fixFlag = noFix ? "" : "--fix";
341
+ const configFlag = config ? `--config '${config}'` : "";
342
+ const cmd = [
343
+ mdlintCmd,
344
+ configFlag,
345
+ fixFlag,
346
+ files
347
+ ].filter(Boolean).join(" ");
348
+ return cmd;
349
+ };
350
+ }
351
+ }
352
+ class TsDocLinter {
353
+ eslint;
354
+ constructor(options = {}){
355
+ const ignorePatterns = options.ignorePatterns ?? [];
356
+ const config = [
357
+ {
358
+ ignores: [
359
+ "**/node_modules/**",
360
+ "**/dist/**",
361
+ "**/coverage/**",
362
+ ...ignorePatterns
363
+ ]
364
+ },
365
+ {
366
+ files: [
367
+ "**/*.ts",
368
+ "**/*.tsx",
369
+ "**/*.mts",
370
+ "**/*.cts"
371
+ ],
372
+ languageOptions: {
373
+ parser: parser
374
+ },
375
+ plugins: {
376
+ tsdoc: eslint_plugin_tsdoc
377
+ },
378
+ rules: {
379
+ "tsdoc/syntax": "error"
380
+ }
381
+ }
382
+ ];
383
+ this.eslint = new ESLint({
384
+ overrideConfigFile: true,
385
+ overrideConfig: config
386
+ });
387
+ }
388
+ async lintFiles(filePaths) {
389
+ if (0 === filePaths.length) return [];
390
+ const results = await this.eslint.lintFiles(filePaths);
391
+ return results.map((result)=>({
392
+ filePath: result.filePath,
393
+ errorCount: result.errorCount,
394
+ warningCount: result.warningCount,
395
+ messages: result.messages.map((msg)=>({
396
+ line: msg.line,
397
+ column: msg.column,
398
+ severity: msg.severity,
399
+ message: msg.message,
400
+ ruleId: msg.ruleId
401
+ }))
402
+ }));
403
+ }
404
+ async lintFilesAndThrow(filePaths) {
405
+ const results = await this.lintFiles(filePaths);
406
+ const errors = [];
407
+ for (const result of results)if (result.errorCount > 0) {
408
+ for (const msg of result.messages)if (2 === msg.severity) errors.push(`${result.filePath}:${msg.line}:${msg.column} - ${msg.message}`);
409
+ }
410
+ if (errors.length > 0) throw new Error(`TSDoc validation failed:\n${errors.join("\n")}`);
411
+ }
412
+ static formatResults(results) {
413
+ const lines = [];
414
+ for (const result of results)if (0 !== result.errorCount || 0 !== result.warningCount) {
415
+ lines.push(`\n${result.filePath}`);
416
+ for (const msg of result.messages){
417
+ const severity = 2 === msg.severity ? "error" : "warning";
418
+ const rule = msg.ruleId ? ` (${msg.ruleId})` : "";
419
+ lines.push(` ${msg.line}:${msg.column} ${severity} ${msg.message}${rule}`);
420
+ }
421
+ }
422
+ const totalErrors = results.reduce((sum, r)=>sum + r.errorCount, 0);
423
+ const totalWarnings = results.reduce((sum, r)=>sum + r.warningCount, 0);
424
+ if (totalErrors > 0 || totalWarnings > 0) lines.push(`\n✖ ${totalErrors} error(s), ${totalWarnings} warning(s)`);
425
+ return lines.join("\n");
426
+ }
427
+ static hasErrors(results) {
428
+ return results.some((r)=>r.errorCount > 0);
429
+ }
430
+ }
431
+ const TS_EXTENSIONS = [
432
+ ".ts",
433
+ ".tsx",
434
+ ".mts",
435
+ ".cts"
436
+ ];
437
+ class EntryExtractor {
438
+ extract(packageJson) {
439
+ const entries = {};
440
+ const unresolved = [];
441
+ const { exports } = packageJson;
442
+ if (!exports) {
443
+ const mainEntry = packageJson.module ?? packageJson.main;
444
+ if (mainEntry && this.isTypeScriptFile(mainEntry)) entries["."] = mainEntry;
445
+ else if (mainEntry) unresolved.push(".");
446
+ return {
447
+ entries,
448
+ unresolved
449
+ };
450
+ }
451
+ if ("string" == typeof exports) {
452
+ if (this.isTypeScriptFile(exports)) entries["."] = exports;
453
+ else unresolved.push(".");
454
+ return {
455
+ entries,
456
+ unresolved
457
+ };
458
+ }
459
+ this.extractFromObject(exports, entries, unresolved, ".");
460
+ return {
461
+ entries,
462
+ unresolved
463
+ };
464
+ }
465
+ extractFromObject(obj, entries, unresolved, currentPath) {
466
+ for (const [key, value] of Object.entries(obj)){
467
+ const exportPath = key.startsWith(".") ? key : currentPath;
468
+ if ("string" == typeof value) {
469
+ if (this.isTypeScriptFile(value)) entries[exportPath] = value;
470
+ else if (key.startsWith(".")) unresolved.push(exportPath);
471
+ } else if (value && "object" == typeof value && !Array.isArray(value)) {
472
+ const nested = value;
473
+ const tsPath = this.findTypeScriptCondition(nested);
474
+ if (tsPath) entries[exportPath] = tsPath;
475
+ else if (key.startsWith(".")) this.extractFromObject(nested, entries, unresolved, exportPath);
476
+ else {
477
+ const sourcePath = this.findSourceCondition(nested);
478
+ if (sourcePath && this.isTypeScriptFile(sourcePath)) entries[exportPath] = sourcePath;
479
+ }
480
+ }
481
+ }
482
+ }
483
+ findTypeScriptCondition(conditions) {
484
+ const priorityKeys = [
485
+ "source",
486
+ "typescript",
487
+ "development",
488
+ "default"
489
+ ];
490
+ for (const key of priorityKeys){
491
+ const value = conditions[key];
492
+ if ("string" == typeof value && this.isTypeScriptFile(value)) return value;
493
+ if (value && "object" == typeof value) {
494
+ const nested = this.findTypeScriptCondition(value);
495
+ if (nested) return nested;
496
+ }
497
+ }
498
+ return null;
499
+ }
500
+ findSourceCondition(conditions) {
501
+ const priorityKeys = [
502
+ "source",
503
+ "import",
504
+ "require",
505
+ "default"
506
+ ];
507
+ for (const key of priorityKeys){
508
+ const value = conditions[key];
509
+ if ("string" == typeof value) return value;
510
+ if (value && "object" == typeof value) {
511
+ const nested = this.findSourceCondition(value);
512
+ if (nested) return nested;
513
+ }
514
+ }
515
+ return null;
516
+ }
517
+ isTypeScriptFile(filePath) {
518
+ return TS_EXTENSIONS.some((ext)=>filePath.endsWith(ext));
519
+ }
520
+ }
521
+ class ImportGraph {
522
+ options;
523
+ program = null;
524
+ compilerOptions = null;
525
+ moduleResolutionCache = null;
526
+ constructor(options){
527
+ this.options = options;
528
+ }
529
+ traceFromEntries(entryPaths) {
530
+ const errors = [];
531
+ const visited = new Set();
532
+ const entries = [];
533
+ const initResult = this.initializeProgram();
534
+ if (!initResult.success) return {
535
+ files: [],
536
+ entries: [],
537
+ errors: [
538
+ initResult.error
539
+ ]
540
+ };
541
+ for (const entryPath of entryPaths){
542
+ const absolutePath = this.resolveEntryPath(entryPath);
543
+ if (!existsSync(absolutePath)) {
544
+ errors.push({
545
+ type: "entry_not_found",
546
+ message: `Entry file not found: ${entryPath}`,
547
+ path: absolutePath
548
+ });
549
+ continue;
550
+ }
551
+ entries.push(absolutePath);
552
+ this.traceImports(absolutePath, visited, errors);
553
+ }
554
+ const files = Array.from(visited).filter((file)=>this.isSourceFile(file));
555
+ return {
556
+ files: files.sort(),
557
+ entries,
558
+ errors
559
+ };
560
+ }
561
+ traceFromPackageExports(packageJsonPath) {
562
+ const absolutePath = this.resolveEntryPath(packageJsonPath);
563
+ let packageJson;
564
+ try {
565
+ if (!existsSync(absolutePath)) return {
566
+ files: [],
567
+ entries: [],
568
+ errors: [
569
+ {
570
+ type: "package_json_not_found",
571
+ message: `Failed to read package.json: File not found at ${absolutePath}`,
572
+ path: absolutePath
573
+ }
574
+ ]
575
+ };
576
+ const content = readFileSync(absolutePath, "utf-8");
577
+ packageJson = JSON.parse(content);
578
+ } catch (error) {
579
+ const message = error instanceof Error ? error.message : String(error);
580
+ return {
581
+ files: [],
582
+ entries: [],
583
+ errors: [
584
+ {
585
+ type: "package_json_parse_error",
586
+ message: `Failed to parse package.json: ${message}`,
587
+ path: absolutePath
588
+ }
589
+ ]
590
+ };
591
+ }
592
+ const extractor = new EntryExtractor();
593
+ const { entries } = extractor.extract(packageJson);
594
+ const packageDir = dirname(absolutePath);
595
+ const entryPaths = Object.values(entries).map((p)=>resolve(packageDir, p));
596
+ return this.traceFromEntries(entryPaths);
597
+ }
598
+ initializeProgram() {
599
+ if (this.program) return {
600
+ success: true
601
+ };
602
+ const configPath = this.findTsConfig();
603
+ if (!configPath) {
604
+ this.compilerOptions = {
605
+ moduleResolution: typescript.ModuleResolutionKind.NodeNext,
606
+ module: typescript.ModuleKind.NodeNext,
607
+ target: typescript.ScriptTarget.ESNext,
608
+ strict: true
609
+ };
610
+ this.moduleResolutionCache = typescript.createModuleResolutionCache(this.options.rootDir, (fileName)=>fileName.toLowerCase(), this.compilerOptions);
611
+ const host = typescript.createCompilerHost(this.compilerOptions, true);
612
+ host.getCurrentDirectory = ()=>this.options.rootDir;
613
+ this.program = typescript.createProgram([], this.compilerOptions, host);
614
+ return {
615
+ success: true
616
+ };
617
+ }
618
+ const configFile = typescript.readConfigFile(configPath, (path)=>readFileSync(path, "utf-8"));
619
+ if (configFile.error) {
620
+ const message = typescript.flattenDiagnosticMessageText(configFile.error.messageText, "\n");
621
+ return {
622
+ success: false,
623
+ error: {
624
+ type: "tsconfig_read_error",
625
+ message: `Failed to read tsconfig.json: ${message}`,
626
+ path: configPath
627
+ }
628
+ };
629
+ }
630
+ const parsed = typescript.parseJsonConfigFileContent(configFile.config, typescript.sys, dirname(configPath));
631
+ if (parsed.errors.length > 0) {
632
+ const messages = parsed.errors.map((e)=>typescript.flattenDiagnosticMessageText(e.messageText, "\n")).join("\n");
633
+ return {
634
+ success: false,
635
+ error: {
636
+ type: "tsconfig_parse_error",
637
+ message: `Failed to parse tsconfig.json: ${messages}`,
638
+ path: configPath
639
+ }
640
+ };
641
+ }
642
+ this.compilerOptions = parsed.options;
643
+ this.moduleResolutionCache = typescript.createModuleResolutionCache(this.options.rootDir, (fileName)=>fileName.toLowerCase(), this.compilerOptions);
644
+ const host = typescript.createCompilerHost(this.compilerOptions, true);
645
+ host.getCurrentDirectory = ()=>this.options.rootDir;
646
+ this.program = typescript.createProgram([], this.compilerOptions, host);
647
+ return {
648
+ success: true
649
+ };
650
+ }
651
+ findTsConfig() {
652
+ if (this.options.tsconfigPath) {
653
+ const customPath = isAbsolute(this.options.tsconfigPath) ? this.options.tsconfigPath : resolve(this.options.rootDir, this.options.tsconfigPath);
654
+ if (existsSync(customPath)) return customPath;
655
+ return null;
656
+ }
657
+ const configPath = typescript.findConfigFile(this.options.rootDir, (path)=>existsSync(path));
658
+ return configPath ?? null;
659
+ }
660
+ resolveEntryPath(entryPath) {
661
+ if (isAbsolute(entryPath)) return normalize(entryPath);
662
+ return normalize(resolve(this.options.rootDir, entryPath));
663
+ }
664
+ traceImports(filePath, visited, errors) {
665
+ const normalizedPath = normalize(filePath);
666
+ if (visited.has(normalizedPath)) return;
667
+ if (this.isExternalModule(normalizedPath)) return;
668
+ visited.add(normalizedPath);
669
+ let content;
670
+ try {
671
+ content = readFileSync(normalizedPath, "utf-8");
672
+ } catch {
673
+ errors.push({
674
+ type: "file_read_error",
675
+ message: `Failed to read file: ${normalizedPath}`,
676
+ path: normalizedPath
677
+ });
678
+ return;
679
+ }
680
+ const sourceFile = typescript.createSourceFile(normalizedPath, content, typescript.ScriptTarget.Latest, true);
681
+ const imports = this.extractImports(sourceFile);
682
+ for (const importPath of imports){
683
+ const resolved = this.resolveImport(importPath, normalizedPath);
684
+ if (resolved) this.traceImports(resolved, visited, errors);
685
+ }
686
+ }
687
+ extractImports(sourceFile) {
688
+ const imports = [];
689
+ const visit = (node)=>{
690
+ if (typescript.isImportDeclaration(node)) {
691
+ const specifier = node.moduleSpecifier;
692
+ if (typescript.isStringLiteral(specifier)) imports.push(specifier.text);
693
+ } else if (typescript.isExportDeclaration(node)) {
694
+ const specifier = node.moduleSpecifier;
695
+ if (specifier && typescript.isStringLiteral(specifier)) imports.push(specifier.text);
696
+ } else if (typescript.isCallExpression(node)) {
697
+ const expression = node.expression;
698
+ if (expression.kind === typescript.SyntaxKind.ImportKeyword && node.arguments.length > 0) {
699
+ const arg = node.arguments[0];
700
+ if (arg && typescript.isStringLiteral(arg)) imports.push(arg.text);
701
+ }
702
+ }
703
+ typescript.forEachChild(node, visit);
704
+ };
705
+ visit(sourceFile);
706
+ return imports;
707
+ }
708
+ resolveImport(specifier, fromFile) {
709
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
710
+ if (!this.compilerOptions?.paths || !Object.keys(this.compilerOptions.paths).length) return null;
711
+ }
712
+ if (!this.compilerOptions || !this.moduleResolutionCache) return null;
713
+ const resolved = typescript.resolveModuleName(specifier, fromFile, this.compilerOptions, typescript.sys, this.moduleResolutionCache);
714
+ if (resolved.resolvedModule) {
715
+ const resolvedPath = resolved.resolvedModule.resolvedFileName;
716
+ if (resolved.resolvedModule.isExternalLibraryImport) return null;
717
+ if (resolvedPath.endsWith(".d.ts")) {
718
+ const sourcePath = resolvedPath.replace(/\.d\.ts$/, ".ts");
719
+ if (existsSync(sourcePath)) return sourcePath;
720
+ return null;
721
+ }
722
+ return resolvedPath;
723
+ }
724
+ return null;
725
+ }
726
+ isExternalModule(filePath) {
727
+ return filePath.includes("/node_modules/") || filePath.includes("\\node_modules\\");
728
+ }
729
+ isSourceFile(filePath) {
730
+ if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx") && !filePath.endsWith(".mts") && !filePath.endsWith(".cts")) return false;
731
+ if (filePath.endsWith(".d.ts") || filePath.endsWith(".d.mts") || filePath.endsWith(".d.cts")) return false;
732
+ if (filePath.includes(".test.") || filePath.includes(".spec.")) return false;
733
+ if (filePath.includes("/__test__/") || filePath.includes("\\__test__\\")) return false;
734
+ if (filePath.includes("/__tests__/") || filePath.includes("\\__tests__\\")) return false;
735
+ const excludePatterns = this.options.excludePatterns ?? [];
736
+ for (const pattern of excludePatterns)if (filePath.includes(pattern)) return false;
737
+ return true;
738
+ }
739
+ static fromEntries(entryPaths, options) {
740
+ const graph = new ImportGraph(options);
741
+ return graph.traceFromEntries(entryPaths);
742
+ }
743
+ static fromPackageExports(packageJsonPath, options) {
744
+ const graph = new ImportGraph(options);
745
+ return graph.traceFromPackageExports(packageJsonPath);
746
+ }
747
+ }
748
+ class TsDocResolver {
749
+ options;
750
+ constructor(options){
751
+ this.options = options;
752
+ }
753
+ resolve() {
754
+ const { rootDir } = this.options;
755
+ const workspaces = [];
756
+ const repoTsdocPath = join(rootDir, "tsdoc.json");
757
+ const repoTsdocConfig = existsSync(repoTsdocPath) ? repoTsdocPath : void 0;
758
+ const workspaceInfos = getWorkspaceInfos(rootDir);
759
+ const isMonorepo = void 0 !== workspaceInfos && workspaceInfos.length > 1;
760
+ if (void 0 === workspaceInfos || 0 === workspaceInfos.length) {
761
+ const result = this.resolveWorkspace(rootDir, repoTsdocConfig);
762
+ if (result) workspaces.push(result);
763
+ } else for (const info of workspaceInfos){
764
+ const workspacePath = info.path;
765
+ const result = this.resolveWorkspace(workspacePath, repoTsdocConfig);
766
+ if (result) workspaces.push(result);
767
+ }
768
+ const result = {
769
+ workspaces,
770
+ isMonorepo
771
+ };
772
+ if (void 0 !== repoTsdocConfig) result.repoTsdocConfig = repoTsdocConfig;
773
+ return result;
774
+ }
775
+ resolveWorkspace(workspacePath, repoTsdocConfig) {
776
+ const packageJsonPath = join(workspacePath, "package.json");
777
+ if (!existsSync(packageJsonPath)) return null;
778
+ let packageJson;
779
+ try {
780
+ const content = readFileSync(packageJsonPath, "utf-8");
781
+ packageJson = JSON.parse(content);
782
+ } catch {
783
+ return null;
784
+ }
785
+ const workspaceTsdocPath = join(workspacePath, "tsdoc.json");
786
+ const workspaceTsdocConfig = existsSync(workspaceTsdocPath) ? workspaceTsdocPath : void 0;
787
+ const tsdocConfigPath = workspaceTsdocConfig ?? repoTsdocConfig;
788
+ if (!tsdocConfigPath) return null;
789
+ if (!packageJson.exports) return null;
790
+ const name = packageJson.name ?? relative(this.options.rootDir, workspacePath);
791
+ const errors = [];
792
+ const graphOptions = {
793
+ rootDir: workspacePath
794
+ };
795
+ if (void 0 !== this.options.excludePatterns) graphOptions.excludePatterns = this.options.excludePatterns;
796
+ const graph = new ImportGraph(graphOptions);
797
+ const result = graph.traceFromPackageExports(packageJsonPath);
798
+ for (const error of result.errors)errors.push(error.message);
799
+ return {
800
+ name,
801
+ path: workspacePath,
802
+ tsdocConfigPath,
803
+ files: result.files,
804
+ errors
805
+ };
806
+ }
807
+ filterStagedFiles(stagedFiles) {
808
+ const result = this.resolve();
809
+ const output = [];
810
+ for (const workspace of result.workspaces){
811
+ const workspaceFiles = new Set(workspace.files);
812
+ const matchedFiles = stagedFiles.filter((f)=>workspaceFiles.has(f));
813
+ if (matchedFiles.length > 0) output.push({
814
+ files: matchedFiles,
815
+ tsdocConfigPath: workspace.tsdocConfigPath
816
+ });
817
+ }
818
+ return output;
819
+ }
820
+ needsLinting(filePath) {
821
+ const result = this.resolve();
822
+ for (const workspace of result.workspaces)if (workspace.files.includes(filePath)) return true;
823
+ return false;
824
+ }
825
+ getTsDocConfig(filePath) {
826
+ const result = this.resolve();
827
+ for (const workspace of result.workspaces)if (workspace.files.includes(filePath)) return workspace.tsdocConfigPath;
828
+ }
829
+ findWorkspace(filePath) {
830
+ const result = this.resolve();
831
+ for (const workspace of result.workspaces)if (filePath.startsWith(workspace.path)) return workspace;
832
+ }
833
+ }
834
+ class TypeScript {
835
+ static glob = "*.{ts,cts,mts,tsx}";
836
+ static defaultExcludes = [];
837
+ static defaultTsdocExcludes = [
838
+ ".test.",
839
+ ".spec.",
840
+ "__test__",
841
+ "__tests__"
842
+ ];
843
+ static detectCompiler(cwd = process.cwd()) {
844
+ const packageJsonPath = join(cwd, "package.json");
845
+ if (!existsSync(packageJsonPath)) return;
846
+ try {
847
+ const content = readFileSync(packageJsonPath, "utf-8");
848
+ const pkg = JSON.parse(content);
849
+ const allDeps = {
850
+ ...pkg.dependencies,
851
+ ...pkg.devDependencies
852
+ };
853
+ if ("@typescript/native-preview" in allDeps) return "tsgo";
854
+ if ("typescript" in allDeps) return "tsc";
855
+ } catch {}
856
+ }
857
+ static isAvailable() {
858
+ return void 0 !== TypeScript.detectCompiler();
859
+ }
860
+ static getDefaultTypecheckCommand() {
861
+ const compiler = TypeScript.detectCompiler();
862
+ if (!compiler) throw new Error("No TypeScript compiler found. Install 'typescript' or '@typescript/native-preview' as a dev dependency.");
863
+ const pm = Command_Command.detectPackageManager();
864
+ const prefix = Command_Command.getExecPrefix(pm);
865
+ return [
866
+ ...prefix,
867
+ compiler,
868
+ "--noEmit"
869
+ ].join(" ");
870
+ }
871
+ static handler = TypeScript.create();
872
+ static isTsdocAvailable(cwd = process.cwd()) {
873
+ const tsdocPath = join(cwd, "tsdoc.json");
874
+ return existsSync(tsdocPath);
875
+ }
876
+ static create(options = {}) {
877
+ const excludes = options.exclude ?? [
878
+ ...TypeScript.defaultExcludes
879
+ ];
880
+ const tsdocExcludes = options.excludeTsdoc ?? [
881
+ ...TypeScript.defaultTsdocExcludes
882
+ ];
883
+ const skipTsdoc = options.skipTsdoc ?? false;
884
+ const skipTypecheck = options.skipTypecheck ?? false;
885
+ const rootDir = options.rootDir ?? process.cwd();
886
+ let typecheckCommand;
887
+ const getTypecheckCommand = ()=>{
888
+ if (void 0 === typecheckCommand) typecheckCommand = options.typecheckCommand ?? TypeScript.getDefaultTypecheckCommand();
889
+ return typecheckCommand;
890
+ };
891
+ return async (filenames)=>{
892
+ const filtered = Filter.exclude(filenames, excludes);
893
+ if (0 === filtered.length) return [];
894
+ const commands = [];
895
+ if (!skipTsdoc) {
896
+ const resolver = new TsDocResolver({
897
+ rootDir,
898
+ excludePatterns: [
899
+ ...tsdocExcludes
900
+ ]
901
+ });
902
+ const absoluteFiles = filtered.map((f)=>isAbsolute(f) ? f : join(rootDir, f));
903
+ const tsdocGroups = resolver.filterStagedFiles(absoluteFiles);
904
+ for (const group of tsdocGroups)if (group.files.length > 0) {
905
+ const linter = new TsDocLinter({
906
+ ignorePatterns: tsdocExcludes.map((p)=>`**/*${p}*`)
907
+ });
908
+ const results = await linter.lintFiles(group.files);
909
+ if (TsDocLinter.hasErrors(results)) {
910
+ const output = TsDocLinter.formatResults(results);
911
+ throw new Error(`TSDoc validation failed:\n${output}`);
912
+ }
913
+ }
914
+ }
915
+ if (!skipTypecheck && filtered.length > 0) commands.push(getTypecheckCommand());
916
+ return commands;
917
+ };
918
+ }
919
+ }
920
+ const CHECK_MARK = "\u2713";
921
+ const WARNING = "\u26A0";
922
+ const EXECUTABLE_MODE = 493;
923
+ const HUSKY_HOOK_PATH = ".husky/pre-commit";
924
+ const DEFAULT_CONFIG_PATH = "lib/configs/lint-staged.config.js";
925
+ const BEGIN_MARKER = "# --- BEGIN SAVVY-LINT MANAGED SECTION ---";
926
+ const END_MARKER = "# --- END SAVVY-LINT MANAGED SECTION ---";
927
+ function generateManagedContent(configPath) {
928
+ return `# DO NOT EDIT between these markers - managed by savvy-lint
929
+ # Skip in CI environment
930
+ { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; } && exit 0
931
+
932
+ # Get repo root directory
933
+ ROOT=$(git rev-parse --show-toplevel)
934
+
935
+ # Detect package manager from package.json or lockfiles
936
+ detect_pm() {
937
+ # Check packageManager field in package.json (e.g., "pnpm@9.0.0")
938
+ if [ -f "$ROOT/package.json" ]; then
939
+ pm=$(jq -r '.packageManager // empty' "$ROOT/package.json" 2>/dev/null | cut -d'@' -f1)
940
+ if [ -n "$pm" ]; then
941
+ echo "$pm"
942
+ return
943
+ fi
944
+ fi
945
+
946
+ # Fallback to lockfile detection
947
+ if [ -f "$ROOT/pnpm-lock.yaml" ]; then
948
+ echo "pnpm"
949
+ elif [ -f "$ROOT/yarn.lock" ]; then
950
+ echo "yarn"
951
+ elif [ -f "$ROOT/bun.lock" ]; then
952
+ echo "bun"
953
+ else
954
+ echo "npm"
955
+ fi
956
+ }
957
+
958
+ # Get the exec command for the detected package manager
959
+ PM=$(detect_pm)
960
+ case "$PM" in
961
+ pnpm) CMD="pnpm exec" ;;
962
+ yarn) CMD="yarn exec" ;;
963
+ bun) CMD="bunx" ;;
964
+ *) CMD="npx --no --" ;;
965
+ esac
966
+
967
+ $CMD lint-staged --config "$ROOT/${configPath}"`;
968
+ }
969
+ function generateFullHookContent(configPath) {
970
+ return `#!/usr/bin/env sh
971
+ # Pre-commit hook with savvy-lint managed section
972
+ # Custom hooks can go above or below the managed section
973
+
974
+ ${BEGIN_MARKER}
975
+ ${generateManagedContent(configPath)}
976
+ ${END_MARKER}
977
+ `;
978
+ }
979
+ function extractManagedSection(content) {
980
+ const beginIndex = content.indexOf(BEGIN_MARKER);
981
+ const endIndex = content.indexOf(END_MARKER);
982
+ if (-1 === beginIndex || -1 === endIndex || endIndex <= beginIndex) return {
983
+ beforeSection: content,
984
+ managedSection: "",
985
+ afterSection: "",
986
+ found: false
987
+ };
988
+ return {
989
+ beforeSection: content.slice(0, beginIndex),
990
+ managedSection: content.slice(beginIndex, endIndex + END_MARKER.length),
991
+ afterSection: content.slice(endIndex + END_MARKER.length),
992
+ found: true
993
+ };
994
+ }
995
+ function updateManagedSection(existingContent, configPath) {
996
+ const { beforeSection, afterSection, found } = extractManagedSection(existingContent);
997
+ const newManagedSection = `${BEGIN_MARKER}\n${generateManagedContent(configPath)}\n${END_MARKER}`;
998
+ if (found) return `${beforeSection}${newManagedSection}${afterSection}`;
999
+ const trimmedContent = existingContent.trimEnd();
1000
+ return `${trimmedContent}\n\n${newManagedSection}\n`;
1001
+ }
1002
+ function generateConfigContent(preset) {
1003
+ return `/**
1004
+ * @type {import('lint-staged').Configuration}
1005
+ * Generated by savvy-lint init
1006
+ */
1007
+ import { Preset } from "@savvy-web/lint-staged";
1008
+
1009
+ export default Preset.${preset}();
1010
+ `;
1011
+ }
1012
+ const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite entire hook file (not just managed section)"), Options.withDefault(false));
1013
+ 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));
1014
+ const presetOption = Options.choice("preset", [
1015
+ "minimal",
1016
+ "standard",
1017
+ "full"
1018
+ ]).pipe(Options.withAlias("p"), Options.withDescription("Preset to use: minimal, standard, or full"), Options.withDefault("full"));
1019
+ function makeExecutable(path) {
1020
+ return Effect.tryPromise(()=>import("node:fs/promises").then((fs)=>fs.chmod(path, EXECUTABLE_MODE)));
1021
+ }
1022
+ const initCommand = Command.make("init", {
1023
+ force: forceOption,
1024
+ config: configOption,
1025
+ preset: presetOption
1026
+ }, ({ force, config, preset })=>Effect.gen(function*() {
1027
+ const fs = yield* FileSystem.FileSystem;
1028
+ if (config.startsWith("/")) yield* Effect.fail(new Error("Config path must be relative to repository root, not absolute"));
1029
+ yield* Effect.log("Initializing lint-staged configuration...\n");
1030
+ const huskyExists = yield* fs.exists(HUSKY_HOOK_PATH);
1031
+ if (huskyExists && !force) {
1032
+ const existingContent = yield* fs.readFileString(HUSKY_HOOK_PATH);
1033
+ const { found } = extractManagedSection(existingContent);
1034
+ const updatedContent = updateManagedSection(existingContent, config);
1035
+ yield* fs.writeFileString(HUSKY_HOOK_PATH, updatedContent);
1036
+ yield* makeExecutable(HUSKY_HOOK_PATH);
1037
+ if (found) yield* Effect.log(`${CHECK_MARK} Updated managed section in ${HUSKY_HOOK_PATH}`);
1038
+ else yield* Effect.log(`${CHECK_MARK} Added managed section to ${HUSKY_HOOK_PATH}`);
1039
+ } else if (huskyExists && force) {
1040
+ yield* fs.writeFileString(HUSKY_HOOK_PATH, generateFullHookContent(config));
1041
+ yield* makeExecutable(HUSKY_HOOK_PATH);
1042
+ yield* Effect.log(`${CHECK_MARK} Replaced ${HUSKY_HOOK_PATH} (--force)`);
1043
+ } else {
1044
+ yield* fs.makeDirectory(".husky", {
1045
+ recursive: true
1046
+ });
1047
+ yield* fs.writeFileString(HUSKY_HOOK_PATH, generateFullHookContent(config));
1048
+ yield* makeExecutable(HUSKY_HOOK_PATH);
1049
+ yield* Effect.log(`${CHECK_MARK} Created ${HUSKY_HOOK_PATH}`);
1050
+ }
1051
+ const configExists = yield* fs.exists(config);
1052
+ if (configExists && !force) yield* Effect.log(`${WARNING} ${config} already exists (use --force to overwrite)`);
1053
+ else {
1054
+ const configDir = dirname(config);
1055
+ if (configDir && "." !== configDir) yield* fs.makeDirectory(configDir, {
1056
+ recursive: true
1057
+ });
1058
+ yield* fs.writeFileString(config, generateConfigContent(preset));
1059
+ yield* Effect.log(`${CHECK_MARK} Created ${config} (preset: ${preset})`);
1060
+ }
1061
+ yield* Effect.log("\nDone! Lint-staged is ready to use.");
1062
+ })).pipe(Command.withDescription("Initialize lint-staged configuration and husky hooks"));
1063
+ const check_CHECK_MARK = "\u2713";
1064
+ const CROSS_MARK = "\u2717";
1065
+ const check_WARNING = "\u26A0";
1066
+ const BULLET = "\u2022";
1067
+ const check_HUSKY_HOOK_PATH = ".husky/pre-commit";
1068
+ const CONFIG_FILES = [
1069
+ "lint-staged.config.js",
1070
+ "lint-staged.config.mjs",
1071
+ "lint-staged.config.cjs",
1072
+ "lint-staged.config.ts",
1073
+ ".lintstagedrc",
1074
+ ".lintstagedrc.json",
1075
+ ".lintstagedrc.yaml",
1076
+ ".lintstagedrc.yml",
1077
+ ".lintstagedrc.js",
1078
+ ".lintstagedrc.cjs",
1079
+ ".lintstagedrc.mjs"
1080
+ ];
1081
+ const CONFIG_SEARCH_PATHS = [
1082
+ "lib/configs/lint-staged.config.js",
1083
+ "lib/configs/lint-staged.config.ts",
1084
+ ...CONFIG_FILES
1085
+ ];
1086
+ function findConfigFile(fs) {
1087
+ return Effect.gen(function*() {
1088
+ for (const file of CONFIG_SEARCH_PATHS)if (yield* fs.exists(file)) return file;
1089
+ return null;
1090
+ });
1091
+ }
1092
+ function extractConfigPathFromManaged(managedContent) {
1093
+ const match = managedContent.match(/lint-staged --config "\$ROOT\/([^"]+)"/);
1094
+ return match ? match[1] : null;
1095
+ }
1096
+ function checkManagedSectionStatus(existingManaged) {
1097
+ const configPath = extractConfigPathFromManaged(existingManaged);
1098
+ if (!configPath) return {
1099
+ isUpToDate: false,
1100
+ configPath: null,
1101
+ needsUpdate: true
1102
+ };
1103
+ const expectedContent = `${BEGIN_MARKER}\n${generateManagedContent(configPath)}\n${END_MARKER}`;
1104
+ const normalizedExisting = existingManaged.trim().replace(/\s+/g, " ");
1105
+ const normalizedExpected = expectedContent.trim().replace(/\s+/g, " ");
1106
+ const isUpToDate = normalizedExisting === normalizedExpected;
1107
+ return {
1108
+ isUpToDate,
1109
+ configPath,
1110
+ needsUpdate: !isUpToDate
1111
+ };
1112
+ }
1113
+ const quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Only output warnings (for postinstall usage)"), Options.withDefault(false));
1114
+ const checkCommand = Command.make("check", {
1115
+ quiet: quietOption
1116
+ }, ({ quiet })=>Effect.gen(function*() {
1117
+ const fs = yield* FileSystem.FileSystem;
1118
+ const warnings = [];
1119
+ const foundConfig = yield* findConfigFile(fs);
1120
+ const hasHuskyHook = yield* fs.exists(check_HUSKY_HOOK_PATH);
1121
+ let managedStatus = {
1122
+ isUpToDate: false,
1123
+ configPath: null,
1124
+ needsUpdate: false,
1125
+ found: false
1126
+ };
1127
+ if (hasHuskyHook) {
1128
+ const hookContent = yield* fs.readFileString(check_HUSKY_HOOK_PATH);
1129
+ const { managedSection, found } = extractManagedSection(hookContent);
1130
+ if (found) {
1131
+ const status = checkManagedSectionStatus(managedSection);
1132
+ managedStatus = {
1133
+ ...status,
1134
+ found: true
1135
+ };
1136
+ if (status.needsUpdate) warnings.push(`${check_WARNING} Your ${check_HUSKY_HOOK_PATH} managed section is outdated.\n Run 'savvy-lint init' to update it (preserves your custom hooks).`);
1137
+ } else {
1138
+ managedStatus = {
1139
+ isUpToDate: false,
1140
+ configPath: null,
1141
+ needsUpdate: false,
1142
+ found: false
1143
+ };
1144
+ warnings.push(`${check_WARNING} Your ${check_HUSKY_HOOK_PATH} does not have a savvy-lint managed section.\n Run 'savvy-lint init' to add it.`);
1145
+ }
1146
+ } else warnings.push(`${check_WARNING} No husky pre-commit hook found.\n Run 'savvy-lint init' to create it.`);
1147
+ if (!foundConfig) warnings.push(`${check_WARNING} No lint-staged config file found.\n Run 'savvy-lint init' to create one.`);
1148
+ if (quiet) {
1149
+ if (warnings.length > 0) for (const warning of warnings)yield* Effect.log(warning);
1150
+ return;
1151
+ }
1152
+ yield* Effect.log("Checking lint-staged configuration...\n");
1153
+ if (foundConfig) yield* Effect.log(`${check_CHECK_MARK} Config file: ${foundConfig}`);
1154
+ else yield* Effect.log(`${CROSS_MARK} No lint-staged config file found`);
1155
+ if (hasHuskyHook) yield* Effect.log(`${check_CHECK_MARK} Husky hook: ${check_HUSKY_HOOK_PATH}`);
1156
+ else yield* Effect.log(`${CROSS_MARK} No husky pre-commit hook found`);
1157
+ if (hasHuskyHook) if (managedStatus.found) if (managedStatus.isUpToDate) yield* Effect.log(`${check_CHECK_MARK} Managed section: up-to-date`);
1158
+ else yield* Effect.log(`${check_WARNING} Managed section: outdated (run 'savvy-lint init' to update)`);
1159
+ else yield* Effect.log(`${BULLET} Managed section: not found (run 'savvy-lint init' to add)`);
1160
+ yield* Effect.log("\nTool availability:");
1161
+ const biomeAvailable = Biome.isAvailable();
1162
+ const biomeConfig = Biome.findConfig();
1163
+ if (biomeAvailable) {
1164
+ const configInfo = biomeConfig ? ` (config: ${biomeConfig})` : "";
1165
+ yield* Effect.log(` ${check_CHECK_MARK} Biome${configInfo}`);
1166
+ } else yield* Effect.log(` ${BULLET} Biome: not installed`);
1167
+ const markdownAvailable = Markdown.isAvailable();
1168
+ const markdownConfig = Markdown.findConfig();
1169
+ if (markdownAvailable) {
1170
+ const configInfo = markdownConfig ? ` (config: ${markdownConfig})` : "";
1171
+ yield* Effect.log(` ${check_CHECK_MARK} markdownlint-cli2${configInfo}`);
1172
+ } else yield* Effect.log(` ${BULLET} markdownlint-cli2: not installed`);
1173
+ const typescriptAvailable = TypeScript.isAvailable();
1174
+ if (typescriptAvailable) {
1175
+ const compiler = TypeScript.detectCompiler();
1176
+ yield* Effect.log(` ${check_CHECK_MARK} TypeScript (${compiler})`);
1177
+ } else yield* Effect.log(` ${BULLET} TypeScript: not installed`);
1178
+ const tsdocAvailable = TypeScript.isTsdocAvailable();
1179
+ if (tsdocAvailable) yield* Effect.log(` ${check_CHECK_MARK} TSDoc (tsdoc.json found)`);
1180
+ else yield* Effect.log(` ${BULLET} TSDoc: no tsdoc.json found`);
1181
+ yield* Effect.log("");
1182
+ const hasIssues = !foundConfig || !hasHuskyHook || !managedStatus.found || managedStatus.needsUpdate;
1183
+ if (hasIssues) yield* Effect.log(`${check_WARNING} Some issues found. Run 'savvy-lint init' to fix.`);
1184
+ else yield* Effect.log(`${check_CHECK_MARK} Lint-staged is configured correctly.`);
1185
+ })).pipe(Command.withDescription("Check current lint-staged configuration and tool availability"));
1186
+ const rootCommand = Command.make("savvy-lint").pipe(Command.withSubcommands([
1187
+ initCommand,
1188
+ checkCommand
1189
+ ]));
1190
+ const cli = Command.run(rootCommand, {
1191
+ name: "savvy-lint",
1192
+ version: "0.2.0"
1193
+ });
1194
+ function runCli() {
1195
+ const main = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(NodeContext.layer));
1196
+ NodeRuntime.runMain(main);
1197
+ }
1198
+ export { Biome, Command_Command as Command, ConfigSearch, EntryExtractor, Filter, ImportGraph, Markdown, TsDocLinter, TsDocResolver, TypeScript, checkCommand, existsSync, initCommand, readFileSync, rootCommand, runCli, writeFileSync };