@securityreviewai/vibereview-cli 0.1.2 → 0.1.4
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/README.md +7 -3
- package/dist/src/cli.js +9 -3
- package/dist/src/commands/generate.js +28 -3
- package/dist/src/commands/init.js +54 -28
- package/dist/src/core/code-guardrails.js +9 -3
- package/dist/src/core/generation-prompt.js +1 -0
- package/dist/src/providers/auth.js +59 -0
- package/dist/src/runners/cursor.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Milestone 3 is implemented across Cursor, Codex, Claude Code, and GitHub Copilot
|
|
|
13
13
|
- deterministic pack matching and guardrail deduplication;
|
|
14
14
|
- bundled `guardrail-generator` skill with a strict, versioned output contract;
|
|
15
15
|
- bounded collection of security-relevant source evidence;
|
|
16
|
-
-
|
|
16
|
+
- optional, explicitly confirmed code-specific guardrail generation;
|
|
17
17
|
- explicit `vibereview guardrails generate` regeneration; and
|
|
18
18
|
- atomic workspace updates that preserve custom guardrails;
|
|
19
19
|
- native workspace instructions, project skills, and session hooks for every provider;
|
|
@@ -80,12 +80,16 @@ node bin/vibereview.js init --provider codex --yes
|
|
|
80
80
|
|
|
81
81
|
Accepted provider IDs are `cursor`, `codex`, `claude`, and `copilot`.
|
|
82
82
|
|
|
83
|
-
Initialization
|
|
83
|
+
Initialization deterministically installs baseline rules first. In an interactive terminal it then explains code-specific guardrails, discloses that the selected provider agent will consume tokens, and asks for confirmation. Declining leaves the baseline workflow fully active. If the user accepts and authentication is missing, VibeReview launches the provider's browser-capable login flow before generation.
|
|
84
|
+
|
|
85
|
+
To generate or regenerate code-specific rules later, with the same confirmation:
|
|
84
86
|
|
|
85
87
|
```bash
|
|
86
|
-
|
|
88
|
+
npx @securityreviewai/vibereview-cli guardrails generate
|
|
87
89
|
```
|
|
88
90
|
|
|
91
|
+
For explicitly authorized non-interactive use, pass `--yes`. Non-interactive initialization skips code-specific generation unless `--generate` is also supplied.
|
|
92
|
+
|
|
89
93
|
Initialization also installs the local security workflow for the selected provider. Restart an already-running IDE agent after initialization so it reloads workspace instructions, skills, and hooks.
|
|
90
94
|
|
|
91
95
|
Additional options:
|
package/dist/src/cli.js
CHANGED
|
@@ -10,12 +10,13 @@ Usage:
|
|
|
10
10
|
vibereview guardrails generate [options]
|
|
11
11
|
|
|
12
12
|
Commands:
|
|
13
|
-
init Detect the stack
|
|
14
|
-
guardrails generate
|
|
13
|
+
init Detect the stack and install baseline guardrails and the IDE workflow
|
|
14
|
+
guardrails generate Optionally generate code-specific guardrails with the configured provider
|
|
15
15
|
|
|
16
16
|
Options:
|
|
17
17
|
--provider <name> cursor, codex, claude, or copilot
|
|
18
18
|
--yes, -y Accept defaults for non-interactive use
|
|
19
|
+
--generate Generate code-specific guardrails during non-interactive init
|
|
19
20
|
--force Refresh managed files; preserve custom rules
|
|
20
21
|
--json Print machine-readable output
|
|
21
22
|
--verbose Show detection details and warnings
|
|
@@ -48,6 +49,7 @@ function parseInitOptions(args) {
|
|
|
48
49
|
cwd: process.cwd(), provider: undefined, yes: false, force: false,
|
|
49
50
|
skipPreflight: false, json: false, verbose: false,
|
|
50
51
|
skipGeneration: false,
|
|
52
|
+
generate: false,
|
|
51
53
|
};
|
|
52
54
|
for (let index = 0; index < args.length; index += 1) {
|
|
53
55
|
const arg = args[index];
|
|
@@ -63,6 +65,8 @@ function parseInitOptions(args) {
|
|
|
63
65
|
options.skipPreflight = true;
|
|
64
66
|
else if (arg === "--skip-generation")
|
|
65
67
|
options.skipGeneration = true;
|
|
68
|
+
else if (arg === "--generate")
|
|
69
|
+
options.generate = true;
|
|
66
70
|
else if (arg === "--json")
|
|
67
71
|
options.json = true;
|
|
68
72
|
else if (arg === "--verbose")
|
|
@@ -73,7 +77,7 @@ function parseInitOptions(args) {
|
|
|
73
77
|
return options;
|
|
74
78
|
}
|
|
75
79
|
function parseGenerateOptions(args) {
|
|
76
|
-
const options = { cwd: process.cwd(), json: false, verbose: false, skipPreflight: false };
|
|
80
|
+
const options = { cwd: process.cwd(), json: false, verbose: false, skipPreflight: false, yes: false };
|
|
77
81
|
for (let index = 0; index < args.length; index += 1) {
|
|
78
82
|
const arg = args[index];
|
|
79
83
|
if (arg === "--cwd")
|
|
@@ -84,6 +88,8 @@ function parseGenerateOptions(args) {
|
|
|
84
88
|
options.verbose = true;
|
|
85
89
|
else if (arg === "--skip-preflight")
|
|
86
90
|
options.skipPreflight = true;
|
|
91
|
+
else if (arg === "--yes" || arg === "-y")
|
|
92
|
+
options.yes = true;
|
|
87
93
|
else
|
|
88
94
|
throw new Error(`Unknown option ${JSON.stringify(arg)}. Run \`vibereview --help\`.`);
|
|
89
95
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { confirm } from "@inquirer/prompts";
|
|
2
3
|
import { generateCodeGuardrails } from "../core/generator.js";
|
|
3
4
|
import { findRepositoryRoot } from "../core/repository.js";
|
|
4
5
|
import { loadWorkspace, updateCodeSpecificGuardrails } from "../core/workspace.js";
|
|
5
|
-
import {
|
|
6
|
+
import { ensureProviderAuthentication, isProviderAuthenticationError, launchProviderAuthentication } from "../providers/auth.js";
|
|
7
|
+
import { PROVIDER_NAMES } from "../providers/index.js";
|
|
6
8
|
import { ui } from "../ui.js";
|
|
7
9
|
export async function generateCommand(options) {
|
|
8
10
|
const root = await findRepositoryRoot(options.cwd);
|
|
@@ -10,12 +12,35 @@ export async function generateCommand(options) {
|
|
|
10
12
|
const provider = workspace.config.provider;
|
|
11
13
|
if (!options.json)
|
|
12
14
|
ui.title("VibeReview guardrail generation");
|
|
13
|
-
|
|
15
|
+
if (!options.yes) {
|
|
16
|
+
if (options.json || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
17
|
+
throw new Error("Code-specific generation requires confirmation. Re-run with `--yes` after accepting the provider token cost.");
|
|
18
|
+
}
|
|
19
|
+
ui.info("Code-specific guardrails are repository-specific security invariants derived from a bounded set of source files.");
|
|
20
|
+
ui.warning(`This runs ${PROVIDER_NAMES[provider]} and consumes tokens from your configured provider account.`);
|
|
21
|
+
const accepted = await confirm({ message: "Generate code-specific guardrails now?", default: false });
|
|
22
|
+
if (!accepted) {
|
|
23
|
+
ui.info("No changes made. Baseline guardrails remain active.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const preflight = options.skipPreflight ? undefined : await ensureProviderAuthentication(provider);
|
|
14
28
|
if (!options.json) {
|
|
15
29
|
ui.step(`${PROVIDER_NAMES[provider]} CLI detected${preflight ? ` (${preflight.version})` : " (preflight skipped)"}`);
|
|
16
30
|
ui.info(`\nGenerating code-specific guardrails with ${PROVIDER_NAMES[provider]}...`);
|
|
17
31
|
}
|
|
18
|
-
|
|
32
|
+
let result;
|
|
33
|
+
try {
|
|
34
|
+
result = await generateCodeGuardrails(provider, root, workspace.profile, workspace.guardrails.baseline);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (options.skipPreflight || !isProviderAuthenticationError(error))
|
|
38
|
+
throw error;
|
|
39
|
+
if (!options.json)
|
|
40
|
+
ui.warning(`${PROVIDER_NAMES[provider]} requested authentication. Opening its login flow...`);
|
|
41
|
+
await launchProviderAuthentication(provider);
|
|
42
|
+
result = await generateCodeGuardrails(provider, root, workspace.profile, workspace.guardrails.baseline);
|
|
43
|
+
}
|
|
19
44
|
const outputPath = await updateCodeSpecificGuardrails(root, workspace.guardrails, result.guardrails);
|
|
20
45
|
if (options.json) {
|
|
21
46
|
process.stdout.write(`${JSON.stringify({
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { access } from "node:fs/promises";
|
|
4
|
-
import { select } from "@inquirer/prompts";
|
|
4
|
+
import { confirm, select } from "@inquirer/prompts";
|
|
5
5
|
import { loadCatalog } from "../core/catalog.js";
|
|
6
6
|
import { detectTechnologyProfile } from "../core/detector.js";
|
|
7
7
|
import { sha256, stableJson } from "../core/hash.js";
|
|
8
8
|
import { matchGuardrails } from "../core/matcher.js";
|
|
9
9
|
import { findRepositoryRoot } from "../core/repository.js";
|
|
10
|
-
import { initializeWorkspace } from "../core/workspace.js";
|
|
10
|
+
import { initializeWorkspace, loadWorkspace, updateCodeSpecificGuardrails } from "../core/workspace.js";
|
|
11
11
|
import { generateCodeGuardrails } from "../core/generator.js";
|
|
12
12
|
import { installProviderIntegration } from "../core/integration.js";
|
|
13
|
-
import {
|
|
13
|
+
import { ensureProviderAuthentication, isProviderAuthenticationError, launchProviderAuthentication } from "../providers/auth.js";
|
|
14
|
+
import { isProvider, PROVIDER_NAMES, PROVIDERS } from "../providers/index.js";
|
|
14
15
|
import { ui } from "../ui.js";
|
|
15
16
|
export async function initCommand(options) {
|
|
16
17
|
const root = await findRepositoryRoot(options.cwd);
|
|
@@ -21,15 +22,7 @@ export async function initCommand(options) {
|
|
|
21
22
|
if (!options.force && await exists(path.join(root, ".vibereview", "config.json"))) {
|
|
22
23
|
throw new Error("VibeReview is already initialized. Re-run with `--force` to regenerate its managed files.");
|
|
23
24
|
}
|
|
24
|
-
const preflight = options.skipPreflight ? undefined : await checkProviderPreflight(provider);
|
|
25
25
|
if (!options.json) {
|
|
26
|
-
ui.step(`${PROVIDER_NAMES[provider]} CLI detected${preflight ? ` (${preflight.version})` : " (preflight skipped)"}`);
|
|
27
|
-
if (preflight) {
|
|
28
|
-
if (preflight.authentication === "deferred")
|
|
29
|
-
ui.warning(preflight.authenticationMessage);
|
|
30
|
-
else
|
|
31
|
-
ui.step(preflight.authenticationMessage);
|
|
32
|
-
}
|
|
33
26
|
ui.info("\nAnalyzing workspace...");
|
|
34
27
|
}
|
|
35
28
|
const catalog = await loadCatalog();
|
|
@@ -50,13 +43,46 @@ export async function initCommand(options) {
|
|
|
50
43
|
ui.detail(` No bundled pack: ${matches.unmatchedTechnologies.join(", ")}`);
|
|
51
44
|
}
|
|
52
45
|
}
|
|
46
|
+
const written = await initializeWorkspace({
|
|
47
|
+
root,
|
|
48
|
+
projectName,
|
|
49
|
+
provider,
|
|
50
|
+
profile,
|
|
51
|
+
guardrails: matches.guardrails,
|
|
52
|
+
catalogHash: sha256(stableJson(catalog)),
|
|
53
|
+
force: options.force,
|
|
54
|
+
});
|
|
55
|
+
const integrationFiles = await installProviderIntegration({ root, projectName, provider });
|
|
56
|
+
written.push(...integrationFiles);
|
|
57
|
+
if (!options.json)
|
|
58
|
+
ui.step("Installed baseline guardrails and local security workflow");
|
|
59
|
+
const generationRequested = await shouldGenerateCodeGuardrails(options, provider);
|
|
53
60
|
let generation;
|
|
54
61
|
let generationError;
|
|
55
|
-
if (
|
|
62
|
+
if (generationRequested) {
|
|
56
63
|
if (!options.json)
|
|
57
64
|
ui.info(`\nGenerating code-specific guardrails with ${PROVIDER_NAMES[provider]}...`);
|
|
58
65
|
try {
|
|
59
|
-
|
|
66
|
+
const preflight = options.skipPreflight ? undefined : await ensureProviderAuthentication(provider);
|
|
67
|
+
if (!options.json) {
|
|
68
|
+
ui.step(`${PROVIDER_NAMES[provider]} CLI detected${preflight ? ` (${preflight.version})` : " (preflight skipped)"}`);
|
|
69
|
+
if (preflight && preflight.authentication !== "deferred")
|
|
70
|
+
ui.step(preflight.authenticationMessage);
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
generation = await generateCodeGuardrails(provider, root, profile, matches.guardrails);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (options.skipPreflight || !isProviderAuthenticationError(error))
|
|
77
|
+
throw error;
|
|
78
|
+
if (!options.json)
|
|
79
|
+
ui.warning(`${PROVIDER_NAMES[provider]} requested authentication. Opening its login flow...`);
|
|
80
|
+
await launchProviderAuthentication(provider);
|
|
81
|
+
generation = await generateCodeGuardrails(provider, root, profile, matches.guardrails);
|
|
82
|
+
}
|
|
83
|
+
const workspace = await loadWorkspace(root);
|
|
84
|
+
const outputPath = await updateCodeSpecificGuardrails(root, workspace.guardrails, generation.guardrails);
|
|
85
|
+
written.push(outputPath);
|
|
60
86
|
if (!options.json) {
|
|
61
87
|
ui.step(`Analyzed ${generation.evidence.files.length} security-relevant file${generation.evidence.files.length === 1 ? "" : "s"}`);
|
|
62
88
|
ui.step(`Generated ${generation.guardrails.length} code-specific guardrail${generation.guardrails.length === 1 ? "" : "s"}`);
|
|
@@ -67,26 +93,14 @@ export async function initCommand(options) {
|
|
|
67
93
|
catch (error) {
|
|
68
94
|
generationError = error instanceof Error ? error.message : String(error);
|
|
69
95
|
if (!options.json) {
|
|
70
|
-
ui.warning("Code-specific generation failed; baseline guardrails
|
|
96
|
+
ui.warning("Code-specific generation failed; installed baseline guardrails remain active.");
|
|
71
97
|
ui.detail(` ${friendlyGenerationError(provider, generationError)}`);
|
|
72
98
|
if (options.verbose && friendlyGenerationError(provider, generationError) !== generationError)
|
|
73
99
|
ui.detail(` Detail: ${generationError}`);
|
|
74
|
-
ui.detail(" Retry later with: vibereview guardrails generate");
|
|
100
|
+
ui.detail(" Retry later with: npx @securityreviewai/vibereview-cli guardrails generate");
|
|
75
101
|
}
|
|
76
102
|
}
|
|
77
103
|
}
|
|
78
|
-
const written = await initializeWorkspace({
|
|
79
|
-
root,
|
|
80
|
-
projectName,
|
|
81
|
-
provider,
|
|
82
|
-
profile,
|
|
83
|
-
guardrails: matches.guardrails,
|
|
84
|
-
...(generation ? { codeSpecific: generation.guardrails } : {}),
|
|
85
|
-
catalogHash: sha256(stableJson(catalog)),
|
|
86
|
-
force: options.force,
|
|
87
|
-
});
|
|
88
|
-
const integrationFiles = await installProviderIntegration({ root, projectName, provider });
|
|
89
|
-
written.push(...integrationFiles);
|
|
90
104
|
if (options.json) {
|
|
91
105
|
process.stdout.write(`${JSON.stringify({
|
|
92
106
|
project_name: projectName,
|
|
@@ -95,7 +109,7 @@ export async function initCommand(options) {
|
|
|
95
109
|
packs: matches.packs.map((p) => p.slug),
|
|
96
110
|
baseline_guardrail_count: matches.guardrails.length,
|
|
97
111
|
code_specific_guardrail_count: generation?.guardrails.length ?? 0,
|
|
98
|
-
generation_status:
|
|
112
|
+
generation_status: !generationRequested ? "skipped" : generation ? "completed" : "failed",
|
|
99
113
|
...(generationError ? { generation_error: generationError } : {}),
|
|
100
114
|
written,
|
|
101
115
|
}, null, 2)}\n`);
|
|
@@ -108,6 +122,18 @@ export async function initCommand(options) {
|
|
|
108
122
|
ui.detail(`Reports: ${path.relative(root, path.join(root, ".vibereview", "reports"))}/*.md`);
|
|
109
123
|
ui.info("\nVibeReview is ready. Restart the IDE agent if it is already running.\n");
|
|
110
124
|
}
|
|
125
|
+
async function shouldGenerateCodeGuardrails(options, provider) {
|
|
126
|
+
if (options.skipGeneration)
|
|
127
|
+
return false;
|
|
128
|
+
if (options.generate)
|
|
129
|
+
return true;
|
|
130
|
+
if (options.json || options.yes || !process.stdin.isTTY || !process.stdout.isTTY)
|
|
131
|
+
return false;
|
|
132
|
+
ui.info("\nOptional: generate code-specific guardrails from a bounded set of security-relevant source files.");
|
|
133
|
+
ui.info("These add repository-specific invariants on top of the installed baseline guardrails.");
|
|
134
|
+
ui.warning(`This runs ${PROVIDER_NAMES[provider]} and consumes tokens from your configured provider account.`);
|
|
135
|
+
return confirm({ message: "Generate code-specific guardrails now?", default: false });
|
|
136
|
+
}
|
|
111
137
|
async function exists(filePath) {
|
|
112
138
|
try {
|
|
113
139
|
await access(filePath);
|
|
@@ -60,9 +60,15 @@ function unwrapKnownWrapper(value) {
|
|
|
60
60
|
if (isRecord(value.result))
|
|
61
61
|
return value.result;
|
|
62
62
|
if (typeof value.result === "string") {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
try {
|
|
64
|
+
return unwrapProviderOutput(value.result);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof GuardrailOutputError) {
|
|
68
|
+
throw new GuardrailOutputError(`provider result contained ${value.result.length} characters but no valid guardrail JSON`);
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
66
72
|
}
|
|
67
73
|
return value;
|
|
68
74
|
}
|
|
@@ -30,6 +30,7 @@ export function buildGenerationPrompt(input) {
|
|
|
30
30
|
if (input.repairError) {
|
|
31
31
|
sections.push("<repair_request>", `Your previous response failed validation: ${input.repairError}`, "Return a corrected JSON object only.", "<previous_response>", (input.previousResponse ?? "").slice(0, 30_000), "</previous_response>", "</repair_request>");
|
|
32
32
|
}
|
|
33
|
+
sections.push("<final_response_requirement>", "Return exactly one JSON object that conforms to the output contract.", "The first non-whitespace character must be { and the last must be }.", "Do not return a plan, explanation, acknowledgement, Markdown fence, or tool call.", "When no repository-specific rule qualifies, return a valid object with an empty guardrails array.", "</final_response_requirement>");
|
|
33
34
|
return `${sections.join("\n")}\n`;
|
|
34
35
|
}
|
|
35
36
|
//# sourceMappingURL=generation-prompt.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { checkProviderPreflight, PROVIDER_NAMES } from "./index.js";
|
|
3
|
+
const LOGIN_COMMANDS = {
|
|
4
|
+
cursor: { executable: "cursor-agent", args: ["login"] },
|
|
5
|
+
codex: { executable: "codex", args: ["login"] },
|
|
6
|
+
claude: { executable: "claude", args: ["auth", "login"] },
|
|
7
|
+
copilot: { executable: "copilot", args: ["login", "--web-flow"] },
|
|
8
|
+
};
|
|
9
|
+
export function providerLoginCommand(provider) {
|
|
10
|
+
const command = LOGIN_COMMANDS[provider];
|
|
11
|
+
return { executable: command.executable, args: [...command.args] };
|
|
12
|
+
}
|
|
13
|
+
export async function ensureProviderAuthentication(provider, dependencies = {}) {
|
|
14
|
+
const check = dependencies.check ?? checkProviderPreflight;
|
|
15
|
+
const launch = dependencies.launch ?? launchInteractive;
|
|
16
|
+
let preflight;
|
|
17
|
+
try {
|
|
18
|
+
preflight = await check(provider);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
if (/not found|could not be started/i.test(message))
|
|
23
|
+
throw error;
|
|
24
|
+
await launchProviderAuthentication(provider, launch);
|
|
25
|
+
return verifyAfterLogin(provider, check);
|
|
26
|
+
}
|
|
27
|
+
if (preflight.authentication !== "deferred")
|
|
28
|
+
return preflight;
|
|
29
|
+
await launchProviderAuthentication(provider, launch);
|
|
30
|
+
return verifyAfterLogin(provider, check);
|
|
31
|
+
}
|
|
32
|
+
export function isProviderAuthenticationError(error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
return /authentication required|not authenticated|not logged in|logged out|unauthenticated|no authentication information/i.test(message);
|
|
35
|
+
}
|
|
36
|
+
export async function launchProviderAuthentication(provider, launch = launchInteractive) {
|
|
37
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
38
|
+
const command = providerLoginCommand(provider);
|
|
39
|
+
throw new Error(`${PROVIDER_NAMES[provider]} authentication requires an interactive terminal. Run \`${[command.executable, ...command.args].join(" ")}\`, then retry.`);
|
|
40
|
+
}
|
|
41
|
+
const command = providerLoginCommand(provider);
|
|
42
|
+
await launch(command.executable, command.args);
|
|
43
|
+
}
|
|
44
|
+
async function verifyAfterLogin(provider, check) {
|
|
45
|
+
try {
|
|
46
|
+
return await check(provider);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new Error(`${PROVIDER_NAMES[provider]} authentication did not complete successfully.`, { cause: error });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function launchInteractive(executable, args) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const child = spawn(executable, args, { stdio: "inherit", env: process.env });
|
|
55
|
+
child.on("error", reject);
|
|
56
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${executable} login exited with code ${code ?? "unknown"}.`)));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=auth.js.map
|