OverviewFilesCommitsIssuesDocsReleasesPeers

pearbrowser-desktop / scripts / gen-catalogue-seed.mjs

106 lines · javascript ref:
// Generate backend/catalogue-seed.js (the desktop offline seed) FROM the
// versioned PearBrowser Network catalogue manifest at
//   catalog-source/pearbrowser-network.catalog.json
//
// This is the unification primitive: the SAME source feeds (1) this offline
// seed, (2) the published Hyperbee catalog (publish-catalog-bee.js), and (3) the
// relay firehose manifests — so the three can never drift again. The browser's
// dedupeApps() collapses the offline seed against the live bee by driveKey/link,
// so an app present in both shows once.
//
//   node scripts/gen-catalogue-seed.mjs
//   node scripts/gen-catalogue-seed.mjs --source /path/to/catalog.json
//
// The manifest is rich (id, pearLink, homepage, source, license, platforms, …);
// seed rows are validated against the strict APPS_SCHEMA
// (additionalProperties:false), so we project onto exactly its allowed fields and
// infer `type` from `link`. Native delivery is projected into the schema's
// dedicated columns and never copied into the browsable `link` field.

import { readFileSync, writeFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join, resolve } from 'node:path'

const here = dirname(fileURLToPath(import.meta.url))
let sourceArg = null
for (let i = 2; i < process.argv.length; i++) {
  if (process.argv[i] === '--source') sourceArg = process.argv[++i]
  else if (process.argv[i] === '--help' || process.argv[i] === '-h') {
    console.log('usage: node scripts/gen-catalogue-seed.mjs [--source catalog.json]')
    process.exit(0)
  }
}

const SRC = sourceArg
  ? resolve(sourceArg)
  : join(here, '..', 'catalog-source', 'pearbrowser-network.catalog.json')
const OUT = join(here, '..', 'backend', 'catalogue-seed.js')

const cat = JSON.parse(readFileSync(SRC, 'utf8'))
if (!Array.isArray(cat.apps) || cat.apps.length === 0) throw new Error('source manifest has no apps[]')

function manifestType (value) {
  const type = typeof value === 'string' ? value.trim().toLowerCase() : ''
  return type === 'standalone' || type === 'hypersite' ? type : ''
}

// Project a rich manifest entry onto the strict APPS_SCHEMA fields only.
// type: explicit manifest type wins; otherwise a launchable Pear/file app
// defaults to 'standalone' (own window), while a browsable Hyperdrive-only site
// defaults to 'hypersite' (tab). publishedAt is stamped at seed time by
// ensureDevCatalogue, so it is intentionally omitted here.
function toSeedRow (a) {
  const row = { name: a.name, type: manifestType(a.type) || (a.link ? 'standalone' : 'hypersite') }
  if (a.driveKey && /^[0-9a-f]{64}$/i.test(a.driveKey)) row.driveKey = a.driveKey
  if (a.link) row.link = a.link
  if (typeof a.legacyMigrationId === 'string') row.legacyMigrationId = a.legacyMigrationId
  if (a.nativeDelivery && typeof a.nativeDelivery.status === 'string') {
    row.nativeDeliveryStatus = a.nativeDelivery.status
    if (a.nativeDelivery.kind) row.nativeDeliveryKind = a.nativeDelivery.kind
    if (a.nativeDelivery.installLink) row.nativeInstallLink = a.nativeDelivery.installLink
    if (a.nativeDelivery.productName) row.nativeProductName = a.nativeDelivery.productName
    if (Array.isArray(a.nativeDelivery.targets) && a.nativeDelivery.targets.length) row.nativeTargets = a.nativeDelivery.targets
  }
  // Inline icon (data: URI) for apps without a fetchable drive icon — rendered
  // directly by AppIcon, so every catalogue app shows a real icon, not a glyph.
  if (a.iconData) row.iconData = a.iconData
  if (a.author) row.author = a.author
  if (Array.isArray(a.categories) && a.categories.length) row.categories = a.categories
  if (a.description) row.description = a.description
  if (a.version) row.version = String(a.version)
  if (a.homepage) row.homepage = a.homepage
  if (a.sourceUrl || a.source) row.sourceUrl = a.sourceUrl || a.source
  if (a.license) row.license = a.license
  row.verification = a.verification || 'relay-listed'
  return row
}

const rows = cat.apps.map(toSeedRow)
for (const [i, r] of rows.entries()) {
  if (!r.name || !r.type) throw new Error(`row ${i} missing name/type`)
  if (!r.driveKey && !r.link && !r.legacyMigrationId && !r.nativeInstallLink) throw new Error(`row ${i} (${r.name}) has no browsable, migration, or native delivery identity`)
}

const header = `/**
 * Dev catalogue seed data — GENERATED FILE, do not edit by hand.
 *
 * Source of truth: catalog-source/pearbrowser-network.catalog.json
 * Regenerate:      node scripts/gen-catalogue-seed.mjs
 *
 * The backend can seed this into a local schema-sheets demo room when
 * PEARBROWSER_DEV_CATALOGUE=1 (ensureDevCatalogue in index.js). Normal release
 * launches use the curated live Hyperbee catalog plus this offline seed source;
 * dedupeApps() collapses overlaps by driveKey/link, so each app appears once.
 *
 * Each entry is one \`apps\` row (validated against APPS_SCHEMA): name + type
 * required, driveKey OR link, type ∈ {standalone (window), hypersite (run-in-tab)}.
 */
`

const body = 'module.exports = {\n  SEED_APPS: ' +
  JSON.stringify(rows, null, 2).replace(/\n/g, '\n  ') + '\n}\n'

writeFileSync(OUT, header + body)
console.log(`wrote ${OUT}`)
console.log(`${rows.length} apps:`, rows.map((r) => `${r.name} (${r.type})`).join(', '))