@creatorem/cli 0.0.1 → 1.0.5
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/README.md +148 -0
- package/dist/cli.js +74992 -0
- package/dist/cli.js.map +1 -0
- package/package.json +26 -5
- package/src/cli.tsx +141 -0
- package/src/commands/create-dashboard.tsx +455 -0
- package/src/commands/create-mobile.tsx +555 -0
- package/src/commands/create.tsx +1119 -0
- package/src/commands/generate-migration.mjs +17 -66
- package/src/commands/generate-migration.tsx +46 -0
- package/src/commands/generate-schemas.mjs +2 -2
- package/src/commands/generate-schemas.tsx +36 -0
- package/src/dashboard-features/ai/index.ts +102 -0
- package/src/dashboard-features/analytics/index.ts +31 -0
- package/src/dashboard-features/billing/index.ts +349 -0
- package/src/dashboard-features/content-type/index.ts +64 -0
- package/src/dashboard-features/email-templates/index.ts +17 -0
- package/src/dashboard-features/emailer/index.ts +27 -0
- package/src/dashboard-features/index.ts +28 -0
- package/src/dashboard-features/keybindings/index.ts +52 -0
- package/src/dashboard-features/manager.ts +349 -0
- package/src/dashboard-features/monitoring/index.ts +16 -0
- package/src/dashboard-features/notification/index.ts +40 -0
- package/src/dashboard-features/onboarding/index.ts +65 -0
- package/src/dashboard-features/organization/index.ts +38 -0
- package/src/dashboard-features/types.ts +41 -0
- package/src/mobile-features/index.ts +12 -0
- package/src/mobile-features/manager.ts +1 -0
- package/src/mobile-features/notification/index.ts +41 -0
- package/src/mobile-features/onboarding/index.ts +35 -0
- package/src/mobile-features/organization/index.ts +38 -0
- package/src/mobile-features/types.ts +1 -0
- package/src/shims/signal-exit.js +32 -0
- package/src/ui/app.tsx +68 -0
- package/src/ui/multi-select.tsx +106 -0
- package/src/utils/ast.ts +422 -0
- package/src/utils/env-template.ts +635 -0
- package/tests/test-cli-features.sh +81 -0
- package/tests/test-cli-mobile.sh +65 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +21 -0
- package/bin/cli.mjs +0 -40
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { FeatureRemover } from '../types.js';
|
|
2
|
+
import { createProject, loadFile, removeImport } from '../../utils/ast.js';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
export const BillingFeature: FeatureRemover = {
|
|
7
|
+
key: 'billing',
|
|
8
|
+
cliUI: {
|
|
9
|
+
'title': 'Billing',
|
|
10
|
+
'features': [
|
|
11
|
+
'Settings billing UI',
|
|
12
|
+
'Support for Stripe & Lemon Squeezy',
|
|
13
|
+
'One time payment / Subscription',
|
|
14
|
+
'Credit wallet implementation',
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
dependenciesToRemove: [
|
|
18
|
+
'@kit/billing'
|
|
19
|
+
],
|
|
20
|
+
i18nNamespacePrefix: 'p_billing',
|
|
21
|
+
useFilters: 'useBillingFilters',
|
|
22
|
+
crossEnvFilter: 'initBillingFilters',
|
|
23
|
+
filesToDelete: [
|
|
24
|
+
'config/billing.config.stripe.ts',
|
|
25
|
+
'config/billing.config.lemon-squeezy.ts',
|
|
26
|
+
'config/billing.config.ts',
|
|
27
|
+
'app/api/billing',
|
|
28
|
+
'app/dashboard/[slug]/settings/organization/billing'
|
|
29
|
+
],
|
|
30
|
+
apply: async (projectRoot: string) => {
|
|
31
|
+
const dashboardRoot = projectRoot;
|
|
32
|
+
|
|
33
|
+
// 1. Update use-filters.ts — remove billingConfig import and its usage in useAIFilters
|
|
34
|
+
const useFiltersPath = path.join(dashboardRoot, 'hooks/use-filters.ts');
|
|
35
|
+
if (fs.existsSync(useFiltersPath)) {
|
|
36
|
+
const project = createProject();
|
|
37
|
+
const sourceFile = loadFile(project, useFiltersPath);
|
|
38
|
+
removeImport(sourceFile, '~/config/billing.config');
|
|
39
|
+
let text = sourceFile.getFullText();
|
|
40
|
+
// Remove billingConfig from useAIFilters call
|
|
41
|
+
text = text.replace(/\s*billingConfig,?\n?/g, (match, offset, str) => {
|
|
42
|
+
// Only remove if inside the useAIFilters block
|
|
43
|
+
return match.includes('billingConfig') ? '' : match;
|
|
44
|
+
});
|
|
45
|
+
// Clean up empty object: useAIFilters({,\n aiConfig,\n}) -> useAIFilters({ aiConfig })
|
|
46
|
+
text = text.replace(/useAIFilters\(\{\s*,?\s*([\w]+),?\s*\}\)/g, 'useAIFilters({\n $1,\n })');
|
|
47
|
+
sourceFile.replaceWithText(text);
|
|
48
|
+
await sourceFile.save();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Update nav-user.tsx
|
|
52
|
+
const navUserPath = path.join(dashboardRoot, 'components/dashboard/nav-user.tsx');
|
|
53
|
+
if (fs.existsSync(navUserPath)) {
|
|
54
|
+
const project = createProject();
|
|
55
|
+
const sourceFile = loadFile(project, navUserPath);
|
|
56
|
+
|
|
57
|
+
// Remove handleNavigateToBillingPage usages
|
|
58
|
+
let text = sourceFile.getFullText();
|
|
59
|
+
text = text.replace(/const handleNavigateToBillingPage\s*=\s*\(\):\s*void\s*=>\s*\{[\s\S]*?dashboardRoutes\.paths\.dashboard\.slug\.settings\.organization\.billing\)\);\s*\};\n?/g, '');
|
|
60
|
+
text = text.replace(/b:\s*\{\s*action:\s*handleNavigateToBillingPage,\s*shift:\s*true\s*\},\n?/g, '');
|
|
61
|
+
text = text.replace(/<DropdownMenuItem\s+className="cursor-pointer"\s+onClick=\{handleNavigateToBillingPage\}>[\s\S]*?<\/DropdownMenuItem>\n?/g, '');
|
|
62
|
+
|
|
63
|
+
sourceFile.replaceWithText(text);
|
|
64
|
+
await sourceFile.save();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
router: {
|
|
68
|
+
importName: 'billingRouter',
|
|
69
|
+
importPath: '@kit/billing/router',
|
|
70
|
+
},
|
|
71
|
+
repo: {
|
|
72
|
+
apply: async (repoRoot: string) => {
|
|
73
|
+
const kitAiRoot = path.join(repoRoot, 'kit', 'ai');
|
|
74
|
+
if (!fs.existsSync(kitAiRoot)) return;
|
|
75
|
+
|
|
76
|
+
// 1. Remove @kit/billing from kit/ai/package.json
|
|
77
|
+
const kitAiPkgPath = path.join(kitAiRoot, 'package.json');
|
|
78
|
+
if (fs.existsSync(kitAiPkgPath)) {
|
|
79
|
+
const pkg = await fs.readJson(kitAiPkgPath);
|
|
80
|
+
delete pkg.dependencies?.['@kit/billing'];
|
|
81
|
+
delete pkg.peerDependencies?.['@kit/billing'];
|
|
82
|
+
await fs.writeJson(kitAiPkgPath, pkg, { spaces: 4 });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Delete billing-dependent source files
|
|
86
|
+
for (const file of [
|
|
87
|
+
'src/components/settings/user-wallet.tsx',
|
|
88
|
+
'src/components/settings/user-ai-plan-usage.tsx',
|
|
89
|
+
'src/components/settings/ai-usage-chart.tsx',
|
|
90
|
+
'src/router/get-ai-usage.ts',
|
|
91
|
+
]) {
|
|
92
|
+
const p = path.join(kitAiRoot, file);
|
|
93
|
+
if (fs.existsSync(p)) await fs.remove(p);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 3. Rewrite router.ts — remove getAiUsage endpoint
|
|
97
|
+
const routerPath = path.join(kitAiRoot, 'src/router/router.ts');
|
|
98
|
+
if (fs.existsSync(routerPath)) {
|
|
99
|
+
await fs.writeFile(routerPath, `import { CtxRouter } from '@creatorem/next-trpc';
|
|
100
|
+
import { AppClient } from '@kit/db';
|
|
101
|
+
import { createThreadAction, createThreadSchema } from './create-thread';
|
|
102
|
+
import { deleteThreadAction, deleteThreadSchema } from './delete-thread';
|
|
103
|
+
import { selectThreadsAction, selectThreadsSchema } from './select-threads';
|
|
104
|
+
import { updateThreadAction, updateThreadSchema } from './update-thread';
|
|
105
|
+
|
|
106
|
+
const ctx = new CtxRouter<{ db: AppClient }>();
|
|
107
|
+
|
|
108
|
+
export const aiRouter = ctx.router({
|
|
109
|
+
// Thread management
|
|
110
|
+
createThread: ctx.endpoint.input(createThreadSchema).action(createThreadAction),
|
|
111
|
+
selectThreads: ctx.endpoint.input(selectThreadsSchema).action(selectThreadsAction),
|
|
112
|
+
updateThread: ctx.endpoint.input(updateThreadSchema).action(updateThreadAction),
|
|
113
|
+
deleteThread: ctx.endpoint.input(deleteThreadSchema).action(deleteThreadAction),
|
|
114
|
+
});
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 4. Rewrite ai-usage-manager.ts — remove all billing dependencies, return allowed:true always
|
|
119
|
+
const usageManagerPath = path.join(kitAiRoot, 'src/server/ai-usage-manager.ts');
|
|
120
|
+
if (fs.existsSync(usageManagerPath)) {
|
|
121
|
+
await fs.writeFile(usageManagerPath, `import { AiConfig } from '@kit/ai/config';
|
|
122
|
+
import { AppClient } from '@kit/db';
|
|
123
|
+
|
|
124
|
+
export interface UsageLimitValidationResult {
|
|
125
|
+
allowed: boolean;
|
|
126
|
+
reason?: string;
|
|
127
|
+
errorMessage?: string;
|
|
128
|
+
includedAmount?: string;
|
|
129
|
+
source?: 'plan' | 'wallet' | 'no-limit';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class AiUsageManager {
|
|
133
|
+
constructor(private db: AppClient, private aiConfig: AiConfig) {}
|
|
134
|
+
|
|
135
|
+
async validateUsageLimit(): Promise<UsageLimitValidationResult> {
|
|
136
|
+
return { allowed: true, source: 'no-limit' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async handleWalletDeduction(): Promise<void> {
|
|
140
|
+
// Billing removed — no-op
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createAiUsageManager(db: AppClient, aiConfig: AiConfig): AiUsageManager {
|
|
145
|
+
return new AiUsageManager(db, aiConfig);
|
|
146
|
+
}
|
|
147
|
+
`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 5. Rewrite get-record-ai-usage-handlers.ts — remove billing/subscription checks
|
|
151
|
+
const handlersPath = path.join(kitAiRoot, 'src/server/get-record-ai-usage-handlers.ts');
|
|
152
|
+
if (fs.existsSync(handlersPath)) {
|
|
153
|
+
await fs.writeFile(handlersPath, `import { getDBClient } from '@kit/supabase-server';
|
|
154
|
+
import { eq, sql } from 'drizzle-orm';
|
|
155
|
+
import { aiMessage, aiThread, aiUsage } from '@kit/drizzle';
|
|
156
|
+
import { logger } from '@kit/utils';
|
|
157
|
+
import { AiConfig } from '../config';
|
|
158
|
+
import { StreamTextOnFinishCallback, ToolSet } from 'ai';
|
|
159
|
+
import type { AppClient } from '@kit/db';
|
|
160
|
+
|
|
161
|
+
const createThread = async (
|
|
162
|
+
db: AppClient,
|
|
163
|
+
userId: string,
|
|
164
|
+
title?: string,
|
|
165
|
+
metadata?: Record<string, unknown>,
|
|
166
|
+
): Promise<string> => {
|
|
167
|
+
const metadataJson = JSON.stringify(metadata ?? {});
|
|
168
|
+
const result = await db.rls.transaction(async (tx) => {
|
|
169
|
+
const inserted = await tx
|
|
170
|
+
.insert(aiThread)
|
|
171
|
+
.values({ userId, title: title ?? null, metadata: JSON.parse(metadataJson) })
|
|
172
|
+
.returning({ id: aiThread.id });
|
|
173
|
+
return inserted;
|
|
174
|
+
});
|
|
175
|
+
const threadId = result?.[0]?.id;
|
|
176
|
+
if (!threadId) throw new Error('Failed to create thread');
|
|
177
|
+
return threadId;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const calculateAiUsageCost = (
|
|
181
|
+
aiConfig: AiConfig,
|
|
182
|
+
modelId: string,
|
|
183
|
+
tokenUsage: { inputTokens?: number; outputTokens?: number; reasoningTokens?: number; cachedInputTokens?: number },
|
|
184
|
+
): number => {
|
|
185
|
+
const modelPricing = aiConfig.pricing[modelId];
|
|
186
|
+
if (!modelPricing) {
|
|
187
|
+
logger.warn({ modelId }, '[AI Usage] No pricing found for model');
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
return (
|
|
191
|
+
((tokenUsage.inputTokens || 0) / 1_000_000) * modelPricing.inputTokens +
|
|
192
|
+
((tokenUsage.outputTokens || 0) / 1_000_000) * modelPricing.outputTokens +
|
|
193
|
+
((tokenUsage.reasoningTokens || 0) / 1_000_000) * modelPricing.reasoningTokens +
|
|
194
|
+
((tokenUsage.cachedInputTokens || 0) / 1_000_000) * modelPricing.cachedInputTokens
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const getRecordAiUsageHandlers = async ({
|
|
199
|
+
aiConfig,
|
|
200
|
+
body,
|
|
201
|
+
}: {
|
|
202
|
+
aiConfig: AiConfig;
|
|
203
|
+
body: { threadId?: string; title?: string; metadata?: Record<string, unknown> };
|
|
204
|
+
}) => {
|
|
205
|
+
const db = await getDBClient();
|
|
206
|
+
const user = await db.user.require();
|
|
207
|
+
|
|
208
|
+
const effectiveThreadId = body.threadId ?? (await createThread(db, user.id, body.title, body.metadata));
|
|
209
|
+
|
|
210
|
+
const onStreamTextFinish: StreamTextOnFinishCallback<ToolSet> = async (result) => {
|
|
211
|
+
try {
|
|
212
|
+
const assistantText =
|
|
213
|
+
typeof result.text === 'string' ? result.text : JSON.stringify(result.text ?? '');
|
|
214
|
+
await db.rls.transaction(async (tx) => {
|
|
215
|
+
await tx.insert(aiMessage).values({
|
|
216
|
+
threadId: effectiveThreadId!,
|
|
217
|
+
userId: user.id,
|
|
218
|
+
role: 'assistant',
|
|
219
|
+
content: assistantText,
|
|
220
|
+
});
|
|
221
|
+
await tx
|
|
222
|
+
.update(aiThread)
|
|
223
|
+
.set({ updatedAt: sql\`now()\` })
|
|
224
|
+
.where(eq(aiThread.id, effectiveThreadId!));
|
|
225
|
+
if (result.totalUsage) {
|
|
226
|
+
const modelId = result.response?.modelId || 'unknown';
|
|
227
|
+
const cost = calculateAiUsageCost(aiConfig, modelId, result.totalUsage);
|
|
228
|
+
await tx.insert(aiUsage).values({
|
|
229
|
+
userId: user.id,
|
|
230
|
+
inputTokens: result.totalUsage.inputTokens || 0,
|
|
231
|
+
outputTokens: result.totalUsage.outputTokens || 0,
|
|
232
|
+
reasoningTokens: result.totalUsage.reasoningTokens || 0,
|
|
233
|
+
cachedInputTokens: result.totalUsage.cachedInputTokens || 0,
|
|
234
|
+
modelId,
|
|
235
|
+
cost: cost.toString(),
|
|
236
|
+
aiTimestamp: result.response?.timestamp
|
|
237
|
+
? new Date(result.response.timestamp).toISOString()
|
|
238
|
+
: new Date().toISOString(),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
} catch (persistErr) {
|
|
243
|
+
logger.warn({ error: persistErr }, 'Failed to persist assistant message or AI usage');
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return { effectiveThreadId, onStreamTextFinish };
|
|
248
|
+
};
|
|
249
|
+
`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 6. Rewrite use-filters.ts — remove billingConfig param and BillingConfig import
|
|
253
|
+
const useFiltersPath = path.join(kitAiRoot, 'src/www/filters/use-filters.ts');
|
|
254
|
+
if (fs.existsSync(useFiltersPath)) {
|
|
255
|
+
await fs.writeFile(useFiltersPath, `'use client';
|
|
256
|
+
|
|
257
|
+
import { useSettingsFilters } from './use-filters/use-settings-filters';
|
|
258
|
+
import { AiConfig } from '../../config';
|
|
259
|
+
|
|
260
|
+
export default function useAiFilters({ aiConfig }: { aiConfig: AiConfig }) {
|
|
261
|
+
useSettingsFilters({ aiConfig });
|
|
262
|
+
}
|
|
263
|
+
`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 7. Rewrite use-settings-filters.tsx — remove UserWallet, billingRouter, BillingConfig and wallet section
|
|
267
|
+
const useSettingsFiltersPath = path.join(kitAiRoot, 'src/www/filters/use-filters/use-settings-filters.tsx');
|
|
268
|
+
if (fs.existsSync(useSettingsFiltersPath)) {
|
|
269
|
+
await fs.writeFile(useSettingsFiltersPath, `'use client';
|
|
270
|
+
|
|
271
|
+
import type { TrpcClientWithQuery } from '@creatorem/next-trpc/query-client';
|
|
272
|
+
import { Muted } from '@kit/ui/text';
|
|
273
|
+
import { FilterCallback, useEnqueueFilter } from '@kit/utils/filters';
|
|
274
|
+
import { useTranslation } from 'react-i18next';
|
|
275
|
+
import { AiConfig } from '../../../config';
|
|
276
|
+
import { aiRouter } from '../../../router/router';
|
|
277
|
+
import { UserAiPlanUsage } from '../../../components/settings/user-ai-plan-usage';
|
|
278
|
+
|
|
279
|
+
export function useSettingsFilters({ aiConfig }: { aiConfig: AiConfig }) {
|
|
280
|
+
const { t } = useTranslation('p_ai');
|
|
281
|
+
|
|
282
|
+
const ADD_AI_SETTINGS_UI_CONFIG = 'addAiSettingsUIConfig';
|
|
283
|
+
const addAiSettingsUIConfig: FilterCallback<'get_settings_ui_config'> = (
|
|
284
|
+
settingsSchema,
|
|
285
|
+
{ clientTrpc },
|
|
286
|
+
) => {
|
|
287
|
+
const otherGroups = settingsSchema.ui.slice(0, -1);
|
|
288
|
+
const lastGroup = settingsSchema.ui.at(-1);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
ui: [
|
|
292
|
+
...otherGroups,
|
|
293
|
+
...(lastGroup
|
|
294
|
+
? ([
|
|
295
|
+
{
|
|
296
|
+
...lastGroup,
|
|
297
|
+
settingsPages: [
|
|
298
|
+
...lastGroup.settingsPages,
|
|
299
|
+
{
|
|
300
|
+
slug: 'usage',
|
|
301
|
+
title: t('usage.title'),
|
|
302
|
+
icon: 'ChartNoAxesCombined',
|
|
303
|
+
description: t('usage.description'),
|
|
304
|
+
settings: [
|
|
305
|
+
{
|
|
306
|
+
type: 'wrapper',
|
|
307
|
+
settings: [
|
|
308
|
+
{
|
|
309
|
+
type: 'ui',
|
|
310
|
+
render: (
|
|
311
|
+
<div className="space-y-4">
|
|
312
|
+
<div className="flex flex-col gap-2">
|
|
313
|
+
<div className="text-2xl font-bold">
|
|
314
|
+
{t('usage.aiUsage.title')}
|
|
315
|
+
</div>
|
|
316
|
+
<Muted>{t('usage.aiUsage.description')}</Muted>
|
|
317
|
+
</div>
|
|
318
|
+
<UserAiPlanUsage
|
|
319
|
+
clientTrpc={
|
|
320
|
+
clientTrpc as TrpcClientWithQuery<typeof aiRouter>
|
|
321
|
+
}
|
|
322
|
+
aiConfig={aiConfig}
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
),
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
] as ReturnType<FilterCallback<'get_settings_ui_config'>>['ui'])
|
|
334
|
+
: []),
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
useEnqueueFilter('get_settings_ui_config', {
|
|
340
|
+
name: ADD_AI_SETTINGS_UI_CONFIG,
|
|
341
|
+
fn: addAiSettingsUIConfig,
|
|
342
|
+
priority: 40,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
`);
|
|
346
|
+
}
|
|
347
|
+
}, // closes apply
|
|
348
|
+
}, // closes repo: {}
|
|
349
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { FeatureRemover } from '../types.js';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const PLACEHOLDER_FILE = `'use client';
|
|
6
|
+
|
|
7
|
+
import NiceModal, { useModal } from '@ebay/nice-modal-react';
|
|
8
|
+
|
|
9
|
+
const ContentSearchModal = NiceModal.create(() => {
|
|
10
|
+
const modal = useModal();
|
|
11
|
+
|
|
12
|
+
if (!modal.visible) return null;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
17
|
+
onClick={() => modal.hide()}
|
|
18
|
+
>
|
|
19
|
+
<div
|
|
20
|
+
className="bg-background border rounded-xl shadow-lg p-8 max-w-md w-full text-center"
|
|
21
|
+
onClick={(e) => e.stopPropagation()}
|
|
22
|
+
>
|
|
23
|
+
<p className="text-muted-foreground text-sm">
|
|
24
|
+
Implement your content search component here.
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const showContentTypeCmdSearch = () => {
|
|
32
|
+
NiceModal.show(ContentSearchModal);
|
|
33
|
+
};
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
export const ContentTypeFeature: FeatureRemover = {
|
|
37
|
+
key: 'content-type',
|
|
38
|
+
cliUI: {
|
|
39
|
+
title: 'Content-type',
|
|
40
|
+
description: 'Add premade UI & logic around your app tables',
|
|
41
|
+
'features': [
|
|
42
|
+
'Premade analytics components',
|
|
43
|
+
'Search tools helpers',
|
|
44
|
+
'Archive, edit form components, ...'
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
dependenciesToRemove: [
|
|
48
|
+
'@kit/content-type'
|
|
49
|
+
],
|
|
50
|
+
filesToDelete: [],
|
|
51
|
+
apply: async (projectRoot: string) => {
|
|
52
|
+
const dashboardRoot = projectRoot;
|
|
53
|
+
|
|
54
|
+
// Replace lib/show-content-type-cmd-search.tsx with a NiceModal placeholder
|
|
55
|
+
const showCmdSearchPath = path.join(dashboardRoot, 'lib/show-content-type-cmd-search.tsx');
|
|
56
|
+
if (fs.existsSync(showCmdSearchPath)) {
|
|
57
|
+
await fs.writeFile(showCmdSearchPath, PLACEHOLDER_FILE);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
router: {
|
|
61
|
+
importName: 'contentTypeRouter',
|
|
62
|
+
importPath: '@kit/content-type/router',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FeatureRemover } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export const EmailTemplatesFeature: FeatureRemover = {
|
|
4
|
+
key: 'email-templates',
|
|
5
|
+
cliUI: {
|
|
6
|
+
title: 'Email templates',
|
|
7
|
+
description: 'Built with react-email, templates for authentication emails and more.'
|
|
8
|
+
},
|
|
9
|
+
dependenciesToRemove: [
|
|
10
|
+
'@kit/email-templates',
|
|
11
|
+
],
|
|
12
|
+
repo: {
|
|
13
|
+
filesToDelete: [
|
|
14
|
+
'kit/email/email-templates',
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { FeatureRemover } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export const EmailerFeature: FeatureRemover = {
|
|
4
|
+
key: 'emailer',
|
|
5
|
+
// description: 'Emailer (transactional email sending)',
|
|
6
|
+
'cliUI': {
|
|
7
|
+
title: 'Email sending',
|
|
8
|
+
'description': 'Send emails using one of the following providers :',
|
|
9
|
+
'features': [
|
|
10
|
+
'Nodemailer (self hosted solution)',
|
|
11
|
+
'Postmark',
|
|
12
|
+
'Resend',
|
|
13
|
+
'Sendgrid'
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
dependenciesToRemove: [
|
|
17
|
+
'@kit/emailer',
|
|
18
|
+
],
|
|
19
|
+
filesToDelete: [
|
|
20
|
+
'app/api/auth-email-webhook',
|
|
21
|
+
],
|
|
22
|
+
repo: {
|
|
23
|
+
filesToDelete: [
|
|
24
|
+
'kit/email/emailer',
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { FeatureRemover } from './types.js';
|
|
2
|
+
import { OrganizationFeature } from './organization/index.js';
|
|
3
|
+
import { KeybindingsFeature } from './keybindings/index.js';
|
|
4
|
+
import { AnalyticsFeature } from './analytics/index.js';
|
|
5
|
+
import { MonitoringFeature } from './monitoring/index.js';
|
|
6
|
+
import { AIFeature } from './ai/index.js';
|
|
7
|
+
import { NotificationFeature } from './notification/index.js';
|
|
8
|
+
import { BillingFeature } from './billing/index.js';
|
|
9
|
+
import { ContentTypeFeature } from './content-type/index.js';
|
|
10
|
+
import { OnboardingFeature } from './onboarding/index.js';
|
|
11
|
+
import { EmailTemplatesFeature } from './email-templates/index.js';
|
|
12
|
+
import { EmailerFeature } from './emailer/index.js';
|
|
13
|
+
|
|
14
|
+
export const features: FeatureRemover[] = [
|
|
15
|
+
OrganizationFeature,
|
|
16
|
+
KeybindingsFeature,
|
|
17
|
+
AnalyticsFeature,
|
|
18
|
+
MonitoringFeature,
|
|
19
|
+
AIFeature,
|
|
20
|
+
NotificationFeature,
|
|
21
|
+
BillingFeature,
|
|
22
|
+
ContentTypeFeature,
|
|
23
|
+
OnboardingFeature,
|
|
24
|
+
EmailTemplatesFeature,
|
|
25
|
+
EmailerFeature,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const getFeature = (key: string) => features.find(f => f.key === key);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { FeatureRemover } from '../types.js';
|
|
2
|
+
import { createProject, loadFile, removeImport, removeInlineJSX, removePropertyAssignment } from '../../utils/ast.js';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
export const KeybindingsFeature: FeatureRemover = {
|
|
7
|
+
key: 'keybindings',
|
|
8
|
+
cliUI: {
|
|
9
|
+
title: 'Keybindings',
|
|
10
|
+
features: [
|
|
11
|
+
'Settings UI table to set keybindings',
|
|
12
|
+
'React hooks to register and display keybindings',
|
|
13
|
+
'Local storage support'
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
// description: 'Keyboard shortcuts support',
|
|
17
|
+
dependenciesToRemove: ['@kit/keybindings'],
|
|
18
|
+
i18nNamespacePrefix: 'p_keybindings',
|
|
19
|
+
useFilters: 'useKeybindingsFilters',
|
|
20
|
+
crossEnvFilter: 'initKeybindingsFilters',
|
|
21
|
+
serverFilter: 'initKeybindingsServerFilters',
|
|
22
|
+
filesToDelete: [
|
|
23
|
+
'@types/keybindings.d.ts',
|
|
24
|
+
'config/keybindings.config.ts',
|
|
25
|
+
'components/providers/keybindings-handlers.tsx',
|
|
26
|
+
],
|
|
27
|
+
apply: async (projectRoot: string) => {
|
|
28
|
+
const dashboardRoot = projectRoot;
|
|
29
|
+
|
|
30
|
+
// Update layout-client.tsx
|
|
31
|
+
const layoutClientPath = path.join(dashboardRoot, 'app/dashboard/[slug]/layout-client.tsx');
|
|
32
|
+
if (fs.existsSync(layoutClientPath)) {
|
|
33
|
+
const project = createProject();
|
|
34
|
+
const sourceFile = loadFile(project, layoutClientPath);
|
|
35
|
+
|
|
36
|
+
removeImport(sourceFile, '~/components/providers/keybindings-handlers');
|
|
37
|
+
removeImport(sourceFile, '~/config/keybindings.config');
|
|
38
|
+
|
|
39
|
+
// Remove <KeybindingsHandlers />
|
|
40
|
+
removeInlineJSX(sourceFile, 'KeybindingsHandlers');
|
|
41
|
+
|
|
42
|
+
// Remove keybindingsModel from options
|
|
43
|
+
removePropertyAssignment(sourceFile, 'keybindingsModel');
|
|
44
|
+
|
|
45
|
+
await sourceFile.save();
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
router: {
|
|
49
|
+
importName: 'getKeybindingsRouter',
|
|
50
|
+
importPath: '@kit/keybindings/router',
|
|
51
|
+
},
|
|
52
|
+
};
|