#!/data/data/com.termux/files/usr/bin/bash
# ============================================================================
#  PadStudio IDE v13 — CAVIAR EDITION · consolidated master installer
#  Sovereign mobile dev workstation · Xiaomi Pad 8 (Android 16 / ARM64)
#
#  Single file · zero-patch · idempotent · self-verifying (bash -n / node --check)
#
#  Run inside Termux (F-Droid build, with Termux:Boot + Termux:API installed):
#      bash install-padstudio-v13.0.sh
#
#  What you get (the caviar, nothing else):
#    :8080   token-gated editor gate  ->  code-server :18080 (internal)
#    :7681   ttyd + tmux 'agy_main'   ->  Google Antigravity agent CLI
#    :7682   bridge control-plane API (constant-time token auth)
#    :4000   hybrid LLM router        (remote workstation first, local fallback)
#    :11435  optional local llama-server (Qwen2.5-Coder-3B, on demand)
#    APK     Jetpack Compose shell: WebView + OAuth Custom-Tabs fix +
#            vscode:// deep-link relay + external-display Presentation
#    EGM     elephant architect / goldfish builder+QA swarm in git worktrees
#    plus:   supervisor + Termux:Boot autostart, HyperOS freezer lock,
#            engine-matched Copilot/Antigravity VSIX installers, age-encrypted vault,
#            A/B visual diff (headless chromium + pixelmatch), project switcher
# ============================================================================
set -euo pipefail

# ─────────────────────────────── stage detection ────────────────────────────
if [ -z "${PADSTUDIO_STAGE2:-}" ] && [ ! -f /etc/debian_version ]; then
# ============================================================================
#  STAGE 1 — TERMUX
# ============================================================================
echo ""
echo "━━━ PadStudio CAVIAR · stage 1/2 · Termux bootstrap ━━━"

yes | pkg update -y  >/dev/null 2>&1 || true
pkg install -y proot-distro termux-api tmux curl >/dev/null 2>&1 || pkg install -y proot-distro tmux curl

# storage access for APK export + encrypted vault (/sdcard)
[ -d "$HOME/storage" ] || termux-setup-storage || true

ROOTFS="$PREFIX/var/lib/proot-distro/installed-rootfs/debian"
[ -d "$ROOTFS" ] || proot-distro install debian || true

# ship this installer into Debian for stage 2 (layout-agnostic: pipe via login)
cat "$(readlink -f "$0")" | proot-distro login debian -- bash -c 'cat > /root/padstudio-install.sh'

# Termux:Boot autostart (survives reboots — open Termux:Boot app once)
mkdir -p "$HOME/.termux/boot"
cat > "$HOME/.termux/boot/padstudio-boot.sh" <<'EOF_BOOT'
#!/data/data/com.termux/files/usr/bin/bash
termux-wake-lock 2>/dev/null
padstudio up >/dev/null 2>&1
EOF_BOOT
chmod +x "$HOME/.termux/boot/padstudio-boot.sh"

# convenience launcher:  padstudio [up|status|restart|switch|llm|build-apk|...]
# 'up' holds the proot guest open via a detached Termux tmux session whose
# foreground process is the supervisor (it never exits, so proot never tears
# the service mesh down — the classic proot+nohup failure is avoided).
cat > "$PREFIX/bin/padstudio" <<'EOF_WRAP'
#!/data/data/com.termux/files/usr/bin/bash
termux-wake-lock 2>/dev/null
CMD=${1:-up}
shift 2>/dev/null || true
if [ "$CMD" = shell ]; then exec proot-distro login debian; fi
if [ "$CMD" = up ]; then
  if ! tmux has-session -t padstudio 2>/dev/null; then
    tmux new-session -d -s padstudio \
      "proot-distro login debian -- node /root/padstudio/services/supervisor.mjs"
  fi
  sleep 6
  exec proot-distro login debian -- /root/padstudio/bin/padstudio-status
fi
if [ "$CMD" = down ]; then
  tmux kill-session -t padstudio 2>/dev/null
  echo "padstudio stopped"; exit 0
fi
exec proot-distro login debian -- "/root/padstudio/bin/padstudio-$CMD" "$@"
EOF_WRAP
chmod +x "$PREFIX/bin/padstudio"

echo "━━━ entering Debian for stage 2/2 (this takes a while) ━━━"
proot-distro login debian -- env PADSTUDIO_STAGE2=1 bash /root/padstudio-install.sh

echo ""
echo "━━━ launching services ━━━"
termux-wake-lock 2>/dev/null
tmux kill-session -t padstudio 2>/dev/null || true
tmux new-session -d -s padstudio \
  "proot-distro login debian -- node /root/padstudio/services/supervisor.mjs"
sleep 8
proot-distro login debian -- /root/padstudio/bin/padstudio-status || true

cat <<'EOF_DONE'

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 PadStudio CAVIAR installed.   Daily driver commands (from Termux):

   padstudio status            service health + safe access hints
   padstudio switch <name>     create/activate project (/root/workspace)
   padstudio copilot-vsix      engine-matched GitHub Copilot install
   padstudio antigravity-vsix  install Google Antigravity editor extension
   padstudio build-apk         build + sign the PadStudio Android app
   padstudio hyperos-lock      print the ADB anti-freezer commands (DO THIS)
   padstudio llm status        hybrid LLM router control
   padstudio egm-init          scaffold the elephant/goldfish swarm
   padstudio backup            age-encrypted vault -> /sdcard/PadStudioVault
   padstudio shell             drop into Debian

 One-time: open the Termux:Boot app once so autostart is registered,
 then run `padstudio hyperos-lock` and apply the ADB commands.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF_DONE
exit 0
fi

# ============================================================================
#  STAGE 2 — DEBIAN (proot-distro)
# ============================================================================
export DEBIAN_FRONTEND=noninteractive
step(){ printf '\n\033[1;36m[%s]\033[0m %s\n' "$1" "$2"; }

step 1/9 "base packages"
apt-get update -y >/dev/null
apt-get install -y --no-install-recommends \
  curl ca-certificates git tmux ripgrep jq unzip zip age \
  nodejs npm procps psmisc openssh-client >/dev/null
apt-get install -y aapt >/dev/null 2>&1 || true   # native ARM64 aapt2 for APK builds
# ttyd: apt when available, else official static aarch64 binary
if ! command -v ttyd >/dev/null 2>&1; then
  apt-get install -y ttyd >/dev/null 2>&1 || {
    curl -fL -o /usr/local/bin/ttyd \
      https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.aarch64
    chmod +x /usr/local/bin/ttyd
  }
fi

step 2/9 "directories + loopback token (256-bit CSPRNG)"
mkdir -p /root/padstudio/{bin,services,logs,run,keys} /root/projects /root/.config/padstudio
TOKEN_FILE=/root/.config/padstudio/loopback.token
if [ ! -s "$TOKEN_FILE" ]; then
  head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "$TOKEN_FILE"
fi
chmod 600 "$TOKEN_FILE"
# default project + workspace symlink
if [ ! -e /root/workspace ]; then
  mkdir -p /root/projects/scratch
  git -C /root/projects/scratch init -q -b main 2>/dev/null || true
  ln -sfn /root/projects/scratch /root/workspace
fi
grep -q 'padstudio/bin' /root/.bashrc 2>/dev/null || \
  echo 'export PATH="/root/padstudio/bin:$PATH"' >> /root/.bashrc

step 3/9 "code-server (internal :18080, auth delegated to the gate)"
command -v code-server >/dev/null || curl -fsSL https://code-server.dev/install.sh | sh >/dev/null
mkdir -p /root/.config/code-server
cat > /root/.config/code-server/config.yaml <<'EOF_CSCONF'
bind-addr: 127.0.0.1:18080
auth: none
cert: false
EOF_CSCONF

