Перейти к содержимому

commands/

Декларативные определения команд для DWE-проекта.

workspace/commands/ — дом для каждого переиспользуемого, скриптуемого действия, которое проект предоставляет через CLI: shell-доступ к контейнерам, шаги сборки, операции с базой данных, многошаговые сценарии, хуки деплоя, пользовательские скрипты и т. д.

Каждый YAML-файл объявляет одну или несколько именованных команд. Команды обнаруживаются автоматически обходом дерева каталогов, адресуются через точечный идентификатор и могут выполняться напрямую (dwe commands <id>) либо ссылаться из сценариев и пайплайнов (deploy.yml, lifecycle.yml).

Эта страница — точка входа в справочник по конфигурации. Директивы, типы, шаблонизация и валидация описаны на отдельных соседних страницах — см. Дополнительное чтение в конце.

Структура файлов и идентификаторы команд

Заголовок раздела «Структура файлов и идентификаторы команд»

Структура каталогов определяет префикс группы, а имя файла без расширения определяет конечный сегмент. Имена файлов и подкаталогов полностью на усмотрение проекта — приведённые ниже названия иллюстративны, и допускается произвольное количество файлов любой вложенности.

workspace/commands/
├── <top-group>.yml → group: <top-group>
├── … → any number of top-level groups
└── <parent-group>/ → optional subdirectory expands a group
├── <child>.yml → group: <parent-group>.<child>
├── <child>/ → optional deeper subdirectory
│ └── <leaf>.yml → group: <parent-group>.<child>.<leaf>
└── … → any number of children, any depth

Конкретный проект может организовать всё, например, так — каждое имя здесь выбрано проектом, это не соглашение, навязываемое CLI:

workspace/commands/
├── db.yml → group: db
├── app.yml → group: app
└── services/
├── <service-a>.yml → group: services.<service-a>
├── <service-a>/
│ └── db.yml → group: services.<service-a>.db
└── <service-b>.yml → group: services.<service-b>

Полный идентификатор каждой команды — <group>.<name>, где <name> — ключ в карте commands: внутри файла.

Шаблон: размещайте основные команды группы в одном файле, названном по группе (services/<service>.yml), и разделяйте большие группы в соседний подкаталог только тогда, когда команд достаточно, чтобы оправдать логические подгруппы (services/<service>/db.yml, services/<service>/cache.yml). Подкаталог опционален — маленькие группы остаются в одном файле. Обязательных файлов нет: у проекта может быть ноль, одна или десятки групп на любой глубине.

# workspace/commands/db.yml → group "db"
commands:
cli: # full ID: db.cli
type: service_exec
...
dump-create: # full ID: db.dump-create
type: script
...

Зарезервированных имён файлов нет — каждый файл *.yml вносит сегмент, выводимый из его пути.

У каждого файла два ключа верхнего уровня: опциональный блок метаданных group: и обязательная карта commands:.

group:
title: Database
description: Database container management commands
commands:
<local-name>:
type: <type>
description: <text>
# ... directives below ...
ПолеТипОписание
group.titlestringОтображаемый заголовок, показываемый в dwe commands list
group.descriptionstringКороткое описание, отображаемое рядом с группой; как и у команды, там показывается только его первая непустая строка (см. Описание и краткая строка)
group.hidestringОпциональное выражение-условие; когда truthy, скрывает группу и каскадно — все её потомки (команды и подгруппы). См. Условие hide.
commandsmapИменованные определения команд (ключ = локальное имя)

Каждая команда, независимо от типа, проходит один и тот же пайплайн выполнения:

flowchart TD
    A[Разрешение параметров] --> B[Разрешение контекста]
    B --> C[Вычисление путей файлов]
    C --> D{Подтверждение?}
    D -- да --> E[Запрос пользователя]
    D -- нет --> F[Подготовка файловых эффектов]
    E --> F
    F --> G[Запуск раннера]
    G --> H{Успех?}
    H -- да --> I[Сообщение об успехе]
    H -- нет --> J[Выполнение cleanup в порядке LIFO]
    J --> K[Сообщение об ошибке]

