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>
103 lines
4.1 KiB
JavaScript
103 lines
4.1 KiB
JavaScript
/*
|
|
* Copyright 2026 QRun.IO, Inc.
|
|
* Licensed under the Apache License, Version 2.0 (see LICENSE).
|
|
*/
|
|
|
|
/**
|
|
* Writes packages/core/src/api/operations.ts from the merged OpenAPI document: one entry per
|
|
* operation (id, method, path, path and query parameters, body content types), so the kernel can
|
|
* call any operation by id without the generated types, and the CLI can complete operation ids.
|
|
*
|
|
* Usage: node scripts/gen-operations.mjs [spec.json] [out.ts]
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
import { dirname } from 'node:path'
|
|
|
|
const [specPath = 'packages/ui-api-client/openapi/qqq-v1.json', outPath = 'packages/core/src/api/operations.ts'] = process.argv.slice(2)
|
|
const spec = JSON.parse(readFileSync(specPath, 'utf8'))
|
|
const extensions = spec.info['x-qqq-extensions'] ?? []
|
|
const extensionByOperation = new Map()
|
|
for (const e of extensions) for (const id of e.operations ?? []) extensionByOperation.set(id, e.name)
|
|
const ESTABLISHED_PREFIXES = { esb: 'esb', alerts: 'alerts', assistant: 'assistant', 'sql-workbench': 'sql-workbench', health: 'health' }
|
|
|
|
/** The extension an operation belongs to: x-qqq-extensions, else its /qbit/{name} or established path prefix. */
|
|
function extensionOf(id, path) {
|
|
if (extensionByOperation.has(id)) return extensionByOperation.get(id)
|
|
const rest = path.replace(/^\/qqq\/v1/, '').split('/')
|
|
if (rest[1] === 'qbit' && rest[2]) return rest[2]
|
|
return ESTABLISHED_PREFIXES[rest[1]]
|
|
}
|
|
|
|
const operations = []
|
|
for (const [path, item] of Object.entries(spec.paths)) {
|
|
for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
|
|
const op = item[method]
|
|
if (!op) continue
|
|
const params = [...(item.parameters ?? []), ...(op.parameters ?? [])]
|
|
operations.push({
|
|
id: op.operationId,
|
|
method: method.toUpperCase(),
|
|
path,
|
|
summary: (op.summary ?? op.description ?? '').split('\n')[0].slice(0, 120),
|
|
tag: (op.tags ?? [])[0] ?? '',
|
|
pathParams: params.filter((p) => p.in === 'path').map((p) => p.name),
|
|
queryParams: params.filter((p) => p.in === 'query').map((p) => p.name),
|
|
bodyTypes: Object.keys(op.requestBody?.content ?? {}),
|
|
...(extensionOf(op.operationId, path) ? { extension: extensionOf(op.operationId, path) } : {}),
|
|
})
|
|
}
|
|
}
|
|
operations.sort((a, b) => a.id.localeCompare(b.id))
|
|
const lines = [
|
|
'/*',
|
|
' * Copyright 2026 QRun.IO, Inc.',
|
|
' * Licensed under the Apache License, Version 2.0 (see LICENSE).',
|
|
' */',
|
|
'',
|
|
`// Generated by scripts/gen-operations.mjs from the UI API ${spec.info.version} document (core and ${extensions.length} extensions). Do not edit.`,
|
|
'',
|
|
'export interface OperationInfo {',
|
|
' id: string',
|
|
" method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'",
|
|
' path: string',
|
|
' summary: string',
|
|
' tag: string',
|
|
' pathParams: string[]',
|
|
' queryParams: string[]',
|
|
' bodyTypes: string[]',
|
|
' /** The extension (qbit or module) that serves this operation; absent for core operations. */',
|
|
' extension?: string',
|
|
'}',
|
|
'',
|
|
'/** An extension document merged into the client document (info.x-qqq-extensions). */',
|
|
'export interface DocumentExtension {',
|
|
' name: string',
|
|
' version: string',
|
|
' pathPrefix?: string',
|
|
' description?: string',
|
|
'}',
|
|
'',
|
|
`export const UI_API_DOCUMENT_VERSION = ${JSON.stringify(spec.info.version)}`,
|
|
'',
|
|
'/** The extensions merged into the client document, with the versions it was generated from. */',
|
|
'export const DOCUMENT_EXTENSIONS: DocumentExtension[] = [',
|
|
...extensions.map((e) => ` ${JSON.stringify({ name: e.name, version: e.version, pathPrefix: e.pathPrefix, description: (e.description ?? '').split('\n')[0].slice(0, 160) })},`),
|
|
']',
|
|
'',
|
|
'export const OPERATIONS: OperationInfo[] = [',
|
|
...operations.map((o) => ` ${JSON.stringify(o)},`),
|
|
']',
|
|
'',
|
|
'const byId = new Map(OPERATIONS.map((o) => [o.id, o]))',
|
|
'',
|
|
'/** The operation with this id, or undefined. */',
|
|
'export function operation(id: string): OperationInfo | undefined {',
|
|
' return byId.get(id)',
|
|
'}',
|
|
'',
|
|
]
|
|
mkdirSync(dirname(outPath), { recursive: true })
|
|
writeFileSync(outPath, lines.join('\n'))
|
|
console.log(`${operations.length} operations -> ${outPath}`)
|