@haystackeditor/cli 0.4.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.
- 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 +34 -19
- 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 +139 -1
- 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/README.md
CHANGED
|
@@ -9,7 +9,7 @@ npx @haystackeditor/cli init
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
This auto-detects your framework, package manager, and ports, then creates:
|
|
12
|
-
- `.haystack.
|
|
12
|
+
- `.haystack.json` - Configuration for the verification agent
|
|
13
13
|
- `.agents/skills/haystack.md` - Skill file for AI agent discovery
|
|
14
14
|
|
|
15
15
|
## Commands
|
|
@@ -75,7 +75,7 @@ npx @haystackeditor/cli secrets set API_KEY xxx --scope repo --scope-id owner/re
|
|
|
75
75
|
|
|
76
76
|
## Configuration
|
|
77
77
|
|
|
78
|
-
The `init` command creates `.haystack.
|
|
78
|
+
The `init` command creates `.haystack.json`:
|
|
79
79
|
|
|
80
80
|
```yaml
|
|
81
81
|
version: "1"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config commands - manage user preferences stored on Haystack Platform
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Get current sandbox status
|
|
6
|
+
*/
|
|
7
|
+
export declare function getSandboxStatus(): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Enable sandbox mode
|
|
10
|
+
*/
|
|
11
|
+
export declare function enableSandbox(): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Disable sandbox mode
|
|
14
|
+
*/
|
|
15
|
+
export declare function disableSandbox(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Handle sandbox subcommand
|
|
18
|
+
*/
|
|
19
|
+
export declare function handleSandbox(action?: string): Promise<void>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config commands - manage user preferences stored on Haystack Platform
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { loadToken } from './login.js';
|
|
6
|
+
const API_BASE = 'https://haystackeditor.com/api/preferences';
|
|
7
|
+
async function requireAuth() {
|
|
8
|
+
const token = await loadToken();
|
|
9
|
+
if (!token) {
|
|
10
|
+
console.error(chalk.red('\nNot logged in. Run `haystack login` first.\n'));
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
return token;
|
|
14
|
+
}
|
|
15
|
+
async function apiRequest(method, token, body) {
|
|
16
|
+
const response = await fetch(API_BASE, {
|
|
17
|
+
method,
|
|
18
|
+
headers: {
|
|
19
|
+
'Authorization': `Bearer ${token}`,
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
'User-Agent': 'Haystack-CLI',
|
|
22
|
+
},
|
|
23
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
24
|
+
});
|
|
25
|
+
return response;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get current sandbox status
|
|
29
|
+
*/
|
|
30
|
+
export async function getSandboxStatus() {
|
|
31
|
+
const token = await requireAuth();
|
|
32
|
+
console.log(chalk.dim('\nFetching preferences...\n'));
|
|
33
|
+
try {
|
|
34
|
+
const response = await apiRequest('GET', token);
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
if (response.status === 401) {
|
|
37
|
+
console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Failed to get preferences: ${response.status}`);
|
|
41
|
+
}
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
console.log(chalk.bold('Sandbox mode:'), data.sandbox_enabled
|
|
44
|
+
? chalk.green('enabled')
|
|
45
|
+
: chalk.yellow('disabled'));
|
|
46
|
+
console.log();
|
|
47
|
+
if (data.sandbox_enabled) {
|
|
48
|
+
console.log(chalk.dim('Haystack can clone and store your repos for verification.'));
|
|
49
|
+
console.log(chalk.dim('All data is deleted after 24 hours.'));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(chalk.dim('Sandbox mode is disabled. Automated verification will not work.'));
|
|
53
|
+
console.log(chalk.dim('Enable with: haystack config sandbox on'));
|
|
54
|
+
}
|
|
55
|
+
console.log();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Enable sandbox mode
|
|
64
|
+
*/
|
|
65
|
+
export async function enableSandbox() {
|
|
66
|
+
const token = await requireAuth();
|
|
67
|
+
console.log(chalk.dim('\nEnabling sandbox mode...'));
|
|
68
|
+
try {
|
|
69
|
+
const response = await apiRequest('PUT', token, { sandbox_enabled: true });
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
if (response.status === 401) {
|
|
72
|
+
console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const error = await response.json().catch(() => ({}));
|
|
76
|
+
throw new Error(error.error || `Failed to update preferences: ${response.status}`);
|
|
77
|
+
}
|
|
78
|
+
console.log(chalk.green('\nSandbox mode enabled.\n'));
|
|
79
|
+
console.log(chalk.dim('Haystack can now clone and store your repos for verification.'));
|
|
80
|
+
console.log(chalk.dim('All data is deleted after 24 hours.\n'));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Disable sandbox mode
|
|
89
|
+
*/
|
|
90
|
+
export async function disableSandbox() {
|
|
91
|
+
const token = await requireAuth();
|
|
92
|
+
console.log(chalk.dim('\nDisabling sandbox mode...'));
|
|
93
|
+
try {
|
|
94
|
+
const response = await apiRequest('PUT', token, { sandbox_enabled: false });
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
if (response.status === 401) {
|
|
97
|
+
console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
const error = await response.json().catch(() => ({}));
|
|
101
|
+
throw new Error(error.error || `Failed to update preferences: ${response.status}`);
|
|
102
|
+
}
|
|
103
|
+
console.log(chalk.yellow('\nSandbox mode disabled.\n'));
|
|
104
|
+
console.log(chalk.dim('Automated verification will not work until you re-enable sandbox mode.'));
|
|
105
|
+
console.log(chalk.dim('Re-enable with: haystack config sandbox on\n'));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Handle sandbox subcommand
|
|
114
|
+
*/
|
|
115
|
+
export async function handleSandbox(action) {
|
|
116
|
+
switch (action?.toLowerCase()) {
|
|
117
|
+
case 'on':
|
|
118
|
+
case 'enable':
|
|
119
|
+
case 'true':
|
|
120
|
+
return enableSandbox();
|
|
121
|
+
case 'off':
|
|
122
|
+
case 'disable':
|
|
123
|
+
case 'false':
|
|
124
|
+
return disableSandbox();
|
|
125
|
+
case 'status':
|
|
126
|
+
case undefined:
|
|
127
|
+
return getSandboxStatus();
|
|
128
|
+
default:
|
|
129
|
+
console.error(chalk.red(`\nUnknown action: ${action}`));
|
|
130
|
+
console.log('\nUsage: haystack config sandbox [on|off|status]\n');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* haystack init - Interactive setup wizard
|
|
3
3
|
*
|
|
4
|
-
* Creates .haystack.
|
|
4
|
+
* Creates .haystack.json with auto-detected settings.
|
|
5
5
|
*/
|
|
6
6
|
import inquirer from 'inquirer';
|
|
7
7
|
import chalk from 'chalk';
|
|
@@ -24,7 +24,7 @@ export async function initCommand(options) {
|
|
|
24
24
|
{
|
|
25
25
|
type: 'confirm',
|
|
26
26
|
name: 'overwrite',
|
|
27
|
-
message: '.haystack.
|
|
27
|
+
message: '.haystack.json already exists. Overwrite?',
|
|
28
28
|
default: false,
|
|
29
29
|
},
|
|
30
30
|
]);
|
|
@@ -62,20 +62,32 @@ export async function initCommand(options) {
|
|
|
62
62
|
console.log(chalk.green(`✓ Created ${commandPath} (use /haystack in Claude Code)\n`));
|
|
63
63
|
// Security validation
|
|
64
64
|
await runSecurityCheck(configPath);
|
|
65
|
-
//
|
|
65
|
+
// Print the prompt for the AI agent to follow
|
|
66
66
|
console.log(chalk.cyan('━'.repeat(70)));
|
|
67
|
-
console.log(chalk.cyan.bold('\n📋
|
|
68
|
-
console.log(chalk.white('
|
|
69
|
-
console.log(chalk.white('
|
|
70
|
-
console.log(chalk.
|
|
71
|
-
console.log(chalk.
|
|
72
|
-
console.log(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
console.log(chalk.cyan.bold('\n📋 NEXT STEP: Add verification flows\n'));
|
|
68
|
+
console.log(chalk.white('The config has dev server settings. Now add flows to tell Haystack'));
|
|
69
|
+
console.log(chalk.white('what to verify when PRs are opened.\n'));
|
|
70
|
+
console.log(chalk.yellow.bold('Give this prompt to your AI coding agent:\n'));
|
|
71
|
+
console.log(chalk.cyan('━'.repeat(70)));
|
|
72
|
+
console.log(`
|
|
73
|
+
${chalk.white('Add Haystack verification flows to .haystack.json')}
|
|
74
|
+
|
|
75
|
+
${chalk.bold('STEP 1: Understand the app')}
|
|
76
|
+
- Read src/App.tsx and the main components
|
|
77
|
+
- Write 2 sentences: What does this app do? What's the core feature?
|
|
78
|
+
|
|
79
|
+
${chalk.bold('STEP 2: Identify what PRs would touch')}
|
|
80
|
+
- List the 5 most important UI areas that PRs change
|
|
81
|
+
- These need the most thorough flows
|
|
82
|
+
|
|
83
|
+
${chalk.bold('STEP 3: Add flows')}
|
|
84
|
+
- Read .agents/skills/haystack.md for the flow format
|
|
85
|
+
- Add flows for the 5 core areas first, then secondary pages
|
|
86
|
+
|
|
87
|
+
${chalk.bold('STEP 4: Validate')}
|
|
88
|
+
- Confirm the core feature from Step 1 has flows
|
|
89
|
+
- Verify selectors are specific (not generic like "#root")
|
|
90
|
+
`);
|
|
79
91
|
console.log(chalk.cyan('━'.repeat(70)));
|
|
80
92
|
return;
|
|
81
93
|
}
|
|
@@ -129,8 +141,7 @@ export async function initCommand(options) {
|
|
|
129
141
|
// Show the generated config
|
|
130
142
|
console.log(chalk.dim('Generated configuration:'));
|
|
131
143
|
console.log(chalk.dim('─'.repeat(40)));
|
|
132
|
-
|
|
133
|
-
console.log(yaml.stringify(config));
|
|
144
|
+
console.log(JSON.stringify(config, null, 2));
|
|
134
145
|
console.log(chalk.dim('─'.repeat(40)));
|
|
135
146
|
// Security validation
|
|
136
147
|
await runSecurityCheck(configPath);
|
|
@@ -252,12 +263,15 @@ async function configureMonorepo(detected, name) {
|
|
|
252
263
|
const [key, value] = authBypass.split('=');
|
|
253
264
|
env[key] = value || 'true';
|
|
254
265
|
}
|
|
266
|
+
// Check if this service has detected dependencies
|
|
267
|
+
const dependsOn = detectedService?.suggestedDependsOn?.filter((dep) => selectedServices.includes(dep));
|
|
255
268
|
services[serviceName] = {
|
|
256
269
|
...(answers.root !== './' ? { root: answers.root } : {}),
|
|
257
270
|
command: answers.command,
|
|
258
271
|
...(answers.type === 'server' ? { port: answers.port } : { type: 'batch' }),
|
|
259
272
|
ready_pattern: 'ready|started|listening|Local:',
|
|
260
273
|
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
274
|
+
...(dependsOn?.length ? { depends_on: dependsOn } : {}),
|
|
261
275
|
};
|
|
262
276
|
}
|
|
263
277
|
return {
|
|
@@ -282,6 +296,7 @@ function buildConfigFromDetection(detected) {
|
|
|
282
296
|
...(s.type === 'batch' ? { type: 'batch' } : {}),
|
|
283
297
|
ready_pattern: 'ready|started|listening|Local:',
|
|
284
298
|
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
299
|
+
...(s.suggestedDependsOn?.length ? { depends_on: s.suggestedDependsOn } : {}),
|
|
285
300
|
};
|
|
286
301
|
}
|
|
287
302
|
return {
|
|
@@ -315,8 +330,8 @@ function buildConfigFromDetection(detected) {
|
|
|
315
330
|
}
|
|
316
331
|
function printNextSteps() {
|
|
317
332
|
console.log(chalk.cyan('Next steps:'));
|
|
318
|
-
console.log(` 1. Review .haystack.
|
|
333
|
+
console.log(` 1. Review .haystack.json and adjust as needed`);
|
|
319
334
|
console.log(` 2. Add auth bypass env var if your app requires login`);
|
|
320
335
|
console.log(` 3. Add flows to describe key user journeys to verify`);
|
|
321
|
-
console.log(` 4. Commit: ${chalk.green('git add .haystack.
|
|
336
|
+
console.log(` 4. Commit: ${chalk.green('git add .haystack.json .agents/ && git commit -m "Add Haystack"')}\n`);
|
|
322
337
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* haystack status - Check if .haystack.
|
|
2
|
+
* haystack status - Check if .haystack.json exists and is valid
|
|
3
3
|
*/
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { loadConfig, findConfigPath } from '../utils/config.js';
|
|
6
6
|
export async function statusCommand() {
|
|
7
7
|
const configPath = await findConfigPath();
|
|
8
8
|
if (!configPath) {
|
|
9
|
-
console.log(chalk.yellow('⚠ No .haystack.
|
|
9
|
+
console.log(chalk.yellow('⚠ No .haystack.json found'));
|
|
10
10
|
console.log(chalk.dim('\nRun `haystack init` to create one.\n'));
|
|
11
11
|
process.exit(1);
|
|
12
12
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* This enables AI agents to spin up sandboxes of your app for testing.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* npx @haystackeditor/cli init # Set up .haystack.
|
|
9
|
+
* npx @haystackeditor/cli init # Set up .haystack.json
|
|
10
10
|
* npx @haystackeditor/cli status # Check configuration
|
|
11
11
|
* npx @haystackeditor/cli login # Authenticate with GitHub
|
|
12
12
|
* npx @haystackeditor/cli secrets list # List stored secrets
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* This enables AI agents to spin up sandboxes of your app for testing.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* npx @haystackeditor/cli init # Set up .haystack.
|
|
9
|
+
* npx @haystackeditor/cli init # Set up .haystack.json
|
|
10
10
|
* npx @haystackeditor/cli status # Check configuration
|
|
11
11
|
* npx @haystackeditor/cli login # Authenticate with GitHub
|
|
12
12
|
* npx @haystackeditor/cli secrets list # List stored secrets
|
|
@@ -16,6 +16,7 @@ import { statusCommand } from './commands/status.js';
|
|
|
16
16
|
import { initCommand } from './commands/init.js';
|
|
17
17
|
import { loginCommand, logoutCommand } from './commands/login.js';
|
|
18
18
|
import { listSecrets, setSecret, deleteSecret } from './commands/secrets.js';
|
|
19
|
+
import { handleSandbox } from './commands/config.js';
|
|
19
20
|
const program = new Command();
|
|
20
21
|
program
|
|
21
22
|
.name('haystack')
|
|
@@ -23,10 +24,10 @@ program
|
|
|
23
24
|
.version('0.3.0');
|
|
24
25
|
program
|
|
25
26
|
.command('init')
|
|
26
|
-
.description('Create .haystack.
|
|
27
|
+
.description('Create .haystack.json configuration')
|
|
27
28
|
.option('-y, --yes', 'Use auto-detected defaults without prompting')
|
|
28
29
|
.addHelpText('after', `
|
|
29
|
-
This creates a .haystack.
|
|
30
|
+
This creates a .haystack.json file that configures:
|
|
30
31
|
• How to start your dev server
|
|
31
32
|
• Verification commands (build, lint, typecheck)
|
|
32
33
|
• Auth bypass for sandbox environments
|
|
@@ -36,7 +37,7 @@ Once set up, AI agents can automatically spin up and test your app.
|
|
|
36
37
|
.action(initCommand);
|
|
37
38
|
program
|
|
38
39
|
.command('status')
|
|
39
|
-
.description('Check if .haystack.
|
|
40
|
+
.description('Check if .haystack.json exists and is valid')
|
|
40
41
|
.action(statusCommand);
|
|
41
42
|
program
|
|
42
43
|
.command('login')
|
|
@@ -66,6 +67,25 @@ secrets
|
|
|
66
67
|
.command('delete <key>')
|
|
67
68
|
.description('Delete a secret')
|
|
68
69
|
.action(deleteSecret);
|
|
70
|
+
// Config subcommands
|
|
71
|
+
const config = program
|
|
72
|
+
.command('config')
|
|
73
|
+
.description('Manage user preferences');
|
|
74
|
+
config
|
|
75
|
+
.command('sandbox [action]')
|
|
76
|
+
.description('Manage sandbox mode (on|off|status)')
|
|
77
|
+
.addHelpText('after', `
|
|
78
|
+
Actions:
|
|
79
|
+
on, enable Enable sandbox mode (allow repo cloning)
|
|
80
|
+
off, disable Disable sandbox mode
|
|
81
|
+
status Show current status (default)
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
haystack config sandbox # Show current status
|
|
85
|
+
haystack config sandbox on # Enable sandbox mode
|
|
86
|
+
haystack config sandbox off # Disable sandbox mode
|
|
87
|
+
`)
|
|
88
|
+
.action(handleSandbox);
|
|
69
89
|
// Show help if no command provided
|
|
70
90
|
if (process.argv.length === 2) {
|
|
71
91
|
program.help();
|
package/dist/types.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Haystack CLI Types
|
|
3
3
|
*
|
|
4
|
-
* These types define the .haystack.
|
|
4
|
+
* These types define the .haystack.json schema that sandbox.py reads.
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
|
-
* Main configuration file schema (.haystack.
|
|
7
|
+
* Main configuration file schema (.haystack.json)
|
|
8
8
|
*/
|
|
9
9
|
export interface HaystackConfig {
|
|
10
10
|
/** Schema version - must be "1" */
|
|
@@ -27,6 +27,26 @@ export interface HaystackConfig {
|
|
|
27
27
|
services?: Record<string, Service>;
|
|
28
28
|
/** Verification configuration */
|
|
29
29
|
verification?: VerificationConfig;
|
|
30
|
+
/**
|
|
31
|
+
* Network egress configuration for the sandbox.
|
|
32
|
+
* By default, sandbox can only reach Cloudflare + GitHub IPs.
|
|
33
|
+
* Add your own destinations here (staging servers, S3, etc.).
|
|
34
|
+
*/
|
|
35
|
+
network?: NetworkConfig;
|
|
36
|
+
/**
|
|
37
|
+
* Secrets for fixture fetching and other operations.
|
|
38
|
+
* These are injected as environment variables in the sandbox.
|
|
39
|
+
* Supports tokens for staging APIs, S3 credentials, etc.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ SECURITY: Never commit real secrets to git!
|
|
42
|
+
* Use environment variable references: $ENV_VAR or ${ENV_VAR}
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* secrets:
|
|
46
|
+
* STAGING_TOKEN: "$STAGING_API_TOKEN" # Read from local env
|
|
47
|
+
* AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}"
|
|
48
|
+
*/
|
|
49
|
+
secrets?: Record<string, string>;
|
|
30
50
|
}
|
|
31
51
|
/**
|
|
32
52
|
* Dev server configuration (simple mode)
|
|
@@ -57,6 +77,16 @@ export interface Service {
|
|
|
57
77
|
type?: 'server' | 'batch';
|
|
58
78
|
/** Environment variables to set */
|
|
59
79
|
env?: Record<string, string>;
|
|
80
|
+
/**
|
|
81
|
+
* Services that must be started before this service.
|
|
82
|
+
* Used when a service proxies to another (e.g., frontend → API worker).
|
|
83
|
+
* Auto-detected from vite.config.ts proxy configuration.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* depends_on:
|
|
87
|
+
* - haystack-worker # Start review-chat worker first
|
|
88
|
+
*/
|
|
89
|
+
depends_on?: string[];
|
|
60
90
|
}
|
|
61
91
|
/**
|
|
62
92
|
* Verification configuration
|
|
@@ -67,6 +97,28 @@ export interface VerificationConfig {
|
|
|
67
97
|
/** Fixture definitions - array of pattern/source pairs */
|
|
68
98
|
fixtures?: Fixture[];
|
|
69
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Network egress configuration
|
|
102
|
+
*/
|
|
103
|
+
export interface NetworkConfig {
|
|
104
|
+
/**
|
|
105
|
+
* Additional hosts/domains to allow egress to.
|
|
106
|
+
* The sandbox always allows Cloudflare and GitHub IPs.
|
|
107
|
+
* Add your own destinations here.
|
|
108
|
+
*
|
|
109
|
+
* Supports:
|
|
110
|
+
* - Exact domains: "staging.mycompany.com"
|
|
111
|
+
* - Wildcards: "*.s3.amazonaws.com"
|
|
112
|
+
* - CIDR blocks: "10.0.0.0/8"
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* allow:
|
|
116
|
+
* - "staging.mycompany.com"
|
|
117
|
+
* - "*.s3.amazonaws.com"
|
|
118
|
+
* - "internal-api.example.com"
|
|
119
|
+
*/
|
|
120
|
+
allow?: string[];
|
|
121
|
+
}
|
|
70
122
|
/**
|
|
71
123
|
* Fixture definition - pattern to match and source to load from
|
|
72
124
|
*/
|
|
@@ -155,6 +207,21 @@ export interface DetectedService {
|
|
|
155
207
|
framework?: string;
|
|
156
208
|
suggestedCommand?: string;
|
|
157
209
|
suggestedPort?: number;
|
|
210
|
+
/** Auto-detected dependencies from proxy configuration */
|
|
211
|
+
suggestedDependsOn?: string[];
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Proxy dependency detection result
|
|
215
|
+
*/
|
|
216
|
+
export interface ProxyDependency {
|
|
217
|
+
/** Proxy path pattern (e.g., "/api/review-chat") */
|
|
218
|
+
path: string;
|
|
219
|
+
/** Target port (e.g., 8787) */
|
|
220
|
+
port: number;
|
|
221
|
+
/** Target host (usually localhost) */
|
|
222
|
+
host: string;
|
|
223
|
+
/** Matched service name if found */
|
|
224
|
+
matchedService?: string;
|
|
158
225
|
}
|
|
159
226
|
/**
|
|
160
227
|
* Auth credentials stored in Haystack
|
package/dist/types.js
CHANGED
package/dist/utils/config.d.ts
CHANGED
|
@@ -19,6 +19,6 @@ export declare function saveConfig(config: HaystackConfig, configPath?: string):
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function configExists(startDir?: string): Promise<boolean>;
|
|
21
21
|
/**
|
|
22
|
-
* Get the project root (directory containing .haystack.
|
|
22
|
+
* Get the project root (directory containing .haystack.json or current dir)
|
|
23
23
|
*/
|
|
24
24
|
export declare function getProjectRoot(): Promise<string>;
|
package/dist/utils/config.js
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as fs from 'fs/promises';
|
|
5
5
|
import * as path from 'path';
|
|
6
|
-
|
|
7
|
-
const CONFIG_FILENAME = '.haystack.yml';
|
|
6
|
+
const CONFIG_FILENAME = '.haystack.json';
|
|
8
7
|
/**
|
|
9
8
|
* Find the config file by walking up the directory tree
|
|
10
9
|
*/
|
|
@@ -37,7 +36,7 @@ export async function loadConfig(configPath) {
|
|
|
37
36
|
}
|
|
38
37
|
try {
|
|
39
38
|
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
40
|
-
return
|
|
39
|
+
return JSON.parse(content);
|
|
41
40
|
}
|
|
42
41
|
catch (err) {
|
|
43
42
|
throw new Error(`Failed to parse ${resolvedPath}: ${err.message}`);
|
|
@@ -48,10 +47,7 @@ export async function loadConfig(configPath) {
|
|
|
48
47
|
*/
|
|
49
48
|
export async function saveConfig(config, configPath) {
|
|
50
49
|
const resolvedPath = configPath || path.join(process.cwd(), CONFIG_FILENAME);
|
|
51
|
-
const content =
|
|
52
|
-
indent: 2,
|
|
53
|
-
lineWidth: 0, // Don't wrap lines
|
|
54
|
-
});
|
|
50
|
+
const content = JSON.stringify(config, null, 2);
|
|
55
51
|
await fs.writeFile(resolvedPath, content, 'utf-8');
|
|
56
52
|
return resolvedPath;
|
|
57
53
|
}
|
|
@@ -63,7 +59,7 @@ export async function configExists(startDir) {
|
|
|
63
59
|
return configPath !== null;
|
|
64
60
|
}
|
|
65
61
|
/**
|
|
66
|
-
* Get the project root (directory containing .haystack.
|
|
62
|
+
* Get the project root (directory containing .haystack.json or current dir)
|
|
67
63
|
*/
|
|
68
64
|
export async function getProjectRoot() {
|
|
69
65
|
const configPath = await findConfigPath();
|
package/dist/utils/detect.d.ts
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
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
|
-
import type { DetectedProject } from '../types.js';
|
|
7
|
+
import type { DetectedProject, DetectedService, ProxyDependency } from '../types.js';
|
|
7
8
|
/**
|
|
8
9
|
* Detect project configuration
|
|
9
10
|
*/
|
|
10
11
|
export declare function detectProject(rootDir?: string): Promise<DetectedProject>;
|
|
12
|
+
/**
|
|
13
|
+
* Detect proxy dependencies from vite.config.ts/js
|
|
14
|
+
*
|
|
15
|
+
* Parses the vite config file to find proxy targets that point to localhost,
|
|
16
|
+
* which indicate dependencies on other local services.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // vite.config.ts with proxy
|
|
20
|
+
* server: {
|
|
21
|
+
* proxy: {
|
|
22
|
+
* '/api/review-chat': {
|
|
23
|
+
* target: 'http://localhost:8787',
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* // Returns: [{ path: '/api/review-chat', port: 8787, host: 'localhost' }]
|
|
28
|
+
*/
|
|
29
|
+
export declare function detectProxyDependencies(rootDir: string): Promise<ProxyDependency[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Match proxy dependencies to detected services
|
|
32
|
+
*
|
|
33
|
+
* Given a list of proxy targets (ports) and detected services,
|
|
34
|
+
* determines which services are dependencies based on port matching.
|
|
35
|
+
*/
|
|
36
|
+
export declare function matchProxyToServices(proxyDeps: ProxyDependency[], services: DetectedService[]): Map<string, string[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Suggest depends_on for services based on proxy configuration
|
|
39
|
+
*
|
|
40
|
+
* For the main service (usually frontend), detect if it proxies to other local services
|
|
41
|
+
* and suggest those as dependencies.
|
|
42
|
+
*/
|
|
43
|
+
export declare function suggestServiceDependencies(rootDir: string, services: DetectedService[]): Promise<DetectedService[]>;
|