@haystackeditor/cli 0.2.0 → 0.4.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 +48 -129
- package/dist/commands/init.js +26 -3
- package/dist/commands/login.d.ts +9 -0
- package/dist/commands/login.js +162 -0
- package/dist/commands/secrets.d.ts +18 -0
- package/dist/commands/secrets.js +133 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +35 -3
- package/dist/utils/skill.d.ts +6 -0
- package/dist/utils/skill.js +421 -1
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -1,105 +1,81 @@
|
|
|
1
1
|
# @haystackeditor/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Set up Haystack verification for your project. When PRs are opened, an AI agent spins up your app in a sandbox and verifies changes work correctly.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
# Global install
|
|
9
|
-
npm install -g @haystackeditor/cli
|
|
10
|
-
|
|
11
|
-
# Or run directly with npx
|
|
12
8
|
npx @haystackeditor/cli init
|
|
13
9
|
```
|
|
14
10
|
|
|
11
|
+
This auto-detects your framework, package manager, and ports, then creates:
|
|
12
|
+
- `.haystack.yml` - Configuration for the verification agent
|
|
13
|
+
- `.agents/skills/haystack.md` - Skill file for AI agent discovery
|
|
14
|
+
|
|
15
15
|
## Commands
|
|
16
16
|
|
|
17
17
|
### `haystack init`
|
|
18
18
|
|
|
19
|
-
Interactive setup wizard
|
|
19
|
+
Interactive setup wizard:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
npx @haystackeditor/cli init # Interactive wizard
|
|
23
|
+
npx @haystackeditor/cli init -y # Accept all defaults
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
### `haystack
|
|
26
|
+
### `haystack status`
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Check if your project is configured:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
|
|
32
|
-
haystack login --token # Use existing GitHub token
|
|
33
|
-
haystack logout # Clear stored credentials
|
|
31
|
+
npx @haystackeditor/cli status
|
|
34
32
|
```
|
|
35
33
|
|
|
36
|
-
### `haystack
|
|
34
|
+
### `haystack login`
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
Authenticate with GitHub (required for secrets management):
|
|
39
37
|
|
|
40
38
|
```bash
|
|
41
|
-
|
|
42
|
-
haystack secrets set KEY value # Set a secret
|
|
43
|
-
haystack secrets delete KEY # Delete a secret
|
|
39
|
+
npx @haystackeditor/cli login
|
|
44
40
|
```
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
### `haystack record`
|
|
49
|
-
|
|
50
|
-
Record API responses as fixtures:
|
|
42
|
+
This uses GitHub's device flow - you'll get a code to enter at github.com/login/device.
|
|
51
43
|
|
|
52
44
|
```bash
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
# Log out (removes stored credentials)
|
|
46
|
+
npx @haystackeditor/cli logout
|
|
55
47
|
```
|
|
56
48
|
|
|
57
|
-
### `haystack
|
|
49
|
+
### `haystack secrets`
|
|
58
50
|
|
|
59
|
-
|
|
51
|
+
Manage secrets that will be injected into your sandbox environment:
|
|
60
52
|
|
|
61
53
|
```bash
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
haystack verify --dry-run # Show commands without running
|
|
65
|
-
```
|
|
54
|
+
# List all secrets (keys only, values are never shown)
|
|
55
|
+
npx @haystackeditor/cli secrets list
|
|
66
56
|
|
|
67
|
-
|
|
57
|
+
# Set a secret
|
|
58
|
+
npx @haystackeditor/cli secrets set OPENAI_API_KEY sk-xxx
|
|
68
59
|
|
|
69
|
-
|
|
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
|
|
60
|
+
# Delete a secret
|
|
61
|
+
npx @haystackeditor/cli secrets delete OPENAI_API_KEY
|
|
75
62
|
```
|
|
76
63
|
|
|
77
|
-
|
|
64
|
+
Secrets are encrypted and stored securely. They're automatically injected as environment variables when the sandbox runs your app.
|
|
78
65
|
|
|
79
|
-
|
|
66
|
+
**Scopes**: By default, secrets are user-scoped. You can also scope to an org or repo:
|
|
80
67
|
|
|
81
68
|
```bash
|
|
82
|
-
|
|
83
|
-
|
|
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`
|
|
69
|
+
# Org-scoped (available to all repos in the org)
|
|
70
|
+
npx @haystackeditor/cli secrets set API_KEY xxx --scope org --scope-id myorg
|
|
90
71
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
# Add to Claude Code
|
|
95
|
-
claude mcp add haystack -- npx @haystackeditor/cli mcp
|
|
72
|
+
# Repo-scoped (available only to this repo)
|
|
73
|
+
npx @haystackeditor/cli secrets set API_KEY xxx --scope repo --scope-id owner/repo
|
|
96
74
|
```
|
|
97
75
|
|
|
98
76
|
## Configuration
|
|
99
77
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
### Simple Project
|
|
78
|
+
The `init` command creates `.haystack.yml`:
|
|
103
79
|
|
|
104
80
|
```yaml
|
|
105
81
|
version: "1"
|
|
@@ -118,84 +94,27 @@ verification:
|
|
|
118
94
|
run: pnpm build
|
|
119
95
|
- name: lint
|
|
120
96
|
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
97
|
```
|
|
169
98
|
|
|
170
|
-
|
|
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 |
|
|
99
|
+
### Customizing After Init
|
|
183
100
|
|
|
184
|
-
|
|
101
|
+
| If your app has... | Add this |
|
|
102
|
+
|-------------------|----------|
|
|
103
|
+
| Login/authentication | Auth bypass env var in `dev_server.env` |
|
|
104
|
+
| Key user journeys | Flows describing what to verify |
|
|
105
|
+
| API calls needing auth | Fixtures to mock responses |
|
|
185
106
|
|
|
186
|
-
|
|
187
|
-
fixtures:
|
|
188
|
-
"*/api/private/*":
|
|
189
|
-
source: s3://my-bucket/fixtures/data.json
|
|
190
|
-
headers:
|
|
191
|
-
Authorization: Bearer $AWS_TOKEN
|
|
192
|
-
```
|
|
107
|
+
See the generated `.agents/skills/haystack.md` for full documentation on flows, fixtures, and monorepo configuration.
|
|
193
108
|
|
|
194
|
-
|
|
109
|
+
## How It Works
|
|
195
110
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
111
|
+
1. You run `npx @haystackeditor/cli init` and commit the config
|
|
112
|
+
2. When a PR is opened, Haystack's AI agent:
|
|
113
|
+
- Spins up your app in a Modal sandbox
|
|
114
|
+
- Reads the flows to understand what to verify
|
|
115
|
+
- Navigates the app autonomously
|
|
116
|
+
- Captures screenshots and evidence
|
|
117
|
+
- Reports results on the PR
|
|
199
118
|
|
|
200
119
|
## License
|
|
201
120
|
|
package/dist/commands/init.js
CHANGED
|
@@ -8,9 +8,15 @@ import chalk from 'chalk';
|
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import { detectProject } from '../utils/detect.js';
|
|
10
10
|
import { saveConfig, configExists } from '../utils/config.js';
|
|
11
|
-
import { createSkillFile } from '../utils/skill.js';
|
|
11
|
+
import { createSkillFile, createClaudeCommand } from '../utils/skill.js';
|
|
12
12
|
import { validateConfigSecurity, formatSecurityReport } from '../utils/secrets.js';
|
|
13
13
|
export async function initCommand(options) {
|
|
14
|
+
// Auto-use defaults when not in interactive terminal (e.g., when run by AI agents)
|
|
15
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
16
|
+
if (!isInteractive && !options.yes) {
|
|
17
|
+
console.log(chalk.dim('Non-interactive mode detected. Using --yes defaults.\n'));
|
|
18
|
+
options.yes = true;
|
|
19
|
+
}
|
|
14
20
|
console.log(chalk.cyan('\n🌾 Haystack Setup Wizard\n'));
|
|
15
21
|
// Check if config already exists
|
|
16
22
|
if (await configExists()) {
|
|
@@ -50,10 +56,27 @@ export async function initCommand(options) {
|
|
|
50
56
|
console.log(chalk.green(`✓ Created ${configPath}`));
|
|
51
57
|
// Create skill file for agent discovery
|
|
52
58
|
const skillPath = await createSkillFile();
|
|
53
|
-
console.log(chalk.green(`✓ Created ${skillPath}
|
|
59
|
+
console.log(chalk.green(`✓ Created ${skillPath}`));
|
|
60
|
+
// Create Claude Code slash command
|
|
61
|
+
const commandPath = await createClaudeCommand();
|
|
62
|
+
console.log(chalk.green(`✓ Created ${commandPath} (use /haystack in Claude Code)\n`));
|
|
54
63
|
// Security validation
|
|
55
64
|
await runSecurityCheck(configPath);
|
|
56
|
-
|
|
65
|
+
// Explain purpose and next steps
|
|
66
|
+
console.log(chalk.cyan('━'.repeat(70)));
|
|
67
|
+
console.log(chalk.cyan.bold('\n📋 WHAT IS HAYSTACK?\n'));
|
|
68
|
+
console.log(chalk.white('When a PR is opened, Haystack spins up your app in a sandbox and'));
|
|
69
|
+
console.log(chalk.white('an AI agent verifies the changes work correctly by:\n'));
|
|
70
|
+
console.log(chalk.white(' • Frontend: Navigating pages, clicking buttons, taking screenshots'));
|
|
71
|
+
console.log(chalk.white(' • Backend: Calling API endpoints, checking responses\n'));
|
|
72
|
+
console.log(chalk.white('The "flows" in .haystack.yml tell the agent WHAT to verify.\n'));
|
|
73
|
+
console.log(chalk.cyan.bold('⚠️ ACTION REQUIRED:\n'));
|
|
74
|
+
console.log(chalk.white('The config above only has dev server settings. You MUST add flows.\n'));
|
|
75
|
+
console.log(chalk.white.bold('1. Run: cat .agents/skills/haystack.md'));
|
|
76
|
+
console.log(chalk.white.bold('2. Read the "Codebase Discovery Guide" section'));
|
|
77
|
+
console.log(chalk.white.bold('3. Follow it to discover features and add flows to .haystack.yml\n'));
|
|
78
|
+
console.log(chalk.dim('Without flows, Haystack has nothing to verify.\n'));
|
|
79
|
+
console.log(chalk.cyan('━'.repeat(70)));
|
|
57
80
|
return;
|
|
58
81
|
}
|
|
59
82
|
// Interactive prompts
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login command - GitHub OAuth device flow
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Load token from disk
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadToken(): Promise<string | null>;
|
|
8
|
+
export declare function loginCommand(): Promise<void>;
|
|
9
|
+
export declare function logoutCommand(): Promise<void>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login command - GitHub OAuth device flow
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
const GITHUB_CLIENT_ID = 'Ov23liW3JE38D3gZWa85'; // Haystack GitHub OAuth App
|
|
9
|
+
const CONFIG_DIR = path.join(os.homedir(), '.haystack');
|
|
10
|
+
const TOKEN_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
11
|
+
/**
|
|
12
|
+
* Start device flow and get user code
|
|
13
|
+
*/
|
|
14
|
+
async function startDeviceFlow() {
|
|
15
|
+
const response = await fetch('https://github.com/login/device/code', {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Accept': 'application/json',
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
client_id: GITHUB_CLIENT_ID,
|
|
23
|
+
scope: 'read:user read:org repo',
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`Failed to start device flow: ${response.status}`);
|
|
28
|
+
}
|
|
29
|
+
return response.json();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Poll for access token
|
|
33
|
+
*/
|
|
34
|
+
async function pollForToken(deviceCode, interval) {
|
|
35
|
+
while (true) {
|
|
36
|
+
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
|
37
|
+
const response = await fetch('https://github.com/login/oauth/access_token', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Accept': 'application/json',
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
client_id: GITHUB_CLIENT_ID,
|
|
45
|
+
device_code: deviceCode,
|
|
46
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
if (data.access_token) {
|
|
51
|
+
return data.access_token;
|
|
52
|
+
}
|
|
53
|
+
if (data.error === 'authorization_pending') {
|
|
54
|
+
// Still waiting for user
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (data.error === 'slow_down') {
|
|
58
|
+
// Increase interval
|
|
59
|
+
interval += 5;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (data.error === 'expired_token') {
|
|
63
|
+
throw new Error('Authorization timed out. Please try again.');
|
|
64
|
+
}
|
|
65
|
+
if (data.error === 'access_denied') {
|
|
66
|
+
throw new Error('Authorization denied by user.');
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`OAuth error: ${data.error}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Save token to disk
|
|
73
|
+
*/
|
|
74
|
+
async function saveToken(token) {
|
|
75
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
76
|
+
const credentials = {
|
|
77
|
+
github_token: token,
|
|
78
|
+
created_at: new Date().toISOString(),
|
|
79
|
+
};
|
|
80
|
+
await fs.writeFile(TOKEN_FILE, JSON.stringify(credentials, null, 2), {
|
|
81
|
+
mode: 0o600, // Owner read/write only
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Load token from disk
|
|
86
|
+
*/
|
|
87
|
+
export async function loadToken() {
|
|
88
|
+
try {
|
|
89
|
+
const content = await fs.readFile(TOKEN_FILE, 'utf-8');
|
|
90
|
+
const credentials = JSON.parse(content);
|
|
91
|
+
return credentials.github_token;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Verify token is valid by calling GitHub API
|
|
99
|
+
*/
|
|
100
|
+
async function verifyToken(token) {
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch('https://api.github.com/user', {
|
|
103
|
+
headers: {
|
|
104
|
+
'Authorization': `Bearer ${token}`,
|
|
105
|
+
'User-Agent': 'Haystack-CLI',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return response.json();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function loginCommand() {
|
|
118
|
+
console.log(chalk.bold('\nHaystack Login\n'));
|
|
119
|
+
// Check if already logged in
|
|
120
|
+
const existingToken = await loadToken();
|
|
121
|
+
if (existingToken) {
|
|
122
|
+
const user = await verifyToken(existingToken);
|
|
123
|
+
if (user) {
|
|
124
|
+
console.log(chalk.green(`Already logged in as ${chalk.bold(user.login)}`));
|
|
125
|
+
console.log(chalk.dim('Run `haystack logout` to sign out.\n'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
console.log('Authenticating with GitHub...\n');
|
|
130
|
+
try {
|
|
131
|
+
// Start device flow
|
|
132
|
+
const deviceFlow = await startDeviceFlow();
|
|
133
|
+
console.log(chalk.yellow('Open this URL in your browser:\n'));
|
|
134
|
+
console.log(` ${chalk.bold(deviceFlow.verification_uri)}\n`);
|
|
135
|
+
console.log(chalk.yellow('And enter this code:\n'));
|
|
136
|
+
console.log(` ${chalk.bold.cyan(deviceFlow.user_code)}\n`);
|
|
137
|
+
console.log(chalk.dim('Waiting for authorization...'));
|
|
138
|
+
// Poll for token
|
|
139
|
+
const token = await pollForToken(deviceFlow.device_code, deviceFlow.interval);
|
|
140
|
+
// Verify and save
|
|
141
|
+
const user = await verifyToken(token);
|
|
142
|
+
if (!user) {
|
|
143
|
+
throw new Error('Failed to verify token');
|
|
144
|
+
}
|
|
145
|
+
await saveToken(token);
|
|
146
|
+
console.log(chalk.green(`\nLogged in as ${chalk.bold(user.login)}`));
|
|
147
|
+
console.log(chalk.dim('Credentials saved to ~/.haystack/credentials.json\n'));
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.error(chalk.red(`\nLogin failed: ${error.message}\n`));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export async function logoutCommand() {
|
|
155
|
+
try {
|
|
156
|
+
await fs.unlink(TOKEN_FILE);
|
|
157
|
+
console.log(chalk.green('\nLogged out successfully.\n'));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
console.log(chalk.yellow('\nNot logged in.\n'));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets commands - manage secrets stored on Haystack Platform
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* List all secrets (keys only, not values)
|
|
6
|
+
*/
|
|
7
|
+
export declare function listSecrets(): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Set a secret
|
|
10
|
+
*/
|
|
11
|
+
export declare function setSecret(key: string, value: string, options: {
|
|
12
|
+
scope?: string;
|
|
13
|
+
scopeId?: string;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Delete a secret
|
|
17
|
+
*/
|
|
18
|
+
export declare function deleteSecret(key: string): Promise<void>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets commands - manage secrets stored on Haystack Platform
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { loadToken } from './login.js';
|
|
6
|
+
const API_BASE = 'https://haystackeditor.com/api/secrets';
|
|
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, path, token, body) {
|
|
16
|
+
const url = `${API_BASE}${path}`;
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
method,
|
|
19
|
+
headers: {
|
|
20
|
+
'Authorization': `Bearer ${token}`,
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
'User-Agent': 'Haystack-CLI',
|
|
23
|
+
},
|
|
24
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
25
|
+
});
|
|
26
|
+
return response;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* List all secrets (keys only, not values)
|
|
30
|
+
*/
|
|
31
|
+
export async function listSecrets() {
|
|
32
|
+
const token = await requireAuth();
|
|
33
|
+
console.log(chalk.dim('\nFetching secrets...\n'));
|
|
34
|
+
try {
|
|
35
|
+
const response = await apiRequest('GET', '', token);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
if (response.status === 401) {
|
|
38
|
+
console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Failed to list secrets: ${response.status}`);
|
|
42
|
+
}
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
const secrets = data.secrets || [];
|
|
45
|
+
if (secrets.length === 0) {
|
|
46
|
+
console.log(chalk.yellow('No secrets found.\n'));
|
|
47
|
+
console.log(chalk.dim('Set a secret with: haystack secrets set KEY VALUE\n'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(chalk.bold('Your secrets:\n'));
|
|
51
|
+
for (const secret of secrets) {
|
|
52
|
+
const scope = secret.scope === 'user' ? '' : chalk.dim(` (${secret.scope}: ${secret.scopeId})`);
|
|
53
|
+
console.log(` ${chalk.cyan(secret.key)}${scope}`);
|
|
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
|
+
* Set a secret
|
|
64
|
+
*/
|
|
65
|
+
export async function setSecret(key, value, options) {
|
|
66
|
+
const token = await requireAuth();
|
|
67
|
+
if (!key || !value) {
|
|
68
|
+
console.error(chalk.red('\nUsage: haystack secrets set KEY VALUE\n'));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
// Validate key format
|
|
72
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
|
|
73
|
+
console.error(chalk.red('\nSecret key must be uppercase with underscores (e.g., MY_API_KEY)\n'));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
console.log(chalk.dim(`\nSetting secret ${key}...`));
|
|
77
|
+
try {
|
|
78
|
+
const body = {
|
|
79
|
+
key,
|
|
80
|
+
plaintextValue: value, // Server will encrypt
|
|
81
|
+
};
|
|
82
|
+
if (options.scope) {
|
|
83
|
+
body.scope = options.scope;
|
|
84
|
+
}
|
|
85
|
+
if (options.scopeId) {
|
|
86
|
+
body.scopeId = options.scopeId;
|
|
87
|
+
}
|
|
88
|
+
const response = await apiRequest('POST', '', token, body);
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
if (response.status === 401) {
|
|
91
|
+
console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const error = await response.json().catch(() => ({}));
|
|
95
|
+
throw new Error(error.error || `Failed to set secret: ${response.status}`);
|
|
96
|
+
}
|
|
97
|
+
console.log(chalk.green(`\nSecret ${chalk.bold(key)} saved.\n`));
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Delete a secret
|
|
106
|
+
*/
|
|
107
|
+
export async function deleteSecret(key) {
|
|
108
|
+
const token = await requireAuth();
|
|
109
|
+
if (!key) {
|
|
110
|
+
console.error(chalk.red('\nUsage: haystack secrets delete KEY\n'));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
console.log(chalk.dim(`\nDeleting secret ${key}...`));
|
|
114
|
+
try {
|
|
115
|
+
const response = await apiRequest('DELETE', `/${encodeURIComponent(key)}`, token);
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
if (response.status === 401) {
|
|
118
|
+
console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
if (response.status === 404) {
|
|
122
|
+
console.error(chalk.yellow(`\nSecret ${key} not found.\n`));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Failed to delete secret: ${response.status}`);
|
|
126
|
+
}
|
|
127
|
+
console.log(chalk.green(`\nSecret ${chalk.bold(key)} deleted.\n`));
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
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
|
|
10
|
-
* npx @haystackeditor/cli status
|
|
9
|
+
* npx @haystackeditor/cli init # Set up .haystack.yml
|
|
10
|
+
* npx @haystackeditor/cli status # Check configuration
|
|
11
|
+
* npx @haystackeditor/cli login # Authenticate with GitHub
|
|
12
|
+
* npx @haystackeditor/cli secrets list # List stored secrets
|
|
11
13
|
*/
|
|
12
14
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -6,17 +6,21 @@
|
|
|
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
|
|
10
|
-
* npx @haystackeditor/cli status
|
|
9
|
+
* npx @haystackeditor/cli init # Set up .haystack.yml
|
|
10
|
+
* npx @haystackeditor/cli status # Check configuration
|
|
11
|
+
* npx @haystackeditor/cli login # Authenticate with GitHub
|
|
12
|
+
* npx @haystackeditor/cli secrets list # List stored secrets
|
|
11
13
|
*/
|
|
12
14
|
import { Command } from 'commander';
|
|
13
15
|
import { statusCommand } from './commands/status.js';
|
|
14
16
|
import { initCommand } from './commands/init.js';
|
|
17
|
+
import { loginCommand, logoutCommand } from './commands/login.js';
|
|
18
|
+
import { listSecrets, setSecret, deleteSecret } from './commands/secrets.js';
|
|
15
19
|
const program = new Command();
|
|
16
20
|
program
|
|
17
21
|
.name('haystack')
|
|
18
22
|
.description('Set up Haystack verification for your project')
|
|
19
|
-
.version('0.
|
|
23
|
+
.version('0.3.0');
|
|
20
24
|
program
|
|
21
25
|
.command('init')
|
|
22
26
|
.description('Create .haystack.yml configuration')
|
|
@@ -34,6 +38,34 @@ program
|
|
|
34
38
|
.command('status')
|
|
35
39
|
.description('Check if .haystack.yml exists and is valid')
|
|
36
40
|
.action(statusCommand);
|
|
41
|
+
program
|
|
42
|
+
.command('login')
|
|
43
|
+
.description('Authenticate with GitHub')
|
|
44
|
+
.action(loginCommand);
|
|
45
|
+
program
|
|
46
|
+
.command('logout')
|
|
47
|
+
.description('Remove stored credentials')
|
|
48
|
+
.action(logoutCommand);
|
|
49
|
+
// Secrets subcommands
|
|
50
|
+
const secrets = program
|
|
51
|
+
.command('secrets')
|
|
52
|
+
.description('Manage secrets for sandbox environments');
|
|
53
|
+
secrets
|
|
54
|
+
.command('list')
|
|
55
|
+
.description('List all secrets (keys only)')
|
|
56
|
+
.action(listSecrets);
|
|
57
|
+
secrets
|
|
58
|
+
.command('set <key> <value>')
|
|
59
|
+
.description('Set a secret')
|
|
60
|
+
.option('--scope <scope>', 'Scope: user, org, or repo (default: user)')
|
|
61
|
+
.option('--scope-id <id>', 'Scope ID (org name or owner/repo)')
|
|
62
|
+
.action((key, value, options) => {
|
|
63
|
+
setSecret(key, value, options);
|
|
64
|
+
});
|
|
65
|
+
secrets
|
|
66
|
+
.command('delete <key>')
|
|
67
|
+
.description('Delete a secret')
|
|
68
|
+
.action(deleteSecret);
|
|
37
69
|
// Show help if no command provided
|
|
38
70
|
if (process.argv.length === 2) {
|
|
39
71
|
program.help();
|
package/dist/utils/skill.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create the .agents/skills/haystack.md file for agent discovery
|
|
3
|
+
* and .claude/commands/haystack.md for Claude Code slash command
|
|
3
4
|
*/
|
|
4
5
|
export declare function createSkillFile(): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Create the .claude/commands/haystack.md file for Claude Code slash command
|
|
8
|
+
* Users can invoke with /haystack to start the setup wizard
|
|
9
|
+
*/
|
|
10
|
+
export declare function createClaudeCommand(): Promise<string>;
|
package/dist/utils/skill.js
CHANGED
|
@@ -1,11 +1,161 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create the .agents/skills/haystack.md file for agent discovery
|
|
3
|
+
* and .claude/commands/haystack.md for Claude Code slash command
|
|
3
4
|
*/
|
|
4
5
|
import * as fs from 'fs/promises';
|
|
5
6
|
import * as path from 'path';
|
|
7
|
+
/**
|
|
8
|
+
* Claude Code slash command - invoked with /haystack
|
|
9
|
+
* This is the "one command" entry point for users.
|
|
10
|
+
* Uses task decomposition - complete one step, validate, then next step.
|
|
11
|
+
*/
|
|
12
|
+
const CLAUDE_COMMAND_CONTENT = `# Set Up Haystack Verification
|
|
13
|
+
|
|
14
|
+
You are setting up Haystack PR verification. Complete each step IN ORDER. Do NOT skip ahead.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## STEP 1: Initialize config
|
|
19
|
+
|
|
20
|
+
\`\`\`bash
|
|
21
|
+
npx @haystackeditor/cli init --yes
|
|
22
|
+
\`\`\`
|
|
23
|
+
|
|
24
|
+
✅ **Checkpoint**: \`.haystack.yml\` exists with dev_server config.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## STEP 2: Discover all routes
|
|
29
|
+
|
|
30
|
+
Find every route in the app:
|
|
31
|
+
\`\`\`bash
|
|
32
|
+
grep -r "path=\\|Route\\|<Link" src/ --include="*.tsx" | head -30
|
|
33
|
+
ls src/pages/ src/app/ 2>/dev/null
|
|
34
|
+
\`\`\`
|
|
35
|
+
|
|
36
|
+
Add a flow for EACH route to \`.haystack.yml\`. Use \`trigger: always\` for main pages, \`trigger: on_change\` with \`watch_patterns\` for others.
|
|
37
|
+
|
|
38
|
+
✅ **Checkpoint**: Count your flows. You should have one for every route.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## STEP 3: Fix ALL selectors (CRITICAL)
|
|
43
|
+
|
|
44
|
+
⛔ **STOP**: Look at every \`wait_for\` selector in your flows.
|
|
45
|
+
|
|
46
|
+
If ANY selector is \`#root\`, \`div\`, or \`h1\`, you MUST fix it now:
|
|
47
|
+
\`\`\`bash
|
|
48
|
+
# Find specific selectors in the codebase
|
|
49
|
+
grep -r "data-testid\\|className=" src/components/ --include="*.tsx" | head -20
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
Replace generic selectors with specific ones:
|
|
53
|
+
- \`[data-testid='dashboard']\`
|
|
54
|
+
- \`.dashboard-content\`
|
|
55
|
+
- \`[role='main']\`
|
|
56
|
+
|
|
57
|
+
✅ **Checkpoint**: Run \`grep "wait_for" .haystack.yml\` - NONE should have \`#root\`.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## STEP 4: Add 3+ interactive flows (REQUIRED)
|
|
62
|
+
|
|
63
|
+
Find interactive elements:
|
|
64
|
+
\`\`\`bash
|
|
65
|
+
grep -r "onClick\\|Modal\\|Dialog\\|toggle\\|Switch" src/ --include="*.tsx" | head -20
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
Add AT LEAST 3 flows with \`click\` or \`type\` actions:
|
|
69
|
+
|
|
70
|
+
\`\`\`yaml
|
|
71
|
+
- name: "Theme toggle works"
|
|
72
|
+
steps:
|
|
73
|
+
- action: navigate
|
|
74
|
+
url: "/"
|
|
75
|
+
- action: click
|
|
76
|
+
selector: "[data-testid='theme-toggle']"
|
|
77
|
+
- action: screenshot
|
|
78
|
+
name: "after-toggle"
|
|
79
|
+
|
|
80
|
+
- name: "Modal opens"
|
|
81
|
+
steps:
|
|
82
|
+
- action: navigate
|
|
83
|
+
url: "/dashboard"
|
|
84
|
+
- action: click
|
|
85
|
+
selector: "button[aria-label='Settings']"
|
|
86
|
+
- action: wait_for
|
|
87
|
+
selector: "[role='dialog']"
|
|
88
|
+
- action: screenshot
|
|
89
|
+
name: "modal-open"
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
✅ **Checkpoint**: Run \`grep -c "action: click" .haystack.yml\` - must be ≥3.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## STEP 5: Add fixtures for data-dependent pages
|
|
97
|
+
|
|
98
|
+
Find pages that fetch data:
|
|
99
|
+
\`\`\`bash
|
|
100
|
+
grep -r "useParams\\|fetch(\\|useQuery" src/ --include="*.tsx" | head -10
|
|
101
|
+
\`\`\`
|
|
102
|
+
|
|
103
|
+
For each data-dependent page, add fixtures:
|
|
104
|
+
\`\`\`yaml
|
|
105
|
+
fixtures:
|
|
106
|
+
- pattern: "/api/user/*"
|
|
107
|
+
source: "https://staging.example.com/api/user/demo"
|
|
108
|
+
- pattern: "/api/pr/*"
|
|
109
|
+
source: "file://fixtures/sample-pr.json"
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
✅ **Checkpoint**: Every page with \`:id\` or API calls has a fixture.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## STEP 6: Final validation
|
|
117
|
+
|
|
118
|
+
Count and verify:
|
|
119
|
+
\`\`\`bash
|
|
120
|
+
echo "=== Selector check (should be 0 #root) ==="
|
|
121
|
+
grep "#root" .haystack.yml | wc -l
|
|
122
|
+
|
|
123
|
+
echo "=== Interactive flows (should be ≥3) ==="
|
|
124
|
+
grep -c "action: click\\|action: type" .haystack.yml
|
|
125
|
+
|
|
126
|
+
echo "=== Fixtures (should be >0 for data pages) ==="
|
|
127
|
+
grep -c "pattern:" .haystack.yml
|
|
128
|
+
\`\`\`
|
|
129
|
+
|
|
130
|
+
If any check fails, GO BACK to that step and fix it.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## STEP 7: Commit
|
|
135
|
+
|
|
136
|
+
\`\`\`bash
|
|
137
|
+
git add .haystack.yml .agents/ .claude/ fixtures/
|
|
138
|
+
git commit -m "Add Haystack verification"
|
|
139
|
+
\`\`\`
|
|
140
|
+
|
|
141
|
+
🎉 Done!
|
|
142
|
+
`;
|
|
6
143
|
const SKILL_CONTENT = `# Haystack Verification
|
|
7
144
|
|
|
8
|
-
|
|
145
|
+
## What is Haystack?
|
|
146
|
+
|
|
147
|
+
Haystack provides **automated PR verification**. When a PR is opened:
|
|
148
|
+
|
|
149
|
+
1. A sandbox spins up with your app running (dev server + any backend services)
|
|
150
|
+
2. An AI agent reads the "flows" in \`.haystack.yml\`
|
|
151
|
+
3. The agent executes each flow to verify changes work correctly
|
|
152
|
+
4. Results (screenshots, API responses, errors) are posted to the PR
|
|
153
|
+
|
|
154
|
+
**Frontend flows**: The agent uses a browser to navigate pages, click buttons, fill forms, and take screenshots. This catches visual regressions and broken interactions.
|
|
155
|
+
|
|
156
|
+
**Backend flows**: The agent makes HTTP requests to API endpoints and verifies responses. This catches broken endpoints and API contract changes.
|
|
157
|
+
|
|
158
|
+
**Without flows, Haystack has nothing to verify.** The config's \`dev_server\` settings just tell it how to start your app - the flows tell it what to actually test.
|
|
9
159
|
|
|
10
160
|
## Setup Workflow
|
|
11
161
|
|
|
@@ -237,6 +387,263 @@ Most apps need auth bypassed for testing. Common patterns:
|
|
|
237
387
|
| Rails | \`SKIP_AUTH=true\` |
|
|
238
388
|
|
|
239
389
|
Add to \`dev_server.env\` or \`services.*.env\` in your config.
|
|
390
|
+
|
|
391
|
+
## Codebase Discovery Guide
|
|
392
|
+
|
|
393
|
+
**Follow these steps to create comprehensive verification flows.**
|
|
394
|
+
|
|
395
|
+
### ⚠️ REQUIRED CHECKLIST - Complete ALL items before finishing:
|
|
396
|
+
|
|
397
|
+
1. [ ] **Page flows**: Every route in the app has a flow
|
|
398
|
+
2. [ ] **Specific selectors**: Using \`[data-testid='x']\` or \`.specific-class\`, NOT \`#root\` or \`div\`
|
|
399
|
+
3. [ ] **Interactive flows**: At least 3 flows that click buttons, open modals, or submit forms
|
|
400
|
+
4. [ ] **Fixtures**: Pages with \`:id\` params or API fetches have fixtures (staging URL or local file)
|
|
401
|
+
5. [ ] **Backend API flows**: If app has API endpoints, add http_request flows to test them
|
|
402
|
+
6. [ ] **Watch patterns**: Each flow's \`watch_patterns\` matches the component file paths
|
|
403
|
+
|
|
404
|
+
**You are NOT done until all 6 items are checked.**
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
### Step 1: Trace the Component Tree
|
|
409
|
+
|
|
410
|
+
Start from the entry point and trace imports to discover ALL features:
|
|
411
|
+
|
|
412
|
+
\`\`\`bash
|
|
413
|
+
# Find the entry point
|
|
414
|
+
cat src/main.tsx # or src/index.tsx, pages/_app.tsx, etc.
|
|
415
|
+
|
|
416
|
+
# Trace the router to find all routes
|
|
417
|
+
grep -r "Route\|path=" src/ --include="*.tsx"
|
|
418
|
+
|
|
419
|
+
# Find all page/feature components
|
|
420
|
+
ls src/pages/ src/components/ src/features/
|
|
421
|
+
\`\`\`
|
|
422
|
+
|
|
423
|
+
### Step 2: Find Good Selectors
|
|
424
|
+
|
|
425
|
+
**DON'T use generic selectors like \`#root\`.** The agent needs specific selectors to know the page loaded correctly.
|
|
426
|
+
|
|
427
|
+
Priority order for selectors:
|
|
428
|
+
1. \`[data-testid='feature-name']\` - Best, explicit test hooks
|
|
429
|
+
2. \`[role='main']\`, \`[role='navigation']\` - Semantic roles
|
|
430
|
+
3. \`.feature-specific-class\` - Component-specific classes
|
|
431
|
+
4. \`h1\`, \`.page-title\` - Unique page identifiers
|
|
432
|
+
|
|
433
|
+
**How to find selectors:**
|
|
434
|
+
\`\`\`bash
|
|
435
|
+
# Search for data-testid attributes
|
|
436
|
+
grep -r "data-testid" src/ --include="*.tsx"
|
|
437
|
+
|
|
438
|
+
# Search for unique classNames in a component
|
|
439
|
+
grep -r "className=" src/components/Dashboard.tsx
|
|
440
|
+
|
|
441
|
+
# Look for page-specific elements
|
|
442
|
+
grep -r "<h1\|<header\|role=" src/pages/
|
|
443
|
+
\`\`\`
|
|
444
|
+
|
|
445
|
+
**Example - BAD vs GOOD:**
|
|
446
|
+
\`\`\`yaml
|
|
447
|
+
# BAD - too generic, every page has #root
|
|
448
|
+
- action: wait_for
|
|
449
|
+
selector: "#root"
|
|
450
|
+
|
|
451
|
+
# GOOD - specific to this feature
|
|
452
|
+
- action: wait_for
|
|
453
|
+
selector: "[data-testid='dashboard-content']"
|
|
454
|
+
# or
|
|
455
|
+
- action: wait_for
|
|
456
|
+
selector: ".dashboard-stats-grid"
|
|
457
|
+
# or
|
|
458
|
+
- action: wait_for
|
|
459
|
+
selector: "h1:has-text('Dashboard')"
|
|
460
|
+
\`\`\`
|
|
461
|
+
|
|
462
|
+
### Step 3: Add Interactive Flows
|
|
463
|
+
|
|
464
|
+
Don't just screenshot static pages. Verify that interactions work:
|
|
465
|
+
|
|
466
|
+
**Look for interactive elements:**
|
|
467
|
+
\`\`\`bash
|
|
468
|
+
# Find buttons and clickable elements
|
|
469
|
+
grep -r "onClick\|button\|Button" src/ --include="*.tsx"
|
|
470
|
+
|
|
471
|
+
# Find modals and dialogs
|
|
472
|
+
grep -r "Modal\|Dialog\|Drawer" src/ --include="*.tsx"
|
|
473
|
+
|
|
474
|
+
# Find forms
|
|
475
|
+
grep -r "<form\|onSubmit\|handleSubmit" src/ --include="*.tsx"
|
|
476
|
+
|
|
477
|
+
# Find toggles and switches
|
|
478
|
+
grep -r "toggle\|Switch\|theme" src/ --include="*.tsx"
|
|
479
|
+
\`\`\`
|
|
480
|
+
|
|
481
|
+
**Example interactive flows:**
|
|
482
|
+
\`\`\`yaml
|
|
483
|
+
# Theme toggle
|
|
484
|
+
- name: "Theme toggle works"
|
|
485
|
+
steps:
|
|
486
|
+
- action: navigate
|
|
487
|
+
url: "/"
|
|
488
|
+
- action: click
|
|
489
|
+
selector: "[data-testid='theme-toggle']"
|
|
490
|
+
- action: screenshot
|
|
491
|
+
name: "dark-mode"
|
|
492
|
+
- action: click
|
|
493
|
+
selector: "[data-testid='theme-toggle']"
|
|
494
|
+
- action: screenshot
|
|
495
|
+
name: "light-mode"
|
|
496
|
+
|
|
497
|
+
# Modal open/close
|
|
498
|
+
- name: "Settings modal opens"
|
|
499
|
+
steps:
|
|
500
|
+
- action: navigate
|
|
501
|
+
url: "/dashboard"
|
|
502
|
+
- action: click
|
|
503
|
+
selector: "[aria-label='Settings']"
|
|
504
|
+
- action: wait_for
|
|
505
|
+
selector: "[role='dialog']"
|
|
506
|
+
- action: screenshot
|
|
507
|
+
name: "settings-modal"
|
|
508
|
+
|
|
509
|
+
# Form submission
|
|
510
|
+
- name: "Contact form submits"
|
|
511
|
+
steps:
|
|
512
|
+
- action: navigate
|
|
513
|
+
url: "/contact"
|
|
514
|
+
- action: type
|
|
515
|
+
selector: "input[name='email']"
|
|
516
|
+
value: "test@example.com"
|
|
517
|
+
- action: click
|
|
518
|
+
selector: "button[type='submit']"
|
|
519
|
+
- action: wait_for
|
|
520
|
+
selector: ".success-message"
|
|
521
|
+
\`\`\`
|
|
522
|
+
|
|
523
|
+
### Step 4: Handle Data-Dependent Pages
|
|
524
|
+
|
|
525
|
+
**How to identify pages that need fixtures:**
|
|
526
|
+
\`\`\`bash
|
|
527
|
+
# Find components that fetch data
|
|
528
|
+
grep -r "useQuery\|useSWR\|fetch(\|axios\|useEffect.*fetch" src/ --include="*.tsx"
|
|
529
|
+
|
|
530
|
+
# Find API route parameters (these pages need data)
|
|
531
|
+
grep -r "useParams\|router.query\|\[.*\]" src/pages/ src/app/ --include="*.tsx"
|
|
532
|
+
\`\`\`
|
|
533
|
+
|
|
534
|
+
If a page has \`:id\`, \`:slug\`, or fetches from \`/api/*\`, it needs fixtures.
|
|
535
|
+
|
|
536
|
+
**Option A: Pull from staging (recommended for large/dynamic data)**
|
|
537
|
+
\`\`\`yaml
|
|
538
|
+
fixtures:
|
|
539
|
+
# Pull real data from staging API
|
|
540
|
+
- pattern: "/api/pr/*"
|
|
541
|
+
source: "https://staging.example.com/api/pr/sample"
|
|
542
|
+
headers:
|
|
543
|
+
Authorization: "Bearer $STAGING_TOKEN"
|
|
544
|
+
|
|
545
|
+
# Or from S3 bucket
|
|
546
|
+
- pattern: "/api/analytics/*"
|
|
547
|
+
source: "s3://my-fixtures-bucket/analytics-sample.json"
|
|
548
|
+
\`\`\`
|
|
549
|
+
|
|
550
|
+
**Option B: Commit small fixture files**
|
|
551
|
+
For small, stable data only:
|
|
552
|
+
\`\`\`bash
|
|
553
|
+
mkdir -p fixtures
|
|
554
|
+
cat > fixtures/user.json << 'EOF'
|
|
555
|
+
{"id": 1, "name": "Test User", "email": "test@example.com"}
|
|
556
|
+
EOF
|
|
557
|
+
\`\`\`
|
|
558
|
+
|
|
559
|
+
\`\`\`yaml
|
|
560
|
+
fixtures:
|
|
561
|
+
- pattern: "/api/user"
|
|
562
|
+
source: "file://fixtures/user.json"
|
|
563
|
+
\`\`\`
|
|
564
|
+
|
|
565
|
+
**When to use each:**
|
|
566
|
+
| Data Type | Use |
|
|
567
|
+
|-----------|-----|
|
|
568
|
+
| User profiles, settings | Local file (small, stable) |
|
|
569
|
+
| PR data, analytics, lists | Staging API or S3 (large, dynamic) |
|
|
570
|
+
| Auth tokens, sessions | Passthrough or mock inline |
|
|
571
|
+
|
|
572
|
+
**Option B: Use demo/example routes**
|
|
573
|
+
\`\`\`bash
|
|
574
|
+
# Look for demo or example routes in the router
|
|
575
|
+
grep -r "demo\|example\|sample" src/ --include="*.tsx"
|
|
576
|
+
\`\`\`
|
|
577
|
+
|
|
578
|
+
**Option C: Use real test data**
|
|
579
|
+
If the app has seeded test data, use those identifiers:
|
|
580
|
+
\`\`\`yaml
|
|
581
|
+
- name: "PR review page loads"
|
|
582
|
+
steps:
|
|
583
|
+
- action: navigate
|
|
584
|
+
url: "/review/test-org/test-repo/1" # Known test PR
|
|
585
|
+
- action: wait_for
|
|
586
|
+
selector: "[data-testid='pr-diff']"
|
|
587
|
+
\`\`\`
|
|
588
|
+
|
|
589
|
+
### Step 5: Add Backend API Flows (if applicable)
|
|
590
|
+
|
|
591
|
+
If the app has API endpoints, test them directly:
|
|
592
|
+
|
|
593
|
+
\`\`\`bash
|
|
594
|
+
# Find API routes
|
|
595
|
+
ls src/api/ app/api/ pages/api/ 2>/dev/null
|
|
596
|
+
grep -r "app.get\|app.post\|router.get" --include="*.ts"
|
|
597
|
+
\`\`\`
|
|
598
|
+
|
|
599
|
+
**Example API flows:**
|
|
600
|
+
\`\`\`yaml
|
|
601
|
+
flows:
|
|
602
|
+
- name: "API health check"
|
|
603
|
+
description: "Verify API server responds"
|
|
604
|
+
trigger: always
|
|
605
|
+
steps:
|
|
606
|
+
- action: http_request
|
|
607
|
+
method: GET
|
|
608
|
+
url: "http://localhost:3001/health"
|
|
609
|
+
- action: assert_status
|
|
610
|
+
status: 200
|
|
611
|
+
|
|
612
|
+
- name: "API returns valid data"
|
|
613
|
+
trigger: on_change
|
|
614
|
+
watch_patterns:
|
|
615
|
+
- "src/api/**"
|
|
616
|
+
steps:
|
|
617
|
+
- action: http_request
|
|
618
|
+
method: GET
|
|
619
|
+
url: "http://localhost:3001/api/users"
|
|
620
|
+
- action: assert_status
|
|
621
|
+
status: 200
|
|
622
|
+
\`\`\`
|
|
623
|
+
|
|
624
|
+
### Step 6: Verify Your Flows
|
|
625
|
+
|
|
626
|
+
After adding flows, validate the config:
|
|
627
|
+
\`\`\`bash
|
|
628
|
+
# Check YAML syntax
|
|
629
|
+
npx @haystackeditor/cli validate
|
|
630
|
+
|
|
631
|
+
# Or manually check
|
|
632
|
+
cat .haystack.yml | head -50
|
|
633
|
+
\`\`\`
|
|
634
|
+
|
|
635
|
+
### ✅ Final Checklist (ALL required):
|
|
636
|
+
|
|
637
|
+
Before you finish, verify:
|
|
638
|
+
|
|
639
|
+
1. [ ] **Page flows**: Every route has a flow
|
|
640
|
+
2. [ ] **Specific selectors**: All use \`[data-testid='x']\` or \`.class-name\`, NOT \`#root\`/\`div\`/\`h1\`
|
|
641
|
+
3. [ ] **Interactive flows**: At least 3 flows with \`click\` or \`type\` actions
|
|
642
|
+
4. [ ] **Fixtures configured**: Data-dependent pages have fixtures (staging URL preferred, or local JSON)
|
|
643
|
+
5. [ ] **Backend API flows**: API endpoints have \`http_request\` flows (if app has backend)
|
|
644
|
+
6. [ ] **Watch patterns**: Each \`watch_patterns\` matches component file paths
|
|
645
|
+
|
|
646
|
+
⚠️ **If you have 0 interactive flows or 0 fixtures for data pages, you are not done.**
|
|
240
647
|
`;
|
|
241
648
|
export async function createSkillFile() {
|
|
242
649
|
const skillDir = path.join(process.cwd(), '.agents', 'skills');
|
|
@@ -247,3 +654,16 @@ export async function createSkillFile() {
|
|
|
247
654
|
await fs.writeFile(skillPath, SKILL_CONTENT, 'utf-8');
|
|
248
655
|
return skillPath;
|
|
249
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* Create the .claude/commands/haystack.md file for Claude Code slash command
|
|
659
|
+
* Users can invoke with /haystack to start the setup wizard
|
|
660
|
+
*/
|
|
661
|
+
export async function createClaudeCommand() {
|
|
662
|
+
const commandDir = path.join(process.cwd(), '.claude', 'commands');
|
|
663
|
+
const commandPath = path.join(commandDir, 'haystack.md');
|
|
664
|
+
// Create directory if needed
|
|
665
|
+
await fs.mkdir(commandDir, { recursive: true });
|
|
666
|
+
// Write command file
|
|
667
|
+
await fs.writeFile(commandPath, CLAUDE_COMMAND_CONTENT, 'utf-8');
|
|
668
|
+
return commandPath;
|
|
669
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haystackeditor/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Set up Haystack verification for your project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"haystack": "./dist/index.js"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsc --watch",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
9
15
|
"keywords": [
|
|
10
16
|
"haystack",
|
|
11
17
|
"verification",
|
|
@@ -42,10 +48,5 @@
|
|
|
42
48
|
],
|
|
43
49
|
"engines": {
|
|
44
50
|
"node": ">=18"
|
|
45
|
-
},
|
|
46
|
-
"scripts": {
|
|
47
|
-
"build": "tsc",
|
|
48
|
-
"dev": "tsc --watch",
|
|
49
|
-
"start": "node dist/index.js"
|
|
50
51
|
}
|
|
51
|
-
}
|
|
52
|
+
}
|