extract(cubecli): move cube CLI out of cubesys into standalone crate

The `cube` binary (REPL + script runner over one shared CubeStore) is
now its own crate at `cubecli/`, matching the spec's package outline
that lists cubecli as a separate crate from the composition layer.

What changed:
- New `cubecli/` crate with `Cargo.toml` (deps: cubecoords, cubestore,
  cubefs, cubecode, cubecrypt, cubesys) and `src/main.rs` (copy of
  the former `cubesys/src/bin/cube.rs`).
- `cubesys/Cargo.toml`: removed the `[[bin]]` entry for `cube`; keeps
  `cube-demo`, `cube-server`, and `cubec` as before.
- `Cargo.toml` (workspace): added `cubecli` to members.
- `cubesys/src/bin/cube.rs` removed (code now lives in cubecli).

The `cube` binary is unchanged: same CLI surface, same demo, same
commands. The `cube-demo` binary in cubesys still wraps
`cubesys::demo::run()` and is unaffected.

Verification: `./check` gate passes (fmt + tests + clippy -D warnings);
`cube --help` and `cube demo` both work.

Co-Authored-By: Hermes Agent
This commit is contained in:
CUBELinux-2
2026-08-20 18:05:56 -04:00
co-authored by Hermes Agent
parent fdfcd2c728
commit ce8ac78a24
4 changed files with 19 additions and 16 deletions
+141
View File
@@ -0,0 +1,141 @@
//! `cube` — the CUBELinux-2 system CLI (local, in-process).
//!
//! One process holds ONE in-memory `CubeStore` shared by cubefs, cubecode and
//! cubecrypt. Commands are issued as a script (file) or interactively (REPL);
//! every command operates on that shared store, so `write` then `run` then
//! `seal` then `open` all see the same cube.
//!
//! For a persistent store served over a socket by a long-running daemon, use
//! `cube-server` + `cubec` instead (see `docs/integration.md`).
//!
//! Usage:
//! cube # print this help
//! cube demo # run the built-in integration demo
//! cube repl # read commands from stdin, one per line
//! cube script <file> # read commands from <file>, one per line
//!
//! Commands (operate on the shared cube):
//! prog <path> <ops...> # write CUBEVM bytecode from op names
//! write <path> <bytes...> # store cubevm bytecode (hex/dec) at a path
//! run <path> # load the code cell at <path> and run the VM
//! ls <dir> # list a cubefs directory
//! stat <path> # getattr via cubefs
//! seal <path> <K.Z.Y.X|auto> <tf> # encrypt the record at <path> (tf: none|gcm|chacha|xts)
//! open <path> <K.Z.Y.X|auto> <tf> # decrypt + decode + run the sealed record
//! keyinit # ensure the OS Null-space keystore exists
//!
//! Coordinates are written `C.Z.Y.X` (decimal). Key cells live in Null space,
//! so they are given directly as coordinates, not as cubefs paths.
use std::io::BufRead;
use cubesys::commands::Session;
/// Command words understood by the shared interpreter. When `cube`'s first
/// argument is one of these, it is run as a single command against a fresh
/// in-memory store (the same path as `cube repl`), so the OS / a script can
/// invoke e.g. `cube open /c001/z001/y001/x001 auto none` directly
/// — this is the Phase-3 "open by CZYX + flags" surface made a first-class
/// CLI command rather than REPL-only.
fn is_command_word(w: &str) -> bool {
matches!(
w,
"prog"
| "write"
| "run"
| "ls"
| "stat"
| "seal"
| "open"
| "keyinit"
| "query"
| "begin"
| "commit"
| "rollback"
| "stats"
| "audit"
)
}
fn main() {
let args: Vec<String> = std::env::args().collect();
match args.get(1).map(|s| s.as_str()) {
None => print_help(),
Some("demo") => cubesys::demo::run(),
// A direct command word (e.g. `cube open <path> <K.Z.Y.X> <tf>`): run it
// against a fresh in-memory session and exit with the result. This makes
// the coordinate-addressed API a real CLI surface the OS can call.
Some(word) if is_command_word(word) => {
let line = args[1..].join(" ");
let mut session = Session::new();
match session.exec(&line) {
Ok(out) => println!("{out}"),
Err(e) => {
eprintln!("error: {e}");
std::process::exit(1);
}
}
}
Some("repl") => {
let mut session = Session::new();
let stdin = std::io::stdin();
let lock = stdin.lock();
for line in lock.lines() {
let line = match line {
Ok(l) => l,
Err(_) => break,
};
if line.trim().is_empty() || line.starts_with('#') {
continue;
}
match session.exec(&line) {
Ok(out) => println!("{out}"),
Err(e) => eprintln!("error: {e}"),
}
}
}
Some("script") => {
let file = args.get(2).expect("script needs <file>");
let text = std::fs::read_to_string(file).expect("cannot read script file");
let mut session = Session::new();
for line in text.lines() {
if line.trim().is_empty() || line.starts_with('#') {
continue;
}
match session.exec(line) {
Ok(out) => println!("{out}"),
Err(e) => {
eprintln!("error: {e}");
std::process::exit(1);
}
}
}
}
Some(other) => {
eprintln!("unknown subcommand: {other}\n");
print_help();
std::process::exit(2);
}
}
}
fn print_help() {
println!(
"cube - CUBELinux-2 system CLI (cubefs + cubecode + cubecrypt over one store)\n\
\n\
Usage:\n \
cube show this help\n \
cube demo run the built-in integration demo\n \
cube repl read commands from stdin (one per line)\n \
cube script <file> run commands from a file\n\
\nCommands:\n \
prog <path> <ops...> write CUBEVM bytecode from op names\n \
write <path> <bytes...> store cubevm bytecode at a path\n \
run <path> run the code cell at <path>\n \
ls <dir> list a cubefs directory\n \
stat <path> getattr via cubefs\n \
seal <path> <K.Z.Y.X|auto> <tf> encrypt a record (tf: none|gcm|chacha|xts)\n \
open <path> <K.Z.Y.X|auto> <tf> decrypt + decode + run a sealed record\n \
keyinit ensure the OS Null-space keystore exists\n"
);
}