add dx build system / CLI orchestrator

This commit is contained in:
Nuno Duque Nunes 2026-05-05 16:32:23 +01:00
commit 51a186ace6
74 changed files with 5829 additions and 0 deletions

321
README.md Normal file
View file

@ -0,0 +1,321 @@
# 🛠️ DX
`dx` is a framework-agnostic bash CLI that orchestrates the full development lifecycle for PHP projects — Docker, Apache, Traefik, environment config, migrations, and tests — across multiple frameworks (Yii2, Laravel).
The `dx` script at the project root is the entrypoint. It loads `dxkit/bootstrap.sh` and dispatches to the command and module system inside `dxkit/`.
## Prerequisites
- Bash (Linux/macOS native; Windows via Git Bash or WSL)
- Docker + Docker Compose
- `dx` must be executable: `chmod +x dx`
## Quick start
```bash
./dx setup # first time: build image + init environment
./dx up # start the stack
./dx shell # enter the app container
./dx console migrate # run database migrations
```
## Command reference
### Setup & initialization
| Command | Description |
|---|---|
| `dx build` | Build the Docker image and generate the compose stack |
| `dx init [env]` | Bring the project to a runnable state for an environment (default: `dev`) |
| `dx setup` | `build` + `init` in one step |
**`dx build` flags**
| Flag | Description |
|---|---|
| `--dev` / `--qly` / `--prd` | Target environment (default: `--dev`) |
| `--project=NAME` | Override the project name |
| `--port=PORT` | Override the app HTTP port |
| `--db=mysql\|mssql` | Database engine (default: `mysql`) |
| `--db-port=PORT` | Override the database port |
| `--sync-hosts` | Write the project domain to `/etc/hosts` after build |
| `--no-vhosts` | Skip Apache virtual host generation |
| `--no-interact` | Non-interactive mode (no prompts) |
| `--debug` | Enable shell trace output |
**`dx init` flags**
| Flag | Description |
|---|---|
| `--skip-vendors` | Skip `composer install` |
| `--skip-framework-init` | Skip framework init script |
| `--skip-framework-config` | Skip framework config generation |
| `--skip-proxy` | Skip starting the reverse proxy |
| `--debug` | Enable shell trace output |
---
### Runtime
| Command | Description |
|---|---|
| `dx up` | Start the Docker stack |
| `dx down` | Stop the Docker stack |
| `dx logs` | Tail container logs |
| `dx shell` / `dx bash` | Open an interactive shell inside the app container |
| `dx exec <cmd> [args]` | Run a command inside the app container |
| `dx list` / `dx ls` | List stack containers |
| `dx test [args]` | Run PHPUnit tests inside the container |
**`dx up` flags**
| Flag | Description |
|---|---|
| `--sync-hosts` | Sync domain entries to `/etc/hosts` before starting |
| `--skip-proxy` | Do not ensure the reverse proxy is running |
| `--recreate` | Force container recreation |
| `--build` | Rebuild the image before starting |
| `--foreground` | Run in foreground (don't detach) |
| `--debug` | Enable shell trace output |
**`dx down` flags**
| Flag | Description |
|---|---|
| `--volumes` | Also remove Docker volumes |
| `--orphans` | Remove orphan containers |
| `--debug` | Enable shell trace output |
---
### Framework console
| Command | Description |
|---|---|
| `dx console <cmd> [args]` | Run a framework console command |
| `dx yii <cmd>` | Alias for Yii2 (`php yii <cmd>`) |
| `dx artisan <cmd>` | Alias for Laravel (`php artisan <cmd>`) |
---
### Migration shortcuts
These are resolved by the active framework driver.
**Yii2**
| Command | Description |
|---|---|
| `dx migrate` | Run all pending migrations |
| `dx migrate-create <name>` | Generate a new timestamped migration file in `console/migrations/` |
| `dx migrate-down [n]` | Revert `n` migrations (default: 1) |
| `dx migrate-new` | Show pending migrations |
| `dx migrate-history` | Show migration history |
**Laravel**
| Command | Description |
|---|---|
| `dx migrate` | Run all pending migrations |
| `dx migrate-create <name>` | Create a migration via `artisan make:migration` |
| `dx migrate-rollback [--step=N]` | Revert migrations |
| `dx migrate-status` | Show migration status |
| `dx migrate-fresh` | Drop all tables and re-run all migrations |
---
### Apache
| Command | Description |
|---|---|
| `dx apache validate` | Validate vhost configuration |
| `dx apache reload` | Graceful reload (picks up config changes without downtime) |
| `dx apache restart` | Full restart |
| `dx apache vhosts` | List all loaded virtual hosts |
| `dx apache modules` | List all loaded Apache modules |
| `dx apache version` | Show Apache version |
| `dx apache logs [error\|access]` | Tail error or access logs |
---
### Proxy (Traefik)
| Command | Description |
|---|---|
| `dx proxy start` | Start the Traefik reverse proxy |
| `dx proxy stop` | Stop the proxy |
| `dx proxy restart` | Restart the proxy |
| `dx proxy status` | Show proxy status |
| `dx proxy logs` | Tail proxy logs |
---
### Network & hosts
| Command | Description |
|---|---|
| `dx network status` | Show resolved port assignments |
| `dx network resolve` | Re-run port resolution and show changes |
| `dx network hosts` | Alias for the `hosts` command |
| `dx hosts sync` | Write project domain entries to `/etc/hosts` (requires root/sudo) |
| `dx hosts remove` | Remove project entries from `/etc/hosts` |
| `dx hosts list` | Show current `/etc/hosts` entries for this project |
| `dx hosts preview` | Preview entries that would be written |
---
### Workspace
| Command | Description |
|---|---|
| `dx workspace` / `dx ws` | Drop into a subshell where all `dx` commands and framework driver commands are available without the `dx` prefix |
---
## Global flags
These flags are accepted by all commands:
| Flag | Description |
|---|---|
| `--framework=NAME` | Override auto-detected framework |
| `--debug` | Enable shell trace output (`set -x`) |
---
## Environments
Three built-in environments:
| Name | Use |
|---|---|
| `dev` | Development (default)
| `qly` | Quality |
| `prd` | Production |
Environment variables are layered in order:
1. `dxkit/env/globals.env` — shared defaults, source-controlled
2. `.project/artifacts/env/${ENVIRONMENT}.env` — local overrides, gitignored
3. `.project/artifacts/env/${ENVIRONMENT}.resolved.env` — generated resolved values, gitignored
Resolved port assignments are written to `.project/artifacts/ports` and loaded back on subsequent runs.
Port values in env files may use the `auto:PORT` syntax — dxkit will find the next available port starting from that number.
---
## Framework support
Framework detection reads `composer.json` at project root.
| Framework | Detected by | Console binary | `dx console` alias |
|---|---|---|---|
| Yii2 Advanced | `yiisoft/yii2` + advanced template structure | `yii` | `dx yii` |
| Yii2 Basic | `yiisoft/yii2` + basic template structure | `yii` | `dx yii` |
| Laravel | `laravel/framework` | `artisan` | `dx artisan` |
Use `--framework=NAME` to override detection (e.g. `--framework=yii2-advanced`).
---
## Architecture
For contributors and maintainers.
```
dx # project-level entrypoint; defines aliases, calls dx::dispatch()
dxkit/
├── bootstrap.sh # loads core.sh, exports PROJECT_ROOT, defines dx::load_modules()
├── core.sh # sources all core subsystems in order
├── core/ # low-level infrastructure
│ ├── platform.sh # OS detection, privilege elevation
│ ├── context.sh # execution context management
│ ├── utils.sh # general utilities
│ ├── string.sh # string helpers
│ ├── hook.sh # pre/post hook system
│ ├── module.sh # module loader
│ ├── command.sh # command registration framework
│ ├── loader.sh # dynamic file loader
│ ├── flag.sh # flag/argument parsing
│ └── runtime/runtime.sh # runtime abstraction entry
├── commands/ # one file per command, loaded dynamically by the dispatcher
│ ├── build.command.sh
│ ├── init.command.sh
│ ├── setup.command.sh
│ ├── console.command.sh
│ ├── apache.command.sh
│ ├── hosts.command.sh
│ ├── network.command.sh
│ ├── proxy.command.sh
│ ├── workspace.command.sh
│ ├── docker/ # low-level docker subcommands
│ └── runtime/ # runtime-abstracted subcommands (used by default)
├── modules/ # reusable function libraries sourced at startup
│ ├── app.module.sh # framework abstraction layer (scaffold, init, config, console)
│ ├── docker.module.sh # Docker and Docker Compose wrappers
│ ├── env.module.sh # environment variable loading and derivation
│ ├── log.module.sh # structured logging with icons and context prefixes
│ ├── fs.module.sh # cross-platform file write and sed utilities
│ ├── network.module.sh # port resolution and persistence
│ ├── apache.module.sh # Apache management functions
│ ├── proxy.module.sh # proxy abstraction (delegates to docker/proxy.module.sh)
│ ├── artifact.module.sh # runtime artifact directory management
│ ├── composer.module.sh # composer operations (install, update, require, auth)
│ ├── template.module.sh # envsubst-based template rendering
│ ├── yii.module.sh # Yii2 shared utilities (exec, env name mapping)
│ ├── phpstorm.module.sh # PhpStorm datasource config generation
│ ├── docker/
│ │ ├── db.module.sh # database port/path/env helpers (MySQL + MSSQL)
│ │ └── proxy.module.sh # Traefik container lifecycle and label generation
│ └── framework/
│ ├── yii2-advanced.module.sh
│ ├── yii2-basic.module.sh
│ └── laravel.module.sh
├── drivers/ # framework-specific command registration
│ ├── dispatcher.sh # resolves framework → driver, populates DRIVER_COMMANDS
│ ├── yii2/
│ │ ├── driver.sh # registers yii, migrate-* commands
│ │ └── migrate.sh # migration file generation helpers
│ └── laravel/
│ ├── driver.sh # registers artisan, migrate-* commands
│ └── migrate.sh # artisan make:migration wrappers
├── runtime/ # runtime abstraction (Docker vs. native execution)
│ ├── runtime.sh # selects active runtime
│ ├── docker.runtime.sh # implements runtime::exec via docker compose exec
│ └── native.runtime.sh # implements runtime::exec directly on host
├── templates/ # envsubst-rendered config templates
│ ├── docker/
│ │ ├── Dockerfile.template
│ │ └── docker-compose.yml.template
│ ├── apache/
│ │ ├── yii2-advanced/vhosts.conf.template
│ │ └── yii2-basic/vhosts.conf.template
│ ├── yii2-advanced/
│ │ ├── main-local.php.template
│ │ └── components/ # db.mysql, db.mssql, log php templates
│ └── yii2-basic/
│ ├── db.mysql.php.template
│ └── db.mssql.php.template
├── env/ # layered environment variable files
│ ├── globals.env
│ ├── dev.env
│ ├── qly.env
│ └── prd.env
└── docker/
└── entrypoint.sh # container entrypoint script
```
### How command dispatch works
1. `dx <command> [args]` calls `dx::dispatch()`
2. Aliases in `dx` are resolved first (e.g. `sh``runtime/shell`, `ls``runtime/list`)
3. Built-ins (`help`, `elevate`) are handled inline
4. Driver commands registered in `DRIVER_COMMANDS` (via `dxkit/drivers/dispatcher.sh`) are checked next
5. Remaining commands are matched to files in `dxkit/commands/` and loaded dynamically
### Adding a new command
1. Create `dxkit/commands/<name>.command.sh` with a function `command::<name>()`
2. Optionally register an alias in `dx` under the alias map
3. For framework-specific commands, register the command name in the driver file (`dxkit/drivers/<framework>/driver.sh`)

156
dx Normal file
View file

@ -0,0 +1,156 @@
#!/usr/bin/env bash
set -Eeuo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/dxkit/bootstrap.sh"
# ============================================
# Alias map (commands)
# ============================================
declare -A CMD_ALIASES=(
[up]=runtime/up
[down]=runtime/down
[logs]=runtime/logs
[list]=runtime/list
[ls]=runtime/list
[list-stack]=runtime/list
[list-containers]=runtime/list
[exec]=runtime/exec
[shell]=runtime/shell
[sh]=runtime/shell
[bash]=runtime/shell
[test]=runtime/test
[net]=network
[ws]=workspace
)
# ============================================
# Modules
# ============================================
load_module env
load_module log
load_module fs
load_module docker
load_module network
load_module artifact
# ============================================
# Dispatch
# ============================================
function dx::resolve_alias() {
local cmd="$1"
echo "${CMD_ALIASES[$cmd]:-$cmd}"
}
function dx::dispatch() {
local raw_cmd="${1:-help}"
shift || true
local cmd
cmd="$(dx::resolve_alias "$raw_cmd")"
# Special built-ins that are not file-based commands
case "$cmd" in
help) dx::help; return ;;
elevate) platform::elevate "$@"; return ;;
esac
if driver::has_command "$cmd"; then
driver::run "$cmd" "$@"
return
fi
# Dynamic: load command file and call <cmd>::run
if load_command "$cmd"; then
if command::exists "${cmd}"; then
command::run "${cmd}" "$@"
else
log::error "Command file for '${cmd}' loaded but '${cmd}::run' is not defined"
exit 1
fi
else
log::error "Unknown command: '${raw_cmd}'"
echo "Run '$(basename "$0") help' to see the available commands." >&2
exit 1
fi
}
# ============================================
# Help
# ============================================
function dx::help() {
local console_name
console_name="$(app::console_name)"
local framework
framework="$(app::framework)"
local console_cmd
console_cmd="${console_name} | console"
cat <<HELP
Usage: $(basename "$0") <command> [args...]
Active framework: ${framework}
Commands:
up Start stack
build Build stack
down Stop stack
logs Tail logs
list | ls List stack containers
shell | bash Enter container shell
exec <cmd> Run command inside container
${console_cmd} Run framework console command
init [env] Initialize project for environment
setup Build and init in one step
test Run tests
apache <cmd> Manage Apache web server
hosts <cmd> Manage /etc/hosts entries
proxy <cmd> Manage reverse proxy
network | net <cmd> Network utilities
Global flags:
--framework=NAME Override active framework
--debug Enable shell trace
Examples:
$(basename "$0") up
$(basename "$0") build
$(basename "$0") ${console_name} migrate
$(basename "$0") init prd
$(basename "$0") setup
$(basename "$0") exec ls -la
$(basename "$0") shell
$(basename "$0") --framework=laravel build
HELP
}
# ============================================
# Main
# ============================================
function main() {
local args=()
for arg in "$@"; do
case "$arg" in
--framework=*) export APP_FRAMEWORK="${arg#*=}" ;;
--debug) set -x ;;
*) args+=("$arg") ;;
esac
done
load_module app # app::on_load loads framework/* and registers console alias
source "$(ctx::dxkit)/drivers/dispatcher.sh"
driver::load
env::load_env_files
dx::dispatch "${args[@]+"${args[@]}"}"
}
main "$@"

34
dxkit/bootstrap.sh Normal file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env bash
# ============================================
# Directories
# ============================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Only set PROJECT_ROOT if not already exported by parent
if [[ -z "${PROJECT_ROOT:-}" ]]; then
readonly PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
fi
export PROJECT_ROOT
# ============================================
# Load Core
# ============================================
source "$SCRIPT_DIR/core.sh"
platform::detect
# ============================================
# Module Loader
# ============================================
function dx::load_modules() {
load_module env
load_module log
load_module fs
load_module docker
load_module network
load_module artifact
}

View file

@ -0,0 +1,128 @@
#!/usr/bin/env bash
# commands/apache.command.sh
#
# Manages the Apache web server.
#
# Usage:
# dx apache validate — validate vhost configuration
# dx apache reload — graceful reload (picks up vhost changes)
# dx apache restart — full restart
# dx apache vhosts — list all loaded virtual hosts
# dx apache modules — list all loaded Apache modules
# dx apache version — show Apache version
# dx apache logs <type> — tail error or access logs
# ============================================
# Lifecycle
# ============================================
function cmd::apache::on_load() {
load_module apache
flag::register --debug
}
# ============================================
# Public entrypoint
# ============================================
function cmd::apache::run() {
local subcmd="${1:-help}"
shift || true
case "$subcmd" in
validate) cmd::apache::validate ;;
reload) cmd::apache::reload ;;
restart) cmd::apache::restart ;;
vhosts) cmd::apache::vhosts ;;
modules) cmd::apache::modules ;;
version) cmd::apache::version ;;
logs) cmd::apache::logs "$@" ;;
help) cmd::apache::help ;;
*)
log::error "Unknown subcommand: '${subcmd}'"
cmd::apache::help
return 1
;;
esac
}
# ============================================
# Help
# ============================================
function cmd::apache::help() {
cat <<EOF
Usage: dx apache <subcommand>
Manages the Apache web server.
Subcommands:
validate Validate vhost configuration
reload Graceful reload (picks up config changes)
restart Full restart
vhosts List all loaded virtual hosts
modules List all loaded Apache modules
version Show Apache version
logs [type] Tail logs (error | access, default: error)
Examples:
dx apache validate
dx apache reload
dx apache vhosts
dx apache logs access
EOF
}
# ============================================
# Subcommands
# ============================================
function cmd::apache::validate() {
log::info "Validating Apache configuration..."
if apache::validate_config; then
log::success "Configuration is valid"
else
log::error "Configuration has errors"
return 1
fi
}
function cmd::apache::reload() {
log::info "Reloading Apache..."
apache::reload
log::success "Apache reloaded"
}
function cmd::apache::restart() {
log::info "Restarting Apache..."
apache::restart
log::success "Apache restarted"
}
function cmd::apache::vhosts() {
log::info "Loaded virtual hosts:"
apache::list_vhosts
}
function cmd::apache::modules() {
log::info "Loaded Apache modules:"
apache::list_modules
}
function cmd::apache::version() {
apache::version
}
function cmd::apache::logs() {
local type="${1:-error}"
case "$type" in
error) apache::logs::error ;;
access) apache::logs::access ;;
*)
log::error "Unknown log type: '${type}' (must be 'error' or 'access')"
return 1
;;
esac
}

View file

