@aifabrix/server-setup 1.4.0 → 1.5.3
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 +88 -58
- package/assets/aifabrix-apply-dev-users.sh +92 -6
- package/assets/setup-dev-server-no-node.sh +122 -96
- package/assets/setup-install-init.sh +42 -0
- package/dist/cli.js +46 -3
- package/dist/install-init.d.ts +6 -0
- package/dist/install-init.js +41 -0
- package/dist/install-init.spec.d.ts +1 -0
- package/dist/install-init.spec.js +10 -0
- package/dist/install.d.ts +3 -0
- package/dist/install.js +60 -1
- package/dist/install.spec.d.ts +1 -0
- package/dist/install.spec.js +23 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -17,7 +17,9 @@ This is the **one document** you need to get your builder-server **from zero to
|
|
|
17
17
|
**The builder-server Docker image is not on a public registry.** You get it with the **AI Fabrix platform**.
|
|
18
18
|
|
|
19
19
|
1. Install the **AI Fabrix Builder** CLI: `npm install -g @aifabrix/builder`
|
|
20
|
-
2. Install the **AI Fabrix server-setup** CLI (af-server): `npm install -g @aifabrix/server-setup`
|
|
20
|
+
2. Install the **AI Fabrix server-setup** CLI (af-server): `npm install -g @aifabrix/server-setup`
|
|
21
|
+
- If you get **EACCES** (permission denied), use either:
|
|
22
|
+
`sudo npm install -g @aifabrix/server-setup`
|
|
21
23
|
3. Use the Builder CLI to run or deploy the platform (including builder-server). See [AI Fabrix Builder](https://github.com/esystemsdev/aifabrix-builder) and [docs/README.md](https://github.com/esystemsdev/aifabrix-builder/blob/main/docs/README.md).
|
|
22
24
|
|
|
23
25
|
Once you have the platform (and the builder-server image), use **af-server** to install that server on your own host.
|
|
@@ -42,72 +44,93 @@ Do the steps in [What you must do before running af-server](#what-you-must-do-be
|
|
|
42
44
|
|
|
43
45
|
Complete the [manual prerequisites](#what-you-must-do-before-running-af-server) first (DNS, SSL directory, certificate and key on the server).
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
**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
48
|
|
|
47
|
-
|
|
49
|
+
| Step | Where | Action |
|
|
50
|
+
| ---- | --------- | ------ |
|
|
51
|
+
| 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. |
|
|
52
|
+
| 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`. |
|
|
53
|
+
| 3 | From PC | Copy SSL certificate and key to the server (see [SSL directory and certificates](#ssl-directory-and-certificates)); example commands below. |
|
|
54
|
+
| 4 | From PC | Log in to the server via SSH (passwordless). |
|
|
55
|
+
| 5 | **On server** | `sudo af-server install` — install all services (Docker, nginx package, Mutagen, cron, data dir); no builder-server container yet. |
|
|
56
|
+
| 6 | On server | Get the builder-server image (e.g. `az login` / `docker pull`). |
|
|
57
|
+
| 7 | **On server** | `sudo af-server install-server --dev-domain $DOMAIN` — nginx vhost, builder-server container, Docker TLS. |
|
|
58
|
+
| 8 | — | Done. |
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
npm install -g @aifabrix/builder
|
|
51
|
-
npm install -g @aifabrix/server-setup
|
|
52
|
-
```
|
|
60
|
+
### Step 1: Bootstrap the server (from PC)
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
Set your target and run the only command that uses SSH from your PC:
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
```bash
|
|
65
|
+
export SSH=serveradmin@builder02.aifabrix.dev
|
|
66
|
+
export DOMAIN=builder02.aifabrix.dev
|
|
67
|
+
af-server install-init $SSH
|
|
68
|
+
```
|
|
61
69
|
|
|
62
|
-
|
|
70
|
+
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
71
|
|
|
64
|
-
|
|
65
|
-
af-server install-ssh $SSH
|
|
66
|
-
```
|
|
72
|
+
### Step 2: Passwordless SSH (from PC)
|
|
67
73
|
|
|
68
|
-
|
|
74
|
+
Log in to the server once (e.g. with password) to accept the host key and/or approve auth. Then from your PC:
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
```bash
|
|
77
|
+
af-server ssh-cert install $SSH
|
|
78
|
+
```
|
|
73
79
|
|
|
74
|
-
|
|
80
|
+
### Step 3: Copy SSL (from PC)
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
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
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
```bash
|
|
85
|
+
export HDD=/workspace/aifabrix-setup/certificates
|
|
86
|
+
ssh $SSH "sudo mkdir -p /opt/aifabrix/ssl"
|
|
87
|
+
scp $HDD/wildcard.crt $SSH:/tmp/wildcard.crt
|
|
88
|
+
scp $HDD/wildcard.key $SSH:/tmp/wildcard.key
|
|
89
|
+
ssh $SSH "sudo mv /tmp/wildcard.crt /tmp/wildcard.key /opt/aifabrix/ssl/ && sudo chmod 600 /opt/aifabrix/ssl/wildcard.key"
|
|
90
|
+
```
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
### Step 4: Log in to the server
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
```bash
|
|
95
|
+
ssh $SSH
|
|
96
|
+
```
|
|
90
97
|
|
|
91
|
-
|
|
98
|
+
### Step 5: Install services (on server)
|
|
92
99
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
```bash
|
|
101
|
+
sudo af-server install
|
|
102
|
+
```
|
|
96
103
|
|
|
97
|
-
|
|
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'`).
|
|
104
|
+
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
105
|
|
|
100
|
-
###
|
|
106
|
+
### Step 6: Get the builder-server image (on server)
|
|
101
107
|
|
|
102
|
-
|
|
108
|
+
The image is not on a public registry. Use your platform’s method (e.g. Azure CLI or Docker login), then pull. Example:
|
|
103
109
|
|
|
104
110
|
```bash
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
az login
|
|
112
|
+
az acr login --name aifabrixdevacr
|
|
113
|
+
docker pull aifabrixdevacr.azurecr.io/aifabrix/builder-server:latest
|
|
108
114
|
```
|
|
109
115
|
|
|
110
|
-
|
|
116
|
+
Or with Docker login (username/password from your registry):
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
docker login <registry> -u <user> -p <password>
|
|
120
|
+
docker pull <registry>/aifabrix/builder-server:latest
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Step 7: Install server (nginx vhost + container) (on server)
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
sudo af-server install-server --dev-domain $DOMAIN
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
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`.
|
|
130
|
+
|
|
131
|
+
### Step 8: Done
|
|
132
|
+
|
|
133
|
+
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
134
|
|
|
112
135
|
---
|
|
113
136
|
|
|
@@ -115,7 +138,9 @@ After install, your builder-server is up. Use the **AI Fabrix Builder CLI** for
|
|
|
115
138
|
|
|
116
139
|
| Command | Description |
|
|
117
140
|
| -------- | ----------- |
|
|
118
|
-
| `af-server install
|
|
141
|
+
| `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. |
|
|
142
|
+
| `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. |
|
|
143
|
+
| `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
144
|
| `af-server backup [ user@host ] [ -d DATA_DIR ] [ -o output.zip ] [ -i SSH_KEY ]` | On-demand backup (config + DB + keys). |
|
|
120
145
|
| `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
146
|
| `af-server restore backup.zip [ user@host ] [ -d DATA_DIR ] [ --force ] [ -i SSH_KEY ]` | Restore backup to DATA_DIR. |
|
|
@@ -171,18 +196,23 @@ If you SSH as a non-root user, that user must be able to sudo. The script will a
|
|
|
171
196
|
|
|
172
197
|
---
|
|
173
198
|
|
|
174
|
-
## High level: what
|
|
199
|
+
## High level: what install vs install-server does
|
|
200
|
+
|
|
201
|
+
**`af-server install`** (step 5 — run on the server) does **infra only**:
|
|
202
|
+
|
|
203
|
+
- **System** — `apt update` and `apt upgrade`; optional hostname if `SETUP_HOSTNAME` is set.
|
|
204
|
+
- **Docker** — Installs Docker if missing; enables and starts it.
|
|
205
|
+
- **Admin user** — Adds the admin user (default `serveradmin`) to the `docker` group and grants passwordless sudo.
|
|
206
|
+
- **Nginx** — Installs the nginx **package** only; enables and starts it. Does **not** write the builder vhost yet.
|
|
207
|
+
- **Data dir** — Creates the data directory (default `/opt/aifabrix/builder-server/data`), workspace and ssh-keys subdirs, ownership for the container.
|
|
208
|
+
- **Apply-dev-users** — Installs the script and cron job (every 2 minutes) that sync per-developer OS users from builder-server state. The script configures a user-writable npm/pnpm prefix (`~/.local`) for dev users and the aifabrix user so they can run `npm install -g` and `pnpm add -g` without sudo. When run as root, it can also grant passwordless sudo to the aifabrix user (override with `SUDO_NOPASSWD_USER` in apply-dev-users-defaults). **Without sudo:** run the script as the current user (e.g. `SUDO_NOPASSWD_USER=aifabrix`); it will only set up that user's `~/.local` and `~/.npmrc`/`~/.pnpmrc` so npm and pnpm global installs work without sudo.
|
|
209
|
+
- **Mutagen** — Downloads and installs Mutagen; systemd service and daemon.
|
|
210
|
+
- **Optional** — If `INSTALL_PORTAINER=1`, installs the Portainer container.
|
|
175
211
|
|
|
176
|
-
|
|
212
|
+
**`af-server install-server`** (step 7 — run on the server) does the **server phase**:
|
|
177
213
|
|
|
178
|
-
- **
|
|
179
|
-
- **
|
|
180
|
-
- **
|
|
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.
|
|
214
|
+
- **Nginx vhost** — Writes the builder site config from template (domain, SSL dir, proxy to builder-server), reloads nginx.
|
|
215
|
+
- **Builder-server container** — Creates data dir (if needed), starts the builder-server container (if the image is present).
|
|
216
|
+
- **Docker TLS** — Copies certs and configures `/etc/docker/daemon.json` for TLS (using website cert and builder-server CA).
|
|
187
217
|
|
|
188
|
-
Result:
|
|
218
|
+
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.
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
# Apply per-developer OS users from builder-server state (DATA_DIR/ssh-keys, pending-removals).
|
|
3
3
|
# Run via cron every 2 minutes. Creates/updates dev<id> users, .ssh/authorized_keys, workspace symlink,
|
|
4
4
|
# and optional ~/.aifabrix/config.yaml when missing. Processes pending-removals then applies keys.
|
|
5
|
-
#
|
|
5
|
+
# Also: npm/pnpm user prefix (~/.local) for dev users so they can install global packages without sudo;
|
|
6
|
+
# optional passwordless sudo for SUDO_NOPASSWD_USER (only when run as root; skipped without sudo).
|
|
7
|
+
# Run as root for full sync (useradd, dev users, etc.). When run without root as SUDO_NOPASSWD_USER,
|
|
8
|
+
# only the current user's npm/pnpm prefix is configured so they can install global packages without sudo.
|
|
9
|
+
# DATA_DIR must match builder-server (e.g. /opt/aifabrix/builder-server/data).
|
|
6
10
|
|
|
7
11
|
set -e
|
|
8
12
|
|
|
@@ -12,10 +16,36 @@ PENDING_REMOVALS="${DATA_DIR}/pending-removals"
|
|
|
12
16
|
# Defaults matching builder-server env.template (override via env or apply-dev-users-defaults)
|
|
13
17
|
AIFABRIX_SECRETS="${AIFABRIX_SECRETS:-/aifabrix-miso/builder/secrets.local.yaml}"
|
|
14
18
|
AIFABRIX_ENV_CONFIG="${AIFABRIX_ENV_CONFIG:-aifabrix-miso/builder/env-config.yaml}"
|
|
19
|
+
# User that gets npm/pnpm prefix when run as that user without root; passwordless sudo only when root.
|
|
20
|
+
SUDO_NOPASSWD_USER="${SUDO_NOPASSWD_USER:-aifabrix}"
|
|
15
21
|
|
|
22
|
+
# Only root can write sudoers and manage dev users; without root we only set up current user's npm/pnpm.
|
|
23
|
+
ROOT_OK=0
|
|
24
|
+
[ "$(id -u)" = 0 ] && ROOT_OK=1
|
|
25
|
+
|
|
26
|
+
# When not root, current user can run script to set up only their npm/pnpm prefix (DATA_DIR not required).
|
|
16
27
|
if [ ! -d "$DATA_DIR" ]; then
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
if [ "$ROOT_OK" = 1 ] || [ "$(id -un)" != "$SUDO_NOPASSWD_USER" ]; then
|
|
29
|
+
echo "DATA_DIR not found: $DATA_DIR"
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# --- 0. Passwordless sudo for SUDO_NOPASSWD_USER (only when run as root; skip when no sudo access) ---
|
|
35
|
+
if [ "$ROOT_OK" = 1 ]; then
|
|
36
|
+
case "$SUDO_NOPASSWD_USER" in
|
|
37
|
+
*'..'*|*'/'*|*';'*|*'|'*|*'&'*|*'$'*|*'`'*|*' '*)
|
|
38
|
+
;;
|
|
39
|
+
*)
|
|
40
|
+
if [ -n "$SUDO_NOPASSWD_USER" ] && getent passwd "$SUDO_NOPASSWD_USER" >/dev/null 2>&1; then
|
|
41
|
+
SUDOERS_FILE="/etc/sudoers.d/99-nopasswd-${SUDO_NOPASSWD_USER}"
|
|
42
|
+
if [ ! -f "$SUDOERS_FILE" ]; then
|
|
43
|
+
echo "${SUDO_NOPASSWD_USER} ALL=(ALL) NOPASSWD:ALL" > "$SUDOERS_FILE"
|
|
44
|
+
chmod 440 "$SUDOERS_FILE"
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
;;
|
|
48
|
+
esac
|
|
19
49
|
fi
|
|
20
50
|
|
|
21
51
|
# Read secrets-encryption key from server data dir (same as builder-server ENCRYPTION_KEY_PATH); do not log.
|
|
@@ -24,8 +54,8 @@ if [ -f "${DATA_DIR}/secrets-encryption.key" ]; then
|
|
|
24
54
|
secrets_encryption_value=$(cat "${DATA_DIR}/secrets-encryption.key" | tr -d '\n\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
25
55
|
fi
|
|
26
56
|
|
|
27
|
-
# --- 1. Process removals ---
|
|
28
|
-
if [ -f "$PENDING_REMOVALS" ]; then
|
|
57
|
+
# --- 1. Process removals (root only) ---
|
|
58
|
+
if [ "$ROOT_OK" = 1 ] && [ -f "$PENDING_REMOVALS" ]; then
|
|
29
59
|
while IFS= read -r dev_id || [ -n "$dev_id" ]; do
|
|
30
60
|
dev_id=$(echo "$dev_id" | tr -d '\r\n ')
|
|
31
61
|
[ -z "$dev_id" ] && continue
|
|
@@ -37,7 +67,8 @@ if [ -f "$PENDING_REMOVALS" ]; then
|
|
|
37
67
|
: > "$PENDING_REMOVALS"
|
|
38
68
|
fi
|
|
39
69
|
|
|
40
|
-
# --- 2. Process each developer that has keys ---
|
|
70
|
+
# --- 2. Process each developer that has keys (root only) ---
|
|
71
|
+
if [ "$ROOT_OK" = 1 ]; then
|
|
41
72
|
for key_file in "${SSH_KEYS_DIR}"/*/authorized_keys; do
|
|
42
73
|
[ -f "$key_file" ] || continue
|
|
43
74
|
dev_id=$(dirname "$key_file" | xargs basename)
|
|
@@ -120,4 +151,59 @@ for key_file in "${SSH_KEYS_DIR}"/*/authorized_keys; do
|
|
|
120
151
|
echo '[ -d "$HOME/workspace" ] && cd "$HOME/workspace"' >> "$bashrc"
|
|
121
152
|
chown "${user_name}:${user_name}" "$bashrc"
|
|
122
153
|
fi
|
|
154
|
+
|
|
155
|
+
# npm/pnpm: user install prefix so dev users can run npm install -g, pnpm install, pnpm setup without sudo
|
|
156
|
+
mkdir -p "${home_dir}/.local/bin"
|
|
157
|
+
chown -R "${user_name}:${user_name}" "${home_dir}/.local"
|
|
158
|
+
printf 'prefix=%s/.local\n' "$home_dir" > "${home_dir}/.npmrc"
|
|
159
|
+
chown "${user_name}:${user_name}" "${home_dir}/.npmrc"
|
|
160
|
+
if [ ! -f "${home_dir}/.pnpmrc" ] || ! grep -q '^global-bin-dir=' "${home_dir}/.pnpmrc" 2>/dev/null; then
|
|
161
|
+
printf 'global-bin-dir=%s/.local/bin\n' "$home_dir" >> "${home_dir}/.pnpmrc"
|
|
162
|
+
chown "${user_name}:${user_name}" "${home_dir}/.pnpmrc"
|
|
163
|
+
fi
|
|
164
|
+
for f in "$profile" "$bashrc"; do
|
|
165
|
+
if ! grep -q '\.local/bin' "$f" 2>/dev/null; then
|
|
166
|
+
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$f"
|
|
167
|
+
chown "${user_name}:${user_name}" "$f"
|
|
168
|
+
fi
|
|
169
|
+
done
|
|
123
170
|
done
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# --- 3. npm/pnpm prefix so SUDO_NOPASSWD_USER can install global packages without sudo ---
|
|
174
|
+
# When root: set up that user's home. When not root: set up current user's home (no chown).
|
|
175
|
+
setup_npm_pnpm_prefix() {
|
|
176
|
+
_home="$1"
|
|
177
|
+
_user="$2"
|
|
178
|
+
_use_chown="$3"
|
|
179
|
+
mkdir -p "${_home}/.local/bin"
|
|
180
|
+
[ "$_use_chown" = 1 ] && chown -R "${_user}:${_user}" "${_home}/.local"
|
|
181
|
+
_npmrc="${_home}/.npmrc"
|
|
182
|
+
if [ ! -f "$_npmrc" ] || ! grep -q '^prefix=' "$_npmrc" 2>/dev/null; then
|
|
183
|
+
printf 'prefix=%s/.local\n' "$_home" >> "$_npmrc"
|
|
184
|
+
[ "$_use_chown" = 1 ] && chown "${_user}:${_user}" "$_npmrc"
|
|
185
|
+
fi
|
|
186
|
+
for _f in "${_home}/.profile" "${_home}/.bashrc"; do
|
|
187
|
+
if [ -f "$_f" ] && ! grep -q '\.local/bin' "$_f" 2>/dev/null; then
|
|
188
|
+
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$_f"
|
|
189
|
+
[ "$_use_chown" = 1 ] && chown "${_user}:${_user}" "$_f"
|
|
190
|
+
fi
|
|
191
|
+
done
|
|
192
|
+
_pnpmrc="${_home}/.pnpmrc"
|
|
193
|
+
if [ ! -f "$_pnpmrc" ] || ! grep -q '^global-bin-dir=' "$_pnpmrc" 2>/dev/null; then
|
|
194
|
+
printf 'global-bin-dir=%s/.local/bin\n' "$_home" >> "$_pnpmrc"
|
|
195
|
+
[ "$_use_chown" = 1 ] && chown "${_user}:${_user}" "$_pnpmrc"
|
|
196
|
+
fi
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case "$SUDO_NOPASSWD_USER" in
|
|
200
|
+
*'..'*|*'/'*|*';'*|*'|'*|*'&'*|*'$'*|*'`'*|*' '*) ;;
|
|
201
|
+
*)
|
|
202
|
+
if [ "$ROOT_OK" = 1 ] && [ -n "$SUDO_NOPASSWD_USER" ] && getent passwd "$SUDO_NOPASSWD_USER" >/dev/null 2>&1; then
|
|
203
|
+
admin_home=$(getent passwd "$SUDO_NOPASSWD_USER" | cut -d: -f6)
|
|
204
|
+
[ -n "$admin_home" ] && [ -d "$admin_home" ] && setup_npm_pnpm_prefix "$admin_home" "$SUDO_NOPASSWD_USER" 1
|
|
205
|
+
elif [ "$ROOT_OK" = 0 ] && [ -n "$SUDO_NOPASSWD_USER" ] && [ "$(id -un)" = "$SUDO_NOPASSWD_USER" ]; then
|
|
206
|
+
[ -d "$HOME" ] && setup_npm_pnpm_prefix "$HOME" "$(id -un)" 0
|
|
207
|
+
fi
|
|
208
|
+
;;
|
|
209
|
+
esac
|
|
@@ -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(
|
|
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,
|
|
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.
|
|
3
|
+
"version": "1.5.3",
|
|
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",
|
|
@@ -21,12 +21,15 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"archiver": "^7.0.1",
|
|
24
|
-
"better-sqlite3": "^
|
|
24
|
+
"better-sqlite3": "^12.0.0",
|
|
25
25
|
"commander": "^12.1.0",
|
|
26
26
|
"extract-zip": "^2.0.1",
|
|
27
27
|
"read": "^1.0.7",
|
|
28
28
|
"ssh2": "^1.15.0"
|
|
29
29
|
},
|
|
30
|
+
"overrides": {
|
|
31
|
+
"glob": "^13.0.0"
|
|
32
|
+
},
|
|
30
33
|
"devDependencies": {
|
|
31
34
|
"@eslint/js": "^9.17.0",
|
|
32
35
|
"@types/archiver": "^6.0.2",
|
|
@@ -46,5 +49,10 @@
|
|
|
46
49
|
"files": [
|
|
47
50
|
"dist",
|
|
48
51
|
"assets"
|
|
49
|
-
]
|
|
52
|
+
],
|
|
53
|
+
"pnpm": {
|
|
54
|
+
"onlyBuiltDependencies": [
|
|
55
|
+
"better-sqlite3"
|
|
56
|
+
]
|
|
57
|
+
}
|
|
50
58
|
}
|