@securityreviewai/securityreview-kit 0.1.47 → 0.1.49

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 (79) hide show
  1. package/dist/api.js +44 -0
  2. package/dist/commands/guardrails.js +13 -0
  3. package/dist/commands/init.js +88 -0
  4. package/dist/commands/profile.js +14 -0
  5. package/dist/commands/status.js +27 -0
  6. package/dist/commands/sync.js +6 -0
  7. package/dist/config.js +18 -0
  8. package/dist/fs.js +43 -0
  9. package/dist/index.js +44 -0
  10. package/dist/profile.js +113 -0
  11. package/dist/scaffold/claude-code.js +37 -0
  12. package/dist/scaffold/codex.js +35 -0
  13. package/dist/scaffold/cursor.js +39 -0
  14. package/dist/scaffold/gemini.js +10 -0
  15. package/dist/scaffold/index.js +22 -0
  16. package/dist/scaffold/mcp.js +15 -0
  17. package/dist/scaffold/rules.js +165 -0
  18. package/dist/scaffold/vibreview.js +24 -0
  19. package/dist/scaffold/vscode.js +22 -0
  20. package/dist/scaffold/windsurf.js +10 -0
  21. package/dist/sync/index.js +34 -0
  22. package/dist/sync/payload.js +23 -0
  23. package/dist/sync/state.js +12 -0
  24. package/dist/types.js +1 -0
  25. package/package.json +24 -30
  26. package/templates/claude/CLAUDE.md +13 -0
  27. package/templates/claude/agents/guardrail_profiler.md +12 -0
  28. package/templates/claude/agents/threat_modeler.md +5 -0
  29. package/templates/claude/skills/vibreview/SKILL.md +21 -0
  30. package/templates/claude/skills/vibreview/guardrail_patterns.md +12 -0
  31. package/templates/cursor/rules/vibreview-security.mdc +8 -0
  32. package/README.md +0 -105
  33. package/bin/securityreview-kit.js +0 -5
  34. package/src/cli.js +0 -109
  35. package/src/commands/init.js +0 -851
  36. package/src/commands/status.js +0 -99
  37. package/src/commands/switch-project.js +0 -207
  38. package/src/generators/mcp/claude.js +0 -85
  39. package/src/generators/mcp/claude.test.js +0 -64
  40. package/src/generators/mcp/codex.js +0 -70
  41. package/src/generators/mcp/codex.test.js +0 -43
  42. package/src/generators/mcp/cursor.js +0 -29
  43. package/src/generators/mcp/cursor.test.js +0 -50
  44. package/src/generators/mcp/gemini.js +0 -28
  45. package/src/generators/mcp/vscode.js +0 -48
  46. package/src/generators/mcp/vscode.test.js +0 -21
  47. package/src/generators/mcp/windsurf.js +0 -27
  48. package/src/generators/rules/antigravity.js +0 -22
  49. package/src/generators/rules/claude.js +0 -87
  50. package/src/generators/rules/claude.test.js +0 -60
  51. package/src/generators/rules/codex.js +0 -141
  52. package/src/generators/rules/codex.test.js +0 -59
  53. package/src/generators/rules/content.js +0 -110
  54. package/src/generators/rules/content.md +0 -57
  55. package/src/generators/rules/cursor.js +0 -128
  56. package/src/generators/rules/gemini.js +0 -13
  57. package/src/generators/rules/guardrails-init-profile.md +0 -56
  58. package/src/generators/rules/guardrails-profiler/SKILL.md +0 -130
  59. package/src/generators/rules/guardrails-profiler/references/signal-registry.json +0 -514
  60. package/src/generators/rules/guardrails-selection/SKILL.md +0 -187
  61. package/src/generators/rules/guardrails-selection/references/category-threat-map.md +0 -232
  62. package/src/generators/rules/guardrails_rule.md +0 -94
  63. package/src/generators/rules/hooks.json +0 -11
  64. package/src/generators/rules/skill.md +0 -256
  65. package/src/generators/rules/srai-profile.md +0 -32
  66. package/src/generators/rules/vibereview-sync/SKILL.md +0 -378
  67. package/src/generators/rules/vscode.js +0 -101
  68. package/src/generators/rules/vscode.test.js +0 -54
  69. package/src/generators/rules/windsurf.js +0 -13
  70. package/src/utils/constants.js +0 -95
  71. package/src/utils/cursor-agent-path.js +0 -67
  72. package/src/utils/cursor-cli-permissions.js +0 -28
  73. package/src/utils/detect.js +0 -27
  74. package/src/utils/fs-helpers.js +0 -82
  75. package/src/utils/guardrails-profiler-bundle.js +0 -84
  76. package/src/utils/ide-cli-install.js +0 -138
  77. package/src/utils/profiler-agent.js +0 -446
  78. package/src/utils/profiler-agent.test.js +0 -81
  79. package/src/utils/srai.js +0 -252
