@haystackeditor/cli 0.25.1 → 0.27.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 (109) hide show
  1. package/README.md +39 -463
  2. package/dist/capture/app-config.js +30 -6
  3. package/dist/commands/capture-brief.js +45 -35
  4. package/dist/commands/capture-contract.js +4 -4
  5. package/dist/commands/crawl-report.js +196 -0
  6. package/dist/commands/db-profile-upload.js +3 -3
  7. package/dist/commands/feedback.js +66 -0
  8. package/dist/commands/init-telemetry.js +55 -9
  9. package/dist/commands/init.js +8 -3
  10. package/dist/commands/lockfile-pin.js +307 -0
  11. package/dist/commands/telemetry-token.js +3 -3
  12. package/dist/commands/tokens.js +4 -4
  13. package/dist/commands/verify-explore.js +3 -3
  14. package/dist/commands/verify-history.js +2 -2
  15. package/dist/commands/verify-onboarding.js +13 -16
  16. package/dist/commands/verify-precompute.js +3 -4
  17. package/dist/commands/verify.js +31 -118
  18. package/dist/index.js +55 -968
  19. package/dist/schema.js +4 -10
  20. package/dist/utils/haystack-api.js +8 -36
  21. package/package.json +1 -5
  22. package/schemas/feedback.v1.json +13 -0
  23. package/schemas/pre-verify.v2.json +240 -0
  24. package/schemas/verify-raw.v1.json +1132 -0
  25. package/schemas/verify.v2.json +655 -0
  26. package/dist/assets/hooks/agent-context/detect.ts +0 -316
  27. package/dist/assets/hooks/agent-context/format.ts +0 -100
  28. package/dist/assets/hooks/agent-context/index.ts +0 -41
  29. package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
  30. package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
  31. package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
  32. package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
  33. package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
  34. package/dist/assets/hooks/agent-context/types.ts +0 -58
  35. package/dist/assets/hooks/llm-rules-template.md +0 -59
  36. package/dist/assets/hooks/package-lock.json +0 -598
  37. package/dist/assets/hooks/package.json +0 -12
  38. package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
  39. package/dist/assets/hooks/scripts/post-commit.sh +0 -5
  40. package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
  41. package/dist/assets/hooks/scripts/pre-push.sh +0 -25
  42. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
  43. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
  44. package/dist/assets/hooks/truncation-checker/index.ts +0 -595
  45. package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
  46. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
  47. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
  48. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
  49. package/dist/assets/skills/map-your-system.md +0 -143
  50. package/dist/assets/skills/submit.md +0 -200
  51. package/dist/commands/ask.js +0 -20
  52. package/dist/commands/cloud-verifier-behaviors.js +0 -218
  53. package/dist/commands/cloud-verifier-data-store-census.js +0 -539
  54. package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
  55. package/dist/commands/cloud-verifier-identity-census.js +0 -4060
  56. package/dist/commands/cloud-verifier-materialization.js +0 -704
  57. package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
  58. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
  59. package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
  60. package/dist/commands/cloud-verifier-universe.js +0 -10178
  61. package/dist/commands/config.js +0 -549
  62. package/dist/commands/design-verify.js +0 -311
  63. package/dist/commands/dismiss.js +0 -159
  64. package/dist/commands/hooks.js +0 -226
  65. package/dist/commands/inbox.js +0 -137
  66. package/dist/commands/mcp.js +0 -201
  67. package/dist/commands/policy.js +0 -371
  68. package/dist/commands/pr-status.js +0 -207
  69. package/dist/commands/pr.js +0 -105
  70. package/dist/commands/prepare-universe-review.js +0 -1092
  71. package/dist/commands/production-source-deny-policy.js +0 -100
  72. package/dist/commands/request-review.js +0 -74
  73. package/dist/commands/review.js +0 -191
  74. package/dist/commands/rules.js +0 -98
  75. package/dist/commands/scaffold-provisional-universe.js +0 -806
  76. package/dist/commands/setup.js +0 -1170
  77. package/dist/commands/skills.js +0 -447
  78. package/dist/commands/status.js +0 -35
  79. package/dist/commands/submit.js +0 -745
  80. package/dist/commands/system-map.js +0 -228
  81. package/dist/commands/triage.js +0 -598
  82. package/dist/commands/webhooks.js +0 -241
  83. package/dist/states.js +0 -46
  84. package/dist/tools/detect.js +0 -832
  85. package/dist/triage/astra.js +0 -202
  86. package/dist/triage/prompts.js +0 -188
  87. package/dist/triage/runner.js +0 -200
  88. package/dist/triage/types.js +0 -7
  89. package/dist/types.js +0 -326
  90. package/dist/utils/action-output.js +0 -26
  91. package/dist/utils/analysis-api.js +0 -416
  92. package/dist/utils/config.js +0 -54
  93. package/dist/utils/design-verifier-api.js +0 -294
  94. package/dist/utils/design-verifier-history.js +0 -79
  95. package/dist/utils/design-verifier-result.js +0 -424
  96. package/dist/utils/github-api.js +0 -324
  97. package/dist/utils/pending-state.js +0 -86
  98. package/dist/utils/pr-ref.js +0 -56
  99. package/dist/utils/prompter.js +0 -328
  100. package/schemas/action.v1.json +0 -22
  101. package/schemas/ask.v1.json +0 -40
  102. package/schemas/inbox.v1.json +0 -27
  103. package/schemas/pr-status.v1.json +0 -61
  104. package/schemas/pr.v1.json +0 -97
  105. package/schemas/pr.v3.json +0 -45
  106. package/schemas/setup.v1.json +0 -75
  107. package/schemas/submit.v1.json +0 -90
  108. package/schemas/triage.v1.json +0 -103
  109. package/schemas/triage.v2.json +0 -64
