litclaude-ai 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/README_ko-KR.md +374 -0
- package/RELEASE_CHECKLIST.md +165 -0
- package/bin/litclaude-ai.js +643 -0
- package/cover.png +0 -0
- package/docs/agents.md +67 -0
- package/docs/hooks.md +134 -0
- package/docs/lsp.md +40 -0
- package/docs/migration.md +209 -0
- package/docs/workflow-compatibility-audit.md +119 -0
- package/generate_cover.py +123 -0
- package/package.json +48 -0
- package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
- package/plugins/litclaude/.lsp.json +13 -0
- package/plugins/litclaude/.mcp.json +9 -0
- package/plugins/litclaude/agents/boulder-executor.md +12 -0
- package/plugins/litclaude/agents/librarian-researcher.md +15 -0
- package/plugins/litclaude/agents/oracle-verifier.md +16 -0
- package/plugins/litclaude/agents/prometheus-planner.md +13 -0
- package/plugins/litclaude/agents/qa-runner.md +16 -0
- package/plugins/litclaude/agents/quality-reviewer.md +17 -0
- package/plugins/litclaude/bin/litclaude-hook.js +110 -0
- package/plugins/litclaude/bin/litclaude-hud.js +271 -0
- package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
- package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
- package/plugins/litclaude/commands/deep-interview.md +21 -0
- package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
- package/plugins/litclaude/commands/lit-loop.md +40 -0
- package/plugins/litclaude/commands/lit-plan.md +35 -0
- package/plugins/litclaude/commands/litgoal.md +30 -0
- package/plugins/litclaude/commands/review-work.md +35 -0
- package/plugins/litclaude/commands/start-work.md +36 -0
- package/plugins/litclaude/hooks/hooks.json +54 -0
- package/plugins/litclaude/lib/context-pressure.mjs +25 -0
- package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
- package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
- package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
- package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
- package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
- package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
- package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
- package/plugins/litclaude/lib/workflow-check.mjs +83 -0
- package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
- package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
- package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
- package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
- package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
- package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
- package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
- package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
- package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
- package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
- package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
- package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
- package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
- package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
- package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
- package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
- package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
- package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
- package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
- package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
- package/plugins/litclaude/skills/programming/SKILL.md +106 -0
- package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
- package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
- package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
- package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
- package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
- package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
- package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
- package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
- package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
- package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
- package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
- package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
- package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
- package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
- package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
- package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
- package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
- package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
- package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
- package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
- package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
- package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
- package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
- package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
- package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
- package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
- package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
- package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
- package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
- package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
- package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
- package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
- package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
- package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
- package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
- package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
- package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
- package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
- package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
- package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
- package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
- package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
- package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
- package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
- package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
- package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
- package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
- package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
- package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
- package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
- package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
- package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
- package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
- package/plugins/litclaude/skills/rules/SKILL.md +66 -0
- package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
- package/scripts/audit-plan-checkboxes.mjs +37 -0
- package/scripts/doctor.mjs +41 -0
- package/scripts/inspect-agent-tools.mjs +27 -0
- package/scripts/postinstall.mjs +50 -0
- package/scripts/qa-claude-plugin-smoke.sh +60 -0
- package/scripts/qa-portable-install.sh +136 -0
- package/scripts/validate-plugin.mjs +72 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# CLI Stack — clap + color-eyre + tracing + indicatif + dialoguer
|
|
2
|
+
|
|
3
|
+
The default for any new CLI tool. Strict typing on arguments, beautiful errors, progress feedback, interactive prompts when needed.
|
|
4
|
+
|
|
5
|
+
## Cargo.toml
|
|
6
|
+
|
|
7
|
+
```toml
|
|
8
|
+
[package]
|
|
9
|
+
name = "mytool"
|
|
10
|
+
version = "0.1.0"
|
|
11
|
+
edition = "2024"
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
clap = { version = "4", features = ["derive", "env", "wrap_help", "color", "unicode"] }
|
|
15
|
+
clap_complete = "4"
|
|
16
|
+
color-eyre = "0.6"
|
|
17
|
+
tracing = "0.1"
|
|
18
|
+
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
|
19
|
+
anyhow = "1"
|
|
20
|
+
indicatif = { version = "0.17", features = ["tokio"] }
|
|
21
|
+
dialoguer = { version = "0.11", features = ["fuzzy-select"] }
|
|
22
|
+
console = "0.15"
|
|
23
|
+
tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs", "process", "signal"] }
|
|
24
|
+
|
|
25
|
+
[profile.release]
|
|
26
|
+
opt-level = 3
|
|
27
|
+
lto = "fat"
|
|
28
|
+
codegen-units = 1
|
|
29
|
+
strip = "symbols"
|
|
30
|
+
panic = "abort"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Command structure
|
|
34
|
+
|
|
35
|
+
```rust
|
|
36
|
+
// src/cli.rs
|
|
37
|
+
use clap::{Parser, Subcommand, ValueEnum};
|
|
38
|
+
use std::path::PathBuf;
|
|
39
|
+
|
|
40
|
+
#[derive(Debug, Parser)]
|
|
41
|
+
#[command(
|
|
42
|
+
name = "mytool",
|
|
43
|
+
author,
|
|
44
|
+
version,
|
|
45
|
+
about = "A short description",
|
|
46
|
+
long_about = "A longer description that appears in --help",
|
|
47
|
+
arg_required_else_help = true,
|
|
48
|
+
)]
|
|
49
|
+
pub struct Cli {
|
|
50
|
+
/// Configuration file path
|
|
51
|
+
#[arg(short, long, env = "MYTOOL_CONFIG", default_value = "config.toml", global = true)]
|
|
52
|
+
pub config: PathBuf,
|
|
53
|
+
|
|
54
|
+
/// Increase verbosity (-v info, -vv debug, -vvv trace)
|
|
55
|
+
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
|
|
56
|
+
pub verbose: u8,
|
|
57
|
+
|
|
58
|
+
/// Suppress all non-error output
|
|
59
|
+
#[arg(short, long, global = true, conflicts_with = "verbose")]
|
|
60
|
+
pub quiet: bool,
|
|
61
|
+
|
|
62
|
+
/// Force colored output even when stdout is not a terminal
|
|
63
|
+
#[arg(long, global = true, value_enum, default_value_t = ColorChoice::Auto)]
|
|
64
|
+
pub color: ColorChoice,
|
|
65
|
+
|
|
66
|
+
/// Output format
|
|
67
|
+
#[arg(short, long, global = true, value_enum, default_value_t = OutputFormat::Pretty)]
|
|
68
|
+
pub format: OutputFormat,
|
|
69
|
+
|
|
70
|
+
#[command(subcommand)]
|
|
71
|
+
pub command: Command,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#[derive(Debug, Clone, ValueEnum)]
|
|
75
|
+
pub enum ColorChoice { Auto, Always, Never }
|
|
76
|
+
|
|
77
|
+
#[derive(Debug, Clone, ValueEnum)]
|
|
78
|
+
pub enum OutputFormat { Pretty, Json, Plain }
|
|
79
|
+
|
|
80
|
+
#[derive(Debug, Subcommand)]
|
|
81
|
+
pub enum Command {
|
|
82
|
+
/// Build the thing
|
|
83
|
+
Build(BuildArgs),
|
|
84
|
+
/// Watch and rebuild
|
|
85
|
+
Watch(WatchArgs),
|
|
86
|
+
/// Generate shell completions
|
|
87
|
+
Completions { #[arg(value_enum)] shell: clap_complete::Shell },
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[derive(Debug, clap::Args)]
|
|
91
|
+
pub struct BuildArgs {
|
|
92
|
+
/// Target directory
|
|
93
|
+
#[arg(short, long, default_value = "target")]
|
|
94
|
+
pub target: PathBuf,
|
|
95
|
+
/// Build mode
|
|
96
|
+
#[arg(short, long, value_enum, default_value_t = Mode::Release)]
|
|
97
|
+
pub mode: Mode,
|
|
98
|
+
/// Specific files to build (default: all)
|
|
99
|
+
pub files: Vec<PathBuf>,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#[derive(Debug, clap::Args)]
|
|
103
|
+
pub struct WatchArgs {
|
|
104
|
+
/// Glob pattern to watch
|
|
105
|
+
#[arg(short, long, default_value = "**/*.rs")]
|
|
106
|
+
pub pattern: String,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#[derive(Debug, Clone, ValueEnum)]
|
|
110
|
+
pub enum Mode { Debug, Release }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Key clap derive patterns:
|
|
114
|
+
|
|
115
|
+
- `env = "VAR"` — falls back to env var if flag not given.
|
|
116
|
+
- `global = true` — flag inherits to subcommands.
|
|
117
|
+
- `arg_required_else_help = true` — running with no args prints help instead of erroring.
|
|
118
|
+
- `value_enum` on an enum — case-insensitive parsing + auto-completion.
|
|
119
|
+
- `action = clap::ArgAction::Count` — `-v` is 1, `-vv` is 2, etc.
|
|
120
|
+
- `conflicts_with` — incompatible flags.
|
|
121
|
+
|
|
122
|
+
## Main + tracing init
|
|
123
|
+
|
|
124
|
+
```rust
|
|
125
|
+
// src/main.rs
|
|
126
|
+
use clap::Parser;
|
|
127
|
+
use mytool::cli::{Cli, Command, ColorChoice};
|
|
128
|
+
use tracing::Level;
|
|
129
|
+
use tracing_subscriber::EnvFilter;
|
|
130
|
+
|
|
131
|
+
fn main() -> color_eyre::Result<()> {
|
|
132
|
+
color_eyre::install()?;
|
|
133
|
+
let cli = Cli::parse();
|
|
134
|
+
init_tracing(&cli);
|
|
135
|
+
|
|
136
|
+
if matches!(cli.color, ColorChoice::Always) {
|
|
137
|
+
console::set_colors_enabled(true);
|
|
138
|
+
} else if matches!(cli.color, ColorChoice::Never) {
|
|
139
|
+
console::set_colors_enabled(false);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
match cli.command {
|
|
143
|
+
Command::Build(args) => mytool::commands::build::run(&cli, args),
|
|
144
|
+
Command::Watch(args) => mytool::commands::watch::run(&cli, args),
|
|
145
|
+
Command::Completions { shell } => {
|
|
146
|
+
let mut cmd = <Cli as clap::CommandFactory>::command();
|
|
147
|
+
clap_complete::generate(shell, &mut cmd, "mytool", &mut std::io::stdout());
|
|
148
|
+
Ok(())
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn init_tracing(cli: &Cli) {
|
|
154
|
+
let level = if cli.quiet {
|
|
155
|
+
Level::ERROR
|
|
156
|
+
} else {
|
|
157
|
+
match cli.verbose {
|
|
158
|
+
0 => Level::WARN,
|
|
159
|
+
1 => Level::INFO,
|
|
160
|
+
2 => Level::DEBUG,
|
|
161
|
+
_ => Level::TRACE,
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
let filter = EnvFilter::try_from_default_env()
|
|
165
|
+
.unwrap_or_else(|_| EnvFilter::new(format!("mytool={level}")));
|
|
166
|
+
tracing_subscriber::fmt()
|
|
167
|
+
.with_env_filter(filter)
|
|
168
|
+
.with_target(false)
|
|
169
|
+
.without_time()
|
|
170
|
+
.compact()
|
|
171
|
+
.with_writer(std::io::stderr)
|
|
172
|
+
.init();
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Tracing on a CLI:
|
|
177
|
+
- **Write to stderr.** stdout is for the tool's actual output (which the user might pipe). Logs and progress bars go to stderr.
|
|
178
|
+
- **Verbosity from `-v`, not from `RUST_LOG`.** Users expect `-v` on a CLI; `RUST_LOG` is a developer escape hatch (kept, but secondary).
|
|
179
|
+
|
|
180
|
+
## Progress bars — `indicatif`
|
|
181
|
+
|
|
182
|
+
```rust
|
|
183
|
+
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
|
184
|
+
use std::time::Duration;
|
|
185
|
+
|
|
186
|
+
let mp = MultiProgress::new();
|
|
187
|
+
let pb = mp.add(ProgressBar::new(files.len() as u64));
|
|
188
|
+
pb.set_style(
|
|
189
|
+
ProgressStyle::with_template(
|
|
190
|
+
"{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta}) {msg}"
|
|
191
|
+
)?
|
|
192
|
+
.progress_chars("=>-")
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
for file in files {
|
|
196
|
+
pb.set_message(file.display().to_string());
|
|
197
|
+
process(&file)?;
|
|
198
|
+
pb.inc(1);
|
|
199
|
+
}
|
|
200
|
+
pb.finish_with_message("done");
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
For unbounded operations:
|
|
204
|
+
|
|
205
|
+
```rust
|
|
206
|
+
let spinner = ProgressBar::new_spinner();
|
|
207
|
+
spinner.enable_steady_tick(Duration::from_millis(80));
|
|
208
|
+
spinner.set_message("connecting…");
|
|
209
|
+
let result = connect().await?;
|
|
210
|
+
spinner.finish_and_clear();
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
With multiple parallel tasks:
|
|
214
|
+
|
|
215
|
+
```rust
|
|
216
|
+
let mp = MultiProgress::new();
|
|
217
|
+
let bars: Vec<_> = (0..workers).map(|i| {
|
|
218
|
+
let pb = mp.add(ProgressBar::new(unit));
|
|
219
|
+
pb.set_style(ProgressStyle::with_template("worker {prefix}: {pos}/{len}")?);
|
|
220
|
+
pb.set_prefix(i.to_string());
|
|
221
|
+
pb
|
|
222
|
+
}).collect();
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`MultiProgress` keeps bars stacked and redraws cleanly even with concurrent updates from multiple tasks.
|
|
226
|
+
|
|
227
|
+
When stdout is not a terminal, indicatif silently disables animation. Force on/off with `pb.set_draw_target(ProgressDrawTarget::stdout())` / `hidden()`.
|
|
228
|
+
|
|
229
|
+
## Interactive prompts — `dialoguer`
|
|
230
|
+
|
|
231
|
+
```rust
|
|
232
|
+
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select, FuzzySelect, MultiSelect};
|
|
233
|
+
|
|
234
|
+
let name: String = Input::with_theme(&ColorfulTheme::default())
|
|
235
|
+
.with_prompt("Project name")
|
|
236
|
+
.validate_with(|input: &String| -> Result<(), &str> {
|
|
237
|
+
if input.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
|
|
238
|
+
Ok(())
|
|
239
|
+
} else {
|
|
240
|
+
Err("alphanumeric, dash, underscore only")
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
.interact_text()?;
|
|
244
|
+
|
|
245
|
+
let secret = Password::with_theme(&ColorfulTheme::default())
|
|
246
|
+
.with_prompt("API key")
|
|
247
|
+
.with_confirmation("Repeat", "passwords don't match")
|
|
248
|
+
.interact()?;
|
|
249
|
+
|
|
250
|
+
let go: bool = Confirm::with_theme(&ColorfulTheme::default())
|
|
251
|
+
.with_prompt(format!("Delete {}? This cannot be undone.", path.display()))
|
|
252
|
+
.default(false)
|
|
253
|
+
.interact()?;
|
|
254
|
+
if !go { return Ok(()); }
|
|
255
|
+
|
|
256
|
+
let items = ["yes", "no", "maybe"];
|
|
257
|
+
let idx = Select::with_theme(&ColorfulTheme::default())
|
|
258
|
+
.with_prompt("Pick one")
|
|
259
|
+
.items(&items)
|
|
260
|
+
.default(0)
|
|
261
|
+
.interact()?;
|
|
262
|
+
|
|
263
|
+
let picks = MultiSelect::with_theme(&ColorfulTheme::default())
|
|
264
|
+
.with_prompt("Toggle features")
|
|
265
|
+
.items(&["alpha", "beta", "gamma"])
|
|
266
|
+
.defaults(&[true, false, false])
|
|
267
|
+
.interact()?;
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Detect non-TTY before prompting:
|
|
271
|
+
|
|
272
|
+
```rust
|
|
273
|
+
if !console::user_attended() {
|
|
274
|
+
return Err(anyhow::anyhow!("input required but stdin is not a terminal"));
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
For automated tests, expose a `--non-interactive` flag and gate all prompts behind it.
|
|
279
|
+
|
|
280
|
+
## Structured output
|
|
281
|
+
|
|
282
|
+
```rust
|
|
283
|
+
match cli.format {
|
|
284
|
+
OutputFormat::Json => {
|
|
285
|
+
serde_json::to_writer(std::io::stdout().lock(), &result)?;
|
|
286
|
+
println!();
|
|
287
|
+
}
|
|
288
|
+
OutputFormat::Plain => {
|
|
289
|
+
for row in &result.rows {
|
|
290
|
+
println!("{}\t{}\t{}", row.a, row.b, row.c);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
OutputFormat::Pretty => {
|
|
294
|
+
use console::{style, Term};
|
|
295
|
+
let term = Term::stdout();
|
|
296
|
+
for row in &result.rows {
|
|
297
|
+
term.write_line(&format!(
|
|
298
|
+
"{} {} {}",
|
|
299
|
+
style(&row.a).green(),
|
|
300
|
+
style(&row.b).yellow(),
|
|
301
|
+
style(&row.c).dim(),
|
|
302
|
+
))?;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Always offer `--format json` for piping into `jq`, scripts, and other tools.
|
|
309
|
+
|
|
310
|
+
## Shell completions
|
|
311
|
+
|
|
312
|
+
Already shown in the `Completions` subcommand above. Distribute completions by adding to the install script:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
mytool completions bash > /etc/bash_completion.d/mytool
|
|
316
|
+
mytool completions fish > ~/.config/fish/completions/mytool.fish
|
|
317
|
+
mytool completions zsh > "${fpath[1]}/_mytool"
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Signal handling
|
|
321
|
+
|
|
322
|
+
```rust
|
|
323
|
+
// In an async CLI command
|
|
324
|
+
use tokio::signal::ctrl_c;
|
|
325
|
+
|
|
326
|
+
tokio::select! {
|
|
327
|
+
_ = ctrl_c() => {
|
|
328
|
+
tracing::warn!("interrupted, cleaning up");
|
|
329
|
+
cleanup().await?;
|
|
330
|
+
std::process::exit(130); // standard exit code for SIGINT
|
|
331
|
+
}
|
|
332
|
+
result = long_running_task() => {
|
|
333
|
+
result
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
For sync CLIs, install a one-shot handler with `ctrlc` crate:
|
|
339
|
+
|
|
340
|
+
```rust
|
|
341
|
+
let interrupted = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
342
|
+
let i = interrupted.clone();
|
|
343
|
+
ctrlc::set_handler(move || i.store(true, std::sync::atomic::Ordering::SeqCst))?;
|
|
344
|
+
|
|
345
|
+
while !interrupted.load(std::sync::atomic::Ordering::Relaxed) {
|
|
346
|
+
do_step()?;
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Error reporting with color-eyre
|
|
351
|
+
|
|
352
|
+
```rust
|
|
353
|
+
fn main() -> color_eyre::Result<()> {
|
|
354
|
+
color_eyre::config::HookBuilder::default()
|
|
355
|
+
.display_env_section(false) // hide SPANTRACE/BACKTRACE env hints by default
|
|
356
|
+
.display_location_section(false) // hide file:line section
|
|
357
|
+
.panic_section("If this is a bug, please report at https://github.com/me/mytool/issues")
|
|
358
|
+
.install()?;
|
|
359
|
+
real_main()
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Errors with `.wrap_err("...")` from `eyre::WrapErr` (compatible with anyhow's `.context`) show as a numbered chain. `RUST_BACKTRACE=1` shows the full trace; `RUST_SPANTRACE=1` shows tracing spans where the error fired.
|
|
364
|
+
|
|
365
|
+
## Distribution
|
|
366
|
+
|
|
367
|
+
- Add `cargo dist init` for prebuilt binary release pipeline (cross-platform tarballs + installers).
|
|
368
|
+
- Publish to Homebrew tap, AUR, scoop, Chocolatey via dist.
|
|
369
|
+
- Sign Linux binaries with `cosign` if your audience is enterprise.
|
|
370
|
+
- Build single static binary on Linux with `--target x86_64-unknown-linux-musl` (or `aarch64-unknown-linux-musl`).
|
|
371
|
+
- For wasm-runnable CLIs (`wasi-cli`), add `--target wasm32-wasip1`.
|
|
372
|
+
|
|
373
|
+
## Common mistakes
|
|
374
|
+
|
|
375
|
+
1. **Mixing stdout and stderr.** Tool output goes to stdout; logs and progress go to stderr.
|
|
376
|
+
2. **No `--non-interactive` flag.** Interactive prompts block automation.
|
|
377
|
+
3. **Printing colored output unconditionally.** Honor `NO_COLOR` env var, detect TTY with `console::user_attended()`.
|
|
378
|
+
4. **`println!` for errors.** Use `tracing::error!` so logs go to stderr automatically and respect verbosity.
|
|
379
|
+
5. **`unwrap()` on `Cli::parse()`.** clap returns clean errors with `--help` text; `parse()` exits on its own.
|
|
380
|
+
6. **Long subcommand handlers in `main.rs`.** Split into `src/commands/<name>.rs` per command.
|
|
381
|
+
7. **Missing exit code semantics.** Use `std::process::exit(1)` (general error), `2` (usage), `130` (SIGINT) appropriately. Or return `Result` and let main map.
|
|
382
|
+
|
|
383
|
+
## Testing CLIs
|
|
384
|
+
|
|
385
|
+
```rust
|
|
386
|
+
// tests/cli.rs
|
|
387
|
+
use assert_cmd::Command;
|
|
388
|
+
use predicates::prelude::*;
|
|
389
|
+
|
|
390
|
+
#[test]
|
|
391
|
+
fn shows_help() {
|
|
392
|
+
Command::cargo_bin("mytool").unwrap()
|
|
393
|
+
.arg("--help")
|
|
394
|
+
.assert()
|
|
395
|
+
.success()
|
|
396
|
+
.stdout(predicate::str::contains("Usage:"));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#[test]
|
|
400
|
+
fn rejects_unknown_subcommand() {
|
|
401
|
+
Command::cargo_bin("mytool").unwrap()
|
|
402
|
+
.arg("nope")
|
|
403
|
+
.assert()
|
|
404
|
+
.failure()
|
|
405
|
+
.stderr(predicate::str::contains("unrecognized subcommand"));
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
`assert_cmd` builds the binary once per test run and gives a fluent assertion API.
|