@rafter-security/cli 0.5.1 → 0.5.5

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
@@ -566,7 +566,7 @@ Agent security settings are stored in `~/.rafter/config.json`. Key settings:
566
566
 
567
567
  **File Locations:**
568
568
  - Config: `~/.rafter/config.json`
569
- - Audit log: `~/.rafter/audit.log`
569
+ - Audit log: `~/.rafter/audit.jsonl`
570
570
  - Binaries: `~/.rafter/bin/`
571
571
  - Patterns: `~/.rafter/patterns/`
572
572
 
@@ -18,7 +18,7 @@ async function auditSkill(skillPath, opts) {
18
18
  // Validate skill file exists
19
19
  if (!fs.existsSync(skillPath)) {
20
20
  console.error(`Error: Skill file not found: ${skillPath}`);
21
- process.exit(1);
21
+ process.exit(2);
22
22
  }
23
23
  const absolutePath = path.resolve(skillPath);
24
24
  const skillContent = fs.readFileSync(absolutePath, "utf-8");
@@ -48,6 +48,9 @@ async function auditSkill(skillPath, opts) {
48
48
  rafterSkillInstalled
49
49
  };
50
50
  console.log(JSON.stringify(result, null, 2));
51
+ if (quickScan.secrets > 0 || quickScan.highRiskCommands.length > 0) {
52
+ process.exit(1);
53
+ }
51
54
  return;
52
55
  }
53
56
  // Check if we can use OpenClaw
@@ -85,6 +88,9 @@ async function auditSkill(skillPath, opts) {
85
88
  console.log("─".repeat(60));
86
89
  }
87
90
  console.log();
91
+ if (quickScan.secrets > 0 || quickScan.highRiskCommands.length > 0) {
92
+ process.exit(1);
93
+ }
88
94
  }
