@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/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, SectionSchemaPushMode } 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,75 @@ 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
+ /** Whether `--force` was given. */
352
+ force: boolean;
353
+ /** What the server is expected to do with this push. */
354
+ expected: ExpectedPushMode;
355
+ }
356
+ /**
357
+ * - `replaced`: same format, or `--force`
358
+ * - `merged`: field-DSL fields at a FormBuilder schema; the server adds nodes and keeps the rest
359
+ * - `conflict`: a FormBuilder schema at a field-DSL one; the server answers 409 without `--force`
360
+ */
361
+ type ExpectedPushMode = "replaced" | "merged" | "conflict";
362
+ /** True when both formats are known and differ. */
363
+ declare function formatsDiffer(preview: Pick<SchemaPushPreview, "currentFormat" | "nextFormat">): boolean;
364
+ declare function expectedPushMode(preview: Pick<SchemaPushPreview, "currentFormat" | "nextFormat" | "force">): ExpectedPushMode;
365
+ /** The preview for a push, with formats detected from the schemas unless given. */
366
+ declare function previewSchemaPush(name: string, current: unknown, next: unknown, options?: {
367
+ currentFormat?: SectionSchemaFormat | null;
368
+ nextFormat?: SectionSchemaFormat | null;
369
+ force?: boolean;
370
+ }): SchemaPushPreview;
371
+ /**
372
+ * The diff as the CLI prints it: one header line with both formats and
373
+ * field counts (and, for a merge, what the server will do), then one line
374
+ * per non-empty direction. The `kept` line is always printed, with "none"
375
+ * when nothing survives. In a merge the dashboard's extra nodes are kept,
376
+ * so that line says so instead of "removed".
377
+ */
378
+ declare function formatSchemaDiff(preview: SchemaPushPreview): string;
379
+
322
380
  /** A client from the resolved config. `needPersonalToken` makes a missing `BOTLR_PAT` an error up front. */
