@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 +49 -0
- package/README.md +157 -20
- package/dist/index.mjs +18 -13
- package/package.json +5 -5
- package/src/commands/bundle.test.ts +192 -0
- package/src/commands/bundle.ts +18 -8
- package/src/commands/clean.test.ts +67 -0
- package/src/commands/generate-docs.test.ts +30 -0
- package/src/commands/generate-sdk.test.ts +382 -0
- package/src/commands/generate-sdk.ts +13 -5
- package/src/commands/lint.test.ts +125 -0
- package/src/commands/publish-registries.test.ts +160 -0
- package/src/commands/publish-sdk.test.ts +383 -0
- package/src/commands/publish-sdk.ts +1 -2
- package/src/commands/serve-docs.test.ts +30 -0
- package/src/program.test.ts +282 -0
- package/src/test-support/fixtures.ts +93 -0
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
|
-
|
|
1
|
+
<!--suppress HtmlDeprecatedAttribute, HtmlUnknownTarget -->
|
|
2
|
+
<h1 id="title" align="center">@octalmesh/seagull-cli</h1>
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
76
|
+
await bundleCommand(config);
|
|
77
|
+
await generateSdkCommand(config);
|
|
78
|
+
```
|
|
26
79
|
|
|
27
|
-
|
|
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
|
-
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
89
|
+
const specPath = path.join(config.paths.specs, specFilename(contractName, specFormat));
|
|
86
90
|
const raw = await readFile(specPath, "utf8");
|
|
87
|
-
const spec =
|
|
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
|
-
|
|
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.
|
|
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
|
-
/**
|
|
312
|
-
*
|
|
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.
|
|
4
|
-
"description": "Seagull's
|
|
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://
|
|
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.
|
|
35
|
-
"@octalmesh/seagull-docs": "0.
|
|
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
|
+
});
|
package/src/commands/bundle.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
}
|