@haystackeditor/cli 0.2.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 ADDED
@@ -0,0 +1,202 @@
1
+ # @haystackeditor/cli
2
+
3
+ Unified CLI for Haystack verification, fixtures, and sandboxes.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Global install
9
+ npm install -g @haystackeditor/cli
10
+
11
+ # Or run directly with npx
12
+ npx @haystackeditor/cli init
13
+ ```
14
+
15
+ ## Commands
16
+
17
+ ### `haystack init`
18
+
19
+ Interactive setup wizard to create `.haystack.yml` configuration:
20
+
21
+ ```bash
22
+ haystack init # Interactive wizard
23
+ haystack init -y # Accept all defaults
24
+ ```
25
+
26
+ ### `haystack login`
27
+
28
+ Authenticate with Haystack via GitHub OAuth:
29
+
30
+ ```bash
31
+ haystack login # Opens browser for GitHub OAuth
32
+ haystack login --token # Use existing GitHub token
33
+ haystack logout # Clear stored credentials
34
+ ```
35
+
36
+ ### `haystack secrets`
37
+
38
+ Manage secrets for fixtures and integrations:
39
+
40
+ ```bash
41
+ haystack secrets list # List all secrets
42
+ haystack secrets set KEY value # Set a secret
43
+ haystack secrets delete KEY # Delete a secret
44
+ ```
45
+
46
+ Secrets are stored securely in the Haystack platform and referenced in `.haystack.yml` using `$VARIABLE` syntax.
47
+
48
+ ### `haystack record`
49
+
50
+ Record API responses as fixtures:
51
+
52
+ ```bash
53
+ haystack record https://api.example.com/data
54
+ haystack record https://api.example.com/data --output fixtures/data.json
55
+ ```
56
+
57
+ ### `haystack verify`
58
+
59
+ Run verification commands from `.haystack.yml`:
60
+
61
+ ```bash
62
+ haystack verify # Run all verification commands
63
+ haystack verify -c build # Run specific command
64
+ haystack verify --dry-run # Show commands without running
65
+ ```
66
+
67
+ ### `haystack dev`
68
+
69
+ Start dev server with fixtures enabled:
70
+
71
+ ```bash
72
+ haystack dev # Start dev server
73
+ haystack dev -s frontend # Start specific service (monorepo)
74
+ haystack dev --no-fixtures # Start without fixture loading
75
+ ```
76
+
77
+ ### `haystack sandbox`
78
+
79
+ Manage Haystack sandboxes:
80
+
81
+ ```bash
82
+ haystack sandbox create # Create sandbox for current branch
83
+ haystack sandbox status # Check sandbox status
84
+ haystack sandbox open # Open sandbox in browser
85
+ haystack sandbox logs # Stream sandbox logs
86
+ haystack sandbox destroy # Destroy sandbox
87
+ ```
88
+
89
+ ### `haystack mcp`
90
+
91
+ Run as MCP server for Claude Code integration:
92
+
93
+ ```bash
94
+ # Add to Claude Code
95
+ claude mcp add haystack -- npx @haystackeditor/cli mcp
96
+ ```
97
+
98
+ ## Configuration
99
+
100
+ Create `.haystack.yml` in your project root:
101
+
102
+ ### Simple Project
103
+
104
+ ```yaml
105
+ version: "1"
106
+ name: my-app
107
+
108
+ dev_server:
109
+ command: pnpm dev
110
+ port: 3000
111
+ ready_pattern: "Local:"
112
+ env:
113
+ SKIP_AUTH: "true"
114
+
115
+ verification:
116
+ commands:
117
+ - name: build
118
+ run: pnpm build
119
+ - name: lint
120
+ run: pnpm lint
121
+ - name: typecheck
122
+ run: pnpm tsc --noEmit
123
+ ```
124
+
125
+ ### Monorepo
126
+
127
+ ```yaml
128
+ version: "1"
129
+ name: my-monorepo
130
+
131
+ services:
132
+ frontend:
133
+ command: pnpm dev
134
+ port: 3000
135
+ ready_pattern: "Local:"
136
+ env:
137
+ SKIP_AUTH: "true"
138
+
139
+ api:
140
+ root: packages/api
141
+ command: pnpm dev
142
+ port: 8080
143
+ ready_pattern: "listening"
144
+
145
+ worker:
146
+ root: infra/worker
147
+ command: pnpm dev
148
+ ready_pattern: "Ready"
149
+
150
+ analysis:
151
+ root: packages/analysis
152
+ type: batch
153
+ command: pnpm start
154
+
155
+ verification:
156
+ commands:
157
+ - name: build
158
+ run: pnpm build
159
+ - name: test
160
+ run: pnpm test
161
+
162
+ fixtures:
163
+ "*/api/github/*":
164
+ source: file://fixtures/github-api.json
165
+
166
+ "*/api/analysis/*":
167
+ source: pr://haystackeditor/example-repo/42
168
+ ```
169
+
170
+ ## Fixtures
171
+
172
+ Fixtures mock external API responses during development and testing. Sources:
173
+
174
+ | Prefix | Description |
175
+ |--------|-------------|
176
+ | `file://` | Local JSON file |
177
+ | `https://` | Remote URL (cached) |
178
+ | `s3://` | AWS S3 bucket |
179
+ | `r2://` | Cloudflare R2 bucket |
180
+ | `pr://owner/repo/number` | Haystack PR analysis data |
181
+ | `recorded://id` | Previously recorded fixture |
182
+ | `passthrough` | Don't intercept, let request through |
183
+
184
+ ### Using Secrets in Fixtures
185
+
186
+ ```yaml
187
+ fixtures:
188
+ "*/api/private/*":
189
+ source: s3://my-bucket/fixtures/data.json
190
+ headers:
191
+ Authorization: Bearer $AWS_TOKEN
192
+ ```
193
+
194
+ Set the secret:
195
+
196
+ ```bash
197
+ haystack secrets set AWS_TOKEN "your-token-here"
198
+ ```
199
+
200
+ ## License
201
+
202
+ MIT
@@ -0,0 +1,10 @@
1
+ /**
2
+ * haystack init - Interactive setup wizard
3
+ *
4
+ * Creates .haystack.yml with auto-detected settings.
5
+ */
6
+ interface InitOptions {
7
+ yes?: boolean;
8
+ }
9
+ export declare function initCommand(options: InitOptions): Promise<void>;
10
+ export {};
@@ -0,0 +1,299 @@
1
+ /**
2
+ * haystack init - Interactive setup wizard
3
+ *
4
+ * Creates .haystack.yml with auto-detected settings.
5
+ */
6
+ import inquirer from 'inquirer';
7
+ import chalk from 'chalk';
8
+ import * as path from 'path';
9
+ import { detectProject } from '../utils/detect.js';
10
+ import { saveConfig, configExists } from '../utils/config.js';
11
+ import { createSkillFile } from '../utils/skill.js';
12
+ import { validateConfigSecurity, formatSecurityReport } from '../utils/secrets.js';
13
+ export async function initCommand(options) {
14
+ console.log(chalk.cyan('\n🌾 Haystack Setup Wizard\n'));
15
+ // Check if config already exists
16
+ if (await configExists()) {
17
+ const { overwrite } = await inquirer.prompt([
18
+ {
19
+ type: 'confirm',
20
+ name: 'overwrite',
21
+ message: '.haystack.yml already exists. Overwrite?',
22
+ default: false,
23
+ },
24
+ ]);
25
+ if (!overwrite) {
26
+ console.log(chalk.yellow('Setup cancelled.'));
27
+ return;
28
+ }
29
+ }
30
+ // Detect project
31
+ console.log(chalk.dim('Detecting project configuration...\n'));
32
+ const detected = await detectProject();
33
+ // Show what was detected
34
+ console.log(chalk.green('✓ Detected:'));
35
+ console.log(` Package manager: ${chalk.bold(detected.packageManager)}`);
36
+ if (detected.framework) {
37
+ console.log(` Framework: ${chalk.bold(detected.framework)}`);
38
+ }
39
+ if (detected.isMonorepo) {
40
+ console.log(` Monorepo: ${chalk.bold(detected.monorepoTool || 'yes')}`);
41
+ if (detected.services?.length) {
42
+ console.log(` Services: ${chalk.bold(detected.services.map((s) => s.name).join(', '))}`);
43
+ }
44
+ }
45
+ console.log('');
46
+ // If --yes flag, use all defaults
47
+ if (options.yes) {
48
+ const config = buildConfigFromDetection(detected);
49
+ const configPath = await saveConfig(config);
50
+ console.log(chalk.green(`✓ Created ${configPath}`));
51
+ // Create skill file for agent discovery
52
+ const skillPath = await createSkillFile();
53
+ console.log(chalk.green(`✓ Created ${skillPath}\n`));
54
+ // Security validation
55
+ await runSecurityCheck(configPath);
56
+ printNextSteps();
57
+ return;
58
+ }
59
+ // Interactive prompts
60
+ const answers = await inquirer.prompt([
61
+ {
62
+ type: 'input',
63
+ name: 'name',
64
+ message: 'Project name:',
65
+ default: path.basename(process.cwd()),
66
+ },
67
+ {
68
+ type: 'confirm',
69
+ name: 'isMonorepo',
70
+ message: 'Is this a monorepo with multiple services?',
71
+ default: detected.isMonorepo,
72
+ },
73
+ ]);
74
+ let config;
75
+ if (answers.isMonorepo) {
76
+ config = await configureMonorepo(detected, answers.name);
77
+ }
78
+ else {
79
+ config = await configureSingleService(detected, answers.name);
80
+ }
81
+ // Verification commands
82
+ const { commands } = await inquirer.prompt([
83
+ {
84
+ type: 'input',
85
+ name: 'commands',
86
+ message: 'Verification commands (comma-separated):',
87
+ default: `${detected.packageManager} build, ${detected.packageManager} lint, ${detected.packageManager} tsc --noEmit`,
88
+ },
89
+ ]);
90
+ config.verification = {
91
+ commands: commands
92
+ .split(',')
93
+ .map((cmd) => cmd.trim())
94
+ .filter(Boolean)
95
+ .map((cmd) => {
96
+ const name = cmd.split(' ').pop() || cmd;
97
+ return { name, run: cmd };
98
+ }),
99
+ };
100
+ // Save config
101
+ const configPath = await saveConfig(config);
102
+ console.log(chalk.green(`✓ Created ${configPath}`));
103
+ // Create skill file for agent discovery
104
+ const skillPath = await createSkillFile();
105
+ console.log(chalk.green(`✓ Created ${skillPath}\n`));
106
+ // Show the generated config
107
+ console.log(chalk.dim('Generated configuration:'));
108
+ console.log(chalk.dim('─'.repeat(40)));
109
+ const yaml = await import('yaml');
110
+ console.log(yaml.stringify(config));
111
+ console.log(chalk.dim('─'.repeat(40)));
112
+ // Security validation
113
+ await runSecurityCheck(configPath);
114
+ printNextSteps();
115
+ }
116
+ /**
117
+ * Run security check on config file
118
+ */
119
+ async function runSecurityCheck(configPath) {
120
+ const { safe, findings } = await validateConfigSecurity(configPath);
121
+ if (!safe) {
122
+ console.log(chalk.yellow('\n⚠️ Security Warning:\n'));
123
+ console.log(formatSecurityReport(findings));
124
+ console.log(chalk.yellow('Please remove hardcoded secrets before committing.\n'));
125
+ }
126
+ else if (findings.length > 0) {
127
+ console.log(chalk.dim('\n' + formatSecurityReport(findings)));
128
+ }
129
+ }
130
+ async function configureSingleService(detected, name) {
131
+ const answers = await inquirer.prompt([
132
+ {
133
+ type: 'input',
134
+ name: 'command',
135
+ message: 'Dev server command:',
136
+ default: detected.suggestedDevCommand,
137
+ },
138
+ {
139
+ type: 'number',
140
+ name: 'port',
141
+ message: 'Dev server port:',
142
+ default: detected.suggestedPort,
143
+ },
144
+ {
145
+ type: 'input',
146
+ name: 'readyPattern',
147
+ message: 'Ready pattern (stdout text when server is ready):',
148
+ default: detected.suggestedReadyPattern,
149
+ },
150
+ {
151
+ type: 'input',
152
+ name: 'authBypass',
153
+ message: 'Auth bypass env var (leave blank if no auth):',
154
+ default: detected.suggestedAuthBypass,
155
+ },
156
+ ]);
157
+ const env = {};
158
+ if (answers.authBypass) {
159
+ const [key, value] = answers.authBypass.split('=');
160
+ env[key] = value || 'true';
161
+ }
162
+ return {
163
+ version: '1',
164
+ name,
165
+ dev_server: {
166
+ command: answers.command,
167
+ port: answers.port,
168
+ ready_pattern: answers.readyPattern,
169
+ ...(Object.keys(env).length > 0 ? { env } : {}),
170
+ },
171
+ };
172
+ }
173
+ async function configureMonorepo(detected, name) {
174
+ // Get list of services
175
+ const detectedServices = detected.services || [];
176
+ const serviceNames = detectedServices.map((s) => s.name);
177
+ const { selectedServices } = await inquirer.prompt([
178
+ {
179
+ type: 'checkbox',
180
+ name: 'selectedServices',
181
+ message: 'Which services should the sandbox run?',
182
+ choices: serviceNames.length > 0 ? serviceNames : ['frontend', 'api'],
183
+ default: serviceNames,
184
+ },
185
+ ]);
186
+ // Ask about auth bypass
187
+ const { authBypass } = await inquirer.prompt([
188
+ {
189
+ type: 'input',
190
+ name: 'authBypass',
191
+ message: 'Auth bypass env var (applied to all services):',
192
+ default: detected.suggestedAuthBypass,
193
+ },
194
+ ]);
195
+ const services = {};
196
+ for (const serviceName of selectedServices) {
197
+ const detectedService = detectedServices.find((s) => s.name === serviceName);
198
+ console.log(chalk.dim(`\nConfiguring ${serviceName}...`));
199
+ const answers = await inquirer.prompt([
200
+ {
201
+ type: 'input',
202
+ name: 'root',
203
+ message: ` Directory:`,
204
+ default: detectedService?.root || './',
205
+ },
206
+ {
207
+ type: 'input',
208
+ name: 'command',
209
+ message: ` Command:`,
210
+ default: detectedService?.suggestedCommand || `${detected.packageManager} dev`,
211
+ },
212
+ {
213
+ type: 'list',
214
+ name: 'type',
215
+ message: ` Type:`,
216
+ choices: ['server', 'batch'],
217
+ default: detectedService?.type || 'server',
218
+ },
219
+ {
220
+ type: 'number',
221
+ name: 'port',
222
+ message: ` Port:`,
223
+ default: detectedService?.suggestedPort || 3000,
224
+ when: (ans) => ans.type === 'server',
225
+ },
226
+ ]);
227
+ const env = {};
228
+ if (authBypass) {
229
+ const [key, value] = authBypass.split('=');
230
+ env[key] = value || 'true';
231
+ }
232
+ services[serviceName] = {
233
+ ...(answers.root !== './' ? { root: answers.root } : {}),
234
+ command: answers.command,
235
+ ...(answers.type === 'server' ? { port: answers.port } : { type: 'batch' }),
236
+ ready_pattern: 'ready|started|listening|Local:',
237
+ ...(Object.keys(env).length > 0 ? { env } : {}),
238
+ };
239
+ }
240
+ return {
241
+ version: '1',
242
+ name,
243
+ services,
244
+ };
245
+ }
246
+ function buildConfigFromDetection(detected) {
247
+ const env = {};
248
+ if (detected.suggestedAuthBypass) {
249
+ const [key, value] = detected.suggestedAuthBypass.split('=');
250
+ env[key] = value || 'true';
251
+ }
252
+ if (detected.isMonorepo && detected.services?.length) {
253
+ const services = {};
254
+ for (const s of detected.services) {
255
+ services[s.name] = {
256
+ ...(s.root !== './' ? { root: s.root } : {}),
257
+ command: s.suggestedCommand || 'pnpm dev',
258
+ ...(s.type === 'server' && s.suggestedPort ? { port: s.suggestedPort } : {}),
259
+ ...(s.type === 'batch' ? { type: 'batch' } : {}),
260
+ ready_pattern: 'ready|started|listening|Local:',
261
+ ...(Object.keys(env).length > 0 ? { env } : {}),
262
+ };
263
+ }
264
+ return {
265
+ version: '1',
266
+ name: path.basename(process.cwd()),
267
+ services,
268
+ verification: {
269
+ commands: [
270
+ { name: 'build', run: `${detected.packageManager} build` },
271
+ { name: 'lint', run: `${detected.packageManager} lint` },
272
+ ],
273
+ },
274
+ };
275
+ }
276
+ return {
277
+ version: '1',
278
+ name: path.basename(process.cwd()),
279
+ dev_server: {
280
+ command: detected.suggestedDevCommand || 'pnpm dev',
281
+ port: detected.suggestedPort || 3000,
282
+ ready_pattern: detected.suggestedReadyPattern || 'Local:',
283
+ ...(Object.keys(env).length > 0 ? { env } : {}),
284
+ },
285
+ verification: {
286
+ commands: [
287
+ { name: 'build', run: `${detected.packageManager} build` },
288
+ { name: 'lint', run: `${detected.packageManager} lint` },
289
+ ],
290
+ },
291
+ };
292
+ }
293
+ function printNextSteps() {
294
+ console.log(chalk.cyan('Next steps:'));
295
+ console.log(` 1. Review .haystack.yml and adjust as needed`);
296
+ console.log(` 2. Add auth bypass env var if your app requires login`);
297
+ console.log(` 3. Add flows to describe key user journeys to verify`);
298
+ console.log(` 4. Commit: ${chalk.green('git add .haystack.yml .agents/ && git commit -m "Add Haystack"')}\n`);
299
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * haystack status - Check if .haystack.yml exists and is valid
3
+ */
4
+ export declare function statusCommand(): Promise<void>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * haystack status - Check if .haystack.yml exists and is valid
3
+ */
4
+ import chalk from 'chalk';
5
+ import { loadConfig, findConfigPath } from '../utils/config.js';
6
+ export async function statusCommand() {
7
+ const configPath = await findConfigPath();
8
+ if (!configPath) {
9
+ console.log(chalk.yellow('⚠ No .haystack.yml found'));
10
+ console.log(chalk.dim('\nRun `haystack init` to create one.\n'));
11
+ process.exit(1);
12
+ }
13
+ console.log(chalk.green(`✓ Found ${configPath}`));
14
+ try {
15
+ const config = await loadConfig(configPath);
16
+ if (!config) {
17
+ console.log(chalk.red('✗ Failed to load config'));
18
+ process.exit(1);
19
+ }
20
+ console.log(chalk.green('✓ Config is valid\n'));
21
+ // Show config summary
22
+ if (config.name) {
23
+ console.log(` ${chalk.dim('Name:')} ${config.name}`);
24
+ }
25
+ if (config.dev_server) {
26
+ console.log(` ${chalk.dim('Dev server:')} ${config.dev_server.command} (port ${config.dev_server.port})`);
27
+ }
28
+ if (config.services) {
29
+ const serviceNames = Object.keys(config.services);
30
+ console.log(` ${chalk.dim('Services:')} ${serviceNames.join(', ')}`);
31
+ }
32
+ if (config.verification?.commands?.length) {
33
+ const cmdNames = config.verification.commands.map((c) => c.name);
34
+ console.log(` ${chalk.dim('Verification:')} ${cmdNames.join(', ')}`);
35
+ }
36
+ console.log('');
37
+ }
38
+ catch (e) {
39
+ console.log(chalk.red(`✗ Config is invalid: ${e.message}`));
40
+ process.exit(1);
41
+ }
42
+ }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Haystack CLI
4
+ *
5
+ * Set up your project for Haystack verification.
6
+ * This enables AI agents to spin up sandboxes of your app for testing.
7
+ *
8
+ * Usage:
9
+ * npx @haystackeditor/cli init # Set up .haystack.yml
10
+ * npx @haystackeditor/cli status # Check configuration
11
+ */
12
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Haystack CLI
4
+ *
5
+ * Set up your project for Haystack verification.
6
+ * This enables AI agents to spin up sandboxes of your app for testing.
7
+ *
8
+ * Usage:
9
+ * npx @haystackeditor/cli init # Set up .haystack.yml
10
+ * npx @haystackeditor/cli status # Check configuration
11
+ */
12
+ import { Command } from 'commander';
13
+ import { statusCommand } from './commands/status.js';
14
+ import { initCommand } from './commands/init.js';
15
+ const program = new Command();
16
+ program
17
+ .name('haystack')
18
+ .description('Set up Haystack verification for your project')
19
+ .version('0.2.0');
20
+ program
21
+ .command('init')
22
+ .description('Create .haystack.yml configuration')
23
+ .option('-y, --yes', 'Use auto-detected defaults without prompting')
24
+ .addHelpText('after', `
25
+ This creates a .haystack.yml file that configures:
26
+ • How to start your dev server
27
+ • Verification commands (build, lint, typecheck)
28
+ • Auth bypass for sandbox environments
29
+
30
+ Once set up, AI agents can automatically spin up and test your app.
31
+ `)
32
+ .action(initCommand);
33
+ program
34
+ .command('status')
35
+ .description('Check if .haystack.yml exists and is valid')
36
+ .action(statusCommand);
37
+ // Show help if no command provided
38
+ if (process.argv.length === 2) {
39
+ program.help();
40
+ }
41
+ program.parse();