@robsun/create-keystone-app 0.1.18 → 0.2.1

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 (45) hide show
  1. package/README.md +18 -5
  2. package/{bin → dist}/create-keystone-app.js +114 -116
  3. package/dist/create-module.js +638 -0
  4. package/package.json +11 -7
  5. package/template/README.md +17 -13
  6. package/template/apps/server/config.example.yaml +0 -1
  7. package/template/apps/server/config.yaml +0 -1
  8. package/template/apps/server/internal/modules/example/api/handler/item_handler.go +162 -0
  9. package/template/apps/server/internal/modules/example/bootstrap/migrations/item.go +21 -0
  10. package/template/apps/server/internal/modules/example/bootstrap/seeds/item.go +33 -0
  11. package/template/apps/server/internal/modules/example/domain/models/item.go +30 -0
  12. package/template/apps/server/internal/modules/{demo → example}/domain/service/errors.go +1 -1
  13. package/template/apps/server/internal/modules/example/domain/service/item_service.go +110 -0
  14. package/template/apps/server/internal/modules/example/infra/repository/item_repository.go +49 -0
  15. package/template/apps/server/internal/modules/example/module.go +55 -17
  16. package/template/apps/server/internal/modules/manifest.go +0 -2
  17. package/template/apps/web/src/app.config.ts +1 -1
  18. package/template/apps/web/src/main.tsx +1 -3
  19. package/template/apps/web/src/modules/example/help/faq.md +23 -0
  20. package/template/apps/web/src/modules/example/help/items.md +26 -0
  21. package/template/apps/web/src/modules/example/help/overview.md +18 -4
  22. package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +227 -0
  23. package/template/apps/web/src/modules/example/routes.tsx +33 -10
  24. package/template/apps/web/src/modules/example/services/exampleItems.ts +32 -0
  25. package/template/apps/web/src/modules/example/types.ts +10 -0
  26. package/template/docs/CONVENTIONS.md +44 -0
  27. package/template/docs/GETTING_STARTED.md +54 -0
  28. package/template/package.json +2 -1
  29. package/template/scripts/check-modules.js +7 -1
  30. package/template/apps/server/internal/modules/demo/api/handler/task_handler.go +0 -152
  31. package/template/apps/server/internal/modules/demo/bootstrap/migrations/task.go +0 -21
  32. package/template/apps/server/internal/modules/demo/bootstrap/seeds/task.go +0 -33
  33. package/template/apps/server/internal/modules/demo/domain/models/task.go +0 -30
  34. package/template/apps/server/internal/modules/demo/domain/service/task_service.go +0 -95
  35. package/template/apps/server/internal/modules/demo/infra/repository/task_repository.go +0 -49
  36. package/template/apps/server/internal/modules/demo/module.go +0 -91
  37. package/template/apps/server/internal/modules/example/handlers.go +0 -19
  38. package/template/apps/web/src/modules/demo/help/overview.md +0 -12
  39. package/template/apps/web/src/modules/demo/index.ts +0 -7
  40. package/template/apps/web/src/modules/demo/pages/DemoTasksPage.tsx +0 -185
  41. package/template/apps/web/src/modules/demo/routes.tsx +0 -43
  42. package/template/apps/web/src/modules/demo/services/demoTasks.ts +0 -28
  43. package/template/apps/web/src/modules/demo/types.ts +0 -9
  44. package/template/apps/web/src/modules/example/pages/ExamplePage.tsx +0 -41
  45. package/template/apps/web/src/modules/example/services/api.ts +0 -8
package/README.md CHANGED
@@ -5,20 +5,32 @@
5
5
  npx @robsun/create-keystone-app <dir> [options]
6
6
  pnpm dlx @robsun/create-keystone-app <dir> [options]
7
7
  ```
8
+ 不传 options 会进入交互式向导。
8
9
 
9
10
  ## 选项
10
11
  - `<dir>`:目标目录(必填),可为新目录名或 `.`(当前目录)。
11
- - `--profile <starter|full>`:模板档位,默认 `starter`。
12
- - `--demo` / `--no-demo`:是否包含 Demo 模块(`full` 默认包含)。
13
12
  - `--db <sqlite|postgres>`:数据库驱动(默认 `sqlite`)。
14
13
  - `--queue <memory|redis>`:队列驱动(默认 `memory`)。
15
14
  - `--storage <local|s3>`:存储驱动(默认 `local`)。
16
15
 
17
16
  ## 示例
18
17
  ```bash
