Skip to content

Commit fa0b4d5

Browse files
authored
Add dev mode to CLI (#140)
* Add dev mode to CLI * Updated readme * Removed dev mode README in favor of adding to Canasta-Documentation * Fixed xdebug configuration to correctly trigger breakpoints * Support dev mode in start/stop/restart * Remove unnecessary varnish bypass * Removed unused devmode code * Fix port in Caddyfile * Remove WithoutDevMode orchestrator functions * Cleaned up orchestrator function names * Combined Start/start and Stop/stop * Add --build-from flag to build from local source repositories Enable building Canasta installations from local source code for development and testing. When --build-from is specified: - Build CanastaBase from local source (if directory exists) - Build Canasta from local source - Copy Canasta-DockerCompose from local source (if directory exists) - Set CANASTA_IMAGE in .env to use the locally built image Additional changes: - Separate --dev (boolean) from --dev-tag (image tag specification) - Fix SaveEnvVariable to add new keys, not just update existing ones - --dev-tag and --build-from are mutually exclusive * Fix symlinking in dev mode for user extensions/skins
1 parent e098649 commit fa0b4d5

File tree

19 files changed

+981
-88
lines changed

19 files changed

+981
-88
lines changed

cmd/add/add.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func AddWiki(instance config.Installation, wikiID, siteName, domain, wikipath, d
148148
}
149149

150150
//Checking Running status
151-
err = orchestrators.CheckRunningStatus(instance.Path, instance.Id, instance.Orchestrator)
151+
err = orchestrators.CheckRunningStatus(instance)
152152
if err != nil {
153153
return err
154154
}
@@ -201,7 +201,8 @@ func AddWiki(instance config.Installation, wikiID, siteName, domain, wikipath, d
201201
if err != nil {
202202
return err
203203
}
204-
err = restart.Restart(instance)
204+
205+
err = restart.Restart(instance, false, false)
205206
if err != nil {
206207
return err
207208
}

cmd/create/create.go

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,32 @@ import (
1111

1212
"github.com/CanastaWiki/Canasta-CLI/internal/canasta"
1313
"github.com/CanastaWiki/Canasta-CLI/internal/config"
14+
"github.com/CanastaWiki/Canasta-CLI/internal/devmode"
1415
"github.com/CanastaWiki/Canasta-CLI/internal/farmsettings"
16+
"github.com/CanastaWiki/Canasta-CLI/internal/imagebuild"
17+
"github.com/CanastaWiki/Canasta-CLI/internal/logging"
1518
"github.com/CanastaWiki/Canasta-CLI/internal/mediawiki"
1619
"github.com/CanastaWiki/Canasta-CLI/internal/orchestrators"
1720
"github.com/CanastaWiki/Canasta-CLI/internal/spinner"
1821
)
1922

2023
func NewCmdCreate() *cobra.Command {
2124
var (
22-
path string
23-
orchestrator string
24-
workingDir string
25-
wikiID string
26-
siteName string
27-
domain string
28-
yamlPath string
29-
err error
30-
keepConfig bool
31-
canastaInfo canasta.CanastaVariables
32-
override string
33-
envFile string
25+
path string
26+
orchestrator string
27+
workingDir string
28+
wikiID string
29+
siteName string
30+
domain string
31+
yamlPath string
32+
err error
33+
keepConfig bool
34+
canastaInfo canasta.CanastaVariables
35+
override string
36+
envFile string
37+
devModeFlag bool // enable dev mode
38+
devTag string // registry image tag for dev mode
39+
buildFromPath string // path to build Canasta from source
3440
)
3541
createCmd := &cobra.Command{
3642
Use: "create",
@@ -53,15 +59,23 @@ func NewCmdCreate() *cobra.Command {
5359
log.Fatal(fmt.Errorf("Error: Canasta instance ID should not contain spaces or non-ASCII characters, only alphanumeric characters are allowed"))
5460
}
5561

62+
// Validate --dev-tag and --build-from are mutually exclusive
63+
if devTag != "latest" && buildFromPath != "" {
64+
log.Fatal(fmt.Errorf("Error: --dev-tag and --build-from are mutually exclusive"))
65+
}
66+
5667
// Generate passwords (auto-gen if not provided via flags)
5768
if canastaInfo, err = canasta.GeneratePasswords(workingDir, canastaInfo); err != nil {
5869
log.Fatal(err)
5970
}
6071

6172
description := "Creating Canasta installation '" + canastaInfo.Id + "'..."
73+
if devModeFlag {
74+
description = "Creating Canasta installation '" + canastaInfo.Id + "' with dev mode..."
75+
}
6276
_, done := spinner.New(description)
6377

64-
if err = createCanasta(canastaInfo, workingDir, path, wikiID, siteName, domain, yamlPath, orchestrator, override, envFile, done); err != nil {
78+
if err = createCanasta(canastaInfo, workingDir, path, wikiID, siteName, domain, yamlPath, orchestrator, override, envFile, devModeFlag, devTag, buildFromPath, done); err != nil {
6579
fmt.Print(err.Error(), "\n")
6680
if !keepConfig {
6781
canasta.DeleteConfigAndContainers(keepConfig, path+"/"+canastaInfo.Id, orchestrator)
@@ -93,6 +107,9 @@ func NewCmdCreate() *cobra.Command {
93107
createCmd.Flags().StringVar(&canastaInfo.WikiDBUsername, "wikidbuser", "root", "The username of the wiki database user (default: \"root\")")
94108
createCmd.Flags().StringVar(&canastaInfo.WikiDBPassword, "wikidbpass", "", "Wiki database password (if not provided, auto-generates and saves to .env). Tip: Use --wikidbpass \"$WIKI_DB_PASS\" to avoid exposing password in shell history")
95109
createCmd.Flags().StringVarP(&envFile, "envfile", "e", "", "Path to .env file with password overrides (merged with .env.example)")
110+
createCmd.Flags().BoolVarP(&devModeFlag, "dev", "D", false, "Enable development mode with Xdebug and code extraction")
111+
createCmd.Flags().StringVar(&devTag, "dev-tag", "latest", "Canasta image tag to use (e.g., latest, dev-branch)")
112+
createCmd.Flags().StringVar(&buildFromPath, "build-from", "", "Build Canasta image from local source directory (expects Canasta/, optionally CanastaBase/)")
96113

97114
// Mark required flags
98115
createCmd.MarkFlagRequired("id")
@@ -102,7 +119,7 @@ func NewCmdCreate() *cobra.Command {
102119
}
103120

104121
// createCanasta accepts all the keyword arguments and creates an installation of the latest Canasta.
105-
func createCanasta(canastaInfo canasta.CanastaVariables, workingDir, path, wikiID, siteName, domain, yamlPath, orchestrator, override, envFile string, done chan struct{}) error {
122+
func createCanasta(canastaInfo canasta.CanastaVariables, workingDir, path, wikiID, siteName, domain, yamlPath, orchestrator, override, envFile string, devModeEnabled bool, devTag, buildFromPath string, done chan struct{}) error {
106123
// Pass a message to the "done" channel indicating the completion of createCanasta function.
107124
// This signals the spinner to stop printing progress, regardless of success or failure.
108125
defer func() {
@@ -111,8 +128,24 @@ func createCanasta(canastaInfo canasta.CanastaVariables, workingDir, path, wikiI
111128
if _, err := config.GetDetails(canastaInfo.Id); err == nil {
112129
log.Fatal(fmt.Errorf("Canasta installation with the ID already exist!"))
113130
}
131+
132+
// Determine the base image to use
133+
var baseImage string
134+
if buildFromPath != "" {
135+
// Build Canasta (and optionally CanastaBase) from source
136+
logging.Print("Building Canasta from local source...\n")
137+
builtImage, err := imagebuild.BuildFromSource(buildFromPath)
138+
if err != nil {
139+
return fmt.Errorf("failed to build from source: %w", err)
140+
}
141+
baseImage = builtImage
142+
} else {
143+
// Use registry image with specified tag
144+
baseImage = canasta.GetImageWithTag(devTag)
145+
}
146+
114147
// Clone the stack repository first to create the installation directory
115-
if err := canasta.CloneStackRepo(orchestrator, canastaInfo.Id, &path); err != nil {
148+
if err := canasta.CloneStackRepo(orchestrator, canastaInfo.Id, &path, buildFromPath); err != nil {
116149
return err
117150
}
118151

@@ -132,6 +165,12 @@ func createCanasta(canastaInfo canasta.CanastaVariables, workingDir, path, wikiI
132165
if err := canasta.CreateEnvFile(envFile, path, workingDir, canastaInfo.RootDBPassword, canastaInfo.WikiDBPassword); err != nil {
133166
return err
134167
}
168+
// Set CANASTA_IMAGE in .env for local builds so docker-compose uses the locally built image
169+
if buildFromPath != "" {
170+
if err := canasta.SaveEnvVariable(path+"/.env", "CANASTA_IMAGE", baseImage); err != nil {
171+
return err
172+
}
173+
}
135174
if err := canasta.CopySettings(path); err != nil {
136175
return err
137176
}
@@ -141,18 +180,46 @@ func createCanasta(canastaInfo canasta.CanastaVariables, workingDir, path, wikiI
141180
if err := orchestrators.CopyOverrideFile(path, orchestrator, override, workingDir); err != nil {
142181
return err
143182
}
144-
if err := orchestrators.Start(path, orchestrator); err != nil {
183+
184+
// Dev mode: extract code and build xdebug image before starting
185+
if devModeEnabled {
186+
if err := devmode.SetupFullDevMode(path, orchestrator, baseImage); err != nil {
187+
return err
188+
}
189+
}
190+
191+
// Always start without dev mode for installation to avoid xdebug interference
192+
// (xdebug can cause hangs if a debugger is listening during install.php)
193+
tempInstance := config.Installation{Path: path, Orchestrator: orchestrator, DevMode: false}
194+
if err := orchestrators.Start(tempInstance); err != nil {
145195
return err
146196
}
197+
147198
if _, err := mediawiki.Install(path, yamlPath, orchestrator, canastaInfo); err != nil {
148199
return err
149200
}
150-
if err := config.Add(config.Installation{Id: canastaInfo.Id, Path: path, Orchestrator: orchestrator}); err != nil {
201+
202+
instance := config.Installation{Id: canastaInfo.Id, Path: path, Orchestrator: orchestrator, DevMode: devModeEnabled}
203+
if err := config.Add(instance); err != nil {
151204
return err
152205
}
153-
if err := orchestrators.StopAndStart(path, orchestrator); err != nil {
206+
207+
// Restart to apply all settings
208+
// Stop containers (started without dev mode)
209+
if err := orchestrators.Stop(tempInstance); err != nil {
210+
log.Fatal(err)
211+
}
212+
213+
// Start with appropriate mode (orchestrators.Start handles dev mode automatically)
214+
if err := orchestrators.Start(instance); err != nil {
154215
log.Fatal(err)
155216
}
217+
218+
if devModeEnabled {
219+
fmt.Println("\033[32mDevelopment mode enabled. Edit files in mediawiki-code/ - changes appear immediately.\033[0m")
220+
fmt.Println("\033[32mVSCode: Open the installation directory, install PHP Debug extension, and start 'Listen for Xdebug'.\033[0m")
221+
}
222+
156223
fmt.Println("\033[32mIf you need email enabled for this wiki, please set $wgSMTP; email will not work otherwise. See https://mediawiki.org/wiki/Manual:$wgSMTP for options.\033[0m")
157224
return nil
158225
}

cmd/import/importExisting.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func importCanasta(workingDir, canastaID, domainName, path, orchestrator, databa
7070
if _, err := config.GetDetails(canastaID); err == nil {
7171
log.Fatal(fmt.Errorf("Canasta installation with the ID already exist!"))
7272
}
73-
if err := canasta.CloneStackRepo(orchestrator, canastaID, &path); err != nil {
73+
if err := canasta.CloneStackRepo(orchestrator, canastaID, &path, ""); err != nil {
7474
return err
7575
}
7676
if err := canasta.CopyEnvFile(envPath, path, workingDir); err != nil {
@@ -85,10 +85,11 @@ func importCanasta(workingDir, canastaID, domainName, path, orchestrator, databa
8585
if err := orchestrators.CopyOverrideFile(path, orchestrator, override, workingDir); err != nil {
8686
return err
8787
}
88-
if err := orchestrators.Start(path, orchestrator); err != nil {
88+
instance := config.Installation{Id: canastaID, Path: path, Orchestrator: orchestrator}
89+
if err := orchestrators.Start(instance); err != nil {
8990
return err
9091
}
91-
if err := config.Add(config.Installation{Id: canastaID, Path: path, Orchestrator: orchestrator}); err != nil {
92+
if err := config.Add(instance); err != nil {
9293
return err
9394
}
9495
return nil

cmd/remove/remove.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func RemoveWiki(instance config.Installation, wikiID string) error {
5757
}
5858

5959
//Checking Running status
60-
err = orchestrators.CheckRunningStatus(instance.Path, instance.Id, instance.Orchestrator)
60+
err = orchestrators.CheckRunningStatus(instance)
6161
if err != nil {
6262
return err
6363
}
@@ -111,8 +111,8 @@ func RemoveWiki(instance config.Installation, wikiID string) error {
111111
return err
112112
}
113113

114-
//Stop the Canasta Instance
115-
err = restart.Restart(instance)
114+
//Restart the Canasta Instance
115+
err = restart.Restart(instance, false, false)
116116
if err != nil {
117117
return err
118118
}

cmd/restart/restart.go

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ import (
99

1010
"github.com/CanastaWiki/Canasta-CLI/internal/canasta"
1111
"github.com/CanastaWiki/Canasta-CLI/internal/config"
12+
"github.com/CanastaWiki/Canasta-CLI/internal/devmode"
1213
"github.com/CanastaWiki/Canasta-CLI/internal/logging"
1314
"github.com/CanastaWiki/Canasta-CLI/internal/orchestrators"
1415
)
1516

1617
func NewCmdCreate() *cobra.Command {
1718
var instance config.Installation
1819
var verbose bool
20+
var devModeFlag bool
21+
var noDevFlag bool
1922
var restartCmd = &cobra.Command{
2023
Use: "restart",
2124
Short: "Restart the Canasta installation",
@@ -24,8 +27,12 @@ func NewCmdCreate() *cobra.Command {
2427
if instance.Id == "" && len(args) > 0 {
2528
instance.Id = args[0]
2629
}
27-
fmt.Println("Restarting Canasta installation '" + instance.Id + "'...")
28-
err := Restart(instance)
30+
resolvedInstance, err := canasta.CheckCanastaId(instance)
31+
if err != nil {
32+
log.Fatal(err)
33+
}
34+
fmt.Println("Restarting Canasta installation '" + resolvedInstance.Id + "'...")
35+
err = Restart(resolvedInstance, devModeFlag, noDevFlag)
2936
if err != nil {
3037
log.Fatal(err)
3138
}
@@ -41,24 +48,54 @@ func NewCmdCreate() *cobra.Command {
4148
restartCmd.Flags().StringVarP(&instance.Id, "id", "i", "", "Canasta instance ID")
4249
restartCmd.Flags().StringVarP(&instance.Orchestrator, "orchestrator", "o", "compose", "Orchestrator to use for installation")
4350
restartCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose Output")
51+
restartCmd.Flags().BoolVarP(&devModeFlag, "dev", "D", false, "Restart in development mode with Xdebug")
52+
restartCmd.Flags().BoolVar(&noDevFlag, "no-dev", false, "Restart without development mode (disable dev mode)")
4453
return restartCmd
4554
}
4655

47-
func Restart(instance config.Installation) error {
56+
func Restart(instance config.Installation, enableDev, disableDev bool) error {
57+
// Handle --dev and --no-dev flags
58+
if enableDev && disableDev {
59+
return fmt.Errorf("cannot specify both --dev and --no-dev")
60+
}
61+
62+
// Migrate to the new version Canasta
4863
var err error
49-
if instance.Id != "" {
50-
instance, err = config.GetDetails(instance.Id)
51-
if err != nil {
52-
return err
53-
}
64+
if err = canasta.MigrateToNewVersion(instance.Path); err != nil {
65+
return err
5466
}
5567

56-
//Migrate to the new version Canasta
57-
err = canasta.MigrateToNewVersion(instance.Path)
58-
if err != nil {
68+
// Stop containers (orchestrators.Stop handles dev mode automatically)
69+
if err = orchestrators.Stop(instance); err != nil {
5970
return err
6071
}
6172

62-
err = orchestrators.StopAndStart(instance.Path, instance.Orchestrator)
63-
return err
73+
// Handle dev mode enable/disable
74+
if enableDev {
75+
// Enable dev mode using default registry image
76+
baseImage := canasta.GetDefaultImage()
77+
if err = devmode.EnableDevMode(instance.Path, instance.Orchestrator, baseImage); err != nil {
78+
return err
79+
}
80+
instance.DevMode = true
81+
if instance.Id != "" {
82+
if err = config.Update(instance); err != nil {
83+
logging.Print(fmt.Sprintf("Warning: could not update config: %v\n", err))
84+
}
85+
}
86+
} else if disableDev {
87+
// Disable dev mode - restore extensions/skins as real directories
88+
if err = devmode.DisableDevMode(instance.Path); err != nil {
89+
return err
90+
}
91+
instance.DevMode = false
92+
if instance.Id != "" {
93+
if err = config.Update(instance); err != nil {
94+
logging.Print(fmt.Sprintf("Warning: could not update config: %v\n", err))
95+
}
96+
}
97+
}
98+
99+
// Start containers (orchestrators.Start handles dev mode automatically)
100+
return orchestrators.Start(instance)
64101
}

0 commit comments

Comments
 (0)