Skip to content

Commit a9567e6

Browse files
committed
[ADD] support for managed Extras installation via CLI and TUI
1 parent 974bd16 commit a9567e6

15 files changed

Lines changed: 2337 additions & 13 deletions

File tree

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ evo install my-project --force # Force install even if directory exists
112112
evo install my-project --cli --log # Non-interactive mode + write log.md
113113
evo install my-project --composer-update # Use composer update during setup
114114
evo install my-project --composer-clear-cache # Clear Composer cache before install
115+
evo install my-project --github-pat=TOKEN # GitHub PAT for API requests
116+
evo install my-project --extras=sTask@main,sSeo # Install extras after setup (optional)
115117
```
116118

117119
### Available Options
@@ -136,6 +138,30 @@ evo install my-project --composer-clear-cache # Clear Composer cache before ins
136138
- `--quiet`: Reduce CLI output (warnings/errors only)
137139
- `--composer-clear-cache`: Clear Composer cache before install
138140
- `--composer-update`: Use `composer update` instead of `composer install` during setup
141+
- `--github-pat` / `--github_pat`: GitHub PAT token for API requests (avoids GitHub rate limits)
142+
- `--extras`: Comma-separated extras to install after setup (e.g., `sTask@main,sSeo`)
143+
144+
### CLI Example (Non-interactive)
145+
146+
```bash
147+
evo install demo \
148+
--cli \
149+
--branch=3.5.x \
150+
--db-type=sqlite \
151+
--db-name=database.sqlite \
152+
--admin-username=admin \
153+
--admin-email=admin@example.com \
154+
--admin-password=123456 \
155+
--admin-directory=manager \
156+
--language=uk \
157+
--composer-clear-cache \
158+
--github-pat=YOUR_GITHUB_PAT \
159+
--extras=sTask@main,sSeo
160+
```
161+
162+
Notes:
163+
- `--cli` skips the Extras wizard prompt; use `--extras` to auto-install.
164+
- `--extras` works in both TUI and CLI; when provided, the wizard is skipped and installation starts immediately.
139165

140166
## Presets
141167

@@ -172,6 +198,14 @@ You can create custom presets by extending the `Preset` class. See the `src/Pres
172198
- **Install Type Detection**: Automatically detects if this is a fresh install or an update
173199
- **Secure Configuration**: Creates database config files with proper permissions (read-only)
174200

201+
### Managed Extras Wizard (TUI)
202+
203+
- **Post-install prompt**: After the success screen, the installer asks whether to install additional Extras.
204+
- **Selection UI**: Shows the full managed Extras list (type 0, install via Composer) with checkboxes, versions, and descriptions.
205+
- **Batch install**: Installs selected Extras one-by-one via `php artisan extras extras <Name>` and shows progress/status.
206+
- **Post steps**: Runs `php artisan migrate` once after all Extras, then `php artisan cache:clear-full`.
207+
- **Flow**: Install -> Success screen -> Prompt -> Extras selection -> Progress -> Summary
208+
175209
### Inspired by Docker Implementation
176210