Фазы:

  1. Разрешение параметров — для каждого объявленного параметра пробуются по очереди: переданное значение → default_from (точечный путь в объединённый конфиг; пустой результат считается отсутствием) → литеральный default → ошибка обязательности. Затем значение приводится к объявленному типу и проверяется на pattern.

  2. Разрешение контекста — каждый точечный путь context.<key>.from читается из объединённого конфига.

    dwe validate предупреждает (домен commands), когда один из этих dot-путей не резолвится в собранном конфиге. Последствия у полей разные: неразрешимый params.<name>.default_from уходит в default: либо валит параметр, если тот required: и другого значения нет; неразрешимый params.<name>.options.from даёт пустой список опций, и форма выбора не предлагает ничего; неразрешимый context.<name>.from рендерится пустой строкой. dwe validate --strict превращает предупреждение в ошибку.

  3. Вычисление путей файлов — рендерятся шаблоны path / candidates, пути нормализуются в абсолютные, выполняется поиск файлов. Без побочных эффектов.

  4. Подтверждение — при confirmation: true запрашивается подтверждение пользователя; запрос обходится только через SkipConfirm (устанавливается флагом --yes / -y и наследуется дочерними шагами сценария). Иначе диспетчеризация по stdin: TTY → huh.Confirm, не-TTY → простой fallback Y/n, который автоматически отвечает «yes», когда переменная окружения CI установлена (в любое непустое значение). Отказ прерывает команду. Полное дерево решений см. в Поток подтверждения.

  5. Подготовка файловых эффектов — mkdir, проверки overwrite, регистрация cleanup-колбэков.

  6. Запуск — диспетчеризация в раннер, специфичный для типа (host shell, DWE CLI, контейнерные exec/run, скрипт или сценарий).

  7. Успех / ошибка — выводится messages.success или messages.error. При ошибке зарегистрированные cleanup-колбэки срабатывают в порядке LIFO до сообщения об ошибке.

  • Файлы под workspace/commands/ рекурсивно обнаруживаются на старте.
  • Каждый файл парсится и валидируется; сбой загрузки останавливает запуск со структурированной ошибкой, указывающей на файл и поле.
  • private: true скрывает команды из dwe commands list и отвергает прямой вызов через dwe commands. На приватные команды по-прежнему можно ссылаться из сценариев и пайплайнов — полезно для шагов, которые должны запускаться только как часть более крупной последовательности.
db.up:
type: dwe
private: true # used only inside db.start workflow
cmd: "docker up db"
db.create:
type: service_exec
description: Create a database in the db container
service: db
params:
database:
type: string
required: true
pattern: ^[a-zA-Z0-9_-]+$
env:
MYSQL_PWD: "${vars.db.password}"
messages:
success: "Database `${param.database}` is ready."
error: "Failed to create database `${param.database}`."
cmd: "mariadb -u${vars.db.user} -e 'CREATE DATABASE IF NOT EXISTS `${param.database}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'"
db.dump-create:
type: script
description: Create a database dump file
params:
database:
type: string
default_from: vars.db.database
pattern: ^[a-zA-Z0-9_-]+$
dump_dir:
type: string
default_from: vars.db.backup_dir
required: true
pattern: ^[^*?\[\]]+$
dump_date:
type: bool
default: true
env:
DB_NAME: "${param.database}"
DB_USER: "${vars.db.user}"
MYSQL_PWD: "${vars.db.password}"
files:
dump:
access: write
path: "${param.dump_dir}/${param.database}{{ if .Params.dump_date }}_{{ now | date \"2006-01-02\" }}{{ end }}.sql.gz"
mkdir: true
overwrite: true
on_error: remove
env: DUMP_FILE
script:
path: workspace/scripts/db/dump-create.sh
shell: bash
messages:
success: "Database dump created at ${files.dump.path}"
error: "Failed to create database dump"
db.dump-deploy:
type: script
confirmation: true
confirmation_text: "This will DROP and recreate `${param.target_database}`. Continue?"
params:
target_database:
default_from: vars.db.database
required: true
dump_dir:
default_from: vars.db.backup_dir
required: true
files:
dump:
access: read
candidates:
- glob: "${param.dump_dir}/${param.target_database}_*.sql.gz"
match: '\d{4}-\d{2}-\d{2}'
sort: name_desc
- path: "${param.dump_dir}/${param.target_database}.sql.gz"
required: true
env: DUMP_FILE
script:
path: workspace/scripts/db/dump-deploy.sh
shell: bash

