@botlr/cli 0.2.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @botlr/cli
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `botlr schema push` follows the server's merge behaviour for FormBuilder schemas instead of asking for `--force`.
8
+
9
+ - Field-DSL `schema` in the manifest pushed at a definition whose dashboard schema is in the FormBuilder format is merged by the server: a node per field the dashboard lacks, every existing node kept with its id, label and settings, nothing removed. The CLI's diff header says so (`the server will add the missing fields as FormBuilder nodes and keep the dashboard's (format kept; --force would replace it)`), lists the dashboard's extra nodes as "dashboard only, kept", asks as before, and then prints what the server did: `merged schema of <name> (form-builder): added ...; kept ...`, plus `the dashboard also defines: ... (not in the code)` when the dashboard has nodes the code does not declare. Same format: `replaced schema of <name>`. `changed: false`: `nothing to change`.
10
+ - A FormBuilder `schema` pushed at a field-DSL definition still needs `--force`: the CLI stops right after the diff with the hint, without asking or sending anything (the server would answer 409). `--force` replaces the schema outright whatever the formats, and the question says which format replaces which.
11
+ - `@botlr/sdk`: `SectionSchemaPutResponse` gains `mode: "replaced" | "merged"` (the `SectionSchemaPushMode` type), `added` and `dashboardOnly`.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies
16
+ - @botlr/sdk@0.6.0
17
+
18
+ ## 0.3.0
19
+
20
+ ### Minor Changes
21
+
22
+ - `botlr schema pull` and `botlr schema push`: the explicit way to resolve props schema drift in either direction. A code push still never changes the schema.
23
+
24
+ - `botlr schema pull <name...>` (or `--all` for every manifest in `sectionsDir`) fetches the section definition's props schema from the dashboard and writes it as `schema` into `<name>.botlr.json`, in the CLI's fixed key order and in whichever format the dashboard has (field DSL or FormBuilder). It says "unchanged" when the manifest already holds the same schema, and regenerates `<name>.props.ts` afterwards unless `--no-types` or `"types": false`.
25
+ - `botlr schema push <name> [--force] [--yes]` reads `schema` from the manifest (an error when it is missing), fetches the dashboard's current schema, prints both formats and a diff of the field keys (added, removed, kept; node names for FormBuilder schemas), asks for confirmation unless `--yes`, then replaces the dashboard's schema (needs `BOTLR_PAT`). When the two schemas are in different formats the server answers 409 and the CLI prints both formats and says to rerun with `--force`; a 422 (not an object of fields, or the definition is not project-scoped) prints the server's message. A successful push regenerates the props file.
26
+ - The drift warning of `push` and `sections status` now names both commands: `botlr schema pull <name>` to take the dashboard's schema, `botlr schema push <name>` to send the code's. A manifest `schema` in the FormBuilder format now takes part in the drift comparison too (by node names), since `schema pull` can write one.
27
+ - `@botlr/sdk`: `client.sections.getSchema(name)` (site token, `GET /api/sections/{name}/schema`) and `client.sections.putSchema(name, { schema, force })` (personal token, `PUT /api/sections/{name}/schema`), with the `SectionSchemaFormat`, `SectionSchemaGetResponse`, `SectionSchemaPutBody` and `SectionSchemaPutResponse` types, `schemaFormat(schema)`, `SectionDefinition.format`, `BotlrSchemaFormatConflictError` (the 409 with `currentFormat` and `newFormat`) and `botlrErrorMessage(error)` to read a server's `detail` message. `SectionManifest.schema` is typed as `SectionSchema` (either format).
28
+
29
+ ### Patch Changes
30
+
31
+ - Updated dependencies
32
+ - @botlr/sdk@0.5.0
33
+
3
34
  ## 0.2.0
4
35
 
5
36
  ### Minor Changes
package/README.md CHANGED
@@ -28,8 +28,9 @@ component registry `pull` keeps up to date and `sections init` reads.
28
28
  `types` (default true) makes `pull` write `<name>.props.ts` from the
29
29
  definition's props schema; `false` turns it off for the app.
30
30
 
31
- Environment: `BOTLR_API_KEY` (site token, for pull/list/status) and
32
- `BOTLR_PAT` (personal access token with `write:sections`, for push).
31
+ Environment: `BOTLR_API_KEY` (site token, for pull/list/status and `schema
32
+ pull`) and `BOTLR_PAT` (personal access token with `write:sections`, for
33
+ `push` and `schema push`).
33
34
  `BOTLR_BASE_URL`, `BOTLR_APP_ID`, `BOTLR_FRAMEWORK` and `BOTLR_SECTIONS_DIR`
34
35
  override the file; `--framework` and `--dir` override both.
35
36
 
@@ -41,6 +42,8 @@ botlr push <name> [-m msg] [--shared] [--create] [--force]
41
42
  botlr sections list
42
43
  botlr sections status [name...] [--offline]
43
44
  botlr sections init [--force] [--dry-run]
45
+ botlr schema pull <name...> | --all [--no-types]
46
+ botlr schema push <name> [--force] [-y, --yes] [--no-types]
44
47
  ```
