use std::fs;
use std::path::PathBuf;
use anyhow::{bail, Context, Result};
use clap::{Parser, ValueEnum};
use crate::ui;
#[derive(Debug, Clone, ValueEnum)]
pub enum InitTemplate {
Default,
Empty,
}
#[derive(Debug, Parser)]
pub struct InitArgs {
pub name: String,
#[arg(long, value_enum, default_value_t = InitTemplate::Default)]
pub template: InitTemplate,
#[arg(long)]
pub path: Option<PathBuf>,
}
pub fn run(args: InitArgs) -> Result<()> {
ui::header(
"portaki init",
"Scaffold a module crate — buildable, runnable in the sandbox, publishable.",
);
let dest = args
.path
.clone()
.unwrap_or_else(|| PathBuf::from(&args.name));
if dest.exists() {
bail!("destination already exists: {}", dest.display());
}
let template_dir = match args.template {
InitTemplate::Default => template_root().join("default-module"),
InitTemplate::Empty => template_root().join("empty-module"),
};
if !template_dir.exists() {
bail!(
"template not found: {} (run from portaki-sdk checkout)",
template_dir.display()
);
}
let scaffolding = ui::step(format!(
"scaffolding {} from the {} template",
args.name,
label(&args.template)
));
copy_template(&template_dir, &dest, &args.name)?;
scaffolding.done(format!("created {}", dest.display()));
describe(&args.template);
ui::next(&[
(
&format!("cd {}", dest.display()),
"everything below runs from the module root",
),
(
"portaki build",
"compile to wasm32 and assemble the manifest",
),
(
"portaki dev --watch",
"run it in the hosted sandbox on every save",
),
]);
ui::blank();
Ok(())
}
fn describe(template: &InitTemplate) {
let mut rows = vec![
("src/lib.rs", "the module — entity, capability, manifest"),
("src/ids.rs", "typed surface and operation ids"),
];
if matches!(template, InitTemplate::Default) {
rows.push(("src/host/", "surfaces the host dashboard renders"));
rows.push(("src/guest/", "surfaces the guest booklet renders"));
}
rows.push((
"i18n/*.json",
"one file per locale — the keys ids.rs points at",
));
rows.push(("Cargo.toml", "wired to portaki-sdk, cdylib for wasm32"));
ui::list("what you got", &rows);
}
fn label(template: &InitTemplate) -> &'static str {
match template {
InitTemplate::Default => "default",
InitTemplate::Empty => "empty",
}
}
fn template_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../templates")
.canonicalize()
.unwrap_or_else(|_| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../templates"))
}
fn copy_template(source: &PathBuf, dest: &PathBuf, module_name: &str) -> Result<()> {
fs::create_dir_all(dest).with_context(|| format!("create {}", dest.display()))?;
for entry in fs::read_dir(source)? {
let entry = entry?;
let file_name = entry.file_name();
let name = file_name.to_string_lossy();
let target = dest.join(&*name);
if entry.file_type()?.is_dir() {
copy_template(&entry.path(), &target, module_name)?;
continue;
}
let source_path = entry.path();
let mut target_name = name.to_string();
if target_name.ends_with(".template") {
target_name = target_name.trim_end_matches(".template").to_string();
}
let target = dest.join(&target_name);
let text = fs::read_to_string(&source_path)?;
let rendered = text
.replace("{{MODULE_NAME}}", module_name)
.replace("{{SDK_VERSION}}", env!("CARGO_PKG_VERSION"));
fs::write(&target, rendered).with_context(|| format!("write {}", target.display()))?;
}
Ok(())
}