@ -0,0 +1,177 @@
#!/usr/bin/env bash
# commands/build.command.sh
#
# Builds the Docker image and compose stack for a given environment.
#
# Usage:
# dx build [flags]
#
# Flags:
# --dev / --qly / --prd Target environment (default: dev)
# --project=NAME Override project name
# --port=PORT Override app port (e.g. 8082, auto:8080)
# --db=TYPE Override DB engine (mysql | mssql)
# --db-port=PORT Override DB port
# --sync-hosts Sync domain to /etc/hosts (requires root)
# --no-vhosts Skip Apache vhost generation
# --no-interact Non-interactive mode
# --debug Enable shell trace (set -x)
# ============================================
# Lifecycle
# ============================================
function cmd::build::on_load() {
load_module template
load_module apache
flag::register \
--sync-hosts \
--no-vhosts \
--no-interact \
--scaffold \
--framework \
--debug
}
# ============================================
# Public entrypoint
# ============================================
function cmd::build::run() {
local flags=()
for arg in "$@"; do
flags+=("$arg")
done
if ! flag::parse "${flags[@]+"${flags[@]}"}"; then
cmd::build::help
return 1
fi
# CLI overrides — applied after env files so flags win
[[ -n "${PROJECT_NAME_OVERRIDE:-}" ]] && export PROJECT_NAME="$PROJECT_NAME_OVERRIDE"
[[ -n "${APP_PORT_OVERRIDE:-}" ]] && export APP_PORT="$APP_PORT_OVERRIDE"
[[ -n "${DB_ENGINE_OVERRIDE:-}" ]] && export DB_ENGINE="$DB_ENGINE_OVERRIDE"
[[ -n "${DB_PORT_OVERRIDE:-}" ]] && export DB_PORT="$DB_PORT_OVERRIDE"
# ============================================
# Project scaffolding
# ============================================
if ! app::exists || flag::enabled --scaffold; then
app::scaffold "$(ctx::root)"
fi
# ============================================
# Runtime artifacts
# ============================================
artifact::init
# ============================================
# Network & Virtual Hosts
# ============================================
network::ports::prepare
if flag::enabled --sync-hosts && ! flag::enabled --no-interact; then
load_command hosts
hosts::sync
fi
if ! flag::enabled --no-vhosts; then
apache::generate_vhosts
fi
# ============================================
# Docker image
# ============================================
runtime::run_if BUILD_IMAGE docker::image::generate || {
log::error "Failed to generate Docker image (${APP_IMAGE}:${ENVIRONMENT})"
return 1
}
runtime::run_if BUILD_IMAGE docker::image::build || {
log::error "Failed to build Docker image (${APP_IMAGE}:${ENVIRONMENT})"
return 1
}
# ============================================
# Docker compose stack
# ============================================
runtime::run_if BUILD_STACK docker::compose::generate || {
log::error "Failed to generate compose stack (${ENVIRONMENT})"
return 1
}
runtime::run_if BUILD_STACK docker::compose::build || {
log::error "Failed to build compose stack (${ENVIRONMENT})"
return 1
}
# ============================================
# Done
# ============================================
cmd::build::_show_networking
log::success "Build complete!"
flag::scaffold_defaults_file
cmd::build::_hint_next_steps
}
# ============================================
# Help
# ============================================
function cmd::build::help() {
cat <<EOF
Usage: dx build [flags]
Generates and builds the Docker image and compose stack.
Flags:
--dev / --qly / --prd Target environment (default: dev)
--project=NAME Override project name
--port=PORT Override app port
--db=TYPE Override DB engine (mysql | mssql)
--db-port=PORT Override DB port
--sync-hosts Sync domain to /etc/hosts
--no-vhosts Skip vhost generation
--no-interact Non-interactive mode
--debug Shell trace
Examples:
dx build
dx build --qly
dx build --port=8082
dx build --sync-hosts
dx build --project=myapp --db=mssql
EOF
}
# ============================================
# Private
# ============================================
function cmd::build::_show_networking() {
printf "\n🌐 Accessible on: http://%s\n\n" "$DOMAIN"
echo "🔌 Ports:"
echo " App: ${APP_PORT}"
echo " DB: $(docker::db::external_port)"
printf "\n"
}
function cmd::build::_hint_next_steps() {
cat <<EOF
Next steps:
dx init Initialize environment
dx up Start the stack
EOF
}

View file

@ -0,0 +1,56 @@
#!/usr/bin/env bash
# commands/console.command.sh
#
# Framework-agnostic console command runner.
# Delegates to the active framework's console implementation.
#
# Aliased automatically to the framework's native name (yii, artisan etc.)
# via app::on_load — so both forms work:
#
# dx console migrate
# dx yii migrate (Yii2)
# dx artisan migrate (Laravel)
# ============================================
# Lifecycle
# ============================================
function cmd::console::on_load() {
flag::register --debug
}
# ============================================
# Public entrypoint
# ============================================
function cmd::console::run() {
if [[ $# -eq 0 ]]; then
cmd::console::help
return 0
fi
app::console "$@"
}
# ============================================
# Help
# ============================================
function cmd::console::help() {
local console_name
console_name="$(app::console_name)"
cat <<EOF
Usage: dx console <command> [args...]
dx ${console_name} <command> [args...]
Runs a console command inside the app container via the active framework.
Active framework: $(app::framework)
Examples:
dx console migrate
dx console db:seed
dx ${console_name} migrate
dx ${console_name} cache/flush
EOF
}

View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
# commands/docker/down.command.sh
function cmd::docker::down::on_load() {
flag::register --debug
}
function cmd::docker::down::run() {
flag::parse "$@" || { cmd::docker::down::help; return 1; }
docker::compose::down
}
function cmd::docker::down::help() {
cat <<EOF
Usage: dx down [flags]
Stops the stack.
Flags:
--debug Shell trace
Examples:
dx down
EOF
}

View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# commands/docker/exec.command.sh
function cmd::docker::exec::on_load() {
flag::register --debug
}
function cmd::docker::exec::run() {
if [[ $# -eq 0 ]]; then
cmd::docker::exec::help
return 1
fi
runtime::exec "$@"
}
function cmd::docker::exec::help() {
cat <<EOF
Usage: dx exec <cmd> [args...]
Runs a command inside the app container.
Examples:
dx exec ls -la
dx exec php --version
dx exec composer install
EOF
}

View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# commands/docker/list.command.sh
function cmd::docker::list::on_load() {
flag::register --debug
}
function cmd::docker::list::run() {
flag::parse "$@" || { cmd::docker::list::help; return 1; }
docker::list_containers
}
function cmd::docker::list::help() {
cat <<EOF
Usage: dx list [flags]
Lists stack containers.
Aliases: ls, list-stack, list-containers
Flags:
--debug Shell trace
Examples:
dx list
dx ls
EOF
}

View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
# commands/docker/logs.command.sh
function cmd::docker::logs::on_load() {
flag::register --debug
}
function cmd::docker::logs::run() {
flag::parse "$@" || { cmd::docker::logs::help; return 1; }
docker::compose::logs
}
function cmd::docker::logs::help() {
cat <<EOF
Usage: dx logs [flags]
Tails stack logs.
Flags:
--debug Shell trace
Examples:
dx logs
EOF
}

View file

@ -0,0 +1,24 @@
#!/usr/bin/env bash
# commands/docker/shell.command.sh
function cmd::docker::shell::on_load() {
flag::register --debug
}
function cmd::docker::shell::run() {
runtime::shell
}
function cmd::docker::shell::help() {
cat <<EOF
Usage: dx shell
Opens an interactive shell inside the app container.
Aliases: bash
Examples:
dx shell
dx bash
EOF
}

View file

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# commands/docker/test.command.sh
function cmd::docker::test::on_load() {
flag::register --debug
}
function cmd::docker::test::run() {
runtime::exec php vendor/bin/phpunit "$@"
}
function cmd::docker::test::help() {
cat <<EOF
Usage: dx test [args...]
Runs PHPUnit tests inside the app container.
Examples:
dx test
dx test --filter MyTest
dx test tests/unit
EOF
}

View file

@ -0,0 +1,42 @@
#!/usr/bin/env bash
# commands/docker/up.command.sh
function cmd::docker::up::on_load() {
flag::register \
--sync-hosts \
--skip-proxy \
--debug
}
function cmd::docker::up::run() {
flag::parse "$@" || { cmd::docker::up::help; return 1; }
if flag::enabled --sync-hosts; then
load_command hosts
cmd::hosts::sync
fi
if ! flag::enabled --skip-proxy; then
proxy::ensure
fi
docker::compose::up
}
function cmd::docker::up::help() {
cat <<EOF
Usage: dx up [flags]
Starts the stack.
Flags:
--sync-hosts Sync domain to /etc/hosts before starting
--skip-proxy Skip proxy startup
--debug Shell trace
Examples:
dx up
dx up --sync-hosts
dx up --skip-proxy
EOF
}

View file

@ -0,0 +1,203 @@
#!/usr/bin/env bash
# commands/hosts.command.sh
#
# Manages /etc/hosts entries for the project.
#
# Usage:
# dx hosts sync — write project entries to /etc/hosts (requires root)
# dx hosts remove — remove project entries from /etc/hosts (requires root)
# dx hosts list — show current /etc/hosts entries for this project
# dx hosts preview — preview entries that would be written
#
# Also accessible via:
# dx network hosts <subcommand>
# ============================================
# Lifecycle
# ============================================
function cmd::hosts::on_load() {
flag::register \
--debug
}
# ============================================
# Public entrypoint
# ============================================
function cmd::hosts::run() {
local subcmd="${1:-help}"
shift || true
case "$subcmd" in
sync) cmd::hosts::sync "$@" ;;
remove) cmd::hosts::remove "$@" ;;
list) cmd::hosts::list "$@" ;;
preview) cmd::hosts::preview "$@" ;;
help) cmd::hosts::help ;;
*)
log::error "Unknown subcommand: '${subcmd}'"
cmd::hosts::help
return 1
;;
esac
}
# ============================================
# Help
# ============================================
function cmd::hosts::help() {
cat <<EOF
Usage: dx hosts <subcommand>
Manages /etc/hosts entries for ${PROJECT_NAME}.
Subcommands:
sync Write project entries to /etc/hosts (requires root)
remove Remove project entries from /etc/hosts (requires root)
list Show current entries in /etc/hosts for this project
preview Preview entries that would be written
Examples:
dx hosts sync
dx hosts remove
dx hosts list
dx hosts preview
EOF
}
# ============================================
# Sync
# ============================================
function cmd::hosts::sync() {
local hosts_file
hosts_file="$(platform::hosts_file)"
platform::require_privileges || {
log::warn "$(platform::elevation_fail)"
log::error "Could not sync hosts — permission denied"
return 1
}
local block
block="$(cmd::hosts::_build_block)"
[[ -f "$hosts_file" ]] || touch "$hosts_file" || return 1
local tmp
tmp="$(fs::replace_block \
"$hosts_file" \
"$(cmd::hosts::_marker)" \
"$(cmd::hosts::_end_marker)" \
"$block"
)" || return 1
fs::write_file "$tmp" "$hosts_file"
log::success "Hosts synced for ${PROJECT_NAME}"
cmd::hosts::_show_entries "$block"
}
# ============================================
# Remove
# ============================================
function cmd::hosts::remove() {
local hosts_file
hosts_file="$(platform::hosts_file)"
platform::require_privileges || {
log::warn "$(platform::elevation_fail)"
log::error "Could not remove hosts — permission denied"
return 1
}
if ! grep -q "$(cmd::hosts::_marker)" "$hosts_file" 2>/dev/null; then
log::info "No entries found for ${PROJECT_NAME} in $(platform::hosts_file)"
return 0
fi
local tmp
tmp="$(fs::replace_block \
"$hosts_file" \
"$(cmd::hosts::_marker)" \
"$(cmd::hosts::_end_marker)" \
""
)" || return 1
fs::write_file "$tmp" "$hosts_file"
log::success "Hosts entries removed for ${PROJECT_NAME}"
}
# ============================================
# List
# ============================================
function cmd::hosts::list() {
local hosts_file
hosts_file="$(platform::hosts_file)"
if ! grep -q "$(cmd::hosts::_marker)" "$hosts_file" 2>/dev/null; then
log::info "No entries found for ${PROJECT_NAME} in ${hosts_file}"
return 0
fi
log::info "Current entries for ${PROJECT_NAME} in ${hosts_file}:"
printf "\n"
# Extract block between markers
awk "/$(cmd::hosts::_marker)/,/$(cmd::hosts::_end_marker)/" "$hosts_file" \
| grep -v "^#"
printf "\n"
}
# ============================================
# Preview
# ============================================
function cmd::hosts::preview() {
log::info "Entries that would be written for ${PROJECT_NAME}:"
printf "\n"
cmd::hosts::_generate_entries
printf "\n"
}
# ============================================
# Private
# ============================================
function cmd::hosts::_marker() { echo "# >>> ${PROJECT_NAME} >>>"; }
function cmd::hosts::_end_marker() { echo "# <<< ${PROJECT_NAME} <<<"; }
function cmd::hosts::_generate_entries() {
local ip="127.0.0.1"
local domains=(
"$DOMAIN"
"$BACKEND_DOMAIN"
)
for domain in "${domains[@]}"; do
echo "${ip} ${domain}"
done
}
function cmd::hosts::_build_block() {
local entries
entries="$(cmd::hosts::_generate_entries | tr -d '\r')"
printf "%s\n%s\n%s" \
"$(cmd::hosts::_marker)" \
"$entries" \
"$(cmd::hosts::_end_marker)"
}
function cmd::hosts::_show_entries() {
local entries="$1"
printf "\n"
echo "$entries"
printf "\n"
}

View file

@ -0,0 +1,150 @@
#!/usr/bin/env bash
# commands/init.command.sh
#
# Initializes the project for a given environment.
# Brings the stack to a runnable state: containers up, vendors installed,
# proxy started, framework initialized and configured.
#
# Usage:
# dx init [env] [flags]
#
# Flags:
# --skip-vendors Skip composer install
# --skip-framework-init Skip framework init
# --skip-framework-config Skip framework config generation
# --debug Enable shell trace
#
# ============================================
# Public entrypoint
# ============================================
function cmd::init::on_load() {
flag::register \
--skip-vendors \
--skip-framework-init \
--skip-framework-config \
--skip-proxy \
--debug
}
function cmd::init::run() {
local env=""
local flags=()
for arg in "$@"; do
case "$arg" in
--*) flags+=("$arg") ;;
*) [[ -z "$env" ]] && env="$arg" ;;
esac
done
if [[ ${#flags[@]} -gt 0 ]]; then
if ! flag::parse "${flags[@]}"; then
cmd::init::help
return 1
fi
fi
env="${env:-${ENVIRONMENT:-dev}}"
local framework_env
framework_env="$(app::resolve_env "$env")"
log::env "Initializing environment: ${env} (${framework_env})..."
# ============================================
# Docker
# ============================================
docker::compose::up -d >/dev/null
# ============================================
# Vendors
# ============================================
if ! flag::enabled --skip-vendors; then
app::install_vendors
else
log::fs_warning "$(app::vendor_skip_message)"
fi
# ============================================
# Proxy
# ============================================
if ! flag::enabled --skip-proxy; then
log::run_step network info "Starting proxy..." \
proxy::start
else
log::network_warning "Skipping Proxy initialization... (--skip-proxy)"
fi
# ============================================
# Framework
# ============================================
if ! flag::enabled --skip-framework-init; then
app::init "$env"
else
log::env_warning "$(app::init_skip_message)"
fi
if ! flag::enabled --skip-framework-config; then
app::config
else
log::env_warning "$(app::config_skip_message)"
fi
# ============================================
# Done
# ============================================
log::env_success "Environment '${env}' is ready!"
flag::scaffold_defaults_file
cmd::init::_hint_post_init
}
# ============================================
# Help
# ============================================
function cmd::init::help() {
cat <<EOF
Usage: dx init [env] [flags]
Brings the project to a runnable state for the given environment.
Arguments:
env dev | qly | prd (default: dev)
Flags:
--skip-vendors Skip vendor install
--skip-framework-init Skip framework init
--skip-framework-config Skip framework config generation
--debug Shell trace
EOF
app::post_init_hint
cat <<EOF
Examples:
dx init
dx init qly
dx init --skip-vendors
dx init qly --skip-framework-init --skip-framework-config
EOF
}
# ============================================
# Private
# ============================================
function cmd::init::_hint_post_init() {
echo ""
app::post_init_hint;
echo ""
}

View file

@ -0,0 +1,158 @@
#!/usr/bin/env bash
# commands/network.command.sh
#
# Network diagnostics, port management, and host entries.
#
# Usage:
# dx network status — show port assignments and their status
# dx network resolve — re-run port resolution and show changes
# dx network hosts <subcmd> — manage /etc/hosts entries (delegates to hosts command)
#
# Hosts subcommands are also available directly:
# dx hosts sync
# dx hosts list
# dx hosts remove
# ============================================
# Lifecycle
# ============================================
function cmd::network::on_load() {
flag::register \
--debug
}
# ============================================
# Public entrypoint
# ============================================
function cmd::network::run() {
local subcmd="${1:-help}"
shift || true
case "$subcmd" in
status) cmd::network::status::run "$@" ;;
resolve) cmd::network::resolve::run "$@" ;;
hosts)
load_command hosts
hosts::run "$@"
;;
help) cmd::network::help ;;
*)
log::error "Unknown subcommand: '${subcmd}'"
cmd::network::help
return 1
;;
esac
}
# ============================================
# Help
# ============================================
function cmd::network::help() {
cat <<EOF
Usage: dx network <subcommand>
Subcommands:
status Show current port assignments and their status
resolve Re-run port resolution and show what changed
hosts <subcmd> Manage /etc/hosts entries
Hosts subcommands:
hosts sync Sync project domain to /etc/hosts
hosts list Show current /etc/hosts entries for this project
hosts remove Remove project entries from /etc/hosts
Examples:
dx network status
dx network resolve
dx network hosts sync
dx hosts sync (shorthand)
EOF
}
# ============================================
# Status
# ============================================
function cmd::network::status::run() {
log::info "Port status for ${PROJECT_NAME}..."
printf "\n"
printf "%-20s %-10s %s\n" "Service" "Port" "Status"
printf "%-20s %-10s %s\n" "-------" "----" "------"
cmd::network::status::_report "App" "$APP_PORT"
cmd::network::status::_report "Database" "$(docker::db::external_port)"
printf "\n"
}
function cmd::network::status::_report() {
local label="$1"
local port="$2"
local status
if ! network::ports::in_use "$port"; then
status="✓ free"
else
local container
container="$(docker ps --format '{{.Names}}' | grep "^${PROJECT_NAME}" | head -1)"
if [[ -n "$container" ]]; then
status="✓ in use by ${PROJECT_NAME}"
else
status="⚠️ in use by another process — run: dx network resolve"
fi
fi
printf "%-20s %-10s %s\n" "$label" "$port" "$status"
}
# ============================================
# Resolve
# ============================================
function cmd::network::resolve::run() {
log::info "Resolving ports for ${PROJECT_NAME}..."
local old_app_port="$APP_PORT"
local old_db_port
old_db_port="$(docker::db::external_port)"
network::ports::resolve_many APP_PORT DB_PORT
local new_db_port
new_db_port="$(docker::db::external_port)"
printf "\n"
printf "%-20s %-15s %-15s %s\n" "Service" "Current" "Resolved" "Status"
printf "%-20s %-15s %-15s %s\n" "-------" "-------" "--------" "------"
cmd::network::resolve::_report "App" "$old_app_port" "$APP_PORT"
cmd::network::resolve::_report "Database" "$old_db_port" "$new_db_port"
printf "\n"
if [[ "$old_app_port" != "$APP_PORT" || "$old_db_port" != "$new_db_port" ]]; then
log::warn "Ports changed — run 'dx build' to apply"
else
log::success "No changes — all ports are available"
fi
}
function cmd::network::resolve::_report() {
local label="$1"
local old="$2"
local resolved="$3"
local status
if [[ "$old" == "$resolved" ]]; then
status="✓ unchanged"
else
status="⚠️ remapped: ${old}${resolved}"
fi
printf "%-20s %-15s %-15s %s\n" "$label" "$old" "$resolved" "$status"
}

