4bnode 3.5.2 → 4.0.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.
Files changed (43) hide show
  1. package/README.md +139 -114
  2. package/add-api.js +288 -156
  3. package/add-crud.js +226 -0
  4. package/add-login.js +152 -319
  5. package/add-mongo-delete.js +67 -138
  6. package/add-mongo-insert.js +87 -204
  7. package/add-mongo-read.js +100 -216
  8. package/add-mongo-schema.js +431 -199
  9. package/add-mongo-update.js +89 -202
  10. package/add-mongo.js +125 -218
  11. package/add-serialport.js +53 -98
  12. package/add-socket.js +43 -66
  13. package/add-websocket.js +68 -140
  14. package/index.js +288 -155
  15. package/lib/env.js +18 -0
  16. package/lib/indexFile.js +87 -0
  17. package/lib/names.js +21 -0
  18. package/lib/project.js +122 -0
  19. package/lib/routeFile.js +56 -0
  20. package/lib/ui.js +161 -0
  21. package/package.json +9 -16
  22. package/skeleton/dev-api.js +2785 -0
  23. package/skeleton/index.js +10 -3
  24. package/skeleton/package.json +9 -5
  25. package/skeleton/public/fonts/NotoSans-Bold.woff2 +0 -0
  26. package/skeleton/public/fonts/NotoSans-Light.woff2 +0 -0
  27. package/skeleton/public/fonts/NotoSans-Medium.woff2 +0 -0
  28. package/skeleton/public/fonts/NotoSans-Regular.woff2 +0 -0
  29. package/skeleton/public/fonts/NotoSans-SemiBold.woff2 +0 -0
  30. package/skeleton/public/images/mongo.png +0 -0
  31. package/skeleton/public/images/mysql.png +0 -0
  32. package/skeleton/public/images/postgresql.png +0 -0
  33. package/skeleton/public/images/sqlite.png +0 -0
  34. package/skeleton/public/index.html +7562 -64
  35. package/README_4bnode.md +0 -197
  36. package/src/auth/passport.js +0 -21
  37. package/src/db.js +0 -13
  38. package/test-server/.env.development +0 -1
  39. package/test-server/.env.production +0 -1
  40. package/test-server/index.js +0 -82
  41. package/test-server/package-lock.json +0 -1267
  42. package/test-server/package.json +0 -20
  43. package/test-server/public/index.html +0 -90
@@ -2,230 +2,462 @@
2
2
 
3
3
  import fs from "fs";
4
4
  import path from "path";
5
- import readline from "readline";
6
5
  import chalk from "chalk";
6
+ import { input, select, checkbox } from "@inquirer/prompts";
7
+ import { toCamelCase, toPascalCase, sanitizeName } from "./lib/names.js";
8
+ import { getProjectRoot, listModels, getModelSchemaDetailed } from "./lib/project.js";
9
+ import {
10
+ showHeader,
11
+ showTaskDone,
12
+ showWarning,
13
+ showError,
14
+ showFileAction,
15
+ showInfo,
16
+ } from "./lib/ui.js";
7
17
 
8
- const rl = readline.createInterface({
9
- input: process.stdin,
10
- output: process.stdout,
11
- });
18
+ // ── Presets ────────────────────────────────────────
12
19
 
13
- const fieldTypes = [
14
- "String",
15
- "Number",
16
- "Date",
17
- "Boolean",
18
- "Array",
19
- "Buffer",
20
- "Mixed",
21
- "ObjectId",
22
- ];
23
-
24
- function toCamelCase(str) {
25
- return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
26
- }
20
+ const presets = {
21
+ user: [
22
+ { name: "name", type: "String", required: true },
23
+ { name: "email", type: "String", required: true, unique: true },
24
+ { name: "password", type: "String", required: true },
25
+ { name: "role", type: "String", default: "user" },
26
+ ],
27
+ product: [
28
+ { name: "name", type: "String", required: true },
29
+ { name: "price", type: "Number", required: true },
30
+ { name: "description", type: "String" },
31
+ { name: "category", type: "String" },
32
+ { name: "inStock", type: "Boolean", default: true },
33
+ ],
34
+ post: [
35
+ { name: "title", type: "String", required: true },
36
+ { name: "content", type: "String", required: true },
37
+ { name: "author", type: "String" },
38
+ { name: "tags", type: "Array" },
39
+ { name: "published", type: "Boolean", default: false },
40
+ ],
41
+ };
27
42
 