89
95
  async function runQuickScan(content) {
90
96
  // 1. Scan for secrets
@@ -0,0 +1,203 @@
1
+ import { Command } from "commander";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { fmt } from "../../utils/formatter.js";
6
+ import { RegexScanner } from "../../scanners/regex-scanner.js";
7
+ import { GitleaksScanner } from "../../scanners/gitleaks.js";
8
+ import { ConfigManager } from "../../core/config-manager.js";
9
+ const BASELINE_PATH = path.join(os.homedir(), ".rafter", "baseline.json");
10
+ function loadBaseline() {
11
+ if (!fs.existsSync(BASELINE_PATH)) {
12
+ return { version: 1, created: "", updated: "", entries: [] };
13
+ }
14
+ try {
15
+ return JSON.parse(fs.readFileSync(BASELINE_PATH, "utf-8"));
16
+ }
17
+ catch {
18
+ return { version: 1, created: "", updated: "", entries: [] };
19
+ }
20
+ }
21
+ function saveBaseline(baseline) {
22
+ const dir = path.dirname(BASELINE_PATH);
23
+ if (!fs.existsSync(dir)) {
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ }
26
+ fs.writeFileSync(BASELINE_PATH, JSON.stringify(baseline, null, 2), "utf-8");
27
+ }
28
+ export function createBaselineCommand() {
29
+ const baseline = new Command("baseline")
30
+ .description("Manage the findings baseline (allowlist for known findings)");
31
+ baseline.addCommand(createBaselineCreateCommand());
32
+ baseline.addCommand(createBaselineShowCommand());
33
+ baseline.addCommand(createBaselineClearCommand());
34
+ baseline.addCommand(createBaselineAddCommand());
35
+ return baseline;
36
+ }
37
+ function createBaselineCreateCommand() {
38
+ return new Command("create")
39
+ .description("Scan and save all current findings as the baseline")
40
+ .argument("[path]", "Path to scan", ".")
41
+ .option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
42
+ .action(async (scanPath, opts) => {
43
+ const resolvedPath = path.resolve(scanPath);
44
+ if (!fs.existsSync(resolvedPath)) {
45
+ console.error(`Error: Path not found: ${resolvedPath}`);
46
+ process.exit(2);
47
+ }
48
+ const manager = new ConfigManager();
49
+ const cfg = manager.loadWithPolicy();
50
+ const scanCfg = cfg.agent?.scan;
51
+ console.error(`Scanning ${resolvedPath} to build baseline...`);
52
+ const engine = await selectEngine(opts.engine || "auto");
53
+ let results;
54
+ if (fs.statSync(resolvedPath).isDirectory()) {
55
+ results = await scanDirectory(resolvedPath, engine, scanCfg);
56
+ }
57
+ else {
58
+ results = await scanFile(resolvedPath, engine);
59
+ }
60
+ const now = new Date().toISOString();
61
+ const entries = [];
62
+ for (const r of results) {
63
+ for (const m of r.matches) {
64
+ entries.push({
65
+ file: r.file,
66
+ line: m.line ?? null,
67
+ pattern: m.pattern.name,
68
+ addedAt: now,
69
+ });
70
+ }
71
+ }
72
+ const existing = loadBaseline();
73
+ const baseline = {
74
+ version: 1,
75
+ created: existing.created || now,
76
+ updated: now,
77
+ entries,
78
+ };
79
+ saveBaseline(baseline);
80
+ if (entries.length === 0) {
81
+ console.log(fmt.success("No findings — baseline is empty (all clean)"));
82
+ }
83
+ else {
84
+ console.log(fmt.success(`Baseline saved: ${entries.length} finding(s) recorded`));
85
+ console.log(` Location: ${BASELINE_PATH}`);
86
+ console.log();
87
+ console.log("Future scans with --baseline will suppress these findings.");
88
+ }
89
+ });
90
+ }
91
+ function createBaselineShowCommand() {
92
+ return new Command("show")
93
+ .description("Show current baseline entries")
94
+ .option("--json", "Output as JSON")
95
+ .action((opts) => {
96
+ const baseline = loadBaseline();
97
+ if (opts.json) {
98
+ console.log(JSON.stringify(baseline, null, 2));
99
+ return;
100
+ }
101
+ if (baseline.entries.length === 0) {
102
+ console.log("Baseline is empty. Run: rafter agent baseline create");
103
+ return;
104
+ }
105
+ console.log(`Baseline: ${baseline.entries.length} entries`);
106
+ if (baseline.updated) {
107
+ console.log(`Updated: ${baseline.updated}`);
108
+ }
109
+ console.log();
110
+ // Group by file
111
+ const byFile = new Map();
112
+ for (const entry of baseline.entries) {
113
+ const list = byFile.get(entry.file) || [];
114
+ list.push(entry);
115
+ byFile.set(entry.file, list);
116
+ }
117
+ for (const [file, entries] of byFile) {
118
+ console.log(fmt.info(file));
119
+ for (const e of entries) {
120
+ const loc = e.line != null ? `line ${e.line}` : "unknown location";
121
+ console.log(` ${e.pattern} (${loc})`);
122
+ }
123
+ console.log();
124
+ }
125
+ });
126
+ }
127
+ function createBaselineClearCommand() {
128
+ return new Command("clear")
129
+ .description("Remove all baseline entries")
130
+ .action(() => {
131
+ if (!fs.existsSync(BASELINE_PATH)) {
132
+ console.log("No baseline file found — nothing to clear.");
133
+ return;
134
+ }
135
+ fs.unlinkSync(BASELINE_PATH);
136
+ console.log(fmt.success("Baseline cleared"));
137
+ });
138
+ }
139
+ function createBaselineAddCommand() {
140
+ return new Command("add")
141
+ .description("Manually add a finding to the baseline")
142
+ .requiredOption("--file <path>", "File path")
143
+ .requiredOption("--pattern <name>", "Pattern name (e.g. 'AWS Access Key')")
144
+ .option("--line <number>", "Line number")
145
+ .action((opts) => {
146
+ const baseline = loadBaseline();
147
+ const now = new Date().toISOString();
148
+ const entry = {
149
+ file: path.resolve(opts.file),
150
+ line: opts.line != null ? parseInt(opts.line, 10) : null,
151
+ pattern: opts.pattern,
152
+ addedAt: now,
153
+ };
154
+ baseline.entries.push(entry);
155
+ baseline.updated = now;
156
+ if (!baseline.created)
157
+ baseline.created = now;
158
+ saveBaseline(baseline);
159
+ console.log(fmt.success(`Added to baseline: ${opts.pattern} in ${opts.file}`));
160
+ });
161
+ }
162
+ // ── helpers ─────────────────────────────────────────────────────────
163
+ async function selectEngine(preference) {
164
+ if (preference === "patterns")
165
+ return "patterns";
166
+ if (preference === "gitleaks") {
167
+ const g = new GitleaksScanner();
168
+ return (await g.isAvailable()) ? "gitleaks" : "patterns";
169
+ }
170
+ const g = new GitleaksScanner();
171
+ return (await g.isAvailable()) ? "gitleaks" : "patterns";
172
+ }
173
+ async function scanFile(filePath, engine) {
174
+ if (engine === "gitleaks") {
175
+ try {
176
+ const g = new GitleaksScanner();
177
+ const r = await g.scanFile(filePath);
178
+ return r.matches.length > 0 ? [r] : [];
179
+ }
180
+ catch {
181
+ const s = new RegexScanner();
182
+ const r = s.scanFile(filePath);
183
+ return r.matches.length > 0 ? [r] : [];
184
+ }
185
+ }
186
+ const s = new RegexScanner();
187
+ const r = s.scanFile(filePath);
188
+ return r.matches.length > 0 ? [r] : [];
189
+ }
190
+ async function scanDirectory(dirPath, engine, scanCfg) {
191
+ if (engine === "gitleaks") {
192
+ try {
193
+ const g = new GitleaksScanner();
194
+ return await g.scanDirectory(dirPath);
195
+ }
196
+ catch {
197
+ const s = new RegexScanner();
198
+ return s.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
199
+ }
200
+ }
201
+ const s = new RegexScanner();
202
+ return s.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
203
+ }
@@ -6,6 +6,10 @@ import { createConfigCommand } from "./config.js";
6
6
  import { createExecCommand } from "./exec.js";
7
7
  import { createAuditSkillCommand } from "./audit-skill.js";
8
8
  import { createInstallHookCommand } from "./install-hook.js";
9
+ import { createVerifyCommand } from "./verify.js";
10
+ import { createStatusCommand } from "./status.js";
11
+ import { createUpdateGitleaksCommand } from "./update-gitleaks.js";
12
+ import { createBaselineCommand } from "./baseline.js";
9
13
  export function createAgentCommand() {
10
14
  const agent = new Command("agent")
11
15
  .description("Agent security features");
@@ -17,5 +21,9 @@ export function createAgentCommand() {
17
21
  agent.addCommand(createAuditCommand());
18
22
  agent.addCommand(createAuditSkillCommand());
19
23
  agent.addCommand(createInstallHookCommand());
24
+ agent.addCommand(createVerifyCommand());
25
+ agent.addCommand(createStatusCommand());
26
+ agent.addCommand(createUpdateGitleaksCommand());
27
+ agent.addCommand(createBaselineCommand());
20
28
  return agent;
21
29
  }
@@ -32,16 +32,25 @@ function installClaudeCodeHooks() {
32
32
  settings.hooks = {};
33
33
  if (!settings.hooks.PreToolUse)
34
34
  settings.hooks.PreToolUse = [];
35
- const rafterHook = { type: "command", command: "rafter hook pretool" };
35
+ if (!settings.hooks.PostToolUse)
36
+ settings.hooks.PostToolUse = [];
37
+ const preHook = { type: "command", command: "rafter hook pretool" };
38
+ const postHook = { type: "command", command: "rafter hook posttool" };
36
39
  // Remove any existing Rafter hooks to avoid duplicates
37
40
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((entry) => {
38
41
  const hooks = entry.hooks || [];
39
42
  return !hooks.some((h) => h.command === "rafter hook pretool");
40
43
  });
44
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((entry) => {
45
+ const hooks = entry.hooks || [];
46
+ return !hooks.some((h) => h.command === "rafter hook posttool");
47
+ });
41
48
  // Add Rafter hooks
42
- settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [rafterHook] }, { matcher: "Write|Edit", hooks: [rafterHook] });
49
+ settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [preHook] }, { matcher: "Write|Edit", hooks: [preHook] });
50
+ settings.hooks.PostToolUse.push({ matcher: ".*", hooks: [postHook] });
43
51
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
44
52
  console.log(fmt.success(`Installed PreToolUse hooks to ${settingsPath}`));
