qwgit / cmd.js
http{/,s} git server
git clone http://git.nthia.dev/qwgit

#!/usr/bin/env node
const path = require('path')
const fs = require('fs')
const { spawn } = require('child_process')
const { homedir } = require('os')
const [args,argv] = require('./lib/args.js')(process.argv.slice(2), {
  boolean: ['h','help','q','quiet']
})
const defaultPort = { ssl: 8765, clear: 8764 }

if (argv.get('help') ?? argv.get('h') !== undefined) return usage(0)
if (args.length === 0) return usage(1)
let quiet = (argv.get('quiet') ?? argv.get('q') ?? [])[0] ?? false

let datadir = (argv.get('datadir') ?? argv.get('d') ?? [])[0]
  ?? path.join(homedir(), '.local/share/qwgit')
let repodir = path.join(datadir, 'repos')
let configDir = argv.get('configdir') ?? path.join(homedir(), '.config/qwgit')
let certdir = (argv.get('certdir') ?? [configDir])[0]
let lcertdir = (argv.get('local-cert-dir') ?? [])[0] ?? path.join(certdir, 'local-certs')
let rcertdir = (argv.get('remote-cert-dir') ?? [])[0] ?? path.join(certdir, 'remote-certs')

let files = {
  repo: (argv.get('repos') ?? []).concat(argv.get('repo') ?? [])[0]
    ?? path.join(configDir, 'repos.json'),
  auth: (argv.get('auth') ?? [])[0] ?? path.join(configDir, 'auth.json'),
  namespaces: (argv.get('namespaces') ?? [])[0] ?? path.join(configDir, 'namespaces.json'),
  remoteCerts: (argv.get('remote-certs') ?? [])[0] ?? path.join(certdir,'remote-certs.json'),
}

let mkConfigDir = true