View file

@ -0,0 +1,121 @@
#!/usr/bin/env bash
# commands/proxy.command.sh
#
# Manages the reverse proxy service.
#
# Usage:
# dx proxy start — start the proxy
# dx proxy stop — stop the proxy
# dx proxy restart — restart the proxy
# dx proxy status — show proxy status
# dx proxy logs — tail proxy logs
# ============================================
# Lifecycle
# ============================================
function cmd::proxy::on_load() {
flag::register --debug
}
# ============================================
# Public entrypoint
# ============================================
function cmd::proxy::run() {
local subcmd="${1:-help}"
shift || true
case "$subcmd" in
start) cmd::proxy::start "$@" ;;
stop) cmd::proxy::stop "$@" ;;
restart) cmd::proxy::restart "$@" ;;
status) cmd::proxy::status "$@" ;;
logs) cmd::proxy::logs "$@" ;;
help) cmd::proxy::help ;;
*)
log::error "Unknown subcommand: '${subcmd}'"
cmd::proxy::help
return 1
;;
esac
}
# ============================================
# Help
# ============================================
function cmd::proxy::help() {
cat <<EOF
Usage: dx proxy <subcommand>
Manages the reverse proxy service (${PROXY_SERVICE}).
Subcommands:
start Start the proxy
stop Stop the proxy
restart Restart the proxy
status Show proxy status
logs Tail proxy logs
Examples:
dx proxy start
dx proxy stop
dx proxy restart
dx proxy status
dx proxy logs
EOF
}
# ============================================
# Subcommands
# ============================================
function cmd::proxy::start() {
if proxy::running; then
log::info "Proxy (${PROXY_SERVICE}) is already running"
return 0
fi
log::info "Starting proxy (${PROXY_SERVICE})..."
proxy::start
log::success "Proxy started"
}
function cmd::proxy::stop() {
if ! proxy::running; then
log::info "Proxy (${PROXY_SERVICE}) is not running"
return 0
fi
log::info "Stopping proxy (${PROXY_SERVICE})..."
proxy::stop
log::success "Proxy stopped"
}
function cmd::proxy::restart() {
log::info "Restarting proxy (${PROXY_SERVICE})..."
proxy::restart
log::success "Proxy restarted"
}
function cmd::proxy::status() {
printf "\n"
printf "%-20s %s\n" "Service:" "${PROXY_SERVICE}"
printf "%-20s %s\n" "Network:" "${PROXY_NETWORK}"
printf "%-20s %s\n" "Driver:" "$(proxy::driver)"
printf "%-20s " "Status:"
if proxy::running; then
echo "✓ running"
else
echo "⚠️ stopped"
fi
printf "\n"
}
function cmd::proxy::logs() {
log::info "Tailing proxy logs (${PROXY_SERVICE})..."
proxy::logs
}

View file

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# commands/runtime/down.command.sh
function cmd::runtime::down::on_load() {
flag::register \
--volumes \
--orphans \
--debug
}
function cmd::runtime::down::run() {
flag::parse "$@" || { cmd::runtime::down::help; return 1; }
runtime::down
}
function cmd::runtime::down::help() {
cat <<EOF
Usage: dx down [flags]
Stops the stack.
Flags:
--volumes Also remove volumes
--orphans Remove orphan containers
--debug Shell trace
Examples:
dx down
dx down --volumes
dx down --orphans
EOF
}

