noah-cursor 1.2.0 → 1.3.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 +7 -13
- package/dist/noah-registry/manifest.json +25 -25
- package/dist/noah-registry/presets/.gitkeep +0 -0
- package/dist/noah-registry/skills/larastan-fix/SKILL.md +203 -0
- package/dist/noah-registry/skills/nestjs-knip-fix/SKILL.md +147 -0
- package/dist/noah-registry/skills/nestjs-knip-fix/config/knip.nestjs.json +29 -0
- package/dist/noah-registry/skills/node-doctor-fix/SKILL.md +146 -0
- package/dist/noah-registry/skills/node-doctor-fix/config/node-doctor.knip.json +22 -0
- package/dist/noah-registry/skills/node-doctor-fix/scripts/node-doctor.mjs +393 -0
- package/dist/noah-registry/skills/react-doctor-fix/SKILL.md +185 -0
- package/package.json +1 -1
- package/dist/noah-registry/presets/noah-web-stack/preset.json +0 -3
- package/dist/noah-registry/presets/test/preset.json +0 -3
package/README.md
CHANGED
|
@@ -79,7 +79,6 @@ Examples:
|
|
|
79
79
|
# Install from Noah's official registry
|
|
80
80
|
npx noah-cursor add --skill test
|
|
81
81
|
npx noah-cursor add --rule test
|
|
82
|
-
npx noah-cursor add --preset test
|
|
83
82
|
npx noah-cursor add --all
|
|
84
83
|
npx noah-cursor add itsmenoahpoli/noah-cursor --skill commit-push
|
|
85
84
|
|
|
@@ -114,10 +113,14 @@ presets/
|
|
|
114
113
|
### Starter assets
|
|
115
114
|
|
|
116
115
|
- **skill** `test`
|
|
116
|
+
- **skill** `commit-push`
|
|
117
|
+
- **skill** `react-doctor-fix` — run react-doctor, fix findings, verify no regressions
|
|
118
|
+
- **skill** `larastan-fix` — run Larastan on Laravel, fix findings, verify no regressions
|
|
119
|
+
- **skill** `nestjs-knip-fix` — run Knip on NestJS, fix unused code/deps, verify no regressions
|
|
120
|
+
- **skill** `node-doctor-fix` — bundled node-doctor (Knip + package hygiene) for generic Node
|
|
117
121
|
- **rule** `test`
|
|
118
|
-
- **preset** `test` (installs both)
|
|
119
122
|
|
|
120
|
-
Add curated Noah assets under those folders and register them in `manifest.json`.
|
|
123
|
+
Add curated Noah assets under those folders and register them in `manifest.json`. Presets are supported by the CLI but none are published in this registry yet.
|
|
121
124
|
|
|
122
125
|
### manifest.json
|
|
123
126
|
|
|
@@ -143,16 +146,7 @@ Add curated Noah assets under those folders and register them in `manifest.json`
|
|
|
143
146
|
],
|
|
144
147
|
"prompts": [],
|
|
145
148
|
"mcp": [],
|
|
146
|
-
"presets": [
|
|
147
|
-
{
|
|
148
|
-
"id": "test",
|
|
149
|
-
"version": "1.0.0",
|
|
150
|
-
"includes": {
|
|
151
|
-
"skills": ["test"],
|
|
152
|
-
"rules": ["test"]
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
]
|
|
149
|
+
"presets": []
|
|
156
150
|
}
|
|
157
151
|
```
|
|
158
152
|
|
|
@@ -15,6 +15,30 @@
|
|
|
15
15
|
"version": "1.0.0",
|
|
16
16
|
"description": "Stage all changes, group by category, draft a linepush-style commit, commit, and push",
|
|
17
17
|
"tags": ["git", "commit", "push", "conventional-commits"]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "react-doctor-fix",
|
|
21
|
+
"version": "1.0.0",
|
|
22
|
+
"description": "Run react-doctor, fix all findings, and verify features still work",
|
|
23
|
+
"tags": ["react", "react-doctor", "lint", "refactor", "qa"]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "larastan-fix",
|
|
27
|
+
"version": "1.0.0",
|
|
28
|
+
"description": "Run Larastan on Laravel, fix all findings, and verify features still work",
|
|
29
|
+
"tags": ["laravel", "php", "larastan", "phpstan", "qa"]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "nestjs-knip-fix",
|
|
33
|
+
"version": "1.0.0",
|
|
34
|
+
"description": "Run Knip on NestJS, fix unused code/deps, and verify features still work",
|
|
35
|
+
"tags": ["nestjs", "knip", "node", "typescript", "qa"]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "node-doctor-fix",
|
|
39
|
+
"version": "1.0.0",
|
|
40
|
+
"description": "Run bundled node-doctor (Knip + hygiene), fix findings, verify features still work",
|
|
41
|
+
"tags": ["node", "knip", "doctor", "qa", "cli"]
|
|
18
42
|
}
|
|
19
43
|
],
|
|
20
44
|
"rules": [
|
|
@@ -57,29 +81,5 @@
|
|
|
57
81
|
],
|
|
58
82
|
"prompts": [],
|
|
59
83
|
"mcp": [],
|
|
60
|
-
"presets": [
|
|
61
|
-
{
|
|
62
|
-
"id": "test",
|
|
63
|
-
"version": "1.0.0",
|
|
64
|
-
"description": "Installs the sample test skill and rule",
|
|
65
|
-
"includes": {
|
|
66
|
-
"skills": ["test"],
|
|
67
|
-
"rules": ["test"]
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"id": "noah-web-stack",
|
|
72
|
-
"version": "1.0.0",
|
|
73
|
-
"description": "Full Noah web stack rules: architecture, React SPA, Laravel API, Next.js and Nuxt marketing",
|
|
74
|
-
"includes": {
|
|
75
|
-
"rules": [
|
|
76
|
-
"stack-architecture",
|
|
77
|
-
"react-spa-dashboard",
|
|
78
|
-
"laravel-api",
|
|
79
|
-
"nextjs-marketing",
|
|
80
|
-
"nuxt-marketing"
|
|
81
|
-
]
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
]
|
|
84
|
+
"presets": []
|
|
85
85
|
}
|
|
File without changes
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: larastan-fix
|
|
3
|
+
description: >-
|
|
4
|
+
Runs Larastan (PHPStan for Laravel), constructs fixes for all findings, and
|
|
5
|
+
verifies that implementations, features, and functionalities are not broken
|
|
6
|
+
after changes. Use when the user runs /larastan-fix, asks to run Larastan,
|
|
7
|
+
PHPStan on a Laravel app, clean up static analysis errors, or fix Larastan
|
|
8
|
+
findings without regressions.
|
|
9
|
+
disable-model-invocation: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /larastan-fix
|
|
13
|
+
|
|
14
|
+
Run **Larastan** (PHPStan + Laravel extension), fix **all** findings, and prove behavior still works.
|
|
15
|
+
|
|
16
|
+
Do **not** stop after the first file. Do **not** mass-ignore or regenerate a baseline to “pass.” Prefer root-cause fixes. After every batch of edits, re-verify with project checks so implementations, features, and functionalities are not broken.
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
Copy this checklist and track progress:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Larastan Fix Progress:
|
|
24
|
+
- [ ] 1. Confirm Laravel project + Larastan setup + verification commands
|
|
25
|
+
- [ ] 2. Baseline analyse
|
|
26
|
+
- [ ] 3. Triage findings (priority order)
|
|
27
|
+
- [ ] 4. Fix in batches (explain → edit → verify)
|
|
28
|
+
- [ ] 5. Re-analyse until clean (or document intentional leftovers)
|
|
29
|
+
- [ ] 6. Final verification + report
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 1. Confirm Laravel project + Larastan setup + verification commands
|
|
33
|
+
|
|
34
|
+
From the project root:
|
|
35
|
+
|
|
36
|
+
1. Confirm this is a Laravel app (`artisan`, `composer.json` with `laravel/framework`) — abort with a clear message if not.
|
|
37
|
+
2. Confirm Larastan is available:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
composer show larastan/larastan 2>/dev/null || composer show nunomaduro/larastan 2>/dev/null
|
|
41
|
+
test -f phpstan.neon -o -f phpstan.neon.dist -o -f phpstan.dist.neon
|
|
42
|
+
test -x vendor/bin/phpstan
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If Larastan / PHPStan is missing, install only with user approval:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
composer require --dev larastan/larastan
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Ensure config includes the Larastan extension (create `phpstan.neon` only if none exists and the user wants setup):
|
|
52
|
+
|
|
53
|
+
```neon
|
|
54
|
+
includes:
|
|
55
|
+
- vendor/larastan/larastan/extension.neon
|
|
56
|
+
- vendor/nesbot/carbon/extension.neon
|
|
57
|
+
|
|
58
|
+
parameters:
|
|
59
|
+
paths:
|
|
60
|
+
- app/
|
|
61
|
+
level: 5
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Do **not** raise the configured level unless the user asks. Analyse at the project’s configured level.
|
|
65
|
+
|
|
66
|
+
3. Detect verification commands. Prefer existing Composer / Artisan scripts:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
composer test # or phpunit / pest
|
|
70
|
+
php artisan test
|
|
71
|
+
vendor/bin/pest
|
|
72
|
+
vendor/bin/phpunit
|
|
73
|
+
vendor/bin/pint --test # style check only; not a substitute for tests
|
|
74
|
+
php artisan route:list # smoke when helpful
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Record the chosen commands before editing. If no tests exist, say so and still use the strongest available checks (at least a second Larastan run + Pint if present).
|
|
78
|
+
|
|
79
|
+
### 2. Baseline analyse
|
|
80
|
+
|
|
81
|
+
Use the project binary (respects `phpstan.neon*`):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
./vendor/bin/phpstan analyse --memory-limit=2G
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If Composer defines a script (`composer phpstan` / `composer analyse`), prefer that.
|
|
88
|
+
|
|
89
|
+
For machine-readable output when practical:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
./vendor/bin/phpstan analyse --memory-limit=2G --error-format=json > .larastan-baseline.json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Delete or leave untracked any temporary report files before finishing.
|
|
96
|
+
|
|
97
|
+
Path-scoped cleanup only when the user asks:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
./vendor/bin/phpstan analyse app/Http/Controllers --memory-limit=2G
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Default for this skill is a **full** configured analyse unless the user scopes it.
|
|
104
|
+
|
|
105
|
+
### 3. Triage findings
|
|
106
|
+
|
|
107
|
+
Work findings in this priority:
|
|
108
|
+
|
|
109
|
+
1. **Runtime-breaking** — undefined methods/properties, wrong argument counts, impossible types that would crash
|
|
110
|
+
2. **Null safety** — unsafe property/method access on nullable models, requests, relations
|
|
111
|
+
3. **Laravel / Eloquent correctness** — wrong relation types, builder misuse, container/`auth`/`request` type gaps Larastan flags
|
|
112
|
+
4. **PHPDoc / generics** — collection/model generics, `@var` / `@param` / `@return` that hide real bugs
|
|
113
|
+
5. **Dead code / unreachable** — always-true/false conditions, unused private methods (only remove when safe)
|
|
114
|
+
6. **Remaining level noise** — stricter level complaints that need small, local typing improvements
|
|
115
|
+
|
|
116
|
+
Group by file or by shared root cause (e.g. one incomplete Form Request type used everywhere).
|
|
117
|
+
|
|
118
|
+
### 4. Fix in batches (explain → edit → verify)
|
|
119
|
+
|
|
120
|
+
For each batch (prefer one error identifier / one file group):
|
|
121
|
+
|
|
122
|
+
1. **Explain the fix** briefly: what PHPStan reported, why the change is safe, what runtime behavior must stay the same.
|
|
123
|
+
2. **Edit** the minimum code needed. Match Laravel project patterns (Form Requests, Actions, Eloquent APIs, API Resources).
|
|
124
|
+
3. **Verify immediately** with the commands from step 1 (fastest relevant suite after each batch; full suite before the final report).
|
|
125
|
+
4. If verification fails: **revert or repair** before continuing. Never leave the tree broken to chase a clean Larastan run.
|
|
126
|
+
|
|
127
|
+
#### Fix construction rules
|
|
128
|
+
|
|
129
|
+
- Preserve HTTP contracts: routes, status codes, JSON shapes, validation rules, auth/gates, jobs/events side effects.
|
|
130
|
+
- Prefer real types and narrowing (`=== null` checks, early returns, assert helpers) over silencing.
|
|
131
|
+
- Prefer accurate PHPDoc / generics on Eloquent models and collections when the code is already correct.
|
|
132
|
+
- Do not change business logic to satisfy the analyser when a precise type assertion or request DTO is enough.
|
|
133
|
+
- Do not upgrade Laravel/PHP major versions or rewrite architecture unless the user expands scope.
|
|
134
|
+
- Keep diffs small and reviewable.
|
|
135
|
+
|
|
136
|
+
#### Ignores / baseline (last resort only)
|
|
137
|
+
|
|
138
|
+
Prefer fixing the root cause. Use ignores only when:
|
|
139
|
+
|
|
140
|
+
- the finding is a true false positive for this Laravel version/package, **or**
|
|
141
|
+
- a correct fix would be an out-of-scope rewrite the user did not approve
|
|
142
|
+
|
|
143
|
+
Narrow inline ignore:
|
|
144
|
+
|
|
145
|
+
```php
|
|
146
|
+
// @phpstan-ignore-next-line
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Or identifier-based ignores when the project PHPStan version supports them. Prefer next-line / specific `ignoreErrors` path patterns over global rules.
|
|
150
|
+
|
|
151
|
+
Do **not** run `--generate-baseline` to clear findings unless the user explicitly asks for a baseline workflow.
|
|
152
|
+
|
|
153
|
+
Document every ignore in the final report with file, message/identifier, and reason.
|
|
154
|
+
|
|
155
|
+
### 5. Re-analyse until clean
|
|
156
|
+
|
|
157
|
+
After fixes:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
./vendor/bin/phpstan analyse --memory-limit=2G
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Repeat triage → fix → verify until:
|
|
164
|
+
|
|
165
|
+
- analyse exits 0 with no errors, **or**
|
|
166
|
+
- only explicitly approved / documented ignores or deferred items remain
|
|
167
|
+
|
|
168
|
+
Do not claim “all clear” if analyse still fails.
|
|
169
|
+
|
|
170
|
+
### 6. Final verification + report
|
|
171
|
+
|
|
172
|
+
Run the full verification set from step 1 again.
|
|
173
|
+
|
|
174
|
+
Report in this shape:
|
|
175
|
+
|
|
176
|
+
```markdown
|
|
177
|
+
## Larastan fix report
|
|
178
|
+
|
|
179
|
+
### Analyse
|
|
180
|
+
- Config: <phpstan.neon*|level>
|
|
181
|
+
- Baseline: <N errors>
|
|
182
|
+
- Final: <clean | N remaining>
|
|
183
|
+
|
|
184
|
+
### Fixed
|
|
185
|
+
- <error summary> — file(s) — one-line fix summary
|
|
186
|
+
|
|
187
|
+
### Ignores
|
|
188
|
+
- none | file:line — reason
|
|
189
|
+
|
|
190
|
+
### Verification
|
|
191
|
+
- <command> — pass/fail
|
|
192
|
+
- <command> — pass/fail
|
|
193
|
+
|
|
194
|
+
### Deferred (needs user decision)
|
|
195
|
+
- <finding> — why not fixed
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Safety
|
|
199
|
+
|
|
200
|
+
- Do not commit or push unless the user asks.
|
|
201
|
+
- Do not use `--force` git commands.
|
|
202
|
+
- Do not commit secrets (`.env`, credentials).
|
|
203
|
+
- Stop and ask if a “fix” requires deleting substantial features, changing API contracts, or broad behavioral changes.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-knip-fix
|
|
3
|
+
description: >-
|
|
4
|
+
Runs Knip on NestJS projects, constructs fixes for unused files, exports, and
|
|
5
|
+
dependencies, and verifies that implementations, features, and functionalities
|
|
6
|
+
are not broken after changes. Use when the user runs /nestjs-knip-fix, asks
|
|
7
|
+
to run knip on NestJS, clean up unused NestJS code, or fix Knip findings
|
|
8
|
+
without regressions.
|
|
9
|
+
disable-model-invocation: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /nestjs-knip-fix
|
|
13
|
+
|
|
14
|
+
Run **`npx knip`** on a **NestJS** app, fix **all** findings, and prove behavior still works.
|
|
15
|
+
|
|
16
|
+
Do **not** stop after the first issue type. Do **not** mass-ignore. Prefer root-cause removals and export cleanups. After every batch of edits, re-verify so implementations, features, and functionalities are not broken.
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
Copy this checklist and track progress:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
NestJS Knip Fix Progress:
|
|
24
|
+
- [ ] 1. Confirm NestJS project + verification commands
|
|
25
|
+
- [ ] 2. Ensure Knip config (Nest-aware)
|
|
26
|
+
- [ ] 3. Baseline knip scan
|
|
27
|
+
- [ ] 4. Triage findings (priority order)
|
|
28
|
+
- [ ] 5. Fix in batches (explain → edit → verify)
|
|
29
|
+
- [ ] 6. Re-scan until clean (or document intentional leftovers)
|
|
30
|
+
- [ ] 7. Final verification + report
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 1. Confirm NestJS project + verification commands
|
|
34
|
+
|
|
35
|
+
From the project root:
|
|
36
|
+
|
|
37
|
+
1. Confirm NestJS (`@nestjs/core` in `package.json`, usually `nest-cli.json` / `src/main.ts`) — abort if not.
|
|
38
|
+
2. Detect verification commands (prefer existing scripts):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm test # or pnpm / yarn / bun
|
|
42
|
+
npm run test:e2e
|
|
43
|
+
npm run build # nest build / tsc
|
|
44
|
+
npm run lint
|
|
45
|
+
npx tsc --noEmit
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Record the chosen commands before editing.
|
|
49
|
+
|
|
50
|
+
### 2. Ensure Knip config (Nest-aware)
|
|
51
|
+
|
|
52
|
+
If the project has no `knip.json` / `knip.ts` / `knip` field in `package.json`, copy the skill template into the project root as `knip.json`:
|
|
53
|
+
|
|
54
|
+
- Template: [`config/knip.nestjs.json`](config/knip.nestjs.json)
|
|
55
|
+
|
|
56
|
+
Adjust `entry` / `project` for monorepos (`apps/*`, `libs/*`) when needed. Prefer extending the template over inventing a blank config.
|
|
57
|
+
|
|
58
|
+
Do not overwrite an existing Knip config unless the user asks — merge Nest entry patterns into the existing config instead.
|
|
59
|
+
|
|
60
|
+
### 3. Baseline knip scan
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx knip --no-progress
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If a local config path is required:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx knip --config knip.json --no-progress
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Optional production-only pass (after the default scan is clean or when the user asks):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx knip --production --no-progress
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Capture output for triage. Default for this skill is a **full** workspace scan unless the user scopes it.
|
|
79
|
+
|
|
80
|
+
### 4. Triage findings
|
|
81
|
+
|
|
82
|
+
Work findings in this priority:
|
|
83
|
+
|
|
84
|
+
1. **Unused dependencies / devDependencies** — remove from `package.json` only after confirming Nest/CLI/plugins do not need them
|
|
85
|
+
2. **Unlisted dependencies** — add the real import to `dependencies` / `devDependencies` (do not rely on transitive deps)
|
|
86
|
+
3. **Unused files** — delete only when not dynamically loaded (`i18n`, `assets`, `path.join(__dirname, …)`, Nest schematics paths)
|
|
87
|
+
4. **Unused exports** — remove `export` or delete dead symbols; keep Nest decorators’ public APIs (`@Module` providers/controllers/imports that Knip misunderstands → fix config, don’t gut the module)
|
|
88
|
+
5. **Duplicate / enum / namespace export noise** — resolve carefully; suppress only as last resort
|
|
89
|
+
|
|
90
|
+
**NestJS cautions**
|
|
91
|
+
|
|
92
|
+
- Controllers, providers, guards, interceptors, pipes, filters registered only via module metadata must stay reachable from entry modules — if Knip says unused, verify `*.module.ts` wiring first.
|
|
93
|
+
- Do not remove `main.ts`, `app.module.ts`, or Nest CLI schematics entrypoints.
|
|
94
|
+
- Spec / e2e files: prefer config `ignore` / project patterns over deleting tests.
|
|
95
|
+
|
|
96
|
+
### 5. Fix in batches (explain → edit → verify)
|
|
97
|
+
|
|
98
|
+
For each batch:
|
|
99
|
+
|
|
100
|
+
1. **Explain** what Knip reported and why the change is safe.
|
|
101
|
+
2. **Edit** the minimum (package.json, exports, delete files).
|
|
102
|
+
3. **Verify** with build + unit tests (and e2e when relevant).
|
|
103
|
+
4. If verification fails: **revert or repair** before continuing.
|
|
104
|
+
|
|
105
|
+
#### Fix construction rules
|
|
106
|
+
|
|
107
|
+
- Preserve HTTP routes, DTO contracts, swagger models, queues, cron jobs, and microservice patterns.
|
|
108
|
+
- Prefer removing unused exports over deleting whole Nest providers that are still registered.
|
|
109
|
+
- Run `npm install` / lockfile update after dependency changes.
|
|
110
|
+
- Keep diffs small. No drive-by refactors.
|
|
111
|
+
|
|
112
|
+
#### Ignores (last resort)
|
|
113
|
+
|
|
114
|
+
Use Knip `ignore`, `ignoreDependencies`, or `ignoreBinaries` only for true false positives (Nest dynamic modules, intentional public SDK barrels). Document each ignore in the final report.
|
|
115
|
+
|
|
116
|
+
### 6. Re-scan until clean
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npx knip --no-progress
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Repeat until exit 0 or only approved ignores remain.
|
|
123
|
+
|
|
124
|
+
### 7. Final verification + report
|
|
125
|
+
|
|
126
|
+
```markdown
|
|
127
|
+
## NestJS Knip fix report
|
|
128
|
+
|
|
129
|
+
### Scan
|
|
130
|
+
- Baseline: <summary>
|
|
131
|
+
- Final: <clean | remaining>
|
|
132
|
+
|
|
133
|
+
### Fixed
|
|
134
|
+
- <issue type> — <what changed>
|
|
135
|
+
|
|
136
|
+
### Ignores / config
|
|
137
|
+
- none | <item> — reason
|
|
138
|
+
|
|
139
|
+
### Verification
|
|
140
|
+
- <command> — pass/fail
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Safety
|
|
144
|
+
|
|
145
|
+
- Do not commit or push unless the user asks.
|
|
146
|
+
- Do not delete `.env` or secrets.
|
|
147
|
+
- Stop and ask before removing large feature modules or public API packages.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/knip@5/schema.json",
|
|
3
|
+
"entry": [
|
|
4
|
+
"src/main.ts",
|
|
5
|
+
"src/main.js",
|
|
6
|
+
"apps/**/src/main.ts",
|
|
7
|
+
"src/**/*.module.ts",
|
|
8
|
+
"apps/**/*.module.ts",
|
|
9
|
+
"libs/**/src/index.ts",
|
|
10
|
+
"test/**/*.ts",
|
|
11
|
+
"test/**/*.js"
|
|
12
|
+
],
|
|
13
|
+
"project": [
|
|
14
|
+
"src/**/*.{ts,js}",
|
|
15
|
+
"apps/**/*.{ts,js}",
|
|
16
|
+
"libs/**/*.{ts,js}",
|
|
17
|
+
"test/**/*.{ts,js}"
|
|
18
|
+
],
|
|
19
|
+
"ignore": [
|
|
20
|
+
"**/*.spec.ts",
|
|
21
|
+
"**/*.e2e-spec.ts",
|
|
22
|
+
"dist/**",
|
|
23
|
+
"coverage/**"
|
|
24
|
+
],
|
|
25
|
+
"ignoreDependencies": [
|
|
26
|
+
"@nestjs/schematics",
|
|
27
|
+
"@nestjs/testing"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: node-doctor-fix
|
|
3
|
+
description: >-
|
|
4
|
+
Runs the bundled node-doctor tool (Knip + package hygiene) on generic Node.js
|
|
5
|
+
projects, constructs fixes for all findings, and verifies that implementations,
|
|
6
|
+
features, and functionalities are not broken after changes. Use when the user
|
|
7
|
+
runs /node-doctor-fix, asks to doctor a Node.js package/CLI, clean unused
|
|
8
|
+
Node exports/deps, or fix node-doctor findings without regressions.
|
|
9
|
+
disable-model-invocation: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /node-doctor-fix
|
|
13
|
+
|
|
14
|
+
Run the bundled **`node-doctor`** tool on a **generic Node.js** project (library, CLI, or service — not Nest/Next-first apps), fix **all** findings, and prove behavior still works.
|
|
15
|
+
|
|
16
|
+
`node-doctor` = **Knip** (unused files / exports / dependencies) using [`config/node-doctor.knip.json`](config/node-doctor.knip.json) when the project has no Knip config, **plus** package hygiene checks (`bin` / `main` / `exports` / script paths / `engines.node`).
|
|
17
|
+
|
|
18
|
+
Do **not** mass-ignore. Prefer root-cause fixes. After every batch of edits, re-verify so implementations, features, and functionalities are not broken.
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
|
|
22
|
+
Copy this checklist and track progress:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Node Doctor Fix Progress:
|
|
26
|
+
- [ ] 1. Confirm Node project + route frameworks elsewhere
|
|
27
|
+
- [ ] 2. Baseline node-doctor scan
|
|
28
|
+
- [ ] 3. Triage findings (priority order)
|
|
29
|
+
- [ ] 4. Fix in batches (explain → edit → verify)
|
|
30
|
+
- [ ] 5. Re-scan until clean (or document intentional leftovers)
|
|
31
|
+
- [ ] 6. Final verification + report
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 1. Confirm Node project + route frameworks elsewhere
|
|
35
|
+
|
|
36
|
+
From the project root:
|
|
37
|
+
|
|
38
|
+
1. Confirm `package.json` exists.
|
|
39
|
+
2. If the app is primarily **NestJS** → use `/nestjs-knip-fix` instead.
|
|
40
|
+
3. If the app is primarily **React** → use `/react-doctor-fix` (optionally Knip later).
|
|
41
|
+
4. Detect verification commands:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm test
|
|
45
|
+
npm run typecheck # or npx tsc --noEmit
|
|
46
|
+
npm run lint
|
|
47
|
+
npm run build
|
|
48
|
+
node ./dist/index.js --help # CLIs when applicable
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Baseline node-doctor scan
|
|
52
|
+
|
|
53
|
+
Resolve the skill directory (installed under `.cursor/skills/node-doctor-fix` or this registry path), then:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
node <skillDir>/scripts/node-doctor.mjs . --json > .node-doctor-baseline.json
|
|
57
|
+
node <skillDir>/scripts/node-doctor.mjs .
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
node .cursor/skills/node-doctor-fix/scripts/node-doctor.mjs .
|
|
64
|
+
node skills/node-doctor-fix/scripts/node-doctor.mjs .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Flags:
|
|
68
|
+
|
|
69
|
+
- `--json` — machine-readable findings
|
|
70
|
+
- `--knip-only` — skip package hygiene
|
|
71
|
+
- `--hygiene-only` — skip Knip
|
|
72
|
+
|
|
73
|
+
If the project already has `knip.json` / `package.json#knip`, node-doctor uses that for Knip; otherwise it uses the bundled [`config/node-doctor.knip.json`](config/node-doctor.knip.json).
|
|
74
|
+
|
|
75
|
+
Delete temporary report files before finishing (or keep them untracked).
|
|
76
|
+
|
|
77
|
+
### 3. Triage findings
|
|
78
|
+
|
|
79
|
+
Priority:
|
|
80
|
+
|
|
81
|
+
1. **Broken package entrypoints** — missing `bin` / `main` / `exports` / script paths (hygiene errors)
|
|
82
|
+
2. **Unlisted dependencies** — add real imports to package.json
|
|
83
|
+
3. **Unused dependencies** — remove only when not required by tooling/config plugins
|
|
84
|
+
4. **Unused files** — delete when not dynamically required
|
|
85
|
+
5. **Unused exports / types** — remove or stop exporting
|
|
86
|
+
6. **Hygiene warnings** — e.g. missing `engines.node`
|
|
87
|
+
|
|
88
|
+
### 4. Fix in batches (explain → edit → verify)
|
|
89
|
+
|
|
90
|
+
For each batch:
|
|
91
|
+
|
|
92
|
+
1. **Explain** the finding and why the fix is safe.
|
|
93
|
+
2. **Edit** minimally (`package.json`, exports, delete dead files).
|
|
94
|
+
3. **Verify** with test / typecheck / build / CLI smoke.
|
|
95
|
+
4. On failure: **revert or repair** before continuing.
|
|
96
|
+
|
|
97
|
+
#### Fix construction rules
|
|
98
|
+
|
|
99
|
+
- Preserve public package API (`exports`, documented CLI flags, published bin names).
|
|
100
|
+
- Do not remove files loaded dynamically (`fs.readFile`, runtime plugin paths) without proof.
|
|
101
|
+
- Update the lockfile after dependency changes.
|
|
102
|
+
- Do not convert the project into Nest/Next-specific structure.
|
|
103
|
+
|
|
104
|
+
#### Ignores (last resort)
|
|
105
|
+
|
|
106
|
+
Prefer fixing. If Knip false-positives remain, add narrow `ignore` / `ignoreDependencies` to the **project** Knip config (create one from the bundled config rather than silencing the tool). Document every ignore.
|
|
107
|
+
|
|
108
|
+
### 5. Re-scan until clean
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
node <skillDir>/scripts/node-doctor.mjs .
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Exit code must be `0` (or only approved documented leftovers remain).
|
|
115
|
+
|
|
116
|
+
### 6. Final verification + report
|
|
117
|
+
|
|
118
|
+
```markdown
|
|
119
|
+
## Node doctor fix report
|
|
120
|
+
|
|
121
|
+
### Scan
|
|
122
|
+
- Baseline: <N findings>
|
|
123
|
+
- Final: <clean | N remaining>
|
|
124
|
+
|
|
125
|
+
### Fixed
|
|
126
|
+
- <type> — summary
|
|
127
|
+
|
|
128
|
+
### Ignores / config
|
|
129
|
+
- none | item — reason
|
|
130
|
+
|
|
131
|
+
### Verification
|
|
132
|
+
- <command> — pass/fail
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Tool reference
|
|
136
|
+
|
|
137
|
+
| Path | Role |
|
|
138
|
+
|------|------|
|
|
139
|
+
| `scripts/node-doctor.mjs` | CLI analyser |
|
|
140
|
+
| `config/node-doctor.knip.json` | Default Knip config for generic Node |
|
|
141
|
+
|
|
142
|
+
## Safety
|
|
143
|
+
|
|
144
|
+
- Do not commit or push unless the user asks.
|
|
145
|
+
- Do not commit secrets.
|
|
146
|
+
- Stop and ask before deleting public API surface or major packages.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/knip@5/schema.json",
|
|
3
|
+
"entry": [
|
|
4
|
+
"{index,cli,main,app,server}.{js,cjs,mjs,ts,cts,mts}",
|
|
5
|
+
"src/{index,cli,main,app,server}.{js,cjs,mjs,ts,cts,mts}",
|
|
6
|
+
"bin/**/*.{js,cjs,mjs,ts}",
|
|
7
|
+
"scripts/**/*.{js,cjs,mjs,ts}"
|
|
8
|
+
],
|
|
9
|
+
"project": [
|
|
10
|
+
"**/*.{js,cjs,mjs,ts,cts,mts}!"
|
|
11
|
+
],
|
|
12
|
+
"ignore": [
|
|
13
|
+
"**/dist/**",
|
|
14
|
+
"**/build/**",
|
|
15
|
+
"**/coverage/**",
|
|
16
|
+
"**/.next/**",
|
|
17
|
+
"**/node_modules/**"
|
|
18
|
+
],
|
|
19
|
+
"ignoreBinaries": [
|
|
20
|
+
"noah-cursor"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* node-doctor — analyse a generic Node.js project.
|
|
4
|
+
*
|
|
5
|
+
* Combines:
|
|
6
|
+
* 1) Knip (unused files / exports / dependencies) using the bundled config
|
|
7
|
+
* 2) Package hygiene checks (bin/main/exports/scripts paths, engines)
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/node-doctor.mjs [projectDir] [--json] [--knip-only] [--hygiene-only]
|
|
11
|
+
*
|
|
12
|
+
* Exit codes: 0 clean, 1 findings, 2 usage/setup error
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const DEFAULT_KNIP_CONFIG = path.resolve(
|
|
21
|
+
__dirname,
|
|
22
|
+
"../config/node-doctor.knip.json",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
const opts = {
|
|
27
|
+
dir: process.cwd(),
|
|
28
|
+
json: false,
|
|
29
|
+
knipOnly: false,
|
|
30
|
+
hygieneOnly: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
for (const arg of argv) {
|
|
34
|
+
if (arg === "--json") opts.json = true;
|
|
35
|
+
else if (arg === "--knip-only") opts.knipOnly = true;
|
|
36
|
+
else if (arg === "--hygiene-only") opts.hygieneOnly = true;
|
|
37
|
+
else if (arg === "--help" || arg === "-h") opts.help = true;
|
|
38
|
+
else if (arg.startsWith("-")) {
|
|
39
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
40
|
+
} else opts.dir = path.resolve(arg);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return opts;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJson(filePath) {
|
|
47
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function exists(filePath) {
|
|
51
|
+
try {
|
|
52
|
+
fs.accessSync(filePath);
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isNodeProject(dir) {
|
|
60
|
+
return exists(path.join(dir, "package.json"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function looksLikeFrameworkProject(pkg) {
|
|
64
|
+
const all = {
|
|
65
|
+
...pkg.dependencies,
|
|
66
|
+
...pkg.devDependencies,
|
|
67
|
+
...pkg.peerDependencies,
|
|
68
|
+
};
|
|
69
|
+
const keys = Object.keys(all ?? {});
|
|
70
|
+
return {
|
|
71
|
+
nest: keys.some((k) => k.startsWith("@nestjs/")),
|
|
72
|
+
next: keys.includes("next"),
|
|
73
|
+
react: keys.includes("react") && !keys.includes("next"),
|
|
74
|
+
laravelHint: false,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Resolve package.json "exports" / "bin" / "main" targets to filesystem paths. */
|
|
79
|
+
function collectPackagePaths(pkg, dir) {
|
|
80
|
+
const targets = [];
|
|
81
|
+
|
|
82
|
+
const add = (rel, kind, label) => {
|
|
83
|
+
if (!rel || typeof rel !== "string") return;
|
|
84
|
+
if (rel.startsWith("http:") || rel.startsWith("https:") || rel.startsWith("node:")) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const cleaned = rel.replace(/^\.\//, "");
|
|
88
|
+
targets.push({ kind, label, rel: cleaned, abs: path.join(dir, cleaned) });
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (pkg.main) add(pkg.main, "main", "main");
|
|
92
|
+
if (pkg.module) add(pkg.module, "module", "module");
|
|
93
|
+
if (pkg.types) add(pkg.types, "types", "types");
|
|
94
|
+
if (pkg.typings) add(pkg.typings, "types", "typings");
|
|
95
|
+
|
|
96
|
+
if (typeof pkg.bin === "string") add(pkg.bin, "bin", "bin");
|
|
97
|
+
else if (pkg.bin && typeof pkg.bin === "object") {
|
|
98
|
+
for (const [name, rel] of Object.entries(pkg.bin)) {
|
|
99
|
+
add(rel, "bin", `bin.${name}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const walkExports = (value, label) => {
|
|
104
|
+
if (!value) return;
|
|
105
|
+
if (typeof value === "string") add(value, "exports", label);
|
|
106
|
+
else if (typeof value === "object") {
|
|
107
|
+
for (const [k, v] of Object.entries(value)) {
|
|
108
|
+
walkExports(v, `${label}.${k}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
walkExports(pkg.exports, "exports");
|
|
113
|
+
|
|
114
|
+
return targets;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function scriptLocalRefs(script) {
|
|
118
|
+
if (typeof script !== "string") return [];
|
|
119
|
+
const refs = [];
|
|
120
|
+
const patterns = [
|
|
121
|
+
/\bnode\s+([./][^\s]+)/g,
|
|
122
|
+
/\bts-node\s+([./][^\s]+)/g,
|
|
123
|
+
/\btsx\s+([./][^\s]+)/g,
|
|
124
|
+
/\bsh\s+([./][^\s]+)/g,
|
|
125
|
+
/\bbash\s+([./][^\s]+)/g,
|
|
126
|
+
];
|
|
127
|
+
for (const re of patterns) {
|
|
128
|
+
let m;
|
|
129
|
+
while ((m = re.exec(script)) !== null) {
|
|
130
|
+
refs.push(m[1]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return refs;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function runHygiene(dir, pkg) {
|
|
137
|
+
const findings = [];
|
|
138
|
+
|
|
139
|
+
if (!pkg.name) {
|
|
140
|
+
findings.push({
|
|
141
|
+
type: "package-hygiene",
|
|
142
|
+
severity: "error",
|
|
143
|
+
message: 'package.json is missing "name"',
|
|
144
|
+
file: "package.json",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (!pkg.version) {
|
|
148
|
+
findings.push({
|
|
149
|
+
type: "package-hygiene",
|
|
150
|
+
severity: "error",
|
|
151
|
+
message: 'package.json is missing "version"',
|
|
152
|
+
file: "package.json",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!pkg.engines?.node) {
|
|
157
|
+
findings.push({
|
|
158
|
+
type: "package-hygiene",
|
|
159
|
+
severity: "warning",
|
|
160
|
+
message: 'package.json is missing "engines.node" (recommended for Node libraries/CLIs)',
|
|
161
|
+
file: "package.json",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const target of collectPackagePaths(pkg, dir)) {
|
|
166
|
+
if (!exists(target.abs)) {
|
|
167
|
+
findings.push({
|
|
168
|
+
type: "package-hygiene",
|
|
169
|
+
severity: "error",
|
|
170
|
+
message: `${target.label} points to missing file: ${target.rel}`,
|
|
171
|
+
file: "package.json",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (pkg.scripts && typeof pkg.scripts === "object") {
|
|
177
|
+
for (const [name, script] of Object.entries(pkg.scripts)) {
|
|
178
|
+
for (const ref of scriptLocalRefs(script)) {
|
|
179
|
+
const abs = path.resolve(dir, ref);
|
|
180
|
+
if (!exists(abs)) {
|
|
181
|
+
findings.push({
|
|
182
|
+
type: "package-hygiene",
|
|
183
|
+
severity: "error",
|
|
184
|
+
message: `scripts.${name} references missing file: ${ref}`,
|
|
185
|
+
file: "package.json",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return findings;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function runKnip(dir) {
|
|
196
|
+
if (!exists(DEFAULT_KNIP_CONFIG)) {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
findings: [
|
|
200
|
+
{
|
|
201
|
+
type: "setup",
|
|
202
|
+
severity: "error",
|
|
203
|
+
message: `Bundled Knip config missing: ${DEFAULT_KNIP_CONFIG}`,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
raw: "",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Prefer project knip config when present; otherwise use bundled Node defaults.
|
|
211
|
+
const projectConfigs = ["knip.json", "knip.jsonc", "knip.ts", "knip.config.ts"];
|
|
212
|
+
const hasProjectConfig = projectConfigs.some((f) => exists(path.join(dir, f))) ||
|
|
213
|
+
(() => {
|
|
214
|
+
try {
|
|
215
|
+
const pkg = readJson(path.join(dir, "package.json"));
|
|
216
|
+
return Boolean(pkg.knip);
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
})();
|
|
221
|
+
|
|
222
|
+
const args = ["knip", "--no-progress", "--reporter", "json"];
|
|
223
|
+
if (!hasProjectConfig) {
|
|
224
|
+
args.push("--config", DEFAULT_KNIP_CONFIG);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const result = spawnSync("npx", args, {
|
|
228
|
+
cwd: dir,
|
|
229
|
+
encoding: "utf8",
|
|
230
|
+
shell: process.platform === "win32",
|
|
231
|
+
env: process.env,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
235
|
+
const findings = [];
|
|
236
|
+
|
|
237
|
+
// Knip JSON reporter may print a JSON object or array; also tolerate text fallback.
|
|
238
|
+
try {
|
|
239
|
+
const parsed = JSON.parse(result.stdout || "null");
|
|
240
|
+
if (parsed && typeof parsed === "object") {
|
|
241
|
+
const pushIssue = (type, items) => {
|
|
242
|
+
if (!Array.isArray(items)) return;
|
|
243
|
+
for (const item of items) {
|
|
244
|
+
const file = item.file ?? item.name ?? item.path ?? undefined;
|
|
245
|
+
const name = item.name ?? item.symbol ?? item.identifier;
|
|
246
|
+
findings.push({
|
|
247
|
+
type,
|
|
248
|
+
severity: "error",
|
|
249
|
+
message: name ? `${type}: ${name}` : String(type),
|
|
250
|
+
file,
|
|
251
|
+
raw: item,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Shape varies by knip version — handle common keys.
|
|
257
|
+
pushIssue("unused-files", parsed.files);
|
|
258
|
+
pushIssue("unused-dependencies", parsed.dependencies);
|
|
259
|
+
pushIssue("unused-devDependencies", parsed.devDependencies);
|
|
260
|
+
pushIssue("unlisted-dependencies", parsed.unlisted);
|
|
261
|
+
pushIssue("unused-exports", parsed.exports);
|
|
262
|
+
pushIssue("unused-types", parsed.types);
|
|
263
|
+
pushIssue("unused-duplicates", parsed.duplicates);
|
|
264
|
+
pushIssue("unused-enumMembers", parsed.enumMembers);
|
|
265
|
+
pushIssue("unused-namespaceMembers", parsed.nsExports ?? parsed.nsTypes);
|
|
266
|
+
pushIssue("unused-binaries", parsed.binaries);
|
|
267
|
+
|
|
268
|
+
if (Array.isArray(parsed.issues)) {
|
|
269
|
+
for (const issue of parsed.issues) {
|
|
270
|
+
findings.push({
|
|
271
|
+
type: issue.type ?? "knip",
|
|
272
|
+
severity: "error",
|
|
273
|
+
message: issue.message ?? JSON.stringify(issue),
|
|
274
|
+
file: issue.file,
|
|
275
|
+
raw: issue,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
if (result.status && result.status !== 0 && raw) {
|
|
282
|
+
findings.push({
|
|
283
|
+
type: "knip",
|
|
284
|
+
severity: "error",
|
|
285
|
+
message: "Knip reported issues (see raw output)",
|
|
286
|
+
raw,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// If JSON parse yielded nothing but knip failed, surface stdout/stderr.
|
|
292
|
+
if (findings.length === 0 && result.status && result.status !== 0) {
|
|
293
|
+
findings.push({
|
|
294
|
+
type: "knip",
|
|
295
|
+
severity: "error",
|
|
296
|
+
message: raw || `knip exited with code ${result.status}`,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { ok: result.status === 0 && findings.length === 0, findings, raw, status: result.status };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function printHuman(findings, knipRaw) {
|
|
304
|
+
if (findings.length === 0) {
|
|
305
|
+
console.log("node-doctor: clean — no findings");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log(`node-doctor: ${findings.length} finding(s)\n`);
|
|
310
|
+
const byType = new Map();
|
|
311
|
+
for (const f of findings) {
|
|
312
|
+
const list = byType.get(f.type) ?? [];
|
|
313
|
+
list.push(f);
|
|
314
|
+
byType.set(f.type, list);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
for (const [type, list] of byType) {
|
|
318
|
+
console.log(`${type} (${list.length})`);
|
|
319
|
+
for (const f of list) {
|
|
320
|
+
const loc = f.file ? ` ${f.file}` : "";
|
|
321
|
+
console.log(` - [${f.severity}]${loc} ${f.message}`);
|
|
322
|
+
}
|
|
323
|
+
console.log();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (knipRaw && findings.some((f) => f.type === "knip" && f.raw === knipRaw)) {
|
|
327
|
+
console.log("knip raw output:\n" + knipRaw);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function main() {
|
|
332
|
+
let opts;
|
|
333
|
+
try {
|
|
334
|
+
opts = parseArgs(process.argv.slice(2));
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error(String(error instanceof Error ? error.message : error));
|
|
337
|
+
process.exit(2);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (opts.help) {
|
|
341
|
+
console.log(`Usage: node-doctor.mjs [projectDir] [--json] [--knip-only] [--hygiene-only]`);
|
|
342
|
+
process.exit(0);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const dir = opts.dir;
|
|
346
|
+
if (!isNodeProject(dir)) {
|
|
347
|
+
console.error(`Not a Node.js project (package.json missing): ${dir}`);
|
|
348
|
+
process.exit(2);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const pkg = readJson(path.join(dir, "package.json"));
|
|
352
|
+
const frameworks = looksLikeFrameworkProject(pkg);
|
|
353
|
+
if (frameworks.nest) {
|
|
354
|
+
console.error(
|
|
355
|
+
"This looks like a NestJS project. Use /nestjs-knip-fix (npx knip) instead of node-doctor.",
|
|
356
|
+
);
|
|
357
|
+
process.exit(2);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const findings = [];
|
|
361
|
+
let knipRaw = "";
|
|
362
|
+
|
|
363
|
+
if (!opts.hygieneOnly) {
|
|
364
|
+
const knip = runKnip(dir);
|
|
365
|
+
knipRaw = knip.raw;
|
|
366
|
+
findings.push(...knip.findings);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!opts.knipOnly) {
|
|
370
|
+
findings.push(...runHygiene(dir, pkg));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (opts.json) {
|
|
374
|
+
console.log(
|
|
375
|
+
JSON.stringify(
|
|
376
|
+
{
|
|
377
|
+
tool: "node-doctor",
|
|
378
|
+
cwd: dir,
|
|
379
|
+
findingCount: findings.length,
|
|
380
|
+
findings,
|
|
381
|
+
},
|
|
382
|
+
null,
|
|
383
|
+
2,
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
} else {
|
|
387
|
+
printHuman(findings, knipRaw);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
process.exit(findings.length === 0 ? 0 : 1);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
main();
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-doctor-fix
|
|
3
|
+
description: >-
|
|
4
|
+
Runs react-doctor, constructs fixes for all findings, and verifies that
|
|
5
|
+
implementations, features, and functionalities are not broken after changes.
|
|
6
|
+
Use when the user runs /react-doctor-fix, asks to run react-doctor, clean up
|
|
7
|
+
React Doctor diagnostics, or fix React Doctor findings without regressions.
|
|
8
|
+
disable-model-invocation: true
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# /react-doctor-fix
|
|
12
|
+
|
|
13
|
+
Run **React Doctor**, fix **all** findings, and prove behavior still works.
|
|
14
|
+
|
|
15
|
+
Do **not** stop after the first category. Do **not** mass-suppress. Prefer root-cause fixes. After every batch of edits, re-verify with project checks so implementations, features, and functionalities are not broken.
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
Copy this checklist and track progress:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
React Doctor Fix Progress:
|
|
23
|
+
- [ ] 1. Confirm React project + available verification commands
|
|
24
|
+
- [ ] 2. Baseline scan (verbose + JSON)
|
|
25
|
+
- [ ] 3. Triage findings (priority order)
|
|
26
|
+
- [ ] 4. Fix in batches (explain → edit → verify)
|
|
27
|
+
- [ ] 5. Re-scan until clean (or document intentional leftovers)
|
|
28
|
+
- [ ] 6. Final verification + report
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 1. Confirm React project + verification commands
|
|
32
|
+
|
|
33
|
+
From the project root:
|
|
34
|
+
|
|
35
|
+
1. Confirm this is a React / Next.js / Vite / Remix / Expo (or similar) app — abort with a clear message if not.
|
|
36
|
+
2. Detect how to verify nothing broke. Prefer existing scripts from `package.json` in this order when present:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm test # or pnpm / yarn / bun equivalent
|
|
40
|
+
npm run typecheck # or tsc --noEmit
|
|
41
|
+
npm run lint
|
|
42
|
+
npm run build
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Record the chosen commands before editing. If none exist, use the strongest available subset (at least typecheck or build) and say what you could not run.
|
|
46
|
+
|
|
47
|
+
Optional monorepo:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx react-doctor@latest -y --verbose --project <name>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Baseline scan
|
|
54
|
+
|
|
55
|
+
Always non-interactive for agents:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx react-doctor@latest -y --verbose --no-score
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Also capture machine-readable output when practical:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx react-doctor@latest -y --verbose --no-score --json > .react-doctor-baseline.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If the JSON file would be committed accidentally, delete it before finishing (or keep it untracked). Prefer a temp path when easy.
|
|
68
|
+
|
|
69
|
+
For branch-scoped cleanup only (when the user asks for changed files / PR noise only):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx react-doctor@latest -y --verbose --no-score --scope changed
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Default for this skill is a **full** scan unless the user scopes it.
|
|
76
|
+
|
|
77
|
+
### 3. Triage findings
|
|
78
|
+
|
|
79
|
+
Work findings in this priority:
|
|
80
|
+
|
|
81
|
+
1. **Security**
|
|
82
|
+
2. **Correctness** / broken behavior risks
|
|
83
|
+
3. **State & effects** (derived state in effects, effect loops, missing deps that matter)
|
|
84
|
+
4. **Performance**
|
|
85
|
+
5. **Accessibility**
|
|
86
|
+
6. **Architecture** / dead code / bundle / remaining warnings
|
|
87
|
+
|
|
88
|
+
Within a priority, fix `error` before `warning`.
|
|
89
|
+
|
|
90
|
+
For an unclear rule:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx react-doctor@latest rules explain <rule>
|
|
94
|
+
npx react-doctor@latest why path/to/file.tsx:123
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Optional per-rule guidance from React Doctor prompts (when helpful):
|
|
98
|
+
|
|
99
|
+
`https://www.react.doctor/prompts/rules/react-doctor/<rule>.md`
|
|
100
|
+
|
|
101
|
+
Example: `https://www.react.doctor/prompts/rules/react-doctor/no-derived-state.md`
|
|
102
|
+
|
|
103
|
+
### 4. Fix in batches (explain → edit → verify)
|
|
104
|
+
|
|
105
|
+
For each batch (prefer one rule or one tightly related file group):
|
|
106
|
+
|
|
107
|
+
1. **Explain the fix** briefly: what is wrong, why the change is safe, what behavior must stay the same.
|
|
108
|
+
2. **Edit** the minimum code needed. Match project patterns. Do not drive-by refactors.
|
|
109
|
+
3. **Verify immediately** with the commands from step 1 (at least the fastest relevant ones after each batch; full suite before the final report).
|
|
110
|
+
4. If verification fails: **revert or repair** the fix before continuing. Never leave the tree broken to chase a cleaner doctor score.
|
|
111
|
+
|
|
112
|
+
#### Fix construction rules
|
|
113
|
+
|
|
114
|
+
- Preserve public behavior, routes, props contracts, visible UI, and user flows.
|
|
115
|
+
- Prefer derived values during render over syncing state in `useEffect`.
|
|
116
|
+
- Prefer event handlers / key handlers over effect-driven “do X when Y changes” when Y is a user action.
|
|
117
|
+
- Do not remove “unused” exports/files that are part of a public package API, generated barrels, or framework entrypoints without confirming.
|
|
118
|
+
- Do not change dependency versions unless a finding explicitly requires it and the user wants that scope.
|
|
119
|
+
- Keep diffs small and reviewable.
|
|
120
|
+
|
|
121
|
+
#### Suppressions (last resort only)
|
|
122
|
+
|
|
123
|
+
Prefer fixing the root cause. Suppress only when:
|
|
124
|
+
|
|
125
|
+
- the finding is a true false positive for this codebase, **or**
|
|
126
|
+
- a correct fix would be an out-of-scope rewrite the user did not approve
|
|
127
|
+
|
|
128
|
+
Keep suppressions narrow:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
// react-doctor-disable-next-line react-doctor/no-derived-state
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Never disable whole categories (`rules category … off`) unless the user explicitly asks.
|
|
135
|
+
|
|
136
|
+
Document every suppression in the final report with file, rule, and reason.
|
|
137
|
+
|
|
138
|
+
### 5. Re-scan until clean
|
|
139
|
+
|
|
140
|
+
After fixes:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx react-doctor@latest -y --verbose --no-score
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Repeat triage → fix → verify until:
|
|
147
|
+
|
|
148
|
+
- the scan reports no remaining findings, **or**
|
|
149
|
+
- only explicitly approved / documented suppressions or deferred items remain
|
|
150
|
+
|
|
151
|
+
Do not claim “all clear” if the scan still fails.
|
|
152
|
+
|
|
153
|
+
### 6. Final verification + report
|
|
154
|
+
|
|
155
|
+
Run the full verification set from step 1 again.
|
|
156
|
+
|
|
157
|
+
Report in this shape:
|
|
158
|
+
|
|
159
|
+
```markdown
|
|
160
|
+
## React Doctor fix report
|
|
161
|
+
|
|
162
|
+
### Scan
|
|
163
|
+
- Baseline: <N findings / score if shown>
|
|
164
|
+
- Final: <clean | N remaining>
|
|
165
|
+
|
|
166
|
+
### Fixed
|
|
167
|
+
- `rule-id` — file(s) — one-line fix summary
|
|
168
|
+
|
|
169
|
+
### Suppressions
|
|
170
|
+
- none | `rule-id` @ file:line — reason
|
|
171
|
+
|
|
172
|
+
### Verification
|
|
173
|
+
- <command> — pass/fail
|
|
174
|
+
- <command> — pass/fail
|
|
175
|
+
|
|
176
|
+
### Deferred (needs user decision)
|
|
177
|
+
- <finding> — why not fixed
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Safety
|
|
181
|
+
|
|
182
|
+
- Do not commit or push unless the user asks.
|
|
183
|
+
- Do not use `--force` git commands.
|
|
184
|
+
- Do not run `react-doctor` network-sharing flows that the user declined; `--no-score` avoids score/share API usage.
|
|
185
|
+
- Stop and ask if a “fix” requires deleting substantial features, changing product behavior, or broad API breakage.
|
package/package.json
CHANGED