feat(boot): ke-bootstrap docker image + boot:remote режим (story 6.1)

Образ ghcr.io/coopenomics/bootstrap для one-shot первичного деплоя
контрактов на KE-узел в docker-compose. Соответствует решению из
project_ke_bootstrap_strategy: boot/contracts собираются в mono,
apps-catalog только потребляет готовые образы.

Состав:
- components/boot/src/index.ts — команда `boot:remote`: ждёт RPC
  по CHAIN_URL, вызывает startInfra() через eosjs (без dockerode).
  Опциональные initial-data/extra-data через env-флаги.
- components/boot/src/configs/contracts.ts — env-driven CONTRACTS_DIR.
  Когда задан — flat layout /contracts/<name>/<name>.{wasm,abi}
  (как в dicoop/contracts), иначе старые относительные пути.
- components/boot/package.json — script `build` (unbuild) +
  `boot:remote` для локального запуска.
- components/boot/docker/Dockerfile — multi-stage:
  deps → build (cooptypes/factory/boot) → pnpm deploy --prod →
  COPY --from=dicoop/contracts:<tag> → node:20-alpine slim runtime.
- components/boot/docker/entrypoint.sh — режимы boot:remote / verify
  / shell + проверка обязательных env'ов.
- .github/workflows/build-bootstrap.yaml — публикация в GHCR по push
  в dev/testnet/main + verify smoke-test после push'а.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