45
48
 
46
49
  - `pull` writes every file of the section under `sectionsDir`, plus
@@ -76,6 +79,10 @@ botlr sections init [--force] [--dry-run]
76
79
  `.optional()`, `.nullable()` or `.default(...)`). Fields it cannot read are
77
80
  left out, and so is `schema` when nothing could be read. Review them, then
78
81
  `botlr push <name> --create` per section.
82
+ - `schema pull` copies the dashboard's props schema into the manifest's
83
+ `schema`; `schema push` sends the manifest's `schema` to the dashboard.
84
+ These are the only commands that change a definition's schema; see
85
+ "Schema drift" below.
79
86
 
80
87
  ## Prop types
81
88
 
@@ -117,22 +124,75 @@ export function Hero(props: HeroProps) { ... }
117
124
 
118
125
  ## Schema drift
119
126
 
120
- A manifest may carry a `schema` object in the field DSL (the same shape the
121
- dashboard stores): the props the code expects. The CLI never writes it to
122
- the server's definition except as the seed for `push --create`. On `push`
123
- and `sections status` it compares that object's keys with the definition's
124
- schema and warns:
127
+ A manifest may carry a `schema` object (normally the field DSL, the same
128
+ shape the dashboard stores): the props the code expects. A code `push`
129
+ never writes it to the server's definition except as the seed for
130
+ `push --create`. On `push` and `sections status` the CLI compares that
131
+ object's keys with the definition's schema and warns:
125
132
 
126
133
  ```
127
134
  warning: props schema of 'hero' differs between the code and the dashboard
128
135
  the code expects props the dashboard does not define: ctaLink
129
136
  the dashboard defines props the code does not list: image
130
- the dashboard owns the schema: edit it there, or update 'schema' in hero.botlr.json
137
+ the dashboard owns the schema; a code push never changes it. To resolve:
138
+ botlr schema pull hero takes the dashboard's schema into hero.botlr.json
139
+ botlr schema push hero sends the code's schema to the dashboard
131
140
  ```
132
141
 
