Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -55,7 +55,7 @@ To help you start building real-world applications, we've created Kool Presets a

- **Node**: [NestJS](docs/03-Presets/NestJS.md), [AdonisJs](docs/03-Presets/AdonisJs.md), [Express.js](/docs/03-Presets/ExpressJS.md)
- **PHP**: [Laravel](docs/03-Presets/Laravel.md), [Laravel Octane](docs/03-Presets/Laravel+Octane.md), [Symfony](docs/03-Presets/Symfony.md), [CodeIgniter](docs/03-Presets/CodeIgniter.md)
- **Javascript**: [Next.js](docs/03-Presets/NextJS.md), [NuxtJS](docs/03-Presets/NuxtJS.md)
- **Javascript**: [Next.js](docs/03-Presets/NextJS.md), [NuxtJS](docs/03-Presets/NuxtJS.md), [Bun](docs/03-Presets/Bun.md)
- **Others**: [Hugo](docs/03-Presets/Hugo.md), [WordPress](docs/03-Presets/WordPress.md)

#### Monorepo structures
Expand Down
45 changes: 35 additions & 10 deletions commands/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"kool-dev/kool/core/presets"
"kool-dev/kool/core/shell"
"os"
"path"
"path/filepath"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -83,14 +82,16 @@ func (c *KoolCreate) Execute(args []string) (err error) {
}
}

// sets env variable CREATE_DIRECTORY so preset can use it
c.env.Set("CREATE_DIRECTORY", createDirectory)

if !c.parser.Exists(preset) {
err = fmt.Errorf("unknown preset %s", preset)
return
}

var projectDir string
if projectDir, err = c.prepareCreateTarget(createDirectory); err != nil {
return
}

c.Shell().Println("Creating new", preset, "project...")

