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>
86 lines
3.9 KiB
TypeScript
86 lines
3.9 KiB
TypeScript
import type { CatalogueEntry } from './catalogue';
|
|
|
|
// Can this machine install this sidecar at all — asked BEFORE anything is written.
|
|
//
|
|
// The point is the ordering. An install that discovers a missing dependency halfway through has already
|
|
// created a directory, possibly started a container, and written a row; it then has to unwind, and the
|
|
// user is left with something that neither works nor uninstalls. A check that costs 30ms up front is
|
|
// worth a great deal of that.
|
|
//
|
|
// ── What this deliberately does NOT do ──
|
|
//
|
|
// 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.
|
|
//
|
|
// Until that exists, the honest thing is to detect and report rather than to guess or to half-install.
|
|
|
|
export type Preflight =
|
|
| { ok: true }
|
|
| { ok: false; reason: string; /** What the user has to do about it. */ remedy: string };
|
|
|
|
/**
|
|
* Docker is needed to PROVISION, never to point at something already running. Derived from `modes`
|
|
* rather than declared per entry, so the two cannot drift: an entry that can provision needs Docker, by
|
|
* definition, and nobody has to remember to tick a second box.
|
|
*/
|
|
export const needsDocker = (entry: CatalogueEntry): boolean => entry.modes.includes('provisioned');
|
|
|
|
/** `docker` on PATH, the daemon reachable, and the compose plugin present. All three, or it is not usable. */
|
|
export async function checkDocker(): Promise<Preflight> {
|
|
try {
|
|
// `docker compose version` exercises the binary, the daemon connection and the plugin in one call.
|
|
// `docker --version` would pass with a dead daemon, which is the failure people actually hit.
|
|
const proc = Bun.spawn(['docker', 'compose', 'version'], { stdout: 'pipe', stderr: 'pipe' });
|
|
const code = await proc.exited;
|
|
if (code === 0) return { ok: true };
|
|
|
|
const err = (await new Response(proc.stderr).text()).trim();
|
|
// The daemon being down and the plugin being absent need different remedies, and the message is the
|
|
// only way to tell them apart — the exit code is 1 for both.
|
|
if (/permission denied|daemon|cannot connect/i.test(err)) {
|
|
return {
|
|
ok: false,
|
|
reason: 'The Docker daemon is not reachable.',
|
|
remedy:
|
|
'Start Docker (`sudo systemctl start docker`), or add your user to the `docker` group and log in again.',
|
|
};
|
|
}
|
|
return {
|
|
ok: false,
|
|
reason: 'Docker Compose is not available.',
|
|
remedy: 'Install the Docker Compose plugin (`docker-compose-plugin`).',
|
|
};
|
|
} catch {
|
|
return {
|
|
ok: false,
|
|
reason: 'Docker is not installed.',
|
|
remedy: 'Install Docker Engine, then try again. https://docs.docker.com/engine/install/',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Everything that must be true before `mode` can be attempted for `entry`.
|
|
*
|
|
* Split by mode on purpose: a host with no Docker can still install Photos by pointing at an Immich
|
|
* somewhere else. Refusing the whole entry would be wrong, and is the sort of over-strict check that
|
|
* makes people work around the installer instead of using it.
|
|
*/
|
|
export async function preflight(entry: CatalogueEntry, mode: string): Promise<Preflight> {
|
|
if (entry.requires === 'linux-display' && process.platform !== 'linux') {
|
|
return {
|
|
ok: false,
|
|
reason: `${entry.label} mirrors an Xorg display, which this platform does not have.`,
|
|
remedy: 'This sidecar only runs on a Linux host with a display.',
|
|
};
|
|
}
|
|
|
|
if (mode === 'provisioned' && needsDocker(entry)) return checkDocker();
|
|
|
|
return { ok: true };
|
|
}
|