Multi-repository docker-compose workspace orchestrator: one YAML, one `cupli up`, every container running.
Project description
Multi-repository docker-compose workspace orchestrator. One YAML, one `cupli up`, every container running.
๐ท๐บ README ะฝะฐ ััััะบะพะผ
cupli is for projects where each component (backend, frontend,
worker, shared SDK, infra) lives in its own git repository but the
whole stack needs to come up on one machine with one command. It builds
on docker compose without replacing it.
- Spec-first. One
space.cupli.yamldeclares everything: repos, bases, mounts, services, shortcuts. - Inline or external compose. Define services in YAML directly, or
point at an existing
docker-compose.yml. Mix freely. - Multi-repo git.
cupli git status / pull / fetch / checkoutoperate across every cloned component in parallel, with per-repo selectors and per-repo branch maps. - Variable scope. Space โ bases (C3) โ app, with
${VAR}and${VAR:-default}interpolation everywhere. - Branch pinning + drift.
branch: mainon a component is honoured bycupli init(git clone -b) and surfaced incupli git statuswhen the working tree drifts off. - Mount toggling.
cupli mounts attach <name>bind-mounts a shared SDK into N containers without YAML edits. - Shell completion for every name (apps, services, mounts, tags, shortcuts, error codes).
Table of contents
- Install
- Quick-start
- Concepts
- The
space.cupli.yamlreference - CLI reference
- Recipes
- IDE setup
- Comparison with similar tools
- Limitations
- Troubleshooting + error codes
Install
uv tool install cupli # recommended
# or
pipx install cupli
# or
pip install --user cupli
Verify:
cupli -V # cupli 0.1.0 (script-friendly)
cupli --version # full info: python, platform, deps
Cupli requires Python โฅ 3.10. Docker / docker compose must be on PATH.
Shell completion
One-shot, picks your shell automatically from $SHELL:
cupli completion install
Or pin the shell:
cupli completion install --shell bash # bash | zsh | fish | pwsh
cupli completion show --shell zsh > ~/.zsh/completions/_cupli
Quick-start
mkdir my-workspace && cd my-workspace
cupli init --name my-workspace # scaffolds space.cupli.yaml + .env + .locals/
$EDITOR space.cupli.yaml # describe your apps
cupli up # build + start everything
cupli ps # see what's running
cupli logs my-api -f
cupli down # tear down
The smallest possible workspace:
# space.cupli.yaml
schema_version: 1
name: hello
apps:
cache:
service: # inline compose-spec
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
ports: ["6379:6379"]
cupli up
cupli exec -c cache -- redis-cli ping # PONG
See docs/examples/minimal/ for the same workspace with comments.
Concepts
Space
A space is the unit cupli operates on โ one space.cupli.yaml,
one project, one docker-compose project. Spaces have a name:, which
also doubles as the docker-compose project name and the default network
name.
App
An app is what cupli starts and stops. Each app binds to one or more docker-compose services. The binding is declared one of four ways:
- Implicit โ service name equals app name.
service: "name"โ bind to an existing compose service by name.service: {image: ..., command: ..., ...}โ inline single-service spec, no separate compose file.services: { name1: {...}, name2: {...} }โ compound app with multiple compose services (think: api + celery workers + beat).
In forms 3 and 4 the dict accepts any docker-compose service
attribute (image, build, command, environment, depends_on,
healthcheck, volumes, restart, โฆ). Cupli reserves vars and
ports for its own injection logic; everything else is passed through
to docker-compose verbatim via a generated docker-compose.inline.yml.
Base
A base is a reusable template. Apps cite bases via
bases: [name1, name2] and inherit vars:, envs:, composes:,
repo: from them in C3 linearisation order. Bases keep boilerplate
DRY across apps that share runtime.
Mount
A mount is a host-to-container bind that toggles on/off without
editing YAML. Useful for hot-swapping a vendored SDK to a local
checkout. cupli mounts attach/detach <name> flips the state.
Service
A service in cupli is exactly what docker-compose calls a service โ a container declaration. Apps own services; one app may own many.
Workspace registry
Spaces register themselves in ~/.config/cupli/spaces.json so you can
operate by name from anywhere:
cupli workspace add -n shop -f ~/work/shop/space.cupli.yaml
cupli -s shop up
cupli workspace select shop # sticky: subsequent cupli calls target shop
cupli workspace unselect # back to cwd-detect
The space.cupli.yaml reference
The full reference is space.cupli.yaml at the
repo root and copied to
docs/examples/full-reference/. Below is the
schema with one-line descriptions.
Top level
| Key | Type | Default | What it does |
|---|---|---|---|
schema_version |
int | โ | Version pin. Only 1 is supported. |
name |
string | โ | Project identifier. Used as docker-compose project name and default network name. Matches ^[A-Za-z][A-Za-z0-9_-]*$. |
cupli_min / cupli_max |
string | "*" |
โ | Tool-version guards. |
extends |
string | โ | Path to a parent space (one level only in v1). |
envs |
list[string] | [] |
.env files loaded into space scope, before vars. |
vars |
map[str, str] | {} |
Space-scope variables; visible everywhere; written to override.env for docker-compose substitution. |
bases |
map[str, base] | {} |
Reusable templates. |
apps |
map[str, app] | {} |
Run units. |
mounts |
map[str, mount] | {} |
Toggleable bind-mounts. |
hooks |
map[str, hook-override] | {} |
Per-target tweaks for cupli hooks install. |
commands |
map[str, command-shortcut] | {} |
cupli sc <name> / cupli <name> (with top_level: true). |
networks |
map[str, dict] | {} |
Top-level docker-compose networks: block. Values are compose-spec verbatim (driver, name, ipam, etc.). Cupli's default network is merged in automatically. |
bases.<name>
| Key | Type | Default | What it does |
|---|---|---|---|
path |
string | ${BASES_PATH}/<name> |
On-disk location. |
repo |
string | โ | Git URL (omit for an in-place base). |
branch |
string | โ | Branch to clone (git clone -b <branch>). |
post_clone |
string | โ | Shell command run on host after a successful clone. |
init_vars |
map | {} |
Env exported to clone + post_clone. |
vars |
map | {} |
Variables contributed to inheriting apps. |
envs |
list[string] | [] |
Env files loaded into the base scope. |
composes |
list[string] | [] |
Compose-fragments prepended to inheriting apps' COMPOSE_FILE chain. |
apps.<name>
| Key | Type | Default | What it does |
|---|---|---|---|
path |
string | ${APPS_PATH}/<name> |
On-disk location. |
repo |
string | โ | Git URL. |
branch |
string | โ | Branch to clone. cupli git status flags drift. |
post_clone |
string | โ | Shell command run on host after clone. |
init_vars |
map | {} |
Env exported to clone + post_clone. |
bases |
list[string] | [] |
Bases to inherit (C3 multi-inherit). |
deps |
map[str, list[modes]] | {} |
Cross-app depends_on (mode-filtered). |
tags |
list[string] | [] |
For cupli up --tag <tag>. |
mode |
enum | up |
up (long-running), oneshot (run-once), disabled. |
composes |
list[string] | [] |
External compose files. |
service |
string | dict | โ | Single-service binding. Dict form is inline compose-spec. |
services |
map[str, dict] | list[str] | โ | Multi-service map (each value is a compose-spec with optional cupli-only vars and ports) or a bare list of service names (equivalent to a map with empty overrides). Mutually exclusive with service. |
vars |
map | {} |
Variables; injected as environment on every managed service. |
envs |
list[string] | [] |
Env files loaded into app scope. |
ports |
list[string] | [] |
Compose-style port mappings; injected into the app's primary service (or every service in services:). |
forward_ssh |
bool | false |
Mount $SSH_AUTH_SOCK into the container. |
Service binding forms โ all four are valid
# 1) implicit (service name = app name)
apps:
api: {}
# 2) string (rename binding)
apps:
redis:
service: agora-redis # bind to compose service `agora-redis`
composes: [./compose.yml]
# 3) inline single-service (any compose attribute is fair game)
apps:
cache:
service:
image: memcached:1.6
command: ["memcached", "-m", "64"]
healthcheck: {test: ["CMD", "echo", "stats", "|", "nc", "localhost", "11211"]}
vars: {LOG_LEVEL: info}
ports: ["11211:11211"]
# 4) services map (one app, N compose services)
apps:
backend:
vars: {DATABASE_URL: ...} # shared with every service below
services:
backend:
image: ${IMAGE}
command: [uvicorn, app.main:app]
celery-worker:
image: ${IMAGE}
command: [celery, -A, app.tasks, worker]
vars: {CELERY_LOG_LEVEL: info} # per-service override (merged)
ports: [] # explicit empty: opt out of app-level ports
# 4b) services as a bare list โ same as `{name: {}}` for each
apps:
fleet:
composes: [${APP_PATH}/docker-compose.yml]
services:
- api
- worker
- beat
${VAR}inside inline compose-spec (service.build.context: ${APP_PATH}) is substituted by docker-compose, not cupli โ use${<APP_NAME>_APP_PATH}(per-component path-var, which cupli writes intooverride.env) when you need the path of a specific app. Bare${APP_PATH}only resolves where cupli does the substitution itself (e.g.composes:).
mounts.<name>
| Key | Type | Default | What it does |
|---|---|---|---|
path |
string | ${MOUNTS_PATH}/<name> |
Host source dir. |
repo |
string | โ | Git URL. |
branch |
string | โ | Branch to clone. |
post_clone |
string | โ | After-clone host command. |
hosted_in |
list[string] | required | App names whose every service gets the bind. |
exec_path |
string | required | Absolute POSIX path inside container. |
mode |
enum | rw |
rw | ro. |
mac_volume |
enum | โ | macOS volume consistency hint. |
envs |
list[string] | [] |
Env files. |
vars |
map | {} |
Variables. |
commands.<name>
| Key | Type | Default | What it does |
|---|---|---|---|
container |
string | required | App name whose primary service runs the command. |
run |
string | required | Shell command line. |
workdir |
string | โ | Working directory inside the container. |
help |
string | โ | Short help shown in cupli --help. |
top_level |
bool | false |
When true, also exposes as cupli <name> (alongside cupli sc <name>). |
Auto-vars (always interpolatable)
- Space scope โ
SPACE_NAME,SPACE_PATH,APPS_DIR,APPS_PATH,BASES_DIR,BASES_PATH,MOUNTS_DIR,MOUNTS_PATH,LOCALS_DIR,LOCALS_PATH,NETWORK,COMPOSE_PROJECT_NAME. - Per-component โ
<NAME>_APP_PATHfor every app,<NAME>_BASE_PATHfor every base,<NAME>_MOUNT_PATHfor every mount. Name is upper-cased with-mapped to_. Visible in YAML AND inoverride.env. - App / base โ
APP_NAME,APP_PATH,APP_LOCAL_PATH(apps only). - Mount โ
MOUNT_NAME,MOUNT_PATH,MOUNT_HOST,MOUNT_EXEC_PATH.
Default paths: APPS_PATH = $SPACE_PATH/src/apps, similarly for
bases and mounts. Override per-component with an explicit path:.
Interpolation rules
${VAR}and${VAR:-literal-default}only; bareword$VARis not recognised.- Nested
${...}inside a default is not supported. The default is literal. - Cycles raise
E014. - Unknown vars resolve to
""with a yellow warning. Pass--strict-varsto make them hard errors (E016). - Shadowing a reserved auto-var name raises
E015unless--allow-shadowis passed.
CLI reference
cupli --help lists everything. Highlights:
Lifecycle
| Command | What |
|---|---|
cupli up [services] [--tag t] [--mode m] [--build] [--pull p] |
docker compose up. Service args can be app names OR individual compose-service names from a compound app's services: map. |
cupli stop [services] [--tag t] |
docker compose stop. |
cupli restart [services] [--tag t] [--hard] |
restart, or down+up with --hard. |
cupli down [-v] [--images] |
down --remove-orphans, optional volumes + images. |
cupli ps [--tag t] |
running services table. |
cupli logs [service] [-f] |
per-service or all. |
cupli build [services] [--tag t] |
build images. |
cupli pull [services] [--tag t] |
pull images. |
cupli compose -- <args> |
pass-through to docker compose. |
cupli config |
merged compose configuration. |
cupli watch [services] |
docker compose watch โ for develop.watch declared on a service. |
--mode default|hook|full filters cross-app deps: by their declared
mode-list. Use it to express dev-vs-prod-style dependency sets:
api: {deps: {redis: [default, full]}} pulls redis on both modes;
audit: {deps: {redis: [full]}} skips it under --mode default.
Exec / run
| Command | What |
|---|---|
cupli exec -c <service> -- <cmd> |
run inside a running container. |
cupli run -c <service> -- <cmd> |
one-shot container (run --rm). |
cupli shell -c <service> |
open /bin/bash (override with --shell). |
cupli wrap -c <app> -- <cmd> |
run on the host with the app's env exported. |
cupli env [-c <app>] [--export] |
print resolved env. |
Shortcuts
| Command | What |
|---|---|
cupli sc |
list declared commands:. |
cupli sc <name> [args] |
run shortcut. |
cupli <name> |
same shortcut when top_level: true. |
Workspace
| Command | What |
|---|---|
cupli init [-n name] [--path .] [--force] [--no-sync] [--no-ide] |
scaffold + register. Creates space.cupli.yaml, .env, .locals/; src/apps/, src/bases/, src/mounts/ are created lazily by cupli space sync when a declared component first needs them. |
cupli workspace add -n <name> -f <file> |
register an existing space. |
cupli workspace list |
every registered space with * on the active one. |
cupli workspace select <name> |
sticky active selection. |
cupli workspace unselect |
clear it (cwd-detect resumes). |
cupli workspace current |
what would be targeted right now. |
cupli workspace remove <name> |
drop from registry (filesystem untouched). |
cupli space sync [--apps/--bases/--mounts] [--pull] |
clone declared repos + optional pull. |
cupli space doctor [--strict] |
validate paths + repos. |
Git (across every cloned component)
| Command | What |
|---|---|
cupli git status [targets] |
status table. Flags drifted when working tree branch โ pinned. |
cupli git pull [targets] [--rebase] |
parallel pull. |
cupli git fetch [targets] |
parallel fetch. |
cupli git checkout <branch> [-t target] [-m name=branch] |
branch switch with per-repo overrides. |
Mounts
| Command | What |
|---|---|
cupli mounts list |
every declared mount and its state. |
cupli mounts attach <name> |
bind-mount into hosted_in apps. |
cupli mounts detach <name> |
remove the bind. |
Hooks
| Command | What |
|---|---|
cupli hooks install <hooks-dir> [--scope all/apps/bases/mounts] [--target name] |
install per-target git-hook shims. |
cupli hooks remove [--scope] [--target] |
remove shims. |
Hook scripts under <hooks-dir>/<hook-name>/*.sh are dispatched into the
target's container. A first-line directive overrides the defaults:
#!/usr/bin/env bash
# cupli: container=api workdir=/app shell=sh
echo "running inside the container"
shell=sh switches the in-container interpreter from bash (default) to
POSIX sh โ useful for alpine-based images that have no bash.
IDE
| Command | What |
|---|---|
cupli ide setup [--target auto/vscode/pycharm/all] [--force] |
write JSON-schema mappings for the workspace. auto walks up looking for .vscode/ / .idea/ (stops at the git-repo boundary) and writes only for the editor(s) found. |
Diagnostics
| Command | What |
|---|---|
cupli graph |
tree of bases / apps / mounts / commands. |
cupli dashboard [-i interval] |
live status table. |
cupli stats [--follow] |
docker stats scoped to the workspace. |
cupli explain <code> |
error code reference. |
Recipes
Single inline service, no compose file
schema_version: 1
name: hello
apps:
cache:
service:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
ports: ["6379:6379"]
Compound app (celery)
apps:
backend:
vars: {DATABASE_URL: ..., REDIS_URL: ...}
services:
backend:
image: ${IMAGE}
command: [uvicorn, app.main:app]
ports: ["8000:8000"]
celery-worker:
image: ${IMAGE}
command: [celery, -A, app.tasks, worker]
depends_on: [backend]
celery-beat:
image: ${IMAGE}
command: [celery, -A, app.tasks, beat]
depends_on: [backend]
Full file: docs/examples/celery/.
Multi-repo workspace
repo:+branch:on every app/mount that has its own checkout.cupli initclones them undersrc/apps/<name>.cupli git statusaggregates state across all of them.
Full file: docs/examples/multi-repo-shop/.
Renaming a compose service
apps:
redis:
service: agora-redis # compose-fragment calls it agora-redis
composes: [./compose.yml]
Hot-swap a vendored SDK
mounts:
shared-sdk:
repo: git@github.com:example/shared-sdk.git
hosted_in: [shop-web]
exec_path: /opt/shared-sdk
cupli mounts attach shared-sdk # mount in
cupli mounts detach shared-sdk # mount out
Per-repo branch on checkout
cupli git checkout main # all repos โ main
cupli git checkout main -t shop-api -t shop-web # only these two
cupli git checkout -m shop-api=feature/x -m shop-web=main
Tag-based filtering
apps:
postgres: {tags: [infra, db]}
redis: {tags: [infra, cache]}
shop-api: {tags: [backend]}
cupli up --tag infra # only postgres + redis
Targeting one service of a compound app
cupli up backend # all services owned by `backend`
cupli up celery-worker # only that compose-service of the compound app
cupli up celery-worker celery-beat # several specific services
Custom networks
networks:
shared-net:
name: my-org-shared
driver: bridge
monitoring:
driver: bridge
apps:
api:
service:
image: ...
networks: [default, shared-net] # `default` is cupli's auto network
metrics:
service:
image: ...
networks: [monitoring]
IDE setup
cupli init and cupli ide setup write JSON-schema mappings for the
editor(s) it detects around the workspace (auto walks parent dirs up
to the git-repo boundary, looking for .vscode/ or .idea/). Every
generated space.cupli.yaml also carries a
# yaml-language-server: $schema=... directive on line 1, so modern
editors pick the schema up even without the config files.
VS Code
Install the YAML extension. That's it โ the schema directive is honoured. Optional pinning if you prefer:
// .vscode/settings.json
{
"yaml.schemas": {
"./space.schema.json": "space.cupli.yaml"
}
}
PyCharm / IntelliJ
The bundled YAML plugin understands # yaml-language-server: $schema=
directives in IntelliJ 2023.2+. If yours doesn't:
Settings โ Languages & Frameworks โ Schemas and DTDs โ JSON Schema Mappings โ +
- Name:
cupli space - Schema file: pick
space.schema.jsonfrom the repo root - File path pattern:
space.cupli.yaml(or*.cupli.yaml)
neovim with LSP
yaml-language-server understands the inline directive. Make sure it's
running on *.cupli.yaml.
Generating the schema
The schema lives at space.schema.json and is
generated from the Pydantic models:
make schema # or: uv run python scripts/generate_schema.py
Re-run after changing src/cupli/domain/models.py.
Why JSON Schema for a YAML file? JSON Schema is the editor-side contract โ both
yaml-language-server(VS Code, neovim) and IntelliJ's bundled YAML support understand it natively and apply it to YAML files. No conversion needed.
Custom file icon for space.cupli.yaml
JSON-Schema mappings don't change file icons. If you want the cupli logo on the file in your project tree:
VS Code (with Material Icon Theme)
โ add to .vscode/settings.json:
{
"material-icon-theme.files.associations": {
"space.cupli.yaml": "docs/resources/logo.svg",
"*.cupli.yaml": "docs/resources/logo.svg"
}
}
PyCharm / IntelliJ โ a custom file icon needs a real Kotlin plugin
(FileIconProvider extension point); JSON-Schema mappings alone can't
do it.
Comparison with similar tools
| cupli | docker compose | Tilt | Skaffold | Garden | Dip | |
|---|---|---|---|---|---|---|
| Multi-repo | โ | โ | partial | โ | โ | โ |
| Spec format | YAML | YAML | Tiltfile (Python) | YAML | YAML | YAML |
| docker-compose | โ | โ | partial | partial | โ | โ |
| Kubernetes | โ | โ | โ | โ | โ | โ |
| UI / dashboard | basic | โ | rich | basic | rich | โ |
| Mount toggling | โ | โ | โ | โ | โ | โ |
| Cross-repo git | โ | โ | โ | โ | โ | โ |
| Branch pinning + drift | โ | โ | โ | โ | โ | โ |
| Live reload | via compose watch | partial | rich | โ | โ | โ |
Cupli's niche: docker-compose-first multi-repo workspaces. If you're on
Kubernetes locally, Tilt or Garden are better. If everything's in one
repo, plain docker compose + make is fine.
Limitations
- One project at a time. A
space.cupli.yamlmaps to exactly one docker-compose project. To compose two cupli workspaces, share infra via external networks. - No native Kubernetes. Compose-only.
- No remote build farm. Builds run locally via
docker compose. - No secrets management. Use
.env.local(gitignored) and the usual${VAR}substitution. Cupli doesn't ship a vault integration. - Schema v1. Forward-incompatible breaking changes are gated on
schema_version.cupli upgrade-configis the migration path placeholder.
Troubleshooting + error codes
cupli explain <code> prints the full description. The cheat-sheet:
| Code | Meaning |
|---|---|
E001 |
Space file not found. |
E002 |
Validation failed (pydantic). Per-field messages include file:line:col. |
E003 |
Empty / comment-only space file. |
E004 |
YAML syntax error. |
E014 |
Variable interpolation cycle. |
E015 |
User variable shadows a reserved auto-var. |
E016 |
Unknown ${VAR} reference under --strict-vars. |
E017 |
git clone failed. |
E020 |
Unknown name (app / mount / target / space). |
E028 |
Unknown cupli error code (catch-all). |
E029 |
Space file already exists. |
E030 |
Per-component env-var name collision (e.g. shop-api and shop_api both โ SHOP_API_APP_PATH). |
For cupli space doctor and cupli config errors, the output now
includes a per-field summary with source locations.
Contributing
git clone https://github.com/<user>/cupli && cd cupli
uv sync
make # lint + tests
make test
make schema # regenerate space.schema.json
Conventions: see AGENTS.md (for AI agents) and the
inline doctests / docstrings.
Changelog
v0.1.1
Hotfix release on top of v0.1.0 to make CI green on a fresh runner and on the full ubuntu / macos / windows ร py3.10โ3.13 matrix.
Fixes
create_filenow creates parent directories. The per-user spaces registry at${XDG_CONFIG_HOME:-~/.config}/cupli/spaces.jsonwas being touched without ensuringcupli/existed first, which madecupli ... graph,examples-validate, and ~70 unit/CLI tests fail on any machine that had never had~/.config/cupli/.- Test suite no longer relies on
FORCE_COLOR=0. A top-levelconftest.pynow popsFORCE_COLORand setsNO_COLOR=1before any cupli module is imported, so rich-formatted output does not inject ANSI escape sequences that break substring assertions on captured stdout. - Windows compatibility in path assertions. Loader tests that match
computed
*_PATHvars with a literal forward-slash suffix now normalise throughPath(...).as_posix()so they pass underWindowsPath. install_hookschmod check skipped on Windows. Windows file systems do not model POSIX executable bits, so thest_mode & 0o111assertion is@pytest.mark.skipifonwin32._pid_aliveusesOpenProcesson Windows. CPython routesos.kill(pid, 0)toGenerateConsoleCtrlEvent(CTRL_C_EVENT, pid)on Windows, which actively sendsCtrl+Cto a process group instead of probing liveness โ that interrupted the test session as soon as the lock module checked an unknown PID. The Windows path now opens the process withPROCESS_QUERY_LIMITED_INFORMATIONand inspects the exit code viaGetExitCodeProcess.- Registry prefix detection accepts both separators. The longest-
prefix matcher hard-coded
/as the directory delimiter, sodetect_current_spacecould not locate a registered space from a Windowscwdunder it. Both/andos.separe now treated as valid separators.
v0.1.0
Initial release.
Highlights
-
Schema (
schema_version: 1) โ top-levelapps,bases,mounts,hooks,commands,networks; per-appmode/service/services/forward_ssh; per-mounthosted_in/exec_path/mac_volume. -
Two ways to declare compound apps.
services:accepts either a map of service-name โ override or a bare list of names:services: # map form: per-service overrides api: {} worker: vars: {LOG_LEVEL: debug} services: # list form: just the names - api - worker - beat
-
Top-level
networks:carries any docker-composenetworks.<name>.*spec verbatim and is merged intodocker-compose.pre.ymlalongside the auto-attacheddefaultworkspace network. -
Pydantic v2 models with cross-references (
bases,deps,hosted_in,commands.container) validated by a singlemodel_validator. Barevars:(YAML null) is coerced to{}. -
Line-aware parser (ruamel.yaml round-trip) feeds a
LineMarkslookup table that backs friendly validation errors. -
Numbered error catalog (
E001โE031) pluscupli explain <code>.E031fires when a planned service is not declared in any compose source โ surfaces the missing app + service beforedocker composewould error out. -
CLI surface โ
init,workspace add/list/select/unselect/remove,space sync/doctor,up/stop/restart/down/ps/logs/build/pull/compose/config/ watch,exec/run/shell/wrap/sc,mounts list/attach/detach,hooks install/remove,git status/pull/fetch/checkout,ide setup,dashboard,env,explain,upgrade-config,completion,--list/--version.cupli upaccepts--tag <t>(repeatable),--mode default|hook|fulland bare service names โ including individual services of compound apps (cupli up api-1targets just that service). -
Compose overrides are emitted into the per-space state dir using docker-compose naming convention:
docker-compose.pre.yml(defaults โ network, container_name; merged BEFORE user composes),docker-compose.post.yml(forced โ env injection, ports, mount volumes, cross-filedepends_on; merged AFTER) anddocker-compose.inline.ymlfor services declared inline underapps.<x>.service/apps.<x>.services. Deps onmode: oneshotapps emitcondition: service_completed_successfullyso dependants block on the one-off command. -
Hooks-in-docker (elc-style) โ
cupli hooks install <dir>installs idempotent bash shims tagged with# cupli-hook v1. Per-script first-line directives override defaults:# cupli: container=node workdir=/app shell=sh.shell=lets alpine-only images use busyboxsh; default staysbash. Pre-commit-framework conflicts surface asE024unless--forceis passed. -
Workspace commands declared under
commands:are surfaced throughcupli sc <name>(with optionaltop_level: trueto expose as barecupli <name>).run:is a shell command line โ cupli wraps it insh -cso&&,|,${VAR}work inside the container. -
C3 linearisation for
apps[*].basesโ deterministic even under diamond inheritance when nested bases land later. -
Parallel
space syncviaconcurrent.futures.ThreadPoolExecutor.${VAR}references inrepo:/branch:/post_clone:are substituted before invoking git, so self-hosted file:// URLs and parameterised branches work. -
Scaffold (
cupli init) writes a minimal layout:space.cupli.yaml,.env,.locals/.src/apps/,src/bases/,src/mounts/are created lazily bycupli space syncwhen a declared component first needs them. -
State directory layout under
.locals/<space>/state/:docker-compose.pre.yml,docker-compose.post.yml,docker-compose.inline.yml,override.env,vars.json,cache.json,hooks-manifest.json,active-mounts.json,lock. -
IDE integration โ
cupli ide setupwalks up from the workspace looking for.vscode//.idea/(stopping at the git-repo boundary) and writes JSON-schema mappings only for the editor(s) found. On a brand-new workspace where nothing is detected, writes both as a safe default.cupli initcalls the same flow. -
Layered architecture enforced by an
.importlinterconfig:cli โ services โ core โ domain โ utils.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cupli-0.1.1.tar.gz.
File metadata
- Download URL: cupli-0.1.1.tar.gz
- Upload date:
- Size: 128.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
31edbe0045a338a9016533dccdcd9ea75f34bee5027dde6ca5e128daf4b11fed
|
|
| MD5 |
8b7e029cb82a3d8e35051df264c81fe4
|
|
| BLAKE2b-256 |
972bcca34c4ca1705386cd8d47a24d0241f180a58623741d830d835a98896b5b
|
Provenance
The following attestation bundles were made for cupli-0.1.1.tar.gz:
Publisher:
ci.yml on extralait-web/cupli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cupli-0.1.1.tar.gz -
Subject digest:
31edbe0045a338a9016533dccdcd9ea75f34bee5027dde6ca5e128daf4b11fed - Sigstore transparency entry: 1631115737
- Sigstore integration time:
-
Permalink:
extralait-web/cupli@653869e456a2568cfc3e2ca8d33a0a4e404d0f06 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/extralait-web
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@653869e456a2568cfc3e2ca8d33a0a4e404d0f06 -
Trigger Event:
push
-
Statement type:
File details
Details for the file cupli-0.1.1-py3-none-any.whl.
File metadata
- Download URL: cupli-0.1.1-py3-none-any.whl
- Upload date:
- Size: 108.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f8f750064851ad6189557bd5271e07618677f263a3d03dbd3f1abc4a645d9884
|
|
| MD5 |
f2b0db6b6d81b35cee94d7c5885c9a23
|
|
| BLAKE2b-256 |
5a425d11323ba886b18bb70dfa2cc5db8a19a20a77221b619679a38af55f464d
|
Provenance
The following attestation bundles were made for cupli-0.1.1-py3-none-any.whl:
Publisher:
ci.yml on extralait-web/cupli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cupli-0.1.1-py3-none-any.whl -
Subject digest:
f8f750064851ad6189557bd5271e07618677f263a3d03dbd3f1abc4a645d9884 - Sigstore transparency entry: 1631115744
- Sigstore integration time:
-
Permalink:
extralait-web/cupli@653869e456a2568cfc3e2ca8d33a0a4e404d0f06 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/extralait-web
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@653869e456a2568cfc3e2ca8d33a0a4e404d0f06 -
Trigger Event:
push
-
Statement type: