@rafter-security/cli 0.6.6 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +29 -10
  2. package/dist/commands/agent/audit-skill.js +22 -20
  3. package/dist/commands/agent/audit.js +27 -0
  4. package/dist/commands/agent/components.js +800 -0
  5. package/dist/commands/agent/config.js +2 -1
  6. package/dist/commands/agent/disable.js +47 -0
  7. package/dist/commands/agent/enable.js +50 -0
  8. package/dist/commands/agent/exec.js +2 -0
  9. package/dist/commands/agent/index.js +6 -0
  10. package/dist/commands/agent/init.js +162 -163
  11. package/dist/commands/agent/install-hook.js +15 -14
  12. package/dist/commands/agent/list.js +72 -0
  13. package/dist/commands/agent/scan.js +4 -3
  14. package/dist/commands/agent/verify.js +1 -1
  15. package/dist/commands/backend/run.js +12 -3
  16. package/dist/commands/backend/scan-status.js +3 -2
  17. package/dist/commands/brief.js +22 -2
  18. package/dist/commands/ci/init.js +25 -21
  19. package/dist/commands/completion.js +4 -3
  20. package/dist/commands/docs/index.js +18 -0
  21. package/dist/commands/docs/list.js +37 -0
  22. package/dist/commands/docs/show.js +64 -0
  23. package/dist/commands/mcp/server.js +84 -0
  24. package/dist/commands/report.js +42 -41
  25. package/dist/commands/scan/index.js +7 -5
  26. package/dist/commands/skill/index.js +14 -0
  27. package/dist/commands/skill/install.js +89 -0
  28. package/dist/commands/skill/list.js +79 -0
  29. package/dist/commands/skill/registry.js +273 -0
  30. package/dist/commands/skill/remote.js +333 -0
  31. package/dist/commands/skill/review.js +975 -0
  32. package/dist/commands/skill/uninstall.js +65 -0
  33. package/dist/core/audit-logger.js +262 -21
  34. package/dist/core/config-manager.js +3 -0
  35. package/dist/core/docs-loader.js +148 -0
  36. package/dist/core/policy-loader.js +72 -1
  37. package/dist/core/risk-rules.js +16 -3
  38. package/dist/index.js +19 -9
  39. package/dist/scanners/gitleaks.js +6 -2
  40. package/package.json +1 -1
  41. package/resources/skills/rafter/SKILL.md +77 -97
  42. package/resources/skills/rafter/docs/backend.md +106 -0
  43. package/resources/skills/rafter/docs/cli-reference.md +199 -0
  44. package/resources/skills/rafter/docs/finding-triage.md +79 -0
  45. package/resources/skills/rafter/docs/guardrails.md +91 -0
  46. package/resources/skills/rafter/docs/shift-left.md +64 -0
  47. package/resources/skills/rafter-agent-security/SKILL.md +1 -1
  48. package/resources/skills/rafter-code-review/SKILL.md +91 -0
  49. package/resources/skills/rafter-code-review/docs/api.md +90 -0
  50. package/resources/skills/rafter-code-review/docs/asvs.md +120 -0
  51. package/resources/skills/rafter-code-review/docs/cwe-top25.md +78 -0
  52. package/resources/skills/rafter-code-review/docs/investigation-playbook.md +101 -0
  53. package/resources/skills/rafter-code-review/docs/llm.md +87 -0
  54. package/resources/skills/rafter-code-review/docs/web-app.md +84 -0
  55. package/resources/skills/rafter-secure-design/SKILL.md +103 -0
  56. package/resources/skills/rafter-secure-design/docs/api-design.md +97 -0
  57. package/resources/skills/rafter-secure-design/docs/auth.md +67 -0
  58. package/resources/skills/rafter-secure-design/docs/data-storage.md +90 -0
  59. package/resources/skills/rafter-secure-design/docs/dependencies.md +101 -0
  60. package/resources/skills/rafter-secure-design/docs/deployment.md +104 -0
  61. package/resources/skills/rafter-secure-design/docs/ingestion.md +98 -0
  62. package/resources/skills/rafter-secure-design/docs/standards-pointers.md +102 -0
  63. package/resources/skills/rafter-secure-design/docs/threat-modeling.md +128 -0
  64. package/resources/skills/rafter-skill-review/SKILL.md +106 -0
  65. package/resources/skills/rafter-skill-review/docs/authorship-provenance.md +82 -0
  66. package/resources/skills/rafter-skill-review/docs/changelog-review.md +99 -0
  67. package/resources/skills/rafter-skill-review/docs/data-practices.md +88 -0
  68. package/resources/skills/rafter-skill-review/docs/malware-indicators.md +79 -0
  69. package/resources/skills/rafter-skill-review/docs/prompt-injection.md +85 -0
  70. package/resources/skills/rafter-skill-review/docs/telemetry.md +78 -0