if (args[0] === 'server') {
  const LocalCerts = require('./lib/local-certs.js')
  let certs = new LocalCerts({ dir: lcertdir })
  let certname = getCertName(argv) ?? 'server'
  loadFiles(files, (err,data) => {
    let qwgit = require('./')({
      repodir,
      repos: data.repo ?? [],
      auth: data.auth ?? [],
      namespaces: data.namespaces ?? [],
    })
    setListeners()
    qwgit.on('reloadConfig', () => {
      loadFiles(files, (err,data) => {
        qwgit.setConfig(data)
        setListeners()
      })
    })
    let fds = argv.get('fd') ?? []
    let listenArgs = args.length > 1 || fds.length > 0
      ? args.slice(1) : [ '+'+defaultPort.ssl, defaultPort.clear ]
    listenArgs.forEach(arg => {
      let opts = {}, ssl = false, cname = certname
      let m = null
      if (m = /^(\+)?(\d+)(?::([^:]+))?$/.exec(arg)) { // port:certname
        ssl = m[1] === '+'
        opts.port = m[2]
        cname = m[3] ?? cname
      } else if (m = /^([\d.]+):(\+)?(\d+)(?::([^:]+))?$/.exec(arg)) { // ipv4:port:certname
        opts.host = m[1]
        ssl = m[2] === '+'
        opts.port = Number(m[3])
        cname = m[3] ?? cname
      } else if (m = /^\[([^\]+])\]:(\+)?(\d+)(?::([^:]+))?$/.exec(arg)) { // [ipv6]:port:certname
        opts.host = m[1]
        ssl = m[2] === '+'
        opts.port = Number(m[3])
        cname = m[4] ?? cname
      } else {
        return error(1, `unexpected formatting for port: ${arg}`)
      }
      createServer(ssl, cname, opts)
    })
    fds.forEach(arg => {
      let m = /^(\+)?(\d+)(?::[^:]+)?$/.exec(arg)
      if (!m) return error(1, `unexpected formatting for fd: ${arg}`)
      let ssl = m[1] === '+'
      let fd = Number(m[2])
      let cname = m[3] ?? cname
      createServer(ssl, cname, { fd })
    })
    function setListeners() {
      qwgit.repos.on('create', repo => {
        writeRepos(qwgit, err => { if (err) console.error(err) })
      })
      qwgit.repos.on('update', repo => {
        writeRepos(qwgit, err => { if (err) console.error(err) })
      })
      qwgit.repos.on('remove', repo => {
        fs.rm(path.join(repodir, repo.id), { recursive: true }, err => {
          if (err) console.error(err)
        })
        writeRepos(qwgit, err => {
          if (err) console.error(err)
        })
      })
    }
    function createServer(ssl, cname, opts) {
      if (ssl) {
        certs.getAllFiles(cname, (err,cdata) => {
          if (err) return error(1, err)
          let server = qwgit.createServer(cdata)
          server.on('error', err => console.error(err))
          server.listen(opts)
          server.on('listening', () => {
            console.error(`listening on ${opts.host ?? ''}:${String(opts.port).padEnd(5)} `
              + `https://${opts.host ?? '127.0.0.1'}:${opts.port}/`)
          })
        })
      } else {
        let server = qwgit.createServer()
        server.on('error', err => console.error(err))
        server.listen(opts)
        server.on('listening', () => {
          console.error(`listening on ${opts.host ?? ''}:${String(opts.port).padEnd(5)} `
            + `http://${opts.host ?? '127.0.0.1'}:${opts.port}/`)
        })
      }
    }
  })
} else if (args[0] === 'config' && /^(auth|repos?)$/.test(args[1]) && args[2] === 'file') {
  let key = args[1]
  if (key === 'repos') key = 'repo'
  console.log(files[key])
} else if (args[0] === 'config' && /^(auth|repos?)$/.test(args[1]) && args[2] === 'show') {
  let key = args[1]
  if (key === 'repos') key = 'repo'
  fs.readFile(files[key], 'utf8', (err,src) => {
    if (err) error(1, err)
    else console.log(src)
  })
} else if (args[0] === 'config' && args[1] === 'reload') {
  let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0]
    ?? 'https://localhost:' + defaultPort.ssl
  loadAPI([getCertName(argv),'client','default'], remote, (err,api) => {
    if (err) return error(1, err)
    api.reloadConfig(err => {
      if (err) return error(1, err)
    })
  })
} else if (args[0] === 'certs' && args.length === 1
|| (args[0] === 'cert' && (args[1] === 'list' || args[1] === undefined))) {
  const LocalCerts = require('./lib/local-certs.js')
  let certs = new LocalCerts({ dir: lcertdir })
  certs.list((err,names) => {
    if (err) error(1, err)
    else names.forEach(name => console.log(name))
  })
} else if (args[0] === 'cert' && args[1] === 'new') {
  const LocalCerts = require('./lib/local-certs.js')
  let certs = new LocalCerts({ dir: lcertdir })
  const { randomBytes, X509Certificate } = require('crypto')
  let name = getCertName(argv) ?? args[2] ?? randomBytes(4).toString('hex')
  certs.create(name, err => {
    if (err) return error(1, err)
    if (!quiet) {
      certs.getFingerprint(name, 'cert.pem', (err,fp) => {
        if (err) return error(1, err)
        console.error(`created cert with name: ${name}`)
        console.error(`created cert with fingerprint: ${fp}`)
      })
    }
  })
} else if (args[0] === 'cert' && args[1] === 'rm') {
  const LocalCerts = require('./lib/local-certs.js')
  let certs = new LocalCerts({ dir: lcertdir })
  let names = (argv.get('name') ?? []).concat(argv.get('n') ?? []).concat(args.slice(2))
  ;(function next(i) {
    if (i >= names.length) return
    certs.remove(names[i], err => {
      if (err) error(1, err)
      else next(i+1)
    })
  })(0)
} else if (args[0] === 'cert' && /^(rename|mv|move)$/.test(args[1])) {
  const LocalCerts = require('./lib/local-certs.js')
  let certs = new LocalCerts({ dir: lcertdir })
  let oldName = args[2], newName = args[3]
  if (oldName === undefined) return error(1, 'old name not given in cert rename')
  if (newName === undefined) return error(1, 'new name not given in cert rename')
  certs.rename(oldName, newName, err => {
    if (err) error(1, err)
  })
} else if (args[0] === 'cert' && (args[1] === 'import' || args[1] === 'link')) {
  const LocalCerts = require('./lib/local-certs.js')
  let certs = new LocalCerts({ dir: lcertdir })
  const { randomBytes, X509Certificate } = require('crypto')
  let name = (argv.get('name') ?? []).concat(argv.get('n') ?? [])[0]
    ?? randomBytes(4).toString('hex')
  let files = args.slice(2)
  if (files.length === 0) {
    return error(1, `usage: ${getCmd()} cert ${args[1]} [CERT FILES OR DIRECTORY]`)
  }
  let link = args[1] === 'link'
  certs.import({ name, files, link }, (err,imported) => {
    if (err) return error(1, err)
    let okCert = imported.includes('cert.pem')
    let okKey = imported.includes('key.pem')
    if (!okCert || !okKey) {
      if (!okCert) console.error(`${args[1]} failed: cert not provided`)
      if (!okKey) console.error(`${args[1]} failed: private key not provided`)
      certs.remove(name, err => {
        if (err) return error(1, err)
        else process.exit(1)
      })
    } else if (!quiet) {
      certs.getFingerprint(name, 'cert.pem', (err,fp) => {
        if (err) return error(1, err)
        console.error(`${args[1]}ed cert with name: ${name}`)
        console.error(`${args[1]}ed cert with fingerprint: ${fp}`)
      })
    }
  })
} else if (args[0] === 'cert' && /^(fp|fingerprint)$/.test(args[1])) {
  const LocalCerts = require('./lib/local-certs.js')
  let certs = new LocalCerts({ dir: lcertdir })
  let names = null
  let showName = false
  let next = function next(i) {
    if (i >= names.length) return
    certs.getFingerprint(names[i], 'cert.pem', (err,fp) => {
      if (err) return error(1, err)
      if (showName) console.log(names[i],fp)
      else console.log(fp)
      next(i+1)
    })
  }
  if (args.length === 2) {
    showName = true
    certs.list((err,list) => {
      if (err) return error(1, err)
      names = list
      next(0)
    })
  } else {
    names = args
    next(2)
  }
} else if (args[0] === 'cert' && args[1] === 'path') {
  if (args.length !== 4) {
    return error(1, `usage: ${getCmd()} cert file NAME FILE`)
  }
  console.log(path.join(lcertdir, args[2], args[3]+'.pem'))
} else if (args[0] === 'cert' && (args[1] === 'remotes' || (args[1] === 'remote' && args[2] === 'list'))) {
  const loadRemoteCerts = require('./lib/remote-certs.js')
  loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
    if (err) return error(1, err)
    rcerts.list().forEach(row => console.log(row.hostname, row.fingerprint512 ?? row.fingerprint))
  })
} else if (args[0] === 'cert' && args[1] === 'remote' && args[2] === 'add') {
  if (args.length < 5) return error(1, `usage: ${getCmd()} cert remote add HOST FINGERPRINT`)
  const loadRemoteCerts = require('./lib/remote-certs.js')
  loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
    if (err) return error(1, err)
    let hostname = args[3]
    let fingerprint = args[4]
    rcerts.add({ fingerprint, hostname })
    rcerts.flush(err => {
      if (err) return error(1, err)
    })
  })
} else if (args[0] === 'cert' && args[1] === 'remote' && /^(rm|remove)$/.test(args[2])) {
  if (args.length < 4) return error(1, `usage: ${getCmd()} cert remote remove HOST FINGERPRINT`)
  const loadRemoteCerts = require('./lib/remote-certs.js')
  loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
    if (err) return error(1, err)
    let hostname = args[3]
    let fingerprint = args[4]
    if (args.length === 5) {
      rcerts.remove({ fingerprint, hostname })
    } else {
      rcerts.remove({ hostname })
    }
    rcerts.flush(err => {
      if (err) return error(1, err)
    })
  })
} else if (args[0] === 'cert' && args[1] === 'remote' && /^fetch(-cert)?$/.test(args[2])) {
  let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0] ?? args[3]
  let u = new URL(remote)
  const getCert = require('./lib/get-cert.js')
  getCert(remote, (err,cert) => {
    if (err) return error(1, err)
    if (!cert) return error(1, 'remote did not provide a tls certificate')
    console.log(cert.fingerprint512?.toLowerCase().replace(/:/g,''))
  })
} else if (args[0] === 'cert' && args[1] === 'remote' && args[2] === 'fetch-ca') {
  let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0] ?? args[3]
  let u = new URL(remote)
  const getCert = require('./lib/get-cert.js')
  getCert(remote, (err,cert) => {
    if (err) return error(1, err)
    if (!cert) return error(1, 'remote did not provide a tls certificate')
    console.log(rawToPEM(cert.raw))
  })
} else if (args[0] === 'cert' && args[1] === 'remote' && args[2] === 'import') {
  let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0] ?? args[3]
  let u = new URL(/^https?:/.test(remote) ? remote : 'https://' + remote)
  const loadRemoteCerts = require('./lib/remote-certs.js')
  const getCert = require('./lib/get-cert.js')
  loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
    if (err) return error(1, err)
    getCert(remote, (err,cert) => {
      if (err) return error(1, err)
      if (!cert) return error(1, 'remote did not provide a tls certificate')
      let recs = rcerts.getFromFingerprint(cert.fingerprint512)
      rcerts.add({
        fingerprint: cert.fingerprint512,
        hostname: hostname(u),
        cert: rawToPEM(cert.raw),
      })
      rcerts.flush(err => {
        if (err) return error(1, err)
      })
    })
  })
} else if (args[0] === 'cert' && args[1] === 'remote' && args[2] === 'fetch-or-import') {
  let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0] ?? args[3]
  let u = new URL(/^https?:/.test(remote) ? remote : 'https://' + remote)
  const loadRemoteCerts = require('./lib/remote-certs.js')
  const getCert = require('./lib/get-cert.js')
  loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
    if (err) return error(1, err)
    let hrecs = rcerts.getFromHostname(hostname(u))
    if (hrecs.length > 0) return hrecs.forEach(r => {
      console.log(r.cert)
    })
    getCert(remote, (err,cert) => {
      if (err) return error(1, err)
      if (!cert) return error(1, 'remote did not provide a tls certificate')
      let recs = rcerts.getFromFingerprint(cert.fingerprint512)
      let rec = {
        fingerprint: cert.fingerprint512,
        hostname: hostname(u),
        cert: rawToPEM(cert.raw),
      }
      rcerts.add(rec)
      rcerts.flush(err => {
        if (err) return error(1, err)
        console.log(rec.cert)
      })
    })
  })
} else if (args[0] === 'install') {
  const loadRemoteCerts = require('./lib/remote-certs.js')
  findGitRoot((err,gitDir) => {
    if (err) return error(1, err)
    let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0]
      ?? args[1]
      ?? 'https://localhost:' + defaultPort.ssl + '/' + path.basename(gitDir)
    if (/^[\/]/.test(remote)) remote = 'https://localhost:' + defaultPort.ssl + remote
    if (!/^https?:/.test(remote)) remote = 'https://' + remote
    let rindex = (argv.get('index') ?? []).concat(argv.get('i') ?? [])[0] ?? 0
    let u = new URL(remote)
    let rhost = u.protocol + '//' + u.host
    let remoteName = (argv.get('remote-name') ?? []).concat(argv.get('rn') ?? [])[0]
      ?? u.hostname
    loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
      let recs = rcerts.getFromHostname(hostname(u))
      if (recs.length === 0) return errImport()
      if (recs[rindex] === undefined) return errImport()
      if (recs[rindex].cert === undefined) return errImport()
      let caFile = rcerts.certPath(recs[rindex].cert)
      fs.stat(caFile, (err,s) => {
        if (err && err.code === 'ENOENT') return errImport()
        getLocalCertFiles([getCertName(argv),'client','default'], (err,opts,ldir) => {
          let certFile = path.join(ldir,'cert.pem')
          let keyFile = path.join(ldir,'key.pem')
          mcmd([
            ['git',['config','--local',`http.${rhost}.sslCAinfo`,caFile], { stdio: 'inherit' }],
            ['git',['config','--local',`http.${rhost}.sslCert`,certFile], { stdio: 'inherit' }],
            ['git',['config','--local',`http.${rhost}.sslKey`,keyFile], { stdio: 'inherit' }],
            ['git',['remote','add',remoteName,remote], { stdio: 'inherit' }],
          ], err => {
            if (err) return error(1, err)
          })
        })
      })
    })
  })
} else if (args[0] === 'cert' && args[1] === 'remote' && args[2] === 'ca') {
  let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0] ?? args[3]
  let u = new URL(/^https?:/.test(remote) ? remote : 'https://' + remote)
  const loadRemoteCerts = require('./lib/remote-certs.js')
  const getCert = require('./lib/get-cert.js')
  loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
    if (err) return error(1, err)
    let recs = rcerts.getFromHostname(hostname(u))
    recs.forEach(rec => {
      if (rec.cert) console.log(rec.cert)
    })
  })
} else if ((args[0] === 'repos' && args.length === 1) || (args[0] === 'repo' && args[1] === 'list')) {
  let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0]
    ?? 'https://localhost:' + defaultPort.ssl
  loadAPI([getCertName(argv),'client','default'], remote, (err,api) => {
    if (err) return error(1, err)
    api.listRepos(onlist)
  })
  function onlist(err,repos) {
    if (err && err.code === 'UNTRUSTED_FINGERPRINT') {
      const loadRemoteCerts = require('./lib/remote-certs.js')
      loadRemoteCerts(files.remoteCerts, rcertdir, (ierr,rcerts) => {
        if (ierr) return error(1, ierr)
        untrustedError(rcerts, err)
      })
      return
    }
    if (err) return error(1, err)
    repos.forEach(repo => console.log(JSON.stringify(repo)))
  }
} else if (args[0] === 'repo' && /^(create|new)$/.test(args[1])) {
  let [args,argv] = require('./lib/args.js')(process.argv.slice(2), {
    boolean: ['h','help','q','quiet','public','unlisted']
  })
  let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0]
    ?? 'https://localhost:' + defaultPort.ssl
  let paths = (argv.get('path') ?? []).concat(argv.get('paths') ?? []).concat(args.slice(2))
  let r = {
    path: paths,
    list: (argv.get('list') ?? []).concat(argv.get('l') ?? []).concat(argv.get('x') ?? [])
      .concat(argv.get('all') ?? []),
    push: (argv.get('push') ?? []).concat(argv.get('write') ?? [])
      .concat(argv.get('all') ?? []),
    pull: (argv.get('pull') ?? []).concat(argv.get('read') ?? [])
      .concat(argv.get('all') ?? []),
    name: (argv.get('name') ?? []).concat(argv.get('n') ?? [])[0] ?? path.basename(paths[0] ?? ''),
    description: (argv.get('description') ?? [])
      .concat(argv.get('desc') ?? []).concat(argv.get('d') ?? []) ?? '',
  }
  let vcount = updateVisibility(r, argv)
  r.list = r.list.concat(r['list:add'] ?? [])
  let lrm = r['list:remove'] ?? []
  r.list = r.list.filter(x => !lrm.includes(x))
  delete r['list:add']
  delete r['list:remove']

  r.push = r.push.concat(r['push:add'] ?? [])
  let pushrm = r['push:remove'] ?? []
  r.push = r.push.filter(x => !pushrm.includes(x))
  delete r['push:add']
  delete r['push:remove']

  r.pull = r.pull.concat(r['pull:add'] ?? [])
  let pullrm = r['pull:remove'] ?? []
  r.pull = r.pull.filter(x => !pullrm.includes(x))
  delete r['pull:add']
  delete r['pull:remove']

  if (vcount > 1) return error(1, 'multiple visibility modifiers are in conflict')
  loadAPI([getCertName(argv),'client','default'], remote, (err,api) => {
    if (err) return error(1, err)
    api.createRepo(r, (err,repo) => {
      if (err && err.code === 'UNTRUSTED_FINGERPRINT') {
        const loadRemoteCerts = require('./lib/remote-certs.js')
        loadRemoteCerts(files.remoteCerts, rcertdir, (ierr,rcerts) => {
          if (ierr) return error(1, ierr)
          untrustedError(rcerts, err)
        })
      }
      else if (err) error(1, err)
      else console.log(repo.id)
    })
  })
} else if (args[0] === 'repo' && args[1] === 'update') {
  let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0]
    ?? 'https://localhost:' + defaultPort.ssl
  let rid = args[2]
  let r = {}
  let id = (argv.get('id') ?? [])[0]
  if (id !== undefined) r.id = id

  let mods = [
    ['paths','path'],
    ['list','l','x','all'],
    ['push','write','all'],
    ['pull','read','all'],
    ['tags','tag'],
  ]
  mods.forEach(ms => {
    let xs = ms.flatMap(m => argv.get(m) ?? [])
    if (xs.length > 0) r[ms[0]] = xs
    let rs = ms.flatMap(r => (argv.get(r+':remove') ?? []).concat(argv.get(r+':rm') ?? []))
    if (rs.length > 0) r[ms[0]+':remove'] = rs
    let as = ms.flatMap(r => (argv.get(r+':add') ?? []).concat(argv.get(r+':push') ?? []))
    if (as.length > 0) r[ms[0]+':add'] = as
  })

  let name = (argv.get('name') ?? []).concat(argv.get('n') ?? [])[0]
  if (name !== undefined) r.name = name
  let description = (argv.get('description') ?? [])
    .concat(argv.get('desc') ?? []).concat(argv.get('d') ?? [])
  if (description !== undefined) r.description = description

  let vcount = updateVisibility(r, argv)
  if (vcount > 1) return error(1, 'multiple visibility modifiers are in conflict')

  loadAPI([getCertName(argv),'client','default'], remote, (err,api) => {
    if (err) return error(1, err)
    if (/^\//.test(rid)) {
      api.updateRepoFromPath(rid, r, (err,repo) => {
        if (err) error(1, err)
      })
    } else {
      api.updateRepo(rid, r, (err,repo) => {
        if (err) error(1, err)
      })
    }
  })
} else if (args[0] === 'repo' && /^(rm|remove|del(?:ete)?)$/.test(args[1])) {
  let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0]
    ?? 'https://localhost:' + defaultPort.ssl
  let id = (argv.get('id') ?? [])[0] ?? args[2]
  loadAPI([getCertName(argv),'client','default'], remote, (err,api) => {
    if (err) return error(1, err)
    if (/^\//.test(id)) {
      api.removeRepoFromPath(id, err => {
        if (err) error(1, err)
      })
    } else {
      api.removeRepo(id, err => {
        if (err) error(1, err)
      })
    }
  })
} else if (args[0] === 'clone') {
  let remote = args[1]
  if (/^[\/]/.test(remote)) remote = 'https://localhost:' + defaultPort.ssl + remote
  if (!/^https?:/.test(remote)) remote = 'https://' + remote
  let u = new URL(/^https?:/.test(remote) ? remote : 'https://' + remote)
  let rindex = (argv.get('index') ?? []).concat(argv.get('i') ?? [])[0] ?? 0
  const loadRemoteCerts = require('./lib/remote-certs.js')
  const getCert = require('./lib/get-cert.js')
  loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
    if (err) return error(1, err)
    let recs = rcerts.getFromHostname(hostname(u))
    if (recs.length === 0) return errImport()
    if (recs[rindex] === undefined) return errImport()
    if (recs[rindex].cert === undefined) return errImport()
    let caFile = rcerts.certPath(recs[rindex].cert)
    fs.stat(caFile, (err,s) => {
      if (err && err.code === 'ENOENT') return errImport()
      getLocalCertFiles([getCertName(argv),'client','default'], (err,opts,ldir) => {
        let certFile = path.join(ldir,'cert.pem')
        let keyFile = path.join(ldir,'key.pem')
        spawn('git', [
          '-c', 'http.sslCAinfo=' + caFile,
          '-c', 'http.sslCert=' + certFile,
          '-c', 'http.sslKey=' + keyFile,
          'clone',
          remote
        ].concat(args.slice(2)), { stdio: 'inherit' })
      })
    })
  })
} else {
  usage(1)
}

