@haystackeditor/cli 0.5.0 → 0.7.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.
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Project detection utilities
3
3
  *
4
- * Auto-detects framework, package manager, monorepo structure, and services.
4
+ * Auto-detects framework, package manager, monorepo structure, services,
5
+ * and inter-service dependencies from proxy configurations.
5
6
  */
6
7
  import * as fs from 'fs/promises';
7
8
  import * as path from 'path';
@@ -19,10 +20,19 @@ export async function detectProject(rootDir = process.cwd()) {
19
20
  result.isMonorepo = true;
20
21
  result.monorepoTool = monorepo.tool;
21
22
  result.services = await detectServices(rootDir, monorepo.workspaces);
23
+ // Detect proxy dependencies and add them to services
24
+ if (result.services && result.services.length > 0) {
25
+ result.services = await suggestServiceDependencies(rootDir, result.services);
26
+ }
22
27
  }
23
28
  // Detect framework
24
29
  result.framework = await detectFramework(rootDir);
25
- // Set suggestions based on framework
30
+ // Extract custom port from config if framework supports it
31
+ const customPort = await extractPortFromConfig(rootDir, result.framework);
32
+ if (customPort) {
33
+ result.suggestedPort = customPort;
34
+ }
35
+ // Set suggestions based on framework (won't override customPort if set)
26
36
  setSuggestions(result);
27
37
  // Detect auth bypass from .env.example
28
38
  result.suggestedAuthBypass = await detectAuthBypass(rootDir);
