OverviewFilesCommitsIssuesDocsReleasesPeers

pear-dep-audit commits / 5081815

pear-dep-audit: snapshot Keet AppImage + audit project dep drift
Four scripts to surface dep-version drift across our Pear / Holepunch projects, anchored against the manifest a current Keet AppImage ships. snapshot-keet-versions.sh AppImage → snapshots/keet-<v>.json snapshot-pear-bundle.cjs boot.bundle (bare-bundle on-disk) → snapshot dep-audit.cjs project node_modules vs snapshot (--fix → overrides) ecosystem-audit.cjs ~/.config/pgh/published-index.json cross-app conflicts Built after the hyper-tps fresh-storage boot hang traced to caret-resolved @hyperswarm/rpc 3.2.0 (no Bare events import map) where Keet AppImage 4.14.1 shipped 3.4.1. README.md captures the Keet update topology and pin discipline notes; .claude/skills/dep-audit/ holds the load-on-demand reference. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
5081815d44062ed9b037610655746bec5931e92c
by plankatron <fleeky@gmail.com> · 2026-05-13T16:41:42.000Z

Changed files (7) +1127 -0

.gitignore +5 -0
……@@ -0,0 +1,5 @@
1snapshots/keet-unknown.json
2snapshots/keet-pear-runtime-live.json
3node_modules/
4*.swp
5.DS_Store
README.md +142 -0
……@@ -0,0 +1,142 @@
1# pear-dep-audit
2
3Four small scripts that find and fix npm dependency drift across our Pear /
4Holepunch projects, anchored against the version manifest a current Keet
5AppImage ships.
6
7Built 2026-05-13 after the hyper-tps fresh-storage boot hang traced to
8`@hyperswarm/rpc 3.2.0` silently caret-resolving in place of Keet AppImage's
9bundled `3.4.1` — the older version lacks the Bare `events` import map that
103.4.1 added, causing every onlineBoot to die with "Identity is required".
11
12## Scripts
13
14| File | Purpose |
15|------|---------|
16| `snapshot-keet-versions.sh` | AppImage → `snapshots/keet-X.Y.Z.json`. Also scans `~/.config/pear/by-dkey/` to detect whether pear-runtime has fetched an OTA update. |
17| `snapshot-pear-bundle.cjs` | `boot.bundle` (bare-bundle on-disk format) → snapshot JSON. Use when pear-runtime has fetched a newer drive than the AppImage. |
18| `dep-audit.cjs` | Project `node_modules` vs snapshot drift report. `--fix` prints a ready-to-paste `overrides` block. |
19| `ecosystem-audit.cjs` | `~/.config/pgh/published-index.json` cross-app conflicts. Surfaces the same "N dep conflicts blocking X" data that `pgh publish` would emit, centrally. |
20
21## Quick start
22
23```bash
24# 1. Capture canonical versions from the current Keet AppImage
25./snapshot-keet-versions.sh # writes snapshots/keet-<v>.json
26
27# 2. Find drift in any project
28node dep-audit.cjs /path/to/project
29node dep-audit.cjs /path/to/project --fix # prints overrides block
30
31# 3. Find cross-app conflicts in the pgh ecosystem
32node ecosystem-audit.cjs
33node ecosystem-audit.cjs --snapshot=snapshots/keet-<v>.json # also flag snapshot mismatches
34
35# 4. (rare) Extract from a pear-runtime-fetched bundle
36node snapshot-pear-bundle.cjs ~/.config/pear/by-dkey/<dkey>/0/boot.bundle live.json
37```
38
39## Why an AppImage snapshot is canonical (when no OTA swap has happened)
40
41Keet ships as an Electron + Bare-sidecar hybrid:
42
43```
44AppImage (squashfs, read-only mount)
45└── resources/app/
46 ├── .webpack/main/index.js ← Electron main process runs this
47 ├── .webpack/main/workers/core/ ← Bare sub-process runs this
48 ├── node_modules/<pkg>/package.json ← BUILD-TIME version manifest
49 └── package.json
50 └── upgrade: pear://<key> ← pear-runtime drive key
51
52pear-runtime cache (~/.config/pear/by-dkey/<dkey>/0/)
53├── checkout binary: <key:32><length:varint><fork:varint><platform><arch>
54├── boot.bundle bare-bundle: pear-runtime bootstrap loader (NOT app modules)
55└── by-arch/linux-x64/ fetched platform files; app/<name>/ only after applyUpdate()
56```
57
58Key facts (verified on tabertron):
59
601. **The AppImage's `node_modules/*/package.json` IS the build-time manifest** — the canonical version pin set Keet was webpack-bundled from.
612. **`pear-runtime-updater` fetches updates into the by-dkey cache**, then `applyUpdate()` does `fsx.swap(staged, this.app)` — atomic directory swap. The squashfs mount is read-only, so `this.app` must be a writable copy Keet maintains.
623. **`boot.bundle` only contains the bootstrap layer** (bare-runtime + hyperdrive + corestore + hyperswarm). The actual app modules (`@holepunchto/*`, `@hyperswarm/rpc`, etc.) are inside the hyperdrive corestore — not directly readable as a file tree.
634. **Typical install never applies a swap** — `pearRuntimeCache.stagedAppPresent: false`. Keet runs verbatim from the AppImage's `.webpack/main/`. The snapshot script flags this automatically.
64
65## Snapshot JSON shape
66
67```json
68{
69 "source": "/home/fleeky/Downloads/Keet.AppImage",
70 "capturedAt": "2026-05-13T15:30:00Z",
71 "appName": "keet",
72 "appVersion": "4.14.2",
73 "upgrade": "pear://17pwkcszz18de...",
74 "upgradeKeyHex": "975b4532d7bc...",
75 "pearRuntimeCache": null,
76 "packages": {
77 "@hyperswarm/rpc": "3.4.1",
78 "autobase": "7.27.3",
79 "hypercore": "11.28.1",
80 "...": "..."
81 }
82}
83```
84
85When pear-runtime has fetched the drive, `pearRuntimeCache` becomes:
86
87```json
88{
89 "path": "/home/fleeky/.config/pear/by-dkey/<dkey>",
90 "discoveryKey": "<dkey>",
91 "driveLength": 3243,
92 "driveFork": 0,
93 "stagedAppPresent": false,
94 "bootBundlePresent": true
95}
96```
97
98`stagedAppPresent: true` is the warning sign — pear-runtime is about to swap newer modules in; the AppImage snapshot is no longer canonical.
99
100## boot.bundle format (bare-bundle)
101
102```
103<decimal-length>\n # length of the JSON manifest region
104<json-manifest>\n # parse with a string-aware brace counter
105<binary-tail> # concatenated file bytes, indexed by manifest.files[path].offset
106```
107
108Manifest shape: `files: { "<path>": { offset, length, mode } }`. The extractor walks every `*/package.json` entry, slices its bytes from the tail, parses `{name, version}`. See `snapshot-pear-bundle.cjs`.
109
110## Pin discipline notes
111
112- When `dep-audit --fix` prints an `overrides` block, **direct deps in `dependencies` that conflict with an override must be pinned to the exact override version** — npm refuses caret-conflicts with `EOVERRIDE`.
113- **The pgh ecosystem may have its own internal version split** independent of Keet. As of 2026-05-13, `pgh`/`pgh-sidecar`/`pear-git-v3`/`hyper-overlay` are uniformly ~13 packages ahead of Keet 4.14.2 (mostly `bare-*` and `b4a`). Aligning strictly to Keet creates pgh-publish conflicts. Run `ecosystem-audit` before mass-bumping a project.
114
115## How `snapshot-keet-versions.sh` finds the right pear-runtime cache
116
117`pear://` URL → discoveryKey is **not** a simple `blake2b(key, 'hypercore')` —
118Hyperdrive uses a derived bee core with manifest-aware hashing.
119
120Instead of computing the discoveryKey, the script **walks every `~/.config/pear/by-dkey/<d>/0/checkout`** file, reads the first 32 bytes (the drive's public key), and matches against the app's z32-decoded upgrade key. Bullet-proof and version-agnostic.
121
122## Drift inventory (2026-05-13, 12 repos vs Keet 4.14.2)
123
124| Repo | matched | drift | direction |
125|-----------------------|--------:|------:|------------------|
126| keet-cli | 249 | 87 | all behind (worst) |
127| hyper-wallet | 102 | 48 | mostly behind |
128| pear-git | 103 | 49 | mostly behind |
129| hypernote | 114 | 41 | mostly behind |
130| hyper-tps | 115 | 35 | split |
131| godot-keet | 84 | 32 | mostly behind |
132| hyper-deploy | 98 | 9 | mostly behind |
133| pgh | 115 | 13 | all ahead |
134| pgh-sidecar | 104 | 13 | all ahead |
135| pear-git-v3 | 115 | 13 | all ahead |
136| hyper-overlay | 108 | 13 | all ahead |
137| hyper-sync | 76 | 12 | all ahead |
138| git-remote-punch | 93 | 3 | ahead |
139
140## License
141
142Internal tooling, no formal license.
dep-audit.cjs +162 -0
……@@ -0,0 +1,162 @@
1#!/usr/bin/env node
2// dep-audit.cjs — diff a project's installed deps against a Keet AppImage
3// snapshot (produced by snapshot-keet-versions.sh).
4//
5// Usage:
6// node dep-audit.cjs <project-dir> [--snapshot=path] [--fix] [--all]
7//
8// Flags:
9// --snapshot=PATH path to a snapshot JSON
10// (default: <pear-dep-audit>/snapshots/keet-<latest>.json)
11// --fix also print a ready-to-paste "overrides" block
12// --all include only-installed-here packages (default: only drift)
13//
14// Examples:
15// node dep-audit.cjs ../../godot/hyper-tps
16// node dep-audit.cjs ../packages/keet-daemon --fix
17// node dep-audit.cjs <project> --snapshot=snapshots/keet-4.14.2.json
18
19const fs = require('fs')
20const path = require('path')
21
22function usage (msg) {
23 if (msg) process.stderr.write('error: ' + msg + '\n')
24 process.stderr.write('usage: dep-audit.cjs <project-dir> [--snapshot=PATH] [--fix] [--all]\n')
25 process.exit(msg ? 1 : 0)
26}
27
28const args = process.argv.slice(2)
29let projArg = null
30let snapshotArg = null
31let fix = false
32let showAll = false
33for (const a of args) {
34 if (a === '--fix') fix = true
35 else if (a === '--all') showAll = true
36 else if (a === '--help' || a === '-h') usage()
37 else if (a.startsWith('--snapshot=')) snapshotArg = a.slice('--snapshot='.length)
38 else if (a.startsWith('--')) usage('unknown flag: ' + a)
39 else if (!projArg) projArg = a
40 else usage('extra positional: ' + a)
41}
42if (!projArg) usage('missing <project-dir>')
43
44const proj = path.resolve(projArg)
45const nm = path.join(proj, 'node_modules')
46if (!fs.existsSync(nm)) usage('no node_modules in ' + proj + ' (run `npm install` first)')
47
48// Resolve snapshot path: explicit flag, else <script-dir>/snapshots/keet-*.json (latest by mtime)
49let snapshotPath = snapshotArg
50if (!snapshotPath) {
51 const snapDir = path.resolve(__dirname, 'snapshots')
52 if (!fs.existsSync(snapDir)) usage('no default snapshots dir at ' + snapDir + '; pass --snapshot=PATH')
53 const candidates = fs.readdirSync(snapDir)
54 .filter(n => n.startsWith('keet-') && n.endsWith('.json') && n !== 'keet-unknown.json')
55 .map(n => ({ name: n, mtime: fs.statSync(path.join(snapDir, n)).mtimeMs }))
56 .sort((a, b) => b.mtime - a.mtime)
57 if (!candidates.length) usage('no snapshot files found in ' + snapDir)
58 snapshotPath = path.join(snapDir, candidates[0].name)
59}
60const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8'))
61const expected = snapshot.packages || {}
62
63// Walk node_modules — yields the directory of every installed package.
64function * walkNm (dir) {
65 for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
66 if (e.name.startsWith('.')) continue
67 const p = path.join(dir, e.name)
68 if (e.name.startsWith('@') && e.isDirectory()) {
69 for (const sub of fs.readdirSync(p, { withFileTypes: true })) {
70 if (sub.isDirectory()) yield path.join(p, sub.name)
71 }
72 } else if (e.isDirectory()) {
73 yield p
74 }
75 }
76}
77
78function cmpVersion (a, b) {
79 const pa = String(a).split(/[.-]/).map(s => /^\d+$/.test(s) ? Number(s) : s)
80 const pb = String(b).split(/[.-]/).map(s => /^\d+$/.test(s) ? Number(s) : s)
81 for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
82 const va = pa[i] === undefined ? 0 : pa[i]
83 const vb = pb[i] === undefined ? 0 : pb[i]
84 if (va < vb) return -1
85 if (va > vb) return 1
86 }
87 return 0
88}
89
90const drift = []
91const onlyHere = []
92const matched = []
93
94for (const pkgDir of walkNm(nm)) {
95 const pjPath = path.join(pkgDir, 'package.json')
96 if (!fs.existsSync(pjPath)) continue
97 let pj
98 try { pj = JSON.parse(fs.readFileSync(pjPath, 'utf8')) } catch { continue }
99 const name = pj.name
100 const installed = pj.version
101 if (!name || !installed) continue
102 const wanted = expected[name]
103 if (wanted === undefined) {
104 onlyHere.push({ name, installed })
105 } else if (wanted !== installed) {
106 const cmp = cmpVersion(installed, wanted)
107 drift.push({ name, installed, wanted, direction: cmp === 0 ? 'equal' : cmp > 0 ? 'ahead' : 'behind' })
108 } else {
109 matched.push(name)
110 }
111}
112
113drift.sort((a, b) => a.name.localeCompare(b.name))
114onlyHere.sort((a, b) => a.name.localeCompare(b.name))
115
116const counts = { ahead: 0, behind: 0 }
117for (const d of drift) counts[d.direction]++
118
119const colName = Math.max(12, ...drift.map(d => d.name.length))
120const colInst = Math.max(10, ...drift.map(d => d.installed.length))
121const colWant = Math.max(10, ...drift.map(d => d.wanted.length))
122
123console.log('# dep-audit: ' + proj)
124console.log(' snapshot: ' + path.relative(process.cwd(), snapshotPath))
125console.log(' reference: ' + (snapshot.appName || '?') + '@' + (snapshot.appVersion || '?'))
126console.log(' matched: ' + matched.length)
127console.log(' drift: ' + drift.length + ' (ahead=' + counts.ahead + ' behind=' + counts.behind + ')')
128console.log(' only here: ' + onlyHere.length)
129console.log()
130
131if (drift.length === 0) {
132 console.log(' ✓ no drift against snapshot')
133} else {
134 console.log(' ' + 'package'.padEnd(colName) + ' ' + 'installed'.padEnd(colInst) + ' ' + 'snapshot'.padEnd(colWant) + ' direction')
135 console.log(' ' + '-'.repeat(colName + colInst + colWant + 14))
136 for (const d of drift) {
137 console.log(' ' + d.name.padEnd(colName) + ' ' + d.installed.padEnd(colInst) + ' ' + d.wanted.padEnd(colWant) + ' ' + d.direction)
138 }
139}
140
141if (showAll && onlyHere.length) {
142 console.log()
143 console.log('## only installed here (not in snapshot)')
144 for (const o of onlyHere) console.log(' ' + o.name + ' ' + o.installed)
145}
146
147if (fix && drift.length) {
148 console.log()
149 console.log('## suggested overrides block (paste into package.json):')
150 console.log('"overrides": {')
151 for (let i = 0; i < drift.length; i++) {
152 const d = drift[i]
153 const comma = i === drift.length - 1 ? '' : ','
154 console.log(' "' + d.name + '": "' + d.wanted + '"' + comma)
155 }
156 console.log('}')
157 console.log()
158 console.log('# note: caret-style direct deps in "dependencies" must also be tightened')
159 console.log('# to exact versions if any conflict with an override (npm EOVERRIDE).')
160}
161
162process.exit(drift.length > 0 ? 1 : 0)
ecosystem-audit.cjs +139 -0
……@@ -0,0 +1,139 @@
1#!/usr/bin/env node
2// ecosystem-audit.cjs — find packages where 2+ pgh apps' latest releases
3// disagree on version. Reads ~/.config/pgh/published-index.json.
4//
5// This is the EXACT data pgh publish enumerates per-project as "dep
6// conflicts"; centralising it lets you align the ecosystem once instead
7// of resolving each app's publish friction separately.
8//
9// Usage:
10// node ecosystem-audit.cjs [--index=PATH] [--filter=REGEX] [--apps=a,b,c]
11// node ecosystem-audit.cjs --snapshot=../snapshots/keet-4.14.2.json
12// ^ also flags pkgs that disagree with a reference snapshot
13//
14// Flags:
15// --index=PATH path to published-index.json
16// (default: ~/.config/pgh/published-index.json)
17// --filter=RE only show packages matching this regex
18// --apps=a,b,c only audit these apps (comma-sep; default: all)
19// --snapshot=PATH cross-check ecosystem versions against a snapshot
20// --verbose include matching packages too
21
22const fs = require('fs')
23const path = require('path')
24const os = require('os')
25
26function usage (msg) {
27 if (msg) process.stderr.write('error: ' + msg + '\n')
28 process.stderr.write('usage: ecosystem-audit.cjs [--index=PATH] [--filter=RE] [--apps=a,b,c] [--snapshot=PATH] [--verbose]\n')
29 process.exit(msg ? 1 : 0)
30}
31
32let indexPath = path.join(os.homedir(), '.config', 'pgh', 'published-index.json')
33let filterRe = null
34let onlyApps = null
35let snapshotPath = null
36let verbose = false
37for (const a of process.argv.slice(2)) {
38 if (a === '--help' || a === '-h') usage()
39 else if (a === '--verbose') verbose = true
40 else if (a.startsWith('--index=')) indexPath = a.slice(8)
41 else if (a.startsWith('--filter=')) filterRe = new RegExp(a.slice(9))
42 else if (a.startsWith('--apps=')) onlyApps = new Set(a.slice(7).split(',').filter(Boolean))
43 else if (a.startsWith('--snapshot=')) snapshotPath = a.slice(11)
44 else usage('unknown flag: ' + a)
45}
46
47if (!fs.existsSync(indexPath)) usage('no published-index at ' + indexPath)
48const data = JSON.parse(fs.readFileSync(indexPath, 'utf8'))
49const apps = data.apps || {}
50
51const snapshot = snapshotPath ? JSON.parse(fs.readFileSync(snapshotPath, 'utf8')) : null
52const snapshotPkgs = snapshot ? (snapshot.packages || {}) : null
53
54// Use the LATEST published version of each app as its current ecosystem footprint.
55const latest = {}
56for (const [name, app] of Object.entries(apps)) {
57 if (onlyApps && !onlyApps.has(name)) continue
58 const versions = app.versions || []
59 if (!versions.length) continue
60 latest[name] = versions[versions.length - 1]
61}
62
63// Build pkg → { version → [appNames...] }
64const matrix = {}
65for (const [appName, rel] of Object.entries(latest)) {
66 const deps = (rel.lockfile && rel.lockfile.deps) || []
67 for (const dep of deps) {
68 if (!dep.name || !dep.version) continue
69 if (filterRe && !filterRe.test(dep.name)) continue
70 if (!matrix[dep.name]) matrix[dep.name] = {}
71 if (!matrix[dep.name][dep.version]) matrix[dep.name][dep.version] = []
72 matrix[dep.name][dep.version].push(appName)
73 }
74}
75
76// Partition into conflicts (>1 version) + agreements (1 version, optionally
77// vs snapshot)
78const conflicts = []
79const snapshotMismatches = []
80const matches = []
81for (const [pkg, vers] of Object.entries(matrix)) {
82 const verKeys = Object.keys(vers)
83 if (verKeys.length > 1) {
84 conflicts.push({ pkg, vers })
85 } else {
86 const v = verKeys[0]
87 if (snapshotPkgs && snapshotPkgs[pkg] && snapshotPkgs[pkg] !== v) {
88 snapshotMismatches.push({ pkg, ecoVersion: v, snapshotVersion: snapshotPkgs[pkg], apps: vers[v] })
89 } else {
90 matches.push({ pkg, v, apps: vers[v] })
91 }
92 }
93}
94conflicts.sort((a, b) => a.pkg.localeCompare(b.pkg))
95snapshotMismatches.sort((a, b) => a.pkg.localeCompare(b.pkg))
96
97console.log('# ecosystem-audit: ' + indexPath)
98console.log(' apps: ' + Object.keys(latest).length + ' (' + Object.keys(latest).join(', ') + ')')
99console.log(' unique packages: ' + Object.keys(matrix).length)
100console.log(' conflicting packages: ' + conflicts.length)
101if (snapshotPath) {
102 console.log(' vs snapshot: ' + path.relative(process.cwd(), snapshotPath))
103 console.log(' snapshot mismatches: ' + snapshotMismatches.length + ' (eco agrees internally but differs from snapshot)')
104}
105console.log()
106
107if (conflicts.length === 0) {
108 console.log(' ✓ no internal conflicts')
109} else {
110 console.log('## conflicts (pkg disagreed across apps)')
111 console.log()
112 for (const c of conflicts) {
113 console.log(c.pkg + (snapshotPkgs && snapshotPkgs[c.pkg] ? ' (snapshot: ' + snapshotPkgs[c.pkg] + ')' : ''))
114 const rows = Object.entries(c.vers).sort()
115 for (const [v, appList] of rows) {
116 const mark = snapshotPkgs && snapshotPkgs[c.pkg] === v ? ' ←snapshot' : ''
117 console.log(' ' + v.padEnd(12) + ' ' + appList.join(', ') + mark)
118 }
119 console.log()
120 }
121}
122
123if (snapshotPath && snapshotMismatches.length) {
124 console.log('## ecosystem agrees but drifts from snapshot')
125 console.log()
126 for (const m of snapshotMismatches) {
127 console.log(' ' + m.pkg.padEnd(40) + ' eco=' + m.ecoVersion.padEnd(12) + ' snapshot=' + m.snapshotVersion + ' (' + m.apps.join(', ') + ')')
128 }
129 console.log()
130}
131
132if (verbose) {
133 console.log('## matching (' + matches.length + ' packages aligned)')
134 for (const m of matches.sort((a, b) => a.pkg.localeCompare(b.pkg))) {
135 console.log(' ' + m.pkg.padEnd(40) + ' ' + m.v + ' (' + m.apps.join(', ') + ')')
136 }
137}
138
139process.exit(conflicts.length > 0 ? 1 : 0)
snapshot-keet-versions.sh +150 -0
……@@ -0,0 +1,150 @@
1#!/usr/bin/env bash
2# snapshot-keet-versions.sh — extract {pkg → version} from a Keet AppImage.
3#
4# Reads every node_modules/*/package.json (including @scope/pkg) and writes
5# the resulting map to a JSON file. Use this as the canonical source-of-truth
6# for "the versions the messaging stack actually runs on" — then point
7# dep-audit.cjs at it to find drift in our projects.
8#
9# Usage:
10# ./snapshot-keet-versions.sh [/path/to/Keet.AppImage] [output.json]
11#
12# Defaults:
13# AppImage: ~/Downloads/Keet.AppImage
14# Output: snapshots/keet-<appVersion>.json (relative to this script's parent)
15
16set -euo pipefail
17
18APPIMAGE="${1:-${HOME}/Downloads/Keet.AppImage}"
19OUTPUT="${2:-}"
20
21if [ ! -x "$APPIMAGE" ]; then
22 echo "error: AppImage not found or not executable: $APPIMAGE" >&2
23 echo " pass the path as the first argument, or place Keet.AppImage in ~/Downloads/" >&2
24 exit 1
25fi
26
27# Always extract fresh to a tmp dir. Hand-extracted /tmp/squashfs-root dirs
28# from prior debugging sessions can be partial, which would silently produce
29# an incomplete snapshot.
30TMPDIR_MADE=$(mktemp -d -t keet-snapshot-XXXXXX)
31trap "rm -rf '$TMPDIR_MADE'" EXIT
32cd "$TMPDIR_MADE"
33echo "extracting node_modules package.json files from AppImage..." >&2
34"$APPIMAGE" --appimage-extract 'resources/app/node_modules/*/package.json' >/dev/null
35"$APPIMAGE" --appimage-extract 'resources/app/node_modules/@*/*/package.json' >/dev/null 2>&1 || true
36"$APPIMAGE" --appimage-extract 'resources/app/package.json' >/dev/null 2>&1 || true
37SQRT="$TMPDIR_MADE/squashfs-root"
38
39NM="$SQRT/resources/app/node_modules"
40APP_PKG="$SQRT/resources/app/package.json"
41
42# Default output path uses the discovered app version (so multiple AppImages
43# don't overwrite each other).
44APP_VERSION=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('version','unknown'))" "$APP_PKG" 2>/dev/null || echo "unknown")
45SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
46if [ -z "$OUTPUT" ]; then
47 OUTPUT="$SCRIPT_DIR/snapshots/keet-${APP_VERSION}.json"
48fi
49mkdir -p "$(dirname "$OUTPUT")"
50
51python3 - "$NM" "$APP_PKG" "$APPIMAGE" "$OUTPUT" <<'PY'
52import json, os, sys, glob, datetime, struct
53nm, app_pkg, appimage, output = sys.argv[1:5]
54out = {
55 "source": appimage,
56 "capturedAt": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
57 "packages": {}
58}
59upgrade = None
60try:
61 app = json.load(open(app_pkg))
62 out["appName"] = app.get("name")
63 out["appVersion"] = app.get("version")
64 upgrade = app.get("upgrade")
65 out["upgrade"] = upgrade
66except Exception as e:
67 out["appName"] = None
68 out["appVersion"] = None
69
70pjs = sorted(set(glob.glob(os.path.join(nm, "*/package.json")) + glob.glob(os.path.join(nm, "@*/*/package.json"))))
71for pj in pjs:
72 try:
73 d = json.load(open(pj))
74 name = d.get("name")
75 ver = d.get("version")
76 if name and ver:
77 out["packages"][name] = ver
78 except Exception:
79 continue
80
81# pear-runtime cache check: if app's `upgrade` field is a pear:// URL, derive
82# its discoveryKey and look for the cache at ~/.config/pear/by-dkey/<dkey>/0/.
83# This tells us whether pear-runtime has fetched an OTA update for this app.
84def z32_decode(s, alphabet='ybndrfg8ejkmcpqxot1uwisza345h769'):
85 bits = ''.join(format(alphabet.index(c), '05b') for c in s)
86 n = (len(bits) // 8) * 8
87 return bytes(int(bits[i:i+8], 2) for i in range(0, n, 8))
88
89pear_cache = None
90if upgrade and upgrade.startswith('pear://'):
91 try:
92 key_z32 = upgrade.split('://', 1)[1].split('/', 1)[0].split('?', 1)[0]
93 key_bytes = z32_decode(key_z32)
94 out["upgradeKeyHex"] = key_bytes.hex()
95 # Scan every ~/.config/pear/by-dkey/<dkey>/0/checkout — the first 32 bytes
96 # of each checkout file are the drive's public key. If any match, that's
97 # the cache for this app's pear-runtime updates.
98 root = os.path.expanduser('~/.config/pear/by-dkey')
99 if os.path.isdir(root):
100 for d in os.listdir(root):
101 ck = os.path.join(root, d, '0', 'checkout')
102 if not os.path.exists(ck): continue
103 with open(ck, 'rb') as f: cb = f.read()
104 if len(cb) < 32: continue
105 if cb[:32].hex() != key_bytes.hex(): continue
106 cache_dir = os.path.join(root, d)
107 pear_cache = {"path": cache_dir, "discoveryKey": d}
108 # checkout file: <key:32><length:varint><fork:varint><platform:str><arch:str>
109 def cdec(buf, off):
110 b = buf[off]
111 if b < 0xfd: return b, off+1
112 if b == 0xfd: return struct.unpack_from('<H', buf, off+1)[0], off+3
113 if b == 0xfe: return struct.unpack_from('<I', buf, off+1)[0], off+5
114 return struct.unpack_from('<Q', buf, off+1)[0], off+9
115 try:
116 p = 32
117 length, p = cdec(cb, p)
118 fork, p = cdec(cb, p)
119 pear_cache["driveLength"] = length
120 pear_cache["driveFork"] = fork
121 except Exception:
122 pass
123 # Staged app tree (set by applyUpdate before fsx.swap)
124 staged_app = os.path.join(cache_dir, '0', 'by-arch', 'linux-x64', 'app')
125 pear_cache["stagedAppPresent"] = os.path.isdir(staged_app)
126 pear_cache["bootBundlePresent"] = os.path.exists(os.path.join(cache_dir, '0', 'boot.bundle'))
127 break
128 except Exception as e:
129 out["upgradeError"] = str(e)
130out["pearRuntimeCache"] = pear_cache
131
132with open(output, "w") as f:
133 json.dump(out, f, indent=2, sort_keys=True)
134
135print(f"wrote {output}", file=sys.stderr)
136print(f" app: {out.get('appName')}@{out.get('appVersion')}", file=sys.stderr)
137print(f" packages from AppImage: {len(out['packages'])}", file=sys.stderr)
138if pear_cache:
139 print(f" pear-runtime cache: {pear_cache['path']}", file=sys.stderr)
140 if 'driveLength' in pear_cache:
141 print(f" fetched drive length: {pear_cache['driveLength']} (fork {pear_cache['driveFork']})", file=sys.stderr)
142 print(f" boot.bundle present: {pear_cache['bootBundlePresent']}", file=sys.stderr)
143 print(f" staged app present: {pear_cache['stagedAppPresent']}", file=sys.stderr)
144 if not pear_cache['stagedAppPresent']:
145 print(f" → no swap pending; app runs from AppImage versions verbatim", file=sys.stderr)
146 else:
147 print(f" → WARNING: staged app tree exists; pending swap may install newer modules", file=sys.stderr)
148else:
149 print(f" pear-runtime cache: not found (no OTA fetch attempted)", file=sys.stderr)
150PY
snapshot-pear-bundle.cjs +131 -0
……@@ -0,0 +1,131 @@
1#!/usr/bin/env node
2// snapshot-pear-bundle.cjs — extract a {pkg → version} snapshot from a
3// pear-runtime cached boot.bundle (bare-bundle on-disk format).
4//
5// pear-runtime fetches the app drive into ~/.config/pear/by-dkey/<dkey>/0/
6// where boot.bundle is a single bare-bundle file containing every module's
7// source. This script parses it and dumps the package.json versions, so the
8// "what Keet is actually running over OTA" question gets a concrete answer
9// alongside the "what the AppImage was built with" snapshot.
10//
11// Bundle format (verified empirically against Keet 4.14.2's drive):
12// <decimal-length>\n
13// <json-manifest>\n
14// <binary-tail of concatenated file bytes>
15//
16// Manifest fields used:
17// files: { "<path>": { offset, length, mode } }
18//
19// Usage:
20// node snapshot-pear-bundle.cjs <path-to-boot.bundle> [output.json]
21// node snapshot-pear-bundle.cjs ~/.config/pear/by-dkey/<dkey>/0/boot.bundle
22//
23// Companion: tools/snapshot-keet-versions.sh (AppImage) — same output shape
24
25const fs = require('fs')
26const path = require('path')
27
28function usage (msg) {
29 if (msg) process.stderr.write('error: ' + msg + '\n')
30 process.stderr.write('usage: snapshot-pear-bundle.cjs <boot.bundle> [output.json]\n')
31 process.exit(msg ? 1 : 0)
32}
33
34const bundlePath = process.argv[2]
35const outputArg = process.argv[3] || null
36if (!bundlePath) usage('missing <boot.bundle>')
37if (!fs.existsSync(bundlePath)) usage('not found: ' + bundlePath)
38
39const buf = fs.readFileSync(bundlePath)
40
41// Parse header line: <decimal>\n
42const nlIdx = buf.indexOf(0x0a) // '\n'
43if (nlIdx < 0 || nlIdx > 16) usage('no header line found — not a bare-bundle file?')
44const headerNum = parseInt(buf.subarray(0, nlIdx).toString('utf8'), 10)
45if (!Number.isFinite(headerNum)) usage('header decimal not parseable')
46
47// Manifest starts at nlIdx + 1. JSON is followed by a separator newline
48// before the binary tail. Walk a brace counter through the manifest to
49// find the exact JSON boundary — string-aware so a `}` inside a string
50// value doesn't false-match.
51const manifestStart = nlIdx + 1
52let pos = manifestStart
53let depth = 0
54let inString = false
55let prevEscape = false
56let started = false
57let jsonEnd = -1
58while (pos < buf.length) {
59 const b = buf[pos]
60 if (inString) {
61 if (prevEscape) { prevEscape = false }
62 else if (b === 0x5c /* \ */) prevEscape = true
63 else if (b === 0x22 /* " */) inString = false
64 } else {
65 if (b === 0x22 /* " */) inString = true
66 else if (b === 0x7b /* { */) { depth++; started = true }
67 else if (b === 0x7d /* } */) {
68 depth--
69 if (started && depth === 0) { jsonEnd = pos + 1; break }
70 }
71 }
72 pos++
73}
74if (jsonEnd < 0) usage('did not find balanced JSON manifest')
75const jsonText = buf.subarray(manifestStart, jsonEnd).toString('utf8')
76let manifest
77try {
78 manifest = JSON.parse(jsonText)
79} catch (e) {
80 usage('manifest JSON parse failed: ' + e.message)
81}
82
83// Binary tail starts after the JSON + a single trailing newline separator.
84let tailStart = jsonEnd
85while (tailStart < buf.length && (buf[tailStart] === 0x0a || buf[tailStart] === 0x20)) tailStart++
86
87const files = manifest.files || {}
88const packages = {}
89const skipped = []
90
91for (const [filePath, entry] of Object.entries(files)) {
92 if (!filePath.endsWith('/package.json')) continue
93 if (!entry || typeof entry.offset !== 'number' || typeof entry.length !== 'number') continue
94 const off = tailStart + entry.offset
95 const blob = buf.subarray(off, off + entry.length)
96 let pj
97 try {
98 pj = JSON.parse(blob.toString('utf8'))
99 } catch (e) {
100 skipped.push({ path: filePath, reason: 'json parse: ' + e.message })
101 continue
102 }
103 if (!pj.name || !pj.version) continue
104 // Prefer first-seen for the canonical (top-level) entry; nested duplicates
105 // can show up for the same package at different depths — we want the leaf
106 // closest to the entry.
107 if (!packages[pj.name]) packages[pj.name] = pj.version
108}
109
110const out = {
111 source: path.resolve(bundlePath),
112 capturedAt: new Date().toISOString(),
113 bundleVersion: manifest.version === undefined ? null : manifest.version,
114 bundleMain: manifest.main || null,
115 fileCount: Object.keys(files).length,
116 packageJsonCount: Object.keys(files).filter(k => k.endsWith('/package.json')).length,
117 packages
118}
119
120const json = JSON.stringify(out, null, 2)
121if (outputArg) {
122 fs.mkdirSync(path.dirname(path.resolve(outputArg)), { recursive: true })
123 fs.writeFileSync(outputArg, json)
124 process.stderr.write('wrote ' + outputArg + '\n')
125 process.stderr.write(' files in bundle: ' + out.fileCount + '\n')
126 process.stderr.write(' package.json files: ' + out.packageJsonCount + '\n')
127 process.stderr.write(' distinct packages: ' + Object.keys(packages).length + '\n')
128 if (skipped.length) process.stderr.write(' skipped (parse fails): ' + skipped.length + '\n')
129} else {
130 process.stdout.write(json + '\n')
131}
snapshots/keet-4.14.2.json +398 -0
……@@ -0,0 +1,398 @@
1{
2 "appName": "keet",
3 "appVersion": "4.14.2",
4 "capturedAt": "2026-05-13T16:05:32Z",
5 "packages": {
6 "@babel/runtime": "7.29.2",
7 "@floating-ui/core": "1.7.5",
8 "@floating-ui/dom": "1.7.6",
9 "@floating-ui/react": "0.27.19",
10 "@floating-ui/react-dom": "2.1.8",
11 "@floating-ui/utils": "0.2.11",
12 "@holepunchto/broadcast-call": "2.0.0",
13 "@holepunchto/emojis": "3.2.0",
14 "@holepunchto/hyperdiscovery": "1.1.1",
15 "@holepunchto/hyperindex": "2.3.5",
16 "@holepunchto/keet-call": "10.3.3",
17 "@holepunchto/keet-core": "4.14.5",
18 "@holepunchto/keet-core-api": "2.14.2",
19 "@holepunchto/keet-core-schemas": "2.17.0",
20 "@holepunchto/keet-debug-helper": "2.0.3",
21 "@holepunchto/keet-default-config": "3.1.0",
22 "@holepunchto/keet-editor": "3.17.2",
23 "@holepunchto/keet-editor-parser": "4.0.2",
24 "@holepunchto/keet-indexing": "6.3.2",
25 "@holepunchto/keet-link-preview": "1.2.5",
26 "@holepunchto/keet-media": "2.4.1",
27 "@holepunchto/keet-package": "3.0.3",
28 "@holepunchto/keet-push": "4.2.0",
29 "@holepunchto/keet-registry": "3.7.0",
30 "@holepunchto/keet-registry-client": "5.1.1",
31 "@holepunchto/keet-registry-schema": "3.4.0",
32 "@holepunchto/keet-schemas": "1.24.14",
33 "@holepunchto/keet-store": "6.14.30",
34 "@holepunchto/keet-translations": "2.5.0",
35 "@holepunchto/keet-unread-anchors": "2.0.0",
36 "@holepunchto/legacy-keet-core-schemas": "2.17.0",
37 "@hyperswarm/rpc": "3.4.1",
38 "@hyperswarm/secret-stream": "6.9.1",
39 "@jridgewell/gen-mapping": "0.3.13",
40 "@jridgewell/resolve-uri": "3.1.2",
41 "@jridgewell/source-map": "0.3.11",
42 "@jridgewell/sourcemap-codec": "1.5.5",
43 "@jridgewell/trace-mapping": "0.3.31",
44 "@popperjs/core": "2.11.8",
45 "@redux-saga/core": "1.4.2",
46 "@redux-saga/deferred": "1.3.1",
47 "@redux-saga/delay-p": "1.3.1",
48 "@redux-saga/is": "1.2.1",
49 "@redux-saga/symbols": "1.2.1",
50 "@redux-saga/types": "1.3.1",
51 "@reduxjs/toolkit": "2.10.1",
52 "@remirror/core-constants": "3.0.0",
53 "@sentry/core": "9.47.1",
54 "@sentry/utils": "8.55.1",
55 "@standard-schema/spec": "1.1.0",
56 "@standard-schema/utils": "0.3.0",
57 "@tiptap/core": "2.27.2",
58 "@tiptap/extension-blockquote": "2.27.2",
59 "@tiptap/extension-bold": "2.27.2",
60 "@tiptap/extension-bubble-menu": "2.27.2",
61 "@tiptap/extension-bullet-list": "2.27.2",
62 "@tiptap/extension-code": "2.27.2",
63 "@tiptap/extension-code-block": "2.27.2",
64 "@tiptap/extension-document": "2.27.2",
65 "@tiptap/extension-dropcursor": "2.27.2",
66 "@tiptap/extension-floating-menu": "2.27.2",
67 "@tiptap/extension-gapcursor": "2.27.2",
68 "@tiptap/extension-hard-break": "2.27.2",
69 "@tiptap/extension-history": "2.27.2",
70 "@tiptap/extension-italic": "2.27.2",
71 "@tiptap/extension-link": "2.27.2",
72 "@tiptap/extension-list-item": "2.27.2",
73 "@tiptap/extension-mention": "2.27.2",
74 "@tiptap/extension-ordered-list": "2.27.2",
75 "@tiptap/extension-paragraph": "2.27.2",
76 "@tiptap/extension-placeholder": "2.27.2",
77 "@tiptap/extension-strike": "2.27.2",
78 "@tiptap/extension-text": "2.27.2",
79 "@tiptap/pm": "2.27.2",
80 "@tiptap/react": "2.27.2",
81 "@tiptap/suggestion": "2.27.2",
82 "@twind/content": "0.1.0",
83 "@twind/forms": "0.1.4",
84 "@types/linkify-it": "5.0.0",
85 "@types/markdown-it": "14.1.2",
86 "@types/mdurl": "2.0.0",
87 "@types/react": "19.2.14",
88 "@types/use-sync-external-store": "0.0.6",
89 "acorn": "8.16.0",
90 "adaptive-timeout": "1.0.1",
91 "argparse": "2.0.1",
92 "autobase": "7.27.3",
93 "b4a": "1.8.0",
94 "bare-abort": "2.0.13",
95 "bare-addon-resolve": "1.10.0",
96 "bare-ansi-escapes": "2.2.3",
97 "bare-assert": "1.2.0",
98 "bare-bmp": "1.0.0",
99 "bare-buffer": "3.6.0",
100 "bare-bundle": "1.10.0",
101 "bare-bundle-id": "1.0.2",
102 "bare-console": "6.1.0",
103 "bare-crypto": "1.13.4",
104 "bare-dns": "2.1.4",
105 "bare-env": "3.0.0",
106 "bare-events": "2.8.2",
107 "bare-exif": "1.0.1",
108 "bare-fetch": "2.8.2",
109 "bare-ffmpeg": "1.2.2",
110 "bare-file-logger": "1.2.1",
111 "bare-form-data": "1.2.1",
112 "bare-format": "1.0.2",
113 "bare-fs": "4.7.0",
114 "bare-gif": "1.1.3",
115 "bare-heif": "1.0.10",
116 "bare-hrtime": "2.1.1",
117 "bare-http-parser": "1.1.3",
118 "bare-http1": "4.5.6",
119 "bare-https": "2.1.3",
120 "bare-ico": "1.0.0",
121 "bare-image-resample": "1.0.2",
122 "bare-inspect": "3.1.4",
123 "bare-inspector": "4.0.3",
124 "bare-jpeg": "1.0.4",
125 "bare-logger": "2.0.3",
126 "bare-media": "2.6.0",
127 "bare-module": "6.1.3",
128 "bare-module-lexer": "1.4.7",
129 "bare-module-resolve": "1.12.1",
130 "bare-module-traverse": "2.0.1",
131 "bare-net": "2.3.1",
132 "bare-os": "3.8.7",
133 "bare-pack": "2.0.1",
134 "bare-path": "3.0.0",
135 "bare-performance": "1.3.0",
136 "bare-pipe": "4.1.5",
137 "bare-png": "1.0.5",
138 "bare-process": "4.4.1",
139 "bare-rpc": "1.2.0",
140 "bare-semver": "1.0.3",
141 "bare-sidecar": "0.3.0",
142 "bare-signals": "4.2.0",
143 "bare-stdio": "1.0.2",
144 "bare-stream": "2.13.0",
145 "bare-subprocess": "5.2.3",
146 "bare-svg": "1.0.1",
147 "bare-system-logger": "1.0.3",
148 "bare-tcp": "2.2.7",
149 "bare-tiff": "1.0.2",
150 "bare-tls": "2.2.3",
151 "bare-tty": "5.1.0",
152 "bare-type": "1.1.0",
153 "bare-url": "2.4.0",
154 "bare-webp": "1.3.0",
155 "bare-ws": "2.1.0",
156 "bare-zlib": "1.3.3",
157 "big-sparse-array": "1.0.3",
158 "binary-stream-equals": "1.0.0",
159 "bip39-mnemonic": "2.5.0",
160 "bits-to-bytes": "1.3.0",
161 "blake2b": "2.1.4",
162 "blake2b-wasm": "2.4.0",
163 "blind-pairing-core": "2.10.0",
164 "blind-peer-muxer": "1.1.1",
165 "blind-peering": "2.0.1",
166 "blind-relay": "1.4.0",
167 "bogon": "1.2.0",
168 "bucket-rate-limit": "1.0.0",
169 "buffer-from": "1.1.2",
170 "bundlebee": "1.2.0",
171 "canvas-resize-image": "1.0.1",
172 "chacha20-universal": "1.0.4",
173 "cmake-fetch": "1.4.10",
174 "codecs": "3.1.0",
175 "compact-encoding": "2.19.2",
176 "compact-encoding-bitfield": "1.0.0",
177 "compact-encoding-net": "1.2.0",
178 "copy-text-to-clipboard": "3.2.0",
179 "core-coupler": "2.0.0",
180 "corestore": "7.9.2",
181 "crc-native": "1.1.8",
182 "crc-universal": "1.0.4",
183 "crelt": "1.0.6",
184 "cross-worker": "2.1.0",
185 "csstype": "3.2.3",
186 "dayjs": "1.11.20",
187 "db-lock": "1.2.0",
188 "debounceify": "1.1.0",
189 "deterministic-sealed-box": "1.0.2",
190 "device-file": "2.3.1",
191 "dht-rpc": "6.26.4",
192 "dom-serializer": "1.4.1",
193 "domelementtype": "2.3.0",
194 "domhandler": "4.3.1",
195 "domutils": "2.8.0",
196 "encryption-encoding": "1.0.3",
197 "escape-string-regexp": "4.0.0",
198 "event-loop-delay": "1.1.2",
199 "events-universal": "1.0.1",
200 "fast-deep-equal": "3.1.3",
201 "fast-fifo": "1.3.2",
202 "fast-meta-tags": "1.3.3",
203 "fd-lock": "2.1.1",
204 "fifofile": "2.0.3",
205 "flat-tree": "1.13.0",
206 "framed-stream": "1.0.1",
207 "fs-native-extensions": "1.5.0",
208 "fs-native-lock": "1.2.0",
209 "generate-object-property": "2.0.0",
210 "generate-string": "1.0.1",
211 "get-file-format": "1.1.1",
212 "get-mime-type": "2.0.2",
213 "graceful-goodbye": "1.3.3",
214 "hotkeys-js": "3.13.15",
215 "hrpc": "4.3.0",
216 "htmlparser2": "6.1.0",
217 "hyperbee": "2.27.3",
218 "hyperbee2": "2.9.3",
219 "hyperblobs": "2.11.1",
220 "hyperconf": "1.0.3",
221 "hypercore": "11.28.1",
222 "hypercore-blob-server": "1.12.0",
223 "hypercore-byte-stream": "2.0.2",
224 "hypercore-crypto": "3.6.1",
225 "hypercore-errors": "1.5.0",
226 "hypercore-id-encoding": "1.3.0",
227 "hypercore-proof-queue": "2.0.2",
228 "hypercore-storage": "2.8.0",
229 "hyperdb": "5.0.1",
230 "hyperdht": "6.30.0",
231 "hyperdht-address": "1.0.1",
232 "hyperdispatch": "1.5.1",
233 "hyperdrive": "13.3.2",
234 "hyperschema": "1.20.1",
235 "hyperswarm": "4.17.0",
236 "hyperswarm-capability": "1.1.1",
237 "hypertrace": "1.4.2",
238 "immer": "10.2.0",
239 "index-encoder": "3.3.1",
240 "is-options": "1.0.2",
241 "is-property": "1.0.2",
242 "jsqr": "1.4.0",
243 "kademlia-routing-table": "1.0.6",
244 "keet-identity-key": "3.1.0",
245 "linkify-it": "5.0.0",
246 "linkifyjs": "4.3.2",
247 "listen-async": "1.0.0",
248 "localdrive": "2.2.1",
249 "lodash.debounce": "4.0.8",
250 "lodash.isequal": "4.5.0",
251 "lodash.throttle": "4.1.1",
252 "lodash.toarray": "4.4.0",
253 "markdown-it": "14.1.1",
254 "mdurl": "2.0.0",
255 "mirror-drive": "1.14.1",
256 "ms": "2.1.3",
257 "msix-manager": "0.2.5",
258 "multi-profile-store": "1.0.3",
259 "mutexify": "1.4.0",
260 "nanoassert": "2.0.0",
261 "nat-sampler": "1.0.1",
262 "noise-curve-ed": "2.1.0",
263 "noise-handshake": "4.2.0",
264 "orderedmap": "2.1.1",
265 "paparam": "1.10.1",
266 "pear-aliases": "1.0.7",
267 "pear-errors": "1.0.1",
268 "pear-hyperdb": "1.1.0",
269 "pear-link": "4.2.1",
270 "pear-runtime": "0.5.0",
271 "pear-runtime-legacy-storage": "1.1.0",
272 "pear-runtime-updater": "2.0.2",
273 "pear-user-dirs": "1.0.2",
274 "prettier-bytes": "1.0.4",
275 "process-top": "1.7.1",
276 "promaphore": "1.0.0",
277 "prosemirror-changeset": "2.4.0",
278 "prosemirror-collab": "1.3.1",
279 "prosemirror-commands": "1.7.1",
280 "prosemirror-dropcursor": "1.8.2",
281 "prosemirror-gapcursor": "1.4.1",
282 "prosemirror-history": "1.5.0",
283 "prosemirror-inputrules": "1.5.1",
284 "prosemirror-keymap": "1.2.3",
285 "prosemirror-markdown": "1.13.4",
286 "prosemirror-menu": "1.3.0",
287 "prosemirror-model": "1.25.4",
288 "prosemirror-schema-basic": "1.2.4",
289 "prosemirror-schema-list": "1.5.1",
290 "prosemirror-state": "1.4.4",
291 "prosemirror-tables": "1.8.5",
292 "prosemirror-trailing-node": "3.0.0",
293 "prosemirror-transform": "1.12.0",
294 "prosemirror-view": "1.41.8",
295 "protocol-buffers-encodings": "1.2.0",
296 "protomux": "3.10.1",
297 "protomux-rpc": "1.9.0",
298 "protomux-rpc-client": "2.3.0",
299 "protomux-rpc-client-pool": "2.2.0",
300 "protomux-wakeup": "2.9.0",
301 "punycode.js": "2.3.1",
302 "qrjs": "0.2.0",
303 "queue-tick": "1.0.1",
304 "quickbit-native": "2.4.8",
305 "quickbit-universal": "2.2.0",
306 "rabin-native": "2.0.0",
307 "rabin-stream": "2.0.0",
308 "rache": "1.0.0",
309 "random-array-iterator": "1.0.0",
310 "react": "19.2.5",
311 "react-dom": "19.2.5",
312 "react-redux": "9.2.0",
313 "read-write-mutexify": "2.1.0",
314 "ready-guard": "1.0.0",
315 "ready-resource": "1.2.0",
316 "record-cache": "1.2.0",
317 "redux": "5.0.1",
318 "redux-saga": "1.4.2",
319 "redux-thunk": "3.1.0",
320 "refcounter": "1.0.0",
321 "require-addon": "1.2.0",
322 "require-asset": "1.2.1",
323 "reselect": "5.1.1",
324 "resolve-reject-promise": "1.1.0",
325 "resource-on-exit": "1.0.0",
326 "rocksdb-blobs": "1.0.1",
327 "rocksdb-native": "3.15.0",
328 "rope-sequence": "1.3.4",
329 "safety-catch": "1.0.3",
330 "same-data": "1.0.0",
331 "scheduler": "0.27.0",
332 "scope-lock": "1.2.4",
333 "sdp-transform": "2.15.0",
334 "sentry-bare": "0.1.1",
335 "sha256-universal": "1.2.1",
336 "sha256-wasm": "2.2.2",
337 "sha512-universal": "1.2.1",
338 "sha512-wasm": "2.3.4",
339 "shuffled-priority-queue": "2.1.0",
340 "signal-promise": "1.0.3",
341 "signed-varint": "2.0.1",
342 "simdle-native": "1.3.9",
343 "simdle-universal": "1.1.2",
344 "siphash24": "1.3.1",
345 "sodium-hmac": "2.1.0",
346 "sodium-javascript": "0.8.0",
347 "sodium-native": "5.1.0",
348 "sodium-secretstream": "1.2.0",
349 "sodium-universal": "5.0.1",
350 "source-map": "0.6.1",
351 "source-map-support": "0.5.21",
352 "speedometer": "1.1.0",
353 "streamx": "2.25.0",
354 "style-vendorizer": "2.2.3",
355 "sub-encoder": "2.1.3",
356 "suspend-resource": "1.0.0",
357 "suspendify": "1.7.2",
358 "tabbable": "6.4.0",
359 "task-backoff": "1.0.1",
360 "teex": "1.0.1",
361 "terser": "5.46.1",
362 "text-decoder": "1.2.7",
363 "throwaway-local-cache": "1.3.0",
364 "time-ordered-set": "2.0.1",
365 "timeout-abort-signal": "1.0.0",
366 "timeout-refresh": "2.0.1",
367 "tiny-buffer-map": "1.1.1",
368 "tiny-buffer-rpc": "2.3.1",
369 "tiny-preferences": "1.1.1",
370 "tippy.js": "6.3.7",
371 "twind": "0.16.19",
372 "typescript": "5.9.3",
373 "typescript-compare": "0.0.2",
374 "typescript-logic": "0.0.0",
375 "typescript-tuple": "2.2.1",
376 "uc.micro": "2.1.0",
377 "udx-native": "1.19.2",
378 "uncaughts": "1.1.3",
379 "unicode-to-plain-text": "2.1.0",
380 "unix-path-resolve": "1.0.2",
381 "unordered-set": "2.0.1",
382 "unslab": "1.3.0",
383 "url-file-url": "1.0.5",
384 "use-sync-external-store": "1.6.0",
385 "varint": "5.0.0",
386 "w3c-keyname": "2.2.8",
387 "webcomponent-qr-code": "1.3.0",
388 "which-runtime": "1.3.2",
389 "xache": "1.2.1",
390 "xor-distance": "2.0.0",
391 "xsalsa20": "1.2.0",
392 "z32": "1.1.0"
393 },
394 "pearRuntimeCache": null,
395 "source": "/home/fleeky/Downloads/Keet.AppImage",
396 "upgrade": "pear://17pwkcszz18deaccarhrrixhzf1f5ko1b1dz6j3pxhexebutjwzy",
397 "upgradeKeyHex": "975b4532d7bc8e34618cc1384255fcb9645daa120c877f272d7f10f406714d2e"
398}