Skip to content

Commit 26dc918

Browse files
Antonis Kalipetisclaude
authored andcommitted
feat(commands): add non-interactive mode and Discover function
Add --no-interaction flag and a Discover() function that can be called programmatically by external services like source-integration-apps. - Add NoInteraction field to Answers (defaults to false for CLI) - Add --no-interaction flag to platformify/upsunify commands - Extract Discover() function from Platformify() — accepts an fs.FS and returns *UserInput without writing files - Platformify() now calls Discover() internally, then writes files - FilesOverwrite accepts a file list and skips in non-interactive mode - All interactive question handlers skip prompts when NoInteraction is true, using Discoverer for auto-detection instead - WorkingDirectory handler skips git detection in non-interactive mode and supports pre-set WorkingDirectory from callers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a2655d1 commit 26dc918

13 files changed

Lines changed: 148 additions & 70 deletions

File tree

commands/commands.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88

99
// Execute executes the ify command and sets flags appropriately.
1010
func Execute(assets *vendorization.VendorAssets) error {
11-
cmd := NewPlatformifyCmd(assets)
11+
rootCmd := NewPlatformifyCmd(assets)
1212
validateCmd := NewValidateCommand(assets)
13-
cmd.AddCommand(validateCmd)
14-
return cmd.ExecuteContext(vendorization.WithVendorAssets(context.Background(), assets))
13+
rootCmd.AddCommand(validateCmd)
14+
return rootCmd.ExecuteContext(vendorization.WithVendorAssets(context.Background(), assets))
1515
}

commands/platformify.go

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"io/fs"
89
"os"
910
"path"
1011

@@ -23,28 +24,48 @@ type contextKey string
2324
var FlavorKey contextKey = "flavor"
2425

2526
func NewPlatformifyCmd(assets *vendorization.VendorAssets) *cobra.Command {
27+
var noInteraction bool
2628
cmd := &cobra.Command{
2729
Use: assets.Use,
2830
Aliases: []string{"ify"},
2931
Short: fmt.Sprintf("Creates starter YAML files for your %s project", assets.ServiceName),
3032
SilenceUsage: true,
3133
SilenceErrors: true,
3234
RunE: func(cmd *cobra.Command, _ []string) error {
33-
return Platformify(cmd.Context(), cmd.OutOrStderr(), cmd.ErrOrStderr(), assets)
35+
return Platformify(
36+
cmd.Context(),
37+
cmd.OutOrStderr(),
38+
cmd.ErrOrStderr(),
39+
noInteraction,
40+
assets,
41+
)
3442
},
3543
}
3644

45+
cmd.Flags().BoolVar(&noInteraction, "no-interaction", false, "Disable interactive prompts")
3746
return cmd
3847
}
3948