177211
This installer incorporates best practices from the [Evolution CMS Docker implementation](https://github.com/evolution-cms/evolution/blob/nightly/docker/entrypoint.sh), including:

cmd/evo/cli.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,16 @@ func applyCLIEvent(ev domain.Event, stepLabels map[string]string, actions chan<-
195195
printCLILine("✗", ev.StepID, msg, os.Stderr)
196196
}
197197
}
198+
case domain.EventExtras:
199+
if p, ok := ev.Payload.(domain.ExtrasState); ok && p.Stage == domain.ExtrasStageSelect {
200+
sendAction(actions, domain.Action{
201+
Type: domain.ActionExtrasDecision,
202+
QuestionID: "extras_select",
203+
OptionID: "skip",
204+
})
205+
fmt.Fprintln(os.Stdout, "Extras selection skipped in --cli mode.")
206+
return true
207+
}
198208
}
199209
return false
200210
}
@@ -218,6 +228,14 @@ func handleCLIQuestion(q domain.QuestionState, actions chan<- domain.Action, can
218228
*hadError = true
219229
fmt.Fprintln(os.Stderr, "Database connection failed; exiting (no retry in --cli mode).")
220230
return true
231+
case "extras_prompt":
232+
sendAction(actions, domain.Action{
233+
Type: domain.ActionAnswerSelect,
234+
QuestionID: q.ID,
235+
OptionID: "no",
236+
})
237+
fmt.Fprintln(os.Stdout, "Extras installation skipped in --cli mode.")
238+
return true
221239
default:
222240
*hadError = true
223241
fmt.Fprintln(os.Stderr, cliMissingInputMessage(q))

cmd/evo/main.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ func runInstall(ctx context.Context, args []string) int {
221221
adminPassword := fs.String("admin-password", "", "Admin password")
222222
adminDirectory := fs.String("admin-directory", "", "Admin directory (default: manager)")
223223
language := fs.String("language", "", "Installation language (e.g., en, uk)")
224+
githubPat := fs.String("github-pat", "", "GitHub PAT token for API requests")
225+
githubPatAlt := fs.String("github_pat", "", "GitHub PAT token for API requests")
226+
extras := fs.String("extras", "", "Comma-separated extras to install (e.g., sTask@main,sSeo)")
224227
logToFile := fs.Bool("log", false, "Write installer log to file")
225228
cliMode := fs.Bool("cli", false, "Run in non-interactive CLI mode (no TUI)")
226229
quiet := fs.Bool("quiet", false, "Reduce CLI output (warnings/errors only)")
@@ -230,6 +233,11 @@ func runInstall(ctx context.Context, args []string) int {
230233
if err := fs.Parse(flagArgs); err != nil {
231234
return 2
232235
}
236+
pat := strings.TrimSpace(*githubPat)
237+
if pat == "" {
238+
pat = strings.TrimSpace(*githubPatAlt)
239+
}
240+
233241
opt := installengine.Options{
234242
Force: *force,
235243
Dir: installDir,
@@ -248,7 +256,14 @@ func runInstall(ctx context.Context, args []string) int {
248256
AdminPassword: *adminPassword,
249257
AdminDirectory: strings.TrimSpace(*adminDirectory),
250258
Language: strings.ToLower(strings.TrimSpace(*language)),
259+
GithubPat: pat,
260+
}
261+
extrasSelections, err := parseExtrasSelections(*extras)
262+
if err != nil {
263+
fmt.Fprintln(os.Stderr, err)
264+
return 2
251265
}
266+
opt.Extras = extrasSelections
252267
if *cliMode {
253268
if err := applyCLIDefaults(&opt); err != nil {
254269
fmt.Fprintln(os.Stderr, err)
@@ -258,13 +273,42 @@ func runInstall(ctx context.Context, args []string) int {
258273
return runInstaller(ctx, ui.ModeInstall, &opt, *logToFile, *cliMode, *quiet)
259274
}
260275

276+
func parseExtrasSelections(raw string) ([]domain.ExtrasSelection, error) {
277+
raw = strings.TrimSpace(raw)
278+
if raw == "" {
279+
return nil, nil
280+
}
281+
parts := strings.Split(raw, ",")
282+
out := make([]domain.ExtrasSelection, 0, len(parts))
283+
for _, part := range parts {
284+
part = strings.TrimSpace(part)
285+
if part == "" {
286+
continue
287+
}
288+
name := part
289+
version := ""
290+
if strings.Contains(part, "@") {
291+
chunks := strings.SplitN(part, "@", 2)
292+
name = strings.TrimSpace(chunks[0])
293+
if len(chunks) > 1 {
294+
version = strings.TrimSpace(chunks[1])
295+
}
296+
}
297+
if name == "" {
298+
return nil, fmt.Errorf("invalid --extras value: %q", part)
299+
}
300+
out = append(out, domain.ExtrasSelection{Name: name, Version: version})
301+
}
302+
return out, nil
303+
}
304+
261305
func splitInstallArgs(args []string) (installDir string, flagArgs []string, err error) {
262306
flagArgs = make([]string, 0, len(args))
263307

264308
expectsValue := func(flag string) bool {
265309
switch flag {
266310
case "branch", "db-type", "db-host", "db-port", "db-name", "db-user", "db-password",
267-
"admin-username", "admin-email", "admin-password", "admin-directory", "language":
311+
"admin-username", "admin-email", "admin-password", "admin-directory", "language", "github-pat", "github_pat", "extras":
268312
return true
269313
default:
270314
return false

internal/domain/action.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ package domain
33
type ActionType string
44

55
const (
6-
ActionAnswerSelect ActionType = "answer_select"
7-
ActionAnswerInput ActionType = "answer_input"
6+
ActionAnswerSelect ActionType = "answer_select"
7+
ActionAnswerInput ActionType = "answer_input"
8+
ActionExtrasDecision ActionType = "extras_decision"
89
)
910

1011
type Action struct {
@@ -13,5 +14,6 @@ type Action struct {
1314
QuestionID string
1415
OptionID string
1516
Text string
17+
Values []string
18+
Extras []ExtrasSelection
1619
}
17-

internal/domain/event.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const (
1414
EventWarning EventType = "warning"
1515
EventError EventType = "error"
1616
EventExecRequest EventType = "exec_request"
17+
EventExtras EventType = "extras"
1718
)
1819

1920
type Severity string

internal/domain/extras.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package domain
2+
3+
type ExtrasStage string
4+
5+
const (
6+
ExtrasStageSelect ExtrasStage = "select"
7+
ExtrasStageProgress ExtrasStage = "progress"
8+
ExtrasStageSummary ExtrasStage = "summary"
9+
)
10+
11+
type ExtrasItemStatus string
12+
13+
const (
14+
ExtrasStatusPending ExtrasItemStatus = "pending"
15+
ExtrasStatusRunning ExtrasItemStatus = "running"
16+
ExtrasStatusSuccess ExtrasItemStatus = "success"
17+
ExtrasStatusError ExtrasItemStatus = "error"
18+
)
19+
20+
type ExtrasPackage struct {
21+
Name string `json:"name"`
22+
Version string `json:"version"`
23+
Versions []string `json:"versions,omitempty"`
24+
Description string `json:"description"`
25+
DefaultInstallMode string `json:"defaultInstallMode"`
26+
DefaultBranch string `json:"defaultBranch,omitempty"`
27+
}
28+
29+
type ExtrasSelection struct {
30+
Name string
31+
Version string
32+
}
33+
34+
type ExtrasItemResult struct {
35+
Name string
36+
Status ExtrasItemStatus
37+
Message string
38+
}
39+
40+
type ExtrasItemDetail struct {
41+
Name string
42+
Output string
43+
}
44+
45+
type ExtrasState struct {
46+
Active bool
47+
Stage ExtrasStage
48+
Packages []ExtrasPackage
49+
Selections []ExtrasSelection
50+
Results []ExtrasItemResult
51+
Current string
52+
CurrentIndex int
53+
Total int
54+
Details []ExtrasItemDetail
55+
}

internal/engine/install/engine.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ type Options struct {
4545
AdminPassword string
4646
AdminDirectory string
4747
Language string
48+
49+
GithubPat string
50+
Extras []domain.ExtrasSelection
4851
}
4952

5053
type Engine struct {
@@ -85,6 +88,7 @@ func (e *Engine) Run(ctx context.Context, ch chan<- domain.Event, actions <-chan
8588
{ID: "presets", Label: "Step 5: Install presets", Status: domain.StepPending},
8689
{ID: "dependencies", Label: "Step 6: Install dependencies", Status: domain.StepPending},
8790
{ID: "finalize", Label: "Step 7: Finalize installation", Status: domain.StepPending},
91+
{ID: "extras", Label: "Step 8: Install Extras (optional)", Status: domain.StepPending},
8892
},
8993
},
9094
})
@@ -316,7 +320,7 @@ func (e *Engine) Run(ctx context.Context, ch chan<- domain.Event, actions <-chan
316320
Payload: domain.StepStartPayload{
317321
Label: "Step 1: Validate PHP version",
318322
Index: 1,
319-
Total: 7,
323+
Total: 8,
320324
},
321325
})
322326

@@ -388,7 +392,7 @@ func (e *Engine) Run(ctx context.Context, ch chan<- domain.Event, actions <-chan
388392
Payload: domain.StepStartPayload{
389393
Label: "Step 2: Check database connection",
390394
Index: 2,
391-
Total: 7,
395+
Total: 8,
392396
},
393397
})
394398

@@ -950,7 +954,7 @@ func (e *Engine) Run(ctx context.Context, ch chan<- domain.Event, actions <-chan
950954
Payload: domain.StepStartPayload{
951955
Label: "Step 3: Download Evolution CMS",
952956
Index: 3,
953-
Total: 7,
957+
Total: 8,
954958
},
955959
})
956960

@@ -971,6 +975,8 @@ func (e *Engine) Run(ctx context.Context, ch chan<- domain.Event, actions <-chan
971975
WorkDir: workDir,
972976
ComposerClearCache: e.opt.ComposerClearCache,
973977
ComposerUpdate: e.opt.ComposerUpdate,
978+
GithubPat: strings.TrimSpace(e.opt.GithubPat),
979+
Extras: e.opt.Extras,
974980
}); err != nil {
975981
_ = emit(domain.Event{
976982
Type: domain.EventError,
@@ -984,6 +990,8 @@ func (e *Engine) Run(ctx context.Context, ch chan<- domain.Event, actions <-chan
984990
})
985991
return
986992
}
993+
994+
e.maybeRunExtras(ctx, emit, actions, workDir)
987995
}()
988996
}
989997

@@ -1001,6 +1009,9 @@ type phpNewOptions struct {
10011009
AdminDirectory string
10021010
Language string
10031011

1012+
GithubPat string
1013+
Extras []domain.ExtrasSelection
1014+
10041015
Force bool
10051016
Branch string
10061017
WorkDir string
@@ -1100,6 +1111,9 @@ func runPHPNewCommand(ctx context.Context, emit func(domain.Event) bool, opt php
11001111
if strings.TrimSpace(opt.Branch) != "" {
11011112
args = append(args, "--branch="+strings.TrimSpace(opt.Branch))
11021113
}
1114+
if strings.TrimSpace(opt.GithubPat) != "" {
1115+
args = append(args, "--github-pat="+strings.TrimSpace(opt.GithubPat))
1116+
}
11031117
if opt.Force {
11041118
args = append(args, "--force")
11051119
}
@@ -1342,7 +1356,7 @@ func (t *stepTracker) start(stepID, label string, index int) {
13421356
Payload: domain.StepStartPayload{
13431357
Label: label,
13441358
Index: index,
1345-
Total: 7,
1359+
Total: 8,
13461360
},
13471361
})
13481362
}

0 commit comments

Comments
 (0)