View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# commands/runtime/exec.command.sh
function cmd::runtime::exec::on_load() {
flag::register --debug
}
function cmd::runtime::exec::run() {
if [[ $# -eq 0 ]]; then
cmd::runtime::exec::help
return 1
fi
runtime::exec "$@"
}
function cmd::runtime::exec::help() {
cat <<EOF
Usage: dx exec <cmd> [args...]
Runs a command inside the runtime.
Examples:
dx exec ls -la
dx exec php --version
dx exec composer install
EOF
}

View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# commands/runtime/list.command.sh
function cmd::runtime::list::on_load() {
flag::register --debug
}
function cmd::runtime::list::run() {
flag::parse "$@" || { cmd::runtime::list::help; return 1; }
runtime::list
}
function cmd::runtime::list::help() {
cat <<EOF
Usage: dx list [flags]
Lists stack containers.
Aliases: ls, list-stack, list-containers
Flags:
--debug Shell trace
Examples:
dx list
dx ls
EOF
}

View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# commands/runtime/logs.command.sh
function cmd::runtime::logs::on_load() {
flag::register \
--follow \
--debug
}
function cmd::runtime::logs::run() {
flag::parse "$@" || { cmd::runtime::logs::help; return 1; }
runtime::logs
}
function cmd::runtime::logs::help() {
cat <<EOF
Usage: dx logs [flags]
Tails stack logs.
Flags:
--follow Follow log output
--debug Shell trace
Examples:
dx logs
dx logs --follow
EOF
}

View file

@ -0,0 +1,24 @@
#!/usr/bin/env bash
# commands/runtime/shell.command.sh
function cmd::runtime::shell::on_load() {
flag::register --debug
}
function cmd::runtime::shell::run() {
runtime::shell
}
function cmd::runtime::shell::help() {
cat <<EOF
Usage: dx shell
Opens an interactive shell inside the runtime.
Aliases: bash
Examples:
dx shell
dx bash
EOF
}

View file

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# commands/runtime/test.command.sh
function cmd::runtime::test::on_load() {
flag::register --debug
}
function cmd::runtime::test::run() {
runtime::exec php vendor/bin/phpunit "$@"
}
function cmd::runtime::test::help() {
cat <<EOF
Usage: dx test [args...]
Runs PHPUnit tests inside the runtime.
Examples:
dx test
dx test --filter MyTest
dx test tests/unit
EOF
}

View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
# commands/runtime/up.command.sh
function cmd::runtime::up::on_load() {
flag::register \
--sync-hosts \
--skip-proxy \
--recreate \
--build \
--foreground \
--debug
}
function cmd::runtime::up::run() {
flag::parse "$@" || { cmd::runtime::up::help; return 1; }
if flag::enabled --sync-hosts; then
load_command hosts
cmd::hosts::sync
fi
if ! flag::enabled --skip-proxy; then
proxy::ensure
fi
runtime::up
}
function cmd::runtime::up::help() {
cat <<EOF
Usage: dx up [flags]
Starts the stack.
Flags:
--sync-hosts Sync domain to /etc/hosts before starting
--skip-proxy Skip proxy startup
--recreate Force-recreate containers (drops existing state)
--build Rebuild images before starting
--foreground Run in foreground (don't detach)
--debug Shell trace
Examples:
dx up
dx up --recreate
dx up --build --recreate
dx up --sync-hosts
dx up --foreground
EOF
}

View file

@ -0,0 +1,27 @@
#!/usr/bin/env bash
load_command build
load_command init
function cmd::setup::run() {
if ! flag::parse "$@"; then
cmd::setup::help
return 1
fi
build::run
init::run "${ENVIRONMENT:-dev}"
}
function cmd::setup::help() {
cat <<EOF
Usage: dx setup [flags]
Runs build followed by init in a single step.
Accepts all flags from both commands.
$(cmd::build::help)
$(cmd::init::help)
EOF
}

View file

@ -0,0 +1,145 @@
#!/usr/bin/env bash
# ============================================
# Workspace Command
# ============================================
#
# Drops into a subshell with all driver commands
# AND all static dx commands available as plain
# functions — no 'dx' prefix needed.
#
# The prompt changes to signal you are inside
# the dx workspace.
#
# Usage:
# dx workspace
# dx ws
function cmd::workspace::run() {
local framework
framework="$(app::framework)"
local driver
driver="$(driver::resolve)"
local driver_file
driver_file="$(ctx::dxkit)/drivers/${driver}/driver.sh"
if [[ ! -f "$driver_file" ]]; then
log::error "No driver found for framework '${framework}' (resolved: ${driver})"
log::error "Cannot start dx workspace without an active driver."
return 1
fi
local rc_file
rc_file="$(mktemp /tmp/dx-workspace-rc.XXXXXX)"
cat > "$rc_file" <<RCEOF
# ============================================
# dx workspace rc — sourced for subshell only
# ============================================
# Carry over user's existing rc if present
if [[ -f "\$HOME/.bashrc" ]]; then
source "\$HOME/.bashrc" 2>/dev/null || true
elif [[ -f "\$HOME/.bash_profile" ]]; then
source "\$HOME/.bash_profile" 2>/dev/null || true
fi
# Load dxkit bootstrap — sources core, detects platform,
# applies MSYS2 guards, and defines dx::load_modules
source "$(ctx::dxkit)/bootstrap.sh"
# Set framework before loading app so app::on_load
# resolves the correct framework module
export APP_FRAMEWORK="${framework}"
# Load all standard modules (mirrors dx entrypoint)
dx::load_modules
load_module app
env::load_env_files
# Source the active driver so its functions are in scope
source "${driver_file}"
# ============================================
# Driver command wrappers (run in-process)
# e.g. migrate-create → yii2::migrate::create
# ============================================
$(
for cmd in "${!DRIVER_COMMANDS[@]}"; do
echo "function ${cmd}() { ${DRIVER_COMMANDS[$cmd]} \"\$@\"; }"
done
)
# ============================================
# Static dx command wrappers (subprocess)
# Uses \$PROJECT_ROOT/dx to avoid path mangling
# on Windows/MSYS2 — PROJECT_ROOT is exported
# by bootstrap so it is always available here.
# ============================================
$(
while IFS= read -r -d '' cmd_file; do
cmd_name="$(basename "$cmd_file" .command.sh)"
[[ "$cmd_name" == "workspace" ]] && continue
echo "function ${cmd_name}() { \"\$PROJECT_ROOT/dx\" ${cmd_name} \"\$@\"; }"
done < <(find "$(ctx::dxkit)/commands" -name "*.command.sh" -print0)
)
# ============================================
# Prompt
# ============================================
export PS1="\[\033[0;36m\][dx:${driver}]\[\033[0m\] \w \$ "
# ============================================
# Welcome
# ============================================
echo ""
echo " dx workspace — ${framework}"
echo " Type 'exit' to return to your shell."
echo ""
echo " Framework commands:"
$(
for cmd in $(echo "${!DRIVER_COMMANDS[@]}" | tr ' ' '\n' | sort); do
echo " echo \" ${cmd}\""
done
)
echo ""
RCEOF
bash --rcfile "$rc_file" -i
rm -f "$rc_file"
}
function cmd::workspace::help() {
local framework
framework="$(app::framework)"
cat <<EOF
Usage: dx workspace
dx ws
Drops into a subshell with all framework and dx commands
available without the 'dx' prefix. Type 'exit' to return.
Active framework: ${framework}
Inside dx workspace, all of these work without 'dx':
Framework commands:
$(
for cmd in $(echo "${!DRIVER_COMMANDS[@]}" | tr ' ' '\n' | sort); do
printf " %-28s\n" "$cmd"
done
)
Static dx commands:
$(
while IFS= read -r -d '' cmd_file; do
cmd_name="$(basename "$cmd_file" .command.sh)"
[[ "$cmd_name" == "workspace" ]] && continue
printf " %-28s\n" "$cmd_name"
done < <(find "$(ctx::dxkit)/commands" -name "*.command.sh" -print0)
)
Shorthand: dx ws
EOF
}

12
dxkit/core.sh Normal file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env bash
source "$SCRIPT_DIR/core/platform.sh"
source "$SCRIPT_DIR/core/context.sh"
source "$SCRIPT_DIR/core/utils.sh"
source "$SCRIPT_DIR/core/string.sh"
source "$SCRIPT_DIR/core/hook.sh"
source "$SCRIPT_DIR/core/module.sh"
source "$SCRIPT_DIR/core/command.sh"
source "$SCRIPT_DIR/core/loader.sh"
source "$SCRIPT_DIR/core/flag.sh"
source "$SCRIPT_DIR/runtime/runtime.sh"

29
dxkit/core/command.sh Normal file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
declare -A _LOADED_COMMANDS=() # Unused for now
readonly _COMMAND_NAMESPACE="cmd"
readonly _COMMAND_AUTO_LOAD_HOOK="on_load"
function command::namespace { echo "${_COMMAND_NAMESPACE}"; }
function command::fn { echo "$(command::namespace)::$(command::to_namespace "$1")::${2}"; }
function command::loaded() { [[ -n "${_LOADED_COMMANDS["$1"]:-}" ]]; }
function command::has_function() { declare -F "$(command::fn "$1" "$2")" >/dev/null 2>&1; }
function command::is_auto_load() { declare -F "$(command::fn "$1" on_load)" >/dev/null 2>&1; }
function command::to_namespace() { echo "${1//\//::}"; }
function command::exists() { command::has_function "$1" run; }
function command::run() {
local cmd="$1"
shift
hook::run "$cmd" pre
core::call_function "$(command::fn "$cmd" run)" "$@"
local exit_code=$?
if [[ $exit_code -eq 0 ]]; then
hook::run "$cmd" post
fi
return $exit_code
}

72
dxkit/core/context.sh Normal file
View file

@ -0,0 +1,72 @@
#!/usr/bin/env bash
_CTX_ROOT="$PROJECT_ROOT"
_CTX_CONTAINER_ROOT="/app"
_CTX_DXKIT="${_CTX_ROOT}/dxkit"
_CTX_MODULES="${_CTX_DXKIT}/modules"
_CTX_COMMANDS="${_CTX_DXKIT}/commands"
_CTX_TEMPLATES="${_CTX_DXKIT}/templates"
_CTX_ENV="${_CTX_DXKIT}/env"
_CTX_PROJECT="${_CTX_ROOT}/.project"
_CTX_ARTIFACT="${_CTX_PROJECT}/artifacts"
# ============================================
# Wrappers
# ============================================
function ctx::require() {
local var="$1"
[[ -z "${!var}" ]] && {
echo "Missing required context variable: $var" >&2
return 1
}
}
# ============================================
# Static Context
# ============================================
function ctx::root() { echo "$_CTX_ROOT"; }
function ctx::dxkit() { echo "$_CTX_DXKIT"; }
function ctx::modules() { echo "$_CTX_MODULES"; }
function ctx::commands() { echo "$_CTX_COMMANDS"; }
function ctx::templates() { echo "$_CTX_TEMPLATES"; }
function ctx::env() { echo "$_CTX_ENV"; }
function ctx::project() { echo "$_CTX_PROJECT"; }
function ctx::artifact() { echo "$_CTX_ARTIFACT"; }
function ctx::container::root() { echo $_CTX_CONTAINER_ROOT; }
# ============================================
# Derived Context
# ============================================
function ctx::runtime() {
# ctx::require ENVIRONMENT || return 1
echo "$(ctx::artifact)/$ENVIRONMENT"
}
function ctx::docker() {
echo "$(ctx::artifact)/docker"
}
# ============================================
# Path helpers
# ============================================
function ctx::artifact::path() {
local IFS="/"
echo "$(ctx::artifact)/$*"
}
function ctx::container::path() {
local base="$(ctx::container::root)"
if [[ $# -eq 0 ]]; then
echo "$base"
return
fi
local IFS="/"
echo "$base/$*"
}

185
dxkit/core/flag.sh Normal file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env bash
# dxkit/core/flags.sh
#
# Shared CLI flag parsing for build and init commands.
#
# Parses flags into exported RUN_* and other control vars.
# Both build::run and init::run call flag::parse before doing any work,
# so --skip-* flags work identically whether passed to dx build or dx init.
#
# Usage:
# flag::parse "$@"
# flag::parse --init --skip-vendors --port=8082
declare -A _REGISTERED_FLAGS=()
function flag::defaults() {
flag::set --framework yii2-advanced
flag::set --skip-vendors false
flag::set --skip-framework-init false
flag::set --skip-framework-config false
}
function flag::register() {
for flag in "$@"; do
_REGISTERED_FLAGS["$flag"]=1
done
}
function flag::registered() {
[[ -n "${_REGISTERED_FLAGS["$1"]:-}" ]]
}
function flag::set() {
local var
var="$(flag::to_var "$1")"
export "${var}"="${2:-true}"
}
function flag::enable() { flag::set "$1" true; }
function flag::disable() { flag::set "$1" false; }
function flag::enabled() {
local var
var="$(flag::to_var "$1")"
[[ "${!var:-false}" == true ]]
}
function flag::to_var() {
local flag="${1#--}"
flag="${flag//-/_}"
echo "${flag^^}"
}
function flag::value() {
local flag="$1"
local default="${2:-}"
# Look for --flag=value in the raw args
local var
var="$(flag::to_var "$flag")"
# Check if set as a var (from flag::parse)
if [[ -n "${!var:-}" ]]; then
echo "${!var}"
return 0
fi
echo "$default"
}
function flag::load_defaults() {
local defaults_file
defaults_file="$(ctx::artifact)/.dx-flags"
[[ -f "$defaults_file" ]] || return 0
# Read non-empty, non-comment lines as flags
local flags=()
while IFS= read -r line; do
[[ -z "$line" || "$line" == "#"* ]] && continue
flags+=("$line")
done < "$defaults_file"
[[ ${#flags[@]} -eq 0 ]] && return 0
flag::parse "${flags[@]}"
}
function flag::scaffold_defaults_file() {
local file="$(ctx::artifact)/.dx-flags"
[[ -f "$file" ]] && return 0 # already exists, don't overwrite
cat > "$file" <<EOF
# .dx-flags — local developer flag defaults
# Uncomment to enable. This file is gitignored.
#
# --skip-vendors
# --skip-framework-init
# --skip-framework-config
EOF
log::fs_write "Created .dx-flags — edit to set your local flag defaults"
}
function flag::parse() {
for arg in "$@"; do
case "$arg" in
--framework=*) export APP_FRAMEWORK="${arg#*=}" ;;
--dev) export ENVIRONMENT="dev" ;;
--qly) export ENVIRONMENT="qly" ;;
--prd) export ENVIRONMENT="prd" ;;
--project=*) export PROJECT_NAME="${arg#*=}" ;;
--port=*) export APP_PORT="${arg#*=}" ;;
--db=*) export DB_ENGINE="${arg#*=}" ;;
--db-port=*) export DB_PORT="${arg#*=}" ;;
--debug) set -x ;;
--help) return 1 ;;
--*)
if [[ "$arg" == *"="* ]]; then
# --flag=value form
local flag_name="${arg%%=*}"
local flag_value="${arg#*=}"
if [[ ${#_REGISTERED_FLAGS[@]} -gt 0 ]] && ! flag::registered "$flag_name"; then
log::error "Unknown flag: '${flag_name}'"
return 1
fi
local var
var="$(flag::to_var "$flag_name")"
export "${var}"="$flag_value"
else
if [[ ${#_REGISTERED_FLAGS[@]} -gt 0 ]] && ! flag::registered "$arg"; then
log::error "Unknown flag: '${arg}'"
return 1
fi
flag::enable "$arg"
fi
;;
*)
log::error "Unknown argument: '${arg}'"
return 1
;;
esac
done
}
# Write local overrides to env/local.env so subsequent dx commands
# (dx up, dx init, etc.) see the same values without re-passing flags.
#
# Only writes vars that differ from their defaults, keeping local.env minimal.
# Called by build::run after flag::parse.
function flag::persist_overrides() {
local local_env
local_env="$(ctx::artifact)/env/local.env"
# Vars that are meaningful to persist across commands
local persist_vars=(
ENVIRONMENT
PROJECT_NAME
APP_PORT
DB_ENGINE
DB_PORT
USE_VHOSTS
)
# Build the new content
local content=""
for var in "${persist_vars[@]}"; do
[[ -n "${!var:-}" ]] && content+="export ${var}=\"${!var}\"\n"
done
[[ -z "$content" ]] && return 0
printf "%b" "$content" > "$local_env"
log::info "Persisted overrides to env/local.env"
}
# ============================================
# Specify Defaults
# ============================================
flag::defaults
flag::load_defaults

135
dxkit/core/hook.sh Normal file
View file

@ -0,0 +1,135 @@
#!/usr/bin/env bash
# core/hook.sh
#
# Lifecycle hook runner.
#
# Hooks are shell scripts sourced in lexicographic order from:
# dxkit/hooks/<command>/<timing>/
#
# Timing:
# pre — runs before the command's main logic
# post — runs after the command's main logic
#
# Naming convention:
# NN-description.sh (e.g. 10-permissions.sh, 20-update-cron.sh)
# Numeric prefix controls execution order.
#
# Hooks are sourced (not executed) so they have full access to all
# loaded modules, functions, and environment variables.
#
# Usage:
# hook::run init pre
# hook::run init post
# hook::run build post
#
# Example structure:
# dxkit/hooks/
# init/
# pre/
# 10-something.sh
# post/
# 10-permissions.sh
# 20-update-cron.sh
# build/
# post/
# 10-something.sh
# ============================================
# Runner
# ============================================
function hook::run() {
local command="$1"
local timing="$2"
local hook_dir
hook_dir="$(ctx::dxkit)/hooks/${command}/${timing}"
[[ -d "$hook_dir" ]] || return 0
local hooks
hooks="$(hook::_list "$hook_dir")"
[[ -z "$hooks" ]] && return 0
log::info "Running ${timing}-${command} hooks..."
local hook
while IFS= read -r hook; do
[[ -f "$hook" ]] || continue
hook::_run_one "$hook"
done <<< "$hooks"
}
# ============================================
# Run a single hook
# ============================================
function hook::_run_one() {
local hook="$1"
local name
name="$(basename "$hook")"
log::info "${name}"
# shellcheck disable=SC1090
source "$hook" || {
log::error "Hook failed: ${name}"
return 1
}
}
# ============================================
# List hooks in a directory sorted by name
# ============================================
function hook::_list() {
local dir="$1"
find "$dir" -maxdepth 1 -name "*.sh" -type f | sort
}
# ============================================
# Predicates
# ============================================
function hook::has() {
local command="$1"
local timing="$2"
local hook_dir
hook_dir="$(ctx::dxkit)/hooks/${command}/${timing}"
[[ -d "$hook_dir" ]] && [[ -n "$(hook::_list "$hook_dir")" ]]
}
# ============================================
# Scaffold hook directory and example file
# ============================================
function hook::scaffold() {
local command="$1"
local timing="${2:-post}"
local hook_dir
hook_dir="$(ctx::dxkit)/hooks/${command}/${timing}"
mkdir -p "$hook_dir"
local example="${hook_dir}/10-example.sh"
[[ -f "$example" ]] && return 0
cat > "$example" <<EOF
#!/usr/bin/env bash
# ${timing}-${command} hook: 10-example.sh
#
# This hook runs ${timing} dx ${command}.
# All modules and environment variables are available.
#
# Remove or rename this file when adding real hooks.
# log::info "Running example ${timing}-${command} hook..."
EOF
log::info "Scaffolded hook: ${example}"
}

122
dxkit/core/loader.sh Normal file
View file

@ -0,0 +1,122 @@
#!/usr/bin/env bash
function require_file() {
[[ -f "$1" ]]
}
function require_directory() {
[[ -d "$1" ]]
}
function load_file() {
local mode="required"
local file
# Check if first argument is a mode
if [[ "$1" == "required" || "$1" == "optional" ]]; then
mode="$1"
shift
fi
file="$1"
if [[ "$mode" == "required" && ! -f "$file" ]]; then
echo "❌ Missing file: $file" >&2
return 1
fi
# Source if file exists
if [[ -f "$file" ]]; then
source "$file"
fi
}
# ============================================
# load_module — Loads $(ctx::modules)/<name>.module.sh
# Always required.
# ============================================
function load_module() {
local name="$1"
# Wildcard: Load all submodules
if [[ "$name" == *"/*" ]]; then
local dir="${name%/*}"
local module_dir
module_dir="$(ctx::modules)/${dir}"
if [[ ! -d "$module_dir" ]]; then
log::error "Module directory not found: ${dir}"
return 1
fi
for file in "${module_dir}"/*.module.sh; do
[[ -f "$file" ]] || continue
local subname="${dir}/$(basename "${file%.module.sh}")"
load_module "$subname"
done
return 0
fi
# Normal single module load
local file
file="$(ctx::modules)/${name}.module.sh"
module::loaded "$name" && return 0
if [[ ! -f "$file" ]]; then
log::error "Module not found: ${name}"
return 1
fi
source "$file"
_LOADED_MODULES["$name"]=1
core::call_if_exists "$(module::to_namespace "$name")::on_load"
}
# ============================================
# load_command — Loads $(ctx::commands)/<name>.command.sh
#
# Returns:
# 0 — file found and sourced
# 1 — file not found (caller decides how to handle)
#
# After sourcing, does NOT validate ::run here —
# that's the dispatcher's job, keeping this function
# a clean "did the file exist?" predicate.
# ============================================
function load_command() {
local name="$1"
local file
file="$(ctx::commands)/${name}.command.sh"
if [[ ! -f "$file" ]]; then
return 1 # No command file, not an error by itself
fi
source "$file"
_LOADED_COMMANDS["$name"]=1
core::call_if_exists "$(command::fn "$name" on_load)"
# core::call_if_exists "$(command::to_namespace "$name")::on_load"
return 0
}
# ============================================
# load_command_strict — Load + assert ::run exists
# ============================================
function load_command_strict() {
local name="$1"
if ! load_command "$name"; then
echo "❌ No command file found for: '${name}'" >&2
return 1
fi
if ! core::function_exists "${name}::run"; then
echo "❌ Command '${name}' loaded but '${name}::run' is not defined" >&2
echo " Expected function: ${name}::run()" >&2
return 1
fi
}

9
dxkit/core/module.sh Normal file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
declare -A _LOADED_MODULES=()
readonly _MODULE_AUTO_LOAD_HOOK="on_load"
function module::loaded() { [[ -n "${_LOADED_MODULES["$1"]:-}" ]]; }
function module::has_function() { declare -F "${1}::${2}" >/dev/null 2>&1; }
function module::is_auto_load() { declare -F "${1}::on_load" >/dev/null 2>&1; }
function module::to_namespace() { echo "${1//\//::}"; }

100
dxkit/core/platform.sh Normal file
View file

@ -0,0 +1,100 @@
#!/usr/bin/env bash
# ============================================
# Detection
# ============================================
function platform::detect() {
case "$(uname -s)" in
Linux*) PLATFORM="linux" ;;
Darwin*) PLATFORM="macos" ;;
CYGWIN*|MINGW*|MSYS*) PLATFORM="windows" ;;
*) PLATFORM="unknown" ;;
esac
platform::_apply_platform_guards
}
function platform::is_macos() { [[ "$PLATFORM" == "macos" ]]; }
function platform::is_linux() { [[ "$PLATFORM" == "linux" ]]; }
function platform::is_windows() { [[ "$PLATFORM" == "windows" ]]; }
# ============================================
function platform::_apply_platform_guards() {
if platform::is_windows; then
export MSYS_NO_PATHCONV=1
export MSYS2_ARG_CONV_EXCL="*"
fi
}
# ============================================
function platform::elevation_fail() {
if platform::is_windows; then
echo "Action must be run as Administrator"
else
echo "Action must be run with sudo or as root"
fi
}
function platform::hosts_file() {
local hostsPath='/etc/hosts'
if platform::is_windows; then
hostsPath='/c/Windows/System32/drivers/etc/hosts'
fi
echo $hostsPath
}
function platform::sudo() {
if platform::is_windows; then
"$@"
else
[[ $EUID -ne 0 ]] && sudo "$1" "$@"
fi
}
function platform::require_privileges() {
if platform::is_windows; then
if ! net session >/dev/null 2>&1; then
return 1
fi
else
platform::sudo -n true 2>/dev/null || platform::sudo true || return 1
fi
}
# ============================================
# Docker
# ============================================
function platform::docker_path() {
local path="$1"
if platform::is_windows; then
cygpath -m "$path"
else
echo "$path"
fi
}
# ============================================
# Platform functions
# ============================================
# Detect GNU realpath (supports --relative-to)
function platform::has_gnu_realpath() {
realpath --help 2>&1 | grep -q -- '--relative-to'
}
# Detect grealpath (Homebrew GNU coreutils)
function platform::has_grealpath() {
command -v grealpath >/dev/null 2>&1
}
# Detect python3
function platform::has_python() {
command -v python3 >/dev/null 2>&1
}

63
dxkit/core/string.sh Normal file
View file

@ -0,0 +1,63 @@
#!/usr/bin/env bash
function string::trim() {
local var="$*"
var="${var#"${var%%[![:space:]]*}"}"
var="${var%"${var##*[![:space:]]}"}"
printf "%s" "$var"
}
function string::lowercase() {
printf "%s" "$1" | tr '[:upper:]' '[:lower:]'
}
function string::uppercase() {
printf "%s" "$1" | tr '[:lower:]' '[:upper:]'
}
function string::replace() {
local str="$1" from="$2" to="$3"
echo "${str//$from/$to}"
}
function string::strip_prefix() {
local str="$1" prefix="$2"
echo "${str#"$prefix"}"
}
function string::strip_suffix() {
local str="$1" suffix="$2"
echo "${str%"$suffix"}"
}
function string::slugify() {
echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-'
}
function string::pad_right() {
local str="$1" width="$2"
printf "%-${width}s" "$str"
}
function string::pad_left() {
local str="$1" width="$2"
printf "%${width}s" "$str"
}
function string::repeat() {
local char="$1" count="$2"
printf "%${count}s" | tr " " "$char"
}
function string::split() {
local str="$1" delimiter="$2"
local -n result_ref="$3" # nameref — caller passes array name
IFS="$delimiter" read -ra result_ref <<< "$str"
}
function string::is_empty() { [[ -z "$1" ]]; }
function string::is_not_empty() { [[ -n "$1" ]]; }
function string::starts_with() { [[ "$1" == "$2"* ]]; }
function string::ends_with() { [[ "$1" == *"$2" ]]; }
function string::contains() { [[ "$1" == *"$2"* ]]; }

99
dxkit/core/utils.sh Normal file
View file

@ -0,0 +1,99 @@
#!/usr/bin/env bash
# ===========================================
# System
# ===========================================
function system::uuid() {
od -x /dev/urandom | head -1 | awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}'
}
function system::hosts() {
cat "$(platform::hosts_file)"
}
function system::stderr() {
"$1" >&2
}
# ===========================================
# Path
# ===========================================
function path::relative_to_root() {
local abs="$1"
local root="$(ctx::root)"
# remove root prefix
echo "${abs#$root/}"
}
function path::from_root() {
local path="$1"
[[ "$path" == /* ]] && echo "$path" || echo "$(ctx::root)/${path}"
}
function path::relative_to() {
local from="$1"
local to="$2"
# ===========================================
# 1. GNU realpath (best case)
# ===========================================
if platform::has_gnu_realpath; then
realpath --relative-to="$from" "$to"
return 0
fi
# ===========================================
# 2. Homebrew GNU coreutils
# ===========================================
if platform::has_grealpath; then
grealpath --relative-to="$from" "$to"
return 0
fi
# ===========================================
# 3. Python fallback (POSIX-safe)
# ===========================================
if platform::has_python; then
python3 -c '
import os, sys
print(os.path.relpath(sys.argv[2], sys.argv[1]))
' "$from" "$to"
return 0
fi
# ===========================================
# 4. Hard failure
# ===========================================
echo "path::relative_to: no suitable backend found" >&2
return 1
}
# ===========================================
# Core
# ===========================================
# Returns true if stdout is connected to a real TTY.
# Used to decide whether winpty or interactive flags are needed.
# False inside $(), pipes, or non-interactive subshells.
function core::has_tty() { [[ -t 1 ]]; } # stdout is a TTY
function core::is_interactive() { [[ $- == *i* ]]; } # shell is interactive mode
function core::function_exists() { declare -F "$1" >/dev/null 2>&1; }
function core::variable_exists() { declare -p "$1" &>/dev/null; }
function core::array_exists() { [[ "$(declare -p "$1" 2>/dev/null)" == "declare -a"* ]]; }
function core::call_function() {
local fn="$1"
shift
"$fn" "$@";
}
function core::call_if_exists() {
local fn="$1"
shift
if declare -F "$fn" >/dev/null 2>&1; then
"$fn" "$@"
fi
return 0
}

View file

@ -0,0 +1,5 @@
#!/bin/sh
set -e
service ssh start
/usr/sbin/apache2ctl -D FOREGROUND

View file

@ -0,0 +1,98 @@
#!/usr/bin/env bash
# ============================================
# Driver Dispatcher
# ============================================
#
# Resolves the active framework via app::framework()
# and loads the corresponding driver from dxkit/drivers/.
#
# Called in main() after load_module app, so app::framework()
# is guaranteed to be available.
#
# Each driver registers its commands into DRIVER_COMMANDS
# for dx::dispatch to pick up.
#
# Usage (in dx entrypoint, after load_module app):
# driver::load
declare -gA DRIVER_COMMANDS=()
# ============================================
# Resolution
# ============================================
# Maps APP_FRAMEWORK values to driver directory names.
# Multiple framework variants can share one driver (e.g. yii2-advanced
# and yii2-basic both use the yii2 driver).
# Unknown frameworks fall through as-is, so new drivers
# just need a matching directory — no changes here required.
function driver::resolve() {
local framework
framework="$(app::framework)"
case "$framework" in
yii2-advanced|yii2-basic) echo "yii2" ;;
laravel) echo "laravel" ;;
*) echo "$framework" ;;
esac
}
# ============================================
# Loader
# ============================================
function driver::load() {
local driver
driver="$(driver::resolve)"
local driver_file
driver_file="$(ctx::dxkit)/drivers/${driver}/driver.sh"
if [[ ! -f "$driver_file" ]]; then
log::warn "No driver found for framework '$(app::framework)' at ${driver_file}"
log::warn "Framework-specific commands (e.g. migrate-create) will not be available."
return 0
fi
# shellcheck source=/dev/null
source "$driver_file"
log::debug "Loaded driver: ${driver} (framework: $(app::framework))"
}
# ============================================
# Runtime Helpers
# ============================================
# Returns true if a command is registered by the active driver.
function driver::has_command() {
local cmd="$1"
[[ -n "${DRIVER_COMMANDS[$cmd]:-}" ]]
}
# Runs a driver command.
function driver::run() {
local cmd="$1"
shift
if ! driver::has_command "$cmd"; then
log::error "Driver command not found: '${cmd}'"
return 1
fi
"${DRIVER_COMMANDS[$cmd]}" "$@"
}
# Prints all registered driver commands (used by dx help and dx workspace).
function driver::list_commands() {
if [[ ${#DRIVER_COMMANDS[@]} -eq 0 ]]; then
return 0
fi
echo ""
echo " Framework commands ($(app::framework)):"
for cmd in $(echo "${!DRIVER_COMMANDS[@]}" | tr ' ' '\n' | sort); do
printf " %-28s\n" "$cmd"
done
}

View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# ============================================
# Laravel Driver
# ============================================
#
# Entry point for the Laravel driver.
# Sources all Laravel command modules and registers
# them into DRIVER_COMMANDS for dx::dispatch.
# ============================================
# Load Modules
# ============================================
# shellcheck source=/dev/null
source "$(ctx::dxkit)/drivers/laravel/migrate.sh"
# ============================================
# Register Commands
# ============================================
DRIVER_COMMANDS=(
[artisan]="laravel::exec"
[migrate]="laravel::migrate::run"
[migrate-create]="laravel::migrate::create"
[migrate-rollback]="laravel::migrate::rollback"
[migrate-status]="laravel::migrate::status"
[migrate-fresh]="laravel::migrate::fresh"
)

View file

@ -0,0 +1,54 @@
#!/usr/bin/env bash
# ============================================
# Laravel — Migration Commands
# ============================================
#
# Thin wrappers around artisan migrate commands,
# run inside the app container via artisan::exec.
# ============================================
# Commands
# ============================================
# Creates a new migration via artisan make:migration.
#
# Usage:
# dx migrate-create create_invoices_table
function laravel::migrate::create() {
local name="${1:?Usage: dx migrate-create <migration_name>}"
artisan::exec make:migration "$name"
}
# Runs all pending migrations.
#
# Usage:
# dx migrate
function laravel::migrate::run() {
artisan::exec migrate "$@"
}
# Rolls back the last batch of migrations.
#
# Usage:
# dx migrate-rollback
# dx migrate-rollback --step=3
function laravel::migrate::rollback() {
artisan::exec migrate:rollback "$@"
}
# Shows the status of all migrations.
#
# Usage:
# dx migrate-status
function laravel::migrate::status() {
artisan::exec migrate:status "$@"
}
# Drops all tables and re-runs all migrations.
#
# Usage:
# dx migrate-fresh
function laravel::migrate::fresh() {
artisan::exec migrate:fresh "$@"
}

View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# ============================================
# Yii2 Driver
# ============================================
#
# Entry point for the yii2-advanced driver.
# Sources all yii2 command modules and registers
# them into DRIVER_COMMANDS for dx::dispatch.
# ============================================
# Load Modules
# ============================================
# shellcheck source=/dev/null
source "$(ctx::dxkit)/drivers/yii2/migrate.sh"
# ============================================
# Register Commands
# ============================================
DRIVER_COMMANDS=(
[yii]="yii2::exec"
[migrate]="yii2::migrate::run"
[migrate-create]="yii2::migrate::create"
[migrate-down]="yii2::migrate::down"
[migrate-new]="yii2::migrate::new"
[migrate-history]="yii2::migrate::history"
)

View file

@ -0,0 +1,138 @@
#!/usr/bin/env bash
# ============================================
# Yii2 — Commands
# ============================================
#
# All functions run inside the app container
# via runtime::exec, so they work regardless
# of whether the stack is Docker or native.
# ============================================
# Helpers
# ============================================
# Generates a snake_case migration class name with timestamp.
# e.g. m260430_151441_create_invoice_table
function yii2::migrate::_class_name() {
local name="$1"
local timestamp
timestamp="$(date '+%y%m%d_%H%M%S')"
echo "m${timestamp}_${name}"
}
# Resolves the migrations directory on the host.
function yii2::migrate::_dir() {
echo "$(ctx::root)/console/migrations"
}
# ============================================
# Yii
# ============================================
# Runs any yii console command inside the container.
#
# Usage:
# dx yii <command> [args...]
# yii <command> [args...] (inside dx workspace)
#
# Examples:
# dx yii migrate
# dx yii rbac/init
# yii cache/flush-all
function yii2::exec() {
runtime::exec php yii "$@"
}
# ============================================
# Migrations
# ============================================
# Creates a new migration file with the correct namespace,
# class name, and stub — bypassing Yii's generator entirely.
#
# Usage:
# dx migrate-create <name>
# e.g. dx migrate-create create_invoice_table
function yii2::migrate::create() {
local name="${1:?Usage: dx migrate-create <migration_name>}"
local namespace="console\\migrations"
local class
class="$(yii2::migrate::_class_name "$name")"
local dir
dir="$(yii2::migrate::_dir)"
local file="${dir}/${class}.php"
local title
title="$(echo "$name" | sed 's/_/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1')"
mkdir -p "$dir"
cat > "$file" <<PHP
<?php
namespace ${namespace};
use console\models\Migration;
/**
* ${title}
*/
class ${class} extends Migration
{
public \$tableName = '{{%TODO}}';
/**
* {@inheritdoc}
*/
public function safeUp()
{
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
\$this->dropTable(\$this->tableName);
}
}
PHP
log::success "Created: ${file}"
}
# Runs all pending migrations.
#
# Usage:
# dx migrate
function yii2::migrate::run() {
yii2::exec migrate "$@"
}
# Reverts the last N migrations (default: 1).
#
# Usage:
# dx migrate-down
# dx migrate-down 3
function yii2::migrate::down() {
local n="${1:-1}"
yii2::exec migrate/down "$n" "${@:2}"
}
# Shows new (pending) migrations.
#
# Usage:
# dx migrate-new
function yii2::migrate::new() {
yii2::exec migrate/new "$@"
}
# Shows migration history.
#
# Usage:
# dx migrate-history
function yii2::migrate::history() {
yii2::exec migrate/history "$@"
}

39
dxkit/env/globals.env vendored Normal file
View file

@ -0,0 +1,39 @@
# App Settings
export BASE_APP_IMAGE="yiisoftware/yii2-php:8.3-apache"
export APP_IMAGE="${PROJECT_NAME}-app"
export APP_PORT=auto:8080
# Domains
export TLD="erlog"
export DOMAIN="${PROJECT_NAME}.${TLD}"
# Database Config
export DB_DRIVER="db"
export DB_IMAGE="mariadb:latest"
export DB_PORT=auto
export DB_HOST="${PROJECT_NAME}-${DB_DRIVER}"
export DB_NAME="${PROJECT_NAME}"
export DB_ENGINE="mysql"
# Database Auth
export DB_USER="root"
export DB_PASS="kxpto1l"
export DB_ROOT_PASSWORD="kxpto1l"
# Composer Auth
export GIT_DOMAIN="git.erlog.pt"
export GIT_AUTH_TOKEN_ID="gitlab+deploy-token-5"
export GIT_AUTH_TOKEN_PW="gldt-CMbMgwQ_EsxBydc13Ssv"
export COMPOSER_AUTH="{
\"http-basic\": {
\"${GIT_DOMAIN}\": {
\"username\": \"${GIT_AUTH_TOKEN_ID}\",
\"password\": \"${GIT_AUTH_TOKEN_PW}\"
}
}
}"
# Proxy
export PROXY_SERVICE="traefik"
export PROXY_NETWORK="proxy"

View file

@ -0,0 +1,75 @@
#!/usr/bin/env bash
# modules/apache.module.sh
#
# Apache management module.
# Handles vhost generation, config validation, reloads, and module toggling.
#
# Runs commands through runtime::exec so it works identically with
# Docker and native runtimes.
# ============================================
# Vhost generation
# ============================================
function apache::generate_vhosts() {
log::env_load "Generating Virtual Hosts (${ENVIRONMENT})..."
template::render \
"$(app::vhost_template_path)" \
"$(ctx::artifact::path apache vhosts vhosts.conf)" \
APP_PATH="$(ctx::container::root)"
}
# ============================================
# Config validation
# ============================================
function apache::validate_config() {
runtime::exec apache2ctl configtest
}
# ============================================
# Lifecycle
# ============================================
function apache::reload() {
runtime::exec apache2ctl graceful
}
function apache::restart() {
runtime::exec apache2ctl restart
}
# ============================================
# Inspection
# ============================================
function apache::list_vhosts() {
runtime::exec apache2ctl -S
}
function apache::list_modules() {
runtime::exec apache2ctl -M
}
function apache::version() {
runtime::exec apache2 -v
}
# ============================================
# Logs
# ============================================
function apache::logs::error() { runtime::exec tail -f /var/log/apache2/error.log; }
function apache::logs::access() { runtime::exec tail -f /var/log/apache2/access.log; }
# ============================================
# Module management
# ============================================
function apache::module::enable() { runtime::exec a2enmod "$@"; }
function apache::module::disable() { runtime::exec a2dismod "$@"; }
function apache::module::is_enabled() {
runtime::exec a2query -m "$1" >/dev/null 2>&1
}

View file

@ -0,0 +1,44 @@
#!/usr/bin/env bash
# modules/app.module.sh
#
# Framework abstraction layer.
# Dispatches app lifecycle functions to the active framework module.
#
# Active framework is determined by --framework flag (default: yii2-advanced)
# Set in .dx-flags or passed as: dx build --framework=laravel
function app::on_load() {
load_module "framework/$(app::framework)"
load_module proxy
load_module composer
# Register framework console name as alias for console command
# yii2-advanced → yii, laravel → artisan
# CMD_ALIASES is declared in dx
CMD_ALIASES["$(app::console_name)"]=console
CMD_ALIASES[console]=console
}
# ============================================
# Framework identity
# ============================================
function app::framework { echo "${APP_FRAMEWORK:-yii2-advanced}"; }
function app::console_name() { "framework::$(app::framework)::console_name"; }
# ============================================
# Interface
# ============================================
function app::scaffold() { "framework::$(app::framework)::scaffold" "${1:-$(ctx::root)}"; }
function app::init() { "framework::$(app::framework)::init" "$@"; }
function app::config() { "framework::$(app::framework)::config" "$@"; }
function app::console() { "framework::$(app::framework)::console" "$@"; }
function app::resolve_env() { "framework::$(app::framework)::resolve_env" "$@"; }
function app::install_vendors() { "framework::$(app::framework)::install_vendors"; }
function app::vendor_skip_message() { "framework::$(app::framework)::vendor_skip_message"; }
function app::vhost_template_path() { "framework::$(app::framework)::vhost_template_path"; }
function app::exists() { [[ -f "$(ctx::root)/composer.json" ]]; }
function app::init_skip_message() { "framework::$(app::framework)::init_skip_message" "$@"; }
function app::config_skip_message() { "framework::$(app::framework)::config_skip_message" "$@"; }
function app::post_init_hint() { "framework::$(app::framework)::post_init_hint"; }

View file

@ -0,0 +1,18 @@
#!/usr/bin/env bash
function artifact::dirs() {
local dirs=(
"$(ctx::artifact)"
"$(ctx::artifact::path env)"
"$(ctx::artifact::path apache)"
"$(ctx::artifact::path database)"
"$(ctx::artifact::path docker)"
)
printf "%s\n" "${dirs[@]}"
}
function artifact::init() {
while IFS= read -r dir; do
mkdir -p "$dir"
done < <(artifact::dirs)
}

View file

@ -0,0 +1,107 @@
#!/usr/bin/env bash
# modules/composer.module.sh
#
# Composer wrapper for both in-runtime and bootstrap operations.
#
# Two execution modes:
# composer::run — runs inside the project's runtime
# (use when the runtime is up)
# composer::run_isolated — runs in a one-shot Docker container
# (use for bootstrap before runtime exists)
# ============================================
# Execution modes
# ============================================
# Run composer inside the project's runtime.
# Requires the runtime to be up.
function composer::run() {
runtime::exec composer "$@"
}
# Run composer in a one-shot Docker container.
# Use for scaffold and other operations that happen before the runtime exists.
function composer::run_isolated() {
docker::raw run --rm \
-v "$(ctx::root):/app" \
-w /app \
composer:latest "$@"
}
# ============================================
# Auth
# ============================================
function composer::config_auth() {
local mode="cache"
if [[ "$1" == "cache" || "$1" == "nocache" ]]; then
mode="$1"
shift
fi
local domain="$1"
local id="$2"
local pw="$3"
if [[ -z "$domain" || -z "$id" || -z "$pw" ]]; then
log::error "composer::config_auth: domain, id, and password are required"
return 1
fi
[[ "$mode" == "nocache" ]] && \
runtime::exec rm -f /root/.composer/auth.json
runtime::exec composer config \
--global \
--auth "http-basic.${domain}" \
"$id" \
"$pw"
}
# ============================================
# Vendor management
# ============================================
function composer::install() {
runtime::exec composer install \
--prefer-dist \
--no-interaction \
--no-progress \
--optimize-autoloader \
"$@"
}
function composer::update() {
runtime::exec composer update \
--prefer-dist \
--no-interaction \
--no-progress \
"$@"
}
function composer::require() {
runtime::exec composer require \
--no-interaction \
--no-progress \
"$@"
}
function composer::delete_lock() {
runtime::exec rm -f composer.lock
}
# ============================================
# Inspection
# ============================================
function composer::version() { runtime::exec composer --version; }
function composer::validate() { runtime::exec composer validate; }
function composer::has_lockfile() {
[[ -f "$(ctx::root)/composer.lock" ]]
}
function composer::has_manifest() {
[[ -f "$(ctx::root)/composer.json" ]]
}

View file

@ -0,0 +1,305 @@
#!/usr/bin/env bash
# shellcheck disable=SC2120
# ===========================================
# Load
# ===========================================
function docker::on_load() { load_module 'docker/*'; }
# ===========================================
# Docker
# ===========================================
# Non-interactive docker calls — bypasses all shell aliases.
function docker::raw() {
command docker "$@"
}
# Interactive docker calls that need a TTY.
# On Windows: prepends winpty to allocate a pseudo-terminal.
# On Unix: same as docker::raw.
function docker::tty() {
if platform::is_windows; then
winpty docker "$@"
else
command docker "$@"
fi
}
function docker::exec() {
local container="$1"
shift
docker::tty exec -it "$container" "$@"
}
function docker::container_running() {
local name="$1"
docker::raw ps --format '{{.Names}}' \
| grep -qx "$name"
}
function docker::container_exists() {
local name="$1"
docker::raw ps -a --format '{{.Names}}' \
| grep -qx "$name"
}
function docker::build() {
local dockerfile=$(docker::image::file)
local tag="${APP_IMAGE}:${ENVIRONMENT}"
require_file "$dockerfile" || {
log::docker_error "Dockerfile not found for environment $ENVIRONMENT (Dockerfile-${ENVIRONMENT})!"
return 1
}
log::docker_build "Building image ${tag}..."
if docker::raw build -f "$dockerfile" -t "$tag" "$(ctx::root)"; then
log::docker_success "Image built: ${tag}"
else
log::docker_error "Failed to build image: ${tag}"
return 1
fi
}
function docker::list_containers() {
docker::raw ps --filter "name=${PROJECT_NAME}" \
--format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"
}
function docker::status() {
local container="$1"
if ! docker::raw inspect "$container" >/dev/null 2>&1; then
echo "missing"
return
fi
docker::raw inspect -f '{{.State.Status}}' "$container"
}
function docker::health() {
local container="$1"
local health
health="$(docker::raw inspect -f '{{.State.Health.Status}}' "$container" 2>/dev/null)"
if [[ -z "$health" || "$health" == "<no value>" ]]; then
echo "unknown"
else
echo "$health"
fi
}
function docker::inside_container() {
[[ -f /.dockerenv ]]
}
# ===========================================
# Compose
# ===========================================
function docker::compose() {
local stackName="docker-compose-${ENVIRONMENT}.yml"
require_file "$(docker::compose::file)" || {
log::docker_error "Docker Compose Stack not found for Environment $ENVIRONMENT ($stackName)!"
return 1
}
if core::has_tty; then
docker::tty compose -p "${PROJECT_NAME}_${ENVIRONMENT}" -f "$(docker::compose::file)" "$@"
else
docker::raw compose -p "${PROJECT_NAME}_${ENVIRONMENT}" -f "$(docker::compose::file)" "$@"
fi
}
function docker::compose::interactive() {
local file
file="$(docker::compose::file)"
require_file "$file" || {
log::docker_error "Missing compose file"
return 1
}
docker::tty compose \
-p "${PROJECT_NAME}_${ENVIRONMENT}" \
-f "$file" \
"$@"
}
function docker::compose::default_service() { echo "app"; }
function docker::compose::service_running() {
local service="${1:-app}"
local id
id="$(docker::compose ps -q "$service" 2>/dev/null)"
id="${id//$'\r'/}" # strip carriage returns using parameter expansion, no pipe
[[ -n "$id" ]]
}
function docker::compose::ensure_running() {
local service="${1:-app}"
if ! docker::compose::service_running "$service"; then
log::docker_start "Stack not running, starting..."
docker::compose up -d
fi
}
function docker::compose::logs() {
docker::compose logs -f
}
function docker::compose::up() {
docker::compose up -d --remove-orphans \
&& { log::docker_start "Stack started for Environment $ENVIRONMENT"; } \
|| { log::docker_error "Failed to start Stack for Environment $ENVIRONMENT"; return 1; }
}
function docker::compose::down() {
docker::compose down \
&& { log::docker_stop "Stack stopped for Environment $ENVIRONMENT"; } \
|| { log::docker_error "Failed to stop Stack for Environment $ENVIRONMENT"; return 1; }
}
function docker::compose::status() {
local service="${1:-$(docker::compose::default_service)}"
local id
id="$(docker::compose::container "$service")" || {
echo "missing"
return
}
docker::raw inspect -f '{{.State.Status}}' "$id"
}
function docker::compose::exec() {
local service="$1"
shift
docker::compose::interactive exec "$service" "$@"
}
function docker::compose::container() {
local service="$1"
docker::compose ps -q "$service"
}
function docker::compose::service_exists() {
local service="${1:-$(docker::compose::default_service)}"
[[ -n "$(docker::compose::container "$service")" ]]
}
function docker::compose::list_containers() {
docker::compose ps \
--format "table {{.Name}}\t{{.Service}}\t{{.Status}}\t{{.Ports}}"
}
function docker::compose::ports() {
local service="${1:-$(docker::compose::default_service)}"
local id
id="$(docker::compose::container "$service")" || return 1
docker::raw port "$id" 2>/dev/null
}
# ===========================================
# Paths
# ===========================================
function docker::path() {
local dir=""
local file=""
if [[ $# -eq 1 ]]; then
file="$1"
else
dir="$1"
file="$2"
fi
if [[ -n "$dir" ]]; then
echo "$(ctx::docker)/$dir/$file"
else
echo "$(ctx::docker)/$file"
fi
}
function docker::path::relative() { path::relative_to "$(ctx::docker)" "$1"; }
function docker::image::file() { platform::docker_path "$(ctx::docker)/Dockerfile-${ENVIRONMENT}"; }
function docker::compose::file() { platform::docker_path "$(ctx::docker)/docker-compose-${ENVIRONMENT}.yml"; }
function docker::image::path() { path::relative_to_root "$(ctx::docker)/Dockerfile-${ENVIRONMENT}"; }
function docker::compose::path() { path::relative_to_root "$(ctx::docker)/docker-compose-${ENVIRONMENT}.yml"; }
function docker::app::path() { docker::path::relative "$(ctx::root)"; }
function docker::apache::logs_path() { docker::path::relative "$(ctx::artifact)/apache/logs"; }
function docker::apache::vhosts_path() { docker::path::relative "$(ctx::artifact)/apache/vhosts"; }
function docker::apache::certs_path() { docker::path::relative "$(ctx::artifact)/apache/certificates"; }
function docker::apache::xdebug_path() { docker::path::relative "$(ctx::artifact)/php/xdebug.ini"; }
# ===========================================
# Render
# ===========================================
function docker::compose::render_volume() {
local host="$1"
local container="$2"
echo "- ${host}:${container}"
}
function docker::compose::db_volume() {
docker::compose::render_volume \
"$(docker::path::relative "$(db::volume_host_path)")" \
"$(db::volume_container_path)"
}
function docker::compose::volume() { docker::compose::render_volume "$(docker::path::relative "$1")" "$2"; }
# ===========================================
# Generation
# ===========================================
function docker::image::generate() {
local tag="${APP_IMAGE}:${ENVIRONMENT}"
log::docker_build "Generating Docker Image ($tag)..."
template::render \
"$(template::path docker Dockerfile)" \
"$(docker::image::path)"
}
function docker::compose::generate() {
log::docker_build "Generating Docker Compose Stack (${ENVIRONMENT})..."
template::render \
"$(template::path docker docker-compose.yml)" \
"$(docker::compose::path)" \
ENVIRONMENT="$ENVIRONMENT" \
APP_IMAGE="$APP_IMAGE" \
CTX_BUILD="$(docker::app::path)" \
CTX_APP="$(docker::app::path)" \
CTX_APACHE_LOGS="$(docker::apache::logs_path)" \
CTX_APACHE_VHOSTS="$(docker::apache::vhosts_path)" \
CTX_APACHE_CERTS="$(docker::apache::certs_path)" \
CTX_XDEBUG="$(docker::apache::xdebug_path)" \
APP_PATH="$(ctx::container::root)" \
IMAGE_PATH="$(docker::image::path)" \
TRAEFIK_LABELS="$(docker::proxy::traefik_labels "$PROJECT_NAME" "$DOMAIN" "$BACKEND_DOMAIN" 80)" \
DB_ENVIRONMENT="$(docker::db::environment)" \
DB_VOLUMES="$(docker::compose::volume "$(docker::db::external_path)" "$(docker::db::internal_path)")" \
DB_STACK_PORT="$(docker::db::external_port):$(docker::db::internal_port)"
}
# ===========================================
# Build
# ===========================================
function docker::image::build() {
local dockerfile
dockerfile="$(path::relative_to_root "$(docker::image::path)")"
local tag="${APP_IMAGE}:${ENVIRONMENT}"
require_file "$dockerfile" || {
log::docker_error "Dockerfile not found for environment $ENVIRONMENT"
return 1
}
log::docker_build "Building image ${tag}..."
local context
context="$(platform::docker_path "$(ctx::root)")"
if docker::raw build -f "$dockerfile" -t "$tag" "$context"; then
log::docker_success "Image built: ${tag}"
else
return 1
fi
}
function docker::compose::build() {
docker::compose build
}

View file

@ -0,0 +1,69 @@
#!/usr/bin/env bash
# ============================================
# Engine config
# ============================================
MYSQL_INTERNAL_PORT=3306
MSSQL_INTERNAL_PORT=1433
function docker::db::internal_port() {
case "$DB_ENGINE" in
mysql) echo $MYSQL_INTERNAL_PORT ;;
mssql) echo $MSSQL_INTERNAL_PORT ;;
esac
}
function docker::db::external_port() {
# If already resolved to a plain number, use it
if [[ -n "$DB_PORT" && "$DB_PORT" != "auto" ]]; then
echo "$DB_PORT"
return
fi
# Otherwise resolve from internal port as base
network::ports::resolve "auto:$(docker::db::internal_port)"
}
function docker::db::internal_path() {
case "$DB_ENGINE" in
mysql) echo "/var/lib/mysql" ;;
mssql) echo "/var/opt/mssql/data" ;;
esac
}
function docker::db::external_path() { ctx::artifact::path database; }
function docker::db::_mysql_vars() {
cat << EOF
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
MYSQL_DATABASE: "${DB_NAME}"
MYSQL_USER: "${DB_USER}"
MYSQL_PASSWORD: "${DB_PASS}"
EOF
}
function docker::db::_mssql_vars() {
cat << EOF
SA_PASSWORD: ${DB_PASS}
ACCEPT_EULA: 'Y'
EOF
}
# ============================================
# Generation
# ============================================
function docker::db::environment() {
case "$DB_ENGINE" in
mysql) docker::db::_mysql_vars ;;
mssql) docker::db::_mssql_vars ;;
esac
}
# ============================================
# Load
# ============================================
#function docker::db::on_load() {
#
#}

