@effected/schemastore-cli 0.9.1 → 0.10.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/ConfigLoader.js CHANGED
@@ -41,6 +41,14 @@ const describeMalformedTarget = (schemas) => {
41
41
  if (!(record !== void 0 && Schema.isSchema(record.schema) && typeof record.$id === "string" && typeof record.path === "string" && typeof record.published === "boolean")) return `schemas[${index}] is not a SchemaTarget (missing schema/$id/path/published)`;
42
42
  }
43
43
  };
44
+ const describeMalformedCatalog = (catalog) => {
45
+ if (!Array.isArray(catalog)) return "catalog is not an array";
46
+ for (const [index, entry] of catalog.entries()) {
47
+ const record = typeof entry === "object" && entry !== null ? entry : void 0;
48
+ const config = record !== void 0 && typeof record.config === "object" && record.config !== null ? record.config : void 0;
49
+ if (config === void 0 || typeof config.path !== "string") return `catalog[${index}] is not a catalog entry (missing config.path)`;
50
+ }
51
+ };
44
52
  const describeDuplicatePath = (config) => {
45
53
  const seen = /* @__PURE__ */ new Set();
46
54
  const paths = [...config.schemas.map((target) => target.path), ...config.catalog.map((c) => c.config.path)];
@@ -141,6 +149,11 @@ var ConfigLoader = class ConfigLoader {
141
149
  path: configPath,
142
150
  reason: malformed
143
151
  }));
152
+ const malformedCatalog = describeMalformedCatalog(exported.catalog);
153
+ if (malformedCatalog !== void 0) return yield* Effect.fail(new ConfigLoadError({
154
+ path: configPath,
155
+ reason: malformedCatalog
156
+ }));
144
157
  const directory = path.dirname(configPath);
145
158
  const config = yield* ConfigLoader.resolvePaths(exported, directory);
146
159
  const duplicate = describeDuplicatePath(config);
