#!/usr/bin/env node
/**
* keet-audit.cjs — automate phases A + B + C-scaffold + E-raw-diff of a
* Keet ecosystem audit.
*
* Reads the currently-running Keet AppImage's `@holepunchto/*` packages
* (plus `keet-identity-key`) and produces a frozen, prettified mirror
* plus a structural scaffold:
*
* keet-audit-4.x/
* raw/ — verbatim copy of every package as shipped
* pretty/ — same tree, all .js prettified for reading
* MANIFEST.md — package → version + AppImage release
* STRUCTURE.md — per-package factual scaffold (exports,
* top-level requires, file inventory) ready
* to fill with narrative
* DIFF-RAW.md — file-level diff of overlapping packages
* vs prior keet-core-hyperdb 1.22.0 source
*
* What this script does NOT do: narrative analysis. The STRUCTURE.md
* skeleton is intentionally minimal — the audit value is in the prose
* an analyst writes on top. Same for DIFF-RAW.md: it lists structural
* changes; "what those changes mean" is the deep-dive step.
*
* Usage: node keet-audit.cjs [--src <appimage-mount>] [--out <dir>]
*/
'use strict'
const fs = require('fs')
const path = require('path')
const { spawnSync } = require('child_process')
const argv = process.argv.slice(2)
function arg (name, def) {
const i = argv.indexOf(name)
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def
}
const SRC_ROOT = arg('--src', '/tmp/.mount_Keet.AjtX4HI/resources/app/node_modules')
const OUT_ROOT = arg('--out', path.resolve(__dirname, '../../keet-audit-4.x'))
const PRIOR_DEOBFUSC = arg('--prior',
path.resolve(__dirname, '../deobfuscated'))
const PKG_SOURCES = []
// Every @holepunchto/* package
const hpDir = path.join(SRC_ROOT, '@holepunchto')
if (fs.existsSync(hpDir)) {
for (const p of fs.readdirSync(hpDir)) {
const full = path.join(hpDir, p)
if (fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, 'package.json'))) {
PKG_SOURCES.push({ name: '@holepunchto/' + p, srcDir: full })
}
}
}
// Plus keet-identity-key (not under @holepunchto but Keet-specific)
const idkDir = path.join(SRC_ROOT, 'keet-identity-key')
if (fs.existsSync(path.join(idkDir, 'package.json'))) {
PKG_SOURCES.push({ name: 'keet-identity-key', srcDir: idkDir })
}
if (PKG_SOURCES.length === 0) {
console.error('No source packages found at ' + SRC_ROOT)
console.error('Hint: is the Keet AppImage running so its FUSE mount is up?')
process.exit(1)
}
console.error('audit source: ' + SRC_ROOT)
console.error('audit output: ' + OUT_ROOT)
console.error('prior compare: ' + PRIOR_DEOBFUSC)
console.error('packages: ' + PKG_SOURCES.length)
const RAW = path.join(OUT_ROOT, 'raw')
const PRETTY = path.join(OUT_ROOT, 'pretty')
fs.mkdirSync(RAW, { recursive: true })
fs.mkdirSync(PRETTY, { recursive: true })
// ──────────────────────────────────────────────────────────────
// Phase A — freeze raw source
// ──────────────────────────────────────────────────────────────
console.error('\n[A] freeze raw source')
function cpRecursive (src, dst) {
// Use system cp -r; it handles permissions, symlinks, and large trees
// faster than walking with node's fs. The AppImage FUSE mount has
// weird permission bits we don't want to faithfully replicate, so we
// re-chmod readable on the destination at the end.
const r = spawnSync('cp', ['-rL', src + '/.', dst], { stdio: 'inherit' })
if (r.status !== 0) throw new Error('cp failed for ' + src)
}
for (const pkg of PKG_SOURCES) {
const dst = path.join(RAW, pkg.name)
fs.mkdirSync(dst, { recursive: true })
cpRecursive(pkg.srcDir, dst)
}
spawnSync('chmod', ['-R', 'u+rw,go+r', RAW])
console.error(' raw frozen at ' + RAW)
// Read AppImage release number (best-effort) for the manifest. We
// already know it from `pear info pear://keet` so just record what we
// can find on disk.
let appimageRelease = null
const peerVersion = path.join(SRC_ROOT, '..', 'package.json')
if (fs.existsSync(peerVersion)) {
try {
const pkg = JSON.parse(fs.readFileSync(peerVersion, 'utf8'))
if (pkg.version) appimageRelease = pkg.version
} catch {}
}
// ──────────────────────────────────────────────────────────────
// Phase A.2 — MANIFEST.md
// ──────────────────────────────────────────────────────────────
const manifestLines = []
manifestLines.push('# Keet 4.x audit — package manifest')
manifestLines.push('')
manifestLines.push('Captured: ' + new Date().toISOString())
manifestLines.push('Source: ' + SRC_ROOT)
if (appimageRelease) manifestLines.push('AppImage app version: ' + appimageRelease)
manifestLines.push('')
manifestLines.push('| Package | Version | Description |')
manifestLines.push('|---|---|---|')
const PKG_META = []
for (const pkg of PKG_SOURCES) {
let meta = { name: pkg.name, version: '?', description: '' }
try {
const pj = JSON.parse(fs.readFileSync(path.join(pkg.srcDir, 'package.json'), 'utf8'))
meta = { name: pj.name || pkg.name, version: pj.version || '?', description: pj.description || '' }
} catch {}
PKG_META.push({ ...pkg, meta })
manifestLines.push('| `' + meta.name + '` | ' + meta.version + ' | ' + escapeMd(meta.description) + ' |')
}
manifestLines.push('')
fs.writeFileSync(path.join(OUT_ROOT, 'MANIFEST.md'), manifestLines.join('\n') + '\n')
console.error(' wrote ' + path.join(OUT_ROOT, 'MANIFEST.md'))
// ──────────────────────────────────────────────────────────────
// Phase B — prettify
// ──────────────────────────────────────────────────────────────
console.error('\n[B] prettify')
cpRecursive(RAW, PRETTY)
const prettierBin = path.resolve(__dirname, '../node_modules/.bin/prettier')
const prettierCmd = fs.existsSync(prettierBin) ? prettierBin : 'npx'
const prettierArgs = fs.existsSync(prettierBin)
? ['--parser', 'babel', '--no-semi', '--print-width', '100', '--tab-width', '2', '--write']
: ['--yes', 'prettier', '--parser', 'babel', '--no-semi', '--print-width', '100', '--tab-width', '2', '--write']
// Find all .js files
function findJs (dir, acc = []) {
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, ent.name)
if (ent.isDirectory()) {
if (ent.name === 'node_modules' || ent.name.startsWith('.')) continue
findJs(full, acc)
} else if (ent.isFile() && ent.name.endsWith('.js') && !ent.name.endsWith('.min.js')) {
acc.push(full)
}
}
return acc
}
const jsFiles = findJs(PRETTY)
console.error(' prettifying ' + jsFiles.length + ' .js files in batches…')
const BATCH = 50
let prettified = 0
let prettierErrors = 0
for (let i = 0; i < jsFiles.length; i += BATCH) {
const batch = jsFiles.slice(i, i + BATCH)
const r = spawnSync(prettierCmd, [...prettierArgs, ...batch], {
stdio: ['ignore', 'ignore', 'pipe']
})
if (r.status !== 0) {
prettierErrors += batch.length
const err = r.stderr ? r.stderr.toString().slice(0, 200) : ''
console.error(' batch ' + i + ' failed: ' + err.split('\n')[0])
} else {
prettified += batch.length
}
if ((i / BATCH) % 4 === 0) process.stderr.write('.')
}
process.stderr.write('\n')
console.error(' prettified ' + prettified + ' / errors ' + prettierErrors + ' / total ' + jsFiles.length)
// ──────────────────────────────────────────────────────────────
// Phase C — STRUCTURE.md scaffold
// ──────────────────────────────────────────────────────────────
console.error('\n[C] STRUCTURE.md scaffold')
const structLines = []
structLines.push('# Keet 4.x audit — structure scaffold')
structLines.push('')
structLines.push('Auto-generated by `tools/keet-audit.cjs`. Each section below contains the')
structLines.push('FACTUAL data extracted from the package (version, exports, top-level imports,')
structLines.push('file inventory). The "## Notes" subsection is intentionally blank — fill it')
structLines.push('with narrative analysis as the deep dives happen.')
structLines.push('')
structLines.push('Sections are alphabetized by package name. Skim the manifest in MANIFEST.md')
structLines.push('first for a high-level inventory.')
structLines.push('')
for (const pkg of PKG_META.slice().sort((a, b) => a.meta.name.localeCompare(b.meta.name))) {
const meta = pkg.meta
const pkgPretty = path.join(PRETTY, pkg.name)
structLines.push('## `' + meta.name + '`')
structLines.push('')
structLines.push('- **Version**: ' + meta.version)
if (meta.description) structLines.push('- **Description**: ' + meta.description)
// package.json fields of interest
try {
const pj = JSON.parse(fs.readFileSync(path.join(pkgPretty, 'package.json'), 'utf8'))
if (pj.main) structLines.push('- **Main**: `' + pj.main + '`')
if (pj.exports) {
const exportsKeys = typeof pj.exports === 'object'
? Object.keys(pj.exports).slice(0, 8).map(k => '`' + k + '`').join(', ')
: '`' + pj.exports + '`'
structLines.push('- **Exports**: ' + exportsKeys)
}
if (pj.dependencies) {
const deps = Object.keys(pj.dependencies)
structLines.push('- **Dependencies** (' + deps.length + '): ' +
deps.slice(0, 12).map(d => '`' + d + '`').join(', ') +
(deps.length > 12 ? ', …' : ''))
}
} catch {}
// File inventory
let pkgFiles = []
try { pkgFiles = findJs(pkgPretty) } catch {}
structLines.push('- **JS files**: ' + pkgFiles.length)
// Top-level requires of the main entry
const mainPath = path.join(pkgPretty, 'index.js')
if (fs.existsSync(mainPath)) {
const src = fs.readFileSync(mainPath, 'utf8')
const reqs = [...src.matchAll(/require\(["']([^"']+)["']\)/g)].map(m => m[1])
const uniq = [...new Set(reqs)]
if (uniq.length) {
structLines.push('- **Top-level `require()` (index.js)**:')
structLines.push(' ```')
for (const r of uniq.slice(0, 25)) structLines.push(' ' + r)
if (uniq.length > 25) structLines.push(' … ' + (uniq.length - 25) + ' more')
structLines.push(' ```')
}
}
// Directory layout (1 level)
try {
const ents = fs.readdirSync(pkgPretty, { withFileTypes: true })
const dirs = ents.filter(e => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
.map(e => e.name).sort()
const files = ents.filter(e => e.isFile()).map(e => e.name).sort()
if (dirs.length || files.length) {
structLines.push('- **Top-level layout**:')
structLines.push(' ```')
for (const d of dirs) structLines.push(' ' + d + '/')
for (const f of files) structLines.push(' ' + f)
structLines.push(' ```')
}
} catch {}
structLines.push('')
structLines.push('### Notes')
structLines.push('')
structLines.push('_(fill in during deep dives)_')
structLines.push('')
structLines.push('---')
structLines.push('')
}
fs.writeFileSync(path.join(OUT_ROOT, 'STRUCTURE.md'), structLines.join('\n'))
console.error(' wrote ' + path.join(OUT_ROOT, 'STRUCTURE.md'))
// ──────────────────────────────────────────────────────────────
// Phase E — raw diff vs 1.22.0
// ──────────────────────────────────────────────────────────────
console.error('\n[E] raw diff vs prior')
const diffLines = []
diffLines.push('# Keet audit — diff vs keet-core-hyperdb 1.22.0')
diffLines.push('')
diffLines.push('Captured: ' + new Date().toISOString())
diffLines.push('Prior source: ' + PRIOR_DEOBFUSC)
diffLines.push('')
if (!fs.existsSync(PRIOR_DEOBFUSC)) {
diffLines.push('Prior `deobfuscated/` source not found — skipping diff.')
} else {
// The prior tree was the deobfuscation of keet-core-hyperdb 1.22.0
// (one package). The new architecture splits it into keet-core,
// keet-core-api, keet-core-schemas plus much else. The most useful
// diffs are between the prior root and the new keet-core/keet-core-api
// / keet-core-schemas trees.
for (const newPkg of ['@holepunchto/keet-core']) {
const newPretty = path.join(PRETTY, newPkg)
if (!fs.existsSync(newPretty)) continue
diffLines.push('## ' + newPkg + ' vs prior 1.22.0')
diffLines.push('')
diffLines.push('```')
const r = spawnSync('diff', ['-rq', PRIOR_DEOBFUSC, newPretty], {
encoding: 'utf8'
})
// diff exit 0 = identical, 1 = differences, 2 = error
const lines = (r.stdout || '').split('\n')
// Summarize by category
let onlyOld = 0, onlyNew = 0, differ = 0
const onlyOldEx = [], onlyNewEx = [], differEx = []
for (const line of lines) {
if (line.startsWith('Only in ' + PRIOR_DEOBFUSC)) { onlyOld++; if (onlyOldEx.length < 8) onlyOldEx.push(line) }
else if (line.startsWith('Only in ' + newPretty)) { onlyNew++; if (onlyNewEx.length < 8) onlyNewEx.push(line) }
else if (line.startsWith('Files ')) { differ++; if (differEx.length < 8) differEx.push(line) }
}
diffLines.push('only in prior (1.22.0): ' + onlyOld)
diffLines.push('only in current (4.x): ' + onlyNew)
diffLines.push('present in both, differ: ' + differ)
diffLines.push('```')
diffLines.push('')
if (onlyOldEx.length) {
diffLines.push('### Samples — only in prior (gone from 4.x)')
diffLines.push('')
diffLines.push('```')
for (const x of onlyOldEx) diffLines.push(x)
if (onlyOld > onlyOldEx.length) diffLines.push('… ' + (onlyOld - onlyOldEx.length) + ' more')
diffLines.push('```')
diffLines.push('')
}
if (onlyNewEx.length) {
diffLines.push('### Samples — new in 4.x')
diffLines.push('')
diffLines.push('```')
for (const x of onlyNewEx) diffLines.push(x)
if (onlyNew > onlyNewEx.length) diffLines.push('… ' + (onlyNew - onlyNewEx.length) + ' more')
diffLines.push('```')
diffLines.push('')
}
if (differEx.length) {
diffLines.push('### Samples — present in both, content differs')
diffLines.push('')
diffLines.push('```')
for (const x of differEx) diffLines.push(x)
if (differ > differEx.length) diffLines.push('… ' + (differ - differEx.length) + ' more')
diffLines.push('```')
diffLines.push('')
}
}
}
fs.writeFileSync(path.join(OUT_ROOT, 'DIFF-RAW.md'), diffLines.join('\n'))
console.error(' wrote ' + path.join(OUT_ROOT, 'DIFF-RAW.md'))
console.error('\nDone.')
console.error('Next: read MANIFEST.md → STRUCTURE.md → DIFF-RAW.md, then start filling Notes sections.')
// ──────────────────────────────────────────────────────────────
// helpers
// ──────────────────────────────────────────────────────────────
function escapeMd (s) {
if (!s) return ''
return String(s).replace(/\|/g, '\\|').replace(/\n/g, ' ')
}