startx 0.4.0 → 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.
- package/apps/cli/package.json +43 -0
- package/apps/cli/src/commands/ping.ts +10 -0
- package/apps/cli/src/index.ts +12 -0
- package/apps/cli/src/types.ts +0 -0
- package/apps/cli/src/utils/inquirer.ts +160 -0
- package/apps/core-server/package.json +4 -1
- package/apps/startx-cli/eslint.config.ts +4 -0
- package/{packages/cli → apps/startx-cli}/package.json +7 -4
- package/apps/startx-cli/src/commands/init.ts +415 -0
- package/apps/startx-cli/src/configs/deps.ts +45 -0
- package/apps/startx-cli/src/configs/files.ts +51 -0
- package/apps/startx-cli/src/configs/scripts.ts +158 -0
- package/apps/startx-cli/src/constants.ts +4 -0
- package/apps/startx-cli/src/index.ts +21 -0
- package/apps/startx-cli/src/types.ts +64 -0
- package/apps/startx-cli/src/utils/cli-utils.ts +104 -0
- package/apps/startx-cli/src/utils/config.ts +104 -0
- package/apps/startx-cli/src/utils/file-handler.ts +130 -0
- package/apps/startx-cli/src/utils/inquirer.ts +160 -0
- package/apps/startx-cli/tsconfig.json +12 -0
- package/apps/startx-cli/tsdown.config.ts +18 -0
- package/configs/eslint-config/package.json +1 -1
- package/configs/tsdown-config/package.json +1 -1
- package/configs/vitest-config/package.json +1 -1
- package/package.json +5 -4
- package/packages/@repo/constants/package.json +1 -1
- package/packages/@repo/db/package.json +5 -1
- package/packages/@repo/env/package.json +1 -1
- package/packages/@repo/lib/package.json +1 -1
- package/packages/@repo/logger/package.json +1 -1
- package/packages/@repo/mail/package.json +4 -1
- package/packages/@repo/mail/src/index.ts +0 -1
- package/packages/@repo/redis/package.json +1 -1
- package/packages/ui/package.json +1 -1
- package/startx.json +1 -0
- package/packages/cli/dist/index.mjs +0 -203
- /package/{packages → apps}/cli/eslint.config.ts +0 -0
- /package/{packages → apps}/cli/tsconfig.json +0 -0
- /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,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
|
+
}
|
|
@@ -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
|
-
"
|
|
10
|
+
"cli": "cross-env STARTX_ENV=development tsx src/index.ts"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [],
|
|
13
13
|
"author": "",
|
|
@@ -29,10 +29,13 @@
|
|
|
29
29
|
"type-fest": "^5.4.4"
|
|
30
30
|
},
|
|
31
31
|
"startx": {
|
|
32
|
-
"mode": "
|
|
33
|
-
"
|
|
32
|
+
"mode": "silent",
|
|
33
|
+
"iTags": [
|
|
34
34
|
"node"
|
|
35
35
|
],
|
|
36
|
+
"tags": [
|
|
37
|
+
"cli"
|
|
38
|
+
],
|
|
36
39
|
"requiredDeps": [
|
|
37
40
|
"@repo/env",
|
|
38
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
|
+
}
|