@effected/cli 0.26.0 → 0.28.0

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/bin/effected.js CHANGED
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { main } from "../main.js";
3
-
4
2
  // src/bin.ts
3
+ if (import.meta.url.endsWith(".ts")) {
4
+ const { register } = await import([
5
+ "tsx",
6
+ "esm",
7
+ "api"
8
+ ].join("/"));
9
+ register();
10
+ }
11
+ const { main } = await import("../main.js");
5
12
  main();
6
13
 
7
14
  export { };
package/ci.js ADDED
@@ -0,0 +1,16 @@
1
+ import { Option } from "effect";
2
+ import { RuntimeEnv } from "@effected/env";
3
+
4
+ // src/ci.ts
5
+ /**
6
+ * Whether the process runs under CI, by `@effected/env`'s one rule: a known
7
+ * provider's marker, or a truthy `CI` or `CONTINUOUS_INTEGRATION`; `CI=false`
8
+ * is not CI. Its own module so `main` and its test call the same expression,
9
+ * and so `main` never reaches `@effected/engine/build`, which loads the
10
+ * bundler and its toolchain.
11
+ *
12
+ * @internal
13
+ */
14
+ const isCI = (env) => Option.isSome(RuntimeEnv.fromRecord(env).ci);
15
+
16
+ export { isCI };
@@ -0,0 +1,9 @@
1
+ import { execute } from "./execute.js";
2
+ import { buildFlags } from "./flags.js";
3
+ import { Command } from "effect/cli";
4
+
5
+ // src/commands/action/build.ts
6
+ /** `effected action build`: build the GitHub Action in the working directory into its `dist`. */
7
+ const makeBuildCommand = (deps) => Command.make("build", buildFlags, (input) => execute(input, deps)).pipe(Command.withDescription("Build the GitHub Action in the working directory from the [action] table of its effected.toml into dist"));
8
+
9
+ export { makeBuildCommand };
@@ -0,0 +1,54 @@
1
+ import { Schema } from "effect";
2
+
3
+ // src/commands/action/errors.ts
4
+ /**
5
+ * Validation stopped the build or an entry failed. Exit `1`.
6
+ *
7
+ * @public
8
+ */
9
+ var ActionBuildFailedError = class extends Schema.TaggedError()("ActionBuildFailedError", {
10
+ /** How many validation errors there were. */
11
+ errors: Schema.Number,
12
+ /** How many entries failed to bundle or load. */
13
+ failedEntries: Schema.Number
14
+ }) {
15
+ get message() {
16
+ const parts = [...this.errors > 0 ? [`${this.errors} validation error(s)`] : [], ...this.failedEntries > 0 ? [`${this.failedEntries} failed entry(ies)`] : []];
17
+ return parts.length > 0 ? `The action build failed with ${parts.join(" and ")}.` : "The action build failed: warnings are errors in strict mode.";
18
+ }
19
+ };
20
+ /**
21
+ * The build could not start: its `effected.toml` would not load, it has no `[action]` table, or the directory is not a
22
+ * package. Exit `2`: exit `1` means it ran and found a problem.
23
+ *
24
+ * @remarks
25
+ * Carries the engine's message. This command's own type rather than the engine's, because the engine's build entry is
26
+ * loaded only when the command runs.
27
+ *
28
+ * @public
29
+ */
30
+ var ActionConfigError = class extends Schema.TaggedError()("ActionConfigError", { message: Schema.String }) {};
31
+ /**
32
+ * The build could not finish: an entry is missing with validation off, or a file could not be read, cleaned, written or
33
+ * copied. `action init` fails with it too when a write fails after its checks, naming the files already written. Exit
34
+ * `3`, the infrastructure tier.
35
+ *
36
+ * @remarks
37
+ * Carries the engine's message, as {@link ActionConfigError} does.
38
+ *
39
+ * @public
40
+ */
41
+ var ActionRunError = class extends Schema.TaggedError()("ActionRunError", { message: Schema.String }) {};
42
+ /**
43
+ * `action init` could not start: the directory is not a package, the action's name or description was found nowhere
44
+ * or holds a control character, files it would write exist and `--force` was not given, or a path cannot be written
45
+ * over (a symbolic link, the wrong kind of entry), which `--force` never overrides. Exit `2`, and nothing was written.
46
+ *
47
+ * @remarks
48
+ * Carries the engine's message, as {@link ActionConfigError} does.
49
+ *
50
+ * @public
51
+ */
52
+ var ActionInitError = class extends Schema.TaggedError()("ActionInitError", { message: Schema.String }) {};
53
+
54
+ export { ActionBuildFailedError, ActionConfigError, ActionInitError, ActionRunError };
@@ -0,0 +1,44 @@
1
+ import { InitDoc } from "../../render/InitDoc.js";
2
+ import { resolveFormat } from "../schema/execute.js";
3
+ import { ActionInitError, ActionRunError } from "./errors.js";
4
+ import { Console, Effect, Option, Schema } from "effect";
5
+ import { CliRuntime, Doc } from "@effected/cli-kit";
6
+
7
+ // src/commands/action/execute-init.ts
8
+ /**
9
+ * Run one `action init`.
10
+ *
11
+ * @remarks
12
+ * The engine's build entry is imported here, as `action build` imports it, so `effected --version` never loads it.
13
+ */
14
+ const executeInit = Effect.fn("effected.action.init")(function* (input, deps) {
15
+ const format = yield* resolveFormat(input.format);
16
+ const { ActionInit, ActionInitResult } = yield* Effect.promise(() => import("@effected/engine/build"));
17
+ const refused = (error) => Effect.fail(CliRuntime.reported(new ActionInitError({ message: error.message }), 2));
18
+ const stopped = (error) => Effect.fail(CliRuntime.reported(new ActionRunError({ message: error.message }), 3));
19
+ const answer = yield* ActionInit.run({
20
+ ...Option.match(input.name, {
21
+ onNone: () => ({}),
22
+ onSome: (name) => ({ name })
23
+ }),
24
+ ...Option.match(input.description, {
25
+ onNone: () => ({}),
26
+ onSome: (description) => ({ description })
27
+ }),
28
+ force: input.force
29
+ }, { cwd: deps.cwd }).pipe(Effect.catchTags({
30
+ NotAPackageError: refused,
31
+ ActionMetadataMissingError: refused,
32
+ ActionMetadataInvalidError: refused,
33
+ ScaffoldConflictError: refused,
34
+ ScaffoldExistsError: (error) => Effect.fail(CliRuntime.reported(new ActionInitError({ message: `${error.message}: pass --force to replace them` }), 2)),
35
+ ScaffoldWriteError: stopped,
36
+ PlatformError: stopped
37
+ }));
38
+ if (format === "json") {
39
+ yield* Console.log(JSON.stringify(Schema.encodeSync(ActionInitResult)(answer)));
40
+ for (const line of InitDoc.lines(answer)) yield* Effect.logInfo(line);
41
+ } else yield* Doc.print([InitDoc.render(answer)]);
42
+ });
43
+
44
+ export { executeInit };
@@ -0,0 +1,47 @@
1
+ import { ActionDoc } from "../../render/ActionDoc.js";
2
+ import { resolveFormat } from "../schema/execute.js";
3
+ import { ActionBuildFailedError, ActionConfigError, ActionRunError } from "./errors.js";
4
+ import { Console, Effect, Schema } from "effect";
5
+ import { CliRuntime, Doc } from "@effected/cli-kit";
6
+ import { StepSummary } from "@effected/engine/schema";
7
+
8
+ // src/commands/action/execute.ts
9
+ /**
10
+ * Run one `action build`.
11
+ *
12
+ * @remarks
13
+ * The engine's build entry is imported here rather than at the top of the module: it loads the bundler and its
14
+ * toolchain, which `effected --version` and every `effected schema` run have no use for. The report is emitted first and
15
+ * the failure follows, so a run that fails has still said why.
16
+ */
17
+ const execute = Effect.fn("effected.action.execute")(function* (input, deps) {
18
+ const format = yield* resolveFormat(input.format);
19
+ const { ActionRun, ActionRunResult } = yield* Effect.promise(() => import("@effected/engine/build"));
20
+ const refused = (error) => Effect.fail(CliRuntime.reported(new ActionConfigError({ message: error.message }), 2));
21
+ const stopped = (error) => Effect.fail(CliRuntime.reported(new ActionRunError({ message: error.message }), 3));
22
+ const answer = yield* ActionRun.run({
23
+ validate: !input.noValidate,
24
+ persist: !input.noPersist
25
+ }, deps).pipe(Effect.catchTags({
26
+ EffectedConfigError: refused,
27
+ NotAPackageError: refused,
28
+ NotAnActionError: refused,
29
+ BundleFailed: stopped,
30
+ CleanError: stopped,
31
+ WriteError: stopped,
32
+ PersistLocalError: stopped,
33
+ ActionYmlPathError: stopped,
34
+ PlatformError: stopped
35
+ }));
36
+ if (format === "json") {
37
+ yield* Console.log(JSON.stringify(Schema.encodeSync(ActionRunResult)(answer)));
38
+ for (const line of ActionDoc.lines(answer.result)) yield* Effect.logInfo(line);
39
+ } else yield* Doc.print([ActionDoc.render(answer.result)]);
40
+ yield* StepSummary.append(ActionDoc.markdown(answer.result));
41
+ if (answer.verdict === "failed") return yield* Effect.fail(CliRuntime.reported(new ActionBuildFailedError({
42
+ errors: answer.result.validation.errors.length,
43
+ failedEntries: answer.result.build.entries.filter((entry) => !entry.success).length
44
+ }), 1));
45
+ });
46
+
47
+ export { execute };
@@ -0,0 +1,19 @@
1
+ import { formatFlag } from "../schema/flags.js";
2
+ import { Flag } from "effect/cli";
3
+
4
+ // src/commands/action/flags.ts
5
+ /** What `action build` takes. */
6
+ const buildFlags = {
7
+ format: formatFlag,
8
+ noValidate: Flag.Boolean("no-validate").pipe(Flag.withDefault(false), Flag.withDescription("Skip validating the config and action.yml, and the bundle size check")),
9
+ noPersist: Flag.Boolean("no-persist").pipe(Flag.withDefault(false), Flag.withDescription("Skip copying the build to the local action directory"))
10
+ };
11
+ /** What `action init` takes. A name or description left out is read from `package.json`. */
12
+ const initFlags = {
13
+ format: formatFlag,
14
+ name: Flag.String("name").pipe(Flag.optional, Flag.withDescription("The action's display name in action.yml; omitted, package.json's name without its scope")),
15
+ description: Flag.String("description").pipe(Flag.optional, Flag.withDescription("What the action does, for action.yml; omitted, package.json's description")),
16
+ force: Flag.Boolean("force").pipe(Flag.withDefault(false), Flag.withDescription("Replace files that already exist; without it, any existing file stops the run before a write"))
17
+ };
18
+
19
+ export { buildFlags, initFlags };
@@ -0,0 +1,9 @@
1
+ import { makeBuildCommand } from "./build.js";
2
+ import { makeInitCommand } from "./init.js";
3
+ import { Command } from "effect/cli";
4
+
5
+ // src/commands/action/index.ts
6
+ /** `effected action`: the group, closed over the process boundary it runs against. */
7
+ const makeActionCommand = (deps) => Command.make("action", {}).pipe(Command.withDescription("Scaffold or build the GitHub Action in the working directory, from its effected.toml"), Command.withSubcommands([makeBuildCommand(deps), makeInitCommand(deps)]));
8
+
9
+ export { makeActionCommand };
@@ -0,0 +1,9 @@
1
+ import { executeInit } from "./execute-init.js";
2
+ import { initFlags } from "./flags.js";
3
+ import { Command } from "effect/cli";
4
+
5
+ // src/commands/action/init.ts
6
+ /** `effected action init`: scaffold a new GitHub Action in the working directory. */
7
+ const makeInitCommand = (deps) => Command.make("init", initFlags, (input) => executeInit(input, deps)).pipe(Command.withDescription("Scaffold a new GitHub Action in the working directory: the [action] table of effected.toml, action.yml, src/main.ts, src/program.ts and tsconfig.json"));
8
+
9
+ export { makeInitCommand };
@@ -0,0 +1,9 @@
1
+ import { execute } from "./execute.js";
2
+ import { buildFlags } from "./flags.js";
3
+ import { Command } from "effect/cli";
4
+
5
+ // src/commands/package/build.ts
6
+ /** `effected package build`: build the package in the working directory into its `dist/prod`. */
7
+ const makeBuildCommand = (deps) => Command.make("build", buildFlags, (input) => execute(input, deps)).pipe(Command.withDescription("Build the package in the working directory into dist/prod: JS, declarations, the manifest, the API model and any binary"));
8
+
9
+ export { makeBuildCommand };
@@ -0,0 +1,38 @@
1
+ import { Schema } from "effect";
2
+
3
+ // src/commands/package/errors.ts
4
+ /**
5
+ * The build crashed or recorded an error. Exit `1`.
6
+ *
7
+ * @public
8
+ */
9
+ var BuildFailedError = class extends Schema.TaggedError()("BuildFailedError", {
10
+ /** How many errors the build recorded. */
11
+ errors: Schema.Number,
12
+ /** What ended the build, when it did not reach its end. */
13
+ failure: Schema.optionalKey(Schema.String)
14
+ }) {
15
+ get message() {
16
+ const count = `${this.errors} error(s)`;
17
+ return this.failure !== void 0 ? `The build failed with ${count}: ${this.failure}` : `The build recorded ${count}.`;
18
+ }
19
+ };
20
+ /**
21
+ * The build could not run: its `effected.toml` or a plugin would not load, the directory is not a package, or the
22
+ * bundler refused the config before any work. Exit `2`: exit `1` means it ran and found a problem.
23
+ *
24
+ * @remarks
25
+ * Carries the engine's message, which names the file at fault and the key within it. This command's own type rather
26
+ * than the bundler's, because the CLI reaches the bundler only through the engine.
27
+ *
28
+ * @public
29
+ */
30
+ var PackageConfigError = class extends Schema.TaggedError()("PackageConfigError", { message: Schema.String }) {};
31
+ /**
32
+ * A file the build needs could not be read or written. Exit `3`, the infrastructure tier.
33
+ *
34
+ * @public
35
+ */
36
+ var PackageRunError = class extends Schema.TaggedError()("PackageRunError", { message: Schema.String }) {};
37
+
38
+ export { BuildFailedError, PackageConfigError, PackageRunError };
@@ -0,0 +1,52 @@
1
+ import { IssuesDoc } from "../../render/IssuesDoc.js";
2
+ import { resolveFormat } from "../schema/execute.js";
3
+ import { BuildFailedError, PackageConfigError, PackageRunError } from "./errors.js";
4
+ import { Console, Effect, Schema } from "effect";
5
+ import { CliRuntime, Doc } from "@effected/cli-kit";
6
+ import { StepSummary } from "@effected/engine/schema";
7
+ import { Audience } from "@effected/env";
8
+
9
+ // src/commands/package/execute.ts
10
+ /**
11
+ * Run one `package build`.
12
+ *
13
+ * @remarks
14
+ * The engine's build entry is imported here rather than at the top of the module: it loads the bundler and its
15
+ * toolchain, which `effected --version` and every `effected schema` run have no use for. The report is emitted first and
16
+ * the failure follows, so a run that fails has still said why.
17
+ *
18
+ * With `--format json`, stdout holds one document, so CI's annotations go to stderr instead, where the runner reads
19
+ * workflow commands too.
20
+ */
21
+ const execute = Effect.fn("effected.package.execute")(function* (input, deps) {
22
+ const format = yield* resolveFormat(input.format);
23
+ const audience = yield* Audience;
24
+ const { BuildRun, BuildRunResult, renderCiAnnotations } = yield* Effect.promise(() => import("@effected/engine/build"));
25
+ const refused = (error) => Effect.fail(CliRuntime.reported(new PackageConfigError({ message: error.message }), 2));
26
+ const result = yield* BuildRun.run({
27
+ noExe: input.noExe,
28
+ verbose: input.verbose
29
+ }, deps).pipe(Effect.catchTags({
30
+ EffectedConfigError: refused,
31
+ PluginLoadError: refused,
32
+ NotAPackageError: refused,
33
+ ConfigValidationError: refused,
34
+ PlatformError: (error) => Effect.fail(CliRuntime.reported(new PackageRunError({ message: error.message }), 3))
35
+ }));
36
+ const { issues } = result;
37
+ if (format === "json") {
38
+ yield* Console.log(JSON.stringify(Schema.encodeSync(BuildRunResult)(result)));
39
+ for (const line of IssuesDoc.lines(issues)) yield* Effect.logInfo(line);
40
+ } else yield* Doc.print([IssuesDoc.render(issues)]);
41
+ if (audience.kind === "ci") {
42
+ const write = format === "json" ? Console.error : Console.log;
43
+ for (const line of renderCiAnnotations(result.report)) yield* write(line);
44
+ }
45
+ yield* StepSummary.append(IssuesDoc.markdown(issues));
46
+ if (result.verdict === "failed") return yield* Effect.fail(CliRuntime.reported(new BuildFailedError({
47
+ errors: issues.errors.length,
48
+ ...issues.failure !== void 0 ? { failure: issues.failure.message } : {}
49
+ }), 1));
50
+ });
51
+
52
+ export { execute };
@@ -0,0 +1,12 @@
1
+ import { formatFlag } from "../schema/flags.js";
2
+ import { Flag } from "effect/cli";
3
+
4
+ // src/commands/package/flags.ts
5
+ /** What `package build` takes. */
6
+ const buildFlags = {
7
+ format: formatFlag,
8
+ noExe: Flag.Boolean("no-exe").pipe(Flag.withDefault(false), Flag.withDescription("Skip the single-executable compile; the manifest still points at the binary")),
9
+ verbose: Flag.Boolean("verbose").pipe(Flag.withDefault(false), Flag.withDescription("Measure gzip sizes for the report"))
10
+ };
11
+
12
+ export { buildFlags };
@@ -0,0 +1,8 @@
1
+ import { makeBuildCommand } from "./build.js";
2
+ import { Command } from "effect/cli";
3
+
4
+ // src/commands/package/index.ts
5
+ /** `effected package`: the group, closed over the process boundary it runs against. */
6
+ const makePackageCommand = (deps) => Command.make("package", {}).pipe(Command.withDescription("Build the package in the working directory from its effected.toml"), Command.withSubcommands([makeBuildCommand(deps)]));
7
+
8
+ export { makePackageCommand };
package/commands/root.js CHANGED
@@ -1,14 +1,20 @@
1
+ import { makeActionCommand } from "./action/index.js";
2
+ import { makePackageCommand } from "./package/index.js";
3
+ import { makeSchemaCommand } from "./schema/index.js";
1
4
  import { CliAudience } from "@effected/cli-kit";
