| … | … | @@ -0,0 +1,5 @@ |
| 1 | snapshots/keet-unknown.json | |
| 2 | snapshots/keet-pear-runtime-live.json | |
| 3 | node_modules/ | |
| 4 | *.swp | |
| 5 | .DS_Store |
| … | … | @@ -0,0 +1,142 @@ |
| 1 | # pear-dep-audit | |
| 2 | ||
| 3 | Four small scripts that find and fix npm dependency drift across our Pear / | |
| 4 | Holepunch projects, anchored against the version manifest a current Keet | |
| 5 | AppImage ships. | |
| 6 | ||
| 7 | Built 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 | |
| 9 | bundled `3.4.1` — the older version lacks the Bare `events` import map that | |
| 10 | 3.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 | |
| 28 | node dep-audit.cjs /path/to/project | |
| 29 | node dep-audit.cjs /path/to/project --fix # prints overrides block | |
| 30 | ||
| 31 | # 3. Find cross-app conflicts in the pgh ecosystem | |
| 32 | node ecosystem-audit.cjs | |
| 33 | node ecosystem-audit.cjs --snapshot=snapshots/keet-<v>.json # also flag snapshot mismatches | |
| 34 | ||
| 35 | # 4. (rare) Extract from a pear-runtime-fetched bundle | |
| 36 | node 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 | ||
| 41 | Keet ships as an Electron + Bare-sidecar hybrid: | |
| 42 | ||
| 43 | ``` | |
| 44 | AppImage (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 | ||
| 52 | pear-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 | ||
| 58 | Key facts (verified on tabertron): | |
| 59 | ||
| 60 | 1. **The AppImage's `node_modules/*/package.json` IS the build-time manifest** — the canonical version pin set Keet was webpack-bundled from. | |
| 61 | 2. **`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. | |
| 62 | 3. **`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. | |
| 63 | 4. **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 | ||
| 85 | When 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 | ||
| 108 | Manifest 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')` — | |
| 118 | Hyperdrive uses a derived bee core with manifest-aware hashing. | |
| 119 | ||
| 120 | Instead 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 | ||
| 142 | Internal tooling, no formal license. |
| … | … | @@ -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 | ||
| 19 | const fs = require('fs') | |
| 20 | const path = require('path') | |
| 21 | ||
| 22 | function 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 | ||
| 28 | const args = process.argv.slice(2) | |
| 29 | let projArg = null | |
| 30 | let snapshotArg = null | |
| 31 | let fix = false | |
| 32 | let showAll = false | |
| 33 | for (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 | } | |
| 42 | if (!projArg) usage('missing <project-dir>') | |
| 43 | ||
| 44 | const proj = path.resolve(projArg) | |
| 45 | const nm = path.join(proj, 'node_modules') | |
| 46 | if (!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) | |
| 49 | let snapshotPath = snapshotArg | |
| 50 | if (!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 | } | |
| 60 | const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8')) | |
| 61 | const expected = snapshot.packages || {} | |
| 62 | ||
| 63 | // Walk node_modules — yields the directory of every installed package. | |
| 64 | function * 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 | ||
| 78 | function 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 | ||
| 90 | const drift = [] | |
| 91 | const onlyHere = [] | |
| 92 | const matched = [] | |
| 93 | ||
| 94 | for (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 | ||
| 113 | drift.sort((a, b) => a.name.localeCompare(b.name)) | |
| 114 | onlyHere.sort((a, b) => a.name.localeCompare(b.name)) | |
| 115 | ||
| 116 | const counts = { ahead: 0, behind: 0 } | |
| 117 | for (const d of drift) counts[d.direction]++ | |
| 118 | ||
| 119 | const colName = Math.max(12, ...drift.map(d => d.name.length)) | |
| 120 | const colInst = Math.max(10, ...drift.map(d => d.installed.length)) | |
| 121 | const colWant = Math.max(10, ...drift.map(d => d.wanted.length)) | |
| 122 | ||
| 123 | console.log('# dep-audit: ' + proj) | |
| 124 | console.log(' snapshot: ' + path.relative(process.cwd(), snapshotPath)) | |
| 125 | console.log(' reference: ' + (snapshot.appName || '?') + '@' + (snapshot.appVersion || '?')) | |
| 126 | console.log(' matched: ' + matched.length) | |
| 127 | console.log(' drift: ' + drift.length + ' (ahead=' + counts.ahead + ' behind=' + counts.behind + ')') | |
| 128 | console.log(' only here: ' + onlyHere.length) | |
| 129 | console.log() | |
| 130 | ||
| 131 | if (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 | ||
| 141 | if (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 | ||
| 147 | if (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 | ||
| 162 | process.exit(drift.length > 0 ? 1 : 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 | ||
| 22 | const fs = require('fs') | |
| 23 | const path = require('path') | |
| 24 | const os = require('os') | |
| 25 | ||
| 26 | function 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 | ||
| 32 | let indexPath = path.join(os.homedir(), '.config', 'pgh', 'published-index.json') | |
| 33 | let filterRe = null | |
| 34 | let onlyApps = null | |
| 35 | let snapshotPath = null | |
| 36 | let verbose = false | |
| 37 | for (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 | ||
| 47 | if (!fs.existsSync(indexPath)) usage('no published-index at ' + indexPath) | |
| 48 | const data = JSON.parse(fs.readFileSync(indexPath, 'utf8')) | |
| 49 | const apps = data.apps || {} | |
| 50 | ||
| 51 | const snapshot = snapshotPath ? JSON.parse(fs.readFileSync(snapshotPath, 'utf8')) : null | |
| 52 | const snapshotPkgs = snapshot ? (snapshot.packages || {}) : null | |
| 53 | ||
| 54 | // Use the LATEST published version of each app as its current ecosystem footprint. | |
| 55 | const latest = {} | |
| 56 | for (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...] } | |
| 64 | const matrix = {} | |
| 65 | for (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) | |
| 78 | const conflicts = [] | |
| 79 | const snapshotMismatches = [] | |
| 80 | const matches = [] | |
| 81 | for (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 | } | |
| 94 | conflicts.sort((a, b) => a.pkg.localeCompare(b.pkg)) | |
| 95 | snapshotMismatches.sort((a, b) => a.pkg.localeCompare(b.pkg)) | |
| 96 | ||
| 97 | console.log('# ecosystem-audit: ' + indexPath) | |
| 98 | console.log(' apps: ' + Object.keys(latest).length + ' (' + Object.keys(latest).join(', ') + ')') | |
| 99 | console.log(' unique packages: ' + Object.keys(matrix).length) | |
| 100 | console.log(' conflicting packages: ' + conflicts.length) | |
| 101 | if (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 | } | |
| 105 | console.log() | |
| 106 | ||
| 107 | if (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 | ||
| 123 | if (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 | ||
| 132 | if (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 | ||
| 139 | process.exit(conflicts.length > 0 ? 1 : 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 | ||
| 16 | set -euo pipefail | |
| 17 | ||
| 18 | APPIMAGE="${1:-${HOME}/Downloads/Keet.AppImage}" | |
| 19 | OUTPUT="${2:-}" | |
| 20 | ||
| 21 | if [ ! -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 | |
| 25 | fi | |
| 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. | |
| 30 | TMPDIR_MADE=$(mktemp -d -t keet-snapshot-XXXXXX) | |
| 31 | trap "rm -rf '$TMPDIR_MADE'" EXIT | |
| 32 | cd "$TMPDIR_MADE" | |
| 33 | echo "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 | |
| 37 | SQRT="$TMPDIR_MADE/squashfs-root" | |
| 38 | ||
| 39 | NM="$SQRT/resources/app/node_modules" | |
| 40 | APP_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). | |
| 44 | APP_VERSION=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('version','unknown'))" "$APP_PKG" 2>/dev/null || echo "unknown") | |
| 45 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| 46 | if [ -z "$OUTPUT" ]; then | |
| 47 | OUTPUT="$SCRIPT_DIR/snapshots/keet-${APP_VERSION}.json" | |
| 48 | fi | |
| 49 | mkdir -p "$(dirname "$OUTPUT")" | |
| 50 | ||
| 51 | python3 - "$NM" "$APP_PKG" "$APPIMAGE" "$OUTPUT" <<'PY' | |
| 52 | import json, os, sys, glob, datetime, struct | |
| 53 | nm, app_pkg, appimage, output = sys.argv[1:5] | |
| 54 | out = { | |
| 55 | "source": appimage, | |
| 56 | "capturedAt": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), | |
| 57 | "packages": {} | |
| 58 | } | |
| 59 | upgrade = None | |
| 60 | try: | |
| 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 | |
| 66 | except Exception as e: | |
| 67 | out["appName"] = None | |
| 68 | out["appVersion"] = None | |
| 69 | ||
| 70 | pjs = sorted(set(glob.glob(os.path.join(nm, "*/package.json")) + glob.glob(os.path.join(nm, "@*/*/package.json")))) | |
| 71 | for 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. | |
| 84 | def 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 | ||
| 89 | pear_cache = None | |
| 90 | if 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) | |
| 130 | out["pearRuntimeCache"] = pear_cache | |
| 131 | ||
| 132 | with open(output, "w") as f: | |
| 133 | json.dump(out, f, indent=2, sort_keys=True) | |
| 134 | ||
| 135 | print(f"wrote {output}", file=sys.stderr) | |
| 136 | print(f" app: {out.get('appName')}@{out.get('appVersion')}", file=sys.stderr) | |
| 137 | print(f" packages from AppImage: {len(out['packages'])}", file=sys.stderr) | |
| 138 | if 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) | |
| 148 | else: | |
| 149 | print(f" pear-runtime cache: not found (no OTA fetch attempted)", file=sys.stderr) | |
| 150 | PY |
| … | … | @@ -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 | ||
| 25 | const fs = require('fs') | |
| 26 | const path = require('path') | |
| 27 | ||
| 28 | function 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 | ||
| 34 | const bundlePath = process.argv[2] | |
| 35 | const outputArg = process.argv[3] || null | |
| 36 | if (!bundlePath) usage('missing <boot.bundle>') | |
| 37 | if (!fs.existsSync(bundlePath)) usage('not found: ' + bundlePath) | |
| 38 | ||
| 39 | const buf = fs.readFileSync(bundlePath) | |
| 40 | ||
| 41 | // Parse header line: <decimal>\n | |
| 42 | const nlIdx = buf.indexOf(0x0a) // '\n' | |
| 43 | if (nlIdx < 0 || nlIdx > 16) usage('no header line found — not a bare-bundle file?') | |
| 44 | const headerNum = parseInt(buf.subarray(0, nlIdx).toString('utf8'), 10) | |
| 45 | if (!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. | |
| 51 | const manifestStart = nlIdx + 1 | |
| 52 | let pos = manifestStart | |
| 53 | let depth = 0 | |
| 54 | let inString = false | |
| 55 | let prevEscape = false | |
| 56 | let started = false | |
| 57 | let jsonEnd = -1 | |
| 58 | while (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 | } | |
| 74 | if (jsonEnd < 0) usage('did not find balanced JSON manifest') | |
| 75 | const jsonText = buf.subarray(manifestStart, jsonEnd).toString('utf8') | |
| 76 | let manifest | |
| 77 | try { | |
| 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. | |
| 84 | let tailStart = jsonEnd | |
| 85 | while (tailStart < buf.length && (buf[tailStart] === 0x0a || buf[tailStart] === 0x20)) tailStart++ | |
| 86 | ||
| 87 | const files = manifest.files || {} | |
| 88 | const packages = {} | |
| 89 | const skipped = [] | |
| 90 | ||
| 91 | for (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 | ||
| 110 | const 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 | ||
| 120 | const json = JSON.stringify(out, null, 2) | |
| 121 | if (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 | } |
| … | … | @@ -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 | } |