@linter-spec/cli 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/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/actions/init/index.d.ts +2 -0
- package/dist/actions/init/index.js +67 -0
- package/dist/actions/init/install-deps.d.ts +9 -0
- package/dist/actions/init/install-deps.js +36 -0
- package/dist/actions/init/prompts.d.ts +4 -0
- package/dist/actions/init/prompts.js +10 -0
- package/dist/actions/init/setup-husky.d.ts +7 -0
- package/dist/actions/init/setup-husky.js +18 -0
- package/dist/actions/init/write-vscode.d.ts +4 -0
- package/dist/actions/init/write-vscode.js +7 -0
- package/dist/actions/scan/index.d.ts +5 -0
- package/dist/actions/scan/index.js +20 -0
- package/dist/actions/scan/orchestrate.d.ts +6 -0
- package/dist/actions/scan/orchestrate.js +49 -0
- package/dist/actions/update.d.ts +5 -0
- package/dist/actions/update.js +61 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +9 -0
- package/dist/commands/commit-file-scan.d.ts +2 -0
- package/dist/commands/commit-file-scan.js +35 -0
- package/dist/commands/commit-msg-scan.d.ts +2 -0
- package/dist/commands/commit-msg-scan.js +24 -0
- package/dist/commands/fix.d.ts +2 -0
- package/dist/commands/fix.js +26 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.js +15 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +31 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +36 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +8 -0
- package/dist/config/_editorconfig.ejs +13 -0
- package/dist/config/_markdownlint-cli2.cjs.ejs +13 -0
- package/dist/config/_stylelintignore.ejs +5 -0
- package/dist/config/_vscode/extensions.json.ejs +9 -0
- package/dist/config/_vscode/settings.json.ejs +26 -0
- package/dist/config/commitlint.config.mjs.ejs +3 -0
- package/dist/config/eslint.config.mjs.ejs +16 -0
- package/dist/config/linter-spec.config.mjs.ejs +6 -0
- package/dist/config/prettier.config.mjs.ejs +10 -0
- package/dist/config/stylelint.config.mjs.ejs +5 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +23 -0
- package/dist/lints/eslint/do-eslint.d.ts +7 -0
- package/dist/lints/eslint/do-eslint.js +31 -0
- package/dist/lints/eslint/format-results.d.ts +6 -0
- package/dist/lints/eslint/format-results.js +25 -0
- package/dist/lints/eslint/get-config-type.d.ts +12 -0
- package/dist/lints/eslint/get-config-type.js +30 -0
- package/dist/lints/eslint/get-config.d.ts +10 -0
- package/dist/lints/eslint/get-config.js +39 -0
- package/dist/lints/eslint/index.d.ts +4 -0
- package/dist/lints/eslint/index.js +4 -0
- package/dist/lints/index.d.ts +4 -0
- package/dist/lints/index.js +4 -0
- package/dist/lints/markdownlint/do-markdownlint.d.ts +7 -0
- package/dist/lints/markdownlint/do-markdownlint.js +33 -0
- package/dist/lints/markdownlint/format-results.d.ts +7 -0
- package/dist/lints/markdownlint/format-results.js +38 -0
- package/dist/lints/markdownlint/get-config.d.ts +10 -0
- package/dist/lints/markdownlint/get-config.js +31 -0
- package/dist/lints/markdownlint/index.d.ts +3 -0
- package/dist/lints/markdownlint/index.js +3 -0
- package/dist/lints/prettier/do-prettier.d.ts +2 -0
- package/dist/lints/prettier/do-prettier.js +31 -0
- package/dist/lints/prettier/index.d.ts +1 -0
- package/dist/lints/prettier/index.js +1 -0
- package/dist/lints/resolve-files.d.ts +10 -0
- package/dist/lints/resolve-files.js +20 -0
- package/dist/lints/stylelint/do-stylelint.d.ts +7 -0
- package/dist/lints/stylelint/do-stylelint.js +15 -0
- package/dist/lints/stylelint/format-results.d.ts +6 -0
- package/dist/lints/stylelint/format-results.js +39 -0
- package/dist/lints/stylelint/get-config.d.ts +7 -0
- package/dist/lints/stylelint/get-config.js +33 -0
- package/dist/lints/stylelint/get-doc-url.d.ts +4 -0
- package/dist/lints/stylelint/get-doc-url.js +18 -0
- package/dist/lints/stylelint/index.d.ts +4 -0
- package/dist/lints/stylelint/index.js +4 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +1 -0
- package/dist/utils/conflict-resolve.d.ts +2 -0
- package/dist/utils/conflict-resolve.js +103 -0
- package/dist/utils/constants.d.ts +43 -0
- package/dist/utils/constants.js +91 -0
- package/dist/utils/errors.d.ts +9 -0
- package/dist/utils/errors.js +12 -0
- package/dist/utils/generate-template.d.ts +7 -0
- package/dist/utils/generate-template.js +67 -0
- package/dist/utils/git.d.ts +9 -0
- package/dist/utils/git.js +28 -0
- package/dist/utils/log.d.ts +8 -0
- package/dist/utils/log.js +23 -0
- package/dist/utils/messages.d.ts +47 -0
- package/dist/utils/messages.js +54 -0
- package/dist/utils/npm.d.ts +20 -0
- package/dist/utils/npm.js +76 -0
- package/dist/utils/print-report.d.ts +5 -0
- package/dist/utils/print-report.js +63 -0
- package/dist/utils/read-config.d.ts +3 -0
- package/dist/utils/read-config.js +15 -0
- package/package.json +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SotherWind
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @linter-spec/cli
|
|
2
|
+
|
|
3
|
+
`linter-spec` 前端编码规范的一站式 Lint 工具链。将 ESLint 9、Stylelint 17、markdownlint、Prettier 3 和 commitlint 19 封装在单个 CLI(以及一个轻量 Node API)之下,让项目用一条命令即可接入、扫描、修复并对提交做卡点。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add -D @linter-spec/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
各共享配置(`@linter-spec/eslint-config`、`@linter-spec/stylelint-config`、`@linter-spec/markdownlint-config`、`@linter-spec/commitlint-config`)以依赖形式一并安装——装好 CLI 即可扫描项目。
|
|
12
|
+
|
|
13
|
+
> 需要 Node.js `^20.19.0 || >=22.12.0`。
|
|
14
|
+
|
|
15
|
+
## 快速开始
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
# 交互式接入:写入配置、VS Code 设置与 git 钩子
|
|
19
|
+
npx linter-spec init
|
|
20
|
+
|
|
21
|
+
# 扫描项目中的规范问题
|
|
22
|
+
npx linter-spec scan
|
|
23
|
+
|
|
24
|
+
# 自动修复可修复的问题(先跑 Prettier,再跑各 Linter)
|
|
25
|
+
npx linter-spec fix
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`init` 会向 `package.json` 添加 `linter-spec-scan` / `linter-spec-fix` 脚本,并安装 husky 钩子(pre-commit 上的 `commit-file-scan`、commit-msg 上的 `commit-msg-scan`)。
|
|
29
|
+
|
|
30
|
+
## 命令
|
|
31
|
+
|
|
32
|
+
| 命令 | 说明 |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `init` | 初始化项目:选择项目类型、写入 Lint 配置、合并 `.vscode/settings.json`、配置 husky 钩子。`--vscode` 仅写入 VS Code 设置。 |
|
|
35
|
+
| `scan` | 运行已启用的 Linter 并打印报告。有错误时以非零状态码退出。 |
|
|
36
|
+
| `fix` | 自动修复:Prettier 先格式化,随后 ESLint / Stylelint / markdownlint 应用各自的修复。 |
|
|
37
|
+
| `commit-file-scan` | 扫描已暂存待提交的文件(供 pre-commit 钩子使用)。 |
|
|
38
|
+
| `commit-msg-scan [msgPath]` | 通过 commitlint 校验提交信息(供 commit-msg 钩子使用)。 |
|
|
39
|
+
| `update` | 将 `@linter-spec/cli` 升级到最新版本。 |
|
|
40
|
+
|
|
41
|
+
### 常用选项
|
|
42
|
+
|
|
43
|
+
| 选项 | 适用命令 | 说明 |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `-i, --include <dirpath>` | `scan`、`fix` | 要扫描的目录(默认为项目根目录)。 |
|
|
46
|
+
| `-q, --quiet` | `scan` | 仅报告错误,忽略警告。 |
|
|
47
|
+
| `-o, --output-report` | `scan` | 写出一份 `linter-spec-report.json` 报告文件。 |
|
|
48
|
+
| `--no-ignore` | `scan`、`fix` | 忽略 ESLint 的 ignore 文件 / 规则。 |
|
|
49
|
+
| `-s, --strict` | `commit-file-scan` | 警告也视为失败,而不仅仅是错误。 |
|
|
50
|
+
| `--vscode` | `init` | 仅写入 `.vscode/settings.json`。 |
|
|
51
|
+
|
|
52
|
+
## Node API
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { init, scan } from '@linter-spec/cli';
|
|
56
|
+
|
|
57
|
+
// 编程式 init(绝不会自我升级 CLI)
|
|
58
|
+
await init({
|
|
59
|
+
cwd: process.cwd(),
|
|
60
|
+
eslintType: 'typescript/react', // 取值见下文「项目类型」
|
|
61
|
+
enableStylelint: true,
|
|
62
|
+
enableMarkdownlint: true,
|
|
63
|
+
enablePrettier: true,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 编程式 scan —— 返回结构化报告
|
|
67
|
+
const report = await scan({ cwd: process.cwd(), include: process.cwd(), fix: false });
|
|
68
|
+
console.log(report.errorCount, report.warningCount);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`scan` 解析为一个 `ScanReport`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
interface ScanReport {
|
|
75
|
+
results: ScanResult[]; // 每个文件的消息与计数
|
|
76
|
+
errorCount: number;
|
|
77
|
+
warningCount: number;
|
|
78
|
+
runErrors: Error[]; // 某个 Linter 崩溃不会中断其余 Linter
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 项目类型
|
|
83
|
+
|
|
84
|
+
`init` 的 `eslintType`(以及交互式选项)与 `@linter-spec/eslint-config` 的入口一一对应:
|
|
85
|
+
|
|
86
|
+
| 取值 | 项目 |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `index` | 纯 JavaScript |
|
|
89
|
+
| `typescript` | TypeScript |
|
|
90
|
+
| `react` / `typescript/react` | React(JS / TS) |
|
|
91
|
+
| `vue` / `typescript/vue` | Vue(JS / TS) |
|
|
92
|
+
| `node` / `typescript/node` | Node.js(JS / TS) |
|
|
93
|
+
| `es5` | 传统 ES5 |
|
|
94
|
+
|
|
95
|
+
## 配置文件
|
|
96
|
+
|
|
97
|
+
在项目根目录放置 `linter-spec.config.{mjs,cjs,js}` 即可开关各 Linter 或透传选项。传给 Node API 的内联配置优先级更高。
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
// linter-spec.config.mjs
|
|
101
|
+
export default {
|
|
102
|
+
enableESLint: true,
|
|
103
|
+
enableStylelint: true,
|
|
104
|
+
enableMarkdownlint: true,
|
|
105
|
+
enablePrettier: true,
|
|
106
|
+
// eslintOptions / stylelintOptions / markdownlintOptions 会被透传
|
|
107
|
+
};
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 仓库
|
|
111
|
+
|
|
112
|
+
- 源码:<https://github.com/SotherWind/linter-spec>
|
|
113
|
+
- 许可证:MIT
|
|
114
|
+
- 作者:SotherWind
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import update from '../update.js';
|
|
4
|
+
import log from '../../utils/log.js';
|
|
5
|
+
import conflictResolve from '../../utils/conflict-resolve.js';
|
|
6
|
+
import generateTemplate from '../../utils/generate-template.js';
|
|
7
|
+
import { CLI_NAME, PROJECT_TYPES } from '../../utils/constants.js';
|
|
8
|
+
import { messages } from '../../utils/messages.js';
|
|
9
|
+
import { chooseEnableMarkdownlint, chooseEnablePrettier, chooseEnableStylelint, chooseEslintType, } from './prompts.js';
|
|
10
|
+
import { installCliDep } from './install-deps.js';
|
|
11
|
+
import { setupHusky } from './setup-husky.js';
|
|
12
|
+
export default async function init(options) {
|
|
13
|
+
const cwd = options.cwd || process.cwd();
|
|
14
|
+
const isTest = process.env.NODE_ENV === 'test';
|
|
15
|
+
const checkVersionUpdate = options.checkVersionUpdate || false;
|
|
16
|
+
const disableNpmInstall = options.disableNpmInstall || false;
|
|
17
|
+
const pkgPath = path.resolve(cwd, 'package.json');
|
|
18
|
+
let pkg = fs.readJSONSync(pkgPath);
|
|
19
|
+
let step = 0;
|
|
20
|
+
if (!isTest && checkVersionUpdate) {
|
|
21
|
+
await update(false);
|
|
22
|
+
}
|
|
23
|
+
const enableESLint = typeof options.enableESLint === 'boolean' ? options.enableESLint : true;
|
|
24
|
+
const eslintType = options.eslintType && PROJECT_TYPES.some((c) => c.value === options.eslintType)
|
|
25
|
+
? options.eslintType
|
|
26
|
+
: await chooseEslintType(++step);
|
|
27
|
+
const enableStylelint = typeof options.enableStylelint === 'boolean'
|
|
28
|
+
? options.enableStylelint
|
|
29
|
+
: await chooseEnableStylelint(++step, !/node/.test(eslintType));
|
|
30
|
+
const enableMarkdownlint = typeof options.enableMarkdownlint === 'boolean'
|
|
31
|
+
? options.enableMarkdownlint
|
|
32
|
+
: await chooseEnableMarkdownlint(++step);
|
|
33
|
+
const enablePrettier = typeof options.enablePrettier === 'boolean'
|
|
34
|
+
? options.enablePrettier
|
|
35
|
+
: await chooseEnablePrettier(++step);
|
|
36
|
+
const config = {
|
|
37
|
+
enableESLint,
|
|
38
|
+
eslintType,
|
|
39
|
+
enableStylelint,
|
|
40
|
+
enableMarkdownlint,
|
|
41
|
+
enablePrettier,
|
|
42
|
+
};
|
|
43
|
+
if (!isTest) {
|
|
44
|
+
log.info(messages.stepConflict(++step));
|
|
45
|
+
pkg = await conflictResolve(cwd, options.rewriteConfig);
|
|
46
|
+
log.success(messages.stepConflictDone(step));
|
|
47
|
+
if (!disableNpmInstall) {
|
|
48
|
+
log.info(messages.stepInstall(++step));
|
|
49
|
+
await installCliDep(cwd);
|
|
50
|
+
log.success(messages.stepInstallDone(step));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Re-read: install / conflict-resolve may have rewritten package.json.
|
|
54
|
+
pkg = fs.readJSONSync(pkgPath);
|
|
55
|
+
if (!pkg.scripts)
|
|
56
|
+
pkg.scripts = {};
|
|
57
|
+
pkg.scripts[`${CLI_NAME}-scan`] ??= `${CLI_NAME} scan`;
|
|
58
|
+
pkg.scripts[`${CLI_NAME}-fix`] ??= `${CLI_NAME} fix`;
|
|
59
|
+
log.info(messages.stepHusky(++step));
|
|
60
|
+
setupHusky(cwd, pkg);
|
|
61
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
62
|
+
log.success(messages.stepHuskyDone(step));
|
|
63
|
+
log.info(messages.stepWrite(++step));
|
|
64
|
+
generateTemplate(cwd, config);
|
|
65
|
+
log.success(messages.stepWriteDone(step));
|
|
66
|
+
log.success(messages.initDone);
|
|
67
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** True when the project carries its own lint config files. */
|
|
2
|
+
export declare function hasLocalLintConfig(cwd: string): boolean;
|
|
3
|
+
/** Install the CLI itself as a dev dependency of the target project. */
|
|
4
|
+
export declare function installCliDep(cwd: string): Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* When a project relies on the bundled lint configs but has no `node_modules`,
|
|
7
|
+
* install its dependencies first (otherwise config resolution fails).
|
|
8
|
+
*/
|
|
9
|
+
export declare function installProjectDepsIfMissing(cwd: string, hasLocalConfig: boolean): Promise<void>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import fg from 'fast-glob';
|
|
5
|
+
import { detectPackageManager, addDevCommand, installAllCommand } from '../../utils/npm.js';
|
|
6
|
+
import log from '../../utils/log.js';
|
|
7
|
+
import { messages } from '../../utils/messages.js';
|
|
8
|
+
import { PKG_NAME } from '../../utils/constants.js';
|
|
9
|
+
/** True when the project carries its own lint config files. */
|
|
10
|
+
export function hasLocalLintConfig(cwd) {
|
|
11
|
+
return (fg.sync([
|
|
12
|
+
'.eslintrc?(.@(js|cjs|mjs|yaml|yml|json))',
|
|
13
|
+
'eslint.config.@(js|mjs|cjs|ts)',
|
|
14
|
+
'.stylelintrc?(.@(js|cjs|mjs|yaml|yml|json))',
|
|
15
|
+
'stylelint.config.@(js|mjs|cjs)',
|
|
16
|
+
'.markdownlint?(-cli2).@(jsonc|json|yaml|yml|cjs|mjs)',
|
|
17
|
+
], { cwd, dot: true }).length > 0);
|
|
18
|
+
}
|
|
19
|
+
/** Install the CLI itself as a dev dependency of the target project. */
|
|
20
|
+
export async function installCliDep(cwd) {
|
|
21
|
+
const [command, args] = addDevCommand(detectPackageManager(cwd), PKG_NAME);
|
|
22
|
+
spawn.sync(command, args, { stdio: 'inherit', cwd });
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* When a project relies on the bundled lint configs but has no `node_modules`,
|
|
26
|
+
* install its dependencies first (otherwise config resolution fails).
|
|
27
|
+
*/
|
|
28
|
+
export async function installProjectDepsIfMissing(cwd, hasLocalConfig) {
|
|
29
|
+
const nodeModulesPath = path.resolve(cwd, 'node_modules');
|
|
30
|
+
if (!fs.existsSync(nodeModulesPath) && hasLocalConfig) {
|
|
31
|
+
const pm = detectPackageManager(cwd);
|
|
32
|
+
log.info(messages.installingDeps(pm));
|
|
33
|
+
const [command, args] = installAllCommand(pm);
|
|
34
|
+
spawn.sync(command, args, { cwd, stdio: 'inherit' });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const chooseEslintType: (step: number) => Promise<string>;
|
|
2
|
+
export declare const chooseEnableStylelint: (step: number, defaultValue: boolean) => Promise<boolean>;
|
|
3
|
+
export declare const chooseEnableMarkdownlint: (step: number) => Promise<boolean>;
|
|
4
|
+
export declare const chooseEnablePrettier: (step: number) => Promise<boolean>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
2
|
+
import { PROJECT_TYPES } from '../../utils/constants.js';
|
|
3
|
+
import { messages } from '../../utils/messages.js';
|
|
4
|
+
export const chooseEslintType = (step) => select({
|
|
5
|
+
message: messages.stepChooseType(step),
|
|
6
|
+
choices: PROJECT_TYPES.map((t) => ({ name: t.name, value: t.value })),
|
|
7
|
+
});
|
|
8
|
+
export const chooseEnableStylelint = (step, defaultValue) => confirm({ message: messages.stepEnableStylelint(step), default: defaultValue });
|
|
9
|
+
export const chooseEnableMarkdownlint = (step) => confirm({ message: messages.stepEnableMarkdownlint(step), default: true });
|
|
10
|
+
export const chooseEnablePrettier = (step) => confirm({ message: messages.stepEnablePrettier(step), default: true });
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PKG } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Configure git commit hooks via husky v9.
|
|
4
|
+
* Writes the hook scripts and ensures a `prepare` script so husky installs on
|
|
5
|
+
* the next `npm install`. Mutates `pkg.scripts` (caller persists package.json).
|
|
6
|
+
*/
|
|
7
|
+
export declare function setupHusky(cwd: string, pkg: PKG): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { CLI_NAME } from '../../utils/constants.js';
|
|
4
|
+
/**
|
|
5
|
+
* Configure git commit hooks via husky v9.
|
|
6
|
+
* Writes the hook scripts and ensures a `prepare` script so husky installs on
|
|
7
|
+
* the next `npm install`. Mutates `pkg.scripts` (caller persists package.json).
|
|
8
|
+
*/
|
|
9
|
+
export function setupHusky(cwd, pkg) {
|
|
10
|
+
if (!pkg.scripts)
|
|
11
|
+
pkg.scripts = {};
|
|
12
|
+
if (!pkg.scripts.prepare)
|
|
13
|
+
pkg.scripts.prepare = 'husky';
|
|
14
|
+
const huskyDir = path.resolve(cwd, '.husky');
|
|
15
|
+
fs.ensureDirSync(huskyDir);
|
|
16
|
+
fs.writeFileSync(path.join(huskyDir, 'pre-commit'), `${CLI_NAME} commit-file-scan\n`, 'utf8');
|
|
17
|
+
fs.writeFileSync(path.join(huskyDir, 'commit-msg'), `${CLI_NAME} commit-msg-scan "$1"\n`, 'utf8');
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { CLI_NAME } from '../../utils/constants.js';
|
|
4
|
+
import { readLinterSpecConfig } from '../../utils/read-config.js';
|
|
5
|
+
import { orchestrate } from './orchestrate.js';
|
|
6
|
+
/**
|
|
7
|
+
* Read project config, run the linters, and (optionally) write a JSON report.
|
|
8
|
+
*/
|
|
9
|
+
export default async function scan(options) {
|
|
10
|
+
const { cwd, outputReport, config: inlineConfig } = options;
|
|
11
|
+
const pkgPath = path.resolve(cwd, 'package.json');
|
|
12
|
+
const pkg = fs.existsSync(pkgPath) ? fs.readJSONSync(pkgPath) : {};
|
|
13
|
+
const config = inlineConfig || (await readLinterSpecConfig(cwd));
|
|
14
|
+
const report = await orchestrate(options, pkg, config);
|
|
15
|
+
if (outputReport) {
|
|
16
|
+
const reportPath = path.resolve(cwd, `./${CLI_NAME}-report.json`);
|
|
17
|
+
await fs.outputFile(reportPath, JSON.stringify(report.results, null, 2));
|
|
18
|
+
}
|
|
19
|
+
return report;
|
|
20
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Config, PKG, ScanOptions, ScanReport } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run the enabled linters over the project and collect a unified report.
|
|
4
|
+
* Each linter is isolated so one crashing does not abort the others.
|
|
5
|
+
*/
|
|
6
|
+
export declare function orchestrate(options: ScanOptions, pkg: PKG, config: Config): Promise<ScanReport>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { doESLint, doMarkdownlint, doPrettier, doStylelint } from '../../lints/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run the enabled linters over the project and collect a unified report.
|
|
4
|
+
* Each linter is isolated so one crashing does not abort the others.
|
|
5
|
+
*/
|
|
6
|
+
export async function orchestrate(options, pkg, config) {
|
|
7
|
+
const { fix } = options;
|
|
8
|
+
const runErrors = [];
|
|
9
|
+
let results = [];
|
|
10
|
+
// Prettier first (only on fix), so subsequent linters see formatted code.
|
|
11
|
+
if (fix && config.enablePrettier !== false) {
|
|
12
|
+
try {
|
|
13
|
+
await doPrettier(options);
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
runErrors.push(e);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (config.enableESLint !== false) {
|
|
20
|
+
try {
|
|
21
|
+
results = results.concat(await doESLint({ ...options, pkg, config }));
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
runErrors.push(e);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (config.enableStylelint !== false) {
|
|
28
|
+
try {
|
|
29
|
+
results = results.concat(await doStylelint({ ...options, pkg, config }));
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
runErrors.push(e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (config.enableMarkdownlint !== false) {
|
|
36
|
+
try {
|
|
37
|
+
results = results.concat(await doMarkdownlint({ ...options, pkg, config }));
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
runErrors.push(e);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
results,
|
|
45
|
+
errorCount: results.reduce((count, { errorCount }) => count + errorCount, 0),
|
|
46
|
+
warningCount: results.reduce((count, { warningCount }) => count + warningCount, 0),
|
|
47
|
+
runErrors,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import log from '../utils/log.js';
|
|
4
|
+
import { detectPackageManager, addGlobalCommand } from '../utils/npm.js';
|
|
5
|
+
import { messages } from '../utils/messages.js';
|
|
6
|
+
import { PKG_NAME, PKG_VERSION } from '../utils/constants.js';
|
|
7
|
+
/** Return the latest published version if it is newer than the local one. */
|
|
8
|
+
async function checkLatestVersion() {
|
|
9
|
+
// Query the registry directly — package-manager agnostic, and avoids spawning
|
|
10
|
+
// a child process just to read one version string.
|
|
11
|
+
const res = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`);
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
return null;
|
|
14
|
+
const data = (await res.json());
|
|
15
|
+
const latestVersion = data.version;
|
|
16
|
+
if (!latestVersion || PKG_VERSION === latestVersion)
|
|
17
|
+
return null;
|
|
18
|
+
// NOTE: simple numeric `x.y.z` comparison only — pre-release / build metadata
|
|
19
|
+
// (`1.0.0-beta.1`, `1.0.0+build.5`) is not handled. Our own releases are plain
|
|
20
|
+
// semver, so this holds; swap in the `semver` package if that ever changes.
|
|
21
|
+
const current = PKG_VERSION.split('.').map(Number);
|
|
22
|
+
const latest = latestVersion.split('.').map(Number);
|
|
23
|
+
for (let i = 0; i < current.length; i++) {
|
|
24
|
+
if ((current[i] ?? 0) > (latest[i] ?? 0))
|
|
25
|
+
return null;
|
|
26
|
+
if ((current[i] ?? 0) < (latest[i] ?? 0))
|
|
27
|
+
return latestVersion;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check for (and optionally install) a newer version of the CLI.
|
|
33
|
+
* @param install install the new version globally when found
|
|
34
|
+
*/
|
|
35
|
+
export default async function update(install = true) {
|
|
36
|
+
const checking = ora(messages.updateChecking);
|
|
37
|
+
checking.start();
|
|
38
|
+
try {
|
|
39
|
+
const pm = detectPackageManager();
|
|
40
|
+
const latestVersion = await checkLatestVersion();
|
|
41
|
+
checking.stop();
|
|
42
|
+
if (latestVersion && install) {
|
|
43
|
+
const updating = ora(messages.updateFound(latestVersion));
|
|
44
|
+
updating.start();
|
|
45
|
+
const [command, args] = addGlobalCommand(pm, PKG_NAME);
|
|
46
|
+
await execa(command, args, { stdio: 'inherit' });
|
|
47
|
+
updating.stop();
|
|
48
|
+
}
|
|
49
|
+
else if (latestVersion) {
|
|
50
|
+
const [command, args] = addGlobalCommand(pm, `${PKG_NAME}@latest`);
|
|
51
|
+
log.warn(messages.updateHint(latestVersion, PKG_VERSION, [command, ...args].join(' ')));
|
|
52
|
+
}
|
|
53
|
+
else if (install) {
|
|
54
|
+
log.info(messages.updateNone);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
checking.stop();
|
|
59
|
+
log.error(e);
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import { registerCommands } from './commands/index.js';
|
|
4
|
+
import { messages } from './utils/messages.js';
|
|
5
|
+
import { CLI_NAME, PKG_VERSION } from './utils/constants.js';
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
program.name(CLI_NAME).version(PKG_VERSION).description(messages.description);
|
|
8
|
+
registerCommands(program, cwd);
|
|
9
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import scan from '../actions/scan/index.js';
|
|
3
|
+
import printReport from '../utils/print-report.js';
|
|
4
|
+
import log from '../utils/log.js';
|
|
5
|
+
import { getAmendFiles, getCommitFiles } from '../utils/git.js';
|
|
6
|
+
import { hasLocalLintConfig, installProjectDepsIfMissing } from '../actions/init/install-deps.js';
|
|
7
|
+
import { messages } from '../utils/messages.js';
|
|
8
|
+
export function registerCommitFileScan(program, cwd) {
|
|
9
|
+
program
|
|
10
|
+
.command('commit-file-scan')
|
|
11
|
+
.description(messages.commitFileScanDescription)
|
|
12
|
+
.option('-s, --strict', messages.optStrict)
|
|
13
|
+
.action(async (cmd) => {
|
|
14
|
+
await installProjectDepsIfMissing(cwd, hasLocalLintConfig(cwd));
|
|
15
|
+
const files = await getAmendFiles({ cwd });
|
|
16
|
+
if (files)
|
|
17
|
+
log.warn(messages.notStaged(files));
|
|
18
|
+
const checking = ora();
|
|
19
|
+
checking.start(messages.runCommitChecking);
|
|
20
|
+
const { results, errorCount, warningCount } = await scan({
|
|
21
|
+
cwd,
|
|
22
|
+
include: cwd,
|
|
23
|
+
quiet: !cmd.strict,
|
|
24
|
+
files: await getCommitFiles({ cwd }),
|
|
25
|
+
});
|
|
26
|
+
if (errorCount > 0 || (cmd.strict && warningCount > 0)) {
|
|
27
|
+
checking.fail();
|
|
28
|
+
printReport(results, false);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
checking.succeed();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { messages } from '../utils/messages.js';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
export function registerCommitMsgScan(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('commit-msg-scan [msgPath]')
|
|
8
|
+
.description(messages.commitMsgScanDescription)
|
|
9
|
+
.action((msgPath) => {
|
|
10
|
+
const editFile = msgPath || process.env.HUSKY_GIT_PARAMS || process.env.GIT_PARAMS || '.git/COMMIT_EDITMSG';
|
|
11
|
+
// Resolve @commitlint/cli from our own dependencies and run it via node,
|
|
12
|
+
// rather than assuming a `commitlint` binary is on PATH — it isn't when
|
|
13
|
+
// this CLI is installed globally into a project that lacks commitlint.
|
|
14
|
+
const commitlintBin = require.resolve('@commitlint/cli/cli.js');
|
|
15
|
+
const result = spawn.sync(process.execPath, [commitlintBin, '--edit', editFile], {
|
|
16
|
+
stdio: 'inherit',
|
|
17
|
+
});
|
|
18
|
+
if (result.status) {
|
|
19
|
+
// Propagate commitlint's own exit code to the git hook verbatim.
|
|
20
|
+
// eslint-disable-next-line n/no-process-exit
|
|
21
|
+
process.exit(result.status);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import scan from '../actions/scan/index.js';
|
|
3
|
+
import printReport from '../utils/print-report.js';
|
|
4
|
+
import { hasLocalLintConfig, installProjectDepsIfMissing } from '../actions/init/install-deps.js';
|
|
5
|
+
import { messages } from '../utils/messages.js';
|
|
6
|
+
export function registerFix(program, cwd) {
|
|
7
|
+
program
|
|
8
|
+
.command('fix')
|
|
9
|
+
.description(messages.fixDescription)
|
|
10
|
+
.option('-i, --include <dirpath>', messages.optInclude)
|
|
11
|
+
.option('--no-ignore', messages.optNoIgnore)
|
|
12
|
+
.action(async (cmd) => {
|
|
13
|
+
await installProjectDepsIfMissing(cwd, hasLocalLintConfig(cwd));
|
|
14
|
+
const checking = ora();
|
|
15
|
+
checking.start(messages.runFixing);
|
|
16
|
+
const { results } = await scan({
|
|
17
|
+
cwd,
|
|
18
|
+
fix: true,
|
|
19
|
+
include: cmd.include || cwd,
|
|
20
|
+
ignore: cmd.ignore,
|
|
21
|
+
});
|
|
22
|
+
checking.succeed();
|
|
23
|
+
if (results.length > 0)
|
|
24
|
+
printReport(results, true);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { registerInit } from './init.js';
|
|
2
|
+
import { registerScan } from './scan.js';
|
|
3
|
+
import { registerFix } from './fix.js';
|
|
4
|
+
import { registerUpdate } from './update.js';
|
|
5
|
+
import { registerCommitMsgScan } from './commit-msg-scan.js';
|
|
6
|
+
import { registerCommitFileScan } from './commit-file-scan.js';
|
|
7
|
+
/** Register every CLI command on the commander program. */
|
|
8
|
+
export function registerCommands(program, cwd) {
|
|
9
|
+
registerInit(program, cwd);
|
|
10
|
+
registerScan(program, cwd);
|
|
11
|
+
registerFix(program, cwd);
|
|
12
|
+
registerCommitMsgScan(program);
|
|
13
|
+
registerCommitFileScan(program, cwd);
|
|
14
|
+
registerUpdate(program);
|
|
15
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import init from '../actions/init/index.js';
|
|
2
|
+
import { writeVSCodeConfig } from '../actions/init/write-vscode.js';
|
|
3
|
+
import { readLinterSpecConfig } from '../utils/read-config.js';
|
|
4
|
+
import { CliAbortError } from '../utils/errors.js';
|
|
5
|
+
import log from '../utils/log.js';
|
|
6
|
+
import { messages } from '../utils/messages.js';
|
|
7
|
+
export function registerInit(program, cwd) {
|
|
8
|
+
program
|
|
9
|
+
.command('init')
|
|
10
|
+
.description(messages.initDescription)
|
|
11
|
+
.option('--vscode', messages.optVscode)
|
|
12
|
+
.action(async (cmd) => {
|
|
13
|
+
try {
|
|
14
|
+
if (cmd.vscode) {
|
|
15
|
+
const config = await readLinterSpecConfig(cwd);
|
|
16
|
+
writeVSCodeConfig(cwd, config);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
await init({ cwd, checkVersionUpdate: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
// User declined a destructive step — exit cleanly, not as a crash.
|
|
24
|
+
if (e instanceof CliAbortError) {
|
|
25
|
+
log.info(messages.conflictCancelled);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|