@effected/cli 0.26.0 → 0.27.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/commands/root.js +5 -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 +117 -15
- package/index.js +3 -2
- package/main.d.ts +24 -1
- package/main.js +23 -8
- package/package.json +10 -7
- package/render/DiffDoc.js +51 -0
- package/version.js +1 -1
package/commands/root.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import { makeSchemaCommand } from "./schema/index.js";
|
|
1
2
|
import { CliAudience } from "@effected/cli-kit";
|
|
2
3
|
import { Command } from "effect/cli";
|
|
3
4
|
|
|
4
5
|
// src/commands/root.ts
|
|
5
6
|
/**
|
|
6
|
-
* The `effected` command tree
|
|
7
|
-
*
|
|
8
|
-
* the root so every future subcommand accepts them.
|
|
7
|
+
* The `effected` command tree, closed over the process boundary it runs against: `main` hands in the process values, a
|
|
8
|
+
* test its own. The audience flags are shared on the root so every subcommand accepts them.
|
|
9
9
|
*
|
|
10
10
|
* @public
|
|
11
11
|
*/
|
|
12
|
-
const
|
|
12
|
+
const makeRootCommand = (deps) => Command.make("effected", {}).pipe(Command.withDescription("The effected tool: a toolchain for Effect v4 applications."), Command.withSharedFlags(CliAudience.flags()), Command.withSubcommands([makeSchemaCommand(deps)]));
|
|
13
13
|
|
|
14
|
-
export {
|
|
14
|
+
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 };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { DiffDoc } from "../../render/DiffDoc.js";
|
|
2
|
+
import { CatalogMergeError, DriftError, GateError, NoConfigError, StaleError } from "./errors.js";
|
|
3
|
+
import { CliInteractive, CliRuntime, Doc } from "@effected/cli-kit";
|
|
4
|
+
import { ConfigLoadError, ConfigLoader, Report, SchemaRun, StepSummary } from "@effected/engine/schema";
|
|
5
|
+
import { Audience } from "@effected/env";
|
|
6
|
+
import { Console, Effect, Option } from "effect";
|
|
7
|
+
|
|
8
|
+
// src/commands/schema/execute.ts
|
|
9
|
+
/** `--format` as given, else what the audience reads: JSON for an agent, human text for a person and for CI. */
|
|
10
|
+
const resolveFormat = (format) => Effect.map(Audience, (audience) => Option.getOrElse(format, () => audience.kind === "agent" ? "json" : "human"));
|
|
11
|
+
/** Warned before the walk, because an error from the walk does not carry the mismatch. */
|
|
12
|
+
const warnVersionMismatch = (mismatch) => Effect.logWarning(`VERSION MISMATCH IGNORED (--ignore-version-mismatch): ${mismatch.message}`);
|
|
13
|
+
/** A missing config, with the way out; a load failure's first line, with the rest (a stack) kept at debug. */
|
|
14
|
+
const configFailures = (usage, config) => ({
|
|
15
|
+
ConfigNotFoundError: (error) => Effect.fail(CliRuntime.reported(new NoConfigError({
|
|
16
|
+
names: ConfigLoader.CONFIG_NAMES,
|
|
17
|
+
searched: error.searched,
|
|
18
|
+
explicit: Option.isSome(config),
|
|
19
|
+
usage
|
|
20
|
+
}), 2)),
|
|
21
|
+
ConfigLoadError: (error) => Effect.gen(function* () {
|
|
22
|
+
const [first = "", ...rest] = error.reason.split("\n");
|
|
23
|
+
if (rest.length > 0) yield* Effect.logDebug(rest.join("\n"));
|
|
24
|
+
return yield* Effect.fail(CliRuntime.reported(new ConfigLoadError({
|
|
25
|
+
path: error.path,
|
|
26
|
+
reason: first
|
|
27
|
+
}), 2));
|
|
28
|
+
})
|
|
29
|
+
});
|
|
30
|
+
const LINE_TOKENS = [
|
|
31
|
+
["DRIFT", "error"],
|
|
32
|
+
["GATE FAILED", "error"],
|
|
33
|
+
["CATALOG BLOCKED", "error"],
|
|
34
|
+
["would write", "warning"],
|
|
35
|
+
["orphaned", "warning"],
|
|
36
|
+
["written", "success"],
|
|
37
|
+
["unchanged", "muted"],
|
|
38
|
+
["held", "muted"]
|
|
39
|
+
];
|
|
40
|
+
const paintedLine = (line) => {
|
|
41
|
+
const token = LINE_TOKENS.find(([prefix]) => line.startsWith(prefix))?.[1];
|
|
42
|
+
return token === void 0 ? Doc.text(line) : Doc.text(line, token);
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The human report: the outcome lines, each changed document's diff, then the summary, which `Report.human` writes
|
|
46
|
+
* last and which stays at the bottom where a person's eye lands. `Doc.text` is data to every renderer, so a path or a
|
|
47
|
+
* diff line cannot repaint the terminal.
|
|
48
|
+
*/
|
|
49
|
+
const humanDocument = (report) => {
|
|
50
|
+
const lines = Report.human(report);
|
|
51
|
+
const outcomes = lines.slice(0, -1);
|
|
52
|
+
const summary = lines.slice(-1);
|
|
53
|
+
return Doc.section(void 0, [
|
|
54
|
+
...outcomes.length > 0 ? [Doc.lines(outcomes.map(paintedLine), { wrap: false })] : [],
|
|
55
|
+
...report.schemas.flatMap((schema) => schema.diff !== void 0 ? [DiffDoc.render(schema.path, schema.diff)] : []),
|
|
56
|
+
Doc.lines(summary.map(paintedLine), { wrap: false })
|
|
57
|
+
]);
|
|
58
|
+
};
|
|
59
|
+
const emit = (report, format, versionMismatch) => Effect.gen(function* () {
|
|
60
|
+
if (format === "json") {
|
|
61
|
+
yield* Console.log(Report.json(report, versionMismatch));
|
|
62
|
+
for (const line of Report.human(report)) yield* Effect.logInfo(line);
|
|
63
|
+
} else yield* Doc.print([humanDocument(report)]);
|
|
64
|
+
for (const line of Report.warnings(report)) yield* Effect.logWarning(line);
|
|
65
|
+
});
|
|
66
|
+
const failOnVerdict = (result) => {
|
|
67
|
+
const { report, verdict } = result;
|
|
68
|
+
const merged = report.catalog?.merged;
|
|
69
|
+
switch (verdict) {
|
|
70
|
+
case "ok": return Effect.void;
|
|
71
|
+
case "gate-failed": {
|
|
72
|
+
const count = report.schemas.filter((schema) => schema.outcome === "gate-failed").length;
|
|
73
|
+
return Effect.fail(CliRuntime.reported(new GateError({ count }), 1));
|
|
74
|
+
}
|
|
75
|
+
case "drift": {
|
|
76
|
+
const drifted = report.schemas.filter((schema) => schema.verdict === "drift").map((schema) => ({
|
|
77
|
+
$id: schema.$id,
|
|
78
|
+
change: schema.change,
|
|
79
|
+
...schema.version !== void 0 ? { version: schema.version } : {},
|
|
80
|
+
...schema.nextVersion !== void 0 ? { nextVersion: schema.nextVersion } : {}
|
|
81
|
+
}));
|
|
82
|
+
return Effect.fail(CliRuntime.reported(new DriftError({ drifted }), 1));
|
|
83
|
+
}
|
|
84
|
+
case "catalog-blocked": return Effect.fail(CliRuntime.reported(new CatalogMergeError({
|
|
85
|
+
path: merged?.path ?? "",
|
|
86
|
+
conflicts: merged?.conflicts ?? [],
|
|
87
|
+
invalid: merged?.invalid ?? []
|
|
88
|
+
}), 1));
|
|
89
|
+
case "stale": {
|
|
90
|
+
const catalogFiles = [report.catalog?.slice?.outcome, merged?.outcome];
|
|
91
|
+
const orphaned = catalogFiles.filter((outcome) => outcome === "orphaned").length + (report.orphaned?.length ?? 0);
|
|
92
|
+
const count = report.schemas.filter((schema) => schema.outcome === "would-write").length + catalogFiles.filter((outcome) => outcome === "would-write").length + orphaned;
|
|
93
|
+
return Effect.fail(CliRuntime.reported(new StaleError({
|
|
94
|
+
count,
|
|
95
|
+
...orphaned > 0 ? { orphaned } : {}
|
|
96
|
+
}), 1));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Run one `build` or `check`.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* The live view is for a person watching, so it needs human output and an interactive run, which `CliInteractive` only
|
|
105
|
+
* ever answers `true` for a human audience with terminals. That module, and Ink and React with it, is imported only
|
|
106
|
+
* then: an agent, CI or piped run never loads them. The report is emitted first and the failure follows, so a run that
|
|
107
|
+
* fails has still said why.
|
|
108
|
+
*/
|
|
109
|
+
const execute = Effect.fn("effected.schema.execute")(function* (mode, input, deps) {
|
|
110
|
+
const format = yield* resolveFormat(input.format);
|
|
111
|
+
const interactive = yield* CliInteractive;
|
|
112
|
+
const announce = (event) => Effect.gen(function* () {
|
|
113
|
+
if (event._tag !== "Started") return;
|
|
114
|
+
if (input.force) yield* Effect.logWarning(`--force: drift policy is allow for this run; a published document ${mode === "check" ? "would be" : "may be"} rewritten in place, which breaks every consumer pinned to its URL.`);
|
|
115
|
+
if (event.versionMismatch !== void 0) yield* warnVersionMismatch(event.versionMismatch);
|
|
116
|
+
});
|
|
117
|
+
const walk = (onProgress) => SchemaRun.run({
|
|
118
|
+
mode,
|
|
119
|
+
force: input.force,
|
|
120
|
+
versionMismatch: input.ignoreVersionMismatch ? "warn" : "fail",
|
|
121
|
+
...Option.isSome(input.config) ? { config: input.config.value } : {},
|
|
122
|
+
...Option.isSome(input.drift) ? { drift: input.drift.value } : {},
|
|
123
|
+
...Option.isSome(input.onDrift) ? { onDrift: input.onDrift.value } : {}
|
|
124
|
+
}, {
|
|
125
|
+
...deps,
|
|
126
|
+
onProgress: (event) => Effect.andThen(announce(event), onProgress(event))
|
|
127
|
+
});
|
|
128
|
+
const result = yield* (format === "human" && interactive ? Effect.flatMap(Effect.promise(() => import("./LiveProgress.js")), ({ withLiveProgress }) => withLiveProgress(walk)) : walk(() => Effect.void)).pipe(Effect.catchTags({
|
|
129
|
+
...configFailures(`${mode} lib/schemastore.config.ts`, input.config),
|
|
130
|
+
ConflictingFlagsError: (error) => Effect.fail(CliRuntime.reported(error, 64)),
|
|
131
|
+
VersionMismatchError: (error) => Effect.fail(CliRuntime.reported(error, 2)),
|
|
132
|
+
FrozenVersionMissingError: (error) => Effect.fail(CliRuntime.reported(error, 1)),
|
|
133
|
+
FrozenVersionIdMismatchError: (error) => Effect.fail(CliRuntime.reported(error, 1)),
|
|
134
|
+
CatalogDirError: (error) => Effect.fail(CliRuntime.reported(error, 2))
|
|
135
|
+
}));
|
|
136
|
+
yield* emit(result.report, format, result.versionMismatch);
|
|
137
|
+
yield* StepSummary.append(Report.markdown(result.report));
|
|
138
|
+
yield* failOnVerdict(result);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export { configFailures, execute, resolveFormat, warnVersionMismatch };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Argument, Flag } from "effect/cli";
|
|
2
|
+
|
|
3
|
+
// src/commands/schema/flags.ts
|
|
4
|
+
/** The optional positional config path; omitted, the config is discovered upward from the working directory. */
|
|
5
|
+
const configArgument = Argument.String("config").pipe(Argument.withDescription("Path to a schemastore config module; omitted, schemastore.config.{ts,mts,js,mjs} is searched upward from the working directory"), Argument.optional);
|
|
6
|
+
/** `--format`: absent, JSON for an agent and human text for everyone else. */
|
|
7
|
+
const formatFlag = Flag.Literals("format", ["human", "json"]).pipe(Flag.withDescription("Output format, by default json for an agent and human otherwise; json writes one document to stdout and moves human text to stderr"), Flag.optional);
|
|
8
|
+
/** `--ignore-version-mismatch`: run on past a config from another `@effected/schemastore`. */
|
|
9
|
+
const ignoreVersionMismatchFlag = Flag.Boolean("ignore-version-mismatch").pipe(Flag.withDefault(false), Flag.withDescription("Run even when the config's values come from a different @effected/schemastore than this tool's; warns loudly"));
|
|
10
|
+
/** What `build` and `check` take. */
|
|
11
|
+
const commandFlags = {
|
|
12
|
+
config: configArgument,
|
|
13
|
+
drift: Flag.Literals("drift", [
|
|
14
|
+
"strict",
|
|
15
|
+
"semantic",
|
|
16
|
+
"allow"
|
|
17
|
+
]).pipe(Flag.withDescription("Drift tolerance for published schemas; overrides every schema's drift"), Flag.optional),
|
|
18
|
+
onDrift: Flag.Literals("on-drift", ["error", "warn"]).pipe(Flag.withDescription("What drift does: refuse every write (error) or write and warn; overrides the config's onDrift"), Flag.optional),
|
|
19
|
+
force: Flag.Boolean("force").pipe(Flag.withDefault(false), Flag.withDescription("Shorthand for --drift=allow")),
|
|
20
|
+
format: formatFlag,
|
|
21
|
+
ignoreVersionMismatch: ignoreVersionMismatchFlag
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export { commandFlags, configArgument, formatFlag, ignoreVersionMismatchFlag };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { makeBuildCommand } from "./build.js";
|
|
2
|
+
import { makeCheckCommand } from "./check.js";
|
|
3
|
+
import { makeValidateCommand } from "./validate.js";
|
|
4
|
+
import { Command } from "effect/cli";
|
|
5
|
+
|
|
6
|
+
// src/commands/schema/index.ts
|
|
7
|
+
/** `effected schema`: the group, closed over the process boundary it runs against. */
|
|
8
|
+
const makeSchemaCommand = (deps) => Command.make("schema", {}).pipe(Command.withDescription("Build and check SchemaStore-shaped JSON Schema documents from a schemastore.config.ts, and validate payloads against them"), Command.withSubcommands([
|
|
9
|
+
makeBuildCommand(deps),
|
|
10
|
+
makeCheckCommand(deps),
|
|
11
|
+
makeValidateCommand(deps)
|
|
12
|
+
]));
|
|
13
|
+
|
|
14
|
+
export { makeSchemaCommand };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ValidationFailedError } from "./errors.js";
|
|
2
|
+
import { configFailures, resolveFormat, warnVersionMismatch } from "./execute.js";
|
|
3
|
+
import { configArgument, formatFlag, ignoreVersionMismatchFlag } from "./flags.js";
|
|
4
|
+
import { CliRuntime, Doc } from "@effected/cli-kit";
|
|
5
|
+
import { Argument, Command, Flag } from "effect/cli";
|
|
6
|
+
import { SchemaValidate, ValidateReport } from "@effected/engine/schema";
|
|
7
|
+
import { Console, Effect, Option, Schema } from "effect";
|
|
8
|
+
|
|
9
|
+
// src/commands/schema/validate.ts
|
|
10
|
+
const findingLine = (finding) => {
|
|
11
|
+
const at = finding.location;
|
|
12
|
+
const where = at !== void 0 ? `${at.file}:${at.line}:${at.column}: ` : " ";
|
|
13
|
+
const keyword = finding.keyword !== void 0 ? ` [${finding.keyword}]` : "";
|
|
14
|
+
return `${where}${finding.path === "" ? "(root)" : finding.path}: ${finding.message}${keyword}`;
|
|
15
|
+
};
|
|
16
|
+
const humanLines = (report) => report.valid ? [`valid ${report.payload} against ${report.source}`] : [...report.findings.map(findingLine), `${report.findings.length} finding(s): ${report.payload} does not conform to ${report.$ref}`];
|
|
17
|
+
/** Run one `validate`. */
|
|
18
|
+
const runValidate = Effect.fn("effected.schema.validate")(function* (input, deps) {
|
|
19
|
+
const format = yield* resolveFormat(input.format);
|
|
20
|
+
const report = yield* SchemaValidate.run({
|
|
21
|
+
payload: input.payload,
|
|
22
|
+
versionMismatch: input.ignoreVersionMismatch ? "warn" : "fail",
|
|
23
|
+
...Option.isSome(input.config) ? { config: input.config.value } : {},
|
|
24
|
+
...Option.isSome(input.schema) ? { schema: input.schema.value } : {}
|
|
25
|
+
}, {
|
|
26
|
+
...deps,
|
|
27
|
+
onProgress: (event) => event._tag === "Started" && event.versionMismatch !== void 0 ? warnVersionMismatch(event.versionMismatch) : Effect.void
|
|
28
|
+
}).pipe(Effect.catchTags({
|
|
29
|
+
...configFailures("validate payload.json lib/schemastore.config.ts", input.config),
|
|
30
|
+
VersionMismatchError: (error) => Effect.fail(CliRuntime.reported(error, 2)),
|
|
31
|
+
PayloadError: (error) => Effect.fail(CliRuntime.reported(error, 2)),
|
|
32
|
+
SchemaResolutionError: (error) => Effect.fail(CliRuntime.reported(error, 2)),
|
|
33
|
+
MissingSchemaRefError: (error) => Effect.fail(CliRuntime.reported(error, 64))
|
|
34
|
+
}));
|
|
35
|
+
const lines = humanLines(report);
|
|
36
|
+
if (format === "json") {
|
|
37
|
+
yield* Console.log(JSON.stringify(Schema.encodeSync(ValidateReport.Json)(report)));
|
|
38
|
+
for (const line of lines) yield* Effect.logInfo(line);
|
|
39
|
+
} else yield* Doc.print([Doc.lines(lines.map((line, index) => Doc.text(line, report.valid ? "success" : index === lines.length - 1 ? "emphasis" : "error")), { wrap: false })]);
|
|
40
|
+
if (!report.valid) return yield* Effect.fail(CliRuntime.reported(new ValidationFailedError({
|
|
41
|
+
count: report.findings.length,
|
|
42
|
+
payload: report.payload,
|
|
43
|
+
schema: report.$ref
|
|
44
|
+
}), 1));
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* `effected schema validate`: check a payload against a published schema document, named by `--schema` or the payload's
|
|
48
|
+
* `$schema`: a file path, or an identity the config derives. No network is involved.
|
|
49
|
+
*/
|
|
50
|
+
const makeValidateCommand = (deps) => Command.make("validate", {
|
|
51
|
+
payload: Argument.String("payload").pipe(Argument.withDescription("Path to the JSON payload to validate")),
|
|
52
|
+
schema: Flag.String("schema").pipe(Flag.optional, Flag.withDescription("The document to validate against: a file path, or a $id/url a config schema derives; omitted, the payload's own $schema property")),
|
|
53
|
+
config: configArgument,
|
|
54
|
+
format: formatFlag,
|
|
55
|
+
ignoreVersionMismatch: ignoreVersionMismatchFlag
|
|
56
|
+
}, (input) => runValidate(input, deps)).pipe(Command.withDescription("Validate a JSON payload against a published schema document, resolved by path or by the config's derived $id/url identities"));
|
|
57
|
+
|
|
58
|
+
export { makeValidateCommand, runValidate };
|
package/index.d.ts
CHANGED
|
@@ -1,22 +1,123 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "effect/cli";
|
|
2
|
+
import { SchemaRunDeps } from "@effected/engine/schema";
|
|
3
|
+
import "@effected/env";
|
|
4
|
+
import { Schema } from "effect";
|
|
5
|
+
declare const DriftError_base: Schema.Class<DriftError, Schema.TaggedStruct<"DriftError", {
|
|
6
|
+
readonly drifted: Schema.$Array<Schema.Struct<{
|
|
7
|
+
readonly $id: Schema.String;
|
|
8
|
+
readonly change: Schema.Literals<readonly ["none", "created", "annotations", "contract"]>;
|
|
9
|
+
readonly version: Schema.optionalKey<Schema.String>;
|
|
10
|
+
readonly nextVersion: Schema.optionalKey<Schema.String>;
|
|
11
|
+
}>>;
|
|
12
|
+
}>, import("effect/Cause").YieldableError>;
|
|
2
13
|
/**
|
|
3
|
-
*
|
|
4
|
-
* and `--help` come from the framework, and the audience flags are shared on
|
|
5
|
-
* the root so every future subcommand accepts them.
|
|
14
|
+
* A published schema drifted under `onDrift: "error"`, so nothing was written. Exit `1`.
|
|
6
15
|
*
|
|
7
16
|
* @public
|
|
8
17
|
*/
|
|
9
|
-
export declare
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
export declare class DriftError extends DriftError_base {
|
|
19
|
+
get count(): number;
|
|
20
|
+
get message(): string;
|
|
21
|
+
}
|
|
22
|
+
declare const GateError_base: Schema.Class<GateError, Schema.TaggedStruct<"GateError", {
|
|
23
|
+
readonly count: Schema.Number;
|
|
24
|
+
}>, import("effect/Cause").YieldableError>;
|
|
25
|
+
/**
|
|
26
|
+
* A schema failed the lint/validation gate, so nothing was written. Exit `1`.
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export declare class GateError extends GateError_base {
|
|
31
|
+
get message(): string;
|
|
32
|
+
}
|
|
33
|
+
declare const CatalogMergeError_base: Schema.Class<CatalogMergeError, Schema.TaggedStruct<"CatalogMergeError", {
|
|
34
|
+
readonly path: Schema.String;
|
|
35
|
+
readonly conflicts: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
|
|
36
|
+
readonly kind: Schema.Literal<"url">;
|
|
37
|
+
readonly url: Schema.String;
|
|
38
|
+
readonly slices: Schema.$Array<Schema.String>;
|
|
39
|
+
}>, Schema.Struct<{
|
|
40
|
+
readonly kind: Schema.Literal<"name">;
|
|
41
|
+
readonly name: Schema.String;
|
|
42
|
+
readonly slices: Schema.$Array<Schema.String>;
|
|
43
|
+
}>]>>;
|
|
44
|
+
readonly invalid: Schema.$Array<Schema.Struct<{
|
|
45
|
+
readonly path: Schema.String;
|
|
46
|
+
readonly reason: Schema.String;
|
|
47
|
+
}>>;
|
|
48
|
+
}>, import("effect/Cause").YieldableError>;
|
|
49
|
+
/**
|
|
50
|
+
* The merged catalog could not be assembled: a catalog URL or entry name is advertised more than once across the
|
|
51
|
+
* slices, or a slice is not a catalog entry array. Nothing is merged silently, so the merged catalog was left as it
|
|
52
|
+
* is. Exit `1` under both `build` and `check`: only an edit to the slices or the configs clears it.
|
|
53
|
+
*
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
export declare class CatalogMergeError extends CatalogMergeError_base {
|
|
57
|
+
get message(): string;
|
|
58
|
+
}
|
|
59
|
+
declare const StaleError_base: Schema.Class<StaleError, Schema.TaggedStruct<"StaleError", {
|
|
60
|
+
readonly count: Schema.Number;
|
|
61
|
+
readonly orphaned: Schema.optionalKey<Schema.Number>;
|
|
62
|
+
}>, import("effect/Cause").YieldableError>;
|
|
63
|
+
/**
|
|
64
|
+
* `check` found committed documents that differ from what the config generates, or outputs nothing claims, which
|
|
65
|
+
* `build` never deletes. `check` is the CI drift gate, so a stale tree fails it. Exit `1`. `count` is every finding;
|
|
66
|
+
* `orphaned` the part of it a build cannot clear, so the message names both remedies.
|
|
67
|
+
*
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
export declare class StaleError extends StaleError_base {
|
|
71
|
+
get message(): string;
|
|
72
|
+
}
|
|
73
|
+
declare const ValidationFailedError_base: Schema.Class<ValidationFailedError, Schema.TaggedStruct<"ValidationFailedError", {
|
|
74
|
+
readonly count: Schema.Number;
|
|
75
|
+
readonly payload: Schema.String;
|
|
76
|
+
readonly schema: Schema.String;
|
|
77
|
+
}>, import("effect/Cause").YieldableError>;
|
|
78
|
+
/**
|
|
79
|
+
* The payload does not conform to the resolved document: the engine answered findings, each already reported. Exit
|
|
80
|
+
* `1`: a validation verdict, the reason CI runs the command.
|
|
81
|
+
*
|
|
82
|
+
* @public
|
|
83
|
+
*/
|
|
84
|
+
export declare class ValidationFailedError extends ValidationFailedError_base {
|
|
85
|
+
get message(): string;
|
|
86
|
+
}
|
|
87
|
+
declare const NoConfigError_base: Schema.Class<NoConfigError, Schema.TaggedStruct<"NoConfigError", {
|
|
88
|
+
/** The file names looked for. */
|
|
89
|
+
readonly names: Schema.$Array<Schema.String>;
|
|
90
|
+
/** The directories walked, or the one explicit path checked. */
|
|
91
|
+
readonly searched: Schema.$Array<Schema.String>;
|
|
92
|
+
/** Whether `searched` is the one path the user named. */
|
|
93
|
+
readonly explicit: Schema.Boolean;
|
|
94
|
+
/** What follows `effected schema` in the example: the subcommand and its positionals, with a config path. */
|
|
95
|
+
readonly usage: Schema.String;
|
|
96
|
+
}>, import("effect/Cause").YieldableError>;
|
|
97
|
+
/**
|
|
98
|
+
* No schemastore config was found. Exit `2`: the command could not run, where exit `1` means it ran and found a
|
|
99
|
+
* problem. The engine's error says what was searched; this adds the way out, which is the front end's to state because
|
|
100
|
+
* it shows the command that was run.
|
|
101
|
+
*
|
|
102
|
+
* @public
|
|
103
|
+
*/
|
|
104
|
+
export declare class NoConfigError extends NoConfigError_base {
|
|
105
|
+
get message(): string;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* What `effected schema` is given by the process boundary, and the test seams: the engine's dependencies, less the
|
|
109
|
+
* progress callback the command supplies itself. `main` reads `cwd`, and nothing else reads the process.
|
|
110
|
+
*
|
|
111
|
+
* @public
|
|
112
|
+
*/
|
|
113
|
+
type SchemaCommandDeps = Omit<SchemaRunDeps, "onProgress">;
|
|
114
|
+
/**
|
|
115
|
+
* The `effected` command tree, closed over the process boundary it runs against: `main` hands in the process values, a
|
|
116
|
+
* test its own. The audience flags are shared on the root so every subcommand accepts them.
|
|
117
|
+
*
|
|
118
|
+
* @public
|
|
119
|
+
*/
|
|
120
|
+
export declare const makeRootCommand: any;
|
|
20
121
|
/**
|
|
21
122
|
* The `effected` command's version. The bundler replaces the exact expression
|
|
22
123
|
* `process.env.__PACKAGE_VERSION__` with the package version at build time, so
|
|
@@ -25,4 +126,5 @@ export declare const rootCommand: Command.Command<"effected", {
|
|
|
25
126
|
* @public
|
|
26
127
|
*/
|
|
27
128
|
export declare const CLI_VERSION: string;
|
|
129
|
+
export type { SchemaCommandDeps };
|
|
28
130
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makeRootCommand } from "./commands/root.js";
|
|
2
|
+
import { CatalogMergeError, DriftError, GateError, NoConfigError, StaleError, ValidationFailedError } from "./commands/schema/errors.js";
|
|
2
3
|
import { CLI_VERSION } from "./version.js";
|
|
3
4
|
|
|
4
|
-
export { CLI_VERSION,
|
|
5
|
+
export { CLI_VERSION, CatalogMergeError, DriftError, GateError, NoConfigError, StaleError, ValidationFailedError, makeRootCommand };
|
package/main.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { Distribution } from "@effected/kit";
|
|
2
2
|
import { Layer } from "effect";
|
|
3
|
+
import { SchemaRunDeps } from "@effected/engine/schema";
|
|
4
|
+
import "@effected/env";
|
|
5
|
+
/**
|
|
6
|
+
* What `effected schema` is given by the process boundary, and the test seams: the engine's dependencies, less the
|
|
7
|
+
* progress callback the command supplies itself. `main` reads `cwd`, and nothing else reads the process.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
type SchemaCommandDeps = Omit<SchemaRunDeps, "onProgress">;
|
|
3
12
|
/**
|
|
4
13
|
* Options a carrier's bin shim passes to {@link main}. `distribution` names
|
|
5
14
|
* the package the `effected` bin was installed through; omitted for a direct
|
|
@@ -10,6 +19,19 @@ import { Layer } from "effect";
|
|
|
10
19
|
export interface MainOptions {
|
|
11
20
|
readonly distribution?: Distribution;
|
|
12
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* How `CliRuntime.main` runs the command: exit `3` for a typed error that carries no code of its own, the infrastructure
|
|
24
|
+
* tier, and this module as the program's own, so the failure report's span trail keeps the command's spans when it is
|
|
25
|
+
* installed under `node_modules/@effected/` and leaves out the kit's.
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export declare const mainOptions: {
|
|
30
|
+
readonly exitCode: 3;
|
|
31
|
+
readonly env: {
|
|
32
|
+
readonly appModule: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
13
35
|
/**
|
|
14
36
|
* The `effected` program under `CliRuntime.main`, over `platform`: what
|
|
15
37
|
* {@link main} runs with the Node services, and what a test runs over a test
|
|
@@ -17,7 +39,7 @@ export interface MainOptions {
|
|
|
17
39
|
*
|
|
18
40
|
* @public
|
|
19
41
|
*/
|
|
20
|
-
export declare const run: <RP, EP>(options: MainOptions, platform: Layer.Layer<RP, EP>) => import("effect/Effect").Effect<void, Error, Exclude<import("effect/process/ChildProcessSpawner").ChildProcessSpawner, RP> | Exclude<import("effect/FileSystem").FileSystem, RP> | Exclude<import("effect/Path").Path, RP> | Exclude<import("effect/Stdio").Stdio, RP> | Exclude<import("effect/Stdio").Stdio, RP> | Exclude<import("effect/Terminal").Terminal, RP>>;
|
|
42
|
+
export declare const run: <RP, EP>(options: MainOptions, deps: SchemaCommandDeps, platform: Layer.Layer<RP, EP>) => import("effect/Effect").Effect<void, Error, Exclude<import("effect/process/ChildProcessSpawner").ChildProcessSpawner, RP> | Exclude<import("effect/FileSystem").FileSystem, RP> | Exclude<import("effect/Path").Path, RP> | Exclude<import("effect/Stdio").Stdio, RP> | Exclude<import("effect/Stdio").Stdio, RP> | Exclude<import("effect/Terminal").Terminal, RP>>;
|
|
21
43
|
/**
|
|
22
44
|
* Run the `effected` command. Owns the process: installs the runtime teardown
|
|
23
45
|
* and sets the exit code.
|
|
@@ -25,4 +47,5 @@ export declare const run: <RP, EP>(options: MainOptions, platform: Layer.Layer<R
|
|
|
25
47
|
* @public
|
|
26
48
|
*/
|
|
27
49
|
export declare const main: (options?: MainOptions) => void;
|
|
50
|
+
export type { SchemaCommandDeps };
|
|
28
51
|
//# sourceMappingURL=main.d.ts.map
|
package/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makeRootCommand } from "./commands/root.js";
|
|
2
2
|
import { CLI_VERSION } from "./version.js";
|
|
3
|
-
import { CliAudience, CliRuntime } from "@effected/cli-kit";
|
|
3
|
+
import { CliAudience, CliLogger, CliRuntime } from "@effected/cli-kit";
|
|
4
4
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
5
5
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
6
6
|
|
|
@@ -17,20 +17,35 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
|
17
17
|
*/
|
|
18
18
|
const versionFormatter = { formatVersion: (name, version) => `${name} ${version}` };
|
|
19
19
|
/**
|
|
20
|
+
* How `CliRuntime.main` runs the command: exit `3` for a typed error that carries no code of its own, the infrastructure
|
|
21
|
+
* tier, and this module as the program's own, so the failure report's span trail keeps the command's spans when it is
|
|
22
|
+
* installed under `node_modules/@effected/` and leaves out the kit's.
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
const mainOptions = {
|
|
27
|
+
exitCode: 3,
|
|
28
|
+
env: { appModule: import.meta.url }
|
|
29
|
+
};
|
|
30
|
+
/** stdout is the report and nothing else, so every log level goes to stderr. */
|
|
31
|
+
const logger = CliLogger.layer({ stderrFrom: "All" });
|
|
32
|
+
/**
|
|
20
33
|
* The `effected` program under `CliRuntime.main`, over `platform`: what
|
|
21
34
|
* {@link main} runs with the Node services, and what a test runs over a test
|
|
22
35
|
* environment.
|
|
23
36
|
*
|
|
24
37
|
* @public
|
|
25
38
|
*/
|
|
26
|
-
const run = (options, platform) => CliRuntime.main(CliAudience.run(
|
|
39
|
+
const run = (options, deps, platform) => CliRuntime.main(CliAudience.run(makeRootCommand(deps), { version: CLI_VERSION }), {
|
|
27
40
|
platform,
|
|
41
|
+
logger,
|
|
28
42
|
distribution: options.distribution ?? null,
|
|
29
|
-
exitCode:
|
|
43
|
+
exitCode: mainOptions.exitCode,
|
|
30
44
|
helpOnUsageError: "stderr",
|
|
31
45
|
env: {
|
|
32
|
-
|
|
33
|
-
formatter: versionFormatter
|
|
46
|
+
...mainOptions.env,
|
|
47
|
+
formatter: versionFormatter,
|
|
48
|
+
helpFlags: { inheritedFlags: "summary" }
|
|
34
49
|
}
|
|
35
50
|
});
|
|
36
51
|
/**
|
|
@@ -40,7 +55,7 @@ const run = (options, platform) => CliRuntime.main(CliAudience.run(rootCommand,
|
|
|
40
55
|
* @public
|
|
41
56
|
*/
|
|
42
57
|
const main = (options = {}) => {
|
|
43
|
-
NodeRuntime.runMain(run(options, NodeServices.layer));
|
|
58
|
+
NodeRuntime.runMain(run(options, { cwd: process.cwd() }, NodeServices.layer));
|
|
44
59
|
};
|
|
45
60
|
|
|
46
|
-
export { main, run };
|
|
61
|
+
export { main, mainOptions, run };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The effected command: the tool's command tree on the kit's front-end runtime",
|
|
6
6
|
"keywords": [
|
|
@@ -43,12 +43,15 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@effect/platform-node": "~4.1.2",
|
|
46
|
-
"@effected/cli-kit": "^0.
|
|
47
|
-
"@effected/
|
|
48
|
-
"@effected/
|
|
49
|
-
"@effected/
|
|
50
|
-
"@effected/
|
|
51
|
-
"
|
|
46
|
+
"@effected/cli-kit": "^0.27.0",
|
|
47
|
+
"@effected/engine": "0.13.0",
|
|
48
|
+
"@effected/env": "^0.11.0",
|
|
49
|
+
"@effected/glob": "^0.19.0",
|
|
50
|
+
"@effected/kit": "^0.13.0",
|
|
51
|
+
"@effected/walker": "^0.25.1",
|
|
52
|
+
"effect": "~4.1.2",
|
|
53
|
+
"ink": "^8.0.0",
|
|
54
|
+
"react": "^19.3.0"
|
|
52
55
|
},
|
|
53
56
|
"engines": {
|
|
54
57
|
"node": ">=24.11.0"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Doc } from "@effected/cli-kit";
|
|
2
|
+
|
|
3
|
+
// src/render/DiffDoc.ts
|
|
4
|
+
/**
|
|
5
|
+
* How many diff rows `DiffDoc.render` shows per schema before it cuts the rest with a "… N more lines" tail: a
|
|
6
|
+
* screenful of context that keeps a many-schema report readable.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_MAX_DIFF_LINES = 40;
|
|
11
|
+
const expandTabs = (text) => text.replaceAll(" ", " ");
|
|
12
|
+
/**
|
|
13
|
+
* A schema's line diff as a `Doc`, in git's shape: an `@@ path @@` header, context lines as they are, removes in the
|
|
14
|
+
* `error` token and adds in the `success` token.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Each tab in a line's text is expanded to two spaces, so a tab-indented document keeps its shape. Every line's text
|
|
18
|
+
* goes into the document as a `Doc.text` run, which is data: the renderers remove escape and control characters from it
|
|
19
|
+
* and never read it as markup, so a diff of untrusted text cannot repaint the terminal.
|
|
20
|
+
* A document with more than one hunk separates them with a muted `@@ -a,b +c,d @@` range row. The cap counts every
|
|
21
|
+
* row after the header (diff lines, range rows and `` markers) and the tail names exactly
|
|
22
|
+
* how many were cut. Nothing here loads Ink or React.
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
var DiffDoc = class {
|
|
27
|
+
constructor() {}
|
|
28
|
+
/**
|
|
29
|
+
* @param path - the schema file the diff is for, shown in the header
|
|
30
|
+
* @param diff - the diff
|
|
31
|
+
* @param options - `maxLines`, the cap on shown rows
|
|
32
|
+
*/
|
|
33
|
+
static render(path, diff, options) {
|
|
34
|
+
const cap = Math.max(0, Math.floor(options?.maxLines ?? 40));
|
|
35
|
+
const rows = [];
|
|
36
|
+
diff.hunks.forEach((hunk) => {
|
|
37
|
+
if (diff.hunks.length > 1) rows.push(Doc.text(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`, "muted"));
|
|
38
|
+
for (const line of hunk.lines) {
|
|
39
|
+
const text = expandTabs(line.text);
|
|
40
|
+
rows.push(line.kind === "add" ? Doc.text(`+${text}`, "success") : line.kind === "remove" ? Doc.text(`-${text}`, "error") : Doc.text(` ${text}`));
|
|
41
|
+
if (line.noNewline === true) rows.push(Doc.text("\", "muted"));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const hidden = rows.length - cap;
|
|
45
|
+
const shown = hidden > 0 ? rows.slice(0, cap) : rows;
|
|
46
|
+
if (hidden > 0) shown.push(Doc.text(`… ${hidden} more ${hidden === 1 ? "line" : "lines"}`, "muted"));
|
|
47
|
+
return Doc.lines([Doc.text(`@@ ${path} @@`, "accent"), ...shown], { wrap: false });
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export { DiffDoc };
|