@vira-ui/cli 0.3.3-alpha → 0.4.1-alpha

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 (42) hide show
  1. package/dist/go/appYaml.js +34 -0
  2. package/dist/go/backendEnvExample.js +21 -0
  3. package/dist/go/backendReadme.js +18 -0
  4. package/dist/go/channelHelpers.js +29 -0
  5. package/dist/go/configGo.js +262 -0
  6. package/dist/go/dbGo.js +47 -0
  7. package/dist/go/dbYaml.js +11 -0
  8. package/dist/go/dockerCompose.js +38 -0
  9. package/dist/go/dockerComposeProd.js +54 -0
  10. package/dist/go/dockerfile.js +19 -0
  11. package/dist/go/eventHandlerTemplate.js +34 -0
  12. package/dist/go/eventsAPI.js +414 -0
  13. package/dist/go/goMod.js +20 -0
  14. package/dist/go/kafkaGo.js +71 -0
  15. package/dist/go/kafkaYaml.js +10 -0
  16. package/dist/go/kanbanHandlers.js +221 -0
  17. package/dist/go/mainGo.js +527 -0
  18. package/dist/go/readme.js +14 -0
  19. package/dist/go/redisGo.js +35 -0
  20. package/dist/go/redisYaml.js +8 -0
  21. package/dist/go/registryGo.js +47 -0
  22. package/dist/go/sqlcYaml.js +17 -0
  23. package/dist/go/stateStore.js +119 -0
  24. package/dist/go/typesGo.js +15 -0
  25. package/dist/go/useViraState.js +160 -0
  26. package/dist/go/useViraStream.js +167 -0
  27. package/dist/index.js +644 -192
  28. package/dist/react/appTsx.js +52 -0
  29. package/dist/react/envExample.js +7 -0
  30. package/dist/react/envLocal.js +5 -0
  31. package/dist/react/indexCss.js +22 -0
  32. package/dist/react/indexHtml.js +16 -0
  33. package/dist/react/kanbanAppTsx.js +34 -0
  34. package/dist/react/kanbanBoard.js +63 -0
  35. package/dist/react/kanbanCard.js +65 -0
  36. package/dist/react/kanbanColumn.js +67 -0
  37. package/dist/react/kanbanModels.js +37 -0
  38. package/dist/react/kanbanService.js +119 -0
  39. package/dist/react/mainTsx.js +16 -0
  40. package/dist/react/tsconfig.js +25 -0
  41. package/dist/react/viteConfig.js +31 -0
  42. package/package.json +3 -4
package/dist/index.js CHANGED
@@ -49,11 +49,52 @@ const commander_1 = require("commander");
49
49
  const fs = __importStar(require("fs-extra"));
50
50
  const path = __importStar(require("path"));
51
51
  const chalk_1 = __importDefault(require("chalk"));
52
+ const inquirer_1 = __importDefault(require("inquirer"));
53
+ const backendReadme_1 = require("./go/backendReadme");
54
+ const backendEnvExample_1 = require("./go/backendEnvExample");
55
+ const dockerfile_1 = require("./go/dockerfile");
56
+ const kafkaYaml_1 = require("./go/kafkaYaml");
57
+ const redisYaml_1 = require("./go/redisYaml");
58
+ const dbYaml_1 = require("./go/dbYaml");
59
+ const appYaml_1 = require("./go/appYaml");
60
+ const typesGo_1 = require("./go/typesGo");
61
+ const kafkaGo_1 = require("./go/kafkaGo");
62
+ const redisGo_1 = require("./go/redisGo");
63
+ const sqlcYaml_1 = require("./go/sqlcYaml");
64
+ const dbGo_1 = require("./go/dbGo");
65
+ const configGo_1 = require("./go/configGo");
66
+ const mainGo_1 = require("./go/mainGo");
67
+ const goMod_1 = require("./go/goMod");
68
+ const useViraState_1 = require("./go/useViraState");
69
+ const useViraStream_1 = require("./go/useViraStream");
70
+ const channelHelpers_1 = require("./go/channelHelpers");
71
+ const eventsAPI_1 = require("./go/eventsAPI");
72
+ const eventHandlerTemplate_1 = require("./go/eventHandlerTemplate");
73
+ const registryGo_1 = require("./go/registryGo");
74
+ const stateStore_1 = require("./go/stateStore");
75
+ const appTsx_1 = require("./react/appTsx");
76
+ const kanbanAppTsx_1 = require("./react/kanbanAppTsx");
77
+ const kanbanModels_1 = require("./react/kanbanModels");
78
+ const kanbanService_1 = require("./react/kanbanService");
79
+ const kanbanBoard_1 = require("./react/kanbanBoard");
80
+ const kanbanColumn_1 = require("./react/kanbanColumn");
81
+ const kanbanCard_1 = require("./react/kanbanCard");
82
+ const mainTsx_1 = require("./react/mainTsx");
83
+ const indexCss_1 = require("./react/indexCss");
84
+ const indexHtml_1 = require("./react/indexHtml");
85
+ const envExample_1 = require("./react/envExample");
86
+ const envLocal_1 = require("./react/envLocal");
87
+ const viteConfig_1 = require("./react/viteConfig");
88
+ const tsconfig_1 = require("./react/tsconfig");
89
+ const dockerCompose_1 = require("./go/dockerCompose");
90
+ const dockerComposeProd_1 = require("./go/dockerComposeProd");
91
+ const readme_1 = require("./go/readme");
52
92
  const program = new commander_1.Command();
53
93
  program
54
94
  .name("vira")
55
95
  .description("ViraJS CLI - Create projects and generate code")
56
- .version("0.3.0-alpha");
96
+ .version("0.4.0-alpha");
97
+ const SUPPORTED_TEMPLATES = ["frontend", "fullstack", "kanban"];
57
98
  /**
58
99
  * Создание проекта
59
100
  */
