@rafter-security/cli 0.6.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ yarn global add @rafter-security/cli
23
23
 
24
24
  ### Getting an API Key
25
25
 
26
- To use backend code analysis features, you'll need a Rafter API key:
26
+ To use remote code analysis features, you'll need a Rafter API key:
27
27
 
28
28
  1. **Sign up**: Create an account at [rafter.so](https://rafter.so)
29
29
  2. **Get API key**: Navigate to Dashboard → Settings → API Keys
@@ -36,9 +36,9 @@ To use backend code analysis features, you'll need a Rafter API key:
36
36
  echo "RAFTER_API_KEY=your-api-key-here" >> .env
37
37
  ```
38
38
 
39
- **Note**: Agent security features (secret scanning, command execution) work **without an API key**. Only backend code analysis requires authentication.
39
+ **Note**: Agent security features (secret scanning, command execution) work **without an API key**. Only remote code analysis requires authentication.
40
40
 
41
- ### Backend Code Analysis
41
+ ### Remote Code Analysis
42
42
 
43
43
  ```bash
44
44
  # Set your API key (from above)
@@ -500,7 +500,7 @@ Generate CI/CD pipeline configuration for secret scanning.
500
500
  **Options:**
501
501
  - `--platform <platform>` - CI platform: `github`, `gitlab`, `circleci` (default: auto-detect)
502
502
  - `--output <path>` - Output file path (default: platform-specific)
503
- - `--with-backend` - Include backend security audit job (requires `RAFTER_API_KEY`)
503
+ - `--with-remote` - Include remote security audit job (requires `RAFTER_API_KEY`)
504
504
 
505
505
  **Auto-detection:** Checks for `.github/`, `.gitlab-ci.yml`, `.circleci/` in the current directory.
506
506
 
@@ -512,8 +512,8 @@ rafter ci init
512
512
  # Generate GitHub Actions workflow
513
513
  rafter ci init --platform github
514
514
 
515
- # Include backend scanning job
516
- rafter ci init --with-backend
515
+ # Include remote scanning job
516
+ rafter ci init --with-remote
517
517
 
518
518
  # Custom output path
519
519
  rafter ci init --output .github/workflows/security.yml
@@ -624,7 +624,7 @@ When OpenClaw is detected, `rafter agent init` automatically installs a skill to
624
624
 
625
625
  Rafter provides TWO skills for Claude Code:
626
626
 
627
- ### 1. Backend Code Analysis Skill (Core Feature)
627
+ ### 1. Remote Code Analysis Skill (Core Feature)
628
628
 
629
629
  **Automatic Integration** - Claude can proactively suggest security scans
630
630
 
@@ -680,10 +680,10 @@ Explicitly invoke commands:
680
680
 
681
681
  ### Why Two Skills?
682
682
 
683
- - **Backend code analysis skill** - Safe for Claude to auto-invoke (read-only API calls)
683
+ - **Remote code analysis skill** - Safe for Claude to auto-invoke (read-only API calls)
684
684
  - **Agent security skill** - Requires user permission (local file access, command execution)
685
685
 
686
- This separation emphasizes Rafter's core backend code analysis capabilities while keeping local security features safely behind user control.
686
+ This separation emphasizes Rafter's core remote code analysis capabilities while keeping local security features safely behind user control.
687
687
 
688
688
  ## Documentation
689
689
 
@@ -4,6 +4,7 @@ import path from "path";
4
4
  import { PatternEngine } from "../../core/pattern-engine.js";
5
5
  import { DEFAULT_SECRET_PATTERNS } from "../../scanners/secret-patterns.js";
6
6
  import { SkillManager } from "../../utils/skill-manager.js";
7
+ import { fmt } from "../../utils/formatter.js";
7
8
  export function createAuditSkillCommand() {
8
9
  return new Command("audit-skill")
9
10
  .description("Security audit of a Claude Code skill file")
@@ -17,7 +18,7 @@ export function createAuditSkillCommand() {
17
18
  async function auditSkill(skillPath, opts) {
18
19
  // Validate skill file exists
19
20
  if (!fs.existsSync(skillPath)) {
20
- console.error(`Error: Skill file not found: ${skillPath}`);
21
+ console.error(fmt.error(`Skill file not found: ${skillPath}`));
21
22
  process.exit(2);
22
23
  }
23
24
  const absolutePath = path.resolve(skillPath);
@@ -25,8 +26,8 @@ async function auditSkill(skillPath, opts) {
25
26
  const skillName = path.basename(absolutePath);
26
27
  // Run deterministic analysis
27
28
  if (!opts.json) {
28
- console.log(`\n🔍 Auditing skill: ${skillName}\n`);
29
- console.log("═".repeat(60));
29
+ console.log(fmt.header(`Auditing skill: ${skillName}`));
30
+ console.log(fmt.divider());
30
31
  console.log("Running quick security scan...\n");
31
32
  }
32
33
  const quickScan = await runQuickScan(skillContent);
@@ -56,11 +57,11 @@ async function auditSkill(skillPath, opts) {
56
57
  // Check if we can use OpenClaw
57
58
  if (openClawAvailable && !opts.skipOpenclaw) {
58
59
  if (!rafterSkillInstalled) {
59
- console.log("\n⚠️ Rafter Security skill not installed in OpenClaw.");
60
+ console.log(`\n${fmt.warning("Rafter Security skill not installed in OpenClaw.")}`);
60
61
  console.log(" Run: rafter agent init\n");
61
62
  }
62
63
  else {
63
- console.log("\n🤖 For comprehensive security review:\n");
64
+ console.log(`\n${fmt.info("For comprehensive security review:")}\n`);
64
65
  console.log(" 1. Open OpenClaw");
65
66
  console.log(` 2. Run: /rafter-audit-skill ${absolutePath}`);
66
67
  console.log("\n The auditor will analyze:");
@@ -80,12 +81,12 @@ async function auditSkill(skillPath, opts) {
80
81
  }
81
82
  else {
82
83
  // OpenClaw not available or skipped - show manual review prompt
83
- console.log("\n📋 Manual Security Review Prompt\n");
84
- console.log("═".repeat(60));
84
+ console.log(fmt.header("Manual Security Review Prompt"));
85
+ console.log(fmt.divider());
85
86
  console.log("\nCopy the following to your AI assistant for review:\n");
86
- console.log("─".repeat(60));
87
+ console.log(fmt.divider());
87
88
  console.log(generateManualReviewPrompt(skillName, absolutePath, quickScan, skillContent));
88
- console.log("─".repeat(60));
89
+ console.log(fmt.divider());
89
90
  }
90
91
  console.log();
91
92
  if (quickScan.secrets > 0 || quickScan.highRiskCommands.length > 0) {
@@ -134,25 +135,25 @@ async function runQuickScan(content) {
134
135
  };
135
136
  }
136
137
  function displayQuickScan(scan, skillName) {
137
- console.log("📊 Quick Scan Results");
138
- console.log("═".repeat(60));
138
+ console.log(fmt.header("Quick Scan Results"));
139
+ console.log(fmt.divider());
139
140
  // Secrets
140
141
  if (scan.secrets === 0) {
141
- console.log("Secrets: None detected");
142
+ console.log(fmt.success("Secrets: None detected"));
142
143
  }
143
144
  else {
144
- console.log(`⚠️ Secrets: ${scan.secrets} found`);
145
+ console.log(fmt.warning(`Secrets: ${scan.secrets} found`));
145
146
  console.log(" → API keys, tokens, or credentials detected");
146
147
  console.log(" → Run: rafter scan local <path> for details");
147
148
  }
148
149
  // URLs
149
150
  if (scan.urls.length === 0) {
150
- console.log("External URLs: None");
151
+ console.log(fmt.success("External URLs: None"));
151
152
  }
152
153
  else {
153
- console.log(`⚠️ External URLs: ${scan.urls.length} found`);
154
+ console.log(fmt.warning(`External URLs: ${scan.urls.length} found`));
154
155
  scan.urls.slice(0, 5).forEach(url => {
155
- console.log(` ${url}`);
156
+ console.log(` - ${url}`);
156
157
  });
157
158
  if (scan.urls.length > 5) {
158
159
  console.log(` ... and ${scan.urls.length - 5} more`);
@@ -160,12 +161,12 @@ function displayQuickScan(scan, skillName) {
160
161
  }
161
162
  // High-risk commands
162
163
  if (scan.highRiskCommands.length === 0) {
163
- console.log("High-risk commands: None detected");
164
+ console.log(fmt.success("High-risk commands: None detected"));
164
165
  }
165
166
  else {
166
- console.log(`⚠️ High-risk commands: ${scan.highRiskCommands.length} found`);
167
+ console.log(fmt.warning(`High-risk commands: ${scan.highRiskCommands.length} found`));
167
168
  scan.highRiskCommands.slice(0, 5).forEach(cmd => {
168
- console.log(` ${cmd.command} (line ${cmd.line})`);
169
+ console.log(` - ${cmd.command} (line ${cmd.line})`);
169
170
  });
170
171
  if (scan.highRiskCommands.length > 5) {
171
172
  console.log(` ... and ${scan.highRiskCommands.length - 5} more`);
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { ConfigManager } from "../../core/config-manager.js";
3
+ import { fmt } from "../../utils/formatter.js";
3
4
  export function createConfigCommand() {
4
5
  const config = new Command("config")
5
6
  .description("Manage agent configuration");
@@ -48,7 +49,7 @@ export function createConfigCommand() {
48
49
  // Use as string
49
50
  }
50
51
  manager.set(key, parsedValue);
51
- console.log(`✓ Set ${key} = ${JSON.stringify(parsedValue)}`);
52
+ console.log(fmt.success(`Set ${key} = ${JSON.stringify(parsedValue)}`));
52
53
  });
53
54
  return config;
54
55
  }
@@ -112,6 +112,8 @@ async function promptApproval() {
112
112
  output: process.stdout
113
113
  });
114
114
  return new Promise((resolve) => {
115
+ // Handle EOF / non-interactive stdin (e.g. piped or closed stdin)
116
+ rl.on("close", () => resolve(false));
115
117
  rl.question("Approve this command? (yes/no): ", (answer) => {
116
118
  rl.close();
117
119
  const normalized = answer.trim().toLowerCase();
@@ -419,10 +419,10 @@ async function installClaudeCodeSkills() {
419
419
  }
420
420
  if (fs.existsSync(backendTemplatePath)) {
421
421
  fs.copyFileSync(backendTemplatePath, backendSkillPath);
422
- console.log(fmt.success(`Installed Rafter Backend skill to ${backendSkillPath}`));
422
+ console.log(fmt.success(`Installed Rafter Remote skill to ${backendSkillPath}`));
423
423
  }
424
424
  else {
425
- console.log(fmt.warning(`Backend skill template not found at ${backendTemplatePath}`));
425
+ console.log(fmt.warning(`Remote skill template not found at ${backendTemplatePath}`));
426
426
  }
427
427
  // Install Agent Security Skill
428
428
  const agentSkillDir = path.join(claudeSkillsDir, "rafter-agent-security");
@@ -451,10 +451,10 @@ function installCodexSkills() {
451
451
  }
452
452
  if (fs.existsSync(backendTemplatePath)) {
453
453
  fs.copyFileSync(backendTemplatePath, backendSkillPath);
454
- console.log(fmt.success(`Installed Rafter Backend skill to ${backendSkillPath}`));
454
+ console.log(fmt.success(`Installed Rafter Remote skill to ${backendSkillPath}`));
455
455
  }
456
456
  else {
457
- console.log(fmt.warning(`Backend skill template not found at ${backendTemplatePath}`));
457
+ console.log(fmt.warning(`Remote skill template not found at ${backendTemplatePath}`));
458
458
  }
459
459
  // Install Agent Security Skill
460
460
  const agentDir = path.join(agentsSkillsDir, "rafter-agent-security");
@@ -698,8 +698,9 @@ export function createInitCommand() {
698
698
  }
699
699
  }
700
700
  // Install Claude Code skills + hooks if opted in
701
+ // When --with-claude-code is explicitly passed, install even if .claude doesn't exist yet
701
702
  let claudeCodeOk = false;
702
- if (hasClaudeCode && wantClaudeCode) {
703
+ if ((hasClaudeCode || opts.withClaudeCode) && wantClaudeCode) {
703
704
  try {
704
705
  await installClaudeCodeSkills();
705
706
  installClaudeCodeHooks();
@@ -4,6 +4,7 @@ import os from "os";
4
4
  import path from "path";
5
5
  import { execSync } from "child_process";
6
6
  import { fileURLToPath } from 'url';
7
+ import { fmt } from "../../utils/formatter.js";
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
9
10
  export function createInstallHookCommand() {
@@ -28,7 +29,7 @@ async function installHook(opts) {
28
29
  function getTemplatePath(templateName) {
29
30
  const templatePath = path.join(__dirname, "..", "..", "..", "resources", templateName);
30
31
  if (!fs.existsSync(templatePath)) {
31
- console.error("❌ Error: Hook template not found");
32
+ console.error(fmt.error("Hook template not found"));
32
33
  console.error(` Expected at: ${templatePath}`);
33
34
  process.exit(1);
34
35
  }
@@ -42,7 +43,7 @@ async function installLocalHook(hookName, templateName) {
42
43
  execSync("git rev-parse --git-dir", { stdio: "pipe" });
43
44
  }
44
45
  catch (e) {
45
- console.error("❌ Error: Not in a git repository");
46
+ console.error(fmt.error("Not in a git repository"));
46
47
  console.error(" Run this command from inside a git repository");
47
48
  process.exit(1);
48
49
  }
@@ -56,30 +57,30 @@ async function installLocalHook(hookName, templateName) {
56
57
  const existing = fs.readFileSync(hookPath, "utf-8");
57
58
  const marker = hookName === "pre-push" ? "Rafter Security Pre-Push Hook" : "Rafter Security Pre-Commit Hook";
58
59
  if (existing.includes(marker)) {
59
- console.log(`✓ Rafter ${hookName} hook already installed`);
60
+ console.log(fmt.success(`Rafter ${hookName} hook already installed`));
60
61
  return;
61
62
  }
62
63
  const backupPath = `${hookPath}.backup-${Date.now()}`;
63
64
  fs.copyFileSync(hookPath, backupPath);
64
- console.log(`📦 Backed up existing hook to: ${path.basename(backupPath)}`);
65
+ console.log(fmt.info(`Backed up existing hook to: ${path.basename(backupPath)}`));
65
66
  }
66
67
  const hookContent = fs.readFileSync(getTemplatePath(templateName), "utf-8");
67
68
  fs.writeFileSync(hookPath, hookContent, "utf-8");
68
69
  fs.chmodSync(hookPath, 0o755);
69
- console.log(`✓ Installed Rafter ${hookName} hook`);
70
+ console.log(fmt.success(`Installed Rafter ${hookName} hook`));
70
71
  console.log(` Location: ${hookPath}`);
71
72
  console.log();
72
73
  if (hookName === "pre-push") {
73
74
  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)");
75
+ console.log(" - Scan commits being pushed for secrets");
76
+ console.log(" - Block pushes if secrets are detected");
77
+ console.log(" - Can be bypassed with: git push --no-verify (not recommended)");
77
78
  }
78
79
  else {
79
80
  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)");
81
+ console.log(" - Scan staged files for secrets before each commit");
82
+ console.log(" - Block commits if secrets are detected");
83
+ console.log(" - Can be bypassed with: git commit --no-verify (not recommended)");
83
84
  }
84
85
  console.log();
85
86
  }
@@ -89,7 +90,7 @@ async function installLocalHook(hookName, templateName) {
89
90
  async function installGlobalHook(hookName, templateName) {
90
91
  const homeDir = os.homedir();
91
92
  if (!homeDir) {
92
- console.error("❌ Error: Could not determine home directory");
93
+ console.error(fmt.error("Could not determine home directory"));
93
94
  process.exit(1);
94
95
  }
95
96
  const globalHooksDir = path.join(homeDir, ".rafter", "git-hooks");
@@ -102,7 +103,7 @@ async function installGlobalHook(hookName, templateName) {
102
103
  fs.chmodSync(hookPath, 0o755);
103
104
  try {
104
105
  execSync(`git config --global core.hooksPath "${globalHooksDir}"`, { stdio: "pipe" });
105
- console.log(`✓ Installed Rafter ${hookName} hook globally`);
106
+ console.log(fmt.success(`Installed Rafter ${hookName} hook globally`));
106
107
  console.log(` Location: ${hookPath}`);
107
108
  console.log(` Git config: core.hooksPath = ${globalHooksDir}`);
108
109
  console.log();
@@ -116,7 +117,7 @@ async function installGlobalHook(hookName, templateName) {
116
117
  console.log();
117
118
  }
118
119
  catch (e) {
119
- console.error("Failed to configure global git hooks");
120
+ console.error(fmt.error("Failed to configure global git hooks"));
120
121
  console.error(" You may need to manually set: git config --global core.hooksPath ~/.rafter/git-hooks");
121
122
  process.exit(1);
122
123
  }
@@ -47,6 +47,7 @@ export function createScanCommand() {
47
47
  .option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
48
48
  .option("--baseline", "Filter findings present in the saved baseline")
49
49
  .option("--watch", "Watch for file changes and re-scan on change")
50
+ .option("--history", "Scan git history for secrets (requires gitleaks engine)")
50
51
  .action(async (scanPath, opts) => {
51
52
  // Validate flags before doing any work
52
53
  const validEngines = ["auto", "gitleaks", "patterns"];
@@ -103,7 +104,7 @@ export function createScanCommand() {
103
104
  if (!opts.quiet) {
104
105
  console.error(`Scanning directory: ${resolvedPath} (${engine})`);
105
106
  }
106
- results = await scanDirectory(resolvedPath, engine, scanCfg);
107
+ results = await scanDirectory(resolvedPath, engine, scanCfg, opts.history);
107
108
  }
108
109
  else {
109
110
  if (!opts.quiet) {
@@ -372,11 +373,11 @@ async function scanFile(filePath, engine, scanCfg) {
372
373
  /**
373
374
  * Scan a directory with selected engine
374
375
  */
375
- async function scanDirectory(dirPath, engine, scanCfg) {
376
+ async function scanDirectory(dirPath, engine, scanCfg, history) {
376
377
  if (engine === "gitleaks") {
377
378
  try {
378
379
  const gitleaks = new GitleaksScanner();
379
- return await gitleaks.scanDirectory(dirPath);
380
+ return await gitleaks.scanDirectory(dirPath, { useGit: history ?? false });
380
381
  }
381
382
  catch (e) {
382
383
  console.error(fmt.warning("Gitleaks scan failed, falling back to patterns"));
@@ -193,7 +193,7 @@ export function createVerifyCommand() {
193
193
  console.log(fmt.success(`${passed.length}/${results.length} core checks passed${warnNote}`));
194
194
  }
195
195
  else {
196
- console.log(fmt.error(`${hardFailed.length} check${hardFailed.length > 1 ? "s" : ""} failed`));
196
+ console.log(fmt.error(`${passed.length}/${results.length} checks passed ${hardFailed.length} failed`));
197
197
  }
198
198
  console.log();
199
199
  if (hardFailed.length > 0) {
@@ -9,6 +9,7 @@ import { handleScanStatus } from "./scan-status.js";
9
9
  */
10
10
  export async function runRemoteScan(opts) {
11
11
  const key = resolveKey(opts.apiKey);
12
+ const ghToken = opts.githubToken || process.env.RAFTER_GITHUB_TOKEN;
12
13
  let repo, branch;
13
14
  try {
14
15
  ({ repo, branch } = detectRepo({ repo: opts.repo, branch: opts.branch, quiet: opts.quiet }));
@@ -22,10 +23,17 @@ export async function runRemoteScan(opts) {
22
23
  }
23
24
  process.exit(EXIT_GENERAL_ERROR);
24
25
  }
26
+ const body = {
27
+ repository_name: repo,
28
+ branch_name: branch,
29
+ scan_mode: opts.mode ?? "fast",
30
+ };
31
+ if (ghToken)
32
+ body.github_token = ghToken;
25
33
  if (!opts.quiet) {
26
34
  const spinner = ora("Submitting scan").start();
27
35
  try {
28
- const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch, scan_mode: opts.mode ?? "fast" }, { headers: { "x-api-key": key } });
36
+ const { data } = await axios.post(`${API}/static/scan`, body, { headers: { "x-api-key": key } });
29
37
  spinner.succeed(`Scan ID: ${data.scan_id}`);
30
38
  if (opts.skipInteractive)
31
39
  return;
@@ -56,7 +64,7 @@ export async function runRemoteScan(opts) {
56
64
  }
57
65
  else {
58
66
  try {
59
- const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch, scan_mode: opts.mode ?? "fast" }, { headers: { "x-api-key": key } });
67
+ const { data } = await axios.post(`${API}/static/scan`, body, { headers: { "x-api-key": key } });
60
68
  if (opts.skipInteractive)
61
69
  return;
62
70
  const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format ?? "md", opts.quiet);
@@ -90,12 +98,13 @@ function addRunOptions(cmd) {
90
98
  .option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
91
99
  .option("-f, --format <format>", "json | md", "md")
92
100
  .option("-m, --mode <mode>", "scan mode: fast | plus", "fast")
101
+ .option("--github-token <token>", "GitHub PAT for private repos (or RAFTER_GITHUB_TOKEN env var)")
93
102
  .option("--skip-interactive", "do not wait for scan to complete")
94
103
  .option("--quiet", "suppress status messages");
95
104
  }
96
105
  export function createRunCommand() {
97
106
  return addRunOptions(new Command("run")
98
- .description("Trigger a remote backend security scan")).action(async (opts) => {
107
+ .description("Trigger a remote security scan")).action(async (opts) => {
99
108
  await runRemoteScan(opts);
100
109
  });
101
110
  }
@@ -1,6 +1,7 @@
1
1
  import axios from "axios";
2
2
  import ora from "ora";
3
3
  import { API, writePayload, EXIT_GENERAL_ERROR, EXIT_SCAN_NOT_FOUND } from "../../utils/api.js";
4
+ import { fmt as output } from "../../utils/formatter.js";
4
5
  export async function handleScanStatus(scan_id, headers, fmt, quiet) {
5
6
  // First poll
6
7
  let poll;
@@ -9,10 +10,10 @@ export async function handleScanStatus(scan_id, headers, fmt, quiet) {
9
10
  }
10
11
  catch (e) {
11
12
  if (e.response?.status === 404) {
12
- console.error(`Scan '${scan_id}' not found`);
13
+ console.error(output.error(`Scan '${scan_id}' not found`));
13
14
  return EXIT_SCAN_NOT_FOUND;
14
15
  }
15
- console.error(`Error: ${e.response?.data || e.message}`);
16
+ console.error(output.error(`${e.response?.data || e.message}`));
16
17
  return EXIT_GENERAL_ERROR;
17
18
  }
18
19
  let status = poll.data.status;
@@ -54,7 +54,7 @@ function buildTopics() {
54
54
  render: () => loadSkill("rafter-agent-security"),
55
55
  },
56
56
  scanning: {
57
- description: "Remote SAST/SCA code analysis via backend API",
57
+ description: "Remote SAST/SCA code analysis via Rafter API",
58
58
  render: () => loadSkill("rafter"),
59
59
  },
60
60
  commands: {
@@ -78,7 +78,7 @@ function buildTopics() {
78
78
  return [
79
79
  "# Rafter Command Reference",
80
80
  "",
81
- "## Backend (Remote Code Analysis)",
81
+ "## Remote Code Analysis",
82
82
  "",
83
83
  backCmds,
84
84
  "",
@@ -1,13 +1,14 @@
1
1
  import { Command } from "commander";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
- import { fmt } from "../../utils/formatter.js";
4
+ import { fmt, isAgentMode } from "../../utils/formatter.js";
5
5
  export function createCiInitCommand() {
6
6
  return new Command("init")
7
7
  .description("Generate CI/CD pipeline config for secret scanning")
8
8
  .option("--platform <platform>", "CI platform: github, gitlab, circleci (default: auto-detect)")
9
9
  .option("--output <path>", "Output file path (default: platform-specific)")
10
- .option("--with-backend", "Include backend security audit job (requires RAFTER_API_KEY)")
10
+ .option("--with-remote", "Include remote security audit job (requires RAFTER_API_KEY)")
11
+ .option("--with-backend", "Deprecated: use --with-remote")
11
12
  .action((opts) => {
12
13
  const platform = opts.platform || detectPlatform();
13
14
  if (!platform) {
@@ -21,7 +22,8 @@ export function createCiInitCommand() {
21
22
  console.error(`Valid options: ${validPlatforms.join(", ")}`);
22
23
  process.exit(1);
23
24
  }
24
- const { content, defaultPath } = generateTemplate(platform, !!opts.withBackend);
25
+ const includeRemote = !!(opts.includeRemote || opts.withBackend);
26
+ const { content, defaultPath } = generateTemplate(platform, includeRemote);
25
27
  const outputPath = opts.output || defaultPath;
26
28
  const outputDir = path.dirname(outputPath);
27
29
  if (!fs.existsSync(outputDir)) {
@@ -29,28 +31,30 @@ export function createCiInitCommand() {
29
31
  }
30
32
  fs.writeFileSync(outputPath, content, "utf-8");
31
33
  console.log(fmt.success(`Generated ${platform} CI config at ${outputPath}`));
32
- console.log();
33
- console.log("Next steps:");
34
- console.log(` 1. Review the generated file: ${outputPath}`);
35
- if (opts.withBackend) {
36
- if (platform === "github") {
37
- console.log(" 2. Add RAFTER_API_KEY to repo Settings > Secrets > Actions");
38
- }
39
- else if (platform === "gitlab") {
40
- console.log(" 2. Add RAFTER_API_KEY to Settings > CI/CD > Variables");
34
+ if (!isAgentMode()) {
35
+ console.log();
36
+ console.log("Next steps:");
37
+ console.log(` 1. Review the generated file: ${outputPath}`);
38
+ if (includeRemote) {
39
+ if (platform === "github") {
40
+ console.log(" 2. Add RAFTER_API_KEY to repo Settings > Secrets > Actions");
41
+ }
42
+ else if (platform === "gitlab") {
43
+ console.log(" 2. Add RAFTER_API_KEY to Settings > CI/CD > Variables");
44
+ }
45
+ else {
46
+ console.log(" 2. Add RAFTER_API_KEY to project environment variables");
47
+ }
41
48
  }
42
- else {
43
- console.log(" 2. Add RAFTER_API_KEY to project environment variables");
49
+ console.log(` ${includeRemote ? "3" : "2"}. Commit and push to trigger the pipeline`);
50
+ if (platform === "github") {
51
+ console.log();
52
+ console.log("Alternatives:");
53
+ console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v1");
54
+ console.log(" - Pre-commit: https://github.com/Raftersecurity/rafter-cli#pre-commit-framework");
44
55
  }
45
- }
46
- console.log(` ${opts.withBackend ? "3" : "2"}. Commit and push to trigger the pipeline`);
47
- if (platform === "github") {
48
56
  console.log();
49
- console.log("Alternatives:");
50
- console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v1");
51
- console.log(" - Pre-commit: https://github.com/Raftersecurity/rafter-cli#pre-commit-framework");
52
57
  }
53
- console.log();
54
58
  });
55
59
  }
56
60
  function detectPlatform() {
@@ -68,7 +68,7 @@ _rafter_completions() {
68
68
  if [[ "\${COMP_WORDS[1]}" == "agent" ]]; then
69
69
  COMPREPLY=( $(compgen -W "--risk-level --with-openclaw --with-claude-code --with-codex --with-gemini --with-aider --with-cursor --with-windsurf --with-continue --with-gitleaks --all --help" -- "\${cur}") )
70
70
  elif [[ "\${COMP_WORDS[1]}" == "ci" ]]; then
71
- COMPREPLY=( $(compgen -W "--platform --output --with-backend --help" -- "\${cur}") )
71
+ COMPREPLY=( $(compgen -W "--platform --output --with-remote --with-backend --help" -- "\${cur}") )
72
72
  fi
73
73
  return 0
74
74
  ;;
@@ -81,7 +81,7 @@ const ZSH_COMPLETION = `#compdef rafter
81
81
  _rafter() {
82
82
  local -a commands
83
83
  commands=(
84
- 'run:Submit a security scan to the Rafter backend'
84
+ 'run:Submit a remote security scan'
85
85
  'scan:Alias for run'
86
86
  'get:Retrieve scan results'
87
87
  'usage:Check API usage quota'
@@ -369,7 +369,8 @@ complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcom
369
369
  complete -c rafter -n '__fish_seen_subcommand_from ci' -a init -d 'Initialize CI pipeline'
370
370
  complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l platform -d 'CI platform' -ra 'github gitlab circleci'
371
371
  complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l output -d 'Output path' -r
372
- complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-backend -d 'Include backend audit'
372
+ complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-remote -d 'Include remote audit'
373
+ complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-backend -d 'Deprecated: use --with-remote'
373
374
 
374
375
  # hook subcommands
375
376
  complete -c rafter -n '__fish_seen_subcommand_from hook' -a pretool -d 'PreToolUse hook handler'
@@ -86,11 +86,11 @@ function generateHtmlReport(results, title) {
86
86
  ? "Low"
87
87
  : "None";
88
88
  const riskColor = {
89
- Critical: "#dc2626",
90
- High: "#ea580c",
91
- Medium: "#2563eb",
92
- Low: "#16a34a",
93
- None: "#16a34a",
89
+ Critical: "hsl(0 40% 55%)",
90
+ High: "hsl(25 35% 55%)",
91
+ Medium: "hsl(0 0% 64%)",
92
+ Low: "hsl(0 0% 50%)",
93
+ None: "hsl(0 0% 50%)",
94
94
  }[riskLevel];
95
95
  const topPatterns = Object.entries(patternCounts)
96
96
  .sort((a, b) => b[1] - a[1])
@@ -113,38 +113,39 @@ function generateHtmlReport(results, title) {
113
113
  <title>${escapeHtml(title)}</title>
114
114
  <style>
115
115
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
116
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #1e293b; background: #f8fafc; }
116
+ body { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; line-height: 1.6; color: hsl(0 0% 98%); background: hsl(0 0% 3.9%); }
117
117
  .container { max-width: 1100px; margin: 0 auto; padding: 2rem 1.5rem; }
118
- header { background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); color: white; padding: 2rem 0; margin-bottom: 2rem; }
118
+ header { background: hsl(0 0% 7%); color: hsl(0 0% 98%); padding: 2rem 0; margin-bottom: 2rem; border-bottom: 1px solid hsl(0 0% 14.9%); }
119
119
  header .container { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; }
120
120
  header h1 { font-size: 1.5rem; font-weight: 700; }
121
- header .meta { font-size: 0.85rem; opacity: 0.8; text-align: right; }
122
- .card { background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); padding: 1.5rem; margin-bottom: 1.5rem; }
123
- .card h2 { font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem; color: #334155; }
121
+ header .meta { font-size: 0.85rem; opacity: 0.6; text-align: right; }
122
+ .card { background: hsl(0 0% 7%); border-radius: 8px; border: 1px solid hsl(0 0% 14.9%); padding: 1.5rem; margin-bottom: 1.5rem; }
123
+ .card h2 { font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem; color: hsl(0 0% 98%); }
124
124
  .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; }
125
- .stat { text-align: center; padding: 1rem; border-radius: 6px; background: #f1f5f9; }
126
- .stat .value { font-size: 2rem; font-weight: 700; }
127
- .stat .label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; margin-top: 0.25rem; }
128
- .risk-badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 4px; color: white; font-weight: 600; font-size: 0.85rem; }
129
- .sev-critical { background: #dc2626; }
130
- .sev-high { background: #ea580c; }
131
- .sev-medium { background: #2563eb; }
132
- .sev-low { background: #16a34a; }
125
+ .stat { text-align: center; padding: 1rem; border-radius: 6px; background: hsl(0 0% 10%); border: 1px solid hsl(0 0% 14.9%); }
126
+ .stat .value { font-size: 2rem; font-weight: 700; color: hsl(0 0% 98%); }
127
+ .stat .label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: hsl(0 0% 50%); margin-top: 0.25rem; }
128
+ .risk-badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 4px; font-weight: 600; font-size: 0.85rem; }
129
+ .sev-critical { background: hsl(0 30% 20%); color: hsl(0 40% 75%); border: 1px solid hsl(0 30% 30%); }
130
+ .sev-high { background: hsl(25 25% 18%); color: hsl(25 35% 70%); border: 1px solid hsl(25 25% 28%); }
131
+ .sev-medium { background: hsl(0 0% 18%); color: hsl(0 0% 70%); border: 1px solid hsl(0 0% 25%); }
132
+ .sev-low { background: hsl(0 0% 14%); color: hsl(0 0% 55%); border: 1px solid hsl(0 0% 22%); }
133
133
  .bar-chart { margin-top: 0.5rem; }
134
134
  .bar-row { display: flex; align-items: center; margin-bottom: 0.4rem; }
135
- .bar-label { width: 180px; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
136
- .bar-track { flex: 1; height: 20px; background: #e2e8f0; border-radius: 3px; overflow: hidden; }
137
- .bar-fill { height: 100%; border-radius: 3px; min-width: 2px; }
138
- .bar-count { width: 40px; text-align: right; font-size: 0.85rem; font-weight: 600; color: #475569; margin-left: 0.5rem; }
135
+ .bar-label { width: 180px; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: hsl(0 0% 70%); }
136
+ .bar-track { flex: 1; height: 20px; background: hsl(0 0% 14.9%); border-radius: 3px; overflow: hidden; }
137
+ .bar-fill { height: 100%; border-radius: 3px; min-width: 2px; background: hsl(0 0% 98%); opacity: 0.6; }
138
+ .bar-count { width: 40px; text-align: right; font-size: 0.85rem; font-weight: 600; color: hsl(0 0% 64%); margin-left: 0.5rem; }
139
139
  table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
140
- th { text-align: left; padding: 0.6rem 0.75rem; background: #f1f5f9; border-bottom: 2px solid #e2e8f0; font-weight: 600; color: #475569; white-space: nowrap; }
141
- td { padding: 0.6rem 0.75rem; border-bottom: 1px solid #e2e8f0; vertical-align: top; }
142
- tr:hover td { background: #f8fafc; }
143
- .sev-pill { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 3px; color: white; font-weight: 600; font-size: 0.75rem; text-transform: uppercase; }
144
- .file-path { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.8rem; word-break: break-all; }
145
- .redacted { font-family: monospace; font-size: 0.8rem; color: #94a3b8; }
146
- footer { text-align: center; padding: 2rem 0; font-size: 0.8rem; color: #94a3b8; }
147
- .no-findings { text-align: center; padding: 3rem; color: #16a34a; }
140
+ th { text-align: left; padding: 0.6rem 0.75rem; background: hsl(0 0% 10%); border-bottom: 2px solid hsl(0 0% 14.9%); font-weight: 600; color: hsl(0 0% 64%); white-space: nowrap; text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.75rem; }
141
+ td { padding: 0.6rem 0.75rem; border-bottom: 1px solid hsl(0 0% 14.9%); vertical-align: top; }
142
+ tr:hover td { background: hsl(0 0% 10%); }
143
+ .sev-pill { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 3px; font-weight: 600; font-size: 0.75rem; text-transform: uppercase; }
144
+ .file-path { font-size: 0.8rem; word-break: break-all; }
145
+ .redacted { font-size: 0.8rem; color: hsl(0 0% 40%); }
146
+ .description { color: hsl(0 0% 50%); }
147
+ footer { text-align: center; padding: 2rem 0; font-size: 0.8rem; color: hsl(0 0% 35%); border-top: 1px solid hsl(0 0% 14.9%); }
148
+ .no-findings { text-align: center; padding: 3rem; color: hsl(0 0% 64%); }
148
149
  .no-findings .icon { font-size: 3rem; margin-bottom: 0.5rem; }
149
150
  @media (max-width: 768px) {
150
151
  .summary-grid { grid-template-columns: repeat(2, 1fr); }
@@ -152,9 +153,9 @@ function generateHtmlReport(results, title) {
152
153
  table { display: block; overflow-x: auto; }
153
154
  }
154
155
  @media print {
155
- body { background: white; }
156
- .card { box-shadow: none; border: 1px solid #e2e8f0; break-inside: avoid; }
157
- header { background: #0f172a !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
156
+ body { background: hsl(0 0% 3.9%); color: hsl(0 0% 98%); }
157
+ .card { break-inside: avoid; }
158
+ header { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
158
159
  }
159
160
  </style>
160
161
  </head>
@@ -173,7 +174,7 @@ function generateHtmlReport(results, title) {
173
174
  <h2>Executive Summary</h2>
174
175
  <div class="summary-grid">
175
176
  <div class="stat">
176
- <div class="value" style="color:${riskColor}">${totalFindings}</div>
177
+ <div class="value">${totalFindings}</div>
177
178
  <div class="label">Total Findings</div>
178
179
  </div>
179
180
  <div class="stat">
@@ -181,7 +182,7 @@ function generateHtmlReport(results, title) {
181
182
  <div class="label">Files Affected</div>
182
183
  </div>
183
184
  <div class="stat">
184
- <div class="value"><span class="risk-badge" style="background:${riskColor}">${riskLevel}</span></div>
185
+ <div class="value"><span class="risk-badge" style="background:${riskColor};color:hsl(0 0% 98%)">${riskLevel}</span></div>
185
186
  <div class="label">Overall Risk</div>
186
187
  </div>
187
188
  </div>
@@ -190,10 +191,10 @@ function generateHtmlReport(results, title) {
190
191
  <div class="card">
191
192
  <h2>Severity Breakdown</h2>
192
193
  <div class="summary-grid">
193
- <div class="stat"><div class="value" style="color:#dc2626">${severityCounts.critical}</div><div class="label">Critical</div></div>
194
- <div class="stat"><div class="value" style="color:#ea580c">${severityCounts.high}</div><div class="label">High</div></div>
195
- <div class="stat"><div class="value" style="color:#2563eb">${severityCounts.medium}</div><div class="label">Medium</div></div>
196
- <div class="stat"><div class="value" style="color:#16a34a">${severityCounts.low}</div><div class="label">Low</div></div>
194
+ <div class="stat"><div class="value" style="color:hsl(0 40% 70%)">${severityCounts.critical}</div><div class="label">Critical</div></div>
195
+ <div class="stat"><div class="value" style="color:hsl(25 30% 65%)">${severityCounts.high}</div><div class="label">High</div></div>
196
+ <div class="stat"><div class="value">${severityCounts.medium}</div><div class="label">Medium</div></div>
197
+ <div class="stat"><div class="value" style="color:hsl(0 0% 50%)">${severityCounts.low}</div><div class="label">Low</div></div>
197
198
  </div>
198
199
  </div>
199
200
 
@@ -204,7 +205,7 @@ ${topPatterns.map(([name, count]) => {
204
205
  const pct = Math.round((count / totalFindings) * 100);
205
206
  return ` <div class="bar-row">
206
207
  <div class="bar-label" title="${escapeHtml(name)}">${escapeHtml(name)}</div>
207
- <div class="bar-track"><div class="bar-fill sev-medium" style="width:${pct}%"></div></div>
208
+ <div class="bar-track"><div class="bar-fill" style="width:${pct}%"></div></div>
208
209
  <div class="bar-count">${count}</div>
209
210
  </div>`;
210
211
  }).join("\n")}
@@ -226,7 +227,7 @@ ${totalFindings > 0 ? ` <div class="card">
226
227
  <tbody>
227
228
  ${findingsRows.map((f) => ` <tr>
228
229
  <td><span class="sev-pill sev-${f.severity}">${f.severity}</span></td>
229
- <td>${f.pattern}${f.description ? `<br><small style="color:#94a3b8">${f.description}</small>` : ""}</td>
230
+ <td>${f.pattern}${f.description ? `<br><small class="description">${f.description}</small>` : ""}</td>
230
231
  <td class="file-path">${f.file}</td>
231
232
  <td>${f.line}</td>
232
233
  <td class="redacted">${f.redacted}</td>
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * rafter scan — top-level scan command group.
3
3
  *
4
- * Default (no subcommand): remote backend scan (same as `rafter run`)
5
- * rafter scan remote: explicit alias for remote backend scan
4
+ * Default (no subcommand): remote scan (same as `rafter run`)
5
+ * rafter scan remote: explicit alias for remote scan
6
6
  * rafter scan local [path]: local secret scanner (was `rafter agent scan`)
7
7
  */
8
8
  import { Command } from "commander";
@@ -21,25 +21,27 @@ export function createScanGroupCommand() {
21
21
  .option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
22
22
  .option("-f, --format <format>", "json | md", "md")
23
23
  .option("-m, --mode <mode>", "scan mode: fast | plus", "fast")
24
+ .option("--github-token <token>", "GitHub PAT for private repos (or RAFTER_GITHUB_TOKEN env var)")
24
25
  .option("--skip-interactive", "do not wait for scan to complete")
25
26
  .option("--quiet", "suppress status messages")
26
27
  .action(async (opts) => {
27
28
  await runRemoteScan(opts);
28
29
  });
29
- // Root scan group — default action is remote backend scan
30
+ // Root scan group — default action is remote scan
30
31
  const scanGroup = new Command("scan")
31
- .description("Scan for security issues. Default: remote backend scan. Use 'scan local' for local secret scanning.")
32
+ .description("Scan for security issues. Default: remote scan. Use 'scan local' for local secret scanning.")
32
33
  .enablePositionalOptions()
33
34
  .option("-r, --repo <repo>", "org/repo (default: current)")
34
35
  .option("-b, --branch <branch>", "branch (default: current else main)")
35
36
  .option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
36
37
  .option("-f, --format <format>", "json | md", "md")
37
38
  .option("-m, --mode <mode>", "scan mode: fast | plus", "fast")
39
+ .option("--github-token <token>", "GitHub PAT for private repos (or RAFTER_GITHUB_TOKEN env var)")
38
40
  .option("--skip-interactive", "do not wait for scan to complete")
39
41
  .option("--quiet", "suppress status messages");
40
42
  scanGroup.addCommand(localCmd);
41
43
  scanGroup.addCommand(remoteCmd);
42
- // When invoked with no subcommand, run remote backend scan
44
+ // When invoked with no subcommand, run remote scan
43
45
  scanGroup.action(async (opts) => {
44
46
  await runRemoteScan(opts);
45
47
  });
@@ -2,9 +2,15 @@
2
2
  * Centralized risk assessment rules.
3
3
  * Single source of truth — imported by command-interceptor, audit-logger, and config-defaults.
4
4
  */
5
+ /** Directories where `rm -rf /<dir>` is catastrophic (data loss / unbootable). */
6
+ const CRITICAL_DIRS = "home|etc|usr|boot|root|sys|proc|lib|lib64|bin|sbin|opt";
5
7
  export const CRITICAL_PATTERNS = [
6
- /rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s+\//, // rm -rf / (any flag order: -rf, -fr, -r -f, -f -r)
7
- /rm\s+(-[a-z]*f[a-z]*\s+)*-[a-z]*r[a-z]*\s+\//, // rm -fr / (reversed)
8
+ // rm -rf / (root only, any flag order)
9
+ new RegExp(`rm\\s+(-[a-z]*r[a-z]*\\s+)*-[a-z]*f[a-z]*\\s+/(\\s|$)`),
10
+ new RegExp(`rm\\s+(-[a-z]*f[a-z]*\\s+)*-[a-z]*r[a-z]*\\s+/(\\s|$)`),
11
+ // rm -rf on critical top-level directories
12
+ new RegExp(`rm\\s+(-[a-z]*r[a-z]*\\s+)*-[a-z]*f[a-z]*\\s+/(${CRITICAL_DIRS})(/|\\s|$)`),
13
+ new RegExp(`rm\\s+(-[a-z]*f[a-z]*\\s+)*-[a-z]*r[a-z]*\\s+/(${CRITICAL_DIRS})(/|\\s|$)`),
8
14
  /:\(\)\{\s*:\|:&\s*\};:/, // fork bomb
9
15
  /dd\s+if=.*of=\/dev\/sd/,
10
16
  />\s*\/dev\/sd/,
@@ -55,11 +61,18 @@ export const DEFAULT_REQUIRE_APPROVAL = [
55
61
  "git push --force-if-includes",
56
62
  "git push .* \\+",
57
63
  ];
64
+ /** Read-only commands whose arguments should not trigger risk patterns. */
65
+ const SAFE_PREFIX = /^(grep|egrep|fgrep|rg|ag|ack|echo|printf)\s/;
66
+ /** Shell operators that chain independent commands. */
67
+ const CHAIN_OPERATORS = /[;|&]|&&|\|\|/;
58
68
  /**
59
69
  * Assess risk level of a command string.
60
70
  */
61
71
  export function assessCommandRisk(command) {
62
- const cmd = command.toLowerCase();
72
+ const cmd = command.toLowerCase().trim();
73
+ // Safe prefix only applies to simple (non-chained) commands
74
+ if (SAFE_PREFIX.test(cmd) && !CHAIN_OPERATORS.test(cmd))
75
+ return "low";
63
76
  for (const pattern of CRITICAL_PATTERNS) {
64
77
  if (pattern.test(cmd))
65
78
  return "critical";
package/dist/index.js CHANGED
@@ -21,24 +21,22 @@ import { createRequire } from "module";
21
21
  dotenv.config();
22
22
  const require = createRequire(import.meta.url);
23
23
  const { version: VERSION } = require("../package.json");
24
+ // Set agent mode early from argv — preAction hooks may not propagate to nested
25
+ // subcommands on Node 18, so we detect -a/--agent before Commander parses.
26
+ if (process.argv.includes("-a") || process.argv.includes("--agent")) {
27
+ setAgentMode(true);
28
+ }
24
29
  const program = new Command()
25
30
  .name("rafter")
26
31
  .description("Rafter CLI — the default security agent for AI workflows. Free for individuals and open source. No account required.")
27
32
  .version(VERSION)
28
33
  .enablePositionalOptions()
29
34
  .option("-a, --agent", "Plain output for AI agents (no colors/emoji)");
30
- // Set agent mode before any subcommand runs
31
- program.hook("preAction", (thisCommand) => {
32
- const opts = thisCommand.opts();
33
- if (opts.agent) {
34
- setAgentMode(true);
35
- }
36
- });
37
- // Backend commands
35
+ // Remote scan commands
38
36
  program.addCommand(createRunCommand());
39
37
  program.addCommand(createGetCommand());
40
38
  program.addCommand(createUsageCommand());
41
- // Scan command group (default: remote backend scan; subcommands: local, remote)
39
+ // Scan command group (default: remote scan; subcommands: local, remote)
42
40
  program.addCommand(createScanGroupCommand());
43
41
  // Agent commands
44
42
  program.addCommand(createAgentCommand());
@@ -60,6 +58,12 @@ program.addCommand(createNotifyCommand());
60
58
  program.addCommand(createReportCommand());
61
59
  // Shell completions
62
60
  program.addCommand(createCompletionCommand());
61
+ // Version subcommand (also available as --version)
62
+ program.addCommand(new Command("version")
63
+ .description("Print version and exit")
64
+ .action(() => {
65
+ console.log(VERSION);
66
+ }));
63
67
  // Non-blocking update check — runs after command, prints to stderr
64
68
  checkForUpdate(VERSION).then((notice) => {
65
69
  if (notice)
@@ -82,15 +82,19 @@ export class GitleaksScanner {
82
82
  /**
83
83
  * Scan a directory
84
84
  */
85
- async scanDirectory(dirPath) {
85
+ async scanDirectory(dirPath, opts) {
86
86
  if (!await this.isAvailable()) {
87
87
  throw new Error("Gitleaks not available");
88
88
  }
89
89
  const gitleaksPath = this.binaryManager.getGitleaksPath();
90
90
  const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${randomBytes(6).toString("hex")}.json`);
91
91
  try {
92
+ const args = ["detect", "-f", "json", "-r", tmpReport, "-s", dirPath];
93
+ if (!opts?.useGit) {
94
+ args.splice(1, 0, "--no-git");
95
+ }
92
96
  // Run gitleaks detect on directory
93
- await execFileAsync(gitleaksPath, ["detect", "--no-git", "-f", "json", "-r", tmpReport, "-s", dirPath], { timeout: 60000 });
97
+ await execFileAsync(gitleaksPath, args, { timeout: 60000 });
94
98
  // No leaks found
95
99
  if (!fs.existsSync(tmpReport)) {
96
100
  return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafter-security/cli",
3
- "version": "0.6.6",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "rafter": "./dist/index.js"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: rafter
3
3
  description: "Rafter — the security toolkit built for AI workflows. Three tiers: (1) fast local secret scanning, deterministic, no API key needed; (2) remote SAST/SCA with deterministic secret detection and dependency checks via API (fast mode, default); (3) agentic deep-dive analysis with additional passes (plus mode). Use when checking for vulnerabilities, leaked credentials, or whether code is safe to push. Also use before merging PRs, deploying, or shipping new features. If RAFTER_API_KEY is not set, local scanning works fully — don't block on it. Run `rafter brief commands` for full CLI reference."
4
- version: 0.6.5
4
+ version: 0.7.0
5
5
  allowed-tools: [Bash]
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: rafter-agent-security
3
3
  description: "Rafter local security tools — deterministic secret scanning, command risk assessment, skill auditing, and audit log review. Use when: checking for leaked credentials or API keys, evaluating whether code is safe to push, auditing skills before installation, reviewing security events. Works offline, no API key needed. Run `rafter brief security` for full capabilities."
4
- version: 0.6.5
4
+ version: 0.7.0
5
5
  allowed-tools: [Bash, Read, Glob, Grep]
6
6
  ---
7
7