@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/CHANGELOG.md +16 -0
- package/README.md +54 -11
- package/dist/cli.js +240 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +109 -8
- package/dist/index.js +155 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @botlr/cli
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `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.
|
|
8
|
+
|
|
9
|
+
- `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`.
|
|
10
|
+
- `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.
|
|
11
|
+
- 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.
|
|
12
|
+
- `@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).
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- @botlr/sdk@0.5.0
|
|
18
|
+
|
|
3
19
|
## 0.2.0
|
|
4
20
|
|
|
5
21
|
### 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
|
|
32
|
-
`BOTLR_PAT` (personal access token with `write:sections`, for
|
|
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,58 @@ export function Hero(props: HeroProps) { ... }
|
|
|
117
124
|
|
|
118
125
|
## Schema drift
|
|
119
126
|
|
|
120
|
-
A manifest may carry a `schema` object
|
|
121
|
-
dashboard stores): the props the code expects.
|
|
122
|
-
the server's definition except as the seed for
|
|
123
|
-
and `sections status`
|
|
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
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
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
|
|
174
|
+
`schema` is not an object of field definitions, or the definition is not
|
|
175
|
+
project-scoped and the template has to be installed into the project first)
|
|
176
|
+
prints the server's message. After a successful push the CLI regenerates
|
|
177
|
+
`<name>.props.ts` from the stored schema. `schema push` needs `BOTLR_PAT`,
|
|
178
|
+
like `push`.
|
|
136
179
|
|
|
137
180
|
## Manifest format
|
|
138
181
|
|
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
|
|
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)
|
|
454
|
+
if (field) schema2[m[1] ?? m[2] ?? m[3]] = field;
|
|
455
455
|
}
|
|
456
|
-
return Object.keys(
|
|
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
|
|
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
|
-
...
|
|
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(
|
|
737
|
+
function collectDslFields(schema2) {
|
|
738
738
|
const out = [];
|
|
739
|
-
for (const [name, value] of Object.entries(
|
|
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(
|
|
763
|
-
if (isFormBuilderSchema(
|
|
764
|
-
if (isFieldSchema(
|
|
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(
|
|
768
|
-
return collectFields(
|
|
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(
|
|
787
|
-
const fields = collectFields(
|
|
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,
|
|
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(
|
|
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 {
|
|
903
|
+
import { schemaFormat } from "@botlr/sdk";
|
|
904
904
|
function manifestSchema(manifest) {
|
|
905
|
-
const
|
|
906
|
-
return
|
|
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(
|
|
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
|
|
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,149 @@ async function initSections(config, options = {}) {
|
|
|
1093
1095
|
return results;
|
|
1094
1096
|
}
|
|
1095
1097
|
|
|
1098
|
+
// src/commands/schema.ts
|
|
1099
|
+
import { BotlrError, BotlrSchemaFormatConflictError, botlrErrorMessage } from "@botlr/sdk";
|
|
1100
|
+
|
|
1101
|
+
// src/schema-diff.ts
|
|
1102
|
+
import { schemaFormat as schemaFormat2 } from "@botlr/sdk";
|
|
1103
|
+
function schemaKeys(schema2) {
|
|
1104
|
+
return [...new Set(schemaFieldNames(schema2))];
|
|
1105
|
+
}
|
|
1106
|
+
function diffSchemaKeys(current, next) {
|
|
1107
|
+
const currentKeys = new Set(schemaKeys(current));
|
|
1108
|
+
const nextKeys = new Set(schemaKeys(next));
|
|
1109
|
+
return {
|
|
1110
|
+
added: [...nextKeys].filter((k) => !currentKeys.has(k)).sort(),
|
|
1111
|
+
removed: [...currentKeys].filter((k) => !nextKeys.has(k)).sort(),
|
|
1112
|
+
kept: [...nextKeys].filter((k) => currentKeys.has(k)).sort()
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
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)
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function plural(n, word) {
|
|
1126
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1127
|
+
}
|
|
1128
|
+
function formatName(format) {
|
|
1129
|
+
return format ?? "unknown";
|
|
1130
|
+
}
|
|
1131
|
+
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")})`;
|
|
1134
|
+
const lines = [header];
|
|
1135
|
+
if (diff.added.length) lines.push(` + added ${diff.added.join(", ")}`);
|
|
1136
|
+
if (diff.removed.length) lines.push(` - removed ${diff.removed.join(", ")}`);
|
|
1137
|
+
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}`);
|
|
1140
|
+
}
|
|
1141
|
+
if (!diff.added.length && !diff.removed.length) lines.push(" the field keys match; only field settings may change");
|
|
1142
|
+
return lines.join("\n");
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/commands/schema.ts
|
|
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) {
|
|
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.name = "SchemaFormatConflict";
|
|
1183
|
+
}
|
|
1184
|
+
sectionName;
|
|
1185
|
+
currentFormat;
|
|
1186
|
+
newFormat;
|
|
1187
|
+
};
|
|
1188
|
+
var SchemaRejected = class extends Error {
|
|
1189
|
+
constructor(sectionName, message) {
|
|
1190
|
+
super(message);
|
|
1191
|
+
this.sectionName = sectionName;
|
|
1192
|
+
this.name = "SchemaRejected";
|
|
1193
|
+
}
|
|
1194
|
+
sectionName;
|
|
1195
|
+
};
|
|
1196
|
+
async function pushSchema(config, options) {
|
|
1197
|
+
const client = clientFor(config, { needPersonalToken: true });
|
|
1198
|
+
const { name } = options;
|
|
1199
|
+
const manifest = await readManifest(config.sectionsDir, name);
|
|
1200
|
+
const next = manifest.schema;
|
|
1201
|
+
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
1202
|
+
throw new ManifestError(
|
|
1203
|
+
`${name}.botlr.json has no 'schema' object to push; add one, or run 'botlr schema pull ${name}' to take the dashboard's`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
let current;
|
|
1207
|
+
try {
|
|
1208
|
+
current = (await client.sections.getSchema(name)).definition;
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
if (!isNotFound(error)) throw error;
|
|
1211
|
+
throw new ManifestError(`the app has no section type '${name}'; push the code first with 'botlr push ${name} --create'`);
|
|
1212
|
+
}
|
|
1213
|
+
const preview = previewSchemaPush(name, current.schema, next, { currentFormat: current.format });
|
|
1214
|
+
if (options.confirm && !await options.confirm(preview)) {
|
|
1215
|
+
return { name, preview, pushed: false };
|
|
1216
|
+
}
|
|
1217
|
+
let response;
|
|
1218
|
+
try {
|
|
1219
|
+
response = await client.sections.putSchema(name, { schema: next, force: options.force ?? false });
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
if (error instanceof BotlrSchemaFormatConflictError) {
|
|
1222
|
+
throw new SchemaFormatConflict(name, error.currentFormat, error.newFormat, botlrErrorMessage(error));
|
|
1223
|
+
}
|
|
1224
|
+
if (error instanceof BotlrError && error.status === 422) {
|
|
1225
|
+
throw new SchemaRejected(name, botlrErrorMessage(error) ?? "the server rejected the schema (422)");
|
|
1226
|
+
}
|
|
1227
|
+
throw error;
|
|
1228
|
+
}
|
|
1229
|
+
const types = config.types && !options.noTypes ? await writePropsTypes(config.sectionsDir, manifest, response.definition.schema) : void 0;
|
|
1230
|
+
return {
|
|
1231
|
+
name,
|
|
1232
|
+
preview,
|
|
1233
|
+
pushed: true,
|
|
1234
|
+
changed: response.changed,
|
|
1235
|
+
previousFormat: response.previousFormat,
|
|
1236
|
+
format: response.definition.format,
|
|
1237
|
+
types
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1096
1241
|
// src/output.ts
|
|
1097
1242
|
function table(rows, header) {
|
|
1098
1243
|
const all = header ? [header, ...rows] : rows;
|
|
@@ -1110,6 +1255,7 @@ function fail(message) {
|
|
|
1110
1255
|
}
|
|
1111
1256
|
|
|
1112
1257
|
// src/cli.ts
|
|
1258
|
+
import { createInterface } from "readline/promises";
|
|
1113
1259
|
var require2 = createRequire(import.meta.url);
|
|
1114
1260
|
var { version } = require2("../package.json");
|
|
1115
1261
|
function parseVersion(value) {
|
|
@@ -1133,6 +1279,17 @@ async function run(action) {
|
|
|
1133
1279
|
latest on server: ${latest}
|
|
1134
1280
|
pull first, or push --force to overwrite`);
|
|
1135
1281
|
}
|
|
1282
|
+
if (error instanceof SchemaFormatConflict) {
|
|
1283
|
+
fail(
|
|
1284
|
+
`${error.message}
|
|
1285
|
+
dashboard: ${error.currentFormat}
|
|
1286
|
+
code: ${error.newFormat}
|
|
1287
|
+
rerun with --force to replace the ${error.currentFormat} schema: botlr schema push ${error.sectionName} --force`
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
if (error instanceof SchemaRejected) {
|
|
1291
|
+
fail(`the server rejected the schema of '${error.sectionName}': ${error.message}`);
|
|
1292
|
+
}
|
|
1136
1293
|
fail(error.message);
|
|
1137
1294
|
}
|
|
1138
1295
|
}
|
|
@@ -1256,5 +1413,67 @@ withCommon(
|
|
|
1256
1413
|
review the manifests, then push each section: botlr push <name> --create`);
|
|
1257
1414
|
})
|
|
1258
1415
|
);
|
|
1416
|
+
var schema = program.command("schema").description("copy a section's props schema between the dashboard and <name>.botlr.json");
|
|
1417
|
+
withCommon(
|
|
1418
|
+
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")
|
|
1419
|
+
).action(
|
|
1420
|
+
(names, opts) => run(async () => {
|
|
1421
|
+
if (names.length === 0 && !opts.all) fail("name a section or pass --all");
|
|
1422
|
+
const config = await loadConfig(process.cwd(), overrides(opts));
|
|
1423
|
+
const results = await pullSchemas(config, { names, all: opts.all, noTypes: opts.types === false });
|
|
1424
|
+
if (results.length === 0) {
|
|
1425
|
+
console.log(`no *.botlr.json manifests in ${config.sectionsDir}`);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
for (const r of results) {
|
|
1429
|
+
if (r.error) {
|
|
1430
|
+
console.log(`skip ${r.name}: ${r.error}`);
|
|
1431
|
+
process.exitCode = 1;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
const format = r.format ? ` (${r.format})` : "";
|
|
1435
|
+
if (r.changed) console.log(`pull ${r.name}: wrote schema${format} to ${path7.relative(process.cwd(), r.file)}`);
|
|
1436
|
+
else console.log(`pull ${r.name}: schema unchanged${format}`);
|
|
1437
|
+
if (r.types?.changed) console.log(` types: wrote ${r.types.interfaceName} to ${path7.relative(process.cwd(), r.types.file)}`);
|
|
1438
|
+
}
|
|
1439
|
+
})
|
|
1440
|
+
);
|
|
1441
|
+
async function confirmOnTerminal(preview) {
|
|
1442
|
+
console.log(formatSchemaDiff(preview));
|
|
1443
|
+
if (!process.stdin.isTTY) {
|
|
1444
|
+
fail(`not a terminal: rerun with --yes to push the schema of '${preview.name}' without asking`);
|
|
1445
|
+
}
|
|
1446
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1447
|
+
try {
|
|
1448
|
+
const answer = await rl.question(`replace the dashboard's schema of '${preview.name}' with the code's? [y/N] `);
|
|
1449
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1450
|
+
} finally {
|
|
1451
|
+
rl.close();
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
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")
|
|
1456
|
+
).action(
|
|
1457
|
+
(name, opts) => run(async () => {
|
|
1458
|
+
const config = await loadConfig(process.cwd(), overrides(opts));
|
|
1459
|
+
const result = await pushSchema(config, {
|
|
1460
|
+
name,
|
|
1461
|
+
force: opts.force,
|
|
1462
|
+
noTypes: opts.types === false,
|
|
1463
|
+
confirm: opts.yes ? (preview) => {
|
|
1464
|
+
console.log(formatSchemaDiff(preview));
|
|
1465
|
+
return true;
|
|
1466
|
+
} : confirmOnTerminal
|
|
1467
|
+
});
|
|
1468
|
+
if (!result.pushed) {
|
|
1469
|
+
console.log(`nothing pushed`);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
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}`);
|
|
1475
|
+
if (result.types?.changed) console.log(` types: wrote ${result.types.interfaceName} to ${path7.relative(process.cwd(), result.types.file)}`);
|
|
1476
|
+
})
|
|
1477
|
+
);
|
|
1259
1478
|
program.parseAsync(process.argv).catch((error) => fail(error.message));
|
|
1260
1479
|
//# sourceMappingURL=cli.js.map
|