diff --git a/experimental/apps-mcp/cmd/init_template.go b/experimental/apps-mcp/cmd/init_template.go index 538fa54611..92a4b2c8ba 100644 --- a/experimental/apps-mcp/cmd/init_template.go +++ b/experimental/apps-mcp/cmd/init_template.go @@ -7,6 +7,8 @@ import ( "fmt" "os" "path/filepath" + "sort" + "strings" "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/experimental/apps-mcp/lib/common" @@ -69,98 +71,150 @@ func readClaudeMd(ctx context.Context, configFile string) { cmdio.LogString(ctx, "=================\n") } -func newInitTemplateCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "init-template [TEMPLATE_PATH]", - Short: "Initialize using a bundle template", - Args: root.MaximumNArgs(1), - Long: fmt.Sprintf(`Initialize using a bundle template to get started quickly. +// generateFileTree creates a tree-style visualization of the file structure. +// Collapses directories with more than 10 files to avoid clutter. +func generateFileTree(outputDir string) (string, error) { + const maxFilesToShow = 10 + + // collect all files in the output directory + var allFiles []string + err := filepath.Walk(outputDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + relPath, err := filepath.Rel(outputDir, path) + if err != nil { + return err + } + allFiles = append(allFiles, filepath.ToSlash(relPath)) + } + return nil + }) + if err != nil { + return "", err + } -TEMPLATE_PATH optionally specifies which template to use. It can be one of the following: -%s -- a local file system path with a template directory -- a Git repository URL, e.g. https://github.com/my/repository + // build a tree structure + tree := make(map[string][]string) -Supports the same options as 'databricks bundle init' plus: - --describe: Display template schema without materializing - --config_json: Provide config as JSON string instead of file + for _, relPath := range allFiles { + parts := strings.Split(relPath, "/") + + if len(parts) == 1 { + // root level file + tree[""] = append(tree[""], parts[0]) + } else { + // file in subdirectory + dir := strings.Join(parts[:len(parts)-1], "/") + fileName := parts[len(parts)-1] + tree[dir] = append(tree[dir], fileName) + } + } + + // format as tree + var output strings.Builder + var sortedDirs []string + for dir := range tree { + sortedDirs = append(sortedDirs, dir) + } + sort.Strings(sortedDirs) + + for _, dir := range sortedDirs { + filesInDir := tree[dir] + if dir == "" { + // root files - always show all + for _, file := range filesInDir { + output.WriteString(file) + output.WriteString("\n") + } + } else { + // directory + output.WriteString(dir) + output.WriteString("/\n") + if len(filesInDir) <= maxFilesToShow { + // show all files + for _, file := range filesInDir { + output.WriteString(" ") + output.WriteString(file) + output.WriteString("\n") + } + } else { + // collapse large directories + output.WriteString(fmt.Sprintf(" (%d files)\n", len(filesInDir))) + } + } + } + + return output.String(), nil +} + +const ( + defaultTemplateRepo = "https://github.com/databricks/cli" + defaultTemplateDir = "experimental/apps-mcp/templates/appkit" + defaultBranch = "main" + templatePathEnvVar = "DATABRICKS_APPKIT_TEMPLATE_PATH" +) + +func newInitTemplateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "init-template", + Short: "Initialize a Databricks App using the appkit template", + Args: cobra.NoArgs, + Long: `Initialize a Databricks App using the appkit template. Examples: - experimental apps-mcp tools init-template # Choose from built-in templates - experimental apps-mcp tools init-template default-python # Python jobs and notebooks - experimental apps-mcp tools init-template --output-dir ./my-project - experimental apps-mcp tools init-template default-python --describe - experimental apps-mcp tools init-template default-python --config_json '{"project_name":"my-app"}' + experimental apps-mcp tools init-template --name my-app + experimental apps-mcp tools init-template --name my-app --warehouse abc123 + experimental apps-mcp tools init-template --name my-app --description "My cool app" + experimental apps-mcp tools init-template --name my-app --output-dir ./projects + +Environment variables: + DATABRICKS_APPKIT_TEMPLATE_PATH Override template source with local path (for development) After initialization: databricks bundle deploy --target dev - -See https://docs.databricks.com/en/dev-tools/bundles/templates.html for more information on templates.`, template.HelpDescriptions()), +`, } - var configFile string + var name string + var warehouse string + var description string var outputDir string - var templateDir string - var tag string - var branch string - var configJSON string var describe bool - cmd.Flags().StringVar(&configFile, "config-file", "", "JSON file containing key value pairs of input parameters required for template initialization.") - cmd.Flags().StringVar(&templateDir, "template-dir", "", "Directory path within a Git repository containing the template.") - cmd.Flags().StringVar(&outputDir, "output-dir", "", "Directory to write the initialized template to.") - cmd.Flags().StringVar(&branch, "tag", "", "Git tag to use for template initialization") - cmd.Flags().StringVar(&tag, "branch", "", "Git branch to use for template initialization") - cmd.Flags().StringVar(&configJSON, "config-json", "", "JSON string containing key value pairs (alternative to --config-file).") + cmd.Flags().StringVar(&name, "name", "", "Project name (required)") + cmd.Flags().StringVar(&warehouse, "warehouse", "", "SQL warehouse ID") + cmd.Flags().StringVar(&description, "description", "", "App description") + cmd.Flags().StringVar(&outputDir, "output-dir", "", "Directory to write the initialized template to") cmd.Flags().BoolVar(&describe, "describe", false, "Display template schema without initializing") cmd.PreRunE = root.MustWorkspaceClient cmd.RunE = func(cmd *cobra.Command, args []string) error { - if tag != "" && branch != "" { - return errors.New("only one of --tag or --branch can be specified") - } - - if configFile != "" && configJSON != "" { - return errors.New("only one of --config-file or --config-json can be specified") - } + ctx := cmd.Context() - if configFile != "" { - if configBytes, err := os.ReadFile(configFile); err == nil { - var userConfigMap map[string]any - if err := json.Unmarshal(configBytes, &userConfigMap); err == nil { - if projectName, ok := userConfigMap["project_name"].(string); ok { - if err := validateAppNameLength(projectName); err != nil { - return err - } - } - } - } - } + // Resolve template source: env var override or default remote + templatePathOrUrl := os.Getenv(templatePathEnvVar) + templateDir := "" + branch := "" - var templatePathOrUrl string - if len(args) > 0 { - templatePathOrUrl = args[0] + if templatePathOrUrl == "" { + templatePathOrUrl = defaultTemplateRepo + templateDir = defaultTemplateDir + branch = defaultBranch } - ctx := cmd.Context() - - // NEW: Describe mode - show schema only + // Describe mode - show schema only if describe { r := template.Resolver{ TemplatePathOrUrl: templatePathOrUrl, ConfigFile: "", OutputDir: outputDir, TemplateDir: templateDir, - Tag: tag, Branch: branch, } tmpl, err := r.Resolve(ctx) - if errors.Is(err, template.ErrCustomSelected) { - cmdio.LogString(ctx, "Please specify a path or Git repository to use a custom template.") - cmdio.LogString(ctx, "See https://docs.databricks.com/en/dev-tools/bundles/templates.html to learn more about custom templates.") - return nil - } if err != nil { return err } @@ -179,55 +233,55 @@ See https://docs.databricks.com/en/dev-tools/bundles/templates.html for more inf return nil } - if configJSON != "" { - var userConfigMap map[string]any - if err := json.Unmarshal([]byte(configJSON), &userConfigMap); err != nil { - return fmt.Errorf("invalid JSON in --config-json: %w", err) - } + // Validate required flag + if name == "" { + return errors.New("--name is required") + } - // Validate app name length - if projectName, ok := userConfigMap["project_name"].(string); ok { - if err := validateAppNameLength(projectName); err != nil { - return err - } - } + if err := validateAppNameLength(name); err != nil { + return err + } - tmpFile, err := os.CreateTemp("", "mcp-template-config-*.json") - if err != nil { - return fmt.Errorf("create temp config file: %w", err) - } - defer os.Remove(tmpFile.Name()) + // Build config map from flags + configMap := map[string]any{ + "project_name": name, + } + if warehouse != "" { + configMap["sql_warehouse_id"] = warehouse + } + if description != "" { + configMap["app_description"] = description + } - configBytes, err := json.Marshal(userConfigMap) - if err != nil { - return fmt.Errorf("marshal config: %w", err) - } - if _, err := tmpFile.Write(configBytes); err != nil { - return fmt.Errorf("write config file: %w", err) - } - if err := tmpFile.Close(); err != nil { - return fmt.Errorf("close config file: %w", err) - } + // Write config to temp file + tmpFile, err := os.CreateTemp("", "mcp-template-config-*.json") + if err != nil { + return fmt.Errorf("create temp config file: %w", err) + } + defer os.Remove(tmpFile.Name()) - configFile = tmpFile.Name() + configBytes, err := json.Marshal(configMap) + if err != nil { + return fmt.Errorf("marshal config: %w", err) + } + if _, err := tmpFile.Write(configBytes); err != nil { + return fmt.Errorf("write config file: %w", err) } + if err := tmpFile.Close(); err != nil { + return fmt.Errorf("close config file: %w", err) + } + + configFile := tmpFile.Name() - // Standard materialize flow (identical to bundle/init.go) r := template.Resolver{ TemplatePathOrUrl: templatePathOrUrl, ConfigFile: configFile, OutputDir: outputDir, TemplateDir: templateDir, - Tag: tag, Branch: branch, } tmpl, err := r.Resolve(ctx) - if errors.Is(err, template.ErrCustomSelected) { - cmdio.LogString(ctx, "Please specify a path or Git repository to use a custom template.") - cmdio.LogString(ctx, "See https://docs.databricks.com/en/dev-tools/bundles/templates.html to learn more about custom templates.") - return nil - } if err != nil { return err } @@ -239,18 +293,15 @@ See https://docs.databricks.com/en/dev-tools/bundles/templates.html for more inf } tmpl.Writer.LogTelemetry(ctx) - // Show branded success message - templateName := "bundle" - if templatePathOrUrl != "" { - templateName = filepath.Base(templatePathOrUrl) - } - outputPath := outputDir - if outputPath == "" { - outputPath = "." + // Determine actual output directory (template writes to subdirectory with project name) + actualOutputDir := outputDir + if actualOutputDir == "" { + actualOutputDir = name } + // Count files if we can fileCount := 0 - if absPath, err := filepath.Abs(outputPath); err == nil { + if absPath, err := filepath.Abs(actualOutputDir); err == nil { _ = filepath.Walk(absPath, func(path string, info os.FileInfo, err error) error { if err == nil && !info.IsDir() { fileCount++ @@ -258,7 +309,14 @@ See https://docs.databricks.com/en/dev-tools/bundles/templates.html for more inf return nil }) } - cmdio.LogString(ctx, common.FormatScaffoldSuccess(templateName, outputPath, fileCount)) + cmdio.LogString(ctx, common.FormatScaffoldSuccess("appkit", actualOutputDir, fileCount)) + + // Generate and print file tree structure + fileTree, err := generateFileTree(actualOutputDir) + if err == nil && fileTree != "" { + cmdio.LogString(ctx, "\nFile structure:") + cmdio.LogString(ctx, fileTree) + } // Try to read and display CLAUDE.md if present readClaudeMd(ctx, configFile) diff --git a/experimental/apps-mcp/lib/prompts/apps.tmpl b/experimental/apps-mcp/lib/prompts/apps.tmpl index a164db006a..1988a4268f 100644 --- a/experimental/apps-mcp/lib/prompts/apps.tmpl +++ b/experimental/apps-mcp/lib/prompts/apps.tmpl @@ -13,7 +13,7 @@ DATABRICKS APPS DEVELOPMENT ⚠️ ALWAYS start by scaffolding a new Databricks app using command: -invoke_databricks_cli 'experimental apps-mcp tools init-template https://github.com/databricks/cli --template-dir experimental/apps-mcp/templates/appkit --branch main --config-json '{"project_name":"my-app-name","app_description":"my-app-description","sql_warehouse_id":"{{if .WarehouseID}}{{.WarehouseID}}{{end}}"}'' +invoke_databricks_cli 'experimental apps-mcp tools init-template --name my-app-name --description "My app description"' # Validation diff --git a/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/databricks.yml.tmpl b/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/databricks.yml.tmpl index c9c1f0630a..555092d2fd 100644 --- a/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/databricks.yml.tmpl +++ b/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/databricks.yml.tmpl @@ -10,8 +10,8 @@ variables: resources: apps: app: - name: ${bundle.target}-{{.project_name}} - description: {{.app_description}} + name: "${bundle.target}-{{.project_name}}" + description: "{{.app_description}}" source_code_path: ./ # Uncomment to enable on behalf of user API scopes. Available scopes: sql, dashboards.genie, files.files