OverviewFilesCommitsIssuesDocsReleasesPeers

keet-cli / tools / keet-audit.cjs

358 lines · javascript ref:
#!/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, ' ')
}