@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 +9 -2
- package/ci.js +16 -0
- package/commands/action/build.js +9 -0
- package/commands/action/errors.js +54 -0
- package/commands/action/execute-init.js +44 -0
- package/commands/action/execute.js +47 -0
- package/commands/action/flags.js +19 -0
- package/commands/action/index.js +9 -0
- package/commands/action/init.js +9 -0
- package/commands/package/build.js +9 -0
- package/commands/package/errors.js +38 -0
- package/commands/package/execute.js +52 -0
- package/commands/package/flags.js +12 -0
- package/commands/package/index.js +8 -0
- package/commands/root.js +11 -5
- package/commands/schema/LiveProgress.js +114 -0
- package/commands/schema/build.js +9 -0
- package/commands/schema/check.js +12 -0
- package/commands/schema/errors.js +121 -0
- package/commands/schema/execute.js +141 -0
- package/commands/schema/flags.js +24 -0
- package/commands/schema/index.js +14 -0
- package/commands/schema/validate.js +58 -0
- package/index.d.ts +231 -15
- package/index.js +5 -2
- package/main.d.ts +46 -1
- package/main.js +27 -8
- package/package.json +14 -7
- package/render/ActionDoc.js +66 -0
- package/render/DiffDoc.js +51 -0
- package/render/InitDoc.js +35 -0
- package/render/IssuesDoc.js +73 -0
- package/version.js +1 -1
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
|
|
7
|
-
*
|
|
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
|
|
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 {
|
|
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 };
|