28
- function toPascalCase(str) {
29
- const camelCaseStr = toCamelCase(str);
30
- return camelCaseStr.charAt(0).toUpperCase() + camelCaseStr.slice(1);
31
- }
43
+ const validTypes = ["String", "Number", "Boolean", "Date", "Array", "ObjectId", "Mixed", "Buffer"];
32
44
 
33
- function convertDefaultValue(fieldType, value) {
34
- try {
35
- switch (fieldType) {
36
- case "Number":
37
- if (isNaN(value)) throw new Error("Invalid number");
38
- return parseFloat(value);
39
- case "Date":
40
- const date = new Date(value);
41
- if (isNaN(date.getTime())) throw new Error("Invalid date");
42
- return date;
43
- case "Boolean":
44
- if (value.toLowerCase() !== "true" && value.toLowerCase() !== "false") {
45
- throw new Error("Invalid boolean");
46
- }
47
- return value.toLowerCase() === "true";
48
- case "Array":
49
- return JSON.parse(value);
50
- case "ObjectId":
51
- return value; // Keep as string; Mongoose will cast it
52
- case "Buffer":
53
- return Buffer.from(value);
54
- default:
55
- return value;
56
- }
57
- } catch (e) {
58
- return null;
59
- }
60
- }
45
+ // ── Format field for display ───────────────────────
61
46
 
