@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.
Files changed (42) hide show
  1. package/README.md +148 -0
  2. package/dist/cli.js +74992 -0
  3. package/dist/cli.js.map +1 -0
  4. package/package.json +26 -5
  5. package/src/cli.tsx +141 -0
  6. package/src/commands/create-dashboard.tsx +455 -0
  7. package/src/commands/create-mobile.tsx +555 -0
  8. package/src/commands/create.tsx +1119 -0
  9. package/src/commands/generate-migration.mjs +17 -66
  10. package/src/commands/generate-migration.tsx +46 -0
  11. package/src/commands/generate-schemas.mjs +2 -2
  12. package/src/commands/generate-schemas.tsx +36 -0
  13. package/src/dashboard-features/ai/index.ts +102 -0
  14. package/src/dashboard-features/analytics/index.ts +31 -0
  15. package/src/dashboard-features/billing/index.ts +349 -0
  16. package/src/dashboard-features/content-type/index.ts +64 -0
  17. package/src/dashboard-features/email-templates/index.ts +17 -0
  18. package/src/dashboard-features/emailer/index.ts +27 -0
  19. package/src/dashboard-features/index.ts +28 -0
  20. package/src/dashboard-features/keybindings/index.ts +52 -0
  21. package/src/dashboard-features/manager.ts +349 -0
  22. package/src/dashboard-features/monitoring/index.ts +16 -0
  23. package/src/dashboard-features/notification/index.ts +40 -0
  24. package/src/dashboard-features/onboarding/index.ts +65 -0
  25. package/src/dashboard-features/organization/index.ts +38 -0
  26. package/src/dashboard-features/types.ts +41 -0
  27. package/src/mobile-features/index.ts +12 -0
  28. package/src/mobile-features/manager.ts +1 -0
  29. package/src/mobile-features/notification/index.ts +41 -0
  30. package/src/mobile-features/onboarding/index.ts +35 -0
  31. package/src/mobile-features/organization/index.ts +38 -0
  32. package/src/mobile-features/types.ts +1 -0
  33. package/src/shims/signal-exit.js +32 -0
  34. package/src/ui/app.tsx +68 -0
  35. package/src/ui/multi-select.tsx +106 -0
  36. package/src/utils/ast.ts +422 -0
  37. package/src/utils/env-template.ts +635 -0
  38. package/tests/test-cli-features.sh +81 -0
  39. package/tests/test-cli-mobile.sh +65 -0
  40. package/tsconfig.json +15 -0
  41. package/tsup.config.ts +21 -0
  42. 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
+ };