@securityreviewai/securityreview-kit 0.1.51 → 0.1.52

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.
Files changed (78) hide show
  1. package/README.md +105 -0
  2. package/bin/securityreview-kit.js +5 -0
  3. package/package.json +30 -24
  4. package/src/cli.js +109 -0
  5. package/src/commands/init.js +851 -0
  6. package/src/commands/status.js +99 -0
  7. package/src/commands/switch-project.js +207 -0
  8. package/src/generators/mcp/claude.js +85 -0
  9. package/src/generators/mcp/claude.test.js +64 -0
  10. package/src/generators/mcp/codex.js +70 -0
  11. package/src/generators/mcp/codex.test.js +43 -0
  12. package/src/generators/mcp/cursor.js +29 -0
  13. package/src/generators/mcp/cursor.test.js +50 -0
  14. package/src/generators/mcp/gemini.js +28 -0
  15. package/src/generators/mcp/vscode.js +29 -0
  16. package/src/generators/mcp/windsurf.js +27 -0
  17. package/src/generators/rules/antigravity.js +22 -0
  18. package/src/generators/rules/claude.js +87 -0
  19. package/src/generators/rules/claude.test.js +60 -0
  20. package/src/generators/rules/codex.js +141 -0
  21. package/src/generators/rules/codex.test.js +59 -0
  22. package/src/generators/rules/content.js +110 -0
  23. package/{templates/shared → src/generators/rules}/content.md +6 -9
  24. package/src/generators/rules/cursor.js +128 -0
  25. package/src/generators/rules/gemini.js +13 -0
  26. package/src/generators/rules/guardrails-init-profile.md +56 -0
  27. package/src/generators/rules/guardrails-profiler/SKILL.md +130 -0
  28. package/src/generators/rules/guardrails-profiler/references/signal-registry.json +514 -0
  29. package/{templates/shared/guardrails-selection.md → src/generators/rules/guardrails-selection/SKILL.md} +12 -10
  30. package/src/generators/rules/guardrails-selection/references/category-threat-map.md +232 -0
  31. package/src/generators/rules/guardrails_rule.md +94 -0
  32. package/src/generators/rules/hooks.json +11 -0
  33. package/{templates/shared/threat-modelling.md → src/generators/rules/skill.md} +11 -14
  34. package/src/generators/rules/srai-profile.md +32 -0
  35. package/{templates/shared → src/generators/rules}/vibereview-sync/SKILL.md +12 -15
  36. package/src/generators/rules/vscode.js +101 -0
  37. package/src/generators/rules/vscode.test.js +54 -0
  38. package/src/generators/rules/windsurf.js +13 -0
  39. package/src/utils/constants.js +95 -0
  40. package/src/utils/cursor-agent-path.js +67 -0
  41. package/src/utils/cursor-cli-permissions.js +28 -0
  42. package/src/utils/detect.js +27 -0
  43. package/src/utils/fs-helpers.js +82 -0
  44. package/src/utils/guardrails-profiler-bundle.js +84 -0
  45. package/src/utils/ide-cli-install.js +138 -0
  46. package/src/utils/profiler-agent.js +446 -0
  47. package/src/utils/profiler-agent.test.js +81 -0
  48. package/src/utils/srai.js +252 -0
  49. package/dist/api.js +0 -44
  50. package/dist/commands/guardrails.js +0 -13
  51. package/dist/commands/init.js +0 -88
  52. package/dist/commands/profile.js +0 -14
  53. package/dist/commands/status.js +0 -27
  54. package/dist/commands/sync.js +0 -6
  55. package/dist/config.js +0 -18
  56. package/dist/fs.js +0 -43
  57. package/dist/index.js +0 -44
  58. package/dist/profile.js +0 -113
  59. package/dist/scaffold/claude-code.js +0 -43
  60. package/dist/scaffold/codex.js +0 -41
  61. package/dist/scaffold/cursor.js +0 -45
  62. package/dist/scaffold/gemini.js +0 -10
  63. package/dist/scaffold/index.js +0 -22
  64. package/dist/scaffold/mcp.js +0 -15
  65. package/dist/scaffold/rules.js +0 -191
  66. package/dist/scaffold/vibreview.js +0 -30
  67. package/dist/scaffold/vscode.js +0 -28
  68. package/dist/scaffold/windsurf.js +0 -10
  69. package/dist/sync/index.js +0 -34
  70. package/dist/sync/payload.js +0 -23
  71. package/dist/sync/state.js +0 -12
  72. package/dist/types.js +0 -1
  73. package/templates/claude/CLAUDE.md +0 -13
  74. package/templates/claude/agents/guardrail_profiler.md +0 -12
  75. package/templates/claude/agents/threat_modeler.md +0 -5
  76. package/templates/claude/skills/vibreview/SKILL.md +0 -21
  77. package/templates/claude/skills/vibreview/guardrail_patterns.md +0 -12
  78. package/templates/cursor/rules/vibreview-security.mdc +0 -8