@@ -1,101 +0,0 @@
1
- import { existsSync, unlinkSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import {
4
- GUARDRAILS_SELECTION_SKILL_REL_DIR,
5
- THREAT_MODELLING_SKILL_REL_DIR,
6
- VIBEREVIEW_SYNC_SKILL_REL_DIR,
7
- } from '../../utils/constants.js';
8
- import { upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
9
- import { getRuleContent, getThreatModellingSkillContent, getVibeReviewSyncSkillContent } from './content.js';
10
-
11
- function writeGeneratedText(filePath, content) {
12
- const action = existsSync(filePath) ? 'updated' : 'created';
13
- writeText(filePath, content);
14
- return { filePath, action };
15
- }
16
-
17
- function removeGeneratedText(filePath) {
18
- if (existsSync(filePath)) {
19
- unlinkSync(filePath);
20
- return { filePath, action: 'deleted' };
21
- }
22
- return null;
23
- }
24
-
25
- function getCopilotSessionHookContent() {
26
- const additionalContext = [
27
- '## MANDATORY SECURITY GATE (Session Policy)',
28
- '',
29
- 'For any request with security impact (auth, authorization, crypto, input handling, secrets, network, data storage, dependencies, new APIs/endpoints, infrastructure, or code handling untrusted data), follow this order:',
30
- '',
31
- '1. Fetch Vibe Guardrails first using .github/skills/guardrails-selection/SKILL.md. Resolve the project, call get_guardrails, shortlist relevant guardrails, then call get_guardrail_by_id for the shortlist.',
32
- '2. Run PWNISMS threat modelling using .github/skills/threat-modelling/SKILL.md. Explicitly walk Product, Workload, Network, IAM, Secrets, Monitoring, and Supply Chain.',
33
- '3. Implement secure code using both the hydrated guardrails and PWNISMS findings.',
34
- '4. Read .github/skills/vibereview-sync/SKILL.md. Write a structured .md artifact under vibereview/, do not read sibling markdown files there just to infer format, validate it, and call sync_ai_ide_markdown directly after threat modelling or guardrail enforcement. Reuse the exact guardrail shortlist; do not re-query guardrails during the final sync.',
35
- '',
36
- 'Do not use project-profile exploration tools during normal coding tasks. No blocking and no deferral: guardrails first, PWNISMS second, implementation third, VibeReview sync last.',
37
- ].join('\n');
38
-
39
- const commandPayload = JSON.stringify({
40
- hookSpecificOutput: {
41
- hookEventName: 'SessionStart',
42
- additionalContext,
43
- },
44
- });
45
-
46
- return JSON.stringify(
47
- {
48
- hooks: {
49
- SessionStart: [
50
- {
51
- type: 'command',
52
- command: `printf '%s\\n' '${commandPayload.replaceAll("'", "'\\''")}'`,
53
- timeout: 5,
54
- },
55
- ],
56
- },
57
- },
58
- null,
59
- 2,
60
- ) + '\n';
61
- }
62
-
63
- /**
64
- * Generate VS Code Copilot instructions — appends to .github/copilot-instructions.md
65
- */
66
- export function generate(cwd, options = {}) {
67
- const optionsWithSkillDirs = {
68
- ...options,
69
- guardrailsSelectionSkillDir: GUARDRAILS_SELECTION_SKILL_REL_DIR.vscode,
70
- threatModellingSkillDir: THREAT_MODELLING_SKILL_REL_DIR.vscode,
71
- vibereviewSyncSkillDir: VIBEREVIEW_SYNC_SKILL_REL_DIR.vscode,
72
- };
73
- const filePath = join(cwd, '.github', 'copilot-instructions.md');
74
- const content = getRuleContent(optionsWithSkillDirs);
75
- const action = upsertSentinelBlock(filePath, content);
76
-
77
- const threatSkillPath = join(cwd, THREAT_MODELLING_SKILL_REL_DIR.vscode, 'SKILL.md');
78
- const threatSkill = writeGeneratedText(
79
- threatSkillPath,
80
- getThreatModellingSkillContent(optionsWithSkillDirs),
81
- );
82
-
83
- const vibereviewSyncSkillPath = join(cwd, VIBEREVIEW_SYNC_SKILL_REL_DIR.vscode, 'SKILL.md');
84
- const vibereviewSyncSkill = writeGeneratedText(
85
- vibereviewSyncSkillPath,
86
- getVibeReviewSyncSkillContent(optionsWithSkillDirs),
87
- );
88
-
89
- const deletedLegacyAgent = removeGeneratedText(join(cwd, '.github', 'agents', 'ctm_sync.agent.md'));
90
-
91
- const hooksPath = join(cwd, '.github', 'hooks', 'srai-session-policy.json');
92
- const hooks = writeGeneratedText(hooksPath, getCopilotSessionHookContent());
93
-
94
- return [
95
- { filePath, action, kind: 'rule' },
96
- { ...threatSkill, kind: 'skill' },
97
- { ...vibereviewSyncSkill, kind: 'skill' },
98
- { ...hooks, kind: 'hooks' },
99
- ...(deletedLegacyAgent ? [{ ...deletedLegacyAgent, kind: 'cleanup' }] : []),
100
- ];
101
- }
@@ -1,54 +0,0 @@
1
- import { existsSync, mkdtempSync, readFileSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { test } from 'node:test';
5
- import assert from 'node:assert/strict';
6
- import { generate } from './vscode.js';
7
-
8
- test('VS Code Copilot generator writes instructions, skills, and hooks', () => {
9
- const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-vscode-'));
10
-
11
- const results = generate(cwd, { projectName: 'SmokeProject' });
12
-
13
- const expectedPaths = [
14
- '.github/copilot-instructions.md',
15
- '.github/skills/threat-modelling/SKILL.md',
16
- '.github/skills/vibereview-sync/SKILL.md',
17
- '.github/hooks/srai-session-policy.json',
18
- ];
19
-
20
- for (const relPath of expectedPaths) {
21
- assert.equal(existsSync(join(cwd, relPath)), true, `${relPath} should exist`);
22
- }
23
-
24
- assert.deepEqual(results.map((entry) => entry.kind), ['rule', 'skill', 'skill', 'hooks']);
25
-
26
- const instructions = readFileSync(join(cwd, '.github/copilot-instructions.md'), 'utf8');
27
- assert.match(instructions, /\.github\/skills\/guardrails-selection\/SKILL\.md/);
28
- assert.match(instructions, /\.github\/skills\/threat-modelling\/SKILL\.md/);
29
- assert.match(instructions, /\.github\/skills\/vibereview-sync\/SKILL\.md/);
30
- assert.match(instructions, /sync_ai_ide_markdown/);
31
- assert.match(instructions, /vibereview\//);
32
- assert.doesNotMatch(instructions, /\.cursor/);
33
-
34
- const threatSkill = readFileSync(join(cwd, '.github/skills/threat-modelling/SKILL.md'), 'utf8');
35
- for (const heading of ['Product', 'Workload', 'Network', 'IAM', 'Secrets', 'Monitoring', 'Supply Chain']) {
36
- assert.match(threatSkill, new RegExp(heading));
37
- }
38
- assert.match(threatSkill, /sync_ai_ide_markdown/);
39
- assert.match(threatSkill, /vibereview\//);
40
- assert.doesNotMatch(threatSkill, /get_project_profile_description/);
41
-
42
- const vibereviewSkill = readFileSync(join(cwd, '.github/skills/vibereview-sync/SKILL.md'), 'utf8');
43
- assert.match(vibereviewSkill, /Do not read other/i);
44
- assert.match(vibereviewSkill, /sync_ai_ide_markdown/);
45
- assert.match(vibereviewSkill, /Guardrails Applied/);
46
- assert.match(vibereviewSkill, /Practice 1/);
47
- assert.match(vibereviewSkill, /practice_name:/);
48
-
49
- const hooks = JSON.parse(readFileSync(join(cwd, '.github/hooks/srai-session-policy.json'), 'utf8'));
50
- assert.equal(hooks.hooks.SessionStart[0].type, 'command');
51
- assert.match(hooks.hooks.SessionStart[0].command, /vibereview/);
52
- assert.match(hooks.hooks.SessionStart[0].command, /sync_ai_ide_markdown/);
53
- assert.match(hooks.hooks.SessionStart[0].command, /vibereview-sync\/SKILL\.md/);
54
- });
@@ -1,13 +0,0 @@
1
- import { join } from 'node:path';
2
- import { writeText } from '../../utils/fs-helpers.js';
3
- import { getRuleContent } from './content.js';
4
-
5
- /**
6
- * Generate Windsurf workspace rule at .windsurf/rules/srai-security-review.md
7
- */
8
- export function generate(cwd, options = {}) {
9
- const filePath = join(cwd, '.windsurf', 'rules', 'srai-security-review.md');
10
- const content = getRuleContent(options);
11
- writeText(filePath, content + '\n');
12
- return filePath;
13
- }
@@ -1,95 +0,0 @@
1
- // Shared constants for securityreview-kit
2
-
3
- export const MCP_SERVER_PACKAGE = '@securityreviewai/security-review-mcp';
4
- export const MCP_SERVER_NAME = 'security-review-mcp';
5
-
6
- export const ENV_VARS = {
7
- apiUrl: 'SECURITY_REVIEW_API_URL',
8
- apiToken: 'SECURITY_REVIEW_API_TOKEN',
9
- };
10
-
11
- export const TARGETS = {
12
- cursor: {
13
- name: 'Cursor',
14
- mcpConfigPath: '.cursor/mcp.json',
15
- rulePath: '.cursor/rules/srai-security-review.mdc',
16
- detectDirs: ['.cursor'],
17
- },
18
- claude: {
19
- name: 'Claude Code',
20
- mcpConfigPath: '.mcp.json',
21
- rulePath: '.claude/CLAUDE.md',
22
- ruleMode: 'append',
23
- detectDirs: ['.claude'],
24
- },
25
- vscode: {
26
- name: 'VS Code Copilot',
27
- mcpConfigPath: '.vscode/mcp.json',
28
- rulePath: '.github/copilot-instructions.md',
29
- ruleMode: 'append',
30
- detectDirs: ['.vscode'],
31
- },
32
- windsurf: {
33
- name: 'Windsurf',
34
- mcpConfigPath: '.windsurf/mcp_config.json',
35
- rulePath: '.windsurf/rules/srai-security-review.md',
36
- detectDirs: ['.windsurf'],
37
- },
38
- codex: {
39
- name: 'Codex',
40
- mcpConfigPath: '.codex/config.toml',
41
- rulePath: '.codex/AGENTS.md',
42
- ruleMode: 'append',
43
- detectDirs: ['.codex'],
44
- },
45
- gemini: {
46
- name: 'Gemini CLI',
47
- mcpConfigPath: '.gemini/settings.json',
48
- rulePath: 'GEMINI.md',
49
- ruleMode: 'append',
50
- detectDirs: ['.gemini'],
51
- },
52
- antigravity: {
53
- name: 'Antigravity',
54
- mcpConfigPath: '.gemini/settings.json',
55
- rulePath: '.agent/rules/srai-security-review.md',
56
- detectDirs: ['.agent'],
57
- },
58
- };
59
-
60
- export const TARGET_NAMES = Object.keys(TARGETS);
61
-
62
- /** Relative workspace dirs for the guardrails-profiler skill (per IDE / CLI). */
63
- export const GUARDRAILS_PROFILER_SKILL_REL_DIR = {
64
- cursor: '.cursor/skills/guardrails-profiler',
65
- claude: '.claude/skills/guardrails-profiler',
66
- vscode: '.github/skills/guardrails-profiler',
67
- codex: '.codex/skills/guardrails-profiler',
68
- };
69
-
70
- /** Relative workspace dirs for the guardrails-selection skill (per IDE / CLI). */
71
- export const GUARDRAILS_SELECTION_SKILL_REL_DIR = {
72
- cursor: '.cursor/skills/guardrails-selection',
73
- claude: '.claude/skills/guardrails-selection',
74
- vscode: '.github/skills/guardrails-selection',
75
- codex: '.codex/skills/guardrails-selection',
76
- };
77
-
78
- /** Relative workspace dirs for the PWNISMS threat-modelling skill (per IDE / CLI). */
79
- export const THREAT_MODELLING_SKILL_REL_DIR = {
80
- cursor: '.cursor/skills/threat-modelling',
81
- claude: '.claude/skills/threat-modelling',
82
- vscode: '.github/skills/threat-modelling',
83
- codex: '.codex/skills/threat-modelling',
84
- };
85
-
86
- /** Relative workspace dirs for the VibeReview markdown sync skill (per IDE / CLI). */
87
- export const VIBEREVIEW_SYNC_SKILL_REL_DIR = {
88
- cursor: '.cursor/skills/vibereview-sync',
89
- claude: '.claude/skills/vibereview-sync',
90
- vscode: '.github/skills/vibereview-sync',
91
- codex: '.codex/skills/vibereview-sync',
92
- };
93
-
94
- export const SENTINEL_START = '<!-- securityreview-kit:start -->';
95
- export const SENTINEL_END = '<!-- securityreview-kit:end -->';
@@ -1,67 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { spawnSync } from 'node:child_process';
3
- import { homedir } from 'node:os';
4
- import { delimiter, isAbsolute, join } from 'node:path';
5
-
6
- /**
7
- * Extra PATH segments where Cursor / other CLIs are often installed.
8
- * GUI-launched terminals load shell rc files; Node subprocesses often do not.
9
- */
10
- export function augmentPathEnv(baseEnv = process.env) {
11
- const home = homedir();
12
- const extra = [
13
- join(home, '.local', 'bin'),
14
- join(home, '.cursor', 'bin'),
15
- '/opt/homebrew/bin',
16
- '/usr/local/bin',
17
- ];
18
- if (process.platform === 'win32') {
19
- extra.push(join(home, 'AppData', 'Local', 'Programs', 'cursor'));
20
- }
21
- const pathKey = process.platform === 'win32' ? 'Path' : 'PATH';
22
- const current = baseEnv[pathKey] || baseEnv.PATH || '';
23
- const merged = [...extra.filter((p) => p), current].filter(Boolean).join(delimiter);
24
- return { ...baseEnv, [pathKey]: merged, PATH: merged };
25
- }
26
-
27
- /**
28
- * Cursor Agent CLI binary (`agent` or legacy `cursor-agent`) that responds to `--version`, or null.
29
- * New installs often only ship `agent` in ~/.local/bin (see https://cursor.com/docs/cli/installation).
30
- */
31
- export function resolveCursorAgentExecutable() {
32
- const env = augmentPathEnv();
33
- const home = homedir();
34
- const candidates = [
35
- join(home, '.local', 'bin', 'agent'),
36
- join(home, '.local', 'bin', 'cursor-agent'),
37
- ...(process.platform === 'win32'
38
- ? [
39
- join(home, '.local', 'bin', 'agent.cmd'),
40
- join(home, '.local', 'bin', 'cursor-agent.cmd'),
41
- ]
42
- : []),
43
- join(home, '.cursor', 'bin', 'agent'),
44
- join(home, '.cursor', 'bin', 'cursor-agent'),
45
- 'agent',
46
- 'cursor-agent',
47
- ];
48
- if (process.platform === 'win32') {
49
- const base = join(home, 'AppData', 'Local', 'Programs', 'cursor');
50
- candidates.splice(
51
- candidates.length - 2,
52
- 0,
53
- join(base, 'agent.exe'),
54
- join(base, 'cursor-agent.exe'),
55
- );
56
- }
57
- for (const cmd of candidates) {
58
- if (isAbsolute(cmd) && !existsSync(cmd)) {
59
- continue;
60
- }
61
- const r = spawnSync(cmd, ['--version'], { stdio: 'ignore', env });
62
- if (r.status === 0) {
63
- return cmd;
64
- }
65
- }
66
- return null;
67
- }
@@ -1,28 +0,0 @@
1
- import { join } from 'node:path';
2
- import { MCP_SERVER_NAME } from './constants.js';
3
- import { readJson, writeJson } from './fs-helpers.js';
4
-
5
- /**
6
- * Merge project-level Cursor CLI permissions so cursor-agent can use the SRAI MCP tools.
7
- * @see https://docs.cursor.com/cli/reference/configuration
8
- * @see https://docs.cursor.com/cli/reference/permissions
9
- */
10
- export function mergeCursorCliMcpAllowlist(cwd) {
11
- const cliPath = join(cwd, '.cursor', 'cli.json');
12
- const existing = readJson(cliPath) || {};
13
- if (!existing.permissions || typeof existing.permissions !== 'object' || Array.isArray(existing.permissions)) {
14
- existing.permissions = {};
15
- }
16
- if (!Array.isArray(existing.permissions.allow)) {
17
- existing.permissions.allow = [];
18
- }
19
- const token = `Mcp(${MCP_SERVER_NAME}:*)`;
20
- if (!existing.permissions.allow.includes(token)) {
21
- existing.permissions.allow.push(token);
22
- }
23
- if (!Array.isArray(existing.permissions.deny)) {
24
- existing.permissions.deny = [];
25
- }
26
- writeJson(cliPath, existing);
27
- return cliPath;
28
- }
@@ -1,27 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { TARGETS, TARGET_NAMES } from './constants.js';
4
-
5
- /**
6
- * Auto-detect which IDE/CLI targets are configured in the given directory
7
- * by checking for known directories.
8
- * @param {string} cwd - The directory to scan
9
- * @returns {string[]} - List of detected target keys
10
- */
11
- export function detectTargets(cwd) {
12
- const detected = [];
13
- const seen = new Set();
14
-
15
- for (const key of TARGET_NAMES) {
16
- const target = TARGETS[key];
17
- for (const dir of target.detectDirs) {
18
- const fullPath = join(cwd, dir);
19
- if (existsSync(fullPath) && !seen.has(key)) {
20
- detected.push(key);
21
- seen.add(key);
22
- }
23
- }
24
- }
25
-
26
- return detected;
27
- }
@@ -1,82 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { SENTINEL_START, SENTINEL_END } from './constants.js';
4
-
5
- /**
6
- * Ensure a directory exists, creating parent dirs as needed.
7
- */
8
- export function ensureDir(dirPath) {
9
- if (!existsSync(dirPath)) {
10
- mkdirSync(dirPath, { recursive: true });
11
- }
12
- }
13
-
14
- /**
15
- * Read a JSON file, returning null if it doesn't exist or is invalid.
16
- */
17
- export function readJson(filePath) {
18
- try {
19
- const raw = readFileSync(filePath, 'utf-8');
20
- return JSON.parse(raw);
21
- } catch {
22
- return null;
23
- }
24
- }
25
-
26
- /**
27
- * Write an object as pretty-printed JSON.
28
- */
29
- export function writeJson(filePath, data) {
30
- ensureDir(dirname(filePath));
31
- writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
32
- }
33
-
34
- /**
35
- * Read a text file, returning empty string if it doesn't exist.
36
- */
37
- export function readText(filePath) {
38
- try {
39
- return readFileSync(filePath, 'utf-8');
40
- } catch {
41
- return '';
42
- }
43
- }
44
-
45
- /**
46
- * Write raw text to a file, ensuring parent dirs exist.
47
- */
48
- export function writeText(filePath, content) {
49
- ensureDir(dirname(filePath));
50
- writeFileSync(filePath, content, 'utf-8');
51
- }
52
-
53
- /**
54
- * Append or replace a sentinel-guarded block in a file.
55
- * If the file exists and already has the block, it replaces it.
56
- * If the file exists but doesn't have the block, it appends.
57
- * If the file doesn't exist, it creates it with just the block.
58
- */
59
- export function upsertSentinelBlock(filePath, content) {
60
- const block = `${SENTINEL_START}\n${content}\n${SENTINEL_END}`;
61
- const existing = readText(filePath);
62
-
63
- if (!existing) {
64
- writeText(filePath, block + '\n');
65
- return 'created';
66
- }
67
-
68
- const startIdx = existing.indexOf(SENTINEL_START);
69
- const endIdx = existing.indexOf(SENTINEL_END);
70
-
71
- if (startIdx !== -1 && endIdx !== -1) {
72
- const before = existing.substring(0, startIdx);
73
- const after = existing.substring(endIdx + SENTINEL_END.length);
74
- writeText(filePath, before + block + after);
75
- return 'updated';
76
- }
77
-
78
- // Append with a blank line separator
79
- const separator = existing.endsWith('\n') ? '\n' : '\n\n';
80
- writeText(filePath, existing + separator + block + '\n');
81
- return 'appended';
82
- }
@@ -1,84 +0,0 @@
1
- import { copyFileSync, readFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { GUARDRAILS_PROFILER_SKILL_REL_DIR, GUARDRAILS_SELECTION_SKILL_REL_DIR } from './constants.js';
5
- import { ensureDir, writeText } from './fs-helpers.js';
6
-
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
-
9
- function sanitizeProjectName(value) {
10
- return String(value || '').replace(/[\r\n`]/g, ' ').trim();
11
- }
12
-
13
- function injectProjectName(content, projectName) {
14
- const resolved = sanitizeProjectName(projectName) || '<SRAI_PROJECT_NAME>';
15
- return content.replaceAll('{{SRAI_PROJECT_NAME}}', resolved).replaceAll('<SRAI_PROJECT_NAME>', resolved);
16
- }
17
-
18
- function injectSkillDir(content, skillDirRel) {
19
- return content
20
- .replaceAll('<GUARDRAILS_SKILL_DIR>', skillDirRel)
21
- .replaceAll('{{GUARDRAILS_SELECTION_SKILL_DIR}}', skillDirRel);
22
- }
23
-
24
- function injectSkillTemplate(content, projectName, skillDirRel) {
25
- return injectSkillDir(injectProjectName(content, projectName), skillDirRel);
26
- }
27
-
28
- const PROFILER_BUNDLE_ROOT = join(__dirname, '..', 'generators', 'rules', 'guardrails-profiler');
29
- const SELECTION_BUNDLE_ROOT = join(__dirname, '..', 'generators', 'rules', 'guardrails-selection');
30
-
31
- /**
32
- * Writes bundled guardrails skills under each selected IDE skills directory.
33
- *
34
- * @returns {string[]} Absolute paths to each skill root written */
35
- export function writeGuardrailsSkillBundles(cwd, options = {}) {
36
- const targets = Array.isArray(options.targets) ? options.targets : [];
37
- const written = [];
38
-
39
- for (const target of targets) {
40
- const profilerRel = GUARDRAILS_PROFILER_SKILL_REL_DIR[target];
41
- if (profilerRel) {
42
- const profilerDestBase = join(cwd, profilerRel);
43
- const profilerDestRefs = join(profilerDestBase, 'references');
44
- ensureDir(profilerDestRefs);
45
-
46
- const profilerSkillTemplate = readFileSync(join(PROFILER_BUNDLE_ROOT, 'SKILL.md'), 'utf-8');
47
- writeText(
48
- join(profilerDestBase, 'SKILL.md'),
49
- injectSkillTemplate(profilerSkillTemplate, options.projectName, profilerRel),
50
- );
51
-
52
- copyFileSync(
53
- join(PROFILER_BUNDLE_ROOT, 'references', 'signal-registry.json'),
54
- join(profilerDestRefs, 'signal-registry.json'),
55
- );
56
-
57
- written.push(profilerDestBase);
58
- }
59
-
60
- const selectionRel = GUARDRAILS_SELECTION_SKILL_REL_DIR[target];
61
- if (selectionRel) {
62
- const selectionDestBase = join(cwd, selectionRel);
63
- const selectionDestRefs = join(selectionDestBase, 'references');
64
- ensureDir(selectionDestRefs);
65
-
66
- const selectionSkillTemplate = readFileSync(join(SELECTION_BUNDLE_ROOT, 'SKILL.md'), 'utf-8');
67
- writeText(
68
- join(selectionDestBase, 'SKILL.md'),
69
- injectSkillTemplate(selectionSkillTemplate, options.projectName, selectionRel),
70
- );
71
-
72
- copyFileSync(
73
- join(SELECTION_BUNDLE_ROOT, 'references', 'category-threat-map.md'),
74
- join(selectionDestRefs, 'category-threat-map.md'),
75
- );
76
-
77
- written.push(selectionDestBase);
78
- }
79
- }
80
-
81
- return written;
82
- }
83
-
84
- export const writeGuardrailsProfilerBundles = writeGuardrailsSkillBundles;