diff --git a/docs/cli-matrix.json b/docs/cli-matrix.json new file mode 100644 index 0000000..a3f77d4 --- /dev/null +++ b/docs/cli-matrix.json @@ -0,0 +1,1439 @@ +{ + "python": { + "scan": [ + "--all", + "--copy", + "--format", + "--lines", + "--message", + "--no-copy", + "--output", + "--resolve", + "--stdout", + "--yes", + "-a", + "-c", + "-l", + "-m", + "-o", + "-r", + "-s", + "-y" + ], + "digest": [ + "--copy", + "--message", + "--no-copy", + "--output", + "--resolve", + "--stdout", + "-c", + "-m", + "-o", + "-r", + "-s" + ], + "tree": [ + "--all", + "--copy", + "--depth", + "--no-copy", + "--output", + "--resolve", + "--stdout", + "-L", + "-a", + "-c", + "-o", + "-r", + "-s" + ], + "mark": [], + "diff": [ + "--copy", + "--message", + "--no-copy", + "--output", + "--resolve", + "--stdout", + "-c", + "-m", + "-o", + "-r", + "-s" + ], + "semdiff": [ + "--copy", + "--message", + "--no-copy", + "--output", + "--resolve", + "--stdout", + "-c", + "-m", + "-o", + "-r", + "-s" + ], + "cap": [], + "pack": [ + "--budget", + "--copy", + "--message", + "--no-copy", + "--output", + "--stdout", + "--with", + "-b", + "-c", + "-m", + "-o", + "-s" + ], + "mcp": [], + "status": [], + "init": [ + "--force", + "-f" + ], + "clean": [ + "--force", + "--yes", + "-f", + "-y" + ], + "term": [ + "--context", + "--copy", + "--count", + "--failed", + "--format", + "--here", + "--history-only", + "--index", + "--no-copy", + "--output", + "--stdout", + "-c", + "-f", + "-i", + "-n", + "-o", + "-s" + ], + "term clear": [], + "term path": [], + "term ingest": [ + "--cwd", + "--end", + "--exit-code", + "--file", + "--shell", + "--start" + ], + "term install": [ + "--shell" + ], + "term uninstall": [ + "--shell" + ], + "shell-help": [], + "shell-help install": [], + "shell-help uninstall": [] + }, + "rust": { + "scan": [ + "--all", + "--copy", + "--format", + "--lines", + "--message", + "--no-copy", + "--output", + "--resolve", + "--stdout", + "--yes", + "-a", + "-c", + "-l", + "-m", + "-o", + "-r", + "-s", + "-y" + ], + "digest": [ + "--copy", + "--message", + "--no-copy", + "--output", + "--resolve", + "--stdout", + "-c", + "-m", + "-o", + "-r", + "-s" + ], + "tree": [ + "--all", + "--copy", + "--depth", + "--no-copy", + "--output", + "--resolve", + "--stdout", + "-L", + "-a", + "-c", + "-o", + "-r", + "-s" + ], + "mark": [], + "diff": [ + "--copy", + "--message", + "--no-copy", + "--output", + "--resolve", + "--stdout", + "-c", + "-m", + "-o", + "-r", + "-s" + ], + "semdiff": [ + "--copy", + "--message", + "--no-copy", + "--output", + "--resolve", + "--stdout", + "-c", + "-m", + "-o", + "-r", + "-s" + ], + "cap": [], + "pack": [ + "--budget", + "--copy", + "--message", + "--no-copy", + "--output", + "--stdout", + "--with", + "-b", + "-c", + "-m", + "-o", + "-s" + ], + "term": [ + "--context", + "--copy", + "--count", + "--failed", + "--format", + "--here", + "--history-only", + "--index", + "--no-copy", + "--output", + "--raw", + "--stdout", + "-c", + "-f", + "-i", + "-n", + "-o", + "-s" + ], + "term clear": [], + "term path": [], + "term install": [ + "--shell" + ], + "term uninstall": [ + "--shell" + ], + "term ingest": [ + "--cwd", + "--end", + "--exit-code", + "--file", + "--shell", + "--start" + ], + "mcp": [], + "status": [], + "init": [ + "--force", + "-f" + ], + "clean": [ + "--force", + "--yes", + "-f", + "-y" + ], + "shell-help": [], + "shell-help install": [], + "shell-help uninstall": [] + }, + "statuses": { + "both": "both builds", + "python-only": "Python build only", + "rust-only": "Rust build only", + "hidden": "both builds, not shown by --help", + "pending": "in an open pull request or planned; not on main", + "differs": "both builds, with a different result" + }, + "commands": [ + { + "command": "scan", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "digest", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "tree", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "mark", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "diff", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "semdiff", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "cap", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "pack", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "mcp", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "status", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "init", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "clean", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "term", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "term clear", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "term path", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "term ingest", + "python": true, + "rust": true, + "status": "hidden", + "note": "" + }, + { + "command": "term install", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "term uninstall", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "shell-help", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "shell-help install", + "python": true, + "rust": true, + "status": "both", + "note": "" + }, + { + "command": "shell-help uninstall", + "python": true, + "rust": true, + "status": "both", + "note": "" + } + ], + "options": [ + { + "command": "", + "option": "--version", + "python": [ + "--version", + "-v" + ], + "rust": [ + "--version", + "-V", + "-v" + ], + "status": "both", + "note": "names differ: Python --version, -v; Rust --version, -V, -v" + }, + { + "command": "scan", + "option": "--output", + "python": [ + "--output", + "-o" + ], + "rust": [ + "--output", + "-o" + ], + "status": "both", + "note": "" + }, + { + "command": "scan", + "option": "--copy", + "python": [ + "--copy", + "-c" + ], + "rust": [ + "--copy", + "-c" + ], + "status": "both", + "note": "" + }, + { + "command": "scan", + "option": "--no-copy", + "python": [ + "--no-copy" + ], + "rust": [ + "--no-copy" + ], + "status": "both", + "note": "hidden in the Rust build only" + }, + { + "command": "scan", + "option": "--stdout", + "python": [ + "--stdout", + "-s" + ], + "rust": [ + "--stdout", + "-s" + ], + "status": "both", + "note": "" + }, + { + "command": "scan", + "option": "--all", + "python": [ + "--all", + "-a" + ], + "rust": [ + "--all", + "-a" + ], + "status": "both", + "note": "" + }, + { + "command": "scan", + "option": "--message", + "python": [ + "--message", + "-m" + ], + "rust": [ + "--message", + "-m" + ], + "status": "both", + "note": "" + }, + { + "command": "scan", + "option": "--lines", + "python": [ + "--lines", + "-l" + ], + "rust": [ + "--lines", + "-l" + ], + "status": "both", + "note": "" + }, + { + "command": "scan", + "option": "--yes", + "python": [ + "--yes", + "-y" + ], + "rust": [ + "--yes", + "-y" + ], + "status": "both", + "note": "" + }, + { + "command": "scan", + "option": "--resolve", + "python": [ + "--resolve", + "-r" + ], + "rust": [ + "--resolve", + "-r" + ], + "status": "both", + "note": "" + }, + { + "command": "scan", + "option": "--format", + "python": [ + "--format" + ], + "rust": [ + "--format" + ], + "status": "both", + "note": "" + }, + { + "command": "digest", + "option": "--copy", + "python": [ + "--copy", + "-c" + ], + "rust": [ + "--copy", + "-c" + ], + "status": "both", + "note": "" + }, + { + "command": "digest", + "option": "--no-copy", + "python": [ + "--no-copy" + ], + "rust": [ + "--no-copy" + ], + "status": "both", + "note": "hidden in the Rust build only" + }, + { + "command": "digest", + "option": "--output", + "python": [ + "--output", + "-o" + ], + "rust": [ + "--output", + "-o" + ], + "status": "both", + "note": "" + }, + { + "command": "digest", + "option": "--stdout", + "python": [ + "--stdout", + "-s" + ], + "rust": [ + "--stdout", + "-s" + ], + "status": "both", + "note": "" + }, + { + "command": "digest", + "option": "--message", + "python": [ + "--message", + "-m" + ], + "rust": [ + "--message", + "-m" + ], + "status": "both", + "note": "" + }, + { + "command": "digest", + "option": "--resolve", + "python": [ + "--resolve", + "-r" + ], + "rust": [ + "--resolve", + "-r" + ], + "status": "both", + "note": "" + }, + { + "command": "tree", + "option": "--copy", + "python": [ + "--copy", + "-c" + ], + "rust": [ + "--copy", + "-c" + ], + "status": "both", + "note": "" + }, + { + "command": "tree", + "option": "--no-copy", + "python": [ + "--no-copy" + ], + "rust": [ + "--no-copy" + ], + "status": "both", + "note": "hidden in the Rust build only" + }, + { + "command": "tree", + "option": "--output", + "python": [ + "--output", + "-o" + ], + "rust": [ + "--output", + "-o" + ], + "status": "both", + "note": "" + }, + { + "command": "tree", + "option": "--stdout", + "python": [ + "--stdout", + "-s" + ], + "rust": [ + "--stdout", + "-s" + ], + "status": "both", + "note": "" + }, + { + "command": "tree", + "option": "--all", + "python": [ + "--all", + "-a" + ], + "rust": [ + "--all", + "-a" + ], + "status": "both", + "note": "" + }, + { + "command": "tree", + "option": "--resolve", + "python": [ + "--resolve", + "-r" + ], + "rust": [ + "--resolve", + "-r" + ], + "status": "both", + "note": "" + }, + { + "command": "tree", + "option": "--depth", + "python": [ + "--depth", + "-L" + ], + "rust": [ + "--depth", + "-L" + ], + "status": "both", + "note": "" + }, + { + "command": "diff", + "option": "--copy", + "python": [ + "--copy", + "-c" + ], + "rust": [ + "--copy", + "-c" + ], + "status": "both", + "note": "" + }, + { + "command": "diff", + "option": "--no-copy", + "python": [ + "--no-copy" + ], + "rust": [ + "--no-copy" + ], + "status": "both", + "note": "hidden in the Rust build only" + }, + { + "command": "diff", + "option": "--output", + "python": [ + "--output", + "-o" + ], + "rust": [ + "--output", + "-o" + ], + "status": "both", + "note": "" + }, + { + "command": "diff", + "option": "--stdout", + "python": [ + "--stdout", + "-s" + ], + "rust": [ + "--stdout", + "-s" + ], + "status": "both", + "note": "" + }, + { + "command": "diff", + "option": "--message", + "python": [ + "--message", + "-m" + ], + "rust": [ + "--message", + "-m" + ], + "status": "both", + "note": "" + }, + { + "command": "diff", + "option": "--resolve", + "python": [ + "--resolve", + "-r" + ], + "rust": [ + "--resolve", + "-r" + ], + "status": "both", + "note": "" + }, + { + "command": "semdiff", + "option": "--copy", + "python": [ + "--copy", + "-c" + ], + "rust": [ + "--copy", + "-c" + ], + "status": "both", + "note": "" + }, + { + "command": "semdiff", + "option": "--no-copy", + "python": [ + "--no-copy" + ], + "rust": [ + "--no-copy" + ], + "status": "both", + "note": "hidden in the Rust build only" + }, + { + "command": "semdiff", + "option": "--output", + "python": [ + "--output", + "-o" + ], + "rust": [ + "--output", + "-o" + ], + "status": "both", + "note": "" + }, + { + "command": "semdiff", + "option": "--stdout", + "python": [ + "--stdout", + "-s" + ], + "rust": [ + "--stdout", + "-s" + ], + "status": "both", + "note": "" + }, + { + "command": "semdiff", + "option": "--message", + "python": [ + "--message", + "-m" + ], + "rust": [ + "--message", + "-m" + ], + "status": "both", + "note": "" + }, + { + "command": "semdiff", + "option": "--resolve", + "python": [ + "--resolve", + "-r" + ], + "rust": [ + "--resolve", + "-r" + ], + "status": "both", + "note": "" + }, + { + "command": "pack", + "option": "--message", + "python": [ + "--message", + "-m" + ], + "rust": [ + "--message", + "-m" + ], + "status": "both", + "note": "" + }, + { + "command": "pack", + "option": "--budget", + "python": [ + "--budget", + "-b" + ], + "rust": [ + "--budget", + "-b" + ], + "status": "both", + "note": "" + }, + { + "command": "pack", + "option": "--with", + "python": [ + "--with" + ], + "rust": [ + "--with" + ], + "status": "both", + "note": "" + }, + { + "command": "pack", + "option": "--copy", + "python": [ + "--copy", + "-c" + ], + "rust": [ + "--copy", + "-c" + ], + "status": "both", + "note": "" + }, + { + "command": "pack", + "option": "--no-copy", + "python": [ + "--no-copy" + ], + "rust": [ + "--no-copy" + ], + "status": "both", + "note": "hidden in the Rust build only" + }, + { + "command": "pack", + "option": "--stdout", + "python": [ + "--stdout", + "-s" + ], + "rust": [ + "--stdout", + "-s" + ], + "status": "both", + "note": "" + }, + { + "command": "pack", + "option": "--output", + "python": [ + "--output", + "-o" + ], + "rust": [ + "--output", + "-o" + ], + "status": "both", + "note": "" + }, + { + "command": "init", + "option": "--force", + "python": [ + "--force", + "-f" + ], + "rust": [ + "--force", + "-f" + ], + "status": "both", + "note": "" + }, + { + "command": "clean", + "option": "--yes", + "python": [ + "--yes", + "--force", + "-f", + "-y" + ], + "rust": [ + "--yes", + "--force", + "-f", + "-y" + ], + "status": "both", + "note": "" + }, + { + "command": "term", + "option": "--count", + "python": [ + "--count", + "-n" + ], + "rust": [ + "--count", + "-n" + ], + "status": "both", + "note": "" + }, + { + "command": "term", + "option": "--index", + "python": [ + "--index", + "-i" + ], + "rust": [ + "--index", + "-i" + ], + "status": "both", + "note": "" + }, + { + "command": "term", + "option": "--failed", + "python": [ + "--failed", + "-f" + ], + "rust": [ + "--failed", + "-f" + ], + "status": "both", + "note": "" + }, + { + "command": "term", + "option": "--here", + "python": [ + "--here" + ], + "rust": [ + "--here" + ], + "status": "both", + "note": "" + }, + { + "command": "term", + "option": "--history-only", + "python": [ + "--history-only" + ], + "rust": [ + "--history-only" + ], + "status": "both", + "note": "" + }, + { + "command": "term", + "option": "--context", + "python": [ + "--context" + ], + "rust": [ + "--context" + ], + "status": "both", + "note": "" + }, + { + "command": "term", + "option": "--format", + "python": [ + "--format" + ], + "rust": [ + "--format" + ], + "status": "both", + "note": "" + }, + { + "command": "term", + "option": "--copy", + "python": [ + "--copy", + "-c" + ], + "rust": [ + "--copy", + "-c" + ], + "status": "both", + "note": "" + }, + { + "command": "term", + "option": "--no-copy", + "python": [ + "--no-copy" + ], + "rust": [ + "--no-copy" + ], + "status": "both", + "note": "hidden in the Rust build only" + }, + { + "command": "term", + "option": "--stdout", + "python": [ + "--stdout", + "-s" + ], + "rust": [ + "--stdout", + "-s" + ], + "status": "both", + "note": "" + }, + { + "command": "term", + "option": "--output", + "python": [ + "--output", + "-o" + ], + "rust": [ + "--output", + "-o" + ], + "status": "both", + "note": "" + }, + { + "command": "term", + "option": "--raw", + "python": null, + "rust": [ + "--raw" + ], + "status": "rust-only", + "note": "" + }, + { + "command": "term ingest", + "option": "--file", + "python": [ + "--file" + ], + "rust": [ + "--file" + ], + "status": "both", + "note": "" + }, + { + "command": "term ingest", + "option": "--exit-code", + "python": [ + "--exit-code" + ], + "rust": [ + "--exit-code" + ], + "status": "both", + "note": "" + }, + { + "command": "term ingest", + "option": "--cwd", + "python": [ + "--cwd" + ], + "rust": [ + "--cwd" + ], + "status": "both", + "note": "" + }, + { + "command": "term ingest", + "option": "--shell", + "python": [ + "--shell" + ], + "rust": [ + "--shell" + ], + "status": "both", + "note": "" + }, + { + "command": "term ingest", + "option": "--start", + "python": [ + "--start" + ], + "rust": [ + "--start" + ], + "status": "both", + "note": "" + }, + { + "command": "term ingest", + "option": "--end", + "python": [ + "--end" + ], + "rust": [ + "--end" + ], + "status": "both", + "note": "" + }, + { + "command": "term install", + "option": "--shell", + "python": [ + "--shell" + ], + "rust": [ + "--shell" + ], + "status": "both", + "note": "" + }, + { + "command": "term uninstall", + "option": "--shell", + "python": [ + "--shell" + ], + "rust": [ + "--shell" + ], + "status": "both", + "note": "" + }, + { + "command": "scan", + "option": "--github", + "python": null, + "rust": null, + "status": "pending", + "note": "Python: #33, not merged. Rust: waits for the decision in DotFiles#81." + }, + { + "command": "scan", + "option": "--ref", + "python": null, + "rust": null, + "status": "pending", + "note": "Python: #33, not merged. Rust: waits for the decision in DotFiles#81." + }, + { + "command": "scan", + "option": "--path", + "python": null, + "rust": null, + "status": "pending", + "note": "Python: #33, not merged. Rust: waits for the decision in DotFiles#81." + }, + { + "command": "scan", + "option": "--auth", + "python": null, + "rust": null, + "status": "pending", + "note": "Python: #33, not merged. Rust: waits for the decision in DotFiles#81." + }, + { + "command": "scan", + "option": "--max-file-size", + "python": null, + "rust": null, + "status": "pending", + "note": "Python: #33, not merged. Rust: waits for the decision in DotFiles#81." + }, + { + "command": "mcp", + "option": "--allow-remote", + "python": null, + "rust": null, + "status": "pending", + "note": "Python: planned (docs/remote-scan.md in #30). Rust: planned (same)." + } + ], + "global": [ + { + "command": "every command", + "option": "--preset NAME", + "python": "yes", + "rust": "yes", + "status": "both", + "note": "expanded before parsing" + } + ], + "behavior": [ + { + "command": "digest", + "what": "Python and MATLAB outlines", + "python": "yes", + "rust": "yes", + "status": "both", + "note": "" + }, + { + "command": "digest", + "what": "Rust, JavaScript, TypeScript and Go outlines", + "python": "no", + "rust": "yes", + "status": "rust-only", + "note": "tree-sitter grammars" + }, + { + "command": "digest", + "what": "outline of a Python file with a syntax error", + "python": "nothing", + "rust": "what parses", + "status": "rust-only", + "note": "" + }, + { + "command": "semdiff", + "what": "function and class changes", + "python": "Python files", + "rust": "Python files", + "status": "both", + "note": "Python only in both builds" + }, + { + "command": "pack", + "what": "order of modules called by a failing test", + "python": "ast.walk order", + "rust": "order of the calls", + "status": "differs", + "note": "" + }, + { + "command": "cap", + "what": "Ctrl+C", + "python": "default", + "rust": "forwarded, 5 s before kill", + "status": "differs", + "note": "" + }, + { + "command": "init, clean, term clear", + "what": "status line ending on Windows", + "python": "\\r\\n", + "rust": "\\n", + "status": "differs", + "note": "results and -o files use the platform ending in both" + }, + { + "command": "scan", + "what": "a GitHub repository (OWNER/REPO --github, URL)", + "python": "#33, not merged", + "rust": "no", + "status": "pending", + "note": "network requests, see docs/remote-scan.md" + } + ] +} diff --git a/docs/cli-matrix.md b/docs/cli-matrix.md new file mode 100644 index 0000000..085894f --- /dev/null +++ b/docs/cli-matrix.md @@ -0,0 +1,207 @@ +# Commands and options by build + +Generated by `docs/cli_matrix.py` from the Typer app (Python build) and clap (Rust +build, `rust/examples/cli_options.rs`); do not edit by hand. `cli-matrix.json` holds the +same data. Hidden commands and options are included; `--help` and `-h` are left out. + +| Status | Meaning | Options | +| --- | --- | ---: | +| both | both builds | 64 | +| python-only | Python build only | 0 | +| rust-only | Rust build only | 1 | +| hidden | both builds, not shown by --help | 0 | +| pending | in an open pull request or planned; not on main | 6 | +| differs | both builds, with a different result | behavior only | + +Nothing in either build is marked experimental. Rows marked pending are not on `main`. + +## Commands + +| Command | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `cdg scan` | yes | yes | both | | +| `cdg digest` | yes | yes | both | | +| `cdg tree` | yes | yes | both | | +| `cdg mark` | yes | yes | both | | +| `cdg diff` | yes | yes | both | | +| `cdg semdiff` | yes | yes | both | | +| `cdg cap` | yes | yes | both | | +| `cdg pack` | yes | yes | both | | +| `cdg mcp` | yes | yes | both | | +| `cdg status` | yes | yes | both | | +| `cdg init` | yes | yes | both | | +| `cdg clean` | yes | yes | both | | +| `cdg term` | yes | yes | both | | +| `cdg term clear` | yes | yes | both | | +| `cdg term path` | yes | yes | both | | +| `cdg term ingest` | yes | yes | hidden | | +| `cdg term install` | yes | yes | both | | +| `cdg term uninstall` | yes | yes | both | | +| `cdg shell-help` | yes | yes | both | | +| `cdg shell-help install` | yes | yes | both | | +| `cdg shell-help uninstall` | yes | yes | both | | + +## Options + +### `cdg` (no command) + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--version` | `--version`, `-v` | `--version`, `-V`, `-v` | both | names differ: Python --version, -v; Rust --version, -V, -v | + +### `cdg scan` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--output` | `--output`, `-o` | `--output`, `-o` | both | | +| `--copy` | `--copy`, `-c` | `--copy`, `-c` | both | | +| `--no-copy` | `--no-copy` | `--no-copy` | both | hidden in the Rust build only | +| `--stdout` | `--stdout`, `-s` | `--stdout`, `-s` | both | | +| `--all` | `--all`, `-a` | `--all`, `-a` | both | | +| `--message` | `--message`, `-m` | `--message`, `-m` | both | | +| `--lines` | `--lines`, `-l` | `--lines`, `-l` | both | | +| `--yes` | `--yes`, `-y` | `--yes`, `-y` | both | | +| `--resolve` | `--resolve`, `-r` | `--resolve`, `-r` | both | | +| `--format` | `--format` | `--format` | both | | +| `--github` | – | – | pending | Python: #33, not merged. Rust: waits for the decision in DotFiles#81. | +| `--ref` | – | – | pending | Python: #33, not merged. Rust: waits for the decision in DotFiles#81. | +| `--path` | – | – | pending | Python: #33, not merged. Rust: waits for the decision in DotFiles#81. | +| `--auth` | – | – | pending | Python: #33, not merged. Rust: waits for the decision in DotFiles#81. | +| `--max-file-size` | – | – | pending | Python: #33, not merged. Rust: waits for the decision in DotFiles#81. | + +### `cdg digest` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--copy` | `--copy`, `-c` | `--copy`, `-c` | both | | +| `--no-copy` | `--no-copy` | `--no-copy` | both | hidden in the Rust build only | +| `--output` | `--output`, `-o` | `--output`, `-o` | both | | +| `--stdout` | `--stdout`, `-s` | `--stdout`, `-s` | both | | +| `--message` | `--message`, `-m` | `--message`, `-m` | both | | +| `--resolve` | `--resolve`, `-r` | `--resolve`, `-r` | both | | + +### `cdg tree` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--copy` | `--copy`, `-c` | `--copy`, `-c` | both | | +| `--no-copy` | `--no-copy` | `--no-copy` | both | hidden in the Rust build only | +| `--output` | `--output`, `-o` | `--output`, `-o` | both | | +| `--stdout` | `--stdout`, `-s` | `--stdout`, `-s` | both | | +| `--all` | `--all`, `-a` | `--all`, `-a` | both | | +| `--resolve` | `--resolve`, `-r` | `--resolve`, `-r` | both | | +| `--depth` | `--depth`, `-L` | `--depth`, `-L` | both | | + +### `cdg diff` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--copy` | `--copy`, `-c` | `--copy`, `-c` | both | | +| `--no-copy` | `--no-copy` | `--no-copy` | both | hidden in the Rust build only | +| `--output` | `--output`, `-o` | `--output`, `-o` | both | | +| `--stdout` | `--stdout`, `-s` | `--stdout`, `-s` | both | | +| `--message` | `--message`, `-m` | `--message`, `-m` | both | | +| `--resolve` | `--resolve`, `-r` | `--resolve`, `-r` | both | | + +### `cdg semdiff` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--copy` | `--copy`, `-c` | `--copy`, `-c` | both | | +| `--no-copy` | `--no-copy` | `--no-copy` | both | hidden in the Rust build only | +| `--output` | `--output`, `-o` | `--output`, `-o` | both | | +| `--stdout` | `--stdout`, `-s` | `--stdout`, `-s` | both | | +| `--message` | `--message`, `-m` | `--message`, `-m` | both | | +| `--resolve` | `--resolve`, `-r` | `--resolve`, `-r` | both | | + +### `cdg pack` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--message` | `--message`, `-m` | `--message`, `-m` | both | | +| `--budget` | `--budget`, `-b` | `--budget`, `-b` | both | | +| `--with` | `--with` | `--with` | both | | +| `--copy` | `--copy`, `-c` | `--copy`, `-c` | both | | +| `--no-copy` | `--no-copy` | `--no-copy` | both | hidden in the Rust build only | +| `--stdout` | `--stdout`, `-s` | `--stdout`, `-s` | both | | +| `--output` | `--output`, `-o` | `--output`, `-o` | both | | + +### `cdg init` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--force` | `--force`, `-f` | `--force`, `-f` | both | | + +### `cdg clean` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--yes` | `--yes`, `--force`, `-f`, `-y` | `--yes`, `--force`, `-f`, `-y` | both | | + +### `cdg term` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--count` | `--count`, `-n` | `--count`, `-n` | both | | +| `--index` | `--index`, `-i` | `--index`, `-i` | both | | +| `--failed` | `--failed`, `-f` | `--failed`, `-f` | both | | +| `--here` | `--here` | `--here` | both | | +| `--history-only` | `--history-only` | `--history-only` | both | | +| `--context` | `--context` | `--context` | both | | +| `--format` | `--format` | `--format` | both | | +| `--copy` | `--copy`, `-c` | `--copy`, `-c` | both | | +| `--no-copy` | `--no-copy` | `--no-copy` | both | hidden in the Rust build only | +| `--stdout` | `--stdout`, `-s` | `--stdout`, `-s` | both | | +| `--output` | `--output`, `-o` | `--output`, `-o` | both | | +| `--raw` | – | `--raw` | rust-only | | + +### `cdg term ingest` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--file` | `--file` | `--file` | both | | +| `--exit-code` | `--exit-code` | `--exit-code` | both | | +| `--cwd` | `--cwd` | `--cwd` | both | | +| `--shell` | `--shell` | `--shell` | both | | +| `--start` | `--start` | `--start` | both | | +| `--end` | `--end` | `--end` | both | | + +### `cdg term install` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--shell` | `--shell` | `--shell` | both | | + +### `cdg term uninstall` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--shell` | `--shell` | `--shell` | both | | + +### `cdg mcp` + +| Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | +| `--allow-remote` | – | – | pending | Python: planned (docs/remote-scan.md in #30). Rust: planned (same). | + +## Options outside the parsers + +| Command | Option | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | --- | +| every command | `--preset NAME` | yes | yes | both | expanded before parsing | + +## Behavior + +Kept by hand from `rust/README.md` ("Differences from the Python build") and open pull +requests. + +| Command | What | Python | Rust | Status | Note | +| --- | --- | --- | --- | --- | --- | +| `digest` | Python and MATLAB outlines | yes | yes | both | | +| `digest` | Rust, JavaScript, TypeScript and Go outlines | no | yes | rust-only | tree-sitter grammars | +| `digest` | outline of a Python file with a syntax error | nothing | what parses | rust-only | | +| `semdiff` | function and class changes | Python files | Python files | both | Python only in both builds | +| `pack` | order of modules called by a failing test | ast.walk order | order of the calls | differs | | +| `cap` | Ctrl+C | default | forwarded, 5 s before kill | differs | | +| `init, clean, term clear` | status line ending on Windows | \r\n | \n | differs | results and -o files use the platform ending in both | +| `scan` | a GitHub repository (OWNER/REPO --github, URL) | #33, not merged | no | pending | network requests, see docs/remote-scan.md | diff --git a/docs/cli_matrix.py b/docs/cli_matrix.py new file mode 100644 index 0000000..9299775 --- /dev/null +++ b/docs/cli_matrix.py @@ -0,0 +1,375 @@ +""" +Write docs/cli-matrix.md and docs/cli-matrix.json: which commands and options each build has. + + uv run python docs/cli_matrix.py # write both files + uv run python docs/cli_matrix.py --check # exit 1 when they are out of date + +The Python build is read from Typer the way site/scripts/check_reference.py reads it, the Rust +build from clap through `cargo run --example cli_options` (or a file saved from it, given with +--rust-json). In the JSON, "python" and "rust" map each command path to its option names, the +shape check_reference.py builds from the CLI, so either can stand in for it. +""" + +import argparse +import json +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +import click +import typer + +from codigest.main import app + +DOCS = Path(__file__).resolve().parent +ROOT = DOCS.parent +MARKDOWN = DOCS / "cli-matrix.md" +JSON_FILE = DOCS / "cli-matrix.json" +IGNORED = {"--help", "-h"} + +# Options that are in open pull requests or only planned, so neither build on main has them. +PENDING = [ + ("scan", "--github", "#33, not merged", "waits for the decision in DotFiles#81"), + ("scan", "--ref", "#33, not merged", "waits for the decision in DotFiles#81"), + ("scan", "--path", "#33, not merged", "waits for the decision in DotFiles#81"), + ("scan", "--auth", "#33, not merged", "waits for the decision in DotFiles#81"), + ("scan", "--max-file-size", "#33, not merged", "waits for the decision in DotFiles#81"), + ("mcp", "--allow-remote", "planned (docs/remote-scan.md in #30)", "planned (same)"), +] + +# Not options, so not read from the CLIs: --preset is expanded before either parser sees the +# arguments (src/codigest/core/presets.py, rust/src/presets.rs). +GLOBAL = [("every command", "--preset NAME", "yes", "yes", "both", "expanded before parsing")] + +# Behavior that differs without a different option, from rust/README.md ("Differences from the +# Python build"), kept by hand: update it with that section. +BEHAVIOR = [ + ( + "digest", + "Python and MATLAB outlines", + "yes", + "yes", + "both", + "", + ), + ( + "digest", + "Rust, JavaScript, TypeScript and Go outlines", + "no", + "yes", + "rust-only", + "tree-sitter grammars", + ), + ( + "digest", + "outline of a Python file with a syntax error", + "nothing", + "what parses", + "rust-only", + "", + ), + ( + "semdiff", + "function and class changes", + "Python files", + "Python files", + "both", + "Python only in both builds", + ), + ( + "pack", + "order of modules called by a failing test", + "ast.walk order", + "order of the calls", + "differs", + "", + ), + ( + "cap", + "Ctrl+C", + "default", + "forwarded, 5 s before kill", + "differs", + "", + ), + ( + "init, clean, term clear", + "status line ending on Windows", + "\\r\\n", + "\\n", + "differs", + "results and -o files use the platform ending in both", + ), + ( + "scan", + "a GitHub repository (OWNER/REPO --github, URL)", + "#33, not merged", + "no", + "pending", + "network requests, see docs/remote-scan.md", + ), +] + +STATUS_WORDS = { + "both": "both builds", + "python-only": "Python build only", + "rust-only": "Rust build only", + "hidden": "both builds, not shown by --help", + "pending": "in an open pull request or planned; not on main", + "differs": "both builds, with a different result", +} + + +@dataclass(slots=True, frozen=True) +class Option: + key: str # the first long name, or the short name of an option that has none + names: tuple[str, ...] + hidden: bool + + +def python_build() -> dict[str, dict]: + """Command path -> {"hidden": bool, "options": [Option]}, hidden commands included.""" + found: dict[str, dict] = {} + + def walk(command: click.Command, path: list[str], hidden: bool) -> None: + options = [] + for param in command.params: + if not isinstance(param, click.Option): + continue + # A --flag/--no-flag pair is two options, as the Rust build defines it. + for names in (param.opts, param.secondary_opts): + names = [n for n in names if n not in IGNORED] + if names: + options.append(_option(names, param.hidden)) + found[" ".join(path)] = {"hidden": hidden, "options": options} + if isinstance(command, click.Group): + for name, sub in command.commands.items(): + walk(sub, [*path, name], hidden or sub.hidden) + + walk(typer.main.get_command(app), [], False) + return found + + +def rust_build(saved: Path | None) -> dict[str, dict]: + if saved: + raw = json.loads(saved.read_text(encoding="utf-8")) + else: + result = subprocess.run( + ["cargo", "run", "--quiet", "--example", "cli_options"], + cwd=ROOT / "rust", + capture_output=True, + text=True, + encoding="utf-8", + check=True, + ) + raw = json.loads(result.stdout) + return { + path: { + "hidden": entry["hidden"], + "options": [_option(o["names"], o["hidden"]) for o in entry["options"]], + } + for path, entry in raw.items() + } + + +def _option(names: list[str], hidden: bool) -> Option: + longs = [n for n in names if n.startswith("--")] + shorts = sorted(n for n in names if not n.startswith("--")) + key = longs[0] if longs else shorts[0] + return Option(key, (*longs, *shorts), hidden) + + +def names_by_path(build: dict[str, dict]) -> dict[str, list[str]]: + """The shape of check_reference.py's cli_options(): command path -> option names.""" + return { + path: sorted({n for o in entry["options"] for n in o.names}) + for path, entry in build.items() + if path + } + + +def command_rows(python: dict, rust: dict) -> list[dict]: + rows = [] + for path in _ordered(python, rust): + if not path: + continue + p, r = python.get(path), rust.get(path) + if p and r: + status = "hidden" if p["hidden"] and r["hidden"] else "both" + else: + status = "python-only" if p else "rust-only" + note = "" + if p and r and p["hidden"] != r["hidden"]: + note = f"hidden in the {'Python' if p['hidden'] else 'Rust'} build only" + rows.append( + {"command": path, "python": bool(p), "rust": bool(r), "status": status, "note": note} + ) + return rows + + +def option_rows(python: dict, rust: dict) -> list[dict]: + rows = [] + for path in _ordered(python, rust): + p = {o.key: o for o in python.get(path, {}).get("options", [])} + r = {o.key: o for o in rust.get(path, {}).get("options", [])} + for key in [*p, *(k for k in r if k not in p)]: + po, ro = p.get(key), r.get(key) + notes = [] + if po and ro: + status = "hidden" if po.hidden and ro.hidden else "both" + if po.names != ro.names: + notes.append( + f"names differ: Python {', '.join(po.names)}; Rust {', '.join(ro.names)}" + ) + if po.hidden != ro.hidden: + notes.append(f"hidden in the {'Python' if po.hidden else 'Rust'} build only") + else: + status = "python-only" if po else "rust-only" + if (po or ro).hidden: + notes.append("hidden") + rows.append( + { + "command": path, + "option": key, + "python": list(po.names) if po else None, + "rust": list(ro.names) if ro else None, + "status": status, + "note": "; ".join(notes), + } + ) + present = {(row["command"], row["option"]) for row in rows} + for command, option, python_note, rust_note in PENDING: + if (command, option) in present: + raise SystemExit(f"{command} {option} is on main now: update PENDING in {__file__}") + rows.append( + { + "command": command, + "option": option, + "python": None, + "rust": None, + "status": "pending", + "note": f"Python: {python_note}. Rust: {rust_note}.", + } + ) + return rows + + +def _ordered(python: dict, rust: dict) -> list[str]: + return [*python, *(path for path in rust if path not in python)] + + +def build(python: dict, rust: dict) -> tuple[str, str]: + commands = command_rows(python, rust) + options = option_rows(python, rust) + data = { + "python": names_by_path(python), + "rust": names_by_path(rust), + "statuses": STATUS_WORDS, + "commands": commands, + "options": options, + "global": [ + dict(zip(("command", "option", "python", "rust", "status", "note"), g, strict=True)) + for g in GLOBAL + ], + "behavior": [ + dict(zip(("command", "what", "python", "rust", "status", "note"), b, strict=True)) + for b in BEHAVIOR + ], + } + return _markdown(commands, options), json.dumps(data, indent=2, ensure_ascii=False) + "\n" + + +def _cell(names: list[str] | None) -> str: + return ", ".join(f"`{n}`" for n in names) if names else "–" + + +def _markdown(commands: list[dict], options: list[dict]) -> str: + counts = {s: sum(1 for o in options if o["status"] == s) for s in STATUS_WORDS} + lines = [ + "# Commands and options by build", + "", + "Generated by `docs/cli_matrix.py` from the Typer app (Python build) and clap (Rust", + "build, `rust/examples/cli_options.rs`); do not edit by hand. `cli-matrix.json` holds the", + "same data. Hidden commands and options are included; `--help` and `-h` are left out.", + "", + "| Status | Meaning | Options |", + "| --- | --- | ---: |", + *( + f"| {s} | {w} | {counts[s] if s != 'differs' else 'behavior only'} |" + for s, w in STATUS_WORDS.items() + ), + "", + "Nothing in either build is marked experimental. Rows marked pending are not on `main`.", + "", + "## Commands", + "", + "| Command | Python | Rust | Status | Note |", + "| --- | --- | --- | --- | --- |", + *( + f"| `cdg {c['command']}` | {'yes' if c['python'] else 'no'} | " + f"{'yes' if c['rust'] else 'no'} | {c['status']} | {c['note']} |" + for c in commands + ), + "", + "## Options", + "", + ] + for path in dict.fromkeys(o["command"] for o in options): + lines += [ + f"### `cdg {path}`" if path else "### `cdg` (no command)", + "", + "| Option | Python | Rust | Status | Note |", + "| --- | --- | --- | --- | --- |", + *( + f"| `{o['option']}` | {_cell(o['python'])} | {_cell(o['rust'])} | " + f"{o['status']} | {o['note']} |" + for o in options + if o["command"] == path + ), + "", + ] + lines += [ + "## Options outside the parsers", + "", + "| Command | Option | Python | Rust | Status | Note |", + "| --- | --- | --- | --- | --- | --- |", + *(f"| {c} | `{o}` | {p} | {r} | {s} | {n} |" for c, o, p, r, s, n in GLOBAL), + "", + "## Behavior", + "", + 'Kept by hand from `rust/README.md` ("Differences from the Python build") and open pull', + "requests.", + "", + "| Command | What | Python | Rust | Status | Note |", + "| --- | --- | --- | --- | --- | --- |", + *(f"| `{c}` | {w} | {p} | {r} | {s} | {n} |" for c, w, p, r, s, n in BEHAVIOR), + "", + ] + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0].strip()) + parser.add_argument("--check", action="store_true", help="fail if the files are out of date") + parser.add_argument("--rust-json", type=Path, help="output of the cli_options example") + args = parser.parse_args() + + markdown, data = build(python_build(), rust_build(args.rust_json)) + if args.check: + stale = [ + f.name + for f, text in ((MARKDOWN, markdown), (JSON_FILE, data)) + if not f.exists() or f.read_text(encoding="utf-8") != text + ] + for name in stale: + print(f"{name} is out of date: run uv run python docs/cli_matrix.py", file=sys.stderr) + return 1 if stale else 0 + MARKDOWN.write_text(markdown, encoding="utf-8", newline="\n") + JSON_FILE.write_text(data, encoding="utf-8", newline="\n") + print(f"wrote {MARKDOWN.name} and {JSON_FILE.name}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/rust/examples/cli_options.rs b/rust/examples/cli_options.rs new file mode 100644 index 0000000..c428bd6 --- /dev/null +++ b/rust/examples/cli_options.rs @@ -0,0 +1,57 @@ +//! Prints every command of the Rust build with its options as JSON, read from clap itself +//! (hidden commands and options included), for `docs/cli_matrix.py`. +//! +//! cargo run --quiet --example cli_options + +use clap::{Command, CommandFactory}; +use codigest::commands::Cli; +use serde_json::{Map, Value, json}; + +/// Each option's names, long names first (`--copy`, then aliases, then `-c`), and whether it is +/// hidden from `--help`. +fn options(command: &Command) -> Vec { + command + .get_arguments() + .filter(|a| !a.is_positional()) + .filter_map(|arg| { + let mut names: Vec = Vec::new(); + names.extend(arg.get_long().map(|l| format!("--{l}"))); + names.extend( + arg.get_all_aliases() + .unwrap_or_default() + .iter() + .map(|l| format!("--{l}")), + ); + names.extend(arg.get_short().map(|c| format!("-{c}"))); + names.extend( + arg.get_all_short_aliases() + .unwrap_or_default() + .iter() + .map(|c| format!("-{c}")), + ); + names.retain(|n| n != "-h" && n != "--help"); + (!names.is_empty()).then(|| json!({"names": names, "hidden": arg.is_hide_set()})) + }) + .collect() +} + +fn walk(command: &Command, path: &[&str], hidden: bool, out: &mut Map) { + out.insert( + path.join(" "), + json!({"hidden": hidden, "options": options(command)}), + ); + for sub in command.get_subcommands().filter(|s| s.get_name() != "help") { + let mut sub_path = path.to_vec(); + sub_path.push(sub.get_name()); + walk(sub, &sub_path, hidden || sub.is_hide_set(), out); + } +} + +fn main() { + let mut out = Map::new(); + walk(&Cli::command(), &[], false, &mut out); + println!( + "{}", + serde_json::to_string_pretty(&Value::Object(out)).expect("valid JSON") + ); +}