瀏覽代碼

feat(cli): add safe instructions-md init command

Add a genuinely useful /init command that creates a starter INSTRUCTIONS.md from the current repository shape without inventing unsupported setup flows. The scaffold pulls in real verification commands and repo-structure notes for this workspace, and it refuses to overwrite an existing INSTRUCTIONS.md.

This keeps the command honest and low-risk while moving the CLI closer to Claw Code's practical bootstrap surface.

Constraint: /init must be non-destructive and must not overwrite an existing INSTRUCTIONS.md

Constraint: Generated guidance must come from observable repo structure rather than placeholder text

Rejected: Interactive multi-step init workflow | too much unsupported UI/state machinery for this Rust CLI slice

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Keep generated INSTRUCTIONS.md templates concise and repo-derived; do not let /init drift into fake setup promises

Tested: cargo fmt; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace

Not-tested: manual /init invocation in a separate temporary repository without a preexisting INSTRUCTIONS.md
Yeachan-Heo 2 月之前
父節點
當前提交
41abf7dfd5
共有 2 個文件被更改,包括 99 次插入4 次删除
  1. 11 1
      rust/crates/commands/src/lib.rs
  2. 88 3
      rust/crates/rusty-claude-cli/src/main.rs

+ 11 - 1
rust/crates/commands/src/lib.rs

@@ -88,6 +88,11 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
         summary: "Inspect loaded Claude instruction memory files",
         argument_hint: None,
     },
+    SlashCommandSpec {
+        name: "init",
+        summary: "Create a starter CLAUDE.md for this repo",
+        argument_hint: None,
+    },
 ];
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -102,6 +107,7 @@ pub enum SlashCommand {
     Resume { session_path: Option<String> },
     Config,
     Memory,
+    Init,
     Unknown(String),
 }
 
@@ -132,6 +138,7 @@ impl SlashCommand {
             },
             "config" => Self::Config,
             "memory" => Self::Memory,
+            "init" => Self::Init,
             other => Self::Unknown(other.to_string()),
         })
     }
@@ -195,6 +202,7 @@ pub fn handle_slash_command(
         | SlashCommand::Resume { .. }
         | SlashCommand::Config
         | SlashCommand::Memory
+        | SlashCommand::Init
         | SlashCommand::Unknown(_) => None,
     }
 }
@@ -236,6 +244,7 @@ mod tests {
         );
         assert_eq!(SlashCommand::parse("/config"), Some(SlashCommand::Config));
         assert_eq!(SlashCommand::parse("/memory"), Some(SlashCommand::Memory));
+        assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));
     }
 
     #[test]
@@ -251,7 +260,8 @@ mod tests {
         assert!(help.contains("/resume <session-path>"));
         assert!(help.contains("/config"));
         assert!(help.contains("/memory"));
-        assert_eq!(slash_command_specs().len(), 10);
+        assert!(help.contains("/init"));
+        assert_eq!(slash_command_specs().len(), 11);
     }
 
     #[test]

+ 88 - 3
rust/crates/rusty-claude-cli/src/main.rs

@@ -2,6 +2,7 @@ mod input;
 mod render;
 
 use std::env;
+use std::fs;
 use std::io::{self, Write};
 use std::path::{Path, PathBuf};
 
@@ -282,6 +283,7 @@ fn run_resume_command(
         | SlashCommand::Model { .. }
         | SlashCommand::Permissions { .. }
         | SlashCommand::Clear
+        | SlashCommand::Init
         | SlashCommand::Unknown(_) => Err("unsupported resumed slash command".into()),
     }
 }
@@ -377,6 +379,7 @@ impl LiveCli {
             SlashCommand::Resume { session_path } => self.resume_session(session_path)?,
             SlashCommand::Config => Self::print_config()?,
             SlashCommand::Memory => Self::print_memory()?,
+            SlashCommand::Init => Self::run_init()?,
             SlashCommand::Unknown(name) => eprintln!("unknown slash command: /{name}"),
         }
         Ok(())
@@ -502,6 +505,11 @@ impl LiveCli {
         Ok(())
     }
 
+    fn run_init() -> Result<(), Box<dyn std::error::Error>> {
+        println!("{}", init_claude_md()?);
+        Ok(())
+    }
+
     fn compact(&mut self) -> Result<(), Box<dyn std::error::Error>> {
         let result = self.runtime.compact(CompactionConfig::default());
         let removed = result.removed_message_count;
@@ -614,6 +622,74 @@ fn render_memory_report() -> Result<String, Box<dyn std::error::Error>> {
     ))
 }
 
