@haystackeditor/cli 0.8.1 ā 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/assets/hooks/llm-rules-template.md +21 -0
- package/dist/assets/hooks/scripts/pre-push.sh +20 -0
- package/dist/assets/skills/prepare-haystack.md +323 -0
- package/dist/assets/skills/secrets.md +164 -0
- package/dist/assets/skills/setup-external-sandbox.md +243 -0
- package/dist/assets/skills/setup-haystack.md +639 -0
- package/dist/assets/skills/submit.md +154 -0
- package/dist/assets/templates/CLAUDE.md.snippet +42 -0
- package/dist/assets/templates/haystack.yml +193 -0
- package/dist/commands/check-pending.d.ts +19 -0
- package/dist/commands/check-pending.js +217 -0
- package/dist/commands/config.d.ts +13 -21
- package/dist/commands/config.js +278 -92
- package/dist/commands/install-session-hooks.d.ts +16 -0
- package/dist/commands/install-session-hooks.js +302 -0
- package/dist/commands/login.js +1 -1
- package/dist/commands/policy.d.ts +31 -0
- package/dist/commands/policy.js +365 -0
- package/dist/commands/skills.d.ts +2 -2
- package/dist/commands/skills.js +51 -186
- package/dist/commands/submit.d.ts +22 -0
- package/dist/commands/submit.js +428 -0
- package/dist/commands/triage.d.ts +16 -0
- package/dist/commands/triage.js +354 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +232 -2
- package/dist/tools/detect.d.ts +50 -0
- package/dist/tools/detect.js +853 -0
- package/dist/tools/fixtures.d.ts +38 -0
- package/dist/tools/fixtures.js +199 -0
- package/dist/tools/setup.d.ts +43 -0
- package/dist/tools/setup.js +597 -0
- package/dist/triage/prompts.d.ts +31 -0
- package/dist/triage/prompts.js +266 -0
- package/dist/triage/runner.d.ts +21 -0
- package/dist/triage/runner.js +325 -0
- package/dist/triage/traces.d.ts +20 -0
- package/dist/triage/traces.js +305 -0
- package/dist/triage/types.d.ts +47 -0
- package/dist/triage/types.js +7 -0
- package/dist/types.d.ts +1387 -191
- package/dist/types.js +254 -2
- package/dist/utils/analysis-api.d.ts +108 -0
- package/dist/utils/analysis-api.js +194 -0
- package/dist/utils/config.js +1 -1
- package/dist/utils/git.d.ts +80 -0
- package/dist/utils/git.js +302 -0
- package/dist/utils/github-api.d.ts +83 -0
- package/dist/utils/github-api.js +266 -0
- package/dist/utils/pending-state.d.ts +38 -0
- package/dist/utils/pending-state.js +86 -0
- package/dist/utils/secrets.js +3 -3
- package/dist/utils/skill.js +257 -0
- package/package.json +4 -2
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* haystack policy - Manage review policies
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* list - List all review policies
|
|
6
|
+
* add - Add a new policy interactively
|
|
7
|
+
* remove - Remove a policy by name
|
|
8
|
+
* init - Create initial review-policy.md
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import inquirer from 'inquirer';
|
|
14
|
+
const POLICY_PATH = '.haystack/review-policy.md';
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Markdown Parsing
|
|
17
|
+
// ============================================================================
|
|
18
|
+
/**
|
|
19
|
+
* Parse review-policy.md content into ReviewPolicy objects and instructions
|
|
20
|
+
*/
|
|
21
|
+
function parsePolicyMarkdown(content) {
|
|
22
|
+
const policies = [];
|
|
23
|
+
const instructions = [];
|
|
24
|
+
// Split by H2 headings (## )
|
|
25
|
+
const sections = content.split(/^## /m).slice(1); // Skip content before first H2
|
|
26
|
+
for (const section of sections) {
|
|
27
|
+
const lines = section.trim().split('\n');
|
|
28
|
+
if (lines.length === 0)
|
|
29
|
+
continue;
|
|
30
|
+
// First line is the section name
|
|
31
|
+
const name = lines[0].trim();
|
|
32
|
+
if (!name)
|
|
33
|
+
continue;
|
|
34
|
+
// ## Instructions ā collect bullet points as raw semantic directives
|
|
35
|
+
if (name.toLowerCase() === 'instructions') {
|
|
36
|
+
for (const line of lines.slice(1)) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
const bulletMatch = trimmed.match(/^[-*]\s+(.+)$/);
|
|
39
|
+
if (bulletMatch) {
|
|
40
|
+
instructions.push(bulletMatch[1].trim());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
let paths = [];
|
|
46
|
+
let severity = 'medium';
|
|
47
|
+
let reason = '';
|
|
48
|
+
// Parse bullet points
|
|
49
|
+
for (const line of lines.slice(1)) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
// Match **Paths**: `pattern1`, `pattern2`
|
|
52
|
+
const pathsMatch = trimmed.match(/^\*\*Paths?\*\*:\s*(.+)$/i);
|
|
53
|
+
if (pathsMatch) {
|
|
54
|
+
const pathString = pathsMatch[1];
|
|
55
|
+
const backtickPatterns = pathString.match(/`([^`]+)`/g);
|
|
56
|
+
if (backtickPatterns) {
|
|
57
|
+
paths = backtickPatterns.map(p => p.replace(/`/g, '').trim());
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
paths = pathString.split(',').map(p => p.trim());
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Match **Severity**: critical|high|medium|low
|
|
65
|
+
const severityMatch = trimmed.match(/^\*\*Severity\*\*:\s*(critical|high|medium|low)/i);
|
|
66
|
+
if (severityMatch) {
|
|
67
|
+
severity = severityMatch[1].toLowerCase();
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Match **Reason**: ...
|
|
71
|
+
const reasonMatch = trimmed.match(/^\*\*Reason\*\*:\s*(.+)$/i);
|
|
72
|
+
if (reasonMatch) {
|
|
73
|
+
reason = reasonMatch[1].trim();
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Only add if we have at least paths and reason
|
|
78
|
+
if (paths.length > 0 && reason) {
|
|
79
|
+
policies.push({ name, paths, reason, severity });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { policies, instructions };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Serialize ReviewPolicy objects and instructions back to markdown
|
|
86
|
+
*/
|
|
87
|
+
function serializePolicies(policies, instructions = []) {
|
|
88
|
+
const lines = ['# Review Policies', ''];
|
|
89
|
+
if (instructions.length > 0) {
|
|
90
|
+
lines.push('## Instructions', '');
|
|
91
|
+
for (const instruction of instructions) {
|
|
92
|
+
lines.push(`- ${instruction}`);
|
|
93
|
+
}
|
|
94
|
+
lines.push('');
|
|
95
|
+
}
|
|
96
|
+
for (const policy of policies) {
|
|
97
|
+
lines.push(`## ${policy.name}`);
|
|
98
|
+
lines.push(`- **Paths**: ${policy.paths.map(p => '`' + p + '`').join(', ')}`);
|
|
99
|
+
lines.push(`- **Severity**: ${policy.severity}`);
|
|
100
|
+
lines.push(`- **Reason**: ${policy.reason}`);
|
|
101
|
+
lines.push('');
|
|
102
|
+
}
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Severity Colors
|
|
107
|
+
// ============================================================================
|
|
108
|
+
function severityColor(severity) {
|
|
109
|
+
switch (severity) {
|
|
110
|
+
case 'critical': return chalk.red;
|
|
111
|
+
case 'high': return chalk.yellow;
|
|
112
|
+
case 'medium': return chalk.blue;
|
|
113
|
+
case 'low': return chalk.dim;
|
|
114
|
+
default: return chalk.white;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Commands
|
|
119
|
+
// ============================================================================
|
|
120
|
+
/**
|
|
121
|
+
* List all review policies
|
|
122
|
+
*/
|
|
123
|
+
export async function listPolicies() {
|
|
124
|
+
const filePath = join(process.cwd(), POLICY_PATH);
|
|
125
|
+
if (!existsSync(filePath)) {
|
|
126
|
+
console.log(chalk.yellow('\nNo review-policy.md found.'));
|
|
127
|
+
console.log(chalk.dim('Run "haystack policy init" to create one.\n'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
131
|
+
const { policies, instructions } = parsePolicyMarkdown(content);
|
|
132
|
+
if (policies.length === 0 && instructions.length === 0) {
|
|
133
|
+
console.log(chalk.yellow('\nNo policies or instructions defined in review-policy.md'));
|
|
134
|
+
console.log(chalk.dim('Run "haystack policy add" to create one.\n'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (instructions.length > 0) {
|
|
138
|
+
console.log(chalk.cyan('\nš Review Instructions\n'));
|
|
139
|
+
for (const instruction of instructions) {
|
|
140
|
+
console.log(` ${chalk.dim('ā¢')} ${instruction}`);
|
|
141
|
+
}
|
|
142
|
+
console.log('');
|
|
143
|
+
}
|
|
144
|
+
if (policies.length > 0) {
|
|
145
|
+
console.log(chalk.cyan('š Review Policies\n'));
|
|
146
|
+
for (const policy of policies) {
|
|
147
|
+
const color = severityColor(policy.severity);
|
|
148
|
+
console.log(` ${chalk.bold(policy.name)}`);
|
|
149
|
+
console.log(` Severity: ${color(policy.severity)}`);
|
|
150
|
+
console.log(` Paths: ${chalk.dim(policy.paths.join(', '))}`);
|
|
151
|
+
console.log(` Reason: ${policy.reason}`);
|
|
152
|
+
console.log('');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Add a new review policy interactively
|
|
158
|
+
*/
|
|
159
|
+
export async function addPolicy(nameArg) {
|
|
160
|
+
const filePath = join(process.cwd(), POLICY_PATH);
|
|
161
|
+
// Load existing policies and instructions
|
|
162
|
+
let policies = [];
|
|
163
|
+
let instructions = [];
|
|
164
|
+
if (existsSync(filePath)) {
|
|
165
|
+
const existingContent = readFileSync(filePath, 'utf-8');
|
|
166
|
+
const parsed = parsePolicyMarkdown(existingContent);
|
|
167
|
+
policies = parsed.policies;
|
|
168
|
+
instructions = parsed.instructions;
|
|
169
|
+
}
|
|
170
|
+
console.log(chalk.cyan('\nā Add Review Policy\n'));
|
|
171
|
+
// Prompt for policy details
|
|
172
|
+
const answers = await inquirer.prompt([
|
|
173
|
+
{
|
|
174
|
+
type: 'input',
|
|
175
|
+
name: 'name',
|
|
176
|
+
message: 'Policy name:',
|
|
177
|
+
default: nameArg,
|
|
178
|
+
validate: (input) => {
|
|
179
|
+
if (!input.trim())
|
|
180
|
+
return 'Name is required';
|
|
181
|
+
if (policies.some(p => p.name.toLowerCase() === input.toLowerCase())) {
|
|
182
|
+
return 'A policy with this name already exists';
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
type: 'input',
|
|
189
|
+
name: 'paths',
|
|
190
|
+
message: 'File patterns (comma-separated globs):',
|
|
191
|
+
validate: (input) => input.trim() ? true : 'At least one path pattern is required',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
type: 'list',
|
|
195
|
+
name: 'severity',
|
|
196
|
+
message: 'Severity:',
|
|
197
|
+
choices: [
|
|
198
|
+
{ name: chalk.red('critical') + ' - Always requires human review', value: 'critical' },
|
|
199
|
+
{ name: chalk.yellow('high') + ' - Usually requires review', value: 'high' },
|
|
200
|
+
{ name: chalk.blue('medium') + ' - Should be reviewed', value: 'medium' },
|
|
201
|
+
{ name: chalk.dim('low') + ' - Nice to review', value: 'low' },
|
|
202
|
+
],
|
|
203
|
+
default: 'medium',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: 'input',
|
|
207
|
+
name: 'reason',
|
|
208
|
+
message: 'Reason for requiring review:',
|
|
209
|
+
validate: (input) => input.trim() ? true : 'Reason is required',
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
// Parse paths from comma-separated input
|
|
213
|
+
const paths = answers.paths.split(',').map((p) => p.trim()).filter(Boolean);
|
|
214
|
+
const newPolicy = {
|
|
215
|
+
name: answers.name.trim(),
|
|
216
|
+
paths,
|
|
217
|
+
severity: answers.severity,
|
|
218
|
+
reason: answers.reason.trim(),
|
|
219
|
+
};
|
|
220
|
+
policies.push(newPolicy);
|
|
221
|
+
// Ensure directory exists
|
|
222
|
+
const dir = dirname(filePath);
|
|
223
|
+
if (!existsSync(dir)) {
|
|
224
|
+
mkdirSync(dir, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
// Write updated file
|
|
227
|
+
writeFileSync(filePath, serializePolicies(policies, instructions));
|
|
228
|
+
console.log(chalk.green(`\nā Added policy "${newPolicy.name}"`));
|
|
229
|
+
console.log(chalk.dim(` Updated ${POLICY_PATH}\n`));
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Remove a policy by name
|
|
233
|
+
*/
|
|
234
|
+
export async function removePolicy(name) {
|
|
235
|
+
const filePath = join(process.cwd(), POLICY_PATH);
|
|
236
|
+
if (!existsSync(filePath)) {
|
|
237
|
+
console.log(chalk.red('\nNo review-policy.md found.\n'));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
241
|
+
const parsed = parsePolicyMarkdown(content);
|
|
242
|
+
let policies = parsed.policies;
|
|
243
|
+
const instructions = parsed.instructions;
|
|
244
|
+
// Find policy (case-insensitive)
|
|
245
|
+
const index = policies.findIndex(p => p.name.toLowerCase() === name.toLowerCase());
|
|
246
|
+
if (index === -1) {
|
|
247
|
+
console.log(chalk.red(`\nPolicy "${name}" not found.\n`));
|
|
248
|
+
console.log(chalk.dim('Available policies:'));
|
|
249
|
+
for (const p of policies) {
|
|
250
|
+
console.log(chalk.dim(` - ${p.name}`));
|
|
251
|
+
}
|
|
252
|
+
console.log('');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const removed = policies[index];
|
|
256
|
+
// Confirm removal
|
|
257
|
+
const { confirm } = await inquirer.prompt([
|
|
258
|
+
{
|
|
259
|
+
type: 'confirm',
|
|
260
|
+
name: 'confirm',
|
|
261
|
+
message: `Remove policy "${removed.name}"?`,
|
|
262
|
+
default: false,
|
|
263
|
+
},
|
|
264
|
+
]);
|
|
265
|
+
if (!confirm) {
|
|
266
|
+
console.log(chalk.dim('\nCancelled.\n'));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Remove and write
|
|
270
|
+
policies = policies.filter((_, i) => i !== index);
|
|
271
|
+
writeFileSync(filePath, serializePolicies(policies, instructions));
|
|
272
|
+
console.log(chalk.green(`\nā Removed policy "${removed.name}"`));
|
|
273
|
+
console.log(chalk.dim(` Updated ${POLICY_PATH}\n`));
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Add a review instruction
|
|
277
|
+
*/
|
|
278
|
+
export async function addInstruction(text) {
|
|
279
|
+
const filePath = join(process.cwd(), POLICY_PATH);
|
|
280
|
+
// Load existing
|
|
281
|
+
let policies = [];
|
|
282
|
+
let instructions = [];
|
|
283
|
+
if (existsSync(filePath)) {
|
|
284
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
285
|
+
const parsed = parsePolicyMarkdown(content);
|
|
286
|
+
policies = parsed.policies;
|
|
287
|
+
instructions = parsed.instructions;
|
|
288
|
+
}
|
|
289
|
+
let instruction = text?.trim();
|
|
290
|
+
if (!instruction) {
|
|
291
|
+
const answers = await inquirer.prompt([
|
|
292
|
+
{
|
|
293
|
+
type: 'input',
|
|
294
|
+
name: 'instruction',
|
|
295
|
+
message: 'Review instruction:',
|
|
296
|
+
validate: (input) => input.trim() ? true : 'Instruction text is required',
|
|
297
|
+
},
|
|
298
|
+
]);
|
|
299
|
+
instruction = answers.instruction.trim();
|
|
300
|
+
}
|
|
301
|
+
instructions.push(instruction);
|
|
302
|
+
// Ensure directory exists
|
|
303
|
+
const dir = dirname(filePath);
|
|
304
|
+
if (!existsSync(dir)) {
|
|
305
|
+
mkdirSync(dir, { recursive: true });
|
|
306
|
+
}
|
|
307
|
+
writeFileSync(filePath, serializePolicies(policies, instructions));
|
|
308
|
+
console.log(chalk.green(`\nā Added instruction: "${instruction}"`));
|
|
309
|
+
console.log(chalk.dim(` Updated ${POLICY_PATH}\n`));
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Create initial review-policy.md with example policies
|
|
313
|
+
*/
|
|
314
|
+
export async function initPolicies(options) {
|
|
315
|
+
const filePath = join(process.cwd(), POLICY_PATH);
|
|
316
|
+
if (existsSync(filePath) && !options.force) {
|
|
317
|
+
console.log(chalk.yellow('\nreview-policy.md already exists. Use --force to overwrite.\n'));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// Default template policies
|
|
321
|
+
const defaultPolicies = [
|
|
322
|
+
{
|
|
323
|
+
name: 'Infrastructure changes',
|
|
324
|
+
paths: ['terraform/**', '*.tf', 'pulumi/**', 'cdk/**'],
|
|
325
|
+
severity: 'high',
|
|
326
|
+
reason: 'Infrastructure changes require manual review for security and cost implications',
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: 'Secret files',
|
|
330
|
+
paths: ['**/*.secret*', '**/*.env*', '**/credentials*', '**/*.pem', '**/*.key'],
|
|
331
|
+
severity: 'critical',
|
|
332
|
+
reason: 'Files potentially containing secrets require security review',
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'CI/CD pipelines',
|
|
336
|
+
paths: ['.github/workflows/**', '.gitlab-ci.yml', 'Jenkinsfile', '.circleci/**'],
|
|
337
|
+
severity: 'high',
|
|
338
|
+
reason: 'CI/CD changes can affect deployment security and reliability',
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: 'Weak test coverage',
|
|
342
|
+
paths: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py', '**/*.go', '**/*.rs'],
|
|
343
|
+
severity: 'medium',
|
|
344
|
+
reason: 'Non-trivial logic changes without test coverage should be reviewed by a human to verify correctness',
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: 'Unverified agent changes',
|
|
348
|
+
paths: ['**/*'],
|
|
349
|
+
severity: 'medium',
|
|
350
|
+
reason: 'Agent-authored PRs without verification evidence (tests run, build checked, UI verified) should be reviewed by a human',
|
|
351
|
+
},
|
|
352
|
+
];
|
|
353
|
+
// Ensure directory exists
|
|
354
|
+
const dir = dirname(filePath);
|
|
355
|
+
if (!existsSync(dir)) {
|
|
356
|
+
mkdirSync(dir, { recursive: true });
|
|
357
|
+
}
|
|
358
|
+
writeFileSync(filePath, serializePolicies(defaultPolicies));
|
|
359
|
+
console.log(chalk.green(`\nā Created ${POLICY_PATH}`));
|
|
360
|
+
console.log(chalk.dim('\nDefault policies added:'));
|
|
361
|
+
for (const p of defaultPolicies) {
|
|
362
|
+
console.log(chalk.dim(` - ${p.name} (${p.severity})`));
|
|
363
|
+
}
|
|
364
|
+
console.log(chalk.dim('\nEdit the file or use "haystack policy add" / "haystack policy add-instruction" to customize.\n'));
|
|
365
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skills command -
|
|
3
|
-
*
|
|
2
|
+
* Skills command - Copies Haystack skill files into the project
|
|
3
|
+
* for AI agent discovery (Claude Code, Codex CLI, Cursor)
|
|
4
4
|
*/
|
|
5
5
|
export declare function installSkills(options: {
|
|
6
6
|
cli?: string;
|
package/dist/commands/skills.js
CHANGED
|
@@ -1,200 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skills command -
|
|
3
|
-
*
|
|
2
|
+
* Skills command - Copies Haystack skill files into the project
|
|
3
|
+
* for AI agent discovery (Claude Code, Codex CLI, Cursor)
|
|
4
4
|
*/
|
|
5
|
-
import { execSync } from 'child_process';
|
|
6
5
|
import chalk from 'chalk';
|
|
7
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
}
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
/** Directory containing bundled skill files */
|
|
11
|
+
function getSkillsSourceDir() {
|
|
12
|
+
// In dist/commands/skills.js ā assets are at dist/assets/skills/
|
|
13
|
+
return join(__dirname, '..', 'assets', 'skills');
|
|
68
14
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
15
|
+
const SKILL_FILES = [
|
|
16
|
+
'setup-haystack.md',
|
|
17
|
+
'prepare-haystack.md',
|
|
18
|
+
'secrets.md',
|
|
19
|
+
'setup-external-sandbox.md',
|
|
20
|
+
'submit.md',
|
|
21
|
+
];
|
|
22
|
+
function installSkillFiles(targetDir) {
|
|
23
|
+
const sourceDir = getSkillsSourceDir();
|
|
24
|
+
if (!existsSync(sourceDir)) {
|
|
25
|
+
console.error(chalk.red(`Skills source directory not found: ${sourceDir}`));
|
|
77
26
|
return false;
|
|
78
27
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
let config = {};
|
|
91
|
-
if (existsSync(configPath)) {
|
|
92
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
93
|
-
config = JSON.parse(content);
|
|
28
|
+
// Ensure target directory exists
|
|
29
|
+
if (!existsSync(targetDir)) {
|
|
30
|
+
mkdirSync(targetDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
let copied = 0;
|
|
33
|
+
for (const file of SKILL_FILES) {
|
|
34
|
+
const src = join(sourceDir, file);
|
|
35
|
+
if (existsSync(src)) {
|
|
36
|
+
const dest = join(targetDir, file);
|
|
37
|
+
writeFileSync(dest, readFileSync(src, 'utf-8'));
|
|
38
|
+
copied++;
|
|
94
39
|
}
|
|
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
40
|
}
|
|
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'));
|
|
41
|
+
return copied > 0;
|
|
128
42
|
}
|
|
129
43
|
export async function installSkills(options) {
|
|
130
44
|
console.log(chalk.cyan('\nš¦ Installing Haystack skills...\n'));
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
}
|
|
45
|
+
const projectRoot = process.cwd();
|
|
46
|
+
// Install to .agents/skills/ for generic agent discovery
|
|
47
|
+
const agentsSkillsDir = join(projectRoot, '.agents', 'skills');
|
|
48
|
+
console.log(chalk.gray(`Copying skills to ${agentsSkillsDir}\n`));
|
|
49
|
+
const success = installSkillFiles(agentsSkillsDir);
|
|
50
|
+
if (!success) {
|
|
51
|
+
console.error(chalk.red('Failed to install skill files.'));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Also create .claude/commands/ entry for Claude Code slash commands
|
|
55
|
+
const claudeCommandsDir = join(projectRoot, '.claude', 'commands');
|
|
56
|
+
if (!existsSync(claudeCommandsDir)) {
|
|
57
|
+
mkdirSync(claudeCommandsDir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
const claudeCommandPath = join(claudeCommandsDir, 'setup-haystack.md');
|
|
60
|
+
writeFileSync(claudeCommandPath, '# Set Up Haystack Verification\n\nFollow .agents/skills/setup-haystack.md to set up Haystack verification for this repo.\n');
|
|
61
|
+
showSuccessMessage();
|
|
188
62
|
}
|
|
189
|
-
function showSuccessMessage(
|
|
190
|
-
const config = CLI_CONFIGS[cli];
|
|
63
|
+
function showSuccessMessage() {
|
|
191
64
|
console.log(chalk.green('\nā
Haystack skills installed!\n'));
|
|
192
65
|
console.log(chalk.white('Available skills:'));
|
|
193
66
|
console.log(chalk.cyan(' /setup-haystack ') + chalk.gray('- Create .haystack.json with AI assistance'));
|
|
194
67
|
console.log(chalk.cyan(' /prepare-haystack ') + chalk.gray('- Add aria-labels and data-testid attributes'));
|
|
195
68
|
console.log(chalk.cyan(' /setup-haystack-secrets ') + chalk.gray('- Configure API keys and secrets'));
|
|
196
69
|
console.log();
|
|
197
|
-
console.log(chalk.white(
|
|
70
|
+
console.log(chalk.white('Run in your coding CLI:'));
|
|
198
71
|
console.log(chalk.cyan(' /setup-haystack'));
|
|
199
72
|
console.log();
|
|
200
73
|
}
|
|
@@ -204,12 +77,4 @@ export async function listSkills() {
|
|
|
204
77
|
console.log(chalk.cyan(' /prepare-haystack ') + chalk.gray('- Add accessibility attributes'));
|
|
205
78
|
console.log(chalk.cyan(' /setup-haystack-secrets ') + chalk.gray('- Configure secrets for sandboxes'));
|
|
206
79
|
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
80
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Submit command - Create a PR from current changes
|
|
3
|
+
*
|
|
4
|
+
* Runs pre-PR triage (code review, rules, intent drift) via parallel sub-agents,
|
|
5
|
+
* then pushes the current branch and creates a PR.
|
|
6
|
+
*
|
|
7
|
+
* PRs are routed based on flags:
|
|
8
|
+
* - Default: auto-merge queue (analysis runs, auto-merged if approved)
|
|
9
|
+
* - --review: human review required
|
|
10
|
+
* - --force: skip triage checks
|
|
11
|
+
*/
|
|
12
|
+
export interface SubmitOptions {
|
|
13
|
+
title?: string;
|
|
14
|
+
body?: string;
|
|
15
|
+
bodyFile?: string;
|
|
16
|
+
base?: string;
|
|
17
|
+
draft?: boolean;
|
|
18
|
+
review?: string | true;
|
|
19
|
+
force?: boolean;
|
|
20
|
+
wait?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function submitCommand(options: SubmitOptions): Promise<void>;
|