step 4/9 "Google Antigravity CLI"
if ! command -v agy >/dev/null 2>&1; then
  curl -fsSL https://antigravity.google/cli/install.sh | bash -s -- --dir /usr/local/bin
fi
command -v agy >/dev/null 2>&1 || {
  echo "FATAL: Google Antigravity CLI (agy) is not available on PATH."
  exit 1
}


step 5/9 "core services (token-gated loopback mesh)"

# ── editor gate :8080 → code-server :18080 ──────────────────────────────────
# Every Android app shares 127.0.0.1 — nothing is reachable without the token.
# /callback is exempted so OAuth redirects (Copilot sign-in) can land.
cat > /root/padstudio/services/editor-gate.mjs <<'EOF_GATE'
#!/usr/bin/env node
// PadStudio editor gate — token-gated reverse proxy (HTTP + WebSocket)
import http from 'node:http';
import crypto from 'node:crypto';
import fs from 'node:fs';
import net from 'node:net';

const TOKEN = fs.readFileSync('/root/.config/padstudio/loopback.token', 'utf8').trim();
const UP = { host: '127.0.0.1', port: 18080 };
const COOKIE = 'padstudio_tkn';

const safeEq = (a, b) => {
  const A = Buffer.from(String(a)), B = Buffer.from(String(b));
  return A.length === B.length && crypto.timingSafeEqual(A, B);
};

// returns false | true (cookie) | 'set' (query token -> set cookie and redirect)
function authed(req) {
  const u = new URL(req.url, 'http://x');
  if (u.pathname.startsWith('/callback')) return true;          // OAuth exemption
  const q = u.searchParams.get('tkn');
  if (q && safeEq(q, TOKEN)) return 'set';
  const m = /(?:^|;\s*)padstudio_tkn=([^;]+)/.exec(req.headers.cookie || '');
  return !!(m && safeEq(decodeURIComponent(m[1]), TOKEN));
}

const server = http.createServer((req, res) => {
  const a = authed(req);
  if (!a) { res.writeHead(403); res.end('PadStudio: loopback token required'); return; }
  if (a === 'set') {
    const u = new URL(req.url, 'http://x');
    u.searchParams.delete('tkn');
    res.writeHead(303, {
      location: `${u.pathname}${u.search}`,
      'cache-control': 'no-store',
      'set-cookie': `${COOKIE}=${encodeURIComponent(TOKEN)}; Path=/; HttpOnly; SameSite=Lax`,
    });
    res.end();
    return;
  }
  const headers = { ...req.headers, host: '127.0.0.1:18080' };
  const p = http.request({ ...UP, path: req.url, method: req.method, headers }, pr => {
    res.writeHead(pr.statusCode, pr.headers);
    pr.pipe(res);
  });
  p.on('error', () => { if (!res.headersSent) res.writeHead(502); res.end('editor upstream down'); });
  req.pipe(p);
});

server.on('upgrade', (req, sock, head) => {                       // VS Code websockets
  if (!authed(req)) { sock.destroy(); return; }
  // Raw TCP relay: replay the exact upgrade request and let code-server do its
  // own 101 handshake. Far more robust than reconstructing the response.
  sock.setTimeout(0);
  sock.setNoDelay(true);
  sock.setKeepAlive(true, 20000);
  const upstream = net.connect(18080, '127.0.0.1', () => {
    upstream.setTimeout(0);
    upstream.setNoDelay(true);
    upstream.setKeepAlive(true, 20000);
    // Preserve the original Host header: code-server's anti-DNS-rebinding
    // check compares Origin against Host, and both must stay 127.0.0.1:8080.
    let raw = `${req.method} ${req.url} HTTP/1.1\r\n`;
    for (let i = 0; i < req.rawHeaders.length; i += 2) {
      raw += `${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r\n`;
    }
    raw += '\r\n';
    upstream.write(raw);
    if (head && head.length) upstream.write(head);
    upstream.pipe(sock);
    sock.pipe(upstream);
  });
  const kill = () => { sock.destroy(); upstream.destroy(); };
  upstream.on('error', kill);
  sock.on('error', kill);
  upstream.on('close', () => sock.destroy());
  sock.on('close', () => upstream.destroy());
});

server.keepAliveTimeout = 0;
server.headersTimeout = 0;
server.requestTimeout = 0;

server.listen(8080, '127.0.0.1', () => console.log('[gate] :8080 -> code-server :18080'));
EOF_GATE

# ── bridge control-plane :7682 ──────────────────────────────────────────────
cat > /root/padstudio/services/bridge.mjs <<'EOF_BRIDGE'
#!/usr/bin/env node
// PadStudio bridge — unified control-plane API (X-PadStudio-Token, timing-safe)
import http from 'node:http';
import crypto from 'node:crypto';
import fs from 'node:fs';
import net from 'node:net';
import { execFile } from 'node:child_process';

const TOKEN = fs.readFileSync('/root/.config/padstudio/loopback.token', 'utf8').trim();
const PORTS = { editor: 8080, 'code-server': 18080, terminal: 7681, router: 4000, 'local-llm': 11435 };

function ok(req) {
  const t = String(req.headers['x-padstudio-token'] || '');
  const A = Buffer.from(t), B = Buffer.from(TOKEN);
  return A.length === B.length && crypto.timingSafeEqual(A, B);
}

const probe = port => new Promise(res => {
  const s = net.connect({ port, host: '127.0.0.1' });
  const done = v => { s.destroy(); res(v); };
  s.once('connect', () => done(true));
  s.once('error', () => done(false));
  s.setTimeout(900, () => done(false));
});

http.createServer(async (req, res) => {
  const send = (c, o) => { res.writeHead(c, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)); };
  if (!ok(req)) return send(401, { error: 'bad token' });
  const u = new URL(req.url, 'http://x');
  if (u.pathname === '/status') {
    const out = {};
    for (const [k, p] of Object.entries(PORTS)) out[k] = (await probe(p)) ? 'up' : 'down';
    try { out.project = fs.readlinkSync('/root/workspace'); } catch { out.project = null; }
    return send(200, out);
  }
  if (u.pathname === '/restart' && req.method === 'POST') {
    const svc = u.searchParams.get('svc') || 'all';
    send(202, { restarting: svc });
    execFile('/root/padstudio/bin/padstudio-restart', [svc], () => {});
    return;
  }
  if (u.pathname === '/switch' && req.method === 'POST') {
    const p = u.searchParams.get('project') || '';
    if (!/^[a-zA-Z0-9._-]+$/.test(p)) return send(400, { error: 'bad slug' });
    send(202, { switching: p });
    execFile('/root/padstudio/bin/padstudio-switch', [p], () => {});
    return;
  }
  send(404, { error: 'unknown route' });
}).listen(7682, '127.0.0.1', () => console.log('[bridge] :7682 up'));
EOF_BRIDGE


# ── hybrid LLM router :4000 (remote-first, local fallback, OpenAI-compatible) ─
cat > /root/padstudio/services/llm-router.mjs <<'EOF_ROUTER'
#!/usr/bin/env node
// PadStudio LLM router — tries upstreams in order (remote workstation tunnels
// first, local llama-server last). model:"auto" resolves to upstream's first model.
import http from 'node:http';
import fs from 'node:fs';

const CFG = '/root/.config/padstudio/router.json';
let healthy = [];
const modelCache = new Map();

function upstreams() {
  try { return JSON.parse(fs.readFileSync(CFG, 'utf8')).upstreams || []; }
  catch { return []; }
}

async function alive(base) {
  try {
    const r = await fetch(base + '/v1/models', { signal: AbortSignal.timeout(1500) });
    return r.ok;
  } catch { return false; }
}

async function sweep() {
  const h = [];
  for (const b of upstreams()) if (await alive(b)) h.push(b);
  healthy = h;
}
setInterval(sweep, 10000);
sweep();