View file

@ -0,0 +1,93 @@
#!/usr/bin/env bash
# modules/docker/proxy.module.sh
#
# Proxy docker module.
# Configures proxy, generates what's needed to compose docker stack --
# and starts/stops the proxy service.
function docker::proxy::running() { docker::raw ps --format '{{.Names}}' | grep -qx "${PROXY_SERVICE}"; }
function docker::proxy::exists() { docker::raw ps -a --format '{{.Names}}' | grep -qx "${PROXY_SERVICE}"; }
function docker::proxy::traefik_labels() {
local service="$1"
local domain="$2"
local backend_domain="$3"
local port="$4"
cat <<EOF
- "traefik.enable=true"
- "traefik.docker.network=${PROXY_NETWORK}"
- "traefik.http.routers.${service}-frontend.rule=Host(\`${domain}\`)"
- "traefik.http.routers.${service}-frontend.entrypoints=web"
- "traefik.http.routers.${service}-frontend.service=${service}"
- "traefik.http.routers.${service}-backend.rule=Host(\`${backend_domain}\`)"
- "traefik.http.routers.${service}-backend.entrypoints=web"
- "traefik.http.routers.${service}-backend.service=${service}"
- "traefik.http.services.${service}.loadbalancer.server.port=${port}"
EOF
}
function docker::proxy::start() {
docker::proxy::running && return 0
docker::proxy::exists && {
docker::raw start "$PROXY_SERVICE" > /dev/null 2>&1
return 0
};
docker::proxy::_create_container
}
function docker::proxy::stop() {
if docker::proxy::running "${PROXY_SERVICE}"; then
docker::raw stop "${PROXY_SERVICE}" > /dev/null 2>&1
fi
}
function docker::proxy::restart() {
if docker::proxy::running "${PROXY_SERVICE}"; then
docker::raw restart "${PROXY_SERVICE}" > /dev/null 2>&1
fi
}
function docker::proxy::ensure_network() {
docker::raw network inspect "${PROXY_NETWORK}" >/dev/null 2>&1 || \
docker::raw network create "${PROXY_NETWORK}"
}
function docker::proxy::ensure_proxy_running() {
docker::proxy::ensure_network
docker::proxy::start
}
# ============================================
# Logs
# ============================================
function docker::proxy::logs() {
docker::raw logs -f "${PROXY_SERVICE}"
}
# ============================================
# Private
# ============================================
function docker::proxy::_create_container() {
docker::raw run -d \
--name "$PROXY_SERVICE" \
--restart unless-stopped \
-p 80:80 -p 443:443 -p 8080:8080 \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
--network "$PROXY_NETWORK" \
traefik:v3.0 \
--api.dashboard=true --api.insecure=true \
--providers.docker=true \
--providers.docker.exposedbydefault=false \
--providers.docker.network="$PROXY_NETWORK" \
--entrypoints.web.address=:80 \
--entrypoints.websecure.address=:443 \
--log.level=DEBUG \
> /dev/null 2>&1
}

