@ahmadubaidillah/cli 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/bin.js +59 -17
  2. package/dist/plugins/admin-panel/files/src/modules/admin/components/OverviewChart.tsx +56 -0
  3. package/dist/plugins/admin-panel/files/src/modules/admin/services/admin.service.ts +40 -0
  4. package/dist/plugins/admin-panel/files/src/modules/admin/views/AdminDashboard.tsx +106 -0
  5. package/dist/plugins/admin-panel/plugin.config.json +10 -0
  6. package/dist/plugins/admin-panel/tsconfig.json +12 -0
  7. package/dist/plugins/analytics/files/src/modules/analytics/services/analytics.service.ts +71 -0
  8. package/dist/plugins/analytics/plugin.config.json +8 -0
  9. package/dist/plugins/auth/files/src/modules/auth/auth.schema.ts +21 -0
  10. package/dist/plugins/auth/files/src/modules/auth/db/rls.ts +31 -0
  11. package/dist/plugins/auth/files/src/modules/auth/db/schema.ts +103 -0
  12. package/dist/plugins/auth/files/src/modules/auth/middleware/auth.middleware.ts +12 -0
  13. package/dist/plugins/auth/files/src/modules/auth/middleware/tenant.middleware.ts +50 -0
  14. package/dist/plugins/auth/files/src/modules/auth/routes/auth.routes.ts +40 -0
  15. package/dist/plugins/auth/files/src/modules/auth/services/auth.service.ts +113 -0
  16. package/dist/plugins/auth/plugin.config.json +9 -0
  17. package/dist/plugins/cms/files/src/modules/cms/cms.schema.ts +24 -0
  18. package/dist/plugins/cms/files/src/modules/cms/db/schema.ts +88 -0
  19. package/dist/plugins/cms/files/src/modules/cms/routes/cms.routes.ts +67 -0
  20. package/dist/plugins/cms/files/src/modules/cms/services/cms.service.ts +99 -0
  21. package/dist/plugins/cms/plugin.config.json +9 -0
  22. package/dist/plugins/deployment/files/Dockerfile +33 -0
  23. package/dist/plugins/deployment/files/docker-compose.yml +27 -0
  24. package/dist/plugins/deployment/files/vercel.json +14 -0
  25. package/dist/plugins/deployment/plugin.config.json +5 -0
  26. package/dist/plugins/email/files/src/modules/email/services/email.service.ts +30 -0
  27. package/dist/plugins/email/plugin.config.json +9 -0
  28. package/dist/plugins/file_upload/files/src/modules/storage/services/storage.service.ts +39 -0
  29. package/dist/plugins/file_upload/plugin.config.json +10 -0
  30. package/dist/plugins/github-actions/files/.github/workflows/ci.yml +34 -0
  31. package/dist/plugins/github-actions/plugin.config.json +14 -0
  32. package/dist/plugins/openapi/files/src/modules/openapi/openapi.routes.ts +17 -0
  33. package/dist/plugins/openapi/files/src/modules/openapi/openapi.schema.ts +10 -0
  34. package/dist/plugins/openapi/plugin.config.json +10 -0
  35. package/dist/plugins/payments/files/src/modules/billing/billing.schema.ts +14 -0
  36. package/dist/plugins/payments/files/src/modules/billing/routes/billing.routes.ts +57 -0
  37. package/dist/plugins/payments/files/src/modules/billing/services/stripe.service.ts +47 -0
  38. package/dist/plugins/payments/plugin.config.json +10 -0
  39. package/dist/plugins/queue/files/src/modules/queue/services/queue.service.ts +61 -0
  40. package/dist/plugins/queue/plugin.config.json +10 -0
  41. package/dist/plugins/search/files/src/modules/search/services/search.service.ts +98 -0
  42. package/dist/plugins/search/plugin.config.json +9 -0
  43. package/dist/plugins/websocket/files/src/modules/websocket/services/ws.service.ts +51 -0
  44. package/dist/plugins/websocket/plugin.config.json +7 -0
  45. package/dist/templates/templates/saas/files/package.json +1 -1
  46. package/package.json +2 -2
