@botlr/cli 0.2.0 → 0.3.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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { SectionSourceScope, SectionManifest, SectionSourceFileInput, SectionSourceScopeSummary, SectionSourceSummary, SectionFieldSchema, SectionSchemaField, SectionFormBuilderNode, BotlrClient } from '@botlr/sdk';
1
+ import { SectionSourceScope, SectionManifest, SectionSourceFileInput, SectionSourceScopeSummary, SectionSourceSummary, SectionFieldSchema, SectionSchemaField, SectionFormBuilderNode, SectionSchema, SectionSchemaFormat, BotlrClient } from '@botlr/sdk';
2
2
 
3
3
  declare const CONFIG_FILE = "botlr.config.json";
4
4
  declare const DEFAULT_FRAMEWORK = "react-next";
@@ -308,17 +308,59 @@ interface SchemaDrift {
308
308
  /** Props the dashboard defines that the code does not list. */
309
309
  missingInCode: string[];
310
310
  }
311
- /** The manifest's `schema` when it is a field DSL object, else null. */
312
- declare function manifestSchema(manifest: SectionManifest): Record<string, unknown> | null;
311
+ /** The manifest's `schema` when it is a schema object in either format, else null. */
312
+ declare function manifestSchema(manifest: SectionManifest): SectionSchema | null;
313
313
  /**
314
- * Key difference between the code's schema (field DSL) and the definition's
315
- * schema (either format). Null when the manifest has no schema object or
316
- * the keys match.
314
+ * Key difference between the code's schema and the definition's schema
315
+ * (either format on either side; FormBuilder schemas contribute their node
316
+ * names). Null when the manifest has no schema object or the keys match.
317
317
  */
318
318
  declare function compareSchemas(manifest: SectionManifest, definitionSchema: unknown): SchemaDrift | null;
319
- /** The warning text for a drift, one line per direction, pointing at the dashboard. */
319
+ /** The warning text for a drift, one line per direction, naming the two commands that resolve it. */
320
320
  declare function formatDriftWarning(name: string, drift: SchemaDrift): string;
321
321
 
322
+ /**
323
+ * Field-key diff between two props schemas, for `botlr schema push` to show
324
+ * before it replaces the dashboard's schema with the code's. Field DSL keys
325
+ * are the object's keys; FormBuilder schemas contribute their node names
326
+ * (groups flattened), so the two formats can be compared with each other.
327
+ */
328
+
329
+ interface SchemaKeyDiff {
330
+ /** Keys the new schema has that the current one lacks. */
331
+ added: string[];
332
+ /** Keys the current schema has that the new one lacks. */
333
+ removed: string[];
334
+ /** Keys both have. */
335
+ kept: string[];
336
+ }
337
+ /** The field keys of a schema in either format, in schema order and without duplicates. */
338
+ declare function schemaKeys(schema: unknown): string[];
339
+ /** Keys added, removed and kept going from `current` to `next`; each list sorted. */
340
+ declare function diffSchemaKeys(current: unknown, next: unknown): SchemaKeyDiff;
341
+ interface SchemaPushPreview {
342
+ name: string;
343
+ /** The dashboard's current schema. */
344
+ current: unknown;
345
+ /** Its format as the server reports it, else as detected. */
346
+ currentFormat: SectionSchemaFormat | null;
347
+ /** The manifest's schema, about to replace it. */
348
+ next: unknown;
349
+ nextFormat: SectionSchemaFormat | null;
350
+ diff: SchemaKeyDiff;
351
+ }
352
+ /** The preview for a push, with formats detected from the schemas unless given. */
353
+ declare function previewSchemaPush(name: string, current: unknown, next: unknown, formats?: {
354
+ currentFormat?: SectionSchemaFormat | null;
355
+ nextFormat?: SectionSchemaFormat | null;
356
+ }): SchemaPushPreview;
357
+ /**
358
+ * The diff as the CLI prints it: one header line with both formats and
359
+ * field counts, then one line per non-empty direction. The `kept` line is
360
+ * always printed, with "none" when nothing survives.
361
+ */
362
+ declare function formatSchemaDiff(preview: SchemaPushPreview): string;
363
+
322
364
  /** A client from the resolved config. `needPersonalToken` makes a missing `BOTLR_PAT` an error up front. */