View file

@ -0,0 +1,92 @@
#!/usr/bin/env bash
function env::on_load() {
env::defaults
env::load_globals
}
function env::artifact::path() { path::relative_to_root "$(ctx::artifact::path env)"; }
function env::artifact::resolved() { path::relative_to_root "$(ctx::artifact::path env)/${ENVIRONMENT}.resolved.env"; }
function env::project::name() { echo "${PROJECT_NAME}"; }
# ============================================
# Defaults
# ============================================
function env::defaults() {
: "${PROJECT_NAME:=$(basename "$(ctx::root)")}"
: "${APP_FRAMEWORK:=yii2-advanced}"
: "${APP_PORT:=auto:8080}"
: "${TLD:="lan"}"
: "${DB_DRIVER:=db}"
: "${DB_IMAGE:=mariadb:latest}"
: "${DB_ENGINE:=mysql}"
: "${DB_PORT:=auto}"
: "${PROXY_SERVICE:=traefik}"
: "${PROXY_NETWORK:=proxy}"
# Environment
: "${ENVIRONMENT:=dev}"
}
function env::defaults::derived() {
APP_IMAGE="${APP_IMAGE:-${PROJECT_NAME}-app}"
DB_NAME="${DB_NAME:-${PROJECT_NAME}}"
DB_HOST="${DB_HOST:-${PROJECT_NAME}-${DB_DRIVER}}"
DOMAIN="${DOMAIN:-${PROJECT_NAME}.${TLD}}"
BACKEND_DOMAIN="${BACKEND_DOMAIN:-admin.${DOMAIN}}"
}
# ============================================
function env::load_globals() {
load_file required "$(ctx::env)/globals.env";
env::defaults::derived
}
function env::load_environment() {
load_file optional "$(ctx::artifact::path env)/${ENVIRONMENT}.env"
env::defaults::derived
}
function env::load_environment_resolved() {
load_file optional "$(env::artifact::resolved)"
}
function env::load_env_files() {
set -a
env::load_globals || { set +a; exit 1; }
env::load_environment
env::load_environment_resolved
set +a
}
function env::set_env_var() {
local key="$1"
local value="$2"
local file="${3:-$(ctx::artifact::path env)/${ENVIRONMENT}.env}"
touch "$file"
if grep -q "^${key}=" "$file"; then
fs::sed_inplace "s/^${key}=.*/${key}=${value}/" "$file"
else
printf "\n%s=%s" "$key" "$value" >> "$file"
fi
}
# ============================================
# Derived
# ============================================
function env::derive() {
APP_IMAGE="${PROJECT_NAME}-app"
DOMAIN="${PROJECT_NAME}.${TLD}"
BACKEND_DOMAIN="admin-${PROJECT_NAME}.${TLD}"
DB_HOST="${PROJECT_NAME}-db"
DB_NAME="${PROJECT_NAME}"
export APP_IMAGE DOMAIN BACKEND_DOMAIN DB_HOST DB_NAME
}

View file

@ -0,0 +1,125 @@
#!/usr/bin/env bash
# framework/laravel.module.sh
#
# Laravel framework implementation.
# Implements the framework:: interface for Laravel projects.
# ============================================
# Skip messages
# ============================================
function framework::laravel::init_skip_message() { echo "Skipping Laravel bootstrap (--skip-framework-init)"; }
function framework::laravel::config_skip_message() { echo "Skipping Laravel config (--skip-framework-config)"; }
function framework::laravel::vendor_skip_message() { echo "Skipping Vendors (--skip-vendors)"; }
# ============================================
# Scaffold
# ============================================
function framework::laravel::scaffold() {
local target="$1"
log::run_step env "Building Laravel application (${target})..." \
composer::run_isolated create-project laravel/laravel "$target" --no-install
}
# ============================================
# Init
# ============================================
function framework::laravel::init() {
local env="$1"
local laravel_env
laravel_env="$(framework::laravel::resolve_env "$env")"
log::run_step env "Bootstrapping Laravel (${laravel_env})..." \
framework::laravel::_write_env_file "$env"
}
# ============================================
# Config
# ============================================
function framework::laravel::config() {
log::run_step env "Generating Laravel config cache..." \
framework::laravel::console config:cache
log::run_step env "Generating Laravel route cache..." \
framework::laravel::console route:cache
}
# ============================================
# Console
# ============================================
function framework::laravel::console_name() { echo "artisan"; }
function framework::laravel::console() { runtime::exec php artisan "$@"; }
# ============================================
# Resolve env
# ============================================
function framework::laravel::resolve_env() {
case "$1" in
dev) echo "local" ;;
qly) echo "staging" ;;
prd) echo "production" ;;
*) echo "$1" ;;
esac
}
# ============================================
# Vendors
# ============================================
function framework::laravel::install_vendors() {
log::run_step auth info "Setting Composer auth for domain ${GIT_DOMAIN}..." \
composer::config_auth nocache "$GIT_DOMAIN" "$GIT_AUTH_TOKEN_ID" "$GIT_AUTH_TOKEN_PW"
log::run_step fs "Installing vendors..." \
composer::install
}
# ============================================
# Post-init hint
# ============================================
function framework::laravel::post_init_hint() {
cat <<EOF
Next steps (run manually after):
dx artisan migrate
dx artisan db:seed
dx artisan key:generate
EOF
}
# ============================================
# Private
# ============================================
function framework::laravel::_write_env_file() {
local env="$1"
local env_file
env_file="$(ctx::root)/.env"
if [[ -f "$env_file" ]]; then
log::info ".env already exists — skipping"
return 0
fi
if [[ ! -f "$(ctx::root)/.env.example" ]]; then
log::error ".env.example not found — cannot generate .env"
return 1
fi
cp "$(ctx::root)/.env.example" "$env_file"
local laravel_env
laravel_env="$(framework::laravel::resolve_env "$env")"
# Patch APP_ENV and APP_URL in the generated .env
sed -i "s/^APP_ENV=.*/APP_ENV=${laravel_env}/" "$env_file"
sed -i "s|^APP_URL=.*|APP_URL=http://${DOMAIN}|" "$env_file"
log::success ".env generated for environment: ${laravel_env}"
}

View file

@ -0,0 +1,176 @@
#!/usr/bin/env bash
# framework/yii2-advanced.module.sh
#
# Yii2 Advanced framework implementation.
# Implements the framework:: interface for Yii2 Advanced projects.
# Owns config generation, init lifecycle, and template handling
# specific to the advanced template's directory layout.
# ============================================
# Load
# ============================================
function framework::yii2-advanced::on_load() {
load_module yii
load_module template
}
# ============================================
# Skip messages
# ============================================
function framework::yii2-advanced::init_skip_message() { echo "Skipping Yii init (--skip-framework-init)"; }
function framework::yii2-advanced::config_skip_message() { echo "Skipping Yii config (--skip-framework-config)"; }
function framework::yii2-advanced::vendor_skip_message() { echo "Skipping Vendors (--skip-vendors)"; }
# ============================================
# Templates
# ============================================
function framework::yii2-advanced::vhost_template_path() { echo "$(ctx::templates)/apache/yii2-advanced/vhosts.conf.template"; }
function framework::yii2-advanced::_template_root() { echo "$(ctx::templates)/yii2-advanced"; }
function framework::yii2-advanced::_components_template_root() { echo "$(framework::yii2-advanced::_template_root)/components"; }
function framework::yii2-advanced::_config_root() { echo "$(ctx::root)/common/config"; }
function framework::yii2-advanced::_template() {
local templateFile="$1"
local path="$(framework::yii2-advanced::_template_root)/${templateFile}.php.template"
require_file "$path" || {
log::fs_error "${templateFile}.php.template does not exist!"
return 1
}
printf "%s" "$path"
}
function framework::yii2-advanced::_template_component() {
local templateFile="$1"
local path="$(framework::yii2-advanced::_components_template_root)/${templateFile}.php.template"
require_file "$path" || {
log::fs_error "${templateFile}.php.template does not exist!"
return 1
}
printf "%s" "$path"
}
function framework::yii2-advanced::_components() {
printf '%s\n' \
"db.${DB_ENGINE}" \
"log"
}
function framework::yii2-advanced::_build_components() {
local components=()
for component in $(framework::yii2-advanced::_components); do
local comp
comp=$(envsubst < "$(framework::yii2-advanced::_template_component "$component")")
# remove blank lines
comp=$(printf "%s" "$comp" | sed '/^[[:space:]]*$/d')
components+=("$comp")
done
printf "%s\n" "${components[@]}"
}
# ============================================
# Scaffold
# ============================================
function framework::yii2-advanced::scaffold() {
local target="$1"
log::run_step env "Building Yii2 Advanced application ($target)..." \
composer::run_isolated create-project yiisoft/yii2-app-advanced "$target" --no-install
}
function framework::yii2-advanced::scaffold() {
local target="$1"
local tmp="_scaffold_tmp"
log::run_step env "Scaffolding Yii2 Advanced (${target})..." \
composer::run_isolated sh -c \
'composer create-project yiisoft/yii2-app-advanced "$1" --no-install && cp -rn "$1/." . && rm -rf "$1"' \
-- "$tmp"
}
# ============================================
# Init
# ============================================
function framework::yii2-advanced::init() {
local env="$1"
local yii_env
yii_env="$(yii::resolve_env "$env")"
log::run_step env "Initializing Yii2 Advanced (${yii_env})..." \
runtime::exec php init \
--env="$yii_env" \
--overwrite=All \
--interactive=0
}
# ============================================
# Config
# ============================================
function framework::yii2-advanced::config() {
log::run_step env "Generating Yii config..." \
framework::yii2-advanced::_generate_config main-local.php
}
function framework::yii2-advanced::_generate_config() {
local output="$(framework::yii2-advanced::_config_root)/$1"
local components="$(framework::yii2-advanced::_build_components)"
# indent components for placement inside the components: array
local indented_components
indented_components=$(printf "%s\n" "$components" | sed 's/^/ /')
template::render \
"$(framework::yii2-advanced::_template main-local)" \
"$output" \
YII_MAIN_LOCAL_COMPONENTS="$indented_components"
}
# ============================================
# Console / Env
# ============================================
function framework::yii2-advanced::console_name() { echo "yii"; }
function framework::yii2-advanced::console() { yii::exec "$@"; }
function framework::yii2-advanced::resolve_env() { yii::resolve_env "$@"; }
# ============================================
# Vendors
# ============================================
function framework::yii2-advanced::install_vendors() {
log::run_step auth info "Setting Composer auth for domain ${GIT_DOMAIN}..." \
composer::config_auth nocache "$GIT_DOMAIN" "$GIT_AUTH_TOKEN_ID" "$GIT_AUTH_TOKEN_PW"
log::run_step fs "Installing vendors..." \
composer::install
}
# ============================================
# Post-init hint
# ============================================
function framework::yii2-advanced::post_init_hint() {
cat <<EOF
Next steps (run manually after):
dx yii migrate
dx yii gatekeeper/sync-permissions
EOF
}
# ============================================
# Private
# ============================================

