Skip to main content

Multi-repository docker-compose workspace orchestrator: one YAML, one `cupli up`, every container running.

Project description

cupli

Multi-repository docker-compose workspace orchestrator. One YAML, one `cupli up`, every container running.

CI pypi downloads versions license

๐Ÿ‡ท๐Ÿ‡บ 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.yaml declares 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 / checkout operate 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: main on a component is honoured by cupli init (git clone -b) and surfaced in cupli git status when 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 at AGENTS.md โ€” a self-contained guide to the schema, service binding, commands:, top-level blocks, and error codes.


Table of contents

  1. Install
  2. Quick-start
  3. Concepts
  4. The space.cupli.yaml reference
  5. CLI reference
  6. Recipes
  7. IDE setup
  8. Limitations
  9. 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:

  1. Implicit โ€” service name equals app name.
  2. service: "name" โ€” bind to an existing compose service by name.
  3. service: {image: ..., command: ..., ...} โ€” inline single-service spec, no separate compose file.
  4. 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.

Host resolution for IDEs (host_bridge & exports)

IDEs index the host filesystem, while cupli runs everything inside containers. Two opt-in features bridge that gap (both are off by default and exist purely for editor resolution โ€” never for running host tooling):

  • host_bridge on a mount keeps an inverse host symlink (<host-equivalent of exec_path> โ†’ mount.path) so a library mounted under the app workdir is visible on the host at the same relative path the container uses. Lifecycle-managed (up / mounts attach/detach) or explicit via cupli mounts bridge / unbridge.
  • exports copies a container-built directory (typically a named volume like node_modules) onto the host. strategy: sync (default) mirrors the volume on refresh_on events; strategy: bind-seeded turns the path into a live host bind. Manage with cupli exports sync / clean.

They compose: pnpm's relative symlinks inside an exported node_modules (@scope/<lib> โ†’ ../../packages/<lib>) resolve on the host only when packages/<lib> is bridged. There's an asymmetry between stacks โ€” a JS remote (Docker) interpreter does not resolve dependencies, so node_modules must exist on the host; a Python remote interpreter resolves fine, so prefer it over exporting .venv (a .venv export is skipped unless rewrite_paths: true). See the exports.<name> reference for fields.

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.
exports map[str, export] {} Materialise container-built dirs (node_modules) onto the host for IDE resolution.
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 list[str] | map[str, โ€ฆ] {} Cross-app depends_on. List form [a, b] or map with per-dep settings (mode tags for --mode filtering, compose condition / restart / required). See Dependency conditions.
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: cache-redis        # bind to compose service `cache-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 into override.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.
host_bridge bool | map false Maintain an inverse host symlink so host tooling (IDEs) sees the mount at the container-relative path. true auto-derives the link from the hosting app's workdir bind; a map ({link, relative}) overrides it. See host_bridge & exports.
envs list[string] [] Env files.
vars map {} Variables.

exports.<name>

Materialise a directory built inside a container (typically a named volume such as node_modules) onto the host so IDEs that only resolve from the local filesystem can index it. For IDE indexing, not for running host tooling โ€” exported native binaries may target the image's libc, not the host's.

Key Type Default What it does
from string required App (single) whose service owns the source directory.
exec_path string required Absolute POSIX source path inside the container.
path string required Host destination path (${VAR} resolved in scope).
strategy enum sync sync (keep the named volume, copy to host on refresh_on) or bind-seeded (turn exec_path into a host bind seeded from the image โ€” always live).
refresh_on list[enum] | string [build] Lifecycle events that re-materialise the export: up, build, restart.
gitignore bool true Add path to the root .gitignore (under a # cupli exports section).
mac_volume enum โ€” macOS volume consistency hint.
rewrite_paths bool false Experimental: sync a .venv-like export anyway and rewrite absolute container paths (/app/...) in .pth / .egg-link files to host equivalents. Without it, a .venv-like export is skipped (E034).

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.
strict bool false When false, CLI tokens not matching a declared arg (flags + positionals) are forwarded to the end of the command; when true, unknown tokens are rejected.

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_PATH for every app, <NAME>_BASE_PATH for every base, <NAME>_MOUNT_PATH for every mount, <NAME>_EXPORT_PATH for every export. Name is upper-cased with - mapped to _. Visible in YAML AND in override.env. A mount with an explicit host_bridge.link also exposes <NAME>_BRIDGE_PATH.
  • App / base โ€” APP_NAME, APP_PATH, APP_LOCAL_PATH (apps only).
  • Mount โ€” MOUNT_NAME, MOUNT_PATH, MOUNT_HOST, MOUNT_EXEC_PATH.
  • Export โ€” EXPORT_NAME, EXPORT_PATH, EXPORT_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}, ${VAR:-literal-default}, and bare $VAR are all recognised. $$ escapes a literal $ (docker-compose convention).
  • Nested ${...} inside a default is not supported. The default is literal.
  • Cycles raise E014.
  • Unknown vars resolve to "" with a yellow warning. Pass --strict-vars to make them hard errors (E016).
  • Shadowing a reserved auto-var name raises E015 unless --allow-shadow is 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.

