Skip to content

Commit 496a104

Browse files
authored
output files tree, add quotes, simplify init-template (#4036)
## Changes Add files tree to agentic scaffolding output and simplify init-template api ## Why To reduce # of turns wasted on agentic exploration. ## Tests To be verified with bulk generation, WIP
1 parent eb890d2 commit 496a104

File tree

3 files changed

+167
-109
lines changed

3 files changed

+167
-109
lines changed

experimental/apps-mcp/cmd/init_template.go

Lines changed: 164 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"fmt"
88
"os"
99
"path/filepath"
10+
"sort"
11+
"strings"
1012

1113
"github.com/databricks/cli/cmd/root"
1214
"github.com/databricks/cli/experimental/apps-mcp/lib/common"
@@ -69,98 +71,150 @@ func readClaudeMd(ctx context.Context, configFile string) {
6971
cmdio.LogString(ctx, "=================\n")
7072
}
7173

72-
func newInitTemplateCmd() *cobra.Command {
73-
cmd := &cobra.Command{
74-
Use: "init-template [TEMPLATE_PATH]",
75-
Short: "Initialize using a bundle template",
76-
Args: root.MaximumNArgs(1),
77-
Long: fmt.Sprintf(`Initialize using a bundle template to get started quickly.
74+
// generateFileTree creates a tree-style visualization of the file structure.
75+
// Collapses directories with more than 10 files to avoid clutter.
76+
func generateFileTree(outputDir string) (string, error) {
77+
const maxFilesToShow = 10
78+
79+
// collect all files in the output directory
80+
var allFiles []string
81+
err := filepath.Walk(outputDir, func(path string, info os.FileInfo, err error) error {
82+
if err != nil {
83+
return err
84+
}
85+
if !info.IsDir() {
86+
relPath, err := filepath.Rel(outputDir, path)
87+
if err != nil {
88+
return err
89+
}
90+
allFiles = append(allFiles, filepath.ToSlash(relPath))
91+
}
92+
return nil
93+
})
94+
if err != nil {
95+
return "", err
96+
}
7897

79-
TEMPLATE_PATH optionally specifies which template to use. It can be one of the following:
80-
%s
81-
- a local file system path with a template directory
82-
- a Git repository URL, e.g. https://github.com/my/repository
98+
// build a tree structure
99+
tree := make(map[string][]string)
83100

84-
Supports the same options as 'databricks bundle init' plus:
85-
--describe: Display template schema without materializing
86-
--config_json: Provide config as JSON string instead of file
101+
for _, relPath := range allFiles {
102+
parts := strings.Split(relPath, "/")
103+
104+
if len(parts) == 1 {
105+
// root level file
106+
tree[""] = append(tree[""], parts[0])
107+
} else {
108+
// file in subdirectory
109+
dir := strings.Join(parts[:len(parts)-1], "/")
110+
fileName := parts[len(parts)-1]
111+
tree[dir] = append(tree[dir], fileName)
112+
}
113+
}
114+
115+
// format as tree
116+
var output strings.Builder
117+
var sortedDirs []string
118+
for dir := range tree {
119+
sortedDirs = append(sortedDirs, dir)
120+
}
121+
sort.Strings(sortedDirs)
122+
123+
for _, dir := range sortedDirs {
124+
filesInDir := tree[dir]
125+
if dir == "" {
126+
// root files - always show all
127+
for _, file := range filesInDir {
128+
output.WriteString(file)
129+
output.WriteString("\n")
130+
}
131+
} else {
132+
// directory
133+
output.WriteString(dir)
134+
output.WriteString("/\n")
135+
if len(filesInDir) <= maxFilesToShow {
136+
// show all files
137+
for _, file := range filesInDir {
138+
output.WriteString(" ")
139+
output.WriteString(file)
140+
output.WriteString("\n")
141+
}
142+
} else {
143+
// collapse large directories
144+
output.WriteString(fmt.Sprintf(" (%d files)\n", len(filesInDir)))
145+
}
146+
}
147+
}
148+
149+
return output.String(), nil
150+
}
151+
152+
const (
153+
defaultTemplateRepo = "https://github.com/databricks/cli"
154+
defaultTemplateDir = "experimental/apps-mcp/templates/appkit"
155+
defaultBranch = "main"
156+
templatePathEnvVar = "DATABRICKS_APPKIT_TEMPLATE_PATH"
157+
)
158+
159+
func newInitTemplateCmd() *cobra.Command {
160+
cmd := &cobra.Command{
161+
Use: "init-template",
162+
Short: "Initialize a Databricks App using the appkit template",
163+
Args: cobra.NoArgs,
164+
Long: `Initialize a Databricks App using the appkit template.
87165
88166
Examples:
89-
experimental apps-mcp tools init-template # Choose from built-in templates
90-
experimental apps-mcp tools init-template default-python # Python jobs and notebooks
91-
experimental apps-mcp tools init-template --output-dir ./my-project
92-
experimental apps-mcp tools init-template default-python --describe
93-
experimental apps-mcp tools init-template default-python --config_json '{"project_name":"my-app"}'
167+
experimental apps-mcp tools init-template --name my-app
168+
experimental apps-mcp tools init-template --name my-app --warehouse abc123
169+
experimental apps-mcp tools init-template --name my-app --description "My cool app"
170+
experimental apps-mcp tools init-template --name my-app --output-dir ./projects
171+
172+
Environment variables:
173+
DATABRICKS_APPKIT_TEMPLATE_PATH Override template source with local path (for development)
94174
95175
After initialization:
96176
databricks bundle deploy --target dev
97-
98-
See https://docs.databricks.com/en/dev-tools/bundles/templates.html for more information on templates.`, template.HelpDescriptions()),
177+
`,
99178
}
100179

101-
var configFile string
180+
var name string
181+
var warehouse string
182+
var description string
102183
var outputDir string
103-
var templateDir string
104-
var tag string
105-
var branch string
106-
var configJSON string
107184
var describe bool
108185

109-
cmd.Flags().StringVar(&configFile, "config-file", "", "JSON file containing key value pairs of input parameters required for template initialization.")
110-
cmd.Flags().StringVar(&templateDir, "template-dir", "", "Directory path within a Git repository containing the template.")
111-
cmd.Flags().StringVar(&outputDir, "output-dir", "", "Directory to write the initialized template to.")
112-
cmd.Flags().StringVar(&branch, "tag", "", "Git tag to use for template initialization")
113-
cmd.Flags().StringVar(&tag, "branch", "", "Git branch to use for template initialization")
114-
cmd.Flags().StringVar(&configJSON, "config-json", "", "JSON string containing key value pairs (alternative to --config-file).")
186+
cmd.Flags().StringVar(&name, "name", "", "Project name (required)")
187+
cmd.Flags().StringVar(&warehouse, "warehouse", "", "SQL warehouse ID")
188+
cmd.Flags().StringVar(&description, "description", "", "App description")
189+
cmd.Flags().StringVar(&outputDir, "output-dir", "", "Directory to write the initialized template to")
115190
cmd.Flags().BoolVar(&describe, "describe", false, "Display template schema without initializing")
116191

117192
cmd.PreRunE = root.MustWorkspaceClient
118193
cmd.RunE = func(cmd *cobra.Command, args []string) error {
119-
if tag != "" && branch != "" {
120-
return errors.New("only one of --tag or --branch can be specified")
121-
}
122-
123-
if configFile != "" && configJSON != "" {
124-
return errors.New("only one of --config-file or --config-json can be specified")
125-
}
194+
ctx := cmd.Context()
126195

127-
if configFile != "" {
128-
if configBytes, err := os.ReadFile(configFile); err == nil {
129-
var userConfigMap map[string]any
130-
if err := json.Unmarshal(configBytes, &userConfigMap); err == nil {
131-
if projectName, ok := userConfigMap["project_name"].(string); ok {
132-
if err := validateAppNameLength(projectName); err != nil {
133-
return err
134-
}
135-
}
136-
}
137-
}
138-
}
196+
// Resolve template source: env var override or default remote
197+
templatePathOrUrl := os.Getenv(templatePathEnvVar)
198+
templateDir := ""
199+
branch := ""
139200

140-
var templatePathOrUrl string
141-
if len(args) > 0 {
142-
templatePathOrUrl = args[0]
201+
if templatePathOrUrl == "" {
202+
templatePathOrUrl = defaultTemplateRepo
203+
templateDir = defaultTemplateDir
204+
branch = defaultBranch
143205
}
144206

145-
ctx := cmd.Context()
146-
147-
// NEW: Describe mode - show schema only
207+
// Describe mode - show schema only
148208
if describe {
149209
r := template.Resolver{
150210
TemplatePathOrUrl: templatePathOrUrl,
151211
ConfigFile: "",
152212
OutputDir: outputDir,
153213
TemplateDir: templateDir,
154-
Tag: tag,
155214
Branch: branch,
156215
}
157216

158217
tmpl, err := r.Resolve(ctx)
159-
if errors.Is(err, template.ErrCustomSelected) {
160-
cmdio.LogString(ctx, "Please specify a path or Git repository to use a custom template.")
161-
cmdio.LogString(ctx, "See https://docs.databricks.com/en/dev-tools/bundles/templates.html to learn more about custom templates.")
162-
return nil
163-
}
164218
if err != nil {
165219
return err
166220
}
@@ -179,55 +233,55 @@ See https://docs.databricks.com/en/dev-tools/bundles/templates.html for more inf
179233
return nil
180234
}
181235

182-
if configJSON != "" {
183-
var userConfigMap map[string]any
184-
if err := json.Unmarshal([]byte(configJSON), &userConfigMap); err != nil {
185-
return fmt.Errorf("invalid JSON in --config-json: %w", err)
186-
}
236+
// Validate required flag
237+
if name == "" {
238+
return errors.New("--name is required")
239+
}
187240

188-
// Validate app name length
189-
if projectName, ok := userConfigMap["project_name"].(string); ok {
190-
if err := validateAppNameLength(projectName); err != nil {
191-
return err
192-
}
193-
}
241+
if err := validateAppNameLength(name); err != nil {
242+
return err
243+
}
194244

195-
tmpFile, err := os.CreateTemp("", "mcp-template-config-*.json")
196-
if err != nil {
197-
return fmt.Errorf("create temp config file: %w", err)
198-
}
199-
defer os.Remove(tmpFile.Name())
245+
// Build config map from flags
246+
configMap := map[string]any{
247+
"project_name": name,
248+
}
249+
if warehouse != "" {
250+
configMap["sql_warehouse_id"] = warehouse
251+
}
252+
if description != "" {
253+
configMap["app_description"] = description
254+
}
200255

201-
configBytes, err := json.Marshal(userConfigMap)
202-
if err != nil {
203-
return fmt.Errorf("marshal config: %w", err)
204-
}
205-
if _, err := tmpFile.Write(configBytes); err != nil {
206-
return fmt.Errorf("write config file: %w", err)
207-
}
208-
if err := tmpFile.Close(); err != nil {
209-
return fmt.Errorf("close config file: %w", err)
210-
}
256+
// Write config to temp file
257+
tmpFile, err := os.CreateTemp("", "mcp-template-config-*.json")
258+
if err != nil {
259+
return fmt.Errorf("create temp config file: %w", err)
260+
}
261+
defer os.Remove(tmpFile.Name())
211262

212-
configFile = tmpFile.Name()
263+
configBytes, err := json.Marshal(configMap)
264+
if err != nil {
265+
return fmt.Errorf("marshal config: %w", err)
266+
}
267+
if _, err := tmpFile.Write(configBytes); err != nil {
268+
return fmt.Errorf("write config file: %w", err)
213269
}
270+
if err := tmpFile.Close(); err != nil {
271+
return fmt.Errorf("close config file: %w", err)
272+
}
273+
274+
configFile := tmpFile.Name()
214275

215-
// Standard materialize flow (identical to bundle/init.go)
216276
r := template.Resolver{
217277
TemplatePathOrUrl: templatePathOrUrl,
218278
ConfigFile: configFile,
219279
OutputDir: outputDir,
220280
TemplateDir: templateDir,
221-
Tag: tag,
222281
Branch: branch,
223282
}
224283

225284
tmpl, err := r.Resolve(ctx)
226-
if errors.Is(err, template.ErrCustomSelected) {
227-
cmdio.LogString(ctx, "Please specify a path or Git repository to use a custom template.")
228-
cmdio.LogString(ctx, "See https://docs.databricks.com/en/dev-tools/bundles/templates.html to learn more about custom templates.")
229-
return nil
230-
}
231285
if err != nil {
232286
return err
233287
}
@@ -239,26 +293,30 @@ See https://docs.databricks.com/en/dev-tools/bundles/templates.html for more inf
239293
}
240294
tmpl.Writer.LogTelemetry(ctx)
241295

242-
// Show branded success message
243-
templateName := "bundle"
244-
if templatePathOrUrl != "" {
245-
templateName = filepath.Base(templatePathOrUrl)
246-
}
247-
outputPath := outputDir
248-
if outputPath == "" {
249-
outputPath = "."
296+
// Determine actual output directory (template writes to subdirectory with project name)
297+
actualOutputDir := outputDir
298+
if actualOutputDir == "" {
299+
actualOutputDir = name
250300
}
301+
251302
// Count files if we can
252303
fileCount := 0
253-
if absPath, err := filepath.Abs(outputPath); err == nil {
304+
if absPath, err := filepath.Abs(actualOutputDir); err == nil {
254305
_ = filepath.Walk(absPath, func(path string, info os.FileInfo, err error) error {
255306
if err == nil && !info.IsDir() {
256307
fileCount++
257308
}
258309
return nil
259310
})
260311
}
261-
cmdio.LogString(ctx, common.FormatScaffoldSuccess(templateName, outputPath, fileCount))
312+
cmdio.LogString(ctx, common.FormatScaffoldSuccess("appkit", actualOutputDir, fileCount))
313+
314+
// Generate and print file tree structure
315+
fileTree, err := generateFileTree(actualOutputDir)
316+
if err == nil && fileTree != "" {
317+
cmdio.LogString(ctx, "\nFile structure:")
318+
cmdio.LogString(ctx, fileTree)
319+
}
262320

263321
// Try to read and display CLAUDE.md if present
264322
readClaudeMd(ctx, configFile)

experimental/apps-mcp/lib/prompts/apps.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ DATABRICKS APPS DEVELOPMENT
1313

1414
⚠️ ALWAYS start by scaffolding a new Databricks app using command:
1515

16-
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}}"}''
16+
invoke_databricks_cli 'experimental apps-mcp tools init-template --name my-app-name --description "My app description"'
1717

1818
# Validation
1919

experimental/apps-mcp/templates/appkit/template/{{.project_name}}/databricks.yml.tmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ variables:
1010
resources:
1111
apps:
1212
app:
13-
name: ${bundle.target}-{{.project_name}}
14-
description: {{.app_description}}
13+
name: "${bundle.target}-{{.project_name}}"
14+
description: "{{.app_description}}"
1515
source_code_path: ./
1616

1717
# Uncomment to enable on behalf of user API scopes. Available scopes: sql, dashboards.genie, files.files

0 commit comments

Comments
 (0)