1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
//! §8 dispatch — argv → verb → run, and the pre-verb help/skill affordances.
//!
//! The crate root ([`crate`]) owns the module map, the branch constants, and the
//! [`crate::usage`] taxonomy bit; this module owns the entrypoint that resolves a
//! command line to its verb and routes it to the right subsystem. [`run`] is
//! re-exported as `balls::run`, the one symbol the `bl` binary calls.
use crate::edge::Edge;
use crate::verb::Verb;
use crate::{checkout, conf, help, import, install, mutate, reads, skill};
/// Appended to `bl skill`'s output (the bare subcommand spelling only): the
/// subcommand form is on a deprecation path in favor of the flag form `bl
/// --skill`, symmetric with the per-command `bl <cmd> --skill`. Kept working for
/// now — the note is the migration signal, not a removal.
const SKILL_DEPRECATION: &str = "\n\
---\n\
Note: `bl skill` is on a DEPRECATION PATH. Use `bl --skill` for this guide, and\n\
`bl <command> --skill` for a command's full usage (`--help` is an alias).\n";
/// The §8 dispatch entrypoint: resolve argv to its verb and run it. `prime`/
/// `sync` (§12/§13) wire to the engine via [`checkout`]; the deliverable verbs
/// (§9) via [`mutate`]; the read verbs (`show`/`list`, §9) via
/// [`reads`] — they author no diff and print the store view; `install` (§6)
/// seals its path-copy onto the landing or store via [`install::run`].
/// `--skill`/`skill` print the top-level operating guide ([`skill::top`]) and
/// `bl <cmd> --skill`/`--help` a command's full doc ([`skill::command`]); `help`
/// (also `--help`/`-h`) prints the terse command directory ([`help::directory`]).
/// `edge` carries the host inputs `main` resolved.
///
/// Returns the process exit code: `0` on success (including `skill`/`help`), `1`
/// on an op failure (a plugin aborted, a bad flag), `2` for an unknown or missing
/// command (usage convention — the message points at `bl help`).
///
/// `--log-level LEVEL` is the §4 layer-1 CLI override (the only global flag): it
/// is stripped here from anywhere in argv and stamped onto the [`Edge`] the op
/// reads, so the per-verb parsers never see it. A trailing `--log-level` with no
/// value is a usage error (exit 2).
pub fn run(edge: &Edge, args: &[String]) -> i32 {
let (log_level, rest) = match strip_log_level(args) {
Ok(split) => split,
Err(e) => {
eprintln!("bl: {e}");
return 2;
}
};
// `skill`/`--skill` (the guide) and `help` (terse command directory) are help
// OUTPUT, not ops: kept out of `Verb`, dispatched here, print to stdout, exit
// 0. `--skill` is the canonical spelling (symmetric with `bl <cmd> --skill`);
// the `skill` subcommand is kept but deprecated (a trailing note). A known
// command after either spelling gets ITS full doc; bare gets the top guide.
match rest.first().map(String::as_str) {
Some("--skill") => {
match rest.get(1).map(String::as_str).and_then(Verb::parse) {
Some(verb) => print!("{}", skill::command(verb)),
None => print!("{}", skill::top()),
}
return 0;
}
Some("skill") => {
if let Some(verb) = rest.get(1).map(String::as_str).and_then(Verb::parse) {
print!("{}", skill::command(verb));
} else {
print!("{}", skill::top());
print!("{SKILL_DEPRECATION}");
}
return 0;
}
// `bl help [<cmd>]`: a known command after `help` gets ITS full doc (the
// per-command skill, into which `--help` is folded); bare `help`/`--help`/
// `-h` gets the terse command directory.
Some("help" | "--help" | "-h") => {
match rest.get(1).map(String::as_str).and_then(Verb::parse) {
Some(verb) => print!("{}", skill::command(verb)),
None => print!("{}", help::directory()),
}
return 0;
}
_ => {}
}
let edge = &Edge { log_level, ..edge.clone() };
let Some(token) = rest.first().map(String::as_str) else {
eprintln!("usage: bl <command> — run `bl help` for the list");
return 2;
};
let Some(verb) = Verb::parse(token) else {
eprintln!("bl: unknown command '{token}' — run `bl help` for the list");
return 2;
};
// `bl <cmd> --skill` (canonical) / `--help` / `-h`: that command's full doc,
// before its parser runs (so it works on an unprimed checkout and never needs
// the verb's positionals). `--help` is folded into `--skill` — one per-command
// doc, both spellings. A flag past the `--` end-of-options is a positional,
// not a help request.
if rest[1..].iter().take_while(|a| *a != "--").any(|a| a == "--skill" || a == "--help" || a == "-h") {
print!("{}", skill::command(verb));
return 0;
}
let result = match verb {
Verb::Prime => checkout::prime(edge, &rest[1..]),
Verb::Sync => checkout::sync(edge, &rest[1..]),
Verb::Show | Verb::List => reads::run(edge, verb, &rest[1..]),
// `import` is the write inverse of the bedrock read (§16): records ride
// stdin, so the host stream is bound here at the edge and injected.
// UNLOCKED (`Stdin` locks per read): the `--legacy` edge pass re-enters
// stdin via `mutate::run`'s editor seam, and the std stdin mutex is not
// reentrant — a lock held across the verb self-deadlocks (bl-0a80).
Verb::Import => import::run(edge, &mut std::io::stdin(), &rest[1..]),
Verb::Install => install::run(edge, &rest[1..]),
Verb::Conf => conf::run(edge, &rest[1..]),
// Everything left is a deliverable verb (§9); mutate's own dispatch
// still rejects a non-mutating verb defensively.
v => mutate::run(edge, v, &rest[1..]),
};
match result {
Ok(()) => 0,
Err(e) => {
// `e` already names the verb where it adds clarity (`claim: … blocked
// by …`, `show: needs a ball id`); the wrapper just tags it as a bl
// error, so the verb is named ONCE — not the doubled `bl show: show:`.
eprintln!("bl: {e}");
// A USAGE error — the argv was malformed (an unknown flag, a missing
// value, the wrong positional count) — surfaces the command's doc
// (usage + flags lead it, bl-7990); an operational failure (a blocked
// op, a missing ball) stays terse. The [`crate::usage`] tag is the
// only thing that tells them apart, so the doc is offered exactly
// where it answers.
if e.kind() == std::io::ErrorKind::InvalidInput {
eprintln!();
eprint!("{}", skill::command(verb));
}
1
}
}
}
/// Pull the global `--log-level LEVEL` flag out of argv (from any position),
/// returning the requested level and argv with the flag removed. A `--log-level`
/// with no following value is a usage error.
fn strip_log_level(args: &[String]) -> Result<(Option<String>, Vec<String>), String> {
let mut level = None;
let mut rest = Vec::new();
let mut i = 0;
while i < args.len() {
if args[i] == "--log-level" {
i += 1;
level = Some(args.get(i).ok_or("--log-level needs a value")?.clone());
} else {
rest.push(args[i].clone());
}
i += 1;
}
Ok((level, rest))
}
#[cfg(test)]
#[path = "dispatch_test_support.rs"]
mod support;
#[cfg(test)]
#[path = "dispatch_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "dispatch_help_tests.rs"]
mod help_tests;