From 8be0bda802013874bb0eda57824f9f950f24cbb2 Mon Sep 17 00:00:00 2001 From: Sheng Kun Chang Date: Mon, 4 May 2026 17:53:21 +0800 Subject: [PATCH 1/3] feat(module): scaffold local source tree on init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `mirrorstack module init` now does both halves of "create a module": register on the platform AND scaffold a local Go source tree from the SDK template. Without this, every developer's first `mirrorstack dev` (future) would have nothing to launch. Two new flags: - --no-scaffold: register only, skip filesystem (CI / re-registration) - --dir : override default .// (use --dir . for cwd) The scaffold target's writability is checked before any remote POST so we never leave a registered module without a local tree because of e.g. a non-empty target dir. Each file is written via OpenOptions::create_new so a file slipped in during the API round-trip surfaces as an error instead of a silent overwrite. Templates are vendored under templates/module/ and embedded via include_str!. Source of truth for the shape is app-module-sdk's examples/template/; vendored copies must be re-synced when the SDK changes shape (no automatic mirror yet). go.mod.tmpl pins github.com/mirrorstack-ai/app-module-sdk v0.1.0 — the SDK's first published tag — so scaffolded modules `go mod tidy` cleanly from anywhere on disk, no replace directive needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/commands/{module.rs => module/mod.rs} | 184 +++++++++++----- src/commands/module/scaffold.rs | 197 ++++++++++++++++++ templates/module/go.mod.tmpl | 8 + templates/module/main.go.tmpl | 51 +++++ templates/module/mcp.go.tmpl | 48 +++++ templates/module/routes.go.tmpl | 35 ++++ .../module/sql/app/0001_init.up.sql.tmpl | 13 ++ 7 files changed, 485 insertions(+), 51 deletions(-) rename src/commands/{module.rs => module/mod.rs} (66%) create mode 100644 src/commands/module/scaffold.rs create mode 100644 templates/module/go.mod.tmpl create mode 100644 templates/module/main.go.tmpl create mode 100644 templates/module/mcp.go.tmpl create mode 100644 templates/module/routes.go.tmpl create mode 100644 templates/module/sql/app/0001_init.up.sql.tmpl diff --git a/src/commands/module.rs b/src/commands/module/mod.rs similarity index 66% rename from src/commands/module.rs rename to src/commands/module/mod.rs index 0bdaa7d..59218e4 100644 --- a/src/commands/module.rs +++ b/src/commands/module/mod.rs @@ -1,15 +1,14 @@ //! `mirrorstack module …` — developer module management. //! -//! Today: only `module init` (register a module on the platform). Filesystem -//! scaffolding (templates, SDK pull) is intentionally out of scope per the -//! current product direction — modules are developed locally with whatever -//! tooling the developer prefers, and `mirrorstack dev` (future) opens a WSS -//! tunnel into the platform. +//! Today: `module init` registers the module on the platform AND scaffolds a +//! local source tree from the SDK template. The scaffolded tree is what +//! `mirrorstack dev` (future) launches and tunnels into the platform. use std::io::IsTerminal; +use std::path::{Path, PathBuf}; use std::time::Duration; -use anyhow::{Result, anyhow}; +use anyhow::{Context, Result, anyhow}; use clap::{Args, Subcommand}; use console::style; use dialoguer::{Confirm, Input, theme::ColorfulTheme}; @@ -19,6 +18,8 @@ use crate::api::{self, ApiError, CreateModuleInput}; use crate::credentials; use crate::http; +mod scaffold; + use super::{ DEFAULT_API_BASE, DEFAULT_APPS_API_BASE, DEFAULT_WEB_BASE, ENV_API_URL, ENV_APPS_API_URL, ENV_WEB_URL, ok_mark, resolve_base, @@ -54,6 +55,13 @@ pub struct InitArgs { /// continue. Useful for re-running init in CI without conditional logic. #[arg(long)] used: bool, + /// Skip filesystem scaffolding. Only register the module on the platform. + #[arg(long)] + no_scaffold: bool, + /// Where to scaffold the new module source tree. Defaults to .// + /// in the current directory. Pass `.` to scaffold into the cwd directly. + #[arg(long)] + dir: Option, } pub fn run(args: ModuleArgs) -> Result<()> { @@ -93,75 +101,135 @@ fn init(args: InitArgs) -> Result<()> { )); } + // Pre-flight: scaffold target. If the caller wants scaffolding, fail now + // (before any remote POST) so we never leave a registered module with no + // local tree because of e.g. a non-empty target dir. + let scaffold_target = if args.no_scaffold { + None + } else { + let target = resolve_scaffold_target(args.dir.as_deref(), &slug); + scaffold::ensure_target_writable(&target)?; + Some(target) + }; + // Pre-flight: reject early if the caller already owns this slug. Catches // the common "I forgot I made this last week" case before we POST. // Reserved/invalid still surface server-side from the POST below. let pre_check = with_spinner("Checking availability…", || { api::get_module(&client, &apps_base, &creds.access_token, &slug) }); - match pre_check { + // Determine whether we still need to POST. `--used` + already-exists + // short-circuits without prompting for confirmation again. + let needs_create = match pre_check { Ok(Some(existing)) if args.used => { print_already_exists(username, &existing.slug, Some(&existing.id)); - return Ok(()); + false } Ok(Some(_)) => { return Err(anyhow!( "@{username}/{slug} already exists (pass --used to ignore when re-running)" )); } - Ok(None) => {} + Ok(None) => true, Err(ApiError::Unauthenticated) => return Err(session_expired()), Err(e) => return Err(e.into()), - } + }; - if !args.yes { - eprintln!(); - eprintln!(" {} {}", style("Module:").dim(), style(&name).bold()); - eprintln!( - " {} {}", - style("Slug:").dim(), - style(format!("@{username}/{slug}")).cyan().bold() - ); - let confirmed = Confirm::with_theme(&theme) - .with_prompt("Create this module?") - .default(true) - .interact()?; - if !confirmed { - eprintln!("{}", style("aborted.").yellow()); - return Ok(()); + if needs_create { + if !args.yes { + eprintln!(); + eprintln!(" {} {}", style("Module:").dim(), style(&name).bold()); + eprintln!( + " {} {}", + style("Slug:").dim(), + style(format!("@{username}/{slug}")).cyan().bold() + ); + let confirmed = Confirm::with_theme(&theme) + .with_prompt("Create this module?") + .default(true) + .interact()?; + if !confirmed { + eprintln!("{}", style("aborted.").yellow()); + return Ok(()); + } + } + + let create_result = with_spinner("Creating module…", || { + api::create_module( + &client, + &apps_base, + &creds.access_token, + &CreateModuleInput { + name: &name, + slug: &slug, + }, + ) + }); + + match create_result { + Ok(m) => print_created(username, &m.slug, &m.id), + Err(ApiError::Server { code, .. }) if code == "slug_taken" && args.used => { + print_already_exists(username, &slug, None); + } + Err(ApiError::Server { code, message, .. }) => { + return Err(anyhow!( + "{code}: {message}{hint}", + hint = slug_error_hint(&code) + )); + } + Err(ApiError::Unauthenticated) => return Err(session_expired()), + Err(e) => return Err(e.into()), } } - let create_result = with_spinner("Creating module…", || { - api::create_module( - &client, - &apps_base, - &creds.access_token, - &CreateModuleInput { - name: &name, - slug: &slug, - }, - ) - }); + scaffold_if_requested( + scaffold_target.as_deref(), + &scaffold::Inputs { + slug: &slug, + name: &name, + }, + ) +} - match create_result { - Ok(m) => { - print_created(username, &m.slug, &m.id); - Ok(()) - } - Err(ApiError::Server { code, .. }) if code == "slug_taken" && args.used => { - print_already_exists(username, &slug, None); - Ok(()) - } - Err(ApiError::Server { code, message, .. }) => Err(anyhow!( - "{code}: {message}{hint}", - hint = slug_error_hint(&code) - )), - Err(ApiError::Unauthenticated) => Err(session_expired()), - Err(e) => Err(e.into()), +/// Resolve the target dir for scaffolding. `--dir ` wins; otherwise +/// default to `.//`. Pass `--dir .` to scaffold into the cwd directly. +fn resolve_scaffold_target(dir: Option<&Path>, slug: &str) -> PathBuf { + match dir { + Some(d) => d.to_path_buf(), + None => PathBuf::from(slug), } } +fn scaffold_if_requested(target: Option<&Path>, inputs: &scaffold::Inputs<'_>) -> Result<()> { + let Some(target) = target else { return Ok(()) }; + scaffold::write_tree(target, inputs) + .with_context(|| format!("scaffold into {}", target.display()))?; + print_scaffold_summary(target); + Ok(()) +} + +fn print_scaffold_summary(target: &Path) { + eprintln!( + "{} scaffolded {}", + ok_mark(), + style(target.display()).cyan().bold() + ); + let next = if is_cwd(target) { + "go mod tidy && mirrorstack dev".to_string() + } else { + format!("cd {} && go mod tidy && mirrorstack dev", target.display()) + }; + eprintln!(" {} {next}", style("next:").dim()); +} + +/// Robust check for "scaffold into the current dir." Catches both `.` and +/// `./` (and other normalizations of the same path) — bare `target.as_os_str() +/// == "."` would miss `./` and trailing-slash variants. +fn is_cwd(target: &Path) -> bool { + let mut comps = target.components(); + matches!(comps.next(), Some(std::path::Component::CurDir)) && comps.next().is_none() +} + fn session_expired() -> anyhow::Error { anyhow!("session expired. Run `mirrorstack login` to sign in again.") } @@ -352,4 +420,18 @@ mod tests { fn slug_error_hint_for_taken() { assert!(slug_error_hint("slug_taken").contains("--used")); } + + #[test] + fn is_cwd_matches_cwd_variants() { + assert!(is_cwd(Path::new("."))); + assert!(is_cwd(Path::new("./"))); + } + + #[test] + fn is_cwd_rejects_non_cwd_paths() { + assert!(!is_cwd(Path::new("media"))); + assert!(!is_cwd(Path::new("./media"))); + assert!(!is_cwd(Path::new("a/b"))); + assert!(!is_cwd(Path::new("/tmp"))); + } } diff --git a/src/commands/module/scaffold.rs b/src/commands/module/scaffold.rs new file mode 100644 index 0000000..4e23bab --- /dev/null +++ b/src/commands/module/scaffold.rs @@ -0,0 +1,197 @@ +//! Scaffold a new local module source tree from the embedded template. +//! +//! Templates are vendored under `templates/module/` and embedded at build +//! time via `include_str!`. Substitution is straight string replace using +//! `__MS_*__` markers — no template engine, no Go-template syntax conflicts. +//! +//! Source of truth for the template shape is the SDK's +//! `examples/template/`. When the SDK changes shape, the vendored copy here +//! must be re-synced — there is no automatic mirroring yet. + +use std::fs; +use std::io::Write; +use std::path::Path; + +use anyhow::{Context, Result, anyhow}; + +const MAIN_GO: &str = include_str!("../../../templates/module/main.go.tmpl"); +const MCP_GO: &str = include_str!("../../../templates/module/mcp.go.tmpl"); +const ROUTES_GO: &str = include_str!("../../../templates/module/routes.go.tmpl"); +const GO_MOD: &str = include_str!("../../../templates/module/go.mod.tmpl"); +const SQL_INIT: &str = + include_str!("../../../templates/module/sql/app/0001_init.up.sql.tmpl"); + +// Placeholder tokens. Kept in one place so the contract between the .tmpl +// files and this renderer is auditable from a single grep. +const SLUG_TOKEN: &str = "__MS_SLUG__"; +const NAME_TOKEN: &str = "__MS_NAME__"; +const TABLE_PREFIX_TOKEN: &str = "__MS_TABLE_PREFIX__"; + +/// Inputs collected by the caller before scaffolding. +pub(super) struct Inputs<'a> { + pub slug: &'a str, + pub name: &'a str, +} + +/// Refuse if `target` already contains files. Empty dir or missing dir = OK. +pub(super) fn ensure_target_writable(target: &Path) -> Result<()> { + match fs::read_dir(target) { + Ok(mut entries) => { + if entries.next().is_some() { + return Err(anyhow!( + "{} is not empty — pick a different --dir or remove it", + target.display() + )); + } + Ok(()) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(anyhow!("scaffold: read {}: {e}", target.display())), + } +} + +pub(super) fn write_tree(target: &Path, inputs: &Inputs<'_>) -> Result<()> { + fs::create_dir_all(target.join("sql/app")) + .with_context(|| format!("scaffold: mkdir {}/sql/app", target.display()))?; + + write_file(target, "main.go", MAIN_GO, inputs)?; + write_file(target, "mcp.go", MCP_GO, inputs)?; + write_file(target, "routes.go", ROUTES_GO, inputs)?; + write_file(target, "go.mod", GO_MOD, inputs)?; + write_file(target, "sql/app/0001_init.up.sql", SQL_INIT, inputs)?; + Ok(()) +} + +/// Write a rendered template using `create_new` semantics so a file racing +/// in between `ensure_target_writable` and here surfaces as an error rather +/// than a silent overwrite. The caller's own `--dir` is the source of truth +/// for what's safe to write — anything that appeared after we checked is +/// not ours to clobber. +fn write_file(target: &Path, rel: &str, body: &str, inputs: &Inputs<'_>) -> Result<()> { + let path = target.join(rel); + let rendered = render(body, inputs); + let mut f = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .with_context(|| format!("scaffold: create {}", path.display()))?; + f.write_all(rendered.as_bytes()) + .with_context(|| format!("scaffold: write {}", path.display()))?; + Ok(()) +} + +fn render(body: &str, inputs: &Inputs<'_>) -> String { + body.replace(SLUG_TOKEN, inputs.slug) + .replace(NAME_TOKEN, inputs.name) + .replace(TABLE_PREFIX_TOKEN, &table_prefix(inputs.slug)) +} + +/// Convert a module slug (`my-mod`) into a valid Postgres identifier prefix +/// (`my_mod`). The SQL template appends `_items` etc. — caller controls the +/// suffix. +fn table_prefix(slug: &str) -> String { + slug.replace('-', "_") +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ins<'a>(slug: &'a str, name: &'a str) -> Inputs<'a> { + Inputs { slug, name } + } + + #[test] + fn render_substitutes_slug_and_name() { + let out = render(MAIN_GO, &ins("my-mod", "My Mod")); + assert!(out.contains(r#"ID: "my-mod""#)); + assert!(out.contains(r#"Name: "My Mod""#)); + assert!(!out.contains("__MS_SLUG__")); + assert!(!out.contains("__MS_NAME__")); + } + + #[test] + fn render_substitutes_in_go_mod() { + let out = render(GO_MOD, &ins("media", "Media")); + assert!(out.starts_with("module media\n")); + assert!(!out.contains("__MS_SLUG__")); + } + + #[test] + fn table_prefix_replaces_hyphens() { + assert_eq!(table_prefix("media"), "media"); + assert_eq!(table_prefix("my-mod"), "my_mod"); + assert_eq!(table_prefix("a-b-c"), "a_b_c"); + } + + #[test] + fn render_sql_uses_table_prefix() { + let out = render(SQL_INIT, &ins("my-mod", "My Mod")); + assert!(out.contains("CREATE TABLE IF NOT EXISTS my_mod_items")); + assert!(!out.contains("__MS_TABLE_PREFIX__")); + } + + #[test] + fn render_routes_substitutes_slug_in_response_body() { + let out = render(ROUTES_GO, &ins("media", "Media")); + assert!(out.contains(r#""hello from media""#)); + } + + #[test] + fn ensure_target_writable_accepts_missing_dir() { + let tmp = tempfile::tempdir().unwrap(); + let missing = tmp.path().join("does-not-exist"); + ensure_target_writable(&missing).unwrap(); + } + + #[test] + fn ensure_target_writable_accepts_empty_dir() { + let tmp = tempfile::tempdir().unwrap(); + ensure_target_writable(tmp.path()).unwrap(); + } + + #[test] + fn ensure_target_writable_rejects_non_empty_dir() { + let tmp = tempfile::tempdir().unwrap(); + fs::write(tmp.path().join("anything"), b"x").unwrap(); + let err = ensure_target_writable(tmp.path()).unwrap_err(); + assert!(err.to_string().contains("not empty")); + } + + #[test] + fn write_tree_creates_expected_files() { + let tmp = tempfile::tempdir().unwrap(); + let target = tmp.path().join("media"); + write_tree(&target, &ins("media", "Media")).unwrap(); + for rel in [ + "main.go", + "mcp.go", + "routes.go", + "go.mod", + "sql/app/0001_init.up.sql", + ] { + let p = target.join(rel); + assert!(p.exists(), "missing {}", p.display()); + } + let main = fs::read_to_string(target.join("main.go")).unwrap(); + assert!(main.contains(r#"ID: "media""#)); + } + + #[test] + fn write_tree_refuses_to_clobber_a_racing_file() { + // Mirrors the TOCTOU window between ensure_target_writable and + // write_tree: a file slipped in after the empty-dir check should + // surface as an error, not be silently overwritten. + let tmp = tempfile::tempdir().unwrap(); + let target = tmp.path().to_path_buf(); + fs::create_dir_all(target.join("sql/app")).unwrap(); + fs::write(target.join("main.go"), b"do not clobber").unwrap(); + let err = write_tree(&target, &ins("media", "Media")).unwrap_err(); + assert!(err.to_string().contains("create")); + // Original file still intact. + assert_eq!( + fs::read_to_string(target.join("main.go")).unwrap(), + "do not clobber" + ); + } +} diff --git a/templates/module/go.mod.tmpl b/templates/module/go.mod.tmpl new file mode 100644 index 0000000..f00bb2a --- /dev/null +++ b/templates/module/go.mod.tmpl @@ -0,0 +1,8 @@ +module __MS_SLUG__ + +go 1.26 + +require ( + github.com/go-chi/chi/v5 v5.2.5 + github.com/mirrorstack-ai/app-module-sdk v0.1.0 +) diff --git a/templates/module/main.go.tmpl b/templates/module/main.go.tmpl new file mode 100644 index 0000000..b8f86c3 --- /dev/null +++ b/templates/module/main.go.tmpl @@ -0,0 +1,51 @@ +// Package main is a MirrorStack module. All features live in sibling files +// which register themselves via postInitHooks — main.go never changes when +// the CLI adds or removes features. +package main + +import ( + "embed" + "log" + + ms "github.com/mirrorstack-ai/app-module-sdk" + "github.com/mirrorstack-ai/app-module-sdk/system" +) + +//go:embed sql/* +var sqlFS embed.FS + +// postInitHooks is the extension point every feature file registers into via +// its own init(). Hooks run after ms.Init (so the default module exists) and +// before ms.Start (so registrations complete before routes mount). +// +// This indirection is the reason the CLI can drop any feature file without +// editing main.go. +var postInitHooks []func() + +func main() { + if err := ms.Init(ms.Config{ + ID: "__MS_SLUG__", + Name: "__MS_NAME__", + Icon: "extension", + SQL: sqlFS, + Versions: map[string]system.MigrationVersions{ + "v0.1.0": {App: "0001"}, + }, + }); err != nil { + log.Fatalf("mirrorstack: init failed: %v", err) + } + + ms.Describe("Replace this description with your module's purpose.") + + // Required deps go here — these become install-time preconditions. + // + // ms.DependsOn("oauth-core") + + for _, hook := range postInitHooks { + hook() + } + + if err := ms.Start(); err != nil { + log.Fatalf("mirrorstack: start failed: %v", err) + } +} diff --git a/templates/module/mcp.go.tmpl b/templates/module/mcp.go.tmpl new file mode 100644 index 0000000..31a453e --- /dev/null +++ b/templates/module/mcp.go.tmpl @@ -0,0 +1,48 @@ +package main + +// Agent-facing MCP surface. Always present — the MirrorStack agentic CMS story +// assumes every module exposes at least one tool or resource so agents can +// reason about it. + +import ( + "context" + + ms "github.com/mirrorstack-ai/app-module-sdk" +) + +func init() { + postInitHooks = append(postInitHooks, registerMCP) +} + +// GreetArgs are decoded from the agent's tool call and validated against the +// JSON Schema derived from this struct. +type GreetArgs struct { + Name string `json:"name" jsonschema:"description=Name of the person to greet"` +} + +// GreetResult is serialized back to the agent. +type GreetResult struct { + Message string `json:"message"` +} + +// StatusResource is an MCPResource example — an agent-readable snapshot. +type StatusResource struct { + Healthy bool `json:"healthy"` + Version string `json:"version"` +} + +// moduleVersion should be set at build time via -ldflags, or wire it to your +// own embed/constant before shipping. +const moduleVersion = "0.0.0-unknown" + +func registerMCP() { + ms.MCPTool("greet", "Say hi to someone", + func(ctx context.Context, args GreetArgs) (GreetResult, error) { + return GreetResult{Message: "hi " + args.Name}, nil + }) + + ms.MCPResource("status", "Current module health + version", + func(ctx context.Context) (StatusResource, error) { + return StatusResource{Healthy: true, Version: moduleVersion}, nil + }) +} diff --git a/templates/module/routes.go.tmpl b/templates/module/routes.go.tmpl new file mode 100644 index 0000000..1298bae --- /dev/null +++ b/templates/module/routes.go.tmpl @@ -0,0 +1,35 @@ +package main + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + ms "github.com/mirrorstack-ai/app-module-sdk" +) + +func init() { + postInitHooks = append(postInitHooks, registerRoutes) +} + +func registerRoutes() { + // Platform scope — authenticated platform users (host dashboard). + ms.Platform(func(r chi.Router) { + r.Get("/hello", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"message":"hello from __MS_SLUG__"}`)) + }) + }) + + // Public scope — anonymous HTTP endpoints (webhooks, OAuth callbacks). + ms.Public(func(r chi.Router) { + r.Get("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + }) + + // Internal scope — platform-to-module only (HMAC-signed). + ms.Internal(func(r chi.Router) { + r.Get("/debug", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"ok":true}`)) + }) + }) +} diff --git a/templates/module/sql/app/0001_init.up.sql.tmpl b/templates/module/sql/app/0001_init.up.sql.tmpl new file mode 100644 index 0000000..f342df9 --- /dev/null +++ b/templates/module/sql/app/0001_init.up.sql.tmpl @@ -0,0 +1,13 @@ +-- App-scoped migrations run per-app-tenant. The platform applies them via the +-- lifecycle install/upgrade routes. Rows are isolated per app by the DB role. +-- +-- CONVENTION: every table name MUST start with your module ID + underscore. +-- This prevents collisions when multiple modules share the same app_ schema. +-- +-- Replace this example with your module's real schema. + +CREATE TABLE IF NOT EXISTS __MS_TABLE_PREFIX___items ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +); From 325c0695bea8fdf0d2cdd9e2383b09eedbfb7456 Mon Sep 17 00:00:00 2001 From: Sheng Kun Chang Date: Mon, 4 May 2026 17:54:10 +0800 Subject: [PATCH 2/3] chore: ignore local mirrorstack symlink Used by devs as a convenience pointer to target/release/mirrorstack so they can run ./mirrorstack from the repo root without typing the full target path. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 816f885..be65833 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ dist/ .env .plan/ + +# Local convenience symlink to the release binary (created by +# `ln -sf target/release/mirrorstack mirrorstack`). +/mirrorstack From f41e39e6efe611f8191e378c6c35e19690de03b8 Mon Sep 17 00:00:00 2001 From: Sheng Kun Chang Date: Mon, 4 May 2026 23:59:03 +0800 Subject: [PATCH 3/3] chore: cargo fmt CI fmt --check failed on the SQL_INIT include_str! line; rustfmt prefers the long form on a single line. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/commands/module/scaffold.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/module/scaffold.rs b/src/commands/module/scaffold.rs index 4e23bab..70656ac 100644 --- a/src/commands/module/scaffold.rs +++ b/src/commands/module/scaffold.rs @@ -18,8 +18,7 @@ const MAIN_GO: &str = include_str!("../../../templates/module/main.go.tmpl"); const MCP_GO: &str = include_str!("../../../templates/module/mcp.go.tmpl"); const ROUTES_GO: &str = include_str!("../../../templates/module/routes.go.tmpl"); const GO_MOD: &str = include_str!("../../../templates/module/go.mod.tmpl"); -const SQL_INIT: &str = - include_str!("../../../templates/module/sql/app/0001_init.up.sql.tmpl"); +const SQL_INIT: &str = include_str!("../../../templates/module/sql/app/0001_init.up.sql.tmpl"); // Placeholder tokens. Kept in one place so the contract between the .tmpl // files and this renderer is auditable from a single grep.