@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.
- package/dist/go/appYaml.js +34 -0
- package/dist/go/backendEnvExample.js +21 -0
- package/dist/go/backendReadme.js +18 -0
- package/dist/go/channelHelpers.js +29 -0
- package/dist/go/configGo.js +262 -0
- package/dist/go/dbGo.js +47 -0
- package/dist/go/dbYaml.js +11 -0
- package/dist/go/dockerCompose.js +38 -0
- package/dist/go/dockerComposeProd.js +54 -0
- package/dist/go/dockerfile.js +19 -0
- package/dist/go/eventHandlerTemplate.js +34 -0
- package/dist/go/eventsAPI.js +414 -0
- package/dist/go/goMod.js +20 -0
- package/dist/go/kafkaGo.js +71 -0
- package/dist/go/kafkaYaml.js +10 -0
- package/dist/go/kanbanHandlers.js +221 -0
- package/dist/go/mainGo.js +527 -0
- package/dist/go/readme.js +14 -0
- package/dist/go/redisGo.js +35 -0
- package/dist/go/redisYaml.js +8 -0
- package/dist/go/registryGo.js +47 -0
- package/dist/go/sqlcYaml.js +17 -0
- package/dist/go/stateStore.js +119 -0
- package/dist/go/typesGo.js +15 -0
- package/dist/go/useViraState.js +160 -0
- package/dist/go/useViraStream.js +167 -0
- package/dist/index.js +608 -190
- package/dist/react/appTsx.js +52 -0
- package/dist/react/envExample.js +7 -0
- package/dist/react/envLocal.js +5 -0
- package/dist/react/indexCss.js +22 -0
- package/dist/react/indexHtml.js +16 -0
- package/dist/react/kanbanAppTsx.js +34 -0
- package/dist/react/kanbanBoard.js +63 -0
- package/dist/react/kanbanCard.js +65 -0
- package/dist/react/kanbanColumn.js +67 -0
- package/dist/react/kanbanModels.js +37 -0
- package/dist/react/kanbanService.js +119 -0
- package/dist/react/mainTsx.js +16 -0
- package/dist/react/tsconfig.js +25 -0
- package/dist/react/viteConfig.js +31 -0
- 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.
|
|
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", "
|
|
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,
|
|
119
|
+
await createProjectStructure(projectPath, template);
|
|
75
120
|
console.log(chalk_1.default.green(`✓ Project ${name} created successfully!`));
|
|
76
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
await fs.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
await fs.writeFile(path.join(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
}
|