@haystackeditor/cli 0.7.2 ā 0.8.1
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 +59 -12
- package/dist/assets/hooks/agent-context/detect.ts +136 -0
- package/dist/assets/hooks/agent-context/format.ts +99 -0
- package/dist/assets/hooks/agent-context/index.ts +39 -0
- package/dist/assets/hooks/agent-context/parsers/claude.ts +253 -0
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +155 -0
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +174 -0
- package/dist/assets/hooks/agent-context/tsconfig.json +13 -0
- package/dist/assets/hooks/agent-context/types.ts +58 -0
- package/dist/assets/hooks/llm-rules-template.md +35 -0
- package/dist/assets/hooks/package.json +11 -0
- package/dist/assets/hooks/scripts/commit-msg.sh +4 -0
- package/dist/assets/hooks/scripts/post-commit.sh +4 -0
- package/dist/assets/hooks/scripts/pre-commit.sh +92 -0
- package/dist/assets/hooks/scripts/pre-push.sh +5 -0
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +528 -0
- package/dist/assets/hooks/truncation-checker/index.ts +595 -0
- package/dist/assets/hooks/truncation-checker/tsconfig.json +13 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.js +89 -0
- package/dist/commands/hooks.d.ts +17 -0
- package/dist/commands/hooks.js +269 -0
- 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/commands/skills.d.ts +8 -0
- package/dist/commands/skills.js +215 -0
- package/dist/index.js +107 -7
- package/dist/types.d.ts +32 -8
- package/dist/utils/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +226 -0
- package/dist/utils/skill.d.ts +1 -1
- package/dist/utils/skill.js +481 -13
- package/package.json +2 -2
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
|
*/
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills command - Registers the haystack-verify MCP server with coding CLIs
|
|
3
|
+
* Supports: Claude Code, Codex CLI, Cursor, and manual setup
|
|
4
|
+
*/
|
|
5
|
+
export declare function installSkills(options: {
|
|
6
|
+
cli?: string;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
export declare function listSkills(): Promise<void>;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills command - Registers the haystack-verify MCP server with coding CLIs
|
|
3
|
+
* Supports: Claude Code, Codex CLI, Cursor, and manual setup
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
const CLI_CONFIGS = {
|
|
11
|
+
claude: {
|
|
12
|
+
name: 'claude',
|
|
13
|
+
displayName: 'Claude Code',
|
|
14
|
+
checkCommand: 'which claude',
|
|
15
|
+
installCommand: 'claude mcp add haystack-verify -- npx @haystackeditor/verify',
|
|
16
|
+
},
|
|
17
|
+
codex: {
|
|
18
|
+
name: 'codex',
|
|
19
|
+
displayName: 'Codex CLI',
|
|
20
|
+
checkCommand: 'which codex',
|
|
21
|
+
installCommand: 'codex mcp add haystack-verify -- npx @haystackeditor/verify',
|
|
22
|
+
},
|
|
23
|
+
cursor: {
|
|
24
|
+
name: 'cursor',
|
|
25
|
+
displayName: 'Cursor',
|
|
26
|
+
checkCommand: '', // Cursor is always "available" - we check config file
|
|
27
|
+
installCommand: '', // We modify config directly
|
|
28
|
+
configPath: join(homedir(), '.cursor', 'mcp.json'),
|
|
29
|
+
},
|
|
30
|
+
manual: {
|
|
31
|
+
name: 'manual',
|
|
32
|
+
displayName: 'Manual Setup',
|
|
33
|
+
checkCommand: '',
|
|
34
|
+
installCommand: '',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
function detectAvailableCLIs() {
|
|
38
|
+
const available = [];
|
|
39
|
+
// Check Claude Code
|
|
40
|
+
try {
|
|
41
|
+
execSync('which claude', { stdio: 'ignore' });
|
|
42
|
+
available.push('claude');
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
// Check Codex CLI
|
|
46
|
+
try {
|
|
47
|
+
execSync('which codex', { stdio: 'ignore' });
|
|
48
|
+
available.push('codex');
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
// Check Cursor (config-based)
|
|
52
|
+
const cursorConfigDir = join(homedir(), '.cursor');
|
|
53
|
+
if (existsSync(cursorConfigDir)) {
|
|
54
|
+
available.push('cursor');
|
|
55
|
+
}
|
|
56
|
+
return available;
|
|
57
|
+
}
|
|
58
|
+
function installForClaude() {
|
|
59
|
+
const config = CLI_CONFIGS.claude;
|
|
60
|
+
console.log(chalk.gray(`Running: ${config.installCommand}\n`));
|
|
61
|
+
try {
|
|
62
|
+
execSync(config.installCommand, { stdio: 'inherit' });
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function installForCodex() {
|
|
70
|
+
const config = CLI_CONFIGS.codex;
|
|
71
|
+
console.log(chalk.gray(`Running: ${config.installCommand}\n`));
|
|
72
|
+
try {
|
|
73
|
+
execSync(config.installCommand, { stdio: 'inherit' });
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function installForCursor() {
|
|
81
|
+
const configPath = CLI_CONFIGS.cursor.configPath;
|
|
82
|
+
const configDir = join(homedir(), '.cursor');
|
|
83
|
+
console.log(chalk.gray(`Updating: ${configPath}\n`));
|
|
84
|
+
try {
|
|
85
|
+
// Ensure directory exists
|
|
86
|
+
if (!existsSync(configDir)) {
|
|
87
|
+
mkdirSync(configDir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
// Read or create config
|
|
90
|
+
let config = {};
|
|
91
|
+
if (existsSync(configPath)) {
|
|
92
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
93
|
+
config = JSON.parse(content);
|
|
94
|
+
}
|
|
95
|
+
// Add haystack-verify server
|
|
96
|
+
config.mcpServers = config.mcpServers || {};
|
|
97
|
+
config.mcpServers['haystack-verify'] = {
|
|
98
|
+
command: 'npx',
|
|
99
|
+
args: ['@haystackeditor/verify'],
|
|
100
|
+
};
|
|
101
|
+
// Write config
|
|
102
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error(chalk.red(`Failed to update Cursor config: ${error}`));
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function showManualInstructions() {
|
|
111
|
+
console.log(chalk.white('\nš Manual Setup Instructions\n'));
|
|
112
|
+
console.log(chalk.cyan('Claude Code:'));
|
|
113
|
+
console.log(chalk.gray(' claude mcp add haystack-verify -- npx @haystackeditor/verify\n'));
|
|
114
|
+
console.log(chalk.cyan('Codex CLI:'));
|
|
115
|
+
console.log(chalk.gray(' codex mcp add haystack-verify -- npx @haystackeditor/verify\n'));
|
|
116
|
+
console.log(chalk.cyan('Cursor:'));
|
|
117
|
+
console.log(chalk.gray(' Add to ~/.cursor/mcp.json:'));
|
|
118
|
+
console.log(chalk.gray(` {
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"haystack-verify": {
|
|
121
|
+
"command": "npx",
|
|
122
|
+
"args": ["@haystackeditor/verify"]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}\n`));
|
|
126
|
+
console.log(chalk.cyan('VS Code + Continue:'));
|
|
127
|
+
console.log(chalk.gray(' Add to .continue/config.json or settings\n'));
|
|
128
|
+
}
|
|
129
|
+
export async function installSkills(options) {
|
|
130
|
+
console.log(chalk.cyan('\nš¦ Installing Haystack skills...\n'));
|
|
131
|
+
// If CLI specified, use it directly
|
|
132
|
+
if (options.cli) {
|
|
133
|
+
const cli = options.cli.toLowerCase();
|
|
134
|
+
if (cli === 'manual') {
|
|
135
|
+
showManualInstructions();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!['claude', 'codex', 'cursor'].includes(cli)) {
|
|
139
|
+
console.log(chalk.red(`Unknown CLI: ${options.cli}`));
|
|
140
|
+
console.log(chalk.gray('Supported: claude, codex, cursor, manual'));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const success = await installForCLI(cli);
|
|
144
|
+
if (success) {
|
|
145
|
+
showSuccessMessage(cli);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Auto-detect available CLIs
|
|
150
|
+
const available = detectAvailableCLIs();
|
|
151
|
+
if (available.length === 0) {
|
|
152
|
+
console.log(chalk.yellow('No supported coding CLI detected.\n'));
|
|
153
|
+
showManualInstructions();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (available.length === 1) {
|
|
157
|
+
// Only one CLI available, use it
|
|
158
|
+
const cli = available[0];
|
|
159
|
+
console.log(chalk.gray(`Detected: ${CLI_CONFIGS[cli].displayName}\n`));
|
|
160
|
+
const success = await installForCLI(cli);
|
|
161
|
+
if (success) {
|
|
162
|
+
showSuccessMessage(cli);
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Multiple CLIs available - install for all
|
|
167
|
+
console.log(chalk.gray(`Detected: ${available.map(c => CLI_CONFIGS[c].displayName).join(', ')}\n`));
|
|
168
|
+
for (const cli of available) {
|
|
169
|
+
console.log(chalk.white(`\nInstalling for ${CLI_CONFIGS[cli].displayName}...`));
|
|
170
|
+
await installForCLI(cli);
|
|
171
|
+
}
|
|
172
|
+
console.log(chalk.green('\nā
Haystack skills installed!\n'));
|
|
173
|
+
console.log(chalk.white('Run one of these in your coding CLI:'));
|
|
174
|
+
console.log(chalk.cyan(' /setup-haystack'));
|
|
175
|
+
console.log();
|
|
176
|
+
}
|
|
177
|
+
async function installForCLI(cli) {
|
|
178
|
+
switch (cli) {
|
|
179
|
+
case 'claude':
|
|
180
|
+
return installForClaude();
|
|
181
|
+
case 'codex':
|
|
182
|
+
return installForCodex();
|
|
183
|
+
case 'cursor':
|
|
184
|
+
return installForCursor();
|
|
185
|
+
default:
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function showSuccessMessage(cli) {
|
|
190
|
+
const config = CLI_CONFIGS[cli];
|
|
191
|
+
console.log(chalk.green('\nā
Haystack skills installed!\n'));
|
|
192
|
+
console.log(chalk.white('Available skills:'));
|
|
193
|
+
console.log(chalk.cyan(' /setup-haystack ') + chalk.gray('- Create .haystack.json with AI assistance'));
|
|
194
|
+
console.log(chalk.cyan(' /prepare-haystack ') + chalk.gray('- Add aria-labels and data-testid attributes'));
|
|
195
|
+
console.log(chalk.cyan(' /setup-haystack-secrets ') + chalk.gray('- Configure API keys and secrets'));
|
|
196
|
+
console.log();
|
|
197
|
+
console.log(chalk.white(`Open ${config.displayName} and run:`));
|
|
198
|
+
console.log(chalk.cyan(' /setup-haystack'));
|
|
199
|
+
console.log();
|
|
200
|
+
}
|
|
201
|
+
export async function listSkills() {
|
|
202
|
+
console.log(chalk.white('\nAvailable Haystack skills:\n'));
|
|
203
|
+
console.log(chalk.cyan(' /setup-haystack ') + chalk.gray('- Master setup - creates .haystack.json'));
|
|
204
|
+
console.log(chalk.cyan(' /prepare-haystack ') + chalk.gray('- Add accessibility attributes'));
|
|
205
|
+
console.log(chalk.cyan(' /setup-haystack-secrets ') + chalk.gray('- Configure secrets for sandboxes'));
|
|
206
|
+
console.log();
|
|
207
|
+
const available = detectAvailableCLIs();
|
|
208
|
+
if (available.length > 0) {
|
|
209
|
+
console.log(chalk.gray('Detected CLIs: ') + chalk.white(available.map(c => CLI_CONFIGS[c].displayName).join(', ')));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
console.log(chalk.gray('No coding CLI detected. Run: ') + chalk.white('haystack skills install --cli manual'));
|
|
213
|
+
}
|
|
214
|
+
console.log();
|
|
215
|
+
}
|