create-fluxstack 1.18.0 → 1.19.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 (54) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/app/client/src/App.tsx +7 -7
  3. package/app/client/src/components/AppLayout.tsx +60 -23
  4. package/app/client/src/components/ColorWheel.tsx +195 -0
  5. package/app/client/src/components/DemoPage.tsx +5 -3
  6. package/app/client/src/components/LiveUploadWidget.tsx +1 -1
  7. package/app/client/src/components/ThemePicker.tsx +307 -0
  8. package/app/client/src/config/theme.config.ts +127 -0
  9. package/app/client/src/hooks/useThemeClock.ts +66 -0
  10. package/app/client/src/index.css +193 -0
  11. package/app/client/src/lib/theme-clock.ts +201 -0
  12. package/app/client/src/live/AuthDemo.tsx +9 -9
  13. package/app/client/src/live/CounterDemo.tsx +10 -10
  14. package/app/client/src/live/FormDemo.tsx +8 -8
  15. package/app/client/src/live/PingPongDemo.tsx +10 -10
  16. package/app/client/src/live/RoomChatDemo.tsx +10 -10
  17. package/app/client/src/live/SharedCounterDemo.tsx +5 -5
  18. package/app/client/src/pages/ApiTestPage.tsx +5 -5
  19. package/app/client/src/pages/HomePage.tsx +12 -12
  20. package/app/server/index.ts +8 -0
  21. package/app/server/live/auto-generated-components.ts +1 -1
  22. package/app/server/live/rooms/ChatRoom.ts +13 -8
  23. package/app/server/routes/index.ts +20 -10
  24. package/core/build/index.ts +1 -1
  25. package/core/cli/command-registry.ts +1 -1
  26. package/core/cli/commands/build.ts +25 -6
  27. package/core/cli/commands/plugin-deps.ts +1 -2
  28. package/core/cli/generators/plugin.ts +433 -581
  29. package/core/framework/server.ts +34 -8
  30. package/core/index.ts +6 -5
  31. package/core/plugins/index.ts +71 -199
  32. package/core/plugins/types.ts +76 -461
  33. package/core/server/index.ts +1 -1
  34. package/core/utils/logger/startup-banner.ts +26 -4
  35. package/core/utils/version.ts +6 -6
  36. package/create-fluxstack.ts +216 -107
  37. package/package.json +108 -107
  38. package/tsconfig.json +2 -1
  39. package/app/client/.live-stubs/LiveAdminPanel.js +0 -15
  40. package/app/client/.live-stubs/LiveCounter.js +0 -9
  41. package/app/client/.live-stubs/LiveForm.js +0 -11
  42. package/app/client/.live-stubs/LiveLocalCounter.js +0 -8
  43. package/app/client/.live-stubs/LivePingPong.js +0 -10
  44. package/app/client/.live-stubs/LiveRoomChat.js +0 -11
  45. package/app/client/.live-stubs/LiveSharedCounter.js +0 -10
  46. package/app/client/.live-stubs/LiveUpload.js +0 -15
  47. package/core/plugins/config.ts +0 -356
  48. package/core/plugins/dependency-manager.ts +0 -481
  49. package/core/plugins/discovery.ts +0 -379
  50. package/core/plugins/executor.ts +0 -353
  51. package/core/plugins/manager.ts +0 -645
  52. package/core/plugins/module-resolver.ts +0 -227
  53. package/core/plugins/registry.ts +0 -913
  54. package/vitest.config.live.ts +0 -69
