|
| 1 | +// Command crucible is a headless command-line tool over the crucible |
| 2 | +// state-machine IR. It lints, renders, diffs, validates, and ejects a machine's |
| 3 | +// serialized IR JSON without running any behavior. |
| 4 | +package main |
| 5 | + |
| 6 | +import ( |
| 7 | + "fmt" |
| 8 | + "io" |
| 9 | + "os" |
| 10 | +) |
| 11 | + |
| 12 | +// version is the crucible CLI's own version, independent of the state module's |
| 13 | +// v1 freeze. |
| 14 | +const version = "0.1.0" |
| 15 | + |
| 16 | +// exit codes. 0 is success, 1 a runtime or load error, 2 a usage error, and a |
| 17 | +// non-zero code (1) also signals lint findings. |
| 18 | +const ( |
| 19 | + exitOK = 0 |
| 20 | + exitError = 1 |
| 21 | + exitUsage = 2 |
| 22 | + exitFindings = 1 |
| 23 | +) |
| 24 | + |
| 25 | +func main() { |
| 26 | + os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) |
| 27 | +} |
| 28 | + |
| 29 | +// run dispatches a subcommand and returns its exit code. It is the testable |
| 30 | +// seam: every subcommand is a func(args, stdout, stderr) int, so the whole CLI |
| 31 | +// is exercised without spawning a process. |
| 32 | +func run(args []string, stdout, stderr io.Writer) int { |
| 33 | + if len(args) == 0 { |
| 34 | + usage(stderr) |
| 35 | + return exitUsage |
| 36 | + } |
| 37 | + |
| 38 | + cmd, rest := args[0], args[1:] |
| 39 | + switch cmd { |
| 40 | + case "lint": |
| 41 | + return runLint(rest, stdout, stderr) |
| 42 | + case "render": |
| 43 | + return runRender(rest, stdout, stderr) |
| 44 | + case "diff": |
| 45 | + return runDiff(rest, stdout, stderr) |
| 46 | + case "validate": |
| 47 | + return runValidate(rest, stdout, stderr) |
| 48 | + case "eject": |
| 49 | + return runEject(rest, stdout, stderr) |
| 50 | + case "version": |
| 51 | + emitln(stdout, version) |
| 52 | + return exitOK |
| 53 | + case "-version", "--version": |
| 54 | + emitln(stdout, version) |
| 55 | + return exitOK |
| 56 | + case "-h", "--help", "help": |
| 57 | + usage(stdout) |
| 58 | + return exitOK |
| 59 | + default: |
| 60 | + emitf(stderr, "crucible: unknown subcommand %q\n\n", cmd) |
| 61 | + usage(stderr) |
| 62 | + return exitUsage |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +// printf, println, and print write diagnostics and command output to an |
| 67 | +// io.Writer. A failure writing to stdout or stderr is unrecoverable for a |
| 68 | +// command-line tool (the stream the user reads is gone), so the error is |
| 69 | +// deliberately ignored here rather than threaded back through every call site. |
| 70 | +func emitf(w io.Writer, format string, a ...any) { _, _ = fmt.Fprintf(w, format, a...) } |
| 71 | +func emitln(w io.Writer, a ...any) { _, _ = fmt.Fprintln(w, a...) } |
| 72 | +func emit(w io.Writer, a ...any) { _, _ = fmt.Fprint(w, a...) } |
| 73 | + |
| 74 | +// usage prints the top-level command listing. |
| 75 | +func usage(w io.Writer) { |
| 76 | + emit(w, `crucible - headless tooling for the crucible state-machine IR |
| 77 | +
|
| 78 | +Usage: |
| 79 | + crucible <command> [arguments] |
| 80 | +
|
| 81 | +Commands: |
| 82 | + lint <ir.json> run static analysis and report findings |
| 83 | + render <ir.json> [-format f] render the machine as mermaid (default) or dot |
| 84 | + diff <old.json> <new.json> classify changes and recommend a semver bump |
| 85 | + validate <ir.json> confirm the IR loads and assembles |
| 86 | + eject <ir.json> [-package p] [-o f] generate typed Go behavior stubs |
| 87 | + version print the crucible CLI version |
| 88 | +
|
| 89 | +Each command reads an IR JSON file path, or - for stdin. |
| 90 | +
|
| 91 | +Flags: |
| 92 | + -version print the crucible CLI version |
| 93 | + -h, --help show this help |
| 94 | +`) |
| 95 | +} |
0 commit comments