@haystackeditor/cli 0.8.0 → 0.9.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.
Files changed (76) hide show
  1. package/README.md +105 -12
  2. package/dist/assets/hooks/agent-context/detect.ts +136 -0
  3. package/dist/assets/hooks/agent-context/format.ts +99 -0
  4. package/dist/assets/hooks/agent-context/index.ts +39 -0
  5. package/dist/assets/hooks/agent-context/parsers/claude.ts +253 -0
  6. package/dist/assets/hooks/agent-context/parsers/gemini.ts +155 -0
  7. package/dist/assets/hooks/agent-context/parsers/opencode.ts +174 -0
  8. package/dist/assets/hooks/agent-context/tsconfig.json +13 -0
  9. package/dist/assets/hooks/agent-context/types.ts +58 -0
  10. package/dist/assets/hooks/llm-rules-template.md +56 -0
  11. package/dist/assets/hooks/package.json +11 -0
  12. package/dist/assets/hooks/scripts/commit-msg.sh +4 -0
  13. package/dist/assets/hooks/scripts/post-commit.sh +4 -0
  14. package/dist/assets/hooks/scripts/pre-commit.sh +92 -0
  15. package/dist/assets/hooks/scripts/pre-push.sh +25 -0
  16. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
  17. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +528 -0
  18. package/dist/assets/hooks/truncation-checker/index.ts +595 -0
  19. package/dist/assets/hooks/truncation-checker/tsconfig.json +13 -0
  20. package/dist/assets/skills/prepare-haystack.md +323 -0
  21. package/dist/assets/skills/secrets.md +164 -0
  22. package/dist/assets/skills/setup-external-sandbox.md +243 -0
  23. package/dist/assets/skills/setup-haystack.md +639 -0
  24. package/dist/assets/skills/submit.md +154 -0
  25. package/dist/assets/templates/CLAUDE.md.snippet +42 -0
  26. package/dist/assets/templates/haystack.yml +193 -0
  27. package/dist/commands/check-pending.d.ts +19 -0
  28. package/dist/commands/check-pending.js +217 -0
  29. package/dist/commands/config.d.ts +18 -12
  30. package/dist/commands/config.js +327 -52
  31. package/dist/commands/hooks.d.ts +17 -0
  32. package/dist/commands/hooks.js +269 -0
  33. package/dist/commands/install-session-hooks.d.ts +16 -0
  34. package/dist/commands/install-session-hooks.js +302 -0
  35. package/dist/commands/login.js +1 -1
  36. package/dist/commands/policy.d.ts +31 -0
  37. package/dist/commands/policy.js +365 -0
  38. package/dist/commands/skills.d.ts +8 -0
  39. package/dist/commands/skills.js +80 -0
  40. package/dist/commands/submit.d.ts +22 -0
  41. package/dist/commands/submit.js +428 -0
  42. package/dist/commands/triage.d.ts +16 -0
  43. package/dist/commands/triage.js +354 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.js +317 -2
  46. package/dist/tools/detect.d.ts +50 -0
  47. package/dist/tools/detect.js +853 -0
  48. package/dist/tools/fixtures.d.ts +38 -0
  49. package/dist/tools/fixtures.js +199 -0
  50. package/dist/tools/setup.d.ts +43 -0
  51. package/dist/tools/setup.js +597 -0
  52. package/dist/triage/prompts.d.ts +31 -0
  53. package/dist/triage/prompts.js +266 -0
  54. package/dist/triage/runner.d.ts +21 -0
  55. package/dist/triage/runner.js +325 -0
  56. package/dist/triage/traces.d.ts +20 -0
  57. package/dist/triage/traces.js +305 -0
  58. package/dist/triage/types.d.ts +47 -0
  59. package/dist/triage/types.js +7 -0
  60. package/dist/types.d.ts +1387 -191
  61. package/dist/types.js +254 -2
  62. package/dist/utils/analysis-api.d.ts +108 -0
  63. package/dist/utils/analysis-api.js +194 -0
  64. package/dist/utils/config.js +1 -1
  65. package/dist/utils/git.d.ts +80 -0
  66. package/dist/utils/git.js +302 -0
  67. package/dist/utils/github-api.d.ts +83 -0
  68. package/dist/utils/github-api.js +266 -0
  69. package/dist/utils/hooks.d.ts +26 -0
  70. package/dist/utils/hooks.js +226 -0
  71. package/dist/utils/pending-state.d.ts +38 -0
  72. package/dist/utils/pending-state.js +86 -0
  73. package/dist/utils/secrets.js +3 -3
  74. package/dist/utils/skill.d.ts +1 -1
  75. package/dist/utils/skill.js +658 -1
  76. package/package.json +5 -3
