zc2 0.0.27

P2P compute broker with credit-based billing, WAL, and broker mesh support
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
//! Unified command model shared by the CLI and the interactive REPL.
//!
//! Phase 0 of the terminal refactor (see `docs/TERMINAL_REFACTOR.md`).
//!
//! This module establishes the *catalog* and *types*. The giant `match
//! args.len()` ladder in `main.rs` and the hand-written `help()` text will, in
//! Phase 2, be replaced by dispatch and help generation that read from
//! [`CommandRegistry`]. Phase 0 deliberately does **not** wire execution yet, so
//! it is a purely additive, no-behavior-change change.

#![allow(dead_code)]

pub mod spec;

pub use spec::{Category, CommandSpec};

/// A unit of rendered output produced by a command.
///
/// In CLI mode these are printed to stdout; in REPL mode they are pushed into
/// the scrollback buffer. Kept intentionally small for Phase 0; richer variants
/// (tables, key/value grids, spinners) arrive with the rendering work in
/// Phase 5.
#[derive(Debug, Clone)]
pub enum RenderBlock {
    /// Plain text, theme-styled by the renderer.
    Text(String),
    /// Markdown source to be rendered to styled terminal text.
    Markdown(String),
    /// An error message, styled with the theme's error color.
    Error(String),
}

/// Outcome of running a command.
#[derive(Debug, Clone)]
pub enum CommandResult {
    /// Emit a block of output.
    Output(RenderBlock),
    /// Switch the REPL into a named view (e.g. "monitor", "attach").
    /// Ignored in non-interactive CLI mode.
    SwitchView(String),
    /// Nothing to render.
    Noop,
    /// Quit the REPL / exit the process.
    Quit,
    /// The command failed.
    Error(String),
}

/// A registry of known command specifications.
///
/// Lookup is alias-aware. `search` currently does substring matching; Phase 4
/// swaps in fuzzy ranking for the palette without changing this surface.
#[derive(Debug, Default, Clone)]
pub struct CommandRegistry {
    specs: Vec<CommandSpec>,
}

impl CommandRegistry {
    /// An empty registry.
    pub fn new() -> Self {
        CommandRegistry { specs: Vec::new() }
    }

    /// A registry seeded with the full built-in command catalog.
    pub fn with_builtins() -> Self {
        CommandRegistry {
            specs: builtin_specs().to_vec(),
        }
    }

    /// Add a spec to the registry.
    pub fn register(&mut self, spec: CommandSpec) {
        self.specs.push(spec);
    }

    /// All registered specs, in registration order.
    pub fn all(&self) -> &[CommandSpec] {
        &self.specs
    }

    /// Resolve a command by canonical name or alias.
    pub fn resolve(&self, token: &str) -> Option<&CommandSpec> {
        self.specs.iter().find(|s| s.matches(token))
    }

    /// Specs in a given category, in registration order.
    pub fn by_category(&self, cat: Category) -> Vec<&CommandSpec> {
        self.specs.iter().filter(|s| s.category == cat).collect()
    }

    /// Substring search over names, aliases, and summaries. Phase 4 replaces
    /// the ranking with a fuzzy matcher; the signature stays the same.
    pub fn search(&self, query: &str) -> Vec<&CommandSpec> {
        let q = query.trim().to_ascii_lowercase();
        if q.is_empty() {
            return self.specs.iter().collect();
        }
        self.specs
            .iter()
            .filter(|s| {
                s.name.to_ascii_lowercase().contains(&q)
                    || s.summary.to_ascii_lowercase().contains(&q)
                    || s.aliases
                        .iter()
                        .any(|a| a.to_ascii_lowercase().contains(&q))
            })
            .collect()
    }
}

