@octalmesh/seagull-cli 0.0.2 → 0.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,49 @@
1
+ # @octalmesh/seagull-cli
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - ec5c83b: Add LICENSE file to individual npm packages.
8
+ - 71ab054: Expand supported Node.js and pnpm version ranges in `engines` to improve
9
+ compatibility.
10
+ - Updated dependencies [ec5c83b]
11
+ - Updated dependencies [71ab054]
12
+ - @octalmesh/seagull-core@0.1.1
13
+ - @octalmesh/seagull-docs@0.1.1
14
+
15
+ ## 0.1.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 3e1d383: Restructured into a monorepo of internal, private packages
20
+ (`@octalmesh/seagull-core`, `@octalmesh/seagull-cli`, `@octalmesh/seagull-docs`)
21
+ bundled into the single published `@octalmesh/seagull` package - nothing
22
+ changes for consumers of the CLI itself.
23
+
24
+ - `seagull.yaml` now requires a top-level `configVersion: 1` field, decoupled
25
+ from the npm package's own version - it only changes when the config
26
+ schema itself changes in a breaking way. **Action required:** add
27
+ `configVersion: 1` to existing configs.
28
+ - 3e1d383: `paths.specFormat` lets bundled specs be written as `yaml` as well as (or
29
+ instead of) `json` - Redocly's `bundle` already infers its output format
30
+ from the file extension on its own, seagull just wasn't giving it the
31
+ choice. Accepts a single value (`specFormat: yaml`) or a list
32
+ (`specFormat: [json, yaml]`) to bundle into more than one format at once;
33
+ defaults to `json`, unchanged from before. When more than one format is
34
+ configured, the first one listed is the "primary" format SDK generation,
35
+ version/hash resolution, and the docs site actually read from - the rest
36
+ are bundled as additional static artifacts alongside it.
37
+
38
+ `seagull.yaml` (and `redocly.base.yaml`) now also support the YAML `<<:
39
+ *anchor` merge key, the same pattern used in Docker Compose files, so a
40
+ config can define a `defaults: &defaults {...}` block once and reuse it
41
+ across e.g. multiple `generators:` entries instead of repeating the same
42
+ fields everywhere.
43
+
44
+ ### Patch Changes
45
+
46
+ - Updated dependencies [3e1d383]
47
+ - Updated dependencies [3e1d383]
48
+ - @octalmesh/seagull-core@0.1.0
49
+ - @octalmesh/seagull-docs@0.1.0
package/README.md CHANGED
@@ -1,29 +1,166 @@
1
- # @octalmesh/seagull-cli
1
+ <!--suppress HtmlDeprecatedAttribute, HtmlUnknownTarget -->
2
+ <h1 id="title" align="center">@octalmesh/seagull-cli</h1>
2
3
 
