#!/usr/bin/env nu # new-site.nu — scaffold a new Rustelo website from a self-contained template. # # Interactive: nu new-site.nu # Non-interactive (InitConfig-style, like `cargo rustelo init --config`): # nu new-site.nu --project acme --render-mode htmx-ssr --domain acme.dev \ # --site-title "Acme" --languages "en,es" --default-lang en --ontoref minimal --yes # # The site is created as a sibling of the rustelo repo (constellation or flat). # All {{RUSTELO_ROOT}} placeholders in template files are replaced with the # correct relative path from each file's execution context to the framework root. def fw-root [] { $env.FILE_PWD | path join ".." ".." | path expand } def ask [prompt: string, default: string] { let ans = (input $"($prompt) [($default)]: ") if ($ans | str trim | is-empty) { $default } else { ($ans | str trim) } } def snake [s: string] { $s | str replace --all "-" "_" } # Compute the relative path from directory `from` to directory `to`. # Both must be absolute expanded paths. def rel-path [from: string, to: string] { let from_parts = ($from | path split | where { |p| ($p | str length) > 0 }) let to_parts = ($to | path split | where { |p| ($p | str length) > 0 }) let min_len = ([$from_parts $to_parts] | each { length } | math min) mut common = 0 for i in 0..<$min_len { if ($from_parts | get $i) == ($to_parts | get $i) { $common = ($common + 1) } else { break } } let ups = ($from_parts | length) - $common let down = ($to_parts | skip $common) let parts = ((0..<$ups | each { ".." }) ++ $down) if ($parts | is-empty) { "." } else { $parts | str join "/" } } # Replace {{RUSTELO_ROOT}} in every text file under `root` with the correct # relative path from that file's execution-context directory to `fw`. # # Files under `workspace_root_dirs` (like justfiles/) run in the workspace # root cwd context even though they sit in a subdirectory, so they receive the # workspace-root-relative path rather than the file-directory-relative path. def apply-rustelo-root [root: string, fw: string] { let bin = [png jpg jpeg gif ico svg webp avif woff woff2 ttf otf eot wasm db "db-shm" "db-wal" pdf zip tgz gz xz lock map] let workspace_root_dirs = ["justfiles"] let files = (glob ($root | path join "**" "*") --no-dir | where { |f| ($f | path parse | get extension) not-in $bin } | where { |f| not ($f | str contains "node_modules") }) for f in $files { try { let c = (open --raw $f | decode utf-8) if not ($c | str contains "{{RUSTELO_ROOT}}") { continue } let file_dir = ($f | path dirname) let rel_parts = ($file_dir | path relative-to $root | path split | where { |p| ($p | str length) > 0 }) let effective_dir = if (($rel_parts | length) > 0 and ($rel_parts | first) in $workspace_root_dirs) { $root } else { $file_dir } let rustelo_rel = (rel-path $effective_dir $fw) ($c | str replace --all "{{RUSTELO_ROOT}}" $rustelo_rel) | save -f $f } catch { } } } def main [ --project: string = "" --render-mode: string = "" --site-title: string = "" --domain: string = "" --languages: string = "" --default-lang: string = "" --ontoref: string = "" --out: string = "" # parent dir for the new site (default: sibling of fw repo) --yes # non-interactive: take provided flags / defaults ] { let fw = (fw-root) let opts = (^nickel export ($fw | path join "templates" "options.ncl") | from json) let defs = ($opts.questions | reduce --fold {} { |q, acc| $acc | insert $q.id $q.default }) # resolve each answer: flag > prompt (interactive) > default let interactive = (not $yes) let project = (if ($project | is-not-empty) { $project } else if $interactive { ask "Project name (kebab-case)" $defs.project } else { $defs.project }) let render_mode = (if ($render_mode | is-not-empty) { $render_mode } else if $interactive { ask "Render mode (htmx-ssr|leptos-hydration)" $defs.render_mode } else { $defs.render_mode }) let site_title = (if ($site_title | is-not-empty) { $site_title } else if $interactive { ask "Site title" $defs.site_title } else { $defs.site_title }) let domain = (if ($domain | is-not-empty) { $domain } else if $interactive { ask "Domain (no scheme)" $defs.domain } else { $defs.domain }) let languages = (if ($languages | is-not-empty) { $languages } else if $interactive { ask "Languages (csv)" $defs.languages } else { $defs.languages }) let default_lang = (if ($default_lang | is-not-empty) { $default_lang } else if $interactive { ask "Default language" $defs.default_lang } else { $defs.default_lang }) let ontoref = (if ($ontoref | is-not-empty) { $ontoref } else if $interactive { ask "ontoref onboarding (full|minimal|none)" $defs.ontoref } else { $defs.ontoref }) if ($render_mode not-in ($opts.template_for | columns)) { error make { msg: $"unknown render_mode ($render_mode); expected one of ($opts.template_for | columns)" } } let tmpl_name = ($opts.template_for | get $render_mode) let tmpl = ($fw | path join "templates" $tmpl_name) if not ($tmpl | path exists) { error make { msg: $"template not found: ($tmpl)" } } # Determine parent directory for the new site. # In constellation layout (fw basename == "code"), the fw repo root is one # level up, and the site should be a sibling of that repo root. let parent = if ($out | is-not-empty) { ($out | path expand) } else if (($fw | path basename) == "code") { $fw | path dirname | path dirname } else { $fw | path dirname } let dest = ($parent | path join $project) if ($dest | path exists) { error make { msg: $"destination exists: ($dest)" } } print $"[new-site] ($project) mode=($render_mode) → ($dest)" cp -r $tmpl $dest rm -f ($dest | path join "TEMPLATE-SETUP.md") # re-added at end with status # Substitute {{RUSTELO_ROOT}} with the correct per-file relative path to fw apply-rustelo-root $dest $fw # --- rename crate identifiers website-* → -* (skip if project == website) if $project != "website" { let p = $project let ps = (snake $project) let renames = [ ["website-pages-htmx" $"($p)-pages-htmx"] ["website_pages_htmx" $"($ps)_pages_htmx"] ["website-pages" $"($p)-pages"] ["website_pages" $"($ps)_pages"] ["website-client" $"($p)-client"] ["website_client" $"($ps)_client"] ["rustelo-htmx-server" $"($p)-htmx-server"] ["rustelo_htmx_server" $"($ps)_htmx_server"] ["rustelo-leptos-server" $"($p)-leptos-server"] ["rustelo_leptos_server" $"($ps)_leptos_server"] ["website-shared" $"($p)-shared"] ["website_shared" $"($ps)_shared"] ["name = \"website\"" $"name = \"($p)\""] # leptos metadata output name ] sanitize-tree $dest $renames } # --- set deploy/identity values let domain_subs = [ ["example.com" $domain] ] sanitize-tree $dest $domain_subs # --- ontoref onboarding let onto = ($env.FILE_PWD | path join "onto-onboard.nu") nu $onto --level $ontoref --root $dest --project $project --languages $languages --profile $render_mode # --- write a fresh setup checklist with the resolved values [ $"# ($site_title) — generated site" "" $"- project: ($project) render mode: ($render_mode) languages: ($languages) (default ($default_lang))" $"- domain: ($domain) ontoref: ($ontoref)" "" "## Remaining COMPLETE items" "- site/config/site.ncl — set site name (title) and languages/default_language." "- site/config/routes.ncl — your routes; content under site/content/." "- .env (copy from .env.example) — session secret, DB, SMTP/OAuth." "- provisioning/project.ncl + lian-build/build_directives.ncl — registry, namespace, secrets (search: example.com, registry.example.com, secrets-base)." "- Logos under site/public/images/logos/ + site/config/site.ncl logo.*" "- Replace the example post in site/content/blog/{en,es}/getting-started/." "" "Build: just build-auto (auto-detects profile)" "Pack: just build::distro" "Verify: cargo check" ] | str join "\n" | save -f ($dest | path join "SETUP.md") print "" print $"[new-site] done → ($dest)" print $" next: cd ($dest); cat SETUP.md; cargo check" } # replace a list of [from to] pairs across all text files in a tree (binary-safe). def sanitize-tree [root: string, pairs: list] { let bin = [png jpg jpeg gif ico svg webp avif woff woff2 ttf otf eot wasm db "db-shm" "db-wal" pdf zip tgz gz xz lock map] let files = (glob ($root | path join "**" "*") --no-dir | where { |f| (($f | path parse | get extension) not-in $bin) and (not ($f | str contains "node_modules")) }) for f in $files { try { mut c = (open --raw $f | decode utf-8) for pair in $pairs { $c = ($c | str replace --all ($pair | get 0) ($pair | get 1)) } $c | save -f $f } catch { } } }