View file

@ -0,0 +1,131 @@
#!/usr/bin/env bash
# modules/framework/yii2-basic.module.sh
#
# Yii2 Basic framework implementation.
# ============================================
# Skip messages
# ============================================
function framework::yii2-basic::init_skip_message() { echo "Skipping Yii init (--skip-framework-init)"; }
function framework::yii2-basic::config_skip_message() { echo "Skipping Yii config (--skip-framework-config)"; }
function framework::yii2-basic::vendor_skip_message() { echo "Skipping Vendors (--skip-vendors)"; }
# ============================================
# Identity
# ============================================
function framework::yii2-basic::console_name() { echo "yii"; }
# ============================================
# Templates
# ============================================
function framework::yii2-basic::vhost_template_path() {
echo "$(ctx::templates)/apache/yii2-basic/vhosts.conf.template"
}
function framework::yii2-basic::_template_root() { echo "$(ctx::templates)/yii2-basic"; }
function framework::yii2-basic::_config_root() { echo "$(ctx::root)/config"; }
function framework::yii2-basic::_template() {
local templateFile="$1"
local path="$(framework::yii2-basic::_template_root)/${templateFile}.php.template"
require_file "$path" || {
log::fs_error "${templateFile}.php.template does not exist!"
return 1
}
printf "%s" "$path"
}
# ============================================
# Scaffold
# ============================================
function framework::yii2-basic::scaffold() {
local target="$1"
log::run_step env "Building Yii2 Basic application (${target})..." \
composer::run_isolated create-project yiisoft/yii2-app-basic "$target" --no-install
}
# ============================================
# Init
# ============================================
function framework::yii2-basic::init() {
local env="$1"
local yii_env
yii_env="$(framework::yii2-basic::resolve_env "$env")"
log::run_step env "Initializing Yii2 Basic (${yii_env})..." \
framework::yii2-basic::_ensure_runtime_dirs
}
# ============================================
# Config
# ============================================
function framework::yii2-basic::config() {
log::run_step env "Generating Yii config..." \
framework::yii2-basic::_generate_config db.php
}
function framework::yii2-basic::_generate_config() {
local output="$(framework::yii2-basic::_config_root)/$1"
template::render \
"$(framework::yii2-basic::_template "db.${DB_ENGINE}")" \
"$output" \
DB_NAME="$DB_NAME" \
DB_USER="$DB_USER" \
DB_PASS="$DB_PASS"
}
# ============================================
# Console + env resolution
# ============================================
function framework::yii2-basic::console() { yii::exec "$@"; }
function framework::yii2-basic::resolve_env() { yii::resolve_env "$@"; }
# ============================================
# Vendors
# ============================================
function framework::yii2-basic::install_vendors() {
log::run_step auth info "Setting Composer auth for domain ${GIT_DOMAIN}..." \
composer::config_auth nocache "$GIT_DOMAIN" "$GIT_AUTH_TOKEN_ID" "$GIT_AUTH_TOKEN_PW"
log::run_step fs "Installing vendors..." \
composer::install
}
# ============================================
# Post-init hint
# ============================================
function framework::yii2-basic::post_init_hint() {
cat <<EOF
Next steps (run manually after):
dx yii migrate
EOF
}
# ============================================
# Private
# ============================================
function framework::yii2-basic::_ensure_runtime_dirs() {
local dirs=(
"$(ctx::root)/runtime"
"$(ctx::root)/web/assets"
)
local dir
for dir in "${dirs[@]}"; do
[[ -d "$dir" ]] || mkdir -p "$dir"
done
}

View file

@ -0,0 +1,83 @@
#!/usr/bin/env bash
function fs::write_file() {
local src="$1"
local dest="$2"
if platform::is_windows; then
# Windows: no sudo, just overwrite (assumes elevated shell if needed)
mv "$src" "$dest"
return $?
fi
# macOS / Linux
sudo tee "$dest" >/dev/null < "$src"
}
function fs::sed_inplace() {
local use_sudo=false
local suffix=""
local expr
local file
# Detect sudo flag
if [[ "$1" == "--sudo" ]]; then
use_sudo=true
shift
fi
# Detect suffix
if [[ "$1" == .* ]]; then
suffix="$1"
shift
fi
expr="$1"
file="$2"
local SUDO=""
if $use_sudo && ! platform::is_windows; then
SUDO="sudo"
fi
if platform::is_macos; then
if [[ -z "$suffix" ]]; then
$SUDO sed -i '' "$expr" "$file"
else
$SUDO sed -i "$suffix" "$expr" "$file"
fi
else
if [[ -z "$suffix" ]]; then
$SUDO sed -i "$expr" "$file"
else
$SUDO sed -i"$suffix" "$expr" "$file"
fi
fi
}
function fs::replace_block() {
local file="$1"
local start_marker="$2"
local end_marker="$3"
local content="$4"
local tmp
tmp="$(mktemp)"
# Remove existing block safely (no regex, no sed)
awk -v start="$start_marker" -v end="$end_marker" '
$0 == start {skip=1; next}
$0 == end {skip=0; next}
!skip
' "$file" > "$tmp"
# Only add leading newline if file has content and doesn't already end with one
if [[ -s "$tmp" ]]; then
local last_char
last_char="$(tail -c1 "$tmp")"
[[ "$last_char" != "" ]] && printf "\n" >> "$tmp"
fi
printf "%s\n" "$content" >> "$tmp"
echo "$tmp"
}

282
dxkit/modules/log.module.sh Normal file
View file

@ -0,0 +1,282 @@
#!/usr/bin/env bash
# ============================================
# Core
# ============================================
LOG_LEVEL=${LOG_LEVEL:-INFO}
# ============================================
# Internal
# ============================================
function internal::get_log_priority() {
case "$1" in
DEBUG) echo 0 ;;
INFO) echo 1 ;;
WARN) echo 2 ;;
ERROR) echo 3 ;;
SUCCESS) echo 4 ;;
*) echo 1 ;;
esac
}
function internal::log() {
local level="$1"
shift
local current_priority
local message_priority
current_priority=$(internal::get_log_priority "$LOG_LEVEL")
message_priority=$(internal::get_log_priority "$level")
if (( message_priority < current_priority )); then
return 0
fi
case "$level" in
DEBUG) color="\033[0;37m" ;; # cyan
INFO) color="\033[1;34m" ;; # blue
WARN) color="\033[1;33m" ;; # yellow
ERROR) color="\033[1;31m" ;; # red
SUCCESS)color="\033[1;32m" ;; # red
esac
echo -e "${color}=> ${level}:\033[0m $*"
}
function internal::icon() {
local context="$1"
local action="$2"
case "$context:$action" in
docker:log) echo "🐳 " ;;
docker:start) echo "🟢 🐳 " ;;
docker:stop) echo "🔴 🐳 " ;;
docker:success) echo "✅ 🐳 " ;;
docker:warning) echo "⚠️ 🐳 " ;;
docker:error) echo "❌ 🐳 " ;;
docker:logs) echo "📜 🐳 " ;;
docker:list) echo "🔍 🐳 " ;;
docker:build) echo "📦 🐳 " ;;
build:log) echo "🏗️ " ;;
build:start) echo "🟢 🏗️ " ;;
build:stop) echo "🔴 🏗️ " ;;
build:success) echo "✅ 🏗️ " ;;
build:warning) echo "⚠️ 🏗️ " ;;
build:error) echo "❌ 🏗️ " ;;
network:log) echo "🌐 " ;;
network:setup) echo "⚙️ 🌐 " ;;
network:stop) echo "🔴 🌐 " ;;
network:success) echo "✅ 🌐 " ;;
network:warning) echo "⚠️ 🌐 " ;;
network:error) echo "❌ 🌐 " ;;
auth:log) echo "🔑 " ;;
auth:setup) echo "⚙️ 🔑 " ;;
auth:login) echo "🔐 🔑 " ;;
auth:success) echo "✅ 🔑 " ;;
auth:warning) echo "⚠️ 🔑 " ;;
auth:error) echo "❌ 🔑 " ;;
env:log) echo "⚙️ " ;;
env:load) echo "📥 ⚙️ " ;;
env:success) echo "✅ ⚙️ " ;;
env:warning) echo "⚠️ ⚙️ " ;;
env:error) echo "❌ ⚙️ " ;;
fs:log) echo "📁 " ;;
fs:read) echo "📥 📁 " ;;
fs:write) echo "📤 📁 " ;;
fs:success) echo "✅ 📁 " ;;
fs:warning) echo "⚠️ 📁 " ;;
fs:error) echo "❌ 📁 " ;;
db:log) echo "🗄️ " ;;
db:start) echo "🟢 🗄️ " ;;
db:migrate) echo "📜 🗄️ " ;;
db:success) echo "✅ 🗄️ " ;;
db:warning) echo "⚠️ 🗄️ " ;;
db:error) echo "❌ 🗄️ " ;;
log:info) echo "🔹 " ;;
log:warn) echo "⚠️ " ;;
log:error) echo "❌ " ;;
log:success) echo "✅ " ;;
log:debug) echo "🔍 " ;;
*) echo "🔹" ;;
esac
}
function internal::get_context_icon() {
case "$1" in
docker) echo "🐳" ;;
build) echo "🏗️" ;;
network) echo "🌐" ;;
auth) echo "🔑" ;;
env) echo "⚙️" ;;
fs) echo "📁" ;;
db) echo "🗄️" ;;
*) echo "🔹" ;;
esac
}
# ============================================
# Loggers
# ============================================
function internal::log::info() { internal::log INFO "$*"; }
function internal::log::warn() { internal::log WARN "$*"; }
function internal::log::error() { internal::log ERROR "$*"; }
function internal::log::success() { internal::log SUCCESS "$*"; }
function internal::log::debug() { internal::log DEBUG "$*"; }
# ============================================
# Context Loggers
# ============================================
function log::context() {
local context="$1"
local action="$2"
shift 2
internal::log::info "$(internal::icon "$context" "$action") $*"
}
function log::warn_context() {
local context="$1"
local action="$2"
shift 2
internal::log::warn "$(internal::icon "$context" "$action") $*"
}
function log::error_context() {
local context="$1"
local action="$2"
shift 2
internal::log::error "$(internal::icon "$context" "$action") $*"
}
function log::success_context() {
local context="$1"
local action="$2"
shift 2
internal::log::success "$(internal::icon "$context" "$action") $*"
}
function log::debug_context() {
local context="$1"
local action="$2"
shift 2
internal::log::debug "$(internal::icon "$context" "$action") $*"
}
# ============================================
# Logger Helpers
# ============================================
function log::info() { log::context log info "$@"; }
function log::warn() { log::warn_context log warn "$@"; }
function log::error() { log::error_context log error "$@"; }
function log::success() { log::context log success "$@"; }
function log::debug() { log::debug_context log debug "$@"; }
function log::docker() { log::context docker log "$@"; }
function log::docker_start() { log::context docker start "$@"; }
function log::docker_stop() { log::context docker stop "$@"; }
function log::docker_success() { log::context docker success "$@"; }
function log::docker_logs() { log::context docker logs "$@"; }
function log::docker_list() { log::context docker list "$@"; }
function log::docker_build() { log::context docker build "$@"; }
function log::docker_warning() { log::warn_context docker warning "$@"; }
function log::docker_error() { log::error_context docker error "$@"; }
function log::build() { log::context build log "$@"; }
function log::build_start() { log::context build start "$@"; }
function log::build_stop() { log::context build stop "$@"; }
function log::build_success() { log::context build success "$@"; }
function log::build_warning() { log::warn_context build warning "$@"; }
function log::build_error() { log::error_context build error "$@"; }
function log::network() { log::context network log "$@"; }
function log::network_setup() { log::context network setup "$@"; }
function log::network_stop() { log::context network stop "$@"; }
function log::network_success() { log::context network success "$@"; }
function log::network_warning() { log::warn_context network warning "$@"; }
function log::network_error() { log::error_context network error "$@"; }
function log::auth() { log::context auth log "$@"; }
function log::auth_setup() { log::context auth setup "$@"; }
function log::auth_login() { log::context auth login "$@"; }
function log::auth_success() { log::context auth success "$@"; }
function log::auth_warning() { log::warn_context auth warning "$@"; }
function log::auth_error() { log::error_context auth error "$@"; }
function log::env() { log::context env log "$@"; }
function log::env_load() { log::context env load "$@"; }
function log::env_success() { log::context env success "$@"; }
function log::env_warning() { log::warn_context env warning "$@"; }
function log::env_error() { log::error_context env error "$@"; }
function log::fs() { log::context fs log "$@"; }
function log::fs_read() { log::context fs read "$@"; }
function log::fs_write() { log::context fs write "$@"; }
function log::fs_success() { log::context fs success "$@"; }
function log::fs_warning() { log::warn_context fs warning "$@"; }
function log::fs_error() { log::error_context fs error "$@"; }
function log::db() { log::context database log "$@"; }
function log::db_start() { log::context database start "$@"; }
function log::db_migrate() { log::context database migrate "$@"; }
function log::db_success() { log::context database success "$@"; }
function log::db_warning() { log::warn_context database warning "$@"; }
function log::db_error() { log::error_context database error "$@"; }
function log::run_step() {
local context="$1"
local mode="strict"
local description
shift
if [[ "$1" == "soft" || "$1" == "strict" || "$1" == "info" ]]; then
mode="$1"
shift
fi
description="$1"
shift
local icon=$(internal::get_context_icon "$context")
if [[ "$mode" == "info" ]]; then
internal::log::info "$icon $description"
else
internal::log::info "🔄 $icon $description"
fi
"$@"
local status=$?
# SUCCESS
if [[ $status -eq 0 ]]; then
if [[ "$mode" == "info" ]]; then
return 0
fi
internal::log::info "$icon $description"
return 0
fi
# FAILURE
if [[ "$mode" == "soft" || "$mode" == "info" ]]; then
internal::log::info "⚠️ $icon $description → skipped"
return 0
fi
internal::log::info "$icon $description → failed"
return $status
}

View file

@ -0,0 +1,97 @@
#!/usr/bin/env bash
# ============================================
# Ports
# ============================================
#function network::ports::in_use() {
# local port="$1"
#
# if command -v lsof >/dev/null 2>&1; then
# lsof -iTCP:"$port" -sTCP:LISTEN -t >/dev/null 2>&1
# else
# netstat -an | grep -q ":$port "
# fi
#}
function network::ports::in_use() {
local port="$1"
# Check Docker port bindings — reliable across platforms
if command -v docker >/dev/null 2>&1; then
docker ps --format '{{.Ports}}' | grep -q "0.0.0.0:${port}->" && return 0
docker ps --format '{{.Ports}}' | grep -q "\[::\]:${port}->" && return 0
fi
# Fallback to netstat — handles both Unix and Windows output formats
netstat -an 2>/dev/null | grep -qE ":${port}[^0-9].*LISTEN" && return 0
return 1
}
function network::ports::resolve() {
local value="$1"
# auto:X → dynamic allocation
if [[ "$value" == auto:* ]]; then
local base="${value#auto:}"
if ! [[ "$base" =~ ^[0-9]+$ ]]; then
log::network_error "Invalid auto port syntax: $value"
return 1
fi
# fallback if malformed (optional safety)
[[ -z "$base" ]] && base=8000
local p="$base"
while network::ports::in_use "$p"; do
((p++))
done
echo "$p"
return
fi
# static port
echo "$value"
}
function network::ports::resolve_many() {
local var
for var in "$@"; do
local value="${!var}"
local resolved
resolved="$(network::ports::resolve "$value")" || return 1
printf -v "$var" "%s" "$resolved"
done
}
function network::ports::persist() {
local ports_file
ports_file="$(ctx::artifact)/ports"
cat > "$ports_file" <<EOF
APP_PORT=${APP_PORT}
DB_PORT=${DB_PORT}
EOF
}
function network::ports::load() {
local ports_file
ports_file="$(ctx::artifact)/ports"
[[ -f "$ports_file" ]] || return 0
source "$ports_file"
export APP_PORT DB_PORT
}
function network::ports::prepare() {
[[ "$DB_PORT" == "auto" ]] && export DB_PORT="auto:$(docker::db::internal_port)"
network::ports::resolve_many APP_PORT DB_PORT
network::ports::persist
}

View file

@ -0,0 +1,27 @@
#!/usr/bin/env bash
phpstorm::config_path() { echo "$(ctx::root)/.idea"; }
function phpstorm::db_config() {
cat << EOF
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="cimlt_elevadores2026" uuid="4835f100-344a-4aa8-b98c-7c956c74c3e3">
<driver-ref>mariadb</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mariadb://localhost:3309</jdbc-url>
<jdbc-additional-properties>
<property name="database.introspection.mysql.dbe5060" value="true" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
<project>
EOF
}
function phpstorm::generate_db_config() {
phpstorm::db_config > "$(phpstorm::config_path)/dataSources.xml"
}

View file

@ -0,0 +1,10 @@
# modules/proxy.module.sh
function proxy::driver() { echo "${PROXY_DRIVER:-docker}"; }
function proxy::start() { "$(proxy::driver)::proxy::start"; }
function proxy::stop() { "$(proxy::driver)::proxy::stop"; }
function proxy::restart() { "$(proxy::driver)::proxy::restart"; }
function proxy::running() { "$(proxy::driver)::proxy::running"; }
function proxy::ensure() { proxy::running || proxy::start; }
function proxy::logs() { "$(proxy::driver)::proxy::logs"; }

View file

