# Set shell for commands set shell := ["bash", "-c"] project_root := justfile_directory() # Load environment variables from .env file if it exists set dotenv-load := true [doc("Show available recipes")] default: @just --list # ============================================================================= # LAMINA LAYER BUILDS # ============================================================================= lamina_root := justfile_directory() + "/../../lamina" libre_forge_root := justfile_directory() + "/../../workspaces/libre-forge" lian_build_root := justfile_directory() + "/../../lian-build/code" secrets_base := justfile_directory() + "/../../workspaces/libre-daoshi" rustelo_root := justfile_directory() + "/../../rustelo/code" strops_root := justfile_directory() + "/../../stratumiops" # reg.librecloud.online's push credential is SOPS-encrypted for libre-wuji's # age identity — a different workspace/key than secrets_base (libre-daoshi) # above. Override with LIAN_BUILD_WUJI_SECRETS_BASE if wuji lives somewhere # else; defaults relative to this justfile like every other _root var here. wuji_secrets_base := env_var_or_default("LIAN_BUILD_WUJI_SECRETS_BASE", justfile_directory() + "/../../workspaces/libre-wuji") wuji_kage := wuji_secrets_base + "/infra/libre-wuji/.kage" # Build and push: no arg or "website" → build this project via fleet; layer name → delegate to lamina # Pass log=/path/file.log to capture output to a file instead of printing to screen. build-push-remote layer="website" log="": #!/usr/bin/env nu if "{{ layer }}" != "website" { just -f "{{ lamina_root }}/justfile" build-push-remote {{ layer }} return } let forge = "{{ libre_forge_root }}" let kage = ($forge | path join "infra/control-plane/.kage") let sc_file = ($forge | path join "infra/control-plane/secrets/sccache.sops.yaml") let nk_file = ($forge | path join "infra/control-plane/secrets/nkeys.sops.yaml") let handler = ($forge | path join "scripts/provision-relay.nu") for p in [$kage, $sc_file, $nk_file] { if not ($p | path exists) { error make { msg: $"missing: ($p)" } } } let sc_raw = (with-env { SOPS_AGE_KEY_FILE: $kage } { do { ^sops --decrypt --output-type json $sc_file } | complete }) if $sc_raw.exit_code != 0 { error make { msg: $"sops sccache: ($sc_raw.stderr | str trim)" } } let sc = ($sc_raw.stdout | from json) let nk_raw = (with-env { SOPS_AGE_KEY_FILE: $kage } { do { ^sops --decrypt --extract '["forge_control_seed"]' $nk_file } | complete }) if $nk_raw.exit_code != 0 { error make { msg: $"sops nkeys: ($nk_raw.stderr | str trim)" } } let nk_seed = ($nk_raw.stdout | str trim) let image_tag = (open "{{ justfile_directory() }}/Cargo.toml" | get workspace.package.version) let log_file = if ("{{ log }}" != "") { "{{ log }}" } else { null } if $log_file != null { print $" lian-build log: ($log_file)" } # Render profile is derived, never declared (ADR-006): build_directives.ncl # exports one directive PER profile, keyed by profile name. Mirror # provisioning/build.nu's resolve-profile so the nickel export below (and # the --directives handed to lian-build) see a flat BuildDirectives, not # the profile-keyed dict. let detector = "{{ rustelo_root }}/scripts/check-wasm-needed.nu" let profile = if ($detector | path exists) { let res = (do { cd "{{ justfile_directory() }}" ^nu $detector --routes-dir site/config --workspace-config site/config/rendering.ncl } | complete) if $res.exit_code == 0 { "leptos-hydration" } else if $res.exit_code == 1 { "htmx-ssr" } else { error make { msg: $"check-wasm-needed.nu failed: ($res.stderr | str trim)" } } } else { let p = (open "{{ justfile_directory() }}/site/config/rendering.ncl" | parse --regex 'default_profile = "(?P