function getCertName(argv) {
  return (argv.get('certname') ?? argv.get('cname') ?? argv.get('cn') ?? [])[0]
}

function loadAPI(certnames, remote, cb) {
  const loadRemoteCerts = require('./lib/remote-certs.js')
  const API = require('./lib/api.js')
  getLocalCertFiles(certnames, (err,opts) => {
    if (err) return cb(err)
    loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
      if (err) return error(1, err)
      let api = new API(remote, Object.assign({ verify }, opts))
      cb(null, api)
      function verify(hostname, fp, f) {
        f(null, rcerts.verify({ hostname, fingerprint: fp }))
      }
    })
  })
}

function getLocalCertFiles(certnames, cb) {
  const LocalCerts = require('./lib/local-certs.js')
  certnames = certnames.filter(Boolean)
  if (certnames.length === 0) return cb(null, null)
  ;(function next(i) {
    if (i >= certnames.length) {
      return cb(new Error(`no certs found for cert list: ${certnames.join(', ')}`))
    }
    let lcerts = new LocalCerts({ dir: lcertdir })
    lcerts.getAllFiles(certnames[i], (err,opts) => {
      if (err) return error(1, err)
      if (!opts) return next(i+1)
      cb(null, opts, path.join(lcertdir, certnames[i]))
    })
  })(0)
}