+fn init_claude_md() -> Result<String, Box<dyn std::error::Error>> {
+    let cwd = env::current_dir()?;
+    let claude_md = cwd.join("CLAUDE.md");
+    if claude_md.exists() {
+        return Ok(format!(
+            "init: skipped because {} already exists",
+            claude_md.display()
+        ));
+    }
+
+    let content = render_init_claude_md(&cwd);
+    fs::write(&claude_md, content)?;
+    Ok(format!("init: created {}", claude_md.display()))
+}
+
+fn render_init_claude_md(cwd: &Path) -> String {
+    let mut lines = vec![
+        "# CLAUDE.md".to_string(),
+        String::new(),
+        "This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.".to_string(),
+        String::new(),
+    ];
+
+    let mut command_lines = Vec::new();
+    if cwd.join("rust").join("Cargo.toml").is_file() {
+        command_lines.push("- Run Rust verification from `rust/`: `cargo fmt`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`".to_string());
+    } else if cwd.join("Cargo.toml").is_file() {
+        command_lines.push("- Run Rust verification from the repo root: `cargo fmt`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`".to_string());
+    }
+    if cwd.join("tests").is_dir() && cwd.join("src").is_dir() {
+        command_lines.push("- `src/` and `tests/` are also present; check those surfaces before removing or renaming Python-era compatibility assets.".to_string());
+    }
+    if !command_lines.is_empty() {
+        lines.push("## Verification".to_string());
+        lines.extend(command_lines);
+        lines.push(String::new());
+    }
+
+    let mut structure_lines = Vec::new();
+    if cwd.join("rust").is_dir() {
+        structure_lines.push(
+            "- `rust/` contains the Rust workspace and the active CLI/runtime implementation."
+                .to_string(),
+        );
+    }
+    if cwd.join("src").is_dir() {
+        structure_lines.push("- `src/` contains the older Python-first workspace artifacts referenced by the repo history and tests.".to_string());
+    }
+    if cwd.join("tests").is_dir() {
+        structure_lines.push("- `tests/` exercises compatibility and porting behavior across the repository surfaces.".to_string());
+    }
+    if !structure_lines.is_empty() {
+        lines.push("## Repository shape".to_string());
+        lines.extend(structure_lines);
+        lines.push(String::new());
+    }
+
+    lines.push("## Working agreement".to_string());
+    lines.push("- Prefer small, reviewable Rust changes and keep slash-command behavior aligned between the shared command registry and the CLI entrypoints.".to_string());
+    lines.push("- Do not overwrite existing CLAUDE.md content automatically; update it intentionally when repo workflows change.".to_string());
+    lines.push(String::new());
+
+    lines.join(
+        "
+",
+    )
+}
+
 fn normalize_permission_mode(mode: &str) -> Option<&'static str> {
     match mode.trim() {
         "read-only" => Some("read-only"),
@@ -951,11 +1027,11 @@ fn print_help() {
 #[cfg(test)]
 mod tests {
     use super::{
-        format_status_line, normalize_permission_mode, parse_args, render_repl_help, CliAction,
-        SlashCommand, DEFAULT_MODEL,
+        format_status_line, normalize_permission_mode, parse_args, render_init_claude_md,
+        render_repl_help, CliAction, SlashCommand, DEFAULT_MODEL,
     };
     use runtime::{ContentBlock, ConversationMessage, MessageRole};
-    use std::path::PathBuf;
+    use std::path::{Path, PathBuf};
 
     #[test]
     fn defaults_to_repl_when_no_args() {
@@ -1029,6 +1105,7 @@ mod tests {
         assert!(help.contains("/resume <session-path>"));
         assert!(help.contains("/config"));
         assert!(help.contains("/memory"));
+        assert!(help.contains("/init"));
         assert!(help.contains("/exit"));
     }
 
@@ -1084,6 +1161,14 @@ mod tests {
         );
         assert_eq!(SlashCommand::parse("/config"), Some(SlashCommand::Config));
         assert_eq!(SlashCommand::parse("/memory"), Some(SlashCommand::Memory));
+        assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));
+    }
+
+    #[test]
+    fn init_template_mentions_detected_rust_workspace() {
+        let rendered = render_init_claude_md(Path::new("."));
+        assert!(rendered.contains("# CLAUDE.md"));
+        assert!(rendered.contains("cargo clippy --workspace --all-targets -- -D warnings"));
     }
 
     #[test]