@haystackeditor/cli 0.11.0 → 0.12.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 +9 -12
- package/dist/assets/hooks/agent-context/detect.ts +180 -0
- package/dist/assets/hooks/agent-context/format.ts +1 -0
- package/dist/assets/hooks/agent-context/index.ts +2 -0
- package/dist/assets/hooks/agent-context/parsers/claude.ts +14 -5
- package/dist/assets/hooks/agent-context/parsers/codex.ts +416 -0
- package/dist/assets/hooks/agent-context/tsconfig.json +1 -0
- package/dist/assets/hooks/agent-context/types.ts +1 -1
- package/dist/assets/hooks/package.json +2 -1
- package/dist/assets/hooks/scripts/pre-commit.sh +80 -2
- package/dist/assets/hooks/scripts/pre-push.sh +1 -1
- package/dist/assets/skills/submit.md +20 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.js +15 -5
- package/dist/commands/install-session-hooks.d.ts +3 -2
- package/dist/commands/install-session-hooks.js +27 -11
- package/dist/commands/pr-status.d.ts +5 -0
- package/dist/commands/pr-status.js +2 -2
- package/dist/commands/setup.js +21 -10
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.js +11 -18
- package/dist/commands/triage.d.ts +16 -4
- package/dist/commands/triage.js +210 -203
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30 -32
- package/dist/triage/traces.js +9 -3
- package/dist/types.d.ts +14 -0
- package/dist/types.js +2 -0
- package/package.json +1 -1
- package/dist/commands/check-pending.d.ts +0 -19
- package/dist/commands/check-pending.js +0 -217
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* npx @haystackeditor/cli status # Check configuration
|
|
12
12
|
* npx @haystackeditor/cli login # Authenticate with GitHub
|
|
13
13
|
* npx @haystackeditor/cli submit # Create a PR (auto-merge or review)
|
|
14
|
-
* npx @haystackeditor/cli
|
|
14
|
+
* npx @haystackeditor/cli triage # Check last submitted PR
|
|
15
15
|
* npx @haystackeditor/cli triage 123 # View analysis results for a PR
|
|
16
16
|
* npx @haystackeditor/cli dismiss 123 # Dismiss findings for a PR
|
|
17
17
|
* npx @haystackeditor/cli mark-reviewed 123 # Mark review as not needed
|
|
@@ -23,11 +23,10 @@ import { Command } from 'commander';
|
|
|
23
23
|
import { statusCommand } from './commands/status.js';
|
|
24
24
|
import { initCommand } from './commands/init.js';
|
|
25
25
|
import { loginCommand, logoutCommand } from './commands/login.js';
|
|
26
|
-
import { handleAgenticTool, handleAutoMerge, handleWaitForReviewers } from './commands/config.js';
|
|
26
|
+
import { handleAgenticTool, handleAutoMerge, handleWaitForReviewers, isAutoMergeEnabled } from './commands/config.js';
|
|
27
27
|
import { installSkills, listSkills } from './commands/skills.js';
|
|
28
28
|
import { hooksInstall, hooksStatus, hooksUpdate } from './commands/hooks.js';
|
|
29
29
|
import { submitCommand } from './commands/submit.js';
|
|
30
|
-
import { checkPendingCommand } from './commands/check-pending.js';
|
|
31
30
|
import { installSessionHooks, sessionHooksStatus } from './commands/install-session-hooks.js';
|
|
32
31
|
import { listPolicies, addPolicy, removePolicy, initPolicies, addInstruction } from './commands/policy.js';
|
|
33
32
|
import { triageCommand } from './commands/triage.js';
|
|
@@ -95,7 +94,8 @@ program
|
|
|
95
94
|
.option('--draft', 'Create as draft PR')
|
|
96
95
|
.option('--review [reviewer]', 'Request human review (BLOCKS auto-merge — only use when explicitly asked)')
|
|
97
96
|
.option('--force', 'Skip pre-PR triage checks')
|
|
98
|
-
.option('--auto-fix', '
|
|
97
|
+
.option('--auto-fix', 'Alpha auto-fix for straightforward mechanical issues (discouraged)')
|
|
98
|
+
.option('--auto-merge', 'Apply auto-merge label (default: from .haystack.json, --no-auto-merge to disable)')
|
|
99
99
|
.option('--no-wait', 'Skip waiting for analysis results')
|
|
100
100
|
.addHelpText('after', `
|
|
101
101
|
This command is designed for AI coding agents to submit PRs.
|
|
@@ -113,12 +113,17 @@ Pre-PR Triage:
|
|
|
113
113
|
|
|
114
114
|
Use --force to skip triage entirely.
|
|
115
115
|
Use --no-wait to skip waiting for analysis results.
|
|
116
|
-
Use --auto-fix
|
|
116
|
+
Use --auto-fix only when explicitly opting into the alpha fixer for
|
|
117
|
+
straightforward mechanical issues.
|
|
117
118
|
|
|
118
119
|
Review Routing:
|
|
119
120
|
By default, PRs enter the auto-merge queue and merge automatically once
|
|
120
121
|
Haystack analysis passes. This is the correct default for most PRs.
|
|
121
122
|
|
|
123
|
+
⚠ --auto-fix is currently alpha and discouraged by default. Prefer plain
|
|
124
|
+
haystack submit unless you explicitly want the sandbox fixer to attempt
|
|
125
|
+
straightforward mechanical fixes before issues hit the Feed.
|
|
126
|
+
|
|
122
127
|
⚠ Only use --review when the user EXPLICITLY asks for human review.
|
|
123
128
|
--review BLOCKS auto-merge — the PR will NOT merge until a human approves,
|
|
124
129
|
which delays merging. Do not add --review "just to be safe".
|
|
@@ -128,8 +133,8 @@ Review Routing:
|
|
|
128
133
|
• --review <username> Same label, plus requests review from that GitHub user
|
|
129
134
|
|
|
130
135
|
Examples:
|
|
131
|
-
haystack submit
|
|
132
|
-
haystack submit
|
|
136
|
+
haystack submit # Recommended: triage, create PR, auto-merge queue
|
|
137
|
+
haystack submit --auto-fix # Discouraged alpha: auto-fix straightforward mechanical issues
|
|
133
138
|
haystack submit --force # Skip triage
|
|
134
139
|
haystack submit --no-wait # Don't wait for analysis
|
|
135
140
|
haystack submit --title "Fix auth" # Custom PR title
|
|
@@ -141,38 +146,28 @@ Examples:
|
|
|
141
146
|
haystack submit --review # ⚠ Blocks auto-merge, needs human approval
|
|
142
147
|
haystack submit --review octocat # ⚠ Blocks auto-merge, requests review from octocat
|
|
143
148
|
`)
|
|
144
|
-
.action(
|
|
145
|
-
|
|
146
|
-
.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.addHelpText('after', `
|
|
152
|
-
After \`haystack submit\`, this checks whether the analysis finished
|
|
153
|
-
and shows a two-state verdict:
|
|
154
|
-
|
|
155
|
-
Good to merge — No issues found, safe to merge
|
|
156
|
-
Needs your input — Bugs or rule violations found
|
|
157
|
-
|
|
158
|
-
This runs automatically on CLI session start if session hooks are installed.
|
|
159
|
-
|
|
160
|
-
Examples:
|
|
161
|
-
haystack check-pending # Rich terminal output
|
|
162
|
-
haystack check-pending --hook # Minimal output (for hooks)
|
|
163
|
-
haystack check-pending --json # Machine-readable JSON
|
|
164
|
-
haystack check-pending --clear # Clear pending state
|
|
165
|
-
`)
|
|
166
|
-
.action(checkPendingCommand);
|
|
149
|
+
.action(async (options) => {
|
|
150
|
+
// Resolve --auto-merge default from .haystack.json when not explicitly set
|
|
151
|
+
if (options.autoMerge === undefined) {
|
|
152
|
+
options.autoMerge = await isAutoMergeEnabled();
|
|
153
|
+
}
|
|
154
|
+
return submitCommand(options);
|
|
155
|
+
});
|
|
167
156
|
program
|
|
168
|
-
.command('triage
|
|
157
|
+
.command('triage [pr]')
|
|
169
158
|
.description('View Haystack analysis results for a PR')
|
|
170
159
|
.option('--json', 'Output as JSON')
|
|
160
|
+
.option('--hook', 'Minimal single-line output for session-start hooks')
|
|
161
|
+
.option('--clear', 'Clear pending submit state')
|
|
171
162
|
.option('--no-wait', 'Exit immediately if analysis is still pending')
|
|
172
163
|
.addHelpText('after', `
|
|
173
164
|
Look up triage results (bugs, rule violations, verdict) for any PR.
|
|
165
|
+
When called without a PR identifier, checks the last submitted PR.
|
|
166
|
+
|
|
167
|
+
Runs automatically on CLI session start if session hooks are installed.
|
|
174
168
|
|
|
175
169
|
PR identifier formats:
|
|
170
|
+
(none) Last submitted PR
|
|
176
171
|
123 PR number (uses current repo)
|
|
177
172
|
#123 PR number with hash
|
|
178
173
|
owner/repo#123 Fully qualified
|
|
@@ -182,11 +177,14 @@ By default, if analysis is still in progress, the command will poll
|
|
|
182
177
|
for up to 5 minutes. Use --no-wait to exit immediately instead.
|
|
183
178
|
|
|
184
179
|
Examples:
|
|
180
|
+
haystack triage # Last submitted PR
|
|
185
181
|
haystack triage 42 # Current repo, PR #42
|
|
186
182
|
haystack triage acme/widgets#99 # Specific repo
|
|
187
183
|
haystack triage https://github.com/o/r/pull/1 # From URL
|
|
188
184
|
haystack triage 42 --json # Machine-readable output
|
|
189
185
|
haystack triage 42 --no-wait # Don't wait if pending
|
|
186
|
+
haystack triage --hook # Session-start hook output
|
|
187
|
+
haystack triage --clear # Clear pending state
|
|
190
188
|
`)
|
|
191
189
|
.action(triageCommand);
|
|
192
190
|
program
|
|
@@ -429,7 +427,7 @@ hooks
|
|
|
429
427
|
.description('Install session-start hooks for coding CLIs')
|
|
430
428
|
.option('--cli <name>', 'Target CLI: claude, codex, gemini, or all')
|
|
431
429
|
.addHelpText('after', `
|
|
432
|
-
Configures your coding CLI to run \`haystack
|
|
430
|
+
Configures your coding CLI to run \`haystack triage --hook\` on session start.
|
|
433
431
|
This shows pending PR analysis results when you open a new terminal session.
|
|
434
432
|
|
|
435
433
|
Claude Code: Native SessionStart hook (.claude/settings.json)
|
package/dist/triage/traces.js
CHANGED
|
@@ -146,7 +146,10 @@ function extractHumanText(content) {
|
|
|
146
146
|
return null;
|
|
147
147
|
const textParts = [];
|
|
148
148
|
for (const block of content) {
|
|
149
|
-
|
|
149
|
+
// Support both old ({type: "text", text}) and new ({id, text}) content blocks
|
|
150
|
+
if (typeof block?.text !== 'string')
|
|
151
|
+
continue;
|
|
152
|
+
if (block.type && block.type !== 'text')
|
|
150
153
|
continue;
|
|
151
154
|
const stripped = stripWrappers(block.text);
|
|
152
155
|
if (stripped && !isSlashCommand(stripped)) {
|
|
@@ -231,9 +234,12 @@ function extractUserMessages(tracePath) {
|
|
|
231
234
|
}
|
|
232
235
|
const entries = parseJsonl(content, tracePath);
|
|
233
236
|
for (const entry of entries) {
|
|
234
|
-
|
|
237
|
+
// Support both old format (entry.message.role === 'user') and new (entry.type === 'user')
|
|
238
|
+
const role = entry.message?.role ?? entry?.type;
|
|
239
|
+
if (role !== 'user')
|
|
235
240
|
continue;
|
|
236
|
-
|
|
241
|
+
// Support both old format ({message: {content}}) and new ({content} at top level)
|
|
242
|
+
const msg = entry.message?.content ?? entry.content;
|
|
237
243
|
if (!msg)
|
|
238
244
|
continue;
|
|
239
245
|
const text = extractHumanText(msg);
|
package/dist/types.d.ts
CHANGED
|
@@ -84,14 +84,18 @@ declare const MergeQueueConfigSchema: z.ZodObject<{
|
|
|
84
84
|
min_batch_size: z.ZodOptional<z.ZodNumber>;
|
|
85
85
|
/** Maximum PRs per batch (cap: 20) */
|
|
86
86
|
max_batch_size: z.ZodOptional<z.ZodNumber>;
|
|
87
|
+
/** Seconds to wait after grace period for more PRs to accumulate (default: 30, 0 to disable) */
|
|
88
|
+
batch_window_seconds: z.ZodOptional<z.ZodNumber>;
|
|
87
89
|
}, "strip", z.ZodTypeAny, {
|
|
88
90
|
enabled?: boolean | undefined;
|
|
89
91
|
min_batch_size?: number | undefined;
|
|
90
92
|
max_batch_size?: number | undefined;
|
|
93
|
+
batch_window_seconds?: number | undefined;
|
|
91
94
|
}, {
|
|
92
95
|
enabled?: boolean | undefined;
|
|
93
96
|
min_batch_size?: number | undefined;
|
|
94
97
|
max_batch_size?: number | undefined;
|
|
98
|
+
batch_window_seconds?: number | undefined;
|
|
95
99
|
}>>;
|
|
96
100
|
/** Wait for external AI code reviewers before merging — all bots must post */
|
|
97
101
|
wait_for_reviewer: z.ZodOptional<z.ZodObject<{
|
|
@@ -117,6 +121,7 @@ declare const MergeQueueConfigSchema: z.ZodObject<{
|
|
|
117
121
|
enabled?: boolean | undefined;
|
|
118
122
|
min_batch_size?: number | undefined;
|
|
119
123
|
max_batch_size?: number | undefined;
|
|
124
|
+
batch_window_seconds?: number | undefined;
|
|
120
125
|
} | undefined;
|
|
121
126
|
wait_for_reviewer?: {
|
|
122
127
|
bots: string[];
|
|
@@ -133,6 +138,7 @@ declare const MergeQueueConfigSchema: z.ZodObject<{
|
|
|
133
138
|
enabled?: boolean | undefined;
|
|
134
139
|
min_batch_size?: number | undefined;
|
|
135
140
|
max_batch_size?: number | undefined;
|
|
141
|
+
batch_window_seconds?: number | undefined;
|
|
136
142
|
} | undefined;
|
|
137
143
|
wait_for_reviewer?: {
|
|
138
144
|
bots: string[];
|
|
@@ -1182,14 +1188,18 @@ export declare const HaystackConfigSchema: z.ZodObject<{
|
|
|
1182
1188
|
min_batch_size: z.ZodOptional<z.ZodNumber>;
|
|
1183
1189
|
/** Maximum PRs per batch (cap: 20) */
|
|
1184
1190
|
max_batch_size: z.ZodOptional<z.ZodNumber>;
|
|
1191
|
+
/** Seconds to wait after grace period for more PRs to accumulate (default: 30, 0 to disable) */
|
|
1192
|
+
batch_window_seconds: z.ZodOptional<z.ZodNumber>;
|
|
1185
1193
|
}, "strip", z.ZodTypeAny, {
|
|
1186
1194
|
enabled?: boolean | undefined;
|
|
1187
1195
|
min_batch_size?: number | undefined;
|
|
1188
1196
|
max_batch_size?: number | undefined;
|
|
1197
|
+
batch_window_seconds?: number | undefined;
|
|
1189
1198
|
}, {
|
|
1190
1199
|
enabled?: boolean | undefined;
|
|
1191
1200
|
min_batch_size?: number | undefined;
|
|
1192
1201
|
max_batch_size?: number | undefined;
|
|
1202
|
+
batch_window_seconds?: number | undefined;
|
|
1193
1203
|
}>>;
|
|
1194
1204
|
/** Wait for external AI code reviewers before merging — all bots must post */
|
|
1195
1205
|
wait_for_reviewer: z.ZodOptional<z.ZodObject<{
|
|
@@ -1215,6 +1225,7 @@ export declare const HaystackConfigSchema: z.ZodObject<{
|
|
|
1215
1225
|
enabled?: boolean | undefined;
|
|
1216
1226
|
min_batch_size?: number | undefined;
|
|
1217
1227
|
max_batch_size?: number | undefined;
|
|
1228
|
+
batch_window_seconds?: number | undefined;
|
|
1218
1229
|
} | undefined;
|
|
1219
1230
|
wait_for_reviewer?: {
|
|
1220
1231
|
bots: string[];
|
|
@@ -1231,6 +1242,7 @@ export declare const HaystackConfigSchema: z.ZodObject<{
|
|
|
1231
1242
|
enabled?: boolean | undefined;
|
|
1232
1243
|
min_batch_size?: number | undefined;
|
|
1233
1244
|
max_batch_size?: number | undefined;
|
|
1245
|
+
batch_window_seconds?: number | undefined;
|
|
1234
1246
|
} | undefined;
|
|
1235
1247
|
wait_for_reviewer?: {
|
|
1236
1248
|
bots: string[];
|
|
@@ -1251,6 +1263,7 @@ export declare const HaystackConfigSchema: z.ZodObject<{
|
|
|
1251
1263
|
enabled?: boolean | undefined;
|
|
1252
1264
|
min_batch_size?: number | undefined;
|
|
1253
1265
|
max_batch_size?: number | undefined;
|
|
1266
|
+
batch_window_seconds?: number | undefined;
|
|
1254
1267
|
} | undefined;
|
|
1255
1268
|
wait_for_reviewer?: {
|
|
1256
1269
|
bots: string[];
|
|
@@ -1372,6 +1385,7 @@ export declare const HaystackConfigSchema: z.ZodObject<{
|
|
|
1372
1385
|
enabled?: boolean | undefined;
|
|
1373
1386
|
min_batch_size?: number | undefined;
|
|
1374
1387
|
max_batch_size?: number | undefined;
|
|
1388
|
+
batch_window_seconds?: number | undefined;
|
|
1375
1389
|
} | undefined;
|
|
1376
1390
|
wait_for_reviewer?: {
|
|
1377
1391
|
bots: string[];
|
package/dist/types.js
CHANGED
|
@@ -138,6 +138,8 @@ const MergeQueueConfigSchema = z.object({
|
|
|
138
138
|
min_batch_size: z.number().optional(),
|
|
139
139
|
/** Maximum PRs per batch (cap: 20) */
|
|
140
140
|
max_batch_size: z.number().optional(),
|
|
141
|
+
/** Seconds to wait after grace period for more PRs to accumulate (default: 30, 0 to disable) */
|
|
142
|
+
batch_window_seconds: z.number().optional(),
|
|
141
143
|
}).optional(),
|
|
142
144
|
/** Wait for external AI code reviewers before merging — all bots must post */
|
|
143
145
|
wait_for_reviewer: z.object({
|
package/package.json
CHANGED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* check-pending command — Check status of pending Haystack analysis.
|
|
3
|
-
*
|
|
4
|
-
* Reads `.haystack/pending-submit.json`, polls the analysis API if needed,
|
|
5
|
-
* and outputs a two-state verdict:
|
|
6
|
-
* - "Good to merge" (no issues)
|
|
7
|
-
* - "Needs your input" (issues found)
|
|
8
|
-
*
|
|
9
|
-
* Output modes:
|
|
10
|
-
* --hook Minimal single-line for session-start hooks
|
|
11
|
-
* --json Machine-readable JSON
|
|
12
|
-
* (default) Rich terminal output
|
|
13
|
-
*/
|
|
14
|
-
export interface CheckPendingOptions {
|
|
15
|
-
json?: boolean;
|
|
16
|
-
hook?: boolean;
|
|
17
|
-
clear?: boolean;
|
|
18
|
-
}
|
|
19
|
-
export declare function checkPendingCommand(options: CheckPendingOptions): Promise<void>;
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* check-pending command — Check status of pending Haystack analysis.
|
|
3
|
-
*
|
|
4
|
-
* Reads `.haystack/pending-submit.json`, polls the analysis API if needed,
|
|
5
|
-
* and outputs a two-state verdict:
|
|
6
|
-
* - "Good to merge" (no issues)
|
|
7
|
-
* - "Needs your input" (issues found)
|
|
8
|
-
*
|
|
9
|
-
* Output modes:
|
|
10
|
-
* --hook Minimal single-line for session-start hooks
|
|
11
|
-
* --json Machine-readable JSON
|
|
12
|
-
* (default) Rich terminal output
|
|
13
|
-
*/
|
|
14
|
-
import chalk from 'chalk';
|
|
15
|
-
import { loadPendingSubmit, updatePendingSubmit, clearPendingSubmit } from '../utils/pending-state.js';
|
|
16
|
-
import { checkAnalysisReady, fetchAnalysisResults } from '../utils/analysis-api.js';
|
|
17
|
-
import { findExistingPR } from '../utils/github-api.js';
|
|
18
|
-
import { loadToken } from './login.js';
|
|
19
|
-
// ============================================================================
|
|
20
|
-
// Polling
|
|
21
|
-
// ============================================================================
|
|
22
|
-
const POLL_INTERVAL_MS = 3_000;
|
|
23
|
-
const POLL_TIMEOUT_MS = 30_000;
|
|
24
|
-
/**
|
|
25
|
-
* Poll the result endpoint until analysis is ready or timeout.
|
|
26
|
-
*/
|
|
27
|
-
async function waitForAnalysis(owner, repo, prNumber, timeoutMs, token) {
|
|
28
|
-
const deadline = Date.now() + timeoutMs;
|
|
29
|
-
let consecutiveErrors = 0;
|
|
30
|
-
while (Date.now() < deadline) {
|
|
31
|
-
const result = await checkAnalysisReady(owner, repo, prNumber, token);
|
|
32
|
-
if (result.status === 'ready')
|
|
33
|
-
return 'completed';
|
|
34
|
-
if (result.status === 'error') {
|
|
35
|
-
consecutiveErrors++;
|
|
36
|
-
if (consecutiveErrors >= 5)
|
|
37
|
-
return 'failed';
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
consecutiveErrors = 0;
|
|
41
|
-
}
|
|
42
|
-
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
43
|
-
}
|
|
44
|
-
return 'pending';
|
|
45
|
-
}
|
|
46
|
-
// ============================================================================
|
|
47
|
-
// Output formatting
|
|
48
|
-
// ============================================================================
|
|
49
|
-
function printHookOutput(pending) {
|
|
50
|
-
if (!pending.analysisResult) {
|
|
51
|
-
console.log(`[Haystack] PR #${pending.prNumber}: Analysis in progress...`);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const result = pending.analysisResult;
|
|
55
|
-
if (result.state === 'good-to-merge') {
|
|
56
|
-
if (result.autoMerged) {
|
|
57
|
-
console.log(`[Haystack] \u2705 PR #${pending.prNumber} "${getBranchLabel(pending)}": Auto-merged`);
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
console.log(`[Haystack] \u2705 PR #${pending.prNumber} "${getBranchLabel(pending)}": Good to merge`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
const parts = [];
|
|
65
|
-
if (result.bugCount > 0)
|
|
66
|
-
parts.push(`${result.bugCount} bug(s)`);
|
|
67
|
-
if (result.ruleViolationCount > 0)
|
|
68
|
-
parts.push(`${result.ruleViolationCount} rule violation(s)`);
|
|
69
|
-
if (parts.length === 0)
|
|
70
|
-
parts.push(`${result.warningCount} warning(s)`);
|
|
71
|
-
console.log(`[Haystack] \u26A0\uFE0F PR #${pending.prNumber} "${getBranchLabel(pending)}": Needs your input (${parts.join(', ')})`);
|
|
72
|
-
console.log(` Review: ${result.reviewUrl}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function printRichOutput(pending) {
|
|
76
|
-
console.log(chalk.bold('\n' + '\u2501'.repeat(50)));
|
|
77
|
-
console.log(chalk.bold(` Haystack Analysis: PR #${pending.prNumber}`));
|
|
78
|
-
console.log(chalk.bold('\u2501'.repeat(50) + '\n'));
|
|
79
|
-
console.log(` ${chalk.dim('Branch:')} ${pending.branch} \u2192 ${pending.baseBranch}`);
|
|
80
|
-
console.log(` ${chalk.dim('URL:')} ${chalk.cyan(pending.prUrl)}`);
|
|
81
|
-
if (!pending.analysisResult) {
|
|
82
|
-
if (pending.status === 'polling') {
|
|
83
|
-
console.log(` ${chalk.dim('Status:')} ${chalk.yellow('Analysis in progress...')}`);
|
|
84
|
-
}
|
|
85
|
-
else if (pending.status === 'failed') {
|
|
86
|
-
console.log(` ${chalk.dim('Status:')} ${chalk.red('Analysis failed')}`);
|
|
87
|
-
}
|
|
88
|
-
else if (pending.status === 'stale') {
|
|
89
|
-
console.log(` ${chalk.dim('Status:')} ${chalk.dim('Stale (submitted > 2h ago)')}`);
|
|
90
|
-
}
|
|
91
|
-
console.log('');
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const result = pending.analysisResult;
|
|
95
|
-
if (result.state === 'good-to-merge') {
|
|
96
|
-
if (result.autoMerged) {
|
|
97
|
-
console.log(` ${chalk.dim('Verdict:')} ${chalk.green.bold('Auto-merged')}`);
|
|
98
|
-
console.log(` ${chalk.dim('Issues:')} ${chalk.green('None — PR was automatically merged')}`);
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
console.log(` ${chalk.dim('Verdict:')} ${chalk.green.bold('Good to merge')}`);
|
|
102
|
-
console.log(` ${chalk.dim('Issues:')} ${chalk.green('None')}`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
console.log(` ${chalk.dim('Verdict:')} ${chalk.yellow.bold('Needs your input')}`);
|
|
107
|
-
console.log(` ${chalk.dim('Issues:')} ${result.summary}`);
|
|
108
|
-
console.log('');
|
|
109
|
-
for (const issue of result.issues) {
|
|
110
|
-
const icon = issue.severity === 'error' ? chalk.red('\u2717') : chalk.yellow('!');
|
|
111
|
-
const loc = issue.line ? `${issue.file}:${issue.line}` : issue.file;
|
|
112
|
-
console.log(` ${icon} ${chalk.dim(loc)} ${issue.message}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
console.log(`\n ${chalk.dim('Review:')} ${chalk.cyan(result.reviewUrl)}`);
|
|
116
|
-
console.log('');
|
|
117
|
-
}
|
|
118
|
-
function printJsonOutput(pending) {
|
|
119
|
-
const output = {
|
|
120
|
-
prNumber: pending.prNumber,
|
|
121
|
-
prUrl: pending.prUrl,
|
|
122
|
-
branch: pending.branch,
|
|
123
|
-
baseBranch: pending.baseBranch,
|
|
124
|
-
status: pending.status,
|
|
125
|
-
submittedAt: pending.submittedAt,
|
|
126
|
-
verdict: pending.analysisResult?.state || null,
|
|
127
|
-
bugCount: pending.analysisResult?.bugCount || 0,
|
|
128
|
-
warningCount: pending.analysisResult?.warningCount || 0,
|
|
129
|
-
ruleViolationCount: pending.analysisResult?.ruleViolationCount || 0,
|
|
130
|
-
summary: pending.analysisResult?.summary || null,
|
|
131
|
-
issues: pending.analysisResult?.issues || [],
|
|
132
|
-
reviewUrl: pending.analysisResult?.reviewUrl || null,
|
|
133
|
-
autoMerged: pending.analysisResult?.autoMerged || false,
|
|
134
|
-
};
|
|
135
|
-
console.log(JSON.stringify(output, null, 2));
|
|
136
|
-
}
|
|
137
|
-
function getBranchLabel(pending) {
|
|
138
|
-
return pending.branch.replace(/^refs\/heads\//, '');
|
|
139
|
-
}
|
|
140
|
-
// ============================================================================
|
|
141
|
-
// Main command
|
|
142
|
-
// ============================================================================
|
|
143
|
-
export async function checkPendingCommand(options) {
|
|
144
|
-
// Handle --clear
|
|
145
|
-
if (options.clear) {
|
|
146
|
-
clearPendingSubmit();
|
|
147
|
-
if (!options.hook) {
|
|
148
|
-
console.log(chalk.dim('Pending submit state cleared.'));
|
|
149
|
-
}
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
// Load pending state
|
|
153
|
-
const pending = loadPendingSubmit();
|
|
154
|
-
if (!pending) {
|
|
155
|
-
if (options.json) {
|
|
156
|
-
console.log(JSON.stringify({ status: 'none' }));
|
|
157
|
-
}
|
|
158
|
-
else if (!options.hook) {
|
|
159
|
-
// Silent for hook mode — no pending submits is fine
|
|
160
|
-
console.log(chalk.dim('No pending submits.\n'));
|
|
161
|
-
}
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
// Load token for private repo auth (best-effort, don't block if unavailable)
|
|
165
|
-
const token = await loadToken() || undefined;
|
|
166
|
-
// If still polling, try to resolve it
|
|
167
|
-
if (pending.status === 'polling') {
|
|
168
|
-
// Quick check: is the PR still open?
|
|
169
|
-
try {
|
|
170
|
-
const pr = await findExistingPR(pending.owner, pending.repo, pending.branch);
|
|
171
|
-
if (!pr) {
|
|
172
|
-
// PR was closed or merged
|
|
173
|
-
updatePendingSubmit({ status: 'stale', resolvedAt: new Date().toISOString() });
|
|
174
|
-
pending.status = 'stale';
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
// Auth not available or API error — continue with polling
|
|
179
|
-
}
|
|
180
|
-
if (pending.status === 'polling') {
|
|
181
|
-
// For hook mode, do a single quick poll (no waiting)
|
|
182
|
-
const timeout = options.hook ? 5_000 : POLL_TIMEOUT_MS;
|
|
183
|
-
const result = await waitForAnalysis(pending.owner, pending.repo, pending.prNumber, timeout, token);
|
|
184
|
-
if (result === 'completed') {
|
|
185
|
-
// Fetch the actual analysis results
|
|
186
|
-
try {
|
|
187
|
-
const verdict = await fetchAnalysisResults(pending.owner, pending.repo, pending.prNumber, token);
|
|
188
|
-
updatePendingSubmit({
|
|
189
|
-
status: 'completed',
|
|
190
|
-
analysisResult: verdict,
|
|
191
|
-
resolvedAt: new Date().toISOString(),
|
|
192
|
-
});
|
|
193
|
-
pending.status = 'completed';
|
|
194
|
-
pending.analysisResult = verdict;
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
// Could not fetch results — leave as polling
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
else if (result === 'failed') {
|
|
201
|
-
updatePendingSubmit({ status: 'failed', resolvedAt: new Date().toISOString() });
|
|
202
|
-
pending.status = 'failed';
|
|
203
|
-
}
|
|
204
|
-
// 'pending' — leave as-is for next check
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
// Output based on mode
|
|
208
|
-
if (options.json) {
|
|
209
|
-
printJsonOutput(pending);
|
|
210
|
-
}
|
|
211
|
-
else if (options.hook) {
|
|
212
|
-
printHookOutput(pending);
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
printRichOutput(pending);
|
|
216
|
-
}
|
|
217
|
-
}
|