@dianzhong/create-harness-app 0.1.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/dist/index.mjs +412 -0
- package/package.json +29 -0
- package/templates/axios/.env.example +2 -0
- package/templates/axios/src/api/auth.ts +19 -0
- package/templates/axios/src/api/request.ts +61 -0
- package/templates/axios/src/types/api.ts +26 -0
- package/templates/axios/src/utils/auth.ts +5 -0
- package/templates/axios/src/utils/storage.ts +17 -0
- package/templates/harness/full/.agents/skills/find-skills/SKILL.md +143 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/LICENSE.md +21 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/SKILL.md +155 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/SYNC.md +5 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/animation-class-based-technique.md +258 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/animation-state-driven-technique.md +287 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-async.md +99 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-data-flow.md +313 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-fallthrough-attrs.md +179 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-keep-alive.md +139 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-slots.md +226 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-suspense.md +231 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-teleport.md +110 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-transition-group.md +131 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-transition.md +135 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/composables.md +303 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/directives.md +168 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +177 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +185 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-virtualize-large-lists.md +182 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/plugins.md +178 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/reactivity.md +371 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/render-functions.md +227 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/sfc.md +355 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/state-management.md +138 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/updated-hook-performance.md +193 -0
- package/templates/harness/full/.claude/agents/code-reviewer.md +109 -0
- package/templates/harness/full/.claude/agents/harness-reviewer.md +51 -0
- package/templates/harness/full/.claude/hooks/guard-tool.cjs +234 -0
- package/templates/harness/full/.claude/hooks/notify.cjs +168 -0
- package/templates/harness/full/.claude/hooks/quality-gate.cjs +135 -0
- package/templates/harness/full/.claude/rules/delivery.md +66 -0
- package/templates/harness/full/.claude/rules/formatting.md +7 -0
- package/templates/harness/full/.claude/rules/git.md +8 -0
- package/templates/harness/full/.claude/rules/skills-mcp.md +13 -0
- package/templates/harness/full/.claude/rules/vue.md +227 -0
- package/templates/harness/full/.claude/settings.json +123 -0
- package/templates/harness/full/.claude/skills/find-skills/SKILL.md +143 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/LICENSE.md +21 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/SKILL.md +155 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/SYNC.md +5 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/animation-class-based-technique.md +258 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/animation-state-driven-technique.md +287 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-async.md +99 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-data-flow.md +313 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-fallthrough-attrs.md +179 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-keep-alive.md +139 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-slots.md +226 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-suspense.md +231 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-teleport.md +110 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-transition-group.md +131 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-transition.md +135 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/composables.md +303 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/directives.md +168 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +177 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +185 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/perf-virtualize-large-lists.md +182 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/plugins.md +178 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/reactivity.md +371 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/render-functions.md +227 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/sfc.md +355 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/state-management.md +138 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/updated-hook-performance.md +193 -0
- package/templates/harness/full/.editorconfig +8 -0
- package/templates/harness/full/.husky/commit-msg +1 -0
- package/templates/harness/full/.husky/pre-commit +1 -0
- package/templates/harness/full/.lintstagedrc.json +4 -0
- package/templates/harness/full/.nvmrc +1 -0
- package/templates/harness/full/.oxlintrc.json +11 -0
- package/templates/harness/full/.prettierrc.json +6 -0
- package/templates/harness/full/AGENTS.md +3 -0
- package/templates/harness/full/CLAUDE.md +28 -0
- package/templates/harness/full/GEMINI.md +3 -0
- package/templates/harness/full/commitlint.config.ts +3 -0
- package/templates/harness/full/docs/ai-harness.md +77 -0
- package/templates/harness/full/docs/delivery-template.md +66 -0
- package/templates/harness/full/docs/git.md +24 -0
- package/templates/harness/full/docs/harness-quick-reference.md +89 -0
- package/templates/harness/full/docs/review-checklist.md +49 -0
- package/templates/harness/full/scripts/harness-hooks.test.mjs +218 -0
- package/templates/harness/full/scripts/verify-skills.mjs +248 -0
- package/templates/harness/full/scripts/verify-skills.test.mjs +72 -0
- package/templates/harness/full/skills-lock.json +50 -0
- package/templates/harness/minimal/.claude/hooks/guard-tool.cjs +234 -0
- package/templates/harness/minimal/.claude/hooks/quality-gate.cjs +135 -0
- package/templates/harness/minimal/.claude/settings.json +27 -0
- package/templates/harness/minimal/CLAUDE.md +12 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import path7 from "path";
|
|
5
|
+
import fse2 from "fs-extra";
|
|
6
|
+
import { outro } from "@clack/prompts";
|
|
7
|
+
|
|
8
|
+
// src/prompts.ts
|
|
9
|
+
import { intro, text, select, confirm, isCancel, cancel } from "@clack/prompts";
|
|
10
|
+
function defaultConfig(projectName) {
|
|
11
|
+
return {
|
|
12
|
+
projectName,
|
|
13
|
+
router: true,
|
|
14
|
+
pinia: true,
|
|
15
|
+
vitest: false,
|
|
16
|
+
uiLibrary: "element-plus",
|
|
17
|
+
axios: true,
|
|
18
|
+
harness: "full"
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function check(val) {
|
|
22
|
+
if (isCancel(val)) {
|
|
23
|
+
cancel("\u5DF2\u53D6\u6D88");
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
return val;
|
|
27
|
+
}
|
|
28
|
+
async function collectConfig(initialName) {
|
|
29
|
+
intro("create-harness-app \u2014 \u70B9\u4F17\u524D\u7AEF\u9879\u76EE\u521D\u59CB\u5316\u5668");
|
|
30
|
+
const projectName = initialName ?? check(await text({
|
|
31
|
+
message: "\u9879\u76EE\u540D\u79F0",
|
|
32
|
+
placeholder: "my-app",
|
|
33
|
+
validate: (v) => /^[a-z][a-z0-9-]*$/.test(v) ? void 0 : "\u8BF7\u4F7F\u7528\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u8FDE\u5B57\u7B26"
|
|
34
|
+
}));
|
|
35
|
+
const router = check(await confirm({ message: "\u542F\u7528 Vue Router\uFF1F", initialValue: true }));
|
|
36
|
+
const pinia = check(await confirm({ message: "\u542F\u7528 Pinia \u72B6\u6001\u7BA1\u7406\uFF1F", initialValue: true }));
|
|
37
|
+
const vitest = check(await confirm({ message: "\u542F\u7528 Vitest \u5355\u5143\u6D4B\u8BD5\uFF1F", initialValue: false }));
|
|
38
|
+
const uiLibrary = check(await select({
|
|
39
|
+
message: "UI \u7EC4\u4EF6\u5E93",
|
|
40
|
+
options: [
|
|
41
|
+
{ value: "element-plus", label: "Element Plus\uFF08\u9ED8\u8BA4\uFF09" },
|
|
42
|
+
{ value: "ant-design-vue", label: "Ant Design Vue" },
|
|
43
|
+
{ value: "none", label: "\u81EA\u5DF1\u914D\u7F6E" }
|
|
44
|
+
]
|
|
45
|
+
}));
|
|
46
|
+
const axios = check(await confirm({ message: "\u96C6\u6210 axios \u8BF7\u6C42\u5C42\uFF1F", initialValue: true }));
|
|
47
|
+
const harness = check(await select({
|
|
48
|
+
message: "Harness \u6CBB\u7406\u5C42",
|
|
49
|
+
options: [
|
|
50
|
+
{ value: "full", label: "Full \u2014 \u5B8C\u6574 hooks/rules/agents/skills\uFF08\u9ED8\u8BA4\uFF09" },
|
|
51
|
+
{ value: "minimal", label: "Minimal \u2014 \u4EC5\u5B89\u5168\u5B88\u62A4 + \u8D28\u91CF\u95E8\u7981" },
|
|
52
|
+
{ value: "none", label: "\u4E0D\u96C6\u6210" }
|
|
53
|
+
]
|
|
54
|
+
}));
|
|
55
|
+
return { projectName, router, pinia, vitest, uiLibrary, axios, harness };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/spawn-create-vue.ts
|
|
59
|
+
import { execa } from "execa";
|
|
60
|
+
async function spawnCreateVue(config, parentDir) {
|
|
61
|
+
const flags = [
|
|
62
|
+
"--typescript",
|
|
63
|
+
"--eslint-with-prettier",
|
|
64
|
+
"--force",
|
|
65
|
+
...config.router ? ["--router"] : [],
|
|
66
|
+
...config.pinia ? ["--pinia"] : [],
|
|
67
|
+
...config.vitest ? ["--vitest"] : []
|
|
68
|
+
];
|
|
69
|
+
await execa("npx", ["--yes", "create-vue@latest", config.projectName, ...flags], {
|
|
70
|
+
cwd: parentDir,
|
|
71
|
+
stdio: "inherit"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/overlay.ts
|
|
76
|
+
import path6 from "path";
|
|
77
|
+
|
|
78
|
+
// src/utils/fs.ts
|
|
79
|
+
import fse from "fs-extra";
|
|
80
|
+
import path from "path";
|
|
81
|
+
async function copyDir(src, dest) {
|
|
82
|
+
await fse.ensureDir(dest);
|
|
83
|
+
await fse.copy(src, dest, { overwrite: true });
|
|
84
|
+
}
|
|
85
|
+
async function readText(p) {
|
|
86
|
+
return fse.readFile(p, "utf-8");
|
|
87
|
+
}
|
|
88
|
+
async function writeText(p, content) {
|
|
89
|
+
await fse.ensureDir(path.dirname(p));
|
|
90
|
+
return fse.writeFile(p, content, "utf-8");
|
|
91
|
+
}
|
|
92
|
+
async function readJson(p) {
|
|
93
|
+
return fse.readJson(p);
|
|
94
|
+
}
|
|
95
|
+
async function writeJson(p, data) {
|
|
96
|
+
await fse.ensureDir(path.dirname(p));
|
|
97
|
+
return fse.writeJson(p, data, { spaces: 2 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/merge-json.ts
|
|
101
|
+
function mergePackageJson(existing, additions) {
|
|
102
|
+
const result = { ...existing };
|
|
103
|
+
if (additions.dependencies) {
|
|
104
|
+
result.dependencies = mergeDeps(
|
|
105
|
+
existing.dependencies ?? {},
|
|
106
|
+
additions.dependencies
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (additions.devDependencies) {
|
|
110
|
+
result.devDependencies = mergeDeps(
|
|
111
|
+
existing.devDependencies ?? {},
|
|
112
|
+
additions.devDependencies
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (additions.scripts) {
|
|
116
|
+
result.scripts = {
|
|
117
|
+
...existing.scripts ?? {},
|
|
118
|
+
...additions.scripts
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function mergeDeps(existing, additions) {
|
|
124
|
+
const result = { ...existing };
|
|
125
|
+
for (const [pkg, version] of Object.entries(additions)) {
|
|
126
|
+
if (!(pkg in result)) result[pkg] = version;
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/features/axios.ts
|
|
132
|
+
import path2 from "path";
|
|
133
|
+
import { fileURLToPath } from "url";
|
|
134
|
+
var TEMPLATES_DIR = fileURLToPath(new URL("../templates", import.meta.url));
|
|
135
|
+
var AXIOS_PKG = {
|
|
136
|
+
dependencies: { axios: "^1.16.0" }
|
|
137
|
+
};
|
|
138
|
+
var SHOW_ERROR = {
|
|
139
|
+
"element-plus": `import { ElMessage } from 'element-plus'
|
|
140
|
+
|
|
141
|
+
export function showError(msg: string): void {
|
|
142
|
+
ElMessage.error(msg)
|
|
143
|
+
}
|
|
144
|
+
`,
|
|
145
|
+
"ant-design-vue": `import { message } from 'ant-design-vue'
|
|
146
|
+
|
|
147
|
+
export function showError(msg: string): void {
|
|
148
|
+
message.error(msg)
|
|
149
|
+
}
|
|
150
|
+
`,
|
|
151
|
+
"none": `// TODO: Replace with your UI library's notification component
|
|
152
|
+
export function showError(msg: string): void {
|
|
153
|
+
console.error('[API Error]', msg)
|
|
154
|
+
}
|
|
155
|
+
`
|
|
156
|
+
};
|
|
157
|
+
async function overlayAxios(projectRoot, config) {
|
|
158
|
+
await copyDir(path2.join(TEMPLATES_DIR, "axios"), projectRoot);
|
|
159
|
+
await writeText(
|
|
160
|
+
path2.join(projectRoot, "src/utils/show-error.ts"),
|
|
161
|
+
SHOW_ERROR[config.uiLibrary]
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/features/ui-element-plus.ts
|
|
166
|
+
import path3 from "path";
|
|
167
|
+
|
|
168
|
+
// src/inject.ts
|
|
169
|
+
var AnchorNotFoundError = class extends Error {
|
|
170
|
+
constructor(anchor, file) {
|
|
171
|
+
super(`Anchor not found in "${file}": ${JSON.stringify(anchor)}`);
|
|
172
|
+
this.name = "AnchorNotFoundError";
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
function insertAfter(source, anchor, insertion, filePath = "<unknown>") {
|
|
176
|
+
const idx = source.indexOf(anchor);
|
|
177
|
+
if (idx === -1) throw new AnchorNotFoundError(anchor, filePath);
|
|
178
|
+
const end = idx + anchor.length;
|
|
179
|
+
return source.slice(0, end) + insertion + source.slice(end);
|
|
180
|
+
}
|
|
181
|
+
function insertBefore(source, anchor, insertion, filePath = "<unknown>") {
|
|
182
|
+
const idx = source.indexOf(anchor);
|
|
183
|
+
if (idx === -1) throw new AnchorNotFoundError(anchor, filePath);
|
|
184
|
+
return source.slice(0, idx) + insertion + source.slice(idx);
|
|
185
|
+
}
|
|
186
|
+
function hasMarker(source, marker) {
|
|
187
|
+
return source.includes(marker);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/features/ui-element-plus.ts
|
|
191
|
+
var EP_MARKER = "// ELEMENT_PLUS_INJECTED";
|
|
192
|
+
var VITE_IMPORTS = `import AutoImport from 'unplugin-auto-import/vite'
|
|
193
|
+
import Components from 'unplugin-vue-components/vite'
|
|
194
|
+
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
|
195
|
+
`;
|
|
196
|
+
var VITE_PLUGINS = `
|
|
197
|
+
AutoImport({ resolvers: [ElementPlusResolver()] }),
|
|
198
|
+
Components({ resolvers: [ElementPlusResolver()] }),`;
|
|
199
|
+
var MAIN_IMPORTS = `import ElementPlus from 'element-plus'
|
|
200
|
+
import 'element-plus/dist/index.css'
|
|
201
|
+
import './styles/element-plus.scss'
|
|
202
|
+
`;
|
|
203
|
+
var SCSS_CONTENT = `/* Element Plus \u4E3B\u9898\u8986\u76D6
|
|
204
|
+
* \u6587\u6863: https://element-plus.org/en-US/guide/theming.html
|
|
205
|
+
*/
|
|
206
|
+
`;
|
|
207
|
+
var ELEMENT_PLUS_PKG = {
|
|
208
|
+
dependencies: {
|
|
209
|
+
"element-plus": "^2.13.7",
|
|
210
|
+
"@element-plus/icons-vue": "^2.3.2"
|
|
211
|
+
},
|
|
212
|
+
devDependencies: {
|
|
213
|
+
"unplugin-auto-import": "^21.0.0",
|
|
214
|
+
"unplugin-vue-components": "^32.0.0",
|
|
215
|
+
"sass": "^1.95.1"
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
async function overlayElementPlus(projectRoot) {
|
|
219
|
+
await writeText(path3.join(projectRoot, "src/styles/element-plus.scss"), SCSS_CONTENT);
|
|
220
|
+
const mainPath = path3.join(projectRoot, "src/main.ts");
|
|
221
|
+
let main2 = await readText(mainPath);
|
|
222
|
+
if (!hasMarker(main2, EP_MARKER)) {
|
|
223
|
+
main2 = insertBefore(main2, "import { createApp }", `${EP_MARKER}
|
|
224
|
+
${MAIN_IMPORTS}`, mainPath);
|
|
225
|
+
main2 = insertAfter(main2, "const app = createApp(App)", `
|
|
226
|
+
app.use(ElementPlus)`, mainPath);
|
|
227
|
+
await writeText(mainPath, main2);
|
|
228
|
+
}
|
|
229
|
+
const vitePath = path3.join(projectRoot, "vite.config.ts");
|
|
230
|
+
let vite = await readText(vitePath);
|
|
231
|
+
if (!hasMarker(vite, EP_MARKER)) {
|
|
232
|
+
vite = insertBefore(vite, "export default defineConfig(", `${EP_MARKER}
|
|
233
|
+
${VITE_IMPORTS}`, vitePath);
|
|
234
|
+
vite = insertAfter(vite, "plugins: [", VITE_PLUGINS, vitePath);
|
|
235
|
+
await writeText(vitePath, vite);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/features/ui-antdv.ts
|
|
240
|
+
import path4 from "path";
|
|
241
|
+
var ANTDV_MARKER = "// ANT_DESIGN_VUE_INJECTED";
|
|
242
|
+
var VITE_IMPORTS2 = `import Components from 'unplugin-vue-components/vite'
|
|
243
|
+
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
|
|
244
|
+
`;
|
|
245
|
+
var VITE_PLUGINS2 = `
|
|
246
|
+
Components({ resolvers: [AntDesignVueResolver()] }),`;
|
|
247
|
+
var MAIN_IMPORTS2 = `import Antd from 'ant-design-vue'
|
|
248
|
+
import 'ant-design-vue/dist/reset.css'
|
|
249
|
+
`;
|
|
250
|
+
var ANTDV_PKG = {
|
|
251
|
+
dependencies: {
|
|
252
|
+
"ant-design-vue": "^4.2.0",
|
|
253
|
+
"@ant-design/icons-vue": "^7.0.0"
|
|
254
|
+
},
|
|
255
|
+
devDependencies: {
|
|
256
|
+
"unplugin-vue-components": "^32.0.0"
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
async function overlayAntdv(projectRoot) {
|
|
260
|
+
const mainPath = path4.join(projectRoot, "src/main.ts");
|
|
261
|
+
let main2 = await readText(mainPath);
|
|
262
|
+
if (!hasMarker(main2, ANTDV_MARKER)) {
|
|
263
|
+
main2 = insertBefore(main2, "import { createApp }", `${ANTDV_MARKER}
|
|
264
|
+
${MAIN_IMPORTS2}`, mainPath);
|
|
265
|
+
main2 = insertAfter(main2, "const app = createApp(App)", `
|
|
266
|
+
app.use(Antd)`, mainPath);
|
|
267
|
+
await writeText(mainPath, main2);
|
|
268
|
+
}
|
|
269
|
+
const vitePath = path4.join(projectRoot, "vite.config.ts");
|
|
270
|
+
let vite = await readText(vitePath);
|
|
271
|
+
if (!hasMarker(vite, ANTDV_MARKER)) {
|
|
272
|
+
vite = insertBefore(vite, "export default defineConfig(", `${ANTDV_MARKER}
|
|
273
|
+
${VITE_IMPORTS2}`, vitePath);
|
|
274
|
+
vite = insertAfter(vite, "plugins: [", VITE_PLUGINS2, vitePath);
|
|
275
|
+
await writeText(vitePath, vite);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/features/harness.ts
|
|
280
|
+
import path5 from "path";
|
|
281
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
282
|
+
var TEMPLATES_DIR2 = fileURLToPath2(new URL("../templates", import.meta.url));
|
|
283
|
+
var HARNESS_FULL_PKG = {
|
|
284
|
+
devDependencies: {
|
|
285
|
+
"oxlint": "~1.57.0",
|
|
286
|
+
"@commitlint/cli": "^20.2.0",
|
|
287
|
+
"@commitlint/config-conventional": "^20.2.0",
|
|
288
|
+
"lint-staged": "^16.2.7",
|
|
289
|
+
"husky": "^9.1.7"
|
|
290
|
+
},
|
|
291
|
+
scripts: {
|
|
292
|
+
"harness:sync": "node scripts/verify-skills.mjs --write",
|
|
293
|
+
"harness:check": "node scripts/verify-skills.mjs && node scripts/harness-hooks.test.mjs",
|
|
294
|
+
"harness:test": "node scripts/harness-hooks.test.mjs && node scripts/verify-skills.test.mjs",
|
|
295
|
+
"check": "pnpm type-check && pnpm lint && pnpm format:check && pnpm harness:check"
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
var HARNESS_MINIMAL_PKG = {
|
|
299
|
+
devDependencies: {},
|
|
300
|
+
scripts: {
|
|
301
|
+
"check": "pnpm type-check && pnpm lint && pnpm format:check"
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
async function overlayHarness(projectRoot, level) {
|
|
305
|
+
await copyDir(path5.join(TEMPLATES_DIR2, "harness", level), projectRoot);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/overlay.ts
|
|
309
|
+
async function overlay(projectRoot, config) {
|
|
310
|
+
const pkgPath = path6.join(projectRoot, "package.json");
|
|
311
|
+
let pkg = await readJson(pkgPath);
|
|
312
|
+
const add = { dependencies: {}, devDependencies: {}, scripts: {} };
|
|
313
|
+
const merge = (src) => {
|
|
314
|
+
if (src.dependencies) Object.assign(add.dependencies, src.dependencies);
|
|
315
|
+
if (src.devDependencies) Object.assign(add.devDependencies, src.devDependencies);
|
|
316
|
+
if (src.scripts) Object.assign(add.scripts, src.scripts);
|
|
317
|
+
};
|
|
318
|
+
if (config.axios) {
|
|
319
|
+
await overlayAxios(projectRoot, config);
|
|
320
|
+
merge(AXIOS_PKG);
|
|
321
|
+
}
|
|
322
|
+
if (config.uiLibrary === "element-plus") {
|
|
323
|
+
await overlayElementPlus(projectRoot);
|
|
324
|
+
merge(ELEMENT_PLUS_PKG);
|
|
325
|
+
} else if (config.uiLibrary === "ant-design-vue") {
|
|
326
|
+
await overlayAntdv(projectRoot);
|
|
327
|
+
merge(ANTDV_PKG);
|
|
328
|
+
}
|
|
329
|
+
if (config.harness !== "none") {
|
|
330
|
+
await overlayHarness(projectRoot, config.harness);
|
|
331
|
+
merge(config.harness === "full" ? HARNESS_FULL_PKG : HARNESS_MINIMAL_PKG);
|
|
332
|
+
}
|
|
333
|
+
pkg = mergePackageJson(pkg, add);
|
|
334
|
+
await writeJson(pkgPath, pkg);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/utils/logger.ts
|
|
338
|
+
import pc from "picocolors";
|
|
339
|
+
var info = (msg) => console.log(pc.cyan(` ${msg}`));
|
|
340
|
+
var success = (msg) => console.log(pc.green(`\u2713 ${msg}`));
|
|
341
|
+
var error = (msg) => console.error(pc.red(`\u2717 ${msg}`));
|
|
342
|
+
var log = (msg) => console.log(msg);
|
|
343
|
+
|
|
344
|
+
// src/index.ts
|
|
345
|
+
function parseArgs(argv) {
|
|
346
|
+
return {
|
|
347
|
+
projectName: argv.find((a) => !a.startsWith("-")),
|
|
348
|
+
isYes: argv.includes("--yes"),
|
|
349
|
+
uiLibrary: argv.find((a) => a.startsWith("--ui="))?.split("=")[1],
|
|
350
|
+
harness: argv.find((a) => a.startsWith("--harness="))?.split("=")[1],
|
|
351
|
+
noAxios: argv.includes("--no-axios"),
|
|
352
|
+
vitest: argv.includes("--vitest")
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async function main() {
|
|
356
|
+
const args = parseArgs(process.argv.slice(2));
|
|
357
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
358
|
+
log(`Usage: create-harness-app [project-name] [options]
|
|
359
|
+
|
|
360
|
+
Options:
|
|
361
|
+
--yes Non-interactive: TS + Router + Pinia + ElementPlus + axios + harness:full
|
|
362
|
+
--ui=<lib> element-plus | ant-design-vue | none (default: element-plus)
|
|
363
|
+
--harness=<level> full | minimal | none (default: full)
|
|
364
|
+
--no-axios Skip axios layer
|
|
365
|
+
--vitest Enable Vitest
|
|
366
|
+
-h, --help Show this help
|
|
367
|
+
|
|
368
|
+
Examples:
|
|
369
|
+
npm create @dianzhong/harness-app my-app
|
|
370
|
+
npm create @dianzhong/harness-app my-app -- --yes
|
|
371
|
+
npm create @dianzhong/harness-app my-app -- --ui=ant-design-vue --harness=minimal
|
|
372
|
+
`);
|
|
373
|
+
process.exit(0);
|
|
374
|
+
}
|
|
375
|
+
let config;
|
|
376
|
+
if (args.isYes || args.projectName && args.uiLibrary !== void 0 && args.harness !== void 0) {
|
|
377
|
+
const base = defaultConfig(args.projectName ?? "my-app");
|
|
378
|
+
config = {
|
|
379
|
+
...base,
|
|
380
|
+
uiLibrary: args.uiLibrary ?? base.uiLibrary,
|
|
381
|
+
harness: args.harness ?? base.harness,
|
|
382
|
+
axios: !args.noAxios,
|
|
383
|
+
vitest: args.vitest
|
|
384
|
+
};
|
|
385
|
+
info(`\u975E\u4EA4\u4E92\u6A21\u5F0F: ${config.projectName} | UI=${config.uiLibrary} | harness=${config.harness}`);
|
|
386
|
+
} else {
|
|
387
|
+
config = await collectConfig(args.projectName);
|
|
388
|
+
}
|
|
389
|
+
const targetDir = path7.resolve(process.cwd(), config.projectName);
|
|
390
|
+
if (await fse2.pathExists(targetDir)) {
|
|
391
|
+
const entries = await fse2.readdir(targetDir);
|
|
392
|
+
if (entries.length > 0) {
|
|
393
|
+
error(`\u76EE\u5F55 "${config.projectName}" \u5DF2\u5B58\u5728\u4E14\u4E0D\u4E3A\u7A7A`);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
info("\u4F7F\u7528 create-vue \u751F\u6210\u57FA\u7840\u9AA8\u67B6...");
|
|
398
|
+
await spawnCreateVue(config, path7.dirname(targetDir));
|
|
399
|
+
info("\u53E0\u52A0 features...");
|
|
400
|
+
await overlay(targetDir, config);
|
|
401
|
+
success("\u9879\u76EE\u521D\u59CB\u5316\u5B8C\u6210\uFF01");
|
|
402
|
+
outro("\u4E0B\u4E00\u6B65");
|
|
403
|
+
log(`
|
|
404
|
+
cd ${config.projectName}`);
|
|
405
|
+
log(" pnpm install");
|
|
406
|
+
if (config.harness === "full") log(" pnpm harness:sync # \u91CD\u7B97 skills \u6307\u7EB9");
|
|
407
|
+
log(" pnpm dev\n");
|
|
408
|
+
}
|
|
409
|
+
main().catch((err) => {
|
|
410
|
+
error(String(err.message ?? err));
|
|
411
|
+
process.exit(1);
|
|
412
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dianzhong/create-harness-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "点众前端项目初始化器:封装 create-vue,叠加 UI 库、axios 层与 AI Harness 治理体系",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "create-harness-app": "dist/index.mjs" },
|
|
7
|
+
"files": ["dist/", "templates/"],
|
|
8
|
+
"engines": { "node": "^20.19.0 || >=22.12.0" },
|
|
9
|
+
"keywords": ["create-vue", "scaffold", "harness", "vue3"],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"dev": "tsup --watch",
|
|
14
|
+
"test": "node --test --experimental-strip-types tests/merge-json.test.ts tests/inject.test.ts",
|
|
15
|
+
"test:e2e": "node --experimental-strip-types tests/e2e/generate.test.ts"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@clack/prompts": "^0.10.0",
|
|
19
|
+
"execa": "^9.0.0",
|
|
20
|
+
"fs-extra": "^11.3.0",
|
|
21
|
+
"picocolors": "^1.1.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/fs-extra": "^11.0.4",
|
|
25
|
+
"@types/node": "^22.0.0",
|
|
26
|
+
"tsup": "^8.5.0",
|
|
27
|
+
"typescript": "~5.7.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import request from './request'
|
|
2
|
+
|
|
3
|
+
/** 用户名密码登录 */
|
|
4
|
+
export function loginAPI(data: { username: string; password: string }) {
|
|
5
|
+
return request<{ token: string }>({ url: '/api/auth/login', method: 'POST', data })
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** 退出登录 */
|
|
9
|
+
export function logoutAPI() {
|
|
10
|
+
return request<void>({ url: '/api/auth/logout', method: 'POST' })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 获取当前用户信息 */
|
|
14
|
+
export function getUserInfoAPI() {
|
|
15
|
+
return request<{ id: number; name: string; roles: string[] }>({
|
|
16
|
+
url: '/api/auth/userInfo',
|
|
17
|
+
method: 'GET',
|
|
18
|
+
})
|
|
19
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import type { AxiosRequestConfig } from 'axios'
|
|
3
|
+
import type { ApiResponse } from '@/types/api'
|
|
4
|
+
import { getAccessToken, removeAccessToken } from '@/utils/auth'
|
|
5
|
+
import { showError } from '@/utils/show-error'
|
|
6
|
+
|
|
7
|
+
const axiosInstance = axios.create({
|
|
8
|
+
timeout: 15000,
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
axiosInstance.interceptors.request.use((config) => {
|
|
13
|
+
const token = getAccessToken()
|
|
14
|
+
if (token) config.headers.Authorization = `Bearer ${token}`
|
|
15
|
+
return config
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
axiosInstance.interceptors.response.use(
|
|
19
|
+
(response) => {
|
|
20
|
+
const res = response.data as ApiResponse<unknown>
|
|
21
|
+
if (res.code === 401) {
|
|
22
|
+
removeAccessToken()
|
|
23
|
+
window.location.href = '/login'
|
|
24
|
+
return Promise.reject(new Error(res.msg || '登录已过期'))
|
|
25
|
+
}
|
|
26
|
+
if (res.code !== 200) {
|
|
27
|
+
showError(res.msg || '请求失败')
|
|
28
|
+
return Promise.reject(new Error(res.msg || '请求失败'))
|
|
29
|
+
}
|
|
30
|
+
return response
|
|
31
|
+
},
|
|
32
|
+
(error) => {
|
|
33
|
+
if (error.response?.status === 401) {
|
|
34
|
+
removeAccessToken()
|
|
35
|
+
window.location.href = '/login'
|
|
36
|
+
return Promise.reject(new Error('登录已过期'))
|
|
37
|
+
}
|
|
38
|
+
const message = error.response?.data?.msg || error.message || '网络异常'
|
|
39
|
+
showError(message)
|
|
40
|
+
return Promise.reject(new Error(message))
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
export default function request<T>(config: {
|
|
45
|
+
url: string
|
|
46
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
|
47
|
+
data?: unknown
|
|
48
|
+
params?: unknown
|
|
49
|
+
headers?: Record<string, string>
|
|
50
|
+
onUploadProgress?: (progressEvent: { loaded: number; total?: number }) => void
|
|
51
|
+
}): Promise<T> {
|
|
52
|
+
const axiosConfig: AxiosRequestConfig = {
|
|
53
|
+
url: config.url,
|
|
54
|
+
method: config.method,
|
|
55
|
+
data: config.data,
|
|
56
|
+
params: config.params,
|
|
57
|
+
onUploadProgress: config.onUploadProgress,
|
|
58
|
+
headers: { ...config.headers },
|
|
59
|
+
}
|
|
60
|
+
return axiosInstance(axiosConfig).then((res) => (res.data as ApiResponse<T>).data)
|
|
61
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ApiResponse<T> {
|
|
2
|
+
code: number
|
|
3
|
+
msg: string
|
|
4
|
+
data: T
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PageParams {
|
|
8
|
+
pageNo?: number
|
|
9
|
+
limit?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PageResult<T> {
|
|
13
|
+
data: T[]
|
|
14
|
+
pageNo: number
|
|
15
|
+
limit: number
|
|
16
|
+
totalNum: number
|
|
17
|
+
totalPage: number
|
|
18
|
+
/** 兼容后端 records 命名 */
|
|
19
|
+
records?: T[]
|
|
20
|
+
/** 兼容后端 total 命名 */
|
|
21
|
+
total?: number
|
|
22
|
+
/** 兼容后端 size 命名 */
|
|
23
|
+
size?: number
|
|
24
|
+
/** 兼容后端 current 命名 */
|
|
25
|
+
current?: number
|
|
26
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { getStorageItem, removeStorageItem, setStorageItem } from './storage'
|
|
2
|
+
|
|
3
|
+
export function getAccessToken() { return getStorageItem('token') }
|
|
4
|
+
export function setAccessToken(token: string) { setStorageItem('token', token) }
|
|
5
|
+
export function removeAccessToken() { removeStorageItem('token') }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function getStorageItem(key: string): string | null {
|
|
2
|
+
return window.localStorage.getItem(key)
|
|
3
|
+
}
|
|
4
|
+
export function setStorageItem(key: string, value: string) {
|
|
5
|
+
window.localStorage.setItem(key, value)
|
|
6
|
+
}
|
|
7
|
+
export function removeStorageItem(key: string) {
|
|
8
|
+
window.localStorage.removeItem(key)
|
|
9
|
+
}
|
|
10
|
+
export function getJsonStorageItem<T>(key: string): T | null {
|
|
11
|
+
const raw = getStorageItem(key)
|
|
12
|
+
if (!raw) return null
|
|
13
|
+
try { return JSON.parse(raw) as T } catch { removeStorageItem(key); return null }
|
|
14
|
+
}
|
|
15
|
+
export function setJsonStorageItem<T>(key: string, value: T) {
|
|
16
|
+
setStorageItem(key, JSON.stringify(value))
|
|
17
|
+
}
|