@ -0,0 +1,65 @@
#!/usr/bin/env bash
function template::path() {
local dir=""
local file=""
if [[ $# -eq 1 ]]; then
file="$1"
else
dir="$1"
file="$2"
fi
if [[ -n "$dir" ]]; then
echo "$(ctx::templates)/$dir/$file.template"
else
echo "$(ctx::templates)/$file.template"
fi
}
function template::render() {
local template="$1"
local target="$2"
shift 2
require_file "$template" || {
log::fs_error "Template not found: $template"
return 1
}
mkdir -p "$(dirname "$target")"
# Build env assignment list
local env_vars=()
for var in "$@"; do
env_vars+=("$var")
done
if ! env "${env_vars[@]}" envsubst < "$template" > "$target"; then
log::fs_error "Failed rendering template: $template"
return 1
fi
}
#function template::render() {
# local template="$1"
# local target="$2"
# shift 2
#
# require_file "$template" || {
# log::fs_error "Template not found: $template"
# return 1
# }
#
# mkdir -p "$(dirname "$target")"
#
# for var in "$@"; do
# export "$var"
# done
#
# if ! envsubst < "$template" > "$target"; then
# log::fs_error "Failed rendering template: $template"
# return 1
# fi
#}

View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
# modules/yii.module.sh
#
# Shared Yii utilities for both yii2-basic and yii2-advanced.
# Anything specific to one variant lives in its respective framework module.
# ============================================
# Console runner
# ============================================
function yii::exec() {
runtime::exec php yii "$@"
}
# ============================================
# Environment resolution
# ============================================
# Maps shorthand env names (dev/qly/prd) to Yii's expected names.
# Used by both basic and advanced framework modules.
function yii::resolve_env() {
case "$(string::lowercase "$1")" in
dev|development) echo "Development" ;;
qly|qa|quality) echo "Quality" ;;
prd|prod|production) echo "Production" ;;
*)
log::error "Unknown Yii environment: '$1'"
return 1
;;
esac
}

View file

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# dxkit/runtime/docker.runtime.sh
#
# Docker Compose runtime driver.
# Implements the runtime:: interface using docker compose.
function runtime::docker::up() {
local args=()
flag::enabled --recreate && args+=(--force-recreate)
flag::enabled --build && args+=(--build)
flag::enabled --foreground || args+=(-d)
docker::compose::up "${args[@]+"${args[@]}"}"
}
function runtime::docker::down() {
local args=()
flag::enabled --volumes && args+=(--volumes)
flag::enabled --orphans && args+=(--remove-orphans)
docker::compose::down "${args[@]+"${args[@]}"}"
}
function runtime::docker::logs() {
local args=()
flag::enabled --follow && args+=(-f)
docker::compose::logs "${args[@]+"${args[@]}"}"
}
function runtime::docker::list() {
docker::list_containers
}
function runtime::docker::status() {
docker::compose::ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
}
function runtime::docker::build() {
docker::compose::build "$@"
}
function runtime::docker::ensure() {
docker::compose::ensure_running
}
function runtime::docker::is_inside() {
docker::inside_container
}
function runtime::docker::exec() {
docker::compose::exec "$(runtime::service)" "$@"
}
function runtime::docker::shell() {
docker::compose::exec -it "$(runtime::service)" bash 2>/dev/null \
|| docker::compose::exec -it "$(runtime::service)" sh
}

View file

@ -0,0 +1,222 @@
#!/usr/bin/env bash
# dxkit/runtime/native.runtime.sh
#
# Native runtime driver — runs services directly on the host
# without containerization. Supports macOS (Homebrew) and Linux (systemd).
#
# To activate:
# export RUNTIME_DRIVER=native # in globals.env or local.env
# export NATIVE_WEB_SERVER=apache # apache | nginx (auto-detected if unset)
#
# Requirements:
# macOS — Homebrew, httpd or nginx, php
# Linux — systemd, apache2 or nginx, php-fpm
# ============================================
# Platform + web server detection
# ============================================
function runtime::native::_platform() {
case "$(uname -s)" in
Darwin) echo "macos" ;;
Linux) echo "linux" ;;
*)
log::error "Unsupported platform: $(uname -s)"
return 1
;;
esac
}
function runtime::native::_webserver() {
if [[ -n "${NATIVE_WEB_SERVER:-}" ]]; then
echo "$NATIVE_WEB_SERVER"
return
fi
local platform
platform="$(runtime::native::_platform)"
if [[ "$platform" == "macos" ]]; then
brew list httpd &>/dev/null && echo "apache" && return
brew list nginx &>/dev/null && echo "nginx" && return
else
systemctl list-unit-files apache2.service &>/dev/null && echo "apache" && return
systemctl list-unit-files nginx.service &>/dev/null && echo "nginx" && return
fi
log::error "No supported web server found (apache2 / nginx)"
log::error "Install one or set NATIVE_WEB_SERVER=apache|nginx"
return 1
}
# ============================================
# Service management — platform dispatch
# ============================================
function runtime::native::_service_start() {
local service="$1"
case "$(runtime::native::_platform)" in
macos) brew services start "$service" ;;
linux) sudo systemctl start "$service" ;;
esac
}
function runtime::native::_service_stop() {
local service="$1"
case "$(runtime::native::_platform)" in
macos) brew services stop "$service" ;;
linux) sudo systemctl stop "$service" ;;
esac
}
function runtime::native::_service_running() {
local service="$1"
case "$(runtime::native::_platform)" in
macos) brew services list | awk -v s="$service" '$1==s{print $2}' | grep -q "started" ;;
linux) systemctl is-active --quiet "$service" ;;
esac
}
function runtime::native::_service_logs() {
local service="$1"
case "$(runtime::native::_platform)" in
macos)
local log_dir
log_dir="$(brew --prefix)/var/log"
tail -f "${log_dir}/${service}/error.log" "${log_dir}/${service}/access.log" 2>/dev/null \
|| tail -f "${log_dir}/${service}.log" 2>/dev/null
;;
linux)
sudo journalctl -u "$service" -f --no-pager
;;
esac
}
# ============================================
# Service name resolution
# ============================================
function runtime::native::_apache_service() {
case "$(runtime::native::_platform)" in
macos) echo "httpd" ;;
linux) echo "apache2" ;;
esac
}
function runtime::native::_nginx_service() { echo "nginx"; }
function runtime::native::_fpm_service() {
local php_ver
php_ver="$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' 2>/dev/null || echo "8.3")"
case "$(runtime::native::_platform)" in
macos) echo "php@${php_ver}" ;;
linux) echo "php${php_ver}-fpm" ;;
esac
}
function runtime::native::_active_services() {
local ws
ws="$(runtime::native::_webserver)"
case "$ws" in
apache) echo "$(runtime::native::_apache_service)" ;;
nginx) echo "$(runtime::native::_fpm_service)"
echo "$(runtime::native::_nginx_service)" ;;
esac
}
# ============================================
# Interface implementation
# ============================================
function runtime::native::up() {
log::info "Starting native runtime ($(runtime::native::_webserver))..."
local service
while IFS= read -r service; do
runtime::native::_service_start "$service"
done < <(runtime::native::_active_services)
log::success "Stack up"
}
function runtime::native::down() {
log::info "Stopping native runtime..."
# Stop in reverse order
local services=()
while IFS= read -r service; do
services+=("$service")
done < <(runtime::native::_active_services)
for ((i=${#services[@]}-1; i>=0; i--)); do
runtime::native::_service_stop "${services[i]}"
done
}
function runtime::native::ensure() {
local service
while IFS= read -r service; do
if ! runtime::native::_service_running "$service"; then
runtime::native::_service_start "$service"
fi
done < <(runtime::native::_active_services)
}
function runtime::native::logs() {
local ws
ws="$(runtime::native::_webserver)"
case "$ws" in
apache)
runtime::native::_service_logs "$(runtime::native::_apache_service)"
;;
nginx)
runtime::native::_service_logs "$(runtime::native::_nginx_service)"
;;
esac
}
function runtime::native::list() {
printf "%-25s %s\n" "SERVICE" "STATUS"
printf "%-25s %s\n" "-------" "------"
local service
while IFS= read -r service; do
if runtime::native::_service_running "$service"; then
printf "%-25s %s\n" "$service" "running"
else
printf "%-25s %s\n" "$service" "stopped"
fi
done < <(runtime::native::_active_services)
}
function runtime::native::status() {
printf "%-20s %s\n" "Driver:" "native"
printf "%-20s %s\n" "Platform:" "$(runtime::native::_platform)"
printf "%-20s %s\n" "Web server:" "$(runtime::native::_webserver)"
printf "%-20s %s\n" "App root:" "$(ctx::root)"
echo ""
runtime::native::list
}
function runtime::native::build() {
# Native has no images to build — but vhost generation still needs to happen
log::info "Native runtime has no build step (services run directly)"
log::info "Reload configs with: dx proxy restart"
}
function runtime::native::is_inside() {
# Native always runs on host — there's no "inside" concept
return 1
}
function runtime::native::exec() {
# No container boundary — run directly in the app root
cd "$(ctx::root)" && "$@"
}
function runtime::native::shell() {
cd "$(ctx::root)" && exec "${SHELL:-bash}"
}

89
dxkit/runtime/runtime.sh Normal file
View file

@ -0,0 +1,89 @@
#!/usr/bin/env bash
# dxkit/runtime/runtime.sh
#
# Runtime abstraction layer.
# Commands and modules call runtime::* — this file dispatches to
# the active driver based on RUNTIME_DRIVER (default: docker).
# ============================================
# Driver loader
# ============================================
RUNTIME_DRIVER="${RUNTIME_DRIVER:-docker}"
function runtime::driver() { echo "${RUNTIME_DRIVER}"; }
function runtime::service() { echo "${APP_SERVICE:-app}"; }
function runtime::_load_driver() {
local driver_file
driver_file="$(ctx::dxkit)/runtime/${RUNTIME_DRIVER}.runtime.sh"
if [[ ! -f "$driver_file" ]]; then
log::error "Unknown runtime driver: '${RUNTIME_DRIVER}'"
log::error "Expected: ${driver_file}"
return 1
fi
source "$driver_file"
}
runtime::_load_driver
# ============================================
# Interface
# ============================================
function runtime::up() { "runtime::$(runtime::driver)::up" "$@"; }
function runtime::down() { "runtime::$(runtime::driver)::down" "$@"; }
function runtime::logs() { "runtime::$(runtime::driver)::logs" "$@"; }
function runtime::list() { "runtime::$(runtime::driver)::list" "$@"; }
function runtime::status() { "runtime::$(runtime::driver)::status" "$@"; }
function runtime::build() { "runtime::$(runtime::driver)::build" "$@"; }
function runtime::ensure() { "runtime::$(runtime::driver)::ensure" "$@"; }
function runtime::is_inside() { "runtime::$(runtime::driver)::is_inside" "$@"; }
# Exec and shell are special — they handle "running inside the runtime"
# vs "running on host" transparently
function runtime::exec() {
local args=("$@")
[[ "${#args[@]}" -eq 0 ]] && return 1
if runtime::is_inside; then
"${args[@]}"
else
runtime::ensure
"runtime::$(runtime::driver)::exec" "${args[@]}"
fi
}
function runtime::shell() {
if runtime::is_inside; then
runtime::_local_shell
else
runtime::ensure
"runtime::$(runtime::driver)::shell"
fi
}
# Conditionally run a function based on a flag value.
# Driver-agnostic — useful in build pipelines.
function runtime::run_if() {
local flag="$1"
shift
if [[ "${!flag:-}" == true ]]; then
"$@" || return 1
fi
}
# ============================================
# Internal helpers
# ============================================
function runtime::_local_shell() {
if command -v bash >/dev/null 2>&1; then
bash
else
sh
fi
}

View file

@ -0,0 +1,119 @@
# ===========================================
# Frontend — ${DOMAIN}
# ===========================================
<VirtualHost *:80>
ServerName ${DOMAIN}
ServerAdmin webmaster@localhost
AddDefaultCharset UTF-8
DocumentRoot ${APP_PATH}/frontend/web/
# ===========================================
# Backend + API aliases (path-based routing)
# ===========================================
Alias /admin ${APP_PATH}/backend/web/
Alias /api ${APP_PATH}/api/web/
<Directory ${APP_PATH}/frontend/web/>
Options FollowSymLinks
AllowOverride none
Require all granted
DirectoryIndex index.php index.html
RewriteEngine On
# Don't rewrite requests headed for /admin or /api
RewriteCond %{REQUEST_URI} !^/admin
RewriteCond %{REQUEST_URI} !^/api
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
</Directory>
<Directory ${APP_PATH}/backend/web/>
Options FollowSymLinks
AllowOverride none
Require all granted
DirectoryIndex index.php index.html
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
</Directory>
<Directory ${APP_PATH}/api/web/>
Options FollowSymLinks
AllowOverride none
Require all granted
DirectoryIndex index.php index.html
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
</Directory>
# ===========================================
# Security: block dotfiles globally
# ===========================================
RewriteEngine On
RewriteRule (^|/)\. - [F,L]
# ===========================================
# Friendly route fixes
# ===========================================
RewriteCond %{REQUEST_URI} ^/admin$
RewriteRule ^ /admin/ [R=301,L]
RewriteCond %{REQUEST_URI} ^/api$
RewriteRule ^ /api/ [R=301,L]
# ===========================================
# Logs
# ===========================================
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
# ===========================================
# Backend — ${BACKEND_DOMAIN}
# ===========================================
<VirtualHost *:80>
ServerName ${BACKEND_DOMAIN}
ServerAdmin webmaster@localhost
AddDefaultCharset UTF-8
DocumentRoot ${APP_PATH}/backend/web/
<Directory ${APP_PATH}/backend/web/>
Options FollowSymLinks
AllowOverride none
Require all granted
DirectoryIndex index.php index.html
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
</Directory>
# ===========================================
# Security: block dotfiles globally
# ===========================================
RewriteEngine On
RewriteRule (^|/)\. - [F,L]
# ===========================================
# Logs
# ===========================================
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

View file

@ -0,0 +1,36 @@
# ===========================================
# Frontend — ${DOMAIN}
# ===========================================
<VirtualHost *:80>
ServerName ${DOMAIN}
ServerAdmin webmaster@localhost
AddDefaultCharset UTF-8
DocumentRoot ${APP_PATH}/web/
<Directory ${APP_PATH}/web/>
Options FollowSymLinks
AllowOverride none
Require all granted
DirectoryIndex index.php index.html
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
</Directory>
# ===========================================
# Security: block dotfiles globally
# ===========================================
RewriteEngine On
RewriteRule (^|/)\. - [F,L]
# ===========================================
# Logs
# ===========================================
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

View file

@ -0,0 +1,50 @@
FROM ${BASE_APP_IMAGE}
RUN groupmod -g 1000 www-data && usermod -u 1000 -g www-data www-data
RUN apt-get update && apt-get install --no-install-recommends -y \
apt-transport-https \
gettext \
git \
unzip \
vim \
tree \
wget \
gnupg2 \
dialog \
nano \
libldap2-dev \
cron \
curl \
openssl \
jq \
openssh-server
# Install Xdebug
#RUN pecl install xdebug
#RUN docker-php-ext-enable xdebug
# Enable Apache mod_rewrite
RUN a2enmod rewrite
# Enable Apache SSL module
RUN a2enmod ssl
RUN a2dissite 000-default default-ssl
# Install Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY dxkit/docker/entrypoint.sh /entrypoint.sh
RUN echo "root:Docker!" | chpasswd && chmod u+x /entrypoint.sh
RUN apt-get install -y locales && \
echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
echo "pt_PT.UTF-8 UTF-8" >> /etc/locale.gen && \
locale-gen && \
apt-get update
RUN rm -rf /var/lib/apt/lists/*
EXPOSE 80 443 2222
ENTRYPOINT [ "/entrypoint.sh" ]

View file

@ -0,0 +1,38 @@
services:
app:
build:
context: ${CTX_BUILD}
dockerfile: ${IMAGE_PATH}
image: ${APP_IMAGE}:${ENVIRONMENT}
restart: unless-stopped
ports:
- "${APP_PORT}:80"
volumes:
- ${CTX_APP}:${APP_PATH}
- ${CTX_APACHE_LOGS}:/var/log/apache2
- ${CTX_APACHE_VHOSTS}:/etc/apache2/sites-enabled
- ${CTX_APACHE_CERTS}:/usr/local/share/ca-certificates/custom:ro
labels:
${TRAEFIK_LABELS}
depends_on:
- db
networks:
- app-network
- proxy
db:
image: ${DB_IMAGE}
restart: unless-stopped
environment:
${DB_ENVIRONMENT}
volumes:
${DB_VOLUMES}
ports:
- "${DB_STACK_PORT}"
networks:
- app-network
networks:
app-network:
proxy:
external: true

View file

@ -0,0 +1,11 @@
'db' => [
'class' => \yii\db\Connection::class,
'driverName' => 'sqlsrv',
'dsn' => 'sqlsrv:server=db;database=${PROJECT_NAME};encrypt=false',
'username' => '${DB_USER}',
'password' => '${DB_PASS}',
'charset' => 'utf8',
'attributes' => [
\PDO::SQLSRV_ATTR_ENCODING => \PDO::SQLSRV_ENCODING_SYSTEM
]
],

View file

@ -0,0 +1,7 @@
'db' => [
'class' => \yii\db\Connection::class,
'dsn' => 'mysql:host=db;dbname=${DB_NAME}',
'username' => '${DB_USER}',
'password' => '${DB_PASS}',
'charset' => 'utf8',
],

View file

@ -0,0 +1,27 @@
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning', 'info'],
],
'sync' => [
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
'logFile' => '@runtime/logs/sync.log',
'exportInterval' => YII_DEBUG ? 1 : 100,
'maxLogFiles' => 100,
'categories' => ['common\user\*'],
'logVars' => []
],
'requests' => [
'class' => 'yii\log\FileTarget',
'levels' => ['info'],
'logFile' => '@runtime/logs/requests.log',
'exportInterval' => YII_DEBUG ? 1 : 100,
'maxLogFiles' => 100,
//'categories' => ['common\user\*'],
'logVars' => []
]
],
],

View file

@ -0,0 +1,7 @@
<?php
return [
'components' => [
${YII_MAIN_LOCAL_COMPONENTS}
],
];

View file

@ -0,0 +1,18 @@
<?php
return [
'class' => \yii\db\Connection::class,
'driverName' => 'sqlsrv',
'dsn' => 'sqlsrv:server=db;database=${PROJECT_NAME};encrypt=false',
'username' => '${DB_USER}',
'password' => '${DB_PASS}',
'charset' => 'utf8',
'attributes' => [
\PDO::SQLSRV_ATTR_ENCODING => \PDO::SQLSRV_ENCODING_SYSTEM
]
// Schema cache options (for production environment)
//'enableSchemaCache' => true,
//'schemaCacheDuration' => 60,
//'schemaCache' => 'cache',
];

View file

@ -0,0 +1,14 @@
<?php
return [
'class' => \yii\db\Connection::class,
'dsn' => 'mysql:host=db;dbname=${DB_NAME}',
'username' => '${DB_USER}',
'password' => '${DB_PASS}',
'charset' => 'utf8',
// Schema cache options (for production environment)
//'enableSchemaCache' => true,
//'schemaCacheDuration' => 60,
//'schemaCache' => 'cache',
];