@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.
- package/README.md +2 -2
- package/dist/commands/config.d.ts +19 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +11 -9
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +24 -4
- package/dist/types.d.ts +69 -2
- package/dist/types.js +1 -1
- package/dist/utils/config.d.ts +1 -1
- package/dist/utils/config.js +4 -8
- package/dist/utils/detect.d.ts +35 -2
- package/dist/utils/detect.js +204 -5
- package/dist/utils/secrets.d.ts +1 -1
- package/dist/utils/secrets.js +1 -1
- package/dist/utils/skill.d.ts +2 -2
- package/dist/utils/skill.js +655 -446
- package/package.json +1 -1
package/dist/utils/detect.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project detection utilities
|
|
3
3
|
*
|
|
4
|
-
* Auto-detects framework, package manager, monorepo structure,
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/utils/secrets.d.ts
CHANGED
|
@@ -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.
|
|
33
|
+
* Scan .haystack.json specifically for secrets
|
|
34
34
|
*/
|
|
35
35
|
export declare function scanHaystackConfig(configPath: string): Promise<SecretFinding[]>;
|
|
36
36
|
/**
|
package/dist/utils/secrets.js
CHANGED
|
@@ -202,7 +202,7 @@ export async function scanFile(filePath) {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
/**
|
|
205
|
-
* Scan .haystack.
|
|
205
|
+
* Scan .haystack.json specifically for secrets
|
|
206
206
|
*/
|
|
207
207
|
export async function scanHaystackConfig(configPath) {
|
|
208
208
|
return scanFile(configPath);
|
package/dist/utils/skill.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function createSkillFile(): Promise<string>;
|
|
6
6
|
/**
|
|
7
|
-
* Create the .claude/commands/
|
|
8
|
-
* Users can invoke with /haystack
|
|
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>;
|