Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
49032c8
feat(proxy): run multiple local shops in parallel behind a shared proxy
tturkowski Jul 17, 2026
fffb28a
feat(proxy): reach a shop's own URL from inside its containers
tturkowski Jul 21, 2026
bcf7d19
Use admin-watch subdomain url with proxy enabled
tturkowski Jul 22, 2026
beb27cb
fix(proxy): keep APP_URL and theme in sync with the proxy hostname
tturkowski Jul 23, 2026
cce71d4
feat(proxy): run the storefront watcher through the shared proxy
tturkowski Jul 23, 2026
f7661be
feat(create): add local domains choice with one-time setup
tturkowski Jul 28, 2026
194d5e3
feat(dev): serve proxy projects automatically
tturkowski Jul 28, 2026
6e79b88
feat(devtui): show local domains status in the overview
tturkowski Jul 28, 2026
f4d9dfd
docs(proxy): document the local domains create and dev flows
tturkowski Jul 28, 2026
ad2e282
feat(devtui): show running proxy instances and memory in the overview
tturkowski Jul 28, 2026
4f6e902
docs(proxy): explain the setup-once, agents-need-no-sudo model
tturkowski Jul 28, 2026
68114f3
feat(dev): fall back to ports when the shared proxy cannot start
tturkowski Jul 28, 2026
cd43959
fix(dev): run project dev start/stop without a TTY
tturkowski Jul 28, 2026
c635a54
refactor(proxy): self-review fixes for local-domain dev flow
tturkowski Jul 29, 2026
ff45ff8
fix(devtui): compact watcher links and cap overview right column
tturkowski Jul 29, 2026
0e75e36
fix(proxy): route admin-watch to the version-correct dev-server port
tturkowski Jul 29, 2026
0599705
fix(proxy): repoint sales channel URL on Shopware 6.6 without replace…
tturkowski Jul 29, 2026
98a3ceb
feat(proxy): print WSL Windows-access steps for CA trust and hosts
tturkowski Jul 30, 2026
e96201c
feat(proxy): show WSL nsswitch fix when OS resolution fails
tturkowski Jul 30, 2026
5750952
fix(proxy): map underscores to dashes in derived hostnames; short-cir…
tturkowski Jul 31, 2026
fdd9492
fix(proxy): theme the teardown confirmation with the shopware palette
tturkowski Jul 31, 2026
76ca1fc
feat(devtui): clearer local-domains status labels in the overview
tturkowski Aug 3, 2026
3ef61c0
feat(devtui): proxy instances table and clearer local-domains status
tturkowski Aug 4, 2026
cb170e6
feat(devtui): scrollable overview with mouse wheel
tturkowski Aug 4, 2026
02c58f4
refactor(devtui): use extension.AdminDevServerPort for the admin watc…
tturkowski Aug 6, 2026
c26ca18
chore(proxy): satisfy stricter perfsprint lint from next
tturkowski Aug 6, 2026
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
59 changes: 59 additions & 0 deletions cmd/project/project_create.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package project

import (
"fmt"
"path/filepath"
"strings"

"github.com/shyim/go-composer/repository"
"github.com/spf13/cobra"

"github.com/shopware/shopware-cli/internal/proxy"
"github.com/shopware/shopware-cli/internal/shop"
"github.com/shopware/shopware-cli/internal/system"
"github.com/shopware/shopware-cli/internal/tui"
Expand Down Expand Up @@ -46,6 +51,14 @@ type createOptions struct {
withElasticsearch bool
withAMQP bool
noAudit bool
// useLocalDomain serves the shop at a stable hostname
// (<name>.<baseDomain>) through the shared proxy instead of a fixed port.
// Only meaningful with Docker.
useLocalDomain bool
// setupProxyNow runs the one-time machine setup (DNS + HTTPS trust, needs
// sudo) inline during create, so the local domain works immediately. Set
// only when the user opts in and the machine is not configured yet.
setupProxyNow bool

interactive bool
elasticsearchExplicit bool
Expand All @@ -66,6 +79,35 @@ func (o *createOptions) clearPHP() {
}
}

// localDomainHostname returns the stable proxy hostname for a project name,
// e.g. "my-shop.shopware.local". Underscores (valid in a project name but not
// in a hostname) become dashes.
func localDomainHostname(name, baseDomain string) string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we may move somet of those functions into the proxy pkg?

label := strings.ReplaceAll(filepath.Base(name), "_", "-")
return label + "." + baseDomain
}
Comment on lines +82 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Malformed hostname when the project folder is .. localDomainHostname maps filepath.Base(".") to the label ".", so the derived hostname is "..shopware.local". This value is written to .shopware-project.yml and printed as a shop URL.

  • cmd/project/project_create.go#L82-L88: resolve the working directory name when name is empty or ".", and lowercase the label.
  • cmd/project/project_dev_test.go#L11-L21: add a TestLocalDomainHostname case for the "." input that asserts the resolved directory name is used.
