@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 +202 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +299 -0
- package/dist/commands/status.d.ts +4 -0
- package/dist/commands/status.js +42 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +41 -0
- package/dist/types.d.ts +177 -0
- package/dist/types.js +6 -0
- package/dist/utils/config.d.ts +24 -0
- package/dist/utils/config.js +74 -0
- package/dist/utils/detect.d.ts +10 -0
- package/dist/utils/detect.js +306 -0
- package/dist/utils/secrets.d.ts +47 -0
- package/dist/utils/secrets.js +241 -0
- package/dist/utils/skill.d.ts +4 -0
- package/dist/utils/skill.js +249 -0
- package/package.json +51 -0
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,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,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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();
|