// ===================================================================== // dist.sx — the `dist` distribution CLI entry point (subplan 03, Slice 1). // // Wires the real process argv (via `std.cli`'s `os_args`) to subcommand // handlers through `cli.parse`. The three groups/commands are dispatched // to STUB handlers for now — they acknowledge and emit a known result; the // real publish/promote/rollback logic lands in P3.4/P3.5. // // dist ci publish // dist release promote // dist release rollback // // EXIT-CODE CONTRACT (sysexits, via std.cli): success ends with // `exit_ok()` (EX_OK = 0); a no-command / unknown-or-missing // group/command/flag ends with `exit_usage()` (EX_USAGE = 64). An // explicit `-h`/`--help` is not an error and ends 0. // // `--json` PURITY: every command accepts the reserved global `--json` // flag (surfaced by the parser as `parsed.json`). In json mode stdout // carries ONLY the machine-readable JSON object (emitted via `std.json`, // isolated in `json_out.sx`); ALL human-readable text (help, progress, // errors) goes to stderr. In non-json mode human text on stdout is fine. // ===================================================================== #import "modules/std.sx"; #import "modules/std/cli.sx"; jout :: #import "json_out.sx"; // Direct stderr writer (fd 2), so human help/usage/progress never lands on // stdout's data stream. `out` (std builtin) targets stdout (fd 1). cstdlib :: #library "c"; fd_write :: (fd: s32, buf: [*]u8, count: usize) -> isize #foreign cstdlib "write"; eputs :: (s: string) { if s.len > 0 { fd_write(2, s.ptr, xx s.len); } } // Human text router: stderr in json mode (keep stdout pure), stdout // otherwise. emit_human :: (s: string, json_mode: bool) { if json_mode { eputs(s); } else { out(s); } } HELP :: "dist — application distribution CLI\n\nUsage:\n dist [flags]\n\nGroups & commands:\n ci\n ci publish publish a release from CI (stub)\n release\n release promote promote a release onto a channel (stub)\n release rollback roll a channel back to a prior release (stub)\n\nGlobal flags:\n --json emit machine-readable JSON on stdout; human text to stderr\n -h, --help show this help and exit\n\nExit codes:\n 0 success\n 64 usage error (no command, or an unknown/missing command or flag)\n"; // True if `name` appears as a token in `args`. has_flag :: (args: []string, name: string) -> bool { i := 0; while i < args.len { if args[i] == name { return true; } i += 1; } return false; } // Human phrase for a parser failure, used in the stderr diagnostic. error_phrase :: (e: CliError) -> string { if e == error.UnknownCommand { return "unknown or missing command"; } if e == error.UnknownFlag { return "unknown flag"; } if e == error.MissingValue { return "flag is missing its value"; } if e == error.MissingRequired { return "missing required flag"; } if e == error.TooManyFlags { return "too many flags for command"; } return "usage error"; } // ── Stub handlers ──────────────────────────────────────────────────── // Honest stubs: acknowledge the command and emit a known result. NO real // publish/promote/rollback logic (that is P3.4/P3.5). handle_ci_publish :: (p: *Parsed, json_mode: bool) { ack("ci publish", json_mode); } handle_release_promote :: (p: *Parsed, json_mode: bool) { ack("release promote", json_mode); } handle_release_rollback :: (p: *Parsed, json_mode: bool) { ack("release rollback", json_mode); } // Emit a stub command's acknowledgement. In json mode: a single JSON // object on stdout (and a human progress note on stderr). Otherwise: a // human acknowledgement on stdout. ack :: (cmd: string, json_mode: bool) { if !json_mode { out(concat(concat("dist: ", cmd), " (stub) ok\n")); return; } eputs(concat(concat("dist: ", cmd), " (stub) acknowledged\n")); raw : [4096]u8 = ---; werr := false; n := jout.write_stub(cmd, string.{ ptr = @raw[0], len = 4096 }) catch { werr = true; 0 }; if werr { eputs("dist: internal error: JSON serialization failed\n"); exit_usage(); } out(string.{ ptr = @raw[0], len = n }); out("\n"); } // Route a parsed (group, command) to its stub handler. `parse` only // returns a (group, command) present in the table, so one arm always // matches. dispatch :: (p: *Parsed, json_mode: bool) { if p.group == "ci" and p.command == "publish" { handle_ci_publish(p, json_mode); return; } if p.group == "release" and p.command == "promote" { handle_release_promote(p, json_mode); return; } if p.group == "release" and p.command == "rollback" { handle_release_rollback(p, json_mode); return; } eputs("dist: internal error: unrouted command\n"); exit_usage(); } main :: () -> ! { // Real process argv -> logical args: drop argv[0] (the program path). storage : [64]string = ---; argbuf : []string = ---; argbuf.ptr = @storage[0]; argbuf.len = 64; argv := os_args(argbuf); args : []string = if argv.len <= 1 then argv[argv.len ..] else argv[1 ..]; json_mode := has_flag(args, "--json"); if has_flag(args, "-h") or has_flag(args, "--help") { emit_human(HELP, json_mode); exit_ok(); } if args.len == 0 { eputs("dist: no command given\n\n"); eputs(HELP); exit_usage(); } // Command table + flag specs live in this scope; `Parsed` holds VIEWS // into them, used before `main` returns. Per-command flags arrive with // the real handlers in P3.4/P3.5 — the global `--json` is recognized by // the parser without being declared here. no_flags : []FlagSpec = .[]; cmds : []Command = .[ Command.{ group = "ci", command = "publish", flags = no_flags }, Command.{ group = "release", command = "promote", flags = no_flags }, Command.{ group = "release", command = "rollback", flags = no_flags }, ]; diag : Diag = .{}; p, perr := parse(args, cmds, @diag); if perr { eputs(concat(concat(concat("dist: ", error_phrase(perr)), ": "), diag.token)); eputs("\n\n"); eputs(HELP); exit_usage(); } if !perr { dispatch(@p, json_mode); } exit_ok(); }