40-
func Platformify(ctx context.Context, stdout, stderr io.Writer, assets *vendorization.VendorAssets) error {
41-
answers := models.NewAnswers()
42-
answers.Flavor, _ = ctx.Value(FlavorKey).(string)
43-
ctx = models.ToContext(ctx, answers)
44-
ctx = colors.ToContext(ctx, stdout, stderr)
49+
// Discover detects project configuration non-interactively.
50+
// It is the entry point for programmatic callers like source-integration-apps.
51+
func Discover(
52+
ctx context.Context,
53+
flavor string,
54+
noInteraction bool,
55+
fileSystem fs.FS,
56+
) (*platformifier.UserInput, error) {
57+
answers, _ := models.FromContext(ctx)
58+
if answers == nil {
59+
answers = models.NewAnswers()
60+
ctx = models.ToContext(ctx, answers)
61+
}
62+
answers.Flavor = flavor
63+
answers.NoInteraction = noInteraction
64+
if fileSystem != nil {
65+
answers.WorkingDirectory = fileSystem
66+
}
4567
q := questionnaire.New(
4668
&question.WorkingDirectory{},
47-
&question.FilesOverwrite{},
4869
&question.Welcome{},
4970
&question.Stack{},
5071
&question.Type{},
@@ -63,23 +84,48 @@ func Platformify(ctx context.Context, stdout, stderr io.Writer, assets *vendoriz
6384
)
6485
err := q.AskQuestions(ctx)
6586
if errors.Is(err, questionnaire.ErrSilent) {
66-
return nil
87+
return nil, nil
6788
}
6889

6990
if err != nil {
70-
fmt.Fprintln(stderr, colors.Colorize(colors.ErrorCode, err.Error()))
71-
return err
91+
return nil, err
7292
}
7393

74-
input := answers.ToUserInput()
94+
return answers.ToUserInput(), nil
95+
}
7596

97+
func Platformify(
98+
ctx context.Context,
99+
stdout, stderr io.Writer,
100+
noInteraction bool,
101+
assets *vendorization.VendorAssets,
102+
) error {
103+
ctx = colors.ToContext(ctx, stdout, stderr)
104+
ctx = models.ToContext(ctx, models.NewAnswers())
105+
input, err := Discover(ctx, assets.ConfigFlavor, noInteraction, nil)
106+
if err != nil {
107+
return err
108+
}
109+
if input == nil {
110+
return nil
111+
}
112+
answers, _ := models.FromContext(ctx)
76113
pfier := platformifier.New(input, assets.ConfigFlavor)
77114
configFiles, err := pfier.Platformify(ctx)
78115
if err != nil {
79-
fmt.Fprintln(stderr, colors.Colorize(colors.ErrorCode, err.Error()))
80116
return fmt.Errorf("could not configure project: %w", err)
81117
}
82118

119+
filesToCreateUpdate := make([]string, 0, len(configFiles))
120+
for file := range configFiles {
121+
filesToCreateUpdate = append(filesToCreateUpdate, file)
122+
}
123+
124+
filesOverwrite := question.FilesOverwrite{FilesToCreateUpdate: filesToCreateUpdate}
125+
if err := filesOverwrite.Ask(ctx); err != nil {
126+
return err
127+
}
128+
83129
for file, contents := range configFiles {
84130
filePath := path.Join(answers.Cwd, file)
85131
if err := os.MkdirAll(path.Dir(filePath), os.ModeDir|os.ModePerm); err != nil {

internal/question/almost_done.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66

77
"github.com/platformsh/platformify/internal/colors"
8+
"github.com/platformsh/platformify/internal/question/models"
89
)
910

1011
type AlmostDone struct{}
@@ -14,6 +15,10 @@ func (q *AlmostDone) Ask(ctx context.Context) error {
1415
if !ok {
1516
return nil
1617
}
18+
answers, ok := models.FromContext(ctx)
19+
if !ok || answers.NoInteraction {
20+
return nil
21+
}
1722

1823
fmt.Fprintln(out)
1924
fmt.Fprintln(out, colors.Colorize(colors.AccentCode, " (\\_/)"))

internal/question/dependency_manager.go

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,7 @@ import (
1010
)
1111

1212
const (
13-
npmLockFileName = "package-lock.json"
14-
yarnLockFileName = "yarn.lock"
15-
poetryLockFile = "poetry.lock"
16-
pipenvLockFile = "Pipfile.lock"
17-
pipLockFile = "requirements.txt"
18-
composerLockFile = "composer.lock"
19-
bundlerLockFile = "Gemfile.lock"
13+
bundlerLockFile = "Gemfile.lock"
2014
)
2115

2216
type DependencyManager struct{}
@@ -58,24 +52,13 @@ func (q *DependencyManager) Ask(ctx context.Context) error {
5852
}
5953
}()
6054

61-
if exists := utils.FileExists(answers.WorkingDirectory, "", poetryLockFile); exists {
62-
answers.DependencyManagers = append(answers.DependencyManagers, models.Poetry)
63-
} else if exists := utils.FileExists(answers.WorkingDirectory, "", pipenvLockFile); exists {
64-
answers.DependencyManagers = append(answers.DependencyManagers, models.Pipenv)
65-
} else if exists := utils.FileExists(answers.WorkingDirectory, "", pipLockFile); exists {
66-
answers.DependencyManagers = append(answers.DependencyManagers, models.Pip)
55+
dependencyManagers, err := answers.Discoverer.DependencyManagers()
56+
if err != nil {
57+
return err
6758
}
68-
69-
if exists := utils.FileExists(answers.WorkingDirectory, "", composerLockFile); exists {
70-
answers.DependencyManagers = append(answers.DependencyManagers, models.Composer)
71-
answers.Dependencies["php"] = map[string]string{"composer/composer": "^2"}
72-
}
73-
74-
if exists := utils.FileExists(answers.WorkingDirectory, "", yarnLockFileName); exists {
75-
answers.DependencyManagers = append(answers.DependencyManagers, models.Yarn)
76-
answers.Dependencies["nodejs"] = map[string]string{"yarn": "^1.22.0"}
77-
} else if exists := utils.FileExists(answers.WorkingDirectory, "", npmLockFileName); exists {
78-
answers.DependencyManagers = append(answers.DependencyManagers, models.Npm)
59+
answers.DependencyManagers = make([]models.DepManager, 0, len(dependencyManagers))
60+
for _, dm := range dependencyManagers {
61+
answers.DependencyManagers = append(answers.DependencyManagers, models.DepManager(dm))
7962
}
8063

8164
if exists := utils.FileExists(answers.WorkingDirectory, "", bundlerLockFile); exists {

internal/question/environment.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,11 @@ func (q *Environment) Ask(ctx context.Context) error {
1414
return nil
1515
}
1616

17-
answers.Environment = make(map[string]string)
18-
for _, dm := range answers.DependencyManagers {
19-
switch dm {
20-
case models.Poetry:
21-
answers.Environment["POETRY_VERSION"] = "1.8.4"
22-
answers.Environment["POETRY_VIRTUALENVS_IN_PROJECT"] = "true"
23-
case models.Pipenv:
24-
answers.Environment["PIPENV_TOOL_VERSION"] = "2024.2.0"
25-
answers.Environment["PIPENV_VENV_IN_PROJECT"] = "1"
26-
}
17+
environment, err := answers.Discoverer.Environment()
18+
if err != nil {
19+
return err
2720
}
2821

22+
answers.Environment = environment
2923
return nil
3024
}

internal/question/files_overwrite.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,43 @@ import (
1313
"github.com/platformsh/platformify/vendorization"
1414
)
1515

16-
type FilesOverwrite struct{}
16+
// FilesOverwrite prompts the user to confirm overwriting existing config files.
17+
// If FilesToCreateUpdate is set, those files are checked instead of the
18+
// default proprietary files list.
19+
type FilesOverwrite struct {
20+
FilesToCreateUpdate []string
21+
}
1722

1823
func (q *FilesOverwrite) Ask(ctx context.Context) error {
1924
answers, ok := models.FromContext(ctx)
2025
if !ok {
2126
return nil
2227
}
2328

29+
if answers.NoInteraction {
30+
return nil
31+
}
32+
2433
_, stderr, ok := colors.FromContext(ctx)
2534
if !ok {
2635
return nil
2736
}
2837

29-
assets, _ := vendorization.FromContext(ctx)
30-
existingFiles := make([]string, 0, len(assets.ProprietaryFiles()))
31-
for _, p := range assets.ProprietaryFiles() {
38+
filesToCheck := q.FilesToCreateUpdate
39+
if len(filesToCheck) == 0 {
40+
assets, _ := vendorization.FromContext(ctx)
41+
filesToCheck = assets.ProprietaryFiles()
42+
}
43+
44+
existingFiles := make([]string, 0, len(filesToCheck))
45+
for _, p := range filesToCheck {
3246
if st, err := fs.Stat(answers.WorkingDirectory, p); err == nil && !st.IsDir() {
3347
existingFiles = append(existingFiles, p)
3448
}
3549
}
3650

3751
if len(existingFiles) > 0 {
52+
assets, _ := vendorization.FromContext(ctx)
3853
fmt.Fprintln(
3954
stderr,
4055
colors.Colorize(

internal/question/models/answer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
)
1313

1414
type Answers struct {
15+
NoInteraction bool
1516
Stack Stack `json:"stack"`
1617
Flavor string `json:"flavor"`
1718
Type RuntimeType `json:"type"`

internal/question/name.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,20 @@ func (q *Name) Ask(ctx context.Context) error {
2424
if !ok {
2525
return nil
2626
}
27+
defaultName := slugify(path.Base(answers.Cwd))
28+
if defaultName == "" {
29+
defaultName = "app"
30+
}
31+
if answers.NoInteraction {
32+
answers.Name = defaultName
33+
}
2734
if answers.Name != "" {
2835
// Skip the step
2936
return nil
3037
}
3138

3239
question := &survey.Input{
33-
Message: "Tell us your project's application name:", Default: slugify(path.Base(answers.Cwd)),
40+
Message: "Tell us your project's application name:", Default: defaultName,
3441
}
3542

3643
var name string

internal/question/services.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func (q *Services) Ask(ctx context.Context) error {
1818
if !ok {
1919
return nil
2020
}
21-
if len(answers.Services) != 0 {
21+
if len(answers.Services) != 0 || answers.NoInteraction {
2222
// Skip the step
2323
return nil
2424
}

internal/question/stack.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ func (q *Stack) Ask(ctx context.Context) error {
6868
answers.Stack = models.Rails
6969
return nil
7070
case platformifier.Symfony:
71+
if answers.NoInteraction {
72+
answers.Stack = models.GenericStack
73+
return nil
74+
}
7175
// Interactive: offer Symfony CLI below.
7276
default:
7377
answers.Stack = models.GenericStack

0 commit comments

Comments
 (0)