From 9c353f5f0da02976ded0eb8cf550ea976b77939b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 12 Aug 2026 04:02:19 +0000 Subject: [PATCH] move host setup into scripts/setup/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/ was holding two unrelated kinds of thing: install-this-machine, and run-this-occasionally. The eight installers now live in scripts/setup/; what stays at the top level is the build steps (gen-index, prebuild, build/) and the two maintenance scripts (reindex-music, rebuild-soulseek-tree). The move is not just a rename. Three of these derive the repo root from their own location: setup.sh:51 PROJECT_DIR="$(dirname "$SCRIPT_DIR")" setup_mac_light.sh:51 same cleanup-desktop.sh:134 ENV_FILE="$(dirname "$0")/../.env" Left alone, all three would now resolve to scripts/ — and nothing downstream complains. PROJECT_DIR is where .env is written, where `bun install`, `gen:index` and `db:push` run, and what pm2 is pointed at, so a fresh install would have quietly provisioned scripts/ and reported success. cleanup-desktop.sh fails the other way: it would find no .env, print "No .env — skipping", and leave the real VNC_PASSWORD in the real file. All three are now `../..` with a comment saying why the level matters. provision-user-dirs.ts imports data-path.ts relatively; that one tsgo caught. Also disambiguated `setup.sh` where it had become two files. app-store/templates//setup.sh is a per-sidecar installer with its own contract, and preflight.ts + docs/sidecar-app-store.md discussed both in the same paragraph. The host one is now spelled with its full path at those sites. Verified: bash -n on all six shell scripts, tsgo clean, os-user tests pass, both derivations resolve to the repo root, starship.toml still resolves from os-user-shell.ts, and provision-user-dirs.ts runs under DRY_RUN. Co-Authored-By: Claude Opus 5 --- TODO.md | 2 +- docs/per-user-linux-accounts.md | 2 +- docs/sidecar-app-store.md | 10 +++++++--- ecosystem.light.config.cjs | 2 +- ecosystem.mac.light.config.cjs | 2 +- package.json | 2 +- scripts/{ => setup}/cleanup-desktop.sh | 6 ++++-- scripts/{ => setup}/officer-set-display.sh | 0 scripts/{ => setup}/provision-user-dirs.ts | 8 ++++---- scripts/{ => setup}/setup-desktop.sh | 2 +- scripts/{ => setup}/setup-dockers.sh | 4 ++-- scripts/{ => setup}/setup.sh | 11 +++++++---- scripts/{ => setup}/setup_mac_light.sh | 15 +++++++++------ scripts/{ => setup}/starship.toml | 0 src/servers/app-store/preflight.ts | 7 ++++--- src/servers/data-path.ts | 2 +- src/servers/os-user-claude.ts | 4 ++-- src/servers/os-user-shell.ts | 4 ++-- 18 files changed, 48 insertions(+), 35 deletions(-) rename scripts/{ => setup}/cleanup-desktop.sh (96%) rename scripts/{ => setup}/officer-set-display.sh (100%) rename scripts/{ => setup}/provision-user-dirs.ts (88%) rename scripts/{ => setup}/setup-desktop.sh (99%) rename scripts/{ => setup}/setup-dockers.sh (98%) rename scripts/{ => setup}/setup.sh (98%) rename scripts/{ => setup}/setup_mac_light.sh (97%) rename scripts/{ => setup}/starship.toml (100%) diff --git a/TODO.md b/TODO.md index 4720814c..25930baa 100644 --- a/TODO.md +++ b/TODO.md @@ -52,7 +52,7 @@ the owner's OS user and can never be granted. Indirection there really is accide `'Unverified'` and `signin.ts` refuses anything else with a bare UNAUTHORIZED, which is the trap the hand-INSERT route fell into. The owner sets the password and reads it out; `passwordChangedAt` stays null. Directories come from the shared `provisionUserDirs`/`USER_DIRS` in `data-path.ts`, - which `scripts/provision-user-dirs.ts` now imports rather than restating. + which `scripts/setup/provision-user-dirs.ts` now imports rather than restating. - [ ] **Still no invite flow, and no password reset for a member.** The owner types the password and tells the person, which means the owner knows it and the member cannot change it back if they diff --git a/docs/per-user-linux-accounts.md b/docs/per-user-linux-accounts.md index 33a99dd1..4fcef7f3 100644 --- a/docs/per-user-linux-accounts.md +++ b/docs/per-user-linux-accounts.md @@ -146,7 +146,7 @@ test (`os-user.test.ts` → "does not pass the platform environment through"). sudoers *policy* — `env_reset`, `env_keep`, `always_set_home` — and "which variables cross into a member's shell" must not depend on a config file someone may have edited. -Root is available: `scripts/setup.sh` §4 installs `/etc/sudoers.d/officer-service` granting the service +Root is available: `scripts/setup/setup.sh` §4 installs `/etc/sudoers.d/officer-service` granting the service user `NOPASSWD: ALL` on the full profile. The light profile deliberately skips it, so a light install that wants OS users needs a **narrow** entry — `useradd`, `chown`, `setpriv` — which is better than the blanket rule anyway. diff --git a/docs/sidecar-app-store.md b/docs/sidecar-app-store.md index dbbff301..9b0caebb 100644 --- a/docs/sidecar-app-store.md +++ b/docs/sidecar-app-store.md @@ -107,9 +107,13 @@ consequences, both wanted: ### Docker is assumed, and nothing guarantees it -Verified: **nothing in `scripts/` installs Docker, and nothing checks for it.** `setup.sh` calls -`setup-dockers.sh`, which invokes `docker compose` with no preflight, so a fresh host without Docker -fails partway through setup with a bare "command not found". +Verified: **nothing in `scripts/` installs Docker, and nothing checks for it.** The host installer +`scripts/setup/setup.sh` calls `scripts/setup/setup-dockers.sh`, which invokes `docker compose` with no +preflight, so a fresh host without Docker fails partway through setup with a bare "command not found". + +(Not to be confused with the per-template `setup.sh` below — `app-store/templates//setup.sh` — which +is a different file with a different contract. The host one provisions the machine; a template one +provisions a single sidecar.) That is the seam where this project's origin shows — it began as one person's own machine, provisioned by his own scripts, where Docker was simply always there. diff --git a/ecosystem.light.config.cjs b/ecosystem.light.config.cjs index 38dd3d79..64522568 100644 --- a/ecosystem.light.config.cjs +++ b/ecosystem.light.config.cjs @@ -1,7 +1,7 @@ // Linux light profile — the platform without the self-hosted estate around it. // // For a machine that should run the file browser, the terminal and Claude/opencode chat, and nothing -// else. Paired with `OFFICER_PROFILE=light bash scripts/setup.sh`, which installs only what these +// else. Paired with `OFFICER_PROFILE=light bash scripts/setup/setup.sh`, which installs only what these // processes need: node, bun, ffmpeg, Postgres, pm2 and the two agent CLIs. // // This is a subset of ecosystem.config.cjs, not a copy of it — see ecosystem.profile.cjs for why, and diff --git a/ecosystem.mac.light.config.cjs b/ecosystem.mac.light.config.cjs index 186f0cbd..3b68b682 100644 --- a/ecosystem.mac.light.config.cjs +++ b/ecosystem.mac.light.config.cjs @@ -1,6 +1,6 @@ // macOS light profile — the same process set as the Linux light profile, on a laptop. // -// Paired with scripts/setup_mac_light.sh. Runs the file browser, the terminal and Claude/opencode +// Paired with scripts/setup/setup_mac_light.sh. Runs the file browser, the terminal and Claude/opencode // chat; nothing else. // // This is a subset of ecosystem.config.cjs, not a copy of it. That distinction is here because of this diff --git a/package.json b/package.json index 40c4bed1..829662e4 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "format": "{ git diff --name-only HEAD -- 'src/**/*.ts' 'src/**/*.tsx'; git ls-files --others --exclude-standard -- 'src/**/*.ts' 'src/**/*.tsx'; } | xargs -r prettier --write", "format:all": "prettier --write \"src/**/*.{ts,tsx}\"", "format:check": "prettier --check \"src/**/*.{ts,tsx}\"", - "setup": "bash scripts/setup.sh" + "setup": "bash scripts/setup/setup.sh" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.41", diff --git a/scripts/cleanup-desktop.sh b/scripts/setup/cleanup-desktop.sh similarity index 96% rename from scripts/cleanup-desktop.sh rename to scripts/setup/cleanup-desktop.sh index d15803ec..54063436 100755 --- a/scripts/cleanup-desktop.sh +++ b/scripts/setup/cleanup-desktop.sh @@ -26,7 +26,7 @@ set -euo pipefail # their profile in it. Opt in rather than surprise someone. # # Usage: -# bash scripts/cleanup-desktop.sh [--purge-brave] +# bash scripts/setup/cleanup-desktop.sh [--purge-brave] # # Reboot afterwards: the GDM and GRUB changes are both boot-time. @@ -131,7 +131,9 @@ fi # --- Step 7: .env --- echo "[7/7] Cleaning .env..." -ENV_FILE="$(cd "$(dirname "$0")/.." && pwd)/.env" +# ../.. — this lives in scripts/setup/. A wrong level here is quiet in the other direction: the sed +# below simply finds no file, reports "No .env" and leaves the real VNC_PASSWORD in place. +ENV_FILE="$(cd "$(dirname "$0")/../.." && pwd)/.env" if [ -f "$ENV_FILE" ]; then sed -i '/^VNC_PASSWORD=/d; /^VNC_PORT=/d' "$ENV_FILE" echo " Removed VNC entries." diff --git a/scripts/officer-set-display.sh b/scripts/setup/officer-set-display.sh similarity index 100% rename from scripts/officer-set-display.sh rename to scripts/setup/officer-set-display.sh diff --git a/scripts/provision-user-dirs.ts b/scripts/setup/provision-user-dirs.ts similarity index 88% rename from scripts/provision-user-dirs.ts rename to scripts/setup/provision-user-dirs.ts index 0b576fa9..b0bb3677 100644 --- a/scripts/provision-user-dirs.ts +++ b/scripts/setup/provision-user-dirs.ts @@ -1,7 +1,7 @@ // Create the per-user root under DATA_PATH for the given accounts. // -// bun scripts/provision-user-dirs.ts a@b.com c@d.com -// DRY_RUN=1 bun scripts/provision-user-dirs.ts a@b.com +// bun scripts/setup/provision-user-dirs.ts a@b.com c@d.com +// DRY_RUN=1 bun scripts/setup/provision-user-dirs.ts a@b.com // // Takes emails as arguments rather than reading the user table: the directory layout does not depend // on the database, and keeping the DB out means this runs with nothing else up. At invite time the @@ -19,13 +19,13 @@ import { join } from 'node:path'; // The list and DATA_PATH itself come from the platform rather than being restated here. The owner's // create-account handler provisions the same skeleton, and a script that drifted from it would produce // accounts that differ by how they were made. Importing data-path.ts pulls in no database and no server. -import { DATA_PATH, USER_DIRS } from '../src/servers/data-path'; +import { DATA_PATH, USER_DIRS } from '../../src/servers/data-path'; const DRY_RUN = process.env.DRY_RUN === '1'; const emails = process.argv.slice(2).filter(Boolean); if (!emails.length) { - console.error('Usage: bun scripts/provision-user-dirs.ts [email...]'); + console.error('Usage: bun scripts/setup/provision-user-dirs.ts [email...]'); process.exit(2); } diff --git a/scripts/setup-desktop.sh b/scripts/setup/setup-desktop.sh similarity index 99% rename from scripts/setup-desktop.sh rename to scripts/setup/setup-desktop.sh index bb8f52a5..d1411d09 100755 --- a/scripts/setup-desktop.sh +++ b/scripts/setup/setup-desktop.sh @@ -7,7 +7,7 @@ set -euo pipefail # capture an Xorg server, NOT a Wayland compositor — so we install the full GNOME desktop but force GDM # onto the Xorg session (WaylandEnable=false). Auto-login is enabled so a user session owns :0 for the # mirror to attach to. Switching the display manager takes effect on the next reboot. -# Usage: ./scripts/setup-desktop.sh +# Usage: ./scripts/setup/setup-desktop.sh echo "=== Officer Remote Desktop Setup (Ubuntu GNOME on Xorg) ===" echo "" diff --git a/scripts/setup-dockers.sh b/scripts/setup/setup-dockers.sh similarity index 98% rename from scripts/setup-dockers.sh rename to scripts/setup/setup-dockers.sh index f5ebf69d..34bd57c8 100755 --- a/scripts/setup-dockers.sh +++ b/scripts/setup/setup-dockers.sh @@ -4,8 +4,8 @@ # Outputs parseable key=value lines to stdout; all prompts go to stderr. # # Usage: -# bash scripts/setup-dockers.sh -# eval "$(bash scripts/setup-dockers.sh)" +# bash scripts/setup/setup-dockers.sh +# eval "$(bash scripts/setup/setup-dockers.sh)" # # Environment overrides: # SETUP_DOCKER_SERVICES="1 2 3" — pre-select services (or "all"/"none") diff --git a/scripts/setup.sh b/scripts/setup/setup.sh similarity index 98% rename from scripts/setup.sh rename to scripts/setup/setup.sh index dccdc3c2..a8e27af9 100755 --- a/scripts/setup.sh +++ b/scripts/setup/setup.sh @@ -3,8 +3,8 @@ # Run once on a fresh Ubuntu/Debian host before launching the server. # # Usage: -# bash scripts/setup.sh # full server install -# OFFICER_PROFILE=light bash scripts/setup.sh # light install +# bash scripts/setup/setup.sh # full server install +# OFFICER_PROFILE=light bash scripts/setup/setup.sh # light install # # PROFILES # full Everything: the self-hosted estate, the remote desktop, the music/audio stack, the shell @@ -48,7 +48,10 @@ is_light() { [ "$OFFICER_PROFILE" = "light" ]; } if is_light; then ECOSYSTEM_FILE="ecosystem.light.config.cjs"; else ECOSYSTEM_FILE="ecosystem.config.cjs"; fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +# ../.. — this lives in scripts/setup/, so the repo root is two levels up, not one. Nothing here fails +# loudly if that is wrong: PROJECT_DIR is where .env is written, where `bun install` and `db:push` run and +# where pm2 is pointed, so an off-by-one level silently sets up scripts/ instead of the repo. +PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" # Resolve the real user's home even when running under sudo if [[ -n "${SUDO_USER:-}" ]]; then @@ -540,7 +543,7 @@ if [ ! -f "$STARSHIP_DEST" ]; then elif cmp -s "$SCRIPT_DIR/starship.toml" "$STARSHIP_DEST"; then skip "starship config" else - warn "starship config kept — yours differs (cp scripts/starship.toml ~/.config/ to take this one)" + warn "starship config kept — yours differs (cp scripts/setup/starship.toml ~/.config/ to take this one)" fi diff --git a/scripts/setup_mac_light.sh b/scripts/setup/setup_mac_light.sh similarity index 97% rename from scripts/setup_mac_light.sh rename to scripts/setup/setup_mac_light.sh index a0771b15..2ab5a3af 100755 --- a/scripts/setup_mac_light.sh +++ b/scripts/setup/setup_mac_light.sh @@ -1,14 +1,14 @@ #!/bin/bash # Officer — macOS laptop setup. # -# The barebones counterpart to scripts/setup.sh (which targets an Ubuntu/Debian server and is left +# The barebones counterpart to scripts/setup/setup.sh (which targets an Ubuntu/Debian server and is left # alone). This installs only what a laptop workflow needs: the file browser, Claude/opencode chat, # and a terminal. No Go/Rust/cliamp/PulseAudio, no neovim, no shell dotfile stack, no VNC desktop, # no sudoers grant, no power-management changes. # # EVERY STEP IS OPTIONAL. Each one prompts before doing anything, and can be preset non-interactively: # -# SETUP_POSTGRES=0 SETUP_OPENCODE=0 bash scripts/setup_mac_light.sh +# SETUP_POSTGRES=0 SETUP_OPENCODE=0 bash scripts/setup/setup_mac_light.sh # # SETUP_PACKAGES brew node@22 / bun / ffmpeg SETUP_CLAUDE claude code CLI # SETUP_POSTGRES brew postgresql@18 + createdb SETUP_OPENCODE opencode CLI @@ -22,7 +22,7 @@ # This script never calls sudo itself — everything lands under the Homebrew prefix or $HOME. Note # that Homebrew's own installer does ask for an administrator password on a fresh Mac. # -# Usage: bash scripts/setup_mac_light.sh +# Usage: bash scripts/setup/setup_mac_light.sh set -euo pipefail @@ -48,7 +48,10 @@ FAILURES=() note_failure() { FAILURES+=("$1"); fail "$1"; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +# ../.. — this lives in scripts/setup/. See the note in setup.sh: PROJECT_DIR is where .env is written +# and where bun install, gen:index, db:push and pm2 are pointed, and none of them fails loudly on the +# wrong directory. +PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" PG_FORMULA="postgresql@18" PG_DATABASE="officer_dev" @@ -103,7 +106,7 @@ echo "════════════════════════ step "Preflight" if [ "$(uname -s)" != "Darwin" ]; then - fail "This script is macOS-only. On Linux use scripts/setup.sh." + fail "This script is macOS-only. On Linux use scripts/setup/setup.sh." exit 1 fi @@ -532,5 +535,5 @@ echo " • Not run on macOS: VNC desktop, email sync, music indexer, cliamp aud echo " that front a container or an external service — vault, slskd, headscale, transmission," echo " invoiceshelf, memos, photos, caldav, notify, wallet. See ecosystem.mac.light.config.cjs." echo " • Pin a specific Claude CLI with CLAUDE_BIN=/path/to/claude in .env if you need to" -echo " • Re-run any single step with e.g. SETUP_OPENCODE=1 bash scripts/setup_mac_light.sh" +echo " • Re-run any single step with e.g. SETUP_OPENCODE=1 bash scripts/setup/setup_mac_light.sh" echo "" diff --git a/scripts/starship.toml b/scripts/setup/starship.toml similarity index 100% rename from scripts/starship.toml rename to scripts/setup/starship.toml diff --git a/src/servers/app-store/preflight.ts b/src/servers/app-store/preflight.ts index 2634f6d8..24a6bdbf 100644 --- a/src/servers/app-store/preflight.ts +++ b/src/servers/app-store/preflight.ts @@ -9,9 +9,10 @@ import type { CatalogueEntry } from './catalogue'; // // ── What this deliberately does NOT do ── // -// It does not install anything. Today nothing in `scripts/` installs Docker either — `setup.sh` runs -// `setup-dockers.sh`, which invokes `docker compose` without ever checking it exists, so a fresh host -// without Docker fails partway through setup with a bare "command not found". That is a real gap, and +// It does not install anything. Today nothing in `scripts/` installs Docker either — the host installer +// `scripts/setup/setup.sh` runs `scripts/setup/setup-dockers.sh`, which invokes `docker compose` without +// ever checking it exists, so a fresh host without Docker fails partway through setup with a bare +// "command not found". That is a real gap, and // the intended fix is a per-sidecar `setup.sh` that ensures its own dependencies — which is also the // shape a sidecar needs once it lives in its own repository and ships independently. // diff --git a/src/servers/data-path.ts b/src/servers/data-path.ts index b74c2768..69e65ab7 100644 --- a/src/servers/data-path.ts +++ b/src/servers/data-path.ts @@ -50,7 +50,7 @@ export const getOwnerHomeDir = (email: string): string => process.env.HOME_DIR ? // non-owner's sessions would run. // // Single-sourced here rather than in the script that used to own the list, because there are now two -// callers — `scripts/provision-user-dirs.ts` and the owner's create-account handler — and a skeleton +// callers — `scripts/setup/provision-user-dirs.ts` and the owner's create-account handler — and a skeleton // that differs depending on how the account was made is a bug nobody would think to look for. export const USER_DIRS = ['home', 'attachments', 'cache', 'dashboards', 'email_accounts', 'logs', 'sidecar'] as const; diff --git a/src/servers/os-user-claude.ts b/src/servers/os-user-claude.ts index b2d681a1..d0c3682b 100644 --- a/src/servers/os-user-claude.ts +++ b/src/servers/os-user-claude.ts @@ -8,7 +8,7 @@ import { osUserHome, runAs } from './os-user'; // A single `/usr/local/bin/claude` would be less disk and one version to reason about, and the argument for // it is real: the private part of Claude is the credential in `~/.claude`, not the executable. It is still // the wrong shape here. `claude` updates itself — that is why the owner's own install goes through -// Anthropic's installer rather than npm (`scripts/setup.sh:853`) — and a root-owned binary is one a member +// Anthropic's installer rather than npm (`scripts/setup/setup.sh:861`) — and a root-owned binary is one a member // cannot update, which turns "my agent is a version behind" into a request to the owner. Per-member also // means the account's agent keeps working exactly as the tool ships, with no platform-shaped exception to // explain. Same command the owner ran, run as them, in their home. @@ -29,7 +29,7 @@ import { osUserHome, runAs } from './os-user'; // report whether the credential has appeared, so the UI can render the one-line instruction instead of an // agent that fails for reasons nobody can see. -/** Anthropic's own installer — the same one `scripts/setup.sh` uses for the owner, chosen for auto-update. */ +/** Anthropic's own installer — the same one `scripts/setup/setup.sh` uses for the owner, chosen for auto-update. */ const CLAUDE_INSTALL_URL = 'https://claude.ai/install.sh'; /** diff --git a/src/servers/os-user-shell.ts b/src/servers/os-user-shell.ts index 0b075db5..a3b0558f 100644 --- a/src/servers/os-user-shell.ts +++ b/src/servers/os-user-shell.ts @@ -14,7 +14,7 @@ import { osUserHome } from './os-user'; // // ── What it is ── // -// `shell-skel/zshrc` → `~/.zshrc`, and the platform's own `scripts/starship.toml` → `~/.config/starship.toml` +// `shell-skel/zshrc` → `~/.zshrc`, and the platform's own `scripts/setup/starship.toml` → `~/.config/starship.toml` // so a member's prompt is the same one the owner's install deploys. That file is the single source for both: // setup.sh copies it for the owner and this copies it for everybody else, so the two cannot drift. // @@ -31,7 +31,7 @@ import { osUserHome } from './os-user'; /** Where the templates live, relative to this file. */ const SKEL_DIR = join(import.meta.dir, 'shell-skel'); /** The prompt config the owner's own install uses — one file, both audiences. */ -const STARSHIP_SRC = join(import.meta.dir, '../../scripts/starship.toml'); +const STARSHIP_SRC = join(import.meta.dir, '../../scripts/setup/starship.toml'); type SudoResult = { ok: boolean; out: string };