async function firstModel(base) {
  if (modelCache.has(base)) return modelCache.get(base);
  const r = await fetch(base + '/v1/models', { signal: AbortSignal.timeout(2500) });
  const id = (await r.json()).data?.[0]?.id || 'default';
  modelCache.set(base, id);
  return id;
}

http.createServer(async (req, res) => {
  const fail = (code, msg) => {
    if (res.headersSent) { res.end(); return; }
    res.writeHead(code, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ error: { message: msg } }));
  };
  const base = healthy[0];
  if (!base) return fail(503,
    'no healthy LLM upstream — run: padstudio-llm local install  (or)  padstudio-llm remote add <url>');
  try {
    const chunks = [];
    for await (const c of req) chunks.push(c);
    let body = Buffer.concat(chunks);
    if ((req.headers['content-type'] || '').includes('json') && body.length) {
      try {
        const o = JSON.parse(body.toString());
        if (!o.model || o.model === 'auto') {
          o.model = await firstModel(base);
          body = Buffer.from(JSON.stringify(o));
        }
      } catch {}
    }
    const t = new URL(base);
    const headers = { ...req.headers, host: t.host, 'content-length': String(body.length) };
    delete headers['transfer-encoding'];
    const p = http.request({ hostname: t.hostname, port: t.port || 80, path: req.url,
      method: req.method, headers }, pr => {
      res.writeHead(pr.statusCode, pr.headers);
      pr.pipe(res);                                   // SSE streaming passes through
    });
    p.on('error', () => fail(502, 'upstream error: ' + base));
    p.end(body);
  } catch (e) { fail(500, String(e)); }
}).listen(4000, '127.0.0.1', () => console.log('[router] :4000 up'));
EOF_ROUTER

# ── supervisor (port-probing restart loop, exponential backoff, singleton) ───
cat > /root/padstudio/services/supervisor.mjs <<'EOF_SUP'
#!/usr/bin/env node
// PadStudio supervisor — keeps the mesh alive under HyperOS pressure.
import net from 'node:net';
import fs from 'node:fs';
import { spawn } from 'node:child_process';

const ROOT = '/root/padstudio';
fs.mkdirSync(`${ROOT}/run`, { recursive: true });
fs.mkdirSync(`${ROOT}/logs`, { recursive: true });

// singleton lock
const pidFile = `${ROOT}/run/supervisor.pid`;
try {
  const old = Number(fs.readFileSync(pidFile, 'utf8'));
  if (old && old !== process.pid) {
    process.kill(old, 0);                              // throws if dead
    console.error('[supervisor] already running as pid', old);
    process.exit(0);
  }
} catch {}
fs.writeFileSync(pidFile, String(process.pid));

const FLAGS = '/root/.config/padstudio';
const SERVICES = [
  { name: 'code-server', port: 18080, cmd: ['code-server'] },
  { name: 'gate',        port: 8080,  cmd: ['node', `${ROOT}/services/editor-gate.mjs`] },
  { name: 'terminal',    port: 7681,  cmd: ['bash', `${ROOT}/bin/padstudio-ttyd`] },
  { name: 'bridge',      port: 7682,  cmd: ['node', `${ROOT}/services/bridge.mjs`] },
  { name: 'router',      port: 4000,  cmd: ['node', `${ROOT}/services/llm-router.mjs`], flag: `${FLAGS}/router.enabled` },
  { name: 'local-llm',   port: 11435, cmd: ['bash', `${ROOT}/bin/padstudio-local-llm`], flag: `${FLAGS}/local-llm.enabled` },
];
const state = Object.fromEntries(SERVICES.map(s => [s.name, { fails: 0, until: 0 }]));

const probe = port => new Promise(res => {
  const s = net.connect({ port, host: '127.0.0.1' });
  const done = v => { s.destroy(); res(v); };
  s.once('connect', () => done(true));
  s.once('error', () => done(false));
  s.setTimeout(1000, () => done(false));
});

function start(svc) {
  const log = fs.openSync(`${ROOT}/logs/${svc.name}.log`, 'a');
  const child = spawn(svc.cmd[0], svc.cmd.slice(1), { stdio: ['ignore', log, log], detached: true });
  child.unref();
  fs.closeSync(log);
  console.log(new Date().toISOString(), '[supervisor] started', svc.name, 'pid', child.pid);
}

async function tick() {
  for (const svc of SERVICES) {
    const st = state[svc.name];
    if (svc.flag && !fs.existsSync(svc.flag)) continue;
    if (await probe(svc.port)) { st.fails = 0; continue; }
    const now = Date.now();
    if (now < st.until) continue;
    st.fails++;
    st.until = now + Math.min(90, 10 * st.fails) * 1000;   // backoff: 10s,20s,…,90s
    start(svc);
  }
}
setInterval(tick, 5000);
tick();
console.log('[supervisor] online, pid', process.pid);
EOF_SUP

# ── LLM shield MCP server (protects cloud quota via local delegation) ────────
cat > /root/padstudio/services/llm-shield-mcp.mjs <<'EOF_MCP'
#!/usr/bin/env node
// PadStudio LLM shield — MCP stdio server exposing local-LLM tools so the
// cloud agent (Antigravity/Gemini) can delegate grunt work off-quota.
import readline from 'node:readline';

const send = o => process.stdout.write(JSON.stringify(o) + '\n');
const TOOLS = [
  {
    name: 'local_scout',
    description: 'Cheap reconnaissance: ask the local/offloaded LLM a question (summaries, explanations, codebase questions). Protects cloud quota.',
    inputSchema: { type: 'object', properties: { prompt: { type: 'string' } }, required: ['prompt'] },
  },
  {
    name: 'local_draft_code',
    description: 'Delegate boilerplate/code drafting to the local/offloaded LLM. Returns code only.',
    inputSchema: { type: 'object', properties: { prompt: { type: 'string' } }, required: ['prompt'] },
  },
];

async function callLocal(prompt, system) {
  const r = await fetch('http://127.0.0.1:4000/v1/chat/completions', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      model: 'auto', stream: false,
      messages: [{ role: 'system', content: system }, { role: 'user', content: prompt }],
    }),
  });
  if (!r.ok) throw new Error('router replied ' + r.status);
  const j = await r.json();
  return j.choices?.[0]?.message?.content ?? '';
}

const rl = readline.createInterface({ input: process.stdin });
rl.on('line', async line => {
  let msg; try { msg = JSON.parse(line); } catch { return; }
  const { id, method, params } = msg;
  if (method === 'initialize') {
    send({ jsonrpc: '2.0', id, result: {
      protocolVersion: params?.protocolVersion || '2024-11-05',
      capabilities: { tools: {} },
      serverInfo: { name: 'padstudio-llm-shield', version: '13.0.0' },
    }});
  } else if (method === 'tools/list') {
    send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
  } else if (method === 'tools/call') {
    try {
      const system = params?.name === 'local_draft_code'
        ? 'You are a precise senior engineer. Output only code, no prose.'
        : 'You are a concise, factual code scout.';
      const text = await callLocal(params?.arguments?.prompt || '', system);
      send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } });
    } catch (e) {
      send({ jsonrpc: '2.0', id, result: {
        content: [{ type: 'text', text: `local LLM unavailable (${e.message}) — run: padstudio-llm local install` }],
        isError: true,
      }});
    }
  } else if (id !== undefined) {
    send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'method not found' } });
  }
});
EOF_MCP

# ── marketplace engine matcher (GitHub Copilot ↔ code-server Code version) ───
cat > /root/padstudio/services/vsix-match.mjs <<'EOF_VSIXJS'
#!/usr/bin/env node
// prints "<downloadUrl>#<version>" of the newest stable marketplace version
// whose engines.vscode is satisfied by the host Code version. exit 2 = none.
const [host, ext] = process.argv.slice(2);
if (!host || !ext) { console.error('usage: vsix-match.mjs <hostCodeVersion> <Publisher.name>'); process.exit(1); }
const hv = host.split('.').map(Number);
const cmp = (a, b) => { for (let i = 0; i < 3; i++) { const d = (a[i] || 0) - (b[i] || 0); if (d) return d; } return 0; };

