@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.
Files changed (36) hide show
  1. package/README.md +59 -12
  2. package/dist/assets/hooks/agent-context/detect.ts +136 -0
  3. package/dist/assets/hooks/agent-context/format.ts +99 -0
  4. package/dist/assets/hooks/agent-context/index.ts +39 -0
  5. package/dist/assets/hooks/agent-context/parsers/claude.ts +253 -0
  6. package/dist/assets/hooks/agent-context/parsers/gemini.ts +155 -0
  7. package/dist/assets/hooks/agent-context/parsers/opencode.ts +174 -0
  8. package/dist/assets/hooks/agent-context/tsconfig.json +13 -0
  9. package/dist/assets/hooks/agent-context/types.ts +58 -0
  10. package/dist/assets/hooks/llm-rules-template.md +35 -0
  11. package/dist/assets/hooks/package.json +11 -0
  12. package/dist/assets/hooks/scripts/commit-msg.sh +4 -0
  13. package/dist/assets/hooks/scripts/post-commit.sh +4 -0
  14. package/dist/assets/hooks/scripts/pre-commit.sh +92 -0
  15. package/dist/assets/hooks/scripts/pre-push.sh +5 -0
  16. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
  17. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +528 -0
  18. package/dist/assets/hooks/truncation-checker/index.ts +595 -0
  19. package/dist/assets/hooks/truncation-checker/tsconfig.json +13 -0
  20. package/dist/commands/config.d.ts +14 -0
  21. package/dist/commands/config.js +89 -0
  22. package/dist/commands/hooks.d.ts +17 -0
  23. package/dist/commands/hooks.js +269 -0
  24. package/dist/commands/init.d.ts +1 -1
  25. package/dist/commands/init.js +20 -239
  26. package/dist/commands/secrets.d.ts +15 -0
  27. package/dist/commands/secrets.js +83 -0
  28. package/dist/commands/skills.d.ts +8 -0
  29. package/dist/commands/skills.js +215 -0
  30. package/dist/index.js +107 -7
  31. package/dist/types.d.ts +32 -8
  32. package/dist/utils/hooks.d.ts +26 -0
  33. package/dist/utils/hooks.js +226 -0
  34. package/dist/utils/skill.d.ts +1 -1
  35. package/dist/utils/skill.js +481 -13
  36. package/package.json +2 -2
@@ -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
  */
@@ -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
+ }