1mod contracts;
17mod generate;
18mod migrations;
19mod scaffold;
20mod update;
21mod validate;
22mod version;
23
24pub use generate::{check as check_generated, run as generate};
25pub use scaffold::{Options as NewOptions, run as new};
26pub use update::run as update;
27pub use version::Version;
28
29pub(crate) use generate::find_manifests;
30pub use validate::run as validate;
31
32use std::path::{Path, PathBuf};
33use std::process::{Command, ExitCode};
34
35pub fn main() -> ExitCode {
37 let task = std::env::args().nth(1);
38 let flags: Vec<String> = std::env::args().skip(2).collect();
39
40 let result = match task.as_deref() {
41 Some("validate") => validate(&workspace_root()),
42 Some("generate") if flags.iter().any(|flag| flag == "--check") => {
43 check_generated(&workspace_root())
44 }
45 Some("generate") => generate(&workspace_root()),
46 Some("update") => update(
47 &workspace_root(),
48 flags.iter().any(|flag| flag == "--dry-run"),
49 ),
50 Some("new") => new_from_args(&flags),
51 Some("ci") => ci(),
52 Some("demo") => demo(),
53 Some(other) => {
54 eprintln!("unknown task `{other}`");
55 usage();
56 return ExitCode::FAILURE;
57 }
58 None => {
59 usage();
60 return ExitCode::FAILURE;
61 }
62 };
63
64 match result {
65 Ok(()) => ExitCode::SUCCESS,
66 Err(message) => {
67 eprintln!("\nxtask failed: {message}");
68 ExitCode::FAILURE
69 }
70 }
71}
72
73fn usage() {
74 eprintln!(
75 "\nusage: cargo xtask <task>\n\n\
76 tasks:\n \
77 validate check the rules in ARCHITECTURE.md\n \
78 generate write the files derived from app.toml\n \
79 generate --check fail if generated files are stale or hand-edited\n \
80 update run pending migrations and regenerate\n \
81 update --dry-run report what an update would change\n \
82 new <slug> create a new application from the template\n \
83 ci fmt --check, clippy -D warnings, test, generate --check, validate\n \
84 demo run the reference application\n"
85 );
86}
87
88fn new_from_args(flags: &[String]) -> Result<(), String> {
90 let root = workspace_root();
91
92 let slug = flags
93 .first()
94 .filter(|argument| !argument.starts_with("--"))
95 .ok_or_else(|| {
96 "usage: cargo xtask new <slug> [--name X] [--id Y] [--into DIR] [--local]".to_owned()
97 })?
98 .clone();
99
100 let value = |name: &str| -> Option<String> {
101 flags
102 .iter()
103 .position(|flag| flag == name)
104 .and_then(|index| flags.get(index + 1))
105 .cloned()
106 };
107
108 let options = scaffold::Options {
109 name: value("--name").unwrap_or_else(|| title_case(&slug)),
110 id: value("--id").unwrap_or_else(|| format!("dev.local.{}", slug.replace('-', ""))),
111 into: value("--into")
112 .map(PathBuf::from)
113 .unwrap_or_else(|| root.clone()),
114 local: flags
117 .iter()
118 .any(|flag| flag == "--local")
119 .then(|| root.clone()),
120 slug,
121 };
122
123 scaffold::run(&root, &options)
124}
125
126fn title_case(slug: &str) -> String {
128 slug.split(['-', '_'])
129 .map(|word| {
130 let mut characters = word.chars();
131 match characters.next() {
132 Some(first) => first.to_uppercase().collect::<String>() + characters.as_str(),
133 None => String::new(),
134 }
135 })
136 .collect::<Vec<_>>()
137 .join(" ")
138}
139
140fn ci() -> Result<(), String> {
141 let root = workspace_root();
142
143 cargo(&["fmt", "--all", "--check"])?;
144 cargo(&[
145 "clippy",
146 "--workspace",
147 "--all-targets",
148 "--",
149 "-D",
150 "warnings",
151 ])?;
152 cargo(&["test", "--workspace"])?;
153 check_generated(&root)?;
154 validate(&root)
155}
156
157fn demo() -> Result<(), String> {
158 run("pnpm", &["--filter", "@origin/demo", "tauri", "dev"])
159}
160
161fn cargo(args: &[&str]) -> Result<(), String> {
162 run("cargo", args)
163}
164
165fn run(program: &str, args: &[&str]) -> Result<(), String> {
166 println!("\n$ {program} {}", args.join(" "));
167
168 let status = Command::new(program)
169 .args(args)
170 .current_dir(workspace_root())
171 .status()
172 .map_err(|error| format!("cannot run {program}: {error}"))?;
173
174 if status.success() {
175 Ok(())
176 } else {
177 Err(format!("{program} {} failed", args.join(" ")))
178 }
179}
180
181pub fn workspace_root() -> PathBuf {
187 let start = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
188
189 for directory in start.ancestors() {
190 if is_workspace_root(directory) {
191 return directory.to_path_buf();
192 }
193 }
194
195 start
196}
197
198fn is_workspace_root(directory: &Path) -> bool {
199 let manifest = directory.join("Cargo.toml");
200 let Ok(contents) = std::fs::read_to_string(&manifest) else {
201 return false;
202 };
203
204 contents
205 .lines()
206 .any(|line| line.trim_start().starts_with("[workspace]"))
207}