vibed-infra 0.3.0 → 0.4.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,21 @@ 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
+ ## Tests
75
+
76
+ ```bash
77
+ npm test # package + dry-run all dist/install-*.sh
78
+ npm run test:dist # build images + e2e api, ui, nodes (sequential)
79
+ ./examples/vps-hello/test-dist.sh --profile api # single profile
80
+ ```
81
+
82
+ CI: `validate` job then parallel `dist-e2e` matrix (`api`, `ui`, `nodes`).
83
+
70
84
  ## Publishing (npm)
71
85
 
72
86
  Every **merge to `main`** runs [Publish npm](.github/workflows/publish.yml):
@@ -75,25 +89,10 @@ Every **merge to `main`** runs [Publish npm](.github/workflows/publish.yml):
75
89
  2. Commits `chore: release vibed-infra v…`, tags `v…`, pushes
76
90
  3. Runs `npm publish` (needs repo secret `NPM_TOKEN`)
77
91
 
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`).
92
+ **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
93
 
82
- Set repository secret **`NPM_TOKEN`** (npm automation token with publish access) before the first merge to `main`.
94
+ Set repository secret **`NPM_TOKEN`** before the first merge to `main`.
83
95
 
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
- ```
96
+ Ordinary PRs only run CI (`npm test` + dist e2e); they do **not** publish.
98
97
 
99
98
  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/package.py ADDED
