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.
@@ -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>;
@@ -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 [dir]")
159
- .description("Scan a directory, file, or safely staged artifact target for AI tool config risks")
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
- .action(async (dir, options) => {
176
- const rawTarget = dir ?? ".";
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") {
@@ -20,6 +20,7 @@ export interface ScanCommandOptions {
20
20
  force?: boolean;
21
21
  resetState?: boolean;
22
22
  includeUserScope?: boolean;
23
+ skill?: string;
23
24
  }
24
25
  export interface ScanRunnerInput {
25
26
  version: 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: deepScanNotes });
381
+ deps.renderTui?.({ view: "dashboard", report, notices: scanNotes });
378
382
  deps.renderTui?.({ view: "summary", report });
379
383
  }
380
384
  else {
381
- if (deepScanNotes.length > 0) {
382
- for (const note of deepScanNotes) {
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
- export function cloneGitRepo(rawTarget) {
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
  }
@@ -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: "Deep scan:" }), 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] }));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codegate-ai",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Pre-flight security scanner for AI coding tool configurations.",
5
5
  "license": "MIT",
6
6
  "type": "module",