@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 CHANGED
@@ -1,105 +1,81 @@
1
1
  # @haystackeditor/cli
2
2
 
3
- Unified CLI for Haystack verification, fixtures, and sandboxes.
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
- ## Installation
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 to create `.haystack.yml` configuration:
19
+ Interactive setup wizard:
20
20
 
21
21
  ```bash
22
- haystack init # Interactive wizard
23
- haystack init -y # Accept all defaults
22
+ npx @haystackeditor/cli init # Interactive wizard
23
+ npx @haystackeditor/cli init -y # Accept all defaults
24
24
  ```
25
25
 
26
- ### `haystack login`
26
+ ### `haystack status`
27
27
 
28
- Authenticate with Haystack via GitHub OAuth:
28
+ Check if your project is configured:
29
29
 
30
30
  ```bash
31
- haystack login # Opens browser for GitHub OAuth
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 secrets`
34
+ ### `haystack login`
37
35
 
38
- Manage secrets for fixtures and integrations:
36
+ Authenticate with GitHub (required for secrets management):
39
37
 
40
38
  ```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
39
+ npx @haystackeditor/cli login
44
40
  ```
45
41
 
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:
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
- haystack record https://api.example.com/data
54
- haystack record https://api.example.com/data --output fixtures/data.json
45
+ # Log out (removes stored credentials)
46
+ npx @haystackeditor/cli logout
55
47
  ```
56
48
 
57
- ### `haystack verify`
49
+ ### `haystack secrets`
58
50
 
59
- Run verification commands from `.haystack.yml`:
51
+ Manage secrets that will be injected into your sandbox environment:
60
52
 
61
53
  ```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
- ```
54
+ # List all secrets (keys only, values are never shown)
55
+ npx @haystackeditor/cli secrets list
66
56
 
67
- ### `haystack dev`
57
+ # Set a secret
58
+ npx @haystackeditor/cli secrets set OPENAI_API_KEY sk-xxx
68
59
 
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
60
+ # Delete a secret
61
+ npx @haystackeditor/cli secrets delete OPENAI_API_KEY
75
62
  ```
76
63
 
77
- ### `haystack sandbox`
64
+ Secrets are encrypted and stored securely. They're automatically injected as environment variables when the sandbox runs your app.
78
65
 
79
- Manage Haystack sandboxes:
66
+ **Scopes**: By default, secrets are user-scoped. You can also scope to an org or repo:
80
67
 
81
68
  ```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`
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
- 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
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
- Create `.haystack.yml` in your project root:
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
- ## 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 |
99
+ ### Customizing After Init
183
100
 
184
- ### Using Secrets in Fixtures
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
- ```yaml
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
- Set the secret:
109
+ ## How It Works
195
110
 
196
- ```bash
197
- haystack secrets set AWS_TOKEN "your-token-here"
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
 
@@ -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}\n`));
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
- printNextSteps();
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 # Set up .haystack.yml
10
- * npx @haystackeditor/cli status # Check configuration
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 # Set up .haystack.yml
10
- * npx @haystackeditor/cli status # Check configuration
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.2.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();
@@ -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>;
@@ -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
- Haystack provides automated PR verification. When a PR is opened, an AI agent spins up the app in a sandbox and verifies changes work correctly.
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.2.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
+ }