Dependency conditions

deps: accepts compose-style start conditions per dependency. Default is service_started (or service_completed_successfully for a mode: oneshot dep). String value = condition shorthand; null = defaults; list = mode tags (back-compat with --mode filtering); mapping = full spec.

apps:
  api:
    deps:
      postgres: service_healthy             # wait for healthcheck
      redis: ~                              # default: service_started
      init-data:
        condition: service_completed_successfully
        restart: true                       # restart api when init-data restarts
        required: false                     # api still starts if init-data can't

These map straight onto compose depends_on.<svc>.{condition,restart,required}.

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 (incl. host_bridge).
cupli mounts attach <name> bind-mount into hosted_in apps.
cupli mounts detach <name> remove the bind.
cupli mounts bridge [names] create/repair host_bridge symlinks.
cupli mounts unbridge [names] remove cupli-created host_bridge symlinks.

Exports

Command What
cupli exports list every declared export and its status (missing/stale/seeded/synced).
cupli exports sync [names] materialise / refresh host copies.
cupli exports clean [names] remove sync host copies (bind-seeded data kept).

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 init clones them under src/apps/<name>.
  • cupli git status aggregates state across all of them.

Full file: docs/examples/multi-repo-shop/.

Renaming a compose service

apps:
  redis:
    service: cache-redis             # compose-fragment calls it cache-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.json from 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.yaml maps 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-config is 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.8.1

Bug-fix release. Makes cupli exec / run / shell / sc usable from non-interactive contexts (git hooks, CI, pipes).

Fixes

  • exec/run/shell/sc no longer fail outside a terminal. docker compose exec/run allocate a pseudo-TTY by default, so these commands aborted with cannot attach stdin to a TTY-enabled container because stdin is not a terminal whenever stdin was not a terminal โ€” e.g. a workspace command invoked through cupli sc during a git pre-commit hook, in CI, or behind a pipe. cupli now adds -T automatically when stdin is not a terminal, while an interactive terminal keeps its TTY (colors, interactivity). The in-container hook runner already passed -T; the shortcut/exec path now matches it.

v0.8.0

Feature release. Adds mounts.<x>.exclude so a container's package-manager output stays in the container instead of leaking onto the host bind.

Features

  • mounts.<x>.exclude โ€” sub-path masking. An rw library bind is two-way, so a package manager running in the container (pnpm/npm/uv) writes artifacts laid out for the container โ€” e.g. node_modules symlinks into /app/node_modules/.pnpm/... โ€” that leak back onto the host bind, where they are broken and pollute the working copy. exclude masks listed sub-paths of the bind with their own volume so those artifacts live only inside the container. Each entry injects a volume at <exec_path>/<sub> (it nests deeper than the bind, so docker resolves it by path length); on the host the sub-path stays an empty mount point, pre-created as the user rather than root. Bare strings (exclude: [node_modules]) are shorthand for an anonymous overlay; the expanded form takes strategy (anonymous default โ€” re-seeds from the image on recreate โ€” or named โ€” a persistent volume declared in the generated pre-override) and, for named, refresh_on: [up|build|restart] to drop the volume after that event so the next up re-seeds it from the fresh image (best-effort โ€” skipped while the volume is in use). Sub-paths must be concrete relative paths (globs and .. are rejected), and derived named-volume keys are checked for collisions. cupli mounts list gains an excludes column (shown only when declared). Pairs with host_bridge / exports: the library is bridged for the IDE, its own node_modules is masked, and the app-level node_modules is exported โ€” so resolution works without a phantom node_modules under the library.

v0.7.0

Hygiene release. Fixture and example services renamed to project-neutral names; no functional changes vs v0.6.3.

