OverviewFilesCommitsIssuesDocsReleasesPeers

pear-dep-audit / snapshot-pear-bundle.cjs

132 lines · javascript ref:
#!/usr/bin/env node
// snapshot-pear-bundle.cjs — extract a {pkg → version} snapshot from a
// pear-runtime cached boot.bundle (bare-bundle on-disk format).
//
// pear-runtime fetches the app drive into ~/.config/pear/by-dkey/<dkey>/0/
// where boot.bundle is a single bare-bundle file containing every module's
// source. This script parses it and dumps the package.json versions, so the
// "what Keet is actually running over OTA" question gets a concrete answer
// alongside the "what the AppImage was built with" snapshot.
//
// Bundle format (verified empirically against Keet 4.14.2's drive):
//   <decimal-length>\n
//   <json-manifest>\n
//   <binary-tail of concatenated file bytes>
//
// Manifest fields used:
//   files: { "<path>": { offset, length, mode } }
//
// Usage:
//   node snapshot-pear-bundle.cjs <path-to-boot.bundle> [output.json]
//   node snapshot-pear-bundle.cjs ~/.config/pear/by-dkey/<dkey>/0/boot.bundle
//
// Companion: tools/snapshot-keet-versions.sh (AppImage) — same output shape

const fs = require('fs')
const path = require('path')

function usage (msg) {
  if (msg) process.stderr.write('error: ' + msg + '\n')
  process.stderr.write('usage: snapshot-pear-bundle.cjs <boot.bundle> [output.json]\n')
  process.exit(msg ? 1 : 0)
}

const bundlePath = process.argv[2]
const outputArg = process.argv[3] || null
if (!bundlePath) usage('missing <boot.bundle>')
if (!fs.existsSync(bundlePath)) usage('not found: ' + bundlePath)

const buf = fs.readFileSync(bundlePath)

// Parse header line: <decimal>\n
const nlIdx = buf.indexOf(0x0a) // '\n'
if (nlIdx < 0 || nlIdx > 16) usage('no header line found — not a bare-bundle file?')
const headerNum = parseInt(buf.subarray(0, nlIdx).toString('utf8'), 10)
if (!Number.isFinite(headerNum)) usage('header decimal not parseable')

// Manifest starts at nlIdx + 1. JSON is followed by a separator newline
// before the binary tail. Walk a brace counter through the manifest to
// find the exact JSON boundary — string-aware so a `}` inside a string
// value doesn't false-match.
const manifestStart = nlIdx + 1
let pos = manifestStart
let depth = 0
let inString = false
let prevEscape = false
let started = false
let jsonEnd = -1
while (pos < buf.length) {
  const b = buf[pos]
  if (inString) {
    if (prevEscape) { prevEscape = false }
    else if (b === 0x5c /* \ */) prevEscape = true
    else if (b === 0x22 /* " */) inString = false
  } else {
    if (b === 0x22 /* " */) inString = true
    else if (b === 0x7b /* { */) { depth++; started = true }
    else if (b === 0x7d /* } */) {
      depth--
      if (started && depth === 0) { jsonEnd = pos + 1; break }
    }
  }
  pos++
}
if (jsonEnd < 0) usage('did not find balanced JSON manifest')
const jsonText = buf.subarray(manifestStart, jsonEnd).toString('utf8')
let manifest
try {
  manifest = JSON.parse(jsonText)
} catch (e) {
  usage('manifest JSON parse failed: ' + e.message)
}

// Binary tail starts after the JSON + a single trailing newline separator.
let tailStart = jsonEnd
while (tailStart < buf.length && (buf[tailStart] === 0x0a || buf[tailStart] === 0x20)) tailStart++

const files = manifest.files || {}
const packages = {}
const skipped = []

for (const [filePath, entry] of Object.entries(files)) {
  if (!filePath.endsWith('/package.json')) continue
  if (!entry || typeof entry.offset !== 'number' || typeof entry.length !== 'number') continue
  const off = tailStart + entry.offset
  const blob = buf.subarray(off, off + entry.length)
  let pj
  try {
    pj = JSON.parse(blob.toString('utf8'))
  } catch (e) {
    skipped.push({ path: filePath, reason: 'json parse: ' + e.message })
    continue
  }
  if (!pj.name || !pj.version) continue
  // Prefer first-seen for the canonical (top-level) entry; nested duplicates
  // can show up for the same package at different depths — we want the leaf
  // closest to the entry.
  if (!packages[pj.name]) packages[pj.name] = pj.version
}

const out = {
  source: path.resolve(bundlePath),
  capturedAt: new Date().toISOString(),
  bundleVersion: manifest.version === undefined ? null : manifest.version,
  bundleMain: manifest.main || null,
  fileCount: Object.keys(files).length,
  packageJsonCount: Object.keys(files).filter(k => k.endsWith('/package.json')).length,
  packages
}

const json = JSON.stringify(out, null, 2)
if (outputArg) {
  fs.mkdirSync(path.dirname(path.resolve(outputArg)), { recursive: true })
  fs.writeFileSync(outputArg, json)
  process.stderr.write('wrote ' + outputArg + '\n')
  process.stderr.write('  files in bundle:        ' + out.fileCount + '\n')
  process.stderr.write('  package.json files:     ' + out.packageJsonCount + '\n')
  process.stderr.write('  distinct packages:      ' + Object.keys(packages).length + '\n')
  if (skipped.length) process.stderr.write('  skipped (parse fails):  ' + skipped.length + '\n')
} else {
  process.stdout.write(json + '\n')
}