2
5
  import { Command } from "effect/cli";
3
6
 
4
7
  // src/commands/root.ts
5
8
  /**
6
- * The `effected` command tree. This release carries no subcommand: `--version`
7
- * and `--help` come from the framework, and the audience flags are shared on
8
- * the root so every future subcommand accepts them.
9
+ * The `effected` command tree, closed over the process boundary it runs against: `main` hands in the process values, a
10
+ * test its own. The audience flags are shared on the root so every subcommand accepts them.
9
11
  *
10
12
  * @public
11
13
  */
12
- const rootCommand = Command.make("effected", {}).pipe(Command.withDescription("The effected tool: a toolchain for Effect v4 applications."), Command.withSharedFlags(CliAudience.flags()));
14
+ const makeRootCommand = (deps) => Command.make("effected", {}).pipe(Command.withDescription("The effected tool: a toolchain for Effect v4 applications."), Command.withSharedFlags(CliAudience.flags()), Command.withSubcommands([
15
+ makeSchemaCommand(deps),
16
+ makePackageCommand(deps),
17
+ makeActionCommand(deps)
18
+ ]));
13
19
 
14
- export { rootCommand };
20
+ export { makeRootCommand };
@@ -0,0 +1,114 @@
1
+ import { Console, Effect, PubSub } from "effect";
2
+ import { CliUi, useGlyphs, useStyle } from "@effected/cli-kit/ui";
3
+ import { Box, Text } from "ink";
4
+ import { createElement } from "react";
5
+
6
+ // src/commands/schema/LiveProgress.ts
7
+ const SETTLED = {
8
+ written: {
9
+ label: "written",
10
+ token: "success"
11
+ },
12
+ unchanged: {
13
+ label: "unchanged",
14
+ token: "muted"
15
+ },
16
+ "would-write": {
17
+ label: "would write",
18
+ token: "warning"
19
+ },
20
+ drift: {
21
+ label: "drift",
22
+ token: "error"
23
+ },
24
+ held: {
25
+ label: "held",
26
+ token: "muted"
27
+ },
28
+ "gate-failed": {
29
+ label: "gate failed",
30
+ token: "error"
31
+ }
32
+ };
33
+ const reduce = (state, event) => {
34
+ const update = (name, row) => ({ rows: state.rows.map((current) => current.name === name ? {
35
+ name,
36
+ ...row
37
+ } : current) });
38
+ switch (event._tag) {
39
+ case "Started": return { rows: event.names.map((name) => ({
40
+ name,
41
+ pending: true,
42
+ label: "checking",
43
+ token: "muted"
44
+ })) };
45
+ case "SchemaChecked": return update(event.name, event.blocked ? {
46
+ pending: true,
47
+ label: "gate failed",
48
+ token: "error"
49
+ } : event.verdict === "drift" ? {
50
+ pending: true,
51
+ label: "drift",
52
+ token: "error"
53
+ } : event.change === "none" ? {
54
+ pending: true,
55
+ label: "clean",
56
+ token: "muted"
57
+ } : {
58
+ pending: true,
59
+ label: event.change,
60
+ token: "warning"
61
+ });
62
+ case "SchemaDone": return update(event.report.name, {
63
+ pending: false,
64
+ ...SETTLED[event.report.outcome]
65
+ });
66
+ case "Ended": return state;
67
+ }
68
+ };
69
+ const RowView = ({ row, frame }) => {
70
+ const glyphs = useGlyphs();
71
+ const name = useStyle("emphasis");
72
+ const label = useStyle(row.token);
73
+ const mark = row.pending ? glyphs.spinner[frame % glyphs.spinner.length] ?? "" : glyphs.bullet;
74
+ return createElement(Text, null, createElement(Text, label, `${mark} `), createElement(Text, name, row.name), createElement(Text, label, ` ${row.label}`));
75
+ };
76
+ /**
77
+ * The view's options, less the events: one value, so the test drives what the command runs.
78
+ *
79
+ * @internal
80
+ */
81
+ const liveProgress = {
82
+ initial: { rows: [] },
83
+ reduce,
84
+ render: (state, frame) => createElement(Box, { flexDirection: "column" }, ...state.rows.map((row) => createElement(RowView, {
85
+ key: row.name,
86
+ row,
87
+ frame
88
+ }))),
89
+ isStart: (event) => event._tag === "Started",
90
+ isTerminal: (event) => event._tag === "Ended"
91
+ };
92
+ /**
93
+ * Run the walk under the view: its progress feeds the rows, and its log lines write above the frame.
94
+ *
95
+ * @remarks
96
+ * The frame is committed before the walk's answer is handed back, whichever way it ended, so what follows prints
97
+ * below it and a walk that failed keeps the rows it reached.
98
+ *
99
+ * @internal
100
+ */
101
+ const withLiveProgress = (walk) => Effect.gen(function* () {
102
+ const pubsub = yield* PubSub.unbounded();
103
+ const events = yield* PubSub.subscribe(pubsub);
104
+ const view = yield* CliUi.live({
105
+ ...liveProgress,
106
+ events
107
+ });
108
+ const exit = yield* Effect.exit(walk((event) => Effect.asVoid(PubSub.publish(pubsub, event))).pipe(Effect.provideService(Console.Console, view.logConsole)));
109
+ yield* PubSub.publish(pubsub, { _tag: "Ended" });
110
+ yield* view.close;
111
+ return yield* exit;
112
+ }).pipe(Effect.scoped);
113
+
114
+ export { liveProgress, withLiveProgress };
@@ -0,0 +1,9 @@
1
+ import { execute } from "./execute.js";
2
+ import { commandFlags } from "./flags.js";
3
+ import { Command } from "effect/cli";
4
+
5
+ // src/commands/schema/build.ts
6
+ /** `effected schema build`: generate, gate and write every schema and catalog entry the config declares. */
7
+ const makeBuildCommand = (deps) => Command.make("build", commandFlags, (input) => execute("build", input, deps)).pipe(Command.withDescription("Generate, gate and write every schema the config declares, its catalog slice, and the merged catalog"));
8
+
9
+ export { makeBuildCommand };
@@ -0,0 +1,12 @@
1
+ import { execute } from "./execute.js";
2
+ import { commandFlags } from "./flags.js";
3
+ import { Command } from "effect/cli";
4
+
5
+ // src/commands/schema/check.ts
6
+ /**
7
+ * `effected schema check`: the same walk as `build`, reported and never written. The CI drift gate: it also fails when
8
+ * the committed documents are stale, and on outputs nothing claims, which `build` never deletes.
9
+ */
10
+ const makeCheckCommand = (deps) => Command.make("check", commandFlags, (input) => execute("check", input, deps)).pipe(Command.withDescription("Report what build would do, fail when it would write anything or refuse to, or when a catalog file or document is orphaned or the merged catalog is blocked, and write nothing"));
11
+
12
+ export { makeCheckCommand };
@@ -0,0 +1,121 @@
1
+ import { Schema } from "effect";
2
+
3
+ // src/commands/schema/errors.ts
4
+ const DriftedSchema = Schema.Struct({
5
+ $id: Schema.String,
6
+ change: Schema.Literals([
7
+ "none",
8
+ "created",
9
+ "annotations",
10
+ "contract"
11
+ ]),
12
+ version: Schema.optionalKey(Schema.String),
13
+ nextVersion: Schema.optionalKey(Schema.String)
14
+ });
15
+ /**
16
+ * A published schema drifted under `onDrift: "error"`, so nothing was written. Exit `1`.
17
+ *
18
+ * @public
19
+ */
20
+ var DriftError = class extends Schema.TaggedError()("DriftError", { drifted: Schema.Array(DriftedSchema) }) {
21
+ get count() {
22
+ return this.drifted.length;
23
+ }
24
+ get message() {
25
+ const lines = this.drifted.map((s) => ` ${s.$id}: ${s.change}${s.version !== void 0 ? ` at published ${s.version}` : ""}${s.nextVersion !== void 0 ? ` → suggest ${s.nextVersion}` : ""}`);
26
+ return `${this.count} published schema(s) drifted; nothing was written.\n${lines.join("\n")}\nBump the drifting versions in the config, or re-run with --force to write anyway.`;
27
+ }
28
+ };
29
+ /**
30
+ * A schema failed the lint/validation gate, so nothing was written. Exit `1`.
31
+ *
32
+ * @public
33
+ */
34
+ var GateError = class extends Schema.TaggedError()("GateError", { count: Schema.Number }) {
35
+ get message() {
36
+ return `${this.count} schema(s) failed the lint/validation gate; nothing was written.`;
37
+ }
38
+ };
39
+ /**
40
+ * The merged catalog could not be assembled: a catalog URL or entry name is advertised more than once across the
41
+ * slices, or a slice is not a catalog entry array. Nothing is merged silently, so the merged catalog was left as it
42
+ * is. Exit `1` under both `build` and `check`: only an edit to the slices or the configs clears it.
43
+ *
44
+ * @public
45
+ */
46
+ var CatalogMergeError = class extends Schema.TaggedError()("CatalogMergeError", {
47
+ path: Schema.String,
48
+ conflicts: Schema.Array(Schema.Union([Schema.Struct({
49
+ kind: Schema.Literal("url"),
50
+ url: Schema.String,
51
+ slices: Schema.Array(Schema.String)
52
+ }), Schema.Struct({
53
+ kind: Schema.Literal("name"),
54
+ name: Schema.String,
55
+ slices: Schema.Array(Schema.String)
56
+ })])),
57
+ invalid: Schema.Array(Schema.Struct({
58
+ path: Schema.String,
59
+ reason: Schema.String
60
+ }))
61
+ }) {
62
+ get message() {
63
+ const lines = [...this.conflicts.map((conflict) => ` ${conflict.kind === "url" ? `url ${conflict.url}` : `name ${conflict.name}`} is advertised by ${conflict.slices.join(", ")}`), ...this.invalid.map(({ path, reason }) => ` ${path} is invalid: ${reason}`)];
64
+ return `The merged catalog ${this.path} was not written.\n${lines.join("\n")}\nGive each catalog URL and name to exactly one config, and fix or delete every invalid slice.`;
65
+ }
66
+ };
67
+ /**
68
+ * `check` found committed documents that differ from what the config generates, or outputs nothing claims, which
69
+ * `build` never deletes. `check` is the CI drift gate, so a stale tree fails it. Exit `1`. `count` is every finding;
70
+ * `orphaned` the part of it a build cannot clear, so the message names both remedies.
71
+ *
72
+ * @public
73
+ */
74
+ var StaleError = class extends Schema.TaggedError()("StaleError", {
75
+ count: Schema.Number,
76
+ orphaned: Schema.optionalKey(Schema.Number)
77
+ }) {
78
+ get message() {
79
+ const orphaned = this.orphaned ?? 0;
80
+ const stale = this.count - orphaned;
81
+ return [...stale > 0 ? [`${stale} document(s) are stale; run \`effected schema build\` and commit the result.`] : [], ...orphaned > 0 ? [`${orphaned} orphaned output(s) must be deleted by hand; build never will.`] : []].join(" ");
82
+ }
83
+ };
84
+ /**
85
+ * The payload does not conform to the resolved document: the engine answered findings, each already reported. Exit
86
+ * `1`: a validation verdict, the reason CI runs the command.
87
+ *
88
+ * @public
89
+ */
90
+ var ValidationFailedError = class extends Schema.TaggedError()("ValidationFailedError", {
91
+ count: Schema.Number,
92
+ payload: Schema.String,
93
+ schema: Schema.String
94
+ }) {
95
+ get message() {
96
+ return `${this.count} finding(s): ${this.payload} does not conform to ${this.schema}`;
97
+ }
98
+ };
99
+ /**
100
+ * No schemastore config was found. Exit `2`: the command could not run, where exit `1` means it ran and found a
101
+ * problem. The engine's error says what was searched; this adds the way out, which is the front end's to state because
102
+ * it shows the command that was run.
103
+ *
104
+ * @public
105
+ */
106
+ var NoConfigError = class extends Schema.TaggedError()("NoConfigError", {
107
+ /** The file names looked for. */
108
+ names: Schema.Array(Schema.String),
109
+ /** The directories walked, or the one explicit path checked. */
110
+ searched: Schema.Array(Schema.String),
111
+ /** Whether `searched` is the one path the user named. */
112
+ explicit: Schema.Boolean,
113
+ /** What follows `effected schema` in the example: the subcommand and its positionals, with a config path. */
114
+ usage: Schema.String
115
+ }) {
116
+ get message() {
117
+ return `${this.explicit ? `The schemastore config ${this.searched.join(", ")} does not exist.` : `No schemastore config found. Looked for ${this.names.join(", ")} in: ${this.searched.join(", ")}.`}\nRun next to a config, or pass its path, for example: effected schema ${this.usage}`;
118
+ }
119
+ };
120
+
121
+ export { CatalogMergeError, DriftError, GateError, NoConfigError, StaleError, ValidationFailedError };