@haystackeditor/cli 0.19.0 → 0.21.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 +38 -35
- package/dist/commands/fleet-policy.js +1 -22
- package/dist/commands/init.js +376 -129
- package/dist/commands/install-session-hooks.js +27 -10
- package/dist/commands/login.js +133 -34
- package/dist/commands/onboarding-contract.js +6 -0
- package/dist/commands/setup.js +1 -2
- package/dist/commands/verify-onboarding.js +365 -0
- package/dist/commands/verify-precompute.js +37 -14
- package/dist/commands/verify.js +108 -25
- package/dist/index.js +99 -26
- package/dist/schema.js +5 -1
- package/dist/utils/config.js +0 -16
- package/dist/utils/haystack-api.js +12 -1
- package/dist/utils/secrets.js +0 -51
- package/dist/utils/telemetry.js +32 -0
- package/package.json +2 -2
- package/schemas/init.v1.json +35 -0
- package/schemas/login.v1.json +17 -0
- package/schemas/verify-answer.v1.json +86 -0
- package/schemas/verify-onboarding.v1.json +13 -0
- package/schemas/verify.v1.json +270 -2
- package/dist/utils/detect.js +0 -507
package/dist/utils/detect.js
DELETED
|
@@ -1,507 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project detection utilities
|
|
3
|
-
*
|
|
4
|
-
* Auto-detects framework, package manager, monorepo structure, services,
|
|
5
|
-
* and inter-service dependencies from proxy configurations.
|
|
6
|
-
*/
|
|
7
|
-
import * as fs from 'fs/promises';
|
|
8
|
-
import * as path from 'path';
|
|
9
|
-
/**
|
|
10
|
-
* Detect project configuration
|
|
11
|
-
*/
|
|
12
|
-
export async function detectProject(rootDir = process.cwd()) {
|
|
13
|
-
const result = {
|
|
14
|
-
packageManager: await detectPackageManager(rootDir),
|
|
15
|
-
isMonorepo: false,
|
|
16
|
-
};
|
|
17
|
-
// Detect monorepo
|
|
18
|
-
const monorepo = await detectMonorepo(rootDir);
|
|
19
|
-
if (monorepo) {
|
|
20
|
-
result.isMonorepo = true;
|
|
21
|
-
result.monorepoTool = monorepo.tool;
|
|
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
|
-
}
|
|
27
|
-
}
|
|
28
|
-
// Detect framework
|
|
29
|
-
result.framework = await detectFramework(rootDir);
|
|
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)
|
|
36
|
-
setSuggestions(result);
|
|
37
|
-
// Detect auth bypass from .env.example
|
|
38
|
-
result.suggestedAuthBypass = await detectAuthBypass(rootDir);
|
|
39
|
-
return result;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Detect package manager from lockfiles
|
|
43
|
-
*/
|
|
44
|
-
async function detectPackageManager(rootDir) {
|
|
45
|
-
const checks = [
|
|
46
|
-
{ file: 'pnpm-lock.yaml', manager: 'pnpm' },
|
|
47
|
-
{ file: 'yarn.lock', manager: 'yarn' },
|
|
48
|
-
{ file: 'bun.lockb', manager: 'bun' },
|
|
49
|
-
{ file: 'package-lock.json', manager: 'npm' },
|
|
50
|
-
];
|
|
51
|
-
for (const { file, manager } of checks) {
|
|
52
|
-
try {
|
|
53
|
-
await fs.access(path.join(rootDir, file));
|
|
54
|
-
return manager;
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
// Not found, continue
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return 'npm'; // Default
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Detect monorepo structure
|
|
64
|
-
*/
|
|
65
|
-
async function detectMonorepo(rootDir) {
|
|
66
|
-
// Check pnpm workspaces
|
|
67
|
-
try {
|
|
68
|
-
const pnpmWorkspace = await fs.readFile(path.join(rootDir, 'pnpm-workspace.yaml'), 'utf-8');
|
|
69
|
-
const match = pnpmWorkspace.match(/packages:\s*\n([\s\S]*?)(?:\n\w|$)/);
|
|
70
|
-
if (match) {
|
|
71
|
-
const workspaces = match[1]
|
|
72
|
-
.split('\n')
|
|
73
|
-
.map((line) => line.trim().replace(/^-\s*['"]?|['"]?$/g, ''))
|
|
74
|
-
.filter(Boolean);
|
|
75
|
-
return { tool: 'pnpm-workspaces', workspaces };
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
// Not found
|
|
80
|
-
}
|
|
81
|
-
// Check package.json workspaces (yarn/npm)
|
|
82
|
-
try {
|
|
83
|
-
const pkgJson = JSON.parse(await fs.readFile(path.join(rootDir, 'package.json'), 'utf-8'));
|
|
84
|
-
if (pkgJson.workspaces) {
|
|
85
|
-
const workspaces = Array.isArray(pkgJson.workspaces)
|
|
86
|
-
? pkgJson.workspaces
|
|
87
|
-
: pkgJson.workspaces.packages || [];
|
|
88
|
-
return { tool: 'pnpm-workspaces', workspaces };
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
// Not found
|
|
93
|
-
}
|
|
94
|
-
// Check for nx.json
|
|
95
|
-
try {
|
|
96
|
-
await fs.access(path.join(rootDir, 'nx.json'));
|
|
97
|
-
return { tool: 'nx', workspaces: ['packages/*', 'apps/*'] };
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
// Not found
|
|
101
|
-
}
|
|
102
|
-
// Check for turbo.json
|
|
103
|
-
try {
|
|
104
|
-
await fs.access(path.join(rootDir, 'turbo.json'));
|
|
105
|
-
return { tool: 'turborepo', workspaces: ['packages/*', 'apps/*'] };
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
// Not found
|
|
109
|
-
}
|
|
110
|
-
// Check for lerna.json
|
|
111
|
-
try {
|
|
112
|
-
await fs.access(path.join(rootDir, 'lerna.json'));
|
|
113
|
-
return { tool: 'lerna', workspaces: ['packages/*'] };
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
// Not found
|
|
117
|
-
}
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Detect services in a monorepo
|
|
122
|
-
*/
|
|
123
|
-
async function detectServices(rootDir, workspacePatterns) {
|
|
124
|
-
const services = [];
|
|
125
|
-
const glob = await import('fast-glob');
|
|
126
|
-
for (const pattern of workspacePatterns) {
|
|
127
|
-
const matches = await glob.default(pattern, {
|
|
128
|
-
cwd: rootDir,
|
|
129
|
-
onlyDirectories: true,
|
|
130
|
-
ignore: ['node_modules'],
|
|
131
|
-
});
|
|
132
|
-
for (const match of matches) {
|
|
133
|
-
const service = await detectService(rootDir, match);
|
|
134
|
-
if (service) {
|
|
135
|
-
services.push(service);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
// Also check common directories
|
|
140
|
-
const commonDirs = ['infra', 'services', 'apps', 'packages'];
|
|
141
|
-
for (const dir of commonDirs) {
|
|
142
|
-
try {
|
|
143
|
-
const entries = await fs.readdir(path.join(rootDir, dir), {
|
|
144
|
-
withFileTypes: true,
|
|
145
|
-
});
|
|
146
|
-
for (const entry of entries) {
|
|
147
|
-
if (entry.isDirectory()) {
|
|
148
|
-
const servicePath = path.join(dir, entry.name);
|
|
149
|
-
// Skip if already detected
|
|
150
|
-
if (services.some((s) => s.root === servicePath))
|
|
151
|
-
continue;
|
|
152
|
-
const service = await detectService(rootDir, servicePath);
|
|
153
|
-
if (service) {
|
|
154
|
-
services.push(service);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
// Directory doesn't exist
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return services;
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Detect a single service
|
|
167
|
-
*/
|
|
168
|
-
async function detectService(rootDir, servicePath) {
|
|
169
|
-
const fullPath = path.join(rootDir, servicePath);
|
|
170
|
-
// Check for package.json
|
|
171
|
-
let pkgJson = null;
|
|
172
|
-
try {
|
|
173
|
-
pkgJson = JSON.parse(await fs.readFile(path.join(fullPath, 'package.json'), 'utf-8'));
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
return null; // No package.json, not a service
|
|
177
|
-
}
|
|
178
|
-
const name = pkgJson?.name?.replace(/^@[^/]+\//, '') || path.basename(servicePath);
|
|
179
|
-
const scripts = pkgJson?.scripts || {};
|
|
180
|
-
// Detect if it's a Cloudflare Worker
|
|
181
|
-
const isWorker = await hasFile(fullPath, 'wrangler.toml') ||
|
|
182
|
-
await hasFile(fullPath, 'wrangler.jsonc') ||
|
|
183
|
-
await hasFile(fullPath, 'wrangler.json');
|
|
184
|
-
// Detect framework
|
|
185
|
-
let framework;
|
|
186
|
-
let suggestedCommand;
|
|
187
|
-
let suggestedPort;
|
|
188
|
-
if (isWorker) {
|
|
189
|
-
framework = 'cloudflare-worker';
|
|
190
|
-
suggestedCommand = 'pnpm wrangler dev --local';
|
|
191
|
-
suggestedPort = 8787;
|
|
192
|
-
}
|
|
193
|
-
else if (await hasFile(fullPath, 'vite.config.ts') || await hasFile(fullPath, 'vite.config.js')) {
|
|
194
|
-
framework = 'vite';
|
|
195
|
-
suggestedCommand = 'pnpm dev';
|
|
196
|
-
// Try to extract custom port from vite config, fallback to default
|
|
197
|
-
suggestedPort = await extractPortFromConfig(fullPath, 'vite') ?? 5173;
|
|
198
|
-
}
|
|
199
|
-
else if (await hasFile(fullPath, 'next.config.js') || await hasFile(fullPath, 'next.config.ts')) {
|
|
200
|
-
framework = 'nextjs';
|
|
201
|
-
suggestedCommand = 'pnpm dev';
|
|
202
|
-
// Try to extract custom port from next config, fallback to default
|
|
203
|
-
suggestedPort = await extractPortFromConfig(fullPath, 'nextjs') ?? 3000;
|
|
204
|
-
}
|
|
205
|
-
else if (scripts.dev) {
|
|
206
|
-
suggestedCommand = 'pnpm dev';
|
|
207
|
-
suggestedPort = 3000;
|
|
208
|
-
}
|
|
209
|
-
else if (scripts.start) {
|
|
210
|
-
suggestedCommand = 'pnpm start';
|
|
211
|
-
}
|
|
212
|
-
// Determine if it's a batch job or server
|
|
213
|
-
const type = name.includes('analysis') || name.includes('batch') || name.includes('job')
|
|
214
|
-
? 'batch'
|
|
215
|
-
: 'server';
|
|
216
|
-
return {
|
|
217
|
-
name,
|
|
218
|
-
root: servicePath,
|
|
219
|
-
type,
|
|
220
|
-
framework,
|
|
221
|
-
suggestedCommand,
|
|
222
|
-
suggestedPort: type === 'server' ? suggestedPort : undefined,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Detect framework from config files
|
|
227
|
-
*/
|
|
228
|
-
async function detectFramework(rootDir) {
|
|
229
|
-
const checks = [
|
|
230
|
-
{ files: ['vite.config.ts', 'vite.config.js'], framework: 'vite' },
|
|
231
|
-
{ files: ['next.config.js', 'next.config.ts', 'next.config.mjs'], framework: 'nextjs' },
|
|
232
|
-
{ files: ['remix.config.js', 'remix.config.ts'], framework: 'remix' },
|
|
233
|
-
{ files: ['nuxt.config.ts', 'nuxt.config.js'], framework: 'nuxt' },
|
|
234
|
-
{ files: ['svelte.config.js', 'svelte.config.ts'], framework: 'sveltekit' },
|
|
235
|
-
{ files: ['astro.config.mjs', 'astro.config.ts'], framework: 'astro' },
|
|
236
|
-
{ files: ['wrangler.toml', 'wrangler.jsonc'], framework: 'cloudflare-worker' },
|
|
237
|
-
];
|
|
238
|
-
for (const { files, framework } of checks) {
|
|
239
|
-
for (const file of files) {
|
|
240
|
-
if (await hasFile(rootDir, file)) {
|
|
241
|
-
return framework;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
// Check for CRA
|
|
246
|
-
try {
|
|
247
|
-
const pkgJson = JSON.parse(await fs.readFile(path.join(rootDir, 'package.json'), 'utf-8'));
|
|
248
|
-
if (pkgJson.dependencies?.['react-scripts']) {
|
|
249
|
-
return 'cra';
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
// Not found
|
|
254
|
-
}
|
|
255
|
-
return undefined;
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Detect auth bypass env var from .env.example
|
|
259
|
-
*/
|
|
260
|
-
async function detectAuthBypass(rootDir) {
|
|
261
|
-
const envFiles = ['.env.example', '.env.local.example', '.env.development'];
|
|
262
|
-
for (const file of envFiles) {
|
|
263
|
-
try {
|
|
264
|
-
const content = await fs.readFile(path.join(rootDir, file), 'utf-8');
|
|
265
|
-
// Common patterns
|
|
266
|
-
const patterns = [
|
|
267
|
-
/^(SKIP_AUTH)=/m,
|
|
268
|
-
/^(BYPASS_AUTH)=/m,
|
|
269
|
-
/^(AUTH_DISABLED)=/m,
|
|
270
|
-
/^(MOCK_AUTH)=/m,
|
|
271
|
-
/^(NEXT_PUBLIC_MOCK_AUTH)=/m,
|
|
272
|
-
/^(VITE_MOCK_AUTH)=/m,
|
|
273
|
-
/^(VITE_SKIP_AUTH)=/m,
|
|
274
|
-
];
|
|
275
|
-
for (const pattern of patterns) {
|
|
276
|
-
const match = content.match(pattern);
|
|
277
|
-
if (match) {
|
|
278
|
-
return `${match[1]}=true`;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
// File not found
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
// No evidence of an auth-bypass variable — suggest nothing rather than
|
|
287
|
-
// fabricating a SKIP_AUTH the app doesn't read.
|
|
288
|
-
return undefined;
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Set suggestions based on detected framework
|
|
292
|
-
* Note: This is sync, so custom port extraction happens in detectProject
|
|
293
|
-
*/
|
|
294
|
-
function setSuggestions(result) {
|
|
295
|
-
const frameworkDefaults = {
|
|
296
|
-
vite: { command: 'pnpm dev', port: 5173, ready: 'Local:' },
|
|
297
|
-
nextjs: { command: 'pnpm dev', port: 3000, ready: 'Ready' },
|
|
298
|
-
remix: { command: 'pnpm dev', port: 3000, ready: 'started' },
|
|
299
|
-
nuxt: { command: 'pnpm dev', port: 3000, ready: 'Local:' },
|
|
300
|
-
sveltekit: { command: 'pnpm dev', port: 5173, ready: 'Local:' },
|
|
301
|
-
astro: { command: 'pnpm dev', port: 4321, ready: 'Local:' },
|
|
302
|
-
cra: { command: 'pnpm start', port: 3000, ready: 'Compiled' },
|
|
303
|
-
'cloudflare-worker': { command: 'pnpm wrangler dev', port: 8787, ready: 'Ready' },
|
|
304
|
-
};
|
|
305
|
-
const defaults = result.framework ? frameworkDefaults[result.framework] : null;
|
|
306
|
-
result.suggestedDevCommand = defaults?.command || `${result.packageManager} dev`;
|
|
307
|
-
// suggestedPort may already be set from custom config extraction
|
|
308
|
-
result.suggestedPort = result.suggestedPort || defaults?.port || 3000;
|
|
309
|
-
result.suggestedReadyPattern = defaults?.ready || 'ready|started|listening|Local:';
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Check if a file exists
|
|
313
|
-
*/
|
|
314
|
-
async function hasFile(dir, file) {
|
|
315
|
-
try {
|
|
316
|
-
await fs.access(path.join(dir, file));
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
catch {
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Extract custom port from framework config files
|
|
325
|
-
*
|
|
326
|
-
* Parses vite.config.ts, next.config.js, etc. to find custom port settings
|
|
327
|
-
* Returns undefined if no custom port is configured (use framework default)
|
|
328
|
-
*/
|
|
329
|
-
async function extractPortFromConfig(rootDir, framework) {
|
|
330
|
-
if (!framework)
|
|
331
|
-
return undefined;
|
|
332
|
-
// Config files to check based on framework
|
|
333
|
-
const configFiles = {
|
|
334
|
-
vite: ['vite.config.ts', 'vite.config.js', 'vite.config.mjs'],
|
|
335
|
-
nextjs: ['next.config.js', 'next.config.ts', 'next.config.mjs'],
|
|
336
|
-
nuxt: ['nuxt.config.ts', 'nuxt.config.js'],
|
|
337
|
-
sveltekit: ['svelte.config.js', 'svelte.config.ts'],
|
|
338
|
-
astro: ['astro.config.mjs', 'astro.config.ts'],
|
|
339
|
-
};
|
|
340
|
-
const files = configFiles[framework];
|
|
341
|
-
if (!files)
|
|
342
|
-
return undefined;
|
|
343
|
-
let configContent = null;
|
|
344
|
-
for (const file of files) {
|
|
345
|
-
try {
|
|
346
|
-
configContent = await fs.readFile(path.join(rootDir, file), 'utf-8');
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
catch {
|
|
350
|
-
// File not found, try next
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
if (!configContent)
|
|
354
|
-
return undefined;
|
|
355
|
-
// Patterns to match port configuration
|
|
356
|
-
// Vite: server: { port: 3000 } or server.port = 3000
|
|
357
|
-
// Next: env: { PORT: 3000 } or experimental.serverPort
|
|
358
|
-
const patterns = [
|
|
359
|
-
/server\s*:\s*\{[^}]*port\s*:\s*(\d+)/s, // server: { port: 3000 }
|
|
360
|
-
/server\s*\.\s*port\s*[=:]\s*(\d+)/, // server.port = 3000
|
|
361
|
-
/port\s*:\s*(\d+)/, // port: 3000 (generic)
|
|
362
|
-
/PORT\s*[=:]\s*['"]?(\d+)['"]?/, // PORT = 3000
|
|
363
|
-
];
|
|
364
|
-
for (const pattern of patterns) {
|
|
365
|
-
const match = configContent.match(pattern);
|
|
366
|
-
if (match) {
|
|
367
|
-
const port = parseInt(match[1], 10);
|
|
368
|
-
if (port > 0 && port < 65536) {
|
|
369
|
-
return port;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return undefined;
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Detect proxy dependencies from vite.config.ts/js
|
|
377
|
-
*
|
|
378
|
-
* Parses the vite config file to find proxy targets that point to localhost,
|
|
379
|
-
* which indicate dependencies on other local services.
|
|
380
|
-
*
|
|
381
|
-
* @example
|
|
382
|
-
* // vite.config.ts with proxy
|
|
383
|
-
* server: {
|
|
384
|
-
* proxy: {
|
|
385
|
-
* '/api/review-chat': {
|
|
386
|
-
* target: 'http://localhost:8787',
|
|
387
|
-
* }
|
|
388
|
-
* }
|
|
389
|
-
* }
|
|
390
|
-
* // Returns: [{ path: '/api/review-chat', port: 8787, host: 'localhost' }]
|
|
391
|
-
*/
|
|
392
|
-
export async function detectProxyDependencies(rootDir) {
|
|
393
|
-
const dependencies = [];
|
|
394
|
-
// Try vite.config.ts, then vite.config.js
|
|
395
|
-
const configFiles = ['vite.config.ts', 'vite.config.js', 'vite.config.mjs'];
|
|
396
|
-
let configContent = null;
|
|
397
|
-
for (const file of configFiles) {
|
|
398
|
-
try {
|
|
399
|
-
configContent = await fs.readFile(path.join(rootDir, file), 'utf-8');
|
|
400
|
-
break;
|
|
401
|
-
}
|
|
402
|
-
catch {
|
|
403
|
-
// File not found, try next
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
if (!configContent) {
|
|
407
|
-
return dependencies;
|
|
408
|
-
}
|
|
409
|
-
// Parse proxy configurations using regex
|
|
410
|
-
// Matches patterns like:
|
|
411
|
-
// '/api/path': { target: 'http://localhost:8787' }
|
|
412
|
-
// '/api/path': 'http://localhost:8787'
|
|
413
|
-
// proxy: { '/api/path': { target: 'http://localhost:8787' } }
|
|
414
|
-
// Pattern for object-style proxy: '/path': { target: 'http://localhost:PORT' }
|
|
415
|
-
const objectProxyRegex = /['"]([^'"]+)['"]\s*:\s*\{[^}]*target\s*:\s*['"]https?:\/\/(localhost|127\.0\.0\.1):(\d+)['"]/g;
|
|
416
|
-
let match;
|
|
417
|
-
while ((match = objectProxyRegex.exec(configContent)) !== null) {
|
|
418
|
-
dependencies.push({
|
|
419
|
-
path: match[1],
|
|
420
|
-
host: match[2],
|
|
421
|
-
port: parseInt(match[3], 10),
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
// Pattern for string-style proxy: '/path': 'http://localhost:PORT'
|
|
425
|
-
const stringProxyRegex = /['"]([^'"]+)['"]\s*:\s*['"]https?:\/\/(localhost|127\.0\.0\.1):(\d+)['"]/g;
|
|
426
|
-
let stringMatch;
|
|
427
|
-
while ((stringMatch = stringProxyRegex.exec(configContent)) !== null) {
|
|
428
|
-
// Avoid duplicates from object pattern
|
|
429
|
-
const exists = dependencies.some(d => d.path === stringMatch[1] && d.port === parseInt(stringMatch[3], 10));
|
|
430
|
-
if (!exists) {
|
|
431
|
-
dependencies.push({
|
|
432
|
-
path: stringMatch[1],
|
|
433
|
-
host: stringMatch[2],
|
|
434
|
-
port: parseInt(stringMatch[3], 10),
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return dependencies;
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Match proxy dependencies to detected services
|
|
442
|
-
*
|
|
443
|
-
* Given a list of proxy targets (ports) and detected services,
|
|
444
|
-
* determines which services are dependencies based on port matching.
|
|
445
|
-
*/
|
|
446
|
-
export function matchProxyToServices(proxyDeps, services) {
|
|
447
|
-
// Create a port → service name mapping
|
|
448
|
-
const portToService = new Map();
|
|
449
|
-
for (const service of services) {
|
|
450
|
-
if (service.suggestedPort) {
|
|
451
|
-
portToService.set(service.suggestedPort, service.name);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
// For each service that has proxy deps pointing to another service's port,
|
|
455
|
-
// record the dependency
|
|
456
|
-
const serviceDeps = new Map();
|
|
457
|
-
for (const dep of proxyDeps) {
|
|
458
|
-
const matchedService = portToService.get(dep.port);
|
|
459
|
-
if (matchedService) {
|
|
460
|
-
dep.matchedService = matchedService;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
return serviceDeps;
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Suggest depends_on for services based on proxy configuration
|
|
467
|
-
*
|
|
468
|
-
* For the main service (usually frontend), detect if it proxies to other local services
|
|
469
|
-
* and suggest those as dependencies.
|
|
470
|
-
*/
|
|
471
|
-
export async function suggestServiceDependencies(rootDir, services) {
|
|
472
|
-
// Detect proxy dependencies from vite config
|
|
473
|
-
const proxyDeps = await detectProxyDependencies(rootDir);
|
|
474
|
-
if (proxyDeps.length === 0) {
|
|
475
|
-
return services;
|
|
476
|
-
}
|
|
477
|
-
// Create port → service mapping
|
|
478
|
-
const portToService = new Map();
|
|
479
|
-
for (const service of services) {
|
|
480
|
-
if (service.suggestedPort) {
|
|
481
|
-
portToService.set(service.suggestedPort, service.name);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
// Match proxy ports to services
|
|
485
|
-
const dependsOn = [];
|
|
486
|
-
for (const dep of proxyDeps) {
|
|
487
|
-
const serviceName = portToService.get(dep.port);
|
|
488
|
-
if (serviceName) {
|
|
489
|
-
dep.matchedService = serviceName;
|
|
490
|
-
if (!dependsOn.includes(serviceName)) {
|
|
491
|
-
dependsOn.push(serviceName);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
// Update the main frontend service with depends_on
|
|
496
|
-
// The main service is typically the one at root with port 3000/5173
|
|
497
|
-
return services.map(service => {
|
|
498
|
-
// If this service is at root and has detected proxy dependencies
|
|
499
|
-
if ((service.root === '.' || service.root === './') && dependsOn.length > 0) {
|
|
500
|
-
return {
|
|
501
|
-
...service,
|
|
502
|
-
suggestedDependsOn: dependsOn,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
return service;
|
|
506
|
-
});
|
|
507
|
-
}
|