@@ -0,0 +1,853 @@
1
+ import { existsSync, readFileSync, statSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { globSync } from 'glob';
4
+ const FRAMEWORK_PATTERNS = [
5
+ // Vite
6
+ {
7
+ files: ['vite.config.ts', 'vite.config.js', 'vite.config.mjs'],
8
+ framework: {
9
+ name: 'vite',
10
+ devCommand: 'pnpm dev',
11
+ port: 5173,
12
+ readyPattern: 'Local:',
13
+ },
14
+ },
15
+ // Next.js
16
+ {
17
+ files: ['next.config.js', 'next.config.mjs', 'next.config.ts'],
18
+ framework: {
19
+ name: 'nextjs',
20
+ devCommand: 'pnpm dev',
21
+ port: 3000,
22
+ readyPattern: 'Ready in',
23
+ },
24
+ },
25
+ // Remix
26
+ {
27
+ files: ['remix.config.js', 'remix.config.ts'],
28
+ framework: {
29
+ name: 'remix',
30
+ devCommand: 'pnpm dev',
31
+ port: 3000,
32
+ readyPattern: 'started',
33
+ },
34
+ },
35
+ // Nuxt
36
+ {
37
+ files: ['nuxt.config.ts', 'nuxt.config.js'],
38
+ framework: {
39
+ name: 'nuxt',
40
+ devCommand: 'pnpm dev',
41
+ port: 3000,
42
+ readyPattern: 'Listening',
43
+ },
44
+ },
45
+ // SvelteKit
46
+ {
47
+ files: ['svelte.config.js', 'svelte.config.ts'],
48
+ framework: {
49
+ name: 'sveltekit',
50
+ devCommand: 'pnpm dev',
51
+ port: 5173,
52
+ readyPattern: 'Local:',
53
+ },
54
+ },
55
+ // Astro
56
+ {
57
+ files: ['astro.config.mjs', 'astro.config.ts'],
58
+ framework: {
59
+ name: 'astro',
60
+ devCommand: 'pnpm dev',
61
+ port: 4321,
62
+ readyPattern: 'Local',
63
+ },
64
+ },
65
+ // Create React App
66
+ {
67
+ files: ['react-scripts'], // Check in package.json dependencies
68
+ framework: {
69
+ name: 'create-react-app',
70
+ devCommand: 'npm start',
71
+ port: 3000,
72
+ readyPattern: 'Compiled',
73
+ },
74
+ },
75
+ // Cloudflare Workers (Wrangler)
76
+ {
77
+ files: ['wrangler.toml', 'wrangler.json'],
78
+ framework: {
79
+ name: 'cloudflare-worker',
80
+ devCommand: 'pnpm wrangler dev --local',
81
+ port: 8787,
82
+ readyPattern: 'Ready on',
83
+ },
84
+ },
85
+ // Express/Node.js (generic)
86
+ {
87
+ files: ['server.js', 'server.ts', 'app.js', 'app.ts'],
88
+ framework: {
89
+ name: 'node-server',
90
+ devCommand: 'pnpm dev',
91
+ port: 3000,
92
+ readyPattern: 'listening',
93
+ },
94
+ },
95
+ ];
96
+ const MONOREPO_INDICATORS = [
97
+ 'pnpm-workspace.yaml',
98
+ 'lerna.json',
99
+ 'nx.json',
100
+ 'turbo.json',
101
+ 'rush.json',
102
+ ];
103
+ const PACKAGE_MANAGER_LOCKFILES = {
104
+ 'pnpm-lock.yaml': 'pnpm',
105
+ 'yarn.lock': 'yarn',
106
+ 'package-lock.json': 'npm',
107
+ 'bun.lockb': 'bun',
108
+ };
109
+ /**
110
+ * Detect the package manager from lockfiles
111
+ */
112
+ function detectPackageManager(rootDir) {
113
+ for (const [lockfile, manager] of Object.entries(PACKAGE_MANAGER_LOCKFILES)) {
114
+ if (existsSync(join(rootDir, lockfile))) {
115
+ return manager;
116
+ }
117
+ }
118
+ return 'npm';
119
+ }
120
+ /**
121
+ * Check if a file exists in a directory
122
+ */
123
+ function hasFile(dir, filename) {
124
+ return existsSync(join(dir, filename));
125
+ }
126
+ /**
127
+ * Detect framework from files in a directory
128
+ */
129
+ function detectFramework(dir) {
130
+ for (const pattern of FRAMEWORK_PATTERNS) {
131
+ for (const file of pattern.files) {
132
+ if (hasFile(dir, file)) {
133
+ return {
134
+ ...pattern.framework,
135
+ configFile: file,
136
+ };
137
+ }
138
+ }
139
+ }
140
+ // Check package.json for react-scripts (CRA)
141
+ const pkgPath = join(dir, 'package.json');
142
+ if (existsSync(pkgPath)) {
143
+ try {
144
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
145
+ if (pkg.dependencies?.['react-scripts'] || pkg.devDependencies?.['react-scripts']) {
146
+ return {
147
+ name: 'create-react-app',
148
+ devCommand: 'npm start',
149
+ port: 3000,
150
+ readyPattern: 'Compiled',
151
+ configFile: 'package.json',
152
+ };
153
+ }
154
+ }
155
+ catch {
156
+ // Ignore parse errors
157
+ }
158
+ }
159
+ return null;
160
+ }
161
+ /**
162
+ * Detect if project is a monorepo
163
+ */
164
+ function detectMonorepo(rootDir) {
165
+ return MONOREPO_INDICATORS.some((file) => hasFile(rootDir, file));
166
+ }
167
+ /**
168
+ * Find workspace packages in a pnpm monorepo
169
+ */
170
+ function findPnpmWorkspaces(rootDir) {
171
+ const workspaceFile = join(rootDir, 'pnpm-workspace.yaml');
172
+ if (!existsSync(workspaceFile))
173
+ return [];
174
+ try {
175
+ const content = readFileSync(workspaceFile, 'utf-8');
176
+ const lines = content.split('\n');
177
+ const packages = [];
178
+ let inPackages = false;
179
+ for (const line of lines) {
180
+ // Check for packages: section start
181
+ if (line.match(/^packages:/)) {
182
+ inPackages = true;
183
+ continue;
184
+ }
185
+ // New section starts (non-indented, non-comment line)
186
+ if (inPackages && line.match(/^[a-z]/)) {
187
+ break;
188
+ }
189
+ // Parse package entries (handles comments between entries)
190
+ if (inPackages) {
191
+ const trimmed = line.trim();
192
+ if (trimmed.startsWith('-') && !trimmed.startsWith('#')) {
193
+ // Extract package path, removing quotes and inline comments
194
+ const pkg = trimmed
195
+ .replace(/^-\s*['"]?/, '')
196
+ .replace(/['"]?\s*$/, '')
197
+ .replace(/#.*/, '')
198
+ .trim();
199
+ if (pkg)
200
+ packages.push(pkg);
201
+ }
202
+ }
203
+ }
204
+ return packages;
205
+ }
206
+ catch {
207
+ return [];
208
+ }
209
+ }
210
+ /**
211
+ * Expand glob patterns to actual directories
212
+ */
213
+ function expandWorkspaceGlobs(rootDir, patterns) {
214
+ const dirs = [];
215
+ for (const pattern of patterns) {
216
+ // Remove trailing /* or /**
217
+ const cleanPattern = pattern.replace(/\/\*+$/, '');
218
+ try {
219
+ // Use glob to find matches
220
+ const matches = globSync(cleanPattern, { cwd: rootDir });
221
+ // Filter to only directories
222
+ for (const match of matches) {
223
+ const fullPath = join(rootDir, match);
224
+ try {
225
+ if (statSync(fullPath).isDirectory()) {
226
+ dirs.push(match);
227
+ }
228
+ }
229
+ catch {
230
+ // Skip if stat fails
231
+ }
232
+ }
233
+ }
234
+ catch {
235
+ // If glob fails, try as literal path
236
+ if (existsSync(join(rootDir, cleanPattern))) {
237
+ dirs.push(cleanPattern);
238
+ }
239
+ }
240
+ }
241
+ return [...new Set(dirs)];
242
+ }
243
+ /**
244
+ * Detect services in a monorepo
245
+ */
246
+ function detectServices(rootDir) {
247
+ const services = [];
248
+ // Find workspace packages
249
+ const workspacePatterns = findPnpmWorkspaces(rootDir);
250
+ const packageDirs = expandWorkspaceGlobs(rootDir, workspacePatterns);
251
+ // Also check common directories
252
+ const commonDirs = [
253
+ 'packages',
254
+ 'apps',
255
+ 'services',
256
+ 'infra',
257
+ 'workers',
258
+ 'functions',
259
+ 'api',
260
+ 'web',
261
+ 'frontend',
262
+ 'backend',
263
+ ];
264
+ for (const dir of commonDirs) {
265
+ if (existsSync(join(rootDir, dir)) && !packageDirs.includes(dir)) {
266
+ packageDirs.push(dir);
267
+ }
268
+ }
269
+ // Detect framework in each directory
270
+ for (const dir of packageDirs) {
271
+ const fullPath = join(rootDir, dir);
272
+ if (!existsSync(fullPath))
273
+ continue;
274
+ // Check if this directory has its own package.json (is a package)
275
+ if (!hasFile(fullPath, 'package.json'))
276
+ continue;
277
+ const framework = detectFramework(fullPath);
278
+ const name = dir.split('/').pop() || dir;
279
+ services.push({
280
+ name,
281
+ root: dir,
282
+ type: framework?.name === 'cloudflare-worker' ? 'server' : 'server',
283
+ framework: framework || undefined,
284
+ });
285
+ }
286
+ // Also check the root directory
287
+ const rootFramework = detectFramework(rootDir);
288
+ if (rootFramework) {
289
+ services.unshift({
290
+ name: 'frontend',
291
+ root: './',
292
+ type: 'server',
293
+ framework: rootFramework,
294
+ });
295
+ }
296
+ return services;
297
+ }
298
+ /**
299
+ * Get project name from package.json
300
+ */
301
+ function getProjectName(rootDir) {
302
+ const pkgPath = join(rootDir, 'package.json');
303
+ if (existsSync(pkgPath)) {
304
+ try {
305
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
306
+ return pkg.name || 'my-app';
307
+ }
308
+ catch {
309
+ // Ignore
310
+ }
311
+ }
312
+ return 'my-app';
313
+ }
314
+ /**
315
+ * Detect common auth bypass patterns
316
+ */
317
+ function detectAuthBypass(rootDir) {
318
+ // Check for common env var patterns in code
319
+ const envFiles = ['.env.example', '.env.sample', '.env.template'];
320
+ for (const envFile of envFiles) {
321
+ const path = join(rootDir, envFile);
322
+ if (existsSync(path)) {
323
+ try {
324
+ const content = readFileSync(path, 'utf-8');
325
+ // Look for auth-related env vars
326
+ const authPatterns = [
327
+ /^(SKIP_AUTH|BYPASS_AUTH|DISABLE_AUTH|VITE_SKIP_AUTH|NEXT_PUBLIC_SKIP_AUTH)=/m,
328
+ /^(AUTH_DISABLED|NO_AUTH|DEV_AUTH)=/m,
329
+ ];
330
+ for (const pattern of authPatterns) {
331
+ const match = content.match(pattern);
332
+ if (match) {
333
+ return `${match[1]}=true`;
334
+ }
335
+ }
336
+ }
337
+ catch {
338
+ // Ignore
339
+ }
340
+ }
341
+ }
342
+ return undefined;
343
+ }
344
+ /**
345
+ * Extract localhost URLs and ports from a string.
346
+ * Handles patterns like: http://localhost:8787, localhost:3001, 127.0.0.1:8080
347
+ */
348
+ function extractLocalhostTargets(content, pathPattern) {
349
+ const targets = [];
350
+ const urlRegex = /(?:https?:\/\/)?(?:localhost|127\.0\.0\.1):(\d+)/g;
351
+ let match;
352
+ while ((match = urlRegex.exec(content)) !== null) {
353
+ const port = parseInt(match[1], 10);
354
+ const target = match[0].startsWith('http') ? match[0] : `http://${match[0]}`;
355
+ // Avoid duplicates
356
+ if (!targets.some((t) => t.port === port)) {
357
+ targets.push({
358
+ path: pathPattern || '/api',
359
+ target,
360
+ port,
361
+ });
362
+ }
363
+ }
364
+ return targets;
365
+ }
366
+ /**
367
+ * Detect proxy targets from various config files.
368
+ * Supports: Vite, Next.js, Create React App, Vercel, Webpack
369
+ * This helps identify backend services the frontend depends on.
370
+ */
371
+ function detectProxyTargets(dir) {
372
+ const targets = [];
373
+ const seenPorts = new Set();
374
+ const addTarget = (target) => {
375
+ if (!seenPorts.has(target.port)) {
376
+ seenPorts.add(target.port);
377
+ targets.push(target);
378
+ }
379
+ };
380
+ // 1. Vite - vite.config.ts/js with server.proxy
381
+ const viteConfigs = ['vite.config.ts', 'vite.config.js', 'vite.config.mjs'];
382
+ for (const configFile of viteConfigs) {
383
+ const configPath = join(dir, configFile);
384
+ if (existsSync(configPath)) {
385
+ try {
386
+ const content = readFileSync(configPath, 'utf-8');
387
+ // Look for proxy patterns like: '/api': { target: 'http://localhost:8787' }
388
+ const proxyMatches = content.matchAll(/['"]([^'"]+)['"]\s*:\s*(?:\{[^}]*target\s*:\s*['"]([^'"]+)['"]|['"]([^'"]+)['"])/g);
389
+ for (const match of proxyMatches) {
390
+ const path = match[1];
391
+ const target = match[2] || match[3];
392
+ if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
393
+ const portMatch = target.match(/:(\d+)/);
394
+ if (portMatch) {
395
+ addTarget({
396
+ path,
397
+ target,
398
+ port: parseInt(portMatch[1], 10),
399
+ });
400
+ }
401
+ }
402
+ }
403
+ }
404
+ catch {
405
+ // Ignore parse errors
406
+ }
407
+ }
408
+ }
409
+ // 2. Next.js - next.config.js/mjs/ts with rewrites()
410
+ const nextConfigs = ['next.config.js', 'next.config.mjs', 'next.config.ts'];
411
+ for (const configFile of nextConfigs) {
412
+ const configPath = join(dir, configFile);
413
+ if (existsSync(configPath)) {
414
+ try {
415
+ const content = readFileSync(configPath, 'utf-8');
416
+ // Look for rewrite patterns: { source: '/api/:path*', destination: 'http://localhost:3001/:path*' }
417
+ const rewriteMatches = content.matchAll(/destination\s*:\s*['"`]([^'"`]+)['"`]/g);
418
+ for (const match of rewriteMatches) {
419
+ const destination = match[1];
420
+ if (destination.includes('localhost') || destination.includes('127.0.0.1')) {
421
+ const portMatch = destination.match(/:(\d+)/);
422
+ if (portMatch) {
423
+ // Try to find the associated source
424
+ const sourceMatch = content.match(new RegExp(`source\\s*:\\s*['"\`]([^'"\`]+)['"\`][^}]*destination\\s*:\\s*['"\`]${destination.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
425
+ addTarget({
426
+ path: sourceMatch?.[1] || '/api',
427
+ target: destination.replace(/\/:[^/]+\*?/g, ''), // Remove path params
428
+ port: parseInt(portMatch[1], 10),
429
+ });
430
+ }
431
+ }
432
+ }
433
+ }
434
+ catch {
435
+ // Ignore parse errors
436
+ }
437
+ }
438
+ }
439
+ // 3. Create React App - package.json with "proxy" field
440
+ const pkgPath = join(dir, 'package.json');
441
+ if (existsSync(pkgPath)) {
442
+ try {
443
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
444
+ if (typeof pkg.proxy === 'string') {
445
+ const portMatch = pkg.proxy.match(/:(\d+)/);
446
+ if (portMatch) {
447
+ addTarget({
448
+ path: '/api',
449
+ target: pkg.proxy,
450
+ port: parseInt(portMatch[1], 10),
451
+ });
452
+ }
453
+ }
454
+ }
455
+ catch {
456
+ // Ignore parse errors
457
+ }
458
+ }
459
+ // 4. Create React App - setupProxy.js (http-proxy-middleware)
460
+ const setupProxyPath = join(dir, 'src', 'setupProxy.js');
461
+ if (existsSync(setupProxyPath)) {
462
+ try {
463
+ const content = readFileSync(setupProxyPath, 'utf-8');
464
+ // Look for: createProxyMiddleware('/api', { target: 'http://localhost:3001' })
465
+ const proxyMatches = content.matchAll(/createProxyMiddleware\s*\(\s*['"]([^'"]+)['"]\s*,\s*\{[^}]*target\s*:\s*['"]([^'"]+)['"]/g);
466
+ for (const match of proxyMatches) {
467
+ const path = match[1];
468
+ const target = match[2];
469
+ const portMatch = target.match(/:(\d+)/);
470
+ if (portMatch) {
471
+ addTarget({
472
+ path,
473
+ target,
474
+ port: parseInt(portMatch[1], 10),
475
+ });
476
+ }
477
+ }
478
+ }
479
+ catch {
480
+ // Ignore parse errors
481
+ }
482
+ }
483
+ // 5. Vercel - vercel.json with rewrites
484
+ const vercelPath = join(dir, 'vercel.json');
485
+ if (existsSync(vercelPath)) {
486
+ try {
487
+ const vercelConfig = JSON.parse(readFileSync(vercelPath, 'utf-8'));
488
+ if (Array.isArray(vercelConfig.rewrites)) {
489
+ for (const rewrite of vercelConfig.rewrites) {
490
+ if (rewrite.destination && (rewrite.destination.includes('localhost') || rewrite.destination.includes('127.0.0.1'))) {
491
+ const portMatch = rewrite.destination.match(/:(\d+)/);
492
+ if (portMatch) {
493
+ addTarget({
494
+ path: rewrite.source || '/api',
495
+ target: rewrite.destination,
496
+ port: parseInt(portMatch[1], 10),
497
+ });
498
+ }
499
+ }
500
+ }
501
+ }
502
+ }
503
+ catch {
504
+ // Ignore parse errors
505
+ }
506
+ }
507
+ // 6. Webpack - webpack.config.js with devServer.proxy
508
+ const webpackConfigs = ['webpack.config.js', 'webpack.config.ts'];
509
+ for (const configFile of webpackConfigs) {
510
+ const configPath = join(dir, configFile);
511
+ if (existsSync(configPath)) {
512
+ try {
513
+ const content = readFileSync(configPath, 'utf-8');
514
+ // Look for devServer.proxy patterns
515
+ const proxyMatches = content.matchAll(/['"]([^'"]+)['"]\s*:\s*(?:\{[^}]*target\s*:\s*['"]([^'"]+)['"]|['"]([^'"]+)['"])/g);
516
+ for (const match of proxyMatches) {
517
+ const path = match[1];
518
+ const target = match[2] || match[3];
519
+ if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
520
+ const portMatch = target.match(/:(\d+)/);
521
+ if (portMatch) {
522
+ addTarget({
523
+ path,
524
+ target,
525
+ port: parseInt(portMatch[1], 10),
526
+ });
527
+ }
528
+ }
529
+ }
530
+ }
531
+ catch {
532
+ // Ignore parse errors
533
+ }
534
+ }
535
+ }
536
+ // 7. Vue CLI - vue.config.js with devServer.proxy (same pattern as webpack)
537
+ const vueConfigPath = join(dir, 'vue.config.js');
538
+ if (existsSync(vueConfigPath)) {
539
+ try {
540
+ const content = readFileSync(vueConfigPath, 'utf-8');
541
+ const proxyMatches = content.matchAll(/['"]([^'"]+)['"]\s*:\s*(?:\{[^}]*target\s*:\s*['"]([^'"]+)['"]|['"]([^'"]+)['"])/g);
542
+ for (const match of proxyMatches) {
543
+ const path = match[1];
544
+ const target = match[2] || match[3];
545
+ if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
546
+ const portMatch = target.match(/:(\d+)/);
547
+ if (portMatch) {
548
+ addTarget({
549
+ path,
550
+ target,
551
+ port: parseInt(portMatch[1], 10),
552
+ });
553
+ }
554
+ }
555
+ }
556
+ }
557
+ catch {
558
+ // Ignore parse errors
559
+ }
560
+ }
561
+ // 8. Angular - proxy.conf.json or proxy.conf.js
562
+ const angularProxyConfigs = ['proxy.conf.json', 'proxy.conf.js'];
563
+ for (const configFile of angularProxyConfigs) {
564
+ const configPath = join(dir, configFile);
565
+ if (existsSync(configPath)) {
566
+ try {
567
+ const content = readFileSync(configPath, 'utf-8');
568
+ if (configFile.endsWith('.json')) {
569
+ // JSON format: { "/api": { "target": "http://localhost:3000" } }
570
+ const config = JSON.parse(content);
571
+ for (const [path, settings] of Object.entries(config)) {
572
+ const target = settings?.target;
573
+ if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
574
+ const portMatch = target.match(/:(\d+)/);
575
+ if (portMatch) {
576
+ addTarget({
577
+ path,
578
+ target,
579
+ port: parseInt(portMatch[1], 10),
580
+ });
581
+ }
582
+ }
583
+ }
584
+ }
585
+ else {
586
+ // JS format - use regex
587
+ const proxyMatches = content.matchAll(/['"]([^'"]+)['"]\s*:\s*\{[^}]*target\s*:\s*['"]([^'"]+)['"]/g);
588
+ for (const match of proxyMatches) {
589
+ const path = match[1];
590
+ const target = match[2];
591
+ if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
592
+ const portMatch = target.match(/:(\d+)/);
593
+ if (portMatch) {
594
+ addTarget({
595
+ path,
596
+ target,
597
+ port: parseInt(portMatch[1], 10),
598
+ });
599
+ }
600
+ }
601
+ }
602
+ }
603
+ }
604
+ catch {
605
+ // Ignore parse errors
606
+ }
607
+ }
608
+ }
609
+ // 9. Nuxt.js - nuxt.config.ts/js with routeRules or proxy
610
+ const nuxtConfigs = ['nuxt.config.ts', 'nuxt.config.js'];
611
+ for (const configFile of nuxtConfigs) {
612
+ const configPath = join(dir, configFile);
613
+ if (existsSync(configPath)) {
614
+ try {
615
+ const content = readFileSync(configPath, 'utf-8');
616
+ // Look for proxy or routeRules with proxy settings
617
+ const proxyMatches = content.matchAll(/(?:proxy|target)\s*:\s*['"]([^'"]+)['"]/g);
618
+ for (const match of proxyMatches) {
619
+ const target = match[1];
620
+ if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
621
+ const portMatch = target.match(/:(\d+)/);
622
+ if (portMatch) {
623
+ addTarget({
624
+ path: '/api',
625
+ target,
626
+ port: parseInt(portMatch[1], 10),
627
+ });
628
+ }
629
+ }
630
+ }
631
+ }
632
+ catch {
633
+ // Ignore parse errors
634
+ }
635
+ }
636
+ }
637
+ // 10. Gatsby - gatsby-config.js with proxy
638
+ const gatsbyConfigPath = join(dir, 'gatsby-config.js');
639
+ if (existsSync(gatsbyConfigPath)) {
640
+ try {
641
+ const content = readFileSync(gatsbyConfigPath, 'utf-8');
642
+ // Look for developMiddleware proxy patterns
643
+ const proxyMatches = content.matchAll(/proxy\s*:\s*\{[^}]*url\s*:\s*['"]([^'"]+)['"]/g);
644
+ for (const match of proxyMatches) {
645
+ const target = match[1];
646
+ if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
647
+ const portMatch = target.match(/:(\d+)/);
648
+ if (portMatch) {
649
+ addTarget({
650
+ path: '/api',
651
+ target,
652
+ port: parseInt(portMatch[1], 10),
653
+ });
654
+ }
655
+ }
656
+ }
657
+ }
658
+ catch {
659
+ // Ignore parse errors
660
+ }
661
+ }
662
+ // 11. Parcel - .proxyrc or .proxyrc.json
663
+ const parcelProxyConfigs = ['.proxyrc', '.proxyrc.json'];
664
+ for (const configFile of parcelProxyConfigs) {
665
+ const configPath = join(dir, configFile);
666
+ if (existsSync(configPath)) {
667
+ try {
668
+ const content = readFileSync(configPath, 'utf-8');
669
+ const config = JSON.parse(content);
670
+ // Parcel format: { "/api": { "target": "http://localhost:3000" } }
671
+ for (const [path, settings] of Object.entries(config)) {
672
+ const target = settings?.target;
673
+ if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
674
+ const portMatch = target.match(/:(\d+)/);
675
+ if (portMatch) {
676
+ addTarget({
677
+ path,
678
+ target,
679
+ port: parseInt(portMatch[1], 10),
680
+ });
681
+ }
682
+ }
683
+ }
684
+ }
685
+ catch {
686
+ // Ignore parse errors
687
+ }
688
+ }
689
+ }
690
+ // 12. Netlify - netlify.toml with redirects
691
+ const netlifyConfigPath = join(dir, 'netlify.toml');
692
+ if (existsSync(netlifyConfigPath)) {
693
+ try {
694
+ const content = readFileSync(netlifyConfigPath, 'utf-8');
695
+ // Look for redirect rules with localhost: [[redirects]] from = "/api/*" to = "http://localhost:3000/:splat"
696
+ const redirectMatches = content.matchAll(/\[\[redirects\]\][^[]*from\s*=\s*["']([^"']+)["'][^[]*to\s*=\s*["']([^"']+)["']/gs);
697
+ for (const match of redirectMatches) {
698
+ const path = match[1];
699
+ const target = match[2];
700
+ if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
701
+ const portMatch = target.match(/:(\d+)/);
702
+ if (portMatch) {
703
+ addTarget({
704
+ path,
705
+ target,
706
+ port: parseInt(portMatch[1], 10),
707
+ });
708
+ }
709
+ }
710
+ }
711
+ }
712
+ catch {
713
+ // Ignore parse errors
714
+ }
715
+ }
716
+ // 13. Remix - remix.config.js doesn't have proxy, but check for serverBuildTarget
717
+ // Remix uses loaders, so no proxy detection needed
718
+ // 14. Esbuild/custom - check for common proxy middleware in server files
719
+ const serverFiles = ['server.js', 'server.ts', 'dev-server.js', 'dev-server.ts'];
720
+ for (const serverFile of serverFiles) {
721
+ const serverPath = join(dir, serverFile);
722
+ if (existsSync(serverPath)) {
723
+ try {
724
+ const content = readFileSync(serverPath, 'utf-8');
725
+ // Look for http-proxy-middleware or manual proxy setups
726
+ const proxyMatches = content.matchAll(/(?:createProxyMiddleware|proxy)\s*\(\s*['"]([^'"]+)['"]\s*,\s*\{[^}]*target\s*:\s*['"]([^'"]+)['"]/g);
727
+ for (const match of proxyMatches) {
728
+ const path = match[1];
729
+ const target = match[2];
730
+ if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
731
+ const portMatch = target.match(/:(\d+)/);
732
+ if (portMatch) {
733
+ addTarget({
734
+ path,
735
+ target,
736
+ port: parseInt(portMatch[1], 10),
737
+ });
738
+ }
739
+ }
740
+ }
741
+ }
742
+ catch {
743
+ // Ignore parse errors
744
+ }
745
+ }
746
+ }
747
+ return targets;
748
+ }
749
+ /**
750
+ * Match proxy targets to detected services by port.
751
+ * Returns service names that match the proxy targets.
752
+ */
753
+ function matchProxyToServices(proxyTargets, services) {
754
+ const dependsOn = [];
755
+ for (const proxy of proxyTargets) {
756
+ // Find a service running on this port
757
+ const matchingService = services.find((s) => s.framework?.port === proxy.port);
758
+ if (matchingService && !dependsOn.includes(matchingService.name)) {
759
+ dependsOn.push(matchingService.name);
760
+ }
761
+ }
762
+ return dependsOn;
763
+ }
764
+ /**
765
+ * Main detection function - analyzes a project directory
766
+ */
767
+ export function detectProject(rootDir = process.cwd()) {
768
+ const isMonorepo = detectMonorepo(rootDir);
769
+ const packageManager = detectPackageManager(rootDir);
770
+ const projectName = getProjectName(rootDir);
771
+ let frameworks = [];
772
+ let services = [];
773
+ let proxyTargets = [];
774
+ if (isMonorepo) {
775
+ services = detectServices(rootDir);
776
+ frameworks = services
777
+ .filter((s) => s.framework)
778
+ .map((s) => s.framework);
779
+ // Detect proxy targets from root config
780
+ proxyTargets = detectProxyTargets(rootDir);
781
+ // Match proxy targets to services and set depends_on
782
+ if (proxyTargets.length > 0) {
783
+ const dependsOn = matchProxyToServices(proxyTargets, services);
784
+ // Find the frontend/main service and add depends_on
785
+ const frontendService = services.find((s) => s.root === './' || s.root === '.' || s.name === 'frontend');
786
+ if (frontendService && dependsOn.length > 0) {
787
+ frontendService.depends_on = dependsOn;
788
+ }
789
+ }
790
+ }
791
+ else {
792
+ const framework = detectFramework(rootDir);
793
+ if (framework) {
794
+ frameworks = [framework];
795
+ }
796
+ proxyTargets = detectProxyTargets(rootDir);
797
+ }
798
+ // Build suggestions based on detection
799
+ const primaryFramework = frameworks[0];
800
+ const runPrefix = packageManager === 'npm' ? 'npm run' : packageManager;
801
+ const suggestions = {
802
+ projectName,
803
+ devCommand: primaryFramework?.devCommand || `${runPrefix} dev`,
804
+ port: primaryFramework?.port || 3000,
805
+ readyPattern: primaryFramework?.readyPattern || 'ready|started|listening',
806
+ authBypassEnv: detectAuthBypass(rootDir),
807
+ };
808
+ return {
809
+ isMonorepo,
810
+ packageManager,
811
+ frameworks,
812
+ services,
813
+ proxyTargets,
814
+ suggestions,
815
+ };
816
+ }
817
+ /**
818
+ * Format detection results for display to user
819
+ */
820
+ export function formatDetectionSummary(detection) {
821
+ const lines = [];
822
+ lines.push(`Project: ${detection.suggestions.projectName}`);
823
+ lines.push(`Package Manager: ${detection.packageManager}`);
824
+ lines.push(`Monorepo: ${detection.isMonorepo ? 'Yes' : 'No'}`);
825
+ if (detection.frameworks.length > 0) {
826
+ lines.push(`\nDetected Frameworks:`);
827
+ for (const fw of detection.frameworks) {
828
+ lines.push(` - ${fw.name} (${fw.configFile})`);
829
+ }
830
+ }
831
+ if (detection.isMonorepo && detection.services.length > 0) {
832
+ lines.push(`\nDetected Services:`);
833
+ for (const svc of detection.services) {
834
+ const fw = svc.framework ? ` (${svc.framework.name})` : '';
835
+ const deps = svc.depends_on?.length ? ` → depends on: ${svc.depends_on.join(', ')}` : '';
836
+ lines.push(` - ${svc.name}: ${svc.root}${fw}${deps}`);
837
+ }
838
+ }
839
+ if (detection.proxyTargets.length > 0) {
840
+ lines.push(`\nDetected Proxy Targets:`);
841
+ for (const proxy of detection.proxyTargets) {
842
+ lines.push(` - ${proxy.path} → ${proxy.target}`);
843
+ }
844
+ }
845
+ lines.push(`\nSuggested Settings:`);
846
+ lines.push(` Dev Command: ${detection.suggestions.devCommand}`);
847
+ lines.push(` Port: ${detection.suggestions.port}`);
848
+ lines.push(` Ready Pattern: "${detection.suggestions.readyPattern}"`);
849
+ if (detection.suggestions.authBypassEnv) {
850
+ lines.push(` Auth Bypass: ${detection.suggestions.authBypassEnv}`);
851
+ }
852
+ return lines.join('\n');
853
+ }