@haystackeditor/cli 0.7.2 → 0.8.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.
@@ -4,7 +4,7 @@
4
4
  * Creates .haystack.json with auto-detected settings.
5
5
  */
6
6
  interface InitOptions {
7
- yes?: boolean;
7
+ force?: boolean;
8
8
  }
9
9
  export declare function initCommand(options: InitOptions): Promise<void>;
10
10
  export {};
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Creates .haystack.json with auto-detected settings.
5
5
  */
6
- import inquirer from 'inquirer';
7
6
  import chalk from 'chalk';
8
7
  import * as path from 'path';
9
8
  import { detectProject } from '../utils/detect.js';
@@ -11,27 +10,11 @@ import { saveConfig, configExists } from '../utils/config.js';
11
10
  import { createSkillFile, createClaudeCommand } from '../utils/skill.js';
12
11
  import { validateConfigSecurity, formatSecurityReport } from '../utils/secrets.js';
13
12
  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
- }
20
- console.log(chalk.cyan('\n🌾 Haystack Setup Wizard\n'));
13
+ console.log(chalk.cyan('\n🌾 Haystack Setup\n'));
21
14
  // Check if config already exists
22
- if (await configExists()) {
23
- const { overwrite } = await inquirer.prompt([
24
- {
25
- type: 'confirm',
26
- name: 'overwrite',
27
- message: '.haystack.json already exists. Overwrite?',
28
- default: false,
29
- },
30
- ]);
31
- if (!overwrite) {
32
- console.log(chalk.yellow('Setup cancelled.'));
33
- return;
34
- }
15
+ if (await configExists() && !options.force) {
16
+ console.log(chalk.yellow('.haystack.json already exists. Use --force to overwrite.\n'));
17
+ return;
35
18
  }
36
19
  // Detect project
37
20
  console.log(chalk.dim('Detecting project configuration...\n'));
@@ -45,107 +28,31 @@ export async function initCommand(options) {
45
28
  if (detected.isMonorepo) {
46
29
  console.log(` Monorepo: ${chalk.bold(detected.monorepoTool || 'yes')}`);
47
30
  if (detected.services?.length) {
48
- console.log(` Services: ${chalk.bold(detected.services.map((s) => s.name).join(', '))}`);
31
+ console.log(` Services: ${chalk.bold(detected.services.length)} packages detected`);
49
32
  }
50
33
  }
51
34
  console.log('');
52
- // If --yes flag, use all defaults
53
- if (options.yes) {
54
- const config = buildConfigFromDetection(detected);
55
- const configPath = await saveConfig(config);
56
- console.log(chalk.green(`āœ“ Created ${configPath}`));
57
- // Create skill file for agent discovery
58
- const skillPath = await createSkillFile();
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`));
63
- // Security validation
64
- await runSecurityCheck(configPath);
65
- // Print the prompt for the AI agent to follow
66
- console.log(chalk.cyan('━'.repeat(70)));
67
- console.log(chalk.cyan.bold('\nšŸ“‹ NEXT STEP: Add verification flows\n'));
68
- console.log(chalk.white('The config has dev server settings. Now add flows to tell Haystack'));
69
- console.log(chalk.white('what to verify when PRs are opened.\n'));
70
- console.log(chalk.yellow.bold('Give this prompt to your AI coding agent:\n'));
71
- console.log(chalk.cyan('━'.repeat(70)));
72
- console.log(`
73
- ${chalk.white('Add Haystack verification flows to .haystack.json')}
74
-
75
- ${chalk.bold('STEP 1: Understand the app')}
76
- - Read src/App.tsx and the main components
77
- - Write 2 sentences: What does this app do? What's the core feature?
78
-
79
- ${chalk.bold('STEP 2: Identify what PRs would touch')}
80
- - List the 5 most important UI areas that PRs change
81
- - These need the most thorough flows
82
-
83
- ${chalk.bold('STEP 3: Add flows')}
84
- - Read .agents/skills/haystack.md for the flow format
85
- - Add flows for the 5 core areas first, then secondary pages
86
-
87
- ${chalk.bold('STEP 4: Validate')}
88
- - Confirm the core feature from Step 1 has flows
89
- - Verify selectors are specific (not generic like "#root")
90
- `);
91
- console.log(chalk.cyan('━'.repeat(70)));
92
- return;
93
- }
94
- // Interactive prompts
95
- const answers = await inquirer.prompt([
96
- {
97
- type: 'input',
98
- name: 'name',
99
- message: 'Project name:',
100
- default: path.basename(process.cwd()),
101
- },
102
- {
103
- type: 'confirm',
104
- name: 'isMonorepo',
105
- message: 'Is this a monorepo with multiple services?',
106
- default: detected.isMonorepo,
107
- },
108
- ]);
109
- let config;
110
- if (answers.isMonorepo) {
111
- config = await configureMonorepo(detected, answers.name);
112
- }
113
- else {
114
- config = await configureSingleService(detected, answers.name);
115
- }
116
- // Verification commands
117
- const { commands } = await inquirer.prompt([
118
- {
119
- type: 'input',
120
- name: 'commands',
121
- message: 'Verification commands (comma-separated):',
122
- default: `${detected.packageManager} build, ${detected.packageManager} lint, ${detected.packageManager} tsc --noEmit`,
123
- },
124
- ]);
125
- config.verification = {
126
- commands: commands
127
- .split(',')
128
- .map((cmd) => cmd.trim())
129
- .filter(Boolean)
130
- .map((cmd) => {
131
- const name = cmd.split(' ').pop() || cmd;
132
- return { name, run: cmd };
133
- }),
134
- };
135
- // Save config
35
+ // Build config from detection (no interactive prompts)
36
+ const config = buildConfigFromDetection(detected);
136
37
  const configPath = await saveConfig(config);
137
38
  console.log(chalk.green(`āœ“ Created ${configPath}`));
138
39
  // Create skill file for agent discovery
139
40
  const skillPath = await createSkillFile();
140
- console.log(chalk.green(`āœ“ Created ${skillPath}\n`));
141
- // Show the generated config
142
- console.log(chalk.dim('Generated configuration:'));
143
- console.log(chalk.dim('─'.repeat(40)));
144
- console.log(JSON.stringify(config, null, 2));
145
- console.log(chalk.dim('─'.repeat(40)));
41
+ console.log(chalk.green(`āœ“ Created ${skillPath}`));
42
+ // Create Claude Code slash command
43
+ const commandPath = await createClaudeCommand();
44
+ console.log(chalk.green(`āœ“ Created ${commandPath}`));
146
45
  // Security validation
147
46
  await runSecurityCheck(configPath);
148
- printNextSteps();
47
+ // Print next steps - direct to skill
48
+ console.log(chalk.cyan('\n━'.repeat(60)));
49
+ console.log(chalk.cyan.bold('\nšŸ“‹ NEXT: Add verification flows\n'));
50
+ console.log(chalk.white('Base config created. Now an AI agent needs to add flows.\n'));
51
+ console.log(chalk.bold('Option 1: Claude Code'));
52
+ console.log(chalk.dim(' Run /setup-haystack in Claude Code\n'));
53
+ console.log(chalk.bold('Option 2: Other AI agents'));
54
+ console.log(chalk.dim(' Give your agent: "Read .agents/skills/setup-haystack.md and follow it"\n'));
55
+ console.log(chalk.cyan('━'.repeat(60) + '\n'));
149
56
  }
150
57
  /**
151
58
  * Run security check on config file
@@ -161,125 +68,6 @@ async function runSecurityCheck(configPath) {
161
68
  console.log(chalk.dim('\n' + formatSecurityReport(findings)));
162
69
  }
163
70
  }
164
- async function configureSingleService(detected, name) {
165
- const answers = await inquirer.prompt([
166
- {
167
- type: 'input',
168
- name: 'command',
169
- message: 'Dev server command:',
170
- default: detected.suggestedDevCommand,
171
- },
172
- {
173
- type: 'number',
174
- name: 'port',
175
- message: 'Dev server port:',
176
- default: detected.suggestedPort,
177
- },
178
- {
179
- type: 'input',
180
- name: 'readyPattern',
181
- message: 'Ready pattern (stdout text when server is ready):',
182
- default: detected.suggestedReadyPattern,
183
- },
184
- {
185
- type: 'input',
186
- name: 'authBypass',
187
- message: 'Auth bypass env var (leave blank if no auth):',
188
- default: detected.suggestedAuthBypass,
189
- },
190
- ]);
191
- const env = {};
192
- if (answers.authBypass) {
193
- const [key, value] = answers.authBypass.split('=');
194
- env[key] = value || 'true';
195
- }
196
- return {
197
- version: '1',
198
- name,
199
- dev_server: {
200
- command: answers.command,
201
- port: answers.port,
202
- ready_pattern: answers.readyPattern,
203
- ...(Object.keys(env).length > 0 ? { env } : {}),
204
- },
205
- };
206
- }
207
- async function configureMonorepo(detected, name) {
208
- // Get list of services
209
- const detectedServices = detected.services || [];
210
- const serviceNames = detectedServices.map((s) => s.name);
211
- const { selectedServices } = await inquirer.prompt([
212
- {
213
- type: 'checkbox',
214
- name: 'selectedServices',
215
- message: 'Which services should the sandbox run?',
216
- choices: serviceNames.length > 0 ? serviceNames : ['frontend', 'api'],
217
- default: serviceNames,
218
- },
219
- ]);
220
- // Ask about auth bypass
221
- const { authBypass } = await inquirer.prompt([
222
- {
223
- type: 'input',
224
- name: 'authBypass',
225
- message: 'Auth bypass env var (applied to all services):',
226
- default: detected.suggestedAuthBypass,
227
- },
228
- ]);
229
- const services = {};
230
- for (const serviceName of selectedServices) {
231
- const detectedService = detectedServices.find((s) => s.name === serviceName);
232
- console.log(chalk.dim(`\nConfiguring ${serviceName}...`));
233
- const answers = await inquirer.prompt([
234
- {
235
- type: 'input',
236
- name: 'root',
237
- message: ` Directory:`,
238
- default: detectedService?.root || './',
239
- },
240
- {
241
- type: 'input',
242
- name: 'command',
243
- message: ` Command:`,
244
- default: detectedService?.suggestedCommand || `${detected.packageManager} dev`,
245
- },
246
- {
247
- type: 'list',
248
- name: 'type',
249
- message: ` Type:`,
250
- choices: ['server', 'batch'],
251
- default: detectedService?.type || 'server',
252
- },
253
- {
254
- type: 'number',
255
- name: 'port',
256
- message: ` Port:`,
257
- default: detectedService?.suggestedPort || 3000,
258
- when: (ans) => ans.type === 'server',
259
- },
260
- ]);
261
- const env = {};
262
- if (authBypass) {
263
- const [key, value] = authBypass.split('=');
264
- env[key] = value || 'true';
265
- }
266
- // Check if this service has detected dependencies
267
- const dependsOn = detectedService?.suggestedDependsOn?.filter((dep) => selectedServices.includes(dep));
268
- services[serviceName] = {
269
- ...(answers.root !== './' ? { root: answers.root } : {}),
270
- command: answers.command,
271
- ...(answers.type === 'server' ? { port: answers.port } : { type: 'batch' }),
272
- ready_pattern: 'ready|started|listening|Local:',
273
- ...(Object.keys(env).length > 0 ? { env } : {}),
274
- ...(dependsOn?.length ? { depends_on: dependsOn } : {}),
275
- };
276
- }
277
- return {
278
- version: '1',
279
- name,
280
- services,
281
- };
282
- }
283
71
  function buildConfigFromDetection(detected) {
284
72
  const env = {};
285
73
  if (detected.suggestedAuthBypass) {
@@ -328,10 +116,3 @@ function buildConfigFromDetection(detected) {
328
116
  },
329
117
  };
330
118
  }
331
- function printNextSteps() {
332
- console.log(chalk.cyan('Next steps:'));
333
- console.log(` 1. Review .haystack.json and adjust as needed`);
334
- console.log(` 2. Add auth bypass env var if your app requires login`);
335
- console.log(` 3. Add flows to describe key user journeys to verify`);
336
- console.log(` 4. Commit: ${chalk.green('git add .haystack.json .agents/ && git commit -m "Add Haystack"')}\n`);
337
- }
@@ -12,6 +12,21 @@ export declare function setSecret(key: string, value: string, options: {
12
12
  scope?: string;
13
13
  scopeId?: string;
14
14
  }): Promise<void>;
15
+ /**
16
+ * Get a secret value (for programmatic use)
17
+ * Returns the value to stdout for piping to other commands.
18
+ * Use --quiet to suppress all output except the value.
19
+ */
20
+ export declare function getSecret(key: string, options: {
21
+ quiet?: boolean;
22
+ }): Promise<void>;
23
+ /**
24
+ * Get multiple secrets needed by a repo's .haystack.json
25
+ * Returns JSON object with key-value pairs.
26
+ */
27
+ export declare function getSecretsForRepo(keys: string[], options: {
28
+ json?: boolean;
29
+ }): Promise<void>;
15
30
  /**
16
31
  * Delete a secret
17
32
  */
@@ -101,6 +101,89 @@ export async function setSecret(key, value, options) {
101
101
  process.exit(1);
102
102
  }
103
103
  }
104
+ /**
105
+ * Get a secret value (for programmatic use)
106
+ * Returns the value to stdout for piping to other commands.
107
+ * Use --quiet to suppress all output except the value.
108
+ */
109
+ export async function getSecret(key, options) {
110
+ const token = await requireAuth();
111
+ if (!key) {
112
+ console.error(chalk.red('\nUsage: haystack secrets get KEY\n'));
113
+ process.exit(1);
114
+ }
115
+ try {
116
+ const response = await apiRequest('GET', `/${encodeURIComponent(key)}`, token);
117
+ if (!response.ok) {
118
+ if (response.status === 401) {
119
+ if (!options.quiet) {
120
+ console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
121
+ }
122
+ process.exit(1);
123
+ }
124
+ if (response.status === 404) {
125
+ if (!options.quiet) {
126
+ console.error(chalk.yellow(`\nSecret ${key} not found.\n`));
127
+ }
128
+ process.exit(1);
129
+ }
130
+ throw new Error(`Failed to get secret: ${response.status}`);
131
+ }
132
+ const data = await response.json();
133
+ // Output just the value (no newline if quiet, for piping)
134
+ if (options.quiet) {
135
+ process.stdout.write(data.value);
136
+ }
137
+ else {
138
+ console.log(data.value);
139
+ }
140
+ }
141
+ catch (error) {
142
+ if (!options.quiet) {
143
+ console.error(chalk.red(`\nError: ${error.message}\n`));
144
+ }
145
+ process.exit(1);
146
+ }
147
+ }
148
+ /**
149
+ * Get multiple secrets needed by a repo's .haystack.json
150
+ * Returns JSON object with key-value pairs.
151
+ */
152
+ export async function getSecretsForRepo(keys, options) {
153
+ const token = await requireAuth();
154
+ if (keys.length === 0) {
155
+ console.error(chalk.red('\nUsage: haystack secrets get-for-repo KEY1 KEY2 ...\n'));
156
+ process.exit(1);
157
+ }
158
+ try {
159
+ const response = await apiRequest('POST', '/batch', token, { keys });
160
+ if (!response.ok) {
161
+ if (response.status === 401) {
162
+ console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
163
+ process.exit(1);
164
+ }
165
+ throw new Error(`Failed to get secrets: ${response.status}`);
166
+ }
167
+ const data = await response.json();
168
+ if (options.json) {
169
+ console.log(JSON.stringify(data.secrets));
170
+ }
171
+ else {
172
+ if (data.missing.length > 0) {
173
+ console.log(chalk.yellow(`\nMissing secrets: ${data.missing.join(', ')}\n`));
174
+ }
175
+ console.log(chalk.bold('\nSecrets loaded:\n'));
176
+ for (const [k, v] of Object.entries(data.secrets)) {
177
+ console.log(` ${chalk.cyan(k)}: ${chalk.dim('***' + v.slice(-4))}`);
178
+ }
179
+ console.log();
180
+ }
181
+ }
182
+ catch (error) {
183
+ console.error(chalk.red(`\nError: ${error.message}\n`));
184
+ process.exit(1);
185
+ }
186
+ }
104
187
  /**
105
188
  * Delete a secret
106
189
  */
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import { Command } from 'commander';
15
15
  import { statusCommand } from './commands/status.js';
16
16
  import { initCommand } from './commands/init.js';
17
17
  import { loginCommand, logoutCommand } from './commands/login.js';
18
- import { listSecrets, setSecret, deleteSecret } from './commands/secrets.js';
18
+ import { listSecrets, setSecret, getSecret, getSecretsForRepo, deleteSecret } from './commands/secrets.js';
19
19
  import { handleSandbox } from './commands/config.js';
20
20
  const program = new Command();
21
21
  program
@@ -25,14 +25,15 @@ program
25
25
  program
26
26
  .command('init')
27
27
  .description('Create .haystack.json configuration')
28
- .option('-y, --yes', 'Use auto-detected defaults without prompting')
28
+ .option('-f, --force', 'Overwrite existing .haystack.json')
29
29
  .addHelpText('after', `
30
- This creates a .haystack.json file that configures:
31
- • How to start your dev server
32
- • Verification commands (build, lint, typecheck)
30
+ This creates a .haystack.json file with auto-detected settings:
31
+ • Dev server command and port
32
+ • Services (for monorepos)
33
33
  • Auth bypass for sandbox environments
34
34
 
35
- Once set up, AI agents can automatically spin up and test your app.
35
+ After running, use /setup-haystack in Claude Code (or give your AI agent
36
+ .agents/skills/setup-haystack.md) to add verification flows.
36
37
  `)
37
38
  .action(initCommand);
38
39
  program
@@ -63,6 +64,20 @@ secrets
63
64
  .action((key, value, options) => {
64
65
  setSecret(key, value, options);
65
66
  });
67
+ secrets
68
+ .command('get <key>')
69
+ .description('Get a secret value')
70
+ .option('-q, --quiet', 'Output only the value (for piping)')
71
+ .action((key, options) => {
72
+ getSecret(key, options);
73
+ });
74
+ secrets
75
+ .command('get-for-repo <keys...>')
76
+ .description('Get multiple secrets needed by a repo')
77
+ .option('--json', 'Output as JSON')
78
+ .action((keys, options) => {
79
+ getSecretsForRepo(keys, options);
80
+ });
66
81
  secrets
67
82
  .command('delete <key>')
68
83
  .description('Delete a secret')
package/dist/types.d.ts CHANGED
@@ -34,19 +34,43 @@ export interface HaystackConfig {
34
34
  */
35
35
  network?: NetworkConfig;
36
36
  /**
37
- * Secrets for fixture fetching and other operations.
38
- * These are injected as environment variables in the sandbox.
39
- * Supports tokens for staging APIs, S3 credentials, etc.
37
+ * Secrets required by this project.
38
+ * Values are stored securely on the Haystack platform via `haystack secrets set`.
39
+ * At runtime, secrets are fetched and injected as environment variables.
40
40
  *
41
- * āš ļø SECURITY: Never commit real secrets to git!
42
- * Use environment variable references: $ENV_VAR or ${ENV_VAR}
41
+ * This field declares WHAT secrets are needed, not their values.
42
+ * Use `haystack secrets set KEY VALUE` to store the actual values.
43
43
  *
44
44
  * @example
45
45
  * secrets:
46
- * STAGING_TOKEN: "$STAGING_API_TOKEN" # Read from local env
47
- * AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}"
46
+ * OPENAI_API_KEY:
47
+ * description: "OpenAI API key for LLM calls"
48
+ * required: true
49
+ * STAGING_TOKEN:
50
+ * description: "Token for staging API access"
51
+ * required: false
48
52
  */
49
- secrets?: Record<string, string>;
53
+ secrets?: Record<string, SecretDeclaration>;
54
+ }
55
+ /**
56
+ * Secret declaration - describes what a secret is for, not its value
57
+ */
58
+ export interface SecretDeclaration {
59
+ /** Human-readable description of what this secret is used for */
60
+ description?: string;
61
+ /**
62
+ * Whether this secret is required for verification to run.
63
+ * If true and the secret is not set, verification will fail with an error.
64
+ * If false, verification will continue with a warning.
65
+ * @default false
66
+ */
67
+ required?: boolean;
68
+ /**
69
+ * Services that use this secret.
70
+ * If specified, the secret is only injected into these services' environments.
71
+ * If omitted, the secret is available to all services.
72
+ */
73
+ services?: string[];
50
74
  }
51
75
  /**
52
76
  * Dev server configuration (simple mode)
@@ -104,17 +104,32 @@ grep '"name":' .haystack.json
104
104
  }
105
105
  \`\`\`
106
106
 
107
- ### Finding Golden Inputs
107
+ ---
108
108
 
109
- For analysis/processing pipelines:
110
- - Pick a **small, stable PR** that won't change
111
- - Document what the expected output should be
112
- - Add it as a comment in the flow description
109
+ ## STOP - Confirm Golden URLs and Golden Data
113
110
 
114
- \`\`\`bash
115
- # Find a good golden PR (small, merged, stable)
116
- gh pr list --state merged --limit 10 --json number,title,changedFiles | jq '.[] | select(.changedFiles < 5)'
117
- \`\`\`
111
+ **If you identified backend services that need verification, you MUST ask the user with a proposed example:**
112
+
113
+ First, explore the codebase to find:
114
+ - Example inputs used in tests, scripts, or documentation
115
+ - Default/demo values in config files or environment variables
116
+ - IDs or URLs referenced in comments or READMEs
117
+
118
+ Then propose a specific example:
119
+
120
+ > I found these backend services that need verification:
121
+ > - **[service name]**: [what it does]
122
+ >
123
+ > For backend verification, I need **golden test data** - stable inputs that produce predictable output.
124
+ >
125
+ > **Based on what I found in the codebase, here's my suggestion:**
126
+ > - **Golden URL/Input**: \`[specific example you found, e.g., a test ID, demo endpoint, or sample file]\`
127
+ > - **Command**: \`[the command to run with this input]\`
128
+ > - **Expected Output**: \`[what success looks like based on the code]\`
129
+ >
130
+ > Does this look right? Or should I use different test data?
131
+
132
+ **Wait for the user to confirm or provide alternatives before adding backend flows.**
118
133
 
119
134
  ---
120
135
 
@@ -596,6 +611,49 @@ The verification Planner needs to find UI elements by selectors. Generic selecto
596
611
 
597
612
  ---
598
613
 
614
+ ## āš ļø CRITICAL RULE: Every Button MUST Have \`data-testid\` and \`aria-label\`
615
+
616
+ **This is non-negotiable.** Every \`<button>\` element in the codebase MUST have:
617
+ 1. \`data-testid="descriptive-name"\` - For automated testing/verification
618
+ 2. \`aria-label="Action description"\` - For accessibility AND verification
619
+
620
+ **Why?** The verification system clicks buttons by their \`data-testid\` or \`aria-label\`. Without these, buttons are invisible to automation. We learned this the hard way - a theme toggle button without these attributes broke our entire verification pipeline.
621
+
622
+ \`\`\`tsx
623
+ // āŒ WRONG - No identifiers, verification cannot click this
624
+ <button onClick={toggleTheme}>
625
+ <SunIcon />
626
+ </button>
627
+
628
+ // āœ… CORRECT - Both data-testid AND aria-label
629
+ <button
630
+ onClick={toggleTheme}
631
+ data-testid="theme-toggle"
632
+ aria-label="Toggle dark mode"
633
+ >
634
+ <SunIcon />
635
+ </button>
636
+ \`\`\`
637
+
638
+ **For button wrapper components**, pass through these props:
639
+ \`\`\`tsx
640
+ interface ButtonProps {
641
+ 'data-testid'?: string;
642
+ 'aria-label'?: string;
643
+ // ...other props
644
+ }
645
+
646
+ function IconButton({ 'data-testid': testId, 'aria-label': ariaLabel, ...props }: ButtonProps) {
647
+ return (
648
+ <button data-testid={testId} aria-label={ariaLabel} {...props}>
649
+ {props.children}
650
+ </button>
651
+ );
652
+ }
653
+ \`\`\`
654
+
655
+ ---
656
+
599
657
  ## What to Add
600
658
 
601
659
  ### 1. \`aria-label\` on Interactive Elements
@@ -727,13 +785,21 @@ Forms should have proper labels and descriptions:
727
785
 
728
786
  ## Step 1: Scan for Missing Identifiers
729
787
 
788
+ **Start with buttons - these are the most critical:**
789
+
730
790
  \`\`\`bash
731
- # Find buttons without aria-label
732
- grep -rn "<button" src/ --include="*.tsx" | grep -v "aria-label" | head -20
791
+ # CRITICAL: Find ALL buttons missing data-testid (fix ALL of these!)
792
+ grep -rn "<button" src/ --include="*.tsx" | grep -v "data-testid" | head -30
793
+
794
+ # CRITICAL: Find ALL buttons missing aria-label (fix ALL of these!)
795
+ grep -rn "<button" src/ --include="*.tsx" | grep -v "aria-label" | head -30
733
796
 
734
- # Find icon-only buttons (likely missing labels)
797
+ # Find icon-only buttons (MUST have aria-label since no visible text)
735
798
  grep -rn "<button.*Icon\\|<button.*>.*</.*Icon>" src/ --include="*.tsx" | head -20
736
799
 
800
+ # Find button wrapper components that need to pass through data-testid/aria-label
801
+ grep -rn "function.*Button\\|const.*Button.*=" src/ --include="*.tsx" | head -10
802
+
737
803
  # Find modals/dialogs without role
738
804
  grep -rn "modal\\|dialog\\|Modal\\|Dialog" src/ --include="*.tsx" | grep -v "role=" | head -20
739
805
 
@@ -744,6 +810,8 @@ grep -rn "<input\\|<select\\|<textarea" src/ --include="*.tsx" | grep -v "aria-l
744
810
  ls src/components/ src/pages/ 2>/dev/null
745
811
  \`\`\`
746
812
 
813
+ **Every button found without \`data-testid\` or \`aria-label\` MUST be fixed.**
814
+
747
815
  ---
748
816
 
749
817
  ## Step 2: Prioritize by Impact
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Set up Haystack verification for your project",
5
5
  "type": "module",
6
6
  "bin": {