@vira-ui/cli 1.0.1 → 1.1.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 (3) hide show
  1. package/README.md +965 -0
  2. package/dist/index.js +1271 -40
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -91,8 +91,79 @@ 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("0.4.0-alpha");
94
+ .version("1.1.0");
95
95
  const SUPPORTED_TEMPLATES = ["frontend", "fullstack", "kanban"];
96
+ /**
97
+ * Инициализация проекта в текущей директории
98
+ */
99
+ program
100
+ .command("init")
101
+ .description("Initialize a Vira project in the current directory")
102
+ .option("-t, --template <template>", "Template type (frontend|fullstack|kanban). If not specified, interactive selection will be shown.")
103
+ .action(async (options) => {
104
+ const projectPath = process.cwd();
105
+ const projectName = path.basename(projectPath);
106
+ // Проверяем, не пуста ли директория
107
+ const files = await fs.readdir(projectPath);
108
+ const ignoreFiles = ['.git', '.gitignore', 'node_modules', '.DS_Store'];
109
+ const visibleFiles = files.filter(f => !ignoreFiles.includes(f) && !f.startsWith('.'));
110
+ if (visibleFiles.length > 0) {
111
+ const { proceed } = await inquirer_1.default.prompt([
112
+ {
113
+ type: "confirm",
114
+ name: "proceed",
115
+ message: `Directory is not empty. Continue anyway?`,
116
+ default: false,
117
+ },
118
+ ]);
119
+ if (!proceed) {
120
+ console.log(chalk_1.default.yellow("Init cancelled."));
121
+ process.exit(0);
122
+ }
123
+ }
124
+ console.log(chalk_1.default.blue(`\nInitializing Vira project in: ${projectPath}\n`));
125
+ // Интерактивный выбор шаблона, если не указан
126
+ let template;
127
+ if (options.template) {
128
+ template = options.template;
129
+ if (!SUPPORTED_TEMPLATES.includes(template)) {
130
+ console.error(chalk_1.default.red(`Unknown template: ${template}. Use one of: ${SUPPORTED_TEMPLATES.join(", ")}`));
131
+ process.exit(1);
132
+ }
133
+ }
134
+ else {
135
+ const { selectedTemplate } = await inquirer_1.default.prompt([
136
+ {
137
+ type: "list",
138
+ name: "selectedTemplate",
139
+ message: "Select a template:",
140
+ choices: [
141
+ {
142
+ name: "Frontend (React + Vite + Vira UI)",
143
+ value: "frontend",
144
+ short: "frontend",
145
+ },
146
+ {
147
+ name: "Fullstack (Frontend + Go Backend + Docker)",
148
+ value: "fullstack",
149
+ short: "fullstack",
150
+ },
151
+ {
152
+ name: "Kanban (Reference app with VRP)",
153
+ value: "kanban",
154
+ short: "kanban",
155
+ },
156
+ ],
157
+ default: "frontend",
158
+ },
159
+ ]);
160
+ template = selectedTemplate;
161
+ }
162
+ // Создаём структуру проекта
163
+ await createProjectStructure(projectPath, template);
164
+ console.log(chalk_1.default.green(`\n✓ Project initialized successfully!\n`));
165
+ printNextSteps(projectName, template, true);
166
+ });
96
167
  /**
97
168
  * Создание проекта
98
169
  */
@@ -159,18 +230,27 @@ program
159
230
  .command("generate")
160
231
  .alias("g")
161
232
  .description("Generate code")
162
- .argument("<type>", "Type: component, service, page, model, route")
233
+ .argument("<type>", "Type: component, service, page, model, route, test")
163
234
  .argument("<name>", "Name")
164
235
  .option("-d, --dir <directory>", "Output directory", "src")