@@ -0,0 +1,40 @@
1
+ import { Hono } from 'hono';
2
+ import { signupSchema, loginSchema, organizationSchema } from '../auth.schema';
3
+
4
+ export const authRoutes = new Hono();
5
+
6
+ authRoutes.post('/signup', async (c) => {
7
+ const body = await c.req.json();
8
+ const result = signupSchema.safeParse(body);
9
+ if (!result.success) return c.json({ error: result.error.format() }, 400);
10
+ return c.json({ message: 'User signed up successfully' });
11
+ });
12
+
13
+ authRoutes.post('/login', async (c) => {
14
+ const body = await c.req.json();
15
+ const result = loginSchema.safeParse(body);
16
+ if (!result.success) return c.json({ error: result.error.format() }, 400);
17
+ return c.json({ message: 'User logged in successfully' });
18
+ });
19
+
20
+ authRoutes.get('/me', (c) => {
21
+ return c.json({ user: { id: '1', email: 'user@example.com' } });
22
+ });
23
+
24
+ /**
25
+ * Organization Management
26
+ */
27
+ authRoutes.post('/organizations', async (c) => {
28
+ const body = await c.req.json();
29
+ const result = organizationSchema.safeParse(body);
30
+ if (!result.success) return c.json({ error: result.error.format() }, 400);
31
+ return c.json({ message: 'Organization created successfully', id: 'org_123' });
32
+ });
33
+
34
+ authRoutes.get('/organizations', (c) => {
35
+ return c.json({
36
+ organizations: [
37
+ { id: 'org_123', name: 'DevForge Team', slug: 'devforge' }
38
+ ]
39
+ });
40
+ });
@@ -0,0 +1,113 @@
1
+ import { betterAuth } from "better-auth";
2
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
3
+ import { organization } from "better-auth/plugins";
4
+ // In a scaffolded project, this usually resolves to @devforge/core or relative path
5
+ // For the purpose of the SDK, we'll assume a relative path for now
6
+ import { eventBus } from "../../../../../../../core/src/hooks/event-bus";
7
+
8
+ /**
9
+ * AuthService
10
+ *
11
+ * This service provides a robust wrapper around Better Auth.
12
+ * It is designed to work with Drizzle ORM.
13
+ */
14
+ export class AuthService {
15
+ public auth: any;
16
+
17
+ constructor(db: any, provider: "pg" | "sqlite" | "mysql" = "pg", schema?: any) {
18
+ this.auth = betterAuth({
19
+ database: drizzleAdapter(db, {
20
+ provider,
21
+ schema,
22
+ }),
23
+ secret: process.env.BETTER_AUTH_SECRET || "development_secret_only_for_scaffolding",
24
+ baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
25
+ emailAndPassword: {
26
+ enabled: true
27
+ },
28
+ plugins: [
29
+ organization({
30
+ allowUserToCreateOrganization: true,
31
+ creatorRole: "owner",
32
+ })
33
+ ]
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Authenticate a user with email and password.
39
+ * Returns user data and headers to be sent to the client.
40
+ */
41
+ async login(credentials: { email: string; password: string }) {
42
+ try {
43
+ const response = await this.auth.api.signInEmail({
44
+ body: {
45
+ email: credentials.email,
46
+ password: credentials.password,
47
+ }
48
+ });
49
+ // Better Auth returns a response object that can include headers for setting cookies
50
+ return {
51
+ success: true,
52
+ user: response.user,
53
+ session: response.session,
54
+ headers: response.headers
55
+ };
56
+ } catch (error: any) {
57
+ throw new Error(error.message || 'Login failed');
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Create a new user account.
63
+ */
64
+ async signup(data: any) {
65
+ try {
66
+ const response = await this.auth.api.signUpEmail({
67
+ body: {
68
+ email: data.email,
69
+ password: data.password,
70
+ name: data.name,
71
+ }
72
+ });
73
+ return {
74
+ success: true,
75
+ user: response.user,
76
+ session: response.session,
77
+ headers: response.headers
78
+ };
79
+ } catch (error: any) {
80
+ throw new Error(error.message || 'Signup failed');
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Create an organization.
86
+ */
87
+ async createOrganization(data: any, user: any) {
88
+ try {
89
+ const org = await this.auth.api.createOrganization({
90
+ body: data,
91
+ headers: new Headers({ "authorization": `Bearer ${user.id}` }) // Mocking auth for example
92
+ });
93
+
94
+ eventBus.dispatch('org.created', { org, creatorId: user.id });
95
+
96
+ return org;
97
+ } catch (error: any) {
98
+ throw new Error(error.message || 'Organization creation failed');
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Retrieve the current session from the request headers.
104
+ */
105
+ async getSession(request: Request | { headers: Headers }) {
106
+ try {
107
+ return await this.auth.api.getSession({ headers: request.headers });
108
+ } catch (error) {
109
+ return null;
110
+ }
111
+ }
112
+ }
113
+
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "auth",
3
+ "description": "Authentication module using Better Auth",
4
+ "compatibleTemplates": ["saas", "cms", "marketplace", "booking", "finance", "crm"],
5
+ "packageDependencies": {
6
+ "better-auth": "latest",
7
+ "@better-auth/drizzle-adapter": "latest"
8
+ }
9
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+
3
+ export const postSchema = z.object({
4
+ title: z.string().min(1),
5
+ content: z.string().min(1),
6
+ slug: z.string().min(1),
7
+ categoryId: z.string().optional(),
8
+ tagIds: z.array(z.string()).optional(),
9
+ status: z.enum(['draft', 'published']).default('draft'),
10
+ });
11
+
12
+ export const categorySchema = z.object({
13
+ name: z.string().min(1),
14
+ slug: z.string().min(1),
15
+ description: z.string().optional(),
16
+ });
17
+
18
+ export const tagSchema = z.object({
19
+ name: z.string().min(1),
20
+ });
21
+
22
+ export type PostInput = z.infer<typeof postSchema>;
23
+ export type CategoryInput = z.infer<typeof categorySchema>;
24
+ export type TagInput = z.infer<typeof tagSchema>;
@@ -0,0 +1,88 @@
1
+ import { pgTable, text, timestamp, uuid, index, primaryKey, jsonb } from "drizzle-orm/pg-core";
2
+
3
+ /**
4
+ * Categories Table
5
+ * Allows organizing posts into primary buckets.
6
+ */
7
+ export const categories = pgTable("category", {
8
+ id: uuid("id").defaultRandom().primaryKey(),
9
+ name: text("name").notNull(),
10
+ slug: text("slug").notNull(),
11
+ organizationId: text("organization_id").notNull(),
12
+ createdAt: timestamp("created_at").defaultNow().notNull(),
13
+ }, (table) => [
14
+ index("category_org_idx").on(table.organizationId),
15
+ index("category_slug_idx").on(table.slug),
16
+ ]);
17
+
18
+ /**
19
+ * Tags Table
20
+ * Flexible labeling system for posts.
21
+ */
22
+ export const tags = pgTable("tag", {
23
+ id: uuid("id").defaultRandom().primaryKey(),
24
+ name: text("name").notNull(),
25
+ slug: text("slug").notNull(),
26
+ organizationId: text("organization_id").notNull(),
27
+ createdAt: timestamp("created_at").defaultNow().notNull(),
28
+ }, (table) => [
29
+ index("tag_org_idx").on(table.organizationId),
30
+ index("tag_slug_idx").on(table.slug),
31
+ ]);
32
+
33
+ /**
34
+ * Posts Table
35
+ * Updated with category, featured image, and SEO metadata.
36
+ */
37
+ export const posts = pgTable("post", {
38
+ id: uuid("id").defaultRandom().primaryKey(),
39
+ title: text("title").notNull(),
40
+ slug: text("slug").notNull(),
41
+ content: jsonb("content"), // Support for rich text JSON
42
+ seo: jsonb("seo"), // Metadata: title, description, ogImage
43
+ status: text("status", { enum: ["draft", "published"] }).default("draft").notNull(),
44
+ authorId: text("author_id").notNull(),
45
+ organizationId: text("organization_id").notNull(),
46
+ categoryId: uuid("category_id").references(() => categories.id),
47
+ featuredImageId: text("featured_image_id"), // Refers to external storage plugin
48
+ createdAt: timestamp("created_at").defaultNow().notNull(),
49
+ updatedAt: timestamp("updated_at")
50
+ .defaultNow()
51
+ .$onUpdate(() => new Date())
52
+ .notNull(),
53
+ }, (table) => [
54
+ index("post_org_idx").on(table.organizationId),
55
+ index("post_slug_idx").on(table.slug),
56
+ ]);
57
+
58
+ /**
59
+ * Post-Tags Junction Table
60
+ * Many-to-Many relationship between Posts and Tags.
61
+ */
62
+ export const postTags = pgTable("post_tag", {
63
+ postId: uuid("post_id").notNull().references(() => posts.id, { onDelete: 'cascade' }),
64
+ tagId: uuid("tag_id").notNull().references(() => tags.id, { onDelete: 'cascade' }),
65
+ }, (table) => [
66
+ primaryKey({ columns: [table.postId, table.tagId] })
67
+ ]);
68
+
69
+ /**
70
+ * Pages Table
71
+ * Updated with SEO metadata.
72
+ */
73
+ export const pages = pgTable("page", {
74
+ id: uuid("id").defaultRandom().primaryKey(),
75
+ title: text("title").notNull(),
76
+ slug: text("slug").notNull(),
77
+ content: jsonb("content"),
78
+ seo: jsonb("seo"),
79
+ organizationId: text("organization_id").notNull(),
80
+ createdAt: timestamp("created_at").defaultNow().notNull(),
81
+ updatedAt: timestamp("updated_at")
82
+ .defaultNow()
83
+ .$onUpdate(() => new Date())
84
+ .notNull(),
85
+ }, (table) => [
86
+ index("page_org_idx").on(table.organizationId),
87
+ index("page_slug_idx").on(table.slug),
88
+ ]);
@@ -0,0 +1,67 @@
1
+ import { Hono } from 'hono';
2
+ import { CMSService } from '../services/cms.service';
3
+ import { postSchema, categorySchema, tagSchema } from '../cms.schema';
4
+
5
+ export const cmsRoutes = new Hono<{
6
+ Variables: {
7
+ organizationId: string;
8
+ db: any;
9
+ }
10
+ }>();
11
+
12
+ // --- Posts ---
13
+ cmsRoutes.get('/posts', async (c) => {
14
+ const cmsService = new CMSService(c.get('db'));
15
+ const categorySlug = c.req.query('category');
16
+ const posts = await cmsService.getPosts(c.get('organizationId'), { categorySlug });
17
+ return c.json({ posts });
18
+ });
19
+
20
+ cmsRoutes.post('/posts', async (c) => {
21
+ const cmsService = new CMSService(c.get('db'));
22
+ const body = await c.req.json();
23
+ const result = postSchema.safeParse(body);
24
+ if (!result.success) return c.json({ error: result.error.format() }, 400);
25
+ const post = await cmsService.createPost(body, c.get('organizationId'));
26
+ return c.json({ post });
27
+ });
28
+
29
+ // --- Categories ---
30
+ cmsRoutes.get('/categories', async (c) => {
31
+ const cmsService = new CMSService(c.get('db'));
32
+ const categories = await cmsService.getCategories(c.get('organizationId'));
33
+ return c.json({ categories });
34
+ });
35
+
36
+ cmsRoutes.post('/categories', async (c) => {
37
+ const cmsService = new CMSService(c.get('db'));
38
+ const body = await c.req.json();
39
+ const result = categorySchema.safeParse(body);
40
+ if (!result.success) return c.json({ error: result.error.format() }, 400);
41
+ const category = await cmsService.createCategory(body, c.get('organizationId'));
42
+ return c.json({ category });
43
+ });
44
+
45
+ // --- Tags ---
46
+ cmsRoutes.get('/tags', async (c) => {
47
+ const cmsService = new CMSService(c.get('db'));
48
+ const tags = await cmsService.getTags(c.get('organizationId'));
49
+ return c.json({ tags });
50
+ });
51
+
52
+ cmsRoutes.post('/tags', async (c) => {
53
+ const cmsService = new CMSService(c.get('db'));
54
+ const body = await c.req.json();
55
+ const result = tagSchema.safeParse(body);
56
+ if (!result.success) return c.json({ error: result.error.format() }, 400);
57
+ const tag = await cmsService.createTag(body, c.get('organizationId'));
58
+ return c.json({ tag });
59
+ });
60
+
61
+ // --- Pages ---
62
+ cmsRoutes.get('/pages/:slug', async (c) => {
63
+ const cmsService = new CMSService(c.get('db'));
64
+ const page = await cmsService.getPageBySlug(c.req.param('slug'), c.get('organizationId'));
65
+ if (!page) return c.json({ message: 'Page not found' }, 404);
66
+ return c.json({ page });
67
+ });
@@ -0,0 +1,99 @@
1
+ import { eq, and, inArray } from 'drizzle-orm';
2
+ import { posts, pages, categories, tags, postTags } from '../db/schema';
3
+ import { eventBus } from '../../../../../../../core/src/hooks/event-bus';
4
+
5
+ export class CMSService {
6
+ private db: any;
7
+
8
+ constructor(db: any) {
9
+ this.db = db;
10
+ }
11
+
12
+ // --- Posts ---
13
+ async getPosts(organizationId: string, options: { categorySlug?: string } = {}) {
14
+ try {
15
+ let query = this.db.select().from(posts).where(eq(posts.organizationId, organizationId));
16
+
17
+ if (options.categorySlug) {
18
+ const cat = await this.getCategoryBySlug(options.categorySlug, organizationId);
19
+ if (cat) {
20
+ query = this.db.select().from(posts).where(
21
+ and(eq(posts.organizationId, organizationId), eq(posts.categoryId, cat.id))
22
+ );
23
+ }
24
+ }
25
+
26
+ return await query;
27
+ } catch (error: any) {
28
+ console.error(`[CMSService] Failed to fetch posts: ${error.message}`);
29
+ throw error;
30
+ }
31
+ }
32
+
33
+ async createPost(data: any, organizationId: string) {
34
+ try {
35
+ const { tagIds, ...postData } = data;
36
+ const [post] = await this.db.insert(posts).values({
37
+ ...postData,
38
+ organizationId,
39
+ }).returning();
40
+
41
+ if (tagIds && tagIds.length > 0) {
42
+ await this.db.insert(postTags).values(
43
+ tagIds.map((tagId: string) => ({ postId: post.id, tagId }))
44
+ );
45
+ }
46
+
47
+ eventBus.dispatch('cms.post.created', { post, organizationId });
48
+
49
+ return post;
50
+ } catch (error: any) {
51
+ console.error(`[CMSService] Post creation failed: ${error.message}`);
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ // --- Categories ---
57
+ async getCategories(organizationId: string) {
58
+ return await this.db.select().from(categories).where(eq(categories.organizationId, organizationId));
59
+ }
60
+
61
+ async createCategory(data: any, organizationId: string) {
62
+ try {
63
+ return await this.db.insert(categories).values({
64
+ ...data,
65
+ organizationId,
66
+ }).returning();
67
+ } catch (error: any) {
68
+ console.error(`[CMSService] Category creation failed: ${error.message}`);
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ async getCategoryBySlug(slug: string, organizationId: string) {
74
+ const [category] = await this.db.select()
75
+ .from(categories)
76
+ .where(and(eq(categories.slug, slug), eq(categories.organizationId, organizationId)));
77
+ return category;
78
+ }
79
+
80
+ // --- Tags ---
81
+ async getTags(organizationId: string) {
82
+ return await this.db.select().from(tags).where(eq(tags.organizationId, organizationId));
83
+ }
84
+
85
+ async createTag(data: any, organizationId: string) {
86
+ return await this.db.insert(tags).values({
87
+ ...data,
88
+ organizationId,
89
+ }).returning();
90
+ }
91
+
92
+ // --- Pages ---
93
+ async getPageBySlug(slug: string, organizationId: string) {
94
+ const [page] = await this.db.select()
95
+ .from(pages)
96
+ .where(and(eq(pages.slug, slug), eq(pages.organizationId, organizationId)));
97
+ return page;
98
+ }
99
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "cms",
3
+ "description": "Headless CMS module for managing posts and pages",
4
+ "compatibleTemplates": ["saas", "cms", "marketplace"],
5
+ "packageDependencies": {
6
+ "drizzle-orm": "latest"
7
+ },
8
+ "pluginDependencies": ["auth", "file_upload"]
9
+ }
@@ -0,0 +1,33 @@
1
+ # Stage 1: Build
2
+ FROM oven/bun:latest AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ COPY package.json ./
7
+ COPY bun.lockb* ./
8
+
9
+ RUN bun install --frozen-lockfile
10
+
11
+ COPY . .
12
+
13
+ # If there's a build step, run it
14
+ RUN if [ -f "src/app.ts" ]; then bun build ./src/app.ts --outdir ./dist; fi
15
+
16
+ # Stage 2: Production
17
+ FROM oven/bun:distroless
18
+
19
+ WORKDIR /app
20
+
21
+ # Copy only the built assets and necessary files
22
+ COPY --from=builder /app/package.json ./
23
+ COPY --from=builder /app/src ./src
24
+ COPY --from=builder /app/node_modules ./node_modules
25
+
26
+ # Environment variables
27
+ ENV NODE_ENV=production
28
+ ENV PORT=3000
29
+
30
+ EXPOSE 3000
31
+
32
+ # Start the application
33
+ CMD ["run", "src/app.ts"]
@@ -0,0 +1,27 @@
1
+ version: '3.8'
2
+
3
+ services:
4
+ "app-{{PROJECT_NAME}}":
5
+ container_name: "{{PROJECT_NAME}}-app"
6
+ build: .
7
+ ports:
8
+ - "3000:3000"
9
+ environment:
10
+ - NODE_ENV=development
11
+ - DATABASE_URL=postgres://user:pass@db:5432/devforge
12
+ depends_on:
13
+ - db
14
+
15
+ db:
16
+ image: postgres:15-alpine
17
+ environment:
18
+ - POSTGRES_USER=user
19
+ - POSTGRES_PASSWORD=pass
20
+ - POSTGRES_DB=devforge
21
+ volumes:
22
+ - pgdata:/var/lib/postgresql/data
23
+ ports:
24
+ - "5432:5432"
25
+
26
+ volumes:
27
+ pgdata:
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 2,
3
+ "framework": "hono",
4
+ "name": "{{PROJECT_NAME}}",
5
+ "installCommand": "bun install",
6
+ "buildCommand": "bun run build",
7
+ "outputDirectory": "dist",
8
+ "rewrites": [
9
+ {
10
+ "source": "/(.*)",
11
+ "destination": "/api"
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "deployment",
3
+ "description": "Docker and Vercel deployment configurations for various stacks.",
4
+ "compatibleTemplates": ["saas", "cms", "marketplace", "ai_wrapper", "preact"]
5
+ }
@@ -0,0 +1,30 @@
1
+ import { Resend } from 'resend';
2
+
3
+ export interface EmailOptions {
4
+ to: string | string[];
5
+ subject: string;
6
+ html: string;
7
+ from?: string;
8
+ }
9
+
10
+ export class EmailService {
11
+ private resend: Resend;
12
+
13
+ constructor(apiKey: string) {
14
+ this.resend = new Resend(apiKey);
15
+ }
16
+
17
+ async sendEmail(options: EmailOptions) {
18
+ try {
19
+ return await this.resend.emails.send({
20
+ from: options.from || 'noreply@{{DOMAIN}}',
21
+ to: options.to,
22
+ subject: options.subject,
23
+ html: options.html,
24
+ });
25
+ } catch (error: any) {
26
+ console.error(`[EmailService] Failed to send email: ${error.message}`);
27
+ throw error;
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "email",
3
+ "description": "Transactional email service with support for multiple providers.",
4
+ "compatibleTemplates": ["saas", "marketplace", "booking", "crm", "cms", "landing"],
5
+ "packageDependencies": {
6
+ "resend": "latest",
7
+ "zod": "^3.22.4"
8
+ }
9
+ }
@@ -0,0 +1,39 @@
1
+ import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
2
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
3
+
4
+ export class StorageService {
5
+ private client: S3Client;
6
+
7
+ constructor(config: { region: string; credentials: { accessKeyId: string; secretAccessKey: string } }) {
8
+ this.client = new S3Client(config);
9
+ }
10
+
11
+ async getUploadUrl(bucket: string, key: string, expiresIn: number = 3600) {
12
+ try {
13
+ const command = new PutObjectCommand({
14
+ Bucket: bucket,
15
+ Key: key,
16
+ });
17
+
18
+ return await getSignedUrl(this.client, command, { expiresIn });
19
+ } catch (error: any) {
20
+ console.error(`[StorageService] Failed to get upload URL: ${error.message}`);
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ async uploadFile(bucket: string, key: string, body: Buffer | Uint8Array | string) {
26
+ try {
27
+ const command = new PutObjectCommand({
28
+ Bucket: bucket,
29
+ Key: key,
30
+ Body: body,
31
+ });
32
+
33
+ return await this.client.send(command);
34
+ } catch (error: any) {
35
+ console.error(`[StorageService] File upload failed: ${error.message}`);
36
+ throw error;
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "file_upload",
3
+ "description": "S3-compatible file upload service.",
4
+ "compatibleTemplates": ["saas", "marketplace", "cms", "crm"],
5
+ "packageDependencies": {
6
+ "@aws-sdk/client-s3": "latest",
7
+ "@aws-sdk/s3-request-presigner": "latest",
8
+ "zod": "^3.22.4"
9
+ }
10
+ }
@@ -0,0 +1,34 @@
1
+ name: DevForge Post-Scaffold CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ jobs:
10
+ validate:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ node-version: [20.x]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Setup Bun
20
+ uses: oven-sh/setup-bun@v1
21
+ with:
22
+ bun-version: latest
23
+
24
+ - name: Install Dependencies
25
+ run: bun install
26
+
27
+ - name: Run Typecheck
28
+ run: bun run typecheck
29
+
30
+ - name: Run Linter
31
+ run: bun lint
32
+
33
+ - name: Execute Tests
34
+ run: bun test --coverage
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "github-actions",
3
+ "description": "DevForge github-actions CI Pipeline generator",
4
+ "compatibleTemplates": [
5
+ "saas",
6
+ "marketplace",
7
+ "crm",
8
+ "ai_wrapper",
9
+ "booking",
10
+ "finance"
11
+ ],
12
+ "packageDependencies": {},
13
+ "packageDevDependencies": {}
14
+ }