/// The built-in command catalog.
///
/// These mirror the commands currently dispatched in `main.rs`/`help()`. In
/// Phase 2 each gains an executable handler; here they provide the data that
/// drives generated help and the REPL palette.
pub fn builtin_specs() -> &'static [CommandSpec] {
    use spec::ArgSpec as A;
    static SPECS: &[CommandSpec] = &[
        CommandSpec {
            name: "broker",
            aliases: &[],
            args: &[
                A::optional("host", "Bind host (default 0.0.0.0)"),
                A::optional("port", "Bind port (default 9000)"),
            ],
            summary: "Start the compute broker with a live transaction log.",
            help_md: "Start the broker. Use `-t broker` for the interactive dashboard, \
                      or `-d broker` to run in the background (daemon).",
            category: Category::Broker,
        },
        CommandSpec {
            name: "up",
            aliases: &[],
            args: &[
                A::optional("--workers N", "Number of workers to start (default 1)"),
                A::optional("--port P", "Base worker port (default 3960)"),
                A::optional("--broker-port P", "Broker port (default 9000)"),
            ],
            summary: "Start a local cluster (broker + N workers).",
            help_md: "Start a local cluster. Example: `zc up --workers 4` starts a \
                      broker plus four workers. Add `-d` to run in the background.",
            category: Category::Cluster,
        },
        CommandSpec {
            name: "mesh",
            aliases: &[],
            args: &[
                A::optional("up|down|status", "Mesh action (default status)"),
                A::optional("N", "Node count for `up` (default 4)"),
            ],
            summary: "Launch/manage a local Docker compute-node mesh + broker.",
            help_md: "Orchestrate a local mesh of zakuro compute nodes: `mesh up 4` \
                      launches 4 nodes on a shared network, starts a worker on each, and \
                      runs a broker that discovers them; `mesh down` tears it down; \
                      `mesh status` lists running nodes. Local mode — no auth.",
            category: Category::Cluster,
        },
        CommandSpec {
            name: "agent",
            aliases: &[],
            args: &[A::optional(
                "run|install|uninstall|status",
                "Agent action (default status)",
            )],
            summary: "Run the loopback agent behind the macOS menu-bar app and widget.",
            help_md: "`zc agent install` starts the agent at login (macOS launchd); it \
                      supervises this Mac's broker and workers and serves the menu-bar \
                      app on 127.0.0.1, preferring port 4720 but falling back to an \
                      OS-assigned one if that's taken -- read the real port from \
                      ~/.zakuro/agent/agent.json. `zc agent status [--json]` shows its \
                      state; `zc agent uninstall` drains the workers and removes it.",
            category: Category::Cluster,
        },
        CommandSpec {
            name: "down",
            aliases: &[],
            args: &[
                A::optional("--port BASE", "Base worker port to scan"),
                A::optional("--broker-port P", "Broker port"),
            ],
            summary: "Stop all workers + broker started by 'zc up'.",
            help_md: "Stop the local cluster previously started with `zc up`.",
            category: Category::Cluster,
        },
        CommandSpec {
            name: "workers",
            aliases: &[],
            args: &[A::optional("zc://node", "Broker to query (default local)")],
            summary: "List workers on a broker.",
            help_md: "List workers on the local broker, or on a named broker: \
                      `zc workers zc://node-name`.",
            category: Category::Monitoring,
        },
        CommandSpec {
            name: "brokers",
            aliases: &[],
            args: &[],
            summary: "List mesh brokers by key-derived id (IP-free).",
            help_md: "List the brokers reachable from the local broker's mesh view, each \
                      shown as its key-derived `zc://node-<fp>` id with a reachability dot. \
                      Never prints an IP or peer address.",
            category: Category::Monitoring,
        },
        CommandSpec {
            name: "price",
            aliases: &[],
            args: &[A::optional("value", "New price (credits/hour) to set")],
            summary: "Show or set this Mac's price on the hub (credits/hour).",
            help_md: "Show this Mac's price with `zc price`, or set it with `zc price <value>`. \
                      The hub owns prices: with `zc agent` running the change goes through \
                      it, otherwise straight to the hub with your stored credentials.",
            category: Category::Monitoring,
        },
        CommandSpec {
            name: "bench",
            aliases: &[],
            args: &[
                A::optional("-s strategy", "Routing strategy (e.g. best_latency)"),
                A::optional("--compare", "Compare all routing strategies"),
                A::optional("zc://node", "Broker to benchmark"),
            ],
            summary: "Benchmark broker throughput and latency.",
            help_md: "Benchmark the broker. `bench --compare` compares all routing \
                      strategies; `bench -c 50 -n 10000` sets concurrency and request count.",
            category: Category::Benchmark,
        },
        CommandSpec {
            name: "me",
            aliases: &["whoami", "credits"],
            args: &[],
            summary: "Show authenticated user info and credit balance.",
            help_md: "Show the authenticated user and credit balance. Requires \
                      `ZAKURO_API_KEY`.",
            category: Category::Identity,
        },
        CommandSpec {
            name: "discovery",
            aliases: &["discover"],
            args: &[A::optional("zc://node", "Broker to query (default local)")],
            summary: "List the worker fleet discovered by the broker.",
            help_md: "List the workers a broker has discovered, e.g. `/discovery` for the \
                      local broker or `/discovery zc://node`. Shows node, status, URI, CPU \
                      and memory for each registered worker.",
            category: Category::Monitoring,
        },
        CommandSpec {
            name: "attach",
            aliases: &[],
            args: &[A::required("zc://node", "Broker to attach to")],
            summary: "Attach to a broker's live dashboard.",
            help_md: "Attach to a running broker's interactive dashboard, e.g. \
                      `zc attach zc://node-name` or `zc attach zc://localhost`.",
            category: Category::Monitoring,
        },
        CommandSpec {
            name: "info",
            aliases: &[],
            args: &[],
            summary: "Show system info, clusters, and network status.",
            help_md: "Print local system information, detected clusters, and \
                      network/mesh status.",
            category: Category::Diagnostics,
        },
        CommandSpec {
            name: "update",
            aliases: &[],
            args: &[],
            summary: "Update the command line.",
            help_md: "Update `zc` to the latest released version.",
            category: Category::Container,
        },
        CommandSpec {
            name: "pull",
            aliases: &[],
            args: &[],
            summary: "Pull updated images.",
            help_md: "Pull the latest Zakuro container images.",
            category: Category::Container,
        },
        CommandSpec {
            name: "images",
            aliases: &[],
            args: &[],
            summary: "List Zakuro images built on the machine.",
            help_md: "List Zakuro container images present locally.",
            category: Category::Container,
        },
        CommandSpec {
            name: "ps",
            aliases: &[],
            args: &[],
            summary: "List running Zakuro containers.",
            help_md: "List currently running Zakuro containers.",
            category: Category::Container,
        },
        CommandSpec {
            name: "kill",
            aliases: &[],
            args: &[],
            summary: "Remove running Zakuro containers.",
            help_md: "Stop and remove running Zakuro containers.",
            category: Category::Container,
        },
        CommandSpec {
            name: "restart",
            aliases: &[],
            args: &[],
            summary: "Restart containers with updated images.",
            help_md: "Restart Zakuro containers using updated images.",
            category: Category::Container,
        },
        CommandSpec {
            name: "theme",
            aliases: &[],
            args: &[A::optional("name", "Theme name (dark, light)")],
            summary: "Show or switch the interface theme.",
            help_md: "Show the current theme, or switch it: `/theme light`. Custom \
                      themes load from `~/.config/zc/theme.toml`.",
            category: Category::Ui,
        },
        CommandSpec {
            name: "mode",
            aliases: &[],
            args: &[A::optional("local|p2p", "Network mode to switch to")],
            summary: "Show or switch the broker network mode (local/p2p).",
            help_md: "Show the current mode, or switch: `/mode p2p` joins the peer \
                      network (requires `ZAKURO_API_KEY`); `/mode local` runs against \
                      the local broker with no auth.",
            category: Category::Ui,
        },
        CommandSpec {
            name: "vpn",
            aliases: &[],
            args: &[A::optional(
                "connect|disconnect|status",
                "VPN action (default status)",
            )],
            summary: "Connect zc to the zakuro mesh VPN (p2p).",
            help_md: "Join the zakuro WireGuard mesh: `zc vpn connect` fetches this \
                      node's profile (requires `ZAKURO_API_KEY`) and brings up the \
                      tunnel (native if root+WireGuard, else a Docker sidecar). \
                      `zc vpn status` shows the link + peers; `zc vpn disconnect` \
                      tears it down. Add `--native` or `--docker` to force a backend.",
            category: Category::Cluster,
        },
        CommandSpec {
            name: "shell",
            aliases: &["repl"],
            args: &[],
            summary: "Launch the interactive terminal.",
            help_md: "Launch the interactive `zc` terminal: a REPL with slash \
                      commands, a command palette, and the live dashboard as a view.",
            category: Category::Ui,
        },
        CommandSpec {
            name: "help",
            aliases: &["-h", "--help"],
            args: &[A::optional("command", "Show help for a specific command")],
            summary: "Show help.",
            help_md: "Show general help, or help for a specific command: `help bench`.",
            category: Category::Meta,
        },
        CommandSpec {
            name: "clear",
            aliases: &[],
            args: &[],
            summary: "Clear the REPL scrollback.",
            help_md: "Clear the interactive terminal's output area.",
            category: Category::Meta,
        },
        CommandSpec {
            name: "quit",
            aliases: &["exit", "q"],
            args: &[],
            summary: "Exit the interactive terminal.",
            help_md: "Exit the interactive `zc` terminal.",
            category: Category::Meta,
        },
    ];
    SPECS
}

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

    #[test]
    fn builtins_are_registered() {
        let reg = CommandRegistry::with_builtins();
        assert!(!reg.all().is_empty());
        assert!(reg.resolve("workers").is_some());
    }

    #[test]
    fn aliases_resolve() {
        let reg = CommandRegistry::with_builtins();
        let me = reg.resolve("whoami").expect("alias resolves");
        assert_eq!(me.name, "me");
        let credits = reg.resolve("credits").expect("alias resolves");
        assert_eq!(credits.name, "me");
    }

    #[test]
    fn usage_formatting() {
        let reg = CommandRegistry::with_builtins();
        let attach = reg.resolve("attach").unwrap();
        assert_eq!(attach.usage(), "attach <zc://node>");
    }

    #[test]
    fn search_matches_summary_and_name() {
        let reg = CommandRegistry::with_builtins();
        assert!(reg.search("bench").iter().any(|s| s.name == "bench"));
        assert!(reg.search("credit").iter().any(|s| s.name == "me"));
        // Empty query returns everything.
        assert_eq!(reg.search("").len(), reg.all().len());
    }

    #[test]
    fn categories_have_titles() {
        for c in Category::order() {
            assert!(!c.title().is_empty());
        }
    }

    #[test]
    fn agent_command_is_in_the_catalog() {
        let reg = CommandRegistry::with_builtins();
        let agent = reg.resolve("agent").expect("zc agent is catalogued");
        assert_eq!(agent.usage(), "agent [run|install|uninstall|status]");
    }
}