#!/usr/bin/env node /** * cli:publish-api-contract * * Publishes `.smartstack/api-public//` — OpenAPI (with real response * schemas), a Postman collection and a French integration guide — and refuses * a breaking change under an unchanged version. * * Usage: * npx --prefer-offline tsx skills/external-api/cli/publish-api-contract/index.ts \ * --spec '{"projectPath":"","applicationCode":"crm","version":"v1"}' */ import { parseArgs } from 'node:util' import { executeEnvelope, failExecute, printEnvelope } from '../../../lib/output.js' import { readSpecArg } from '../../../lib/spec-arg.js' import { validate } from './validate.js' import { execute } from './execute.js' import { PublishApiContractInputSchema, type PublishReport } from './types.js' const COMMAND = 'publish-api-contract' async function main(): Promise { const { values } = parseArgs({ options: { spec: { type: 'string' }, 'spec-file': { type: 'string' } }, strict: true, }) const specSrc = readSpecArg(values) if ('error' in specSrc) { printEnvelope(failExecute(COMMAND, [specSrc.error])) process.exit(1) } let raw: unknown try { raw = JSON.parse(specSrc.raw) } catch (err) { printEnvelope(failExecute(COMMAND, [`--spec is not valid JSON: ${(err as Error).message}`])) process.exit(1) } const validation = validate(raw) if (!validation.valid) { printEnvelope(failExecute(COMMAND, validation.errors)) process.exit(1) } try { const spec = PublishApiContractInputSchema.parse(raw) const { report, warnings, errors } = await execute(spec) if (errors.length > 0) { printEnvelope(failExecute(COMMAND, errors)) process.exit(1) } printEnvelope(executeEnvelope(COMMAND, { report, warnings: [...validation.warnings, ...warnings], nextSteps: report.endpointCount === 0 ? ['No catalogued public endpoint found — run scaffold-external-api first, and check audit-dev-external-api for a route with no catalogue row.'] : [ `Commit .smartstack/api-public/${report.version}/ — it is the contract a third party integrates against.`, 'Send the guide + the Postman collection to the partner; the secret itself comes from provision-external-app and is shown once.', ], })) } catch (err) { printEnvelope(failExecute(COMMAND, [(err as Error).message])) process.exit(1) } } void main()