@@ -1,832 +0,0 @@
1
- import { existsSync, readFileSync, statSync } from 'fs';
2
- import { join } from 'path';
3
- import fg from 'fast-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
- // onlyFiles: false matches directories as well as files, as glob did.
221
- const matches = fg.sync(cleanPattern, { cwd: rootDir, onlyFiles: false });
222
- // Filter to only directories
223
- for (const match of matches) {
224
- const fullPath = join(rootDir, match);
225
- try {
226
- if (statSync(fullPath).isDirectory()) {
227
- dirs.push(match);
228
- }
229
- }
230
- catch {
231
- // Skip if stat fails
232
- }
233
- }
234
- }
235
- catch {
236
- // If glob fails, try as literal path
237
- if (existsSync(join(rootDir, cleanPattern))) {
238
- dirs.push(cleanPattern);
239
- }
240
- }
241
- }
242
- return [...new Set(dirs)];
243
- }
244
- /**
245
- * Detect services in a monorepo
246
- */
247
- function detectServices(rootDir) {
248
- const services = [];
249
- // Find workspace packages
250
- const workspacePatterns = findPnpmWorkspaces(rootDir);
251
- const packageDirs = expandWorkspaceGlobs(rootDir, workspacePatterns);
252
- // Also check common directories
253
- const commonDirs = [
254
- 'packages',
255
- 'apps',
256
- 'services',
257
- 'infra',
258
- 'workers',
259
- 'functions',
260
- 'api',
261
- 'web',
262
- 'frontend',
263
- 'backend',
264
- ];
265
- for (const dir of commonDirs) {
266
- if (existsSync(join(rootDir, dir)) && !packageDirs.includes(dir)) {
267
- packageDirs.push(dir);
268
- }
269
- }
270
- // Detect framework in each directory
271
- for (const dir of packageDirs) {
272
- const fullPath = join(rootDir, dir);
273
- if (!existsSync(fullPath))
274
- continue;
275
- // Check if this directory has its own package.json (is a package)
276
- if (!hasFile(fullPath, 'package.json'))
277
- continue;
278
- const framework = detectFramework(fullPath);
279
- const name = dir.split('/').pop() || dir;
280
- services.push({
281
- name,
282
- root: dir,
283
- type: framework?.name === 'cloudflare-worker' ? 'server' : 'server',
284
- framework: framework || undefined,
285
- });
286
- }
287
- // Also check the root directory
288
- const rootFramework = detectFramework(rootDir);
289
- if (rootFramework) {
290
- services.unshift({
291
- name: 'frontend',
292
- root: './',
293
- type: 'server',
294
- framework: rootFramework,
295
- });
296
- }
297
- return services;
298
- }
299
- /**
300
- * Get project name from package.json
301
- */
302
- function getProjectName(rootDir) {
303
- const pkgPath = join(rootDir, 'package.json');
304
- if (existsSync(pkgPath)) {
305
- try {
306
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
307
- return pkg.name || 'my-app';
308
- }
309
- catch {
310
- // Ignore
311
- }
312
- }
313
- return 'my-app';
314
- }
315
- /**
316
- * Detect common auth bypass patterns
317
- */
318
- function detectAuthBypass(rootDir) {
319
- // Check for common env var patterns in code
320
- const envFiles = ['.env.example', '.env.sample', '.env.template'];
321
- for (const envFile of envFiles) {
322
- const path = join(rootDir, envFile);
323
- if (existsSync(path)) {
324
- try {
325
- const content = readFileSync(path, 'utf-8');
326
- // Look for auth-related env vars
327
- const authPatterns = [
328
- /^(SKIP_AUTH|BYPASS_AUTH|DISABLE_AUTH|VITE_SKIP_AUTH|NEXT_PUBLIC_SKIP_AUTH)=/m,
329
- /^(AUTH_DISABLED|NO_AUTH|DEV_AUTH)=/m,
330
- ];
331
- for (const pattern of authPatterns) {
332
- const match = content.match(pattern);
333
- if (match) {
334
- return `${match[1]}=true`;
335
- }
336
- }
337
- }
338
- catch {
339
- // Ignore
340
- }
341
- }
342
- }
343
- return undefined;
344
- }
345
- /**
346
- * Detect proxy targets from various config files.
347
- * Supports: Vite, Next.js, Create React App, Vercel, Webpack
348
- * This helps identify backend services the frontend depends on.
349
- */
350
- function detectProxyTargets(dir) {
351
- const targets = [];
352
- const seenPorts = new Set();
353
- const addTarget = (target) => {
354
- if (!seenPorts.has(target.port)) {
355
- seenPorts.add(target.port);
356
- targets.push(target);
357
- }
358
- };
359
- // 1. Vite - vite.config.ts/js with server.proxy
360
- const viteConfigs = ['vite.config.ts', 'vite.config.js', 'vite.config.mjs'];
361
- for (const configFile of viteConfigs) {
362
- const configPath = join(dir, configFile);
363
- if (existsSync(configPath)) {
364
- try {
365
- const content = readFileSync(configPath, 'utf-8');
366
- // Look for proxy patterns like: '/api': { target: 'http://localhost:8787' }
367
- const proxyMatches = content.matchAll(/['"]([^'"]+)['"]\s*:\s*(?:\{[^}]*target\s*:\s*['"]([^'"]+)['"]|['"]([^'"]+)['"])/g);
368
- for (const match of proxyMatches) {
369
- const path = match[1];
370
- const target = match[2] || match[3];
371
- if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
372
- const portMatch = target.match(/:(\d+)/);
373
- if (portMatch) {
374
- addTarget({
375
- path,
376
- target,
377
- port: parseInt(portMatch[1], 10),
378
- });
379
- }
380
- }
381
- }
382
- }
383
- catch {
384
- // Ignore parse errors
385
- }
386
- }
387
- }
388
- // 2. Next.js - next.config.js/mjs/ts with rewrites()
389
- const nextConfigs = ['next.config.js', 'next.config.mjs', 'next.config.ts'];
390
- for (const configFile of nextConfigs) {
391
- const configPath = join(dir, configFile);
392
- if (existsSync(configPath)) {
393
- try {
394
- const content = readFileSync(configPath, 'utf-8');
395
- // Look for rewrite patterns: { source: '/api/:path*', destination: 'http://localhost:3001/:path*' }
396
- const rewriteMatches = content.matchAll(/destination\s*:\s*['"`]([^'"`]+)['"`]/g);
397
- for (const match of rewriteMatches) {
398
- const destination = match[1];
399
- if (destination.includes('localhost') || destination.includes('127.0.0.1')) {
400
- const portMatch = destination.match(/:(\d+)/);
401
- if (portMatch) {
402
- // Try to find the associated source
403
- const sourceMatch = content.match(new RegExp(`source\\s*:\\s*['"\`]([^'"\`]+)['"\`][^}]*destination\\s*:\\s*['"\`]${destination.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
404
- addTarget({
405
- path: sourceMatch?.[1] || '/api',
406
- target: destination.replace(/\/:[^/]+\*?/g, ''), // Remove path params
407
- port: parseInt(portMatch[1], 10),
408
- });
409
- }
410
- }
411
- }
412
- }
413
- catch {
414
- // Ignore parse errors
415
- }
416
- }
417
- }
418
- // 3. Create React App - package.json with "proxy" field
419
- const pkgPath = join(dir, 'package.json');
420
- if (existsSync(pkgPath)) {
421
- try {
422
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
423
- if (typeof pkg.proxy === 'string') {
424
- const portMatch = pkg.proxy.match(/:(\d+)/);
425
- if (portMatch) {
426
- addTarget({
427
- path: '/api',
428
- target: pkg.proxy,
429
- port: parseInt(portMatch[1], 10),
430
- });
431
- }
432
- }
433
- }
434
- catch {
435
- // Ignore parse errors
436
- }
437
- }
438
- // 4. Create React App - setupProxy.js (http-proxy-middleware)
439
- const setupProxyPath = join(dir, 'src', 'setupProxy.js');
440
- if (existsSync(setupProxyPath)) {
441
- try {
442
- const content = readFileSync(setupProxyPath, 'utf-8');
443
- // Look for: createProxyMiddleware('/api', { target: 'http://localhost:3001' })
444
- const proxyMatches = content.matchAll(/createProxyMiddleware\s*\(\s*['"]([^'"]+)['"]\s*,\s*\{[^}]*target\s*:\s*['"]([^'"]+)['"]/g);
445
- for (const match of proxyMatches) {
446
- const path = match[1];
447
- const target = match[2];
448
- const portMatch = target.match(/:(\d+)/);
449
- if (portMatch) {
450
- addTarget({
451
- path,
452
- target,
453
- port: parseInt(portMatch[1], 10),
454
- });
455
- }
456
- }
457
- }
458
- catch {
459
- // Ignore parse errors
460
- }
461
- }
462
- // 5. Vercel - vercel.json with rewrites
463
- const vercelPath = join(dir, 'vercel.json');
464
- if (existsSync(vercelPath)) {
465
- try {
466
- const vercelConfig = JSON.parse(readFileSync(vercelPath, 'utf-8'));
467
- if (Array.isArray(vercelConfig.rewrites)) {
468
- for (const rewrite of vercelConfig.rewrites) {
469
- if (rewrite.destination && (rewrite.destination.includes('localhost') || rewrite.destination.includes('127.0.0.1'))) {
470
- const portMatch = rewrite.destination.match(/:(\d+)/);
471
- if (portMatch) {
472
- addTarget({
473
- path: rewrite.source || '/api',
474
- target: rewrite.destination,
475
- port: parseInt(portMatch[1], 10),
476
- });
477
- }
478
- }
479
- }
480
- }
481
- }
482
- catch {
483
- // Ignore parse errors
484
- }
485
- }
486
- // 6. Webpack - webpack.config.js with devServer.proxy
487
- const webpackConfigs = ['webpack.config.js', 'webpack.config.ts'];
488
- for (const configFile of webpackConfigs) {
489
- const configPath = join(dir, configFile);
490
- if (existsSync(configPath)) {
491
- try {
492
- const content = readFileSync(configPath, 'utf-8');
493
- // Look for devServer.proxy patterns
494
- const proxyMatches = content.matchAll(/['"]([^'"]+)['"]\s*:\s*(?:\{[^}]*target\s*:\s*['"]([^'"]+)['"]|['"]([^'"]+)['"])/g);
495
- for (const match of proxyMatches) {
496
- const path = match[1];
497
- const target = match[2] || match[3];
498
- if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
499
- const portMatch = target.match(/:(\d+)/);
500
- if (portMatch) {
501
- addTarget({
502
- path,
503
- target,
504
- port: parseInt(portMatch[1], 10),
505
- });
506
- }
507
- }
508
- }
509
- }
510
- catch {
511
- // Ignore parse errors
512
- }
513
- }
514
- }
515
- // 7. Vue CLI - vue.config.js with devServer.proxy (same pattern as webpack)
516
- const vueConfigPath = join(dir, 'vue.config.js');
517
- if (existsSync(vueConfigPath)) {
518
- try {
519
- const content = readFileSync(vueConfigPath, 'utf-8');
520
- const proxyMatches = content.matchAll(/['"]([^'"]+)['"]\s*:\s*(?:\{[^}]*target\s*:\s*['"]([^'"]+)['"]|['"]([^'"]+)['"])/g);
521
- for (const match of proxyMatches) {
522
- const path = match[1];
523
- const target = match[2] || match[3];
524
- if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
525
- const portMatch = target.match(/:(\d+)/);
526
- if (portMatch) {
527
- addTarget({
528
- path,
529
- target,
530
- port: parseInt(portMatch[1], 10),
531
- });
532
- }
533
- }
534
- }
535
- }
536
- catch {
537
- // Ignore parse errors
538
- }
539
- }
540
- // 8. Angular - proxy.conf.json or proxy.conf.js
541
- const angularProxyConfigs = ['proxy.conf.json', 'proxy.conf.js'];
542
- for (const configFile of angularProxyConfigs) {
543
- const configPath = join(dir, configFile);
544
- if (existsSync(configPath)) {
545
- try {
546
- const content = readFileSync(configPath, 'utf-8');
547
- if (configFile.endsWith('.json')) {
548
- // JSON format: { "/api": { "target": "http://localhost:3000" } }
549
- const config = JSON.parse(content);
550
- for (const [path, settings] of Object.entries(config)) {
551
- const target = settings?.target;
552
- if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
553
- const portMatch = target.match(/:(\d+)/);
554
- if (portMatch) {
555
- addTarget({
556
- path,
557
- target,
558
- port: parseInt(portMatch[1], 10),
559
- });
560
- }
561
- }
562
- }
563
- }
564
- else {
565
- // JS format - use regex
566
- const proxyMatches = content.matchAll(/['"]([^'"]+)['"]\s*:\s*\{[^}]*target\s*:\s*['"]([^'"]+)['"]/g);
567
- for (const match of proxyMatches) {
568
- const path = match[1];
569
- const target = match[2];
570
- if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
571
- const portMatch = target.match(/:(\d+)/);
572
- if (portMatch) {
573
- addTarget({
574
- path,
575
- target,
576
- port: parseInt(portMatch[1], 10),
577
- });
578
- }
579
- }
580
- }
581
- }
582
- }
583
- catch {
584
- // Ignore parse errors
585
- }
586
- }
587
- }
588
- // 9. Nuxt.js - nuxt.config.ts/js with routeRules or proxy
589
- const nuxtConfigs = ['nuxt.config.ts', 'nuxt.config.js'];
590
- for (const configFile of nuxtConfigs) {
591
- const configPath = join(dir, configFile);
592
- if (existsSync(configPath)) {
593
- try {
594
- const content = readFileSync(configPath, 'utf-8');
595
- // Look for proxy or routeRules with proxy settings
596
- const proxyMatches = content.matchAll(/(?:proxy|target)\s*:\s*['"]([^'"]+)['"]/g);
597
- for (const match of proxyMatches) {
598
- const target = match[1];
599
- if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
600
- const portMatch = target.match(/:(\d+)/);
601
- if (portMatch) {
602
- addTarget({
603
- path: '/api',
604
- target,
605
- port: parseInt(portMatch[1], 10),
606
- });
607
- }
608
- }
609
- }
610
- }
611
- catch {
612
- // Ignore parse errors
613
- }
614
- }
615
- }
616
- // 10. Gatsby - gatsby-config.js with proxy
617
- const gatsbyConfigPath = join(dir, 'gatsby-config.js');
618
- if (existsSync(gatsbyConfigPath)) {
619
- try {
620
- const content = readFileSync(gatsbyConfigPath, 'utf-8');
621
- // Look for developMiddleware proxy patterns
622
- const proxyMatches = content.matchAll(/proxy\s*:\s*\{[^}]*url\s*:\s*['"]([^'"]+)['"]/g);
623
- for (const match of proxyMatches) {
624
- const target = match[1];
625
- if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
626
- const portMatch = target.match(/:(\d+)/);
627
- if (portMatch) {
628
- addTarget({
629
- path: '/api',
630
- target,
631
- port: parseInt(portMatch[1], 10),
632
- });
633
- }
634
- }
635
- }
636
- }
637
- catch {
638
- // Ignore parse errors
639
- }
640
- }
641
- // 11. Parcel - .proxyrc or .proxyrc.json
642
- const parcelProxyConfigs = ['.proxyrc', '.proxyrc.json'];
643
- for (const configFile of parcelProxyConfigs) {
644
- const configPath = join(dir, configFile);
645
- if (existsSync(configPath)) {
646
- try {
647
- const content = readFileSync(configPath, 'utf-8');
648
- const config = JSON.parse(content);
649
- // Parcel format: { "/api": { "target": "http://localhost:3000" } }
650
- for (const [path, settings] of Object.entries(config)) {
651
- const target = settings?.target;
652
- if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
653
- const portMatch = target.match(/:(\d+)/);
654
- if (portMatch) {
655
- addTarget({
656
- path,
657
- target,
658
- port: parseInt(portMatch[1], 10),
659
- });
660
- }
661
- }
662
- }
663
- }
664
- catch {
665
- // Ignore parse errors
666
- }
667
- }
668
- }
669
- // 12. Netlify - netlify.toml with redirects
670
- const netlifyConfigPath = join(dir, 'netlify.toml');
671
- if (existsSync(netlifyConfigPath)) {
672
- try {
673
- const content = readFileSync(netlifyConfigPath, 'utf-8');
674
- // Look for redirect rules with localhost: [[redirects]] from = "/api/*" to = "http://localhost:3000/:splat"
675
- const redirectMatches = content.matchAll(/\[\[redirects\]\][^[]*from\s*=\s*["']([^"']+)["'][^[]*to\s*=\s*["']([^"']+)["']/gs);
676
- for (const match of redirectMatches) {
677
- const path = match[1];
678
- const target = match[2];
679
- if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
680
- const portMatch = target.match(/:(\d+)/);
681
- if (portMatch) {
682
- addTarget({
683
- path,
684
- target,
685
- port: parseInt(portMatch[1], 10),
686
- });
687
- }
688
- }
689
- }
690
- }
691
- catch {
692
- // Ignore parse errors
693
- }
694
- }
695
- // 13. Remix - remix.config.js doesn't have proxy, but check for serverBuildTarget
696
- // Remix uses loaders, so no proxy detection needed
697
- // 14. Esbuild/custom - check for common proxy middleware in server files
698
- const serverFiles = ['server.js', 'server.ts', 'dev-server.js', 'dev-server.ts'];
699
- for (const serverFile of serverFiles) {
700
- const serverPath = join(dir, serverFile);
701
- if (existsSync(serverPath)) {
702
- try {
703
- const content = readFileSync(serverPath, 'utf-8');
704
- // Look for http-proxy-middleware or manual proxy setups
705
- const proxyMatches = content.matchAll(/(?:createProxyMiddleware|proxy)\s*\(\s*['"]([^'"]+)['"]\s*,\s*\{[^}]*target\s*:\s*['"]([^'"]+)['"]/g);
706
- for (const match of proxyMatches) {
707
- const path = match[1];
708
- const target = match[2];
709
- if (target && (target.includes('localhost') || target.includes('127.0.0.1'))) {
710
- const portMatch = target.match(/:(\d+)/);
711
- if (portMatch) {
712
- addTarget({
713
- path,
714
- target,
715
- port: parseInt(portMatch[1], 10),
716
- });
717
- }
718
- }
719
- }
720
- }
721
- catch {
722
- // Ignore parse errors
723
- }
724
- }
725
- }
726
- return targets;
727
- }
728
- /**
729
- * Match proxy targets to detected services by port.
730
- * Returns service names that match the proxy targets.
731
- */
732
- function matchProxyToServices(proxyTargets, services) {
733
- const dependsOn = [];
734
- for (const proxy of proxyTargets) {
735
- // Find a service running on this port
736
- const matchingService = services.find((s) => s.framework?.port === proxy.port);
737
- if (matchingService && !dependsOn.includes(matchingService.name)) {
738
- dependsOn.push(matchingService.name);
739
- }
740
- }
741
- return dependsOn;
742
- }
743
- /**
744
- * Main detection function - analyzes a project directory
745
- */
746
- export function detectProject(rootDir = process.cwd()) {
747
- const isMonorepo = detectMonorepo(rootDir);
748
- const packageManager = detectPackageManager(rootDir);
749
- const projectName = getProjectName(rootDir);
750
- let frameworks = [];
751
- let services = [];
752
- let proxyTargets = [];
753
- if (isMonorepo) {
754
- services = detectServices(rootDir);
755
- frameworks = services
756
- .filter((s) => s.framework)
757
- .map((s) => s.framework);
758
- // Detect proxy targets from root config
759
- proxyTargets = detectProxyTargets(rootDir);
760
- // Match proxy targets to services and set depends_on
761
- if (proxyTargets.length > 0) {
762
- const dependsOn = matchProxyToServices(proxyTargets, services);
763
- // Find the frontend/main service and add depends_on
764
- const frontendService = services.find((s) => s.root === './' || s.root === '.' || s.name === 'frontend');
765
- if (frontendService && dependsOn.length > 0) {
766
- frontendService.depends_on = dependsOn;
767
- }
768
- }
769
- }
770
- else {
771
- const framework = detectFramework(rootDir);
772
- if (framework) {
773
- frameworks = [framework];
774
- }
775
- proxyTargets = detectProxyTargets(rootDir);
776
- }
777
- // Build suggestions based on detection
778
- const primaryFramework = frameworks[0];
779
- const runPrefix = packageManager === 'npm' ? 'npm run' : packageManager;
780
- const suggestions = {
781
- projectName,
782
- devCommand: primaryFramework?.devCommand || `${runPrefix} dev`,
783
- port: primaryFramework?.port || 3000,
784
- readyPattern: primaryFramework?.readyPattern || 'ready|started|listening',
785
- authBypassEnv: detectAuthBypass(rootDir),
786
- };
787
- return {
788
- isMonorepo,
789
- packageManager,
790
- frameworks,
791
- services,
792
- proxyTargets,
793
- suggestions,
794
- };
795
- }
796
- /**
797
- * Format detection results for display to user
798
- */
799
- export function formatDetectionSummary(detection) {
800
- const lines = [];
801
- lines.push(`Project: ${detection.suggestions.projectName}`);
802
- lines.push(`Package Manager: ${detection.packageManager}`);
803
- lines.push(`Monorepo: ${detection.isMonorepo ? 'Yes' : 'No'}`);
804
- if (detection.frameworks.length > 0) {
805
- lines.push(`\nDetected Frameworks:`);
806
- for (const fw of detection.frameworks) {
807
- lines.push(` - ${fw.name} (${fw.configFile})`);
808
- }
809
- }
810
- if (detection.isMonorepo && detection.services.length > 0) {
811
- lines.push(`\nDetected Services:`);
812
- for (const svc of detection.services) {
813
- const fw = svc.framework ? ` (${svc.framework.name})` : '';
814
- const deps = svc.depends_on?.length ? ` → depends on: ${svc.depends_on.join(', ')}` : '';
815
- lines.push(` - ${svc.name}: ${svc.root}${fw}${deps}`);
816
- }
817
- }
818
- if (detection.proxyTargets.length > 0) {
819
- lines.push(`\nDetected Proxy Targets:`);
820
- for (const proxy of detection.proxyTargets) {
821
- lines.push(` - ${proxy.path} → ${proxy.target}`);
822
- }
823
- }
824
- lines.push(`\nSuggested Settings:`);
825
- lines.push(` Dev Command: ${detection.suggestions.devCommand}`);
826
- lines.push(` Port: ${detection.suggestions.port}`);
827
- lines.push(` Ready Pattern: "${detection.suggestions.readyPattern}"`);
828
- if (detection.suggestions.authBypassEnv) {
829
- lines.push(` Auth Bypass: ${detection.suggestions.authBypassEnv}`);
830
- }
831
- return lines.join('\n');
832
- }