fix(ui): correct mirrored chat empty-state + clipped IP placeholder; add readiness-check skill; privacy page nav/meta
- app/session/[id].tsx: chat empty-state rendered mirrored on Android (inverted FlatList) — now an untransformed overlay - app/connection/add.tsx: shortened clipped host placeholder - .agents/skills/readiness-check: production-readiness gate (Play+F-Droid published, app+site health) - docs/privacy: back-nav + canonical/theme-color/description meta Verified: tsc clean, CUA smoke green (run 26814702062) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
74
.agents/skills/readiness-check/SKILL.md
Normal file
74
.agents/skills/readiness-check/SKILL.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
name: readiness-check
|
||||||
|
description: Verify OpenCode Mobile is PRODUCTION READY end-to-end. Triggers like "check production readiness", "are we ready to ship", "readiness check", "is opencode-mobile live", "is the app published". Runs a script that confirms both Google Play and F-Droid (self-hosted + mainline) are PUBLISHED and the app/site are healthy.
|
||||||
|
category: release
|
||||||
|
version: 1.0.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# OpenCode Mobile — Readiness Check
|
||||||
|
|
||||||
|
Single command that answers one question: **is OpenCode Mobile production ready?**
|
||||||
|
|
||||||
|
"Production ready" has a precise definition of done:
|
||||||
|
|
||||||
|
- The app is **PUBLISHED on Google Play** (`cc.agentlabs.opencode`).
|
||||||
|
- The app is **PUBLISHED on F-Droid mainline** (`f-droid.org/packages/cc.agentlabs.opencode`).
|
||||||
|
- The **self-hosted F-Droid repo** serves the latest APK.
|
||||||
|
- The **app builds and tests green** (typecheck + node:test suite).
|
||||||
|
- The **web presence** (landing, guide, privacy, sitemap, robots, OG image, QR codes) is all live.
|
||||||
|
|
||||||
|
If, and only if, every REQUIRED gate passes, the verdict is `PRODUCTION READY`.
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
From the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash .agents/skills/readiness-check/check.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Fast mode — skip the slow npm app-health gates, only check live published-status URLs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash .agents/skills/readiness-check/check.sh --quick
|
||||||
|
```
|
||||||
|
|
||||||
|
The script needs only `curl` and `git`. It opportunistically uses `python3` or `jq`
|
||||||
|
to parse the F-Droid index, and `gh` for the optional repo-discoverability gate.
|
||||||
|
Missing optional tools degrade gracefully (gate reports `WARN` or `UNKNOWN`, never crashes).
|
||||||
|
|
||||||
|
## What it checks
|
||||||
|
|
||||||
|
Gates are grouped. REQUIRED gates decide the verdict; nice-to-have gates only WARN.
|
||||||
|
|
||||||
|
- **A. App health** (REQUIRED, skipped with `--quick`)
|
||||||
|
- `npm run typecheck` exits clean.
|
||||||
|
- `npm test` passes.
|
||||||
|
- **B. F-Droid self-hosted repo LIVE** (REQUIRED)
|
||||||
|
- `https://dzianisv.github.io/opencode-mobile/fdroid/repo/index-v1.json` parses and contains `cc.agentlabs.opencode`; prints served versionName.
|
||||||
|
- `https://github.com/dzianisv/opencode-mobile/releases/latest` returns 200/3xx.
|
||||||
|
- **C. F-Droid MAINLINE published** (REQUIRED, headline)
|
||||||
|
- `https://f-droid.org/packages/cc.agentlabs.opencode/` returns 200. A 404 means not yet merged on f-droid.org → this gate FAILS. Do not confuse with the self-hosted gate (B).
|
||||||
|
- **D. Google Play PUBLISHED** (REQUIRED, headline)
|
||||||
|
- `https://play.google.com/store/apps/details?id=cc.agentlabs.opencode` returns 200 with a real store page. While in-review/draft it 404s → gate FAILS.
|
||||||
|
- **E. Web presence** (REQUIRED)
|
||||||
|
- landing `/`, `/guide/`, `/privacy/`, `sitemap.xml`, `robots.txt`, `og.png`, `fdroid-qr.png`, `apk-qr.png` under `https://dzianisv.github.io/opencode-mobile/` all return 200.
|
||||||
|
- **F. Repo discoverability** (nice-to-have, WARN only)
|
||||||
|
- `gh repo view dzianisv/opencode-mobile --json repositoryTopics,homepageUrl` shows topics + homepage. Skipped gracefully if `gh` is missing/unauthenticated.
|
||||||
|
|
||||||
|
## Interpreting results
|
||||||
|
|
||||||
|
Each gate prints one line: `[PASS] / [FAIL] / [WARN] / [UNKNOWN] <gate> — <detail>`.
|
||||||
|
|
||||||
|
The summary at the bottom is the answer:
|
||||||
|
|
||||||
|
- **`PRODUCTION READY ✅`** — every REQUIRED gate passed. Both stores are live, app and site healthy. Script exits `0`.
|
||||||
|
- **`NOT READY ❌`** — at least one REQUIRED gate failed; the failing gates are listed. Script exits `1`.
|
||||||
|
|
||||||
|
The two headline gates are **D (Google Play)** and **C (F-Droid mainline)** because
|
||||||
|
"published on both stores" is the definition of done. Until Play review completes and
|
||||||
|
the F-Droid mainline MR is merged, expect those two to FAIL and the verdict to be NOT READY —
|
||||||
|
that is correct behavior, not a script bug.
|
||||||
|
|
||||||
|
`UNKNOWN` means a network/tool problem prevented the check (e.g. offline). UNKNOWN on a
|
||||||
|
REQUIRED gate is treated as not-passing, so the verdict will be NOT READY.
|
||||||
316
.agents/skills/readiness-check/check.sh
Executable file
316
.agents/skills/readiness-check/check.sh
Executable file
@@ -0,0 +1,316 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# readiness-check — verify OpenCode Mobile is PRODUCTION READY.
|
||||||
|
# Verdict is PRODUCTION READY only if every REQUIRED gate passes:
|
||||||
|
# A app-health, B fdroid-selfhosted, C fdroid-mainline, D google-play, E web.
|
||||||
|
# Usage:
|
||||||
|
# bash check.sh full check
|
||||||
|
# bash check.sh --quick skip slow npm app-health gates
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
APP_ID="cc.agentlabs.opencode"
|
||||||
|
SITE="https://dzianisv.github.io/opencode-mobile"
|
||||||
|
FDROID_INDEX="$SITE/fdroid/repo/index-v1.json"
|
||||||
|
RELEASES="https://github.com/dzianisv/opencode-mobile/releases/latest"
|
||||||
|
PLAY="https://play.google.com/store/apps/details?id=$APP_ID"
|
||||||
|
FDROID_MAINLINE="https://f-droid.org/packages/$APP_ID/"
|
||||||
|
|
||||||
|
CURL_OPTS=(--silent --show-error --location --max-time 20 --connect-timeout 8 --retry 1)
|
||||||
|
|
||||||
|
# Resolve repo root (script may be invoked from anywhere).
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
QUICK=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--quick) QUICK=1 ;;
|
||||||
|
-h|--help) sed -n '2,11p' "$0"; exit 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---- color (only if tty) ----
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
C_RST=$'\033[0m'; C_GRN=$'\033[32m'; C_RED=$'\033[31m'; C_YEL=$'\033[33m'; C_DIM=$'\033[2m'; C_BLD=$'\033[1m'
|
||||||
|
else
|
||||||
|
C_RST=""; C_GRN=""; C_RED=""; C_YEL=""; C_DIM=""; C_BLD=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- result accumulation ----
|
||||||
|
# Each gate appends "ID|STATUS|detail" ; REQUIRED gates listed in REQUIRED[].
|
||||||
|
declare -a RESULTS=()
|
||||||
|
declare -a REQUIRED=(A_typecheck A_test B_selfhosted B_release C_mainline D_play E_landing E_guide E_privacy E_sitemap E_robots E_og E_fdroidqr E_apkqr)
|
||||||
|
|
||||||
|
record() { # id status detail
|
||||||
|
RESULTS+=("$1|$2|$3")
|
||||||
|
}
|
||||||
|
|
||||||
|
print_line() { # status gate detail
|
||||||
|
local s="$1" gate="$2" detail="$3" tag color
|
||||||
|
case "$s" in
|
||||||
|
PASS) tag="[PASS]"; color="$C_GRN" ;;
|
||||||
|
FAIL) tag="[FAIL]"; color="$C_RED" ;;
|
||||||
|
WARN) tag="[WARN]"; color="$C_YEL" ;;
|
||||||
|
UNKNOWN) tag="[UNKNOWN]"; color="$C_YEL" ;;
|
||||||
|
*) tag="[????]"; color="$C_RST" ;;
|
||||||
|
esac
|
||||||
|
printf " %s%-9s%s %-22s %s—%s %s\n" "$color" "$tag" "$C_RST" "$gate" "$C_DIM" "$C_RST" "$detail"
|
||||||
|
}
|
||||||
|
|
||||||
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
# HTTP status helper. Echoes the numeric code, or "000" on network failure.
|
||||||
|
http_code() {
|
||||||
|
curl "${CURL_OPTS[@]}" -o /dev/null -w "%{http_code}" "$1" 2>/dev/null || echo "000"
|
||||||
|
}
|
||||||
|
|
||||||
|
section() { printf "\n%s%s%s\n" "$C_BLD" "$1" "$C_RST"; }
|
||||||
|
|
||||||
|
echo "${C_BLD}OpenCode Mobile — Production Readiness Check${C_RST}"
|
||||||
|
echo "${C_DIM}app id: $APP_ID mode: $([ $QUICK -eq 1 ] && echo quick || echo full) $(date -u '+%Y-%m-%dT%H:%M:%SZ')${C_RST}"
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# A. App health
|
||||||
|
# =====================================================================
|
||||||
|
section "A. App health (build + tests)"
|
||||||
|
if [ $QUICK -eq 1 ]; then
|
||||||
|
record A_typecheck WARN "skipped (--quick)"
|
||||||
|
record A_test WARN "skipped (--quick)"
|
||||||
|
print_line WARN "typecheck" "skipped (--quick)"
|
||||||
|
print_line WARN "test" "skipped (--quick)"
|
||||||
|
elif ! have npm; then
|
||||||
|
record A_typecheck UNKNOWN "npm not installed"
|
||||||
|
record A_test UNKNOWN "npm not installed"
|
||||||
|
print_line UNKNOWN "typecheck" "npm not installed"
|
||||||
|
print_line UNKNOWN "test" "npm not installed"
|
||||||
|
else
|
||||||
|
# typecheck
|
||||||
|
if npm --prefix "$REPO_ROOT" run typecheck >/tmp/rc_typecheck.log 2>&1; then
|
||||||
|
record A_typecheck PASS "npm run typecheck clean"
|
||||||
|
print_line PASS "typecheck" "npm run typecheck clean"
|
||||||
|
else
|
||||||
|
record A_typecheck FAIL "tsc errors (see /tmp/rc_typecheck.log)"
|
||||||
|
print_line FAIL "typecheck" "tsc errors (see /tmp/rc_typecheck.log)"
|
||||||
|
fi
|
||||||
|
# test
|
||||||
|
if npm --prefix "$REPO_ROOT" test >/tmp/rc_test.log 2>&1; then
|
||||||
|
record A_test PASS "npm test green"
|
||||||
|
print_line PASS "test" "npm test green"
|
||||||
|
else
|
||||||
|
record A_test FAIL "test failures (see /tmp/rc_test.log)"
|
||||||
|
print_line FAIL "test" "test failures (see /tmp/rc_test.log)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# B. F-Droid — self-hosted repo LIVE (REQUIRED)
|
||||||
|
# =====================================================================
|
||||||
|
section "B. F-Droid — self-hosted repo (REQUIRED)"
|
||||||
|
idx_body="$(curl "${CURL_OPTS[@]}" "$FDROID_INDEX" 2>/dev/null)"
|
||||||
|
idx_rc=$?
|
||||||
|
if [ $idx_rc -ne 0 ] || [ -z "$idx_body" ]; then
|
||||||
|
record B_selfhosted UNKNOWN "could not fetch index-v1.json (network?)"
|
||||||
|
print_line UNKNOWN "fdroid-selfhosted" "could not fetch index-v1.json (network?)"
|
||||||
|
else
|
||||||
|
version=""
|
||||||
|
if have python3; then
|
||||||
|
version="$(printf '%s' "$idx_body" | python3 -c '
|
||||||
|
import json,sys
|
||||||
|
try:
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
except Exception:
|
||||||
|
sys.exit(2)
|
||||||
|
pkg="'"$APP_ID"'"
|
||||||
|
pkgs=d.get("packages",{})
|
||||||
|
if pkg not in pkgs and pkg not in d.get("apps",{}) and not any(
|
||||||
|
(a.get("packageName")==pkg) for a in (d.get("apps",[]) if isinstance(d.get("apps"),list) else [])):
|
||||||
|
sys.exit(3)
|
||||||
|
vs=pkgs.get(pkg)
|
||||||
|
ver=""
|
||||||
|
if isinstance(vs,list) and vs:
|
||||||
|
ver=vs[0].get("versionName","")
|
||||||
|
print(ver)
|
||||||
|
' 2>/dev/null)"
|
||||||
|
py_rc=$?
|
||||||
|
elif have jq; then
|
||||||
|
if printf '%s' "$idx_body" | jq -e --arg p "$APP_ID" '.packages[$p] // empty' >/dev/null 2>&1; then
|
||||||
|
version="$(printf '%s' "$idx_body" | jq -r --arg p "$APP_ID" '.packages[$p][0].versionName // ""' 2>/dev/null)"
|
||||||
|
py_rc=0
|
||||||
|
else
|
||||||
|
py_rc=3
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# no parser: substring fallback
|
||||||
|
if printf '%s' "$idx_body" | grep -q "$APP_ID"; then py_rc=0; version="?"; else py_rc=3; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${py_rc:-1}" -eq 0 ]; then
|
||||||
|
record B_selfhosted PASS "serves $APP_ID${version:+ v$version}"
|
||||||
|
print_line PASS "fdroid-selfhosted" "serves $APP_ID${version:+ v$version}"
|
||||||
|
elif [ "${py_rc:-1}" -eq 2 ]; then
|
||||||
|
record B_selfhosted FAIL "index-v1.json did not parse as JSON"
|
||||||
|
print_line FAIL "fdroid-selfhosted" "index-v1.json did not parse as JSON"
|
||||||
|
else
|
||||||
|
record B_selfhosted FAIL "$APP_ID not present in self-hosted index"
|
||||||
|
print_line FAIL "fdroid-selfhosted" "$APP_ID not present in self-hosted index"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# direct APK / releases
|
||||||
|
rel_code="$(http_code "$RELEASES")"
|
||||||
|
if [ "$rel_code" = "000" ]; then
|
||||||
|
record B_release UNKNOWN "releases/latest unreachable"
|
||||||
|
print_line UNKNOWN "fdroid-apk-release" "releases/latest unreachable"
|
||||||
|
elif [ "$rel_code" -ge 200 ] && [ "$rel_code" -lt 400 ]; then
|
||||||
|
record B_release PASS "releases/latest HTTP $rel_code"
|
||||||
|
print_line PASS "fdroid-apk-release" "releases/latest HTTP $rel_code"
|
||||||
|
else
|
||||||
|
record B_release FAIL "releases/latest HTTP $rel_code"
|
||||||
|
print_line FAIL "fdroid-apk-release" "releases/latest HTTP $rel_code"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# C. F-Droid — MAINLINE published (REQUIRED, headline)
|
||||||
|
# =====================================================================
|
||||||
|
section "C. F-Droid — MAINLINE on f-droid.org (REQUIRED · headline)"
|
||||||
|
play_mainline_code="$(http_code "$FDROID_MAINLINE")"
|
||||||
|
if [ "$play_mainline_code" = "000" ]; then
|
||||||
|
record C_mainline UNKNOWN "f-droid.org unreachable"
|
||||||
|
print_line UNKNOWN "fdroid-mainline" "f-droid.org unreachable"
|
||||||
|
elif [ "$play_mainline_code" = "200" ]; then
|
||||||
|
record C_mainline PASS "LIVE on f-droid.org (HTTP 200)"
|
||||||
|
print_line PASS "fdroid-mainline" "LIVE on f-droid.org (HTTP 200)"
|
||||||
|
elif [ "$play_mainline_code" = "404" ]; then
|
||||||
|
record C_mainline FAIL "not yet published on f-droid.org (HTTP 404 — MR not merged)"
|
||||||
|
print_line FAIL "fdroid-mainline" "not yet published on f-droid.org (HTTP 404 — MR not merged)"
|
||||||
|
else
|
||||||
|
record C_mainline FAIL "unexpected HTTP $play_mainline_code"
|
||||||
|
print_line FAIL "fdroid-mainline" "unexpected HTTP $play_mainline_code"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# D. Google Play — PUBLISHED (REQUIRED, headline)
|
||||||
|
# =====================================================================
|
||||||
|
section "D. Google Play — PUBLISHED (REQUIRED · headline)"
|
||||||
|
play_code="$(http_code "$PLAY")"
|
||||||
|
if [ "$play_code" = "000" ]; then
|
||||||
|
record D_play UNKNOWN "play.google.com unreachable"
|
||||||
|
print_line UNKNOWN "google-play" "play.google.com unreachable"
|
||||||
|
elif [ "$play_code" = "200" ]; then
|
||||||
|
record D_play PASS "PUBLISHED on Google Play (HTTP 200)"
|
||||||
|
print_line PASS "google-play" "PUBLISHED on Google Play (HTTP 200)"
|
||||||
|
elif [ "$play_code" = "404" ]; then
|
||||||
|
record D_play FAIL "not public — in review/draft (HTTP 404)"
|
||||||
|
print_line FAIL "google-play" "not public — in review/draft (HTTP 404)"
|
||||||
|
else
|
||||||
|
record D_play FAIL "unexpected HTTP $play_code"
|
||||||
|
print_line FAIL "google-play" "unexpected HTTP $play_code"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# E. Web presence (REQUIRED)
|
||||||
|
# =====================================================================
|
||||||
|
section "E. Web presence (REQUIRED)"
|
||||||
|
check_url() { # id label url
|
||||||
|
local id="$1" label="$2" url="$3" code
|
||||||
|
code="$(http_code "$url")"
|
||||||
|
if [ "$code" = "000" ]; then
|
||||||
|
record "$id" UNKNOWN "$label unreachable"
|
||||||
|
print_line UNKNOWN "$label" "unreachable ($url)"
|
||||||
|
elif [ "$code" = "200" ]; then
|
||||||
|
record "$id" PASS "$label HTTP 200"
|
||||||
|
print_line PASS "$label" "HTTP 200"
|
||||||
|
else
|
||||||
|
record "$id" FAIL "$label HTTP $code"
|
||||||
|
print_line FAIL "$label" "HTTP $code ($url)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
check_url E_landing "landing" "$SITE/"
|
||||||
|
check_url E_guide "guide" "$SITE/guide/"
|
||||||
|
check_url E_privacy "privacy" "$SITE/privacy/"
|
||||||
|
check_url E_sitemap "sitemap.xml" "$SITE/sitemap.xml"
|
||||||
|
check_url E_robots "robots.txt" "$SITE/robots.txt"
|
||||||
|
check_url E_og "og.png" "$SITE/og.png"
|
||||||
|
check_url E_fdroidqr "fdroid-qr.png" "$SITE/fdroid-qr.png"
|
||||||
|
check_url E_apkqr "apk-qr.png" "$SITE/apk-qr.png"
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# F. Repo discoverability (nice-to-have, WARN only)
|
||||||
|
# =====================================================================
|
||||||
|
section "F. Repo discoverability (nice-to-have)"
|
||||||
|
if ! have gh; then
|
||||||
|
print_line WARN "gh-repo-meta" "gh not installed — skipped"
|
||||||
|
elif ! gh auth status >/dev/null 2>&1; then
|
||||||
|
print_line WARN "gh-repo-meta" "gh unauthenticated — skipped"
|
||||||
|
else
|
||||||
|
meta="$(gh repo view dzianisv/opencode-mobile --json repositoryTopics,homepageUrl 2>/dev/null)"
|
||||||
|
if [ -z "$meta" ]; then
|
||||||
|
print_line WARN "gh-repo-meta" "could not read repo metadata"
|
||||||
|
else
|
||||||
|
if have jq; then
|
||||||
|
topics="$(printf '%s' "$meta" | jq -r '[.repositoryTopics[]?.name] | join(",")' 2>/dev/null)"
|
||||||
|
home="$(printf '%s' "$meta" | jq -r '.homepageUrl // ""' 2>/dev/null)"
|
||||||
|
elif have python3; then
|
||||||
|
topics="$(printf '%s' "$meta" | python3 -c 'import json,sys;d=json.load(sys.stdin);print(",".join(t.get("name","") for t in d.get("repositoryTopics") or []))' 2>/dev/null)"
|
||||||
|
home="$(printf '%s' "$meta" | python3 -c 'import json,sys;d=json.load(sys.stdin);print(d.get("homepageUrl") or "")' 2>/dev/null)"
|
||||||
|
else
|
||||||
|
topics="(install jq/python3 to parse)"; home=""
|
||||||
|
fi
|
||||||
|
if [ -n "$topics" ] || [ -n "$home" ]; then
|
||||||
|
print_line PASS "gh-repo-meta" "topics=[${topics:-none}] homepage=${home:-none}"
|
||||||
|
else
|
||||||
|
print_line WARN "gh-repo-meta" "no topics/homepage set"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# Summary / verdict
|
||||||
|
# =====================================================================
|
||||||
|
status_of() { # id -> echoes status, "MISSING" if not recorded
|
||||||
|
local id="$1" r
|
||||||
|
for r in "${RESULTS[@]}"; do
|
||||||
|
case "$r" in "$id|"*) printf '%s' "${r#*|}" | cut -d'|' -f1; return;; esac
|
||||||
|
done
|
||||||
|
echo "MISSING"
|
||||||
|
}
|
||||||
|
|
||||||
|
declare -a FAILING=()
|
||||||
|
declare -a SKIPPED=()
|
||||||
|
for id in "${REQUIRED[@]}"; do
|
||||||
|
st="$(status_of "$id")"
|
||||||
|
if [ "$st" = "PASS" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# In --quick mode the app-health gates are intentionally skipped (WARN);
|
||||||
|
# don't list them as "failing", but they still block a PRODUCTION READY verdict.
|
||||||
|
if [ $QUICK -eq 1 ] && [ "$st" = "WARN" ] && { [ "$id" = "A_typecheck" ] || [ "$id" = "A_test" ]; }; then
|
||||||
|
SKIPPED+=("$id")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
FAILING+=("$id($st)")
|
||||||
|
done
|
||||||
|
|
||||||
|
section "Headline gates"
|
||||||
|
print_line "$(status_of D_play)" "GOOGLE PLAY" "$([ "$(status_of D_play)" = PASS ] && echo PUBLISHED || echo 'NOT published')"
|
||||||
|
print_line "$(status_of C_mainline)" "F-DROID MAINLINE" "$([ "$(status_of C_mainline)" = PASS ] && echo PUBLISHED || echo 'NOT published')"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=============================================================="
|
||||||
|
if [ ${#FAILING[@]} -eq 0 ] && [ ${#SKIPPED[@]} -eq 0 ]; then
|
||||||
|
echo "${C_GRN}${C_BLD} PRODUCTION READY ✅${C_RST}"
|
||||||
|
echo "${C_DIM} All required gates passed — both stores live, app + site healthy.${C_RST}"
|
||||||
|
echo "=============================================================="
|
||||||
|
exit 0
|
||||||
|
elif [ ${#FAILING[@]} -eq 0 ] && [ ${#SKIPPED[@]} -gt 0 ]; then
|
||||||
|
# All checked required gates passed, but --quick skipped app-health.
|
||||||
|
echo "${C_YEL}${C_BLD} LIVE GATES PASS — run full check to confirm PRODUCTION READY ⚠${C_RST}"
|
||||||
|
echo "${C_DIM} Skipped in --quick mode: ${SKIPPED[*]} (run without --quick).${C_RST}"
|
||||||
|
echo "=============================================================="
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "${C_RED}${C_BLD} NOT READY ❌${C_RST}"
|
||||||
|
echo " Failing required gates:"
|
||||||
|
for f in "${FAILING[@]}"; do echo " - $f"; done
|
||||||
|
echo "=============================================================="
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -165,7 +165,7 @@ export default function AddConnectionScreen() {
|
|||||||
<View style={styles.ipRow}>
|
<View style={styles.ipRow}>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.input, styles.ipInput, isDark && styles.inputDark]}
|
style={[styles.input, styles.ipInput, isDark && styles.inputDark]}
|
||||||
placeholder="192.168.1.100 or my-mac.ts.net"
|
placeholder="192.168.1.100"
|
||||||
placeholderTextColor={isDark ? "#666666" : "#999999"}
|
placeholderTextColor={isDark ? "#666666" : "#999999"}
|
||||||
value={ip}
|
value={ip}
|
||||||
onChangeText={setIp}
|
onChangeText={setIp}
|
||||||
|
|||||||
@@ -516,14 +516,16 @@ export default function SessionScreen() {
|
|||||||
</View>
|
</View>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
ListEmptyComponent={
|
|
||||||
<View style={s.emptyInverted}>
|
|
||||||
<Ionicons name="chatbubble-outline" size={48} color={isDark ? "#444444" : "#cccccc"} />
|
|
||||||
<Text style={[s.emptyText, isDark && s.metaDark]}>Start a conversation</Text>
|
|
||||||
<Text style={[s.emptyHint, isDark && s.metaDark]}>Type / for commands</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
{/* Empty state rendered OUTSIDE the inverted list to avoid the
|
||||||
|
inverted transform mirroring its text/icon (see #ui-mirror). */}
|
||||||
|
{messageData.length === 0 && (
|
||||||
|
<View style={s.emptyOverlay} pointerEvents="none">
|
||||||
|
<Ionicons name="chatbubble-outline" size={48} color={isDark ? "#444444" : "#cccccc"} />
|
||||||
|
<Text style={[s.emptyText, isDark && s.metaDark]}>Start a conversation</Text>
|
||||||
|
<Text style={[s.emptyHint, isDark && s.metaDark]}>Type / for commands</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
{showScrollButton && (
|
{showScrollButton && (
|
||||||
<TouchableOpacity style={[s.scrollBtn, isDark && s.scrollBtnDark]} onPress={() => scrollToBottom(true)}>
|
<TouchableOpacity style={[s.scrollBtn, isDark && s.scrollBtnDark]} onPress={() => scrollToBottom(true)}>
|
||||||
<Ionicons name="chevron-down" size={24} color={isDark ? "#ffffff" : "#0a0a0a"} />
|
<Ionicons name="chevron-down" size={24} color={isDark ? "#ffffff" : "#0a0a0a"} />
|
||||||
@@ -690,13 +692,13 @@ const s = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
loadingMoreText: { fontSize: 13, color: "#999999" },
|
loadingMoreText: { fontSize: 13, color: "#999999" },
|
||||||
|
|
||||||
// Empty (inverted list flips content, so use transform to un-flip)
|
// Empty state overlay — sits on top of the (empty) inverted list, untransformed,
|
||||||
emptyInverted: {
|
// so its text/icon render upright and un-mirrored on Android.
|
||||||
flex: 1,
|
emptyOverlay: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingVertical: 64,
|
paddingVertical: 64,
|
||||||
transform: [{ scaleY: -1 }],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Empty
|
// Empty
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>OpenCode Mobile — Privacy Policy</title>
|
<title>OpenCode Mobile — Privacy Policy</title>
|
||||||
|
<meta name="description" content="Privacy policy for OpenCode Mobile, the open-source Android client for the OpenCode AI coding agent. We do not collect your code, prompts, server URLs, or credentials.">
|
||||||
|
<meta name="theme-color" content="#3b82f6">
|
||||||
|
<link rel="canonical" href="https://dzianisv.github.io/opencode-mobile/privacy/">
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
body {
|
body {
|
||||||
@@ -85,6 +88,10 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
<p style="font-size:14px;margin-bottom:18px;">
|
||||||
|
<a href="https://dzianisv.github.io/opencode-mobile/">← Back to OpenCode Mobile</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1>OpenCode Mobile — Privacy Policy</h1>
|
<h1>OpenCode Mobile — Privacy Policy</h1>
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
@@ -306,7 +313,10 @@
|
|||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
© 2026 VIBE TECHNOLOGIES, LLC. OpenCode Mobile is MIT-licensed open-source software.
|
© 2026 VIBE TECHNOLOGIES, LLC. OpenCode Mobile is MIT-licensed open-source software.
|
||||||
Privacy policy effective 2026-05-24.
|
Privacy policy effective 2026-05-24.<br>
|
||||||
|
<a href="https://dzianisv.github.io/opencode-mobile/">Home</a> ·
|
||||||
|
<a href="https://dzianisv.github.io/opencode-mobile/guide/">Setup guide</a> ·
|
||||||
|
<a href="https://github.com/dzianisv/opencode-mobile">GitHub</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user