3
- **Internal package** - not published to npm on its own. This holds every
4
- pipeline command (`lint`, `bundle`, `generate`, `docs generate`, `docs serve`,
5
- `publish sdk`, `publish registries`, `clean`) and the `commander` program
6
- that wires them up (`createProgram()`), bundled straight into
7
- [`@octalmesh/seagull`](../..) at build time - that package owns the actual
8
- `seagull` executable and reads its own `package.json` for `--version`/
9
- `--help` text, then calls `createProgram()` from here to build the rest.
4
+ <div align="center">
5
+ <!-- Version Badge -->
6
+ <a rel="noopener noreferrer" href="https://npmjs.com/package/@octalmesh/seagull-cli">
7
+ <picture>
8
+ <source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/npm/v/@octalmesh/seagull-cli?style=for-the-badge&label=Version&color=363636&labelColor=464646&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEgNy44di01UTEuMiAxLjIgMi44IDFoNXEuNyAwIDEuMi41bDYuMyA2LjNhMiAyIDAgMCAxIDAgMi40bC01IDVhMiAyIDAgMCAxLTIuNSAwTDEuNSA5QTIgMiAwIDAgMSAxIDcuOG0xLjUgMFY4bDYuMyA2LjJoLjRsNS01di0uNEw4IDIuNmwtLjItLjFoLTVsLS4zLjNaTTYgNWExIDEgMCAxIDEgMCAyIDEgMSAwIDAgMSAwLTIiLz48L3N2Zz4=" />
9
+ <img alt="Version" src="https://img.shields.io/npm/v/@octalmesh/seagull-cli?style=for-the-badge&label=Version&color=fff&labelColor=eaeaea&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iIzM2MzYzNiIgZD0iTTEgNy44di01UTEuMiAxLjIgMi44IDFoNXEuNyAwIDEuMi41bDYuMyA2LjNhMiAyIDAgMCAxIDAgMi40bC01IDVhMiAyIDAgMCAxLTIuNSAwTDEuNSA5QTIgMiAwIDAgMSAxIDcuOG0xLjUgMFY4bDYuMyA2LjJoLjRsNS01di0uNEw4IDIuNmwtLjItLjFoLTVsLS4zLjNaTTYgNWExIDEgMCAxIDEgMCAyIDEgMSAwIDAgMSAwLTIiLz48L3N2Zz4=" />
10
+ </picture>
11
+ </a>
12
+ <!-- NPM Downloads Badge -->
13
+ <a rel="noopener noreferrer" href="https://www.npmjs.com/package/@octalmesh/seagull-cli">
14
+ <picture>
15
+ <source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/npm/dm/@octalmesh/seagull-cli?style=for-the-badge&logo=npm&color=363636&labelColor=464646" />
16
+ <img alt="NPM Downloads" src="https://img.shields.io/npm/dm/@octalmesh/seagull-cli?style=for-the-badge&logo=npm&logoColor=464646&color=fff&labelColor=eaeaea" />
17
+ </picture>
18
+ </a>
19
+ <!-- License Badge -->
20
+ <a rel="noopener noreferrer" href="LICENSE.md">
21
+ <picture>
22
+ <source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/github/license/OctalMesh/Seagull?style=for-the-badge&color=363636&labelColor=464646&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTguOC44VjJoMXEuMyAwIC44LjJsMS4zLjhoMi40YS44LjggMCAwIDEgMCAxLjVoLS41TDE2IDkuMmExIDEgMCAwIDEtLjEuOGwtLjUtLjUuNS41di4xbC0uOC40cS0uNi41LTIgLjVhNSA1IDAgMCAxLTItLjVsLS43LS40YTEgMSAwIDAgMS0uMi0xbDItNC42cS0uNiAwLTEtLjJMMTAgMy41SDguN1YxM2gyLjZhLjguOCAwIDAgMSAwIDEuNUg0LjhhLjguOCAwIDAgMSAwLTEuNWgyLjVWMy41SDZMNSA0LjNsLTEgLjIgMiA0LjdhMSAxIDAgMCAxLS4xLjhsLS41LS41LjUuNXYuMWwtLjguNHEtLjYuNS0yIC41YTUgNSAwIDAgMS0yLS41bC0uNy0uNEExIDEgMCAwIDEgMCA5bDItNC42aC0uNGEuOC44IDAgMCAxIDAtMS41aDIuNGwxLjMtLjguOS0uMmgxVi44YS44LjggMCAwIDEgMS41IDBtMi45IDguNHEuNC4zIDEuMy4zYy45IDAgMS0uMSAxLjMtLjNMMTMgNi4zWm0tMTAgMHEuNC4zIDEuMy4zYy45IDAgMS0uMSAxLjMtLjNMMyA2LjNaIi8+PC9zdmc+" />
23
+ <img alt="License" src="https://img.shields.io/github/license/OctalMesh/Seagull?style=for-the-badge&color=fff&labelColor=eaeaea&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iIzM2MzYzNiIgZD0iTTguOC44VjJoMXEuMyAwIC44LjJsMS4zLjhoMi40YS44LjggMCAwIDEgMCAxLjVoLS41TDE2IDkuMmExIDEgMCAwIDEtLjEuOGwtLjUtLjUuNS41di4xbC0uOC40cS0uNi41LTIgLjVhNSA1IDAgMCAxLTItLjVsLS43LS40YTEgMSAwIDAgMS0uMi0xbDItNC42cS0uNiAwLTEtLjJMMTAgMy41SDguN1YxM2gyLjZhLjguOCAwIDAgMSAwIDEuNUg0LjhhLjguOCAwIDAgMSAwLTEuNWgyLjVWMy41SDZMNSA0LjNsLTEgLjIgMiA0LjdhMSAxIDAgMCAxLS4xLjhsLS41LS41LjUuNXYuMWwtLjguNHEtLjYuNS0yIC41YTUgNSAwIDAgMS0yLS41bC0uNy0uNEExIDEgMCAwIDEgMCA5bDItNC42aC0uNGEuOC44IDAgMCAxIDAtMS41aDIuNGwxLjMtLjguOS0uMmgxVi44YS44LjggMCAwIDEgMS41IDBtMi45IDguNHEuNC4zIDEuMy4zYy45IDAgMS0uMSAxLjMtLjNMMTMgNi4zWm0tMTAgMHEuNC4zIDEuMy4zYy45IDAgMS0uMSAxLjMtLjNMMyA2LjNaIi8+PC9zdmc+" />
24
+ </picture>
25
+ </a>
26
+ </div>
10
27
 
