@vira-ui/cli 1.1.0 → 1.1.2

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 (3) hide show
  1. package/README.md +64 -0
  2. package/dist/index.js +194 -36
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -457,6 +457,18 @@ vira make migration create-users
457
457
  # Создаёт: migrations/20240101120000_create-users.up.sql
458
458
  # migrations/20240101120000_create-users.down.sql
459
459
 
460
+ # Выполнение миграций
461
+ vira db migrate # Применить все миграции
462
+ vira db up # Алиас для migrate
463
+ vira db rollback # Откатить последнюю миграцию
464
+ vira db down # Алиас для rollback
465
+ vira db status # Показать статус миграций
466
+
467
+ # С указанием URL базы данных
468
+ vira db migrate --db-url "postgres://user:pass@localhost/dbname?sslmode=disable"
469
+ # Или через переменную окружения
470
+ DATABASE_URL="postgres://user:pass@localhost/dbname?sslmode=disable" vira db migrate
471
+
460
472
  # Генерация event handler
461
473
  vira make event user.created
462
474
  # Создаёт: backend/internal/events/user_created.go
@@ -594,6 +606,58 @@ func DeleteUser(w http.ResponseWriter, r *http.Request) {
594
606
 
595
607
  ---
596
608
 
609
+ ### `vira db` — Команды для работы с базой данных
610
+
611
+ Управление миграциями базы данных через goose.
612
+
613
+ **Подкоманды:**
614
+
615
+ #### `vira db migrate` (или `vira db up`)
616
+ Применить все неприменённые миграции.
617
+
618
+ **Опции:**
619
+ - `-d, --dir <directory>` — Директория с миграциями (по умолчанию: `migrations`)
620
+ - `--db-url <url>` — URL подключения к базе данных (или используйте переменную `DATABASE_URL`)
621
+ - `--driver <driver>` — Драйвер БД: `postgres`, `mysql`, `sqlite3` (по умолчанию: `postgres`)
622
+
623
+ **Примеры:**
624
+ ```bash
625
+ # Использование переменной окружения
626
+ export DATABASE_URL="postgres://user:pass@localhost/mydb?sslmode=disable"
627
+ vira db migrate
628
+
629
+ # Или напрямую в команде
630
+ vira db migrate --db-url "postgres://user:pass@localhost/mydb?sslmode=disable"
631
+
632
+ # С указанием директории миграций
633
+ vira db migrate --dir migrations --db-url "postgres://..."
634
+ ```
635
+
636
+ #### `vira db rollback` (или `vira db down`)
637
+ Откатить последнюю применённую миграцию.
638
+
639
+ **Опции:** те же, что у `migrate`
640
+
641
+ **Пример:**
642
+ ```bash
643
+ vira db rollback
644
+ vira db rollback --db-url "postgres://user:pass@localhost/mydb?sslmode=disable"
645
+ ```
646
+
647
+ #### `vira db status`
648
+ Показать статус всех миграций (какие применены, какие нет).
649
+
650
+ **Опции:** те же, что у `migrate`
651
+
652
+ **Пример:**
653
+ ```bash
654
+ vira db status
655
+ ```
656
+
657
+ **Примечание:** Для работы команд требуется установленный `goose`. CLI автоматически попытается установить его при первом использовании, если он не найден.
658
+
659
+ ---
660
+
597
661
  ### `vira sync` — Синхронизация типов
598
662
 
599
663
  Синхронизирует TypeScript типы из Go структур.
package/dist/index.js CHANGED
@@ -91,7 +91,7 @@ const program = new commander_1.Command();
91
91
  program
92
92
  .name("vira")
93
93
  .description("ViraJS CLI - Create projects and generate code")
94
- .version("1.1.0");
94
+ .version("1.1.2");
95
95
  const SUPPORTED_TEMPLATES = ["frontend", "fullstack", "kanban"];
96
96
  /**
97
97
  * Инициализация проекта в текущей директории
@@ -283,15 +283,6 @@ make
283
283
  await generateGoHandler(name, options.dir);
284
284
  console.log(chalk_1.default.green(`✓ handler ${name} created in ${options.dir}`));
285
285
  });
286
- make
287
- .command("model")
288
- .description("Create Go model struct")
289
- .argument("<name>", "Model name (e.g. User)")
290
- .option("-d, --dir <directory>", "Target directory", path.join("backend", "internal", "models"))
291
- .action(async (name, options) => {
292
- await generateGoModel(name, options.dir);
293
- console.log(chalk_1.default.green(`✓ model ${name} created in ${options.dir}`));
294
- });
295
286
  make
296
287
  .command("migration")
297
288
  .description("Create SQL migration (up/down)")
@@ -301,6 +292,39 @@ make
301
292
  await generateMigration(name, options.dir);
302
293
  console.log(chalk_1.default.green(`✓ migration ${name} created in ${options.dir}`));
303
294
  });
295
+ // Команды для выполнения миграций
296
+ const dbCommand = program
297
+ .command("db")
298
+ .description("Database migration commands");
299
+ dbCommand
300
+ .command("migrate")
301
+ .alias("up")
302
+ .description("Run database migrations")
303
+ .option("-d, --dir <directory>", "Migrations directory", "migrations")
304
+ .option("--db-url <url>", "Database connection URL (or use DATABASE_URL env var)")
305
+ .option("--driver <driver>", "Database driver (postgres, mysql, sqlite3)", "postgres")
306
+ .action(async (options) => {
307
+ await runMigrations(options.dir || "migrations", options.dbUrl, options.driver || "postgres", "up");
308
+ });
309
+ dbCommand
310
+ .command("rollback")
311
+ .alias("down")
312
+ .description("Rollback last migration")
313
+ .option("-d, --dir <directory>", "Migrations directory", "migrations")
314
+ .option("--db-url <url>", "Database connection URL (or use DATABASE_URL env var)")
315
+ .option("--driver <driver>", "Database driver (postgres, mysql, sqlite3)", "postgres")
316
+ .action(async (options) => {
317
+ await runMigrations(options.dir || "migrations", options.dbUrl, options.driver || "postgres", "down");
318
+ });
319
+ dbCommand
320
+ .command("status")
321
+ .description("Show migration status")
322
+ .option("-d, --dir <directory>", "Migrations directory", "migrations")
323
+ .option("--db-url <url>", "Database connection URL (or use DATABASE_URL env var)")
324
+ .option("--driver <driver>", "Database driver (postgres, mysql, sqlite3)", "postgres")
325
+ .action(async (options) => {
326
+ await showMigrationStatus(options.dir || "migrations", options.dbUrl, options.driver || "postgres");
327
+ });
304
328
  make
305
329
  .command("event")
306
330
  .description("Create Go event handler stub")
@@ -837,7 +861,29 @@ ${props.map((p) => ` ${p.name}${p.required ? "" : "?"}: ${p.type};`).join("\n")
837
861
  function buildComponentBody(name, vrpConfig, props, hasProps, useViraUI) {
838
862
  const propsUsage = props.map((p) => ` ${p.name}={props.${p.name}}`).join("\n");
839
863
  if (vrpConfig) {
840
- return ` const { data, sendEvent, sendUpdate, sendDiff } = useViraState<${vrpConfig.stateType}>('${vrpConfig.channel}', null);
864
+ // Проверяем, есть ли в channel плейсхолдер {id} или подобный
865
+ const hasPlaceholder = vrpConfig.channel.includes('{id}') || vrpConfig.channel.includes('${id}');
866
+ let channelCode = '';
867
+ if (hasPlaceholder && hasProps) {
868
+ // Находим prop, который может быть id (clientId, userId, id и т.д.)
869
+ const idProp = props.find(p => p.name.toLowerCase().includes('id') ||
870
+ p.name === 'id');
871
+ if (idProp) {
872
+ // Заменяем {id} на значение из props
873
+ const placeholder = vrpConfig.channel.includes('${id}') ? '${id}' : '{id}';
874
+ const channelTemplate = vrpConfig.channel.replace(placeholder, `\${props.${idProp.name}}`);
875
+ channelCode = ` // Динамически формируем channel с ${idProp.name} из props
876
+ const channel = \`${channelTemplate}\`;
877
+ const { data, sendEvent, sendUpdate, sendDiff } = useViraState<${vrpConfig.stateType}>(channel, null);`;
878
+ }
879
+ else {
880
+ channelCode = ` const { data, sendEvent, sendUpdate, sendDiff } = useViraState<${vrpConfig.stateType}>('${vrpConfig.channel}', null);`;
881
+ }
882
+ }
883
+ else {
884
+ channelCode = ` const { data, sendEvent, sendUpdate, sendDiff } = useViraState<${vrpConfig.stateType}>('${vrpConfig.channel}', null);`;
885
+ }
886
+ return `${channelCode}
841
887
 
842
888
  // Use data from VRP state
843
889
  // Example: const value = data?.field ?? defaultValue;
@@ -850,7 +896,7 @@ function buildComponentBody(name, vrpConfig, props, hasProps, useViraUI) {
850
896
  // }, { debounce: 500 });
851
897
 
852
898
  return createElement('div', { className: '${name.toLowerCase()}' },
853
- ${propsUsage ? propsUsage + ",\n" : ""} // Add your content here
899
+ // Add your content here
854
900
  );`;
855
901
  }
856
902
  if (useViraUI) {
@@ -897,7 +943,12 @@ async function generateComponent(name, dir, config) {
897
943
  `;
898
944
  // Интерфейс состояния VRP
899
945
  if (vrpConfig) {
900
- componentCode += `export interface ${vrpConfig.stateType} {
946
+ const typeName = vrpConfig.stateType.replace('State', '');
947
+ componentCode += `// TODO: Если у вас есть синхронизированные типы из backend, используйте их:
948
+ // import type { ${typeName} } from '../vira-types';
949
+ // export type ${vrpConfig.stateType} = ${typeName};
950
+
951
+ export interface ${vrpConfig.stateType} {
901
952
  // Add your state fields here
902
953
  id?: string;
903
954
  }
@@ -976,10 +1027,15 @@ async function collectServiceVRPConfig(name, useVRP, interactive) {
976
1027
  */
977
1028
  function buildVRPService(name, vrpConfig) {
978
1029
  const lowerName = name.toLowerCase();
1030
+ // Пытаемся определить имя типа из синхронизированных типов (например, Client вместо ClientState)
1031
+ const typeName = vrpConfig.stateType.replace('State', '');
979
1032
  return `// ${name} service using Vira Core DI container + VRP
980
- import { createService, useService } from '@vira-ui/core';
1033
+ import { createService, useService, batch } from '@vira-ui/core';
981
1034
  import { useViraState } from '@vira-ui/react';
982
1035
  import { v4 as uuid } from 'uuid';
1036
+ // TODO: Если у вас есть синхронизированные типы из backend, используйте их:
1037
+ // import type { ${typeName} } from '../vira-types';
1038
+ // export type ${vrpConfig.stateType} = ${typeName};
983
1039
 
984
1040
  export interface ${vrpConfig.stateType} {
985
1041
  // Add your state fields here
@@ -1009,22 +1065,26 @@ createService('${lowerName}', () => ({
1009
1065
  // Hook for ${name} operations (combines service + VRP state)
1010
1066
  export function use${name}(id?: string) {
1011
1067
  const channel = id ? \`${vrpConfig.channel}:\${id}\` : '${vrpConfig.channel}';
1012
- const { data, sendEvent, sendUpdate, sendDiff } = id
1068
+ const vrpState = id
1013
1069
  ? useViraState<${vrpConfig.stateType}>(channel, null)
1014
1070
  : useVrpList<${vrpConfig.stateType}>(channel);
1015
1071
  const ${lowerName}Service = useService<{ processData: (data: ${vrpConfig.stateType} | null) => any }>('${lowerName}');
1016
1072
 
1073
+ // Извлекаем методы из VRP состояния
1074
+ const { data, sendEvent, sendDiff } = vrpState;
1075
+ const sendUpdate = 'sendUpdate' in vrpState ? vrpState.sendUpdate : undefined;
1076
+
1017
1077
  return {
1018
1078
  data,
1019
1079
  // 🎯 1️⃣ Создание с авто-генерацией UUID на фронте (VRP сразу знает id, не надо ждать бэка)
1020
1080
  create(item: Omit<${vrpConfig.stateType}, 'id' | 'created_at' | 'updated_at'>) {
1021
- const id = uuid();
1081
+ const itemId = uuid();
1022
1082
  const newItem: ${vrpConfig.stateType} = {
1023
1083
  ...item,
1024
- id,
1084
+ id: itemId,
1025
1085
  created_at: new Date().toISOString(),
1026
1086
  updated_at: new Date().toISOString(),
1027
- };
1087
+ } as ${vrpConfig.stateType};
1028
1088
  sendEvent('${lowerName}.created', {
1029
1089
  ...newItem,
1030
1090
  timestamp: new Date().toISOString()
@@ -1048,16 +1108,17 @@ export function use${name}(id?: string) {
1048
1108
  },
1049
1109
  sendEvent,
1050
1110
  sendDiff,
1111
+ sendUpdate,
1051
1112
  // Service methods
1052
1113
  processData() {
1053
- return ${lowerName}Service.processData(data);
1114
+ // data может быть массивом или объектом, обрабатываем оба случая
1115
+ const singleData = Array.isArray(data) ? null : (data as ${vrpConfig.stateType} | null);
1116
+ return ${lowerName}Service.processData(singleData);
1054
1117
  },
1055
1118
  };
1056
1119
  }
1057
1120
 
1058
1121
  // 🎯 2️⃣ Сервис для bulk actions с VRP (переиспользуемый для любых сущностей)
1059
- // Используем batch() из @vira-ui/core для оптимизации множественных обновлений
1060
- import { batch } from '@vira-ui/core';
1061
1122
 
1062
1123
  createService('${lowerName}Bulk', () => ({
1063
1124
  bulkUpdate(ids: string[], payload: Partial<${vrpConfig.stateType}>, sendEvent: Function) {
@@ -1590,6 +1651,78 @@ async function generateMigration(name, dir) {
1590
1651
  await fs.writeFile(upPath, upTemplate);
1591
1652
  await fs.writeFile(downPath, downTemplate);
1592
1653
  }
1654
+ /**
1655
+ * Выполнение миграций через goose
1656
+ */
1657
+ async function runMigrations(migrationsDir, dbUrl, driver, direction) {
1658
+ const { execSync } = require("child_process");
1659
+ const migrationsPath = path.resolve(process.cwd(), migrationsDir);
1660
+ // Проверяем наличие директории миграций
1661
+ if (!(await fs.pathExists(migrationsPath))) {
1662
+ console.error(chalk_1.default.red(`✗ Migrations directory not found: ${migrationsPath}`));
1663
+ console.log(chalk_1.default.yellow(` Create migrations with: npx vira make migration <name>`));
1664
+ process.exit(1);
1665
+ }
1666
+ // Получаем URL базы данных
1667
+ const databaseUrl = dbUrl || process.env.DATABASE_URL;
1668
+ if (!databaseUrl) {
1669
+ console.error(chalk_1.default.red("✗ Database URL not provided"));
1670
+ console.log(chalk_1.default.yellow(" Use --db-url option or set DATABASE_URL environment variable"));
1671
+ console.log(chalk_1.default.yellow(" Example: DATABASE_URL=postgres://user:pass@localhost/dbname?sslmode=disable"));
1672
+ process.exit(1);
1673
+ }
1674
+ // Проверяем наличие goose
1675
+ try {
1676
+ execSync("which goose", { stdio: "ignore" });
1677
+ }
1678
+ catch {
1679
+ console.log(chalk_1.default.blue("Installing goose..."));
1680
+ try {
1681
+ execSync("go install github.com/pressly/goose/v3/cmd/goose@latest", { stdio: "inherit" });
1682
+ }
1683
+ catch (error) {
1684
+ console.error(chalk_1.default.red("✗ Failed to install goose"));
1685
+ console.log(chalk_1.default.yellow(" Install manually: go install github.com/pressly/goose/v3/cmd/goose@latest"));
1686
+ process.exit(1);
1687
+ }
1688
+ }
1689
+ // Выполняем миграции
1690
+ try {
1691
+ const command = direction === "up" ? "up" : "down";
1692
+ console.log(chalk_1.default.blue(`Running migrations ${direction}...`));
1693
+ execSync(`goose -dir "${migrationsPath}" ${driver} "${databaseUrl}" ${command}`, { stdio: "inherit", cwd: process.cwd() });
1694
+ console.log(chalk_1.default.green(`✓ Migrations ${direction} completed successfully`));
1695
+ }
1696
+ catch (error) {
1697
+ console.error(chalk_1.default.red(`✗ Migration failed: ${error.message}`));
1698
+ process.exit(1);
1699
+ }
1700
+ }
1701
+ /**
1702
+ * Показать статус миграций
1703
+ */
1704
+ async function showMigrationStatus(migrationsDir, dbUrl, driver) {
1705
+ const { execSync } = require("child_process");
1706
+ const migrationsPath = path.resolve(process.cwd(), migrationsDir);
1707
+ if (!(await fs.pathExists(migrationsPath))) {
1708
+ console.error(chalk_1.default.red(`✗ Migrations directory not found: ${migrationsPath}`));
1709
+ process.exit(1);
1710
+ }
1711
+ const databaseUrl = dbUrl || process.env.DATABASE_URL;
1712
+ if (!databaseUrl) {
1713
+ console.error(chalk_1.default.red("✗ Database URL not provided"));
1714
+ console.log(chalk_1.default.yellow(" Use --db-url option or set DATABASE_URL environment variable"));
1715
+ process.exit(1);
1716
+ }
1717
+ try {
1718
+ console.log(chalk_1.default.blue("Migration status:"));
1719
+ execSync(`goose -dir "${migrationsPath}" ${driver} "${databaseUrl}" status`, { stdio: "inherit", cwd: process.cwd() });
1720
+ }
1721
+ catch (error) {
1722
+ console.error(chalk_1.default.red(`✗ Failed to get migration status: ${error.message}`));
1723
+ process.exit(1);
1724
+ }
1725
+ }
1593
1726
  async function generateEventHandler(name, dir) {
1594
1727
  const targetDir = path.join(process.cwd(), dir);
1595
1728
  await fs.ensureDir(targetDir);
@@ -1643,6 +1776,29 @@ async function generateCRUDHandler(name, dir, modelName) {
1643
1776
  const model = modelName || capitalize(name);
1644
1777
  const targetDir = path.join(process.cwd(), dir);
1645
1778
  await fs.ensureDir(targetDir);
1779
+ // Попытка определить модуль из go.mod
1780
+ let modulePath = "your-project/backend";
1781
+ try {
1782
+ // Ищем go.mod в разных возможных местах
1783
+ const possiblePaths = [
1784
+ path.join(process.cwd(), "go.mod"), // корень проекта
1785
+ path.join(process.cwd(), dir, "..", "..", "go.mod"), // backend/../go.mod
1786
+ path.join(process.cwd(), "backend", "go.mod"), // backend/go.mod
1787
+ ];
1788
+ for (const goModPath of possiblePaths) {
1789
+ if (await fs.pathExists(goModPath)) {
1790
+ const goModContent = await fs.readFile(goModPath, "utf8");
1791
+ const moduleMatch = goModContent.match(/^module\s+(.+)$/m);
1792
+ if (moduleMatch) {
1793
+ modulePath = moduleMatch[1];
1794
+ break;
1795
+ }
1796
+ }
1797
+ }
1798
+ }
1799
+ catch (e) {
1800
+ // Игнорируем ошибки, используем дефолтный путь
1801
+ }
1646
1802
  const handlerCode = `package handlers
1647
1803
 
1648
1804
  import (
@@ -1653,6 +1809,8 @@ import (
1653
1809
  "github.com/gorilla/mux"
1654
1810
  "github.com/go-playground/validator/v10"
1655
1811
  "github.com/google/uuid"
1812
+
1813
+ "${modulePath}/internal/models"
1656
1814
  )
1657
1815
 
1658
1816
  var validate = validator.New()
@@ -1666,7 +1824,7 @@ type PaginationParams struct {
1666
1824
 
1667
1825
  // 🎯 Production-ready: List response with pagination
1668
1826
  type ${handlerName}ListResponse struct {
1669
- Items []${model} \`json:"items"\`
1827
+ Items []models.${model} \`json:"items"\`
1670
1828
  Total int \`json:"total"\`
1671
1829
  Limit int \`json:"limit"\`
1672
1830
  Offset int \`json:"offset"\`
@@ -1679,8 +1837,8 @@ type ${handlerName}Event struct {
1679
1837
  Type string \`json:"type"\` // created, updated, deleted
1680
1838
  EntityID string \`json:"entity_id"\`
1681
1839
  UserID string \`json:"user_id,omitempty"\`
1682
- OldValue *${model} \`json:"old_value,omitempty"\`
1683
- NewValue *${model} \`json:"new_value,omitempty"\`
1840
+ OldValue *models.${model} \`json:"old_value,omitempty"\`
1841
+ NewValue *models.${model} \`json:"new_value,omitempty"\`
1684
1842
  Timestamp time.Time \`json:"timestamp"\`
1685
1843
  }
1686
1844
 
@@ -1729,7 +1887,7 @@ func List${handlerName}(w http.ResponseWriter, r *http.Request) {
1729
1887
  // }
1730
1888
 
1731
1889
  response := ${handlerName}ListResponse{
1732
- Items: []${model}{},
1890
+ Items: []models.${model}{},
1733
1891
  Total: 0,
1734
1892
  Limit: limit,
1735
1893
  Offset: offset,
@@ -1763,7 +1921,7 @@ func Get${handlerName}(w http.ResponseWriter, r *http.Request) {
1763
1921
  // return
1764
1922
  // }
1765
1923
 
1766
- item := ${model}{ID: id}
1924
+ item := models.${model}{ID: id}
1767
1925
 
1768
1926
  // 🎯 Production-ready: Cache detail (TTL 5min)
1769
1927
  // redis.Set(cacheKey, item, 5*time.Minute)
@@ -1773,7 +1931,7 @@ func Get${handlerName}(w http.ResponseWriter, r *http.Request) {
1773
1931
 
1774
1932
  // Create${handlerName} handles POST /${safeName}
1775
1933
  func Create${handlerName}(w http.ResponseWriter, r *http.Request) {
1776
- var input ${model}
1934
+ var input models.${model}
1777
1935
 
1778
1936
  if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
1779
1937
  http.Error(w, "Invalid JSON", http.StatusBadRequest)
@@ -1806,13 +1964,13 @@ func Create${handlerName}(w http.ResponseWriter, r *http.Request) {
1806
1964
  // }
1807
1965
 
1808
1966
  // 🎯 Production-ready: Log event for audit trail
1809
- event := ${handlerName}Event{
1810
- ID: uuid.New().String(),
1811
- Type: "created",
1812
- EntityID: input.ID,
1813
- NewValue: &input,
1814
- Timestamp: time.Now(),
1815
- }
1967
+ // event := ${handlerName}Event{
1968
+ // ID: uuid.New().String(),
1969
+ // Type: "created",
1970
+ // EntityID: input.ID,
1971
+ // NewValue: &input,
1972
+ // Timestamp: time.Now(),
1973
+ // }
1816
1974
  // logEvent(event) // Implement event logging to client_events table
1817
1975
 
1818
1976
  // 🎯 Production-ready: Invalidate cache
@@ -1831,7 +1989,7 @@ func Update${handlerName}(w http.ResponseWriter, r *http.Request) {
1831
1989
  vars := mux.Vars(r)
1832
1990
  id := vars["id"]
1833
1991
 
1834
- var input ${model}
1992
+ var input models.${model}
1835
1993
  if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
1836
1994
  http.Error(w, "Invalid JSON", http.StatusBadRequest)
1837
1995
  return
@@ -1884,7 +2042,7 @@ func Update${handlerName}(w http.ResponseWriter, r *http.Request) {
1884
2042
  // Delete${handlerName} handles DELETE /${safeName}/{id}
1885
2043
  func Delete${handlerName}(w http.ResponseWriter, r *http.Request) {
1886
2044
  vars := mux.Vars(r)
1887
- id := vars["id"]
2045
+ id := vars["id"] // id will be used when implementing DB delete and event logging
1888
2046
 
1889
2047
  // 🎯 Production-ready: Get value for event logging
1890
2048
  // oldValue, _ := db.Get${handlerName}(id)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vira-ui/cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "CLI tool for ViraJS project generation",
5
5
  "author": "Vira Team",
6
6
  "license": "MIT",