Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066#!/bin/bash# ac-os — Single command for AC Native OS operations# Usage:# ac-os flash Build everything + flash USB# ac-os build Build everything (no flash)# ac-os upload Upload current build as OTA release# ac-os flash+upload Build + flash USB + upload OTA release# ac-os pull Download latest OTA kernel + flash USB (no local build)# ac-os scan Find AC Native OS devices on local network (Swank port 4005)# ac-os repl [IP] Connect to a running device's Swank REPL# ac-os oven [REF] Trigger remote OTA build on oven (default: HEAD)# ac-os oven status Show oven build queue + last builds# ac-os oven watch Tail logs for active oven build (SSE)set -eSCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"BUILD_DIR="${SCRIPT_DIR}/build"VMLINUZ="${BUILD_DIR}/vmlinuz"INITRAMFS_ROOT="${BUILD_DIR}/initramfs-root"AC_MEDIA_NATIVE_DIR="${SCRIPT_DIR}"MEDIA_LAYOUT_LIB="${SCRIPT_DIR}/scripts/media-layout.sh"MEDIA_HELPER_IMAGE="${AC_MEDIA_HELPER_IMAGE:-ac-os-media-helper:img-v2-ext-initrd}"MEDIA_HELPER_DOCKERFILE="${SCRIPT_DIR}/Dockerfile.flash-helper"CMD="${1:-build}"# Freeze version strings once — all stages use the same valuesexport AC_GIT_HASH="${AC_GIT_HASH:-$(cd "${SCRIPT_DIR}" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")}"export AC_BUILD_TS="${AC_BUILD_TS:-$(date -u '+%Y-%m-%dT%H:%M')}"export AC_BUILD_NAME="${AC_BUILD_NAME:-$(cd "${SCRIPT_DIR}" && bash scripts/build-name.sh 2>/dev/null || echo "unknown")}"log() { echo -e "\033[0;36m[ac-os]\033[0m $*"; }err() { echo -e "\033[0;31m[ac-os]\033[0m $*" >&2; }# Drive the AC Electron menu-bar "USB" badge (~/.ac-os/tray.json, watched live# by ac-electron's tray-renderer). States: ready (image staged to burn — slow# blink), flashing (writing now — fast pulse), clear (gone). Best-effort; never# fails the flash. Skip on non-macOS (the badge is a macOS menu-bar feature).ac_tray_state() { [ "$(uname)" = "Darwin" ] || return 0 local f="${HOME}/.ac-os/tray.json" mkdir -p "${HOME}/.ac-os" 2>/dev/null || true case "$1" in ready) printf '%s\n' '{ "show": true, "badge": "USB", "logoColor": "accent", "color": "accent", "bg": "#ffffff", "outline": "accent", "outlineWidth": 1, "pt": 8, "effect": "blink", "periodMs": 1600, "fps": 8 }' > "$f" 2>/dev/null || true ;; flashing) printf '%s\n' '{ "show": true, "badge": "USB", "logoColor": "accent", "color": "accent", "bg": "#ffffff", "outline": "accent", "outlineWidth": 1, "pt": 8, "effect": "pulse", "periodMs": 650, "fps": 15 }' > "$f" 2>/dev/null || true ;; clear|*) rm -f "$f" 2>/dev/null || true ;; esac}# shellcheck source=/workspaces/aesthetic-computer/fedac/native/scripts/media-layout.shsource "${MEDIA_LAYOUT_LIB}"# Load env vars (MongoDB, etc.) if not already setif [ -z "${MONGODB_CONNECTION_STRING:-}" ] && [ -f "/home/me/envs/devcontainer.env" ]; then while IFS='=' read -r key value; do [[ "$key" =~ ^[[:space:]]*# ]] && continue [[ -z "$key" ]] && continue key=$(echo "$key" | xargs) [ -z "${!key:-}" ] && export "$key=$value" 2>/dev/null || true done < /home/me/envs/devcontainer.envfibuild_binary() { log "Building binary..." cd "${SCRIPT_DIR}" CC_USE="${CC:-gcc}" # Run make and capture exit code — version strings frozen at top of script mkdir -p "${BUILD_DIR}" local MAKE_LOG="${BUILD_DIR}/.make.log" make -j$(nproc) CC="${CC_USE}" BUILD_TS="${AC_BUILD_TS}" GIT_HASH="${AC_GIT_HASH}" BUILD_NAME="${AC_BUILD_NAME}" > "${MAKE_LOG}" 2>&1 local MAKE_RC=$? grep -E "Built:|error:" "${MAKE_LOG}" || true if [ ${MAKE_RC} -ne 0 ]; then err "Binary build failed (exit ${MAKE_RC}) — refusing to continue with stale binary" grep "error:" "${MAKE_LOG}" >&2 exit 1 fi # Verify the binary embeds the frozen git hash if strings "${BUILD_DIR}/ac-native" | grep -q "${AC_GIT_HASH}"; then log "Binary verified: contains ${AC_GIT_HASH}" else err "Binary does NOT contain git hash ${AC_GIT_HASH} — stale build!" exit 1 fi # Telemetry: check binary can resolve all shared libs local INTERP=$(readelf -l "${BUILD_DIR}/ac-native" 2>/dev/null | grep -oP '(?<=interpreter: ).*(?=\])') log "Binary interpreter: ${INTERP:-static}" local MISSING_LIBS="" for lib in $(readelf -d "${BUILD_DIR}/ac-native" 2>/dev/null | grep -oP '(?<=\[)lib[^\]]+'); do if ! ldconfig -p 2>/dev/null | grep -q "$lib"; then MISSING_LIBS="${MISSING_LIBS} ${lib}" fi done if [ -n "${MISSING_LIBS}" ]; then err "Binary has missing shared libs:${MISSING_LIBS}" exit 1 fi log "Binary shared libs: all resolved" # Build BPF trace tools (ftrace fallback — no clang/libbpf required) if [ -f "${SCRIPT_DIR}/bpf/Makefile" ]; then log "Building BPF trace tools..." make -C "${SCRIPT_DIR}/bpf" CC="${CC_USE}" 2>&1 | tail -3 || true fi}build_initramfs() { log "Building initramfs..." cd "${SCRIPT_DIR}" # The old build-and-flash.sh populates the initramfs root (dirs, init script, # pieces, shared libs, WiFi bins, ALSA config, firmware, busybox, etc.) # Run it with --skip-kernel --skip-binary to only do initramfs setup. # The shim (scripts/build-and-flash.sh) must NOT call back into ac-os. local OLD_SCRIPT="${SCRIPT_DIR}/scripts/build-and-flash-initramfs.sh" if [ -f "${OLD_SCRIPT}" ]; then log "Populating initramfs root..." sudo bash "${OLD_SCRIPT}" --skip-kernel --skip-binary 2>&1 \ | grep -E "Baked|pieces|WiFi|ALSA|libs|firmware|init|Bundled|error|config" || true fi # KidLisp: npx not available under sudo, bundle as user then copy local KIDLISP_SRC="${SCRIPT_DIR}/../../system/public/aesthetic.computer/lib/kidlisp.mjs" if [ -f "${KIDLISP_SRC}" ] && command -v npx &>/dev/null; then log "Bundling KidLisp..." npx esbuild "${KIDLISP_SRC}" --bundle --format=iife \ --global-name=KidLispModule --platform=node \ --outfile=/tmp/kidlisp-bundle.js 2>&1 | grep -v "^npm warn" || true sudo mkdir -p "${INITRAMFS_ROOT}/jslib" sudo cp /tmp/kidlisp-bundle.js "${INITRAMFS_ROOT}/jslib/kidlisp-bundle.js" fi # FPS system bundle (Camera + Dolly + CamDoll) for pieces that export # `system = "fps"` (arena.mjs etc.). Mirrors the docker-build.sh path. local FPS_ENTRY="${SCRIPT_DIR}/scripts/fps-bundle-entry.mjs" if [ -f "${FPS_ENTRY}" ] && command -v npx &>/dev/null; then log "Bundling FPS system (Camera + Dolly + CamDoll)..." npx esbuild "${FPS_ENTRY}" --bundle --format=iife \ --global-name=__FpsSystem --platform=neutral \ --outfile=/tmp/fps-system-bundle.js 2>&1 | grep -v "^npm warn" || true if [ -f /tmp/fps-system-bundle.js ]; then sudo mkdir -p "${INITRAMFS_ROOT}/jslib" sudo cp /tmp/fps-system-bundle.js "${INITRAMFS_ROOT}/jslib/fps-system-bundle.js" fi fi # Copy fresh binary into initramfs sudo cp "${BUILD_DIR}/ac-native" "${INITRAMFS_ROOT}/ac-native" # Claude Code native binary (no Node.js needed). # Look in the builder's home first; if nothing's installed (oven build # machines don't ship claude pre-installed) fall back to downloading # the latest native binary via claude.ai/install.sh so every OS build # ships a working /bin/claude instead of the "claude: command not # found" that was leaving devices broken post-flash. local CLAUDE_NATIVE="${HOME}/.local/share/claude/versions" local CLAUDE_BIN="" if [ -d "${CLAUDE_NATIVE}" ]; then CLAUDE_BIN=$(ls -t "${CLAUDE_NATIVE}/"* 2>/dev/null | head -1) fi if [ -z "${CLAUDE_BIN}" ] || [ ! -f "${CLAUDE_BIN}" ]; then log "Claude native binary not found locally; fetching via claude.ai/install.sh..." # install.sh is safe to run on CI — writes to $HOME/.local only. if curl -fsSL https://claude.ai/install.sh | bash 2>&1 | sed 's/^/ [claude-install] /' | tail -20; then if [ -d "${CLAUDE_NATIVE}" ]; then CLAUDE_BIN=$(ls -t "${CLAUDE_NATIVE}/"* 2>/dev/null | head -1) fi else log " claude-install failed (non-fatal — will ship image without /bin/claude)" fi fi if [ -n "${CLAUDE_BIN}" ] && [ -f "${CLAUDE_BIN}" ]; then log "Bundling Claude Code (native binary)..." sudo cp "${CLAUDE_BIN}" "${INITRAMFS_ROOT}/bin/claude" sudo chmod +x "${INITRAMFS_ROOT}/bin/claude" # NOTE: Do NOT strip — Claude Code is a Bun SEA binary and stripping # destroys the embedded JS blob, leaving a bare Bun runtime that errors # with "script not found 'claude'". # Copy shared libraries needed by the binary for lib in $(ldd "${CLAUDE_BIN}" 2>/dev/null | grep -oP '/\S+'); do [ -f "$lib" ] && sudo cp -n "$lib" "${INITRAMFS_ROOT}/lib64/" 2>/dev/null || true done log " claude: $(du -sh "${INITRAMFS_ROOT}/bin/claude" | cut -f1) (native binary)" # Bake Claude OAuth token (plain text file, read by PTY at spawn time) sudo rm -f "${INITRAMFS_ROOT}/claude-token" 2>/dev/null if [ -f "${HOME}/.claude/.credentials.json" ]; then node -e " const c=JSON.parse(require('fs').readFileSync(process.env.HOME+'/.claude/.credentials.json','utf8')); process.stdout.write(c.claudeAiOauth.accessToken); " 2>/dev/null | sudo tee "${INITRAMFS_ROOT}/claude-token" > /dev/null sudo chmod 600 "${INITRAMFS_ROOT}/claude-token" log " claude token: baked ($(sudo cat "${INITRAMFS_ROOT}/claude-token" | wc -c) bytes)" fi # Bake GitHub PAT if available (for git operations on device) local GH_PAT=$(gh auth token 2>/dev/null) if [ -n "${GH_PAT}" ]; then echo -n "${GH_PAT}" | sudo tee "${INITRAMFS_ROOT}/github-pat" > /dev/null sudo chmod 600 "${INITRAMFS_ROOT}/github-pat" log " github pat: baked (${#GH_PAT} chars)" fi # Bake Tangled SSH identity if available so on-device git pushes can # reach knot.aesthetic.computer as well as the GitHub mirror. local TANGLED_KEY TANGLED_KEY="$(ssh -G knot.aesthetic.computer 2>/dev/null | awk '/^identityfile / {print $2; exit}')" case "${TANGLED_KEY}" in "~/"*) TANGLED_KEY="${HOME}/${TANGLED_KEY#~/}" ;; esac if [ -z "${TANGLED_KEY}" ] && [ -f "${HOME}/.ssh/tangled" ]; then TANGLED_KEY="${HOME}/.ssh/tangled" fi if [ -n "${TANGLED_KEY}" ] && [ -f "${TANGLED_KEY}" ]; then sudo cp "${TANGLED_KEY}" "${INITRAMFS_ROOT}/tangled-key" sudo chmod 600 "${INITRAMFS_ROOT}/tangled-key" if [ -f "${HOME}/.ssh/known_hosts" ]; then sudo cp "${HOME}/.ssh/known_hosts" "${INITRAMFS_ROOT}/tangled-known-hosts" sudo chmod 600 "${INITRAMFS_ROOT}/tangled-known-hosts" elif command -v ssh-keyscan >/dev/null 2>&1; then ssh-keyscan -H knot.aesthetic.computer 2>/dev/null | sudo tee "${INITRAMFS_ROOT}/tangled-known-hosts" >/dev/null || true sudo chmod 600 "${INITRAMFS_ROOT}/tangled-known-hosts" 2>/dev/null || true fi sudo tee "${INITRAMFS_ROOT}/tangled-ssh-config" > /dev/null << 'SSHCFG'Host knot.aesthetic.computer HostName knot.aesthetic.computer User git IdentityFile ~/.ssh/tangled IdentitiesOnly yes BatchMode yes StrictHostKeyChecking accept-new UserKnownHostsFile ~/.ssh/known_hostsSSHCFG sudo chmod 600 "${INITRAMFS_ROOT}/tangled-ssh-config" log " tangled ssh: baked (${TANGLED_KEY})" else log " tangled ssh: not found (skipping knot push setup)" fi # Bake CLAUDE.md for device context (init copies to /tmp/ac/CLAUDE.md) if [ -f "${SCRIPT_DIR}/device-claude.md" ]; then sudo cp "${SCRIPT_DIR}/device-claude.md" "${INITRAMFS_ROOT}/device-claude.md" log " CLAUDE.md: baked into initramfs" fi # Bake SCORE.md for project context (symlinked from /tmp/ac/SCORE.md) if [ -f "${SCRIPT_DIR}/SCORE.md" ]; then sudo cp "${SCRIPT_DIR}/SCORE.md" "${INITRAMFS_ROOT}/device-score.md" log " SCORE.md: baked into initramfs" fi # Bake Claude state file (init copies to /tmp/.claude.json) if [ -f "${HOME}/.claude.json" ]; then node -e " const d=JSON.parse(require('fs').readFileSync(process.env.HOME+'/.claude.json','utf8')); const m={oauthAccount:d.oauthAccount,hasCompletedOnboarding:true,installMethod:'manual',numStartups:1,autoUpdates:false,autoUpdatesProtectedForNative:true}; console.log(JSON.stringify(m)); " 2>/dev/null | sudo tee "${INITRAMFS_ROOT}/claude-state.json" > /dev/null log " claude state: baked into initramfs" fi else log "Claude Code native binary not found — skipping" log " Install with: curl -fsSL https://claude.ai/install.sh | bash" fi # Browser stack — only bundled if USE_BROWSER=1 (disabled by default). # Claude Code authenticates via pre-baked credentials in initramfs. if [ "${USE_BROWSER:-0}" = "1" ]; then local FIREFOX_CACHE="/tmp/ac-firefox-cache" local FIREFOX_TAR="/tmp/firefox.tar.xz" local FIREFOX_URL="https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US" if [ ! -d "${FIREFOX_CACHE}" ]; then log "Downloading Firefox..." curl -fsSL "${FIREFOX_URL}" -o "${FIREFOX_TAR}" 2>&1 mkdir -p "${FIREFOX_CACHE}" tar xf "${FIREFOX_TAR}" -C "${FIREFOX_CACHE}" --strip-components=1 # Strip unnecessary components for OAuth-only use rm -f "${FIREFOX_CACHE}/crashreporter" "${FIREFOX_CACHE}/crashhelper" \ "${FIREFOX_CACHE}/pingsender" "${FIREFOX_CACHE}/updater" \ "${FIREFOX_CACHE}/update-settings.ini" \ "${FIREFOX_CACHE}/libonnxruntime.so" "${FIREFOX_CACHE}/libmozinference.so" rm -rf "${FIREFOX_CACHE}/dictionaries" rm -f "${FIREFOX_CACHE}/fonts/TwemojiMozilla.ttf" rm -f "${FIREFOX_TAR}" log " firefox: $(du -sh "${FIREFOX_CACHE}" | cut -f1) (stripped)" else log " firefox: cached at ${FIREFOX_CACHE}" fi sudo mkdir -p "${INITRAMFS_ROOT}/opt/firefox" sudo cp -a "${FIREFOX_CACHE}/." "${INITRAMFS_ROOT}/opt/firefox/" sudo ln -sf /opt/firefox/firefox-bin "${INITRAMFS_ROOT}/bin/firefox" # xdg-open shim — Claude Code calls this to open OAuth URLs sudo tee "${INITRAMFS_ROOT}/bin/xdg-open" > /dev/null << 'XDGOPEN'#!/bin/sh# xdg-open shim — launches Firefox for OAuth (Claude Code calls this)# Always log that we were called (tmpfs — always works)echo "[xdg-open] called: $0 $*" >> /tmp/xdg-open.logecho "[xdg-open] WAYLAND=$WAYLAND_DISPLAY DISPLAY=$DISPLAY BROWSER=$BROWSER" >> /tmp/xdg-open.logURL="$1"[ -z "$URL" ] && exit 1if [ -n "$WAYLAND_DISPLAY" ]; then # Under cage: launch Firefox as sibling Wayland client export MOZ_ENABLE_WAYLAND=1 GDK_BACKEND=wayland MOZ_DISABLE_CONTENT_SANDBOX=1 export DBUS_SESSION_BUS_ADDRESS=disabled: MOZ_DBUS_REMOTE=0 export HOME=/tmp LD_LIBRARY_PATH=/lib64:/opt/firefox GRE_HOME=/opt/firefox export MOZ_GFX_BACKEND=cairo MOZ_DISABLE_GPU=1 export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp/xdg}" export GDK_PIXBUF_MODULE_FILE="/usr/lib64/gdk-pixbuf-2.0/2.10.0/loaders.cache" export GDK_PIXBUF_MODULEDIR="/usr/lib64/gdk-pixbuf-2.0/2.10.0/loaders" mkdir -p /tmp/.mozilla "$XDG_RUNTIME_DIR" 2>/dev/null # Write debug to tmpfs (reliable), ac-native copies to USB LOG=/tmp/firefox-debug.log echo "=== xdg-open ===" > $LOG echo "URL=$URL" >> $LOG echo "ENV: WAYLAND=$WAYLAND_DISPLAY XDG=$XDG_RUNTIME_DIR" >> $LOG ls -la "$XDG_RUNTIME_DIR/wayland-0" >> $LOG 2>&1 cd /opt/firefox mkdir -p /tmp/ff-profile # Try headless first to isolate display vs init issues echo "=== headless test ===" >> $LOG MOZ_HEADLESS=1 timeout 5 ./firefox-bin --headless --no-remote --profile /tmp/ff-profile "about:blank" >> $LOG 2>&1 echo "headless-rc=$?" >> $LOG # Now try actual launch echo "=== real launch ===" >> $LOG MOZ_CRASHREPORTER_DISABLE=1 ./firefox-bin --no-remote --profile /tmp/ff-profile "$URL" >> $LOG 2>&1 echo "rc=$?" >> $LOG # Copy to USB cp $LOG /mnt/firefox.log 2>/dev/null syncelse # DRM mode: signal ac-native to handle DRM handoff + cage+firefox echo "$URL" > /tmp/.browser-url ACPID=$(pidof ac-native 2>/dev/null || echo 1) kill -USR1 $ACPID 2>/dev/nullfiXDGOPEN sudo chmod +x "${INITRAMFS_ROOT}/bin/xdg-open" # Bundle cage (Wayland kiosk compositor) + seatd + wlroots libs local CAGE_BIN="$(command -v cage 2>/dev/null || true)" local SEATD_BIN="$(command -v seatd 2>/dev/null || true)" if [ -n "${CAGE_BIN}" ] && [ -f "${CAGE_BIN}" ]; then log "Bundling cage..." sudo cp "${CAGE_BIN}" "${INITRAMFS_ROOT}/bin/cage" sudo chmod +x "${INITRAMFS_ROOT}/bin/cage" for lib in $(ldd "${CAGE_BIN}" 2>/dev/null | grep -oP '/\S+'); do [ -f "$lib" ] && sudo cp -n "$lib" "${INITRAMFS_ROOT}/lib64/" 2>/dev/null || true done log " cage: $(du -sh "${INITRAMFS_ROOT}/bin/cage" | cut -f1)" else log "cage not installed — skipping (sudo dnf install cage)" fi # seatd — seat daemon required by wlroots 0.18 (libseat) if [ -n "${SEATD_BIN}" ] && [ -f "${SEATD_BIN}" ]; then sudo cp "${SEATD_BIN}" "${INITRAMFS_ROOT}/bin/seatd" sudo chmod +x "${INITRAMFS_ROOT}/bin/seatd" for lib in $(ldd "${SEATD_BIN}" 2>/dev/null | grep -oP '/\S+'); do [ -f "$lib" ] && sudo cp -nL "$lib" "${INITRAMFS_ROOT}/lib64/" 2>/dev/null || true done log " seatd: $(du -sh "${INITRAMFS_ROOT}/bin/seatd" | cut -f1)" else log "seatd not installed — skipping (sudo dnf install seatd)" fi # Firefox needs GTK/Wayland/Pango/Cairo/etc libs — many loaded via dlopen at runtime # Scan the full dependency tree of firefox-bin, libxul.so, and all GTK/Pango/Cairo libs log "Bundling Firefox shared libs (GTK3 + Wayland + Pango + Cairo)..." local _ff_libs=$(mktemp) for so in "${INITRAMFS_ROOT}/opt/firefox/firefox-bin" \ "${INITRAMFS_ROOT}/opt/firefox/libxul.so" \ /usr/lib64/libgtk-3.so.0 /usr/lib64/libgdk-3.so.0 \ /usr/lib64/libpango-1.0.so.0 /usr/lib64/libpangocairo-1.0.so.0 \ /usr/lib64/libcairo.so.2 /usr/lib64/libcairo-gobject.so.2 \ /usr/lib64/libgdk_pixbuf-2.0.so.0 \ /usr/lib64/libatk-1.0.so.0 /usr/lib64/libatk-bridge-2.0.so.0 \ /usr/lib64/libharfbuzz.so.0 /usr/lib64/libgio-2.0.so.0 \ /usr/lib64/libglib-2.0.so.0; do ldd "$so" 2>/dev/null | grep -oP '/\S+' done | sort -u > "$_ff_libs" # Also add libs that Firefox dlopen's but ldd doesn't show for extra in /usr/lib64/libpangoft2-1.0.so.0 \ /usr/lib64/libfontconfig.so.1 \ /usr/lib64/libfreetype.so.6 \ /usr/lib64/libpixman-1.so.0 \ /usr/lib64/libdbus-1.so.3 \ /usr/lib64/libxkbcommon.so.0 \ /usr/lib64/libwayland-client.so.0 \ /usr/lib64/libwayland-cursor.so.0 \ /usr/lib64/libwayland-egl.so.1 \ /usr/lib64/libEGL.so.1 \ /usr/lib64/libEGL_mesa.so.0 \ /usr/lib64/libgbm.so.1; do [ -f "$extra" ] && echo "$extra" >> "$_ff_libs" done local _count=0 while IFS= read -r lib; do [ -f "$lib" ] && sudo cp -nL "$lib" "${INITRAMFS_ROOT}/lib64/" 2>/dev/null && _count=$((_count + 1)) done < "$_ff_libs" rm -f "$_ff_libs" log " firefox libs: ${_count} files, $(du -sh "${INITRAMFS_ROOT}/lib64/" | cut -f1) total" # Firefox runtime deps: fontconfig, xkb keyboard data, a basic font log "Bundling Firefox runtime deps (fontconfig, xkb, font)..." sudo mkdir -p "${INITRAMFS_ROOT}/etc/fonts" "${INITRAMFS_ROOT}/usr/share/X11" \ "${INITRAMFS_ROOT}/usr/share/fonts" sudo cp -a /etc/fonts/. "${INITRAMFS_ROOT}/etc/fonts/" 2>/dev/null || true # xkb is often a symlink — dereference it sudo cp -aL /usr/share/X11/xkb "${INITRAMFS_ROOT}/usr/share/X11/" 2>/dev/null || \ sudo cp -rL /usr/share/X11/xkb "${INITRAMFS_ROOT}/usr/share/X11/" 2>/dev/null || true # Single font is enough for OAuth page local FONT_SRC="$(find /usr/share/fonts -name 'DroidSans.ttf' -o -name 'DejaVuSans.ttf' 2>/dev/null | head -1)" if [ -n "${FONT_SRC}" ]; then sudo mkdir -p "${INITRAMFS_ROOT}/usr/share/fonts/default" sudo cp "${FONT_SRC}" "${INITRAMFS_ROOT}/usr/share/fonts/default/" fi # Fontconfig cache dir sudo mkdir -p "${INITRAMFS_ROOT}/var/cache/fontconfig" # GLib schemas, GTK modules, GDK-Pixbuf loaders — Firefox segfaults without these sudo mkdir -p "${INITRAMFS_ROOT}/usr/share/glib-2.0" sudo cp -a /usr/share/glib-2.0/schemas "${INITRAMFS_ROOT}/usr/share/glib-2.0/" 2>/dev/null || true sudo cp -aL /usr/lib64/gtk-3.0 "${INITRAMFS_ROOT}/usr/lib64/" 2>/dev/null || true sudo cp -aL /usr/lib64/gdk-pixbuf-2.0 "${INITRAMFS_ROOT}/usr/lib64/" 2>/dev/null || true # Generate gdk-pixbuf loaders cache gdk-pixbuf-query-loaders-64 2>/dev/null | sudo tee "${INITRAMFS_ROOT}/usr/lib64/gdk-pixbuf-2.0/2.10.0/loaders.cache" > /dev/null # Glycin image loaders — Fedora 43's gdk-pixbuf delegates PNG/JPEG to Glycin if [ -f /usr/lib64/libglycin-2.so.0 ]; then sudo cp /usr/lib64/libglycin-2.so.0 "${INITRAMFS_ROOT}/lib64/" sudo mkdir -p "${INITRAMFS_ROOT}/usr/libexec/glycin-loaders/2+" sudo cp /usr/libexec/glycin-loaders/2+/glycin-image-rs "${INITRAMFS_ROOT}/usr/libexec/glycin-loaders/2+/" sudo mkdir -p "${INITRAMFS_ROOT}/usr/share/glycin-loaders/2+/conf.d" sudo cp /usr/share/glycin-loaders/2+/conf.d/glycin-image-rs.conf "${INITRAMFS_ROOT}/usr/share/glycin-loaders/2+/conf.d/" # Bundle glycin-image-rs dependencies for lib in $(ldd /usr/libexec/glycin-loaders/2+/glycin-image-rs 2>/dev/null | grep -oP '/\S+'); do [ -f "$lib" ] && sudo cp -n "$lib" "${INITRAMFS_ROOT}/lib64/" 2>/dev/null || true done log " glycin: $(du -sh "${INITRAMFS_ROOT}/usr/libexec/glycin-loaders" | cut -f1)" fi # Pango modules sudo cp -aL /usr/lib64/pango "${INITRAMFS_ROOT}/usr/lib64/" 2>/dev/null || true log " fontconfig+xkb+font: $(du -sh "${INITRAMFS_ROOT}/etc/fonts" "${INITRAMFS_ROOT}/usr/share/X11/xkb" 2>/dev/null | tail -1 | cut -f1)" fi # USE_BROWSER # Node.js: only bundled if explicitly requested (USE_NODE=1) # KidLisp evaluation uses QuickJS in ac-native, not Node.js if [ "${USE_NODE:-0}" = "1" ]; then local NODE_BIN="$(command -v node 2>/dev/null || true)" if [ -n "${NODE_BIN}" ] && [ -f "${NODE_BIN}" ]; then log "Bundling Node.js (USE_NODE=1)..." sudo cp "${NODE_BIN}" "${INITRAMFS_ROOT}/bin/node" sudo chmod +x "${INITRAMFS_ROOT}/bin/node" for lib in $(ldd "${NODE_BIN}" 2>/dev/null | grep -oP '/\S+'); do [ -f "$lib" ] && sudo cp -n "$lib" "${INITRAMFS_ROOT}/lib64/" 2>/dev/null || true done log " node: $(du -sh "${INITRAMFS_ROOT}/bin/node" | cut -f1) ($(${NODE_BIN} --version))" fi else # Remove stale node binary from previous builds sudo rm -f "${INITRAMFS_ROOT}/bin/node" fi # Copy extra firmware blobs from source tree into initramfs if [ -d "${SCRIPT_DIR}/firmware" ]; then log "Copying firmware blobs..." sudo cp -r "${SCRIPT_DIR}/firmware/"* "${INITRAMFS_ROOT}/lib/firmware/" 2>/dev/null || true fi # Repack cpio + zstd (better compression than lz4, kernel supports it) log "Repacking initramfs..." cd "${INITRAMFS_ROOT}" sudo find . -print0 | sudo cpio --null -ov --format=newc \ > "${BUILD_DIR}/initramfs.cpio" 2>/dev/null lz4 -l -9 -f "${BUILD_DIR}/initramfs.cpio" "${BUILD_DIR}/initramfs.cpio.lz4" # Fix ownership so non-root users (oven) can rebuild next time sudo chown -R "$(whoami)" "${INITRAMFS_ROOT}" "${BUILD_DIR}/initramfs.cpio" "${BUILD_DIR}/initramfs.cpio.lz4" 2>/dev/null || true # === Validate initramfs will boot === log "Validating initramfs..." local BOOT_OK=1 # 1. init script exists and is executable if [ ! -x "${INITRAMFS_ROOT}/init" ]; then err "VALIDATE: /init missing or not executable" BOOT_OK=0 fi # 2. ac-native binary exists if [ ! -f "${INITRAMFS_ROOT}/ac-native" ]; then err "VALIDATE: /ac-native missing" BOOT_OK=0 fi # 3. Dynamic linker matches binary's interpreter local INTERP=$(readelf -l "${INITRAMFS_ROOT}/ac-native" 2>/dev/null | grep -oP '(?<=interpreter: ).*(?=\])') if [ -n "${INTERP}" ] && [ ! -f "${INITRAMFS_ROOT}${INTERP}" ]; then err "VALIDATE: binary needs ${INTERP} but it's missing from initramfs" BOOT_OK=0 else log " linker: ${INTERP} OK" fi # 4. All NEEDED shared libs exist in initramfs local MISSING="" for lib in $(readelf -d "${INITRAMFS_ROOT}/ac-native" 2>/dev/null | grep -oP '(?<=\[)lib[^\]]+'); do if [ ! -f "${INITRAMFS_ROOT}/lib/${lib}" ] && [ ! -f "${INITRAMFS_ROOT}/lib64/${lib}" ]; then MISSING="${MISSING} ${lib}" fi done if [ -n "${MISSING}" ]; then err "VALIDATE: missing shared libs in initramfs:${MISSING}" BOOT_OK=0 else log " shared libs: all present" fi # 5. piece.mjs exists if [ ! -f "${INITRAMFS_ROOT}/piece.mjs" ]; then err "VALIDATE: /piece.mjs missing" BOOT_OK=0 fi # 6. WiFi firmware exists (non-fatal warning) if ! ls "${INITRAMFS_ROOT}/lib/firmware/iwlwifi"*.ucode &>/dev/null; then log " WARNING: no iwlwifi firmware — WiFi will not work" else log " firmware: $(ls ${INITRAMFS_ROOT}/lib/firmware/iwlwifi*.ucode | wc -l) iwlwifi blobs" fi local INITRAMFS_SIZE=$(du -sh "${BUILD_DIR}/initramfs.cpio.lz4" | cut -f1) log " initramfs size: ${INITRAMFS_SIZE}" if [ "${BOOT_OK}" -eq 0 ]; then err "Initramfs validation FAILED — kernel would panic. Aborting." exit 1 fi log "Initramfs validated — will boot"}smoke_test() { log "Smoke test: booting kernel in QEMU (15s timeout)..." local SERIAL_LOG=$(mktemp) local TIMEOUT=15 local QEMU_BIN="" # Find QEMU binary if command -v qemu-system-x86_64 &>/dev/null; then QEMU_BIN="qemu-system-x86_64" elif sudo docker info &>/dev/null 2>&1; then # Fall back to Docker container log " using Docker for QEMU..." sudo docker rm -f ac-os-smoke 2>/dev/null || true local CID CID=$(sudo docker create --name ac-os-smoke --device /dev/kvm \ alpine:latest sh -c "apk add --quiet qemu-system-x86_64timeout ${TIMEOUT} qemu-system-x86_64 \ -enable-kvm \ -kernel /tmp/vmlinuz \ -append 'console=ttyS0 loglevel=4' \ -m 512M \ -no-reboot \ -nographic \ -serial stdio \ -nic none 2>&1 || true") sudo docker cp "${VMLINUZ}" "${CID}:/tmp/vmlinuz" sudo docker start -a "${CID}" > "${SERIAL_LOG}" 2>&1 & local PID=$! sleep ${TIMEOUT} kill ${PID} 2>/dev/null || true sudo docker rm -f ac-os-smoke 2>/dev/null || true # Check serial output if grep -q "Booted in\|JS: boot=1\|exec-ac-native" "${SERIAL_LOG}"; then log "SMOKE TEST PASSED — ac-native started" rm -f "${SERIAL_LOG}" return 0 elif grep -qi "panic\|Oops\|not syncing" "${SERIAL_LOG}"; then err "SMOKE TEST FAILED — kernel panic detected" err "Serial output (last 20 lines):" tail -20 "${SERIAL_LOG}" >&2 rm -f "${SERIAL_LOG}" exit 1 else log " WARNING: no definitive boot signal (may need longer timeout)" log " Serial tail:" tail -10 "${SERIAL_LOG}" rm -f "${SERIAL_LOG}" return 0 fi else log " QEMU not available — skipping smoke test" return 0 fi # Direct QEMU (no Docker) timeout ${TIMEOUT} ${QEMU_BIN} \ $([ -e /dev/kvm ] && echo "-enable-kvm") \ -kernel "${VMLINUZ}" \ -append "console=ttyS0 loglevel=4" \ -m 512M \ -no-reboot \ -nographic \ -serial stdio \ -nic none > "${SERIAL_LOG}" 2>&1 || true if grep -q "Booted in\|JS: boot=1\|exec-ac-native" "${SERIAL_LOG}"; then log "SMOKE TEST PASSED — ac-native started" rm -f "${SERIAL_LOG}" return 0 elif grep -qi "panic\|Oops\|not syncing" "${SERIAL_LOG}"; then err "SMOKE TEST FAILED — kernel panic detected" err "Serial output (last 20 lines):" tail -20 "${SERIAL_LOG}" >&2 rm -f "${SERIAL_LOG}" exit 1 else log " WARNING: no definitive boot signal (may need longer timeout)" tail -10 "${SERIAL_LOG}" rm -f "${SERIAL_LOG}" return 0 fi}build_kernel() { local LINUX_DIR="${BUILD_DIR}/linux-6.14.2" local HOST_FW_DIR="" if [ ! -d "${LINUX_DIR}" ]; then err "Linux source not found at ${LINUX_DIR}" exit 1 fi # Sync config-minimal into kernel .config if it's newer local CFG_SRC="${SCRIPT_DIR}/kernel/config-minimal" if [ -f "${CFG_SRC}" ] && [ "${CFG_SRC}" -nt "${LINUX_DIR}/.config" ]; then log "Updating kernel .config from config-minimal..." cp "${CFG_SRC}" "${LINUX_DIR}/.config" cd "${LINUX_DIR}" make olddefconfig 2>&1 | tail -5 else cd "${LINUX_DIR}" fi copy_builtin_fw_blob() { local rel="$1" local dst="$2/$rel" mkdir -p "$(dirname "${dst}")" # Already staged from a previous run or manual download if [ -f "${dst}" ]; then return 0 fi if [ -f "${HOST_FW_DIR}/${rel}" ]; then cp -L "${HOST_FW_DIR}/${rel}" "${dst}" return 0 fi if [ -f "${HOST_FW_DIR}/${rel}.zst" ]; then zstd -d "${HOST_FW_DIR}/${rel}.zst" -o "${dst}" 2>/dev/null && return 0 fi if [ -f "${HOST_FW_DIR}/${rel}.xz" ]; then xz -dc "${HOST_FW_DIR}/${rel}.xz" > "${dst}" 2>/dev/null && return 0 fi return 1 } for d in /usr/lib/firmware /lib/firmware; do if [ -d "${d}" ]; then HOST_FW_DIR="${d}" break fi done # Inject built-in firmware paths if firmware blobs exist locally local FW_DIR="${BUILD_DIR}/firmware" local FW_LIST FW_LIST=$(sed -n 's/^CONFIG_EXTRA_FIRMWARE="\([^"]*\)"/\1/p' "${LINUX_DIR}/.config" | head -1) if [ -n "${FW_LIST}" ]; then mkdir -p "${FW_DIR}" if [ -n "${HOST_FW_DIR}" ]; then local missing_fw="" local fw="" for fw in ${FW_LIST}; do if ! copy_builtin_fw_blob "${fw}" "${FW_DIR}"; then missing_fw="${missing_fw} ${fw}" fi done if [ -n "${missing_fw}" ]; then err "Missing built-in firmware blobs:${missing_fw}" err "Looked under: ${HOST_FW_DIR}" exit 1 fi fi sed -i "s|^CONFIG_EXTRA_FIRMWARE=.*|CONFIG_EXTRA_FIRMWARE=\"${FW_LIST}\"|" "${LINUX_DIR}/.config" sed -i "s|^CONFIG_EXTRA_FIRMWARE_DIR=.*|CONFIG_EXTRA_FIRMWARE_DIR=\"${FW_DIR}\"|" "${LINUX_DIR}/.config" log "Built-in firmware: ${FW_LIST}" fi # Copy fresh initramfs into kernel tree (kernel embeds it — must be updated!) log "Syncing initramfs into kernel tree..." sudo cp "${BUILD_DIR}/initramfs.cpio.lz4" "${LINUX_DIR}/initramfs.cpio.lz4" # Force kernel to re-embed: remove cached initramfs object (make won't detect content changes) sudo rm -f "${LINUX_DIR}/usr/initramfs_data.o" "${LINUX_DIR}/usr/.initramfs_data.o.cmd" log "Building kernel..." make -j$(nproc) bzImage 2>&1 | tail -1 sudo cp arch/x86/boot/bzImage "${VMLINUZ}" sudo chown $(whoami) "${VMLINUZ}" log "Kernel: $(du -sh "${VMLINUZ}" | cut -f1)" # Also build a slim kernel WITHOUT embedded initramfs for Mac EFI boot. # Mac firmware can't load a 270MB EFI application. The slim kernel (~15MB) # loads via systemd-boot, which then provides the initramfs separately. log "Building slim kernel (no embedded initramfs) for Mac boot..." sed -i 's|^CONFIG_INITRAMFS_SOURCE=.*|CONFIG_INITRAMFS_SOURCE=""|' "${LINUX_DIR}/.config" make olddefconfig 2>&1 | tail -5 make clean 2>/dev/null || true sudo rm -f "${LINUX_DIR}/usr/initramfs_data.o" "${LINUX_DIR}/usr/.initramfs_data.o.cmd" make -j$(nproc) bzImage 2>&1 | tail -1 sudo cp arch/x86/boot/bzImage "${BUILD_DIR}/vmlinuz-slim" sudo chown $(whoami) "${BUILD_DIR}/vmlinuz-slim" log "Slim kernel: $(du -sh "${BUILD_DIR}/vmlinuz-slim" | cut -f1)" # Restore config for next build sed -i 's|^CONFIG_INITRAMFS_SOURCE=.*|CONFIG_INITRAMFS_SOURCE="initramfs.cpio.lz4"|' "${LINUX_DIR}/.config" make olddefconfig 2>&1 | tail -5}record_build() { # Record every build in MongoDB (even local/flash-only builds) if ! command -v node &>/dev/null; then log "Node not available — skipping MongoDB build record" return 0 fi local SHA=$(sha256sum "${VMLINUZ}" 2>/dev/null | cut -d' ' -f1) local SIZE=$(stat -c%s "${VMLINUZ}" 2>/dev/null || echo 0) cd "${SCRIPT_DIR}" log "Recording build: ${AC_BUILD_NAME} (${AC_GIT_HASH})" echo "{ \"name\": \"${AC_BUILD_NAME}\", \"buildNum\": -1, \"version\": \"${AC_GIT_HASH}-${AC_BUILD_TS}\", \"sha256\": \"${SHA}\", \"size\": ${SIZE}, \"git_hash\": \"${AC_GIT_HASH}\", \"build_ts\": \"${AC_BUILD_TS}\", \"url\": \"local\", \"target\": \"${1:-build}\", \"user\": \"${AC_USER_SUB:-${ADMIN_SUB:-unknown}}\" }" | node scripts/track-build.mjs record 2>&1 || log "MongoDB record failed (non-fatal)"}find_usb_dev() { if [ -n "${AC_USB_DEV:-}" ]; then if [ ! -b "${AC_USB_DEV}" ]; then err "AC_USB_DEV is not a block device: ${AC_USB_DEV}" return 1 fi echo "${AC_USB_DEV}" return 0 fi # Find removable USB block device via sysfs for dev in sda sdb sdc sdd sde; do if [ -e "/sys/block/${dev}/removable" ]; then local rem=$(cat "/sys/block/${dev}/removable" 2>/dev/null) if [ "${rem}" = "1" ] && [ -e "/sys/block/${dev}" ]; then local dev_node="/dev/${dev}" if [ ! -b "${dev_node}" ]; then local majmin majmin=$(cat "/sys/block/${dev}/dev" 2>/dev/null || true) if [ -z "${majmin}" ]; then err "USB sysfs node exists but major:minor is missing for ${dev}" return 1 fi log "Repairing ${dev_node} as block device ${majmin}" >&2 sudo rm -f "${dev_node}" sudo mknod "${dev_node}" b "${majmin%%:*}" "${majmin##*:}" fi if [ -b "${dev_node}" ]; then echo "${dev_node}" return 0 fi fi fi done return 1}ensure_media_helper_image() { if sudo docker image inspect "${MEDIA_HELPER_IMAGE}" >/dev/null 2>&1; then return 0 fi log "Building media helper image (${MEDIA_HELPER_IMAGE})..." local repo_root repo_root="$(cd "${SCRIPT_DIR}/../.." && pwd)" sudo env DOCKER_BUILDKIT=0 docker build \ -t "${MEDIA_HELPER_IMAGE}" \ -f "${MEDIA_HELPER_DOCKERFILE}" \ "${repo_root}" >/dev/null}usb_part_path() { local dev="$1" local idx="$2" if [[ "${dev}" =~ [0-9]$ ]]; then printf '%sp%s\n' "${dev}" "${idx}" else printf '%s%s\n' "${dev}" "${idx}" fi}preserve_existing_usb_wifi_creds() { local USB_DEV="$1" local OUTPUT_FILE="$2" local MOUNT_DIR MOUNT_DIR="$(mktemp -d /tmp/ac-usb-preserve.XXXXXX)" for idx in 1 2 3 4; do local PART PART="$(usb_part_path "${USB_DEV}" "${idx}")" [ -b "${PART}" ] || continue if ! sudo mount -t vfat -o ro "${PART}" "${MOUNT_DIR}" 2>/dev/null; then continue fi if [ -f "${MOUNT_DIR}/wifi_creds.json" ]; then if sudo cp "${MOUNT_DIR}/wifi_creds.json" "${OUTPUT_FILE}"; then sudo chown "$(id -u):$(id -g)" "${OUTPUT_FILE}" 2>/dev/null || true sudo umount "${MOUNT_DIR}" >/dev/null 2>&1 || true rmdir "${MOUNT_DIR}" 2>/dev/null || true log "Preserved wifi_creds.json from ${PART}" return 0 fi fi sudo umount "${MOUNT_DIR}" >/dev/null 2>&1 || true done rmdir "${MOUNT_DIR}" 2>/dev/null || true return 1}require_login() { # Require ac-login before flashing — ensures builds are tied to an authenticated user local TOKEN_FILE="${HOME}/.ac-token" local LOGIN_SCRIPT="${SCRIPT_DIR}/../../tezos/ac-login.mjs" if [ ! -f "${TOKEN_FILE}" ]; then if [ -f "${LOGIN_SCRIPT}" ]; then log "No token found — starting ac-login..." node "${LOGIN_SCRIPT}" else err "Not logged in and ac-login script not found." exit 1 fi fi if [ ! -f "${TOKEN_FILE}" ]; then err "Login failed or cancelled. Run: ac-login" exit 1 fi # Read user sub from token file local USER_SUB=$(node -e "const t=JSON.parse(require('fs').readFileSync('${TOKEN_FILE}','utf8')); console.log(t.user?.sub || '')" 2>/dev/null) if [ -z "${USER_SUB}" ]; then err "Token expired or invalid. Run: ac-login" exit 1 fi # Read handle and email too local USER_HANDLE=$(node -e "const t=JSON.parse(require('fs').readFileSync('${TOKEN_FILE}','utf8')); let h=t.user?.handle || t.user?.name || ''; if(h.startsWith('@'))h=h.slice(1); console.log(h)" 2>/dev/null) local USER_EMAIL=$(node -e "const t=JSON.parse(require('fs').readFileSync('${TOKEN_FILE}','utf8')); console.log(t.user?.email || '')" 2>/dev/null) export AC_USER_SUB="${USER_SUB}" export AC_USER_HANDLE="${USER_HANDLE}" export AC_USER_EMAIL="${USER_EMAIL}" log "Authenticated: @${USER_HANDLE}"}read_ac_access_token() { local TOKEN_FILE="${HOME}/.ac-token" [ -f "${TOKEN_FILE}" ] || return 1 node -e " const t = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8')); process.stdout.write(t.access_token || ''); " "${TOKEN_FILE}" 2>/dev/null}fetch_authenticated_handle_tokens() { local AC_ACCESS_TOKEN="${1:-}" [ -n "${AC_ACCESS_TOKEN}" ] || return 1 curl -fsSL \ -H "Authorization: Bearer ${AC_ACCESS_TOKEN}" \ "https://aesthetic.computer/.netlify/functions/claude-token" \ --max-time 20 2>/dev/null}should_bake_local_claude_session() { local MODE="${AC_FLASH_INCLUDE_LOCAL_CLAUDE:-auto}" case "${MODE}" in 1|true|TRUE|yes|YES) return 0 ;; 0|false|FALSE|no|NO) return 1 ;; esac [ -n "${AC_USER_EMAIL:-}" ] || return 1 [ -f "${HOME}/.claude.json" ] || return 1 local LOCAL_EMAIL LOCAL_EMAIL=$(node -e " const d = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8')); const email = d.oauthAccount?.emailAddress || ''; process.stdout.write(String(email).trim().toLowerCase()); " "${HOME}/.claude.json" 2>/dev/null) [ -n "${LOCAL_EMAIL}" ] || return 1 [ "${LOCAL_EMAIL}" = "$(printf '%s' "${AC_USER_EMAIL}" | tr '[:upper:]' '[:lower:]')" ]}write_usb_config_file() { local CONFIG_FILE="$1" local USB_CONFIG local AC_ACCESS_TOKEN local HANDLE_TOKEN_JSON="" local HANDLE_TOKEN_HANDLE="" local CLAUDE_TOKEN="" local GITHUB_PAT="" local CLAUDE_CREDS="" local CLAUDE_STATE="" # Pre-built inscription path: ac-inscribe writes a JSON envelope to # AC_INSCRIPTION_FILE (default /tmp/ac-os-pull/inscription.json). When # present, skip the live API fetch and use the artifact's usbConfig # directly. This makes flash deterministic — no network calls during # the USB write — and lets `ac-inscribe` print a separate validation # summary for the operator before block-device work begins. local AC_INSCRIPTION_FILE="${AC_INSCRIPTION_FILE:-/tmp/ac-os-pull/inscription.json}" if [ -f "${AC_INSCRIPTION_FILE}" ]; then local INSCRIBE_LIB="${SCRIPT_DIR}/scripts/inscribe-lib.sh" if [ -f "${INSCRIBE_LIB}" ]; then # shellcheck source=/dev/null source "${INSCRIBE_LIB}" USB_CONFIG=$(aci_extract_usb_config_from_inscription "${AC_INSCRIPTION_FILE}") || { err "Failed to read inscription at ${AC_INSCRIPTION_FILE} (corrupt?)" return 1 } ac_media_write_legacy_config "${CONFIG_FILE}" "${USB_CONFIG}" log "USB config: from inscription ${AC_INSCRIPTION_FILE}" log "USB config: $(ac_media_summarize_config_file "${CONFIG_FILE}")" return 0 fi fi # AC_ANON=1 produces a shareable USB — no handle, no tokens, no # Claude/GitHub creds. Flash this when giving someone a device to # boot into notepat without inscribing your session onto it. if [ "${AC_ANON:-0}" = "1" ]; then USB_CONFIG='{"handle":"anonymous"}' ac_media_write_legacy_config "${CONFIG_FILE}" "${USB_CONFIG}" log "USB config: anonymous (no tokens, no session)" return 0 fi if [ -n "${AC_USER_HANDLE:-}" ]; then AC_ACCESS_TOKEN=$(read_ac_access_token) HANDLE_TOKEN_JSON=$(fetch_authenticated_handle_tokens "${AC_ACCESS_TOKEN}" || true) if [ -n "${HANDLE_TOKEN_JSON}" ]; then HANDLE_TOKEN_HANDLE=$(node -e " const d = JSON.parse(process.argv[1] || '{}'); process.stdout.write(d.handle || ''); " "${HANDLE_TOKEN_JSON}" 2>/dev/null) CLAUDE_TOKEN=$(node -e " const d = JSON.parse(process.argv[1] || '{}'); process.stdout.write(d.token || ''); " "${HANDLE_TOKEN_JSON}" 2>/dev/null) GITHUB_PAT=$(node -e " const d = JSON.parse(process.argv[1] || '{}'); process.stdout.write(d.githubPat || ''); " "${HANDLE_TOKEN_JSON}" 2>/dev/null) if [ -n "${HANDLE_TOKEN_HANDLE}" ] && [ "${HANDLE_TOKEN_HANDLE}" != "${AC_USER_HANDLE}" ]; then err "Authenticated handle mismatch: token API returned @${HANDLE_TOKEN_HANDLE}, CLI login is @${AC_USER_HANDLE}" fi log "Handle tokens: claude=$([ -n "${CLAUDE_TOKEN}" ] && echo yes || echo no) github=$([ -n "${GITHUB_PAT}" ] && echo yes || echo no)" else log "Handle tokens: unavailable" fi if should_bake_local_claude_session; then if [ -f "${HOME}/.claude/.credentials.json" ]; then CLAUDE_CREDS=$(cat "${HOME}/.claude/.credentials.json" 2>/dev/null) fi if [ -f "${HOME}/.claude.json" ]; then CLAUDE_STATE=$(node -e " const d=JSON.parse(require('fs').readFileSync(process.env.HOME+'/.claude.json','utf8')); console.log(JSON.stringify({ oauthAccount:d.oauthAccount, hasCompletedOnboarding:true, installMethod:'manual', numStartups:1, autoUpdates:false, autoUpdatesProtectedForNative:true })); " 2>/dev/null) fi log "Local Claude session: baked for ${AC_USER_EMAIL}" elif [ -f "${HOME}/.claude.json" ] || [ -f "${HOME}/.claude/.credentials.json" ]; then log "Local Claude session: skipped (email mismatch or disabled)" fi USB_CONFIG=$(node -e " const [handle, sub, email, token, claudeToken, githubPat, claudeCreds, claudeState] = process.argv.slice(1); const cfg = { handle, sub, email, token }; if (claudeToken) cfg.claudeToken = claudeToken; if (githubPat) cfg.githubPat = githubPat; try { if (claudeCreds) cfg.claudeCreds = JSON.parse(claudeCreds); } catch (e) {} try { if (claudeState) cfg.claudeState = JSON.parse(claudeState); } catch (e) {} console.log(JSON.stringify(cfg)); " "${AC_USER_HANDLE}" "${AC_USER_SUB}" "${AC_USER_EMAIL}" "${AC_ACCESS_TOKEN}" "${CLAUDE_TOKEN}" "${GITHUB_PAT}" "${CLAUDE_CREDS}" "${CLAUDE_STATE}" 2>/dev/null) else USB_CONFIG='{"handle":"unknown"}' fi ac_media_write_legacy_config "${CONFIG_FILE}" "${USB_CONFIG}" log "USB config: $(ac_media_summarize_config_file "${CONFIG_FILE}")"}nixos_data_partition_for() { local USB_DEV="$1" local DATA_LABEL local PART DATA_LABEL="$(ac_media_nixos_data_label)" for PART in "${USB_DEV}"? "${USB_DEV}"p? "${USB_DEV}"?? "${USB_DEV}"p??; do [ -b "${PART}" ] || continue if [ "$(sudo blkid -o value -s LABEL "${PART}" 2>/dev/null || true)" = "${DATA_LABEL}" ]; then printf '%s\n' "${PART}" return 0 fi done return 1}write_nixos_usb_config() { local USB_DEV="$1" local CONFIG_FILE="$2" local DATA_PART="" local DATA_MOUNT local attempts sudo partprobe "${USB_DEV}" 2>/dev/null || true sudo udevadm settle 2>/dev/null || true for attempts in $(seq 1 40); do DATA_PART="$(nixos_data_partition_for "${USB_DEV}" || true)" [ -n "${DATA_PART}" ] && break sleep 0.25 done if [ -z "${DATA_PART}" ]; then err "No $(ac_media_nixos_data_label) partition found on ${USB_DEV}" return 1 fi DATA_MOUNT="$(mktemp -d /tmp/ac-nixos-data.XXXXXX)" if ! sudo mount -t vfat \ -o "rw,uid=1000,gid=100,umask=0077,shortname=mixed,utf8=1" \ "${DATA_PART}" "${DATA_MOUNT}"; then rmdir "${DATA_MOUNT}" 2>/dev/null || true err "Failed to mount ${DATA_PART}" return 1 fi if ! sudo mkdir -p "${DATA_MOUNT}/logs" || ! sudo cp "${CONFIG_FILE}" "${DATA_MOUNT}/config.json"; then sudo umount "${DATA_MOUNT}" >/dev/null 2>&1 || true rmdir "${DATA_MOUNT}" 2>/dev/null || true err "Failed to write NixOS config to ${DATA_PART}" return 1 fi sync sudo umount "${DATA_MOUNT}" rmdir "${DATA_MOUNT}" log "Wrote NixOS config to ${DATA_PART}"}prepare_nixos_image() { local IMAGE_PATH="$1" local CONFIG_FILE="$2" local WORK_DIR ensure_media_helper_image WORK_DIR="$(dirname "${IMAGE_PATH}")" sudo docker run --rm --privileged \ -v "${WORK_DIR}:/work" \ --entrypoint /bin/bash "${MEDIA_HELPER_IMAGE}" \ -lc "exec /usr/local/bin/ac-os-nixos-image-helper '/work/$(basename "${IMAGE_PATH}")' '/work/$(basename "${CONFIG_FILE}")'"}flash_nixos_image_to_usb() { local IMAGE_PATH="$1" local CONFIG_FILE="$2" local USB_DEV prepare_nixos_image "${IMAGE_PATH}" "${CONFIG_FILE}" USB_DEV="$(find_usb_dev)" || { err "No USB device found"; exit 1; } log "Flashing ${IMAGE_PATH} to ${USB_DEV}..." sudo dd if="${IMAGE_PATH}" of="${USB_DEV}" bs=4M status=progress oflag=sync sync write_nixos_usb_config "${USB_DEV}" "${CONFIG_FILE}" log "NixOS USB ready!"}ensure_inscription() { # Auto-build a credential inscription if none was prepared by # ac-inscribe ahead of time. Both the macOS flash-mac.sh path and # the Linux write_usb_config_file path read from this artifact, so # the inscribe step is the unified credential-gathering point. local INSCRIPTION="${AC_INSCRIPTION_FILE:-/tmp/ac-os-pull/inscription.json}" export AC_INSCRIPTION_FILE="${INSCRIPTION}" if [ -f "${INSCRIPTION}" ]; then log "Using existing inscription: ${INSCRIPTION}" return 0 fi local AC_INSCRIBE="${SCRIPT_DIR}/ac-inscribe" if [ ! -x "${AC_INSCRIBE}" ]; then log "ac-inscribe not found at ${AC_INSCRIBE} — falling back to legacy live fetch" return 0 fi log "No inscription found — running ac-inscribe..." if [ "${AC_ANON:-0}" = "1" ]; then "${AC_INSCRIBE}" --anon --yes --out "${INSCRIPTION}" || { err "ac-inscribe --anon failed" return 1 } else "${AC_INSCRIBE}" --yes --out "${INSCRIPTION}" || { err "ac-inscribe failed — fix above and retry" return 1 } fi}flash_usb() { ensure_inscription || exit 1 # macOS path: defer to scripts/flash-mac.sh (uses diskutil + sgdisk; no # Docker / no /sys/block). The Linux flow below uses the privileged # flash-helper container. if [ "$(uname)" = "Darwin" ]; then local MAC_USB="${AC_USB_DEV:-}" if [ -z "${MAC_USB}" ]; then log "Auto-detecting external USB via diskutil..." # Pick the first removable external whole-disk identifier. MAC_USB=$(diskutil list external 2>/dev/null \ | awk '/^\/dev\/disk[0-9]+ \(external, physical\):/{print $1; exit}') [ -z "${MAC_USB}" ] && { err "No external USB found. Set AC_USB_DEV=/dev/diskN"; exit 1; } fi local SRC="${PULL_DIR:-/tmp/ac-os-pull}" log "Flashing ${MAC_USB} via flash-mac.sh (source: ${SRC})..." ac_tray_state flashing sudo "${SCRIPT_DIR}/scripts/flash-mac.sh" "${MAC_USB}" "${SRC}" local _frc=$? [ "${_frc}" -eq 0 ] && ac_tray_state clear # burned → clear the badge return "${_frc}" fi local USB_DEV local MEDIA_TMP local CONFIG_FILE local STAGE_ROOT local CONTAINER local WIFI_CREDS_FILE USB_DEV="$(find_usb_dev)" || { err "No USB device found"; exit 1; } log "Flashing to ${USB_DEV}..." MEDIA_TMP="$(mktemp -d /tmp/ac-media.XXXXXX)" CONFIG_FILE="${MEDIA_TMP}/config.json" STAGE_ROOT="${MEDIA_TMP}/staged-root" WIFI_CREDS_FILE="${MEDIA_TMP}/wifi_creds.json" cleanup_flash_usb() { if [ -n "${CONTAINER:-}" ]; then sudo docker rm -f "${CONTAINER}" >/dev/null 2>&1 || true fi rm -rf "${MEDIA_TMP}" } write_usb_config_file "${CONFIG_FILE}" # Always seed the global preset Wi-Fi list (kept in sync with the JS # auto-connect entries in notepat.mjs / wifi.mjs / prompt.mjs). If the # USB still has a valid wifi_creds.json from a previous flash, merge # the user's saved networks on top of the presets so personal entries # never get clobbered by a re-flash. local GLOBAL_WIFI_FILE="${MEDIA_TMP}/global-wifi-creds.json" local PRESERVED_WIFI_FILE="${MEDIA_TMP}/preserved-wifi-creds.json" ac_media_write_global_wifi_creds "${GLOBAL_WIFI_FILE}" if preserve_existing_usb_wifi_creds "${USB_DEV}" "${PRESERVED_WIFI_FILE}"; then ac_media_merge_wifi_creds "${WIFI_CREDS_FILE}" "${GLOBAL_WIFI_FILE}" "${PRESERVED_WIFI_FILE}" || \ cp "${GLOBAL_WIFI_FILE}" "${WIFI_CREDS_FILE}" else cp "${GLOBAL_WIFI_FILE}" "${WIFI_CREDS_FILE}" fi ac_media_stage_boot_tree "${STAGE_ROOT}" "${VMLINUZ}" "${CONFIG_FILE}" cp "${WIFI_CREDS_FILE}" "${STAGE_ROOT}/wifi_creds.json" ensure_media_helper_image CONTAINER=$(sudo docker create --rm --privileged -v /dev:/dev \ --entrypoint /bin/bash "${MEDIA_HELPER_IMAGE}" \ -lc "exec /usr/local/bin/ac-os-flash-helper '${USB_DEV}' /tmp/staged-root") sudo docker cp "${STAGE_ROOT}" "${CONTAINER}:/tmp/" if sudo docker start -a "${CONTAINER}"; then : else local rc=$? cleanup_flash_usb return "${rc}" fi log "Verified config: $(ac_media_summarize_config_file "${CONFIG_FILE}")" log "Verified $(ac_media_summarize_wifi_creds_file "${WIFI_CREDS_FILE}")" cleanup_flash_usb ac_tray_state clear # burned → clear the menu-bar badge}load_vault_creds() { # Decrypt DO Spaces credentials from vault GPG file # Tries: 1) already in env, 2) plaintext cache, 3) GPG decrypt (agent or prompt) if [ -n "${DO_SPACES_KEY:-}" ] && [ -n "${DO_SPACES_SECRET:-}" ]; then return 0 fi local CACHE="/tmp/.ac-upload-env" if [ -f "${CACHE}" ]; then set -a; source "${CACHE}"; set +a if [ -n "${DO_SPACES_KEY:-}" ] && [ -n "${DO_SPACES_SECRET:-}" ]; then log "Credentials loaded from session cache" return 0 fi fi # Find the encrypted file local GPG_FILE="" for candidate in \ "${SCRIPT_DIR}/upload.env.gpg" \ "${SCRIPT_DIR}/../../aesthetic-computer-vault/fedac/native/upload.env.gpg" \ "/home/me/aesthetic-computer-vault/fedac/native/upload.env.gpg"; do if [ -f "${candidate}" ]; then GPG_FILE="${candidate}" break fi done if [ -z "${GPG_FILE}" ]; then err "No upload.env.gpg found in vault or local paths" err "Expected locations:" err " ${SCRIPT_DIR}/upload.env.gpg" err " aesthetic-computer-vault/fedac/native/upload.env.gpg" return 1 fi log "Decrypting credentials from $(basename "${GPG_FILE}")..." # Try GPG agent first (no tty needed if key is cached) local DECRYPTED DECRYPTED=$(gpg --batch --pinentry-mode loopback -d "${GPG_FILE}" 2>/dev/null | grep "=") || true # If agent didn't have it cached, prompt for passphrase if [ -z "${DECRYPTED}" ]; then if [ -t 0 ]; then # Interactive terminal — let GPG prompt naturally log "GPG agent needs passphrase..." DECRYPTED=$(gpg --pinentry-mode loopback -d "${GPG_FILE}" 2>/dev/null | grep "=") || true fi fi if [ -z "${DECRYPTED}" ]; then err "Failed to decrypt ${GPG_FILE}" err "Import your key: gpg --import <key.asc>" err "Then retry — GPG will prompt for your passphrase" return 1 fi # Cache decrypted creds for this session (readable only by us) echo "${DECRYPTED}" > "${CACHE}" chmod 600 "${CACHE}" set -a; eval "${DECRYPTED}"; set +a log "Credentials decrypted and cached for session"}# Decrypt the oven OS_BUILD_ADMIN_KEY from the vault. Echoes the key to# stdout — caller is responsible for keeping it out of process listings,# logs, and shell history. Cached in /tmp for the session (chmod 600).load_oven_admin_key() { local CACHE="/tmp/.ac-oven-admin-key" if [ -f "${CACHE}" ]; then cat "${CACHE}" return 0 fi local GPG_FILE="" for candidate in \ "${SCRIPT_DIR}/../../aesthetic-computer-vault/oven/os-build-admin-key.txt.gpg" \ "${HOME}/aesthetic-computer-vault/oven/os-build-admin-key.txt.gpg" \ "/home/me/aesthetic-computer-vault/oven/os-build-admin-key.txt.gpg"; do if [ -f "${candidate}" ]; then GPG_FILE="${candidate}" break fi done if [ -z "${GPG_FILE}" ]; then err "No oven admin key found. Expected:" err " aesthetic-computer-vault/oven/os-build-admin-key.txt.gpg" return 1 fi local KEY KEY=$(gpg --batch --pinentry-mode loopback -d "${GPG_FILE}" 2>/dev/null | tr -d '\n\r ') if [ -z "${KEY}" ] && [ -t 0 ]; then # Agent didn't have it — let GPG prompt KEY=$(gpg --pinentry-mode loopback -d "${GPG_FILE}" 2>/dev/null | tr -d '\n\r ') fi if [ -z "${KEY}" ]; then err "Failed to decrypt ${GPG_FILE}" return 1 fi umask 077 printf '%s' "${KEY}" > "${CACHE}" printf '%s' "${KEY}"}# Trigger a remote OTA build on the oven. Reads admin key from the vault,# sends ref + changed-paths from local git state. Server-side workflow:# preflight-sync → docker build → kernel → upload OTA → done.oven_build() { local REF="${1:-HEAD}" local OVEN_HOST="${AC_OVEN_HOST:-https://oven.aesthetic.computer}" # Resolve ref to a full SHA. The poller checks out exactly this ref # on the build host; passing "HEAD" or a branch name is ambiguous. local SHA SHA=$(cd "${SCRIPT_DIR}" && git rev-parse "${REF}" 2>/dev/null) || { err "Cannot resolve ref: ${REF}" exit 1 } # Changed paths since the previous commit — informational only on the # server; the build always builds the resolved ref in full. local CHANGED CHANGED=$(cd "${SCRIPT_DIR}" && git show --name-only --pretty=format: "${SHA}" 2>/dev/null \ | grep -v '^$' | tr '\n' ',' | sed 's/,$//') local KEY KEY=$(load_oven_admin_key) || exit 1 log "Triggering oven build for ${SHA:0:9} on ${OVEN_HOST}..." local RESP RESP=$(curl -sS -w '\n__HTTP_STATUS__:%{http_code}' -X POST "${OVEN_HOST}/native-build" \ -H "Authorization: Bearer ${KEY}" \ -H "Content-Type: application/json" \ -d "{\"ref\":\"${SHA}\",\"changed_paths\":\"${CHANGED}\"}") || { err "Request failed" exit 1 } unset KEY local STATUS=$(printf '%s\n' "${RESP}" | grep '^__HTTP_STATUS__:' | cut -d: -f2) local BODY=$(printf '%s\n' "${RESP}" | sed '/^__HTTP_STATUS__:/d') case "${STATUS}" in 202) local JOB_ID=$(printf '%s' "${BODY}" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(d).id||'')}catch{}})" 2>/dev/null) log "Build started: ${JOB_ID}" log "Watch: ac-os oven watch" log "Status: ac-os oven status" log "Stream: ${OVEN_HOST}/native-build/${JOB_ID}/stream" ;; 409) local ACTIVE=$(printf '%s' "${BODY}" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(d).activeJobId||'')}catch{}})" 2>/dev/null) err "Oven busy — active job: ${ACTIVE}" err "Cancel with: curl -X POST ${OVEN_HOST}/native-build/${ACTIVE}/cancel -H 'Authorization: Bearer <key>'" exit 1 ;; *) err "HTTP ${STATUS}: ${BODY}" exit 1 ;; esac}oven_status() { local OVEN_HOST="${AC_OVEN_HOST:-https://oven.aesthetic.computer}" curl -sS "${OVEN_HOST}/native-build" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{ const j=JSON.parse(d); if(j.active){ const a=j.active; console.log('● ACTIVE '+a.id+' '+a.status+' '+a.stage+' '+(a.percent||0)+'% ref='+(a.ref||'').slice(0,9)); if(a.commitMsg)console.log(' '+a.commitMsg); } else { console.log('○ idle'); } console.log(''); console.log('recent:'); for(const r of (j.recent||[]).slice(0,8)){ const mark=r.status==='success'?'✓':r.status==='failed'?'✗':'·'; console.log(' '+mark+' '+r.id+' '+r.status.padEnd(10)+(r.buildName||'(unnamed)').padEnd(28)+(r.commitMsg||'').slice(0,60)); }});"}oven_watch() { local OVEN_HOST="${AC_OVEN_HOST:-https://oven.aesthetic.computer}" local ACTIVE=$(curl -sS "${OVEN_HOST}/native-build" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(d);console.log(j.activeJobId||'')}catch{}})" 2>/dev/null) if [ -z "${ACTIVE}" ]; then log "No active oven build." oven_status return 0 fi log "Streaming logs for ${ACTIVE} (Ctrl+C to detach)..." curl -sS -N "${OVEN_HOST}/native-build/${ACTIVE}/stream"}generate_template_image() { local IMAGE="/tmp/ac-native.img" local STAGING="/tmp/ac-img-staging" local LEGACY_CONFIG="/tmp/ac-legacy-config.json" local IDENTITY_FILE="/tmp/$(ac_media_identity_filename)" local MANIFEST="/tmp/ac-manifest.json" log "Generating template .img..." >&2 rm -rf "${STAGING}" "${IMAGE}" "${LEGACY_CONFIG}" "${IDENTITY_FILE}" "${MANIFEST}" ac_media_write_legacy_config "${LEGACY_CONFIG}" ac_media_write_identity_config "${IDENTITY_FILE}" ac_media_stage_boot_tree "${STAGING}" "${VMLINUZ}" "${LEGACY_CONFIG}" cp "${IDENTITY_FILE}" "${STAGING}/$(ac_media_identity_filename)" # Embed the global Wi-Fi presets so every freshly-flashed download has # the lab/home networks ready out of the box. ac_media_create_efi_disk_image # will pick this up automatically (it copies stage_root/wifi_creds.json # into the EFI partition when present). ac_media_write_global_wifi_creds "${STAGING}/wifi_creds.json" ac_media_create_efi_disk_image "${STAGING}" "${IMAGE}" "AC_NATIVE" ac_media_generate_manifest "${IMAGE}" "${AC_BUILD_NAME:-unknown}" "${MANIFEST}" rm -rf "${STAGING}" "${LEGACY_CONFIG}" "${IDENTITY_FILE}" local IMAGE_BYTES local IMAGE_MB IMAGE_BYTES=$(stat -c%s "${IMAGE}") IMAGE_MB=$(( IMAGE_BYTES / 1048576 )) log "Template image: ${IMAGE} (${IMAGE_MB}MB)" >&2 log "Manifest: ${MANIFEST}" >&2 echo "${IMAGE}"}upload_ota() { log "Uploading OTA release..." cd "${SCRIPT_DIR}" # Get AC token from ac-login local TOKEN_FILE="${HOME}/.ac-token" if [ ! -f "${TOKEN_FILE}" ]; then err "Not logged in. Run: ac-login" exit 1 fi local AC_TOKEN=$(node -e "const t=JSON.parse(require('fs').readFileSync('${TOKEN_FILE}','utf8')); console.log(t.access_token || '')" 2>/dev/null) if [ -z "${AC_TOKEN}" ]; then err "Token missing or expired. Run: ac-login" exit 1 fi # Build metadata (use pre-computed name from top level, skip dirty check — # require_clean already ensured clean state and stashed any hook artifacts) local COMMIT_MSG=$(git log -1 --format=%s HEAD 2>/dev/null | head -c 200) local SIZE=$(stat -c%s "${VMLINUZ}") local SIZE_MB=$(( SIZE / 1048576 )) local SHA=$(sha256sum "${VMLINUZ}" | cut -d' ' -f1) log "Uploading ${AC_BUILD_NAME} (${AC_GIT_HASH}, ${SIZE_MB}MB)..." local UPLOAD_URL="${AC_UPLOAD_URL:-https://aesthetic.computer/api/os-release-upload}" # Step 1: Get presigned S3 URL from edge function local STEP1 STEP1=$(curl -sf -X POST "${UPLOAD_URL}" \ -H "Authorization: Bearer ${AC_TOKEN}" \ -H "X-Build-Name: ${AC_BUILD_NAME}" \ -H "X-Git-Hash: ${AC_GIT_HASH}" \ -H "X-Build-Ts: ${AC_BUILD_TS}" \ -H "X-Commit-Msg: ${COMMIT_MSG}" \ -H "X-Handle: ${AC_USER_HANDLE}" \ --max-time 30) || { err "Failed to get upload URL. Check connection or run: ac-login" exit 1 } local PUT_URL=$(echo "$STEP1" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).vmlinuz_put_url||''))" 2>/dev/null) if [ -z "${PUT_URL}" ]; then err "No presigned URL returned: ${STEP1}" exit 1 fi log "Uploading vmlinuz directly to S3..." # Step 2: Upload vmlinuz directly to DO Spaces via presigned URL curl -sf -X PUT "${PUT_URL}" \ -H "Content-Type: application/octet-stream" \ -H "x-amz-acl: public-read" \ --data-binary "@${VMLINUZ}" \ --max-time 300 || { err "S3 upload failed" exit 1 } log "Finalizing release..." # Step 3: Tell edge function to update metadata + releases.json local RESPONSE RESPONSE=$(curl -sf -X POST "${UPLOAD_URL}" \ -H "Authorization: Bearer ${AC_TOKEN}" \ -H "X-Build-Name: ${AC_BUILD_NAME}" \ -H "X-Git-Hash: ${AC_GIT_HASH}" \ -H "X-Build-Ts: ${AC_BUILD_TS}" \ -H "X-Commit-Msg: ${COMMIT_MSG}" \ -H "X-Finalize: true" \ -H "X-Sha256: ${SHA}" \ -H "X-Size: ${SIZE}" \ -H "X-Handle: ${AC_USER_HANDLE}" \ --max-time 30) || { err "Finalize failed" exit 1 } # Parse response local NAME=$(echo "$RESPONSE" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).name||''))" 2>/dev/null) local URL=$(echo "$RESPONSE" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).url||''))" 2>/dev/null) log "Published: ${NAME}" log " SHA: ${SHA}" log " URL: ${URL}" # Step 3b: Upload versioned archive (for rollback support) local VERSIONED_KEY="os/builds/${AC_BUILD_NAME}.vmlinuz" local VERSIONED_STEP VERSIONED_STEP=$(curl -sf -X POST "${UPLOAD_URL}" \ -H "Authorization: Bearer ${AC_TOKEN}" \ -H "X-Build-Name: ${AC_BUILD_NAME}" \ -H "X-Versioned-Upload: true" \ -H "X-Versioned-Key: ${VERSIONED_KEY}" \ --max-time 30 2>/dev/null) || true if [ -n "${VERSIONED_STEP}" ]; then local VERSIONED_PUT=$(echo "$VERSIONED_STEP" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).versioned_put_url||''))" 2>/dev/null) if [ -n "${VERSIONED_PUT}" ]; then curl -sf -X PUT "${VERSIONED_PUT}" \ -H "Content-Type: application/octet-stream" \ -H "x-amz-acl: public-read" \ --data-binary "@${VMLINUZ}" \ --max-time 300 && \ log " Archive: ${VERSIONED_KEY}" || \ log " Archive upload failed (non-fatal)" fi fi # Step 4: Generate and upload template .img for personalized downloads local TEMPLATE_IMG TEMPLATE_IMG=$(generate_template_image) && { log "Uploading template .img..." local IMAGE_STEP IMAGE_STEP=$(curl -sf -X POST "${UPLOAD_URL}" \ -H "Authorization: Bearer ${AC_TOKEN}" \ -H "X-Build-Name: ${AC_BUILD_NAME}" \ -H "X-Template-Upload: true" \ --max-time 30) || { log "Template presign failed (non-fatal)"; } if [ -n "${IMAGE_STEP}" ]; then local IMAGE_PUT_URL=$(echo "$IMAGE_STEP" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).image_put_url||''))" 2>/dev/null) if [ -n "${IMAGE_PUT_URL}" ] && [ -f "${TEMPLATE_IMG}" ]; then log "Uploading template $(stat -c%s "${TEMPLATE_IMG}" | numfmt --to=iec) to S3..." curl -sf -X PUT "${IMAGE_PUT_URL}" \ -H "Content-Type: application/octet-stream" \ -H "x-amz-acl: public-read" \ -T "${TEMPLATE_IMG}" \ --max-time 300 && { log "Template .img uploaded" # Upload manifest for edge worker image patching local MANIFEST="/tmp/ac-manifest.json" if [ -f "${MANIFEST}" ]; then local MANIFEST_STEP MANIFEST_STEP=$(curl -sf -X POST "${UPLOAD_URL}" \ -H "Authorization: Bearer ${AC_TOKEN}" \ -H "X-Build-Name: ${AC_BUILD_NAME}" \ -H "X-Manifest-Upload: true" \ --max-time 30 2>/dev/null) || true if [ -n "${MANIFEST_STEP}" ]; then local MANIFEST_PUT=$(echo "$MANIFEST_STEP" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).manifest_put_url||''))" 2>/dev/null) if [ -n "${MANIFEST_PUT}" ]; then curl -sf -X PUT "${MANIFEST_PUT}" \ -H "Content-Type: application/json" \ -H "x-amz-acl: public-read" \ -T "${MANIFEST}" \ --max-time 30 && log "Manifest uploaded" \ || log "Manifest upload failed (non-fatal)" fi fi fi # Flush oven's in-memory cache so downloads get the new template immediately curl -sf -X POST "https://oven.aesthetic.computer/os-cache-flush" \ --max-time 10 && log "Oven template cache flushed" \ || log "Cache flush failed (non-fatal)" } || log "Template upload failed (non-fatal)" fi fi rm -f "${TEMPLATE_IMG}" } || log "Template generation failed (non-fatal)" # Record in MongoDB if command -v node &>/dev/null; then echo "{ \"name\": \"${AC_BUILD_NAME}\", \"buildNum\": -1, \"version\": \"${AC_GIT_HASH}-${AC_BUILD_TS}\", \"sha256\": \"${SHA}\", \"size\": ${SIZE}, \"git_hash\": \"${AC_GIT_HASH}\", \"build_ts\": \"${AC_BUILD_TS}\", \"url\": \"${URL}\", \"target\": \"upload\", \"user\": \"${AC_USER_SUB:-${ADMIN_SUB:-unknown}}\" }" | node scripts/track-build.mjs record 2>&1 || log "MongoDB record failed (non-fatal)" fi}require_clean() { # Ensure clean git state for OTA builds. # Hooks (post-commit, memory) can dirty the tree between push and build, # so we retry once after a short pause to let hooks settle. if git diff --quiet HEAD 2>/dev/null && git diff --cached --quiet HEAD 2>/dev/null; then return 0 fi log "Tree dirty — waiting 2s for hooks to settle..." sleep 2 # Stash any hook-generated noise (untracked files don't count, only modifications) if ! git diff --quiet HEAD 2>/dev/null || ! git diff --cached --quiet HEAD 2>/dev/null; then local dirty=$(git diff --name-only HEAD 2>/dev/null; git diff --cached --name-only HEAD 2>/dev/null) log "Still dirty after wait: ${dirty}" log "Stashing hook artifacts..." git stash push -q -m "ac-os: auto-stash during OTA build" 2>/dev/null || { err "Uncommitted changes detected and stash failed. Commit first." exit 1 } OTA_STASHED=1 log "Stashed — will restore after build (git stash pop)" fi} # Pre-compute build name and timestamp while cwd is clean (build_kernel changes directory) # AC_BUILD_TS is captured once and passed to Make via env so the binary, upload metadata, # and .version file all use the exact same timestamp (prevents "current" vs "available" mismatch).export AC_BUILD_NAME=$(cd "${SCRIPT_DIR}" && bash scripts/build-name.sh 2>/dev/null || echo "unknown")export AC_BUILD_TS=$(date -u '+%Y-%m-%dT%H:%M')log "Build name: ${AC_BUILD_NAME}"# Restore stash on exit if we auto-stashedOTA_STASHED=0cleanup_stash() { if [ "${OTA_STASHED}" = "1" ]; then log "Restoring stashed changes..." git checkout stash -- . 2>/dev/null && git stash drop -q 2>/dev/null \ || git stash pop -q 2>/dev/null \ || log "Stash restore failed (may need manual: git stash pop)" fi}trap cleanup_stash EXITCDN_BASE="https://releases-aesthetic-computer.sfo3.digitaloceanspaces.com/os"pull_ota() { # Download latest OTA kernel from CDN and flash to USB (no local build) local VERSION_URL="${CDN_BASE}/native-notepat-latest.version" local KERNEL_URL="${CDN_BASE}/native-notepat-latest.vmlinuz" local HASH_URL="${CDN_BASE}/native-notepat-latest.sha256" log "Fetching latest OTA version..." local VERSION_INFO VERSION_INFO=$(curl -sf "${VERSION_URL}") || { err "Failed to fetch version from CDN"; exit 1; } local BUILD_NAME=$(echo "${VERSION_INFO}" | head -1) local KERNEL_SIZE=$(echo "${VERSION_INFO}" | tail -1) log "Latest OTA: ${BUILD_NAME} (${KERNEL_SIZE} bytes)" # Download kernel (use /tmp — build dir may be a broken symlink in devcontainers) local PULL_DIR="/tmp/ac-os-pull" mkdir -p "${PULL_DIR}" local PULLED_KERNEL="${PULL_DIR}/vmlinuz" # Fetch expected hash up-front so we can skip the download if the # cached file already matches. Saves ~260 MB over slow links when # re-flashing the same OTA release. log "Verifying SHA256..." local EXPECTED_HASH EXPECTED_HASH=$(curl -sf "${HASH_URL}") || { err "Failed to fetch hash"; exit 1; } local SKIP_DOWNLOAD=0 if [ -f "${PULLED_KERNEL}" ]; then local CACHED_HASH CACHED_HASH=$(sha256sum "${PULLED_KERNEL}" | cut -d' ' -f1) if [ "${CACHED_HASH}" = "${EXPECTED_HASH}" ]; then log "Cached kernel matches — skipping download" SKIP_DOWNLOAD=1 fi fi if [ "${SKIP_DOWNLOAD}" -eq 0 ]; then log "Downloading kernel (~$(( KERNEL_SIZE / 1048576 ))MB)..." curl -f --progress-bar -o "${PULLED_KERNEL}" "${KERNEL_URL}" || { err "Download failed"; exit 1; } local ACTUAL_HASH ACTUAL_HASH=$(sha256sum "${PULLED_KERNEL}" | cut -d' ' -f1) if [ "${ACTUAL_HASH}" != "${EXPECTED_HASH}" ]; then err "SHA256 mismatch!" err " expected: ${EXPECTED_HASH}" err " got: ${ACTUAL_HASH}" exit 1 fi fi log "SHA256 verified: ${EXPECTED_HASH:0:16}..." ac_tray_state ready # local image staged & verified → badge the menu bar # Download slim kernel + initramfs for universal Mac/ThinkPad boot local SLIM_URL="${CDN_BASE}/native-notepat-latest.vmlinuz-slim" local INITRAMFS_URL="${CDN_BASE}/native-notepat-latest.initramfs.cpio.gz" local PULLED_SLIM="${PULL_DIR}/vmlinuz-slim" local PULLED_INITRAMFS="${PULL_DIR}/initramfs.cpio.gz" if curl -sf --head "${SLIM_URL}" >/dev/null 2>&1; then # Cache-aware downloads for slim+initramfs too. We don't have a # published hash for these, so we use HEAD's Content-Length as # a cheap freshness check — if the local size matches remote, # assume the file is the one the kernel hash above verified. local SLIM_REMOTE_SIZE=$(curl -sfI "${SLIM_URL}" | awk -F'[: ]+' '/^[Cc]ontent-[Ll]ength:/ {print $2+0}' | tr -d '\r') local INIT_REMOTE_SIZE=$(curl -sfI "${INITRAMFS_URL}" | awk -F'[: ]+' '/^[Cc]ontent-[Ll]ength:/ {print $2+0}' | tr -d '\r') local SLIM_LOCAL_SIZE=$([ -f "${PULLED_SLIM}" ] && wc -c < "${PULLED_SLIM}" 2>/dev/null | tr -d ' ' || echo 0) local INIT_LOCAL_SIZE=$([ -f "${PULLED_INITRAMFS}" ] && wc -c < "${PULLED_INITRAMFS}" 2>/dev/null | tr -d ' ' || echo 0) if [ "${SLIM_LOCAL_SIZE}" = "${SLIM_REMOTE_SIZE}" ] && [ "${SLIM_REMOTE_SIZE}" -gt 0 ]; then log "Cached slim kernel matches — skipping download" else log "Downloading slim kernel..." curl -f --retry 5 --retry-all-errors -C - --progress-bar -o "${PULLED_SLIM}" "${SLIM_URL}" || true fi if [ "${INIT_LOCAL_SIZE}" = "${INIT_REMOTE_SIZE}" ] && [ "${INIT_REMOTE_SIZE}" -gt 0 ]; then log "Cached initramfs matches — skipping download" else log "Downloading initramfs..." curl -f --retry 5 --retry-all-errors -C - --progress-bar -o "${PULLED_INITRAMFS}" "${INITRAMFS_URL}" || true fi # A dropped transfer above leaves a truncated file (the || true # swallows curl's error) — flashing it makes an unbootable stick. # Compare against the remote Content-Length before proceeding. local INIT_GOT_SIZE=$([ -f "${PULLED_INITRAMFS}" ] && wc -c < "${PULLED_INITRAMFS}" | tr -d ' ' || echo 0) if [ "${INIT_REMOTE_SIZE:-0}" -gt 0 ] && [ "${INIT_GOT_SIZE}" != "${INIT_REMOTE_SIZE}" ]; then err "Initramfs truncated (${INIT_GOT_SIZE}/${INIT_REMOTE_SIZE} bytes) — refusing to flash" rm -f "${PULLED_INITRAMFS}" exit 1 fi if [ -f "${PULLED_SLIM}" ] && [ -f "${PULLED_INITRAMFS}" ]; then log "Universal boot files ready (slim $(du -sh "${PULLED_SLIM}" | cut -f1) + initramfs $(du -sh "${PULLED_INITRAMFS}" | cut -f1))" fi fi # Override VMLINUZ and build name so flash_usb uses the downloaded kernel # (not the locally-generated name from scripts/build-name.sh) VMLINUZ="${PULLED_KERNEL}" export AC_BUILD_NAME="${BUILD_NAME}" log "Ready to flash: ${AC_BUILD_NAME}"}case "${CMD}" in build) build_binary build_initramfs build_kernel record_build "build" log "Build complete: ${VMLINUZ}" ;; flash) require_login build_binary build_initramfs build_kernel record_build "flash" flash_usb ;; upload) require_login require_clean build_binary build_initramfs build_kernel record_build "upload" upload_ota ;; flash+upload|upload+flash|bfu) require_login require_clean build_binary build_initramfs build_kernel record_build "flash+upload" flash_usb upload_ota ;; qemu|vm) # Boot current build in QEMU with VNC (interactive, no timeout) if [ ! -f "${VMLINUZ}" ]; then log "No kernel found — building first..." build_binary build_initramfs build_kernel fi log "Starting QEMU with VNC on port 5900..." log "Connect: vnc://localhost:5900" sudo docker rm -f ac-os-qemu 2>/dev/null || true local QEMU_CID QEMU_CID=$(sudo docker create --name ac-os-qemu --device /dev/kvm \ -p 5900:5900 \ alpine:latest sh -c "apk add --quiet qemu-system-x86_64 ovmfexec qemu-system-x86_64 \ -enable-kvm \ -bios /usr/share/OVMF/OVMF.fd \ -kernel /tmp/vmlinuz \ -append 'loglevel=3' \ -m 1G \ -no-reboot \ -display vnc=0.0.0.0:0 \ -vga std \ -usb -device usb-kbd -device usb-mouse \ -nic user,model=e1000 2>&1") sudo docker cp "${VMLINUZ}" "${QEMU_CID}:/tmp/vmlinuz" sudo docker start -a "${QEMU_CID}" & local QEMU_PID=$! log "QEMU PID: ${QEMU_PID} (Ctrl+C to stop)" wait ${QEMU_PID} 2>/dev/null sudo docker rm -f ac-os-qemu 2>/dev/null || true ;; test|smoke) # Headless smoke test: boot in QEMU, check serial for ac-native startup if [ ! -f "${VMLINUZ}" ]; then err "No kernel at ${VMLINUZ} — run 'ac-os build' first" exit 1 fi smoke_test ;; simuflash|sf) # Build + smoke test + flash (only flashes if boot test passes) build_binary build_initramfs build_kernel record_build "simuflash" smoke_test flash_usb log "Smoke test passed — USB flashed!" ;; pull) # AC_ANON=1 skips login — pull_ota only hits public CDN URLs and # the flash writes an anonymous config.json (no baked tokens). [ "${AC_ANON:-0}" != "1" ] && require_login pull_ota flash_usb ;; scan) SWANK_PORT=4005 # Get local subnet SUBNET=$(ip -4 -o addr show 2>/dev/null | grep -v '127.0.0.1' | head -1 | awk '{print $4}' | sed 's|\.[0-9]*/.*||') if [ -z "$SUBNET" ]; then err "No network interface found" exit 1 fi log "Scanning ${SUBNET}.0/24 for AC Native OS (Swank port ${SWANK_PORT})..." FOUND=0 for i in $(seq 1 254); do IP="${SUBNET}.${i}" # Parallel TCP probe with short timeout (timeout 0.5 bash -c "echo >/dev/tcp/${IP}/${SWANK_PORT}" 2>/dev/null && { echo "" log " ✦ Found: ${IP}:${SWANK_PORT}" echo "" echo " Connect: ac-os repl ${IP}" echo " Emacs: M-x slime-connect → ${IP} → ${SWANK_PORT}" echo "" }) & done wait [ "$FOUND" = "0" ] && log "No devices found. Is the CL build on WiFi?" ;; repl) SWANK_IP="${2:-}" SWANK_PORT=4005 if [ -z "$SWANK_IP" ]; then # Auto-scan for first device SUBNET=$(ip -4 -o addr show 2>/dev/null | grep -v '127.0.0.1' | head -1 | awk '{print $4}' | sed 's|\.[0-9]*/.*||') if [ -n "$SUBNET" ]; then log "Scanning for device..." for i in $(seq 1 254); do IP="${SUBNET}.${i}" timeout 0.3 bash -c "echo >/dev/tcp/${IP}/${SWANK_PORT}" 2>/dev/null && { SWANK_IP="$IP" break } done fi if [ -z "$SWANK_IP" ]; then err "No device found. Usage: ac-os repl <IP>" exit 1 fi fi log "Connecting to Swank at ${SWANK_IP}:${SWANK_PORT}..." # Use rlwrap + netcat for a basic REPL, or sbcl if available if command -v sbcl &>/dev/null; then sbcl --non-interactive \ --eval '(require :asdf)' \ --eval '(asdf:load-system :swank-client)' \ --eval "(swank-client:slime-connect \"${SWANK_IP}\" ${SWANK_PORT})" \ 2>/dev/null || { # Fallback: raw Swank protocol via netcat log "sbcl swank-client not available, using raw connection" exec nc "${SWANK_IP}" "${SWANK_PORT}" } else # Raw Swank protocol — can still eval forms log "Tip: install sbcl for a proper REPL experience" exec nc "${SWANK_IP}" "${SWANK_PORT}" fi ;; oven|ota) # Trigger / observe remote OTA builds on oven.aesthetic.computer. # Default action with no subcommand: trigger build for HEAD. OVEN_SUB="${2:-build}" case "${OVEN_SUB}" in status) oven_status ;; watch|tail|stream) oven_watch ;; cancel) JOB_ID="${3:-}" if [ -z "${JOB_ID}" ]; then JOB_ID=$(curl -sS "${AC_OVEN_HOST:-https://oven.aesthetic.computer}/native-build" \ | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(d);console.log(j.activeJobId||'')}catch{}})" 2>/dev/null) fi if [ -z "${JOB_ID}" ]; then err "No active oven job to cancel" exit 1 fi KEY=$(load_oven_admin_key) || exit 1 curl -sS -X POST "${AC_OVEN_HOST:-https://oven.aesthetic.computer}/native-build/${JOB_ID}/cancel" \ -H "Authorization: Bearer ${KEY}" unset KEY echo ;; build|"") # Optional 3rd arg = ref. Default = HEAD. oven_build "${3:-HEAD}" ;; *) # Treat the first non-keyword arg as a ref (e.g. `ac-os oven abc1234`) oven_build "${OVEN_SUB}" ;; esac ;; nixos) NIXOS_CMD="${2:-build}" NIXOS_DIR="${SCRIPT_DIR}/../nixos" if [ ! -f "${NIXOS_DIR}/flake.nix" ]; then err "NixOS flake not found at ${NIXOS_DIR}/flake.nix" exit 1 fi case "${NIXOS_CMD}" in build) log "Building NixOS image..." cd "${NIXOS_DIR}" AC_NIX_NATIVE_SRC="${SCRIPT_DIR}" nix build .#usb-image --impure --print-out-paths log "NixOS image built" ;; flash) require_login log "Building + flashing NixOS image to USB..." cd "${NIXOS_DIR}" IMAGE_PATH=$(AC_NIX_NATIVE_SRC="${SCRIPT_DIR}" nix build .#usb-image --impure --print-out-paths --no-link) IMG=$(find "${IMAGE_PATH}" -name '*.img' -type f | head -1) if [ -z "${IMG}" ]; then err "No image found in nix build output" exit 1 fi NIX_FLASH_DIR="$(mktemp -d /tmp/ac-nixos-flash.XXXXXX)" NIX_FLASH_IMAGE="${NIX_FLASH_DIR}/ac-os-nixos.img" NIX_FLASH_CONFIG="${NIX_FLASH_DIR}/config.json" cp "${IMG}" "${NIX_FLASH_IMAGE}" write_usb_config_file "${NIX_FLASH_CONFIG}" flash_nixos_image_to_usb "${NIX_FLASH_IMAGE}" "${NIX_FLASH_CONFIG}" rm -rf "${NIX_FLASH_DIR}" ;; upload) require_login require_clean log "Building + uploading NixOS image..." cd "${NIXOS_DIR}" IMAGE_PATH=$(AC_NIX_NATIVE_SRC="${SCRIPT_DIR}" nix build .#usb-image --impure --print-out-paths --no-link) IMG=$(find "${IMAGE_PATH}" -name '*.img' -type f | head -1) if [ -z "${IMG}" ]; then err "No image found in nix build output" exit 1 fi load_vault_creds NIX_UPLOAD_DIR="$(mktemp -d /tmp/ac-nixos-upload.XXXXXX)" NIX_UPLOAD_IMAGE="${NIX_UPLOAD_DIR}/ac-os-nixos.img" NIX_UPLOAD_CONFIG="${NIX_UPLOAD_DIR}/config.json" cp "${IMG}" "${NIX_UPLOAD_IMAGE}" ac_media_write_legacy_config "${NIX_UPLOAD_CONFIG}" "$(ac_media_default_identity_json)" prepare_nixos_image "${NIX_UPLOAD_IMAGE}" "${NIX_UPLOAD_CONFIG}" OTA_CHANNEL=nix bash "${SCRIPT_DIR}/scripts/upload-release.sh" --image "${NIX_UPLOAD_IMAGE}" rm -rf "${NIX_UPLOAD_DIR}" ;; pull) require_login NIX_VERSION_URL="${CDN_BASE}/nix-native-notepat-latest.version" NIX_IMG_URL="${CDN_BASE}/nix-native-notepat-latest.img" NIX_HASH_URL="${CDN_BASE}/nix-native-notepat-latest.sha256" log "Fetching latest NixOS OTA version..." VERSION_INFO=$(curl -sf "${NIX_VERSION_URL}") || { err "No NixOS release found on CDN"; exit 1; } BUILD_NAME=$(echo "${VERSION_INFO}" | head -1) IMAGE_SIZE=$(echo "${VERSION_INFO}" | tail -1) log "Latest NixOS: ${BUILD_NAME} (${IMAGE_SIZE} bytes)" PULL_DIR="/tmp/ac-os-nix-pull" mkdir -p "${PULL_DIR}" PULLED_IMG="${PULL_DIR}/ac-os-nixos.img" log "Downloading NixOS image (~$(( IMAGE_SIZE / 1048576 ))MB)..." curl -f --progress-bar -o "${PULLED_IMG}" "${NIX_IMG_URL}" || { err "Download failed"; exit 1; } log "Verifying SHA256..." EXPECTED_HASH=$(curl -sf "${NIX_HASH_URL}") || { err "Failed to fetch hash"; exit 1; } ACTUAL_HASH=$(sha256sum "${PULLED_IMG}" | cut -d' ' -f1) if [ "${ACTUAL_HASH}" != "${EXPECTED_HASH}" ]; then err "SHA256 mismatch!" exit 1 fi log "SHA256 verified" PULL_CONFIG="${PULL_DIR}/config.json" write_usb_config_file "${PULL_CONFIG}" flash_nixos_image_to_usb "${PULLED_IMG}" "${PULL_CONFIG}" rm -rf "${PULL_DIR}" ;; *) echo "Usage: ac-os nixos {build|flash|upload|pull}" exit 1 ;; esac ;; *) echo "Usage: ac-os {build|flash|upload|flash+upload|bfu|pull|scan|repl|simuflash|test|qemu|nixos}" exit 1 ;;esac