coopops
2026-04-30 11:14:09 +00:00
parent dec2162142
commit 6d89303544
6 changed files with 455 additions and 21 deletions
+146
View File
@@ -0,0 +1,146 @@
name: Build bootstrap container
# Сборка и публикация образа `ghcr.io/coopenomics/bootstrap` —
# one-shot оркестратора первичного деплоя контрактов на KE-узел
# (см. `components/boot/docker/Dockerfile`). Образ потребляется
# в docker-compose рядом с `dicoop/blockchain_v5.1.1`-нодой как
# `depends_on: ke-node: service_healthy`.
#
# Триггер — push в одну из веток-окружений (dev / testnet / main).
# Маппинг по convention `feedback_branch_convention`:
#
# dev → tag=dev, CONTRACTS_TAG=dev
# testnet → tag=testnet, CONTRACTS_TAG=testnet
# main → tag=main + latest, CONTRACTS_TAG=main
#
# Зависимость: образ `dicoop/contracts:<tag>` должен существовать
# (он публикуется через `build-contracts.yaml` отдельным workflow'ом).
# Если contracts ещё не собраны для целевой ветки — `docker build`
# упадёт на multi-stage `FROM dicoop/contracts:${CONTRACTS_TAG}`.
# Пересборка триггерится через `workflow_dispatch` после ручного
# обновления contracts.
#
# Path-фильтр на `components/boot/**`, `components/cooptypes/**`,
# `components/factory/**` (boot их импортирует через workspace) и
# на сам Dockerfile/workflow. Изменения контрактов сами по себе
# bootstrap не триггерят — они идут через `dicoop/contracts:<tag>`,
# и для подхвата обновлённого образа нужно либо пушнуть ветку
# заново, либо запустить workflow вручную.
#
# Секреты:
# GITHUB_TOKEN — авто, для push в ghcr.io
# TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID — уведомления
on:
push:
branches: [dev, testnet, main]
paths:
- 'components/boot/**'
- 'components/cooptypes/**'
- 'components/factory/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- '.github/workflows/build-bootstrap.yaml'
workflow_dispatch:
inputs:
contracts_tag:
description: 'Тег dicoop/contracts (override; default = тег ветки)'
required: false
default: ''
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine docker tag and contracts tag
run: |
case "${{ github.ref_name }}" in
main)
echo "DOCKER_TAG=main" >> $GITHUB_ENV
echo "EXTRA_TAG=latest" >> $GITHUB_ENV
echo "CONTRACTS_TAG=main" >> $GITHUB_ENV
;;
testnet)
echo "DOCKER_TAG=testnet" >> $GITHUB_ENV
echo "EXTRA_TAG=" >> $GITHUB_ENV
echo "CONTRACTS_TAG=testnet" >> $GITHUB_ENV
;;
dev)
echo "DOCKER_TAG=dev" >> $GITHUB_ENV
echo "EXTRA_TAG=" >> $GITHUB_ENV
echo "CONTRACTS_TAG=dev" >> $GITHUB_ENV
;;
*)
echo "Unsupported branch ${{ github.ref_name }}" >&2
exit 1
;;
esac
# workflow_dispatch override
if [ -n "${{ github.event.inputs.contracts_tag }}" ]; then
echo "CONTRACTS_TAG=${{ github.event.inputs.contracts_tag }}" >> $GITHUB_ENV
fi
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull base contracts image (sanity check)
run: |
docker pull "dicoop/contracts:${CONTRACTS_TAG}"
docker run --rm "dicoop/contracts:${CONTRACTS_TAG}" list | head -25
- name: Build and push image
run: |
IMAGE="ghcr.io/coopenomics/bootstrap"
docker build \
--build-arg "CONTRACTS_TAG=${CONTRACTS_TAG}" \
--label "org.opencontainers.image.revision=${{ github.sha }}" \
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--label "build.contracts_tag=${CONTRACTS_TAG}" \
-t "${IMAGE}:${DOCKER_TAG}" \
-f components/boot/docker/Dockerfile \
.
docker push "${IMAGE}:${DOCKER_TAG}"
if [ -n "${EXTRA_TAG}" ]; then
docker tag "${IMAGE}:${DOCKER_TAG}" "${IMAGE}:${EXTRA_TAG}"
docker push "${IMAGE}:${EXTRA_TAG}"
fi
# SHA-pinned tag для воспроизводимости (тот же подход, что и
# в build-contracts.yaml — позволяет запинить bootstrap к
# конкретному коммиту, а не двигающемуся branch-тегу).
SHORT_SHA="${GITHUB_SHA::7}"
docker tag "${IMAGE}:${DOCKER_TAG}" "${IMAGE}:${DOCKER_TAG}-${SHORT_SHA}"
docker push "${IMAGE}:${DOCKER_TAG}-${SHORT_SHA}"
- name: Verify pushed image
run: |
IMAGE="ghcr.io/coopenomics/bootstrap"
docker run --rm "${IMAGE}:${DOCKER_TAG}" verify
- name: Telegram notify success
if: ${{ success() }}
run: |
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="✅ [GITHUB MONO] ghcr.io/coopenomics/bootstrap:${DOCKER_TAG} (contracts=${CONTRACTS_TAG}, sha=${GITHUB_SHA::7})"
- name: Telegram notify failure
if: ${{ failure() }}
run: |
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="❌ [GITHUB MONO] Ошибка сборки ghcr.io/coopenomics/bootstrap на ${{ github.ref_name }} (sha=${GITHUB_SHA::7})"
+121
View File
@@ -0,0 +1,121 @@
# ke-bootstrap: образ-оркестратор первичного деплоя контрактов на KE-ноду.
#
# Назначение:
# Один раз при поднятии узла (testnet/local-dev) этот контейнер ждёт
# готовности RPC `${CHAIN_URL}/v1/chain/get_info`, затем запускает
# `boot:remote` — деплоит все контракты из примонтированного образа
# `dicoop/contracts` через RPC (`eosjs::setContract`), активирует
# protocol features, создаёт системный токен и т.д. После завершения
# контейнер выходит с кодом 0.
#
# Не управляет docker-контейнером ноды (в отличие от `boot`/`boot:clean`/
# `boot:extra` — тех режимов, что вызывают `dockerode` для запуска ноды
# рядом с собой). Нода поднимается отдельным сервисом docker-compose
# (`dicoop/blockchain_v5.1.1`), и зависимость декларируется через
# `depends_on: ke-node: service_healthy`.
#
# Параметры (env):
# CHAIN_URL — URL RPC ноды (default `http://localhost:8888`).
# EOSIO_PUB_KEY — public key для системных аккаунтов (обязателен).
# EOSIO_PRV_KEY — private key для подписи (обязателен).
# PASSWORD — пароль для cleos-wallet (используется командой `unlock`).
# CONTRACTS_DIR — корень wasm/abi (default `/contracts`).
# RPC_WAIT_TIMEOUT_MS — таймаут ожидания RPC (default 120000).
# INSTALL_INITIAL_DATA=1 — опционально: записать кооператив `voskhod` в
# MongoDB+PostgreSQL (требует `MONGO_URI`,
# `PG_HOST`/`PG_USER`/...). Без этого флага
# bootstrap делает только on-chain работу.
# INSTALL_EXTRA_DATA=1 — то же + расширенный совет (4 доп. пайщика).
#
# Layout слоёв:
# Stage 1 (deps) — `pnpm install` всего workspace, нужен для
# workspace-зависимостей `@coopenomics/factory`,
# `cooptypes` (boot их импортирует).
# Stage 2 (build) — `pnpm run build` для cooptypes/factory/boot
# (unbuild → dist/index.cjs + .mjs).
# Stage 3 (deploy) — `pnpm deploy --filter @coopenomics/boot --prod`
# складывает приложение + только prod-deps в /app.
# workspace:* → реальные пакеты, без cross-link.
# Stage 4 (contracts) — alias для `dicoop/contracts:<tag>`, отсюда
# берутся wasm/abi через `COPY --from`.
# Stage 5 (runtime) — slim node:20-alpine, `/app` + `/contracts`.
# ── Stage 1: install dependencies ────────────────────────────────────
FROM node:20-alpine AS deps
RUN corepack enable && corepack prepare pnpm@9.0.6 --activate \
&& apk add --no-cache git python3 make g++
WORKDIR /repo
# Workspace-конфиг + lockfile + корневой package.json — нужны pnpm для
# построения графа workspace до того, как мы скопируем компоненты.
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml lerna.json ./
# Рабочие пакеты, нужные для boot:
# - cooptypes / factory — workspace:* зависимости
# - boot — собственно сборка
COPY components/cooptypes/ ./components/cooptypes/
COPY components/factory/ ./components/factory/
COPY components/boot/ ./components/boot/
RUN pnpm install --frozen-lockfile
# ── Stage 2: build TS пакетов ────────────────────────────────────────
FROM deps AS build
# Порядок сборки важен: cooptypes → factory → boot (downstream зависят
# от dist/ upstream-пакетов). Lerna-граф решает это автоматически через
# --include-dependencies, но явное перечисление дешевле и предсказуемее.
RUN pnpm --filter cooptypes run build \
&& pnpm --filter @coopenomics/factory run build \
&& pnpm --filter @coopenomics/boot run build
# ── Stage 3: pnpm deploy → standalone /app ───────────────────────────
FROM build AS deploy
# `pnpm deploy --prod` создаёт автономную копию пакета с разрешёнными
# `workspace:*` зависимостями (тянет реальные пакеты из workspace, не
# симлинки). devDeps отрезаются, так что esno/typescript/vitest не
# попадают в финальный образ.
RUN pnpm deploy --filter @coopenomics/boot --prod /app
# ── Stage 4: contracts (wasm/abi артефакты) ──────────────────────────
# Тег этого образа должен совпадать с тегом ke-bootstrap (см.
# `.github/workflows/build-bootstrap.yaml`). Multi-stage `COPY` не
# зависит от того, что dicoop/contracts что-то делает в ENTRYPOINT —
# нам нужен только filesystem.
ARG CONTRACTS_TAG=dev
FROM dicoop/contracts:${CONTRACTS_TAG} AS contracts
# ── Stage 5: runtime ─────────────────────────────────────────────────
FROM node:20-alpine AS runtime
# bash + curl — entrypoint использует bash (set -e, конструкции `[[ ]]`)
# и опционально curl для дебага healthcheck'а.
RUN apk add --no-cache bash curl
# Приложение
COPY --from=deploy /app /app
# Артефакты контрактов (flat layout: /contracts/<name>/<name>.{wasm,abi})
COPY --from=contracts /contracts /contracts
# Entrypoint скрипт лежит в исходниках boot, копируем отдельно
COPY components/boot/docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
WORKDIR /app
# Дефолтные env. CHAIN_URL переопределяется в docker-compose, остальное
# — sane defaults для bootstrap-сценария.
ENV CONTRACTS_DIR=/contracts \
CHAIN_URL=http://ke-node:8888 \
RPC_WAIT_TIMEOUT_MS=120000
LABEL org.opencontainers.image.title="coopenomics-bootstrap"
LABEL org.opencontainers.image.description="One-shot bootstrapper для KE-ноды: ждёт RPC, деплоит контракты и инициализирует протокол"
LABEL org.opencontainers.image.source="https://github.com/coopenomics/mono"
ENTRYPOINT ["/entrypoint.sh"]
CMD ["boot:remote"]
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
#
# Entrypoint для образа ke-bootstrap (`ghcr.io/coopenomics/bootstrap`).
#
# Один и тот же бинарь поддерживает несколько режимов через первый
# аргумент CMD (по умолчанию `boot:remote`). Все режимы — short-lived,
# контейнер выходит после завершения. Перезапускать его при сбое RPC не
# нужно: `boot:remote` сам ждёт готовности ноды по `CHAIN_URL`.
#
# Доступные режимы:
# boot:remote — деплой контрактов на уже работающую ноду (default)
# verify — sanity-check: вывести версию + список контрактов
# shell — открыть bash для отладки (только если контейнер
# запущен интерактивно)
#
# В CI (`build-bootstrap.yaml`) `verify` запускается после push'а как
# smoke-test, чтобы убедиться что образ хотя бы стартует.
set -euo pipefail
CMD="${1:-boot:remote}"
# Проверяем минимальный набор обязательных env'ов перед стартом — так
# падение происходит сразу с понятной ошибкой, а не где-то внутри boot
# при попытке подписать транзакцию пустым ключом.
require_env() {
local var
for var in "$@"; do
if [ -z "${!var:-}" ]; then
echo "ERROR: env $var is required for mode '$CMD'" >&2
exit 1
fi
done
}
case "$CMD" in
boot:remote)
require_env CHAIN_URL EOSIO_PUB_KEY EOSIO_PRV_KEY
echo "[ke-bootstrap] CHAIN_URL=$CHAIN_URL"
echo "[ke-bootstrap] CONTRACTS_DIR=${CONTRACTS_DIR:-/contracts}"
echo "[ke-bootstrap] starting boot:remote..."
# node dist/index.cjs — pnpm deploy укладывает unbuild-output сюда
exec node /app/dist/index.cjs boot:remote
;;
verify)
# Smoke-test для CI после push'а образа.
echo "[ke-bootstrap] node version:"; node --version
echo "[ke-bootstrap] dist/index.cjs:"; ls -la /app/dist/index.cjs || exit 1
echo "[ke-bootstrap] CONTRACTS_DIR contents:"; ls /contracts | head -30
echo "[ke-bootstrap] manifest:"
if [ -f /contracts/manifest.json ]; then
cat /contracts/manifest.json
else
echo "(missing)"
fi
echo "[ke-bootstrap] cli help (sanity-check, RPC недоступен — это ожидаемо):"
# `--help` не делает RPC-вызовов, только парсит commander
node /app/dist/index.cjs --help || true
;;
shell)
exec /bin/bash
;;
*)
# Любой другой аргумент пробрасываем дальше как arg для node (ESC-hatch
# для ad-hoc-сценариев типа `docker run ... cleos get info`).
exec node /app/dist/index.cjs "$@"
;;
esac
+2
View File
@@ -9,6 +9,7 @@
"license": "",
"keywords": [],
"scripts": {
"build": "unbuild",
"enter": "./scripts/enter.sh",
"cli": "esno src/index.ts",
"deploy": "esno src/index.ts deploy",
@@ -17,6 +18,7 @@
"boot": "esno src/index.ts boot",
"boot:clean": "esno src/index.ts boot:clean",
"boot:extra": "esno src/index.ts boot:extra",
"boot:remote": "esno src/index.ts boot:remote",
"reboot:clean": "cd components/boot/scripts && bash clean_reboot.sh",
"reboot:extra": "cd components/boot/scripts && bash extra_reboot.sh",
"create-coop": "esno src/index.ts create-coop",
+39 -21
View File
@@ -4,110 +4,128 @@ import { config } from 'dotenv'
config()
/**
* Резолвер пути до wasm/abi пары контракта.
*
* Layout:
* - **CONTRACTS_DIR не задан** (локальная сборка через `components/contracts/build*.sh`)
* — `eosio.*` лежат в `system/contracts/<name>/`, остальные в `<name>/`.
* - **CONTRACTS_DIR задан** (образ `dicoop/contracts`, mounted в bootstrap-контейнере)
* — flat layout: все контракты в `<CONTRACTS_DIR>/<name>/<name>.{wasm,abi}`,
* включая `eosio.*` (см. `.github/workflows/build-contracts.yaml` — system-контракты
* распакованы из `system/contracts/eosio.*` в плоский корень при упаковке).
*
* Это позволяет одному и тому же `boot:remote` работать и из локального workspace,
* и из bootstrap-образа, без дублирования конфигов.
*/
const CONTRACTS_DIR = process.env.CONTRACTS_DIR
const userBase = CONTRACTS_DIR ?? path.resolve(process.cwd(), '../contracts/build/contracts')
const systemBase = CONTRACTS_DIR ?? path.resolve(process.cwd(), '../contracts/build/contracts/system/contracts')
export default [
{
name: 'eosio.boot',
path: path.resolve(process.cwd(), '../contracts/build/contracts/system/contracts/eosio.boot'),
path: path.join(systemBase, 'eosio.boot'),
target: 'eosio',
},
{
name: 'eosio.system',
path: path.resolve(process.cwd(), '../contracts/build/contracts/system/contracts/eosio.system'),
path: path.join(systemBase, 'eosio.system'),
target: 'eosio',
},
{
name: 'eosio.token',
path: path.resolve(process.cwd(), '../contracts/build/contracts/system/contracts/eosio.token'),
path: path.join(systemBase, 'eosio.token'),
target: 'eosio.token',
},
{
name: 'eosio.msig',
path: path.resolve(process.cwd(), '../contracts/build/contracts/system/contracts/eosio.msig'),
path: path.join(systemBase, 'eosio.msig'),
target: 'eosio.msig',
},
{
name: 'eosio.wrap',
path: path.resolve(process.cwd(), '../contracts/build/contracts/system/contracts/eosio.wrap'),
path: path.join(systemBase, 'eosio.wrap'),
target: 'eosio.wrap',
},
{
name: 'registrator',
path: path.resolve(process.cwd(), '../contracts/build/contracts/registrator'),
path: path.join(userBase, 'registrator'),
target: 'registrator',
},
{
name: 'soviet',
path: path.resolve(process.cwd(), '../contracts/build/contracts/soviet'),
path: path.join(userBase, 'soviet'),
target: 'soviet',
},
{
name: 'marketplace',
path: path.resolve(process.cwd(), '../contracts/build/contracts/marketplace'),
path: path.join(userBase, 'marketplace'),
target: 'marketplace',
},
{
name: 'draft',
path: path.resolve(process.cwd(), '../contracts/build/contracts/draft'),
path: path.join(userBase, 'draft'),
target: 'draft',
},
{
name: 'branch',
path: path.resolve(process.cwd(), '../contracts/build/contracts/branch'),
path: path.join(userBase, 'branch'),
target: 'branch',
},
{
name: 'gateway',
path: path.resolve(process.cwd(), '../contracts/build/contracts/gateway'),
path: path.join(userBase, 'gateway'),
target: 'gateway',
},
{
name: 'fund',
path: path.resolve(process.cwd(), '../contracts/build/contracts/fund'),
path: path.join(userBase, 'fund'),
target: 'fund',
},
{
name: 'ledger',
path: path.resolve(process.cwd(), '../contracts/build/contracts/ledger'),
path: path.join(userBase, 'ledger'),
target: 'ledger',
},
{
name: 'ledger2',
path: path.resolve(process.cwd(), '../contracts/build/contracts/ledger2'),
path: path.join(userBase, 'ledger2'),
target: 'ledger2',
},
{
name: 'test',
path: path.resolve(process.cwd(), '../contracts/build/contracts/test'),
path: path.join(userBase, 'test'),
target: 'test',
},
{
name: 'contributor',
path: path.resolve(process.cwd(), '../contracts/build/contracts/contributor'),
path: path.join(userBase, 'contributor'),
target: 'contributor',
},
{
name: 'capital',
path: path.resolve(process.cwd(), '../contracts/build/contracts/capital'),
path: path.join(userBase, 'capital'),
target: 'capital',
},
{
name: 'wallet',
path: path.resolve(process.cwd(), '../contracts/build/contracts/wallet'),
path: path.join(userBase, 'wallet'),
target: 'wallet',
},
{
name: 'loan',
path: path.resolve(process.cwd(), '../contracts/build/contracts/loan'),
path: path.join(userBase, 'loan'),
target: 'loan',
},
{
name: 'meet',
path: path.resolve(process.cwd(), '../contracts/build/contracts/meet'),
path: path.join(userBase, 'meet'),
target: 'meet',
},
{
name: 'apps',
path: path.resolve(process.cwd(), '../contracts/build/contracts/apps'),
path: path.join(userBase, 'apps'),
target: 'apps',
},
]
+76
View File
@@ -286,6 +286,82 @@ program
}
})
/**
* Bootstrap режим для удалённой ноды (без управления docker-контейнером).
*
* Используется внутри образа `ghcr.io/coopenomics/bootstrap` (one-shot job
* в docker-compose), запускаемого рядом с уже работающим `ke-node`-сервисом.
* RPC URL берётся из `CHAIN_URL` (тот же конфиг, что у обычного `boot`),
* пути до wasm/abi — из `CONTRACTS_DIR` (см. `configs/contracts.ts`).
*
* Поведение:
* 1. Ждёт готовности RPC по `${CHAIN_URL}/v1/chain/get_info` (timeout
* `RPC_WAIT_TIMEOUT_MS`, default 120 c).
* 2. Запускает `startInfra()` — деплоит все контракты, активирует фичи,
* создаёт токен, инициализирует системные параметры.
* 3. Опционально — initial-data (`INSTALL_INITIAL_DATA=1`, требует MONGO/PG)
* и extra-data (`INSTALL_EXTRA_DATA=1`).
*
* Не пытается запускать/останавливать docker-контейнеры — нода поднимается
* отдельным сервисом docker-compose, bootstrap только катит на неё контракты.
*/
program
.command('boot:remote')
.description('Bootstrap protocol against an externally running KE node (no docker)')
.action(async () => {
const config = (await import('./configs')).default
const { startInfra, installInitialData, installExtraData } = await import('./init/infra')
const url = `${config.network.protocol}://${config.network.host}${config.network.port}`
const timeoutMs = Number(process.env.RPC_WAIT_TIMEOUT_MS ?? 120000)
console.log(`Waiting for RPC at ${url} (timeout ${timeoutMs}ms)...`)
const deadline = Date.now() + timeoutMs
let ready = false
while (Date.now() < deadline) {
try {
const r = await fetch(`${url}/v1/chain/get_info`, {
signal: AbortSignal.timeout(2000),
} as RequestInit)
if (r.ok) {
ready = true
break
}
}
catch {
// RPC ещё не готов — пробуем снова
}
await sleep(1000)
}
if (!ready) {
console.error(`RPC ${url} not ready within ${timeoutMs}ms`)
process.exit(1)
}
console.log(`RPC ready at ${url}`)
try {
const blockchain = await startInfra()
console.log('startInfra: done')
if (process.env.INSTALL_INITIAL_DATA === '1') {
console.log('installInitialData: start')
await installInitialData(blockchain, false)
}
if (process.env.INSTALL_EXTRA_DATA === '1') {
console.log('installInitialData (extended): start')
await installInitialData(blockchain, true)
await installExtraData(blockchain)
}
console.log('boot:remote completed')
process.exit(0)
}
catch (e) {
console.error('boot:remote failed:', e)
process.exit(1)
}
})
program.parse(process.argv) // Пуск парсинга аргументов
async function gracefulShutdown() {