323
365
  declare function clientFor(config: ResolvedConfig, options?: {
324
366
  needPersonalToken?: boolean;
@@ -411,4 +453,63 @@ interface InitResult {
411
453
  */
412
454
  declare function initSections(config: ResolvedConfig, options?: InitOptions): Promise<InitResult[]>;
413
455
 
414
- export { type BotlrCliConfigFile, CONFIG_FILE, ConfigError, type ConfigOverrides, DEFAULT_FRAMEWORK, DEFAULT_REGISTRY_FILE, DEFAULT_SECTIONS_DIR, type DefinitionMeta, type DeriveOptions, type DerivedManifest, type GenerateOptions, type InitOptions, type InitResult, LOCK_DIR, LOCK_FILE, type LockEntry, MANIFEST_KEY_ORDER, MANIFEST_SUFFIX, ManifestError, PROPS_SUFFIX, type PropField, type PropsTypesUpdate, type PullOptions, type PullResult, type PushOptions, type PushResult, REGISTRY_FILE, type RegistryAddition, type RegistryEditResult, type RegistryEntry, type RegistryUpdate, type ResolvedConfig, type SchemaDrift, type SectionStatus, type SectionStatusRow, type SectionsLock, type StatusInput, addRegistryEntry, clientFor, collectDslFields, collectFields, collectFormBuilderFields, compareSchemas, computeStatus, deriveManifests, findConfigFile, formatDriftWarning, formatManifest, generatePropsSource, hasRegistry, importPathFor, initSections, listManifests, listSections, loadConfig, lockPath, manifestPath, manifestSchema, manifestsEqual, nextLockEntry, normalizeManifest, optionUnion, parseDefinition, parseImports, parseRegistry, parseZodSchema, pascalCase, propsInterfaceName, propsPath, pullSections, pushSection, readConfigFile, readLock, readManifest, readNpmVersions, readSectionFiles, registerSection, remoteForLock, safeJoin, schemaFieldNames, sectionStatuses, tsTypeForField, updateLock, validateManifest, writeLock, writeManifest, writeManifestIfChanged, writePropsTypes, writeSectionFiles, zodFieldToSchemaField };
456
+ interface SchemaPullOptions {
457
+ /** Section names; empty with `all: true` means every manifest in the sections directory. */
458
+ names: string[];
459
+ all?: boolean;
460
+ /** Do not write `<name>.props.ts`; also off when the config says `types: false`. */
461
+ noTypes?: boolean;
462
+ }
463
+ interface SchemaPullResult {
464
+ name: string;
465
+ /** Whether `<name>.botlr.json` changed; false when its `schema` already equalled the dashboard's. */
466
+ changed: boolean;
467
+ /** The manifest file. */
468
+ file: string;
469
+ format?: string;
470
+ /** What happened to `<name>.props.ts`; absent when types are off or the pull failed. */
471
+ types?: PropsTypesUpdate;
472
+ /** Why nothing was written: no manifest, or no section type of that name in the app. */
473
+ error?: string;
474
+ }
475
+ /** Copy the dashboard's schema into each manifest's `schema`, then regenerate the prop types. */
476
+ declare function pullSchemas(config: ResolvedConfig, options: SchemaPullOptions): Promise<SchemaPullResult[]>;
477
+ interface SchemaPushOptions {
478
+ name: string;
479
+ /** Replace the schema even when the dashboard's is in the other format. */
480
+ force?: boolean;
481
+ /**
482
+ * Asked once with the preview before anything is sent; return false to
483
+ * stop. Omit to push without asking (`--yes`).
484
+ */
485
+ confirm?: (preview: SchemaPushPreview) => Promise<boolean> | boolean;
486
+ /** Do not write `<name>.props.ts` afterwards; also off when the config says `types: false`. */
487
+ noTypes?: boolean;
488
+ }
489
+ interface SchemaPushResult {
490
+ name: string;
491
+ preview: SchemaPushPreview;
492
+ /** False when `confirm` said no; nothing was sent. */
493
+ pushed: boolean;
494
+ /** From the server: false when the stored schema already equalled the manifest's. */
495
+ changed?: boolean;
496
+ previousFormat?: string;
497
+ format?: string;
498
+ types?: PropsTypesUpdate;
499
+ }
500
+ /** 409: the dashboard's schema is in the other format and `--force` was not given. */
501
+ declare class SchemaFormatConflict extends Error {
502
+ readonly sectionName: string;
503
+ readonly currentFormat: string;
504
+ readonly newFormat: string;
505
+ constructor(sectionName: string, currentFormat: string, newFormat: string, message?: string);
506
+ }
507
+ /** 422: the server refused the schema (not an object of fields, or the definition is not project-scoped). */
508
+ declare class SchemaRejected extends Error {
509
+ readonly sectionName: string;
510
+ constructor(sectionName: string, message: string);
511
+ }
512
+ /** Send the manifest's `schema` to the dashboard, after showing the diff and asking. */
513
+ declare function pushSchema(config: ResolvedConfig, options: SchemaPushOptions): Promise<SchemaPushResult>;
514
+
515
+ export { type BotlrCliConfigFile, CONFIG_FILE, ConfigError, type ConfigOverrides, DEFAULT_FRAMEWORK, DEFAULT_REGISTRY_FILE, DEFAULT_SECTIONS_DIR, type DefinitionMeta, type DeriveOptions, type DerivedManifest, type GenerateOptions, type InitOptions, type InitResult, LOCK_DIR, LOCK_FILE, type LockEntry, MANIFEST_KEY_ORDER, MANIFEST_SUFFIX, ManifestError, PROPS_SUFFIX, type PropField, type PropsTypesUpdate, type PullOptions, type PullResult, type PushOptions, type PushResult, REGISTRY_FILE, type RegistryAddition, type RegistryEditResult, type RegistryEntry, type RegistryUpdate, type ResolvedConfig, type SchemaDrift, SchemaFormatConflict, type SchemaKeyDiff, type SchemaPullOptions, type SchemaPullResult, type SchemaPushOptions, type SchemaPushPreview, type SchemaPushResult, SchemaRejected, type SectionStatus, type SectionStatusRow, type SectionsLock, type StatusInput, addRegistryEntry, clientFor, collectDslFields, collectFields, collectFormBuilderFields, compareSchemas, computeStatus, deriveManifests, diffSchemaKeys, findConfigFile, formatDriftWarning, formatManifest, formatSchemaDiff, generatePropsSource, hasRegistry, importPathFor, initSections, listManifests, listSections, loadConfig, lockPath, manifestPath, manifestSchema, manifestsEqual, nextLockEntry, normalizeManifest, optionUnion, parseDefinition, parseImports, parseRegistry, parseZodSchema, pascalCase, previewSchemaPush, propsInterfaceName, propsPath, pullSchemas, pullSections, pushSchema, pushSection, readConfigFile, readLock, readManifest, readNpmVersions, readSectionFiles, registerSection, remoteForLock, safeJoin, schemaFieldNames, schemaKeys, sectionStatuses, tsTypeForField, updateLock, validateManifest, writeLock, writeManifest, writeManifestIfChanged, writePropsTypes, writeSectionFiles, zodFieldToSchemaField };
package/dist/index.js CHANGED
@@ -805,15 +805,15 @@ async function writePropsTypes(sectionsDir, manifest, schema) {
805
805
  }
806
806
 
807
807
  // src/drift.ts
808
- import { isFieldSchema as isFieldSchema2 } from "@botlr/sdk";
808
+ import { schemaFormat } from "@botlr/sdk";
809
809
  function manifestSchema(manifest) {
810
810
  const schema = manifest.schema;
811
- return isFieldSchema2(schema) ? schema : null;
811
+ return schemaFormat(schema) ? schema : null;
812
812
  }
813
813
  function compareSchemas(manifest, definitionSchema) {
814
814
  const code = manifestSchema(manifest);
815
815
  if (!code) return null;
816
- const codeKeys = new Set(Object.keys(code).filter((k) => code[k] && typeof code[k] === "object"));
816
+ const codeKeys = new Set(schemaFieldNames(code));
817
817
  const dashboardKeys = new Set(schemaFieldNames(definitionSchema));
818
818
  const missingInDashboard = [...codeKeys].filter((k) => !dashboardKeys.has(k)).sort();
819
819
  const missingInCode = [...dashboardKeys].filter((k) => !codeKeys.has(k)).sort();
@@ -828,7 +828,53 @@ function formatDriftWarning(name, drift) {
828
828
  if (drift.missingInCode.length) {
829
829
  lines.push(` the dashboard defines props the code does not list: ${drift.missingInCode.join(", ")}`);
830
830
  }
831
- lines.push(` the dashboard owns the schema: edit it there, or update 'schema' in ${name}.botlr.json`);
831
+ lines.push(` the dashboard owns the schema; a code push never changes it. To resolve:`);
832
+ lines.push(` botlr schema pull ${name} takes the dashboard's schema into ${name}.botlr.json`);
833
+ lines.push(` botlr schema push ${name} sends the code's schema to the dashboard`);
834
+ return lines.join("\n");
835
+ }
836
+
837
+ // src/schema-diff.ts
838
+ import { schemaFormat as schemaFormat2 } from "@botlr/sdk";
839
+ function schemaKeys(schema) {
840
+ return [...new Set(schemaFieldNames(schema))];
841
+ }
842
+ function diffSchemaKeys(current, next) {
843
+ const currentKeys = new Set(schemaKeys(current));
844
+ const nextKeys = new Set(schemaKeys(next));
845
+ return {
846
+ added: [...nextKeys].filter((k) => !currentKeys.has(k)).sort(),
847
+ removed: [...currentKeys].filter((k) => !nextKeys.has(k)).sort(),
848
+ kept: [...nextKeys].filter((k) => currentKeys.has(k)).sort()
849
+ };
850
+ }
851
+ function previewSchemaPush(name, current, next, formats = {}) {
852
+ return {
853
+ name,
854
+ current,
855
+ currentFormat: formats.currentFormat ?? schemaFormat2(current),
856
+ next,
857
+ nextFormat: formats.nextFormat ?? schemaFormat2(next),
858
+ diff: diffSchemaKeys(current, next)
859
+ };
860
+ }
861
+ function plural(n, word) {
862
+ return `${n} ${word}${n === 1 ? "" : "s"}`;
863
+ }
864
+ function formatName(format) {
865
+ return format ?? "unknown";
866
+ }
867
+ function formatSchemaDiff(preview) {
868
+ const { diff } = preview;
869
+ 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")})`;
870
+ const lines = [header];
871
+ if (diff.added.length) lines.push(` + added ${diff.added.join(", ")}`);
872
+ if (diff.removed.length) lines.push(` - removed ${diff.removed.join(", ")}`);
873
+ lines.push(` = kept ${diff.kept.length ? diff.kept.join(", ") : "none"}`);
874
+ if (preview.currentFormat && preview.nextFormat && preview.currentFormat !== preview.nextFormat) {
875
+ lines.push(` the formats differ: the push needs --force to replace ${preview.currentFormat} with ${preview.nextFormat}`);
876
+ }
877
+ if (!diff.added.length && !diff.removed.length) lines.push(" the field keys match; only field settings may change");
832
878
  return lines.join("\n");
833
879
  }
834
880
 
@@ -1078,6 +1124,103 @@ async function initSections(config, options = {}) {
1078
1124
  }
1079
1125
  return results;
1080
1126
  }
1127
+
1128
+ // src/commands/schema.ts
1129
+ import { BotlrError, BotlrSchemaFormatConflictError, botlrErrorMessage } from "@botlr/sdk";
1130
+ function isNotFound(error) {
1131
+ return error instanceof BotlrError && error.status === 404;
1132
+ }
1133
+ async function pullSchemas(config, options) {
1134
+ const client = clientFor(config);
1135
+ const names = options.all ? await listManifests(config.sectionsDir) : options.names;
1136
+ const results = [];
1137
+ for (const name of names) {
1138
+ let manifest;
1139
+ try {
1140
+ manifest = await readManifest(config.sectionsDir, name);
1141
+ } catch (error) {
1142
+ results.push({ name, changed: false, file: "", error: error.message });
1143
+ continue;
1144
+ }
1145
+ let definition;
1146
+ try {
1147
+ definition = (await client.sections.getSchema(name)).definition;
1148
+ } catch (error) {
1149
+ if (!isNotFound(error)) throw error;
1150
+ results.push({ name, changed: false, file: "", error: `the app has no section type '${name}' (botlr push ${name} --create adds one)` });
1151
+ continue;
1152
+ }
1153
+ const updated = { ...manifest, schema: definition.schema };
1154
+ const written = await writeManifestIfChanged(config.sectionsDir, name, updated);
1155
+ const types = config.types && !options.noTypes ? await writePropsTypes(config.sectionsDir, manifest, definition.schema) : void 0;
1156
+ results.push({ name, changed: written.changed, file: written.file, format: definition.format, types });
1157
+ }
1158
+ return results;
1159
+ }
1160
+ var SchemaFormatConflict = class extends Error {
1161
+ constructor(sectionName, currentFormat, newFormat, message) {
1162
+ super(message ?? `the schema of '${sectionName}' is stored as ${currentFormat}, the manifest's is ${newFormat}`);
1163
+ this.sectionName = sectionName;
1164
+ this.currentFormat = currentFormat;
1165
+ this.newFormat = newFormat;
1166
+ this.name = "SchemaFormatConflict";
1167
+ }
1168
+ sectionName;
1169
+ currentFormat;
1170
+ newFormat;
1171
+ };
1172
+ var SchemaRejected = class extends Error {
1173
+ constructor(sectionName, message) {
1174
+ super(message);
1175
+ this.sectionName = sectionName;
1176
+ this.name = "SchemaRejected";
1177
+ }
1178
+ sectionName;
1179
+ };
1180
+ async function pushSchema(config, options) {
1181
+ const client = clientFor(config, { needPersonalToken: true });
1182
+ const { name } = options;
1183
+ const manifest = await readManifest(config.sectionsDir, name);
1184
+ const next = manifest.schema;
1185
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
1186
+ throw new ManifestError(
1187
+ `${name}.botlr.json has no 'schema' object to push; add one, or run 'botlr schema pull ${name}' to take the dashboard's`
1188
+ );
1189
+ }
1190
+ let current;
1191
+ try {
1192
+ current = (await client.sections.getSchema(name)).definition;
1193
+ } catch (error) {
1194
+ if (!isNotFound(error)) throw error;
1195
+ throw new ManifestError(`the app has no section type '${name}'; push the code first with 'botlr push ${name} --create'`);
1196
+ }
1197
+ const preview = previewSchemaPush(name, current.schema, next, { currentFormat: current.format });
1198
+ if (options.confirm && !await options.confirm(preview)) {
1199
+ return { name, preview, pushed: false };
1200
+ }
1201
+ let response;
1202
+ try {
1203
+ response = await client.sections.putSchema(name, { schema: next, force: options.force ?? false });
1204
+ } catch (error) {
1205
+ if (error instanceof BotlrSchemaFormatConflictError) {
1206
+ throw new SchemaFormatConflict(name, error.currentFormat, error.newFormat, botlrErrorMessage(error));
1207
+ }
1208
+ if (error instanceof BotlrError && error.status === 422) {
1209
+ throw new SchemaRejected(name, botlrErrorMessage(error) ?? "the server rejected the schema (422)");
1210
+ }
1211
+ throw error;
1212
+ }
1213
+ const types = config.types && !options.noTypes ? await writePropsTypes(config.sectionsDir, manifest, response.definition.schema) : void 0;
1214
+ return {
1215
+ name,
1216
+ preview,
1217
+ pushed: true,
1218
+ changed: response.changed,
1219
+ previousFormat: response.previousFormat,
1220
+ format: response.definition.format,
1221
+ types
1222
+ };
1223
+ }
1081
1224
  export {
1082
1225
  CONFIG_FILE,
1083
1226
  ConfigError,
@@ -1091,6 +1234,8 @@ export {
1091
1234
  ManifestError,
1092
1235
  PROPS_SUFFIX,
1093
1236
  REGISTRY_FILE,
1237
+ SchemaFormatConflict,
1238
+ SchemaRejected,
1094
1239
  addRegistryEntry,
1095
1240
  clientFor,
1096
1241
  collectDslFields,
@@ -1099,9 +1244,11 @@ export {
1099
1244
  compareSchemas,
1100
1245
  computeStatus,
1101
1246
  deriveManifests,
1247
+ diffSchemaKeys,
1102
1248
  findConfigFile,
1103
1249
  formatDriftWarning,
1104
1250
  formatManifest,
1251
+ formatSchemaDiff,
1105
1252
  generatePropsSource,
1106
1253
  hasRegistry,
1107
1254
  importPathFor,
@@ -1121,9 +1268,12 @@ export {
1121
1268
  parseRegistry,
1122
1269
  parseZodSchema,
1123
1270
  pascalCase,
1271
+ previewSchemaPush,
1124
1272
  propsInterfaceName,
1125
1273
  propsPath,
1274
+ pullSchemas,
1126
1275
  pullSections,
1276
+ pushSchema,
1127
1277
  pushSection,
1128
1278
  readConfigFile,
1129
1279
  readLock,
@@ -1134,6 +1284,7 @@ export {
1134
1284
  remoteForLock,
1135
1285
  safeJoin,
1136
1286
  schemaFieldNames,
1287
+ schemaKeys,
1137
1288
  sectionStatuses,
1138
1289
  tsTypeForField,
1139
1290
  updateLock,