use anyhow::Result;
use clap::{builder::ValueHint, CommandFactory, Parser, Subcommand, ValueEnum};
use clap_complete::Shell;
use std::path::PathBuf;
mod commands;
mod manifest;
mod tui;
#[derive(Parser, Debug)]
#[command(
name = "node-app",
about = "Scaffold, validate, and package Node mini-app .deb files",
long_about = None,
version,
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
New {
name: Option<String>,
#[arg(short = 't', long = "type", value_enum)]
kind: Option<AppKind>,
#[arg(short, long, value_hint = ValueHint::DirPath)]
out: Option<PathBuf>,
#[arg(long)]
git: bool,
#[arg(long, value_name = "ORG/REPO")]
github: Option<String>,
#[arg(long)]
no_deps_update: bool,
#[arg(long, value_name = "ORG/REPO-OR-PATH")]
templates: Option<String>,
},
Validate {
#[arg(short, long, default_value = ".", value_hint = ValueHint::DirPath)]
path: PathBuf,
},
Package {
#[arg(short, long, default_value = ".", value_hint = ValueHint::DirPath)]
path: PathBuf,
#[arg(short, long, value_enum, default_value_t = DebTarget::All)]
target: DebTarget,
#[arg(long)]
version: Option<String>,
#[arg(short, long, value_hint = ValueHint::DirPath)]
out: Option<PathBuf>,
},
Agentskills {
#[command(subcommand)]
action: SkillsAction,
},
Dev {
#[arg(short, long, default_value = ".", value_hint = ValueHint::DirPath)]
path: PathBuf,
#[arg(long, value_parser = ["deb", "docker", "clone", "monorepo"])]
daemon: Option<String>,
#[arg(long, value_hint = ValueHint::DirPath)]
monorepo_path: Option<PathBuf>,
#[arg(long, value_hint = ValueHint::FilePath)]
socket: Option<PathBuf>,
#[arg(long, value_hint = ValueHint::DirPath)]
dev_dir: Option<PathBuf>,
#[arg(long)]
once: bool,
#[arg(long, value_name = "PATH", action = clap::ArgAction::Append, value_hint = ValueHint::DirPath)]
dep: Vec<PathBuf>,
#[arg(long)]
no_tui: bool,
#[arg(long = "config", value_name = "KEY=VALUE", action = clap::ArgAction::Append)]
config_overrides: Vec<String>,
#[arg(long, value_parser = ["alice", "bob"], value_delimiter = ',', action = clap::ArgAction::Append)]
instances: Vec<String>,
},
#[command(hide = true)]
Completions {
#[command(subcommand)]
action: CompletionsAction,
},
}
#[derive(Subcommand, Debug)]
enum SkillsAction {
List {
#[arg(long, default_value = "econ-v1/agentskills")]
repo: String,
},
Install {
names: Vec<String>,
#[arg(long)]
local: bool,
#[arg(long)]
update: bool,
#[arg(long, default_value = "econ-v1/agentskills")]
repo: String,
},
}
#[derive(Subcommand, Debug)]
enum CompletionsAction {
Generate {
shell: Shell,
},
Install {
#[arg(long)]
shell: Option<Shell>,
},
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
pub enum AppKind {
Bun,
#[value(name = "bun-fullstack")]
BunFullstack,
Cdylib,
#[value(name = "cdylib-fullstack")]
CdylibFullstack,
#[value(name = "standalone-rust")]
StandaloneRust,
#[value(name = "standalone-bun")]
StandaloneBun,
}
impl AppKind {
pub fn label(self) -> &'static str {
match self {
AppKind::Bun => "bun",
AppKind::BunFullstack => "bun-fullstack",
AppKind::Cdylib => "cdylib",
AppKind::CdylibFullstack => "cdylib-fullstack",
AppKind::StandaloneRust => "standalone-rust",
AppKind::StandaloneBun => "standalone-bun",
}
}
pub fn template_dir_name(self) -> &'static str {
self.label()
}
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum DebTarget {
Amd64,
Arm64,
All,
}
impl DebTarget {
pub fn as_str(self) -> &'static str {
match self {
DebTarget::Amd64 => "amd64",
DebTarget::Arm64 => "arm64",
DebTarget::All => "all",
}
}
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Agentskills { action } => match action {
SkillsAction::List { repo } => commands::skills::list(&repo),
SkillsAction::Install {
names,
local,
update,
repo,
} => commands::skills::install(commands::skills::SkillsInstallArgs {
names: &names,
global: !local,
update,
repo: &repo,
}),
},
Command::New {
name,
kind,
out,
git,
github,
no_deps_update,
templates,
} => {
let templates_repo = templates
.or_else(|| std::env::var("NODE_APP_TEMPLATES_REPO").ok())
.unwrap_or_else(|| commands::new::DEFAULT_TEMPLATES_REPO.to_string());
commands::new::run(
name,
kind,
out,
git,
github,
no_deps_update,
templates_repo,
)
}
Command::Validate { path } => commands::validate::run(&path).map(|_| ()),
Command::Package {
path,
target,
version,
out,
} => commands::package::run(&path, target.as_str(), version.as_deref(), out.as_deref()),
Command::Dev {
path,
daemon,
monorepo_path,
socket,
dev_dir,
once,
dep,
no_tui,
config_overrides,
instances,
} => {
let config: Vec<(String, String)> = config_overrides
.into_iter()
.filter_map(|s| {
let (k, v) = s.split_once('=')?;
Some((k.to_string(), v.to_string()))
})
.collect();
commands::dev::run(commands::dev::DevArgs {
project_path: &path,
daemon: daemon.as_deref(),
monorepo_path: monorepo_path.as_deref(),
socket_override: socket.as_deref(),
dev_dir_override: dev_dir.as_deref(),
once,
dep_paths: dep,
no_tui,
config,
instances,
})
}
Command::Completions { action } => match action {
CompletionsAction::Generate { shell } => {
commands::completions::generate_to_stdout(shell, &mut Cli::command());
Ok(())
}
CompletionsAction::Install { shell } => {
commands::completions::install(shell, &mut Cli::command())
}
},
}
}