@shipworthy/openapi-cli 0.0.0-stage → 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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +442 -2
  3. package/dist/bin.d.ts +2 -0
  4. package/dist/bin.js +19 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +6 -0
  7. package/dist/commands/bundle/command.d.ts +6 -0
  8. package/dist/commands/bundle/command.js +23 -0
  9. package/dist/commands/generate/command.d.ts +2 -0
  10. package/dist/commands/generate/command.js +6 -0
  11. package/dist/commands/generate/ensure.d.ts +9 -0
  12. package/dist/commands/generate/ensure.js +31 -0
  13. package/dist/commands/generate/files.d.ts +4 -0
  14. package/dist/commands/generate/files.js +16 -0
  15. package/dist/commands/generate/force.d.ts +2 -0
  16. package/dist/commands/generate/force.js +2 -0
  17. package/dist/commands/generate/plugins/standard-schema/plugin.d.ts +7 -0
  18. package/dist/commands/generate/plugins/standard-schema/plugin.js +44 -0
  19. package/dist/commands/generate/plugins/tanstack-query/plugin.d.ts +9 -0
  20. package/dist/commands/generate/plugins/tanstack-query/plugin.js +42 -0
  21. package/dist/commands/generate/plugins/typescript/flags.d.ts +35 -0
  22. package/dist/commands/generate/plugins/typescript/flags.js +37 -0
  23. package/dist/commands/generate/plugins/typescript/plugin.d.ts +32 -0
  24. package/dist/commands/generate/plugins/typescript/plugin.js +84 -0
  25. package/dist/commands/generate/plugins/zod/plugin.d.ts +8 -0
  26. package/dist/commands/generate/plugins/zod/plugin.js +48 -0
  27. package/dist/commands/generate/publish.d.ts +11 -0
  28. package/dist/commands/generate/publish.js +23 -0
  29. package/dist/commands/generate/stamp.d.ts +13 -0
  30. package/dist/commands/generate/stamp.js +61 -0
  31. package/dist/commands/input.d.ts +4 -0
  32. package/dist/commands/input.js +16 -0
  33. package/dist/commands/lint/command.d.ts +7 -0
  34. package/dist/commands/lint/command.js +35 -0
  35. package/dist/commands/overlay/command.d.ts +9 -0
  36. package/dist/commands/overlay/command.js +33 -0
  37. package/dist/version.d.ts +2 -0
  38. package/dist/version.js +10 -0
  39. package/package.json +72 -4
