@rafter-security/cli 0.4.2 → 0.5.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.
Files changed (36) hide show
  1. package/README.md +101 -1
  2. package/dist/commands/agent/audit-skill.js +6 -0
  3. package/dist/commands/agent/audit.js +15 -3
  4. package/dist/commands/agent/exec.js +9 -8
  5. package/dist/commands/agent/index.js +4 -0
  6. package/dist/commands/agent/init.js +132 -47
  7. package/dist/commands/agent/install-hook.js +2 -1
  8. package/dist/commands/agent/scan.js +180 -103
  9. package/dist/commands/agent/status.js +115 -0
  10. package/dist/commands/agent/verify.js +117 -0
  11. package/dist/commands/ci/index.js +8 -0
  12. package/dist/commands/ci/init.js +191 -0
  13. package/dist/commands/completion.js +170 -0
  14. package/dist/commands/hook/index.js +10 -0
  15. package/dist/commands/hook/posttool.js +73 -0
  16. package/dist/commands/hook/pretool.js +122 -0
  17. package/dist/commands/mcp/index.js +8 -0
  18. package/dist/commands/mcp/server.js +205 -0
  19. package/dist/commands/policy/export.js +81 -0
  20. package/dist/commands/policy/index.js +8 -0
  21. package/dist/core/audit-logger.js +2 -33
  22. package/dist/core/command-interceptor.js +6 -50
  23. package/dist/core/config-defaults.js +4 -15
  24. package/dist/core/config-manager.js +68 -0
  25. package/dist/core/custom-patterns.js +157 -0
  26. package/dist/core/policy-loader.js +167 -0
  27. package/dist/core/risk-rules.js +72 -0
  28. package/dist/index.js +26 -2
  29. package/dist/scanners/gitleaks.js +7 -6
  30. package/dist/scanners/regex-scanner.js +28 -12
  31. package/dist/utils/binary-manager.js +100 -7
  32. package/dist/utils/formatter.js +52 -0
  33. package/dist/utils/skill-manager.js +22 -9
  34. package/package.json +7 -3
  35. package/resources/pre-commit-hook.sh +45 -0
  36. package/resources/rafter-security-skill.md +323 -0
@@ -1,31 +1,44 @@
1
1
  import { Command } from "commander";
2
2
  import { RegexScanner } from "../../scanners/regex-scanner.js";
3
3
  import { GitleaksScanner } from "../../scanners/gitleaks.js";
4
- import { execSync } from "child_process";
4
+ import { ConfigManager } from "../../core/config-manager.js";
5
+ import { execSync, execFileSync } from "child_process";
5
6
  import fs from "fs";
6
7
  import path from "path";
