Skip to main content

borg/
completions.rs

1//! The command table of the borg CLI, and `borg completions <bash|zsh|fish>`: a shell completion script
2//! for borg, printed to stdout.
3//!
4//! One table (`GROUPS`, plus `DEBUG` for `borg debug`) feeds `borg help`, the dispatch gate in main.rs
5//! (`known`) and all three shells, so a new command needs one entry here; `HARNESSES` stands for the
6//! harnesses' names (harness/mod.rs `ALL`). Paths complete as files where a command takes one (`why`).
7
8/// In a subcommand list: every visible harness's name.
9pub const HARNESSES: &str = "<harnesses>";
10
11/// One command: its name, its help rows (the arguments after the name, what it does), and for the
12/// shells its subcommands, its flags and whether it takes a path.
13pub struct Command {
14    pub name: &'static str,
15    pub help: &'static [(&'static str, &'static str)],
16    pub subs: &'static [&'static str],
17    pub flags: &'static [&'static str],
18    pub path: bool,
19}
20
21const fn cmd(name: &'static str, help: &'static [(&'static str, &'static str)], subs: &'static [&'static str], flags: &'static [&'static str]) -> Command {
22    Command { name, help, subs, flags, path: false }
23}
24
25/// `borg help`: the commands grouped by what they are for.
26pub const GROUPS: &[(&str, &[Command])] = &[
27    (
28        "Setup",
29        &[
30            cmd("setup", &[("[--yes] [--no-vectors]", "guided setup: collectives, team, vectors, wiring")], &[], crate::install::setup::FLAGS),
31            cmd("install", &[("<HARNESS|all> [--apply]", "wire the hooks (a dry run without --apply)")], &[HARNESSES, "all"], &["--apply", "--home"]),
32            cmd("uninstall", &[("<HARNESS|all> [--apply]", "unwire them")], &[HARNESSES, "all"], &["--apply", "--home"]),
33            cmd("status", &[("", "each collective's lessons, outcomes and wiring")], &[], &["--home"]),
34            cmd("doctor", &[("", "what is broken: config, hooks, adapters, the model CLI")], &[], &["--home"]),
35            Command { path: true, ..cmd("collective", &[("[DIR]", "which collective a directory belongs to")], &[], &[]) },
36            cmd("vectors", &[("[--fetch|--rebuild] [--store LESSONS]", "the vector model and index")], &[], &["--fetch", "--rebuild", "--store"]),
37            cmd("completions", &[("<bash|zsh|fish>", "a shell completion script")], &["bash", "zsh", "fish"], &[]),
38            cmd("version", &[("", "borg's version (also --version, -V)")], &[], &[]),
39        ],
40    ),
41    (
42        "Learning",
43        &[
44            cmd("learn", &[("", "turn waiting corrections into lessons now")], &[], &[]),
45            cmd(
46                "no",
47                &[("[--session ID] <reason>", "say no to a drone's work: shown lessons lose credit, the reason becomes a lesson")],
48                &[],
49                crate::veto::FLAGS,
50            ),
51            cmd(
52                "assimilate",
53                &[
54                    ("[--apply] [--since DATE] [--limit N]", "import corrections from past sessions"),
55                    ("--from-dir DIR [--plan FILE] [--apply]", "import lessons from team documents"),
56                ],
57                &[],
58                crate::assimilate::FLAGS,
59            ),
60            cmd("reach", &[("[--plan FILE] [--apply] [--jobs N]", "sort lessons into team, repo-only and personal")], &[], &["--plan", "--apply", "--jobs"]),
61            cmd("facets", &[("[--apply] [--store FILE] [--jobs N]", "search facets for lessons")], &[], &["--apply", "--store", "--jobs"]),
62        ],
63    ),
64    (
65        "Decisions",
66        &[
67            cmd(
68                "queen",
69                &[("[show|ask|answer|skip|dismiss|dedupe]", "open decisions waiting for you")],
70                &["show", "ask", "answer", "skip", "dismiss", "dedupe"],
71                &["--json", "--repo", "--dir", "--collective", "--apply"],
72            ),
73            Command {
74                path: true,
75                ..cmd("why", &[("<path>[:<line>[-<line>]] | <commit>", "the reasoning behind code  [--full] [--json]")], &[], &["--full", "--json", "--dir"])
76            },
77            cmd("notes", &[("fetch [<commit>...]", "fetch the team's reasoning notes behind commits")], &["fetch"], &["--dir"]),
78            cmd(
79                "regress",
80                &[("[--replay A..B] [--explain] [--json]", "which earlier decisions later commits fixed (dry run)")],
81                &[],
82                crate::notes::regress_cli::FLAGS,
83            ),
84            cmd(
85                "review-fix",
86                &[("[--since DATE] [--json]", "bugs workflow reviews found and fix steps fixed, before any commit")],
87                &[],
88                crate::review_fix::FLAGS,
89            ),
90            cmd("trust", &[("[--dir DIR]", "trust per engineer and area")], &[], &["--dir"]),
91        ],
92    ),
93    (
94        "Watching",
95        &[
96            cmd("top", &[("[--once] [--json] [--dir DIR]", "the drones running across the team, live (q quits)")], &[], &["--once", "--json", "--dir"]),
97            cmd("web", &[("[--port PORT] [--dir DIR]", "a live page of knowledge and stats on 127.0.0.1")], &[], &["--port", "--dir"]),
98        ],
99    ),
100    (
101        "Team",
102        &[
103            cmd(
104                "mesh",
105                &[("<command>", "sync with team members (borg mesh --help)")],
106                &["id", "init", "invite", "invites", "join", "revoke", "members", "peer", "serve", "sync", "fetch"],
107                &["--collective", "--apply", "--paths", "--role", "--expires", "--uses", "--png", "--revoke"],
108            ),
109            cmd(
110                "tell",
111                &[(
112                    "<session|drone|local|collective|engineer> <text> [--re ID]",
113                    "a message to other sessions: local is this machine's, collective (or all) every member's",
114                )],
115                &["local", "collective", "all"],
116                crate::msg::FLAGS,
117            ),
118            cmd(
119                "ask",
120                &[("<session|drone|local|collective|engineer> <question> [--re ID]", "a message that asks for a reply")],
121                &["local", "collective", "all"],
122                crate::msg::FLAGS,
123            ),
124            cmd("inbox", &[("[--all] [--json]", "this session's messages; from a terminal, every recent one")], &[], crate::msg::INBOX_FLAGS),
125            cmd(
126                "listen",
127                &[("[--off]", "wake this session when a message arrives while it is idle"), ("--wait|--stream|--queue", "the waiter (started by borg)")],
128                &[],
129                crate::listen::FLAGS,
130            ),
131            cmd(
132                "pause",
133                &[("<session|drone|local|collective> [note]", "refuse a session's tool calls until it is resumed")],
134                &["local", "collective", "all"],
135                crate::msg::STEER_FLAGS,
136            ),
137            cmd(
138                "resume",
139                &[("<session|drone|local|collective> [note]", "let a paused session's tool calls run again")],
140                &["local", "collective", "all"],
141                crate::msg::STEER_FLAGS,
142            ),
143        ],
144    ),
145];
146
147/// `borg debug <command>`: evaluation and debugging tools, out of the main help.
148pub const DEBUG: &[Command] = &[
149    Command { path: true, ..cmd("recall", &[("<lessons.jsonl> [before_ms]", "rank lessons for text on stdin")], &[], &[]) },
150    Command { path: true, ..cmd("resolve", &[("<lessons.jsonl>", "the word rule's changed/ignored for {asked, before, edits, final} on stdin")], &[], &[]) },
151    Command { path: true, ..cmd("prefilter", &[("<lessons.jsonl> [before_ms]", "the free gate's score for a turn on stdin")], &[], &[]) },
152    Command { path: true, ..cmd("memory", &[("<lessons.jsonl> [--session ID]", "each lesson's activation and usefulness")], &[], &["--session"]) },
153    Command {
154        path: true,
155        ..cmd("expert", &[("<repo> --at <commit> --q <text>", "at most 3 cited claims from an expert repo")], &[], &["--at", "--version", "--q"])
156    },
157    cmd("vectors", &[("--query [--store LESSONS]", "the vector shortlist for a context on stdin")], &[], &["--query", "--store"]),
158    cmd("ope", &[("[--dir DIR | --file FILE] [--json]", "off-policy estimates of the belief's hand-set weights")], &[], &["--dir", "--file", "--json"]),
159];
160
161/// What hooks and drones run, never listed: dispatched, not shown.
162pub const INTERNAL: &[&str] = &["hook", "check"];
163
164/// Every command of the main help, in its order.
165pub fn commands() -> impl Iterator<Item = &'static Command> {
166    GROUPS.iter().flat_map(|(_, cmds)| cmds.iter())
167}
168
169/// Whether `name` is a top-level borg command (dispatched by main.rs): listed, `debug`, `help`, the
170/// version flags or internal.
171pub fn known(name: &str) -> bool {
172    commands().any(|c| c.name == name) || INTERNAL.contains(&name) || matches!(name, "debug" | "help" | "-h" | "--help" | "-V" | "--version")
173}
174
175/// The completions' commands: the main help's, then `debug` with the debug commands as subcommands and
176/// all their flags.
177fn completed() -> Vec<(&'static str, Vec<&'static str>, Vec<&'static str>, bool)> {
178    let mut out: Vec<_> = commands().map(|c| (c.name, subs_of(c.subs), c.flags.to_vec(), c.path)).collect();
179    let mut flags: Vec<&str> = DEBUG.iter().flat_map(|c| c.flags.iter().copied()).collect();
180    flags.sort_unstable();
181    flags.dedup();
182    out.push(("debug", DEBUG.iter().map(|c| c.name).chain(["help"]).collect(), flags, true));
183    out
184}
185
186/// The debug commands that take subcommands of their own: (name, its subcommands and flags). Completed
187/// by the third word, so such a command offers its own subcommands rather than the debug commands again.
188fn nested() -> Vec<(&'static str, String)> {
189    DEBUG.iter().filter(|c| !c.subs.is_empty()).map(|c| (c.name, [subs_of(c.subs).as_slice(), c.flags].concat().join(" "))).collect()
190}
191
192const USAGE: &str = "usage: borg completions <bash|zsh|fish>
193  bash: borg completions bash > ~/.local/share/bash-completion/completions/borg
194  zsh:  borg completions zsh > \"${fpath[1]}/_borg\"   (then restart the shell)
195  fish: borg completions fish > ~/.config/fish/completions/borg.fish";
196
197pub fn main(args: &[&str]) -> i32 {
198    match args.first().copied() {
199        Some("bash") => print!("{}", bash()),
200        Some("zsh") => print!("{}", zsh()),
201        Some("fish") => print!("{}", fish()),
202        _ => {
203            eprintln!("{USAGE}");
204            return 2;
205        }
206    }
207    0
208}
209
210/// A command's subcommands, `HARNESSES` spelled out.
211fn subs_of(subs: &[&'static str]) -> Vec<&'static str> {
212    subs.iter().flat_map(|s| if *s == HARNESSES { crate::harness::visible().map(|h| h.name()).collect() } else { vec![*s] }).collect()
213}
214
215/// The shell's `case` lines: `line(command, its subcommands and flags, takes a path)` for each command.
216fn cases(line: impl Fn(&str, &str, bool) -> String) -> String {
217    completed().iter().map(|(cmd, subs, flags, path)| line(cmd, &[subs.as_slice(), flags].concat().join(" "), *path)).collect()
218}
219
220fn names() -> String {
221    completed().iter().map(|c| c.0).collect::<Vec<_>>().join(" ")
222}
223
224pub fn bash() -> String {
225    let cases = cases(|cmd, words, path| format!("        {cmd}) words=\"{words}\"; files=\"{}\" ;;\n", if path { " -f" } else { "" }));
226    let nested: String =
227        nested().iter().map(|(cmd, words)| format!("            {cmd}) COMPREPLY=($(compgen -W \"{words}\" -- \"$cur\")); return ;;\n")).collect();
228    format!(
229        r#"# bash completion for borg (borg completions bash)
230_borg() {{
231    local cur="${{COMP_WORDS[COMP_CWORD]}}" words="" files=""
232    if [ "$COMP_CWORD" -eq 1 ]; then
233        COMPREPLY=($(compgen -W "{names}" -- "$cur"))
234        return
235    fi
236    if [ "$COMP_CWORD" -gt 2 ] && [ "${{COMP_WORDS[1]}}" = debug ]; then
237        case "${{COMP_WORDS[2]}}" in
238{nested}        esac
239    fi
240    case "${{COMP_WORDS[1]}}" in
241{cases}    esac
242    COMPREPLY=($(compgen -W "$words" -- "$cur"))
243    if [ -n "$files" ] && [ "${{cur:0:1}}" != "-" ]; then
244        COMPREPLY+=($(compgen -f -- "$cur"))
245    fi
246}}
247complete -o default -F _borg borg
248"#,
249        names = names()
250    )
251}
252
253pub fn zsh() -> String {
254    let cases = cases(|cmd, words, path| format!("        {cmd}) compadd -- {words}{} ;;\n", if path { "; _files" } else { "" }));
255    let nested: String = nested().iter().map(|(cmd, words)| format!("            {cmd}) compadd -- {words}; return ;;\n")).collect();
256    format!(
257        r#"#compdef borg
258# zsh completion for borg (borg completions zsh)
259_borg() {{
260    if (( CURRENT == 2 )); then
261        compadd -- {names}
262        return
263    fi
264    if (( CURRENT > 3 )) && [[ ${{words[2]}} == debug ]]; then
265        case "${{words[3]}}" in
266{nested}        esac
267    fi
268    case "${{words[2]}}" in
269{cases}    esac
270}}
271_borg "$@"
272"#,
273        names = names()
274    )
275}
276
277pub fn fish() -> String {
278    let mut out =
279        format!("# fish completion for borg (borg completions fish)\ncomplete -c borg -f\ncomplete -c borg -n __fish_use_subcommand -a '{}'\n", names());
280    // fish sees every word: `borg debug vectors` has also seen `vectors`, so a command a debug one shares a
281    // name with completes only outside `debug`, and a nested debug command's subcommands only inside it.
282    for (cmd, subs, flags, path) in completed() {
283        let when = if DEBUG.iter().any(|d| d.name == cmd) {
284            format!("__fish_seen_subcommand_from {cmd}; and not __fish_seen_subcommand_from debug")
285        } else {
286            format!("__fish_seen_subcommand_from {cmd}")
287        };
288        if !subs.is_empty() {
289            out.push_str(&format!("complete -c borg -n '{when}' -a '{}'\n", subs.join(" ")));
290        }
291        for f in flags {
292            out.push_str(&format!("complete -c borg -n '{when}' -l {}\n", f.trim_start_matches("--")));
293        }
294        if path {
295            out.push_str(&format!("complete -c borg -n '{when}' -F\n"));
296        }
297    }
298    for c in DEBUG.iter().filter(|c| !c.subs.is_empty()) {
299        let subs = subs_of(c.subs).join(" ");
300        out.push_str(&format!("complete -c borg -n '__fish_seen_subcommand_from debug; and __fish_seen_subcommand_from {}' -a '{subs}'\n", c.name));
301    }
302    out
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn every_shell_names_every_command() {
311        for script in [bash(), zsh(), fish()] {
312            for (cmd, ..) in completed() {
313                assert!(script.contains(cmd), "{cmd} missing");
314            }
315        }
316        assert!(bash().contains("queen) words=\"show ask answer"));
317        assert!(zsh().starts_with("#compdef borg"));
318        assert!(fish().contains("-n '__fish_seen_subcommand_from install' -l apply"));
319        assert!(bash().contains("install) words=\"claude omp opencode codex all --apply"), "{}", bash());
320        assert!(!bash().contains(" sim ") && !bash().contains(HARNESSES), "the simulated harness stays hidden");
321    }
322
323    #[test]
324    fn a_command_shared_with_debug_completes_outside_debug_only() {
325        let fish = fish();
326        assert!(fish.contains("-n '__fish_seen_subcommand_from vectors; and not __fish_seen_subcommand_from debug' -l fetch"), "{fish}");
327        assert!(fish.contains("-n '__fish_seen_subcommand_from debug' -a 'recall"), "{fish}");
328    }
329}