Files
qqq-cli/scripts/merge-extensions.mjs
james.maesandClaude Fable 5.1 1bc4ddd0be release: qqq 1.0.0 (#11)
qqq 1.0.0: the command-line client and the full-screen terminal UI (qqq ui) for QQQ applications over the UI API (/qqq/v1), with @qrun-io/qqq-cli 1.0.0, @qrun-io/qqq-core 1.0.0, @qrun-io/qqq-ui-api-client 1.8.0 and @qrun-io/qqq-ui-api-conformance 1.6.0 going final together. Squash of the release/1.0.0 branch at 79ba68e (the branch is kept with the full signed history; the tags v1.8.0 and cli-v1.0.0 point at that commit, which is what the released binaries were built from). See CHANGELOG.md [1.0.0] and docs/release-notes/1.0.0.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 12:49:06 -06:00

92 lines
4.0 KiB
JavaScript

#!/usr/bin/env node
/*
* Copyright 2026 QRun.IO, Inc.
* Licensed under the Apache License, Version 2.0 (see LICENSE).
*/
/**
* Builds the client document from a core document plus every extension document found in the
* sibling checkouts of the QRun-IO workspace, and records where each came from.
*
* Usage: node scripts/merge-extensions.mjs [--core <url|file>] [--root <QRun-IO dir>] [--out <file>]
*
* --core the core document; default the sample server's http://localhost:8077/qqq/v1/openapi.json
* (a server's document is already merged with the extensions that server has; those are
* checked for equality and recorded, not duplicated)
* --root the directory holding the qbit checkouts (default: QRUN_IO_ROOT, else the nearest
* ancestor of this repository that contains qbit-* directories)
* --out default packages/ui-api-client/openapi/qqq-v1.json
*
* Writes packages/ui-api-client/openapi/extension-sources.json: each extension's name, version,
* document path and the git branch and commit it was read at.
*/
import { execFileSync } from 'node:child_process'
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
import { dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parse } from 'yaml'
const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const args = process.argv.slice(2)
const option = (name, fallback) => {
const i = args.indexOf(name)
return i >= 0 ? args[i + 1] : fallback
}
const core = option('--core', 'http://localhost:8077/qqq/v1/openapi.json')
const out = option('--out', join(repo, 'packages/ui-api-client/openapi/qqq-v1.json'))
function findRoot() {
if (process.env.QRUN_IO_ROOT) return process.env.QRUN_IO_ROOT
for (let dir = dirname(repo); dir !== dirname(dir); dir = dirname(dir)) {
if (readdirSync(dir).some((d) => d.startsWith('qbit-'))) return dir
}
throw new Error('No qbit-* checkouts found above this repository; pass --root or set QRUN_IO_ROOT.')
}
const root = option('--root', findRoot())
/** Where each repository keeps its extension documents (a qbit's own repo, or qqq for esb and health). */
function candidates() {
const found = []
const qqq = [join(root, '.worktrees/qqq-develop'), join(root, 'qqq')].find((d) => existsSync(join(d, 'qqq-esb')))
if (qqq) {
for (const module of ['qqq-esb', 'qqq-middleware-javalin']) {
const dir = join(qqq, module, 'src/main/resources/openapi/qqq-v1')
if (existsSync(dir)) for (const f of readdirSync(dir)) if (f.endsWith('.yaml')) found.push(join(dir, f))
}
}
for (const entry of readdirSync(root).filter((d) => d.startsWith('qbit-')).sort()) {
for (const base of [join(root, entry), join(root, entry, entry)]) {
const dir = join(base, 'src/main/resources/openapi/qqq-v1')
if (existsSync(dir)) for (const f of readdirSync(dir)) if (f.endsWith('.yaml')) found.push(join(dir, f))
}
}
return found
}
function git(dir, ...gitArgs) {
try {
return execFileSync('git', ['-C', dir, ...gitArgs], { encoding: 'utf8' }).trim()
} catch {
return ''
}
}
const documents = candidates()
if (!documents.length) throw new Error(`No extension documents under ${root}`)
execFileSync('node', [join(repo, 'scripts/merge-spec.mjs'), '--out', out, core, ...documents], { stdio: 'inherit' })
const sources = documents.map((file) => {
const info = parse(readFileSync(file, 'utf8')).info ?? {}
const dir = dirname(file)
return {
name: String(info.title ?? '').replace(/^QQQ Middleware API extension: /, ''),
version: info.version,
document: relative(root, file),
branch: git(dir, 'rev-parse', '--abbrev-ref', 'HEAD'),
commit: git(dir, 'rev-parse', '--short', 'HEAD'),
}
})
writeFileSync(join(dirname(out), 'extension-sources.json'), JSON.stringify({ core, extensions: sources }, null, 2) + '\n')
for (const s of sources) console.log(`${s.name.padEnd(16)} ${String(s.version).padEnd(7)} ${s.branch}@${s.commit} ${s.document}`)