OverviewFilesCommitsIssuesDocsReleasesPeers

pear-dep-audit / snapshot-keet-versions.sh

151 lines · bash ref:
#!/usr/bin/env bash
# snapshot-keet-versions.sh — extract {pkg → version} from a Keet AppImage.
#
# Reads every node_modules/*/package.json (including @scope/pkg) and writes
# the resulting map to a JSON file. Use this as the canonical source-of-truth
# for "the versions the messaging stack actually runs on" — then point
# dep-audit.cjs at it to find drift in our projects.
#
# Usage:
#   ./snapshot-keet-versions.sh [/path/to/Keet.AppImage] [output.json]
#
# Defaults:
#   AppImage:   ~/Downloads/Keet.AppImage
#   Output:     snapshots/keet-<appVersion>.json  (relative to this script's parent)

set -euo pipefail

APPIMAGE="${1:-${HOME}/Downloads/Keet.AppImage}"
OUTPUT="${2:-}"

if [ ! -x "$APPIMAGE" ]; then
  echo "error: AppImage not found or not executable: $APPIMAGE" >&2
  echo "       pass the path as the first argument, or place Keet.AppImage in ~/Downloads/" >&2
  exit 1
fi

# Always extract fresh to a tmp dir. Hand-extracted /tmp/squashfs-root dirs
# from prior debugging sessions can be partial, which would silently produce
# an incomplete snapshot.
TMPDIR_MADE=$(mktemp -d -t keet-snapshot-XXXXXX)
trap "rm -rf '$TMPDIR_MADE'" EXIT
cd "$TMPDIR_MADE"
echo "extracting node_modules package.json files from AppImage..." >&2
"$APPIMAGE" --appimage-extract 'resources/app/node_modules/*/package.json' >/dev/null
"$APPIMAGE" --appimage-extract 'resources/app/node_modules/@*/*/package.json' >/dev/null 2>&1 || true
"$APPIMAGE" --appimage-extract 'resources/app/package.json' >/dev/null 2>&1 || true
SQRT="$TMPDIR_MADE/squashfs-root"

NM="$SQRT/resources/app/node_modules"
APP_PKG="$SQRT/resources/app/package.json"

# Default output path uses the discovered app version (so multiple AppImages
# don't overwrite each other).
APP_VERSION=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('version','unknown'))" "$APP_PKG" 2>/dev/null || echo "unknown")
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -z "$OUTPUT" ]; then
  OUTPUT="$SCRIPT_DIR/snapshots/keet-${APP_VERSION}.json"
fi
mkdir -p "$(dirname "$OUTPUT")"

python3 - "$NM" "$APP_PKG" "$APPIMAGE" "$OUTPUT" <<'PY'
import json, os, sys, glob, datetime, struct
nm, app_pkg, appimage, output = sys.argv[1:5]
out = {
  "source": appimage,
  "capturedAt": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
  "packages": {}
}
upgrade = None
try:
  app = json.load(open(app_pkg))
  out["appName"] = app.get("name")
  out["appVersion"] = app.get("version")
  upgrade = app.get("upgrade")
  out["upgrade"] = upgrade
except Exception as e:
  out["appName"] = None
  out["appVersion"] = None

pjs = sorted(set(glob.glob(os.path.join(nm, "*/package.json")) + glob.glob(os.path.join(nm, "@*/*/package.json"))))
for pj in pjs:
  try:
    d = json.load(open(pj))
    name = d.get("name")
    ver = d.get("version")
    if name and ver:
      out["packages"][name] = ver
  except Exception:
    continue

# pear-runtime cache check: if app's `upgrade` field is a pear:// URL, derive
# its discoveryKey and look for the cache at ~/.config/pear/by-dkey/<dkey>/0/.
# This tells us whether pear-runtime has fetched an OTA update for this app.
def z32_decode(s, alphabet='ybndrfg8ejkmcpqxot1uwisza345h769'):
  bits = ''.join(format(alphabet.index(c), '05b') for c in s)
  n = (len(bits) // 8) * 8
  return bytes(int(bits[i:i+8], 2) for i in range(0, n, 8))

pear_cache = None
if upgrade and upgrade.startswith('pear://'):
  try:
    key_z32 = upgrade.split('://', 1)[1].split('/', 1)[0].split('?', 1)[0]
    key_bytes = z32_decode(key_z32)
    out["upgradeKeyHex"] = key_bytes.hex()
    # Scan every ~/.config/pear/by-dkey/<dkey>/0/checkout — the first 32 bytes
    # of each checkout file are the drive's public key. If any match, that's
    # the cache for this app's pear-runtime updates.
    root = os.path.expanduser('~/.config/pear/by-dkey')
    if os.path.isdir(root):
      for d in os.listdir(root):
        ck = os.path.join(root, d, '0', 'checkout')
        if not os.path.exists(ck): continue
        with open(ck, 'rb') as f: cb = f.read()
        if len(cb) < 32: continue
        if cb[:32].hex() != key_bytes.hex(): continue
        cache_dir = os.path.join(root, d)
        pear_cache = {"path": cache_dir, "discoveryKey": d}
        # checkout file: <key:32><length:varint><fork:varint><platform:str><arch:str>
        def cdec(buf, off):
          b = buf[off]
          if b < 0xfd: return b, off+1
          if b == 0xfd: return struct.unpack_from('<H', buf, off+1)[0], off+3
          if b == 0xfe: return struct.unpack_from('<I', buf, off+1)[0], off+5
          return struct.unpack_from('<Q', buf, off+1)[0], off+9
        try:
          p = 32
          length, p = cdec(cb, p)
          fork, p = cdec(cb, p)
          pear_cache["driveLength"] = length
          pear_cache["driveFork"] = fork
        except Exception:
          pass
        # Staged app tree (set by applyUpdate before fsx.swap)
        staged_app = os.path.join(cache_dir, '0', 'by-arch', 'linux-x64', 'app')
        pear_cache["stagedAppPresent"] = os.path.isdir(staged_app)
        pear_cache["bootBundlePresent"] = os.path.exists(os.path.join(cache_dir, '0', 'boot.bundle'))
        break
  except Exception as e:
    out["upgradeError"] = str(e)
out["pearRuntimeCache"] = pear_cache

with open(output, "w") as f:
  json.dump(out, f, indent=2, sort_keys=True)

print(f"wrote {output}", file=sys.stderr)
print(f"  app:                  {out.get('appName')}@{out.get('appVersion')}", file=sys.stderr)
print(f"  packages from AppImage: {len(out['packages'])}", file=sys.stderr)
if pear_cache:
  print(f"  pear-runtime cache:    {pear_cache['path']}", file=sys.stderr)
  if 'driveLength' in pear_cache:
    print(f"    fetched drive length: {pear_cache['driveLength']} (fork {pear_cache['driveFork']})", file=sys.stderr)
  print(f"    boot.bundle present:  {pear_cache['bootBundlePresent']}", file=sys.stderr)
  print(f"    staged app present:   {pear_cache['stagedAppPresent']}", file=sys.stderr)
  if not pear_cache['stagedAppPresent']:
    print(f"    → no swap pending; app runs from AppImage versions verbatim", file=sys.stderr)
  else:
    print(f"    → WARNING: staged app tree exists; pending swap may install newer modules", file=sys.stderr)
else:
  print(f"  pear-runtime cache:    not found (no OTA fetch attempted)", file=sys.stderr)
PY