53
+ console.log(fmt.success(`Installed PostToolUse hooks to ${settingsPath}`));
45
54
  }
46
55
  async function installClaudeCodeSkills() {
47
56
  const homeDir = os.homedir();
@@ -87,6 +96,7 @@ export function createInitCommand() {
87
96
  .option("--skip-claude-code", "Skip Claude Code skill installation")
88
97
  .option("--claude-code", "Force Claude Code skill installation")
89
98
  .option("--skip-gitleaks", "Skip Gitleaks binary download")
99
+ .option("--update", "Re-download gitleaks and reinstall integrations without resetting config")
90
100
  .action(async (opts) => {
91
101
  console.log(fmt.header("Rafter Agent Security Setup"));
92
102
  console.log(fmt.divider());
@@ -125,49 +135,90 @@ export function createInitCommand() {
125
135
  }
126
136
  manager.set("agent.riskLevel", opts.riskLevel);
127
137
  console.log(fmt.success(`Set risk level: ${opts.riskLevel}`));
128
- // Download Gitleaks binary (optional)
138
+ // Check / download Gitleaks binary (optional)
129
139
  if (!opts.skipGitleaks) {
130
140
  const binaryManager = new BinaryManager();
131
141
  const platformInfo = binaryManager.getPlatformInfo();
132
- if (!platformInfo.supported) {
133
- console.log(fmt.info(`Gitleaks not available for ${platformInfo.platform}/${platformInfo.arch}`));
134
- console.log(fmt.success("Using pattern-based scanning (21 patterns)"));
135
- }
136
- else if (binaryManager.isGitleaksInstalled()) {
137
- const version = await binaryManager.getGitleaksVersion();
138
- console.log(fmt.success(`Gitleaks already installed (${version})`));
142
+ // Helper: show diagnostics for a failing binary (mirrors Python's agent init)
143
+ const showDiagnostics = async (binaryPath, verResult) => {
144
+ if (verResult.stderr) {
145
+ console.log(fmt.info(` stderr: ${verResult.stderr}`));
146
+ }
147
+ const diag = await binaryManager.collectBinaryDiagnostics(binaryPath);
148
+ if (diag) {
149
+ console.log(fmt.info("Diagnostics:"));
150
+ console.log(diag);
151
+ }
152
+ 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'."));
153
+ console.log();
154
+ };
155
+ if (!opts.update && binaryManager.isGitleaksInstalled()) {
156
+ // Local binary exists — verify it actually works
157
+ const verResult = await binaryManager.verifyGitleaksVerbose();
158
+ if (verResult.ok) {
159
+ console.log(fmt.success(`Gitleaks already installed (${verResult.stdout})`));
160
+ }
161
+ else {
162
+ console.log(fmt.warning("Gitleaks binary found locally but failed to execute."));
163
+ console.log(fmt.info(` Binary: ${binaryManager.getGitleaksPath()}`));
164
+ await showDiagnostics(binaryManager.getGitleaksPath(), verResult);
165
+ }
139
166
  }
140
167
  else {
141
- console.log();
142
- console.log(fmt.info("Downloading Gitleaks (enhanced secret detection)..."));
143
- try {
144
- await binaryManager.downloadGitleaks((msg) => {
145
- console.log(` ${msg}`);
146
- });
147
- console.log();
168
+ // Not installed locally (or --update forcing re-download) — check PATH first
169
+ // unless --update was passed (in that case force a fresh managed install)
170
+ const pathBinary = opts.update ? null : binaryManager.findGitleaksOnPath();
171
+ if (pathBinary) {
172
+ const verResult = await binaryManager.verifyGitleaksVerbose(pathBinary);
173
+ if (verResult.ok) {
174
+ console.log(fmt.success(`Gitleaks available on PATH (${verResult.stdout})`));
175
+ }
176
+ else {
177
+ console.log(fmt.warning("Gitleaks found on PATH but failed to execute."));
178
+ console.log(fmt.info(` Binary: ${pathBinary}`));
179
+ await showDiagnostics(pathBinary, verResult);
180
+ }
181
+ }
182
+ else if (!platformInfo.supported) {
183
+ console.log(fmt.info(`Gitleaks not available for ${platformInfo.platform}/${platformInfo.arch}`));
184
+ console.log(fmt.success("Using pattern-based scanning (21 patterns)"));
148
185
  }
149
- catch (e) {
150
- console.log(fmt.warning(`Failed to download Gitleaks: ${e}`));
151
- console.log(fmt.success("Falling back to pattern-based scanning"));
186
+ else {
187
+ // Not on PATH, not installed locally — download
152
188
  console.log();
189
+ console.log(fmt.info("Downloading Gitleaks (enhanced secret detection)..."));
190
+ try {
191
+ await binaryManager.downloadGitleaks((msg) => {
192
+ console.log(` ${msg}`);
193
+ });
194
+ console.log();
195
+ }
196
+ catch (e) {
197
+ console.log();
198
+ console.log(fmt.error(`Gitleaks setup failed — pattern-based scanning will be used instead.`));
199
+ console.log(fmt.warning(String(e)));
200
+ console.log();
201
+ console.log(fmt.info("To fix: install gitleaks manually (https://github.com/gitleaks/gitleaks/releases) and ensure it is on PATH, then re-run 'rafter agent init'."));
202
+ console.log();
203
+ }
153
204
  }
154
205
  }
155
206
  }
156
207
  // Install OpenClaw skill if applicable
157
208
  if (hasOpenClaw && !opts.skipOpenclaw) {
158
- try {
159
- const skillManager = new SkillManager();
160
- const installed = await skillManager.installRafterSkill();
161
- if (installed) {
162
- console.log(fmt.success("Installed Rafter Security skill to ~/.openclaw/skills/rafter-security.md"));
163
- manager.set("agent.environments.openclaw.enabled", true);
164
- }
165
- else {
166
- console.log(fmt.warning("Failed to install Rafter Security skill"));
167
- }
209
+ const skillManager = new SkillManager();
210
+ const result = await skillManager.installRafterSkillVerbose();
211
+ if (result.ok) {
212
+ console.log(fmt.success("Installed Rafter Security skill to ~/.openclaw/skills/rafter-security.md"));
213
+ manager.set("agent.environments.openclaw.enabled", true);
168
214
  }
169
- catch (e) {
170
- console.error(fmt.error(`Failed to install OpenClaw skill: ${e}`));
215
+ else {
216
+ console.log(fmt.error("Failed to install Rafter Security skill"));
217
+ console.log(fmt.warning(` Source: ${result.sourcePath}`));
218
+ console.log(fmt.warning(` Destination: ${result.destPath}`));
219
+ if (result.error) {
220
+ console.log(fmt.warning(` Error: ${result.error}`));
221
+ }
171
222
  }
172
223
  }
173
224
  // Install Claude Code skills + hooks if applicable
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import fs from "fs";
3
+ import os from "os";
3
4
  import path from "path";
4
5
  import { execSync } from "child_process";
5
6
  import { fileURLToPath } from 'url';
@@ -7,25 +8,36 @@ const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = path.dirname(__filename);
8
9
  export function createInstallHookCommand() {
9
10
  return new Command("install-hook")
10
- .description("Install pre-commit hook to scan for secrets")
11
+ .description("Install git hook to scan for secrets")
11
12
  .option("--global", "Install globally for all repos (via git config)")
13
+ .option("--push", "Install pre-push hook instead of pre-commit")
12
14
  .action(async (opts) => {
13
15
  await installHook(opts);
14
16
  });
15
17
  }
16
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";
17
21
  if (opts.global) {
18
- await installGlobalHook();
22
+ await installGlobalHook(hookName, templateName);
19
23
  }
20
24
  else {
21
- await installLocalHook();
25
+ await installLocalHook(hookName, templateName);
22
26
  }
23
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
+ }
24
37
  /**
25
- * Install pre-commit hook for current repository
38
+ * Install hook for current repository
26
39
  */
27
- async function installLocalHook() {
28
- // Check if in a git repository
40
+ async function installLocalHook(hookName, templateName) {
29
41
  try {
30
42
  execSync("git rev-parse --git-dir", { stdio: "pipe" });
31
43
  }
@@ -34,80 +46,63 @@ async function installLocalHook() {
34
46
  console.error(" Run this command from inside a git repository");
35
47
  process.exit(1);
36
48
  }
37
- // Get .git directory
38
49
  const gitDir = execSync("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
39
50
  const hooksDir = path.resolve(gitDir, "hooks");
40
- const hookPath = path.join(hooksDir, "pre-commit");
41
- // Ensure hooks directory exists
51
+ const hookPath = path.join(hooksDir, hookName);
42
52
  if (!fs.existsSync(hooksDir)) {
43
53
  fs.mkdirSync(hooksDir, { recursive: true });
44
54
  }
45
- // Check if hook already exists
46
55
  if (fs.existsSync(hookPath)) {
47
56
  const existing = fs.readFileSync(hookPath, "utf-8");
48
- // Check if it's already a Rafter hook
49
- if (existing.includes("Rafter Security Pre-Commit Hook")) {
50
- console.log("✓ Rafter pre-commit hook already installed");
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`);
51
60
  return;
52
61
  }
53
- // Backup existing hook
54
62
  const backupPath = `${hookPath}.backup-${Date.now()}`;
55
63
  fs.copyFileSync(hookPath, backupPath);
56
64
  console.log(`📦 Backed up existing hook to: ${path.basename(backupPath)}`);
57
65
  }
58
- // Get hook template path
59
- const templatePath = path.join(__dirname, "..", "..", "..", "resources", "pre-commit-hook.sh");
60
- if (!fs.existsSync(templatePath)) {
61
- console.error("❌ Error: Hook template not found");
62
- console.error(` Expected at: ${templatePath}`);
63
- process.exit(1);
64
- }
65
- // Copy hook template
66
- const hookContent = fs.readFileSync(templatePath, "utf-8");
66
+ const hookContent = fs.readFileSync(getTemplatePath(templateName), "utf-8");
67
67
  fs.writeFileSync(hookPath, hookContent, "utf-8");
68
- // Make executable
69
68
  fs.chmodSync(hookPath, 0o755);
70
- console.log("✓ Installed Rafter pre-commit hook");
69
+ console.log(`✓ Installed Rafter ${hookName} hook`);
71
70
  console.log(` Location: ${hookPath}`);
72
71
  console.log();
73
- console.log("The hook will:");
74
- console.log(" Scan staged files for secrets before each commit");
75
- console.log(" • Block commits if secrets are detected");
76
- console.log(" • Can be bypassed with: git commit --no-verify (not recommended)");
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
+ }
77
84
  console.log();
78
85
  }
79
86
  /**
80
- * Install pre-commit hook globally for all repositories
87
+ * Install hook globally for all repositories
81
88
  */
82
- async function installGlobalHook() {
83
- // Create global hooks directory
84
- const homeDir = process.env.HOME || process.env.USERPROFILE;
89
+ async function installGlobalHook(hookName, templateName) {
90
+ const homeDir = os.homedir();
85
91
  if (!homeDir) {
86
92
  console.error("❌ Error: Could not determine home directory");
87
93
  process.exit(1);
88
94
  }
89
95
  const globalHooksDir = path.join(homeDir, ".rafter", "git-hooks");
90
- const hookPath = path.join(globalHooksDir, "pre-commit");
91
- // Create directory
96
+ const hookPath = path.join(globalHooksDir, hookName);
92
97
  if (!fs.existsSync(globalHooksDir)) {
93
98
  fs.mkdirSync(globalHooksDir, { recursive: true });
94
99
  }
95
- // Get hook template path
96
- const templatePath = path.join(__dirname, "..", "..", "..", "resources", "pre-commit-hook.sh");
97
- if (!fs.existsSync(templatePath)) {
98
- console.error("❌ Error: Hook template not found");
99
- console.error(` Expected at: ${templatePath}`);
100
- process.exit(1);
101
- }
102
- // Copy hook template
103
- const hookContent = fs.readFileSync(templatePath, "utf-8");
100
+ const hookContent = fs.readFileSync(getTemplatePath(templateName), "utf-8");
104
101
  fs.writeFileSync(hookPath, hookContent, "utf-8");
105
- // Make executable
106
102
  fs.chmodSync(hookPath, 0o755);
107
- // Configure git to use global hooks directory
108
103
  try {
109
104
  execSync(`git config --global core.hooksPath "${globalHooksDir}"`, { stdio: "pipe" });
110
- console.log("✓ Installed Rafter pre-commit hook globally");
105
+ console.log(`✓ Installed Rafter ${hookName} hook globally`);
111
106
  console.log(` Location: ${hookPath}`);
112
107
  console.log(` Git config: core.hooksPath = ${globalHooksDir}`);
113
108
  console.log();
@@ -117,7 +112,7 @@ async function installGlobalHook() {
117
112
  console.log(` git config --global --unset core.hooksPath`);
118
113
  console.log();
119
114
  console.log("To install per-repository instead:");
120
- console.log(` cd <repo> && rafter agent install-hook`);
115
+ console.log(` cd <repo> && rafter agent install-hook${hookName === "pre-push" ? " --push" : ""}`);
121
116
  console.log();
122
117
  }
123
118
  catch (e) {