Skip to content

Commit 3a9ece3

Browse files
authored
Merge pull request #6 from voidreamer/claude/anvil-first-run-experience-NJpCd
Add first-run hints, init --config, and verbose flag support
2 parents 6e9eec7 + 511b84e commit 3a9ece3

5 files changed

Lines changed: 346 additions & 19 deletions

File tree

README.md

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,19 @@ cargo install anvil-env
1515

1616
Or build from source with `cargo build --release` and use `target/release/anvil`.
1717

18+
`cargo install` drops the binary in `~/.cargo/bin/anvil`. Add that directory to
19+
`PATH` so wrappers, hooks, and project scripts can call `anvil` directly
20+
instead of hard-coding the absolute path:
21+
22+
```bash
23+
# bash / zsh
24+
export PATH="$HOME/.cargo/bin:$PATH"
25+
```
26+
1827
## Quick start
1928

20-
Write a package at `~/packages/maya-2024.yaml`:
29+
Run `anvil init --config` to scaffold a starter `~/.anvil.yaml`, then write a
30+
package at `~/packages/maya-2024.yaml`:
2131

2232
```yaml
2333
name: maya
@@ -132,17 +142,30 @@ overwrite does not slip through.
132142

133143
The `commands` map lets `anvil run` pick a program from the package definition.
134144
Values expand the same way as `environment` values, and can include baked in
135-
arguments with whitespace or tilde segments:
145+
arguments with whitespace or tilde segments — anvil tokenises the value with
146+
POSIX shell rules, expands `~/` in every token, then runs the first token
147+
with the remaining tokens prepended to whatever the user passes after `--`.
136148

137149
```yaml
138150
commands:
151+
# Bare path
152+
maya: ${MAYA_LOCATION}/bin/maya
153+
154+
# Program + baked-in flags (multi-token alias)
139155
nukex: ${NUKE_HOME}/Nuke${VERSION} --nukex
156+
157+
# Launcher in front of an interpreter (e.g. Python script with a specific runtime)
140158
usdview: python3.14 ~/USD/bin/usdview
159+
160+
# Wrapper that injects defaults; user's `-- <extra args>` are appended
161+
hython-debug: ${HFS}/bin/hython -d -v
141162
```
142163
143-
Anvil tokenises the value with POSIX shell rules, expands `~/` in every token,
144-
then runs the first token with the remaining tokens prepended to whatever the
145-
user passes after `--`.
164+
`anvil run nukex -- --view` therefore exec's
165+
`${NUKE_HOME}/Nuke${VERSION} --nukex --view` — packages can ship sane defaults
166+
for every tool they expose without forcing users to memorise flag soup. Quoted
167+
substrings are preserved as a single argv element, so paths with spaces work
168+
without escaping the whole value.
146169

147170
## Commands
148171

@@ -234,12 +257,13 @@ anvil context shell render.ctx.json
234257

235258
### `anvil init`
236259

237-
Scaffold a new package definition.
260+
Scaffold a new package definition, or a starter global config.
238261

239262
```bash
240263
anvil init my-tools # my-tools/1.0.0/package.yaml
241264
anvil init my-tools --version 2.0 # my-tools/2.0/package.yaml
242265
anvil init my-tools --flat # my-tools-1.0.0.yaml
266+
anvil init --config # ~/.anvil.yaml with a commented template
243267
```
244268

245269
### `anvil completions`
@@ -363,7 +387,11 @@ filters:
363387
|---|---|
364388
| `ANVIL_CONFIG` | override config file location |
365389
| `ANVIL_PACKAGES` | additional package paths, colon separated |
366-
| `RUST_LOG` | log verbosity, e.g. `RUST_LOG=debug` |
390+
| `RUST_LOG` | log verbosity, e.g. `RUST_LOG=debug` (overrides `-v`) |
391+
392+
By default anvil only logs warnings and errors so it can be piped safely
393+
(`eval "$(anvil env maya-2024 --export)"`). Pass `-v` for info-level diagnostics
394+
or `-vv` for debug.
367395

368396
If no config file is found, anvil falls back to `$ANVIL_PACKAGES`,
369397
`$HOME/packages`, `$HOME/.local/share/anvil/packages`, and `/opt/packages`.

src/cli.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ pub struct Cli {
1212
#[arg(long, global = true)]
1313
pub refresh: bool,
1414

15+
/// Increase log verbosity: `-v` enables info, `-vv` enables debug.
16+
/// `RUST_LOG` overrides this when set.
17+
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, global = true)]
18+
pub verbose: u8,
19+
1520
#[command(subcommand)]
1621
pub command: Commands,
1722
}
@@ -107,18 +112,22 @@ pub enum Commands {
107112
action: ContextAction,
108113
},
109114

110-
/// Scaffold a new package definition
115+
/// Scaffold a new package definition (or `--config` for the global config)
111116
Init {
112-
/// Package name (e.g., my-tools)
113-
name: String,
117+
/// Package name (e.g., my-tools). Omit when using `--config`.
118+
name: Option<String>,
114119

115120
/// Package version (default: 1.0.0)
116-
#[arg(short, long, default_value = "1.0.0")]
121+
#[arg(long, default_value = "1.0.0")]
117122
version: String,
118123

119124
/// Create as a flat YAML file instead of a nested directory
120-
#[arg(long)]
125+
#[arg(long, conflicts_with = "config")]
121126
flat: bool,
127+
128+
/// Scaffold a global `~/.anvil.yaml` instead of a package
129+
#[arg(long, conflicts_with_all = ["flat", "name"])]
130+
config: bool,
122131
},
123132

124133
/// Generate shell completions

src/config.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,42 @@ impl Config {
189189
Ok(config)
190190
}
191191

192+
/// Diagnose why no packages were found and return a hint suitable for
193+
/// printing to stderr. Returns `None` when at least one package path
194+
/// exists on disk (i.e. there's no obvious config problem to surface).
195+
pub fn first_run_hint(&self) -> Option<String> {
196+
let config_path = Self::config_path();
197+
if !config_path.exists() {
198+
return Some(format!(
199+
"No anvil config found at {}.\n - Run `anvil init --config` to scaffold one\n - Or set ANVIL_PACKAGES to a colon-separated list of package directories",
200+
config_path.display()
201+
));
202+
}
203+
204+
if self.package_paths.is_empty() {
205+
return Some(format!(
206+
"{} sets no `package_paths`.\n - Add e.g. `package_paths: [~/packages]` to point anvil at your packages\n - Or set ANVIL_PACKAGES",
207+
config_path.display()
208+
));
209+
}
210+
211+
if self.all_package_paths().is_empty() {
212+
let listed = self
213+
.package_paths
214+
.iter()
215+
.map(|p| format!(" - {}", p))
216+
.collect::<Vec<_>>()
217+
.join("\n");
218+
return Some(format!(
219+
"None of the configured package_paths exist on disk:\n{}\n Create one of them, or edit {}.",
220+
listed,
221+
config_path.display()
222+
));
223+
}
224+
225+
None
226+
}
227+
192228
/// Get config file path
193229
pub fn config_path() -> PathBuf {
194230
if let Ok(path) = std::env::var("ANVIL_CONFIG") {

src/main.rs

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,28 @@ use context::{ContextPackage, Lockfile, SavedContext};
2020
use resolver::Resolver;
2121

2222
fn main() -> Result<()> {
23-
// Initialize logging
23+
let cli = Cli::parse();
24+
25+
// Default to WARN so casual `anvil env <pkg>` invocations don't litter
26+
// stderr with "Loaded N packages" and similar. `-v` / `-vv` step up
27+
// to info / debug; `RUST_LOG` still wins when set.
28+
let default_filter = match cli.verbose {
29+
0 => "anvil=warn",
30+
1 => "anvil=info",
31+
_ => "anvil=debug",
32+
};
2433
tracing_subscriber::registry()
2534
.with(
2635
tracing_subscriber::EnvFilter::try_from_default_env()
27-
.unwrap_or_else(|_| "anvil=info".into()),
36+
.unwrap_or_else(|_| default_filter.into()),
37+
)
38+
.with(
39+
tracing_subscriber::fmt::layer()
40+
.with_target(false)
41+
.with_writer(std::io::stderr),
2842
)
29-
.with(tracing_subscriber::fmt::layer().with_target(false))
3043
.init();
3144

32-
let cli = Cli::parse();
33-
3445
// Load config
3546
let config = Config::load()?;
3647
let refresh = cli.refresh;
@@ -71,8 +82,17 @@ fn main() -> Result<()> {
7182
cmd_context_shell(&config, &file, shell)?;
7283
}
7384
},
74-
Commands::Init { name, version, flat } => {
75-
cmd_init(&name, &version, flat)?;
85+
Commands::Init { name, version, flat, config: scaffold_config } => {
86+
if scaffold_config {
87+
cmd_init_config()?;
88+
} else {
89+
let name = name.ok_or_else(|| {
90+
anyhow::anyhow!(
91+
"anvil init: provide a package name, or pass --config to scaffold ~/.anvil.yaml"
92+
)
93+
})?;
94+
cmd_init(&name, &version, flat)?;
95+
}
7696
}
7797
Commands::Completions { shell } => {
7898
Cli::print_completions(shell);
@@ -240,6 +260,21 @@ fn cmd_list(config: &Config, package: Option<String>, refresh: bool) -> Result<(
240260
} else {
241261
// List all packages
242262
let packages = resolver.list_packages()?;
263+
if packages.is_empty() {
264+
if let Some(hint) = config.first_run_hint() {
265+
eprintln!("No packages found.\n {}", hint.replace('\n', "\n "));
266+
} else {
267+
eprintln!(
268+
"No packages found in any of the configured paths:\n{}",
269+
config
270+
.all_package_paths()
271+
.iter()
272+
.map(|p| format!(" - {}", p.display()))
273+
.collect::<Vec<_>>()
274+
.join("\n")
275+
);
276+
}
277+
}
243278
for pkg in packages {
244279
println!("{}", pkg);
245280
}
@@ -255,6 +290,15 @@ fn cmd_info(config: &Config, package: &str, refresh: bool) -> Result<()> {
255290

256291
println!("Name: {}", pkg.name);
257292
println!("Version: {}", pkg.version);
293+
// When the user asked for a bare name (e.g. `anvil info resolver`) and
294+
// there are several versions on disk, surface them so the asymmetry
295+
// between filename (`resolver-1.yaml`) and package name (`resolver`)
296+
// doesn't hide the others.
297+
if let Ok(versions) = resolver.list_versions(&pkg.name) {
298+
if versions.len() > 1 {
299+
println!("Available versions: {}", versions.join(", "));
300+
}
301+
}
258302
if let Some(desc) = &pkg.description {
259303
println!("Description: {}", desc);
260304
}
@@ -524,6 +568,52 @@ environment:
524568
Ok(())
525569
}
526570

571+
/// Scaffold a global `~/.anvil.yaml` so first-time users have something to
572+
/// edit instead of an empty file.
573+
fn cmd_init_config() -> Result<()> {
574+
let path = Config::config_path();
575+
if path.exists() {
576+
anyhow::bail!("{} already exists", path.display());
577+
}
578+
579+
if let Some(parent) = path.parent() {
580+
std::fs::create_dir_all(parent)
581+
.with_context(|| format!("Failed to create {}", parent.display()))?;
582+
}
583+
584+
let template = r#"# Anvil global config — see https://github.com/voidreamer/anvil
585+
586+
# Where to look for package definitions, in priority order.
587+
# Each entry can be a directory of flat `<name>-<version>.yaml` files
588+
# and/or nested `<name>/<version>/package.yaml` packages.
589+
package_paths:
590+
- ~/packages
591+
# - /studio/packages
592+
# - ${STUDIO_ROOT}/packages
593+
594+
# Optional: package set aliases (use as `anvil run <alias-name> -- ...`).
595+
# aliases:
596+
# maya-anim:
597+
# - maya-2024
598+
# - studio-tools
599+
600+
# Optional: shell that `anvil shell` uses by default.
601+
# default_shell: zsh
602+
603+
# Optional: hide / restrict packages by glob.
604+
# filters:
605+
# include: ["maya-*", "houdini-*"]
606+
# exclude: ["*-dev"]
607+
"#;
608+
609+
std::fs::write(&path, template)
610+
.with_context(|| format!("Failed to write {}", path.display()))?;
611+
println!("Created {}", path.display());
612+
println!("Edit it to point `package_paths` at your package directory, then run `anvil list`.");
613+
614+
Ok(())
615+
}
616+
527617
// ---------------------------------------------------------------------------
528618
// Wrap
529619
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)