@famgia/omnify-gui 1.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.
@@ -0,0 +1,1087 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server/index.ts
4
+ import { createServer } from "http";
5
+ import { join as join3 } from "path";
6
+ import open from "open";
7
+
8
+ // src/server/app.ts
9
+ import express from "express";
10
+ import { fileURLToPath } from "url";
11
+ import { dirname, join as join2 } from "path";
12
+
13
+ // src/server/api/schemas.ts
14
+ import { Router } from "express";
15
+
16
+ // src/server/services/schemaService.ts
17
+ import { loadSchemas } from "@famgia/omnify-core";
18
+ import { writeFile, unlink } from "fs/promises";
19
+ import { join } from "path";
20
+ import { stringify } from "yaml";
21
+ function normalizeEnumValues(values) {
22
+ if (!values || !Array.isArray(values)) return void 0;
23
+ return values.map((v) => {
24
+ if (typeof v === "object" && v !== null && "value" in v) {
25
+ const obj = v;
26
+ return {
27
+ value: String(obj.value),
28
+ label: obj.label ? String(obj.label) : void 0,
29
+ extra: obj.extra
30
+ };
31
+ }
32
+ return { value: String(v) };
33
+ });
34
+ }
35
+ var SchemaService = class {
36
+ cache = /* @__PURE__ */ new Map();
37
+ async loadAll(schemasDir2) {
38
+ try {
39
+ const schemas = await loadSchemas(schemasDir2);
40
+ const guiSchemas = {};
41
+ for (const [name, schema] of Object.entries(schemas)) {
42
+ guiSchemas[name] = {
43
+ name: schema.name,
44
+ kind: schema.kind ?? "object",
45
+ displayName: schema.displayName,
46
+ filePath: schema.filePath,
47
+ relativePath: schema.relativePath,
48
+ properties: schema.properties,
49
+ options: schema.options,
50
+ values: normalizeEnumValues(schema.values),
51
+ isDirty: false,
52
+ validationErrors: []
53
+ };
54
+ }
55
+ this.cache.set(schemasDir2, guiSchemas);
56
+ return guiSchemas;
57
+ } catch (error) {
58
+ if (error.code === "ENOENT") {
59
+ return {};
60
+ }
61
+ throw error;
62
+ }
63
+ }
64
+ async load(schemasDir2, name) {
65
+ const schemas = await this.loadAll(schemasDir2);
66
+ return schemas[name] ?? null;
67
+ }
68
+ async save(schemasDir2, schema) {
69
+ const { isDirty: _isDirty, validationErrors: _validationErrors, filePath: _filePath, ...schemaData } = schema;
70
+ const fileName = `${schema.name}.yaml`;
71
+ const targetPath = join(schemasDir2, fileName);
72
+ const yamlContent = stringify(schemaData, {
73
+ lineWidth: 120,
74
+ defaultKeyType: "PLAIN",
75
+ defaultStringType: "PLAIN"
76
+ });
77
+ await writeFile(targetPath, yamlContent, "utf-8");
78
+ return {
79
+ ...schemaData,
80
+ filePath: targetPath,
81
+ isDirty: false,
82
+ validationErrors: []
83
+ };
84
+ }
85
+ async delete(schemasDir2, name) {
86
+ const fileName = `${name}.yaml`;
87
+ const targetPath = join(schemasDir2, fileName);
88
+ await unlink(targetPath);
89
+ this.cache.delete(schemasDir2);
90
+ }
91
+ clearCache(schemasDir2) {
92
+ if (schemasDir2) {
93
+ this.cache.delete(schemasDir2);
94
+ } else {
95
+ this.cache.clear();
96
+ }
97
+ }
98
+ };
99
+ var schemaService = new SchemaService();
100
+
101
+ // src/server/api/schemas.ts
102
+ var schemasRouter = Router();
103
+ schemasRouter.get("/", async (req, res) => {
104
+ try {
105
+ const config = req.app.locals.config;
106
+ const schemas = await schemaService.loadAll(config.schemasDir);
107
+ const response = {
108
+ success: true,
109
+ data: schemas
110
+ };
111
+ res.json(response);
112
+ } catch (error) {
113
+ const response = {
114
+ success: false,
115
+ error: {
116
+ code: "LOAD_ERROR",
117
+ message: error.message
118
+ }
119
+ };
120
+ res.status(500).json(response);
121
+ }
122
+ });
123
+ schemasRouter.get("/:name", async (req, res) => {
124
+ try {
125
+ const config = req.app.locals.config;
126
+ const { name } = req.params;
127
+ const schema = await schemaService.load(config.schemasDir, name);
128
+ if (!schema) {
129
+ const response2 = {
130
+ success: false,
131
+ error: {
132
+ code: "NOT_FOUND",
133
+ message: `Schema "${name}" not found`
134
+ }
135
+ };
136
+ res.status(404).json(response2);
137
+ return;
138
+ }
139
+ const response = {
140
+ success: true,
141
+ data: schema
142
+ };
143
+ res.json(response);
144
+ } catch (error) {
145
+ const response = {
146
+ success: false,
147
+ error: {
148
+ code: "LOAD_ERROR",
149
+ message: error.message
150
+ }
151
+ };
152
+ res.status(500).json(response);
153
+ }
154
+ });
155
+ schemasRouter.post("/", async (req, res) => {
156
+ try {
157
+ const config = req.app.locals.config;
158
+ const schema = req.body;
159
+ if (!schema.name) {
160
+ const response2 = {
161
+ success: false,
162
+ error: {
163
+ code: "VALIDATION_ERROR",
164
+ message: "Schema name is required"
165
+ }
166
+ };
167
+ res.status(400).json(response2);
168
+ return;
169
+ }
170
+ const saved = await schemaService.save(config.schemasDir, schema);
171
+ const response = {
172
+ success: true,
173
+ data: saved
174
+ };
175
+ res.status(201).json(response);
176
+ } catch (error) {
177
+ const response = {
178
+ success: false,
179
+ error: {
180
+ code: "SAVE_ERROR",
181
+ message: error.message
182
+ }
183
+ };
184
+ res.status(500).json(response);
185
+ }
186
+ });
187
+ schemasRouter.put("/:name", async (req, res) => {
188
+ try {
189
+ const config = req.app.locals.config;
190
+ const { name } = req.params;
191
+ const schema = req.body;
192
+ schema.name = name;
193
+ const saved = await schemaService.save(config.schemasDir, schema);
194
+ const response = {
195
+ success: true,
196
+ data: saved
197
+ };
198
+ res.json(response);
199
+ } catch (error) {
200
+ const response = {
201
+ success: false,
202
+ error: {
203
+ code: "SAVE_ERROR",
204
+ message: error.message
205
+ }
206
+ };
207
+ res.status(500).json(response);
208
+ }
209
+ });
210
+ schemasRouter.delete("/:name", async (req, res) => {
211
+ try {
212
+ const config = req.app.locals.config;
213
+ const { name } = req.params;
214
+ await schemaService.delete(config.schemasDir, name);
215
+ const response = {
216
+ success: true
217
+ };
218
+ res.json(response);
219
+ } catch (error) {
220
+ const response = {
221
+ success: false,
222
+ error: {
223
+ code: "DELETE_ERROR",
224
+ message: error.message
225
+ }
226
+ };
227
+ res.status(500).json(response);
228
+ }
229
+ });
230
+
231
+ // src/server/api/validate.ts
232
+ import { Router as Router2 } from "express";
233
+
234
+ // src/server/services/validationService.ts
235
+ import { loadSchemas as loadSchemas2, validateSchemas } from "@famgia/omnify-core";
236
+ function toLoadedSchema(schema) {
237
+ const result = {
238
+ name: schema.name,
239
+ kind: schema.kind,
240
+ filePath: schema.filePath,
241
+ relativePath: schema.relativePath ?? schema.name + ".yaml"
242
+ };
243
+ if (schema.displayName !== void 0) {
244
+ result.displayName = schema.displayName;
245
+ }
246
+ if (schema.properties !== void 0) {
247
+ result.properties = schema.properties;
248
+ }
249
+ if (schema.options !== void 0) {
250
+ result.options = schema.options;
251
+ }
252
+ if (schema.values !== void 0) {
253
+ result.values = schema.values;
254
+ }
255
+ return result;
256
+ }
257
+ var ValidationService = class {
258
+ async validateSchema(schema, schemasDir2) {
259
+ try {
260
+ const allSchemas = await loadSchemas2(schemasDir2);
261
+ const loadedSchema = toLoadedSchema(schema);
262
+ const schemasToValidate = { ...allSchemas };
263
+ schemasToValidate[schema.name] = loadedSchema;
264
+ const result = validateSchemas(schemasToValidate);
265
+ const schemaResult = result.schemas.find((s) => s.schemaName === schema.name);
266
+ const schemaErrors = [];
267
+ if (schemaResult) {
268
+ for (const e of schemaResult.errors) {
269
+ schemaErrors.push({
270
+ path: this.getErrorPath(e, schema.name),
271
+ message: e.message,
272
+ severity: "error"
273
+ });
274
+ }
275
+ }
276
+ return {
277
+ valid: schemaErrors.length === 0,
278
+ errors: schemaErrors
279
+ };
280
+ } catch (error) {
281
+ return {
282
+ valid: false,
283
+ errors: [
284
+ {
285
+ path: schema.name,
286
+ message: error.message,
287
+ severity: "error"
288
+ }
289
+ ]
290
+ };
291
+ }
292
+ }
293
+ async validateAll(schemas) {
294
+ try {
295
+ const loadedSchemas = {};
296
+ for (const [name, schema] of Object.entries(schemas)) {
297
+ loadedSchemas[name] = toLoadedSchema(schema);
298
+ }
299
+ const result = validateSchemas(loadedSchemas);
300
+ const errors = [];
301
+ for (const schemaResult of result.schemas) {
302
+ for (const e of schemaResult.errors) {
303
+ errors.push({
304
+ path: this.getErrorPath(e, schemaResult.schemaName),
305
+ message: e.message,
306
+ severity: "error"
307
+ });
308
+ }
309
+ }
310
+ return {
311
+ valid: result.valid,
312
+ errors
313
+ };
314
+ } catch (error) {
315
+ return {
316
+ valid: false,
317
+ errors: [
318
+ {
319
+ path: "root",
320
+ message: error.message,
321
+ severity: "error"
322
+ }
323
+ ]
324
+ };
325
+ }
326
+ }
327
+ async validateFromDisk(schemasDir2) {
328
+ try {
329
+ const schemas = await loadSchemas2(schemasDir2);
330
+ const guiSchemas = {};
331
+ for (const [name, schema] of Object.entries(schemas)) {
332
+ guiSchemas[name] = {
333
+ name: schema.name,
334
+ kind: schema.kind ?? "object",
335
+ displayName: schema.displayName,
336
+ filePath: schema.filePath,
337
+ relativePath: schema.relativePath,
338
+ properties: schema.properties,
339
+ options: schema.options,
340
+ values: schema.values
341
+ };
342
+ }
343
+ return this.validateAll(guiSchemas);
344
+ } catch (error) {
345
+ return {
346
+ valid: false,
347
+ errors: [
348
+ {
349
+ path: "root",
350
+ message: error.message,
351
+ severity: "error"
352
+ }
353
+ ]
354
+ };
355
+ }
356
+ }
357
+ getErrorPath(error, schemaName) {
358
+ const details = error.details;
359
+ if (details && "propertyName" in details && details.propertyName) {
360
+ return `${schemaName}.${String(details.propertyName)}`;
361
+ }
362
+ return schemaName;
363
+ }
364
+ };
365
+ var validationService = new ValidationService();
366
+
367
+ // src/server/api/validate.ts
368
+ var validateRouter = Router2();
369
+ validateRouter.post("/", async (req, res) => {
370
+ try {
371
+ const config = req.app.locals.config;
372
+ const body = req.body;
373
+ let result;
374
+ if (body.schema) {
375
+ result = await validationService.validateSchema(body.schema, config.schemasDir);
376
+ } else if (body.schemas) {
377
+ result = await validationService.validateAll(body.schemas);
378
+ } else {
379
+ result = await validationService.validateFromDisk(config.schemasDir);
380
+ }
381
+ const response = {
382
+ success: true,
383
+ data: result
384
+ };
385
+ res.json(response);
386
+ } catch (error) {
387
+ const response = {
388
+ success: false,
389
+ error: {
390
+ code: "VALIDATION_ERROR",
391
+ message: error.message
392
+ }
393
+ };
394
+ res.status(500).json(response);
395
+ }
396
+ });
397
+
398
+ // src/server/api/preview.ts
399
+ import { Router as Router3 } from "express";
400
+
401
+ // src/server/services/previewService.ts
402
+ import { loadSchemas as loadSchemas3 } from "@famgia/omnify-core";
403
+ import { generateMigrations as generateLaravelMigrations } from "@famgia/omnify-laravel";
404
+ import { generateMigrations as generateSqlMigrations } from "@famgia/omnify-sql";
405
+ var PreviewService = class {
406
+ async generateAll(schemasDir2, type) {
407
+ const schemas = await loadSchemas3(schemasDir2);
408
+ const previews = [];
409
+ switch (type) {
410
+ case "laravel": {
411
+ const migrations = await generateLaravelMigrations(schemas);
412
+ for (const migration of migrations) {
413
+ previews.push({
414
+ type: "laravel",
415
+ content: migration.content,
416
+ fileName: migration.fileName
417
+ });
418
+ }
419
+ break;
420
+ }
421
+ case "sql": {
422
+ const migrations = generateSqlMigrations(schemas, { dialect: "mysql" });
423
+ for (const migration of migrations) {
424
+ previews.push({
425
+ type: "sql",
426
+ content: migration.content,
427
+ fileName: migration.fileName
428
+ });
429
+ }
430
+ break;
431
+ }
432
+ case "typescript": {
433
+ for (const [name, schema] of Object.entries(schemas)) {
434
+ if (schema.kind === "enum") {
435
+ previews.push({
436
+ type: "typescript",
437
+ content: this.generateEnumType(name, schema),
438
+ fileName: `${name}.ts`
439
+ });
440
+ } else {
441
+ previews.push({
442
+ type: "typescript",
443
+ content: this.generateInterfaceType(name, schema),
444
+ fileName: `${name}.ts`
445
+ });
446
+ }
447
+ }
448
+ break;
449
+ }
450
+ }
451
+ return previews;
452
+ }
453
+ async generateForSchema(schemasDir2, schemaName, type) {
454
+ const schemas = await loadSchemas3(schemasDir2);
455
+ const schema = schemas[schemaName];
456
+ if (!schema) {
457
+ return null;
458
+ }
459
+ switch (type) {
460
+ case "laravel": {
461
+ const migrations = await generateLaravelMigrations({ [schemaName]: schema });
462
+ const migration = migrations[0];
463
+ return migration ? {
464
+ type: "laravel",
465
+ content: migration.content,
466
+ fileName: migration.fileName
467
+ } : null;
468
+ }
469
+ case "sql": {
470
+ const migrations = generateSqlMigrations({ [schemaName]: schema }, { dialect: "mysql" });
471
+ const migration = migrations[0];
472
+ return migration ? {
473
+ type: "sql",
474
+ content: migration.content,
475
+ fileName: migration.fileName
476
+ } : null;
477
+ }
478
+ case "typescript": {
479
+ if (schema.kind === "enum") {
480
+ return {
481
+ type: "typescript",
482
+ content: this.generateEnumType(schemaName, schema),
483
+ fileName: `${schemaName}.ts`
484
+ };
485
+ }
486
+ return {
487
+ type: "typescript",
488
+ content: this.generateInterfaceType(schemaName, schema),
489
+ fileName: `${schemaName}.ts`
490
+ };
491
+ }
492
+ }
493
+ }
494
+ generateEnumType(name, schema) {
495
+ const values = schema.values ?? [];
496
+ return `export type ${name} = ${values.map((v) => `'${v}'`).join(" | ") || "never"};
497
+ `;
498
+ }
499
+ generateInterfaceType(name, schema) {
500
+ const lines = [`export interface ${name} {`];
501
+ if (schema.properties) {
502
+ for (const [propName, prop] of Object.entries(schema.properties)) {
503
+ const tsType = this.mapToTsType(prop.type);
504
+ const optional = prop.nullable ? "?" : "";
505
+ lines.push(` ${propName}${optional}: ${tsType};`);
506
+ }
507
+ }
508
+ lines.push("}");
509
+ return lines.join("\n") + "\n";
510
+ }
511
+ mapToTsType(omnifyType) {
512
+ const typeMap = {
513
+ String: "string",
514
+ Int: "number",
515
+ BigInt: "number",
516
+ Float: "number",
517
+ Decimal: "number",
518
+ Boolean: "boolean",
519
+ Text: "string",
520
+ LongText: "string",
521
+ Date: "string",
522
+ Time: "string",
523
+ Timestamp: "string",
524
+ Json: "Record<string, unknown>",
525
+ Email: "string",
526
+ Password: "string",
527
+ File: "string",
528
+ MultiFile: "string[]",
529
+ Point: "{ lat: number; lng: number }",
530
+ Coordinates: "{ latitude: number; longitude: number }"
531
+ };
532
+ return typeMap[omnifyType] ?? "unknown";
533
+ }
534
+ };
535
+ var previewService = new PreviewService();
536
+
537
+ // src/server/api/preview.ts
538
+ var previewRouter = Router3();
539
+ previewRouter.get("/:type", async (req, res) => {
540
+ try {
541
+ const config = req.app.locals.config;
542
+ const { type } = req.params;
543
+ if (!["laravel", "typescript", "sql"].includes(type)) {
544
+ const response2 = {
545
+ success: false,
546
+ error: {
547
+ code: "INVALID_TYPE",
548
+ message: `Invalid preview type: ${type}. Valid types: laravel, typescript, sql`
549
+ }
550
+ };
551
+ res.status(400).json(response2);
552
+ return;
553
+ }
554
+ const previews = await previewService.generateAll(config.schemasDir, type);
555
+ const response = {
556
+ success: true,
557
+ data: previews
558
+ };
559
+ res.json(response);
560
+ } catch (error) {
561
+ const response = {
562
+ success: false,
563
+ error: {
564
+ code: "PREVIEW_ERROR",
565
+ message: error.message
566
+ }
567
+ };
568
+ res.status(500).json(response);
569
+ }
570
+ });
571
+ previewRouter.get("/:type/:name", async (req, res) => {
572
+ try {
573
+ const config = req.app.locals.config;
574
+ const { type, name } = req.params;
575
+ if (!["laravel", "typescript", "sql"].includes(type)) {
576
+ const response2 = {
577
+ success: false,
578
+ error: {
579
+ code: "INVALID_TYPE",
580
+ message: `Invalid preview type: ${type}. Valid types: laravel, typescript, sql`
581
+ }
582
+ };
583
+ res.status(400).json(response2);
584
+ return;
585
+ }
586
+ const preview = await previewService.generateForSchema(
587
+ config.schemasDir,
588
+ name,
589
+ type
590
+ );
591
+ if (!preview) {
592
+ const response2 = {
593
+ success: false,
594
+ error: {
595
+ code: "NOT_FOUND",
596
+ message: `Schema "${name}" not found`
597
+ }
598
+ };
599
+ res.status(404).json(response2);
600
+ return;
601
+ }
602
+ const response = {
603
+ success: true,
604
+ data: preview
605
+ };
606
+ res.json(response);
607
+ } catch (error) {
608
+ const response = {
609
+ success: false,
610
+ error: {
611
+ code: "PREVIEW_ERROR",
612
+ message: error.message
613
+ }
614
+ };
615
+ res.status(500).json(response);
616
+ }
617
+ });
618
+
619
+ // src/server/api/config.ts
620
+ import { Router as Router4 } from "express";
621
+
622
+ // src/shared/constants.ts
623
+ var DEFAULT_PORT = 3456;
624
+ var DEFAULT_HOST = "localhost";
625
+
626
+ // src/server/api/config.ts
627
+ var configRouter = Router4();
628
+ configRouter.get("/", (req, res) => {
629
+ const appConfig = req.app.locals.config;
630
+ const config = {
631
+ schemasDir: appConfig.schemasDir,
632
+ port: Number(process.env.PORT) || DEFAULT_PORT,
633
+ host: process.env.HOST ?? DEFAULT_HOST
634
+ };
635
+ const response = {
636
+ success: true,
637
+ data: config
638
+ };
639
+ res.json(response);
640
+ });
641
+
642
+ // src/server/api/versions.ts
643
+ import { Router as Router5 } from "express";
644
+
645
+ // src/server/services/versionService.ts
646
+ import { loadSchemas as loadSchemas4 } from "@famgia/omnify-core";
647
+ import {
648
+ createVersionStore
649
+ } from "@famgia/omnify-core";
650
+ var store = null;
651
+ var schemasDir = null;
652
+ function initVersionStore(baseDir, schemasDirPath) {
653
+ store = createVersionStore({ baseDir, maxVersions: 100 });
654
+ schemasDir = schemasDirPath;
655
+ }
656
+ function getStore() {
657
+ if (!store) {
658
+ throw new Error("Version store not initialized. Call initVersionStore first.");
659
+ }
660
+ return store;
661
+ }
662
+ async function listVersions() {
663
+ return getStore().listVersions();
664
+ }
665
+ async function getVersion(version) {
666
+ return getStore().readVersion(version);
667
+ }
668
+ async function getLatestVersion() {
669
+ return getStore().readLatestVersion();
670
+ }
671
+ async function diffVersions(fromVersion, toVersion) {
672
+ return getStore().diffVersions(fromVersion, toVersion);
673
+ }
674
+ function propertyToSnapshot(prop) {
675
+ return {
676
+ type: prop.type,
677
+ ...prop.displayName !== void 0 && { displayName: prop.displayName },
678
+ ...prop.description !== void 0 && { description: prop.description },
679
+ ...prop.nullable !== void 0 && { nullable: prop.nullable },
680
+ ...prop.unique !== void 0 && { unique: prop.unique },
681
+ ...prop.default !== void 0 && { default: prop.default },
682
+ ...prop.length !== void 0 && { length: prop.length },
683
+ ...prop.unsigned !== void 0 && { unsigned: prop.unsigned },
684
+ ...prop.precision !== void 0 && { precision: prop.precision },
685
+ ...prop.scale !== void 0 && { scale: prop.scale },
686
+ ...prop.enum !== void 0 && { enum: prop.enum },
687
+ ...prop.relation !== void 0 && { relation: prop.relation },
688
+ ...prop.target !== void 0 && { target: prop.target },
689
+ ...prop.targets !== void 0 && { targets: prop.targets },
690
+ ...prop.morphName !== void 0 && { morphName: prop.morphName },
691
+ ...prop.onDelete !== void 0 && { onDelete: prop.onDelete },
692
+ ...prop.onUpdate !== void 0 && { onUpdate: prop.onUpdate },
693
+ ...prop.mappedBy !== void 0 && { mappedBy: prop.mappedBy },
694
+ ...prop.inversedBy !== void 0 && { inversedBy: prop.inversedBy },
695
+ ...prop.joinTable !== void 0 && { joinTable: prop.joinTable },
696
+ ...prop.owning !== void 0 && { owning: prop.owning }
697
+ };
698
+ }
699
+ function schemasToSnapshot(schemas) {
700
+ const snapshot = {};
701
+ for (const [name, schema] of Object.entries(schemas)) {
702
+ const properties = {};
703
+ if (schema.properties) {
704
+ for (const [propName, prop] of Object.entries(schema.properties)) {
705
+ properties[propName] = propertyToSnapshot(prop);
706
+ }
707
+ }
708
+ const opts = schema.options;
709
+ snapshot[name] = {
710
+ name: schema.name,
711
+ kind: schema.kind ?? "object",
712
+ ...Object.keys(properties).length > 0 && { properties },
713
+ ...schema.values && { values: schema.values },
714
+ ...opts && {
715
+ options: {
716
+ ...opts.id !== void 0 && { id: opts.id },
717
+ ...opts.idType !== void 0 && { idType: opts.idType },
718
+ ...opts.timestamps !== void 0 && { timestamps: opts.timestamps },
719
+ ...opts.softDelete !== void 0 && { softDelete: opts.softDelete },
720
+ ...opts.tableName !== void 0 && { tableName: opts.tableName },
721
+ ...opts.translations !== void 0 && { translations: opts.translations },
722
+ ...opts.authenticatable !== void 0 && { authenticatable: opts.authenticatable }
723
+ }
724
+ }
725
+ };
726
+ }
727
+ return snapshot;
728
+ }
729
+ async function getPendingChanges() {
730
+ if (!schemasDir) {
731
+ throw new Error("Schemas directory not initialized");
732
+ }
733
+ const storeInstance = getStore();
734
+ const currentSchemas = await loadSchemas4(schemasDir);
735
+ const currentSnapshot = schemasToSnapshot(currentSchemas);
736
+ const latestVersion = await storeInstance.readLatestVersion();
737
+ if (!latestVersion) {
738
+ const changes2 = Object.keys(currentSnapshot).map((name) => ({
739
+ action: "schema_added",
740
+ schema: name
741
+ }));
742
+ return {
743
+ hasChanges: changes2.length > 0,
744
+ changes: changes2,
745
+ currentSchemaCount: Object.keys(currentSnapshot).length,
746
+ previousSchemaCount: 0,
747
+ latestVersion: null
748
+ };
749
+ }
750
+ const changes = storeInstance.computeSnapshotDiff(latestVersion.snapshot, currentSnapshot);
751
+ return {
752
+ hasChanges: changes.length > 0,
753
+ changes,
754
+ currentSchemaCount: Object.keys(currentSnapshot).length,
755
+ previousSchemaCount: Object.keys(latestVersion.snapshot).length,
756
+ latestVersion: latestVersion.version
757
+ };
758
+ }
759
+
760
+ // src/server/api/versions.ts
761
+ var versionsRouter = Router5();
762
+ versionsRouter.get("/", async (_req, res) => {
763
+ try {
764
+ const versions = await listVersions();
765
+ const response = {
766
+ success: true,
767
+ data: versions
768
+ };
769
+ res.json(response);
770
+ } catch (error) {
771
+ const response = {
772
+ success: false,
773
+ error: {
774
+ code: "VERSION_LIST_ERROR",
775
+ message: error.message
776
+ }
777
+ };
778
+ res.status(500).json(response);
779
+ }
780
+ });
781
+ versionsRouter.get("/pending", async (_req, res) => {
782
+ try {
783
+ const pending = await getPendingChanges();
784
+ const response = {
785
+ success: true,
786
+ data: pending
787
+ };
788
+ res.json(response);
789
+ } catch (error) {
790
+ const response = {
791
+ success: false,
792
+ error: {
793
+ code: "PENDING_CHANGES_ERROR",
794
+ message: error.message
795
+ }
796
+ };
797
+ res.status(500).json(response);
798
+ }
799
+ });
800
+ versionsRouter.get("/latest", async (_req, res) => {
801
+ try {
802
+ const version = await getLatestVersion();
803
+ const response = {
804
+ success: true,
805
+ data: version
806
+ };
807
+ res.json(response);
808
+ } catch (error) {
809
+ const response = {
810
+ success: false,
811
+ error: {
812
+ code: "VERSION_READ_ERROR",
813
+ message: error.message
814
+ }
815
+ };
816
+ res.status(500).json(response);
817
+ }
818
+ });
819
+ versionsRouter.get("/:version", async (req, res) => {
820
+ try {
821
+ const versionNum = parseInt(req.params.version, 10);
822
+ if (isNaN(versionNum)) {
823
+ const response2 = {
824
+ success: false,
825
+ error: {
826
+ code: "INVALID_VERSION",
827
+ message: "Version must be a number"
828
+ }
829
+ };
830
+ res.status(400).json(response2);
831
+ return;
832
+ }
833
+ const version = await getVersion(versionNum);
834
+ if (!version) {
835
+ const response2 = {
836
+ success: false,
837
+ error: {
838
+ code: "VERSION_NOT_FOUND",
839
+ message: `Version ${versionNum} not found`
840
+ }
841
+ };
842
+ res.status(404).json(response2);
843
+ return;
844
+ }
845
+ const response = {
846
+ success: true,
847
+ data: version
848
+ };
849
+ res.json(response);
850
+ } catch (error) {
851
+ const response = {
852
+ success: false,
853
+ error: {
854
+ code: "VERSION_READ_ERROR",
855
+ message: error.message
856
+ }
857
+ };
858
+ res.status(500).json(response);
859
+ }
860
+ });
861
+ versionsRouter.get("/diff/:from/:to", async (req, res) => {
862
+ try {
863
+ const fromVersion = parseInt(req.params.from, 10);
864
+ const toVersion = parseInt(req.params.to, 10);
865
+ if (isNaN(fromVersion) || isNaN(toVersion)) {
866
+ const response2 = {
867
+ success: false,
868
+ error: {
869
+ code: "INVALID_VERSION",
870
+ message: "Version numbers must be integers"
871
+ }
872
+ };
873
+ res.status(400).json(response2);
874
+ return;
875
+ }
876
+ const diff = await diffVersions(fromVersion, toVersion);
877
+ if (!diff) {
878
+ const response2 = {
879
+ success: false,
880
+ error: {
881
+ code: "DIFF_ERROR",
882
+ message: "Could not compute diff. One or both versions may not exist."
883
+ }
884
+ };
885
+ res.status(404).json(response2);
886
+ return;
887
+ }
888
+ const response = {
889
+ success: true,
890
+ data: diff
891
+ };
892
+ res.json(response);
893
+ } catch (error) {
894
+ const response = {
895
+ success: false,
896
+ error: {
897
+ code: "DIFF_ERROR",
898
+ message: error.message
899
+ }
900
+ };
901
+ res.status(500).json(response);
902
+ }
903
+ });
904
+
905
+ // src/server/app.ts
906
+ var __filename = fileURLToPath(import.meta.url);
907
+ var __dirname = dirname(__filename);
908
+ function createApp(config) {
909
+ const app = express();
910
+ app.locals.config = config;
911
+ initVersionStore(config.cwd, config.schemasDir);
912
+ app.use(express.json());
913
+ app.use("/api/schemas", schemasRouter);
914
+ app.use("/api/validate", validateRouter);
915
+ app.use("/api/preview", previewRouter);
916
+ app.use("/api/config", configRouter);
917
+ app.use("/api/versions", versionsRouter);
918
+ if (process.env.NODE_ENV === "production") {
919
+ const clientDist = join2(__dirname, "../client");
920
+ app.use(express.static(clientDist));
921
+ app.get("*", (_req, res) => {
922
+ res.sendFile(join2(clientDist, "index.html"));
923
+ });
924
+ }
925
+ app.use((err, _req, res, _next) => {
926
+ console.error("Server error:", err);
927
+ const response = {
928
+ success: false,
929
+ error: {
930
+ code: "INTERNAL_ERROR",
931
+ message: err.message
932
+ }
933
+ };
934
+ res.status(500).json(response);
935
+ });
936
+ return app;
937
+ }
938
+
939
+ // src/server/ws/handler.ts
940
+ import { WebSocketServer, WebSocket } from "ws";
941
+ function createWsHandler(server) {
942
+ const wss = new WebSocketServer({ server, path: "/ws" });
943
+ const clients = /* @__PURE__ */ new Set();
944
+ wss.on("connection", (ws) => {
945
+ clients.add(ws);
946
+ console.log(" WebSocket client connected");
947
+ const readyEvent = {
948
+ type: "connection:ready",
949
+ payload: {
950
+ schemasDir: process.env.SCHEMAS_DIR ?? "schemas",
951
+ schemaCount: 0
952
+ }
953
+ };
954
+ ws.send(JSON.stringify(readyEvent));
955
+ ws.on("message", (data) => {
956
+ try {
957
+ const event = JSON.parse(data.toString());
958
+ handleClientEvent(event, ws);
959
+ } catch {
960
+ console.error("Invalid WebSocket message");
961
+ }
962
+ });
963
+ ws.on("close", () => {
964
+ clients.delete(ws);
965
+ console.log(" WebSocket client disconnected");
966
+ });
967
+ ws.on("error", (error) => {
968
+ console.error("WebSocket error:", error);
969
+ clients.delete(ws);
970
+ });
971
+ });
972
+ function handleClientEvent(_event, _ws) {
973
+ }
974
+ function broadcast(event) {
975
+ const message = JSON.stringify(event);
976
+ for (const client of clients) {
977
+ if (client.readyState === WebSocket.OPEN) {
978
+ client.send(message);
979
+ }
980
+ }
981
+ }
982
+ function close() {
983
+ for (const client of clients) {
984
+ client.close();
985
+ }
986
+ wss.close();
987
+ }
988
+ return { broadcast, close };
989
+ }
990
+
991
+ // src/server/watcher/fileWatcher.ts
992
+ import chokidar from "chokidar";
993
+ import { basename } from "path";
994
+ function createFileWatcher(schemasDir2, wsHandler) {
995
+ const watcher = chokidar.watch(`${schemasDir2}/*.yaml`, {
996
+ persistent: true,
997
+ ignoreInitial: true,
998
+ awaitWriteFinish: {
999
+ stabilityThreshold: 200,
1000
+ pollInterval: 100
1001
+ }
1002
+ });
1003
+ watcher.on("add", (filePath) => {
1004
+ console.log(` Schema added: ${basename(filePath)}`);
1005
+ void notifySchemaChange(filePath, "file");
1006
+ });
1007
+ watcher.on("change", (filePath) => {
1008
+ console.log(` Schema changed: ${basename(filePath)}`);
1009
+ void notifySchemaChange(filePath, "file");
1010
+ });
1011
+ watcher.on("unlink", (filePath) => {
1012
+ console.log(` Schema deleted: ${basename(filePath)}`);
1013
+ void notifyReload();
1014
+ });
1015
+ async function notifySchemaChange(filePath, source) {
1016
+ try {
1017
+ schemaService.clearCache(schemasDir2);
1018
+ const schemas = await schemaService.loadAll(schemasDir2);
1019
+ const name = basename(filePath, ".yaml");
1020
+ const schema = schemas[name];
1021
+ if (schema) {
1022
+ const event = {
1023
+ type: "schema:changed",
1024
+ payload: { name, schema, source }
1025
+ };
1026
+ wsHandler.broadcast(event);
1027
+ }
1028
+ } catch (error) {
1029
+ console.error("Error notifying schema change:", error);
1030
+ }
1031
+ }
1032
+ async function notifyReload() {
1033
+ try {
1034
+ schemaService.clearCache(schemasDir2);
1035
+ const schemas = await schemaService.loadAll(schemasDir2);
1036
+ const event = {
1037
+ type: "schemas:reloaded",
1038
+ payload: { schemas }
1039
+ };
1040
+ wsHandler.broadcast(event);
1041
+ } catch (error) {
1042
+ console.error("Error notifying reload:", error);
1043
+ }
1044
+ }
1045
+ return {
1046
+ close: () => watcher.close()
1047
+ };
1048
+ }
1049
+
1050
+ // src/server/index.ts
1051
+ async function main() {
1052
+ const port = Number(process.env.PORT) || DEFAULT_PORT;
1053
+ const host = process.env.HOST ?? DEFAULT_HOST;
1054
+ const cwd = process.cwd();
1055
+ const schemasDir2 = process.env.SCHEMAS_DIR ?? join3(cwd, "schemas");
1056
+ console.log("Starting Omnify GUI...");
1057
+ console.log(` Schemas directory: ${schemasDir2}`);
1058
+ const app = createApp({ schemasDir: schemasDir2, cwd });
1059
+ const server = createServer(app);
1060
+ const wsHandler = createWsHandler(server);
1061
+ const watcher = createFileWatcher(schemasDir2, wsHandler);
1062
+ server.listen(port, host, () => {
1063
+ const url = `http://${host}:${port}`;
1064
+ console.log(` GUI running at: ${url}`);
1065
+ console.log(" Press Ctrl+C to stop\n");
1066
+ if (process.env.NODE_ENV !== "production") {
1067
+ open(url).catch(() => {
1068
+ });
1069
+ }
1070
+ });
1071
+ const shutdown = () => {
1072
+ console.log("\nShutting down...");
1073
+ watcher.close();
1074
+ wsHandler.close();
1075
+ server.close(() => {
1076
+ console.log("Server closed");
1077
+ process.exit(0);
1078
+ });
1079
+ };
1080
+ process.on("SIGINT", shutdown);
1081
+ process.on("SIGTERM", shutdown);
1082
+ }
1083
+ main().catch((error) => {
1084
+ console.error("Failed to start server:", error);
1085
+ process.exit(1);
1086
+ });
1087
+ //# sourceMappingURL=index.js.map