@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,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install-session-hooks — Wire up session-start hooks for coding CLIs.
|
|
3
|
+
*
|
|
4
|
+
* When a CLI session starts, `haystack check-pending --hook` runs automatically
|
|
5
|
+
* and shows the user whether their pending PRs are "Good to merge" or "Need input".
|
|
6
|
+
*
|
|
7
|
+
* Supported CLIs:
|
|
8
|
+
* - Claude Code: Native SessionStart hook in .claude/settings.json
|
|
9
|
+
* - Codex CLI: AGENTS.md instructions (no native hook support)
|
|
10
|
+
* - Gemini CLI: GEMINI.md instructions (no native hook support)
|
|
11
|
+
*/
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { findGitRoot } from '../utils/git.js';
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// CLI detection (reused from triage runner pattern)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
function isCLIInstalled(name) {
|
|
21
|
+
try {
|
|
22
|
+
execSync(process.platform === 'win32' ? `where ${name}` : `which ${name}`, {
|
|
23
|
+
stdio: 'ignore',
|
|
24
|
+
});
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function detectInstalledCLIs() {
|
|
32
|
+
const installed = [];
|
|
33
|
+
if (isCLIInstalled('claude'))
|
|
34
|
+
installed.push('claude');
|
|
35
|
+
if (isCLIInstalled('codex'))
|
|
36
|
+
installed.push('codex');
|
|
37
|
+
if (isCLIInstalled('gemini'))
|
|
38
|
+
installed.push('gemini');
|
|
39
|
+
return installed;
|
|
40
|
+
}
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Claude Code: Native SessionStart hook
|
|
43
|
+
// ============================================================================
|
|
44
|
+
const HAYSTACK_HOOK_COMMAND = 'haystack check-pending --hook';
|
|
45
|
+
function installClaudeHook(gitRoot) {
|
|
46
|
+
const settingsDir = join(gitRoot, '.claude');
|
|
47
|
+
const settingsPath = join(settingsDir, 'settings.json');
|
|
48
|
+
try {
|
|
49
|
+
// Ensure .claude directory exists
|
|
50
|
+
if (!existsSync(settingsDir)) {
|
|
51
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
// Read or create settings
|
|
54
|
+
let settings = {};
|
|
55
|
+
if (existsSync(settingsPath)) {
|
|
56
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
57
|
+
}
|
|
58
|
+
// Ensure hooks object exists
|
|
59
|
+
const hooks = (settings.hooks || {});
|
|
60
|
+
settings.hooks = hooks;
|
|
61
|
+
// Ensure SessionStart array exists
|
|
62
|
+
const sessionStart = (hooks.SessionStart || []);
|
|
63
|
+
hooks.SessionStart = sessionStart;
|
|
64
|
+
// Check if haystack hook already installed
|
|
65
|
+
const alreadyInstalled = sessionStart.some(entry => entry.hooks?.some(h => h.command?.includes('haystack check-pending')));
|
|
66
|
+
if (alreadyInstalled) {
|
|
67
|
+
console.log(chalk.dim(' Claude Code hook already installed.'));
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
// Add the hook
|
|
71
|
+
sessionStart.push({
|
|
72
|
+
matcher: '',
|
|
73
|
+
hooks: [
|
|
74
|
+
{
|
|
75
|
+
type: 'command',
|
|
76
|
+
command: HAYSTACK_HOOK_COMMAND,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
console.error(chalk.red(` Failed to update .claude/settings.json: ${err}`));
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function checkClaudeHook(gitRoot) {
|
|
89
|
+
const settingsPath = join(gitRoot, '.claude', 'settings.json');
|
|
90
|
+
if (!existsSync(settingsPath))
|
|
91
|
+
return false;
|
|
92
|
+
try {
|
|
93
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
94
|
+
const sessionStart = settings?.hooks?.SessionStart || [];
|
|
95
|
+
return sessionStart.some((entry) => entry.hooks?.some((h) => h.command?.includes('haystack check-pending')));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// Codex CLI: AGENTS.md instructions
|
|
103
|
+
// ============================================================================
|
|
104
|
+
const CODEX_HOOK_MARKER = '<!-- haystack-session-hook -->';
|
|
105
|
+
const CODEX_HOOK_BLOCK = `
|
|
106
|
+
${CODEX_HOOK_MARKER}
|
|
107
|
+
## Haystack Session Check
|
|
108
|
+
|
|
109
|
+
When starting a new session, run this command first to check for pending PR analysis results:
|
|
110
|
+
|
|
111
|
+
\`\`\`bash
|
|
112
|
+
haystack check-pending --hook
|
|
113
|
+
\`\`\`
|
|
114
|
+
|
|
115
|
+
This shows whether your submitted PRs are "Good to merge" or "Need your input".
|
|
116
|
+
`;
|
|
117
|
+
function installCodexHook(gitRoot) {
|
|
118
|
+
const agentsPath = join(gitRoot, 'AGENTS.md');
|
|
119
|
+
try {
|
|
120
|
+
let content = '';
|
|
121
|
+
if (existsSync(agentsPath)) {
|
|
122
|
+
content = readFileSync(agentsPath, 'utf-8');
|
|
123
|
+
if (content.includes(CODEX_HOOK_MARKER)) {
|
|
124
|
+
console.log(chalk.dim(' Codex AGENTS.md already has session hook.'));
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
content += CODEX_HOOK_BLOCK;
|
|
129
|
+
writeFileSync(agentsPath, content);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
console.error(chalk.red(` Failed to update AGENTS.md: ${err}`));
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function checkCodexHook(gitRoot) {
|
|
138
|
+
const agentsPath = join(gitRoot, 'AGENTS.md');
|
|
139
|
+
if (!existsSync(agentsPath))
|
|
140
|
+
return false;
|
|
141
|
+
try {
|
|
142
|
+
const content = readFileSync(agentsPath, 'utf-8');
|
|
143
|
+
return content.includes(CODEX_HOOK_MARKER);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// Gemini CLI: GEMINI.md instructions
|
|
151
|
+
// ============================================================================
|
|
152
|
+
const GEMINI_HOOK_MARKER = '<!-- haystack-session-hook -->';
|
|
153
|
+
const GEMINI_HOOK_BLOCK = `
|
|
154
|
+
${GEMINI_HOOK_MARKER}
|
|
155
|
+
## Haystack Session Check
|
|
156
|
+
|
|
157
|
+
When starting a new session, run this command first to check for pending PR analysis results:
|
|
158
|
+
|
|
159
|
+
\`\`\`bash
|
|
160
|
+
haystack check-pending --hook
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
163
|
+
This shows whether your submitted PRs are "Good to merge" or "Need your input".
|
|
164
|
+
`;
|
|
165
|
+
function installGeminiHook(gitRoot) {
|
|
166
|
+
const geminiPath = join(gitRoot, 'GEMINI.md');
|
|
167
|
+
try {
|
|
168
|
+
let content = '';
|
|
169
|
+
if (existsSync(geminiPath)) {
|
|
170
|
+
content = readFileSync(geminiPath, 'utf-8');
|
|
171
|
+
if (content.includes(GEMINI_HOOK_MARKER)) {
|
|
172
|
+
console.log(chalk.dim(' Gemini GEMINI.md already has session hook.'));
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
content += GEMINI_HOOK_BLOCK;
|
|
177
|
+
writeFileSync(geminiPath, content);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error(chalk.red(` Failed to update GEMINI.md: ${err}`));
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function checkGeminiHook(gitRoot) {
|
|
186
|
+
const geminiPath = join(gitRoot, 'GEMINI.md');
|
|
187
|
+
if (!existsSync(geminiPath))
|
|
188
|
+
return false;
|
|
189
|
+
try {
|
|
190
|
+
const content = readFileSync(geminiPath, 'utf-8');
|
|
191
|
+
return content.includes(GEMINI_HOOK_MARKER);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// CLI config registry
|
|
199
|
+
// ============================================================================
|
|
200
|
+
const CLI_HOOKS = {
|
|
201
|
+
claude: {
|
|
202
|
+
name: 'claude',
|
|
203
|
+
displayName: 'Claude Code',
|
|
204
|
+
hasNativeHook: true,
|
|
205
|
+
install: installClaudeHook,
|
|
206
|
+
check: checkClaudeHook,
|
|
207
|
+
},
|
|
208
|
+
codex: {
|
|
209
|
+
name: 'codex',
|
|
210
|
+
displayName: 'Codex CLI',
|
|
211
|
+
hasNativeHook: false,
|
|
212
|
+
install: installCodexHook,
|
|
213
|
+
check: checkCodexHook,
|
|
214
|
+
},
|
|
215
|
+
gemini: {
|
|
216
|
+
name: 'gemini',
|
|
217
|
+
displayName: 'Gemini CLI',
|
|
218
|
+
hasNativeHook: false,
|
|
219
|
+
install: installGeminiHook,
|
|
220
|
+
check: checkGeminiHook,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
// ============================================================================
|
|
224
|
+
// Commands
|
|
225
|
+
// ============================================================================
|
|
226
|
+
export async function installSessionHooks(options) {
|
|
227
|
+
console.log(chalk.cyan('\nHaystack Session Hooks\n'));
|
|
228
|
+
const gitRoot = findGitRoot();
|
|
229
|
+
if (!gitRoot) {
|
|
230
|
+
console.error(chalk.red('Not a git repository.\n'));
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
// Determine which CLIs to install for
|
|
234
|
+
let targets;
|
|
235
|
+
if (options.cli) {
|
|
236
|
+
const cli = options.cli.toLowerCase();
|
|
237
|
+
if (cli === 'all') {
|
|
238
|
+
targets = detectInstalledCLIs();
|
|
239
|
+
}
|
|
240
|
+
else if (cli in CLI_HOOKS) {
|
|
241
|
+
targets = [cli];
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
console.error(chalk.red(`Unknown CLI: ${options.cli}`));
|
|
245
|
+
console.log(chalk.dim('Supported: claude, codex, gemini, all'));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// Auto-detect
|
|
251
|
+
targets = detectInstalledCLIs();
|
|
252
|
+
}
|
|
253
|
+
if (targets.length === 0) {
|
|
254
|
+
console.log(chalk.yellow('No supported coding CLI detected.\n'));
|
|
255
|
+
console.log(chalk.dim('Supported CLIs: Claude Code, Codex CLI, Gemini CLI'));
|
|
256
|
+
console.log(chalk.dim('Install one and try again, or specify --cli <name>.\n'));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
console.log(chalk.dim(`Installing for: ${targets.map(t => CLI_HOOKS[t].displayName).join(', ')}\n`));
|
|
260
|
+
for (const target of targets) {
|
|
261
|
+
const config = CLI_HOOKS[target];
|
|
262
|
+
const hookType = config.hasNativeHook ? 'native hook' : 'markdown instructions';
|
|
263
|
+
console.log(` Installing ${config.displayName} (${hookType})...`);
|
|
264
|
+
const success = config.install(gitRoot);
|
|
265
|
+
if (success) {
|
|
266
|
+
console.log(chalk.green(` ✓ ${config.displayName} configured`));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
console.log(chalk.red(` ✗ ${config.displayName} failed`));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
console.log(chalk.green('\n✓ Session hooks installed!\n'));
|
|
273
|
+
console.log(chalk.dim('When you start a new session, you\'ll see:'));
|
|
274
|
+
console.log(chalk.green(' [Haystack] ✓ PR #42 "Fix auth": Good to merge'));
|
|
275
|
+
console.log(chalk.yellow(' [Haystack] ⚠ PR #37 "Refactor API": Needs your input (2 bugs)'));
|
|
276
|
+
console.log('');
|
|
277
|
+
}
|
|
278
|
+
export async function sessionHooksStatus() {
|
|
279
|
+
console.log(chalk.cyan('\nHaystack Session Hooks Status\n'));
|
|
280
|
+
const gitRoot = findGitRoot();
|
|
281
|
+
if (!gitRoot) {
|
|
282
|
+
console.error(chalk.red('Not a git repository.\n'));
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
const installed = detectInstalledCLIs();
|
|
286
|
+
for (const cli of ['claude', 'codex', 'gemini']) {
|
|
287
|
+
const config = CLI_HOOKS[cli];
|
|
288
|
+
const isInstalled = installed.includes(cli);
|
|
289
|
+
const hookConfigured = config.check(gitRoot);
|
|
290
|
+
const cliIcon = isInstalled ? chalk.green('✓') : chalk.dim('-');
|
|
291
|
+
const hookIcon = hookConfigured ? chalk.green('✓') : chalk.red('✗');
|
|
292
|
+
console.log(` ${cliIcon} ${config.displayName} ${isInstalled ? '(installed)' : '(not found)'}`);
|
|
293
|
+
if (isInstalled) {
|
|
294
|
+
console.log(` ${hookIcon} Session hook ${hookConfigured ? 'configured' : 'not configured'}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const anyMissing = installed.some(cli => !CLI_HOOKS[cli].check(gitRoot));
|
|
298
|
+
if (anyMissing) {
|
|
299
|
+
console.log(chalk.dim('\nRun: haystack hooks install-session'));
|
|
300
|
+
}
|
|
301
|
+
console.log('');
|
|
302
|
+
}
|
package/dist/commands/login.js
CHANGED
|
@@ -32,7 +32,7 @@ async function startDeviceFlow() {
|
|
|
32
32
|
* Poll for access token
|
|
33
33
|
*/
|
|
34
34
|
async function pollForToken(deviceCode, interval) {
|
|
35
|
-
|
|
35
|
+
for (;;) {
|
|
36
36
|
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
|
37
37
|
const response = await fetch('https://github.com/login/oauth/access_token', {
|
|
38
38
|
method: 'POST',
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
/**
|
|
11
|
+
* List all review policies
|
|
12
|
+
*/
|
|
13
|
+
export declare function listPolicies(): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Add a new review policy interactively
|
|
16
|
+
*/
|
|
17
|
+
export declare function addPolicy(nameArg?: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Remove a policy by name
|
|
20
|
+
*/
|
|
21
|
+
export declare function removePolicy(name: string): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Add a review instruction
|
|
24
|
+
*/
|
|
25
|
+
export declare function addInstruction(text?: string): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Create initial review-policy.md with example policies
|
|
28
|
+
*/
|
|
29
|
+
export declare function initPolicies(options: {
|
|
30
|
+
force?: boolean;
|
|
31
|
+
}): Promise<void>;
|