{"_id":"@busyburger/p3k","name":"@busyburger/p3k","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@busyburger/p3k","version":"0.1.0","description":"CLI для локального окружения: dev поднимает проект, doctor объясняет, почему он не поднялся","type":"module","bin":{"p3k":"dist/cli.js"},"engines":{"node":">=18"},"scripts":{"build":"tsc","dev":"tsc --watch","test":"tsc -p tsconfig.test.json && cd .test-build && node --test","start":"node dist/cli.js","prepublishOnly":"npm run build && npm test"},"devDependencies":{"@types/node":"^24.0.0","typescript":"^5.5.4"},"license":"MIT","keywords":["cli","dev","orchestration","doctor","diagnostics","localhost","monorepo","tasks"],"repository":{"type":"git","url":"git+https://github.com/busyburgerr/p3k.git","directory":"cli"},"bugs":{"url":"https://github.com/busyburgerr/p3k/issues"},"homepage":"https://github.com/busyburgerr/p3k#readme","publishConfig":{"access":"public"},"_id":"@busyburger/p3k@0.1.0","_nodeVersion":"24.19.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-Ha6QKgsyps5J9yQhBoAtzXvPBpN/a9VgGz0nrTyj2rwj8nAIUiC2R7bDxwCVB66PVB1mb8BmNkrO7ZxkRS9ojQ==","shasum":"9dc400217cd32e32ee343c6b264d08621ab1bb32","tarball":"https://registry.npmjs.org/@busyburger/p3k/-/p3k-0.1.0.tgz","fileCount":61,"unpackedSize":346386,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIHZZWIll3V8H4/fkBmj25YJWkR0DwJvP+LO5r92BAVGyAiEAp34IYSUWymIXoKKLF3lTdwv+b8MMEH4ng3kOX+y2+vI="}]},"_npmUser":{"name":"busyburgerr","email":"whqhub@bk.ru"},"directories":{},"maintainers":[{"name":"busyburgerr","email":"whqhub@bk.ru"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/p3k_0.1.0_1788726284279_0.9499158806630079"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-06T20:24:44.117Z","0.1.0":"2026-09-06T20:24:44.415Z","modified":"2026-09-06T20:24:44.657Z"},"maintainers":[{"name":"busyburgerr","email":"whqhub@bk.ru"}],"description":"CLI для локального окружения: dev поднимает проект, doctor объясняет, почему он не поднялся","homepage":"https://github.com/busyburgerr/p3k#readme","keywords":["cli","dev","orchestration","doctor","diagnostics","localhost","monorepo","tasks"],"repository":{"type":"git","url":"git+https://github.com/busyburgerr/p3k.git","directory":"cli"},"bugs":{"url":"https://github.com/busyburgerr/p3k/issues"},"license":"MIT","readme":"# p3k\r\n\r\nCLI для пути проекта от пустой папки до продакшена. Сейчас пройдены первые шаги:\r\n\r\n- **`init`** — каркас проекта и `p3k.json`, с которого начинается всё остальное.\r\n- **`dev`** — поднять окружение одной командой, в правильном порядке, с одним потоком логов и корректной остановкой.\r\n- **`check`** — прогнать проверки проекта до того, как это сделает CI.\r\n- **`doctor`** — выяснить, почему оно не поднимается.\r\n\r\nСвязывает их конфиг: `init` его пишет, `dev` и `check` читают, дальше его же будет читать `ship`.\r\n\r\nРантайм-зависимостей нет. Нужен Node 18+.\r\n\r\n```bash\r\nnpm install\r\nnpm run build\r\nnpm test\r\nnode dist/cli.js init мой-проект --template static\r\nnode dist/cli.js dev\r\nnode dist/cli.js check\r\nnode dist/cli.js doctor\r\n```\r\n\r\n---\r\n\r\n## init\r\n\r\n```\r\n$ p3k init my-site --template static\r\n\r\n  Статический сайт → /tmp/my-site\r\n\r\n  + package.json\r\n  + build.mjs\r\n  + server.mjs\r\n  + public/index.html\r\n  + p3k.json\r\n  + .env.example\r\n  + .gitignore\r\n\r\n  создан git-репозиторий\r\n\r\n  Дальше:\r\n    cd my-site\r\n    p3k dev\r\n    откройте http://localhost:4000\r\n```\r\n\r\nШаблоны (`--list`):\r\n\r\n| id | что это | нужно |\r\n|---|---|---|\r\n| `static` | сборка и сервер статики — два процесса, показывает одноразовый шаг | — |\r\n| `node-postgres` | база в контейнере, миграции, API — весь граф целиком | Docker |\r\n\r\nСмысл команды не в раскладывании файлов, а в том, что она **порождает конфиг**:\r\nшаблон описывает не только исходники, но и то, из чего проект состоит и что от\r\nчего зависит. Дальше этот файл читает `dev`.\r\n\r\n### Осторожность с файлами\r\n\r\nНепустой каталог — повод остановиться, а не «аккуратно дополнить»: без `--force`\r\nкоманда откажется работать. Но и с `--force` **существующий файл не\r\nперезаписывается никогда** — он пропускается, и пропуск виден в отчёте.\r\nПерезапись чужой работы необратима, а дописать файл руками легко.\r\n\r\nОпции: `--template <id>`, `--name <имя>`, `--list`, `--force`, `--no-git`,\r\n`--help`. Без `--template` в интерактивном терминале спросим, в\r\nнеинтерактивном — откажемся с перечнем шаблонов.\r\n\r\n---\r\n\r\n## dev\r\n\r\nПроцессы описываются в `p3k.json`:\r\n\r\n```json\r\n{\r\n  \"processes\": {\r\n    \"db\": {\r\n      \"command\": \"docker run --rm --name pg-dev -p 5432:5432 -e POSTGRES_PASSWORD=dev postgres:16\",\r\n      \"ready\": { \"port\": 5432 },\r\n      \"stop\": \"docker stop pg-dev\"\r\n    },\r\n    \"migrate\": {\r\n      \"command\": \"npm run migrate\",\r\n      \"needs\": [\"db\"],\r\n      \"oneShot\": true\r\n    },\r\n    \"api\": {\r\n      \"command\": \"npm run dev\",\r\n      \"cwd\": \"api\",\r\n      \"env\": { \"PORT\": \"4000\" },\r\n      \"needs\": [\"migrate\"],\r\n      \"ready\": { \"port\": 4000 }\r\n    },\r\n    \"web\": {\r\n      \"command\": \"npm run dev\",\r\n      \"needs\": [\"api\"],\r\n      \"ready\": { \"log\": \"ready in\" }\r\n    }\r\n  }\r\n}\r\n```\r\n\r\nВывод:\r\n\r\n```\r\n        │ конфиг: /project/p3k.json\r\n        │ процессов: 4, волн запуска: 4\r\n        │ db: запуск — docker run --rm --name pg-dev …\r\n        │ db: готов (порт 5432)\r\n        │ migrate: запуск — npm run migrate\r\nmigrate │ применено 2 миграции\r\n        │ migrate: выполнен\r\n        │ api: запуск — npm run dev\r\napi     │ listening on 4000\r\n        │ api: готов (порт 4000)\r\nweb     │ ready in 312 ms\r\n        │ всё поднято — Ctrl+C для остановки\r\n```\r\n\r\n### Что здесь важного\r\n\r\n**Порядок с гарантией.** `needs` задаёт зависимости, процессы стартуют волнами:\r\nвнутри волны параллельно, следующая волна — только когда вся предыдущая\r\n**отвечает**, а не «запущена». Ждать умеет три вещи:\r\n\r\n| `ready` | когда готов | для чего |\r\n|---|---|---|\r\n| `{ \"port\": 5432 }` | порт принимает соединение | быстро, но см. ниже |\r\n| `{ \"exec\": \"docker exec pg pg_isready\" }` | команда вернула 0 | базы данных |\r\n| `{ \"http\": \"http://localhost:4000/health\" }` | ответ со статусом < 500 | HTTP-службы |\r\n| `{ \"log\": \"ready in\" }` | строка появилась в выводе | сборщики, воркеры |\r\n| `{ \"delay\": 800 }` | просто пауза | когда другого способа нет |\r\n\r\n**`port` врёт на службах, которые открывают порт раньше, чем готовы\r\nобслуживать.** Postgres принимает соединения ещё во время инициализации\r\nкластера: `{ \"port\": 5432 }` сработает сразу, зависимый процесс стартует и\r\nполучит отказ уже от самой базы. Для баз честен только `exec`, для HTTP —\r\n`http`. Ожидание порта проверяется соединением, а если оно заблокировано\r\nлокальной политикой — по таблице слушающих сокетов. Без `ready` процесс\r\nсчитается готовым сразу.\r\n\r\n**Одноразовые задачи.** `\"oneShot\": true` — процесс должен отработать и выйти\r\n(миграции, кодогенерация). Его готовность — успешный выход, и его завершение\r\nне считается поводом гасить окружение. Без этого флага любой завершившийся\r\nпроцесс роняет всю сессию, потому что работать без части системы бессмысленно.\r\n\r\n**Остановка снимает дерево, а не оболочку.** `command` исполняется через shell,\r\nто есть прямой потомок — это `cmd.exe` или `sh`, а работа идёт во внуках. Убить\r\nодного потомка недостаточно: именно так после Ctrl+C остаются висеть занятые\r\nпорты и контейнеры. `dev` гасит всю группу процессов в порядке, обратном\r\nзависимостям, а для того, что переживает смерть родителя, есть `stop`.\r\n\r\n**Порты проверяются до запуска.** Занятый порт даёт внятную строку сразу, а не\r\nневнятную ошибку изнутри чужого процесса через десять секунд.\r\n\r\n### Опции\r\n\r\n`--only <имя>` запустить только этот процесс и то, от чего он зависит (можно\r\nповторять). `--help` справка.\r\n\r\nКод возврата: код упавшего процесса, `2` при ошибке конфигурации, `0` при\r\nштатной остановке.\r\n\r\n---\r\n\r\n## check\r\n\r\nТе же проверки, что потом прогонит CI, но у вас на машине и до пуша.\r\nПроверка — это либо команда, которая должна завершиться успешно, либо бюджет\r\nразмера собранных файлов.\r\n\r\n```\r\n$ p3k check\r\n\r\n  /project/p3k.json\r\n\r\n  ✓  types · 2.3с\r\n  ✓  build · 1.5с\r\n  ✓  bundle — 40.0kb из 60.0kb (gzip) · 0.0с\r\n\r\n  3 из 3 · 2.3с\r\n```\r\n\r\n```json\r\n\"checks\": {\r\n  \"types\":  { \"command\": \"npm run typecheck\" },\r\n  \"build\":  { \"command\": \"npm run build\" },\r\n  \"bundle\": { \"size\": { \"path\": \"dist/assets\", \"max\": \"60kb\", \"gzip\": true }, \"needs\": [\"build\"] },\r\n  \"lint\":   { \"command\": \"npm run lint\", \"optional\": true }\r\n}\r\n```\r\n\r\n### Что здесь важного\r\n\r\n**Параллельно, но в предсказуемом порядке.** Проверки запускаются все сразу, а\r\nпечатаются в порядке конфига — вывод одинаков от прогона к прогону, но ждать\r\nприходится по самой медленной, а не по сумме всех. В примере выше 2,3с против\r\n3,8с последовательно.\r\n\r\n**`needs` — как в dev.** Поле говорит, какие проверки должны пройти раньше этой.\r\nБюджет размера считает файлы в `dist`, а создаёт их сборка: без `needs` он\r\nпомерял бы прошлую сборку или пустой каталог. Проверки идут волнами; если то, от\r\nчего они зависят, не прошло, зависимые помечаются пропущенными, а не\r\nпроваленными. Это разные вещи: проваленная значит «проверили, и плохо»,\r\nпропущенная — «проверить не смогли».\r\n\r\n**Вывод показывается только у непрошедших.** Логи параллельных проверок\r\nвперемешку нечитаемы, поэтому они копятся и печатаются только там, где что-то\r\nсломалось.\r\n\r\n**Бюджет размера** считает файл или каталог целиком; `\"gzip\": true` сжимает\r\nкаждый файл отдельно и складывает — так же, как их отдаст сервер. При\r\nпревышении печатаются пять самых крупных файлов: это то, с чем идти разбираться.\r\n\r\n**Без секции `checks`** проверки выводятся из `scripts` в package.json\r\n(`typecheck`, `types`, `lint`, `test` — от быстрых к медленным). `build` туда\r\nнамеренно не входит: у него побочные эффекты. Источник всегда напечатан первой\r\nстрокой.\r\n\r\n**Кэш по содержимому входов.** Поле `inputs` перечисляет файлы и каталоги, от\r\nкоторых зависит результат; пока они не менялись, успешный результат берётся из\r\nкэша, а проверка не запускается. Ключ считается по содержимому, а не по `mtime`:\r\nпосле `git checkout` время меняется, а содержимое нет, и кэш по времени\r\nсбрасывался бы впустую. Сама команда тоже входит в ключ.\r\n\r\n```json\r\n\"types\": { \"command\": \"npm run typecheck\", \"inputs\": [\"src\", \"tsconfig.json\"] }\r\n```\r\n\r\nЗапоминается только успех: причина провала часто лежит вне объявленных входов —\r\nсеть, чужой процесс, занятый порт. Без `inputs` проверка не кэшируется вовсе.\r\nКэш лежит в `node_modules/.cache/p3k/`, сбрасывается флагом `--no-cache`.\r\n\r\nОпции: `--only <имя>` (подтягивает то, от чего проверка зависит), `--list`,\r\n`--no-cache`, `--json`, `--help`. Код возврата `1`, если не прошли обязательные проверки, `2`\r\nпри ошибке конфигурации.\r\n\r\n---\r\n\r\n## doctor\r\n\r\nОбычный dev-сервер при сбое печатает ошибку того слоя, где сломалось. Проблема\r\nв том, что самые частые сбои **не дают ошибки вовсе** — или дают такую, которая\r\nуводит не туда. `doctor` проверяет окружение напрямую и печатает, что именно\r\nсломано и какой командой это чинится.\r\n\r\n```\r\n  СЕТЬ\r\n  ✗  3000 слушается только по IPv6\r\n     привязка: ::1 — этот адрес не принимает IPv4-соединения\r\n     localhost резолвится в ::1, 127.0.0.1\r\n     браузеры и curl, выбирающие IPv4, получат отказ: страница будет пустой,\r\n     а в логе сервера не появится ни одной ошибки\r\n     → привяжите сервер к IPv4: server.host = '127.0.0.1' в конфиге\r\n```\r\n\r\n| Группа | Что смотрит |\r\n|---|---|\r\n| РАНТАЙМЫ | версия Node против `engines.node`; демон Docker — только если в проекте есть Dockerfile или compose |\r\n| ПОРТЫ | занят ли порт и каким процессом (PID и имя) |\r\n| СЕТЬ | резолвится ли `localhost`; доступен ли слушающий порт и по IPv4, и по IPv6 |\r\n| ОКРУЖЕНИЕ | ключи из `.env.example`, которых нет ни в `.env`, ни в окружении процесса |\r\n| ЗАВИСИМОСТИ | установлен ли `node_modules` и не отстал ли от локфайла; нарушенные peer-зависимости |\r\n\r\nПорты берутся из `scripts` в package.json и из конфигов (`vite.config.*` и\r\nподобных). Конфиги читаются **текстом, а не исполняются**: запускать чужой код\r\nради номера порта — плохой размен. Поэтому у каждого порта указан источник.\r\n\r\nОпции: `--port <n>`, `--json`, `--help`. Код возврата `1` при блокирующих\r\nпроблемах — годится для CI и pre-commit.\r\n\r\n### Три правила, по которым отбираются проверки\r\n\r\n1. **Детерминированный сигнал.** Не эвристика, а факт: сокет либо слушает\r\n   адрес, либо нет.\r\n2. **Готовое действие.** Конкретная команда или правка, а не «проверьте\r\n   настройки сети».\r\n3. **Реальная частота.** Иначе это шум, в котором тонет полезное.\r\n\r\nОтсюда четвёртое — **честность о границах**. Проверка, которая ничего не\r\nвыяснила, обязана сказать «не выяснил», а не «всё хорошо» и не «всё сломано».\r\nЗаблокированная фаерволом проба соединения — это отсутствие данных, а не\r\nзакрытый порт. Всё пропущенное перечисляется отдельной строкой в конце отчёта.\r\n\r\n---\r\n\r\n## Устройство\r\n\r\n```\r\nsrc/\r\n  cli.ts          диспетчер команд\r\n  types.ts        общие типы\r\n  util/           exec, net, semver, project, plural — общее для всех команд\r\n  config.ts       поиск и чтение p3k.json — общее для dev и check\r\n  init/           index (аргументы и запись файлов), templates (шаблоны)\r\n  dev/            index (аргументы), config, graph, supervisor, log\r\n  check/          index (прогон и вывод), config (проверки), size, cache\r\ntest/             тесты на чистые функции: node:test, без зависимостей\r\n  doctor/         index (сбор отчёта), report (вывод), checks/*\r\n```\r\n\r\nПроверка доктора — объект `{ id, group, run(ctx) }`; упавшая проверка не роняет\r\nотчёт, а превращается в `skip` с текстом ошибки. Добавить новую — один файл в\r\n`src/doctor/checks/` и строка в массиве в `src/doctor/index.ts`.\r\n\r\nПланировщик волн (`util/graph.ts`) общий: `dev` раскладывает по нему процессы,\r\n`check` — проверки. Задача одна и та же, и решать её дважды незачем.\r\n\r\nРазбор диапазонов версий свой, ~130 строк, без зависимости от `semver`: умеет\r\n`^ ~ >= > <= <`, неполные версии (`>=18`), `1.2.x`, `||` и пересечения. Всё, что\r\nразобрать не удалось, даёт «не знаю», а не «не подходит».\r\n\r\n## Тесты\r\n\r\n```bash\r\nnpm test    # tsc -p tsconfig.test.json && node --test .test-build/test/\r\n```\r\n\r\n43 теста на `node:test` — встроенный раннер, зависимостей не добавляет. Покрыты\r\nчистые функции, где ошибка тихая и дорогая: разбор диапазонов версий,\r\nпланировщик волн, измерение размеров, разбор обоих конфигов и ключ кэша.\r\nПроцессы и сеть тестами не покрыты — они проверяются прогонами.\r\n\r\n## Лицензия\r\n\r\nMIT.\r\n\r\n## Известные ограничения\r\n\r\n**Конфиг `dev` — JSON, а не TypeScript.** Исполнять пользовательский TS\r\nпришлось бы через сборщик: зависимость и целый класс ошибок ради удобства\r\nзаписи. TS-конфиг возможен позже, отдельным шагом.\r\n\r\n**Осиротевшие процессы при жёстком убийстве родителя.** Если сам `p3k dev`\r\nснят через SIGKILL или `taskkill /F`, обработчик не отработает и дети выживут.\r\nНадёжное решение на Windows — job object с `KILL_ON_JOB_CLOSE`, а это нативный\r\nмодуль, то есть зависимость. Штатный путь остановки (Ctrl+C, падение процесса)\r\nдерево снимает полностью.\r\n\r\n**Мягкая остановка на Windows почти всегда не срабатывает.** `taskkill` без\r\n`/F` шлёт `WM_CLOSE`, который консольные приложения игнорируют, поэтому там\r\nпауза перед принудительным снятием короткая — растягивать её значит просто\r\nзадерживать выход.\r\n\r\n**`check` не умеет диффа миграций** — он есть в замысле, но требует поднятой\r\nбазы и разбора схемы. Пока это описывается обычной проверкой вида `command`.\r\n\r\n**Контейнеры оркестрируются через обычный `command`**, а не через docker API:\r\n`docker run …` плюс `stop`. Отдельного вида `service` пока нет.\r\n\r\n**Unix-ветка определения владельца порта** (`lsof`, с откатом на `ss`) написана,\r\nно на живой системе не проверялась — тестовая машина под Windows.\r\n\r\n**Шаблон `node-postgres` целиком не прогонялся**: на тестовой машине не запущен\r\nDocker. Проверено, что `init` его раскладывает и что `dev` разбирает\r\nпорождённый конфиг в правильные волны (`db` → `migrate` → `api`); сам подъём\r\nконтейнера не проверялся.\r\n\r\n**Шаблоны — это файлы, а не запуск чужих скаффолдеров.** Делегирование вида\r\n`npm create vite@latest` в план заложено, но в этой версии его нет: оно требует\r\nсети и делает результат непроверяемым.\r\n\r\n**Имя проекта приводится к безопасному виду** (латиница, цифры, дефис) — оно\r\nпопадает в `package.json` и в имя контейнера. Если пришлось что-то заменить,\r\nкоманда об этом скажет.\r\n","readmeFilename":"README.md","_rev":"1-ff41dc8f02b6c8da64bd80725a9c2571"}