feat(cli): aura nodes new — node-crate scaffold + attach (#241 T5-T6)
- New clap family `aura nodes new <name> [--engine-path] [--namespace]`: scaffolds a node crate as a SIBLING of the project root (Cargo.toml with the aura-core path-dep, the parameterized Scale starter node, roster macro, crate-scoped .gitignore + CLAUDE.md, own git repo) and appends the `[nodes] crates = ["../<name>"]` pointer to the project's Aura.toml. Refuses outside a project, on any existing [nodes] section (incl. an empty crates array), and on an existing destination; joins the load-phase bypass beside `aura new` (a scaffolder must work in an unbuilt tree). - The old single-tier scaffold() is gone; the crate templates live on in scaffold_node_crate; dead-code bridges removed with their callers. - tests/project_nodes.rs: attach + refusals + namespace override + the full role-2 loop (scaffold, cargo build, vocabulary resolves <ns>::Scale end to end). - tests/project_sweep_campaign.rs fixture reworked to the two-tier shape (aura new + aura nodes new --namespace knob_lab); the two campaign acceptance tests are byte-unchanged and green. refs #241
This commit is contained in:
@@ -2455,6 +2455,8 @@ enum Command {
|
||||
Reproduce(ReproduceCmd),
|
||||
/// Scaffold a new research project crate.
|
||||
New(NewCmd),
|
||||
/// Scaffold and attach node crates (native-node development).
|
||||
Nodes(NodesCmd),
|
||||
/// Validate, introspect, and register process documents (methodology).
|
||||
Process(research_docs::ProcessCmd),
|
||||
/// Validate, introspect, and register campaign documents (experiment intent).
|
||||
@@ -2479,6 +2481,30 @@ struct NewCmd {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct NodesCmd {
|
||||
#[command(subcommand)]
|
||||
command: NodesCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum NodesCommand {
|
||||
/// Scaffold a node crate beside this project and attach it via [nodes].
|
||||
New(NodesNewCmd),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct NodesNewCmd {
|
||||
/// Crate name; also the default namespace (dashes become underscores).
|
||||
name: String,
|
||||
/// Engine checkout for the crate's aura-core path-dep.
|
||||
#[arg(long)]
|
||||
engine_path: Option<std::path::PathBuf>,
|
||||
/// Vocabulary namespace override.
|
||||
#[arg(long)]
|
||||
namespace: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[command(args_conflicts_with_subcommands = true)]
|
||||
struct GraphCmd {
|
||||
@@ -3332,6 +3358,70 @@ fn dispatch_new(a: NewCmd, _env: &project::Env) {
|
||||
);
|
||||
}
|
||||
|
||||
fn dispatch_nodes(cmd: NodesCmd) {
|
||||
match cmd.command {
|
||||
NodesCommand::New(a) => dispatch_nodes_new(a),
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_nodes_new(a: NodesNewCmd) {
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|e| {
|
||||
eprintln!("aura: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
let Some(root) = project::discover_from(&cwd) else {
|
||||
eprintln!(
|
||||
"aura: `aura nodes new` needs a project (no Aura.toml found up from {})",
|
||||
cwd.display()
|
||||
);
|
||||
std::process::exit(1);
|
||||
};
|
||||
let toml = match project::read_aura_toml(&root) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
eprintln!("aura: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
// Mirror `append_nodes_pointer`'s textual `[nodes]`-section check (not just
|
||||
// `crates.is_empty()`) so a `[nodes]` section with an empty crates array
|
||||
// is caught here too, before scaffold_node_crate writes a crate that
|
||||
// append_nodes_pointer would then refuse to attach.
|
||||
let already_attached = !toml.nodes.crates.is_empty()
|
||||
|| std::fs::read_to_string(root.join("Aura.toml")).is_ok_and(|t| t.contains("[nodes]"));
|
||||
if already_attached {
|
||||
eprintln!("aura: a node crate is already attached (multi-crate loading is not yet supported)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let parent = root.parent().unwrap_or(&root).to_path_buf();
|
||||
let spec = match scaffold::scaffold_spec(
|
||||
&a.name,
|
||||
a.engine_path.as_deref(),
|
||||
a.namespace.as_deref(),
|
||||
&parent,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(m) => {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
};
|
||||
if let Err(m) = scaffold::scaffold_node_crate(&spec) {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if let Err(m) = scaffold::append_nodes_pointer(&root, &format!("../{}", a.name)) {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!(
|
||||
"created node crate \"../{}\" (namespace \"{}\") and attached it to {}",
|
||||
a.name,
|
||||
spec.namespace,
|
||||
root.file_name().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
|
||||
/// `aura sweep`: loaded-blueprint by-name axis sweep (or `--list-axes` probe) when the
|
||||
/// first-positional is an existing `.json`, else the built-in `--strategy` grid sweep.
|
||||
fn dispatch_sweep(a: SweepCmd, env: &project::Env) {
|
||||
@@ -3701,9 +3791,9 @@ fn main() {
|
||||
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||
}
|
||||
let cli = Cli::parse();
|
||||
// `aura new` scaffolds; it must not require a loadable project even
|
||||
// when invoked inside one (e.g. an unbuilt tree).
|
||||
let env = if matches!(cli.command, Command::New(_)) {
|
||||
// `aura new`/`aura nodes new` scaffold; they must not require a
|
||||
// loadable project even when invoked inside one (e.g. an unbuilt tree).
|
||||
let env = if matches!(cli.command, Command::New(_) | Command::Nodes(_)) {
|
||||
project::Env::std()
|
||||
} else {
|
||||
match std::env::current_dir()
|
||||
@@ -3731,6 +3821,7 @@ fn main() {
|
||||
Command::Runs(a) => dispatch_runs(a, &env),
|
||||
Command::Reproduce(a) => dispatch_reproduce(a, &env),
|
||||
Command::New(a) => dispatch_new(a, &env),
|
||||
Command::Nodes(a) => dispatch_nodes(a),
|
||||
Command::Process(a) => research_docs::process_cmd(a, &env),
|
||||
Command::Campaign(a) => research_docs::campaign_cmd(a, &env),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user