Сценарий с условными шагами и шагом подтверждения

Заголовок раздела «Сценарий с условными шагами и шагом подтверждения»
reset-and-bootstrap:
type: workflow
description: Drop, recreate, and bootstrap the main service
steps:
- confirm: "Drop and re-bootstrap `${vars.db.database}`?"
- command: db.drop
with:
database: "${vars.db.database}"
- command: services.main.db.create
- command: services.main.composer-install
when: "file-missing services/main/src/vendor/autoload.php"
- command: services.main.migrate
- command: optional-cache-warm
continue_on_error: true
workspace/commands/db.yml
db.up:
type: dwe
private: true
description: Start the database container in the background
cmd: "docker up db"
db.wait:
type: builtin
private: true
description: Wait for the db container to become healthy
cmd: docker_wait_healthy
with:
services: [db]
timeout: 120s
interval: 2s
db.start:
type: workflow
private: true
description: Start the database container and wait until healthy
steps:
- command: db.up
- command: db.wait

db.start нельзя вызвать напрямую через dwe commands db.start, но bootstrap может ссылаться на неё из своих steps:. Композиция выше — каноничный шаблон: тонкий type: dwe для запуска, type: builtin для ожидания и type: workflow, связывающий их вместе.

Кастомные имена сетей + частичный up/run. Если ваш compose-файл задаёт сети явное networks.<x>.name:, учитывайте квирк docker-compose: labels сети выигрывает та команда, которая первой её материализует. Частичный docker up db или шаг type: service_run (запускающий docker compose run --rm --no-deps …), выполненный до полного подъёма стека, может создать именованную сеть с labels, которые последующий up --wait затем отвергнет (network <x> … has incorrect label com.docker.compose.network). Это поведение самого compose, а не баг DWE — DWE не объявляет собственных сетей и передаёт один и тот же project/-f во все вызовы. Решение — порядок: поднимите весь стек (docker up --wait, встроенная финальная фаза деплоя) до любого частичного up <svc> или run --rm, который трогает кастомно-именованную сеть, либо уберите явное name: и дайте compose заскоупить сеть на проект.

  • dwe commands list — перечислить все публичные команды, сгруппированные по файлам
  • dwe commands <id> [--set k=v] [--yes] [-- <args>] — выполнить команду (псевдоним: dwe cmd <id>). Всё, что после --, предлагается команде как ${args} — включается для каждой команды отдельно, см. директивы § Сквозные аргументы
  • dwe commands --inspect <id> (или -i) — показать разрешённое определение (params, context, env, runner)
  • dwe docs generate — перегенерировать справочник по командам в docs/reference/commands/

Когда dwe commands вызывается без точного идентификатора команды на интерактивном терминале — и без --inspect, которому идентификатор обязателен, — открывается интерактивный двухпанельный браузер команд; см. Интерактивный браузер.

--inspect / -i взаимоисключающие с --set и --yes; требует точного идентификатора команды и печатает определение, не запуская её.

dwe commands list [group] --output json печатает объект, поле commands которого — один плоский массив, отсортированный по id. Необязательный group сужает его до этой группы и всего, что в неё вложено (admin совпадает с admin.lint, но не с administration):