c.parser.PrepareExecutor(c.Shell())
Expand All @@ -101,17 +102,16 @@ func (c *KoolCreate) Execute(args []string) (err error) {

c.Shell().Println("Initializing", preset, "preset...")

if !path.IsAbs(createDirectory) {
if createDirectory, err = filepath.Abs(createDirectory); err != nil {
return
}
if _, err = os.Stat(projectDir); os.IsNotExist(err) {
err = fmt.Errorf("preset did not create folder %s", projectDir)
return
}

if err = os.Chdir(createDirectory); err != nil {
if err = os.Chdir(projectDir); err != nil {
return
}

c.env.Set("PWD", createDirectory)
c.env.Set("PWD", projectDir)

if err = c.parser.Install(preset); err != nil {
return
Expand All @@ -122,6 +122,31 @@ func (c *KoolCreate) Execute(args []string) (err error) {
return
}

// prepareCreateTarget makes CREATE_DIRECTORY a folder name relative to the
// current working directory so `kool docker` (which mounts cwd at /app) writes
// the new project onto the host. Absolute paths like /tmp/my-app otherwise
// land in the container's own /tmp and vanish when the container exits.
func (c *KoolCreate) prepareCreateTarget(createDirectory string) (projectDir string, err error) {
if projectDir, err = filepath.Abs(createDirectory); err != nil {
return
}
projectDir = filepath.Clean(projectDir)

parent := filepath.Dir(projectDir)
base := filepath.Base(projectDir)

if err = os.MkdirAll(parent, 0755); err != nil {
return
}
if err = os.Chdir(parent); err != nil {
return
}

c.env.Set("CREATE_DIRECTORY", base)
c.env.Set("PWD", parent)
return
}

// NewCreateCommand initializes new kool create command
func NewCreateCommand(create *KoolCreate) (createCmd *cobra.Command) {
createCmd = &cobra.Command{
Expand Down
26 changes: 26 additions & 0 deletions commands/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"kool-dev/kool/core/presets"
"kool-dev/kool/core/shell"
"os"
"path/filepath"
"strings"
"testing"
)
Expand Down Expand Up @@ -117,3 +118,28 @@ func TestErrInstallCreateCommand(t *testing.T) {
// return to original folder
_ = os.Chdir(cwd)
}

func TestCreateCommandUsesRelativeCreateDirectory(t *testing.T) {
f := newFakeKoolCreate()
f.parser.(*presets.FakeParser).MockExists = true

cwd, _ := os.Getwd()
defer func() { _ = os.Chdir(cwd) }()

parent := t.TempDir()
dest := filepath.Join(parent, "my-app")
if err := os.Mkdir(dest, 0755); err != nil {
t.Fatal(err)
}

cmd := NewCreateCommand(f)
cmd.SetArgs([]string{"laravel", dest})

if err := cmd.Execute(); err != nil {
t.Errorf("unexpected error executing create command; error: %v", err)
}

if got := f.env.Get("CREATE_DIRECTORY"); got != "my-app" {
t.Errorf("CREATE_DIRECTORY should be the folder name for docker mounts, got %q", got)
}
}
10 changes: 9 additions & 1 deletion commands/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type KoolDockerFlags struct {
Volumes []string
Publish []string
Network []string
User string
}

// KoolDocker holds handlers and functions to implement the docker command logic
Expand All @@ -39,7 +40,7 @@ func AddKoolDocker(root *cobra.Command) {
func NewKoolDocker() *KoolDocker {
return &KoolDocker{
*newDefaultKoolService(),
&KoolDockerFlags{[]string{}, []string{}, []string{}, []string{}},
&KoolDockerFlags{[]string{}, []string{}, []string{}, []string{}, ""},
environment.NewEnvStorage(),
builder.NewCommand("docker", "run", "--init", "--rm", "-w", "/app", "-i"),
}
Expand All @@ -58,6 +59,12 @@ func (d *KoolDocker) Execute(args []string) (err error) {
d.dockerRun.AppendArgs("--env", "ASUSER="+asuser)
}

// run the container as the given user/UID (e.g. to keep created files
// owned by the host user on images that do not honor kool's ASUSER)
if d.Flags.User != "" {
d.dockerRun.AppendArgs("--user", d.Flags.User)
}

if len(d.Flags.EnvVariables) > 0 {
for _, envVar := range d.Flags.EnvVariables {
d.dockerRun.AppendArgs("--env", envVar)
Expand Down Expand Up @@ -107,6 +114,7 @@ the [COMMAND] to provide optional arguments required by the COMMAND.`,
cmd.Flags().StringArrayVarP(&docker.Flags.Volumes, "volume", "v", []string{}, "Bind mount a volume.")
cmd.Flags().StringArrayVarP(&docker.Flags.Publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.")
cmd.Flags().StringArrayVarP(&docker.Flags.Network, "network", "n", []string{}, "Connect a container to a network.")
cmd.Flags().StringVarP(&docker.Flags.User, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>]) to run the container as.")

//After a non-flag arg, stop parsing flags
cmd.Flags().SetInterspersed(false)
Expand Down
26 changes: 24 additions & 2 deletions commands/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func newFakeKoolDocker() *KoolDocker {
return &KoolDocker{
*(newDefaultKoolService().Fake()),
&KoolDockerFlags{[]string{}, []string{}, []string{}, []string{}},
&KoolDockerFlags{[]string{}, []string{}, []string{}, []string{}, ""},
environment.NewFakeEnvStorage(),
&builder.FakeCommand{MockCmd: "docker"},
}
Expand All @@ -23,7 +23,7 @@ func newFakeKoolDocker() *KoolDocker {
func newFailedFakeKoolDocker() *KoolDocker {
return &KoolDocker{
*(newDefaultKoolService().Fake()),
&KoolDockerFlags{[]string{}, []string{}, []string{}, []string{}},
&KoolDockerFlags{[]string{}, []string{}, []string{}, []string{}, ""},
environment.NewFakeEnvStorage(),
&builder.FakeCommand{MockCmd: "docker", MockInteractiveError: errors.New("error docker")},
}
Expand All @@ -50,6 +50,10 @@ func TestNewKoolDocker(t *testing.T) {
if len(k.Flags.Publish) > 0 {
t.Errorf("bad default value for Publish flag on default KoolDocker instance")
}

if k.Flags.User != "" {
t.Errorf("bad default value for User flag on default KoolDocker instance")
}
}

if _, ok := k.dockerRun.(*builder.DefaultCommand); !ok {
Expand Down Expand Up @@ -169,6 +173,24 @@ func TestEnvFlagNewDockerCommand(t *testing.T) {
}
}

func TestUserFlagNewDockerCommand(t *testing.T) {
f := newFakeKoolDocker()
f.shell.(*shell.FakeShell).MockIsTerminal = false
cmd := NewDockerCommand(f)

cmd.SetArgs([]string{"--user=1000", "image"})

if err := cmd.Execute(); err != nil {
t.Errorf("unexpected error executing docker command; error: %v", err)
}

argsAppend := f.dockerRun.(*builder.FakeCommand).ArgsAppend

if len(argsAppend) != 4 || argsAppend[0] != "--user" || argsAppend[1] != "1000" {
t.Errorf("bad arguments to KoolDocker.dockerRun Command with User flag: %v", argsAppend)
}
}

func TestVolumesFlagNewDockerCommand(t *testing.T) {
f := newFakeKoolDocker()
f.shell.(*shell.FakeShell).MockIsTerminal = false
Expand Down
46 changes: 35 additions & 11 deletions commands/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,33 @@ func (s *KoolStatus) getServices() (services []string, err error) {
return
}

parsedServices := strings.Split(strings.ReplaceAll(output, "\r\n", "\n"), "\n")
for _, s := range parsedServices {
if s != "" {
services = append(services, s)
services = parseComposeServices(output)
return
}

func composeTokenLines(output string) (tokens []string) {
for _, line := range strings.Split(strings.ReplaceAll(output, "\r\n", "\n"), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.ContainsAny(line, " \t") {
continue
}
tokens = append(tokens, line)
}

return
}

func parseComposeServices(output string) []string {
return composeTokenLines(output)
}

func firstComposeToken(output string) string {
tokens := composeTokenLines(output)
if len(tokens) == 0 {
return ""
}
return tokens[0]
}

func (s *KoolStatus) fetchServiceInfo(service string, chStatus chan *statusService, wg *sync.WaitGroup) {
var isRunning bool

Expand All @@ -173,12 +190,19 @@ func (s *KoolStatus) fetchServiceInfo(service string, chStatus chan *statusServi
}

func (s *KoolStatus) getServiceInfo(service string) (isRunning bool, status, port string, err error) {
var serviceID string
if serviceID, err = s.Shell().Exec(s.getServiceIDCmd, service); err == nil && serviceID != "" {
status, port = s.getStatusPort(serviceID)
if strings.HasPrefix(status, "Up") {
isRunning = true
}
var output, serviceID string
if output, err = s.Shell().Exec(s.getServiceIDCmd, service); err != nil {
return
}

// docker compose may mix warnings into combined output; keep only the container id
if serviceID = firstComposeToken(output); serviceID == "" {
return
}

status, port = s.getStatusPort(serviceID)
if strings.HasPrefix(status, "Up") {
isRunning = true
}
return
}
Expand Down
14 changes: 14 additions & 0 deletions commands/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,17 @@ cache | Not running | | output`
t.Errorf("Expected '%s', got '%s'", expected, output)
}
}

func TestParseComposeServicesIgnoresWarnings(t *testing.T) {
out := "time=\"2026-08-25T17:03:52-03:00\" level=warning msg=\"the attribute `version` is obsolete\"\napp\n"
services := parseComposeServices(out)

if len(services) != 1 || services[0] != "app" {
t.Errorf("expected only 'app', got %v", services)
}

idOut := "time=\"2026-08-25T17:03:52-03:00\" level=warning msg=\"the attribute `version` is obsolete\"\nabc123def456\n"
if got := firstComposeToken(idOut); got != "abc123def456" {
t.Errorf("expected container id, got %q", got)
}
}
2 changes: 0 additions & 2 deletions docs/01-Getting-Started/4-Starting-new-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ $ touch docker-compose.yml
Copy/paste into this **docker-compose.yml** file a simple, generic Docker Compose configuration for an `app` service container:

```yaml
version: "3.8" # optional since v1.27.0
services:
app:
image: kooldev/php:8.0-nginx
Expand Down Expand Up @@ -174,7 +173,6 @@ scripts:
Here's a more extensible, **Kool-optimized** setup for **docker-compose.yml** into which you can easily add additional services (i.e. database, cache, etc):

```yaml
version: "3.8" # optional since v1.27.0
services:
app:
image: kooldev/php:8.0-nginx
Expand Down
1 change: 1 addition & 0 deletions docs/03-Presets/0-About-Presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ By leveraging Kool Presets, you not only reduce the learning curve associated wi

- [Next.js](/docs/03-Presets/NextJS.md)
- [NuxtJS](/docs/03-Presets/NuxtJS.md)
- [Bun](/docs/03-Presets/Bun.md)

## Others

Expand Down
8 changes: 6 additions & 2 deletions docs/03-Presets/AdonisJs.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,13 @@ $ Preset adonis is initializing!
? Which javascript package manager do you want to use [Use arrows to move, type to filter]
> npm
yarn
bun

$ Preset adonis initialized!
```

> **Experimental:** if you pick **bun**, the `app` service is configured to run the <a href="https://hub.docker.com/r/oven/bun" target="_blank">oven/bun:1</a> image (`command: bun --bun run dev`). AdonisJs officially targets Node, so running it on the Bun runtime is experimental and may require adjustments. To use Bun only as the package manager, keep `kooldev/node:20` as the `app` image in your **docker-compose.yml**.

Now, move into your new AdonisJs project:

```bash
Expand Down Expand Up @@ -143,11 +146,11 @@ To help get you started, **kool.yml** comes prebuilt with an initial set of scri
```yaml
scripts:
adonis: kool exec app adonis
npm: kool exec app npm # or yarn
npm: kool exec app npm # or yarn / bun
npx: kool exec app npx

setup:
- kool docker kooldev/node:20 npm install # or yarn install
- kool docker kooldev/node:20 npm install # or yarn install / bun install (kool docker oven/bun:1 bun install)
- kool start
```

Expand Down Expand Up @@ -246,6 +249,7 @@ $ kool start

We have more presets to help you start projects with **kool** in a standardized way across different frameworks.

- **[Bun](/docs/03-Presets/Bun.md)**
- **[CodeIgniter](/docs/03-Presets/CodeIgniter.md)**
- **[Express.js](/docs/03-Presets/ExpressJS.md)**
- **[Hugo](/docs/03-Presets/Hugo.md)**
Expand Down
Loading
Loading