cadr-cli 2.0.0 → 2.0.2
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/adr/adr.d.ts +17 -0
- package/dist/adr/adr.d.ts.map +1 -0
- package/dist/{adr.js → adr/adr.js} +4 -44
- package/dist/adr/adr.js.map +1 -0
- package/dist/adr/adr.test.d.ts +5 -0
- package/dist/{adr.test.d.ts.map → adr/adr.test.d.ts.map} +1 -1
- package/dist/{adr.test.js → adr/adr.test.js} +0 -14
- package/dist/adr/adr.test.js.map +1 -0
- package/dist/adr/index.d.ts +2 -0
- package/dist/adr/index.d.ts.map +1 -0
- package/dist/adr/index.js +18 -0
- package/dist/adr/index.js.map +1 -0
- package/dist/analysis/analysis.orchestrator.d.ts +14 -0
- package/dist/analysis/analysis.orchestrator.d.ts.map +1 -0
- package/dist/analysis/analysis.orchestrator.js +175 -0
- package/dist/analysis/analysis.orchestrator.js.map +1 -0
- package/dist/analysis/analysis.orchestrator.test.d.ts +2 -0
- package/dist/analysis/analysis.orchestrator.test.d.ts.map +1 -0
- package/dist/analysis/analysis.orchestrator.test.js +177 -0
- package/dist/analysis/analysis.orchestrator.test.js.map +1 -0
- package/dist/analysis/strategies/git-strategy.d.ts +22 -0
- package/dist/analysis/strategies/git-strategy.d.ts.map +1 -0
- package/dist/analysis/strategies/git-strategy.js +114 -0
- package/dist/analysis/strategies/git-strategy.js.map +1 -0
- package/dist/analysis/strategies/git-strategy.test.d.ts +2 -0
- package/dist/analysis/strategies/git-strategy.test.d.ts.map +1 -0
- package/dist/analysis/strategies/git-strategy.test.js +147 -0
- package/dist/analysis/strategies/git-strategy.test.js.map +1 -0
- package/dist/commands/analyze.js +3 -3
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/analyze.test.d.ts +2 -0
- package/dist/commands/analyze.test.d.ts.map +1 -0
- package/dist/commands/analyze.test.js +70 -0
- package/dist/commands/analyze.test.js.map +1 -0
- package/dist/commands/init.test.js +128 -2
- package/dist/commands/init.test.js.map +1 -1
- package/dist/config.test.js +167 -0
- package/dist/config.test.js.map +1 -1
- package/dist/git/git.errors.d.ts +6 -0
- package/dist/git/git.errors.d.ts.map +1 -0
- package/dist/git/git.errors.js +15 -0
- package/dist/git/git.errors.js.map +1 -0
- package/dist/git/git.errors.test.d.ts +2 -0
- package/dist/git/git.errors.test.d.ts.map +1 -0
- package/dist/git/git.errors.test.js +34 -0
- package/dist/git/git.errors.test.js.map +1 -0
- package/dist/git/git.operations.d.ts +12 -0
- package/dist/git/git.operations.d.ts.map +1 -0
- package/dist/git/git.operations.js +64 -0
- package/dist/git/git.operations.js.map +1 -0
- package/dist/git/git.operations.test.d.ts +2 -0
- package/dist/git/git.operations.test.d.ts.map +1 -0
- package/dist/git/git.operations.test.js +164 -0
- package/dist/git/git.operations.test.js.map +1 -0
- package/dist/git/index.d.ts +4 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +19 -0
- package/dist/git/index.js.map +1 -0
- package/dist/llm/index.d.ts +3 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +19 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/llm.d.ts +35 -0
- package/dist/llm/llm.d.ts.map +1 -0
- package/dist/{llm.js → llm/llm.js} +16 -58
- package/dist/llm/llm.js.map +1 -0
- package/dist/{llm.test.d.ts.map → llm/llm.test.d.ts.map} +1 -1
- package/dist/llm/llm.test.js +224 -0
- package/dist/llm/llm.test.js.map +1 -0
- package/dist/{prompts.d.ts → llm/prompts.d.ts} +1 -38
- package/dist/llm/prompts.d.ts.map +1 -0
- package/dist/{prompts.js → llm/prompts.js} +9 -54
- package/dist/llm/prompts.js.map +1 -0
- package/dist/llm/response-parser.d.ts +9 -0
- package/dist/llm/response-parser.d.ts.map +1 -0
- package/dist/llm/response-parser.js +67 -0
- package/dist/llm/response-parser.js.map +1 -0
- package/dist/llm/response-parser.test.d.ts +2 -0
- package/dist/llm/response-parser.test.d.ts.map +1 -0
- package/dist/llm/response-parser.test.js +134 -0
- package/dist/llm/response-parser.test.js.map +1 -0
- package/dist/presenters/console-presenter.d.ts +35 -0
- package/dist/presenters/console-presenter.d.ts.map +1 -0
- package/dist/presenters/console-presenter.js +114 -0
- package/dist/presenters/console-presenter.js.map +1 -0
- package/dist/presenters/console-presenter.test.d.ts +2 -0
- package/dist/presenters/console-presenter.test.d.ts.map +1 -0
- package/dist/presenters/console-presenter.test.js +227 -0
- package/dist/presenters/console-presenter.test.js.map +1 -0
- package/dist/version.test.d.ts +1 -2
- package/dist/version.test.d.ts.map +1 -1
- package/dist/version.test.js +29 -16
- package/dist/version.test.js.map +1 -1
- package/package.json +1 -1
- package/src/{adr.test.ts → adr/adr.test.ts} +10 -23
- package/src/{adr.ts → adr/adr.ts} +7 -48
- package/src/adr/index.ts +1 -0
- package/src/analysis/analysis.orchestrator.test.ts +237 -0
- package/src/analysis/analysis.orchestrator.ts +175 -0
- package/src/analysis/strategies/git-strategy.test.ts +210 -0
- package/src/analysis/strategies/git-strategy.ts +106 -0
- package/src/commands/analyze.test.ts +91 -0
- package/src/commands/analyze.ts +8 -9
- package/src/commands/init.test.ts +200 -5
- package/src/config.test.ts +232 -2
- package/src/git/git.errors.test.ts +43 -0
- package/src/git/git.errors.ts +10 -0
- package/src/git/git.operations.test.ts +222 -0
- package/src/git/git.operations.ts +85 -0
- package/src/git/index.ts +3 -0
- package/src/llm/index.ts +2 -0
- package/src/llm/llm.test.ts +315 -0
- package/src/{llm.ts → llm/llm.ts} +46 -107
- package/src/{prompts.ts → llm/prompts.ts} +30 -72
- package/src/llm/response-parser.test.ts +170 -0
- package/src/llm/response-parser.ts +90 -0
- package/src/presenters/console-presenter.test.ts +259 -0
- package/src/presenters/console-presenter.ts +152 -0
- package/src/version.test.ts +30 -16
- package/dist/adr.d.ts +0 -50
- package/dist/adr.d.ts.map +0 -1
- package/dist/adr.js.map +0 -1
- package/dist/adr.test.d.ts +0 -8
- package/dist/adr.test.js.map +0 -1
- package/dist/analysis.d.ts +0 -24
- package/dist/analysis.d.ts.map +0 -1
- package/dist/analysis.js +0 -281
- package/dist/analysis.js.map +0 -1
- package/dist/analysis.test.d.ts +0 -8
- package/dist/analysis.test.d.ts.map +0 -1
- package/dist/analysis.test.js +0 -351
- package/dist/analysis.test.js.map +0 -1
- package/dist/git.d.ts +0 -54
- package/dist/git.d.ts.map +0 -1
- package/dist/git.js +0 -204
- package/dist/git.js.map +0 -1
- package/dist/llm.d.ts +0 -73
- package/dist/llm.d.ts.map +0 -1
- package/dist/llm.js.map +0 -1
- package/dist/llm.test.js +0 -592
- package/dist/llm.test.js.map +0 -1
- package/dist/prompts.d.ts.map +0 -1
- package/dist/prompts.js.map +0 -1
- package/dist/prompts.test.d.ts +0 -2
- package/dist/prompts.test.d.ts.map +0 -1
- package/dist/prompts.test.js +0 -427
- package/dist/prompts.test.js.map +0 -1
- package/src/analysis.test.ts +0 -396
- package/src/analysis.ts +0 -262
- package/src/git.ts +0 -300
- package/src/llm.test.ts +0 -701
- package/src/prompts.test.ts +0 -515
- /package/dist/{llm.test.d.ts → llm/llm.test.d.ts} +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StagedChangesStrategy,
|
|
3
|
+
AllChangesStrategy,
|
|
4
|
+
BranchDiffStrategy,
|
|
5
|
+
createGitStrategy,
|
|
6
|
+
} from './git-strategy';
|
|
7
|
+
import { GitError } from '../../git/git.errors';
|
|
8
|
+
import {
|
|
9
|
+
getStagedFiles,
|
|
10
|
+
getStagedDiff,
|
|
11
|
+
getAllChanges,
|
|
12
|
+
getAllDiff,
|
|
13
|
+
type DiffOptions,
|
|
14
|
+
} from '../../git/git.operations';
|
|
15
|
+
|
|
16
|
+
jest.mock('../../git/git.operations');
|
|
17
|
+
|
|
18
|
+
let mockExecAsync: jest.Mock;
|
|
19
|
+
|
|
20
|
+
jest.mock('child_process', () => ({
|
|
21
|
+
exec: jest.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
jest.mock('util', () => {
|
|
25
|
+
const actual = jest.requireActual('util');
|
|
26
|
+
return {
|
|
27
|
+
...actual,
|
|
28
|
+
promisify: jest.fn(() => (...args: unknown[]) => mockExecAsync(...args)),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const mockedGetStagedFiles = getStagedFiles as jest.MockedFunction<typeof getStagedFiles>;
|
|
33
|
+
const mockedGetStagedDiff = getStagedDiff as jest.MockedFunction<typeof getStagedDiff>;
|
|
34
|
+
const mockedGetAllChanges = getAllChanges as jest.MockedFunction<typeof getAllChanges>;
|
|
35
|
+
const mockedGetAllDiff = getAllDiff as jest.MockedFunction<typeof getAllDiff>;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
mockExecAsync = jest.fn();
|
|
39
|
+
jest.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
jest.restoreAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('StagedChangesStrategy', () => {
|
|
47
|
+
let strategy: StagedChangesStrategy;
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
strategy = new StagedChangesStrategy();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('getFiles() delegates to getStagedFiles', async () => {
|
|
54
|
+
const files = ['src/a.ts', 'src/b.ts'];
|
|
55
|
+
mockedGetStagedFiles.mockResolvedValue(files);
|
|
56
|
+
|
|
57
|
+
const result = await strategy.getFiles();
|
|
58
|
+
|
|
59
|
+
expect(result).toEqual(files);
|
|
60
|
+
expect(getStagedFiles).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('getDiff() delegates to getStagedDiff', async () => {
|
|
64
|
+
const diff = 'diff --git a/file.ts b/file.ts\n+added line';
|
|
65
|
+
mockedGetStagedDiff.mockResolvedValue(diff);
|
|
66
|
+
|
|
67
|
+
const result = await strategy.getDiff();
|
|
68
|
+
|
|
69
|
+
expect(result).toBe(diff);
|
|
70
|
+
expect(getStagedDiff).toHaveBeenCalledTimes(1);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('AllChangesStrategy', () => {
|
|
75
|
+
let strategy: AllChangesStrategy;
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
strategy = new AllChangesStrategy();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('getFiles() delegates to getAllChanges', async () => {
|
|
82
|
+
const files = ['lib/x.ts'];
|
|
83
|
+
mockedGetAllChanges.mockResolvedValue(files);
|
|
84
|
+
|
|
85
|
+
const result = await strategy.getFiles();
|
|
86
|
+
|
|
87
|
+
expect(result).toEqual(files);
|
|
88
|
+
expect(getAllChanges).toHaveBeenCalledTimes(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('getDiff() delegates to getAllDiff', async () => {
|
|
92
|
+
const diff = 'diff output here';
|
|
93
|
+
mockedGetAllDiff.mockResolvedValue(diff);
|
|
94
|
+
|
|
95
|
+
const result = await strategy.getDiff();
|
|
96
|
+
|
|
97
|
+
expect(result).toBe(diff);
|
|
98
|
+
expect(getAllDiff).toHaveBeenCalledTimes(1);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('BranchDiffStrategy', () => {
|
|
103
|
+
let strategy: BranchDiffStrategy;
|
|
104
|
+
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
strategy = new BranchDiffStrategy('main', 'feat/test');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('getFiles()', () => {
|
|
110
|
+
it('parses stdout newline list and returns array of files', async () => {
|
|
111
|
+
mockExecAsync.mockResolvedValue({
|
|
112
|
+
stdout: 'src/a.ts\nsrc/b.ts\n',
|
|
113
|
+
stderr: '',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const result = await strategy.getFiles();
|
|
117
|
+
|
|
118
|
+
expect(result).toEqual(['src/a.ts', 'src/b.ts']);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('throws GitError with "Invalid git reference" when exec fails with code 128', async () => {
|
|
122
|
+
const execError = Object.assign(new Error('fatal: bad revision'), { code: 128 });
|
|
123
|
+
mockExecAsync.mockRejectedValue(execError);
|
|
124
|
+
|
|
125
|
+
await expect(strategy.getFiles()).rejects.toThrow(GitError);
|
|
126
|
+
await expect(strategy.getFiles()).rejects.toThrow(/Invalid git reference/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('throws GitError with "Unable to read Git repository" when exec fails with other code', async () => {
|
|
130
|
+
const execError = Object.assign(new Error('some error'), { code: 1 });
|
|
131
|
+
mockExecAsync.mockRejectedValue(execError);
|
|
132
|
+
|
|
133
|
+
await expect(strategy.getFiles()).rejects.toThrow(GitError);
|
|
134
|
+
await expect(strategy.getFiles()).rejects.toThrow(/Unable to read Git repository/);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('getDiff()', () => {
|
|
139
|
+
it('returns raw stdout on success', async () => {
|
|
140
|
+
const diffOutput = 'diff --git a/file.ts b/file.ts\n+new line';
|
|
141
|
+
mockExecAsync.mockResolvedValue({
|
|
142
|
+
stdout: diffOutput,
|
|
143
|
+
stderr: '',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = await strategy.getDiff();
|
|
147
|
+
|
|
148
|
+
expect(result).toBe(diffOutput);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('throws GitError when exec fails with code 128', async () => {
|
|
152
|
+
const execError = Object.assign(new Error('fatal: bad revision'), { code: 128 });
|
|
153
|
+
mockExecAsync.mockRejectedValue(execError);
|
|
154
|
+
|
|
155
|
+
await expect(strategy.getDiff()).rejects.toThrow(GitError);
|
|
156
|
+
await expect(strategy.getDiff()).rejects.toThrow(/Invalid git reference/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('throws GitError when exec fails with other code', async () => {
|
|
160
|
+
const execError = Object.assign(new Error('permission denied'), { code: 2 });
|
|
161
|
+
mockExecAsync.mockRejectedValue(execError);
|
|
162
|
+
|
|
163
|
+
await expect(strategy.getDiff()).rejects.toThrow(GitError);
|
|
164
|
+
await expect(strategy.getDiff()).rejects.toThrow(/Unable to read Git repository/);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('uses correct base and head in git commands', async () => {
|
|
169
|
+
const customStrategy = new BranchDiffStrategy('origin/develop', 'feature/abc');
|
|
170
|
+
mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
|
|
171
|
+
|
|
172
|
+
await customStrategy.getFiles();
|
|
173
|
+
expect(mockExecAsync).toHaveBeenCalledWith(
|
|
174
|
+
'git diff --name-only origin/develop...feature/abc'
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
mockExecAsync.mockClear();
|
|
178
|
+
|
|
179
|
+
await customStrategy.getDiff();
|
|
180
|
+
expect(mockExecAsync).toHaveBeenCalledWith(
|
|
181
|
+
'git diff origin/develop...feature/abc --unified=1'
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('createGitStrategy', () => {
|
|
187
|
+
it('returns StagedChangesStrategy for mode "staged"', () => {
|
|
188
|
+
const strategy = createGitStrategy({ mode: 'staged' });
|
|
189
|
+
|
|
190
|
+
expect(strategy).toBeInstanceOf(StagedChangesStrategy);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('returns AllChangesStrategy for mode "all"', () => {
|
|
194
|
+
const strategy = createGitStrategy({ mode: 'all' });
|
|
195
|
+
|
|
196
|
+
expect(strategy).toBeInstanceOf(AllChangesStrategy);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('returns BranchDiffStrategy for mode "branch-diff"', () => {
|
|
200
|
+
const strategy = createGitStrategy({ mode: 'branch-diff', base: 'main', head: 'feat' });
|
|
201
|
+
|
|
202
|
+
expect(strategy).toBeInstanceOf(BranchDiffStrategy);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('returns AllChangesStrategy for unknown mode (default case)', () => {
|
|
206
|
+
const strategy = createGitStrategy({ mode: 'unknown' as unknown as DiffOptions['mode'] });
|
|
207
|
+
|
|
208
|
+
expect(strategy).toBeInstanceOf(AllChangesStrategy);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getStagedFiles,
|
|
3
|
+
getStagedDiff,
|
|
4
|
+
getAllChanges,
|
|
5
|
+
getAllDiff,
|
|
6
|
+
type DiffOptions,
|
|
7
|
+
} from '../../git/git.operations';
|
|
8
|
+
import { GitError } from '../../git';
|
|
9
|
+
|
|
10
|
+
export interface GitStrategy {
|
|
11
|
+
getFiles(): Promise<string[]>;
|
|
12
|
+
getDiff(): Promise<string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class StagedChangesStrategy implements GitStrategy {
|
|
16
|
+
async getFiles(): Promise<string[]> {
|
|
17
|
+
return getStagedFiles();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async getDiff(): Promise<string> {
|
|
21
|
+
return getStagedDiff();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class AllChangesStrategy implements GitStrategy {
|
|
26
|
+
async getFiles(): Promise<string[]> {
|
|
27
|
+
return getAllChanges();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getDiff(): Promise<string> {
|
|
31
|
+
return getAllDiff();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class BranchDiffStrategy implements GitStrategy {
|
|
36
|
+
constructor(
|
|
37
|
+
private base: string,
|
|
38
|
+
private head: string
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
async getFiles(): Promise<string[]> {
|
|
42
|
+
const { exec } = await import('child_process');
|
|
43
|
+
const { promisify } = await import('util');
|
|
44
|
+
const execAsync = promisify(exec);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const { stdout } = await execAsync(`git diff --name-only ${this.base}...${this.head}`);
|
|
48
|
+
return stdout
|
|
49
|
+
.split('\n')
|
|
50
|
+
.map((f) => f.trim())
|
|
51
|
+
.filter((f) => f.length > 0);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const errorWithCode = error as { code?: number };
|
|
54
|
+
if (errorWithCode.code === 128) {
|
|
55
|
+
throw new GitError(
|
|
56
|
+
`Invalid git reference: ${this.base} or ${this.head}. Please ensure both references exist.`,
|
|
57
|
+
'GIT_ERROR',
|
|
58
|
+
error instanceof Error ? error : new Error(String(error))
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
throw new GitError(
|
|
62
|
+
'Unable to read Git repository. Please check repository permissions.',
|
|
63
|
+
'GIT_ERROR',
|
|
64
|
+
error instanceof Error ? error : new Error(String(error))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async getDiff(): Promise<string> {
|
|
70
|
+
const { exec } = await import('child_process');
|
|
71
|
+
const { promisify } = await import('util');
|
|
72
|
+
const execAsync = promisify(exec);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const { stdout } = await execAsync(`git diff ${this.base}...${this.head} --unified=1`);
|
|
76
|
+
return stdout;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const errorWithCode = error as { code?: number };
|
|
79
|
+
if (errorWithCode.code === 128) {
|
|
80
|
+
throw new GitError(
|
|
81
|
+
`Invalid git reference: ${this.base} or ${this.head}. Please ensure both references exist.`,
|
|
82
|
+
'GIT_ERROR',
|
|
83
|
+
error instanceof Error ? error : new Error(String(error))
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
throw new GitError(
|
|
87
|
+
'Unable to read Git repository. Please check repository permissions.',
|
|
88
|
+
'GIT_ERROR',
|
|
89
|
+
error instanceof Error ? error : new Error(String(error))
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createGitStrategy(options: DiffOptions): GitStrategy {
|
|
96
|
+
switch (options.mode) {
|
|
97
|
+
case 'staged':
|
|
98
|
+
return new StagedChangesStrategy();
|
|
99
|
+
case 'all':
|
|
100
|
+
return new AllChangesStrategy();
|
|
101
|
+
case 'branch-diff':
|
|
102
|
+
return new BranchDiffStrategy(options.base || 'origin/main', options.head || 'HEAD');
|
|
103
|
+
default:
|
|
104
|
+
return new AllChangesStrategy();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { analyzeCommand } from './analyze';
|
|
2
|
+
import { runAnalysis } from '../analysis/analysis.orchestrator';
|
|
3
|
+
import { loggerInstance as logger } from '../logger';
|
|
4
|
+
|
|
5
|
+
jest.mock('../analysis/analysis.orchestrator');
|
|
6
|
+
jest.mock('../logger');
|
|
7
|
+
|
|
8
|
+
const mockRunAnalysis = runAnalysis as jest.MockedFunction<typeof runAnalysis>;
|
|
9
|
+
const mockLogger = logger as jest.Mocked<typeof logger>;
|
|
10
|
+
|
|
11
|
+
describe('analyzeCommand', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
mockRunAnalysis.mockResolvedValue(undefined);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
jest.restoreAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should call runAnalysis with mode "all" when no args provided', async () => {
|
|
22
|
+
await analyzeCommand([]);
|
|
23
|
+
|
|
24
|
+
expect(mockRunAnalysis).toHaveBeenCalledWith({ mode: 'all' });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should call runAnalysis with mode "all" when --all flag is provided', async () => {
|
|
28
|
+
await analyzeCommand(['--all']);
|
|
29
|
+
|
|
30
|
+
expect(mockRunAnalysis).toHaveBeenCalledWith({ mode: 'all' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should call runAnalysis with mode "staged" when --staged flag is provided', async () => {
|
|
34
|
+
await analyzeCommand(['--staged']);
|
|
35
|
+
|
|
36
|
+
expect(mockRunAnalysis).toHaveBeenCalledWith({ mode: 'staged' });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should call runAnalysis with mode "branch-diff" and base when --base is provided', async () => {
|
|
40
|
+
await analyzeCommand(['--base', 'origin/main']);
|
|
41
|
+
|
|
42
|
+
expect(mockRunAnalysis).toHaveBeenCalledWith({
|
|
43
|
+
mode: 'branch-diff',
|
|
44
|
+
base: 'origin/main',
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should call runAnalysis with mode "branch-diff", base and head when both flags are provided', async () => {
|
|
49
|
+
await analyzeCommand(['--base', 'origin/main', '--head', 'feature']);
|
|
50
|
+
|
|
51
|
+
expect(mockRunAnalysis).toHaveBeenCalledWith({
|
|
52
|
+
mode: 'branch-diff',
|
|
53
|
+
base: 'origin/main',
|
|
54
|
+
head: 'feature',
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should fall back to mode "all" when --base has no value after it', async () => {
|
|
59
|
+
await analyzeCommand(['--base']);
|
|
60
|
+
|
|
61
|
+
expect(mockRunAnalysis).toHaveBeenCalledWith({ mode: 'all' });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should detect --staged flag anywhere in the args array', async () => {
|
|
65
|
+
await analyzeCommand(['analyze', '--staged']);
|
|
66
|
+
|
|
67
|
+
expect(mockRunAnalysis).toHaveBeenCalledWith({ mode: 'staged' });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should catch errors from runAnalysis and not re-throw', async () => {
|
|
71
|
+
mockRunAnalysis.mockRejectedValue(new Error('analysis failed'));
|
|
72
|
+
|
|
73
|
+
await expect(analyzeCommand(['--all'])).resolves.not.toThrow();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should log "Analyze command started" with mode information', async () => {
|
|
77
|
+
await analyzeCommand(['--staged']);
|
|
78
|
+
|
|
79
|
+
expect(mockLogger.info).toHaveBeenCalledWith('Analyze command started', {
|
|
80
|
+
mode: 'staged',
|
|
81
|
+
base: undefined,
|
|
82
|
+
head: undefined,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should log "Analyze command completed" on success', async () => {
|
|
87
|
+
await analyzeCommand([]);
|
|
88
|
+
|
|
89
|
+
expect(mockLogger.info).toHaveBeenCalledWith('Analyze command completed');
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/commands/analyze.ts
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Analyze Command
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Triggers LLM-powered analysis of code changes.
|
|
5
5
|
* Thin wrapper around analysis orchestration module.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { runAnalysis } from '../analysis';
|
|
8
|
+
import { runAnalysis } from '../analysis/analysis.orchestrator';
|
|
9
9
|
import { DiffOptions } from '../git';
|
|
10
10
|
import { loggerInstance as logger } from '../logger';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Execute the analyze command
|
|
14
14
|
* Analyzes code changes for architectural significance
|
|
15
|
-
*
|
|
15
|
+
*
|
|
16
16
|
* @param args - Command line arguments (e.g., ['--staged'], ['--all'])
|
|
17
17
|
*/
|
|
18
18
|
export async function analyzeCommand(args: string[] = []): Promise<void> {
|
|
19
19
|
try {
|
|
20
20
|
// Parse command line flags to determine diff options
|
|
21
21
|
const diffOptions: DiffOptions = { mode: 'all' }; // Default to all uncommitted
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
// Check for --base flag (implies branch-diff mode)
|
|
24
24
|
const baseIndex = args.indexOf('--base');
|
|
25
25
|
if (baseIndex !== -1 && baseIndex + 1 < args.length) {
|
|
26
26
|
diffOptions.mode = 'branch-diff';
|
|
27
27
|
diffOptions.base = args[baseIndex + 1];
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
// Check for optional --head flag
|
|
30
30
|
const headIndex = args.indexOf('--head');
|
|
31
31
|
if (headIndex !== -1 && headIndex + 1 < args.length) {
|
|
@@ -36,11 +36,11 @@ export async function analyzeCommand(args: string[] = []): Promise<void> {
|
|
|
36
36
|
} else if (args.includes('--all')) {
|
|
37
37
|
diffOptions.mode = 'all';
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
logger.info('Analyze command started', {
|
|
39
|
+
|
|
40
|
+
logger.info('Analyze command started', {
|
|
41
41
|
mode: diffOptions.mode,
|
|
42
42
|
base: diffOptions.base,
|
|
43
|
-
head: diffOptions.head
|
|
43
|
+
head: diffOptions.head,
|
|
44
44
|
});
|
|
45
45
|
await runAnalysis(diffOptions);
|
|
46
46
|
logger.info('Analyze command completed');
|
|
@@ -53,4 +53,3 @@ export async function analyzeCommand(args: string[] = []): Promise<void> {
|
|
|
53
53
|
console.error('Please check the logs for more details.\n');
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
|
|
@@ -1,27 +1,222 @@
|
|
|
1
1
|
import { initCommand } from './init';
|
|
2
2
|
import * as config from '../config';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { loggerInstance as logger } from '../logger';
|
|
3
5
|
|
|
4
6
|
// Mock dependencies
|
|
5
7
|
jest.mock('../config');
|
|
8
|
+
jest.mock('fs', () => ({
|
|
9
|
+
existsSync: jest.fn().mockReturnValue(false),
|
|
10
|
+
}));
|
|
11
|
+
jest.mock('../logger');
|
|
12
|
+
|
|
13
|
+
const mockExistsSync = existsSync as jest.Mock;
|
|
14
|
+
const mockCreateConfig = config.createConfig as jest.Mock;
|
|
15
|
+
const mockGetDefaultConfigPath = config.getDefaultConfigPath as jest.Mock;
|
|
16
|
+
const mockValidateConfig = config.validateConfig as jest.Mock;
|
|
17
|
+
|
|
18
|
+
function makeValidConfig(overrides: Partial<config.AnalysisConfig> = {}): config.AnalysisConfig {
|
|
19
|
+
return {
|
|
20
|
+
provider: 'openai',
|
|
21
|
+
analysis_model: 'gpt-4',
|
|
22
|
+
api_key_env: 'OPENAI_API_KEY',
|
|
23
|
+
timeout_seconds: 30,
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
6
27
|
|
|
7
28
|
describe('Init Command', () => {
|
|
29
|
+
let consoleSpy: {
|
|
30
|
+
log: jest.SpyInstance;
|
|
31
|
+
error: jest.SpyInstance;
|
|
32
|
+
warn: jest.SpyInstance;
|
|
33
|
+
};
|
|
34
|
+
|
|
8
35
|
beforeEach(() => {
|
|
9
36
|
jest.clearAllMocks();
|
|
37
|
+
mockExistsSync.mockReturnValue(false);
|
|
38
|
+
mockGetDefaultConfigPath.mockReturnValue('/fake/path/cadr.yaml');
|
|
39
|
+
mockValidateConfig.mockReturnValue({ valid: true, errors: [] });
|
|
40
|
+
consoleSpy = {
|
|
41
|
+
log: jest.spyOn(console, 'log').mockImplementation(),
|
|
42
|
+
error: jest.spyOn(console, 'error').mockImplementation(),
|
|
43
|
+
warn: jest.spyOn(console, 'warn').mockImplementation(),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
consoleSpy.log.mockRestore();
|
|
49
|
+
consoleSpy.error.mockRestore();
|
|
50
|
+
consoleSpy.warn.mockRestore();
|
|
10
51
|
});
|
|
11
52
|
|
|
12
53
|
describe('initCommand', () => {
|
|
13
54
|
test('calls createConfig when no config exists', async () => {
|
|
14
|
-
|
|
15
|
-
|
|
55
|
+
mockCreateConfig.mockResolvedValue(makeValidConfig());
|
|
56
|
+
|
|
16
57
|
await initCommand();
|
|
17
|
-
|
|
58
|
+
|
|
18
59
|
expect(config.createConfig).toHaveBeenCalled();
|
|
19
60
|
});
|
|
20
61
|
|
|
21
62
|
test('handles config creation errors gracefully', async () => {
|
|
22
|
-
|
|
23
|
-
|
|
63
|
+
mockCreateConfig.mockRejectedValue(new Error('Permission denied'));
|
|
64
|
+
|
|
24
65
|
await expect(initCommand()).resolves.not.toThrow();
|
|
25
66
|
});
|
|
67
|
+
|
|
68
|
+
test('prints already-exists message and does not call createConfig when config exists', async () => {
|
|
69
|
+
mockExistsSync.mockReturnValue(true);
|
|
70
|
+
|
|
71
|
+
await initCommand();
|
|
72
|
+
|
|
73
|
+
expect(consoleSpy.log).toHaveBeenCalledWith(
|
|
74
|
+
expect.stringContaining('already exists'),
|
|
75
|
+
);
|
|
76
|
+
expect(mockCreateConfig).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('prints failure message and does not display summary when createConfig returns null', async () => {
|
|
80
|
+
mockCreateConfig.mockResolvedValue(null);
|
|
81
|
+
|
|
82
|
+
await initCommand();
|
|
83
|
+
|
|
84
|
+
expect(consoleSpy.error).toHaveBeenCalledWith(
|
|
85
|
+
expect.stringContaining('Failed to create configuration'),
|
|
86
|
+
);
|
|
87
|
+
// Summary header should NOT appear
|
|
88
|
+
const logCalls = consoleSpy.log.mock.calls.map((c: unknown[]) => c[0]);
|
|
89
|
+
expect(logCalls).not.toEqual(
|
|
90
|
+
expect.arrayContaining([expect.stringContaining('Configuration Summary')]),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('displays summary with provider, model, api_key_env, and timeout when config is valid', async () => {
|
|
95
|
+
const cfg = makeValidConfig({
|
|
96
|
+
provider: 'gemini',
|
|
97
|
+
analysis_model: 'gemini-pro',
|
|
98
|
+
api_key_env: 'GEMINI_API_KEY',
|
|
99
|
+
timeout_seconds: 45,
|
|
100
|
+
});
|
|
101
|
+
mockCreateConfig.mockResolvedValue(cfg);
|
|
102
|
+
process.env['GEMINI_API_KEY'] = 'fake-key';
|
|
103
|
+
|
|
104
|
+
await initCommand();
|
|
105
|
+
|
|
106
|
+
expect(consoleSpy.log).toHaveBeenCalledWith('📋 Configuration Summary:');
|
|
107
|
+
expect(consoleSpy.log).toHaveBeenCalledWith(' Provider: gemini');
|
|
108
|
+
expect(consoleSpy.log).toHaveBeenCalledWith(' Model: gemini-pro');
|
|
109
|
+
expect(consoleSpy.log).toHaveBeenCalledWith(' API Key Env: GEMINI_API_KEY');
|
|
110
|
+
expect(consoleSpy.log).toHaveBeenCalledWith(' Timeout: 45s');
|
|
111
|
+
|
|
112
|
+
delete process.env['GEMINI_API_KEY'];
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('includes ignore patterns line when config has ignore_patterns', async () => {
|
|
116
|
+
const cfg = makeValidConfig({
|
|
117
|
+
ignore_patterns: ['node_modules', '*.log'],
|
|
118
|
+
});
|
|
119
|
+
mockCreateConfig.mockResolvedValue(cfg);
|
|
120
|
+
process.env['OPENAI_API_KEY'] = 'fake-key';
|
|
121
|
+
|
|
122
|
+
await initCommand();
|
|
123
|
+
|
|
124
|
+
expect(consoleSpy.log).toHaveBeenCalledWith(
|
|
125
|
+
' Ignore Patterns: node_modules, *.log',
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
delete process.env['OPENAI_API_KEY'];
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('does not show ignore patterns line when ignore_patterns is undefined', async () => {
|
|
132
|
+
const cfg = makeValidConfig();
|
|
133
|
+
delete cfg.ignore_patterns;
|
|
134
|
+
mockCreateConfig.mockResolvedValue(cfg);
|
|
135
|
+
process.env['OPENAI_API_KEY'] = 'fake-key';
|
|
136
|
+
|
|
137
|
+
await initCommand();
|
|
138
|
+
|
|
139
|
+
const logCalls = consoleSpy.log.mock.calls.map((c: unknown[]) => c[0]);
|
|
140
|
+
expect(logCalls).not.toEqual(
|
|
141
|
+
expect.arrayContaining([expect.stringContaining('Ignore Patterns')]),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
delete process.env['OPENAI_API_KEY'];
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('does not display warning when API key env var is set', async () => {
|
|
148
|
+
const cfg = makeValidConfig({ api_key_env: 'OPENAI_API_KEY' });
|
|
149
|
+
mockCreateConfig.mockResolvedValue(cfg);
|
|
150
|
+
process.env['OPENAI_API_KEY'] = 'fake-key';
|
|
151
|
+
|
|
152
|
+
await initCommand();
|
|
153
|
+
|
|
154
|
+
expect(consoleSpy.warn).not.toHaveBeenCalled();
|
|
155
|
+
|
|
156
|
+
delete process.env['OPENAI_API_KEY'];
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('displays warning with OpenAI link when API key env var is not set for OpenAI', async () => {
|
|
160
|
+
const cfg = makeValidConfig({ provider: 'openai', api_key_env: 'OPENAI_API_KEY' });
|
|
161
|
+
mockCreateConfig.mockResolvedValue(cfg);
|
|
162
|
+
delete process.env['OPENAI_API_KEY'];
|
|
163
|
+
|
|
164
|
+
await initCommand();
|
|
165
|
+
|
|
166
|
+
expect(consoleSpy.warn).toHaveBeenCalledWith(
|
|
167
|
+
expect.stringContaining('OPENAI_API_KEY is not set'),
|
|
168
|
+
);
|
|
169
|
+
expect(consoleSpy.warn).toHaveBeenCalledWith(
|
|
170
|
+
expect.stringContaining('https://platform.openai.com/api-keys'),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('displays warning with Gemini link when API key env var is not set for Gemini', async () => {
|
|
175
|
+
const cfg = makeValidConfig({ provider: 'gemini', api_key_env: 'GEMINI_API_KEY' });
|
|
176
|
+
mockCreateConfig.mockResolvedValue(cfg);
|
|
177
|
+
delete process.env['GEMINI_API_KEY'];
|
|
178
|
+
|
|
179
|
+
await initCommand();
|
|
180
|
+
|
|
181
|
+
expect(consoleSpy.warn).toHaveBeenCalledWith(
|
|
182
|
+
expect.stringContaining('GEMINI_API_KEY is not set'),
|
|
183
|
+
);
|
|
184
|
+
expect(consoleSpy.warn).toHaveBeenCalledWith(
|
|
185
|
+
expect.stringContaining('https://aistudio.google.com/app/apikey'),
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('logs validation warning when validateConfig returns invalid', async () => {
|
|
190
|
+
const cfg = makeValidConfig();
|
|
191
|
+
mockCreateConfig.mockResolvedValue(cfg);
|
|
192
|
+
mockValidateConfig.mockReturnValue({
|
|
193
|
+
valid: false,
|
|
194
|
+
errors: ['timeout too low'],
|
|
195
|
+
});
|
|
196
|
+
process.env['OPENAI_API_KEY'] = 'fake-key';
|
|
197
|
+
|
|
198
|
+
await initCommand();
|
|
199
|
+
|
|
200
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
201
|
+
'Created config has validation warnings',
|
|
202
|
+
{ errors: ['timeout too low'] },
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
delete process.env['OPENAI_API_KEY'];
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('catches unexpected error and prints error message', async () => {
|
|
209
|
+
mockCreateConfig.mockRejectedValue(new Error('unexpected boom'));
|
|
210
|
+
|
|
211
|
+
await initCommand();
|
|
212
|
+
|
|
213
|
+
expect(consoleSpy.error).toHaveBeenCalledWith(
|
|
214
|
+
expect.stringContaining('unexpected error occurred'),
|
|
215
|
+
);
|
|
216
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
217
|
+
'Init command failed',
|
|
218
|
+
expect.objectContaining({ error: expect.any(Error) }),
|
|
219
|
+
);
|
|
220
|
+
});
|
|
26
221
|
});
|
|
27
222
|
});
|