@@ -0,0 +1,259 @@
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
+ """
54
+
55
+
56
+ def _env_ui(meta: dict, conf: dict, ui: dict) -> str:
57
+ au = conf["profiles"]["ui"]["autoUpdate"]
58
+ return f"""# Generated by vibed-infra package
59
+ DOCKER_NETWORK={meta['network']}
60
+ UI_NAME={meta['uiContainer']}
61
+ UI_IMAGE={conf['images']['ui']}
62
+ UI_MEMORY_LIMIT=32m
63
+ {au['flag']}=0
64
+ {au['intervalEnv']}=20
65
+ {au['stopTimeoutEnv']}=30
66
+ """
67
+
68
+
69
+ def _env_nodes(meta: dict, conf: dict, nodes: dict) -> str:
70
+ au = conf["profiles"]["nodes"]["autoUpdate"]
71
+ api_host = meta["apiContainer"]
72
+ return f"""# Generated by vibed-infra package
73
+ API_URL=http://{api_host}:{meta['apiPort']}
74
+ API_TOKEN=change-me-api-token
75
+ DOCKER_NETWORK={meta['network']}
76
+ WORKER_IMAGE={conf['images']['worker']}
77
+ WORKER_CONTAINER_NAME={meta['workerContainer']}
78
+ WORKER_ROLE=heartbeat
79
+ INTERVAL_SEC=60
80
+ WORKER_MEMORY_LIMIT=64m
81
+ {au['flag']}=0
82
+ {au['intervalEnv']}=30
83
+ {au['stopTimeoutEnv']}=30
84
+ """
85
+
86
+
87
+ def _env_gateway(meta: dict, conf: dict, sites: list) -> str:
88
+ au = conf["profiles"]["gateway"]["autoUpdate"]
89
+ host = sites[0]["host"] if sites else "example.com"
90
+ cert_dir = sites[0].get("tlsCertDir", f"/etc/letsencrypt/live/{host}") if sites else "/etc/letsencrypt/live/example.com"
91
+ flags = au.get("flags") or ["GATEWAY_AUTO_UPDATE"]
92
+ flag_lines = "\n".join(f"{f}=0" for f in flags)
93
+ return f"""# Generated by vibed-infra package
94
+ DOCKER_NETWORK={meta['network']}
95
+ GATEWAY_NAME={meta['gatewayContainer']}
96
+ NGINX_IMAGE={conf['images']['nginx']}
97
+ TLS_FULLCHAIN={cert_dir}/fullchain.pem
98
+ TLS_PRIVKEY={cert_dir}/privkey.pem
99
+ CERTBOT_WWW=./certbot-www
100
+ HTTP_PORT=80
101
+ HTTPS_PORT=443
102
+ GATEWAY_MEMORY_LIMIT=64m
103
+ {flag_lines}
104
+ {au.get('intervalEnv', 'GATEWAY_AUTO_UPDATE_INTERVAL_MIN')}=20
105
+ {au.get('stopTimeoutEnv', 'GATEWAY_STOP_TIMEOUT')}=30
106
+ """
107
+
108
+
109
+ def _compose_workers(meta: dict, conf: dict) -> str:
110
+ return f"""# Generated by vibed-infra package
111
+ networks:
112
+ edge:
113
+ external: true
114
+ name: ${{DOCKER_NETWORK:-{meta['network']}}}
115
+
116
+ services:
117
+ worker:
118
+ image: ${{WORKER_IMAGE:-{conf['images']['worker']}}}
119
+ container_name: ${{WORKER_CONTAINER_NAME:-{meta['workerContainer']}}}
120
+ restart: unless-stopped
121
+ mem_limit: ${{WORKER_MEMORY_LIMIT:-64m}}
122
+ env_file: .env
123
+ environment:
124
+ WORKER_ROLE: ${{WORKER_ROLE:-heartbeat}}
125
+ WORKER_CONFIG: /config/workers.yaml
126
+ API_URL: ${{API_URL:-http://{meta['apiContainer']}:{meta['apiPort']}}}
127
+ INTERVAL_SEC: ${{INTERVAL_SEC:-60}}
128
+ volumes:
129
+ - ./logs:/data/logs
130
+ - ./nodes-workers.yaml:/config/workers.yaml:ro
131
+ extra_hosts:
132
+ - "host.docker.internal:host-gateway"
133
+ networks:
134
+ - edge
135
+ """
136
+
137
+
138
+ def _install_wrapper(profile: str, packager_raw: str) -> str:
139
+ return f"""#!/usr/bin/env bash
140
+ # wget install — profile {profile}. Generated by vibed-infra package.
141
+ set -euo pipefail
142
+ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]:-$0}}")" && pwd)"
143
+ PACKAGER_RAW="${{PACKAGER_RAW:-{packager_raw}}}"
144
+ export PACKAGECONFIG_URL="${{PACKAGECONFIG_URL:-$SCRIPT_DIR/packageconfig.yaml}}"
145
+ export PRODUCT_RAW="${{PRODUCT_RAW:-$SCRIPT_DIR}}"
146
+ export INFRA_PROFILE={profile}
147
+ export INSTALL_DIR="${{INSTALL_DIR:-.}}"
148
+ if [[ "$PACKAGER_RAW" =~ ^/ ]]; then
149
+ exec bash "$PACKAGER_RAW/install.sh" --profile {profile}
150
+ fi
151
+ if command -v curl >/dev/null 2>&1; then
152
+ exec bash <(curl -fsSL "${{PACKAGER_RAW}}/install.sh") --profile {profile}
153
+ else
154
+ exec bash <(wget -qO- "${{PACKAGER_RAW}}/install.sh") --profile {profile}
155
+ fi
156
+ """
157
+
158
+
159
+ def build_dist(product_dir: Path, out_dir: Path, packager_root: Path) -> None:
160
+ product_dir = product_dir.resolve()
161
+ out_dir = out_dir.resolve()
162
+ packager_root = packager_root.resolve()
163
+
164
+ if out_dir.exists():
165
+ shutil.rmtree(out_dir)
166
+ out_dir.mkdir(parents=True)
167
+
168
+ product = load_product(product_dir)
169
+ tpl = product["templates_dir"]
170
+ raw_base = str(out_dir) # local dist path; operators override PRODUCT_RAW via wget URL
171
+
172
+ conf = compile_packageconfig(
173
+ product,
174
+ raw_base=raw_base,
175
+ packager_raw=str(packager_root),
176
+ )
177
+ meta = conf["_meta"]
178
+ comps = product["components"]
179
+ sites = conf["profiles"]["gateway"].get("sites") or []
180
+
181
+ # Copy source configs into dist
182
+ shutil.copy2(tpl / "vibed-infra-config.yml", out_dir / "vibed-infra-config.yml")
183
+ refs = product["infra"].get("templates") or {}
184
+ for dst, key in (
185
+ ("api-config.yaml", "api"),
186
+ ("ui-config.yaml", "ui"),
187
+ ("nodes-config.yaml", "nodes"),
188
+ ):
189
+ src_name = refs.get(key) or f"{key}-config.yaml"
190
+ shutil.copy2(tpl / src_name, out_dir / dst)
191
+
192
+ _write(out_dir / "packageconfig.yaml", dump_yaml(conf))
193
+
194
+ write_app_config(comps["api"], out_dir / "api-app.yaml")
195
+ write_app_config(comps["nodes"], out_dir / "nodes-workers.yaml")
196
+
197
+ _write(out_dir / ".env.api.example", _env_api(meta, conf, comps["api"]))
198
+ _write(out_dir / ".env.ui.example", _env_ui(meta, conf, comps["ui"]))
199
+ _write(out_dir / ".env.nodes.example", _env_nodes(meta, conf, comps["nodes"]))
200
+ _write(out_dir / ".env.gateway.example", _env_gateway(meta, conf, sites))
201
+ _write(out_dir / "docker-compose.workers.yml", _compose_workers(meta, conf))
202
+
203
+ gw_dir = out_dir / "gateway"
204
+ gw_dir.mkdir()
205
+ shutil.copy2(packager_root / "templates" / "generic" / "gateway" / "nginx.conf", gw_dir / "nginx.conf")
206
+
207
+ for script in (
208
+ "start-api.sh",
209
+ "start-ui.sh",
210
+ "start-nodes.sh",
211
+ "start-gateway.sh",
212
+ "update-api.sh",
213
+ "update-ui.sh",
214
+ "update-nodes.sh",
215
+ "update-gateway.sh",
216
+ "gen-dev-certs.sh",
217
+ ):
218
+ _copy_generic(packager_root, out_dir, script)
219
+
220
+ packager_raw_url = "https://raw.githubusercontent.com/naiemk/vibed-infra/main"
221
+ for profile in ("api", "ui", "nodes", "gateway"):
222
+ name = f"install-{profile}.sh"
223
+ _write(out_dir / name, _install_wrapper(profile, packager_raw_url), executable=True)
224
+
225
+ readme = f"""# dist — generated by vibed-infra package
226
+
227
+ Do not edit by hand. Run `../package.sh` from the product root and commit.
228
+
229
+ ## VPS install
230
+
231
+ ```bash
232
+ wget -qO- https://raw.githubusercontent.com/ORG/REPO/main/dist/install-api.sh | bash
233
+ wget -qO- https://raw.githubusercontent.com/ORG/REPO/main/dist/install-ui.sh | bash
234
+ wget -qO- https://raw.githubusercontent.com/ORG/REPO/main/dist/install-nodes.sh | bash
235
+ wget -qO- https://raw.githubusercontent.com/ORG/REPO/main/dist/install-gateway.sh | bash
236
+ ```
237
+
238
+ Network: `{meta['network']}`. API container: `{meta['apiContainer']}`.
239
+ """
240
+ _write(out_dir / "README.md", readme)
241
+ print(f"packaged: {out_dir}")
242
+
243
+
244
+ def main() -> int:
245
+ ap = argparse.ArgumentParser(description="Build product dist/ for VPS wget install")
246
+ ap.add_argument("--product", required=True, help="Product root (contains templates/)")
247
+ ap.add_argument("--out", required=True, help="Output dist directory")
248
+ ap.add_argument(
249
+ "--packager",
250
+ default=str(Path(__file__).resolve().parent.parent),
251
+ help="vibed-infra root",
252
+ )
253
+ args = ap.parse_args()
254
+ build_dist(Path(args.product), Path(args.out), Path(args.packager))
255
+ return 0
256
+
257
+
258
+ if __name__ == "__main__":
259
+ sys.exit(main())