Skip to content

Commit ad10804

Browse files
authored
Merge pull request #15 from CodeAnt-AI/start-scan
cli start scan
2 parents c56011a + 27e1dd4 commit ad10804

5 files changed

Lines changed: 182 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codeant-cli",
3-
"version": "0.4.7",
3+
"version": "0.4.8",
44
"description": "Code review CLI tool",
55
"type": "module",
66
"bin": {

scans.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,47 @@ codeant scans dismissed --repo acme/backend
217217
# List dismissed secrets alerts
218218
codeant scans dismissed --repo acme/backend --analysis-type secrets
219219
```
220+
221+
---
222+
223+
### `scans start-scan`
224+
225+
Trigger a new analysis run for a repository.
226+
227+
```bash
228+
codeant scans start-scan [options]
229+
```
230+
231+
All options are optional — `repo`, `branch`, and `commit` are auto-detected from the local git context when not provided.
232+
233+
**Options:**
234+
235+
| Option | Description |
236+
|--------|-------------|
237+
| `--repo <repo>` | Repository in `owner/repo` format (auto-detected from git remote) |
238+
| `--branch <name>` | Branch to scan (auto-detected from current checkout) |
239+
| `--commit <sha>` | Commit SHA to scan (resolved from remote HEAD of branch if omitted) |
240+
| `--include <paths>` | Comma-separated file path glob patterns to include |
241+
| `--exclude <paths>` | Comma-separated file path glob patterns to exclude |
242+
243+
**Examples:**
244+
245+
```bash
246+
# Zero-config — auto-detects repo, branch, and latest commit
247+
codeant scans start-scan
248+
249+
# Explicit repo and branch
250+
codeant scans start-scan --repo CodeAnt-AI/codeant-cli --branch main
251+
252+
# Explicit commit SHA
253+
codeant scans start-scan \
254+
--repo CodeAnt-AI/codeant-cli \
255+
--branch main \
256+
--commit b509bffa1721da442f35a7ccab969822711a67f0
257+
258+
# Scan only specific files
259+
codeant scans start-scan \
260+
--branch main \
261+
--include "src/main.py,src/utils.py" \
262+
--exclude "tests/,*.md"
263+
```

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

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)