function writeRepos(qwgit, cb) {
  let repos = qwgit.repos.list()
  let str = '[' + repos.map(r => JSON.stringify(r)).join(',\n') + ']\n'
  if (mkConfigDir) {
    fs.mkdir(path.dirname(files.repo), { recursive: true }, err => {
      if (err) cb(err)
      else fs.writeFile(files.repo, str, cb)
    })
  } else {
    fs.writeFile(files.repo, str, cb)
  }
  mkConfigDir = false
}

function loadFiles(files, cb) {
  let out = {}, pending = 1
  Object.entries(files).forEach(([key,file]) => {
    pending++
    fs.readFile(file, (err,src) => {
      if (err && err.code !== 'ENOENT') return cb(err)
      if (src) {
        try { out[key] = JSON.parse(src) }
        catch (err) {
          cb = noop
          return cb(err)
        }
      }
      if (--pending === 0) cb(null, out)
    })
  })
  if (--pending === 0) cb(null, out)
}

function getCerts(dir, cb) {
  let opts = {}, pending = 4
  mread(dir, ['privkey.pem','key.pem'], (err, src) => {
    if (src) opts.key = src
    if (--pending === 0) cb(null, opts)
  })
  mread(dir, ['cert.pem'], (err, src) => {
    if (src) opts.cert = src
    if (--pending === 0) cb(null, opts)
  })
  mread(dir, ['fullchain.pem','ca.pem'], (err, src) => {
    if (src) opts.ca = src
    if (--pending === 0) cb(null, opts)
  })
  if (--pending === 0) cb(null, opts)
}

