popeye-cli 1.4.1 → 1.4.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.
Files changed (56) hide show
  1. package/dist/adapters/claude.d.ts.map +1 -1
  2. package/dist/adapters/claude.js +6 -0
  3. package/dist/adapters/claude.js.map +1 -1
  4. package/dist/cli/interactive.d.ts.map +1 -1
  5. package/dist/cli/interactive.js +82 -5
  6. package/dist/cli/interactive.js.map +1 -1
  7. package/dist/types/workflow.d.ts +12 -0
  8. package/dist/types/workflow.d.ts.map +1 -1
  9. package/dist/types/workflow.js +4 -0
  10. package/dist/types/workflow.js.map +1 -1
  11. package/dist/workflow/auto-fix.d.ts +17 -1
  12. package/dist/workflow/auto-fix.d.ts.map +1 -1
  13. package/dist/workflow/auto-fix.js +97 -16
  14. package/dist/workflow/auto-fix.js.map +1 -1
  15. package/dist/workflow/execution-mode.d.ts +24 -0
  16. package/dist/workflow/execution-mode.d.ts.map +1 -1
  17. package/dist/workflow/execution-mode.js +292 -19
  18. package/dist/workflow/execution-mode.js.map +1 -1
  19. package/dist/workflow/index.d.ts +1 -0
  20. package/dist/workflow/index.d.ts.map +1 -1
  21. package/dist/workflow/index.js +1 -0
  22. package/dist/workflow/index.js.map +1 -1
  23. package/dist/workflow/milestone-workflow.d.ts.map +1 -1
  24. package/dist/workflow/milestone-workflow.js +63 -3
  25. package/dist/workflow/milestone-workflow.js.map +1 -1
  26. package/dist/workflow/project-structure.d.ts +29 -0
  27. package/dist/workflow/project-structure.d.ts.map +1 -0
  28. package/dist/workflow/project-structure.js +193 -0
  29. package/dist/workflow/project-structure.js.map +1 -0
  30. package/dist/workflow/project-verification.d.ts +24 -1
  31. package/dist/workflow/project-verification.d.ts.map +1 -1
  32. package/dist/workflow/project-verification.js +105 -22
  33. package/dist/workflow/project-verification.js.map +1 -1
  34. package/dist/workflow/remediation.d.ts +124 -0
  35. package/dist/workflow/remediation.d.ts.map +1 -0
  36. package/dist/workflow/remediation.js +510 -0
  37. package/dist/workflow/remediation.js.map +1 -0
  38. package/dist/workflow/task-workflow.d.ts.map +1 -1
  39. package/dist/workflow/task-workflow.js +202 -4
  40. package/dist/workflow/task-workflow.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/adapters/claude.ts +6 -0
  43. package/src/cli/interactive.ts +85 -5
  44. package/src/types/workflow.ts +9 -0
  45. package/src/workflow/auto-fix.ts +123 -18
  46. package/src/workflow/execution-mode.ts +364 -20
  47. package/src/workflow/index.ts +1 -0
  48. package/src/workflow/milestone-workflow.ts +80 -3
  49. package/src/workflow/project-structure.ts +233 -0
  50. package/src/workflow/project-verification.ts +128 -21
  51. package/src/workflow/remediation.ts +711 -0
  52. package/src/workflow/task-workflow.ts +237 -4
  53. package/tests/workflow/auto-fix-enhanced.test.ts +351 -0
  54. package/tests/workflow/project-structure.test.ts +131 -0
  55. package/tests/workflow/project-verification.test.ts +130 -0
  56. package/tests/workflow/remediation.test.ts +238 -0
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Project structure scanner for build verification context enrichment
3
+ * Scans project directory and returns a concise summary for embedding in AI prompts
4
+ */
5
+
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { isWorkspace, languageToApps, type OutputLanguage } from '../types/project.js';
9
+
10
+ /**
11
+ * Summary of a project's directory structure
12
+ */
13
+ export interface ProjectStructureSummary {
14
+ tree: string;
15
+ fileCounts: Record<string, number>;
16
+ appFileCounts?: Record<string, Record<string, number>>;
17
+ workspaceApps: Array<{ name: string; path: string; exists: boolean }>;
18
+ totalSourceFiles: number;
19
+ tsconfigInfo?: string;
20
+ formatted: string;
21
+ }
22
+
23
+ /** Directories to skip during scanning */
24
+ const SKIP_DIRS = new Set([
25
+ 'node_modules', 'dist', 'build', '.git', '__pycache__', '.venv', 'venv',
26
+ '.next', '.turbo', '.cache', 'coverage', 'out', '.vercel',
27
+ ]);
28
+
29
+ /** Source file extensions to count */
30
+ const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py']);
31
+
32
+ /** Maximum tree entries before truncation */
33
+ const MAX_TREE_ENTRIES = 30;
34
+
35
+ /**
36
+ * Recursively scan a directory up to a given depth, building a tree and counting files
37
+ *
38
+ * @param dir - Directory to scan
39
+ * @param depth - Current recursion depth
40
+ * @param maxDepth - Maximum depth to recurse
41
+ * @returns Tree lines and file counts by extension
42
+ */
43
+ async function scanDirectory(
44
+ dir: string,
45
+ depth: number,
46
+ maxDepth: number
47
+ ): Promise<{ lines: string[]; counts: Record<string, number> }> {
48
+ const lines: string[] = [];
49
+ const counts: Record<string, number> = {};
50
+
51
+ if (depth > maxDepth) return { lines, counts };
52
+
53
+ try {
54
+ const entries = await fs.readdir(dir, { withFileTypes: true });
55
+ const sorted = entries.sort((a, b) => {
56
+ // Directories first, then files
57
+ if (a.isDirectory() && !b.isDirectory()) return -1;
58
+ if (!a.isDirectory() && b.isDirectory()) return 1;
59
+ return a.name.localeCompare(b.name);
60
+ });
61
+
62
+ for (const entry of sorted) {
63
+ if (SKIP_DIRS.has(entry.name)) continue;
64
+
65
+ const indent = ' '.repeat(depth);
66
+ if (entry.isDirectory()) {
67
+ lines.push(`${indent}${entry.name}/`);
68
+ const sub = await scanDirectory(path.join(dir, entry.name), depth + 1, maxDepth);
69
+ lines.push(...sub.lines);
70
+ for (const [ext, count] of Object.entries(sub.counts)) {
71
+ counts[ext] = (counts[ext] || 0) + count;
72
+ }
73
+ } else if (entry.isFile()) {
74
+ const ext = path.extname(entry.name);
75
+ if (SOURCE_EXTENSIONS.has(ext)) {
76
+ counts[ext] = (counts[ext] || 0) + 1;
77
+ }
78
+ lines.push(`${indent}${entry.name}`);
79
+ }
80
+ }
81
+ } catch {
82
+ // Directory access error - ignore
83
+ }
84
+
85
+ return { lines, counts };
86
+ }
87
+
88
+ /**
89
+ * Count source files within a specific directory (shallow recursion, max 5 levels)
90
+ *
91
+ * @param dir - Directory to count files in
92
+ * @returns File counts by extension
93
+ */
94
+ async function countSourceFiles(dir: string): Promise<Record<string, number>> {
95
+ const result = await scanDirectory(dir, 0, 5);
96
+ return result.counts;
97
+ }
98
+
99
+ /**
100
+ * Read and summarize tsconfig.json if present
101
+ *
102
+ * @param projectDir - Project root directory
103
+ * @returns Brief tsconfig summary string or undefined
104
+ */
105
+ async function summarizeTsconfig(projectDir: string): Promise<string | undefined> {
106
+ try {
107
+ const tsconfigPath = path.join(projectDir, 'tsconfig.json');
108
+ const content = await fs.readFile(tsconfigPath, 'utf-8');
109
+ const tsconfig = JSON.parse(content);
110
+
111
+ const parts: string[] = ['tsconfig: exists'];
112
+
113
+ if (tsconfig.references) {
114
+ parts.push(`references=${tsconfig.references.length}`);
115
+ }
116
+ if (tsconfig.include) {
117
+ const includeStr = JSON.stringify(tsconfig.include);
118
+ parts.push(`include=${includeStr.length > 60 ? includeStr.slice(0, 57) + '...' : includeStr}`);
119
+ }
120
+ if (tsconfig.exclude) {
121
+ parts.push(`exclude=${tsconfig.exclude.length} entries`);
122
+ }
123
+ if (tsconfig.compilerOptions?.baseUrl) {
124
+ parts.push(`baseUrl="${tsconfig.compilerOptions.baseUrl}"`);
125
+ }
126
+ if (tsconfig.compilerOptions?.paths) {
127
+ parts.push('paths=true');
128
+ }
129
+
130
+ return parts.join(', ');
131
+ } catch {
132
+ return undefined;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Scan a project directory and return a concise summary for embedding in AI prompts
138
+ *
139
+ * @param projectDir - Root directory of the project
140
+ * @param language - Project language type
141
+ * @returns Project structure summary with tree, counts, and formatted string
142
+ */
143
+ export async function getProjectStructureSummary(
144
+ projectDir: string,
145
+ language: string
146
+ ): Promise<ProjectStructureSummary> {
147
+ // Scan directory tree (max 3 levels deep)
148
+ const { lines: treeLines, counts: fileCounts } = await scanDirectory(projectDir, 0, 3);
149
+
150
+ // Truncate tree if too large
151
+ let tree: string;
152
+ if (treeLines.length > MAX_TREE_ENTRIES) {
153
+ const extra = treeLines.length - MAX_TREE_ENTRIES;
154
+ tree = treeLines.slice(0, MAX_TREE_ENTRIES).join('\n') + `\n... (+${extra} more)`;
155
+ } else {
156
+ tree = treeLines.join('\n');
157
+ }
158
+
159
+ const totalSourceFiles = Object.values(fileCounts).reduce((sum, c) => sum + c, 0);
160
+
161
+ // Workspace app detection
162
+ const workspaceApps: ProjectStructureSummary['workspaceApps'] = [];
163
+ let appFileCounts: Record<string, Record<string, number>> | undefined;
164
+
165
+ if (isWorkspace(language as OutputLanguage)) {
166
+ const apps = languageToApps(language as OutputLanguage);
167
+ appFileCounts = {};
168
+
169
+ for (const appType of apps) {
170
+ const appPath = path.join('apps', appType);
171
+ const absolutePath = path.join(projectDir, appPath);
172
+ let exists = false;
173
+ try {
174
+ await fs.access(absolutePath);
175
+ exists = true;
176
+ } catch {
177
+ // App directory doesn't exist
178
+ }
179
+ workspaceApps.push({ name: appType, path: appPath, exists });
180
+
181
+ if (exists) {
182
+ appFileCounts[appType] = await countSourceFiles(absolutePath);
183
+ }
184
+ }
185
+ }
186
+
187
+ // tsconfig awareness
188
+ const tsconfigInfo = await summarizeTsconfig(projectDir);
189
+
190
+ // Assemble formatted summary
191
+ const formattedParts: string[] = [];
192
+ formattedParts.push(`Source files: ${totalSourceFiles} total`);
193
+
194
+ const countEntries = Object.entries(fileCounts).filter(([, c]) => c > 0);
195
+ if (countEntries.length > 0) {
196
+ formattedParts.push(`Extensions: ${countEntries.map(([ext, c]) => `${ext}=${c}`).join(', ')}`);
197
+ }
198
+
199
+ if (workspaceApps.length > 0) {
200
+ const appSummary = workspaceApps
201
+ .map(a => `${a.name}: ${a.exists ? 'exists' : 'MISSING'}`)
202
+ .join(', ');
203
+ formattedParts.push(`Workspace apps: ${appSummary}`);
204
+
205
+ if (appFileCounts) {
206
+ for (const [appName, counts] of Object.entries(appFileCounts)) {
207
+ const appCountStr = Object.entries(counts)
208
+ .filter(([, c]) => c > 0)
209
+ .map(([ext, c]) => `${ext}=${c}`)
210
+ .join(', ');
211
+ if (appCountStr) {
212
+ formattedParts.push(` ${appName}: ${appCountStr}`);
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ if (tsconfigInfo) {
219
+ formattedParts.push(tsconfigInfo);
220
+ }
221
+
222
+ const formatted = formattedParts.join('\n');
223
+
224
+ return {
225
+ tree,
226
+ fileCounts,
227
+ appFileCounts,
228
+ workspaceApps,
229
+ totalSourceFiles,
230
+ tsconfigInfo,
231
+ formatted,
232
+ };
233
+ }
@@ -36,6 +36,14 @@ export interface VerificationReport {
36
36
  criticalIssues: string[];
37
37
  }
38
38
 
39
+ /**
40
+ * Resolved project paths for frontend and backend directories
41
+ */
42
+ export interface ProjectPaths {
43
+ frontendDir: string | null;
44
+ backendDir: string | null;
45
+ }
46
+
39
47
  /**
40
48
  * Check if a file exists
41
49
  */
@@ -125,15 +133,66 @@ function findTodoPlaceholders(content: string): string[] {
125
133
  return todos;
126
134
  }
127
135
 
136
+ /**
137
+ * Resolve the correct frontend/backend paths based on language and what exists on disk.
138
+ *
139
+ * Workspace projects (fullstack, all) use apps/ or packages/ subdirectories.
140
+ * Single-language projects use the project root as their frontend or backend dir.
141
+ *
142
+ * @param projectDir - The project root directory
143
+ * @param language - The project language/type
144
+ * @returns Resolved frontend and backend directory paths (null if not applicable)
145
+ */
146
+ export async function resolveProjectPaths(projectDir: string, language: string): Promise<ProjectPaths> {
147
+ // Workspace projects (fullstack, all): check apps/ first, then packages/
148
+ if (language === 'fullstack' || language === 'all') {
149
+ const appsF = path.join(projectDir, 'apps', 'frontend');
150
+ const pkgsF = path.join(projectDir, 'packages', 'frontend');
151
+ const appsB = path.join(projectDir, 'apps', 'backend');
152
+ const pkgsB = path.join(projectDir, 'packages', 'backend');
153
+
154
+ return {
155
+ frontendDir: await fileExists(appsF) ? appsF : await fileExists(pkgsF) ? pkgsF : null,
156
+ backendDir: await fileExists(appsB) ? appsB : await fileExists(pkgsB) ? pkgsB : null,
157
+ };
158
+ }
159
+
160
+ // Website: root IS the frontend
161
+ if (language === 'website') {
162
+ return { frontendDir: projectDir, backendDir: null };
163
+ }
164
+
165
+ // TypeScript/JavaScript: root IS the frontend
166
+ if (language === 'typescript' || language === 'javascript') {
167
+ return { frontendDir: projectDir, backendDir: null };
168
+ }
169
+
170
+ // Python: root IS the backend
171
+ if (language === 'python') {
172
+ return { frontendDir: null, backendDir: projectDir };
173
+ }
174
+
175
+ return { frontendDir: null, backendDir: null };
176
+ }
177
+
128
178
  /**
129
179
  * Verify CSS/Styling setup
130
180
  */
131
- async function verifyStylingSetup(projectDir: string): Promise<VerificationResult[]> {
181
+ async function verifyStylingSetup(paths: ProjectPaths): Promise<VerificationResult[]> {
132
182
  const results: VerificationResult[] = [];
133
- const frontendDir = path.join(projectDir, 'packages', 'frontend');
183
+ const frontendDir = paths.frontendDir;
184
+
185
+ if (!frontendDir) {
186
+ return results;
187
+ }
134
188
 
135
189
  // Check if frontend uses Tailwind classes
136
- const tsxFiles = await findFiles(path.join(frontendDir, 'src'), /\.tsx$/);
190
+ const srcDir = path.join(frontendDir, 'src');
191
+ if (!await fileExists(srcDir)) {
192
+ return results;
193
+ }
194
+
195
+ const tsxFiles = await findFiles(srcDir, /\.tsx$/);
137
196
  let usesTailwind = false;
138
197
 
139
198
  for (const file of tsxFiles.slice(0, 20)) { // Check first 20 files
@@ -216,9 +275,13 @@ async function verifyStylingSetup(projectDir: string): Promise<VerificationResul
216
275
  /**
217
276
  * Verify authentication setup
218
277
  */
219
- async function verifyAuthSetup(projectDir: string): Promise<VerificationResult[]> {
278
+ async function verifyAuthSetup(paths: ProjectPaths): Promise<VerificationResult[]> {
220
279
  const results: VerificationResult[] = [];
221
- const frontendDir = path.join(projectDir, 'packages', 'frontend');
280
+ const frontendDir = paths.frontendDir;
281
+
282
+ if (!frontendDir) {
283
+ return results;
284
+ }
222
285
 
223
286
  // Check if project uses Auth0
224
287
  const pkgJson = await readFile(path.join(frontendDir, 'package.json'));
@@ -265,9 +328,13 @@ async function verifyAuthSetup(projectDir: string): Promise<VerificationResult[]
265
328
  /**
266
329
  * Verify routes are complete (no TODO placeholders)
267
330
  */
268
- async function verifyRouteCompleteness(projectDir: string): Promise<VerificationResult[]> {
331
+ async function verifyRouteCompleteness(paths: ProjectPaths): Promise<VerificationResult[]> {
269
332
  const results: VerificationResult[] = [];
270
- const frontendDir = path.join(projectDir, 'packages', 'frontend');
333
+ const frontendDir = paths.frontendDir;
334
+
335
+ if (!frontendDir) {
336
+ return results;
337
+ }
271
338
 
272
339
  // Check routes file
273
340
  const routesFile = await readFile(path.join(frontendDir, 'src', 'routes', 'index.tsx'));
@@ -288,7 +355,12 @@ async function verifyRouteCompleteness(projectDir: string): Promise<Verification
288
355
  }
289
356
 
290
357
  // Check all page components
291
- const pageFiles = await findFiles(path.join(frontendDir, 'src', 'pages'), /\.tsx$/);
358
+ const pagesDir = path.join(frontendDir, 'src', 'pages');
359
+ if (!await fileExists(pagesDir)) {
360
+ return results;
361
+ }
362
+
363
+ const pageFiles = await findFiles(pagesDir, /\.tsx$/);
292
364
  const incompletePages: string[] = [];
293
365
 
294
366
  for (const file of pageFiles) {
@@ -325,13 +397,15 @@ async function verifyRouteCompleteness(projectDir: string): Promise<Verification
325
397
  /**
326
398
  * Verify database setup
327
399
  */
328
- async function verifyDatabaseSetup(projectDir: string): Promise<VerificationResult[]> {
400
+ async function verifyDatabaseSetup(projectDir: string, paths: ProjectPaths): Promise<VerificationResult[]> {
329
401
  const results: VerificationResult[] = [];
330
- const backendDir = path.join(projectDir, 'packages', 'backend');
402
+ const backendDir = paths.backendDir;
331
403
 
332
- // Check .env.example has database config
333
- const envExample = await readFile(path.join(projectDir, '.env.example')) ||
334
- await readFile(path.join(backendDir, '.env.example'));
404
+ // Check .env.example has database config (check root and backend dir)
405
+ let envExample = await readFile(path.join(projectDir, '.env.example'));
406
+ if (!envExample && backendDir) {
407
+ envExample = await readFile(path.join(backendDir, '.env.example'));
408
+ }
335
409
 
336
410
  const hasDbConfig = envExample?.includes('DATABASE_URL') ||
337
411
  envExample?.includes('DB_HOST');
@@ -368,11 +442,35 @@ async function verifyDatabaseSetup(projectDir: string): Promise<VerificationResu
368
442
  /**
369
443
  * Verify the app actually starts
370
444
  */
371
- async function verifyAppStarts(projectDir: string): Promise<VerificationResult[]> {
445
+ async function verifyAppStarts(paths: ProjectPaths): Promise<VerificationResult[]> {
372
446
  const results: VerificationResult[] = [];
373
- const frontendDir = path.join(projectDir, 'packages', 'frontend');
447
+ const frontendDir = paths.frontendDir;
448
+
449
+ if (!frontendDir) {
450
+ return results;
451
+ }
452
+
453
+ // Verify directory exists before attempting build
454
+ if (!await fileExists(frontendDir)) {
455
+ return results;
456
+ }
457
+
458
+ // Check package.json exists and has a build script
459
+ const pkgJsonContent = await readFile(path.join(frontendDir, 'package.json'));
460
+ if (!pkgJsonContent) {
461
+ return results;
462
+ }
374
463
 
375
- // Try to start frontend briefly
464
+ try {
465
+ const pkgJson = JSON.parse(pkgJsonContent);
466
+ if (!pkgJson.scripts?.build) {
467
+ return results;
468
+ }
469
+ } catch {
470
+ return results;
471
+ }
472
+
473
+ // Try to build frontend
376
474
  try {
377
475
  await execAsync('npm run build', {
378
476
  cwd: frontendDir,
@@ -404,27 +502,36 @@ async function verifyAppStarts(projectDir: string): Promise<VerificationResult[]
404
502
 
405
503
  /**
406
504
  * Run comprehensive project verification
505
+ *
506
+ * @param projectDir - The project root directory
507
+ * @param language - The project language/type (e.g. 'fullstack', 'typescript', 'python')
508
+ * @param onProgress - Optional progress callback
509
+ * @returns Verification report
407
510
  */
408
511
  export async function runComprehensiveVerification(
409
512
  projectDir: string,
513
+ language: string,
410
514
  onProgress?: (message: string) => void
411
515
  ): Promise<VerificationReport> {
412
516
  const allResults: VerificationResult[] = [];
413
517
 
518
+ // Resolve correct paths based on language and disk layout
519
+ const paths = await resolveProjectPaths(projectDir, language);
520
+
414
521
  onProgress?.('Checking styling setup...');
415
- allResults.push(...await verifyStylingSetup(projectDir));
522
+ allResults.push(...await verifyStylingSetup(paths));
416
523
 
417
524
  onProgress?.('Checking authentication setup...');
418
- allResults.push(...await verifyAuthSetup(projectDir));
525
+ allResults.push(...await verifyAuthSetup(paths));
419
526
 
420
527
  onProgress?.('Checking route completeness...');
421
- allResults.push(...await verifyRouteCompleteness(projectDir));
528
+ allResults.push(...await verifyRouteCompleteness(paths));
422
529
 
423
530
  onProgress?.('Checking database setup...');
424
- allResults.push(...await verifyDatabaseSetup(projectDir));
531
+ allResults.push(...await verifyDatabaseSetup(projectDir, paths));
425
532
 
426
533
  onProgress?.('Verifying app builds...');
427
- allResults.push(...await verifyAppStarts(projectDir));
534
+ allResults.push(...await verifyAppStarts(paths));
428
535
 
429
536
  // Calculate summary
430
537
  const passedChecks = allResults.filter(r => r.passed).length;