Chores

  • Renamed fixture/example services across README (EN + RU), full-reference example, tests, and one docstring to project-neutral identifiers (cache-redis, api-backend).

v0.6.3

Patch release. Two fixes from a fresh-clone deploy of the exports / host_bridge features.

Fixes

  • bind-seeded exports reinstalled dependencies at runtime instead of copying from the image. On an empty host the bind started empty, so the container ran a slow, network-bound pnpm / uv install (hanging on ETIMEDOUT in a restricted network). Two causes: the source image/volume were resolved against every service in the merged compose (so an unrelated app's image could win โ€” e.g. a Python api-backend image used to seed a JS node_modules export), and on a fresh cupli up --build the image was not built yet when the seed ran. Source resolution is now scoped to the export's from app's compose service(s), and cupli up builds first (as a separate step) so the seed copies real content from the image โ€” an offline docker cp-style copy of exec_path, never a package install. cupli up --build therefore no longer forwards --build to the up step.

  • A fresh cupli up raced docker's named-volume initialisation when several services of an app shared one volume (failed to mkdir โ€ฆ/_data/โ€ฆ: file exists), so the first deploy failed and needed a manual retry. Before up, cupli now pre-initialises each named volume shared by two or more services once, serially (building any missing image first). This is a no-op once the volume exists and is gated so spaces without compound apps / exports pay nothing.

v0.6.2

Patch release. Seven fixes from the second real-world run of the exports / host_bridge features.

Fixes

  • host_bridge: true auto-derivation returned "no hosting bind found". derive_host_link matched the mount's own injected bind (whose target equals exec_path), yielding an empty relative part. It now requires a strict ancestor, so the app's /app workdir bind wins โ€” host_bridge: true works without an explicit link: in the common case. Derivation is scoped to the hosting app's compose service(s); a bind from an unrelated service (e.g. another app that also binds its own directory to /app) no longer leaks in.

  • cupli mounts unbridge reported "not cupli-owned" for symlinks cupli created. A conflicting mount raised mid-loop, so state/bridges.json was never written and symlinks created earlier in the batch were orphaned. Conflicts are now reported as per-mount results rather than a raised error, ownership is persisted even when a later mount conflicts, and the conflict no longer aborts the batch or cupli up. cupli mounts bridge still exits non-zero when any mount conflicts.

  • host_bridge broke on mounts bound directly under the app's /app bind (e.g. docs, mkdocs.yml). The host link path is the docker sub-mount point, which the daemon creates as an empty directory or a 0-byte root-owned file. cupli already reclaimed an empty directory; it now also reclaims a 0-byte file stub and replaces it with the bridge symlink. Only a non-empty directory, a non-empty file, or a foreign symlink is a conflict (E032).

  • .venv exports synced despite E034, producing broken editable installs. A .venv-like export is now skipped unless rewrite_paths: true is set โ€” editable .pth files carry absolute container paths (/app/...) that do not exist on the host. With rewrite_paths: true (experimental) the sync rewrites the container workdir prefix (/app/... โ†’ the app's host path) in .pth / .egg-link files.

  • cupli exports clean reported missing for a copy it had just removed. It now reports removed, and prunes the export's entry from the root .gitignore (dropping the # cupli exports section once it is empty).

  • cupli graph now lists an exports section (name / from / exec_path โ†’ host path / strategy), alongside bases / apps / mounts / commands.

v0.6.1

Patch release. Fixes from the first real-world run of the v0.6.0 exports / host_bridge features.

Fixes

  • exports sync produced an empty root:root directory and failed chown (E033). The sync helper copied as --user <uid>:<gid>, which can neither read root-owned named-volume content nor chown the result; the follow-up host-side chown then hit EPERM, leaving an empty directory while exports list reported missing. The copy and the chown now run as root inside the helper container (cp -a <src>/. /dst/ && chown -R <uid>:<gid> /dst), so the source is readable regardless of its in-image ownership and the result is owned by the host user โ€” no host-side chown, no E033. cp -a preserves the relative symlinks inside node_modules; a find -mindepth 1 -delete pass makes repeated syncs idempotent. The reported status now reconciles with the on-disk fact (missing when nothing materialised, never a false synced). bind-seeded seeding uses the same root copy+chown path, so it no longer leaves root-owned files either.

  • cupli mounts bridge raised a false E032 on an empty directory. An empty packages/<lib> mount point (routinely left by docker or a prior run) was treated as a foreign object. An empty directory on the link path is now reclaimed (rmdir + symlink); only a non-empty directory, a file, or a foreign symlink is a conflict (E032).

  • Top-level ${<NAME>_APP_PATH} ignored an app.path override. In top-level exports / mounts, ${<NAME>_APP_PATH} resolved to the default ${APPS_PATH}/<name> instead of the app's overridden path:, so exports.path could point at the wrong directory. The loader now overwrites the pre-pass default path-vars with each component's actual resolved path before resolving top-level mounts / exports.

  • host_bridge: true auto-derivation missed the workdir bind from an external composes: file when the compose service name differed from the app name (no hosting bind found). Auto-derivation now falls back to scanning every merged-compose service's binds for the one whose target is an ancestor of exec_path.

v0.6.0

Feature release: host-side materialisation for IDEs โ€” host_bridge mount symlinks and the exports: block. Both are opt-in and backward-compatible; existing space files are unaffected. Configs using the new keys should pin cupli_min: 0.6.0.

Features

  • host_bridge on a mount. A mount whose exec_path lives under the hosting app's workdir bind (e.g. ${APP_PATH}:/app) can keep an inverse host symlink โ€” <host-equivalent of exec_path> โ†’ mount.path โ€” so host tooling (IDEs, workspace-package resolvers) sees the library at the same relative path the container uses. This makes the relative symlinks pnpm writes inside node_modules (@scope/<lib> โ†’ ../../packages/<lib>) resolve on the host.

    mounts:
      web-ui-kit:
        hosted_in: [web]
        path: ${LIBS_PATH}/ui-kit
        exec_path: /app/packages/ui-kit
        host_bridge: true                 # auto-derive the host link from the workdir bind
        # host_bridge:                     #   or override:
        #   link: ${WEB_APP_PATH}/packages/ui-kit
        #   relative: true                 #   relative symlink (default; portable)
    

    The link is created/repaired on cupli up and cupli mounts attach, and removed on cupli mounts detach; manage it explicitly with cupli mounts bridge [namesโ€ฆ] / cupli mounts unbridge [namesโ€ฆ]. cupli mounts list gains a bridge column (none/pending/ok/broken/conflict). cupli only touches symlinks it created (tracked in state/bridges.json); a foreign non-symlink on the link path is left alone and surfaces as E032. Auto-derivation reads the workdir bind from docker compose config; an explicit link: works offline and is exposed as ${<MOUNT>_BRIDGE_PATH}. Symlink creation degrades gracefully on platforms that cannot create symlinks.

  • exports: โ€” materialise container-built directories onto the host. A new top-level block copies a directory built inside a container (typically a named volume such as node_modules) out to the host so IDEs that only resolve from the local filesystem can index it. For IDE indexing, not for running host tooling โ€” the exported tree may carry native binaries built for the image's libc, not the host's.

    exports:
      web-node-modules:
        from: web                                   # the app (single) that owns it
        exec_path: /app/node_modules                # container source
        path: ${WEB_APP_PATH}/node_modules          # host destination
        strategy: sync                              # sync (default) | bind-seeded
        refresh_on: [build]                         # up | build | restart (default: [build])
        gitignore: true                             # add path to root .gitignore (default true)
    
    • sync (default) keeps the named volume for container I/O and copies it to the host one-way on each refresh_on event (symlinks preserved, so pnpm's .pnpm / @scope/<lib> structure survives).
    • bind-seeded turns exec_path into a host bind (injected into the generated post-override) seeded from the image, so the container writes straight to the host (always live).

    Seeded/refreshed automatically after up / build / restart per refresh_on; manage manually with cupli exports list / sync / clean. Auto-vars ${EXPORT_PATH} / ${EXPORT_EXEC_PATH} / ${<NAME>_EXPORT_PATH} are available in scope. A foreign non-empty host directory cupli did not create raises E032. Exporting a .venv-like path with editable installs warns E034 (a Python remote interpreter resolves fine โ€” prefer it; opt in with experimental rewrite_paths: true).

  • New error codes E032 (host path occupied), E033 (ownership mismatch), E034 (editable paths point into the container).

Chores

  • AGENTS.md documents the per-service-verb closure semantics introduced in v0.5.3, plus the new host_bridge / exports schema, CLI verbs, auto-vars, and error codes. README (EN + RU) and the full-reference example are updated; space.schema.json is regenerated.

v0.5.3

Patch release.

Fixes

  • Per-service lifecycle verbs no longer drag every dependency along. cupli restart api restarted api and every container the app depends on (postgres, redis, minio, โ€ฆ) โ€” bouncing the whole stack. Same shape for cupli stop, cupli down, cupli ps, cupli build, and cupli pull. Root cause: these verbs forwarded the closure-expanded plan.services to docker compose, but filter_service.closure always walks apps[*].deps transitively. Closure expansion is intended for cupli up (deps must start first), not for operations on already-running containers. Per-service verbs now resolve user-named seeds via the new compose_service.target_services helper which skips the deps walk; docker compose <verb> <svc> already handles per-verb semantics correctly. Workspace-wide forms (cupli restart without arguments) and tag-filtered forms (cupli restart --tag api) keep their existing behaviour. cupli up retains closure expansion. The v0.5.2 audit caught the silent-drop side of cupli down but missed this broader closure-expansion issue across the verb family โ€” both bug classes are addressed now.

v0.5.2

Patch release.

Fixes

  • cupli down <service> no longer silently tears the whole stack down. down_command discarded the service-name half of the passthrough split, so cupli down kanchi ran a full docker compose down --remove-orphans and removed every container in the workspace. down now declares an optional services positional: with services named, only those containers go down (docker compose down [SERVICESโ€ฆ] โ€” compose v2 supports the per-service form); without arguments the workspace-wide teardown behaviour is unchanged. The other lifecycle verbs (up, stop, restart, ps, logs, build, pull) were audited at the same time and already forward service names correctly.

v0.5.1

Patch release.

Fixes

  • cupli --list works under typer โ‰ฅ0.25 / click โ‰ฅ8.4. Newer typer restructured the class hierarchy so TyperGroup is no longer a subclass of click.Group. The isinstance(cmd, click.Group) guard in _list_commands and the builtin-collision check silently returned an empty group, hiding every command from cupli --list. The guard now duck-types obj.commands so cupli is compatible with both old and new typer/click lines. cupli --list and top-level shortcut promotion work under a fresh pipx install again.

v0.5.0

Feature release: compose-style start conditions for apps[*].deps.

Features

  • Compose start conditions on deps. A dependency in apps[*].deps can now declare its docker-compose start condition (service_started / service_healthy / service_completed_successfully) plus the restart and required flags. Previously cupli always rendered service_started (with a special case to service_completed_successfully for mode: oneshot deps), which forced workspaces to ship wait-for-pg scripts to actually wait for a healthy database. Now:

    apps:
      api-backend:
        deps:
          postgres: service_healthy       # condition shorthand
          redis: ~                         # default (service_started)
          migrate:                         # full DepSpec form
            condition: service_completed_successfully
            modes: [default]
            restart: true
            required: true
    

    Accepted deps value forms (back-compat preserved):

    • list of names โ†’ defaults (unchanged);
    • ~ (null) โ†’ defaults;
    • string โ†’ condition shorthand;
    • list of mode tags โ†’ modes for --mode filtering (unchanged);
    • mapping โ†’ full DepSpec (modes / condition / restart / required).

    Mode-tag and condition name spaces are disjoint, so a bare string is unambiguous. Cupli forwards restart / required to compose only when they differ from compose defaults.

v0.4.2

Patch release.

Fixes

  • File-placeholder for sub-binds is read-only on the host. Cupli pre-creates a 0-byte file on the host for every sub-bind whose source is a single file (e.g. mkdocs.yml mounted at /app/mkdocs.yml). The placeholder is just a mount point โ€” docker overlays the bind source on top inside the container, and the host file stays empty by design. To stop IDEs and humans from editing that empty file by mistake, the placeholder is now created with chmod 0o444. Docker mount ignores host perms, so the running container is unaffected. Directory placeholders keep their default perms (some are parents to other sub-bind placeholders and need write perm during prep).

v0.4.1

Patch release.

Fixes

  • Pre-create host placeholders for sub-mounts under bind targets. Docker daemon (running as root) creates a missing mount point on demand, so under a bind host:/app plus sub-mounts targeting /app/<sub> (named volumes, cupli mounts, additional binds) the daemon ended up creating those host placeholders as root โ€” leaving root-owned junk on the host. Cupli now resolves the merged compose config before up / build / run / watch and pre-creates each placeholder as the current user; the daemon finds an existing mount point and skips the root creation. Idempotent and silent on every failure mode โ€” prep never blocks compose.

v0.4.0

Feature release: workspace-command ergonomics and variable interpolation.

Features

  • Undeclared command args pass through (+ strict). When a commands.<name> declares args, CLI tokens that don't match a declared arg (flags and positionals) are forwarded verbatim to the end of the command โ€” e.g. cupli sc deploy prod --force-recreate. A new strict: true attribute restores reject-unknown.
  • cupli sc <name> are real, typed subcommands. sc is now a group whose subcommands resolve live from the active space (-f / -s / cwd, cold cache, or freshly edited YAML); declared args parse, appear in cupli sc <name> --help, and tab-complete. Completion is per-space (no cross-space leakage).
  • Builtin compose verbs forward unknown flags. up / down / build / pull / stop / restart / ps / logs pass unknown flags through to docker compose (e.g. cupli up --force-recreate); service names are not mistaken for flags. Use the --opt=value form for value-taking flags.
  • Bare $VAR interpolation. $VAR is recognised alongside ${VAR} and ${VAR:-default}; $$ escapes a literal $ (docker-compose convention).
  • env-file values are interpolated against the cupli scope. A value such as DATABASE_URL=postgres://db:${POSTGRES_PORT}/app inside an env file now resolves ${POSTGRES_PORT} from vars / earlier env layers, so port-dependent wiring and credential reuse can live in env files, not only in vars:.
  • Richer schema completion. service: / services: compose-service fields in the JSON schema were expanded to the compose-spec set (~88 properties: cpus, mem_limit, network_mode, sysctls, gpus, extends, develop, โ€ฆ) for accurate editor completion and hover docs.

Fixes

  • A declared envs: file is read with interpolation disabled, so python-dotenv no longer collapses cupli-scope ${VAR} references to empty strings.
  • A missing declared envs: file warns under --strict-vars (silent by default to preserve optional .env.local).

v0.3.1

Hotfix release.

Fixes

  • click is now a declared dependency. v0.3.0 imports click directly (for the typed command-shortcut parameters), but relied on it being present transitively via typer. In environments where typer does not pull click in (e.g. a pipx install), cupli crashed on startup with ModuleNotFoundError: No module named 'click'. click is now listed in the project dependencies so it is always installed.

Chores

  • Bumped GitHub Actions off the deprecated Node 20 runtime. actions/checkout โ†’ v6, actions/setup-python โ†’ v6, actions/upload-artifact โ†’ v7, actions/download-artifact โ†’ v8, astral-sh/setup-uv โ†’ v7, dawidd6/action-download-artifact โ†’ v21.

v0.3.0

Feature release extending workspace command shortcuts (commands:).

Features

  • Command grouping. A group: label groups a command under a panel in cupli --help and in the cupli sc listing.
  • Multi-line run. run accepts a single line, a YAML block scalar, or a list of lines (joined with newlines) and runs via sh -c. The legacy $@ passthrough is appended only for a single-line snippet with no declared args.
  • Typed arguments. A commands.<name>.args list declares typed parameters (str/int/bool, positional or --option, with help, short alias, required/default). For top-level promoted commands they become real typer parameters shown in cupli <name> --help; for cupli sc they are parsed with click. Values are substituted into run via {{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 new execute: field selects sequential (default, fail-fast), continue (run all, non-zero if any failed), or parallel (concurrent, output captured per container).

Docs

  • AGENTS.md actualized with the new command capabilities and the top-level volumes: / 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 alongside networks:. They are merged verbatim into docker-compose.pre.yml, so inline services (apps.<x>.service / services) can reference them without a separate compose file โ€” e.g. a minio_data:/data named volume or a CI_JOB_TOKEN build secret. No synthetic default is 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.yaml and IDE-setup link to master. The default branch of extralait-web/cupli is master, but the yaml-language-server $schema= URL, the README reference comment in the scaffolded space, and SCHEMA_URL_DEFAULT all pointed at https://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/git and cli/_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_file now creates parent directories. The per-user spaces registry at ${XDG_CONFIG_HOME:-~/.config}/cupli/spaces.json was being touched without ensuring cupli/ existed first, which made cupli ... 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-level conftest.py now pops FORCE_COLOR and sets NO_COLOR=1 before 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 *_PATH vars with a literal forward-slash suffix now normalise through Path(...).as_posix() so they pass under WindowsPath.
  • install_hooks chmod check skipped on Windows. Windows file systems do not model POSIX executable bits, so the st_mode & 0o111 assertion is @pytest.mark.skipif on win32.
  • _pid_alive uses OpenProcess on Windows. CPython routes os.kill(pid, 0) to GenerateConsoleCtrlEvent(CTRL_C_EVENT, pid) on Windows, which actively sends Ctrl+C to 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 with PROCESS_QUERY_LIMITED_INFORMATION and inspects the exit code via GetExitCodeProcess.
  • Registry prefix detection accepts both separators. The longest- prefix matcher hard-coded / as the directory delimiter, so detect_current_space could not locate a registered space from a Windows cwd under it. Both / and os.sep are now treated as valid separators.

v0.1.0

Initial release.

Highlights

  • Schema (schema_version: 1) โ€” top-level apps, bases, mounts, hooks, commands, networks; per-app mode / service / services / forward_ssh; per-mount hosted_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-compose networks.<name>.* spec verbatim and is merged into docker-compose.pre.yml alongside the auto-attached default workspace network.

  • Pydantic v2 models with cross-references (bases, deps, hosted_in, commands.container) validated by a single model_validator. Bare vars: (YAML null) is coerced to {}.

  • Line-aware parser (ruamel.yaml round-trip) feeds a LineMarks lookup table that backs friendly validation errors.

  • Numbered error catalog (E001โ€“E031) plus cupli explain <code>. E031 fires when a planned service is not declared in any compose source โ€” surfaces the missing app + service before docker compose would 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 up accepts --tag <t> (repeatable), --mode default|hook|full and bare service names โ€” including individual services of compound apps (cupli up api-1 targets 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-file depends_on; merged AFTER) and docker-compose.inline.yml for services declared inline under apps.<x>.service / apps.<x>.services. Deps on mode: oneshot apps emit condition: service_completed_successfully so 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 busybox sh; default stays bash. Pre-commit-framework conflicts surface as E024 unless --force is passed.

  • Workspace commands declared under commands: are surfaced through cupli sc <name> (with optional top_level: true to expose as bare cupli <name>). run: is a shell command line โ€” cupli wraps it in sh -c so &&, |, ${VAR} work inside the container.

  • C3 linearisation for apps[*].bases โ€” deterministic even under diamond inheritance when nested bases land later.

  • Parallel space sync via concurrent.futures.ThreadPoolExecutor. ${VAR} references in repo: / 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 by cupli space sync when 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 setup walks 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 init calls the same flow.

  • Layered architecture enforced by an .importlinter config: cli โ†’ services โ†’ core โ†’ domain โ†’ utils.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cupli-0.8.1.tar.gz (211.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cupli-0.8.1-py3-none-any.whl (160.9 kB view details)

Uploaded Python 3

File details

Details for the file cupli-0.8.1.tar.gz.

File metadata

  • Download URL: cupli-0.8.1.tar.gz
  • Upload date:
  • Size: 211.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cupli-0.8.1.tar.gz
Algorithm Hash digest
SHA256 66ac736ec77661f27fcfd74eafa8c4584cbdeb49bbf7bae1dd3c13913f48a7f9
MD5 0d9a6d68b3a0323e9c74c2607fa95584
BLAKE2b-256 202fe1dff0d119857fcabfc6d9be5083cbc1f8f75376db6a4f7e585ce1619eaf

See more details on using hashes here.

Provenance

The following attestation bundles were made for cupli-0.8.1.tar.gz:

Publisher: ci.yml on extralait-web/cupli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cupli-0.8.1-py3-none-any.whl.

File metadata

  • Download URL: cupli-0.8.1-py3-none-any.whl
  • Upload date:
  • Size: 160.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cupli-0.8.1-py3-none-any.whl
Algorithm Hash digest
SHA256 36ede4fc265dfc789c38831f15cb99e9bd5f37e06599c4e7ae736dda50ad9f8d
MD5 e9140841369f7ccd505a042733ab3dad
BLAKE2b-256 b2d3d5d572e2ace57eb4b41584704eb1daa9f5f9e68b5155669c2731e9c40e43

See more details on using hashes here.

Provenance

The following attestation bundles were made for cupli-0.8.1-py3-none-any.whl:

Publisher: ci.yml on extralait-web/cupli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page