function engineOk(engine) {
  const m = /([0-9]+)\.([0-9]+)\.([0-9]+|x)/.exec(engine || '');
  if (!m) return false;
  const ev = [Number(m[1]), Number(m[2]), m[3] === 'x' ? 0 : Number(m[3])];
  return ev[0] === hv[0] && cmp(hv, ev) >= 0;          // host >= minimum engine
}

const r = await fetch('https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery', {
  method: 'POST',
  headers: { 'content-type': 'application/json', accept: 'application/json;api-version=3.0-preview.1' },
  body: JSON.stringify({
    filters: [{ criteria: [{ filterType: 7, value: ext }], pageNumber: 1, pageSize: 1 }],
    flags: 0x1 | 0x2 | 0x10,                           // versions | files | version properties
  }),
});
if (!r.ok) { console.error('marketplace query failed:', r.status); process.exit(1); }
const data = (await r.json()).results?.[0]?.extensions?.[0];
for (const v of data?.versions || []) {
  const prop = k => v.properties?.find(p => p.key === k)?.value;
  if (prop('Microsoft.VisualStudio.Code.PreRelease') === 'true') continue;
  if (!engineOk(prop('Microsoft.VisualStudio.Code.Engine'))) continue;
  const file = v.files?.find(f => f.assetType === 'Microsoft.VisualStudio.Services.VSIXPackage');
  const url = file?.source || `${v.assetUri}/Microsoft.VisualStudio.Services.VSIXPackage`;
  console.log(url + '#' + v.version);
  process.exit(0);
}
process.exit(2);
EOF_VSIXJS

# ── A/B pixel diff helper ────────────────────────────────────────────────────
cat > /root/padstudio/services/abdiff.mjs <<'EOF_ABJS'
#!/usr/bin/env node
// usage: abdiff.mjs <a.png> <b.png> <out.png>   (exit 1 when pixels differ)
import { createRequire } from 'node:module';
import fs from 'node:fs';
const require = createRequire('/root/padstudio/abdiff/package.json');
const { PNG } = require('pngjs');
const pixelmatch = require('pixelmatch');

const a = PNG.sync.read(fs.readFileSync(process.argv[2]));
const b = PNG.sync.read(fs.readFileSync(process.argv[3]));
if (a.width !== b.width || a.height !== b.height)
  console.log(`DIMENSIONS differ (overflow?): A ${a.width}x${a.height} vs B ${b.width}x${b.height}`);

const w = Math.min(a.width, b.width), h = Math.min(a.height, b.height);
const crop = img => { const o = new PNG({ width: w, height: h }); PNG.bitblt(img, o, 0, 0, w, h, 0, 0); return o; };
const out = new PNG({ width: w, height: h });
const n = pixelmatch(crop(a).data, crop(b).data, out.data, w, h, { threshold: 0.1 });
fs.writeFileSync(process.argv[4], PNG.sync.write(out));
console.log(`${n} px changed (${((n / (w * h)) * 100).toFixed(2)}%) -> ${process.argv[4]}`);
process.exit(n > 0 ? 1 : 0);
EOF_ABJS


step 6/9 "operator commands"
BIN=/root/padstudio/bin

cat > "$BIN/padstudio-up" <<'EOF_UP'
#!/usr/bin/env bash
mkdir -p /root/padstudio/run /root/padstudio/logs
if ! pgrep -f 'services/supervisor.mjs' >/dev/null 2>&1; then
  nohup node /root/padstudio/services/supervisor.mjs \
    >> /root/padstudio/logs/supervisor.log 2>&1 &
  disown || true
  sleep 2
fi
exec /root/padstudio/bin/padstudio-status
EOF_UP

cat > "$BIN/padstudio-status" <<'EOF_STATUS'
#!/usr/bin/env bash
G='\033[32m'; R='\033[31m'; B='\033[1m'; N='\033[0m'
check(){
  if timeout 1 bash -c "</dev/tcp/127.0.0.1/$2" 2>/dev/null; then
    printf " ${G}●${N} %s\n" "$1"
  else
    printf " ${R}○${N} %s\n" "$1"
  fi
}
printf "${B}── PadStudio IDE · CAVIAR ──────────────────────────${N}\n"
check "editor gate     :8080"  8080
check "code-server     :18080" 18080
check "agent terminal  :7681"  7681
check "bridge api      :7682"  7682
check "llm router      :4000   (padstudio-llm enable)"  4000
check "local llm       :11435  (padstudio-llm local …)" 11435
printf "${B}────────────────────────────────────────────────────${N}\n"
echo " project  : $(readlink /root/workspace 2>/dev/null || echo '(none)')"
echo " editor   : open the PadStudio app (local authentication is automatic)"
echo " terminal : open the Agent tab in the PadStudio app"
echo " bridge   : authenticated API available to the PadStudio app; token is not displayed"
EOF_STATUS

cat > "$BIN/padstudio-restart" <<'EOF_RESTART'
#!/usr/bin/env bash
SVC=${1:-all}
case "$SVC" in
  editor|code-server) pkill -f 'code-server' 2>/dev/null || true ;;
  gate)      pkill -f 'editor-gate.mjs'  2>/dev/null || true ;;
  terminal)  pkill -x ttyd               2>/dev/null || true ;;
  bridge)    pkill -f 'bridge.mjs'       2>/dev/null || true ;;
  router)    pkill -f 'llm-router.mjs'   2>/dev/null || true ;;
  local-llm) pkill -f 'llama-server'     2>/dev/null || true ;;
  supervisor|all)
    pkill -f 'services/supervisor.mjs' 2>/dev/null || true
    pkill -f 'editor-gate.mjs' 2>/dev/null || true
    pkill -f 'bridge.mjs'      2>/dev/null || true
    pkill -f 'llm-router.mjs'  2>/dev/null || true
    pkill -x ttyd              2>/dev/null || true
    pkill -f 'code-server'     2>/dev/null || true
    sleep 1
    exec /root/padstudio/bin/padstudio-up
    ;;
  *) echo "usage: padstudio-restart [editor|gate|terminal|bridge|router|local-llm|all]"; exit 1 ;;
esac
echo "restart requested: $SVC (supervisor respawns within ~5s)"
EOF_RESTART

cat > "$BIN/padstudio-switch" <<'EOF_SWITCH'
#!/usr/bin/env bash
set -euo pipefail
SLUG=${1:?usage: padstudio-switch <project-name>}
case "$SLUG" in (*[!a-zA-Z0-9._-]*) echo "bad slug: letters/digits/._- only"; exit 1;; esac
P="/root/projects/$SLUG"
if [ ! -d "$P" ]; then
  mkdir -p "$P"
  git -C "$P" init -q -b main
  echo "created new project: $P"
fi
ln -sfn "$P" /root/workspace
# respawn the agent pane into the new workspace
if tmux has-session -t agy_main 2>/dev/null; then
  tmux respawn-pane -k -t agy_main -c /root/workspace /root/padstudio/bin/padstudio-agy 2>/dev/null || true
fi
echo "workspace -> $P"
echo "editor: reopen http://127.0.0.1:8080/?folder=/root/workspace"
EOF_SWITCH

cat > "$BIN/padstudio-agy" <<'EOF_AGY'
#!/usr/bin/env bash
cd /root/workspace 2>/dev/null || cd /root
for c in agy antigravity gemini; do
  if command -v "$c" >/dev/null 2>&1; then exec "$c" "$@"; fi
done
echo "No Google Antigravity CLI found. Run: curl -fsSL https://antigravity.google/cli/install.sh | bash"
exec bash
EOF_AGY