@@ -13,21 +13,33 @@ import { fmt } from "../../utils/formatter.js";
13
13
  import { injectInstructionFile } from "./instruction-block.js";
14
14
  const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = path.dirname(__filename);
16
+ /**
17
+ * Skills installed by `rafter agent init` for Claude Code / Codex.
18
+ *
19
+ * Sourced from `resources/skills/<name>/SKILL.md` in the shipped package.
20
+ * Keep this list in sync with Python's installer and the skills that actually
21
+ * ship in both resources/skills/ trees.
22
+ */
23
+ const AGENT_SKILLS = [
24
+ { name: "rafter", description: "Rafter Remote" },
25
+ { name: "rafter-agent-security", description: "Rafter Agent Security" },
26
+ { name: "rafter-secure-design", description: "Rafter Secure Design" },
27
+ { name: "rafter-code-review", description: "Rafter Code Review" },
28
+ ];
16
29
  /**
17
30
  * Install global instruction files for platforms that support them.
18
31
  *
19
- * Only Claude Code (~/.claude/CLAUDE.md) and Cursor (~/.cursor/rules/*.mdc)
20
- * have confirmed global instruction file paths. Other platforms (Codex, Gemini,
21
- * Windsurf, Continue.dev, Aider) only support project-level instruction files
22
- * (AGENTS.md, GEMINI.md, .windsurfrules, .continuerules, .aider/conventions.md)
23
- * which are handled by `rafter agent init-project` (not yet implemented).
32
+ * User scope: Claude Code (~/.claude/CLAUDE.md), Cursor (~/.cursor/rules/*.mdc).
33
+ * Project scope: both platforms also honor <cwd>/.claude/CLAUDE.md and
34
+ * <cwd>/.cursor/rules/*.mdc. Other platforms (Codex, Gemini, Windsurf,
35
+ * Continue.dev, Aider) have project-only instruction file conventions
36
+ * (AGENTS.md, GEMINI.md, etc.) handled by `rafter agent init-project`.
24
37
  */
