nestjs-doctor 0.2.0 → 0.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 +34 -0
- package/dist/cli/index.mjs +46 -19
- package/package.json +3 -2
- package/skill/SKILL.md +119 -0
package/README.md
CHANGED
|
@@ -96,11 +96,45 @@ Usage: nestjs-doctor [directory] [options]
|
|
|
96
96
|
--json JSON output (for tooling)
|
|
97
97
|
--min-score <n> Minimum passing score (0-100). Exits with code 1 if below threshold
|
|
98
98
|
--config <p> Path to config file
|
|
99
|
+
--init Set up the /nestjs-doctor Claude Code skill
|
|
99
100
|
-h, --help Show help
|
|
100
101
|
```
|
|
101
102
|
|
|
102
103
|
---
|
|
103
104
|
|
|
105
|
+
## Claude Code
|
|
106
|
+
|
|
107
|
+
nestjs-doctor ships with a [Claude Code skill](https://docs.anthropic.com/en/docs/claude-code/skills) that scans your codebase and fixes issues interactively.
|
|
108
|
+
|
|
109
|
+
### Setup
|
|
110
|
+
|
|
111
|
+
If you haven't already, install as a devDependency:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pnpm add -D nestjs-doctor
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then scaffold the skill:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx nestjs-doctor --init
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This creates `.claude/skills/nestjs-doctor/SKILL.md` in your project. Commit it so every contributor gets the skill automatically.
|
|
124
|
+
|
|
125
|
+
### Usage
|
|
126
|
+
|
|
127
|
+
In Claude Code, type:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
/nestjs-doctor
|
|
131
|
+
/nestjs-doctor src/
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Claude will scan your codebase, present a prioritized health report, and offer to fix every issue found.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
104
138
|
## Configuration
|
|
105
139
|
|
|
106
140
|
Optional. Create `nestjs-doctor.config.json` in your project root:
|
package/dist/cli/index.mjs
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { defineCommand, runMain } from "citty";
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
6
|
import { performance } from "node:perf_hooks";
|
|
7
7
|
import { Project, SyntaxKind } from "ts-morph";
|
|
8
8
|
import { glob } from "tinyglobby";
|
|
9
9
|
import picomatch from "picomatch";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
10
11
|
import pc from "picocolors";
|
|
11
12
|
import ora from "ora";
|
|
12
13
|
|
|
@@ -2526,27 +2527,14 @@ const flags = {
|
|
|
2526
2527
|
config: {
|
|
2527
2528
|
type: "string",
|
|
2528
2529
|
description: "Config file path"
|
|
2530
|
+
},
|
|
2531
|
+
init: {
|
|
2532
|
+
type: "boolean",
|
|
2533
|
+
description: "Set up the /nestjs-doctor Claude Code skill in your project",
|
|
2534
|
+
default: false
|
|
2529
2535
|
}
|
|
2530
2536
|
};
|
|
2531
2537
|
|
|
2532
|
-
//#endregion
|
|
2533
|
-
//#region src/cli/min-score.ts
|
|
2534
|
-
const validateMinScoreArg = (raw) => {
|
|
2535
|
-
if (raw.trim() === "") return `Invalid --min-score value: "${raw}". Must be an integer between 0 and 100.`;
|
|
2536
|
-
const num = Number(raw);
|
|
2537
|
-
if (!Number.isInteger(num)) return `Invalid --min-score value: "${raw}". Must be an integer between 0 and 100.`;
|
|
2538
|
-
if (num < 0 || num > 100) return `Invalid --min-score value: "${raw}". Must be an integer between 0 and 100.`;
|
|
2539
|
-
return null;
|
|
2540
|
-
};
|
|
2541
|
-
const resolveMinScore = (cliValue, configValue) => {
|
|
2542
|
-
if (cliValue !== void 0) return Number(cliValue);
|
|
2543
|
-
return configValue;
|
|
2544
|
-
};
|
|
2545
|
-
const checkMinScore = (actualScore, minScore) => {
|
|
2546
|
-
if (minScore === void 0) return true;
|
|
2547
|
-
return actualScore >= minScore;
|
|
2548
|
-
};
|
|
2549
|
-
|
|
2550
2538
|
//#endregion
|
|
2551
2539
|
//#region src/cli/output/highlighter.ts
|
|
2552
2540
|
const highlighter = {
|
|
@@ -2597,6 +2585,41 @@ const logger = {
|
|
|
2597
2585
|
}
|
|
2598
2586
|
};
|
|
2599
2587
|
|
|
2588
|
+
//#endregion
|
|
2589
|
+
//#region src/cli/init-skill.ts
|
|
2590
|
+
const SKILL_DIR = ".claude/skills/nestjs-doctor";
|
|
2591
|
+
const SKILL_FILE = "SKILL.md";
|
|
2592
|
+
const initSkill = async (targetPath) => {
|
|
2593
|
+
const skillDir = join(targetPath, SKILL_DIR);
|
|
2594
|
+
const skillPath = join(skillDir, SKILL_FILE);
|
|
2595
|
+
if (existsSync(skillPath)) {
|
|
2596
|
+
logger.info(`${SKILL_DIR}/${SKILL_FILE} already exists — skipping.`);
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
const template = await readFile(createRequire(import.meta.url).resolve("../../skill/SKILL.md"), "utf-8");
|
|
2600
|
+
await mkdir(skillDir, { recursive: true });
|
|
2601
|
+
await writeFile(skillPath, template, "utf-8");
|
|
2602
|
+
logger.success(`Created ${SKILL_DIR}/${SKILL_FILE} — use /nestjs-doctor in Claude Code to scan and fix your NestJS project.`);
|
|
2603
|
+
};
|
|
2604
|
+
|
|
2605
|
+
//#endregion
|
|
2606
|
+
//#region src/cli/min-score.ts
|
|
2607
|
+
const validateMinScoreArg = (raw) => {
|
|
2608
|
+
if (raw.trim() === "") return `Invalid --min-score value: "${raw}". Must be an integer between 0 and 100.`;
|
|
2609
|
+
const num = Number(raw);
|
|
2610
|
+
if (!Number.isInteger(num)) return `Invalid --min-score value: "${raw}". Must be an integer between 0 and 100.`;
|
|
2611
|
+
if (num < 0 || num > 100) return `Invalid --min-score value: "${raw}". Must be an integer between 0 and 100.`;
|
|
2612
|
+
return null;
|
|
2613
|
+
};
|
|
2614
|
+
const resolveMinScore = (cliValue, configValue) => {
|
|
2615
|
+
if (cliValue !== void 0) return Number(cliValue);
|
|
2616
|
+
return configValue;
|
|
2617
|
+
};
|
|
2618
|
+
const checkMinScore = (actualScore, minScore) => {
|
|
2619
|
+
if (minScore === void 0) return true;
|
|
2620
|
+
return actualScore >= minScore;
|
|
2621
|
+
};
|
|
2622
|
+
|
|
2600
2623
|
//#endregion
|
|
2601
2624
|
//#region src/cli/output/console-reporter.ts
|
|
2602
2625
|
const PERFECT_SCORE = 100;
|
|
@@ -2859,6 +2882,10 @@ runMain(defineCommand({
|
|
|
2859
2882
|
},
|
|
2860
2883
|
async run({ args }) {
|
|
2861
2884
|
const targetPath = resolve(args.path ?? ".");
|
|
2885
|
+
if (args.init) {
|
|
2886
|
+
await initSkill(targetPath);
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2862
2889
|
const isSilent = args.score || args.json;
|
|
2863
2890
|
const rawMinScore = args["min-score"];
|
|
2864
2891
|
if (rawMinScore !== void 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nestjs-doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Diagnostic CLI tool that scans NestJS codebases and produces a health score with actionable diagnostics",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"skill"
|
|
21
22
|
],
|
|
22
23
|
"keywords": [
|
|
23
24
|
"nestjs",
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Scan a NestJS project with nestjs-doctor, present a health report, and fix issues interactively
|
|
3
|
+
disable-model-invocation: true
|
|
4
|
+
allowed-tools: Bash, Read, Edit, Glob, Grep, Write
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /nestjs-doctor — NestJS Health Scanner & Fixer
|
|
8
|
+
|
|
9
|
+
Scan the NestJS codebase, present a prioritized health report, and offer to fix every issue found.
|
|
10
|
+
|
|
11
|
+
## Step 1: Scan
|
|
12
|
+
|
|
13
|
+
Run nestjs-doctor and capture the full JSON output:
|
|
14
|
+
|
|
15
|
+
```!
|
|
16
|
+
npx nestjs-doctor $ARGUMENTS --json 2>/dev/null
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Step 2: Summarize
|
|
20
|
+
|
|
21
|
+
Parse the JSON output above. Present a summary:
|
|
22
|
+
|
|
23
|
+
- **Score**: value / 100 (label)
|
|
24
|
+
- **Severity counts**: errors, warnings, info
|
|
25
|
+
- **Project info**: name, NestJS version, ORM, file count, module count
|
|
26
|
+
|
|
27
|
+
## Step 3: Report
|
|
28
|
+
|
|
29
|
+
Group diagnostics by severity (errors first, then warnings, then info). Within each severity, group by category (security > correctness > architecture > performance).
|
|
30
|
+
|
|
31
|
+
For each group show:
|
|
32
|
+
- Rule name and severity
|
|
33
|
+
- Message and help text
|
|
34
|
+
- Number of occurrences
|
|
35
|
+
- Affected file paths (with line numbers)
|
|
36
|
+
|
|
37
|
+
If there are more than 30 diagnostics, show the top 30 (prioritizing errors and warnings) and ask if the user wants to see the rest.
|
|
38
|
+
|
|
39
|
+
## Step 4: Offer to Fix
|
|
40
|
+
|
|
41
|
+
Ask the developer what they want to fix:
|
|
42
|
+
1. **Fix all** — apply fixes for every diagnostic
|
|
43
|
+
2. **Fix by category** — fix all issues in a specific category (security, correctness, architecture, performance)
|
|
44
|
+
3. **Fix specific rules** — fix only specific rule violations
|
|
45
|
+
4. **Review only** — no changes, just the report
|
|
46
|
+
|
|
47
|
+
## Step 5: Fix
|
|
48
|
+
|
|
49
|
+
For each diagnostic to fix:
|
|
50
|
+
1. Read the affected file
|
|
51
|
+
2. Apply the appropriate fix based on the rule (see Fix Guide below)
|
|
52
|
+
3. Explain what was changed and why
|
|
53
|
+
|
|
54
|
+
### Fix Guide by Rule
|
|
55
|
+
|
|
56
|
+
#### Security
|
|
57
|
+
|
|
58
|
+
- **no-hardcoded-secrets**: Extract the secret value to an environment variable. Import `ConfigService`, inject it, and use `this.configService.get('ENV_VAR_NAME')`. Add the env var name to `.env.example` if it exists.
|
|
59
|
+
- **no-wildcard-cors**: Replace `origin: '*'` or `origin: true` with an explicit allowlist array or `configService.get('CORS_ORIGINS').split(',')`.
|
|
60
|
+
- **no-unsafe-raw-query**: Replace template literal interpolation in raw SQL with parameterized queries. Use `$1, $2` placeholders (Postgres) or `?` (MySQL) and pass values as the second argument.
|
|
61
|
+
- **no-eval**: Remove `eval()` or `new Function()`. Replace with safe alternatives: `JSON.parse()` for JSON, a proper expression parser for dynamic evaluation, or refactor to avoid dynamic code execution entirely.
|
|
62
|
+
- **no-csrf-disabled**: Remove the code that explicitly disables CSRF protection, or add a comment explaining why it's intentionally disabled with a `// nestjs-doctor-ignore` comment.
|
|
63
|
+
- **no-dangerous-redirects**: Validate redirect URLs against an allowlist of trusted domains. Never pass user input directly to `res.redirect()`.
|
|
64
|
+
- **no-weak-crypto**: Replace `createHash('md5')` or `createHash('sha1')` with `createHash('sha256')` or stronger.
|
|
65
|
+
- **no-exposed-env-vars**: Replace direct `process.env.X` access with `ConfigService`. Inject `ConfigService` and use `this.configService.get('X')` or `this.configService.getOrThrow('X')`.
|
|
66
|
+
- **require-validation-pipe**: Add a validation pipe. Either add `@UsePipes(new ValidationPipe())` to the handler/controller, or use a global validation pipe in `main.ts`, or add a DTO class with `class-validator` decorators.
|
|
67
|
+
- **no-exposed-stack-trace**: Remove `error.stack` from response objects. Log the stack trace server-side with `this.logger.error(error.stack)` and return a generic error message to the client.
|
|
68
|
+
- **require-auth-guard**: Add `@UseGuards(AuthGuard)` to the controller class or individual routes. If intentionally public, add a comment explaining why.
|
|
69
|
+
|
|
70
|
+
#### Correctness
|
|
71
|
+
|
|
72
|
+
- **no-missing-injectable**: Add `@Injectable()` decorator to the class. Import it from `@nestjs/common`.
|
|
73
|
+
- **no-duplicate-routes**: Remove or rename the duplicate route. Change the HTTP method or path to make each route unique.
|
|
74
|
+
- **no-missing-guard-method**: Add the `canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean>` method to the guard class. Implement `CanActivate` interface.
|
|
75
|
+
- **no-missing-pipe-method**: Add the `transform(value: any, metadata: ArgumentMetadata)` method to the pipe class. Implement `PipeTransform` interface.
|
|
76
|
+
- **no-missing-filter-catch**: Add the `catch(exception: T, host: ArgumentHost)` method to the exception filter class. Implement `ExceptionFilter` interface.
|
|
77
|
+
- **no-missing-interceptor-method**: Add the `intercept(context: ExecutionContext, next: CallHandler): Observable<any>` method. Implement `NestInterceptor` interface.
|
|
78
|
+
- **require-inject-decorator**: Add `@Inject('TOKEN_NAME')` decorator to untyped constructor parameters, or add a proper type annotation.
|
|
79
|
+
- **prefer-readonly-injection**: Add the `readonly` modifier to constructor-injected parameters: `constructor(private readonly myService: MyService)`.
|
|
80
|
+
- **require-lifecycle-interface**: Add the corresponding interface to the class implements clause. E.g., if using `onModuleInit()`, add `implements OnModuleInit`.
|
|
81
|
+
- **no-empty-handlers**: Add implementation to the empty HTTP handler. If it's a placeholder, add a `throw new NotImplementedException()`.
|
|
82
|
+
- **no-async-without-await**: Either add an `await` expression, remove the `async` keyword, or if it intentionally returns a Promise, remove `async` and return the promise directly.
|
|
83
|
+
- **no-duplicate-module-metadata**: Remove duplicate entries from `@Module()` arrays (providers, controllers, imports, exports).
|
|
84
|
+
- **no-missing-module-decorator**: Add `@Module({})` decorator to the class. Import it from `@nestjs/common`.
|
|
85
|
+
|
|
86
|
+
#### Architecture
|
|
87
|
+
|
|
88
|
+
- **no-business-logic-in-controllers**: Extract the business logic (loops, complex conditionals, data transforms) into a service method. The controller should only call the service and return the result.
|
|
89
|
+
- **no-repository-in-controllers**: Remove the repository injection from the controller. Create or use an existing service that wraps the repository, and inject that service instead.
|
|
90
|
+
- **no-orm-in-controllers**: Remove PrismaService/EntityManager/DataSource injection from the controller. Create or use an existing service that wraps the ORM operations.
|
|
91
|
+
- **no-circular-module-deps**: Break the circular dependency. Extract shared functionality into a separate module, use `forwardRef(() => Module)`, or restructure the module boundaries.
|
|
92
|
+
- **no-manual-instantiation**: Replace `new SomeService()` with constructor injection. Add the service to the module's providers and inject it via the constructor.
|
|
93
|
+
- **no-orm-in-services**: Consider introducing a repository layer. Create a repository class that wraps ORM operations, and inject the repository into the service instead of the ORM directly.
|
|
94
|
+
- **no-god-module**: Split the module into smaller, focused feature modules. Group related providers together and create dedicated modules for each feature area.
|
|
95
|
+
- **no-god-service**: Split the service into smaller, focused services. Extract groups of related methods into separate service classes.
|
|
96
|
+
- **require-feature-modules**: Move providers out of AppModule into dedicated feature modules. Create feature modules and register providers there instead.
|
|
97
|
+
- **prefer-constructor-injection**: Replace `@Inject()` property injection with constructor injection. Move the dependency to a constructor parameter.
|
|
98
|
+
- **require-module-boundaries**: Replace deep imports (`import { X } from '../other-module/services/x.service'`) with imports through the module's public API (barrel file / index.ts).
|
|
99
|
+
- **prefer-interface-injection**: Consider creating an interface/abstract class for the dependency and injecting that instead of the concrete implementation.
|
|
100
|
+
- **no-barrel-export-internals**: Remove repository re-exports from barrel files. Repositories should only be accessible within their own module.
|
|
101
|
+
|
|
102
|
+
#### Performance
|
|
103
|
+
|
|
104
|
+
- **no-sync-io**: Replace synchronous I/O (`readFileSync`, `writeFileSync`, etc.) with async equivalents (`readFile`, `writeFile` from `fs/promises`).
|
|
105
|
+
- **no-query-in-loop**: Replace the N+1 query pattern. Use batch queries, `Promise.all()`, or eager loading/joins to fetch all related data in a single query.
|
|
106
|
+
- **no-blocking-constructor**: Move async operations and loops out of the constructor into `onModuleInit()` lifecycle hook. Implement `OnModuleInit` interface.
|
|
107
|
+
- **no-dynamic-require**: Replace `require(variable)` with a static import or a switch/map pattern that uses static `require()` calls.
|
|
108
|
+
- **no-unused-providers**: Remove the unused provider from the module's providers array, or start using it. If it's intended for external consumers, add it to the module's exports.
|
|
109
|
+
- **no-logging-in-loops**: Move logging outside the loop, log a summary after the loop completes, or use conditional logging (e.g., log every Nth iteration).
|
|
110
|
+
- **no-unnecessary-async**: Remove the `async` keyword since the function contains no `await` expressions. If it returns a Promise, return it directly without `async`.
|
|
111
|
+
- **prefer-pagination**: Add pagination parameters (`skip`/`take`, `limit`/`offset`, or cursor-based) to `findMany()`/`find()` calls to avoid loading unbounded result sets.
|
|
112
|
+
- **no-unused-module-exports**: Remove the unused export from the module's exports array, or start importing the module where the exported provider is needed.
|
|
113
|
+
- **no-orphan-modules**: Import this module in another module that needs it, or remove it if it's truly unused. If it's the root module, this can be ignored.
|
|
114
|
+
|
|
115
|
+
## Step 6: Verify
|
|
116
|
+
|
|
117
|
+
After applying fixes, suggest re-running the scan:
|
|
118
|
+
|
|
119
|
+
> Fixes applied. Run `/nestjs-doctor` again to verify the score improved.
|