[^"]+)"' | get p?.0? | default "leptos-hydration") if $p == "htmx-ssr" { "htmx-ssr" } else { "leptos-hydration" } } print $" render profile: ($profile)" let ctx = (^mktemp -d | str trim) print $" assembling context: ($ctx)" ^rsync -a --delete "--exclude=.git/" "--exclude=target/" "--exclude=node_modules/" "--exclude=.coder/" "{{ justfile_directory() }}/" $"($ctx)/" ^rsync -a --delete "--exclude=.git/" "--exclude=target/" "{{ rustelo_root }}/" $"($ctx)/rustelo/code/" if ("{{ strops_root }}" | path exists) { ^rsync -a --delete "--exclude=.git/" "--exclude=target/" "{{ strops_root }}/" $"($ctx)/stratumiops/" } # Write version-patched project.ncl into lian_build_root so Nickel finds it by logical name. # build_directives.ncl does `import "jpl-project.ncl"` — resolved via LIAN_BUILD_NICKEL_IMPORT_PATH # which already points to lian_build_root. No path changes needed; file is deleted on exit. let proj_override = $"{{ lian_build_root }}/jpl-project.ncl" open "{{ justfile_directory() }}/provisioning/project.ncl" | str replace 'image_tag = "latest"' $'image_tag = "($image_tag)"' | str replace 'SECRET_BASE_PLACEHOLDER' "{{ wuji_secrets_base }}:{{ secrets_base }}" | str replace 'KEY_FILE_PLACEHOLDER' "{{ wuji_kage }}:{{ secrets_base }}/infra/libre-daoshi/.kage" | save -f $proj_override # Wrapper selects the single profile's directive — same trick as build.nu — # so both the local nickel export and lian-build itself see a flat object. let directives_src = "{{ justfile_directory() }}/lian-build/build_directives.ncl" let directives_wrapper = $"{{ lian_build_root }}/jpl-directives-selected.ncl" ('(import "' + $directives_src + '")."' + $profile + '"') | save -f $directives_wrapper let dir_raw = (do { ^nickel export --import-path "{{ lian_build_root }}" $directives_wrapper } | complete) if $dir_raw.exit_code != 0 { rm -f $proj_override $directives_wrapper; error make { msg: $"nickel export directives: ($dir_raw.stderr | str trim)" } } let fleet_nats_url = ($dir_raw.stdout | from json | get adapter.nats_url) # nats CLI requires the NKey seed as a file (--nkey), not as $NATS_SEED string. let nkey_file = (^mktemp | str trim) $nk_seed | save -f $nkey_file let relay_job = (job spawn --description "provision-relay" { ^nats --server $fleet_nats_url --nkey $nkey_file reply "fleet.libre-forge.ops.provision.>" --command $"nu ($handler)" }) print $" provision-relay: job ($relay_job)" print $" daemon logs: ssh libre-daoshi-0 'k0s kubectl -n fleet-system logs -l app=fleet-daemon -f'" let daoshi_kage = ("{{ secrets_base }}" | path join "infra/libre-daoshi/.kage") try { with-env { AWS_ACCESS_KEY_ID: $sc.access_key_id, AWS_SECRET_ACCESS_KEY: $sc.secret_access_key, NATS_NKEY_SEED: $nk_seed, LIAN_BUILD_NICKEL_IMPORT_PATH: "{{ lian_build_root }}", SOPS_AGE_KEY_FILE: $daoshi_kage, } { if $log_file != null { ^lian-build build --directives $directives_wrapper --context $ctx --secrets-base "{{ secrets_base }}" o+e> $log_file } else { ^lian-build build --directives $directives_wrapper --context $ctx --secrets-base "{{ secrets_base }}" } } } catch { |e| job kill $relay_job rm -f $nkey_file $proj_override $directives_wrapper rm -rf $ctx let hint = if $log_file != null { $" — see ($log_file)" } else { "" } error make { msg: $"lian-build failed($hint)\n($e.msg)" } } job kill $relay_job rm -f $nkey_file $proj_override $directives_wrapper rm -rf $ctx # Build server binary only: WASM is compiled locally then sent as part of the context. # wasm= override the WASM artifacts dir (default: target/site/). # If absent and no override is given, builds WASM locally first. # log= capture lian-build output to a file instead of printing to screen. build-push-server wasm="" log="": #!/usr/bin/env nu let forge = "{{ libre_forge_root }}" let kage = ($forge | path join "infra/control-plane/.kage") let sc_file = ($forge | path join "infra/control-plane/secrets/sccache.sops.yaml") let nk_file = ($forge | path join "infra/control-plane/secrets/nkeys.sops.yaml") let handler = ($forge | path join "scripts/provision-relay.nu") for p in [$kage, $sc_file, $nk_file] { if not ($p | path exists) { error make { msg: $"missing: ($p)" } } } let sc_raw = (with-env { SOPS_AGE_KEY_FILE: $kage } { do { ^sops --decrypt --output-type json $sc_file } | complete }) if $sc_raw.exit_code != 0 { error make { msg: $"sops sccache: ($sc_raw.stderr | str trim)" } } let sc = ($sc_raw.stdout | from json) let nk_raw = (with-env { SOPS_AGE_KEY_FILE: $kage } { do { ^sops --decrypt --extract '["forge_control_seed"]' $nk_file } | complete }) if $nk_raw.exit_code != 0 { error make { msg: $"sops nkeys: ($nk_raw.stderr | str trim)" } } let nk_seed = ($nk_raw.stdout | str trim) let image_tag = (open "{{ justfile_directory() }}/Cargo.toml" | get workspace.package.version) let wasm_site = if ("{{ wasm }}" != "") { "{{ wasm }}" } else { let target_dir = ( $env.CARGO_TARGET_DIR? | default ( let cfg = "{{ justfile_directory() }}/.cargo/config.toml" if ($cfg | path exists) { (open $cfg).build?."target-dir"? | default "{{ justfile_directory() }}/target" } else { "{{ justfile_directory() }}/target" } ) ) $target_dir | path join "site" } if not ($wasm_site | path exists) { print " building WASM frontend locally (no artifacts at ($wasm_site))..." with-env { SITE_CONFIG_PATH: "{{ justfile_directory() }}/site/config/index.ncl", NICKEL_IMPORT_PATH: $"{{ rustelo_root }}/resources/nickel:{{ justfile_directory() }}/site/config", } { ^cargo leptos build --frontend-only --release } } let log_file = if ("{{ log }}" != "") { "{{ log }}" } else { null } if $log_file != null { print $" lian-build log: ($log_file)" } let ctx = (^mktemp -d | str trim) print $" assembling context: ($ctx)" ^rsync -a --delete "--exclude=.git/" "--exclude=target/" "--exclude=node_modules/" "--exclude=.coder/" "{{ justfile_directory() }}/" $"($ctx)/" ^rsync -a --delete "--exclude=.git/" "--exclude=target/" "{{ rustelo_root }}/" $"($ctx)/rustelo/code/" if ("{{ strops_root }}" | path exists) { ^rsync -a --delete "--exclude=.git/" "--exclude=target/" "{{ strops_root }}/" $"($ctx)/stratumiops/" } # Inject WASM artifacts into context as wasm-site/ ^rsync -a --delete $"($wasm_site)/" $"($ctx)/wasm-site/" print $" wasm-site: ($wasm_site)" # Write version-patched project.ncl into lian_build_root — same approach as build-push-remote. let proj_override = $"{{ lian_build_root }}/jpl-project.ncl" open "{{ justfile_directory() }}/provisioning/project.ncl" | str replace 'image_tag = "latest"' $'image_tag = "($image_tag)"' | str replace 'SECRET_BASE_PLACEHOLDER' "{{ wuji_secrets_base }}:{{ secrets_base }}" | str replace 'KEY_FILE_PLACEHOLDER' "{{ wuji_kage }}:{{ secrets_base }}/infra/libre-daoshi/.kage" | save -f $proj_override let dir_raw = (do { ^nickel export --import-path "{{ lian_build_root }}" "{{ justfile_directory() }}/lian-build/build_directives_server.ncl" } | complete) if $dir_raw.exit_code != 0 { rm -f $proj_override; error make { msg: $"nickel export directives: ($dir_raw.stderr | str trim)" } } let fleet_nats_url = ($dir_raw.stdout | from json | get adapter.nats_url) let nkey_file = (^mktemp | str trim) $nk_seed | save -f $nkey_file let relay_job = (job spawn --description "provision-relay" { ^nats --server $fleet_nats_url --nkey $nkey_file reply "fleet.libre-forge.ops.provision.>" --command $"nu ($handler)" }) print $" provision-relay: job ($relay_job)" print $" daemon logs: ssh libre-daoshi-0 'k0s kubectl -n fleet-system logs -l app=fleet-daemon -f'" let daoshi_kage = ("{{ secrets_base }}" | path join "infra/libre-daoshi/.kage") try { with-env { AWS_ACCESS_KEY_ID: $sc.access_key_id, AWS_SECRET_ACCESS_KEY: $sc.secret_access_key, NATS_NKEY_SEED: $nk_seed, LIAN_BUILD_NICKEL_IMPORT_PATH: "{{ lian_build_root }}", SOPS_AGE_KEY_FILE: $daoshi_kage, } { if $log_file != null { ^lian-build build --directives "{{ justfile_directory() }}/lian-build/build_directives_server.ncl" --context $ctx --secrets-base "{{ secrets_base }}" o+e> $log_file } else { ^lian-build build --directives "{{ justfile_directory() }}/lian-build/build_directives_server.ncl" --context $ctx --secrets-base "{{ secrets_base }}" } } } catch { |e| job kill $relay_job rm -f $nkey_file $proj_override rm -rf $ctx let hint = if $log_file != null { $" — see ($log_file)" } else { "" } error make { msg: $"lian-build failed($hint)\n($e.msg)" } } job kill $relay_job rm -f $nkey_file $proj_override rm -rf $ctx libre_wuji_root := justfile_directory() + "/../workspaces/secrets-base" # Register this project into a workspace repo, injecting the current Cargo.toml version as image_tag. # workspace= override the target workspace root (default: secrets-base sibling). [doc("Write versioned appserv/builds stubs into the workspace repo")] register workspace="" workspace_name="secrets-base": #!/usr/bin/env nu let image_tag = (open "{{ justfile_directory() }}/Cargo.toml" | get workspace.package.version) let ws = if ("{{ workspace }}" | is-not-empty) { "{{ workspace }}" } else { "{{ libre_wuji_root }}" } if not ($ws | path exists) { error make { msg: $"workspace not found: ($ws)" } } print $"[register] image_tag: ($image_tag), workspace: ($ws)" ^nu "{{ justfile_directory() }}/provisioning/register.nu" --workspace $ws --workspace-name "{{ workspace_name }}" --image-tag $image_tag # ============================================================================= # MODULE IMPORTS # ============================================================================= mod dev "justfiles/dev.just" mod build "justfiles/build.just" mod database "justfiles/database.just" mod docs "justfiles/docs.just" mod test "justfiles/test.just" mod utils "justfiles/utils.just" mod tools "justfiles/tools.just" mod helptext "justfiles/helptext.just" mod cache "justfiles/cache.just" mod ui "justfiles/ui.just" mod content "justfiles/content.just" # ============================================================================= # ALIASES # ============================================================================= alias a := list-alias alias b := build-dev alias bp := build-prod alias s := dev-full alias sp := serve-prod alias sc := serve-cms alias cb := content-build alias oa := dev::organize-artifacts alias sbl := server-browser-logs alias bl := browser-logs alias t := test-run alias d := start alias df := dev-fast alias dc := dev-coordinated alias dm := dev-minimal alias ba := build-artifacts alias bs := build-status alias ca := clean-build-artifacts alias cs := cache::cs alias cc := cache::cache-clean alias cca := cache::cache-clean # alias dev := start # Conflicts with module name # alias h := help # Defined as recipe below alias ha := help-all alias o := overview alias c := cross-build alias pt := page-tester alias pr := pages-report alias pn := page-new alias sh := show # Tools aliases alias ta := build-tools-analyze alias tm := build-tools-manage alias tg := build-tools-generate-page alias ts := build-tools-status # Navigation Testing aliases alias nts := dev::with-nav-test # Nav Test Start alias nt := dev::nav-test-route # Nav Test route alias ns := dev::nav-test-sequence # Nav test Sequence alias nv := dev::nav-test-validate # Nav Validate alias nb := dev::nav-test-bench # Nav Bench alias na := dev::nav-test-all # Nav All tests alias nc := dev::nav-quick-check # Nav Check (CI/CD) alias nd := dev::nav-dashboard # Nav Dashboard alias nh := dev::nav-help # Nav Help # Quick nav test aliases for common routes alias nt-home := nav-test-home # Test home route alias nt-services := nav-test-services # Test services route alias nt-contact := nav-test-contact # Test contact route alias nt-blog := nav-test-blog # Test blog route alias nt-es := nav-test-es # Test Spanish home alias nt-contactar := nav-test-contactar # Test Spanish contact # UI Management aliases alias routes := ui::routes-list alias pages := ui::pages-list alias components := ui::components-list # ============================================================================= # CORE COMMANDS (Backward Compatibility & Common Tasks) # ============================================================================= list-alias: @echo "🔗 List of aliases:" @cat {{justfile()}} | grep "^alias" | sed 's/^alias / /g' # Show comprehensive system overview overview: @just utils::overview # Customization-seam verifier: brand / the site's own public origin / site identity # must not be hardcoded in the ACTIVE htmx-ssr shared Rust (crates/server/src + # crates/pages_htmx/src). Mirrors .ontoref/reflection/modes/validate-seam.ncl and is # the transition condition of the consumer-neutrality FSM dimension. HARD GATE: # exits 1 on any FAIL (currently green — so it only bites future regressions). # Scope excludes crates/pages (Leptos hydration pages, optional dep absent from the # htmx-ssr binary; their outbound project links are content, not baked identity). validate-seam: #!/usr/bin/env bash set -uo pipefail scope="crates/server/src crates/pages_htmx/src" fails=0 echo "── check-no-brand-hex ──" m=$(rg -n -i '#(e8a838|c0ccd8|0f1319|c88a20|f0c265)' $scope 2>/dev/null) if [ -n "$m" ]; then echo "FAIL brand hex in shared code:"; echo "$m"; fails=$((fails+1)); else echo "PASS"; fi echo "── check-no-hardcoded-origin ──" m=$(rg -n 'https?://[a-z0-9.-]+\.(dev|com|pro|org)' $scope -g '*.rs' 2>/dev/null | rg -v '//!|///' | rg -vi 'example\.|w3\.org|schema\.org|localhost|nickel-lang|github\.com|yourusername') if [ -n "$m" ]; then echo "FAIL hardcoded production origin in shared code:"; echo "$m"; fails=$((fails+1)); else echo "PASS"; fi echo "── check-no-site-identity ──" m=$(rg -n -i 'jesusperez|jpl-website' $scope -g '*.rs' 2>/dev/null | rg -v '//!|///') if [ -n "$m" ]; then echo "FAIL site identity literal in shared code:"; echo "$m"; fails=$((fails+1)); else echo "PASS"; fi if [ "$fails" -gt 0 ]; then echo "── seam: $fails check(s) FAILING — move the leak to its layer (brand→CSS, origin→env, identity→site/config) ──"; exit 1; fi echo "── seam: all checks pass (active htmx-ssr surface respects the seam) ──" # Route superset verifier (ADR-001, single-image deploy): the baked route table # must declare a ContentIndex + PostViewer for every standard content kind, so a # consumer's menu/About links resolve against the shared binary instead of 404-ing. # Mirrors .ontoref/reflection/modes/validate-route-superset.ncl. Hard gate. validate-route-superset: #!/usr/bin/env bash set -uo pipefail routes=site/config/routes.ncl fails=0 for kind in blog projects adr catalog; do idx=$(rg -c "make_content_route \"ContentIndex\" \"$kind\"" "$routes" 2>/dev/null || true) pv=$(rg -c "make_content_route \"PostViewer\" \"$kind\"" "$routes" 2>/dev/null || true) if [ "${idx:-0}" -ge 1 ] && [ "${pv:-0}" -ge 1 ]; then echo "PASS $kind (ContentIndex + PostViewer)" else miss=""; [ "${idx:-0}" -lt 1 ] && miss="$miss ContentIndex"; [ "${pv:-0}" -lt 1 ] && miss="$miss PostViewer" echo "FAIL $kind — missing:$miss"; fails=$((fails+1)) fi done if [ "$fails" -gt 0 ]; then echo "── route-superset: $fails kind(s) incomplete — add to site/config/{routes,content}.ncl + rebuild ──"; exit 1; fi echo "── route-superset: all standard kinds present (baked superset complete) ──" # Neutral-binary verifier (ADR-001): per-site identity must come from the running # consumer's config at runtime, never baked/hardcoded into the shared binary. The # logo is read from the consumer's site.ncl [logo]; the home hero must use that # (the site_logo global), not a hardcoded path. Origin/name stay runtime env. # Mirrors .ontoref/reflection/modes/validate-neutral-binary.ncl. Hard gate. validate-neutral-binary: #!/usr/bin/env bash set -uo pipefail fails=0 have() { if rg -q "$1" "$2" 2>/dev/null; then echo "PASS $3"; else echo "FAIL $3 — expected /$1/ in ${2##*/}"; fails=$((fails+1)); fi; } absent() { m=$(rg -n "$1" "$2" 2>/dev/null); if [ -z "$m" ]; then echo "PASS $3"; else echo "FAIL $3 — hardcoded in ${2##*/}:"; echo "$m"; fails=$((fails+1)); fi; } have 'logo_from_site_config|site\.ncl' crates/server/src/theme.rs "logo read from consumer site.ncl [logo] (config-driven)" absent 'src="/images/' crates/pages_htmx/templates/pages/home.j2 "home template has no hardcoded image (logo via site_logo global, others via config/texts)" have '"SITE_BASE_URL"' crates/server/src/shell/seo.rs "public origin from SITE_BASE_URL env" have '"SITE_NAME"' crates/server/src/shell/seo.rs "site name from SITE_NAME env" if [ "$fails" -gt 0 ]; then echo "── neutral-binary: $fails check(s) FAILING — move per-site identity to config (site.ncl) / the site_logo global ──"; exit 1; fi echo "── neutral-binary: logo config-driven, no template hardcode, origin/name from env (binary site-neutral) ──" # Config-drift verifier (ADR-001): a consumer's route paths must be a SUBSET of the # baked route superset, or its menu/links 404 against the shared binary. Pass the # consumer's site/config dir; defaults to this repo's own (self-check = zero drift). # Mirrors .ontoref/reflection/modes/validate-config-drift.ncl. Hard gate. validate-config-drift consumer_config="site/config": #!/usr/bin/env bash set -uo pipefail NICKEL="{{ rustelo_root }}/resources/nickel" paths() { nickel export --import-path "$NICKEL" --import-path "$1" "$1/routes.ncl" 2>/dev/null | jq -r '[.routes[].paths.en, .routes[].paths.es] | map(select(.)) | .[]' 2>/dev/null | sort -u; } sup=$(paths site/config) cons=$(paths "{{ consumer_config }}") if [ -z "$sup" ]; then echo "FAIL could not export the superset (site/config/routes.ncl)"; exit 1; fi if [ -z "$cons" ]; then echo "FAIL could not export consumer routes ({{ consumer_config }}/routes.ncl)"; exit 1; fi drift=$(comm -23 <(echo "$cons") <(echo "$sup")) if [ -n "$drift" ]; then echo "FAIL — consumer route paths NOT in the baked superset (these 404):"; echo "$drift" | sed 's/^/ /'; echo "── add them to site/config/routes.ncl + rebuild, or remove from the consumer ──"; exit 1; fi echo "── config-drift: consumer routes ⊆ superset ($(echo "$cons" | grep -c . ) paths, 0 drift) ──" # Framework↔consumer contract gate (ADR-001): all single-image verifiers at once. # config-drift defaults to this repo's own config (self-check); pass a consumer dir # to check a real consumer. Wire into CI to enforce the seam + superset + neutrality. validate-contract consumer_config="site/config": @just validate-seam @just validate-route-superset @just validate-neutral-binary @just validate-config-drift "{{ consumer_config }}" @echo "✓ framework↔consumer contract: seam · superset · neutral-binary · config-drift all green" # Show information about specified topic (try: info, status, config, overview, aliases, modules, help) show *TOPIC: #!/usr/bin/env bash TOPIC="{{TOPIC}}" if [ -z "$TOPIC" ]; then echo "📋 Available topics to show:" echo " info - Project information" echo " status - Application health status" echo " config - Configuration settings" echo " overview - System overview" echo " aliases - List of command aliases" echo " modules - Available modules" echo " build-tools - Build tools system" echo " help - Help system" echo "" echo "Usage: just show or just sh " exit 0 fi case "$TOPIC" in "info"|"information") just utils::info ;; "status"|"health") just utils::health ;; "config"|"configuration") just utils::config ;; "overview"|"system") just utils::overview ;; "aliases"|"alias") just list-alias ;; "modules"|"module") just helptext::modules ;; "build-tools"|"tools"|"tool") just helptext::tools ;; "help") just help ;; *) echo "📋 Available topics to show:" echo " info - Project information" echo " status - Application health status" echo " config - Configuration settings" echo " overview - System overview" echo " aliases - List of command aliases" echo " modules - Available modules" echo " build-tools - Build tools system" echo " help - Help system" echo "" echo "Usage: just show or just sh " ;; esac # ============================================================================= # PRIMARY WORKFLOW COMMANDS (Unified Interface) # ============================================================================= # Start development server (main entry point) start: @just dev::serve # Backward compatibility: redirect 'dev' to 'start' dev-start: @echo "💡 Note: 'just dev' has been renamed to 'just start'" @echo "🔄 Redirecting to development server..." @just start # Start development server with full features dev-full: @just dev::full # Build for development build-dev: @just build::dev # Build for production build-prod: @just build::prod # Smoke-test the htmx-ssr surface after a build (full page / fragment / theme / lang) htmx-smoke base="http://127.0.0.1:3030": @set -e; BASE="{{base}}"; \ echo "→ full page must include and /assets/htmx/htmx.min.js"; \ curl -fsS "$BASE/" | grep -q " and "; \ resp=$(curl -fsS -H "HX-Request: true" "$BASE/"); \ echo "$resp" | grep -qv "[^"]+)"' | get p?.0? | default "unknown" ) let chosen = if ("{{ profile }}" == "") { print "" print $" Current rendering profile: (ansi yellow_bold)($current)(ansi reset)" print "" for i in ($profiles | enumerate) { let marker = if $i.item.name == $current { $"(ansi green)▶(ansi reset)" } else { " " } print $" ($marker) [($i.index + 1)] ($i.item.label)" } print "" let raw = (input " Select [1/2] or Enter to cancel: " | str trim) if ($raw == "") { print " Cancelled."; exit 0 } let idx = ($raw | into int | $in - 1) if $idx < 0 or $idx >= ($profiles | length) { error make { msg: $"Invalid selection: ($raw)" } } $profiles | get $idx | get name } else { "{{ profile }}" } let valid_names = ($profiles | get name) if ($chosen not-in $valid_names) { error make { msg: $"Unknown profile '($chosen)'. Valid: ($valid_names | str join ', ')" } } if $chosen == $current { print $" Already on ($chosen) — nothing to do." exit 0 } open $ncl | str replace --regex 'default_profile = "(leptos-hydration|htmx-ssr)"' $'default_profile = "($chosen)"' | save -f $ncl print $" ✅ ($current) → ($chosen)" print "" if $chosen == "htmx-ssr" { print $" Dev: (ansi cyan)just dev::htmx(ansi reset) — cargo watch, no wasm32" print $" Prod: (ansi cyan)just build-auto(ansi reset) — server-only binary" } else { print $" Dev: (ansi cyan)just start(ansi reset) — cargo leptos serve" print $" Prod: (ansi cyan)just build-auto(ansi reset) — cargo leptos build --release" } # Build for production with automatic WASM-or-native dispatch driven by site/config/rendering.ncl (ADR-006) build-auto: #!/usr/bin/env nu let nickel_path = $"{{ rustelo_root }}/resources/nickel:{{ justfile_directory() }}/site/config" let site_cfg = "{{ justfile_directory() }}/site/config/index.ncl" let check_script = "{{ rustelo_root }}/scripts/check-wasm-needed.nu" let wasm_needed = (do { nu $check_script --verbose --routes-dir "site/config" --workspace-config "site/config/rendering.ncl" } | complete | get exit_code) == 0 if $wasm_needed { print "🔨 WASM required — cargo leptos build --release" with-env { NICKEL_IMPORT_PATH: $nickel_path, SITE_CONFIG_PATH: $site_cfg } { ^cargo leptos build --release } } else { print "🪶 Pure htmx-ssr — skipping wasm32" with-env { NICKEL_IMPORT_PATH: $nickel_path, SITE_CONFIG_PATH: $site_cfg } { ^cargo build --release -p rustelo-htmx-server --no-default-features --features htmx-ssr } } # Serve production build serve-prod: @just build::serve-prod # Serve with CMS features serve-cms: @just build::serve-cms # Run all tests test-run: @just test::run # Quality checks with mandatory rule validation (main interface) quality: @echo "🔥 Running quality checks with MANDATORY rule validation..." @echo "📖 Reading critical rules from @rules/CLAUDE_CRITICAL_RULES.md..." @scripts/rules/validate-rules.nu --strict @just test::quality # Build CSS files css-build: @just dev::css-build # Watch CSS files css-watch: @just dev::css-watch # Database setup db-setup: @just database::setup # Database migrations db-migrate: @just database::migrate # Generate documentation docs-generate: @just docs::generate # Build documentation docs-build: @just docs::build # Check code quality check: @just test::check # Format code fmt: @just test::format # Fast development modes dev-fast: @just dev::dev-fast dev-coordinated: @just dev::dev-coordinated dev-minimal: @just dev::ultra-minimal build-artifacts: @just dev::build-artifacts build-status: @just dev::build-status clean-build-artifacts: @just dev::clean-build-artifacts # Complete project setup setup: @just utils::setup # Project information info: @just utils::info # Health check health: @just utils::health # ============================================================================= # NAVIGATION TESTING SHORTCUTS (Common Routes) # ============================================================================= # Test home route nav-test-home: @just dev::nav-test-route / # Test services route nav-test-services: @just dev::nav-test-route /services # Test contact route nav-test-contact: @just dev::nav-test-route /contact # Test blog route nav-test-blog: @just dev::nav-test-route /blog # Test Spanish home nav-test-es: @just dev::nav-test-route /es # Test Spanish contact nav-test-contactar: @just dev::nav-test-route /es/contactar # Clean build artifacts clean: @just build::clean # ============================================================================= # CONVENIENCE COMMANDS (Direct Module Access) # ============================================================================= # Browser testing page-tester *ARGS: @just test::page {{ARGS}} pages-report *ARGS: @just test::pages-report {{ARGS}} page-new: @just test::page-new # Cross-platform build cross-build: @just build::cross # Content operations content-build: @just dev::content-build # Export NCL metadata to index.json (NCL pipeline) content-ncl-to-json type="" lang="": @just dev::content-ncl-to-json {{type}} {{lang}} # Full NCL pipeline: _index.ncl → index.json + copy images content-ncl-build type="" lang="": @just dev::content-ncl-build {{type}} {{lang}} # Copy page-bundle images to site/public for HTTP serving content-copy-images type="" lang="": @just dev::content-copy-images {{type}} {{lang}} # Sync FTL files from source project index.html files (requires data-key attributes). # Each project index.ncl must have source_html set. sync-pages: nu scripts/sync/sync-all-pages.nu # Sync a single project FTL. Usage: just sync-page ontoref sync-page project: nu scripts/sync/sync-all-pages.nu --only {{project}} # Preview FTL sync without writing. Usage: just sync-page-dry ontoref sync-page-dry project: nu scripts/sync/sync-all-pages.nu --only {{project}} --dry-run # Sync site/public → target/site (mirrors what cargo-leptos does at build time). # Use when the dev server is not running and static assets need to be available immediately. sync-public: @rsync -a --exclude='.DS_Store' site/public/ target/site/ @echo "synced site/public → target/site" content-generate type title *args: @just dev::content-generate {{type}} {{title}} {{args}} content-generate-indices: @just dev::content-generate-indices # Generate *.ncl metadata files from existing markdown frontmatter content-md-to-ncl type="" lang="": @nu scripts/content/md-to-ncl.nu \ --content-dir "${SITE_CONTENT_PATH:-site/content}" \ {{ if type != "" { "--type " + type } else { "" } }} \ {{ if lang != "" { "--lang " + lang } else { "" } }} # Fill translations field in all *.ncl files by cross-lang stem matching (implies --force) content-md-to-ncl-translations type="" lang="": @nu scripts/content/md-to-ncl.nu --fill-translations \ --content-dir "${SITE_CONTENT_PATH:-site/content}" \ {{ if type != "" { "--type " + type } else { "" } }} \ {{ if lang != "" { "--lang " + lang } else { "" } }} # Validate all generated post *.ncl files pass their Nickel contracts content-ncl-validate type="" lang="": @nu scripts/content/md-to-ncl.nu --validate-contracts \ --content-dir "${SITE_CONTENT_PATH:-site/content}" \ {{ if type != "" { "--type " + type } else { "" } }} \ {{ if lang != "" { "--lang " + lang } else { "" } }} # Generate _index.ncl files from per-post *.ncl metadata files # Generate _index.ncl files. Pass validate=true to also check slug/id uniqueness. # Usage: just content-ncl-index [type] [lang] [validate] content-ncl-index type="" lang="" validate="": @nu scripts/content/generate-ncl-index.nu \ --content-dir "${SITE_CONTENT_PATH:-site/content}" \ {{ if type != "" { "--type " + type } else { "" } }} \ {{ if lang != "" { "--lang " + lang } else { "" } }} \ {{ if validate != "" { "--validate" } else { "" } }} # Generate + validate slug/id uniqueness in one step content-ncl-index-validate type="" lang="": @nu scripts/content/generate-ncl-index.nu --validate \ --content-dir "${SITE_CONTENT_PATH:-site/content}" \ {{ if type != "" { "--type " + type } else { "" } }} \ {{ if lang != "" { "--lang " + lang } else { "" } }} # Same as content-ncl-index but shows what would be written without writing content-ncl-index-dry type="" lang="": @nu scripts/content/generate-ncl-index.nu --dry-run --verbose \ --content-dir "${SITE_CONTENT_PATH:-site/content}" \ {{ if type != "" { "--type " + type } else { "" } }} \ {{ if lang != "" { "--lang " + lang } else { "" } }} # Full content publish pipeline for a type (default blog). Site-agnostic — drives # $SITE_CONTENT_PATH / $RUSTELO_STATIC_DIR so it runs against ANY site's content # (this outreach/site and others). Chain: # 1. md-to-ncl --force → (re)generate .ncl metadata; the patch PRESERVES # graph + thumbnail/thumbnail_dark from existing .ncl. # 2. content-ncl-validate → gate: every .ncl passes its Nickel contract. # 3. content-ncl-build → _index.ncl → index.json + filter-index + copy images # (this is what the grid card reads). # `schema_dir` is the metadata schema md-to-ncl writes the import against; leave # empty for the built-in default, or point it at the site-local nickel schema # (e.g. $HOME/.local/share/rustelo/nickel/content/metadata). # Pre/post steps that are SITE-specific (brand-linkify, ES vocab gate, content # -graph `just graph`, `just about-json`) stay in each site's justfile. # Usage: SITE_CONTENT_PATH=site/content just content-publish blog # just content-publish blog en $HOME/.local/share/rustelo/nickel/content/metadata content-publish type="blog" lang="" schema_dir="": @echo "📤 content-publish: type={{type}} lang={{lang}}" @nu scripts/content/md-to-ncl.nu --force \ --content-dir "${SITE_CONTENT_PATH:-site/content}" \ {{ if schema_dir != "" { "--schema-dir " + schema_dir } else { "" } }} \ {{ if type != "" { "--type " + type } else { "" } }} \ {{ if lang != "" { "--lang " + lang } else { "" } }} @nu scripts/content/md-to-ncl.nu --validate-contracts \ --content-dir "${SITE_CONTENT_PATH:-site/content}" \ {{ if schema_dir != "" { "--schema-dir " + schema_dir } else { "" } }} \ {{ if type != "" { "--type " + type } else { "" } }} \ {{ if lang != "" { "--lang " + lang } else { "" } }} @just content-ncl-build "{{type}}" "{{lang}}" @echo "✅ content-publish done — index.json + filter-index rebuilt" # Standalone build-tools generation command build-tools-info: @just dev::build-tools-info # Tools operations build-tools-analyze format="markdown" output="site/info": @just tools::build-tools-analyze {{format}} {{output}} build-tools-manage mode="dashboard": @just tools::tools-manage {{mode}} build-tools-generate-page name template="basic": @just tools::build-tools-generate-page {{name}} {{template}} build-tools-status: @just tools::build-tools-status # Install development dependencies npm-install: @just utils::npm-install cargo-check: @just utils::cargo-check # ============================================================================= # WORKFLOW COMMANDS # ============================================================================= # Complete development workflow workflow-dev: @echo "🔄 Running development workflow..." @just utils::setup-deps @just css-build @just check @just test @just dev # Complete production workflow workflow-prod: @echo "🔄 Running production workflow..." @just test::quality @just build-prod @just build::docker @just deploy # Pre-commit workflow pre-commit: @echo "🔄 Running pre-commit workflow..." @just fmt @just test::check-strict @just test @just css-build # CI/CD workflow ci: @echo "🔄 Running CI/CD workflow..." @just test::format-check @just test::check-strict @just test @just test::audit @just build-prod # ============================================================================= # HELP COMMANDS (Delegated to help module) # ============================================================================= # Show help for development commands help-dev: @just helptext::dev # Show help for build commands help-build: @just helptext::build # Show help for testing commands help-test: @just helptext::test # Show help for database commands help-db: @just helptext::db # Show help for documentation commands help-docs: @just helptext::docs # Show help for utility commands help-utils: @just helptext::utils # Show help for build-tools commands help-build-tools: @just helptext::build-tools # Show help for modules help-modules: @just helptext::modules # Show comprehensive help help-all: @just helptext::all # Main help entry point help: @just helptext::main # Help with topic argument help-topic TOPIC: @just helptext::topic {{TOPIC}} # Alias h that redirects to help with arguments h *TOPIC: #!/usr/bin/env bash TOPIC="{{TOPIC}}" if [ -n "$TOPIC" ]; then just help-topic "$TOPIC" else just help fi # Logo (delegated to help module) logo: @just helptext::logo server-browser-logs: @just dev::server-browser-logs browser-tools-server: @just dev::browser-tools-server browser-logs page="/": @just dev::browser-logs {{page}} # ============================================================================= # LOCAL INSTALL # Builds the htmx-ssr binary, installs it to ~/.local/libexec, syncs rustelo # nickel resources, assembles htmx templates, and writes the site-root-aware # wrapper to ~/.local/bin/rustelo-htmx-server. # ============================================================================= local-install: #!/usr/bin/env nu let nickel_path = $"{{ rustelo_root }}/resources/nickel:{{ justfile_directory() }}/site/config" let site_cfg = "{{ justfile_directory() }}/site/config/index.ncl" print "→ building rustelo-htmx-server (release, htmx-ssr)" with-env { NICKEL_IMPORT_PATH: $nickel_path, SITE_CONFIG_PATH: $site_cfg } { ^cargo build --release -p rustelo-htmx-server --no-default-features --features htmx-ssr } let cargo_cfg = "{{ justfile_directory() }}/.cargo/config.toml" let target_dir = ( $env.CARGO_TARGET_DIR? | default ( if ($cargo_cfg | path exists) { (open $cargo_cfg).build?."target-dir"? | default "{{ justfile_directory() }}/target" } else { "{{ justfile_directory() }}/target" } ) ) let home = $env.HOME let libexec = ($home | path join ".local/libexec") let bin_dir = ($home | path join ".local/bin") let share_dir = ($home | path join ".local/share/rustelo-htmx-server") let nickel_dir = ($home | path join ".local/share/rustelo/nickel") mkdir $libexec $bin_dir $share_dir $nickel_dir print "→ installing binary → ~/.local/libexec/rustelo-htmx-server" cp ($target_dir | path join "release/rustelo-htmx-server") ($libexec | path join "rustelo-htmx-server") print "→ syncing rustelo nickel → ~/.local/share/rustelo/nickel" ^rsync -a --delete $"{{ rustelo_root }}/resources/nickel/" $"($nickel_dir)/" print "→ assembling htmx templates" let tmpl_out = ($target_dir | path join "site/htmx-templates") nu "{{ justfile_directory() }}/scripts/build/assemble-htmx-templates.nu" --out $tmpl_out print "→ installing templates → ~/.local/share/rustelo-htmx-server/htmx-templates" ^rsync -a --delete $"($tmpl_out)/" ($share_dir | path join "htmx-templates/") print "→ installing htmx runtime → ~/.local/share/rustelo-htmx-server/htmx" ^rsync -a --delete $"{{ rustelo_root }}/templates/shared/htmx/" ($share_dir | path join "htmx/") # gen-content-graph: build from rustelo workspace so workspace = true deps resolve. # --manifest-path lets Cargo walk up to rustelo/code/Cargo.toml as workspace root. print "→ building gen-content-graph (release, gen)" let gen_manifest = $"{{ rustelo_root }}/crates/foundation/crates/rustelo_content_graph_ssr/Cargo.toml" with-env { CARGO_TARGET_DIR: $target_dir, NICKEL_IMPORT_PATH: $nickel_path, SITE_CONFIG_PATH: $site_cfg } { ^cargo build --release -p rustelo_content_graph_ssr --features gen --manifest-path $gen_manifest } print "→ installing gen-content-graph → ~/.local/bin/gen-content-graph" let gen_dst = ($bin_dir | path join "gen-content-graph") cp ($target_dir | path join "release/gen-content-graph") $gen_dst ^chmod 755 $gen_dst # content_processor: the REAL generator is the framework's rustelo_server bin # (requires content-static); the project's same-named bin is a stub. Build it # into rustelo's own target dir to avoid colliding with that stub in our target. print "→ building content_processor (release, content-static)" let rustelo_cargo_cfg = $"{{ rustelo_root }}/.cargo/config.toml" let rustelo_target = ( if ($rustelo_cargo_cfg | path exists) { (open $rustelo_cargo_cfg).build?."target-dir"? | default $"{{ rustelo_root }}/target" } else { $"{{ rustelo_root }}/target" } ) let cp_manifest = $"{{ rustelo_root }}/crates/foundation/crates/rustelo_server/Cargo.toml" with-env { CARGO_TARGET_DIR: $rustelo_target, NICKEL_IMPORT_PATH: $nickel_path, SITE_CONFIG_PATH: $site_cfg } { ^cargo build --release -p rustelo_server --bin content_processor --features content-static --manifest-path $cp_manifest } print "→ installing content_processor → ~/.local/libexec/content_processor" cp ($rustelo_target | path join "release/content_processor") ($libexec | path join "content_processor") print "→ writing content_processor wrapper → ~/.local/bin/content_processor" let cp_wrapper = ($bin_dir | path join "content_processor") cp "{{ justfile_directory() }}/scripts/local-install/content_processor.sh" $cp_wrapper ^chmod 755 $cp_wrapper print "→ writing wrapper → ~/.local/bin/rustelo-htmx-server" let wrapper_dst = ($bin_dir | path join "rustelo-htmx-server") cp "{{ justfile_directory() }}/scripts/local-install/rustelo-htmx-server.sh" $wrapper_dst ^chmod 755 $wrapper_dst print "" print "✅ done — usage:" print " rustelo-htmx-server # auto-detects site/ in $PWD" print " rustelo-htmx-server --site # explicit workspace root" print $" # or place site tree at ($home)/.config/rustelo-htmx-server/site/" print " gen-content-graph # regenerate content_graph.json (needs ONTOREF_NICKEL_IMPORT_PATH)" print " content_processor --content-type blog # regenerate site/r content indexes from site/content" # ============================================================================= # CONTENT TOOLKIT INSTALL # Installs the Nushell content-authoring scripts to ~/.local/share/rustelo-content # and the `rustelo-content` dispatcher to ~/.local/bin. Sites consume the toolkit # via the content.just module (mod content) without a source checkout alongside. # ============================================================================= content-install: #!/usr/bin/env nu let home = $env.HOME let share = ($home | path join ".local/share/rustelo-content") let bin = ($home | path join ".local/bin") mkdir $share $bin print "→ installing content scripts → ~/.local/share/rustelo-content" (^rsync -a --delete --exclude "old/" --exclude ".image-spend.jsonl" --exclude "pending/" $"{{ justfile_directory() }}/scripts/content/" $"($share)/") print "→ writing dispatcher → ~/.local/bin/rustelo-content" let dst = ($bin | path join "rustelo-content") cp "{{ justfile_directory() }}/scripts/local-install/rustelo-content.sh" $dst ^chmod 755 $dst print "" print "✅ done — usage from a site root (dir containing site/):" print " rustelo-content images --dry-run --type blog" print " rustelo-content sync validate-translations" print " # via just module: mod content \".just/content.just\" → just content images" # ============================================================================= # AUTHORING SETUP # Wire a consumer site to the authoring toolkit: ensure the rustelo-content CLI # is installed, symlink this repo's content.just into /.just/ (relative, # portable), and add `mod authoring` to the site justfile. Idempotent. # just authoring-setup /path/to/site (defaults to PWD) # ============================================================================= authoring-setup site=".": #!/usr/bin/env nu let src_module = ("{{ justfile_directory() }}/justfiles/content.just" | path expand) let site = ("{{ site }}" | path expand) let site_just = ($site | path join "justfile") if not ($site_just | path exists) { error make { msg: $"No justfile at ($site) — pass a site root: just authoring-setup " } } # 1. ensure the CLI is on PATH if (which rustelo-content | is-empty) { print "→ rustelo-content not found; running content-install" just content-install } else { print "→ rustelo-content already on PATH" } # 2. symlink the module into /.just/ with a relative target (survives # relocating the whole tree; symlink target is relative to the link's dir) let just_dir = ($site | path join ".just") mkdir $just_dir let link = ($just_dir | path join "content.just") let from = ($just_dir | path split) let to = ($src_module | path split) let n = ([($from | length) ($to | length)] | math min) mut i = 0 while $i < $n and ($from | get $i) == ($to | get $i) { $i = $i + 1 } let ups = (0..<(($from | length) - $i) | each { ".." }) let rel = (($ups ++ ($to | skip $i)) | path join) ^ln -sf $rel $link print $"→ linked ($link) → ($rel)" # 3. ensure `mod authoring` in the site justfile (idempotent) let txt = (open $site_just) if ($txt | str contains "mod authoring") { print "→ mod authoring already present" } else { let lines = ($txt | lines) let assign_rows = ($lines | enumerate | where { |e| let t = ($e.item | str trim) (not ($t | str starts-with "#")) and ($t | str contains ":=") }) let set_rows = ($lines | enumerate | where { |e| ($e.item | str trim | str starts-with "set ") }) let anchor = if (not ($assign_rows | is-empty)) { ($assign_rows | last | get index) } else if (not ($set_rows | is-empty)) { ($set_rows | last | get index) } else { -1 } let modline = 'mod authoring ".just/content.just"' let comment = '# Content-authoring toolkit (rustelo-content): images | sync | validate | new' let new_lines = if $anchor < 0 { ([$comment $modline ""] ++ $lines) } else { ($lines | enumerate | each { |e| if $e.index == $anchor { [$e.item "" $comment $modline] } else { [$e.item] } } | flatten) } $new_lines | str join "\n" | save --force $site_just print $"→ added `mod authoring` to ($site_just)" } print "" print "✅ site wired — run: just authoring images --dry-run --type blog" # ============================================================================= # AUTHORING SETUP — BATCH # Discover every justfile under that targets this source repo (references # "website-htmx-rustelo") and wire each site via authoring-setup. defaults # to the dir two levels above this repo (the shared Development tree). Idempotent. # just authoring-setup-all [root] # ============================================================================= authoring-setup-all root="": #!/usr/bin/env nu if (which rg | is-empty) { error make { msg: "ripgrep (rg) is required for site discovery" } } let src_root = ("{{ justfile_directory() }}" | path expand) let root = if ("{{ root }}" | is-empty) { ($src_root | path dirname | path dirname) } else { ("{{ root }}" | path expand) } print $"→ scanning ($root) for sites targeting website-htmx-rustelo" # rg exit 1 = no matches (benign); >1 = real error let scan = (^rg -l --no-ignore -g "justfile" "website-htmx-rustelo" $root | complete) if $scan.exit_code > 1 { error make { msg: $"rg failed: ($scan.stderr)" } } let sites = ( $scan.stdout | lines | where { |l| ($l | str trim) != "" } | where { |l| not (($l | path expand) | str starts-with $src_root) } | each { |j| $j | path expand | path dirname } | uniq ) if ($sites | is-empty) { print " no consumer sites found." return } print $" found ($sites | length) site\(s\):" $sites | each { |s| print $" ($s)" } print "" for s in $sites { print $"━━ wiring ($s)" just -f $"{{ justfile_directory() }}/justfile" authoring-setup $s } print "" print $"✅ wired ($sites | length) site\(s\)"