25
- function installGlobalInstructions(platforms) {
26
- const homeDir = os.homedir();
27
- // Claude Code — ~/.claude/CLAUDE.md (confirmed global instruction file)
38
+ function installGlobalInstructions(platforms, root) {
39
+ // Claude Code — <root>/.claude/CLAUDE.md
28
40
  if (platforms.claudeCode) {
29
41
  try {
30
- const filePath = path.join(homeDir, ".claude", "CLAUDE.md");
42
+ const filePath = path.join(root, ".claude", "CLAUDE.md");
31
43
  injectInstructionFile(filePath);
32
44
  console.log(fmt.success(`Installed Rafter instructions to ${filePath}`));
33
45
  }
@@ -35,10 +47,10 @@ function installGlobalInstructions(platforms) {
35
47
  console.log(fmt.warning(`Failed to write Claude Code instructions: ${e}`));
36
48
  }
37
49
  }
38
- // Cursor — ~/.cursor/rules/rafter-security.mdc (global rules directory, markdown format)
50
+ // Cursor — <root>/.cursor/rules/rafter-security.mdc
39
51
  if (platforms.cursor) {
40
52
  try {
41
- const filePath = path.join(homeDir, ".cursor", "rules", "rafter-security.mdc");
53
+ const filePath = path.join(root, ".cursor", "rules", "rafter-security.mdc");
42
54
  injectInstructionFile(filePath);
43
55
  console.log(fmt.success(`Installed Rafter instructions to ${filePath}`));
44
56
  }
@@ -47,10 +59,9 @@ function installGlobalInstructions(platforms) {
47
59
  }
48
60
  }
49
61
  }
50
- function installClaudeCodeHooks() {
51
- const homeDir = os.homedir();
52
- const settingsPath = path.join(homeDir, ".claude", "settings.json");
53
- const claudeDir = path.join(homeDir, ".claude");
62
+ function installClaudeCodeHooks(root) {
63
+ const settingsPath = path.join(root, ".claude", "settings.json");
64
+ const claudeDir = path.join(root, ".claude");
54
65
  if (!fs.existsSync(claudeDir)) {
55
66
  fs.mkdirSync(claudeDir, { recursive: true });
56
67
  }
@@ -90,9 +101,8 @@ function installClaudeCodeHooks() {
90
101
  console.log(fmt.success(`Installed PreToolUse hooks to ${settingsPath}`));
91
102
  console.log(fmt.success(`Installed PostToolUse hooks to ${settingsPath}`));
92
103
  }
93
- function installCodexHooks() {
94
- const homeDir = os.homedir();
95
- const codexDir = path.join(homeDir, ".codex");
104
+ function installCodexHooks(root) {
105
+ const codexDir = path.join(root, ".codex");
96
106
  if (!fs.existsSync(codexDir)) {
97
107
  fs.mkdirSync(codexDir, { recursive: true });
98
108
  }
@@ -123,9 +133,8 @@ function installCodexHooks() {
123
133
  fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
124
134
  console.log(fmt.success(`Installed hooks to ${hooksPath}`));
125
135
  }
126
- function installCursorHooks() {
127
- const homeDir = os.homedir();
128
- const cursorDir = path.join(homeDir, ".cursor");
136
+ function installCursorHooks(root) {
137
+ const cursorDir = path.join(root, ".cursor");
129
138
  if (!fs.existsSync(cursorDir)) {
130
139
  fs.mkdirSync(cursorDir, { recursive: true });
131
140
  }
@@ -155,9 +164,8 @@ function installCursorHooks() {
155
164
  fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
156
165
  console.log(fmt.success(`Installed hooks to ${hooksPath}`));
157
166
  }
158
- function installGeminiHooks() {
159
- const homeDir = os.homedir();
160
- const geminiDir = path.join(homeDir, ".gemini");
167
+ function installGeminiHooks(root) {
168
+ const geminiDir = path.join(root, ".gemini");
161
169
  if (!fs.existsSync(geminiDir)) {
162
170
  fs.mkdirSync(geminiDir, { recursive: true });
163
171
  }
@@ -191,9 +199,8 @@ function installGeminiHooks() {
191
199
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
192
200
  console.log(fmt.success(`Installed hooks to ${settingsPath}`));
193
201
  }
194
- function installWindsurfHooks() {
195
- const homeDir = os.homedir();
196
- const windsurfDir = path.join(homeDir, ".windsurf");
202
+ function installWindsurfHooks(root) {
203
+ const windsurfDir = path.join(root, ".windsurf");
197
204
  if (!fs.existsSync(windsurfDir)) {
198
205
  fs.mkdirSync(windsurfDir, { recursive: true });
199
206
  }
@@ -227,9 +234,8 @@ function installWindsurfHooks() {
227
234
  fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
228
235
  console.log(fmt.success(`Installed hooks to ${hooksPath}`));
229
236
  }
230
- function installContinueDevHooks() {
231
- const homeDir = os.homedir();
232
- const continueDir = path.join(homeDir, ".continue");
237
+ function installContinueDevHooks(root) {
238
+ const continueDir = path.join(root, ".continue");
233
239
  if (!fs.existsSync(continueDir)) {
234
240
  fs.mkdirSync(continueDir, { recursive: true });
235
241
  }
@@ -267,9 +273,8 @@ const RAFTER_MCP_ENTRY = {
267
273
  /**
268
274
  * Install MCP server config for Gemini CLI (~/.gemini/settings.json)
269
275
  */
270
- function installGeminiMcp() {
271
- const homeDir = os.homedir();
272
- const geminiDir = path.join(homeDir, ".gemini");
276
+ function installGeminiMcp(root) {
277
+ const geminiDir = path.join(root, ".gemini");
273
278
  const settingsPath = path.join(geminiDir, "settings.json");
274
279
  if (!fs.existsSync(geminiDir)) {
275
280
  fs.mkdirSync(geminiDir, { recursive: true });
@@ -293,9 +298,8 @@ function installGeminiMcp() {
293
298
  /**
294
299
  * Install MCP server config for Cursor (~/.cursor/mcp.json)
295
300
  */
296
- function installCursorMcp() {
297
- const homeDir = os.homedir();
298
- const cursorDir = path.join(homeDir, ".cursor");
301
+ function installCursorMcp(root) {
302
+ const cursorDir = path.join(root, ".cursor");
299
303
  const mcpPath = path.join(cursorDir, "mcp.json");
300
304
  if (!fs.existsSync(cursorDir)) {
301
305
  fs.mkdirSync(cursorDir, { recursive: true });
@@ -319,9 +323,8 @@ function installCursorMcp() {
319
323
  /**
320
324
  * Install MCP server config for Windsurf (~/.codeium/windsurf/mcp_config.json)
321
325
  */
322
- function installWindsurfMcp() {
323
- const homeDir = os.homedir();
324
- const windsurfDir = path.join(homeDir, ".codeium", "windsurf");
326
+ function installWindsurfMcp(root) {
327
+ const windsurfDir = path.join(root, ".codeium", "windsurf");
325
328
  const mcpPath = path.join(windsurfDir, "mcp_config.json");
326
329
  if (!fs.existsSync(windsurfDir)) {
327
330
  fs.mkdirSync(windsurfDir, { recursive: true });
@@ -345,9 +348,8 @@ function installWindsurfMcp() {
345
348
  /**
346
349
  * Install MCP server config for Continue.dev (~/.continue/config.json)
347
350
  */
348
- function installContinueDevMcp() {
349
- const homeDir = os.homedir();
350
- const continueDir = path.join(homeDir, ".continue");
351
+ function installContinueDevMcp(root) {
352
+ const continueDir = path.join(root, ".continue");
351
353
  const configPath = path.join(continueDir, "config.json");
352
354
  if (!fs.existsSync(continueDir)) {
353
355
  fs.mkdirSync(continueDir, { recursive: true });
@@ -384,9 +386,8 @@ function installContinueDevMcp() {
384
386
  * Install MCP config for Aider (~/.aider.conf.yml)
385
387
  * Aider uses YAML config with mcpServers list
386
388
  */
387
- function installAiderMcp() {
388
- const homeDir = os.homedir();
389
- const configPath = path.join(homeDir, ".aider.conf.yml");
389
+ function installAiderMcp(root) {
390
+ const configPath = path.join(root, ".aider.conf.yml");
390
391
  // Aider's YAML config is simple — we append the MCP flag if not present
391
392
  let content = "";
392
393
  if (fs.existsSync(configPath)) {
@@ -403,73 +404,31 @@ function installAiderMcp() {
403
404
  console.log(fmt.success(`Installed Rafter MCP server to ${configPath}`));
404
405
  return true;
405
406
  }
406
- async function installClaudeCodeSkills() {
407
- const homeDir = os.homedir();
408
- const claudeSkillsDir = path.join(homeDir, ".claude", "skills");
409
- // Ensure .claude/skills directory exists
410
- if (!fs.existsSync(claudeSkillsDir)) {
411
- fs.mkdirSync(claudeSkillsDir, { recursive: true });
412
- }
413
- // Install Backend Skill
414
- const backendSkillDir = path.join(claudeSkillsDir, "rafter");
415
- const backendSkillPath = path.join(backendSkillDir, "SKILL.md");
416
- const backendTemplatePath = path.join(__dirname, "..", "..", "..", "resources", "skills", "rafter", "SKILL.md");
417
- if (!fs.existsSync(backendSkillDir)) {
418
- fs.mkdirSync(backendSkillDir, { recursive: true });
419
- }
420
- if (fs.existsSync(backendTemplatePath)) {
421
- fs.copyFileSync(backendTemplatePath, backendSkillPath);
422
- console.log(fmt.success(`Installed Rafter Backend skill to ${backendSkillPath}`));
423
- }
424
- else {
425
- console.log(fmt.warning(`Backend skill template not found at ${backendTemplatePath}`));
426
- }
427
- // Install Agent Security Skill
428
- const agentSkillDir = path.join(claudeSkillsDir, "rafter-agent-security");
429
- const agentSkillPath = path.join(agentSkillDir, "SKILL.md");
430
- const agentTemplatePath = path.join(__dirname, "..", "..", "..", "resources", "skills", "rafter-agent-security", "SKILL.md");
431
- if (!fs.existsSync(agentSkillDir)) {
432
- fs.mkdirSync(agentSkillDir, { recursive: true });
407
+ function installSkillsTo(skillsDir) {
408
+ if (!fs.existsSync(skillsDir)) {
409
+ fs.mkdirSync(skillsDir, { recursive: true });
433
410
  }
434
- if (fs.existsSync(agentTemplatePath)) {
435
- fs.copyFileSync(agentTemplatePath, agentSkillPath);
436
- console.log(fmt.success(`Installed Rafter Agent Security skill to ${agentSkillPath}`));
437
- }
438
- else {
439
- console.log(fmt.warning(`Agent Security skill template not found at ${agentTemplatePath}`));
411
+ for (const skill of AGENT_SKILLS) {
412
+ const destDir = path.join(skillsDir, skill.name);
413
+ const destPath = path.join(destDir, "SKILL.md");
414
+ const srcPath = path.join(__dirname, "..", "..", "..", "resources", "skills", skill.name, "SKILL.md");
415
+ if (!fs.existsSync(destDir)) {
416
+ fs.mkdirSync(destDir, { recursive: true });
417
+ }
418
+ if (fs.existsSync(srcPath)) {
419
+ fs.copyFileSync(srcPath, destPath);
420
+ console.log(fmt.success(`Installed ${skill.description} skill to ${destPath}`));
421
+ }
422
+ else {
423
+ console.log(fmt.warning(`${skill.description} skill template not found at ${srcPath}`));
424
+ }
440
425
  }
441
426
  }
442
- function installCodexSkills() {
443
- const homeDir = os.homedir();
444
- const agentsSkillsDir = path.join(homeDir, ".agents", "skills");
445
- // Install Backend Skill
446
- const backendDir = path.join(agentsSkillsDir, "rafter");
447
- const backendSkillPath = path.join(backendDir, "SKILL.md");
448
- const backendTemplatePath = path.join(__dirname, "..", "..", "..", "resources", "skills", "rafter", "SKILL.md");
449
- if (!fs.existsSync(backendDir)) {
450
- fs.mkdirSync(backendDir, { recursive: true });
451
- }
452
- if (fs.existsSync(backendTemplatePath)) {
453
- fs.copyFileSync(backendTemplatePath, backendSkillPath);
454
- console.log(fmt.success(`Installed Rafter Backend skill to ${backendSkillPath}`));
455
- }
456
- else {
457
- console.log(fmt.warning(`Backend skill template not found at ${backendTemplatePath}`));
458
- }
459
- // Install Agent Security Skill
460
- const agentDir = path.join(agentsSkillsDir, "rafter-agent-security");
461
- const agentSkillPath = path.join(agentDir, "SKILL.md");
462
- const agentTemplatePath = path.join(__dirname, "..", "..", "..", "resources", "skills", "rafter-agent-security", "SKILL.md");
463
- if (!fs.existsSync(agentDir)) {
464
- fs.mkdirSync(agentDir, { recursive: true });
465
- }
466
- if (fs.existsSync(agentTemplatePath)) {
467
- fs.copyFileSync(agentTemplatePath, agentSkillPath);
468
- console.log(fmt.success(`Installed Rafter Agent Security skill to ${agentSkillPath}`));
469
- }
470
- else {
471
- console.log(fmt.warning(`Agent Security skill template not found at ${agentTemplatePath}`));
472
- }
427
+ async function installClaudeCodeSkills(root) {
428
+ installSkillsTo(path.join(root, ".claude", "skills"));
429
+ }
430
+ function installCodexSkills(root) {
431
+ installSkillsTo(path.join(root, ".agents", "skills"));
473
432
  }
474
433
  async function askYesNo(question, defaultYes = true) {
475
434
  const rl = createInterface({ input: process.stdin, output: process.stderr });
@@ -501,30 +460,43 @@ export function createInitCommand() {
501
460
  .option("--all", "Install all detected integrations and download Gitleaks")
502
461
  .option("-i, --interactive", "Guided setup — prompts for each detected integration")
503
462
  .option("--update", "Re-download gitleaks and reinstall integrations without resetting config")
463
+ .option("--local", "Install integration configs project-locally (in CWD) instead of user-globally. " +
464
+ "Supported for Claude Code, Codex, Gemini, Cursor. Other platforms are skipped in local mode.")
504
465
  .action(async (opts) => {
505
466
  console.log(fmt.header("Rafter Agent Security Setup"));
506
467
  console.log(fmt.divider());
507
468
  console.log();
508
469
  const manager = new ConfigManager();
509
- // Detect environments
510
- const hasOpenClaw = fs.existsSync(path.join(os.homedir(), ".openclaw"));
511
- const hasClaudeCode = fs.existsSync(path.join(os.homedir(), ".claude"));
512
- const hasCodex = fs.existsSync(path.join(os.homedir(), ".codex"));
513
- const hasGemini = fs.existsSync(path.join(os.homedir(), ".gemini"));
514
- const hasCursor = fs.existsSync(path.join(os.homedir(), ".cursor"));
515
- const hasWindsurf = fs.existsSync(path.join(os.homedir(), ".codeium", "windsurf"));
516
- const hasContinueDev = fs.existsSync(path.join(os.homedir(), ".continue"));
517
- const hasAider = fs.existsSync(path.join(os.homedir(), ".aider.conf.yml"));
518
- // Resolve opt-in flags (--all enables all detected, --interactive prompts)
519
- let wantOpenClaw = opts.withOpenclaw || opts.all;
470
+ const root = opts.local ? process.cwd() : os.homedir();
471
+ const scope = opts.local ? "project" : "user";
472
+ if (opts.local) {
473
+ console.log(fmt.info(`Project-local install — writing configs under ${root}`));
474
+ }
475
+ // Platforms supported in --local scope: Claude Code, Codex, Gemini, Cursor.
476
+ // Windsurf, Continue.dev, Aider are skipped in --local because their
477
+ // project-local config story is not established in their CLIs today.
478
+ // Detect environments. In local scope, don't probe user-global paths —
479
+ // the user must opt in explicitly via --with-<platform>.
480
+ const hasOpenClaw = scope === "user" && fs.existsSync(path.join(os.homedir(), ".openclaw"));
481
+ const hasClaudeCode = scope === "user" && fs.existsSync(path.join(os.homedir(), ".claude"));
482
+ const hasCodex = scope === "user" && fs.existsSync(path.join(os.homedir(), ".codex"));
483
+ const hasGemini = scope === "user" && fs.existsSync(path.join(os.homedir(), ".gemini"));
484
+ const hasCursor = scope === "user" && fs.existsSync(path.join(os.homedir(), ".cursor"));
485
+ const hasWindsurf = scope === "user" && fs.existsSync(path.join(os.homedir(), ".codeium", "windsurf"));
486
+ const hasContinueDev = scope === "user" && fs.existsSync(path.join(os.homedir(), ".continue"));
487
+ const hasAider = scope === "user" && fs.existsSync(path.join(os.homedir(), ".aider.conf.yml"));
488
+ // Resolve opt-in flags (--all enables all detected, --interactive prompts).
489
+ // In --local scope, --all is restricted to platforms that have a project-local
490
+ // config story (claudeCode, codex, gemini, cursor). The rest require user scope.
491
+ let wantOpenClaw = opts.withOpenclaw || (opts.all && !opts.local);
520
492
  let wantClaudeCode = opts.withClaudeCode || opts.all;
521
493
  let wantCodex = opts.withCodex || opts.all;
522
494
  let wantGemini = opts.withGemini || opts.all;
523
495
  let wantCursor = opts.withCursor || opts.all;
524
- let wantWindsurf = opts.withWindsurf || opts.all;
525
- let wantContinue = opts.withContinue || opts.all;
526
- let wantAider = opts.withAider || opts.all;
527
- let wantGitleaks = opts.withGitleaks || opts.all;
496
+ let wantWindsurf = opts.withWindsurf || (opts.all && !opts.local);
497
+ let wantContinue = opts.withContinue || (opts.all && !opts.local);
498
+ let wantAider = opts.withAider || (opts.all && !opts.local);
499
+ let wantGitleaks = opts.withGitleaks || (opts.all && !opts.local);
528
500
  // Interactive mode: prompt for each detected integration
529
501
  if (opts.interactive && !opts.all) {
530
502
  console.log();
@@ -574,23 +546,26 @@ export function createInitCommand() {
574
546
  else {
575
547
  console.log(fmt.info("No agent environments detected"));
576
548
  }
577
- // Warn about requested but undetected environments
578
- if (wantOpenClaw && !hasOpenClaw)
579
- console.log(fmt.warning("OpenClaw requested but not detected (~/.openclaw not found)"));
580
- if (wantClaudeCode && !hasClaudeCode)
581
- console.log(fmt.warning("Claude Code requested but not detected (~/.claude not found)"));
582
- if (wantCodex && !hasCodex)
583
- console.log(fmt.warning("Codex CLI requested but not detected (~/.codex not found)"));
584
- if (wantGemini && !hasGemini)
585
- console.log(fmt.warning("Gemini CLI requested but not detected (~/.gemini not found)"));
586
- if (wantCursor && !hasCursor)
587
- console.log(fmt.warning("Cursor requested but not detected (~/.cursor not found)"));
588
- if (wantWindsurf && !hasWindsurf)
589
- console.log(fmt.warning("Windsurf requested but not detected (~/.codeium/windsurf not found)"));
590
- if (wantContinue && !hasContinueDev)
591
- console.log(fmt.warning("Continue.dev requested but not detected (~/.continue not found)"));
592
- if (wantAider && !hasAider)
593
- console.log(fmt.warning("Aider requested but not detected (~/.aider.conf.yml not found)"));
549
+ // Warn about requested but undetected environments (user scope only —
550
+ // in --local scope we create the directories in CWD as needed).
551
+ if (scope === "user") {
552
+ if (wantOpenClaw && !hasOpenClaw)
553
+ console.log(fmt.warning("OpenClaw requested but not detected (~/.openclaw not found)"));
554
+ if (wantClaudeCode && !hasClaudeCode)
555
+ console.log(fmt.warning("Claude Code requested but not detected (~/.claude not found)"));
556
+ if (wantCodex && !hasCodex)
557
+ console.log(fmt.warning("Codex CLI requested but not detected (~/.codex not found)"));
558
+ if (wantGemini && !hasGemini)
559
+ console.log(fmt.warning("Gemini CLI requested but not detected (~/.gemini not found)"));
560
+ if (wantCursor && !hasCursor)
561
+ console.log(fmt.warning("Cursor requested but not detected (~/.cursor not found)"));
562
+ if (wantWindsurf && !hasWindsurf)
563
+ console.log(fmt.warning("Windsurf requested but not detected (~/.codeium/windsurf not found)"));
564
+ if (wantContinue && !hasContinueDev)
565
+ console.log(fmt.warning("Continue.dev requested but not detected (~/.continue not found)"));
566
+ if (wantAider && !hasAider)
567
+ console.log(fmt.warning("Aider requested but not detected (~/.aider.conf.yml not found)"));
568
+ }
594
569
  // Initialize directory structure
595
570
  try {
596
571
  await manager.initialize();
@@ -697,13 +672,20 @@ export function createInitCommand() {
697
672
  }
698
673
  }
699
674
  }
675
+ // Helper: warn that a platform is not supported in --local mode.
676
+ const localUnsupported = (label) => {
677
+ console.log(fmt.warning(`${label} is not supported in --local mode yet. Skipping. ` +
678
+ `Re-run without --local to install for this platform user-globally.`));
679
+ };
700
680
  // Install Claude Code skills + hooks if opted in
681
+ // When --with-claude-code is explicitly passed (or --local), install even if <root>/.claude doesn't exist yet
701
682
  let claudeCodeOk = false;
702
- if (hasClaudeCode && wantClaudeCode) {
683
+ if ((hasClaudeCode || opts.withClaudeCode || (opts.local && wantClaudeCode)) && wantClaudeCode) {
703
684
  try {
704
- await installClaudeCodeSkills();
705
- installClaudeCodeHooks();
706
- manager.set("agent.environments.claudeCode.enabled", true);
685
+ await installClaudeCodeSkills(root);
686
+ installClaudeCodeHooks(root);
687
+ if (scope === "user")
688
+ manager.set("agent.environments.claudeCode.enabled", true);
707
689
  claudeCodeOk = true;
708
690
  }
709
691
  catch (e) {
@@ -712,11 +694,12 @@ export function createInitCommand() {
712
694
  }
713
695
  // Install Codex CLI skills + hooks if opted in
714
696
  let codexOk = false;
715
- if (hasCodex && wantCodex) {
697
+ if ((hasCodex || (opts.local && wantCodex)) && wantCodex) {
716
698
  try {
717
- installCodexSkills();
718
- installCodexHooks();
719
- manager.set("agent.environments.codex.enabled", true);
699
+ installCodexSkills(root);
700
+ installCodexHooks(root);
701
+ if (scope === "user")
702
+ manager.set("agent.environments.codex.enabled", true);
720
703
  codexOk = true;
721
704
  }
722
705
  catch (e) {
@@ -725,11 +708,11 @@ export function createInitCommand() {
725
708
  }
726
709
  // Install Gemini CLI MCP + hooks if opted in
727
710
  let geminiOk = false;
728
- if (hasGemini && wantGemini) {
711
+ if ((hasGemini || (opts.local && wantGemini)) && wantGemini) {
729
712
  try {
730
- geminiOk = installGeminiMcp();
731
- installGeminiHooks();
732
- if (geminiOk)
713
+ geminiOk = installGeminiMcp(root);
714
+ installGeminiHooks(root);
715
+ if (geminiOk && scope === "user")
733
716
  manager.set("agent.environments.gemini.enabled", true);
734
717
  }
735
718
  catch (e) {
@@ -738,11 +721,11 @@ export function createInitCommand() {
738
721
  }
739
722
  // Install Cursor MCP + hooks if opted in
740
723
  let cursorOk = false;
741
- if (hasCursor && wantCursor) {
724
+ if ((hasCursor || (opts.local && wantCursor)) && wantCursor) {
742
725
  try {
743
- cursorOk = installCursorMcp();
744
- installCursorHooks();
745
- if (cursorOk)
726
+ cursorOk = installCursorMcp(root);
727
+ installCursorHooks(root);
728
+ if (cursorOk && scope === "user")
746
729
  manager.set("agent.environments.cursor.enabled", true);
747
730
  }
748
731
  catch (e) {
@@ -753,8 +736,8 @@ export function createInitCommand() {
753
736
  let windsurfOk = false;
754
737
  if (hasWindsurf && wantWindsurf) {
755
738
  try {
756
- windsurfOk = installWindsurfMcp();
757
- installWindsurfHooks();
739
+ windsurfOk = installWindsurfMcp(root);
740
+ installWindsurfHooks(root);
758
741
  if (windsurfOk)
759
742
  manager.set("agent.environments.windsurf.enabled", true);
760
743
  }
@@ -762,12 +745,15 @@ export function createInitCommand() {
762
745
  console.error(fmt.error(`Failed to install Windsurf integration: ${e}`));
763
746
  }
764
747
  }
748
+ else if (opts.local && wantWindsurf) {
749
+ localUnsupported("Windsurf");
750
+ }
765
751
  // Install Continue.dev MCP + hooks if opted in
766
752
  let continueOk = false;
767
753
  if (hasContinueDev && wantContinue) {
768
754
  try {
769
- continueOk = installContinueDevMcp();
770
- installContinueDevHooks();
755
+ continueOk = installContinueDevMcp(root);
756
+ installContinueDevHooks(root);
771
757
  if (continueOk)
772
758
  manager.set("agent.environments.continueDev.enabled", true);
773
759
  }
@@ -775,11 +761,14 @@ export function createInitCommand() {
775
761
  console.error(fmt.error(`Failed to install Continue.dev integration: ${e}`));
776
762
  }
777
763
  }
764
+ else if (opts.local && wantContinue) {
765
+ localUnsupported("Continue.dev");
766
+ }
778
767
  // Install Aider MCP if opted in
779
768
  let aiderOk = false;
780
769
  if (hasAider && wantAider) {
781
770
  try {
782
- aiderOk = installAiderMcp();
771
+ aiderOk = installAiderMcp(root);
783
772
  if (aiderOk)
784
773
  manager.set("agent.environments.aider.enabled", true);
785
774
  }
@@ -787,11 +776,14 @@ export function createInitCommand() {
787
776
  console.error(fmt.error(`Failed to install Aider integration: ${e}`));
788
777
  }
789
778
  }
779
+ else if (opts.local && wantAider) {
780
+ localUnsupported("Aider");
781
+ }
790
782
  // Install global instruction files for platforms that support them
791
783
  installGlobalInstructions({
792
784
  claudeCode: claudeCodeOk,
793
785
  cursor: cursorOk,
794
- });
786
+ }, root);
795
787
  console.log();
796
788
  console.log(fmt.success("Agent security initialized!"));
797
789
  console.log();
@@ -815,6 +807,13 @@ export function createInitCommand() {
815
807
  if (aiderOk)
816
808
  console.log(" - Restart Aider to load MCP server");
817
809
  }
810
+ else if (scope === "project") {
811
+ console.log("No integrations were installed. In --local mode, pass one or more opt-in flags:");
812
+ console.log(" rafter agent init --local --with-claude-code");
813
+ console.log(" rafter agent init --local --with-codex");
814
+ console.log(" rafter agent init --local --with-gemini");
815
+ console.log(" rafter agent init --local --with-cursor");
816
+ }
818
817
  else if (detected.length > 0) {
819
818
  console.log("No integrations were installed. To install, re-run with opt-in flags:");
820
819
  console.log(" rafter agent init --all # Install all detected");
@@ -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
  }