8
+ import { fmt } from "../../utils/formatter.js";
7
9
  export function createScanCommand() {
8
10
  return new Command("scan")
9
11
  .description("Scan files or directories for secrets")
10
12
  .argument("[path]", "File or directory to scan", ".")
11
13
  .option("-q, --quiet", "Only output if secrets found")
12
14
  .option("--json", "Output as JSON")
15
+ .option("--format <format>", "Output format: text, json, sarif", "text")
13
16
  .option("--staged", "Scan only git staged files")
17
+ .option("--diff <ref>", "Scan files changed since a git ref")
14
18
  .option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
15
19
  .action(async (scanPath, opts) => {
20
+ // Load policy-merged config for excludePaths/customPatterns
21
+ const manager = new ConfigManager();
22
+ const cfg = manager.loadWithPolicy();
23
+ const scanCfg = cfg.agent?.scan;
24
+ // Handle --diff flag
25
+ if (opts.diff) {
26
+ await scanDiffFiles(opts.diff, opts, scanCfg);
27
+ return;
28
+ }
16
29
  // Handle --staged flag
17
30
  if (opts.staged) {
18
- await scanStagedFiles(opts);
31
+ await scanStagedFiles(opts, scanCfg);
19
32
  return;
20
33
  }
21
34
  const resolvedPath = path.resolve(scanPath);
22
35
  // Check if path exists
23
36
  if (!fs.existsSync(resolvedPath)) {
24
37
  console.error(`Error: Path not found: ${resolvedPath}`);
25
- process.exit(1);
38
+ process.exit(2);
26
39
  }
27
40
  // Determine scan engine
28
- const engine = await selectEngine(opts.engine, opts.quiet);
41
+ const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
29
42
  // Determine if path is file or directory
30
43
  const stats = fs.statSync(resolvedPath);
31
44
  let results;
@@ -33,61 +46,173 @@ export function createScanCommand() {
33
46
  if (!opts.quiet) {
34
47
  console.error(`Scanning directory: ${resolvedPath} (${engine})`);
35
48
  }
36
- results = await scanDirectory(resolvedPath, engine);
49
+ results = await scanDirectory(resolvedPath, engine, scanCfg);
37
50
  }
38
51
  else {
39
52
  if (!opts.quiet) {
40
53
  console.error(`Scanning file: ${resolvedPath} (${engine})`);
41
54
  }
42
- results = await scanFile(resolvedPath, engine);
43
- }
44
- // Output results
45
- if (opts.json) {
46
- console.log(JSON.stringify(results, null, 2));
55
+ results = await scanFile(resolvedPath, engine, scanCfg);
47
56
  }
48
- else {
49
- if (results.length === 0) {
50
- if (!opts.quiet) {
51
- console.log("\n✓ No secrets detected\n");
52
- }
53
- process.exit(0);
57
+ outputScanResults(results, opts);
58
+ });
59
+ }
60
+ /**
61
+ * Emit SARIF 2.1.0 JSON for GitHub/GitLab security tab integration
62
+ */
63
+ function outputSarif(results) {
64
+ const rules = new Map();
65
+ const sarifResults = [];
66
+ for (const r of results) {
67
+ for (const m of r.matches) {
68
+ const ruleId = m.pattern.name.toLowerCase().replace(/\s+/g, "-");
69
+ if (!rules.has(ruleId)) {
70
+ rules.set(ruleId, {
71
+ id: ruleId,
72
+ name: m.pattern.name,
73
+ shortDescription: m.pattern.description || m.pattern.name,
74
+ });
54
75
  }
55
- else {
56
- console.log(`\n⚠️ Found secrets in ${results.length} file(s):\n`);
57
- let totalMatches = 0;
58
- for (const result of results) {
59
- console.log(`\n📄 ${result.file}`);
60
- for (const match of result.matches) {
61
- totalMatches++;
62
- const location = match.line ? `Line ${match.line}` : "Unknown location";
63
- const severity = getSeverityEmoji(match.pattern.severity);
64
- console.log(` ${severity} [${match.pattern.severity.toUpperCase()}] ${match.pattern.name}`);
65
- console.log(` Location: ${location}`);
66
- console.log(` Pattern: ${match.pattern.description || match.pattern.regex}`);
67
- console.log(` Redacted: ${match.redacted}`);
68
- console.log();
69
- }
70
- }
71
- console.log(`\n⚠️ Total: ${totalMatches} secret(s) detected in ${results.length} file(s)\n`);
72
- console.log("Run 'rafter agent audit' to see the security log.\n");
73
- process.exit(1);
76
+ sarifResults.push({
77
+ ruleId,
78
+ level: m.pattern.severity === "critical" || m.pattern.severity === "high" ? "error" : "warning",
79
+ message: { text: `${m.pattern.name} detected` },
80
+ locations: [
81
+ {
82
+ physicalLocation: {
83
+ artifactLocation: { uri: r.file.replace(/\\/g, "/"), uriBaseId: "%SRCROOT%" },
84
+ region: m.line ? { startLine: m.line, startColumn: m.column ?? 1 } : undefined,
85
+ },
86
+ },
87
+ ],
88
+ });
89
+ }
90
+ }
91
+ const sarif = {
92
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
93
+ version: "2.1.0",
94
+ runs: [
95
+ {
96
+ tool: {
97
+ driver: {
98
+ name: "rafter",
99
+ informationUri: "https://rafter.so",
100
+ rules: Array.from(rules.values()),
101
+ },
102
+ },
103
+ results: sarifResults,
104
+ },
105
+ ],
106
+ };
107
+ console.log(JSON.stringify(sarif, null, 2));
108
+ process.exit(results.length > 0 ? 1 : 0);
109
+ }
110
+ /**
111
+ * Shared output logic for scan results
112
+ */
113
+ function outputScanResults(results, opts, context) {
114
+ const format = opts.format ?? (opts.json ? "json" : "text");
115
+ if (format === "sarif") {
116
+ outputSarif(results);
117
+ return;
118
+ }
119
+ if (format === "json" || opts.json) {
120
+ const out = results.map((r) => ({
121
+ file: r.file,
122
+ matches: r.matches.map((m) => ({
123
+ pattern: { name: m.pattern.name, severity: m.pattern.severity, description: m.pattern.description || "" },
124
+ line: m.line ?? null,
125
+ column: m.column ?? null,
126
+ redacted: m.redacted || "",
127
+ })),
128
+ }));
129
+ console.log(JSON.stringify(out, null, 2));
130
+ process.exit(results.length > 0 ? 1 : 0);
131
+ }
132
+ if (results.length === 0) {
133
+ if (!opts.quiet) {
134
+ const msg = context ? `No secrets detected in ${context}` : "No secrets detected";
135
+ console.log(`\n${fmt.success(msg)}\n`);
136
+ }
137
+ process.exit(0);
138
+ }
139
+ console.log(`\n${fmt.warning(`Found secrets in ${results.length} file(s):`)}\n`);
140
+ let totalMatches = 0;
141
+ for (const result of results) {
142
+ console.log(`\n${fmt.info(result.file)}`);
143
+ for (const match of result.matches) {
144
+ totalMatches++;
145
+ const location = match.line ? `Line ${match.line}` : "Unknown location";
146
+ const sev = fmt.severity(match.pattern.severity);
147
+ console.log(` ${sev} ${match.pattern.name}`);
148
+ console.log(` Location: ${location}`);
149
+ console.log(` Pattern: ${match.pattern.description || match.pattern.regex}`);
150
+ console.log(` Redacted: ${match.redacted}`);
151
+ console.log();
152
+ }
153
+ }
154
+ console.log(`\n${fmt.warning(`Total: ${totalMatches} secret(s) detected in ${results.length} file(s)`)}\n`);
155
+ if (context === "staged files") {
156
+ console.log(`${fmt.error("Commit blocked. Remove secrets before committing.")}\n`);
157
+ }
158
+ else {
159
+ console.log(`Run 'rafter agent audit' to see the security log.\n`);
160
+ }
161
+ process.exit(1);
162
+ }
163
+ /**
164
+ * Scan files changed since a git ref
165
+ */
166
+ async function scanDiffFiles(ref, opts, scanCfg) {
167
+ try {
168
+ const diffOutput = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACM", ref], {
169
+ encoding: "utf-8",
170
+ stdio: ["pipe", "pipe", "ignore"],
171
+ }).trim();
172
+ if (!diffOutput) {
173
+ if (!opts.quiet) {
174
+ console.log(fmt.success(`No files changed since ${ref}`));
74
175
  }
176
+ process.exit(0);
75
177
  }
76
- });
178
+ const changedFiles = diffOutput.split("\n").map(f => f.trim()).filter(f => f);
179
+ if (!opts.quiet) {
180
+ console.error(`Scanning ${changedFiles.length} file(s) changed since ${ref}...`);
181
+ }
182
+ const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
183
+ const allResults = [];
184
+ for (const file of changedFiles) {
185
+ const filePath = path.resolve(file);
186
+ if (!fs.existsSync(filePath))
187
+ continue;
188
+ const stats = fs.statSync(filePath);
189
+ if (!stats.isFile())
190
+ continue;
191
+ const results = await scanFile(filePath, engine, scanCfg);
192
+ allResults.push(...results);
193
+ }
194
+ outputScanResults(allResults, opts, `files changed since ${ref}`);
195
+ }
196
+ catch (error) {
197
+ if (error.status === 128) {
198
+ console.error("Error: Not in a git repository or invalid ref");
199
+ process.exit(2);
200
+ }
201
+ throw error;
202
+ }
77
203
  }
