Files
pbakaus_impeccable/crates/cli/src/main.rs
T
641ff95502 CLI: --version reports the npm package version (#731)
* CLI: --version reports the npm package version

npx impeccable --version printed 3.6.0 after the 4.0.0 release because
the shim handed every argument to the engine, whose baked-in
CLI_VERSION still said 3.6.0. The shim now answers --version and -v
from its own package.json, as docs/CLI-CONTRACT.md specifies, without
locating or downloading a binary; a test covers both flags. The
engine's CLI_VERSION moves to 4.0.0 for the next engine release, with
the cli-version golden re-recorded and the delta noted.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* CLI shim: --version wins whenever it leads, trailing arguments or not

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-04 13:52:32 -07:00

137 lines
6.1 KiB
Rust

//! `impeccable` binary: verb router.
//!
//! Every skill script and CLI subcommand is a verb here. Verb crates expose
//! `run(args: &[String], io: &mut Io) -> i32` (exit code) and never call
//! `std::process::exit` themselves, so this file is the single place exit codes
//! and stream flushing are decided (contract: docs/CLI-CONTRACT.md in the
//! public repo). Verb names are the JS script basenames; a few carry aliases
//! (`signals` for context-signals, `hooks` for hook-admin).
use std::io::Write;
use impeccable_common::Io;
mod font_render;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut io = Io::stdio();
let code = run(&args, &mut io);
let _ = io.stdout.flush();
let _ = io.stderr.flush();
std::process::exit(code);
}
fn run(args: &[String], io: &mut Io) -> i32 {
// cli/bin/cli.js dispatch: help / version / detect / ignores / skills verbs
let Some(verb) = args.first().map(String::as_str) else {
io.out(impeccable_detect::ROOT_USAGE);
return 0;
};
let rest = &args[1..];
match verb {
"--help" | "-h" => {
io.out(impeccable_detect::ROOT_USAGE);
0
}
"--version" | "-v" => {
io.out(&format!("{CLI_VERSION}\n"));
0
}
// Launcher handshake: a cheap discriminator so the launchers can tell
// this engine apart from the retired 3.x npm CLI (which answers any
// unknown verb with `Unknown command`, exit 1) before exec'ing a
// candidate found on PATH or in the unversioned user cache. Kept out
// of --help on purpose; not part of the user-facing contract.
"engine-probe" => {
io.out(&format!("impeccable-engine {VERSION}\n"));
0
}
"detect" => impeccable_detect::run_detect(rest, io, &engines()),
"ignores" | "ignore" => impeccable_detect::run_ignores(rest, io),
"skills" => impeccable_skills::run(rest, io),
"help" | "install" | "link" | "update" | "check" => impeccable_skills::run(args, io),
// skill scripts
"context" => impeccable_context::run_context(rest, io),
"pin" => impeccable_context::run_pin(rest, io),
"detect-csp" => impeccable_context::run_detect_csp(rest, io),
"palette" => impeccable_context::run_palette(rest, io),
"surface-brief" => impeccable_context::run_surface_brief(rest, io),
"critique-storage" => impeccable_context::run_critique_storage(rest, io),
"embed-prompt" => impeccable_context::run_embed_prompt(rest, io),
"signals" | "context-signals" => impeccable_context::run_signals(rest, io),
"doctor" => impeccable_context::run_doctor(rest, io),
"concept-seed" => impeccable_context::run_concept_seed(rest, io),
"generate-image" => impeccable_context::run_generate_image(rest, io),
"serve-question" => impeccable_context::run_serve_question(rest, io),
// comp-fidelity verbs (crates/comp-verbs over crates/comp)
"comp-spec" => impeccable_comp_verbs::run_comp_spec(rest, io),
"comp-diff" => impeccable_comp_verbs::run_comp_diff(rest, io),
"font-match" => {
let mut renderer = font_render::CdpFontRenderer::from_process_env();
impeccable_comp_verbs::run_font_match(rest, io, &mut renderer)
}
"build-phase" => {
// Inject the organic-clip-path CSS scanner (a rule that lives in the
// closed `core` crate) so comp-verbs stays core-free.
let organic = |html: &str| -> Vec<(Option<String>, String)> {
impeccable_core::checks::css_scan::scan_css_text_for_organic_clip_path(html)
.into_iter()
.map(|f| (f.selector, f.snippet))
.collect()
};
impeccable_comp_verbs::run_build_phase(rest, io, &organic)
}
"hook" => impeccable_hook::run_hook(rest, io, engines().html),
"hook-before-edit" => impeccable_hook::run_hook_before_edit(rest, io, engines().html),
"hooks" | "hook-admin" => impeccable_hook::run_hook_admin(rest, io),
v if v.starts_with("live") => impeccable_live::run(v, rest, io),
// `npx impeccable src/` shorthand: a path-shaped, flag, URL, or existing
// first arg is a detect target (cli.js looksLikeDetectTarget).
v if impeccable_detect::looks_like_detect_target(v, &io.cwd.to_string_lossy()) => {
impeccable_detect::run_detect(args, io, &engines())
}
"init" => {
io.err(impeccable_detect::INIT_MESSAGE);
1
}
other => {
io.err(&format!(
"Unknown command: \"{other}\"\n\nTo see a list of supported commands, run:\n impeccable --help\n"
));
1
}
}
}
/// The npm `impeccable` package version `cli.js --version` prints (its
/// `package.json`), tracked separately from the crate version.
pub const CLI_VERSION: &str = "4.0.0";
/// The engines wired into `impeccable detect`: the static HTML engine
/// (crates/html). The browser engine (crates/browser) plugs in here once it
/// lands; until then URL scans report the puppeteer message.
fn engines() -> impeccable_detect::Engines<'static> {
static HTML: impeccable_html::StaticHtmlEngine = impeccable_html::StaticHtmlEngine {
// The shipped binary carries the built-in rules only.
static_rule_pack: None,
};
impeccable_detect::Engines {
html: &HTML,
url: Some(url_engine()),
}
}
// --- browser engine (crates/browser) -------------------------------------
/// The URL engine, built once from the process environment (browser
/// discovery reads `IMPECCABLE_BROWSER` / `PUPPETEER_EXECUTABLE_PATH` /
/// `CHROME_PATH`, sandbox flags read `CI`).
fn url_engine() -> &'static impeccable_browser::BrowserEngine {
static ENGINE: std::sync::OnceLock<impeccable_browser::BrowserEngine> =
std::sync::OnceLock::new();
ENGINE.get_or_init(impeccable_browser::BrowserEngine::from_process_env)
}
// -------------------------------------------------------------------------