@octalmesh/seagull-core 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1364 @@
1
+ import { createRequire } from "node:module";
2
+ import { z } from "zod";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { parse, stringify } from "yaml";
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { createHash } from "node:crypto";
8
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
9
+ //#region src/config/schema.ts
10
+ /**
11
+ * The seagull config *schema* version - not the npm package's own version.
12
+ * Bumped only when the shape of `seagull.yaml` changes in a breaking way, so
13
+ * older configs fail with a clear "this config targets schema vN, seagull
14
+ * expects vM" error instead of a confusing validation failure on some
15
+ * unrelated field once the schema moves on.
16
+ */
17
+ const CONFIG_SCHEMA_VERSION = 1;
18
+ const varsTreeSchema = z.lazy(() => z.record(z.string(), z.union([
19
+ z.string(),
20
+ z.number(),
21
+ z.boolean(),
22
+ varsTreeSchema
23
+ ])));
24
+ const githubSchema = z.object({
25
+ owner: z.string().min(1),
26
+ repo: z.string().min(1)
27
+ });
28
+ const pathsSchema = z.object({
29
+ dist: z.string().min(1).default("dist"),
30
+ specs: z.string().min(1).optional(),
31
+ docs: z.string().min(1).optional(),
32
+ sdk: z.string().min(1).optional()
33
+ });
34
+ const docsSchema = z.object({
35
+ server: z.object({
36
+ host: z.string().min(1),
37
+ port: z.number().int().positive()
38
+ }),
39
+ metadata: z.object({
40
+ title: z.string().min(1),
41
+ description: z.string().min(1),
42
+ favicon: z.string().min(1),
43
+ baseServerUrl: z.string().min(1)
44
+ })
45
+ });
46
+ const sdkToolSchema = z.enum(["openapi-generator", "openapi-typescript"]);
47
+ const sdkLangSchema = z.enum([
48
+ "typescript",
49
+ "go",
50
+ "java"
51
+ ]);
52
+ const sdkKindSchema = z.enum(["client", "server"]);
53
+ /**
54
+ * `-g`/`--additional-properties` values: openapi-generator accepts strings,
55
+ * numbers and booleans, all rendered as `key=value` on the CLI.
56
+ */
57
+ const additionalPropertiesSchema = z.record(z.string(), z.union([
58
+ z.string(),
59
+ z.number(),
60
+ z.boolean()
61
+ ])).default({});
62
+ const mavenCoordsSchema = z.object({
63
+ groupId: z.string().min(1),
64
+ artifactId: z.string().min(1)
65
+ });
66
+ /**
67
+ * Publishing conventions - git branch/tag naming, and where registry-backed
68
+ * artifacts (npm, Maven) get pushed. Every field is a template supporting the
69
+ * usual `{...}` placeholders (`{service}`, `{id}`, `{github.*}`, `{vars.*}`,
70
+ * and for `tag` only, also `{version}`).
71
+ *
72
+ * Required at the root level - seagull has no built-in opinion on branch/tag
73
+ * naming or which registry to use, so this has to come from the config, not
74
+ * from a hardcoded convention baked into the tool. Per-generator
75
+ * (`generators.<id>.publishing`) and per-contract-artifact
76
+ * (`artifacts[].overrides.publishing`) blocks only need to override the
77
+ * fields that differ for that generator/artifact - see
78
+ * {@link publishingOverrideSchema}.
79
+ */
80
+ const publishingSchema = z.object({
81
+ /**
82
+ * Git branch artifacts publish to. Resolved once, at config-load time -
83
+ * no `{version}` available here, e.g. `"sdk/svc-{service}/{id}"`.
84
+ */
85
+ branch: z.string().min(1),
86
+ /**
87
+ * Git tag artifacts are tagged with on publish. Resolved at publish time,
88
+ * once the version is known, e.g. `"svc-{service}-{id}-v{version}"`.
89
+ */
90
+ tag: z.string().min(1),
91
+ /**
92
+ * Template for the `repository.url` field written into generated
93
+ * `package.json` (and shown in default README templates), e.g.
94
+ * `"https://github.com/{github.owner}/{github.repo}"`.
95
+ */
96
+ repositoryUrl: z.string().min(1),
97
+ npm: z.object({
98
+ registry: z.string().min(1),
99
+ access: z.enum(["public", "restricted"])
100
+ }),
101
+ maven: z.object({
102
+ repositoryId: z.string().min(1),
103
+ repositoryUrl: z.string().min(1)
104
+ })
105
+ });
106
+ /**
107
+ * The generator-level / per-artifact-override form of {@link publishingSchema} -
108
+ * every field optional, since it only needs to override whichever fields
109
+ * differ from the root-level `publishing:` (which is guaranteed complete).
110
+ */
111
+ const publishingOverrideSchema = z.object({
112
+ branch: z.string().min(1).optional(),
113
+ tag: z.string().min(1).optional(),
114
+ repositoryUrl: z.string().min(1).optional(),
115
+ npm: z.object({
116
+ registry: z.string().min(1).optional(),
117
+ access: z.enum(["public", "restricted"]).optional()
118
+ }).optional(),
119
+ maven: z.object({
120
+ repositoryId: z.string().min(1).optional(),
121
+ repositoryUrl: z.string().min(1).optional()
122
+ }).optional()
123
+ });
124
+ /**
125
+ * The shape of a generator "recipe", without the cross-field checks below -
126
+ * kept separate so {@link artifactRefSchema}'s `overrides:` can `.partial()`
127
+ * it (zod rejects `.partial()` on a schema with `.check()` refinements
128
+ * attached).
129
+ */
130
+ const generatorDefBaseSchema = z.object({
131
+ tool: sdkToolSchema,
132
+ lang: sdkLangSchema,
133
+ kind: sdkKindSchema,
134
+ /**
135
+ * `openapi-generator -g <generator>` value. Required when `tool` is
136
+ * `openapi-generator`.
137
+ */
138
+ generator: z.string().min(1).optional(),
139
+ /** npm package name template, e.g. `"@{vars.org}/{service}-client"`. */
140
+ package: z.string().min(1).optional(),
141
+ goModule: z.string().min(1).optional(),
142
+ goPackageName: z.string().min(1).optional(),
143
+ maven: mavenCoordsSchema.optional(),
144
+ additionalProperties: additionalPropertiesSchema,
145
+ /**
146
+ * Optional path (relative to the config file's directory) to a custom README
147
+ * template for this artifact - supports the same `{...}` placeholders as
148
+ * naming templates, plus `{version}`, `{title}`, and `{artifact.*}`. If
149
+ * omitted, a built-in default template for the artifact's language/kind is
150
+ * used instead.
151
+ */
152
+ readme: z.string().min(1).optional(),
153
+ /**
154
+ * Publishing conventions (branch/tag naming, registry URLs) for this
155
+ * generator specifically - overrides whichever fields differ from the
156
+ * root-level `publishing:` (required, see {@link publishingSchema}).
157
+ */
158
+ publishing: publishingOverrideSchema.optional()
159
+ });
160
+ /**
161
+ * A single generator "recipe": which tool to invoke and how. Referenced by id
162
+ * from one or more contracts' `artifacts:` list.
163
+ */
164
+ const generatorDefSchema = generatorDefBaseSchema.check((ctx) => {
165
+ const value = ctx.value;
166
+ if (value.tool === "openapi-generator" && !value.generator) ctx.issues.push({
167
+ code: "custom",
168
+ message: "\"generator\" is required when tool is \"openapi-generator\"",
169
+ input: value
170
+ });
171
+ if (value.lang === "go" && !value.goModule) ctx.issues.push({
172
+ code: "custom",
173
+ message: "\"goModule\" is required for lang \"go\"",
174
+ input: value
175
+ });
176
+ if (value.lang === "java" && !value.maven) ctx.issues.push({
177
+ code: "custom",
178
+ message: "\"maven\" ({ groupId, artifactId }) is required for lang \"java\"",
179
+ input: value
180
+ });
181
+ if (value.lang === "typescript" && !value.package) ctx.issues.push({
182
+ code: "custom",
183
+ message: "\"package\" is required for lang \"typescript\"",
184
+ input: value
185
+ });
186
+ });
187
+ const generatorsSchema = z.record(z.string(), generatorDefSchema);
188
+ /**
189
+ * A contract's reference to a generator by id. The plain-string form just runs
190
+ * that generator as-is; the object form lets one contract tweak a shared
191
+ * generator (extra/overridden `additionalProperties`, a different
192
+ * `maven`/`package`/`readme`/... value) without duplicating the whole recipe
193
+ * under a new id, and `as` renames the artifact's own id (output
194
+ * folder / branch / tag segment) if a contract needs two variants of the same
195
+ * base generator.
196
+ */
197
+ const artifactRefSchema = z.union([z.string().min(1), z.object({
198
+ generator: z.string().min(1),
199
+ as: z.string().min(1).optional(),
200
+ overrides: generatorDefBaseSchema.partial().optional()
201
+ })]);
202
+ const contractSchema = z.object({
203
+ name: z.string().min(1),
204
+ title: z.string().min(1),
205
+ entrypoint: z.string().min(1),
206
+ artifacts: z.array(artifactRefSchema).min(1)
207
+ });
208
+ const rootConfigSchema = z.object({
209
+ /**
210
+ * The config schema version this file targets. Currently must be `1`
211
+ * (the only version that exists) - see {@link CONFIG_SCHEMA_VERSION}.
212
+ */
213
+ configVersion: z.literal(1),
214
+ github: githubSchema,
215
+ vars: varsTreeSchema.default({}),
216
+ paths: pathsSchema.default({ dist: "dist" }),
217
+ docs: docsSchema,
218
+ /**
219
+ * Publishing conventions (branch/tag naming, registry URLs), applied to
220
+ * every artifact unless overridden per-generator or
221
+ * per-contract-artifact. Required - seagull has no built-in default here,
222
+ * see {@link publishingSchema}.
223
+ */
224
+ publishing: publishingSchema,
225
+ generators: generatorsSchema,
226
+ contracts: z.array(contractSchema).min(1)
227
+ });
228
+ //#endregion
229
+ //#region src/config/template.ts
230
+ const PLACEHOLDER = /\{([a-zA-Z0-9_.]+)}/g;
231
+ /**
232
+ * Replaces every `{dot.path}` placeholder in `template` with its value from
233
+ * `context`, throwing a descriptive error if a placeholder has no match -
234
+ * a typo'd `{vars.org}` should fail loudly at config-load time, not silently
235
+ * produce a package named literally `@{vars.org}/...`.
236
+ *
237
+ * @param template - The template string, e.g. `"@{vars.org}/{service}-client"`.
238
+ * @param context - The flattened context to resolve placeholders against.
239
+ * @returns The interpolated string.
240
+ */
241
+ function interpolate(template, context) {
242
+ return template.replace(PLACEHOLDER, (_match, key) => {
243
+ const value = context[key];
244
+ if (value === void 0) {
245
+ const available = Object.keys(context).sort().join(", ");
246
+ throw new Error(`Unknown template placeholder "{${key}}" in "${template}" (available: ${available})`);
247
+ }
248
+ return value;
249
+ });
250
+ }
251
+ /**
252
+ * Recursively interpolates every string value in `value` (walking through plain
253
+ * objects), leaving non-string leaves untouched. Used to resolve
254
+ * `additionalProperties` maps, which may mix templated strings with plain
255
+ * booleans/numbers.
256
+ *
257
+ * @param value - The value (string, object, or primitive) to interpolate.
258
+ * @param context - The flattened context to resolve placeholders against.
259
+ * @returns A deep copy of `value` with every string interpolated.
260
+ */
261
+ function interpolateDeep(value, context) {
262
+ if (typeof value === "string") return interpolate(value, context);
263
+ if (Array.isArray(value)) return value.map((item) => interpolateDeep(item, context));
264
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, interpolateDeep(child, context)]));
265
+ return value;
266
+ }
267
+ /**
268
+ * Flattens a nested scope object (e.g. `{ service: "auth", vars: { org: "x" } }`)
269
+ * into dot-path lookup keys (`{ service: "auth", "vars.org": "x" }`) for use
270
+ * with {@link interpolate}.
271
+ *
272
+ * @param scope - The nested scope to flatten.
273
+ * @returns A flat dot-path -> string map.
274
+ */
275
+ function buildTemplateContext(scope) {
276
+ const out = {};
277
+ flatten(scope, "", out);
278
+ return out;
279
+ }
280
+ function flatten(value, prefix, out) {
281
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
282
+ for (const [key, child] of Object.entries(value)) flatten(child, prefix ? `${prefix}.${key}` : key, out);
283
+ return;
284
+ }
285
+ if (value === void 0) return;
286
+ out[prefix] = String(value);
287
+ }
288
+ //#endregion
289
+ //#region src/config/loader.ts
290
+ /**
291
+ * Reads and validates a CLI config file, throwing a readable, multi-issue error
292
+ * message if it doesn't match the schema.
293
+ *
294
+ * @param configPath - Absolute path to the YAML config file.
295
+ * @returns The validated (but not yet resolved) raw config.
296
+ */
297
+ function readRawConfig(configPath) {
298
+ const raw = parse(readFileSync(configPath, "utf8"));
299
+ const result = rootConfigSchema.safeParse(raw);
300
+ if (!result.success) {
301
+ const issues = z.prettifyError(result.error);
302
+ throw new Error(`Invalid ${path.basename(configPath)}:\n${issues}`);
303
+ }
304
+ return result.data;
305
+ }
306
+ /**
307
+ * Deep-merges a generator override (from an artifact's `overrides:` block)
308
+ * onto its base generator def. `additionalProperties`, `maven`, and
309
+ * `publishing` are merged key-by-key; every other field is a plain override.
310
+ *
311
+ * @param base - The base generator def, looked up by id from `generators:`.
312
+ * @param overrides - The partial override from the artifact reference, if any.
313
+ * @returns The merged generator def.
314
+ */
315
+ function mergeGeneratorOverride(base, overrides) {
316
+ if (!overrides) return base;
317
+ return {
318
+ ...base,
319
+ ...overrides,
320
+ maven: overrides.maven ? {
321
+ ...base.maven,
322
+ ...overrides.maven
323
+ } : base.maven,
324
+ additionalProperties: {
325
+ ...base.additionalProperties,
326
+ ...overrides.additionalProperties
327
+ },
328
+ publishing: mergePublishingOverride(base.publishing, overrides.publishing)
329
+ };
330
+ }
331
+ /**
332
+ * Deep-merges two partial `publishing:` overrides (from a generator def and
333
+ * an artifact-ref's `overrides:` block) into one.
334
+ *
335
+ * @param base - The base override (maybe undefined).
336
+ * @param overrides - The overriding override (maybe undefined).
337
+ * @returns The merged override, or undefined if both inputs were.
338
+ */
339
+ function mergePublishingOverride(base, overrides) {
340
+ if (!overrides) return base;
341
+ return {
342
+ ...base,
343
+ ...overrides,
344
+ npm: overrides.npm ? {
345
+ ...base?.npm,
346
+ ...overrides.npm
347
+ } : base?.npm,
348
+ maven: overrides.maven ? {
349
+ ...base?.maven,
350
+ ...overrides.maven
351
+ } : base?.maven
352
+ };
353
+ }
354
+ /**
355
+ * Applies a (possibly partial) `publishing:` override onto the required
356
+ * root-level `publishing:` block, producing a fully complete result - the
357
+ * root block is guaranteed complete by the schema, so there's no fallback
358
+ * case to handle here (unlike {@link mergePublishingOverride}).
359
+ *
360
+ * @param root - The root-level `publishing:` config (required, always
361
+ * complete).
362
+ * @param override - The generator/artifact-level override, if any.
363
+ * @returns The fully complete, merged publishing config.
364
+ */
365
+ function applyPublishingOverride(root, override) {
366
+ if (!override) return root;
367
+ return {
368
+ ...root,
369
+ ...override,
370
+ npm: override.npm ? {
371
+ ...root.npm,
372
+ ...override.npm
373
+ } : root.npm,
374
+ maven: override.maven ? {
375
+ ...root.maven,
376
+ ...override.maven
377
+ } : root.maven
378
+ };
379
+ }
380
+ /**
381
+ * Resolves a single artifact reference (string id, or `{ generator, overrides,
382
+ * as }`) into its `{ id, def }` pair, looking up the base generator by id and
383
+ * applying any overrides.
384
+ *
385
+ * @param ref - The artifact reference from a contract's `artifacts:`
386
+ * list.
387
+ * @param generators - The full `generators:` map from the raw config.
388
+ * @param contractName - The owning contract's name.
389
+ * @returns The artifact's resolved id and generator def (templates not yet
390
+ * interpolated).
391
+ */
392
+ function resolveArtifactRef(ref, generators, contractName) {
393
+ const generatorId = typeof ref === "string" ? ref : ref.generator;
394
+ const base = generators[generatorId];
395
+ if (!base) {
396
+ const available = Object.keys(generators).sort().join(", ");
397
+ throw new Error(`Contract "${contractName}" references unknown generator "${generatorId}" (available: ${available})`);
398
+ }
399
+ return {
400
+ id: typeof ref === "string" ? ref : ref.as ?? ref.generator,
401
+ def: mergeGeneratorOverride(base, typeof ref === "string" ? void 0 : ref.overrides)
402
+ };
403
+ }
404
+ /**
405
+ * Resolves an artifact's publishing conventions: applies the (already
406
+ * override-merged) generator-level `publishing:` override onto the required
407
+ * root-level `publishing:` block, then interpolates every field except `tag`
408
+ * (which keeps `{version}` unresolved, since it isn't known until publish time;
409
+ * see `config/publishing.ts`).
410
+ *
411
+ * @param generatorPublishing - The (override-merged) generator's own
412
+ * `publishing:` override, if any.
413
+ * @param rootPublishing - The required root-level `publishing:` config
414
+ * from the config file.
415
+ * @param context - The flattened template context for this artifact
416
+ * (`service`, `id`, `github.*`, `vars.*`).
417
+ * @returns The fully resolved publishing conventions for this artifact.
418
+ */
419
+ function resolvePublishing(generatorPublishing, rootPublishing, context) {
420
+ const merged = applyPublishingOverride(rootPublishing, generatorPublishing);
421
+ return {
422
+ branch: interpolate(merged.branch, context),
423
+ tagTemplate: merged.tag,
424
+ repositoryUrl: interpolate(merged.repositoryUrl, context),
425
+ npmRegistry: interpolate(merged.npm.registry, context),
426
+ npmAccess: merged.npm.access,
427
+ mavenRepositoryId: interpolate(merged.maven.repositoryId, context),
428
+ mavenRepositoryUrl: interpolate(merged.maven.repositoryUrl, context)
429
+ };
430
+ }
431
+ /**
432
+ * Interpolates templates and resolves absolute paths for a single artifact.
433
+ *
434
+ * @param id - The artifact's resolved id (output folder / branch /
435
+ * tag segment).
436
+ * @param def - The (override-merged, not-yet-interpolated)
437
+ * generator def.
438
+ * @param rootDir - Absolute repo root, `readme` template paths are
439
+ * resolved relative to this.
440
+ * @param sdkDir - Absolute path to the SDK output root (`<dist>/sdk`).
441
+ * @param contractName - The owning contract's name.
442
+ * @param contractContext - The flattened template context for this contract
443
+ * (`service`, `github.*`, `vars.*` - not yet `id`).
444
+ * @param rootPublishing - The required root-level `publishing:` config from
445
+ * the config file.
446
+ * @returns The fully resolved artifact.
447
+ */
448
+ function resolveArtifact(id, def, rootDir, sdkDir, contractName, contractContext, rootPublishing) {
449
+ const context = {
450
+ ...contractContext,
451
+ id
452
+ };
453
+ const resolved = interpolateDeep(def, context);
454
+ const publishing = resolvePublishing(def.publishing, rootPublishing, context);
455
+ return {
456
+ id,
457
+ tool: resolved.tool,
458
+ lang: resolved.lang,
459
+ kind: resolved.kind,
460
+ generator: resolved.generator,
461
+ outputDir: path.join(sdkDir, contractName, id),
462
+ branch: publishing.branch,
463
+ publishing,
464
+ additionalProperties: resolved.additionalProperties,
465
+ package: resolved.package,
466
+ goModule: resolved.goModule,
467
+ goPackageName: resolved.goPackageName,
468
+ maven: resolved.maven,
469
+ readmeTemplate: resolved.readme ? path.resolve(rootDir, resolved.readme) : void 0
470
+ };
471
+ }
472
+ /**
473
+ * Resolves a single contract: its entrypoint path and every artifact in its
474
+ * `artifacts:` list.
475
+ *
476
+ * @param input - The raw contract config.
477
+ * @param rootDir - Absolute repo root, entrypoints/`readme` paths are
478
+ * resolved relative to this.
479
+ * @param sdkDir - Absolute path to the SDK output root (`<dist>/sdk`).
480
+ * @param generators - The full `generators:` map from the raw config.
481
+ * @param githubCtx - `{ owner, repo }`, exposed to templates as
482
+ * `{github.owner}`/`{github.repo}`.
483
+ * @param vars - The `vars:` tree from the raw config, exposed as
484
+ * `{vars.*}`.
485
+ * @param rootPublishing - The required root-level `publishing:` config from the
486
+ * config file.
487
+ * @returns The fully resolved contract.
488
+ */
489
+ function resolveContract(input, rootDir, sdkDir, generators, githubCtx, vars, rootPublishing) {
490
+ const context = buildTemplateContext({
491
+ service: input.name,
492
+ github: githubCtx,
493
+ vars
494
+ });
495
+ const artifacts = input.artifacts.map((ref) => {
496
+ const { id, def } = resolveArtifactRef(ref, generators, input.name);
497
+ return resolveArtifact(id, def, rootDir, sdkDir, input.name, context, rootPublishing);
498
+ });
499
+ const entrypoint = path.join(rootDir, input.entrypoint);
500
+ return {
501
+ name: input.name,
502
+ title: input.title,
503
+ entrypoint,
504
+ entrypointRelative: path.relative(rootDir, entrypoint),
505
+ artifacts
506
+ };
507
+ }
508
+ /**
509
+ * Loads, validates, and fully resolves a CLI config file - the single entry
510
+ * point every command uses to get its configuration.
511
+ *
512
+ * Unlike a build tool bundled into the consumer's own repo, seagull is
513
+ * installed as a dependency, so it has no way to guess where the consumer's
514
+ * config lives on its own - `configPath` must be supplied by the caller (the
515
+ * CLI resolves it via `resolveConfigPath()` in `config/resolve-config-file.ts`,
516
+ * or `--config`).
517
+ *
518
+ * @param configPath - Absolute path to the CLI config file.
519
+ * @returns The fully resolved config.
520
+ */
521
+ function loadConfig(configPath) {
522
+ const rootDir = path.dirname(configPath);
523
+ const raw = readRawConfig(configPath);
524
+ const distDir = path.resolve(rootDir, raw.paths.dist);
525
+ const specsDir = raw.paths.specs ? path.resolve(rootDir, raw.paths.specs) : path.join(distDir, "specs");
526
+ const docsDir = raw.paths.docs ? path.resolve(rootDir, raw.paths.docs) : path.join(distDir, "docs");
527
+ const sdkDir = raw.paths.sdk ? path.resolve(rootDir, raw.paths.sdk) : path.join(distDir, "sdk");
528
+ const contracts = raw.contracts.map((contract) => resolveContract(contract, rootDir, sdkDir, raw.generators, raw.github, raw.vars, raw.publishing));
529
+ return {
530
+ configVersion: raw.configVersion,
531
+ rootDir,
532
+ paths: {
533
+ dist: distDir,
534
+ specs: specsDir,
535
+ docs: docsDir,
536
+ sdk: sdkDir
537
+ },
538
+ github: raw.github,
539
+ vars: raw.vars,
540
+ docs: raw.docs,
541
+ contracts,
542
+ allArtifacts: contracts.flatMap((contract) => contract.artifacts.map((artifact) => ({
543
+ contract,
544
+ artifact
545
+ })))
546
+ };
547
+ }
548
+ //#endregion
549
+ //#region src/config/publishing.ts
550
+ /**
551
+ * Renders an artifact's final git tag from its `publishing.tagTemplate` -
552
+ * the one piece of `publishing:` config that can't be resolved at config-load
553
+ * time, since it needs the artifact's version, which is only known once the
554
+ * contract's spec has been bundled.
555
+ *
556
+ * @param artifact - The resolved artifact (for `id` and
557
+ * `publishing.tagTemplate`).
558
+ * @param contractName - The owning contract's name, exposed to the template as
559
+ * `{service}`.
560
+ * @param version - The resolved SDK version, exposed to the template as
561
+ * `{version}`.
562
+ * @param github - `{ owner, repo }`, exposed as `{github.owner}`/
563
+ * `{github.repo}`.
564
+ * @param vars - The config's `vars:` tree, exposed as `{vars.*}`.
565
+ * @returns The rendered tag name.
566
+ */
567
+ function renderArtifactTag(artifact, contractName, version, github, vars) {
568
+ const context = buildTemplateContext({
569
+ service: contractName,
570
+ id: artifact.id,
571
+ version,
572
+ github,
573
+ vars
574
+ });
575
+ return interpolate(artifact.publishing.tagTemplate, context);
576
+ }
577
+ //#endregion
578
+ //#region src/config/resolve-config-file.ts
579
+ /**
580
+ * Config filenames CLI recognizes, checked in this order.
581
+ */
582
+ const CONFIG_FILENAMES = [
583
+ ".seagull",
584
+ ".seagull.yaml",
585
+ ".seagull.yml",
586
+ "seagull.yaml",
587
+ "seagull.yml"
588
+ ];
589
+ /**
590
+ * Finds the CLI config file in a directory, trying each of
591
+ * {@link CONFIG_FILENAMES} in order.
592
+ *
593
+ * @param cwd - The directory to look in (typically `process.cwd()`).
594
+ * @returns The absolute path to the first matching config file.
595
+ * @throws Error if none of the candidate filenames exist in `cwd`.
596
+ *
597
+ * @see {@link CONFIG_FILENAMES} - the list of filenames checked, in order.
598
+ */
599
+ function resolveConfigPath(cwd) {
600
+ for (const filename of CONFIG_FILENAMES) {
601
+ const candidate = path.join(cwd, filename);
602
+ if (existsSync(candidate)) return candidate;
603
+ }
604
+ throw new Error(`No CLI config found in ${cwd} - looked for: ${CONFIG_FILENAMES.join(", ")}. Create one of these, or pass --config <path>.`);
605
+ }
606
+ //#endregion
607
+ //#region src/generator/generator.ts
608
+ /**
609
+ * The root primitive every concrete SDK generator implements.
610
+ *
611
+ * One instance per underlying tool (`openapi-generator-cli`,
612
+ * `openapi-typescript`, ...) - not one per language, since a single tool
613
+ * invocation (e.g. `openapi-generator-cli -g java`/`-g go`) already covers
614
+ * every language it supports. Language-specific behaviour (patching `go.mod`,
615
+ * `package.json`, `pom.xml`, ...) is composed in via patchers rather than
616
+ * living in per-language subclasses.
617
+ */
618
+ var Generator = class {};
619
+ //#endregion
620
+ //#region src/generator/registry.ts
621
+ /**
622
+ * Looks up the concrete {@link Generator} implementation for a given tool name.
623
+ */
624
+ var GeneratorRegistry = class {
625
+ generators = /* @__PURE__ */ new Map();
626
+ /**
627
+ * Registers a generator implementation under its own {@link Generator.tool}.
628
+ *
629
+ * @param generator - The generator instance to register.
630
+ * @returns `this`, for chaining.
631
+ */
632
+ register(generator) {
633
+ this.generators.set(generator.tool, generator);
634
+ return this;
635
+ }
636
+ /**
637
+ * Resolves the generator implementation for a given tool name.
638
+ *
639
+ * @param tool - The tool name, e.g. `"openapi-generator"`.
640
+ * @returns The registered generator.
641
+ * @throws Error if no generator is registered for that tool.
642
+ */
643
+ resolve(tool) {
644
+ const generator = this.generators.get(tool);
645
+ if (!generator) {
646
+ const available = [...this.generators.keys()].join(", ");
647
+ throw new Error(`No generator implementation registered for tool "${tool}" (available: ${available})`);
648
+ }
649
+ return generator;
650
+ }
651
+ /**
652
+ * All distinct tools currently registered.
653
+ *
654
+ * @returns The registered tool names.
655
+ */
656
+ tools() {
657
+ return [...this.generators.keys()];
658
+ }
659
+ };
660
+ //#endregion
661
+ //#region src/process/exec.ts
662
+ /**
663
+ * Runs a command to completion, streaming its stdio straight through
664
+ * (`inherit`), and rejects if it exits non-zero.
665
+ *
666
+ * This is the async counterpart used for the "one long-running tool" commands
667
+ * (`redocly`, `openapi-generator-cli`, `openapi-typescript`); for short
668
+ * synchronous calls (git plumbing, `npm publish`/`mvn deploy`), see
669
+ * {@link runSync}.
670
+ *
671
+ * @param command - The executable to run.
672
+ * @param args - Arguments to pass to it.
673
+ * @param cwd - The working directory to run it in.
674
+ * @returns A promise that resolves on exit code 0, and rejects otherwise.
675
+ */
676
+ function run(command, args, cwd) {
677
+ return new Promise((resolvePromise, reject) => {
678
+ spawn(command, args, {
679
+ cwd,
680
+ stdio: "inherit",
681
+ shell: true
682
+ }).on("close", (code) => {
683
+ if (code === 0) {
684
+ resolvePromise();
685
+ return;
686
+ }
687
+ reject(/* @__PURE__ */ new Error(`${command} ${args.join(" ")} exited with ${code}`));
688
+ });
689
+ });
690
+ }
691
+ /**
692
+ * Runs a command to completion synchronously, streaming its stdio straight
693
+ * through (`inherit`).
694
+ *
695
+ * @param command - The executable to run.
696
+ * @param args - Arguments to pass to it.
697
+ * @param cwd - The working directory to run it in.
698
+ * @returns The exit status (0 on success).
699
+ */
700
+ function runSync(command, args, cwd) {
701
+ return spawnSync(command, args, {
702
+ cwd,
703
+ stdio: "inherit",
704
+ shell: true
705
+ }).status ?? 1;
706
+ }
707
+ //#endregion
708
+ //#region src/process/resolve-bin.ts
709
+ const require = createRequire(import.meta.url);
710
+ /**
711
+ * Resolves the absolute path to an installed npm package's own CLI entrypoint
712
+ * script, using Node's standard module resolution algorithm - so it works the
713
+ * same way regardless of which package manager (npm/pnpm/yarn) installed CLI
714
+ * and its dependencies, or how deeply they get hoisted. Shelling out to
715
+ * `pnpm exec`/`npx` instead would assume a specific package manager and a
716
+ * particular install layout, which doesn't hold once CLI is just another
717
+ * dependency in someone else's project.
718
+ *
719
+ * @param pkgName - The npm package name, e.g. `"@org/cli"`.
720
+ * @param binName - Which entry to resolve from that package's `bin` field.
721
+ * Defaults to the package's own unscoped name.
722
+ * @returns The absolute path to the resolved bin script.
723
+ * @throws Error if the package or the requested bin entry can't be found.
724
+ */
725
+ function resolveBinPath(pkgName, binName) {
726
+ const pkgJsonPath = require.resolve(`${pkgName}/package.json`);
727
+ const pkgDir = path.dirname(pkgJsonPath);
728
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
729
+ const key = binName ?? pkg.name.split("/").pop();
730
+ const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.[key];
731
+ if (!bin) throw new Error(`Could not resolve a "${key}" bin entry for package "${pkgName}" - is it installed, and does it expose that bin?`);
732
+ return path.join(pkgDir, bin);
733
+ }
734
+ //#endregion
735
+ //#region src/git/git.ts
736
+ /**
737
+ * Execute a git command in a given working directory and return the result.
738
+ *
739
+ * @param args - The command-line arguments to pass to the git command.
740
+ * @param cwd - The working directory in which to execute the git command.
741
+ * @returns The result of the git command.
742
+ *
743
+ * @see {@link GitResult} - The result of executing the git command.
744
+ */
745
+ function git(args, cwd) {
746
+ const result = spawnSync("git", args, {
747
+ cwd,
748
+ encoding: "utf8"
749
+ });
750
+ return {
751
+ status: result.status ?? 1,
752
+ stdout: (result.stdout ?? "").trim(),
753
+ stderr: (result.stderr ?? "").trim()
754
+ };
755
+ }
756
+ /**
757
+ * Check if a remote branch exists in the given repository.
758
+ *
759
+ * @param repoRoot - The root directory of the repository.
760
+ * @param branch - The name of the branch to check.
761
+ * @returns Whether the remote branch exists (true) or not (false).
762
+ */
763
+ function remoteBranchExists(repoRoot, branch) {
764
+ return git([
765
+ "ls-remote",
766
+ "--exit-code",
767
+ "--heads",
768
+ "origin",
769
+ branch
770
+ ], repoRoot).status === 0;
771
+ }
772
+ /**
773
+ * Check if a remote tag exists in the given repository.
774
+ *
775
+ * @param repoRoot - The root directory of the repository.
776
+ * @param tag - The name of the tag to check.
777
+ * @returns Whether the remote tag exists (true) or not (false).
778
+ */
779
+ function tagExists(repoRoot, tag) {
780
+ return git([
781
+ "ls-remote",
782
+ "--exit-code",
783
+ "--tags",
784
+ "origin",
785
+ tag
786
+ ], repoRoot).status === 0;
787
+ }
788
+ /**
789
+ * Fetch a single tag's object from origin into the local repo, without
790
+ * fetching the rest of history/tags.
791
+ *
792
+ * @param repoRoot - The root directory of the repository.
793
+ * @param tag - The name of the tag to fetch.
794
+ * @returns The result of the underlying `git fetch` command.
795
+ */
796
+ function fetchTag(repoRoot, tag) {
797
+ return git([
798
+ "fetch",
799
+ "origin",
800
+ `refs/tags/${tag}:refs/tags/${tag}`,
801
+ "--force"
802
+ ], repoRoot);
803
+ }
804
+ /**
805
+ * Read a single file's content as it existed at a given git tag, without
806
+ * checking out a worktree.
807
+ *
808
+ * Returns `null` (rather than throwing) both when the tag can't be fetched and
809
+ * when the tag exists but doesn't contain the requested file - the latter is
810
+ * expected for tags published before that file was introduced, and callers
811
+ * should treat "unknown" the same as "no mismatch to report".
812
+ *
813
+ * @param repoRoot - The root directory of the repository.
814
+ * @param tag - The tag to read the file from.
815
+ * @param filePath - The path of the file within that tag's tree.
816
+ * @returns The file's content, or `null` if it couldn't be read.
817
+ */
818
+ function readFileAtTag(repoRoot, tag, filePath) {
819
+ if (fetchTag(repoRoot, tag).status !== 0) return null;
820
+ const show = git(["show", `${tag}:${filePath}`], repoRoot);
821
+ return show.status === 0 ? show.stdout.trim() : null;
822
+ }
823
+ /**
824
+ * Require that a git command succeeded, throwing an error with the given
825
+ * message if it did not.
826
+ *
827
+ * @param result - The result of the git command to check.
828
+ * @param message - The error message to throw if the command failed.
829
+ * @throws Error if the git command failed (non-zero exit code).
830
+ */
831
+ function requireOk(result, message) {
832
+ if (result.status !== 0) throw new Error(`${message}: ${result.stderr || result.stdout}`);
833
+ }
834
+ //#endregion
835
+ //#region src/version/version.ts
836
+ /**
837
+ * Resolves the version to stamp onto a single contract's generated SDK
838
+ * artifacts (npm/Maven packages, Go module tags, git branches, etc.).
839
+ *
840
+ * The single source of truth is that contract's own `info.version` field in its
841
+ * `openapi.yaml` - bump it there and every artifact, package, and git tag for
842
+ * that contract picks up the new version on the next release. Versions are
843
+ * resolved independently per contract: two services can be at different
844
+ * versions at the same time.
845
+ *
846
+ * `SDK_VERSION_OVERRIDE` (wired up from the release workflow's manual `version`
847
+ * input) bypasses the spec entirely and stamps every contract with the same
848
+ * given value. It exists for one-off emergency republishes, not routine
849
+ * releases. Routine releases should always go through `info.version`.
850
+ *
851
+ * @param spec - The parsed, bundled OpenAPI document for the contract.
852
+ * @param contractName - The contract name, used only for the error message.
853
+ * @returns The resolved version string (no leading `v`).
854
+ * @throws Error if no override is set and the spec has no `info.version`.
855
+ */
856
+ function resolveVersion(spec, contractName) {
857
+ const override = process.env.SDK_VERSION_OVERRIDE?.trim();
858
+ if (override) return override.replace(/^v/, "");
859
+ const specVersion = spec.info?.version?.trim();
860
+ if (!specVersion) throw new Error(`specs/${contractName}/openapi.yaml is missing "info.version" - set it to a semver value. This field is the single source of truth for the ${contractName} contract's SDK version.`);
861
+ return specVersion.replace(/^v/, "");
862
+ }
863
+ /**
864
+ * Computes a stable content hash of a bundled OpenAPI document's raw JSON text.
865
+ * Stamped alongside `VERSION` into every generated SDK package so
866
+ * `publish-sdk.ts` can tell a genuine no-op republish (same spec, same version)
867
+ * apart from a spec that changed without its `info.version` being bumped.
868
+ *
869
+ * @param raw - The raw bundled spec file contents (JSON text).
870
+ * @returns A `sha256` hex digest of the raw contents.
871
+ */
872
+ function hashSpec(raw) {
873
+ return createHash("sha256").update(raw).digest("hex");
874
+ }
875
+ //#endregion
876
+ //#region src/readme/default-templates.ts
877
+ /**
878
+ * Renders a sensible default `README.md` for a generated SDK package, used
879
+ * whenever the artifact has no custom `readme:` template configured. Covers
880
+ * every built-in `lang`/`kind` combination; anything more specific (house
881
+ * install instructions, extra usage notes) belongs in a custom template
882
+ * instead - see `readme-renderer.ts`. Registry URLs and branch/tag names
883
+ * come from the artifact's resolved `publishing:` config, so a repo that
884
+ * overrides its registry sees that reflected here automatically.
885
+ *
886
+ * @param args - The contract, artifact, version, and github/vars coordinates
887
+ * to render for.
888
+ * @returns The rendered README content.
889
+ */
890
+ function renderDefaultReadme({ contract, artifact, version, github, vars }) {
891
+ return `${`# ${contract.title} - ${label(artifact)}
892
+
893
+ > Generated from \`${contract.entrypointRelative}\` in [${github.owner}/${github.repo}](https://github.com/${github.owner}/${github.repo}).
894
+ > Do not edit by hand - this package is regenerated and republished on every release.
895
+
896
+ Version: \`${version}\`
897
+ Source branch: \`${artifact.branch}\`
898
+ `}\n${body(contract, artifact, version, github, vars)}\n`;
899
+ }
900
+ function label(artifact) {
901
+ return `${{
902
+ typescript: "TypeScript",
903
+ go: "Go",
904
+ java: "Java"
905
+ }[artifact.lang]} ${artifact.kind === "client" ? "Client SDK" : artifact.lang === "typescript" ? "Server Types" : "Server Stubs"}`;
906
+ }
907
+ function body(contract, artifact, version, github, vars) {
908
+ switch (`${artifact.lang}-${artifact.kind}`) {
909
+ case "typescript-client": return tsClient(artifact, version);
910
+ case "typescript-server": return tsServer(artifact, version);
911
+ case "go-client": return goClient(contract, artifact, github, vars);
912
+ case "go-server": return goServer(artifact);
913
+ case "java-client": return javaClient(artifact, version);
914
+ case "java-server": return javaServer(artifact, version);
915
+ default: return "";
916
+ }
917
+ }
918
+ function tsClient(artifact, version) {
919
+ return `## Install
920
+
921
+ \`\`\`bash
922
+ npm config set <scope>:registry ${artifact.publishing.npmRegistry}
923
+ npm install ${artifact.package}@${version}
924
+ \`\`\`
925
+
926
+ (Requires an authenticated \`.npmrc\` with a token for that registry.)
927
+
928
+ ## Usage
929
+
930
+ \`\`\`ts
931
+ import { Configuration, DefaultApi } from "${artifact.package}";
932
+
933
+ const api = new DefaultApi(
934
+ new Configuration({ basePath: "https://api.your-domain.com" }),
935
+ );
936
+
937
+ const result = await api.someOperation();
938
+ \`\`\`
939
+ `;
940
+ }
941
+ function tsServer(artifact, version) {
942
+ return `## Install
943
+
944
+ \`\`\`bash
945
+ npm config set <scope>:registry ${artifact.publishing.npmRegistry}
946
+ npm install --save-dev ${artifact.package}@${version}
947
+ \`\`\`
948
+
949
+ (Requires an authenticated \`.npmrc\` with a token for that registry.)
950
+
951
+ ## Usage
952
+
953
+ \`\`\`ts
954
+ import type { components, operations } from "${artifact.package}";
955
+
956
+ type LoginResponses = operations["login"]["responses"];
957
+
958
+ // Example: an Express handler typed against the contract
959
+ app.post("/login", (req, res) => {
960
+ const body = req.body as components["schemas"]["LoginRequest"];
961
+ const response: LoginResponses[200]["content"]["application/json"] = {
962
+ // ...
963
+ };
964
+ res.json(response);
965
+ });
966
+ \`\`\`
967
+ `;
968
+ }
969
+ function goClient(contract, artifact, github, vars) {
970
+ const exampleTag = renderArtifactTag(artifact, contract.name, "<version>", github, vars);
971
+ return `## Install
972
+
973
+ Go has no package registry, so this module is pulled directly from its
974
+ publishing branch:
975
+
976
+ \`\`\`bash
977
+ go get ${artifact.goModule}@${artifact.branch}
978
+ \`\`\`
979
+
980
+ To pin an exact release instead of the branch head, use the matching tag:
981
+
982
+ \`\`\`bash
983
+ go get ${artifact.goModule}@${exampleTag}
984
+ \`\`\`
985
+
986
+ ## Usage
987
+
988
+ \`\`\`go
989
+ import (
990
+ "context"
991
+
992
+ ${artifact.goPackageName} "${artifact.goModule}"
993
+ )
994
+
995
+ func main() {
996
+ cfg := ${artifact.goPackageName}.NewConfiguration()
997
+ client := ${artifact.goPackageName}.NewAPIClient(cfg)
998
+
999
+ resp, _, err := client.DefaultAPI.SomeOperation(context.Background()).Execute()
1000
+ _ = resp
1001
+ _ = err
1002
+ }
1003
+ \`\`\`
1004
+ `;
1005
+ }
1006
+ function goServer(artifact) {
1007
+ return `## Install
1008
+
1009
+ \`\`\`bash
1010
+ go get ${artifact.goModule}@${artifact.branch}
1011
+ \`\`\`
1012
+
1013
+ ## Usage
1014
+
1015
+ Implement the generated \`${artifact.goPackageName}.*ApiServicer\` interfaces and
1016
+ wire them into the generated router:
1017
+
1018
+ \`\`\`go
1019
+ router := ${artifact.goPackageName}.NewRouter(
1020
+ ${artifact.goPackageName}.NewSomeApiController(yourServiceImpl),
1021
+ )
1022
+ \`\`\`
1023
+ `;
1024
+ }
1025
+ function javaClient(artifact, version) {
1026
+ return `## Install (Maven)
1027
+
1028
+ \`\`\`xml
1029
+ <dependency>
1030
+ <groupId>${artifact.maven?.groupId}</groupId>
1031
+ <artifactId>${artifact.maven?.artifactId}</artifactId>
1032
+ <version>${version}</version>
1033
+ </dependency>
1034
+ \`\`\`
1035
+
1036
+ Add the repository to your \`settings.xml\` (or \`pom.xml\`) with credentials
1037
+ for that repository:
1038
+
1039
+ \`\`\`xml
1040
+ <repository>
1041
+ <id>${artifact.publishing.mavenRepositoryId}</id>
1042
+ <url>${artifact.publishing.mavenRepositoryUrl}</url>
1043
+ </repository>
1044
+ \`\`\`
1045
+
1046
+ ## Usage
1047
+
1048
+ Generated with \`library=restclient\` - Spring's \`RestClient\`, the current
1049
+ recommended synchronous HTTP client for Spring apps:
1050
+
1051
+ \`\`\`java
1052
+ @Configuration
1053
+ public class SomeServiceClientConfig {
1054
+
1055
+ @Bean
1056
+ public ApiClient someServiceApiClient(RestClient.Builder builder) {
1057
+ ApiClient client = new ApiClient(builder.build());
1058
+ client.setBasePath("https://api.your-domain.com");
1059
+ return client;
1060
+ }
1061
+
1062
+ @Bean
1063
+ public DefaultApi someServiceApi(ApiClient someServiceApiClient) {
1064
+ return new DefaultApi(someServiceApiClient);
1065
+ }
1066
+ }
1067
+ \`\`\`
1068
+ `;
1069
+ }
1070
+ function javaServer(artifact, version) {
1071
+ return `## Install (Maven)
1072
+
1073
+ \`\`\`xml
1074
+ <dependency>
1075
+ <groupId>${artifact.maven?.groupId}</groupId>
1076
+ <artifactId>${artifact.maven?.artifactId}</artifactId>
1077
+ <version>${version}</version>
1078
+ </dependency>
1079
+ \`\`\`
1080
+
1081
+ Add the repository to your \`settings.xml\` (or \`pom.xml\`) with credentials
1082
+ for that repository:
1083
+
1084
+ \`\`\`xml
1085
+ <repository>
1086
+ <id>${artifact.publishing.mavenRepositoryId}</id>
1087
+ <url>${artifact.publishing.mavenRepositoryUrl}</url>
1088
+ </repository>
1089
+ \`\`\`
1090
+
1091
+ ## Usage
1092
+
1093
+ This artifact only contains the generated Spring \`@RestController\` interfaces
1094
+ (\`interfaceOnly=true\`) - implement them in your service:
1095
+
1096
+ \`\`\`java
1097
+ @RestController
1098
+ public class SomeController implements SomeApi {
1099
+ // interface methods generated from the OpenAPI contract
1100
+ }
1101
+ \`\`\`
1102
+ `;
1103
+ }
1104
+ //#endregion
1105
+ //#region src/readme/readme-renderer.ts
1106
+ /**
1107
+ * Renders the root-level `README.md` for a generated SDK package.
1108
+ *
1109
+ * If the artifact has a `readme:` path configured (resolved at config-load time
1110
+ * to `artifact.readmeTemplate`), that file is read and interpolated with the
1111
+ * same `{...}` placeholder engine naming templates use - `{service}`,
1112
+ * `{title}`, `{version}`, `{vars.*}`, `{github.owner}`, `{github.repo}`, plus
1113
+ * `{artifact.*}` (id/lang/kind/package/goModule/goPackageName/maven.groupId/
1114
+ * maven.artifactId/branch/tag/npmRegistry/mavenRepositoryUrl). Otherwise,
1115
+ * falls back to a built-in default template for the artifact's language/kind.
1116
+ *
1117
+ * @param args - The contract, artifact, version, and github/vars context to
1118
+ * render for.
1119
+ * @returns The rendered README content.
1120
+ */
1121
+ async function renderReadme(args) {
1122
+ if (!args.artifact.readmeTemplate) return renderDefaultReadme(args);
1123
+ return interpolate(await readFile(args.artifact.readmeTemplate, "utf8"), buildTemplateContext({
1124
+ service: args.contract.name,
1125
+ title: args.contract.title,
1126
+ version: args.version,
1127
+ github: args.github,
1128
+ vars: args.vars,
1129
+ artifact: {
1130
+ id: args.artifact.id,
1131
+ lang: args.artifact.lang,
1132
+ kind: args.artifact.kind,
1133
+ package: args.artifact.package,
1134
+ goModule: args.artifact.goModule,
1135
+ goPackageName: args.artifact.goPackageName,
1136
+ maven: args.artifact.maven,
1137
+ branch: args.artifact.branch,
1138
+ tag: renderArtifactTag(args.artifact, args.contract.name, args.version, args.github, args.vars),
1139
+ npmRegistry: args.artifact.publishing.npmRegistry,
1140
+ mavenRepositoryUrl: args.artifact.publishing.mavenRepositoryUrl
1141
+ }
1142
+ }));
1143
+ }
1144
+ //#endregion
1145
+ //#region src/redocly/redocly-sync.ts
1146
+ const GENERATED_HEADER = "# AUTO-GENERATED by Seagull from its config and 'redocly.base.yaml'.\n# Do not edit directly, edit those two files instead (this file is\n# regenerated on every lint/bundle/generate run).\n\n";
1147
+ /**
1148
+ * Regenerates `redocly.yaml`'s `apis:` section from the resolved CLI config,
1149
+ * merging it with the hand-authored `extends`/`rules` in `redocly.base.yaml`.
1150
+ *
1151
+ * The CLI config stays the single source of truth for which APIs exist and
1152
+ * where their TypeScript server types land, instead of that being duplicated
1153
+ * by hand into `redocly.yaml`.
1154
+ *
1155
+ * Called at the start of every command that shells out to `redocly` or
1156
+ * `openapi-typescript` (both read `redocly.yaml` directly), so it's always
1157
+ * up to date before those tools run.
1158
+ *
1159
+ * @param config - The resolved CLI config.
1160
+ */
1161
+ async function syncRedoclyConfig(config) {
1162
+ const basePath = path.join(config.rootDir, "redocly.base.yaml");
1163
+ const base = parse(await readFile(basePath, "utf8"));
1164
+ const apis = Object.fromEntries(config.contracts.map((contract) => {
1165
+ const typesArtifact = contract.artifacts.find((artifact) => artifact.tool === "openapi-typescript");
1166
+ return [`${contract.name}@v1`, {
1167
+ root: contract.entrypointRelative,
1168
+ ...typesArtifact ? { "x-openapi-ts": { output: path.join(path.relative(config.rootDir, typesArtifact.outputDir), "index.d.ts") } } : {}
1169
+ }];
1170
+ }));
1171
+ const final = {
1172
+ extends: base.extends,
1173
+ apis,
1174
+ rules: base.rules
1175
+ };
1176
+ await writeFile(path.join(config.rootDir, "redocly.yaml"), GENERATED_HEADER + stringify(final));
1177
+ }
1178
+ //#endregion
1179
+ //#region src/generators/openapi-generator-cli/patchers/go-module.patcher.ts
1180
+ /**
1181
+ * Patches the `go.mod` file in a generated Go SDK package to set the correct
1182
+ * module path (`openapi-generator-cli` has no way to be told this up front for
1183
+ * every template).
1184
+ */
1185
+ var GoModulePatcher = class {
1186
+ async patch({ artifact }) {
1187
+ if (!artifact.goModule) return;
1188
+ const moduleFile = path.join(artifact.outputDir, "go.mod");
1189
+ try {
1190
+ const contents = await readFile(moduleFile, "utf8");
1191
+ await writeFile(moduleFile, contents.replace(/^module .*$/m, `module ${artifact.goModule}`));
1192
+ } catch {}
1193
+ }
1194
+ };
1195
+ //#endregion
1196
+ //#region src/generators/openapi-generator-cli/patchers/maven.patcher.ts
1197
+ /**
1198
+ * Patches the `pom.xml` file in a generated Java SDK package to set the correct
1199
+ * version and distribution management information for publishing, using the
1200
+ * artifact's resolved `publishing:` config rather than a hardcoded registry.
1201
+ */
1202
+ var MavenPomPatcher = class {
1203
+ async patch({ artifact, version }) {
1204
+ const pomFile = path.join(artifact.outputDir, "pom.xml");
1205
+ try {
1206
+ let pom = await readFile(pomFile, "utf8");
1207
+ pom = pom.replace(/<version>[^<]*<\/version>/, `<version>${version}</version>`);
1208
+ if (!pom.includes("<distributionManagement>")) pom = pom.replace("</project>", [
1209
+ " <distributionManagement>",
1210
+ " <repository>",
1211
+ ` <id>${artifact.publishing.mavenRepositoryId}</id>`,
1212
+ ` <url>${artifact.publishing.mavenRepositoryUrl}</url>`,
1213
+ " </repository>",
1214
+ " </distributionManagement>",
1215
+ "</project>"
1216
+ ].join("\n"));
1217
+ await writeFile(pomFile, pom);
1218
+ } catch {}
1219
+ }
1220
+ };
1221
+ //#endregion
1222
+ //#region src/generators/openapi-generator-cli/patchers/npm.patcher.ts
1223
+ /**
1224
+ * Patches the `package.json` file in a generated TypeScript SDK package (the
1225
+ * client target - `openapi-generator-cli` produces its own `package.json`, this
1226
+ * just fills in the version and publishing metadata) to set the correct version
1227
+ * and repository information, using the artifact's resolved `publishing:`
1228
+ * config rather than a hardcoded registry.
1229
+ */
1230
+ var NpmPackagePatcher = class {
1231
+ async patch({ artifact, version }) {
1232
+ const pkgFile = path.join(artifact.outputDir, "package.json");
1233
+ const pkgData = await readFile(pkgFile, "utf8");
1234
+ const pkg = JSON.parse(pkgData);
1235
+ pkg.version = version;
1236
+ pkg.repository = {
1237
+ type: "git",
1238
+ url: `git+${artifact.publishing.repositoryUrl}.git`
1239
+ };
1240
+ pkg.publishConfig = {
1241
+ registry: artifact.publishing.npmRegistry,
1242
+ access: artifact.publishing.npmAccess
1243
+ };
1244
+ await writeFile(pkgFile, JSON.stringify(pkg, null, 2));
1245
+ }
1246
+ };
1247
+ //#endregion
1248
+ //#region src/generators/openapi-generator-cli/openapi-generator-cli.generator.ts
1249
+ /**
1250
+ * Additional-properties that are fully derivable from an artifact's own
1251
+ * `package`/`goPackageName`/`maven` fields - conventional openapi-generator
1252
+ * knobs (`npmName`, `groupId`, ...) that would otherwise have to be duplicated
1253
+ * by hand in every generator's `additionalProperties:` block, in lockstep with
1254
+ * those same fields. Tool-specific tuning that isn't derivable this way
1255
+ * (`library=restclient`, `withGoMod`, ...) still lives in
1256
+ * `additionalProperties:` and is layered on top of these.
1257
+ *
1258
+ * @param artifact - The resolved artifact to derive properties for.
1259
+ * @returns The derived additional-properties, before the artifact's own
1260
+ * `additionalProperties` are layered on top.
1261
+ */
1262
+ function deriveAdditionalProperties(artifact) {
1263
+ if (artifact.lang === "typescript" && artifact.package) return { npmName: artifact.package };
1264
+ if (artifact.lang === "go" && artifact.goPackageName) return { packageName: artifact.goPackageName };
1265
+ if (artifact.lang === "java" && artifact.maven) {
1266
+ const invokerPackage = `${artifact.maven.groupId}.${artifact.kind}`;
1267
+ return {
1268
+ groupId: artifact.maven.groupId,
1269
+ artifactId: artifact.maven.artifactId,
1270
+ invokerPackage,
1271
+ apiPackage: `${invokerPackage}.api`,
1272
+ modelPackage: `${invokerPackage}.model`
1273
+ };
1274
+ }
1275
+ return {};
1276
+ }
1277
+ /**
1278
+ * Renders an artifact's additional-properties (derived + explicit, explicit
1279
+ * wins on conflicts) as the `key=value,key=value` string
1280
+ * `openapi-generator-cli --additional-properties` expects.
1281
+ *
1282
+ * @param artifact - The resolved artifact.
1283
+ * @returns The rendered `--additional-properties` value.
1284
+ */
1285
+ function buildAdditionalPropertiesArg(artifact) {
1286
+ const merged = {
1287
+ ...deriveAdditionalProperties(artifact),
1288
+ ...artifact.additionalProperties
1289
+ };
1290
+ return Object.entries(merged).map(([key, value]) => `${key}=${String(value)}`).join(",");
1291
+ }
1292
+ /**
1293
+ * Wraps `openapi-generator-cli` - the single tool implementation behind every
1294
+ * `-g` template (`typescript-fetch`, `go`, `go-server`, `java`, `spring`, ...),
1295
+ * regardless of language. Language-specific output patching is delegated to a
1296
+ * {@link Patcher}, selected by `artifact.lang`.
1297
+ */
1298
+ var OpenApiGeneratorCli = class extends Generator {
1299
+ tool = "openapi-generator";
1300
+ patchers = {
1301
+ go: new GoModulePatcher(),
1302
+ typescript: new NpmPackagePatcher(),
1303
+ java: new MavenPomPatcher()
1304
+ };
1305
+ async generate(ctx) {
1306
+ const { artifact, rootDir, specInputPath } = ctx;
1307
+ if (!artifact.generator) throw new Error(`Artifact "${artifact.id}" uses tool "openapi-generator" but has no "generator" value`);
1308
+ await mkdir(artifact.outputDir, { recursive: true });
1309
+ await run("node", [
1310
+ resolveBinPath("@openapitools/openapi-generator-cli", "openapi-generator-cli"),
1311
+ "generate",
1312
+ "-i",
1313
+ specInputPath,
1314
+ "-g",
1315
+ artifact.generator,
1316
+ "-o",
1317
+ artifact.outputDir,
1318
+ `--additional-properties=${buildAdditionalPropertiesArg(artifact)}`
1319
+ ], rootDir);
1320
+ await this.patchers[artifact.lang]?.patch(ctx);
1321
+ }
1322
+ };
1323
+ //#endregion
1324
+ //#region src/generators/openapi-typescript/openapi-typescript.generator.ts
1325
+ /**
1326
+ * Wraps `openapi-typescript`. Unlike `openapi-generator-cli`, it isn't invoked
1327
+ * once per artifact - it reads `redocly.yaml`'s `apis:` map (kept in sync with
1328
+ * the CLI config by `core/redocly/redocly-sync.ts`) and writes every contract's
1329
+ * `index.d.ts` to its configured `x-openapi-ts.output` path in a single run,
1330
+ * so that single global invocation happens once in {@link prepare}.
1331
+ * {@link generate} then only has to write each artifact's package.json` -
1332
+ * `openapi-typescript` emits `index.d.ts` alone, with no package manifest of
1333
+ * its own to patch.
1334
+ */
1335
+ var OpenApiTypescriptGenerator = class extends Generator {
1336
+ tool = "openapi-typescript";
1337
+ async prepare({ rootDir, entries }) {
1338
+ await Promise.all(entries.map((entry) => mkdir(entry.artifact.outputDir, { recursive: true })));
1339
+ await run("node", [resolveBinPath("openapi-typescript", "openapi-typescript")], rootDir);
1340
+ }
1341
+ async generate({ contract, artifact, version }) {
1342
+ const pkg = {
1343
+ name: artifact.package,
1344
+ version,
1345
+ description: `Types-only OpenAPI contract for the ${contract.name} service.`,
1346
+ types: "./index.d.ts",
1347
+ files: ["index.d.ts"],
1348
+ license: "MIT",
1349
+ repository: {
1350
+ type: "git",
1351
+ url: `git+${artifact.publishing.repositoryUrl}.git`
1352
+ },
1353
+ publishConfig: {
1354
+ registry: artifact.publishing.npmRegistry,
1355
+ access: artifact.publishing.npmAccess
1356
+ }
1357
+ };
1358
+ await writeFile(path.join(artifact.outputDir, "package.json"), JSON.stringify(pkg, null, 2));
1359
+ }
1360
+ };
1361
+ //#endregion
1362
+ export { CONFIG_FILENAMES, CONFIG_SCHEMA_VERSION, Generator, GeneratorRegistry, OpenApiGeneratorCli, OpenApiTypescriptGenerator, git, hashSpec, loadConfig, readFileAtTag, remoteBranchExists, renderArtifactTag, renderReadme, requireOk, resolveBinPath, resolveConfigPath, resolveVersion, run, runSync, syncRedoclyConfig, tagExists };
1363
+
1364
+ //# sourceMappingURL=index.mjs.map