vibed-infra 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,59 +1,63 @@
1
1
  # vibed-infra
2
2
 
3
- Product-agnostic VPS deployment packager: wget installer, Docker Compose layouts, digest-gated auto-update, TLS/nginx helpers, and CI workflow templates.
3
+ Product-agnostic VPS deployment packager: **`package.sh`** turns four YAML configs into a committed **`dist/`** tree operators wget on the box install scripts, generic start/update helpers, env examples, and gateway nginx.
4
4
 
5
5
  Published on npm as **`vibed-infra`**.
6
6
 
7
- ## Install (npm)
7
+ ## Product maintainer flow
8
8
 
9
- ```bash
10
- npm install vibed-infra
11
- # or in a monorepo sibling checkout:
12
- # "vibed-infra": "file:../vibed-infra"
13
- ```
14
-
15
- Resolve the packager root from Node:
9
+ Author **`templates/`** in your product repo:
16
10
 
17
- ```bash
18
- node -e "console.log(require('path').dirname(require.resolve('vibed-infra/package.json')))"
19
- ```
11
+ | File | Purpose |
12
+ |------|---------|
13
+ | `vibed-infra-config.yml` | Product name, template refs, edge network, gateway `sites[]`, auto-update |
14
+ | `api-config.yaml` | API image, port, opaque `config:` |
15
+ | `ui-config.yaml` | UI image, port |
16
+ | `nodes-config.yaml` | Worker image, opaque `config:` |
20
17
 
21
- Run install locally (product `packageconfig.yaml` + templates required):
18
+ Then package and commit **`dist/`**:
22
19
 
23
20
  ```bash
24
- PACKAGER_RAW=/path/to/node_modules/vibed-infra \
25
- PACKAGECONFIG_URL=/path/to/product/deploy/packageconfig.yaml \
26
- PRODUCT_RAW=/path/to/product/deploy/templates \
27
- bash /path/to/node_modules/vibed-infra/install.sh --profile api
21
+ ./package.sh # exec vibed-infra packager → writes dist/
22
+ git add dist && git commit && git push
28
23
  ```
29
24
 
30
- Or use the bin:
25
+ Resolve the packager from npm:
31
26
 
32
27
  ```bash
33
- npx vibed-infra --profile api # still needs PACKAGECONFIG_URL / PRODUCT_RAW (or packageconfig rawBase)
28
+ node -e "console.log(require('path').dirname(require.resolve('vibed-infra/package.json')))"
34
29
  ```
35
30
 
36
- ## Install (wget, operators)
31
+ Example product: [`examples/vps-hello`](examples/vps-hello) (`app/`, `build-images.sh`, four YAMLs, committed `dist/`).
32
+
33
+ ## Operator flow (wget on VPS)
37
34
 
38
35
  ```bash
39
- wget -qO- https://raw.githubusercontent.com/naiemk/vibed-infra/main/install.sh | \
40
- env INFRA_PROFILE=api \
41
- PACKAGECONFIG_URL=https://raw.githubusercontent.com/ORG/REPO/main/deploy/packageconfig.yaml \
42
- PRODUCT_RAW=https://raw.githubusercontent.com/ORG/REPO/main/deploy/templates \
43
- bash
36
+ wget -qO- https://raw.githubusercontent.com/ORG/REPO/main/dist/install-api.sh | bash
37
+ wget -qO- .../dist/install-nodes.sh | bash
38
+ wget -qO- .../dist/install-ui.sh | bash
39
+ wget -qO- .../dist/install-gateway.sh | bash
40
+ # edit each install dir .env, then ./start-*.sh
44
41
  ```
45
42
 
43
+ | Profile | Role |
44
+ |---------|------|
45
+ | `api` | Backend on shared edge network |
46
+ | `ui` | UI container (separate install) |
47
+ | `nodes` | Worker compose on edge network |
48
+ | `gateway` | HTTPS nginx only (API + UI must already run) |
49
+
46
50
  ## Layout
47
51
 
48
52
  | Path | Purpose |
49
53
  |------|---------|
54
+ | `package.sh` / `lib/package.py` | Build product `dist/` from four YAMLs + generic templates |
50
55
  | `install.sh` | Single entrypoint (`INFRA_PROFILE` or `--profile`) |
