@securityreviewai/vibereview-cli 0.1.1 → 0.1.3

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 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
- - automatic code-specific guardrail generation during initialization;
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 performs deterministic stack detection first, then automatically asks the selected provider CLI to generate code-specific rules. If that provider request fails, VibeReview still installs the baseline rules and prints a retry command. To regenerate after the code changes:
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
- node bin/vibereview.js guardrails generate
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, generate guardrails, and install the IDE workflow
14
- guardrails generate Regenerate code-specific guardrails with the configured provider
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 { checkProviderPreflight, PROVIDER_NAMES } from "../providers/index.js";
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
- const preflight = options.skipPreflight ? undefined : await checkProviderPreflight(provider);
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
- const result = await generateCodeGuardrails(provider, root, workspace.profile, workspace.guardrails.baseline);
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 { checkProviderPreflight, isProvider, PROVIDER_NAMES, PROVIDERS } from "../providers/index.js";
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 (!options.skipGeneration) {
62
+ if (generationRequested) {
56
63
  if (!options.json)
57
64
  ui.info(`\nGenerating code-specific guardrails with ${PROVIDER_NAMES[provider]}...`);
58
65
  try {
59
- generation = await generateCodeGuardrails(provider, root, profile, matches.guardrails);
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 will still be installed.");
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: options.skipGeneration ? "skipped" : generation ? "completed" : "failed",
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,7 @@ function unwrapKnownWrapper(value) {
60
60
  if (isRecord(value.result))
61
61
  return value.result;
62
62
  if (typeof value.result === "string") {
63
- const nested = tryJson(value.result.trim());
64
- if (nested !== undefined)
65
- return nested;
63
+ return unwrapProviderOutput(value.result);
66
64
  }
67
65
  return value;
68
66
  }
@@ -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
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@securityreviewai/vibereview-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Local-first security guardrails for AI coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "vibereview": "bin/vibereview.js"
7
+ "vibereview": "bin/vibereview.js",
8
+ "vibereview-cli": "bin/vibereview.js"
8
9
  },
9
10
  "files": [
10
11
  "bin",