@@ -0,0 +1,252 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { MCP_SERVER_NAME, TARGET_NAMES, TARGETS } from './constants.js';
4
+ import { readJson, readText } from './fs-helpers.js';
5
+
6
+ const PROJECT_LIST_PATH = '/api/projects/';
7
+
8
+ /**
9
+ * Normalize API URL from raw input.
10
+ */
11
+ export function normalizeApiUrl(value) {
12
+ const trimmed = String(value || '').trim();
13
+ if (!trimmed) return '';
14
+
15
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
16
+ return trimmed;
17
+ }
18
+
19
+ return `https://${trimmed}`;
20
+ }
21
+
22
+ function parseTomlEnvValue(content, key) {
23
+ const match = content.match(new RegExp(`${key}\\s*=\\s*"(.*?)"`));
24
+ return match ? match[1].trim() : '';
25
+ }
26
+
27
+ function extractCredentialsFromJsonConfig(content) {
28
+ const servers = content?.mcpServers || content?.servers;
29
+ if (!servers || typeof servers !== 'object') return null;
30
+
31
+ const server = servers[MCP_SERVER_NAME];
32
+ if (!server || typeof server !== 'object') return null;
33
+
34
+ const apiUrl = normalizeApiUrl(server?.env?.SECURITY_REVIEW_API_URL || '');
35
+ const apiToken = String(server?.env?.SECURITY_REVIEW_API_TOKEN || '').trim();
36
+ if (!apiUrl || !apiToken) return null;
37
+
38
+ return { apiUrl, apiToken };
39
+ }
40
+
41
+ /**
42
+ * Read previously saved API credentials from any configured MCP file.
43
+ */
44
+ export function getStoredCredentials(cwd) {
45
+ for (const targetName of TARGET_NAMES) {
46
+ const target = TARGETS[targetName];
47
+ const mcpPath = join(cwd, target.mcpConfigPath);
48
+ if (!existsSync(mcpPath)) continue;
49
+
50
+ if (mcpPath.endsWith('.toml')) {
51
+ const content = readText(mcpPath);
52
+ if (!content) continue;
53
+
54
+ const apiUrl = normalizeApiUrl(parseTomlEnvValue(content, 'SECURITY_REVIEW_API_URL'));
55
+ const apiToken = parseTomlEnvValue(content, 'SECURITY_REVIEW_API_TOKEN');
56
+ if (apiUrl && apiToken) {
57
+ return { apiUrl, apiToken };
58
+ }
59
+ continue;
60
+ }
61
+
62
+ const json = readJson(mcpPath);
63
+ const extracted = extractCredentialsFromJsonConfig(json);
64
+ if (extracted) {
65
+ return extracted;
66
+ }
67
+ }
68
+
69
+ return { apiUrl: '', apiToken: '' };
70
+ }
71
+
72
+ function resolveProjectsEndpoint(apiUrl) {
73
+ const parsed = new URL(apiUrl);
74
+ const pathname = parsed.pathname.replace(/\/+$/, '');
75
+
76
+ if (pathname.endsWith('/api/projects')) {
77
+ parsed.pathname = `${pathname}/`;
78
+ } else if (pathname.endsWith('/api')) {
79
+ parsed.pathname = `${pathname}/projects/`;
80
+ } else if (!pathname || pathname === '/') {
81
+ parsed.pathname = PROJECT_LIST_PATH;
82
+ } else {
83
+ parsed.pathname = `${pathname}${PROJECT_LIST_PATH}`;
84
+ }
85
+
86
+ parsed.search = '';
87
+ parsed.hash = '';
88
+ return parsed.toString();
89
+ }
90
+
91
+ function toProjectName(entry) {
92
+ if (typeof entry === 'string') return entry.trim();
93
+ if (!entry || typeof entry !== 'object') return '';
94
+
95
+ const nameFields = [
96
+ 'name',
97
+ 'projectName',
98
+ 'project_name',
99
+ 'displayName',
100
+ 'display_name',
101
+ 'title',
102
+ ];
103
+
104
+ for (const key of nameFields) {
105
+ const value = entry[key];
106
+ if (typeof value === 'string' && value.trim()) {
107
+ return value.trim();
108
+ }
109
+ }
110
+
111
+ if (entry.project && typeof entry.project === 'object') {
112
+ return toProjectName(entry.project);
113
+ }
114
+
115
+ return '';
116
+ }
117
+
118
+ function collectProjectLists(payload) {
119
+ const collections = [];
120
+
121
+ if (Array.isArray(payload)) {
122
+ collections.push(payload);
123
+ }
124
+
125
+ if (payload && typeof payload === 'object') {
126
+ const keys = ['projects', 'data', 'results', 'items'];
127
+ for (const key of keys) {
128
+ if (Array.isArray(payload[key])) {
129
+ collections.push(payload[key]);
130
+ }
131
+ }
132
+ }
133
+
134
+ return collections;
135
+ }
136
+
137
+ function extractProjectNames(payload) {
138
+ const names = new Set();
139
+ for (const list of collectProjectLists(payload)) {
140
+ for (const entry of list) {
141
+ const name = toProjectName(entry);
142
+ if (name) names.add(name);
143
+ }
144
+ }
145
+
146
+ return [...names].sort((a, b) => a.localeCompare(b));
147
+ }
148
+
149
+ /**
150
+ * Whether a project object is marked for AI / vibe review kit selection.
151
+ * Only entries with an explicit true-like `is_vibe_review` (or common API aliases) qualify.
152
+ */
153
+ export function isVibeReviewProject(entry) {
154
+ if (!entry || typeof entry !== 'object') return false;
155
+
156
+ const keys = ['is_vibe_review', 'isVibeReview', 'vibe_review', 'vibeReview'];
157
+ for (const key of keys) {
158
+ if (!Object.prototype.hasOwnProperty.call(entry, key)) continue;
159
+ const value = entry[key];
160
+ if (value === true || value === 1) return true;
161
+ if (typeof value === 'string') {
162
+ const v = value.toLowerCase().trim();
163
+ if (v === 'true' || v === '1' || v === 'yes') return true;
164
+ }
165
+ return false;
166
+ }
167
+
168
+ return false;
169
+ }
170
+
171
+ /**
172
+ * Extract { name, entry } pairs from a projects API payload.
173
+ */
174
+ export function extractProjectEntries(payload) {
175
+ const out = [];
176
+ const seen = new Set();
177
+
178
+ for (const list of collectProjectLists(payload)) {
179
+ for (const entry of list) {
180
+ const name = toProjectName(entry);
181
+ if (!name || seen.has(name)) continue;
182
+ seen.add(name);
183
+ out.push({ name, entry });
184
+ }
185
+ }
186
+
187
+ out.sort((a, b) => a.name.localeCompare(b.name));
188
+ return out;
189
+ }
190
+
191
+ async function fetchProjectsPayload(apiUrl, apiToken) {
192
+ const endpoint = resolveProjectsEndpoint(apiUrl);
193
+
194
+ let response;
195
+ try {
196
+ response = await fetch(endpoint, {
197
+ method: 'GET',
198
+ headers: {
199
+ Authorization: `Bearer ${apiToken}`,
200
+ Accept: 'application/json',
201
+ },
202
+ });
203
+ } catch (err) {
204
+ throw new Error(`Could not reach SRAI API at ${endpoint}: ${err.message}`);
205
+ }
206
+
207
+ if (!response.ok) {
208
+ throw new Error(
209
+ `Project fetch failed (${response.status} ${response.statusText}) at ${endpoint}`,
210
+ );
211
+ }
212
+
213
+ let payload;
214
+ try {
215
+ payload = await response.json();
216
+ } catch {
217
+ throw new Error(`Project fetch returned non-JSON content at ${endpoint}`);
218
+ }
219
+
220
+ return { payload, endpoint };
221
+ }
222
+
223
+ /**
224
+ * Fetch all project names from SRAI (no filtering).
225
+ */
226
+ export async function fetchProjectNames(apiUrl, apiToken) {
227
+ const { payload, endpoint } = await fetchProjectsPayload(apiUrl, apiToken);
228
+ const names = extractProjectNames(payload);
229
+ if (names.length === 0) {
230
+ throw new Error(`No project names found in API response from ${endpoint}`);
231
+ }
232
+
233
+ return names;
234
+ }
235
+
236
+ /**
237
+ * Fetch project names that have `is_vibe_review` (or API alias) set true — used for kit init / project switch.
238
+ */
239
+ export async function fetchVibeReviewProjectNames(apiUrl, apiToken) {
240
+ const { payload, endpoint } = await fetchProjectsPayload(apiUrl, apiToken);
241
+ const entries = extractProjectEntries(payload);
242
+ const names = entries.filter(({ entry }) => isVibeReviewProject(entry)).map(({ name }) => name);
243
+
244
+ if (names.length === 0) {
245
+ throw new Error(
246
+ `No projects with vibe review enabled (is_vibe_review=true) were returned from ${endpoint}. ` +
247
+ 'Enable vibe review on at least one SRAI project, or check your API token and URL.',
248
+ );
249
+ }
250
+
251
+ return names;
252
+ }
package/dist/api.js DELETED
@@ -1,44 +0,0 @@
1
- import { apiBaseUrl } from "./config.js";
2
- export class VibeReviewApiClient {
3
- baseUrl;
4
- apiKey;
5
- constructor(config) {
6
- this.baseUrl = apiBaseUrl(config).replace(/\/$/, "");
7
- this.apiKey = config.api_key;
8
- }
9
- async listProjects() {
10
- return this.request("/v1/projects");
11
- }
12
- async getAuthContext() {
13
- return this.request("/v1/mcp/auth-context");
14
- }
15
- async getProject(projectSlug) {
16
- return this.request(`/v1/projects/${encodeURIComponent(projectSlug)}`);
17
- }
18
- async getGuardrails(projectSlug) {
19
- return this.request(`/v1/projects/${encodeURIComponent(projectSlug)}/guardrails`);
20
- }
21
- async syncCTMMarkdown(projectSlug, payload) {
22
- return this.request(`/v1/mcp/projects/${encodeURIComponent(projectSlug)}/ctm-sync-markdown`, {
23
- method: "POST",
24
- body: payload,
25
- });
26
- }
27
- async request(path, options = {}) {
28
- if (!this.apiKey) {
29
- throw new Error("Missing VibeReview API key in .vibreview/config.json");
30
- }
31
- const response = await fetch(`${this.baseUrl}${path}`, {
32
- method: options.method ?? "GET",
33
- headers: {
34
- Authorization: `Bearer ${this.apiKey}`,
35
- ...(options.body ? { "Content-Type": "application/json" } : {}),
36
- },
37
- body: options.body ? JSON.stringify(options.body) : undefined,
38
- });
39
- if (!response.ok) {
40
- throw new Error(`VibeReview API request failed (${response.status}): ${await response.text()}`);
41
- }
42
- return response.json();
43
- }
44
- }
@@ -1,13 +0,0 @@
1
- import chalk from "chalk";
2
- import { VibeReviewApiClient } from "../api.js";
3
- import { readConfig } from "../config.js";
4
- export async function guardrailsCommand(options = {}) {
5
- const config = await readConfig(options.cwd ?? process.cwd());
6
- if (!config)
7
- throw new Error("VibeReview is not initialized. Run `securityreview-kit init` first.");
8
- const guardrails = await new VibeReviewApiClient(config).getGuardrails(config.project_slug);
9
- console.log(chalk.bold(`${guardrails.length} guardrail(s)`));
10
- for (const guardrail of guardrails) {
11
- console.log(`${guardrail.type.toUpperCase()} [${guardrail.category}] ${guardrail.title}`);
12
- }
13
- }
@@ -1,88 +0,0 @@
1
- import inquirer from "inquirer";
2
- import chalk from "chalk";
3
- import { VibeReviewApiClient } from "../api.js";
4
- import { scaffoldProject } from "../scaffold/index.js";
5
- export async function initCommand(options = {}) {
6
- const cwd = options.cwd ?? process.cwd();
7
- const answers = await promptForMissing(options);
8
- const client = new VibeReviewApiClient({
9
- server_url: answers.serverUrl,
10
- api_base_url: answers.apiBaseUrl,
11
- api_key: answers.apiKey,
12
- });
13
- const authContext = await client.getAuthContext();
14
- const projects = await loadProjects(client);
15
- const selected = await selectProject(projects, authContext, answers.projectSlug, answers.projectId, answers.yes);
16
- const config = {
17
- version: 1,
18
- server_url: answers.serverUrl,
19
- api_base_url: answers.apiBaseUrl,
20
- api_key: answers.apiKey,
21
- tenant_id: authContext.tenant_id,
22
- project_slug: selected.slug,
23
- project_id: selected.id,
24
- project_name: selected.name,
25
- ide: answers.ide,
26
- };
27
- await scaffoldProject(cwd, config);
28
- console.log(chalk.green(`VibeReview initialized for project ${selected.slug}.`));
29
- console.log(chalk.dim(`Tenant: ${authContext.tenant_id}`));
30
- console.log(chalk.dim(`MCP: ${answers.serverUrl.replace(/\/$/, "")}/mcp`));
31
- }
32
- async function promptForMissing(options) {
33
- const questions = [];
34
- if (!options.ide) {
35
- questions.push({
36
- type: "checkbox",
37
- name: "ide",
38
- message: "Which IDEs do you use?",
39
- choices: [
40
- { name: "Claude Code", value: "claude_code" },
41
- { name: "Cursor", value: "cursor" },
42
- { name: "VSCode Copilot", value: "vscode_copilot" },
43
- { name: "Codex", value: "codex" },
44
- { name: "Gemini", value: "gemini" },
45
- { name: "Windsurf", value: "windsurf" },
46
- ],
47
- default: ["claude_code"],
48
- });
49
- }
50
- if (!options.serverUrl)
51
- questions.push({ type: "input", name: "serverUrl", message: "VibeReview MCP server URL", default: "http://localhost:3000" });
52
- if (!options.apiBaseUrl)
53
- questions.push({ type: "input", name: "apiBaseUrl", message: "VibeReview API URL", default: "http://localhost:8000" });
54
- if (!options.apiKey)
55
- questions.push({ type: "password", name: "apiKey", message: "VibeReview API key", mask: "*" });
56
- const answers = questions.length ? await inquirer.prompt(questions) : {};
57
- return {
58
- serverUrl: (options.serverUrl ?? answers.serverUrl),
59
- apiBaseUrl: (options.apiBaseUrl ?? answers.apiBaseUrl),
60
- apiKey: (options.apiKey ?? answers.apiKey),
61
- projectSlug: options.projectSlug,
62
- projectId: options.projectId,
63
- ide: (options.ide ?? answers.ide ?? ["claude_code"]),
64
- yes: options.yes ?? false,
65
- };
66
- }
67
- async function loadProjects(client) {
68
- return client.listProjects();
69
- }
70
- async function selectProject(projects, authContext, slug, projectId, yes) {
71
- const requested = projects.find((project) => project.id === projectId || project.slug === slug);
72
- if (requested)
73
- return requested;
74
- const scopedProject = authContext.project_id ? projects.find((project) => project.id === authContext.project_id) : undefined;
75
- if (scopedProject)
76
- return scopedProject;
77
- if (slug && yes)
78
- return { id: projectId ?? "", name: slug, slug };
79
- if (projects.length === 0) {
80
- throw new Error("No VibeReview projects were visible for this API key. Create a project in the SaaS app first.");
81
- }
82
- if (yes)
83
- return projects[0];
84
- const answer = await inquirer.prompt([
85
- { type: "list", name: "project", message: "Select a VibeReview project", choices: projects.map((project) => ({ name: `${project.name} (${project.slug})`, value: project.slug })) },
86
- ]);
87
- return projects.find((project) => project.slug === answer.project) ?? projects[0];
88
- }
@@ -1,14 +0,0 @@
1
- import chalk from "chalk";
2
- import { readConfig } from "../config.js";
3
- export async function profileCommand(options = {}) {
4
- const cwd = options.cwd ?? process.cwd();
5
- const config = await readConfig(cwd);
6
- if (!config) {
7
- console.log(chalk.yellow("VibeReview is not initialized. Run `securityreview-kit init` first."));
8
- return;
9
- }
10
- console.log(chalk.yellow("Local profiling is deprecated for VibeReview SaaS."));
11
- console.log(`Project ${config.project_slug} is profiled server-side after project creation, and guardrails are served through the VibeReview MCP/KV path.`);
12
- if (options.push)
13
- console.log(chalk.dim("--push was ignored."));
14
- }
@@ -1,27 +0,0 @@
1
- import chalk from "chalk";
2
- import { VibeReviewApiClient } from "../api.js";
3
- import { readConfig } from "../config.js";
4
- export async function statusCommand(options = {}) {
5
- const cwd = options.cwd ?? process.cwd();
6
- const config = await readConfig(cwd);
7
- if (!config) {
8
- console.log(chalk.yellow("VibeReview is not initialized. Run `securityreview-kit init`."));
9
- return;
10
- }
11
- console.log(chalk.bold("VibeReview status"));
12
- console.log(`Project: ${config.project_slug}`);
13
- if (config.project_id)
14
- console.log(`Project ID: ${config.project_id}`);
15
- if (config.tenant_id)
16
- console.log(`Tenant ID: ${config.tenant_id}`);
17
- console.log(`MCP server: ${config.server_url}`);
18
- console.log(`IDEs: ${config.ide.join(", ")}`);
19
- console.log(`Last synced: ${config.last_synced ?? "never"}`);
20
- try {
21
- const guardrails = await new VibeReviewApiClient(config).getGuardrails(config.project_slug);
22
- console.log(`Guardrails: ${guardrails.length}`);
23
- }
24
- catch (error) {
25
- console.log(chalk.yellow(`Remote check skipped: ${error instanceof Error ? error.message : String(error)}`));
26
- }
27
- }
@@ -1,6 +0,0 @@
1
- import chalk from "chalk";
2
- import { syncTelemetry } from "../sync/index.js";
3
- export async function syncCommand(options = {}) {
4
- const result = await syncTelemetry(options.cwd ?? process.cwd(), { force: options.force });
5
- console.log(chalk.green(`Synced ${result.synced} artifact(s), skipped ${result.skipped} unchanged artifact(s).`));
6
- }
package/dist/config.js DELETED
@@ -1,18 +0,0 @@
1
- import { join } from "node:path";
2
- import { readJson, writeJson } from "./fs.js";
3
- export const CONFIG_DIR = ".vibreview";
4
- export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
5
- export const SYNC_STATE_PATH = join(CONFIG_DIR, "sync-state.json");
6
- export const SCANS_DIR = join(CONFIG_DIR, "scans");
7
- export function resolveConfigPath(cwd) {
8
- return join(cwd, CONFIG_PATH);
9
- }
10
- export async function readConfig(cwd) {
11
- return readJson(resolveConfigPath(cwd));
12
- }
13
- export async function writeConfig(cwd, config) {
14
- await writeJson(resolveConfigPath(cwd), config);
15
- }
16
- export function apiBaseUrl(config) {
17
- return config.api_base_url ?? config.server_url.replace(/\/mcp\/?$/, "").replace(/:3000$/, ":8000");
18
- }
package/dist/fs.js DELETED
@@ -1,43 +0,0 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname } from "node:path";
3
- export async function ensureDir(path) {
4
- await mkdir(path, { recursive: true });
5
- }
6
- export async function readText(path) {
7
- try {
8
- return await readFile(path, "utf8");
9
- }
10
- catch {
11
- return "";
12
- }
13
- }
14
- export async function writeText(path, content) {
15
- await ensureDir(dirname(path));
16
- await writeFile(path, content, "utf8");
17
- }
18
- export async function readJson(path) {
19
- try {
20
- return JSON.parse(await readFile(path, "utf8"));
21
- }
22
- catch {
23
- return null;
24
- }
25
- }
26
- export async function writeJson(path, data) {
27
- await writeText(path, `${JSON.stringify(data, null, 2)}\n`);
28
- }
29
- export async function upsertBlock(path, start, end, content) {
30
- const block = `${start}\n${content.trim()}\n${end}`;
31
- const existing = await readText(path);
32
- if (!existing) {
33
- await writeText(path, `${block}\n`);
34
- return;
35
- }
36
- const startIndex = existing.indexOf(start);
37
- const endIndex = existing.indexOf(end);
38
- if (startIndex >= 0 && endIndex >= 0) {
39
- await writeText(path, `${existing.slice(0, startIndex)}${block}${existing.slice(endIndex + end.length)}`);
40
- return;
41
- }
42
- await writeText(path, `${existing.trimEnd()}\n\n${block}\n`);
43
- }
package/dist/index.js DELETED
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import { guardrailsCommand } from "./commands/guardrails.js";
4
- import { initCommand } from "./commands/init.js";
5
- import { profileCommand } from "./commands/profile.js";
6
- import { statusCommand } from "./commands/status.js";
7
- import { syncCommand } from "./commands/sync.js";
8
- const program = new Command();
9
- program.name("securityreview-kit").description("VibeReview IDE integration CLI").version("0.1.0");
10
- program
11
- .command("init")
12
- .description("Initialize VibeReview in this repository")
13
- .option("--server-url <url>", "VibeReview MCP server URL")
14
- .option("--api-base-url <url>", "VibeReview API URL")
15
- .option("--api-key <key>", "VibeReview API key")
16
- .option("--project-slug <slug>", "VibeReview project slug")
17
- .option("--project-id <id>", "VibeReview project ID")
18
- .option("--ide <ide...>", "IDE targets")
19
- .option("-y, --yes", "Use provided values without project selection prompt")
20
- .action((options) => initCommand({
21
- serverUrl: options.serverUrl,
22
- apiBaseUrl: options.apiBaseUrl,
23
- apiKey: options.apiKey,
24
- projectSlug: options.projectSlug,
25
- projectId: options.projectId,
26
- ide: options.ide,
27
- yes: options.yes,
28
- }));
29
- program
30
- .command("profile")
31
- .description("Explain server-side VibeReview profiling")
32
- .option("--push", "Deprecated; profiling runs server-side")
33
- .action((options) => profileCommand({ push: options.push }));
34
- program
35
- .command("sync")
36
- .description("Sync local scan telemetry")
37
- .option("--force", "Re-sync unchanged artifacts")
38
- .action((options) => syncCommand({ force: options.force }));
39
- program.command("status").description("Show VibeReview link status").action(() => statusCommand());
40
- program.command("guardrails").description("List project guardrails").action(() => guardrailsCommand());
41
- program.parseAsync().catch((error) => {
42
- console.error(error instanceof Error ? error.message : String(error));
43
- process.exitCode = 1;
44
- });
package/dist/profile.js DELETED
@@ -1,113 +0,0 @@
1
- import { readdir, readFile, stat } from "node:fs/promises";
2
- import { extname, join } from "node:path";
3
- const LANGUAGE_BY_EXTENSION = {
4
- ".py": "python",
5
- ".ts": "typescript",
6
- ".tsx": "typescript",
7
- ".js": "javascript",
8
- ".jsx": "javascript",
9
- ".go": "go",
10
- ".java": "java",
11
- ".rb": "ruby",
12
- ".php": "php",
13
- ".cs": "csharp",
14
- ".rs": "rust",
15
- ".kt": "kotlin",
16
- ".swift": "swift",
17
- };
18
- const SKIP_DIRS = new Set([".git", "node_modules", ".venv", "venv", "dist", "build", ".turbo", "__pycache__"]);
19
- export async function profileRepository(cwd) {
20
- const files = await collectFiles(cwd);
21
- const languageCounts = {};
22
- const frameworks = new Set();
23
- const databases = new Set();
24
- const auth = new Set();
25
- const infrastructure = new Set();
26
- const apiProtocols = new Set();
27
- for (const file of files) {
28
- const language = LANGUAGE_BY_EXTENSION[extname(file)];
29
- if (language)
30
- languageCounts[language] = (languageCounts[language] ?? 0) + 1;
31
- const lower = file.toLowerCase();
32
- if (lower.endsWith("package.json")) {
33
- const pkg = await readJsonSafe(file);
34
- const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
35
- if (deps.react)
36
- frameworks.add("react");
37
- if (deps.next)
38
- frameworks.add("nextjs");
39
- if (deps["@nestjs/core"])
40
- frameworks.add("nestjs");
41
- if (deps.express)
42
- frameworks.add("express");
43
- if (deps.hono)
44
- frameworks.add("honojs");
45
- if (deps["@apollo/server"])
46
- apiProtocols.add("graphql");
47
- }
48
- if (lower.endsWith("pyproject.toml") || lower.endsWith("requirements.txt")) {
49
- const text = await readFile(file, "utf8").catch(() => "");
50
- if (/fastapi/i.test(text))
51
- frameworks.add("fastapi");
52
- if (/flask/i.test(text))
53
- frameworks.add("flask");
54
- if (/django/i.test(text))
55
- frameworks.add("django");
56
- if (/sqlalchemy/i.test(text))
57
- frameworks.add("sqlalchemy");
58
- }
59
- if (lower.includes("dockerfile") || lower.endsWith("docker-compose.yml"))
60
- infrastructure.add("docker");
61
- if (lower.includes(".github/workflows"))
62
- infrastructure.add("github-actions");
63
- if (lower.includes("kubernetes") || lower.endsWith(".helm.yaml"))
64
- infrastructure.add("kubernetes");
65
- if (/postgres|postgresql/.test(lower))
66
- databases.add("postgresql");
67
- if (/redis/.test(lower))
68
- databases.add("redis");
69
- if (/mongo/.test(lower))
70
- databases.add("mongodb");
71
- if (/jwt|oauth|oidc|auth0|zitadel|okta|keycloak/.test(lower))
72
- auth.add(RegExp.lastMatch.toLowerCase());
73
- if (/openapi|swagger|graphql|grpc/.test(lower))
74
- apiProtocols.add(RegExp.lastMatch.toLowerCase());
75
- }
76
- return {
77
- languages: percentages(languageCounts),
78
- frameworks: [...frameworks].sort(),
79
- databases: [...databases].sort(),
80
- auth: [...auth].sort(),
81
- infrastructure: [...infrastructure].sort(),
82
- api_protocols: [...apiProtocols].sort(),
83
- generated_at: new Date().toISOString(),
84
- };
85
- }
86
- async function collectFiles(root, current = root, output = []) {
87
- for (const entry of await readdir(current, { withFileTypes: true }).catch(() => [])) {
88
- if (entry.isDirectory()) {
89
- if (!SKIP_DIRS.has(entry.name))
90
- await collectFiles(root, join(current, entry.name), output);
91
- continue;
92
- }
93
- const path = join(current, entry.name);
94
- const info = await stat(path).catch(() => null);
95
- if (info && info.size < 1_000_000)
96
- output.push(path);
97
- }
98
- return output;
99
- }
100
- async function readJsonSafe(path) {
101
- try {
102
- return JSON.parse(await readFile(path, "utf8"));
103
- }
104
- catch {
105
- return null;
106
- }
107
- }
108
- function percentages(counts) {
109
- const total = Object.values(counts).reduce((sum, value) => sum + value, 0);
110
- if (total === 0)
111
- return {};
112
- return Object.fromEntries(Object.entries(counts).map(([key, value]) => [key, Math.round((value / total) * 100)]));
113
- }