Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,12 @@ jobs:

run_workflow() {
local command="$1"
shift
local output
local status

set +e
output="$(target/debug/wright "$command" --kind workshop "$PROJECT" --format json)"
output="$(target/debug/wright "$command" "$@" --kind workshop "$PROJECT" --format json)"
status=$?
set -e

Expand All @@ -220,16 +221,22 @@ jobs:
printf '%s' "$output"
}

check_output="$(run_workflow check)"
lint_output="$(run_workflow lint)"
check_output="$(run_workflow check --all)"
lint_output="$(run_workflow lint --all)"
inspect_output="$(run_workflow inspect)"
jq -e -n \
--argjson check "$check_output" \
--argjson lint "$lint_output" \
--argjson inspect "$inspect_output" \
--argjson expected_exit "$EXPECTED_EXIT" \
'(
$check.diagnostics == $lint.diagnostics and
'def diagnostic_contract:
map({code, severity, evidence,
location: (.location // (if .span then
"\(.span.path):\(.span.start.line):\(.span.start.col)"
else null end))}) | sort;
(
($check.diagnostics | diagnostic_contract) ==
(($lint.diagnostics + ($lint.result.findings // [])) | diagnostic_contract) and
$check.ok == $lint.ok and
$check.exit == $lint.exit and
(if $expected_exit == 0
Expand Down
11 changes: 6 additions & 5 deletions crates/wright-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ UPDATE OPTIONS:

#[derive(Debug, Subcommand)]
pub(crate) enum Command {
/// Parse, lower, validate, and emit Workshop text.
/// Parse, lower, validate, lint, and emit Workshop text.
Compile(CompileArgs),
/// Reconstruct validated Workshop input as canonical OPY source; OSTW is
/// recognized but unavailable because DEL/OSTW provider support is not
/// currently shipped.
Convert(ConvertArgs),
/// Check frontend, project, semantic, and validation correctness.
/// Check frontend, project, semantic, validation, and lint diagnostics.
Check(ReportArgs),
/// Summarize Workshop cost, complexity hotspots, risk indicators, and
/// cross-cutting state.
Expand Down Expand Up @@ -136,6 +136,8 @@ pub(crate) enum Command {
pub(crate) struct CompileArgs {
#[command(flatten)]
pub(crate) common: CommonArgs,
#[command(flatten)]
pub(crate) policy: crate::serve::LintPolicyArgs,
/// Write the program to PATH instead of `<input stem>.ow`; `-` prints the
/// program to stdout and nothing else.
#[arg(short = 'o', long, value_name = "PATH")]
Expand Down Expand Up @@ -190,9 +192,8 @@ pub(crate) struct ReportArgs {
pub(crate) common: CommonArgs,
#[command(flatten)]
pub(crate) select: SelectArgs,
/// Load a local YAML rule file or directory (repeatable).
#[arg(long = "rule-file", value_name = "PATH")]
pub(crate) rule_file: Vec<PathBuf>,
#[command(flatten)]
pub(crate) policy: crate::serve::LintPolicyArgs,
}

/// Arguments of `analyze`: shared workflow options, finding selection, and
Expand Down
137 changes: 137 additions & 0 deletions crates/wright-cli/src/compile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use std::io::{IsTerminal, Write};
use std::process::ExitCode;

use wright_driver::result::exit;
use wright_driver::{CompileWrite, OutputDest, Overwrite};

use crate::cli::{self, OutputFormatArg};
use crate::{config_from_common, present, run_configured};

pub(crate) fn run(args: &cli::CompileArgs) -> ExitCode {
let to_stdout = args.output.as_deref().is_some_and(|p| p.as_os_str() == "-");
if to_stdout && args.common.format == OutputFormatArg::Json {
present::terminal::report_error(
present::Presentation::from_common(&args.common),
"USAGE",
"compile",
"-o - prints the program to stdout and cannot be combined with --format json",
);
return ExitCode::from(exit::USAGE);
}
let dest = match &args.output {
_ if to_stdout => OutputDest::Stdout,
Some(path) => OutputDest::Path(path.clone()),
None => OutputDest::Default,
};
let interactive = std::io::stdin().is_terminal() && std::io::stderr().is_terminal();
let copy = args.copy;
let force = args.force;
let mut compile_config = config_from_common(&args.common);
if let Err(message) = args.policy.apply_to(&mut compile_config) {
present::terminal::report_error(
present::Presentation::from_common(&args.common),
"USAGE",
"compile",
&message,
);
return ExitCode::from(exit::USAGE);
}
compile_config.run = wright_driver::config::RunOptions {
no_cache: args.no_cache,
baseline: args.baseline.clone(),
save_baseline: args.save_baseline.clone(),
max_total: None,
};
run_configured(
"compile",
compile_config,
present::Presentation::from_common(&args.common),
None,
move |session: &mut wright_driver::CompilerSession| {
let mut confirm = |path: &std::path::Path| {
confirm_overwrite(
path,
&mut std::io::stdin().lock(),
&mut std::io::stderr().lock(),
)
};
let overwrite = if force {
Overwrite::Force
} else if interactive {
Overwrite::Ask(&mut confirm)
} else {
Overwrite::Refuse
};
let to = args.to.clone();
let mut envelope = session.compile_to(CompileWrite {
dest,
overwrite,
to,
});
let copied = envelope
.result
.output
.as_ref()
.filter(|_| copy && envelope.ok)
.map(|output| {
arboard::Clipboard::new()
.and_then(|mut clipboard| clipboard.set_text(&output.text))
});
if let Some(Err(error)) = copied {
envelope
.diagnostics
.push(wright_driver::Diagnostic::warning(
"clipboard-failed",
wright_driver::Stage::Emission,
format!("cannot copy to the clipboard: {error}"),
));
}
envelope
},
)
}

fn confirm_overwrite(
path: &std::path::Path,
input: &mut impl std::io::BufRead,
prompt: &mut impl Write,
) -> bool {
let _ = write!(
prompt,
"Overwrite {}? [y/N] ",
wright_driver::input::display_path(path)
);
let _ = prompt.flush();
let mut answer = String::new();
input.read_line(&mut answer).is_ok()
&& matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn overwrite_prompt_defaults_to_no_and_accepts_only_yes() {
for (answer, expected) in [
("y\n", true),
("Yes\n", true),
("\n", false),
("n\n", false),
("", false),
("maybe\n", false),
] {
let mut prompt = Vec::new();
let confirmed = confirm_overwrite(
std::path::Path::new("mode.ow"),
&mut answer.as_bytes(),
&mut prompt,
);
assert_eq!(confirmed, expected, "{answer:?}");
assert_eq!(
String::from_utf8(prompt).unwrap(),
"Overwrite mode.ow? [y/N] "
);
}
}
}
131 changes: 12 additions & 119 deletions crates/wright-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod agent;
mod agenttools;
mod cli;
mod compile;
mod completion;
mod essential;
mod lint;
Expand All @@ -13,8 +14,8 @@ mod tooldefs;
mod update;
mod usage_hint;

use std::io::Read;
use std::io::Write;
use std::io::{IsTerminal, Read};
use std::process::ExitCode;
use std::sync::Arc;

Expand All @@ -23,7 +24,6 @@ use clap::{CommandFactory, Parser, error::ErrorKind};
use wright_driver::config::{InputSpec, OutputFormat, SessionConfig, SourceKind};
use wright_driver::result::exit;
use wright_driver::source_provider::SourceBackend;
use wright_driver::{CompileWrite, OutputDest, Overwrite};

use crate::cli::{Cli, Command, CommonArgs, ConvertTargetArg, OutputFormatArg};

Expand Down Expand Up @@ -160,7 +160,7 @@ fn main() -> ExitCode {

fn run_workflow(command: Command) -> ExitCode {
match command {
Command::Compile(args) => run_compile(&args),
Command::Compile(args) => compile::run(&args),
Command::Convert(args) => {
let target = match args.target {
ConvertTargetArg::Opy => wright_driver::ConvertTarget::Opy,
Expand All @@ -177,7 +177,15 @@ fn run_workflow(command: Command) -> ExitCode {
Command::Check(args) => {
let mut config = config_from_common(&args.common);
config.selection = selection_from_args(&args.select);
config.lint_rule_paths = args.rule_file;
if let Err(message) = args.policy.apply_to(&mut config) {
present::terminal::report_error(
present::Presentation::from_common(&args.common),
"USAGE",
"check",
&message,
);
return ExitCode::from(exit::USAGE);
}
run_configured(
"check",
config,
Expand Down Expand Up @@ -368,97 +376,6 @@ pub(crate) fn run_configured<
ExitCode::from(run_command(&mut session, run, presentation, subject))
}

fn run_compile(args: &cli::CompileArgs) -> ExitCode {
let to_stdout = args.output.as_deref().is_some_and(|p| p.as_os_str() == "-");
if to_stdout && args.common.format == OutputFormatArg::Json {
present::terminal::report_error(
present::Presentation::from_common(&args.common),
"USAGE",
"compile",
"-o - prints the program to stdout and cannot be combined with --format json",
);
return ExitCode::from(exit::USAGE);
}
let dest = match &args.output {
_ if to_stdout => OutputDest::Stdout,
Some(path) => OutputDest::Path(path.clone()),
None => OutputDest::Default,
};
let interactive = std::io::stdin().is_terminal() && std::io::stderr().is_terminal();
let copy = args.copy;
let force = args.force;
let mut compile_config = config_from_common(&args.common);
compile_config.run = wright_driver::config::RunOptions {
no_cache: args.no_cache,
baseline: args.baseline.clone(),
save_baseline: args.save_baseline.clone(),
max_total: None,
};
run_configured(
"compile",
compile_config,
present::Presentation::from_common(&args.common),
None,
move |session: &mut wright_driver::CompilerSession| {
let mut confirm = |path: &std::path::Path| {
confirm_overwrite(
path,
&mut std::io::stdin().lock(),
&mut std::io::stderr().lock(),
)
};
let overwrite = if force {
Overwrite::Force
} else if interactive {
Overwrite::Ask(&mut confirm)
} else {
Overwrite::Refuse
};
let to = args.to.clone();
let mut envelope = session.compile_to(CompileWrite {
dest,
overwrite,
to,
});
let copied = envelope
.result
.output
.as_ref()
.filter(|_| copy && envelope.ok)
.map(|output| {
arboard::Clipboard::new()
.and_then(|mut clipboard| clipboard.set_text(&output.text))
});
if let Some(Err(error)) = copied {
envelope
.diagnostics
.push(wright_driver::Diagnostic::warning(
"clipboard-failed",
wright_driver::Stage::Emission,
format!("cannot copy to the clipboard: {error}"),
));
}
envelope
},
)
}

fn confirm_overwrite(
path: &std::path::Path,
input: &mut impl std::io::BufRead,
prompt: &mut impl Write,
) -> bool {
let _ = write!(
prompt,
"Overwrite {}? [y/N] ",
wright_driver::input::display_path(path)
);
let _ = prompt.flush();
let mut answer = String::new();
input.read_line(&mut answer).is_ok()
&& matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes")
}

fn run_lookup(args: &cli::LookupArgs) -> ExitCode {
let pres = present::Presentation::from_output(args.format, args.renderer, args.color);
let config = SessionConfig {
Expand Down Expand Up @@ -760,30 +677,6 @@ mod tests {
}
}

#[test]
fn overwrite_prompt_defaults_to_no_and_accepts_only_yes() {
for (answer, expected) in [
("y\n", true),
("Yes\n", true),
("\n", false),
("n\n", false),
("", false),
("maybe\n", false),
] {
let mut prompt = Vec::new();
let confirmed = confirm_overwrite(
std::path::Path::new("mode.ow"),
&mut answer.as_bytes(),
&mut prompt,
);
assert_eq!(confirmed, expected, "{answer:?}");
assert_eq!(
String::from_utf8(prompt).unwrap(),
"Overwrite mode.ow? [y/N] "
);
}
}

#[test]
fn every_opy_workflow_selects_the_provider_backend() {
let opy = common(Some("main.opy"), cli::SourceKindArg::Auto);
Expand Down
Loading
Loading