133
- The warning changes nothing; fix the schema in the dashboard, pull to
134
- refresh `<name>.props.ts`, and update the manifest's `schema` (or drop it)
135
- when the code catches up.
142
+ The warning changes nothing. Resolve it in one of two directions:
143
+
144
+ ```bash
145
+ botlr schema pull hero # the dashboard is right: copy its schema into hero.botlr.json
146
+ botlr schema push hero # the code is right: replace the dashboard's schema with the manifest's
147
+ ```
148
+
149
+ `schema pull <name...>` (or `--all` for every manifest in `sectionsDir`)
150
+ fetches the definition's props schema and writes it as `schema` into
151
+ `<name>.botlr.json`, in the fixed key order and in whichever format the
152
+ dashboard has. It prints `schema unchanged` when the manifest already says
153
+ the same, and regenerates `<name>.props.ts` afterwards unless `--no-types`
154
+ (or `"types": false`). A section the app has no definition for is skipped
155
+ with a notice.
156
+
157
+ `schema push <name>` reads `schema` from the manifest (an error when it is
158
+ missing), fetches the dashboard's current schema and prints the formats and
159
+ a diff of the field keys, then asks before sending (`--yes` skips the
160
+ question, and is required when there is no terminal):
161
+
162
+ ```
163
+ schema of 'hero': dashboard has field-dsl (2 fields), code has field-dsl (3 fields)
164
+ + added ctaLink, subheading
165
+ - removed image
166
+ = kept heading
167
+ replace the dashboard's schema of 'hero' with the code's? [y/N]
168
+ ```
169
+
170
+ For a FormBuilder schema (`{ nodes: [...] }`) the keys are the node names.
171
+ What the server does depends on the two formats:
172
+
173
+ - Same format: the schema is replaced.
174
+ - Field DSL in the manifest, FormBuilder in the dashboard (the common case
175
+ for sections configured in the dashboard first): the server **merges**. It
176
+ adds a FormBuilder node per field the dashboard lacks, built like the
177
+ dashboard's own "Convert to FormBuilder", keeps every existing node with
178
+ its id, label and settings, only sets `required: true` where the code
179
+ requires it, and removes nothing. The diff header says so, the dashboard's
180
+ extra nodes are listed as "dashboard only, kept", and the result line
181
+ reads `merged schema of hero (form-builder): added ctaLink, subheading;
182
+ kept heading`, followed by `the dashboard also defines: image (not in the
183
+ code)` when the dashboard has nodes the code does not declare.
184
+ - FormBuilder in the manifest, field DSL in the dashboard: the CLI prints
185
+ the diff and both formats and stops with a hint to rerun with `--force`,
186
+ without asking or sending anything (the server would refuse with 409).
187
+
188
+ `--force` replaces the schema outright whatever the formats, and the
189
+ question says which format replaces which. When nothing had to change the
190
+ result line says `nothing to change`. A 422 (the
191
+ `schema` is not an object of field definitions, or the definition is not
192
+ project-scoped and the template has to be installed into the project first)
193
+ prints the server's message. After a successful push the CLI regenerates
194
+ `<name>.props.ts` from the stored schema. `schema push` needs `BOTLR_PAT`,
195
+ like `push`.
136
196
 
137
197
  ## Manifest format
138
198
 
package/dist/cli.js CHANGED
@@ -446,14 +446,14 @@ function parseZodSchema(source, schemaName) {
446
446
  }
447
447
  }
448
448
  if (end < 0) return null;
449
- const schema = {};
449
+ const schema2 = {};
450
450
  for (const entry of splitTopLevel(source.slice(open, end))) {
451
451
  const m = entry.match(/^(?:"([^"]+)"|'([^']+)'|([A-Za-z_$][A-Za-z0-9_$]*))\s*:\s*([\s\S]+)$/);
452
452
  if (!m) continue;
453
453
  const field = zodFieldToSchemaField(m[4]);
454
- if (field) schema[m[1] ?? m[2] ?? m[3]] = field;
454
+ if (field) schema2[m[1] ?? m[2] ?? m[3]] = field;
455
455
  }
456
- return Object.keys(schema).length ? schema : null;
456
+ return Object.keys(schema2).length ? schema2 : null;
457
457
  }
458
458
  function parseImports(source) {
459
459
  const internal = /* @__PURE__ */ new Set();
@@ -488,7 +488,7 @@ async function deriveManifests(sectionsDir, options) {
488
488
  if (!entryFile) continue;
489
489
  const source = await readFile4(entryFile, "utf8");
490
490
  const meta = parseDefinition(source);
491
- const schema = meta?.schemaName ? parseZodSchema(source, meta.schemaName) : null;
491
+ const schema2 = meta?.schemaName ? parseZodSchema(source, meta.schemaName) : null;
492
492
  const imports = parseImports(source);
493
493
  const relative = path4.relative(sectionsDir, entryFile).split(path4.sep).join("/");
494
494
  const npm = {};
@@ -510,7 +510,7 @@ async function deriveManifests(sectionsDir, options) {
510
510
  ...Object.keys(npm).length ? { npm } : {},
511
511
  ...imports.internal.length ? { internal: imports.internal } : {}
512
512
  },
513
- ...schema ? { schema } : {}
513
+ ...schema2 ? { schema: schema2 } : {}
514
514
  };
515
515
  out.push({ name: entry.slug, manifest, entryFile });
516
516
  }
@@ -734,9 +734,9 @@ function docFor(field) {
734
734
  }
735
735
  return doc;
736
736
  }
737
- function collectDslFields(schema) {
737
+ function collectDslFields(schema2) {
738
738
  const out = [];
739
- for (const [name, value] of Object.entries(schema)) {
739
+ for (const [name, value] of Object.entries(schema2)) {
740
740
  if (!value || typeof value !== "object" || Array.isArray(value)) continue;
741
741
  const field = value;
742
742
  out.push({ name, type: tsTypeForField(field), required: field.required === true, doc: docFor(field) });
@@ -759,13 +759,13 @@ function collectFormBuilderFields(nodes) {
759
759
  }
760
760
  return out;
761
761
  }
762
- function collectFields(schema) {
763
- if (isFormBuilderSchema(schema)) return collectFormBuilderFields(schema.nodes);
764
- if (isFieldSchema(schema)) return collectDslFields(schema);
762
+ function collectFields(schema2) {
763
+ if (isFormBuilderSchema(schema2)) return collectFormBuilderFields(schema2.nodes);
764
+ if (isFieldSchema(schema2)) return collectDslFields(schema2);
765
765
  return [];
766
766
  }
767
- function schemaFieldNames(schema) {
768
- return collectFields(schema).map((f) => f.name);
767
+ function schemaFieldNames(schema2) {
768
+ return collectFields(schema2).map((f) => f.name);
769
769
  }
770
770
  var IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
771
771
  function propertyKey(name) {
@@ -783,8 +783,8 @@ function propsInterfaceName(manifest) {
783
783
  const base = manifest.exportName && IDENTIFIER_RE.test(manifest.exportName) ? manifest.exportName : pascalCase(manifest.section);
784
784
  return `${base}Props`;
785
785
  }
786
- function generatePropsSource(schema, options) {
787
- const fields = collectFields(schema);
786
+ function generatePropsSource(schema2, options) {
787
+ const fields = collectFields(schema2);
788
788
  const lines = [
789
789
  `// Generated by @botlr/cli from the Botlr section definition "${options.section}".`,
790
790
  "// Do not edit: the props schema lives in the dashboard, and `botlr pull` rewrites this file.",
@@ -810,10 +810,10 @@ function generatePropsSource(schema, options) {
810
810
  function propsPath(sectionsDir, name) {
811
811
  return path6.join(sectionsDir, `${name}${PROPS_SUFFIX}`);
812
812
  }
813
- async function writePropsTypes(sectionsDir, manifest, schema) {
813
+ async function writePropsTypes(sectionsDir, manifest, schema2) {
814
814
  const interfaceName = propsInterfaceName(manifest);
815
815
  const file = propsPath(sectionsDir, manifest.section);
816
- const text = generatePropsSource(schema, { section: manifest.section, interfaceName });
816
+ const text = generatePropsSource(schema2, { section: manifest.section, interfaceName });
817
817
  try {
818
818
  if (await readFile6(file, "utf8") === text) return { file, interfaceName, changed: false };
819
819
  } catch {
@@ -900,15 +900,15 @@ import {
900
900
  } from "@botlr/sdk";
901
901
 
902
902
  // src/drift.ts
903
- import { isFieldSchema as isFieldSchema2 } from "@botlr/sdk";
903
+ import { schemaFormat } from "@botlr/sdk";
904
904
  function manifestSchema(manifest) {
905
- const schema = manifest.schema;
906
- return isFieldSchema2(schema) ? schema : null;
905
+ const schema2 = manifest.schema;
906
+ return schemaFormat(schema2) ? schema2 : null;
907
907
  }
908
908
  function compareSchemas(manifest, definitionSchema) {
909
909
  const code = manifestSchema(manifest);
910
910
  if (!code) return null;
911
- const codeKeys = new Set(Object.keys(code).filter((k) => code[k] && typeof code[k] === "object"));
911
+ const codeKeys = new Set(schemaFieldNames(code));
912
912
  const dashboardKeys = new Set(schemaFieldNames(definitionSchema));
913
913
  const missingInDashboard = [...codeKeys].filter((k) => !dashboardKeys.has(k)).sort();
914
914
  const missingInCode = [...dashboardKeys].filter((k) => !codeKeys.has(k)).sort();
@@ -923,7 +923,9 @@ function formatDriftWarning(name, drift) {
923
923
  if (drift.missingInCode.length) {
924
924
  lines.push(` the dashboard defines props the code does not list: ${drift.missingInCode.join(", ")}`);
925
925
  }
926
- lines.push(` the dashboard owns the schema: edit it there, or update 'schema' in ${name}.botlr.json`);
926
+ lines.push(` the dashboard owns the schema; a code push never changes it. To resolve:`);
927
+ lines.push(` botlr schema pull ${name} takes the dashboard's schema into ${name}.botlr.json`);
928
+ lines.push(` botlr schema push ${name} sends the code's schema to the dashboard`);
927
929
  return lines.join("\n");
928
930
  }
929
931
 
@@ -1093,6 +1095,174 @@ async function initSections(config, options = {}) {
1093
1095
  return results;
1094
1096
  }
1095
1097
 
1098
+ // src/commands/schema.ts
1099
+ import {
1100
+ BotlrError,
1101
+ BotlrSchemaFormatConflictError,
1102
+ botlrErrorMessage
1103
+ } from "@botlr/sdk";
1104
+
1105
+ // src/schema-diff.ts
1106
+ import { schemaFormat as schemaFormat2 } from "@botlr/sdk";
1107
+ function schemaKeys(schema2) {
1108
+ return [...new Set(schemaFieldNames(schema2))];
1109
+ }
1110
+ function diffSchemaKeys(current, next) {
1111
+ const currentKeys = new Set(schemaKeys(current));
1112
+ const nextKeys = new Set(schemaKeys(next));
1113
+ return {
1114
+ added: [...nextKeys].filter((k) => !currentKeys.has(k)).sort(),
1115
+ removed: [...currentKeys].filter((k) => !nextKeys.has(k)).sort(),
1116
+ kept: [...nextKeys].filter((k) => currentKeys.has(k)).sort()
1117
+ };
1118
+ }
1119
+ function formatsDiffer(preview) {
1120
+ return !!preview.currentFormat && !!preview.nextFormat && preview.currentFormat !== preview.nextFormat;
1121
+ }
1122
+ function expectedPushMode(preview) {
1123
+ if (preview.force || !formatsDiffer(preview)) return "replaced";
1124
+ return preview.currentFormat === "form-builder" && preview.nextFormat === "field-dsl" ? "merged" : "conflict";
1125
+ }
1126
+ function previewSchemaPush(name, current, next, options = {}) {
1127
+ const formats = {
1128
+ currentFormat: options.currentFormat ?? schemaFormat2(current),
1129
+ nextFormat: options.nextFormat ?? schemaFormat2(next),
1130
+ force: options.force ?? false
1131
+ };
1132
+ return { name, current, next, diff: diffSchemaKeys(current, next), ...formats, expected: expectedPushMode(formats) };
1133
+ }
1134
+ function plural(n, word) {
1135
+ return `${n} ${word}${n === 1 ? "" : "s"}`;
1136
+ }
1137
+ function formatName(format) {
1138
+ return format ?? "unknown";
1139
+ }
1140
+ function formatSchemaDiff(preview) {
1141
+ const { diff, expected } = preview;
1142
+ let header = `schema of '${preview.name}': dashboard has ${formatName(preview.currentFormat)} (${plural(schemaKeys(preview.current).length, "field")}), code has ${formatName(preview.nextFormat)} (${plural(schemaKeys(preview.next).length, "field")})`;
1143
+ if (expected === "merged") {
1144
+ header += ": the server will add the missing fields as FormBuilder nodes and keep the dashboard's (format kept; --force would replace it)";
1145
+ }
1146
+ const lines = [header];
1147
+ if (diff.added.length) lines.push(` + added ${diff.added.join(", ")}`);
1148
+ if (diff.removed.length) {
1149
+ lines.push(expected === "merged" ? ` ~ dashboard only, kept ${diff.removed.join(", ")}` : ` - removed ${diff.removed.join(", ")}`);
1150
+ }
1151
+ lines.push(` = kept ${diff.kept.length ? diff.kept.join(", ") : "none"}`);
1152
+ if (formatsDiffer(preview) && expected !== "merged") {
1153
+ lines.push(
1154
+ preview.force ? ` the formats differ: replacing ${preview.currentFormat} with ${preview.nextFormat} (--force)` : ` the formats differ: the push needs --force to replace ${preview.currentFormat} with ${preview.nextFormat}`
1155
+ );
1156
+ }
1157
+ if (!diff.added.length && !diff.removed.length) lines.push(" the field keys match; only field settings may change");
1158
+ return lines.join("\n");
1159
+ }
1160
+
1161
+ // src/commands/schema.ts
1162
+ function isNotFound(error) {
1163
+ return error instanceof BotlrError && error.status === 404;
1164
+ }
1165
+ async function pullSchemas(config, options) {
1166
+ const client = clientFor(config);
1167
+ const names = options.all ? await listManifests(config.sectionsDir) : options.names;
1168
+ const results = [];
1169
+ for (const name of names) {
1170
+ let manifest;
1171
+ try {
1172
+ manifest = await readManifest(config.sectionsDir, name);
1173
+ } catch (error) {
1174
+ results.push({ name, changed: false, file: "", error: error.message });
1175
+ continue;
1176
+ }
1177
+ let definition;
1178
+ try {
1179
+ definition = (await client.sections.getSchema(name)).definition;
1180
+ } catch (error) {
1181
+ if (!isNotFound(error)) throw error;
1182
+ results.push({ name, changed: false, file: "", error: `the app has no section type '${name}' (botlr push ${name} --create adds one)` });
1183
+ continue;
1184
+ }
1185
+ const updated = { ...manifest, schema: definition.schema };
1186
+ const written = await writeManifestIfChanged(config.sectionsDir, name, updated);
1187
+ const types = config.types && !options.noTypes ? await writePropsTypes(config.sectionsDir, manifest, definition.schema) : void 0;
1188
+ results.push({ name, changed: written.changed, file: written.file, format: definition.format, types });
1189
+ }
1190
+ return results;
1191
+ }
1192
+ var SchemaFormatConflict = class extends Error {
1193
+ constructor(sectionName, currentFormat, newFormat, message, preview) {
1194
+ super(message ?? `the schema of '${sectionName}' is stored as ${currentFormat}, the manifest's is ${newFormat}`);
1195
+ this.sectionName = sectionName;
1196
+ this.currentFormat = currentFormat;
1197
+ this.newFormat = newFormat;
1198
+ this.preview = preview;
1199
+ this.name = "SchemaFormatConflict";
1200
+ }
1201
+ sectionName;
1202
+ currentFormat;
1203
+ newFormat;
1204
+ preview;
1205
+ };
1206
+ var SchemaRejected = class extends Error {
1207
+ constructor(sectionName, message) {
1208
+ super(message);
1209
+ this.sectionName = sectionName;
1210
+ this.name = "SchemaRejected";
1211
+ }
1212
+ sectionName;
1213
+ };
1214
+ async function pushSchema(config, options) {
1215
+ const client = clientFor(config, { needPersonalToken: true });
1216
+ const { name } = options;
1217
+ const manifest = await readManifest(config.sectionsDir, name);
1218
+ const next = manifest.schema;
1219
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
1220
+ throw new ManifestError(
1221
+ `${name}.botlr.json has no 'schema' object to push; add one, or run 'botlr schema pull ${name}' to take the dashboard's`
1222
+ );
1223
+ }
1224
+ let current;
1225
+ try {
1226
+ current = (await client.sections.getSchema(name)).definition;
1227
+ } catch (error) {
1228
+ if (!isNotFound(error)) throw error;
1229
+ throw new ManifestError(`the app has no section type '${name}'; push the code first with 'botlr push ${name} --create'`);
1230
+ }
1231
+ const force = options.force ?? false;
1232
+ const preview = previewSchemaPush(name, current.schema, next, { currentFormat: current.format, force });
1233
+ if (preview.expected === "conflict") {
1234
+ throw new SchemaFormatConflict(name, preview.currentFormat, preview.nextFormat, void 0, preview);
1235
+ }
1236
+ if (options.confirm && !await options.confirm(preview)) {
1237
+ return { name, preview, pushed: false };
1238
+ }
1239
+ let response;
1240
+ try {
1241
+ response = await client.sections.putSchema(name, { schema: next, force });
1242
+ } catch (error) {
1243
+ if (error instanceof BotlrSchemaFormatConflictError) {
1244
+ throw new SchemaFormatConflict(name, error.currentFormat, error.newFormat, botlrErrorMessage(error));
1245
+ }
1246
+ if (error instanceof BotlrError && error.status === 422) {
1247
+ throw new SchemaRejected(name, botlrErrorMessage(error) ?? "the server rejected the schema (422)");
1248
+ }
1249
+ throw error;
1250
+ }
1251
+ const types = config.types && !options.noTypes ? await writePropsTypes(config.sectionsDir, manifest, response.definition.schema) : void 0;
1252
+ return {
1253
+ name,
1254
+ preview,
1255
+ pushed: true,
1256
+ changed: response.changed,
1257
+ previousFormat: response.previousFormat,
1258
+ format: response.definition.format,
1259
+ mode: response.mode,
1260
+ added: response.added,
1261
+ dashboardOnly: response.dashboardOnly,
1262
+ types
1263
+ };
1264
+ }
1265
+
1096
1266
  // src/output.ts
1097
1267
  function table(rows, header) {
1098
1268
  const all = header ? [header, ...rows] : rows;
@@ -1110,6 +1280,7 @@ function fail(message) {
1110
1280
  }
1111
1281
 
1112
1282
  // src/cli.ts
1283
+ import { createInterface } from "readline/promises";
1113
1284
  var require2 = createRequire(import.meta.url);
1114
1285
  var { version } = require2("../package.json");
1115
1286
  function parseVersion(value) {
@@ -1133,6 +1304,18 @@ async function run(action) {
1133
1304
  latest on server: ${latest}
1134
1305
  pull first, or push --force to overwrite`);
1135
1306
  }
1307
+ if (error instanceof SchemaFormatConflict) {
1308
+ if (error.preview) console.log(formatSchemaDiff(error.preview));
1309
+ fail(
1310
+ `${error.message}
1311
+ dashboard: ${error.currentFormat}
1312
+ code: ${error.newFormat}
1313
+ rerun with --force to replace the ${error.currentFormat} schema: botlr schema push ${error.sectionName} --force`
1314
+ );
1315
+ }
1316
+ if (error instanceof SchemaRejected) {
1317
+ fail(`the server rejected the schema of '${error.sectionName}': ${error.message}`);
1318
+ }
1136
1319
  fail(error.message);
1137
1320
  }
1138
1321
  }
@@ -1256,5 +1439,75 @@ withCommon(
1256
1439
  review the manifests, then push each section: botlr push <name> --create`);
1257
1440
  })
1258
1441
  );
1442
+ var schema = program.command("schema").description("copy a section's props schema between the dashboard and <name>.botlr.json");
1443
+ withCommon(
1444
+ schema.command("pull [names...]").description("write the dashboard's props schema into <name>.botlr.json as 'schema', then regenerate <name>.props.ts").option("--all", "every section with a manifest in the sections directory").option("--no-types", "do not write <name>.props.ts afterwards")
1445
+ ).action(
1446
+ (names, opts) => run(async () => {
1447
+ if (names.length === 0 && !opts.all) fail("name a section or pass --all");
1448
+ const config = await loadConfig(process.cwd(), overrides(opts));
1449
+ const results = await pullSchemas(config, { names, all: opts.all, noTypes: opts.types === false });
1450
+ if (results.length === 0) {
1451
+ console.log(`no *.botlr.json manifests in ${config.sectionsDir}`);
1452
+ return;
1453
+ }
1454
+ for (const r of results) {
1455
+ if (r.error) {
1456
+ console.log(`skip ${r.name}: ${r.error}`);
1457
+ process.exitCode = 1;
1458
+ continue;
1459
+ }
1460
+ const format = r.format ? ` (${r.format})` : "";
1461
+ if (r.changed) console.log(`pull ${r.name}: wrote schema${format} to ${path7.relative(process.cwd(), r.file)}`);
1462
+ else console.log(`pull ${r.name}: schema unchanged${format}`);
1463
+ if (r.types?.changed) console.log(` types: wrote ${r.types.interfaceName} to ${path7.relative(process.cwd(), r.types.file)}`);
1464
+ }
1465
+ })
1466
+ );
1467
+ async function confirmOnTerminal(preview) {
1468
+ console.log(formatSchemaDiff(preview));
1469
+ if (!process.stdin.isTTY) {
1470
+ fail(`not a terminal: rerun with --yes to push the schema of '${preview.name}' without asking`);
1471
+ }
1472
+ const question = preview.expected === "merged" ? `merge the code's fields into the dashboard's ${preview.currentFormat} schema of '${preview.name}'? [y/N] ` : preview.force && preview.currentFormat !== preview.nextFormat ? `replace the dashboard's ${preview.currentFormat} schema of '${preview.name}' with the code's ${preview.nextFormat} schema? [y/N] ` : `replace the dashboard's schema of '${preview.name}' with the code's? [y/N] `;
1473
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1474
+ try {
1475
+ const answer = await rl.question(question);
1476
+ return /^y(es)?$/i.test(answer.trim());
1477
+ } finally {
1478
+ rl.close();
1479
+ }
1480
+ }
1481
+ withCommon(
1482
+ schema.command("push <name>").description("send the manifest's 'schema' to the dashboard as the definition's props schema (needs BOTLR_PAT)").option("--force", "replace the schema outright, instead of merging the code's fields into a FormBuilder schema").option("-y, --yes", "push without asking").option("--no-types", "do not write <name>.props.ts afterwards")
1483
+ ).action(
1484
+ (name, opts) => run(async () => {
1485
+ const config = await loadConfig(process.cwd(), overrides(opts));
1486
+ const result = await pushSchema(config, {
1487
+ name,
1488
+ force: opts.force,
1489
+ noTypes: opts.types === false,
1490
+ confirm: opts.yes ? (preview) => {
1491
+ console.log(formatSchemaDiff(preview));
1492
+ return true;
1493
+ } : confirmOnTerminal
1494
+ });
1495
+ if (!result.pushed) {
1496
+ console.log(`nothing pushed`);
1497
+ return;
1498
+ }
1499
+ const formats = result.previousFormat && result.format && result.previousFormat !== result.format ? ` (${result.previousFormat} -> ${result.format})` : result.format ? ` (${result.format})` : "";
1500
+ if (!result.changed) console.log(`schema of ${result.name}: nothing to change${formats}`);
1501
+ else if (result.mode === "merged") {
1502
+ const added = result.added?.length ? `added ${result.added.join(", ")}` : "added nothing";
1503
+ const kept = result.preview.diff.kept.length ? `; kept ${result.preview.diff.kept.join(", ")}` : "";
1504
+ console.log(`merged schema of ${result.name}${formats}: ${added}${kept}`);
1505
+ } else console.log(`replaced schema of ${result.name}${formats}`);
1506
+ if (result.dashboardOnly?.length) {
1507
+ console.log(` the dashboard also defines: ${result.dashboardOnly.join(", ")} (not in the code)`);
1508
+ }
1509
+ if (result.types?.changed) console.log(` types: wrote ${result.types.interfaceName} to ${path7.relative(process.cwd(), result.types.file)}`);
1510
+ })
1511
+ );
1259
1512
  program.parseAsync(process.argv).catch((error) => fail(error.message));
1260
1513
  //# sourceMappingURL=cli.js.map