@eduardbar/drift 0.3.0 → 0.5.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/dist/cli.js CHANGED
@@ -4,7 +4,10 @@ import { writeFileSync } from 'node:fs';
4
4
  import { resolve } from 'node:path';
5
5
  import { analyzeProject } from './analyzer.js';
6
6
  import { buildReport, formatMarkdown, formatAIOutput } from './reporter.js';
7
- import { printConsole } from './printer.js';
7
+ import { printConsole, printDiff } from './printer.js';
8
+ import { loadConfig } from './config.js';
9
+ import { extractFilesAtRef, cleanupTempDir } from './git.js';
10
+ import { computeDiff } from './diff.js';
8
11
  const program = new Command();
9
12
  program
10
13
  .name('drift')
@@ -18,10 +21,11 @@ program
18
21
  .option('--ai', 'Output AI-optimized JSON for LLM consumption')
19
22
  .option('--fix', 'Show fix suggestions for each issue')
20
23
  .option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
21
- .action((targetPath, options) => {
24
+ .action(async (targetPath, options) => {
22
25
  const resolvedPath = resolve(targetPath ?? '.');
23
26
  process.stderr.write(`\nScanning ${resolvedPath}...\n`);
24
- const files = analyzeProject(resolvedPath);
27
+ const config = await loadConfig(resolvedPath);
28
+ const files = analyzeProject(resolvedPath, config);
25
29
  process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`);
26
30
  const report = buildReport(resolvedPath, files);
27
31
  if (options.ai) {
@@ -46,5 +50,50 @@ program
46
50
  process.exit(1);
47
51
  }
48
52
  });
53
+ program
54
+ .command('diff [ref]')
55
+ .description('Compare current state against a git ref (default: HEAD~1)')
56
+ .option('--json', 'Output raw JSON diff')
57
+ .action(async (ref, options) => {
58
+ const baseRef = ref ?? 'HEAD~1';
59
+ const projectPath = resolve('.');
60
+ let tempDir;
61
+ try {
62
+ process.stderr.write(`\nComputing diff: HEAD vs ${baseRef}...\n\n`);
63
+ // Scan current state
64
+ const config = await loadConfig(projectPath);
65
+ const currentFiles = analyzeProject(projectPath, config);
66
+ const currentReport = buildReport(projectPath, currentFiles);
67
+ // Extract base state from git
68
+ tempDir = extractFilesAtRef(projectPath, baseRef);
69
+ const baseFiles = analyzeProject(tempDir, config);
70
+ // Remap base file paths to match current project paths
71
+ // (temp dir paths → project paths for accurate comparison)
72
+ const baseReport = buildReport(tempDir, baseFiles);
73
+ const remappedBase = {
74
+ ...baseReport,
75
+ files: baseReport.files.map(f => ({
76
+ ...f,
77
+ path: f.path.replace(tempDir, projectPath),
78
+ })),
79
+ };
80
+ const diff = computeDiff(remappedBase, currentReport, baseRef);
81
+ if (options.json) {
82
+ process.stdout.write(JSON.stringify(diff, null, 2) + '\n');
83
+ }
84
+ else {
85
+ printDiff(diff);
86
+ }
87
+ }
88
+ catch (err) {
89
+ const message = err instanceof Error ? err.message : String(err);
90
+ process.stderr.write(`\n Error: ${message}\n\n`);
91
+ process.exit(1);
92
+ }
93
+ finally {
94
+ if (tempDir)
95
+ cleanupTempDir(tempDir);
96
+ }
97
+ });
49
98
  program.parse();
50
99
  //# sourceMappingURL=cli.js.map
@@ -0,0 +1,12 @@
1
+ import type { DriftConfig } from './types.js';
2
+ /**
3
+ * Load drift.config.ts / .js / .json from the given project root.
4
+ * Returns undefined if no config file is found.
5
+ *
6
+ * Search order (first match wins):
7
+ * 1. drift.config.ts
8
+ * 2. drift.config.js
9
+ * 3. drift.config.json
10
+ */
11
+ export declare function loadConfig(projectRoot: string): Promise<DriftConfig | undefined>;
12
+ //# sourceMappingURL=config.d.ts.map
package/dist/config.js ADDED
@@ -0,0 +1,40 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ /**
5
+ * Load drift.config.ts / .js / .json from the given project root.
6
+ * Returns undefined if no config file is found.
7
+ *
8
+ * Search order (first match wins):
9
+ * 1. drift.config.ts
10
+ * 2. drift.config.js
11
+ * 3. drift.config.json
12
+ */
13
+ export async function loadConfig(projectRoot) {
14
+ const candidates = [
15
+ join(projectRoot, 'drift.config.ts'),
16
+ join(projectRoot, 'drift.config.js'),
17
+ join(projectRoot, 'drift.config.json'),
18
+ ];
19
+ for (const candidate of candidates) {
20
+ if (!existsSync(candidate))
21
+ continue;
22
+ try {
23
+ const ext = candidate.split('.').pop();
24
+ if (ext === 'json') {
25
+ const { readFileSync } = await import('node:fs');
26
+ return JSON.parse(readFileSync(candidate, 'utf-8'));
27
+ }
28
+ // .ts / .js — dynamic import via file URL
29
+ const fileUrl = pathToFileURL(resolve(candidate)).href;
30
+ const mod = await import(fileUrl);
31
+ const config = mod.default ?? mod;
32
+ return config;
33
+ }
34
+ catch {
35
+ // drift-ignore: catch-swallow — config is optional; load failure is non-fatal
36
+ }
37
+ }
38
+ return undefined;
39
+ }
40
+ //# sourceMappingURL=config.js.map
package/dist/diff.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { DriftReport, DriftDiff } from './types.js';
2
+ /**
3
+ * Compute the diff between two DriftReports.
4
+ *
5
+ * Issues are matched by (rule + line + column) as a unique key within a file.
6
+ * A "new" issue exists in `current` but not in `base`.
7
+ * A "resolved" issue exists in `base` but not in `current`.
8
+ */
9
+ export declare function computeDiff(base: DriftReport, current: DriftReport, baseRef: string): DriftDiff;
10
+ //# sourceMappingURL=diff.d.ts.map
package/dist/diff.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Compute the diff between two DriftReports.
3
+ *
4
+ * Issues are matched by (rule + line + column) as a unique key within a file.
5
+ * A "new" issue exists in `current` but not in `base`.
6
+ * A "resolved" issue exists in `base` but not in `current`.
7
+ */
8
+ export function computeDiff(base, current, baseRef) {
9
+ const fileDiffs = [];
10
+ // Build a map of base files by path for O(1) lookup
11
+ const baseByPath = new Map(base.files.map(f => [f.path, f]));
12
+ const currentByPath = new Map(current.files.map(f => [f.path, f]));
13
+ // All unique paths across both reports
14
+ const allPaths = new Set([
15
+ ...base.files.map(f => f.path),
16
+ ...current.files.map(f => f.path),
17
+ ]);
18
+ for (const filePath of allPaths) {
19
+ const baseFile = baseByPath.get(filePath);
20
+ const currentFile = currentByPath.get(filePath);
21
+ const scoreBefore = baseFile?.score ?? 0;
22
+ const scoreAfter = currentFile?.score ?? 0;
23
+ const scoreDelta = scoreAfter - scoreBefore;
24
+ const baseIssues = baseFile?.issues ?? [];
25
+ const currentIssues = currentFile?.issues ?? [];
26
+ // Issue identity key: rule + line + column
27
+ const issueKey = (i) => `${i.rule}:${i.line}:${i.column}`;
28
+ const baseKeys = new Set(baseIssues.map(issueKey));
29
+ const currentKeys = new Set(currentIssues.map(issueKey));
30
+ const newIssues = currentIssues.filter(i => !baseKeys.has(issueKey(i)));
31
+ const resolvedIssues = baseIssues.filter(i => !currentKeys.has(issueKey(i)));
32
+ // Only include files that have actual changes
33
+ if (scoreDelta !== 0 || newIssues.length > 0 || resolvedIssues.length > 0) {
34
+ fileDiffs.push({
35
+ path: filePath,
36
+ scoreBefore,
37
+ scoreAfter,
38
+ scoreDelta,
39
+ newIssues,
40
+ resolvedIssues,
41
+ });
42
+ }
43
+ }
44
+ // Sort: most regressed first, then most improved last
45
+ fileDiffs.sort((a, b) => b.scoreDelta - a.scoreDelta);
46
+ return {
47
+ baseRef,
48
+ projectPath: current.targetPath,
49
+ scannedAt: new Date().toISOString(),
50
+ files: fileDiffs,
51
+ totalScoreBefore: base.totalScore,
52
+ totalScoreAfter: current.totalScore,
53
+ totalDelta: current.totalScore - base.totalScore,
54
+ newIssuesCount: fileDiffs.reduce((sum, f) => sum + f.newIssues.length, 0),
55
+ resolvedIssuesCount: fileDiffs.reduce((sum, f) => sum + f.resolvedIssues.length, 0),
56
+ };
57
+ }
58
+ //# sourceMappingURL=diff.js.map
package/dist/git.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Extract all TypeScript files from the project at a given git ref into a
3
+ * temporary directory. Returns the temp directory path.
4
+ *
5
+ * Uses `git ls-tree` to list files and `git show <ref>:<path>` to read each
6
+ * file — no checkout, no stash, no repo state mutation.
7
+ *
8
+ * Throws if the directory is not a git repo or the ref is invalid.
9
+ */
10
+ export declare function extractFilesAtRef(projectPath: string, ref: string): string;
11
+ /**
12
+ * Clean up a temporary directory created by extractFilesAtRef.
13
+ */
14
+ export declare function cleanupTempDir(tempDir: string): void;
15
+ /**
16
+ * Get the short hash of a git ref for display purposes.
17
+ */
18
+ export declare function resolveRefHash(projectPath: string, ref: string): string;
19
+ //# sourceMappingURL=git.d.ts.map
package/dist/git.js ADDED
@@ -0,0 +1,84 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
3
+ import { join, sep } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { randomUUID } from 'node:crypto';
6
+ /**
7
+ * Extract all TypeScript files from the project at a given git ref into a
8
+ * temporary directory. Returns the temp directory path.
9
+ *
10
+ * Uses `git ls-tree` to list files and `git show <ref>:<path>` to read each
11
+ * file — no checkout, no stash, no repo state mutation.
12
+ *
13
+ * Throws if the directory is not a git repo or the ref is invalid.
14
+ */
15
+ export function extractFilesAtRef(projectPath, ref) {
16
+ // Verify git repo
17
+ try {
18
+ execSync('git rev-parse --git-dir', { cwd: projectPath, stdio: 'pipe' });
19
+ }
20
+ catch {
21
+ throw new Error(`Not a git repository: ${projectPath}`);
22
+ }
23
+ // Verify ref exists
24
+ try {
25
+ execSync(`git rev-parse --verify ${ref}`, { cwd: projectPath, stdio: 'pipe' });
26
+ }
27
+ catch {
28
+ throw new Error(`Invalid git ref: '${ref}'. Run 'git log --oneline' to see available commits.`);
29
+ }
30
+ // List all .ts files tracked at this ref (excluding .d.ts)
31
+ let fileList;
32
+ try {
33
+ fileList = execSync(`git ls-tree -r --name-only ${ref}`, { cwd: projectPath, encoding: 'utf-8', stdio: 'pipe' });
34
+ }
35
+ catch {
36
+ throw new Error(`Failed to list files at ref '${ref}'`);
37
+ }
38
+ const tsFiles = fileList
39
+ .split('\n')
40
+ .map(f => f.trim())
41
+ .filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts'));
42
+ if (tsFiles.length === 0) {
43
+ throw new Error(`No TypeScript files found at ref '${ref}'`);
44
+ }
45
+ // Create temp directory
46
+ const tempDir = join(tmpdir(), `drift-diff-${randomUUID()}`);
47
+ mkdirSync(tempDir, { recursive: true });
48
+ // Extract each file
49
+ for (const filePath of tsFiles) {
50
+ let content;
51
+ try {
52
+ content = execSync(`git show ${ref}:${filePath}`, { cwd: projectPath, encoding: 'utf-8', stdio: 'pipe' });
53
+ }
54
+ catch {
55
+ // File may not exist at this ref — skip
56
+ continue;
57
+ }
58
+ const destPath = join(tempDir, filePath.split('/').join(sep));
59
+ const destDir = destPath.substring(0, destPath.lastIndexOf(sep));
60
+ mkdirSync(destDir, { recursive: true });
61
+ writeFileSync(destPath, content, 'utf-8');
62
+ }
63
+ return tempDir;
64
+ }
65
+ /**
66
+ * Clean up a temporary directory created by extractFilesAtRef.
67
+ */
68
+ export function cleanupTempDir(tempDir) {
69
+ if (existsSync(tempDir)) {
70
+ rmSync(tempDir, { recursive: true, force: true });
71
+ }
72
+ }
73
+ /**
74
+ * Get the short hash of a git ref for display purposes.
75
+ */
76
+ export function resolveRefHash(projectPath, ref) {
77
+ try {
78
+ return execSync(`git rev-parse --short ${ref}`, { cwd: projectPath, encoding: 'utf-8', stdio: 'pipe' }).trim();
79
+ }
80
+ catch {
81
+ return ref;
82
+ }
83
+ }
84
+ //# sourceMappingURL=git.js.map
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { analyzeProject, analyzeFile } from './analyzer.js';
2
2
  export { buildReport, formatMarkdown } from './reporter.js';
3
- export type { DriftReport, FileReport, DriftIssue } from './types.js';
3
+ export { computeDiff } from './diff.js';
4
+ export type { DriftReport, FileReport, DriftIssue, DriftDiff, FileDiff } from './types.js';
4
5
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { analyzeProject, analyzeFile } from './analyzer.js';
2
2
  export { buildReport, formatMarkdown } from './reporter.js';
3
+ export { computeDiff } from './diff.js';
3
4
  //# sourceMappingURL=index.js.map
package/dist/printer.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { DriftReport } from './types.js';
1
+ import type { DriftReport, DriftDiff } from './types.js';
2
2
  export declare function printConsole(report: DriftReport, options?: {
3
3
  showFix?: boolean;
4
4
  }): void;
5
+ export declare function printDiff(diff: DriftDiff): void;
5
6
  //# sourceMappingURL=printer.d.ts.map
package/dist/printer.js CHANGED
@@ -33,6 +33,84 @@ function formatFixSuggestion(issue) {
33
33
  'Consolidate with existing function',
34
34
  'Or rename to clarify different behavior',
35
35
  ],
36
+ 'high-complexity': [
37
+ 'Extract each branch into a named function',
38
+ 'Use early returns to reduce nesting and branching',
39
+ 'Consider a strategy pattern or lookup table for switch-heavy logic',
40
+ ],
41
+ 'deep-nesting': [
42
+ 'Invert conditions and return early instead of nesting',
43
+ 'Extract inner blocks into separate functions',
44
+ 'Flatten promise chains with async/await',
45
+ ],
46
+ 'too-many-params': [
47
+ 'Group related params into an options object: foo({ a, b, c, d, e })',
48
+ 'Consider if this function is doing too many things',
49
+ ],
50
+ 'high-coupling': [
51
+ 'Group related imports into a single module',
52
+ 'Consider if this file has too many responsibilities',
53
+ 'Extract a sub-module that encapsulates some of these dependencies',
54
+ ],
55
+ 'promise-style-mix': [
56
+ 'Pick one style and use it consistently: async/await is preferred',
57
+ 'Convert .then()/.catch() chains to async/await',
58
+ ],
59
+ 'magic-number': [
60
+ 'Extract to a named constant: const MAX_RETRIES = 3',
61
+ 'Use an enum for related numeric values',
62
+ ],
63
+ 'comment-contradiction': [
64
+ 'Remove the comment — the code already says what it does',
65
+ 'Replace with a comment explaining WHY, not what: // retry because upstream is flaky',
66
+ ],
67
+ 'unused-export': [
68
+ "Remove the export keyword if it's only used internally",
69
+ 'Or delete the declaration entirely if it serves no purpose',
70
+ ],
71
+ 'dead-file': [
72
+ 'Delete the file if it is no longer needed',
73
+ 'Or import it from an entry point if it should be active',
74
+ ],
75
+ 'unused-dependency': [
76
+ 'Remove it from package.json: npm uninstall <pkg>',
77
+ 'Or verify it is used transitively and document why it is kept',
78
+ ],
79
+ 'circular-dependency': [
80
+ 'Introduce an abstraction (interface or shared module) that both files depend on',
81
+ 'Move shared logic to a third file that neither of the cyclic modules imports',
82
+ 'Use dependency injection to break the compile-time dependency',
83
+ ],
84
+ 'layer-violation': [
85
+ 'Move the import to a layer that is allowed to access this dependency',
86
+ 'Introduce a port/interface in the domain layer to invert the dependency (Dependency Inversion Principle)',
87
+ 'Or adjust the layer rules in drift.config.ts if this import is intentional',
88
+ ],
89
+ 'cross-boundary-import': [
90
+ "Import from the module's public API barrel (index.ts) instead of internal paths",
91
+ 'Or add the module to allowedExternalImports in drift.config.ts if this is intentional',
92
+ 'Consider using dependency injection or an event bus to decouple the modules',
93
+ ],
94
+ 'over-commented': [
95
+ 'Remove comments that restate what the code already expresses clearly',
96
+ 'Keep only comments that explain WHY, not WHAT — prefer self-documenting names',
97
+ ],
98
+ 'hardcoded-config': [
99
+ 'Move the value to an environment variable: process.env.YOUR_VAR',
100
+ 'Or extract it to a config file / constants module imported at the top',
101
+ ],
102
+ 'inconsistent-error-handling': [
103
+ 'Pick one style (async/await + try/catch is preferred) and apply it consistently',
104
+ 'Avoid mixing .then()/.catch() with await in the same file',
105
+ ],
106
+ 'unnecessary-abstraction': [
107
+ 'Inline the abstraction if it has only one implementation and is never reused',
108
+ 'Or document why the extension point exists (e.g., future plugin system)',
109
+ ],
110
+ 'naming-inconsistency': [
111
+ 'Pick one naming convention (camelCase for variables/functions, PascalCase for types)',
112
+ 'Rename snake_case identifiers to camelCase to match TypeScript conventions',
113
+ ],
36
114
  };
37
115
  return suggestions[issue.rule] ?? ['Review and fix manually'];
38
116
  }
@@ -104,4 +182,54 @@ export function printConsole(report, options) {
104
182
  console.log();
105
183
  }
106
184
  }
185
+ export function printDiff(diff) {
186
+ const { totalDelta, totalScoreBefore, totalScoreAfter, newIssuesCount, resolvedIssuesCount } = diff;
187
+ const deltaSign = totalDelta > 0 ? '+' : '';
188
+ const deltaColor = totalDelta > 0 ? kleur.red : totalDelta < 0 ? kleur.green : kleur.white;
189
+ const baseGrade = scoreToGrade(totalScoreBefore);
190
+ const headGrade = scoreToGrade(totalScoreAfter);
191
+ console.log();
192
+ console.log(kleur.bold(' drift diff') + kleur.gray(` — comparing HEAD vs ${diff.baseRef}`));
193
+ console.log(' ' + '─'.repeat(50));
194
+ console.log();
195
+ console.log(` Score ${kleur.bold(String(totalScoreBefore))} ${baseGrade.badge} → ` +
196
+ `${kleur.bold(String(totalScoreAfter))} ${headGrade.badge} ` +
197
+ deltaColor(`(${deltaSign}${totalDelta})`));
198
+ console.log();
199
+ if (newIssuesCount > 0) {
200
+ console.log(` ${kleur.red(`▲ ${newIssuesCount} new issue${newIssuesCount !== 1 ? 's' : ''} introduced`)}`);
201
+ }
202
+ if (resolvedIssuesCount > 0) {
203
+ console.log(` ${kleur.green(`▼ ${resolvedIssuesCount} issue${resolvedIssuesCount !== 1 ? 's' : ''} resolved`)}`);
204
+ }
205
+ if (newIssuesCount === 0 && resolvedIssuesCount === 0) {
206
+ console.log(` ${kleur.gray('No issue changes detected')}`);
207
+ }
208
+ if (diff.files.length === 0) {
209
+ console.log();
210
+ console.log(` ${kleur.gray('No file-level changes detected')}`);
211
+ console.log();
212
+ return;
213
+ }
214
+ console.log();
215
+ console.log(' ' + '─'.repeat(50));
216
+ console.log();
217
+ for (const file of diff.files) {
218
+ const rel = file.path.replace(/\\/g, '/').split('/').pop() ?? file.path;
219
+ const fileDeltaSign = file.scoreDelta > 0 ? '+' : '';
220
+ const fileDeltaColor = file.scoreDelta > 0 ? kleur.red : kleur.green;
221
+ console.log(` ${kleur.bold(rel)}` +
222
+ ` ${kleur.gray(`${file.scoreBefore} → ${file.scoreAfter}`)}` +
223
+ ` ${fileDeltaColor(`${fileDeltaSign}${file.scoreDelta}`)}`);
224
+ for (const issue of file.newIssues) {
225
+ console.log(` ${kleur.red('+')} ${severityIcon(issue.severity)} ` +
226
+ `${kleur.yellow(issue.rule)} ${kleur.gray(`L${issue.line}`)} ${issue.message}`);
227
+ }
228
+ for (const issue of file.resolvedIssues) {
229
+ console.log(` ${kleur.green('-')} ${severityIcon(issue.severity)} ` +
230
+ `${kleur.yellow(issue.rule)} ${kleur.gray(`L${issue.line}`)} ${issue.message}`);
231
+ }
232
+ console.log();
233
+ }
234
+ }
107
235
  //# sourceMappingURL=printer.js.map
package/dist/types.d.ts CHANGED
@@ -52,4 +52,47 @@ export interface AIIssue {
52
52
  fix_suggestion: string;
53
53
  effort: 'low' | 'medium' | 'high';
54
54
  }
55
+ /**
56
+ * Layer definition for architectural boundary enforcement.
57
+ */
58
+ export interface LayerDefinition {
59
+ name: string;
60
+ patterns: string[];
61
+ canImportFrom: string[];
62
+ }
63
+ /**
64
+ * Module boundary definition for cross-boundary enforcement.
65
+ */
66
+ export interface ModuleBoundary {
67
+ name: string;
68
+ root: string;
69
+ allowedExternalImports?: string[];
70
+ }
71
+ /**
72
+ * Optional project-level configuration for drift.
73
+ * Place in drift.config.ts (or .js / .json) at the project root.
74
+ */
75
+ export interface DriftConfig {
76
+ layers?: LayerDefinition[];
77
+ modules?: ModuleBoundary[];
78
+ }
79
+ export interface FileDiff {
80
+ path: string;
81
+ scoreBefore: number;
82
+ scoreAfter: number;
83
+ scoreDelta: number;
84
+ newIssues: DriftIssue[];
85
+ resolvedIssues: DriftIssue[];
86
+ }
87
+ export interface DriftDiff {
88
+ baseRef: string;
89
+ projectPath: string;
90
+ scannedAt: string;
91
+ files: FileDiff[];
92
+ totalScoreBefore: number;
93
+ totalScoreAfter: number;
94
+ totalDelta: number;
95
+ newIssuesCount: number;
96
+ resolvedIssuesCount: number;
97
+ }
55
98
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eduardbar/drift",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Detect silent technical debt left by AI-generated code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,6 +23,14 @@
23
23
  ],
24
24
  "author": "eduardbar",
25
25
  "license": "MIT",
26
+ "homepage": "https://github.com/eduardbar/drift#readme",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/eduardbar/drift.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/eduardbar/drift/issues"
33
+ },
26
34
  "dependencies": {
27
35
  "commander": "^14.0.3",
28
36
  "kleur": "^4.1.5",