@@ -1,581 +1,433 @@
1
- import type { Generator } from "./index"
2
- import type { GeneratorContext, GeneratorOptions, Template } from "./types"
3
- import { templateEngine } from "./template-engine"
4
- import { buildLogger } from "@core/utils/build-logger"
5
- import { join } from "path"
6
-
7
- export class PluginGenerator implements Generator {
8
- name = 'plugin'
9
- description = 'Generate a new FluxStack plugin'
10
-
11
- async generate(context: GeneratorContext, options: GeneratorOptions): Promise<void> {
12
- const template = this.getTemplate(options.template)
13
-
14
- if (template.hooks?.beforeGenerate) {
15
- await template.hooks.beforeGenerate(context, options)
16
- }
17
-
18
- const files = await templateEngine.processTemplate(template, context, options)
19
-
20
- if (options.dryRun) {
21
- buildLogger.info(`\nšŸ“‹ Would generate plugin '${options.name}':\n`)
22
- for (const file of files) {
23
- buildLogger.info(`${file.action === 'create' ? 'šŸ“„' : 'āœļø'} ${file.path}`)
24
- }
25
- return
26
- }
27
-
28
- await templateEngine.generateFiles(files, options.dryRun)
29
-
30
- if (template.hooks?.afterGenerate) {
31
- const filePaths = files.map(f => f.path)
32
- await template.hooks.afterGenerate(context, options, filePaths)
33
- }
34
-
35
- buildLogger.success(`Generated plugin '${options.name}' with ${files.length} files`)
36
- buildLogger.info(`\nšŸ“¦ Next steps:`)
37
- buildLogger.info(` 1. Configure plugin in plugins/${options.name}/config/index.ts`)
38
- buildLogger.info(` 2. Set environment variables (optional): ${options.name.toUpperCase().replace(/-/g, '_')}_*`)
39
- buildLogger.info(` 3. Implement your plugin logic in plugins/${options.name}/index.ts`)
40
- buildLogger.info(` 4. Add server-side code in plugins/${options.name}/server/ (optional)`)
41
- buildLogger.info(` 5. Add client-side code in plugins/${options.name}/client/ (optional)`)
42
- buildLogger.info(` 6. Run: bun run dev`)
43
- }
44
-
45
- private getTemplate(templateName?: string): Template {
46
- switch (templateName) {
47
- case 'full':
48
- return this.getFullTemplate()
49
- case 'server':
50
- return this.getServerOnlyTemplate()
51
- case 'client':
52
- return this.getClientOnlyTemplate()
53
- default:
54
- return this.getBasicTemplate()
55
- }
56
- }
57
-
58
- private getBasicTemplate(): Template {
59
- return {
60
- name: 'basic-plugin',
61
- description: 'Basic plugin template with essential files',
62
- files: [
63
- {
64
- path: 'plugins/{{name}}/package.json',
65
- content: `{
66
- "name": "@fluxstack/{{name}}-plugin",
67
- "version": "1.0.0",
68
- "description": "{{description}}",
69
- "main": "index.ts",
70
- "types": "index.ts",
71
- "exports": {
72
- ".": {
73
- "import": "./index.ts",
74
- "types": "./index.ts"
75
- },
76
- "./config": {
77
- "import": "./config/index.ts",
78
- "types": "./config/index.ts"
79
- }
80
- },
81
- "keywords": [
82
- "fluxstack",
83
- "plugin",
84
- "{{name}}",
85
- "typescript"
86
- ],
87
- "author": "FluxStack Developer",
88
- "license": "MIT",
89
- "peerDependencies": {},
90
- "dependencies": {},
91
- "devDependencies": {
92
- "typescript": "^5.0.0"
93
- },
94
- "fluxstack": {
95
- "plugin": true,
96
- "version": "^1.0.0",
97
- "hooks": [
98
- "setup",
99
- "onServerStart"
100
- ],
101
- "category": "utility",
102
- "tags": ["{{name}}"]
103
- }
104
- }
105
- `
106
- },
107
- {
108
- path: 'plugins/{{name}}/config/index.ts',
109
- content: `/**
110
- * {{pascalName}} Plugin Configuration
111
- * Declarative config using FluxStack config system
112
- */
113
-
114
- import { defineConfig, config } from '@fluxstack/config'
115
-
116
- const {{camelName}}ConfigSchema = {
117
- // Enable/disable plugin
118
- enabled: config.boolean('{{constantName}}_ENABLED', true),
119
-
120
- // Add your configuration options here
121
- // Example:
122
- // apiKey: config.string('{{constantName}}_API_KEY', ''),
123
- // timeout: config.number('{{constantName}}_TIMEOUT', 5000),
124
- // debug: config.boolean('{{constantName}}_DEBUG', false),
125
- } as const
126
-
127
- export const {{camelName}}Config = defineConfig({{camelName}}ConfigSchema)
128
-
129
- export type {{pascalName}}Config = typeof {{camelName}}Config
130
- export default {{camelName}}Config
131
- `
132
- },
133
- {
134
- path: 'plugins/{{name}}/index.ts',
135
- content: `import type { ErrorContext, FluxStack, PluginContext, RequestContext, ResponseContext } from "@core/plugins/types"
136
- // āœ… Plugin imports its own configuration
137
- import { {{camelName}}Config } from './config'
138
-
139
- /**
140
- * {{pascalName}} Plugin
141
- * {{description}}
142
- */
143
- export class {{pascalName}}Plugin implements FluxStack.Plugin {
144
- name = '{{name}}'
145
- version = '1.0.0'
146
-
147
- /**
148
- * Setup hook - called when plugin is loaded
149
- */
150
- async setup(context: PluginContext): Promise<void> {
151
- // Check if plugin is enabled
152
- if (!{{camelName}}Config.enabled) {
153
- context.logger.info(\`[{{name}}] Plugin disabled by configuration\`)
154
- return
155
- }
156
-
157
- console.log(\`[{{name}}] Plugin initialized\`)
158
-
159
- // Add your initialization logic here
160
- // Example: Register middleware, setup database connections, etc.
161
- }
162
-
163
- /**
164
- * Server start hook - called when server starts
165
- */
166
- async onServerStart?(context: PluginContext): Promise<void> {
167
- if (!{{camelName}}Config.enabled) return
168
-
169
- console.log(\`[{{name}}] Server started\`)
170
-
171
- // Add logic to run when server starts
172
- }
173
-
174
- /**
175
- * Request hook - called on each request
176
- */
177
- async onRequest?(context: RequestContext): Promise<void> {
178
- if (!{{camelName}}Config.enabled) return
179
-
180
- // Add request processing logic
181
- }
182
-
183
- /**
184
- * Response hook - called on each response
185
- */
186
- async onResponse?(context: ResponseContext): Promise<void> {
187
- if (!{{camelName}}Config.enabled) return
188
-
189
- // Add response processing logic
190
- }
191
-
192
- /**
193
- * Error hook - called when errors occur
194
- */
195
- async onError?(context: ErrorContext): Promise<void> {
196
- console.error(\`[{{name}}] Error:\`, context.error)
197
-
198
- // Add error handling logic
199
- }
200
- }
201
-
202
- // Export plugin instance
203
- export default new {{pascalName}}Plugin()
204
- `
205
- },
206
- {
207
- path: 'plugins/{{name}}/README.md',
208
- content: `# {{pascalName}} Plugin
209
-
210
- {{description}}
211
-
212
- ## Installation
213
-
214
- This plugin is already in your FluxStack project. To use it:
215
-
216
- 1. Make sure the plugin is enabled in your configuration
217
- 2. Install any additional dependencies (if needed):
218
- \`\`\`bash
219
- bun run cli plugin:deps install
220
- \`\`\`
221
-
222
- ## Configuration
223
-
224
- This plugin uses FluxStack's declarative configuration system. Configure it by editing \`config/index.ts\` or by setting environment variables:
225
-
226
- \`\`\`bash
227
- # Enable/disable plugin
228
- {{constantName}}_ENABLED=true
229
-
230
- # Add your environment variables here
231
- # Example:
232
- # {{constantName}}_API_KEY=your-api-key
233
- # {{constantName}}_TIMEOUT=5000
234
- \`\`\`
235
-
236
- The plugin's configuration is located in \`plugins/{{name}}/config/index.ts\` and is self-contained, making the plugin fully portable.
237
-
238
- ## Usage
239
-
240
- \`\`\`typescript
241
- // The plugin is automatically loaded by FluxStack
242
- // It imports its own configuration from ./config
243
- \`\`\`
244
-
245
- ## API
246
-
247
- Document your plugin's API here.
248
-
249
- ## Hooks
250
-
251
- This plugin uses the following hooks:
252
- - \`setup\`: Initialize plugin resources
253
- - \`onServerStart\`: Run when server starts (optional)
254
- - \`onRequest\`: Process incoming requests (optional)
255
- - \`onResponse\`: Process outgoing responses (optional)
256
- - \`onError\`: Handle errors (optional)
257
-
258
- ## Development
259
-
260
- To modify this plugin:
261
-
262
- 1. Edit \`config/index.ts\` to add configuration options
263
- 2. Edit \`index.ts\` with your logic
264
- 3. Test with: \`bun run dev\`
265
-
266
- ## License
267
-
268
- MIT
269
- `
270
- }
271
- ]
272
- }
273
- }
274
-
275
- private getServerOnlyTemplate(): Template {
276
- const basic = this.getBasicTemplate()
277
- return {
278
- ...basic,
279
- name: 'server-plugin',
280
- description: 'Plugin with server-side code',
281
- files: [
282
- {
283
- path: 'plugins/{{name}}/package.json',
284
- content: `{
285
- "name": "@fluxstack/{{name}}-plugin",
286
- "version": "1.0.0",
287
- "description": "{{description}}",
288
- "main": "index.ts",
289
- "types": "index.ts",
290
- "exports": {
291
- ".": {
292
- "import": "./index.ts",
293
- "types": "./index.ts"
294
- },
295
- "./config": {
296
- "import": "./config/index.ts",
297
- "types": "./config/index.ts"
298
- },
299
- "./server": {
300
- "import": "./server/index.ts",
301
- "types": "./server/index.ts"
302
- }
303
- },
304
- "keywords": [
305
- "fluxstack",
306
- "plugin",
307
- "{{name}}",
308
- "server",
309
- "typescript"
310
- ],
311
- "author": "FluxStack Developer",
312
- "license": "MIT",
313
- "peerDependencies": {
314
- "elysia": "^1.0.0"
315
- },
316
- "dependencies": {},
317
- "devDependencies": {
318
- "typescript": "^5.0.0"
319
- },
320
- "fluxstack": {
321
- "plugin": true,
322
- "version": "^1.0.0",
323
- "hooks": [
324
- "setup",
325
- "onServerStart",
326
- "onRequest",
327
- "onResponse"
328
- ],
329
- "category": "utility",
330
- "tags": ["{{name}}", "server"]
331
- }
332
- }
333
- `
334
- },
335
- ...basic.files.slice(1), // Skip package.json from basic
336
- {
337
- path: 'plugins/{{name}}/server/index.ts',
338
- content: `/**
339
- * Server-side logic for {{pascalName}} plugin
340
- */
341
-
342
- export class {{pascalName}}Service {
343
- async initialize() {
344
- console.log(\`[{{name}}] Server service initialized\`)
345
- }
346
-
347
- // Add your server-side methods here
348
- }
349
-
350
- export const {{camelName}}Service = new {{pascalName}}Service()
351
- `
352
- }
353
- ]
354
- }
355
- }
356
-
357
- private getClientOnlyTemplate(): Template {
358
- const basic = this.getBasicTemplate()
359
- return {
360
- ...basic,
361
- name: 'client-plugin',
362
- description: 'Plugin with client-side code',
363
- files: [
364
- {
365
- path: 'plugins/{{name}}/package.json',
366
- content: `{
367
- "name": "@fluxstack/{{name}}-plugin",
368
- "version": "1.0.0",
369
- "description": "{{description}}",
370
- "main": "index.ts",
371
- "types": "index.ts",
372
- "exports": {
373
- ".": {
374
- "import": "./index.ts",
375
- "types": "./index.ts"
376
- },
377
- "./config": {
378
- "import": "./config/index.ts",
379
- "types": "./config/index.ts"
380
- },
381
- "./client": {
382
- "import": "./client/index.ts",
383
- "types": "./client/index.ts"
384
- }
385
- },
386
- "keywords": [
387
- "fluxstack",
388
- "plugin",
389
- "{{name}}",
390
- "react",
391
- "client",
392
- "typescript"
393
- ],
394
- "author": "FluxStack Developer",
395
- "license": "MIT",
396
- "peerDependencies": {
397
- "react": ">=16.8.0"
398
- },
399
- "peerDependenciesMeta": {
400
- "react": {
401
- "optional": true
402
- }
403
- },
404
- "dependencies": {},
405
- "devDependencies": {
406
- "@types/react": "^18.0.0",
407
- "typescript": "^5.0.0"
408
- },
409
- "fluxstack": {
410
- "plugin": true,
411
- "version": "^1.0.0",
412
- "hooks": [
413
- "setup",
414
- "onServerStart"
415
- ],
416
- "category": "utility",
417
- "tags": ["{{name}}", "client", "react"]
418
- }
419
- }
420
- `
421
- },
422
- ...basic.files.slice(1), // Skip package.json from basic
423
- {
424
- path: 'plugins/{{name}}/client/index.ts',
425
- content: `/**
426
- * Client-side logic for {{pascalName}} plugin
427
- */
428
-
429
- export class {{pascalName}}Client {
430
- initialize() {
431
- console.log(\`[{{name}}] Client initialized\`)
432
- }
433
-
434
- // Add your client-side methods here
435
- }
436
-
437
- export const {{camelName}}Client = new {{pascalName}}Client()
438
- `
439
- }
440
- ]
441
- }
442
- }
443
-
444
- private getFullTemplate(): Template {
445
- const basic = this.getBasicTemplate()
446
- const server = this.getServerOnlyTemplate()
447
- const client = this.getClientOnlyTemplate()
448
-
449
- return {
450
- ...basic,
451
- name: 'full-plugin',
452
- description: 'Complete plugin with server and client code',
453
- files: [
454
- {
455
- path: 'plugins/{{name}}/package.json',
456
- content: `{
457
- "name": "@fluxstack/{{name}}-plugin",
458
- "version": "1.0.0",
459
- "description": "{{description}}",
460
- "main": "index.ts",
461
- "types": "index.ts",
462
- "exports": {
463
- ".": {
464
- "import": "./index.ts",
465
- "types": "./index.ts"
466
- },
467
- "./config": {
468
- "import": "./config/index.ts",
469
- "types": "./config/index.ts"
470
- },
471
- "./server": {
472
- "import": "./server/index.ts",
473
- "types": "./server/index.ts"
474
- },
475
- "./client": {
476
- "import": "./client/index.ts",
477
- "types": "./client/index.ts"
478
- },
479
- "./types": {
480
- "import": "./types.ts",
481
- "types": "./types.ts"
482
- }
483
- },
484
- "keywords": [
485
- "fluxstack",
486
- "plugin",
487
- "{{name}}",
488
- "react",
489
- "server",
490
- "client",
491
- "typescript"
492
- ],
493
- "author": "FluxStack Developer",
494
- "license": "MIT",
495
- "peerDependencies": {
496
- "react": ">=16.8.0",
497
- "elysia": "^1.0.0"
498
- },
499
- "peerDependenciesMeta": {
500
- "react": {
501
- "optional": true
502
- }
503
- },
504
- "dependencies": {},
505
- "devDependencies": {
506
- "@types/react": "^18.0.0",
507
- "typescript": "^5.0.0"
508
- },
509
- "fluxstack": {
510
- "plugin": true,
511
- "version": "^1.0.0",
512
- "hooks": [
513
- "setup",
514
- "onServerStart",
515
- "onRequest",
516
- "onResponse",
517
- "onError"
518
- ],
519
- "category": "utility",
520
- "tags": ["{{name}}", "server", "client", "react"]
521
- }
522
- }
523
- `
524
- },
525
- ...basic.files.slice(1), // Skip package.json from basic
526
- {
527
- path: 'plugins/{{name}}/server/index.ts',
528
- content: `/**
529
- * Server-side logic for {{pascalName}} plugin
530
- */
531
-
532
- export class {{pascalName}}Service {
533
- async initialize() {
534
- console.log(\`[{{name}}] Server service initialized\`)
535
- }
536
-
537
- // Add your server-side methods here
538
- }
539
-
540
- export const {{camelName}}Service = new {{pascalName}}Service()
541
- `
542
- },
543
- {
544
- path: 'plugins/{{name}}/client/index.ts',
545
- content: `/**
546
- * Client-side logic for {{pascalName}} plugin
547
- */
548
-
549
- export class {{pascalName}}Client {
550
- initialize() {
551
- console.log(\`[{{name}}] Client initialized\`)
552
- }
553
-
554
- // Add your client-side methods here
555
- }
556
-
557
- export const {{camelName}}Client = new {{pascalName}}Client()
558
- `
559
- },
560
- {
561
- path: 'plugins/{{name}}/types.ts',
562
- content: `/**
563
- * Type definitions for {{pascalName}} plugin
564
- */
565
-
566
- // Config types are exported from ./config/index.ts
567
- // Import them like: import type { {{pascalName}}Config } from './config'
568
-
569
- export interface {{pascalName}}Options {
570
- // Add your runtime options types here
571
- }
572
-
573
- export interface {{pascalName}}Event {
574
- // Add your event types here
575
- }
576
- `
577
- }
578
- ]
579
- }
580
- }
581
- }
1
+ import type { Generator } from "./index"
2
+ import type { GeneratorContext, GeneratorOptions, Template } from "./types"
3
+ import { templateEngine } from "./template-engine"
4
+ import { buildLogger } from "@core/utils/build-logger"
5
+
6
+ /**
7
+ * Plugin scaffolder for `bun run cli make:plugin <name>`.
8
+ *
9
+ * Emits project-local plugin skeletons into `plugins/<name>/`. The
10
+ * generated code follows the CURRENT plugin model:
11
+ *
12
+ * - Plugin is a plain object literal implementing `Plugin` from
13
+ * `@fluxstack/plugin-kit`. NOT a class.
14
+ * - No `plugin.json`, no auto-discovery, no `fluxstack` manifest
15
+ * block in `package.json`. Plugins live inside the project and
16
+ * are imported via relative path, then registered via
17
+ * `framework.use(myPlugin)` in `app/server/index.ts`.
18
+ * - `package.json` for a project-local plugin is optional and
19
+ * minimal. It is NOT an npm package — it's just a way to let
20
+ * the plugin pin its own dev-time dependencies if needed. Most
21
+ * plugins will not need one at all.
22
+ * - The generator does NOT auto-edit `app/server/index.ts`. After
23
+ * scaffolding, it prints explicit instructions for the user to
24
+ * import and `.use()` the new plugin themselves.
25
+ */
26
+ export class PluginGenerator implements Generator {
27
+ name = 'plugin'
28
+ description = 'Generate a new FluxStack plugin'
29
+
30
+ async generate(context: GeneratorContext, options: GeneratorOptions): Promise<void> {
31
+ const template = this.getTemplate(options.template)
32
+
33
+ // Derive identifier names that already include "Plugin" as a suffix,
34
+ // WITHOUT duplicating it when the user's plugin name already ends
35
+ // with -plugin. Examples:
36
+ // 'my-plugin' → pluginIdent = myPlugin
37
+ // 'csrf-protection' → pluginIdent = csrfProtectionPlugin
38
+ // 'my-test-plugin' → pluginIdent = myTestPlugin
39
+ //
40
+ // These are injected into the template variables via options (the
41
+ // template engine spreads options into the variable bag before
42
+ // processing {{placeholders}}), so the templates can use
43
+ // {{pluginIdent}} / {{pluginIdentPascal}} directly.
44
+ const camelName = this.toCamelCase(options.name)
45
+ const pascalName = this.toPascalCase(options.name)
46
+ const alreadyEndsWithPlugin = /plugin$/i.test(options.name)
47
+ const pluginIdent = alreadyEndsWithPlugin ? camelName : `${camelName}Plugin`
48
+ const pluginIdentPascal = alreadyEndsWithPlugin ? pascalName : `${pascalName}Plugin`
49
+
50
+ const enrichedOptions: GeneratorOptions = {
51
+ ...options,
52
+ pluginIdent,
53
+ pluginIdentPascal,
54
+ }
55
+
56
+ if (template.hooks?.beforeGenerate) {
57
+ await template.hooks.beforeGenerate(context, enrichedOptions)
58
+ }
59
+
60
+ const files = await templateEngine.processTemplate(template, context, enrichedOptions)
61
+
62
+ if (options.dryRun) {
63
+ buildLogger.info(`\nšŸ“‹ Would generate plugin '${options.name}':\n`)
64
+ for (const file of files) {
65
+ buildLogger.info(`${file.action === 'create' ? 'šŸ“„' : 'āœļø'} ${file.path}`)
66
+ }
67
+ return
68
+ }
69
+
70
+ await templateEngine.generateFiles(files, options.dryRun)
71
+
72
+ if (template.hooks?.afterGenerate) {
73
+ const filePaths = files.map(f => f.path)
74
+ await template.hooks.afterGenerate(context, enrichedOptions, filePaths)
75
+ }
76
+
77
+ buildLogger.success(`Generated plugin '${options.name}' with ${files.length} files`)
78
+
79
+ const importPath = `../../plugins/${options.name}`
80
+
81
+ buildLogger.info(`\nšŸ“ Next steps:`)
82
+ buildLogger.info(` 1. Implement your plugin logic in plugins/${options.name}/index.ts`)
83
+ buildLogger.info(` 2. Configure it (optional) in plugins/${options.name}/config/index.ts`)
84
+ buildLogger.info(``)
85
+ buildLogger.info(` 3. Register the plugin in app/server/index.ts:`)
86
+ buildLogger.info(``)
87
+ buildLogger.info(` import { ${pluginIdent} } from '${importPath}'`)
88
+ buildLogger.info(``)
89
+ buildLogger.info(` framework.use(${pluginIdent})`)
90
+ buildLogger.info(``)
91
+ buildLogger.info(` āš ļø Plugins are NOT auto-discovered. They must be explicitly`)
92
+ buildLogger.info(` registered via framework.use() to be loaded at runtime.`)
93
+ buildLogger.info(``)
94
+ buildLogger.info(` 4. Run: bun run dev`)
95
+ }
96
+
97
+ // Local copies of the same case helpers the template engine uses
98
+ // internally. We need them here to precompute pluginIdent /
99
+ // pluginIdentPascal before the template engine runs, so we can
100
+ // inject them into the variable bag via enrichedOptions.
101
+ private toCamelCase(str: string): string {
102
+ return str.replace(/[-_\s]+(.)?/g, (_, c: string | undefined) =>
103
+ c ? c.toUpperCase() : '',
104
+ )
105
+ }
106
+
107
+ private toPascalCase(str: string): string {
108
+ const camel = this.toCamelCase(str)
109
+ return camel.charAt(0).toUpperCase() + camel.slice(1)
110
+ }
111
+
112
+ private getTemplate(templateName?: string): Template {
113
+ switch (templateName) {
114
+ case 'full':
115
+ return this.getFullTemplate()
116
+ case 'server':
117
+ return this.getServerOnlyTemplate()
118
+ case 'client':
119
+ return this.getClientOnlyTemplate()
120
+ default:
121
+ return this.getBasicTemplate()
122
+ }
123
+ }
124
+
125
+ private getBasicTemplate(): Template {
126
+ return {
127
+ name: 'basic-plugin',
128
+ description: 'Basic plugin template (plain object literal)',
129
+ files: [
130
+ {
131
+ path: 'plugins/{{name}}/config/index.ts',
132
+ content: `/**
133
+ * {{pascalName}} Plugin Configuration
134
+ * Declarative config using @fluxstack/config
135
+ */
136
+
137
+ import { defineConfig, config } from '@fluxstack/config'
138
+
139
+ const {{camelName}}ConfigSchema = {
140
+ // Enable/disable plugin
141
+ enabled: config.boolean('{{constantName}}_ENABLED', true),
142
+
143
+ // Add your configuration options here. Example:
144
+ // apiKey: config.string('{{constantName}}_API_KEY', ''),
145
+ // timeout: config.number('{{constantName}}_TIMEOUT', 5000),
146
+ // debug: config.boolean('{{constantName}}_DEBUG', false),
147
+ } as const
148
+
149
+ export const {{camelName}}Config = defineConfig({{camelName}}ConfigSchema)
150
+
151
+ export type {{pascalName}}Config = typeof {{camelName}}Config
152
+ export default {{camelName}}Config
153
+ `
154
+ },
155
+ {
156
+ path: 'plugins/{{name}}/index.ts',
157
+ content: `import type {
158
+ Plugin,
159
+ PluginContext,
160
+ RequestContext,
161
+ ResponseContext,
162
+ ErrorContext,
163
+ } from '@fluxstack/plugin-kit'
164
+ import { {{camelName}}Config } from './config'
165
+
166
+ /**
167
+ * {{pascalName}} Plugin
168
+ * {{description}}
169
+ *
170
+ * Plugins are plain object literals implementing the \`Plugin\`
171
+ * interface from @fluxstack/plugin-kit. To enable this plugin,
172
+ * import it in app/server/index.ts and pass it to framework.use():
173
+ *
174
+ * import { {{pluginIdent}} } from '../../plugins/{{name}}'
175
+ *
176
+ * framework.use({{pluginIdent}})
177
+ */
178
+ export const {{pluginIdent}}: Plugin = {
179
+ name: '{{name}}',
180
+ version: '1.0.0',
181
+ description: '{{description}}',
182
+
183
+ /**
184
+ * Setup hook — runs once during framework.start().
185
+ * Use it to initialize resources, mount Elysia routes via
186
+ * context.app, register client-side hooks via context.clientHooks,
187
+ * etc.
188
+ */
189
+ setup: async (context: PluginContext) => {
190
+ if (!{{camelName}}Config.enabled) {
191
+ context.logger.info('[{{name}}] disabled by configuration')
192
+ return
193
+ }
194
+
195
+ context.logger.info('[{{name}}] initialized')
196
+
197
+ // Add your initialization logic here
198
+ },
199
+
200
+ /**
201
+ * Server start hook — runs once after the HTTP server starts listening.
202
+ */
203
+ onServerStart: async (context: PluginContext) => {
204
+ if (!{{camelName}}Config.enabled) return
205
+ context.logger.debug('[{{name}}] server started')
206
+ },
207
+
208
+ /**
209
+ * Request hook — runs on every incoming request.
210
+ * Remove this if you don't need per-request behavior.
211
+ */
212
+ onRequest: async (_context: RequestContext) => {
213
+ if (!{{camelName}}Config.enabled) return
214
+ // Add request processing logic
215
+ },
216
+
217
+ /**
218
+ * Response hook — runs on every outgoing response.
219
+ * Remove this if you don't need per-response behavior.
220
+ */
221
+ onResponse: async (_context: ResponseContext) => {
222
+ if (!{{camelName}}Config.enabled) return
223
+ // Add response processing logic
224
+ },
225
+
226
+ /**
227
+ * Error hook — runs when an error is thrown from a handler.
228
+ */
229
+ onError: async (context: ErrorContext) => {
230
+ console.error('[{{name}}] error:', context.error.message)
231
+ },
232
+ }
233
+
234
+ export default {{pluginIdent}}
235
+ `
236
+ },
237
+ {
238
+ path: 'plugins/{{name}}/README.md',
239
+ content: `# {{pascalName}} Plugin
240
+
241
+ {{description}}
242
+
243
+ ## Enabling this plugin
244
+
245
+ Plugins are **not** auto-discovered. To enable this plugin, import
246
+ it and register it via \`framework.use()\` in \`app/server/index.ts\`:
247
+
248
+ \`\`\`typescript
249
+ import { {{pluginIdent}} } from '../../plugins/{{name}}'
250
+
251
+ const framework = new FluxStackFramework()
252
+ .use({{pluginIdent}})
253
+ \`\`\`
254
+
255
+ ## Configuration
256
+
257
+ This plugin uses FluxStack's declarative config system
258
+ (\`@fluxstack/config\`). Tweak defaults in
259
+ \`plugins/{{name}}/config/index.ts\`, or set environment variables:
260
+
261
+ \`\`\`bash
262
+ # Enable/disable the plugin
263
+ {{constantName}}_ENABLED=true
264
+
265
+ # Add your own environment variables here. Example:
266
+ # {{constantName}}_API_KEY=your-api-key
267
+ # {{constantName}}_TIMEOUT=5000
268
+ \`\`\`
269
+
270
+ The config is self-contained in the plugin folder, so this plugin
271
+ is fully portable — copy the folder into another FluxStack project,
272
+ register it via \`.use()\`, and it works.
273
+
274
+ ## Hooks this plugin uses
275
+
276
+ - \`setup\` — initialize resources at startup
277
+ - \`onServerStart\` — runs after the HTTP server binds the port
278
+ - \`onRequest\` — runs on every incoming request (remove if unused)
279
+ - \`onResponse\` — runs on every outgoing response (remove if unused)
280
+ - \`onError\` — runs when a handler throws
281
+
282
+ See \`LLMD/reference/plugin-hooks.md\` for the full list of hooks
283
+ and their signatures.
284
+
285
+ ## Development
286
+
287
+ 1. Edit \`config/index.ts\` to add configuration options
288
+ 2. Edit \`index.ts\` to implement your plugin logic
289
+ 3. Register the plugin via \`framework.use()\` (see above)
290
+ 4. Run: \`bun run dev\`
291
+
292
+ ## License
293
+
294
+ MIT
295
+ `
296
+ }
297
+ ]
298
+ }
299
+ }
300
+
301
+ private getServerOnlyTemplate(): Template {
302
+ const basic = this.getBasicTemplate()
303
+ return {
304
+ ...basic,
305
+ name: 'server-plugin',
306
+ description: 'Plugin with server-side service code',
307
+ files: [
308
+ ...basic.files,
309
+ {
310
+ path: 'plugins/{{name}}/server/index.ts',
311
+ content: `/**
312
+ * Server-side service for {{pascalName}} plugin
313
+ *
314
+ * Import this from plugins/{{name}}/index.ts and wire it up inside
315
+ * the plugin's setup hook. Keeping services as named exports (not
316
+ * default-exported singletons) makes them easier to test.
317
+ */
318
+
319
+ export class {{pascalName}}Service {
320
+ async initialize(): Promise<void> {
321
+ console.log('[{{name}}] server service initialized')
322
+ }
323
+
324
+ // Add your server-side methods here
325
+ }
326
+
327
+ export const {{camelName}}Service = new {{pascalName}}Service()
328
+ `
329
+ }
330
+ ]
331
+ }
332
+ }
333
+
334
+ private getClientOnlyTemplate(): Template {
335
+ const basic = this.getBasicTemplate()
336
+ return {
337
+ ...basic,
338
+ name: 'client-plugin',
339
+ description: 'Plugin with client-side code',
340
+ files: [
341
+ ...basic.files,
342
+ {
343
+ path: 'plugins/{{name}}/client/index.ts',
344
+ content: `/**
345
+ * Client-side code for {{pascalName}} plugin
346
+ *
347
+ * This runs in the browser. If you need the plugin's server-side
348
+ * setup hook to inject a <script> tag or a fetch interceptor into
349
+ * the client, use \`context.clientHooks.register()\` from within
350
+ * the plugin's setup() in plugins/{{name}}/index.ts.
351
+ */
352
+
353
+ export class {{pascalName}}Client {
354
+ initialize(): void {
355
+ console.log('[{{name}}] client initialized')
356
+ }
357
+
358
+ // Add your client-side methods here
359
+ }
360
+
361
+ export const {{camelName}}Client = new {{pascalName}}Client()
362
+ `
363
+ }
364
+ ]
365
+ }
366
+ }
367
+
368
+ private getFullTemplate(): Template {
369
+ const basic = this.getBasicTemplate()
370
+
371
+ return {
372
+ ...basic,
373
+ name: 'full-plugin',
374
+ description: 'Complete plugin with server + client code',
375
+ files: [
376
+ ...basic.files,
377
+ {
378
+ path: 'plugins/{{name}}/server/index.ts',
379
+ content: `/**
380
+ * Server-side service for {{pascalName}} plugin
381
+ */
382
+
383
+ export class {{pascalName}}Service {
384
+ async initialize(): Promise<void> {
385
+ console.log('[{{name}}] server service initialized')
386
+ }
387
+
388
+ // Add your server-side methods here
389
+ }
390
+
391
+ export const {{camelName}}Service = new {{pascalName}}Service()
392
+ `
393
+ },
394
+ {
395
+ path: 'plugins/{{name}}/client/index.ts',
396
+ content: `/**
397
+ * Client-side code for {{pascalName}} plugin
398
+ */
399
+
400
+ export class {{pascalName}}Client {
401
+ initialize(): void {
402
+ console.log('[{{name}}] client initialized')
403
+ }
404
+
405
+ // Add your client-side methods here
406
+ }
407
+
408
+ export const {{camelName}}Client = new {{pascalName}}Client()
409
+ `
410
+ },
411
+ {
412
+ path: 'plugins/{{name}}/types.ts',
413
+ content: `/**
414
+ * Type definitions for {{pascalName}} plugin
415
+ */
416
+
417
+ // Config types are exported from ./config/index.ts
418
+ // Import them like:
419
+ // import type { {{pascalName}}Config } from './config'
420
+
421
+ export interface {{pascalName}}Options {
422
+ // Add your runtime options types here
423
+ }
424
+
425
+ export interface {{pascalName}}Event {
426
+ // Add your event types here
427
+ }
428
+ `
429
+ }
430
+ ]
431
+ }
432
+ }
433
+ }