@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.
- package/dist/bin.js +59 -17
- package/dist/plugins/admin-panel/files/src/modules/admin/components/OverviewChart.tsx +56 -0
- package/dist/plugins/admin-panel/files/src/modules/admin/services/admin.service.ts +40 -0
- package/dist/plugins/admin-panel/files/src/modules/admin/views/AdminDashboard.tsx +106 -0
- package/dist/plugins/admin-panel/plugin.config.json +10 -0
- package/dist/plugins/admin-panel/tsconfig.json +12 -0
- package/dist/plugins/analytics/files/src/modules/analytics/services/analytics.service.ts +71 -0
- package/dist/plugins/analytics/plugin.config.json +8 -0
- package/dist/plugins/auth/files/src/modules/auth/auth.schema.ts +21 -0
- package/dist/plugins/auth/files/src/modules/auth/db/rls.ts +31 -0
- package/dist/plugins/auth/files/src/modules/auth/db/schema.ts +103 -0
- package/dist/plugins/auth/files/src/modules/auth/middleware/auth.middleware.ts +12 -0
- package/dist/plugins/auth/files/src/modules/auth/middleware/tenant.middleware.ts +50 -0
- package/dist/plugins/auth/files/src/modules/auth/routes/auth.routes.ts +40 -0
- package/dist/plugins/auth/files/src/modules/auth/services/auth.service.ts +113 -0
- package/dist/plugins/auth/plugin.config.json +9 -0
- package/dist/plugins/cms/files/src/modules/cms/cms.schema.ts +24 -0
- package/dist/plugins/cms/files/src/modules/cms/db/schema.ts +88 -0
- package/dist/plugins/cms/files/src/modules/cms/routes/cms.routes.ts +67 -0
- package/dist/plugins/cms/files/src/modules/cms/services/cms.service.ts +99 -0
- package/dist/plugins/cms/plugin.config.json +9 -0
- package/dist/plugins/deployment/files/Dockerfile +33 -0
- package/dist/plugins/deployment/files/docker-compose.yml +27 -0
- package/dist/plugins/deployment/files/vercel.json +14 -0
- package/dist/plugins/deployment/plugin.config.json +5 -0
- package/dist/plugins/email/files/src/modules/email/services/email.service.ts +30 -0
- package/dist/plugins/email/plugin.config.json +9 -0
- package/dist/plugins/file_upload/files/src/modules/storage/services/storage.service.ts +39 -0
- package/dist/plugins/file_upload/plugin.config.json +10 -0
- package/dist/plugins/github-actions/files/.github/workflows/ci.yml +34 -0
- package/dist/plugins/github-actions/plugin.config.json +14 -0
- package/dist/plugins/openapi/files/src/modules/openapi/openapi.routes.ts +17 -0
- package/dist/plugins/openapi/files/src/modules/openapi/openapi.schema.ts +10 -0
- package/dist/plugins/openapi/plugin.config.json +10 -0
- package/dist/plugins/payments/files/src/modules/billing/billing.schema.ts +14 -0
- package/dist/plugins/payments/files/src/modules/billing/routes/billing.routes.ts +57 -0
- package/dist/plugins/payments/files/src/modules/billing/services/stripe.service.ts +47 -0
- package/dist/plugins/payments/plugin.config.json +10 -0
- package/dist/plugins/queue/files/src/modules/queue/services/queue.service.ts +61 -0
- package/dist/plugins/queue/plugin.config.json +10 -0
- package/dist/plugins/search/files/src/modules/search/services/search.service.ts +98 -0
- package/dist/plugins/search/plugin.config.json +9 -0
- package/dist/plugins/websocket/files/src/modules/websocket/services/ws.service.ts +51 -0
- package/dist/plugins/websocket/plugin.config.json +7 -0
- package/dist/templates/templates/saas/files/package.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { OpenAPIHono } from '@hono/zod-openapi';
|
|
2
|
+
import { swaggerUI } from '@hono/swagger-ui';
|
|
3
|
+
|
|
4
|
+
export const openApiApp = new OpenAPIHono();
|
|
5
|
+
|
|
6
|
+
// Generates the OpenAPI JSON schema
|
|
7
|
+
openApiApp.doc('/openapi.json', {
|
|
8
|
+
openapi: '3.0.0',
|
|
9
|
+
info: {
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
title: 'DevForge Scaffolded API',
|
|
12
|
+
description: 'Auto-generated documentation by the DevForge openapi plugin.',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Swagger UI Route
|
|
17
|
+
openApiApp.get('/docs', swaggerUI({ url: '/openapi.json' }));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// For the plugin's own registry service if we expand it
|
|
4
|
+
export const pluginRegistryItemSchema = z.object({
|
|
5
|
+
name: z.string(),
|
|
6
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
7
|
+
description: z.string().optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type PluginRegistryItem = z.infer<typeof pluginRegistryItemSchema>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openapi",
|
|
3
|
+
"description": "Auto-Generated OpenAPI / Swagger Documentation plugin.",
|
|
4
|
+
"compatibleTemplates": ["saas", "marketplace", "crm"],
|
|
5
|
+
"packageDependencies": {
|
|
6
|
+
"@hono/swagger-ui": "^0.4.0",
|
|
7
|
+
"@hono/zod-openapi": "^0.15.1"
|
|
8
|
+
},
|
|
9
|
+
"packageDevDependencies": {}
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const checkoutSchema = z.object({
|
|
4
|
+
priceId: z.string().startsWith('price_'),
|
|
5
|
+
success_url: z.string().url().optional(),
|
|
6
|
+
cancel_url: z.string().url().optional(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const webhookSchema = z.object({
|
|
10
|
+
signature: z.string().min(1),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type CheckoutInput = z.infer<typeof checkoutSchema>;
|
|
14
|
+
export type WebhookInput = z.infer<typeof webhookSchema>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { StripeService } from '../services/stripe.service';
|
|
3
|
+
import { checkoutSchema } from '../billing.schema';
|
|
4
|
+
|
|
5
|
+
export const billingRoutes = new Hono<{
|
|
6
|
+
Variables: {
|
|
7
|
+
organizationId: string;
|
|
8
|
+
}
|
|
9
|
+
}>();
|
|
10
|
+
|
|
11
|
+
// Ensure STIPE_SECRET_KEY is defined in .env
|
|
12
|
+
const stripeService = new StripeService(process.env.STRIPE_SECRET_KEY || 'sk_test_placeholder');
|
|
13
|
+
|
|
14
|
+
billingRoutes.post('/webhook', async (c) => {
|
|
15
|
+
const body = await c.req.text();
|
|
16
|
+
const signature = c.req.header('stripe-signature');
|
|
17
|
+
|
|
18
|
+
if (!signature) {
|
|
19
|
+
return c.json({ error: 'Missing signature' }, 400);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Webhook handling logic here using StripeService
|
|
23
|
+
const whSecret = process.env.STRIPE_WEBHOOK_SECRET || 'whsec_placeholder';
|
|
24
|
+
try {
|
|
25
|
+
const event = await stripeService.handleWebhook(body, signature, whSecret);
|
|
26
|
+
return c.json({ received: true });
|
|
27
|
+
} catch (err: any) {
|
|
28
|
+
return c.json({ error: `Webhook Error: ${err.message}` }, 400);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
billingRoutes.post('/checkout', async (c) => {
|
|
33
|
+
const body = await c.req.json();
|
|
34
|
+
const result = checkoutSchema.safeParse(body);
|
|
35
|
+
if (!result.success) return c.json({ error: result.error.format() }, 400);
|
|
36
|
+
|
|
37
|
+
const { priceId } = body;
|
|
38
|
+
const organizationId = c.get('organizationId');
|
|
39
|
+
if (!organizationId) {
|
|
40
|
+
return c.json({ error: 'Unauthorized: organizationId is required' }, 401);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Call StripeService to create checkout session linked to the organization
|
|
44
|
+
const session = await stripeService.createCheckoutSession(organizationId, priceId);
|
|
45
|
+
return c.json({ url: session.url });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
billingRoutes.get('/portal', async (c) => {
|
|
49
|
+
const organizationId = c.get('organizationId');
|
|
50
|
+
if (!organizationId) {
|
|
51
|
+
return c.json({ error: 'Unauthorized: organizationId is required' }, 401);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Call StripeService to create billing portal linked to the organization
|
|
55
|
+
const url = await stripeService.createBillingPortal(organizationId);
|
|
56
|
+
return c.json({ url });
|
|
57
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Stripe from 'stripe';
|
|
2
|
+
|
|
3
|
+
export class StripeService {
|
|
4
|
+
private stripe: Stripe;
|
|
5
|
+
|
|
6
|
+
constructor(apiKey: string) {
|
|
7
|
+
this.stripe = new Stripe(apiKey, {
|
|
8
|
+
apiVersion: '2023-10-16' as any,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async createCheckoutSession(customerId: string, priceId: string) {
|
|
13
|
+
try {
|
|
14
|
+
return await this.stripe.checkout.sessions.create({
|
|
15
|
+
customer: customerId,
|
|
16
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
17
|
+
mode: 'subscription',
|
|
18
|
+
success_url: '{{SUCCESS_URL}}',
|
|
19
|
+
cancel_url: '{{CANCEL_URL}}',
|
|
20
|
+
});
|
|
21
|
+
} catch (error: any) {
|
|
22
|
+
console.error(`[StripeService] Checkout session failed: ${error.message}`);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async createBillingPortal(customerId: string) {
|
|
28
|
+
try {
|
|
29
|
+
return await this.stripe.billingPortal.sessions.create({
|
|
30
|
+
customer: customerId,
|
|
31
|
+
return_url: '{{RETURN_URL}}',
|
|
32
|
+
});
|
|
33
|
+
} catch (error: any) {
|
|
34
|
+
console.error(`[StripeService] Billing portal creation failed: ${error.message}`);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async handleWebhook(body: string, signature: string, secret: string) {
|
|
40
|
+
try {
|
|
41
|
+
return this.stripe.webhooks.constructEvent(body, signature, secret);
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
console.error(`[StripeService] Webhook construction failed: ${error.message}`);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "payments",
|
|
3
|
+
"description": "Stripe integration for subscriptions and one-time payments.",
|
|
4
|
+
"compatibleTemplates": ["saas", "marketplace", "booking", "crm"],
|
|
5
|
+
"dependencies": ["auth"],
|
|
6
|
+
"packageDependencies": {
|
|
7
|
+
"stripe": "latest",
|
|
8
|
+
"zod": "^3.22.4"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Queue, Worker, Job } from 'bullmq';
|
|
2
|
+
import Redis from 'ioredis';
|
|
3
|
+
import { EventBus } from '../../../../../../../core/src/hooks/event-bus';
|
|
4
|
+
|
|
5
|
+
export class QueueService {
|
|
6
|
+
private redis: Redis;
|
|
7
|
+
private queue: Queue;
|
|
8
|
+
private worker: Worker;
|
|
9
|
+
|
|
10
|
+
constructor(private eventBus: EventBus) {
|
|
11
|
+
this.redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
|
|
12
|
+
|
|
13
|
+
// Define the default queue for background tasks
|
|
14
|
+
this.queue = new Queue('default-queue', { connection: this.redis as any });
|
|
15
|
+
|
|
16
|
+
// Initialize the background worker
|
|
17
|
+
this.worker = new Worker('default-queue', async (job: Job) => {
|
|
18
|
+
console.log(`Processing job ${job.id} of type ${job.name} with data`, job.data);
|
|
19
|
+
|
|
20
|
+
// If the job type is an event bridge trigger
|
|
21
|
+
if (job.name === 'trigger_event') {
|
|
22
|
+
this.eventBus.dispatch(job.data.eventName, job.data.payload);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Add more specific job processors here as needed
|
|
26
|
+
}, { connection: this.redis as any });
|
|
27
|
+
|
|
28
|
+
this.worker.on('completed', (job: Job) => {
|
|
29
|
+
console.log(`Job ${job.id} has completed successfully.`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.worker.on('failed', (job: Job | undefined, err: Error) => {
|
|
33
|
+
console.log(`Job ${job?.id} has failed with error: ${err.message}`);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Enqueue a job to be processed in the background
|
|
39
|
+
*/
|
|
40
|
+
async addJob(name: string, data: any, opts?: any) {
|
|
41
|
+
try {
|
|
42
|
+
return await this.queue.add(name, data, opts);
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
console.error(`[QueueService] Failed to add job ${name}: ${error.message}`);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Gracefully close connections
|
|
51
|
+
*/
|
|
52
|
+
async close() {
|
|
53
|
+
try {
|
|
54
|
+
await this.worker.close();
|
|
55
|
+
await this.queue.close();
|
|
56
|
+
await this.redis.quit();
|
|
57
|
+
} catch (error: any) {
|
|
58
|
+
console.error(`[QueueService] Failed to close connections: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "queue",
|
|
3
|
+
"description": "Background Jobs and Task Queue powered by BullMQ and Redis.",
|
|
4
|
+
"compatibleTemplates": ["saas", "marketplace", "crm"],
|
|
5
|
+
"packageDependencies": {
|
|
6
|
+
"bullmq": "^5.7.0",
|
|
7
|
+
"ioredis": "^5.3.0"
|
|
8
|
+
},
|
|
9
|
+
"packageDevDependencies": {}
|
|
10
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { MeiliSearch } from 'meilisearch';
|
|
2
|
+
|
|
3
|
+
export interface SearchDriver {
|
|
4
|
+
search(index: string, query: string, options?: any): Promise<any>;
|
|
5
|
+
addDocuments(index: string, documents: any[]): Promise<any>;
|
|
6
|
+
deleteDocuments(index: string, documentIds: string[] | number[]): Promise<any>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class MeiliSearchDriver implements SearchDriver {
|
|
10
|
+
private client: MeiliSearch;
|
|
11
|
+
|
|
12
|
+
constructor(config: { host: string; apiKey: string }) {
|
|
13
|
+
this.client = new MeiliSearch(config);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async search(index: string, query: string, options: any = {}) {
|
|
17
|
+
try {
|
|
18
|
+
return await this.client.index(index).search(query, options);
|
|
19
|
+
} catch (error: any) {
|
|
20
|
+
console.error(`[MeiliSearchDriver] Search failed for index ${index}: ${error.message}`);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async addDocuments(index: string, documents: any[]) {
|
|
26
|
+
try {
|
|
27
|
+
return await this.client.index(index).addDocuments(documents);
|
|
28
|
+
} catch (error: any) {
|
|
29
|
+
console.error(`[MeiliSearchDriver] Add documents failed for index ${index}: ${error.message}`);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async deleteDocuments(index: string, documentIds: string[] | number[]) {
|
|
35
|
+
try {
|
|
36
|
+
return await this.client.index(index).deleteDocuments(documentIds);
|
|
37
|
+
} catch (error: any) {
|
|
38
|
+
console.error(`[MeiliSearchDriver] Delete documents failed for index ${index}: ${error.message}`);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* PostgresSearchDriver
|
|
46
|
+
* Simple fallback using basic SQL pattern matching.
|
|
47
|
+
*/
|
|
48
|
+
export class PostgresSearchDriver implements SearchDriver {
|
|
49
|
+
constructor(private db: any) {}
|
|
50
|
+
|
|
51
|
+
async search(index: string, query: string, _options: any = {}) {
|
|
52
|
+
// In a real implementation, this would use Drizzle's ilike or tsvector
|
|
53
|
+
// For the template, we provide the architectural skeleton
|
|
54
|
+
console.warn(`Postgres Search fallback for index: ${index}`);
|
|
55
|
+
return {
|
|
56
|
+
hits: [],
|
|
57
|
+
query,
|
|
58
|
+
fallback: true
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async addDocuments(_index: string, _documents: any[]) {
|
|
63
|
+
// No-op for the simple driver, or sync to table if needed
|
|
64
|
+
return { success: true };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async deleteDocuments(_index: string, _documentIds: any[]) {
|
|
68
|
+
return { success: true };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class SearchService {
|
|
73
|
+
private driver: SearchDriver;
|
|
74
|
+
|
|
75
|
+
constructor(config: {
|
|
76
|
+
type: 'meilisearch' | 'postgres',
|
|
77
|
+
meiliConfig?: { host: string; apiKey: string },
|
|
78
|
+
db?: any
|
|
79
|
+
}) {
|
|
80
|
+
if (config.type === 'meilisearch' && config.meiliConfig) {
|
|
81
|
+
this.driver = new MeiliSearchDriver(config.meiliConfig);
|
|
82
|
+
} else {
|
|
83
|
+
this.driver = new PostgresSearchDriver(config.db);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async search(index: string, query: string, options: any = {}) {
|
|
88
|
+
return await this.driver.search(index, query, options);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async addDocuments(index: string, documents: any[]) {
|
|
92
|
+
return await this.driver.addDocuments(index, documents);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async deleteDocuments(index: string, documentIds: any[]) {
|
|
96
|
+
return await this.driver.deleteDocuments(index, documentIds);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { EventBus } from '../../../../../../../core/src/hooks/event-bus';
|
|
2
|
+
|
|
3
|
+
export class WebSocketService {
|
|
4
|
+
private clients: Set<any>;
|
|
5
|
+
|
|
6
|
+
constructor(private eventBus: EventBus) {
|
|
7
|
+
this.clients = new Set();
|
|
8
|
+
|
|
9
|
+
// Listen to all public events that should trigger a frontend notification
|
|
10
|
+
this.eventBus.on('notification.broadcast', (payload: any) => {
|
|
11
|
+
this.broadcast(payload);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Listen to tenant-specific events
|
|
15
|
+
this.eventBus.on('tenant.event', (payload: any) => {
|
|
16
|
+
// Logic to filter clients by orgId can be implemented here
|
|
17
|
+
this.broadcast(payload);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Registers a connected socket
|
|
23
|
+
*/
|
|
24
|
+
addClient(ws: any) {
|
|
25
|
+
this.clients.add(ws);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Unregisters a disconnected socket
|
|
30
|
+
*/
|
|
31
|
+
removeClient(ws: any) {
|
|
32
|
+
this.clients.delete(ws);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Broadcasts a message to all connected clients
|
|
37
|
+
*/
|
|
38
|
+
broadcast(message: any) {
|
|
39
|
+
const data = JSON.stringify(message);
|
|
40
|
+
for (const client of this.clients) {
|
|
41
|
+
// Verify the connection is still open before sending
|
|
42
|
+
if (typeof client.send === 'function') {
|
|
43
|
+
try {
|
|
44
|
+
client.send(data);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error('Failed to send message to client', err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahmadubaidillah/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "The elite modular boilerplate engine for agentic applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"devforge-cli": "dist/bin.js"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@ahmadubaidillah/core": "^1.1.
|
|
19
|
+
"@ahmadubaidillah/core": "^1.1.4",
|
|
20
20
|
"@inquirer/prompts": "latest",
|
|
21
21
|
"chalk": "latest",
|
|
22
22
|
"commander": "latest",
|