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