cat > "$BIN/padstudio-ttyd" <<'EOF_TTYD'
#!/usr/bin/env bash
TOKEN=$(cat /root/.config/padstudio/loopback.token)
PASS=${TOKEN:0:16}
tmux has-session -t agy_main 2>/dev/null || \
  tmux new-session -d -s agy_main -c /root/workspace /root/padstudio/bin/padstudio-agy
exec ttyd -p 7681 -i 127.0.0.1 -c "padstudio:${PASS}" -W tmux attach -t agy_main
EOF_TTYD

cat > "$BIN/padstudio-local-llm" <<'EOF_LOCAL'
#!/usr/bin/env bash
BIN=/root/llama.cpp/build/bin/llama-server
MODEL=/root/models/qwen2.5-coder-3b-instruct-q4_k_m.gguf
[ -x "$BIN" ] || { echo "llama-server not built — run: padstudio-llm local install"; exit 1; }
[ -f "$MODEL" ] || { echo "model missing — run: padstudio-llm local install"; exit 1; }
exec "$BIN" -m "$MODEL" --host 127.0.0.1 --port 11435 -c 8192 --threads "$(nproc)"
EOF_LOCAL

cat > "$BIN/padstudio-backup" <<'EOF_BACKUP'
#!/usr/bin/env bash
set -euo pipefail
KEY=/root/.config/padstudio/vault.key
DEST=/sdcard/PadStudioVault
case "${1:-save}" in
  save)
    [ -f "$KEY" ] || { age-keygen -o "$KEY" 2>/dev/null; chmod 600 "$KEY"; }
    PUB=$(age-keygen -y "$KEY")
    mkdir -p "$DEST" 2>/dev/null || DEST=/root/padstudio/vault; mkdir -p "$DEST"
    OUT="$DEST/padstudio-$(date +%Y%m%d-%H%M%S).tar.gz.age"
    SRC=""
    for d in projects .gemini .config/padstudio .local/share/code-server padstudio/keys; do
      [ -e "/root/$d" ] && SRC="$SRC $d"
    done
    # shellcheck disable=SC2086
    tar czf - -C /root --exclude='*/node_modules' $SRC | age -r "$PUB" -o "$OUT"
    echo "vault -> $OUT"
    echo "KEEP THE IDENTITY FILE SAFE (it decrypts everything): $KEY"
    ;;
  restore)
    F=${2:?usage: padstudio-backup restore <file.tar.gz.age>}
    age -d -i "$KEY" "$F" | tar xzf - -C /root
    echo "restored into /root"
    ;;
  *) echo "usage: padstudio-backup [save|restore <file>]"; exit 1 ;;
esac
EOF_BACKUP

cat > "$BIN/padstudio-llm" <<'EOF_LLM'
#!/usr/bin/env bash
set -euo pipefail
CFG_DIR=/root/.config/padstudio
CFG=$CFG_DIR/router.json
[ -f "$CFG" ] || echo '{"upstreams":["http://127.0.0.1:11435"]}' > "$CFG"
case "${1:-status}" in
  status)
    echo "router   : $( [ -f "$CFG_DIR/router.enabled" ] && echo enabled || echo disabled )"
    echo "local llm: $( [ -f "$CFG_DIR/local-llm.enabled" ] && echo enabled || echo disabled )"
    echo "upstreams (priority order, remote tunnels first):"
    jq -r '.upstreams[]' "$CFG" 2>/dev/null | sed 's/^/   - /'
    curl -sf -m 2 http://127.0.0.1:4000/v1/models 2>/dev/null \
      | jq -r '.data[].id' 2>/dev/null | sed 's/^/   model: /' || true
    ;;
  enable)  touch "$CFG_DIR/router.enabled"; echo "router enabled (supervisor starts it within ~5s)";;
  disable) rm -f "$CFG_DIR/router.enabled"; pkill -f llm-router.mjs 2>/dev/null || true; echo "router disabled";;
  remote)
    [ "${2:-}" = add ] || { echo "usage: padstudio-llm remote add <http://host:port>"; exit 1; }
    URL=${3:?usage: padstudio-llm remote add <http://host:port>}
    node -e 'const fs=require("fs");const[p,u]=process.argv.slice(1);const c=JSON.parse(fs.readFileSync(p,"utf8"));c.upstreams=[u,...(c.upstreams||[]).filter(x=>x!==u)];fs.writeFileSync(p,JSON.stringify(c,null,2));' "$CFG" "$URL"
    touch "$CFG_DIR/router.enabled"
    echo "remote upstream added (highest priority): $URL"
    echo "tip: tunnel a workstation with  ssh -N -L 11434:127.0.0.1:11434 user@workstation"
    ;;
  local)
    case "${2:-}" in
      enable)  touch "$CFG_DIR/local-llm.enabled" "$CFG_DIR/router.enabled"; echo "local llm + router enabled";;
      disable) rm -f "$CFG_DIR/local-llm.enabled"; pkill -f llama-server 2>/dev/null || true; echo "local llm disabled";;
      install)
        apt-get install -y cmake build-essential libcurl4-openssl-dev git
        [ -d /root/llama.cpp ] || git clone --depth 1 https://github.com/ggml-org/llama.cpp /root/llama.cpp
        ( cd /root/llama.cpp && cmake -B build -DLLAMA_CURL=ON && cmake --build build -j"$(nproc)" --target llama-server )
        mkdir -p /root/models
        M=/root/models/qwen2.5-coder-3b-instruct-q4_k_m.gguf
        [ -f "$M" ] || curl -fL -o "$M" \
          "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct-GGUF/resolve/main/qwen2.5-coder-3b-instruct-q4_k_m.gguf"
        touch "$CFG_DIR/local-llm.enabled" "$CFG_DIR/router.enabled"
        echo "local llm installed + enabled (supervisor starts it within ~5s)"
        ;;
      *) echo "usage: padstudio-llm local [install|enable|disable]"; exit 1;;
    esac;;
  *) echo "usage: padstudio-llm [status|enable|disable|remote add <url>|local install|local enable|local disable]"; exit 1;;
esac
EOF_LLM


