@haystackeditor/cli 0.7.1 ā 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.
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +20 -239
- package/dist/commands/secrets.d.ts +15 -0
- package/dist/commands/secrets.js +83 -0
- package/dist/index.js +21 -6
- package/dist/types.d.ts +32 -8
- package/dist/utils/skill.js +86 -18
- package/package.json +1 -1
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
31
|
+
console.log(` Services: ${chalk.bold(detected.services.length)} packages detected`);
|
|
49
32
|
}
|
|
50
33
|
}
|
|
51
34
|
console.log('');
|
|
52
|
-
//
|
|
53
|
-
|
|
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}
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
console.log(chalk.
|
|
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
|
-
|
|
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
|
*/
|
package/dist/commands/secrets.js
CHANGED
|
@@ -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('-
|
|
28
|
+
.option('-f, --force', 'Overwrite existing .haystack.json')
|
|
29
29
|
.addHelpText('after', `
|
|
30
|
-
This creates a .haystack.json file
|
|
31
|
-
ā¢
|
|
32
|
-
ā¢
|
|
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
|
-
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
*
|
|
42
|
-
* Use
|
|
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
|
-
*
|
|
47
|
-
*
|
|
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,
|
|
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)
|
package/dist/utils/skill.js
CHANGED
|
@@ -85,36 +85,51 @@ grep '"name":' .haystack.json
|
|
|
85
85
|
|
|
86
86
|
\`\`\`json
|
|
87
87
|
{
|
|
88
|
-
"name": "
|
|
89
|
-
"description": "Run
|
|
88
|
+
"name": "Backend pipeline - golden input",
|
|
89
|
+
"description": "Run pipeline on known input to verify it works",
|
|
90
90
|
"trigger": "on_change",
|
|
91
|
-
"watch_patterns": ["
|
|
92
|
-
"service": "
|
|
91
|
+
"watch_patterns": ["packages/my-pipeline/src/**"],
|
|
92
|
+
"service": "my-pipeline",
|
|
93
93
|
"type": "backend",
|
|
94
94
|
"steps": [
|
|
95
95
|
{
|
|
96
96
|
"action": "run",
|
|
97
97
|
"command": "pnpm start",
|
|
98
|
-
"env": { "
|
|
98
|
+
"env": { "INPUT_ID": "known-good-input-123" },
|
|
99
99
|
"timeout": 120
|
|
100
100
|
},
|
|
101
101
|
{ "action": "assert_exit_code", "code": 0 },
|
|
102
|
-
{ "action": "assert_output_contains", "pattern": "
|
|
102
|
+
{ "action": "assert_output_contains", "pattern": "Processing complete" }
|
|
103
103
|
]
|
|
104
104
|
}
|
|
105
105
|
\`\`\`
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
---
|
|
108
108
|
|
|
109
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
732
|
-
grep -rn "<button" src/ --include="*.tsx" | grep -v "
|
|
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 (
|
|
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
|