function mread(dir, files, i, cb) {
  if (typeof i === 'function') {
    cb = i
    i = 0
  }
  if (i >= files.length) return cb(null, null)
  fs.readFile(path.join(dir, files[i]), (err,src) => {
    if (err && err.code === 'ENOENT') read(dir, files, i+1, cb)
    else if (err) cb(err)
    else cb(null, src)
  })
}

function usage(code) {
  let cmd = getCmd()
  console.log(`usage: ${cmd} COMMAND ...

    ${cmd} server {OPTIONS}

      Start a web server on ADDR:PORT, file descriptor FD, or an AF_UNIX FILE.

      --http-port=PORT        Port to listen on. (default: 9650)
      --http-fd=FD            File descriptor inherited from a parent process.
      -a ADDR, --addr=ADDR    (Default: "0.0.0.0")
      -u FILE, --unix=FILE    Listen on an AF_UNIX FILE.

      --certname=CERT_NAME, --cname=CERT_NAME, --cn=CERT_NAME

        Use the certificate profile by CERT_NAME. Default: "server"

    ${cmd} cert
    ${cmd} cert list

      List the names of all local tls certificates known to qwgit,
      printing one name per line.

    ${cmd} cert new CERT_NAME {OPTIONS}

      Create a new ed25519 local tls certificate and private key.

      If no CERT_NAME is given, a random hex string is used.

    ${cmd} cert rm [NAME...]

      Remove one or more local tls certificates by NAME(s). 

    ${cmd} cert rename OLD_NAME NEW_NAME
    ${cmd} cert move OLD_NAME NEW_NAME
    ${cmd} cert mv OLD_NAME NEW_NAME

      Give a local tls certificate a new name.

    ${cmd} cert import {OPTIONS} [FILES OR DIRECTORY...]

      Copy tls certificate FILES or a DIRECTORY of local certificate files into qwgit.
      Each of the files should be related: private key, certificate file, certificate chain.

      --certname=CERT_NAME, --cname=CERT_NAME, --cn=CERT_NAME

        Assign the certificates a name.

    ${cmd} cert link [FILES OR DIRECTORY...]

      Add local tls certificates to qwgit by symbolic links to FILES or DIRECTORY.
      Each of the files should be related: private key, certificate file, certificate chain.

      --certname=CERT_NAME, --cname=CERT_NAME, --cn=CERT_NAME

        Assign the certificates a name.

    ${cmd} cert remotes
    ${cmd} cert remote list

      List all remote host/fingerprint tls certificate associations known to qwgit.

    ${cmd} cert remote add HOST FINGERPRINT

      Associate a remote HOST with a tls certificate hex string FINGERPRINT.

    ${cmd} cert remote rm HOST FINGERPRINT
    ${cmd} cert remote remove HOST FINGERPRINT

      Unassociate a remote HOST with a tls certificate hex string FINGERPRINT.

    ${cmd} cert remote rm HOST
    ${cmd} cert remote remove HOST

      Unassociate all tls certificate fingerprints from a remote HOST.

    ${cmd} repo list

    ${cmd} repo create REPO

      --unlisted
      --public
      --private
      --only=USERLIST

    ${cmd} repo update REPO

      --unlisted
      --public
      --private
      --only=USERLIST

    ${cmd} mirror

    ${cmd} push

  `.trim().replace(/^ {4}/g,'') + '\n')
  process.exit(code)
}

