Skip to content

Commit 3b20ae1

Browse files
committed
cli start scan
1 parent c56011a commit 3b20ae1

3 files changed

Lines changed: 136 additions & 0 deletions

File tree

src/commands/scans/index.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { runHistory } from './history.js';
44
import { runGet } from './get.js';
55
import { runResults } from './results.js';
66
import { runDismissed } from './dismissed.js';
7+
import { runStartScan } from './start-scan.js';
78
import { setQuiet, setNoColor } from './lib/log.js';
89
import { setNoColor as tableSetNoColor } from './formatters/table.js';
910

@@ -118,4 +119,28 @@ export default function registerScansCommands(program, { runCmd }) {
118119
.action((opts) =>
119120
runCmd(() => runDismissed({ repo: opts.repo, analysisType: opts.analysisType }))
120121
);
122+
123+
// ── start-scan ─────────────────────────────────────────────────────────────
124+
scans
125+
.command('start-scan')
126+
.description('Trigger a new analysis run for a repository')
127+
.option('--repo <repo>', 'Repository (owner/repo, auto-detected from git remote)')
128+
.option('--branch <name>', 'Branch to scan (auto-detected from current checkout)')
129+
.option('--commit <sha>', 'Commit SHA to scan (resolved from remote if omitted)')
130+
.option('--include <paths>', 'Comma-separated file path glob patterns to include')
131+
.option('--exclude <paths>', 'Comma-separated file path glob patterns to exclude')
132+
.action(async (opts) => {
133+
try {
134+
await runStartScan({
135+
repo: opts.repo,
136+
branch: opts.branch,
137+
commit: opts.commit,
138+
include: opts.include,
139+
exclude: opts.exclude,
140+
});
141+
} catch (err) {
142+
process.stderr.write(JSON.stringify({ error: err.message }) + '\n');
143+
process.exit(err.exitCode ?? 1);
144+
}
145+
});
121146
}

src/commands/scans/start-scan.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { exec } from 'child_process';
2+
import { promisify } from 'util';
3+
import { detectRepoName } from '../../scm/index.js';
4+
import { startScan } from '../../scans/startScan.js';
5+
6+
const execAsync = promisify(exec);
7+
8+
// Mirrors splitGlobs in src/index.js (not exported) — preserves commas inside {} brace expansions
9+
function splitGlobs(input) {
10+
const parts = [];
11+
let current = '';
12+
let depth = 0;
13+
for (const ch of String(input)) {
14+
if (ch === '{') depth++;
15+
else if (ch === '}') depth--;
16+
if (ch === ',' && depth === 0) {
17+
parts.push(current.trim());
18+
current = '';
19+
} else {
20+
current += ch;
21+
}
22+
}
23+
if (current.trim()) parts.push(current.trim());
24+
return parts.filter(Boolean);
25+
}
26+
27+
async function resolveCurrentBranch() {
28+
try {
29+
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD');
30+
return stdout.trim();
31+
} catch {
32+
return null;
33+
}
34+
}
35+
36+
async function resolveRemoteCommit(branch) {
37+
try {
38+
const { stdout } = await execAsync(`git ls-remote origin refs/heads/${branch}`);
39+
const sha = stdout.trim().split(/\s+/)[0];
40+
if (!sha) {
41+
const err = new Error(`Branch "${branch}" not found on remote origin. Pass --commit <sha> explicitly.`);
42+
err.exitCode = 1;
43+
throw err;
44+
}
45+
return sha;
46+
} catch (err) {
47+
if (err.exitCode) throw err;
48+
const wrapped = new Error(`Could not resolve commit for branch "${branch}": ${err.message}. Pass --commit <sha> explicitly.`);
49+
wrapped.exitCode = 1;
50+
throw wrapped;
51+
}
52+
}
53+
54+
export async function runStartScan({ repo, branch, commit, include, exclude } = {}) {
55+
const resolvedRepo = repo || detectRepoName();
56+
if (!resolvedRepo) {
57+
const err = new Error('Could not detect repo name. Use --repo owner/repo');
58+
err.exitCode = 1;
59+
throw err;
60+
}
61+
62+
const resolvedBranch = branch || await resolveCurrentBranch();
63+
if (!resolvedBranch) {
64+
const err = new Error('Could not detect current branch. Use --branch <name>');
65+
err.exitCode = 1;
66+
throw err;
67+
}
68+
69+
const commitId = commit || await resolveRemoteCommit(resolvedBranch);
70+
71+
const includeFiles = include ? splitGlobs(include) : [];
72+
const excludeFiles = exclude ? splitGlobs(exclude) : [];
73+
74+
const result = await startScan({
75+
repo: resolvedRepo,
76+
branch: resolvedBranch,
77+
commitId,
78+
includeFiles: includeFiles.length ? includeFiles : undefined,
79+
excludeFiles: excludeFiles.length ? excludeFiles : undefined,
80+
});
81+
82+
if (!result.success) {
83+
const err = new Error(result.error || 'Failed to start scan');
84+
err.exitCode = 1;
85+
throw err;
86+
}
87+
88+
console.log(result.message || 'Analysis started');
89+
}

src/scans/startScan.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { fetchApi } from '../utils/fetchApi.js';
2+
3+
export async function startScan({ repo, branch, commitId, includeFiles, excludeFiles }) {
4+
try {
5+
const body = { repo, branch, commit_id: commitId };
6+
if (includeFiles && includeFiles.length > 0) body.include_files = includeFiles;
7+
if (excludeFiles && excludeFiles.length > 0) body.exclude_files = excludeFiles;
8+
9+
const response = await fetchApi('/extension/analysis/run', 'POST', body);
10+
11+
if (!response) {
12+
return { success: false, error: 'Failed to connect to CodeAnt server' };
13+
}
14+
if (response.status === 'error') {
15+
return { success: false, error: response.message || 'Failed to start scan' };
16+
}
17+
18+
return { success: true, ...response };
19+
} catch (error) {
20+
return { success: false, error: error.message || 'Failed to start scan' };
21+
}
22+
}

0 commit comments

Comments
 (0)