Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/validate-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ on:
- 'src/**/*.rs'
- 'Cargo.toml'
- '**.md'
- '.claude/hooks/*.sh'
push:
branches:
- master
Expand Down Expand Up @@ -54,15 +53,16 @@ jobs:

- name: Verify hook coverage
run: |
HOOK_FILE=".claude/hooks/rtk-rewrite.sh"
if [ ! -f "$HOOK_FILE" ]; then
echo "❌ Hook file not found: $HOOK_FILE"
# Check native hook module (src/hook/) for command coverage
HOOK_DIR="src/hook"
if [ ! -d "$HOOK_DIR" ]; then
echo "❌ Hook module directory not found: $HOOK_DIR"
exit 1
fi

for cmd in ruff pytest pip "go " golangci; do
if ! grep -q "$cmd" "$HOOK_FILE"; then
echo "❌ Hook missing rewrite for: $cmd"
if ! grep -rq "$cmd" "$HOOK_DIR"; then
echo "❌ Hook module missing rewrite for: $cmd"
exit 1
fi
done
Expand Down
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,11 @@ SHARED utils.rs Helpers N/A ✓
tee.rs Full output recovery N/A ✓
```

**Total: 48 modules** (30 command modules + 18 infrastructure modules)
**Total: 51 modules** (33 command modules + 18 infrastructure modules)

### Module Count Breakdown

- **Command Modules**: 29 (directly exposed to users)
- **Command Modules**: 33 (directly exposed to users)
- **Infrastructure Modules**: 18 (utils, filter, tracking, tee, config, init, gain, etc.)
- **Git Commands**: 7 operations (status, diff, log, add, commit, push, branch/checkout)
- **JS/TS Tooling**: 8 modules (modern frontend/fullstack development)
Expand Down
7 changes: 7 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
fn main() {
// Windows default stack is 1 MB, which overflows in debug builds
// due to the large Commands enum (30+ Clap variants) and match routing.
// Set 8 MB stack to match Unix defaults.
#[cfg(target_os = "windows")]
println!("cargo:rustc-link-arg=/STACK:8388608");
}
16 changes: 8 additions & 8 deletions scripts/validate-docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,18 @@ for cmd in "${PYTHON_GO_CMDS[@]}"; do
done
echo "✅ Python/Go commands: documented in README.md and CLAUDE.md"

# 4. Hooks cohérents avec doc
HOOK_FILE=".claude/hooks/rtk-rewrite.sh"
if [ -f "$HOOK_FILE" ]; then
echo "🪝 Checking hook rewrites..."
# 4. Hooks cohérents avec doc (native hook module)
HOOK_DIR="src/hook"
if [ -d "$HOOK_DIR" ]; then
echo "🪝 Checking native hook rewrites..."
for cmd in "${PYTHON_GO_CMDS[@]}"; do
if ! grep -q "$cmd" "$HOOK_FILE"; then
echo "⚠️ Hook may not rewrite $cmd (verify manually)"
if ! grep -rq "$cmd" "$HOOK_DIR"; then
echo "⚠️ Hook module may not rewrite $cmd (verify manually)"
fi
done
echo "✅ Hook file exists and mentions Python/Go commands"
echo "✅ Hook module exists and mentions Python/Go commands"
else
echo "⚠️ Hook file not found: $HOOK_FILE"
echo "⚠️ Hook module directory not found: $HOOK_DIR"
fi

echo ""
Expand Down
58 changes: 58 additions & 0 deletions src/hook/cargo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use lazy_static::lazy_static;
use regex::Regex;

lazy_static! {
/// Cargo toolchain prefix: cargo +nightly ...
static ref CARGO_TOOLCHAIN_RE: Regex =
Regex::new(r"^\+\S+\s+").unwrap();
}

pub fn try_rewrite_cargo(match_cmd: &str, cmd_body: &str) -> Option<String> {
let after_cargo = match_cmd.strip_prefix("cargo ").unwrap_or("");
let after_toolchain = CARGO_TOOLCHAIN_RE.replace(after_cargo, "");
let subcmd = after_toolchain.split_whitespace().next().unwrap_or("");
match subcmd {
"test" | "build" | "clippy" | "check" | "install" | "fmt" => {
Some(format!("rtk {}", cmd_body))
}
_ => None,
}
}

#[cfg(test)]
mod tests {
use super::super::rewrite_chain;

fn rewrite(cmd: &str) -> Option<String> {
if cmd.contains("<<") {
return None;
}
rewrite_chain(cmd)
}

#[test]
fn test_cargo_test() {
assert_eq!(rewrite("cargo test"), Some("rtk cargo test".into()));
}

#[test]
fn test_cargo_build_release() {
assert_eq!(
rewrite("cargo build --release"),
Some("rtk cargo build --release".into())
);
}

#[test]
fn test_cargo_with_toolchain() {
assert_eq!(
rewrite("cargo +nightly build"),
Some("rtk cargo +nightly build".into())
);
}

#[test]
fn test_cargo_run_no_match() {
assert_eq!(rewrite("cargo run"), None);
}
}
79 changes: 79 additions & 0 deletions src/hook/containers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use super::helpers::replace_prefix;
use lazy_static::lazy_static;
use regex::Regex;

lazy_static! {
/// Strips docker global flags: -H, --context, --config
static ref DOCKER_GLOBAL_FLAGS_RE: Regex =
Regex::new(r"(?:(-H|--context|--config)\s+\S+\s*|--[a-z-]+=\S+\s*)").unwrap();

/// Strips kubectl global flags: --context, --kubeconfig, --namespace, -n
static ref KUBECTL_GLOBAL_FLAGS_RE: Regex =
Regex::new(r"(?:(--context|--kubeconfig|--namespace|-n)\s+\S+\s*|--[a-z-]+=\S+\s*)").unwrap();
}

pub fn try_rewrite_docker(match_cmd: &str, cmd_body: &str) -> Option<String> {
let after_docker = match_cmd.strip_prefix("docker ").unwrap_or("");

// docker compose → always rewrite
if after_docker.starts_with("compose") {
return Some(replace_prefix(cmd_body, "docker ", "rtk docker "));
}

// Strip docker global flags
let stripped = DOCKER_GLOBAL_FLAGS_RE.replace_all(after_docker, "");
let stripped = stripped.trim_start();
let subcmd = stripped.split_whitespace().next().unwrap_or("");
match subcmd {
"ps" | "images" | "logs" | "run" | "build" | "exec" => {
Some(replace_prefix(cmd_body, "docker ", "rtk docker "))
}
_ => None,
}
}

pub fn try_rewrite_kubectl(match_cmd: &str, cmd_body: &str) -> Option<String> {
let after_kubectl = match_cmd.strip_prefix("kubectl ").unwrap_or("");
let stripped = KUBECTL_GLOBAL_FLAGS_RE.replace_all(after_kubectl, "");
let stripped = stripped.trim_start();
let subcmd = stripped.split_whitespace().next().unwrap_or("");
match subcmd {
"get" | "logs" | "describe" | "apply" => {
Some(replace_prefix(cmd_body, "kubectl ", "rtk kubectl "))
}
_ => None,
}
}

#[cfg(test)]
mod tests {
use super::super::rewrite_chain;

fn rewrite(cmd: &str) -> Option<String> {
if cmd.contains("<<") {
return None;
}
rewrite_chain(cmd)
}

#[test]
fn test_docker_ps() {
assert_eq!(rewrite("docker ps"), Some("rtk docker ps".into()));
}

#[test]
fn test_docker_compose() {
assert_eq!(
rewrite("docker compose up -d"),
Some("rtk docker compose up -d".into())
);
}

#[test]
fn test_kubectl_get() {
assert_eq!(
rewrite("kubectl get pods"),
Some("rtk kubectl get pods".into())
);
}
}
149 changes: 149 additions & 0 deletions src/hook/files.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use super::helpers::replace_prefix;
use lazy_static::lazy_static;
use regex::Regex;

lazy_static! {
/// head -N file pattern
static ref HEAD_DASH_N_RE: Regex =
Regex::new(r"^head\s+-(\d+)\s+(.+)$").unwrap();

/// head --lines=N file pattern
static ref HEAD_LINES_RE: Regex =
Regex::new(r"^head\s+--lines=(\d+)\s+(.+)$").unwrap();
}

pub fn try_rewrite_head(match_cmd: &str) -> Option<String> {
// head -N file → rtk read file --max-lines N
if let Some(caps) = HEAD_DASH_N_RE.captures(match_cmd) {
let lines = &caps[1];
let file = &caps[2];
return Some(format!("rtk read {} --max-lines {}", file, lines));
}
// head --lines=N file
if let Some(caps) = HEAD_LINES_RE.captures(match_cmd) {
let lines = &caps[1];
let file = &caps[2];
return Some(format!("rtk read {} --max-lines {}", file, lines));
}
None
}

/// Try to rewrite file operation commands (cat, grep, ls, tree, find, diff, curl, wget).
/// Returns Some(rewritten) if matched, None otherwise.
pub fn try_rewrite_file_cmd(match_cmd: &str, cmd_body: &str) -> Option<String> {
if match_cmd.starts_with("cat ") {
return Some(replace_prefix(cmd_body, "cat ", "rtk read "));
}
if match_cmd.starts_with("rg ") {
return Some(replace_prefix(cmd_body, "rg ", "rtk grep "));
}
if match_cmd.starts_with("grep ") {
return Some(replace_prefix(cmd_body, "grep ", "rtk grep "));
}
if match_cmd == "ls" || match_cmd.starts_with("ls ") {
return Some(replace_prefix(cmd_body, "ls", "rtk ls"));
}
if match_cmd == "tree" || match_cmd.starts_with("tree ") {
return Some(replace_prefix(cmd_body, "tree", "rtk tree"));
}
if match_cmd.starts_with("find ") {
return Some(replace_prefix(cmd_body, "find ", "rtk find "));
}
if match_cmd.starts_with("diff ") {
return Some(replace_prefix(cmd_body, "diff ", "rtk diff "));
}
if match_cmd.starts_with("head ") {
return try_rewrite_head(match_cmd);
}
// Network commands
if match_cmd.starts_with("curl ") {
return Some(replace_prefix(cmd_body, "curl ", "rtk curl "));
}
if match_cmd.starts_with("wget ") {
return Some(replace_prefix(cmd_body, "wget ", "rtk wget "));
}
None
}

#[cfg(test)]
mod tests {
use super::super::rewrite_chain;

fn rewrite(cmd: &str) -> Option<String> {
if cmd.contains("<<") {
return None;
}
rewrite_chain(cmd)
}

#[test]
fn test_cat_to_read() {
assert_eq!(
rewrite("cat src/main.rs"),
Some("rtk read src/main.rs".into())
);
}

#[test]
fn test_rg_to_grep() {
assert_eq!(
rewrite("rg pattern src/"),
Some("rtk grep pattern src/".into())
);
}

#[test]
fn test_grep_to_rtk_grep() {
assert_eq!(rewrite("grep -r TODO ."), Some("rtk grep -r TODO .".into()));
}

#[test]
fn test_ls() {
assert_eq!(rewrite("ls -la"), Some("rtk ls -la".into()));
}

#[test]
fn test_ls_bare() {
assert_eq!(rewrite("ls"), Some("rtk ls".into()));
}

#[test]
fn test_find() {
assert_eq!(
rewrite("find . -name '*.rs'"),
Some("rtk find . -name '*.rs'".into())
);
}

#[test]
fn test_head_dash_n() {
assert_eq!(
rewrite("head -20 src/main.rs"),
Some("rtk read src/main.rs --max-lines 20".into())
);
}

#[test]
fn test_head_lines_eq() {
assert_eq!(
rewrite("head --lines=50 README.md"),
Some("rtk read README.md --max-lines 50".into())
);
}

#[test]
fn test_curl() {
assert_eq!(
rewrite("curl https://api.example.com"),
Some("rtk curl https://api.example.com".into())
);
}

#[test]
fn test_wget() {
assert_eq!(
rewrite("wget https://example.com/file"),
Some("rtk wget https://example.com/file".into())
);
}
}
Loading
Loading