51
- | `start.sh` / `update.sh` | Generic lifecycle in install dir |
52
56
  | `install-auto-update.sh` | Cron from profile flags |
53
- | `lib/` | fetch, env, tls, prompt, generate |
54
- | `templates/` | Generic compose/nginx skeletons |
55
- | `schema/packageconfig.md` | Schema reference |
56
- | `examples/vps-hello/` | Full product that installs API + worker + HTTPS gateway on a VPS |
57
+ | `lib/` | fetch, env, tls, prompt, generate, product_config |
58
+ | `templates/generic/` | Generic start/update/env/certs/gateway |
59
+ | `schema/packageconfig.md` | Four-file product schema + compiled packageconfig |
60
+ | `examples/vps-hello/` | Full example with dist e2e tests |
57
61
  | `skills/` | Cursor skills |
58
62
  | `github/workflows/` | Reusable GHCR build workflow |
59
63
 
@@ -62,11 +66,23 @@ wget -qO- https://raw.githubusercontent.com/naiemk/vibed-infra/main/install.sh |
62
66
  | Variable | Meaning |
63
67
  |----------|---------|
64
68
  | `PACKAGER_RAW` | Base URL or local path to this package root |
65
- | `PACKAGECONFIG_URL` | Product `packageconfig.yaml` (URL or path) |
66
- | `PRODUCT_RAW` | Product templates base (overrides `rawBase`; opaque to infra) |
67
- | `INFRA_PROFILE` | `api`, `nodes`, or `gateway` |
69
+ | `PACKAGECONFIG_URL` | Product `packageconfig.yaml` or `dist/` URL (URL or path) |
70
+ | `PRODUCT_RAW` | Product `dist/` base (templates + install scripts) |
71
+ | `INFRA_PROFILE` | `api`, `ui`, `nodes`, or `gateway` |
68
72
  | `INSTALL_DIR` | Target directory (default `.`) |
69
73
 
74
+ Auto-update cron runs profile `update-*.sh`. Each update **prunes dangling Docker images** by default (`DOCKER_AUTO_PRUNE=1`). Set `DOCKER_PRUNE_UNUSED=1` to also drop unused tagged images older than `DOCKER_PRUNE_UNTIL` (default `72h`).
75
+
76
+ ## Tests
77
+
78
+ ```bash
79
+ npm test # package + dry-run all dist/install-*.sh
80
+ npm run test:dist # build images + e2e api, ui, nodes (sequential)
81
+ ./examples/vps-hello/test-dist.sh --profile api # single profile
82
+ ```
83
+
84
+ CI: `validate` job then parallel `dist-e2e` matrix (`api`, `ui`, `nodes`).
85
+
70
86
  ## Publishing (npm)
71
87
 
72
88
  Every **merge to `main`** runs [Publish npm](.github/workflows/publish.yml):
@@ -75,25 +91,10 @@ Every **merge to `main`** runs [Publish npm](.github/workflows/publish.yml):
75
91
  2. Commits `chore: release vibed-infra v…`, tags `v…`, pushes
76
92
  3. Runs `npm publish` (needs repo secret `NPM_TOKEN`)
77
93
 
78
- **Major versions** — set `version` in `package.json` yourself (e.g. `1.0.0`) and include **`[major]`** in the commit message (or PR merge commit). That publishes the pinned version with **no** automatic minor bump.
79
-
80
- Manual run: Actions → Publish npm → `workflow_dispatch` (`minor` / `major` / `none`).
94
+ **Major versions** — set `version` in `package.json` yourself (e.g. `1.0.0`) and include **`[major]`** in the commit message. That publishes the pinned version with **no** automatic minor bump.
81
95
 
82
- Set repository secret **`NPM_TOKEN`** (npm automation token with publish access) before the first merge to `main`.
96
+ Set repository secret **`NPM_TOKEN`** before the first merge to `main`.
83
97
 
