@haystackeditor/cli 0.10.3 → 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 +15 -16
- 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 +94 -69
- package/dist/commands/dismiss.js +2 -1
- 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/request-review.d.ts +15 -0
- package/dist/commands/request-review.js +95 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +285 -2
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.js +12 -19
- package/dist/commands/triage.d.ts +16 -4
- package/dist/commands/triage.js +278 -179
- package/dist/index.d.ts +2 -1
- package/dist/index.js +73 -50
- package/dist/triage/prompts.js +13 -5
- package/dist/triage/runner.js +1 -1
- package/dist/triage/traces.js +9 -3
- package/dist/triage/types.d.ts +1 -1
- package/dist/types.d.ts +146 -26
- package/dist/types.js +44 -5
- package/dist/utils/analysis-api.d.ts +16 -0
- package/dist/utils/analysis-api.js +23 -5
- package/dist/utils/telemetry.d.ts +17 -0
- package/dist/utils/telemetry.js +109 -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/types.js
CHANGED
|
@@ -94,8 +94,31 @@ export const AI_REVIEWER_DISPLAY_NAMES = {
|
|
|
94
94
|
codereviewer: 'CodeReviewBot',
|
|
95
95
|
codium: 'Codium',
|
|
96
96
|
};
|
|
97
|
+
/** Maps friendly source names to exact GitHub bot login usernames. */
|
|
98
|
+
export const AI_REVIEWER_BOT_USERNAMES = {
|
|
99
|
+
cursor: 'cursor-bugbot[bot]',
|
|
100
|
+
coderabbit: 'coderabbitai[bot]',
|
|
101
|
+
copilot: 'copilot[bot]',
|
|
102
|
+
greptile: 'greptile[bot]',
|
|
103
|
+
sourcery: 'sourcery-ai[bot]',
|
|
104
|
+
qodo: 'qodo-merge-pro[bot]',
|
|
105
|
+
codeant: 'codeant-ai[bot]',
|
|
106
|
+
ellipsis: 'ellipsis-dev[bot]',
|
|
107
|
+
korbit: 'korbit-ai[bot]',
|
|
108
|
+
panto: 'pantoaibot[bot]',
|
|
109
|
+
bito: 'bito[bot]',
|
|
110
|
+
graphite: 'graphite-app[bot]',
|
|
111
|
+
deepsource: 'deepsource-io[bot]',
|
|
112
|
+
snyk: 'snyk[bot]',
|
|
113
|
+
codacy: 'codacy-production[bot]',
|
|
114
|
+
sonarcloud: 'sonarcloud[bot]',
|
|
115
|
+
sweep: 'sweep-ai[bot]',
|
|
116
|
+
codeguru: 'aws-codeguru-reviewer[bot]',
|
|
117
|
+
codereviewer: 'codereviewbot[bot]',
|
|
118
|
+
codium: 'codiumai[bot]',
|
|
119
|
+
};
|
|
97
120
|
const AIReviewerSourceSchema = z.enum(AI_REVIEWER_SOURCES);
|
|
98
|
-
const
|
|
121
|
+
const MergeQueueConfigSchema = z.object({
|
|
99
122
|
/** Enable the Haystack auto-merge queue */
|
|
100
123
|
merge_queue: z.boolean().optional(),
|
|
101
124
|
/** Automatically rebase PRs that develop merge conflicts after pushes to the base branch */
|
|
@@ -107,8 +130,24 @@ const BabysitterConfigSchema = z.object({
|
|
|
107
130
|
/** Per-author wait time overrides (author login → minutes) */
|
|
108
131
|
authors: z.record(z.string(), z.number()).optional(),
|
|
109
132
|
}).optional(),
|
|
110
|
-
/**
|
|
111
|
-
|
|
133
|
+
/** Batch multiple PRs into one CI run */
|
|
134
|
+
batch_merge: z.object({
|
|
135
|
+
/** Enable batch merging */
|
|
136
|
+
enabled: z.boolean().optional(),
|
|
137
|
+
/** Minimum PRs to form a batch */
|
|
138
|
+
min_batch_size: z.number().optional(),
|
|
139
|
+
/** Maximum PRs per batch (cap: 20) */
|
|
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(),
|
|
143
|
+
}).optional(),
|
|
144
|
+
/** Wait for external AI code reviewers before merging — all bots must post */
|
|
145
|
+
wait_for_reviewer: z.object({
|
|
146
|
+
/** GitHub bot usernames to wait for (e.g. "cursor-bugbot[bot]") */
|
|
147
|
+
bots: z.array(z.string()),
|
|
148
|
+
/** Give up waiting after this many minutes (default: 30) */
|
|
149
|
+
timeout_minutes: z.number().optional(),
|
|
150
|
+
}).optional(),
|
|
112
151
|
});
|
|
113
152
|
// =============================================================================
|
|
114
153
|
// SERVICE (for monorepos)
|
|
@@ -253,6 +292,6 @@ export const HaystackConfigSchema = z.object({
|
|
|
253
292
|
network: NetworkConfigSchema.optional(),
|
|
254
293
|
/** Secrets required by this project */
|
|
255
294
|
secrets: z.record(z.string(), SecretDeclarationSchema).optional(),
|
|
256
|
-
/**
|
|
257
|
-
|
|
295
|
+
/** Merge queue configuration for autonomous PR maintenance */
|
|
296
|
+
merge_queue: MergeQueueConfigSchema.optional(),
|
|
258
297
|
});
|
|
@@ -105,4 +105,20 @@ export declare function fetchAnalysisResults(owner: string, repo: string, prNumb
|
|
|
105
105
|
* This is the rich analysis data shown on the Haystack feed — includes
|
|
106
106
|
* verdict, rating, structured findings, verified bugs, and agent fix prompts.
|
|
107
107
|
*/
|
|
108
|
+
export interface AutoFixManifest {
|
|
109
|
+
jobId: string;
|
|
110
|
+
repo: string;
|
|
111
|
+
prNumber: number;
|
|
112
|
+
fixedItems: Array<{
|
|
113
|
+
category: string;
|
|
114
|
+
summary: string;
|
|
115
|
+
}>;
|
|
116
|
+
leftForReview: Array<{
|
|
117
|
+
category: string;
|
|
118
|
+
summary: string;
|
|
119
|
+
reason: string;
|
|
120
|
+
}>;
|
|
121
|
+
timestamp: string;
|
|
122
|
+
}
|
|
123
|
+
export declare function fetchAutoFixManifest(owner: string, repo: string, prNumber: number, token?: string): Promise<AutoFixManifest | null>;
|
|
108
124
|
export declare function fetchRatingSynthesis(owner: string, repo: string, prNumber: number, token?: string): Promise<RatingSynthesis | null>;
|
|
@@ -160,11 +160,29 @@ export async function fetchAnalysisResults(owner, repo, prNumber, token) {
|
|
|
160
160
|
autoMerged,
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
export async function fetchAutoFixManifest(owner, repo, prNumber, token) {
|
|
164
|
+
const url = `${PROD_API_BASE}/v3/result/${owner}/${repo}/${prNumber}/auto-fix-manifest.json`;
|
|
165
|
+
try {
|
|
166
|
+
const response = await fetch(url, {
|
|
167
|
+
headers: apiHeaders(token),
|
|
168
|
+
});
|
|
169
|
+
if (!response.ok)
|
|
170
|
+
return null;
|
|
171
|
+
const data = await response.json();
|
|
172
|
+
if (data.download_url) {
|
|
173
|
+
const s3Resp = await fetch(data.download_url, {
|
|
174
|
+
headers: { 'User-Agent': 'Haystack-CLI' },
|
|
175
|
+
});
|
|
176
|
+
if (!s3Resp.ok)
|
|
177
|
+
return null;
|
|
178
|
+
return await s3Resp.json();
|
|
179
|
+
}
|
|
180
|
+
return data;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
168
186
|
export async function fetchRatingSynthesis(owner, repo, prNumber, token) {
|
|
169
187
|
// Use v3/result endpoint (same as the Lambda serves generic files)
|
|
170
188
|
const url = `${PROD_API_BASE}/v3/result/${owner}/${repo}/${prNumber}/rating_synthesis.json`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight CLI telemetry — fire-and-forget error tracking via PostHog HTTP API.
|
|
3
|
+
*
|
|
4
|
+
* Events go to the same PostHog project as the web app, enabling unified
|
|
5
|
+
* alerting across frontend, CLI, and backend.
|
|
6
|
+
*
|
|
7
|
+
* All calls are non-blocking and never throw — telemetry failures must not
|
|
8
|
+
* break CLI commands.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Track an error event. Fire-and-forget — never throws.
|
|
12
|
+
*/
|
|
13
|
+
export declare function trackError(errorType: string, metadata?: Record<string, unknown>): void;
|
|
14
|
+
/**
|
|
15
|
+
* Track a setup event (non-error). Fire-and-forget.
|
|
16
|
+
*/
|
|
17
|
+
export declare function trackSetupEvent(step: string, metadata?: Record<string, unknown>): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight CLI telemetry — fire-and-forget error tracking via PostHog HTTP API.
|
|
3
|
+
*
|
|
4
|
+
* Events go to the same PostHog project as the web app, enabling unified
|
|
5
|
+
* alerting across frontend, CLI, and backend.
|
|
6
|
+
*
|
|
7
|
+
* All calls are non-blocking and never throw — telemetry failures must not
|
|
8
|
+
* break CLI commands.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { dirname, resolve } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
const POSTHOG_HOST = 'https://us.i.posthog.com';
|
|
15
|
+
const POSTHOG_API_KEY = process.env.HAYSTACK_POSTHOG_KEY || 'phc_7wHrCoMlcwuftLe6N38hPb9wKHb4OK0wNw9ayeRySad';
|
|
16
|
+
const CURRENT_FILE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const PACKAGE_JSON_PATH = resolve(CURRENT_FILE_DIR, '../../package.json');
|
|
18
|
+
/**
|
|
19
|
+
* Stable machine identifier for PostHog distinct_id.
|
|
20
|
+
* Uses hostname + username so events are attributable per-machine.
|
|
21
|
+
* Never throws — falls back to hostname-only or 'anonymous'.
|
|
22
|
+
*/
|
|
23
|
+
function getDistinctId() {
|
|
24
|
+
try {
|
|
25
|
+
const hostname = os.hostname();
|
|
26
|
+
try {
|
|
27
|
+
return `cli:${os.userInfo().username}@${hostname}`;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return `cli:${hostname}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return 'cli:anonymous';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Track an error event. Fire-and-forget — never throws.
|
|
39
|
+
*/
|
|
40
|
+
export function trackError(errorType, metadata) {
|
|
41
|
+
try {
|
|
42
|
+
if (typeof globalThis.fetch !== 'function')
|
|
43
|
+
return;
|
|
44
|
+
const event = {
|
|
45
|
+
api_key: POSTHOG_API_KEY,
|
|
46
|
+
distinct_id: getDistinctId(),
|
|
47
|
+
event: 'cli_error',
|
|
48
|
+
properties: {
|
|
49
|
+
error_type: errorType,
|
|
50
|
+
cli_version: getCLIVersion(),
|
|
51
|
+
os: process.platform,
|
|
52
|
+
arch: process.arch,
|
|
53
|
+
node_version: process.version,
|
|
54
|
+
timestamp: new Date().toISOString(),
|
|
55
|
+
...metadata,
|
|
56
|
+
},
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
};
|
|
59
|
+
// Fire-and-forget — never block the CLI
|
|
60
|
+
globalThis.fetch(`${POSTHOG_HOST}/capture/`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify(event),
|
|
64
|
+
}).catch(() => { });
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Telemetry must never crash the CLI
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Track a setup event (non-error). Fire-and-forget.
|
|
72
|
+
*/
|
|
73
|
+
export function trackSetupEvent(step, metadata) {
|
|
74
|
+
try {
|
|
75
|
+
if (typeof globalThis.fetch !== 'function')
|
|
76
|
+
return;
|
|
77
|
+
const event = {
|
|
78
|
+
api_key: POSTHOG_API_KEY,
|
|
79
|
+
distinct_id: getDistinctId(),
|
|
80
|
+
event: 'cli_setup',
|
|
81
|
+
properties: {
|
|
82
|
+
step,
|
|
83
|
+
cli_version: getCLIVersion(),
|
|
84
|
+
os: process.platform,
|
|
85
|
+
arch: process.arch,
|
|
86
|
+
timestamp: new Date().toISOString(),
|
|
87
|
+
...metadata,
|
|
88
|
+
},
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
globalThis.fetch(`${POSTHOG_HOST}/capture/`, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify(event),
|
|
95
|
+
}).catch(() => { });
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Telemetry must never crash the CLI
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function getCLIVersion() {
|
|
102
|
+
try {
|
|
103
|
+
const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf8'));
|
|
104
|
+
return typeof packageJson.version === 'string' ? packageJson.version : 'unknown';
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return 'unknown';
|
|
108
|
+
}
|
|
109
|
+
}
|
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
|
-
}
|