@motivation-labs/crosscheck 0.7.0 → 0.7.1-beta.07ebaac.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/dist/cli.js +17 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands/issue.d.ts.map +1 -1
- package/dist/commands/issue.js +29 -15
- package/dist/commands/issue.js.map +1 -1
- package/dist/commands/onboard.d.ts +3 -2
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +350 -259
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/run.d.ts +8 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +141 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +4 -1
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/watch.d.ts +1 -0
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +4 -1
- package/dist/commands/watch.js.map +1 -1
- package/dist/lib/repo-picker.d.ts +5 -3
- package/dist/lib/repo-picker.d.ts.map +1 -1
- package/dist/lib/repo-picker.js +140 -122
- package/dist/lib/repo-picker.js.map +1 -1
- package/dist/lib/runner.d.ts +2 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +19 -7
- package/dist/lib/runner.js.map +1 -1
- package/dist/lib/workflow.d.ts.map +1 -1
- package/dist/lib/workflow.js +7 -2
- package/dist/lib/workflow.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/onboard.js
CHANGED
|
@@ -1,285 +1,376 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import { createInterface } from 'readline';
|
|
5
4
|
import chalk from 'chalk';
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
import { createInterface } from 'readline';
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import { getGithubToken, loadConfig, resolveConfigPath, detectGitHubLogin, promptDeploymentMode, patchDeploymentConfig, } from '../config/loader.js';
|
|
8
|
+
import { listUserRepos, listUserOrgs, listOrgRepos } 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 } from '../lib/repo-picker.js';
|
|
13
|
+
function ask(question) {
|
|
14
14
|
return new Promise(resolve => {
|
|
15
15
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
16
16
|
rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
async function checkEnv() {
|
|
20
|
+
let codexOk = false;
|
|
21
|
+
let claudeOk = false;
|
|
22
|
+
try {
|
|
23
|
+
execSync('codex --version 2>&1', { encoding: 'utf8' });
|
|
24
|
+
const auth = await checkCodexAuth();
|
|
25
|
+
codexOk = auth.ok;
|
|
26
|
+
const icon = auth.ok ? chalk.green('✓') : chalk.red('✗');
|
|
27
|
+
console.log(` ${icon} ${'codex CLI'.padEnd(20)} ${auth.detail}`);
|
|
28
|
+
if (!auth.ok)
|
|
29
|
+
console.log(` ${chalk.dim('→')} ${chalk.yellow('Run: codex login --device-auth')}`);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
console.log(` ${chalk.red('✗')} ${'codex CLI'.padEnd(20)} not found`);
|
|
33
|
+
console.log(` ${chalk.dim('→')} ${chalk.yellow('Install: npm install -g @openai/codex')}`);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const auth = await checkClaudeAuth();
|
|
37
|
+
claudeOk = auth.ok;
|
|
38
|
+
const icon = auth.ok ? chalk.green('✓') : chalk.red('✗');
|
|
39
|
+
console.log(` ${icon} ${'claude CLI'.padEnd(20)} ${auth.detail}`);
|
|
40
|
+
if (!auth.ok)
|
|
41
|
+
console.log(` ${chalk.dim('→')} ${chalk.yellow('Run: claude auth login')}`);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
console.log(` ${chalk.red('✗')} ${'claude CLI'.padEnd(20)} not found`);
|
|
45
|
+
console.log(` ${chalk.dim('→')} ${chalk.yellow('Install: npm install -g @anthropic-ai/claude-code')}`);
|
|
46
|
+
}
|
|
47
|
+
const envToken = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
48
|
+
let ghAuthed = false;
|
|
49
|
+
try {
|
|
50
|
+
execSync('gh --version 2>&1', { encoding: 'utf8' });
|
|
51
|
+
let authOutput = '';
|
|
52
|
+
try {
|
|
53
|
+
authOutput = execSync('gh auth status 2>&1', { encoding: 'utf8' });
|
|
54
|
+
}
|
|
55
|
+
catch { /* GITHUB_TOKEN in use */ }
|
|
56
|
+
ghAuthed = authOutput.includes('Logged in') || !!envToken;
|
|
57
|
+
const icon = ghAuthed ? chalk.green('✓') : chalk.red('✗');
|
|
58
|
+
console.log(` ${icon} ${'gh CLI'.padEnd(20)} ${ghAuthed ? 'authenticated' : 'not authenticated'}`);
|
|
59
|
+
if (!ghAuthed)
|
|
60
|
+
console.log(` ${chalk.dim('→')} ${chalk.yellow('Run: gh auth login')}`);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
console.log(` ${chalk.red('✗')} ${'gh CLI'.padEnd(20)} not found`);
|
|
64
|
+
console.log(` ${chalk.dim('→')} ${chalk.yellow('Install: brew install gh && gh auth login')}`);
|
|
65
|
+
}
|
|
66
|
+
if (!claudeOk && !codexOk) {
|
|
67
|
+
console.log(chalk.red('\nAt least one AI CLI (codex or claude) must be authenticated.\n'));
|
|
68
|
+
return { ok: false, claudeOk, codexOk };
|
|
69
|
+
}
|
|
70
|
+
if (!ghAuthed) {
|
|
71
|
+
console.log(chalk.red('\nGitHub auth is required to fetch repos and register webhooks.\n'));
|
|
72
|
+
return { ok: false, claudeOk, codexOk };
|
|
73
|
+
}
|
|
74
|
+
return { ok: true, claudeOk, codexOk };
|
|
35
75
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
async function promptVendorMode(claudeOk, codexOk, currentMode, currentClaudeEnabled, currentCodexEnabled, opts) {
|
|
77
|
+
const bothAvailable = claudeOk && codexOk;
|
|
78
|
+
if (!bothAvailable) {
|
|
79
|
+
// Only one CLI is available — auto-select single-vendor
|
|
80
|
+
const vendor = claudeOk ? 'claude' : 'codex';
|
|
81
|
+
console.log(` Mode: ${chalk.cyan('single-vendor')} (only ${chalk.bold(vendor)} is available)`);
|
|
82
|
+
return { mode: 'single-vendor', claudeEnabled: claudeOk, codexEnabled: codexOk };
|
|
83
|
+
}
|
|
84
|
+
if (opts.yes && currentMode) {
|
|
85
|
+
console.log(` Using existing mode: ${chalk.cyan(currentMode)}`);
|
|
86
|
+
return {
|
|
87
|
+
mode: currentMode,
|
|
88
|
+
claudeEnabled: currentClaudeEnabled,
|
|
89
|
+
codexEnabled: currentCodexEnabled,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (currentMode && !opts.yes) {
|
|
93
|
+
const keep = await ask(` Current mode: ${chalk.cyan(currentMode)}. Keep this? [Y/n]: `);
|
|
94
|
+
if (keep.toLowerCase() !== 'n') {
|
|
95
|
+
return {
|
|
96
|
+
mode: currentMode,
|
|
97
|
+
claudeEnabled: currentClaudeEnabled,
|
|
98
|
+
codexEnabled: currentCodexEnabled,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
}
|
|
103
|
+
console.log(' How should reviews be assigned?\n');
|
|
104
|
+
console.log(` [1] cross-vendor — ${chalk.dim('Claude reviews Codex PRs; Codex reviews Claude PRs')}`);
|
|
105
|
+
console.log(` [2] single-vendor — ${chalk.dim('one AI reviews all PRs')}`);
|
|
106
|
+
console.log();
|
|
107
|
+
const choice = await ask(' Choice [1]: ');
|
|
108
|
+
console.log();
|
|
109
|
+
if (choice === '2') {
|
|
110
|
+
console.log(' Which AI should review all PRs?\n');
|
|
111
|
+
console.log(` [1] claude`);
|
|
112
|
+
console.log(` [2] codex`);
|
|
113
|
+
console.log();
|
|
114
|
+
const vendorChoice = await ask(' Choice [1]: ');
|
|
115
|
+
console.log();
|
|
116
|
+
const vendor = vendorChoice === '2' ? 'codex' : 'claude';
|
|
117
|
+
return {
|
|
118
|
+
mode: 'single-vendor',
|
|
119
|
+
claudeEnabled: vendor === 'claude',
|
|
120
|
+
codexEnabled: vendor === 'codex',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return { mode: 'cross-vendor', claudeEnabled: true, codexEnabled: true };
|
|
54
124
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const attr = await ask(' Reviewer attribution line (replaces "Reviewed by {vendor}", Enter to skip): ');
|
|
74
|
-
brand.reviewer_attribution = attr;
|
|
75
|
-
return brand;
|
|
125
|
+
async function promptWorkflowPipeline(currentAutoFixEnabled, opts) {
|
|
126
|
+
if (opts.yes) {
|
|
127
|
+
const preset = currentAutoFixEnabled === true ? 'review-fix' : 'review-only';
|
|
128
|
+
console.log(` Using existing pipeline: ${chalk.cyan(preset)}`);
|
|
129
|
+
return preset;
|
|
130
|
+
}
|
|
131
|
+
console.log(' What should happen after a review?\n');
|
|
132
|
+
console.log(` [1] review only — ${chalk.dim('AI posts a comment; you handle fixes')}`);
|
|
133
|
+
console.log(` [2] review → fix — ${chalk.dim('AI reviews, then auto-applies fixes')} ${chalk.green('(recommended)')}`);
|
|
134
|
+
console.log(` [3] review → fix → re-check — ${chalk.dim('full loop: review, fix, re-review to confirm')}`);
|
|
135
|
+
console.log();
|
|
136
|
+
const choice = await ask(' Choice [2]: ');
|
|
137
|
+
console.log();
|
|
138
|
+
if (choice === '1')
|
|
139
|
+
return 'review-only';
|
|
140
|
+
if (choice === '3')
|
|
141
|
+
return 'review-fix-recheck';
|
|
142
|
+
return 'review-fix';
|
|
76
143
|
}
|
|
77
|
-
//
|
|
78
|
-
|
|
144
|
+
// Workflow YAML for the recheck preset — written to ~/.crosscheck/workflow.yml
|
|
145
|
+
const WORKFLOW_RECHECK_YAML = `# crosscheck workflow — generated by crosscheck onboard
|
|
146
|
+
# Place a .crosscheck/workflow.yml in your project root to override this global file.
|
|
147
|
+
|
|
148
|
+
on: [opened, synchronize]
|
|
149
|
+
steps:
|
|
150
|
+
- name: review
|
|
151
|
+
type: review
|
|
152
|
+
reviewer: auto
|
|
153
|
+
max_rounds: 1
|
|
154
|
+
|
|
155
|
+
- name: fix
|
|
156
|
+
type: fix
|
|
157
|
+
reviewer: origin
|
|
158
|
+
when: "review.verdict != 'APPROVE'"
|
|
159
|
+
max_rounds: 1
|
|
160
|
+
|
|
161
|
+
- name: recheck
|
|
162
|
+
type: recheck
|
|
163
|
+
reviewer: auto
|
|
164
|
+
when: "fix.applied_count > 0"
|
|
165
|
+
max_rounds: 1
|
|
166
|
+
`;
|
|
167
|
+
export async function runOnboard(opts = {}) {
|
|
168
|
+
if (!process.stdin.isTTY) {
|
|
169
|
+
console.error(chalk.red('onboard requires an interactive terminal.'));
|
|
170
|
+
console.error(chalk.dim('Run crosscheck init and edit crosscheck.config.yml manually.'));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
79
173
|
console.log(chalk.bold('\ncrosscheck onboard\n'));
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
console.log();
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
let token
|
|
115
|
-
let login = '';
|
|
116
|
-
let detectedOrgs = [];
|
|
174
|
+
// ── Step 1: Auth check ─────────────────────────────────────────────────────
|
|
175
|
+
console.log(chalk.bold('Step 1 — environment check'));
|
|
176
|
+
const env = await checkEnv();
|
|
177
|
+
if (!env.ok)
|
|
178
|
+
process.exit(1);
|
|
179
|
+
console.log();
|
|
180
|
+
// ── Step 2: Deployment mode ────────────────────────────────────────────────
|
|
181
|
+
console.log(chalk.bold('Step 2 — deployment mode'));
|
|
182
|
+
const configPath = opts.config ?? resolveConfigPath() ?? join(homedir(), '.crosscheck', 'config.yml');
|
|
183
|
+
const existingConfig = existsSync(configPath) ? loadConfig(configPath) : null;
|
|
184
|
+
const currentDeployment = existingConfig?.deployment;
|
|
185
|
+
let deployment;
|
|
186
|
+
if (opts.personal) {
|
|
187
|
+
deployment = 'personal';
|
|
188
|
+
console.log(` Mode: ${chalk.cyan('personal')} (--personal flag)`);
|
|
189
|
+
}
|
|
190
|
+
else if (opts.team) {
|
|
191
|
+
deployment = 'team';
|
|
192
|
+
console.log(` Mode: ${chalk.cyan('team')} (--team flag)`);
|
|
193
|
+
}
|
|
194
|
+
else if (currentDeployment && !opts.yes) {
|
|
195
|
+
const keep = await ask(` Current mode: ${chalk.cyan(currentDeployment)}. Keep this? [Y/n]: `);
|
|
196
|
+
deployment = keep.toLowerCase() === 'n' ? await promptDeploymentMode() : currentDeployment;
|
|
197
|
+
}
|
|
198
|
+
else if (currentDeployment && opts.yes) {
|
|
199
|
+
deployment = currentDeployment;
|
|
200
|
+
console.log(` Using existing mode: ${chalk.cyan(deployment)}`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
deployment = await promptDeploymentMode();
|
|
204
|
+
}
|
|
205
|
+
console.log();
|
|
206
|
+
// ── Step 3: Repo selection ─────────────────────────────────────────────────
|
|
207
|
+
console.log(chalk.bold('Step 3 — select repos to monitor'));
|
|
208
|
+
let token;
|
|
117
209
|
try {
|
|
118
210
|
token = getGithubToken();
|
|
119
|
-
login = detectGitHubLogin() ?? '';
|
|
120
|
-
detectedOrgs = await listUserOrgs(token);
|
|
121
211
|
}
|
|
122
|
-
catch {
|
|
123
|
-
console.
|
|
212
|
+
catch (err) {
|
|
213
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
214
|
+
process.exit(1);
|
|
124
215
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
let
|
|
142
|
-
let
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
: existing.users.length > 0 ? 1
|
|
154
|
-
: existing.orgs.length > 0 ? 2
|
|
155
|
-
: existing.repos.length > 0 ? 1 // curated repos-only = personal scope
|
|
156
|
-
: 3)
|
|
157
|
-
: 3;
|
|
158
|
-
const scopeIdx = await pickOne(`What should crosscheck monitor?${orgSuffix}`, [
|
|
159
|
-
` ${chalk.bold('[1] My personal repos only')} — github.com/${login || 'you'}/* — side projects you own directly`,
|
|
160
|
-
` ${chalk.bold('[2] My org repos only')} — ${orgPreview}`,
|
|
161
|
-
` ${chalk.bold('[3] Both personal repos + orgs')} — everything across your GitHub account ← recommended`,
|
|
162
|
-
], existingScope);
|
|
163
|
-
const includePersonal = scopeIdx !== 2;
|
|
164
|
-
const includeOrgs = scopeIdx !== 1;
|
|
165
|
-
if (includeOrgs && detectedOrgs.length > 0) {
|
|
166
|
-
orgs = await promptOrgPicker(detectedOrgs, existing?.orgs.length ? existing.orgs : undefined);
|
|
167
|
-
}
|
|
168
|
-
if (includePersonal && token && login) {
|
|
169
|
-
if (scopeIdx === 1) {
|
|
170
|
-
// Curated mode (UC-01): pick specific repos
|
|
171
|
-
console.log(chalk.dim('\n Fetching your repos...'));
|
|
172
|
-
try {
|
|
173
|
-
const repoList = await fetchActiveRepos(login, token);
|
|
174
|
-
const existingNames = existing?.repos.map(r => `${r.owner}/${r.name}`) ?? [];
|
|
175
|
-
const picked = await promptRepoPicker(repoList, existingNames.length ? existingNames : undefined);
|
|
176
|
-
repos = picked.map(full => {
|
|
177
|
-
const [owner, name] = full.split('/');
|
|
178
|
-
return { owner: owner ?? login, name: name ?? full };
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
console.log(chalk.dim(' (Could not fetch repos — add them manually to config.repos)\n'));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
// "Both" mode (UC-02): monitor all personal repos via users field
|
|
187
|
-
users = [login];
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
else if (includePersonal) {
|
|
191
|
-
users = login ? [login] : [];
|
|
216
|
+
const login = detectGitHubLogin() ?? '';
|
|
217
|
+
console.log(chalk.dim(` Fetching repos for ${login || 'your account'}...`));
|
|
218
|
+
const [personalRepos, orgs] = await Promise.all([
|
|
219
|
+
login ? listUserRepos(login, token, true).catch(() => []) : Promise.resolve([]),
|
|
220
|
+
listUserOrgs(token).catch(() => []),
|
|
221
|
+
]);
|
|
222
|
+
const orgRepoLists = await Promise.all(orgs.map(org => listOrgRepos(org, token).catch(() => [])));
|
|
223
|
+
const allRepos = [];
|
|
224
|
+
for (const r of personalRepos)
|
|
225
|
+
allRepos.push(`${r.owner}/${r.name}`);
|
|
226
|
+
for (let i = 0; i < orgs.length; i++) {
|
|
227
|
+
for (const r of orgRepoLists[i])
|
|
228
|
+
allRepos.push(`${r.owner}/${r.name}`);
|
|
229
|
+
}
|
|
230
|
+
const currentRepoKeys = new Set((existingConfig?.repos ?? []).map(r => `${r.owner}/${r.name}`));
|
|
231
|
+
const currentOrgs = new Set(existingConfig?.orgs ?? []);
|
|
232
|
+
let selectedRepos;
|
|
233
|
+
let selectedOrgs;
|
|
234
|
+
if (opts.yes && existingConfig) {
|
|
235
|
+
selectedRepos = [...currentRepoKeys];
|
|
236
|
+
selectedOrgs = [...currentOrgs];
|
|
237
|
+
console.log(` Using existing repo selection (${selectedRepos.length} repos, ${selectedOrgs.length} orgs)`);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
if (allRepos.length === 0) {
|
|
241
|
+
console.log(chalk.yellow(' No repos found. You can add repos manually in your config file.'));
|
|
242
|
+
selectedRepos = [];
|
|
243
|
+
selectedOrgs = [];
|
|
192
244
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
245
|
+
else {
|
|
246
|
+
const sorted = [
|
|
247
|
+
...allRepos.filter(r => currentRepoKeys.has(r)),
|
|
248
|
+
...allRepos.filter(r => !currentRepoKeys.has(r)),
|
|
249
|
+
];
|
|
250
|
+
console.log(chalk.dim(` Found ${sorted.length} repos. Use arrows + space to select, enter to confirm.\n`));
|
|
251
|
+
const picked = await promptRepoPicker(sorted, {
|
|
252
|
+
title: 'Select repos to monitor:',
|
|
253
|
+
initialSelected: [...currentRepoKeys],
|
|
254
|
+
});
|
|
255
|
+
console.log();
|
|
256
|
+
const orgSet = new Set(orgs);
|
|
257
|
+
const orgCounts = {};
|
|
258
|
+
for (const r of picked) {
|
|
259
|
+
const owner = r.split('/')[0];
|
|
260
|
+
if (orgSet.has(owner))
|
|
261
|
+
orgCounts[owner] = (orgCounts[owner] ?? 0) + 1;
|
|
203
262
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
console.log(chalk.dim(' Add routing.allowed_authors to your config to restrict later.\n'));
|
|
263
|
+
const orgOffers = Object.entries(orgCounts).filter(([, count]) => count >= 3).map(([org]) => org);
|
|
264
|
+
selectedOrgs = [...currentOrgs];
|
|
265
|
+
selectedRepos = picked;
|
|
266
|
+
for (const org of orgOffers) {
|
|
267
|
+
if (currentOrgs.has(org))
|
|
268
|
+
continue;
|
|
269
|
+
const answer = opts.yes ? 'n' : await ask(` Monitor all of ${chalk.cyan(org)} instead of individual repos? [y/N]: `);
|
|
270
|
+
if (answer.toLowerCase() === 'y') {
|
|
271
|
+
selectedOrgs.push(org);
|
|
272
|
+
selectedRepos = selectedRepos.filter(r => !r.startsWith(`${org}/`));
|
|
215
273
|
}
|
|
216
274
|
}
|
|
217
275
|
}
|
|
218
|
-
// ── Team path ─────────────────────────────────────────────────────────────
|
|
219
276
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
277
|
+
console.log();
|
|
278
|
+
// ── Step 4: Review mode (cross-vendor vs single-vendor) ───────────────────
|
|
279
|
+
console.log(chalk.bold('Step 4 — review mode'));
|
|
280
|
+
const vendorConfig = await promptVendorMode(env.claudeOk, env.codexOk, existingConfig?.mode, existingConfig?.vendors?.claude?.enabled ?? true, existingConfig?.vendors?.codex?.enabled ?? true, opts);
|
|
281
|
+
console.log();
|
|
282
|
+
// ── Step 5: Workflow pipeline ──────────────────────────────────────────────
|
|
283
|
+
console.log(chalk.bold('Step 5 — workflow pipeline'));
|
|
284
|
+
const pipelinePreset = await promptWorkflowPipeline(existingConfig?.post_review?.auto_fix?.enabled, opts);
|
|
285
|
+
console.log();
|
|
286
|
+
// ── Step 6: Confirm and write ──────────────────────────────────────────────
|
|
287
|
+
console.log(chalk.bold('Step 6 — review and write config'));
|
|
288
|
+
console.log();
|
|
289
|
+
console.log(` deployment ${chalk.cyan(deployment)}`);
|
|
290
|
+
console.log(` mode ${chalk.cyan(vendorConfig.mode)}`);
|
|
291
|
+
if (vendorConfig.mode === 'single-vendor') {
|
|
292
|
+
const activeVendor = vendorConfig.claudeEnabled ? 'claude' : 'codex';
|
|
293
|
+
console.log(` vendor ${chalk.cyan(activeVendor)}`);
|
|
294
|
+
}
|
|
295
|
+
console.log(` pipeline ${chalk.cyan(pipelinePreset)}`);
|
|
296
|
+
if (selectedOrgs.length > 0) {
|
|
297
|
+
console.log(` orgs ${selectedOrgs.map(o => chalk.cyan(o)).join(', ')}`);
|
|
298
|
+
}
|
|
299
|
+
if (selectedRepos.length > 0) {
|
|
300
|
+
console.log(` repos ${selectedRepos.slice(0, 5).map(r => chalk.cyan(r)).join(', ')}${selectedRepos.length > 5 ? chalk.dim(` +${selectedRepos.length - 5} more`) : ''}`);
|
|
301
|
+
}
|
|
302
|
+
if (selectedOrgs.length === 0 && selectedRepos.length === 0) {
|
|
303
|
+
console.log(` ${chalk.yellow('No repos or orgs selected. Config will have empty scope.')}`);
|
|
304
|
+
}
|
|
305
|
+
console.log(` config ${chalk.dim(configPath)}`);
|
|
306
|
+
if (pipelinePreset === 'review-fix-recheck') {
|
|
307
|
+
const workflowPath = join(homedir(), '.crosscheck', 'workflow.yml');
|
|
308
|
+
console.log(` workflow ${chalk.dim(workflowPath)}`);
|
|
251
309
|
}
|
|
252
|
-
// ── Optional branding (all paths) ─────────────────────────────────────────
|
|
253
|
-
const brand = await askBranding(existing?.brand);
|
|
254
|
-
// ── Confirmation preview ──────────────────────────────────────────────────
|
|
255
|
-
const COL = 14;
|
|
256
|
-
console.log(chalk.bold('\n Your crosscheck config:\n'));
|
|
257
|
-
console.log(` ${'persona'.padEnd(COL)}${deployment}`);
|
|
258
|
-
if (orgs.length)
|
|
259
|
-
console.log(` ${'orgs'.padEnd(COL)}${orgs.join(', ')}`);
|
|
260
|
-
if (users.length)
|
|
261
|
-
console.log(` ${'users'.padEnd(COL)}${users.join(', ')}`);
|
|
262
|
-
if (repos.length)
|
|
263
|
-
console.log(` ${'repos'.padEnd(COL)}${repos.map(r => `${r.owner}/${r.name}`).join(', ')}`);
|
|
264
|
-
console.log(` ${'filter'.padEnd(COL)}${allowedAuthors.length ? `author = ${allowedAuthors.join(', ')}` : 'all authors'}`);
|
|
265
|
-
if (autoFix)
|
|
266
|
-
console.log(` ${'auto-fix'.padEnd(COL)}${deliveryMode}`);
|
|
267
|
-
if (brand.service_name !== 'crosscheck')
|
|
268
|
-
console.log(` ${'brand'.padEnd(COL)}${brand.service_name}`);
|
|
269
|
-
console.log(` ${'config'.padEnd(COL)}${configPath}`);
|
|
270
310
|
console.log();
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
311
|
+
if (!opts.yes) {
|
|
312
|
+
const confirm = await ask(` Write to config? [Y/n]: `);
|
|
313
|
+
if (confirm.toLowerCase() === 'n') {
|
|
314
|
+
console.log(chalk.dim(' Aborted — no changes written.'));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
319
|
+
patchDeploymentConfig(configPath, deployment, login, selectedOrgs, true);
|
|
320
|
+
// Patch all fields after deployment config is written
|
|
321
|
+
const raw = (yaml.load(readFileSync(configPath, 'utf8')) ?? {});
|
|
322
|
+
// Repos
|
|
323
|
+
raw.repos = selectedRepos.map(r => {
|
|
324
|
+
const [owner, name] = r.split('/');
|
|
325
|
+
return { owner, name };
|
|
281
326
|
});
|
|
282
|
-
|
|
283
|
-
|
|
327
|
+
if (selectedRepos.length > 0 || selectedOrgs.length > 0)
|
|
328
|
+
delete raw.users;
|
|
329
|
+
// Vendor mode
|
|
330
|
+
raw.mode = vendorConfig.mode;
|
|
331
|
+
if (!raw.vendors || typeof raw.vendors !== 'object')
|
|
332
|
+
raw.vendors = {};
|
|
333
|
+
const vendors = raw.vendors;
|
|
334
|
+
if (!vendors.claude)
|
|
335
|
+
vendors.claude = {};
|
|
336
|
+
if (!vendors.codex)
|
|
337
|
+
vendors.codex = {};
|
|
338
|
+
vendors.claude.enabled = vendorConfig.claudeEnabled;
|
|
339
|
+
vendors.codex.enabled = vendorConfig.codexEnabled;
|
|
340
|
+
// Workflow pipeline
|
|
341
|
+
if (!raw.post_review || typeof raw.post_review !== 'object')
|
|
342
|
+
raw.post_review = {};
|
|
343
|
+
const postReview = raw.post_review;
|
|
344
|
+
if (!postReview.auto_fix || typeof postReview.auto_fix !== 'object')
|
|
345
|
+
postReview.auto_fix = {};
|
|
346
|
+
const autoFix = postReview.auto_fix;
|
|
347
|
+
if (pipelinePreset === 'review-only') {
|
|
348
|
+
autoFix.enabled = false;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
autoFix.enabled = true;
|
|
352
|
+
autoFix.trigger = 'on_issues';
|
|
353
|
+
if (!autoFix.delivery || typeof autoFix.delivery !== 'object')
|
|
354
|
+
autoFix.delivery = {};
|
|
355
|
+
const delivery = autoFix.delivery;
|
|
356
|
+
if (!delivery.mode)
|
|
357
|
+
delivery.mode = 'commit';
|
|
358
|
+
}
|
|
359
|
+
writeFileSync(configPath, yaml.dump(raw, { lineWidth: -1, noRefs: true }));
|
|
360
|
+
console.log(chalk.green(` ✓ config written to ${configPath}`));
|
|
361
|
+
// Manage global workflow.yml — write for recheck preset, remove stale file otherwise
|
|
362
|
+
const globalWorkflowPath = join(homedir(), '.crosscheck', 'workflow.yml');
|
|
363
|
+
if (pipelinePreset === 'review-fix-recheck') {
|
|
364
|
+
mkdirSync(join(homedir(), '.crosscheck'), { recursive: true });
|
|
365
|
+
writeFileSync(globalWorkflowPath, WORKFLOW_RECHECK_YAML);
|
|
366
|
+
console.log(chalk.green(` ✓ workflow written to ${globalWorkflowPath}`));
|
|
367
|
+
}
|
|
368
|
+
else if (existsSync(globalWorkflowPath)) {
|
|
369
|
+
unlinkSync(globalWorkflowPath);
|
|
370
|
+
console.log(chalk.dim(` ✓ stale global workflow removed (pipeline changed to ${pipelinePreset})`));
|
|
371
|
+
}
|
|
372
|
+
console.log();
|
|
373
|
+
// ── Step 7: Next step hint ────────────────────────────────────────────────
|
|
374
|
+
console.log(chalk.dim(' Run crosscheck watch to start monitoring.\n'));
|
|
284
375
|
}
|
|
285
376
|
//# sourceMappingURL=onboard.js.map
|