236
+ .option("-i, --interactive", "Interactive mode (for components and services)", false)
237
+ .option("--vrp", "Use Vira Reactive Protocol (VRP)", false)
238
+ .option("--no-vrp", "Do not use VRP (explicit)", false)
165
239
  .action(async (type, name, options) => {
166
240
  console.log(chalk_1.default.blue(`Generating ${type}: ${name}`));
167
241
  switch (type) {
168
242
  case "component":
169
243
  case "comp":
170
- await generateComponent(name, options.dir);
244
+ await generateComponent(name, options.dir, {
245
+ interactive: options.interactive ?? false,
246
+ useVRP: options.vrp ? true : options.noVrp ? false : undefined,
247
+ });
171
248
  break;
172
249
  case "service":
173
- await generateService(name, options.dir);
250
+ await generateService(name, options.dir, {
251
+ interactive: options.interactive ?? false,
252
+ useVRP: options.vrp ? true : options.noVrp ? false : undefined,
253
+ });
174
254
  break;
175
255
  case "page":
176
256
  await generatePage(name, options.dir);
@@ -181,6 +261,9 @@ program
181
261
  case "route":
182
262
  await generateRoute(name, options.dir);
183
263
  break;
264
+ case "test":
265
+ await generateTest(name, options.dir);
266
+ break;
184
267
  default:
185
268
  console.error(chalk_1.default.red(`Unknown type: ${type}`));
186
269
  process.exit(1);
@@ -227,22 +310,115 @@ make
227
310
  await generateEventHandler(name, options.dir);
228
311
  console.log(chalk_1.default.green(`✓ event handler ${name} created in ${options.dir}`));
229
312
  });
313
+ make
314
+ .command("model")
315
+ .description("Create a Go model struct")
316
+ .argument("<name>", "Model name (e.g. Client)")
317
+ .option("-d, --dir <directory>", "Target directory", path.join("backend", "internal", "models"))
318
+ .option("-f, --fields <fields>", "Comma-separated field definitions (e.g. 'name:string,email:string,phone:string')")
319
+ .action(async (name, options) => {
320
+ await generateGoModel(name, options.dir, options.fields || undefined);
321
+ console.log(chalk_1.default.green(`✓ Go model ${name} created in ${options.dir}`));
322
+ });
323
+ make
324
+ .command("crud")
325
+ .description("Create CRUD handlers for a resource")
326
+ .argument("<name>", "Resource name (e.g. user)")
327
+ .option("-d, --dir <directory>", "Target directory", path.join("backend", "internal", "handlers"))
328
+ .option("-m, --model <model>", "Model name (defaults to capitalized resource name)")
329
+ .action(async (name, options) => {
330
+ await generateCRUDHandler(name, options.dir, options.model);
331
+ console.log(chalk_1.default.green(`✓ CRUD handlers for ${name} created in ${options.dir}`));
332
+ });
230
333
  const protoCommand = program
231
334
  .command("proto")
232
- .description("VRP protocol utilities");
335
+ .description("VRP (Vira Reactive Protocol) utilities");
233
336
  protoCommand
234
337
  .command("validate")
235
- .description("Validate VRP protocol schema")
236
- .action(async () => {
237
- console.log(chalk_1.default.green("✓ VRP v0.1 protocol schema validated"));
238
- console.log(chalk_1.default.gray(" See VIRA_PROTOCOL.md for full specification"));
338
+ .description("Validate VRP protocol schema and types")
339
+ .option("--file <path>", "Path to types file", path.join("backend", "internal", "types", "types.go"))
340
+ .action(async (options) => {
341
+ const typesPath = path.resolve(process.cwd(), options.file || path.join("backend", "internal", "types", "types.go"));
342
+ try {
343
+ if (await fs.pathExists(typesPath)) {
344
+ const content = await fs.readFile(typesPath, "utf8");
345
+ const structs = parseGoStructs(content);
346
+ if (structs.length === 0) {
347
+ console.log(chalk_1.default.yellow("⚠ No Go structs found in types file"));
348
+ }
349
+ else {
350
+ console.log(chalk_1.default.green(`✓ VRP v0.1 protocol schema validated`));
351
+ console.log(chalk_1.default.gray(` Found ${structs.length} type(s): ${structs.map(s => s.name).join(", ")}`));
352
+ console.log(chalk_1.default.gray(" See VIRA_PROTOCOL.md for full specification"));
353
+ }
354
+ }
355
+ else {
356
+ console.log(chalk_1.default.yellow(`⚠ Types file not found: ${typesPath}`));
357
+ console.log(chalk_1.default.gray(" VRP protocol schema structure is valid"));
358
+ }
359
+ }
360
+ catch (error) {
361
+ console.log(chalk_1.default.yellow("⚠ Could not validate types file"));
362
+ console.log(chalk_1.default.gray(" VRP protocol schema structure is valid"));
363
+ }
239
364
  });
240
365
  protoCommand
241
366
  .command("generate")
242
- .description("Generate protocol documentation/schemas")
243
- .action(async () => {
244
- console.log(chalk_1.default.green(" VRP protocol docs: VIRA_PROTOCOL.md"));
245
- console.log(chalk_1.default.gray(" Protocol version: v0.1"));
367
+ .description("Generate protocol documentation and channel helpers")
368
+ .option("--file <path>", "Path to types file", path.join("backend", "internal", "types", "types.go"))
369
+ .option("--output <path>", "Output directory", "docs")
370
+ .action(async (options) => {
371
+ const typesPath = path.resolve(process.cwd(), options.file || path.join("backend", "internal", "types", "types.go"));
372
+ const outputDir = path.resolve(process.cwd(), options.output || "docs");
373
+ try {
374
+ if (await fs.pathExists(typesPath)) {
375
+ const content = await fs.readFile(typesPath, "utf8");
376
+ const structs = parseGoStructs(content);
377
+ await fs.ensureDir(outputDir);
378
+ // Генерируем документацию по каналам
379
+ const channelDocs = generateChannelDocumentation(structs);
380
+ await fs.writeFile(path.join(outputDir, "VRP_CHANNELS.md"), channelDocs);
381
+ console.log(chalk_1.default.green(`✓ VRP protocol docs generated`));
382
+ console.log(chalk_1.default.gray(` Channels: ${outputDir}/VRP_CHANNELS.md`));
383
+ console.log(chalk_1.default.gray(` Protocol version: v0.1`));
384
+ console.log(chalk_1.default.gray(` Types: ${structs.length}`));
385
+ }
386
+ else {
387
+ console.log(chalk_1.default.yellow(`⚠ Types file not found: ${typesPath}`));
388
+ console.log(chalk_1.default.gray(" Generating basic protocol documentation"));
389
+ await fs.ensureDir(outputDir);
390
+ const basicDocs = `# Vira Reactive Protocol (VRP)
391
+
392
+ Protocol version: v0.1
393
+
394
+ ## Overview
395
+
396
+ VRP is a WebSocket-based protocol for real-time state synchronization.
397
+
398
+ ## Message Types
399
+
400
+ - \`handshake\` - Initial connection
401
+ - \`ack\` - Acknowledgment
402
+ - \`sub\` - Subscribe to channel
403
+ - \`unsub\` - Unsubscribe from channel
404
+ - \`update\` - Full state update
405
+ - \`event\` - Event notification
406
+ - \`diff\` - Partial state update
407
+ - \`ping\` / \`pong\` - Keep-alive
408
+ - \`error\` - Error message
409
+
410
+ See VIRA_PROTOCOL.md for full specification.
411
+ `;
412
+ await fs.writeFile(path.join(outputDir, "VRP_CHANNELS.md"), basicDocs);
413
+ console.log(chalk_1.default.green(`✓ Basic VRP protocol docs: ${outputDir}/VRP_CHANNELS.md`));
414
+ }
415
+ }
416
+ catch (error) {
417
+ console.error(chalk_1.default.red("✗ Error generating documentation"));
418
+ if (error instanceof Error) {
419
+ console.error(chalk_1.default.red(` ${error.message}`));
420
+ }
421
+ }
246
422
  });
247
423
  program
248
424
  .command("doc")
@@ -268,14 +444,27 @@ program
268
444
  .option("--backend <path>", "Path to Go types file", path.join("backend", "internal", "types", "types.go"))
269
445
  .option("--frontend <path>", "Output TS types path (frontend)", path.join("frontend", "src", "vira-types.ts"))
270
446
  .option("--ui <path>", "Output TS types path (ui)", path.join("ui", "src", "vira-types.ts"))
447
+ .option("-w, --watch", "Watch mode: automatically sync on file changes", false)
271
448
  .action(async (options) => {
272
449
  if (options.types) {
273
- await syncTypes(options);
450
+ if (options.watch) {
451
+ console.log(chalk_1.default.yellow("Watch mode is not yet implemented. Running once..."));
452
+ await syncTypes(options);
453
+ }
454
+ else {
455
+ await syncTypes(options);
456
+ }
274
457
  }
275
458
  else {
276
459
  console.log(chalk_1.default.yellow("Nothing to sync. Use --types to sync TypeScript types."));
277
460
  }
278
461
  });
462
+ program
463
+ .command("validate")
464
+ .description("Validate project structure and configuration")
465
+ .action(async () => {
466
+ await validateProject();
467
+ });
279
468
  program.parse(process.argv);
280
469
  /**
281
470
  * Создание структуры проекта
@@ -313,6 +502,16 @@ async function createFullstackProject(projectPath) {
313
502
  await createBackendStub(backendPath);
314
503
  await createDeployScaffold(deployPath);
315
504
  await createWorkspaceReadme(projectPath);
505
+ // 🎯 6️⃣ Добавляем корневой package.json с start:dev скриптом
506
+ const rootPackageJson = {
507
+ name: path.basename(projectPath),
508
+ version: "0.1.0",
509
+ private: true,
510
+ scripts: {
511
+ "start:dev": "cd deploy && docker compose -f docker-compose.dev.yml up -d && cd ../frontend && npm install && npm run dev",
512
+ },
513
+ };
514
+ await fs.writeJSON(path.join(projectPath, "package.json"), rootPackageJson, { spaces: 2 });
316
515
  }
317
516
  /**
318
517
  * Create Kanban reference app (VRP-only, no direct useState/fetch)
@@ -361,7 +560,10 @@ async function createFrontendProject(projectPath) {
361
560
  dependencies: {
362
561
  "@vira-ui/core": "^1.0.0",
363
562
  "@vira-ui/ui": "^1.0.0",
563
+ "@vira-ui/react": "^1.0.0",
364
564
  "lucide-react": "^0.400.0",
565
+ "uuid": "^9.0.1",
566
+ // lodash не нужен - используем debounceServiceMethod из @vira-ui/core!
365
567
  },
366
568
  devDependencies: {
367
569
  "@vira-ui/babel-plugin": "^1.0.0",
@@ -369,6 +571,7 @@ async function createFrontendProject(projectPath) {
369
571
  "@types/node": "^20.10.0",
370
572
  "@types/react": "^18.2.0",
371
573
  "@types/react-dom": "^18.2.0",
574
+ "@types/uuid": "^9.0.7",
372
575
  "react": "^18.2.0",
373
576
  "react-dom": "^18.2.0",
374
577
  "typescript": "^5.3.0",
@@ -440,10 +643,15 @@ async function createDeployScaffold(deployPath) {
440
643
  async function createWorkspaceReadme(projectPath) {
441
644
  await fs.writeFile(path.join(projectPath, "README.md"), readme_1.readme);
442
645
  }
443
- function printNextSteps(projectName, template) {
646
+ function printNextSteps(projectName, template, isInit = false) {
444
647
  console.log(chalk_1.default.yellow(`\nNext steps:`));
445
648
  if (template === "fullstack") {
446
- console.log(` cd ${projectName}/frontend`);
649
+ if (!isInit) {
650
+ console.log(` cd ${projectName}/frontend`);
651
+ }
652
+ else {
653
+ console.log(` cd frontend`);
654
+ }
447
655
  console.log(` npm install`);
448
656
  console.log(` npm run dev`);
449
657
  console.log(`\nUI package:`);
@@ -455,41 +663,435 @@ function printNextSteps(projectName, template) {
455
663
  console.log(` go mod tidy`);
456
664
  console.log(` go run ./cmd/api`);
457
665
  console.log(`\nDev stack (DB/Redis/Kafka):`);
458
- console.log(` cd ../deploy && docker compose -f docker-compose.dev.yml up`);
666
+ if (!isInit) {
667
+ console.log(` cd ../${projectName}/deploy && docker compose -f docker-compose.dev.yml up`);
668
+ }
669
+ else {
670
+ console.log(` cd ../deploy && docker compose -f docker-compose.dev.yml up`);
671
+ }
459
672
  return;
460
673
  }
461
- console.log(` cd ${projectName}`);
674
+ if (!isInit) {
675
+ console.log(` cd ${projectName}`);
676
+ }
462
677
  console.log(` npm install`);
463
678
  console.log(` npm run dev`);
464
679
  }
680
+ /**
681
+ * Сбор VRP конфигурации
682
+ */
683
+ async function collectVRPConfig(name, useVRP, interactive) {
684
+ if (useVRP === false) {
685
+ return null;
686
+ }
687
+ if (!interactive && useVRP !== true) {
688
+ return null;
689
+ }
690
+ // Если явно указано --vrp, используем значения по умолчанию
691
+ if (useVRP === true && !interactive) {
692
+ return {
693
+ channel: name.toLowerCase(),
694
+ stateType: `${name}State`,
695
+ };
696
+ }
697
+ // Интерактивный режим: спрашиваем
698
+ if (interactive && useVRP === undefined) {
699
+ const answer = await inquirer_1.default.prompt([
700
+ {
701
+ type: "confirm",
702
+ name: "useVRP",
703
+ message: "Use Vira Reactive Protocol (VRP) for state management?",
704
+ default: false,
705
+ },
706
+ ]);
707
+ if (!answer.useVRP) {
708
+ return null;
709
+ }
710
+ }
711
+ // Спрашиваем детали VRP
712
+ const config = await inquirer_1.default.prompt([
713
+ {
714
+ type: "input",
715
+ name: "channel",
716
+ message: "VRP channel name (e.g., 'user', 'task:123', 'demo'):",
717
+ default: name.toLowerCase(),
718
+ validate: (input) => input.length > 0 || "Channel name is required",
719
+ },
720
+ {
721
+ type: "input",
722
+ name: "stateType",
723
+ message: "State type name (interface name):",
724
+ default: `${name}State`,
725
+ },
726
+ ]);
727
+ return {
728
+ channel: config.channel,
729
+ stateType: config.stateType,
730
+ };
731
+ }
732
+ /**
733
+ * Сбор props компонента
734
+ */
735
+ async function collectProps(name, hasVRP, interactive) {
736
+ if (!interactive) {
737
+ return { props: [], hasProps: true };
738
+ }
739
+ const answer = await inquirer_1.default.prompt([
740
+ {
741
+ type: "confirm",
742
+ name: "hasProps",
743
+ message: "Does this component need props?",
744
+ default: !hasVRP,
745
+ },
746
+ ]);
747
+ if (!answer.hasProps) {
748
+ return { props: [], hasProps: false };
749
+ }
750
+ const props = [];
751
+ let addMore = true;
752
+ while (addMore) {
753
+ const prop = await inquirer_1.default.prompt([
754
+ {
755
+ type: "input",
756
+ name: "name",
757
+ message: "Prop name:",
758
+ validate: (input) => input.length > 0 || "Prop name is required",
759
+ },
760
+ {
761
+ type: "list",
762
+ name: "type",
763
+ message: "Prop type:",
764
+ choices: [
765
+ "string",
766
+ "number",
767
+ "boolean",
768
+ "ReactNode",
769
+ "() => void",
770
+ "(e: Event) => void",
771
+ "string[]",
772
+ "Custom",
773
+ ],
774
+ default: "string",
775
+ },
776
+ {
777
+ type: "input",
778
+ name: "customType",
779
+ message: "Custom type:",
780
+ when: (answers) => answers.type === "Custom",
781
+ },
782
+ {
783
+ type: "confirm",
784
+ name: "required",
785
+ message: "Required prop?",
786
+ default: true,
787
+ },
788
+ ]);
789
+ const propType = prop.type === "Custom" ? prop.customType : prop.type;
790
+ props.push({
791
+ name: prop.name,
792
+ type: propType,
793
+ required: prop.required,
794
+ });
795
+ const { continueAdding } = await inquirer_1.default.prompt([
796
+ {
797
+ type: "confirm",
798
+ name: "continueAdding",
799
+ message: "Add another prop?",
800
+ default: true,
801
+ },
802
+ ]);
803
+ addMore = continueAdding;
804
+ }
805
+ return { props, hasProps: true };
806
+ }
807
+ /**
808
+ * Построение импортов
809
+ */
810
+ function buildImports(vrpConfig, useViraUI) {
811
+ let imports = `import { createElement } from '@vira-ui/core';
812
+ import type { ViraComponentProps } from '@vira-ui/core';`;
813
+ if (vrpConfig) {
814
+ imports += `\nimport { useViraState } from '@vira-ui/react';`;
815
+ }
816
+ if (useViraUI) {
817
+ imports += `\nimport { Container, Stack } from '@vira-ui/ui';`;
818
+ }
819
+ return imports;
820
+ }
821
+ /**
822
+ * Построение интерфейса props
823
+ */
824
+ function buildPropsInterface(name, props) {
825
+ if (props.length === 0) {
826
+ return `export interface ${name}Props extends ViraComponentProps {
827
+ // Add your props here
828
+ }`;
829
+ }
830
+ return `export interface ${name}Props extends ViraComponentProps {
831
+ ${props.map((p) => ` ${p.name}${p.required ? "" : "?"}: ${p.type};`).join("\n")}
832
+ }`;
833
+ }
834
+ /**
835
+ * Построение тела компонента
836
+ */
837
+ function buildComponentBody(name, vrpConfig, props, hasProps, useViraUI) {
838
+ const propsUsage = props.map((p) => ` ${p.name}={props.${p.name}}`).join("\n");
839
+ if (vrpConfig) {
840
+ return ` const { data, sendEvent, sendUpdate, sendDiff } = useViraState<${vrpConfig.stateType}>('${vrpConfig.channel}', null);
841
+
842
+ // Use data from VRP state
843
+ // Example: const value = data?.field ?? defaultValue;
844
+
845
+ // 🎯 4️⃣ Для inline-редактирования с авто-save используйте встроенный watch() с debounce:
846
+ // import { watch, signal } from '@vira-ui/core';
847
+ // const [editValue, setEditValue] = signal('');
848
+ // watch(() => editValue(), (newValue) => {
849
+ // sendDiff({ [field]: newValue, updated_at: new Date().toISOString() });
850
+ // }, { debounce: 500 });
851
+
852
+ return createElement('div', { className: '${name.toLowerCase()}' },
853
+ ${propsUsage ? propsUsage + ",\n" : ""} // Add your content here
854
+ );`;
855
+ }
856
+ if (useViraUI) {
857
+ return ` return (
858
+ <Container>
859
+ <Stack>
860
+ ${propsUsage ? propsUsage.split('\n').map(p => ` ${p.replace(' ', '')}`).join('\n') + "\n" : ""} {/* Add your content here */}
861
+ </Stack>
862
+ </Container>
863
+ );`;
864
+ }
865
+ return ` return createElement('div', { className: '${name.toLowerCase()}' },
866
+ ${propsUsage ? propsUsage + ",\n" : ""} // Add your content here
867
+ );`;
868
+ }
465
869
  /**
466
870
  * Генерация компонента
467
871
  */
468
- async function generateComponent(name, dir) {
872
+ async function generateComponent(name, dir, config) {
469
873
  const componentPath = path.join(process.cwd(), dir, "components", `${name}.tsx`);
470
874
  await fs.ensureDir(path.dirname(componentPath));
471
- const componentCode = `import { createElement } from '@vira-ui/core';
472
- import type { ViraComponentProps } from '@vira-ui/core';
473
-
474
- export interface ${name}Props extends ViraComponentProps {
475
- // Add your props here
875
+ // Сбор конфигурации
876
+ const vrpConfig = await collectVRPConfig(name, config.useVRP, config.interactive);
877
+ const { props, hasProps } = await collectProps(name, !!vrpConfig, config.interactive);
878
+ // Спрашиваем про Vira UI, если интерактивный режим
879
+ let useViraUI = false;
880
+ if (config.interactive) {
881
+ const uiAnswer = await inquirer_1.default.prompt([
882
+ {
883
+ type: "confirm",
884
+ name: "useViraUI",
885
+ message: "Use Vira UI components (@vira-ui/ui)?",
886
+ default: true,
887
+ },
888
+ ]);
889
+ useViraUI = uiAnswer.useViraUI;
890
+ }
891
+ // Построение кода
892
+ const imports = buildImports(vrpConfig, useViraUI);
893
+ const propsInterface = buildPropsInterface(name, props);
894
+ const componentBody = buildComponentBody(name, vrpConfig, props, hasProps, useViraUI);
895
+ // Сборка финального кода
896
+ let componentCode = `${imports}
897
+ `;
898
+ // Интерфейс состояния VRP
899
+ if (vrpConfig) {
900
+ componentCode += `export interface ${vrpConfig.stateType} {
901
+ // Add your state fields here
902
+ id?: string;
476
903
  }
477
904
 
478
- export function ${name}(props: ${name}Props) {
479
- return createElement('div', { className: '${name.toLowerCase()}' },
480
- // Add your content here
481
- );
905
+ `;
906
+ }
907
+ // Интерфейс props
908
+ if (hasProps || !vrpConfig) {
909
+ componentCode += `${propsInterface}
910
+
911
+ `;
912
+ }
913
+ // Сигнатура функции
914
+ const functionSignature = hasProps || !vrpConfig
915
+ ? `export function ${name}(props: ${name}Props) {`
916
+ : `export function ${name}() {`;
917
+ componentCode += `${functionSignature}
918
+ ${componentBody}
482
919
  }
483
920
  `;
484
921
  await fs.writeFile(componentPath, componentCode);
485
922
  }
486
923
  /**
487
- * Генерация сервиса
924
+ * Сбор VRP конфигурации для сервиса
488
925
  */
489
- async function generateService(name, dir) {
490
- const servicePath = path.join(process.cwd(), dir, "services", `${name}Service.ts`);
491
- await fs.ensureDir(path.dirname(servicePath));
492
- const serviceCode = `import { createViraService, signal } from '@vira-ui/core';
926
+ async function collectServiceVRPConfig(name, useVRP, interactive) {
927
+ if (useVRP === false) {
928
+ return null;
929
+ }
930
+ if (!interactive && useVRP !== true) {
931
+ return null;
932
+ }
933
+ // Если явно указано --vrp, используем значения по умолчанию
934
+ if (useVRP === true && !interactive) {
935
+ return {
936
+ channel: name.toLowerCase(),
937
+ stateType: `${name}State`,
938
+ };
939
+ }
940
+ // Интерактивный режим: спрашиваем
941
+ if (interactive && useVRP === undefined) {
942
+ const answer = await inquirer_1.default.prompt([
943
+ {
944
+ type: "confirm",
945
+ name: "useVRP",
946
+ message: "Use Vira Reactive Protocol (VRP) for this service?",
947
+ default: true,
948
+ },
949
+ ]);
950
+ if (!answer.useVRP) {
951
+ return null;
952
+ }
953
+ }
954
+ // Спрашиваем детали VRP
955
+ const config = await inquirer_1.default.prompt([
956
+ {
957
+ type: "input",
958
+ name: "channel",
959
+ message: "VRP channel name:",
960
+ default: name.toLowerCase(),
961
+ },
962
+ {
963
+ type: "input",
964
+ name: "stateType",
965
+ message: "State type name:",
966
+ default: `${name}State`,
967
+ },
968
+ ]);
969
+ return {
970
+ channel: config.channel,
971
+ stateType: config.stateType,
972
+ };
973
+ }
974
+ /**
975
+ * Построение VRP-based сервиса
976
+ */
977
+ function buildVRPService(name, vrpConfig) {
978
+ const lowerName = name.toLowerCase();
979
+ return `// ${name} service using Vira Core DI container + VRP
980
+ import { createService, useService } from '@vira-ui/core';
981
+ import { useViraState } from '@vira-ui/react';
982
+ import { v4 as uuid } from 'uuid';
983
+
984
+ export interface ${vrpConfig.stateType} {
985
+ // Add your state fields here
986
+ id?: string;
987
+ created_at?: string;
988
+ updated_at?: string;
989
+ }
990
+
991
+ // 🎯 3️⃣ Универсальный VRP hook для списков (переиспользуемый для любых сущностей)
992
+ export function useVrpList<T>(channel: string) {
993
+ const { data, sendEvent, sendDiff } = useViraState<T[] | T>(channel, []);
994
+ const list = Array.isArray(data) ? data : Object.values(data || {});
995
+ return { data: list, sendEvent, sendDiff };
996
+ }
997
+
998
+ // Create ${lowerName} service (singleton via DI container)
999
+ // Service holds pure business logic helpers
1000
+ createService('${lowerName}', () => ({
1001
+ // Add your business logic methods here
1002
+ processData(data: ${vrpConfig.stateType} | null): any {
1003
+ if (!data) return null;
1004
+ // Add processing logic
1005
+ return data;
1006
+ },
1007
+ }));
1008
+
1009
+ // Hook for ${name} operations (combines service + VRP state)
1010
+ export function use${name}(id?: string) {
1011
+ const channel = id ? \`${vrpConfig.channel}:\${id}\` : '${vrpConfig.channel}';
1012
+ const { data, sendEvent, sendUpdate, sendDiff } = id
1013
+ ? useViraState<${vrpConfig.stateType}>(channel, null)
1014
+ : useVrpList<${vrpConfig.stateType}>(channel);
1015
+ const ${lowerName}Service = useService<{ processData: (data: ${vrpConfig.stateType} | null) => any }>('${lowerName}');
1016
+
1017
+ return {
1018
+ data,
1019
+ // 🎯 1️⃣ Создание с авто-генерацией UUID на фронте (VRP сразу знает id, не надо ждать бэка)
1020
+ create(item: Omit<${vrpConfig.stateType}, 'id' | 'created_at' | 'updated_at'>) {
1021
+ const id = uuid();
1022
+ const newItem: ${vrpConfig.stateType} = {
1023
+ ...item,
1024
+ id,
1025
+ created_at: new Date().toISOString(),
1026
+ updated_at: new Date().toISOString(),
1027
+ };
1028
+ sendEvent('${lowerName}.created', {
1029
+ ...newItem,
1030
+ timestamp: new Date().toISOString()
1031
+ });
1032
+ },
1033
+ // Update operations
1034
+ update(updates: Partial<${vrpConfig.stateType}>) {
1035
+ sendDiff(updates);
1036
+ sendEvent('${lowerName}.updated', {
1037
+ ...updates,
1038
+ updated_at: new Date().toISOString(),
1039
+ timestamp: new Date().toISOString()
1040
+ });
1041
+ },
1042
+ // Delete operation
1043
+ delete(itemId: string) {
1044
+ sendEvent('${lowerName}.deleted', {
1045
+ id: itemId,
1046
+ timestamp: new Date().toISOString()
1047
+ });
1048
+ },
1049
+ sendEvent,
1050
+ sendDiff,
1051
+ // Service methods
1052
+ processData() {
1053
+ return ${lowerName}Service.processData(data);
1054
+ },
1055
+ };
1056
+ }
1057
+
1058
+ // 🎯 2️⃣ Сервис для bulk actions с VRP (переиспользуемый для любых сущностей)
1059
+ // Используем batch() из @vira-ui/core для оптимизации множественных обновлений
1060
+ import { batch } from '@vira-ui/core';
1061
+
1062
+ createService('${lowerName}Bulk', () => ({
1063
+ bulkUpdate(ids: string[], payload: Partial<${vrpConfig.stateType}>, sendEvent: Function) {
1064
+ // batch() группирует все обновления в один цикл - компоненты обновятся только один раз!
1065
+ batch(() => {
1066
+ ids.forEach(id => {
1067
+ sendEvent('${lowerName}.updated', {
1068
+ id,
1069
+ ...payload,
1070
+ updated_at: new Date().toISOString(),
1071
+ timestamp: new Date().toISOString()
1072
+ });
1073
+ });
1074
+ });
1075
+ },
1076
+ bulkDelete(ids: string[], sendEvent: Function) {
1077
+ // batch() для оптимизации массового удаления
1078
+ batch(() => {
1079
+ ids.forEach(id => {
1080
+ sendEvent('${lowerName}.deleted', {
1081
+ id,
1082
+ timestamp: new Date().toISOString()
1083
+ });
1084
+ });
1085
+ });
1086
+ },
1087
+ }));
1088
+ `;
1089
+ }
1090
+ /**
1091
+ * Построение стандартного сервиса с signals
1092
+ */
1093
+ function buildStandardService(name) {
1094
+ return `import { createViraService, signal } from '@vira-ui/core';
493
1095
 
494
1096
  export const ${name}Service = createViraService('${name.toLowerCase()}', () => {
495
1097
  const data = signal([]);
@@ -517,6 +1119,19 @@ export const ${name}Service = createViraService('${name.toLowerCase()}', () => {
517
1119
  };
518
1120
  });
519
1121
  `;
1122
+ }
1123
+ /**
1124
+ * Генерация сервиса
1125
+ */
1126
+ async function generateService(name, dir, config) {
1127
+ const servicePath = path.join(process.cwd(), dir, "services", `${name}Service.ts`);
1128
+ await fs.ensureDir(path.dirname(servicePath));
1129
+ // Сбор VRP конфигурации
1130
+ const vrpConfig = await collectServiceVRPConfig(name, config.useVRP, config.interactive);
1131
+ // Построение кода
1132
+ const serviceCode = vrpConfig
1133
+ ? buildVRPService(name, vrpConfig)
1134
+ : buildStandardService(name);
520
1135
  await fs.writeFile(servicePath, serviceCode);
521
1136
  }
522
1137
  /**
@@ -525,9 +1140,49 @@ export const ${name}Service = createViraService('${name.toLowerCase()}', () => {
525
1140
  async function generatePage(name, dir) {
526
1141
  const pagePath = path.join(process.cwd(), dir, "pages", `${name}Page.tsx`);
527
1142
  await fs.ensureDir(path.dirname(pagePath));
528
- const pageCode = `import { createElement } from '@vira-ui/core';
1143
+ const pageCode = `import { createService, useService, createElement } from '@vira-ui/core';
1144
+ import { useViraState } from '@vira-ui/react';
1145
+
1146
+ // 🎯 5️⃣ VRP notifications: очередь с максимумом (предотвращает перегрузку фронта)
1147
+ const MAX_NOTIFICATIONS = 5;
1148
+
1149
+ // Создаём сервис для управления состоянием страницы
1150
+ createService('${name.toLowerCase()}Page', () => ({
1151
+ searchQuery: '',
1152
+ selectedIds: new Set<string>(),
1153
+
1154
+ setSearchQuery(value: string) {
1155
+ this.searchQuery = value;
1156
+ },
1157
+
1158
+ toggleSelect(id: string) {
1159
+ if (this.selectedIds.has(id)) {
1160
+ this.selectedIds.delete(id);
1161
+ } else {
1162
+ this.selectedIds.add(id);
1163
+ }
1164
+ },
1165
+
1166
+ clearSelection() {
1167
+ this.selectedIds.clear();
1168
+ },
1169
+ }));
529
1170
 
530
1171
  export function ${name}Page() {
1172
+ // VRP для списка
1173
+ // const { data, create, sendEvent } = use${name}();
1174
+
1175
+ // VRP для real-time уведомлений с лимитом
1176
+ const notificationsState = useViraState<any[]>('notifications:${name.toLowerCase()}', []);
1177
+
1178
+ // Используем сервис страницы
1179
+ const pageService = useService('${name.toLowerCase()}Page');
1180
+
1181
+ // Обработка уведомлений с лимитом очереди
1182
+ if (notificationsState.data && notificationsState.data.length > MAX_NOTIFICATIONS) {
1183
+ notificationsState.data = notificationsState.data.slice(-MAX_NOTIFICATIONS);
1184
+ }
1185
+
531
1186
  return createElement('div', { className: '${name.toLowerCase()}-page' },
532
1187
  createElement('h1', null, '${name}'),
533
1188
  // Add your content here
@@ -539,7 +1194,7 @@ export function ${name}Page() {
539
1194
  /**
540
1195
  * Генерация модели
541
1196
  */
542
- async function generateModel(name, dir) {
1197
+ async function generateModel(name, dir, fields) {
543
1198
  const modelPath = path.join(process.cwd(), dir, "models", `${name}.ts`);
544
1199
  await fs.ensureDir(path.dirname(modelPath));
545
1200
  const modelCode = `import { defineModel } from '@vira-ui/core';
@@ -570,6 +1225,78 @@ export const ${name}Route = reactiveRoute({
570
1225
  `;
571
1226
  await fs.writeFile(routePath, routeCode);
572
1227
  }
1228
+ /**
1229
+ * Генерация теста
1230
+ */
1231
+ async function generateTest(name, dir) {
1232
+ // Определяем тип файла (component, service, page)
1233
+ const componentPath = path.join(process.cwd(), dir, "components", `${name}.tsx`);
1234
+ const servicePath = path.join(process.cwd(), dir, "services", `${name}Service.ts`);
1235
+ const pagePath = path.join(process.cwd(), dir, "pages", `${name}Page.tsx`);
1236
+ let testPath;
1237
+ let testCode;
1238
+ if (await fs.pathExists(componentPath)) {
1239
+ // Тест для компонента
1240
+ testPath = path.join(process.cwd(), dir, "components", `${name}.test.tsx`);
1241
+ testCode = `import React from 'react';
1242
+ import { render } from '@testing-library/react';
1243
+ import { ${name} } from './${name}';
1244
+ import type { ${name}Props } from './${name}';
1245
+
1246
+ describe('${name}', () => {
1247
+ it('renders correctly', () => {
1248
+ const props: ${name}Props = {
1249
+ // Add test props here
1250
+ };
1251
+ const { container } = render(React.createElement(${name}, props));
1252
+ expect(container).toBeTruthy();
1253
+ });
1254
+ });
1255
+ `;
1256
+ }
1257
+ else if (await fs.pathExists(servicePath)) {
1258
+ // Тест для сервиса
1259
+ testPath = path.join(process.cwd(), dir, "services", `${name}Service.test.ts`);
1260
+ testCode = `import { ${name}Service } from './${name}Service';
1261
+
1262
+ describe('${name}Service', () => {
1263
+ it('should be defined', () => {
1264
+ expect(${name}Service).toBeDefined();
1265
+ });
1266
+
1267
+ // Add more tests here
1268
+ });
1269
+ `;
1270
+ }
1271
+ else if (await fs.pathExists(pagePath)) {
1272
+ // Тест для страницы
1273
+ testPath = path.join(process.cwd(), dir, "pages", `${name}Page.test.tsx`);
1274
+ testCode = `import React from 'react';
1275
+ import { render } from '@testing-library/react';
1276
+ import { ${name}Page } from './${name}Page';
1277
+
1278
+ describe('${name}Page', () => {
1279
+ it('renders correctly', () => {
1280
+ const { container } = render(React.createElement(${name}Page));
1281
+ expect(container).toBeTruthy();
1282
+ });
1283
+ });
1284
+ `;
1285
+ }
1286
+ else {
1287
+ // Общий тест, если файл не найден
1288
+ testPath = path.join(process.cwd(), dir, "__tests__", `${name}.test.ts`);
1289
+ await fs.ensureDir(path.dirname(testPath));
1290
+ testCode = `describe('${name}', () => {
1291
+ it('should work', () => {
1292
+ expect(true).toBe(true);
1293
+ });
1294
+ });
1295
+ `;
1296
+ }
1297
+ await fs.ensureDir(path.dirname(testPath));
1298
+ await fs.writeFile(testPath, testCode);
1299
+ }
573
1300
  /**
574
1301
  * Sync TypeScript types from Go structs (scaffold-level parser)
575
1302
  */
@@ -728,6 +1455,50 @@ function toCamel(name) {
728
1455
  return name;
729
1456
  return name.charAt(0).toLowerCase() + name.slice(1);
730
1457
  }
1458
+ /**
1459
+ * Генерация документации по VRP каналам
1460
+ */
1461
+ function generateChannelDocumentation(structs) {
1462
+ const lines = [];
1463
+ lines.push("# Vira Reactive Protocol (VRP) Channels");
1464
+ lines.push("");
1465
+ lines.push("Protocol version: v0.1");
1466
+ lines.push("");
1467
+ lines.push("## Available Channels");
1468
+ lines.push("");
1469
+ lines.push("The following channels are available based on your Go types:");
1470
+ lines.push("");
1471
+ for (const struct of structs) {
1472
+ const channelName = struct.name.toLowerCase();
1473
+ lines.push(`### \`${channelName}\` / \`${channelName}:{id}\``);
1474
+ lines.push("");
1475
+ lines.push(`Type: \`${struct.name}\``);
1476
+ lines.push("");
1477
+ if (struct.fields.length > 0) {
1478
+ lines.push("Fields:");
1479
+ lines.push("");
1480
+ for (const field of struct.fields) {
1481
+ const jsonName = field.json || toCamel(field.name);
1482
+ const tsType = goTypeToTs(field.type);
1483
+ lines.push(`- \`${jsonName}\`: \`${tsType}\``);
1484
+ }
1485
+ lines.push("");
1486
+ }
1487
+ }
1488
+ lines.push("## Usage");
1489
+ lines.push("");
1490
+ lines.push("```typescript");
1491
+ lines.push("import { useViraState } from '@vira-ui/react';");
1492
+ lines.push("");
1493
+ if (structs.length > 0) {
1494
+ const firstStruct = structs[0];
1495
+ lines.push(`const { data, sendEvent, sendUpdate, sendDiff } = useViraState<${firstStruct.name}>('${firstStruct.name.toLowerCase()}', null);`);
1496
+ }
1497
+ lines.push("```");
1498
+ lines.push("");
1499
+ lines.push("See VIRA_PROTOCOL.md for full specification.");
1500
+ return lines.join("\n");
1501
+ }
731
1502
  /**
732
1503
  * Go HTTP handler scaffold
733
1504
  */
@@ -760,21 +1531,42 @@ func ${handlerName}(w http.ResponseWriter, r *http.Request) {
760
1531
  /**
761
1532
  * Go model scaffold
762
1533
  */
763
- async function generateGoModel(name, dir) {
1534
+ async function generateGoModel(name, dir, fields) {
764
1535
  const modelName = capitalize(name);
765
1536
  const targetDir = path.join(process.cwd(), dir);
766
1537
  await fs.ensureDir(targetDir);
1538
+ // Парсим поля если указаны
1539
+ let fieldsCode = "";
1540
+ if (fields) {
1541
+ const fieldList = fields.split(",").map(f => f.trim());
1542
+ fieldsCode = fieldList.map(field => {
1543
+ const [fieldName, fieldType] = field.split(":").map(s => s.trim());
1544
+ const goType = mapTypeScriptToGo(fieldType || "string");
1545
+ return ` ${capitalize(fieldName)} ${goType} \`db:"${fieldName.toLowerCase()}" json:"${fieldName.toLowerCase()}"\``;
1546
+ }).join("\n");
1547
+ // Добавляем стандартные поля если их нет
1548
+ if (!fieldList.some(f => f.toLowerCase().includes("id"))) {
1549
+ fieldsCode = ` ID string \`db:"id" json:"id"\`
1550
+ ${fieldsCode}
1551
+ CreatedAt time.Time \`db:"created_at" json:"created_at"\`
1552
+ UpdatedAt time.Time \`db:"updated_at" json:"updated_at"\``;
1553
+ }
1554
+ }
1555
+ else {
1556
+ // Дефолтные поля для типичной модели
1557
+ fieldsCode = ` ID string \`db:"id" json:"id"\`
1558
+ CreatedAt time.Time \`db:"created_at" json:"created_at"\`
1559
+ UpdatedAt time.Time \`db:"updated_at" json:"updated_at"\``;
1560
+ }
767
1561
  const modelCode = `package models
768
1562
 
769
1563
  import "time"
770
1564
 
771
1565
  type ${modelName} struct {
772
- ID string \`db:"id"\`
773
- CreatedAt time.Time \`db:"created_at"\`
774
- UpdatedAt time.Time \`db:"updated_at"\`
1566
+ ${fieldsCode}
775
1567
  }
776
1568
  `;
777
- await fs.writeFile(path.join(targetDir, `${modelName}.go`), modelCode);
1569
+ await fs.writeFile(path.join(targetDir, `${modelName.toLowerCase()}.go`), modelCode);
778
1570
  }
779
1571
  /**
780
1572
  * SQL migration scaffold (timestamped up/down)
@@ -822,6 +1614,19 @@ function capitalize(value) {
822
1614
  return value;
823
1615
  return value.charAt(0).toUpperCase() + value.slice(1);
824
1616
  }
1617
+ /**
1618
+ * Маппинг TypeScript типов в Go типы
1619
+ */
1620
+ function mapTypeScriptToGo(tsType) {
1621
+ const mapping = {
1622
+ "string": "string",
1623
+ "number": "int",
1624
+ "boolean": "bool",
1625
+ "Date": "time.Time",
1626
+ "date": "time.Time",
1627
+ };
1628
+ return mapping[tsType.toLowerCase()] || "string";
1629
+ }
825
1630
  function toPascal(value) {
826
1631
  return value
827
1632
  .split(/[^a-zA-Z0-9]+/)
@@ -829,3 +1634,429 @@ function toPascal(value) {
829
1634
  .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
830
1635
  .join("");
831
1636
  }
1637
+ /**
1638
+ * Генерация CRUD handlers для ресурса
1639
+ */
1640
+ async function generateCRUDHandler(name, dir, modelName) {
1641
+ const safeName = name.toLowerCase();
1642
+ const handlerName = capitalize(name);
1643
+ const model = modelName || capitalize(name);
1644
+ const targetDir = path.join(process.cwd(), dir);
1645
+ await fs.ensureDir(targetDir);
1646
+ const handlerCode = `package handlers
1647
+
1648
+ import (
1649
+ "encoding/json"
1650
+ "net/http"
1651
+ "strconv"
1652
+ "time"
1653
+ "github.com/gorilla/mux"
1654
+ "github.com/go-playground/validator/v10"
1655
+ "github.com/google/uuid"
1656
+ )
1657
+
1658
+ var validate = validator.New()
1659
+
1660
+ // 🎯 Production-ready: Pagination support
1661
+ type PaginationParams struct {
1662
+ Limit int \`json:"limit"\`
1663
+ Offset int \`json:"offset"\`
1664
+ Cursor string \`json:"cursor,omitempty"\`
1665
+ }
1666
+
1667
+ // 🎯 Production-ready: List response with pagination
1668
+ type ${handlerName}ListResponse struct {
1669
+ Items []${model} \`json:"items"\`
1670
+ Total int \`json:"total"\`
1671
+ Limit int \`json:"limit"\`
1672
+ Offset int \`json:"offset"\`
1673
+ HasMore bool \`json:"has_more"\`
1674
+ }
1675
+
1676
+ // 🎯 Production-ready: Event logging structure
1677
+ type ${handlerName}Event struct {
1678
+ ID string \`json:"id"\`
1679
+ Type string \`json:"type"\` // created, updated, deleted
1680
+ EntityID string \`json:"entity_id"\`
1681
+ UserID string \`json:"user_id,omitempty"\`
1682
+ OldValue *${model} \`json:"old_value,omitempty"\`
1683
+ NewValue *${model} \`json:"new_value,omitempty"\`
1684
+ Timestamp time.Time \`json:"timestamp"\`
1685
+ }
1686
+
1687
+ // 🎯 Production-ready: Batch request/response
1688
+ type BatchUpdateRequest struct {
1689
+ IDs []string \`json:"ids" validate:"required,min=1"\`
1690
+ Payload map[string]interface{} \`json:"payload" validate:"required"\`
1691
+ }
1692
+
1693
+ type BatchDeleteRequest struct {
1694
+ IDs []string \`json:"ids" validate:"required,min=1"\`
1695
+ }
1696
+
1697
+ // List${handlerName} handles GET /${safeName} with pagination
1698
+ func List${handlerName}(w http.ResponseWriter, r *http.Request) {
1699
+ w.Header().Set("Content-Type", "application/json")
1700
+
1701
+ // 🎯 Production-ready: Parse pagination params (limit, offset, cursor)
1702
+ limit := 50 // default
1703
+ offset := 0
1704
+
1705
+ if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
1706
+ if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 && parsed <= 100 {
1707
+ limit = parsed
1708
+ }
1709
+ }
1710
+
1711
+ if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
1712
+ if parsed, err := strconv.Atoi(offsetStr); err == nil && parsed >= 0 {
1713
+ offset = parsed
1714
+ }
1715
+ }
1716
+
1717
+ // TODO: Implement actual DB query with limit/offset
1718
+ // items, total, err := db.List${handlerName}(limit, offset)
1719
+ // if err != nil {
1720
+ // http.Error(w, err.Error(), http.StatusInternalServerError)
1721
+ // return
1722
+ // }
1723
+
1724
+ // 🎯 Production-ready: Redis caching (commented for now - implement with your cache layer)
1725
+ // cacheKey := fmt.Sprintf("${safeName}:list:limit=%d:offset=%d", limit, offset)
1726
+ // if cached := redis.Get(cacheKey); cached != nil {
1727
+ // json.NewEncoder(w).Encode(cached)
1728
+ // return
1729
+ // }
1730
+
1731
+ response := ${handlerName}ListResponse{
1732
+ Items: []${model}{},
1733
+ Total: 0,
1734
+ Limit: limit,
1735
+ Offset: offset,
1736
+ HasMore: false,
1737
+ }
1738
+
1739
+ // 🎯 Production-ready: Cache response (TTL 30s for lists)
1740
+ // redis.Set(cacheKey, response, 30*time.Second)
1741
+
1742
+ json.NewEncoder(w).Encode(response)
1743
+ }
1744
+
1745
+ // Get${handlerName} handles GET /${safeName}/{id}
1746
+ func Get${handlerName}(w http.ResponseWriter, r *http.Request) {
1747
+ vars := mux.Vars(r)
1748
+ id := vars["id"]
1749
+
1750
+ w.Header().Set("Content-Type", "application/json")
1751
+
1752
+ // 🎯 Production-ready: Redis caching for detail views
1753
+ // cacheKey := fmt.Sprintf("${safeName}:detail:%s", id)
1754
+ // if cached := redis.Get(cacheKey); cached != nil {
1755
+ // json.NewEncoder(w).Encode(cached)
1756
+ // return
1757
+ // }
1758
+
1759
+ // TODO: Implement actual DB query
1760
+ // item, err := db.Get${handlerName}(id)
1761
+ // if err != nil {
1762
+ // http.Error(w, err.Error(), http.StatusNotFound)
1763
+ // return
1764
+ // }
1765
+
1766
+ item := ${model}{ID: id}
1767
+
1768
+ // 🎯 Production-ready: Cache detail (TTL 5min)
1769
+ // redis.Set(cacheKey, item, 5*time.Minute)
1770
+
1771
+ json.NewEncoder(w).Encode(item)
1772
+ }
1773
+
1774
+ // Create${handlerName} handles POST /${safeName}
1775
+ func Create${handlerName}(w http.ResponseWriter, r *http.Request) {
1776
+ var input ${model}
1777
+
1778
+ if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
1779
+ http.Error(w, "Invalid JSON", http.StatusBadRequest)
1780
+ return
1781
+ }
1782
+
1783
+ // 🎯 Production-ready: Validation (email, phone, unique fields)
1784
+ if err := validate.Struct(input); err != nil {
1785
+ w.Header().Set("Content-Type", "application/json")
1786
+ w.WriteHeader(http.StatusBadRequest)
1787
+ json.NewEncoder(w).Encode(map[string]string{
1788
+ "error": "Validation failed",
1789
+ "details": err.Error(),
1790
+ })
1791
+ return
1792
+ }
1793
+
1794
+ // Generate UUID if not provided
1795
+ if input.ID == "" {
1796
+ input.ID = uuid.New().String()
1797
+ }
1798
+
1799
+ input.CreatedAt = time.Now()
1800
+ input.UpdatedAt = time.Now()
1801
+
1802
+ // TODO: Implement actual DB insert
1803
+ // if err := db.Create${handlerName}(&input); err != nil {
1804
+ // http.Error(w, err.Error(), http.StatusInternalServerError)
1805
+ // return
1806
+ // }
1807
+
1808
+ // 🎯 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
+ }
1816
+ // logEvent(event) // Implement event logging to client_events table
1817
+
1818
+ // 🎯 Production-ready: Invalidate cache
1819
+ // redis.Del("${safeName}:list:*")
1820
+
1821
+ // 🎯 Production-ready: Emit VRP event (batchEmit for large lists)
1822
+ // vrp.BatchEmit("${safeName}", "created", input)
1823
+
1824
+ w.Header().Set("Content-Type", "application/json")
1825
+ w.WriteHeader(http.StatusCreated)
1826
+ json.NewEncoder(w).Encode(input)
1827
+ }
1828
+
1829
+ // Update${handlerName} handles PUT /${safeName}/{id}
1830
+ func Update${handlerName}(w http.ResponseWriter, r *http.Request) {
1831
+ vars := mux.Vars(r)
1832
+ id := vars["id"]
1833
+
1834
+ var input ${model}
1835
+ if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
1836
+ http.Error(w, "Invalid JSON", http.StatusBadRequest)
1837
+ return
1838
+ }
1839
+
1840
+ // 🎯 Production-ready: Get old value for event logging
1841
+ // oldValue, _ := db.Get${handlerName}(id)
1842
+
1843
+ // 🎯 Production-ready: Validation
1844
+ if err := validate.Struct(input); err != nil {
1845
+ w.Header().Set("Content-Type", "application/json")
1846
+ w.WriteHeader(http.StatusBadRequest)
1847
+ json.NewEncoder(w).Encode(map[string]string{
1848
+ "error": "Validation failed",
1849
+ "details": err.Error(),
1850
+ })
1851
+ return
1852
+ }
1853
+
1854
+ input.ID = id
1855
+ input.UpdatedAt = time.Now()
1856
+
1857
+ // TODO: Implement actual DB update
1858
+ // if err := db.Update${handlerName}(&input); err != nil {
1859
+ // http.Error(w, err.Error(), http.StatusNotFound)
1860
+ // return
1861
+ // }
1862
+
1863
+ // 🎯 Production-ready: Log event with old/new values
1864
+ // event := ${handlerName}Event{
1865
+ // ID: uuid.New().String(),
1866
+ // Type: "updated",
1867
+ // EntityID: id,
1868
+ // OldValue: oldValue,
1869
+ // NewValue: &input,
1870
+ // Timestamp: time.Now(),
1871
+ // }
1872
+ // logEvent(event)
1873
+
1874
+ // 🎯 Production-ready: Invalidate cache
1875
+ // redis.Del(fmt.Sprintf("${safeName}:detail:%s", id), "${safeName}:list:*")
1876
+
1877
+ // 🎯 Production-ready: Emit VRP diff for real-time updates
1878
+ // vrp.SendDiff("${safeName}:" + id, input)
1879
+
1880
+ w.Header().Set("Content-Type", "application/json")
1881
+ json.NewEncoder(w).Encode(input)
1882
+ }
1883
+
1884
+ // Delete${handlerName} handles DELETE /${safeName}/{id}
1885
+ func Delete${handlerName}(w http.ResponseWriter, r *http.Request) {
1886
+ vars := mux.Vars(r)
1887
+ id := vars["id"]
1888
+
1889
+ // 🎯 Production-ready: Get value for event logging
1890
+ // oldValue, _ := db.Get${handlerName}(id)
1891
+
1892
+ // TODO: Implement actual DB delete
1893
+ // if err := db.Delete${handlerName}(id); err != nil {
1894
+ // http.Error(w, err.Error(), http.StatusNotFound)
1895
+ // return
1896
+ // }
1897
+
1898
+ // 🎯 Production-ready: Log deletion event
1899
+ // event := ${handlerName}Event{
1900
+ // ID: uuid.New().String(),
1901
+ // Type: "deleted",
1902
+ // EntityID: id,
1903
+ // OldValue: oldValue,
1904
+ // Timestamp: time.Now(),
1905
+ // }
1906
+ // logEvent(event)
1907
+
1908
+ // 🎯 Production-ready: Invalidate cache
1909
+ // redis.Del(fmt.Sprintf("${safeName}:detail:%s", id), "${safeName}:list:*")
1910
+
1911
+ // 🎯 Production-ready: Emit VRP event
1912
+ // vrp.SendEvent("${safeName}", "deleted", map[string]string{"id": id})
1913
+
1914
+ w.WriteHeader(http.StatusNoContent)
1915
+ }
1916
+
1917
+ // 🎯 Production-ready: BatchUpdate handles POST /${safeName}/batch/update
1918
+ func BatchUpdate${handlerName}(w http.ResponseWriter, r *http.Request) {
1919
+ var req BatchUpdateRequest
1920
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1921
+ http.Error(w, "Invalid JSON", http.StatusBadRequest)
1922
+ return
1923
+ }
1924
+
1925
+ if err := validate.Struct(req); err != nil {
1926
+ w.Header().Set("Content-Type", "application/json")
1927
+ w.WriteHeader(http.StatusBadRequest)
1928
+ json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
1929
+ return
1930
+ }
1931
+
1932
+ // TODO: Implement batch update in transaction
1933
+ // tx := db.Begin()
1934
+ // for _, id := range req.IDs {
1935
+ // if err := tx.Update${handlerName}(id, req.Payload); err != nil {
1936
+ // tx.Rollback()
1937
+ // http.Error(w, err.Error(), http.StatusInternalServerError)
1938
+ // return
1939
+ // }
1940
+ // }
1941
+ // tx.Commit()
1942
+
1943
+ // 🎯 Production-ready: Batch emit VRP events
1944
+ // vrp.BatchEmit("${safeName}", "updated", req)
1945
+
1946
+ // Invalidate cache
1947
+ // redis.Del("${safeName}:*")
1948
+
1949
+ w.Header().Set("Content-Type", "application/json")
1950
+ json.NewEncoder(w).Encode(map[string]interface{}{
1951
+ "updated": len(req.IDs),
1952
+ "ids": req.IDs,
1953
+ })
1954
+ }
1955
+
1956
+ // 🎯 Production-ready: BatchDelete handles POST /${safeName}/batch/delete
1957
+ func BatchDelete${handlerName}(w http.ResponseWriter, r *http.Request) {
1958
+ var req BatchDeleteRequest
1959
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1960
+ http.Error(w, "Invalid JSON", http.StatusBadRequest)
1961
+ return
1962
+ }
1963
+
1964
+ if err := validate.Struct(req); err != nil {
1965
+ w.Header().Set("Content-Type", "application/json")
1966
+ w.WriteHeader(http.StatusBadRequest)
1967
+ json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
1968
+ return
1969
+ }
1970
+
1971
+ // TODO: Implement batch delete in transaction
1972
+ // tx := db.Begin()
1973
+ // for _, id := range req.IDs {
1974
+ // if err := tx.Delete${handlerName}(id); err != nil {
1975
+ // tx.Rollback()
1976
+ // http.Error(w, err.Error(), http.StatusInternalServerError)
1977
+ // return
1978
+ // }
1979
+ // }
1980
+ // tx.Commit()
1981
+
1982
+ // 🎯 Production-ready: Batch emit VRP events
1983
+ // vrp.BatchEmit("${safeName}", "deleted", req)
1984
+
1985
+ // Invalidate cache
1986
+ // redis.Del("${safeName}:*")
1987
+
1988
+ w.WriteHeader(http.StatusNoContent)
1989
+ }
1990
+ `;
1991
+ await fs.writeFile(path.join(targetDir, `${safeName}_crud.go`), handlerCode);
1992
+ }
1993
+ /**
1994
+ * Валидация проекта
1995
+ */
1996
+ async function validateProject() {
1997
+ const cwd = process.cwd();
1998
+ const errors = [];
1999
+ const warnings = [];
2000
+ console.log(chalk_1.default.blue("\nValidating Vira project...\n"));
2001
+ // Проверка структуры frontend проекта
2002
+ const frontendPath = path.join(cwd, "frontend");
2003
+ const frontendExists = await fs.pathExists(frontendPath);
2004
+ if (frontendExists) {
2005
+ const requiredFiles = [
2006
+ "package.json",
2007
+ "vite.config.ts",
2008
+ "tsconfig.json",
2009
+ "src/main.tsx",
2010
+ ];
2011
+ for (const file of requiredFiles) {
2012
+ const filePath = path.join(frontendPath, file);
2013
+ if (!(await fs.pathExists(filePath))) {
2014
+ errors.push(`Missing frontend file: ${file}`);
2015
+ }
2016
+ }
2017
+ // Проверка структуры директорий
2018
+ const requiredDirs = ["src/components", "src/services", "src/pages"];
2019
+ for (const dir of requiredDirs) {
2020
+ const dirPath = path.join(frontendPath, dir);
2021
+ if (!(await fs.pathExists(dirPath))) {
2022
+ warnings.push(`Missing frontend directory: ${dir}`);
2023
+ }
2024
+ }
2025
+ }
2026
+ else {
2027
+ // Проверка standalone frontend
2028
+ if (!(await fs.pathExists(path.join(cwd, "package.json")))) {
2029
+ errors.push("Missing package.json");
2030
+ }
2031
+ }
2032
+ // Проверка структуры backend проекта
2033
+ const backendPath = path.join(cwd, "backend");
2034
+ if (await fs.pathExists(backendPath)) {
2035
+ const requiredFiles = ["go.mod", "cmd/api/main.go"];
2036
+ for (const file of requiredFiles) {
2037
+ const filePath = path.join(backendPath, file);
2038
+ if (!(await fs.pathExists(filePath))) {
2039
+ errors.push(`Missing backend file: ${file}`);
2040
+ }
2041
+ }
2042
+ }
2043
+ // Вывод результатов
2044
+ if (errors.length > 0) {
2045
+ console.log(chalk_1.default.red("✗ Errors found:"));
2046
+ errors.forEach((err) => console.log(chalk_1.default.red(` - ${err}`)));
2047
+ }
2048
+ if (warnings.length > 0) {
2049
+ console.log(chalk_1.default.yellow("\n⚠ Warnings:"));
2050
+ warnings.forEach((warn) => console.log(chalk_1.default.yellow(` - ${warn}`)));
2051
+ }
2052
+ if (errors.length === 0 && warnings.length === 0) {
2053
+ console.log(chalk_1.default.green("✓ Project structure is valid!"));
2054
+ }
2055
+ else if (errors.length === 0) {
2056
+ console.log(chalk_1.default.green("\n✓ Project structure is valid (with warnings)"));
2057
+ }
2058
+ else {
2059
+ console.log(chalk_1.default.red("\n✗ Project validation failed"));
2060
+ process.exit(1);
2061
+ }
2062
+ }