The sx 0100 fix (cli.parse / json.parse name collision) is merged on sx master, so `dist.sx` — co-importing std.cli (via dist) and std.json (via json_out) — now lowers and builds. Finish the step: - dist.sx: fix two real frontend errors the old IR-lowering crash had masked — `main` returns `!` (noreturn exit tails), and the post-parse dispatch is guarded by `if !perr` so the failable `p` is used only with its error proven absent. Drop the stale BLOCKED narration. - Makefile: `make build` now also compiles src/dist.sx -> build/dist; `make test` depends on `build` so the acceptance test finds the binary. - tests/cli_dispatch.sx: drives the BUILT build/dist via process.run and asserts the std.cli exit-code + --json purity contract: no-args and unknown-command -> human text on stderr + EX_USAGE (64); `ci publish --json` -> stdout is a single valid JSON object (std.json.parse, no trailing junk) with the human ack on stderr; `--help` lists ci/release. Handlers stay honest stubs (real ci publish is P3.4). Gate green: make build (build/dist), make test (7/7).
158 lines
6.3 KiB
Plaintext
158 lines
6.3 KiB
Plaintext
// =====================================================================
|
|
// 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 <group> <command> [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();
|
|
}
|