function error(code, err) {
  console.error(err)
  process.exit(code)
}
function noop() {}
function getCmd() { return path.basename(process.argv[1]) }

function untrustedError(rcerts, err) {
  const checkCertChain = require('./lib/cert-chain.js')
  let cmd = getCmd()
  let recs = Array.from(rcerts.getFromHostname(err.hostname) ?? new Set)
  let fp = err.fingerprint512
  let tlsErr = checkCertChain(err.certificate)
  if (recs.length === 0) {
    error(1, `No fingerprints set for host "${err.hostname}".

      ${tlsErr
        ? `Error verifying the remote certificate: ${tlsErr.message}`
        : `The TLS certificate provided by the remote is signed by a root certificate bundled with this runtime.`
      }

      If you want to associate the reported fingerprint with the given host,
      run this command:

        ${cmd} cert remote add ${err.hostname} ${fp}
    `.trim().replace(/^ {6}/mg,'') + '\n')
  } else {
    let s = recs.length > 1 ? '' : 's'
    error(1, `!!! WARNING: fingerprint provided by remote does not match previously stored value${s}.
   
      This fingerprint was provided by the remote:

        ${err.fingerprint512}

      And these are the previously saved fingerprint${s}:

        ${recs.map(r => r.fingerprint512).join('\n      ')}

      ${tlsErr
        ? `${tlsErr}`
        : `This TLS certificate appears "trustworthy" according to the hierarchical CA issuer chain.`
      }

      This problem may be caused by one of several scenarios:

        * the connection has been actively compromised
        * the remote updated its certificate genuinely
        * the fingerprint was set incorrectly for the given remote

      If you think this is a compromised connection, you can do nothing or attempt to check the
      fingerprint through other means.

      If you think the provided fingerprint is correct, use a combination of
        \`${cmd} cert remote add HOST FINGERPRINT\` and
        \`${cmd} cert remote remove HOST FINGERPRINT\`
      commands to remedy the situation.

    `.trim().replace(/^ {4}/mg,'') + '\n')
  }
}