19
- npx @robsun/create-keystone-app my-app --profile=full --db=postgres --queue=redis
18
+ npx @robsun/create-keystone-app my-app --db=postgres --queue=redis --storage=s3
20
19
  ```
21
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
+
22
34
  ## 初始化后操作
23
35
  ```bash
24
36
  cd <dir>
@@ -28,6 +40,7 @@ pnpm web:dev
28
40
  pnpm dev
29
41
  ```
30
42
 
31
- ## 端口与 Demo
43
+ ## 端口与 Example
32
44
  - Web 默认端口:`3000`;后端默认端口:`8080`。
33
- - Demo API:`/api/v1/demo/tasks`(仅在包含 Demo 时可用)。
45
+ - Example API:`/api/v1/example/items`。
46
+ - 权限:`example:item:view`、`example:item:manage`。
@@ -1,78 +1,140 @@
1
1
  #!/usr/bin/env node
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
+ const readline = require('readline/promises');
5
+ const { stdin: input, stdout: output } = require('process');
4
6
 
5
7
  const usage = [
6
8
  'Usage: create-keystone-app <dir> [options]',
7
9
  '',
8
10
  'Options:',
9
- ' --profile <starter|full> Template profile (default: starter)',
10
- ' --demo Include demo module',
11
- ' --no-demo Exclude demo module',
12
11
  ' --db <sqlite|postgres> Database driver (default: sqlite)',
13
12
  ' --queue <memory|redis> Queue driver (default: memory)',
14
13
  ' --storage <local|s3> Storage driver (default: local)',
15
14
  ' -h, --help Show help',
15
+ '',
16
+ 'Run without options to use guided prompts.',
16
17
  ].join('\n');
17
18
 
18
- const args = parseArgs(process.argv.slice(2));
19
- if (args.help) {
20
- console.log(usage);
21
- process.exit(0);
22
- }
23
-
24
- if (!args.target) {
25
- console.error(usage);
19
+ main().catch((err) => {
20
+ const message = err instanceof Error ? err.message : String(err);
21
+ console.error(message);
26
22
  process.exit(1);
27
- }
23
+ });
28
24
 
29
- const profile = normalizeChoice(args.profile, ['starter', 'full'], 'profile') || 'starter';
30
- const db = normalizeChoice(args.db, ['sqlite', 'postgres'], 'db') || 'sqlite';
31
- const queue = normalizeChoice(args.queue, ['memory', 'redis'], 'queue') || 'memory';
32
- const storage = normalizeChoice(args.storage, ['local', 's3'], 'storage') || 'local';
25
+ async function main() {
26
+ const args = parseArgs(process.argv.slice(2));
27
+ if (args.help) {
28
+ console.log(usage);
29
+ return;
30
+ }
33
31
 
34
- let includeDemo = profile === 'full';
35
- if (args.demo === true) {
36
- includeDemo = true;
37
- }
38
- if (args.demo === false) {
39
- includeDemo = false;
40
- }
32
+ const canPrompt = isInteractive();
33
+ const rl = canPrompt ? readline.createInterface({ input, output }) : null;
34
+ let db;
35
+ let queue;
36
+ let storage;
37
+
38
+ try {
39
+ if (!args.target) {
40
+ if (!rl) {
41
+ console.error(usage);
42
+ process.exit(1);
43
+ }
44
+ args.target = await promptText(rl, 'Project directory', 'keystone-app');
45
+ }
41
46
 
42
- const targetDir = path.resolve(process.cwd(), args.target);
43
- const targetName = args.target === '.'
44
- ? path.basename(process.cwd())
45
- : path.basename(targetDir);
47
+ db = normalizeChoice(args.db, ['sqlite', 'postgres'], 'db') || null;
48
+ queue = normalizeChoice(args.queue, ['memory', 'redis'], 'queue') || null;
49
+ storage = normalizeChoice(args.storage, ['local', 's3'], 'storage') || null;
46
50
 
47
- if (fs.existsSync(targetDir)) {
48
- const entries = fs.readdirSync(targetDir);
49
- if (entries.length > 0) {
50
- console.error(`Target directory is not empty: ${targetDir}`);
51
- process.exit(1);
51
+ if (!db) {
52
+ db = rl ? await promptSelect(rl, 'Select database driver', ['sqlite', 'postgres'], 0) : 'sqlite';
53
+ }
54
+ if (!queue) {
55
+ queue = rl ? await promptSelect(rl, 'Select queue driver', ['memory', 'redis'], 0) : 'memory';
56
+ }
57
+ if (!storage) {
58
+ storage = rl ? await promptSelect(rl, 'Select storage driver', ['local', 's3'], 0) : 'local';
59
+ }
60
+ } finally {
61
+ if (rl) {
62
+ rl.close();
63
+ }
52
64
  }
53
- } else {
54
- fs.mkdirSync(targetDir, { recursive: true });
55
- }
56
65
 
57
- const templateDir = path.resolve(__dirname, '..', 'template');
58
- copyDir(templateDir, targetDir, {
59
- '__APP_NAME__': normalizePackageName(targetName),
60
- '__RAW_NAME__': targetName,
61
- });
66
+ const targetDir = path.resolve(process.cwd(), args.target);
67
+ const targetName = args.target === '.'
68
+ ? path.basename(process.cwd())
69
+ : path.basename(targetDir);
70
+
71
+ if (fs.existsSync(targetDir)) {
72
+ const entries = fs.readdirSync(targetDir);
73
+ if (entries.length > 0) {
74
+ console.error(`Target directory is not empty: ${targetDir}`);
75
+ process.exit(1);
76
+ }
77
+ } else {
78
+ fs.mkdirSync(targetDir, { recursive: true });
79
+ }
80
+
81
+ const templateDir = path.resolve(__dirname, '..', 'template');
82
+ copyDir(templateDir, targetDir, {
83
+ '__APP_NAME__': normalizePackageName(targetName),
84
+ '__RAW_NAME__': targetName,
85
+ });
86
+
87
+ applyConfigOptions(targetDir, { db, queue, storage });
88
+
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');
96
+ }
62
97
 
63
- applyConfigOptions(targetDir, { db, queue, storage });
98
+ function isInteractive() {
99
+ return Boolean(input.isTTY && output.isTTY);
100
+ }
64
101
 
65
- if (!includeDemo) {
66
- stripDemo(targetDir);
102
+ async function promptText(rl, label, defaultValue) {
103
+ while (true) {
104
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
105
+ const answer = await rl.question(`${label}${suffix}: `);
106
+ const trimmed = answer.trim();
107
+ if (trimmed) {
108
+ return trimmed;
109
+ }
110
+ if (defaultValue) {
111
+ return defaultValue;
112
+ }
113
+ console.log('Please enter a value.');
114
+ }
67
115
  }
68
116
 
69
- console.log(`Created ${targetName}`);
70
- console.log('Next steps:');
71
- console.log(` cd ${args.target}`);
72
- console.log(' pnpm install');
73
- console.log(' pnpm server:dev');
74
- console.log(' pnpm web:dev');
75
- console.log(' pnpm dev');
117
+ async function promptSelect(rl, label, choices, defaultIndex) {
118
+ while (true) {
119
+ const options = choices.map((choice, index) => ` ${index + 1}) ${choice}`).join('\n');
120
+ const answer = await rl.question(
121
+ `${label}\n${options}\nSelect an option [${defaultIndex + 1}]: `
122
+ );
123
+ const normalized = answer.trim().toLowerCase();
124
+ if (!normalized) {
125
+ return choices[defaultIndex];
126
+ }
127
+ const numeric = Number(normalized);
128
+ if (Number.isInteger(numeric) && numeric >= 1 && numeric <= choices.length) {
129
+ return choices[numeric - 1];
130
+ }
131
+ const match = choices.find((choice) => choice.toLowerCase() === normalized);
132
+ if (match) {
133
+ return match;
134
+ }
135
+ console.log('Invalid selection. Try again.');
136
+ }
137
+ }
76
138
 
77
139
  function normalizePackageName(name) {
78
140
  const cleaned = String(name || '')
@@ -120,49 +182,6 @@ function shouldSkipDir(name) {
120
182
  return name === 'node_modules' || name === '.git';
121
183
  }
122
184
 
123
- function stripDemo(targetDir) {
124
- removePath(path.join(targetDir, 'apps', 'web', 'src', 'modules', 'demo'));
125
- removePath(path.join(targetDir, 'apps', 'server', 'internal', 'modules', 'demo'));
126
-
127
- updateFile(path.join(targetDir, 'apps', 'web', 'src', 'main.tsx'), (content) =>
128
- content.replace(/^\s*import ['"]\.\/modules\/demo['"];?\r?\n/m, '')
129
- );
130
- updateFile(path.join(targetDir, 'apps', 'web', 'src', 'app.config.ts'), (content) =>
131
- content.replace(/,\s*['"]demo['"]/, '')
132
- );
133
- updateFile(path.join(targetDir, 'apps', 'server', 'config.yaml'), (content) =>
134
- content.replace(/\r?\n\s*-\s*['"]demo['"]\s*/g, '')
135
- );
136
- updateFile(path.join(targetDir, 'apps', 'server', 'config.example.yaml'), (content) =>
137
- content.replace(/\r?\n\s*-\s*['"]demo['"]\s*/g, '')
138
- );
139
- updateFile(path.join(targetDir, 'README.md'), (content) =>
140
- content.replace(/<!-- DEMO_START -->[\s\S]*?<!-- DEMO_END -->\s*/m, '')
141
- );
142
-
143
- const manifestPath = path.join(
144
- targetDir,
145
- 'apps',
146
- 'server',
147
- 'internal',
148
- 'modules',
149
- 'manifest.go'
150
- );
151
- const manifest = `package modules
152
-
153
- import (
154
- \texample "__APP_NAME__/apps/server/internal/modules/example"
155
- )
156
-
157
- // RegisterAll wires the module registry for this app.
158
- func RegisterAll() {
159
- \tClear()
160
- \tRegister(example.NewModule())
161
- }
162
- `;
163
- fs.writeFileSync(manifestPath, manifest, 'utf8');
164
- }
165
-
166
185
  function applyConfigOptions(targetDir, options) {
167
186
  const configFiles = [
168
187
  path.join(targetDir, 'apps', 'server', 'config.yaml'),
@@ -172,7 +191,6 @@ function applyConfigOptions(targetDir, options) {
172
191
  for (const filePath of configFiles) {
173
192
  updateFile(filePath, (content) => applyYamlOptions(content, options));
174
193
  }
175
-
176
194
  }
177
195
 
178
196
  function applyYamlOptions(content, options) {
@@ -202,12 +220,6 @@ function updateYamlSectionValue(content, section, key, value) {
202
220
  return content.replace(pattern, `$1${value}$3`);
203
221
  }
204
222
 
205
- function removePath(target) {
206
- if (fs.existsSync(target)) {
207
- fs.rmSync(target, { recursive: true, force: true });
208
- }
209
- }
210
-
211
223
  function updateFile(filePath, updater) {
212
224
  if (!fs.existsSync(filePath)) {
213
225
  return;
@@ -228,28 +240,14 @@ function shouldMakeExecutable(filePath) {
228
240
  }
229
241
 
230
242
  function parseArgs(argv) {
231
- const out = { demo: null };
243
+ const out = {};
232
244
  for (let i = 0; i < argv.length; i++) {
233
245
  const arg = argv[i];
234
- if (arg === '--demo') {
235
- out.demo = true;
236
- continue;
237
- }
238
- if (arg === '--no-demo') {
239
- out.demo = false;
240
- continue;
241
- }
242
246
  if (arg === '--help' || arg === '-h') {
243
247
  out.help = true;
244
248
  continue;
245
249
  }
246
250
 
247
- const profile = readValueOption(arg, argv, i, '--profile');
248
- if (profile) {
249
- out.profile = profile.value;
250
- i += profile.skip;
251
- continue;
252
- }
253
251
  const db = readValueOption(arg, argv, i, '--db');
254
252
  if (db) {
255
253
  out.db = db.value;
@@ -315,4 +313,4 @@ function fail(message) {
315
313
  console.error(message);
316
314
  console.error(usage);
317
315
  process.exit(1);
318
- }
316
+ }