@aifabrix/server-setup 1.5.2 → 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 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.
@@ -203,7 +205,7 @@ If you SSH as a non-root user, that user must be able to sudo. The script will a
203
205
  - **Admin user** — Adds the admin user (default `serveradmin`) to the `docker` group and grants passwordless sudo.
204
206
  - **Nginx** — Installs the nginx **package** only; enables and starts it. Does **not** write the builder vhost yet.
205
207
  - **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.
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.
207
209
  - **Mutagen** — Downloads and installs Mutagen; systemd service and daemon.
208
210
  - **Optional** — If `INSTALL_PORTAINER=1`, installs the Portainer container.
209
211
 
@@ -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
- # Requires root. DATA_DIR must match builder-server (e.g. /opt/aifabrix/builder-server/data).
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
- echo "DATA_DIR not found: $DATA_DIR"
18
- exit 0
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/server-setup",
3
- "version": "1.5.2",
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": "^11.6.0",
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",