///|
pub struct ScaffoldFile {
path : String
content : String
} derive(Debug, Eq)
///|
pub struct ScaffoldPlan {
name : String
module_name : String
files : Array[ScaffoldFile]
summary : String
} derive(Debug, Eq)
///|
pub fn scaffold_project(
name~ : String,
module_name? : String = "",
) -> ScaffoldPlan {
let module_id = if module_name == "" { "local/\{name}" } else { module_name }
let readme_content = readme(name)
let files = [
scaffold_file("moon.mod", moon_mod(module_id)),
scaffold_file("moon.pkg", moon_pkg()),
scaffold_file("cmd/main/moon.pkg", cmd_main_pkg(module_id)),
scaffold_file("cmd/main/main.mbt", cmd_main_mbt()),
scaffold_file("app.mbt", app_mbt(name)),
scaffold_file("agent_feed.mbt", agent_feed_mbt()),
scaffold_file("scene.mbt", scene_mbt()),
scaffold_file("updates.mbt", updates_mbt()),
scaffold_file("quality.mbt", quality_mbt()),
scaffold_file("wechat.mbt", wechat_mbt(name)),
scaffold_file("app_test.mbt", app_test_mbt()),
scaffold_file("README.mbt.md", readme_content),
scaffold_file("README.md", readme_content),
scaffold_file(".gitignore", gitignore()),
scaffold_file("scripts/ci.sh", ci_script()),
scaffold_file("scripts/validate_boundaries.sh", boundary_script()),
scaffold_file(".github/workflows/ci.yml", github_ci_workflow()),
]
{
name,
module_name: module_id,
files,
summary: "Bunnia scaffold: name=\{name} module=\{module_id} files=\{files.length()}",
}
}
///|
pub fn scaffold_file(path : String, content : String) -> ScaffoldFile {
{ path, content }
}
///|
fn moon_mod(module_name : String) -> String {
"name = \"\{escape_toml(module_name)}\"\n\n" +
"version = \"0.1.0\"\n\n" +
"readme = \"README.mbt.md\"\n\n" +
"import {\n" +
" \"vectie/bunnia@0.1.0\"\n" +
"}\n\n" +
"preferred_target = \"js\"\n\n" +
"description = \"Bunnia mini-app starter.\"\n"
}
///|
fn gitignore() -> String {
".DS_Store\n" + "_build/\n" + "target/\n" + ".mooncakes/\n" + ".moonagent/\n"
}
///|
fn ci_script() -> String {
"#!/usr/bin/env sh\n" +
"set -eu\n\n" +
"mkdir -p _build\n" +
"moon run cmd/main -- ci-plan --script --strict > _build/bunnia-ci.sh\n" +
"sh _build/bunnia-ci.sh\n"
}
///|
fn boundary_script() -> String {
let blocked_module = "vectie/moon" + "town"
let forbidden = starter_forbidden_pattern()
"#!/usr/bin/env sh\n" +
"set -eu\n\n" +
"blocked_module='\{blocked_module}'\n" +
"forbidden='\{forbidden}'\n\n" +
"if grep -n \"$blocked_module\" moon.mod moon.pkg cmd/main/moon.pkg; then\n" +
" printf '%s\\n' 'boundary violation: starter must not depend on blocked product module'\n" +
" exit 1\n" +
"fi\n\n" +
"if grep -n -E -i \"$forbidden\" app.mbt agent_feed.mbt scene.mbt updates.mbt quality.mbt wechat.mbt app_test.mbt cmd/main/main.mbt README.mbt.md README.md; then\n" +
" printf '%s\\n' 'boundary violation: starter must stay product-neutral; move product nouns into downstream app examples'\n" +
" exit 1\n" +
"fi\n\n" +
"printf '%s\\n' 'boundary=ok'\n"
}
///|
fn starter_forbidden_pattern() -> String {
"moon" +
"town|wen" +
"yu|may" +
"or|civ" +
"ic[_ -]?build" +
"ing|" +
"moon" +
"book|moon" +
"claw|stand" +
"ing[_ -]?watch|to" +
"wn[_ -]?module"
}
///|
fn github_ci_workflow() -> String {
"name: Bunnia CI\n\n" +
"on:\n" +
" push:\n" +
" pull_request:\n" +
" workflow_dispatch:\n\n" +
"jobs:\n" +
" bunnia:\n" +
" runs-on: ubuntu-latest\n" +
" permissions:\n" +
" contents: read\n" +
" steps:\n" +
" - name: Checkout\n" +
" uses: actions/checkout@v5\n" +
" - name: Set up MoonBit\n" +
" run: |\n" +
" curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash\n" +
" echo \"$HOME/.moon/bin\" >> $GITHUB_PATH\n" +
" - name: Run Bunnia starter CI\n" +
" run: sh scripts/ci.sh\n"
}
///|
fn moon_pkg() -> String {
"import {\n" + " \"vectie/bunnia\"\n" + "}\n"
}
///|
fn cmd_main_pkg(module_name : String) -> String {
"import {\n" +
" \"\{escape_toml(module_name)}\" @app,\n" +
" \"vectie/bunnia\",\n" +
" \"moonbitlang/core/env\",\n" +
"}\n\n" +
"options(\n" +
" \"is-main\": true,\n" +
")\n"
}
///|
fn cmd_main_mbt() -> String {
"///|\n" +
"fn main {\n" +
" let command = command_from_args(@env.args())\n" +
" let strict = strict_from_args(@env.args())\n" +
" let target_support = target_support_from_args(@env.args())\n" +
" if command == \"help\" {\n" +
" print_help()\n" +
" } else if command == \"unknown\" {\n" +
" println(\"unsupported command=\\{unknown_command_from_args(@env.args())}\")\n" +
" print_help()\n" +
" panic()\n" +
" } else if command == \"ci-plan\" {\n" +
" if ci_script_from_args(@env.args()) {\n" +
" print_ci_script(strict, target_support)\n" +
" } else {\n" +
" print_ci_plan(strict, target_support)\n" +
" }\n" +
" } else if command == \"limits\" {\n" +
" print_platform_limits(@bunnia.platform_limits(target=target_from_args(@env.args())))\n" +
" } else {\n" +
" ensure_target_generator_available(target_support)\n" +
" let output_dir = output_dir_from_args(@env.args())\n" +
" let project = @app.starter_wechat_project()\n" +
" println(\"command=\\{command}\")\n" +
" println(\"target=\\{target_support.target}\")\n" +
" println(\"strict=\\{strict}\")\n" +
" println(project.summary)\n" +
" println(project.manifest.summary)\n" +
" println(project.report.summary)\n" +
" let inspection_gate = @app.inspection_gate(project)\n" +
" let profile_gate = @app.profile_gate(project)\n" +
" let release = @app.release_readiness(project)\n" +
" println(inspection_gate.summary)\n" +
" println(profile_gate.summary)\n" +
" println(release.summary)\n" +
" if command == \"inspect\" {\n" +
" print_inspect_result(project, profile_gate, inspection_gate, release)\n" +
" } else if command == \"snapshot\" {\n" +
" let snapshot = starter_snapshot(project)\n" +
" let snapshot_path = snapshot_output_path_from_args(@env.args())\n" +
" write_text_file(snapshot_path, snapshot.content)\n" +
" println(snapshot.summary)\n" +
" println(\"snapshot=\\{snapshot_path}\")\n" +
" } else {\n" +
" for file in project.files {\n" +
" write_text_file(join_path(output_dir, file.path), file.content)\n" +
" }\n" +
" println(\"wrote=\\{project.files.length()} output=\\{output_dir}\")\n" +
" }\n" +
" if should_fail(strict, project, profile_gate, inspection_gate, release) {\n" +
" println(\"build_failed=diagnostics\")\n" +
" panic()\n" +
" }\n" +
" if command == \"watch\" {\n" +
" if watch_once_from_args(@env.args()) {\n" +
" println(\"watch=once\")\n" +
" } else {\n" +
" watch_generated_output(target_support.target, output_dir, strict)\n" +
" }\n" +
" }\n" +
" }\n" +
"}\n\n" +
"///|\n" +
"fn command_from_args(args : Array[String]) -> String {\n" +
" if args.contains(\"help\") || args.contains(\"--help\") || args.contains(\"-h\") {\n" +
" return \"help\"\n" +
" }\n" +
" for i in 0.. String {\n" +
" for i in 0.. Bool {\n" +
" item == \"build\" ||\n" +
" item == \"watch\" ||\n" +
" item == \"inspect\" ||\n" +
" item == \"snapshot\" ||\n" +
" item == \"ci-plan\" ||\n" +
" item == \"limits\"\n" +
"}\n\n" +
"///|\n" +
"fn command_arg_is_ignored(args : Array[String], index : Int) -> Bool {\n" +
" let item = args[index]\n" +
" command_arg_is_runner(item) ||\n" +
" item.has_prefix(\"-\") ||\n" +
" (index > 0 && command_option_consumes_value(args[index - 1]))\n" +
"}\n\n" +
"///|\n" +
"fn command_arg_is_runner(item : String) -> Bool {\n" +
" item == \"node\" || item.has_suffix(\"/node\") || item.has_suffix(\".js\")\n" +
"}\n\n" +
"///|\n" +
"fn command_option_consumes_value(item : String) -> Bool {\n" +
" item == \"--target\" ||\n" +
" item == \"--out\" ||\n" +
" item == \"--snapshot-out\"\n" +
"}\n\n" +
"///|\n" +
"fn print_help() -> Unit {\n" +
" println(\"Bunnia starter CLI\")\n" +
" println(\"usage=moon run cmd/main -- [options]\")\n" +
" println(\"command=build writes=_build/wechat\")\n" +
" println(\"command=watch writes=_build/wechat options=--once\")\n" +
" println(\"command=inspect writes=false\")\n" +
" println(\"command=snapshot writes=snapshot-file\")\n" +
" println(\"command=ci-plan writes=false options=--script\")\n" +
" println(\"command=limits writes=false options=--target wechat|alipay|tiktok\")\n" +
" println(\"options=--target --strict --out --snapshot-out --once\")\n" +
"}\n\n" +
"///|\n" +
"fn target_from_args(args : Array[String]) -> String {\n" +
" for i in 0.. String {\n" +
" for i in 0.. String {\n" +
" for i in 0.. Bool {\n" +
" args.contains(\"--strict\") || args.contains(\"--fail-on-diagnostics\")\n" +
"}\n\n" +
"///|\n" +
"fn watch_once_from_args(args : Array[String]) -> Bool {\n" +
" args.contains(\"--once\")\n" +
"}\n\n" +
"///|\n" +
"fn ci_script_from_args(args : Array[String]) -> Bool {\n" +
" args.contains(\"--script\")\n" +
"}\n\n" +
"///|\n" +
"fn target_support_from_args(args : Array[String]) -> @bunnia.PlatformTargetSupport {\n" +
" @bunnia.platform_target_support(target=target_from_args(args))\n" +
"}\n\n" +
"///|\n" +
"fn ensure_target_generator_available(\n" +
" support : @bunnia.PlatformTargetSupport,\n" +
") -> Unit {\n" +
" if !support.generator_available {\n" +
" println(support.summary)\n" +
" print_target_diagnostics(support)\n" +
" panic()\n" +
" }\n" +
"}\n\n" +
"///|\n" +
"fn print_target_diagnostics(support : @bunnia.PlatformTargetSupport) -> Unit {\n" +
" for item in support.diagnostics {\n" +
" println(\"diagnostic=\\{item}\")\n" +
" }\n" +
"}\n\n" +
"///|\n" +
"fn should_fail(\n" +
" strict : Bool,\n" +
" project : @bunnia.WechatProject,\n" +
" profile_gate : @bunnia.BuildProfileGate,\n" +
" inspection_gate : @bunnia.ProjectInspectionGate,\n" +
" release : @bunnia.ReleaseReadinessReport,\n" +
") -> Bool {\n" +
" strict && (\n" +
" project.report.diagnostics.length() > 0 ||\n" +
" project.manifest.diagnostic_count > 0 ||\n" +
" profile_gate.diagnostics.length() > 0 ||\n" +
" inspection_gate.diagnostics.length() > 0 ||\n" +
" release.diagnostics.length() > 0\n" +
" )\n" +
"}\n\n" +
"///|\n" +
"fn starter_snapshot(project : @bunnia.WechatProject) -> @bunnia.GeneratedSnapshot {\n" +
" let profile = @app.build_profile(project)\n" +
" @bunnia.snapshot_wechat_project_with_report(\n" +
" target=\"wechat\",\n" +
" example=\"starter\",\n" +
" project,\n" +
" project.report,\n" +
" profile,\n" +
" budget_profile=\"tight\",\n" +
" render_budget_profile=\"tight\",\n" +
" )\n" +
"}\n\n" +
"///|\n" +
"fn print_inspect_result(\n" +
" project : @bunnia.WechatProject,\n" +
" profile_gate : @bunnia.BuildProfileGate,\n" +
" inspection_gate : @bunnia.ProjectInspectionGate,\n" +
" release : @bunnia.ReleaseReadinessReport,\n" +
") -> Unit {\n" +
" let inspection = @app.project_inspection(project)\n" +
" let profile = @app.build_profile(project)\n" +
" println(profile.summary)\n" +
" println(inspection.summary)\n" +
" println(\"inspect=pages=\\{project.page_count} files=\\{project.files.length()} bytes=\\{project.total_bytes} risk_route=\\{inspection.top_route} risk_score=\\{inspection.risk_score} max_unwindowed_list_children=\\{inspection.max_unwindowed_list_child_count} max_unkeyed_list_children=\\{inspection.max_unkeyed_list_child_count} max_duplicate_list_keys=\\{inspection.max_duplicate_list_key_count} diagnostics=\\{project.manifest.diagnostic_count} profile_gate_diagnostics=\\{profile_gate.diagnostics.length()} inspection_gate_diagnostics=\\{inspection_gate.diagnostics.length()}\")\n" +
" for page in project.manifest.pages {\n" +
" println(\"route=\\{page.route} status=\\{page.status} nodes=\\{page.node_count} events=\\{page.event_count} unwindowed_list_children=\\{page.unwindowed_list_child_count} unkeyed_list_children=\\{page.unkeyed_list_child_count} duplicate_list_keys=\\{page.duplicate_list_key_count} backend_events=\\{page.backend_event_count} backend_streams=\\{page.backend_stream_count} backend_reviews=\\{page.backend_review_count} scene_markers=\\{page.scene_visible_marker_count}/\\{page.scene_marker_count} scene_regions=\\{page.scene_visible_region_count}/\\{page.scene_region_count} first_screen=\\{page.first_screen_bytes} update_payload=\\{page.update_payload_bytes} update_ops=\\{page.update_operation_count} diagnostics=\\{page.diagnostic_count}\")\n" +
" }\n" +
" for item in inspection_gate.diagnostics {\n" +
" println(\"inspection_diagnostic=\\{item}\")\n" +
" }\n" +
" for item in profile_gate.diagnostics {\n" +
" println(\"profile_gate_diagnostic=\\{item}\")\n" +
" }\n" +
" for item in release.diagnostics {\n" +
" println(\"release_diagnostic=\\{item}\")\n" +
" }\n" +
" for file in project.manifest.files {\n" +
" println(\"file=\\{file.path} kind=\\{file.kind} bytes=\\{file.bytes}\")\n" +
" }\n" +
"}\n\n" +
"///|\n" +
"fn print_ci_script(\n" +
" strict : Bool,\n" +
" target_support : @bunnia.PlatformTargetSupport,\n" +
") -> Unit {\n" +
" let strict_flag = if strict { \" --strict\" } else { \"\" }\n" +
" println(\"#!/usr/bin/env sh\")\n" +
" println(\"set -eu\")\n" +
" println(\"\")\n" +
" println(\"# Generated by Bunnia starter ci-plan --script.\")\n" +
" println(\"# \\{target_support.summary}\")\n" +
" println(\"\")\n" +
" if !target_support.generator_available {\n" +
" println(\"# Bunnia starter CI plan: steps=12 diagnostics=\\{target_support.diagnostics.length()}\")\n" +
" println(\"echo 'Bunnia starter CI plan diagnostics:'\")\n" +
" for item in target_support.diagnostics {\n" +
" println(\"echo '\\{item}'\")\n" +
" }\n" +
" println(\"exit 1\")\n" +
" } else {\n" +
" let project = @app.starter_wechat_project()\n" +
" let inspection_gate = @app.inspection_gate(project)\n" +
" let profile_gate = @app.profile_gate(project)\n" +
" let release = @app.release_readiness(project)\n" +
" let diagnostics = project.report.diagnostics.length() +\n" +
" project.manifest.diagnostic_count +\n" +
" inspection_gate.diagnostics.length() +\n" +
" profile_gate.diagnostics.length() +\n" +
" release.diagnostics.length()\n" +
" println(\"# Bunnia starter CI plan: steps=12 diagnostics=\\{diagnostics}\")\n" +
" println(\"# \\{inspection_gate.summary}\")\n" +
" println(\"# \\{profile_gate.summary}\")\n" +
" println(\"# \\{release.summary}\")\n" +
" println(\"\")\n" +
" if diagnostics > 0 {\n" +
" println(\"echo 'Bunnia starter CI plan has diagnostics; run moon run cmd/main -- ci-plan --strict for details.'\")\n" +
" println(\"exit 1\")\n" +
" } else {\n" +
" println(\"echo '==> check'\")\n" +
" println(\"moon check\")\n" +
" println(\"echo '==> test'\")\n" +
" println(\"moon test\")\n" +
" println(\"echo '==> info'\")\n" +
" println(\"moon info\")\n" +
" println(\"echo '==> fmt'\")\n" +
" println(\"moon fmt\")\n" +
" println(\"echo '==> diff-check'\")\n" +
" println(\"if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then git diff --check -- .; else printf '%s\\\\n' 'skip=diff-check reason=no-git-worktree'; fi\")\n" +
" println(\"echo '==> diff-clean'\")\n" +
" println(\"if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then git diff --exit-code -- .; else printf '%s\\\\n' 'skip=diff-clean reason=no-git-worktree'; fi\")\n" +
" println(\"echo '==> boundary'\")\n" +
" println(\"sh scripts/validate_boundaries.sh\")\n" +
" println(\"echo '==> limits'\")\n" +
" println(\"moon run cmd/main -- limits --target \\{target_support.target}\")\n" +
" println(\"echo '==> inspect'\")\n" +
" println(\"moon run cmd/main -- inspect --target \\{target_support.target}\\{strict_flag}\")\n" +
" println(\"echo '==> snapshot'\")\n" +
" println(\"moon run cmd/main -- snapshot --target \\{target_support.target} --snapshot-out _build/snapshots/wechat/starter.snapshot.txt\\{strict_flag}\")\n" +
" println(\"echo '==> build'\")\n" +
" println(\"moon run cmd/main -- build --target \\{target_support.target} --out _build/wechat\\{strict_flag}\")\n" +
" println(\"echo '==> watch-once'\")\n" +
" println(\"moon run cmd/main -- watch --target \\{target_support.target} --out _build/wechat-watch --once\\{strict_flag}\")\n" +
" }\n" +
" }\n" +
"}\n\n" +
"///|\n" +
"fn print_ci_plan(\n" +
" strict : Bool,\n" +
" target_support : @bunnia.PlatformTargetSupport,\n" +
") -> Unit {\n" +
" let strict_flag = if strict { \" --strict\" } else { \"\" }\n" +
" println(\"command=ci-plan\")\n" +
" println(\"target=\\{target_support.target}\")\n" +
" println(\"strict=\\{strict}\")\n" +
" println(target_support.summary)\n" +
" if !target_support.generator_available {\n" +
" println(\"Bunnia starter CI plan: steps=12 diagnostics=\\{target_support.diagnostics.length()}\")\n" +
" print_target_diagnostics(target_support)\n" +
" } else {\n" +
" let project = @app.starter_wechat_project()\n" +
" let inspection_gate = @app.inspection_gate(project)\n" +
" let profile_gate = @app.profile_gate(project)\n" +
" let release = @app.release_readiness(project)\n" +
" let diagnostics = project.report.diagnostics.length() +\n" +
" project.manifest.diagnostic_count +\n" +
" inspection_gate.diagnostics.length() +\n" +
" profile_gate.diagnostics.length() +\n" +
" release.diagnostics.length()\n" +
" println(\"Bunnia starter CI plan: steps=12 diagnostics=\\{diagnostics}\")\n" +
" println(inspection_gate.summary)\n" +
" println(profile_gate.summary)\n" +
" println(release.summary)\n" +
" for item in project.report.diagnostics {\n" +
" println(\"diagnostic=\\{item}\")\n" +
" }\n" +
" for page in project.manifest.pages {\n" +
" for item in page.diagnostics {\n" +
" println(\"route_diagnostic=\\{page.route}:\\{item}\")\n" +
" }\n" +
" }\n" +
" for item in inspection_gate.diagnostics {\n" +
" println(\"inspection_diagnostic=\\{item}\")\n" +
" }\n" +
" for item in profile_gate.diagnostics {\n" +
" println(\"profile_gate_diagnostic=\\{item}\")\n" +
" }\n" +
" for item in release.diagnostics {\n" +
" println(\"release_diagnostic=\\{item}\")\n" +
" }\n" +
" println(\"step=check command=moon check\")\n" +
" println(\"step=test command=moon test\")\n" +
" println(\"step=info command=moon info\")\n" +
" println(\"step=fmt command=moon fmt\")\n" +
" println(\"step=diff-check command=git diff --check -- .\")\n" +
" println(\"step=diff-clean command=git diff --exit-code -- .\")\n" +
" println(\"step=boundary command=sh scripts/validate_boundaries.sh\")\n" +
" println(\"step=limits command=moon run cmd/main -- limits --target \\{target_support.target}\")\n" +
" println(\"step=inspect command=moon run cmd/main -- inspect --target \\{target_support.target}\\{strict_flag}\")\n" +
" println(\"step=snapshot command=moon run cmd/main -- snapshot --target \\{target_support.target} --snapshot-out _build/snapshots/wechat/starter.snapshot.txt\\{strict_flag}\")\n" +
" println(\"step=build command=moon run cmd/main -- build --target \\{target_support.target} --out _build/wechat\\{strict_flag}\")\n" +
" println(\"step=watch-once command=moon run cmd/main -- watch --target \\{target_support.target} --out _build/wechat-watch --once\\{strict_flag}\")\n" +
" }\n" +
"}\n\n" +
"///|\n" +
"fn print_platform_limits(report : @bunnia.PlatformLimitsReport) -> Unit {\n" +
" println(\"command=limits\")\n" +
" println(\"target=\\{report.target}\")\n" +
" println(report.summary)\n" +
" for item in report.diagnostics {\n" +
" println(\"diagnostic=\\{item}\")\n" +
" }\n" +
" println(\"generator_status=\\{report.generator_status_id}\")\n" +
" println(\"capability=canvas:\\{report.supports_canvas} cloud:\\{report.supports_cloud} stream:\\{report.supports_stream} generator:\\{report.generator_available}\")\n" +
" for item in report.component_mappings {\n" +
" println(\"component=\\{item.bunnia_kind} platform_component=\\{item.platform_component}\")\n" +
" }\n" +
" for item in report.event_mappings {\n" +
" println(\"event=\\{item.bunnia_event} platform_event=\\{item.platform_event}\")\n" +
" }\n" +
" for item in report.lifecycle_mappings {\n" +
" println(\"lifecycle=\\{item.bunnia_lifecycle} platform_lifecycle=\\{item.platform_lifecycle}\")\n" +
" }\n" +
" for item in report.request_mappings {\n" +
" println(\"request=\\{item.bunnia_request} platform_api=\\{item.platform_api}\")\n" +
" }\n" +
"}\n\n" +
"///|\n" +
"fn join_path(left : String, right : String) -> String {\n" +
" if left == \"\" {\n" +
" right\n" +
" } else if left.has_suffix(\"/\") {\n" +
" \"\\{left}\\{right}\"\n" +
" } else {\n" +
" \"\\{left}/\\{right}\"\n" +
" }\n" +
"}\n\n" +
"///|\n" +
"#cfg(target=\"js\")\n" +
"extern \"js\" fn watch_generated_output(\n" +
" target : String,\n" +
" output_dir : String,\n" +
" strict : Bool,\n" +
") -> Unit =\n" +
" #| function(target, outputDir, strict) {\n" +
" #| const fs = require(\"fs\")\n" +
" #| const childProcess = require(\"child_process\")\n" +
" #| const watchRoots = [\"cmd\", \"scripts\", \".github\"]\n" +
" #| const rootFiles = [\"moon.mod\", \"moon.pkg\", \"app.mbt\", \"agent_feed.mbt\", \"scene.mbt\", \"updates.mbt\", \"quality.mbt\", \"wechat.mbt\", \"app_test.mbt\", \"README.mbt.md\", \"README.md\"]\n" +
" #| const buildArgs = [\"run\", \"cmd/main\", \"--\", \"build\", \"--target\", target, \"--out\", outputDir]\n" +
" #| if (strict) buildArgs.push(\"--strict\")\n" +
" #| function shouldRebuild(filename) {\n" +
" #| if (!filename) return true\n" +
" #| const text = String(filename)\n" +
" #| if (text.includes(\"_build\") || text.includes(\".git\")) return false\n" +
" #| return text.endsWith(\".mbt\") || text.endsWith(\".mbt.md\") || text.endsWith(\".md\") || text.endsWith(\".mbti\") || text.endsWith(\".sh\") || text.endsWith(\".yml\") || text.endsWith(\"moon.pkg\") || text.endsWith(\"moon.mod\")\n" +
" #| }\n" +
" #| let timer = null\n" +
" #| function rebuild(reason) {\n" +
" #| if (!shouldRebuild(reason)) return\n" +
" #| clearTimeout(timer)\n" +
" #| timer = setTimeout(() => {\n" +
" #| console.log(\"watch=rebuild reason=\" + (reason || \"change\"))\n" +
" #| const result = childProcess.spawnSync(\"moon\", buildArgs, { stdio: \"inherit\" })\n" +
" #| if (result.error) {\n" +
" #| console.error(\"watch_error=\" + result.error.message)\n" +
" #| } else if (result.status !== 0) {\n" +
" #| console.error(\"watch_exit=\" + result.status)\n" +
" #| }\n" +
" #| }, 150)\n" +
" #| }\n" +
" #| for (const file of rootFiles) {\n" +
" #| if (fs.existsSync(file)) fs.watch(file, () => rebuild(file))\n" +
" #| }\n" +
" #| for (const root of watchRoots) {\n" +
" #| if (fs.existsSync(root)) {\n" +
" #| fs.watch(root, { recursive: true }, (_event, filename) => {\n" +
" #| rebuild(filename ? root + \"/\" + filename : root)\n" +
" #| })\n" +
" #| }\n" +
" #| }\n" +
" #| console.log(\"watch=active target=\" + target + \" output=\" + outputDir)\n" +
" #| console.log(\"watching=\" + rootFiles.concat(watchRoots).join(\",\"))\n" +
" #| process.stdin.resume()\n" +
" #| }\n\n" +
"///|\n" +
"#cfg(not(target=\"js\"))\n" +
"fn watch_generated_output(\n" +
" _target : String,\n" +
" _output_dir : String,\n" +
" _strict : Bool,\n" +
") -> Unit {\n" +
" panic()\n" +
"}\n\n" +
"///|\n" +
"#cfg(target=\"js\")\n" +
"extern \"js\" fn write_text_file(path : String, content : String) -> Unit =\n" +
" #| function(path, content) {\n" +
" #| const fs = require(\"fs\")\n" +
" #| const nodePath = require(\"path\")\n" +
" #| fs.mkdirSync(nodePath.dirname(path), { recursive: true })\n" +
" #| fs.writeFileSync(path, content, \"utf8\")\n" +
" #| }\n\n" +
"///|\n" +
"#cfg(not(target=\"js\"))\n" +
"fn write_text_file(_path : String, _content : String) -> Unit {\n" +
" panic()\n" +
"}\n"
}
///|
fn app_mbt(name : String) -> String {
"///|\n" +
"pub fn view() -> @bunnia.Node {\n" +
" @bunnia.page([\n" +
" @bunnia.view_with_attrs([@bunnia.class_name(\"demo-shell\")], [\n" +
" @bunnia.text(\"\{escape_mbt(name)}\"),\n" +
" @bunnia.surface_status_overlay(starter_status(), starter_agent_feed()),\n" +
" @bunnia.static_scene_view(starter_scene()),\n" +
" @bunnia.button(\"Refresh\", \"refresh\"),\n" +
" ]),\n" +
" ])\n" +
"}\n\n" +
"///|\n" +
"fn starter_status() -> @bunnia.SurfaceStatus {\n" +
" @bunnia.surface_stale(\n" +
" label=\"Live context\",\n" +
" detail=\"Refresh keeps the visible feed small while preserving total conversation scale.\",\n" +
" action_message=\"refresh\",\n" +
" )\n" +
"}\n"
}
///|
fn agent_feed_mbt() -> String {
"///|\n" +
"fn starter_agent_feed() -> @bunnia.Node {\n" +
" @bunnia.windowed_message_feed([\n" +
" @bunnia.message(\n" +
" id=\"msg-1\",\n" +
" actor_id=\"agent\",\n" +
" thread_id=\"thread-main\",\n" +
" text=\"Ready for mini-app work.\",\n" +
" status=@bunnia.run_done(),\n" +
" ),\n" +
" ], total_count=1)\n" +
"}\n"
}
///|
fn scene_mbt() -> String {
"///|\n" +
"fn starter_scene() -> @bunnia.Scene {\n" +
" @bunnia.scene(\n" +
" \"starter-map\",\n" +
" @bunnia.scene_size(320, 240),\n" +
" [\n" +
" @bunnia.layer(\"regions\", 1, [], regions=[\n" +
" @bunnia.region(\n" +
" id=\"workbench\",\n" +
" label=\"Workbench\",\n" +
" x=32,\n" +
" y=48,\n" +
" width=180,\n" +
" height=120,\n" +
" status=\"ready\",\n" +
" tap_message=\"select-workbench\",\n" +
" ),\n" +
" ]),\n" +
" @bunnia.layer(\"agents\", 1, [\n" +
" @bunnia.marker(\n" +
" id=\"agent-a\",\n" +
" label=\"Agent A\",\n" +
" x=96,\n" +
" y=120,\n" +
" status=\"ready\",\n" +
" tap_message=\"select-agent-a\",\n" +
" ),\n" +
" ]),\n" +
" ],\n" +
" )\n" +
"}\n\n" +
"///|\n" +
"pub fn scene_render_plan() -> @bunnia.SceneRenderPlan {\n" +
" @bunnia.plan_scene_render(starter_scene())\n" +
"}\n"
}
///|
fn updates_mbt() -> String {
"///|\n" +
"pub fn patch_plan() -> @bunnia.PatchPlan {\n" +
" @bunnia.plan_agent_deltas([\n" +
" @bunnia.append_stream_chunk_delta(\n" +
" \"streams.main.chunks\",\n" +
" \"chunk-1\",\n" +
" \"Refresh requested.\",\n" +
" ),\n" +
" ]).patch_plan\n" +
"}\n\n" +
"///|\n" +
"pub fn scene_patch_plan() -> @bunnia.PatchPlan {\n" +
" @bunnia.plan_patches([\n" +
" @bunnia.select_scene_region(\"starter-map\", \"workbench\"),\n" +
" @bunnia.set_scene_region_status(\"starter-map\", \"workbench\", \"selected\"),\n" +
" @bunnia.select_scene_marker(\"starter-map\", \"agent-a\"),\n" +
" @bunnia.set_scene_marker_status(\"starter-map\", \"agent-a\", \"selected\"),\n" +
" ])\n" +
"}\n"
}
///|
fn quality_mbt() -> String {
"///|\n" +
"pub fn render_plan() -> @bunnia.RenderPlan {\n" +
" @bunnia.plan(view(), @bunnia.wechat())\n" +
"}\n\n" +
"///|\n" +
"pub fn strict_render_plan() -> @bunnia.RenderPlan {\n" +
" @bunnia.plan_with_budget(\n" +
" view(),\n" +
" @bunnia.wechat(),\n" +
" @bunnia.render_budget_for_profile(\"tight\"),\n" +
" )\n" +
"}\n\n" +
"///|\n" +
"pub fn effect_plan() -> @bunnia.EffectPlan {\n" +
" @bunnia.plan_backend_effects_for_platform(\n" +
" starter_backend_contract(),\n" +
" @bunnia.wechat(),\n" +
" )\n" +
"}\n\n" +
"///|\n" +
"pub fn backend_plan() -> @bunnia.BackendPlan {\n" +
" @bunnia.plan_backend_contract(starter_backend_contract())\n" +
"}\n\n" +
"///|\n" +
"pub fn build_profile(project : @bunnia.WechatProject) -> @bunnia.BuildProfile {\n" +
" @bunnia.profile_wechat_project(\n" +
" project,\n" +
" patch_plan(),\n" +
" effect_plans=[effect_plan()],\n" +
" backend_plans=[backend_plan()],\n" +
" scene_plans=[scene_render_plan()],\n" +
" )\n" +
"}\n\n" +
"///|\n" +
"pub fn project_inspection(\n" +
" project : @bunnia.WechatProject,\n" +
") -> @bunnia.ProjectInspection {\n" +
" @bunnia.inspect_wechat_project(\n" +
" target=\"wechat\",\n" +
" example=\"starter\",\n" +
" project,\n" +
" project.report,\n" +
" )\n" +
"}\n\n" +
"///|\n" +
"pub fn inspection_gate(\n" +
" project : @bunnia.WechatProject,\n" +
") -> @bunnia.ProjectInspectionGate {\n" +
" @bunnia.gate_project_inspection(\n" +
" project_inspection(project),\n" +
" @bunnia.inspection_budget_for_profile(\"tight\"),\n" +
" budget_profile=\"tight\",\n" +
" )\n" +
"}\n\n" +
"///|\n" +
"pub fn profile_gate(\n" +
" project : @bunnia.WechatProject,\n" +
") -> @bunnia.BuildProfileGate {\n" +
" @bunnia.gate_build_profile(\n" +
" build_profile(project),\n" +
" @bunnia.build_profile_budget_for_profile(\"tight\"),\n" +
" budget_profile=\"tight\",\n" +
" )\n" +
"}\n\n" +
"///|\n" +
"pub fn release_readiness(\n" +
" project : @bunnia.WechatProject,\n" +
") -> @bunnia.ReleaseReadinessReport {\n" +
" let profile = build_profile(project)\n" +
" let inspection = project_inspection(project)\n" +
" @bunnia.release_readiness_report(\n" +
" target=\"wechat\",\n" +
" example=\"starter\",\n" +
" budget_profile=\"tight\",\n" +
" project,\n" +
" profile,\n" +
" profile_gate(project),\n" +
" inspection,\n" +
" inspection_gate(project),\n" +
" )\n" +
"}\n"
}
///|
fn wechat_mbt(name : String) -> String {
"///|\n" +
"pub fn starter_backend_contract() -> @bunnia.BackendContract {\n" +
" @bunnia.backend_contract(id=\"starter-backend\", endpoints=[\n" +
" @bunnia.agent_message_endpoint(\n" +
" \"refresh\", \"/agent/message\", \"refreshDraft\", \"refreshResult\",\n" +
" ),\n" +
" ])\n" +
"}\n\n" +
"///|\n" +
"pub fn starter_wechat_runtime() -> @bunnia.WechatRuntime {\n" +
" @bunnia.wechat_runtime(\n" +
" initial_data_json=\"{}\",\n" +
" event_patches=[\n" +
" @bunnia.wechat_event_patch(\"refresh\", patch_plan().patches),\n" +
" @bunnia.wechat_event_patch(\"select-workbench\", scene_patch_plan().patches),\n" +
" @bunnia.wechat_event_patch(\"select-agent-a\", scene_patch_plan().patches),\n" +
" ],\n" +
" )\n" +
"}\n\n" +
"///|\n" +
"pub fn starter_wechat_project() -> @bunnia.WechatProject {\n" +
" @bunnia.generate_wechat_project_with_runtime_and_budget_and_backend(\n" +
" name=\"\{escape_mbt(name)}\",\n" +
" route=\"pages/index/index\",\n" +
" root=view(),\n" +
" runtime=starter_wechat_runtime(),\n" +
" render_budget=@bunnia.render_budget_for_profile(\"tight\"),\n" +
" backend_contract=starter_backend_contract(),\n" +
" )\n" +
"}\n\n" +
"///|\n" +
"pub fn starter_wechat_report() -> @bunnia.WechatBuildReport {\n" +
" starter_wechat_project().report\n" +
"}\n"
}
///|
fn app_test_mbt() -> String {
"///|\n" +
"test \"starter render plans stay within tight budgets\" {\n" +
" let plan = strict_render_plan()\n" +
" assert_eq(plan.diagnostics.length(), 0)\n" +
" assert_eq(plan.scene_count, 1)\n" +
" assert_eq(plan.scene_marker_count, 1)\n" +
" assert_eq(plan.scene_region_count, 1)\n" +
" assert_eq(plan.degraded_scene_count, 0)\n" +
" let scene = scene_render_plan()\n" +
" assert_eq(scene.marker_count, 1)\n" +
" assert_eq(scene.visible_marker_count, 1)\n" +
" assert_eq(scene.region_count, 1)\n" +
" assert_eq(scene.visible_region_count, 1)\n" +
" assert_eq(scene.diagnostics.length(), 0)\n" +
"}\n\n" +
"///|\n" +
"test \"starter updates stay bounded\" {\n" +
" let agent = patch_plan()\n" +
" assert_eq(agent.patches.length(), 1)\n" +
" assert_eq(agent.diagnostics.length(), 0)\n" +
" let scene = scene_patch_plan()\n" +
" assert_eq(scene.patches.length(), 4)\n" +
" assert_eq(scene.diagnostics.length(), 0)\n" +
"}\n\n" +
"///|\n" +
"test \"starter generates clean wechat output\" {\n" +
" let project = starter_wechat_project()\n" +
" assert_eq(project.page_count, 1)\n" +
" assert_eq(project.report.diagnostics.length(), 0)\n" +
" assert_eq(project.report.missing_event_patch_count, 0)\n" +
" assert_eq(project.report.orphan_event_patch_count, 0)\n" +
" assert_eq(project.manifest.pages[0].scene_count, 1)\n" +
" assert_eq(project.manifest.pages[0].backend_event_count, 1)\n" +
" assert_eq(backend_plan().endpoint_count, 1)\n" +
" assert_eq(profile_gate(project).diagnostics.length(), 0)\n" +
" assert_eq(inspection_gate(project).diagnostics.length(), 0)\n" +
" assert_eq(release_readiness(project).status, \"ready\")\n" +
" assert_eq(release_readiness(project).diagnostics.length(), 0)\n" +
" assert_true(project.files.length() > 0)\n" +
"}\n"
}
///|
fn readme(name : String) -> String {
"# \{name}\n\n" +
"A Bunnia mini-app starter generated by `bunnia init`.\n\n" +
"The starter keeps app view, agent feed, scene model, bounded updates, backend contract, inspection gates, release readiness, WeChat generation, a local build command, and starter tests in separate files so the app can grow without mixing product code into the framework.\n\n" +
"## Files\n\n" +
"- `app.mbt` owns the page view and keeps product UI platform-neutral.\n" +
"- `agent_feed.mbt` keeps the first agentic surface windowed and keyed.\n" +
"- `scene.mbt` owns the static map scene, region, marker, and render plan.\n" +
"- `updates.mbt` owns bounded patch plans for agent and scene updates.\n" +
"- `quality.mbt` owns render, backend, inspection, profile, and release-readiness gates.\n" +
"- `wechat.mbt` is the only starter app file that generates WeChat output.\n" +
"- `cmd/main/main.mbt` owns build, watch, inspect, snapshot, limits, and CI commands.\n\n" +
"## Workflow\n\n" +
"```bash\n" +
"moon check\n" +
"moon run cmd/main -- help\n" +
"moon test\n" +
"moon info\n" +
"moon fmt\n" +
"git diff --check -- .\n" +
"git diff --exit-code -- .\n" +
"moon run cmd/main -- ci-plan --strict\n" +
"moon run cmd/main -- ci-plan --script --strict\n" +
"sh scripts/ci.sh\n" +
"sh scripts/validate_boundaries.sh\n" +
"# GitHub Actions runs the same script from .github/workflows/ci.yml\n" +
"moon run cmd/main -- limits --target wechat\n" +
"moon run cmd/main -- inspect --target wechat --strict\n" +
"moon run cmd/main -- snapshot --target wechat --snapshot-out _build/snapshots/wechat/starter.snapshot.txt --strict\n" +
"moon run cmd/main -- build --target wechat --out _build/wechat --strict\n" +
"moon run cmd/main -- watch --target wechat --out _build/wechat-watch --once --strict\n" +
"moon run cmd/main -- watch --target wechat --out _build/wechat-watch --strict\n" +
"```\n\n" +
"`inspect` is the no-write route, backend, map, file pressure, and release-readiness report. `snapshot` writes a deterministic review artifact. `watch --once` tests the watch generation path without staying open, while `watch` keeps regenerating WeChat output during local iteration.\n\n" +
"`limits --target wechat` prints the active component mapping and capabilities. `limits --target alipay` or `limits --target tiktok` reports the generic mini-app generator status; generic build, inspect, snapshot, and watch paths are available with release-readiness preflight while deeper vendor-specific hooks remain future adapter work.\n"
}
///|
fn escape_toml(value : String) -> String {
value.replace(old="\\", new="\\\\").replace(old="\"", new="\\\"").to_string()
}
///|
fn escape_mbt(value : String) -> String {
value
.replace(old="\\", new="\\\\")
.replace(old="\"", new="\\\"")
.replace(old="\n", new=" ")
.to_string()
}