@botlr/cli 0.3.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,20 @@
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
+
3
18
  ## 0.3.0
4
19
 
5
20
  ### Minor Changes
package/README.md CHANGED
@@ -168,9 +168,26 @@ replace the dashboard's schema of 'hero' with the code's? [y/N]
168
168
  ```
169
169
 
170
170
  For a FormBuilder schema (`{ nodes: [...] }`) the keys are the node names.
171
- When the dashboard's schema and the manifest's are in different formats the
172
- server refuses with 409; the CLI prints both formats and says to rerun with
173
- `--force`, which replaces the schema whatever its format. A 422 (the
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
174
191
  `schema` is not an object of field definitions, or the definition is not
175
192
  project-scoped and the template has to be installed into the project first)
176
193
  prints the server's message. After a successful push the CLI regenerates
package/dist/cli.js CHANGED
@@ -1096,7 +1096,11 @@ async function initSections(config, options = {}) {
1096
1096
  }
1097
1097
 
1098
1098
  // src/commands/schema.ts
1099
- import { BotlrError, BotlrSchemaFormatConflictError, botlrErrorMessage } from "@botlr/sdk";
1099
+ import {
1100
+ BotlrError,
1101
+ BotlrSchemaFormatConflictError,
1102
+ botlrErrorMessage
1103
+ } from "@botlr/sdk";
1100
1104
 
1101
1105
  // src/schema-diff.ts
1102
1106
  import { schemaFormat as schemaFormat2 } from "@botlr/sdk";
@@ -1112,15 +1116,20 @@ function diffSchemaKeys(current, next) {
1112
1116
  kept: [...nextKeys].filter((k) => currentKeys.has(k)).sort()
1113
1117
  };
1114
1118
  }
1115
- function previewSchemaPush(name, current, next, formats = {}) {
1116
- return {
1117
- name,
1118
- current,
1119
- currentFormat: formats.currentFormat ?? schemaFormat2(current),
1120
- next,
1121
- nextFormat: formats.nextFormat ?? schemaFormat2(next),
1122
- diff: diffSchemaKeys(current, next)
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
1123
1131
  };
1132
+ return { name, current, next, diff: diffSchemaKeys(current, next), ...formats, expected: expectedPushMode(formats) };
1124
1133
  }
1125
1134
  function plural(n, word) {
1126
1135
  return `${n} ${word}${n === 1 ? "" : "s"}`;
@@ -1129,14 +1138,21 @@ function formatName(format) {
1129
1138
  return format ?? "unknown";
1130
1139
  }
1131
1140
  function formatSchemaDiff(preview) {
1132
- const { diff } = preview;
1133
- const 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")})`;
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
+ }
1134
1146
  const lines = [header];
1135
1147
  if (diff.added.length) lines.push(` + added ${diff.added.join(", ")}`);
1136
- if (diff.removed.length) lines.push(` - removed ${diff.removed.join(", ")}`);
1148
+ if (diff.removed.length) {
1149
+ lines.push(expected === "merged" ? ` ~ dashboard only, kept ${diff.removed.join(", ")}` : ` - removed ${diff.removed.join(", ")}`);
1150
+ }
1137
1151
  lines.push(` = kept ${diff.kept.length ? diff.kept.join(", ") : "none"}`);
1138
- if (preview.currentFormat && preview.nextFormat && preview.currentFormat !== preview.nextFormat) {
1139
- lines.push(` the formats differ: the push needs --force to replace ${preview.currentFormat} with ${preview.nextFormat}`);
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
+ );
1140
1156
  }
1141
1157
  if (!diff.added.length && !diff.removed.length) lines.push(" the field keys match; only field settings may change");
1142
1158
  return lines.join("\n");
@@ -1174,16 +1190,18 @@ async function pullSchemas(config, options) {
1174
1190
  return results;
1175
1191
  }