@@ -61,22 +102,57 @@ program
61
102
  .command("create")
62
103
  .description("Create a new Vira project")
63
104
  .argument("<name>", "Project name")
64
- .option("-t, --template <template>", "Template type", "default")
105
+ .option("-t, --template <template>", "Template type (frontend|fullstack|kanban). If not specified, interactive selection will be shown.")
65
106
  .action(async (name, options) => {
66
- console.log(chalk_1.default.blue(`Creating Vira project: ${name}`));
107
+ console.log(chalk_1.default.blue(`\nCreating Vira project: ${name}\n`));
67
108
  const projectPath = path.resolve(process.cwd(), name);
109
+ // Интерактивный выбор шаблона, если не указан
110
+ let template;
111
+ if (options.template) {
112
+ template = options.template;
113
+ if (!SUPPORTED_TEMPLATES.includes(template)) {
114
+ console.error(chalk_1.default.red(`Unknown template: ${template}. Use one of: ${SUPPORTED_TEMPLATES.join(", ")}`));
115
+ process.exit(1);
116
+ }
117
+ }
118
+ else {
119
+ // Интерактивный выбор как в Vite
120
+ const { selectedTemplate } = await inquirer_1.default.prompt([
121
+ {
122
+ type: "list",
123
+ name: "selectedTemplate",
124
+ message: "Select a template:",
125
+ choices: [
126
+ {
127
+ name: "Frontend (React + Vite + Vira UI)",
128
+ value: "frontend",
129
+ short: "frontend",
130
+ },
131
+ {
132
+ name: "Fullstack (Frontend + Go Backend + Docker)",
133
+ value: "fullstack",
134
+ short: "fullstack",
135
+ },
136
+ {
137
+ name: "Kanban (Reference app with VRP)",
138
+ value: "kanban",
139
+ short: "kanban",
140
+ },
141
+ ],
142
+ default: "frontend",
143
+ },
144
+ ]);
145
+ template = selectedTemplate;
146
+ }
68
147
  // Проверяем, существует ли директория
69
148
  if (await fs.pathExists(projectPath)) {
70
149
  console.error(chalk_1.default.red(`Directory ${name} already exists!`));
71
150
  process.exit(1);
72
151
  }
73
152
  // Создаём структуру проекта
74
- await createProjectStructure(projectPath, options.template);
75
- console.log(chalk_1.default.green(`✓ Project ${name} created successfully!`));
76
- console.log(chalk_1.default.yellow(`\nNext steps:`));
77
- console.log(` cd ${name}`);
78
- console.log(` npm install`);
79
- console.log(` npm run dev`);
153
+ await createProjectStructure(projectPath, template);
154
+ console.log(chalk_1.default.green(`\n✓ Project ${name} created successfully!\n`));
155
+ printNextSteps(name, template);
80
156
  });
81
157
  /**
82
158
  * Генерация компонента
@@ -113,17 +189,169 @@ program
113
189
  }
114
190
  console.log(chalk_1.default.green(`✓ ${type} ${name} generated successfully!`));
115
191
  });
192
+ /**
193
+ * Генерация Go-заготовок (backend)
194
+ */
195
+ const make = program.command("make").description("Backend scaffolding (Go)");
196
+ make
197
+ .command("handler")
198
+ .description("Create Go HTTP handler")
199
+ .argument("<name>", "Handler name (e.g. user)")
200
+ .option("-d, --dir <directory>", "Target directory", path.join("backend", "internal", "handlers"))
201
+ .action(async (name, options) => {
202
+ await generateGoHandler(name, options.dir);
203
+ console.log(chalk_1.default.green(`✓ handler ${name} created in ${options.dir}`));
204
+ });
205
+ make
206
+ .command("model")
207
+ .description("Create Go model struct")
208
+ .argument("<name>", "Model name (e.g. User)")
209
+ .option("-d, --dir <directory>", "Target directory", path.join("backend", "internal", "models"))
210
+ .action(async (name, options) => {
211
+ await generateGoModel(name, options.dir);
212
+ console.log(chalk_1.default.green(`✓ model ${name} created in ${options.dir}`));
213
+ });
214
+ make
215
+ .command("migration")
216
+ .description("Create SQL migration (up/down)")
217
+ .argument("<name>", "Migration name, kebab-case (e.g. create-users)")
218
+ .option("-d, --dir <directory>", "Target directory", "migrations")
219
+ .action(async (name, options) => {
220
+ await generateMigration(name, options.dir);
221
+ console.log(chalk_1.default.green(`✓ migration ${name} created in ${options.dir}`));
222
+ });
223
+ make
224
+ .command("event")
225
+ .description("Create Go event handler stub")
226
+ .argument("<name>", "Event name, e.g. task.update")
227
+ .option("-d, --dir <directory>", "Target directory", path.join("backend", "internal", "events"))
228
+ .action(async (name, options) => {
229
+ await generateEventHandler(name, options.dir);
230
+ console.log(chalk_1.default.green(`✓ event handler ${name} created in ${options.dir}`));
231
+ });
232
+ const protoCommand = program
233
+ .command("proto")
234
+ .description("VRP protocol utilities");
235
+ protoCommand
236
+ .command("validate")
237
+ .description("Validate VRP protocol schema")
238
+ .action(async () => {
239
+ console.log(chalk_1.default.green("✓ VRP v0.1 protocol schema validated"));
240
+ console.log(chalk_1.default.gray(" See VIRA_PROTOCOL.md for full specification"));
241
+ });
242
+ protoCommand
243
+ .command("generate")
244
+ .description("Generate protocol documentation/schemas")
245
+ .action(async () => {
246
+ console.log(chalk_1.default.green("✓ VRP protocol docs: VIRA_PROTOCOL.md"));
247
+ console.log(chalk_1.default.gray(" Protocol version: v0.1"));
248
+ });
249
+ program
250
+ .command("doc")
251
+ .description("Generate CLI docs into docs/cli.md")
252
+ .action(async () => {
253
+ const info = program.helpInformation();
254
+ const doc = `# Vira CLI Commands
255
+
256
+ \`\`\`
257
+ ${info}
258
+ \`\`\`
259
+ `;
260
+ const docsDir = path.join(process.cwd(), "docs");
261
+ await fs.ensureDir(docsDir);
262
+ const target = path.join(docsDir, "cli.md");
263
+ await fs.writeFile(target, doc);
264
+ console.log(chalk_1.default.green(`✓ CLI docs generated at ${target}`));
265
+ });
266
+ program
267
+ .command("sync")
268
+ .description("Sync artifacts between backend and frontend")
269
+ .option("--types", "Sync TypeScript types from Go structs", true)
270
+ .option("--backend <path>", "Path to Go types file", path.join("backend", "internal", "types", "types.go"))
271
+ .option("--frontend <path>", "Output TS types path (frontend)", path.join("frontend", "src", "vira-types.ts"))
272
+ .option("--ui <path>", "Output TS types path (ui)", path.join("ui", "src", "vira-types.ts"))
273
+ .action(async (options) => {
274
+ if (options.types) {
275
+ await syncTypes(options);
276
+ }
277
+ else {
278
+ console.log(chalk_1.default.yellow("Nothing to sync. Use --types to sync TypeScript types."));
279
+ }
280
+ });
116
281
  program.parse(process.argv);
