@rahul05ranjan/dhruv-cli 1.6.1 → 1.8.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/CHANGELOG.md +3 -3
- package/LICENSE +21 -0
- package/__tests__/source-bundle.test.ts +135 -0
- package/__tests__/workflows.test.ts +15 -0
- package/dist/core/source-bundle.d.ts +18 -0
- package/dist/core/source-bundle.js +147 -0
- package/package.json +2 -1
- package/src/core/source-bundle.ts +180 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# [1.4.0](https://github.com/rahul05ranjan/dhruv-cli/compare/v1.
|
|
1
|
+
# [1.4.0](https://github.com/rahul05ranjan/dhruv-cli/compare/v1.7.0...v1.4.0) (2026-09-21)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Features
|
|
5
5
|
|
|
6
|
-
* **
|
|
6
|
+
* **distribution:** add canonical MIT LICENSE and package.json license metadata (closes [#107](https://github.com/rahul05ranjan/dhruv-cli/issues/107)) ([5ac966d](https://github.com/rahul05ranjan/dhruv-cli/commit/5ac966dedc091e271e6f2aff5b8e8a8ddad86091))
|
|
7
7
|
# Changelog
|
|
8
8
|
|
|
9
9
|
## [1.1.1](https://github.com/rahul05ranjan/dhruv-cli/compare/v1.1.0...v1.1.1) (2025-06-29)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rahul Ranjan
|
|
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.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it, jest, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { loadSource } from '../src/core/source-bundle';
|
|
7
|
+
|
|
8
|
+
jest.mock('../src/utils/ux', () => ({
|
|
9
|
+
printError: jest.fn(),
|
|
10
|
+
printInfo: jest.fn(),
|
|
11
|
+
printSuccess: jest.fn(),
|
|
12
|
+
printWarning: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe('Source Ingestion Module (loadSource)', () => {
|
|
16
|
+
let root: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'source-bundle-test-'));
|
|
20
|
+
process.exitCode = 0;
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
26
|
+
process.exitCode = 0;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('loads a single source file', () => {
|
|
30
|
+
const file = path.join(root, 'main.ts');
|
|
31
|
+
fs.writeFileSync(file, 'console.log("hello world");');
|
|
32
|
+
|
|
33
|
+
const bundle = loadSource(file);
|
|
34
|
+
|
|
35
|
+
expect(bundle).not.toBeNull();
|
|
36
|
+
expect(bundle?.isDiff).toBe(false);
|
|
37
|
+
expect(bundle?.capped).toBe(false);
|
|
38
|
+
expect(bundle?.files).toHaveLength(1);
|
|
39
|
+
expect(bundle?.files[0].path).toBe('main.ts');
|
|
40
|
+
expect(bundle?.files[0].content).toBe('console.log("hello world");');
|
|
41
|
+
expect(bundle?.promptContent).toBe('console.log("hello world");');
|
|
42
|
+
expect(process.exitCode).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('sets process.exitCode = 1 and returns null when target does not exist', () => {
|
|
46
|
+
const nonExistent = path.join(root, 'nonexistent.ts');
|
|
47
|
+
|
|
48
|
+
const bundle = loadSource(nonExistent);
|
|
49
|
+
|
|
50
|
+
expect(bundle).toBeNull();
|
|
51
|
+
expect(process.exitCode).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('recursively loads directory files while pruning dependency and build directories', () => {
|
|
55
|
+
fs.mkdirSync(path.join(root, 'src', 'deep'), { recursive: true });
|
|
56
|
+
fs.mkdirSync(path.join(root, 'node_modules', 'lib'), { recursive: true });
|
|
57
|
+
fs.mkdirSync(path.join(root, '.next'), { recursive: true });
|
|
58
|
+
fs.mkdirSync(path.join(root, 'dist'), { recursive: true });
|
|
59
|
+
|
|
60
|
+
fs.writeFileSync(path.join(root, 'src', 'index.ts'), 'export const a = 1;');
|
|
61
|
+
fs.writeFileSync(path.join(root, 'src', 'deep', 'util.js'), 'export const b = 2;');
|
|
62
|
+
fs.writeFileSync(path.join(root, 'node_modules', 'lib', 'index.js'), 'ignored');
|
|
63
|
+
fs.writeFileSync(path.join(root, '.next', 'bundle.js'), 'ignored');
|
|
64
|
+
fs.writeFileSync(path.join(root, 'dist', 'out.js'), 'ignored');
|
|
65
|
+
fs.writeFileSync(path.join(root, 'readme.md'), '# Markdown ignored');
|
|
66
|
+
|
|
67
|
+
const bundle = loadSource(root);
|
|
68
|
+
|
|
69
|
+
expect(bundle).not.toBeNull();
|
|
70
|
+
expect(bundle?.files).toHaveLength(2);
|
|
71
|
+
const paths = bundle?.files.map((f: { path: string }) => f.path.replace(/\\/g, '/')).sort();
|
|
72
|
+
expect(paths).toEqual(['src/deep/util.js', 'src/index.ts']);
|
|
73
|
+
expect(bundle?.promptContent).toContain('// File: src/index.ts');
|
|
74
|
+
expect(bundle?.promptContent).toContain('// File: src/deep/util.js');
|
|
75
|
+
expect(bundle?.capped).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('caps directory collection at maxFiles and sets capped to true', () => {
|
|
79
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
80
|
+
for (let i = 0; i < 15; i++) {
|
|
81
|
+
fs.writeFileSync(path.join(root, 'src', `file_${String(i).padStart(2, '0')}.ts`), `const v = ${i};`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const bundle = loadSource(root, { maxFiles: 10 });
|
|
85
|
+
|
|
86
|
+
expect(bundle).not.toBeNull();
|
|
87
|
+
expect(bundle?.files).toHaveLength(10);
|
|
88
|
+
expect(bundle?.capped).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns null and sets process.exitCode = 1 when directory contains no code files', () => {
|
|
92
|
+
fs.writeFileSync(path.join(root, 'notes.txt'), 'text only');
|
|
93
|
+
|
|
94
|
+
const bundle = loadSource(root);
|
|
95
|
+
|
|
96
|
+
expect(bundle).toBeNull();
|
|
97
|
+
expect(process.exitCode).toBe(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('loads git diff when diff option is specified', () => {
|
|
101
|
+
execFileSync('git', ['init'], { cwd: root });
|
|
102
|
+
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: root });
|
|
103
|
+
execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: root });
|
|
104
|
+
|
|
105
|
+
const file = path.join(root, 'code.ts');
|
|
106
|
+
fs.writeFileSync(file, 'const initial = 1;\n');
|
|
107
|
+
execFileSync('git', ['add', '.'], { cwd: root });
|
|
108
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: root });
|
|
109
|
+
|
|
110
|
+
fs.appendFileSync(file, 'const changed = 2;\n');
|
|
111
|
+
|
|
112
|
+
const bundle = loadSource(root, { diff: true });
|
|
113
|
+
|
|
114
|
+
expect(bundle).not.toBeNull();
|
|
115
|
+
expect(bundle?.isDiff).toBe(true);
|
|
116
|
+
expect(bundle?.promptContent).toContain('+const changed = 2;');
|
|
117
|
+
expect(process.exitCode).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns null and sets exitCode = 1 when git diff is empty', () => {
|
|
121
|
+
execFileSync('git', ['init'], { cwd: root });
|
|
122
|
+
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: root });
|
|
123
|
+
execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: root });
|
|
124
|
+
|
|
125
|
+
const file = path.join(root, 'code.ts');
|
|
126
|
+
fs.writeFileSync(file, 'const initial = 1;\n');
|
|
127
|
+
execFileSync('git', ['add', '.'], { cwd: root });
|
|
128
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: root });
|
|
129
|
+
|
|
130
|
+
const bundle = loadSource(root, { diff: true });
|
|
131
|
+
|
|
132
|
+
expect(bundle).toBeNull();
|
|
133
|
+
expect(process.exitCode).toBe(1);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -116,3 +116,18 @@ describe('security workflow requirements', () => {
|
|
|
116
116
|
}
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
describe('package distribution and licensing compliance', () => {
|
|
121
|
+
it('defines an explicit MIT license in package.json', () => {
|
|
122
|
+
const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
|
|
123
|
+
expect(pkg.license).toBe('MIT');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('provides a canonical root LICENSE file with copyright notice', () => {
|
|
127
|
+
const licensePath = resolve(root, 'LICENSE');
|
|
128
|
+
expect(existsSync(licensePath)).toBe(true);
|
|
129
|
+
const content = readFileSync(licensePath, 'utf8');
|
|
130
|
+
expect(content).toContain('MIT License');
|
|
131
|
+
expect(content).toContain('Copyright (c) 2026 Rahul Ranjan');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const CODE_FILE: RegExp;
|
|
2
|
+
export declare const IGNORED_DIRECTORIES: Set<string>;
|
|
3
|
+
export interface SourceFile {
|
|
4
|
+
path: string;
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SourceBundle {
|
|
8
|
+
target: string;
|
|
9
|
+
files: SourceFile[];
|
|
10
|
+
isDiff: boolean;
|
|
11
|
+
capped: boolean;
|
|
12
|
+
promptContent: string;
|
|
13
|
+
}
|
|
14
|
+
export interface LoadSourceOptions {
|
|
15
|
+
diff?: boolean;
|
|
16
|
+
maxFiles?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function loadSource(target: string, options?: LoadSourceOptions): SourceBundle | null;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
import { printError, printInfo } from '../utils/ux.js';
|
|
5
|
+
export const CODE_FILE = /\.(js|ts|jsx|tsx|py|java|cpp|c|go|rs|rb|php)$/;
|
|
6
|
+
export const IGNORED_DIRECTORIES = new Set([
|
|
7
|
+
'.git',
|
|
8
|
+
'node_modules',
|
|
9
|
+
'dist',
|
|
10
|
+
'build',
|
|
11
|
+
'coverage',
|
|
12
|
+
'.dhruv-cache',
|
|
13
|
+
'logs',
|
|
14
|
+
'.next',
|
|
15
|
+
'.turbo',
|
|
16
|
+
'__pycache__',
|
|
17
|
+
'.pytest_cache',
|
|
18
|
+
'target',
|
|
19
|
+
'vendor',
|
|
20
|
+
]);
|
|
21
|
+
export function loadSource(target, options = {}) {
|
|
22
|
+
if (options.diff) {
|
|
23
|
+
return loadGitDiff(target);
|
|
24
|
+
}
|
|
25
|
+
let stat;
|
|
26
|
+
try {
|
|
27
|
+
stat = fs.statSync(target);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
printError(`Path "${target}" does not exist or could not be read.`);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (stat.isFile()) {
|
|
35
|
+
return loadSingleFile(target);
|
|
36
|
+
}
|
|
37
|
+
if (stat.isDirectory()) {
|
|
38
|
+
return loadDirectory(target, options.maxFiles ?? 10);
|
|
39
|
+
}
|
|
40
|
+
printError(`Path "${target}" is neither a file nor a directory.`);
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function loadSingleFile(target) {
|
|
45
|
+
try {
|
|
46
|
+
const content = fs.readFileSync(target, 'utf-8');
|
|
47
|
+
const relativePath = path.basename(target);
|
|
48
|
+
return {
|
|
49
|
+
target,
|
|
50
|
+
files: [{ path: relativePath, content }],
|
|
51
|
+
isDiff: false,
|
|
52
|
+
capped: false,
|
|
53
|
+
promptContent: content,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
printError(`Path "${target}" does not exist or could not be read.`);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function loadDirectory(dir, maxFiles) {
|
|
63
|
+
const collectedFiles = [];
|
|
64
|
+
let capped = false;
|
|
65
|
+
function collect(current) {
|
|
66
|
+
if (collectedFiles.length >= maxFiles) {
|
|
67
|
+
capped = true;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (collectedFiles.length >= maxFiles) {
|
|
79
|
+
capped = true;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const absolute = path.join(current, entry.name);
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
85
|
+
collect(absolute);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (entry.isFile() && CODE_FILE.test(entry.name)) {
|
|
89
|
+
try {
|
|
90
|
+
const content = fs.readFileSync(absolute, 'utf-8');
|
|
91
|
+
const relPath = path.relative(dir, absolute).split(path.sep).join('/');
|
|
92
|
+
collectedFiles.push({ path: relPath, content });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// ignore unreadable file
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
collect(dir);
|
|
101
|
+
if (collectedFiles.length === 0) {
|
|
102
|
+
printError(`No code files found in directory "${dir}".`);
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (capped) {
|
|
107
|
+
printInfo(`Note: Directory review is capped at the first ${maxFiles} source files.`);
|
|
108
|
+
}
|
|
109
|
+
let promptContent = '';
|
|
110
|
+
for (const f of collectedFiles) {
|
|
111
|
+
promptContent += `\n// File: ${f.path}\n${f.content}\n`;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
target: dir,
|
|
115
|
+
files: collectedFiles,
|
|
116
|
+
isDiff: false,
|
|
117
|
+
capped,
|
|
118
|
+
promptContent,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function loadGitDiff(fileOrDir) {
|
|
122
|
+
const root = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
|
|
123
|
+
try {
|
|
124
|
+
const diff = execFileSync('git', ['diff', '--no-ext-diff', '--unified=80', '--'], {
|
|
125
|
+
cwd: root,
|
|
126
|
+
encoding: 'utf8',
|
|
127
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
128
|
+
});
|
|
129
|
+
if (!diff.trim()) {
|
|
130
|
+
printError(`No uncommitted changes found in "${fileOrDir}".`);
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
target: fileOrDir,
|
|
136
|
+
files: [],
|
|
137
|
+
isDiff: true,
|
|
138
|
+
capped: false,
|
|
139
|
+
promptContent: diff,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
printError(`Could not read a git diff for "${fileOrDir}".`);
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
import { printError, printInfo } from '../utils/ux.js';
|
|
5
|
+
|
|
6
|
+
export const CODE_FILE = /\.(js|ts|jsx|tsx|py|java|cpp|c|go|rs|rb|php)$/;
|
|
7
|
+
|
|
8
|
+
export const IGNORED_DIRECTORIES = new Set([
|
|
9
|
+
'.git',
|
|
10
|
+
'node_modules',
|
|
11
|
+
'dist',
|
|
12
|
+
'build',
|
|
13
|
+
'coverage',
|
|
14
|
+
'.dhruv-cache',
|
|
15
|
+
'logs',
|
|
16
|
+
'.next',
|
|
17
|
+
'.turbo',
|
|
18
|
+
'__pycache__',
|
|
19
|
+
'.pytest_cache',
|
|
20
|
+
'target',
|
|
21
|
+
'vendor',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export interface SourceFile {
|
|
25
|
+
path: string;
|
|
26
|
+
content: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SourceBundle {
|
|
30
|
+
target: string;
|
|
31
|
+
files: SourceFile[];
|
|
32
|
+
isDiff: boolean;
|
|
33
|
+
capped: boolean;
|
|
34
|
+
promptContent: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LoadSourceOptions {
|
|
38
|
+
diff?: boolean;
|
|
39
|
+
maxFiles?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function loadSource(target: string, options: LoadSourceOptions = {}): SourceBundle | null {
|
|
43
|
+
if (options.diff) {
|
|
44
|
+
return loadGitDiff(target);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let stat: fs.Stats;
|
|
48
|
+
try {
|
|
49
|
+
stat = fs.statSync(target);
|
|
50
|
+
} catch {
|
|
51
|
+
printError(`Path "${target}" does not exist or could not be read.`);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (stat.isFile()) {
|
|
57
|
+
return loadSingleFile(target);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (stat.isDirectory()) {
|
|
61
|
+
return loadDirectory(target, options.maxFiles ?? 10);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
printError(`Path "${target}" is neither a file nor a directory.`);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function loadSingleFile(target: string): SourceBundle | null {
|
|
70
|
+
try {
|
|
71
|
+
const content = fs.readFileSync(target, 'utf-8');
|
|
72
|
+
const relativePath = path.basename(target);
|
|
73
|
+
return {
|
|
74
|
+
target,
|
|
75
|
+
files: [{ path: relativePath, content }],
|
|
76
|
+
isDiff: false,
|
|
77
|
+
capped: false,
|
|
78
|
+
promptContent: content,
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
printError(`Path "${target}" does not exist or could not be read.`);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function loadDirectory(dir: string, maxFiles: number): SourceBundle | null {
|
|
88
|
+
const collectedFiles: SourceFile[] = [];
|
|
89
|
+
let capped = false;
|
|
90
|
+
|
|
91
|
+
function collect(current: string): void {
|
|
92
|
+
if (collectedFiles.length >= maxFiles) {
|
|
93
|
+
capped = true;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let entries: fs.Dirent[];
|
|
98
|
+
try {
|
|
99
|
+
entries = fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (collectedFiles.length >= maxFiles) {
|
|
106
|
+
capped = true;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const absolute = path.join(current, entry.name);
|
|
111
|
+
if (entry.isDirectory()) {
|
|
112
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
113
|
+
collect(absolute);
|
|
114
|
+
}
|
|
115
|
+
} else if (entry.isFile() && CODE_FILE.test(entry.name)) {
|
|
116
|
+
try {
|
|
117
|
+
const content = fs.readFileSync(absolute, 'utf-8');
|
|
118
|
+
const relPath = path.relative(dir, absolute).split(path.sep).join('/');
|
|
119
|
+
collectedFiles.push({ path: relPath, content });
|
|
120
|
+
} catch {
|
|
121
|
+
// ignore unreadable file
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
collect(dir);
|
|
128
|
+
|
|
129
|
+
if (collectedFiles.length === 0) {
|
|
130
|
+
printError(`No code files found in directory "${dir}".`);
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (capped) {
|
|
136
|
+
printInfo(`Note: Directory review is capped at the first ${maxFiles} source files.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let promptContent = '';
|
|
140
|
+
for (const f of collectedFiles) {
|
|
141
|
+
promptContent += `\n// File: ${f.path}\n${f.content}\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
target: dir,
|
|
146
|
+
files: collectedFiles,
|
|
147
|
+
isDiff: false,
|
|
148
|
+
capped,
|
|
149
|
+
promptContent,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function loadGitDiff(fileOrDir: string): SourceBundle | null {
|
|
154
|
+
const root = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
|
|
155
|
+
try {
|
|
156
|
+
const diff = execFileSync('git', ['diff', '--no-ext-diff', '--unified=80', '--'], {
|
|
157
|
+
cwd: root,
|
|
158
|
+
encoding: 'utf8',
|
|
159
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!diff.trim()) {
|
|
163
|
+
printError(`No uncommitted changes found in "${fileOrDir}".`);
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
target: fileOrDir,
|
|
170
|
+
files: [],
|
|
171
|
+
isDiff: true,
|
|
172
|
+
capped: false,
|
|
173
|
+
promptContent: diff,
|
|
174
|
+
};
|
|
175
|
+
} catch {
|
|
176
|
+
printError(`Could not read a git diff for "${fileOrDir}".`);
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|