@creatorem/cli 0.0.1 → 1.0.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 (39) hide show
  1. package/package.json +26 -5
  2. package/src/cli.tsx +141 -0
  3. package/src/commands/create-dashboard.tsx +455 -0
  4. package/src/commands/create-mobile.tsx +555 -0
  5. package/src/commands/create.tsx +1119 -0
  6. package/src/commands/generate-migration.mjs +17 -66
  7. package/src/commands/generate-migration.tsx +46 -0
  8. package/src/commands/generate-schemas.mjs +2 -2
  9. package/src/commands/generate-schemas.tsx +36 -0
  10. package/src/dashboard-features/ai/index.ts +102 -0
  11. package/src/dashboard-features/analytics/index.ts +31 -0
  12. package/src/dashboard-features/billing/index.ts +349 -0
  13. package/src/dashboard-features/content-type/index.ts +64 -0
  14. package/src/dashboard-features/email-templates/index.ts +17 -0
  15. package/src/dashboard-features/emailer/index.ts +27 -0
  16. package/src/dashboard-features/index.ts +28 -0
  17. package/src/dashboard-features/keybindings/index.ts +52 -0
  18. package/src/dashboard-features/manager.ts +349 -0
  19. package/src/dashboard-features/monitoring/index.ts +16 -0
  20. package/src/dashboard-features/notification/index.ts +40 -0
  21. package/src/dashboard-features/onboarding/index.ts +65 -0
  22. package/src/dashboard-features/organization/index.ts +38 -0
  23. package/src/dashboard-features/types.ts +41 -0
  24. package/src/mobile-features/index.ts +12 -0
  25. package/src/mobile-features/manager.ts +1 -0
  26. package/src/mobile-features/notification/index.ts +41 -0
  27. package/src/mobile-features/onboarding/index.ts +35 -0
  28. package/src/mobile-features/organization/index.ts +38 -0
  29. package/src/mobile-features/types.ts +1 -0
  30. package/src/shims/signal-exit.js +9 -0
  31. package/src/ui/app.tsx +68 -0
  32. package/src/ui/multi-select.tsx +106 -0
  33. package/src/utils/ast.ts +422 -0
  34. package/src/utils/env-template.ts +635 -0
  35. package/tests/test-cli-features.sh +81 -0
  36. package/tests/test-cli-mobile.sh +65 -0
  37. package/tsconfig.json +15 -0
  38. package/tsup.config.ts +21 -0
  39. package/bin/cli.mjs +0 -40