📍 Affects 2 files
  • cmd/project/project_create.go#L82-L88 (this comment)
  • cmd/project/project_dev_test.go#L11-L21
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/project/project_create.go` around lines 82 - 88, Update
localDomainHostname in cmd/project/project_create.go (lines 82-88) to resolve
the current working directory’s base name when name is empty or "." and
lowercase the resulting label before appending baseDomain. Add a
TestLocalDomainHostname case in cmd/project/project_dev_test.go (lines 11-21)
verifying "." uses the resolved directory name.


// proxyBaseDomain returns the machine-wide proxy base domain, falling back to
// the default when no settings are stored yet.
func proxyBaseDomain() string {
if s, err := proxy.LoadSettings(); err == nil {
return s.BaseDomain()
}
return proxy.DefaultDomain
}

// resolveLocalDomainChoice derives the final local-domain settings from the
// individual inputs. Local domains require Docker, so useLocalDomain is always
// gated on useDocker regardless of how the choice was made (interactive or the
// --local-domain flag). setupProxyNow — which triggers the one-time sudo setup
// inline — is only ever true when the choice came from the interactive prompt
// (promptShown), so passing --local-domain never runs sudo without asking.
func resolveLocalDomainChoice(useDocker, wantLocalDomain, promptShown, machineSetupDone, setupNowAnswer bool) (useLocalDomain, setupProxyNow bool) {
useLocalDomain = useDocker && wantLocalDomain
setupProxyNow = useLocalDomain && promptShown && !machineSetupDone && setupNowAnswer
return useLocalDomain, setupProxyNow
}

var projectCreateCmd = &cobra.Command{
Use: "create [name] [version]",
Short: "Create a new Shopware 6 project",
Expand Down Expand Up @@ -136,6 +178,16 @@ var projectCreateCmd = &cobra.Command{
return err
}

// Do the one-time machine setup up front (while the user is still at the
// keyboard for the sudo prompt), before the long composer install. It is
// best-effort: a blocked/declined sudo just means the domain resolves
// once the user runs `project proxy setup` later.
if opts.setupProxyNow {
fmt.Println()
fmt.Println(tui.BoldText.Render("Setting up local domains (one-time, needs sudo)"))
_ = runInlineProxySetup(cmd.Context(), proxyBaseDomain())
}

if err := scaffoldProject(cmd.Context(), &opts, chosenVersion); err != nil {
return err
}
Expand All @@ -150,6 +202,7 @@ func parseCreateFlags(cmd *cobra.Command, args []string) createOptions {
withAMQP, _ := cmd.PersistentFlags().GetBool("with-amqp")
noAudit, _ := cmd.PersistentFlags().GetBool("no-audit")
initGit, _ := cmd.PersistentFlags().GetBool("git")
localDomain, _ := cmd.PersistentFlags().GetBool("local-domain")
versionFlag, _ := cmd.PersistentFlags().GetString("version")
deploymentMethod, _ := cmd.PersistentFlags().GetString("deployment")
ciSystem, _ := cmd.PersistentFlags().GetString("ci")
Expand All @@ -169,6 +222,7 @@ func parseCreateFlags(cmd *cobra.Command, args []string) createOptions {
withAMQP: withAMQP,
noAudit: noAudit,
initGit: initGit,
useLocalDomain: localDomain,
selectedVersion: versionFlag,
selectedDeployment: deploymentMethod,
selectedCI: ciSystem,
Expand Down Expand Up @@ -205,6 +259,10 @@ func applyNonInteractiveDefaults(opts *createOptions) error {
if !opts.elasticsearchExplicit {
opts.withElasticsearch = true
}
// Local domains need Docker; drop the flag if Docker is off. Never run the
// one-time sudo setup non-interactively.
opts.useLocalDomain = opts.useDocker && opts.useLocalDomain
opts.setupProxyNow = false
return nil
}

Expand All @@ -217,6 +275,7 @@ func init() {
projectCreateCmd.PersistentFlags().Bool("with-amqp", false, "Include AMQP queue support (symfony/amqp-messenger)")
projectCreateCmd.PersistentFlags().Bool("no-audit", false, "Disable composer audit blocking insecure packages")
projectCreateCmd.PersistentFlags().Bool("git", false, "Initialize a Git repository")
projectCreateCmd.PersistentFlags().Bool("local-domain", false, "Serve the shop at a stable local hostname (<name>.shopware.local) via the shared proxy instead of a port (requires Docker)")
projectCreateCmd.PersistentFlags().String("version", "", "Shopware version to install (e.g., 6.6.0.0, latest)")
projectCreateCmd.PersistentFlags().String("deployment", "", "Deployment method: none, deployer, platformsh, shopware-paas")
projectCreateCmd.PersistentFlags().String("ci", "", "CI/CD system: none, github, gitlab")
Expand Down
76 changes: 76 additions & 0 deletions cmd/project/project_create_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/shyim/go-version"
"github.com/spf13/cobra"

"github.com/shopware/shopware-cli/internal/proxy"
"github.com/shopware/shopware-cli/internal/shop"
"github.com/shopware/shopware-cli/internal/system"
"github.com/shopware/shopware-cli/internal/tui"
Expand Down Expand Up @@ -72,6 +73,14 @@ func runCreateForm(cmd *cobra.Command, opts *createOptions, releases []repositor
selectElasticsearch := tui.No
selectAMQP := tui.Yes

baseDomain := proxyBaseDomain()
// Default to the stable hostname (recommended); only applies with Docker.
selectLocalDomain := true
// Whether this machine already resolves the proxy domain. When it does, the
// one-time sudo setup is already done, so we never ask for it again.
machineSetupDone := proxy.CheckResolverConfigured(baseDomain).Configured
selectSetupNow := tui.Yes

if !system.IsGitInstalled() {
selectGit = tui.No
}
Expand Down Expand Up @@ -196,6 +205,55 @@ func runCreateForm(cmd *cobra.Command, opts *createOptions, releases []repositor
))
}

if !cmd.PersistentFlags().Changed("local-domain") {
Comment on lines 207 to +208

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you tested also headless usage aka --no-interaction with those flags and without?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc ngocblue FYI

formGroups = append(formGroups, huh.NewGroup(
huh.NewSelect[bool]().
Title("Local domains").
Description("Reach this shop at a stable hostname instead of a changing port").
OptionsFunc(func() []huh.Option[bool] {
host := "<name>." + baseDomain
if opts.projectFolder != "" {
host = localDomainHostname(opts.projectFolder, baseDomain)
}
return []huh.Option[bool]{
huh.NewOption("Yes (recommended) — https://"+host, true),
huh.NewOption("No — use a port (http://localhost:8000)", false),
}
}, &opts.projectFolder).
Value(&selectLocalDomain),
// The shared proxy is Docker-only, so this choice is irrelevant
// without Docker (respecting a --docker flag override).
).WithHideFunc(func() bool {
if cmd.PersistentFlags().Changed("docker") {
return !opts.useDocker
}
return selectDocker != tui.Yes
}))

// Offer the one-time machine setup inline, but only when it is
// actually needed: local domains chosen, Docker on, and the machine
// not configured yet. Every later project skips this automatically.
formGroups = append(formGroups, huh.NewGroup(
tui.NewYesNo().
Title("Set up local domains on this machine now?").
Description("One-time sudo: makes *."+baseDomain+" resolve and trusts its HTTPS certificate. Skip to run `shopware-cli project proxy setup` later.").
Value(&selectSetupNow),
).WithHideFunc(func() bool {
if machineSetupDone {
return true
}
dockerOn := selectDocker == tui.Yes
if cmd.PersistentFlags().Changed("docker") {
dockerOn = opts.useDocker
}
localOn := selectLocalDomain
if cmd.PersistentFlags().Changed("local-domain") {
localOn = opts.useLocalDomain
}
return !dockerOn || !localOn
}))
}

selectAdvanced := tui.No
if needsAdvanced {
formGroups = append(formGroups, huh.NewGroup(
Expand Down Expand Up @@ -329,6 +387,17 @@ func runCreateForm(cmd *cobra.Command, opts *createOptions, releases []repositor
if !cmd.PersistentFlags().Changed("docker") {
opts.useDocker = selectDocker == tui.Yes
}
// The local-domain choice comes from the --local-domain flag when set,
// otherwise from the prompt. The one-time setup is only offered inline
// when we actually prompted for it (not via the flag), so the flag never
// triggers an unprompted sudo.
localFlagChanged := cmd.PersistentFlags().Changed("local-domain")
wantLocalDomain := opts.useLocalDomain
if !localFlagChanged {
wantLocalDomain = selectLocalDomain
}
opts.useLocalDomain, opts.setupProxyNow = resolveLocalDomainChoice(
opts.useDocker, wantLocalDomain, !localFlagChanged, machineSetupDone, selectSetupNow == tui.Yes)
if !cmd.PersistentFlags().Changed("git") {
opts.initGit = selectGit == tui.Yes
}
Expand Down Expand Up @@ -384,6 +453,13 @@ func runCreateForm(cmd *cobra.Command, opts *createOptions, releases []repositor
}
fmt.Printf(" %s %s\n", labelStyle.Render("PHP:"), phpDisplay)
}
if opts.useDocker {
localDomainValue := onOff(opts.useLocalDomain)
if opts.useLocalDomain {
localDomainValue = tui.GreenText.Render("https://" + localDomainHostname(opts.projectFolder, baseDomain))
}
fmt.Printf(" %s %s\n", labelStyle.Render("Local domain:"), localDomainValue)
}
fmt.Printf(" %s %s\n", labelStyle.Render("Git Repository:"), onOff(opts.initGit))
fmt.Printf(" %s %s\n", labelStyle.Render("OpenSearch:"), onOff(opts.withElasticsearch))
fmt.Printf(" %s %s\n", labelStyle.Render("AMQP:"), onOff(opts.withAMQP))
Expand Down
31 changes: 29 additions & 2 deletions cmd/project/project_create_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

dockerpkg "github.com/shopware/shopware-cli/internal/docker"
"github.com/shopware/shopware-cli/internal/git"
"github.com/shopware/shopware-cli/internal/proxy"
"github.com/shopware/shopware-cli/internal/shop"
"github.com/shopware/shopware-cli/internal/system"
"github.com/shopware/shopware-cli/internal/tui"
Expand Down Expand Up @@ -82,6 +83,17 @@ func installAndFinalize(cmd *cobra.Command, opts *createOptions, phpConstraint *
shopCfg.PHPVersion = opts.phpVersion
}

// Serve the shop at a stable hostname through the shared proxy instead of a
// port. The top-level url drives proxy hostname derivation; the environment
// url is what `project dev` shows and installs with.
if opts.useDocker && opts.useLocalDomain {
url := "https://" + localDomainHostname(opts.projectFolder, proxyBaseDomain())
shopCfg.URL = url
if env := shopCfg.Environments["local"]; env != nil {
env.URL = url
}
}

if err := shop.WriteConfig(shopCfg, opts.projectFolder); err != nil {
return err
}
Expand All @@ -107,6 +119,11 @@ func printCreateSummary(ctx context.Context, opts *createOptions) {
fmt.Println(tui.GreenText.Render("✔ Setup complete in " + projectDisplay))

if opts.useDocker {
shopURL := "http://127.0.0.1:8000"
if opts.useLocalDomain {
shopURL = "https://" + localDomainHostname(opts.projectFolder, proxyBaseDomain())
}

fmt.Println()
fmt.Println(tui.SectionHeadingStyle.Render("Next steps"))
fmt.Println()
Expand All @@ -115,12 +132,22 @@ func printCreateSummary(ctx context.Context, opts *createOptions) {
} else {
fmt.Printf(" %s %s\n", tui.GreenText.Render("Start developing:"), tui.BoldText.Render(fmt.Sprintf("cd %s && shopware-cli project dev", opts.projectFolder)))
}
if opts.useLocalDomain && !proxy.CheckResolverConfigured(proxyBaseDomain()).Configured {
fmt.Println()
fmt.Println(tui.DimText.Render(" First time on this machine? Run ") + tui.BoldText.Render("shopware-cli project proxy setup") + tui.DimText.Render(" once (needs sudo)"))
fmt.Println(tui.DimText.Render(" so the local domain resolves and its certificate is trusted."))
}
fmt.Println()
fmt.Println(tui.SectionHeadingStyle.Render("Access your shop (after make setup)"))
fmt.Println()
fmt.Printf(" %s %s\n", tui.GreenText.Render("Storefront:"), tui.BoldText.Render("http://127.0.0.1:8000"))
fmt.Printf(" %s %s\n", tui.GreenText.Render("Admin:"), tui.BoldText.Render("http://127.0.0.1:8000/admin"))
fmt.Printf(" %s %s\n", tui.GreenText.Render("Storefront:"), tui.BoldText.Render(shopURL))
fmt.Printf(" %s %s\n", tui.GreenText.Render("Admin:"), tui.BoldText.Render(shopURL+"/admin"))
fmt.Printf(" %s %s\n", tui.GreenText.Render("Credentials:"), tui.BoldText.Render("admin")+" / "+tui.BoldText.Render("shopware"))

if opts.useLocalDomain {
hostname := localDomainHostname(opts.projectFolder, proxyBaseDomain())
maybePrintWSLWindowsAccess(proxyBrowserHostnames(opts.projectFolder, hostname))
}
}

fmt.Println()
Expand Down
Loading
Loading