fe-kit-cli 0.0.1
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/README.md +89 -0
- package/dist/cli.mjs +1738 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/rules/common/typescript.mdc +21 -0
- package/dist/rules/react/component-conventions.mdc +24 -0
- package/dist/rules/react/hooks.mdc +20 -0
- package/dist/rules/react/react-router.mdc +18 -0
- package/dist/rules/react/state-management.mdc +21 -0
- package/dist/rules/vue/component-conventions.mdc +23 -0
- package/dist/rules/vue/composition-api.mdc +24 -0
- package/dist/rules/vue/state-management.mdc +16 -0
- package/dist/rules/vue/vue-router.mdc +18 -0
- package/dist/skills/app-ui-design/SKILL.md +62 -0
- package/dist/skills/app-ui-design/references/rules.md +127 -0
- package/dist/skills/e2e-testing/SKILL.md +327 -0
- package/dist/skills/eval-harness/SKILL.md +271 -0
- package/dist/skills/frontend-design/SKILL.md +43 -0
- package/dist/skills/frontend-patterns/SKILL.md +643 -0
- package/dist/skills/security-review/SKILL.md +496 -0
- package/dist/skills/tailwindcss-advanced-layouts/SKILL.md +595 -0
- package/dist/skills/tdd-workflow/SKILL.md +464 -0
- package/dist/skills/verification-loop/SKILL.md +127 -0
- package/dist/skills/wechat-ui-design/SKILL.md +64 -0
- package/dist/skills/wechat-ui-design/references/rules.md +121 -0
- package/dist/templates/react-rspack-ts/index.html +11 -0
- package/dist/templates/react-rspack-ts/package.json +20 -0
- package/dist/templates/react-rspack-ts/rspack.config.ts +23 -0
- package/dist/templates/react-rspack-ts/src/App.tsx +7 -0
- package/dist/templates/react-rspack-ts/src/main.tsx +9 -0
- package/dist/templates/react-rspack-ts/tsconfig.json +17 -0
- package/dist/templates/react-vite-ts/index.html +12 -0
- package/dist/templates/react-vite-ts/package.json +22 -0
- package/dist/templates/react-vite-ts/src/App.tsx +7 -0
- package/dist/templates/react-vite-ts/src/main.tsx +9 -0
- package/dist/templates/react-vite-ts/tsconfig.json +19 -0
- package/dist/templates/react-vite-ts/vite.config.ts +9 -0
- package/dist/templates/react-webpack-ts/index.html +11 -0
- package/dist/templates/react-webpack-ts/package.json +25 -0
- package/dist/templates/react-webpack-ts/src/App.tsx +7 -0
- package/dist/templates/react-webpack-ts/src/main.tsx +9 -0
- package/dist/templates/react-webpack-ts/tsconfig.json +17 -0
- package/dist/templates/react-webpack-ts/webpack.config.ts +29 -0
- package/dist/templates/vue-rspack-ts/index.html +11 -0
- package/dist/templates/vue-rspack-ts/package.json +18 -0
- package/dist/templates/vue-rspack-ts/rspack.config.ts +16 -0
- package/dist/templates/vue-rspack-ts/src/App.vue +7 -0
- package/dist/templates/vue-rspack-ts/src/main.ts +4 -0
- package/dist/templates/vue-rspack-ts/tsconfig.json +17 -0
- package/dist/templates/vue-vite-ts/index.html +12 -0
- package/dist/templates/vue-vite-ts/package.json +19 -0
- package/dist/templates/vue-vite-ts/src/App.vue +7 -0
- package/dist/templates/vue-vite-ts/src/main.ts +4 -0
- package/dist/templates/vue-vite-ts/tsconfig.json +19 -0
- package/dist/templates/vue-vite-ts/vite.config.ts +9 -0
- package/dist/templates/vue-webpack-ts/index.html +11 -0
- package/dist/templates/vue-webpack-ts/package.json +24 -0
- package/dist/templates/vue-webpack-ts/src/App.vue +7 -0
- package/dist/templates/vue-webpack-ts/src/main.ts +4 -0
- package/dist/templates/vue-webpack-ts/tsconfig.json +17 -0
- package/dist/templates/vue-webpack-ts/webpack.config.ts +32 -0
- package/package.json +63 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,1738 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/constants/meta.ts
|
|
7
|
+
var CLI_NAME = "fe-kit";
|
|
8
|
+
var META_DIR = ".fe-kit";
|
|
9
|
+
var CLI_VERSION = "0.1.0";
|
|
10
|
+
|
|
11
|
+
// src/constants/frameworks.ts
|
|
12
|
+
var FRAMEWORKS = ["vue", "react"];
|
|
13
|
+
var ROUTERS = {
|
|
14
|
+
vue: "vue-router",
|
|
15
|
+
react: "react-router"
|
|
16
|
+
};
|
|
17
|
+
var STATE_MANAGERS = {
|
|
18
|
+
vue: ["pinia", "vuex"],
|
|
19
|
+
react: ["redux-toolkit", "zustand", "mobx"]
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/constants/bundlers.ts
|
|
23
|
+
var BUNDLERS = ["vite", "webpack", "rspack"];
|
|
24
|
+
|
|
25
|
+
// src/constants/tools.ts
|
|
26
|
+
var DEV_TOOLS = [
|
|
27
|
+
"cursor",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"vscode",
|
|
30
|
+
"codebuddy-cn",
|
|
31
|
+
"trae",
|
|
32
|
+
"idea"
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// src/constants/lint.ts
|
|
36
|
+
var LINT_TOOLS = [
|
|
37
|
+
"eslint",
|
|
38
|
+
"stylelint",
|
|
39
|
+
"prettier",
|
|
40
|
+
"editorconfig"
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// src/prompts/init-prompts.ts
|
|
44
|
+
import path2 from "path";
|
|
45
|
+
import prompts from "prompts";
|
|
46
|
+
|
|
47
|
+
// src/skills/catalog.ts
|
|
48
|
+
import fs from "fs-extra";
|
|
49
|
+
import path from "path";
|
|
50
|
+
import { fileURLToPath } from "url";
|
|
51
|
+
var { pathExistsSync, readdirSync, readFileSync } = fs;
|
|
52
|
+
function toLabel(id) {
|
|
53
|
+
return id.split(/[-_]/g).filter(Boolean).map((w) => w.slice(0, 1).toUpperCase() + w.slice(1)).join(" ");
|
|
54
|
+
}
|
|
55
|
+
function splitFrontmatter(raw) {
|
|
56
|
+
if (!raw.startsWith("---\n")) return { meta: {}, body: raw };
|
|
57
|
+
const end = raw.indexOf("\n---\n", 4);
|
|
58
|
+
if (end === -1) return { meta: {}, body: raw };
|
|
59
|
+
const fm = raw.slice(4, end).trim();
|
|
60
|
+
const body = raw.slice(end + "\n---\n".length).replace(/^\n+/, "");
|
|
61
|
+
const meta = {};
|
|
62
|
+
for (const line of fm.split("\n")) {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
65
|
+
const idx = trimmed.indexOf(":");
|
|
66
|
+
if (idx <= 0) continue;
|
|
67
|
+
const key = trimmed.slice(0, idx).trim();
|
|
68
|
+
let value = trimmed.slice(idx + 1).trim();
|
|
69
|
+
value = value.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
|
|
70
|
+
meta[key] = value;
|
|
71
|
+
}
|
|
72
|
+
return { meta, body };
|
|
73
|
+
}
|
|
74
|
+
function skillsDir() {
|
|
75
|
+
return fileURLToPath(new URL("./skills/", import.meta.url));
|
|
76
|
+
}
|
|
77
|
+
function loadSkillsFromDir() {
|
|
78
|
+
const dir = skillsDir();
|
|
79
|
+
if (!pathExistsSync(dir)) return [];
|
|
80
|
+
const entries = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort((a, b) => a.localeCompare(b));
|
|
81
|
+
const defs = [];
|
|
82
|
+
for (const dirName of entries) {
|
|
83
|
+
const p = path.join(dir, dirName, "SKILL.md");
|
|
84
|
+
if (!pathExistsSync(p)) continue;
|
|
85
|
+
const raw = readFileSync(p, "utf8");
|
|
86
|
+
const { meta, body } = splitFrontmatter(raw);
|
|
87
|
+
const id = meta.name?.trim() || dirName;
|
|
88
|
+
const description = meta.description?.trim() || "";
|
|
89
|
+
defs.push({
|
|
90
|
+
id,
|
|
91
|
+
label: toLabel(id),
|
|
92
|
+
description,
|
|
93
|
+
content: body.trimEnd(),
|
|
94
|
+
tags: [],
|
|
95
|
+
sourcePath: p
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return defs;
|
|
99
|
+
}
|
|
100
|
+
function getSkillCatalog() {
|
|
101
|
+
return loadSkillsFromDir();
|
|
102
|
+
}
|
|
103
|
+
function getSkillById(id) {
|
|
104
|
+
const def = getSkillCatalog().find((s) => s.id === id);
|
|
105
|
+
if (!def) return void 0;
|
|
106
|
+
return { id: def.id, label: def.label, content: def.content, sourcePath: def.sourcePath };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/mcp/catalog.ts
|
|
110
|
+
var BUILTIN_MCP = [
|
|
111
|
+
{
|
|
112
|
+
id: "context7",
|
|
113
|
+
label: "Context7",
|
|
114
|
+
description: "Fetch up-to-date library documentation via Context7.",
|
|
115
|
+
config: {
|
|
116
|
+
command: "npx",
|
|
117
|
+
args: ["-y", "@upstash/context7-mcp@latest"]
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "sequential-thinking",
|
|
122
|
+
label: "Sequential Thinking",
|
|
123
|
+
description: "Step-by-step reasoning MCP server for complex problem solving.",
|
|
124
|
+
config: {
|
|
125
|
+
command: "npx",
|
|
126
|
+
args: ["-y", "@anthropic/sequential-thinking-mcp@latest"]
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "filesystem",
|
|
131
|
+
label: "Filesystem",
|
|
132
|
+
description: "Read/write project files via MCP.",
|
|
133
|
+
config: {
|
|
134
|
+
command: "npx",
|
|
135
|
+
args: ["-y", "@anthropic/filesystem-mcp@latest"]
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "exa-search",
|
|
140
|
+
label: "Exa Search",
|
|
141
|
+
description: "Neural web search for real-time information.",
|
|
142
|
+
config: {
|
|
143
|
+
command: "npx",
|
|
144
|
+
args: ["-y", "exa-mcp-server@latest"],
|
|
145
|
+
env: {
|
|
146
|
+
EXA_API_KEY: "<your-exa-api-key>"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
function getMcpCatalog() {
|
|
152
|
+
return BUILTIN_MCP;
|
|
153
|
+
}
|
|
154
|
+
function getMcpById(id) {
|
|
155
|
+
const def = BUILTIN_MCP.find((m) => m.id === id);
|
|
156
|
+
if (!def) return void 0;
|
|
157
|
+
return { id: def.id, label: def.label, config: def.config };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/prompts/init-prompts.ts
|
|
161
|
+
async function runInitPrompts() {
|
|
162
|
+
const response = await prompts(
|
|
163
|
+
[
|
|
164
|
+
{
|
|
165
|
+
type: "text",
|
|
166
|
+
name: "projectName",
|
|
167
|
+
message: "Project name:",
|
|
168
|
+
validate: (v) => v.trim() ? true : "Project name is required"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: "text",
|
|
172
|
+
name: "projectPath",
|
|
173
|
+
message: "Project path:",
|
|
174
|
+
initial: (prev) => `./${prev}`,
|
|
175
|
+
format: (v) => path2.resolve(v)
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
type: "select",
|
|
179
|
+
name: "framework",
|
|
180
|
+
message: "Frontend framework:",
|
|
181
|
+
choices: FRAMEWORKS.map((f) => ({ title: f.charAt(0).toUpperCase() + f.slice(1), value: f }))
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: "select",
|
|
185
|
+
name: "router",
|
|
186
|
+
message: "Router:",
|
|
187
|
+
choices: (_prev, answers) => {
|
|
188
|
+
const fw = answers.framework;
|
|
189
|
+
return [{ title: ROUTERS[fw], value: ROUTERS[fw] }];
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
type: "select",
|
|
194
|
+
name: "stateManagement",
|
|
195
|
+
message: "State management:",
|
|
196
|
+
choices: (_prev, answers) => {
|
|
197
|
+
const fw = answers.framework;
|
|
198
|
+
return STATE_MANAGERS[fw].map((s) => ({ title: s, value: s }));
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
type: "select",
|
|
203
|
+
name: "bundler",
|
|
204
|
+
message: "Build tool:",
|
|
205
|
+
choices: BUNDLERS.map((b) => ({ title: b.charAt(0).toUpperCase() + b.slice(1), value: b }))
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
type: "multiselect",
|
|
209
|
+
name: "lintTools",
|
|
210
|
+
message: "Code quality tools (space to toggle):",
|
|
211
|
+
choices: LINT_TOOLS.map((t) => ({
|
|
212
|
+
title: t,
|
|
213
|
+
value: t,
|
|
214
|
+
selected: true
|
|
215
|
+
})),
|
|
216
|
+
hint: "ESLint + Prettier recommended"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: "multiselect",
|
|
220
|
+
name: "devTools",
|
|
221
|
+
message: "Dev tools to configure:",
|
|
222
|
+
choices: DEV_TOOLS.map((t) => ({ title: t, value: t, selected: t === "cursor" })),
|
|
223
|
+
min: 1,
|
|
224
|
+
hint: "Select at least one"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
type: "multiselect",
|
|
228
|
+
name: "skills",
|
|
229
|
+
message: "Built-in Skills to enable:",
|
|
230
|
+
choices: getSkillCatalog().map((s) => ({
|
|
231
|
+
title: `${s.label} \u2014 ${s.description}`,
|
|
232
|
+
value: s.id,
|
|
233
|
+
selected: false
|
|
234
|
+
}))
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
type: "multiselect",
|
|
238
|
+
name: "mcpServers",
|
|
239
|
+
message: "MCP servers to enable:",
|
|
240
|
+
choices: getMcpCatalog().map((m) => ({
|
|
241
|
+
title: `${m.label} \u2014 ${m.description}`,
|
|
242
|
+
value: m.id,
|
|
243
|
+
selected: false
|
|
244
|
+
}))
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
{ onCancel: () => process.exit(0) }
|
|
248
|
+
);
|
|
249
|
+
if (!response.projectName) return null;
|
|
250
|
+
return response;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/generators/project-generator.ts
|
|
254
|
+
import path3 from "path";
|
|
255
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
256
|
+
import fs2 from "fs-extra";
|
|
257
|
+
|
|
258
|
+
// src/utils/logger.ts
|
|
259
|
+
import pc from "picocolors";
|
|
260
|
+
var logger = {
|
|
261
|
+
info: (msg) => console.log(pc.cyan("\u2139"), msg),
|
|
262
|
+
success: (msg) => console.log(pc.green("\u2714"), msg),
|
|
263
|
+
warn: (msg) => console.log(pc.yellow("\u26A0"), msg),
|
|
264
|
+
error: (msg) => console.error(pc.red("\u2716"), msg),
|
|
265
|
+
step: (msg) => console.log(pc.blue("\u2192"), msg)
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// src/generators/project-generator.ts
|
|
269
|
+
var __dirname = path3.dirname(fileURLToPath2(import.meta.url));
|
|
270
|
+
function templateDir(framework, bundler) {
|
|
271
|
+
return path3.resolve(__dirname, "..", "templates", `${framework}-${bundler}-ts`);
|
|
272
|
+
}
|
|
273
|
+
async function generateProject(answers) {
|
|
274
|
+
const { projectPath, projectName, framework, bundler, router, stateManagement } = answers;
|
|
275
|
+
await fs2.ensureDir(projectPath);
|
|
276
|
+
const srcTemplate = templateDir(framework, bundler);
|
|
277
|
+
if (await fs2.pathExists(srcTemplate)) {
|
|
278
|
+
await fs2.copy(srcTemplate, projectPath, { overwrite: false });
|
|
279
|
+
logger.success(`Template ${framework}-${bundler}-ts copied`);
|
|
280
|
+
} else {
|
|
281
|
+
logger.warn(`No full template for ${framework}-${bundler}-ts. Generating minimal project.`);
|
|
282
|
+
await generateMinimalProject(answers);
|
|
283
|
+
}
|
|
284
|
+
await patchPackageJson(projectPath, projectName, framework, bundler, router, stateManagement);
|
|
285
|
+
logger.success("package.json configured");
|
|
286
|
+
await ensureNextStyleScaffold(projectPath, framework);
|
|
287
|
+
}
|
|
288
|
+
async function ensureNextStyleScaffold(projectPath, framework) {
|
|
289
|
+
const dirs = [
|
|
290
|
+
path3.join(projectPath, "public"),
|
|
291
|
+
path3.join(projectPath, "src", "app"),
|
|
292
|
+
path3.join(projectPath, "src", "components"),
|
|
293
|
+
path3.join(projectPath, "src", "hooks"),
|
|
294
|
+
path3.join(projectPath, "src", "lib"),
|
|
295
|
+
path3.join(projectPath, "src", "styles")
|
|
296
|
+
];
|
|
297
|
+
await Promise.all(dirs.map((d) => fs2.ensureDir(d)));
|
|
298
|
+
const keepFiles = [
|
|
299
|
+
path3.join(projectPath, "src", "app", ".gitkeep"),
|
|
300
|
+
path3.join(projectPath, "src", "components", ".gitkeep"),
|
|
301
|
+
path3.join(projectPath, "src", "hooks", ".gitkeep"),
|
|
302
|
+
path3.join(projectPath, "src", "lib", ".gitkeep"),
|
|
303
|
+
path3.join(projectPath, "src", "styles", ".gitkeep"),
|
|
304
|
+
path3.join(projectPath, "public", ".gitkeep")
|
|
305
|
+
];
|
|
306
|
+
await Promise.all(keepFiles.map((p) => fs2.ensureFile(p)));
|
|
307
|
+
if (framework === "vue") {
|
|
308
|
+
await fs2.ensureFile(path3.join(projectPath, "src", "hooks", "README.md"));
|
|
309
|
+
await fs2.writeFile(
|
|
310
|
+
path3.join(projectPath, "src", "hooks", "README.md"),
|
|
311
|
+
["# hooks/", "", "Vue \u9879\u76EE\u91CC\u8FD9\u91CC\u66F4\u5E38\u653E `composables/`\uFF08\u53EF\u6309\u56E2\u961F\u4E60\u60EF\u91CD\u547D\u540D\uFF09\u3002", ""].join("\n"),
|
|
312
|
+
"utf-8"
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function generateMinimalProject(answers) {
|
|
317
|
+
const { projectPath, framework, bundler } = answers;
|
|
318
|
+
const srcDir = path3.join(projectPath, "src");
|
|
319
|
+
await fs2.ensureDir(srcDir);
|
|
320
|
+
const mainFile = framework === "react" ? "main.tsx" : "main.ts";
|
|
321
|
+
const mainContent = framework === "react" ? [
|
|
322
|
+
"import React from 'react';",
|
|
323
|
+
"import ReactDOM from 'react-dom/client';",
|
|
324
|
+
"import App from './App';",
|
|
325
|
+
"",
|
|
326
|
+
"ReactDOM.createRoot(document.getElementById('root')!).render(",
|
|
327
|
+
" <React.StrictMode>",
|
|
328
|
+
" <App />",
|
|
329
|
+
" </React.StrictMode>,",
|
|
330
|
+
");"
|
|
331
|
+
].join("\n") : [
|
|
332
|
+
"import { createApp } from 'vue';",
|
|
333
|
+
"import App from './App.vue';",
|
|
334
|
+
"",
|
|
335
|
+
"createApp(App).mount('#app');"
|
|
336
|
+
].join("\n");
|
|
337
|
+
await fs2.writeFile(path3.join(srcDir, mainFile), mainContent, "utf-8");
|
|
338
|
+
if (framework === "react") {
|
|
339
|
+
await fs2.writeFile(
|
|
340
|
+
path3.join(srcDir, "App.tsx"),
|
|
341
|
+
[
|
|
342
|
+
"import React from 'react';",
|
|
343
|
+
"",
|
|
344
|
+
"function App() {",
|
|
345
|
+
" return <div>Hello fe-kit</div>;",
|
|
346
|
+
"}",
|
|
347
|
+
"",
|
|
348
|
+
"export default App;"
|
|
349
|
+
].join("\n"),
|
|
350
|
+
"utf-8"
|
|
351
|
+
);
|
|
352
|
+
} else {
|
|
353
|
+
await fs2.writeFile(
|
|
354
|
+
path3.join(srcDir, "App.vue"),
|
|
355
|
+
[
|
|
356
|
+
'<script setup lang="ts">',
|
|
357
|
+
"const msg = 'Hello fe-kit';",
|
|
358
|
+
"</script>",
|
|
359
|
+
"",
|
|
360
|
+
"<template>",
|
|
361
|
+
" <div>{{ msg }}</div>",
|
|
362
|
+
"</template>"
|
|
363
|
+
].join("\n"),
|
|
364
|
+
"utf-8"
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
await fs2.writeFile(
|
|
368
|
+
path3.join(projectPath, "index.html"),
|
|
369
|
+
[
|
|
370
|
+
"<!DOCTYPE html>",
|
|
371
|
+
'<html lang="en">',
|
|
372
|
+
"<head>",
|
|
373
|
+
' <meta charset="UTF-8" />',
|
|
374
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
|
|
375
|
+
" <title>fe-kit project</title>",
|
|
376
|
+
"</head>",
|
|
377
|
+
"<body>",
|
|
378
|
+
` <div id="${framework === "react" ? "root" : "app"}"></div>`,
|
|
379
|
+
` <script type="module" src="/src/${mainFile}"></script>`,
|
|
380
|
+
"</body>",
|
|
381
|
+
"</html>"
|
|
382
|
+
].join("\n"),
|
|
383
|
+
"utf-8"
|
|
384
|
+
);
|
|
385
|
+
const tsconfig = {
|
|
386
|
+
compilerOptions: {
|
|
387
|
+
target: "ES2020",
|
|
388
|
+
module: "ESNext",
|
|
389
|
+
moduleResolution: "bundler",
|
|
390
|
+
strict: true,
|
|
391
|
+
jsx: framework === "react" ? "react-jsx" : "preserve",
|
|
392
|
+
esModuleInterop: true,
|
|
393
|
+
skipLibCheck: true,
|
|
394
|
+
forceConsistentCasingInFileNames: true,
|
|
395
|
+
resolveJsonModule: true,
|
|
396
|
+
isolatedModules: true,
|
|
397
|
+
baseUrl: ".",
|
|
398
|
+
paths: { "@/*": ["src/*"] }
|
|
399
|
+
},
|
|
400
|
+
include: ["src"]
|
|
401
|
+
};
|
|
402
|
+
await fs2.writeJson(path3.join(projectPath, "tsconfig.json"), tsconfig, { spaces: 2 });
|
|
403
|
+
await writeBundlerConfig(projectPath, framework, bundler);
|
|
404
|
+
}
|
|
405
|
+
async function writeBundlerConfig(projectPath, framework, bundler) {
|
|
406
|
+
if (bundler === "vite") {
|
|
407
|
+
const plugin = framework === "react" ? "import react from '@vitejs/plugin-react';" : "import vue from '@vitejs/plugin-vue';";
|
|
408
|
+
const pluginCall = framework === "react" ? "react()" : "vue()";
|
|
409
|
+
await fs2.writeFile(
|
|
410
|
+
path3.join(projectPath, "vite.config.ts"),
|
|
411
|
+
[
|
|
412
|
+
"import { defineConfig } from 'vite';",
|
|
413
|
+
plugin,
|
|
414
|
+
"",
|
|
415
|
+
"export default defineConfig({",
|
|
416
|
+
` plugins: [${pluginCall}],`,
|
|
417
|
+
" resolve: {",
|
|
418
|
+
" alias: { '@': '/src' },",
|
|
419
|
+
" },",
|
|
420
|
+
"});"
|
|
421
|
+
].join("\n"),
|
|
422
|
+
"utf-8"
|
|
423
|
+
);
|
|
424
|
+
} else if (bundler === "webpack") {
|
|
425
|
+
await fs2.writeFile(
|
|
426
|
+
path3.join(projectPath, "webpack.config.ts"),
|
|
427
|
+
[
|
|
428
|
+
"// Webpack config placeholder \u2014 full template coming in P1",
|
|
429
|
+
"import path from 'path';",
|
|
430
|
+
"",
|
|
431
|
+
"export default {",
|
|
432
|
+
" mode: 'development',",
|
|
433
|
+
" entry: './src/main." + (framework === "react" ? "tsx" : "ts") + "',",
|
|
434
|
+
" resolve: {",
|
|
435
|
+
" extensions: ['.ts', '.tsx', '.js', '.jsx', '.vue'],",
|
|
436
|
+
" },",
|
|
437
|
+
"};"
|
|
438
|
+
].join("\n"),
|
|
439
|
+
"utf-8"
|
|
440
|
+
);
|
|
441
|
+
} else if (bundler === "rspack") {
|
|
442
|
+
await fs2.writeFile(
|
|
443
|
+
path3.join(projectPath, "rspack.config.ts"),
|
|
444
|
+
[
|
|
445
|
+
"// Rspack config placeholder \u2014 full template coming in P1",
|
|
446
|
+
"import { defineConfig } from '@rspack/cli';",
|
|
447
|
+
"",
|
|
448
|
+
"export default defineConfig({",
|
|
449
|
+
" entry: { main: './src/main." + (framework === "react" ? "tsx" : "ts") + "' },",
|
|
450
|
+
"});"
|
|
451
|
+
].join("\n"),
|
|
452
|
+
"utf-8"
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async function patchPackageJson(projectPath, name, framework, bundler, router, stateManagement) {
|
|
457
|
+
const pkgPath = path3.join(projectPath, "package.json");
|
|
458
|
+
const existing = await fs2.pathExists(pkgPath) ? await fs2.readJson(pkgPath) : {};
|
|
459
|
+
const deps = { ...existing.dependencies };
|
|
460
|
+
const devDeps = { ...existing.devDependencies };
|
|
461
|
+
if (framework === "vue") {
|
|
462
|
+
deps.vue = "^3.5.0";
|
|
463
|
+
if (router === "vue-router") deps["vue-router"] = "^4.5.0";
|
|
464
|
+
if (stateManagement === "pinia") deps.pinia = "^2.3.0";
|
|
465
|
+
if (stateManagement === "vuex") deps.vuex = "^4.1.0";
|
|
466
|
+
} else {
|
|
467
|
+
deps.react = "^19.0.0";
|
|
468
|
+
deps["react-dom"] = "^19.0.0";
|
|
469
|
+
devDeps["@types/react"] = "^19.0.0";
|
|
470
|
+
devDeps["@types/react-dom"] = "^19.0.0";
|
|
471
|
+
if (router === "react-router") deps["react-router-dom"] = "^7.0.0";
|
|
472
|
+
if (stateManagement === "redux-toolkit") {
|
|
473
|
+
deps["@reduxjs/toolkit"] = "^2.6.0";
|
|
474
|
+
deps["react-redux"] = "^9.2.0";
|
|
475
|
+
}
|
|
476
|
+
if (stateManagement === "zustand") deps.zustand = "^5.0.0";
|
|
477
|
+
if (stateManagement === "mobx") {
|
|
478
|
+
deps.mobx = "^6.13.0";
|
|
479
|
+
deps["mobx-react-lite"] = "^4.1.0";
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
devDeps.typescript = "^5.8.0";
|
|
483
|
+
if (bundler === "vite") {
|
|
484
|
+
devDeps.vite = "^6.3.0";
|
|
485
|
+
if (framework === "react") devDeps["@vitejs/plugin-react"] = "^4.4.0";
|
|
486
|
+
if (framework === "vue") devDeps["@vitejs/plugin-vue"] = "^5.2.0";
|
|
487
|
+
} else if (bundler === "webpack") {
|
|
488
|
+
devDeps.webpack = "^5.99.0";
|
|
489
|
+
devDeps["webpack-cli"] = "^6.0.0";
|
|
490
|
+
devDeps["ts-loader"] = "^9.5.0";
|
|
491
|
+
} else if (bundler === "rspack") {
|
|
492
|
+
devDeps["@rspack/core"] = "^1.3.0";
|
|
493
|
+
devDeps["@rspack/cli"] = "^1.3.0";
|
|
494
|
+
}
|
|
495
|
+
const scripts = { ...existing.scripts };
|
|
496
|
+
if (bundler === "vite") {
|
|
497
|
+
scripts.dev = "vite";
|
|
498
|
+
scripts.build = "vite build";
|
|
499
|
+
scripts.preview = "vite preview";
|
|
500
|
+
} else if (bundler === "webpack") {
|
|
501
|
+
scripts.dev = "webpack serve --mode development";
|
|
502
|
+
scripts.build = "webpack --mode production";
|
|
503
|
+
} else if (bundler === "rspack") {
|
|
504
|
+
scripts.dev = "rspack serve";
|
|
505
|
+
scripts.build = "rspack build";
|
|
506
|
+
}
|
|
507
|
+
const pkg = {
|
|
508
|
+
...existing,
|
|
509
|
+
name,
|
|
510
|
+
version: "0.1.0",
|
|
511
|
+
private: true,
|
|
512
|
+
type: "module",
|
|
513
|
+
scripts,
|
|
514
|
+
dependencies: deps,
|
|
515
|
+
devDependencies: devDeps
|
|
516
|
+
};
|
|
517
|
+
await fs2.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/generators/lint-generator.ts
|
|
521
|
+
import path4 from "path";
|
|
522
|
+
import fs3 from "fs-extra";
|
|
523
|
+
async function generateLintConfigs(answers) {
|
|
524
|
+
const { projectPath, lintTools, framework } = answers;
|
|
525
|
+
for (const tool of lintTools) {
|
|
526
|
+
await GENERATORS[tool]?.(projectPath, framework);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
var GENERATORS = {
|
|
530
|
+
eslint: generateEslint,
|
|
531
|
+
stylelint: generateStylelint,
|
|
532
|
+
prettier: generatePrettier,
|
|
533
|
+
editorconfig: generateEditorConfig
|
|
534
|
+
};
|
|
535
|
+
async function generateEslint(root, fw) {
|
|
536
|
+
const pkgPath = path4.join(root, "package.json");
|
|
537
|
+
const pkg = await fs3.readJson(pkgPath);
|
|
538
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
539
|
+
devDeps.eslint = "^9.25.0";
|
|
540
|
+
devDeps["@eslint/js"] = "^9.25.0";
|
|
541
|
+
devDeps["typescript-eslint"] = "^8.30.0";
|
|
542
|
+
devDeps.globals = "^16.0.0";
|
|
543
|
+
if (fw === "vue") {
|
|
544
|
+
devDeps["eslint-plugin-vue"] = "^10.0.0";
|
|
545
|
+
} else {
|
|
546
|
+
devDeps["eslint-plugin-react-hooks"] = "^5.2.0";
|
|
547
|
+
devDeps["eslint-plugin-react-refresh"] = "^0.4.0";
|
|
548
|
+
}
|
|
549
|
+
pkg.devDependencies = devDeps;
|
|
550
|
+
await fs3.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
551
|
+
const configLines = fw === "vue" ? [
|
|
552
|
+
"import js from '@eslint/js';",
|
|
553
|
+
"import tseslint from 'typescript-eslint';",
|
|
554
|
+
"import pluginVue from 'eslint-plugin-vue';",
|
|
555
|
+
"",
|
|
556
|
+
"export default tseslint.config(",
|
|
557
|
+
" js.configs.recommended,",
|
|
558
|
+
" ...tseslint.configs.recommended,",
|
|
559
|
+
" ...pluginVue.configs['flat/recommended'],",
|
|
560
|
+
" {",
|
|
561
|
+
" files: ['**/*.vue'],",
|
|
562
|
+
" languageOptions: {",
|
|
563
|
+
" parserOptions: { parser: tseslint.parser },",
|
|
564
|
+
" },",
|
|
565
|
+
" },",
|
|
566
|
+
");"
|
|
567
|
+
] : [
|
|
568
|
+
"import js from '@eslint/js';",
|
|
569
|
+
"import tseslint from 'typescript-eslint';",
|
|
570
|
+
"import reactHooks from 'eslint-plugin-react-hooks';",
|
|
571
|
+
"import reactRefresh from 'eslint-plugin-react-refresh';",
|
|
572
|
+
"import globals from 'globals';",
|
|
573
|
+
"",
|
|
574
|
+
"export default tseslint.config(",
|
|
575
|
+
" js.configs.recommended,",
|
|
576
|
+
" ...tseslint.configs.recommended,",
|
|
577
|
+
" {",
|
|
578
|
+
" files: ['**/*.{ts,tsx}'],",
|
|
579
|
+
" plugins: {",
|
|
580
|
+
" 'react-hooks': reactHooks,",
|
|
581
|
+
" 'react-refresh': reactRefresh,",
|
|
582
|
+
" },",
|
|
583
|
+
" languageOptions: {",
|
|
584
|
+
" globals: globals.browser,",
|
|
585
|
+
" },",
|
|
586
|
+
" rules: {",
|
|
587
|
+
" ...reactHooks.configs.recommended.rules,",
|
|
588
|
+
" 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],",
|
|
589
|
+
" },",
|
|
590
|
+
" },",
|
|
591
|
+
");"
|
|
592
|
+
];
|
|
593
|
+
await fs3.writeFile(path4.join(root, "eslint.config.mjs"), configLines.join("\n"), "utf-8");
|
|
594
|
+
logger.success("ESLint 9 flat config generated");
|
|
595
|
+
}
|
|
596
|
+
async function generateStylelint(root) {
|
|
597
|
+
const pkgPath = path4.join(root, "package.json");
|
|
598
|
+
const pkg = await fs3.readJson(pkgPath);
|
|
599
|
+
pkg.devDependencies = {
|
|
600
|
+
...pkg.devDependencies,
|
|
601
|
+
stylelint: "^16.17.0",
|
|
602
|
+
"stylelint-config-standard": "^37.0.0"
|
|
603
|
+
};
|
|
604
|
+
await fs3.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
605
|
+
await fs3.writeJson(
|
|
606
|
+
path4.join(root, ".stylelintrc.json"),
|
|
607
|
+
{ extends: ["stylelint-config-standard"] },
|
|
608
|
+
{ spaces: 2 }
|
|
609
|
+
);
|
|
610
|
+
logger.success("Stylelint config generated");
|
|
611
|
+
}
|
|
612
|
+
async function generatePrettier(root) {
|
|
613
|
+
const pkgPath = path4.join(root, "package.json");
|
|
614
|
+
const pkg = await fs3.readJson(pkgPath);
|
|
615
|
+
pkg.devDependencies = { ...pkg.devDependencies, prettier: "^3.5.0" };
|
|
616
|
+
await fs3.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
617
|
+
const config = {
|
|
618
|
+
semi: true,
|
|
619
|
+
singleQuote: true,
|
|
620
|
+
tabWidth: 2,
|
|
621
|
+
trailingComma: "all",
|
|
622
|
+
printWidth: 100
|
|
623
|
+
};
|
|
624
|
+
await fs3.writeJson(path4.join(root, ".prettierrc.json"), config, { spaces: 2 });
|
|
625
|
+
logger.success("Prettier config generated");
|
|
626
|
+
}
|
|
627
|
+
async function generateEditorConfig(root) {
|
|
628
|
+
const content = [
|
|
629
|
+
"root = true",
|
|
630
|
+
"",
|
|
631
|
+
"[*]",
|
|
632
|
+
"indent_style = space",
|
|
633
|
+
"indent_size = 2",
|
|
634
|
+
"end_of_line = lf",
|
|
635
|
+
"charset = utf-8",
|
|
636
|
+
"trim_trailing_whitespace = true",
|
|
637
|
+
"insert_final_newline = true"
|
|
638
|
+
].join("\n");
|
|
639
|
+
await fs3.writeFile(path4.join(root, ".editorconfig"), content, "utf-8");
|
|
640
|
+
logger.success("EditorConfig generated");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/generators/fe-kit-meta-generator.ts
|
|
644
|
+
import path7 from "path";
|
|
645
|
+
import fs6 from "fs-extra";
|
|
646
|
+
|
|
647
|
+
// src/utils/fs.ts
|
|
648
|
+
import fs4 from "fs-extra";
|
|
649
|
+
import path5 from "path";
|
|
650
|
+
async function readJsonSafe(filePath) {
|
|
651
|
+
try {
|
|
652
|
+
return await fs4.readJson(filePath);
|
|
653
|
+
} catch {
|
|
654
|
+
return void 0;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async function writeJsonSafe(filePath, data) {
|
|
658
|
+
await fs4.ensureDir(path5.dirname(filePath));
|
|
659
|
+
await fs4.writeJson(filePath, data, { spaces: 2 });
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/core/merge-config.ts
|
|
663
|
+
import deepmerge from "deepmerge";
|
|
664
|
+
function mergeConfig(base, incoming) {
|
|
665
|
+
return deepmerge(base, incoming, {
|
|
666
|
+
arrayMerge: idempotentArrayMerge
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
function idempotentArrayMerge(target, source) {
|
|
670
|
+
if (isIdArray(target) || isIdArray(source)) {
|
|
671
|
+
return mergeById(target, source);
|
|
672
|
+
}
|
|
673
|
+
return [.../* @__PURE__ */ new Set([...target, ...source])];
|
|
674
|
+
}
|
|
675
|
+
function isIdArray(arr) {
|
|
676
|
+
return arr.length > 0 && typeof arr[0] === "object" && arr[0] !== null && "id" in arr[0];
|
|
677
|
+
}
|
|
678
|
+
function mergeById(target, source) {
|
|
679
|
+
const map = /* @__PURE__ */ new Map();
|
|
680
|
+
for (const item of target) map.set(item.id, item);
|
|
681
|
+
for (const item of source) {
|
|
682
|
+
const existing = map.get(item.id);
|
|
683
|
+
map.set(item.id, existing ? deepmerge(existing, item) : item);
|
|
684
|
+
}
|
|
685
|
+
return [...map.values()];
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/core/write-rules.ts
|
|
689
|
+
import path6 from "path";
|
|
690
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
691
|
+
import fs5 from "fs-extra";
|
|
692
|
+
var __dirname2 = path6.dirname(fileURLToPath3(import.meta.url));
|
|
693
|
+
function rulesSourceDir() {
|
|
694
|
+
return path6.resolve(__dirname2, "..", "rules");
|
|
695
|
+
}
|
|
696
|
+
async function writeRules(projectRoot, framework) {
|
|
697
|
+
const targetDir = path6.join(projectRoot, META_DIR, "rules");
|
|
698
|
+
const srcBase = rulesSourceDir();
|
|
699
|
+
const frameworkDir = mapFrameworkRulesDir(framework);
|
|
700
|
+
const dirs = [
|
|
701
|
+
...frameworkDir ? [path6.join(srcBase, frameworkDir)] : [],
|
|
702
|
+
path6.join(srcBase, "common")
|
|
703
|
+
];
|
|
704
|
+
for (const dir of dirs) {
|
|
705
|
+
if (!await fs5.pathExists(dir)) continue;
|
|
706
|
+
const relative = path6.basename(dir);
|
|
707
|
+
await fs5.copy(dir, path6.join(targetDir, relative), { overwrite: false });
|
|
708
|
+
}
|
|
709
|
+
logger.success(`Rules for ${framework} written to ${META_DIR}/rules/`);
|
|
710
|
+
}
|
|
711
|
+
function mapFrameworkRulesDir(fw) {
|
|
712
|
+
if (fw === "vue" || fw === "nuxt") return "vue";
|
|
713
|
+
if (fw === "react" || fw === "next") return "react";
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/generators/fe-kit-meta-generator.ts
|
|
718
|
+
function metaDir(root) {
|
|
719
|
+
return path7.join(root, META_DIR);
|
|
720
|
+
}
|
|
721
|
+
async function writeFeKitMeta(answers) {
|
|
722
|
+
const dir = metaDir(answers.projectPath);
|
|
723
|
+
await fs6.ensureDir(dir);
|
|
724
|
+
const project = {
|
|
725
|
+
name: answers.projectName,
|
|
726
|
+
framework: answers.framework,
|
|
727
|
+
router: answers.router,
|
|
728
|
+
stateManagement: answers.stateManagement,
|
|
729
|
+
bundler: answers.bundler,
|
|
730
|
+
typescript: true,
|
|
731
|
+
lintTools: answers.lintTools,
|
|
732
|
+
templateVersion: "0.1.0",
|
|
733
|
+
rulesVersion: "0.1.0",
|
|
734
|
+
cliVersion: CLI_VERSION
|
|
735
|
+
};
|
|
736
|
+
const skills = {
|
|
737
|
+
enabled: answers.skills.map((id) => ({
|
|
738
|
+
id,
|
|
739
|
+
source: "builtin",
|
|
740
|
+
targets: answers.devTools,
|
|
741
|
+
version: CLI_VERSION
|
|
742
|
+
}))
|
|
743
|
+
};
|
|
744
|
+
const mcp = {
|
|
745
|
+
enabled: answers.mcpServers.map((id) => ({
|
|
746
|
+
id,
|
|
747
|
+
adapterStatus: Object.fromEntries(answers.devTools.map((t) => [t, "written"])),
|
|
748
|
+
version: CLI_VERSION
|
|
749
|
+
})),
|
|
750
|
+
configVersion: CLI_VERSION
|
|
751
|
+
};
|
|
752
|
+
const tools = {
|
|
753
|
+
selectedTools: answers.devTools,
|
|
754
|
+
paths: {},
|
|
755
|
+
extensionRecommendations: []
|
|
756
|
+
};
|
|
757
|
+
await Promise.all([
|
|
758
|
+
writeJsonSafe(path7.join(dir, "project.json"), project),
|
|
759
|
+
writeJsonSafe(path7.join(dir, "skills.json"), skills),
|
|
760
|
+
writeJsonSafe(path7.join(dir, "mcp.json"), mcp),
|
|
761
|
+
writeJsonSafe(path7.join(dir, "tools.json"), tools)
|
|
762
|
+
]);
|
|
763
|
+
await writeRules(answers.projectPath, answers.framework);
|
|
764
|
+
logger.success(`.fe-kit/ metadata written`);
|
|
765
|
+
}
|
|
766
|
+
async function updateFeKitMeta(projectRoot, detection, answers) {
|
|
767
|
+
const dir = metaDir(projectRoot);
|
|
768
|
+
await fs6.ensureDir(dir);
|
|
769
|
+
const fw = detection.framework;
|
|
770
|
+
const isClassicDetection = "router" in detection;
|
|
771
|
+
const projectPath = path7.join(dir, "project.json");
|
|
772
|
+
const existingProject = await readJsonSafe(projectPath) ?? {
|
|
773
|
+
name: detection.name,
|
|
774
|
+
framework: fw,
|
|
775
|
+
router: isClassicDetection ? detection.router ?? "" : "",
|
|
776
|
+
stateManagement: isClassicDetection ? detection.stateManagement ?? "" : "",
|
|
777
|
+
bundler: detection.bundler ?? "vite",
|
|
778
|
+
typescript: true,
|
|
779
|
+
lintTools: [],
|
|
780
|
+
templateVersion: "0.1.0",
|
|
781
|
+
rulesVersion: "0.1.0",
|
|
782
|
+
cliVersion: CLI_VERSION
|
|
783
|
+
};
|
|
784
|
+
await writeJsonSafe(projectPath, { ...existingProject, cliVersion: CLI_VERSION });
|
|
785
|
+
const skillsPath = path7.join(dir, "skills.json");
|
|
786
|
+
const existingSkills = await readJsonSafe(skillsPath) ?? { enabled: [] };
|
|
787
|
+
const newSkillEntries = answers.skills.map((id) => ({
|
|
788
|
+
id,
|
|
789
|
+
source: "builtin",
|
|
790
|
+
targets: answers.devTools,
|
|
791
|
+
version: CLI_VERSION
|
|
792
|
+
}));
|
|
793
|
+
await writeJsonSafe(
|
|
794
|
+
skillsPath,
|
|
795
|
+
mergeConfig(existingSkills, { enabled: newSkillEntries })
|
|
796
|
+
);
|
|
797
|
+
const mcpPath = path7.join(dir, "mcp.json");
|
|
798
|
+
const existingMcp = await readJsonSafe(mcpPath) ?? {
|
|
799
|
+
enabled: [],
|
|
800
|
+
configVersion: CLI_VERSION
|
|
801
|
+
};
|
|
802
|
+
const newMcpEntries = answers.mcpServers.map((id) => ({
|
|
803
|
+
id,
|
|
804
|
+
adapterStatus: Object.fromEntries(answers.devTools.map((t) => [t, "written"])),
|
|
805
|
+
version: CLI_VERSION
|
|
806
|
+
}));
|
|
807
|
+
await writeJsonSafe(
|
|
808
|
+
mcpPath,
|
|
809
|
+
mergeConfig(existingMcp, { enabled: newMcpEntries, configVersion: CLI_VERSION })
|
|
810
|
+
);
|
|
811
|
+
const toolsPath = path7.join(dir, "tools.json");
|
|
812
|
+
const existingTools = await readJsonSafe(toolsPath) ?? {
|
|
813
|
+
selectedTools: [],
|
|
814
|
+
paths: {},
|
|
815
|
+
extensionRecommendations: []
|
|
816
|
+
};
|
|
817
|
+
await writeJsonSafe(
|
|
818
|
+
toolsPath,
|
|
819
|
+
mergeConfig(existingTools, { selectedTools: answers.devTools })
|
|
820
|
+
);
|
|
821
|
+
await writeRules(projectRoot, fw);
|
|
822
|
+
logger.success(`.fe-kit/ metadata updated`);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/generators/readme-generator.ts
|
|
826
|
+
import path8 from "path";
|
|
827
|
+
import fs7 from "fs-extra";
|
|
828
|
+
function scriptBlock(bundler) {
|
|
829
|
+
if (bundler === "vite") {
|
|
830
|
+
return [
|
|
831
|
+
"- `pnpm dev`\uFF1A\u542F\u52A8\u5F00\u53D1\u670D\u52A1\u5668",
|
|
832
|
+
"- `pnpm build`\uFF1A\u6784\u5EFA\u751F\u4EA7\u4EA7\u7269",
|
|
833
|
+
"- `pnpm preview`\uFF1A\u672C\u5730\u9884\u89C8\u751F\u4EA7\u6784\u5EFA"
|
|
834
|
+
].join("\n");
|
|
835
|
+
}
|
|
836
|
+
if (bundler === "webpack") {
|
|
837
|
+
return [
|
|
838
|
+
"- `pnpm dev`\uFF1A\u542F\u52A8\u5F00\u53D1\u670D\u52A1\u5668",
|
|
839
|
+
"- `pnpm build`\uFF1A\u6784\u5EFA\u751F\u4EA7\u4EA7\u7269"
|
|
840
|
+
].join("\n");
|
|
841
|
+
}
|
|
842
|
+
return [
|
|
843
|
+
"- `pnpm dev`\uFF1A\u542F\u52A8\u5F00\u53D1\u670D\u52A1\u5668",
|
|
844
|
+
"- `pnpm build`\uFF1A\u6784\u5EFA\u751F\u4EA7\u4EA7\u7269"
|
|
845
|
+
].join("\n");
|
|
846
|
+
}
|
|
847
|
+
function structureBlock(framework) {
|
|
848
|
+
const lines = [
|
|
849
|
+
"```text",
|
|
850
|
+
".",
|
|
851
|
+
"\u251C\u2500 public/ # \u9759\u6001\u8D44\u6E90\uFF08\u4E0D\u7ECF\u6784\u5EFA\u76F4\u63A5\u8F93\u51FA\uFF09",
|
|
852
|
+
"\u251C\u2500 src/",
|
|
853
|
+
"\u2502 \u251C\u2500 app/ # \u201C\u5E94\u7528\u5C42\u201D\u7EC4\u7EC7",
|
|
854
|
+
"\u2502 \u251C\u2500 components/ # \u4E1A\u52A1/\u901A\u7528\u7EC4\u4EF6",
|
|
855
|
+
"\u2502 \u251C\u2500 hooks/ # React Hooks / Vue Composables\uFF08\u6309\u9700\u653E\uFF09",
|
|
856
|
+
"\u2502 \u251C\u2500 lib/ # \u5DE5\u5177\u51FD\u6570\u3001\u8BF7\u6C42\u5C01\u88C5\u3001\u7EAF\u903B\u8F91\uFF08\u4E0D\u76F4\u63A5\u4F9D\u8D56 UI\uFF09",
|
|
857
|
+
"\u2502 \u251C\u2500 styles/ # \u6837\u5F0F\u4E0E\u8BBE\u8BA1 token\uFF08\u6309\u9700\u653E\uFF09",
|
|
858
|
+
"\u2502 \u2514\u2500 " + (framework === "react" ? "main.tsx" : "main.ts") + " # \u5165\u53E3\u6587\u4EF6",
|
|
859
|
+
"\u251C\u2500 .fe-kit/ # fe-kit \u751F\u6210\u7684\u5143\u6570\u636E\u4E0E\u89C4\u5219",
|
|
860
|
+
"\u251C\u2500 package.json",
|
|
861
|
+
"\u251C\u2500 tsconfig.json",
|
|
862
|
+
"\u2514\u2500 README.md",
|
|
863
|
+
"```"
|
|
864
|
+
];
|
|
865
|
+
return lines.join("\n");
|
|
866
|
+
}
|
|
867
|
+
async function generateReadme(answers) {
|
|
868
|
+
const outPath = path8.join(answers.projectPath, "README.md");
|
|
869
|
+
const content = [
|
|
870
|
+
`# ${answers.projectName}`,
|
|
871
|
+
"",
|
|
872
|
+
"\u4E00\u4E2A\u7531 **fe-kit** \u521D\u59CB\u5316\u7684\u524D\u7AEF\u5DE5\u7A0B\u9AA8\u67B6\u3002",
|
|
873
|
+
"",
|
|
874
|
+
"## \u73AF\u5883\u8981\u6C42",
|
|
875
|
+
"",
|
|
876
|
+
"- Node.js >= 18",
|
|
877
|
+
"- pnpm\uFF08\u63A8\u8350\uFF09",
|
|
878
|
+
"",
|
|
879
|
+
"## \u5FEB\u901F\u5F00\u59CB",
|
|
880
|
+
"",
|
|
881
|
+
"\u5B89\u88C5\u4F9D\u8D56\uFF1A",
|
|
882
|
+
"",
|
|
883
|
+
"```bash",
|
|
884
|
+
"pnpm install",
|
|
885
|
+
"```",
|
|
886
|
+
"",
|
|
887
|
+
"\u5E38\u7528\u811A\u672C\uFF1A",
|
|
888
|
+
"",
|
|
889
|
+
scriptBlock(answers.bundler),
|
|
890
|
+
"",
|
|
891
|
+
"## \u76EE\u5F55\u7ED3\u6784",
|
|
892
|
+
"",
|
|
893
|
+
structureBlock(answers.framework),
|
|
894
|
+
"",
|
|
895
|
+
"## \u7EA6\u5B9A\u8BF4\u660E\uFF08\u91CD\u8981\uFF09",
|
|
896
|
+
"",
|
|
897
|
+
"- **\u4E0D\u5305\u542B service/back-end \u5185\u5BB9**\uFF1A\u672C\u9AA8\u67B6\u4E0D\u751F\u6210\u4EFB\u4F55\u670D\u52A1\u7AEF\u76EE\u5F55\u4E0E API \u8DEF\u7531\u3002",
|
|
898
|
+
"- **\u5E94\u7528\u5C42\u7EC4\u7EC7**\uFF1A`src/app/` \u4EC5\u4F5C\u4E3A\u4EE3\u7801\u7EC4\u7EC7\u7EA6\u5B9A\uFF0C\u4E0D\u5F3A\u7ED1\u5B9A\u5177\u4F53\u6846\u67B6\u8FD0\u884C\u65F6\u3002",
|
|
899
|
+
""
|
|
900
|
+
].join("\n");
|
|
901
|
+
await fs7.writeFile(outPath, content, "utf-8");
|
|
902
|
+
logger.success("README.md generated");
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/adapters/cursor.ts
|
|
906
|
+
import path10 from "path";
|
|
907
|
+
import fs9 from "fs-extra";
|
|
908
|
+
|
|
909
|
+
// src/core/paths.ts
|
|
910
|
+
var TOOL_CONFIG_PATHS = {
|
|
911
|
+
cursor: ".cursor",
|
|
912
|
+
"claude-code": ".claude",
|
|
913
|
+
vscode: ".vscode",
|
|
914
|
+
"codebuddy-cn": ".codebuddy",
|
|
915
|
+
trae: ".trae",
|
|
916
|
+
idea: ".idea"
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// src/core/rule-writers.ts
|
|
920
|
+
import path9 from "path";
|
|
921
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
922
|
+
import fs8 from "fs-extra";
|
|
923
|
+
var __dirname3 = path9.dirname(fileURLToPath4(import.meta.url));
|
|
924
|
+
function rulesSourceDir2() {
|
|
925
|
+
return path9.resolve(__dirname3, "..", "rules");
|
|
926
|
+
}
|
|
927
|
+
function mapFrameworkDir(fw) {
|
|
928
|
+
if (fw === "vue" || fw === "nuxt") return "vue";
|
|
929
|
+
if (fw === "react" || fw === "next") return "react";
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
async function loadRuleSources(framework) {
|
|
933
|
+
const srcBase = rulesSourceDir2();
|
|
934
|
+
const fwDir = mapFrameworkDir(framework);
|
|
935
|
+
const dirs = [
|
|
936
|
+
{ dir: path9.join(srcBase, "common"), category: "common" }
|
|
937
|
+
];
|
|
938
|
+
if (fwDir) {
|
|
939
|
+
dirs.push({ dir: path9.join(srcBase, fwDir), category: fwDir });
|
|
940
|
+
}
|
|
941
|
+
const rules = [];
|
|
942
|
+
for (const { dir, category } of dirs) {
|
|
943
|
+
if (!await fs8.pathExists(dir)) continue;
|
|
944
|
+
const files = await fs8.readdir(dir);
|
|
945
|
+
for (const file of files) {
|
|
946
|
+
if (!file.endsWith(".md")) continue;
|
|
947
|
+
const content = await fs8.readFile(path9.join(dir, file), "utf-8");
|
|
948
|
+
rules.push({
|
|
949
|
+
name: path9.basename(file, ".md"),
|
|
950
|
+
category,
|
|
951
|
+
content: content.trim()
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return rules;
|
|
956
|
+
}
|
|
957
|
+
function toMdcContent(rule, globs) {
|
|
958
|
+
const globLine = globs ? `globs: ${JSON.stringify(globs)}` : "";
|
|
959
|
+
const lines = [
|
|
960
|
+
"---",
|
|
961
|
+
`description: ${rule.category}/${rule.name} conventions`,
|
|
962
|
+
...globLine ? [globLine] : [],
|
|
963
|
+
"alwaysApply: true",
|
|
964
|
+
"---",
|
|
965
|
+
"",
|
|
966
|
+
rule.content,
|
|
967
|
+
""
|
|
968
|
+
];
|
|
969
|
+
return lines.join("\n");
|
|
970
|
+
}
|
|
971
|
+
function ruleGlobs(rule) {
|
|
972
|
+
if (rule.category === "react") return ["**/*.{tsx,jsx,ts,js}"];
|
|
973
|
+
if (rule.category === "vue") return ["**/*.{vue,ts,js}"];
|
|
974
|
+
return void 0;
|
|
975
|
+
}
|
|
976
|
+
async function writeCursorRules(projectRoot, framework) {
|
|
977
|
+
const rules = await loadRuleSources(framework);
|
|
978
|
+
const dir = path9.join(projectRoot, ".cursor", "rules");
|
|
979
|
+
await fs8.ensureDir(dir);
|
|
980
|
+
for (const rule of rules) {
|
|
981
|
+
const filename = `${rule.category}-${rule.name}.mdc`;
|
|
982
|
+
await fs8.writeFile(path9.join(dir, filename), toMdcContent(rule, ruleGlobs(rule)), "utf-8");
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
async function writeCodeBuddyRules(projectRoot, framework) {
|
|
986
|
+
const rules = await loadRuleSources(framework);
|
|
987
|
+
const dir = path9.join(projectRoot, ".codebuddy", "rules");
|
|
988
|
+
await fs8.ensureDir(dir);
|
|
989
|
+
for (const rule of rules) {
|
|
990
|
+
const filename = `${rule.category}-${rule.name}.mdc`;
|
|
991
|
+
await fs8.writeFile(path9.join(dir, filename), toMdcContent(rule, ruleGlobs(rule)), "utf-8");
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async function writeCopilotInstructions(projectRoot, framework) {
|
|
995
|
+
const rules = await loadRuleSources(framework);
|
|
996
|
+
const dir = path9.join(projectRoot, ".github");
|
|
997
|
+
await fs8.ensureDir(dir);
|
|
998
|
+
const sections = rules.map((r) => r.content);
|
|
999
|
+
const body = [
|
|
1000
|
+
"# Project Coding Guidelines",
|
|
1001
|
+
"",
|
|
1002
|
+
...sections.flatMap((s) => [s, ""])
|
|
1003
|
+
].join("\n");
|
|
1004
|
+
await fs8.writeFile(path9.join(dir, "copilot-instructions.md"), body, "utf-8");
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/adapters/cursor.ts
|
|
1008
|
+
var cursorAdapter = {
|
|
1009
|
+
id: "cursor",
|
|
1010
|
+
async applySkills(_ctx, skills) {
|
|
1011
|
+
if (skills.length === 0) return;
|
|
1012
|
+
logger.info("Cursor: skills are applied via rules \u2014 no separate skills directory needed.");
|
|
1013
|
+
},
|
|
1014
|
+
async applyMcp(ctx, mcp) {
|
|
1015
|
+
if (mcp.length === 0) return;
|
|
1016
|
+
const configPath = path10.join(ctx.projectRoot, TOOL_CONFIG_PATHS.cursor, "mcp.json");
|
|
1017
|
+
await fs9.ensureDir(path10.dirname(configPath));
|
|
1018
|
+
const existing = await readJsonSafe(configPath) ?? {};
|
|
1019
|
+
const mcpSection = existing.mcpServers ?? {};
|
|
1020
|
+
for (const m of mcp) {
|
|
1021
|
+
mcpSection[m.id] = m.config;
|
|
1022
|
+
}
|
|
1023
|
+
await writeJsonSafe(configPath, mergeConfig(existing, { mcpServers: mcpSection }));
|
|
1024
|
+
logger.success("Cursor: MCP config written to .cursor/mcp.json");
|
|
1025
|
+
},
|
|
1026
|
+
async applyRules(ctx) {
|
|
1027
|
+
await writeCursorRules(ctx.projectRoot, ctx.framework);
|
|
1028
|
+
logger.success("Cursor: rules written to .cursor/rules/*.mdc");
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
// src/adapters/claude-code.ts
|
|
1033
|
+
import path11 from "path";
|
|
1034
|
+
import fs10 from "fs-extra";
|
|
1035
|
+
var claudeCodeAdapter = {
|
|
1036
|
+
id: "claude-code",
|
|
1037
|
+
async applySkills(ctx, skills) {
|
|
1038
|
+
if (skills.length === 0) return;
|
|
1039
|
+
const skillsRoot = path11.join(ctx.projectRoot, TOOL_CONFIG_PATHS["claude-code"], "skills");
|
|
1040
|
+
await fs10.ensureDir(skillsRoot);
|
|
1041
|
+
for (const skill of skills) {
|
|
1042
|
+
const targetDir = path11.join(skillsRoot, skill.id);
|
|
1043
|
+
await fs10.ensureDir(targetDir);
|
|
1044
|
+
if (skill.sourcePath) {
|
|
1045
|
+
const sourceDir = path11.dirname(skill.sourcePath);
|
|
1046
|
+
await fs10.copy(sourceDir, targetDir, { overwrite: true });
|
|
1047
|
+
} else {
|
|
1048
|
+
await fs10.writeFile(path11.join(targetDir, "SKILL.md"), skill.content, "utf-8");
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
logger.success("Claude Code: skills copied to .claude/skills/<skill>/SKILL.md");
|
|
1052
|
+
},
|
|
1053
|
+
async applyMcp(ctx, mcp) {
|
|
1054
|
+
if (mcp.length === 0) return;
|
|
1055
|
+
const configPath = path11.join(ctx.projectRoot, ".mcp.json");
|
|
1056
|
+
const existing = await readJsonSafe(configPath) ?? {};
|
|
1057
|
+
const mcpSection = existing.mcpServers ?? {};
|
|
1058
|
+
for (const m of mcp) {
|
|
1059
|
+
mcpSection[m.id] = m.config;
|
|
1060
|
+
}
|
|
1061
|
+
await writeJsonSafe(configPath, mergeConfig(existing, { mcpServers: mcpSection }));
|
|
1062
|
+
logger.success("Claude Code: MCP config written to .mcp.json");
|
|
1063
|
+
},
|
|
1064
|
+
async applyRules(ctx) {
|
|
1065
|
+
const rulesDir = path11.join(ctx.projectRoot, TOOL_CONFIG_PATHS["claude-code"], "rules");
|
|
1066
|
+
await fs10.ensureDir(rulesDir);
|
|
1067
|
+
const rules = await loadRuleSources(ctx.framework);
|
|
1068
|
+
for (const rule of rules) {
|
|
1069
|
+
const filename = `${rule.category}-${rule.name}.md`;
|
|
1070
|
+
await fs10.writeFile(path11.join(rulesDir, filename), rule.content + "\n", "utf-8");
|
|
1071
|
+
}
|
|
1072
|
+
logger.success("Claude Code: rules written to .claude/rules/");
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
// src/adapters/vscode.ts
|
|
1077
|
+
import path12 from "path";
|
|
1078
|
+
import fs11 from "fs-extra";
|
|
1079
|
+
var vscodeAdapter = {
|
|
1080
|
+
id: "vscode",
|
|
1081
|
+
async applySkills(_ctx, skills) {
|
|
1082
|
+
if (skills.length === 0) return;
|
|
1083
|
+
logger.info("VS Code: skills are applied via Copilot instructions \u2014 no separate skills directory.");
|
|
1084
|
+
},
|
|
1085
|
+
async applyMcp(ctx, mcp) {
|
|
1086
|
+
if (mcp.length === 0) return;
|
|
1087
|
+
const dir = path12.join(ctx.projectRoot, TOOL_CONFIG_PATHS.vscode);
|
|
1088
|
+
await fs11.ensureDir(dir);
|
|
1089
|
+
const configPath = path12.join(dir, "mcp.json");
|
|
1090
|
+
const existing = await readJsonSafe(configPath) ?? {};
|
|
1091
|
+
const serversSection = existing.servers ?? {};
|
|
1092
|
+
for (const m of mcp) {
|
|
1093
|
+
serversSection[m.id] = m.config;
|
|
1094
|
+
}
|
|
1095
|
+
await writeJsonSafe(configPath, mergeConfig(existing, { servers: serversSection }));
|
|
1096
|
+
logger.success("VS Code: MCP config written to .vscode/mcp.json (servers)");
|
|
1097
|
+
},
|
|
1098
|
+
async applyRules(ctx) {
|
|
1099
|
+
const dir = path12.join(ctx.projectRoot, TOOL_CONFIG_PATHS.vscode);
|
|
1100
|
+
await fs11.ensureDir(dir);
|
|
1101
|
+
const extensionsPath = path12.join(dir, "extensions.json");
|
|
1102
|
+
const existing = await readJsonSafe(extensionsPath) ?? {};
|
|
1103
|
+
const recs = new Set(existing.recommendations ?? []);
|
|
1104
|
+
const recommended = [
|
|
1105
|
+
"dbaeumer.vscode-eslint",
|
|
1106
|
+
"esbenp.prettier-vscode",
|
|
1107
|
+
"stylelint.vscode-stylelint"
|
|
1108
|
+
];
|
|
1109
|
+
if (ctx.framework === "vue") recommended.push("Vue.volar");
|
|
1110
|
+
for (const ext of recommended) recs.add(ext);
|
|
1111
|
+
await writeJsonSafe(extensionsPath, { recommendations: [...recs] });
|
|
1112
|
+
await writeCopilotInstructions(ctx.projectRoot, ctx.framework);
|
|
1113
|
+
logger.success("VS Code: extensions.json + .github/copilot-instructions.md updated");
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
// src/adapters/codebuddy-cn.ts
|
|
1118
|
+
import path13 from "path";
|
|
1119
|
+
var codebuddyCnAdapter = {
|
|
1120
|
+
id: "codebuddy-cn",
|
|
1121
|
+
async applySkills(_ctx, skills) {
|
|
1122
|
+
if (skills.length === 0) return;
|
|
1123
|
+
logger.info("CodeBuddy: skills are applied via rules \u2014 no separate skills directory needed.");
|
|
1124
|
+
},
|
|
1125
|
+
async applyMcp(ctx, mcp) {
|
|
1126
|
+
if (mcp.length === 0) return;
|
|
1127
|
+
const configPath = path13.join(ctx.projectRoot, ".mcp.json");
|
|
1128
|
+
const existing = await readJsonSafe(configPath) ?? {};
|
|
1129
|
+
const mcpSection = existing.mcpServers ?? {};
|
|
1130
|
+
for (const m of mcp) {
|
|
1131
|
+
mcpSection[m.id] = m.config;
|
|
1132
|
+
}
|
|
1133
|
+
await writeJsonSafe(configPath, mergeConfig(existing, { mcpServers: mcpSection }));
|
|
1134
|
+
logger.success("CodeBuddy: MCP config written to .mcp.json");
|
|
1135
|
+
},
|
|
1136
|
+
async applyRules(ctx) {
|
|
1137
|
+
await writeCodeBuddyRules(ctx.projectRoot, ctx.framework);
|
|
1138
|
+
logger.success("CodeBuddy: rules written to .codebuddy/rules/*.mdc");
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
// src/adapters/trae.ts
|
|
1143
|
+
import path14 from "path";
|
|
1144
|
+
import fs12 from "fs-extra";
|
|
1145
|
+
var traeAdapter = {
|
|
1146
|
+
id: "trae",
|
|
1147
|
+
async applySkills(ctx, skills) {
|
|
1148
|
+
if (skills.length === 0) return;
|
|
1149
|
+
const dir = path14.join(ctx.projectRoot, TOOL_CONFIG_PATHS.trae, "rules");
|
|
1150
|
+
await fs12.ensureDir(dir);
|
|
1151
|
+
for (const skill of skills) {
|
|
1152
|
+
const filePath = path14.join(dir, `${skill.id}.md`);
|
|
1153
|
+
await fs12.writeFile(filePath, skill.content, "utf-8");
|
|
1154
|
+
}
|
|
1155
|
+
logger.success("Trae: skills written to .trae/rules/");
|
|
1156
|
+
},
|
|
1157
|
+
async applyMcp(ctx, mcp) {
|
|
1158
|
+
if (mcp.length === 0) return;
|
|
1159
|
+
const configPath = path14.join(ctx.projectRoot, TOOL_CONFIG_PATHS.trae, "mcp.json");
|
|
1160
|
+
await fs12.ensureDir(path14.dirname(configPath));
|
|
1161
|
+
const existing = await readJsonSafe(configPath) ?? {};
|
|
1162
|
+
const mcpSection = existing.mcpServers ?? {};
|
|
1163
|
+
for (const m of mcp) {
|
|
1164
|
+
mcpSection[m.id] = m.config;
|
|
1165
|
+
}
|
|
1166
|
+
await writeJsonSafe(configPath, mergeConfig(existing, { mcpServers: mcpSection }));
|
|
1167
|
+
logger.success("Trae: MCP config written to .trae/mcp.json");
|
|
1168
|
+
},
|
|
1169
|
+
async applyRules(ctx) {
|
|
1170
|
+
const rulesDir = path14.join(ctx.projectRoot, TOOL_CONFIG_PATHS.trae, "rules");
|
|
1171
|
+
await fs12.ensureDir(rulesDir);
|
|
1172
|
+
const rules = await loadRuleSources(ctx.framework);
|
|
1173
|
+
for (const rule of rules) {
|
|
1174
|
+
const filename = `${rule.category}-${rule.name}.md`;
|
|
1175
|
+
await fs12.writeFile(path14.join(rulesDir, filename), rule.content + "\n", "utf-8");
|
|
1176
|
+
}
|
|
1177
|
+
logger.success("Trae: rules written to .trae/rules/");
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
// src/adapters/idea.ts
|
|
1182
|
+
import path15 from "path";
|
|
1183
|
+
import fs13 from "fs-extra";
|
|
1184
|
+
var ideaAdapter = {
|
|
1185
|
+
id: "idea",
|
|
1186
|
+
async applySkills(ctx, skills) {
|
|
1187
|
+
if (skills.length === 0) return;
|
|
1188
|
+
const dir = path15.join(ctx.projectRoot, TOOL_CONFIG_PATHS.idea);
|
|
1189
|
+
await fs13.ensureDir(dir);
|
|
1190
|
+
logger.info("IDEA: skill integration is a placeholder \u2014 manual config may be needed.");
|
|
1191
|
+
},
|
|
1192
|
+
async applyMcp(ctx, mcp) {
|
|
1193
|
+
if (mcp.length === 0) return;
|
|
1194
|
+
const dir = path15.join(ctx.projectRoot, TOOL_CONFIG_PATHS.idea);
|
|
1195
|
+
await fs13.ensureDir(dir);
|
|
1196
|
+
logger.info("IDEA: MCP integration is a placeholder \u2014 manual config may be needed.");
|
|
1197
|
+
},
|
|
1198
|
+
async applyRules(ctx) {
|
|
1199
|
+
const dir = path15.join(ctx.projectRoot, TOOL_CONFIG_PATHS.idea);
|
|
1200
|
+
await fs13.ensureDir(dir);
|
|
1201
|
+
logger.info("IDEA: rules integration is a placeholder.");
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
// src/adapters/registry.ts
|
|
1206
|
+
var adapters = {
|
|
1207
|
+
cursor: cursorAdapter,
|
|
1208
|
+
"claude-code": claudeCodeAdapter,
|
|
1209
|
+
vscode: vscodeAdapter,
|
|
1210
|
+
"codebuddy-cn": codebuddyCnAdapter,
|
|
1211
|
+
trae: traeAdapter,
|
|
1212
|
+
idea: ideaAdapter
|
|
1213
|
+
};
|
|
1214
|
+
function getAdapters(ids) {
|
|
1215
|
+
return ids.map((id) => adapters[id]);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// src/core/apply-adapters.ts
|
|
1219
|
+
async function applyAdapters(projectRoot, input) {
|
|
1220
|
+
const adapters2 = getAdapters(input.devTools);
|
|
1221
|
+
const ctx = { projectRoot, framework: input.framework };
|
|
1222
|
+
const skills = input.skills.map(getSkillById).filter((s) => s !== void 0);
|
|
1223
|
+
const mcp = input.mcpServers.map(getMcpById).filter((m) => m !== void 0);
|
|
1224
|
+
for (const adapter of adapters2) {
|
|
1225
|
+
logger.step(`Applying config for ${adapter.id}...`);
|
|
1226
|
+
await adapter.applySkills(ctx, skills);
|
|
1227
|
+
await adapter.applyMcp(ctx, mcp);
|
|
1228
|
+
await adapter.applyRules(ctx);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// src/commands/init.ts
|
|
1233
|
+
async function initCommand() {
|
|
1234
|
+
logger.step("Starting new project initialization...");
|
|
1235
|
+
const answers = await runInitPrompts();
|
|
1236
|
+
if (!answers) return;
|
|
1237
|
+
await generateProject(answers);
|
|
1238
|
+
await generateLintConfigs(answers);
|
|
1239
|
+
await writeFeKitMeta(answers);
|
|
1240
|
+
await generateReadme(answers);
|
|
1241
|
+
await applyAdapters(answers.projectPath, answers);
|
|
1242
|
+
logger.success(`Project "${answers.projectName}" created at ${answers.projectPath}`);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/commands/enhance.ts
|
|
1246
|
+
import path19 from "path";
|
|
1247
|
+
import fs17 from "fs-extra";
|
|
1248
|
+
|
|
1249
|
+
// src/core/detect-project.ts
|
|
1250
|
+
import path16 from "path";
|
|
1251
|
+
import fs14 from "fs-extra";
|
|
1252
|
+
async function detectProject(root) {
|
|
1253
|
+
const pkgPath = path16.join(root, "package.json");
|
|
1254
|
+
if (!await fs14.pathExists(pkgPath)) return null;
|
|
1255
|
+
const pkg = await readJsonSafe(pkgPath);
|
|
1256
|
+
if (!pkg) return null;
|
|
1257
|
+
const allDeps = {
|
|
1258
|
+
...pkg.dependencies,
|
|
1259
|
+
...pkg.devDependencies
|
|
1260
|
+
};
|
|
1261
|
+
const framework = detectFramework(allDeps);
|
|
1262
|
+
if (!framework) return null;
|
|
1263
|
+
return {
|
|
1264
|
+
name: pkg.name ?? path16.basename(root),
|
|
1265
|
+
framework,
|
|
1266
|
+
router: detectRouter(allDeps, framework),
|
|
1267
|
+
stateManagement: detectStateManagement(allDeps, framework),
|
|
1268
|
+
bundler: detectBundler(allDeps),
|
|
1269
|
+
hasTypeScript: "typescript" in allDeps || await fs14.pathExists(path16.join(root, "tsconfig.json"))
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
function detectFramework(deps) {
|
|
1273
|
+
if ("vue" in deps) return "vue";
|
|
1274
|
+
if ("react" in deps) return "react";
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
function detectRouter(deps, fw) {
|
|
1278
|
+
if (fw === "vue" && "vue-router" in deps) return "vue-router";
|
|
1279
|
+
if (fw === "react" && "react-router" in deps) return "react-router";
|
|
1280
|
+
if (fw === "react" && "react-router-dom" in deps) return "react-router";
|
|
1281
|
+
return void 0;
|
|
1282
|
+
}
|
|
1283
|
+
function detectStateManagement(deps, fw) {
|
|
1284
|
+
if (fw === "vue") {
|
|
1285
|
+
if ("pinia" in deps) return "pinia";
|
|
1286
|
+
if ("vuex" in deps) return "vuex";
|
|
1287
|
+
}
|
|
1288
|
+
if (fw === "react") {
|
|
1289
|
+
if ("@reduxjs/toolkit" in deps) return "redux-toolkit";
|
|
1290
|
+
if ("zustand" in deps) return "zustand";
|
|
1291
|
+
if ("mobx" in deps) return "mobx";
|
|
1292
|
+
}
|
|
1293
|
+
return void 0;
|
|
1294
|
+
}
|
|
1295
|
+
function detectBundler(deps) {
|
|
1296
|
+
if ("vite" in deps) return "vite";
|
|
1297
|
+
if ("webpack" in deps) return "webpack";
|
|
1298
|
+
if ("@rspack/cli" in deps || "@rspack/core" in deps) return "rspack";
|
|
1299
|
+
return void 0;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/core/detect-stack.ts
|
|
1303
|
+
import path17 from "path";
|
|
1304
|
+
import fs15 from "fs-extra";
|
|
1305
|
+
async function detectStack(root) {
|
|
1306
|
+
const pkgPath = path17.join(root, "package.json");
|
|
1307
|
+
if (!await fs15.pathExists(pkgPath)) return null;
|
|
1308
|
+
const pkg = await readJsonSafe(pkgPath);
|
|
1309
|
+
if (!pkg) return null;
|
|
1310
|
+
const allDeps = {
|
|
1311
|
+
...pkg.dependencies,
|
|
1312
|
+
...pkg.devDependencies
|
|
1313
|
+
};
|
|
1314
|
+
const framework = inferFramework(allDeps);
|
|
1315
|
+
const bundler = inferBundler(allDeps);
|
|
1316
|
+
const hasTypeScript = "typescript" in allDeps || await fs15.pathExists(path17.join(root, "tsconfig.json"));
|
|
1317
|
+
const hasGit = await fs15.pathExists(path17.join(root, ".git"));
|
|
1318
|
+
return {
|
|
1319
|
+
name: pkg.name ?? path17.basename(root),
|
|
1320
|
+
projectKind: inferProjectKind(pkg, allDeps, framework),
|
|
1321
|
+
framework,
|
|
1322
|
+
bundler,
|
|
1323
|
+
hasTypeScript,
|
|
1324
|
+
hasGit
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function inferFramework(deps) {
|
|
1328
|
+
if ("nuxt" in deps) return "nuxt";
|
|
1329
|
+
if ("next" in deps) return "next";
|
|
1330
|
+
if ("vue" in deps) return "vue";
|
|
1331
|
+
if ("react" in deps) return "react";
|
|
1332
|
+
return "unknown";
|
|
1333
|
+
}
|
|
1334
|
+
function inferBundler(deps) {
|
|
1335
|
+
if ("vite" in deps) return "vite";
|
|
1336
|
+
if ("webpack" in deps) return "webpack";
|
|
1337
|
+
if ("@rspack/cli" in deps || "@rspack/core" in deps) return "rspack";
|
|
1338
|
+
return void 0;
|
|
1339
|
+
}
|
|
1340
|
+
function inferProjectKind(pkg, deps, framework) {
|
|
1341
|
+
if (framework !== "unknown") return "frontend";
|
|
1342
|
+
const hasNodeSignals = pkg.bin !== void 0 || pkg.main !== void 0 || pkg.exports !== void 0 || "express" in deps || "fastify" in deps || "koa" in deps || "@types/node" in deps;
|
|
1343
|
+
if (hasNodeSignals) return "node";
|
|
1344
|
+
return "unknown";
|
|
1345
|
+
}
|
|
1346
|
+
function summarizeStack(stack) {
|
|
1347
|
+
const parts = [];
|
|
1348
|
+
if (stack.framework !== "unknown") {
|
|
1349
|
+
parts.push(stack.framework.charAt(0).toUpperCase() + stack.framework.slice(1));
|
|
1350
|
+
} else if (stack.projectKind === "node") {
|
|
1351
|
+
parts.push("Node");
|
|
1352
|
+
} else {
|
|
1353
|
+
parts.push("JavaScript/TypeScript");
|
|
1354
|
+
}
|
|
1355
|
+
if (stack.bundler) {
|
|
1356
|
+
parts.push(stack.bundler.charAt(0).toUpperCase() + stack.bundler.slice(1));
|
|
1357
|
+
}
|
|
1358
|
+
if (stack.hasTypeScript) parts.push("TypeScript");
|
|
1359
|
+
return parts.join(" + ");
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/prompts/enhance-prompts.ts
|
|
1363
|
+
import prompts2 from "prompts";
|
|
1364
|
+
function getApplicableTools(stack) {
|
|
1365
|
+
const tools = [];
|
|
1366
|
+
tools.push({
|
|
1367
|
+
id: "eslint",
|
|
1368
|
+
label: "ESLint",
|
|
1369
|
+
description: "Linting for JavaScript / TypeScript",
|
|
1370
|
+
selected: true
|
|
1371
|
+
});
|
|
1372
|
+
tools.push({
|
|
1373
|
+
id: "prettier",
|
|
1374
|
+
label: "Prettier",
|
|
1375
|
+
description: "Code formatting",
|
|
1376
|
+
selected: true
|
|
1377
|
+
});
|
|
1378
|
+
tools.push({
|
|
1379
|
+
id: "editorconfig",
|
|
1380
|
+
label: "EditorConfig",
|
|
1381
|
+
description: "Cross-editor indentation & whitespace settings",
|
|
1382
|
+
selected: true
|
|
1383
|
+
});
|
|
1384
|
+
if (stack.projectKind === "frontend") {
|
|
1385
|
+
tools.push({
|
|
1386
|
+
id: "stylelint",
|
|
1387
|
+
label: "Stylelint",
|
|
1388
|
+
description: "CSS / SCSS linting",
|
|
1389
|
+
selected: false
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
tools.push({
|
|
1393
|
+
id: "commitlint",
|
|
1394
|
+
label: "commitlint",
|
|
1395
|
+
description: "Enforce conventional commit messages",
|
|
1396
|
+
selected: false
|
|
1397
|
+
});
|
|
1398
|
+
return tools;
|
|
1399
|
+
}
|
|
1400
|
+
async function runEnhancePrompts(stack) {
|
|
1401
|
+
const stackLabel = summarizeStack(stack);
|
|
1402
|
+
const applicableTools = getApplicableTools(stack);
|
|
1403
|
+
const response = await prompts2(
|
|
1404
|
+
[
|
|
1405
|
+
{
|
|
1406
|
+
type: "multiselect",
|
|
1407
|
+
name: "devTools",
|
|
1408
|
+
message: `Dev tools to configure for this ${stackLabel} project:`,
|
|
1409
|
+
choices: DEV_TOOLS.map((t) => ({ title: t, value: t, selected: t === "cursor" })),
|
|
1410
|
+
min: 1,
|
|
1411
|
+
hint: "Select at least one"
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
type: "multiselect",
|
|
1415
|
+
name: "qualityTools",
|
|
1416
|
+
message: "Code quality tools to install & configure:",
|
|
1417
|
+
choices: applicableTools.map((t) => ({
|
|
1418
|
+
title: `${t.label} \u2014 ${t.description}`,
|
|
1419
|
+
value: t.id,
|
|
1420
|
+
selected: t.selected
|
|
1421
|
+
})),
|
|
1422
|
+
hint: "Space to toggle, Enter to confirm"
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
type: "multiselect",
|
|
1426
|
+
name: "skills",
|
|
1427
|
+
message: "Skills to add:",
|
|
1428
|
+
choices: getSkillCatalog().map((s) => ({
|
|
1429
|
+
title: `${s.label} \u2014 ${s.description}`,
|
|
1430
|
+
value: s.id,
|
|
1431
|
+
selected: false
|
|
1432
|
+
}))
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
type: "multiselect",
|
|
1436
|
+
name: "mcpServers",
|
|
1437
|
+
message: "MCP servers to add:",
|
|
1438
|
+
choices: getMcpCatalog().map((m) => ({
|
|
1439
|
+
title: `${m.label} \u2014 ${m.description}`,
|
|
1440
|
+
value: m.id,
|
|
1441
|
+
selected: false
|
|
1442
|
+
}))
|
|
1443
|
+
}
|
|
1444
|
+
],
|
|
1445
|
+
{ onCancel: () => process.exit(0) }
|
|
1446
|
+
);
|
|
1447
|
+
if (!response.devTools) return null;
|
|
1448
|
+
return {
|
|
1449
|
+
devTools: response.devTools,
|
|
1450
|
+
qualityTools: response.qualityTools ?? [],
|
|
1451
|
+
skills: response.skills ?? [],
|
|
1452
|
+
mcpServers: response.mcpServers ?? []
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/generators/quality-generator.ts
|
|
1457
|
+
import path18 from "path";
|
|
1458
|
+
import fs16 from "fs-extra";
|
|
1459
|
+
var GENERATORS2 = {
|
|
1460
|
+
eslint: generateEslint2,
|
|
1461
|
+
stylelint: generateStylelint2,
|
|
1462
|
+
prettier: generatePrettier2,
|
|
1463
|
+
editorconfig: generateEditorConfig2,
|
|
1464
|
+
commitlint: generateCommitlint
|
|
1465
|
+
};
|
|
1466
|
+
async function generateQualityTooling(root, stack, tools) {
|
|
1467
|
+
for (const tool of tools) {
|
|
1468
|
+
await GENERATORS2[tool](root, stack);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
async function generateEslint2(root, stack) {
|
|
1472
|
+
const pkg = await readPkg(root);
|
|
1473
|
+
const devDeps = {
|
|
1474
|
+
...pkg.devDependencies
|
|
1475
|
+
};
|
|
1476
|
+
devDeps.eslint = "^9.25.0";
|
|
1477
|
+
devDeps["@eslint/js"] = "^9.25.0";
|
|
1478
|
+
devDeps.globals = "^16.0.0";
|
|
1479
|
+
if (stack.hasTypeScript) {
|
|
1480
|
+
devDeps["typescript-eslint"] = "^8.30.0";
|
|
1481
|
+
}
|
|
1482
|
+
addFrameworkEslintDeps(devDeps, stack.framework);
|
|
1483
|
+
addBundlerEslintPlugin(devDeps, stack);
|
|
1484
|
+
pkg.devDependencies = devDeps;
|
|
1485
|
+
await writePkg(root, pkg);
|
|
1486
|
+
const configContent = buildEslintConfig(stack);
|
|
1487
|
+
await fs16.writeFile(path18.join(root, "eslint.config.mjs"), configContent, "utf-8");
|
|
1488
|
+
logger.success("ESLint 9 flat config generated");
|
|
1489
|
+
}
|
|
1490
|
+
function addFrameworkEslintDeps(devDeps, fw) {
|
|
1491
|
+
if (fw === "vue" || fw === "nuxt") {
|
|
1492
|
+
devDeps["eslint-plugin-vue"] = "^10.0.0";
|
|
1493
|
+
} else if (fw === "react" || fw === "next") {
|
|
1494
|
+
devDeps["eslint-plugin-react-hooks"] = "^5.2.0";
|
|
1495
|
+
devDeps["eslint-plugin-react-refresh"] = "^0.4.0";
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
function addBundlerEslintPlugin(devDeps, stack) {
|
|
1499
|
+
if (stack.projectKind !== "frontend") return;
|
|
1500
|
+
if (stack.bundler === "vite") {
|
|
1501
|
+
devDeps["vite-plugin-eslint2"] = "^5.0.0";
|
|
1502
|
+
} else if (stack.bundler === "webpack" || stack.bundler === "rspack") {
|
|
1503
|
+
devDeps["eslint-webpack-plugin"] = "^4.2.0";
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
function buildEslintConfig(stack) {
|
|
1507
|
+
const fw = stack.framework;
|
|
1508
|
+
if (fw === "vue" || fw === "nuxt") return buildVueEslintConfig(stack);
|
|
1509
|
+
if (fw === "react" || fw === "next") return buildReactEslintConfig(stack);
|
|
1510
|
+
return buildGenericEslintConfig(stack);
|
|
1511
|
+
}
|
|
1512
|
+
function buildVueEslintConfig(stack) {
|
|
1513
|
+
const lines = [
|
|
1514
|
+
"import js from '@eslint/js';",
|
|
1515
|
+
...stack.hasTypeScript ? ["import tseslint from 'typescript-eslint';"] : [],
|
|
1516
|
+
"import pluginVue from 'eslint-plugin-vue';",
|
|
1517
|
+
"",
|
|
1518
|
+
...stack.hasTypeScript ? [
|
|
1519
|
+
"export default tseslint.config(",
|
|
1520
|
+
" js.configs.recommended,",
|
|
1521
|
+
" ...tseslint.configs.recommended,"
|
|
1522
|
+
] : ["export default [", " js.configs.recommended,"],
|
|
1523
|
+
" ...pluginVue.configs['flat/recommended'],",
|
|
1524
|
+
...stack.hasTypeScript ? [
|
|
1525
|
+
" {",
|
|
1526
|
+
" files: ['**/*.vue'],",
|
|
1527
|
+
" languageOptions: {",
|
|
1528
|
+
" parserOptions: { parser: tseslint.parser },",
|
|
1529
|
+
" },",
|
|
1530
|
+
" },",
|
|
1531
|
+
");"
|
|
1532
|
+
] : ["];"]
|
|
1533
|
+
];
|
|
1534
|
+
return lines.join("\n");
|
|
1535
|
+
}
|
|
1536
|
+
function buildReactEslintConfig(stack) {
|
|
1537
|
+
const lines = [
|
|
1538
|
+
"import js from '@eslint/js';",
|
|
1539
|
+
...stack.hasTypeScript ? ["import tseslint from 'typescript-eslint';"] : [],
|
|
1540
|
+
"import reactHooks from 'eslint-plugin-react-hooks';",
|
|
1541
|
+
"import reactRefresh from 'eslint-plugin-react-refresh';",
|
|
1542
|
+
"import globals from 'globals';",
|
|
1543
|
+
"",
|
|
1544
|
+
...stack.hasTypeScript ? [
|
|
1545
|
+
"export default tseslint.config(",
|
|
1546
|
+
" js.configs.recommended,",
|
|
1547
|
+
" ...tseslint.configs.recommended,"
|
|
1548
|
+
] : ["export default [", " js.configs.recommended,"],
|
|
1549
|
+
" {",
|
|
1550
|
+
" files: ['**/*.{ts,tsx,js,jsx}'],",
|
|
1551
|
+
" plugins: {",
|
|
1552
|
+
" 'react-hooks': reactHooks,",
|
|
1553
|
+
" 'react-refresh': reactRefresh,",
|
|
1554
|
+
" },",
|
|
1555
|
+
" languageOptions: {",
|
|
1556
|
+
" globals: globals.browser,",
|
|
1557
|
+
" },",
|
|
1558
|
+
" rules: {",
|
|
1559
|
+
" ...reactHooks.configs.recommended.rules,",
|
|
1560
|
+
" 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],",
|
|
1561
|
+
" },",
|
|
1562
|
+
" },",
|
|
1563
|
+
...stack.hasTypeScript ? [");"] : ["];"]
|
|
1564
|
+
];
|
|
1565
|
+
return lines.join("\n");
|
|
1566
|
+
}
|
|
1567
|
+
function buildGenericEslintConfig(stack) {
|
|
1568
|
+
const lines = [
|
|
1569
|
+
"import js from '@eslint/js';",
|
|
1570
|
+
...stack.hasTypeScript ? ["import tseslint from 'typescript-eslint';"] : [],
|
|
1571
|
+
"import globals from 'globals';",
|
|
1572
|
+
"",
|
|
1573
|
+
...stack.hasTypeScript ? [
|
|
1574
|
+
"export default tseslint.config(",
|
|
1575
|
+
" js.configs.recommended,",
|
|
1576
|
+
" ...tseslint.configs.recommended,"
|
|
1577
|
+
] : ["export default [", " js.configs.recommended,"],
|
|
1578
|
+
" {",
|
|
1579
|
+
" files: ['**/*.{ts,js}'],",
|
|
1580
|
+
" languageOptions: {",
|
|
1581
|
+
" globals: {",
|
|
1582
|
+
" ...globals.node,",
|
|
1583
|
+
" },",
|
|
1584
|
+
" },",
|
|
1585
|
+
" },",
|
|
1586
|
+
...stack.hasTypeScript ? [");"] : ["];"]
|
|
1587
|
+
];
|
|
1588
|
+
return lines.join("\n");
|
|
1589
|
+
}
|
|
1590
|
+
async function generateStylelint2(root, stack) {
|
|
1591
|
+
const pkg = await readPkg(root);
|
|
1592
|
+
const devDeps = {
|
|
1593
|
+
...pkg.devDependencies,
|
|
1594
|
+
stylelint: "^16.17.0",
|
|
1595
|
+
"stylelint-config-standard": "^37.0.0"
|
|
1596
|
+
};
|
|
1597
|
+
const fw = stack.framework;
|
|
1598
|
+
if (fw === "vue" || fw === "nuxt") {
|
|
1599
|
+
devDeps["stylelint-config-standard-vue"] = "^1.0.0";
|
|
1600
|
+
}
|
|
1601
|
+
pkg.devDependencies = devDeps;
|
|
1602
|
+
await writePkg(root, pkg);
|
|
1603
|
+
const extendsArr = ["stylelint-config-standard"];
|
|
1604
|
+
if (fw === "vue" || fw === "nuxt") {
|
|
1605
|
+
extendsArr.push("stylelint-config-standard-vue");
|
|
1606
|
+
}
|
|
1607
|
+
await fs16.writeJson(
|
|
1608
|
+
path18.join(root, ".stylelintrc.json"),
|
|
1609
|
+
{ extends: extendsArr },
|
|
1610
|
+
{ spaces: 2 }
|
|
1611
|
+
);
|
|
1612
|
+
logger.success("Stylelint config generated");
|
|
1613
|
+
}
|
|
1614
|
+
async function generatePrettier2(root) {
|
|
1615
|
+
const pkg = await readPkg(root);
|
|
1616
|
+
pkg.devDependencies = {
|
|
1617
|
+
...pkg.devDependencies,
|
|
1618
|
+
prettier: "^3.5.0"
|
|
1619
|
+
};
|
|
1620
|
+
await writePkg(root, pkg);
|
|
1621
|
+
const config = {
|
|
1622
|
+
semi: true,
|
|
1623
|
+
singleQuote: true,
|
|
1624
|
+
tabWidth: 2,
|
|
1625
|
+
trailingComma: "all",
|
|
1626
|
+
printWidth: 100
|
|
1627
|
+
};
|
|
1628
|
+
await fs16.writeJson(path18.join(root, ".prettierrc.json"), config, { spaces: 2 });
|
|
1629
|
+
logger.success("Prettier config generated");
|
|
1630
|
+
}
|
|
1631
|
+
async function generateEditorConfig2(root) {
|
|
1632
|
+
const content = [
|
|
1633
|
+
"root = true",
|
|
1634
|
+
"",
|
|
1635
|
+
"[*]",
|
|
1636
|
+
"indent_style = space",
|
|
1637
|
+
"indent_size = 2",
|
|
1638
|
+
"end_of_line = lf",
|
|
1639
|
+
"charset = utf-8",
|
|
1640
|
+
"trim_trailing_whitespace = true",
|
|
1641
|
+
"insert_final_newline = true"
|
|
1642
|
+
].join("\n");
|
|
1643
|
+
await fs16.writeFile(path18.join(root, ".editorconfig"), content, "utf-8");
|
|
1644
|
+
logger.success("EditorConfig generated");
|
|
1645
|
+
}
|
|
1646
|
+
async function generateCommitlint(root, stack) {
|
|
1647
|
+
const pkg = await readPkg(root);
|
|
1648
|
+
const devDeps = {
|
|
1649
|
+
...pkg.devDependencies,
|
|
1650
|
+
"@commitlint/cli": "^19.8.0",
|
|
1651
|
+
"@commitlint/config-conventional": "^19.8.0"
|
|
1652
|
+
};
|
|
1653
|
+
devDeps.husky = "^9.1.0";
|
|
1654
|
+
pkg.devDependencies = devDeps;
|
|
1655
|
+
const scripts = pkg.scripts ?? {};
|
|
1656
|
+
scripts.prepare ??= "husky";
|
|
1657
|
+
pkg.scripts = scripts;
|
|
1658
|
+
await writePkg(root, pkg);
|
|
1659
|
+
const isESM = pkg.type === "module";
|
|
1660
|
+
const configName = isESM ? "commitlint.config.cjs" : "commitlint.config.js";
|
|
1661
|
+
const configContent = [
|
|
1662
|
+
"module.exports = { extends: ['@commitlint/config-conventional'] };",
|
|
1663
|
+
""
|
|
1664
|
+
].join("\n");
|
|
1665
|
+
await fs16.writeFile(path18.join(root, configName), configContent, "utf-8");
|
|
1666
|
+
await fs16.ensureDir(path18.join(root, ".husky"));
|
|
1667
|
+
const hookContent = 'npx --no -- commitlint --edit "$1"\n';
|
|
1668
|
+
await fs16.writeFile(path18.join(root, ".husky", "commit-msg"), hookContent, "utf-8");
|
|
1669
|
+
if (!stack.hasGit) {
|
|
1670
|
+
logger.warn("No .git directory detected \u2014 commitlint hooks will activate after git init.");
|
|
1671
|
+
}
|
|
1672
|
+
logger.success("commitlint + husky generated");
|
|
1673
|
+
}
|
|
1674
|
+
async function readPkg(root) {
|
|
1675
|
+
return fs16.readJson(path18.join(root, "package.json"));
|
|
1676
|
+
}
|
|
1677
|
+
async function writePkg(root, data) {
|
|
1678
|
+
await fs16.writeJson(path18.join(root, "package.json"), data, { spaces: 2 });
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/commands/enhance.ts
|
|
1682
|
+
async function enhanceCommand() {
|
|
1683
|
+
logger.step("Analyzing current project...");
|
|
1684
|
+
const projectRoot = process.cwd();
|
|
1685
|
+
const classicDetection = await detectProject(projectRoot);
|
|
1686
|
+
const stack = classicDetection ? await toStackDetection(projectRoot, classicDetection) : await detectStack(projectRoot);
|
|
1687
|
+
if (!stack) {
|
|
1688
|
+
logger.error(
|
|
1689
|
+
"Could not detect a project in the current directory. Make sure package.json exists."
|
|
1690
|
+
);
|
|
1691
|
+
process.exitCode = 1;
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
logger.info(`Detected: ${summarizeStack(stack)}`);
|
|
1695
|
+
const answers = await runEnhancePrompts(stack);
|
|
1696
|
+
if (!answers) return;
|
|
1697
|
+
await updateFeKitMeta(projectRoot, classicDetection ?? stack, answers);
|
|
1698
|
+
if (answers.qualityTools.length > 0) {
|
|
1699
|
+
await generateQualityTooling(projectRoot, stack, answers.qualityTools);
|
|
1700
|
+
}
|
|
1701
|
+
await applyAdapters(projectRoot, {
|
|
1702
|
+
framework: stack.framework,
|
|
1703
|
+
...answers
|
|
1704
|
+
});
|
|
1705
|
+
logger.success("Project enhanced successfully.");
|
|
1706
|
+
}
|
|
1707
|
+
async function toStackDetection(root, d) {
|
|
1708
|
+
return {
|
|
1709
|
+
name: d.name,
|
|
1710
|
+
projectKind: "frontend",
|
|
1711
|
+
framework: d.framework,
|
|
1712
|
+
bundler: d.bundler,
|
|
1713
|
+
hasTypeScript: d.hasTypeScript,
|
|
1714
|
+
hasGit: await fs17.pathExists(path19.join(root, ".git"))
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// src/cli.ts
|
|
1719
|
+
var program = new Command();
|
|
1720
|
+
program.name(CLI_NAME).description("Frontend project scaffold CLI \u2014 init, enhance, and manage your dev environment.").version(CLI_VERSION);
|
|
1721
|
+
program.command("init").description("Initialize a new frontend project with Vue or React + TypeScript").action(async () => {
|
|
1722
|
+
try {
|
|
1723
|
+
await initCommand();
|
|
1724
|
+
} catch (err) {
|
|
1725
|
+
console.error(err);
|
|
1726
|
+
process.exitCode = 1;
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
program.command("enhance").description("Enhance an existing frontend project with dev tools, skills, MCP, and rules").action(async () => {
|
|
1730
|
+
try {
|
|
1731
|
+
await enhanceCommand();
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
console.error(err);
|
|
1734
|
+
process.exitCode = 1;
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
program.parse();
|
|
1738
|
+
//# sourceMappingURL=cli.mjs.map
|