app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
master
John Lauer
rust/verbs/assets/import-scan.ps1 (embedded by import.rs, missed by the rust/ push's extension filter; pup-bridge#118)
9feb6ee
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
#!/usr/bin/env bash
# ship.sh — THE publish pipeline for this bridge, living IN THE REPO.
#
# tools/ship.sh <version> "<changelog sentence>" # -> INSIDERS tier (the default)
# tools/promote.sh <version> "<human's words>" --by "<name>" # -> PUBLIC tier, attested
# tools/rollback.sh <version> "<human's words>" --by "<name>" # re-point PUBLIC at an older build
#
# RELEASE TIERS (pup-bridge#86, ab 2.0.99 insidersManifestUrl; adopted 2026-09-02 on John's word,
# and corrected the same hour when he caught the first cut creating a NEW private page: that is
# wiki-bloat. The fleet convention is ONE shared private page, adom/insiders, whose repo carries
# ONLY <product>/version.json manifests; the bytes live as releases on the product's own page.)
#
# - Every ship: the zip becomes a release on adom/pup-bridge (like ab's installer on
# adom/adom-bridge), the INSIDERS manifest is written to adom/insiders as
# pup-bridge/version.json (bridge-manifest shape), the source text files are pushed to the
# public page repo (source is open), and the skills pkg is published so the page's top
# version row always carries a tarball (release rows + pkg rows share one namespace; a
# release-only row breaks `pkg install`, #66). The PUBLIC manifest
# (adom-bridge-puppeteer-manifest.json, updateManifestUrl) is NEVER touched here.
# - Desktops on ab >= 2.0.99 whose login can read adom/insiders follow the insiders manifest;
# everyone else follows the public manifest and never learns the tier exists.
# - The public manifest moves ONLY via tools/promote.sh, which REFUSES without the human's exact
# words and records them in the manifest. One plain semver per build in both tiers.
#
# Env: FORCE_PUSH=1 pushes even when the working tree looks unchanged.
set -euo pipefail
VER="${1:?usage: tools/ship.sh <version> \"<changelog>\" [--shell rust|node] [--candidate | --insiders-from-candidate]}"
LOG="${2:?changelog sentence required}"
# --shell rust (DEFAULT since Phase 4 of the Rust port, 2026-09-05): the package is the Rust exe.
# --shell node builds the Node zip (the public tier's shell until a Rust build is promoted).
# --candidate: everything EXCEPT the insiders manifest push; the manifest is pushed to the product
# page as adom-bridge-puppeteer-manifest-candidate.json so ONE desktop can bridge_install it and be
# tested thoroughly (John: "test thoroughly before sending to insiders").
# --insiders-from-candidate: only the insiders push, from the archived dist/<ver>/insiders-manifest.json.
SHELL_KIND="rust"; MODE="full"
shift 2 || true
while [ $# -gt 0 ]; do case "$1" in --shell) SHELL_KIND="$2"; shift 2;; --candidate) MODE="candidate"; shift;; --insiders-from-candidate) MODE="finalize"; shift;; *) echo "unknown arg $1"; exit 1;; esac; done
REPO="$(cd "$(dirname "$0")/.." && pwd)"
PAGE="adom/pup-bridge"
INSIDERS_PAGE="adom/insiders"
INSIDERS_DIR="dist/insiders-repo" # working copy shaped like the adom/insiders repo
INSIDERS_REL="pup-bridge/version.json" # the convention: <product>/version.json
ZIP="adom-bridge-puppeteer-v${VER}.zip"
ARCH="dist/${VER}"
cd "$REPO"
if [ "$MODE" = "finalize" ]; then
[ -f "dist/${VER}/insiders-manifest.json" ] || { echo "no archived candidate for $VER (run --candidate first)"; exit 1; }
echo "== finalize: pushing the archived candidate manifest for $VER to insiders =="
mkdir -p "$INSIDERS_DIR/pup-bridge"; cp "dist/${VER}/insiders-manifest.json" "$INSIDERS_DIR/$INSIDERS_REL"
fi
if [ "$MODE" != "finalize" ]; then
echo "== 0. static gates ($SHELL_KIND) =="
if [ "$SHELL_KIND" = "node" ]; then
node --check src/server.js
node --check src/chrome.js
node --check src/card_vault.js
node tests/check_dashboard_script.js >/dev/null
node tests/check_card_vault.js >/dev/null
node tests/check_consent.js >/dev/null
node tests/check_unpack.js >/dev/null
node tests/check_icon.js >/dev/null
echo " server/chrome/card_vault parse; dashboard script parses; card vault checks; dock favicon matches"
fi
echo "== 1. version lockstep =="
printf '%s' "$VER" > src/BRIDGE_VERSION
python3 - "$VER" "$LOG" <<'PY'
import json, sys
v = sys.argv[1]
p = 'src/bridge.json'
d = json.load(open(p))
d['version'] = v
assert d.get('insidersManifestUrl', '').endswith('/pages/insiders/files/pup-bridge/version.json'), 'bridge.json insidersManifestUrl must point at adom/insiders pup-bridge/version.json (pup-bridge#86)'
json.dump(d, open(p, 'w'), indent=2)
open(p, 'a').write('\n')
print(f" src/BRIDGE_VERSION + src/bridge.json -> {v}")
p2 = 'package.json'
d2 = json.load(open(p2))
d2['version'] = v
d2['changelog'] = sys.argv[2] if len(sys.argv) > 2 else d2.get('changelog', '') # forces a pkg version row even when the skills tarball is byte-identical (PUBLISH_UNCHANGED), so the release-born row (#66, wiki#188) never sits on top without a tarball
json.dump(d2, open(p2, 'w'), indent=2)
open(p2, 'a').write('\n')
print(f" package.json (skillpack) -> {v}")
PY
if [ "$SHELL_KIND" = "rust" ]; then
echo "== 2. build the Rust exe zip (tools/rust-build.sh) =="
tools/rust-build.sh "$VER"
else
echo "== 2. build the zip (src/ at zip root, no node_modules) =="
mkdir -p "$ARCH"
python3 - "$ARCH/$ZIP" <<'PY'
import zipfile, os, hashlib, sys
out = sys.argv[1]
with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as z:
for dirpath, dirs, files in os.walk('src'):
if 'node_modules' in dirs: dirs.remove('node_modules')
for f in files:
full = os.path.join(dirpath, f)
z.write(full, os.path.relpath(full, 'src'))
print(f" {out} sha256={hashlib.sha256(open(out,'rb').read()).hexdigest()} size={os.path.getsize(out)}")
PY
fi
echo "== 2b. STRICT YAML gate on every bundled SKILL.md (#103: a double-quoted description with inner quotes was rejected by Codex; the pkg lint only warned) =="
python3 - <<'PYEOF' || { echo "SHIP ABORTED: a SKILL.md frontmatter does not parse strictly; fix it (description: >- folded scalar) and re-run"; exit 1; }
import glob, re, sys, yaml
bad = []
for p in sorted(glob.glob('skills/*/SKILL.md') + glob.glob('dev-skills/*/SKILL.md') + glob.glob('publish-skills/*/SKILL.md') + ['SKILL.md']):
m = re.match(r'^---\n(.*?)\n---\n', open(p).read(), re.S)
if not m: bad.append((p, 'no frontmatter')); continue
try:
d = yaml.safe_load(m.group(1))
if not isinstance(d, dict) or not isinstance(d.get('name'), str) or not isinstance(d.get('description'), str): bad.append((p, 'name/description missing'))
except Exception as e: bad.append((p, str(e).splitlines()[0][:80]))
for p, why in bad: print(' INVALID', p, '|', why)
print(' strict YAML: %d skill files parsed, %d invalid' % (len(glob.glob('skills/*/SKILL.md')) + len(glob.glob('dev-skills/*/SKILL.md')) + len(glob.glob('publish-skills/*/SKILL.md')) + 1, len(bad)))
sys.exit(1 if bad else 0)
PYEOF
echo "== 3a. skills pkg FIRST (a release-born version row never gets the pkg tarball attached: 2.0.398; #66) =="
PUBOUT=$(adom-wiki pkg publish --org adom 2>&1); PUBRC=$?
echo "$PUBOUT" | tail -2
[ $PUBRC -eq 0 ] || { echo "PKG PUBLISH FAILED (exit $PUBRC)"; echo "$PUBOUT" | tail -15; exit 1; }
adom-wiki pkg info adom/pup-bridge 2>/dev/null | python3 -c "
import sys,json
raw=sys.stdin.read()
d,_=json.JSONDecoder().raw_decode(raw[raw.find('{'):])
v=d['versions'][0]
assert v.get('integrity') and v.get('size_bytes'), f\"top row {v['version']} has NO tarball\"
print(f\" pkg tarball verified on top row {v['version']} ({v['size_bytes']} bytes)\")" || { echo "PKG TARBALL VERIFY FAILED"; exit 1; }
echo "== 3. release on the product page ($PAGE) =="
adom-wiki release create "$PAGE" "$VER" >/dev/null 2>&1 || true
UP=$(adom-wiki release upload "$PAGE" "$VER" "$ARCH/$ZIP" --json 2>&1 | tail -c 4000)
mkdir -p "$INSIDERS_DIR/pup-bridge"
PUP_SHELL_KIND="$SHELL_KIND" python3 - "$UP" "$VER" "$ZIP" "$ARCH" "$INSIDERS_DIR/$INSIDERS_REL" <<'PY'
import json, sys, datetime, hashlib, os
raw, ver, zipname, arch, ins_path = sys.argv[1:6]
i = raw.find('{')
d = json.loads(raw[i:]) if i >= 0 else {}
a = d.get('data') or d.get('asset') or d
sha, size = a.get('sha256'), a.get('size')
url = a.get('download_url') or f"https://wiki.adom.inc/download/adom/pup-bridge/{ver}/{zipname}"
if not url.startswith('http'): url = 'https://wiki.adom.inc' + url
zp = os.path.join(arch, zipname)
if not (sha and size):
sha = hashlib.sha256(open(zp, 'rb').read()).hexdigest(); size = os.path.getsize(zp)
# Insiders manifest = the public manifest's SHAPE (name/entrypoint/etc. identical between tiers).
base = json.load(open('adom-bridge-puppeteer-manifest.json'))
for k in ('promotion_attestation', 'promoted_by', 'promoted_at', 'rollback_attestation', 'rolled_back_by', 'rolled_back_at', 'insiders_released_at'):
base.pop(k, None)
bj = json.load(open('src/bridge.json'))
base.update({'verbPrefixes': bj.get('verbPrefixes', base.get('verbPrefixes')), 'statusVerb': bj.get('statusVerb', base.get('statusVerb')), 'healthEndpoint': bj.get('spawn', {}).get('healthEndpoint', '/status')})
base['shell'] = os.environ.get('PUP_SHELL_KIND', 'node')
base.update({'version': ver, 'url': url, 'sha256': sha, 'size': size, 'tier': 'insiders',
'released_at': datetime.datetime.now(datetime.timezone.utc).isoformat().replace('+00:00', 'Z')})
json.dump(base, open(ins_path, 'w'), indent=2); open(ins_path, 'a').write('\n')
json.dump(base, open(os.path.join(arch, 'insiders-manifest.json'), 'w'), indent=2)
print(f" release asset -> {url}\n sha256={sha} size={size}\n insiders manifest staged at {ins_path}; archived {arch}/")
PY
if [ "$MODE" = "candidate" ]; then
echo "== 4c. CANDIDATE manifest -> $PAGE:adom-bridge-puppeteer-manifest-candidate.json (insiders untouched) =="
cp "$ARCH/insiders-manifest.json" adom-bridge-puppeteer-manifest-candidate.json
adom-wiki repo push "$PAGE" --files adom-bridge-puppeteer-manifest-candidate.json src/bridge.json src/BRIDGE_VERSION package.json -m "v${VER} candidate ($SHELL_KIND): ${LOG}" >/dev/null 2>&1 || { echo " CANDIDATE PUSH FAILED"; exit 1; }
echo " install on ONE desktop: adom-bridge --target <box> bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/pup-bridge/files/adom-bridge-puppeteer-manifest-candidate.json\",\"force\":true}'"
echo " then, when it is proven: tools/ship.sh $VER \"<log>\" --insiders-from-candidate"
exit 0
fi
fi # MODE != finalize
echo "== 4. insiders manifest + release log -> $INSIDERS_PAGE:pup-bridge/ =="
# Convention (adom-bridge/README.md, hydrogen/README.md on adom/insiders): every insiders publish
# APPENDS a release-log entry to <product>/README.md before the publish is called done; promote
# edits that entry with the date and the human's words. Newest first.
python3 - "$INSIDERS_DIR/pup-bridge/README.md" "$VER" "$LOG" <<'PY'
import sys, datetime, re, os
path, ver, log = sys.argv[1:4]
today = datetime.date.today().isoformat()
entry = f"- **{ver}** insiders, {today}. {log} Not promoted.\n"
s = open(path).read() if os.path.exists(path) else "# pup (Puppeteer bridge) on the insiders tier\n\n## Release log\n\n"
if f"- **{ver}**" in s: print(f" release log already has {ver}")
else:
s = re.sub(r"(## Release log\n\n)", r"\1" + entry.replace('\\', '\\\\'), s, count=1)
open(path, 'w').write(s); print(f" release log += {ver}")
PY
( cd "$INSIDERS_DIR" && adom-wiki repo push "$INSIDERS_PAGE" --files "$INSIDERS_REL" pup-bridge/README.md -m "pup-bridge ${VER} (insiders): ${LOG}" >/dev/null 2>&1 ) || {
[ "${FORCE_PUSH:-0}" = "1" ] || { echo " INSIDERS PUSH FAILED (set FORCE_PUSH=1 to retry)"; exit 1; }
}
echo "== 5. push the source text files to the product page (source is open; public manifest untouched) =="
# 2.0.595 (pup-bridge#117, Kyle): the Rust tree the exe is built from ships with it, every release, so a
# port (host-mac) is written against code that compiles. Everything under rust/ except target/, plus the
# two build tools. 62 files, ~1.5 MB. Cargo.lock included so a checkout builds the same dependency set.
# 2.0.598 (pup-bridge#118, Kyle): plus every file the Rust build EMBEDS from outside rust/ (the extensions,
# icons, dashboard pages). Derived from the include_bytes!/include_str! sites, so a new embed ships by itself.
# ALL of them, rust/ included: the rust/ find below keeps only source-like extensions, and a clean checkout
# still failed on rust/verbs/assets/import-scan.ps1 (measured from a fresh pull, 2026-09-16).
EMBED_FILES=$(python3 tools/list-embedded.py | tr '\n' ' ') || { echo " an embedded file is missing on disk (tools/list-embedded.py)"; exit 1; }
RUST_FILES=$(find rust -path rust/target -prune -o -type f \( -name '*.rs' -o -name 'Cargo.toml' -o -name 'Cargo.lock' -o -name '*.ttf' -o -name '*.png' -o -name '*.svg' -o -name '*.json' -o -name '*.js' -o -name '*.html' -o -name '*.md' \) -print | tr '\n' ' ')
adom-wiki repo push "$PAGE" \
--files src/bridge.json src/BRIDGE_VERSION src/*.js src/extension/*.js src/extension/*.json SKILL.md package.json \
skills/*/SKILL.md dev-skills/*/SKILL.md publish-skills/*/SKILL.md \
$RUST_FILES $EMBED_FILES tools/rust-build.sh tools/check-borders.sh tools/gen-assets.py tools/list-embedded.py tools/sync-nbe.sh \
-m "v${VER} (insiders): ${LOG}" >/dev/null 2>&1 || {
[ "${FORCE_PUSH:-0}" = "1" ] || { echo " PUSH FAILED (set FORCE_PUSH=1 to retry)"; exit 1; }
}
echo "== 7. DIFF-VERIFY =="
ON_DISK=$(cat src/BRIDGE_VERSION)
MAN=$(python3 -c "import json,sys;print(json.load(open(sys.argv[1]))['version'])" "$INSIDERS_DIR/$INSIDERS_REL")
BJ=$(python3 -c "import json;print(json.load(open('src/bridge.json'))['version'])")
PUB=$(python3 -c "import json;print(json.load(open('adom-bridge-puppeteer-manifest.json'))['version'])")
[ "$ON_DISK" = "$VER" ] && [ "$MAN" = "$VER" ] && [ "$BJ" = "$VER" ] \
&& echo "VERIFIED $VER on INSIDERS (BRIDGE_VERSION + bridge.json + adom/insiders pup-bridge/version.json in lockstep); PUBLIC manifest still at $PUB" \
|| { echo "LOCKSTEP BROKEN: disk=$ON_DISK bridge.json=$BJ insiders=$MAN"; exit 1; }
echo " To roll $VER out to everyone, on the human's words: tools/promote.sh $VER \"<their exact words>\" --by \"<name>\""