@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
package/dist/commands/config.js
CHANGED
|
@@ -1,9 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config commands - manage
|
|
2
|
+
* Config commands - manage preferences
|
|
3
|
+
*
|
|
4
|
+
* Repo-level preferences (sandbox_enabled, auto_merge) are stored in .haystack.json.
|
|
5
|
+
* User-level preferences (agentic_tool) are stored on the Haystack Platform API.
|
|
3
6
|
*/
|
|
4
7
|
import chalk from 'chalk';
|
|
8
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
5
11
|
import { loadToken } from './login.js';
|
|
12
|
+
import { AI_REVIEWER_SOURCES, AI_REVIEWER_DISPLAY_NAMES } from '../types.js';
|
|
6
13
|
const API_BASE = 'https://haystackeditor.com/api/preferences';
|
|
14
|
+
const AGENTIC_TOOL_LABELS = {
|
|
15
|
+
'opencode': 'OpenCode (Haystack billing)',
|
|
16
|
+
'claude-code': 'Claude Code (your Claude Max subscription)',
|
|
17
|
+
'codex': 'Codex CLI (your ChatGPT subscription)',
|
|
18
|
+
};
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// .haystack.json helpers (repo-level preferences)
|
|
21
|
+
// ============================================================================
|
|
22
|
+
function findRepoRoot() {
|
|
23
|
+
try {
|
|
24
|
+
return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
console.error(chalk.red('\nNot inside a git repository.\n'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function loadHaystackConfig() {
|
|
32
|
+
const root = findRepoRoot();
|
|
33
|
+
const configPath = resolve(root, '.haystack.json');
|
|
34
|
+
try {
|
|
35
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
36
|
+
return { config: JSON.parse(raw), path: configPath };
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (err.code === 'ENOENT') {
|
|
40
|
+
console.error(chalk.red(`\nNo .haystack.json found at ${configPath}`));
|
|
41
|
+
console.log(chalk.dim('Create one with: haystack init\n'));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function saveHaystackConfig(config, configPath) {
|
|
48
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
49
|
+
}
|
|
50
|
+
function getPreference(key) {
|
|
51
|
+
const { config } = loadHaystackConfig();
|
|
52
|
+
// Default: sandbox_enabled=true, auto_merge=false
|
|
53
|
+
const defaults = { sandbox_enabled: true, auto_merge: false };
|
|
54
|
+
return config.preferences?.[key] ?? defaults[key];
|
|
55
|
+
}
|
|
56
|
+
function setPreference(key, value) {
|
|
57
|
+
const { config, path } = loadHaystackConfig();
|
|
58
|
+
if (!config.preferences) {
|
|
59
|
+
config.preferences = {};
|
|
60
|
+
}
|
|
61
|
+
config.preferences[key] = value;
|
|
62
|
+
saveHaystackConfig(config, path);
|
|
63
|
+
}
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// API helpers (user-level preferences)
|
|
66
|
+
// ============================================================================
|
|
7
67
|
async function requireAuth() {
|
|
8
68
|
const token = await loadToken();
|
|
9
69
|
if (!token) {
|
|
@@ -13,7 +73,7 @@ async function requireAuth() {
|
|
|
13
73
|
return token;
|
|
14
74
|
}
|
|
15
75
|
async function apiRequest(method, token, body) {
|
|
16
|
-
|
|
76
|
+
return fetch(API_BASE, {
|
|
17
77
|
method,
|
|
18
78
|
headers: {
|
|
19
79
|
'Authorization': `Bearer ${token}`,
|
|
@@ -22,12 +82,64 @@ async function apiRequest(method, token, body) {
|
|
|
22
82
|
},
|
|
23
83
|
body: body ? JSON.stringify(body) : undefined,
|
|
24
84
|
});
|
|
25
|
-
return response;
|
|
26
85
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Sandbox (repo-level via .haystack.json)
|
|
88
|
+
// ============================================================================
|
|
30
89
|
export async function getSandboxStatus() {
|
|
90
|
+
const enabled = getPreference('sandbox_enabled');
|
|
91
|
+
console.log(chalk.bold('\nSandbox mode:'), enabled
|
|
92
|
+
? chalk.green('enabled')
|
|
93
|
+
: chalk.yellow('disabled'));
|
|
94
|
+
console.log();
|
|
95
|
+
if (enabled) {
|
|
96
|
+
console.log(chalk.dim('Haystack can clone and store your repos for verification.'));
|
|
97
|
+
console.log(chalk.dim('All data is deleted after 24 hours.'));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.log(chalk.dim('Sandbox mode is disabled. Automated verification will not work.'));
|
|
101
|
+
console.log(chalk.dim('Enable with: haystack config sandbox on'));
|
|
102
|
+
}
|
|
103
|
+
console.log(chalk.dim('(Stored in .haystack.json — applies to all repo contributors)'));
|
|
104
|
+
console.log();
|
|
105
|
+
}
|
|
106
|
+
export async function enableSandbox() {
|
|
107
|
+
setPreference('sandbox_enabled', true);
|
|
108
|
+
console.log(chalk.green('\nSandbox mode enabled.\n'));
|
|
109
|
+
console.log(chalk.dim('Haystack can now clone and store your repos for verification.'));
|
|
110
|
+
console.log(chalk.dim('All data is deleted after 24 hours.'));
|
|
111
|
+
console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
|
|
112
|
+
}
|
|
113
|
+
export async function disableSandbox() {
|
|
114
|
+
setPreference('sandbox_enabled', false);
|
|
115
|
+
console.log(chalk.yellow('\nSandbox mode disabled.\n'));
|
|
116
|
+
console.log(chalk.dim('Automated verification will not work until you re-enable sandbox mode.'));
|
|
117
|
+
console.log(chalk.dim('Re-enable with: haystack config sandbox on'));
|
|
118
|
+
console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
|
|
119
|
+
}
|
|
120
|
+
export async function handleSandbox(action) {
|
|
121
|
+
switch (action?.toLowerCase()) {
|
|
122
|
+
case 'on':
|
|
123
|
+
case 'enable':
|
|
124
|
+
case 'true':
|
|
125
|
+
return enableSandbox();
|
|
126
|
+
case 'off':
|
|
127
|
+
case 'disable':
|
|
128
|
+
case 'false':
|
|
129
|
+
return disableSandbox();
|
|
130
|
+
case 'status':
|
|
131
|
+
case undefined:
|
|
132
|
+
return getSandboxStatus();
|
|
133
|
+
default:
|
|
134
|
+
console.error(chalk.red(`\nUnknown action: ${action}`));
|
|
135
|
+
console.log('\nUsage: haystack config sandbox [on|off|status]\n');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Agentic tool (user-level via API)
|
|
141
|
+
// ============================================================================
|
|
142
|
+
export async function getAgenticToolStatus() {
|
|
31
143
|
const token = await requireAuth();
|
|
32
144
|
console.log(chalk.dim('\nFetching preferences...\n'));
|
|
33
145
|
try {
|
|
@@ -40,33 +152,28 @@ export async function getSandboxStatus() {
|
|
|
40
152
|
throw new Error(`Failed to get preferences: ${response.status}`);
|
|
41
153
|
}
|
|
42
154
|
const data = await response.json();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
: chalk.yellow('disabled'));
|
|
155
|
+
const currentTool = data.agentic_tool || 'opencode';
|
|
156
|
+
console.log(chalk.bold('Agentic tool:'), chalk.cyan(AGENTIC_TOOL_LABELS[currentTool]));
|
|
46
157
|
console.log();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
else {
|
|
52
|
-
console.log(chalk.dim('Sandbox mode is disabled. Automated verification will not work.'));
|
|
53
|
-
console.log(chalk.dim('Enable with: haystack config sandbox on'));
|
|
158
|
+
console.log(chalk.dim('Available options:'));
|
|
159
|
+
for (const [key, label] of Object.entries(AGENTIC_TOOL_LABELS)) {
|
|
160
|
+
const marker = key === currentTool ? chalk.green(' ✓ ') : ' ';
|
|
161
|
+
console.log(`${marker}${chalk.dim(key)} - ${label}`);
|
|
54
162
|
}
|
|
55
163
|
console.log();
|
|
164
|
+
console.log(chalk.dim('Change with: haystack config agentic-tool <tool>'));
|
|
165
|
+
console.log();
|
|
56
166
|
}
|
|
57
167
|
catch (error) {
|
|
58
168
|
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
59
169
|
process.exit(1);
|
|
60
170
|
}
|
|
61
171
|
}
|
|
62
|
-
|
|
63
|
-
* Enable sandbox mode
|
|
64
|
-
*/
|
|
65
|
-
export async function enableSandbox() {
|
|
172
|
+
export async function setAgenticTool(tool) {
|
|
66
173
|
const token = await requireAuth();
|
|
67
|
-
console.log(chalk.dim(
|
|
174
|
+
console.log(chalk.dim(`\nSetting agentic tool to ${tool}...`));
|
|
68
175
|
try {
|
|
69
|
-
const response = await apiRequest('PUT', token, {
|
|
176
|
+
const response = await apiRequest('PUT', token, { agentic_tool: tool });
|
|
70
177
|
if (!response.ok) {
|
|
71
178
|
if (response.status === 401) {
|
|
72
179
|
console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
|
|
@@ -75,59 +182,227 @@ export async function enableSandbox() {
|
|
|
75
182
|
const error = await response.json().catch(() => ({}));
|
|
76
183
|
throw new Error(error.error || `Failed to update preferences: ${response.status}`);
|
|
77
184
|
}
|
|
78
|
-
console.log(chalk.green(
|
|
79
|
-
|
|
80
|
-
|
|
185
|
+
console.log(chalk.green(`\nAgentic tool set to: ${AGENTIC_TOOL_LABELS[tool]}\n`));
|
|
186
|
+
if (tool === 'claude-code') {
|
|
187
|
+
console.log(chalk.dim('Note: Requires Claude Max subscription.'));
|
|
188
|
+
console.log(chalk.dim('Connect your account in the dashboard settings.\n'));
|
|
189
|
+
}
|
|
190
|
+
else if (tool === 'codex') {
|
|
191
|
+
console.log(chalk.dim('Note: Requires ChatGPT Plus, Pro, or Team subscription.'));
|
|
192
|
+
console.log(chalk.dim('Connect your account in the dashboard settings.\n'));
|
|
193
|
+
}
|
|
81
194
|
}
|
|
82
195
|
catch (error) {
|
|
83
196
|
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
84
197
|
process.exit(1);
|
|
85
198
|
}
|
|
86
199
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const token = await requireAuth();
|
|
92
|
-
console.log(chalk.dim('\nDisabling sandbox mode...'));
|
|
93
|
-
try {
|
|
94
|
-
const response = await apiRequest('PUT', token, { sandbox_enabled: false });
|
|
95
|
-
if (!response.ok) {
|
|
96
|
-
if (response.status === 401) {
|
|
97
|
-
console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
const error = await response.json().catch(() => ({}));
|
|
101
|
-
throw new Error(error.error || `Failed to update preferences: ${response.status}`);
|
|
102
|
-
}
|
|
103
|
-
console.log(chalk.yellow('\nSandbox mode disabled.\n'));
|
|
104
|
-
console.log(chalk.dim('Automated verification will not work until you re-enable sandbox mode.'));
|
|
105
|
-
console.log(chalk.dim('Re-enable with: haystack config sandbox on\n'));
|
|
200
|
+
export async function handleAgenticTool(tool) {
|
|
201
|
+
const validTools = ['opencode', 'claude-code', 'codex'];
|
|
202
|
+
if (!tool || tool === 'status') {
|
|
203
|
+
return getAgenticToolStatus();
|
|
106
204
|
}
|
|
107
|
-
|
|
108
|
-
|
|
205
|
+
const normalizedTool = tool.toLowerCase();
|
|
206
|
+
if (!validTools.includes(normalizedTool)) {
|
|
207
|
+
console.error(chalk.red(`\nUnknown tool: ${tool}`));
|
|
208
|
+
console.log('\nValid options:');
|
|
209
|
+
for (const [key, label] of Object.entries(AGENTIC_TOOL_LABELS)) {
|
|
210
|
+
console.log(` ${chalk.cyan(key)} - ${label}`);
|
|
211
|
+
}
|
|
212
|
+
console.log('\nUsage: haystack config agentic-tool [opencode|claude-code|codex|status]\n');
|
|
109
213
|
process.exit(1);
|
|
110
214
|
}
|
|
215
|
+
return setAgenticTool(normalizedTool);
|
|
111
216
|
}
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// Auto-merge (repo-level via .haystack.json)
|
|
219
|
+
// ============================================================================
|
|
112
220
|
/**
|
|
113
|
-
*
|
|
221
|
+
* Check if auto-merge is enabled for the current repo.
|
|
222
|
+
* Returns false if .haystack.json is missing or on error.
|
|
114
223
|
*/
|
|
115
|
-
export async function
|
|
224
|
+
export async function isAutoMergeEnabled() {
|
|
225
|
+
try {
|
|
226
|
+
return getPreference('auto_merge');
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export async function getAutoMergeStatus() {
|
|
233
|
+
const enabled = getPreference('auto_merge');
|
|
234
|
+
console.log(chalk.bold('\nAuto-merge:'), enabled
|
|
235
|
+
? chalk.green('enabled')
|
|
236
|
+
: chalk.yellow('disabled'));
|
|
237
|
+
console.log();
|
|
238
|
+
if (enabled) {
|
|
239
|
+
console.log(chalk.dim('PRs submitted via `haystack submit` will be auto-merged'));
|
|
240
|
+
console.log(chalk.dim('when analysis finds no issues (safe to merge).'));
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
console.log(chalk.dim('Auto-merge is disabled. PRs require manual merge.'));
|
|
244
|
+
console.log(chalk.dim('Enable with: haystack config auto-merge on'));
|
|
245
|
+
}
|
|
246
|
+
console.log(chalk.dim('(Stored in .haystack.json — applies to all repo contributors)'));
|
|
247
|
+
console.log();
|
|
248
|
+
}
|
|
249
|
+
export async function enableAutoMerge() {
|
|
250
|
+
setPreference('auto_merge', true);
|
|
251
|
+
console.log(chalk.green('\nAuto-merge enabled.\n'));
|
|
252
|
+
console.log(chalk.dim('PRs submitted via `haystack submit` will be auto-merged'));
|
|
253
|
+
console.log(chalk.dim('when analysis finds no issues (safe to merge).'));
|
|
254
|
+
console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
|
|
255
|
+
}
|
|
256
|
+
export async function disableAutoMerge() {
|
|
257
|
+
setPreference('auto_merge', false);
|
|
258
|
+
console.log(chalk.yellow('\nAuto-merge disabled.\n'));
|
|
259
|
+
console.log(chalk.dim('PRs will require manual merge after analysis.'));
|
|
260
|
+
console.log(chalk.dim('Re-enable with: haystack config auto-merge on'));
|
|
261
|
+
console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
|
|
262
|
+
}
|
|
263
|
+
export async function handleAutoMerge(action) {
|
|
116
264
|
switch (action?.toLowerCase()) {
|
|
117
265
|
case 'on':
|
|
118
266
|
case 'enable':
|
|
119
267
|
case 'true':
|
|
120
|
-
return
|
|
268
|
+
return enableAutoMerge();
|
|
121
269
|
case 'off':
|
|
122
270
|
case 'disable':
|
|
123
271
|
case 'false':
|
|
124
|
-
return
|
|
272
|
+
return disableAutoMerge();
|
|
125
273
|
case 'status':
|
|
126
274
|
case undefined:
|
|
127
|
-
return
|
|
275
|
+
return getAutoMergeStatus();
|
|
128
276
|
default:
|
|
129
277
|
console.error(chalk.red(`\nUnknown action: ${action}`));
|
|
130
|
-
console.log('\nUsage: haystack config
|
|
278
|
+
console.log('\nUsage: haystack config auto-merge [on|off|status]\n');
|
|
131
279
|
process.exit(1);
|
|
132
280
|
}
|
|
133
281
|
}
|
|
282
|
+
// ============================================================================
|
|
283
|
+
// Wait-for-reviewers (repo-level via .haystack.json)
|
|
284
|
+
// ============================================================================
|
|
285
|
+
function getBabysitterConfig() {
|
|
286
|
+
const { config, path } = loadHaystackConfig();
|
|
287
|
+
const babysitter = config.babysitter ?? {};
|
|
288
|
+
const reviewers = babysitter.expected_ai_reviewers ?? [];
|
|
289
|
+
return { config, path, reviewers };
|
|
290
|
+
}
|
|
291
|
+
function saveBabysitterReviewers(config, configPath, reviewers) {
|
|
292
|
+
const babysitter = config.babysitter ?? {};
|
|
293
|
+
babysitter.expected_ai_reviewers = reviewers.length > 0 ? reviewers : undefined;
|
|
294
|
+
config.babysitter = babysitter;
|
|
295
|
+
saveHaystackConfig(config, configPath);
|
|
296
|
+
}
|
|
297
|
+
function printAvailableReviewers(currentReviewers) {
|
|
298
|
+
const currentSet = new Set(currentReviewers);
|
|
299
|
+
console.log(chalk.dim('Available AI reviewer sources:'));
|
|
300
|
+
for (const source of AI_REVIEWER_SOURCES) {
|
|
301
|
+
const name = AI_REVIEWER_DISPLAY_NAMES[source];
|
|
302
|
+
const active = currentSet.has(source);
|
|
303
|
+
const marker = active ? chalk.green(' ✓ ') : ' ';
|
|
304
|
+
console.log(`${marker}${chalk.cyan(source)} — ${name}`);
|
|
305
|
+
}
|
|
306
|
+
console.log();
|
|
307
|
+
}
|
|
308
|
+
function validateReviewerSources(sources) {
|
|
309
|
+
const validSet = new Set(AI_REVIEWER_SOURCES);
|
|
310
|
+
const invalid = sources.filter(s => !validSet.has(s));
|
|
311
|
+
if (invalid.length > 0) {
|
|
312
|
+
console.error(chalk.red(`\nUnknown reviewer source(s): ${invalid.join(', ')}`));
|
|
313
|
+
console.log(chalk.dim(`\nValid sources: ${AI_REVIEWER_SOURCES.join(', ')}`));
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
return sources;
|
|
317
|
+
}
|
|
318
|
+
export async function handleWaitForReviewers(action, reviewers) {
|
|
319
|
+
const normalizedAction = action?.toLowerCase();
|
|
320
|
+
// Default: show status
|
|
321
|
+
if (!normalizedAction || normalizedAction === 'list' || normalizedAction === 'status') {
|
|
322
|
+
const { reviewers: current } = getBabysitterConfig();
|
|
323
|
+
console.log(chalk.bold('\nMerge queue — expected AI reviewers\n'));
|
|
324
|
+
if (current.length > 0) {
|
|
325
|
+
console.log('The merge queue will wait for these external reviewers before merging:');
|
|
326
|
+
for (const source of current) {
|
|
327
|
+
const name = AI_REVIEWER_DISPLAY_NAMES[source] ?? source;
|
|
328
|
+
console.log(` ${chalk.green(name)} ${chalk.dim(`(${source})`)}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
console.log('No external AI reviewers configured.');
|
|
333
|
+
console.log(chalk.dim('The merge queue will not wait for any external reviewers.'));
|
|
334
|
+
}
|
|
335
|
+
console.log();
|
|
336
|
+
printAvailableReviewers(current);
|
|
337
|
+
console.log(chalk.dim('Add with: haystack config wait-for-reviewers add <source>'));
|
|
338
|
+
console.log();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (normalizedAction === 'add') {
|
|
342
|
+
if (!reviewers || reviewers.length === 0) {
|
|
343
|
+
console.error(chalk.red('\nSpecify at least one reviewer source to add.'));
|
|
344
|
+
const { reviewers: current } = getBabysitterConfig();
|
|
345
|
+
console.log();
|
|
346
|
+
printAvailableReviewers(current);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
const validated = validateReviewerSources(reviewers);
|
|
350
|
+
const { config, path, reviewers: current } = getBabysitterConfig();
|
|
351
|
+
const merged = Array.from(new Set([...current, ...validated]));
|
|
352
|
+
saveBabysitterReviewers(config, path, merged);
|
|
353
|
+
const added = validated.filter(s => !current.includes(s));
|
|
354
|
+
if (added.length > 0) {
|
|
355
|
+
console.log(chalk.green(`\nAdded: ${added.map(s => AI_REVIEWER_DISPLAY_NAMES[s] ?? s).join(', ')}`));
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
console.log(chalk.dim('\nAll specified reviewers were already configured.'));
|
|
359
|
+
}
|
|
360
|
+
console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (normalizedAction === 'remove') {
|
|
364
|
+
if (!reviewers || reviewers.length === 0) {
|
|
365
|
+
console.error(chalk.red('\nSpecify at least one reviewer source to remove.'));
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
const validated = validateReviewerSources(reviewers);
|
|
369
|
+
const { config, path, reviewers: current } = getBabysitterConfig();
|
|
370
|
+
const removeSet = new Set(validated);
|
|
371
|
+
const remaining = current.filter(s => !removeSet.has(s));
|
|
372
|
+
saveBabysitterReviewers(config, path, remaining);
|
|
373
|
+
const removed = validated.filter(s => current.includes(s));
|
|
374
|
+
if (removed.length > 0) {
|
|
375
|
+
console.log(chalk.yellow(`\nRemoved: ${removed.map(s => AI_REVIEWER_DISPLAY_NAMES[s] ?? s).join(', ')}`));
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
console.log(chalk.dim('\nNone of the specified reviewers were configured.'));
|
|
379
|
+
}
|
|
380
|
+
console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (normalizedAction === 'clear') {
|
|
384
|
+
const { config, path } = getBabysitterConfig();
|
|
385
|
+
saveBabysitterReviewers(config, path, []);
|
|
386
|
+
console.log(chalk.yellow('\nCleared all expected AI reviewers.'));
|
|
387
|
+
console.log(chalk.dim('The merge queue will not wait for any external reviewers.'));
|
|
388
|
+
console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// Unknown action — maybe the user passed a reviewer name directly
|
|
392
|
+
// Try treating action + reviewers as all reviewer names for 'add'
|
|
393
|
+
const allNames = [action, ...(reviewers ?? [])];
|
|
394
|
+
const validSet = new Set(AI_REVIEWER_SOURCES);
|
|
395
|
+
if (allNames.every(n => validSet.has(n))) {
|
|
396
|
+
// Treat as implicit 'add'
|
|
397
|
+
const validated = validateReviewerSources(allNames);
|
|
398
|
+
const { config, path, reviewers: current } = getBabysitterConfig();
|
|
399
|
+
const merged = Array.from(new Set([...current, ...validated]));
|
|
400
|
+
saveBabysitterReviewers(config, path, merged);
|
|
401
|
+
console.log(chalk.green(`\nAdded: ${validated.map(s => AI_REVIEWER_DISPLAY_NAMES[s] ?? s).join(', ')}`));
|
|
402
|
+
console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
console.error(chalk.red(`\nUnknown action: ${action}`));
|
|
406
|
+
console.log('\nUsage: haystack config wait-for-reviewers [list|add|remove|clear] [sources...]\n');
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
interface HooksInstallOptions {
|
|
10
|
+
version?: string;
|
|
11
|
+
force?: boolean;
|
|
12
|
+
skipEntire?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function hooksInstall(options: HooksInstallOptions): Promise<void>;
|
|
15
|
+
export declare function hooksStatus(): Promise<void>;
|
|
16
|
+
export declare function hooksUpdate(): Promise<void>;
|
|
17
|
+
export {};
|