startx 0.3.1 → 0.6.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 (40) hide show
  1. package/apps/cli/package.json +43 -0
  2. package/apps/cli/src/commands/ping.ts +10 -0
  3. package/apps/cli/src/index.ts +12 -0
  4. package/apps/cli/src/types.ts +0 -0
  5. package/apps/cli/src/utils/inquirer.ts +160 -0
  6. package/apps/core-server/package.json +4 -1
  7. package/apps/startx-cli/eslint.config.ts +4 -0
  8. package/{packages/cli → apps/startx-cli}/package.json +7 -3
  9. package/apps/startx-cli/src/commands/init.ts +415 -0
  10. package/apps/startx-cli/src/configs/deps.ts +45 -0
  11. package/apps/startx-cli/src/configs/files.ts +51 -0
  12. package/apps/startx-cli/src/configs/scripts.ts +158 -0
  13. package/apps/startx-cli/src/constants.ts +4 -0
  14. package/apps/startx-cli/src/index.ts +21 -0
  15. package/apps/startx-cli/src/types.ts +64 -0
  16. package/apps/startx-cli/src/utils/cli-utils.ts +104 -0
  17. package/apps/startx-cli/src/utils/config.ts +104 -0
  18. package/apps/startx-cli/src/utils/file-handler.ts +130 -0
  19. package/apps/startx-cli/src/utils/inquirer.ts +160 -0
  20. package/apps/startx-cli/tsconfig.json +12 -0
  21. package/apps/startx-cli/tsdown.config.ts +18 -0
  22. package/configs/eslint-config/package.json +4 -1
  23. package/configs/tsdown-config/package.json +5 -1
  24. package/configs/typescript-config/package.json +4 -0
  25. package/configs/vitest-config/package.json +5 -5
  26. package/package.json +5 -4
  27. package/packages/@repo/constants/package.json +2 -2
  28. package/packages/@repo/db/package.json +6 -1
  29. package/packages/@repo/env/package.json +3 -3
  30. package/packages/@repo/lib/package.json +1 -1
  31. package/packages/@repo/logger/package.json +3 -2
  32. package/packages/@repo/mail/package.json +5 -1
  33. package/packages/@repo/mail/src/index.ts +0 -1
  34. package/packages/@repo/redis/package.json +3 -2
  35. package/packages/ui/package.json +3 -5
  36. package/startx.json +1 -1
  37. package/packages/cli/dist/index.mjs +0 -203
  38. /package/{packages → apps}/cli/eslint.config.ts +0 -0
  39. /package/{packages → apps}/cli/tsconfig.json +0 -0
  40. /package/{packages → apps}/cli/tsdown.config.ts +0 -0
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "cli",
3
+ "version": "1.0.0",
4
+ "description": "Cli powered by Commander.js",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1",
9
+ "build": "tsdown --config-loader unrun",
10
+ "cli": "tsx src/index.ts"
11
+ },
12
+ "keywords": [],
13
+ "author": "",
14
+ "license": "ISC",
15
+ "devDependencies": {
16
+ "eslint-config": "workspace:*",
17
+ "tsdown-config": "workspace:*",
18
+ "typescript-config": "workspace:*",
19
+ "vitest-config": "workspace:*"
20
+ },
21
+ "dependencies": {
22
+ "@inquirer/prompts": "^8.3.0",
23
+ "@repo/logger": "workspace:*",
24
+ "chokidar": "^4.0.3",
25
+ "commander": "^14.0.0",
26
+ "type-fest": "^5.4.4"
27
+ },
28
+ "startx": {
29
+ "gTags": [
30
+ "node",
31
+ "cli"
32
+ ],
33
+ "tags": [
34
+ "commander"
35
+ ],
36
+ "requiredDeps": [
37
+ "@repo/logger"
38
+ ],
39
+ "requiredDevDeps": [
40
+ "typescript-config"
41
+ ]
42
+ }
43
+ }
@@ -0,0 +1,10 @@
1
+ import { logger } from "@repo/logger";
2
+ import { Command } from "commander";
3
+
4
+ export class InitCommand {
5
+ static command = new Command("ping").action(InitCommand.run.bind(InitCommand));
6
+
7
+ private static run() {
8
+ logger.info("pong");
9
+ }
10
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from "commander";
2
+
3
+ import { InitCommand } from "./commands/ping";
4
+ import { version, name, description } from "../../../package.json";
5
+
6
+ const program = new Command();
7
+
8
+ program.name(name).description(description).version(version);
9
+
10
+ program.addCommand(InitCommand.command);
11
+
12
+ program.parse(process.argv);
File without changes
@@ -0,0 +1,160 @@
1
+ import { input, select, checkbox } from "@inquirer/prompts";
2
+ import { type z, type ZodTypeAny, ZodEnum, ZodNumber } from "zod";
3
+
4
+ type PromptProps<T extends ZodTypeAny | undefined> = {
5
+ name: string;
6
+ message: string;
7
+ schema?: T;
8
+ default?: T extends ZodTypeAny ? z.infer<T> : string;
9
+ };
10
+
11
+ type MultiSelectProps<T extends "single" | "multiple"> = {
12
+ message: string;
13
+ options: string[];
14
+ mode?: T;
15
+ default?: T extends "single" ? string : string[];
16
+ includeAllOption?: boolean;
17
+ required?: boolean;
18
+ };
19
+
20
+ export class CommonInquirer {
21
+ static async getText<T extends ZodTypeAny | undefined>(
22
+ props: PromptProps<T>
23
+ ): Promise<T extends ZodTypeAny ? z.infer<T> : string> {
24
+ const { message, schema, default: defaultValue } = props;
25
+
26
+ let validDefault: T extends ZodTypeAny ? z.infer<T> : string;
27
+
28
+ if (schema && defaultValue !== undefined) {
29
+ const result = schema.safeParse(defaultValue);
30
+ if (result.success) {
31
+ validDefault = result.data as T extends ZodTypeAny ? z.infer<T> : string;
32
+ }
33
+ } else if (!schema && defaultValue !== undefined) {
34
+ validDefault = defaultValue;
35
+ }
36
+
37
+ if (schema && schema instanceof ZodEnum) {
38
+ const formattedChoices = schema.options.map(opt => ({
39
+ value: opt,
40
+ }));
41
+
42
+ const answer = await select({
43
+ message,
44
+ choices: formattedChoices,
45
+ default: validDefault! as string,
46
+ });
47
+
48
+ return answer as T extends ZodTypeAny ? z.infer<T> : string;
49
+ }
50
+
51
+ const answer = await input({
52
+ message,
53
+ default: validDefault! as string,
54
+ validate: (value: string) => {
55
+ if (!schema) return true;
56
+
57
+ let valToValidate: string | number = value;
58
+
59
+ if (schema instanceof ZodNumber) {
60
+ if (value.trim() === "") return "Input cannot be empty";
61
+
62
+ const parsed = Number(value);
63
+ if (Number.isNaN(parsed)) {
64
+ return "Please enter a valid number";
65
+ }
66
+
67
+ valToValidate = parsed;
68
+ }
69
+
70
+ const result = schema.safeParse(valToValidate);
71
+
72
+ if (!result.success) {
73
+ return result.error.issues[0]?.message ?? "Invalid input";
74
+ }
75
+
76
+ return true;
77
+ },
78
+ });
79
+
80
+ if (!schema) {
81
+ return answer as T extends ZodTypeAny ? z.infer<T> : string;
82
+ }
83
+
84
+ const finalValue = schema instanceof ZodNumber ? Number(answer) : answer;
85
+
86
+ return schema.parse(finalValue) as T extends ZodTypeAny ? z.infer<T> : string;
87
+ }
88
+
89
+ static async choose(props: MultiSelectProps<"single">): Promise<string>;
90
+ static async choose(props: MultiSelectProps<"multiple">): Promise<string[]>;
91
+ static async choose<T extends "single" | "multiple">(props: MultiSelectProps<T>) {
92
+ const {
93
+ message,
94
+ options,
95
+ mode = "single",
96
+ default: defaultValue,
97
+ includeAllOption = false,
98
+ required = false,
99
+ } = props;
100
+
101
+ const ALL = "__all__";
102
+
103
+ // const defaults = Array.isArray(defaultValue)
104
+ // ? defaultValue
105
+ // : defaultValue
106
+ // ? [defaultValue]
107
+ // : [] as string[];
108
+
109
+ const defaultValues = (
110
+ mode === "single" ? [defaultValue] : [...(defaultValue || [])]
111
+ ) as string[];
112
+
113
+ const choices = [
114
+ ...(mode === "multiple" && includeAllOption
115
+ ? [
116
+ {
117
+ name: "All",
118
+ value: ALL,
119
+ checked: defaultValues.includes(ALL),
120
+ },
121
+ ]
122
+ : []),
123
+ ...options.map(opt => ({
124
+ name: opt,
125
+ value: opt,
126
+ checked: defaultValues.includes(opt),
127
+ })),
128
+ ];
129
+
130
+ if (mode === "multiple") {
131
+ const answer = await checkbox({
132
+ message,
133
+ choices,
134
+ validate: (input: string[]) => {
135
+ if (required && input.length === 0) {
136
+ return "You must select at least one option.";
137
+ }
138
+ return true;
139
+ },
140
+ });
141
+
142
+ if (includeAllOption && answer.includes(ALL)) {
143
+ return options;
144
+ }
145
+
146
+ return answer;
147
+ }
148
+
149
+ const answer = await select({
150
+ message,
151
+ choices: options.map(opt => ({
152
+ name: opt,
153
+ value: opt,
154
+ })),
155
+ default: typeof defaultValue === "string" ? defaultValue : undefined,
156
+ });
157
+
158
+ return answer;
159
+ }
160
+ }
@@ -46,10 +46,13 @@
46
46
  "typescript-config": "workspace:*"
47
47
  },
48
48
  "startx": {
49
- "tags": [
49
+ "gTags": [
50
50
  "node",
51
51
  "backend"
52
52
  ],
53
+ "tags": [
54
+ "dev"
55
+ ],
53
56
  "requiredDeps": [
54
57
  "@repo/env",
55
58
  "@repo/logger"
@@ -0,0 +1,4 @@
1
+ import { baseConfig } from "eslint-config/base";
2
+ import { extend } from "eslint-config/extend";
3
+
4
+ export default extend(baseConfig, {});
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "cli",
2
+ "name": "startx-cli",
3
3
  "version": "1.0.0",
4
4
  "description": "Cli powered by Commander.js",
5
5
  "type": "module",
@@ -7,7 +7,7 @@
7
7
  "scripts": {
8
8
  "test": "echo \"Error: no test specified\" && exit 1",
9
9
  "build": "tsdown --config-loader unrun",
10
- "dev": "cross-env STARTX_ENV=development tsx src/index.ts"
10
+ "cli": "cross-env STARTX_ENV=development tsx src/index.ts"
11
11
  },
12
12
  "keywords": [],
13
13
  "author": "",
@@ -29,9 +29,13 @@
29
29
  "type-fest": "^5.4.4"
30
30
  },
31
31
  "startx": {
32
- "tags": [
32
+ "mode": "silent",
33
+ "iTags": [
33
34
  "node"
34
35
  ],
36
+ "tags": [
37
+ "cli"
38
+ ],
35
39
  "requiredDeps": [
36
40
  "@repo/env",
37
41
  "@repo/lib",
@@ -0,0 +1,415 @@
1
+ import { fsTool } from "@repo/lib/file-system-module";
2
+ import { logger } from "@repo/logger";
3
+ import { Command } from "commander";
4
+ import path from "path";
5
+ import z from "zod";
6
+
7
+ import { FileCheck } from "../configs/files";
8
+ import type { StartXPackageJson, TAGS } from "../types";
9
+ import { CliUtils } from "../utils/cli-utils";
10
+ import { FileHandler } from "../utils/file-handler";
11
+ import { CommonInquirer } from "../utils/inquirer";
12
+
13
+ type InitOptions = {
14
+ dir?: string;
15
+ };
16
+
17
+ type PackageWithJson = {
18
+ packageJson: StartXPackageJson | null;
19
+ type: "apps" | "configs" | "packages";
20
+ path: string;
21
+ name: string;
22
+ };
23
+
24
+ export class InitCommand {
25
+ static command = new Command("init")
26
+ .argument("[projectName]")
27
+ .option("-d, --dir <path>", "workspace directory")
28
+ .action(InitCommand.run.bind(InitCommand));
29
+
30
+ 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
+ );
56
+
57
+ const config = await this.getConfigPrefs({
58
+ selectedApps: prefs.selectedApps,
59
+ packages,
60
+ });
61
+
62
+ const packagePrefs = await this.getPackagesPrefs({
63
+ selectedApps: prefs.selectedApps,
64
+ selectedPackages: config.selectedConfigs,
65
+ packages,
66
+ tags: config.tags,
67
+ });
68
+ await this.installWorkspace({
69
+ name: prefs.projectName,
70
+ tags: [...packagePrefs.tags, "runnable"],
71
+ dir: {
72
+ workspace: prefs.directory.workspace,
73
+ template: prefs.directory.template,
74
+ },
75
+ });
76
+
77
+ 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);
81
+
82
+ if (pkg.packageJson?.startx?.mode === "standalone") tags.add("runnable");
83
+
84
+ if (pkg.type === "apps") {
85
+ tags.add("runnable");
86
+
87
+ 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:^"));
96
+ }
97
+
98
+ await this.installPackage({
99
+ packages: pkg,
100
+ directory: {
101
+ workspace: prefs.directory.workspace,
102
+ template: prefs.directory.template,
103
+ },
104
+ tags: Array.from(tags),
105
+ dependencies: Object.fromEntries(appDeps),
106
+ });
107
+ })
108
+ );
109
+ }
110
+
111
+ private static async getPrefs(props: {
112
+ projectName?: string;
113
+ directory?: string;
114
+ options: InitOptions;
115
+ projects: PackageWithJson[];
116
+ }) {
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
+
138
+ if (props.projects.length === 0) {
139
+ throw new Error("No apps found");
140
+ }
141
+
142
+ const selectedAppNames = await CommonInquirer.choose({
143
+ message: "Select apps to install",
144
+ options: props.projects.map(e => e.name),
145
+ includeAllOption: true,
146
+ mode: "multiple",
147
+ required: true,
148
+ });
149
+
150
+ const selectedApps = props.projects.filter(e => selectedAppNames.includes(e.name));
151
+
152
+ return {
153
+ projectName: props.projectName,
154
+ directory: {
155
+ workspace,
156
+ template: directory.template,
157
+ },
158
+ selectedApps,
159
+ };
160
+ }
161
+
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,
206
+ });
207
+
208
+ availableConfigs
209
+ .filter(e => rawSelectedConfigs.includes(e.name))
210
+ .forEach(e => configs.set(e.name, e));
211
+
212
+ if (tags.has("node")) {
213
+ const formatter: string | string[] = await CommonInquirer.choose({
214
+ message: "Select formatter",
215
+ options: ["prettier + biome", "prettier"],
216
+ mode: "single",
217
+ default: "prettier",
218
+ required: true,
219
+ });
220
+ if (formatter === "prettier") {
221
+ tags.add("prettier");
222
+ } else {
223
+ tags.add("biome");
224
+ tags.add("prettier");
225
+ }
226
+ }
227
+
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
+ });
241
+
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
+ });
247
+
248
+ return {
249
+ tags: Array.from(tags),
250
+ selectedConfigs: Array.from(configs.values()),
251
+ };
252
+ }
253
+
254
+ private static async getPackagesPrefs(props: {
255
+ tags: TAGS[];
256
+ packages: PackageWithJson[];
257
+ selectedApps: PackageWithJson[];
258
+ selectedPackages: PackageWithJson[];
259
+ }) {
260
+ const appTags = new Set<TAGS>(props.tags);
261
+ const packages = new Map<string, PackageWithJson>(props.selectedPackages.map(e => [e.name, e]));
262
+ const availablePackages = props.packages.filter(pkg => {
263
+ if (pkg.type !== "packages") return false;
264
+ if (pkg.packageJson?.startx?.mode === "silent") {
265
+ return false;
266
+ }
267
+ 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,
282
+ });
283
+
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 || [];
291
+
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
+ });
300
+
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
+ });
306
+
307
+ return {
308
+ tags: Array.from(appTags),
309
+ selectedPackages: Array.from(packages.values()),
310
+ };
311
+ }
312
+
313
+ private static async installPackage(props: {
314
+ packages: PackageWithJson;
315
+ directory: {
316
+ workspace: string;
317
+ template: string;
318
+ };
319
+ tags: TAGS[];
320
+ dependencies: Record<string, string>;
321
+ }) {
322
+ const tags = new Set(props.tags.concat(props.packages.packageJson?.startx?.tags || []));
323
+ const { packageJson, isWorkspace } = FileHandler.handlePackageJson({
324
+ app: props.packages.packageJson!,
325
+ tags: Array.from(tags),
326
+ name: props.packages.name,
327
+ });
328
+
329
+ if (isWorkspace) throw new Error("Can't install workspace as package.");
330
+
331
+ let iDirectory = path.join(props.directory.workspace, props.packages.type);
332
+ let iTemplate = path.join(props.directory.template, props.packages.type);
333
+ if (props.packages.packageJson?.name?.startsWith("@repo")) {
334
+ const repoName = props.packages.packageJson.name.split("/")[1];
335
+ iDirectory = path.join(iDirectory, "@repo", repoName);
336
+ iTemplate = path.join(iTemplate, "@repo", repoName);
337
+ } else {
338
+ iDirectory = path.join(iDirectory, props.packages.name);
339
+ iTemplate = path.join(iTemplate, props.packages.name);
340
+ }
341
+
342
+ await fsTool.writeJSONFile({
343
+ dir: iDirectory,
344
+ file: "package",
345
+ content: packageJson,
346
+ });
347
+
348
+ const files = await fsTool.listFiles({ dir: iTemplate });
349
+ for (const file of files) {
350
+ const checked = FileCheck[file];
351
+ if (checked && !checked.tags.every(e => props.tags.includes(e))) continue;
352
+ try {
353
+ await fsTool.copyFile({
354
+ from: path.join(iTemplate, file),
355
+ to: path.join(iDirectory, file),
356
+ });
357
+ } catch (error) {
358
+ logger.error(`Failed to copy file ${file}:`, error);
359
+ }
360
+ }
361
+
362
+ // Installing src
363
+ await fsTool.copyDirectory({
364
+ from: path.join(iTemplate, "src"),
365
+ to: path.join(iDirectory, "src"),
366
+ exclude: !props.tags.includes("vitest") ? /\.test\.tsx?$/ : undefined,
367
+ });
368
+ logger.info(`Successfully installed ${props.packages.name}`);
369
+ }
370
+
371
+ private static async installWorkspace(props: {
372
+ name: string;
373
+ tags: TAGS[];
374
+ dir: {
375
+ workspace: string;
376
+ template: string;
377
+ };
378
+ }) {
379
+ const rootTags = ["root", ...props.tags] as TAGS[];
380
+ const rawPackage = await CliUtils.parsePackageJson({ dir: props.dir.template });
381
+ const startXRawPackage = await CliUtils.parsePackageJson({
382
+ dir: props.dir.template,
383
+ file: "startx",
384
+ });
385
+
386
+ if (!rawPackage) throw new Error("Failed to parse package.json");
387
+ rawPackage.dependencies = startXRawPackage?.dependencies || {};
388
+ rawPackage.devDependencies = startXRawPackage?.devDependencies || {};
389
+ const { packageJson } = FileHandler.handlePackageJson({
390
+ app: rawPackage,
391
+ tags: rootTags,
392
+ name: props.name,
393
+ });
394
+
395
+ await fsTool.writeJSONFile({
396
+ dir: props.dir.workspace,
397
+ file: "package",
398
+ content: packageJson,
399
+ });
400
+
401
+ const files = await fsTool.listFiles({ dir: props.dir.template });
402
+ for (const file of files) {
403
+ const checked = FileCheck[file];
404
+ if (checked && !checked.tags.every(e => rootTags.includes(e))) continue;
405
+ try {
406
+ await fsTool.copyFile({
407
+ from: path.join(props.dir.template, file),
408
+ to: path.join(props.dir.workspace, file),
409
+ });
410
+ } catch (error) {
411
+ logger.error(`Failed to copy file ${file}:`, error);
412
+ }
413
+ }
414
+ }
415
+ }