@robsun/create-keystone-app 0.2.13 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +46 -43
  2. package/dist/create-keystone-app.js +347 -10
  3. package/dist/create-module.js +1219 -607
  4. package/package.json +22 -23
  5. package/template/.claude/skills/keystone-implement/SKILL.md +113 -0
  6. package/template/.claude/skills/keystone-implement/references/CHECKLIST.md +91 -0
  7. package/template/.claude/skills/keystone-implement/references/PATTERNS.md +1088 -0
  8. package/template/.claude/skills/keystone-implement/references/SCHEMA.md +135 -0
  9. package/template/.claude/skills/keystone-implement/references/TESTING.md +231 -0
  10. package/template/.claude/skills/keystone-requirements/SKILL.md +296 -0
  11. package/template/.claude/skills/keystone-requirements/references/CONFIRM_TEMPLATE.md +170 -0
  12. package/template/.claude/skills/keystone-requirements/references/SCHEMA.md +135 -0
  13. package/template/.eslintrc.js +3 -0
  14. package/template/.github/workflows/ci.yml +30 -0
  15. package/template/.github/workflows/release.yml +32 -0
  16. package/template/.golangci.yml +11 -0
  17. package/template/README.md +81 -73
  18. package/template/apps/server/README.md +8 -0
  19. package/template/apps/server/cmd/server/main.go +27 -185
  20. package/template/apps/server/config.example.yaml +31 -1
  21. package/template/apps/server/config.yaml +31 -1
  22. package/template/apps/server/go.mod +60 -18
  23. package/template/apps/server/go.sum +183 -31
  24. package/template/apps/server/internal/frontend/embed.go +3 -8
  25. package/template/apps/server/internal/modules/example/README.md +18 -0
  26. package/template/apps/server/internal/modules/example/api/handler/handler_test.go +9 -0
  27. package/template/apps/server/internal/modules/example/api/handler/item_handler.go +468 -165
  28. package/template/apps/server/internal/modules/example/bootstrap/seeds/item.go +217 -8
  29. package/template/apps/server/internal/modules/example/domain/models/item.go +40 -7
  30. package/template/apps/server/internal/modules/example/domain/service/approval_callback.go +68 -0
  31. package/template/apps/server/internal/modules/example/domain/service/approval_schema.go +41 -0
  32. package/template/apps/server/internal/modules/example/domain/service/errors.go +20 -22
  33. package/template/apps/server/internal/modules/example/domain/service/item_service.go +267 -7
  34. package/template/apps/server/internal/modules/example/domain/service/item_service_test.go +281 -0
  35. package/template/apps/server/internal/modules/example/i18n/keys.go +32 -20
  36. package/template/apps/server/internal/modules/example/i18n/locales/en-US.json +30 -18
  37. package/template/apps/server/internal/modules/example/i18n/locales/zh-CN.json +30 -18
  38. package/template/apps/server/internal/modules/example/infra/exporter/item_exporter.go +119 -0
  39. package/template/apps/server/internal/modules/example/infra/importer/item_importer.go +77 -0
  40. package/template/apps/server/internal/modules/example/infra/repository/item_repository.go +99 -49
  41. package/template/apps/server/internal/modules/example/module.go +171 -97
  42. package/template/apps/server/internal/modules/example/tests/integration_test.go +7 -0
  43. package/template/apps/server/internal/modules/manifest.go +7 -7
  44. package/template/apps/web/README.md +4 -2
  45. package/template/apps/web/package.json +1 -1
  46. package/template/apps/web/src/app.config.ts +8 -6
  47. package/template/apps/web/src/index.css +7 -3
  48. package/template/apps/web/src/main.tsx +2 -5
  49. package/template/apps/web/src/modules/example/help/en-US/faq.md +27 -0
  50. package/template/apps/web/src/modules/example/help/en-US/items.md +30 -0
  51. package/template/apps/web/src/modules/example/help/en-US/overview.md +31 -0
  52. package/template/apps/web/src/modules/example/help/zh-CN/faq.md +27 -0
  53. package/template/apps/web/src/modules/example/help/zh-CN/items.md +31 -0
  54. package/template/apps/web/src/modules/example/help/zh-CN/overview.md +32 -0
  55. package/template/apps/web/src/modules/example/locales/en-US/example.json +99 -32
  56. package/template/apps/web/src/modules/example/locales/zh-CN/example.json +85 -18
  57. package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +840 -237
  58. package/template/apps/web/src/modules/example/services/exampleItems.ts +79 -8
  59. package/template/apps/web/src/modules/example/types.ts +14 -1
  60. package/template/apps/web/src/modules/index.ts +1 -0
  61. package/template/apps/web/vite.config.ts +9 -3
  62. package/template/docs/CONVENTIONS.md +10 -7
  63. package/template/package.json +4 -5
  64. package/template/pnpm-lock.yaml +76 -5
  65. package/template/scripts/build.bat +15 -3
  66. package/template/scripts/build.sh +9 -3
  67. package/template/scripts/check-help.js +249 -0
  68. package/template/scripts/compress-assets.js +89 -0
  69. package/template/scripts/test.bat +23 -0
  70. package/template/scripts/test.sh +16 -0
  71. package/template/.claude/skills/keystone-dev/SKILL.md +0 -103
  72. package/template/.claude/skills/keystone-dev/references/APPROVAL.md +0 -121
  73. package/template/.claude/skills/keystone-dev/references/CAPABILITIES.md +0 -261
  74. package/template/.claude/skills/keystone-dev/references/TEMPLATES.md +0 -532
  75. package/template/.claude/skills/keystone-dev/references/TESTING.md +0 -44
  76. package/template/.codex/skills/keystone-dev/SKILL.md +0 -103
  77. package/template/.codex/skills/keystone-dev/references/APPROVAL.md +0 -121
  78. package/template/.codex/skills/keystone-dev/references/CAPABILITIES.md +0 -261
  79. package/template/.codex/skills/keystone-dev/references/TEMPLATES.md +0 -532
  80. package/template/.codex/skills/keystone-dev/references/TESTING.md +0 -44
  81. package/template/apps/server/internal/app/routes/module_routes.go +0 -16
  82. package/template/apps/server/internal/app/routes/routes.go +0 -226
  83. package/template/apps/server/internal/app/startup/startup.go +0 -74
  84. package/template/apps/server/internal/frontend/handler.go +0 -122
  85. package/template/apps/server/internal/modules/registry.go +0 -145
  86. package/template/apps/web/src/modules/example/help/faq.md +0 -23
  87. package/template/apps/web/src/modules/example/help/items.md +0 -26
  88. package/template/apps/web/src/modules/example/help/overview.md +0 -25
