@vira-ui/cli 0.3.2-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 -200
  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,136 +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
- import path from 'path';
182
-
183
- export default defineConfig({
184
- plugins: [
185
- react({
186
- // Babel plugins can be added here if needed
187
- // babel: {
188
- // plugins: [['@vira-ui/babel-plugin', {}]],
189
- // },
190
- }),
191
- ],
192
- resolve: {
193
- alias: {
194
- '@vira-ui/ui': path.resolve(__dirname, 'node_modules/@vira-ui/ui/src/index.ts'),
195
- '@vira-ui/core': path.resolve(__dirname, 'node_modules/@vira-ui/core/src/index.ts'),
196
- },
197
- },
198
- });
199
- `;
200
- await fs.writeFile(path.join(projectPath, "vite.config.ts"), viteConfig);
201
- // index.html
202
- const indexHtml = `<!DOCTYPE html>
203
- <html lang="en">
204
- <head>
205
- <meta charset="UTF-8" />
206
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
207
- <title>Vira App</title>
208
- </head>
209
- <body>
210
- <div id="root"></div>
211
- <script type="module" src="/src/main.tsx"></script>
212
- </body>
213
- </html>
214
- `;
215
- await fs.writeFile(path.join(projectPath, "index.html"), indexHtml);
216
- // src/index.css
217
- const indexCss = `* {
218
- margin: 0;
219
- padding: 0;
220
- box-sizing: border-box;
221
- }
222
-
223
- body {
224
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
225
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
226
- sans-serif;
227
- -webkit-font-smoothing: antialiased;
228
- -moz-osx-font-smoothing: grayscale;
229
- }
230
-
231
- #root {
232
- min-height: 100vh;
233
- }
234
- `;
235
- await fs.writeFile(path.join(projectPath, "src", "index.css"), indexCss);
236
- // src/main.tsx
237
- const mainTsx = `import React from 'react';
238
- import ReactDOM from 'react-dom/client';
239
- import { App } from './App';
240
- import './index.css';
241
-
242
- const rootElement = document.getElementById('root');
243
- if (rootElement) {
244
- ReactDOM.createRoot(rootElement).render(
245
- React.createElement(React.StrictMode, null,
246
- React.createElement(App)
247
- )
248
- );
249
- }
250
- `;
251
- await fs.writeFile(path.join(projectPath, "src", "main.tsx"), mainTsx);
252
- // src/App.tsx
253
- const appTsx = `import React from 'react';
254
- import { Button } from '@vira-ui/ui';
255
-
256
- export function App() {
257
- return React.createElement('div', {
258
- style: {
259
- padding: '2rem',
260
- maxWidth: '800px',
261
- margin: '0 auto'
262
- }
263
- },
264
- React.createElement('h1', {
265
- style: {
266
- fontSize: '2rem',
267
- fontWeight: 'bold',
268
- marginBottom: '1rem'
269
- }
270
- }, 'Welcome to Vira!'),
271
- React.createElement('p', {
272
- style: {
273
- marginBottom: '1rem',
274
- color: '#666'
275
- }
276
- }, 'Start building your amazing app with Vira UI.'),
277
- React.createElement(Button, {
278
- preset: 'primary',
279
- onClick: () => alert('Hello from Vira!')
280
- }, 'Get Started')
281
- );
282
- }
283
- `;
284
- 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`);
285
434
  }
286
435
  /**
287
436
  * Генерация компонента
@@ -289,18 +438,18 @@ export function App() {
289
438
  async function generateComponent(name, dir) {
290
439
  const componentPath = path.join(process.cwd(), dir, "components", `${name}.tsx`);
291
440
  await fs.ensureDir(path.dirname(componentPath));
292
- const componentCode = `import { createElement } from '@vira-ui/core';
293
- import type { ViraComponentProps } from '@vira-ui/core';
294
-
295
- export interface ${name}Props extends ViraComponentProps {
296
- // Add your props here
297
- }
298
-
299
- export function ${name}(props: ${name}Props) {
300
- return createElement('div', { className: '${name.toLowerCase()}' },
301
- // Add your content here
302
- );
303
- }
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
+ }
304
453
  `;
305
454
  await fs.writeFile(componentPath, componentCode);
306
455
  }
@@ -310,33 +459,33 @@ export function ${name}(props: ${name}Props) {
310
459
  async function generateService(name, dir) {
311
460
  const servicePath = path.join(process.cwd(), dir, "services", `${name}Service.ts`);
312
461
  await fs.ensureDir(path.dirname(servicePath));
313
- const serviceCode = `import { createViraService, signal } from '@vira-ui/core';
314
-
315
- export const ${name}Service = createViraService('${name.toLowerCase()}', () => {
316
- const data = signal([]);
317
- const loading = signal(false);
318
- const error = signal<string | null>(null);
319
-
320
- const fetch = async () => {
321
- loading.set(true);
322
- try {
323
- // Add your logic here
324
- // const result = await api.get('/${name.toLowerCase()}');
325
- // data.set(result);
326
- } catch (e) {
327
- error.set(e instanceof Error ? e.message : 'Unknown error');
328
- } finally {
329
- loading.set(false);
330
- }
331
- };
332
-
333
- return {
334
- data,
335
- loading,
336
- error,
337
- fetch,
338
- };
339
- });
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
+ });
340
489
  `;
341
490
  await fs.writeFile(servicePath, serviceCode);
342
491
  }
@@ -346,14 +495,14 @@ export const ${name}Service = createViraService('${name.toLowerCase()}', () => {
346
495
  async function generatePage(name, dir) {
347
496
  const pagePath = path.join(process.cwd(), dir, "pages", `${name}Page.tsx`);
348
497
  await fs.ensureDir(path.dirname(pagePath));
349
- const pageCode = `import { createElement } from '@vira-ui/core';
350
-
351
- export function ${name}Page() {
352
- return createElement('div', { className: '${name.toLowerCase()}-page' },
353
- createElement('h1', null, '${name}'),
354
- // Add your content here
355
- );
356
- }
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
+ }
357
506
  `;
358
507
  await fs.writeFile(pagePath, pageCode);
359
508
  }
@@ -363,15 +512,15 @@ export function ${name}Page() {
363
512
  async function generateModel(name, dir) {
364
513
  const modelPath = path.join(process.cwd(), dir, "models", `${name}.ts`);
365
514
  await fs.ensureDir(path.dirname(modelPath));
366
- const modelCode = `import { defineModel } from '@vira-ui/core';
367
-
368
- export const ${name}Model = defineModel({
369
- // Add your fields here
370
- id: {
371
- type: 'string',
372
- required: true,
373
- },
374
- });
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
+ });
375
524
  `;
376
525
  await fs.writeFile(modelPath, modelCode);
377
526
  }
@@ -381,13 +530,272 @@ export const ${name}Model = defineModel({
381
530
  async function generateRoute(name, dir) {
382
531
  const routePath = path.join(process.cwd(), dir, "routes", `${name}.ts`);
383
532
  await fs.ensureDir(path.dirname(routePath));
384
- const routeCode = `import { reactiveRoute } from '@vira-ui/core';
385
- import { ${name}Page } from '../pages/${name}Page';
386
-
387
- export const ${name}Route = reactiveRoute({
388
- path: '/${name.toLowerCase()}',
389
- component: ${name}Page,
390
- });
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
+ });
391
540
  `;
392
541
  await fs.writeFile(routePath, routeCode);
393
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
+ }