@rafter-security/cli 0.5.3 → 0.5.9
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 +15 -3
- package/dist/commands/agent/audit-skill.js +2 -2
- package/dist/commands/agent/audit.js +96 -0
- package/dist/commands/agent/baseline.js +213 -0
- package/dist/commands/agent/exec.js +1 -1
- package/dist/commands/agent/index.js +4 -0
- package/dist/commands/agent/init.js +371 -29
- package/dist/commands/agent/install-hook.js +41 -47
- package/dist/commands/agent/scan.js +196 -23
- package/dist/commands/agent/status.js +65 -4
- package/dist/commands/agent/update-gitleaks.js +40 -0
- package/dist/commands/agent/verify.js +18 -4
- package/dist/commands/backend/run.js +69 -61
- package/dist/commands/ci/init.js +10 -3
- package/dist/commands/completion.js +320 -110
- package/dist/commands/hook/posttool.js +21 -7
- package/dist/commands/hook/pretool.js +50 -13
- package/dist/commands/issues/dedup.js +39 -0
- package/dist/commands/issues/from-scan.js +143 -0
- package/dist/commands/issues/from-text.js +185 -0
- package/dist/commands/issues/github-client.js +85 -0
- package/dist/commands/issues/index.js +25 -0
- package/dist/commands/issues/issue-builder.js +101 -0
- package/dist/commands/policy/export.js +7 -2
- package/dist/commands/scan/index.js +44 -0
- package/dist/core/audit-logger.js +41 -0
- package/dist/core/config-defaults.js +28 -0
- package/dist/core/config-manager.js +19 -2
- package/dist/core/pattern-engine.js +26 -1
- package/dist/core/risk-rules.js +5 -3
- package/dist/index.js +8 -2
- package/dist/scanners/gitleaks.js +5 -5
- package/dist/scanners/regex-scanner.js +12 -1
- package/dist/scanners/secret-patterns.js +3 -3
- package/dist/utils/binary-manager.js +59 -20
- package/dist/utils/skill-manager.js +5 -3
- package/package.json +2 -1
- package/resources/pre-commit-hook.sh +2 -2
- package/resources/pre-push-hook.sh +60 -0
- package/resources/rafter-security-skill.md +7 -11
|
@@ -52,6 +52,150 @@ function installClaudeCodeHooks() {
|
|
|
52
52
|
console.log(fmt.success(`Installed PreToolUse hooks to ${settingsPath}`));
|
|
53
53
|
console.log(fmt.success(`Installed PostToolUse hooks to ${settingsPath}`));
|
|
54
54
|
}
|
|
55
|
+
/** MCP server entry for rafter — shared across MCP-native clients */
|
|
56
|
+
const RAFTER_MCP_ENTRY = {
|
|
57
|
+
command: "rafter",
|
|
58
|
+
args: ["mcp", "serve"],
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Install MCP server config for Gemini CLI (~/.gemini/settings.json)
|
|
62
|
+
*/
|
|
63
|
+
function installGeminiMcp() {
|
|
64
|
+
const homeDir = os.homedir();
|
|
65
|
+
const geminiDir = path.join(homeDir, ".gemini");
|
|
66
|
+
const settingsPath = path.join(geminiDir, "settings.json");
|
|
67
|
+
if (!fs.existsSync(geminiDir)) {
|
|
68
|
+
fs.mkdirSync(geminiDir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
let settings = {};
|
|
71
|
+
if (fs.existsSync(settingsPath)) {
|
|
72
|
+
try {
|
|
73
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
console.log(fmt.warning("Existing Gemini settings.json was unreadable, creating new one"));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!settings.mcpServers)
|
|
80
|
+
settings.mcpServers = {};
|
|
81
|
+
settings.mcpServers.rafter = { ...RAFTER_MCP_ENTRY };
|
|
82
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
83
|
+
console.log(fmt.success(`Installed Rafter MCP server to ${settingsPath}`));
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Install MCP server config for Cursor (~/.cursor/mcp.json)
|
|
88
|
+
*/
|
|
89
|
+
function installCursorMcp() {
|
|
90
|
+
const homeDir = os.homedir();
|
|
91
|
+
const cursorDir = path.join(homeDir, ".cursor");
|
|
92
|
+
const mcpPath = path.join(cursorDir, "mcp.json");
|
|
93
|
+
if (!fs.existsSync(cursorDir)) {
|
|
94
|
+
fs.mkdirSync(cursorDir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
let config = {};
|
|
97
|
+
if (fs.existsSync(mcpPath)) {
|
|
98
|
+
try {
|
|
99
|
+
config = JSON.parse(fs.readFileSync(mcpPath, "utf-8"));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
console.log(fmt.warning("Existing Cursor mcp.json was unreadable, creating new one"));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!config.mcpServers)
|
|
106
|
+
config.mcpServers = {};
|
|
107
|
+
config.mcpServers.rafter = { ...RAFTER_MCP_ENTRY };
|
|
108
|
+
fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
109
|
+
console.log(fmt.success(`Installed Rafter MCP server to ${mcpPath}`));
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Install MCP server config for Windsurf (~/.codeium/windsurf/mcp_config.json)
|
|
114
|
+
*/
|
|
115
|
+
function installWindsurfMcp() {
|
|
116
|
+
const homeDir = os.homedir();
|
|
117
|
+
const windsurfDir = path.join(homeDir, ".codeium", "windsurf");
|
|
118
|
+
const mcpPath = path.join(windsurfDir, "mcp_config.json");
|
|
119
|
+
if (!fs.existsSync(windsurfDir)) {
|
|
120
|
+
fs.mkdirSync(windsurfDir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
let config = {};
|
|
123
|
+
if (fs.existsSync(mcpPath)) {
|
|
124
|
+
try {
|
|
125
|
+
config = JSON.parse(fs.readFileSync(mcpPath, "utf-8"));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
console.log(fmt.warning("Existing Windsurf mcp_config.json was unreadable, creating new one"));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!config.mcpServers)
|
|
132
|
+
config.mcpServers = {};
|
|
133
|
+
config.mcpServers.rafter = { ...RAFTER_MCP_ENTRY };
|
|
134
|
+
fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
135
|
+
console.log(fmt.success(`Installed Rafter MCP server to ${mcpPath}`));
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Install MCP server config for Continue.dev (~/.continue/config.json)
|
|
140
|
+
*/
|
|
141
|
+
function installContinueDevMcp() {
|
|
142
|
+
const homeDir = os.homedir();
|
|
143
|
+
const continueDir = path.join(homeDir, ".continue");
|
|
144
|
+
const configPath = path.join(continueDir, "config.json");
|
|
145
|
+
if (!fs.existsSync(continueDir)) {
|
|
146
|
+
fs.mkdirSync(continueDir, { recursive: true });
|
|
147
|
+
}
|
|
148
|
+
let config = {};
|
|
149
|
+
if (fs.existsSync(configPath)) {
|
|
150
|
+
try {
|
|
151
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
console.log(fmt.warning("Existing Continue.dev config.json was unreadable, creating new one"));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!config.mcpServers)
|
|
158
|
+
config.mcpServers = [];
|
|
159
|
+
// Remove existing rafter entry if present (array format)
|
|
160
|
+
if (Array.isArray(config.mcpServers)) {
|
|
161
|
+
config.mcpServers = config.mcpServers.filter((s) => s.name !== "rafter");
|
|
162
|
+
config.mcpServers.push({
|
|
163
|
+
name: "rafter",
|
|
164
|
+
command: RAFTER_MCP_ENTRY.command,
|
|
165
|
+
args: RAFTER_MCP_ENTRY.args,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Object format (newer Continue.dev versions)
|
|
170
|
+
config.mcpServers.rafter = { ...RAFTER_MCP_ENTRY };
|
|
171
|
+
}
|
|
172
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
173
|
+
console.log(fmt.success(`Installed Rafter MCP server to ${configPath}`));
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Install MCP config for Aider (~/.aider.conf.yml)
|
|
178
|
+
* Aider uses YAML config with mcpServers list
|
|
179
|
+
*/
|
|
180
|
+
function installAiderMcp() {
|
|
181
|
+
const homeDir = os.homedir();
|
|
182
|
+
const configPath = path.join(homeDir, ".aider.conf.yml");
|
|
183
|
+
// Aider's YAML config is simple — we append the MCP flag if not present
|
|
184
|
+
let content = "";
|
|
185
|
+
if (fs.existsSync(configPath)) {
|
|
186
|
+
content = fs.readFileSync(configPath, "utf-8");
|
|
187
|
+
}
|
|
188
|
+
// Check if rafter MCP is already configured
|
|
189
|
+
if (content.includes("rafter mcp serve")) {
|
|
190
|
+
console.log(fmt.success("Rafter MCP already configured in Aider config"));
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
// Append MCP server config
|
|
194
|
+
const mcpLine = "\n# Rafter security MCP server\nmcp-server-command: rafter mcp serve\n";
|
|
195
|
+
fs.writeFileSync(configPath, content + mcpLine, "utf-8");
|
|
196
|
+
console.log(fmt.success(`Installed Rafter MCP server to ${configPath}`));
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
55
199
|
async function installClaudeCodeSkills() {
|
|
56
200
|
const homeDir = os.homedir();
|
|
57
201
|
const claudeSkillsDir = path.join(homeDir, ".claude", "skills");
|
|
@@ -88,14 +232,53 @@ async function installClaudeCodeSkills() {
|
|
|
88
232
|
console.log(fmt.warning(`Agent Security skill template not found at ${agentTemplatePath}`));
|
|
89
233
|
}
|
|
90
234
|
}
|
|
235
|
+
function installCodexSkills() {
|
|
236
|
+
const homeDir = os.homedir();
|
|
237
|
+
const agentsSkillsDir = path.join(homeDir, ".agents", "skills");
|
|
238
|
+
// Install Backend Skill
|
|
239
|
+
const backendDir = path.join(agentsSkillsDir, "rafter");
|
|
240
|
+
const backendSkillPath = path.join(backendDir, "SKILL.md");
|
|
241
|
+
const backendTemplatePath = path.join(__dirname, "..", "..", "..", ".claude", "skills", "rafter", "SKILL.md");
|
|
242
|
+
if (!fs.existsSync(backendDir)) {
|
|
243
|
+
fs.mkdirSync(backendDir, { recursive: true });
|
|
244
|
+
}
|
|
245
|
+
if (fs.existsSync(backendTemplatePath)) {
|
|
246
|
+
fs.copyFileSync(backendTemplatePath, backendSkillPath);
|
|
247
|
+
console.log(fmt.success(`Installed Rafter Backend skill to ${backendSkillPath}`));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
console.log(fmt.warning(`Backend skill template not found at ${backendTemplatePath}`));
|
|
251
|
+
}
|
|
252
|
+
// Install Agent Security Skill
|
|
253
|
+
const agentDir = path.join(agentsSkillsDir, "rafter-agent-security");
|
|
254
|
+
const agentSkillPath = path.join(agentDir, "SKILL.md");
|
|
255
|
+
const agentTemplatePath = path.join(__dirname, "..", "..", "..", ".claude", "skills", "rafter-agent-security", "SKILL.md");
|
|
256
|
+
if (!fs.existsSync(agentDir)) {
|
|
257
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
258
|
+
}
|
|
259
|
+
if (fs.existsSync(agentTemplatePath)) {
|
|
260
|
+
fs.copyFileSync(agentTemplatePath, agentSkillPath);
|
|
261
|
+
console.log(fmt.success(`Installed Rafter Agent Security skill to ${agentSkillPath}`));
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
console.log(fmt.warning(`Agent Security skill template not found at ${agentTemplatePath}`));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
91
267
|
export function createInitCommand() {
|
|
92
268
|
return new Command("init")
|
|
93
269
|
.description("Initialize agent security system")
|
|
94
270
|
.option("--risk-level <level>", "Set risk level (minimal, moderate, aggressive)", "moderate")
|
|
95
|
-
.option("--
|
|
96
|
-
.option("--
|
|
97
|
-
.option("--
|
|
98
|
-
.option("--
|
|
271
|
+
.option("--with-openclaw", "Install OpenClaw integration")
|
|
272
|
+
.option("--with-claude-code", "Install Claude Code integration")
|
|
273
|
+
.option("--with-codex", "Install Codex CLI integration")
|
|
274
|
+
.option("--with-gemini", "Install Gemini CLI integration")
|
|
275
|
+
.option("--with-aider", "Install Aider integration")
|
|
276
|
+
.option("--with-cursor", "Install Cursor integration")
|
|
277
|
+
.option("--with-windsurf", "Install Windsurf integration")
|
|
278
|
+
.option("--with-continue", "Install Continue.dev integration")
|
|
279
|
+
.option("--with-gitleaks", "Download and install Gitleaks binary")
|
|
280
|
+
.option("--all", "Install all detected integrations and download Gitleaks")
|
|
281
|
+
.option("--update", "Re-download gitleaks and reinstall integrations without resetting config")
|
|
99
282
|
.action(async (opts) => {
|
|
100
283
|
console.log(fmt.header("Rafter Agent Security Setup"));
|
|
101
284
|
console.log(fmt.divider());
|
|
@@ -103,19 +286,64 @@ export function createInitCommand() {
|
|
|
103
286
|
const manager = new ConfigManager();
|
|
104
287
|
// Detect environments
|
|
105
288
|
const hasOpenClaw = fs.existsSync(path.join(os.homedir(), ".openclaw"));
|
|
106
|
-
const hasClaudeCode =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
289
|
+
const hasClaudeCode = fs.existsSync(path.join(os.homedir(), ".claude"));
|
|
290
|
+
const hasCodex = fs.existsSync(path.join(os.homedir(), ".codex"));
|
|
291
|
+
const hasGemini = fs.existsSync(path.join(os.homedir(), ".gemini"));
|
|
292
|
+
const hasCursor = fs.existsSync(path.join(os.homedir(), ".cursor"));
|
|
293
|
+
const hasWindsurf = fs.existsSync(path.join(os.homedir(), ".codeium", "windsurf"));
|
|
294
|
+
const hasContinueDev = fs.existsSync(path.join(os.homedir(), ".continue"));
|
|
295
|
+
const hasAider = fs.existsSync(path.join(os.homedir(), ".aider.conf.yml"));
|
|
296
|
+
// Resolve opt-in flags (--all enables all detected)
|
|
297
|
+
const wantOpenClaw = opts.withOpenclaw || opts.all;
|
|
298
|
+
const wantClaudeCode = opts.withClaudeCode || opts.all;
|
|
299
|
+
const wantCodex = opts.withCodex || opts.all;
|
|
300
|
+
const wantGemini = opts.withGemini || opts.all;
|
|
301
|
+
const wantCursor = opts.withCursor || opts.all;
|
|
302
|
+
const wantWindsurf = opts.withWindsurf || opts.all;
|
|
303
|
+
const wantContinue = opts.withContinue || opts.all;
|
|
304
|
+
const wantAider = opts.withAider || opts.all;
|
|
305
|
+
const wantGitleaks = opts.withGitleaks || opts.all;
|
|
306
|
+
// Show detected environments with opt-in hints
|
|
307
|
+
const detected = [];
|
|
308
|
+
if (hasOpenClaw)
|
|
309
|
+
detected.push("OpenClaw");
|
|
310
|
+
if (hasClaudeCode)
|
|
311
|
+
detected.push("Claude Code");
|
|
312
|
+
if (hasCodex)
|
|
313
|
+
detected.push("Codex CLI");
|
|
314
|
+
if (hasGemini)
|
|
315
|
+
detected.push("Gemini CLI");
|
|
316
|
+
if (hasCursor)
|
|
317
|
+
detected.push("Cursor");
|
|
318
|
+
if (hasWindsurf)
|
|
319
|
+
detected.push("Windsurf");
|
|
320
|
+
if (hasContinueDev)
|
|
321
|
+
detected.push("Continue.dev");
|
|
322
|
+
if (hasAider)
|
|
323
|
+
detected.push("Aider");
|
|
324
|
+
if (detected.length > 0) {
|
|
325
|
+
console.log(fmt.info(`Detected environments: ${detected.join(", ")}`));
|
|
115
326
|
}
|
|
116
327
|
else {
|
|
117
|
-
console.log(fmt.info("
|
|
328
|
+
console.log(fmt.info("No agent environments detected"));
|
|
118
329
|
}
|
|
330
|
+
// Warn about requested but undetected environments
|
|
331
|
+
if (wantOpenClaw && !hasOpenClaw)
|
|
332
|
+
console.log(fmt.warning("OpenClaw requested but not detected (~/.openclaw not found)"));
|
|
333
|
+
if (wantClaudeCode && !hasClaudeCode)
|
|
334
|
+
console.log(fmt.warning("Claude Code requested but not detected (~/.claude not found)"));
|
|
335
|
+
if (wantCodex && !hasCodex)
|
|
336
|
+
console.log(fmt.warning("Codex CLI requested but not detected (~/.codex not found)"));
|
|
337
|
+
if (wantGemini && !hasGemini)
|
|
338
|
+
console.log(fmt.warning("Gemini CLI requested but not detected (~/.gemini not found)"));
|
|
339
|
+
if (wantCursor && !hasCursor)
|
|
340
|
+
console.log(fmt.warning("Cursor requested but not detected (~/.cursor not found)"));
|
|
341
|
+
if (wantWindsurf && !hasWindsurf)
|
|
342
|
+
console.log(fmt.warning("Windsurf requested but not detected (~/.codeium/windsurf not found)"));
|
|
343
|
+
if (wantContinue && !hasContinueDev)
|
|
344
|
+
console.log(fmt.warning("Continue.dev requested but not detected (~/.continue not found)"));
|
|
345
|
+
if (wantAider && !hasAider)
|
|
346
|
+
console.log(fmt.warning("Aider requested but not detected (~/.aider.conf.yml not found)"));
|
|
119
347
|
// Initialize directory structure
|
|
120
348
|
try {
|
|
121
349
|
await manager.initialize();
|
|
@@ -134,8 +362,8 @@ export function createInitCommand() {
|
|
|
134
362
|
}
|
|
135
363
|
manager.set("agent.riskLevel", opts.riskLevel);
|
|
136
364
|
console.log(fmt.success(`Set risk level: ${opts.riskLevel}`));
|
|
137
|
-
// Check / download Gitleaks binary (
|
|
138
|
-
if (
|
|
365
|
+
// Check / download Gitleaks binary (opt-in via --with-gitleaks or --all)
|
|
366
|
+
if (wantGitleaks) {
|
|
139
367
|
const binaryManager = new BinaryManager();
|
|
140
368
|
const platformInfo = binaryManager.getPlatformInfo();
|
|
141
369
|
// Helper: show diagnostics for a failing binary (mirrors Python's agent init)
|
|
@@ -151,7 +379,7 @@ export function createInitCommand() {
|
|
|
151
379
|
console.log(fmt.info("To fix: install gitleaks (https://github.com/gitleaks/gitleaks/releases) and ensure it is on PATH, then re-run 'rafter agent init'."));
|
|
152
380
|
console.log();
|
|
153
381
|
};
|
|
154
|
-
if (binaryManager.isGitleaksInstalled()) {
|
|
382
|
+
if (!opts.update && binaryManager.isGitleaksInstalled()) {
|
|
155
383
|
// Local binary exists — verify it actually works
|
|
156
384
|
const verResult = await binaryManager.verifyGitleaksVerbose();
|
|
157
385
|
if (verResult.ok) {
|
|
@@ -164,8 +392,9 @@ export function createInitCommand() {
|
|
|
164
392
|
}
|
|
165
393
|
}
|
|
166
394
|
else {
|
|
167
|
-
// Not installed locally — check PATH
|
|
168
|
-
|
|
395
|
+
// Not installed locally (or --update forcing re-download) — check PATH first
|
|
396
|
+
// unless --update was passed (in that case force a fresh managed install)
|
|
397
|
+
const pathBinary = opts.update ? null : binaryManager.findGitleaksOnPath();
|
|
169
398
|
if (pathBinary) {
|
|
170
399
|
const verResult = await binaryManager.verifyGitleaksVerbose(pathBinary);
|
|
171
400
|
if (verResult.ok) {
|
|
@@ -202,10 +431,12 @@ export function createInitCommand() {
|
|
|
202
431
|
}
|
|
203
432
|
}
|
|
204
433
|
}
|
|
205
|
-
// Install OpenClaw skill if
|
|
206
|
-
|
|
434
|
+
// Install OpenClaw skill if opted in
|
|
435
|
+
let openclawOk = false;
|
|
436
|
+
if (hasOpenClaw && wantOpenClaw) {
|
|
207
437
|
const skillManager = new SkillManager();
|
|
208
438
|
const result = await skillManager.installRafterSkillVerbose();
|
|
439
|
+
openclawOk = result.ok;
|
|
209
440
|
if (result.ok) {
|
|
210
441
|
console.log(fmt.success("Installed Rafter Security skill to ~/.openclaw/skills/rafter-security.md"));
|
|
211
442
|
manager.set("agent.environments.openclaw.enabled", true);
|
|
@@ -219,28 +450,139 @@ export function createInitCommand() {
|
|
|
219
450
|
}
|
|
220
451
|
}
|
|
221
452
|
}
|
|
222
|
-
// Install Claude Code skills + hooks if
|
|
223
|
-
|
|
453
|
+
// Install Claude Code skills + hooks if opted in
|
|
454
|
+
let claudeCodeOk = false;
|
|
455
|
+
if (hasClaudeCode && wantClaudeCode) {
|
|
224
456
|
try {
|
|
225
457
|
await installClaudeCodeSkills();
|
|
226
458
|
installClaudeCodeHooks();
|
|
227
459
|
manager.set("agent.environments.claudeCode.enabled", true);
|
|
460
|
+
claudeCodeOk = true;
|
|
228
461
|
}
|
|
229
462
|
catch (e) {
|
|
230
463
|
console.error(fmt.error(`Failed to install Claude Code integration: ${e}`));
|
|
231
464
|
}
|
|
232
465
|
}
|
|
466
|
+
// Install Codex CLI skills if opted in
|
|
467
|
+
let codexOk = false;
|
|
468
|
+
if (hasCodex && wantCodex) {
|
|
469
|
+
try {
|
|
470
|
+
installCodexSkills();
|
|
471
|
+
manager.set("agent.environments.codex.enabled", true);
|
|
472
|
+
codexOk = true;
|
|
473
|
+
}
|
|
474
|
+
catch (e) {
|
|
475
|
+
console.error(fmt.error(`Failed to install Codex CLI integration: ${e}`));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Install Gemini CLI MCP if opted in
|
|
479
|
+
let geminiOk = false;
|
|
480
|
+
if (hasGemini && wantGemini) {
|
|
481
|
+
try {
|
|
482
|
+
geminiOk = installGeminiMcp();
|
|
483
|
+
if (geminiOk)
|
|
484
|
+
manager.set("agent.environments.gemini.enabled", true);
|
|
485
|
+
}
|
|
486
|
+
catch (e) {
|
|
487
|
+
console.error(fmt.error(`Failed to install Gemini CLI integration: ${e}`));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// Install Cursor MCP if opted in
|
|
491
|
+
let cursorOk = false;
|
|
492
|
+
if (hasCursor && wantCursor) {
|
|
493
|
+
try {
|
|
494
|
+
cursorOk = installCursorMcp();
|
|
495
|
+
if (cursorOk)
|
|
496
|
+
manager.set("agent.environments.cursor.enabled", true);
|
|
497
|
+
}
|
|
498
|
+
catch (e) {
|
|
499
|
+
console.error(fmt.error(`Failed to install Cursor integration: ${e}`));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// Install Windsurf MCP if opted in
|
|
503
|
+
let windsurfOk = false;
|
|
504
|
+
if (hasWindsurf && wantWindsurf) {
|
|
505
|
+
try {
|
|
506
|
+
windsurfOk = installWindsurfMcp();
|
|
507
|
+
if (windsurfOk)
|
|
508
|
+
manager.set("agent.environments.windsurf.enabled", true);
|
|
509
|
+
}
|
|
510
|
+
catch (e) {
|
|
511
|
+
console.error(fmt.error(`Failed to install Windsurf integration: ${e}`));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// Install Continue.dev MCP if opted in
|
|
515
|
+
let continueOk = false;
|
|
516
|
+
if (hasContinueDev && wantContinue) {
|
|
517
|
+
try {
|
|
518
|
+
continueOk = installContinueDevMcp();
|
|
519
|
+
if (continueOk)
|
|
520
|
+
manager.set("agent.environments.continueDev.enabled", true);
|
|
521
|
+
}
|
|
522
|
+
catch (e) {
|
|
523
|
+
console.error(fmt.error(`Failed to install Continue.dev integration: ${e}`));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Install Aider MCP if opted in
|
|
527
|
+
let aiderOk = false;
|
|
528
|
+
if (hasAider && wantAider) {
|
|
529
|
+
try {
|
|
530
|
+
aiderOk = installAiderMcp();
|
|
531
|
+
if (aiderOk)
|
|
532
|
+
manager.set("agent.environments.aider.enabled", true);
|
|
533
|
+
}
|
|
534
|
+
catch (e) {
|
|
535
|
+
console.error(fmt.error(`Failed to install Aider integration: ${e}`));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
233
538
|
console.log();
|
|
234
539
|
console.log(fmt.success("Agent security initialized!"));
|
|
235
540
|
console.log();
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
console.log("
|
|
541
|
+
const anyIntegration = openclawOk || claudeCodeOk || codexOk || geminiOk || cursorOk || windsurfOk || continueOk || aiderOk;
|
|
542
|
+
if (anyIntegration) {
|
|
543
|
+
console.log("Next steps:");
|
|
544
|
+
if (openclawOk)
|
|
545
|
+
console.log(" - Restart OpenClaw to load skill");
|
|
546
|
+
if (claudeCodeOk)
|
|
547
|
+
console.log(" - Restart Claude Code to load skills");
|
|
548
|
+
if (codexOk)
|
|
549
|
+
console.log(" - Restart Codex CLI to load skills");
|
|
550
|
+
if (geminiOk)
|
|
551
|
+
console.log(" - Restart Gemini CLI to load MCP server");
|
|
552
|
+
if (cursorOk)
|
|
553
|
+
console.log(" - Restart Cursor to load MCP server");
|
|
554
|
+
if (windsurfOk)
|
|
555
|
+
console.log(" - Restart Windsurf to load MCP server");
|
|
556
|
+
if (continueOk)
|
|
557
|
+
console.log(" - Restart Continue.dev to load MCP server");
|
|
558
|
+
if (aiderOk)
|
|
559
|
+
console.log(" - Restart Aider to load MCP server");
|
|
239
560
|
}
|
|
240
|
-
if (
|
|
241
|
-
console.log("
|
|
561
|
+
else if (detected.length > 0) {
|
|
562
|
+
console.log("No integrations were installed. To install, re-run with opt-in flags:");
|
|
563
|
+
console.log(" rafter agent init --all # Install all detected");
|
|
564
|
+
if (hasClaudeCode)
|
|
565
|
+
console.log(" rafter agent init --with-claude-code # Claude Code only");
|
|
566
|
+
if (hasOpenClaw)
|
|
567
|
+
console.log(" rafter agent init --with-openclaw # OpenClaw only");
|
|
568
|
+
if (hasCodex)
|
|
569
|
+
console.log(" rafter agent init --with-codex # Codex CLI only");
|
|
570
|
+
if (hasGemini)
|
|
571
|
+
console.log(" rafter agent init --with-gemini # Gemini CLI only");
|
|
572
|
+
if (hasCursor)
|
|
573
|
+
console.log(" rafter agent init --with-cursor # Cursor only");
|
|
574
|
+
if (hasWindsurf)
|
|
575
|
+
console.log(" rafter agent init --with-windsurf # Windsurf only");
|
|
576
|
+
if (hasContinueDev)
|
|
577
|
+
console.log(" rafter agent init --with-continue # Continue.dev only");
|
|
578
|
+
if (hasAider)
|
|
579
|
+
console.log(" rafter agent init --with-aider # Aider only");
|
|
242
580
|
}
|
|
243
|
-
|
|
581
|
+
else {
|
|
582
|
+
console.log("No agent environments detected. Install an agent tool and re-run with --with-<tool>.");
|
|
583
|
+
}
|
|
584
|
+
console.log();
|
|
585
|
+
console.log(" - Run: rafter scan local . (test secret scanning)");
|
|
244
586
|
console.log(" - Configure: rafter agent config show");
|
|
245
587
|
console.log();
|
|
246
588
|
});
|
|
@@ -8,25 +8,36 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
8
8
|
const __dirname = path.dirname(__filename);
|
|
9
9
|
export function createInstallHookCommand() {
|
|
10
10
|
return new Command("install-hook")
|
|
11
|
-
.description("Install
|
|
11
|
+
.description("Install git hook to scan for secrets")
|
|
12
12
|
.option("--global", "Install globally for all repos (via git config)")
|
|
13
|
+
.option("--push", "Install pre-push hook instead of pre-commit")
|
|
13
14
|
.action(async (opts) => {
|
|
14
15
|
await installHook(opts);
|
|
15
16
|
});
|
|
16
17
|
}
|
|
17
18
|
async function installHook(opts) {
|
|
19
|
+
const hookName = opts.push ? "pre-push" : "pre-commit";
|
|
20
|
+
const templateName = opts.push ? "pre-push-hook.sh" : "pre-commit-hook.sh";
|
|
18
21
|
if (opts.global) {
|
|
19
|
-
await installGlobalHook();
|
|
22
|
+
await installGlobalHook(hookName, templateName);
|
|
20
23
|
}
|
|
21
24
|
else {
|
|
22
|
-
await installLocalHook();
|
|
25
|
+
await installLocalHook(hookName, templateName);
|
|
23
26
|
}
|
|
24
27
|
}
|
|
28
|
+
function getTemplatePath(templateName) {
|
|
29
|
+
const templatePath = path.join(__dirname, "..", "..", "..", "resources", templateName);
|
|
30
|
+
if (!fs.existsSync(templatePath)) {
|
|
31
|
+
console.error("❌ Error: Hook template not found");
|
|
32
|
+
console.error(` Expected at: ${templatePath}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
return templatePath;
|
|
36
|
+
}
|
|
25
37
|
/**
|
|
26
|
-
* Install
|
|
38
|
+
* Install hook for current repository
|
|
27
39
|
*/
|
|
28
|
-
async function installLocalHook() {
|
|
29
|
-
// Check if in a git repository
|
|
40
|
+
async function installLocalHook(hookName, templateName) {
|
|
30
41
|
try {
|
|
31
42
|
execSync("git rev-parse --git-dir", { stdio: "pipe" });
|
|
32
43
|
}
|
|
@@ -35,80 +46,63 @@ async function installLocalHook() {
|
|
|
35
46
|
console.error(" Run this command from inside a git repository");
|
|
36
47
|
process.exit(1);
|
|
37
48
|
}
|
|
38
|
-
// Get .git directory
|
|
39
49
|
const gitDir = execSync("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
|
|
40
50
|
const hooksDir = path.resolve(gitDir, "hooks");
|
|
41
|
-
const hookPath = path.join(hooksDir,
|
|
42
|
-
// Ensure hooks directory exists
|
|
51
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
43
52
|
if (!fs.existsSync(hooksDir)) {
|
|
44
53
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
45
54
|
}
|
|
46
|
-
// Check if hook already exists
|
|
47
55
|
if (fs.existsSync(hookPath)) {
|
|
48
56
|
const existing = fs.readFileSync(hookPath, "utf-8");
|
|
49
|
-
|
|
50
|
-
if (existing.includes(
|
|
51
|
-
console.log(
|
|
57
|
+
const marker = hookName === "pre-push" ? "Rafter Security Pre-Push Hook" : "Rafter Security Pre-Commit Hook";
|
|
58
|
+
if (existing.includes(marker)) {
|
|
59
|
+
console.log(`✓ Rafter ${hookName} hook already installed`);
|
|
52
60
|
return;
|
|
53
61
|
}
|
|
54
|
-
// Backup existing hook
|
|
55
62
|
const backupPath = `${hookPath}.backup-${Date.now()}`;
|
|
56
63
|
fs.copyFileSync(hookPath, backupPath);
|
|
57
64
|
console.log(`📦 Backed up existing hook to: ${path.basename(backupPath)}`);
|
|
58
65
|
}
|
|
59
|
-
|
|
60
|
-
const templatePath = path.join(__dirname, "..", "..", "..", "resources", "pre-commit-hook.sh");
|
|
61
|
-
if (!fs.existsSync(templatePath)) {
|
|
62
|
-
console.error("❌ Error: Hook template not found");
|
|
63
|
-
console.error(` Expected at: ${templatePath}`);
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
// Copy hook template
|
|
67
|
-
const hookContent = fs.readFileSync(templatePath, "utf-8");
|
|
66
|
+
const hookContent = fs.readFileSync(getTemplatePath(templateName), "utf-8");
|
|
68
67
|
fs.writeFileSync(hookPath, hookContent, "utf-8");
|
|
69
|
-
// Make executable
|
|
70
68
|
fs.chmodSync(hookPath, 0o755);
|
|
71
|
-
console.log(
|
|
69
|
+
console.log(`✓ Installed Rafter ${hookName} hook`);
|
|
72
70
|
console.log(` Location: ${hookPath}`);
|
|
73
71
|
console.log();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
if (hookName === "pre-push") {
|
|
73
|
+
console.log("The hook will:");
|
|
74
|
+
console.log(" • Scan commits being pushed for secrets");
|
|
75
|
+
console.log(" • Block pushes if secrets are detected");
|
|
76
|
+
console.log(" • Can be bypassed with: git push --no-verify (not recommended)");
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
console.log("The hook will:");
|
|
80
|
+
console.log(" • Scan staged files for secrets before each commit");
|
|
81
|
+
console.log(" • Block commits if secrets are detected");
|
|
82
|
+
console.log(" • Can be bypassed with: git commit --no-verify (not recommended)");
|
|
83
|
+
}
|
|
78
84
|
console.log();
|
|
79
85
|
}
|
|
80
86
|
/**
|
|
81
|
-
* Install
|
|
87
|
+
* Install hook globally for all repositories
|
|
82
88
|
*/
|
|
83
|
-
async function installGlobalHook() {
|
|
84
|
-
// Create global hooks directory
|
|
89
|
+
async function installGlobalHook(hookName, templateName) {
|
|
85
90
|
const homeDir = os.homedir();
|
|
86
91
|
if (!homeDir) {
|
|
87
92
|
console.error("❌ Error: Could not determine home directory");
|
|
88
93
|
process.exit(1);
|
|
89
94
|
}
|
|
90
95
|
const globalHooksDir = path.join(homeDir, ".rafter", "git-hooks");
|
|
91
|
-
const hookPath = path.join(globalHooksDir,
|
|
92
|
-
// Create directory
|
|
96
|
+
const hookPath = path.join(globalHooksDir, hookName);
|
|
93
97
|
if (!fs.existsSync(globalHooksDir)) {
|
|
94
98
|
fs.mkdirSync(globalHooksDir, { recursive: true });
|
|
95
99
|
}
|
|
96
|
-
|
|
97
|
-
const templatePath = path.join(__dirname, "..", "..", "..", "resources", "pre-commit-hook.sh");
|
|
98
|
-
if (!fs.existsSync(templatePath)) {
|
|
99
|
-
console.error("❌ Error: Hook template not found");
|
|
100
|
-
console.error(` Expected at: ${templatePath}`);
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
// Copy hook template
|
|
104
|
-
const hookContent = fs.readFileSync(templatePath, "utf-8");
|
|
100
|
+
const hookContent = fs.readFileSync(getTemplatePath(templateName), "utf-8");
|
|
105
101
|
fs.writeFileSync(hookPath, hookContent, "utf-8");
|
|
106
|
-
// Make executable
|
|
107
102
|
fs.chmodSync(hookPath, 0o755);
|
|
108
|
-
// Configure git to use global hooks directory
|
|
109
103
|
try {
|
|
110
104
|
execSync(`git config --global core.hooksPath "${globalHooksDir}"`, { stdio: "pipe" });
|
|
111
|
-
console.log(
|
|
105
|
+
console.log(`✓ Installed Rafter ${hookName} hook globally`);
|
|
112
106
|
console.log(` Location: ${hookPath}`);
|
|
113
107
|
console.log(` Git config: core.hooksPath = ${globalHooksDir}`);
|
|
114
108
|
console.log();
|
|
@@ -118,7 +112,7 @@ async function installGlobalHook() {
|
|
|
118
112
|
console.log(` git config --global --unset core.hooksPath`);
|
|
119
113
|
console.log();
|
|
120
114
|
console.log("To install per-repository instead:");
|
|
121
|
-
console.log(` cd <repo> && rafter agent install-hook`);
|
|
115
|
+
console.log(` cd <repo> && rafter agent install-hook${hookName === "pre-push" ? " --push" : ""}`);
|
|
122
116
|
console.log();
|
|
123
117
|
}
|
|
124
118
|
catch (e) {
|