@haystackeditor/cli 0.5.0 → 0.6.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,6 +20,10 @@ 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);
@@ -304,3 +309,136 @@ async function hasFile(dir, file) {
304
309
  return false;
305
310
  }
306
311
  }
312
+ /**
313
+ * Detect proxy dependencies from vite.config.ts/js
314
+ *
315
+ * Parses the vite config file to find proxy targets that point to localhost,
316
+ * which indicate dependencies on other local services.
317
+ *
318
+ * @example
319
+ * // vite.config.ts with proxy
320
+ * server: {
321
+ * proxy: {
322
+ * '/api/review-chat': {
323
+ * target: 'http://localhost:8787',
324
+ * }
325
+ * }
326
+ * }
327
+ * // Returns: [{ path: '/api/review-chat', port: 8787, host: 'localhost' }]
328
+ */
329
+ export async function detectProxyDependencies(rootDir) {
330
+ const dependencies = [];
331
+ // Try vite.config.ts, then vite.config.js
332
+ const configFiles = ['vite.config.ts', 'vite.config.js', 'vite.config.mjs'];
333
+ let configContent = null;
334
+ for (const file of configFiles) {
335
+ try {
336
+ configContent = await fs.readFile(path.join(rootDir, file), 'utf-8');
337
+ break;
338
+ }
339
+ catch {
340
+ // File not found, try next
341
+ }
342
+ }
343
+ if (!configContent) {
344
+ return dependencies;
345
+ }
346
+ // Parse proxy configurations using regex
347
+ // Matches patterns like:
348
+ // '/api/path': { target: 'http://localhost:8787' }
349
+ // '/api/path': 'http://localhost:8787'
350
+ // proxy: { '/api/path': { target: 'http://localhost:8787' } }
351
+ // Pattern for object-style proxy: '/path': { target: 'http://localhost:PORT' }
352
+ const objectProxyRegex = /['"]([^'"]+)['"]\s*:\s*\{[^}]*target\s*:\s*['"]https?:\/\/(localhost|127\.0\.0\.1):(\d+)['"]/g;
353
+ let match;
354
+ while ((match = objectProxyRegex.exec(configContent)) !== null) {
355
+ dependencies.push({
356
+ path: match[1],
357
+ host: match[2],
358
+ port: parseInt(match[3], 10),
359
+ });
360
+ }
361
+ // Pattern for string-style proxy: '/path': 'http://localhost:PORT'
362
+ const stringProxyRegex = /['"]([^'"]+)['"]\s*:\s*['"]https?:\/\/(localhost|127\.0\.0\.1):(\d+)['"]/g;
363
+ let stringMatch;
364
+ while ((stringMatch = stringProxyRegex.exec(configContent)) !== null) {
365
+ // Avoid duplicates from object pattern
366
+ const exists = dependencies.some(d => d.path === stringMatch[1] && d.port === parseInt(stringMatch[3], 10));
367
+ if (!exists) {
368
+ dependencies.push({
369
+ path: stringMatch[1],
370
+ host: stringMatch[2],
371
+ port: parseInt(stringMatch[3], 10),
372
+ });
373
+ }
374
+ }
375
+ return dependencies;
376
+ }
377
+ /**
378
+ * Match proxy dependencies to detected services
379
+ *
380
+ * Given a list of proxy targets (ports) and detected services,
381
+ * determines which services are dependencies based on port matching.
382
+ */
383
+ export function matchProxyToServices(proxyDeps, services) {
384
+ // Create a port → service name mapping
385
+ const portToService = new Map();
386
+ for (const service of services) {
387
+ if (service.suggestedPort) {
388
+ portToService.set(service.suggestedPort, service.name);
389
+ }
390
+ }
391
+ // For each service that has proxy deps pointing to another service's port,
392
+ // record the dependency
393
+ const serviceDeps = new Map();
394
+ for (const dep of proxyDeps) {
395
+ const matchedService = portToService.get(dep.port);
396
+ if (matchedService) {
397
+ dep.matchedService = matchedService;
398
+ }
399
+ }
400
+ return serviceDeps;
401
+ }
402
+ /**
403
+ * Suggest depends_on for services based on proxy configuration
404
+ *
405
+ * For the main service (usually frontend), detect if it proxies to other local services
406
+ * and suggest those as dependencies.
407
+ */
408
+ export async function suggestServiceDependencies(rootDir, services) {
409
+ // Detect proxy dependencies from vite config
410
+ const proxyDeps = await detectProxyDependencies(rootDir);
411
+ if (proxyDeps.length === 0) {
412
+ return services;
413
+ }
414
+ // Create port → service mapping
415
+ const portToService = new Map();
416
+ for (const service of services) {
417
+ if (service.suggestedPort) {
418
+ portToService.set(service.suggestedPort, service.name);
419
+ }
420
+ }
421
+ // Match proxy ports to services
422
+ const dependsOn = [];
423
+ for (const dep of proxyDeps) {
424
+ const serviceName = portToService.get(dep.port);
425
+ if (serviceName) {
426
+ dep.matchedService = serviceName;
427
+ if (!dependsOn.includes(serviceName)) {
428
+ dependsOn.push(serviceName);
429
+ }
430
+ }
431
+ }
432
+ // Update the main frontend service with depends_on
433
+ // The main service is typically the one at root with port 3000/5173
434
+ return services.map(service => {
435
+ // If this service is at root and has detected proxy dependencies
436
+ if ((service.root === '.' || service.root === './') && dependsOn.length > 0) {
437
+ return {
438
+ ...service,
439
+ suggestedDependsOn: dependsOn,
440
+ };
441
+ }
442
+ return service;
443
+ });
444
+ }
@@ -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>;