{"commands":[
{"id":"app.install","group":"app","title":"install","description":"Install application dependencies","summary":"Install application dependencies","type":"service_exec","service":"app-main","params":[{"name":"env","type":"string","required":true}]},
{"id":"db.migrate","group":"db","title":"migrate","description":"Run database migrations","summary":"Run database migrations","type":"shell"}
]}
КлючПримечания
id, title, typeесть всегда; title — последний сегмент id
groupопускается для команды вне групп
descriptionопускается, если пусто. Полный текст. Локализуется: при неанглийской локали содержит перевод из workspace/i18n/<lang>.yml, поэтому сопоставляйте команды по id, а не по этому тексту
summaryопускается, если пусто. Первая непустая строка description — см. Описание и краткая строка. dwe commands -i <id> --output json содержит ту же пару
serviceопускается, если команда его не объявляет. Это объявленное значение, а не отрендеренное — шаблонный service: app-${param.service} печатается как есть, так что это не всегда буквальное имя контейнера. Команды .start / .logs / .stop / .restart, в которые разворачивается type: daemon, несут собственный сервис демона
privateтолько с --all
paramsопускается, если параметров нет; полное описание параметров — в dwe commands -i <id> --output json

description и service позволяют агенту выбрать команду по одному листингу: чтобы найти то, что выполняется в контейнере, фильтруйте по service; чтобы понять назначение команды, читайте description. Команды, скрытые активным hide:, отсутствуют — как и в текстовом листинге.

Браузер работает на общем фреймворке tui: две панели с рамками (дерево слева, список команд справа), нижняя строка статуса (бренд · breadcrumb · ? help) и модальная справка по ?, которая перечисляет биндинги, активные в текущем режиме. Фокус панелей переключается по Tab / Shift+Tab; сфокусированная панель подсвечивается своей рамкой. Мышь поддерживается (см. Мышь).

Он не конфигурируется: группы верхнего уровня раскрыты на входе, пустые поддеревья сворачиваются при fuzzy-фильтрации, бэйджи типов включены. Это описание полного двухпанельного фрейма; на терминале уже 100 колонок бэйджи и счётчики групп отключаются, а ниже 80 колонок или 15 строк браузер деградирует до плоского списка — см. Fallback-лестница.

Фокус переключается между двумя панелями по Tab / Shift+Tab. Биндинги навигации и выбора диспетчеризуются в сфокусированную панель; биндинги, специфичные для дерева (→/l, ←/h), инертны в списке, а run-режимные глаголы (e, y) отсутствуют вне run-режима. Модалка ? перечисляет ровно те биндинги, что активны в текущем режиме.

БиндингДействие
Tab / Shift+Tabпереключить фокус между панелью дерева и списка
↑/↓, k/jпереместить курсор в сфокусированной панели
→, lраскрыть фокусированную группу или войти в её первого ребёнка (только дерево; no-op в списке)
←, hсвернуть фокусированную группу или перейти к её родителю (только дерево; no-op в списке)
Home / Endперейти к первой / последней строке сфокусированной панели
PgUp / PgDnпроскроллить сфокусированную панель на один вьюпорт
Enterна группе дерева: переключить раскрытие; на элементе списка: подтвердить выбор (запуск, inspect или edit, в зависимости от точки входа)
/войти в режим inline-фильтра
iоткрыть inspect-оверлей для подсвеченной команды
yпереключить skip-confirm (только run-режим); строка статуса показывает [--yes ON]
eподтвердить подсвеченную команду и принудительно открыть форму параметров (только run-режим)
?переключить модальную справку
Esc, q, Ctrl+Cвыйти из браузера

Пока активен inline-фильтр (/), напечатанные символы уточняют fuzzy-запрос — строка запроса рендерится внутри панели дерева, дерево сужается на лету с обновлёнными счётчиками совпадений M/N, Enter фиксирует отфильтрованное раскрытие, а Esc восстанавливает прежнее состояние раскрытия. Буквы-действия (i, e, y, …) при фильтрации печатаются в запрос, а не диспетчеризуются.

Выберите результат поиска клавишами ↑/↓. Первый Enter закрывает запрос и переводит фокус в группу команды, сохраняя выделение этой же команды в полном списке группы. Повторный Enter подтверждает выбор; само закрытие поиска команду не запускает.