package/README.md CHANGED
@@ -1,50 +1,53 @@
1
- # @robsun/create-keystone-app
2
-
3
- ## 用法
4
- ```bash
5
- npx @robsun/create-keystone-app <dir> [options]
6
- pnpm dlx @robsun/create-keystone-app <dir> [options]
7
- ```
8
- 不传 options 会进入交互式向导。
9
-
10
- ## 选项
11
- - `<dir>`:目标目录(必填),可为新目录名或 `.`(当前目录)。
1
+ # @robsun/create-keystone-app
2
+
3
+ ## 用法
4
+ ```bash
5
+ npx @robsun/create-keystone-app <dir> [options]
6
+ pnpm dlx @robsun/create-keystone-app <dir> [options]
7
+ ```
8
+ 不传 options 会进入交互式向导。
9
+
10
+ ## 选项
11
+ - `<dir>`:目标目录(必填),可为新目录名或 `.`(当前目录)。
12
12
  - `--db <sqlite|postgres>`:数据库驱动(默认 `sqlite`)。
13
13
  - `--queue <memory|redis>`:队列驱动(默认 `memory`)。
14
14
  - `--storage <local|s3>`:存储驱动(默认 `local`)。
15
-
16
- ## 示例
17
- ```bash
18
- npx @robsun/create-keystone-app my-app --db=postgres --queue=redis --storage=s3
19
- ```
20
-
21
- ## 模块生成器
22
- ```bash
23
- npx --package @robsun/create-keystone-app create-keystone-module <module-name> [options]
24
- pnpm dlx --package @robsun/create-keystone-app create-keystone-module <module-name> [options]
25
- ```
26
- 不传 options 会进入交互式向导。
27
-
28
- 选项:
29
- - `--frontend-only`:只生成前端模块。
30
- - `--backend-only`:只生成后端模块。
31
- - `--with-crud`:包含 CRUD 示例代码。
32
- - `--skip-register`:跳过自动注册步骤。
33
-
34
- ## AI Skills
35
- - Codex:`.codex/skills/keystone-dev`
36
- - Claude:`.claude/skills/keystone-dev`
37
-
38
- ## 初始化后操作
39
- ```bash
40
- cd <dir>
15
+ - `--with-example`:包含示例模块。
16
+ - `--with-ci`:包含 CI 工作流模板。
17
+ - `--verify`:生成后执行验证(安装依赖并构建)。
18
+
19
+ ## 示例
20
+ ```bash
21
+ npx @robsun/create-keystone-app my-app --db=postgres --queue=redis --storage=s3
22
+ ```
23
+
24
+ ## 模块生成器
25
+ ```bash
26
+ npx --package @robsun/create-keystone-app create-keystone-module <module-name> [options]
27
+ pnpm dlx --package @robsun/create-keystone-app create-keystone-module <module-name> [options]
28
+ ```
29
+ 不传 options 会进入交互式向导。
30
+
31
+ 选项:
32
+ - `--frontend-only`:只生成前端模块。
33
+ - `--backend-only`:只生成后端模块。
34
+ - `--with-crud`:包含 CRUD 示例代码。
35
+ - `--with-approval`:包含审批流代码(自动启用 `--with-crud`)。
36
+ - `--skip-register`:跳过自动注册步骤。
37
+
38
+ ## AI Skills
39
+ - Codex:`.codex/skills/keystone-dev`
40
+ - Claude:`.claude/skills/keystone-dev`
41
+
42
+ ## 初始化后操作
43
+ ```bash
44
+ cd <dir>
41
45
  pnpm install
