OverviewFilesCommitsIssuesDocsReleasesPeers

favela-markdown / bin / favela-markdown.js

80 lines · javascript ref:
#!/usr/bin/env node
// favela-markdown — the CLI. library.md §9.
//
// "Every projection defect so far has been diagnosed by reading daemon logs. This makes it a
// one-liner against a file, offline."
//
// ⚠ This file MAY use Node builtins; lib/ may not (§3 — the peer imports lib/ under Bare, and
// nothing imports bin/). Keep the boundary: all logic lives in lib/, this is argv and printing.

import { readFileSync } from 'node:fs'
import process from 'node:process'
import { strip } from '../lib/strip.js'
import { lint } from '../lib/lint.js'
import { parse } from '../lib/record.js'

const USAGE = `favela-markdown — the record codec, offline

  favela-markdown strip <file>                 text + facets
  favela-markdown lint  <file> [--room KIND]   frontmatter, reach, directives, mentions
  favela-markdown project <file> --target T    not implemented — needs a projection target
  favela-markdown explain <file>               not implemented — needs a projection target

  --room KIND   public | followers | members | dm
                Without it, lint makes NO claim about reach: the room is what sets the
                ceiling, and a file on disk does not say which room it belongs to.
`

const SEV = { error: '⛔', warn: '⚠ ', info: ' ·' }

function read (file) {
  if (!file) { process.stderr.write('missing <file>\n\n' + USAGE); process.exit(2) }
  try {
    return readFileSync(file === '-' ? 0 : file, 'utf8')
  } catch (err) {
    process.stderr.write('cannot read ' + file + ': ' + err.message + '\n')
    process.exit(2)
  }
}

function flag (argv, name) {
  const i = argv.indexOf(name)
  return i >= 0 ? argv[i + 1] : null
}

const [cmd, ...rest] = process.argv.slice(2)
const file = rest.find((a) => !a.startsWith('--')) || null

switch (cmd) {
  case 'strip': {
    const rec = parse(read(file))
    const { text, facets } = strip(rec.body)
    process.stdout.write(JSON.stringify({ text, facets }, null, 2) + '\n')
    break
  }

  case 'lint': {
    const r = lint(read(file), { roomType: flag(rest, '--room') || undefined })
    for (const i of r.issues) {
      process.stdout.write(`${SEV[i.severity]} ${i.code}  ${i.message}\n     ${i.spec}\n`)
    }
    if (!r.issues.length) process.stdout.write('· clean\n')
    process.stdout.write(
      `\n${r.ok ? 'ok' : 'NOT OK'} · ${r.publishable ? 'publishable' : 'NOT publishable'}` +
      (flag(rest, '--room') ? '' : ' · reach unchecked (pass --room to check it)') + '\n')
    process.exit(r.ok ? 0 : 1)
  }

  case 'project':
  case 'explain':
    // Honest rather than a stub that prints nothing: these need a projection target, and
    // project()/fromAS2()/fromBsky() are not written yet.
    process.stderr.write(`'${cmd}' is not implemented — it needs project(), which needs a target.\n`)
    process.exit(3)
    break

  default:
    process.stdout.write(USAGE)
    process.exit(cmd ? 2 : 0)
}