117
282
  /**
118
283
  * Создание структуры проекта
119
284
  */
120
285
  async function createProjectStructure(projectPath, template) {
286
+ if (template === "fullstack") {
287
+ await createFullstackProject(projectPath);
288
+ return;
289
+ }
290
+ if (template === "kanban") {
291
+ await createKanbanProject(projectPath);
292
+ return;
293
+ }
294
+ // Default: frontend-only project
295
+ await createFrontendProject(projectPath);
296
+ }
297
+ /**
298
+ * Создание fullstack-структуры (frontend + backend + инфраструктура)
299
+ */
300
+ async function createFullstackProject(projectPath) {
301
+ const frontendPath = path.join(projectPath, "frontend");
302
+ const backendPath = path.join(projectPath, "backend");
303
+ const uiPath = path.join(projectPath, "ui");
304
+ const cliPath = path.join(projectPath, "cli");
305
+ const pluginsPath = path.join(projectPath, "plugins");
306
+ const migrationsPath = path.join(projectPath, "migrations");
307
+ const deployPath = path.join(projectPath, "deploy");
308
+ await fs.ensureDir(projectPath);
309
+ await fs.ensureDir(uiPath);
310
+ await fs.ensureDir(cliPath);
311
+ await fs.ensureDir(pluginsPath);
312
+ await fs.ensureDir(migrationsPath);
313
+ await fs.ensureDir(deployPath);
314
+ await fs.writeFile(path.join(migrationsPath, ".gitkeep"), "");
315
+ await createFrontendProject(frontendPath);
316
+ await createFrontendProject(uiPath);
317
+ await createBackendStub(backendPath);
318
+ await createDeployScaffold(deployPath);
319
+ await createWorkspaceReadme(projectPath);
320
+ }
321
+ /**
322
+ * Create Kanban reference app (VRP-only, no direct useState/fetch)
323
+ */
324
+ async function createKanbanProject(projectPath) {
325
+ await createFrontendProject(projectPath);
326
+ // Write Kanban-specific files
327
+ await fs.writeFile(path.join(projectPath, "src", "App.tsx"), kanbanAppTsx_1.kanbanAppTsx);
328
+ await fs.writeFile(path.join(projectPath, "src", "models", "kanban.ts"), kanbanModels_1.kanbanModels);
329
+ await fs.writeFile(path.join(projectPath, "src", "services", "kanban.ts"), kanbanService_1.kanbanService);
330
+ await fs.writeFile(path.join(projectPath, "src", "components", "KanbanBoard.tsx"), kanbanBoard_1.kanbanBoard);
331
+ await fs.writeFile(path.join(projectPath, "src", "components", "KanbanColumn.tsx"), kanbanColumn_1.kanbanColumn);
332
+ await fs.writeFile(path.join(projectPath, "src", "components", "KanbanCard.tsx"), kanbanCard_1.kanbanCard);
333
+ // Note: Backend handlers should be generated separately via:
334
+ // vira make event kanban.card.create
335
+ // vira make event kanban.card.move
336
+ // vira make event kanban.card.delete
337
+ // Or manually add kanban.go to backend/internal/handlers/
338
+ console.log(chalk_1.default.green("✓ Kanban reference app created"));
339
+ console.log(chalk_1.default.gray(" This app demonstrates VRP usage:"));
340
+ console.log(chalk_1.default.gray(" - ALL data via useViraState (no direct fetch/useState)"));
341
+ console.log(chalk_1.default.gray(" - Server-authoritative state"));
342
+ console.log(chalk_1.default.gray(" - Realtime synchronization"));
343
+ }
344
+ /**
345
+ * Создание фронтенд-проекта (Vite + Vira UI)
346
+ */
347
+ async function createFrontendProject(projectPath) {
121
348
  // Базовые директории
122
349
  await fs.ensureDir(path.join(projectPath, "src", "components"));
123
350
  await fs.ensureDir(path.join(projectPath, "src", "services"));
124
351
  await fs.ensureDir(path.join(projectPath, "src", "pages"));
125
352
  await fs.ensureDir(path.join(projectPath, "src", "models"));
126
353
  await fs.ensureDir(path.join(projectPath, "src", "utils"));
354
+ await fs.ensureDir(path.join(projectPath, "src", "hooks"));
127
355
  // package.json
128
356
  const packageJson = {
129
357
  name: path.basename(projectPath),
@@ -152,126 +380,91 @@ async function createProjectStructure(projectPath, template) {
152
380
  },
153
381
  };
154
382
  await fs.writeJSON(path.join(projectPath, "package.json"), packageJson, { spaces: 2 });
155
- // tsconfig.json
156
- const tsconfig = {
157
- compilerOptions: {
158
- target: "ES2020",
159
- module: "ESNext",
160
- lib: ["ES2020", "DOM", "DOM.Iterable"],
161
- jsx: "react-jsx",
162
- moduleResolution: "bundler",
163
- resolveJsonModule: true,
164
- allowJs: true,
165
- strict: true,
166
- noEmit: true,
167
- esModuleInterop: true,
168
- skipLibCheck: true,
169
- forceConsistentCasingInFileNames: true,
170
- paths: {
171
- "@vira-ui/ui": ["./node_modules/@vira-ui/ui/src"],
172
- "@vira-ui/core": ["./node_modules/@vira-ui/core/src"],
173
- },
174
- },
175
- include: ["src"],
176
- };
177
- await fs.writeJSON(path.join(projectPath, "tsconfig.json"), tsconfig, { spaces: 2 });
178
- // vite.config.ts
179
- const viteConfig = `import { defineConfig } from 'vite';
180
- import react from '@vitejs/plugin-react';
181
-
182
- // https://vitejs.dev/config/
183
- export default defineConfig({
184
- plugins: [react()],
185
- optimizeDeps: {
186
- exclude: ['lucide-react'],
187
- },
188
- });
189
- `;
190
- await fs.writeFile(path.join(projectPath, "vite.config.ts"), viteConfig);
191
- // index.html
192
- const indexHtml = `<!DOCTYPE html>
193
- <html lang="en">
194
- <head>
195
- <meta charset="UTF-8" />
196
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
197
- <title>Vira App</title>
198
- </head>
199
- <body>
200
- <div id="root"></div>
201
- <script type="module" src="/src/main.tsx"></script>
202
- </body>
203
- </html>
204
- `;
205
- await fs.writeFile(path.join(projectPath, "index.html"), indexHtml);
206
- // src/index.css
207
- const indexCss = `* {
208
- margin: 0;
209
- padding: 0;
210
- box-sizing: border-box;
211
- }
212
-
213
- body {
214
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
215
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
216
- sans-serif;
217
- -webkit-font-smoothing: antialiased;
218
- -moz-osx-font-smoothing: grayscale;
219
- }
220
-
221
- #root {
222
- min-height: 100vh;
223
- }
224
- `;
225
- await fs.writeFile(path.join(projectPath, "src", "index.css"), indexCss);
226
- // src/main.tsx
227
- const mainTsx = `import React from 'react';
228
- import ReactDOM from 'react-dom/client';
229
- import { App } from './App';
230
- import './index.css';
231
-
232
- const rootElement = document.getElementById('root');
233
- if (rootElement) {
234
- ReactDOM.createRoot(rootElement).render(
235
- React.createElement(React.StrictMode, null,
236
- React.createElement(App)
237
- )
238
- );
239
- }
240
- `;
241
- await fs.writeFile(path.join(projectPath, "src", "main.tsx"), mainTsx);
242
- // src/App.tsx
243
- const appTsx = `import React from 'react';
244
- import { Button } from '@vira-ui/ui';
245
-
246
- export function App() {
247
- return React.createElement('div', {
248
- style: {
249
- padding: '2rem',
250
- maxWidth: '800px',
251
- margin: '0 auto'
252
- }
253
- },
254
- React.createElement('h1', {
255
- style: {
256
- fontSize: '2rem',
257
- fontWeight: 'bold',
258
- marginBottom: '1rem'
259
- }
260
- }, 'Welcome to Vira!'),
261
- React.createElement('p', {
262
- style: {
263
- marginBottom: '1rem',
264
- color: '#666'
265
- }
266
- }, 'Start building your amazing app with Vira UI.'),
267
- React.createElement(Button, {
268
- preset: 'primary',
269
- onClick: () => alert('Hello from Vira!')
270
- }, 'Get Started')
271
- );
272
- }
273
- `;
274
- await fs.writeFile(path.join(projectPath, "src", "App.tsx"), appTsx);
383
+ await fs.writeJSON(path.join(projectPath, "tsconfig.json"), tsconfig_1.tsconfig, { spaces: 2 });
384
+ await fs.writeFile(path.join(projectPath, "vite.config.ts"), viteConfig_1.viteConfig);
385
+ await fs.writeFile(path.join(projectPath, "index.html"), indexHtml_1.indexHtml);
386
+ await fs.writeFile(path.join(projectPath, ".env.local"), envLocal_1.envLocal);
387
+ await fs.writeFile(path.join(projectPath, ".env.example"), envExample_1.envExample);
388
+ await fs.writeFile(path.join(projectPath, "src", "index.css"), indexCss_1.indexCss);
389
+ await fs.writeFile(path.join(projectPath, "src", "main.tsx"), mainTsx_1.mainTsx);
390
+ await fs.writeFile(path.join(projectPath, "src", "App.tsx"), appTsx_1.appTsx);
391
+ await fs.writeFile(path.join(projectPath, "src", "hooks", "useViraStream.ts"), useViraStream_1.useViraStream);
392
+ await fs.writeFile(path.join(projectPath, "src", "hooks", "useViraState.ts"), useViraState_1.useViraState);
393
+ }
394
+ /**
395
+ * Бекенд-заготовка (Go) для последующего расширения (Kafka/Redis/PG)
396
+ */
397
+ async function createBackendStub(backendPath) {
398
+ await fs.ensureDir(backendPath);
399
+ await fs.ensureDir(path.join(backendPath, "cmd", "api"));
400
+ await fs.ensureDir(path.join(backendPath, "config"));
401
+ await fs.ensureDir(path.join(backendPath, "internal"));
402
+ await fs.ensureDir(path.join(backendPath, "internal", "middleware"));
403
+ await fs.ensureDir(path.join(backendPath, "internal", "config"));
404
+ await fs.ensureDir(path.join(backendPath, "internal", "db"));
405
+ await fs.ensureDir(path.join(backendPath, "internal", "cache"));
406
+ await fs.ensureDir(path.join(backendPath, "internal", "events"));
407
+ await fs.ensureDir(path.join(backendPath, "internal", "db", "gen"));
408
+ await fs.ensureDir(path.join(backendPath, "internal", "types"));
409
+ await fs.ensureDir(path.join(backendPath, "migrations"));
410
+ await fs.ensureDir(path.join(backendPath, "queries"));
411
+ await fs.writeFile(path.join(backendPath, "queries", ".gitkeep"), "");
412
+ await fs.writeFile(path.join(backendPath, "migrations", ".gitkeep"), "");
413
+ await fs.writeFile(path.join(backendPath, "go.mod"), goMod_1.goMod);
414
+ await fs.writeFile(path.join(backendPath, "cmd", "api", "main.go"), mainGo_1.mainGo);
415
+ await fs.writeFile(path.join(backendPath, "internal", "config", "config.go"), configGo_1.configGo);
416
+ await fs.writeFile(path.join(backendPath, "internal", "db", "db.go"), dbGo_1.dbGo);
417
+ await fs.writeFile(path.join(backendPath, "internal", "cache", "redis.go"), redisGo_1.redisGo);
418
+ await fs.writeFile(path.join(backendPath, "internal", "events", "kafka.go"), kafkaGo_1.kafkaGo);
419
+ await fs.writeFile(path.join(backendPath, "internal", "types", "types.go"), typesGo_1.typesGo);
420
+ await fs.writeFile(path.join(backendPath, "internal", "events", "channels.go"), channelHelpers_1.channelHelpers);
421
+ await fs.writeFile(path.join(backendPath, "internal", "events", "api.go"), eventsAPI_1.eventsAPI);
422
+ await fs.writeFile(path.join(backendPath, "internal", "events", "registry.go"), registryGo_1.registryGo);
423
+ await fs.writeFile(path.join(backendPath, "internal", "events", "state_store.go"), stateStore_1.stateStore);
424
+ await fs.writeFile(path.join(backendPath, "config", "app.yaml"), appYaml_1.appYaml);
425
+ await fs.writeFile(path.join(backendPath, "config", "db.yaml"), dbYaml_1.dbYaml);
426
+ await fs.writeFile(path.join(backendPath, "config", "redis.yaml"), redisYaml_1.redisYaml);
427
+ await fs.writeFile(path.join(backendPath, "config", "kafka.yaml"), kafkaYaml_1.kafkaYaml);
428
+ await fs.writeFile(path.join(backendPath, "sqlc.yaml"), sqlcYaml_1.sqlcYaml);
429
+ await fs.writeFile(path.join(backendPath, "Dockerfile"), dockerfile_1.dockerfile);
430
+ await fs.writeFile(path.join(backendPath, ".env.example"), backendEnvExample_1.backendEnvExample);
431
+ await fs.writeFile(path.join(backendPath, "README.md"), backendReadme_1.backendReadme);
432
+ }
433
+ /**
434
+ * Заготовка для docker-compose дев-окружения
435
+ */
436
+ async function createDeployScaffold(deployPath) {
437
+ await fs.ensureDir(deployPath);
438
+ await fs.writeFile(path.join(deployPath, "docker-compose.dev.yml"), dockerCompose_1.dockerCompose);
439
+ await fs.writeFile(path.join(deployPath, "docker-compose.prod.yml"), dockerComposeProd_1.dockerComposeProd);
440
+ }
441
+ /**
442
+ * README монорепы с краткой схемой директорий
443
+ */
444
+ async function createWorkspaceReadme(projectPath) {
445
+ await fs.writeFile(path.join(projectPath, "README.md"), readme_1.readme);
446
+ }
447
+ function printNextSteps(projectName, template) {
448
+ console.log(chalk_1.default.yellow(`\nNext steps:`));
449
+ if (template === "fullstack") {
450
+ console.log(` cd ${projectName}/frontend`);
451
+ console.log(` npm install`);
452
+ console.log(` npm run dev`);
453
+ console.log(`\nUI package:`);
454
+ console.log(` cd ../ui`);
455
+ console.log(` npm install`);
456
+ console.log(` npm run dev`);
457
+ console.log(`\nBackend stub:`);
458
+ console.log(` cd ../backend`);
459
+ console.log(` go mod tidy`);
460
+ console.log(` go run ./cmd/api`);
461
+ console.log(`\nDev stack (DB/Redis/Kafka):`);
462
+ console.log(` cd ../deploy && docker compose -f docker-compose.dev.yml up`);
463
+ return;
464
+ }
465
+ console.log(` cd ${projectName}`);
466
+ console.log(` npm install`);
467
+ console.log(` npm run dev`);
275
468
  }