cat > "$BIN/padstudio-install-vsix" <<'EOF_INSTALL_VSIX'
#!/usr/bin/env bash
set -euo pipefail
REQUIRED=0
if [ "${1:-}" = "--required" ]; then REQUIRED=1; shift; fi
EXTENSIONS=("$@")
[ "${#EXTENSIONS[@]}" -gt 0 ] || { echo "usage: padstudio-install-vsix [--required] <Publisher.name>..."; exit 1; }
HOSTV=$(code-server --version 2>/dev/null | sed -n 's/.*with Code \([0-9.]*\).*/\1/p' | head -1)
[ -n "$HOSTV" ] || HOSTV=$(code-server --version 2>/dev/null | sed -n 's/.*Code \([0-9][0-9.]*\).*/\1/p' | head -1)
[ -n "$HOSTV" ] || { echo "cannot detect code-server's VS Code version"; exit 1; }
echo "host VS Code engine: $HOSTV"
for EXT in "${EXTENSIONS[@]}"; do
  echo "── $EXT"
  if OUT=$(node /root/padstudio/services/vsix-match.mjs "$HOSTV" "$EXT"); then
    URL=${OUT%%#*}; V=${OUT##*#}
    echo "   matched version: $V"
    curl -fL --compressed -o "/tmp/$EXT.vsix" "$URL"
    code-server --install-extension "/tmp/$EXT.vsix" --force
    rm -f "/tmp/$EXT.vsix"
  else
    echo "   no compatible stable version for engine $HOSTV"
    [ "$REQUIRED" -eq 0 ] || exit 1
  fi
done
EOF_VSIX

cat > "$BIN/padstudio-copilot-vsix" <<'EOF_COPILOT_VSIX'
#!/usr/bin/env bash
exec /root/padstudio/bin/padstudio-install-vsix GitHub.copilot GitHub.copilot-chat
EOF_COPILOT_VSIX

cat > "$BIN/padstudio-antigravity-vsix" <<'EOF_ANTIGRAVITY_VSIX'
#!/usr/bin/env bash
set -euo pipefail
/root/padstudio/bin/padstudio-install-vsix --required Google.google-antigravity
echo "Google Antigravity installed. Open its Activity Bar panel in the editor and sign in with Google."
EOF_ANTIGRAVITY_VSIX

cat > "$BIN/padstudio-abdiff" <<'EOF_ABDIFF'
#!/usr/bin/env bash
set -euo pipefail
A=${1:?usage: padstudio-abdiff <urlA> <urlB>}
B=${2:?usage: padstudio-abdiff <urlA> <urlB>}
command -v chromium >/dev/null || apt-get install -y chromium
D=/root/padstudio/abdiff
mkdir -p "$D"
[ -d "$D/node_modules/pixelmatch" ] || ( cd "$D" && npm init -y >/dev/null 2>&1 && npm i --silent pixelmatch@5 pngjs@7 )
# --no-sandbox / --disable-dev-shm-usage are mandatory under proot
FLAGS="--headless=new --no-sandbox --disable-gpu --disable-dev-shm-usage --hide-scrollbars --virtual-time-budget=8000"
RC=0
for W in 390 820 1440; do
  # shellcheck disable=SC2086
  chromium $FLAGS --window-size=${W},1200 --screenshot="$D/a-$W.png" "$A" >/dev/null 2>&1
  # shellcheck disable=SC2086
  chromium $FLAGS --window-size=${W},1200 --screenshot="$D/b-$W.png" "$B" >/dev/null 2>&1
  echo "── viewport ${W}px"
  node /root/padstudio/services/abdiff.mjs "$D/a-$W.png" "$D/b-$W.png" "$D/diff-$W.png" || RC=1
done
echo "artifacts in $D"
exit $RC
EOF_ABDIFF

cat > "$BIN/padstudio-hyperos-lock" <<'EOF_LOCKSH'
#!/usr/bin/env bash
cat <<'EOF_LOCK'
── HyperOS freezer lock ─────────────────────────────────────────────
HyperOS/Android kills background processes (phantom process killer).
Run these ONCE from a PC with adb (or wireless-debugging pairing):

  adb shell settings put global settings_enable_monitor_phantom_procs false
  adb shell device_config put activity_manager max_phantom_processes 2147483647
  adb shell dumpsys deviceidle whitelist +com.termux
  adb shell dumpsys deviceidle whitelist +com.padstudio.ide
  adb shell cmd appops set com.termux RUN_ANY_IN_BACKGROUND allow
  adb shell cmd appops set com.padstudio.ide RUN_ANY_IN_BACKGROUND allow

On the tablet itself:
  · Settings -> Battery: "No restrictions" for Termux, Termux:Boot, PadStudio
  · Disable battery optimization / MIUI optimization for those apps
  · Open the Termux:Boot app once after every OS update
─────────────────────────────────────────────────────────────────────
EOF_LOCK
EOF_LOCKSH

cat > "$BIN/padstudio-egm-init" <<'EOF_EGMI'
#!/usr/bin/env bash
set -euo pipefail
# EGM: Elephant (stateful architect, owns CANON) + Goldfish (stateless
# builder/QA agents, fresh context each run) in isolated git worktrees.
PROJ=$(readlink -f /root/workspace 2>/dev/null || echo "")
[ -d "$PROJ" ] || { echo "no active project — run: padstudio-switch <name> first"; exit 1; }
cd "$PROJ"
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || git init -q -b main
mkdir -p .egm/specs
grep -qx '.egm/worktrees/' .gitignore 2>/dev/null || echo '.egm/worktrees/' >> .gitignore
if [ ! -f .egm/CANON.md ]; then
cat > .egm/CANON.md <<'EOF_CANON'
# CANON — single source of truth (elephant memory)
The architect maintains this file. Goldfish agents read it fresh each run.

## Product
<one paragraph: what this project is>

## Architecture decisions
-

## Invariants — never violate
-

## Current state / next milestone
-
EOF_CANON
fi
if [ ! -f .egm/specs/SPEC-001.md ]; then
cat > .egm/specs/SPEC-001.md <<'EOF_SPEC'
# SPEC-001 — <title>
Status: DRAFT

## Goal

## Acceptance criteria
- [ ]

## Out of scope
EOF_SPEC
fi
git add -A
git commit -q -m "egm: scaffold" 2>/dev/null || true
git worktree add .egm/worktrees/builder -B swarm/builder >/dev/null 2>&1 || true
git worktree add .egm/worktrees/qa      -B swarm/qa      >/dev/null 2>&1 || true
echo "EGM ready."
echo "  1. edit .egm/CANON.md and .egm/specs/SPEC-001.md"
echo "  2. run: padstudio-egm-autoheal SPEC-001"
EOF_EGMI

cat > "$BIN/padstudio-egm-autoheal" <<'EOF_HEAL'
#!/usr/bin/env bash
set -euo pipefail
SPEC=${1:?usage: padstudio-egm-autoheal SPEC-001 [cycles]}
CYCLES=${2:-3}
PROJ=$(readlink -f /root/workspace)
BW="$PROJ/.egm/worktrees/builder"
QW="$PROJ/.egm/worktrees/qa"
[ -d "$BW" ] && [ -d "$QW" ] || { echo "run padstudio-egm-init first"; exit 1; }
AGY=""
for c in agy antigravity gemini; do command -v "$c" >/dev/null 2>&1 && { AGY=$c; break; }; done
[ -n "$AGY" ] || { echo "no agent CLI installed"; exit 1; }

run_agent(){ local dir=$1; shift
  ( cd "$dir" && "$AGY" --yolo -p "$*" 2>/dev/null ) || ( cd "$dir" && "$AGY" -p "$*" )
}

for i in $(seq 1 "$CYCLES"); do
  echo "━━ cycle $i/$CYCLES · goldfish BUILDER ━━"
  ( cd "$BW" && git merge --no-edit main >/dev/null 2>&1 || true )
  run_agent "$BW" "You are the goldfish BUILDER (no memory of previous runs). \
Read .egm/CANON.md and .egm/specs/$SPEC.md in this repo. Implement the spec exactly, \
respecting every CANON invariant. Run the project's tests/build if any. \
Commit your work to the current branch (swarm/builder) with a clear message."

  echo "━━ cycle $i/$CYCLES · goldfish QA ━━"
  run_agent "$QW" "You are the goldfish QA reviewer (no memory of previous runs). \
Read .egm/CANON.md and .egm/specs/$SPEC.md. Run: git fetch . && git diff main...swarm/builder \
to inspect the builder's work. Check acceptance criteria and CANON invariants. \
Write your findings to .egm/REVIEW.md whose FIRST line must be exactly \
'VERDICT: PASS' or 'VERDICT: FAIL', then commit it."

  if ( cd "$QW" && head -1 .egm/REVIEW.md 2>/dev/null | grep -q 'VERDICT: PASS' ); then
    echo "━━ QA PASS — merging swarm/builder into main ━━"
    ( cd "$PROJ" && git merge --no-edit swarm/builder )
    exit 0
  fi
  echo "QA verdict: FAIL (or missing) — next refinement cycle"
done
echo "autoheal exhausted $CYCLES cycles — human review needed (.egm/REVIEW.md)"
exit 1
EOF_HEAL

cat > "$BIN/padstudio-build-apk" <<'EOF_BUILD'
#!/usr/bin/env bash
set -euo pipefail
# On-device APK build. Everything Java-based (gradle, sdkmanager, d8, apksig)
# runs fine on ARM64; the only native x86_64 tool is aapt2, which we override
# with Debian's ARM64 build via android.aapt2FromMavenOverride.
APP=/root/padstudio/app
SDK=/root/android-sdk
GRADLE_V=8.10.2
export DEBIAN_FRONTEND=noninteractive
echo "━━ PadStudio APK build ━━"

command -v java >/dev/null || apt-get install -y openjdk-21-jdk-headless \
  || apt-get install -y openjdk-17-jdk-headless
command -v aapt2 >/dev/null || apt-get install -y aapt || true
AAPT2=$(command -v aapt2 || true)
[ -n "$AAPT2" ] || { echo "FATAL: native aapt2 not found (apt-get install aapt)"; exit 1; }

if [ ! -d "$SDK/cmdline-tools/latest" ]; then
  mkdir -p "$SDK/cmdline-tools"
  curl -fL -o /tmp/clt.zip \
    https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
  unzip -q -o /tmp/clt.zip -d "$SDK/cmdline-tools"
  mv "$SDK/cmdline-tools/cmdline-tools" "$SDK/cmdline-tools/latest"
  rm -f /tmp/clt.zip
fi
SDKM="$SDK/cmdline-tools/latest/bin/sdkmanager"
yes | "$SDKM" --sdk_root="$SDK" --licenses >/dev/null 2>&1 || true
"$SDKM" --sdk_root="$SDK" "platforms;android-36" "build-tools;35.0.0" >/dev/null

if [ ! -d "/opt/gradle-$GRADLE_V" ]; then
  curl -fL -o /tmp/gradle.zip "https://services.gradle.org/distributions/gradle-$GRADLE_V-bin.zip"
  unzip -q /tmp/gradle.zip -d /opt
  rm -f /tmp/gradle.zip
fi
export PATH="/opt/gradle-$GRADLE_V/bin:$PATH"

mkdir -p /root/padstudio/keys
KEYSTORE=/root/padstudio/keys/padstudio.jks
SIGNING_PROPERTIES=/root/padstudio/keys/signing.properties
if [ ! -s "$SIGNING_PROPERTIES" ]; then
  [ ! -e "$KEYSTORE" ] || {
    echo "FATAL: signing.properties is missing for the existing keystore; restore both files from backup."
    exit 1
  }
  SIGNING_PASSWORD=$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')
  (umask 077; printf 'storePassword=%s\nkeyPassword=%s\n' \
    "$SIGNING_PASSWORD" "$SIGNING_PASSWORD" > "$SIGNING_PROPERTIES")
fi
chmod 600 "$SIGNING_PROPERTIES"
SIGNING_PASSWORD=$(sed -n 's/^storePassword=//p' "$SIGNING_PROPERTIES")
[ -n "$SIGNING_PASSWORD" ] || {
  echo "FATAL: signing password is missing from $SIGNING_PROPERTIES"
  exit 1
}
if [ ! -f "$KEYSTORE" ]; then
  PADSTUDIO_SIGNING_PASSWORD="$SIGNING_PASSWORD" keytool -genkeypair \
    -keystore "$KEYSTORE" -alias padstudio \
    -storepass:env PADSTUDIO_SIGNING_PASSWORD \
    -keypass:env PADSTUDIO_SIGNING_PASSWORD \
    -keyalg RSA -keysize 2048 -validity 10000 \
    -dname "CN=PadStudio,O=PadStudio,C=US"
fi
chmod 600 "$KEYSTORE"
unset SIGNING_PASSWORD

echo "sdk.dir=$SDK" > "$APP/local.properties"
grep -q aapt2FromMavenOverride "$APP/gradle.properties" 2>/dev/null \
  || echo "android.aapt2FromMavenOverride=$AAPT2" >> "$APP/gradle.properties"

( cd "$APP" && gradle --no-daemon assembleRelease )

APK="$APP/app/build/outputs/apk/release/app-release.apk"
if cp "$APK" /sdcard/Download/PadStudio.apk 2>/dev/null; then
  echo "APK -> /sdcard/Download/PadStudio.apk  (open with Files to install)"
else
  echo "APK at $APK"
fi
EOF_BUILD

chmod +x "$BIN"/*
"$BIN/padstudio-antigravity-vsix"


step 7/9 "agent brain: subagents + MCP shield wiring"
mkdir -p /root/.gemini/agents

cat > /root/.gemini/agents/argon-architect.md <<'EOF_AG1'
---
name: argon-architect
description: Elephant architect — owns .egm/CANON.md and specs; delegates grunt work to local tools to protect cloud quota.
---
You are the ARCHITECT (elephant memory). Rules:
1. `.egm/CANON.md` is the single source of truth — read it first, keep it current.
2. Write or refine `.egm/specs/SPEC-XXX.md` before any implementation starts.
3. Delegate cheap reconnaissance to the `local_scout` MCP tool and boilerplate
   drafting to `local_draft_code`; reserve your own reasoning for architecture,
   specs and final review.
4. Never violate a CANON invariant. Update "Current state" after every milestone.
EOF_AG1

cat > /root/.gemini/agents/local-scout.md <<'EOF_AG2'
---
name: local-scout
description: Quota shield — answers codebase/reference questions via the local LLM MCP tools before any cloud call.
---
You are a frugal scout. For summaries, lookups and explanations, call the
`local_scout` MCP tool first and only escalate to your own reasoning when the
local answer is insufficient. Keep responses terse and factual.
EOF_AG2

# register the MCP shield in ~/.gemini/settings.json (merge, don't clobber)
node -e 'const fs=require("fs");const p="/root/.gemini/settings.json";let s={};try{s=JSON.parse(fs.readFileSync(p,"utf8"))}catch{};s.mcpServers=Object.assign({},s.mcpServers,{padstudio:{command:"node",args:["/root/padstudio/services/llm-shield-mcp.mjs"]}});fs.writeFileSync(p,JSON.stringify(s,null,2));'

step 8/9 "Android app project (Jetpack Compose shell)"
APP=/root/padstudio/app
mkdir -p "$APP/app/src/main/java/com/padstudio/ide"

cat > "$APP/settings.gradle.kts" <<'EOF_SETTGR'
pluginManagement {
    repositories { google(); mavenCentral(); gradlePluginPortal() }
}
dependencyResolutionManagement {
    repositories { google(); mavenCentral() }
}
rootProject.name = "PadStudio"
include(":app")
EOF_SETTGR

cat > "$APP/build.gradle.kts" <<'EOF_ROOTGR'
plugins {
    id("com.android.application") version "8.8.0" apply false
    id("org.jetbrains.kotlin.android") version "2.1.0" apply false
    id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false
}
EOF_ROOTGR

cat > "$APP/gradle.properties" <<'EOF_PROPS'
org.gradle.jvmargs=-Xmx3072m -XX:+UseZGC -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.caching=true
android.useAndroidX=true
android.suppressUnsupportedCompileSdk=36
kotlin.code.style=official
EOF_PROPS

cat > "$APP/app/build.gradle.kts" <<'EOF_APPGR'
import java.io.File
import java.util.Properties

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.compose")
}

val loopbackTokenFile = File("/root/.config/padstudio/loopback.token")
require(loopbackTokenFile.isFile && loopbackTokenFile.canRead()) {
    "PadStudio loopback token is missing; rerun the installer before building the APK."
}
val loopbackToken = loopbackTokenFile.readText().trim()
require(loopbackToken.matches(Regex("[0-9a-f]{64}"))) {
    "PadStudio loopback token is invalid; rerun the installer before building the APK."
}

val signingPropertiesFile = File("/root/padstudio/keys/signing.properties")
val signingProperties = Properties().apply {
    require(signingPropertiesFile.isFile && signingPropertiesFile.canRead()) {
        "PadStudio signing properties are missing; run padstudio-build-apk to initialize signing."
    }
    signingPropertiesFile.inputStream().use { load(it) }
}
val signingStorePassword = requireNotNull(signingProperties.getProperty("storePassword")) {
    "PadStudio store password is missing from signing.properties."
}.also { require(it.isNotBlank()) { "PadStudio store password is blank." } }
val signingKeyPassword = requireNotNull(signingProperties.getProperty("keyPassword")) {
    "PadStudio key password is missing from signing.properties."
}.also { require(it.isNotBlank()) { "PadStudio key password is blank." } }

android {
    namespace = "com.padstudio.ide"
    compileSdk = 36

    defaultConfig {
        applicationId = "com.padstudio.ide"
        minSdk = 31
        targetSdk = 36
        versionCode = 13
        versionName = "13.0-caviar"
        buildConfigField("String", "LOOPBACK_TOKEN", "\"$loopbackToken\"")
    }

    signingConfigs {
        create("release") {
            storeFile = file("/root/padstudio/keys/padstudio.jks")
            storePassword = signingStorePassword
            keyAlias = "padstudio"
            keyPassword = signingKeyPassword
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = false   // no R8 stripping surprises
            signingConfig = signingConfigs.getByName("release")
        }
    }

    buildFeatures {
        compose = true
        buildConfig = true
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions { jvmTarget = "17" }
}

dependencies {
    implementation(platform("androidx.compose:compose-bom:2025.02.00"))
    implementation("androidx.activity:activity-compose:1.10.0")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.browser:browser:1.8.0")
}
EOF_APPGR

cat > "$APP/app/src/main/AndroidManifest.xml" <<'EOF_MAN'
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:label="PadStudio"
        android:usesCleartextTraffic="true"
        android:theme="@android:style/Theme.Material.NoActionBar"
        android:allowBackup="false">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTask"
            android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density|keyboard|keyboardHidden">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <!-- OAuth deep-link relay: vscode:// callbacks land here -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="vscode" />
            </intent-filter>
        </activity>
    </application>
</manifest>
EOF_MAN


cat > "$APP/app/src/main/java/com/padstudio/ide/MainActivity.kt" <<'EOF_KT'
package com.padstudio.ide

import android.annotation.SuppressLint
import android.app.Presentation
import android.content.Context
import android.content.Intent
import android.hardware.display.DisplayManager
import android.net.Uri
import android.os.Bundle
import android.view.ViewGroup
import android.webkit.HttpAuthHandler
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.addCallback
import androidx.activity.compose.setContent
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView

private const val TOKEN = BuildConfig.LOOPBACK_TOKEN
private val TABS = listOf("Editor", "Agent", "Preview")

class MainActivity : ComponentActivity() {

    private val webViews = mutableMapOf<Int, WebView>()
    private var presentation: Presentation? = null

    private fun urlFor(tab: Int) = when (tab) {
        0 -> "http://127.0.0.1:8080/?tkn=$TOKEN&folder=/root/workspace"
        1 -> "http://127.0.0.1:7681/"
        else -> "http://127.0.0.1:3000/"
    }

    @SuppressLint("SetJavaScriptEnabled")
    private fun buildWebView(tab: Int): WebView = WebView(this).apply {
        settings.javaScriptEnabled = true
        settings.domStorageEnabled = true
        settings.databaseEnabled = true
        settings.mediaPlaybackRequiresUserGesture = false
        webChromeClient = WebChromeClient()
        webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(
                view: WebView?, request: WebResourceRequest?
            ): Boolean {
                val uri = request?.url ?: return false
                val host = uri.host ?: return false
                if (host == "127.0.0.1" || host == "localhost") return false
                // OAuth in WebView = 403 disallowed_useragent -> Chrome Custom Tab
                return try {
                    CustomTabsIntent.Builder().build().launchUrl(this@MainActivity, uri)
                    true
                } catch (_: Exception) {
                    startActivity(Intent(Intent.ACTION_VIEW, uri)); true
                }
            }

            override fun onReceivedHttpAuthRequest(
                view: WebView?, handler: HttpAuthHandler?, host: String?, realm: String?
            ) {
                handler?.proceed("padstudio", TOKEN.take(16))   // ttyd basic auth
            }
        }
        loadUrl(urlFor(tab))
    }

    private fun webViewFor(tab: Int): WebView =
        webViews.getOrPut(tab) { buildWebView(tab) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        onBackPressedDispatcher.addCallback(this) {
            val wv = webViews.values.firstOrNull { it.isShown }
            if (wv?.canGoBack() == true) wv.goBack()
        }
        setContent { Ui() }
        handleDeepLink(intent)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        handleDeepLink(intent)
    }

    // vscode://authority/path?query  ->  loopback /callback fetch.
    // Must be fetch() with credentials — window.open is blocked by WebView.
    private fun handleDeepLink(i: Intent?) {
        val data: Uri = i?.data ?: return
        if (data.scheme != "vscode") return
        val cb = buildString {
            append("http://127.0.0.1:8080/callback?vscode-authority=")
            append(Uri.encode(data.authority ?: ""))
            append("&vscode-path=").append(Uri.encode(data.path ?: ""))
            data.encodedQuery?.let { append("&").append(it) }
        }
        webViews[0]?.evaluateJavascript(
            "fetch('$cb', {credentials:'include'});", null
        )
    }

    // External monitor: real second workspace (overrides HyperOS mirror mode),
    // preferring the highest refresh rate at native resolution.
    private fun toggleExternalDisplay() {
        presentation?.let { it.dismiss(); presentation = null; return }
        val dm = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
        val disp = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION)
            .firstOrNull() ?: return
        presentation = object : Presentation(this, disp) {
            override fun onCreate(b: Bundle?) {
                super.onCreate(b)
                val best = disp.supportedModes
                    .filter {
                        it.physicalWidth == disp.mode.physicalWidth &&
                            it.physicalHeight == disp.mode.physicalHeight
                    }
                    .maxByOrNull { it.refreshRate }
                best?.let { m ->
                    window?.let { w ->
                        w.attributes = w.attributes.apply { preferredDisplayModeId = m.modeId }
                    }
                }
                setContentView(buildWebView(0))
            }
        }.also { it.show() }
    }

    @Composable
    private fun Ui() {
        var tab by remember { mutableIntStateOf(0) }
        MaterialTheme(colorScheme = darkColorScheme()) {
            Scaffold(
                bottomBar = {
                    NavigationBar {
                        TABS.forEachIndexed { i, label ->
                            NavigationBarItem(
                                selected = tab == i,
                                onClick = { tab = i },
                                icon = { Text(if (i == 0) "{ }" else if (i == 1) ">_" else "▶") },
                                label = { Text(label) }
                            )
                        }
                        NavigationBarItem(
                            selected = false,
                            onClick = { toggleExternalDisplay() },
                            icon = { Text("⇱") },
                            label = { Text("Cast") }
                        )
                    }
                }
            ) { pad ->
                Box(Modifier.fillMaxSize().padding(pad)) {
                    key(tab) {
                        AndroidView(
                            modifier = Modifier.fillMaxSize(),
                            factory = {
                                webViewFor(tab).also { wv ->
                                    (wv.parent as? ViewGroup)?.removeView(wv)
                                }
                            }
                        )
                    }
                }
            }
        }
    }
}
EOF_KT

step 9/9 "syntax verification (zero-patch guarantee)"
FAIL=0
for f in /root/padstudio/services/*.mjs; do
  if node --check "$f" 2>/dev/null; then echo "  ok: $f"; else echo "  SYNTAX FAIL: $f"; FAIL=1; fi
done
for f in /root/padstudio/bin/*; do
  if bash -n "$f" 2>/dev/null; then echo "  ok: $f"; else echo "  SYNTAX FAIL: $f"; FAIL=1; fi
done
[ "$FAIL" = 0 ] || { echo "VERIFICATION FAILED — aborting"; exit 1; }

printf '\n\033[1;32m━━ stage 2 complete: all services installed and verified ━━\033[0m\n'