42
46
  pnpm server:dev
43
- pnpm web:dev
44
47
  pnpm dev
45
48
  ```
46
-
47
- ## 端口与 Example
48
- - Web 默认端口:`3000`;后端默认端口:`8080`。
49
- - Example API:`/api/v1/example/items`。
50
- - 权限:`example:item:view`、`example:item:manage`。
49
+
50
+ ## 端口与 Example
51
+ - Web 默认端口:`3000`;后端默认端口:`8080`。
52
+ - Example API:`/api/v1/example/items`。
53
+ - 权限:`example:item:view`、`example:item:manage`。
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
+ const os = require('os');
4
5
  const readline = require('readline/promises');
6
+ const { execFileSync } = require('child_process');
5
7
  const { stdin: input, stdout: output } = require('process');
6
8
 
7
9
  const usage = [
@@ -11,6 +13,10 @@ const usage = [
11
13
  ' --db <sqlite|postgres> Database driver (default: sqlite)',
12
14
  ' --queue <memory|redis> Queue driver (default: memory)',
13
15
  ' --storage <local|s3> Storage driver (default: local)',
16
+ ' --with-example Include example module',
17
+ ' --with-ci Include CI workflow templates',
18
+ ' --verify Run post-generation verification',
19
+ ' --local Use local keystone packages (for development)',
14
20
  ' -h, --help Show help',
15
21
  '',
16
22
  'Run without options to use guided prompts.',
@@ -34,6 +40,8 @@ async function main() {
34
40
  let db;
35
41
  let queue;
36
42
  let storage;
43
+ let withExample;
44
+ let withCi;
37
45
 
38
46
  try {
39
47
  if (!args.target) {
@@ -47,6 +55,12 @@ async function main() {
47
55
  db = normalizeChoice(args.db, ['sqlite', 'postgres'], 'db') || null;
48
56
  queue = normalizeChoice(args.queue, ['memory', 'redis'], 'queue') || null;
49
57
  storage = normalizeChoice(args.storage, ['local', 's3'], 'storage') || null;
58
+ if (typeof args.withExample === 'boolean') {
59
+ withExample = args.withExample;
60
+ }
61
+ if (typeof args.withCi === 'boolean') {
62
+ withCi = args.withCi;
63
+ }
50
64
 
51
65
  if (!db) {
52
66
  db = rl ? await promptSelect(rl, 'Select database driver', ['sqlite', 'postgres'], 0) : 'sqlite';
@@ -57,6 +71,12 @@ async function main() {
57
71
  if (!storage) {
58
72
  storage = rl ? await promptSelect(rl, 'Select storage driver', ['local', 's3'], 0) : 'local';
59
73
  }
74
+ if (typeof withExample !== 'boolean') {
75
+ withExample = rl ? await promptConfirm(rl, 'Include example module', false) : false;
76
+ }
77
+ if (typeof withCi !== 'boolean') {
78
+ withCi = rl ? await promptConfirm(rl, 'Include CI workflows', false) : false;
79
+ }
60
80
  } finally {
61
81
  if (rl) {
62
82
  rl.close();
@@ -85,14 +105,33 @@ async function main() {
85
105
  });
86
106
 
87
107
  applyConfigOptions(targetDir, { db, queue, storage });
108
+ applyExampleOption(targetDir, { withExample });
109
+ applyCiOption(targetDir, { withCi });
88
110
 
89
- console.log(`Created ${targetName}`);
90
- console.log('Next steps:');
91
- console.log(` cd ${args.target}`);
92
- console.log(' pnpm install');
93
- console.log(' pnpm server:dev');
94
- console.log(' pnpm web:dev');
95
- console.log(' pnpm dev');
111
+ const localMode = Boolean(args.local);
112
+ if (localMode) {
113
+ const keystoneRoot = path.resolve(__dirname, '..', '..', '..');
114
+ applyLocalDev(targetDir, keystoneRoot);
115
+ }
116
+
117
+ if (args.verify) {
118
+ runVerify(targetDir);
119
+ }
120
+
121
+ if (localMode) {
122
+ console.log(`Created ${targetName} (local development mode)`);
123
+ console.log('Next steps:');
124
+ console.log(` cd ${args.target}`);
125
+ console.log(' pnpm install');
126
+ console.log(' pnpm dev');
127
+ } else {
128
+ console.log(`Created ${targetName}`);
129
+ console.log('Next steps:');
130
+ console.log(` cd ${args.target}`);
131
+ console.log(' pnpm install');
132
+ console.log(' pnpm server:dev');
133
+ console.log(' pnpm dev');
134
+ }
96
135
  }
97
136
 
98
137
  function isInteractive() {
@@ -136,6 +175,24 @@ async function promptSelect(rl, label, choices, defaultIndex) {
136
175
  }
137
176
  }
138
177
 
178
+ async function promptConfirm(rl, label, defaultValue) {
179
+ const fallback = defaultValue ? 'Y/n' : 'y/N';
180
+ while (true) {
181
+ const answer = await rl.question(`${label} (${fallback}): `);
182
+ const normalized = answer.trim().toLowerCase();
183
+ if (!normalized) {
184
+ return Boolean(defaultValue);
185
+ }
186
+ if (['y', 'yes'].includes(normalized)) {
187
+ return true;
188
+ }
189
+ if (['n', 'no'].includes(normalized)) {
190
+ return false;
191
+ }
192
+ console.log('Invalid selection. Try again.');
193
+ }
194
+ }
195
+
139
196
  function normalizePackageName(name) {
140
197
  const cleaned = String(name || '')
141
198
  .trim()
@@ -182,6 +239,79 @@ function shouldSkipDir(name) {
182
239
  return name === 'node_modules' || name === '.git';
183
240
  }
184
241
 
242
+ function applyLocalDev(targetDir, keystoneRoot) {
243
+ // Create .npmrc to deduplicate React in local development
244
+ const npmrcPath = path.join(targetDir, '.npmrc');
245
+ const npmrcContent = [
246
+ '# Deduplicate React to avoid "Invalid hook call" errors with linked packages',
247
+ 'public-hoist-pattern[]=react',
248
+ 'public-hoist-pattern[]=react-dom',
249
+ 'public-hoist-pattern[]=dayjs',
250
+ 'public-hoist-pattern[]=@types/react',
251
+ 'public-hoist-pattern[]=@types/react-dom',
252
+ ].join('\n') + '\n';
253
+ fs.writeFileSync(npmrcPath, npmrcContent, 'utf8');
254
+
255
+ // Update frontend package.json to use local packages
256
+ const webPkgPath = path.join(targetDir, 'apps', 'web', 'package.json');
257
+ if (fs.existsSync(webPkgPath)) {
258
+ const relPath = path.relative(path.dirname(webPkgPath), keystoneRoot).replace(/\\/g, '/');
259
+ updateFile(webPkgPath, (content) => {
260
+ const pkg = JSON.parse(content);
261
+ if (pkg.dependencies) {
262
+ if (pkg.dependencies['@robsun/keystone-web-core']) {
263
+ pkg.dependencies['@robsun/keystone-web-core'] = `link:${relPath}/packages/keystone-web-core`;
264
+ }
265
+ if (pkg.dependencies['@robsun/keystone-contracts']) {
266
+ pkg.dependencies['@robsun/keystone-contracts'] = `link:${relPath}/packages/keystone-contracts`;
267
+ }
268
+ }
269
+ return JSON.stringify(pkg, null, 2) + '\n';
270
+ });
271
+ }
272
+
273
+ // Update Vite config for local development
274
+ const viteConfigPath = path.join(targetDir, 'apps', 'web', 'vite.config.ts');
275
+ if (fs.existsSync(viteConfigPath)) {
276
+ const relPath = path.relative(path.join(targetDir, 'apps', 'web'), keystoneRoot).replace(/\\/g, '/');
277
+ updateFile(viteConfigPath, (content) => {
278
+ let updated = content;
279
+ // Add dedupe configuration to resolve section
280
+ if (updated.includes('resolve: {')) {
281
+ updated = updated.replace(
282
+ /resolve:\s*\{/,
283
+ `resolve: {\n dedupe: ['react', 'react-dom', 'dayjs'],`
284
+ );
285
+ }
286
+ // Add fs.allow to server section to allow accessing linked packages
287
+ if (updated.includes('server: {')) {
288
+ updated = updated.replace(
289
+ /server:\s*\{/,
290
+ `server: {\n fs: { allow: ['${relPath}', '.'] },`
291
+ );
292
+ }
293
+ return updated;
294
+ });
295
+ }
296
+
297
+ // Update backend go.mod to use local package
298
+ const goModPath = path.join(targetDir, 'apps', 'server', 'go.mod');
299
+ if (fs.existsSync(goModPath)) {
300
+ const relPath = path.relative(path.dirname(goModPath), keystoneRoot).replace(/\\/g, '/');
301
+ updateFile(goModPath, (content) => {
302
+ if (content.includes('replace github.com/robsuncn/keystone')) {
303
+ return content;
304
+ }
305
+ const lines = content.split('\n');
306
+ const requireIdx = lines.findIndex(l => l.startsWith('require '));
307
+ if (requireIdx !== -1) {
308
+ lines.splice(requireIdx, 0, `replace github.com/robsuncn/keystone => ${relPath}`, '');
309
+ }
310
+ return lines.join('\n');
311
+ });
312
+ }
313
+ }
314
+
185
315
  function applyConfigOptions(targetDir, options) {
186
316
  const configFiles = [
187
317
  path.join(targetDir, 'apps', 'server', 'config.yaml'),
@@ -193,6 +323,156 @@ function applyConfigOptions(targetDir, options) {
193
323
  }
194
324
  }
195
325
 
326
+ function applyExampleOption(targetDir, options) {
327
+ const withExample = Boolean(options?.withExample);
328
+ const webModulesIndex = path.join(targetDir, 'apps', 'web', 'src', 'modules', 'index.ts');
329
+ const webConfigPath = path.join(targetDir, 'apps', 'web', 'src', 'app.config.ts');
330
+ const serverManifestPath = path.join(targetDir, 'apps', 'server', 'internal', 'modules', 'manifest.go');
331
+ const modulePath = resolveGoModulePath(path.join(targetDir, 'apps', 'server', 'go.mod'));
332
+ const serverConfigPaths = [
333
+ path.join(targetDir, 'apps', 'server', 'config.yaml'),
334
+ path.join(targetDir, 'apps', 'server', 'config.example.yaml'),
335
+ ];
336
+
337
+ if (withExample) {
338
+ fs.mkdirSync(path.dirname(webModulesIndex), { recursive: true });
339
+ fs.writeFileSync(webModulesIndex, "import './example'\n", 'utf8');
340
+ updateFile(webConfigPath, enableExampleInWebConfig);
341
+ serverConfigPaths.forEach((filePath) => {
342
+ updateFile(filePath, addExampleModuleToYaml);
343
+ });
344
+ fs.writeFileSync(serverManifestPath, exampleManifestContent(modulePath), 'utf8');
345
+ return;
346
+ }
347
+
348
+ const exampleDirs = [
349
+ path.join(targetDir, 'apps', 'web', 'src', 'modules', 'example'),
350
+ path.join(targetDir, 'apps', 'server', 'internal', 'modules', 'example'),
351
+ ];
352
+ exampleDirs.forEach((dirPath) => {
353
+ if (fs.existsSync(dirPath)) {
354
+ fs.rmSync(dirPath, { recursive: true, force: true });
355
+ }
356
+ });
357
+
358
+ fs.mkdirSync(path.dirname(webModulesIndex), { recursive: true });
359
+ fs.writeFileSync(webModulesIndex, '// Register application modules here.\n', 'utf8');
360
+ updateFile(webConfigPath, removeExampleFromWebConfig);
361
+ serverConfigPaths.forEach((filePath) => {
362
+ updateFile(filePath, removeExampleFromYaml);
363
+ });
364
+ fs.writeFileSync(serverManifestPath, emptyManifestContent(), 'utf8');
365
+ }
366
+
367
+ function applyCiOption(targetDir, options) {
368
+ const withCi = Boolean(options?.withCi);
369
+ if (withCi) {
370
+ return;
371
+ }
372
+ const paths = [
373
+ path.join(targetDir, '.github'),
374
+ path.join(targetDir, '.golangci.yml'),
375
+ path.join(targetDir, '.eslintrc.js'),
376
+ ];
377
+ paths.forEach((target) => {
378
+ if (!fs.existsSync(target)) {
379
+ return;
380
+ }
381
+ fs.rmSync(target, { recursive: true, force: true });
382
+ });
383
+ }
384
+
385
+ function enableExampleInWebConfig(content) {
386
+ let next = content;
387
+ if (!next.includes("'example'")) {
388
+ next = next.replace("enabled: ['keystone'],", "enabled: ['keystone', 'example'],");
389
+ }
390
+ if (!next.includes('example_item')) {
391
+ next = next.replace(
392
+ "{ value: 'general', label: 'General Flow' },",
393
+ "{ value: 'general', label: 'General Flow' },\n { value: 'example_item', label: 'Example Items' },"
394
+ );
395
+ }
396
+ return next;
397
+ }
398
+
399
+ function removeExampleFromWebConfig(content) {
400
+ let next = content;
401
+ next = next.replace("['keystone', 'example']", "['keystone']");
402
+ next = next.replace("['example', 'keystone']", "['keystone']");
403
+ next = next.replace(/\n\s*\{ value: 'example_item'[^}]*\},?/g, '');
404
+ return next;
405
+ }
406
+
407
+ function addExampleModuleToYaml(content) {
408
+ if (content.includes('"example"')) {
409
+ return content;
410
+ }
411
+ const pattern = /(modules:\s*[\s\S]*?enabled:\s*\n\s*-\s*"keystone")/m;
412
+ if (!pattern.test(content)) {
413
+ return content;
414
+ }
415
+ return content.replace(pattern, `$1\n - "example"`);
416
+ }
417
+
418
+ function removeExampleFromYaml(content) {
419
+ return content.replace(/\n\s*-\s*"example"\s*/g, '');
420
+ }
421
+
422
+ function emptyManifestContent() {
423
+ return [
424
+ 'package modules',
425
+ '',
426
+ 'import servermodules "github.com/robsuncn/keystone/server/modules"',
427
+ '',
428
+ '// Register wires application modules into the shared registry.',
429
+ 'func Register(registry *servermodules.Registry) {',
430
+ '\tif registry == nil {',
431
+ '\t\treturn',
432
+ '\t}',
433
+ '\tregistry.Register(servermodules.NewCoreModule())',
434
+ '}',
435
+ '',
436
+ ].join('\n');
437
+ }
438
+
439
+ function exampleManifestContent(modulePath) {
440
+ const importPath = modulePath
441
+ ? `${modulePath}/internal/modules/example`
442
+ : '__APP_NAME__/apps/server/internal/modules/example';
443
+ return [
444
+ 'package modules',
445
+ '',
446
+ 'import (',
447
+ '\tservermodules "github.com/robsuncn/keystone/server/modules"',
448
+ '',
449
+ `\texample "${importPath}"`,
450
+ ')',
451
+ '',
452
+ '// Register wires application modules into the shared registry.',
453
+ 'func Register(registry *servermodules.Registry) {',
454
+ '\tif registry == nil {',
455
+ '\t\treturn',
456
+ '\t}',
457
+ '\tregistry.Register(servermodules.NewCoreModule())',
458
+ '\tregistry.Register(example.NewModule())',
459
+ '}',
460
+ '',
461
+ ].join('\n');
462
+ }
463
+
464
+ function resolveGoModulePath(goModPath) {
465
+ if (!fs.existsSync(goModPath)) {
466
+ return '';
467
+ }
468
+ const content = fs.readFileSync(goModPath, 'utf8');
469
+ const match = content.match(/^module\s+([^\s]+)\s*$/m);
470
+ if (!match) {
471
+ return '';
472
+ }
473
+ return match[1];
474
+ }
475
+
196
476
  function applyYamlOptions(content, options) {
197
477
  let next = content;
198
478
  if (options.db) {
@@ -213,11 +493,23 @@ function applyYamlOptions(content, options) {
213
493
  }
214
494
 
215
495
  function updateYamlSectionValue(content, section, key, value) {
216
- const pattern = new RegExp(`(^${section}:\\s*[\\s\\S]*?^\\s*${key}:\\s*\")([^\"]*)(\")`, 'm');
217
- if (!pattern.test(content)) {
496
+ const sectionPattern = new RegExp(`(^${section}:[^\\n]*\\n)([\\s\\S]*?)(?=^\\S|\\Z)`, 'm');
497
+ const match = sectionPattern.exec(content);
498
+ if (!match) {
218
499
  return content;
219
500
  }
220
- return content.replace(pattern, `$1${value}$3`);
501
+ const block = match[2];
502
+ const keyPattern = new RegExp(`(^\\s*${key}:\\s*\")([^\"]*)(\")`, 'm');
503
+ if (!keyPattern.test(block)) {
504
+ return content;
505
+ }
506
+ const updatedBlock = block.replace(keyPattern, `$1${value}$3`);
507
+ return (
508
+ content.slice(0, match.index) +
509
+ match[1] +
510
+ updatedBlock +
511
+ content.slice(match.index + match[0].length)
512
+ );
221
513
  }
222
514
 
223
515
  function updateFile(filePath, updater) {
@@ -239,6 +531,35 @@ function shouldMakeExecutable(filePath) {
239
531
  return normalized.includes('/.husky/');
240
532
  }
241
533
 
534
+ function runVerify(targetDir) {
535
+ console.log('Running verification...');
536
+ runCmd('pnpm', ['install'], { cwd: targetDir });
537
+ runCmd('pnpm', ['-C', 'apps/web', 'build'], { cwd: targetDir });
538
+ runCmd('go', ['-C', 'apps/server', 'build', './...'], { cwd: targetDir });
539
+ console.log('Verification complete.');
540
+ }
541
+
542
+ function runCmd(command, commandArgs, options) {
543
+ execFileSync(resolveCmd(command), commandArgs, { stdio: 'inherit', ...options });
544
+ }
545
+
546
+ function resolveCmd(command) {
547
+ if (os.platform() !== 'win32') {
548
+ return command;
549
+ }
550
+ if (
551
+ path.isAbsolute(command) ||
552
+ command.includes('\\') ||
553
+ command.includes('/') ||
554
+ command.endsWith('.cmd') ||
555
+ command.endsWith('.exe') ||
556
+ command.endsWith('.bat')
557
+ ) {
558
+ return command;
559
+ }
560
+ return `${command}.cmd`;
561
+ }
562
+
242
563
  function parseArgs(argv) {
243
564
  const out = {};
244
565
  for (let i = 0; i < argv.length; i++) {
@@ -247,6 +568,22 @@ function parseArgs(argv) {
247
568
  out.help = true;
248
569
  continue;
249
570
  }
571
+ if (arg === '--local') {
572
+ out.local = true;
573
+ continue;
574
+ }
575
+ if (arg === '--with-example') {
576
+ out.withExample = true;
577
+ continue;
578
+ }
579
+ if (arg === '--with-ci') {
580
+ out.withCi = true;
581
+ continue;
582
+ }
583
+ if (arg === '--verify') {
584
+ out.verify = true;
585
+ continue;
586
+ }
250
587
 
251
588
  const db = readValueOption(arg, argv, i, '--db');
252
589
  if (db) {