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 +2009 -7
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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,
|
|
140
|
-
const pathParts =
|
|
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 =
|
|
344
|
-
var CONFIG_FILE =
|
|
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
|
-
|
|
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) {
|