323
381
  declare function clientFor(config: ResolvedConfig, options?: {
324
382
  needPersonalToken?: boolean;
@@ -411,4 +469,81 @@ interface InitResult {
411
469
  */
412
470
  declare function initSections(config: ResolvedConfig, options?: InitOptions): Promise<InitResult[]>;
413
471
 
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 };
472
+ /**
473
+ * `botlr schema pull` and `botlr schema push`: the explicit way to resolve
474
+ * drift between the manifest's `schema` (what the code expects) and the
475
+ * dashboard's definition schema. A source push never touches the schema;
476
+ * these two commands do, in the direction the user picks.
477
+ */
478
+
479
+ interface SchemaPullOptions {
480
+ /** Section names; empty with `all: true` means every manifest in the sections directory. */
481
+ names: string[];
482
+ all?: boolean;
483
+ /** Do not write `<name>.props.ts`; also off when the config says `types: false`. */
484
+ noTypes?: boolean;
485
+ }
486
+ interface SchemaPullResult {
487
+ name: string;
488
+ /** Whether `<name>.botlr.json` changed; false when its `schema` already equalled the dashboard's. */
489
+ changed: boolean;
490
+ /** The manifest file. */
491
+ file: string;
492
+ format?: string;
493
+ /** What happened to `<name>.props.ts`; absent when types are off or the pull failed. */
494
+ types?: PropsTypesUpdate;
495
+ /** Why nothing was written: no manifest, or no section type of that name in the app. */
496
+ error?: string;
497
+ }
498
+ /** Copy the dashboard's schema into each manifest's `schema`, then regenerate the prop types. */
499
+ declare function pullSchemas(config: ResolvedConfig, options: SchemaPullOptions): Promise<SchemaPullResult[]>;
500
+ interface SchemaPushOptions {
501
+ name: string;
502
+ /** Replace the schema outright whatever the formats, instead of merging into a FormBuilder schema. */
503
+ force?: boolean;
504
+ /**
505
+ * Asked once with the preview before anything is sent; return false to
506
+ * stop. Omit to push without asking (`--yes`).
507
+ */
508
+ confirm?: (preview: SchemaPushPreview) => Promise<boolean> | boolean;
509
+ /** Do not write `<name>.props.ts` afterwards; also off when the config says `types: false`. */
510
+ noTypes?: boolean;
511
+ }
512
+ interface SchemaPushResult {
513
+ name: string;
514
+ preview: SchemaPushPreview;
515
+ /** False when `confirm` said no; nothing was sent. */
516
+ pushed: boolean;
517
+ /** From the server: false when nothing had to change. */
518
+ changed?: boolean;
519
+ previousFormat?: string;
520
+ format?: string;
521
+ /** What the server did: replaced the schema, or merged the code's fields into a FormBuilder schema. */
522
+ mode?: SectionSchemaPushMode;
523
+ /** Field names that became new nodes (`merged`). */
524
+ added?: string[];
525
+ /** Node names only the dashboard defines, kept (`merged`). */
526
+ dashboardOnly?: string[];
527
+ types?: PropsTypesUpdate;
528
+ }
529
+ /**
530
+ * A FormBuilder schema pushed at a field-DSL definition without `--force`:
531
+ * raised before anything is sent when the formats are known (then `preview`
532
+ * is set), or from the server's 409.
533
+ */
534
+ declare class SchemaFormatConflict extends Error {
535
+ readonly sectionName: string;
536
+ readonly currentFormat: string;
537
+ readonly newFormat: string;
538
+ readonly preview?: SchemaPushPreview | undefined;
539
+ constructor(sectionName: string, currentFormat: string, newFormat: string, message?: string, preview?: SchemaPushPreview | undefined);
540
+ }
541
+ /** 422: the server refused the schema (not an object of fields, or the definition is not project-scoped). */
542
+ declare class SchemaRejected extends Error {
543
+ readonly sectionName: string;
544
+ constructor(sectionName: string, message: string);
545
+ }
546
+ /** Send the manifest's `schema` to the dashboard, after showing the diff and asking. */
547
+ declare function pushSchema(config: ResolvedConfig, options: SchemaPushOptions): Promise<SchemaPushResult>;
548
+
549
+ export { type BotlrCliConfigFile, CONFIG_FILE, ConfigError, type ConfigOverrides, DEFAULT_FRAMEWORK, DEFAULT_REGISTRY_FILE, DEFAULT_SECTIONS_DIR, type DefinitionMeta, type DeriveOptions, type DerivedManifest, type ExpectedPushMode, 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, expectedPushMode, findConfigFile, formatDriftWarning, formatManifest, formatSchemaDiff, formatsDiffer, 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,65 @@ 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 formatsDiffer(preview) {
852
+ return !!preview.currentFormat && !!preview.nextFormat && preview.currentFormat !== preview.nextFormat;
853
+ }
854
+ function expectedPushMode(preview) {
855
+ if (preview.force || !formatsDiffer(preview)) return "replaced";
856
+ return preview.currentFormat === "form-builder" && preview.nextFormat === "field-dsl" ? "merged" : "conflict";
857
+ }
858
+ function previewSchemaPush(name, current, next, options = {}) {
859
+ const formats = {
860
+ currentFormat: options.currentFormat ?? schemaFormat2(current),
861
+ nextFormat: options.nextFormat ?? schemaFormat2(next),
862
+ force: options.force ?? false
863
+ };
864
+ return { name, current, next, diff: diffSchemaKeys(current, next), ...formats, expected: expectedPushMode(formats) };
865
+ }
866
+ function plural(n, word) {
867
+ return `${n} ${word}${n === 1 ? "" : "s"}`;
868
+ }
869
+ function formatName(format) {
870
+ return format ?? "unknown";
871
+ }
872
+ function formatSchemaDiff(preview) {
873
+ const { diff, expected } = preview;
874
+ 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")})`;
875
+ if (expected === "merged") {
876
+ header += ": the server will add the missing fields as FormBuilder nodes and keep the dashboard's (format kept; --force would replace it)";
877
+ }
878
+ const lines = [header];
879
+ if (diff.added.length) lines.push(` + added ${diff.added.join(", ")}`);
880
+ if (diff.removed.length) {
881
+ lines.push(expected === "merged" ? ` ~ dashboard only, kept ${diff.removed.join(", ")}` : ` - removed ${diff.removed.join(", ")}`);
882
+ }
883
+ lines.push(` = kept ${diff.kept.length ? diff.kept.join(", ") : "none"}`);
884
+ if (formatsDiffer(preview) && expected !== "merged") {
885
+ lines.push(
886
+ 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}`
887
+ );
888
+ }
889
+ if (!diff.added.length && !diff.removed.length) lines.push(" the field keys match; only field settings may change");
832
890
  return lines.join("\n");
833
891
  }
834
892
 
@@ -1078,6 +1136,116 @@ async function initSections(config, options = {}) {
1078
1136
  }
1079
1137
  return results;
1080
1138
  }
1139
+
1140
+ // src/commands/schema.ts
1141
+ import {
1142
+ BotlrError,
1143
+ BotlrSchemaFormatConflictError,
1144
+ botlrErrorMessage
1145
+ } from "@botlr/sdk";
1146
+ function isNotFound(error) {
1147
+ return error instanceof BotlrError && error.status === 404;
1148
+ }
1149
+ async function pullSchemas(config, options) {
1150
+ const client = clientFor(config);
1151
+ const names = options.all ? await listManifests(config.sectionsDir) : options.names;
1152
+ const results = [];
1153
+ for (const name of names) {
1154
+ let manifest;
1155
+ try {
1156
+ manifest = await readManifest(config.sectionsDir, name);
1157
+ } catch (error) {
1158
+ results.push({ name, changed: false, file: "", error: error.message });
1159
+ continue;
1160
+ }
1161
+ let definition;
1162
+ try {
1163
+ definition = (await client.sections.getSchema(name)).definition;
1164
+ } catch (error) {
1165
+ if (!isNotFound(error)) throw error;
1166
+ results.push({ name, changed: false, file: "", error: `the app has no section type '${name}' (botlr push ${name} --create adds one)` });
1167
+ continue;
1168
+ }
1169
+ const updated = { ...manifest, schema: definition.schema };
1170
+ const written = await writeManifestIfChanged(config.sectionsDir, name, updated);
1171
+ const types = config.types && !options.noTypes ? await writePropsTypes(config.sectionsDir, manifest, definition.schema) : void 0;
1172
+ results.push({ name, changed: written.changed, file: written.file, format: definition.format, types });
1173
+ }
1174
+ return results;
1175
+ }
1176
+ var SchemaFormatConflict = class extends Error {
1177
+ constructor(sectionName, currentFormat, newFormat, message, preview) {
1178
+ super(message ?? `the schema of '${sectionName}' is stored as ${currentFormat}, the manifest's is ${newFormat}`);
1179
+ this.sectionName = sectionName;
1180
+ this.currentFormat = currentFormat;
1181
+ this.newFormat = newFormat;
1182
+ this.preview = preview;
1183
+ this.name = "SchemaFormatConflict";
1184
+ }
1185
+ sectionName;
1186
+ currentFormat;
1187
+ newFormat;
1188
+ preview;
1189
+ };
1190
+ var SchemaRejected = class extends Error {
1191
+ constructor(sectionName, message) {
1192
+ super(message);
1193
+ this.sectionName = sectionName;
1194
+ this.name = "SchemaRejected";
1195
+ }
1196
+ sectionName;
1197
+ };
1198
+ async function pushSchema(config, options) {
1199
+ const client = clientFor(config, { needPersonalToken: true });
1200
+ const { name } = options;
1201
+ const manifest = await readManifest(config.sectionsDir, name);
1202
+ const next = manifest.schema;
1203
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
1204
+ throw new ManifestError(
1205
+ `${name}.botlr.json has no 'schema' object to push; add one, or run 'botlr schema pull ${name}' to take the dashboard's`
1206
+ );
1207
+ }
1208
+ let current;
1209
+ try {
1210
+ current = (await client.sections.getSchema(name)).definition;
1211
+ } catch (error) {
1212
+ if (!isNotFound(error)) throw error;
1213
+ throw new ManifestError(`the app has no section type '${name}'; push the code first with 'botlr push ${name} --create'`);
1214
+ }
1215
+ const force = options.force ?? false;
1216
+ const preview = previewSchemaPush(name, current.schema, next, { currentFormat: current.format, force });
1217
+ if (preview.expected === "conflict") {
1218
+ throw new SchemaFormatConflict(name, preview.currentFormat, preview.nextFormat, void 0, preview);
1219
+ }
1220
+ if (options.confirm && !await options.confirm(preview)) {
1221
+ return { name, preview, pushed: false };
1222
+ }
1223
+ let response;
1224
+ try {
1225
+ response = await client.sections.putSchema(name, { schema: next, force });
1226
+ } catch (error) {
1227
+ if (error instanceof BotlrSchemaFormatConflictError) {
1228
+ throw new SchemaFormatConflict(name, error.currentFormat, error.newFormat, botlrErrorMessage(error));
1229
+ }
1230
+ if (error instanceof BotlrError && error.status === 422) {
1231
+ throw new SchemaRejected(name, botlrErrorMessage(error) ?? "the server rejected the schema (422)");
1232
+ }
1233
+ throw error;
1234
+ }
1235
+ const types = config.types && !options.noTypes ? await writePropsTypes(config.sectionsDir, manifest, response.definition.schema) : void 0;
1236
+ return {
1237
+ name,
1238
+ preview,
1239
+ pushed: true,
1240
+ changed: response.changed,
1241
+ previousFormat: response.previousFormat,
1242
+ format: response.definition.format,
1243
+ mode: response.mode,
1244
+ added: response.added,
1245
+ dashboardOnly: response.dashboardOnly,
1246
+ types
1247
+ };
1248
+ }
1081
1249
  export {
1082
1250
  CONFIG_FILE,
1083
1251
  ConfigError,
@@ -1091,6 +1259,8 @@ export {
1091
1259
  ManifestError,
1092
1260
  PROPS_SUFFIX,
1093
1261
  REGISTRY_FILE,
1262
+ SchemaFormatConflict,
1263
+ SchemaRejected,
1094
1264
  addRegistryEntry,
1095
1265
  clientFor,
1096
1266
  collectDslFields,
@@ -1099,9 +1269,13 @@ export {
1099
1269
  compareSchemas,
1100
1270
  computeStatus,
1101
1271
  deriveManifests,
1272
+ diffSchemaKeys,
1273
+ expectedPushMode,
1102
1274
  findConfigFile,
1103
1275
  formatDriftWarning,
1104
1276
  formatManifest,
1277
+ formatSchemaDiff,
1278
+ formatsDiffer,
1105
1279
  generatePropsSource,
1106
1280
  hasRegistry,
1107
1281
  importPathFor,
@@ -1121,9 +1295,12 @@ export {
1121
1295
  parseRegistry,
1122
1296
  parseZodSchema,
1123
1297
  pascalCase,
1298
+ previewSchemaPush,
1124
1299
  propsInterfaceName,
1125
1300
  propsPath,
1301
+ pullSchemas,
1126
1302
  pullSections,
1303
+ pushSchema,
1127
1304
  pushSection,
1128
1305
  readConfigFile,
1129
1306
  readLock,
@@ -1134,6 +1311,7 @@ export {
1134
1311
  remoteForLock,
1135
1312
  safeJoin,
1136
1313
  schemaFieldNames,
1314
+ schemaKeys,
1137
1315
  sectionStatuses,
1138
1316
  tsTypeForField,
1139
1317
  updateLock,