@@ -637,6 +637,80 @@ func commitInPlace() (string, error) {
637637 return appName , nil
638638}
639639
640+ // isPreRenderedTemplate returns true when the template directory contains an
641+ // already-materialised project (e.g. one of the app-templates/appkit-* repos)
642+ // rather than a raw Go-template tree.
643+ //
644+ // Detection: there is no {{.project_name}} subdirectory AND the databricks.yml
645+ // file (if present) contains no Go template syntax ("{{").
646+ func isPreRenderedTemplate (templateDir string ) bool {
647+ entries , err := os .ReadDir (templateDir )
648+ if err != nil {
649+ return false
650+ }
651+ for _ , e := range entries {
652+ if e .IsDir () && strings .Contains (e .Name (), "{{.project_name}}" ) {
653+ return false
654+ }
655+ }
656+ data , err := os .ReadFile (filepath .Join (templateDir , "databricks.yml" ))
657+ if err != nil {
658+ // No databricks.yml — can't tell, assume normal template.
659+ return false
660+ }
661+ return ! strings .Contains (string (data ), "{{" )
662+ }
663+
664+ // replaceProjectName updates the project name in key files after copying a
665+ // pre-rendered template. It reads the original bundle name from
666+ // databricks.yml and replaces it with newName in databricks.yml and
667+ // package.json.
668+ func replaceProjectName (destDir , newName string ) error {
669+ // Update package.json name field via JSON round-trip.
670+ pkgPath := filepath .Join (destDir , "package.json" )
671+ if data , err := os .ReadFile (pkgPath ); err == nil {
672+ var pkg map [string ]any
673+ if err := json .Unmarshal (data , & pkg ); err == nil {
674+ pkg ["name" ] = newName
675+ out , err := json .MarshalIndent (pkg , "" , " " )
676+ if err == nil {
677+ // Preserve trailing newline convention.
678+ out = append (out , '\n' )
679+ _ = os .WriteFile (pkgPath , out , 0o644 )
680+ }
681+ }
682+ }
683+
684+ // Update databricks.yml: bundle name and app name.
685+ ymlPath := filepath .Join (destDir , "databricks.yml" )
686+ data , err := os .ReadFile (ymlPath )
687+ if err != nil {
688+ return nil
689+ }
690+ content := string (data )
691+
692+ // Read the original bundle name so we can do a targeted replace of the
693+ // app resource name that usually matches it.
694+ origName := ""
695+ for _ , line := range strings .Split (content , "\n " ) {
696+ trimmed := strings .TrimSpace (line )
697+ if strings .HasPrefix (trimmed , "name:" ) && origName == "" {
698+ origName = strings .TrimSpace (strings .TrimPrefix (trimmed , "name:" ))
699+ origName = strings .Trim (origName , "\" '" )
700+ break
701+ }
702+ }
703+
704+ // Replace the bundle name line (first occurrence of "name:" under "bundle:").
705+ if origName != "" {
706+ content = strings .Replace (content , "name: " + origName , "name: " + newName , 1 )
707+ // Replace the quoted app name in the resources section.
708+ content = strings .Replace (content , "name: \" " + origName + "\" " , "name: \" " + newName + "\" " , 1 )
709+ }
710+
711+ return os .WriteFile (ymlPath , []byte (content ), 0o644 )
712+ }
713+
640714// findProjectSrcDir locates the actual source directory inside a template.
641715// Templates may nest their content inside a {{.project_name}} directory.
642716func findProjectSrcDir (templateDir string ) string {
@@ -981,6 +1055,13 @@ func runCreate(ctx context.Context, opts createOptions) error {
9811055 }
9821056 }
9831057
1058+ // Detect whether this is a pre-rendered template (already-materialised
1059+ // project with no Go template placeholders in databricks.yml).
1060+ preRendered := isPreRenderedTemplate (templateDir )
1061+ if preRendered {
1062+ log .Debugf (ctx , "Detected pre-rendered template at %s" , templateDir )
1063+ }
1064+
9841065 // Start npm install in the background so it runs while the user answers prompts.
9851066 // This is a Node.js-only optimisation — non-Node templates skip this.
9861067 srcProjectDir := findProjectSrcDir (templateDir )
@@ -1010,7 +1091,17 @@ func runCreate(ctx context.Context, opts createOptions) error {
10101091 // Skip deploy/run prompts if in flags mode or if deploy/run flags were explicitly set
10111092 skipDeployRunPrompt := flagsMode || opts .deployChanged || opts .runChanged
10121093
1013- if isInteractive && ! opts .pluginsChanged && ! flagsMode {
1094+ if preRendered {
1095+ // Pre-rendered templates already have their plugins configured.
1096+ // Skip plugin/resource prompting entirely — just use mandatory plugins.
1097+ if isInteractive && ! skipDeployRunPrompt {
1098+ var err error
1099+ shouldDeploy , runMode , err = prompt .PromptForDeployAndRun (ctx )
1100+ if err != nil {
1101+ return err
1102+ }
1103+ }
1104+ } else if isInteractive && ! opts .pluginsChanged && ! flagsMode {
10141105 // Interactive mode without --plugins flag: prompt for plugins, dependencies, description
10151106 config , err := promptForPluginsAndDeps (ctx , m , selectedPlugins , skipDeployRunPrompt , opts .autoApprove )
10161107 if err != nil {
@@ -1068,8 +1159,28 @@ func runCreate(ctx context.Context, opts createOptions) error {
10681159 // Always include mandatory plugins regardless of user selection or flags.
10691160 selectedPlugins = appendUnique (selectedPlugins , m .GetMandatoryPluginNames ()... )
10701161
1162+ // Warn when --features adds plugins that the pre-rendered template
1163+ // cannot inject (they'll appear in .env but not in databricks.yml,
1164+ // server.ts, or app.yaml).
1165+ if preRendered && opts .pluginsChanged {
1166+ mandatoryNames := m .GetMandatoryPluginNames ()
1167+ mandatory := make (map [string ]bool , len (mandatoryNames ))
1168+ for _ , n := range mandatoryNames {
1169+ mandatory [n ] = true
1170+ }
1171+ for _ , p := range opts .plugins {
1172+ if ! mandatory [p ] {
1173+ log .Warnf (ctx , "Feature %q is not supported by this pre-rendered template and will be ignored." +
1174+ " Only .env will include its configuration." +
1175+ " To use all features dynamically, use the default AppKit template instead." , p )
1176+ }
1177+ }
1178+ }
1179+
10711180 // In flags/non-interactive mode, resolve derived values and validate resources.
1072- if flagsMode || ! isInteractive {
1181+ // Skip validation for pre-rendered templates — their resources are already
1182+ // configured in databricks.yml.
1183+ if ! preRendered && (flagsMode || ! isInteractive ) {
10731184 resources := m .CollectResources (selectedPlugins )
10741185
10751186 // Resolve derived values for resources that support it.
@@ -1214,6 +1325,14 @@ func runCreate(ctx context.Context, opts createOptions) error {
12141325 }
12151326 projectCreated = true // From here on, cleanup on failure
12161327
1328+ // For pre-rendered templates, update the project name in key files since
1329+ // Go template substitution had no placeholders to fill.
1330+ if preRendered {
1331+ if err := replaceProjectName (destDir , opts .name ); err != nil {
1332+ log .Warnf (ctx , "Could not update project name in output files: %v" , err )
1333+ }
1334+ }
1335+
12171336 // Get absolute path
12181337 absOutputDir , err := filepath .Abs (destDir )
12191338 if err != nil {
0 commit comments