Extracted from the officer-suite monorepo into its own public repo. The app is
open source and free; the rest of the suite is not, so it could not stay in a
tree that ships an account layer.
What moved:
- apps/offscale/* -> repo root (App shell, app.json, assets)
- packages/officer-net/ -> modules/officer-net/, a `file:` dependency
- packages/core VPN closure -> src/vpn/ (screen, hook, native bridge, invite)
- packages/core shared primitives -> src/ui/ (theme, cards, tabs, backdrop, splash)
- scripts/build-{ios,android}.sh -> scripts/, cut down to this one app
What deliberately did NOT move: the platform account layer. VpnScreen used to
render core's AppSettingsScreen, which drags in ServersSection -> servers.ts,
useAuth, api.ts, shared-session, install-guard, api-keys. None of that exists
here. src/settings/SettingsScreen.tsx is a fresh replacement rendering exactly
what VpnScreen passed it — the app's own sections plus About — with no Servers
tab, no Appearance control (the app is pinned to dark) and no sign-out.
The one platform reference left in the closure was a dead `import { request }
from './api'` in officer-net.ts, unused since enrollVpn() was removed. Gone. The
app now contacts nothing but the control server you point it at, its peers, and
the enrolment sidecar named inside an invite link you tapped.
Dependencies are pinned to the exact versions the monorepo has installed, not to
ranges: this repo starts from versions already proven to build, and an unpinned
install is a supply-chain decision nobody made. react-query, react-dom,
react-native-screens, expo-secure-store, track-player, expo-audio and
expo-file-system were all declared by the old app and imported by none of this
code, so they are not here.
NOTICE carries Tailscale's BSD-3 text, names the built artifacts that contain
their compiled code, and treats the trademark question separately — including
that src/ui/TailscaleMark.tsx reproduces their logo in the attribution strip.
93 lines
4.5 KiB
Bash
Executable File
93 lines
4.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Regenerates the Android adaptive-icon foreground from the iOS icon.
|
|
#
|
|
# scripts/gen-adaptive-icon.sh # check, and generate if missing
|
|
# scripts/gen-adaptive-icon.sh --force # regenerate even if one exists but differs
|
|
#
|
|
# Android launcher masks (round, squircle, teardrop — the launcher decides) crop the icon. The art is
|
|
# a full-bleed render on a black backdrop, so a mask ate its edges. The fix is to shrink the art inside
|
|
# the same canvas and let the transparent margin absorb whatever the mask takes.
|
|
#
|
|
# Only Android needs this. iOS draws icon.png full-bleed with its own rounding, so insetting there
|
|
# would just add a black border — icon.png is never touched.
|
|
#
|
|
# Runs from the `prebuild` npm script, because `expo prebuild` is what turns these assets into
|
|
# android/app/src/main/res/*.webp. The generated PNG is also committed, so a machine without
|
|
# ImageMagick still builds a correct icon — this regenerates it, it is not the only source. That is
|
|
# also why calling `expo prebuild` directly instead of `bun run prebuild` is a mistake: it skips this
|
|
# silently.
|
|
set -euo pipefail
|
|
|
|
# Android's adaptive-icon spec: the outer 1/3 of the foreground is reserved for mask and parallax,
|
|
# leaving a guaranteed-visible circle of ~66%. 72% sits just above that — as large as the art can be
|
|
# drawn while no supported mask clips it.
|
|
readonly INSET_PERCENT=72
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
ICON="$ROOT/assets/icon.png"
|
|
ADAPTIVE="$ROOT/assets/adaptive-icon.png"
|
|
|
|
FORCE=false
|
|
for a in "$@"; do [[ "$a" == "--force" ]] && FORCE=true; done
|
|
|
|
if ! command -v magick >/dev/null 2>&1; then
|
|
echo "!! ImageMagick not found — skipping adaptive-icon generation." >&2
|
|
echo "!! The committed adaptive-icon.png is used as-is. brew install imagemagick" >&2
|
|
exit 0
|
|
fi
|
|
|
|
# True when `existing` already IS `icon` inset — compared by pixel content, not by bytes and not by
|
|
# geometry.
|
|
#
|
|
# Geometry alone is not enough: checking only that the art fills ~72% of the canvas passes an adaptive
|
|
# icon built from SUPERSEDED art, so swapping in new icon.png art leaves the Android icon silently
|
|
# stale. Bytes are not enough in the other direction: re-encoding an identical image can differ, which
|
|
# would rewrite a megabyte on every prebuild. RMSE of the decoded pixels is exact here, so the
|
|
# threshold only absorbs a future encoder change.
|
|
matches_icon() {
|
|
local existing="$1" icon="$2" size="$3" art="$4"
|
|
local tmp="$existing.expected.png"
|
|
magick "$icon" -resize "${art}x${art}" -background none -gravity center \
|
|
-extent "${size}x${size}" "$tmp" 2>/dev/null || { rm -f "$tmp"; return 1; }
|
|
|
|
# Differing canvas sizes make compare fail outright, which is itself a mismatch.
|
|
local rmse
|
|
rmse="$(magick compare -metric RMSE "$existing" "$tmp" null: 2>&1 || true)"
|
|
rmse="${rmse%% *}"
|
|
rm -f "$tmp"
|
|
|
|
awk -v v="$rmse" 'BEGIN { exit !(v ~ /^[0-9.]+$/ && v < 100) }'
|
|
}
|
|
|
|
[[ -f "$ICON" ]] || { echo "!! no assets/icon.png" >&2; exit 1; }
|
|
|
|
# Same canvas as the source; only the art inside it shrinks. Rounded, not truncated.
|
|
size="$(magick identify -format '%w' "$ICON")"
|
|
art=$(((size * INSET_PERCENT + 50) / 100))
|
|
|
|
if [[ -f "$ADAPTIVE" ]] && matches_icon "$ADAPTIVE" "$ICON" "$size" "$art"; then
|
|
echo "adaptive icon: ok"
|
|
elif [[ -f "$ADAPTIVE" ]] && [[ "$FORCE" != true ]]; then
|
|
# Deliberately does NOT overwrite. An existing adaptive icon that does not match icon.png is
|
|
# ambiguous: it is either stale art (regenerate) or art framed on purpose. Say so and let a human
|
|
# decide; --force applies the regeneration.
|
|
echo "adaptive icon: DIFFERS from icon.png — not overwritten (--force to regenerate)"
|
|
else
|
|
magick "$ICON" -resize "${art}x${art}" \
|
|
-background none -gravity center -extent "${size}x${size}" "$ADAPTIVE"
|
|
echo "adaptive icon: regenerated (${size} canvas, ${art} art)"
|
|
fi
|
|
|
|
# The adaptive background sits behind the inset art, so it must match the art's own backdrop or a seam
|
|
# shows at the edges. This only reports — app.json is easy to edit by hand and silently rewriting it
|
|
# is worse than saying what to change.
|
|
corner="#$(magick "$ICON" -format '%[hex:p{0,0}]' info: | tr '[:lower:]' '[:upper:]')"
|
|
configured="$(grep -oE 'backgroundColor.{0,4}#[0-9A-Fa-f]{6}' "$ROOT/app.json" 2>/dev/null \
|
|
| grep -oE '#[0-9A-Fa-f]{6}' | head -1 | tr '[:lower:]' '[:upper:]' || true)"
|
|
if [[ -z "$configured" ]]; then
|
|
echo " no android.adaptiveIcon.backgroundColor set — use $corner"
|
|
elif [[ "$corner" != "$configured" ]]; then
|
|
echo " backgroundColor is $configured, art corner is $corner"
|
|
fi
|