codegate-ai 0.1.8 → 0.2.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-prompts.d.ts +1 -0
- package/dist/cli-prompts.js +29 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +44 -7
- package/dist/commands/scan-command/helpers.d.ts +1 -0
- package/dist/commands/scan-command/helpers.js +23 -0
- package/dist/commands/scan-command.d.ts +1 -0
- package/dist/commands/scan-command.js +8 -4
- package/dist/scan-target/helpers.d.ts +6 -0
- package/dist/scan-target/helpers.js +35 -0
- package/dist/scan-target/staging.d.ts +9 -1
- package/dist/scan-target/staging.js +107 -8
- package/dist/scan-target/types.d.ts +3 -0
- package/dist/scan-target.js +18 -2
- package/dist/tui/views/dashboard.js +1 -1
- package/package.json +1 -1
package/dist/cli-prompts.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export declare function promptDeepScanConsent(resource: DeepScanResource): Promi
|
|
|
4
4
|
export declare function promptDeepAgentSelection(options: DeepAgentOption[]): Promise<DeepAgentOption | null>;
|
|
5
5
|
export declare function promptMetaAgentCommandConsent(context: MetaAgentCommandConsentContext): Promise<boolean>;
|
|
6
6
|
export declare function promptRemediationConsent(context: RemediationConsentContext): Promise<boolean>;
|
|
7
|
+
export declare function promptSkillSelection(options: string[]): Promise<string | null>;
|
package/dist/cli-prompts.js
CHANGED
|
@@ -92,3 +92,32 @@ export async function promptRemediationConsent(context) {
|
|
|
92
92
|
rl.close();
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
+
export async function promptSkillSelection(options) {
|
|
96
|
+
if (options.length === 0) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const rl = createInterface({
|
|
100
|
+
input: process.stdin,
|
|
101
|
+
output: process.stdout,
|
|
102
|
+
});
|
|
103
|
+
const optionLines = options.map((option, index) => ` ${index + 1}. ${option}`);
|
|
104
|
+
const prompt = [
|
|
105
|
+
"Multiple skills detected in repository.",
|
|
106
|
+
...optionLines,
|
|
107
|
+
`Choose [1-${options.length}] (q to cancel): `,
|
|
108
|
+
].join("\n");
|
|
109
|
+
try {
|
|
110
|
+
const answer = (await rl.question(prompt)).trim().toLowerCase();
|
|
111
|
+
if (answer === "q" || answer === "quit" || answer === "cancel") {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const numeric = Number.parseInt(answer, 10);
|
|
115
|
+
if (Number.isNaN(numeric) || numeric < 1 || numeric > options.length) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return options[numeric - 1] ?? null;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
rl.close();
|
|
122
|
+
}
|
|
123
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -26,6 +26,9 @@ export interface CliDeps {
|
|
|
26
26
|
resolveScanTarget?: (input: {
|
|
27
27
|
rawTarget: string;
|
|
28
28
|
cwd: string;
|
|
29
|
+
preferredSkill?: string;
|
|
30
|
+
interactive?: boolean;
|
|
31
|
+
requestSkillSelection?: (options: string[]) => Promise<string | null> | string | null;
|
|
29
32
|
}) => Promise<ResolvedScanTarget> | ResolvedScanTarget;
|
|
30
33
|
stdout: (message: string) => void;
|
|
31
34
|
stderr: (message: string) => void;
|
|
@@ -50,6 +53,7 @@ export interface CliDeps {
|
|
|
50
53
|
runMetaAgentCommand?: (context: MetaAgentCommandConsentContext) => Promise<MetaAgentCommandRunResult> | MetaAgentCommandRunResult;
|
|
51
54
|
requestRemediationConsent?: (context: RemediationConsentContext) => Promise<boolean> | boolean;
|
|
52
55
|
requestRunWarningConsent?: (context: RunWarningConsentContext) => Promise<boolean> | boolean;
|
|
56
|
+
requestSkillSelection?: (options: string[]) => Promise<string | null> | string | null;
|
|
53
57
|
executeDeepResource?: (resource: DeepScanResource) => Promise<ResourceFetchResult>;
|
|
54
58
|
runWrapper?: (input: {
|
|
55
59
|
target: string;
|
package/dist/cli.js
CHANGED
|
@@ -20,13 +20,16 @@ import { executeWrapperRun } from "./wrapper.js";
|
|
|
20
20
|
import { runRemediation as runRemediationWorkflow, } from "./layer4-remediation/remediation-runner.js";
|
|
21
21
|
import { undoLatestSession } from "./commands/undo.js";
|
|
22
22
|
import { executeScanCommand } from "./commands/scan-command.js";
|
|
23
|
-
import { promptDeepAgentSelection, promptDeepScanConsent, promptMetaAgentCommandConsent, promptRemediationConsent, } from "./cli-prompts.js";
|
|
23
|
+
import { promptDeepAgentSelection, promptDeepScanConsent, promptMetaAgentCommandConsent, promptRemediationConsent, promptSkillSelection, } from "./cli-prompts.js";
|
|
24
24
|
import { resetScanState } from "./layer2-static/state/scan-state.js";
|
|
25
25
|
const require = createRequire(import.meta.url);
|
|
26
26
|
const packageJson = require("../package.json");
|
|
27
27
|
function isNoTuiEnabled(options) {
|
|
28
28
|
return options.noTui === true || options.tui === false;
|
|
29
29
|
}
|
|
30
|
+
function renderExampleHelp(lines) {
|
|
31
|
+
return ["", "Examples:", ...lines.map((line) => ` ${line}`)].join("\n");
|
|
32
|
+
}
|
|
30
33
|
export function isDirectCliInvocation(importMetaUrl, argv1, deps = {}) {
|
|
31
34
|
if (!argv1) {
|
|
32
35
|
return false;
|
|
@@ -155,8 +158,8 @@ const defaultCliDeps = {
|
|
|
155
158
|
};
|
|
156
159
|
function addScanCommand(program, version, deps) {
|
|
157
160
|
program
|
|
158
|
-
.command("scan [
|
|
159
|
-
.description("Scan a
|
|
161
|
+
.command("scan [target]")
|
|
162
|
+
.description("Scan a local path or URL target for AI tool config risks")
|
|
160
163
|
.option("--deep", "enable Layer 3 dynamic analysis")
|
|
161
164
|
.option("--remediate", "enter remediation mode after scan")
|
|
162
165
|
.option("--fix-safe", "auto-fix unambiguous critical findings")
|
|
@@ -171,11 +174,21 @@ function addScanCommand(program, version, deps) {
|
|
|
171
174
|
.option("--config <path>", "use a specific global config file")
|
|
172
175
|
.option("--force", "skip interactive confirmations")
|
|
173
176
|
.option("--include-user-scope", "include user/home AI tool config paths in scan")
|
|
177
|
+
.option("--skill <name>", "select one skill directory when scanning a skills index repo URL")
|
|
174
178
|
.option("--reset-state", "clear persisted scan-state history and exit")
|
|
175
|
-
.
|
|
176
|
-
|
|
179
|
+
.addHelpText("after", renderExampleHelp([
|
|
180
|
+
"codegate scan .",
|
|
181
|
+
"codegate scan ./skills/security-review/SKILL.md",
|
|
182
|
+
"codegate scan https://github.com/owner/repo",
|
|
183
|
+
"codegate scan https://github.com/owner/repo --skill security-review",
|
|
184
|
+
"codegate scan https://github.com/owner/repo/blob/main/skills/security-review/SKILL.md",
|
|
185
|
+
"codegate scan https://example.com/security-review/SKILL.md --format json",
|
|
186
|
+
]))
|
|
187
|
+
.action(async (target, options) => {
|
|
188
|
+
const rawTarget = target ?? ".";
|
|
177
189
|
const noTui = isNoTuiEnabled(options);
|
|
178
190
|
const promptCallbacksEnabled = noTui !== true;
|
|
191
|
+
const interactivePromptsEnabled = deps.isTTY() && noTui !== true;
|
|
179
192
|
const cliConfig = {
|
|
180
193
|
format: options.format,
|
|
181
194
|
configPath: options.config,
|
|
@@ -188,6 +201,12 @@ function addScanCommand(program, version, deps) {
|
|
|
188
201
|
resolvedTarget = await resolveTarget({
|
|
189
202
|
rawTarget,
|
|
190
203
|
cwd: deps.cwd(),
|
|
204
|
+
preferredSkill: options.skill,
|
|
205
|
+
interactive: interactivePromptsEnabled,
|
|
206
|
+
requestSkillSelection: promptCallbacksEnabled
|
|
207
|
+
? (deps.requestSkillSelection ??
|
|
208
|
+
(interactivePromptsEnabled ? promptSkillSelection : undefined))
|
|
209
|
+
: undefined,
|
|
191
210
|
});
|
|
192
211
|
const scanTarget = resolvedTarget.scanTarget;
|
|
193
212
|
const baseConfig = deps.resolveConfig({
|
|
@@ -207,7 +226,6 @@ function addScanCommand(program, version, deps) {
|
|
|
207
226
|
deps.setExitCode(0);
|
|
208
227
|
return;
|
|
209
228
|
}
|
|
210
|
-
const interactivePromptsEnabled = deps.isTTY() && noTui !== true;
|
|
211
229
|
await executeScanCommand({
|
|
212
230
|
version,
|
|
213
231
|
cwd: resolve(deps.cwd()),
|
|
@@ -290,6 +308,11 @@ function addRunCommand(program, version, deps) {
|
|
|
290
308
|
.option("--no-tui", "disable TUI and interactive prompts")
|
|
291
309
|
.option("--config <path>", "use a specific global config file")
|
|
292
310
|
.option("--force", "skip interactive confirmations")
|
|
311
|
+
.addHelpText("after", renderExampleHelp([
|
|
312
|
+
"codegate run claude",
|
|
313
|
+
"codegate run codex --force",
|
|
314
|
+
"codegate run cursor",
|
|
315
|
+
]))
|
|
293
316
|
.action(async (tool, options) => {
|
|
294
317
|
const cwd = resolve(deps.cwd());
|
|
295
318
|
const noTui = isNoTuiEnabled(options);
|
|
@@ -346,6 +369,7 @@ function addUndoCommand(program, deps) {
|
|
|
346
369
|
program
|
|
347
370
|
.command("undo [dir]")
|
|
348
371
|
.description("Restore the most recent remediation backup session")
|
|
372
|
+
.addHelpText("after", renderExampleHelp(["codegate undo", "codegate undo ./project"]))
|
|
349
373
|
.action((dir) => {
|
|
350
374
|
const projectRoot = resolve(deps.cwd(), dir ?? ".");
|
|
351
375
|
try {
|
|
@@ -367,6 +391,11 @@ function addInitCommand(program, deps) {
|
|
|
367
391
|
.description("Create ~/.codegate/config.json with defaults")
|
|
368
392
|
.option("--path <path>", "write to a custom config path")
|
|
369
393
|
.option("--force", "overwrite existing config file")
|
|
394
|
+
.addHelpText("after", renderExampleHelp([
|
|
395
|
+
"codegate init",
|
|
396
|
+
"codegate init --path ./.codegate/config.json",
|
|
397
|
+
"codegate init --force",
|
|
398
|
+
]))
|
|
370
399
|
.action((options) => {
|
|
371
400
|
try {
|
|
372
401
|
const home = deps.homeDir?.() ?? homedir();
|
|
@@ -399,6 +428,7 @@ function addUpdateCommands(program, deps) {
|
|
|
399
428
|
program
|
|
400
429
|
.command("update-kb")
|
|
401
430
|
.description("Check for newer knowledge-base content")
|
|
431
|
+
.addHelpText("after", renderExampleHelp(["codegate update-kb"]))
|
|
402
432
|
.action(() => {
|
|
403
433
|
deps.stdout("update-kb:");
|
|
404
434
|
for (const line of guidance) {
|
|
@@ -409,6 +439,7 @@ function addUpdateCommands(program, deps) {
|
|
|
409
439
|
program
|
|
410
440
|
.command("update-rules")
|
|
411
441
|
.description("Check for newer rules content")
|
|
442
|
+
.addHelpText("after", renderExampleHelp(["codegate update-rules"]))
|
|
412
443
|
.action(() => {
|
|
413
444
|
deps.stdout("update-rules:");
|
|
414
445
|
for (const line of guidance) {
|
|
@@ -432,7 +463,13 @@ export function createCli(version = packageJson.version ?? "0.0.0-dev", deps = d
|
|
|
432
463
|
.name(APP_NAME)
|
|
433
464
|
.description("Pre-flight security scanner for AI coding tool configurations.")
|
|
434
465
|
.version(versionDisplay)
|
|
435
|
-
.helpOption("-h, --help", "display help for command")
|
|
466
|
+
.helpOption("-h, --help", "display help for command")
|
|
467
|
+
.addHelpText("after", renderExampleHelp([
|
|
468
|
+
"codegate scan .",
|
|
469
|
+
"codegate scan https://github.com/owner/repo",
|
|
470
|
+
"codegate scan https://github.com/owner/repo/blob/main/skills/security-review/SKILL.md",
|
|
471
|
+
"codegate run claude",
|
|
472
|
+
]));
|
|
436
473
|
addScanCommand(program, version, deps);
|
|
437
474
|
addRunCommand(program, version, deps);
|
|
438
475
|
addUndoCommand(program, deps);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OutputFormat } from "../../config.js";
|
|
2
2
|
import type { RemediationRunnerResult } from "../../layer4-remediation/remediation-runner.js";
|
|
3
3
|
import type { CodeGateReport } from "../../types/report.js";
|
|
4
|
+
export declare function summarizeRequestedTargetFindings(report: CodeGateReport, displayTarget?: string): string | null;
|
|
4
5
|
export declare function metadataSummary(metadata: unknown): string;
|
|
5
6
|
export declare function parseMetaAgentOutput(stdout: string): unknown | null;
|
|
6
7
|
export declare function withMetaAgentFinding(metadata: unknown, finding: {
|
|
@@ -7,6 +7,29 @@ import { renderTerminalReport } from "../../reporter/terminal.js";
|
|
|
7
7
|
function isRecord(value) {
|
|
8
8
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
9
|
}
|
|
10
|
+
function isHttpLikeTarget(target) {
|
|
11
|
+
return /^https?:\/\//iu.test(target);
|
|
12
|
+
}
|
|
13
|
+
function isUserScopeFindingPath(path) {
|
|
14
|
+
return path === "~" || path.startsWith("~/");
|
|
15
|
+
}
|
|
16
|
+
export function summarizeRequestedTargetFindings(report, displayTarget) {
|
|
17
|
+
if (!displayTarget || !isHttpLikeTarget(displayTarget)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const targetFindings = report.findings.filter((finding) => !isUserScopeFindingPath(finding.file_path)).length;
|
|
21
|
+
const userScopeFindings = report.findings.length - targetFindings;
|
|
22
|
+
if (targetFindings === 0) {
|
|
23
|
+
if (userScopeFindings === 0) {
|
|
24
|
+
return "Requested URL target result: no findings were detected in the URL content.";
|
|
25
|
+
}
|
|
26
|
+
return `Requested URL target result: no findings were detected in the URL content. ${userScopeFindings} finding(s) came from enabled user-scope paths (~/*).`;
|
|
27
|
+
}
|
|
28
|
+
if (userScopeFindings === 0) {
|
|
29
|
+
return `Requested URL target result: ${targetFindings} finding(s) were detected in the URL content.`;
|
|
30
|
+
}
|
|
31
|
+
return `Requested URL target result: ${targetFindings} finding(s) were detected in the URL content. ${userScopeFindings} additional finding(s) came from enabled user-scope paths (~/*).`;
|
|
32
|
+
}
|
|
10
33
|
export function metadataSummary(metadata) {
|
|
11
34
|
let raw;
|
|
12
35
|
if (typeof metadata === "string") {
|
|
@@ -6,7 +6,7 @@ import { buildMetaAgentCommand, } from "../layer3-dynamic/command-builder.js";
|
|
|
6
6
|
import { buildPromptEvidenceText, supportsToollessLocalTextAnalysis, } from "../layer3-dynamic/local-text-analysis.js";
|
|
7
7
|
import { buildLocalTextAnalysisPrompt, buildSecurityAnalysisPrompt, } from "../layer3-dynamic/meta-agent.js";
|
|
8
8
|
import { layer3OutcomesToFindings, mergeLayer3Findings, runDeepScanWithConsent, } from "../pipeline.js";
|
|
9
|
-
import { mergeMetaAgentMetadata, metadataSummary, noEligibleDeepResourceNotes, parseLocalTextFindings, parseMetaAgentOutput, remediationSummaryLines, renderByFormat, withMetaAgentFinding, } from "./scan-command/helpers.js";
|
|
9
|
+
import { mergeMetaAgentMetadata, metadataSummary, noEligibleDeepResourceNotes, parseLocalTextFindings, parseMetaAgentOutput, remediationSummaryLines, renderByFormat, summarizeRequestedTargetFindings, withMetaAgentFinding, } from "./scan-command/helpers.js";
|
|
10
10
|
function toMetaAgentPreference(value) {
|
|
11
11
|
const normalized = value.trim().toLowerCase();
|
|
12
12
|
if (normalized === "claude" || normalized === "claude-code") {
|
|
@@ -373,13 +373,17 @@ export async function executeScanCommand(input, deps) {
|
|
|
373
373
|
!input.options.output &&
|
|
374
374
|
deps.isTTY() &&
|
|
375
375
|
deps.renderTui !== undefined;
|
|
376
|
+
const targetSummaryNote = input.config.output_format === "terminal"
|
|
377
|
+
? summarizeRequestedTargetFindings(report, input.displayTarget)
|
|
378
|
+
: null;
|
|
379
|
+
const scanNotes = targetSummaryNote ? [...deepScanNotes, targetSummaryNote] : deepScanNotes;
|
|
376
380
|
if (shouldUseTui) {
|
|
377
|
-
deps.renderTui?.({ view: "dashboard", report, notices:
|
|
381
|
+
deps.renderTui?.({ view: "dashboard", report, notices: scanNotes });
|
|
378
382
|
deps.renderTui?.({ view: "summary", report });
|
|
379
383
|
}
|
|
380
384
|
else {
|
|
381
|
-
if (
|
|
382
|
-
for (const note of
|
|
385
|
+
if (scanNotes.length > 0) {
|
|
386
|
+
for (const note of scanNotes) {
|
|
383
387
|
deps.stdout(note);
|
|
384
388
|
}
|
|
385
389
|
}
|
|
@@ -4,6 +4,10 @@ export interface GitHubFileSource {
|
|
|
4
4
|
repoUrl: string;
|
|
5
5
|
filePath: string;
|
|
6
6
|
}
|
|
7
|
+
export interface GitHubTreeSource {
|
|
8
|
+
repoUrl: string;
|
|
9
|
+
treePath: string;
|
|
10
|
+
}
|
|
7
11
|
export declare function cleanupTempDir(path: string): void;
|
|
8
12
|
export declare function isLikelyHttpUrl(value: string): boolean;
|
|
9
13
|
export declare function isLikelyGitRepoUrl(value: URL): boolean;
|
|
@@ -18,3 +22,5 @@ export declare function inferToolFromReportPath(reportPath: string): string;
|
|
|
18
22
|
export declare function copyDirectoryRecursive(sourceDir: string, destinationDir: string): void;
|
|
19
23
|
export declare function collectExplicitCandidates(root: string): ExplicitScanCandidate[];
|
|
20
24
|
export declare function parseGitHubFileSource(rawTarget: string): GitHubFileSource | null;
|
|
25
|
+
export declare function parseGitHubTreeSource(rawTarget: string): GitHubTreeSource | null;
|
|
26
|
+
export declare function extractSkillFromRepoPath(path: string): string | null;
|
|
@@ -266,3 +266,38 @@ export function parseGitHubFileSource(rawTarget) {
|
|
|
266
266
|
}
|
|
267
267
|
return null;
|
|
268
268
|
}
|
|
269
|
+
export function parseGitHubTreeSource(rawTarget) {
|
|
270
|
+
let url;
|
|
271
|
+
try {
|
|
272
|
+
url = new URL(rawTarget);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
if (url.hostname.toLowerCase() !== "github.com") {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const segments = url.pathname.split("/").filter((segment) => segment.length > 0);
|
|
281
|
+
if (segments.length < 4) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
const [owner, repo, marker, , ...treePathSegments] = segments;
|
|
285
|
+
if (marker !== "tree") {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
repoUrl: `https://github.com/${owner}/${repo}.git`,
|
|
290
|
+
treePath: treePathSegments.join("/"),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
export function extractSkillFromRepoPath(path) {
|
|
294
|
+
const normalized = path.replaceAll("\\", "/").replace(/^\/+/u, "");
|
|
295
|
+
const segments = normalized.split("/").filter((segment) => segment.length > 0);
|
|
296
|
+
if (segments.length < 2) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
if (segments[0]?.toLowerCase() !== "skills") {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
return segments[1] ?? null;
|
|
303
|
+
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { ResolvedScanTarget } from "./types.js";
|
|
2
|
+
interface CloneGitRepoOptions {
|
|
3
|
+
preferredSkill?: string;
|
|
4
|
+
inferredSkill?: string;
|
|
5
|
+
interactive?: boolean;
|
|
6
|
+
requestSkillSelection?: (options: string[]) => Promise<string | null> | string | null;
|
|
7
|
+
displayTarget?: string;
|
|
8
|
+
}
|
|
2
9
|
export declare function stageLocalFile(absolutePath: string): ResolvedScanTarget;
|
|
3
|
-
export declare function cloneGitRepo(rawTarget: string): ResolvedScanTarget
|
|
10
|
+
export declare function cloneGitRepo(rawTarget: string, options?: CloneGitRepoOptions): Promise<ResolvedScanTarget>;
|
|
4
11
|
export declare function stageRepoSubdirectory(repoUrl: string, filePath: string, displayTarget: string): ResolvedScanTarget;
|
|
5
12
|
export declare function downloadRemoteFile(rawTarget: string): Promise<ResolvedScanTarget>;
|
|
13
|
+
export {};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, mkdirSync, mkdtempSync } from "node:fs";
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { cleanupTempDir, collectExplicitCandidates, copyDirectoryRecursive, inferRemoteCandidateFormat, inferLocalFileStagePath, inferRemoteFileStagePath, inferToolFromReportPath, parseGitHubFileSource, preserveTailSegments, shouldStageContainingFolder, } from "./helpers.js";
|
|
5
|
+
import { cleanupTempDir, collectExplicitCandidates, copyDirectoryRecursive, extractSkillFromRepoPath, inferRemoteCandidateFormat, inferLocalFileStagePath, inferRemoteFileStagePath, inferToolFromReportPath, parseGitHubFileSource, preserveTailSegments, shouldStageContainingFolder, } from "./helpers.js";
|
|
6
6
|
function cloneRepository(source, destination) {
|
|
7
7
|
const result = spawnSync("git", ["clone", "--depth", "1", "--filter=blob:none", source, destination], {
|
|
8
8
|
encoding: "utf8",
|
|
@@ -37,27 +37,126 @@ export function stageLocalFile(absolutePath) {
|
|
|
37
37
|
cleanup: () => cleanupTempDir(tempRoot),
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
function listAvailableSkills(repoDir) {
|
|
41
|
+
const skillsRoot = join(repoDir, "skills");
|
|
42
|
+
if (!existsSync(skillsRoot)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
if (!statSync(skillsRoot).isDirectory()) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const skills = readdirSync(skillsRoot, { withFileTypes: true })
|
|
54
|
+
.filter((entry) => entry.isDirectory())
|
|
55
|
+
.map((entry) => entry.name)
|
|
56
|
+
.filter((name) => existsSync(join(skillsRoot, name, "SKILL.md")))
|
|
57
|
+
.sort((left, right) => left.localeCompare(right));
|
|
58
|
+
return skills;
|
|
59
|
+
}
|
|
60
|
+
function copyRootScanSurface(repoDir, stageRoot) {
|
|
61
|
+
for (const entry of readdirSync(repoDir, { withFileTypes: true })) {
|
|
62
|
+
if (entry.name === ".git" || entry.name === "skills") {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const sourcePath = join(repoDir, entry.name);
|
|
66
|
+
const destinationPath = join(stageRoot, entry.name);
|
|
67
|
+
if (entry.isFile()) {
|
|
68
|
+
mkdirSync(stageRoot, { recursive: true });
|
|
69
|
+
copyFileSync(sourcePath, destinationPath);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (entry.isDirectory() && (entry.name.startsWith(".") || entry.name === "hooks")) {
|
|
73
|
+
copyDirectoryRecursive(sourcePath, destinationPath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function normalizeSkillName(value) {
|
|
78
|
+
const trimmed = value?.trim();
|
|
79
|
+
if (!trimmed) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return trimmed.replace(/^skills\//iu, "").replace(/^\/+/u, "");
|
|
83
|
+
}
|
|
84
|
+
function pickSkill(availableSkills, preferredSkill, inferredSkill) {
|
|
85
|
+
const normalizedPreferred = normalizeSkillName(preferredSkill);
|
|
86
|
+
if (normalizedPreferred) {
|
|
87
|
+
return normalizedPreferred;
|
|
88
|
+
}
|
|
89
|
+
const normalizedInferred = normalizeSkillName(inferredSkill);
|
|
90
|
+
if (normalizedInferred) {
|
|
91
|
+
return normalizedInferred;
|
|
92
|
+
}
|
|
93
|
+
if (availableSkills.length === 1) {
|
|
94
|
+
return availableSkills[0] ?? null;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
async function stageSkillAwareRepository(tempRoot, repoDir, displayTarget, options = {}) {
|
|
99
|
+
const availableSkills = listAvailableSkills(repoDir);
|
|
100
|
+
if (availableSkills.length === 0) {
|
|
101
|
+
return {
|
|
102
|
+
scanTarget: repoDir,
|
|
103
|
+
displayTarget,
|
|
104
|
+
cleanup: () => cleanupTempDir(tempRoot),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
let selectedSkill = pickSkill(availableSkills, options.preferredSkill, options.inferredSkill);
|
|
108
|
+
if (!selectedSkill &&
|
|
109
|
+
options.interactive === true &&
|
|
110
|
+
options.requestSkillSelection &&
|
|
111
|
+
availableSkills.length > 1) {
|
|
112
|
+
const choice = await options.requestSkillSelection(availableSkills);
|
|
113
|
+
selectedSkill = normalizeSkillName(choice ?? undefined);
|
|
114
|
+
}
|
|
115
|
+
if (!selectedSkill) {
|
|
116
|
+
throw new Error(`Multiple skills detected (${availableSkills.join(", ")}). Re-run with --skill <name>.`);
|
|
117
|
+
}
|
|
118
|
+
if (!availableSkills.includes(selectedSkill)) {
|
|
119
|
+
throw new Error(`Requested skill "${selectedSkill}" was not found. Available skills: ${availableSkills.join(", ")}.`);
|
|
120
|
+
}
|
|
121
|
+
const stageRoot = join(tempRoot, "staged");
|
|
122
|
+
copyRootScanSurface(repoDir, stageRoot);
|
|
123
|
+
copyDirectoryRecursive(join(repoDir, "skills", selectedSkill), join(stageRoot, "skills", selectedSkill));
|
|
124
|
+
return {
|
|
125
|
+
scanTarget: stageRoot,
|
|
126
|
+
displayTarget,
|
|
127
|
+
explicitCandidates: collectExplicitCandidates(stageRoot),
|
|
128
|
+
cleanup: () => cleanupTempDir(tempRoot),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export async function cloneGitRepo(rawTarget, options = {}) {
|
|
41
132
|
const tempRoot = mkdtempSync(join(tmpdir(), "codegate-scan-repo-"));
|
|
42
133
|
const repoDir = join(tempRoot, "repo");
|
|
43
134
|
try {
|
|
44
135
|
cloneRepository(rawTarget, repoDir);
|
|
136
|
+
return await stageSkillAwareRepository(tempRoot, repoDir, options.displayTarget ?? rawTarget, options);
|
|
45
137
|
}
|
|
46
138
|
catch (error) {
|
|
47
139
|
cleanupTempDir(tempRoot);
|
|
48
140
|
throw error;
|
|
49
141
|
}
|
|
50
|
-
return {
|
|
51
|
-
scanTarget: repoDir,
|
|
52
|
-
displayTarget: rawTarget,
|
|
53
|
-
cleanup: () => cleanupTempDir(tempRoot),
|
|
54
|
-
};
|
|
55
142
|
}
|
|
56
143
|
export function stageRepoSubdirectory(repoUrl, filePath, displayTarget) {
|
|
57
144
|
const tempRoot = mkdtempSync(join(tmpdir(), "codegate-scan-repo-file-"));
|
|
58
145
|
const repoDir = join(tempRoot, "repo");
|
|
59
146
|
try {
|
|
60
147
|
cloneRepository(repoUrl, repoDir);
|
|
148
|
+
const inferredSkill = extractSkillFromRepoPath(filePath);
|
|
149
|
+
if (inferredSkill) {
|
|
150
|
+
const stageRoot = join(tempRoot, "staged");
|
|
151
|
+
copyRootScanSurface(repoDir, stageRoot);
|
|
152
|
+
copyDirectoryRecursive(join(repoDir, "skills", inferredSkill), join(stageRoot, "skills", inferredSkill));
|
|
153
|
+
return {
|
|
154
|
+
scanTarget: stageRoot,
|
|
155
|
+
displayTarget,
|
|
156
|
+
explicitCandidates: collectExplicitCandidates(stageRoot),
|
|
157
|
+
cleanup: () => cleanupTempDir(tempRoot),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
61
160
|
const absoluteFile = join(repoDir, filePath);
|
|
62
161
|
if (!existsSync(absoluteFile)) {
|
|
63
162
|
throw new Error(`Resolved repository file not found after clone: ${filePath}`);
|
|
@@ -15,4 +15,7 @@ export interface ResolvedScanTarget {
|
|
|
15
15
|
export interface ResolveScanTargetInput {
|
|
16
16
|
rawTarget: string;
|
|
17
17
|
cwd: string;
|
|
18
|
+
preferredSkill?: string;
|
|
19
|
+
interactive?: boolean;
|
|
20
|
+
requestSkillSelection?: (options: string[]) => Promise<string | null> | string | null;
|
|
18
21
|
}
|
package/dist/scan-target.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { isLikelyGitRepoUrl, isLikelyHttpUrl } from "./scan-target/helpers.js";
|
|
3
|
+
import { extractSkillFromRepoPath, isLikelyGitRepoUrl, isLikelyHttpUrl, parseGitHubTreeSource, } from "./scan-target/helpers.js";
|
|
4
4
|
import { cloneGitRepo, downloadRemoteFile, stageLocalFile } from "./scan-target/staging.js";
|
|
5
5
|
export async function resolveScanTarget(input) {
|
|
6
6
|
const localPath = resolve(input.cwd, input.rawTarget);
|
|
@@ -25,7 +25,23 @@ export async function resolveScanTarget(input) {
|
|
|
25
25
|
}
|
|
26
26
|
const url = new URL(input.rawTarget);
|
|
27
27
|
if (isLikelyGitRepoUrl(url)) {
|
|
28
|
-
return cloneGitRepo(input.rawTarget
|
|
28
|
+
return cloneGitRepo(input.rawTarget, {
|
|
29
|
+
preferredSkill: input.preferredSkill,
|
|
30
|
+
inferredSkill: extractSkillFromRepoPath(url.pathname) ?? undefined,
|
|
31
|
+
interactive: input.interactive === true,
|
|
32
|
+
requestSkillSelection: input.requestSkillSelection,
|
|
33
|
+
displayTarget: input.rawTarget,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const githubTree = parseGitHubTreeSource(input.rawTarget);
|
|
37
|
+
if (githubTree) {
|
|
38
|
+
return cloneGitRepo(githubTree.repoUrl, {
|
|
39
|
+
preferredSkill: input.preferredSkill,
|
|
40
|
+
inferredSkill: extractSkillFromRepoPath(githubTree.treePath) ?? undefined,
|
|
41
|
+
interactive: input.interactive === true,
|
|
42
|
+
requestSkillSelection: input.requestSkillSelection,
|
|
43
|
+
displayTarget: input.rawTarget,
|
|
44
|
+
});
|
|
29
45
|
}
|
|
30
46
|
return downloadRemoteFile(input.rawTarget);
|
|
31
47
|
}
|
|
@@ -4,5 +4,5 @@ import { toAbsoluteDisplayPath } from "../../path-display.js";
|
|
|
4
4
|
import { defaultTheme } from "../theme.js";
|
|
5
5
|
export function DashboardView(props) {
|
|
6
6
|
const visibleFindings = props.report.findings.slice(0, 5);
|
|
7
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsxs(Text, { color: defaultTheme.title, children: ["CodeGate v", props.report.version] }), _jsxs(Text, { color: defaultTheme.muted, children: ["Target: ", props.report.scan_target] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["Installed tools: ", props.report.tools_detected.join(", ") || "none"] }), _jsxs(Text, { children: ["Findings: ", props.report.summary.total, " (CRITICAL", " ", props.report.summary.by_severity.CRITICAL ?? 0, ", HIGH", " ", props.report.summary.by_severity.HIGH ?? 0, ", MEDIUM", " ", props.report.summary.by_severity.MEDIUM ?? 0, ", LOW", " ", props.report.summary.by_severity.LOW ?? 0, ", INFO", " ", props.report.summary.by_severity.INFO ?? 0, ")"] }), props.notices && props.notices.length > 0 ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: defaultTheme.title, children: "
|
|
7
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsxs(Text, { color: defaultTheme.title, children: ["CodeGate v", props.report.version] }), _jsxs(Text, { color: defaultTheme.muted, children: ["Target: ", props.report.scan_target] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["Installed tools: ", props.report.tools_detected.join(", ") || "none"] }), _jsxs(Text, { children: ["Findings: ", props.report.summary.total, " (CRITICAL", " ", props.report.summary.by_severity.CRITICAL ?? 0, ", HIGH", " ", props.report.summary.by_severity.HIGH ?? 0, ", MEDIUM", " ", props.report.summary.by_severity.MEDIUM ?? 0, ", LOW", " ", props.report.summary.by_severity.LOW ?? 0, ", INFO", " ", props.report.summary.by_severity.INFO ?? 0, ")"] }), props.notices && props.notices.length > 0 ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: defaultTheme.title, children: "Notes:" }), props.notices.map((notice, index) => (_jsx(Text, { color: defaultTheme.muted, children: notice }, `notice-${index}`)))] })) : null] }), visibleFindings.length > 0 ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: defaultTheme.title, children: "Findings detail:" }), visibleFindings.map((finding) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["[", finding.severity, "]", " ", toAbsoluteDisplayPath(props.report.scan_target, finding.file_path)] }), _jsx(Text, { children: finding.description }), finding.evidence ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Evidence:" }), finding.evidence.split("\n").map((line, index) => (_jsx(Text, { children: line }, `${finding.finding_id}-evidence-${index}`)))] })) : null] }, finding.finding_id))), props.report.findings.length > visibleFindings.length ? (_jsxs(Text, { color: defaultTheme.muted, children: ["...and ", props.report.findings.length - visibleFindings.length, " more findings"] })) : null] })) : null] }));
|
|
8
8
|
}
|