Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c3b6237
feat: complete _task_2.md
pedronauck Oct 30, 2025
75c262e
feat: complete _task_3.md
pedronauck Oct 30, 2025
6d758da
docs: update prds
pedronauck Oct 30, 2025
81e0377
savepoint
pedronauck Oct 30, 2025
a7f00f5
savepoint
pedronauck Oct 30, 2025
61af2b5
feat: complete _task_6.md
pedronauck Oct 30, 2025
32f7614
feat: complete _task_7.md
pedronauck Oct 30, 2025
f722962
feat: complete _task_9.md
pedronauck Oct 30, 2025
8b36b16
feat: complete _task_10.md
pedronauck Oct 30, 2025
7dd5812
feat: complete _task_12.md
pedronauck Oct 30, 2025
e1605f5
feat: complete _task_13.md
pedronauck Oct 30, 2025
3a481de
feat: complete _task_14.md
pedronauck Oct 30, 2025
d7d43f0
feat: complete _task_15.md
pedronauck Oct 30, 2025
2218b64
feat: complete _task_16.md
pedronauck Oct 30, 2025
91cd102
feat: complete _task_17.md
pedronauck Oct 30, 2025
4e18da1
feat: complete _task_18.md
pedronauck Oct 30, 2025
c976be9
feat: complete _task_19.md
pedronauck Oct 30, 2025
52b7f6f
feat: complete _task_20.md
pedronauck Oct 30, 2025
2b224ac
feat: complete _task_21.md
pedronauck Oct 30, 2025
4123e0f
feat: complete _task_22.md
pedronauck Oct 30, 2025
c7f0a62
feat: complete _task_28.md
pedronauck Oct 30, 2025
bce1809
feat: complete _task_29.md
pedronauck Oct 31, 2025
ddbfd62
fix(repo): resolve PR #315 issues [batch]
pedronauck Oct 31, 2025
117fa71
feat: complete _task_1.md
pedronauck Oct 31, 2025
69473c3
feat: complete _task_2.md
pedronauck Oct 31, 2025
f5a21a2
feat: complete _task_3.md
pedronauck Oct 31, 2025
aee75a0
feat: complete _task_4.md
pedronauck Oct 31, 2025
437c39d
feat: complete _task_5.md
pedronauck Oct 31, 2025
151517f
feat: complete _task_6.md
pedronauck Oct 31, 2025
8709732
feat: complete _task_7.md
pedronauck Oct 31, 2025
f4cadb5
savepoint
pedronauck Oct 31, 2025
ba19d47
savepoint
pedronauck Nov 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ cd my-ai-app
compozy dev
```

> **Note**: When using MCP tools in standalone mode, ensure `mcp_proxy.port` is set to a fixed port (not 0). See [MCP Configuration](./docs/content/docs/core/configuration/project-setup.mdx) for details.
> **Note**: When using MCP tools in memory or persistent modes, ensure `mcp_proxy.port` is set to a fixed port (not 0). See [MCP Configuration](./docs/content/docs/core/configuration/project-setup.mdx) for details.

For a complete walkthrough, check out our [**Quick Start Guide**](./docs/content/docs/core/getting-started/quick-start.mdx).

Expand Down
17 changes: 9 additions & 8 deletions cli/cmd/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import (

// TestConfigShow_Goldens verifies mode fields appear in config show output and match goldens.
func TestConfigShow_Goldens(t *testing.T) {
t.Run("Should match golden file for standalone config", func(t *testing.T) {
t.Run("Should match golden file for memory config", func(t *testing.T) {
ctx := logger.ContextWithLogger(t.Context(), logger.NewForTests())
mgr := pkgconfig.NewManager(ctx, pkgconfig.NewService())
_, err := mgr.Load(ctx, pkgconfig.NewDefaultProvider(), pkgconfig.NewEnvProvider())
require.NoError(t, err)
cfg := mgr.Get()
cfg.Mode = "standalone"
cfg.Redis.Mode = "standalone"
cfg.Mode = pkgconfig.ModeMemory
cfg.Redis.Mode = pkgconfig.ModeMemory
// Capture stdout
r, w, err := os.Pipe()
require.NoError(t, err)
Expand All @@ -32,7 +32,7 @@ func TestConfigShow_Goldens(t *testing.T) {
require.NoError(t, w.Close())
out, err := io.ReadAll(r)
require.NoError(t, err)
testhelpers.CompareWithGolden(t, out, "testdata/config-show-standalone.golden")
testhelpers.CompareWithGolden(t, out, "testdata/config-show-memory.golden")
})

t.Run("Should match golden file for mixed mode config", func(t *testing.T) {
Expand All @@ -41,8 +41,8 @@ func TestConfigShow_Goldens(t *testing.T) {
_, err := mgr.Load(ctx, pkgconfig.NewDefaultProvider(), pkgconfig.NewEnvProvider())
require.NoError(t, err)
cfg := mgr.Get()
cfg.Mode = "distributed"
cfg.Redis.Mode = "standalone"
cfg.Mode = pkgconfig.ModeDistributed
cfg.Redis.Mode = pkgconfig.ModePersistent
// Capture stdout
r, w, err := os.Pipe()
require.NoError(t, err)
Expand All @@ -64,7 +64,8 @@ func TestDiagnostics_EffectiveModes(t *testing.T) {
_, err := mgr.Load(ctx, pkgconfig.NewDefaultProvider(), pkgconfig.NewEnvProvider())
require.NoError(t, err)
cfg := mgr.Get()
cfg.Mode = "standalone"
cfg.Mode = pkgconfig.ModeMemory
cfg.Redis.Mode = pkgconfig.ModeMemory
ctx = pkgconfig.ContextWithManager(ctx, mgr)
// Capture stdout
r, w, err := os.Pipe()
Expand All @@ -76,5 +77,5 @@ func TestDiagnostics_EffectiveModes(t *testing.T) {
require.NoError(t, w.Close())
out, err := io.ReadAll(r)
require.NoError(t, err)
testhelpers.CompareWithGolden(t, out, "testdata/config-diagnostics-standalone.golden")
testhelpers.CompareWithGolden(t, out, "testdata/config-diagnostics-memory.golden")
}
153 changes: 150 additions & 3 deletions cli/cmd/init/components/project_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,39 @@ import (
"github.com/compozy/compozy/engine/runtime"
)

const (
modeMemory = "memory"
modePersistent = "persistent"
modeDistributed = "distributed"
)

var modeDisplayLabels = map[string]string{
modeMemory: "🚀 Memory",
modePersistent: "💾 Persistent",
modeDistributed: "🏭 Distributed",
}

var modeHelpTexts = map[string]string{
modeMemory: strings.Join([]string{
"Memory Mode (🚀):",
"- Zero dependencies, instant startup",
"- Perfect for tests and quick prototyping",
"- No persistence (data lost on restart)",
}, "\n"),
modePersistent: strings.Join([]string{
"Persistent Mode (💾):",
"- File-based storage, state preserved",
"- Ideal for local development",
"- Still zero external dependencies",
}, "\n"),
modeDistributed: strings.Join([]string{
"Distributed Mode (🏭):",
"- External PostgreSQL, Redis, Temporal",
"- Production-ready, horizontal scaling",
"- Requires Docker or managed services",
}, "\n"),
}

// ProjectFormData holds the project initialization data
type ProjectFormData struct {
Name string
Expand All @@ -17,10 +50,21 @@ type ProjectFormData struct {
Author string
AuthorURL string
Template string
Mode string
IncludeDocker bool
InstallBun bool // Whether to install Bun if not available
}

// GetMode returns the selected project mode.
func (d *ProjectFormData) GetMode() string {
return d.Mode
}

// SetMode updates the selected project mode.
func (d *ProjectFormData) SetMode(mode string) {
d.Mode = mode
}

// NewProjectForm creates the project initialization form
func NewProjectForm(data *ProjectFormData) *huh.Form {
setDefaults(data)
Expand All @@ -37,6 +81,12 @@ func setDefaults(data *ProjectFormData) {
if data.Template == "" {
data.Template = "basic"
}
if !isValidMode(data.Mode) {
data.Mode = modeMemory
}
if data.Mode != modeDistributed {
data.IncludeDocker = false
}
}

// createBaseFields creates the basic form fields
Expand Down Expand Up @@ -70,9 +120,27 @@ func createBaseFields(data *ProjectFormData) []huh.Field {
Description("Project template to use").
Options(huh.NewOption("Basic", "basic")).
Value(&data.Template),
createModeField(data),
}
}

func createModeField(data *ProjectFormData) huh.Field {
selectField := huh.NewSelect[string]().
Title("Mode").
Description(modeHelpText(data.Mode)).
Options(
huh.NewOption(modeDisplayLabels[modeMemory], modeMemory),
huh.NewOption(modeDisplayLabels[modePersistent], modePersistent),
huh.NewOption(modeDisplayLabels[modeDistributed], modeDistributed),
).
Value(&data.Mode).
Validate(validateMode)
selectField.DescriptionFunc(func() string {
return modeHelpText(data.Mode)
}, data)
return selectField
}

// addConditionalFields adds conditional fields based on system state
func addConditionalFields(fields []huh.Field, data *ProjectFormData) []huh.Field {
if !runtime.IsBunAvailable() {
Expand All @@ -96,14 +164,93 @@ func createBunInstallField(data *ProjectFormData) huh.Field {

// createDockerField creates the Docker configuration field
func createDockerField(data *ProjectFormData) huh.Field {
return huh.NewConfirm().
confirm := huh.NewConfirm().
Title("Include Docker configuration?").
Description("This will create a docker-compose.yaml with Redis, Postgres\n" +
"and Temporal including, and a .env.example file.").
WithButtonAlignment(lipgloss.Left).
Value(&data.IncludeDocker).
Affirmative("Yes").
Negative("No")
themeState := huh.ThemeCharm()
enabledTheme := cloneTheme(themeState)
disabledTheme := deriveDisabledConfirmTheme(themeState)
confirm.WithTheme(themeState)
applyDockerToggleState(confirm, data, themeState, enabledTheme, disabledTheme)
confirm.Description(dockerHelpText(data.Mode))
confirm.DescriptionFunc(func() string {
applyDockerToggleState(confirm, data, themeState, enabledTheme, disabledTheme)
return dockerHelpText(data.Mode)
}, data)
return confirm
}

func applyDockerToggleState(
confirm *huh.Confirm,
data *ProjectFormData,
themeState, enabledTheme, disabledTheme *huh.Theme,
) {
disabled := data.Mode != modeDistributed
if disabled {
data.IncludeDocker = false
*themeState = *disabledTheme
confirm.WithKeyMap(disabledConfirmKeyMap())
return
}
*themeState = *enabledTheme
confirm.WithKeyMap(huh.NewDefaultKeyMap())
}

func deriveDisabledConfirmTheme(enabled *huh.Theme) *huh.Theme {
disabled := cloneTheme(enabled)
muted := lipgloss.Color("240")
disabled.Focused.Title = disabled.Focused.Title.Foreground(muted)
disabled.Focused.Description = disabled.Focused.Description.Foreground(muted)
disabled.Focused.FocusedButton = disabled.Focused.FocusedButton.Foreground(muted).Background(lipgloss.Color("236"))
disabled.Focused.BlurredButton = disabled.Focused.BlurredButton.Foreground(muted).Background(lipgloss.Color("236"))
disabled.Blurred = disabled.Focused
return disabled
}

func cloneTheme(theme *huh.Theme) *huh.Theme {
clone := *theme
return &clone
}

func disabledConfirmKeyMap() *huh.KeyMap {
keyMap := huh.NewDefaultKeyMap()
keyMap.Confirm.Toggle.SetEnabled(false)
keyMap.Confirm.Accept.SetEnabled(false)
keyMap.Confirm.Reject.SetEnabled(false)
return keyMap
}

func modeHelpText(mode string) string {
if help, ok := modeHelpTexts[mode]; ok {
return help
}
return modeHelpTexts[modeMemory]
}

func dockerHelpText(mode string) string {
if mode == modeDistributed {
return "Generate docker-compose.yaml for external services"
}
return "Docker not needed for embedded mode"
}

func isValidMode(mode string) bool {
switch mode {
case modeMemory, modePersistent, modeDistributed:
return true
default:
return false
}
}

func validateMode(mode string) error {
if !isValidMode(mode) {
return fmt.Errorf("invalid mode selection")
}
return nil
}

// Validation functions
Expand Down
29 changes: 23 additions & 6 deletions cli/cmd/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import (
"github.com/spf13/cobra"
)

const (
defaultInitMode = "memory"
)

// Options holds the configuration for the init command
type Options struct {
Path string `validate:"required"`
Expand All @@ -31,6 +35,7 @@ type Options struct {
Template string
Author string
AuthorURL string
Mode string `validate:"required,oneof=memory persistent distributed"`
Interactive bool
DockerSetup bool
InstallBun bool
Expand Down Expand Up @@ -73,7 +78,7 @@ Examples:
}

func defaultInitOptions() *Options {
return &Options{Version: "0.1.0"}
return &Options{Version: "0.1.0", Mode: defaultInitMode}
}

func applyInitFlags(command *cobra.Command, opts *Options) {
Expand All @@ -83,6 +88,7 @@ func applyInitFlags(command *cobra.Command, opts *Options) {
command.Flags().StringVarP(&opts.Template, "template", "t", "basic", "Project template")
command.Flags().StringVar(&opts.Author, "author", "", "Author name")
command.Flags().StringVar(&opts.AuthorURL, "author-url", "", "Author URL")
command.Flags().StringVar(&opts.Mode, "mode", defaultInitMode, "Project mode (memory|persistent|distributed)")
command.Flags().BoolVarP(&opts.Interactive, "interactive", "i", false, "Force interactive mode")
command.Flags().BoolVar(&opts.DockerSetup, "docker", false, "Include Docker Compose setup")
command.Flags().BoolVar(&opts.InstallBun, "install-bun", false, "Install Bun runtime if missing")
Expand Down Expand Up @@ -127,6 +133,7 @@ func executeInitCommand(cobraCmd *cobra.Command, opts *Options, args []string) e
func runInitJSON(ctx context.Context, _ *cobra.Command, _ *cmd.CommandExecutor, opts *Options) error {
logger.FromContext(ctx).Debug("executing init command in JSON mode")
logDebugMode(ctx)
logSelectedMode(ctx, opts.Mode)
if err := ensureNameProvided(opts); err != nil {
return err
}
Expand All @@ -136,7 +143,7 @@ func runInitJSON(ctx context.Context, _ *cobra.Command, _ *cmd.CommandExecutor,
if err := installBunIfNeeded(ctx, opts); err != nil {
return err
}
if err := generateProjectStructure(opts); err != nil {
if err := generateProjectStructure(ctx, opts); err != nil {
return err
}
envFileName := determineEnvExampleFile(opts.Path)
Expand All @@ -150,13 +157,14 @@ func runInitTUI(ctx context.Context, _ *cobra.Command, _ *cmd.CommandExecutor, o
if err := runInteractiveForm(ctx, opts); err != nil {
return fmt.Errorf("interactive form failed: %w", err)
}
logSelectedMode(ctx, opts.Mode)
if err := validateProjectOptions(opts); err != nil {
return err
}
if err := installBunIfNeeded(ctx, opts); err != nil {
return err
}
if err := generateProjectStructure(opts); err != nil {
if err := generateProjectStructure(ctx, opts); err != nil {
return err
}
envFileName := determineEnvExampleFile(opts.Path)
Expand All @@ -170,6 +178,10 @@ func logDebugMode(ctx context.Context) {
}
}

func logSelectedMode(ctx context.Context, mode string) {
logger.FromContext(ctx).Debug("init mode selected", "mode", mode)
}

func ensureNameProvided(opts *Options) error {
if opts.Name != "" {
return nil
Expand Down Expand Up @@ -197,25 +209,27 @@ func installBunIfNeeded(ctx context.Context, opts *Options) error {
return nil
}

func generateProjectStructure(opts *Options) error {
func generateProjectStructure(ctx context.Context, opts *Options) error {
if err := ensureTemplatesRegistered(); err != nil {
return fmt.Errorf("failed to initialize templates: %w", err)
}
if err := template.GetService().Generate(opts.Template, buildGenerateOptions(opts)); err != nil {
if err := template.GetService().Generate(opts.Template, buildGenerateOptions(ctx, opts)); err != nil {
return fmt.Errorf("failed to generate project: %w", err)
}
return nil
}

func buildGenerateOptions(opts *Options) *template.GenerateOptions {
func buildGenerateOptions(ctx context.Context, opts *Options) *template.GenerateOptions {
return &template.GenerateOptions{
Context: ctx,
Path: opts.Path,
Name: opts.Name,
Description: opts.Description,
Version: opts.Version,
Author: opts.Author,
AuthorURL: opts.AuthorURL,
DockerSetup: opts.DockerSetup,
Mode: opts.Mode,
}
}

Expand All @@ -234,6 +248,7 @@ func buildInitJSONResponse(opts *Options, envFileName string) map[string]any {
"path": opts.Path,
"name": opts.Name,
"version": opts.Version,
"mode": opts.Mode,
"envFile": envFileName,
"docker": opts.DockerSetup,
"files": map[string]string{
Expand Down Expand Up @@ -282,6 +297,7 @@ func runInteractiveForm(_ context.Context, opts *Options) error {
Author: opts.Author,
AuthorURL: opts.AuthorURL,
Template: opts.Template,
Mode: opts.Mode,
IncludeDocker: opts.DockerSetup,
InstallBun: opts.InstallBun,
}
Expand All @@ -302,6 +318,7 @@ func runInteractiveForm(_ context.Context, opts *Options) error {
opts.Author = projectData.Author
opts.AuthorURL = projectData.AuthorURL
opts.Template = projectData.Template
opts.Mode = projectData.Mode
opts.DockerSetup = projectData.IncludeDocker
opts.InstallBun = projectData.InstallBun
return nil
Expand Down
Loading
Loading