78
204
  /**
79
205
  * Scan git staged files for secrets
80
206
  */
81
- async function scanStagedFiles(opts) {
207
+ async function scanStagedFiles(opts, scanCfg) {
82
208
  try {
83
- // Get list of staged files
84
209
  const stagedFilesOutput = execSync("git diff --cached --name-only --diff-filter=ACM", {
85
210
  encoding: "utf-8",
86
211
  stdio: ["pipe", "pipe", "ignore"]
87
212
  }).trim();
88
213
  if (!stagedFilesOutput) {
89
214
  if (!opts.quiet) {
90
- console.log("No files staged for commit");
215
+ console.log(fmt.success("No files staged for commit"));
91
216
  }
92
217
  process.exit(0);
93
218
  }
@@ -95,74 +220,28 @@ async function scanStagedFiles(opts) {
95
220
  if (!opts.quiet) {
96
221
  console.error(`Scanning ${stagedFiles.length} staged file(s)...`);
97
222
  }
98
- // Determine scan engine
99
223
  const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
100
- // Scan each staged file
101
224
  const allResults = [];
102
225
  for (const file of stagedFiles) {
103
226
  const filePath = path.resolve(file);
104
- // Skip if file doesn't exist (might be deleted)
105
- if (!fs.existsSync(filePath)) {
227
+ if (!fs.existsSync(filePath))
106
228
  continue;
107
- }
108
- // Skip if not a regular file
109
229
  const stats = fs.statSync(filePath);
110
- if (!stats.isFile()) {
230
+ if (!stats.isFile())
111
231
  continue;
112
- }
113
- const results = await scanFile(filePath, engine);
232
+ const results = await scanFile(filePath, engine, scanCfg);
114
233
  allResults.push(...results);
115
234
  }
116
- // Output results (same as regular scan)
117
- if (opts.json) {
118
- console.log(JSON.stringify(allResults, null, 2));
119
- }
120
- else {
121
- if (allResults.length === 0) {
122
- if (!opts.quiet) {
123
- console.log("\n✓ No secrets detected in staged files\n");
124
- }
125
- process.exit(0);
126
- }
127
- else {
128
- console.log(`\n⚠️ Found secrets in ${allResults.length} staged file(s):\n`);
129
- let totalMatches = 0;
130
- for (const result of allResults) {
131
- console.log(`\n📄 ${result.file}`);
132
- for (const match of result.matches) {
133
- totalMatches++;
134
- const location = match.line ? `Line ${match.line}` : "Unknown location";
135
- const severity = getSeverityEmoji(match.pattern.severity);
136
- console.log(` ${severity} [${match.pattern.severity.toUpperCase()}] ${match.pattern.name}`);
137
- console.log(` Location: ${location}`);
138
- console.log(` Pattern: ${match.pattern.description || match.pattern.regex}`);
139
- console.log(` Redacted: ${match.redacted}`);
140
- console.log();
141
- }
142
- }
143
- console.log(`\n⚠️ Total: ${totalMatches} secret(s) detected in ${allResults.length} file(s)\n`);
144
- console.log("❌ Commit blocked. Remove secrets before committing.\n");
145
- process.exit(1);
146
- }
147
- }
235
+ outputScanResults(allResults, opts, "staged files");
148
236
  }
149
237
  catch (error) {
150
238
  if (error.status === 128) {
151
239
  console.error("Error: Not in a git repository");
152
- process.exit(1);
240
+ process.exit(2);
153
241
  }
154
242
  throw error;
155
243
  }
156
244
  }
157
- function getSeverityEmoji(severity) {
158
- const emojiMap = {
159
- critical: "🔴",
160
- high: "🟠",
161
- medium: "🟡",
162
- low: "🟢"
163
- };
164
- return emojiMap[severity] || "⚪";
165
- }
166
245
  /**
167
246
  * Select scan engine based on availability and user preference
168
247
  */
@@ -175,7 +254,7 @@ async function selectEngine(preference, quiet) {
175
254
  const available = await gitleaks.isAvailable();
176
255
  if (!available) {
177
256
  if (!quiet) {
178
- console.error("⚠️ Gitleaks requested but not available, using patterns");
257
+ console.error(fmt.warning("Gitleaks requested but not available, using patterns"));
179
258
  }
180
259
  return "patterns";
181
260
  }
@@ -189,7 +268,7 @@ async function selectEngine(preference, quiet) {
189
268
  /**
190
269
  * Scan a file with selected engine
191
270
  */
192
- async function scanFile(filePath, engine) {
271
+ async function scanFile(filePath, engine, scanCfg) {
193
272
  if (engine === "gitleaks") {
194
273
  try {
195
274
  const gitleaks = new GitleaksScanner();
@@ -197,15 +276,14 @@ async function scanFile(filePath, engine) {
197
276
  return result.matches.length > 0 ? [result] : [];
198
277
  }
199
278
  catch (e) {
200
- // Fall back to patterns on error
201
- console.error(`⚠️ Gitleaks scan failed, falling back to patterns`);
202
- const scanner = new RegexScanner();
279
+ console.error(fmt.warning("Gitleaks scan failed, falling back to patterns"));
280
+ const scanner = new RegexScanner(scanCfg?.customPatterns);
203
281
  const result = scanner.scanFile(filePath);
204
282
  return result.matches.length > 0 ? [result] : [];
205
283
  }
206
284
  }
207
285
  else {
208
- const scanner = new RegexScanner();
286
+ const scanner = new RegexScanner(scanCfg?.customPatterns);
209
287
  const result = scanner.scanFile(filePath);
210
288
  return result.matches.length > 0 ? [result] : [];
211
289
  }
@@ -213,21 +291,20 @@ async function scanFile(filePath, engine) {
213
291
  /**
214
292
  * Scan a directory with selected engine
215
293
  */
216
- async function scanDirectory(dirPath, engine) {
294
+ async function scanDirectory(dirPath, engine, scanCfg) {
217
295
  if (engine === "gitleaks") {
218
296
  try {
219
297
  const gitleaks = new GitleaksScanner();
220
298
  return await gitleaks.scanDirectory(dirPath);
221
299
  }
222
300
  catch (e) {
223
- // Fall back to patterns on error
224
- console.error(`⚠️ Gitleaks scan failed, falling back to patterns`);
225
- const scanner = new RegexScanner();
226
- return scanner.scanDirectory(dirPath);
301
+ console.error(fmt.warning("Gitleaks scan failed, falling back to patterns"));
302
+ const scanner = new RegexScanner(scanCfg?.customPatterns);
303
+ return scanner.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
227
304
  }
228
305
  }
229
306
  else {
230
- const scanner = new RegexScanner();
231
- return scanner.scanDirectory(dirPath);
307
+ const scanner = new RegexScanner(scanCfg?.customPatterns);
308
+ return scanner.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
232
309
  }
233
310
  }
@@ -0,0 +1,115 @@
1
+ import { Command } from "commander";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+ import { execSync } from "child_process";
6
+ import { getRafterDir, getAuditLogPath, getBinDir } from "../../core/config-defaults.js";
7
+ import { AuditLogger } from "../../core/audit-logger.js";
8
+ import { ConfigManager } from "../../core/config-manager.js";
9
+ export function createStatusCommand() {
10
+ return new Command("status")
11
+ .description("Show agent security status dashboard")
12
+ .action(async () => {
13
+ const rafterDir = getRafterDir();
14
+ const auditPath = getAuditLogPath();
15
+ const home = os.homedir();
16
+ console.log("Rafter Agent Status");
17
+ console.log("=".repeat(50));
18
+ // --- Config ---
19
+ const configPath = path.join(rafterDir, "config.json");
20
+ if (fs.existsSync(configPath)) {
21
+ try {
22
+ const cfg = new ConfigManager().load();
23
+ console.log(`\nConfig: ${configPath}`);
24
+ console.log(`Risk level: ${cfg.agent?.riskLevel ?? "moderate"}`);
25
+ }
26
+ catch {
27
+ console.log(`\nConfig: ${configPath} (parse error)`);
28
+ }
29
+ }
30
+ else {
31
+ console.log(`\nConfig: not found — run: rafter agent init`);
32
+ }
33
+ // --- Gitleaks ---
34
+ const localGitleaks = path.join(getBinDir(), "gitleaks");
35
+ let gitleaksStatus = "not found — run: rafter agent init";
36
+ try {
37
+ const ver = execSync("gitleaks version", { timeout: 5000, encoding: "utf-8" }).trim();
38
+ gitleaksStatus = `${ver} (PATH)`;
39
+ }
40
+ catch {
41
+ if (fs.existsSync(localGitleaks)) {
42
+ try {
43
+ const ver = execSync(`"${localGitleaks}" version`, { timeout: 5000, encoding: "utf-8" }).trim();
44
+ gitleaksStatus = `${ver} (local)`;
45
+ }
46
+ catch {
47
+ gitleaksStatus = `${localGitleaks} (binary error)`;
48
+ }
49
+ }
50
+ }
51
+ console.log(`Gitleaks: ${gitleaksStatus}`);
52
+ // --- Claude Code hooks ---
53
+ const settingsPath = path.join(home, ".claude", "settings.json");
54
+ let pretoolOk = false;
55
+ let posttoolOk = false;
56
+ if (fs.existsSync(settingsPath)) {
57
+ try {
58
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
59
+ const hooks = settings.hooks ?? {};
60
+ for (const entry of hooks.PreToolUse ?? []) {
61
+ for (const h of entry.hooks ?? []) {
62
+ if (String(h.command ?? "").includes("rafter hook pretool"))
63
+ pretoolOk = true;
64
+ }
65
+ }
66
+ for (const entry of hooks.PostToolUse ?? []) {
67
+ for (const h of entry.hooks ?? []) {
68
+ if (String(h.command ?? "").includes("rafter hook posttool"))
69
+ posttoolOk = true;
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ // unreadable settings
75
+ }
76
+ }
77
+ console.log(`PreToolUse: ${pretoolOk ? "installed" : "not installed — run: rafter agent init"}`);
78
+ console.log(`PostToolUse: ${posttoolOk ? "installed" : "not installed — run: rafter agent init"}`);
79
+ // --- OpenClaw skill ---
80
+ const skillPath = path.join(home, ".openclaw", "skills", "rafter-security.md");
81
+ const openclawDir = path.join(home, ".openclaw");
82
+ if (fs.existsSync(skillPath)) {
83
+ console.log(`OpenClaw: skill installed (${skillPath})`);
84
+ }
85
+ else if (fs.existsSync(openclawDir)) {
86
+ console.log("OpenClaw: detected but skill missing — run: rafter agent init");
87
+ }
88
+ else {
89
+ console.log("OpenClaw: not detected (optional)");
90
+ }
91
+ // --- Audit log summary ---
92
+ console.log(`\nAudit log: ${auditPath}`);
93
+ if (fs.existsSync(auditPath)) {
94
+ const logger = new AuditLogger();
95
+ const allEntries = logger.read();
96
+ const total = allEntries.length;
97
+ const secrets = allEntries.filter((e) => e.eventType === "secret_detected").length;
98
+ const blocked = allEntries.filter((e) => e.eventType === "command_intercepted" && e.resolution?.actionTaken === "blocked").length;
99
+ console.log(`Total events: ${total} | Secrets detected: ${secrets} | Commands blocked: ${blocked}`);
100
+ const recent = logger.read({ limit: 5 });
101
+ if (recent.length > 0) {
102
+ console.log("\nRecent events:");
103
+ for (const e of [...recent].reverse()) {
104
+ const ts = (e.timestamp ?? "").slice(0, 19).replace("T", " ");
105
+ const action = e.resolution?.actionTaken ?? "";
106
+ console.log(` ${ts} ${e.eventType} [${action}]`);
107
+ }
108
+ }
109
+ }
110
+ else {
111
+ console.log("No events logged yet.");
112
+ }
113
+ console.log();
114
+ });
115
+ }
@@ -0,0 +1,117 @@
1
+ import { Command } from "commander";
2
+ import { BinaryManager } from "../../utils/binary-manager.js";
3
+ import { SkillManager } from "../../utils/skill-manager.js";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import os from "os";
7
+ import { fmt } from "../../utils/formatter.js";
8
+ async function checkGitleaks() {
9
+ const binaryManager = new BinaryManager();
10
+ const name = "Gitleaks";
11
+ // Check PATH first (e.g. Homebrew), then fall back to ~/.rafter/bin
12
+ const pathBinary = binaryManager.findGitleaksOnPath();
13
+ const hasBinary = pathBinary !== null || binaryManager.isGitleaksInstalled();
14
+ if (!hasBinary) {
15
+ return { name, passed: false, detail: `Not found on PATH or at ${binaryManager.getGitleaksPath()}` };
16
+ }
17
+ const binaryPath = pathBinary ?? binaryManager.getGitleaksPath();
18
+ const { ok, stdout, stderr } = await binaryManager.verifyGitleaksVerbose(binaryPath);
19
+ if (!ok) {
20
+ const diag = await binaryManager.collectBinaryDiagnostics(binaryPath);
21
+ return { name, passed: false, detail: `Binary found at ${binaryPath} but failed to execute\n${stdout ? ` stdout: ${stdout}\n` : ""}${stderr ? ` stderr: ${stderr}\n` : ""}${diag}` };
22
+ }
23
+ return { name, passed: true, detail: `${stdout} (${binaryPath})` };
24
+ }
25
+ function checkConfig() {
26
+ const name = "Config";
27
+ const configPath = path.join(os.homedir(), ".rafter", "config.json");
28
+ if (!fs.existsSync(configPath)) {
29
+ return { name, passed: false, detail: `Not found: ${configPath}` };
30
+ }
31
+ try {
32
+ const content = fs.readFileSync(configPath, "utf-8");
33
+ JSON.parse(content);
34
+ return { name, passed: true, detail: configPath };
35
+ }
36
+ catch (e) {
37
+ return { name, passed: false, detail: `Invalid JSON: ${configPath} — ${e}` };
38
+ }
39
+ }
40
+ function checkClaudeCode() {
41
+ const name = "Claude Code";
42
+ const homeDir = os.homedir();
43
+ // optional: warn if absent but don't fail exit code
44
+ const claudeDir = path.join(homeDir, ".claude");
45
+ if (!fs.existsSync(claudeDir)) {
46
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --claude-code' to enable` };
47
+ }
48
+ const settingsPath = path.join(claudeDir, "settings.json");
49
+ if (!fs.existsSync(settingsPath)) {
50
+ return { name, passed: false, optional: true, detail: `Settings file not found: ${settingsPath}` };
51
+ }
52
+ try {
53
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
54
+ const hooks = settings?.hooks?.PreToolUse || [];
55
+ const hasRafterHook = hooks.some((entry) => (entry.hooks || []).some((h) => h.command === "rafter hook pretool"));
56
+ if (!hasRafterHook) {
57
+ return { name, passed: false, optional: true, detail: "Rafter hooks not installed — run 'rafter agent init --claude-code'" };
58
+ }
59
+ return { name, passed: true, detail: "Hooks installed" };
60
+ }
61
+ catch (e) {
62
+ return { name, passed: false, optional: true, detail: `Cannot read settings: ${e}` };
63
+ }
64
+ }
65
+ function checkOpenClaw() {
66
+ const name = "OpenClaw";
67
+ const skillManager = new SkillManager();
68
+ if (!skillManager.isOpenClawInstalled()) {
69
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init' to enable` };
70
+ }
71
+ if (!skillManager.isRafterSkillInstalled()) {
72
+ return { name, passed: false, optional: true, detail: `Rafter skill not installed — run 'rafter agent init'` };
73
+ }
74
+ const version = skillManager.getInstalledVersion();
75
+ return { name, passed: true, detail: `Rafter skill installed${version ? ` (v${version})` : ""}` };
76
+ }
77
+ export function createVerifyCommand() {
78
+ return new Command("verify")
79
+ .description("Check agent security integration status")
80
+ .action(async () => {
81
+ console.log(fmt.header("Rafter Agent Verify"));
82
+ console.log(fmt.divider());
83
+ console.log();
84
+ const results = [
85
+ checkConfig(),
86
+ await checkGitleaks(),
87
+ checkClaudeCode(),
88
+ checkOpenClaw(),
89
+ ];
90
+ for (const r of results) {
91
+ if (r.passed) {
92
+ console.log(fmt.success(`${r.name}: ${r.detail}`));
93
+ }
94
+ else if (r.optional) {
95
+ console.log(fmt.warning(`${r.name}: ${r.detail}`));
96
+ }
97
+ else {
98
+ console.log(fmt.error(`${r.name}: FAIL — ${r.detail}`));
99
+ }
100
+ }
101
+ console.log();
102
+ const hardFailed = results.filter((r) => !r.passed && !r.optional);
103
+ const warned = results.filter((r) => !r.passed && r.optional);
104
+ const passed = results.filter((r) => r.passed);
105
+ if (hardFailed.length === 0) {
106
+ const warnNote = warned.length > 0 ? ` (${warned.length} optional check${warned.length > 1 ? "s" : ""} not configured)` : "";
107
+ console.log(fmt.success(`${passed.length}/${results.length} core checks passed${warnNote}`));
108
+ }
109
+ else {
110
+ console.log(fmt.error(`${hardFailed.length} check${hardFailed.length > 1 ? "s" : ""} failed`));
111
+ }
112
+ console.log();
113
+ if (hardFailed.length > 0) {
114
+ process.exit(1);
115
+ }
116
+ });
117
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from "commander";
2
+ import { createCiInitCommand } from "./init.js";
3
+ export function createCiCommand() {
4
+ const ci = new Command("ci")
5
+ .description("CI/CD integration commands");
6
+ ci.addCommand(createCiInitCommand());
7
+ return ci;
8
+ }