1176
1192
  var SchemaFormatConflict = class extends Error {
1177
- constructor(sectionName, currentFormat, newFormat, message) {
1193
+ constructor(sectionName, currentFormat, newFormat, message, preview) {
1178
1194
  super(message ?? `the schema of '${sectionName}' is stored as ${currentFormat}, the manifest's is ${newFormat}`);
1179
1195
  this.sectionName = sectionName;
1180
1196
  this.currentFormat = currentFormat;
1181
1197
  this.newFormat = newFormat;
1198
+ this.preview = preview;
1182
1199
  this.name = "SchemaFormatConflict";
1183
1200
  }
1184
1201
  sectionName;
1185
1202
  currentFormat;
1186
1203
  newFormat;
1204
+ preview;
1187
1205
  };
1188
1206
  var SchemaRejected = class extends Error {
1189
1207
  constructor(sectionName, message) {
@@ -1210,13 +1228,17 @@ async function pushSchema(config, options) {
1210
1228
  if (!isNotFound(error)) throw error;
1211
1229
  throw new ManifestError(`the app has no section type '${name}'; push the code first with 'botlr push ${name} --create'`);
1212
1230
  }
1213
- const preview = previewSchemaPush(name, current.schema, next, { currentFormat: current.format });
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
+ }
1214
1236
  if (options.confirm && !await options.confirm(preview)) {
1215
1237
  return { name, preview, pushed: false };
1216
1238
  }
1217
1239
  let response;
1218
1240
  try {
1219
- response = await client.sections.putSchema(name, { schema: next, force: options.force ?? false });
1241
+ response = await client.sections.putSchema(name, { schema: next, force });
1220
1242
  } catch (error) {
1221
1243
  if (error instanceof BotlrSchemaFormatConflictError) {
1222
1244
  throw new SchemaFormatConflict(name, error.currentFormat, error.newFormat, botlrErrorMessage(error));
@@ -1234,6 +1256,9 @@ async function pushSchema(config, options) {
1234
1256
  changed: response.changed,
1235
1257
  previousFormat: response.previousFormat,
1236
1258
  format: response.definition.format,
1259
+ mode: response.mode,
1260
+ added: response.added,
1261
+ dashboardOnly: response.dashboardOnly,
1237
1262
  types
1238
1263
  };
1239
1264
  }
@@ -1280,6 +1305,7 @@ async function run(action) {
1280
1305
  pull first, or push --force to overwrite`);
1281
1306
  }
1282
1307
  if (error instanceof SchemaFormatConflict) {
1308
+ if (error.preview) console.log(formatSchemaDiff(error.preview));
1283
1309
  fail(
1284
1310
  `${error.message}
1285
1311
  dashboard: ${error.currentFormat}
@@ -1443,16 +1469,17 @@ async function confirmOnTerminal(preview) {
1443
1469
  if (!process.stdin.isTTY) {
1444
1470
  fail(`not a terminal: rerun with --yes to push the schema of '${preview.name}' without asking`);
1445
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] `;
1446
1473
  const rl = createInterface({ input: process.stdin, output: process.stderr });
1447
1474
  try {
1448
- const answer = await rl.question(`replace the dashboard's schema of '${preview.name}' with the code's? [y/N] `);
1475
+ const answer = await rl.question(question);
1449
1476
  return /^y(es)?$/i.test(answer.trim());
1450
1477
  } finally {
1451
1478
  rl.close();
1452
1479
  }
1453
1480
  }
1454
1481
  withCommon(
1455
- 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 even when the dashboard's is in the other format").option("-y, --yes", "push without asking").option("--no-types", "do not write <name>.props.ts afterwards")
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")
1456
1483
  ).action(
1457
1484
  (name, opts) => run(async () => {
1458
1485
  const config = await loadConfig(process.cwd(), overrides(opts));
@@ -1470,8 +1497,15 @@ withCommon(
1470
1497
  return;
1471
1498
  }
1472
1499
  const formats = result.previousFormat && result.format && result.previousFormat !== result.format ? ` (${result.previousFormat} -> ${result.format})` : result.format ? ` (${result.format})` : "";
1473
- if (result.changed) console.log(`pushed schema of ${result.name}${formats}`);
1474
- else console.log(`schema of ${result.name} already matches, nothing changed${formats}`);
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
+ }
1475
1509
  if (result.types?.changed) console.log(` types: wrote ${result.types.interfaceName} to ${path7.relative(process.cwd(), result.types.file)}`);
1476
1510
  })
1477
1511
  );