@aifabrix/server-setup 1.4.0 → 1.5.2

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
@@ -42,72 +42,93 @@ Do the steps in [What you must do before running af-server](#what-you-must-do-be
42
42
 
43
43
  Complete the [manual prerequisites](#what-you-must-do-before-running-af-server) first (DNS, SSL directory, certificate and key on the server).
44
44
 
45
- ### Option A: From your PC (remote install over SSH)
45
+ **Flow summary:** Only **step 1** runs over SSH from your PC. Steps **5** and **7** run **on the server** after you log in, so errors and output are visible directly there.
46
46
 
47
- 1. **Install CLIs** (one-time):
47
+ | Step | Where | Action |
48
+ | ---- | --------- | ------ |
49
+ | 1 | From PC | `af-server install-init $SSH` — only command over SSH; installs on server: SSH (if needed), Node 18+, npm, and `af-server` CLI. |
50
+ | 2 | One-time | Log in to the server once (e.g. with password) to approve passwordless SSH; then from PC: `af-server ssh-cert install $SSH`. |
51
+ | 3 | From PC | Copy SSL certificate and key to the server (see [SSL directory and certificates](#ssl-directory-and-certificates)); example commands below. |
52
+ | 4 | From PC | Log in to the server via SSH (passwordless). |
53
+ | 5 | **On server** | `sudo af-server install` — install all services (Docker, nginx package, Mutagen, cron, data dir); no builder-server container yet. |
54
+ | 6 | On server | Get the builder-server image (e.g. `az login` / `docker pull`). |
55
+ | 7 | **On server** | `sudo af-server install-server --dev-domain $DOMAIN` — nginx vhost, builder-server container, Docker TLS. |
56
+ | 8 | — | Done. |
48
57
 
49
- ```bash
50
- npm install -g @aifabrix/builder
51
- npm install -g @aifabrix/server-setup
52
- ```
58
+ ### Step 1: Bootstrap the server (from PC)
53
59
 
54
- 2. **Set your server target and local path** (replace with your host, user, and the folder on your PC where your cert and key files are):
60
+ Set your target and run the only command that uses SSH from your PC:
55
61
 
56
- ```bash
57
- export SSH=serveradmin@builder02.aifabrix.dev
58
- export HDD=/c/git/esystemsdev/aifabrix-setup/certificates
59
- export DOMAIN=builder02.aifabrix.dev
60
- ```
62
+ ```bash
63
+ export SSH=serveradmin@builder02.aifabrix.dev
64
+ export DOMAIN=builder02.aifabrix.dev
65
+ af-server install-init $SSH
66
+ ```
61
67
 
62
- 3. **If the server has no SSH yet** (e.g. fresh VM with console only), enable SSH first (you need another way to run one command, e.g. cloud console):
68
+ This installs on the server: openssh-server (if needed), Node 18+, npm, and `@aifabrix/builder` + `@aifabrix/server-setup` so `af-server` is available there. No Docker, nginx, or builder-server yet.
63
69
 
64
- ```bash
65
- af-server install-ssh $SSH
66
- ```
70
+ ### Step 2: Passwordless SSH (from PC)
67
71
 
68
- 4. **Add your SSH key** for passwordless auth (so install can run over SSH):
72
+ Log in to the server once (e.g. with password) to accept the host key and/or approve auth. Then from your PC:
69
73
 
70
- ```bash
71
- af-server ssh-cert install $SSH
72
- ```
74
+ ```bash
75
+ af-server ssh-cert install $SSH
76
+ ```
73
77
 
74
- 5. **Put SSL cert and key on the server** in `/opt/aifabrix/ssl` as `wildcard.crt` and `wildcard.key`. See [SSL directory and certificates](#ssl-directory-and-certificates).
78
+ ### Step 3: Copy SSL (from PC)
75
79
 
76
- Example: copy from your PC to the server (create the directory on the server first, then copy and fix key permissions):
80
+ Put `wildcard.crt` and `wildcard.key` in `/opt/aifabrix/ssl` on the server. Example (replace `$HDD` with the folder on your PC that has the cert and key):
77
81
 
78
- ```bash
79
- ssh $SSH "sudo mkdir -p /opt/aifabrix/ssl"
80
- scp $HDD/wildcard.crt $SSH:/tmp/wildcard.crt
81
- scp $HDD/wildcard.key $SSH:/tmp/wildcard.key
82
- ssh $SSH "sudo mv /tmp/wildcard.crt /tmp/wildcard.key /opt/aifabrix/ssl/ && sudo chmod 600 /opt/aifabrix/ssl/wildcard.key"
83
- ```
82
+ ```bash
83
+ export HDD=/workspace/aifabrix-setup/certificates
84
+ ssh $SSH "sudo mkdir -p /opt/aifabrix/ssl"
85
+ scp $HDD/wildcard.crt $SSH:/tmp/wildcard.crt
86
+ scp $HDD/wildcard.key $SSH:/tmp/wildcard.key
87
+ ssh $SSH "sudo mv /tmp/wildcard.crt /tmp/wildcard.key /opt/aifabrix/ssl/ && sudo chmod 600 /opt/aifabrix/ssl/wildcard.key"
88
+ ```
84
89
 
85
- 6. **Run install** (uses sudo on the server):
90
+ ### Step 4: Log in to the server
86
91
 
87
- ```bash
88
- af-server install $SSH
89
- ```
92
+ ```bash
93
+ ssh $SSH
94
+ ```
90
95
 
91
- To use a different domain or SSL path:
96
+ ### Step 5: Install services (on server)
92
97
 
93
- ```bash
94
- af-server install $SSH --dev-domain $DOMAIN --ssl-dir /opt/aifabrix/ssl
95
- ```
98
+ ```bash
99
+ sudo af-server install
100
+ ```
96
101
 
97
- - Space between `$DOMAIN` and `--ssl-dir` is required.
98
- - **Git Bash (Windows):** quote server paths so the shell doesn't expand `/opt` (e.g. `--ssl-dir '/opt/aifabrix/ssl'`, `--data-dir '/opt/aifabrix/builder-server/data'`).
102
+ This installs Docker, nginx (package only), Mutagen, data dir, apply-dev-users script and cron. It does **not** write the builder nginx vhost or start the builder-server container.
99
103
 
100
- ### Option B: On the server itself (Ubuntu, no target)
104
+ ### Step 6: Get the builder-server image (on server)
101
105
 
102
- If you are already on the server (e.g. over console or SSH), run install locally. Prerequisites (DNS, SSL dir with `wildcard.crt` and `wildcard.key`) must still be done first.
106
+ The image is not on a public registry. Use your platform’s method (e.g. Azure CLI or Docker login), then pull. Example:
103
107
 
104
108
  ```bash
105
- npm install -g @aifabrix/builder
106
- npm install -g @aifabrix/server-setup
107
- sudo af-server install
109
+ az login
110
+ az acr login --name aifabrixdevacr
111
+ docker pull aifabrixdevacr.azurecr.io/aifabrix/builder-server:latest
108
112
  ```
109
113
 
110
- After install, your builder-server is up. Use the **AI Fabrix Builder CLI** for all usage (users, secrets, certs, etc.)—see [Builder documentation](https://github.com/esystemsdev/aifabrix-builder/blob/main/docs/developer-isolation.md).
114
+ Or with Docker login (username/password from your registry):
115
+
116
+ ```bash
117
+ docker login <registry> -u <user> -p <password>
118
+ docker pull <registry>/aifabrix/builder-server:latest
119
+ ```
120
+
121
+ ### Step 7: Install server (nginx vhost + container) (on server)
122
+
123
+ ```bash
124
+ sudo af-server install-server --dev-domain $DOMAIN
125
+ ```
126
+
127
+ Use the same domain as your DNS and SSL. Optional: `--ssl-dir /opt/aifabrix/ssl`, `--data-dir /opt/aifabrix/builder-server/data`, `--builder-port 3000`.
128
+
129
+ ### Step 8: Done
130
+
131
+ Your builder-server is up. Use the **AI Fabrix Builder CLI** for users, secrets, certs, etc.—see [Builder documentation](https://github.com/esystemsdev/aifabrix-builder/blob/main/docs/developer-isolation.md).
111
132
 
112
133
  ---
113
134
 
@@ -115,7 +136,9 @@ After install, your builder-server is up. Use the **AI Fabrix Builder CLI** for
115
136
 
116
137
  | Command | Description |
117
138
  | -------- | ----------- |
118
- | `af-server install [ user@host ] [ -d DATA_DIR ] [ --dev-domain DOMAIN ] [ --ssl-dir PATH ] [ -i SSH_KEY ]` | Install or update: Docker, nginx (builder vhost always updated from template), SSL proxy, sync user, cron. Re-run to apply latest config. |
139
+ | `af-server install-init <user@host> [ -i SSH_KEY ]` | **From PC only.** One-time bootstrap over SSH: install on server SSH (if needed), Node 18+, npm, and af-server CLI. |
140
+ | `af-server install [ user@host ] [ -d DATA_DIR ] [ --dev-domain DOMAIN ] [ --ssl-dir PATH ] [ -i SSH_KEY ]` | **Run on server** (omit target): `sudo af-server install`. Infra only: Docker, nginx pkg, Mutagen, data dir, cron. No builder vhost or container. With target: same infra over SSH. |
141
+ | `af-server install-server --dev-domain DOMAIN [ -d DATA_DIR ] [ --ssl-dir PATH ] [ --builder-port PORT ]` | **On server only.** Nginx vhost, builder-server container, Docker TLS. Run after `sudo af-server install`. |
119
142
  | `af-server backup [ user@host ] [ -d DATA_DIR ] [ -o output.zip ] [ -i SSH_KEY ]` | On-demand backup (config + DB + keys). |
120
143
  | `af-server backup [ user@host ] --schedule [ --backup-dir PATH ] [ --keep-days N ] [ -i SSH_KEY ]` | Cron backup (daily 02:00, keep last N, default 7). |
121
144
  | `af-server restore backup.zip [ user@host ] [ -d DATA_DIR ] [ --force ] [ -i SSH_KEY ]` | Restore backup to DATA_DIR. |
@@ -171,18 +194,23 @@ If you SSH as a non-root user, that user must be able to sudo. The script will a
171
194
 
172
195
  ---
173
196
 
174
- ## High level: what the install does on your Ubuntu server
197
+ ## High level: what install vs install-server does
198
+
199
+ **`af-server install`** (step 5 — run on the server) does **infra only**:
200
+
201
+ - **System** — `apt update` and `apt upgrade`; optional hostname if `SETUP_HOSTNAME` is set.
202
+ - **Docker** — Installs Docker if missing; enables and starts it.
203
+ - **Admin user** — Adds the admin user (default `serveradmin`) to the `docker` group and grants passwordless sudo.
204
+ - **Nginx** — Installs the nginx **package** only; enables and starts it. Does **not** write the builder vhost yet.
205
+ - **Data dir** — Creates the data directory (default `/opt/aifabrix/builder-server/data`), workspace and ssh-keys subdirs, ownership for the container.
206
+ - **Apply-dev-users** — Installs the script and cron job (every 2 minutes) that sync per-developer OS users from builder-server state.
207
+ - **Mutagen** — Downloads and installs Mutagen; systemd service and daemon.
208
+ - **Optional** — If `INSTALL_PORTAINER=1`, installs the Portainer container.
175
209
 
176
- When you run `af-server install`, the script (as root) does the following on the server:
210
+ **`af-server install-server`** (step 7 run on the server) does the **server phase**:
177
211
 
178
- - **System** — `apt update` and `apt upgrade`; optional hostname change if `SETUP_HOSTNAME` is set.
179
- - **Docker** — Installs Docker if missing; enables and starts the Docker service.
180
- - **Admin user** — Adds the admin user (default `serveradmin`) to the `docker` group and grants passwordless sudo (`/etc/sudoers.d/<admin>`).
181
- - **Nginx** — Installs nginx if missing; enables and starts it. Generates a site config for your domain that proxies HTTPS to the builder-server container (using `wildcard.crt` and `wildcard.key` from your SSL dir).
182
- - **Builder-server data dir** — Creates the data directory (default `/opt/aifabrix/builder-server/data`), sets ownership for the container. Starts the `builder-server` container if the image already exists on the server; otherwise prints how to build and run it (you get the image from the AI Fabrix platform).
183
- - **Sync user** — Creates a system user (default `aifabrix-sync`) for Mutagen SSH sync; home under the data dir; creates `.ssh` and `authorized_keys`.
184
- - **Cron job** — Installs a cron job (every 2 minutes) that copies the managed `authorized_keys` file into the sync user’s `.ssh/authorized_keys`.
185
- - **Mutagen** — Downloads and installs the Mutagen binary; creates a systemd service and starts the daemon.
186
- - **Optional** — If `INSTALL_PORTAINER=1`, installs the Portainer container. If Docker TLS is not skipped, writes `/etc/docker/daemon.json`; you can use the **same certificate** from `/opt/aifabrix/ssl` (e.g. symlink or copy `wildcard.crt` and `wildcard.key` to the paths Docker expects: `/etc/docker/server-cert.pem`, `/etc/docker/server-key.pem`, and if needed `ca.pem` for the CA), or provide separate Docker TLS certs.
212
+ - **Nginx vhost** — Writes the builder site config from template (domain, SSL dir, proxy to builder-server), reloads nginx.
213
+ - **Builder-server container** — Creates data dir (if needed), starts the builder-server container (if the image is present).
214
+ - **Docker TLS** — Copies certs and configures `/etc/docker/daemon.json` for TLS (using website cert and builder-server CA).
187
215
 
188
- Result: your Ubuntu server has Docker, nginx (HTTPS for your domain), the builder-server container (if image present), the sync user and key sync, and Mutagen—ready for the Builder CLI to use.
216
+ Result: after both steps, the server has Docker, nginx (HTTPS for your domain), the builder-server container (if image present), and Mutagen—ready for the Builder CLI to use.
@@ -3,9 +3,12 @@
3
3
  # Run via af-server install user@host. REPO_ROOT must be set to the dir containing builder/builder-server/nginx-builder-server.conf.template.
4
4
  # On empty server: installs Docker, nginx, admin user, optional Portainer/Mutagen; then updates nginx config and ensures builder-server container.
5
5
  # Optional env: DEV_DOMAIN, SSL_DIR, DATA_DIR, SETUP_ADMIN_USER, SYNC_USER, BUILDER_SERVER_PORT, NGINX_CONF_DIR, SETUP_HOSTNAME, INSTALL_PORTAINER=1, SKIP_DOCKER_TLS=1.
6
+ # INSTALL_PHASE: infra = Docker, nginx pkg, Mutagen, data dir, apply-dev-users (no vhost, no container, no Docker TLS). server = nginx vhost, builder-server container, Docker TLS. full = both.
6
7
 
7
8
  set -e
8
9
  export DEBIAN_FRONTEND=noninteractive
10
+ # Cache sudo so one password covers the whole script (avoids repeated "[sudo] password for ...")
11
+ sudo -v
9
12
 
10
13
  REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
11
14
  DATA_DIR="${DATA_DIR:-/opt/aifabrix/builder-server/data}"
@@ -15,6 +18,7 @@ SETUP_ADMIN_USER="${SETUP_ADMIN_USER:-serveradmin}"
15
18
  SYNC_USER="${SYNC_USER:-aifabrix-sync}"
16
19
  BUILDER_SERVER_PORT="${BUILDER_SERVER_PORT:-3000}"
17
20
  NGINX_CONF_DIR="${NGINX_CONF_DIR:-/etc/nginx/conf.d}"
21
+ INSTALL_PHASE="${INSTALL_PHASE:-full}"
18
22
 
19
23
  # Sanitize user-controlled env to prevent path/command injection
20
24
  sanitize_domain() {
@@ -52,6 +56,9 @@ require_sudo() {
52
56
  }
53
57
  require_sudo
54
58
 
59
+ # --- Infra phase: hostname, system, Docker, nginx package, Mutagen, data dir, apply-dev-users ---
60
+ if [ "$INSTALL_PHASE" = "infra" ] || [ "$INSTALL_PHASE" = "full" ]; then
61
+
55
62
  # --- Hostname (optional) ---
56
63
  if [ -n "$SETUP_HOSTNAME" ]; then
57
64
  current=$(hostname 2>/dev/null || true)
@@ -76,6 +83,13 @@ apt-get install -y \
76
83
  systemctl enable ssh 2>/dev/null || systemctl enable sshd 2>/dev/null || true
77
84
  systemctl start ssh 2>/dev/null || systemctl start sshd 2>/dev/null || true
78
85
 
86
+ # --- Azure CLI (az) ---
87
+ # Manual one-liner (sudo must apply to bash): curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
88
+ echo "=== Azure CLI (optional - remove block if not needed) ==="
89
+ if ! command -v az >/dev/null 2>&1; then
90
+ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
91
+ fi
92
+
79
93
  # --- Docker ---
80
94
  if ! command -v docker >/dev/null 2>&1; then
81
95
  apt-get install -y docker.io
@@ -112,30 +126,12 @@ if [ "$INSTALL_PORTAINER" = "1" ] && ! docker ps -a --format '{{.Names}}' 2>/dev
112
126
  portainer/portainer-ce:latest 2>/dev/null || true
113
127
  fi
114
128
 
115
- # --- Nginx ---
129
+ # --- Nginx (package only in infra; vhost + reload in server phase) ---
116
130
  if ! command -v nginx >/dev/null 2>&1; then
117
131
  apt-get install -y nginx
118
132
  systemctl enable nginx
119
133
  systemctl start nginx
120
134
  fi
121
- NGINX_CONF="$NGINX_CONF_DIR/$DEV_DOMAIN.conf"
122
- NGINX_TEMPLATE="$REPO_ROOT/builder/builder-server/nginx-builder-server.conf.template"
123
- # Always update builder vhost from template so re-running install applies latest config.
124
- if [ -f "$NGINX_TEMPLATE" ]; then
125
- sed -e "s|DEV_DOMAIN_PLACEHOLDER|$DEV_DOMAIN|g" \
126
- -e "s|SSL_DIR_PLACEHOLDER|$SSL_DIR|g" \
127
- -e "s|BUILDER_SERVER_PORT_PLACEHOLDER|$BUILDER_SERVER_PORT|g" \
128
- -e "s|DATA_DIR_PLACEHOLDER|$DATA_DIR|g" \
129
- "$NGINX_TEMPLATE" > "$NGINX_CONF"
130
- elif [ ! -f "$NGINX_CONF" ]; then
131
- echo "Warning: SSL prereqs required. Place $NGINX_CONF (see SETUP.md) and ensure $SSL_DIR/wildcard.crt and $SSL_DIR/wildcard.key exist."
132
- fi
133
- if command -v nginx >/dev/null 2>&1 && nginx -t 2>/dev/null; then
134
- systemctl reload nginx
135
- elif [ -f "$NGINX_TEMPLATE" ] && command -v nginx >/dev/null 2>&1; then
136
- echo "Warning: nginx -t failed (config was still written to $NGINX_CONF). Nginx was NOT reloaded."
137
- nginx -t 2>&1 || true
138
- fi
139
135
 
140
136
  # --- Mutagen ---
141
137
  if ! command -v mutagen >/dev/null 2>&1; then
@@ -168,6 +164,112 @@ MUTAGEN_EOF
168
164
  fi
169
165
  fi
170
166
 
167
+ # --- Data dir and workspace/ssh-keys (for apply-dev-users) ---
168
+ mkdir -p "$DATA_DIR" "${DATA_DIR}/workspace" "${DATA_DIR}/ssh-keys"
169
+ chown -R 1001:65533 "$DATA_DIR"
170
+ chmod 755 "$DATA_DIR"
171
+ DATA_DIR_ABS=$(cd "$DATA_DIR" && pwd)
172
+
173
+ # --- Host job: apply per-developer OS users from builder-server state ---
174
+ APPLY_DEV_USERS_SCRIPT="/usr/local/bin/aifabrix-apply-dev-users.sh"
175
+ SETUP_ASSETS_DIR="$(cd "$(dirname "$0")" && pwd)"
176
+ if [ ! -f "$APPLY_DEV_USERS_SCRIPT" ]; then
177
+ if [ -f "$SETUP_ASSETS_DIR/aifabrix-apply-dev-users.sh" ]; then
178
+ cp "$SETUP_ASSETS_DIR/aifabrix-apply-dev-users.sh" "$APPLY_DEV_USERS_SCRIPT"
179
+ else
180
+ # Inline fallback when run from builder/builder-server context
181
+ cat > "$APPLY_DEV_USERS_SCRIPT" << 'APPLY_DEV_EOF'
182
+ #!/bin/sh
183
+ DATA_DIR="${DATA_DIR:-/opt/aifabrix/builder-server/data}"
184
+ SSH_KEYS_DIR="${DATA_DIR}/ssh-keys"
185
+ PENDING_REMOVALS="${DATA_DIR}/pending-removals"
186
+ [ ! -d "$DATA_DIR" ] && exit 0
187
+ if [ -f "$PENDING_REMOVALS" ]; then
188
+ while IFS= read -r dev_id || [ -n "$dev_id" ]; do
189
+ dev_id=$(echo "$dev_id" | tr -d '\r\n ')
190
+ [ -z "$dev_id" ] && continue
191
+ user_name="dev${dev_id}"
192
+ getent passwd "$user_name" >/dev/null 2>&1 && ( userdel -r "$user_name" 2>/dev/null || userdel "$user_name" 2>/dev/null || true )
193
+ done < "$PENDING_REMOVALS"
194
+ : > "$PENDING_REMOVALS"
195
+ fi
196
+ for key_file in "${SSH_KEYS_DIR}"/*/authorized_keys; do
197
+ [ -f "$key_file" ] || continue
198
+ dev_id=$(dirname "$key_file" | xargs basename)
199
+ [ -z "$dev_id" ] && continue
200
+ user_name="dev${dev_id}"
201
+ home_dir="/home/${user_name}"
202
+ workspace_target="${DATA_DIR}/workspace/${user_name}"
203
+ getent passwd "$user_name" >/dev/null 2>&1 || useradd -m -s /bin/bash "$user_name"
204
+ mkdir -p "${home_dir}/.ssh"
205
+ cp "$key_file" "${home_dir}/.ssh/authorized_keys"
206
+ chown -R "${user_name}:${user_name}" "${home_dir}/.ssh"
207
+ chmod 700 "${home_dir}/.ssh"
208
+ chmod 600 "${home_dir}/.ssh/authorized_keys"
209
+ config_file="${home_dir}/.aifabrix/config.yaml"
210
+ if [ ! -f "$config_file" ]; then
211
+ mkdir -p "${home_dir}/.aifabrix"
212
+ hostname_val=$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "localhost")
213
+ printf 'user-mutagen-folder: %s\nsecrets-encryption: ""\naifabrix-secrets: ""\naifabrix-env-config: ""\nremote-server: "http://localhost:3000"\ndocker-endpoint: "tcp://%s:2376"\nsync-ssh-user: "%s"\nsync-ssh-host: "%s"\n' "$workspace_target" "$hostname_val" "$user_name" "$hostname_val" > "$config_file"
214
+ chown -R "${user_name}:${user_name}" "${home_dir}/.aifabrix"
215
+ chmod 700 "${home_dir}/.aifabrix"
216
+ chmod 600 "$config_file"
217
+ fi
218
+ mkdir -p "$workspace_target"
219
+ chown -R "${user_name}:${user_name}" "$workspace_target"
220
+ [ -L "${home_dir}/workspace" ] && [ "$(readlink -f "${home_dir}/workspace" 2>/dev/null)" != "$(readlink -f "$workspace_target" 2>/dev/null)" ] && rm -f "${home_dir}/workspace"
221
+ [ ! -e "${home_dir}/workspace" ] && ln -s "$workspace_target" "${home_dir}/workspace" && chown -h "${user_name}:${user_name}" "${home_dir}/workspace"
222
+ profile="${home_dir}/.profile"
223
+ [ ! -f "$profile" ] && touch "$profile" && chown "${user_name}:${user_name}" "$profile"
224
+ grep -q 'cd.*workspace' "$profile" 2>/dev/null || echo '[ -d "$HOME/workspace" ] && cd "$HOME/workspace"' >> "$profile"
225
+ bashrc="${home_dir}/.bashrc"
226
+ [ ! -f "$bashrc" ] && touch "$bashrc" && chown "${user_name}:${user_name}" "$bashrc"
227
+ grep -q 'cd.*workspace' "$bashrc" 2>/dev/null || echo '[ -d "$HOME/workspace" ] && cd "$HOME/workspace"' >> "$bashrc"
228
+ done
229
+ APPLY_DEV_EOF
230
+ fi
231
+ chmod 755 "$APPLY_DEV_USERS_SCRIPT"
232
+ fi
233
+ # Defaults for apply script (aifabrix-secrets, aifabrix-env-config in generated config); match builder-server env.template
234
+ APPLY_DEFAULTS="$DATA_DIR_ABS/apply-dev-users-defaults"
235
+ if [ ! -f "$APPLY_DEFAULTS" ]; then
236
+ {
237
+ echo "# Sourced by cron before aifabrix-apply-dev-users.sh; override to match builder-server .env"
238
+ echo 'export AIFABRIX_SECRETS="${AIFABRIX_SECRETS:-/aifabrix-miso/builder/secrets.local.yaml}"'
239
+ echo 'export AIFABRIX_ENV_CONFIG="${AIFABRIX_ENV_CONFIG:-aifabrix-miso/builder/env-config.yaml}"'
240
+ } > "$APPLY_DEFAULTS"
241
+ chmod 644 "$APPLY_DEFAULTS"
242
+ fi
243
+ if [ -d /etc/cron.d ] && [ ! -f /etc/cron.d/aifabrix-apply-dev-users ]; then
244
+ echo "*/2 * * * * root /bin/sh -c '. $APPLY_DEFAULTS 2>/dev/null; export DATA_DIR=$DATA_DIR_ABS; exec $APPLY_DEV_USERS_SCRIPT'" > /etc/cron.d/aifabrix-apply-dev-users
245
+ chmod 644 /etc/cron.d/aifabrix-apply-dev-users
246
+ fi
247
+
248
+ fi
249
+ # --- End infra phase ---
250
+
251
+ # --- Server phase: nginx vhost, builder-server container, Docker TLS ---
252
+ if [ "$INSTALL_PHASE" = "server" ] || [ "$INSTALL_PHASE" = "full" ]; then
253
+
254
+ # --- Nginx builder vhost from template ---
255
+ NGINX_CONF="$NGINX_CONF_DIR/$DEV_DOMAIN.conf"
256
+ NGINX_TEMPLATE="$REPO_ROOT/builder/builder-server/nginx-builder-server.conf.template"
257
+ if [ -f "$NGINX_TEMPLATE" ]; then
258
+ sed -e "s|DEV_DOMAIN_PLACEHOLDER|$DEV_DOMAIN|g" \
259
+ -e "s|SSL_DIR_PLACEHOLDER|$SSL_DIR|g" \
260
+ -e "s|BUILDER_SERVER_PORT_PLACEHOLDER|$BUILDER_SERVER_PORT|g" \
261
+ -e "s|DATA_DIR_PLACEHOLDER|$DATA_DIR|g" \
262
+ "$NGINX_TEMPLATE" > "$NGINX_CONF"
263
+ elif [ ! -f "$NGINX_CONF" ]; then
264
+ echo "Warning: SSL prereqs required. Place $NGINX_CONF (see SETUP.md) and ensure $SSL_DIR/wildcard.crt and $SSL_DIR/wildcard.key exist."
265
+ fi
266
+ if command -v nginx >/dev/null 2>&1 && nginx -t 2>/dev/null; then
267
+ systemctl reload nginx
268
+ elif [ -f "$NGINX_TEMPLATE" ] && command -v nginx >/dev/null 2>&1; then
269
+ echo "Warning: nginx -t failed (config was still written to $NGINX_CONF). Nginx was NOT reloaded."
270
+ nginx -t 2>&1 || true
271
+ fi
272
+
171
273
  # --- Builder-server data dir and container ---
172
274
  # Paths match AI Fabrix Builder: aifabrix build + resolve use DATA_DIR=/mnt/data in container; host DATA_DIR (e.g. /opt/aifabrix/builder-server/data) is the HDD/mount. See builder/builder-server/README.md and env.template.
173
275
  # Nginx uses DATA_DIR/ca.crt for ssl_client_certificate; container must use the same host path so CA matches.
@@ -308,83 +410,7 @@ DOCKER_EOF
308
410
  fi
309
411
  fi
310
412
 
311
- # --- Workspace and ssh-keys dirs (per-developer users; apply script manages OS users) ---
312
- mkdir -p "${DATA_DIR}/workspace"
313
- mkdir -p "${DATA_DIR}/ssh-keys"
314
-
315
- # --- Host job: apply per-developer OS users from builder-server state ---
316
- APPLY_DEV_USERS_SCRIPT="/usr/local/bin/aifabrix-apply-dev-users.sh"
317
- SETUP_ASSETS_DIR="$(cd "$(dirname "$0")" && pwd)"
318
- if [ ! -f "$APPLY_DEV_USERS_SCRIPT" ]; then
319
- if [ -f "$SETUP_ASSETS_DIR/aifabrix-apply-dev-users.sh" ]; then
320
- cp "$SETUP_ASSETS_DIR/aifabrix-apply-dev-users.sh" "$APPLY_DEV_USERS_SCRIPT"
321
- else
322
- # Inline fallback when run from builder/builder-server context
323
- cat > "$APPLY_DEV_USERS_SCRIPT" << 'APPLY_DEV_EOF'
324
- #!/bin/sh
325
- DATA_DIR="${DATA_DIR:-/opt/aifabrix/builder-server/data}"
326
- SSH_KEYS_DIR="${DATA_DIR}/ssh-keys"
327
- PENDING_REMOVALS="${DATA_DIR}/pending-removals"
328
- [ ! -d "$DATA_DIR" ] && exit 0
329
- if [ -f "$PENDING_REMOVALS" ]; then
330
- while IFS= read -r dev_id || [ -n "$dev_id" ]; do
331
- dev_id=$(echo "$dev_id" | tr -d '\r\n ')
332
- [ -z "$dev_id" ] && continue
333
- user_name="dev${dev_id}"
334
- getent passwd "$user_name" >/dev/null 2>&1 && ( userdel -r "$user_name" 2>/dev/null || userdel "$user_name" 2>/dev/null || true )
335
- done < "$PENDING_REMOVALS"
336
- : > "$PENDING_REMOVALS"
337
- fi
338
- for key_file in "${SSH_KEYS_DIR}"/*/authorized_keys; do
339
- [ -f "$key_file" ] || continue
340
- dev_id=$(dirname "$key_file" | xargs basename)
341
- [ -z "$dev_id" ] && continue
342
- user_name="dev${dev_id}"
343
- home_dir="/home/${user_name}"
344
- workspace_target="${DATA_DIR}/workspace/${user_name}"
345
- getent passwd "$user_name" >/dev/null 2>&1 || useradd -m -s /bin/bash "$user_name"
346
- mkdir -p "${home_dir}/.ssh"
347
- cp "$key_file" "${home_dir}/.ssh/authorized_keys"
348
- chown -R "${user_name}:${user_name}" "${home_dir}/.ssh"
349
- chmod 700 "${home_dir}/.ssh"
350
- chmod 600 "${home_dir}/.ssh/authorized_keys"
351
- config_file="${home_dir}/.aifabrix/config.yaml"
352
- if [ ! -f "$config_file" ]; then
353
- mkdir -p "${home_dir}/.aifabrix"
354
- hostname_val=$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "localhost")
355
- printf 'user-mutagen-folder: %s\nsecrets-encryption: ""\naifabrix-secrets: ""\naifabrix-env-config: ""\nremote-server: "http://localhost:3000"\ndocker-endpoint: "tcp://%s:2376"\nsync-ssh-user: "%s"\nsync-ssh-host: "%s"\n' "$workspace_target" "$hostname_val" "$user_name" "$hostname_val" > "$config_file"
356
- chown -R "${user_name}:${user_name}" "${home_dir}/.aifabrix"
357
- chmod 700 "${home_dir}/.aifabrix"
358
- chmod 600 "$config_file"
359
- fi
360
- mkdir -p "$workspace_target"
361
- chown -R "${user_name}:${user_name}" "$workspace_target"
362
- [ -L "${home_dir}/workspace" ] && [ "$(readlink -f "${home_dir}/workspace" 2>/dev/null)" != "$(readlink -f "$workspace_target" 2>/dev/null)" ] && rm -f "${home_dir}/workspace"
363
- [ ! -e "${home_dir}/workspace" ] && ln -s "$workspace_target" "${home_dir}/workspace" && chown -h "${user_name}:${user_name}" "${home_dir}/workspace"
364
- profile="${home_dir}/.profile"
365
- [ ! -f "$profile" ] && touch "$profile" && chown "${user_name}:${user_name}" "$profile"
366
- grep -q 'cd.*workspace' "$profile" 2>/dev/null || echo '[ -d "$HOME/workspace" ] && cd "$HOME/workspace"' >> "$profile"
367
- bashrc="${home_dir}/.bashrc"
368
- [ ! -f "$bashrc" ] && touch "$bashrc" && chown "${user_name}:${user_name}" "$bashrc"
369
- grep -q 'cd.*workspace' "$bashrc" 2>/dev/null || echo '[ -d "$HOME/workspace" ] && cd "$HOME/workspace"' >> "$bashrc"
370
- done
371
- APPLY_DEV_EOF
372
- fi
373
- chmod 755 "$APPLY_DEV_USERS_SCRIPT"
374
- fi
375
- # Defaults for apply script (aifabrix-secrets, aifabrix-env-config in generated config); match builder-server env.template
376
- APPLY_DEFAULTS="$DATA_DIR_ABS/apply-dev-users-defaults"
377
- if [ ! -f "$APPLY_DEFAULTS" ]; then
378
- {
379
- echo "# Sourced by cron before aifabrix-apply-dev-users.sh; override to match builder-server .env"
380
- echo 'export AIFABRIX_SECRETS="${AIFABRIX_SECRETS:-/aifabrix-miso/builder/secrets.local.yaml}"'
381
- echo 'export AIFABRIX_ENV_CONFIG="${AIFABRIX_ENV_CONFIG:-aifabrix-miso/builder/env-config.yaml}"'
382
- } > "$APPLY_DEFAULTS"
383
- chmod 644 "$APPLY_DEFAULTS"
384
- fi
385
- if [ -d /etc/cron.d ] && [ ! -f /etc/cron.d/aifabrix-apply-dev-users ]; then
386
- echo "*/2 * * * * root /bin/sh -c '. $APPLY_DEFAULTS 2>/dev/null; export DATA_DIR=$DATA_DIR_ABS; exec $APPLY_DEV_USERS_SCRIPT'" > /etc/cron.d/aifabrix-apply-dev-users
387
- chmod 644 /etc/cron.d/aifabrix-apply-dev-users
388
413
  fi
414
+ # --- End server phase ---
389
415
 
390
416
  echo "Setup complete. Ensure manual prerequisites (SSL at $SSL_DIR: wildcard.crt, wildcard.key; DNS $DEV_DOMAIN) are done; see SETUP.md."
@@ -0,0 +1,42 @@
1
+ #!/bin/sh
2
+ # One-time bootstrap: ensure SSH, install Node 18+ and npm, then af-server CLI on the server.
3
+ # Run via: af-server install-init user@host (from PC over SSH). No Docker, nginx, or builder-server.
4
+ # After this, user logs in to the server and runs: sudo af-server install, then sudo af-server install-server --dev-domain DOMAIN.
5
+
6
+ set -e
7
+ export DEBIAN_FRONTEND=noninteractive
8
+
9
+ # --- SSH server (so install-init and later ssh-cert install can connect) ---
10
+ wait_for_apt() {
11
+ i=0
12
+ while [ $i -lt 20 ]; do
13
+ apt-get update -qq 2>/dev/null && return 0
14
+ echo "Waiting for apt lock..."; sleep 6
15
+ i=$((i + 1))
16
+ done
17
+ return 1
18
+ }
19
+ wait_for_apt
20
+ apt-get install -y openssh-server
21
+ systemctl enable ssh 2>/dev/null || systemctl enable sshd 2>/dev/null || true
22
+ systemctl start ssh 2>/dev/null || systemctl start sshd 2>/dev/null || true
23
+
24
+ # --- Node.js 18+ and npm (NodeSource) ---
25
+ need_node=0
26
+ if ! command -v node >/dev/null 2>&1; then
27
+ need_node=1
28
+ else
29
+ case "$(node -v 2>/dev/null)" in v1[89].*|v[2-9]*) ;; *) need_node=1 ;; esac
30
+ fi
31
+ if [ "$need_node" = "1" ]; then
32
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
33
+ apt-get install -y nodejs
34
+ fi
35
+ node -v
36
+ npm -v
37
+
38
+ # --- af-server CLI (same as on PC) ---
39
+ npm install -g @aifabrix/builder @aifabrix/server-setup
40
+ command -v af-server >/dev/null 2>&1 && af-server --version || true
41
+
42
+ echo "Bootstrap complete. Log in to the server and run: sudo af-server install, then sudo af-server install-server --dev-domain YOUR_DOMAIN"
package/dist/cli.js CHANGED
@@ -3,22 +3,42 @@
3
3
  * af-server — Install, backup, and restore AI Fabrix builder-server (config + DB) over SSH.
4
4
  * Usage: af-server <command> [options] [user@host]
5
5
  */
6
+ import { readFileSync } from 'fs';
7
+ import { dirname, join } from 'path';
8
+ import { fileURLToPath } from 'url';
6
9
  import { Command } from 'commander';
7
- import { runInstall, runInstallLocal } from './install.js';
10
+ import { runInstall, runInstallLocal, runInstallServerLocal } from './install.js';
8
11
  import { runBackup, runBackupLocal } from './backup.js';
9
12
  import { runBackupScheduleInstall, runBackupScheduleInstallLocal } from './backup-schedule.js';
10
13
  import { runRestore, runRestoreLocal } from './restore.js';
11
14
  import { runSshCertRequest, runSshCertInstall, runSshCertInstallLocal } from './ssh-cert.js';
12
15
  import { runInstallSsh, runInstallSshLocal } from './install-ssh.js';
16
+ import { runInstallInit } from './install-init.js';
13
17
  import { requireUbuntu } from './ubuntu.js';
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
14
20
  const program = new Command();
15
21
  program
16
22
  .name('af-server')
17
23
  .description('Install, backup, and restore AI Fabrix builder-server over SSH (config + DB only)')
18
- .version('0.1.0');
24
+ .version(pkg.version);
25
+ program
26
+ .command('install-init <user@host>')
27
+ .description('One-time bootstrap over SSH: install on server SSH (if needed), Node 18+, npm, and af-server CLI. Run from PC only.')
28
+ .option('-i, --identity <path>', 'SSH private key path')
29
+ .action(async (target, opts) => {
30
+ try {
31
+ await runInstallInit({ target: target.trim(), privateKeyPath: opts.identity });
32
+ console.log('Install-init complete. Log in to the server and run: sudo af-server install, then sudo af-server install-server --dev-domain YOUR_DOMAIN');
33
+ }
34
+ catch (err) {
35
+ console.error(err instanceof Error ? err.message : err);
36
+ process.exit(1);
37
+ }
38
+ });
19
39
  program
20
40
  .command('install [user@host]')
21
- .description('Run server setup on host or locally (omit target on server). Docker, nginx, SSL, cron.')
41
+ .description('Run server setup on host or locally (omit target on server). Docker, nginx pkg, Mutagen, cron; no builder-server. Run on server: sudo af-server install.')
22
42
  .option('-d, --data-dir <path>', 'DATA_DIR', '/opt/aifabrix/builder-server/data')
23
43
  .option('--dev-domain <domain>', 'DEV_DOMAIN for nginx', 'builder01.aifabrix.dev')
24
44
  .option('--ssl-dir <path>', 'SSL_DIR', '/opt/aifabrix/ssl')
@@ -52,6 +72,29 @@ program
52
72
  process.exit(1);
53
73
  }
54
74
  });
75
+ program
76
+ .command('install-server')
77
+ .description('On server only: nginx vhost, builder-server container, Docker TLS. Run after: sudo af-server install. Requires --dev-domain.')
78
+ .option('-d, --data-dir <path>', 'DATA_DIR', '/opt/aifabrix/builder-server/data')
79
+ .requiredOption('--dev-domain <domain>', 'DEV_DOMAIN for nginx (e.g. builder01.aifabrix.dev)')
80
+ .option('--ssl-dir <path>', 'SSL_DIR', '/opt/aifabrix/ssl')
81
+ .option('--builder-port <port>', 'BUILDER_SERVER_PORT', '3000')
82
+ .action(async (opts) => {
83
+ try {
84
+ requireUbuntu();
85
+ runInstallServerLocal({
86
+ dataDir: opts.dataDir,
87
+ devDomain: opts.devDomain,
88
+ sslDir: opts.sslDir,
89
+ builderServerPort: opts.builderPort ? parseInt(opts.builderPort, 10) : undefined,
90
+ });
91
+ console.log('Install-server complete.');
92
+ }
93
+ catch (err) {
94
+ console.error(err instanceof Error ? err.message : err);
95
+ process.exit(1);
96
+ }
97
+ });
55
98
  program
56
99
  .command('backup [user@host]')
57
100
  .description('On-demand backup, or --schedule to install cron (omit target for local).')
@@ -0,0 +1,6 @@
1
+ /**
2
+ * install-init: One-time bootstrap over SSH. Installs on the server: SSH (if needed), Node 18+, npm, and af-server CLI.
3
+ * No Docker, nginx, or builder-server. After this, the user logs in to the server and runs install + install-server locally.
4
+ */
5
+ import { type SSHConnectionOptions } from './ssh.js';
6
+ export declare function runInstallInit(options: SSHConnectionOptions): Promise<void>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * install-init: One-time bootstrap over SSH. Installs on the server: SSH (if needed), Node 18+, npm, and af-server CLI.
3
+ * No Docker, nginx, or builder-server. After this, the user logs in to the server and runs install + install-server locally.
4
+ */
5
+ import * as path from 'path';
6
+ import * as fs from 'fs';
7
+ import { fileURLToPath } from 'url';
8
+ import { createSSHClient, exec, writeFile, close } from './ssh.js';
9
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
10
+ const ASSETS_DIR = path.resolve(scriptDir, '..', 'assets');
11
+ function toUnixLf(s) {
12
+ return s.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
13
+ }
14
+ function getInitScript() {
15
+ const p = path.join(ASSETS_DIR, 'setup-install-init.sh');
16
+ return toUnixLf(fs.readFileSync(p, 'utf8'));
17
+ }
18
+ export async function runInstallInit(options) {
19
+ const conn = await createSSHClient(options);
20
+ try {
21
+ const tmpDir = `/tmp/aifabrix-init-${Date.now()}`;
22
+ await exec(conn, `mkdir -p ${tmpDir}`);
23
+ await exec(conn, `chmod 755 ${tmpDir}`);
24
+ const script = getInitScript();
25
+ await writeFile(conn, `${tmpDir}/setup-install-init.sh`, script);
26
+ await exec(conn, `chmod +x ${tmpDir}/setup-install-init.sh`);
27
+ const cmd = `sudo ${tmpDir}/setup-install-init.sh`;
28
+ const result = await exec(conn, cmd);
29
+ if (result.stderr)
30
+ process.stderr.write(result.stderr);
31
+ if (result.stdout)
32
+ process.stdout.write(result.stdout);
33
+ if (result.code !== 0) {
34
+ throw new Error(`install-init script exited with code ${result.code}`);
35
+ }
36
+ await exec(conn, `rm -rf ${tmpDir}`);
37
+ }
38
+ finally {
39
+ close(conn);
40
+ }
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Tests for install-init: bootstrap over SSH (mocked).
3
+ * install-init.ts uses import.meta.url; Jest (CJS) fails to load it. Skipped until ESM/import.meta is supported in tests.
4
+ */
5
+ describe.skip('install-init', () => {
6
+ it('placeholder until Jest supports import.meta in transformed modules', () => {
7
+ expect(true).toBe(true);
8
+ });
9
+ });
10
+ export {};
package/dist/install.d.ts CHANGED
@@ -17,3 +17,6 @@ export interface InstallLocalOptions {
17
17
  export declare function runInstall(options: InstallOptions): Promise<void>;
18
18
  /** Run setup script locally (no SSH). Call requireUbuntu() before this. */
19
19
  export declare function runInstallLocal(options?: InstallLocalOptions): void;
20
+ export declare function runInstallServer(options: InstallOptions): Promise<void>;
21
+ /** Run server phase locally (nginx vhost, builder-server container, Docker TLS). Call requireUbuntu() before this. */
22
+ export declare function runInstallServerLocal(options?: InstallLocalOptions): void;
package/dist/install.js CHANGED
@@ -39,6 +39,7 @@ export async function runInstall(options) {
39
39
  const q = (v) => `'${String(v).replace(/'/g, "'\\''")}'`;
40
40
  const env = [
41
41
  `REPO_ROOT=${q(tmpDir)}`,
42
+ `INSTALL_PHASE=infra`,
42
43
  `DATA_DIR=${q(dataDir)}`,
43
44
  `DEV_DOMAIN=${q(devDomain)}`,
44
45
  `SSL_DIR=${q(sslDir)}`,
@@ -71,7 +72,65 @@ export function runInstallLocal(options = {}) {
71
72
  try {
72
73
  fs.writeFileSync(path.join(tmpDir, 'setup.sh'), getSetupScript(), { mode: 0o755 });
73
74
  fs.writeFileSync(path.join(builderSubdir, 'nginx-builder-server.conf.template'), getNginxTemplate());
74
- const env = [`REPO_ROOT=${tmpDir}`, `DATA_DIR=${dataDir}`, `DEV_DOMAIN=${devDomain}`, `SSL_DIR=${sslDir}`, `BUILDER_SERVER_PORT=${builderServerPort}`].join(' ');
75
+ const env = [`REPO_ROOT=${tmpDir}`, `INSTALL_PHASE=infra`, `DATA_DIR=${dataDir}`, `DEV_DOMAIN=${devDomain}`, `SSL_DIR=${sslDir}`, `BUILDER_SERVER_PORT=${builderServerPort}`].join(' ');
76
+ execSync(`sudo ${env} ${tmpDir}/setup.sh`, { stdio: 'inherit' });
77
+ }
78
+ finally {
79
+ fs.rmSync(tmpDir, { recursive: true, force: true });
80
+ }
81
+ }
82
+ export async function runInstallServer(options) {
83
+ const dataDir = options.dataDir ?? '/opt/aifabrix/builder-server/data';
84
+ const devDomain = options.devDomain ?? 'builder01.aifabrix.dev';
85
+ const sslDir = options.sslDir ?? '/opt/aifabrix/ssl';
86
+ const builderServerPort = options.builderServerPort ?? 3000;
87
+ const conn = await createSSHClient(options);
88
+ try {
89
+ const tmpDir = `/tmp/aifabrix-setup-${Date.now()}`;
90
+ await exec(conn, `mkdir -p ${tmpDir}/builder/builder-server`);
91
+ await exec(conn, `chmod 755 ${tmpDir}`);
92
+ const setupScript = getSetupScript();
93
+ const nginxTemplate = getNginxTemplate();
94
+ await writeFile(conn, `${tmpDir}/setup.sh`, setupScript);
95
+ await writeFile(conn, `${tmpDir}/builder/builder-server/nginx-builder-server.conf.template`, nginxTemplate);
96
+ await exec(conn, `chmod +x ${tmpDir}/setup.sh`);
97
+ const q = (v) => `'${String(v).replace(/'/g, "'\\''")}'`;
98
+ const env = [
99
+ `REPO_ROOT=${q(tmpDir)}`,
100
+ `INSTALL_PHASE=server`,
101
+ `DATA_DIR=${q(dataDir)}`,
102
+ `DEV_DOMAIN=${q(devDomain)}`,
103
+ `SSL_DIR=${q(sslDir)}`,
104
+ `BUILDER_SERVER_PORT=${q(String(builderServerPort))}`,
105
+ ].join(' ');
106
+ const cmd = `sudo ${env} ${tmpDir}/setup.sh`;
107
+ const result = await exec(conn, cmd);
108
+ if (result.stderr)
109
+ process.stderr.write(result.stderr);
110
+ if (result.stdout)
111
+ process.stdout.write(result.stdout);
112
+ if (result.code !== 0) {
113
+ throw new Error(`Install-server script exited with code ${result.code}`);
114
+ }
115
+ await exec(conn, `rm -rf ${tmpDir}`);
116
+ }
117
+ finally {
118
+ close(conn);
119
+ }
120
+ }
121
+ /** Run server phase locally (nginx vhost, builder-server container, Docker TLS). Call requireUbuntu() before this. */
122
+ export function runInstallServerLocal(options = {}) {
123
+ const dataDir = options.dataDir ?? '/opt/aifabrix/builder-server/data';
124
+ const devDomain = options.devDomain ?? 'builder01.aifabrix.dev';
125
+ const sslDir = options.sslDir ?? '/opt/aifabrix/ssl';
126
+ const builderServerPort = options.builderServerPort ?? 3000;
127
+ const tmpDir = `/tmp/aifabrix-setup-${Date.now()}`;
128
+ const builderSubdir = path.join(tmpDir, 'builder', 'builder-server');
129
+ fs.mkdirSync(builderSubdir, { recursive: true });
130
+ try {
131
+ fs.writeFileSync(path.join(tmpDir, 'setup.sh'), getSetupScript(), { mode: 0o755 });
132
+ fs.writeFileSync(path.join(builderSubdir, 'nginx-builder-server.conf.template'), getNginxTemplate());
133
+ const env = [`REPO_ROOT=${tmpDir}`, `INSTALL_PHASE=server`, `DATA_DIR=${dataDir}`, `DEV_DOMAIN=${devDomain}`, `SSL_DIR=${sslDir}`, `BUILDER_SERVER_PORT=${builderServerPort}`].join(' ');
75
134
  execSync(`sudo ${env} ${tmpDir}/setup.sh`, { stdio: 'inherit' });
76
135
  }
77
136
  finally {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tests for install: runInstall (infra phase) and runInstallServer / runInstallServerLocal (server phase).
3
+ * install.ts uses import.meta.url; Jest (CJS) fails to load it. Unit tests for runInstall* are skipped; we test script content.
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ const ASSETS_DIR = path.resolve(__dirname, '..', 'assets');
8
+ const SETUP_SCRIPT = path.join(ASSETS_DIR, 'setup-dev-server-no-node.sh');
9
+ describe('install script (setup-dev-server-no-node.sh)', () => {
10
+ it('defines INSTALL_PHASE and infra/server phase blocks', () => {
11
+ const content = fs.readFileSync(SETUP_SCRIPT, 'utf8');
12
+ expect(content).toContain('INSTALL_PHASE="${INSTALL_PHASE:-full}"');
13
+ expect(content).toContain('"$INSTALL_PHASE" = "infra"');
14
+ expect(content).toContain('"$INSTALL_PHASE" = "server"');
15
+ expect(content).toContain('End infra phase');
16
+ expect(content).toContain('End server phase');
17
+ });
18
+ });
19
+ describe.skip('install (runInstall* unit tests)', () => {
20
+ it('placeholder until Jest supports import.meta in transformed modules', () => {
21
+ expect(true).toBe(true);
22
+ });
23
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/server-setup",
3
- "version": "1.4.0",
3
+ "version": "1.5.2",
4
4
  "description": "CLI to install, backup, and restore AI Fabrix builder-server (config + DB) over SSH",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -46,5 +46,10 @@
46
46
  "files": [
47
47
  "dist",
48
48
  "assets"
49
- ]
49
+ ],
50
+ "pnpm": {
51
+ "onlyBuiltDependencies": [
52
+ "better-sqlite3"
53
+ ]
54
+ }
50
55
  }