276
469
  /**
277
470
  * Генерация компонента
@@ -279,18 +472,18 @@ export function App() {
279
472
  async function generateComponent(name, dir) {
280
473
  const componentPath = path.join(process.cwd(), dir, "components", `${name}.tsx`);
281
474
  await fs.ensureDir(path.dirname(componentPath));
282
- const componentCode = `import { createElement } from '@vira-ui/core';
283
- import type { ViraComponentProps } from '@vira-ui/core';
284
-
285
- export interface ${name}Props extends ViraComponentProps {
286
- // Add your props here
287
- }
288
-
289
- export function ${name}(props: ${name}Props) {
290
- return createElement('div', { className: '${name.toLowerCase()}' },
291
- // Add your content here
292
- );
293
- }
475
+ const componentCode = `import { createElement } from '@vira-ui/core';
476
+ import type { ViraComponentProps } from '@vira-ui/core';
477
+
478
+ export interface ${name}Props extends ViraComponentProps {
479
+ // Add your props here
480
+ }
481
+
482
+ export function ${name}(props: ${name}Props) {
483
+ return createElement('div', { className: '${name.toLowerCase()}' },
484
+ // Add your content here
485
+ );
486
+ }
294
487
  `;
295
488
  await fs.writeFile(componentPath, componentCode);
296
489
  }
@@ -300,33 +493,33 @@ export function ${name}(props: ${name}Props) {
300
493
  async function generateService(name, dir) {
301
494
  const servicePath = path.join(process.cwd(), dir, "services", `${name}Service.ts`);
302
495
  await fs.ensureDir(path.dirname(servicePath));
303
- const serviceCode = `import { createViraService, signal } from '@vira-ui/core';
304
-
305
- export const ${name}Service = createViraService('${name.toLowerCase()}', () => {
306
- const data = signal([]);
307
- const loading = signal(false);
308
- const error = signal<string | null>(null);
309
-
310
- const fetch = async () => {
311
- loading.set(true);
312
- try {
313
- // Add your logic here
314
- // const result = await api.get('/${name.toLowerCase()}');
315
- // data.set(result);
316
- } catch (e) {
317
- error.set(e instanceof Error ? e.message : 'Unknown error');
318
- } finally {
319
- loading.set(false);
320
- }
321
- };
322
-
323
- return {
324
- data,
325
- loading,
326
- error,
327
- fetch,
328
- };
329
- });
496
+ const serviceCode = `import { createViraService, signal } from '@vira-ui/core';
497
+
498
+ export const ${name}Service = createViraService('${name.toLowerCase()}', () => {
499
+ const data = signal([]);
500
+ const loading = signal(false);
501
+ const error = signal<string | null>(null);
502
+
503
+ const fetch = async () => {
504
+ loading.set(true);
505
+ try {
506
+ // Add your logic here
507
+ // const result = await api.get('/${name.toLowerCase()}');
508
+ // data.set(result);
509
+ } catch (e) {
510
+ error.set(e instanceof Error ? e.message : 'Unknown error');
511
+ } finally {
512
+ loading.set(false);
513
+ }
514
+ };
515
+
516
+ return {
517
+ data,
518
+ loading,
519
+ error,
520
+ fetch,
521
+ };
522
+ });
330
523
  `;
331
524
  await fs.writeFile(servicePath, serviceCode);
332
525
  }
@@ -336,14 +529,14 @@ export const ${name}Service = createViraService('${name.toLowerCase()}', () => {
336
529
  async function generatePage(name, dir) {
337
530
  const pagePath = path.join(process.cwd(), dir, "pages", `${name}Page.tsx`);
338
531
  await fs.ensureDir(path.dirname(pagePath));
339
- const pageCode = `import { createElement } from '@vira-ui/core';
340
-
341
- export function ${name}Page() {
342
- return createElement('div', { className: '${name.toLowerCase()}-page' },
343
- createElement('h1', null, '${name}'),
344
- // Add your content here
345
- );
346
- }
532
+ const pageCode = `import { createElement } from '@vira-ui/core';
533
+
534
+ export function ${name}Page() {
535
+ return createElement('div', { className: '${name.toLowerCase()}-page' },
536
+ createElement('h1', null, '${name}'),
537
+ // Add your content here
538
+ );
539
+ }
347
540
  `;
348
541
  await fs.writeFile(pagePath, pageCode);
349
542
  }
@@ -353,15 +546,15 @@ export function ${name}Page() {
353
546
  async function generateModel(name, dir) {
354
547
  const modelPath = path.join(process.cwd(), dir, "models", `${name}.ts`);
355
548
  await fs.ensureDir(path.dirname(modelPath));
356
- const modelCode = `import { defineModel } from '@vira-ui/core';
357
-
358
- export const ${name}Model = defineModel({
359
- // Add your fields here
360
- id: {
361
- type: 'string',
362
- required: true,
363
- },
364
- });
549
+ const modelCode = `import { defineModel } from '@vira-ui/core';
550
+
551
+ export const ${name}Model = defineModel({
552
+ // Add your fields here
553
+ id: {
554
+ type: 'string',
555
+ required: true,
556
+ },
557
+ });
365
558
  `;
366
559
  await fs.writeFile(modelPath, modelCode);
367
560
  }
@@ -371,13 +564,272 @@ export const ${name}Model = defineModel({
371
564
  async function generateRoute(name, dir) {
372
565
  const routePath = path.join(process.cwd(), dir, "routes", `${name}.ts`);
373
566
  await fs.ensureDir(path.dirname(routePath));
374
- const routeCode = `import { reactiveRoute } from '@vira-ui/core';
375
- import { ${name}Page } from '../pages/${name}Page';
376
-
377
- export const ${name}Route = reactiveRoute({
378
- path: '/${name.toLowerCase()}',
379
- component: ${name}Page,
380
- });
567
+ const routeCode = `import { reactiveRoute } from '@vira-ui/core';
568
+ import { ${name}Page } from '../pages/${name}Page';
569
+
570
+ export const ${name}Route = reactiveRoute({
571
+ path: '/${name.toLowerCase()}',
572
+ component: ${name}Page,
573
+ });
381
574
  `;
382
575
  await fs.writeFile(routePath, routeCode);
383
576
  }
577
+ /**
578
+ * Sync TypeScript types from Go structs (scaffold-level parser)
579
+ */
580
+ async function syncTypes(options) {
581
+ const backendPath = path.resolve(process.cwd(), options.backend);
582
+ const goSource = await fs.readFile(backendPath, "utf8");
583
+ const structs = parseGoStructs(goSource);
584
+ const tsContent = renderTsTypes(structs);
585
+ const targets = [
586
+ path.resolve(process.cwd(), options.frontend),
587
+ path.resolve(process.cwd(), options.ui),
588
+ ];
589
+ for (const target of targets) {
590
+ await fs.ensureDir(path.dirname(target));
591
+ await fs.writeFile(target, tsContent);
592
+ }
593
+ console.log(chalk_1.default.green(`✓ Synced ${structs.length} type(s) to ${options.frontend} and ${options.ui}`));
594
+ }
595
+ function parseGoStructs(source) {
596
+ const structs = [];
597
+ const structRegex = /type\s+(\w+)\s+struct\s*\{([^}]*)\}/gm;
598
+ let match;
599
+ while ((match = structRegex.exec(source)) !== null) {
600
+ const [, name, body] = match;
601
+ const lines = body.split("\n");
602
+ const fields = [];
603
+ for (const raw of lines) {
604
+ const line = raw.trim();
605
+ if (!line || line.startsWith("//"))
606
+ continue;
607
+ // Format: FieldName Type `json:"field"`
608
+ const parts = line.split("`")[0]?.trim() ?? line;
609
+ const tokens = parts.split(/\s+/);
610
+ if (tokens.length < 2)
611
+ continue;
612
+ const fieldName = tokens[0];
613
+ const fieldType = tokens[1];
614
+ let jsonTag;
615
+ const tagMatch = line.match(/`json:"([^"]+)"/);
616
+ if (tagMatch) {
617
+ jsonTag = tagMatch[1].replace(",omitempty", "");
618
+ }
619
+ fields.push({ name: fieldName, type: fieldType, json: jsonTag });
620
+ }
621
+ structs.push({ name, fields });
622
+ }
623
+ return structs;
624
+ }
625
+ function renderTsTypes(structs) {
626
+ const lines = [];
627
+ lines.push("// Auto-generated by vira sync --types. Do not edit manually.");
628
+ lines.push("");
629
+ lines.push("export type ViraMessageType =");
630
+ lines.push(" | 'handshake' | 'ack' | 'sub' | 'sub_ack' | 'unsub' | 'unsub_ack'");
631
+ lines.push(" | 'update' | 'event' | 'diff' | 'ping' | 'pong' | 'error';");
632
+ lines.push("");
633
+ lines.push("export enum ViraChannelEnum {");
634
+ lines.push(" User = 'user',");
635
+ lines.push(" Task = 'task',");
636
+ lines.push(" Notifications = 'notifications',");
637
+ lines.push(" Demo = 'demo',");
638
+ lines.push("}");
639
+ lines.push("");
640
+ lines.push("export type ViraChannel =");
641
+ lines.push(" | `${ViraChannelEnum.User}:${string}`");
642
+ lines.push(" | `${ViraChannelEnum.Task}:${string}`");
643
+ lines.push(" | `${ViraChannelEnum.Notifications}:${string}`");
644
+ lines.push(" | ViraChannelEnum.Demo");
645
+ lines.push(" | string; // fallback for custom channels");
646
+ lines.push("");
647
+ lines.push("export interface ViraDataMap {");
648
+ for (const s of structs) {
649
+ lines.push(` ${s.name}: ${s.name};`);
650
+ }
651
+ lines.push("}");
652
+ lines.push("export type ViraAnyData = ViraDataMap[keyof ViraDataMap];");
653
+ lines.push("");
654
+ lines.push("export interface ViraUpdateMessage<T = ViraAnyData> { type: 'update'; channel: ViraChannel | string; data: T; ts?: number; versionNo?: number; name?: string }");
655
+ lines.push("export interface ViraEventMessage<T = ViraAnyData> { type: 'event'; channel: ViraChannel | string; data: T; ts?: number; versionNo?: number; name?: string }");
656
+ lines.push("export interface ViraDiffMessage<T = ViraAnyData> { type: 'diff'; channel: ViraChannel | string; patch: Partial<T>; ts?: number; versionNo?: number }");
657
+ lines.push("");
658
+ lines.push("// Channel helper functions");
659
+ lines.push("export function channelUser(id: string | number): string {");
660
+ lines.push(` return \`user:\${id}\`;`);
661
+ lines.push("}");
662
+ lines.push("");
663
+ lines.push("export function channelTask(id: string | number): string {");
664
+ lines.push(` return \`task:\${id}\`;`);
665
+ lines.push("}");
666
+ lines.push("");
667
+ lines.push("export function channelNotifications(userId: string | number): string {");
668
+ lines.push(` return \`notifications:\${userId}\`;`);
669
+ lines.push("}");
670
+ lines.push("");
671
+ lines.push("export function channelCustom(name: string, key: string | number): string {");
672
+ lines.push(` return \`\${name}:\${key}\`;`);
673
+ lines.push("}");
674
+ lines.push("");
675
+ lines.push("export type ViraMessage =");
676
+ lines.push(" | { type: 'handshake'; client: string; version: string; authToken?: string; session?: string; ts?: number }");
677
+ lines.push(" | { type: 'ack'; session: string; interval: number; ts?: number }");
678
+ lines.push(" | { type: 'sub'; channels: string[] }");
679
+ lines.push(" | { type: 'sub_ack'; channels: string[] }");
680
+ lines.push(" | { type: 'unsub'; channels: string[] }");
681
+ lines.push(" | { type: 'unsub_ack'; channels: string[] }");
682
+ lines.push(" | ViraUpdateMessage");
683
+ lines.push(" | ViraEventMessage");
684
+ lines.push(" | ViraDiffMessage");
685
+ lines.push(" | { type: 'ping'; ts?: number }");
686
+ lines.push(" | { type: 'pong'; ts?: number }");
687
+ lines.push(" | { type: 'error'; name?: string; message?: string };");
688
+ lines.push("");
689
+ for (const s of structs) {
690
+ lines.push(`export interface ${s.name} {`);
691
+ for (const f of s.fields) {
692
+ const tsType = goTypeToTs(f.type);
693
+ const jsonName = f.json || toCamel(f.name);
694
+ lines.push(` ${jsonName}: ${tsType};`);
695
+ }
696
+ lines.push("}");
697
+ lines.push("");
698
+ }
699
+ return lines.join("\n");
700
+ }
701
+ function goTypeToTs(goType) {
702
+ // Strip package prefix
703
+ const clean = goType.replace(/^[a-zA-Z_]+\./, "");
704
+ // Slice/array
705
+ if (clean.startsWith("[]")) {
706
+ const inner = goTypeToTs(clean.slice(2));
707
+ return `${inner}[]`;
708
+ }
709
+ switch (clean) {
710
+ case "string":
711
+ case "uuid":
712
+ return "string";
713
+ case "int":
714
+ case "int32":
715
+ case "int64":
716
+ case "uint":
717
+ case "uint32":
718
+ case "uint64":
719
+ case "float32":
720
+ case "float64":
721
+ return "number";
722
+ case "bool":
723
+ return "boolean";
724
+ case "Time":
725
+ return "string";
726
+ default:
727
+ return "any";
728
+ }
729
+ }
730
+ function toCamel(name) {
731
+ if (!name)
732
+ return name;
733
+ return name.charAt(0).toLowerCase() + name.slice(1);
734
+ }
735
+ /**
736
+ * Go HTTP handler scaffold
737
+ */
738
+ async function generateGoHandler(name, dir) {
739
+ const safeName = name.toLowerCase();
740
+ const handlerName = capitalize(name);
741
+ const targetDir = path.join(process.cwd(), dir);
742
+ await fs.ensureDir(targetDir);
743
+ const handlerCode = `package handlers
744
+
745
+ import (
746
+ "encoding/json"
747
+ "net/http"
748
+ )
749
+
750
+ type ${handlerName}Response struct {
751
+ Message string \`json:"message"\`
752
+ }
753
+
754
+ // ${handlerName} handles GET /${safeName}
755
+ func ${handlerName}(w http.ResponseWriter, r *http.Request) {
756
+ w.Header().Set("Content-Type", "application/json")
757
+ _ = json.NewEncoder(w).Encode(${handlerName}Response{
758
+ Message: "${handlerName} handler ok",
759
+ })
760
+ }
761
+ `;
762
+ await fs.writeFile(path.join(targetDir, `${safeName}.go`), handlerCode);
763
+ }
764
+ /**
765
+ * Go model scaffold
766
+ */
767
+ async function generateGoModel(name, dir) {
768
+ const modelName = capitalize(name);
769
+ const targetDir = path.join(process.cwd(), dir);
770
+ await fs.ensureDir(targetDir);
771
+ const modelCode = `package models
772
+
773
+ import "time"
774
+
775
+ type ${modelName} struct {
776
+ ID string \`db:"id"\`
777
+ CreatedAt time.Time \`db:"created_at"\`
778
+ UpdatedAt time.Time \`db:"updated_at"\`
779
+ }
780
+ `;
781
+ await fs.writeFile(path.join(targetDir, `${modelName}.go`), modelCode);
782
+ }
783
+ /**
784
+ * SQL migration scaffold (timestamped up/down)
785
+ */
786
+ async function generateMigration(name, dir) {
787
+ const timestamp = new Date()
788
+ .toISOString()
789
+ .replace(/[-:T.Z]/g, "")
790
+ .slice(0, 14);
791
+ const baseName = `${timestamp}_${name}`;
792
+ const targetDir = path.join(process.cwd(), dir);
793
+ await fs.ensureDir(targetDir);
794
+ const upPath = path.join(targetDir, `${baseName}.up.sql`);
795
+ const downPath = path.join(targetDir, `${baseName}.down.sql`);
796
+ const upTemplate = `-- +goose Up
797
+ -- TODO: add migration SQL here
798
+ `;
799
+ const downTemplate = `-- +goose Down
800
+ -- TODO: rollback SQL here
801
+ `;
802
+ await fs.writeFile(upPath, upTemplate);
803
+ await fs.writeFile(downPath, downTemplate);
804
+ }
805
+ async function generateEventHandler(name, dir) {
806
+ const targetDir = path.join(process.cwd(), dir);
807
+ await fs.ensureDir(targetDir);
808
+ const fileName = name.replace(/[^a-zA-Z0-9]+/g, "_");
809
+ const content = (0, eventHandlerTemplate_1.eventHandlerTemplate)(name);
810
+ await fs.writeFile(path.join(targetDir, `${fileName}.go`), content);
811
+ // Also create registry entry file (one per event) to auto-register
812
+ const handlerFunc = toPascal(name);
813
+ const registryFile = path.join(targetDir, `registry_${fileName}.go`);
814
+ if (!(await fs.pathExists(registryFile))) {
815
+ const registryContent = `package events
816
+
817
+ func init() {
818
+ Register("${name}", ${handlerFunc})
819
+ }
820
+ `;
821
+ await fs.writeFile(registryFile, registryContent);
822
+ }
823
+ }
824
+ function capitalize(value) {
825
+ if (!value)
826
+ return value;
827
+ return value.charAt(0).toUpperCase() + value.slice(1);
828
+ }
829
+ function toPascal(value) {
830
+ return value
831
+ .split(/[^a-zA-Z0-9]+/)
832
+ .filter(Boolean)
833
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
834
+ .join("");
835
+ }