@humanbased/crosscheck 0.14.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/AGENT.md +207 -0
- package/ISSUE.md +234 -0
- package/LICENSE +21 -0
- package/README.md +234 -0
- package/README.zh.md +169 -0
- package/assets/logo.png +0 -0
- package/assets/screenshot-watch-timing.png +0 -0
- package/assets/screenshot-watch-timing.svg +1 -0
- package/assets/screenshot-watch.png +0 -0
- package/crosscheck.config.example.yml +214 -0
- package/dist/__tests__/annotation.test.d.ts +2 -0
- package/dist/__tests__/annotation.test.d.ts.map +1 -0
- package/dist/__tests__/annotation.test.js +134 -0
- package/dist/__tests__/annotation.test.js.map +1 -0
- package/dist/__tests__/backtrace.test.d.ts +2 -0
- package/dist/__tests__/backtrace.test.d.ts.map +1 -0
- package/dist/__tests__/backtrace.test.js +280 -0
- package/dist/__tests__/backtrace.test.js.map +1 -0
- package/dist/__tests__/board.test.d.ts +2 -0
- package/dist/__tests__/board.test.d.ts.map +1 -0
- package/dist/__tests__/board.test.js +149 -0
- package/dist/__tests__/board.test.js.map +1 -0
- package/dist/__tests__/codex.test.d.ts +2 -0
- package/dist/__tests__/codex.test.d.ts.map +1 -0
- package/dist/__tests__/codex.test.js +92 -0
- package/dist/__tests__/codex.test.js.map +1 -0
- package/dist/__tests__/comment-bodies.test.d.ts +2 -0
- package/dist/__tests__/comment-bodies.test.d.ts.map +1 -0
- package/dist/__tests__/comment-bodies.test.js +75 -0
- package/dist/__tests__/comment-bodies.test.js.map +1 -0
- package/dist/__tests__/conflict-resolve.test.d.ts +2 -0
- package/dist/__tests__/conflict-resolve.test.d.ts.map +1 -0
- package/dist/__tests__/conflict-resolve.test.js +123 -0
- package/dist/__tests__/conflict-resolve.test.js.map +1 -0
- package/dist/__tests__/crosscheck-commit.test.d.ts +2 -0
- package/dist/__tests__/crosscheck-commit.test.d.ts.map +1 -0
- package/dist/__tests__/crosscheck-commit.test.js +13 -0
- package/dist/__tests__/crosscheck-commit.test.js.map +1 -0
- package/dist/__tests__/detector.test.d.ts +2 -0
- package/dist/__tests__/detector.test.d.ts.map +1 -0
- package/dist/__tests__/detector.test.js +112 -0
- package/dist/__tests__/detector.test.js.map +1 -0
- package/dist/__tests__/diagnose.test.d.ts +2 -0
- package/dist/__tests__/diagnose.test.d.ts.map +1 -0
- package/dist/__tests__/diagnose.test.js +164 -0
- package/dist/__tests__/diagnose.test.js.map +1 -0
- package/dist/__tests__/diff-hash.test.d.ts +2 -0
- package/dist/__tests__/diff-hash.test.d.ts.map +1 -0
- package/dist/__tests__/diff-hash.test.js +126 -0
- package/dist/__tests__/diff-hash.test.js.map +1 -0
- package/dist/__tests__/durations.test.d.ts +2 -0
- package/dist/__tests__/durations.test.d.ts.map +1 -0
- package/dist/__tests__/durations.test.js +26 -0
- package/dist/__tests__/durations.test.js.map +1 -0
- package/dist/__tests__/event-fields.test.d.ts +2 -0
- package/dist/__tests__/event-fields.test.d.ts.map +1 -0
- package/dist/__tests__/event-fields.test.js +50 -0
- package/dist/__tests__/event-fields.test.js.map +1 -0
- package/dist/__tests__/filter.test.d.ts +2 -0
- package/dist/__tests__/filter.test.d.ts.map +1 -0
- package/dist/__tests__/filter.test.js +21 -0
- package/dist/__tests__/filter.test.js.map +1 -0
- package/dist/__tests__/fix.test.d.ts +2 -0
- package/dist/__tests__/fix.test.d.ts.map +1 -0
- package/dist/__tests__/fix.test.js +124 -0
- package/dist/__tests__/fix.test.js.map +1 -0
- package/dist/__tests__/github-client.test.d.ts +2 -0
- package/dist/__tests__/github-client.test.d.ts.map +1 -0
- package/dist/__tests__/github-client.test.js +22 -0
- package/dist/__tests__/github-client.test.js.map +1 -0
- package/dist/__tests__/github-scan-client.test.d.ts +2 -0
- package/dist/__tests__/github-scan-client.test.d.ts.map +1 -0
- package/dist/__tests__/github-scan-client.test.js +100 -0
- package/dist/__tests__/github-scan-client.test.js.map +1 -0
- package/dist/__tests__/is-fresh-review-comment.test.d.ts +2 -0
- package/dist/__tests__/is-fresh-review-comment.test.d.ts.map +1 -0
- package/dist/__tests__/is-fresh-review-comment.test.js +86 -0
- package/dist/__tests__/is-fresh-review-comment.test.js.map +1 -0
- package/dist/__tests__/issue.test.d.ts +2 -0
- package/dist/__tests__/issue.test.d.ts.map +1 -0
- package/dist/__tests__/issue.test.js +259 -0
- package/dist/__tests__/issue.test.js.map +1 -0
- package/dist/__tests__/kickass.test.d.ts +2 -0
- package/dist/__tests__/kickass.test.d.ts.map +1 -0
- package/dist/__tests__/kickass.test.js +268 -0
- package/dist/__tests__/kickass.test.js.map +1 -0
- package/dist/__tests__/loader.test.d.ts +2 -0
- package/dist/__tests__/loader.test.d.ts.map +1 -0
- package/dist/__tests__/loader.test.js +180 -0
- package/dist/__tests__/loader.test.js.map +1 -0
- package/dist/__tests__/onboard-preservation.test.d.ts +2 -0
- package/dist/__tests__/onboard-preservation.test.d.ts.map +1 -0
- package/dist/__tests__/onboard-preservation.test.js +506 -0
- package/dist/__tests__/onboard-preservation.test.js.map +1 -0
- package/dist/__tests__/optimize.test.d.ts +2 -0
- package/dist/__tests__/optimize.test.d.ts.map +1 -0
- package/dist/__tests__/optimize.test.js +101 -0
- package/dist/__tests__/optimize.test.js.map +1 -0
- package/dist/__tests__/post-review-comment.test.d.ts +2 -0
- package/dist/__tests__/post-review-comment.test.d.ts.map +1 -0
- package/dist/__tests__/post-review-comment.test.js +44 -0
- package/dist/__tests__/post-review-comment.test.js.map +1 -0
- package/dist/__tests__/pr-lock.test.d.ts +2 -0
- package/dist/__tests__/pr-lock.test.d.ts.map +1 -0
- package/dist/__tests__/pr-lock.test.js +115 -0
- package/dist/__tests__/pr-lock.test.js.map +1 -0
- package/dist/__tests__/pr-picker.test.d.ts +2 -0
- package/dist/__tests__/pr-picker.test.d.ts.map +1 -0
- package/dist/__tests__/pr-picker.test.js +57 -0
- package/dist/__tests__/pr-picker.test.js.map +1 -0
- package/dist/__tests__/pr-status-scan.test.d.ts +2 -0
- package/dist/__tests__/pr-status-scan.test.d.ts.map +1 -0
- package/dist/__tests__/pr-status-scan.test.js +92 -0
- package/dist/__tests__/pr-status-scan.test.js.map +1 -0
- package/dist/__tests__/pr-status.test.d.ts +2 -0
- package/dist/__tests__/pr-status.test.d.ts.map +1 -0
- package/dist/__tests__/pr-status.test.js +346 -0
- package/dist/__tests__/pr-status.test.js.map +1 -0
- package/dist/__tests__/repo-picker.test.d.ts +2 -0
- package/dist/__tests__/repo-picker.test.d.ts.map +1 -0
- package/dist/__tests__/repo-picker.test.js +115 -0
- package/dist/__tests__/repo-picker.test.js.map +1 -0
- package/dist/__tests__/review-comment-body.test.d.ts +2 -0
- package/dist/__tests__/review-comment-body.test.d.ts.map +1 -0
- package/dist/__tests__/review-comment-body.test.js +54 -0
- package/dist/__tests__/review-comment-body.test.js.map +1 -0
- package/dist/__tests__/review-models.test.d.ts +2 -0
- package/dist/__tests__/review-models.test.d.ts.map +1 -0
- package/dist/__tests__/review-models.test.js +39 -0
- package/dist/__tests__/review-models.test.js.map +1 -0
- package/dist/__tests__/review-status.test.d.ts +2 -0
- package/dist/__tests__/review-status.test.d.ts.map +1 -0
- package/dist/__tests__/review-status.test.js +95 -0
- package/dist/__tests__/review-status.test.js.map +1 -0
- package/dist/__tests__/runner.test.d.ts +2 -0
- package/dist/__tests__/runner.test.d.ts.map +1 -0
- package/dist/__tests__/runner.test.js +204 -0
- package/dist/__tests__/runner.test.js.map +1 -0
- package/dist/__tests__/scan-cache.test.d.ts +2 -0
- package/dist/__tests__/scan-cache.test.d.ts.map +1 -0
- package/dist/__tests__/scan-cache.test.js +59 -0
- package/dist/__tests__/scan-cache.test.js.map +1 -0
- package/dist/__tests__/scan-client.test.d.ts +2 -0
- package/dist/__tests__/scan-client.test.d.ts.map +1 -0
- package/dist/__tests__/scan-client.test.js +30 -0
- package/dist/__tests__/scan-client.test.js.map +1 -0
- package/dist/__tests__/scan.test.d.ts +2 -0
- package/dist/__tests__/scan.test.d.ts.map +1 -0
- package/dist/__tests__/scan.test.js +115 -0
- package/dist/__tests__/scan.test.js.map +1 -0
- package/dist/__tests__/scopes.test.d.ts +2 -0
- package/dist/__tests__/scopes.test.d.ts.map +1 -0
- package/dist/__tests__/scopes.test.js +101 -0
- package/dist/__tests__/scopes.test.js.map +1 -0
- package/dist/__tests__/sha-cache.test.d.ts +2 -0
- package/dist/__tests__/sha-cache.test.d.ts.map +1 -0
- package/dist/__tests__/sha-cache.test.js +40 -0
- package/dist/__tests__/sha-cache.test.js.map +1 -0
- package/dist/__tests__/smart-switch.test.d.ts +2 -0
- package/dist/__tests__/smart-switch.test.d.ts.map +1 -0
- package/dist/__tests__/smart-switch.test.js +145 -0
- package/dist/__tests__/smart-switch.test.js.map +1 -0
- package/dist/ck.d.ts +3 -0
- package/dist/ck.d.ts.map +1 -0
- package/dist/ck.js +8 -0
- package/dist/ck.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +132 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/diagnose.d.ts +54 -0
- package/dist/commands/diagnose.d.ts.map +1 -0
- package/dist/commands/diagnose.js +294 -0
- package/dist/commands/diagnose.js.map +1 -0
- package/dist/commands/impact.d.ts +38 -0
- package/dist/commands/impact.d.ts.map +1 -0
- package/dist/commands/impact.js +210 -0
- package/dist/commands/impact.js.map +1 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +183 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/issue.d.ts +25 -0
- package/dist/commands/issue.d.ts.map +1 -0
- package/dist/commands/issue.js +445 -0
- package/dist/commands/issue.js.map +1 -0
- package/dist/commands/kickass.d.ts +59 -0
- package/dist/commands/kickass.d.ts.map +1 -0
- package/dist/commands/kickass.js +288 -0
- package/dist/commands/kickass.js.map +1 -0
- package/dist/commands/onboard.d.ts +70 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +883 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/commands/optimize.d.ts +16 -0
- package/dist/commands/optimize.d.ts.map +1 -0
- package/dist/commands/optimize.js +244 -0
- package/dist/commands/optimize.js.map +1 -0
- package/dist/commands/review.d.ts +2 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.js +118 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/run.d.ts +13 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +243 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/scan.d.ts +94 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +276 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/serve.d.ts +9 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +402 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +89 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/watch.d.ts +9 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +902 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/config/loader.d.ts +47 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +334 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +814 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +152 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/github/client.d.ts +139 -0
- package/dist/github/client.d.ts.map +1 -0
- package/dist/github/client.js +711 -0
- package/dist/github/client.js.map +1 -0
- package/dist/github/detector.d.ts +12 -0
- package/dist/github/detector.d.ts.map +1 -0
- package/dist/github/detector.js +120 -0
- package/dist/github/detector.js.map +1 -0
- package/dist/github/merge.d.ts +9 -0
- package/dist/github/merge.d.ts.map +1 -0
- package/dist/github/merge.js +33 -0
- package/dist/github/merge.js.map +1 -0
- package/dist/github/review-status.d.ts +6 -0
- package/dist/github/review-status.d.ts.map +1 -0
- package/dist/github/review-status.js +51 -0
- package/dist/github/review-status.js.map +1 -0
- package/dist/github/webhook.d.ts +41 -0
- package/dist/github/webhook.d.ts.map +1 -0
- package/dist/github/webhook.js +50 -0
- package/dist/github/webhook.js.map +1 -0
- package/dist/lib/annotation.d.ts +23 -0
- package/dist/lib/annotation.d.ts.map +1 -0
- package/dist/lib/annotation.js +103 -0
- package/dist/lib/annotation.js.map +1 -0
- package/dist/lib/backtrace.d.ts +40 -0
- package/dist/lib/backtrace.d.ts.map +1 -0
- package/dist/lib/backtrace.js +169 -0
- package/dist/lib/backtrace.js.map +1 -0
- package/dist/lib/board.d.ts +74 -0
- package/dist/lib/board.d.ts.map +1 -0
- package/dist/lib/board.js +640 -0
- package/dist/lib/board.js.map +1 -0
- package/dist/lib/clone.d.ts +12 -0
- package/dist/lib/clone.d.ts.map +1 -0
- package/dist/lib/clone.js +30 -0
- package/dist/lib/clone.js.map +1 -0
- package/dist/lib/comment-bodies.d.ts +17 -0
- package/dist/lib/comment-bodies.d.ts.map +1 -0
- package/dist/lib/comment-bodies.js +51 -0
- package/dist/lib/comment-bodies.js.map +1 -0
- package/dist/lib/crosscheck-commit.d.ts +2 -0
- package/dist/lib/crosscheck-commit.d.ts.map +1 -0
- package/dist/lib/crosscheck-commit.js +4 -0
- package/dist/lib/crosscheck-commit.js.map +1 -0
- package/dist/lib/diff-hash.d.ts +16 -0
- package/dist/lib/diff-hash.d.ts.map +1 -0
- package/dist/lib/diff-hash.js +71 -0
- package/dist/lib/diff-hash.js.map +1 -0
- package/dist/lib/durations.d.ts +5 -0
- package/dist/lib/durations.d.ts.map +1 -0
- package/dist/lib/durations.js +39 -0
- package/dist/lib/durations.js.map +1 -0
- package/dist/lib/event-fields.d.ts +6 -0
- package/dist/lib/event-fields.d.ts.map +1 -0
- package/dist/lib/event-fields.js +20 -0
- package/dist/lib/event-fields.js.map +1 -0
- package/dist/lib/filter.d.ts +2 -0
- package/dist/lib/filter.d.ts.map +1 -0
- package/dist/lib/filter.js +4 -0
- package/dist/lib/filter.js.map +1 -0
- package/dist/lib/fortune.d.ts +2 -0
- package/dist/lib/fortune.d.ts.map +1 -0
- package/dist/lib/fortune.js +26 -0
- package/dist/lib/fortune.js.map +1 -0
- package/dist/lib/languages.d.ts +3 -0
- package/dist/lib/languages.d.ts.map +1 -0
- package/dist/lib/languages.js +26 -0
- package/dist/lib/languages.js.map +1 -0
- package/dist/lib/log-analysis.d.ts +17 -0
- package/dist/lib/log-analysis.d.ts.map +1 -0
- package/dist/lib/log-analysis.js +72 -0
- package/dist/lib/log-analysis.js.map +1 -0
- package/dist/lib/logger.d.ts +14 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +84 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/port.d.ts +2 -0
- package/dist/lib/port.d.ts.map +1 -0
- package/dist/lib/port.js +21 -0
- package/dist/lib/port.js.map +1 -0
- package/dist/lib/pr-lock.d.ts +4 -0
- package/dist/lib/pr-lock.d.ts.map +1 -0
- package/dist/lib/pr-lock.js +91 -0
- package/dist/lib/pr-lock.js.map +1 -0
- package/dist/lib/pr-picker.d.ts +10 -0
- package/dist/lib/pr-picker.d.ts.map +1 -0
- package/dist/lib/pr-picker.js +80 -0
- package/dist/lib/pr-picker.js.map +1 -0
- package/dist/lib/pr-status.d.ts +206 -0
- package/dist/lib/pr-status.d.ts.map +1 -0
- package/dist/lib/pr-status.js +613 -0
- package/dist/lib/pr-status.js.map +1 -0
- package/dist/lib/repo-picker.d.ts +23 -0
- package/dist/lib/repo-picker.d.ts.map +1 -0
- package/dist/lib/repo-picker.js +411 -0
- package/dist/lib/repo-picker.js.map +1 -0
- package/dist/lib/review-models.d.ts +7 -0
- package/dist/lib/review-models.d.ts.map +1 -0
- package/dist/lib/review-models.js +32 -0
- package/dist/lib/review-models.js.map +1 -0
- package/dist/lib/runner.d.ts +65 -0
- package/dist/lib/runner.d.ts.map +1 -0
- package/dist/lib/runner.js +710 -0
- package/dist/lib/runner.js.map +1 -0
- package/dist/lib/scan-cache.d.ts +31 -0
- package/dist/lib/scan-cache.d.ts.map +1 -0
- package/dist/lib/scan-cache.js +112 -0
- package/dist/lib/scan-cache.js.map +1 -0
- package/dist/lib/scopes.d.ts +16 -0
- package/dist/lib/scopes.d.ts.map +1 -0
- package/dist/lib/scopes.js +37 -0
- package/dist/lib/scopes.js.map +1 -0
- package/dist/lib/sha-cache.d.ts +7 -0
- package/dist/lib/sha-cache.d.ts.map +1 -0
- package/dist/lib/sha-cache.js +44 -0
- package/dist/lib/sha-cache.js.map +1 -0
- package/dist/lib/smart-switch.d.ts +44 -0
- package/dist/lib/smart-switch.d.ts.map +1 -0
- package/dist/lib/smart-switch.js +145 -0
- package/dist/lib/smart-switch.js.map +1 -0
- package/dist/lib/verdict.d.ts +9 -0
- package/dist/lib/verdict.d.ts.map +1 -0
- package/dist/lib/verdict.js +52 -0
- package/dist/lib/verdict.js.map +1 -0
- package/dist/lib/workflow.d.ts +85 -0
- package/dist/lib/workflow.d.ts.map +1 -0
- package/dist/lib/workflow.js +116 -0
- package/dist/lib/workflow.js.map +1 -0
- package/dist/reviewers/address.d.ts +5 -0
- package/dist/reviewers/address.d.ts.map +1 -0
- package/dist/reviewers/address.js +87 -0
- package/dist/reviewers/address.js.map +1 -0
- package/dist/reviewers/claude.d.ts +12 -0
- package/dist/reviewers/claude.d.ts.map +1 -0
- package/dist/reviewers/claude.js +78 -0
- package/dist/reviewers/claude.js.map +1 -0
- package/dist/reviewers/codex.d.ts +9 -0
- package/dist/reviewers/codex.d.ts.map +1 -0
- package/dist/reviewers/codex.js +121 -0
- package/dist/reviewers/codex.js.map +1 -0
- package/dist/reviewers/conflict-resolve.d.ts +15 -0
- package/dist/reviewers/conflict-resolve.d.ts.map +1 -0
- package/dist/reviewers/conflict-resolve.js +219 -0
- package/dist/reviewers/conflict-resolve.js.map +1 -0
- package/dist/reviewers/fix.d.ts +7 -0
- package/dist/reviewers/fix.d.ts.map +1 -0
- package/dist/reviewers/fix.js +197 -0
- package/dist/reviewers/fix.js.map +1 -0
- package/get-started.md +1271 -0
- package/get-started.zh.md +1208 -0
- package/package.json +75 -0
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { createInterface } from 'readline';
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import { getGithubToken, loadConfig, resolveConfigPath, detectGitHubLogin, } from '../config/loader.js';
|
|
8
|
+
import { listUserOrgs, listOrgRepos, fetchActiveRepos } from '../github/client.js';
|
|
9
|
+
import { checkCodexAuth } from '../reviewers/codex.js';
|
|
10
|
+
import { checkClaudeAuth } from '../reviewers/claude.js';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
import { promptRepoPicker, promptSinglePicker } from '../lib/repo-picker.js';
|
|
13
|
+
import { DEFAULT_REVIEW_INSTRUCTIONS, DEFAULT_FIX_INSTRUCTIONS, DEFAULT_RECHECK_INSTRUCTIONS, DEFAULT_CONFLICT_RESOLVE_INSTRUCTIONS } from '../lib/workflow.js';
|
|
14
|
+
// Model and effort settings for each quality tier.
|
|
15
|
+
// These are written directly to vendors.claude / vendors.codex in the config.
|
|
16
|
+
const QUALITY_TIERS = {
|
|
17
|
+
fast: {
|
|
18
|
+
description: 'quick scan, top issues only (~10s, lowest cost)',
|
|
19
|
+
claude: { model: 'haiku', effort: 'low' },
|
|
20
|
+
codex: { model: 'o4-mini', effort: 'low' },
|
|
21
|
+
},
|
|
22
|
+
balanced: {
|
|
23
|
+
description: 'full review, all issues with explanations (~30s)',
|
|
24
|
+
claude: { model: 'sonnet', effort: 'medium' },
|
|
25
|
+
codex: { model: 'o4-mini', effort: 'medium' },
|
|
26
|
+
},
|
|
27
|
+
thorough: {
|
|
28
|
+
description: 'deep multi-pass, security + architecture (~60s+, higher cost)',
|
|
29
|
+
claude: { model: 'opus', effort: 'max' },
|
|
30
|
+
codex: { model: 'o3', effort: 'high' },
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
function ask(question) {
|
|
34
|
+
return new Promise(resolve => {
|
|
35
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
36
|
+
rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function formatAge(date) {
|
|
40
|
+
const days = Math.floor((Date.now() - date.getTime()) / 86_400_000);
|
|
41
|
+
if (days === 0)
|
|
42
|
+
return 'today';
|
|
43
|
+
if (days < 7)
|
|
44
|
+
return `${days}d ago`;
|
|
45
|
+
if (days < 30)
|
|
46
|
+
return `${Math.floor(days / 7)}w ago`;
|
|
47
|
+
if (days < 365)
|
|
48
|
+
return `${Math.floor(days / 30)}mo ago`;
|
|
49
|
+
return `${Math.floor(days / 365)}y ago`;
|
|
50
|
+
}
|
|
51
|
+
async function checkEnv() {
|
|
52
|
+
let codexOk = false;
|
|
53
|
+
let claudeOk = false;
|
|
54
|
+
try {
|
|
55
|
+
execSync('codex --version 2>&1', { encoding: 'utf8' });
|
|
56
|
+
const auth = await checkCodexAuth();
|
|
57
|
+
codexOk = auth.ok;
|
|
58
|
+
const icon = auth.ok ? chalk.green('✓') : chalk.red('✗');
|
|
59
|
+
console.log(` ${icon} ${'codex CLI'.padEnd(20)} ${auth.detail}`);
|
|
60
|
+
if (!auth.ok)
|
|
61
|
+
console.log(` ${chalk.dim('→')} ${chalk.yellow('Run: codex login --device-auth')}`);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
console.log(` ${chalk.red('✗')} ${'codex CLI'.padEnd(20)} not found`);
|
|
65
|
+
console.log(` ${chalk.dim('→')} ${chalk.yellow('Install: npm install -g @openai/codex')}`);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const auth = await checkClaudeAuth();
|
|
69
|
+
claudeOk = auth.ok;
|
|
70
|
+
const icon = auth.ok ? chalk.green('✓') : chalk.red('✗');
|
|
71
|
+
console.log(` ${icon} ${'claude CLI'.padEnd(20)} ${auth.detail}`);
|
|
72
|
+
if (!auth.ok)
|
|
73
|
+
console.log(` ${chalk.dim('→')} ${chalk.yellow('Run: claude auth login')}`);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
console.log(` ${chalk.red('✗')} ${'claude CLI'.padEnd(20)} not found`);
|
|
77
|
+
console.log(` ${chalk.dim('→')} ${chalk.yellow('Install: npm install -g @anthropic-ai/claude-code')}`);
|
|
78
|
+
}
|
|
79
|
+
const envToken = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
80
|
+
let ghAuthed = false;
|
|
81
|
+
try {
|
|
82
|
+
execSync('gh --version 2>&1', { encoding: 'utf8' });
|
|
83
|
+
let authOutput = '';
|
|
84
|
+
try {
|
|
85
|
+
authOutput = execSync('gh auth status 2>&1', { encoding: 'utf8' });
|
|
86
|
+
}
|
|
87
|
+
catch { /* GITHUB_TOKEN in use */ }
|
|
88
|
+
ghAuthed = authOutput.includes('Logged in') || !!envToken;
|
|
89
|
+
const icon = ghAuthed ? chalk.green('✓') : chalk.red('✗');
|
|
90
|
+
console.log(` ${icon} ${'gh CLI'.padEnd(20)} ${ghAuthed ? 'authenticated' : 'not authenticated'}`);
|
|
91
|
+
if (!ghAuthed)
|
|
92
|
+
console.log(` ${chalk.dim('→')} ${chalk.yellow('Run: gh auth login')}`);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
console.log(` ${chalk.red('✗')} ${'gh CLI'.padEnd(20)} not found`);
|
|
96
|
+
console.log(` ${chalk.dim('→')} ${chalk.yellow('Install: brew install gh && gh auth login')}`);
|
|
97
|
+
}
|
|
98
|
+
if (!claudeOk && !codexOk) {
|
|
99
|
+
console.log(chalk.red('\nAt least one AI CLI (codex or claude) must be authenticated.\n'));
|
|
100
|
+
return { ok: false, claudeOk, codexOk };
|
|
101
|
+
}
|
|
102
|
+
if (!ghAuthed) {
|
|
103
|
+
console.log(chalk.red('\nGitHub auth is required to fetch repos and register webhooks.\n'));
|
|
104
|
+
return { ok: false, claudeOk, codexOk };
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, claudeOk, codexOk };
|
|
107
|
+
}
|
|
108
|
+
async function promptVendorMode(claudeOk, codexOk, existingMode, existingClaudeEnabled, existingCodexEnabled, opts) {
|
|
109
|
+
const bothAvailable = claudeOk && codexOk;
|
|
110
|
+
if (!bothAvailable) {
|
|
111
|
+
const vendor = claudeOk ? 'claude' : 'codex';
|
|
112
|
+
console.log(` Mode: ${chalk.cyan('single-vendor')} (only ${chalk.bold(vendor)} is available)`);
|
|
113
|
+
return { mode: 'single-vendor', claudeEnabled: claudeOk, codexEnabled: codexOk };
|
|
114
|
+
}
|
|
115
|
+
if (opts.yes) {
|
|
116
|
+
const mode = (existingMode ?? 'cross-vendor');
|
|
117
|
+
console.log(` Mode: ${chalk.cyan(mode)}`);
|
|
118
|
+
return { mode, claudeEnabled: existingClaudeEnabled, codexEnabled: existingCodexEnabled };
|
|
119
|
+
}
|
|
120
|
+
const modeItems = [
|
|
121
|
+
{ label: 'cross-vendor', description: 'Claude reviews Codex PRs; Codex reviews Claude PRs' },
|
|
122
|
+
{ label: 'single-vendor', description: 'one AI reviews all PRs' },
|
|
123
|
+
];
|
|
124
|
+
const defaultModeIdx = existingMode === 'single-vendor' ? 1 : 0;
|
|
125
|
+
const modeIdx = await promptSinglePicker(modeItems, {
|
|
126
|
+
title: 'How should reviews be assigned?',
|
|
127
|
+
defaultIndex: defaultModeIdx,
|
|
128
|
+
});
|
|
129
|
+
console.log();
|
|
130
|
+
if (modeIdx === 0) {
|
|
131
|
+
return { mode: 'cross-vendor', claudeEnabled: true, codexEnabled: true };
|
|
132
|
+
}
|
|
133
|
+
// Single-vendor: ask which one
|
|
134
|
+
const defaultVendorIdx = (existingMode === 'single-vendor' && existingCodexEnabled && !existingClaudeEnabled) ? 1 : 0;
|
|
135
|
+
const vendorItems = [
|
|
136
|
+
{ label: 'claude', description: 'Claude Code reviews all PRs' },
|
|
137
|
+
{ label: 'codex', description: 'OpenAI Codex reviews all PRs' },
|
|
138
|
+
];
|
|
139
|
+
const vendorIdx = await promptSinglePicker(vendorItems, {
|
|
140
|
+
title: 'Which AI should review all PRs?',
|
|
141
|
+
defaultIndex: defaultVendorIdx,
|
|
142
|
+
});
|
|
143
|
+
console.log();
|
|
144
|
+
return {
|
|
145
|
+
mode: 'single-vendor',
|
|
146
|
+
claudeEnabled: vendorIdx === 0,
|
|
147
|
+
codexEnabled: vendorIdx === 1,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function promptAuthorVendor(login, existingAuthorRoutes, opts) {
|
|
151
|
+
const existing = existingAuthorRoutes?.[login];
|
|
152
|
+
const current = existing === 'codex' ? 'codex' : existing === 'claude' ? 'claude' : 'both';
|
|
153
|
+
if (opts.yes) {
|
|
154
|
+
console.log(` Primary author: ${chalk.cyan(current)}`);
|
|
155
|
+
return current;
|
|
156
|
+
}
|
|
157
|
+
const items = [
|
|
158
|
+
{ label: 'claude', description: 'my PRs without explicit attribution → Codex reviews them' },
|
|
159
|
+
{ label: 'codex', description: 'my PRs without explicit attribution → Claude reviews them' },
|
|
160
|
+
{ label: 'both', description: 'my PRs without explicit attribution → use fallback_reviewer' },
|
|
161
|
+
];
|
|
162
|
+
const defaultIdx = current === 'codex' ? 1 : current === 'claude' ? 0 : 2;
|
|
163
|
+
const idx = await promptSinglePicker(items, {
|
|
164
|
+
title: 'Which AI do you primarily use to write code?',
|
|
165
|
+
defaultIndex: defaultIdx,
|
|
166
|
+
});
|
|
167
|
+
console.log();
|
|
168
|
+
return idx === 1 ? 'codex' : idx === 2 ? 'both' : 'claude';
|
|
169
|
+
}
|
|
170
|
+
async function promptQualityTier(claudeEnabled, codexEnabled, currentTier, opts) {
|
|
171
|
+
if (opts.yes) {
|
|
172
|
+
const tier = (currentTier ?? 'balanced');
|
|
173
|
+
console.log(` Quality: ${chalk.cyan(tier)}`);
|
|
174
|
+
return tier;
|
|
175
|
+
}
|
|
176
|
+
function modelHint(tier) {
|
|
177
|
+
const t = QUALITY_TIERS[tier];
|
|
178
|
+
const parts = [];
|
|
179
|
+
if (claudeEnabled)
|
|
180
|
+
parts.push(`claude: ${t.claude.model} · ${t.claude.effort} effort`);
|
|
181
|
+
if (codexEnabled)
|
|
182
|
+
parts.push(`codex: ${t.codex.model} · ${t.codex.effort} effort`);
|
|
183
|
+
return parts.join(' · ');
|
|
184
|
+
}
|
|
185
|
+
const tiers = ['fast', 'balanced', 'thorough'];
|
|
186
|
+
const items = tiers.map(tier => ({
|
|
187
|
+
label: tier,
|
|
188
|
+
description: QUALITY_TIERS[tier].description,
|
|
189
|
+
hint: modelHint(tier),
|
|
190
|
+
}));
|
|
191
|
+
const defaultIdx = tiers.indexOf((currentTier ?? 'balanced'));
|
|
192
|
+
const idx = await promptSinglePicker(items, {
|
|
193
|
+
title: 'Review quality — how deep should the analysis go?',
|
|
194
|
+
defaultIndex: defaultIdx >= 0 ? defaultIdx : 1,
|
|
195
|
+
});
|
|
196
|
+
console.log();
|
|
197
|
+
return tiers[idx];
|
|
198
|
+
}
|
|
199
|
+
// Exported for tests; `workflowDir` defaults to the user's ~/.crosscheck for runtime callsites.
|
|
200
|
+
export function detectCurrentPreset(workflowDir = join(homedir(), '.crosscheck')) {
|
|
201
|
+
const globalWorkflowPath = join(workflowDir, 'workflow.yml');
|
|
202
|
+
if (existsSync(globalWorkflowPath)) {
|
|
203
|
+
try {
|
|
204
|
+
const raw = yaml.load(readFileSync(globalWorkflowPath, 'utf8'));
|
|
205
|
+
// Normalize legacy 'address' → 'fix' to match the schema-level transform in workflow.ts.
|
|
206
|
+
// Without this, a legacy workflow with `type: address` is misread as `review-only`,
|
|
207
|
+
// which then causes applyOnboardConfig to silently drop the fix step on regenerate.
|
|
208
|
+
const types = (raw?.steps ?? []).map(s => (s.type === 'address' ? 'fix' : s.type));
|
|
209
|
+
if (types.includes('recheck'))
|
|
210
|
+
return 'review-fix-recheck';
|
|
211
|
+
if (types.includes('fix'))
|
|
212
|
+
return 'review-fix';
|
|
213
|
+
return 'review-only';
|
|
214
|
+
}
|
|
215
|
+
catch { /* malformed — default to review-only */ }
|
|
216
|
+
}
|
|
217
|
+
return 'review-only';
|
|
218
|
+
}
|
|
219
|
+
export function detectConflictResolveEnabled(workflowDir = join(homedir(), '.crosscheck')) {
|
|
220
|
+
const path = join(workflowDir, 'workflow.yml');
|
|
221
|
+
if (!existsSync(path))
|
|
222
|
+
return false;
|
|
223
|
+
try {
|
|
224
|
+
const raw = yaml.load(readFileSync(path, 'utf8'));
|
|
225
|
+
return (raw?.steps ?? []).some(s => s.type === 'conflict-resolve');
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function promptConflictResolve(currentEnabled, opts) {
|
|
232
|
+
if (opts.yes) {
|
|
233
|
+
console.log(` Conflict-resolve: ${chalk.cyan(currentEnabled ? 'yes' : 'no')}`);
|
|
234
|
+
return currentEnabled;
|
|
235
|
+
}
|
|
236
|
+
const items = [
|
|
237
|
+
{ label: 'no', description: 'leave merge conflicts for the PR author to resolve manually' },
|
|
238
|
+
{ label: 'yes', description: 'auto-resolve merge conflicts before each review (requires Claude)' },
|
|
239
|
+
];
|
|
240
|
+
const defaultIdx = currentEnabled ? 1 : 0;
|
|
241
|
+
const idx = await promptSinglePicker(items, {
|
|
242
|
+
title: 'Auto-resolve merge conflicts?',
|
|
243
|
+
defaultIndex: defaultIdx,
|
|
244
|
+
});
|
|
245
|
+
console.log();
|
|
246
|
+
return idx === 1;
|
|
247
|
+
}
|
|
248
|
+
async function promptMaxRounds(currentMaxRounds, opts) {
|
|
249
|
+
if (opts.yes) {
|
|
250
|
+
const rounds = currentMaxRounds ?? 1;
|
|
251
|
+
console.log(` Max rounds: ${chalk.cyan(String(rounds))}`);
|
|
252
|
+
return rounds;
|
|
253
|
+
}
|
|
254
|
+
const items = [
|
|
255
|
+
{ label: '1 round', description: 'one fix pass, then re-check once' },
|
|
256
|
+
{ label: '2 rounds', description: 'up to two fix → re-check cycles' },
|
|
257
|
+
{ label: '3 rounds', description: 'up to three fix → re-check cycles (maximum)' },
|
|
258
|
+
];
|
|
259
|
+
const defaultIdx = Math.max(0, Math.min(2, (currentMaxRounds ?? 1) - 1));
|
|
260
|
+
const idx = await promptSinglePicker(items, {
|
|
261
|
+
title: 'How many fix → re-check rounds?',
|
|
262
|
+
defaultIndex: defaultIdx,
|
|
263
|
+
});
|
|
264
|
+
console.log();
|
|
265
|
+
return idx + 1;
|
|
266
|
+
}
|
|
267
|
+
async function promptWorkflowPipeline(opts) {
|
|
268
|
+
const currentPreset = detectCurrentPreset();
|
|
269
|
+
if (opts.yes) {
|
|
270
|
+
console.log(` Pipeline: ${chalk.cyan(currentPreset)}`);
|
|
271
|
+
return currentPreset;
|
|
272
|
+
}
|
|
273
|
+
const presetOrder = ['review-only', 'review-fix', 'review-fix-recheck'];
|
|
274
|
+
const defaultIdx = presetOrder.indexOf(currentPreset);
|
|
275
|
+
const items = [
|
|
276
|
+
{ label: 'review only', description: 'AI posts a comment; you handle fixes' },
|
|
277
|
+
{ label: 'review → fix', description: 'AI reviews, then auto-applies fixes' },
|
|
278
|
+
{ label: 'review → fix → re-check', description: 'full loop: review, fix, then re-review to confirm' },
|
|
279
|
+
];
|
|
280
|
+
const idx = await promptSinglePicker(items, {
|
|
281
|
+
title: 'What should happen after a review?',
|
|
282
|
+
defaultIndex: defaultIdx >= 0 ? defaultIdx : 1,
|
|
283
|
+
});
|
|
284
|
+
console.log();
|
|
285
|
+
if (idx === 0)
|
|
286
|
+
return 'review-only';
|
|
287
|
+
if (idx === 2)
|
|
288
|
+
return 'review-fix-recheck';
|
|
289
|
+
return 'review-fix';
|
|
290
|
+
}
|
|
291
|
+
async function promptConnectionType(currentTunnel, opts) {
|
|
292
|
+
if (opts.yes) {
|
|
293
|
+
const backend = currentTunnel ?? 'localhost.run';
|
|
294
|
+
console.log(` Connection: ${chalk.cyan(backend)}`);
|
|
295
|
+
return backend;
|
|
296
|
+
}
|
|
297
|
+
const items = [
|
|
298
|
+
{
|
|
299
|
+
label: 'localhost.run',
|
|
300
|
+
description: 'zero-config SSH tunnel — reconnects automatically, no install needed',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
label: 'smee.io',
|
|
304
|
+
description: 'webhook relay — events queued while offline, stable channel URL',
|
|
305
|
+
hint: 'Get a free channel URL at smee.io/new — you\'ll paste it in the next step',
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
const defaultIdx = currentTunnel === 'smee' ? 1 : 0;
|
|
309
|
+
const idx = await promptSinglePicker(items, {
|
|
310
|
+
title: 'How will GitHub reach your crosscheck server?',
|
|
311
|
+
defaultIndex: defaultIdx,
|
|
312
|
+
});
|
|
313
|
+
console.log();
|
|
314
|
+
return idx === 1 ? 'smee' : 'localhost.run';
|
|
315
|
+
}
|
|
316
|
+
async function promptCloneProtocol(currentProtocol, opts) {
|
|
317
|
+
if (opts.yes) {
|
|
318
|
+
const protocol = currentProtocol ?? 'ssh';
|
|
319
|
+
console.log(` Clone protocol: ${chalk.cyan(protocol)}`);
|
|
320
|
+
return protocol;
|
|
321
|
+
}
|
|
322
|
+
const items = [
|
|
323
|
+
{
|
|
324
|
+
label: 'ssh',
|
|
325
|
+
description: 'git@github.com:owner/repo.git — uses your local SSH keys',
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
label: 'https',
|
|
329
|
+
description: 'https://github.com/owner/repo.git — uses GitHub token',
|
|
330
|
+
hint: 'Pick https if SSH clone fails or you prefer token-based auth',
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
const defaultIdx = currentProtocol === 'https' ? 1 : 0;
|
|
334
|
+
const idx = await promptSinglePicker(items, {
|
|
335
|
+
title: 'How should crosscheck clone PR repos for review?',
|
|
336
|
+
defaultIndex: defaultIdx,
|
|
337
|
+
});
|
|
338
|
+
console.log();
|
|
339
|
+
return idx === 1 ? 'https' : 'ssh';
|
|
340
|
+
}
|
|
341
|
+
// Build the workflow YAML for the given preset, with inline per-step instructions.
|
|
342
|
+
// Written to ~/.crosscheck/workflow.yml on first onboard. On re-runs, regenerated
|
|
343
|
+
// only when the step-type sequence drifts from the selected preset.
|
|
344
|
+
function buildWorkflowYaml(preset, maxRounds = 1, conflictResolve = false) {
|
|
345
|
+
const conflictResolveStep = {
|
|
346
|
+
name: 'conflict-resolve',
|
|
347
|
+
type: 'conflict-resolve',
|
|
348
|
+
reviewer: 'origin',
|
|
349
|
+
max_rounds: 3,
|
|
350
|
+
instructions: DEFAULT_CONFLICT_RESOLVE_INSTRUCTIONS,
|
|
351
|
+
};
|
|
352
|
+
const reviewStep = {
|
|
353
|
+
name: 'review',
|
|
354
|
+
type: 'review',
|
|
355
|
+
reviewer: 'auto',
|
|
356
|
+
max_rounds: 1,
|
|
357
|
+
instructions: DEFAULT_REVIEW_INSTRUCTIONS,
|
|
358
|
+
};
|
|
359
|
+
const fixStep = {
|
|
360
|
+
name: 'fix',
|
|
361
|
+
type: 'fix',
|
|
362
|
+
reviewer: 'origin',
|
|
363
|
+
when: "review.verdict != 'APPROVE'",
|
|
364
|
+
max_rounds: maxRounds,
|
|
365
|
+
instructions: DEFAULT_FIX_INSTRUCTIONS,
|
|
366
|
+
};
|
|
367
|
+
const recheckStep = {
|
|
368
|
+
name: 'recheck',
|
|
369
|
+
type: 'recheck',
|
|
370
|
+
reviewer: 'auto',
|
|
371
|
+
when: "fix.applied_count > 0",
|
|
372
|
+
max_rounds: maxRounds,
|
|
373
|
+
instructions: DEFAULT_RECHECK_INSTRUCTIONS,
|
|
374
|
+
};
|
|
375
|
+
let steps;
|
|
376
|
+
if (preset === 'review-only')
|
|
377
|
+
steps = [reviewStep];
|
|
378
|
+
else if (preset === 'review-fix')
|
|
379
|
+
steps = [reviewStep, fixStep];
|
|
380
|
+
else
|
|
381
|
+
steps = [reviewStep, fixStep, recheckStep];
|
|
382
|
+
if (conflictResolve)
|
|
383
|
+
steps = [conflictResolveStep, ...steps];
|
|
384
|
+
const header = [
|
|
385
|
+
'# crosscheck workflow — generated by crosscheck onboard',
|
|
386
|
+
'# Edit this file to customize your pipeline. Re-running onboard preserves this file.',
|
|
387
|
+
'# Place a .crosscheck/workflow.yml in your project root to override this global file.',
|
|
388
|
+
'',
|
|
389
|
+
].join('\n');
|
|
390
|
+
return header + yaml.dump({ on: ['opened', 'synchronize'], steps }, { lineWidth: -1, noRefs: true });
|
|
391
|
+
}
|
|
392
|
+
// Writes all onboard decisions to configPath and manages the global workflow.yml.
|
|
393
|
+
// On re-runs, only the fields onboard owns are updated; everything else is preserved.
|
|
394
|
+
export function applyOnboardConfig(configPath, decisions, workflowDir = join(homedir(), '.crosscheck')) {
|
|
395
|
+
const { deployment, login, selectedRepos, selectedOrgs, vendorConfig, qualityTier, pipelinePreset, maxRounds, conflictResolve, tunnelBackend, smeeChannel, cloneProtocol } = decisions;
|
|
396
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
397
|
+
// Load existing config (preserves all custom fields) or start fresh
|
|
398
|
+
const raw = existsSync(configPath)
|
|
399
|
+
? (yaml.load(readFileSync(configPath, 'utf8')) ?? {})
|
|
400
|
+
: {};
|
|
401
|
+
// ── Fields onboard always owns ─────────────────────────────────────────────
|
|
402
|
+
raw.deployment = deployment;
|
|
403
|
+
raw.orgs = selectedOrgs;
|
|
404
|
+
raw.mode = vendorConfig.mode;
|
|
405
|
+
raw.clone_protocol = cloneProtocol;
|
|
406
|
+
// Repos
|
|
407
|
+
raw.repos = selectedRepos.map(r => {
|
|
408
|
+
const [owner, name] = r.split('/');
|
|
409
|
+
return { owner, name };
|
|
410
|
+
});
|
|
411
|
+
// Users: personal mode captures the login; team mode never uses users
|
|
412
|
+
if (deployment === 'personal' && login) {
|
|
413
|
+
raw.users = [login];
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
delete raw.users; // team mode, or personal with no login
|
|
417
|
+
}
|
|
418
|
+
// Scope covered by repos/orgs — users entry not needed even in personal mode
|
|
419
|
+
if (selectedRepos.length > 0 || selectedOrgs.length > 0) {
|
|
420
|
+
delete raw.users;
|
|
421
|
+
}
|
|
422
|
+
// ── Routing: initialise missing fields; never overwrite fields that are set ──
|
|
423
|
+
// Guards on individual fields so a partial routing object (e.g. from an
|
|
424
|
+
// unpatched example config) still gets the personal-mode defaults filled in.
|
|
425
|
+
if (!raw.routing || typeof raw.routing !== 'object')
|
|
426
|
+
raw.routing = {};
|
|
427
|
+
const routing = raw.routing;
|
|
428
|
+
if (deployment === 'personal' && login) {
|
|
429
|
+
const currentAuthors = Array.isArray(routing.allowed_authors) ? routing.allowed_authors : [];
|
|
430
|
+
if (currentAuthors.length === 0)
|
|
431
|
+
routing.allowed_authors = [login];
|
|
432
|
+
if (decisions.vendorConfig.mode === 'cross-vendor') {
|
|
433
|
+
const currentRoutes = routing.author_routes != null && typeof routing.author_routes === 'object'
|
|
434
|
+
? { ...routing.author_routes }
|
|
435
|
+
: {};
|
|
436
|
+
if (decisions.authorVendor === 'both') {
|
|
437
|
+
delete currentRoutes[login];
|
|
438
|
+
if (Object.keys(currentRoutes).length > 0) {
|
|
439
|
+
routing.author_routes = currentRoutes;
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
delete routing.author_routes;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
routing.author_routes = { ...currentRoutes, [login]: decisions.authorVendor };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (routing.fallback_reviewer === undefined)
|
|
451
|
+
routing.fallback_reviewer = 'auto';
|
|
452
|
+
// ── Vendors ─────────────────────────────────────────────────────────────────
|
|
453
|
+
if (!raw.vendors || typeof raw.vendors !== 'object')
|
|
454
|
+
raw.vendors = {};
|
|
455
|
+
const vendors = raw.vendors;
|
|
456
|
+
if (!vendors.claude)
|
|
457
|
+
vendors.claude = {};
|
|
458
|
+
if (!vendors.codex)
|
|
459
|
+
vendors.codex = {};
|
|
460
|
+
vendors.claude.enabled = vendorConfig.claudeEnabled;
|
|
461
|
+
vendors.codex.enabled = vendorConfig.codexEnabled;
|
|
462
|
+
// ── Tunnel ──────────────────────────────────────────────────────────────────
|
|
463
|
+
if (!raw.tunnel || typeof raw.tunnel !== 'object')
|
|
464
|
+
raw.tunnel = {};
|
|
465
|
+
const tunnelObj = raw.tunnel;
|
|
466
|
+
tunnelObj.backend = tunnelBackend;
|
|
467
|
+
if (tunnelBackend === 'smee' && smeeChannel)
|
|
468
|
+
tunnelObj.smee_channel = smeeChannel;
|
|
469
|
+
// ── Quality tier + per-vendor effort ────────────────────────────────────────
|
|
470
|
+
// claude.ts derives the model from quality.tier at runtime (vendor.model is ignored).
|
|
471
|
+
// vendor.model is written for codex only — api-key auth uses it as an override.
|
|
472
|
+
if (!raw.quality || typeof raw.quality !== 'object')
|
|
473
|
+
raw.quality = {};
|
|
474
|
+
raw.quality.tier = qualityTier;
|
|
475
|
+
const tierCfg = QUALITY_TIERS[qualityTier];
|
|
476
|
+
vendors.claude.effort = tierCfg.claude.effort;
|
|
477
|
+
vendors.codex.model = tierCfg.codex.model;
|
|
478
|
+
vendors.codex.effort = tierCfg.codex.effort;
|
|
479
|
+
// ── Fix delivery mechanism (operational config, not pipeline logic) ──────────
|
|
480
|
+
// Pipeline steps and trigger conditions live in workflow.yml.
|
|
481
|
+
// config.yml only retains how fixes land on the PR (commit / pull_request / comment).
|
|
482
|
+
if (!raw.post_review || typeof raw.post_review !== 'object')
|
|
483
|
+
raw.post_review = {};
|
|
484
|
+
const postReview = raw.post_review;
|
|
485
|
+
if (!postReview.auto_fix || typeof postReview.auto_fix !== 'object')
|
|
486
|
+
postReview.auto_fix = {};
|
|
487
|
+
const autoFix = postReview.auto_fix;
|
|
488
|
+
// Remove stale fields written by pre-refactor onboard runs
|
|
489
|
+
delete autoFix.enabled;
|
|
490
|
+
delete autoFix.trigger;
|
|
491
|
+
delete autoFix.min_severity;
|
|
492
|
+
delete autoFix.fixer;
|
|
493
|
+
if (!autoFix.delivery || typeof autoFix.delivery !== 'object')
|
|
494
|
+
autoFix.delivery = {};
|
|
495
|
+
const delivery = autoFix.delivery;
|
|
496
|
+
if (!delivery.mode)
|
|
497
|
+
delivery.mode = 'commit';
|
|
498
|
+
writeFileSync(configPath, yaml.dump(raw, { lineWidth: -1, noRefs: true }));
|
|
499
|
+
// ── Global workflow.yml ──────────────────────────────────────────────────────
|
|
500
|
+
// Written on first onboard. On re-runs, regenerated when the existing step
|
|
501
|
+
// sequence does not match the selected preset — covers both upgrades (missing
|
|
502
|
+
// types) and downgrades (extra types). When the sequence matches exactly, the
|
|
503
|
+
// file is preserved so user edits to instructions survive.
|
|
504
|
+
const globalWorkflowPath = join(workflowDir, 'workflow.yml');
|
|
505
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
506
|
+
const presetStepTypes = {
|
|
507
|
+
'review-only': ['review'],
|
|
508
|
+
'review-fix': ['review', 'fix'],
|
|
509
|
+
'review-fix-recheck': ['review', 'fix', 'recheck'],
|
|
510
|
+
};
|
|
511
|
+
const requiredSet = new Set(presetStepTypes[pipelinePreset]);
|
|
512
|
+
if (conflictResolve)
|
|
513
|
+
requiredSet.add('conflict-resolve');
|
|
514
|
+
const effectiveMaxRounds = maxRounds ?? 1;
|
|
515
|
+
if (!existsSync(globalWorkflowPath)) {
|
|
516
|
+
writeFileSync(globalWorkflowPath, buildWorkflowYaml(pipelinePreset, effectiveMaxRounds, conflictResolve));
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
try {
|
|
520
|
+
const existingRaw = yaml.load(readFileSync(globalWorkflowPath, 'utf8'));
|
|
521
|
+
// Normalize legacy 'address' → 'fix' so workflow.yml files written by older
|
|
522
|
+
// crosscheck versions are not regenerated solely on the renamed step type
|
|
523
|
+
// (matches the schema-level transform in workflow.ts). Steps without a
|
|
524
|
+
// type field are filtered out. Set comparison (not sequence) so user-added
|
|
525
|
+
// duplicate steps or reordered steps are treated as equivalent and preserved.
|
|
526
|
+
const existingSteps = existingRaw?.steps ?? [];
|
|
527
|
+
const existingSet = new Set(existingSteps
|
|
528
|
+
.map(s => (s.type === 'address' ? 'fix' : s.type))
|
|
529
|
+
.filter((t) => Boolean(t)));
|
|
530
|
+
const setsMatch = requiredSet.size === existingSet.size && [...requiredSet].every(t => existingSet.has(t));
|
|
531
|
+
// Also regenerate when max_rounds has changed on any fix or recheck step
|
|
532
|
+
const existingMaxRounds = existingSteps
|
|
533
|
+
.filter(s => s.type === 'fix' || s.type === 'recheck')
|
|
534
|
+
.map(s => s.max_rounds ?? 1);
|
|
535
|
+
const maxRoundsDrifted = existingMaxRounds.length > 0
|
|
536
|
+
&& existingMaxRounds.some(r => r !== effectiveMaxRounds);
|
|
537
|
+
if (!setsMatch) {
|
|
538
|
+
// Preset changed — regenerate from template (step types differ)
|
|
539
|
+
writeFileSync(globalWorkflowPath, buildWorkflowYaml(pipelinePreset, effectiveMaxRounds, conflictResolve));
|
|
540
|
+
}
|
|
541
|
+
else if (maxRoundsDrifted) {
|
|
542
|
+
// Preset unchanged, only max_rounds changed — patch in-place to preserve
|
|
543
|
+
// any custom instructions or structural edits the user may have made.
|
|
544
|
+
const patchedSteps = existingSteps.map(s => {
|
|
545
|
+
if (s.type === 'fix' || s.type === 'recheck') {
|
|
546
|
+
return { ...s, max_rounds: effectiveMaxRounds };
|
|
547
|
+
}
|
|
548
|
+
return s;
|
|
549
|
+
});
|
|
550
|
+
const patchedRaw = { ...existingRaw, steps: patchedSteps };
|
|
551
|
+
writeFileSync(globalWorkflowPath, yaml.dump(patchedRaw, { lineWidth: -1, noRefs: true }));
|
|
552
|
+
}
|
|
553
|
+
// No drift — preserve existing file (may have user-edited instructions or structural customizations)
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
// Malformed workflow file — regenerate
|
|
557
|
+
writeFileSync(globalWorkflowPath, buildWorkflowYaml(pipelinePreset, effectiveMaxRounds, conflictResolve));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
export async function runOnboard(opts = {}) {
|
|
562
|
+
if (!process.stdin.isTTY) {
|
|
563
|
+
console.error(chalk.red('onboard requires an interactive terminal.'));
|
|
564
|
+
console.error(chalk.dim('Run crosscheck init and edit crosscheck.config.yml manually.'));
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
console.log(chalk.bold('\ncrosscheck onboard\n'));
|
|
568
|
+
// ── Step 1: Auth check ─────────────────────────────────────────────────────
|
|
569
|
+
console.log(chalk.bold('Step 1 — environment check'));
|
|
570
|
+
const env = await checkEnv();
|
|
571
|
+
if (!env.ok)
|
|
572
|
+
process.exit(1);
|
|
573
|
+
console.log();
|
|
574
|
+
// ── Step 2: Deployment mode ────────────────────────────────────────────────
|
|
575
|
+
console.log(chalk.bold('Step 2 — deployment mode'));
|
|
576
|
+
const configPath = opts.config ?? resolveConfigPath() ?? join(homedir(), '.crosscheck', 'config.yml');
|
|
577
|
+
const existingConfig = existsSync(configPath) ? loadConfig(configPath) : null;
|
|
578
|
+
const currentDeployment = existingConfig?.deployment;
|
|
579
|
+
let deployment;
|
|
580
|
+
if (opts.personal) {
|
|
581
|
+
deployment = 'personal';
|
|
582
|
+
console.log(` Mode: ${chalk.cyan('personal')} (--personal flag)`);
|
|
583
|
+
}
|
|
584
|
+
else if (opts.team) {
|
|
585
|
+
deployment = 'team';
|
|
586
|
+
console.log(` Mode: ${chalk.cyan('team')} (--team flag)`);
|
|
587
|
+
}
|
|
588
|
+
else if (opts.yes && currentDeployment) {
|
|
589
|
+
deployment = currentDeployment;
|
|
590
|
+
console.log(` Mode: ${chalk.cyan(deployment)}`);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
const deployItems = [
|
|
594
|
+
{ label: 'personal', description: 'monitor your own repos; review only your PRs' },
|
|
595
|
+
{ label: 'team', description: 'monitor org repos; review all PRs from any author' },
|
|
596
|
+
];
|
|
597
|
+
const defaultDeployIdx = currentDeployment === 'team' ? 1 : 0;
|
|
598
|
+
const deployIdx = await promptSinglePicker(deployItems, {
|
|
599
|
+
title: 'How are you using crosscheck?',
|
|
600
|
+
defaultIndex: defaultDeployIdx,
|
|
601
|
+
});
|
|
602
|
+
deployment = deployIdx === 1 ? 'team' : 'personal';
|
|
603
|
+
}
|
|
604
|
+
console.log();
|
|
605
|
+
// ── Step 3: Repo selection (hierarchical: namespace → repos) ───────────────
|
|
606
|
+
console.log(chalk.bold('Step 3 — select repos to monitor'));
|
|
607
|
+
let token;
|
|
608
|
+
try {
|
|
609
|
+
token = getGithubToken();
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
const login = detectGitHubLogin() ?? '';
|
|
616
|
+
console.log(chalk.dim(` Fetching repos for ${login || 'your account'}...`));
|
|
617
|
+
const [personalActivityRepos, orgs] = await Promise.all([
|
|
618
|
+
login ? fetchActiveRepos(login, token).catch(() => []) : Promise.resolve([]),
|
|
619
|
+
listUserOrgs(token).catch(() => []),
|
|
620
|
+
]);
|
|
621
|
+
const orgRepoLists = await Promise.all(orgs.map(org => listOrgRepos(org, token).catch(() => [])));
|
|
622
|
+
const currentRepoKeys = new Set((existingConfig?.repos ?? []).map(r => `${r.owner}/${r.name}`));
|
|
623
|
+
const currentOrgs = new Set(existingConfig?.orgs ?? []);
|
|
624
|
+
let selectedRepos;
|
|
625
|
+
let selectedOrgs;
|
|
626
|
+
if (opts.yes && existingConfig) {
|
|
627
|
+
selectedRepos = [...currentRepoKeys];
|
|
628
|
+
selectedOrgs = [...currentOrgs];
|
|
629
|
+
console.log(` Using existing repo selection (${selectedRepos.length} repos, ${selectedOrgs.length} orgs)`);
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
const totalRepos = personalActivityRepos.length + orgRepoLists.reduce((sum, l) => sum + l.length, 0);
|
|
633
|
+
if (totalRepos === 0) {
|
|
634
|
+
console.log(chalk.yellow(' No repos found. You can add repos manually in your config file.'));
|
|
635
|
+
selectedRepos = [];
|
|
636
|
+
selectedOrgs = [];
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
console.log();
|
|
640
|
+
selectedRepos = [];
|
|
641
|
+
selectedOrgs = [...currentOrgs];
|
|
642
|
+
// Build namespace list: personal account + each org
|
|
643
|
+
const namespaces = [];
|
|
644
|
+
if (login && personalActivityRepos.length > 0)
|
|
645
|
+
namespaces.push(login);
|
|
646
|
+
for (const org of orgs)
|
|
647
|
+
namespaces.push(org);
|
|
648
|
+
let namespacesToBrowse;
|
|
649
|
+
if (namespaces.length <= 1) {
|
|
650
|
+
// Only one namespace — skip group picker
|
|
651
|
+
namespacesToBrowse = namespaces;
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
// Step 3a: pick which namespaces to browse
|
|
655
|
+
const nsDescriptions = new Map();
|
|
656
|
+
if (login) {
|
|
657
|
+
const c = personalActivityRepos.length;
|
|
658
|
+
nsDescriptions.set(login, `personal · ${c} repo${c === 1 ? '' : 's'}`);
|
|
659
|
+
}
|
|
660
|
+
for (let i = 0; i < orgs.length; i++) {
|
|
661
|
+
const c = orgRepoLists[i].length;
|
|
662
|
+
nsDescriptions.set(orgs[i], `org · ${c} repo${c === 1 ? '' : 's'}`);
|
|
663
|
+
}
|
|
664
|
+
// Pre-select namespaces that already have configured repos/orgs; default all on first run
|
|
665
|
+
const currentNamespaces = new Set();
|
|
666
|
+
for (const key of currentRepoKeys)
|
|
667
|
+
currentNamespaces.add(key.split('/')[0]);
|
|
668
|
+
for (const org of currentOrgs)
|
|
669
|
+
currentNamespaces.add(org);
|
|
670
|
+
const initialNs = currentNamespaces.size === 0
|
|
671
|
+
? namespaces
|
|
672
|
+
: namespaces.filter(ns => currentNamespaces.has(ns));
|
|
673
|
+
namespacesToBrowse = await promptRepoPicker(namespaces, {
|
|
674
|
+
title: 'Which accounts do you want to browse?',
|
|
675
|
+
initialSelected: initialNs,
|
|
676
|
+
getDescription: (ns) => nsDescriptions.get(ns) ?? '',
|
|
677
|
+
pageSize: Math.min(namespaces.length, 6),
|
|
678
|
+
});
|
|
679
|
+
console.log();
|
|
680
|
+
}
|
|
681
|
+
// Step 3b: for each selected namespace, show a focused repo picker
|
|
682
|
+
for (const ns of namespacesToBrowse) {
|
|
683
|
+
let repoKeys;
|
|
684
|
+
const descMap = new Map();
|
|
685
|
+
if (ns === login) {
|
|
686
|
+
// Personal repos — already sorted by activity (tier 1 → tier 3, then pushedAt desc)
|
|
687
|
+
repoKeys = personalActivityRepos.map(r => r.fullName);
|
|
688
|
+
for (const r of personalActivityRepos) {
|
|
689
|
+
descMap.set(r.fullName, formatAge(r.pushedAt));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
// Org repos — already sorted by pushedAt desc (sort=pushed in API call)
|
|
694
|
+
const orgIdx = orgs.indexOf(ns);
|
|
695
|
+
const orgRepos = orgIdx >= 0 ? orgRepoLists[orgIdx] : [];
|
|
696
|
+
repoKeys = orgRepos.map(r => `${r.owner}/${r.name}`);
|
|
697
|
+
for (const r of orgRepos) {
|
|
698
|
+
if (r.pushedAt)
|
|
699
|
+
descMap.set(`${r.owner}/${r.name}`, formatAge(r.pushedAt));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (repoKeys.length === 0)
|
|
703
|
+
continue;
|
|
704
|
+
const initialSel = repoKeys.filter(k => currentRepoKeys.has(k));
|
|
705
|
+
const picked = await promptRepoPicker(repoKeys, {
|
|
706
|
+
title: `Select repos from ${ns}:`,
|
|
707
|
+
initialSelected: initialSel,
|
|
708
|
+
getDescription: (key) => descMap.get(key) ?? '',
|
|
709
|
+
pageSize: 5,
|
|
710
|
+
});
|
|
711
|
+
console.log();
|
|
712
|
+
selectedRepos.push(...picked);
|
|
713
|
+
}
|
|
714
|
+
// Offer org-level monitoring when 3+ repos from the same org are selected
|
|
715
|
+
const orgSet = new Set(orgs);
|
|
716
|
+
const orgCounts = {};
|
|
717
|
+
for (const r of selectedRepos) {
|
|
718
|
+
const owner = r.split('/')[0];
|
|
719
|
+
if (orgSet.has(owner))
|
|
720
|
+
orgCounts[owner] = (orgCounts[owner] ?? 0) + 1;
|
|
721
|
+
}
|
|
722
|
+
const orgOffers = Object.entries(orgCounts)
|
|
723
|
+
.filter(([, count]) => count >= 3)
|
|
724
|
+
.map(([org]) => org);
|
|
725
|
+
for (const org of orgOffers) {
|
|
726
|
+
if (currentOrgs.has(org))
|
|
727
|
+
continue;
|
|
728
|
+
const answer = opts.yes ? 'n' : await ask(` Monitor all of ${chalk.cyan(org)} instead of individual repos? [y/N]: `);
|
|
729
|
+
if (answer.toLowerCase() === 'y') {
|
|
730
|
+
selectedOrgs.push(org);
|
|
731
|
+
selectedRepos = selectedRepos.filter(r => !r.startsWith(`${org}/`));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
console.log();
|
|
737
|
+
// ── Step 4: Review mode (cross-vendor vs single-vendor) ───────────────────
|
|
738
|
+
console.log(chalk.bold('Step 4 — review mode'));
|
|
739
|
+
const vendorConfig = await promptVendorMode(env.claudeOk, env.codexOk, existingConfig?.mode, existingConfig?.vendors?.claude?.enabled ?? true, existingConfig?.vendors?.codex?.enabled ?? true, opts);
|
|
740
|
+
console.log();
|
|
741
|
+
// ── Step 5: Primary author (cross-vendor + personal only) ───────────────────
|
|
742
|
+
console.log(chalk.bold('Step 5 — primary author'));
|
|
743
|
+
let authorVendor = 'both';
|
|
744
|
+
if (vendorConfig.mode === 'cross-vendor' && deployment === 'personal') {
|
|
745
|
+
const existingRoutes = existingConfig?.routing?.author_routes ?? null;
|
|
746
|
+
authorVendor = await promptAuthorVendor(login, existingRoutes, opts);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
const reason = vendorConfig.mode === 'single-vendor' ? 'single-vendor mode' : 'team mode';
|
|
750
|
+
console.log(chalk.dim(` Skipped — not applicable in ${reason}.`));
|
|
751
|
+
console.log();
|
|
752
|
+
}
|
|
753
|
+
// ── Step 6: Review quality ────────────────────────────────────────────────
|
|
754
|
+
console.log(chalk.bold('Step 6 — review quality'));
|
|
755
|
+
const qualityTier = await promptQualityTier(vendorConfig.claudeEnabled, vendorConfig.codexEnabled, existingConfig?.quality?.tier, opts);
|
|
756
|
+
console.log();
|
|
757
|
+
// ── Step 7: Workflow pipeline ──────────────────────────────────────────────
|
|
758
|
+
console.log(chalk.bold('Step 7 — workflow pipeline'));
|
|
759
|
+
const pipelinePreset = await promptWorkflowPipeline(opts);
|
|
760
|
+
console.log();
|
|
761
|
+
// ── Step 7.5: Max rounds (review-fix-recheck only) ────────────────────────
|
|
762
|
+
let maxRounds;
|
|
763
|
+
if (pipelinePreset === 'review-fix-recheck') {
|
|
764
|
+
console.log(chalk.bold('Step 7.5 — fix → re-check rounds'));
|
|
765
|
+
const globalWorkflowPath = join(homedir(), '.crosscheck', 'workflow.yml');
|
|
766
|
+
let currentMaxRounds;
|
|
767
|
+
if (existsSync(globalWorkflowPath)) {
|
|
768
|
+
try {
|
|
769
|
+
const raw = yaml.load(readFileSync(globalWorkflowPath, 'utf8'));
|
|
770
|
+
const fixOrRecheckStep = (raw?.steps ?? []).find(s => s.type === 'fix' || s.type === 'recheck');
|
|
771
|
+
currentMaxRounds = fixOrRecheckStep?.max_rounds;
|
|
772
|
+
}
|
|
773
|
+
catch { /* malformed — use default */ }
|
|
774
|
+
}
|
|
775
|
+
maxRounds = await promptMaxRounds(currentMaxRounds, opts);
|
|
776
|
+
console.log();
|
|
777
|
+
}
|
|
778
|
+
// ── Step 7.7: Auto-conflict-resolve (opt-in) ──────────────────────────────
|
|
779
|
+
console.log(chalk.bold('Step 7.7 — auto conflict-resolve'));
|
|
780
|
+
const currentConflictResolve = detectConflictResolveEnabled();
|
|
781
|
+
const conflictResolve = await promptConflictResolve(currentConflictResolve, opts);
|
|
782
|
+
console.log();
|
|
783
|
+
// ── Step 8: Connection type ────────────────────────────────────────────────
|
|
784
|
+
console.log(chalk.bold('Step 8 — connection type'));
|
|
785
|
+
const currentTunnel = existingConfig?.tunnel?.backend;
|
|
786
|
+
let tunnelBackend = await promptConnectionType(currentTunnel, opts);
|
|
787
|
+
let smeeChannel = existingConfig?.tunnel?.smee_channel ?? '';
|
|
788
|
+
if (tunnelBackend === 'smee') {
|
|
789
|
+
if (smeeChannel) {
|
|
790
|
+
console.log(` smee channel ${chalk.cyan(smeeChannel)}`);
|
|
791
|
+
}
|
|
792
|
+
else if (!opts.yes) {
|
|
793
|
+
console.log(chalk.dim(' Paste your smee.io channel URL below (leave blank to use localhost.run instead).\n'));
|
|
794
|
+
const channel = await ask(' smee channel URL: ');
|
|
795
|
+
if (channel) {
|
|
796
|
+
smeeChannel = channel;
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
tunnelBackend = 'localhost.run';
|
|
800
|
+
console.log(chalk.yellow(' No channel provided — falling back to localhost.run.'));
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
tunnelBackend = 'localhost.run';
|
|
805
|
+
console.log(chalk.yellow(' smee selected but no channel configured — falling back to localhost.run.'));
|
|
806
|
+
console.log(chalk.dim(' Set tunnel.smee_channel in config.yml and re-run onboard to use smee.io.'));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
console.log();
|
|
810
|
+
// ── Step 9: Clone protocol ─────────────────────────────────────────────────
|
|
811
|
+
console.log(chalk.bold('Step 9 — clone protocol'));
|
|
812
|
+
const cloneProtocol = await promptCloneProtocol(existingConfig?.clone_protocol, opts);
|
|
813
|
+
// ── Step 10: Confirm and write ─────────────────────────────────────────────
|
|
814
|
+
console.log(chalk.bold('Step 10 — review and write config'));
|
|
815
|
+
console.log();
|
|
816
|
+
console.log(` deployment ${chalk.cyan(deployment)}`);
|
|
817
|
+
console.log(` connection ${chalk.cyan(tunnelBackend)}${tunnelBackend === 'smee' && smeeChannel ? chalk.dim(` (${smeeChannel})`) : ''}`);
|
|
818
|
+
console.log(` clone ${chalk.cyan(cloneProtocol)}`);
|
|
819
|
+
console.log(` mode ${chalk.cyan(vendorConfig.mode)}`);
|
|
820
|
+
if (vendorConfig.mode === 'single-vendor') {
|
|
821
|
+
const activeVendor = vendorConfig.claudeEnabled ? 'claude' : 'codex';
|
|
822
|
+
console.log(` vendor ${chalk.cyan(activeVendor)}`);
|
|
823
|
+
}
|
|
824
|
+
if (vendorConfig.mode === 'cross-vendor' && deployment === 'personal') {
|
|
825
|
+
const routingLabel = authorVendor === 'both'
|
|
826
|
+
? 'both (attribution detection only)'
|
|
827
|
+
: `${authorVendor} → reviewed by ${authorVendor === 'claude' ? 'codex' : 'claude'}`;
|
|
828
|
+
console.log(` routing ${chalk.cyan(routingLabel)}`);
|
|
829
|
+
}
|
|
830
|
+
console.log(` quality ${chalk.cyan(qualityTier)}${chalk.dim(` — ${QUALITY_TIERS[qualityTier].description.split(' ')[0]}`)}`);
|
|
831
|
+
console.log(` pipeline ${chalk.cyan(pipelinePreset)}`);
|
|
832
|
+
if (pipelinePreset === 'review-fix-recheck') {
|
|
833
|
+
console.log(` max rounds ${chalk.cyan(String(maxRounds ?? 1))}`);
|
|
834
|
+
}
|
|
835
|
+
console.log(` conflict-resolve ${chalk.cyan(conflictResolve ? 'yes' : 'no')}`);
|
|
836
|
+
if (selectedOrgs.length > 0) {
|
|
837
|
+
console.log(` orgs ${selectedOrgs.map(o => chalk.cyan(o)).join(', ')}`);
|
|
838
|
+
}
|
|
839
|
+
if (selectedRepos.length > 0) {
|
|
840
|
+
console.log(` repos ${selectedRepos.slice(0, 5).map(r => chalk.cyan(r)).join(', ')}${selectedRepos.length > 5 ? chalk.dim(` +${selectedRepos.length - 5} more`) : ''}`);
|
|
841
|
+
}
|
|
842
|
+
if (selectedOrgs.length === 0 && selectedRepos.length === 0) {
|
|
843
|
+
console.log(` ${chalk.yellow('No repos or orgs selected. Config will have empty scope.')}`);
|
|
844
|
+
}
|
|
845
|
+
console.log(` config ${chalk.dim(configPath)}`);
|
|
846
|
+
console.log(` workflow ${chalk.dim(join(homedir(), '.crosscheck', 'workflow.yml'))}`);
|
|
847
|
+
console.log();
|
|
848
|
+
if (!opts.yes) {
|
|
849
|
+
const confirm = await ask(` Write to config? [Y/n]: `);
|
|
850
|
+
if (confirm.toLowerCase() === 'n') {
|
|
851
|
+
console.log(chalk.dim(' Aborted — no changes written.'));
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
const globalWorkflowPath = join(homedir(), '.crosscheck', 'workflow.yml');
|
|
856
|
+
const hadWorkflow = existsSync(globalWorkflowPath);
|
|
857
|
+
applyOnboardConfig(configPath, {
|
|
858
|
+
deployment,
|
|
859
|
+
login,
|
|
860
|
+
selectedRepos,
|
|
861
|
+
selectedOrgs,
|
|
862
|
+
vendorConfig,
|
|
863
|
+
authorVendor,
|
|
864
|
+
qualityTier,
|
|
865
|
+
pipelinePreset,
|
|
866
|
+
maxRounds,
|
|
867
|
+
conflictResolve,
|
|
868
|
+
tunnelBackend,
|
|
869
|
+
smeeChannel,
|
|
870
|
+
cloneProtocol,
|
|
871
|
+
});
|
|
872
|
+
console.log(chalk.green(` ✓ config written to ${configPath}`));
|
|
873
|
+
if (!hadWorkflow) {
|
|
874
|
+
console.log(chalk.green(` ✓ workflow written to ${globalWorkflowPath}`));
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
console.log(chalk.dim(` keeping existing workflow at ${globalWorkflowPath}`));
|
|
878
|
+
}
|
|
879
|
+
console.log();
|
|
880
|
+
// ── Next step hint ─────────────────────────────────────────────────────────
|
|
881
|
+
console.log(chalk.dim(' Run crosscheck watch to start monitoring.\n'));
|
|
882
|
+
}
|
|
883
|
+
//# sourceMappingURL=onboard.js.map
|