@@ -0,0 +1,349 @@
1
+
2
+ import { FeatureRemover } from './types.js';
3
+ import {
4
+ createProject,
5
+ loadFile,
6
+ removeImport,
7
+ removeHookCall,
8
+ removeObjectFromArray,
9
+ removeI18nNamespace,
10
+ removeInternalPackages,
11
+ unwrapDefaultExport,
12
+ removeSpreadFromObject,
13
+ removeEnvExtendsByModuleSpecifiers
14
+ } from '../utils/ast.js';
15
+ import fs from 'fs-extra';
16
+ import path from 'path';
17
+ import { SyntaxKind } from 'ts-morph';
18
+
19
+ interface FeaturesManifest {
20
+ appType: 'dashboard' | 'mobile';
21
+ selectedFeatures: string[];
22
+ }
23
+
24
+ const matchesNamespacePrefix = (value: string, prefixes: string[]) =>
25
+ prefixes.some(prefix => value === prefix || value.startsWith(`${prefix}-`));
26
+
27
+ const removeMatchingNamespacesFromArray = (arrayLiteral: any, prefixes: string[]) => {
28
+ const elements = arrayLiteral.getElements();
29
+ for (let i = elements.length - 1; i >= 0; i -= 1) {
30
+ const element = elements[i];
31
+ if (element.getKind() !== SyntaxKind.StringLiteral) continue;
32
+ const literalText = element.asKind(SyntaxKind.StringLiteral)?.getLiteralText();
33
+ if (literalText && matchesNamespacePrefix(literalText, prefixes)) {
34
+ arrayLiteral.removeElement(i);
35
+ }
36
+ }
37
+ };
38
+
39
+ const cleanupI18nConfig = async (appRoot: string, namespacePrefixes: string[]) => {
40
+ if (namespacePrefixes.length === 0) return;
41
+
42
+ const i18nConfigPath = path.join(appRoot, 'config', 'i18n.config.ts');
43
+ if (!fs.existsSync(i18nConfigPath)) return;
44
+
45
+ const project = createProject();
46
+ const sourceFile = loadFile(project, i18nConfigPath);
47
+
48
+ const removedImportIdentifiers = new Set<string>();
49
+
50
+ for (const importDeclaration of sourceFile.getImportDeclarations()) {
51
+ const moduleSpecifier = importDeclaration.getModuleSpecifierValue();
52
+ if (!moduleSpecifier.endsWith('.json')) continue;
53
+
54
+ const namespace = path.basename(moduleSpecifier, '.json').replace(/['"]/g, '');
55
+ if (!matchesNamespacePrefix(namespace, namespacePrefixes)) continue;
56
+
57
+ const defaultImport = importDeclaration.getDefaultImport();
58
+ if (defaultImport) {
59
+ removedImportIdentifiers.add(defaultImport.getText());
60
+ }
61
+
62
+ importDeclaration.remove();
63
+ }
64
+
65
+ const propertyAssignments = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment);
66
+ for (const assignment of propertyAssignments) {
67
+ const initializer = assignment.getInitializer();
68
+ if (initializer && removedImportIdentifiers.has(initializer.getText())) {
69
+ assignment.remove();
70
+ continue;
71
+ }
72
+
73
+ const propertyName = assignment.getNameNode().getText().replace(/['"]/g, '');
74
+ if (matchesNamespacePrefix(propertyName, namespacePrefixes)) {
75
+ assignment.remove();
76
+ }
77
+ }
78
+
79
+ const parseI18nConfigCall = sourceFile
80
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
81
+ .find(callExpression => callExpression.getExpression().getText() === 'parseI18nConfig');
82
+
83
+ const optionsArg = parseI18nConfigCall?.getArguments()[0];
84
+ const optionsObject =
85
+ optionsArg?.getKind() === SyntaxKind.ObjectLiteralExpression
86
+ ? optionsArg.asKind(SyntaxKind.ObjectLiteralExpression)
87
+ : undefined;
88
+ const namespacesProperty = optionsObject?.getProperty('namespaces');
89
+ const namespacesInitializer =
90
+ namespacesProperty?.getKind() === SyntaxKind.PropertyAssignment
91
+ ? namespacesProperty.asKind(SyntaxKind.PropertyAssignment)?.getInitializer()
92
+ : undefined;
93
+
94
+ if (namespacesInitializer?.getKind() === SyntaxKind.ArrayLiteralExpression) {
95
+ removeMatchingNamespacesFromArray(
96
+ namespacesInitializer.asKind(SyntaxKind.ArrayLiteralExpression),
97
+ namespacePrefixes,
98
+ );
99
+ } else if (namespacesInitializer?.getKind() === SyntaxKind.Identifier) {
100
+ const namespacesVar = sourceFile.getVariableDeclaration(namespacesInitializer.getText());
101
+ const namespacesVarInitializer = namespacesVar?.getInitializer();
102
+
103
+ if (namespacesVarInitializer?.getKind() === SyntaxKind.ArrayLiteralExpression) {
104
+ removeMatchingNamespacesFromArray(
105
+ namespacesVarInitializer.asKind(SyntaxKind.ArrayLiteralExpression),
106
+ namespacePrefixes,
107
+ );
108
+ } else if (namespacesVarInitializer?.getKind() === SyntaxKind.CallExpression) {
109
+ const namespacesVarCall = namespacesVarInitializer.asKind(SyntaxKind.CallExpression);
110
+ for (const arg of namespacesVarCall?.getArguments() ?? []) {
111
+ if (arg.getKind() === SyntaxKind.ArrayLiteralExpression) {
112
+ removeMatchingNamespacesFromArray(
113
+ arg.asKind(SyntaxKind.ArrayLiteralExpression),
114
+ namespacePrefixes,
115
+ );
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ await sourceFile.save();
122
+ };
123
+
124
+ export const processFeature = async (feature: FeatureRemover, projectRoot: string, appType: 'dashboard' | 'mobile' = 'dashboard') => {
125
+ const dashboardRoot = projectRoot;
126
+ const namespacePrefixes = feature.i18nNamespacePrefix
127
+ ? (Array.isArray(feature.i18nNamespacePrefix)
128
+ ? feature.i18nNamespacePrefix
129
+ : [feature.i18nNamespacePrefix])
130
+ : [];
131
+
132
+ // 1. Remove dependencies from package.json
133
+ if (feature.dependenciesToRemove && feature.dependenciesToRemove.length > 0) {
134
+ const packageJsonPath = path.join(dashboardRoot, 'package.json');
135
+ if (fs.existsSync(packageJsonPath)) {
136
+ const packageJson = await fs.readJson(packageJsonPath);
137
+ let modified = false;
138
+ for (const dep of feature.dependenciesToRemove) {
139
+ if (packageJson.dependencies && packageJson.dependencies[dep]) {
140
+ delete packageJson.dependencies[dep];
141
+ modified = true;
142
+ }
143
+ if (packageJson.devDependencies && packageJson.devDependencies[dep]) {
144
+ delete packageJson.devDependencies[dep];
145
+ modified = true;
146
+ }
147
+ }
148
+ if (modified) {
149
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 4 });
150
+ }
151
+ }
152
+
153
+ // 2. Remove dependencies from next.config.ts (INTERNAL_PACKAGES)
154
+ const nextConfigPath = path.join(dashboardRoot, 'next.config.ts');
155
+ if (fs.existsSync(nextConfigPath)) {
156
+ const project = createProject();
157
+ const sourceFile = loadFile(project, nextConfigPath);
158
+ removeInternalPackages(sourceFile, feature.dependenciesToRemove);
159
+
160
+ // Handle nextConfigProvider if present
161
+ if (feature.nextConfigProvider) {
162
+ const importPath = `@kit/${feature.key}/provider`;
163
+ removeImport(sourceFile, importPath);
164
+ unwrapDefaultExport(sourceFile, feature.nextConfigProvider);
165
+ }
166
+
167
+ await sourceFile.save();
168
+ }
169
+
170
+ // 2b. Remove env imports and extends callbacks from envs.ts
171
+ const envsPath = path.join(dashboardRoot, 'envs.ts');
172
+ if (fs.existsSync(envsPath)) {
173
+ const project = createProject();
174
+ const sourceFile = loadFile(project, envsPath);
175
+ removeEnvExtendsByModuleSpecifiers(sourceFile, feature.dependenciesToRemove);
176
+ await sourceFile.save();
177
+ }
178
+ }
179
+
180
+ // 3. Remove i18n namespace
181
+ if (feature.i18nNamespacePrefix) {
182
+ const i18nPath = path.join(dashboardRoot, '@types/i18next.d.ts');
183
+ if (fs.existsSync(i18nPath)) {
184
+ const project = createProject();
185
+ const sourceFile = loadFile(project, i18nPath);
186
+
187
+ for (const prefix of namespacePrefixes) {
188
+ removeI18nNamespace(sourceFile, prefix);
189
+ }
190
+
191
+ await sourceFile.save();
192
+ }
193
+
194
+ await cleanupI18nConfig(dashboardRoot, namespacePrefixes);
195
+ }
196
+
197
+ // 4. Remove useFilters (hooks/use-filters.ts)
198
+ if (feature.useFilters) {
199
+ const useFiltersPath = path.join(dashboardRoot, 'hooks/use-filters.ts');
200
+ if (fs.existsSync(useFiltersPath)) {
201
+ const project = createProject();
202
+ const sourceFile = loadFile(project, useFiltersPath);
203
+
204
+ const hookModule = appType === 'mobile' ? 'native' : 'www';
205
+ const importPath = `@kit/${feature.key}/${hookModule}/use-filters`;
206
+ removeImport(sourceFile, importPath);
207
+ removeHookCall(sourceFile, feature.useFilters);
208
+
209
+ await sourceFile.save();
210
+ }
211
+ }
212
+
213
+ // 5. Remove proxy (proxy.ts)
214
+ if (feature.proxy) {
215
+ const proxyPath = path.join(dashboardRoot, 'proxy.ts');
216
+ if (fs.existsSync(proxyPath)) {
217
+ const project = createProject();
218
+ const sourceFile = loadFile(project, proxyPath);
219
+
220
+ const proxyModule = appType === 'mobile' ? 'native' : 'www';
221
+ const importPath = `@kit/${feature.key}/${proxyModule}/proxy`;
222
+ removeImport(sourceFile, importPath);
223
+
224
+ // Assume callProxies array contains objects with 'proxy' property matching the variable name
225
+ removeObjectFromArray(sourceFile, 'callProxies', 'proxy', feature.proxy);
226
+
227
+ await sourceFile.save();
228
+ }
229
+ }
230
+
231
+ // 6. Remove cross-env filters (lib/init-cross-env-filters.ts)
232
+ if (feature.crossEnvFilter) {
233
+ const initFiltersPath = path.join(dashboardRoot, 'lib/init-cross-env-filters.ts');
234
+ if (fs.existsSync(initFiltersPath)) {
235
+ const project = createProject();
236
+ const sourceFile = loadFile(project, initFiltersPath);
237
+
238
+ const importPath = `@kit/${feature.key}/www/cross-env-filters`;
239
+ removeImport(sourceFile, importPath);
240
+ removeHookCall(sourceFile, feature.crossEnvFilter);
241
+
242
+ await sourceFile.save();
243
+ }
244
+ }
245
+
246
+ // 7. Remove server filters (lib/init-server-filters.ts)
247
+ if (feature.serverFilter) {
248
+ const initServerFiltersPath = path.join(dashboardRoot, 'lib/init-server-filters.ts');
249
+ if (fs.existsSync(initServerFiltersPath)) {
250
+ const project = createProject();
251
+ const sourceFile = loadFile(project, initServerFiltersPath);
252
+
253
+ const importPath = `@kit/${feature.key}/www/server-filters`;
254
+ removeImport(sourceFile, importPath);
255
+ removeHookCall(sourceFile, feature.serverFilter);
256
+
257
+ await sourceFile.save();
258
+ }
259
+ }
260
+
261
+ // 8. Delete files/directories
262
+ if (feature.filesToDelete && feature.filesToDelete.length > 0) {
263
+ for (const fileOrDir of feature.filesToDelete) {
264
+ const targetPath = path.join(dashboardRoot, fileOrDir);
265
+ if (fs.existsSync(targetPath)) {
266
+ await fs.remove(targetPath);
267
+ }
268
+ }
269
+ }
270
+
271
+ // 9. Remove router entry from shared appRouter file
272
+ if (feature.router) {
273
+ // The router file path is relative to the monorepo root, not the app root.
274
+ // Walk up from dashboardRoot to find the monorepo root.
275
+ const monoMarkers = ['pnpm-workspace.yaml', 'turbo.json', 'lerna.json', 'nx.json'];
276
+ let monoRoot: string | null = null;
277
+ let cur = dashboardRoot;
278
+ while (true) {
279
+ if (monoMarkers.some(m => fs.existsSync(path.join(cur, m)))) {
280
+ monoRoot = cur;
281
+ break;
282
+ }
283
+ const parent = path.dirname(cur);
284
+ if (parent === cur) break;
285
+ cur = parent;
286
+ }
287
+ const resolveRoot = monoRoot ?? dashboardRoot;
288
+ const routerFilePath = path.join(resolveRoot, feature.router.routerFile ?? 'packages/shared/src/server/router.ts');
289
+ if (fs.existsSync(routerFilePath)) {
290
+ const project = createProject();
291
+ const sourceFile = loadFile(project, routerFilePath);
292
+ removeImport(sourceFile, feature.router.importPath);
293
+ removeSpreadFromObject(sourceFile, 'appRouter', feature.router.importName);
294
+ await sourceFile.save();
295
+ }
296
+ }
297
+
298
+ // 10. Run custom apply logic if exists
299
+ if (feature.apply) {
300
+ await feature.apply(projectRoot, appType);
301
+ }
302
+ };
303
+
304
+ /**
305
+ * Handles repo-wide cleanup for a feature when --repo-scope is active.
306
+ * Deletes the kit/{feature.key} package directory (if it exists) and calls
307
+ * feature.repoApply() for any additional custom cleanup.
308
+ */
309
+ export const processFeatureRepo = async (feature: FeatureRemover, repoRoot: string) => {
310
+ // Delete the kit package directory for this feature
311
+ const kitPackageDir = path.join(repoRoot, 'kit', feature.key);
312
+ if (fs.existsSync(kitPackageDir)) {
313
+ await fs.remove(kitPackageDir);
314
+ }
315
+
316
+ // Delete declarative repo-level files/directories
317
+ if (feature.repo?.filesToDelete && feature.repo.filesToDelete.length > 0) {
318
+ for (const fileOrDir of feature.repo.filesToDelete) {
319
+ const targetPath = path.join(repoRoot, fileOrDir);
320
+ if (fs.existsSync(targetPath)) {
321
+ await fs.remove(targetPath);
322
+ }
323
+ }
324
+ }
325
+
326
+ // Run any custom repo-level cleanup
327
+ if (feature.repo?.apply) {
328
+ await feature.repo.apply(repoRoot);
329
+ }
330
+ };
331
+
332
+ export const writeFeatureSelectionManifest = async (
333
+ projectRoot: string,
334
+ appType: 'dashboard' | 'mobile',
335
+ selectedFeatures: string[],
336
+ ) => {
337
+ const normalizedFeatures = Array.from(
338
+ new Set(selectedFeatures.map(key => key.trim()).filter(Boolean)),
339
+ );
340
+
341
+ const manifest: FeaturesManifest = {
342
+ appType,
343
+ selectedFeatures: normalizedFeatures,
344
+ };
345
+
346
+ const metadataDir = path.join(projectRoot, '.creatorem');
347
+ await fs.ensureDir(metadataDir);
348
+ await fs.writeJson(path.join(metadataDir, 'features.json'), manifest, { spaces: 2 });
349
+ };
@@ -0,0 +1,16 @@
1
+ import { FeatureRemover } from '../types.js';
2
+
3
+ export const MonitoringFeature: FeatureRemover = {
4
+ key: 'monitoring',
5
+ cliUI: {
6
+ title: 'Monitoring',
7
+ 'description': 'Monitor your app with Sentry'
8
+ },
9
+ dependenciesToRemove: ['@kit/monitoring'],
10
+ useFilters: 'useMonitoringFilters',
11
+ filesToDelete: [
12
+ 'instrumentation.ts',
13
+ 'instrumentation-client.ts'
14
+ ],
15
+ nextConfigProvider: 'MonitoringProvider.withConfig',
16
+ };
@@ -0,0 +1,40 @@
1
+ import { FeatureRemover } from '../types.js';
2
+ import { createProject, loadFile, removeImport, removeInlineJSX } from '../../utils/ast.js';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+
6
+ export const NotificationFeature: FeatureRemover = {
7
+ key: 'notification',
8
+ cliUI: {
9
+ title: 'Notification',
10
+ features: [
11
+ 'Add notification UI to your dashboard',
12
+ 'Database implementation',
13
+ 'Logic to create notification when you desire'
14
+ ]
15
+ },
16
+ dependenciesToRemove: ['@kit/notification'],
17
+ useFilters: 'useNotificationFilters',
18
+ filesToDelete: [
19
+ 'app/(app)/screens/notifications.tsx',
20
+ "supabase/schemas/023-notifications.sql"
21
+ ],
22
+ apply: async (projectRoot: string, appType: 'dashboard' | 'mobile') => {
23
+ const dashboardRoot = projectRoot;
24
+ const actionGroupPath = path.join(dashboardRoot, 'components/dashboard/dashboard-action-group.tsx');
25
+
26
+ if (fs.existsSync(actionGroupPath)) {
27
+ const project = createProject();
28
+ const sourceFile = loadFile(project, actionGroupPath);
29
+
30
+ removeImport(sourceFile, '@kit/notification/www/ui/notification-button');
31
+ removeInlineJSX(sourceFile, 'NotificationButton');
32
+
33
+ await sourceFile.save();
34
+ }
35
+ },
36
+ router: {
37
+ importName: 'notificationRouter',
38
+ importPath: '@kit/notification/router',
39
+ },
40
+ };
@@ -0,0 +1,65 @@
1
+ import { FeatureRemover } from '../types.js';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+
5
+ export const OnboardingFeature: FeatureRemover = {
6
+ key: 'onboarding',
7
+ cliUI: {
8
+ title: 'Onboarding',
9
+ description: 'Collect additional informations after subscription.'
10
+ },
11
+ dependenciesToRemove: [],
12
+ i18nNamespacePrefix: ['onboarding', 'p_org-onboarding'],
13
+ filesToDelete: [
14
+ 'app/onboarding',
15
+ 'config/onboarding.config.tsx',
16
+ 'public/locales/en/onboarding.json',
17
+ 'public/locales/fr/onboarding.json',
18
+ ],
19
+ apply: async (projectRoot: string) => {
20
+ const dashboardRoot = projectRoot;
21
+
22
+ // 1. Remove 'onboarding' from i18n namespace array in config/i18n.config.ts
23
+ const i18nConfigPath = path.join(dashboardRoot, 'config/i18n.config.ts');
24
+ if (fs.existsSync(i18nConfigPath)) {
25
+ let content = await fs.readFile(i18nConfigPath, 'utf-8');
26
+ // Remove the 'onboarding' entry from the namespaces array
27
+ content = content.replace(/\n?\s*'onboarding',?/g, '');
28
+ await fs.writeFile(i18nConfigPath, content);
29
+ }
30
+
31
+ // 2. Remove completedOnboarding redirect from app/dashboard/layout.tsx
32
+ const dashboardLayoutPath = path.join(dashboardRoot, 'app/dashboard/layout.tsx');
33
+ if (fs.existsSync(dashboardLayoutPath)) {
34
+ let content = await fs.readFile(dashboardLayoutPath, 'utf-8');
35
+ // Remove the completedOnboarding redirect block
36
+ content = content.replace(/\n?\s*if \(!user\.completedOnboarding\) \{[^}]*\}\n?/g, '');
37
+ await fs.writeFile(dashboardLayoutPath, content);
38
+ }
39
+
40
+ // 3. Update authConfig: remove |onboarding from the private scope pattern string
41
+ const authConfigPath = path.join(dashboardRoot, 'config/auth.config.ts');
42
+ if (fs.existsSync(authConfigPath)) {
43
+ let content = await fs.readFile(authConfigPath, 'utf-8');
44
+ // Remove |onboarding (or onboarding|) from the scopePatterns.private value
45
+ content = content.replace(/\|onboarding/g, '');
46
+ content = content.replace(/onboarding\|/g, '');
47
+ await fs.writeFile(authConfigPath, content);
48
+ }
49
+
50
+ // 4. Clean up @types/i18next.d.ts
51
+ const i18nextDtsPath = path.join(dashboardRoot, '@types/i18next.d.ts');
52
+ if (fs.existsSync(i18nextDtsPath)) {
53
+ let content = await fs.readFile(i18nextDtsPath, 'utf-8');
54
+ // Remove the onboarding locale import
55
+ content = content.replace(/import onboarding from '\.\.\/public\/locales\/en\/onboarding\.json';\n?/g, '');
56
+ // Remove the p_org-onboarding kit import
57
+ content = content.replace(/import enOrgOnboarding from '\.\.\/.\.\/\.\.\/(kit|packages)\/organization\/src\/i18n\/locales\/en\/p_org-onboarding\.json';\n?/g, '');
58
+ // Remove 'onboarding: typeof onboarding;' resource type
59
+ content = content.replace(/\n?\s*onboarding: typeof onboarding;/g, '');
60
+ // Remove "'p_org-onboarding': typeof enOrgOnboarding;" resource type
61
+ content = content.replace(/\n?\s*'p_org-onboarding': typeof enOrgOnboarding;/g, '');
62
+ await fs.writeFile(i18nextDtsPath, content);
63
+ }
64
+ }
65
+ };
@@ -0,0 +1,38 @@
1
+ import { FeatureRemover } from '../types.js';
2
+
3
+ export const OrganizationFeature: FeatureRemover = {
4
+ key: 'organization',
5
+ cliUI: {
6
+ title: 'Organization',
7
+ features: [
8
+ 'Database implementation',
9
+ 'Role management',
10
+ 'Member management',
11
+ 'Built React logic (useOrganization, <OrganizationSwitcher />, ...)'
12
+ ]
13
+ },
14
+ dependenciesToRemove: [
15
+ '@kit/organization',
16
+ ],
17
+ i18nNamespacePrefix: 'p_org',
18
+ useFilters: 'useOrgFilters',
19
+ proxy: 'organizationProxy',
20
+ crossEnvFilter: 'initOrgFilters',
21
+ serverFilter: 'initOrgServerFilters',
22
+ router: {
23
+ importName: 'organizationRouter',
24
+ importPath: '@kit/organization/router',
25
+ },
26
+ repo: {
27
+ filesToDelete: [
28
+ 'supabase/schemas/030-organization-enums.sql',
29
+ 'supabase/schemas/031-kit-org.sql',
30
+ 'supabase/schemas/032-organization.sql',
31
+ 'supabase/schemas/033-organization-roles.sql',
32
+ 'supabase/schemas/034-organization-members.sql',
33
+ 'supabase/schemas/035-organization-invitations.sql',
34
+ 'supabase/schemas/036-organization-settings.sql',
35
+ 'supabase/schemas/037-organization-notifications.sql',
36
+ ],
37
+ },
38
+ };
@@ -0,0 +1,41 @@
1
+ export interface FeatureRemover {
2
+ key: string;
3
+ cliUI: {
4
+ title: string;
5
+ description?: string;
6
+ features?: string[]
7
+ }
8
+ dependenciesToRemove: string[];
9
+ i18nNamespacePrefix?: string | string[];
10
+
11
+ // Declarative properties
12
+ useFilters?: string; // Name of the hook. Import inferred: @kit/{key}/www/use-filters
13
+ proxy?: string; // Name of the proxy variable. Import inferred: @kit/{key}/www/proxy
14
+ crossEnvFilter?: string; // Name of the filter. Import inferred: @kit/{key}/www/cross-env-filters
15
+ serverFilter?: string; // Name of the filter. Import inferred: @kit/{key}/www/server-filters
16
+ nextConfigProvider?: string; // Name of the provider to unwrap in next.config.ts. Import inferred: @kit/{key}/provider
17
+
18
+ filesToDelete?: string[]; // Files or directories to delete (relative to app root)
19
+
20
+ /**
21
+ * Router to remove from the shared appRouter.
22
+ * importName: the exported identifier (e.g. 'aiRouter', 'getKeybindingsRouter')
23
+ * routerFile: path to the file containing appRouter, relative to app root
24
+ * (defaults to 'lib/server/router.ts' if omitted)
25
+ */
26
+ router?: {
27
+ importName: string; // e.g. 'aiRouter' | 'getKeybindingsRouter'
28
+ importPath: string; // e.g. '@kit/ai/router'
29
+ routerFile?: string; // relative to app root, default: 'lib/server/router.ts'
30
+ };
31
+
32
+ apply?: (projectRoot: string, appType: 'dashboard' | 'mobile') => Promise<void>;
33
+
34
+ /** Repo-scope cleanup, applied when --repo-scope is active. */
35
+ repo?: {
36
+ /** Files or directories to delete relative to the monorepo root. */
37
+ filesToDelete?: string[];
38
+ /** Custom monorepo-level cleanup (kit packages, supabase schemas, etc.). */
39
+ apply?: (repoRoot: string) => Promise<void>;
40
+ };
41
+ }
@@ -0,0 +1,12 @@
1
+ import { FeatureRemover } from './types.js';
2
+ import { MobileOrganizationFeature } from './organization/index.js';
3
+ import { MobileNotificationFeature } from './notification/index.js';
4
+ import { MobileOnboardingFeature } from './onboarding/index.js';
5
+
6
+ export const mobileFeatures: FeatureRemover[] = [
7
+ MobileOrganizationFeature,
8
+ MobileNotificationFeature,
9
+ MobileOnboardingFeature,
10
+ ];
11
+
12
+ export const getMobileFeature = (key: string) => mobileFeatures.find(f => f.key === key);
@@ -0,0 +1 @@
1
+ export { processFeature, processFeatureRepo, writeFeatureSelectionManifest } from '../dashboard-features/manager.js';
@@ -0,0 +1,41 @@
1
+ import { FeatureRemover } from '../types.js';
2
+ import { createProject, loadFile, removeImport, removeInlineJSX } from '../../utils/ast.js';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+
6
+ export const MobileNotificationFeature: FeatureRemover = {
7
+ key: 'notification',
8
+ cliUI: {
9
+ title: 'Notification',
10
+ features: [
11
+ 'Add notification UI to your dashboard',
12
+ 'Database implementation',
13
+ 'Logic to create notification when you desire'
14
+ ]
15
+ },
16
+ dependenciesToRemove: ['@kit/notification'],
17
+ i18nNamespacePrefix: 'notification',
18
+ router: {
19
+ importName: 'notificationRouter',
20
+ importPath: '@kit/notification/router',
21
+ },
22
+ filesToDelete: [
23
+ 'app/(app)/screens/notifications.tsx',
24
+ 'components/notification-icon.tsx',
25
+ 'locales/en/notification.json',
26
+ 'locales/fr/notification.json',
27
+ "supabase/schemas/023-notifications.sql"
28
+ ],
29
+ apply: async (projectRoot: string) => {
30
+ const homeTabsPath = path.join(projectRoot, 'app/(app)/(tabs)/index.tsx');
31
+ if (!fs.existsSync(homeTabsPath)) return;
32
+
33
+ const project = createProject();
34
+ const sourceFile = loadFile(project, homeTabsPath);
35
+
36
+ removeImport(sourceFile, '~/components/notification-icon');
37
+ removeInlineJSX(sourceFile, 'NotificationIcon');
38
+
39
+ await sourceFile.save();
40
+ },
41
+ };
@@ -0,0 +1,35 @@
1
+ import { FeatureRemover } from '../types.js';
2
+ import { createProject, loadFile } from '../../utils/ast.js';
3
+ import { SyntaxKind } from 'ts-morph';
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+
7
+ export const MobileOnboardingFeature: FeatureRemover = {
8
+ key: 'onboarding',
9
+ cliUI: {
10
+ title: 'Onboarding',
11
+ description: 'Collect additional informations after subscription.'
12
+ },
13
+ dependenciesToRemove: [],
14
+ i18nNamespacePrefix: ['p_org-onboarding'],
15
+ filesToDelete: [
16
+ 'app/onboarding',
17
+ 'config/onboarding.config.tsx',
18
+ ],
19
+ apply: async (projectRoot: string) => {
20
+ const appLayoutPath = path.join(projectRoot, 'app/(app)/_layout.tsx');
21
+ if (!fs.existsSync(appLayoutPath)) return;
22
+
23
+ const project = createProject();
24
+ const sourceFile = loadFile(project, appLayoutPath);
25
+
26
+ const ifStatements = sourceFile.getDescendantsOfKind(SyntaxKind.IfStatement);
27
+ for (const ifStatement of ifStatements) {
28
+ if (ifStatement.getExpression().getText() === '!user.data.completedOnboarding') {
29
+ ifStatement.remove();
30
+ }
31
+ }
32
+
33
+ await sourceFile.save();
34
+ },
35
+ };