lint-tools-sdk 1.0.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/README.md ADDED
@@ -0,0 +1,371 @@
1
+ # Lint Tools SDK
2
+
3
+ 一个用于在多个Git仓库中统一代码质量管理的前端代码规范工具SDK(npm包形式)。
4
+
5
+ ## 核心功能
6
+
7
+ - ✅ **ESLint集成**:支持ESLint v9+ flat config格式,实现代码格式化校验
8
+ - ✅ **commitlint集成**:提交信息规范校验,确保提交信息格式统一
9
+ - ✅ **Git Hooks自动触发**:通过husky在提交代码时自动对暂存文件进行格式化校验
10
+ - ✅ **lint-staged**:仅对Git暂存文件执行ESLint校验与修复
11
+ - ✅ **配置覆盖机制**:项目级配置自动合并覆盖SDK默认配置
12
+ - ✅ **CLI命令支持**:提供丰富的命令行工具,方便使用和集成
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ # 全局安装(可选)
18
+ npm install -g lint-tools-sdk
19
+
20
+ # 局部安装
21
+ npm install --save-dev lint-tools-sdk
22
+ ```
23
+
24
+ ## 快速开始
25
+
26
+ ### 1. 初始化项目
27
+
28
+ 在项目根目录下执行:
29
+
30
+ ```bash
31
+ npx lint-tools-sdk init
32
+ ```
33
+
34
+ 此命令会:
35
+ - 安装Git Hooks(pre-commit和commit-msg)
36
+ - 创建默认配置文件(eslint.config.js、.commitlintrc.js、.lintstagedrc.js)
37
+ - 更新package.json脚本
38
+
39
+ ### 2. 基本使用
40
+
41
+ #### 代码校验
42
+
43
+ ```bash
44
+ # 全量代码校验
45
+ npx lint-tools-sdk lint
46
+
47
+ # 自动修复代码问题
48
+ npx lint-tools-sdk lint:fix
49
+
50
+ # 仅校验暂存文件
51
+ npx lint-tools-sdk lint:staged
52
+ ```
53
+
54
+ #### 提交信息校验
55
+
56
+ ```bash
57
+ # 校验提交信息
58
+ npx lint-tools-sdk commitlint "feat: add new feature"
59
+
60
+ # 使用git commit时会自动触发校验
61
+ ```
62
+
63
+ #### 查看诊断信息
64
+
65
+ ```bash
66
+ # 查看当前配置和服务状态
67
+ npx lint-tools-sdk status
68
+ ```
69
+
70
+ ## 配置说明
71
+
72
+ ### 1. 项目级配置
73
+
74
+ 可以通过以下方式覆盖SDK的默认配置:
75
+
76
+ #### a) package.json中的lintToolsConfig
77
+
78
+ ```json
79
+ {
80
+ "lintToolsConfig": {
81
+ "eslint": {
82
+ "enabled": true,
83
+ "fix": true
84
+ },
85
+ "commitlint": {
86
+ "enabled": true
87
+ },
88
+ "githooks": {
89
+ "enabled": true
90
+ },
91
+ "lintStaged": {
92
+ "enabled": true
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ #### b) .linttoolsrc.js配置文件
99
+
100
+ ```javascript
101
+ module.exports = {
102
+ eslint: {
103
+ enabled: true,
104
+ fix: true
105
+ },
106
+ commitlint: {
107
+ enabled: true
108
+ },
109
+ githooks: {
110
+ enabled: true
111
+ },
112
+ lintStaged: {
113
+ enabled: true
114
+ }
115
+ };
116
+ ```
117
+
118
+ ### 2. ESLint配置
119
+
120
+ SDK使用ESLint v9+的flat config格式,默认配置文件为`eslint.config.js`:
121
+
122
+ ```javascript
123
+ import globals from "globals";
124
+ import pluginJs from "@eslint/js";
125
+
126
+ export default [
127
+ {
128
+ files: ["**/*.js"],
129
+ languageOptions: {
130
+ ecmaVersion: "latest",
131
+ sourceType: "module",
132
+ globals: {
133
+ ...globals.node,
134
+ ...globals.browser
135
+ }
136
+ },
137
+ rules: {
138
+ ...pluginJs.configs.recommended.rules,
139
+ // 项目自定义规则
140
+ }
141
+ }
142
+ ];
143
+ ```
144
+
145
+ ### 3. commitlint配置
146
+
147
+ 默认配置文件为`.commitlintrc.js`:
148
+
149
+ ```javascript
150
+ module.exports = {
151
+ extends: ['@commitlint/config-conventional']
152
+ };
153
+ ```
154
+
155
+ ### 4. lint-staged配置
156
+
157
+ 默认配置文件为`.lintstagedrc.js`:
158
+
159
+ ```javascript
160
+ module.exports = {
161
+ '*.js': ['eslint --fix', 'git add'],
162
+ '*.jsx': ['eslint --fix', 'git add'],
163
+ '*.ts': ['eslint --fix', 'git add'],
164
+ '*.tsx': ['eslint --fix', 'git add']
165
+ };
166
+ ```
167
+
168
+ ## CLI命令
169
+
170
+ | 命令 | 描述 |
171
+ |------|------|
172
+ | `init` | 初始化项目,安装Git Hooks并创建默认配置 |
173
+ | `lint` | 全量代码校验 |
174
+ | `lint:fix` | 自动修复代码问题 |
175
+ | `lint:staged` | 仅校验暂存文件 |
176
+ | `commitlint <message>` | 校验提交信息格式 |
177
+ | `status` | 查看当前配置和服务状态 |
178
+
179
+ ## Git Hooks
180
+
181
+ ### pre-commit
182
+
183
+ 执行lint-staged,对暂存文件进行ESLint校验与修复:
184
+
185
+ ```bash
186
+ npx lint-staged
187
+ ```
188
+
189
+ ### commit-msg
190
+
191
+ 执行commitlint,校验提交信息格式:
192
+
193
+ ```bash
194
+ npx commitlint --edit "$1"
195
+ ```
196
+
197
+ ## 项目结构
198
+
199
+ ```
200
+ lint-tools-sdk/
201
+ ├── src/
202
+ │ ├── index.js # SDK主入口和CLI命令处理
203
+ │ ├── eslint.js # ESLint服务实现
204
+ │ ├── commitlint.js # commitlint服务实现
205
+ │ └── githooks.js # Git Hooks服务实现
206
+ ├── package.json
207
+ └── README.md
208
+ ```
209
+
210
+ ## 模块化架构
211
+
212
+ ### 1. LintToolsSDK (主类)
213
+
214
+ 整合所有服务,提供统一的API和CLI接口。
215
+
216
+ ### 2. ESLintService
217
+
218
+ - 配置加载与管理
219
+ - 文件校验与修复
220
+ - 暂存文件处理
221
+
222
+ ### 3. CommitLintService
223
+
224
+ - 提交信息校验
225
+ - 配置文件管理
226
+
227
+ ### 4. GitHooksService
228
+
229
+ - husky钩子安装与管理
230
+ - 钩子脚本生成
231
+
232
+ ## 技术栈
233
+
234
+ - **Node.js**:运行环境
235
+ - **ESLint v9+**:代码质量检查
236
+ - **commitlint**:提交信息规范
237
+ - **husky**:Git Hooks管理
238
+ - **lint-staged**:暂存文件处理
239
+
240
+ ## 兼容性
241
+
242
+ - **ESLint**:^9.0.0
243
+ - **husky**:^8.0.0 || ^9.0.0
244
+ - **lint-staged**:^15.0.0 || ^16.0.0
245
+
246
+ ## 项目构建与发布
247
+
248
+ ### 1. 构建项目
249
+
250
+ ```bash
251
+ # 安装依赖
252
+ npm install
253
+
254
+ # 验证代码(检查语法和类型错误)
255
+ npm run lint
256
+
257
+ # 运行测试(如有)
258
+ npm run test
259
+ ```
260
+
261
+ ### 2. 发布到npm
262
+
263
+ ```bash
264
+ # 确保已登录npm
265
+ npm login
266
+
267
+ # 更新版本号(根据语义化版本规范)
268
+ npm version patch # 修复bug,版本号第三位加1
269
+ npm version minor # 新增功能,版本号第二位加1
270
+ npm version major # 大版本更新,版本号第一位加1
271
+
272
+ # 发布到npm
273
+ npm publish
274
+ ```
275
+
276
+ ## 在其他项目中使用与验证
277
+
278
+ ### 1. 安装SDK
279
+
280
+ ```bash
281
+ # 从npm安装
282
+ npm install --save-dev lint-tools-sdk
283
+
284
+ # 或从本地开发版本安装
285
+ npm install --save-dev ../path/to/lint-tools-sdk
286
+ ```
287
+
288
+ ### 2. 初始化配置
289
+
290
+ ```bash
291
+ npx lint-tools-sdk init
292
+ ```
293
+
294
+ ### 3. 验证功能
295
+
296
+ #### a) 代码校验
297
+
298
+ ```bash
299
+ # 创建一个包含错误的测试文件
300
+ echo "const a = 1 console.log(a)" > test.js
301
+
302
+ # 执行代码校验,应该能检测到错误
303
+ npx lint-tools-sdk lint
304
+
305
+ # 尝试自动修复
306
+ npx lint-tools-sdk lint:fix
307
+ ```
308
+
309
+ #### b) 提交信息校验
310
+
311
+ ```bash
312
+ # 测试正确的提交信息(应该通过)
313
+ npx lint-tools-sdk commitlint "feat: add new feature"
314
+
315
+ # 测试错误的提交信息(应该失败)
316
+ npx lint-tools-sdk commitlint "add new feature"
317
+ ```
318
+
319
+ #### c) Git Hooks验证
320
+
321
+ ```bash
322
+ # 将测试文件添加到暂存区
323
+ git add test.js
324
+
325
+ # 尝试提交(应该触发pre-commit钩子)
326
+ git commit -m "fix: update test file"
327
+ ```
328
+
329
+ ## 贡献
330
+
331
+ 欢迎提交Issue和Pull Request!
332
+
333
+ ## 许可证
334
+
335
+ ISC
336
+
337
+ ## 项目构建与发布
338
+
339
+ ### 发布说明
340
+ 该项目是一个Node.js SDK,主要包含JavaScript源代码,**不需要**使用webpack、rollup等工具进行打包。由于项目结构简单,直接发布源代码即可正常工作。
341
+
342
+ ### 确保发布内容正确
343
+ 通过以下配置确保只发布必要的文件:
344
+ 1. `.gitignore`:排除node_modules、test-project等非必要文件
345
+ 2. `package.json`的`files`字段:明确指定需要发布的文件和目录
346
+
347
+ ### 发布步骤
348
+ 1. 更新版本号:
349
+ ```bash
350
+ npm version patch # 或 minor/major
351
+ ```
352
+
353
+ 2. 登录npm:
354
+ ```bash
355
+ npm login
356
+ ```
357
+
358
+ 3. 发布到npm:
359
+ ```bash
360
+ npm publish
361
+ ```
362
+
363
+ 4. (可选)发布前验证:
364
+ ```bash
365
+ # 查看将被发布的文件
366
+ npm pack --dry-run
367
+
368
+ # 或生成tgz包检查内容
369
+ npm pack
370
+ tar -tf lint-tools-sdk-*.tgz
371
+ ```
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "lint-tools-sdk",
3
+ "version": "1.0.0",
4
+ "description": "A linting tools SDK for consistent code quality across multiple git repositories",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "lint-tools-sdk": "src/index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "prepare": "husky install"
12
+ },
13
+ "keywords": ["eslint", "commitlint", "git-hooks", "linting", "code-quality"],
14
+ "author": "",
15
+ "license": "ISC",
16
+ "devDependencies": {
17
+ "@commitlint/cli": "^20.2.0",
18
+ "@commitlint/config-conventional": "^20.2.0",
19
+ "@eslint/js": "^9.39.2",
20
+ "eslint": "^9.39.2",
21
+ "globals": "^16.0.0",
22
+ "husky": "^9.1.7",
23
+ "lint-staged": "^16.2.7"
24
+ },
25
+ "peerDependencies": {
26
+ "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0",
27
+ "husky": "^8.0.0 || ^9.0.0",
28
+ "lint-staged": "^15.0.0 || ^16.0.0"
29
+ },
30
+ "files": [
31
+ "src/",
32
+ "README.md",
33
+ "package.json"
34
+ ]
35
+ }
@@ -0,0 +1,110 @@
1
+ const { execSync } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ class CommitLintService {
6
+ constructor(options = {}) {
7
+ this.options = options;
8
+ this.projectRoot = options.projectRoot || process.cwd();
9
+ }
10
+
11
+ findCommitlintConfig() {
12
+ const configFiles = [
13
+ '.commitlintrc.js',
14
+ '.commitlintrc.cjs',
15
+ '.commitlintrc.yaml',
16
+ '.commitlintrc.yml',
17
+ '.commitlintrc.json',
18
+ 'package.json'
19
+ ];
20
+
21
+ for (const configFile of configFiles) {
22
+ const filePath = path.join(this.projectRoot, configFile);
23
+ if (fs.existsSync(filePath)) {
24
+ if (configFile === 'package.json') {
25
+ const packageJson = JSON.parse(fs.readFileSync(filePath, 'utf8'));
26
+ if (packageJson.commitlint) {
27
+ return filePath;
28
+ }
29
+ } else {
30
+ return filePath;
31
+ }
32
+ }
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ lintCommitMessage(message) {
39
+ try {
40
+ const configPath = this.findCommitlintConfig();
41
+
42
+ if (!configPath) {
43
+ console.error('No commitlint configuration found');
44
+ return { success: false };
45
+ }
46
+
47
+ // Use temporary file to avoid pipe/quoting issues on Windows
48
+ const tempFile = path.join(this.projectRoot, '.temp_commit_message.txt');
49
+ fs.writeFileSync(tempFile, message);
50
+
51
+ try {
52
+ const result = execSync(
53
+ `npx commitlint --edit "${tempFile}"`,
54
+ { stdio: ['pipe', 'pipe', 'pipe'], cwd: this.projectRoot }
55
+ );
56
+
57
+ return { success: true, output: result.toString() };
58
+ } finally {
59
+ // Clean up temporary file
60
+ if (fs.existsSync(tempFile)) {
61
+ fs.unlinkSync(tempFile);
62
+ }
63
+ }
64
+ } catch (error) {
65
+ return {
66
+ success: false,
67
+ error: error.stderr ? error.stderr.toString() : error.message,
68
+ output: error.stderr ? error.stderr.toString() : error.message
69
+ };
70
+ }
71
+ }
72
+
73
+ lintCommitFromFile(filePath) {
74
+ try {
75
+ const commitMessage = fs.readFileSync(filePath, 'utf8');
76
+ return this.lintCommitMessage(commitMessage);
77
+ } catch (error) {
78
+ console.error('Failed to read commit message file:', error);
79
+ return { success: false, error: error.message };
80
+ }
81
+ }
82
+
83
+ createDefaultConfig() {
84
+ const configPath = path.join(this.projectRoot, '.commitlintrc.js');
85
+
86
+ if (!fs.existsSync(configPath)) {
87
+ const defaultConfig = `module.exports = {
88
+ extends: ['@commitlint/config-conventional']
89
+ };`;
90
+
91
+ fs.writeFileSync(configPath, defaultConfig);
92
+ console.log('Created default .commitlintrc.js');
93
+ return configPath;
94
+ }
95
+
96
+ return configPath;
97
+ }
98
+
99
+ // Get commitlint version info
100
+ getVersion() {
101
+ try {
102
+ const version = execSync('npx commitlint --version', { encoding: 'utf8', cwd: this.projectRoot });
103
+ return version.trim();
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+ }
109
+
110
+ module.exports = CommitLintService;
package/src/eslint.js ADDED
@@ -0,0 +1,138 @@
1
+ const { ESLint } = require('eslint');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ class ESLintService {
6
+ constructor(options = {}) {
7
+ this.options = options;
8
+ this.projectRoot = options.projectRoot || process.cwd();
9
+ }
10
+
11
+ async createESLintInstance() {
12
+ // For ESLint v9, we use flat config format
13
+ const eslint = new ESLint({
14
+ fix: this.options.fix || false
15
+ });
16
+
17
+ return eslint;
18
+ }
19
+
20
+ findESLintConfig() {
21
+ // ESLint v9 uses eslint.config.* files
22
+ const configFiles = [
23
+ 'eslint.config.js',
24
+ 'eslint.config.cjs',
25
+ 'eslint.config.mjs',
26
+ '.eslintrc.js',
27
+ '.eslintrc.cjs',
28
+ '.eslintrc.yaml',
29
+ '.eslintrc.yml',
30
+ '.eslintrc.json',
31
+ 'package.json'
32
+ ];
33
+
34
+ for (const configFile of configFiles) {
35
+ const filePath = path.join(this.projectRoot, configFile);
36
+ if (fs.existsSync(filePath)) {
37
+ if (configFile === 'package.json') {
38
+ const packageJson = JSON.parse(fs.readFileSync(filePath, 'utf8'));
39
+ if (packageJson.eslintConfig) {
40
+ return filePath;
41
+ }
42
+ } else {
43
+ return filePath;
44
+ }
45
+ }
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ async lintFiles(files) {
52
+ try {
53
+ const eslint = await this.createESLintInstance();
54
+ const results = await eslint.lintFiles(files);
55
+
56
+ // Fix and write changes if requested
57
+ if (this.options.fix) {
58
+ await ESLint.outputFixes(results);
59
+ }
60
+
61
+ // Format results for output
62
+ const formatter = await eslint.loadFormatter('stylish');
63
+ const resultText = formatter.format(results);
64
+
65
+ // Check if there are any errors
66
+ const hasErrors = results.some(result => result.errorCount > 0);
67
+
68
+ return {
69
+ success: !hasErrors,
70
+ results: results,
71
+ output: resultText
72
+ };
73
+ } catch (error) {
74
+ console.error('ESLint failed:', error);
75
+ return {
76
+ success: false,
77
+ error: error.message,
78
+ output: `ESLint error: ${error.message}`
79
+ };
80
+ }
81
+ }
82
+
83
+ async lintStagedFiles() {
84
+ const lintStagedConfig = this.findLintStagedConfig();
85
+
86
+ if (!lintStagedConfig) {
87
+ console.error('No lint-staged configuration found');
88
+ return { success: false };
89
+ }
90
+
91
+ try {
92
+ // Get staged files using git
93
+ const { execSync } = require('child_process');
94
+ const stagedFiles = execSync('git diff --name-only --cached', { encoding: 'utf8' })
95
+ .trim()
96
+ .split('\n')
97
+ .filter(Boolean);
98
+
99
+ if (stagedFiles.length === 0) {
100
+ return { success: true, output: 'No staged files to lint' };
101
+ }
102
+
103
+ return await this.lintFiles(stagedFiles);
104
+ } catch (error) {
105
+ console.error('Failed to get staged files:', error);
106
+ return { success: false, error: error.message };
107
+ }
108
+ }
109
+
110
+ findLintStagedConfig() {
111
+ const configFiles = [
112
+ '.lintstagedrc.js',
113
+ '.lintstagedrc.cjs',
114
+ '.lintstagedrc.yaml',
115
+ '.lintstagedrc.yml',
116
+ '.lintstagedrc.json',
117
+ 'package.json'
118
+ ];
119
+
120
+ for (const configFile of configFiles) {
121
+ const filePath = path.join(this.projectRoot, configFile);
122
+ if (fs.existsSync(filePath)) {
123
+ if (configFile === 'package.json') {
124
+ const packageJson = JSON.parse(fs.readFileSync(filePath, 'utf8'));
125
+ if (packageJson['lint-staged']) {
126
+ return packageJson['lint-staged'];
127
+ }
128
+ } else {
129
+ return require(filePath);
130
+ }
131
+ }
132
+ }
133
+
134
+ return null;
135
+ }
136
+ }
137
+
138
+ module.exports = ESLintService;
@@ -0,0 +1,190 @@
1
+ const { execSync } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ class GitHooksService {
6
+ constructor(options = {}) {
7
+ this.options = options;
8
+ this.projectRoot = options.projectRoot || process.cwd();
9
+ this.huskyDir = path.join(this.projectRoot, '.husky');
10
+ }
11
+
12
+ // Initialize husky
13
+ initHusky() {
14
+ try {
15
+ execSync('npx husky install', { stdio: 'inherit', cwd: this.projectRoot });
16
+ console.log('Husky initialized successfully');
17
+ return true;
18
+ } catch (error) {
19
+ console.error('Failed to initialize husky:', error);
20
+ return false;
21
+ }
22
+ }
23
+
24
+ // Install pre-commit hook
25
+ installPreCommitHook(script = null) {
26
+ try {
27
+ const hookScript = script || this.getDefaultPreCommitScript();
28
+ const hookPath = path.join(this.huskyDir, 'pre-commit');
29
+
30
+ // Ensure husky directory exists
31
+ if (!fs.existsSync(this.huskyDir)) {
32
+ if (!this.initHusky()) {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ // Write and make executable
38
+ fs.writeFileSync(hookPath, hookScript);
39
+ fs.chmodSync(hookPath, '755');
40
+
41
+ console.log('Pre-commit hook installed successfully');
42
+ return true;
43
+ } catch (error) {
44
+ console.error('Failed to install pre-commit hook:', error);
45
+ return false;
46
+ }
47
+ }
48
+
49
+ // Install commit-msg hook
50
+ installCommitMsgHook(script = null) {
51
+ try {
52
+ const hookScript = script || this.getDefaultCommitMsgScript();
53
+ const hookPath = path.join(this.huskyDir, 'commit-msg');
54
+
55
+ // Ensure husky directory exists
56
+ if (!fs.existsSync(this.huskyDir)) {
57
+ if (!this.initHusky()) {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ // Write and make executable
63
+ fs.writeFileSync(hookPath, hookScript);
64
+ fs.chmodSync(hookPath, '755');
65
+
66
+ console.log('Commit-msg hook installed successfully');
67
+ return true;
68
+ } catch (error) {
69
+ console.error('Failed to install commit-msg hook:', error);
70
+ return false;
71
+ }
72
+ }
73
+
74
+ // Install all required hooks
75
+ installAllHooks() {
76
+ const preCommitSuccess = this.installPreCommitHook();
77
+ const commitMsgSuccess = this.installCommitMsgHook();
78
+
79
+ return preCommitSuccess && commitMsgSuccess;
80
+ }
81
+
82
+ // Default pre-commit hook script
83
+ getDefaultPreCommitScript() {
84
+ return `npx lint-staged`;
85
+ }
86
+
87
+ // Default commit-msg hook script
88
+ getDefaultCommitMsgScript() {
89
+ return `npx commitlint --edit "$1"`;
90
+ }
91
+
92
+ // Uninstall a hook
93
+ uninstallHook(hookName) {
94
+ try {
95
+ const hookPath = path.join(this.huskyDir, hookName);
96
+
97
+ if (fs.existsSync(hookPath)) {
98
+ fs.unlinkSync(hookPath);
99
+ console.log(`${hookName} hook uninstalled successfully`);
100
+ return true;
101
+ } else {
102
+ console.log(`${hookName} hook not found`);
103
+ return false;
104
+ }
105
+ } catch (error) {
106
+ console.error(`Failed to uninstall ${hookName} hook:`, error);
107
+ return false;
108
+ }
109
+ }
110
+
111
+ // List installed hooks
112
+ listHooks() {
113
+ try {
114
+ if (!fs.existsSync(this.huskyDir)) {
115
+ console.log('No husky hooks directory found');
116
+ return [];
117
+ }
118
+
119
+ const hooks = fs.readdirSync(this.huskyDir)
120
+ .filter(file => file !== '_' && fs.statSync(path.join(this.huskyDir, file)).isFile());
121
+
122
+ console.log('Installed hooks:', hooks);
123
+ return hooks;
124
+ } catch (error) {
125
+ console.error('Failed to list hooks:', error);
126
+ return [];
127
+ }
128
+ }
129
+
130
+ // Update package.json with prepare script
131
+ updatePrepareScript() {
132
+ try {
133
+ const packageJsonPath = path.join(this.projectRoot, 'package.json');
134
+
135
+ if (fs.existsSync(packageJsonPath)) {
136
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
137
+
138
+ packageJson.scripts = packageJson.scripts || {};
139
+
140
+ // Only add prepare script if it doesn't exist
141
+ if (!packageJson.scripts.prepare) {
142
+ packageJson.scripts.prepare = 'husky install';
143
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
144
+ console.log('Added prepare script to package.json');
145
+ return true;
146
+ }
147
+
148
+ // Check if existing prepare script already includes husky install
149
+ if (packageJson.scripts.prepare.includes('husky install')) {
150
+ console.log('Prepare script already includes husky install');
151
+ return true;
152
+ }
153
+
154
+ // Append husky install to existing prepare script
155
+ packageJson.scripts.prepare += ' && husky install';
156
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
157
+ console.log('Updated prepare script to include husky install');
158
+ return true;
159
+ } else {
160
+ console.error('package.json not found');
161
+ return false;
162
+ }
163
+ } catch (error) {
164
+ console.error('Failed to update prepare script:', error);
165
+ return false;
166
+ }
167
+ }
168
+
169
+ // Check if git repository
170
+ isGitRepository() {
171
+ try {
172
+ execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore', cwd: this.projectRoot });
173
+ return true;
174
+ } catch {
175
+ return false;
176
+ }
177
+ }
178
+
179
+ // Get current git branch
180
+ getCurrentBranch() {
181
+ try {
182
+ const branch = execSync('git symbolic-ref --short HEAD', { encoding: 'utf8', cwd: this.projectRoot });
183
+ return branch.trim();
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+ }
189
+
190
+ module.exports = GitHooksService;
package/src/index.js ADDED
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ // Import service modules
8
+ const ESLintService = require('./eslint');
9
+ const CommitLintService = require('./commitlint');
10
+ const GitHooksService = require('./githooks');
11
+
12
+ class LintToolsSDK {
13
+ constructor(options = {}) {
14
+ this.options = options;
15
+ this.projectRoot = process.cwd();
16
+ this.sdkRoot = path.resolve(__dirname, '..');
17
+
18
+ // Initialize service instances
19
+ this.eslintService = new ESLintService({ projectRoot: this.projectRoot, ...options.eslint });
20
+ this.commitLintService = new CommitLintService({ projectRoot: this.projectRoot, ...options.commitlint });
21
+ this.gitHooksService = new GitHooksService({ projectRoot: this.projectRoot, ...options.githooks });
22
+
23
+ // Load configuration (project config overrides SDK defaults)
24
+ this.config = this.loadConfig();
25
+ }
26
+
27
+ // Load configuration with project-level overrides
28
+ loadConfig() {
29
+ const sdkConfig = {
30
+ eslint: {
31
+ enabled: true,
32
+ configFile: null,
33
+ fix: true
34
+ },
35
+ commitlint: {
36
+ enabled: true,
37
+ configFile: null
38
+ },
39
+ githooks: {
40
+ enabled: true,
41
+ preCommit: true,
42
+ commitMsg: true
43
+ },
44
+ lintStaged: {
45
+ enabled: true,
46
+ configFile: null
47
+ }
48
+ };
49
+
50
+ // Load project-level config from package.json
51
+ const packageJsonPath = path.join(this.projectRoot, 'package.json');
52
+ if (fs.existsSync(packageJsonPath)) {
53
+ try {
54
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
55
+ if (packageJson.lintToolsConfig) {
56
+ // Merge project config with SDK defaults
57
+ this.mergeConfig(sdkConfig, packageJson.lintToolsConfig);
58
+ }
59
+ } catch (error) {
60
+ console.warn('Failed to load project config from package.json:', error);
61
+ }
62
+ }
63
+
64
+ // Load dedicated config file if exists
65
+ const configFilePath = path.join(this.projectRoot, '.linttoolsrc.js');
66
+ if (fs.existsSync(configFilePath)) {
67
+ try {
68
+ const projectConfig = require(configFilePath);
69
+ this.mergeConfig(sdkConfig, projectConfig);
70
+ } catch (error) {
71
+ console.warn('Failed to load project config from .linttoolsrc.js:', error);
72
+ }
73
+ }
74
+
75
+ // Merge with constructor options
76
+ this.mergeConfig(sdkConfig, this.options);
77
+
78
+ return sdkConfig;
79
+ }
80
+
81
+ // Deep merge two configurations
82
+ mergeConfig(target, source) {
83
+ for (const key in source) {
84
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
85
+ if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
86
+ if (!target[key]) {
87
+ target[key] = {};
88
+ }
89
+ this.mergeConfig(target[key], source[key]);
90
+ } else {
91
+ target[key] = source[key];
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ async init() {
98
+ console.log('Initializing Lint Tools SDK...');
99
+
100
+ // Install Git hooks
101
+ if (this.config.githooks.enabled) {
102
+ await this.installHooks();
103
+ }
104
+
105
+ // Create default configs if enabled
106
+ if (this.config.eslint.enabled || this.config.commitlint.enabled) {
107
+ this.createDefaultConfigs();
108
+ }
109
+
110
+ // Update package scripts
111
+ this.updatePackageScripts();
112
+
113
+ console.log('Lint Tools SDK initialized successfully!');
114
+ return true;
115
+ }
116
+
117
+ async installHooks() {
118
+ try {
119
+ const success = this.gitHooksService.installAllHooks();
120
+ if (success) {
121
+ console.log('All Git hooks installed successfully!');
122
+ return true;
123
+ } else {
124
+ console.error('Failed to install some Git hooks');
125
+ return false;
126
+ }
127
+ } catch (error) {
128
+ console.error('Failed to install Git hooks:', error);
129
+ throw error;
130
+ }
131
+ }
132
+
133
+ createDefaultConfigs() {
134
+ console.log('Creating default configurations (if not exist)...');
135
+
136
+ // Create default eslint config if not exists and enabled
137
+ if (this.config.eslint.enabled) {
138
+ // ESLint v9 uses eslint.config.js format
139
+ const eslintConfigPath = path.join(this.projectRoot, 'eslint.config.js');
140
+ if (!fs.existsSync(eslintConfigPath)) {
141
+ const defaultEslintConfig = `// ESLint v9 configuration
142
+ import globals from "globals";
143
+ import pluginJs from "@eslint/js";
144
+
145
+ export default [
146
+ {
147
+ files: ["**/*.js"],
148
+ languageOptions: {
149
+ ecmaVersion: "latest",
150
+ sourceType: "module",
151
+ globals: {
152
+ ...globals.node,
153
+ ...globals.browser
154
+ }
155
+ },
156
+ rules: {
157
+ ...pluginJs.configs.recommended.rules,
158
+ // Add your project-specific rules here
159
+ }
160
+ }
161
+ ];`;
162
+ fs.writeFileSync(eslintConfigPath, defaultEslintConfig);
163
+ console.log('Created default eslint.config.js');
164
+ } else {
165
+ console.log('Using existing eslint.config.js');
166
+ }
167
+ }
168
+
169
+ // Create default commitlint config if not exists and enabled
170
+ if (this.config.commitlint.enabled) {
171
+ this.commitLintService.createDefaultConfig();
172
+ }
173
+
174
+ // Create default lint-staged config if not exists and enabled
175
+ if (this.config.lintStaged.enabled) {
176
+ const lintStagedConfigPath = path.join(this.projectRoot, '.lintstagedrc.js');
177
+ if (!fs.existsSync(lintStagedConfigPath)) {
178
+ const defaultLintStagedConfig = `module.exports = {
179
+ '*.js': ['eslint --fix', 'git add'],
180
+ '*.jsx': ['eslint --fix', 'git add'],
181
+ '*.ts': ['eslint --fix', 'git add'],
182
+ '*.tsx': ['eslint --fix', 'git add']
183
+ };`;
184
+ fs.writeFileSync(lintStagedConfigPath, defaultLintStagedConfig);
185
+ console.log('Created default .lintstagedrc.js');
186
+ } else {
187
+ console.log('Using existing .lintstagedrc.js');
188
+ }
189
+ }
190
+ }
191
+
192
+ updatePackageScripts() {
193
+ const packageJsonPath = path.join(this.projectRoot, 'package.json');
194
+ if (fs.existsSync(packageJsonPath)) {
195
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
196
+
197
+ // Add or update scripts
198
+ packageJson.scripts = packageJson.scripts || {};
199
+
200
+ if (this.config.eslint.enabled) {
201
+ packageJson.scripts.lint = packageJson.scripts.lint || 'eslint .';
202
+ packageJson.scripts['lint:fix'] = packageJson.scripts['lint:fix'] || 'eslint . --fix';
203
+ }
204
+
205
+ if (this.config.githooks.enabled) {
206
+ packageJson.scripts['prepare'] = packageJson.scripts['prepare'] || 'husky install';
207
+ }
208
+
209
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
210
+ console.log('Updated package.json scripts');
211
+ }
212
+ }
213
+
214
+ async runLint() {
215
+ try {
216
+ if (!this.config.eslint.enabled) {
217
+ console.log('ESLint is disabled in configuration');
218
+ return true;
219
+ }
220
+
221
+ const result = await this.eslintService.lintFiles(['.']);
222
+ console.log(result.output);
223
+ return result.success;
224
+ } catch (error) {
225
+ console.error('Lint failed!', error);
226
+ return false;
227
+ }
228
+ }
229
+
230
+ async runLintFix() {
231
+ try {
232
+ if (!this.config.eslint.enabled) {
233
+ console.log('ESLint is disabled in configuration');
234
+ return true;
235
+ }
236
+
237
+ // Temporarily enable fix option
238
+ const originalFixOption = this.eslintService.options.fix;
239
+ this.eslintService.options.fix = true;
240
+
241
+ const result = await this.eslintService.lintFiles(['.']);
242
+ console.log(result.output);
243
+
244
+ // Restore original fix option
245
+ this.eslintService.options.fix = originalFixOption;
246
+
247
+ return result.success;
248
+ } catch (error) {
249
+ console.error('Lint fix failed!', error);
250
+ return false;
251
+ }
252
+ }
253
+
254
+ async runLintStaged() {
255
+ try {
256
+ if (!this.config.eslint.enabled || !this.config.lintStaged.enabled) {
257
+ console.log('ESLint or lint-staged is disabled in configuration');
258
+ return true;
259
+ }
260
+
261
+ const result = await this.eslintService.lintStagedFiles();
262
+ console.log(result.output);
263
+ return result.success;
264
+ } catch (error) {
265
+ console.error('Lint staged failed!', error);
266
+ return false;
267
+ }
268
+ }
269
+
270
+ runCommitLint(commitMessage) {
271
+ try {
272
+ if (!this.config.commitlint.enabled) {
273
+ console.log('Commitlint is disabled in configuration');
274
+ return true;
275
+ }
276
+
277
+ const result = this.commitLintService.lintCommitMessage(commitMessage);
278
+ if (result.output) {
279
+ console.log(result.output);
280
+ }
281
+ return result.success;
282
+ } catch (error) {
283
+ console.error('Commit message lint failed!', error);
284
+ return false;
285
+ }
286
+ }
287
+
288
+ runCommitLintFromFile(filePath) {
289
+ try {
290
+ if (!this.config.commitlint.enabled) {
291
+ console.log('Commitlint is disabled in configuration');
292
+ return true;
293
+ }
294
+
295
+ const result = this.commitLintService.lintCommitFromFile(filePath);
296
+ if (result.output) {
297
+ console.log(result.output);
298
+ }
299
+ return result.success;
300
+ } catch (error) {
301
+ console.error('Commit message lint from file failed!', error);
302
+ return false;
303
+ }
304
+ }
305
+
306
+ // Get diagnostic information about the current configuration
307
+ getDiagnostics() {
308
+ return {
309
+ projectRoot: this.projectRoot,
310
+ sdkRoot: this.sdkRoot,
311
+ config: this.config,
312
+ services: {
313
+ eslint: {
314
+ configFile: this.eslintService.findESLintConfig(),
315
+ version: this.getESLintVersion()
316
+ },
317
+ commitlint: {
318
+ configFile: this.commitLintService.findCommitlintConfig(),
319
+ version: this.commitLintService.getVersion()
320
+ },
321
+ githooks: {
322
+ hooks: this.gitHooksService.listHooks(),
323
+ huskyDir: this.gitHooksService.huskyDir
324
+ }
325
+ }
326
+ };
327
+ }
328
+
329
+ // Helper to get ESLint version
330
+ getESLintVersion() {
331
+ try {
332
+ const version = execSync('npx eslint --version', { encoding: 'utf8', cwd: this.projectRoot });
333
+ return version.trim();
334
+ } catch {
335
+ return null;
336
+ }
337
+ }
338
+ }
339
+
340
+ module.exports = LintToolsSDK;
341
+
342
+ // CLI entry point
343
+ if (require.main === module) {
344
+ const sdk = new LintToolsSDK();
345
+ const command = process.argv[2];
346
+ const args = process.argv.slice(3);
347
+
348
+ // Wrap async commands in IIFE for proper async handling
349
+ (async () => {
350
+ try {
351
+ switch (command) {
352
+ case 'init':
353
+ await sdk.init();
354
+ break;
355
+ case 'lint': {
356
+ const lintSuccess = await sdk.runLint();
357
+ process.exit(lintSuccess ? 0 : 1);
358
+ break;
359
+ }
360
+ case 'lint:fix': {
361
+ const fixSuccess = await sdk.runLintFix();
362
+ process.exit(fixSuccess ? 0 : 1);
363
+ break;
364
+ }
365
+ case 'lint:staged': {
366
+ const stagedSuccess = await sdk.runLintStaged();
367
+ process.exit(stagedSuccess ? 0 : 1);
368
+ break;
369
+ }
370
+ case 'commitlint': {
371
+ if (args.length === 0) {
372
+ console.error('Error: Commit message is required');
373
+ console.log('Usage: npx lint-tools-sdk commitlint "your commit message"');
374
+ process.exit(1);
375
+ }
376
+ const commitLintSuccess = sdk.runCommitLint(args.join(' '));
377
+ process.exit(commitLintSuccess ? 0 : 1);
378
+ break;
379
+ }
380
+ case 'status': {
381
+ const diagnostics = sdk.getDiagnostics();
382
+ console.log(JSON.stringify(diagnostics, null, 2));
383
+ break;
384
+ }
385
+ default:
386
+ console.log('Usage: npx lint-tools-sdk [command]');
387
+ console.log('Commands:');
388
+ console.log(' init Initialize the lint tools in your project');
389
+ console.log(' lint Run eslint on your project');
390
+ console.log(' lint:fix Run eslint with --fix on your project');
391
+ console.log(' lint:staged Run eslint on staged files only');
392
+ console.log(' commitlint Lint a commit message');
393
+ console.log(' status Show diagnostic information');
394
+ break;
395
+ }
396
+ } catch (error) {
397
+ console.error('Error:', error.message);
398
+ process.exit(1);
399
+ }
400
+ })();
401
+ }