62
- function promptSchemaDetails(schemaName) {
63
- const fields = [];
64
-
65
- function askField() {
66
- rl.question(
67
- chalk.yellow("\nEnter field name (or press enter to finish): "),
68
- (fieldName) => {
69
- if (!fieldName.trim()) {
70
- generateSchemaFile(schemaName, fields);
71
- rl.close();
72
- } else {
73
- console.log(chalk.cyan("Choose field type:"));
74
- fieldTypes.forEach((type, index) => {
75
- console.log(chalk.cyan(`${index + 1}. ${type}`));
76
- });
77
-
78
- rl.question(
79
- chalk.yellow("Enter the number for the field type: "),
80
- (fieldTypeIndex) => {
81
- const fieldType = fieldTypes[parseInt(fieldTypeIndex, 10) - 1];
82
- if (!fieldType) {
83
- console.error(
84
- chalk.red("Invalid selection. Please choose a valid number.")
85
- );
86
- askField();
87
- return;
88
- }
89
-
90
- rl.question(
91
- chalk.yellow("Is this field required? (y/n): "),
92
- (isRequired) => {
93
- const required = isRequired.trim().toLowerCase();
94
- if (required !== "y" && required !== "n") {
95
- console.error(
96
- chalk.red("Invalid selection. Please enter 'y' or 'n'.")
97
- );
98
- askField();
99
- return;
100
- }
101
-
102
- rl.question(
103
- chalk.yellow("Is this field unique? (y/n): "),
104
- (isUnique) => {
105
- const unique = isUnique.trim().toLowerCase();
106
- if (unique !== "y" && unique !== "n") {
107
- console.error(
108
- chalk.red(
109
- "Invalid selection. Please enter 'y' or 'n'."
110
- )
111
- );
112
- askField();
113
- return;
114
- }
115
-
116
- function askDefaultValue() {
117
- rl.question(
118
- chalk.yellow(
119
- `Enter default value${
120
- fieldType === "Boolean" ? " (true/false)" : ""
121
- }${
122
- fieldType === "Date"
123
- ? " (e.g., 2023-01-01T00:00:00Z)"
124
- : ""
125
- } (or press enter to skip): `
126
- ),
127
- (defaultValue) => {
128
- if (defaultValue.trim()) {
129
- const convertedDefaultValue = convertDefaultValue(
130
- fieldType,
131
- defaultValue.trim()
132
- );
133
- if (convertedDefaultValue === null) {
134
- console.error(
135
- chalk.red(
136
- "Invalid default value. Please enter a valid value."
137
- )
138
- );
139
- askDefaultValue();
140
- return;
141
- }
142
- fields.push({
143
- name: fieldName.trim(),
144
- type: fieldType,
145
- required: required === "y",
146
- unique: unique === "y",
147
- default: convertedDefaultValue,
148
- });
149
- } else {
150
- fields.push({
151
- name: fieldName.trim(),
152
- type: fieldType,
153
- required: required === "y",
154
- unique: unique === "y",
155
- });
156
- }
157
- askField();
158
- }
159
- );
160
- }
161
-
162
- askDefaultValue();
163
- }
164
- );
165
- }
166
- );
167
- }
168
- );
169
- }
170
- }
171
- );
172
- }
47
+ function formatField(f) {
48
+ const c = chalk.hex("#00c6ff");
49
+ const d = chalk.gray;
50
+ const g = chalk.hex("#22c55e");
51
+ const y = chalk.hex("#ffd200");
173
52
 
174
- askField();
53
+ let line = ` ${c(f.name)} ${d(":")} ${g(f.type)}`;
54
+ const tags = [];
55
+ if (f.required) tags.push(y("required"));
56
+ if (f.unique) tags.push(y("unique"));
57
+ if (f.default !== undefined) tags.push(d(`default=${f.default}`));
58
+ if (tags.length > 0) line += ` ${tags.join(" ")}`;
59
+ return line;
175
60
  }
176
61
 
62
+ // ── Generate schema file ───────────────────────────
63
+
177
64
  function generateSchemaFile(schemaName, fields) {
178
- const projectRoot = process.cwd();
65
+ const projectRoot = getProjectRoot();
179
66
  const modelsDir = path.join(projectRoot, "src", "models");
180
67
 
181
68
  if (!fs.existsSync(modelsDir)) {
182
69
  fs.mkdirSync(modelsDir, { recursive: true });
183
70
  }
184
71
 
185
- const schemaFilePath = path.join(modelsDir, `${toCamelCase(schemaName)}.js`);
186
- const pascalSchemaName = toPascalCase(schemaName);
187
- const schemaContent = `
188
- import mongoose from 'mongoose';
189
-
190
- const ${pascalSchemaName}Schema = new mongoose.Schema({
191
- ${fields
192
- .map((field) => {
193
- let fieldDef = `${field.name}: { type: ${
194
- field.type === "ObjectId"
195
- ? "mongoose.Schema.Types.ObjectId"
196
- : field.type
72
+ const camelName = toCamelCase(schemaName);
73
+ const pascalName = toPascalCase(schemaName);
74
+ const schemaFilePath = path.join(modelsDir, `${camelName}.js`);
75
+
76
+ const fieldDefs = fields
77
+ .map((f) => {
78
+ let def = ` ${f.name}: { type: ${
79
+ f.type === "ObjectId" ? "mongoose.Schema.Types.ObjectId" : f.type
197
80
  }`;
198
- if (field.required) {
199
- fieldDef += `, required: true`;
200
- }
201
- if (field.unique) {
202
- fieldDef += `, unique: true`;
203
- }
204
- if (field.default !== undefined) {
205
- fieldDef += `, default: ${JSON.stringify(field.default)}`;
206
- }
207
- fieldDef += ` }`;
208
- return fieldDef;
81
+ if (f.required) def += ", required: true";
82
+ if (f.unique) def += ", unique: true";
83
+ if (f.default !== undefined) def += `, default: ${JSON.stringify(f.default)}`;
84
+ def += " }";
85
+ return def;
209
86
  })
210
- .join(",\n ")}
87
+ .join(",\n");
88
+
89
+ const content = `import mongoose from 'mongoose';
90
+
91
+ const ${pascalName}Schema = new mongoose.Schema({
92
+ ${fieldDefs}
211
93
  }, { timestamps: true });
212
94
 
213
- const ${pascalSchemaName} = mongoose.model('${pascalSchemaName}', ${pascalSchemaName}Schema);
95
+ const ${pascalName} = mongoose.model('${pascalName}', ${pascalName}Schema);
96
+
97
+ export default ${pascalName};
98
+ `;
99
+
100
+ fs.writeFileSync(schemaFilePath, content, "utf8");
101
+ return schemaFilePath;
102
+ }
103
+
104
+ // ── View models ────────────────────────────────────
105
+
106
+ function viewModel(modelFile) {
107
+ const modelName = path.basename(modelFile, ".js");
108
+ const pascalName = toPascalCase(modelName);
109
+ const fields = getModelSchemaDetailed(modelName);
110
+
111
+ console.log();
112
+ console.log(chalk.hex("#ffd200").bold(` ${pascalName} Schema`));
113
+ console.log(chalk.gray(" ─────────────────────────────────"));
114
+
115
+ if (fields.length === 0) {
116
+ console.log(chalk.gray(" No fields found."));
117
+ } else {
118
+ fields.forEach((f) => console.log(formatField(f)));
119
+ }
120
+
121
+ console.log(chalk.gray(" ─────────────────────────────────"));
122
+ console.log(chalk.gray(` timestamps: true`));
123
+ console.log(chalk.gray(` file: src/models/${modelFile}`));
124
+ console.log();
125
+ }
126
+
127
+ // ── Edit model ─────────────────────────────────────
128
+
129
+ async function editModel(modelFile) {
130
+ const modelName = path.basename(modelFile, ".js");
131
+ const pascalName = toPascalCase(modelName);
132
+ const fields = getModelSchemaDetailed(modelName);
133
+
134
+ viewModel(modelFile);
135
+
136
+ let editing = true;
137
+ while (editing) {
138
+ const action = await select({
139
+ message: `Edit ${pascalName}:`,
140
+ choices: [
141
+ { name: "Add field", value: "add" },
142
+ ...(fields.length > 0
143
+ ? [
144
+ { name: "Remove field", value: "remove" },
145
+ { name: "Toggle required", value: "required" },
146
+ { name: "Toggle unique", value: "unique" },
147
+ ]
148
+ : []),
149
+ { name: "Done — save & exit", value: "done" },
150
+ ],
151
+ });
152
+
153
+ if (action === "add") {
154
+ const typeChoices = validTypes.map((t) => ({ name: t, value: t }));
155
+ showInfo("Add fields one by one. Press enter with empty name to stop.");
156
+
157
+ const added = [];
158
+ while (true) {
159
+ const fieldName = await input({ message: "Field name (enter to stop):" });
160
+ if (!fieldName.trim()) break;
161
+
162
+ if (fields.find((f) => f.name === fieldName.trim())) {
163
+ showWarning(`Field "${fieldName.trim()}" already exists, skipping.`);
164
+ continue;
165
+ }
166
+
167
+ const fieldType = await select({
168
+ message: `Type for "${fieldName.trim()}":`,
169
+ choices: typeChoices,
170
+ });
171
+
172
+ const isRequired = await select({
173
+ message: `Required?`,
174
+ choices: [
175
+ { name: "No", value: false },
176
+ { name: "Yes", value: true },
177
+ ],
178
+ });
179
+
180
+ const isUnique = await select({
181
+ message: `Unique?`,
182
+ choices: [
183
+ { name: "No", value: false },
184
+ { name: "Yes", value: true },
185
+ ],
186
+ });
187
+
188
+ const nf = { name: fieldName.trim(), type: fieldType, required: isRequired, unique: isUnique };
189
+ fields.push(nf);
190
+ added.push(nf);
191
+ console.log(chalk.hex("#22c55e")(` + ${nf.name}: ${nf.type}${nf.required ? " (required)" : ""}${nf.unique ? " (unique)" : ""}`));
192
+ }
193
+
194
+ } else if (action === "remove") {
195
+ const toRemove = await checkbox({
196
+ message: "Select fields to remove:",
197
+ choices: fields.map((f) => ({ name: `${f.name}: ${f.type}`, value: f.name })),
198
+ validate: (v) => (v.length > 0 ? true : "Select at least one."),
199
+ });
200
+ for (const name of toRemove) {
201
+ const idx = fields.findIndex((f) => f.name === name);
202
+ if (idx !== -1) {
203
+ fields.splice(idx, 1);
204
+ console.log(chalk.hex("#ef4444")(` - ${name} removed`));
205
+ }
206
+ }
207
+
208
+ } else if (action === "required") {
209
+ const toggled = await checkbox({
210
+ message: "Toggle required (checked = required):",
211
+ choices: fields.map((f) => ({
212
+ name: f.name,
213
+ value: f.name,
214
+ checked: f.required || false,
215
+ })),
216
+ });
217
+ fields.forEach((f) => {
218
+ f.required = toggled.includes(f.name);
219
+ });
220
+ showInfo("Required fields updated.");
221
+
222
+ } else if (action === "unique") {
223
+ const toggled = await checkbox({
224
+ message: "Toggle unique (checked = unique):",
225
+ choices: fields.map((f) => ({
226
+ name: f.name,
227
+ value: f.name,
228
+ checked: f.unique || false,
229
+ })),
230
+ });
231
+ fields.forEach((f) => {
232
+ f.unique = toggled.includes(f.name);
233
+ });
234
+ showInfo("Unique fields updated.");
235
+
236
+ } else {
237
+ editing = false;
238
+ }
239
+ }
240
+
241
+ if (fields.length === 0) {
242
+ showWarning("No fields left. Schema not saved.");
243
+ return;
244
+ }
214
245
 
215
- export default ${pascalSchemaName};
216
- `;
246
+ console.log();
247
+ generateSchemaFile(modelName, fields);
248
+ showFileAction("updated", `src/models/${modelFile}`);
217
249
 
218
- fs.writeFileSync(schemaFilePath, schemaContent.trim(), "utf8");
219
- console.log(
220
- chalk.green(`MongoDB schema ${schemaName} created successfully.`)
250
+ const fieldList = fields.map(
251
+ (f) => `${f.name}: ${f.type}${f.required ? " (required)" : ""}${f.unique ? " (unique)" : ""}`
221
252
  );
253
+ showTaskDone(`Schema "${modelName}" updated`, fieldList);
222
254
  }
223
255
 
224
- if (process.argv.length !== 3) {
225
- console.error(chalk.red("Usage: add-mongo-schema <schemaName>"));
226
- process.exit(0);
256
+ // ── Create new schema ──────────────────────────────
257
+
258
+ async function createSchema(schemaName) {
259
+ const presetKey = schemaName.toLowerCase();
260
+ const hasPreset = presets[presetKey];
261
+
262
+ let fields = [];
263
+
264
+ // Offer preset if available
265
+ if (hasPreset) {
266
+ const usePreset = await select({
267
+ message: `Use "${presetKey}" preset (${hasPreset.length} fields)?`,
268
+ choices: [
269
+ { name: `Yes — ${hasPreset.map((f) => f.name).join(", ")}`, value: true },
270
+ { name: "No — define fields manually", value: false },
271
+ ],
272
+ });
273
+
274
+ if (usePreset) {
275
+ fields = hasPreset.map((f) => ({ ...f }));
276
+ fields.forEach((f) => console.log(chalk.hex("#22c55e")(
277
+ ` + ${f.name}: ${f.type}${f.required ? " (required)" : ""}${f.unique ? " (unique)" : ""}`
278
+ )));
279
+ }
280
+ }
281
+
282
+ // Manual field entry — fully selection-based
283
+ if (fields.length === 0) {
284
+ showInfo("Add fields one by one. Press enter with empty name to finish.");
285
+ const typeChoices = validTypes.map((t) => ({ name: t, value: t }));
286
+
287
+ while (true) {
288
+ const fieldName = await input({ message: "Field name (enter to finish):" });
289
+ if (!fieldName.trim()) break;
290
+
291
+ if (fields.find((f) => f.name === fieldName.trim())) {
292
+ showWarning(`Field "${fieldName.trim()}" already exists, skipping.`);
293
+ continue;
294
+ }
295
+
296
+ const fieldType = await select({
297
+ message: `Type for "${fieldName.trim()}":`,
298
+ choices: typeChoices,
299
+ });
300
+
301
+ const isRequired = await select({
302
+ message: "Required?",
303
+ choices: [
304
+ { name: "No", value: false },
305
+ { name: "Yes", value: true },
306
+ ],
307
+ });
308
+
309
+ const isUnique = await select({
310
+ message: "Unique?",
311
+ choices: [
312
+ { name: "No", value: false },
313
+ { name: "Yes", value: true },
314
+ ],
315
+ });
316
+
317
+ const nf = { name: fieldName.trim(), type: fieldType, required: isRequired, unique: isUnique };
318
+ fields.push(nf);
319
+ console.log(chalk.hex("#22c55e")(
320
+ ` + ${nf.name}: ${nf.type}${nf.required ? " (required)" : ""}${nf.unique ? " (unique)" : ""}`
321
+ ));
322
+ }
323
+ }
324
+
325
+ if (fields.length === 0) {
326
+ showWarning("No fields defined. Schema not created.");
327
+ process.exit(0);
328
+ }
329
+
330
+ console.log();
331
+ generateSchemaFile(schemaName, fields);
332
+ showFileAction("created", `src/models/${toCamelCase(schemaName)}.js`);
333
+
334
+ const fieldList = fields.map(
335
+ (f) => `${f.name}: ${f.type}${f.required ? " (required)" : ""}${f.unique ? " (unique)" : ""}`
336
+ );
337
+ showTaskDone(`Schema "${schemaName}" created`, fieldList);
227
338
  }
228
339
 
229
- const schemaName = process.argv[2];
340
+ // ── Main ───────────────────────────────────────────
341
+
342
+ async function main() {
343
+ const rawArgs = process.argv.slice(2);
344
+ const isEditMode = rawArgs.includes("--edit");
345
+ const positionalArgs = rawArgs.filter((a) => !a.startsWith("--"));
346
+ const existingModels = listModels();
230
347
 
231
- promptSchemaDetails(schemaName);
348
+ // ── Direct edit mode (from 4bnode edit) ──────────
349
+ if (isEditMode) {
350
+ if (existingModels.length === 0) {
351
+ showError("No models found. Create one first with: 4bnode schema");
352
+ process.exit(1);
353
+ }
354
+
355
+ showHeader("edit", "Edit a schema");
356
+
357
+ let modelFile;
358
+ if (positionalArgs[0]) {
359
+ // Try to match the given name to an existing model
360
+ const target = positionalArgs[0].toLowerCase();
361
+ modelFile = existingModels.find(
362
+ (f) => path.basename(f, ".js").toLowerCase() === target
363
+ );
364
+ if (!modelFile) {
365
+ showError(`Model "${positionalArgs[0]}" not found.`);
366
+ console.log();
367
+ showInfo(`Available models: ${existingModels.map((f) => path.basename(f, ".js")).join(", ")}`);
368
+ process.exit(1);
369
+ }
370
+ } else {
371
+ modelFile = await select({
372
+ message: "Select model to edit:",
373
+ choices: existingModels.map((f) => ({
374
+ name: path.basename(f, ".js"),
375
+ value: f,
376
+ })),
377
+ });
378
+ }
379
+
380
+ await editModel(modelFile);
381
+ return;
382
+ }
383
+
384
+ // ── Normal schema flow ───────────────────────────
385
+ let schemaName = positionalArgs[0];
386
+
387
+ // If models exist and no name given, show action menu
388
+ if (existingModels.length > 0 && !schemaName) {
389
+ showHeader("schema", "Manage schemas");
390
+
391
+ const action = await select({
392
+ message: "What do you want to do?",
393
+ choices: [
394
+ { name: "Create new schema", value: "create" },
395
+ { name: "View existing schema", value: "view" },
396
+ { name: "Edit existing schema", value: "edit" },
397
+ ],
398
+ });
399
+
400
+ if (action === "view") {
401
+ if (existingModels.length === 1) {
402
+ viewModel(existingModels[0]);
403
+ } else {
404
+ const modelFile = await select({
405
+ message: "Select model to view:",
406
+ choices: existingModels.map((f) => ({
407
+ name: path.basename(f, ".js"),
408
+ value: f,
409
+ })),
410
+ });
411
+ viewModel(modelFile);
412
+ }
413
+ return;
414
+ }
415
+
416
+ if (action === "edit") {
417
+ if (existingModels.length === 1) {
418
+ await editModel(existingModels[0]);
419
+ } else {
420
+ const modelFile = await select({
421
+ message: "Select model to edit:",
422
+ choices: existingModels.map((f) => ({
423
+ name: path.basename(f, ".js"),
424
+ value: f,
425
+ })),
426
+ });
427
+ await editModel(modelFile);
428
+ }
429
+ return;
430
+ }
431
+
432
+ // action === "create" — fall through to create flow
433
+ schemaName = await input({
434
+ message: "Enter schema name:",
435
+ validate: (v) => {
436
+ try { sanitizeName(v); return true; } catch (e) { return e.message; }
437
+ },
438
+ });
439
+ schemaName = sanitizeName(schemaName);
440
+
441
+ } else if (!schemaName) {
442
+ showHeader("schema", "Create a new schema");
443
+ schemaName = await input({
444
+ message: "Enter schema name:",
445
+ validate: (v) => {
446
+ try { sanitizeName(v); return true; } catch (e) { return e.message; }
447
+ },
448
+ });
449
+ schemaName = sanitizeName(schemaName);
450
+
451
+ } else {
452
+ schemaName = sanitizeName(schemaName);
453
+ showHeader("schema", `Schema: ${schemaName}`);
454
+ }
455
+
456
+ await createSchema(schemaName);
457
+ }
458
+
459
+ main().catch((err) => {
460
+ if (err.name === "ExitPromptError") process.exit(0);
461
+ showError(err.message);
462
+ process.exit(1);
463
+ });