aislop 0.1.3 → 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/README.md +90 -262
- package/dist/cli.js +109 -14
- package/dist/{engine-info-Bi8pE12U.js → engine-info-DpU0WTTj.js} +1 -1
- package/dist/index.js +176 -107
- package/dist/{json-66-1kHeg.js → json-UG8l_sLC.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,55 +3,38 @@
|
|
|
3
3
|
**Stop AI slop from shipping.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/aislop)
|
|
6
|
+
[](https://www.npmjs.com/package/aislop)
|
|
6
7
|
[](https://github.com/heavykenny/aislop/actions/workflows/ci.yml)
|
|
7
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://nodejs.org)
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
`aislop` is a unified code-quality CLI for multi-language repositories. It runs formatting, linting, code-quality, AI-pattern, architecture, and security checks behind a single command and summarizes everything as one score out of 100.
|
|
11
|
+
`aislop` is a unified code-quality CLI that catches the lazy patterns AI coding tools leave behind. One command, one score out of 100.
|
|
12
12
|
|
|
13
13
|
```
|
|
14
14
|
$ npx aislop scan
|
|
15
15
|
|
|
16
|
-
aislop Scan v0.
|
|
16
|
+
aislop Scan v0.2.0
|
|
17
17
|
|
|
18
18
|
✓ Project my-app (typescript)
|
|
19
19
|
Source files: 142
|
|
20
20
|
|
|
21
|
-
✓ Formatting:
|
|
22
|
-
✓ Linting:
|
|
23
|
-
✓ Code Quality:
|
|
24
|
-
! Maintainability:
|
|
25
|
-
✓ Security:
|
|
21
|
+
✓ Formatting: done (0 issues)
|
|
22
|
+
✓ Linting: done (2 warnings)
|
|
23
|
+
✓ Code Quality: done (1 warning)
|
|
24
|
+
! Maintainability: done (4 warnings)
|
|
25
|
+
✓ Security: done (0 issues)
|
|
26
26
|
|
|
27
27
|
Score: 80/100 (Healthy)
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
-
## Why aislop?
|
|
33
|
-
|
|
34
|
-
Code generated by AI assistants often looks correct at first glance but is full of quality issues that pass review because they are spread across dozens of files. No single existing linter catches all of them. `aislop` was built to be the one tool that does:
|
|
35
|
-
|
|
36
|
-
- **Catches AI-specific patterns** that ESLint and Prettier ignore: trivial comments (`// Import React`), thin wrappers, generic names (`data2`, `helper_1`), swallowed exceptions
|
|
37
|
-
- **Multi-language** -- TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP in one CLI
|
|
38
|
-
- **Single score** -- one number to gate PRs, track in CI, and trend over time
|
|
39
|
-
- **Zero config to start** -- run `npx aislop scan` and get results immediately
|
|
40
|
-
- **Batteries included** -- ships with oxlint, biome, and knip; downloads ruff and golangci-lint automatically on install
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
32
|
## Installation
|
|
45
33
|
|
|
46
|
-
### Run without installing
|
|
47
|
-
|
|
48
34
|
```bash
|
|
35
|
+
# Run without installing
|
|
49
36
|
npx aislop scan
|
|
50
|
-
```
|
|
51
37
|
|
|
52
|
-
### Install as a dev dependency
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
38
|
# npm
|
|
56
39
|
npm install --save-dev aislop
|
|
57
40
|
|
|
@@ -60,298 +43,143 @@ yarn add --dev aislop
|
|
|
60
43
|
|
|
61
44
|
# pnpm
|
|
62
45
|
pnpm add -D aislop
|
|
63
|
-
```
|
|
64
46
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```bash
|
|
47
|
+
# Global
|
|
68
48
|
npm install -g aislop
|
|
69
|
-
aislop scan
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
`aislop` ships with Node-based tooling (oxlint, biome, knip) as package dependencies. On install it also downloads bundled binaries for **ruff** and **golangci-lint**. To skip those downloads:
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
AISLOP_SKIP_TOOL_DOWNLOAD=1 npm install
|
|
76
49
|
```
|
|
77
50
|
|
|
78
|
-
|
|
51
|
+
Also available as [`@heavykenny/aislop`](docs/installation.md) on GitHub Packages.
|
|
79
52
|
|
|
80
53
|
---
|
|
81
54
|
|
|
82
|
-
##
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
# Scan the current directory
|
|
86
|
-
npx aislop scan
|
|
87
|
-
|
|
88
|
-
# Auto-fix formatting and lint issues
|
|
89
|
-
npx aislop fix
|
|
90
|
-
|
|
91
|
-
# Scan only changed files (great for pre-commit)
|
|
92
|
-
npx aislop scan --changes
|
|
55
|
+
## Usage
|
|
93
56
|
|
|
94
|
-
|
|
95
|
-
npx aislop scan --staged
|
|
96
|
-
|
|
97
|
-
# CI-friendly JSON output
|
|
98
|
-
npx aislop ci
|
|
99
|
-
|
|
100
|
-
# Initialize config files
|
|
101
|
-
npx aislop init
|
|
57
|
+
### Scan your project
|
|
102
58
|
|
|
103
|
-
|
|
104
|
-
|
|
59
|
+
```bash
|
|
60
|
+
aislop scan # scan current directory
|
|
61
|
+
aislop scan ./src # scan a specific directory
|
|
62
|
+
aislop scan --changes # only files changed from HEAD
|
|
63
|
+
aislop scan --staged # only staged files (pre-commit)
|
|
64
|
+
aislop scan --json # output JSON
|
|
105
65
|
```
|
|
106
66
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
## Commands
|
|
110
|
-
|
|
111
|
-
| Command | What it does |
|
|
112
|
-
|---|---|
|
|
113
|
-
| `aislop` | Interactive TTY menu (falls back to `scan` in non-TTY) |
|
|
114
|
-
| `aislop scan [dir]` | Run all enabled engines and print a scored report |
|
|
115
|
-
| `aislop fix [dir]` | Apply safe auto-fixes (formatting + lint) |
|
|
116
|
-
| `aislop ci [dir]` | Output JSON for CI pipelines |
|
|
117
|
-
| `aislop init [dir]` | Create `.aislop/config.yml` and `.aislop/rules.yml` |
|
|
118
|
-
| `aislop doctor [dir]` | Report which tools are installed and available |
|
|
119
|
-
| `aislop rules [dir]` | List all built-in and configured rules |
|
|
120
|
-
|
|
121
|
-
### Flags
|
|
122
|
-
|
|
123
|
-
| Flag | Description |
|
|
124
|
-
|---|---|
|
|
125
|
-
| `--changes` | Only scan files changed from `HEAD` |
|
|
126
|
-
| `--staged` | Only scan staged files |
|
|
127
|
-
| `-d, --verbose` | Show detailed per-file output |
|
|
128
|
-
| `--json` | Output JSON instead of terminal UI |
|
|
129
|
-
| `-v, --version` | Print version |
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
## What it catches
|
|
134
|
-
|
|
135
|
-
`aislop` groups checks into six engines. Each engine runs in parallel for speed.
|
|
136
|
-
|
|
137
|
-
### Formatting
|
|
138
|
-
|
|
139
|
-
Enforces consistent formatting using the best tool for each language.
|
|
140
|
-
|
|
141
|
-
| Language | Tool |
|
|
142
|
-
|---|---|
|
|
143
|
-
| TypeScript / JavaScript | Biome |
|
|
144
|
-
| Python | ruff format |
|
|
145
|
-
| Go | gofmt |
|
|
146
|
-
| Rust | cargo fmt |
|
|
147
|
-
| Ruby | rubocop |
|
|
148
|
-
| PHP | php-cs-fixer |
|
|
149
|
-
|
|
150
|
-
### Linting
|
|
151
|
-
|
|
152
|
-
Catches bugs and bad practices.
|
|
153
|
-
|
|
154
|
-
| Language | Tool |
|
|
155
|
-
|---|---|
|
|
156
|
-
| TypeScript / JavaScript | oxlint (bundled, with React/Next.js awareness) |
|
|
157
|
-
| Python | ruff |
|
|
158
|
-
| Go | golangci-lint |
|
|
159
|
-
| Rust | clippy |
|
|
160
|
-
| Ruby | rubocop |
|
|
161
|
-
|
|
162
|
-
### Code quality
|
|
163
|
-
|
|
164
|
-
Measures structural complexity and finds dead code.
|
|
165
|
-
|
|
166
|
-
| Rule | What it checks |
|
|
167
|
-
|---|---|
|
|
168
|
-
| `complexity/function-too-long` | Functions exceeding configurable line limit (default: 80) |
|
|
169
|
-
| `complexity/file-too-large` | Files exceeding configurable line limit (default: 400) |
|
|
170
|
-
| `complexity/deep-nesting` | Control-flow nesting beyond threshold (default: 5) |
|
|
171
|
-
| `complexity/too-many-params` | Functions with too many parameters (default: 6) |
|
|
172
|
-
| `duplication/block` | Cross-file duplicate code blocks (12+ lines) |
|
|
173
|
-
| `knip/files`, `knip/exports`, `knip/types` | Dead code via knip (JS/TS) |
|
|
174
|
-
|
|
175
|
-
### AI slop detection (Maintainability)
|
|
176
|
-
|
|
177
|
-
The rules that make aislop unique. These catch the patterns AI assistants leave behind.
|
|
178
|
-
|
|
179
|
-
| Rule | Severity | What it catches |
|
|
180
|
-
|---|---|---|
|
|
181
|
-
| `ai-slop/trivial-comment` | warning | Comments restating the code (`// Import React`, `// Return the value`) |
|
|
182
|
-
| `ai-slop/swallowed-exception` | error | Empty catch blocks, catch blocks that only log (JS/TS/Python/Go/Ruby/Java) |
|
|
183
|
-
| `ai-slop/thin-wrapper` | warning | Functions that only delegate to another function |
|
|
184
|
-
| `ai-slop/generic-naming` | info | AI-generated names: `helper_1`, `data2`, `temp1` |
|
|
185
|
-
| `ai-slop/unused-import` | warning | Unused imports (JS/TS and Python) |
|
|
186
|
-
| `ai-slop/console-leftover` | warning | `console.log`/`debug`/`info` left in production code |
|
|
187
|
-
| `ai-slop/todo-stub` | info | Unresolved TODO/FIXME/HACK comments |
|
|
188
|
-
| `ai-slop/unreachable-code` | warning | Code after `return`/`throw` statements |
|
|
189
|
-
| `ai-slop/constant-condition` | warning | `if (true)`, `if (false)`, `if (0)` |
|
|
190
|
-
| `ai-slop/empty-function` | info | Empty function bodies |
|
|
191
|
-
| `ai-slop/unsafe-type-assertion` | warning | `as any` in TypeScript |
|
|
192
|
-
| `ai-slop/double-type-assertion` | warning | `as unknown as X` pattern |
|
|
193
|
-
| `ai-slop/ts-directive` | info | `@ts-ignore` / `@ts-expect-error` usage |
|
|
194
|
-
|
|
195
|
-
### Security
|
|
196
|
-
|
|
197
|
-
Finds secrets, risky constructs, and vulnerable dependencies.
|
|
198
|
-
|
|
199
|
-
| Rule | What it catches |
|
|
200
|
-
|---|---|
|
|
201
|
-
| `security/hardcoded-secret` | API keys, AWS credentials, JWT tokens, database URLs, passwords |
|
|
202
|
-
| `security/eval` | `eval()` usage (JS/TS/Python/Ruby/PHP) |
|
|
203
|
-
| `security/innerhtml` | `.innerHTML` and `dangerouslySetInnerHTML` |
|
|
204
|
-
| `security/sql-injection` | String concatenation in SQL queries |
|
|
205
|
-
| `security/shell-injection` | User input in command execution |
|
|
206
|
-
| `security/vulnerable-dependency` | npm/pip/cargo/go dependency audit |
|
|
67
|
+
### Fix issues automatically
|
|
207
68
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
| Rule type | Example |
|
|
213
|
-
|---|---|
|
|
214
|
-
| `forbid_import` | Ban `axios` project-wide |
|
|
215
|
-
| `forbid_import_from_path` | Controllers cannot import database modules |
|
|
216
|
-
| `require_pattern` | Require error handling in API routes |
|
|
69
|
+
```bash
|
|
70
|
+
aislop fix # auto-fix formatting + lint issues
|
|
71
|
+
```
|
|
217
72
|
|
|
218
|
-
|
|
73
|
+
### Use in CI pipelines
|
|
219
74
|
|
|
220
|
-
|
|
75
|
+
```bash
|
|
76
|
+
aislop ci # JSON output, exits 1 if score < threshold
|
|
77
|
+
```
|
|
221
78
|
|
|
222
|
-
|
|
223
|
-
|---|---|
|
|
224
|
-
| Error | 3.0 |
|
|
225
|
-
| Warning | 1.0 |
|
|
226
|
-
| Info | 0.25 |
|
|
79
|
+
### Other commands
|
|
227
80
|
|
|
228
|
-
|
|
81
|
+
```bash
|
|
82
|
+
aislop init # create .aislop/config.yml
|
|
83
|
+
aislop doctor # check which tools are available
|
|
84
|
+
aislop rules # list all built-in rules
|
|
85
|
+
aislop # interactive menu
|
|
86
|
+
```
|
|
229
87
|
|
|
230
|
-
|
|
231
|
-
|---|---|
|
|
232
|
-
| 75 -- 100 | Healthy |
|
|
233
|
-
| 50 -- 74 | Needs Work |
|
|
234
|
-
| 0 -- 49 | Critical |
|
|
88
|
+
See [all commands and flags](docs/commands.md).
|
|
235
89
|
|
|
236
90
|
---
|
|
237
91
|
|
|
238
|
-
##
|
|
92
|
+
## Use in your project
|
|
239
93
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
```yaml
|
|
243
|
-
version: 1
|
|
244
|
-
|
|
245
|
-
engines:
|
|
246
|
-
format: true
|
|
247
|
-
lint: true
|
|
248
|
-
code-quality: true
|
|
249
|
-
ai-slop: true
|
|
250
|
-
architecture: false # opt-in, needs rules.yml
|
|
251
|
-
security: true
|
|
252
|
-
|
|
253
|
-
quality:
|
|
254
|
-
maxFunctionLoc: 80
|
|
255
|
-
maxFileLoc: 400
|
|
256
|
-
maxNesting: 5
|
|
257
|
-
maxParams: 6
|
|
258
|
-
|
|
259
|
-
security:
|
|
260
|
-
audit: true
|
|
261
|
-
auditTimeout: 25000
|
|
262
|
-
|
|
263
|
-
scoring:
|
|
264
|
-
weights:
|
|
265
|
-
format: 0.5
|
|
266
|
-
lint: 1.0
|
|
267
|
-
code-quality: 1.5
|
|
268
|
-
ai-slop: 1.0
|
|
269
|
-
architecture: 1.0
|
|
270
|
-
security: 2.0
|
|
271
|
-
thresholds:
|
|
272
|
-
good: 75
|
|
273
|
-
ok: 50
|
|
94
|
+
### Pre-commit hook
|
|
274
95
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
format: json
|
|
96
|
+
```bash
|
|
97
|
+
npx aislop scan --staged
|
|
278
98
|
```
|
|
279
99
|
|
|
280
|
-
---
|
|
281
|
-
|
|
282
|
-
## CI / CD
|
|
283
|
-
|
|
284
100
|
### GitHub Actions
|
|
285
101
|
|
|
286
102
|
```yaml
|
|
287
|
-
- uses: actions/setup-node@
|
|
103
|
+
- uses: actions/setup-node@v6
|
|
288
104
|
with:
|
|
289
105
|
node-version: 20
|
|
290
|
-
|
|
291
106
|
- run: npx aislop ci
|
|
292
107
|
```
|
|
293
108
|
|
|
294
|
-
###
|
|
109
|
+
### Quality gate
|
|
295
110
|
|
|
296
|
-
Set
|
|
111
|
+
Set a minimum score in `.aislop/config.yml`:
|
|
297
112
|
|
|
298
113
|
```yaml
|
|
299
114
|
ci:
|
|
300
115
|
failBelow: 70
|
|
301
116
|
```
|
|
302
117
|
|
|
303
|
-
`aislop ci` exits with code 1 when the score
|
|
304
|
-
|
|
305
|
-
### Pre-commit hook
|
|
306
|
-
|
|
307
|
-
```bash
|
|
308
|
-
npx aislop scan --staged
|
|
309
|
-
```
|
|
118
|
+
`aislop ci` exits with code 1 when the score drops below the threshold. See [CI/CD docs](docs/ci.md) for more.
|
|
310
119
|
|
|
311
120
|
---
|
|
312
121
|
|
|
313
|
-
##
|
|
122
|
+
## Why aislop?
|
|
314
123
|
|
|
315
|
-
|
|
316
|
-
|---|---|---|---|---|---|
|
|
317
|
-
| TypeScript | Biome | oxlint | knip, complexity | All rules | All rules |
|
|
318
|
-
| JavaScript | Biome | oxlint | knip, complexity | All rules | All rules |
|
|
319
|
-
| Python | ruff | ruff | complexity | Imports, exceptions, comments | Secrets, audit |
|
|
320
|
-
| Go | gofmt | golangci-lint | complexity | Exceptions, comments | Secrets, audit |
|
|
321
|
-
| Rust | cargo fmt | clippy | complexity | Comments | Secrets, audit |
|
|
322
|
-
| Ruby | rubocop | rubocop | complexity | Exceptions, comments | Secrets |
|
|
323
|
-
| PHP | php-cs-fixer | -- | complexity | Comments | Secrets |
|
|
124
|
+
AI-generated code passes review because issues are spread across dozens of files. No single linter catches all of them. `aislop` does:
|
|
324
125
|
|
|
325
|
-
|
|
126
|
+
- **AI-specific pattern detection** — trivial comments, thin wrappers, generic names, swallowed exceptions, `as any` casts
|
|
127
|
+
- **Multi-language** — TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Expo/React Native
|
|
128
|
+
- **Single score** — one number to gate PRs, track in CI, and trend over time
|
|
129
|
+
- **Zero config** — run `npx aislop scan` and get results immediately
|
|
130
|
+
- **Framework-aware** — auto-detects Next.js, React, Expo, Vite, Remix, Django, Flask, FastAPI
|
|
131
|
+
- **Batteries included** — ships with oxlint, biome, knip; downloads ruff and golangci-lint on install
|
|
326
132
|
|
|
327
|
-
##
|
|
133
|
+
## What it catches
|
|
328
134
|
|
|
329
|
-
|
|
135
|
+
Six engines run in parallel: **Formatting**, **Linting**, **Code Quality**, **AI Slop Detection**, **Security**, and **Architecture** (opt-in).
|
|
330
136
|
|
|
331
|
-
|
|
137
|
+
| Engine | Examples |
|
|
138
|
+
|---|---|
|
|
139
|
+
| Formatting | Biome, ruff, gofmt, cargo fmt, rubocop, php-cs-fixer |
|
|
140
|
+
| Linting | oxlint, ruff, golangci-lint, clippy, expo-doctor |
|
|
141
|
+
| Code Quality | Function/file size limits, deep nesting, duplication, dead code, unused dependencies (knip) |
|
|
142
|
+
| AI Slop | Trivial comments, swallowed exceptions, unused imports, console leftovers, type assertion abuse, TODO stubs |
|
|
143
|
+
| Security | Hardcoded secrets, eval, innerHTML, SQL/shell injection, dependency audits |
|
|
144
|
+
| Architecture | Custom import bans, layering rules, required patterns |
|
|
332
145
|
|
|
333
|
-
|
|
146
|
+
See the full [rules reference](docs/rules.md) for all 30+ built-in rules.
|
|
334
147
|
|
|
335
|
-
|
|
336
|
-
# Environment variable (any of these)
|
|
337
|
-
AISLOP_NO_TELEMETRY=1 aislop scan
|
|
338
|
-
DO_NOT_TRACK=1 aislop scan
|
|
148
|
+
---
|
|
339
149
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
150
|
+
## Documentation
|
|
151
|
+
|
|
152
|
+
| Topic | Link |
|
|
153
|
+
|---|---|
|
|
154
|
+
| Installation | [docs/installation.md](docs/installation.md) |
|
|
155
|
+
| Commands & flags | [docs/commands.md](docs/commands.md) |
|
|
156
|
+
| Rules reference | [docs/rules.md](docs/rules.md) |
|
|
157
|
+
| Configuration | [docs/configuration.md](docs/configuration.md) |
|
|
158
|
+
| Scoring | [docs/scoring.md](docs/scoring.md) |
|
|
159
|
+
| CI / CD | [docs/ci.md](docs/ci.md) |
|
|
160
|
+
| Telemetry | [docs/telemetry.md](docs/telemetry.md) |
|
|
344
161
|
|
|
345
162
|
---
|
|
346
163
|
|
|
347
164
|
## Contributing
|
|
348
165
|
|
|
349
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup
|
|
166
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and how to add new rules. AI coding assistants can find project context in [AGENTS.md](AGENTS.md).
|
|
167
|
+
|
|
168
|
+
## Acknowledgments
|
|
169
|
+
|
|
170
|
+
`aislop` is built on top of excellent open-source projects:
|
|
171
|
+
|
|
172
|
+
- [Biome](https://biomejs.dev/) — formatting and linting for JS/TS
|
|
173
|
+
- [oxlint](https://oxc.rs/) — fast JavaScript/TypeScript linter
|
|
174
|
+
- [knip](https://knip.dev/) — unused files, exports, and dependencies
|
|
175
|
+
- [ruff](https://docs.astral.sh/ruff/) — Python linting and formatting
|
|
176
|
+
- [golangci-lint](https://golangci-lint.run/) — Go linting
|
|
177
|
+
- [expo-doctor](https://docs.expo.dev/) — Expo/React Native project health
|
|
350
178
|
|
|
351
|
-
##
|
|
179
|
+
## Contributors
|
|
352
180
|
|
|
353
|
-
|
|
181
|
+
[](https://github.com/heavykenny/aislop/graphs/contributors)
|
|
354
182
|
|
|
355
183
|
## License
|
|
356
184
|
|
|
357
|
-
[MIT](LICENSE)
|
|
185
|
+
[MIT](LICENSE)
|
package/dist/cli.js
CHANGED
|
@@ -1643,13 +1643,41 @@ const isToolInstalled = async (tool) => {
|
|
|
1643
1643
|
//#region src/engines/code-quality/knip.ts
|
|
1644
1644
|
const KNIP_MESSAGE_MAP = {
|
|
1645
1645
|
files: "Unused file",
|
|
1646
|
+
dependencies: "Unused dependency",
|
|
1647
|
+
devDependencies: "Unused devDependency",
|
|
1648
|
+
unlisted: "Unlisted dependency",
|
|
1649
|
+
unresolved: "Unresolved import",
|
|
1650
|
+
binaries: "Unlisted binary",
|
|
1646
1651
|
exports: "Unused export",
|
|
1647
1652
|
types: "Unused type",
|
|
1648
1653
|
duplicates: "Duplicate export"
|
|
1649
1654
|
};
|
|
1655
|
+
const DEPENDENCY_TYPES = [
|
|
1656
|
+
"dependencies",
|
|
1657
|
+
"devDependencies",
|
|
1658
|
+
"unlisted",
|
|
1659
|
+
"unresolved",
|
|
1660
|
+
"binaries"
|
|
1661
|
+
];
|
|
1662
|
+
const isDependencyType = (type) => DEPENDENCY_TYPES.includes(type);
|
|
1663
|
+
const getIssueItems = (fileIssue, issueType) => {
|
|
1664
|
+
const items = fileIssue[issueType];
|
|
1665
|
+
return Array.isArray(items) ? items : [];
|
|
1666
|
+
};
|
|
1667
|
+
const DEPENDENCY_HELP = {
|
|
1668
|
+
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
1669
|
+
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
1670
|
+
unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
|
|
1671
|
+
unresolved: "This import cannot be resolved. Check for typos or missing packages.",
|
|
1672
|
+
binaries: "This binary is used but its package is not in package.json."
|
|
1673
|
+
};
|
|
1650
1674
|
const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
|
|
1651
1675
|
const diagnostics = [];
|
|
1652
|
-
const issues =
|
|
1676
|
+
const issues = getIssueItems(fileIssue, issueType);
|
|
1677
|
+
const category = isDependencyType(issueType) ? "Dependencies" : "Dead Code";
|
|
1678
|
+
const severity = issueType === "unlisted" || issueType === "unresolved" ? "error" : "warning";
|
|
1679
|
+
const fixable = issueType === "dependencies" || issueType === "devDependencies";
|
|
1680
|
+
const help = DEPENDENCY_HELP[issueType] ?? "";
|
|
1653
1681
|
for (const issue of issues) {
|
|
1654
1682
|
const symbol = issue.name ?? issue.symbol ?? "unknown";
|
|
1655
1683
|
const absolutePath = path.resolve(knipCwd, fileIssue.file);
|
|
@@ -1657,13 +1685,13 @@ const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
|
|
|
1657
1685
|
filePath: path.relative(rootDir, absolutePath),
|
|
1658
1686
|
engine: "code-quality",
|
|
1659
1687
|
rule: `knip/${issueType}`,
|
|
1660
|
-
severity
|
|
1688
|
+
severity,
|
|
1661
1689
|
message: `${KNIP_MESSAGE_MAP[issueType]}: ${symbol}`,
|
|
1662
|
-
help
|
|
1690
|
+
help,
|
|
1663
1691
|
line: issue.line ?? 0,
|
|
1664
1692
|
column: issue.col ?? 0,
|
|
1665
|
-
category
|
|
1666
|
-
fixable
|
|
1693
|
+
category,
|
|
1694
|
+
fixable
|
|
1667
1695
|
});
|
|
1668
1696
|
}
|
|
1669
1697
|
return diagnostics;
|
|
@@ -1697,6 +1725,38 @@ const findKnipBin = (rootDirectory, monorepoRoot) => {
|
|
|
1697
1725
|
}
|
|
1698
1726
|
return null;
|
|
1699
1727
|
};
|
|
1728
|
+
const runKnipDependencyCheck = async (rootDirectory) => {
|
|
1729
|
+
return (await runKnip(rootDirectory)).filter((d) => d.rule === "knip/dependencies" || d.rule === "knip/devDependencies");
|
|
1730
|
+
};
|
|
1731
|
+
const fixUnusedDependencies = async (rootDirectory) => {
|
|
1732
|
+
const diagnostics = await runKnipDependencyCheck(rootDirectory);
|
|
1733
|
+
if (diagnostics.length === 0) return;
|
|
1734
|
+
const pkgPath = path.join(rootDirectory, "package.json");
|
|
1735
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
1736
|
+
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
1737
|
+
const pkg = JSON.parse(raw);
|
|
1738
|
+
const unusedDeps = /* @__PURE__ */ new Set();
|
|
1739
|
+
const unusedDevDeps = /* @__PURE__ */ new Set();
|
|
1740
|
+
for (const d of diagnostics) {
|
|
1741
|
+
const pkgName = d.message.replace(/^Unused (dev)?[Dd]ependency: /, "");
|
|
1742
|
+
if (d.rule === "knip/dependencies") unusedDeps.add(pkgName);
|
|
1743
|
+
if (d.rule === "knip/devDependencies") unusedDevDeps.add(pkgName);
|
|
1744
|
+
}
|
|
1745
|
+
let changed = false;
|
|
1746
|
+
if (pkg.dependencies) {
|
|
1747
|
+
for (const name of unusedDeps) if (name in pkg.dependencies) {
|
|
1748
|
+
delete pkg.dependencies[name];
|
|
1749
|
+
changed = true;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (pkg.devDependencies) {
|
|
1753
|
+
for (const name of unusedDevDeps) if (name in pkg.devDependencies) {
|
|
1754
|
+
delete pkg.devDependencies[name];
|
|
1755
|
+
changed = true;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (changed) fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
|
|
1759
|
+
};
|
|
1700
1760
|
const runKnip = async (rootDirectory) => {
|
|
1701
1761
|
const knipRuntime = findKnipBin(rootDirectory, findMonorepoRoot(rootDirectory));
|
|
1702
1762
|
if (!knipRuntime) return [];
|
|
@@ -1730,11 +1790,13 @@ const runKnip = async (rootDirectory) => {
|
|
|
1730
1790
|
fixable: false
|
|
1731
1791
|
});
|
|
1732
1792
|
const issues = parsed.issues ?? [];
|
|
1733
|
-
|
|
1793
|
+
const issueTypes = [
|
|
1794
|
+
...DEPENDENCY_TYPES,
|
|
1734
1795
|
"exports",
|
|
1735
1796
|
"types",
|
|
1736
1797
|
"duplicates"
|
|
1737
|
-
]
|
|
1798
|
+
];
|
|
1799
|
+
for (const fileIssue of issues) for (const type of issueTypes) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
|
|
1738
1800
|
return diagnostics;
|
|
1739
1801
|
} catch {
|
|
1740
1802
|
return [];
|
|
@@ -3118,7 +3180,7 @@ const logger = {
|
|
|
3118
3180
|
* Application version — injected at build time by tsdown from package.json.
|
|
3119
3181
|
* The fallback should always match the "version" field in package.json.
|
|
3120
3182
|
*/
|
|
3121
|
-
const APP_VERSION = "0.
|
|
3183
|
+
const APP_VERSION = "0.2.0";
|
|
3122
3184
|
|
|
3123
3185
|
//#endregion
|
|
3124
3186
|
//#region src/output/layout.ts
|
|
@@ -3692,9 +3754,13 @@ const getAnonymousId = () => {
|
|
|
3692
3754
|
for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
|
|
3693
3755
|
return `aislop_${(hash >>> 0).toString(36)}`;
|
|
3694
3756
|
};
|
|
3757
|
+
/** Pending telemetry request — kept alive so Node doesn't exit before it completes. */
|
|
3758
|
+
let pendingRequest = null;
|
|
3695
3759
|
/**
|
|
3696
3760
|
* Fire-and-forget telemetry event to PostHog.
|
|
3697
|
-
* Never throws, never blocks
|
|
3761
|
+
* Never throws, never blocks CLI output.
|
|
3762
|
+
* The request is kept alive via `flushTelemetry()` so Node doesn't
|
|
3763
|
+
* exit before it completes.
|
|
3698
3764
|
*/
|
|
3699
3765
|
const trackEvent = (event) => {
|
|
3700
3766
|
const payload = {
|
|
@@ -3717,12 +3783,23 @@ const trackEvent = (event) => {
|
|
|
3717
3783
|
},
|
|
3718
3784
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3719
3785
|
};
|
|
3720
|
-
fetch(`${POSTHOG_HOST}/capture/`, {
|
|
3786
|
+
pendingRequest = fetch(`${POSTHOG_HOST}/capture/`, {
|
|
3721
3787
|
method: "POST",
|
|
3722
3788
|
headers: { "Content-Type": "application/json" },
|
|
3723
3789
|
body: JSON.stringify(payload),
|
|
3724
3790
|
signal: AbortSignal.timeout(3e3)
|
|
3725
|
-
}).catch(() => {});
|
|
3791
|
+
}).then(() => {}).catch(() => {});
|
|
3792
|
+
};
|
|
3793
|
+
/**
|
|
3794
|
+
* Wait for any pending telemetry request to complete.
|
|
3795
|
+
* Call this before `process.exit()` to ensure the event is delivered.
|
|
3796
|
+
* Times out after 3 seconds so it never hangs the CLI.
|
|
3797
|
+
*/
|
|
3798
|
+
const flushTelemetry = async () => {
|
|
3799
|
+
if (pendingRequest) {
|
|
3800
|
+
await pendingRequest;
|
|
3801
|
+
pendingRequest = null;
|
|
3802
|
+
}
|
|
3726
3803
|
};
|
|
3727
3804
|
|
|
3728
3805
|
//#endregion
|
|
@@ -4069,6 +4146,9 @@ const fixCommand = async (directory, config, options = {
|
|
|
4069
4146
|
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
|
|
4070
4147
|
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
|
|
4071
4148
|
}
|
|
4149
|
+
if (config.engines["code-quality"]) {
|
|
4150
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
|
|
4151
|
+
}
|
|
4072
4152
|
if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
|
|
4073
4153
|
else {
|
|
4074
4154
|
logger.break();
|
|
@@ -4148,6 +4228,11 @@ const BUILTIN_RULES = [
|
|
|
4148
4228
|
engine: "code-quality",
|
|
4149
4229
|
rules: [
|
|
4150
4230
|
"knip/files",
|
|
4231
|
+
"knip/dependencies",
|
|
4232
|
+
"knip/devDependencies",
|
|
4233
|
+
"knip/unlisted",
|
|
4234
|
+
"knip/unresolved",
|
|
4235
|
+
"knip/binaries",
|
|
4151
4236
|
"knip/exports",
|
|
4152
4237
|
"knip/types",
|
|
4153
4238
|
"complexity/file-too-large",
|
|
@@ -4433,7 +4518,10 @@ const program = new Command().name("aislop").description("The unified code quali
|
|
|
4433
4518
|
verbose: Boolean(flags.verbose),
|
|
4434
4519
|
json: Boolean(flags.json)
|
|
4435
4520
|
});
|
|
4436
|
-
if (exitCode !== 0)
|
|
4521
|
+
if (exitCode !== 0) {
|
|
4522
|
+
await flushTelemetry();
|
|
4523
|
+
process.exit(exitCode);
|
|
4524
|
+
}
|
|
4437
4525
|
}).addHelpText("after", `
|
|
4438
4526
|
${highlighter.dim("Commands:")}
|
|
4439
4527
|
aislop scan [dir] Full code quality scan
|
|
@@ -4460,7 +4548,10 @@ program.command("scan [directory]").description("Run full code quality scan").op
|
|
|
4460
4548
|
verbose: Boolean(flags.verbose),
|
|
4461
4549
|
json: Boolean(flags.json)
|
|
4462
4550
|
});
|
|
4463
|
-
if (exitCode !== 0)
|
|
4551
|
+
if (exitCode !== 0) {
|
|
4552
|
+
await flushTelemetry();
|
|
4553
|
+
process.exit(exitCode);
|
|
4554
|
+
}
|
|
4464
4555
|
});
|
|
4465
4556
|
program.command("fix [directory]").description("Auto-fix formatting and lint issues").option("-d, --verbose", "show detailed fix progress").action(async (directory = ".", _flags, command) => {
|
|
4466
4557
|
const flags = command.optsWithGlobals();
|
|
@@ -4474,13 +4565,17 @@ program.command("doctor [directory]").description("Check installed tools and env
|
|
|
4474
4565
|
});
|
|
4475
4566
|
program.command("ci [directory]").description("CI-friendly JSON output with exit codes").action(async (directory = ".") => {
|
|
4476
4567
|
const { exitCode } = await ciCommand(directory, loadConfig(directory));
|
|
4477
|
-
if (exitCode !== 0)
|
|
4568
|
+
if (exitCode !== 0) {
|
|
4569
|
+
await flushTelemetry();
|
|
4570
|
+
process.exit(exitCode);
|
|
4571
|
+
}
|
|
4478
4572
|
});
|
|
4479
4573
|
program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
|
|
4480
4574
|
await rulesCommand(directory);
|
|
4481
4575
|
});
|
|
4482
4576
|
const main = async () => {
|
|
4483
4577
|
await program.parseAsync();
|
|
4578
|
+
await flushTelemetry();
|
|
4484
4579
|
};
|
|
4485
4580
|
main();
|
|
4486
4581
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Application version — injected at build time by tsdown from package.json.
|
|
4
4
|
* The fallback should always match the "version" field in package.json.
|
|
5
5
|
*/
|
|
6
|
-
const APP_VERSION = "0.
|
|
6
|
+
const APP_VERSION = "0.2.0";
|
|
7
7
|
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region src/output/engine-info.ts
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as getEngineLabel, r as APP_VERSION, t as ENGINE_INFO } from "./engine-info-
|
|
1
|
+
import { n as getEngineLabel, r as APP_VERSION, t as ENGINE_INFO } from "./engine-info-DpU0WTTj.js";
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-99puEEGl.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import fs from "node:fs";
|
|
@@ -719,6 +719,170 @@ const fixRuffFormat = async (rootDirectory) => {
|
|
|
719
719
|
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff format exited with code ${result.exitCode}`);
|
|
720
720
|
};
|
|
721
721
|
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/engines/code-quality/knip.ts
|
|
724
|
+
const KNIP_MESSAGE_MAP = {
|
|
725
|
+
files: "Unused file",
|
|
726
|
+
dependencies: "Unused dependency",
|
|
727
|
+
devDependencies: "Unused devDependency",
|
|
728
|
+
unlisted: "Unlisted dependency",
|
|
729
|
+
unresolved: "Unresolved import",
|
|
730
|
+
binaries: "Unlisted binary",
|
|
731
|
+
exports: "Unused export",
|
|
732
|
+
types: "Unused type",
|
|
733
|
+
duplicates: "Duplicate export"
|
|
734
|
+
};
|
|
735
|
+
const DEPENDENCY_TYPES = [
|
|
736
|
+
"dependencies",
|
|
737
|
+
"devDependencies",
|
|
738
|
+
"unlisted",
|
|
739
|
+
"unresolved",
|
|
740
|
+
"binaries"
|
|
741
|
+
];
|
|
742
|
+
const isDependencyType = (type) => DEPENDENCY_TYPES.includes(type);
|
|
743
|
+
const getIssueItems = (fileIssue, issueType) => {
|
|
744
|
+
const items = fileIssue[issueType];
|
|
745
|
+
return Array.isArray(items) ? items : [];
|
|
746
|
+
};
|
|
747
|
+
const DEPENDENCY_HELP = {
|
|
748
|
+
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
749
|
+
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
750
|
+
unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
|
|
751
|
+
unresolved: "This import cannot be resolved. Check for typos or missing packages.",
|
|
752
|
+
binaries: "This binary is used but its package is not in package.json."
|
|
753
|
+
};
|
|
754
|
+
const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
|
|
755
|
+
const diagnostics = [];
|
|
756
|
+
const issues = getIssueItems(fileIssue, issueType);
|
|
757
|
+
const category = isDependencyType(issueType) ? "Dependencies" : "Dead Code";
|
|
758
|
+
const severity = issueType === "unlisted" || issueType === "unresolved" ? "error" : "warning";
|
|
759
|
+
const fixable = issueType === "dependencies" || issueType === "devDependencies";
|
|
760
|
+
const help = DEPENDENCY_HELP[issueType] ?? "";
|
|
761
|
+
for (const issue of issues) {
|
|
762
|
+
const symbol = issue.name ?? issue.symbol ?? "unknown";
|
|
763
|
+
const absolutePath = path.resolve(knipCwd, fileIssue.file);
|
|
764
|
+
diagnostics.push({
|
|
765
|
+
filePath: path.relative(rootDir, absolutePath),
|
|
766
|
+
engine: "code-quality",
|
|
767
|
+
rule: `knip/${issueType}`,
|
|
768
|
+
severity,
|
|
769
|
+
message: `${KNIP_MESSAGE_MAP[issueType]}: ${symbol}`,
|
|
770
|
+
help,
|
|
771
|
+
line: issue.line ?? 0,
|
|
772
|
+
column: issue.col ?? 0,
|
|
773
|
+
category,
|
|
774
|
+
fixable
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
return diagnostics;
|
|
778
|
+
};
|
|
779
|
+
const findMonorepoRoot = (directory) => {
|
|
780
|
+
let current = path.dirname(directory);
|
|
781
|
+
while (current !== path.dirname(current)) {
|
|
782
|
+
if (fs.existsSync(path.join(current, "pnpm-workspace.yaml")) || (() => {
|
|
783
|
+
const pkgPath = path.join(current, "package.json");
|
|
784
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
785
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
786
|
+
return Array.isArray(pkg.workspaces) || pkg.workspaces?.packages;
|
|
787
|
+
})()) return current;
|
|
788
|
+
current = path.dirname(current);
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
};
|
|
792
|
+
const KNIP_RELATIVE_BIN = path.join("node_modules", "knip", "bin", "knip.js");
|
|
793
|
+
const findKnipBin = (rootDirectory, monorepoRoot) => {
|
|
794
|
+
const localPath = path.join(rootDirectory, KNIP_RELATIVE_BIN);
|
|
795
|
+
if (fs.existsSync(localPath)) return {
|
|
796
|
+
binPath: localPath,
|
|
797
|
+
cwd: rootDirectory
|
|
798
|
+
};
|
|
799
|
+
if (monorepoRoot) {
|
|
800
|
+
const monorepoPath = path.join(monorepoRoot, KNIP_RELATIVE_BIN);
|
|
801
|
+
if (fs.existsSync(monorepoPath)) return {
|
|
802
|
+
binPath: monorepoPath,
|
|
803
|
+
cwd: monorepoRoot
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
return null;
|
|
807
|
+
};
|
|
808
|
+
const runKnipDependencyCheck = async (rootDirectory) => {
|
|
809
|
+
return (await runKnip(rootDirectory)).filter((d) => d.rule === "knip/dependencies" || d.rule === "knip/devDependencies");
|
|
810
|
+
};
|
|
811
|
+
const fixUnusedDependencies = async (rootDirectory) => {
|
|
812
|
+
const diagnostics = await runKnipDependencyCheck(rootDirectory);
|
|
813
|
+
if (diagnostics.length === 0) return;
|
|
814
|
+
const pkgPath = path.join(rootDirectory, "package.json");
|
|
815
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
816
|
+
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
817
|
+
const pkg = JSON.parse(raw);
|
|
818
|
+
const unusedDeps = /* @__PURE__ */ new Set();
|
|
819
|
+
const unusedDevDeps = /* @__PURE__ */ new Set();
|
|
820
|
+
for (const d of diagnostics) {
|
|
821
|
+
const pkgName = d.message.replace(/^Unused (dev)?[Dd]ependency: /, "");
|
|
822
|
+
if (d.rule === "knip/dependencies") unusedDeps.add(pkgName);
|
|
823
|
+
if (d.rule === "knip/devDependencies") unusedDevDeps.add(pkgName);
|
|
824
|
+
}
|
|
825
|
+
let changed = false;
|
|
826
|
+
if (pkg.dependencies) {
|
|
827
|
+
for (const name of unusedDeps) if (name in pkg.dependencies) {
|
|
828
|
+
delete pkg.dependencies[name];
|
|
829
|
+
changed = true;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (pkg.devDependencies) {
|
|
833
|
+
for (const name of unusedDevDeps) if (name in pkg.devDependencies) {
|
|
834
|
+
delete pkg.devDependencies[name];
|
|
835
|
+
changed = true;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (changed) fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
|
|
839
|
+
};
|
|
840
|
+
const runKnip = async (rootDirectory) => {
|
|
841
|
+
const knipRuntime = findKnipBin(rootDirectory, findMonorepoRoot(rootDirectory));
|
|
842
|
+
if (!knipRuntime) return [];
|
|
843
|
+
try {
|
|
844
|
+
const args = [
|
|
845
|
+
knipRuntime.binPath,
|
|
846
|
+
"--no-progress",
|
|
847
|
+
"--reporter",
|
|
848
|
+
"json",
|
|
849
|
+
"--no-exit-code"
|
|
850
|
+
];
|
|
851
|
+
const result = await runSubprocess(process.execPath, args, {
|
|
852
|
+
cwd: knipRuntime.cwd,
|
|
853
|
+
timeout: 2e4,
|
|
854
|
+
env: { FORCE_COLOR: "0" }
|
|
855
|
+
});
|
|
856
|
+
if (!result.stdout) return [];
|
|
857
|
+
const parsed = JSON.parse(result.stdout);
|
|
858
|
+
const diagnostics = [];
|
|
859
|
+
const files = parsed.files ?? [];
|
|
860
|
+
for (const unusedFile of files) diagnostics.push({
|
|
861
|
+
filePath: path.relative(rootDirectory, path.resolve(knipRuntime.cwd, unusedFile)),
|
|
862
|
+
engine: "code-quality",
|
|
863
|
+
rule: "knip/files",
|
|
864
|
+
severity: "warning",
|
|
865
|
+
message: KNIP_MESSAGE_MAP.files,
|
|
866
|
+
help: "This file is not imported by any other file in the project.",
|
|
867
|
+
line: 0,
|
|
868
|
+
column: 0,
|
|
869
|
+
category: "Dead Code",
|
|
870
|
+
fixable: false
|
|
871
|
+
});
|
|
872
|
+
const issues = parsed.issues ?? [];
|
|
873
|
+
const issueTypes = [
|
|
874
|
+
...DEPENDENCY_TYPES,
|
|
875
|
+
"exports",
|
|
876
|
+
"types",
|
|
877
|
+
"duplicates"
|
|
878
|
+
];
|
|
879
|
+
for (const fileIssue of issues) for (const type of issueTypes) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
|
|
880
|
+
return diagnostics;
|
|
881
|
+
} catch {
|
|
882
|
+
return [];
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
|
|
722
886
|
//#endregion
|
|
723
887
|
//#region src/engines/lint/oxlint-config.ts
|
|
724
888
|
const createOxlintConfig = (options) => {
|
|
@@ -1078,9 +1242,13 @@ const getAnonymousId = () => {
|
|
|
1078
1242
|
for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
|
|
1079
1243
|
return `aislop_${(hash >>> 0).toString(36)}`;
|
|
1080
1244
|
};
|
|
1245
|
+
/** Pending telemetry request — kept alive so Node doesn't exit before it completes. */
|
|
1246
|
+
let pendingRequest = null;
|
|
1081
1247
|
/**
|
|
1082
1248
|
* Fire-and-forget telemetry event to PostHog.
|
|
1083
|
-
* Never throws, never blocks
|
|
1249
|
+
* Never throws, never blocks CLI output.
|
|
1250
|
+
* The request is kept alive via `flushTelemetry()` so Node doesn't
|
|
1251
|
+
* exit before it completes.
|
|
1084
1252
|
*/
|
|
1085
1253
|
const trackEvent = (event) => {
|
|
1086
1254
|
const payload = {
|
|
@@ -1103,12 +1271,12 @@ const trackEvent = (event) => {
|
|
|
1103
1271
|
},
|
|
1104
1272
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1105
1273
|
};
|
|
1106
|
-
fetch(`${POSTHOG_HOST}/capture/`, {
|
|
1274
|
+
pendingRequest = fetch(`${POSTHOG_HOST}/capture/`, {
|
|
1107
1275
|
method: "POST",
|
|
1108
1276
|
headers: { "Content-Type": "application/json" },
|
|
1109
1277
|
body: JSON.stringify(payload),
|
|
1110
1278
|
signal: AbortSignal.timeout(3e3)
|
|
1111
|
-
}).catch(() => {});
|
|
1279
|
+
}).then(() => {}).catch(() => {});
|
|
1112
1280
|
};
|
|
1113
1281
|
|
|
1114
1282
|
//#endregion
|
|
@@ -1220,6 +1388,9 @@ const fixCommand = async (directory, config, options = {
|
|
|
1220
1388
|
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
|
|
1221
1389
|
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
|
|
1222
1390
|
}
|
|
1391
|
+
if (config.engines["code-quality"]) {
|
|
1392
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
|
|
1393
|
+
}
|
|
1223
1394
|
if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
|
|
1224
1395
|
else {
|
|
1225
1396
|
logger.break();
|
|
@@ -2696,108 +2867,6 @@ const checkDuplication = async (context) => {
|
|
|
2696
2867
|
return diagnostics;
|
|
2697
2868
|
};
|
|
2698
2869
|
|
|
2699
|
-
//#endregion
|
|
2700
|
-
//#region src/engines/code-quality/knip.ts
|
|
2701
|
-
const KNIP_MESSAGE_MAP = {
|
|
2702
|
-
files: "Unused file",
|
|
2703
|
-
exports: "Unused export",
|
|
2704
|
-
types: "Unused type",
|
|
2705
|
-
duplicates: "Duplicate export"
|
|
2706
|
-
};
|
|
2707
|
-
const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
|
|
2708
|
-
const diagnostics = [];
|
|
2709
|
-
const issues = issueType === "exports" ? fileIssue.exports ?? [] : issueType === "types" ? fileIssue.types ?? [] : fileIssue.duplicates ?? [];
|
|
2710
|
-
for (const issue of issues) {
|
|
2711
|
-
const symbol = issue.name ?? issue.symbol ?? "unknown";
|
|
2712
|
-
const absolutePath = path.resolve(knipCwd, fileIssue.file);
|
|
2713
|
-
diagnostics.push({
|
|
2714
|
-
filePath: path.relative(rootDir, absolutePath),
|
|
2715
|
-
engine: "code-quality",
|
|
2716
|
-
rule: `knip/${issueType}`,
|
|
2717
|
-
severity: "warning",
|
|
2718
|
-
message: `${KNIP_MESSAGE_MAP[issueType]}: ${symbol}`,
|
|
2719
|
-
help: "",
|
|
2720
|
-
line: issue.line ?? 0,
|
|
2721
|
-
column: issue.col ?? 0,
|
|
2722
|
-
category: "Dead Code",
|
|
2723
|
-
fixable: false
|
|
2724
|
-
});
|
|
2725
|
-
}
|
|
2726
|
-
return diagnostics;
|
|
2727
|
-
};
|
|
2728
|
-
const findMonorepoRoot = (directory) => {
|
|
2729
|
-
let current = path.dirname(directory);
|
|
2730
|
-
while (current !== path.dirname(current)) {
|
|
2731
|
-
if (fs.existsSync(path.join(current, "pnpm-workspace.yaml")) || (() => {
|
|
2732
|
-
const pkgPath = path.join(current, "package.json");
|
|
2733
|
-
if (!fs.existsSync(pkgPath)) return false;
|
|
2734
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
2735
|
-
return Array.isArray(pkg.workspaces) || pkg.workspaces?.packages;
|
|
2736
|
-
})()) return current;
|
|
2737
|
-
current = path.dirname(current);
|
|
2738
|
-
}
|
|
2739
|
-
return null;
|
|
2740
|
-
};
|
|
2741
|
-
const KNIP_RELATIVE_BIN = path.join("node_modules", "knip", "bin", "knip.js");
|
|
2742
|
-
const findKnipBin = (rootDirectory, monorepoRoot) => {
|
|
2743
|
-
const localPath = path.join(rootDirectory, KNIP_RELATIVE_BIN);
|
|
2744
|
-
if (fs.existsSync(localPath)) return {
|
|
2745
|
-
binPath: localPath,
|
|
2746
|
-
cwd: rootDirectory
|
|
2747
|
-
};
|
|
2748
|
-
if (monorepoRoot) {
|
|
2749
|
-
const monorepoPath = path.join(monorepoRoot, KNIP_RELATIVE_BIN);
|
|
2750
|
-
if (fs.existsSync(monorepoPath)) return {
|
|
2751
|
-
binPath: monorepoPath,
|
|
2752
|
-
cwd: monorepoRoot
|
|
2753
|
-
};
|
|
2754
|
-
}
|
|
2755
|
-
return null;
|
|
2756
|
-
};
|
|
2757
|
-
const runKnip = async (rootDirectory) => {
|
|
2758
|
-
const knipRuntime = findKnipBin(rootDirectory, findMonorepoRoot(rootDirectory));
|
|
2759
|
-
if (!knipRuntime) return [];
|
|
2760
|
-
try {
|
|
2761
|
-
const args = [
|
|
2762
|
-
knipRuntime.binPath,
|
|
2763
|
-
"--no-progress",
|
|
2764
|
-
"--reporter",
|
|
2765
|
-
"json",
|
|
2766
|
-
"--no-exit-code"
|
|
2767
|
-
];
|
|
2768
|
-
const result = await runSubprocess(process.execPath, args, {
|
|
2769
|
-
cwd: knipRuntime.cwd,
|
|
2770
|
-
timeout: 2e4,
|
|
2771
|
-
env: { FORCE_COLOR: "0" }
|
|
2772
|
-
});
|
|
2773
|
-
if (!result.stdout) return [];
|
|
2774
|
-
const parsed = JSON.parse(result.stdout);
|
|
2775
|
-
const diagnostics = [];
|
|
2776
|
-
const files = parsed.files ?? [];
|
|
2777
|
-
for (const unusedFile of files) diagnostics.push({
|
|
2778
|
-
filePath: path.relative(rootDirectory, path.resolve(knipRuntime.cwd, unusedFile)),
|
|
2779
|
-
engine: "code-quality",
|
|
2780
|
-
rule: "knip/files",
|
|
2781
|
-
severity: "warning",
|
|
2782
|
-
message: KNIP_MESSAGE_MAP.files,
|
|
2783
|
-
help: "This file is not imported by any other file in the project.",
|
|
2784
|
-
line: 0,
|
|
2785
|
-
column: 0,
|
|
2786
|
-
category: "Dead Code",
|
|
2787
|
-
fixable: false
|
|
2788
|
-
});
|
|
2789
|
-
const issues = parsed.issues ?? [];
|
|
2790
|
-
for (const fileIssue of issues) for (const type of [
|
|
2791
|
-
"exports",
|
|
2792
|
-
"types",
|
|
2793
|
-
"duplicates"
|
|
2794
|
-
]) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
|
|
2795
|
-
return diagnostics;
|
|
2796
|
-
} catch {
|
|
2797
|
-
return [];
|
|
2798
|
-
}
|
|
2799
|
-
};
|
|
2800
|
-
|
|
2801
2870
|
//#endregion
|
|
2802
2871
|
//#region src/engines/code-quality/index.ts
|
|
2803
2872
|
const codeQualityEngine = {
|
|
@@ -4000,7 +4069,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
4000
4069
|
});
|
|
4001
4070
|
}
|
|
4002
4071
|
if (options.json) {
|
|
4003
|
-
const { buildJsonOutput } = await import("./json-
|
|
4072
|
+
const { buildJsonOutput } = await import("./json-UG8l_sLC.js");
|
|
4004
4073
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
4005
4074
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
4006
4075
|
return { exitCode };
|