function rawToPEM(raw) {
  return [
    '-----BEGIN CERTIFICATE-----',
    raw.toString('base64').split(/(.{64})/g).filter(Boolean),
    '-----END CERTIFICATE-----'
  ].flat().join('\n')
}

function mcmd(cs,cb) {
  ;(function next(i) {
    if (i >= cs.length) return cb(null, 0)
    let c = cs[i]
    let ps = spawn(c[0],c[1],c[2])
    ps.once('exit', code => {
      if (code === 0) next(i+1)
      else cb(new Error(`non-zero exit code (${code}) in command: ${c[0]} ${c[1].join(' ')}`))
    })
  })(0)
}

function hostname(u) { return u.host }

function findGitRoot(cb) {
  let prev = null
  ;(function next(d) {
    if (d === prev) return cb(null, null)
    prev = d
    fs.stat(path.join(d,'.git'), (err,s) => {
      if (s) cb(null, d)
      else next(path.dirname(d))
    })
  })(process.cwd())
}

function updateVisibility(r, argv) {
  let vcount = 0
  if (argv.has('public')) {
    if (r['list:add'] === undefined) r['list:add'] = []
    if (r['pull:add'] === undefined) r['pull:add'] = []
    r['list:add'].push('*')
    r['pull:add'].push('*')
    vcount++
  }
  if (argv.has('unlisted')) {
    if (r['list:remove'] === undefined) r['list:remove'] = []
    if (r['pull:add'] === undefined) r['pull:add'] = []
    r['list:remove'].push('*')
    r['pull:add'].push('*')
    vcount++
  }
  if (argv.has('private')) {
    if (r['list:remove'] === undefined) r['list:remove'] = []
    if (r['pull:remove'] === undefined) r['pull:remove'] = []
    r['list:remove'].push('*')
    r['pull:remove'].push('*')
    vcount++
  }
  return vcount
}

function errImport() {
  error(1, `no cert imported for remote: ${remote}

    import a certificate for this remote with:

      ${getCmd()} cert remote import '${remote.replace(/\x27/g,'%27').replace(/\\/g,'%5c')}'

  `.replace(/^ {8}/mg,'').trim() + '\n')
}