11
- See the [main README](https://github.com/OctalMesh/Seagull#readme) for the
12
- command reference and full config docs.
28
+ <div align="center">
29
+ <h6>
30
+ <a rel="noopener noreferrer" href="../../README.md">Main Readme</a>
31
+ ·
32
+ <a rel="noopener noreferrer" href="../core/README.md">seagull-core</a>
33
+ ·
34
+ <a rel="noopener noreferrer" href="../docs/README.md">seagull-docs</a>
35
+ </h6>
36
+ </div>
13
37
 
14
- ## What lives here
38
+ Every pipeline command (`lint`, `bundle`, `generate`, `docs generate`,
39
+ `docs serve`, `publish sdk`, `publish registries`, `clean`) plus the
40
+ `commander` program that wires them into the `seagull` CLI (`createProgram()`).
15
41
 
16
- - `commands/` - one function per pipeline step, each taking a
42
+ Published independently for anyone who wants to script against these commands
43
+ directly without the docs bundle. Most people should install
44
+ [`@octalmesh/seagull`](../..) instead - that package owns the actual `seagull`
45
+ executable (reads its own `package.json` for `--version`/`--help` text, then
46
+ calls `createProgram()` from here to build the rest).
47
+
48
+ <div align="center">
49
+ <h2 id="what-lives-here">What lives here</h2>
50
+ </div>
51
+
52
+ - **`commands/`** - one function per pipeline step, each taking a
17
53
  `ResolvedConfig` (from [`@octalmesh/seagull-core`](../core)) and returning
18
- `Promise<void>`.
19
- - `program.ts` - `createProgram(metadata)`, a pure factory building the
54
+ `Promise<void>`. Every one of these is exported and directly callable -
55
+ the CLI commands are thin `commander` wrappers around them, nothing more.
56
+
57
+ - `commands/generate-docs.ts` and `commands/serve-docs.ts` are thin
58
+ delegations to [`@octalmesh/seagull-docs`](../docs) - the actual docs-site
59
+ implementation lives there, kept behind that package's own stable boundary.
60
+
61
+ - **`program.ts`** - `createProgram(metadata)`, a pure factory building the
20
62
  `commander` `Command` tree. No side effects, no `process.argv` parsing -
21
- the actual entrypoint (`@octalmesh/seagull`'s `src/cli.ts`) owns that.
63
+ the actual entrypoint (`@octalmesh/seagull`'s `src/cli.ts`) owns that, so
64
+ this package stays testable and embeddable on its own.
65
+
66
+ <div align="center">
67
+ <h2 id="using-a-command-directly">Using a command directly</h2>
68
+ </div>
69
+
70
+ ```ts
71
+ import { loadConfig } from "@octalmesh/seagull-core";
72
+ import { bundleCommand, generateSdkCommand } from "@octalmesh/seagull-cli";
73
+
74
+ const config = loadConfig("./seagull.yaml");
22
75
 
23
- `commands/generate-docs.ts` and `commands/serve-docs.ts` are thin
24
- delegations to [`@octalmesh/seagull-docs`](../docs) - the actual docs-site
25
- implementation lives there.
76
+ await bundleCommand(config);
77
+ await generateSdkCommand(config);
78
+ ```
26
79
 
27
- ## License
80
+ See [`examples/sdk`](../../examples/sdk) in the repo root for a complete
81
+ walkthrough (importing from the `@octalmesh/seagull` meta-package instead, which
82
+ most scripts should prefer).
28
83
 
29
- MIT
84
+ <div align="center">
85
+ <!--
86
+ =====================
87
+ FOOTER
88
+ =====================
89
+ -->
90
+ <h1></h1>
91
+ <br />
92
+ <!-- OctalMesh Logo -->
93
+ <a rel="noopener noreferrer" target="_blank" href="https://octalmesh.com">
94
+ <picture>
95
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/logo/svg/octal_mesh_center.svg" />
96
+ <img alt="OctalMesh" src="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/logo/svg/octal_mesh_center_white.svg" height="48" />
97
+ </picture>
98
+ </a>
99
+ <br /><br />
100
+ <!-- Socials -->
101
+ <div>
102
+ <!-- Telegram Badge -->
103
+ <a rel="noopener noreferrer" target="_blank" href="https://octalmesh.com/telegram">
104
+ <picture>
105
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/icon/svg/telegram.svg" />
106
+ <img alt="Telegram" src="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/icon/svg/telegram_white.svg" width="48" />
107
+ </picture>
108
+ </a>
109
+ &nbsp;
110
+ <!-- YouTube Badge -->
111
+ <a rel="noopener noreferrer" target="_blank" href="https://octalmesh.com/youtube">
112
+ <picture>
113
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/icon/svg/youtube.svg" />
114
+ <img alt="YouTube" src="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/icon/svg/youtube_white.svg" width="48" />
115
+ </picture>
116
+ </a>
117
+ &nbsp;
118
+ <!-- TikTok Badge -->
119
+ <a rel="noopener noreferrer" target="_blank" href="https://octalmesh.com/tiktok">
120
+ <picture>
121
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/icon/svg/tiktok.svg" />
122
+ <img alt="TikTok" src="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/icon/svg/tiktok_white.svg" width="48" />
123
+ </picture>
124
+ </a>
125
+ &nbsp;
126
+ <!-- Instagram Badge -->
127
+ <a rel="noopener noreferrer" target="_blank" href="https://octalmesh.com/instagram">
128
+ <picture>
129
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/icon/svg/instagram.svg" />
130
+ <img alt="Instagram" src="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/icon/svg/instagram_white.svg" width="48" />
131
+ </picture>
132
+ </a>
133
+ &nbsp;
134
+ <!-- X Badge -->
135
+ <a rel="noopener noreferrer" target="_blank" href="https://octalmesh.com/x">
136
+ <picture>
137
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/icon/svg/x.svg" />
138
+ <img alt="X" src="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/icon/svg/x_white.svg" width="48" />
139
+ </picture>
140
+ </a>
141
+ &nbsp;
142
+ <!-- Reddit Badge -->
143
+ <a rel="noopener noreferrer" target="_blank" href="https://octalmesh.com/reddit">
144
+ <picture>
145
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/icon/svg/reddit.svg" />
146
+ <img alt="Reddit" src="https://raw.githubusercontent.com/OctalMesh/OctalDesign/release/assets/icon/svg/reddit_white.svg" width="48" />
147
+ </picture>
148
+ </a>
149
+ </div>
150
+ </div>
151
+ <h6>
152
+ <div align="center">
153
+ • • •
154
+ <br /><br />
155
+ This project is licensed under the <a rel="noopener noreferrer" href="../../LICENSE.md">MIT License</a>
156
+ <br /><br />
157
+ </div>
158
+ <div align="justify">
159
+ <ul>
160
+ <li>Feel free to use this project for any purpose, including commercial applications.</li>
161
+ <li>You are permitted to modify, distribute, and include this project in any form, as long as the original copyright notice is retained.</li>
162
+ <li>If you share or publish modified versions, attribution to the original <a rel="noopener noreferrer" href="https://github.com/OctalMesh/Seagull">GitHub repository</a> is appreciated.</li>
163
+ <li>This software is provided "as is", without any warranties or guarantees, as detailed in the license terms.</li>
164
+ </ul>
165
+ </div>
166
+ </h6>
package/dist/index.mjs CHANGED
@@ -1,12 +1,14 @@
1
1
  import path from "node:path";
2
- import { GeneratorRegistry, OpenApiGeneratorCli, OpenApiTypescriptGenerator, git, hashSpec, loadConfig, readFileAtTag, remoteBranchExists, renderArtifactTag, renderReadme, requireOk, resolveBinPath, resolveConfigPath, resolveVersion, run, runSync, syncRedoclyConfig, tagExists } from "@octalmesh/seagull-core";
2
+ import { GeneratorRegistry, OpenApiGeneratorCli, OpenApiTypescriptGenerator, git, hashSpec, loadConfig, parseBundledSpec, primarySpecFormat, readFileAtTag, remoteBranchExists, renderArtifactTag, renderReadme, requireOk, resolveBinPath, resolveConfigPath, resolveVersion, run, runSync, specFilename, syncRedoclyConfig, tagExists } from "@octalmesh/seagull-core";
3
3
  import { Command } from "commander";
4
4
  import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
5
5
  import { generateDocsSite, serveDocsSite } from "@octalmesh/seagull-docs";
6
6
  import { tmpdir } from "node:os";
7
7
  //#region src/commands/bundle.ts
8
8
  /**
9
- * Bundles every contract's OpenAPI spec into `dist/specs/<contract>.json`.
9
+ * Bundles every contract's OpenAPI spec into `dist/specs/<contract>.<format>`
10
+ * once per format configured in `paths.specFormat` (`json` by default;
11
+ * configure `yaml`, or both, to bundle into more than one format at once).
10
12
  *
11
13
  * @param config - The resolved seagull config.
12
14
  */
@@ -18,8 +20,8 @@ async function bundleCommand(config) {
18
20
  });
19
21
  await mkdir(config.paths.specs, { recursive: true });
20
22
  const redoclyBin = resolveBinPath("@redocly/cli", "redocly");
21
- for (const contract of config.contracts) {
22
- const output = path.join(config.paths.specs, `${contract.name}.json`);
23
+ for (const contract of config.contracts) for (const format of config.paths.specFormat) {
24
+ const output = path.join(config.paths.specs, specFilename(contract.name, format));
23
25
  await run("node", [
24
26
  redoclyBin,
25
27
  "bundle",
@@ -28,7 +30,8 @@ async function bundleCommand(config) {
28
30
  output
29
31
  ], config.rootDir);
30
32
  }
31
- console.log(`Bundled ${config.contracts.length} specifications into ${config.paths.specs}`);
33
+ const formats = config.paths.specFormat.join(", ");
34
+ console.log(`Bundled ${config.contracts.length} specifications (${formats}) into ${config.paths.specs}`);
32
35
  }
33
36
  //#endregion
34
37
  //#region src/commands/clean.ts
@@ -79,12 +82,13 @@ async function generateSdkCommand(config) {
79
82
  });
80
83
  }
81
84
  const versionCache = /* @__PURE__ */ new Map();
85
+ const specFormat = primarySpecFormat(config.paths.specFormat);
82
86
  async function getVersionInfo(contractName) {
83
87
  const cached = versionCache.get(contractName);
84
88
  if (cached) return cached;
85
- const specPath = path.join(config.paths.specs, `${contractName}.json`);
89
+ const specPath = path.join(config.paths.specs, specFilename(contractName, specFormat));
86
90
  const raw = await readFile(specPath, "utf8");
87
- const spec = JSON.parse(raw);
91
+ const spec = parseBundledSpec(raw, specFormat);
88
92
  const info = {
89
93
  version: resolveVersion(spec, contractName),
90
94
  hash: hashSpec(raw)
@@ -99,8 +103,7 @@ async function generateSdkCommand(config) {
99
103
  contract,
100
104
  artifact,
101
105
  version,
102
- github: config.github,
103
- specInputPath: path.join(config.paths.specs, `${contract.name}.json`)
106
+ specInputPath: path.join(config.paths.specs, specFilename(contract.name, specFormat))
104
107
  });
105
108
  await writeFile(path.join(artifact.outputDir, "VERSION"), `${version}\n`);
106
109
  await writeFile(path.join(artifact.outputDir, "SPEC_HASH"), `${hash}\n`);
@@ -108,7 +111,6 @@ async function generateSdkCommand(config) {
108
111
  contract,
109
112
  artifact,
110
113
  version,
111
- github: config.github,
112
114
  vars: config.vars
113
115
  }));
