@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.
- 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 +9 -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
|
+
|
|
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
|
+
};
|