84
- Ordinary PRs only run CI (`npm test`); they do **not** publish.
85
-
86
- ## Example (VPS)
87
-
88
- [`examples/vps-hello`](examples/vps-hello) is a complete product that uses this packager: `packageconfig.yaml`, install wrappers, start/update scripts, and a tiny API/UI/worker you can build on the box.
89
-
90
- ```bash
91
- git clone https://github.com/naiemk/vibed-infra.git
92
- cd vibed-infra
93
- ./examples/vps-hello/scripts/build-images.sh
94
- mkdir -p ~/hello-vps/api && cd ~/hello-vps/api
95
- bash /path/to/vibed-infra/examples/vps-hello/install/install-api.sh
96
- # edit .env, then ./start-hello-api.sh
97
- ```
98
+ Ordinary PRs only run CI (`npm test` + dist e2e); they do **not** publish.
98
99
 
99
100
  Runbook: [`examples/vps-hello/README.md`](examples/vps-hello/README.md).
@@ -19,14 +19,17 @@ fi
19
19
  if [[ -z "$ROLE" && -n "$PROFILE" ]]; then
20
20
  case "$PROFILE" in
21
21
  api) ROLE=backend ;;
22
+ ui) ROLE=ui ;;
22
23
  nodes) ROLE=workers ;;
23
24
  gateway) ROLE=gateway ;;
24
25
  esac
25
26
  fi
26
27
  if [[ -z "$ROLE" ]]; then
27
- if compgen -G "start-*-gateway.sh" >/dev/null || [[ -f docker-compose.gateway.yml ]]; then
28
+ if [[ -f start-gateway.sh ]] || compgen -G "start-*-gateway.sh" >/dev/null || [[ -f docker-compose.gateway.yml ]]; then
28
29
  ROLE=gateway
29
- elif compgen -G "start-*-nodes.sh" >/dev/null || [[ -f docker-compose.workers.yml ]]; then
30
+ elif [[ -f start-ui.sh ]] || compgen -G "start-*-ui.sh" >/dev/null; then
31
+ ROLE=ui
32
+ elif [[ -f start-nodes.sh ]] || compgen -G "start-*-nodes.sh" >/dev/null || [[ -f docker-compose.workers.yml ]]; then
30
33
  ROLE=workers
31
34
  else
32
35
  ROLE=backend
@@ -47,6 +50,11 @@ case "$ROLE" in
47
50
  INTERVAL_MIN="${API_AUTO_UPDATE_INTERVAL_MIN:-30}"
48
51
  CRON_OFFSET=0
49
52
  ;;
53
+ ui)
54
+ if role_auto_update_on UI_AUTO_UPDATE; then cron_enabled=1; fi
55
+ INTERVAL_MIN="${UI_AUTO_UPDATE_INTERVAL_MIN:-20}"
56
+ CRON_OFFSET=15
57
+ ;;
50
58
  workers|nodes)
51
59
  ROLE=nodes
52
60
  if role_auto_update_on NODES_AUTO_UPDATE; then cron_enabled=1; fi
package/install.sh CHANGED
@@ -18,6 +18,7 @@ while [[ $# -gt 0 ]]; do
18
18
  for c in "${_comps[@]}"; do
19
19
  case "$c" in
20
20
  app|backend|api) PROFILE=api; break ;;
21
+ ui) PROFILE=ui; break ;;
21
22
  workers|nodes) PROFILE=nodes; break ;;
22
23
  gateway) PROFILE=gateway; break ;;
23
24
  esac
@@ -26,7 +27,7 @@ while [[ $# -gt 0 ]]; do
26
27
  ;;
27
28
  --dest) DEST="$2"; shift 2 ;;
28
29
  -h|--help)
29
- echo "usage: install.sh [--profile api|nodes|gateway] [--dest DIR]"
30
+ echo "usage: install.sh [--profile api|ui|nodes|gateway] [--dest DIR]"
30
31
  echo "env: PACKAGECONFIG_URL PRODUCT_RAW PACKAGER_RAW INFRA_PROFILE INSTALL_DIR"
31
32
  exit 0
32
33
  ;;
@@ -160,16 +161,24 @@ if [[ -n "$COMPOSE_MAINNET" ]]; then
160
161
  infra_write_template "${PRODUCT_RAW}/${COMPOSE_MAINNET}" "$DEST/${COMPOSE_MAINNET}" 0
161
162
  fi
162
163
 