114
116
  }
@@ -195,7 +197,7 @@ async function publishSdkCommand(config, options = {}) {
195
197
  for (const { contract, artifact } of config.allArtifacts) {
196
198
  const version = (await readFile(path.join(artifact.outputDir, "VERSION"), "utf8")).trim();
197
199
  const localHash = (await readFile(path.join(artifact.outputDir, "SPEC_HASH"), "utf8")).trim();
198
- const tag = renderArtifactTag(artifact, contract.name, version, config.github, config.vars);
200
+ const tag = renderArtifactTag(artifact, contract.name, version, config.vars);
199
201
  console.log(`\n=== ${contract.name} / ${artifact.id} -> ${artifact.branch} (v${version}) ===`);
200
202
  if (tagExists(config.rootDir, tag)) {
201
203
  const remoteHash = readFileAtTag(config.rootDir, tag, "SPEC_HASH");
@@ -211,6 +213,7 @@ async function publishSdkCommand(config, options = {}) {
211
213
  git([
212
214
  "fetch",
213
215
  "origin",
216
+ "--",
214
217
  artifact.branch
215
218
  ], config.rootDir);
216
219
  const hasRemoteBranch = remoteBranchExists(config.rootDir, artifact.branch);
@@ -308,8 +311,10 @@ async function serveDocsCommand(config) {
308
311
  function createProgram(metadata) {
309
312
  const program = new Command();
310
313
  program.name(metadata.name).description(metadata.description).version(metadata.version).option("-c, --config <path>", "path to the seagull config file (default: auto-detected in the current directory)");
311
- /** Resolves and loads the config, using `--config` if given, else
312
- * auto-discovering it in the current directory. */
314
+ /**
315
+ * Resolves and loads the config, using `--config` if given, else
316
+ * auto-discovering it in the current directory.
317
+ */
313
318
  function resolveConfig() {
314
319
  const { config: configOption } = program.opts();
315
320
  const configPath = configOption ? path.resolve(process.cwd(), configOption) : resolveConfigPath(process.cwd());
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@octalmesh/seagull-cli",
3
- "version": "0.0.2",
4
- "description": "Seagull's pipeline commands and commander program - bundled into @octalmesh/seagull.",
3
+ "version": "0.1.1",
4
+ "description": "Seagull's command-line interface (CLI).",
5
5
  "author": "OctalMesh <contact@octalmesh.com> (https://octalmesh.com)",
6
6
  "license": "MIT",
7
- "homepage": "https://github.com/OctalMesh/Seagull/tree/main/packages/cli",
7
+ "homepage": "https://developers.octalmesh.com/seagull",
8
8
  "type": "module",
9
9
  "repository": {
10
10
  "type": "git",
@@ -31,8 +31,8 @@
31
31
  "node": ">=22.22.0"
32
32
  },
33
33
  "dependencies": {
34
- "@octalmesh/seagull-core": "0.0.2",
35
- "@octalmesh/seagull-docs": "0.0.2",
34
+ "@octalmesh/seagull-core": "0.1.1",
35
+ "@octalmesh/seagull-docs": "0.1.1",
36
36
  "commander": "^15.0.0"
37
37
  },
38
38
  "scripts": {
@@ -0,0 +1,192 @@
1
+ import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import type { MockInstance } from "vitest";
7
+
8
+ import { makeConfig, makeContract } from "../test-support/fixtures";
9
+
10
+ const runMock = vi.fn((..._args: unknown[]) => Promise.resolve());
11
+ const resolveBinPathMock = vi.fn(
12
+ (..._args: unknown[]) => "/fake/bin/redocly.js",
13
+ );
14
+ const syncRedoclyConfigMock = vi.fn((..._args: unknown[]) => Promise.resolve());
15
+
16
+ vi.mock("@octalmesh/seagull-core", async (importOriginal) => {
17
+ const actual =
18
+ await importOriginal<typeof import("@octalmesh/seagull-core")>();
19
+
20
+ return {
21
+ ...actual,
22
+ run: (...a: unknown[]) => runMock(...a),
23
+ resolveBinPath: (...a: unknown[]) => resolveBinPathMock(...a),
24
+ syncRedoclyConfig: (...a: unknown[]) => syncRedoclyConfigMock(...a),
25
+ };
26
+ });
27
+
28
+ const { bundleCommand } = await import("./bundle");
29
+
30
+ describe("bundleCommand", () => {
31
+ let dir: string;
32
+ let logSpy: MockInstance;
33
+
34
+ beforeEach(async () => {
35
+ dir = await mkdtemp(path.join(tmpdir(), "seagull-bundle-"));
36
+ logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
37
+ runMock.mockClear();
38
+ resolveBinPathMock.mockClear();
39
+ syncRedoclyConfigMock.mockClear();
40
+ });
41
+
42
+ afterEach(async () => {
43
+ await rm(dir, { recursive: true, force: true });
44
+ logSpy.mockRestore();
45
+ });
46
+
47
+ it("syncs redocly.yaml before bundling", async () => {
48
+ await bundleCommand(makeConfig(dir, { contracts: [] }));
49
+
50
+ expect(syncRedoclyConfigMock).toHaveBeenCalledOnce();
51
+ });
52
+
53
+ it("recreates the specs output directory (clears stale output first)", async () => {
54
+ const config = makeConfig(dir, { contracts: [] });
55
+
56
+ await mkdir(config.paths.specs, { recursive: true });
57
+ await writeFile(path.join(config.paths.specs, "stale.json"), "{}");
58
+
59
+ await bundleCommand(config);
60
+
61
+ const entries = await readdir(config.paths.specs);
62
+
63
+ expect(entries).toEqual([]);
64
+ });
65
+
66
+ it("invokes redocly bundle once per contract, with -o <specs>/<name>.json", async () => {
67
+ const config = makeConfig(dir, {
68
+ contracts: [
69
+ makeContract({
70
+ name: "auth",
71
+ entrypoint: "/repo/specs/auth/openapi.yaml",
72
+ }),
73
+ makeContract({
74
+ name: "catalog",
75
+ entrypoint: "/repo/specs/catalog/openapi.yaml",
76
+ }),
77
+ ],
78
+ });
79
+
80
+ await bundleCommand(config);
81
+
82
+ expect(resolveBinPathMock).toHaveBeenCalledWith("@redocly/cli", "redocly");
83
+ expect(runMock).toHaveBeenCalledTimes(2);
84
+ expect(runMock).toHaveBeenNthCalledWith(
85
+ 1,
86
+ "node",
87
+ [
88
+ "/fake/bin/redocly.js",
89
+ "bundle",
90
+ "/repo/specs/auth/openapi.yaml",
91
+ "-o",
92
+ path.join(config.paths.specs, "auth.json"),
93
+ ],
94
+ config.rootDir,
95
+ );
96
+ expect(runMock).toHaveBeenNthCalledWith(
97
+ 2,
98
+ "node",
99
+ [
100
+ "/fake/bin/redocly.js",
101
+ "bundle",
102
+ "/repo/specs/catalog/openapi.yaml",
103
+ "-o",
104
+ path.join(config.paths.specs, "catalog.json"),
105
+ ],
106
+ config.rootDir,
107
+ );
108
+ });
109
+
110
+ it("logs a summary with the number of bundled specs and the output dir", async () => {
111
+ const config = makeConfig(dir, {
112
+ contracts: [
113
+ makeContract({ name: "auth" }),
114
+ makeContract({ name: "catalog" }),
115
+ ],
116
+ });
117
+
118
+ await bundleCommand(config);
119
+
120
+ expect(logSpy).toHaveBeenCalledWith(
121
+ `Bundled 2 specifications (json) into ${config.paths.specs}`,
122
+ );
123
+ });
124
+
125
+ it("writes -o <specs>/<name>.yaml when paths.specFormat is 'yaml'", async () => {
126
+ const config = makeConfig(dir, {
127
+ contracts: [
128
+ makeContract({
129
+ name: "auth",
130
+ entrypoint: "/repo/specs/auth/openapi.yaml",
131
+ }),
132
+ ],
133
+ });
134
+
135
+ config.paths.specFormat = ["yaml"];
136
+
137
+ await bundleCommand(config);
138
+
139
+ expect(runMock).toHaveBeenCalledWith(
140
+ "node",
141
+ [
142
+ "/fake/bin/redocly.js",
143
+ "bundle",
144
+ "/repo/specs/auth/openapi.yaml",
145
+ "-o",
146
+ path.join(config.paths.specs, "auth.yaml"),
147
+ ],
148
+ config.rootDir,
149
+ );
150
+ });
151
+
152
+ it("bundles into every configured format, once per format per contract", async () => {
153
+ const config = makeConfig(dir, {
154
+ contracts: [
155
+ makeContract({
156
+ name: "auth",
157
+ entrypoint: "/repo/specs/auth/openapi.yaml",
158
+ }),
159
+ ],
160
+ });
161
+
162
+ config.paths.specFormat = ["json", "yaml"];
163
+
164
+ await bundleCommand(config);
165
+
166
+ expect(runMock).toHaveBeenCalledTimes(2);
167
+ expect(runMock).toHaveBeenNthCalledWith(
168
+ 1,
169
+ "node",
170
+ [
171
+ "/fake/bin/redocly.js",
172
+ "bundle",
173
+ "/repo/specs/auth/openapi.yaml",
174
+ "-o",
175
+ path.join(config.paths.specs, "auth.json"),
176
+ ],
177
+ config.rootDir,
178
+ );
179
+ expect(runMock).toHaveBeenNthCalledWith(
180
+ 2,
181
+ "node",
182
+ [
183
+ "/fake/bin/redocly.js",
184
+ "bundle",
185
+ "/repo/specs/auth/openapi.yaml",
186
+ "-o",
187
+ path.join(config.paths.specs, "auth.yaml"),
188
+ ],
189
+ config.rootDir,
190
+ );
191
+ });
192
+ });
@@ -5,11 +5,14 @@ import {
5
5
  type ResolvedConfig,
6
6
  resolveBinPath,
7
7
  run,
8
+ specFilename,
8
9
  syncRedoclyConfig,
9
10
  } from "@octalmesh/seagull-core";
10
11
 
11
12
  /**
12
- * Bundles every contract's OpenAPI spec into `dist/specs/<contract>.json`.
13
+ * Bundles every contract's OpenAPI spec into `dist/specs/<contract>.<format>`
14
+ * once per format configured in `paths.specFormat` (`json` by default;
15
+ * configure `yaml`, or both, to bundle into more than one format at once).
13
16
  *
14
17
  * @param config - The resolved seagull config.
15
18
  */
@@ -22,16 +25,23 @@ export async function bundleCommand(config: ResolvedConfig): Promise<void> {
22
25
  const redoclyBin = resolveBinPath("@redocly/cli", "redocly");
23
26
 
24
27
  for (const contract of config.contracts) {
25
- const output = path.join(config.paths.specs, `${contract.name}.json`);
28
+ for (const format of config.paths.specFormat) {
29
+ const output = path.join(
30
+ config.paths.specs,
31
+ specFilename(contract.name, format),
32
+ );
26
33
 
27
- await run(
28
- "node",
29
- [redoclyBin, "bundle", contract.entrypoint, "-o", output],
30
- config.rootDir,
31
- );
34
+ await run(
35
+ "node",
36
+ [redoclyBin, "bundle", contract.entrypoint, "-o", output],
37
+ config.rootDir,
38
+ );
39
+ }
32
40
  }
33
41
 
42
+ const formats = config.paths.specFormat.join(", ");
43
+
34
44
  console.log(
35
- `Bundled ${config.contracts.length} specifications into ${config.paths.specs}`,
45
+ `Bundled ${config.contracts.length} specifications (${formats}) into ${config.paths.specs}`,
36
46
  );
37
47
  }