create-fullstack-scaffold 0.1.0 → 0.1.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.
package/dist/cli/index.js CHANGED
@@ -2,9 +2,13 @@
2
2
  import { program } from 'commander';
3
3
  import { hc } from 'hono/client';
4
4
  import { z } from '@hono/zod-openapi';
5
- import fs from 'fs';
6
- import path from 'path';
5
+ import fs, { readdirSync, statSync, existsSync, readFileSync } from 'fs';
6
+ import path2, { join } from 'path';
7
7
  import os from 'os';
8
+ import { fileURLToPath, pathToFileURL } from 'url';
9
+ import fs2 from 'fs-extra';
10
+ import chalk from 'chalk';
11
+ import ora from 'ora';
8
12
 
9
13
  function createRPCClient(baseUrl) {
10
14
  return hc(baseUrl);
@@ -136,8 +140,8 @@ function schemaToOptions(schema) {
136
140
  }
137
141
  return options;
138
142
  }
139
- function pathToApiCall(client, _method, path2) {
140
- const pathParts = path2.split("/").filter(Boolean);
143
+ function pathToApiCall(client, _method, path3) {
144
+ const pathParts = path3.split("/").filter(Boolean);
141
145
  let current = client.api;
142
146
  for (const part of pathParts) {
143
147
  if (part.startsWith("{") && part.endsWith("}")) {
@@ -340,8 +344,8 @@ function registerNotificationCommands(program2) {
340
344
  logger2.info(JSON.stringify(data, null, 2));
341
345
  });
342
346
  }
343
- var CONFIG_DIR = path.join(os.homedir(), ".biomimic");
344
- var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
347
+ var CONFIG_DIR = path2.join(os.homedir(), ".biomimic");
348
+ var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
345
349
  function loadConfig() {
346
350
  try {
347
351
  if (fs.existsSync(CONFIG_FILE)) {
@@ -424,9 +428,2007 @@ function registerModules(program2) {
424
428
  registerNotificationCommands(program2);
425
429
  registerConfigCommands(program2);
426
430
  }
431
+ var tsImportFn;
432
+ async function getTsImport() {
433
+ if (tsImportFn) return tsImportFn;
434
+ const { tsImport } = await import('tsx/esm/api');
435
+ tsImportFn = tsImport;
436
+ return tsImportFn;
437
+ }
438
+ async function loadManifests(templateDir) {
439
+ const tsImport = await getTsImport();
440
+ const serverDir = join(templateDir, "src", "server");
441
+ const modules = /* @__PURE__ */ new Map();
442
+ const entries = readdirSync(serverDir);
443
+ const moduleDirs = entries.filter(
444
+ (e) => e.startsWith("module-") && statSync(join(serverDir, e)).isDirectory()
445
+ );
446
+ const parentURL = pathToFileURL(join(serverDir, "dummy.ts")).href;
447
+ for (const dir of moduleDirs) {
448
+ const manifestPath = join(serverDir, dir, "module.ts");
449
+ if (!existsSync(manifestPath)) continue;
450
+ try {
451
+ const mod = await tsImport(manifestPath, { parentURL });
452
+ const manifest = mod.default;
453
+ if (!manifest || !manifest.name) {
454
+ console.error(`\u274C Invalid manifest in ${dir}: missing name`);
455
+ continue;
456
+ }
457
+ modules.set(manifest.name, manifest);
458
+ } catch (err) {
459
+ console.error(`\u274C Failed to load manifest from ${dir}:`, err);
460
+ }
461
+ }
462
+ return modules;
463
+ }
464
+ async function loadPresets(templateDir) {
465
+ const configPath = join(templateDir, "modules.config.ts");
466
+ if (!existsSync(configPath)) {
467
+ return [getDefaultPreset()];
468
+ }
469
+ try {
470
+ const tsImport = await getTsImport();
471
+ const parentURL = pathToFileURL(configPath).href;
472
+ const mod = await tsImport(configPath, { parentURL });
473
+ if (mod.TEMPLATE_PRESETS) {
474
+ return mod.TEMPLATE_PRESETS;
475
+ }
476
+ } catch (err) {
477
+ console.error("\u274C Failed to load presets:", err);
478
+ }
479
+ return [getDefaultPreset()];
480
+ }
481
+ function getDefaultPreset() {
482
+ return {
483
+ id: "fullstack-admin",
484
+ name: "Full Admin",
485
+ description: "All modules included",
486
+ modules: [
487
+ "todos",
488
+ "chat",
489
+ "notifications",
490
+ "file",
491
+ "captcha",
492
+ "permission",
493
+ "admin",
494
+ "order",
495
+ "ticket",
496
+ "dispute",
497
+ "content"
498
+ ]
499
+ };
500
+ }
501
+ function resolvePreset(preset, allManifests) {
502
+ const modules = /* @__PURE__ */ new Map();
503
+ const toProcess = [...preset.modules];
504
+ const processed = /* @__PURE__ */ new Set();
505
+ while (toProcess.length > 0) {
506
+ const name = toProcess.shift();
507
+ if (processed.has(name)) continue;
508
+ processed.add(name);
509
+ const manifest = allManifests.get(name);
510
+ if (manifest) {
511
+ modules.set(name, manifest);
512
+ for (const dep of manifest.dependsOn) {
513
+ if (!processed.has(dep)) {
514
+ toProcess.push(dep);
515
+ }
516
+ }
517
+ }
518
+ }
519
+ let hasSSE = false;
520
+ let hasWebSocket = false;
521
+ let hasAdmin = false;
522
+ for (const manifest of modules.values()) {
523
+ if (manifest.hasSSE) hasSSE = true;
524
+ if (manifest.hasWebSocket) hasWebSocket = true;
525
+ if (manifest.adminPages && manifest.adminPages.length > 0) hasAdmin = true;
526
+ if (manifest.routes.admin && manifest.routes.admin.length > 0) hasAdmin = true;
527
+ }
528
+ return {
529
+ preset,
530
+ modules,
531
+ hasAdmin,
532
+ hasClient: true,
533
+ hasSSE,
534
+ hasWebSocket,
535
+ hasPermission: modules.has("permission"),
536
+ hasCaptcha: modules.has("captcha")
537
+ };
538
+ }
539
+ function getDbSchemaFiles(resolved) {
540
+ const files = [];
541
+ for (const [, manifest] of resolved.modules) {
542
+ if (manifest.dbSchemas) {
543
+ files.push(...manifest.dbSchemas.files);
544
+ }
545
+ }
546
+ return files;
547
+ }
548
+ function getClientPages(resolved) {
549
+ const pages = [];
550
+ for (const [, manifest] of resolved.modules) {
551
+ if (manifest.clientPages) {
552
+ pages.push(...manifest.clientPages);
553
+ }
554
+ }
555
+ return pages;
556
+ }
557
+ function getAdminPages(resolved) {
558
+ const pages = [];
559
+ for (const [, manifest] of resolved.modules) {
560
+ if (manifest.adminPages) {
561
+ pages.push(...manifest.adminPages);
562
+ }
563
+ }
564
+ return pages;
565
+ }
566
+ function getDefaultRoute(resolved) {
567
+ const pages = getClientPages(resolved);
568
+ if (pages.length > 0) {
569
+ return pages[0].route;
570
+ }
571
+ return "/";
572
+ }
573
+
574
+ // src/generators/file-filter.ts
575
+ function getExcludePatterns(resolved, allManifests) {
576
+ const excludes = [];
577
+ for (const [name, manifest] of allManifests) {
578
+ if (resolved.modules.has(name)) continue;
579
+ excludes.push(`src/server/module-${name}`);
580
+ if (manifest.sharedSchemas) {
581
+ excludes.push(`src/shared/modules/${manifest.sharedSchemas.path}`);
582
+ if (manifest.sharedSchemas.additionalPaths) {
583
+ for (const extra of manifest.sharedSchemas.additionalPaths) {
584
+ excludes.push(`src/shared/modules/${extra}`);
585
+ }
586
+ }
587
+ }
588
+ if (manifest.dbSchemas) {
589
+ for (const file of manifest.dbSchemas.files) {
590
+ excludes.push(`src/server/db/schema/${file}.ts`);
591
+ }
592
+ }
593
+ if (manifest.clientPages) {
594
+ for (const page of manifest.clientPages) {
595
+ excludes.push(`src/client/pages/${page.name}.tsx`);
596
+ excludes.push(`src/client/pages/__tests__/${page.name}.test.tsx`);
597
+ }
598
+ }
599
+ if (manifest.clientStores) {
600
+ for (const store of manifest.clientStores) {
601
+ excludes.push(`src/client/stores/${store}.ts`);
602
+ excludes.push(`src/client/stores/__tests__/${store}.test.ts`);
603
+ }
604
+ }
605
+ if (manifest.adminPages) {
606
+ for (const page of manifest.adminPages) {
607
+ excludes.push(`src/admin/pages/${page.name}.tsx`);
608
+ excludes.push(`src/admin/pages/__tests__/${page.name}.test.tsx`);
609
+ }
610
+ }
611
+ if (manifest.providesMiddleware) {
612
+ for (const mw of manifest.providesMiddleware) {
613
+ excludes.push(`src/server/middleware/${mw.name}.ts`);
614
+ excludes.push(`src/server/middleware/__tests__/${mw.name}.test.ts`);
615
+ }
616
+ }
617
+ if (name === "notifications") {
618
+ excludes.push("src/cli/modules/notification");
619
+ }
620
+ }
621
+ if (!resolved.modules.has("admin")) {
622
+ excludes.push("src/client/components/AuthButton.tsx");
623
+ excludes.push("src/admin");
624
+ excludes.push("admin.html");
625
+ excludes.push("auth-inject.html");
626
+ excludes.push("src/client/components/__tests__/AuthButton.test.tsx");
627
+ excludes.push("src/cli");
628
+ excludes.push("src/server/utils/auth.ts");
629
+ }
630
+ if (!resolved.hasPermission) {
631
+ excludes.push("src/server/utils/permission-utils.ts");
632
+ excludes.push("src/server/middleware/__tests__/auth-simple.test.ts");
633
+ excludes.push("src/server/middleware/__tests__/auth.test.ts");
634
+ excludes.push("src/server/middleware/__tests__/error-response-format.test.ts");
635
+ excludes.push("src/server/utils/__tests__/auth.test.ts");
636
+ } else if (!resolved.modules.has("admin")) {
637
+ excludes.push("src/server/middleware/__tests__/error-response-format.test.ts");
638
+ }
639
+ if (!resolved.modules.has("captcha")) {
640
+ excludes.push("src/server/utils/__tests__/captcha.test.ts");
641
+ }
642
+ return excludes;
643
+ }
644
+ function getGeneratedFiles(resolved) {
645
+ const files = [
646
+ "src/server/route-registry.ts",
647
+ "src/server/db/schema/index.ts",
648
+ "src/client/App.tsx",
649
+ "src/client/components/Navigation.tsx",
650
+ "src/shared/modules/index.ts",
651
+ "src/shared/schemas/index.ts",
652
+ "src/server/middleware/index.ts",
653
+ "src/client/components/index.ts"
654
+ ];
655
+ if (resolved.modules.has("admin")) {
656
+ files.push("src/admin/App.tsx");
657
+ files.push("src/cli/modules/index.ts");
658
+ }
659
+ if (!resolved.hasPermission) {
660
+ files.push("src/server/middleware/auth.ts");
661
+ files.push("src/server/utils/auth.ts");
662
+ }
663
+ if (!resolved.modules.has("admin") && resolved.hasPermission) {
664
+ files.push("src/server/utils/auth.ts");
665
+ }
666
+ files.push("src/server/app.ts");
667
+ if (!resolved.modules.has("admin")) {
668
+ files.push("vite.config.ts");
669
+ }
670
+ const seedModules = ["order", "ticket", "dispute", "content"];
671
+ if (seedModules.some((m) => !resolved.modules.has(m)) || !resolved.hasPermission) {
672
+ files.push("src/server/db/init.ts");
673
+ }
674
+ return files;
675
+ }
676
+
677
+ // src/generators/route-registry.ts
678
+ function generateRouteRegistry(resolved) {
679
+ const imports = [
680
+ `import { OpenAPIHono } from '@hono/zod-openapi'`,
681
+ `import { rateLimitMiddleware } from './middleware/rate-limit'`
682
+ ];
683
+ const clientRoutes = [];
684
+ const adminRoutes = [];
685
+ const moduleEntries = [...resolved.modules.entries()];
686
+ for (const [name, manifest] of moduleEntries) {
687
+ const moduleDir = `module-${name}`;
688
+ if (manifest.routes.client) {
689
+ const { importPath, exportName } = manifest.routes.client;
690
+ imports.push(
691
+ `import { ${exportName} } from './${moduleDir}/${importPath.replace(/^\.\//, "")}'`
692
+ );
693
+ clientRoutes.push(` .route('/api', ${exportName})`);
694
+ }
695
+ if (manifest.routes.admin) {
696
+ for (const route of manifest.routes.admin) {
697
+ const { importPath, exportName } = route;
698
+ imports.push(
699
+ `import { ${exportName} } from './${moduleDir}/${importPath.replace(/^\.\//, "")}'`
700
+ );
701
+ adminRoutes.push(` .route('/api', ${exportName})`);
702
+ }
703
+ }
704
+ }
705
+ let content = imports.join("\n") + "\n\n";
706
+ content += `const apiRateLimit = rateLimitMiddleware({
707
+ `;
708
+ content += ` windowMs: 60 * 1000,
709
+ `;
710
+ content += ` max: 100,
711
+ `;
712
+ content += `})
713
+
714
+ `;
715
+ if (clientRoutes.length > 0) {
716
+ content += `// client API routes
717
+ `;
718
+ content += `export const clientApiRoutes = new OpenAPIHono()
719
+ `;
720
+ content += ` .use('*', apiRateLimit)
721
+ `;
722
+ content += clientRoutes.join("\n") + "\n\n";
723
+ } else {
724
+ content += `// No client modules selected
725
+ `;
726
+ content += `export const clientApiRoutes = new OpenAPIHono()
727
+
728
+ `;
729
+ }
730
+ if (adminRoutes.length > 0) {
731
+ content += `// admin API routes
732
+ `;
733
+ content += `export const adminApiRoutes = new OpenAPIHono()
734
+ `;
735
+ content += adminRoutes.join("\n") + "\n\n";
736
+ } else {
737
+ content += `// No admin modules selected
738
+ `;
739
+ content += `export const adminApiRoutes = new OpenAPIHono()
740
+
741
+ `;
742
+ }
743
+ content += `export type ClientApiRoutes = typeof clientApiRoutes
744
+ `;
745
+ content += `export type AdminApiRoutes = typeof adminApiRoutes
746
+ `;
747
+ return content;
748
+ }
749
+
750
+ // src/generators/client-app.ts
751
+ function generateClientApp(resolved) {
752
+ const pages = getClientPages(resolved);
753
+ const defaultRoute = getDefaultRoute(resolved);
754
+ const imports = [
755
+ `import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'`,
756
+ `import { Layout } from './Layout'`
757
+ ];
758
+ for (const page of pages) {
759
+ imports.push(`import { ${page.name} } from './pages/${page.name}'`);
760
+ }
761
+ const routeElements = [];
762
+ routeElements.push(
763
+ ` <Route path="/" element={<Navigate to="${defaultRoute}" replace />} />`
764
+ );
765
+ for (const page of pages) {
766
+ routeElements.push(
767
+ ` <Route path="${page.route}" element={<${page.name} />} />`
768
+ );
769
+ }
770
+ return `${imports.join("\n")}
771
+
772
+ export function App() {
773
+ return (
774
+ <BrowserRouter>
775
+ <Layout>
776
+ <Routes>
777
+ ${routeElements.join("\n")}
778
+ </Routes>
779
+ </Layout>
780
+ </BrowserRouter>
781
+ )
782
+ }
783
+ `;
784
+ }
785
+
786
+ // src/generators/client-navigation.ts
787
+ var DEFAULT_ICON = "Circle";
788
+ var ICON_MAP = {
789
+ TodoPage: "CheckCircle",
790
+ NotificationPage: "Bell",
791
+ WebSocketPage: "Plug",
792
+ ContentListPage: "FileText"
793
+ };
794
+ function generateClientNavigation(resolved) {
795
+ const pages = getClientPages(resolved).filter((p) => !p.route.includes(":"));
796
+ const iconsNeeded = /* @__PURE__ */ new Set();
797
+ iconsNeeded.add("Rocket");
798
+ iconsNeeded.add("Github");
799
+ for (const page of pages) {
800
+ const icon = ICON_MAP[page.name] || DEFAULT_ICON;
801
+ iconsNeeded.add(icon);
802
+ }
803
+ const routeKeys = [];
804
+ const routeEntries = [];
805
+ for (const page of pages) {
806
+ const key = page.route.replace(/^\//, "").replace(/\//g, "-");
807
+ routeKeys.push(`'${key}'`);
808
+ const icon = ICON_MAP[page.name] || DEFAULT_ICON;
809
+ const label = key === "todos" ? "Todo List" : key === "notifications" ? "Notifications" : key === "websocket" ? "WebSocket" : key.charAt(0).toUpperCase() + key.slice(1);
810
+ const safeKey = /^[a-zA-Z0-9_]+$/.test(key) ? key : `'${key}'`;
811
+ routeEntries.push(` ${safeKey}: { label: '${label}', icon: ${icon}, path: '${page.route}' },`);
812
+ }
813
+ const iconsStr = [...iconsNeeded].join(", ");
814
+ const authButtonImport = resolved.modules.has("admin") ? `
815
+ import { AuthButton } from './AuthButton'` : "";
816
+ const authButtonElement = resolved.modules.has("admin") ? `
817
+ <AuthButton />` : "";
818
+ return `import { NavLink } from 'react-router-dom'
819
+ import { ${iconsStr} } from 'lucide-react'${authButtonImport}
820
+
821
+ type RouteKey = ${routeKeys.join(" | ")}
822
+
823
+ const routes: Record<
824
+ RouteKey,
825
+ { label: string; icon: React.FC<{ className?: string }>; path: string }
826
+ > = {
827
+ ${routeEntries.join("\n")}
828
+ }
829
+
830
+ export const Navigation: React.FC = () => {
831
+ return (
832
+ <nav className="bg-white border-b border-gray-200 sticky top-0 z-50" data-testid="app-nav">
833
+ <div className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
834
+ <div className="flex items-center gap-8">
835
+ <h1
836
+ className="text-xl font-bold text-gray-900 flex items-center gap-2"
837
+ data-testid="app-title"
838
+ >
839
+ <Rocket className="w-6 h-6 text-blue-500" />
840
+ Biomimic App
841
+ </h1>
842
+ <div className="flex items-center gap-1">
843
+ {(Object.keys(routes) as RouteKey[]).map(route => {
844
+ const Icon = routes[route].icon
845
+ return (
846
+ <NavLink
847
+ key={route}
848
+ to={routes[route].path}
849
+ data-testid={\`nav-\${route}-button\`}
850
+ className={({ isActive }) =>
851
+ \`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors \${
852
+ isActive ? 'bg-blue-50 text-blue-600' : 'text-gray-600 hover:bg-gray-100'
853
+ }\`
854
+ }
855
+ >
856
+ <Icon className="w-4 h-4" />
857
+ {routes[route].label}
858
+ </NavLink>
859
+ )
860
+ })}
861
+ </div>
862
+ </div>
863
+ <div className="flex items-center gap-4">${authButtonElement}
864
+ <a
865
+ href="https://github.com"
866
+ target="_blank"
867
+ rel="noopener noreferrer"
868
+ data-testid="github-link"
869
+ className="text-gray-400 hover:text-gray-600 transition-colors"
870
+ >
871
+ <Github className="w-5 h-5" />
872
+ </a>
873
+ </div>
874
+ </div>
875
+ </nav>
876
+ )
877
+ }
878
+ `;
879
+ }
880
+
881
+ // src/generators/admin-app.ts
882
+ function generateAdminApp(resolved) {
883
+ if (!resolved.hasAdmin) return null;
884
+ const pages = getAdminPages(resolved) ?? [];
885
+ if (pages.length === 0) return null;
886
+ const publicPages = pages.filter((p) => p.isPublic);
887
+ const protectedPages = pages.filter((p) => !p.isPublic);
888
+ const imports = [
889
+ `import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'`,
890
+ `import { ConfigProvider } from 'antd'`,
891
+ `import { Layout } from './layouts/Layout'`
892
+ ];
893
+ for (const page of pages) {
894
+ imports.push(`import { ${page.name} } from './pages/${page.name}'`);
895
+ }
896
+ const captchaImport = resolved.hasCaptcha ? ", CaptchaModal" : "";
897
+ imports.push(`import { ProtectedRoute${captchaImport} } from './components'`);
898
+ const protectedRouteElements = protectedPages.map(
899
+ (p) => ` <Route path="${p.route}" element={<${p.name} />} />`
900
+ );
901
+ const defaultProtectedRoute = protectedPages.length > 0 ? protectedPages[0].route : "/";
902
+ const captchaElement = resolved.hasCaptcha ? `
903
+ <CaptchaModal />` : "";
904
+ const publicRouteLines = publicPages.map(
905
+ (p) => ` <Route path="${p.route}" element={<${p.name} />} />`
906
+ );
907
+ return `${imports.join("\n")}
908
+
909
+ export const App: React.FC = () => {
910
+ return (
911
+ <ConfigProvider
912
+ theme={{
913
+ token: {
914
+ colorPrimary: '#1890ff',
915
+ },
916
+ }}
917
+ >
918
+ <BrowserRouter basename="/admin">
919
+ <Routes>
920
+ ${publicRouteLines.join("\n")}
921
+ <Route
922
+ path="/*"
923
+ element={
924
+ <ProtectedRoute>
925
+ <Layout>
926
+ <Routes>
927
+ <Route path="/" element={<Navigate to="${defaultProtectedRoute}" replace />} />
928
+ ${protectedRouteElements.join("\n")}
929
+ <Route path="/system/monitor" element={<div className="p-6"><h2 className="text-xl font-semibold">System Monitor</h2><p className="text-gray-500 mt-2">Coming soon...</p></div>} />
930
+ </Routes>
931
+ </Layout>
932
+ </ProtectedRoute>
933
+ }
934
+ />
935
+ </Routes>${captchaElement}
936
+ </BrowserRouter>
937
+ </ConfigProvider>
938
+ )
939
+ }
940
+ `;
941
+ }
942
+
943
+ // src/generators/db-schema-barrel.ts
944
+ function generateDbSchemaBarrel(resolved) {
945
+ const files = getDbSchemaFiles(resolved);
946
+ const exports$1 = files.map((f) => `export * from './${f}'`);
947
+ return exports$1.join("\n") + "\n";
948
+ }
949
+
950
+ // src/generators/db-init.ts
951
+ var SEED_MODULES = [
952
+ {
953
+ module: "order",
954
+ importLine: "import { seedOrdersIfEmpty } from '../module-order/services/order-service'",
955
+ call: "seedOrdersIfEmpty()"
956
+ },
957
+ {
958
+ module: "ticket",
959
+ importLine: "import { seedTicketsIfEmpty } from '../module-ticket/services/ticket-service'",
960
+ call: "seedTicketsIfEmpty()"
961
+ },
962
+ {
963
+ module: "dispute",
964
+ importLine: "import { seedDisputesIfEmpty } from '../module-dispute/services/dispute-service'",
965
+ call: "seedDisputesIfEmpty()"
966
+ },
967
+ {
968
+ module: "content",
969
+ importLine: "import { seedContentsIfEmpty } from '../module-content/services/content-service'",
970
+ call: "seedContentsIfEmpty()"
971
+ }
972
+ ];
973
+ var INITIAL_PERMISSIONS = `const initialPermissions = [
974
+ {
975
+ id: 'perm_user_view',
976
+ code: 'user:view',
977
+ name: '\u67E5\u770B\u7528\u6237',
978
+ label: '\u67E5\u770B\u7528\u6237',
979
+ category: 'user',
980
+ sortOrder: 1,
981
+ },
982
+ {
983
+ id: 'perm_user_create',
984
+ code: 'user:create',
985
+ name: '\u521B\u5EFA\u7528\u6237',
986
+ label: '\u521B\u5EFA\u7528\u6237',
987
+ category: 'user',
988
+ sortOrder: 2,
989
+ },
990
+ {
991
+ id: 'perm_user_edit',
992
+ code: 'user:edit',
993
+ name: '\u7F16\u8F91\u7528\u6237',
994
+ label: '\u7F16\u8F91\u7528\u6237',
995
+ category: 'user',
996
+ sortOrder: 3,
997
+ },
998
+ {
999
+ id: 'perm_user_delete',
1000
+ code: 'user:delete',
1001
+ name: '\u5220\u9664\u7528\u6237',
1002
+ label: '\u5220\u9664\u7528\u6237',
1003
+ category: 'user',
1004
+ sortOrder: 4,
1005
+ },
1006
+ {
1007
+ id: 'perm_content_view',
1008
+ code: 'content:view',
1009
+ name: '\u67E5\u770B\u5185\u5BB9',
1010
+ label: '\u67E5\u770B\u5185\u5BB9',
1011
+ category: 'content',
1012
+ sortOrder: 1,
1013
+ },
1014
+ {
1015
+ id: 'perm_content_create',
1016
+ code: 'content:create',
1017
+ name: '\u521B\u5EFA\u5185\u5BB9',
1018
+ label: '\u521B\u5EFA\u5185\u5BB9',
1019
+ category: 'content',
1020
+ sortOrder: 2,
1021
+ },
1022
+ {
1023
+ id: 'perm_content_edit',
1024
+ code: 'content:edit',
1025
+ name: '\u7F16\u8F91\u5185\u5BB9',
1026
+ label: '\u7F16\u8F91\u5185\u5BB9',
1027
+ category: 'content',
1028
+ sortOrder: 3,
1029
+ },
1030
+ {
1031
+ id: 'perm_content_delete',
1032
+ code: 'content:delete',
1033
+ name: '\u5220\u9664\u5185\u5BB9',
1034
+ label: '\u5220\u9664\u5185\u5BB9',
1035
+ category: 'content',
1036
+ sortOrder: 4,
1037
+ },
1038
+ {
1039
+ id: 'perm_system_settings',
1040
+ code: 'system:settings',
1041
+ name: '\u7CFB\u7EDF\u8BBE\u7F6E',
1042
+ label: '\u7CFB\u7EDF\u8BBE\u7F6E',
1043
+ category: 'system',
1044
+ sortOrder: 1,
1045
+ },
1046
+ {
1047
+ id: 'perm_system_logs',
1048
+ code: 'system:logs',
1049
+ name: '\u7CFB\u7EDF\u65E5\u5FD7',
1050
+ label: '\u7CFB\u7EDF\u65E5\u5FD7',
1051
+ category: 'system',
1052
+ sortOrder: 2,
1053
+ },
1054
+ {
1055
+ id: 'perm_system_monitor',
1056
+ code: 'system:monitor',
1057
+ name: '\u7CFB\u7EDF\u76D1\u63A7',
1058
+ label: '\u7CFB\u7EDF\u76D1\u63A7',
1059
+ category: 'system',
1060
+ sortOrder: 3,
1061
+ },
1062
+ {
1063
+ id: 'perm_data_export',
1064
+ code: 'data:export',
1065
+ name: '\u6570\u636E\u5BFC\u51FA',
1066
+ label: '\u6570\u636E\u5BFC\u51FA',
1067
+ category: 'data',
1068
+ sortOrder: 1,
1069
+ },
1070
+ {
1071
+ id: 'perm_data_import',
1072
+ code: 'data:import',
1073
+ name: '\u6570\u636E\u5BFC\u5165',
1074
+ label: '\u6570\u636E\u5BFC\u5165',
1075
+ category: 'data',
1076
+ sortOrder: 2,
1077
+ },
1078
+ {
1079
+ id: 'perm_order_view',
1080
+ code: 'order:view',
1081
+ name: '\u67E5\u770B\u8BA2\u5355',
1082
+ label: '\u67E5\u770B\u8BA2\u5355',
1083
+ category: 'order',
1084
+ sortOrder: 1,
1085
+ },
1086
+ {
1087
+ id: 'perm_order_create',
1088
+ code: 'order:create',
1089
+ name: '\u521B\u5EFA\u8BA2\u5355',
1090
+ label: '\u521B\u5EFA\u8BA2\u5355',
1091
+ category: 'order',
1092
+ sortOrder: 2,
1093
+ },
1094
+ {
1095
+ id: 'perm_order_edit',
1096
+ code: 'order:edit',
1097
+ name: '\u7F16\u8F91\u8BA2\u5355',
1098
+ label: '\u7F16\u8F91\u8BA2\u5355',
1099
+ category: 'order',
1100
+ sortOrder: 3,
1101
+ },
1102
+ {
1103
+ id: 'perm_order_delete',
1104
+ code: 'order:delete',
1105
+ name: '\u5220\u9664\u8BA2\u5355',
1106
+ label: '\u5220\u9664\u8BA2\u5355',
1107
+ category: 'order',
1108
+ sortOrder: 4,
1109
+ },
1110
+ {
1111
+ id: 'perm_order_process',
1112
+ code: 'order:process',
1113
+ name: '\u5904\u7406\u8BA2\u5355',
1114
+ label: '\u5904\u7406\u8BA2\u5355',
1115
+ category: 'order',
1116
+ sortOrder: 5,
1117
+ },
1118
+ {
1119
+ id: 'perm_ticket_view',
1120
+ code: 'ticket:view',
1121
+ name: '\u67E5\u770B\u5DE5\u5355',
1122
+ label: '\u67E5\u770B\u5DE5\u5355',
1123
+ category: 'ticket',
1124
+ sortOrder: 1,
1125
+ },
1126
+ {
1127
+ id: 'perm_ticket_create',
1128
+ code: 'ticket:create',
1129
+ name: '\u521B\u5EFA\u5DE5\u5355',
1130
+ label: '\u521B\u5EFA\u5DE5\u5355',
1131
+ category: 'ticket',
1132
+ sortOrder: 2,
1133
+ },
1134
+ {
1135
+ id: 'perm_ticket_edit',
1136
+ code: 'ticket:edit',
1137
+ name: '\u7F16\u8F91\u5DE5\u5355',
1138
+ label: '\u7F16\u8F91\u5DE5\u5355',
1139
+ category: 'ticket',
1140
+ sortOrder: 3,
1141
+ },
1142
+ {
1143
+ id: 'perm_ticket_delete',
1144
+ code: 'ticket:delete',
1145
+ name: '\u5220\u9664\u5DE5\u5355',
1146
+ label: '\u5220\u9664\u5DE5\u5355',
1147
+ category: 'ticket',
1148
+ sortOrder: 4,
1149
+ },
1150
+ {
1151
+ id: 'perm_ticket_reply',
1152
+ code: 'ticket:reply',
1153
+ name: '\u56DE\u590D\u5DE5\u5355',
1154
+ label: '\u56DE\u590D\u5DE5\u5355',
1155
+ category: 'ticket',
1156
+ sortOrder: 5,
1157
+ },
1158
+ {
1159
+ id: 'perm_ticket_close',
1160
+ code: 'ticket:close',
1161
+ name: '\u5173\u95ED\u5DE5\u5355',
1162
+ label: '\u5173\u95ED\u5DE5\u5355',
1163
+ category: 'ticket',
1164
+ sortOrder: 6,
1165
+ },
1166
+ {
1167
+ id: 'perm_dispute_view',
1168
+ code: 'dispute:view',
1169
+ name: '\u67E5\u770B\u4E89\u8BAE',
1170
+ label: '\u67E5\u770B\u4E89\u8BAE',
1171
+ category: 'dispute',
1172
+ sortOrder: 1,
1173
+ },
1174
+ {
1175
+ id: 'perm_dispute_create',
1176
+ code: 'dispute:create',
1177
+ name: '\u521B\u5EFA\u4E89\u8BAE',
1178
+ label: '\u521B\u5EFA\u4E89\u8BAE',
1179
+ category: 'dispute',
1180
+ sortOrder: 2,
1181
+ },
1182
+ {
1183
+ id: 'perm_dispute_edit',
1184
+ code: 'dispute:edit',
1185
+ name: '\u7F16\u8F91\u4E89\u8BAE',
1186
+ label: '\u7F16\u8F91\u4E89\u8BAE',
1187
+ category: 'dispute',
1188
+ sortOrder: 3,
1189
+ },
1190
+ {
1191
+ id: 'perm_dispute_delete',
1192
+ code: 'dispute:delete',
1193
+ name: '\u5220\u9664\u4E89\u8BAE',
1194
+ label: '\u5220\u9664\u4E89\u8BAE',
1195
+ category: 'dispute',
1196
+ sortOrder: 4,
1197
+ },
1198
+ {
1199
+ id: 'perm_dispute_resolve',
1200
+ code: 'dispute:resolve',
1201
+ name: '\u89E3\u51B3\u4E89\u8BAE',
1202
+ label: '\u89E3\u51B3\u4E89\u8BAE',
1203
+ category: 'dispute',
1204
+ sortOrder: 5,
1205
+ },
1206
+ {
1207
+ id: 'perm_role_view',
1208
+ code: 'role:view',
1209
+ name: '\u67E5\u770B\u89D2\u8272',
1210
+ label: '\u67E5\u770B\u89D2\u8272',
1211
+ category: 'role',
1212
+ sortOrder: 1,
1213
+ },
1214
+ {
1215
+ id: 'perm_role_create',
1216
+ code: 'role:create',
1217
+ name: '\u521B\u5EFA\u89D2\u8272',
1218
+ label: '\u521B\u5EFA\u89D2\u8272',
1219
+ category: 'role',
1220
+ sortOrder: 2,
1221
+ },
1222
+ {
1223
+ id: 'perm_role_edit',
1224
+ code: 'role:edit',
1225
+ name: '\u7F16\u8F91\u89D2\u8272',
1226
+ label: '\u7F16\u8F91\u89D2\u8272',
1227
+ category: 'role',
1228
+ sortOrder: 3,
1229
+ },
1230
+ {
1231
+ id: 'perm_role_delete',
1232
+ code: 'role:delete',
1233
+ name: '\u5220\u9664\u89D2\u8272',
1234
+ label: '\u5220\u9664\u89D2\u8272',
1235
+ category: 'role',
1236
+ sortOrder: 4,
1237
+ },
1238
+ ]`;
1239
+ var INITIAL_ROLES = `const initialRoles = [
1240
+ {
1241
+ id: 'role_super_admin',
1242
+ code: 'super_admin',
1243
+ name: '\u8D85\u7EA7\u7BA1\u7406\u5458',
1244
+ label: '\u8D85\u7EA7\u7BA1\u7406\u5458',
1245
+ isSystem: true,
1246
+ sortOrder: 1,
1247
+ },
1248
+ {
1249
+ id: 'role_customer_service',
1250
+ code: 'customer_service',
1251
+ name: '\u5BA2\u670D\u4EBA\u5458',
1252
+ label: '\u5BA2\u670D\u4EBA\u5458',
1253
+ isSystem: true,
1254
+ sortOrder: 2,
1255
+ },
1256
+ {
1257
+ id: 'role_user',
1258
+ code: 'user',
1259
+ name: '\u666E\u901A\u7528\u6237',
1260
+ label: '\u666E\u901A\u7528\u6237',
1261
+ isSystem: true,
1262
+ sortOrder: 3,
1263
+ },
1264
+ ]`;
1265
+ var INITIAL_ROLE_PERMISSIONS = `const initialRolePermissions = [
1266
+ { roleId: 'role_super_admin', permissionId: 'perm_user_view' },
1267
+ { roleId: 'role_super_admin', permissionId: 'perm_user_create' },
1268
+ { roleId: 'role_super_admin', permissionId: 'perm_user_edit' },
1269
+ { roleId: 'role_super_admin', permissionId: 'perm_user_delete' },
1270
+ { roleId: 'role_super_admin', permissionId: 'perm_content_view' },
1271
+ { roleId: 'role_super_admin', permissionId: 'perm_content_create' },
1272
+ { roleId: 'role_super_admin', permissionId: 'perm_content_edit' },
1273
+ { roleId: 'role_super_admin', permissionId: 'perm_content_delete' },
1274
+ { roleId: 'role_super_admin', permissionId: 'perm_system_settings' },
1275
+ { roleId: 'role_super_admin', permissionId: 'perm_system_logs' },
1276
+ { roleId: 'role_super_admin', permissionId: 'perm_system_monitor' },
1277
+ { roleId: 'role_super_admin', permissionId: 'perm_data_export' },
1278
+ { roleId: 'role_super_admin', permissionId: 'perm_data_import' },
1279
+ { roleId: 'role_super_admin', permissionId: 'perm_order_view' },
1280
+ { roleId: 'role_super_admin', permissionId: 'perm_order_create' },
1281
+ { roleId: 'role_super_admin', permissionId: 'perm_order_edit' },
1282
+ { roleId: 'role_super_admin', permissionId: 'perm_order_delete' },
1283
+ { roleId: 'role_super_admin', permissionId: 'perm_order_process' },
1284
+ { roleId: 'role_super_admin', permissionId: 'perm_ticket_view' },
1285
+ { roleId: 'role_super_admin', permissionId: 'perm_ticket_create' },
1286
+ { roleId: 'role_super_admin', permissionId: 'perm_ticket_edit' },
1287
+ { roleId: 'role_super_admin', permissionId: 'perm_ticket_delete' },
1288
+ { roleId: 'role_super_admin', permissionId: 'perm_ticket_reply' },
1289
+ { roleId: 'role_super_admin', permissionId: 'perm_ticket_close' },
1290
+ { roleId: 'role_super_admin', permissionId: 'perm_dispute_view' },
1291
+ { roleId: 'role_super_admin', permissionId: 'perm_dispute_create' },
1292
+ { roleId: 'role_super_admin', permissionId: 'perm_dispute_edit' },
1293
+ { roleId: 'role_super_admin', permissionId: 'perm_dispute_delete' },
1294
+ { roleId: 'role_super_admin', permissionId: 'perm_dispute_resolve' },
1295
+ { roleId: 'role_customer_service', permissionId: 'perm_content_view' },
1296
+ { roleId: 'role_customer_service', permissionId: 'perm_order_view' },
1297
+ { roleId: 'role_customer_service', permissionId: 'perm_order_create' },
1298
+ { roleId: 'role_customer_service', permissionId: 'perm_order_edit' },
1299
+ { roleId: 'role_customer_service', permissionId: 'perm_order_delete' },
1300
+ { roleId: 'role_customer_service', permissionId: 'perm_order_process' },
1301
+ { roleId: 'role_customer_service', permissionId: 'perm_ticket_view' },
1302
+ { roleId: 'role_customer_service', permissionId: 'perm_ticket_create' },
1303
+ { roleId: 'role_customer_service', permissionId: 'perm_ticket_edit' },
1304
+ { roleId: 'role_customer_service', permissionId: 'perm_ticket_delete' },
1305
+ { roleId: 'role_customer_service', permissionId: 'perm_ticket_reply' },
1306
+ { roleId: 'role_customer_service', permissionId: 'perm_ticket_close' },
1307
+ { roleId: 'role_customer_service', permissionId: 'perm_dispute_view' },
1308
+ { roleId: 'role_customer_service', permissionId: 'perm_dispute_create' },
1309
+ { roleId: 'role_customer_service', permissionId: 'perm_dispute_edit' },
1310
+ { roleId: 'role_customer_service', permissionId: 'perm_dispute_delete' },
1311
+ { roleId: 'role_customer_service', permissionId: 'perm_dispute_resolve' },
1312
+ { roleId: 'role_customer_service', permissionId: 'perm_data_export' },
1313
+ { roleId: 'role_customer_service', permissionId: 'perm_system_logs' },
1314
+ { roleId: 'role_user', permissionId: 'perm_content_view' },
1315
+ { roleId: 'role_user', permissionId: 'perm_order_view' },
1316
+ ]`;
1317
+ function generateDbInit(resolved) {
1318
+ const activeSeeds = SEED_MODULES.filter((s) => resolved.modules.has(s.module));
1319
+ if (!resolved.hasPermission) {
1320
+ return generateMinimalDbInit();
1321
+ }
1322
+ return generateFullDbInit(activeSeeds);
1323
+ }
1324
+ function generateMinimalDbInit() {
1325
+ return `import { getDb } from './driver'
1326
+ import { logger } from '../utils/logger'
1327
+
1328
+ const log = logger.db()
1329
+
1330
+ export async function initializeDatabase() {
1331
+ await getDb()
1332
+
1333
+ log.info({}, 'Initializing database...')
1334
+ log.info({}, 'Database initialization complete!')
1335
+ }
1336
+ `;
1337
+ }
1338
+ function generateFullDbInit(activeSeeds) {
1339
+ const seedImports = activeSeeds.map((s) => s.importLine).join("\n");
1340
+ const seedCalls = activeSeeds.map((s) => ` ${s.call}(),`).join("\n");
1341
+ const seedBlock = activeSeeds.length > 0 ? `
1342
+ log.info({}, 'Seeding module data...')
1343
+ await Promise.all([
1344
+ ${seedCalls}
1345
+ ])
1346
+ log.info({}, 'Module data seeding complete!')` : "";
1347
+ return `import { getDb } from './driver'
1348
+ import { permissions, roles, rolePermissions } from './schema'
1349
+ import { logger } from '../utils/logger'
1350
+ ${seedImports}
1351
+
1352
+ const log = logger.db()
1353
+
1354
+ ${INITIAL_PERMISSIONS}
1355
+
1356
+ ${INITIAL_ROLES}
1357
+
1358
+ ${INITIAL_ROLE_PERMISSIONS}
1359
+
1360
+ export async function initializeDatabase() {
1361
+ const db = await getDb()
1362
+
1363
+ log.info({}, 'Initializing database...')
1364
+
1365
+ const existingPermissions = await db.select().from(permissions)
1366
+ if (existingPermissions.length === 0) {
1367
+ log.info({}, 'Inserting initial permissions...')
1368
+ await db.insert(permissions).values(
1369
+ initialPermissions.map(p => ({
1370
+ ...p,
1371
+ description: null,
1372
+ isActive: true,
1373
+ createdAt: new Date(),
1374
+ updatedAt: new Date(),
1375
+ }))
1376
+ )
1377
+ }
1378
+
1379
+ const existingRoles = await db.select().from(roles)
1380
+ if (existingRoles.length === 0) {
1381
+ log.info({}, 'Inserting initial roles...')
1382
+ await db.insert(roles).values(
1383
+ initialRoles.map(r => ({
1384
+ ...r,
1385
+ description: null,
1386
+ isActive: true,
1387
+ createdAt: new Date(),
1388
+ updatedAt: new Date(),
1389
+ }))
1390
+ )
1391
+ }
1392
+
1393
+ const existingRolePermissions = await db.select().from(rolePermissions)
1394
+ if (existingRolePermissions.length === 0) {
1395
+ log.info({}, 'Inserting initial role permissions...')
1396
+ await db.insert(rolePermissions).values(
1397
+ initialRolePermissions.map(rp => ({
1398
+ ...rp,
1399
+ createdAt: new Date(),
1400
+ }))
1401
+ )
1402
+ }
1403
+
1404
+ log.info({}, 'Database initialization complete!')${seedBlock}
1405
+ }
1406
+ `;
1407
+ }
1408
+
1409
+ // src/generators/server-app.ts
1410
+ function generateServerApp(resolved) {
1411
+ const useRealtime = resolved.hasSSE || resolved.hasWebSocket;
1412
+ const useAuditLog = resolved.hasPermission;
1413
+ const useCaptcha = resolved.hasCaptcha;
1414
+ const useFileRoutes = resolved.modules.has("file");
1415
+ const imports = [
1416
+ `import { OpenAPIHono } from '@hono/zod-openapi'`,
1417
+ ``,
1418
+ `import { HTTPException } from 'hono/http-exception'`,
1419
+ `import type { ContentfulStatusCode } from 'hono/utils/http-status'`,
1420
+ `import { ZodError } from 'zod'`,
1421
+ `import type { AppBindings, CreateAppOptions } from './types/bindings'`,
1422
+ `import { AppError } from './utils/app-error'`,
1423
+ `import { autoRegisterRealtime } from './core/realtime-scanner'`,
1424
+ `import { corsMiddleware, loggerMiddleware, errorHandlerMiddleware } from './middleware'`
1425
+ ];
1426
+ if (useRealtime) {
1427
+ imports.push(`import { realtimeEnvMiddleware } from './middleware/realtime-env'`);
1428
+ }
1429
+ if (useAuditLog) {
1430
+ imports.push(`import { auditLogMiddleware } from './middleware/audit-log'`);
1431
+ }
1432
+ if (useCaptcha) {
1433
+ imports.push(`import { captchaMiddleware } from './middleware/captcha'`);
1434
+ }
1435
+ imports.push(
1436
+ `import { createModuleLoggerSync } from './utils/logger'`,
1437
+ `import { adminApiRoutes, clientApiRoutes } from './route-registry'`
1438
+ );
1439
+ if (useFileRoutes) {
1440
+ imports.push(`import { fileRoutes } from './module-file/routes/file-routes'`);
1441
+ }
1442
+ const middlewareChain = [
1443
+ `.use('*', errorHandlerMiddleware())`,
1444
+ `.use('*', loggerMiddleware())`,
1445
+ `.use('*', corsMiddleware())`
1446
+ ];
1447
+ if (useRealtime) {
1448
+ middlewareChain.push(`.use('*', realtimeEnvMiddleware())`);
1449
+ }
1450
+ if (useAuditLog) {
1451
+ middlewareChain.push(`.use('/api/*', auditLogMiddleware())`);
1452
+ }
1453
+ if (useCaptcha) {
1454
+ middlewareChain.push(
1455
+ `.use(
1456
+ '/api/admin/*',
1457
+ captchaMiddleware({
1458
+ maxRequests: 20,
1459
+ windowMs: 60000,
1460
+ })
1461
+ )`
1462
+ );
1463
+ }
1464
+ const routes = [`.route('/', clientApiRoutes)`, `.route('/', adminApiRoutes)`];
1465
+ if (useFileRoutes) {
1466
+ routes.push(`.route('/files', fileRoutes)`);
1467
+ }
1468
+ const indent = " ";
1469
+ const chain = [...middlewareChain, ...routes].join(`
1470
+ ${indent}`);
1471
+ return `${imports.join("\n")}
1472
+
1473
+ export { type AppBindings, type CreateAppOptions } from './types/bindings'
1474
+
1475
+ export function createApp<T extends AppBindings = AppBindings>(_options: CreateAppOptions = {}) {
1476
+ const app = new OpenAPIHono<{ Bindings: T }>()
1477
+ ${chain}
1478
+ .get('/health', async c => {
1479
+ try {
1480
+ const { getDb } = await import('./db')
1481
+ await getDb()
1482
+ return c.json({ status: 'ok', timestamp: new Date().toISOString(), db: 'connected' })
1483
+ } catch {
1484
+ return c.json({ status: 'ok', timestamp: new Date().toISOString(), db: 'not configured' })
1485
+ }
1486
+ })
1487
+ .post('/api/__test__/cleanup', async c => {
1488
+ try {
1489
+ const { cleanupTestDatabase } = await import('./db/test-setup')
1490
+ await cleanupTestDatabase()
1491
+ return c.json({ success: true as const, message: 'Database cleaned up' })
1492
+ } catch (error) {
1493
+ console.error('Error during database cleanup:', error)
1494
+ return c.json({ success: false as const, message: 'Failed to cleanup database' }, 500)
1495
+ }
1496
+ })
1497
+
1498
+ autoRegisterRealtime(app as unknown as Parameters<typeof autoRegisterRealtime>[0])
1499
+
1500
+ app.onError((err, c) => {
1501
+ const log = createModuleLoggerSync('api')
1502
+ c.res.headers.set('Content-Type', 'application/json')
1503
+
1504
+ if (AppError.isAppError(err)) {
1505
+ return c.json(
1506
+ {
1507
+ success: false as const,
1508
+ error: err.message,
1509
+ status: err.statusCode,
1510
+ details: err.details,
1511
+ },
1512
+ err.statusCode as ContentfulStatusCode
1513
+ )
1514
+ }
1515
+
1516
+ if (err instanceof HTTPException) {
1517
+ return c.json(
1518
+ { success: false as const, error: err.message, status: err.status },
1519
+ err.status as ContentfulStatusCode
1520
+ )
1521
+ }
1522
+
1523
+ if (err instanceof ZodError) {
1524
+ const details = err.issues.map(issue => ({
1525
+ field: issue.path.join('.'),
1526
+ message: issue.message,
1527
+ }))
1528
+ return c.json(
1529
+ { success: false as const, error: 'Validation failed', status: 400, details },
1530
+ 400
1531
+ )
1532
+ }
1533
+
1534
+ log.error({ err, path: c.req.path }, 'Unhandled error')
1535
+ return c.json(
1536
+ { success: false as const, error: err.message || 'Internal server error', status: 500 },
1537
+ 500
1538
+ )
1539
+ })
1540
+
1541
+ return app
1542
+ }
1543
+ export type AdminApiType = typeof adminApiRoutes
1544
+ export type ClientApiType = typeof clientApiRoutes
1545
+ export type AppType = ReturnType<typeof createApp>
1546
+ `;
1547
+ }
1548
+
1549
+ // src/generators/shared-modules-index.ts
1550
+ var MODULE_EXPORTS = {
1551
+ chat: {
1552
+ namedExports: ["ChatProtocolSchema", "type ChatProtocol"]
1553
+ },
1554
+ todos: {
1555
+ namedExports: [
1556
+ "TodoSchema",
1557
+ "TodoStatusSchema",
1558
+ "CreateTodoSchema",
1559
+ "UpdateTodoSchema",
1560
+ "TodoIdSchema",
1561
+ "type Todo",
1562
+ "type TodoStatus",
1563
+ "type CreateTodoInput",
1564
+ "type UpdateTodoInput"
1565
+ ]
1566
+ },
1567
+ files: {
1568
+ namedExports: [
1569
+ "FileDownloadSchema",
1570
+ "PrivateFileQuerySchema",
1571
+ "PublicFileUrlSchema",
1572
+ "PrivateFileUrlSchema",
1573
+ "GenerateUrlRequestSchema",
1574
+ "FileUrlResponseSchema",
1575
+ "EmptySchema"
1576
+ ]
1577
+ },
1578
+ notifications: {
1579
+ namedExports: [
1580
+ "NotificationSchema",
1581
+ "NotificationTypeSchema",
1582
+ "CreateNotificationSchema",
1583
+ "NotificationListQuerySchema",
1584
+ "SSEEventSchema",
1585
+ "AppSSEProtocolSchema",
1586
+ "type AppNotification",
1587
+ "type NotificationType",
1588
+ "type CreateNotificationInput",
1589
+ "type NotificationListQuery",
1590
+ "type SSEEvent",
1591
+ "type AppSSEProtocol"
1592
+ ]
1593
+ },
1594
+ admin: {
1595
+ namedExports: [
1596
+ "SystemStatsSchema",
1597
+ "HealthCheckSchema",
1598
+ "RecentActivityItemSchema",
1599
+ "RecentActivitySchema",
1600
+ "AuthUserSchema",
1601
+ "ClearTodosResultSchema",
1602
+ "type SystemStats",
1603
+ "type HealthCheck",
1604
+ "type RecentActivityItem",
1605
+ "type AuthUserResponse",
1606
+ "type ClearTodosResult"
1607
+ ]
1608
+ },
1609
+ permission: {
1610
+ namedExports: [
1611
+ "RoleEnum",
1612
+ "RoleInfoSchema",
1613
+ "PermissionInfoSchema",
1614
+ "UserPermissionsSchema",
1615
+ "RoleListSchema",
1616
+ "PermissionListSchema",
1617
+ "Role",
1618
+ "Permission",
1619
+ "ROLE_PERMISSIONS",
1620
+ "ROLE_LABELS",
1621
+ "PERMISSION_LABELS",
1622
+ "PERMISSION_CATEGORIES",
1623
+ "getPermissionsByRole",
1624
+ "hasPermission",
1625
+ "hasAnyPermission",
1626
+ "hasAllPermissions",
1627
+ "type RoleType",
1628
+ "type RoleInfo",
1629
+ "type PermissionInfo",
1630
+ "type UserPermissions"
1631
+ ]
1632
+ }
1633
+ };
1634
+ function generateSharedModulesIndex(resolved) {
1635
+ const lines = [];
1636
+ const moduleOrder = ["chat", "todos", "file", "notifications", "admin", "permission"];
1637
+ for (const moduleName of moduleOrder) {
1638
+ if (!resolved.modules.has(moduleName)) continue;
1639
+ const manifest = resolved.modules.get(moduleName);
1640
+ const exportKey = manifest.sharedSchemas?.path ?? moduleName;
1641
+ const exports$1 = MODULE_EXPORTS[exportKey];
1642
+ if (!exports$1) continue;
1643
+ lines.push(`export {
1644
+ ${exports$1.namedExports.join(",\n ")},
1645
+ } from './${exportKey}'`);
1646
+ }
1647
+ return lines.join("\n") + "\n";
1648
+ }
1649
+
1650
+ // src/generators/shared-schemas-index.ts
1651
+ var MODULE_EXPORTS2 = {
1652
+ chat: {
1653
+ namedExports: [
1654
+ "ChatProtocolSchema",
1655
+ "WebSocketStatusSchema",
1656
+ "type ChatProtocol",
1657
+ "type WebSocketStatus"
1658
+ ]
1659
+ },
1660
+ file: {
1661
+ namedExports: [
1662
+ "FileDownloadSchema",
1663
+ "PrivateFileQuerySchema",
1664
+ "PublicFileUrlSchema",
1665
+ "PrivateFileUrlSchema",
1666
+ "GenerateUrlRequestSchema",
1667
+ "FileUrlResponseSchema",
1668
+ "EmptySchema",
1669
+ "UploadResultSchema",
1670
+ "UploadFileBodySchema"
1671
+ ]
1672
+ },
1673
+ todos: {
1674
+ namedExports: [
1675
+ "TodoSchema",
1676
+ "TodoStatusSchema",
1677
+ "CreateTodoSchema",
1678
+ "UpdateTodoSchema",
1679
+ "TodoIdSchema",
1680
+ "TodoIdResponseSchema",
1681
+ "TodoAttachmentSchema",
1682
+ "TodoAttachmentListSchema",
1683
+ "TodoWithAttachmentsSchema",
1684
+ "UploadFileSchema",
1685
+ "AttachmentIdResponseSchema",
1686
+ "type Todo",
1687
+ "type TodoStatus",
1688
+ "type CreateTodoInput",
1689
+ "type UpdateTodoInput",
1690
+ "type TodoIdResponse",
1691
+ "type TodoAttachment",
1692
+ "type TodoWithAttachments"
1693
+ ]
1694
+ },
1695
+ notifications: {
1696
+ namedExports: [
1697
+ "NotificationSchema",
1698
+ "NotificationTypeSchema",
1699
+ "CreateNotificationSchema",
1700
+ "NotificationListQuerySchema",
1701
+ "SSEEventSchema",
1702
+ "AppSSEProtocolSchema",
1703
+ "UnreadCountSchema",
1704
+ "NotificationIdSchema",
1705
+ "UnreadCountEventSchema",
1706
+ "type AppNotification",
1707
+ "type NotificationType",
1708
+ "type CreateNotificationInput",
1709
+ "type NotificationListQuery",
1710
+ "type SSEEvent",
1711
+ "type AppSSEProtocol",
1712
+ "type UnreadCount",
1713
+ "type NotificationId",
1714
+ "type UnreadCountEvent"
1715
+ ]
1716
+ }
1717
+ };
1718
+ function generateSharedSchemasIndex(resolved) {
1719
+ const header = `// Re-export interfaces from implementation files
1720
+ export type { WSClient, WSProtocol, WSStatus } from '../core/ws-client'
1721
+ export type { SSEClient, SSEProtocol } from '../core/sse-client'
1722
+
1723
+ // Re-export core
1724
+ export {
1725
+ ApiSuccessSchema,
1726
+ ApiErrorSchema,
1727
+ ApiResponseSchema,
1728
+ type ApiSuccess,
1729
+ type ApiError,
1730
+ type ApiResponse,
1731
+ type RpcMethod,
1732
+ type EventName,
1733
+ type RpcInput,
1734
+ type RpcOutput,
1735
+ type EventPayload,
1736
+ createWSClient,
1737
+ createSSEClient,
1738
+ } from '../core'
1739
+
1740
+ // Re-export modules
1741
+ `;
1742
+ const moduleLines = [];
1743
+ const moduleOrder = ["chat", "file", "todos", "notifications"];
1744
+ for (const moduleName of moduleOrder) {
1745
+ if (!resolved.modules.has(moduleName)) continue;
1746
+ const exports$1 = MODULE_EXPORTS2[moduleName];
1747
+ if (!exports$1) continue;
1748
+ const manifest = resolved.modules.get(moduleName);
1749
+ const importPath = manifest.sharedSchemas?.path ?? moduleName;
1750
+ moduleLines.push(
1751
+ `export {
1752
+ ${exports$1.namedExports.join(",\n ")},
1753
+ } from '../modules/${importPath}'`
1754
+ );
1755
+ }
1756
+ return header + moduleLines.join("\n") + "\n";
1757
+ }
1758
+
1759
+ // src/generators/middleware-index.ts
1760
+ function generateMiddlewareIndex(resolved) {
1761
+ const lines = [];
1762
+ lines.push(`export { corsMiddleware, createCorsMiddleware, type CorsOptions } from './cors'`);
1763
+ lines.push(
1764
+ `export { loggerMiddleware, createLoggerMiddleware, type LoggerOptions } from './logger'`
1765
+ );
1766
+ lines.push(
1767
+ `export {
1768
+ errorHandlerMiddleware,
1769
+ createErrorHandlerMiddleware,
1770
+ type ErrorHandlerOptions,
1771
+ } from './error-handler'`
1772
+ );
1773
+ if (resolved.hasPermission) {
1774
+ lines.push(
1775
+ `export {
1776
+ authMiddleware,
1777
+ requireSuperAdminMiddleware,
1778
+ requireCustomerServiceMiddleware,
1779
+ requirePermissionsMiddleware,
1780
+ type AuthUser,
1781
+ type AuthMiddlewareOptions,
1782
+ } from './auth'`
1783
+ );
1784
+ }
1785
+ if (resolved.hasCaptcha) {
1786
+ lines.push(
1787
+ `export {
1788
+ captchaMiddleware,
1789
+ markCaptchaVerifiedMiddleware,
1790
+ clearCaptchaSessionMiddleware,
1791
+ type CaptchaConfig,
1792
+ } from './captcha'`
1793
+ );
1794
+ }
1795
+ if (resolved.hasPermission) {
1796
+ lines.push(`export { permissionMiddleware } from './permission'`);
1797
+ }
1798
+ lines.push(`export { rateLimitMiddleware, type RateLimitOptions } from './rate-limit'`);
1799
+ if (resolved.hasPermission) {
1800
+ lines.push(`export { getAuthUser } from '../utils/auth'`);
1801
+ }
1802
+ return lines.join("\n") + "\n";
1803
+ }
1804
+
1805
+ // src/generators/auth-middleware.ts
1806
+ function generateAuthMiddleware(_resolved) {
1807
+ return `import type { MiddlewareHandler } from 'hono'
1808
+ import { createModuleLoggerSync } from '../utils/logger'
1809
+
1810
+ export type UserRole = 'user' | 'admin'
1811
+
1812
+ export interface AuthUser {
1813
+ id: string
1814
+ username: string
1815
+ email: string
1816
+ role: UserRole
1817
+ avatar?: string
1818
+ }
1819
+
1820
+ export interface AuthMiddlewareOptions {
1821
+ requiredRole?: UserRole
1822
+ }
1823
+
1824
+ declare module 'hono' {
1825
+ interface ContextVariableMap {
1826
+ authUser: AuthUser
1827
+ }
1828
+ }
1829
+
1830
+ const DEV_USER: AuthUser = {
1831
+ id: 'dev-user-1',
1832
+ username: 'devuser',
1833
+ email: 'dev@example.com',
1834
+ role: 'admin',
1835
+ avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=dev',
1836
+ }
1837
+
1838
+ function extractToken(authHeader: string | undefined): string | null {
1839
+ if (!authHeader) return null
1840
+ if (!authHeader.startsWith('Bearer ')) return null
1841
+ return authHeader.slice(7)
1842
+ }
1843
+
1844
+ export function authMiddleware(_options: AuthMiddlewareOptions = {}): MiddlewareHandler {
1845
+ const log = createModuleLoggerSync('auth')
1846
+
1847
+ return async (c, next) => {
1848
+ const token = extractToken(c.req.header('Authorization'))
1849
+
1850
+ if (!token) {
1851
+ c.set('authUser', { ...DEV_USER, id: 'anonymous' })
1852
+ log.info({ path: c.req.path }, 'Anonymous access')
1853
+ await next()
1854
+ return
1855
+ }
1856
+
1857
+ c.set('authUser', DEV_USER)
1858
+ log.info({ userId: DEV_USER.id, path: c.req.path }, 'Dev user authenticated')
1859
+ await next()
1860
+ }
1861
+ }
1862
+
1863
+ export function requireSuperAdminMiddleware(): MiddlewareHandler {
1864
+ return authMiddleware()
1865
+ }
1866
+
1867
+ export function requireCustomerServiceMiddleware(): MiddlewareHandler {
1868
+ return authMiddleware()
1869
+ }
1870
+
1871
+ export function requirePermissionsMiddleware(): MiddlewareHandler {
1872
+ return authMiddleware()
1873
+ }
1874
+ `;
1875
+ }
1876
+
1877
+ // src/generators/auth-utils.ts
1878
+ function generateAuthUtils(resolved) {
1879
+ if (!resolved.hasPermission) {
1880
+ return `import type { Context } from 'hono'
1881
+ import type { AuthUser } from '../middleware/auth'
1882
+
1883
+ export function getAuthUser(c: Context): AuthUser {
1884
+ return c.get('authUser')
1885
+ }
1886
+ `;
1887
+ }
1888
+ return `import type { Context } from 'hono'
1889
+ import type { AuthUser } from '../middleware/auth'
1890
+ import { Role } from '@shared/modules/permission'
1891
+
1892
+ interface MockUser {
1893
+ id: string
1894
+ username: string
1895
+ email: string
1896
+ role: string
1897
+ status: string
1898
+ avatar: string
1899
+ createdAt: string
1900
+ updatedAt: string
1901
+ }
1902
+
1903
+ const mockUsers: MockUser[] = [
1904
+ {
1905
+ id: '1',
1906
+ username: 'superadmin',
1907
+ email: 'superadmin@example.com',
1908
+ role: Role.SUPER_ADMIN,
1909
+ status: 'active',
1910
+ avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=superadmin',
1911
+ createdAt: '2024-01-01T00:00:00Z',
1912
+ updatedAt: '2024-01-01T00:00:00Z',
1913
+ },
1914
+ {
1915
+ id: '2',
1916
+ username: 'customerservice',
1917
+ email: 'customerservice@example.com',
1918
+ role: Role.CUSTOMER_SERVICE,
1919
+ status: 'active',
1920
+ avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=customerservice',
1921
+ createdAt: '2024-01-02T00:00:00Z',
1922
+ updatedAt: '2024-01-02T00:00:00Z',
1923
+ },
1924
+ {
1925
+ id: '3',
1926
+ username: 'user1',
1927
+ email: 'user1@example.com',
1928
+ role: Role.USER,
1929
+ status: 'active',
1930
+ avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=user1',
1931
+ createdAt: '2024-01-03T00:00:00Z',
1932
+ updatedAt: '2024-01-03T00:00:00Z',
1933
+ },
1934
+ ]
1935
+
1936
+ const mockTokens: Map<string, string> = new Map([
1937
+ ['super-admin-token', '1'],
1938
+ ['customer-service-token', '2'],
1939
+ ['user-token', '3'],
1940
+ ])
1941
+
1942
+ export function getAuthUser(c: Context): AuthUser {
1943
+ return c.get('authUser')
1944
+ }
1945
+
1946
+ export function verifyToken(token: string): MockUser | null {
1947
+ const userId = mockTokens.get(token)
1948
+
1949
+ if (!userId) {
1950
+ return null
1951
+ }
1952
+
1953
+ return mockUsers.find(u => u.id === userId) || null
1954
+ }
1955
+
1956
+ export function getMockUsers(): MockUser[] {
1957
+ return mockUsers
1958
+ }
1959
+
1960
+ export function getMockTokens(): Map<string, string> {
1961
+ return mockTokens
1962
+ }
1963
+ `;
1964
+ }
1965
+
1966
+ // src/generators/client-components-index.ts
1967
+ function generateClientComponentsIndex(resolved) {
1968
+ const lines = [];
1969
+ lines.push(`export { StatusBadge, type ColorScheme } from './StatusBadge'`);
1970
+ lines.push(`export { LoadingSpinner } from './LoadingSpinner'`);
1971
+ lines.push(`export { EmptyState } from './EmptyState'`);
1972
+ if (resolved.modules.has("chat") || resolved.modules.has("notifications")) {
1973
+ lines.push(`export { ConnectionStatus } from './ConnectionStatus'`);
1974
+ }
1975
+ if (resolved.modules.has("chat")) {
1976
+ lines.push(`export { MessageCard } from './MessageCard'`);
1977
+ }
1978
+ if (resolved.modules.has("admin")) {
1979
+ lines.push(`export { AuthButton } from './AuthButton'`);
1980
+ }
1981
+ return lines.join("\n") + "\n";
1982
+ }
1983
+
1984
+ // src/generators/cli-modules-index.ts
1985
+ function generateCliModulesIndex(resolved) {
1986
+ const modules = [];
1987
+ const registrations = [];
1988
+ if (resolved.modules.has("todos")) {
1989
+ modules.push("import { registerTodoCommands } from './todo'");
1990
+ registrations.push("registerTodoCommands(program)");
1991
+ }
1992
+ if (resolved.modules.has("notifications")) {
1993
+ modules.push(
1994
+ "import { registerNotificationCommands } from './notification'"
1995
+ );
1996
+ registrations.push("registerNotificationCommands(program)");
1997
+ }
1998
+ modules.push("import { registerConfigCommands } from './config'");
1999
+ registrations.push("registerConfigCommands(program)");
2000
+ const imports = `import type { Command } from 'commander'
2001
+ ${modules.join("\n")}`;
2002
+ const exports$1 = modules.map((m) => {
2003
+ const match = m.match(/\{ (\w+) \}/);
2004
+ return match ? match[1] : "";
2005
+ }).filter(Boolean);
2006
+ return `${imports}
2007
+
2008
+ export function registerModules(program: Command) {
2009
+ ${registrations.map((r) => ` ${r}`).join("\n")}
2010
+ }
2011
+
2012
+ export { ${exports$1.join(", ")} }
2013
+ `;
2014
+ }
2015
+
2016
+ // src/generators/package-json.ts
2017
+ var MODULE_PACKAGES = {
2018
+ admin: ["bcryptjs"]
2019
+ };
2020
+ var ADMIN_PANEL_PACKAGES = ["antd"];
2021
+ var CLI_PACKAGES = ["commander"];
2022
+ var UNUSED_PACKAGES = ["lodash-es", "chalk", "mysql2"];
2023
+ function filterPackageJson(pkg, resolved) {
2024
+ const result = { ...pkg };
2025
+ const packagesToRemove = new Set(UNUSED_PACKAGES);
2026
+ for (const [module, packages] of Object.entries(MODULE_PACKAGES)) {
2027
+ if (!resolved.modules.has(module)) {
2028
+ for (const pkg2 of packages) {
2029
+ packagesToRemove.add(pkg2);
2030
+ }
2031
+ }
2032
+ }
2033
+ if (!resolved.modules.has("admin")) {
2034
+ for (const pkg2 of ADMIN_PANEL_PACKAGES) {
2035
+ packagesToRemove.add(pkg2);
2036
+ }
2037
+ }
2038
+ if (!resolved.modules.has("admin")) {
2039
+ for (const pkg2 of CLI_PACKAGES) {
2040
+ packagesToRemove.add(pkg2);
2041
+ }
2042
+ }
2043
+ if (result.dependencies && typeof result.dependencies === "object") {
2044
+ const deps = { ...result.dependencies };
2045
+ for (const pkg2 of packagesToRemove) {
2046
+ delete deps[pkg2];
2047
+ }
2048
+ result.dependencies = deps;
2049
+ }
2050
+ if (result.devDependencies && typeof result.devDependencies === "object") {
2051
+ const devDeps = { ...result.devDependencies };
2052
+ if (!resolved.modules.has("admin")) {
2053
+ delete devDeps["@testing-library/user-event"];
2054
+ }
2055
+ result.devDependencies = devDeps;
2056
+ }
2057
+ return result;
2058
+ }
2059
+ function generateViteConfig(resolved, templateDir) {
2060
+ const originalPath = join(templateDir, "vite.config.ts");
2061
+ let content = readFileSync(originalPath, "utf-8");
2062
+ if (!resolved.modules.has("admin")) {
2063
+ content = content.replace(
2064
+ /,\n\s*admin:\s*path\.resolve\(__dirname,\s*['"]admin\.html['"]\)/,
2065
+ ""
2066
+ );
2067
+ }
2068
+ return content;
2069
+ }
2070
+
2071
+ // src/commands/create.ts
2072
+ var __filename$1 = fileURLToPath(import.meta.url);
2073
+ var __dirname$1 = path2.dirname(__filename$1);
2074
+ var TEMPLATE_PROJECT_NAME = "biomimic-todo-app";
2075
+ var TEMPLATE_DB_NAME = "biomimic-todo-db";
2076
+ var ScaffoldError = class extends Error {
2077
+ constructor(message) {
2078
+ super(message);
2079
+ this.name = "ScaffoldError";
2080
+ }
2081
+ };
2082
+ function validateProjectName(name) {
2083
+ if (!name || name.trim().length === 0) {
2084
+ throw new ScaffoldError("Project name cannot be empty");
2085
+ }
2086
+ const validNameRegex = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
2087
+ if (!validNameRegex.test(name)) {
2088
+ throw new ScaffoldError(
2089
+ `Invalid project name "${name}". Use lowercase letters, numbers, hyphens, and underscores only.`
2090
+ );
2091
+ }
2092
+ if (name.length > 214) {
2093
+ throw new ScaffoldError("Project name must be 214 characters or less");
2094
+ }
2095
+ if (name.includes("..") || name.includes("/") || name.includes("\\")) {
2096
+ throw new ScaffoldError("Project name cannot contain path separators");
2097
+ }
2098
+ }
2099
+ function parseGitignore(content) {
2100
+ const negatePatterns = [];
2101
+ const includePatterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
2102
+ if (line.startsWith("!")) {
2103
+ negatePatterns.push(line.slice(1));
2104
+ return null;
2105
+ }
2106
+ return line;
2107
+ }).filter((line) => line !== null).map((pattern) => pattern.replace(/\/$/, "")).map((pattern) => pattern.replace(/^\*\./, "")).map((pattern) => pattern.replace(/^\/+/, "")).filter((pattern) => !pattern.includes("*"));
2108
+ return [...includePatterns, ...negatePatterns.map((p) => `!${p}`)];
2109
+ }
2110
+ function generateDbName(projectName) {
2111
+ const sanitized = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2112
+ return `${sanitized}-db`;
2113
+ }
2114
+ async function updateWranglerToml(targetDir, projectName) {
2115
+ const wranglerPath = path2.join(targetDir, "wrangler.toml");
2116
+ if (!await fs2.pathExists(wranglerPath)) {
2117
+ return;
2118
+ }
2119
+ let content = await fs2.readFile(wranglerPath, "utf-8");
2120
+ const dbName = generateDbName(projectName);
2121
+ content = content.replace(
2122
+ new RegExp(`^name = "${TEMPLATE_PROJECT_NAME}"`, "m"),
2123
+ `name = "${projectName}"`
2124
+ );
2125
+ content = content.replace(
2126
+ new RegExp(`database_name = "${TEMPLATE_DB_NAME}"`, "g"),
2127
+ `database_name = "${dbName}"`
2128
+ );
2129
+ content = content.replace(
2130
+ /database_id = "[^"]+"/,
2131
+ `database_id = "" # TODO: Run 'wrangler d1 create ${dbName}' and paste the ID here`
2132
+ );
2133
+ await fs2.writeFile(wranglerPath, content);
2134
+ }
2135
+ async function updatePackageJson(targetDir, projectName, resolved) {
2136
+ const pkgJsonPath = path2.join(targetDir, "package.json");
2137
+ if (!await fs2.pathExists(pkgJsonPath)) {
2138
+ return;
2139
+ }
2140
+ let pkgJson = await fs2.readJson(pkgJsonPath);
2141
+ pkgJson = filterPackageJson(pkgJson, resolved);
2142
+ pkgJson.name = projectName;
2143
+ if (pkgJson.bin) {
2144
+ delete pkgJson.bin;
2145
+ }
2146
+ await fs2.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
2147
+ }
2148
+ async function updatePackageLockJson(targetDir, projectName) {
2149
+ const lockFilePath = path2.join(targetDir, "package-lock.json");
2150
+ if (!await fs2.pathExists(lockFilePath)) {
2151
+ return;
2152
+ }
2153
+ const lockFile = await fs2.readJson(lockFilePath);
2154
+ if (lockFile.name === TEMPLATE_PROJECT_NAME) {
2155
+ lockFile.name = projectName;
2156
+ }
2157
+ if (lockFile.packages?.[""]?.name === TEMPLATE_PROJECT_NAME) {
2158
+ lockFile.packages[""].name = projectName;
2159
+ }
2160
+ await fs2.writeJson(lockFilePath, lockFile, { spaces: 2 });
2161
+ }
2162
+ async function updateReadme(targetDir, projectName) {
2163
+ const readmePath = path2.join(targetDir, "README.md");
2164
+ if (!await fs2.pathExists(readmePath)) {
2165
+ return;
2166
+ }
2167
+ let content = await fs2.readFile(readmePath, "utf-8");
2168
+ content = content.replace(/^# (.+)$/m, `# ${projectName}`);
2169
+ await fs2.writeFile(readmePath, content);
2170
+ }
2171
+ async function createProject(projectNameOrOptions, useCurrentDir = false, preset) {
2172
+ let projectName;
2173
+ let currentDir;
2174
+ let presetId;
2175
+ let outputDir;
2176
+ let dryRun;
2177
+ if (typeof projectNameOrOptions === "string") {
2178
+ projectName = projectNameOrOptions;
2179
+ currentDir = useCurrentDir;
2180
+ presetId = preset;
2181
+ dryRun = false;
2182
+ } else {
2183
+ projectName = projectNameOrOptions.projectName;
2184
+ currentDir = projectNameOrOptions.currentDir;
2185
+ presetId = projectNameOrOptions.preset;
2186
+ outputDir = projectNameOrOptions.outputDir;
2187
+ dryRun = projectNameOrOptions.dryRun ?? false;
2188
+ }
2189
+ if (!currentDir) {
2190
+ validateProjectName(projectName);
2191
+ }
2192
+ const templateDir = path2.join(__dirname$1, "../../template");
2193
+ let targetDir;
2194
+ if (currentDir) {
2195
+ targetDir = process.cwd();
2196
+ projectName = path2.basename(targetDir);
2197
+ } else if (outputDir) {
2198
+ targetDir = path2.resolve(outputDir);
2199
+ if (await fs2.pathExists(targetDir)) {
2200
+ throw new ScaffoldError(`Directory ${outputDir} already exists`);
2201
+ }
2202
+ } else {
2203
+ targetDir = path2.resolve(process.cwd(), projectName);
2204
+ if (await fs2.pathExists(targetDir)) {
2205
+ throw new ScaffoldError(`Directory ${projectName} already exists`);
2206
+ }
2207
+ }
2208
+ try {
2209
+ const manifestSpinner = ora("Loading module manifests...").start();
2210
+ const allManifests = await loadManifests(templateDir);
2211
+ const presets = await loadPresets(templateDir);
2212
+ const selectedPresetId = presetId || "fullstack-admin";
2213
+ const selectedPreset = presets.find((p) => p.id === selectedPresetId);
2214
+ if (!selectedPreset) {
2215
+ throw new ScaffoldError(
2216
+ `Unknown preset: ${selectedPresetId}. Available: ${presets.map((p) => p.id).join(", ")}`
2217
+ );
2218
+ }
2219
+ const resolved = resolvePreset(selectedPreset, allManifests);
2220
+ manifestSpinner.succeed(
2221
+ chalk.green(`Using preset: ${selectedPreset.name} (${resolved.modules.size} modules)`)
2222
+ );
2223
+ if (dryRun) {
2224
+ const resolvedModuleNames = [...resolved.modules.keys()];
2225
+ const generatedFiles2 = getGeneratedFiles(resolved);
2226
+ console.log("");
2227
+ console.log(chalk.blue("\u{1F4CB} Dry Run - Files that would be generated:\n"));
2228
+ for (const file of generatedFiles2) {
2229
+ console.log(` ${chalk.green("\u2713")} ${file}`);
2230
+ }
2231
+ const gitignorePath2 = path2.join(templateDir, ".gitignore");
2232
+ let ignorePatterns2 = [];
2233
+ if (await fs2.pathExists(gitignorePath2)) {
2234
+ const gitignoreContent = await fs2.readFile(gitignorePath2, "utf-8");
2235
+ ignorePatterns2 = parseGitignore(gitignoreContent);
2236
+ }
2237
+ ignorePatterns2.push("node_modules", ".wrangler");
2238
+ const excludePatterns2 = getExcludePatterns(resolved, allManifests);
2239
+ let templateFileCount = 0;
2240
+ const templateFiles = await fs2.readdir(templateDir, { recursive: true });
2241
+ for (const file of templateFiles) {
2242
+ const relative = String(file);
2243
+ if (!relative) continue;
2244
+ const negated = ignorePatterns2.filter((p) => p.startsWith("!"));
2245
+ const gitIgnored = ignorePatterns2.filter(
2246
+ (p) => !p.startsWith("!") && relative.startsWith(p)
2247
+ );
2248
+ if (gitIgnored.length > 0) {
2249
+ const allowed = negated.some((p) => relative === p.slice(1));
2250
+ if (!allowed) continue;
2251
+ }
2252
+ const normalizedRelative = relative.replace(/\\/g, "/");
2253
+ let excluded = false;
2254
+ for (const pattern of excludePatterns2) {
2255
+ const normalizedPattern = pattern.replace(/\\/g, "/");
2256
+ if (normalizedRelative === normalizedPattern || normalizedRelative.startsWith(normalizedPattern + "/")) {
2257
+ excluded = true;
2258
+ break;
2259
+ }
2260
+ }
2261
+ if (!excluded) templateFileCount++;
2262
+ }
2263
+ console.log("");
2264
+ console.log(chalk.blue("\u{1F4C1} Template files that would be copied:\n"));
2265
+ console.log(` ${chalk.green("\u2713")} ${templateFileCount} template files`);
2266
+ console.log("");
2267
+ console.log(chalk.yellow(` Total generated files: ${generatedFiles2.length}`));
2268
+ console.log(chalk.yellow(` Preset: ${selectedPreset.name}`));
2269
+ console.log(chalk.yellow(` Modules: ${resolvedModuleNames.join(", ")}`));
2270
+ console.log("");
2271
+ return;
2272
+ }
2273
+ if (!currentDir) {
2274
+ const dirSpinner = ora("Creating project directory...").start();
2275
+ await fs2.ensureDir(targetDir);
2276
+ dirSpinner.succeed(chalk.green("Project directory created"));
2277
+ }
2278
+ const copySpinner = ora("Copying template files...").start();
2279
+ const gitignorePath = path2.join(templateDir, ".gitignore");
2280
+ let ignorePatterns = [];
2281
+ if (await fs2.pathExists(gitignorePath)) {
2282
+ const gitignoreContent = await fs2.readFile(gitignorePath, "utf-8");
2283
+ ignorePatterns = parseGitignore(gitignoreContent);
2284
+ }
2285
+ ignorePatterns.push("node_modules", ".wrangler");
2286
+ const excludePatterns = getExcludePatterns(resolved, allManifests);
2287
+ await fs2.copy(templateDir, targetDir, {
2288
+ filter: (src) => {
2289
+ const relative = path2.relative(templateDir, src);
2290
+ if (relative === "") return true;
2291
+ const negated = ignorePatterns.filter((p) => p.startsWith("!"));
2292
+ const gitIgnored = ignorePatterns.filter((p) => !p.startsWith("!") && relative.startsWith(p));
2293
+ if (gitIgnored.length > 0) {
2294
+ const allowed = negated.some((p) => relative === p.slice(1));
2295
+ if (!allowed) return false;
2296
+ }
2297
+ const normalizedRelative = relative.replace(/\\/g, "/");
2298
+ for (const pattern of excludePatterns) {
2299
+ const normalizedPattern = pattern.replace(/\\/g, "/");
2300
+ if (normalizedRelative === normalizedPattern || normalizedRelative.startsWith(normalizedPattern + "/")) {
2301
+ return false;
2302
+ }
2303
+ }
2304
+ return true;
2305
+ },
2306
+ dereference: false
2307
+ });
2308
+ copySpinner.succeed(chalk.green("Template files copied"));
2309
+ const genSpinner = ora("Generating module-specific files...").start();
2310
+ const routeRegistryContent = generateRouteRegistry(resolved);
2311
+ await fs2.writeFile(path2.join(targetDir, "src/server/route-registry.ts"), routeRegistryContent);
2312
+ const dbSchemaContent = generateDbSchemaBarrel(resolved);
2313
+ await fs2.writeFile(path2.join(targetDir, "src/server/db/schema/index.ts"), dbSchemaContent);
2314
+ const clientAppContent = generateClientApp(resolved);
2315
+ await fs2.writeFile(path2.join(targetDir, "src/client/App.tsx"), clientAppContent);
2316
+ const clientNavContent = generateClientNavigation(resolved);
2317
+ await fs2.writeFile(
2318
+ path2.join(targetDir, "src/client/components/Navigation.tsx"),
2319
+ clientNavContent
2320
+ );
2321
+ if (resolved.modules.has("admin")) {
2322
+ const adminAppContent = generateAdminApp(resolved);
2323
+ if (adminAppContent) {
2324
+ await fs2.ensureDir(path2.join(targetDir, "src/admin"));
2325
+ await fs2.writeFile(path2.join(targetDir, "src/admin/App.tsx"), adminAppContent);
2326
+ }
2327
+ }
2328
+ const serverAppContent = generateServerApp(resolved);
2329
+ await fs2.writeFile(path2.join(targetDir, "src/server/app.ts"), serverAppContent);
2330
+ const generatedFiles = getGeneratedFiles(resolved);
2331
+ if (generatedFiles.includes("src/server/db/init.ts")) {
2332
+ const dbInitContent = generateDbInit(resolved);
2333
+ await fs2.writeFile(path2.join(targetDir, "src/server/db/init.ts"), dbInitContent);
2334
+ }
2335
+ const sharedModulesContent = generateSharedModulesIndex(resolved);
2336
+ await fs2.writeFile(path2.join(targetDir, "src/shared/modules/index.ts"), sharedModulesContent);
2337
+ const sharedSchemasContent = generateSharedSchemasIndex(resolved);
2338
+ await fs2.writeFile(path2.join(targetDir, "src/shared/schemas/index.ts"), sharedSchemasContent);
2339
+ const middlewareIndexContent = generateMiddlewareIndex(resolved);
2340
+ await fs2.writeFile(
2341
+ path2.join(targetDir, "src/server/middleware/index.ts"),
2342
+ middlewareIndexContent
2343
+ );
2344
+ if (generatedFiles.includes("src/server/middleware/auth.ts")) {
2345
+ const authMiddlewareContent = generateAuthMiddleware(resolved);
2346
+ await fs2.writeFile(
2347
+ path2.join(targetDir, "src/server/middleware/auth.ts"),
2348
+ authMiddlewareContent
2349
+ );
2350
+ }
2351
+ if (generatedFiles.includes("src/server/utils/auth.ts")) {
2352
+ const authUtilsContent = generateAuthUtils(resolved);
2353
+ await fs2.writeFile(path2.join(targetDir, "src/server/utils/auth.ts"), authUtilsContent);
2354
+ }
2355
+ const clientComponentsContent = generateClientComponentsIndex(resolved);
2356
+ await fs2.writeFile(
2357
+ path2.join(targetDir, "src/client/components/index.ts"),
2358
+ clientComponentsContent
2359
+ );
2360
+ if (resolved.modules.has("admin")) {
2361
+ const cliModulesContent = generateCliModulesIndex(resolved);
2362
+ await fs2.writeFile(path2.join(targetDir, "src/cli/modules/index.ts"), cliModulesContent);
2363
+ }
2364
+ if (generatedFiles.includes("vite.config.ts")) {
2365
+ const viteConfigContent = generateViteConfig(resolved, templateDir);
2366
+ await fs2.writeFile(path2.join(targetDir, "vite.config.ts"), viteConfigContent);
2367
+ }
2368
+ genSpinner.succeed(chalk.green("Module-specific files generated"));
2369
+ const pkgSpinner = ora("Configuring package.json...").start();
2370
+ await updatePackageJson(targetDir, projectName, resolved);
2371
+ pkgSpinner.succeed(chalk.green("package.json configured"));
2372
+ const lockSpinner = ora("Configuring package-lock.json...").start();
2373
+ await updatePackageLockJson(targetDir, projectName);
2374
+ lockSpinner.succeed(chalk.green("package-lock.json configured"));
2375
+ const wranglerSpinner = ora("Configuring wrangler.toml...").start();
2376
+ await updateWranglerToml(targetDir, projectName);
2377
+ wranglerSpinner.succeed(chalk.green("wrangler.toml configured"));
2378
+ const readmeSpinner = ora("Configuring README.md...").start();
2379
+ await updateReadme(targetDir, projectName);
2380
+ readmeSpinner.succeed(chalk.green("README.md configured"));
2381
+ console.log("");
2382
+ console.log(chalk.green(" \u2713 Project created successfully!"));
2383
+ console.log(chalk.gray(` Preset: ${selectedPreset.name}`));
2384
+ console.log(chalk.gray(` Modules: ${[...resolved.modules.keys()].join(", ")}`));
2385
+ console.log("");
2386
+ console.log(chalk.cyan(" Next steps:"));
2387
+ if (!currentDir && !outputDir) {
2388
+ console.log(chalk.white(` cd ${projectName}`));
2389
+ }
2390
+ console.log(chalk.white(" npm install"));
2391
+ console.log(chalk.white(" npm run dev"));
2392
+ console.log("");
2393
+ console.log(chalk.yellow(" \u26A0\uFE0F Cloudflare Setup:"));
2394
+ console.log(
2395
+ chalk.white(` 1. Create D1 database: wrangler d1 create ${generateDbName(projectName)}`)
2396
+ );
2397
+ console.log(chalk.white(" 2. Copy the database ID to wrangler.toml"));
2398
+ console.log(chalk.white(" 3. Deploy: npm run deploy:cf"));
2399
+ console.log("");
2400
+ console.log(chalk.gray(" Happy coding! \u{1F41F}"));
2401
+ console.log("");
2402
+ } catch (error) {
2403
+ if (error instanceof ScaffoldError) throw error;
2404
+ throw new ScaffoldError(`Error creating project: ${error}`);
2405
+ }
2406
+ }
427
2407
 
428
2408
  // src/cli/index.ts
429
- program.name("biomimic").description("Biomimic CLI - RPC service & code generation tools").version("0.1.0").option("-v, --verbose", "Enable verbose output").option("-u, --url <url>", "Server URL", "http://localhost:3010").hook("preAction", (thisCommand) => {
2409
+ var MODULE_COMMANDS = ["todo", "notification", "config"];
2410
+ program.name("create-fullstack-scaffold").description("Create a new fullstack scaffolded project").version("0.1.1").argument("[project-name]", "Name of the project to create").option("-v, --verbose", "Enable verbose output").option("-u, --url <url>", "Server URL", "http://localhost:3010").option("-p, --preset <preset>", "Template preset to use", "fullstack-admin").option("-d, --dry-run", "Preview files without creating them").option("--current-dir", "Scaffold in the current directory").action(async (projectName, cmdOptions) => {
2411
+ if (projectName && !MODULE_COMMANDS.includes(projectName)) {
2412
+ const isCurrentDir = cmdOptions.currentDir === true || projectName === ".";
2413
+ createLogger({ verbose: cmdOptions.verbose });
2414
+ const options = {
2415
+ projectName: isCurrentDir ? "." : projectName,
2416
+ currentDir: isCurrentDir,
2417
+ preset: cmdOptions.preset,
2418
+ dryRun: cmdOptions.dryRun
2419
+ };
2420
+ await createProject(options).catch((err) => {
2421
+ console.error(`Error: ${err.message}`);
2422
+ process.exit(1);
2423
+ });
2424
+ return;
2425
+ }
2426
+ if (!projectName) {
2427
+ program.outputHelp();
2428
+ process.exit(1);
2429
+ }
2430
+ });
2431
+ program.hook("preAction", (thisCommand) => {
430
2432
  const options = thisCommand.opts();
431
2433
  createLogger({ verbose: options.verbose });
432
2434
  if (options.url) {