rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood7d0851de95805d647752c27f0021831aaceb39d6
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Updated",
"patch": {
"id": "f15b2b93586dcc88c7bc4c1b38ef312b85bdeed1",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"title": "cli: add bash completion for the rad command",
"state": {
"status": "open",
"conflicts": []
},
"before": "ed8b086045ee5d7bd1327f579de7861a1cf49e3b",
"after": "7d0851de95805d647752c27f0021831aaceb39d6",
"commits": [
"7d0851de95805d647752c27f0021831aaceb39d6"
],
"target": "22720e718bc8199054cca4c11f23ece43ab15b5d",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "f15b2b93586dcc88c7bc4c1b38ef312b85bdeed1",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"description": "add `rad completion bash` to generate a bash completion file.",
"base": "55cdd880bfee08124d5b6a38cc05036402c7ab6e",
"oid": "6175295c2c54a56c8b74f3c3416a51b76390c02e",
"timestamp": 1756511333
},
{
"id": "0b1b117a0c2930ec60d8984dc385d9c8db7a093e",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"description": "fix missing complete statement at the end of the generated output",
"base": "55cdd880bfee08124d5b6a38cc05036402c7ab6e",
"oid": "e2411271f9615baf3f2a14724f44039ecc1833ca",
"timestamp": 1756547375
},
{
"id": "4a3a3d1aa0c721921dc7e5f9e3ac60e6e9cd0b94",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"description": "automated sub-command and sub-sub-command discovery",
"base": "55cdd880bfee08124d5b6a38cc05036402c7ab6e",
"oid": "c7c5297479ee046db507590961f41d36655a276d",
"timestamp": 1756573277
},
{
"id": "a1353ffc0d8b15b291d1b616ddf8151d10588899",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"description": "rebased on master",
"base": "f00d1d67432882bef11fc940601f071efe55c88d",
"oid": "38804d501eae87217e143857c4236916bfabf82a",
"timestamp": 1757007990
},
{
"id": "76a1376bfd1f9352768efe14e864d9499eb64939",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"description": "rebased on master",
"base": "11fc98c9c9c4c681d265e765df05c2f9d503ddc9",
"oid": "4566a719cb7ef8e6648e5790930c6c44bdc098f8",
"timestamp": 1757424105
},
{
"id": "fa0a54565378bbe1c53a12dae877fa2587688321",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"description": "rebased",
"base": "ed8b086045ee5d7bd1327f579de7861a1cf49e3b",
"oid": "7d0851de95805d647752c27f0021831aaceb39d6",
"timestamp": 1758998219
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "3faa4b7f-860e-42e4-b65d-7d52ecaf0c5e"
},
"info_url": "https://cci.rad.levitte.org//3faa4b7f-860e-42e4-b65d-7d52ecaf0c5e.html"
}
Started at: 2025-09-27 20:37:03.148666+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/3faa4b7f-860e-42e4-b65d-7d52ecaf0c5e/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 123 issues · 12 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout f15b2b93586dcc88c7bc4c1b38ef312b85bdeed1
✓ Switched to branch patch/f15b2b9 at revision fa0a545
✓ Branch patch/f15b2b9 setup to track rad/patches/f15b2b93586dcc88c7bc4c1b38ef312b85bdeed1
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 7d0851de95805d647752c27f0021831aaceb39d6
HEAD is now at 7d0851de cli: add bash completion for the rad command
Exit code: 0
$ git show 7d0851de95805d647752c27f0021831aaceb39d6
commit 7d0851de95805d647752c27f0021831aaceb39d6
Author: Frederic Lepied <flepied@gmail.com>
Date: Fri Aug 29 12:48:40 2025 +0200
cli: add bash completion for the rad command
diff --git a/crates/radicle-cli/src/commands.rs b/crates/radicle-cli/src/commands.rs
index 0fcfb49e..88af29f2 100644
--- a/crates/radicle-cli/src/commands.rs
+++ b/crates/radicle-cli/src/commands.rs
@@ -4,6 +4,7 @@ pub mod checkout;
pub mod clean;
pub mod clone;
pub mod cob;
+pub mod completion;
pub mod config;
pub mod debug;
pub mod diff;
@@ -28,6 +29,5 @@ pub mod unblock;
pub mod unfollow;
pub mod unseed;
pub mod watch;
-
#[path = "commands/self.rs"]
pub mod rad_self;
diff --git a/crates/radicle-cli/src/commands/completion.rs b/crates/radicle-cli/src/commands/completion.rs
new file mode 100644
index 00000000..14a66554
--- /dev/null
+++ b/crates/radicle-cli/src/commands/completion.rs
@@ -0,0 +1,71 @@
+use std::ffi::OsString;
+
+use crate::terminal as term;
+use crate::terminal::args::{Args, Error, Help};
+
+pub const HELP: Help = Help {
+ name: "completion",
+ description: "Generate shell completion scripts",
+ version: env!("RADICLE_VERSION"),
+ usage: "Usage: rad completion <shell> [--help]",
+};
+
+#[derive(Default)]
+pub struct Options {
+ pub shell: String,
+}
+
+impl Args for Options {
+ fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
+ let mut parser = lexopt::Parser::from_args(args);
+ let mut shell = String::new();
+
+ while let Some(arg) = parser.next()? {
+ match arg {
+ lexopt::Arg::Value(val) => {
+ if shell.is_empty() {
+ shell = val.to_string_lossy().to_string();
+ } else {
+ anyhow::bail!("Multiple shell arguments provided");
+ }
+ }
+ lexopt::Arg::Long("help") | lexopt::Arg::Short('h') => {
+ return Err(Error::HelpManual {
+ name: "rad-completion",
+ }
+ .into());
+ }
+ _ => anyhow::bail!(arg.unexpected()),
+ }
+ }
+
+ if shell.is_empty() {
+ shell = "bash".to_string();
+ }
+
+ Ok((Options { shell }, vec![]))
+ }
+}
+
+pub fn run(options: Options, _ctx: impl term::Context) -> anyhow::Result<()> {
+ match options.shell.to_lowercase().as_str() {
+ "bash" => {
+ let completion_script =
+ crate::completion::init_command_registry().generate_bash_completion();
+ println!("{}", completion_script);
+ }
+ "zsh" => {
+ println!("zsh completion not yet implemented");
+ }
+ "fish" => {
+ println!("fish completion not yet implemented");
+ }
+ _ => {
+ anyhow::bail!(
+ "Unsupported shell '{}'. Supported shells: bash, zsh, fish",
+ options.shell
+ );
+ }
+ }
+ Ok(())
+}
diff --git a/crates/radicle-cli/src/commands/help.rs b/crates/radicle-cli/src/commands/help.rs
index a4c6d85a..247ada86 100644
--- a/crates/radicle-cli/src/commands/help.rs
+++ b/crates/radicle-cli/src/commands/help.rs
@@ -14,29 +14,35 @@ const COMMANDS: &[Help] = &[
crate::commands::auth::HELP,
crate::commands::block::HELP,
crate::commands::checkout::HELP,
+ crate::commands::clean::HELP,
crate::commands::clone::HELP,
+ crate::commands::cob::HELP,
+ crate::commands::completion::HELP,
crate::commands::config::HELP,
+ crate::commands::debug::HELP,
+ crate::commands::diff::HELP,
+ crate::commands::follow::HELP,
crate::commands::fork::HELP,
crate::commands::help::HELP,
crate::commands::id::HELP,
- crate::commands::init::HELP,
crate::commands::inbox::HELP,
+ crate::commands::init::HELP,
crate::commands::inspect::HELP,
crate::commands::issue::HELP,
crate::commands::ls::HELP,
crate::commands::node::HELP,
crate::commands::patch::HELP,
crate::commands::path::HELP,
- crate::commands::clean::HELP,
+ crate::commands::publish::HELP,
crate::commands::rad_self::HELP,
+ crate::commands::remote::HELP,
crate::commands::seed::HELP,
- crate::commands::follow::HELP,
+ crate::commands::stats::HELP,
+ crate::commands::sync::HELP,
crate::commands::unblock::HELP,
crate::commands::unfollow::HELP,
crate::commands::unseed::HELP,
- crate::commands::remote::HELP,
- crate::commands::stats::HELP,
- crate::commands::sync::HELP,
+ crate::commands::watch::HELP,
];
#[derive(Default)]
diff --git a/crates/radicle-cli/src/completion.rs b/crates/radicle-cli/src/completion.rs
new file mode 100644
index 00000000..6701e068
--- /dev/null
+++ b/crates/radicle-cli/src/completion.rs
@@ -0,0 +1,489 @@
+use std::collections::HashMap;
+
+// Import all command HELP constants for auto-generation
+use crate::commands::{
+ auth, block, checkout, clean, clone, cob, completion, config, debug, diff, follow, fork, help,
+ id, inbox, init, inspect, issue, ls, node, patch, path, publish, rad_self, remote, seed, stats,
+ sync, unblock, unfollow, unseed, watch,
+};
+
+/// Command metadata structure
+#[derive(Debug, Clone)]
+pub struct CommandMetadata {
+ pub name: String,
+ pub description: String,
+ pub options: Vec<OptionInfo>,
+ pub subcommands: Vec<SubcommandInfo>,
+ pub dynamic_completion: Option<DynamicCompletionType>,
+}
+
+/// Option information
+#[derive(Debug, Clone)]
+pub struct OptionInfo {
+ pub long: &'static str,
+ pub short: Option<char>,
+ pub description: &'static str,
+ pub takes_value: bool,
+}
+
+/// Subcommand metadata structure
+#[derive(Debug, Clone)]
+pub struct SubcommandInfo {
+ pub name: String,
+ pub description: String,
+ pub options: Vec<OptionInfo>,
+ pub subcommands: Vec<SubcommandInfo>,
+ pub dynamic_completion: Option<DynamicCompletionType>,
+}
+
+/// Type of dynamic completion for a command
+#[derive(Debug, Clone)]
+pub enum DynamicCompletionType {
+ PatchIds,
+ IssueIds,
+ BranchNames,
+ RemoteNames,
+ NodeIds,
+}
+
+/// Command registry that holds all completion metadata
+#[derive(Debug)]
+pub struct CommandRegistry {
+ commands: HashMap<&'static str, CommandMetadata>,
+}
+
+impl CommandRegistry {
+ /// Initialize the command registry with auto-generated metadata
+ pub fn init() -> Self {
+ let mut commands = HashMap::new();
+
+ // Auto-generate command metadata from the actual command HELP constants
+ // This ensures completion stays in sync with command implementations
+ let command_helps = [
+ ("auth", auth::HELP),
+ ("block", block::HELP),
+ ("checkout", checkout::HELP),
+ ("clone", clone::HELP),
+ ("cob", cob::HELP),
+ ("completion", completion::HELP),
+ ("config", config::HELP),
+ ("debug", debug::HELP),
+ ("diff", diff::HELP),
+ ("follow", follow::HELP),
+ ("fork", fork::HELP),
+ ("help", help::HELP),
+ ("id", id::HELP),
+ ("inbox", inbox::HELP),
+ ("init", init::HELP),
+ ("inspect", inspect::HELP),
+ ("issue", issue::HELP),
+ ("ls", ls::HELP),
+ ("node", node::HELP),
+ ("patch", patch::HELP),
+ ("path", path::HELP),
+ ("publish", publish::HELP),
+ ("clean", clean::HELP),
+ ("self", rad_self::HELP),
+ ("seed", seed::HELP),
+ ("sync", sync::HELP),
+ ("unblock", unblock::HELP),
+ ("unfollow", unfollow::HELP),
+ ("unseed", unseed::HELP),
+ ("remote", remote::HELP),
+ ("stats", stats::HELP),
+ ("watch", watch::HELP),
+ ];
+
+ for (name, help) in command_helps {
+ commands.insert(
+ name,
+ CommandMetadata {
+ name: name.to_string(),
+ description: help.description.to_string(),
+ options: Self::extract_options_from_help(help.usage),
+ subcommands: Self::extract_subcommands_from_help(help.usage),
+ dynamic_completion: Self::get_dynamic_completion_type(name),
+ },
+ );
+ }
+
+ Self { commands }
+ }
+
+ /// Extract options from help usage text
+ fn extract_options_from_help(usage: &str) -> Vec<OptionInfo> {
+ let mut options = vec![
+ // Common options that most commands support
+ OptionInfo {
+ long: "help",
+ short: Some('h'),
+ description: "Print help",
+ takes_value: false,
+ },
+ ];
+
+ // Parse usage text to find additional options
+ // This is a simple parser - could be enhanced
+ if usage.contains("--json") {
+ options.push(OptionInfo {
+ long: "json",
+ short: None,
+ description: "Output as JSON",
+ takes_value: false,
+ });
+ }
+
+ if usage.contains("--verbose") || usage.contains("-v") {
+ options.push(OptionInfo {
+ long: "verbose",
+ short: Some('v'),
+ description: "Verbose output",
+ takes_value: false,
+ });
+ }
+
+ options
+ }
+
+ /// Extract sub-subcommands from help usage text for a specific subcommand
+ fn extract_sub_subcommands_from_help(usage: &str, subcommand: &str) -> Vec<SubcommandInfo> {
+ let mut sub_subcommands = vec![];
+
+ // Look for patterns like "rad <command> <subcommand> <sub-subcommand> [<option>...]"
+ let lines: Vec<&str> = usage.lines().collect();
+
+ for line in lines {
+ let line = line.trim();
+
+ // Look for lines that contain the specific subcommand pattern
+ // We need to be more flexible here since we don't know the exact command name
+ let parts: Vec<&str> = line.split_whitespace().collect();
+
+ // We need at least 4 parts: "rad", "<command>", "<subcommand>", "<sub-subcommand>"
+ if parts.len() >= 4 && parts[0] == "rad" {
+ // Check if this line contains our subcommand
+ if parts.len() > 2 && parts[2] == subcommand {
+ // Look for the sub-subcommand (4th part)
+ if parts.len() > 3 {
+ let sub_subcommand_name = parts[3];
+
+ // Skip if it's an option, placeholder, or already known
+ if !sub_subcommand_name.starts_with('-')
+ && !sub_subcommand_name.starts_with('<')
+ && !sub_subcommand_name.starts_with('[')
+ && !sub_subcommands
+ .iter()
+ .any(|s: &SubcommandInfo| s.name == sub_subcommand_name)
+ {
+ sub_subcommands.push(SubcommandInfo {
+ name: sub_subcommand_name.to_string(),
+ description: "Sub-subcommand".to_string(),
+ options: vec![],
+ subcommands: vec![], // Could be enhanced for even deeper nesting
+ dynamic_completion: None,
+ });
+ }
+ }
+ }
+ }
+ }
+
+ sub_subcommands
+ }
+
+ /// Extract subcommands from help usage text
+ fn extract_subcommands_from_help(usage: &str) -> Vec<SubcommandInfo> {
+ let mut subcommands = vec![];
+
+ // Parse the usage text to find subcommands at any level
+ // Look for patterns like "rad <command> <subcommand> [<option>...]"
+ // and "rad <command> <subcommand> <sub-subcommand> [<option>...]"
+ let lines: Vec<&str> = usage.lines().collect();
+
+ for line in lines {
+ let line = line.trim();
+
+ // Skip lines that don't contain the command pattern
+ if !line.contains("rad ") {
+ continue;
+ }
+
+ // Look for subcommand patterns in lines like:
+ // "rad patch list [<option>...]"
+ // "rad node db <command> [<option>..]"
+ // "rad issue edit <issue-id> [--title <title>] [<option>...]"
+ let parts: Vec<&str> = line.split_whitespace().collect();
+
+ // We need at least 3 parts: "rad", "<command>", "<subcommand>"
+ if parts.len() >= 3 {
+ let subcommand = parts[2];
+
+ // Skip if it's an option (starts with - or --) or placeholder (starts with < or [)
+ if !subcommand.starts_with('-')
+ && !subcommand.starts_with('<')
+ && !subcommand.starts_with('[')
+ {
+ // Check if we already have this subcommand
+ if !subcommands
+ .iter()
+ .any(|s: &SubcommandInfo| s.name == subcommand)
+ {
+ // Try to extract description from the line
+ let description = Self::extract_description_from_line(line);
+
+ subcommands.push(SubcommandInfo {
+ name: subcommand.to_string(),
+ description: description.to_string(),
+ options: vec![],
+ subcommands: Self::extract_sub_subcommands_from_help(usage, subcommand),
+ dynamic_completion: None,
+ });
+ }
+ }
+ }
+ }
+
+ subcommands
+ }
+
+ /// Extract description from a help line
+ fn extract_description_from_line(line: &str) -> &'static str {
+ // Try to find a description after the command
+ // Look for patterns like "rad patch list [<option>...] # List patches"
+ if let Some(comment_start) = line.find('#') {
+ let comment = &line[comment_start + 1..].trim();
+ if !comment.is_empty() {
+ // For now, return a generic description since we can't store dynamic strings
+ // In the future, we could enhance this to parse actual descriptions
+ return "Subcommand";
+ }
+ }
+
+ // Default description
+ "Subcommand"
+ }
+
+ /// Get dynamic completion type based on command name
+ fn get_dynamic_completion_type(command: &str) -> Option<DynamicCompletionType> {
+ match command {
+ "patch" => Some(DynamicCompletionType::PatchIds),
+ "issue" => Some(DynamicCompletionType::IssueIds),
+ "checkout" => Some(DynamicCompletionType::BranchNames),
+ "remote" => Some(DynamicCompletionType::RemoteNames),
+ "node" => Some(DynamicCompletionType::NodeIds),
+ _ => None,
+ }
+ }
+
+ /// Generate bash completion script
+ pub fn generate_bash_completion(&self) -> String {
+ let mut script = String::new();
+
+ // Generate the main completion function
+ script.push_str(&self.generate_main_completion_function());
+
+ // Generate subcommand completion functions
+ for (name, metadata) in &self.commands {
+ script.push_str(&self.generate_subcommand_completion_function(name, metadata));
+ }
+
+ // Add the completion entry that tells bash to use _rad for the rad command
+ script.push_str("\n# Register the completion function for the 'rad' command\n");
+ script.push_str("complete -F _rad rad\n");
+
+ script
+ }
+
+ /// Generate the main completion function
+ fn generate_main_completion_function(&self) -> String {
+ let mut script = String::new();
+
+ script.push_str("# bash completion for rad\n");
+ script.push_str("# Auto-generated from command implementations\n\n");
+
+ // Generate the main _rad function
+ script.push_str("_rad() {\n");
+ script.push_str(" local cur prev words cword\n");
+ script.push_str(" _init_completion -n =: 2>/dev/null || {\n");
+ script.push_str(" # Fallback for environments without _init_completion\n");
+ script.push_str(" cur=${COMP_WORDS[COMP_CWORD]}\n");
+ script.push_str(" prev=${COMP_WORDS[COMP_CWORD-1]}\n");
+ script.push_str(" words=(\"${COMP_WORDS[@]}\")\n");
+ script.push_str(" cword=$COMP_CWORD\n");
+ script.push_str(" }\n");
+ script.push('\n');
+
+ // Generate command list
+ let command_names: Vec<&str> = self.commands.keys().copied().collect();
+
+ // Split long command lists across multiple lines for better readability
+ if command_names.len() > 10 {
+ script.push_str(" local commands=\"");
+ for (i, name) in command_names.iter().enumerate() {
+ if i > 0 && i % 8 == 0 {
+ script.push_str(" \\\n ");
+ }
+ script.push_str(name);
+ if i < command_names.len() - 1 {
+ script.push_str(" ");
+ }
+ }
+ script.push_str("\"\n");
+ } else {
+ script.push_str(&format!(
+ " local commands=\"{}\"\n",
+ command_names.join(" ")
+ ));
+ }
+ script.push('\n');
+
+ // Generate the main completion logic
+ script.push_str(" case $cword in\n");
+ script.push_str(" 1)\n");
+ script.push_str(" COMPREPLY=($(compgen -W \"$commands\" -- \"$cur\"))\n");
+ script.push_str(" ;;\n");
+ script.push_str(" *)\n");
+ script.push_str(" case \"${words[1]}\" in\n");
+
+ // Generate cases for each command
+ for name in self.commands.keys() {
+ script.push_str(&format!(" {})\n", name));
+ script.push_str(&format!(" _rad_{}\n", name));
+ script.push_str(" ;;\n");
+ }
+
+ script.push_str(" *)\n");
+ script.push_str(" COMPREPLY=($(compgen -W \"--help --version --json\" -- \"$cur\"))\n");
+ script.push_str(" ;;\n");
+ script.push_str(" esac\n");
+ script.push_str(" ;;\n");
+ script.push_str(" esac\n");
+ script.push_str("}\n\n");
+
+ script
+ }
+
+ /// Generate subcommand completion function
+ fn generate_subcommand_completion_function(
+ &self,
+ name: &str,
+ metadata: &CommandMetadata,
+ ) -> String {
+ let mut script = String::new();
+
+ script.push_str(&format!("_rad_{}() {{\n", name));
+ script.push_str(" local cur prev words cword\n");
+ script.push_str(" _init_completion -n =: 2>/dev/null || {\n");
+ script.push_str(" # Fallback for environments without _init_completion\n");
+ script.push_str(" cur=${COMP_WORDS[COMP_CWORD]}\n");
+ script.push_str(" prev=${COMP_WORDS[COMP_CWORD-1]}\n");
+ script.push_str(" words=(\"${COMP_WORDS[@]}\")\n");
+ script.push_str(" cword=$COMP_CWORD\n");
+ script.push_str(" }\n");
+ script.push('\n');
+
+ // Generate subcommand list if any
+ if !metadata.subcommands.is_empty() {
+ let subcommand_names: Vec<&str> = metadata
+ .subcommands
+ .iter()
+ .map(|s| s.name.as_str())
+ .collect();
+ script.push_str(&format!(
+ " local subcommands=\"{}\"\n",
+ subcommand_names.join(" ")
+ ));
+ script.push('\n');
+ }
+
+ // Generate options list
+ let option_strings: Vec<String> = metadata
+ .options
+ .iter()
+ .map(|opt| {
+ if let Some(short) = opt.short {
+ format!("--{} -{}", opt.long, short)
+ } else {
+ format!("--{}", opt.long)
+ }
+ })
+ .collect();
+
+ if !option_strings.is_empty() {
+ script.push_str(&format!(
+ " local options=\"{}\"\n",
+ option_strings.join(" ")
+ ));
+ script.push('\n');
+ }
+
+ // Generate completion logic
+ script.push_str(" case $cword in\n");
+ script.push_str(" 2)\n");
+
+ if !metadata.subcommands.is_empty() {
+ script.push_str(" COMPREPLY=($(compgen -W \"$subcommands\" -- \"$cur\"))\n");
+ } else {
+ script.push_str(" COMPREPLY=($(compgen -W \"$options\" -- \"$cur\"))\n");
+ }
+
+ script.push_str(" ;;\n");
+ script.push_str(" *)\n");
+
+ // Handle dynamic completion
+ if let Some(dynamic_type) = &metadata.dynamic_completion {
+ match dynamic_type {
+ DynamicCompletionType::PatchIds => {
+ script.push_str(" # Dynamic completion for patch IDs\n");
+ script.push_str(" local patch_ids=$(rad patch list 2>/dev/null | awk 'NR>2 {print $3}' | grep \"^$cur\" || echo \"\")\n");
+ script.push_str(" if [[ -n \"$patch_ids\" ]]; then\n");
+ script.push_str(
+ " COMPREPLY=($(compgen -W \"$patch_ids\" -- \"$cur\"))\n",
+ );
+ script.push_str(" else\n");
+ script.push_str(
+ " COMPREPLY=($(compgen -W \"$options\" -- \"$cur\"))\n",
+ );
+ script.push_str(" fi\n");
+ }
+ DynamicCompletionType::IssueIds => {
+ script.push_str(" # Dynamic completion for issue IDs\n");
+ script.push_str(" local issue_ids=$(rad issue list 2>/dev/null | awk 'NR>2 {print $3}' | grep \"^$cur\" || echo \"\")\n");
+ script.push_str(" if [[ -n \"$issue_ids\" ]]; then\n");
+ script.push_str(
+ " COMPREPLY=($(compgen -W \"$issue_ids\" -- \"$cur\"))\n",
+ );
+ script.push_str(" else\n");
+ script.push_str(
+ " COMPREPLY=($(compgen -W \"$options\" -- \"$cur\"))\n",
+ );
+ script.push_str(" fi\n");
+ }
+ _ => {
+ script.push_str(
+ " COMPREPLY=($(compgen -W \"$options\" -- \"$cur\"))\n",
+ );
+ }
+ }
+ } else {
+ script.push_str(" COMPREPLY=($(compgen -W \"$options\" -- \"$cur\"))\n");
+ }
+
+ script.push_str(" ;;\n");
+ script.push_str(" esac\n");
+ script.push_str("}\n\n");
+
+ script
+ }
+}
+
+impl Default for CommandRegistry {
+ fn default() -> Self {
+ Self::init()
+ }
+}
+
+/// Initialize the command registry
+pub fn init_command_registry() -> CommandRegistry {
+ CommandRegistry::init()
+}
diff --git a/crates/radicle-cli/src/lib.rs b/crates/radicle-cli/src/lib.rs
index 88660339..c4682d64 100644
--- a/crates/radicle-cli/src/lib.rs
+++ b/crates/radicle-cli/src/lib.rs
@@ -2,6 +2,7 @@
#![allow(clippy::or_fun_call)]
#![allow(clippy::too_many_arguments)]
pub mod commands;
+pub mod completion;
pub mod git;
pub mod node;
pub mod pager;
diff --git a/crates/radicle-cli/src/main.rs b/crates/radicle-cli/src/main.rs
index 37774ea9..b5138399 100644
--- a/crates/radicle-cli/src/main.rs
+++ b/crates/radicle-cli/src/main.rs
@@ -6,6 +6,7 @@ use anyhow::anyhow;
use radicle::version::Version;
use radicle_cli::commands::*;
+
use radicle_cli::terminal as term;
pub const NAME: &str = "rad";
@@ -154,6 +155,13 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
"cob" => {
term::run_command_args::<cob::Options, _>(cob::HELP, cob::run, args.to_vec());
}
+ "completion" => {
+ term::run_command_args::<completion::Options, _>(
+ completion::HELP,
+ completion::run,
+ args.to_vec(),
+ );
+ }
"config" => {
term::run_command_args::<config::Options, _>(config::HELP, config::run, args.to_vec());
}
Exit code: 0
shell: 'export RUSTDOCFLAGS=''-D warnings'' cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name 3faa4b7f-860e-42e4-b65d-7d52ecaf0c5e -v /opt/radcis/ci.rad.levitte.org/cci/state/3faa4b7f-860e-42e4-b65d-7d52ecaf0c5e/s:/3faa4b7f-860e-42e4-b65d-7d52ecaf0c5e/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/3faa4b7f-860e-42e4-b65d-7d52ecaf0c5e/w:/3faa4b7f-860e-42e4-b65d-7d52ecaf0c5e/w -w /3faa4b7f-860e-42e4-b65d-7d52ecaf0c5e/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:bookworm bash /3faa4b7f-860e-42e4-b65d-7d52ecaf0c5e/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.88-x86_64-unknown-linux-gnu'
info: latest update on 2025-06-26, rust version 1.88.0 (6b00bc388 2025-06-23)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.88.0 (873a06493 2025-05-10)
+ rustc --version
rustc 1.88.0 (6b00bc388 2025-06-23)
+ cargo fmt --check
Diff in /3faa4b7f-860e-42e4-b65d-7d52ecaf0c5e/w/crates/radicle-cli/src/commands.rs:21:
pub mod patch;
pub mod path;
pub mod publish;
+#[path = "commands/self.rs"]
+pub mod rad_self;
pub mod remote;
pub mod seed;
pub mod stats;
Diff in /3faa4b7f-860e-42e4-b65d-7d52ecaf0c5e/w/crates/radicle-cli/src/commands.rs:29:
pub mod unfollow;
pub mod unseed;
pub mod watch;
-#[path = "commands/self.rs"]
-pub mod rad_self;
Exit code: 1
{
"response": "finished",
"result": "failure"
}