@@ -183,12 +193,14 @@ async function detectService(rootDir, servicePath) {
183
193
  else if (await hasFile(fullPath, 'vite.config.ts') || await hasFile(fullPath, 'vite.config.js')) {
184
194
  framework = 'vite';
185
195
  suggestedCommand = 'pnpm dev';
186
- suggestedPort = 5173;
196
+ // Try to extract custom port from vite config, fallback to default
197
+ suggestedPort = await extractPortFromConfig(fullPath, 'vite') ?? 5173;
187
198
  }
188
199
  else if (await hasFile(fullPath, 'next.config.js') || await hasFile(fullPath, 'next.config.ts')) {
189
200
  framework = 'nextjs';
190
201
  suggestedCommand = 'pnpm dev';
191
- suggestedPort = 3000;
202
+ // Try to extract custom port from next config, fallback to default
203
+ suggestedPort = await extractPortFromConfig(fullPath, 'nextjs') ?? 3000;
192
204
  }
193
205
  else if (scripts.dev) {
194
206
  suggestedCommand = 'pnpm dev';
@@ -275,6 +287,7 @@ async function detectAuthBypass(rootDir) {
275
287
  }
276
288
  /**
277
289
  * Set suggestions based on detected framework
290
+ * Note: This is sync, so custom port extraction happens in detectProject
278
291
  */
279
292
  function setSuggestions(result) {
280
293
  const frameworkDefaults = {
@@ -289,7 +302,8 @@ function setSuggestions(result) {
289
302
  };
290
303
  const defaults = result.framework ? frameworkDefaults[result.framework] : null;
291
304
  result.suggestedDevCommand = defaults?.command || `${result.packageManager} dev`;
292
- result.suggestedPort = defaults?.port || 3000;
305
+ // suggestedPort may already be set from custom config extraction
306
+ result.suggestedPort = result.suggestedPort || defaults?.port || 3000;
293
307
  result.suggestedReadyPattern = defaults?.ready || 'ready|started|listening|Local:';
294
308
  }
295
309
  /**
@@ -304,3 +318,188 @@ async function hasFile(dir, file) {
304
318
  return false;
305
319
  }
306
320
  }
321
+ /**
322
+ * Extract custom port from framework config files
323
+ *
324
+ * Parses vite.config.ts, next.config.js, etc. to find custom port settings
325
+ * Returns undefined if no custom port is configured (use framework default)
326
+ */
327
+ async function extractPortFromConfig(rootDir, framework) {
328
+ if (!framework)
329
+ return undefined;
330
+ // Config files to check based on framework
331
+ const configFiles = {
332
+ vite: ['vite.config.ts', 'vite.config.js', 'vite.config.mjs'],
333
+ nextjs: ['next.config.js', 'next.config.ts', 'next.config.mjs'],
334
+ nuxt: ['nuxt.config.ts', 'nuxt.config.js'],
335
+ sveltekit: ['svelte.config.js', 'svelte.config.ts'],
336
+ astro: ['astro.config.mjs', 'astro.config.ts'],
337
+ };
338
+ const files = configFiles[framework];
339
+ if (!files)
340
+ return undefined;
341
+ let configContent = null;
342
+ for (const file of files) {
343
+ try {
344
+ configContent = await fs.readFile(path.join(rootDir, file), 'utf-8');
345
+ break;
346
+ }
347
+ catch {
348
+ // File not found, try next
349
+ }
350
+ }
351
+ if (!configContent)
352
+ return undefined;
353
+ // Patterns to match port configuration
354
+ // Vite: server: { port: 3000 } or server.port = 3000
355
+ // Next: env: { PORT: 3000 } or experimental.serverPort
356
+ const patterns = [
357
+ /server\s*:\s*\{[^}]*port\s*:\s*(\d+)/s, // server: { port: 3000 }
358
+ /server\s*\.\s*port\s*[=:]\s*(\d+)/, // server.port = 3000
359
+ /port\s*:\s*(\d+)/, // port: 3000 (generic)
360
+ /PORT\s*[=:]\s*['"]?(\d+)['"]?/, // PORT = 3000
361
+ ];
362
+ for (const pattern of patterns) {
363
+ const match = configContent.match(pattern);
364
+ if (match) {
365
+ const port = parseInt(match[1], 10);
366
+ if (port > 0 && port < 65536) {
367
+ return port;
368
+ }
369
+ }
370
+ }
371
+ return undefined;
372
+ }
373
+ /**
374
+ * Detect proxy dependencies from vite.config.ts/js
375
+ *
376
+ * Parses the vite config file to find proxy targets that point to localhost,
377
+ * which indicate dependencies on other local services.
378
+ *
379
+ * @example
380
+ * // vite.config.ts with proxy
381
+ * server: {
382
+ * proxy: {
383
+ * '/api/review-chat': {
384
+ * target: 'http://localhost:8787',
385
+ * }
386
+ * }
387
+ * }
388
+ * // Returns: [{ path: '/api/review-chat', port: 8787, host: 'localhost' }]
389
+ */
390
+ export async function detectProxyDependencies(rootDir) {
391
+ const dependencies = [];
392
+ // Try vite.config.ts, then vite.config.js
393
+ const configFiles = ['vite.config.ts', 'vite.config.js', 'vite.config.mjs'];
394
+ let configContent = null;
395
+ for (const file of configFiles) {
396
+ try {
397
+ configContent = await fs.readFile(path.join(rootDir, file), 'utf-8');
398
+ break;
399
+ }
400
+ catch {
401
+ // File not found, try next
402
+ }
403
+ }
404
+ if (!configContent) {
405
+ return dependencies;
406
+ }
407
+ // Parse proxy configurations using regex
408
+ // Matches patterns like:
409
+ // '/api/path': { target: 'http://localhost:8787' }
410
+ // '/api/path': 'http://localhost:8787'
411
+ // proxy: { '/api/path': { target: 'http://localhost:8787' } }
412
+ // Pattern for object-style proxy: '/path': { target: 'http://localhost:PORT' }
413
+ const objectProxyRegex = /['"]([^'"]+)['"]\s*:\s*\{[^}]*target\s*:\s*['"]https?:\/\/(localhost|127\.0\.0\.1):(\d+)['"]/g;
414
+ let match;
415
+ while ((match = objectProxyRegex.exec(configContent)) !== null) {
416
+ dependencies.push({
417
+ path: match[1],
418
+ host: match[2],
419
+ port: parseInt(match[3], 10),
420
+ });
421
+ }
422
+ // Pattern for string-style proxy: '/path': 'http://localhost:PORT'
423
+ const stringProxyRegex = /['"]([^'"]+)['"]\s*:\s*['"]https?:\/\/(localhost|127\.0\.0\.1):(\d+)['"]/g;
424
+ let stringMatch;
425
+ while ((stringMatch = stringProxyRegex.exec(configContent)) !== null) {
426
+ // Avoid duplicates from object pattern
427
+ const exists = dependencies.some(d => d.path === stringMatch[1] && d.port === parseInt(stringMatch[3], 10));
428
+ if (!exists) {
429
+ dependencies.push({
430
+ path: stringMatch[1],
431
+ host: stringMatch[2],
432
+ port: parseInt(stringMatch[3], 10),
433
+ });
434
+ }
435
+ }
436
+ return dependencies;
437
+ }
438
+ /**
439
+ * Match proxy dependencies to detected services
440
+ *
441
+ * Given a list of proxy targets (ports) and detected services,
442
+ * determines which services are dependencies based on port matching.
443
+ */
444
+ export function matchProxyToServices(proxyDeps, services) {
445
+ // Create a port → service name mapping
446
+ const portToService = new Map();
447
+ for (const service of services) {
448
+ if (service.suggestedPort) {
449
+ portToService.set(service.suggestedPort, service.name);
450
+ }
451
+ }
452
+ // For each service that has proxy deps pointing to another service's port,
453
+ // record the dependency
454
+ const serviceDeps = new Map();
455
+ for (const dep of proxyDeps) {
456
+ const matchedService = portToService.get(dep.port);
457
+ if (matchedService) {
458
+ dep.matchedService = matchedService;
459
+ }
460
+ }
461
+ return serviceDeps;
462
+ }
463
+ /**
464
+ * Suggest depends_on for services based on proxy configuration
465
+ *
466
+ * For the main service (usually frontend), detect if it proxies to other local services
467
+ * and suggest those as dependencies.
468
+ */
469
+ export async function suggestServiceDependencies(rootDir, services) {
470
+ // Detect proxy dependencies from vite config
471
+ const proxyDeps = await detectProxyDependencies(rootDir);
472
+ if (proxyDeps.length === 0) {
473
+ return services;
474
+ }
475
+ // Create port → service mapping
476
+ const portToService = new Map();
477
+ for (const service of services) {
478
+ if (service.suggestedPort) {
479
+ portToService.set(service.suggestedPort, service.name);
480
+ }
481
+ }
482
+ // Match proxy ports to services
483
+ const dependsOn = [];
484
+ for (const dep of proxyDeps) {
485
+ const serviceName = portToService.get(dep.port);
486
+ if (serviceName) {
487
+ dep.matchedService = serviceName;
488
+ if (!dependsOn.includes(serviceName)) {
489
+ dependsOn.push(serviceName);
490
+ }
491
+ }
492
+ }
493
+ // Update the main frontend service with depends_on
494
+ // The main service is typically the one at root with port 3000/5173
495
+ return services.map(service => {
496
+ // If this service is at root and has detected proxy dependencies
497
+ if ((service.root === '.' || service.root === './') && dependsOn.length > 0) {
498
+ return {
499
+ ...service,
500
+ suggestedDependsOn: dependsOn,
501
+ };
502
+ }
503
+ return service;
504
+ });
505
+ }
@@ -30,7 +30,7 @@ export declare function scanForSecrets(content: string, filename: string): Secre
30
30
  */
31
31
  export declare function scanFile(filePath: string): Promise<SecretFinding[]>;
32
32
  /**
33
- * Scan .haystack.yml specifically for secrets
33
+ * Scan .haystack.json specifically for secrets
34
34
  */
35
35
  export declare function scanHaystackConfig(configPath: string): Promise<SecretFinding[]>;
36
36
  /**
@@ -202,7 +202,7 @@ export async function scanFile(filePath) {
202
202
  }
203
203
  }
204
204
  /**
205
- * Scan .haystack.yml specifically for secrets
205
+ * Scan .haystack.json specifically for secrets
206
206
  */
207
207
  export async function scanHaystackConfig(configPath) {
208
208
  return scanFile(configPath);
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export declare function createSkillFile(): Promise<string>;
6
6
  /**
7
- * Create the .claude/commands/haystack.md file for Claude Code slash command
8
- * Users can invoke with /haystack to start the setup wizard
7
+ * Create the .claude/commands/ files for Claude Code slash commands
8
+ * Users can invoke with /setup-haystack or /prepare-haystack
9
9
  */
10
10
  export declare function createClaudeCommand(): Promise<string>;