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).
๐ค Using an AI agent to edit
space.cupli.yaml? Point it atAGENTS.mdโ a self-contained guide to the schema, service binding,commands:, top-level blocks, and error codes.
Table of contents
- Install
- Quick-start
- Concepts
- The
space.cupli.yamlreference - CLI reference
- Recipes
- IDE setup
- 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. |
volumes |
map[str, dict] | {} |
Top-level docker-compose volumes: block. Named volumes (compose-spec verbatim) so inline services can reference them without a separate compose file. A null body (minio_data:) is a default-driver volume. |
secrets |
map[str, dict] | {} |
Top-level docker-compose secrets: block. Secret definitions (compose-spec verbatim) referenced by service-level secrets:. |
configs |
map[str, dict] | {} |
Top-level docker-compose configs: block. Config definitions (compose-spec verbatim) referenced by service-level configs:. |
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 | list[string] | required | App name(s) whose primary service runs the command. A list runs it in each. |
run |
string | list[string] | required | Shell command line. A block scalar or list of lines is joined with newlines and run via sh -c. {{name}} placeholders are filled from args. |
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>). |
group |
string | โ | Label; groups the command under a panel in cupli --help and the cupli sc listing. |
execute |
enum | sequential |
For a multi-container command: sequential (fail-fast), continue (run all, non-zero if any failed), or parallel. |
args |
list[arg] | [] |
Declared, typed parameters surfaced in cupli <cmd> --help and substituted into run via {{name}}. A bare list of names is shorthand for required positional string args. |
commands.<name>.args[]
| Key | Type | Default | What it does |
|---|---|---|---|
name |
string | required | Identifier; the {{name}} placeholder and CLI arg/option name. |
help |
string | โ | Description shown in cupli <cmd> --help. |
type |
enum | str |
str, int, or bool. A bool is always an option (flag). |
option |
bool | false |
When true, a --name option; otherwise a positional argument. |
short |
string | โ | Single-letter alias for an option (l โ -l). |
required |
bool | false |
Whether the value must be supplied. Mutually exclusive with default. |
default |
string | โ | Value substituted when the parameter is omitted. |
commands:
db-migrate:
group: Database # `cupli --help` shows it under a "Database" panel
container: api
run: python manage.py migrate {{app}} {{fake}}
args:
- name: app # required positional: `cupli db-migrate users`
required: true
help: Django app label.
- name: fake # bool -> a `--fake` flag
type: bool
top_level: true
pip-freeze:
container: [api, worker] # run in several services
execute: parallel # sequential (default) | continue | parallel
run: pip freeze
For a multi-line script, use a block scalar (newline-separated commands; add
&& or set -e for fail-fast within the script):
setup:
container: api
run: |
python manage.py migrate
python manage.py loaddata initial
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]
Named volumes, secrets, configs
Top-level volumes: / secrets: / configs: are merged verbatim into
docker-compose.pre.yml, so an inline service can reference them without a
separate compose file. No synthetic default is injected (unlike networks),
and an empty block is omitted from the output.
volumes:
minio_data: # null body == default-driver named volume
secrets:
ci_token:
environment: CI_JOB_TOKEN
apps:
minio:
service:
image: minio/minio
command: server /data
volumes: [minio_data:/data] # references the named volume above
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.
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.
Changelog
v0.3.0
Feature release extending workspace command shortcuts (commands:).
Features
- Command grouping. A
group:label groups a command under a panel incupli --helpand in thecupli sclisting. - Multi-line
run.runaccepts a single line, a YAML block scalar, or a list of lines (joined with newlines) and runs viash -c. The legacy$@passthrough is appended only for a single-line snippet with no declared args. - Typed arguments. A
commands.<name>.argslist declares typed parameters (str/int/bool, positional or--option, withhelp,shortalias,required/default). For top-level promoted commands they become real typer parameters shown incupli <name> --help; forcupli scthey are parsed with click. Values are substituted intorunvia{{name}}placeholders and shell-quoted. A bare list of names is shorthand for required positional string arguments. - Multi-container commands.
container:accepts one app or a list, and a newexecute:field selectssequential(default, fail-fast),continue(run all, non-zero if any failed), orparallel(concurrent, output captured per container).
Docs
AGENTS.mdactualized with the new command capabilities and the top-levelvolumes:/secrets:/configs:blocks; READMEs now point agents at it.
v0.2.0
Feature release adding three top-level docker-compose blocks to the space schema.
Features
- Top-level
volumes:,secrets:,configs:blocks. A space can now declare named volumes, secret definitions and config definitions at the top level alongsidenetworks:. They are merged verbatim intodocker-compose.pre.yml, so inline services (apps.<x>.service/services) can reference them without a separate compose file โ e.g. aminio_data:/datanamed volume or aCI_JOB_TOKENbuild secret. No syntheticdefaultis injected and an empty block is omitted. A null body (minio_data:) is treated as a default-driver entry.
v0.1.2
Hotfix release that fixes broken GitHub URLs scaffolded into user workspaces
and surfaced by the IDE-setup command. The earlier v0.1.1 tag was the
intended hotfix but never reached PyPI, so the same payload ships under
v0.1.2 alongside this URL fix.
Fixes
- Scaffolded
space.cupli.yamland IDE-setup link tomaster. The default branch ofextralait-web/cupliismaster, but theyaml-language-server$schema=URL, the README reference comment in the scaffolded space, andSCHEMA_URL_DEFAULTall pointed athttps://raw.githubusercontent.com/extralait-web/cupli/main/โฆ, which 404s. All three references now use/master/.
Tests
- Coverage raised above the smokeshow gate (80%) with new unit and CLI
suites for
utils/json,utils/subprocess,utils/git,domain/runtime,core/cache,cli/diagnostics,cli/lifecycle,cli/gitandcli/_completion.
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.3.0.tar.gz.
File metadata
- Download URL: cupli-0.3.0.tar.gz
- Upload date:
- Size: 153.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9d06dd899790c4e416eea7ca24c058de0a3f15dcc252ca13dadb1e9cf4827b93
|
|
| MD5 |
a11f300f9b98c4244ae8853018d04c06
|
|
| BLAKE2b-256 |
964b4406e7de77bbcd442af812ea75bfd3a146df4545cdc82bff95cc97ef86db
|
Provenance
The following attestation bundles were made for cupli-0.3.0.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.3.0.tar.gz -
Subject digest:
9d06dd899790c4e416eea7ca24c058de0a3f15dcc252ca13dadb1e9cf4827b93 - Sigstore transparency entry: 1644842173
- Sigstore integration time:
-
Permalink:
extralait-web/cupli@470f980542941b2d632c0803761f3921202f45fb -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/extralait-web
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@470f980542941b2d632c0803761f3921202f45fb -
Trigger Event:
push
-
Statement type:
File details
Details for the file cupli-0.3.0-py3-none-any.whl.
File metadata
- Download URL: cupli-0.3.0-py3-none-any.whl
- Upload date:
- Size: 119.7 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 |
63b52f0d7366f7225a60d4160d53c215c5e003832fbeb512961ca8c6127411f7
|
|
| MD5 |
a51a7e6773408d1c413a14f4f1095bb0
|
|
| BLAKE2b-256 |
8dc950fc20a4710742cbe8f94e40649b6ae98bdda0776b1b423258c359ddda1b
|
Provenance
The following attestation bundles were made for cupli-0.3.0-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.3.0-py3-none-any.whl -
Subject digest:
63b52f0d7366f7225a60d4160d53c215c5e003832fbeb512961ca8c6127411f7 - Sigstore transparency entry: 1644842269
- Sigstore integration time:
-
Permalink:
extralait-web/cupli@470f980542941b2d632c0803761f3921202f45fb -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/extralait-web
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@470f980542941b2d632c0803761f3921202f45fb -
Trigger Event:
push
-
Statement type: