startx 0.7.2 → 0.8.0

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 (34) hide show
  1. package/.prettierrc.js +2 -2
  2. package/.vscode/settings.json +2 -1
  3. package/apps/core-server/package.json +1 -1
  4. package/apps/core-server/src/index.ts +0 -1
  5. package/apps/startx-cli/dist/index.mjs +2 -2
  6. package/apps/startx-cli/src/commands/init.ts +183 -255
  7. package/apps/startx-cli/src/configs/files.ts +3 -4
  8. package/apps/startx-cli/src/configs/scripts.ts +24 -6
  9. package/apps/startx-cli/src/types.ts +18 -6
  10. package/apps/startx-cli/src/utils/cli-utils.ts +60 -49
  11. package/apps/startx-cli/src/utils/file-handler.ts +8 -3
  12. package/biome.json +1 -1
  13. package/configs/eslint-config/eslint.config.ts +0 -0
  14. package/configs/eslint-config/src/configs/base.ts +32 -79
  15. package/configs/eslint-config/src/configs/extend.ts +2 -2
  16. package/configs/eslint-config/src/configs/frontend.ts +29 -19
  17. package/configs/eslint-config/src/configs/node.ts +46 -6
  18. package/configs/vitest-config/package.json +3 -2
  19. package/package.json +3 -2
  20. package/packages/@repo/db/drizzle.config.ts +14 -0
  21. package/packages/@repo/db/package.json +5 -1
  22. package/packages/@repo/db/src/index.ts +1 -1
  23. package/packages/@repo/lib/src/otp-module/index.ts +6 -13
  24. package/packages/@repo/lib/tsconfig.json +2 -1
  25. package/packages/@repo/mail/eslint.config.ts +2 -2
  26. package/packages/@repo/mail/src/emails/admin/OtpEmail.tsx +3 -9
  27. package/packages/@repo/mail/src/emails/emails.ts +1 -0
  28. package/packages/@repo/mail/src/index.ts +10 -9
  29. package/packages/@repo/mail/tsconfig.json +4 -2
  30. package/packages/ui/package.json +1 -0
  31. package/packages/ui/src/components/ui/command.tsx +5 -15
  32. package/pnpm-workspace.yaml +1 -0
  33. package/turbo.json +9 -0
  34. package/apps/startx-cli/src/utils/config.ts +0 -104
@@ -5,8 +5,8 @@ import path from "path";
5
5
  import z from "zod";
6
6
 
7
7
  import { FileCheck } from "../configs/files";
8
- import type { StartXPackageJson, TAGS } from "../types";
9
- import { CliUtils } from "../utils/cli-utils";
8
+ import type { TAGS } from "../types";
9
+ import { CliUtils, type PackageItem } from "../utils/cli-utils";
10
10
  import { FileHandler } from "../utils/file-handler";
11
11
  import { CommonInquirer } from "../utils/inquirer";
12
12
 
@@ -14,13 +14,6 @@ type InitOptions = {
14
14
  dir?: string;
15
15
  };
16
16
 
17
- type PackageWithJson = {
18
- packageJson: StartXPackageJson | null;
19
- type: "apps" | "configs" | "packages";
20
- path: string;
21
- name: string;
22
- };
23
-
24
17
  export class InitCommand {
25
18
  static command = new Command("init")
26
19
  .argument("[projectName]")
@@ -28,81 +21,65 @@ export class InitCommand {
28
21
  .action(InitCommand.run.bind(InitCommand));
29
22
 
30
23
  private static async run(projectName: string | undefined, options: InitOptions) {
31
- const packageList: PackageWithJson[] = await Promise.all(
32
- (await CliUtils.getPackageList()).map(async e => ({
33
- ...e,
34
- packageJson: await CliUtils.parsePackageJson({ dir: e.path }),
35
- }))
36
- );
37
-
38
- const prefs = await InitCommand.getPrefs({
39
- projectName,
40
- options,
41
- projects: packageList.filter(e => {
42
- if (e.type !== "apps") return false;
43
- if (e.packageJson?.startx?.mode === "silent") return false;
44
- return true;
45
- }),
46
- });
47
-
48
- const packages = await Promise.all(
49
- packageList
50
- .filter(e => e.type !== "apps")
51
- .map(async e => ({
52
- ...e,
53
- packageJson: await CliUtils.parsePackageJson({ dir: e.path }),
54
- }))
55
- );
24
+ const packageList = await CliUtils.getPackageList();
25
+ const availableApps = packageList.filter(pkg => pkg.type === "apps" && pkg.packageJson?.startx?.mode !== "silent");
26
+ const prefs = await this.getPrefs({ projectName, options, projects: availableApps });
27
+ const nonAppPackages = packageList.filter(pkg => pkg.type !== "apps");
56
28
 
57
29
  const config = await this.getConfigPrefs({
58
30
  selectedApps: prefs.selectedApps,
59
- packages,
31
+ packages: nonAppPackages,
60
32
  });
61
33
 
62
34
  const packagePrefs = await this.getPackagesPrefs({
63
35
  selectedApps: prefs.selectedApps,
64
36
  selectedPackages: config.selectedConfigs,
65
- packages,
66
- tags: config.tags,
37
+ packages: nonAppPackages,
38
+ tags: config.gTags,
67
39
  });
40
+
41
+ // Installing Workspace
42
+ const workspaceTags = [...packagePrefs.gTags, "runnable"] as TAGS[];
68
43
  await this.installWorkspace({
69
44
  name: prefs.projectName,
70
- tags: [...packagePrefs.tags, "runnable"],
71
- dir: {
72
- workspace: prefs.directory.workspace,
73
- template: prefs.directory.template,
74
- },
45
+ tags: [...workspaceTags, "runnable"],
46
+ dir: prefs.directory,
75
47
  });
76
48
 
49
+ // Installing Apps
50
+ const allSelectedPackages = [...packagePrefs.selectedPackages, ...prefs.selectedApps];
77
51
  await Promise.all(
78
- packagePrefs.selectedPackages.concat(prefs.selectedApps).map(async pkg => {
79
- const appDeps = new Map<string, string>();
80
- const tags = new Set(packagePrefs.tags);
52
+ allSelectedPackages.map(async pkg => {
53
+ const appDeps: Record<string, string> = {};
54
+ const tags = new Set<TAGS>(packagePrefs.gTags);
81
55
 
82
- if (pkg.packageJson?.startx?.mode === "standalone") tags.add("runnable");
56
+ if (pkg.packageJson?.startx?.mode === "standalone") {
57
+ tags.add("runnable");
58
+ }
83
59
 
84
60
  if (pkg.type === "apps") {
85
61
  tags.add("runnable");
86
62
 
87
63
  packagePrefs.selectedPackages
88
- .filter(
89
- e =>
90
- e.packageJson?.startx?.mode !== "standalone" &&
91
- e.packageJson?.startx?.iTags?.every(tag =>
92
- pkg.packageJson?.startx?.tags?.includes(tag)
93
- )
94
- )
95
- .forEach(e => appDeps.set(e.packageJson?.name || e.name, "workspace:^"));
64
+ .filter(depPkg => {
65
+ if (depPkg.type !== "packages") return false;
66
+ if (depPkg.packageJson?.startx?.mode !== "standalone") return false;
67
+ const sharesTags = depPkg.packageJson?.startx?.iTags?.every(tag =>
68
+ pkg.packageJson?.startx?.gTags?.includes(tag)
69
+ );
70
+ return sharesTags;
71
+ })
72
+ .forEach(depPkg => {
73
+ const depName = depPkg.packageJson?.name || depPkg.name;
74
+ appDeps[depName] = "workspace:^";
75
+ });
96
76
  }
97
77
 
98
78
  await this.installPackage({
99
- packages: pkg,
100
- directory: {
101
- workspace: prefs.directory.workspace,
102
- template: prefs.directory.template,
103
- },
79
+ pkg,
80
+ directory: prefs.directory,
104
81
  tags: Array.from(tags),
105
- dependencies: Object.fromEntries(appDeps),
82
+ dependencies: appDeps,
106
83
  });
107
84
  })
108
85
  );
@@ -112,104 +89,69 @@ export class InitCommand {
112
89
  projectName?: string;
113
90
  directory?: string;
114
91
  options: InitOptions;
115
- projects: PackageWithJson[];
92
+ projects: PackageItem[];
116
93
  }) {
117
- if (!props.projectName) {
118
- props.projectName = await CommonInquirer.getText({
119
- message: "Project name",
120
- name: "projectName",
121
- schema: z.string().min(2).trim(),
122
- });
123
- }
124
-
125
- // Handling workspace directory
126
- const directory = CliUtils.getDirectory();
127
- let workspace;
128
- if (props.options.dir) {
129
- try {
130
- workspace = path.resolve(directory.workspace, props.options.dir);
131
- } catch {
132
- throw new Error("Invalid template directory");
133
- }
134
- } else {
135
- workspace = path.join(directory.workspace, props.projectName);
136
- }
137
-
94
+ const projectName = await CommonInquirer.getText({
95
+ message: "Project name",
96
+ name: "projectName",
97
+ default: props.projectName,
98
+ schema: z
99
+ .string()
100
+ .min(1, "Package name is required")
101
+ .max(214, "Package name too long")
102
+ .regex(/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/, "Invalid package name"),
103
+ });
138
104
  if (props.projects.length === 0) {
139
- throw new Error("No apps found");
105
+ throw new Error("No apps found to install.");
140
106
  }
107
+ const directory = CliUtils.getDirectory();
108
+ const workspace = props.options.dir
109
+ ? path.resolve(directory.workspace, props.options.dir)
110
+ : path.join(directory.workspace, projectName);
141
111
 
142
112
  const selectedAppNames = await CommonInquirer.choose({
143
113
  message: "Select apps to install",
144
- options: props.projects.map(e => e.name),
114
+ options: props.projects.map(pkg => pkg.name),
145
115
  includeAllOption: true,
146
116
  mode: "multiple",
147
117
  required: true,
148
118
  });
149
119
 
150
- const selectedApps = props.projects.filter(e => selectedAppNames.includes(e.name));
151
-
152
120
  return {
153
- projectName: props.projectName,
154
- directory: {
155
- workspace,
156
- template: directory.template,
157
- },
158
- selectedApps,
121
+ projectName,
122
+ directory: { workspace, template: directory.template },
123
+ selectedApps: props.projects.filter(pkg => selectedAppNames.includes(pkg.name)),
159
124
  };
160
125
  }
161
126
 
162
- private static async getConfigPrefs(props: {
163
- packages: PackageWithJson[];
164
- selectedApps: PackageWithJson[];
165
- }) {
166
- const tags = new Set<TAGS>(["common"]);
167
- const configs = new Map<string, PackageWithJson>();
168
- // Add additional tags from apps
169
- props.selectedApps
170
- .flatMap(app => app.packageJson?.startx?.gTags || [])
171
- .forEach(tag => tags.add(tag));
172
-
173
- // Add required configs from apps
174
- props.selectedApps
175
- .flatMap(app => [
176
- ...(app.packageJson?.startx?.requiredDeps || []),
177
- ...(app.packageJson?.startx?.requiredDevDeps || []),
178
- ])
179
- .forEach(pkg => {
180
- const config = props.packages.find(e => e.packageJson?.name === pkg);
181
- if (config) configs.set(config.name, config);
182
- });
183
-
184
- const availableConfigs = props.packages.filter(e => {
185
- if (e.type !== "configs") return false;
186
- if (e.packageJson?.startx?.mode === "silent") {
187
- return false;
188
- }
189
- if (configs.has(e.name)) return false;
190
- if (!e.packageJson?.startx?.iTags?.every(t => tags.has(t))) return false;
191
- return true;
192
- });
193
-
194
- if (availableConfigs.length === 0)
195
- return {
196
- tags: Array.from(tags),
197
- selectedConfigs: Array.from(configs.values()),
198
- };
199
-
200
- const rawSelectedConfigs = await CommonInquirer.choose({
201
- message: "Select configs to install",
202
- options: availableConfigs.map(e => e.name),
203
- includeAllOption: true,
204
- mode: "multiple",
205
- required: false,
127
+ private static async getConfigPrefs(props: { packages: PackageItem[]; selectedApps: PackageItem[] }) {
128
+ const gTags = new Set<TAGS>(["common"]);
129
+ const configs = new Map<string, PackageItem>();
130
+ // Selected apps globals tags and dependencies resolver
131
+ this.getGlobalTags({ pkgs: props.selectedApps }).forEach(tag => gTags.add(tag));
132
+ this.getPackageDeps({
133
+ allPkgs: props.packages,
134
+ pkgs: props.selectedApps,
135
+ }).forEach(pkg => configs.set(pkg.name, pkg));
136
+
137
+ const availableConfigs = props.packages.filter(pkg => {
138
+ if (pkg.type !== "configs") return false;
139
+ if (pkg.packageJson?.startx?.mode === "silent") return false;
140
+ if (configs.has(pkg.name)) return false;
141
+ return pkg.packageJson?.startx?.iTags?.every(t => gTags.has(t)) ?? true;
206
142
  });
143
+ if (availableConfigs.length > 0) {
144
+ const rawSelectedConfigs = await CommonInquirer.choose({
145
+ message: "Select configs to install",
146
+ options: availableConfigs.map(pkg => pkg.name),
147
+ includeAllOption: true,
148
+ mode: "multiple",
149
+ required: false,
150
+ });
151
+ availableConfigs.filter(pkg => rawSelectedConfigs.includes(pkg.name)).forEach(pkg => configs.set(pkg.name, pkg));
152
+ }
207
153
 
208
- availableConfigs
209
- .filter(e => rawSelectedConfigs.includes(e.name))
210
- .forEach(e => configs.set(e.name, e));
211
-
212
- if (tags.has("node")) {
154
+ if (gTags.has("node")) {
213
155
  const formatter: string | string[] = await CommonInquirer.choose({
214
156
  message: "Select formatter",
215
157
  options: ["prettier + biome", "prettier"],
@@ -218,100 +160,73 @@ export class InitCommand {
218
160
  required: true,
219
161
  });
220
162
  if (formatter === "prettier") {
221
- tags.add("prettier");
163
+ gTags.add("prettier");
222
164
  } else {
223
- tags.add("biome");
224
- tags.add("prettier");
165
+ gTags.add("biome");
166
+ gTags.add("prettier");
225
167
  }
226
168
  }
227
169
 
228
- // Include required configs
229
- Array.from(configs.values()).forEach(e => {
230
- const requiredDeps = e.packageJson?.startx?.requiredDeps || [];
231
- const requiredDevDeps = e.packageJson?.startx?.requiredDevDeps || [];
232
-
233
- const required = [...requiredDeps, ...requiredDevDeps];
234
- required.forEach(pkg => {
235
- const config = props.packages.find(e => e.packageJson?.name === pkg);
236
- if (config) {
237
- configs.set(config.name, config);
238
- }
239
- });
240
- });
170
+ // Resolving deps for selected configs
171
+ this.getPackageDeps({
172
+ allPkgs: props.packages,
173
+ pkgs: Array.from(configs.values()),
174
+ }).forEach(pkg => configs.set(pkg.name, pkg));
241
175
 
242
- // Include all tags
243
- Array.from(configs.values()).forEach(e => {
244
- const gTags = e.packageJson?.startx?.gTags || [];
245
- gTags.forEach(tag => tags.add(tag));
246
- });
176
+ // Adding global tags
177
+ this.getGlobalTags({ pkgs: Array.from(configs.values()) }).forEach(tag => gTags.add(tag));
247
178
 
248
179
  return {
249
- tags: Array.from(tags),
180
+ gTags: Array.from(gTags),
250
181
  selectedConfigs: Array.from(configs.values()),
251
182
  };
252
183
  }
253
184
 
254
185
  private static async getPackagesPrefs(props: {
255
186
  tags: TAGS[];
256
- packages: PackageWithJson[];
257
- selectedApps: PackageWithJson[];
258
- selectedPackages: PackageWithJson[];
187
+ packages: PackageItem[];
188
+ selectedApps: PackageItem[];
189
+ selectedPackages: PackageItem[];
259
190
  }) {
260
- const appTags = new Set<TAGS>(props.tags);
261
- const packages = new Map<string, PackageWithJson>(props.selectedPackages.map(e => [e.name, e]));
191
+ const gTags = new Set<TAGS>(props.tags);
192
+ const packages = new Map<string, PackageItem>(props.selectedPackages.map(pkg => [pkg.name, pkg]));
262
193
  const availablePackages = props.packages.filter(pkg => {
263
194
  if (pkg.type !== "packages") return false;
264
- if (pkg.packageJson?.startx?.mode === "silent") {
265
- return false;
266
- }
195
+ if (pkg.packageJson?.startx?.mode === "silent") return false;
267
196
  if (packages.has(pkg.name)) return false;
268
- if (!pkg.packageJson?.startx?.iTags?.every(t => appTags.has(t))) return false;
269
- return true;
270
- });
271
- if (!availablePackages.length)
272
- return {
273
- selectedPackages: Array.from(packages.values()),
274
- tags: Array.from(appTags),
275
- };
276
- const rawSelectedPackages = await CommonInquirer.choose({
277
- message: "Select packages to install",
278
- options: availablePackages.map(e => e.name),
279
- includeAllOption: true,
280
- mode: "multiple",
281
- required: false,
197
+ return pkg.packageJson?.startx?.iTags?.every(t => gTags.has(t)) ?? false;
282
198
  });
199
+ if (availablePackages.length > 0) {
200
+ const rawSelectedPackages = await CommonInquirer.choose({
201
+ message: "Select packages to install",
202
+ options: availablePackages.map(pkg => pkg.name),
203
+ includeAllOption: true,
204
+ mode: "multiple",
205
+ required: false,
206
+ });
283
207
 
284
- availablePackages
285
- .filter(e => rawSelectedPackages.includes(e.name))
286
- .forEach(e => packages.set(e.name, e));
287
-
288
- Array.from(packages.values()).forEach(e => {
289
- const requiredDeps = e.packageJson?.startx?.requiredDeps || [];
290
- const requiredDevDeps = e.packageJson?.startx?.requiredDevDeps || [];
208
+ availablePackages
209
+ .filter(pkg => rawSelectedPackages.includes(pkg.name))
210
+ .forEach(pkg => packages.set(pkg.name, pkg));
211
+ }
291
212
 
292
- const required = [...requiredDeps, ...requiredDevDeps];
293
- required.forEach(tag => {
294
- const config = props.packages.find(e => e.packageJson?.name === tag);
295
- if (config) {
296
- packages.set(config.name, config);
297
- }
298
- });
299
- });
213
+ this.getPackageDeps({
214
+ allPkgs: props.packages,
215
+ pkgs: Array.from(packages.values()),
216
+ }).forEach(pkg => packages.set(pkg.name, pkg));
300
217
 
301
- // Include all tags
302
- Array.from(packages.values()).forEach(e => {
303
- const gTags = e.packageJson?.startx?.gTags || [];
304
- gTags.forEach(tag => appTags.add(tag));
305
- });
218
+ this.getGlobalTags({
219
+ pkgs: Array.from(packages.values()),
220
+ }).forEach(tag => gTags.add(tag));
306
221
 
307
222
  return {
308
- tags: Array.from(appTags),
223
+ gTags: Array.from(gTags),
309
224
  selectedPackages: Array.from(packages.values()),
310
225
  };
311
226
  }
312
227
 
313
228
  private static async installPackage(props: {
314
- packages: PackageWithJson;
229
+ pkg: PackageItem;
315
230
  directory: {
316
231
  workspace: string;
317
232
  template: string;
@@ -319,57 +234,41 @@ export class InitCommand {
319
234
  tags: TAGS[];
320
235
  dependencies: Record<string, string>;
321
236
  }) {
322
- const tags = new Set(props.tags.concat(props.packages.packageJson?.startx?.tags || []));
323
- if (props.packages.packageJson?.startx?.ignore?.find(e => e === "eslint-config"))
324
- tags.delete("eslint");
325
- if (props.packages.packageJson?.startx?.ignore?.find(e => e === "vitest-config"))
326
- tags.delete("vitest");
237
+ if (!props.pkg.packageJson) {
238
+ throw new Error(`Missing package.json for ${props.pkg.name}`);
239
+ }
240
+ const tags = new Set<TAGS>([...props.tags, ...(props.pkg.packageJson.startx?.tags || [])]);
241
+ const ignoreList = props.pkg.packageJson.startx?.ignore || [];
242
+ if (ignoreList.includes("eslint-config")) tags.delete("eslint");
243
+ if (ignoreList.includes("vitest-config")) tags.delete("vitest");
327
244
  const { packageJson, isWorkspace } = FileHandler.handlePackageJson({
328
- app: props.packages.packageJson!,
245
+ app: props.pkg.packageJson,
329
246
  tags: Array.from(tags),
330
- name: props.packages.name,
247
+ name: props.pkg.name,
331
248
  });
332
249
 
333
- if (isWorkspace) throw new Error("Can't install workspace as package.");
334
-
335
- let iDirectory = path.join(props.directory.workspace, props.packages.type);
336
- let iTemplate = path.join(props.directory.template, props.packages.type);
337
- if (props.packages.packageJson?.name?.startsWith("@repo")) {
338
- const repoName = props.packages.packageJson.name.split("/")[1];
339
- iDirectory = path.join(iDirectory, "@repo", repoName);
340
- iTemplate = path.join(iTemplate, "@repo", repoName);
341
- } else {
342
- iDirectory = path.join(iDirectory, props.packages.name);
343
- iTemplate = path.join(iTemplate, props.packages.name);
250
+ if (isWorkspace) {
251
+ throw new Error(`Cannot install workspace as a package: ${props.pkg.name}`);
344
252
  }
345
-
346
- await fsTool.writeJSONFile({
347
- dir: iDirectory,
348
- file: "package",
349
- content: packageJson,
350
- });
351
-
352
- const files = await fsTool.listFiles({ dir: iTemplate });
353
- for (const file of files) {
354
- const checked = FileCheck[file];
355
- if (checked && !checked.tags.every(e => tags.has(e))) continue;
356
- try {
357
- await fsTool.copyFile({
358
- from: path.join(iTemplate, file),
359
- to: path.join(iDirectory, file),
360
- });
361
- } catch (error) {
362
- logger.error(`Failed to copy file ${file}:`, error);
363
- }
253
+ if (Object.keys(props.dependencies).length > 0) {
254
+ packageJson.dependencies = {
255
+ ...(packageJson.dependencies as object),
256
+ ...props.dependencies,
257
+ };
364
258
  }
365
259
 
366
- // Installing src
260
+ const iDirectory = path.join(props.directory.workspace, props.pkg.relativePath);
261
+ const iTemplate = path.join(props.pkg.path);
262
+ await fsTool.writeJSONFile({ dir: iDirectory, file: "package", content: packageJson });
263
+
264
+ await this.copyValidatedFilesFromFolder(iTemplate, iDirectory, tags);
367
265
  await fsTool.copyDirectory({
368
266
  from: path.join(iTemplate, "src"),
369
267
  to: path.join(iDirectory, "src"),
370
268
  exclude: !tags.has("vitest") ? /\.test\.tsx?$/ : undefined,
371
269
  });
372
- logger.info(`Successfully installed ${props.packages.name}`);
270
+
271
+ logger.info(`Successfully installed ${props.pkg.name}`);
373
272
  }
374
273
 
375
274
  private static async installWorkspace(props: {
@@ -380,19 +279,19 @@ export class InitCommand {
380
279
  template: string;
381
280
  };
382
281
  }) {
383
- const rootTags = ["root", ...props.tags] as TAGS[];
384
282
  const rawPackage = await CliUtils.parsePackageJson({ dir: props.dir.template });
385
283
  const startXRawPackage = await CliUtils.parsePackageJson({
386
284
  dir: props.dir.template,
387
285
  file: "startx",
388
286
  });
389
287
 
390
- if (!rawPackage) throw new Error("Failed to parse package.json");
391
- rawPackage.dependencies = startXRawPackage?.dependencies || {};
392
- rawPackage.devDependencies = startXRawPackage?.devDependencies || {};
288
+ if (!rawPackage) throw new Error("Failed to parse root package.json");
289
+ rawPackage.dependencies = { ...rawPackage.dependencies, ...(startXRawPackage?.dependencies || {}) };
290
+ rawPackage.devDependencies = { ...rawPackage.devDependencies, ...(startXRawPackage?.devDependencies || {}) };
291
+
393
292
  const { packageJson } = FileHandler.handlePackageJson({
394
293
  app: rawPackage,
395
- tags: rootTags,
294
+ tags: ["root", ...props.tags] as TAGS[],
396
295
  name: props.name,
397
296
  });
398
297
 
@@ -401,15 +300,44 @@ export class InitCommand {
401
300
  file: "package",
402
301
  content: packageJson,
403
302
  });
303
+ await this.copyValidatedFilesFromFolder(
304
+ props.dir.template,
305
+ props.dir.workspace,
306
+ new Set(["root", ...props.tags] as TAGS[])
307
+ );
308
+ }
404
309
 
405
- const files = await fsTool.listFiles({ dir: props.dir.template });
310
+ // Helpers
311
+ private static getPackageDeps(props: { pkgs: PackageItem[]; allPkgs: PackageItem[] }) {
312
+ const deps = new Map<string, PackageItem>(props.pkgs.map(pkg => [pkg.name, pkg]));
313
+ Array.from(deps.values()).forEach(pkg => {
314
+ const required = [
315
+ ...(pkg.packageJson?.startx?.requiredDeps || []),
316
+ ...(pkg.packageJson?.startx?.requiredDevDeps || []),
317
+ ];
318
+ required.forEach(reqPkgName => {
319
+ const config = props.allPkgs.find(p => p.packageJson?.name === reqPkgName);
320
+ if (config) deps.set(config.name, config);
321
+ });
322
+ });
323
+ return Array.from(deps.values());
324
+ }
325
+ private static getGlobalTags(props: { pkgs: PackageItem[]; gTags?: TAGS[] }) {
326
+ const tags = new Set<TAGS>(props.gTags || []);
327
+ props.pkgs.forEach(pkg => {
328
+ pkg.packageJson?.startx?.gTags?.forEach(tag => tags.add(tag));
329
+ });
330
+ return Array.from(tags);
331
+ }
332
+ private static async copyValidatedFilesFromFolder(source: string, destination: string, tags: Set<TAGS>) {
333
+ const files = await fsTool.listFiles({ dir: source }).catch(() => []);
406
334
  for (const file of files) {
407
335
  const checked = FileCheck[file];
408
- if (checked && !checked.tags.every(e => rootTags.includes(e))) continue;
336
+ if (checked && !checked.tags.every(tag => tags.has(tag))) continue;
409
337
  try {
410
338
  await fsTool.copyFile({
411
- from: path.join(props.dir.template, file),
412
- to: path.join(props.dir.workspace, file),
339
+ from: path.join(source, file),
340
+ to: path.join(destination, file),
413
341
  });
414
342
  } catch (error) {
415
343
  logger.error(`Failed to copy file ${file}:`, error);
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/naming-convention */
2
1
  import type { WHITELIST_FILES } from "../types";
3
2
 
4
3
  export const FileCheck: WHITELIST_FILES = {
@@ -11,12 +10,12 @@ export const FileCheck: WHITELIST_FILES = {
11
10
  ".npmrc": {
12
11
  tags: ["never"],
13
12
  },
14
- ".prettierignore": {
15
- tags: ["prettier"],
16
- },
17
13
  ".prettier.js": {
18
14
  tags: ["prettier"],
19
15
  },
16
+ ".prettierignore": {
17
+ tags: ["biome"],
18
+ },
20
19
  "biome.json": {
21
20
  tags: ["biome"],
22
21
  },
@@ -1,5 +1,3 @@
1
- /* eslint-disable @typescript-eslint/naming-convention */
2
-
3
1
  import type { SCRIPT } from "../types";
4
2
 
5
3
  export const scripts: SCRIPT = {
@@ -10,7 +8,7 @@ export const scripts: SCRIPT = {
10
8
  },
11
9
  {
12
10
  script: "tsx watch src/index.ts",
13
- tags: ["runnable", "node", "backend", "dev"],
11
+ tags: ["runnable", "node", "backend", "express"],
14
12
  },
15
13
  {
16
14
  script: "email dev --port 3014 --dir ./src",
@@ -24,7 +22,7 @@ export const scripts: SCRIPT = {
24
22
  },
25
23
  {
26
24
  script: "tsx watch --inspect src/index.ts",
27
- tags: ["backend", "node", "runnable", "dev"],
25
+ tags: ["backend", "node", "runnable", "express"],
28
26
  },
29
27
  ],
30
28
  "bun:dev": [
@@ -34,7 +32,7 @@ export const scripts: SCRIPT = {
34
32
  },
35
33
  {
36
34
  script: "bun --watch src/index.ts",
37
- tags: ["backend", "node", "runnable", "dev"],
35
+ tags: ["backend", "node", "runnable", "express"],
38
36
  },
39
37
  ],
40
38
  "build": [
@@ -44,7 +42,7 @@ export const scripts: SCRIPT = {
44
42
  },
45
43
  {
46
44
  script: "tsdown --config-loader unrun",
47
- tags: ["runnable", "node"],
45
+ tags: ["runnable", "node", "tsdown"],
48
46
  },
49
47
  ],
50
48
  "cli": [
@@ -107,6 +105,26 @@ export const scripts: SCRIPT = {
107
105
  tags: ["node"],
108
106
  },
109
107
  ],
108
+ "db:push": [
109
+ {
110
+ script: "drizzle-kit push",
111
+ tags: ["drizzle", "db"],
112
+ },
113
+ {
114
+ script: "turbo run db:push",
115
+ tags: ["db", "root"],
116
+ },
117
+ ],
118
+ "db:studio": [
119
+ {
120
+ script: "drizzle-kit studio",
121
+ tags: ["drizzle", "db"],
122
+ },
123
+ {
124
+ script: "turbo run db:studio",
125
+ tags: ["db", "root"],
126
+ },
127
+ ],
110
128
  "typecheck": [
111
129
  {
112
130
  script: "turbo run typecheck",