package/README.md CHANGED
@@ -90,7 +90,7 @@ schemastore check [config] [--drift=strict|semantic|allow] [--on-drift=error|war
90
90
 
91
91
  - `build` generates every schema, runs the gates (structural lints and ajv strict mode), applies the drift policy, and writes what passes — content-compared, so unchanged files are untouched — along with each catalog entry.
92
92
  - `check` is the identical walk with no writes: it reports what `build` would do under the same flags and exits under the same conditions. It is the CI gate, so it also fails (exit `1`) whenever a build would write anything — a committed schema or catalog entry that differs from what the config generates, or is missing, is stale; run `schemastore build` and commit the result.
93
- - `--drift` and `--on-drift` override the config's `drift` block for one run; `--force` is sugar for `--drift=allow`.
93
+ - `--drift` and `--on-drift` override the config's `drift` block for one run; `--force` is sugar for `--drift=allow` and nothing else — combined with an explicit non-`allow` `--drift` it is a usage error (exit 64), not a precedence question; `--force --drift=allow` is accepted.
94
94
  - `--format=json` emits one JSON document on stdout (config path, per-schema outcome, per-catalog-entry outcome, effective drift policy and its source); human text moves to stderr.
95
95
  - When `GITHUB_STEP_SUMMARY` is set, both commands append a markdown summary table.
96
96
 
@@ -101,7 +101,7 @@ An unpublished schema is never drift: a contract change at a pinned but unpublis
101
101
  | code | meaning |
102
102
  | ---- | -------------------------------------------------------------------------- |
103
103
  | 0 | success, including drift under `onDrift: warn` |
104
- | 1 | drift under `onDrift: error`, a gate failure, or — for `check` — any document `build` would write |
104
+ | 1 | drift under `onDrift: error` (the error lists one line per drifting schema: `$id`, change, current and next version), a gate failure, or — for `check` — any document `build` would write |
105
105
  | 2 | config not found, failed to load, or failed `SchemastoreConfig` validation |
106
106
  | 3 | infrastructure failure |
107
107
  | 64 | usage error |
package/Report.js CHANGED
@@ -40,7 +40,15 @@ const tableRow = (columns) => `| ${columns.join(" | ")} |`;
40
40
  */
41
41
  var Report = class {
42
42
  constructor() {}
43
- /** stdout lines. */
43
+ /**
44
+ * stdout lines.
45
+ *
46
+ * @remarks
47
+ * The summary's `drift` count is the number of schemas whose VERDICT is
48
+ * `"drift"`, independent of `written`/`unchanged`: under
49
+ * `onDrift: "warn"` a drifting schema is written AND counted as drift,
50
+ * so the four counts need not sum to the schema total.
51
+ */
44
52
  static human(report) {
45
53
  const lines = [];
46
54
  for (const schema of report.schemas) lines.push(...schemaLines(schema, report));
package/Runner.js CHANGED
@@ -1,21 +1,12 @@
1
- import { Effect, FileSystem, Path, Schema } from "effect";
1
+ import { Effect, FileSystem, Option, Path, Schema } from "effect";
2
2
  import { CanonicalJson, CatalogEntry, DriftPolicy, SchemaPipeline, SchemaVersioning } from "@effected/schemastore";
3
3
 
4
4
  //#region src/Runner.ts
5
5
  const pipelineOptions = { contractChanges: "allow" };
6
6
  const catalogText = (target) => CanonicalJson.serialize(Schema.encodeSync(CatalogEntry)(target.entry));
7
- const jsonEqual = (a, b) => {
8
- if (a === b) return true;
9
- if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
10
- if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
11
- const left = a;
12
- const right = b;
13
- const keys = Object.keys(left);
14
- return keys.length === Object.keys(right).length && keys.every((k) => Object.hasOwn(right, k) && jsonEqual(left[k], right[k]));
15
- };
16
- const sameJson = (existing, text) => {
7
+ const parsesEqual = (existing, text) => {
17
8
  try {
18
- return jsonEqual(JSON.parse(existing), JSON.parse(text));
9
+ return CanonicalJson.equals(JSON.parse(existing), JSON.parse(text));
19
10
  } catch {
20
11
  return false;
21
12
  }
@@ -88,7 +79,8 @@ var Runner = class {
88
79
  for (const entry of config.catalog) {
89
80
  const { name, path: file } = entry.config;
90
81
  const text = yield* catalogText(entry);
91
- if ((yield* fs.exists(file)) && sameJson(yield* fs.readFileString(file), text)) catalog.push({
82
+ const existing = yield* fs.readFileString(file).pipe(Effect.map(Option.some), Effect.catchIf((error) => error.reason._tag === "NotFound", () => Effect.succeed(Option.none())));
83
+ if (Option.isSome(existing) && parsesEqual(existing.value, text)) catalog.push({
92
84
  name,
93
85
  path: file,
94
86
  outcome: "unchanged"
package/cli/execute.js CHANGED
@@ -7,15 +7,30 @@ import { Console, Effect, Option, Schema } from "effect";
7
7
  import { SchemaFile, SchemaValidator } from "@effected/schemastore";
8
8
 
9
9
  //#region src/cli/execute.ts
10
+ const DriftedSchema = Schema.Struct({
11
+ $id: Schema.String,
12
+ change: Schema.Literals([
13
+ "none",
14
+ "created",
15
+ "annotations",
16
+ "contract"
17
+ ]),
18
+ version: Schema.optionalKey(Schema.String),
19
+ nextVersion: Schema.optionalKey(Schema.String)
20
+ });
10
21
  /**
11
22
  * A published schema drifted under `onDrift: "error"`, so nothing was
12
23
  * written. Exit `1`.
13
24
  *
14
25
  * @public
15
26
  */
16
- var DriftError = class extends Schema.TaggedError()("DriftError", { count: Schema.Number }) {
27
+ var DriftError = class extends Schema.TaggedError()("DriftError", { drifted: Schema.Array(DriftedSchema) }) {
28
+ get count() {
29
+ return this.drifted.length;
30
+ }
17
31
  get message() {
18
- return `${this.count} published schema(s) drifted; nothing was written. Bump the drifting versions in the config, or re-run with --force to write anyway.`;
32
+ 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}` : ""}`);
33
+ 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.`;
19
34
  }
20
35
  };
21
36
  /**
@@ -40,6 +55,18 @@ var StaleError = class extends Schema.TaggedError()("StaleError", { count: Schem
40
55
  return `${this.count} document(s) are stale; run \`schemastore build\` and commit the result.`;
41
56
  }
42
57
  };
58
+ /**
59
+ * `--force` (shorthand for `--drift=allow`) was combined with an explicit
60
+ * `--drift` that is not `allow`. Contradictory, so refused as a usage
61
+ * error rather than silently resolving to `allow`. Exit `64`.
62
+ *
63
+ * @public
64
+ */
65
+ var ConflictingFlagsError = class extends Schema.TaggedError()("ConflictingFlagsError", { policy: Schema.String }) {
66
+ get message() {
67
+ return `--force conflicts with --drift=${this.policy}: --force means --drift=allow`;
68
+ }
69
+ };
43
70
  const effectiveDrift = (configured, input) => {
44
71
  return {
45
72
  policy: input.force ? "allow" : Option.getOrElse(input.drift, () => configured.policy),
@@ -58,17 +85,21 @@ const emit = Effect.fn("schemastore.emit")(function* (report, format) {
58
85
  * Run one `build` or `check`.
59
86
  *
60
87
  * @remarks
61
- * Loads the config, applies the flag overrides, runs the shared walk,
62
- * emits the report in the requested format, appends the step summary, and
63
- * fails typed `GateError`, then `DriftError`, then (for `check` only)
64
- * `StaleError`, each carrying exit `1` when the report says the run
65
- * refused to write or, under `check`, that a build would write. `SchemaFile` is built
66
- * here over the environment's `FileSystem`; the validator is
67
- * `deps.validator` or the real engine.
88
+ * Before anything is loaded, `--force` combined with an explicit `--drift`
89
+ * other than `allow` short-circuits with `ConflictingFlagsError` at exit
90
+ * `64`a usage error, not a run outcome. Otherwise loads the config,
91
+ * applies the flag overrides, runs the shared walk, emits the report in the
92
+ * requested format, appends the step summary, and fails typed
93
+ * `GateError`, then `DriftError`, then (for `check` only) `StaleError`,
94
+ * each carrying exit `1` when the report says the run refused to write
95
+ * or, under `check`, that a build would write. `SchemaFile` is built here
96
+ * over the environment's `FileSystem`; the validator is `deps.validator` or
97
+ * the real engine.
68
98
  *
69
99
  * @public
70
100
  */
71
101
  const execute = Effect.fn("schemastore.execute")(function* (mode, input, deps) {
102
+ if (input.force && Option.isSome(input.drift) && input.drift.value !== "allow") return yield* Effect.fail(CliRuntime.reported(new ConflictingFlagsError({ policy: input.drift.value }), 64));
72
103
  const loaded = yield* ConfigLoader.load({
73
104
  cwd: deps.cwd,
74
105
  ...Option.isSome(input.config) ? { explicit: input.config.value } : {},
@@ -88,8 +119,13 @@ const execute = Effect.fn("schemastore.execute")(function* (mode, input, deps) {
88
119
  return yield* Effect.fail(CliRuntime.reported(new GateError({ count }), 1));
89
120
  }
90
121
  if (report.drifted && drift.onDrift === "error") {
91
- const count = report.schemas.filter((schema) => schema.verdict === "drift").length;
92
- return yield* Effect.fail(CliRuntime.reported(new DriftError({ count }), 1));
122
+ const drifted = report.schemas.filter((schema) => schema.verdict === "drift").map((schema) => ({
123
+ $id: schema.$id,
124
+ change: schema.change,
125
+ ...schema.version !== void 0 ? { version: schema.version } : {},
126
+ ...schema.nextVersion !== void 0 ? { nextVersion: schema.nextVersion } : {}
127
+ }));
128
+ return yield* Effect.fail(CliRuntime.reported(new DriftError({ drifted }), 1));
93
129
  }
94
130
  if (mode === "check") {
95
131
  const count = report.schemas.filter((schema) => schema.outcome === "would-write").length + report.catalog.filter((entry) => entry.outcome === "would-write").length;
@@ -98,4 +134,4 @@ const execute = Effect.fn("schemastore.execute")(function* (mode, input, deps) {
98
134
  });
99
135
 
100
136
  //#endregion
101
- export { DriftError, GateError, StaleError, execute };
137
+ export { ConflictingFlagsError, DriftError, GateError, StaleError, execute };
package/cli/program.js CHANGED
@@ -28,7 +28,8 @@ const trimLoadError = Effect.fn("schemastore.trimLoadError")(function* (error) {
28
28
  * Fails with the marked error the runtime maps to the exit code:
29
29
  * `ShowHelp` is `64` with parse errors and `0` without (help itself was
30
30
  * already rendered); `ConfigNotFoundError` / `ConfigLoadError` are `2`;
31
- * `DriftError` / `GateError` / `StaleError` arrive already marked `1`.
31
+ * `ConflictingFlagsError` arrives already marked `64`; `DriftError` /
32
+ * `GateError` / `StaleError` arrive already marked `1`.
32
33
  *
33
34
  * @public
34
35
  */
package/main.js CHANGED
@@ -15,7 +15,7 @@ const render = (error) => CliError.isCliError(error) && error._tag === "ShowHelp
15
15
  const main = () => {
16
16
  const run = program(process.argv.slice(2), {
17
17
  cwd: process.cwd(),
18
- version: "0.9.1"
18
+ version: "0.10.0"
19
19
  }).pipe(Effect.provide(NodeServices.layer), CliRuntime.reportFailures({
20
20
  exitCode: 3,
21
21
  render
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/schemastore-cli",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "description": "The schemastore command: build and check SchemaStore-shaped JSON Schema documents from a schemastore.config.ts, with a per-schema published flag and a drift policy.",
6
6
  "keywords": [
@@ -36,11 +36,11 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@effect/platform-node": "4.0.0-rc.115",
39
- "@effected/cli": "^0.5.0",
39
+ "@effected/cli": "^0.5.1",
40
40
  "jiti": "^2.6.0"
41
41
  },
42
42
  "peerDependencies": {
43
- "@effected/schemastore": "0.9.1",
43
+ "@effected/schemastore": "0.10.0",
44
44
  "effect": "4.0.0-rc.115"
45
45
  },
46
46
  "engines": {