@haystackeditor/cli 0.8.0 → 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 +105 -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 +56 -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 +25 -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/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 +18 -12
- package/dist/commands/config.js +327 -52
- package/dist/commands/hooks.d.ts +17 -0
- package/dist/commands/hooks.js +269 -0
- 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 +8 -0
- package/dist/commands/skills.js +80 -0
- 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 +317 -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/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +226 -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.d.ts +1 -1
- package/dist/utils/skill.js +658 -1
- package/package.json +5 -3
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* haystack hooks - Install and manage git hooks for AI agent quality checks
|
|
3
|
+
*
|
|
4
|
+
* Installs:
|
|
5
|
+
* - Entire CLI binary (session tracking, powered by https://entire.dev)
|
|
6
|
+
* - Pre-commit hook (agent detection, truncation checking, LLM rules review)
|
|
7
|
+
* - Git hooks for session logging (commit-msg, post-commit, pre-push, prepare-commit-msg)
|
|
8
|
+
*/
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { existsSync, accessSync, constants } from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import { findGitRoot, ENTIRE_DEFAULT_VERSION, ENTIRE_BIN_PATH, getInstalledEntireVersion, getLatestEntireVersion, downloadEntireBinary, copyHookFiles, installHookDeps, copyLlmRulesTemplate, createEntireConfig, updateGitignore, } from '../utils/hooks.js';
|
|
14
|
+
export async function hooksInstall(options) {
|
|
15
|
+
console.log(chalk.cyan('\nHaystack Hooks Setup\n'));
|
|
16
|
+
// Step 1: Validate git repo
|
|
17
|
+
const gitRoot = findGitRoot();
|
|
18
|
+
if (!gitRoot) {
|
|
19
|
+
console.error(chalk.red('Not a git repository. Run this from inside a git repo.\n'));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const hooksDir = path.join(gitRoot, 'hooks');
|
|
23
|
+
// Step 2: Check for existing installation
|
|
24
|
+
if (existsSync(hooksDir) && !options.force) {
|
|
25
|
+
console.error(chalk.yellow('hooks/ directory already exists. Use --force to overwrite.\n'));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
if (existsSync(hooksDir) && options.force) {
|
|
29
|
+
console.log(chalk.yellow('Overwriting existing hooks...'));
|
|
30
|
+
}
|
|
31
|
+
// Step 3: Download Entire binary
|
|
32
|
+
if (!options.skipEntire) {
|
|
33
|
+
const version = options.version || ENTIRE_DEFAULT_VERSION;
|
|
34
|
+
const installedVersion = await getInstalledEntireVersion();
|
|
35
|
+
if (installedVersion === version) {
|
|
36
|
+
console.log(chalk.green(`✓ Entire CLI v${version} already installed`));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.log(chalk.dim(`Downloading Entire CLI v${version}...`));
|
|
40
|
+
try {
|
|
41
|
+
await downloadEntireBinary(version);
|
|
42
|
+
console.log(chalk.green(`✓ Entire CLI v${version} installed to ~/.haystack/bin/entire`));
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
console.error(chalk.red(`Failed to download Entire CLI: ${message}\n`));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(chalk.dim('Skipping Entire CLI download (--skip-entire)'));
|
|
53
|
+
}
|
|
54
|
+
// Step 4: Copy hook files
|
|
55
|
+
try {
|
|
56
|
+
await copyHookFiles(hooksDir);
|
|
57
|
+
console.log(chalk.green('✓ Hook scripts installed to hooks/'));
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
61
|
+
console.error(chalk.red(`Failed to copy hook files: ${message}\n`));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
// Step 5: Install hook dependencies
|
|
65
|
+
console.log(chalk.dim('Installing hook dependencies (tree-sitter)...'));
|
|
66
|
+
try {
|
|
67
|
+
await installHookDeps(hooksDir);
|
|
68
|
+
console.log(chalk.green('✓ Hook dependencies installed'));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
console.log(chalk.yellow('⚠ Failed to install hook dependencies. Run `npm install` in hooks/ manually.'));
|
|
72
|
+
}
|
|
73
|
+
// Step 6: Configure git hooks path
|
|
74
|
+
try {
|
|
75
|
+
execSync('git config core.hooksPath hooks', { cwd: gitRoot, stdio: 'pipe' });
|
|
76
|
+
console.log(chalk.green('✓ Git configured to use hooks/ directory'));
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
80
|
+
console.error(chalk.red(`Failed to set git hooks path: ${message}\n`));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
// Step 7: Create .entire/ configuration
|
|
84
|
+
try {
|
|
85
|
+
await createEntireConfig(gitRoot);
|
|
86
|
+
console.log(chalk.green('✓ Created .entire/settings.json'));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
console.log(chalk.yellow('⚠ Failed to create .entire/ configuration'));
|
|
90
|
+
}
|
|
91
|
+
// Step 8: Create LLM_RULES.md if needed
|
|
92
|
+
try {
|
|
93
|
+
const created = await copyLlmRulesTemplate(gitRoot);
|
|
94
|
+
if (created) {
|
|
95
|
+
console.log(chalk.green('✓ Created LLM_RULES.md (customize this for your project)'));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.log(chalk.dim(' LLM_RULES.md already exists'));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
console.log(chalk.yellow('⚠ Failed to create LLM_RULES.md'));
|
|
103
|
+
}
|
|
104
|
+
// Step 9: Update .gitignore
|
|
105
|
+
try {
|
|
106
|
+
const updated = await updateGitignore(gitRoot);
|
|
107
|
+
if (updated) {
|
|
108
|
+
console.log(chalk.green('✓ Updated .gitignore'));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
console.log(chalk.yellow('⚠ Failed to update .gitignore'));
|
|
113
|
+
}
|
|
114
|
+
// Summary
|
|
115
|
+
console.log(chalk.green('\n✓ Haystack hooks installed!\n'));
|
|
116
|
+
console.log(chalk.dim('Hooks installed:'));
|
|
117
|
+
console.log(chalk.dim(' pre-commit Agent detection, truncation check, LLM rules review'));
|
|
118
|
+
console.log(chalk.dim(' commit-msg Session tracking (powered by Entire)'));
|
|
119
|
+
console.log(chalk.dim(' post-commit Session condensing (powered by Entire)'));
|
|
120
|
+
console.log(chalk.dim(' pre-push Session log push (powered by Entire)'));
|
|
121
|
+
console.log(chalk.dim(' prepare-commit-msg Commit preparation (powered by Entire)'));
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(chalk.dim('Files to commit:'));
|
|
124
|
+
console.log(chalk.dim(' hooks/ Hook scripts and TypeScript modules'));
|
|
125
|
+
console.log(chalk.dim(' .entire/settings.json'));
|
|
126
|
+
console.log(chalk.dim(' .entire/.gitignore'));
|
|
127
|
+
console.log(chalk.dim(' LLM_RULES.md Customize this for your project'));
|
|
128
|
+
console.log('');
|
|
129
|
+
}
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// haystack hooks status
|
|
132
|
+
// ============================================================================
|
|
133
|
+
export async function hooksStatus() {
|
|
134
|
+
const gitRoot = findGitRoot();
|
|
135
|
+
if (!gitRoot) {
|
|
136
|
+
console.error(chalk.red('Not a git repository.\n'));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
console.log(chalk.cyan('\nHaystack Hooks Status\n'));
|
|
140
|
+
const hooksDir = path.join(gitRoot, 'hooks');
|
|
141
|
+
// Check hooks directory
|
|
142
|
+
if (!existsSync(hooksDir)) {
|
|
143
|
+
console.log(chalk.red('✗ hooks/ directory not found'));
|
|
144
|
+
console.log(chalk.dim(' Run: haystack hooks install\n'));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
console.log(chalk.green('✓ hooks/ directory exists'));
|
|
148
|
+
// Check each hook script
|
|
149
|
+
const requiredHooks = [
|
|
150
|
+
'pre-commit',
|
|
151
|
+
'commit-msg',
|
|
152
|
+
'post-commit',
|
|
153
|
+
'pre-push',
|
|
154
|
+
'prepare-commit-msg',
|
|
155
|
+
];
|
|
156
|
+
for (const hook of requiredHooks) {
|
|
157
|
+
const hookPath = path.join(hooksDir, hook);
|
|
158
|
+
if (existsSync(hookPath)) {
|
|
159
|
+
try {
|
|
160
|
+
accessSync(hookPath, constants.X_OK);
|
|
161
|
+
console.log(chalk.green(`✓ ${hook}`));
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
console.log(chalk.yellow(`⚠ ${hook} (not executable)`));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
console.log(chalk.red(`✗ ${hook} missing`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Check TypeScript modules
|
|
172
|
+
const agentContextDir = path.join(hooksDir, 'agent-context');
|
|
173
|
+
const truncationDir = path.join(hooksDir, 'truncation-checker');
|
|
174
|
+
console.log(existsSync(agentContextDir)
|
|
175
|
+
? chalk.green('✓ agent-context/ module')
|
|
176
|
+
: chalk.red('✗ agent-context/ missing'));
|
|
177
|
+
console.log(existsSync(truncationDir)
|
|
178
|
+
? chalk.green('✓ truncation-checker/ module')
|
|
179
|
+
: chalk.red('✗ truncation-checker/ missing'));
|
|
180
|
+
// Check hook dependencies
|
|
181
|
+
const nodeModules = path.join(hooksDir, 'node_modules');
|
|
182
|
+
console.log(existsSync(nodeModules)
|
|
183
|
+
? chalk.green('✓ Hook dependencies installed')
|
|
184
|
+
: chalk.yellow('⚠ Hook dependencies not installed (run npm install in hooks/)'));
|
|
185
|
+
// Check git core.hooksPath
|
|
186
|
+
try {
|
|
187
|
+
const hooksPath = execSync('git config core.hooksPath', {
|
|
188
|
+
cwd: gitRoot,
|
|
189
|
+
encoding: 'utf-8',
|
|
190
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
191
|
+
}).trim();
|
|
192
|
+
if (hooksPath === 'hooks') {
|
|
193
|
+
console.log(chalk.green('✓ Git core.hooksPath = hooks'));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.log(chalk.yellow(`⚠ Git core.hooksPath = ${hooksPath} (expected: hooks)`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
console.log(chalk.red('✗ Git core.hooksPath not set'));
|
|
201
|
+
}
|
|
202
|
+
// Check Entire binary
|
|
203
|
+
const installedVersion = await getInstalledEntireVersion();
|
|
204
|
+
if (installedVersion) {
|
|
205
|
+
console.log(chalk.green(`✓ Entire CLI v${installedVersion}`));
|
|
206
|
+
}
|
|
207
|
+
else if (existsSync(ENTIRE_BIN_PATH)) {
|
|
208
|
+
console.log(chalk.yellow('⚠ Entire CLI binary exists but failed to run'));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
console.log(chalk.yellow('⚠ Entire CLI not installed (~/.haystack/bin/entire)'));
|
|
212
|
+
}
|
|
213
|
+
// Check .entire/settings.json
|
|
214
|
+
const settingsPath = path.join(gitRoot, '.entire', 'settings.json');
|
|
215
|
+
if (existsSync(settingsPath)) {
|
|
216
|
+
console.log(chalk.green('✓ .entire/settings.json'));
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
console.log(chalk.yellow('⚠ .entire/settings.json not found'));
|
|
220
|
+
}
|
|
221
|
+
// Check LLM_RULES.md
|
|
222
|
+
const rulesPath = path.join(gitRoot, 'LLM_RULES.md');
|
|
223
|
+
if (existsSync(rulesPath)) {
|
|
224
|
+
console.log(chalk.green('✓ LLM_RULES.md'));
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
console.log(chalk.yellow('⚠ LLM_RULES.md not found'));
|
|
228
|
+
}
|
|
229
|
+
console.log('');
|
|
230
|
+
}
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// haystack hooks update
|
|
233
|
+
// ============================================================================
|
|
234
|
+
export async function hooksUpdate() {
|
|
235
|
+
console.log(chalk.cyan('\nUpdating Entire CLI...\n'));
|
|
236
|
+
// Check current version
|
|
237
|
+
const currentVersion = await getInstalledEntireVersion();
|
|
238
|
+
if (currentVersion) {
|
|
239
|
+
console.log(chalk.dim(`Current version: v${currentVersion}`));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
console.log(chalk.dim('Entire CLI not currently installed.'));
|
|
243
|
+
}
|
|
244
|
+
// Fetch latest version
|
|
245
|
+
let latestVersion;
|
|
246
|
+
try {
|
|
247
|
+
latestVersion = await getLatestEntireVersion();
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
251
|
+
console.error(chalk.red(`Failed to check for updates: ${message}\n`));
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
console.log(chalk.dim(`Latest version: v${latestVersion}`));
|
|
255
|
+
if (currentVersion === latestVersion) {
|
|
256
|
+
console.log(chalk.green('\n✓ Already up to date.\n'));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Download and install
|
|
260
|
+
try {
|
|
261
|
+
await downloadEntireBinary(latestVersion);
|
|
262
|
+
console.log(chalk.green(`\n✓ Updated Entire CLI to v${latestVersion}\n`));
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
266
|
+
console.error(chalk.red(`Failed to update: ${message}\n`));
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
export interface InstallSessionOptions {
|
|
13
|
+
cli?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function installSessionHooks(options: InstallSessionOptions): Promise<void>;
|
|
16
|
+
export declare function sessionHooksStatus(): Promise<void>;
|
|
@@ -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>;
|