163
- # Generic infra compose templates (optional USE_COMPOSE=1)
164
+ # Generic infra compose templates only when product did not supply compose in packageconfig.
164
165
  case "$ROLE" in
165
166
  backend)
166
- infra_write_template "${PACKAGER_RAW}/templates/docker-compose.backend.yml" "$DEST/docker-compose.backend.yml" 0
167
+ if [[ -z "$COMPOSE_TPL" ]]; then
168
+ infra_write_template "${PACKAGER_RAW}/templates/docker-compose.backend.yml" "$DEST/docker-compose.backend.yml" 0
169
+ fi
170
+ ;;
171
+ ui)
167
172
  ;;
168
173
  workers)
169
- infra_write_template "${PACKAGER_RAW}/templates/docker-compose.workers.yml" "$DEST/docker-compose.workers.yml" 0
174
+ if [[ -z "$COMPOSE_TPL" ]]; then
175
+ infra_write_template "${PACKAGER_RAW}/templates/docker-compose.workers.yml" "$DEST/docker-compose.workers.yml" 0
176
+ fi
170
177
  ;;
171
178
  gateway)
172
- infra_write_template "${PACKAGER_RAW}/templates/docker-compose.gateway.yml" "$DEST/docker-compose.gateway.yml" 0
179
+ if [[ -z "$COMPOSE_MAINNET" && -z "$COMPOSE_TPL" ]]; then
180
+ infra_write_template "${PACKAGER_RAW}/templates/docker-compose.gateway.yml" "$DEST/docker-compose.gateway.yml" 0
181
+ fi
173
182
  ;;
174
183
  esac
175
184
 
package/lib/env.sh CHANGED
@@ -222,3 +222,32 @@ chown_data_dir() {
222
222
  fi
223
223
  chmod 755 "$dir" 2>/dev/null || true
224
224
  }