@@ -0,0 +1,44 @@
1
+ import { generate as transform } from "@shipworthy/openapi-generator";
2
+ import { Clock, Effect, Option, Path, Result, Schema } from "effect";
3
+ import { Argument, CliError, Command, Flag } from "effect/cli";
4
+ import { asCliError, schemaSource } from "../../../input.js";
5
+ import { publish } from "../../publish.js";
6
+ import { fileHeader, generatorStamp } from "../../stamp.js";
7
+ import { writeStdout } from "../typescript/plugin.js";
8
+ export const dependencies = [];
9
+ const specifier = "@shipworthy/openapi-standard-schema";
10
+ const run = Effect.fn("run")(function* (flags) {
11
+ const output = Option.getOrUndefined(flags.output);
12
+ const started = yield* Clock.currentTimeMillis;
13
+ const decoded = Schema.decodeUnknownResult(Schema.String)(yield* transform({
14
+ specifier,
15
+ exportName: "standardSchema",
16
+ source: flags.input,
17
+ }));
18
+ if (Result.isFailure(decoded)) {
19
+ return yield* new CliError.UserError({ cause: decoded.failure });
20
+ }
21
+ const text = `${fileHeader(yield* generatorStamp(specifier, flags.input))}${decoded.success}`;
22
+ if (!output) {
23
+ yield* writeStdout(text);
24
+ return;
25
+ }
26
+ const path = yield* Path.Path;
27
+ yield* publish({
28
+ result: text,
29
+ outputPath: path.resolve(output),
30
+ check: false,
31
+ label: flags.input.href,
32
+ outputLabel: output,
33
+ started,
34
+ });
35
+ }, Effect.mapError(asCliError));
36
+ export const standardSchema = Command.make("standard-schema", {
37
+ input: Argument.String("input").pipe(Argument.withDescription("OpenAPI schema file or URL."), Argument.mapEffect(schemaSource)),
38
+ output: Flag.String("output").pipe(Flag.withAlias("o"), Flag.withDescription("Output file. Writes to stdout when omitted."), Flag.optional),
39
+ }, run).pipe(Command.withDescription("Generate Standard Schema validators from an OpenAPI schema."), Command.withShortDescription("Generate Standard Schema validators"), Command.withExamples([
40
+ {
41
+ command: "openapi generate standard-schema ./openapi.yaml -o ./schemas.ts",
42
+ description: "Write Standard Schema validators",
43
+ },
44
+ ]));
@@ -0,0 +1,9 @@
1
+ import { Path } from "effect";
2
+ import { CliError, Command } from "effect/cli";
3
+ export declare const dependencies: readonly ["typescript"];
4
+ export declare const tanstackQuery: Command.Command<"tanstack-query", {
5
+ readonly input: URL;
6
+ readonly output: string;
7
+ readonly name: string;
8
+ readonly force: boolean;
9
+ }, {}, CliError.UserError, import("effect/Crypto").Crypto | import("effect/Stdio").Stdio | (import("effect/FileSystem").FileSystem | import("effect/http/HttpClient").HttpClient | Path.Path)>;
@@ -0,0 +1,42 @@
1
+ import { generate as transform } from "@shipworthy/openapi-generator";
2
+ import { Clock, Effect, Path, Result, Schema } from "effect";
3
+ import { Argument, CliError, Command, Flag } from "effect/cli";
4
+ import { asCliError, schemaSource } from "../../../input.js";
5
+ import { ensureDependencies } from "../../ensure.js";
6
+ import { force } from "../../force.js";
7
+ import { ClientName, pascalCase, writeGenerated } from "../../files.js";
8
+ import { fileHeader, generatorStamp } from "../../stamp.js";
9
+ export const dependencies = ["typescript"];
10
+ const specifier = "@shipworthy/openapi-tanstack-query/generator";
11
+ const run = Effect.fn("run")(function* (flags) {
12
+ const started = yield* Clock.currentTimeMillis;
13
+ const path = yield* Path.Path;
14
+ yield* ensureDependencies(dependencies, {
15
+ source: flags.input,
16
+ directory: path.resolve(flags.output, flags.name),
17
+ started,
18
+ force: flags.force,
19
+ });
20
+ const decoded = Schema.decodeUnknownResult(Schema.String)(yield* transform({
21
+ specifier,
22
+ exportName: "tanstackQuery",
23
+ source: flags.input,
24
+ pluginOptions: { prefix: pascalCase(flags.name), name: flags.name },
25
+ }));
26
+ if (Result.isFailure(decoded)) {
27
+ return yield* new CliError.UserError({ cause: decoded.failure });
28
+ }
29
+ const header = fileHeader(yield* generatorStamp(specifier, flags.input));
30
+ yield* writeGenerated(flags.output, flags.name, { "client.ts": `${header}${decoded.success}` });
31
+ }, Effect.mapError(asCliError));
32
+ export const tanstackQuery = Command.make("tanstack-query", {
33
+ input: Argument.String("input").pipe(Argument.withDescription("OpenAPI schema file or URL."), Argument.mapEffect(schemaSource)),
34
+ output: Flag.String("output").pipe(Flag.withAlias("o"), Flag.withDescription("Directory that will contain <name>/client.ts.")),
35
+ name: Flag.String("name").pipe(Flag.withDescription("Client name. Used as the output directory and the generated function prefix."), Flag.withSchema(ClientName)),
36
+ force,
37
+ }, run).pipe(Command.withDescription("Generate TanStack Query utils from an OpenAPI schema."), Command.withShortDescription("Generate TanStack Query utils"), Command.withExamples([
38
+ {
39
+ command: "openapi generate tanstack-query ./openapi.yaml -o ./src/generated --name hotels",
40
+ description: "Write hotels/client.ts",
41
+ },
42
+ ]));
@@ -0,0 +1,35 @@
1
+ import { Flag } from "effect/cli";
2
+ export interface TypeFlags {
3
+ readonly arrayLength: boolean;
4
+ readonly conditionalEnums: boolean;
5
+ readonly dedupeEnums: boolean;
6
+ readonly enum: boolean;
7
+ readonly enumValues: boolean;
8
+ readonly exportType: boolean;
9
+ readonly readWriteMarkers: boolean;
10
+ readonly rootTypes: boolean;
11
+ }
12
+ export declare const defaultTypeFlags: TypeFlags;
13
+ export declare const typeFlags: {
14
+ enum: Flag.Flag<boolean>;
15
+ enumValues: Flag.Flag<boolean>;
16
+ conditionalEnums: Flag.Flag<boolean>;
17
+ dedupeEnums: Flag.Flag<boolean>;
18
+ exportType: Flag.Flag<boolean>;
19
+ arrayLength: Flag.Flag<boolean>;
20
+ rootTypes: Flag.Flag<boolean>;
21
+ readWriteMarkers: Flag.Flag<boolean>;
22
+ };
23
+ export declare function pluginOptions(flags: TypeFlags & {
24
+ readonly silent?: boolean;
25
+ }): {
26
+ arrayLength: boolean;
27
+ conditionalEnums: boolean;
28
+ dedupeEnums: boolean;
29
+ enum: boolean;
30
+ enumValues: boolean;
31
+ exportType: boolean;
32
+ readWriteMarkers: boolean;
33
+ rootTypes: boolean;
34
+ silent: boolean;
35
+ };
@@ -0,0 +1,37 @@
1
+ import { Flag } from "effect/cli";
2
+ function flag(name, description) {
3
+ return Flag.Boolean(name).pipe(Flag.withDescription(description), Flag.withDefault(false));
4
+ }
5
+ export const defaultTypeFlags = {
6
+ arrayLength: true,
7
+ conditionalEnums: false,
8
+ dedupeEnums: false,
9
+ enum: false,
10
+ enumValues: false,
11
+ exportType: false,
12
+ readWriteMarkers: false,
13
+ rootTypes: false,
14
+ };
15
+ export const typeFlags = {
16
+ enum: flag("enum", "Export true TS enums instead of unions."),
17
+ enumValues: flag("enum-values", "Export enum values as arrays."),
18
+ conditionalEnums: flag("conditional-enums", "Only generate true TS enums when enum metadata is available."),
19
+ dedupeEnums: flag("dedupe-enums", "Dedupe enum types when --enum is set."),
20
+ exportType: Flag.Boolean("export-type").pipe(Flag.withAlias("t"), Flag.withDescription("Export top-level type aliases instead of interfaces."), Flag.withDefault(false)),
21
+ arrayLength: Flag.Boolean("array-length").pipe(Flag.withDescription("Generate tuples using array minItems / maxItems."), Flag.withDefault(true)),
22
+ rootTypes: flag("root-types", "Export component and operation types at the root level."),
23
+ readWriteMarkers: flag("read-write-markers", "Generate $Read/$Write markers for readOnly/writeOnly properties."),
24
+ };
25
+ export function pluginOptions(flags) {
26
+ return {
27
+ arrayLength: flags.arrayLength,
28
+ conditionalEnums: flags.conditionalEnums,
29
+ dedupeEnums: flags.dedupeEnums,
30
+ enum: flags.enum,
31
+ enumValues: flags.enumValues,
32
+ exportType: flags.exportType,
33
+ readWriteMarkers: flags.readWriteMarkers,
34
+ rootTypes: flags.rootTypes,
35
+ silent: flags.silent ?? false,
36
+ };
37
+ }
@@ -0,0 +1,32 @@
1
+ import { Effect, Option, Path, Stdio } from "effect";
2
+ import { CliError, Command } from "effect/cli";
3
+ import { type TypeFlags } from "./flags.ts";
4
+ export declare const dependencies: readonly [];
5
+ export declare const typescriptSpecifier = "@shipworthy/openapi-typescript";
6
+ export declare const writeStdout: (text: string) => Effect.Effect<void, import("effect/PlatformError").PlatformError, Stdio.Stdio>;
7
+ export declare const renderTypescript: (options: {
8
+ readonly source: URL | string;
9
+ readonly generator: {
10
+ readonly name: string;
11
+ readonly stamp: string;
12
+ };
13
+ readonly outputPath?: string;
14
+ readonly flags: TypeFlags;
15
+ readonly check: boolean;
16
+ readonly label: string;
17
+ readonly outputLabel: string;
18
+ readonly started: number;
19
+ }) => Effect.Effect<void, import("effect/PlatformError").BadArgument | import("effect/PlatformError").PlatformError | import("effect/Schema").SchemaError | CliError.UserError | import("@shipworthy/openapi-generator").GenerateError, Stdio.Stdio | (import("effect/FileSystem").FileSystem | import("effect/http/HttpClient").HttpClient | Path.Path)>;
20
+ export declare const typescript: Command.Command<"typescript", {
21
+ readonly enum: boolean;
22
+ readonly enumValues: boolean;
23
+ readonly conditionalEnums: boolean;
24
+ readonly dedupeEnums: boolean;
25
+ readonly exportType: boolean;
26
+ readonly arrayLength: boolean;
27
+ readonly rootTypes: boolean;
28
+ readonly readWriteMarkers: boolean;
29
+ readonly input: Option.Option<URL>;
30
+ readonly output: Option.Option<string>;
31
+ readonly check: boolean;
32
+ }, {}, CliError.UserError, import("effect/Crypto").Crypto | Stdio.Stdio | (import("effect/FileSystem").FileSystem | import("effect/http/HttpClient").HttpClient | Path.Path)>;
@@ -0,0 +1,84 @@
1
+ import { generate as transform } from "@shipworthy/openapi-generator";
2
+ import { Clock, Console, Effect, Option, Path, Stdio, Stream } from "effect";
3
+ import { Argument, CliError, Command, Flag } from "effect/cli";
4
+ import { asCliError, schemaSource } from "../../../input.js";
5
+ import { version } from "../../../../version.js";
6
+ import { publish } from "../../publish.js";
7
+ import { fileHeader, generatorStamp } from "../../stamp.js";
8
+ import { pluginOptions, typeFlags } from "./flags.js";
9
+ export const dependencies = [];
10
+ export const typescriptSpecifier = "@shipworthy/openapi-typescript";
11
+ const typescriptModule = Effect.fn("typescriptModule")(function* () {
12
+ return yield* Effect.tryPromise({
13
+ try: () => import("@shipworthy/openapi-typescript"),
14
+ catch: (cause) => new CliError.UserError({ cause }),
15
+ });
16
+ });
17
+ const readStdin = Effect.fn("readStdin")(function* () {
18
+ const stdio = yield* Stdio.Stdio;
19
+ return yield* stdio.stdin.pipe(Stream.decodeText(), Stream.mkString);
20
+ });
21
+ export const writeStdout = Effect.fn("writeStdout")(function* (text) {
22
+ const stdio = yield* Stdio.Stdio;
23
+ yield* Stream.run(Stream.make(text), stdio.stdout({ endOnDone: false }));
24
+ });
25
+ export const renderTypescript = Effect.fn("renderTypescript")(function* (options) {
26
+ if (options.outputPath)
27
+ yield* Console.info(`openapi generate typescript ${yield* version}`);
28
+ const nodes = yield* transform({
29
+ specifier: typescriptSpecifier,
30
+ exportName: "typescript",
31
+ source: options.source,
32
+ pluginOptions: pluginOptions({ ...options.flags, silent: options.outputPath === undefined }),
33
+ });
34
+ const types = yield* typescriptModule();
35
+ // Print each top-level declaration on its own so a blank line separates them.
36
+ const body = nodes.map((node) => types.astToString([node])).join("\n");
37
+ const printed = yield* Effect.tryPromise({
38
+ try: () => types.formatTypeScript(`${fileHeader(options.generator)}${body}`),
39
+ catch: (cause) => new CliError.UserError({ cause }),
40
+ });
41
+ if (!options.outputPath) {
42
+ yield* writeStdout(printed);
43
+ return;
44
+ }
45
+ yield* publish({
46
+ result: printed,
47
+ outputPath: options.outputPath,
48
+ check: options.check,
49
+ label: options.label,
50
+ outputLabel: options.outputLabel,
51
+ started: options.started,
52
+ });
53
+ });
54
+ const run = Effect.fn("run")(function* (flags) {
55
+ const output = Option.getOrUndefined(flags.output);
56
+ const input = Option.getOrUndefined(flags.input);
57
+ const started = yield* Clock.currentTimeMillis;
58
+ const path = yield* Path.Path;
59
+ const source = input ?? (yield* readStdin());
60
+ yield* renderTypescript({
61
+ source,
62
+ generator: yield* generatorStamp(typescriptSpecifier, source),
63
+ outputPath: output === undefined ? undefined : path.resolve(output),
64
+ flags,
65
+ check: flags.check,
66
+ label: input?.href ?? "stdin",
67
+ outputLabel: output ?? "stdout",
68
+ started,
69
+ });
70
+ }, Effect.mapError(asCliError));
71
+ export const typescript = Command.make("typescript", {
72
+ input: Argument.String("input").pipe(Argument.withDescription("OpenAPI schema file or URL. Omit to read stdin."), Argument.optional, Argument.mapEffect((input) => Option.match(input, {
73
+ onNone: () => Effect.succeed(Option.none()),
74
+ onSome: (value) => Effect.asSome(schemaSource(value)),
75
+ }))),
76
+ output: Flag.String("output").pipe(Flag.withAlias("o"), Flag.withDescription("Output file. Writes to stdout when omitted."), Flag.optional),
77
+ check: Flag.Boolean("check").pipe(Flag.withDescription("Check that the generated types are up-to-date."), Flag.withDefault(false)),
78
+ ...typeFlags,
79
+ }, run).pipe(Command.withDescription("Convert an OpenAPI schema to TypeScript."), Command.withShortDescription("Generate TypeScript types"), Command.withExamples([
80
+ {
81
+ command: "openapi generate typescript ./openapi.yaml -o ./schema.ts",
82
+ description: "Write TypeScript types for a local schema",
83
+ },
84
+ ]));
@@ -0,0 +1,8 @@
1
+ import { Path } from "effect";
2
+ import { CliError, Command } from "effect/cli";
3
+ export declare const dependencies: readonly ["typescript"];
4
+ export declare const zod: Command.Command<"zod", {
5
+ readonly input: URL;
6
+ readonly output: string;
7
+ readonly force: boolean;
8
+ }, {}, CliError.UserError, import("effect/Crypto").Crypto | import("effect/Stdio").Stdio | (import("effect/FileSystem").FileSystem | import("effect/http/HttpClient").HttpClient | Path.Path)>;
@@ -0,0 +1,48 @@
1
+ import { generate as transform } from "@shipworthy/openapi-generator";
2
+ import { Clock, Effect, Path, Result, Schema } from "effect";
3
+ import { Argument, CliError, Command, Flag } from "effect/cli";
4
+ import { asCliError, schemaSource } from "../../../input.js";
5
+ import { ensureDependencies } from "../../ensure.js";
6
+ import { force } from "../../force.js";
7
+ import { publish } from "../../publish.js";
8
+ import { fileHeader, generatorStamp } from "../../stamp.js";
9
+ export const dependencies = ["typescript"];
10
+ const specifier = "@shipworthy/openapi-zod";
11
+ const run = Effect.fn("run")(function* (flags) {
12
+ const started = yield* Clock.currentTimeMillis;
13
+ const path = yield* Path.Path;
14
+ const outputPath = path.resolve(flags.output);
15
+ yield* ensureDependencies(dependencies, {
16
+ source: flags.input,
17
+ directory: path.dirname(outputPath),
18
+ started,
19
+ force: flags.force,
20
+ });
21
+ const decoded = Schema.decodeUnknownResult(Schema.String)(yield* transform({
22
+ specifier,
23
+ exportName: "zod",
24
+ source: flags.input,
25
+ }));
26
+ if (Result.isFailure(decoded)) {
27
+ return yield* new CliError.UserError({ cause: decoded.failure });
28
+ }
29
+ const header = fileHeader(yield* generatorStamp(specifier, flags.input));
30
+ yield* publish({
31
+ result: `${header}${decoded.success}`,
32
+ outputPath,
33
+ check: false,
34
+ label: flags.input.href,
35
+ outputLabel: flags.output,
36
+ started,
37
+ });
38
+ }, Effect.mapError(asCliError));
39
+ export const zod = Command.make("zod", {
40
+ input: Argument.String("input").pipe(Argument.withDescription("OpenAPI schema file or URL."), Argument.mapEffect(schemaSource)),
41
+ output: Flag.String("output").pipe(Flag.withAlias("o"), Flag.withDescription("Output file. types.ts is written next to it.")),
42
+ force,
43
+ }, run).pipe(Command.withDescription("Generate Zod validators from an OpenAPI schema."), Command.withShortDescription("Generate Zod validators"), Command.withExamples([
44
+ {
45
+ command: "openapi generate zod ./openapi.yaml -o ./src/generated/hotels/zod-schema.ts",
46
+ description: "Write hotels/zod-schema.ts and hotels/types.ts",
47
+ },
48
+ ]));
@@ -0,0 +1,11 @@
1
+ import { Effect, FileSystem, Path } from "effect";
2
+ import { CliError } from "effect/cli";
3
+ export declare function formatTime(t: number): string;
4
+ export declare const publish: (options: {
5
+ readonly result: string;
6
+ readonly outputPath: string;
7
+ readonly check: boolean;
8
+ readonly label: string;
9
+ readonly outputLabel: string;
10
+ readonly started: number;
11
+ }) => Effect.Effect<undefined, import("effect/PlatformError").PlatformError | CliError.UserError, FileSystem.FileSystem | Path.Path>;
@@ -0,0 +1,23 @@
1
+ import { Clock, Console, Effect, FileSystem, Path } from "effect";
2
+ import { CliError } from "effect/cli";
3
+ export function formatTime(t) {
4
+ if (t < 1000)
5
+ return `${Math.round(10 * t) / 10}ms`;
6
+ if (t < 60000)
7
+ return `${Math.round(t / 100) / 10}s`;
8
+ return `${Math.round(t / 6000) / 10}m`;
9
+ }
10
+ export const publish = Effect.fn("publish")(function* (options) {
11
+ const fs = yield* FileSystem.FileSystem;
12
+ const path = yield* Path.Path;
13
+ if (options.check) {
14
+ const previous = yield* fs.readFileString(options.outputPath);
15
+ if (previous === options.result)
16
+ return;
17
+ return yield* new CliError.UserError({ cause: "Generated types are not up-to-date!" });
18
+ }
19
+ yield* fs.makeDirectory(path.dirname(options.outputPath), { recursive: true });
20
+ yield* fs.writeFileString(options.outputPath, options.result);
21
+ const elapsed = (yield* Clock.currentTimeMillis) - options.started;
22
+ yield* Console.log(`${options.label} → ${options.outputLabel} [${formatTime(elapsed)}]`);
23
+ });
@@ -0,0 +1,13 @@
1
+ import { Crypto, Effect, FileSystem, Path } from "effect";
2
+ import { CliError } from "effect/cli";
3
+ import { HttpClient } from "effect/http";
4
+ /** `<package>@<version>:<sha256 of the schema contents>` for the generator at `specifier`. */
5
+ export declare const generatorStamp: (specifier: string, source: string | URL) => Effect.Effect<{
6
+ name: string;
7
+ stamp: string;
8
+ }, import("effect/http/HttpClientError").HttpClientError | import("effect/PlatformError").PlatformError | CliError.UserError, Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path>;
9
+ export declare function fileHeader(generator: {
10
+ readonly name: string;
11
+ readonly stamp: string;
12
+ }): string;
13
+ export declare function readStamp(text: string): string | undefined;
@@ -0,0 +1,61 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { Crypto, Effect, FileSystem, Option, Path, Schema } from "effect";
3
+ import { CliError } from "effect/cli";
4
+ import { HttpClient } from "effect/http";
5
+ const PackageJson = Schema.Struct({ name: Schema.String, version: Schema.String });
6
+ /** Matches the ` * <package>@<version>:<sha256>` line in a generated file's header. */
7
+ const stampPattern = /^ \* (\S+@[^\s@:]+:[0-9a-f]{64})$/m;
8
+ /** `<package>@<version>:<sha256 of the schema contents>` for the generator at `specifier`. */
9
+ export const generatorStamp = Effect.fn("generatorStamp")(function* (specifier, source) {
10
+ const generator = yield* generatorPackage(specifier);
11
+ const hash = yield* sha256(typeof source === "string" ? source : yield* readSource(source));
12
+ return { name: generator.name, stamp: `${generator.name}@${generator.version}:${hash}` };
13
+ });
14
+ export function fileHeader(generator) {
15
+ return `/**
16
+ * This file was auto-generated by ${generator.name}.
17
+ * Do not make direct changes to the file.
18
+ *
19
+ * ${generator.stamp}
20
+ */
21
+
22
+ `;
23
+ }
24
+ export function readStamp(text) {
25
+ return stampPattern.exec(text)?.[1];
26
+ }
27
+ const generatorPackage = Effect.fn("generatorPackage")(function* (specifier) {
28
+ const fs = yield* FileSystem.FileSystem;
29
+ const path = yield* Path.Path;
30
+ const name = specifier.split("/", specifier.startsWith("@") ? 2 : 1).join("/");
31
+ const resolved = yield* Effect.try({
32
+ try: () => fileURLToPath(import.meta.resolve(specifier)),
33
+ catch: (cause) => new CliError.UserError({ cause }),
34
+ });
35
+ for (let directory = path.dirname(resolved);; directory = path.dirname(directory)) {
36
+ const text = yield* fs.readFileString(path.join(directory, "package.json")).pipe(Effect.option);
37
+ if (Option.isSome(text)) {
38
+ const decoded = yield* Schema.decodeEffect(Schema.fromJsonString(PackageJson))(text.value).pipe(Effect.option);
39
+ if (Option.isSome(decoded) && decoded.value.name === name)
40
+ return decoded.value;
41
+ }
42
+ if (path.dirname(directory) === directory) {
43
+ return yield* new CliError.UserError({ cause: `Cannot find package.json for ${name}.` });
44
+ }
45
+ }
46
+ });
47
+ const readSource = Effect.fn("readSource")(function* (source) {
48
+ if (source.protocol === "file:") {
49
+ const fs = yield* FileSystem.FileSystem;
50
+ return yield* fs.readFileString(fileURLToPath(source));
51
+ }
52
+ const http = yield* HttpClient.HttpClient;
53
+ const client = HttpClient.filterStatusOk(http);
54
+ const response = yield* client.get(source.href);
55
+ return yield* response.text;
56
+ });
57
+ const sha256 = Effect.fn("sha256")(function* (text) {
58
+ const crypto = yield* Crypto.Crypto;
59
+ const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(text));
60
+ return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
61
+ });
@@ -0,0 +1,4 @@
1
+ import { Effect, Path } from "effect";
2
+ import { CliError } from "effect/cli";
3
+ export declare const schemaSource: (input: string) => Effect.Effect<URL, CliError.UserError, Path.Path>;
4
+ export declare function asCliError(cause: unknown): CliError.UserError;
@@ -0,0 +1,16 @@
1
+ import { Effect, Path } from "effect";
2
+ import { CliError } from "effect/cli";
3
+ export const schemaSource = Effect.fn("schemaSource")(function* (input) {
4
+ if (input.includes("*")) {
5
+ return yield* new CliError.UserError({ cause: "Globbing is unsupported." });
6
+ }
7
+ if (/^(https?:|file:)/i.test(input))
8
+ return new URL(input);
9
+ const path = yield* Path.Path;
10
+ return yield* Effect.mapError(path.toFileUrl(path.resolve(input)), (cause) => new CliError.UserError({ cause }));
11
+ });
12
+ export function asCliError(cause) {
13
+ if (CliError.isCliError(cause) && cause._tag === "UserError")
14
+ return cause;
15
+ return new CliError.UserError({ cause });
16
+ }
@@ -0,0 +1,7 @@
1
+ import { FileSystem, Path } from "effect";
2
+ import { Command } from "effect/cli";
3
+ import { HttpClient } from "effect/http";
4
+ export declare const lint: Command.Command<"lint", {
5
+ readonly input: URL;
6
+ readonly strict: boolean;
7
+ }, {}, import("effect/cli/CliError").UserError, FileSystem.FileSystem | HttpClient.HttpClient | Path.Path>;
@@ -0,0 +1,35 @@
1
+ import { Linter } from "@shipworthy/openapi-validator";
2
+ import { Effect, FileSystem, Path } from "effect";
3
+ import { Argument, Command, Flag } from "effect/cli";
4
+ import { HttpClient } from "effect/http";
5
+ import { asCliError, schemaSource } from "../input.js";
6
+ const readSource = Effect.fnUntraced(function* (input) {
7
+ if (input.protocol === "file:") {
8
+ const path = yield* Path.Path;
9
+ const fs = yield* FileSystem.FileSystem;
10
+ return yield* fs.readFileString(yield* path.fromFileUrl(input));
11
+ }
12
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk);
13
+ return yield* client.get(input.href).pipe(Effect.flatMap((response) => response.text));
14
+ });
15
+ const run = Effect.fn("run")(function* ({ input, strict }) {
16
+ const source = yield* readSource(input);
17
+ yield* Linter.use((linter) => linter.lintDocument(source, { sourceUrl: input.href, strict }));
18
+ }, Effect.mapError(asCliError), Effect.provide(Linter.layer));
19
+ export const lint = Command.make("lint", {
20
+ input: Argument.String("input").pipe(Argument.withDescription("OpenAPI schema file or URL."), Argument.mapEffect(schemaSource)),
21
+ strict: Flag.Boolean("strict").pipe(Flag.withDescription("Keep Spectral's oas rule severities and fail on YAML/JSON syntax errors."), Flag.withDefault(false)),
22
+ }, run).pipe(Command.withDescription("Lint an OpenAPI schema and print diagnostics."), Command.withShortDescription("Lint an OpenAPI schema"), Command.withExamples([
23
+ {
24
+ command: "openapi lint ./openapi.yaml",
25
+ description: "Lint a local schema",
26
+ },
27
+ {
28
+ command: "openapi lint https://example.com/openapi.yaml",
29
+ description: "Lint a remote schema",
30
+ },
31
+ {
32
+ command: "openapi lint --strict ./openapi.yaml",
33
+ description: "Fail on every error-severity oas rule and on syntax errors",
34
+ },
35
+ ]));
@@ -0,0 +1,9 @@
1
+ import { FileSystem, Path } from "effect";
2
+ import { Command } from "effect/cli";
3
+ import { HttpClient } from "effect/http";
4
+ export declare const overlay: Command.Command<"overlay", {
5
+ readonly input: URL;
6
+ readonly overlay: URL;
7
+ readonly output: string;
8
+ readonly removeUnusedComponents: boolean;
9
+ }, {}, import("effect/cli/CliError").UserError, FileSystem.FileSystem | HttpClient.HttpClient | Path.Path>;
@@ -0,0 +1,33 @@
1
+ import { applyOverlayWithWarnings } from "@shipworthy/overlay";
2
+ import { bundleDocument } from "@shipworthy/openapi-parser";
3
+ import { Console, Effect, FileSystem, Path, Schema } from "effect";
4
+ import { Argument, Command, Flag } from "effect/cli";
5
+ import { HttpClient, HttpClientResponse } from "effect/http";
6
+ import { fileURLToPath } from "node:url";
7
+ import { asCliError, schemaSource } from "../input.js";
8
+ const run = Effect.fn("overlay")(function* ({ input, overlay, output, removeUnusedComponents, }) {
9
+ const document = yield* bundleDocument(input);
10
+ const fs = yield* FileSystem.FileSystem;
11
+ const text = overlay.protocol === "file:"
12
+ ? yield* fs.readFileString(fileURLToPath(overlay))
13
+ : yield* HttpClient.get(overlay.href).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.flatMap((response) => response.text));
14
+ const result = yield* applyOverlayWithWarnings(document, text, { removeUnusedComponents });
15
+ yield* Effect.forEach(result.warnings, (warning) => Console.warn(`WARNING Overlay action ${warning.action} (${warning.target}): ${warning.message}`), { discard: true });
16
+ const path = yield* Path.Path;
17
+ const outputPath = path.resolve(output);
18
+ yield* fs.makeDirectory(path.dirname(outputPath), { recursive: true });
19
+ const serialized = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown, { space: 2 }))(result.document);
20
+ yield* fs.writeFileString(outputPath, `${serialized}\n`);
21
+ yield* Console.log(`${input.href} + ${overlay.href} → ${output}`);
22
+ }, Effect.mapError(asCliError));
23
+ export const overlay = Command.make("overlay", {
24
+ input: Argument.String("input").pipe(Argument.withDescription("OpenAPI schema file or URL. Bundled before applying the overlay."), Argument.mapEffect(schemaSource)),
25
+ overlay: Flag.String("overlay").pipe(Flag.withDescription("Overlay JSON/YAML file or URL."), Flag.mapEffect(schemaSource)),
26
+ output: Flag.String("output").pipe(Flag.withAlias("o"), Flag.withDescription("Output JSON file. Parent directories are created.")),
27
+ removeUnusedComponents: Flag.Boolean("remove-unused-components").pipe(Flag.withDescription("Remove paths without operations and unreachable components after overlaying."), Flag.withDefault(false)),
28
+ }, run).pipe(Command.withDescription("Bundle an OpenAPI document, then apply an OpenAPI Overlay."), Command.withShortDescription("Apply an OpenAPI Overlay"), Command.withExamples([
29
+ {
30
+ command: "openapi overlay openapi.yaml --overlay overlay.yaml -o bundled.openapi.json",
31
+ description: "Bundle a schema and apply an overlay before writing JSON",
32
+ },
33
+ ]));
@@ -0,0 +1,2 @@
1
+ import { Effect, FileSystem, Path, Schema } from "effect";
2
+ export declare const version: Effect.Effect<string, import("effect/PlatformError").BadArgument | import("effect/PlatformError").PlatformError | Schema.SchemaError, FileSystem.FileSystem | Path.Path>;
@@ -0,0 +1,10 @@
1
+ import { Effect, FileSystem, Path, Schema } from "effect";
2
+ const PackageJson = Schema.Struct({ version: Schema.String });
3
+ export const version = Effect.gen(function* () {
4
+ const fs = yield* FileSystem.FileSystem;
5
+ const path = yield* Path.Path;
6
+ const filename = yield* path.fromFileUrl(new URL("../package.json", import.meta.url));
7
+ const source = yield* fs.readFileString(filename);
8
+ const manifest = yield* Schema.decodeEffect(Schema.fromJsonString(PackageJson))(source);
9
+ return manifest.version;
10
+ });