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.

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 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.
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_PATH for every app, <NAME>_BASE_PATH for every base, <NAME>_MOUNT_PATH for every mount. Name is upper-cased with - mapped to _. Visible in YAML AND in override.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 $VAR is 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-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.

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 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: 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.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.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.3.0.tar.gz (153.6 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.3.0-py3-none-any.whl (119.7 kB view details)

Uploaded Python 3

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

Hashes for cupli-0.3.0.tar.gz
Algorithm Hash digest
SHA256 9d06dd899790c4e416eea7ca24c058de0a3f15dcc252ca13dadb1e9cf4827b93
MD5 a11f300f9b98c4244ae8853018d04c06
BLAKE2b-256 964b4406e7de77bbcd442af812ea75bfd3a146df4545cdc82bff95cc97ef86db

See more details on using hashes here.

Provenance

The following attestation bundles were made for cupli-0.3.0.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.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

Hashes for cupli-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 63b52f0d7366f7225a60d4160d53c215c5e003832fbeb512961ca8c6127411f7
MD5 a51a7e6773408d1c413a14f4f1095bb0
BLAKE2b-256 8dc950fc20a4710742cbe8f94e40649b6ae98bdda0776b1b423258c359ddda1b

See more details on using hashes here.

Provenance

The following attestation bundles were made for cupli-0.3.0-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