devkit-quality 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Karan Chourasia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,312 @@
1
+ # DevKit
2
+
3
+ DevKit is a deterministic, offline code-quality scanner for JavaScript and TypeScript repositories. It walks a project, parses every source file with the TypeScript compiler, and reports a 0-10 "code quality" score backed by concrete, file-and-line findings — dead code, unused dependencies, excessive complexity, duplication, unsafe error handling, unsafe TypeScript, security anti-patterns, architecture violations, and repository hygiene issues.
4
+
5
+ It does not call an LLM, an API, or the network. Every finding is reproducible from the source tree alone.
6
+
7
+ ```bash
8
+ npx devkit scan
9
+ ```
10
+
11
+ ```
12
+ DevKit Repository Scan
13
+ /path/to/project
14
+
15
+ 7.8/10 Good
16
+ [███████████████░░░░]
17
+
18
+ Dead Code 8.4
19
+ Dependencies 7.1
20
+ Complexity 6.8
21
+ ...
22
+ ```
23
+
24
+ ## Table of contents
25
+
26
+ - [How it works](#how-it-works)
27
+ - [Architecture](#architecture)
28
+ - [Project layout](#project-layout)
29
+ - [Setup](#setup)
30
+ - [Usage](#usage)
31
+ - [Configuration](#configuration)
32
+ - [Scoring model](#scoring-model)
33
+ - [Output formats](#output-formats)
34
+ - [CI/CD integration](#cicd-integration)
35
+ - [Development](#development)
36
+
37
+ ## How it works
38
+
39
+ A scan runs in a single pass over the repository:
40
+
41
+ 1. **Discovery** — walk the directory tree, apply configured include/exclude and ignore patterns, skip generated files, and select `.js`/`.jsx`/`.ts`/`.tsx`/`.mjs`/`.cjs` files.
42
+ 2. **Parse** — build one `ts.Program` (via the TypeScript Compiler API) covering every discovered file, with `noUnusedLocals`/`noUnusedParameters`/`noImplicitAny` enabled so the compiler itself surfaces dead bindings and implicit `any`.
43
+ 3. **Module graph** — resolve imports, `require()`, dynamic `import()`, TypeScript path aliases, and re-exports into an internal dependency graph (edges, reverse edges, entry points, re-export targets).
44
+ 4. **Rules** — each rule module receives a shared `RuleContext` (files, program, module graph, config, `package.json`) and returns `Finding[]`.
45
+ 5. **Scoring** — findings are weighted by severity × confidence, normalized against repository size, and rolled up into per-category and overall scores.
46
+ 6. **Reporting** — the same `ScanSummary` is rendered as colored terminal output, JSON, Markdown, or SARIF.
47
+
48
+ Design principle carried over from the project's PRD (`devkit-slop-scanner-final-prd.md`): never use an LLM to answer a question static analysis can answer deterministically. DevKit only measures *observable* characteristics of the code (unused, duplicated, overly complex, unsafe) — it makes no claim about authorship or subjective quality.
49
+
50
+ ## Architecture
51
+
52
+ ```
53
+ devkit scan
54
+ |
55
+ v
56
+ scanRepository() scanner.ts
57
+ |
58
+ +-------------+--------------+
59
+ | | |
60
+ walkDirectory createProgram buildModuleGraph
61
+ (discovery.ts) (ast/parse.ts) (moduleGraph.ts)
62
+ | | |
63
+ +-------------+--------------+
64
+ |
65
+ v
66
+ RuleContext context.ts
67
+ |
68
+ +----+----+----+----+----+----+----+----+----+----+
69
+ | | | | | | | | | | |
70
+ deadCode|complexity|duplication|errorHandling|redundancy
71
+ dependencies typescript javascript security
72
+ architecture hygiene
73
+ (src/rules/*.ts, definitions in rules.ts)
74
+ +----+----+----+----+----+----+----+----+----+----+
75
+ |
76
+ v
77
+ Finding[] finding.ts / types.ts
78
+ |
79
+ v
80
+ computeScores() scoring.ts
81
+ |
82
+ v
83
+ ScanSummary
84
+ |
85
+ +-------------+-------------+-------------+
86
+ | | | |
87
+ formatTerminal formatJson formatMarkdown formatSarif
88
+ (reporters.ts)
89
+ ```
90
+
91
+ ### Core modules
92
+
93
+ | Module | Responsibility |
94
+ | --- | --- |
95
+ | [`src/cli.ts`](src/cli.ts) | Commander-based CLI entry point; wires commands to the scanner and reporters. |
96
+ | [`src/scanner.ts`](src/scanner.ts) | Orchestrates a full scan: discovery → program → module graph → rules → metrics → scoring. |
97
+ | [`src/discovery.ts`](src/discovery.ts) | Directory walking, file filtering, generated/test-file detection, line counting. |
98
+ | [`src/ignore.ts`](src/ignore.ts) | Loads `.gitignore` and `.devkitignore` patterns for scan filtering. |
99
+ | [`src/ast/parse.ts`](src/ast/parse.ts) | Creates the shared `ts.Program` (compiler options, script-kind detection). |
100
+ | [`src/ast/walk.ts`](src/ast/walk.ts) | AST traversal helpers (`forEachNode`, function-like detection, line/column lookup). |
101
+ | [`src/ast/comments.ts`](src/ast/comments.ts) | Collects and caches comment ranges for hygiene checks and suppressions. |
102
+ | [`src/moduleResolution.ts`](src/moduleResolution.ts) | Resolves modules using TypeScript config, path aliases, and a per-scan cache. |
103
+ | [`src/moduleGraph.ts`](src/moduleGraph.ts) | Resolves imports/exports/re-exports into an internal dependency graph; detects entry points and import cycles. |
104
+ | [`src/suppressions.ts`](src/suppressions.ts) | Applies `devkit-disable-next-line` directives to findings. |
105
+ | [`src/cliLogic.ts`](src/cliLogic.ts) | Pure finding-filter and quality-gate logic used by the CLI. |
106
+ | [`src/config.ts`](src/config.ts) | Loads/writes `.devkitrc.json`; rule enable/disable and threshold overrides. |
107
+ | [`src/context.ts`](src/context.ts) | `RuleContext` type shared by every rule module. |
108
+ | [`src/rules.ts`](src/rules.ts) | Declarative catalog of every rule's metadata (id, severity, confidence, docs) — consumed by `devkit rules`/`devkit explain`. |
109
+ | [`src/rules/*.ts`](src/rules) | One module per category; each exports a `run*Rules(context)` function that returns `Finding[]`. |
110
+ | [`src/finding.ts`](src/finding.ts) | `buildFinding()` constructs a `Finding` with a stable id. |
111
+ | [`src/scoring.ts`](src/scoring.ts) | Converts findings into category scores and an overall 0-10 score. |
112
+ | [`src/reporters.ts`](src/reporters.ts) | Terminal, JSON, Markdown, and SARIF renderers, plus baseline-compare output. |
113
+ | [`src/terminal.ts`](src/terminal.ts) | ANSI color helpers, score bar, code-frame rendering, spinner. |
114
+ | [`src/glob.ts`](src/glob.ts) | Minimal glob-to-regex matcher used for config include/exclude patterns. |
115
+
116
+ ### Rule categories implemented
117
+
118
+ | Category | Rule IDs | File |
119
+ | --- | --- | --- |
120
+ | Dead code | `DEAD002`–`DEAD011` | [`src/rules/deadCode.ts`](src/rules/deadCode.ts) |
121
+ | Dependencies | `DEP001`–`DEP003` | [`src/rules/dependencies.ts`](src/rules/dependencies.ts) |
122
+ | Complexity | `COMPLEX001`–`COMPLEX005` | [`src/rules/complexity.ts`](src/rules/complexity.ts) |
123
+ | Duplication | `DUP001` | [`src/rules/duplication.ts`](src/rules/duplication.ts) |
124
+ | Error handling | `ERR001`–`ERR004` | [`src/rules/errorHandling.ts`](src/rules/errorHandling.ts) |
125
+ | Redundant logic | `REDUNDANT001`–`REDUNDANT002` | [`src/rules/redundancy.ts`](src/rules/redundancy.ts) |
126
+ | TypeScript safety | `TS001`–`TS003` | [`src/rules/typescript.ts`](src/rules/typescript.ts) |
127
+ | JavaScript hygiene | `JS001`–`JS002` | [`src/rules/javascript.ts`](src/rules/javascript.ts) |
128
+ | Security | `SEC001`–`SEC010` (SEC006 reserved) | [`src/rules/security.ts`](src/rules/security.ts) |
129
+ | Architecture | `ARCH001` | [`src/rules/architecture.ts`](src/rules/architecture.ts) |
130
+ | Hygiene | `HYGIENE001`–`HYGIENE004` | [`src/rules/hygiene.ts`](src/rules/hygiene.ts) |
131
+
132
+ Run `devkit rules` for the live list, or `devkit explain <RULE_ID>` for a rule's full writeup (why it matters, example, fix classification).
133
+
134
+ > **Note:** `devkit-slop-scanner-final-prd.md` in the repo root is the original product-requirements document and describes a larger target surface (incremental caching, monorepo-aware scoring, rule presets, a V2 LLM layer, and more). The table above reflects what is currently implemented in `src/`; treat the PRD as the roadmap, not the current feature set. Minimal `devkit-disable-next-line` suppressions are implemented.
135
+
136
+ ## Project layout
137
+
138
+ ```
139
+ cli-tool/
140
+ ├── bin/devkit.js # Shebang entry point, requires dist/cli.js
141
+ ├── src/
142
+ │ ├── cli.ts # Command definitions (scan, report, metrics, rules, explain, baseline, fix, init)
143
+ │ ├── scanner.ts # scanRepository() — the main pipeline
144
+ │ ├── discovery.ts # File walking/filtering
145
+ │ ├── moduleGraph.ts # Import/export graph + cycle detection
146
+ │ ├── config.ts # .devkitrc.json loading
147
+ │ ├── context.ts # RuleContext type
148
+ │ ├── rules.ts # Rule metadata catalog
149
+ │ ├── rules/ # One file per rule category (the detectors)
150
+ │ ├── ast/ # TypeScript Compiler API helpers
151
+ │ ├── finding.ts # Finding constructor
152
+ │ ├── scoring.ts # Score computation
153
+ │ ├── reporters.ts # terminal/json/markdown/sarif output
154
+ │ ├── terminal.ts # Color/formatting primitives
155
+ │ └── __tests__/ # Vitest suite, one file per rule category + e2e
156
+ ├── dist/ # Compiled output (tsc), what bin/devkit.js actually runs
157
+ ├── tsconfig.json
158
+ └── package.json
159
+ ```
160
+
161
+ ## Setup
162
+
163
+ Requires Node.js (for the TypeScript compiler API and `fs`/`path` usage — any reasonably current LTS version works).
164
+
165
+ ```bash
166
+ # install dependencies
167
+ npm install
168
+
169
+ # build (compiles src/ -> dist/, which bin/devkit.js requires)
170
+ npm run build
171
+
172
+ # run against the current directory
173
+ node dist/cli.js scan
174
+ # or, once linked/installed as a package:
175
+ devkit scan
176
+ ```
177
+
178
+ For local development without a build step, run the CLI directly through `tsx`:
179
+
180
+ ```bash
181
+ npm run dev -- scan # tsx src/cli.ts scan
182
+ npm run scan # shortcut for the above
183
+ ```
184
+
185
+ To use the `devkit` command globally from this checkout:
186
+
187
+ ```bash
188
+ npm run build
189
+ npm link # exposes `devkit` on PATH via the "bin" entry in package.json
190
+ ```
191
+
192
+ ## Usage
193
+
194
+ Initialize a config file in a target project:
195
+
196
+ ```bash
197
+ devkit init
198
+ # writes .devkitrc.json with default include/exclude globs
199
+ ```
200
+
201
+ Run a full scan (defaults to colored terminal output):
202
+
203
+ ```bash
204
+ devkit scan
205
+ devkit scan --json # machine-readable JSON (ScanSummary)
206
+ devkit scan --format markdown # Markdown report
207
+ devkit scan --format sarif # SARIF, for code-scanning platforms
208
+ devkit scan --category dead-code # only one category
209
+ devkit scan --severity high # only one severity level
210
+ ```
211
+
212
+ Other commands:
213
+
214
+ ```bash
215
+ devkit metrics # repository metrics: LOC, function/class counts, largest files, etc.
216
+ devkit report # full Markdown report (same as `scan --format markdown`)
217
+ devkit rules # list every built-in rule (id, title, category)
218
+ devkit explain DEAD010 # full explanation of a single rule
219
+ devkit baseline create # snapshot the current score to .devkit/baseline.json
220
+ devkit baseline compare # compare current score against the stored baseline
221
+ devkit fix # preview findings marked as safe to fix
222
+ ```
223
+
224
+ ## Configuration
225
+
226
+ `devkit init` creates `.devkitrc.json` in the project root:
227
+
228
+ ```json
229
+ {
230
+ "project": { "name": "my-project" },
231
+ "scan": {
232
+ "include": ["src/**", "packages/**"],
233
+ "exclude": ["node_modules/**", "dist/**", "coverage/**", ".git/**", ".devkit/**"]
234
+ },
235
+ "rules": {}
236
+ }
237
+ ```
238
+
239
+ Per-rule overrides live under `rules`:
240
+
241
+ ```json
242
+ {
243
+ "rules": {
244
+ "COMPLEX001": { "max": 15 },
245
+ "JS001": { "enabled": false }
246
+ }
247
+ }
248
+ ```
249
+
250
+ Architecture-layer rules (`ARCH001`) are opt-in and only produce findings once configured:
251
+
252
+ ```json
253
+ {
254
+ "architecture": {
255
+ "layers": {
256
+ "controllers": { "match": ["src/controllers/**"], "cannotImport": ["database"] },
257
+ "database": { "match": ["src/database/**"], "cannotImport": [] }
258
+ }
259
+ }
260
+ }
261
+ ```
262
+
263
+ ## Scoring model
264
+
265
+ Each finding is weighted by `severity × confidence` ([`src/scoring.ts`](src/scoring.ts)) and normalized against source LOC (in 500-line chunks), so a single low-confidence finding in a large repository barely moves the score. Category scores are combined into the overall score using fixed weights:
266
+
267
+ | Category | Weight |
268
+ | --- | ---: |
269
+ | Dead code | 20% |
270
+ | Complexity | 15% |
271
+ | Dependencies | 10% |
272
+ | Duplication | 10% |
273
+ | Redundant logic | 10% |
274
+ | Error handling | 10% |
275
+ | Type safety | 10% |
276
+ | Architecture | 10% |
277
+ | Hygiene | 5% |
278
+
279
+ Security findings are scored and reported separately rather than folded into the overall score.
280
+
281
+ ## Output formats
282
+
283
+ - **Terminal** (default) — colored, with a score bar, per-category breakdown, and code frames for the top finding per category.
284
+ - **JSON** (`--json` / `--format json`) — the full `ScanSummary` object: score, category scores, every finding, repository metrics.
285
+ - **Markdown** (`--format markdown` / `devkit report`) — category table, top deductions, and a flat findings list, suitable for pasting into a PR description.
286
+ - **SARIF** (`--format sarif`) — standard SARIF 2.1.0, for GitHub code scanning and similar tools.
287
+
288
+ ## Current limitations
289
+
290
+ - Framework entry-point detection is limited to package metadata, tests, and a basic Next.js convention check. Other React setups such as Vite may need entry files specified through package metadata or imports.
291
+ - `.gitignore` and `.devkitignore` negation patterns (`!pattern`) are skipped; full Git ignore semantics are not implemented.
292
+ - `devkit fix` only previews findings marked as safe. It does not modify files.
293
+ - `devkit baseline compare` compares the overall score only; it does not report newly added or resolved findings.
294
+ - DevKit has no rule presets, React-specific or test-quality rules, configuration analysis, incremental cache, or monorepo-aware scoring.
295
+
296
+ ## CI/CD integration
297
+
298
+ ```bash
299
+ devkit scan --min-score 7 # exit 1 if the overall score falls below 7
300
+ devkit scan --fail-on high # exit 1 if any HIGH or CRITICAL finding exists
301
+ devkit baseline compare # compare against a previously stored baseline
302
+ ```
303
+
304
+ ## Development
305
+
306
+ ```bash
307
+ npm run build # tsc -p tsconfig.json
308
+ npm test # vitest run
309
+ npm run dev # tsx src/cli.ts (no build step)
310
+ ```
311
+
312
+ Tests live in [`src/__tests__/`](src/__tests__), one file per rule category plus an end-to-end scan test (`e2e.test.ts`) and a scoring test. Each rule test typically builds a small in-memory `RuleContext` (see [`testUtils.ts`](src/__tests__/testUtils.ts)) and asserts on the findings a rule produces for known-good and known-bad snippets.
package/bin/devkit.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require("../dist/cli.js");
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.collectCommentRanges = collectCommentRanges;
7
+ const typescript_1 = __importDefault(require("typescript"));
8
+ const COMMENT_RANGE_CACHE = new WeakMap();
9
+ function collectCommentRanges(sourceFile) {
10
+ const cached = COMMENT_RANGE_CACHE.get(sourceFile);
11
+ if (cached)
12
+ return cached;
13
+ const ranges = new Map();
14
+ const addAt = (position) => {
15
+ for (const range of typescript_1.default.getLeadingCommentRanges(sourceFile.text, position) ?? []) {
16
+ ranges.set(`${range.pos}:${range.end}`, range);
17
+ }
18
+ };
19
+ addAt(sourceFile.getFullStart());
20
+ const visit = (node) => {
21
+ addAt(node.getFullStart());
22
+ typescript_1.default.forEachChild(node, visit);
23
+ };
24
+ visit(sourceFile);
25
+ const result = [...ranges.values()].sort((a, b) => a.pos - b.pos);
26
+ COMMENT_RANGE_CACHE.set(sourceFile, result);
27
+ return result;
28
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PROGRAM_COMPILER_OPTIONS = void 0;
7
+ exports.parseSourceFile = parseSourceFile;
8
+ exports.createProgram = createProgram;
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const typescript_1 = __importDefault(require("typescript"));
11
+ function scriptKindFor(filePath) {
12
+ const ext = node_path_1.default.extname(filePath).toLowerCase();
13
+ switch (ext) {
14
+ case '.tsx':
15
+ return typescript_1.default.ScriptKind.TSX;
16
+ case '.jsx':
17
+ return typescript_1.default.ScriptKind.JSX;
18
+ case '.ts':
19
+ return typescript_1.default.ScriptKind.TS;
20
+ case '.mjs':
21
+ case '.cjs':
22
+ case '.js':
23
+ return typescript_1.default.ScriptKind.JS;
24
+ default:
25
+ return typescript_1.default.ScriptKind.Unknown;
26
+ }
27
+ }
28
+ function parseSourceFile(filePath, text) {
29
+ return typescript_1.default.createSourceFile(filePath, text, typescript_1.default.ScriptTarget.ES2022, true, scriptKindFor(filePath));
30
+ }
31
+ exports.PROGRAM_COMPILER_OPTIONS = {
32
+ target: typescript_1.default.ScriptTarget.ES2022,
33
+ module: typescript_1.default.ModuleKind.ESNext,
34
+ moduleResolution: typescript_1.default.ModuleResolutionKind.Bundler,
35
+ jsx: typescript_1.default.JsxEmit.ReactJSX,
36
+ allowJs: true,
37
+ checkJs: true,
38
+ noEmit: true,
39
+ skipLibCheck: true,
40
+ noUnusedLocals: true,
41
+ noUnusedParameters: true,
42
+ noImplicitAny: true,
43
+ strictNullChecks: true,
44
+ strict: false
45
+ };
46
+ function createProgram(files) {
47
+ // The default host created implicitly by ts.createProgram does not set parent pointers on
48
+ // parsed nodes, which breaks node.getStart()/getEnd()/getText() used throughout the rule
49
+ // engine. Build the host explicitly with setParentNodes enabled.
50
+ const host = typescript_1.default.createCompilerHost(exports.PROGRAM_COMPILER_OPTIONS, true);
51
+ return typescript_1.default.createProgram({
52
+ rootNames: files,
53
+ options: exports.PROGRAM_COMPILER_OPTIONS,
54
+ host
55
+ });
56
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.forEachNode = forEachNode;
7
+ exports.isFunctionLike = isFunctionLike;
8
+ exports.getFunctionName = getFunctionName;
9
+ exports.lineAndColumn = lineAndColumn;
10
+ exports.findNodeAtPosition = findNodeAtPosition;
11
+ exports.isStatementContainer = isStatementContainer;
12
+ const typescript_1 = __importDefault(require("typescript"));
13
+ function forEachNode(root, visitor) {
14
+ visitor(root);
15
+ typescript_1.default.forEachChild(root, (child) => forEachNode(child, visitor));
16
+ }
17
+ function isFunctionLike(node) {
18
+ return (typescript_1.default.isFunctionDeclaration(node) ||
19
+ typescript_1.default.isFunctionExpression(node) ||
20
+ typescript_1.default.isArrowFunction(node) ||
21
+ typescript_1.default.isMethodDeclaration(node) ||
22
+ typescript_1.default.isConstructorDeclaration(node) ||
23
+ typescript_1.default.isGetAccessorDeclaration(node) ||
24
+ typescript_1.default.isSetAccessorDeclaration(node));
25
+ }
26
+ function getFunctionName(node) {
27
+ if (node.name && typescript_1.default.isIdentifier(node.name)) {
28
+ return node.name.text;
29
+ }
30
+ if (typescript_1.default.isConstructorDeclaration(node)) {
31
+ return 'constructor';
32
+ }
33
+ const parent = node.parent;
34
+ if (parent) {
35
+ if (typescript_1.default.isVariableDeclaration(parent) && typescript_1.default.isIdentifier(parent.name)) {
36
+ return parent.name.text;
37
+ }
38
+ if (typescript_1.default.isPropertyAssignment(parent) && typescript_1.default.isIdentifier(parent.name)) {
39
+ return parent.name.text;
40
+ }
41
+ if (typescript_1.default.isPropertyDeclaration(parent) && typescript_1.default.isIdentifier(parent.name)) {
42
+ return parent.name.text;
43
+ }
44
+ }
45
+ return 'anonymous';
46
+ }
47
+ function lineAndColumn(sourceFile, pos) {
48
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
49
+ return { line: line + 1, column: character + 1 };
50
+ }
51
+ function findNodeAtPosition(root, pos) {
52
+ let best = root;
53
+ const visit = (node) => {
54
+ if (pos < node.getFullStart() || pos >= node.getEnd()) {
55
+ return;
56
+ }
57
+ best = node;
58
+ typescript_1.default.forEachChild(node, visit);
59
+ };
60
+ visit(root);
61
+ return best;
62
+ }
63
+ function isStatementContainer(node) {
64
+ return (typescript_1.default.isBlock(node) ||
65
+ typescript_1.default.isSourceFile(node) ||
66
+ typescript_1.default.isCaseClause(node) ||
67
+ typescript_1.default.isDefaultClause(node) ||
68
+ typescript_1.default.isModuleBlock(node));
69
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const commander_1 = require("commander");
10
+ const config_1 = require("./config");
11
+ const rules_1 = require("./rules");
12
+ const reporters_1 = require("./reporters");
13
+ const scanner_1 = require("./scanner");
14
+ const scoring_1 = require("./scoring");
15
+ const terminal_1 = require("./terminal");
16
+ const cliLogic_1 = require("./cliLogic");
17
+ async function runWithSpinner(text, task) {
18
+ const spinner = new terminal_1.Spinner(text);
19
+ spinner.start();
20
+ await (0, terminal_1.waitForNextTick)();
21
+ try {
22
+ return task();
23
+ }
24
+ finally {
25
+ spinner.stop();
26
+ }
27
+ }
28
+ const program = new commander_1.Command();
29
+ program.name('devkit').description('Deterministic repository-quality scanner for JavaScript and TypeScript projects').version('0.1.0');
30
+ program
31
+ .command('init')
32
+ .description('Create a default DevKit config file in the current project')
33
+ .action(() => {
34
+ const configPath = (0, config_1.writeConfig)(process.cwd());
35
+ console.log(`Created config at ${configPath}`);
36
+ });
37
+ async function runScan(options) {
38
+ const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(process.cwd()));
39
+ const filteredFindings = (0, cliLogic_1.filterFindings)(summary.findings, options);
40
+ const filteredSummary = filteredFindings.length === summary.findings.length
41
+ ? summary
42
+ : {
43
+ ...summary,
44
+ findings: filteredFindings,
45
+ ...(0, scoring_1.computeScores)(filteredFindings, summary.metrics.sourceLOC)
46
+ };
47
+ const outputType = options.json ? 'json' : (options.format ?? 'terminal');
48
+ const output = outputType === 'json'
49
+ ? (0, reporters_1.formatJson)(filteredSummary)
50
+ : outputType === 'markdown'
51
+ ? (0, reporters_1.formatMarkdown)(filteredSummary)
52
+ : outputType === 'sarif'
53
+ ? (0, reporters_1.formatSarif)(filteredSummary)
54
+ : (0, reporters_1.formatTerminal)(filteredSummary);
55
+ console.log(output);
56
+ if (options.failOn && !(0, cliLogic_1.isKnownSeverity)(options.failOn))
57
+ console.error(`Unknown severity: ${options.failOn}`);
58
+ return (0, cliLogic_1.determineExitFailure)(filteredSummary, options);
59
+ }
60
+ program
61
+ .command('scan')
62
+ .description('Scan the current repository for code-quality issues')
63
+ .option('--json', 'Output JSON instead of terminal text')
64
+ .option('--format <type>', 'Output format: terminal, json, markdown, sarif')
65
+ .option('--category <name>', 'Filter by category')
66
+ .option('--severity <level>', 'Filter by severity')
67
+ .option('--min-score <score>', 'Fail with exit code 1 when score falls below this threshold')
68
+ .option('--fail-on <severity>', 'Fail with exit code 1 when any finding at or above this severity exists')
69
+ .action(async (options) => {
70
+ if (await runScan(options)) {
71
+ process.exitCode = 1;
72
+ }
73
+ });
74
+ program
75
+ .command('report')
76
+ .description('Generate a detailed Markdown report of the current repository')
77
+ .action(async () => {
78
+ const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(process.cwd()));
79
+ console.log((0, reporters_1.formatMarkdown)(summary));
80
+ });
81
+ program
82
+ .command('metrics')
83
+ .description('Print repository metrics for the current project')
84
+ .action(async () => {
85
+ const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(process.cwd()));
86
+ console.log((0, reporters_1.formatMetrics)(summary));
87
+ });
88
+ program
89
+ .command('rules')
90
+ .description('List the available built-in rules')
91
+ .action(() => {
92
+ for (const rule of (0, rules_1.listRules)()) {
93
+ console.log(`${rule.id} - ${rule.title} (${rule.category})`);
94
+ }
95
+ });
96
+ program
97
+ .command('explain <ruleId>')
98
+ .description('Explain a rule by its ID')
99
+ .action((ruleId) => {
100
+ const rule = (0, rules_1.getRuleById)(ruleId.toUpperCase());
101
+ if (!rule) {
102
+ console.error(`Unknown rule: ${ruleId}`);
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ console.log(`${rule.id}: ${rule.title}`);
107
+ console.log(`Category: ${rule.category}`);
108
+ console.log(`Severity: ${rule.severity}`);
109
+ console.log(`Confidence: ${rule.confidence}`);
110
+ console.log(`Description: ${rule.description}`);
111
+ console.log(`Why it matters: ${rule.why}`);
112
+ console.log(`Example: ${rule.example}`);
113
+ console.log(`Fix classification: ${rule.fixClassification}`);
114
+ if (rule.defaultThreshold !== undefined) {
115
+ console.log(`Default threshold: ${rule.defaultThreshold}`);
116
+ }
117
+ });
118
+ program
119
+ .command('baseline <action>')
120
+ .description('Create or compare a baseline score stored under .devkit')
121
+ .action(async (action) => {
122
+ const root = process.cwd();
123
+ const dir = node_path_1.default.join(root, '.devkit');
124
+ const baselineFile = node_path_1.default.join(dir, 'baseline.json');
125
+ if (!node_fs_1.default.existsSync(dir)) {
126
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
127
+ }
128
+ if (action === 'create') {
129
+ const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(root));
130
+ node_fs_1.default.writeFileSync(baselineFile, JSON.stringify({ score: summary.score }, null, 2));
131
+ console.log(`Created baseline at ${baselineFile} with score ${summary.score.toFixed(1)}`);
132
+ return;
133
+ }
134
+ if (action === 'compare') {
135
+ if (!node_fs_1.default.existsSync(baselineFile)) {
136
+ console.error('No baseline exists. Run "devkit baseline create" first.');
137
+ process.exitCode = 1;
138
+ return;
139
+ }
140
+ const previousBaseline = JSON.parse(node_fs_1.default.readFileSync(baselineFile, 'utf8'));
141
+ const current = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(root));
142
+ const previousScore = Number(previousBaseline.score ?? current.score);
143
+ console.log((0, reporters_1.formatBaselineCompare)(previousScore, current.score));
144
+ return;
145
+ }
146
+ console.error(`Unsupported baseline action: ${action}`);
147
+ process.exitCode = 1;
148
+ });
149
+ program
150
+ .command('fix')
151
+ .description('Preview findings marked as safe to fix')
152
+ .action(async () => {
153
+ const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(process.cwd()));
154
+ const safeFixes = summary.findings.filter((finding) => finding.fixAvailable);
155
+ console.log('Safe fix preview');
156
+ if (safeFixes.length === 0) {
157
+ console.log('No auto-fixable findings were detected.');
158
+ return;
159
+ }
160
+ for (const fix of safeFixes.slice(0, 10)) {
161
+ console.log(`- ${fix.ruleId}: ${fix.file}:${fix.line} - ${fix.message}`);
162
+ }
163
+ });
164
+ program.parseAsync(process.argv).catch((error) => {
165
+ console.error(error instanceof Error ? error.message : String(error));
166
+ process.exitCode = 1;
167
+ });