225
+
226
+ # Default on during auto-update. Set DOCKER_AUTO_PRUNE=0 to disable.
227
+ docker_auto_prune_on() {
228
+ if [[ -n "${DOCKER_AUTO_PRUNE+x}" ]]; then
229
+ env_flag_on DOCKER_AUTO_PRUNE
230
+ return
231
+ fi
232
+ return 0
233
+ }
234
+
235
+ # Drop dangling images left after retagged pulls (e.g. :main). Optionally also
236
+ # remove unused tagged images older than DOCKER_PRUNE_UNTIL when DOCKER_PRUNE_UNUSED=1.
237
+ prune_docker_images() {
238
+ local log_dir="${1:-.}"
239
+ local role_label="${2:-infra}"
240
+ docker_auto_prune_on || return 0
241
+ if ! command -v docker >/dev/null 2>&1; then
242
+ return 0
243
+ fi
244
+ local summary
245
+ summary="$(docker image prune -f 2>/dev/null | awk '/Total reclaimed space/{print; found=1} END{if(!found) print "done"}' || echo done)"
246
+ log_update "$log_dir" "${role_label}: prune dangling images — ${summary}"
247
+ if env_flag_on DOCKER_PRUNE_UNUSED; then
248
+ local until="${DOCKER_PRUNE_UNTIL:-72h}"
249
+ summary="$(docker image prune -af --filter "until=${until}" 2>/dev/null \
250
+ | awk '/Total reclaimed space/{print; found=1} END{if(!found) print "done"}' || echo done)"
251
+ log_update "$log_dir" "${role_label}: prune unused images (until=${until}) — ${summary}"
252
+ fi
253
+ }
package/lib/package.py ADDED
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env python3
2
+ """Build product dist/ from templates/*.yaml — operators wget install-*.sh from dist."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import shutil
7
+ import stat
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
12
+ from product_config import ( # noqa: E402
13
+ compile_packageconfig,
14
+ dump_yaml,
15
+ load_product,
16
+ write_app_config,
17
+ )
18
+
19
+
20
+ def _chmod_x(path: Path) -> None:
21
+ path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
22
+
23
+
24
+ def _write(path: Path, text: str, executable: bool = False) -> None:
25
+ path.parent.mkdir(parents=True, exist_ok=True)
26
+ path.write_text(text, encoding="utf-8")
27
+ if executable:
28
+ _chmod_x(path)
29
+
30
+
31
+ def _copy_generic(packager: Path, out: Path, name: str) -> None:
32
+ src = packager / "templates" / "generic" / name
33
+ if not src.is_file():
34
+ raise FileNotFoundError(f"missing generic template {src}")
35
+ shutil.copy2(src, out / name)
36
+ _chmod_x(out / name)
37
+
38
+
39
+ def _env_api(meta: dict, conf: dict, api: dict) -> str:
40
+ au = conf["profiles"]["api"]["autoUpdate"]
41
+ return f"""# Generated by vibed-infra package — edit .env on the VPS, not in git dist.
42
+ API_TOKEN=change-me-api-token
43
+ DOCKER_NETWORK={meta['network']}
44
+ DOCKER_NAME={meta['apiContainer']}
45
+ BACKEND_IMAGE={conf['images']['backend']}
46
+ HOST_PORT={meta['apiPort']}
47
+ DATA_DIR=./data
48
+ CONFIG_FILE=api-app.yaml
49
+ API_MEMORY_LIMIT=256m
50
+ {au['flag']}=0
51
+ {au['intervalEnv']}=30
52
+ {au['stopTimeoutEnv']}=60
53
+ DOCKER_AUTO_PRUNE=1
54
+ DOCKER_PRUNE_UNUSED=0
55
+ DOCKER_PRUNE_UNTIL=72h
56
+ """
57
+
58
+
59
+ def _env_ui(meta: dict, conf: dict, ui: dict) -> str:
60
+ au = conf["profiles"]["ui"]["autoUpdate"]
61
+ return f"""# Generated by vibed-infra package
62
+ DOCKER_NETWORK={meta['network']}
63
+ UI_NAME={meta['uiContainer']}
64
+ UI_IMAGE={conf['images']['ui']}
65
+ UI_MEMORY_LIMIT=32m
66
+ {au['flag']}=0
67
+ {au['intervalEnv']}=20
68
+ {au['stopTimeoutEnv']}=30
69
+ DOCKER_AUTO_PRUNE=1
70
+ DOCKER_PRUNE_UNUSED=0
71
+ DOCKER_PRUNE_UNTIL=72h
72
+ """
73
+
74
+
75
+ def _env_nodes(meta: dict, conf: dict, nodes: dict) -> str:
76
+ au = conf["profiles"]["nodes"]["autoUpdate"]
77
+ api_host = meta["apiContainer"]
78
+ return f"""# Generated by vibed-infra package
79
+ API_URL=http://{api_host}:{meta['apiPort']}
80
+ API_TOKEN=change-me-api-token
81
+ DOCKER_NETWORK={meta['network']}
82
+ WORKER_IMAGE={conf['images']['worker']}
83
+ WORKER_CONTAINER_NAME={meta['workerContainer']}
84
+ WORKER_ROLE=heartbeat
85
+ INTERVAL_SEC=60
86
+ WORKER_MEMORY_LIMIT=64m
87
+ {au['flag']}=0
88
+ {au['intervalEnv']}=30
89
+ {au['stopTimeoutEnv']}=30
90
+ DOCKER_AUTO_PRUNE=1
91
+ DOCKER_PRUNE_UNUSED=0
92
+ DOCKER_PRUNE_UNTIL=72h
93
+ """
94
+
95
+
96
+ def _env_gateway(meta: dict, conf: dict, sites: list) -> str:
97
+ au = conf["profiles"]["gateway"]["autoUpdate"]
98
+ host = sites[0]["host"] if sites else "example.com"
99
+ cert_dir = sites[0].get("tlsCertDir", f"/etc/letsencrypt/live/{host}") if sites else "/etc/letsencrypt/live/example.com"
100
+ flags = au.get("flags") or ["GATEWAY_AUTO_UPDATE"]
101
+ flag_lines = "\n".join(f"{f}=0" for f in flags)
102
+ return f"""# Generated by vibed-infra package
103
+ DOCKER_NETWORK={meta['network']}
104
+ GATEWAY_NAME={meta['gatewayContainer']}
105
+ NGINX_IMAGE={conf['images']['nginx']}
106
+ TLS_FULLCHAIN={cert_dir}/fullchain.pem
107
+ TLS_PRIVKEY={cert_dir}/privkey.pem
108
+ CERTBOT_WWW=./certbot-www
109
+ HTTP_PORT=80
110
+ HTTPS_PORT=443
111
+ GATEWAY_MEMORY_LIMIT=64m
112
+ {flag_lines}
113
+ {au.get('intervalEnv', 'GATEWAY_AUTO_UPDATE_INTERVAL_MIN')}=20
114
+ {au.get('stopTimeoutEnv', 'GATEWAY_STOP_TIMEOUT')}=30
115
+ DOCKER_AUTO_PRUNE=1
116
+ DOCKER_PRUNE_UNUSED=0
117
+ DOCKER_PRUNE_UNTIL=72h
118
+ """
119
+
120
+
121
+ def _compose_workers(meta: dict, conf: dict) -> str:
122
+ return f"""# Generated by vibed-infra package
123
+ networks:
124
+ edge:
125
+ external: true
126
+ name: ${{DOCKER_NETWORK:-{meta['network']}}}
127
+
128
+ services:
129
+ worker:
130
+ image: ${{WORKER_IMAGE:-{conf['images']['worker']}}}
131
+ container_name: ${{WORKER_CONTAINER_NAME:-{meta['workerContainer']}}}
132
+ restart: unless-stopped
133
+ mem_limit: ${{WORKER_MEMORY_LIMIT:-64m}}
134
+ env_file: .env
135
+ environment:
136
+ WORKER_ROLE: ${{WORKER_ROLE:-heartbeat}}
137
+ WORKER_CONFIG: /config/workers.yaml
138
+ API_URL: ${{API_URL:-http://{meta['apiContainer']}:{meta['apiPort']}}}
139
+ INTERVAL_SEC: ${{INTERVAL_SEC:-60}}
140
+ volumes:
141
+ - ./logs:/data/logs
142
+ - ./nodes-workers.yaml:/config/workers.yaml:ro
143
+ extra_hosts:
144
+ - "host.docker.internal:host-gateway"
145
+ networks:
146
+ - edge
147
+ """
148
+
149
+
150
+ def _install_wrapper(profile: str, packager_raw: str) -> str:
151
+ return f"""#!/usr/bin/env bash
152
+ # wget install — profile {profile}. Generated by vibed-infra package.
153
+ set -euo pipefail
154
+ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]:-$0}}")" && pwd)"
155
+ PACKAGER_RAW="${{PACKAGER_RAW:-{packager_raw}}}"
156
+ export PACKAGECONFIG_URL="${{PACKAGECONFIG_URL:-$SCRIPT_DIR/packageconfig.yaml}}"
157
+ export PRODUCT_RAW="${{PRODUCT_RAW:-$SCRIPT_DIR}}"
158
+ export INFRA_PROFILE={profile}
159
+ export INSTALL_DIR="${{INSTALL_DIR:-.}}"
160
+ if [[ "$PACKAGER_RAW" =~ ^/ ]]; then
161
+ exec bash "$PACKAGER_RAW/install.sh" --profile {profile}
162
+ fi
163
+ if command -v curl >/dev/null 2>&1; then
164
+ exec bash <(curl -fsSL "${{PACKAGER_RAW}}/install.sh") --profile {profile}
165
+ else
166
+ exec bash <(wget -qO- "${{PACKAGER_RAW}}/install.sh") --profile {profile}
167
+ fi
168
+ """
169
+
170
+
171
+ def build_dist(product_dir: Path, out_dir: Path, packager_root: Path) -> None:
172
+ product_dir = product_dir.resolve()
173
+ out_dir = out_dir.resolve()
174
+ packager_root = packager_root.resolve()
175
+
176
+ if out_dir.exists():
177
+ shutil.rmtree(out_dir)
178
+ out_dir.mkdir(parents=True)
179
+
180
+ product = load_product(product_dir)
181
+ tpl = product["templates_dir"]
182
+ raw_base = str(out_dir) # local dist path; operators override PRODUCT_RAW via wget URL
183
+
184
+ conf = compile_packageconfig(
185
+ product,
186
+ raw_base=raw_base,
187
+ packager_raw=str(packager_root),
188
+ )
189
+ meta = conf["_meta"]
190
+ comps = product["components"]
191
+ sites = conf["profiles"]["gateway"].get("sites") or []
192
+
193
+ # Copy source configs into dist
194
+ shutil.copy2(tpl / "vibed-infra-config.yml", out_dir / "vibed-infra-config.yml")
195
+ refs = product["infra"].get("templates") or {}
196
+ for dst, key in (
197
+ ("api-config.yaml", "api"),
198
+ ("ui-config.yaml", "ui"),
199
+ ("nodes-config.yaml", "nodes"),
200
+ ):
201
+ src_name = refs.get(key) or f"{key}-config.yaml"
202
+ shutil.copy2(tpl / src_name, out_dir / dst)
203
+
204
+ _write(out_dir / "packageconfig.yaml", dump_yaml(conf))
205
+
206
+ write_app_config(comps["api"], out_dir / "api-app.yaml")
207
+ write_app_config(comps["nodes"], out_dir / "nodes-workers.yaml")
208
+
209
+ _write(out_dir / ".env.api.example", _env_api(meta, conf, comps["api"]))
210
+ _write(out_dir / ".env.ui.example", _env_ui(meta, conf, comps["ui"]))
211
+ _write(out_dir / ".env.nodes.example", _env_nodes(meta, conf, comps["nodes"]))
212
+ _write(out_dir / ".env.gateway.example", _env_gateway(meta, conf, sites))
213
+ _write(out_dir / "docker-compose.workers.yml", _compose_workers(meta, conf))
214
+
215
+ gw_dir = out_dir / "gateway"
216
+ gw_dir.mkdir()
217
+ shutil.copy2(packager_root / "templates" / "generic" / "gateway" / "nginx.conf", gw_dir / "nginx.conf")
218
+
219
+ for script in (
220
+ "start-api.sh",
221
+ "start-ui.sh",
222
+ "start-nodes.sh",
223
+ "start-gateway.sh",
224
+ "update-api.sh",
225
+ "update-ui.sh",
226
+ "update-nodes.sh",
227
+ "update-gateway.sh",
228
+ "gen-dev-certs.sh",
229
+ ):
230
+ _copy_generic(packager_root, out_dir, script)
231
+
232
+ packager_raw_url = "https://raw.githubusercontent.com/naiemk/vibed-infra/main"
233
+ for profile in ("api", "ui", "nodes", "gateway"):
234
+ name = f"install-{profile}.sh"
235
+ _write(out_dir / name, _install_wrapper(profile, packager_raw_url), executable=True)
236
+
237
+ readme = f"""# dist — generated by vibed-infra package
238
+
239
+ Do not edit by hand. Run `../package.sh` from the product root and commit.
240
+
241
+ ## VPS install
242
+
243
+ ```bash
244
+ wget -qO- https://raw.githubusercontent.com/ORG/REPO/main/dist/install-api.sh | bash
245
+ wget -qO- https://raw.githubusercontent.com/ORG/REPO/main/dist/install-ui.sh | bash
246
+ wget -qO- https://raw.githubusercontent.com/ORG/REPO/main/dist/install-nodes.sh | bash
247
+ wget -qO- https://raw.githubusercontent.com/ORG/REPO/main/dist/install-gateway.sh | bash
248
+ ```
249
+
250
+ Network: `{meta['network']}`. API container: `{meta['apiContainer']}`.
251
+ """
252
+ _write(out_dir / "README.md", readme)
253
+ print(f"packaged: {out_dir}")
254
+
255
+
256
+ def main() -> int:
257
+ ap = argparse.ArgumentParser(description="Build product dist/ for VPS wget install")
258
+ ap.add_argument("--product", required=True, help="Product root (contains templates/)")
259
+ ap.add_argument("--out", required=True, help="Output dist directory")
260
+ ap.add_argument(
261
+ "--packager",
262
+ default=str(Path(__file__).resolve().parent.parent),
263
+ help="vibed-infra root",
264
+ )
265
+ args = ap.parse_args()
266
+ build_dist(Path(args.product), Path(args.out), Path(args.packager))
267
+ return 0
268
+
269
+
270
+ if __name__ == "__main__":
271
+ sys.exit(main())