Пока открыт inspect-оверлей (i), он захватывает ввод: ↑/↓, k/j, PgUp/PgDn, Home/End скроллят центрированный вьюпорт, Enter подтверждает (возвращает тот же Result, что и Enter из списка), а Esc закрывает оверлей.

На пути с двухпанельным фреймом (TTY ≥ 80 колонок) выбор команды с параметрами открывает форму параметров как оверлей поверх браузера — браузер затемняется под ней, строка статуса остаётся видимой — вместо того чтобы сначала разбирать TUI. Вы заполняете параметры на месте; многополевые формы скроллятся внутри оверлея (сфокусированное поле остаётся видимым). При отправке оверлей закрывается, браузер выходит, и команда запускается в обычном терминале (баннер + потоковый вывод) с введёнными значениями. Esc отменяет форму и возвращает вас в браузер без запуска команды, сохраняя состояние курсора / раскрытия / фильтра; Ctrl+C выходит из всего TUI.

Enter автоматически пропускает форму, когда у каждого обязательного параметра уже есть значение (из --set или объявленного дефолта) — команда запускается сразу без оверлея. Используйте e (edit-parameters), чтобы принудительно открыть форму, даже когда обязательные значения уже удовлетворены. Команда без параметров всегда запускается сразу по Enter.

Команда всё равно исполняется после выхода из TUI (она стримит вывод docker / пайплайна в обычный терминал); в оверлей переезжает только ввод параметров. Промпт confirmation:, если он объявлен, по-прежнему показывает своё yes/no в обычном терминале после выхода из TUI, непосредственно перед запуском. На узком (< 80 колонок) fallback браузер сохраняет плоский поток exit-then-form. Прямые вызовы dwe commands <id> (с --set или без), piped / неинтерактивные и --output json не затрагиваются. (Голый dwe commands --set … без id на TTY по-прежнему открывает браузер — значения --set просто предзаполняют оверлей.)

Биндинги e (edit-parameters) и y (skip-confirm) регистрируются только в run-режиме (дефолт, когда --inspect / -i не задан); в inspect-режиме и в edit-режиме vars-браузера они отсутствуют и в keymap, и в справке ?.

Браузер инспектирует терминал на старте и грациозно деградирует. Ниже минимума для двух панелей (или при ошибке чтения размера) он сразу падает на плоский список huh.NewSelect — in-TUI режима с одной панелью больше нет.

УсловиеПоведение
non-TTYвызывающее место завершается с существующей ошибкой no exact command ID given; pass a full command ID or run in an interactive terminal; до браузера дело не доходит (а Run защитно отменяет, если всё же дошло)
TTY, width < 80 или height < 15делегирует плоскому списку huh.NewSelect (pre-browser UX)
TTY, width ∈ [80, 99]two-panel фрейм без счётчиков групп (N) и без бэйджей типов
TTY, width ≥ 100полный two-panel фрейм с бэйджами и счётчиками (breadcrumb всегда показан в нижней строке статуса)

NO_COLOR=1 учитывается автоматически через lipgloss/bubbletea: бэйджи рендерятся как простой текст, focus-маркер использует bold вместо цвета.

Фрейм включает поддержку мыши:

  • Одиночный клик в панели перемещает курсор на кликнутую строку и ставит фокус на эту панель; он никогда не переключает группу и не запускает команду.
  • Двойной клик работает как Enter на кликнутой строке — переключает группу дерева или подтверждает элемент списка.
  • Колесо скроллит панель под указателем (дерево, список команд или открытый оверлей инспекта), не меняя фокус — работает независимо от того, какая панель сфокусирована.
  • Клик по подсказке ? help в строке статуса переключает модальную справку; клики внутри открытой модалки проглатываются.
  • directives.md — общие поля: идентичность, подтверждение, сообщения, уведомления, params, context, env, files
  • types.md — восемь типов команд и их специфичные поля, плюс разрешение workdir
  • templating.md — контекст рендера, резолверы уровня команды, справочник по template-пространству
  • validation.md — шпаргалка по правилам валидации и типичные подводные камни
  • styles.md — ключи палитры, используемые type-бэйджами браузера