popeye-cli 1.8.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -3
- package/cheatsheet.md +33 -0
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/review.d.ts +31 -0
- package/dist/cli/commands/review.d.ts.map +1 -0
- package/dist/cli/commands/review.js +156 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +122 -61
- package/dist/cli/interactive.js.map +1 -1
- package/dist/types/audit.d.ts +623 -0
- package/dist/types/audit.d.ts.map +1 -0
- package/dist/types/audit.js +240 -0
- package/dist/types/audit.js.map +1 -0
- package/dist/types/workflow.d.ts +15 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +5 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/audit-analyzer.d.ts +58 -0
- package/dist/workflow/audit-analyzer.d.ts.map +1 -0
- package/dist/workflow/audit-analyzer.js +438 -0
- package/dist/workflow/audit-analyzer.js.map +1 -0
- package/dist/workflow/audit-mode.d.ts +28 -0
- package/dist/workflow/audit-mode.d.ts.map +1 -0
- package/dist/workflow/audit-mode.js +169 -0
- package/dist/workflow/audit-mode.js.map +1 -0
- package/dist/workflow/audit-recovery.d.ts +61 -0
- package/dist/workflow/audit-recovery.d.ts.map +1 -0
- package/dist/workflow/audit-recovery.js +242 -0
- package/dist/workflow/audit-recovery.js.map +1 -0
- package/dist/workflow/audit-reporter.d.ts +65 -0
- package/dist/workflow/audit-reporter.d.ts.map +1 -0
- package/dist/workflow/audit-reporter.js +301 -0
- package/dist/workflow/audit-reporter.js.map +1 -0
- package/dist/workflow/audit-scanner.d.ts +87 -0
- package/dist/workflow/audit-scanner.d.ts.map +1 -0
- package/dist/workflow/audit-scanner.js +768 -0
- package/dist/workflow/audit-scanner.js.map +1 -0
- package/dist/workflow/index.d.ts +5 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +5 -0
- package/dist/workflow/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/index.ts +1 -0
- package/src/cli/commands/review.ts +187 -0
- package/src/cli/index.ts +2 -0
- package/src/cli/interactive.ts +72 -4
- package/src/types/audit.ts +294 -0
- package/src/types/workflow.ts +15 -0
- package/src/workflow/audit-analyzer.ts +510 -0
- package/src/workflow/audit-mode.ts +240 -0
- package/src/workflow/audit-recovery.ts +284 -0
- package/src/workflow/audit-reporter.ts +370 -0
- package/src/workflow/audit-scanner.ts +873 -0
- package/src/workflow/index.ts +5 -0
- package/tests/cli/commands/review.test.ts +52 -0
- package/tests/types/audit.test.ts +250 -0
- package/tests/workflow/audit-analyzer.test.ts +281 -0
- package/tests/workflow/audit-mode.test.ts +114 -0
- package/tests/workflow/audit-recovery.test.ts +237 -0
- package/tests/workflow/audit-reporter.test.ts +254 -0
- package/tests/workflow/audit-scanner.test.ts +270 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the audit scanner module.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { promises as fs } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import {
|
|
9
|
+
detectWorkspaceComposition,
|
|
10
|
+
scanComponent,
|
|
11
|
+
readPriorityDocs,
|
|
12
|
+
buildWiringMatrix,
|
|
13
|
+
scanProject,
|
|
14
|
+
countLines,
|
|
15
|
+
} from '../../src/workflow/audit-scanner.js';
|
|
16
|
+
|
|
17
|
+
let tmpDir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'audit-scan-'));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Helper to create a file in the tmp directory, including intermediate dirs.
|
|
29
|
+
*/
|
|
30
|
+
async function createFile(relativePath: string, content = ''): Promise<void> {
|
|
31
|
+
const abs = path.join(tmpDir, relativePath);
|
|
32
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
33
|
+
await fs.writeFile(abs, content);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// detectWorkspaceComposition
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
describe('detectWorkspaceComposition', () => {
|
|
41
|
+
it('should detect a fullstack workspace (frontend + backend)', async () => {
|
|
42
|
+
await createFile('apps/frontend/package.json', '{}');
|
|
43
|
+
await createFile('apps/backend/requirements.txt', 'fastapi\n');
|
|
44
|
+
|
|
45
|
+
const kinds = await detectWorkspaceComposition(tmpDir);
|
|
46
|
+
expect(kinds).toContain('frontend');
|
|
47
|
+
expect(kinds).toContain('backend');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should detect a single python project', async () => {
|
|
51
|
+
await createFile('requirements.txt', 'flask\n');
|
|
52
|
+
|
|
53
|
+
const kinds = await detectWorkspaceComposition(tmpDir);
|
|
54
|
+
expect(kinds).toContain('backend');
|
|
55
|
+
expect(kinds).not.toContain('frontend');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should detect a single typescript project', async () => {
|
|
59
|
+
await createFile('package.json', '{}');
|
|
60
|
+
|
|
61
|
+
const kinds = await detectWorkspaceComposition(tmpDir);
|
|
62
|
+
expect(kinds).toContain('frontend');
|
|
63
|
+
expect(kinds).not.toContain('backend');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should detect website component', async () => {
|
|
67
|
+
await createFile('apps/frontend/package.json', '{}');
|
|
68
|
+
await createFile('apps/backend/requirements.txt', 'fastapi\n');
|
|
69
|
+
await createFile('apps/website/package.json', '{}');
|
|
70
|
+
|
|
71
|
+
const kinds = await detectWorkspaceComposition(tmpDir);
|
|
72
|
+
expect(kinds).toContain('website');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should detect infra from docker-compose.yml', async () => {
|
|
76
|
+
await createFile('package.json', '{}');
|
|
77
|
+
await createFile('docker-compose.yml', 'version: "3"\n');
|
|
78
|
+
|
|
79
|
+
const kinds = await detectWorkspaceComposition(tmpDir);
|
|
80
|
+
expect(kinds).toContain('infra');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should detect shared from packages/ directory', async () => {
|
|
84
|
+
await createFile('package.json', '{}');
|
|
85
|
+
await createFile('packages/shared/index.ts', '');
|
|
86
|
+
|
|
87
|
+
const kinds = await detectWorkspaceComposition(tmpDir);
|
|
88
|
+
expect(kinds).toContain('shared');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should return empty array for empty directory', async () => {
|
|
92
|
+
const kinds = await detectWorkspaceComposition(tmpDir);
|
|
93
|
+
expect(kinds).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// scanComponent
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
describe('scanComponent', () => {
|
|
102
|
+
it('should scan a typescript component and find source/test files', async () => {
|
|
103
|
+
await createFile('src/main.tsx', 'export default function App() {}');
|
|
104
|
+
await createFile('src/utils.ts', 'export const x = 1;');
|
|
105
|
+
await createFile('tests/main.test.tsx', 'test("works", () => {})');
|
|
106
|
+
await createFile('package.json', JSON.stringify({
|
|
107
|
+
dependencies: { react: '^18.0.0' },
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
const result = await scanComponent(tmpDir, 'frontend', 'typescript');
|
|
111
|
+
expect(result.kind).toBe('frontend');
|
|
112
|
+
expect(result.framework).toBe('react');
|
|
113
|
+
expect(result.sourceFiles.length).toBeGreaterThanOrEqual(2);
|
|
114
|
+
expect(result.testFiles.length).toBeGreaterThanOrEqual(1);
|
|
115
|
+
expect(result.entryPoints).toContain('src/main.tsx');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should scan a python backend component', async () => {
|
|
119
|
+
await createFile('app.py', 'from fastapi import FastAPI\napp = FastAPI()\n');
|
|
120
|
+
await createFile('routes.py', 'pass');
|
|
121
|
+
await createFile('requirements.txt', 'fastapi==0.100.0\n');
|
|
122
|
+
|
|
123
|
+
const result = await scanComponent(tmpDir, 'backend', 'python');
|
|
124
|
+
expect(result.kind).toBe('backend');
|
|
125
|
+
expect(result.framework).toBe('fastapi');
|
|
126
|
+
expect(result.entryPoints).toContain('app.py');
|
|
127
|
+
expect(result.routeFiles).toContain('routes.py');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should handle empty directory', async () => {
|
|
131
|
+
const result = await scanComponent(tmpDir, 'frontend', 'typescript');
|
|
132
|
+
expect(result.sourceFiles).toEqual([]);
|
|
133
|
+
expect(result.testFiles).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// readPriorityDocs
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
describe('readPriorityDocs', () => {
|
|
142
|
+
it('should read CLAUDE.md first, then README, then other docs', async () => {
|
|
143
|
+
await createFile('CLAUDE.md', '# Claude Instructions');
|
|
144
|
+
await createFile('README.md', '# My Project');
|
|
145
|
+
await createFile('CONTRIBUTING.md', '# Contributing');
|
|
146
|
+
await createFile('docs/architecture.md', '# Architecture');
|
|
147
|
+
|
|
148
|
+
const result = await readPriorityDocs(tmpDir);
|
|
149
|
+
expect(result.claudeMd).toContain('Claude Instructions');
|
|
150
|
+
expect(result.readme).toContain('My Project');
|
|
151
|
+
expect(result.docsIndex[0]).toBe('CLAUDE.md');
|
|
152
|
+
expect(result.docsIndex[1]).toBe('README.md');
|
|
153
|
+
expect(result.docsIndex).toContain('CONTRIBUTING.md');
|
|
154
|
+
expect(result.docsIndex).toContain(path.join('docs', 'architecture.md'));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle missing docs gracefully', async () => {
|
|
158
|
+
const result = await readPriorityDocs(tmpDir);
|
|
159
|
+
expect(result.claudeMd).toBeUndefined();
|
|
160
|
+
expect(result.readme).toBeUndefined();
|
|
161
|
+
expect(result.docsIndex).toEqual([]);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// buildWiringMatrix
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
describe('buildWiringMatrix', () => {
|
|
170
|
+
it('should detect CORS mismatch', async () => {
|
|
171
|
+
await createFile('.env.example', 'VITE_API_URL=http://localhost:8000\nDATABASE_URL=postgres://x\n');
|
|
172
|
+
await createFile('apps/backend/main.py', `
|
|
173
|
+
cors_origins = ["http://localhost:5173"]
|
|
174
|
+
app.add_middleware(CORSMiddleware, allow_origins=cors_origins)
|
|
175
|
+
`);
|
|
176
|
+
|
|
177
|
+
const components = [
|
|
178
|
+
{
|
|
179
|
+
kind: 'frontend' as const,
|
|
180
|
+
rootDir: 'apps/frontend',
|
|
181
|
+
language: 'typescript' as const,
|
|
182
|
+
entryPoints: [],
|
|
183
|
+
routeFiles: [],
|
|
184
|
+
testFiles: [],
|
|
185
|
+
sourceFiles: [],
|
|
186
|
+
dependencyManifests: [],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
kind: 'backend' as const,
|
|
190
|
+
rootDir: 'apps/backend',
|
|
191
|
+
language: 'python' as const,
|
|
192
|
+
entryPoints: [],
|
|
193
|
+
routeFiles: [],
|
|
194
|
+
testFiles: [],
|
|
195
|
+
sourceFiles: [],
|
|
196
|
+
dependencyManifests: [],
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const wiring = await buildWiringMatrix(tmpDir, components);
|
|
201
|
+
expect(wiring.frontendApiBaseEnvKeys).toContain('VITE_API_URL');
|
|
202
|
+
expect(wiring.frontendApiBaseResolved).toBe('http://localhost:8000');
|
|
203
|
+
expect(wiring.backendCorsOrigins).toContain('http://localhost:5173');
|
|
204
|
+
// The FE expects :8000 but CORS only has :5173 — mismatch detected
|
|
205
|
+
expect(wiring.potentialMismatches.length).toBeGreaterThanOrEqual(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should return empty matrix when no env keys found', async () => {
|
|
209
|
+
const wiring = await buildWiringMatrix(tmpDir, []);
|
|
210
|
+
expect(wiring.frontendApiBaseEnvKeys).toEqual([]);
|
|
211
|
+
expect(wiring.potentialMismatches).toEqual([]);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// countLines
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
describe('countLines', () => {
|
|
220
|
+
it('should count lines in source and test files', async () => {
|
|
221
|
+
await createFile('src/index.ts', 'line1\nline2\nline3\n');
|
|
222
|
+
await createFile('tests/index.test.ts', 'test1\ntest2\n');
|
|
223
|
+
|
|
224
|
+
const source = [{ path: 'src/index.ts', extension: '.ts' }];
|
|
225
|
+
const tests = [{ path: 'tests/index.test.ts', extension: '.ts' }];
|
|
226
|
+
const result = await countLines(source, tests, tmpDir);
|
|
227
|
+
expect(result.code).toBe(4); // 3 lines + trailing newline = 4
|
|
228
|
+
expect(result.tests).toBe(3);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// scanProject (integration)
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
describe('scanProject', () => {
|
|
237
|
+
it('should produce a complete scan result for a typescript project', async () => {
|
|
238
|
+
await createFile('package.json', JSON.stringify({
|
|
239
|
+
name: 'test-project',
|
|
240
|
+
dependencies: { react: '^18.0.0' },
|
|
241
|
+
}));
|
|
242
|
+
await createFile('src/main.tsx', 'export default function App() { return <div/>; }');
|
|
243
|
+
await createFile('src/utils.ts', 'export const add = (a: number, b: number) => a + b;');
|
|
244
|
+
await createFile('tests/utils.test.ts', 'test("add", () => {})');
|
|
245
|
+
await createFile('README.md', '# Test Project');
|
|
246
|
+
|
|
247
|
+
const result = await scanProject(tmpDir, 'typescript');
|
|
248
|
+
expect(result.stateLanguage).toBe('typescript');
|
|
249
|
+
expect(result.totalSourceFiles).toBeGreaterThanOrEqual(2);
|
|
250
|
+
expect(result.totalTestFiles).toBeGreaterThanOrEqual(1);
|
|
251
|
+
expect(result.readmeContent).toContain('Test Project');
|
|
252
|
+
expect(result.components.length).toBeGreaterThanOrEqual(1);
|
|
253
|
+
expect(result.tree).toBeTruthy();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should detect composition mismatch', async () => {
|
|
257
|
+
// State says fullstack but only a frontend package.json exists
|
|
258
|
+
await createFile('package.json', '{}');
|
|
259
|
+
const result = await scanProject(tmpDir, 'fullstack');
|
|
260
|
+
expect(result.compositionMismatch).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should call progress callback', async () => {
|
|
264
|
+
await createFile('package.json', '{}');
|
|
265
|
+
const messages: string[] = [];
|
|
266
|
+
await scanProject(tmpDir, 'typescript', (msg) => messages.push(msg));
|
|
267
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
268
|
+
expect(messages.some((m) => m.includes('Scan complete'))).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
});
|