move host setup into scripts/setup/

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/<name>/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 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 04:02:19 +00:00
co-authored by Claude Opus 5
parent 37adc65a12
commit 9c353f5f0d
18 changed files with 48 additions and 35 deletions
+1 -1
View File
@@ -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 `'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` 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`, 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 - [ ] **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 tells the person, which means the owner knows it and the member cannot change it back if they
+1 -1
View File
@@ -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 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. 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 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 wants OS users needs a **narrow** entry — `useradd`, `chown`, `setpriv` — which is better than the blanket
rule anyway. rule anyway.
+7 -3
View File
@@ -107,9 +107,13 @@ consequences, both wanted:
### Docker is assumed, and nothing guarantees it ### Docker is assumed, and nothing guarantees it
Verified: **nothing in `scripts/` installs Docker, and nothing checks for it.** `setup.sh` calls Verified: **nothing in `scripts/` installs Docker, and nothing checks for it.** The host installer
`setup-dockers.sh`, which invokes `docker compose` with no preflight, so a fresh host without Docker `scripts/setup/setup.sh` calls `scripts/setup/setup-dockers.sh`, which invokes `docker compose` with no
fails partway through setup with a bare "command not found". 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/<name>/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 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. by his own scripts, where Docker was simply always there.
+1 -1
View File
@@ -1,7 +1,7 @@
// Linux light profile — the platform without the self-hosted estate around it. // 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 // 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. // 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 // This is a subset of ecosystem.config.cjs, not a copy of it — see ecosystem.profile.cjs for why, and
+1 -1
View File
@@ -1,6 +1,6 @@
// macOS light profile — the same process set as the Linux light profile, on a laptop. // 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. // chat; nothing else.
// //
// This is a subset of ecosystem.config.cjs, not a copy of it. That distinction is here because of this // This is a subset of ecosystem.config.cjs, not a copy of it. That distinction is here because of this
+1 -1
View File
@@ -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": "{ 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:all": "prettier --write \"src/**/*.{ts,tsx}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"", "format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
"setup": "bash scripts/setup.sh" "setup": "bash scripts/setup/setup.sh"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.41", "@anthropic-ai/claude-agent-sdk": "^0.2.41",
@@ -26,7 +26,7 @@ set -euo pipefail
# their profile in it. Opt in rather than surprise someone. # their profile in it. Opt in rather than surprise someone.
# #
# Usage: # 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. # Reboot afterwards: the GDM and GRUB changes are both boot-time.
@@ -131,7 +131,9 @@ fi
# --- Step 7: .env --- # --- Step 7: .env ---
echo "[7/7] Cleaning .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 if [ -f "$ENV_FILE" ]; then
sed -i '/^VNC_PASSWORD=/d; /^VNC_PORT=/d' "$ENV_FILE" sed -i '/^VNC_PASSWORD=/d; /^VNC_PORT=/d' "$ENV_FILE"
echo " Removed VNC entries." echo " Removed VNC entries."
@@ -1,7 +1,7 @@
// Create the per-user root under DATA_PATH for the given accounts. // Create the per-user root under DATA_PATH for the given accounts.
// //
// bun scripts/provision-user-dirs.ts a@b.com c@d.com // bun scripts/setup/provision-user-dirs.ts a@b.com c@d.com
// DRY_RUN=1 bun scripts/provision-user-dirs.ts a@b.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 // 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 // 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 // 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 // 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. // 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 DRY_RUN = process.env.DRY_RUN === '1';
const emails = process.argv.slice(2).filter(Boolean); const emails = process.argv.slice(2).filter(Boolean);
if (!emails.length) { if (!emails.length) {
console.error('Usage: bun scripts/provision-user-dirs.ts <email> [email...]'); console.error('Usage: bun scripts/setup/provision-user-dirs.ts <email> [email...]');
process.exit(2); process.exit(2);
} }
@@ -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 # 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 # 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. # 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 "=== Officer Remote Desktop Setup (Ubuntu GNOME on Xorg) ==="
echo "" echo ""
@@ -4,8 +4,8 @@
# Outputs parseable key=value lines to stdout; all prompts go to stderr. # Outputs parseable key=value lines to stdout; all prompts go to stderr.
# #
# Usage: # Usage:
# bash scripts/setup-dockers.sh # bash scripts/setup/setup-dockers.sh
# eval "$(bash scripts/setup-dockers.sh)" # eval "$(bash scripts/setup/setup-dockers.sh)"
# #
# Environment overrides: # Environment overrides:
# SETUP_DOCKER_SERVICES="1 2 3" — pre-select services (or "all"/"none") # SETUP_DOCKER_SERVICES="1 2 3" — pre-select services (or "all"/"none")
+7 -4
View File
@@ -3,8 +3,8 @@
# Run once on a fresh Ubuntu/Debian host before launching the server. # Run once on a fresh Ubuntu/Debian host before launching the server.
# #
# Usage: # Usage:
# bash scripts/setup.sh # full server install # bash scripts/setup/setup.sh # full server install
# OFFICER_PROFILE=light bash scripts/setup.sh # light install # OFFICER_PROFILE=light bash scripts/setup/setup.sh # light install
# #
# PROFILES # PROFILES
# full Everything: the self-hosted estate, the remote desktop, the music/audio stack, the shell # 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 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)" 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 # Resolve the real user's home even when running under sudo
if [[ -n "${SUDO_USER:-}" ]]; then if [[ -n "${SUDO_USER:-}" ]]; then
@@ -540,7 +543,7 @@ if [ ! -f "$STARSHIP_DEST" ]; then
elif cmp -s "$SCRIPT_DIR/starship.toml" "$STARSHIP_DEST"; then elif cmp -s "$SCRIPT_DIR/starship.toml" "$STARSHIP_DEST"; then
skip "starship config" skip "starship config"
else 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 fi
@@ -1,14 +1,14 @@
#!/bin/bash #!/bin/bash
# Officer — macOS laptop setup. # 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, # 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, # and a terminal. No Go/Rust/cliamp/PulseAudio, no neovim, no shell dotfile stack, no VNC desktop,
# no sudoers grant, no power-management changes. # no sudoers grant, no power-management changes.
# #
# EVERY STEP IS OPTIONAL. Each one prompts before doing anything, and can be preset non-interactively: # 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_PACKAGES brew node@22 / bun / ffmpeg SETUP_CLAUDE claude code CLI
# SETUP_POSTGRES brew postgresql@18 + createdb SETUP_OPENCODE opencode 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 # 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. # 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 set -euo pipefail
@@ -48,7 +48,10 @@ FAILURES=()
note_failure() { FAILURES+=("$1"); fail "$1"; } note_failure() { FAILURES+=("$1"); fail "$1"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 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_FORMULA="postgresql@18"
PG_DATABASE="officer_dev" PG_DATABASE="officer_dev"
@@ -103,7 +106,7 @@ echo "════════════════════════
step "Preflight" step "Preflight"
if [ "$(uname -s)" != "Darwin" ]; then 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 exit 1
fi 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 " 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 " 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 " • 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 "" echo ""
+4 -3
View File
@@ -9,9 +9,10 @@ import type { CatalogueEntry } from './catalogue';
// //
// ── What this deliberately does NOT do ── // ── What this deliberately does NOT do ──
// //
// It does not install anything. Today nothing in `scripts/` installs Docker either — `setup.sh` runs // It does not install anything. Today nothing in `scripts/` installs Docker either — the host installer
// `setup-dockers.sh`, which invokes `docker compose` without ever checking it exists, so a fresh host // `scripts/setup/setup.sh` runs `scripts/setup/setup-dockers.sh`, which invokes `docker compose` without
// without Docker fails partway through setup with a bare "command not found". That is a real gap, and // 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 // 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. // shape a sidecar needs once it lives in its own repository and ships independently.
// //
+1 -1
View File
@@ -50,7 +50,7 @@ export const getOwnerHomeDir = (email: string): string => process.env.HOME_DIR ?
// non-owner's sessions would run. // 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 // 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. // 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; export const USER_DIRS = ['home', 'attachments', 'cache', 'dashboards', 'email_accounts', 'logs', 'sidecar'] as const;
+2 -2
View File
@@ -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 // 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 // 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 // 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 // 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 // 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. // 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 // 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. // 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'; const CLAUDE_INSTALL_URL = 'https://claude.ai/install.sh';
/** /**
+2 -2
View File
@@ -14,7 +14,7 @@ import { osUserHome } from './os-user';
// //
// ── What it is ── // ── 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: // 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. // 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. */ /** Where the templates live, relative to this file. */
const SKEL_DIR = join(import.meta.dir, 'shell-skel'); const SKEL_DIR = join(import.meta.dir, 'shell-skel');
/** The prompt config the owner's own install uses — one file, both audiences. */ /** 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 }; type SudoResult = { ok: boolean; out: string };