vibed-infra 0.3.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 ADDED
@@ -0,0 +1,99 @@
1
+ # vibed-infra
2
+
3
+ Product-agnostic VPS deployment packager: wget installer, Docker Compose layouts, digest-gated auto-update, TLS/nginx helpers, and CI workflow templates.
4
+
5
+ Published on npm as **`vibed-infra`**.
6
+
7
+ ## Install (npm)
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:
16
+
17
+ ```bash
18
+ node -e "console.log(require('path').dirname(require.resolve('vibed-infra/package.json')))"
19
+ ```
20
+
21
+ Run install locally (product `packageconfig.yaml` + templates required):
22
+
23
+ ```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
28
+ ```
29
+
30
+ Or use the bin:
31
+
32
+ ```bash
33
+ npx vibed-infra --profile api # still needs PACKAGECONFIG_URL / PRODUCT_RAW (or packageconfig rawBase)
34
+ ```
35
+
36
+ ## Install (wget, operators)
37
+
38
+ ```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
44
+ ```
45
+
46
+ ## Layout
47
+
48
+ | Path | Purpose |
49
+ |------|---------|
50
+ | `install.sh` | Single entrypoint (`INFRA_PROFILE` or `--profile`) |
51
+ | `start.sh` / `update.sh` | Generic lifecycle in install dir |
52
+ | `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
+ | `skills/` | Cursor skills |
58
+ | `github/workflows/` | Reusable GHCR build workflow |
59
+
60
+ ## Environment
61
+
62
+ | Variable | Meaning |
63
+ |----------|---------|
64
+ | `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` |
68
+ | `INSTALL_DIR` | Target directory (default `.`) |
69
+
70
+ ## Publishing (npm)
71
+
72
+ Every **merge to `main`** runs [Publish npm](.github/workflows/publish.yml):
73
+
74
+ 1. Bumps **minor** (`0.1.0` → `0.2.0`)
75
+ 2. Commits `chore: release vibed-infra v…`, tags `v…`, pushes
76
+ 3. Runs `npm publish` (needs repo secret `NPM_TOKEN`)
77
+
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`).
81
+
82
+ Set repository secret **`NPM_TOKEN`** (npm automation token with publish access) before the first merge to `main`.
83
+
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
+
99
+ Runbook: [`examples/vps-hello/README.md`](examples/vps-hello/README.md).
@@ -0,0 +1,61 @@
1
+ # Reusable GHCR docker build — one image per workflow_call.
2
+ #
3
+ # Caller (repeat per image or use a matrix in the caller workflow):
4
+ #
5
+ # jobs:
6
+ # build-api:
7
+ # uses: ./infra/github/workflows/docker-build-reusable.yml
8
+ # with:
9
+ # dockerfile: deploy/Dockerfile.api
10
+ # image: ghcr.io/${{ github.repository_owner }}/my-api
11
+ # context: .
12
+ # secrets: inherit
13
+
14
+ name: Reusable docker build (single image)
15
+
16
+ on:
17
+ workflow_call:
18
+ inputs:
19
+ dockerfile:
20
+ required: true
21
+ type: string
22
+ image:
23
+ required: true
24
+ type: string
25
+ context:
26
+ required: false
27
+ type: string
28
+ default: .
29
+
30
+ permissions:
31
+ contents: read
32
+ packages: write
33
+
34
+ jobs:
35
+ build-push:
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+ - uses: docker/setup-buildx-action@v3
40
+ - uses: docker/login-action@v3
41
+ with:
42
+ registry: ghcr.io
43
+ username: ${{ github.actor }}
44
+ password: ${{ secrets.GITHUB_TOKEN }}
45
+ - uses: docker/metadata-action@v5
46
+ id: meta
47
+ with:
48
+ images: ${{ inputs.image }}
49
+ tags: |
50
+ type=raw,value=main,enable={{is_default_branch}}
51
+ type=sha,prefix=main-,format=short
52
+ type=semver,pattern={{version}}
53
+ - uses: docker/build-push-action@v6
54
+ with:
55
+ context: ${{ inputs.context }}
56
+ file: ${{ inputs.dockerfile }}
57
+ push: true
58
+ tags: ${{ steps.meta.outputs.tags }}
59
+ labels: |
60
+ org.opencontainers.image.revision=${{ github.sha }}
61
+ org.opencontainers.image.source=https://github.com/${{ github.repository }}
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env bash
2
+ # Install cron for profile auto-update flags (.infra-profile + lib-env.sh).
3
+ set -euo pipefail
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ cd "$SCRIPT_DIR"
6
+ # shellcheck source=lib-env.sh
7
+ source "$SCRIPT_DIR/lib-env.sh"
8
+ load_dotenv .env
9
+
10
+ PROFILE="${INFRA_PROFILE:-}"
11
+ ROLE="${ROLE:-}"
12
+ START_SCRIPT=""
13
+ UPDATE_SCRIPT=""
14
+ if [[ -f .infra-profile ]]; then
15
+ # shellcheck source=/dev/null
16
+ source .infra-profile
17
+ fi
18
+
19
+ if [[ -z "$ROLE" && -n "$PROFILE" ]]; then
20
+ case "$PROFILE" in
21
+ api) ROLE=backend ;;
22
+ nodes) ROLE=workers ;;
23
+ gateway) ROLE=gateway ;;
24
+ esac
25
+ fi
26
+ if [[ -z "$ROLE" ]]; then
27
+ if compgen -G "start-*-gateway.sh" >/dev/null || [[ -f docker-compose.gateway.yml ]]; then
28
+ ROLE=gateway
29
+ elif compgen -G "start-*-nodes.sh" >/dev/null || [[ -f docker-compose.workers.yml ]]; then
30
+ ROLE=workers
31
+ else
32
+ ROLE=backend
33
+ fi
34
+ fi
35
+
36
+ UPDATE_SCRIPT="${UPDATE_SCRIPT:-update.sh}"
37
+ [[ -x "$SCRIPT_DIR/$UPDATE_SCRIPT" ]] || UPDATE_SCRIPT="update.sh"
38
+
39
+ cron_enabled=0
40
+ INTERVAL_MIN=30
41
+ CRON_OFFSET=0
42
+
43
+ case "$ROLE" in
44
+ backend|api)
45
+ ROLE=api
46
+ if role_auto_update_on API_AUTO_UPDATE; then cron_enabled=1; fi
47
+ INTERVAL_MIN="${API_AUTO_UPDATE_INTERVAL_MIN:-30}"
48
+ CRON_OFFSET=0
49
+ ;;
50
+ workers|nodes)
51
+ ROLE=nodes
52
+ if role_auto_update_on NODES_AUTO_UPDATE; then cron_enabled=1; fi
53
+ INTERVAL_MIN="${NODES_AUTO_UPDATE_INTERVAL_MIN:-30}"
54
+ CRON_OFFSET=10
55
+ ;;
56
+ gateway)
57
+ if role_auto_update_on UI_TESTNET_AUTO_UPDATE \
58
+ || role_auto_update_on UI_MAINNET_AUTO_UPDATE \
59
+ || role_auto_update_on GATEWAY_AUTO_UPDATE \
60
+ || role_auto_update_on UI_AUTO_UPDATE; then
61
+ cron_enabled=1
62
+ fi
63
+ INTERVAL_MIN="${GATEWAY_AUTO_UPDATE_INTERVAL_MIN:-20}"
64
+ CRON_OFFSET=20
65
+ ;;
66
+ *)
67
+ ROLE="${ROLE:-api}"
68
+ ;;
69
+ esac
70
+
71
+ MARKER="# infra-auto-update:${ROLE}:${SCRIPT_DIR}"
72
+ build_cron_minutes() {
73
+ local cron_offset="${1:-0}"
74
+ local cron_interval="${2:-30}"
75
+ local m="$cron_offset" parts=()
76
+ while [[ "$m" -lt 60 ]]; do parts+=("$m"); m=$((m + cron_interval)); done
77
+ [[ "${#parts[@]}" -eq 0 ]] && parts=(0)
78
+ local IFS=,
79
+ echo "${parts[*]}"
80
+ }
81
+ CRON_MINUTES="$(build_cron_minutes "${CRON_OFFSET:-0}" "${INTERVAL_MIN:-30}")"
82
+ CRON_SCHED="${CRON_MINUTES} * * * *"
83
+ CRON_CMD="cd ${SCRIPT_DIR} && /bin/bash ${SCRIPT_DIR}/${UPDATE_SCRIPT} >/dev/null 2>&1"
84
+ dir_slug="$(basename "$SCRIPT_DIR" | tr -c 'A-Za-z0-9._-' '_')"
85
+ CRON_D_FILE="/etc/cron.d/infra-${ROLE}-${dir_slug}"
86
+
87
+ remove_user_crontab_marker() {
88
+ command -v crontab >/dev/null 2>&1 || return 0
89
+ local existing filtered
90
+ existing="$(crontab -l 2>/dev/null || true)"
91
+ filtered="$(printf '%s\n' "$existing" | grep -vF "$MARKER" || true)"
92
+ printf '%s\n' "$filtered" | sed '/^$/d' | crontab - 2>/dev/null || true
93
+ }
94
+
95
+ if [[ "$cron_enabled" -eq 1 ]]; then
96
+ if [[ -d /etc/cron.d && -w /etc/cron.d ]]; then
97
+ cat >"$CRON_D_FILE" <<EOF
98
+ # Managed by infra install-auto-update.sh
99
+ # ${MARKER}
100
+ SHELL=/bin/bash
101
+ PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
102
+ ${CRON_SCHED} root ${CRON_CMD}
103
+ EOF
104
+ chmod 644 "$CRON_D_FILE"
105
+ echo "Installed $CRON_D_FILE (minutes ${CRON_MINUTES})"
106
+ remove_user_crontab_marker
107
+ elif command -v crontab >/dev/null 2>&1; then
108
+ existing="$(crontab -l 2>/dev/null || true)"
109
+ filtered="$(printf '%s\n' "$existing" | grep -vF "$MARKER" || true)"
110
+ { printf '%s\n' "$filtered"; echo "${CRON_SCHED} ${CRON_CMD} ${MARKER}"; } | sed '/^$/d' | crontab -
111
+ echo "Installed user cron for $ROLE"
112
+ fi
113
+ else
114
+ rm -f "$CRON_D_FILE" 2>/dev/null || true
115
+ remove_user_crontab_marker
116
+ echo "Auto-update disabled for $ROLE"
117
+ fi
118
+
119
+ # Drop leftover cron markers from older product-specific installers
120
+ MARKER_LEGACY="# onchain-invoice-auto-update:${ROLE}:${SCRIPT_DIR}"
121
+ if command -v crontab >/dev/null 2>&1; then
122
+ existing="$(crontab -l 2>/dev/null || true)"
123
+ filtered="$(printf '%s\n' "$existing" | grep -vF "$MARKER_LEGACY" || true)"
124
+ [[ "$cron_enabled" -eq 1 ]] || printf '%s\n' "$filtered" | sed '/^$/d' | crontab - 2>/dev/null || true
125
+ fi
package/install.sh ADDED
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env bash
2
+ # Infra packager — single wget entrypoint.
3
+ #
4
+ # wget -qO- $PACKAGER_RAW/install.sh | env INFRA_PROFILE=api PACKAGECONFIG_URL=... bash
5
+ #
6
+ # Product wrappers set PACKAGECONFIG_URL and INFRA_PROFILE.
7
+ set -euo pipefail
8
+
9
+ PACKAGER_RAW="${PACKAGER_RAW:-${INFRA_RAW:-https://raw.githubusercontent.com/naiemk/vibed-infra/main}}"
10
+ DEST="${INSTALL_DIR:-.}"
11
+ PROFILE="${INFRA_PROFILE:-}"
12
+
13
+ while [[ $# -gt 0 ]]; do
14
+ case "$1" in
15
+ --profile) PROFILE="$2"; shift 2 ;;
16
+ --components)
17
+ IFS=',' read -ra _comps <<<"$2"
18
+ for c in "${_comps[@]}"; do
19
+ case "$c" in
20
+ app|backend|api) PROFILE=api; break ;;
21
+ workers|nodes) PROFILE=nodes; break ;;
22
+ gateway) PROFILE=gateway; break ;;
23
+ esac
24
+ done
25
+ shift 2
26
+ ;;
27
+ --dest) DEST="$2"; shift 2 ;;
28
+ -h|--help)
29
+ echo "usage: install.sh [--profile api|nodes|gateway] [--dest DIR]"
30
+ echo "env: PACKAGECONFIG_URL PRODUCT_RAW PACKAGER_RAW INFRA_PROFILE INSTALL_DIR"
31
+ exit 0
32
+ ;;
33
+ *) shift ;;
34
+ esac
35
+ done
36
+
37
+ mkdir -p "$DEST"
38
+ DEST="$(cd "$DEST" && pwd)"
39
+
40
+ # Bootstrap libs from packager (works when piped via wget|bash or local PACKAGER_RAW path).
41
+ INFRA_LIB="$DEST/.infra-lib"
42
+ mkdir -p "$INFRA_LIB"
43
+ _bootstrap_lib() {
44
+ local src="$1" dest="$2"
45
+ if [[ -f "$src" ]]; then
46
+ cp -f "$src" "$dest"
47
+ else
48
+ _fetch "${PACKAGER_RAW}/lib/$(basename "$dest")" "$dest"
49
+ fi
50
+ }
51
+ _fetch() {
52
+ local url="$1" out="$2"
53
+ if command -v curl >/dev/null 2>&1; then curl -fsSL "$url" -o "$out"; else wget -qO "$out" "$url"; fi
54
+ }
55
+ LOCAL_PACKAGER=0
56
+ if [[ "$PACKAGER_RAW" =~ ^/ ]] && [[ -d "$PACKAGER_RAW/lib" ]]; then
57
+ LOCAL_PACKAGER=1
58
+ fi
59
+ for lib in fetch.sh env.sh prompt.sh tls.sh load_config.py generate.py; do
60
+ if [[ "$LOCAL_PACKAGER" == "1" ]]; then
61
+ cp -f "${PACKAGER_RAW}/lib/${lib}" "${INFRA_LIB}/${lib}"
62
+ else
63
+ _fetch "${PACKAGER_RAW}/lib/${lib}" "${INFRA_LIB}/${lib}"
64
+ fi
65
+ done
66
+ for lib in start.sh update.sh install-auto-update.sh; do
67
+ if [[ "$LOCAL_PACKAGER" == "1" ]]; then
68
+ cp -f "${PACKAGER_RAW}/${lib}" "${DEST}/${lib}"
69
+ else
70
+ _fetch "${PACKAGER_RAW}/${lib}" "${DEST}/${lib}"
71
+ fi
72
+ chmod +x "${DEST}/${lib}"
73
+ done
74
+
75
+ # shellcheck source=/dev/null
76
+ source "${INFRA_LIB}/fetch.sh"
77
+ # shellcheck source=/dev/null
78
+ source "${INFRA_LIB}/prompt.sh"
79
+ # shellcheck source=/dev/null
80
+ source "${INFRA_LIB}/tls.sh"
81
+
82
+ cd "$DEST"
83
+
84
+ if [[ -z "$PROFILE" ]]; then
85
+ PROFILE="$(infra_pick_profile)"
86
+ fi
87
+
88
+ PACKAGECONFIG_URL="${PACKAGECONFIG_URL:-${PACKAGE_CONFIG_URL:-}}"
89
+ if [[ -z "$PACKAGECONFIG_URL" ]]; then
90
+ echo "Set PACKAGECONFIG_URL to the product packageconfig.yaml (URL or path)" >&2
91
+ exit 1
92
+ fi
93
+ PC_LOCAL="$DEST/.packageconfig.yaml"
94
+ if [[ "$PACKAGECONFIG_URL" =~ ^/ ]]; then
95
+ cp -f "$PACKAGECONFIG_URL" "$PC_LOCAL"
96
+ else
97
+ infra_fetch "$PACKAGECONFIG_URL" "$PC_LOCAL"
98
+ fi
99
+
100
+ if [[ -z "${PRODUCT_RAW:-}" && -n "${ONCHAIN_INVOICE_RAW:-}" ]]; then
101
+ echo "warning: ONCHAIN_INVOICE_RAW is deprecated; use PRODUCT_RAW" >&2
102
+ PRODUCT_RAW="$ONCHAIN_INVOICE_RAW"
103
+ fi
104
+ if [[ -z "${PRODUCT_RAW:-}" ]]; then
105
+ PRODUCT_RAW="$(
106
+ python3 -c "
107
+ import sys
108
+ sys.path.insert(0, '${INFRA_LIB}')
109
+ from load_config import load_packageconfig
110
+ from pathlib import Path
111
+ print(load_packageconfig(Path('${PC_LOCAL}')).get('rawBase',''))
112
+ "
113
+ )"
114
+ fi
115
+ if [[ -z "$PRODUCT_RAW" ]]; then
116
+ echo "Set PRODUCT_RAW or packageconfig rawBase (product templates URL or path)" >&2
117
+ exit 1
118
+ fi
119
+
120
+ echo "== infra install profile=$PROFILE dest=$DEST =="
121
+
122
+ _py_profile() {
123
+ python3 -c "
124
+ import sys, json
125
+ sys.path.insert(0, '${INFRA_LIB}')
126
+ from load_config import load_packageconfig, get_profile
127
+ from pathlib import Path
128
+ p = get_profile(load_packageconfig(Path('${PC_LOCAL}')), '${PROFILE}')
129
+ print(json.dumps(p))
130
+ "
131
+ }
132
+
133
+ PROF_JSON="$(_py_profile)"
134
+ ROLE="$(python3 -c "import json,sys; print(json.loads(sys.argv[1]).get('role','backend'))" "$PROF_JSON")"
135
+ START_SCRIPT="$(python3 -c "import json,sys; print(json.loads(sys.argv[1]).get('startScript','start.sh'))" "$PROF_JSON")"
136
+ UPDATE_SCRIPT="$(python3 -c "import json,sys; print(json.loads(sys.argv[1]).get('updateScript','update.sh'))" "$PROF_JSON")"
137
+ ENV_EXAMPLE="$(python3 -c "import json,sys; t=json.loads(sys.argv[1]).get('templates') or {}; print(t.get('envExample','.env.example'))" "$PROF_JSON")"
138
+ CONFIG_FILE="$(python3 -c "import json,sys; t=json.loads(sys.argv[1]).get('templates') or {}; print(t.get('config',''))" "$PROF_JSON")"
139
+ COMPOSE_TPL="$(python3 -c "import json,sys; t=json.loads(sys.argv[1]).get('templates') or {}; print(t.get('compose',''))" "$PROF_JSON")"
140
+ COMPOSE_MAINNET="$(python3 -c "import json,sys; t=json.loads(sys.argv[1]).get('templates') or {}; print(t.get('composeMainnet',''))" "$PROF_JSON")"
141
+
142
+ cp -f "${INFRA_LIB}/env.sh" "$DEST/lib-env.sh"
143
+ mkdir -p "$DEST/lib"
144
+ cp -f "${INFRA_LIB}/load_config.py" "$DEST/lib/"
145
+ cp -f "${INFRA_LIB}/generate.py" "$DEST/lib/"
146
+
147
+ if [[ -n "$ENV_EXAMPLE" ]]; then
148
+ infra_write_template "${PRODUCT_RAW}/${ENV_EXAMPLE}" "$DEST/${ENV_EXAMPLE}" 0
149
+ cp -f "$DEST/${ENV_EXAMPLE}" "$DEST/.env.example"
150
+ fi
151
+ if [[ -n "$CONFIG_FILE" ]]; then
152
+ infra_write_if_missing "${PRODUCT_RAW}/${CONFIG_FILE}" "$DEST/${CONFIG_FILE}" 0
153
+ infra_write_template "${PRODUCT_RAW}/${CONFIG_FILE}" "$DEST/${CONFIG_FILE}" 0
154
+ fi
155
+ if [[ -n "$COMPOSE_TPL" ]]; then
156
+ infra_write_if_missing "${PRODUCT_RAW}/${COMPOSE_TPL}" "$DEST/${COMPOSE_TPL}" 0
157
+ infra_write_template "${PRODUCT_RAW}/${COMPOSE_TPL}" "$DEST/${COMPOSE_TPL}" 0
158
+ fi
159
+ if [[ -n "$COMPOSE_MAINNET" ]]; then
160
+ infra_write_template "${PRODUCT_RAW}/${COMPOSE_MAINNET}" "$DEST/${COMPOSE_MAINNET}" 0
161
+ fi
162
+
163
+ # Generic infra compose templates (optional USE_COMPOSE=1)
164
+ case "$ROLE" in
165
+ backend)
166
+ infra_write_template "${PACKAGER_RAW}/templates/docker-compose.backend.yml" "$DEST/docker-compose.backend.yml" 0
167
+ ;;
168
+ workers)
169
+ infra_write_template "${PACKAGER_RAW}/templates/docker-compose.workers.yml" "$DEST/docker-compose.workers.yml" 0
170
+ ;;
171
+ gateway)
172
+ infra_write_template "${PACKAGER_RAW}/templates/docker-compose.gateway.yml" "$DEST/docker-compose.gateway.yml" 0
173
+ ;;
174
+ esac
175
+
176
+ if [[ -n "$START_SCRIPT" && "$START_SCRIPT" != "start.sh" ]]; then
177
+ infra_write_if_missing "${PRODUCT_RAW}/${START_SCRIPT}" "$DEST/${START_SCRIPT}" 1
178
+ infra_write_template "${PRODUCT_RAW}/${START_SCRIPT}" "$DEST/${START_SCRIPT}" 1
179
+ fi
180
+ if [[ -n "$UPDATE_SCRIPT" && "$UPDATE_SCRIPT" != "update.sh" ]]; then
181
+ infra_write_if_missing "${PRODUCT_RAW}/${UPDATE_SCRIPT}" "$DEST/${UPDATE_SCRIPT}" 1
182
+ infra_write_template "${PRODUCT_RAW}/${UPDATE_SCRIPT}" "$DEST/${UPDATE_SCRIPT}" 1
183
+ fi
184
+
185
+ python3 -c "
186
+ import json, sys
187
+ extras = json.loads(sys.argv[1]).get('extras') or []
188
+ for e in extras:
189
+ print(e)
190
+ " "$PROF_JSON" | while read -r extra; do
191
+ [[ -n "$extra" ]] || continue
192
+ infra_write_if_missing "${PRODUCT_RAW}/${extra}" "$DEST/${extra}" 1
193
+ infra_write_template "${PRODUCT_RAW}/${extra}" "$DEST/${extra}" 1
194
+ done
195
+
196
+ if [[ "$ROLE" == "gateway" ]]; then
197
+ mkdir -p "$DEST/gateway/conf.d"
198
+ infra_write_if_missing "${PRODUCT_RAW}/gateway/nginx.conf" "$DEST/gateway/nginx.conf" 0
199
+ infra_write_template "${PRODUCT_RAW}/gateway/nginx.conf" "$DEST/gateway/nginx.conf" 0
200
+ if python3 "${INFRA_LIB}/generate.py" "$PC_LOCAL" --profile "$PROFILE" -o "$DEST/gateway/conf.d/domains.conf" 2>/dev/null; then
201
+ echo "generated: gateway/conf.d/domains.conf"
202
+ else
203
+ infra_write_if_missing "${PRODUCT_RAW}/gateway/conf.d/domains.conf" "$DEST/gateway/conf.d/domains.conf" 0
204
+ infra_write_template "${PRODUCT_RAW}/gateway/conf.d/domains.conf" "$DEST/gateway/conf.d/domains.conf" 0
205
+ fi
206
+ fi
207
+
208
+ if [[ ! -f "$DEST/.env" ]]; then
209
+ cp "$DEST/.env.example" "$DEST/.env"
210
+ echo "created: $DEST/.env"
211
+ else
212
+ echo "exists: $DEST/.env"
213
+ fi
214
+
215
+ cat >"$DEST/.infra-profile" <<EOF
216
+ PROFILE=${PROFILE}
217
+ ROLE=${ROLE}
218
+ START_SCRIPT=${START_SCRIPT}
219
+ UPDATE_SCRIPT=${UPDATE_SCRIPT}
220
+ EOF
221
+
222
+ chmod +x "$DEST/${START_SCRIPT}" 2>/dev/null || true
223
+ INFRA_PROFILE="$PROFILE" ./install-auto-update.sh || true
224
+
225
+ if [[ "$ROLE" == "gateway" ]]; then
226
+ # shellcheck source=/dev/null
227
+ source "$DEST/lib-env.sh"
228
+ load_dotenv "$DEST/.env"
229
+ CERT_DIR="$(python3 -c "
230
+ import sys
231
+ sys.path.insert(0, '${INFRA_LIB}')
232
+ from load_config import load_packageconfig, get_profile
233
+ from pathlib import Path
234
+ p = get_profile(load_packageconfig(Path('${PC_LOCAL}')), '${PROFILE}')
235
+ sites = p.get('sites') or []
236
+ if not sites:
237
+ print('/etc/letsencrypt/live/example.com')
238
+ else:
239
+ s = sites[0]
240
+ print(s.get('tlsCertDir') or ('/etc/letsencrypt/live/' + s['host']))
241
+ ")"
242
+ TLS_FULLCHAIN="${TLS_FULLCHAIN:-${CERT_DIR}/fullchain.pem}"
243
+ TLS_PRIVKEY="${TLS_PRIVKEY:-${CERT_DIR}/privkey.pem}"
244
+ mapfile -t DOMAIN_ARR < <(python3 -c "
245
+ import sys
246
+ sys.path.insert(0, '${INFRA_LIB}')
247
+ from load_config import load_packageconfig, get_profile
248
+ from pathlib import Path
249
+ p = get_profile(load_packageconfig(Path('${PC_LOCAL}')), '${PROFILE}')
250
+ for s in p.get('sites') or []:
251
+ print(s['host'])
252
+ for a in s.get('aliases') or []:
253
+ print(a)
254
+ ")
255
+ infra_tls_offer_interactive "$TLS_FULLCHAIN" "$TLS_PRIVKEY" "${DOMAIN_ARR[@]}"
256
+ fi
257
+
258
+ echo ""
259
+ echo "Install complete. Start: cd $DEST && ./${START_SCRIPT}"