@rafter-security/cli 0.6.5 → 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 +25 -25
- package/dist/commands/agent/audit-skill.js +20 -19
- package/dist/commands/agent/config.js +2 -1
- package/dist/commands/agent/exec.js +2 -0
- package/dist/commands/agent/index.js +2 -0
- package/dist/commands/agent/init-project.js +164 -0
- package/dist/commands/agent/init.js +276 -20
- package/dist/commands/agent/install-hook.js +15 -14
- package/dist/commands/agent/instruction-block.js +63 -0
- package/dist/commands/agent/scan.js +4 -3
- package/dist/commands/agent/verify.js +1 -1
- package/dist/commands/backend/run.js +12 -3
- package/dist/commands/backend/scan-status.js +3 -2
- package/dist/commands/brief.js +39 -2
- package/dist/commands/ci/init.js +26 -22
- package/dist/commands/completion.js +4 -3
- package/dist/commands/hook/posttool.js +95 -10
- package/dist/commands/hook/pretool.js +105 -10
- package/dist/commands/mcp/server.js +5 -5
- package/dist/commands/notify.js +278 -0
- package/dist/commands/report.js +274 -0
- package/dist/commands/scan/index.js +7 -5
- package/dist/core/risk-rules.js +18 -3
- package/dist/index.js +20 -10
- package/dist/scanners/gitleaks.js +14 -4
- package/dist/scanners/secret-patterns.js +1 -1
- package/package.json +2 -1
- package/resources/pre-commit-hook.sh +0 -5
- package/resources/rafter-security-skill.md +1 -1
- package/resources/skills/rafter/SKILL.md +25 -6
- package/resources/skills/rafter-agent-security/SKILL.md +25 -35
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @rafter-security/cli
|
|
2
2
|
|
|
3
|
-
Node.js CLI for [Rafter](https://rafter.so) —
|
|
3
|
+
Node.js CLI for [Rafter](https://rafter.so) — the security toolkit for developers. This is the **full-featured package** with both local security and remote code analysis.
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Local security toolkit** — Fast, deterministic secret scanning (21+ patterns, Gitleaks), policy enforcement with risk-tiered rules, pre-commit hooks, extension auditing, custom rule authoring, and full audit logging. Works with Claude Code, Codex CLI, OpenClaw, and 5 more platforms. No API key required. No data leaves your machine.
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**Remote code analysis** — Deep security audits that combine agentic analysis with a full SAST/SCA toolchain. The engine examines your codebase the way a professional cybersecurity auditor would — tracing data flows, reasoning about business logic, and surfacing vulnerabilities that static rules alone miss — then cross-references findings with industry-standard static analysis and dependency scanning. Structured JSON reports with documented exit codes. Your code is deleted immediately after analysis completes.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -23,7 +23,7 @@ yarn global add @rafter-security/cli
|
|
|
23
23
|
|
|
24
24
|
### Getting an API Key
|
|
25
25
|
|
|
26
|
-
To use
|
|
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 scanning 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
|
|
39
|
+
**Note**: Agent security features (secret scanning, command execution) work **without an API key**. Only remote code analysis requires authentication.
|
|
40
40
|
|
|
41
|
-
###
|
|
41
|
+
### Remote Code Analysis
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
# Set your API key (from above)
|
|
@@ -54,10 +54,10 @@ rafter get <scan-id>
|
|
|
54
54
|
rafter usage
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
###
|
|
57
|
+
### Local Security
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
# Initialize
|
|
60
|
+
# Initialize local security
|
|
61
61
|
rafter agent init
|
|
62
62
|
|
|
63
63
|
# Scan files for secrets
|
|
@@ -77,7 +77,7 @@ rafter agent config show
|
|
|
77
77
|
|
|
78
78
|
| Flag | Description |
|
|
79
79
|
|------|-------------|
|
|
80
|
-
| `-a, --agent` | Plain output
|
|
80
|
+
| `-a, --agent` | Plain output (no colors, no emoji) |
|
|
81
81
|
| `-V, --version` | Print version |
|
|
82
82
|
| `-h, --help` | Show help |
|
|
83
83
|
|
|
@@ -92,7 +92,7 @@ Trigger a new security scan for your repository.
|
|
|
92
92
|
- `-r, --repo <repo>` - Repository in format `org/repo` (default: auto-detected)
|
|
93
93
|
- `-b, --branch <branch>` - Branch name (default: auto-detected)
|
|
94
94
|
- `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
|
|
95
|
-
- `-f, --format <format>` - Output format: `json` or `md` (default: `
|
|
95
|
+
- `-f, --format <format>` - Output format: `json` or `md` (default: `md`)
|
|
96
96
|
- `--skip-interactive` - Don't wait for scan completion
|
|
97
97
|
- `--quiet` - Suppress status messages
|
|
98
98
|
|
|
@@ -116,7 +116,7 @@ Retrieve results from a completed scan.
|
|
|
116
116
|
|
|
117
117
|
**Options:**
|
|
118
118
|
- `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
|
|
119
|
-
- `-f, --format <format>` - Output format: `json` or `md` (default: `
|
|
119
|
+
- `-f, --format <format>` - Output format: `json` or `md` (default: `md`)
|
|
120
120
|
- `--interactive` - Poll until scan completes
|
|
121
121
|
- `--quiet` - Suppress status messages
|
|
122
122
|
|
|
@@ -143,13 +143,13 @@ rafter usage
|
|
|
143
143
|
|
|
144
144
|
---
|
|
145
145
|
|
|
146
|
-
##
|
|
146
|
+
## Local Security Commands
|
|
147
147
|
|
|
148
|
-
Rafter
|
|
148
|
+
Rafter is a **security primitive** that any developer or tool can call and trust. Stable exit codes, deterministic findings, and structured output mean any workflow can integrate Rafter without reading prose.
|
|
149
149
|
|
|
150
150
|
### `rafter agent init [options]`
|
|
151
151
|
|
|
152
|
-
Initialize
|
|
152
|
+
Initialize local security system.
|
|
153
153
|
|
|
154
154
|
**Options:**
|
|
155
155
|
- `--risk-level <level>` - Set risk level: `minimal`, `moderate`, or `aggressive` (default: `moderate`)
|
|
@@ -167,7 +167,7 @@ Initialize agent security system.
|
|
|
167
167
|
**What it does:**
|
|
168
168
|
- Creates `~/.rafter/config.json` configuration
|
|
169
169
|
- Initializes directory structure
|
|
170
|
-
- Detects
|
|
170
|
+
- Detects installed platforms
|
|
171
171
|
- Installs opted-in integrations (skills, hooks, MCP servers)
|
|
172
172
|
- Sets up audit logging
|
|
173
173
|
|
|
@@ -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-
|
|
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
|
|
516
|
-
rafter ci init --with-
|
|
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
|
|
@@ -562,9 +562,9 @@ The CLI automatically detects your repository and branch from the current Git re
|
|
|
562
562
|
|
|
563
563
|
**Note**: The CLI only scans remote repositories, not your current local branch.
|
|
564
564
|
|
|
565
|
-
###
|
|
565
|
+
### Local Security Configuration
|
|
566
566
|
|
|
567
|
-
|
|
567
|
+
Security settings are stored in `~/.rafter/config.json`. Key settings:
|
|
568
568
|
|
|
569
569
|
**Risk Levels:**
|
|
570
570
|
- `minimal` - Basic guidance only, most commands allowed
|
|
@@ -584,7 +584,7 @@ Agent security settings are stored in `~/.rafter/config.json`. Key settings:
|
|
|
584
584
|
|
|
585
585
|
## OpenClaw Integration
|
|
586
586
|
|
|
587
|
-
Rafter integrates seamlessly with [OpenClaw](https://openclaw.com)
|
|
587
|
+
Rafter integrates seamlessly with [OpenClaw](https://openclaw.com).
|
|
588
588
|
|
|
589
589
|
### Setup
|
|
590
590
|
|
|
@@ -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.
|
|
627
|
+
### 1. Remote Code Analysis Skill (Core Feature)
|
|
628
628
|
|
|
629
629
|
**Automatic Integration** - Claude can proactively suggest security scans
|
|
630
630
|
|
|
@@ -650,7 +650,7 @@ Claude will automatically suggest Rafter scans when you mention security, vulner
|
|
|
650
650
|
Can you run a Rafter security scan on this repo?
|
|
651
651
|
```
|
|
652
652
|
|
|
653
|
-
### 2.
|
|
653
|
+
### 2. Local Security Skill
|
|
654
654
|
|
|
655
655
|
**User-Invoked** - Requires explicit commands for safety
|
|
656
656
|
|
|
@@ -680,10 +680,10 @@ Explicitly invoke commands:
|
|
|
680
680
|
|
|
681
681
|
### Why Two Skills?
|
|
682
682
|
|
|
683
|
-
- **
|
|
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
|
|
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(`
|
|
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(
|
|
29
|
-
console.log(
|
|
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("
|
|
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("
|
|
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("
|
|
84
|
-
console.log(
|
|
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(
|
|
87
|
+
console.log(fmt.divider());
|
|
87
88
|
console.log(generateManualReviewPrompt(skillName, absolutePath, quickScan, skillContent));
|
|
88
|
-
console.log(
|
|
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("
|
|
138
|
-
console.log(
|
|
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("
|
|
142
|
+
console.log(fmt.success("Secrets: None detected"));
|
|
142
143
|
}
|
|
143
144
|
else {
|
|
144
|
-
console.log(
|
|
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("
|
|
151
|
+
console.log(fmt.success("External URLs: None"));
|
|
151
152
|
}
|
|
152
153
|
else {
|
|
153
|
-
console.log(
|
|
154
|
+
console.log(fmt.warning(`External URLs: ${scan.urls.length} found`));
|
|
154
155
|
scan.urls.slice(0, 5).forEach(url => {
|
|
155
|
-
console.log(`
|
|
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("
|
|
164
|
+
console.log(fmt.success("High-risk commands: None detected"));
|
|
164
165
|
}
|
|
165
166
|
else {
|
|
166
|
-
console.log(
|
|
167
|
+
console.log(fmt.warning(`High-risk commands: ${scan.highRiskCommands.length} found`));
|
|
167
168
|
scan.highRiskCommands.slice(0, 5).forEach(cmd => {
|
|
168
|
-
console.log(`
|
|
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(
|
|
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();
|
|
@@ -2,6 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import { createAuditCommand } from "./audit.js";
|
|
3
3
|
import { createScanCommand } from "./scan.js";
|
|
4
4
|
import { createInitCommand } from "./init.js";
|
|
5
|
+
import { createInitProjectCommand } from "./init-project.js";
|
|
5
6
|
import { createConfigCommand } from "./config.js";
|
|
6
7
|
import { createExecCommand } from "./exec.js";
|
|
7
8
|
import { createAuditSkillCommand } from "./audit-skill.js";
|
|
@@ -15,6 +16,7 @@ export function createAgentCommand() {
|
|
|
15
16
|
.description("Agent security features");
|
|
16
17
|
// Add subcommands
|
|
17
18
|
agent.addCommand(createInitCommand());
|
|
19
|
+
agent.addCommand(createInitProjectCommand());
|
|
18
20
|
agent.addCommand(createScanCommand());
|
|
19
21
|
agent.addCommand(createExecCommand());
|
|
20
22
|
agent.addCommand(createConfigCommand());
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { injectInstructionFile } from "./instruction-block.js";
|
|
3
|
+
import { fmt } from "../../utils/formatter.js";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
/** Find the git root directory, or null if not in a git repo */
|
|
8
|
+
function findGitRoot() {
|
|
9
|
+
try {
|
|
10
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
11
|
+
encoding: "utf-8",
|
|
12
|
+
timeout: 5000,
|
|
13
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
14
|
+
}).trim();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* All project-level instruction file targets.
|
|
22
|
+
*
|
|
23
|
+
* These are files that AI agents read at session start when working in a project.
|
|
24
|
+
* Unlike global files (~/.claude/CLAUDE.md), these live in the repo and are
|
|
25
|
+
* committed alongside the code so every agent session sees them.
|
|
26
|
+
*/
|
|
27
|
+
function getProjectTargets(projectRoot) {
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
platform: "Claude Code",
|
|
31
|
+
filePath: path.join(projectRoot, ".claude", "CLAUDE.md"),
|
|
32
|
+
description: "Claude Code project instructions",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
platform: "Codex CLI",
|
|
36
|
+
filePath: path.join(projectRoot, "AGENTS.md"),
|
|
37
|
+
description: "Codex CLI project instructions",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
platform: "Gemini CLI",
|
|
41
|
+
filePath: path.join(projectRoot, "GEMINI.md"),
|
|
42
|
+
description: "Gemini CLI project instructions",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
platform: "Cursor",
|
|
46
|
+
filePath: path.join(projectRoot, ".cursor", "rules", "rafter-security.mdc"),
|
|
47
|
+
description: "Cursor project rules",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
platform: "Windsurf",
|
|
51
|
+
filePath: path.join(projectRoot, ".windsurfrules"),
|
|
52
|
+
description: "Windsurf project rules",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
platform: "Continue.dev",
|
|
56
|
+
filePath: path.join(projectRoot, ".continuerules"),
|
|
57
|
+
description: "Continue.dev project rules",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
platform: "Aider",
|
|
61
|
+
filePath: path.join(projectRoot, ".aider", "conventions.md"),
|
|
62
|
+
description: "Aider project conventions",
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
export function createInitProjectCommand() {
|
|
67
|
+
return new Command("init-project")
|
|
68
|
+
.description("Generate project-level instruction files so AI agents discover Rafter at session start")
|
|
69
|
+
.option("--only <platforms>", "Comma-separated list of platforms (claude-code,codex,gemini,cursor,windsurf,continue,aider)")
|
|
70
|
+
.option("--list", "List which files would be created without writing them")
|
|
71
|
+
.action(async (opts) => {
|
|
72
|
+
const gitRoot = findGitRoot();
|
|
73
|
+
if (!gitRoot) {
|
|
74
|
+
console.error(fmt.error("Not in a git repository. Run this command from inside a project."));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
console.log(fmt.header("Rafter Project Setup"));
|
|
78
|
+
console.log(fmt.info(`Project root: ${gitRoot}`));
|
|
79
|
+
console.log();
|
|
80
|
+
const allTargets = getProjectTargets(gitRoot);
|
|
81
|
+
// Filter by --only if specified
|
|
82
|
+
let targets = allTargets;
|
|
83
|
+
if (opts.only) {
|
|
84
|
+
const platformMap = {
|
|
85
|
+
"claude-code": "Claude Code",
|
|
86
|
+
"claude": "Claude Code",
|
|
87
|
+
"codex": "Codex CLI",
|
|
88
|
+
"gemini": "Gemini CLI",
|
|
89
|
+
"cursor": "Cursor",
|
|
90
|
+
"windsurf": "Windsurf",
|
|
91
|
+
"continue": "Continue.dev",
|
|
92
|
+
"aider": "Aider",
|
|
93
|
+
};
|
|
94
|
+
const requested = opts.only.split(",").map((s) => s.trim().toLowerCase());
|
|
95
|
+
const platformNames = requested.map(r => platformMap[r]).filter(Boolean);
|
|
96
|
+
if (platformNames.length === 0) {
|
|
97
|
+
console.error(fmt.error(`Unknown platforms: ${opts.only}`));
|
|
98
|
+
console.error("Valid: claude-code, codex, gemini, cursor, windsurf, continue, aider");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
targets = allTargets.filter(t => platformNames.includes(t.platform));
|
|
102
|
+
}
|
|
103
|
+
// --list mode: show what would be created
|
|
104
|
+
if (opts.list) {
|
|
105
|
+
for (const target of targets) {
|
|
106
|
+
const exists = fs.existsSync(target.filePath);
|
|
107
|
+
const hasMarker = exists && fs.readFileSync(target.filePath, "utf-8").includes("<!-- rafter:start -->");
|
|
108
|
+
const status = hasMarker ? "update" : exists ? "append" : "create";
|
|
109
|
+
const rel = path.relative(gitRoot, target.filePath);
|
|
110
|
+
console.log(` [${status}] ${rel} — ${target.description}`);
|
|
111
|
+
}
|
|
112
|
+
console.log();
|
|
113
|
+
console.log(fmt.info("Run without --list to write files."));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Write instruction files
|
|
117
|
+
let created = 0;
|
|
118
|
+
let updated = 0;
|
|
119
|
+
let failed = 0;
|
|
120
|
+
for (const target of targets) {
|
|
121
|
+
const rel = path.relative(gitRoot, target.filePath);
|
|
122
|
+
const existed = fs.existsSync(target.filePath);
|
|
123
|
+
const hadMarker = existed && fs.readFileSync(target.filePath, "utf-8").includes("<!-- rafter:start -->");
|
|
124
|
+
try {
|
|
125
|
+
injectInstructionFile(target.filePath);
|
|
126
|
+
if (hadMarker) {
|
|
127
|
+
console.log(fmt.success(`[updated] ${rel}`));
|
|
128
|
+
updated++;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(fmt.success(`[created] ${rel}`));
|
|
132
|
+
created++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
console.log(fmt.error(`[failed] ${rel} — ${e}`));
|
|
137
|
+
failed++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Check for .rafter.yml
|
|
141
|
+
const policyPath = path.join(gitRoot, ".rafter.yml");
|
|
142
|
+
if (!fs.existsSync(policyPath)) {
|
|
143
|
+
console.log(fmt.info(`[skipped] .rafter.yml — not found (optional: create one for project-specific policy)`));
|
|
144
|
+
}
|
|
145
|
+
// Check for pre-commit hook
|
|
146
|
+
const hookPath = path.join(gitRoot, ".git", "hooks", "pre-commit");
|
|
147
|
+
const hasRafterHook = fs.existsSync(hookPath) &&
|
|
148
|
+
fs.readFileSync(hookPath, "utf-8").includes("rafter");
|
|
149
|
+
if (!hasRafterHook) {
|
|
150
|
+
console.log(fmt.info(`[hint] Run \`rafter agent install-hook\` to add pre-commit secret scanning`));
|
|
151
|
+
}
|
|
152
|
+
console.log();
|
|
153
|
+
if (created > 0 || updated > 0) {
|
|
154
|
+
console.log(fmt.success(`Done: ${created} created, ${updated} updated${failed > 0 ? `, ${failed} failed` : ""}`));
|
|
155
|
+
console.log();
|
|
156
|
+
console.log("Agents starting sessions in this project will now see Rafter security context.");
|
|
157
|
+
console.log("Consider committing these files so all contributors benefit.");
|
|
158
|
+
}
|
|
159
|
+
else if (failed > 0) {
|
|
160
|
+
console.log(fmt.error(`All ${failed} files failed to write.`));
|
|
161
|
+
}
|
|
162
|
+
console.log();
|
|
163
|
+
});
|
|
164
|
+
}
|