vue-feat-cli 26.9.0 → 26.9.2
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 +47 -2
- package/dist/cli.js +932 -288
- package/package.json +9 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/cli.ts
|
|
2
2
|
import { cac } from "cac";
|
|
3
3
|
|
|
4
|
-
// src/commands/
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
4
|
+
// src/commands/agents-init.ts
|
|
5
|
+
import { intro, log, outro } from "@clack/prompts";
|
|
6
|
+
import fs2 from "fs-extra";
|
|
7
|
+
import path2 from "path";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
10
|
import fs from "fs-extra";
|
|
@@ -33,41 +33,236 @@ async function saveConfig(root, config) {
|
|
|
33
33
|
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// src/utils/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
// src/utils/output.ts
|
|
37
|
+
function emitJson(result) {
|
|
38
|
+
console.log(JSON.stringify(result, null, 2));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/commands/agents-init.ts
|
|
42
|
+
function buildAgentsMd(opts) {
|
|
43
|
+
return `# AGENTS.md \u2014 vue-feat-cli conventions
|
|
44
|
+
|
|
45
|
+
This repo uses \`vue-feat-cli\` (\`vf\`) for feature-based Vue 3 scaffolding.
|
|
46
|
+
Follow these conventions when creating or editing features.
|
|
47
|
+
|
|
48
|
+
## Stack (from vf.config.json)
|
|
49
|
+
|
|
50
|
+
- features: \`${opts.featuresDir}/\`
|
|
51
|
+
- shared: \`${opts.sharedDir}/\`
|
|
52
|
+
- alias: \`${opts.alias}\` \u2192 \`src/\`
|
|
53
|
+
- httpClient: \`${opts.httpClient}\`
|
|
54
|
+
- pinia: ${opts.usesPinia ? "enabled (`defineStore`)" : "disabled (`reactive()` fallback)"}
|
|
55
|
+
- vue-router: ${opts.usesVueRouter ? "enabled (routes.ts + views/)" : "disabled"}
|
|
56
|
+
- tanstack-query: ${opts.usesTanstackQuery ? "enabled" : "disabled"}
|
|
57
|
+
|
|
58
|
+
## Architecture (strict layering)
|
|
59
|
+
|
|
60
|
+
\`\`\`
|
|
61
|
+
service.ts
|
|
62
|
+
\u2514\u2500\u2500 useXxxService.ts \u2190 domain data + CRUD, no UI state
|
|
63
|
+
\u2514\u2500\u2500 useXxxPage.ts \u2190 loading + error, delegates to service
|
|
64
|
+
\u2514\u2500\u2500 XxxView.vue \u2190 thin page, calls load() on mount
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
- \`services/\` uses \`httpClient\` from \`${opts.alias}/shared/http/client\` (\`get/post/put/patch/delete\`), base URL from \`VITE_API_BASE_URL\`.
|
|
68
|
+
- \`stores/\` is only for cross-feature shared state.
|
|
69
|
+
- \`types/\` holds entity + CreateDto/UpdateDto.
|
|
70
|
+
- \`index.ts\` is the public barrel; \`components/\` holds feature-local components.
|
|
71
|
+
|
|
72
|
+
## CLI contract (prefer non-interactive flags so agents can run headless)
|
|
73
|
+
|
|
74
|
+
\`\`\`bash
|
|
75
|
+
vf doctor --json # machine-readable project diagnosis (run first)
|
|
76
|
+
vf g:feat Product --dry-run --json # preview files without writing
|
|
77
|
+
vf g:feat Product --json # scaffold, JSON output { files[], warnings[] }
|
|
78
|
+
vf g:feat Order --with-crud --json # full CRUD service + composables + DTOs
|
|
79
|
+
vf g:feat Category --only service,store --json # partial layers
|
|
80
|
+
vf g:feat Category --exclude view,routes --json # everything except
|
|
81
|
+
vf g:feat X --no-store --no-types --json # convenience negations
|
|
82
|
+
vf g:feat X --force --json # overwrite existing files
|
|
83
|
+
vf g:feat X --register-route --json # also patch src/router/index.ts
|
|
84
|
+
vf agents:init --json # regenerate this file
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
- Layers: \`service, serviceComposable, pageComposable, store, types, index, routes, view\` (+ \`composables\` = both).
|
|
88
|
+
- \`--only\` / \`--exclude\` accept comma-separated layers. \`vf.config.json \u2192 layers\` can disable layers by default.
|
|
89
|
+
- Every generator supports \`--dry-run --json --force\`. In \`--json\` mode stdout is pure JSON; warnings are in \`warnings[]\`.
|
|
90
|
+
- Never hand-edit generated barrels inconsistently \u2014 re-run the generator or update \`index.ts\`.
|
|
91
|
+
|
|
92
|
+
## Custom templates
|
|
93
|
+
|
|
94
|
+
- Override per-project via \`.vf/templates/\` (see \`vf templates:init\` + \`CONTEXT.md\`).
|
|
95
|
+
- Variables: feature \u2192 \`name, Name, nameCamel, alias, usesVueRouter\`; component/composable \u2192 \`name, Name\`.
|
|
96
|
+
- Missing local files fall back to built-ins.
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
async function agentsInit(options = {}) {
|
|
100
|
+
const jsonMode = Boolean(options.json);
|
|
101
|
+
const force = Boolean(options.force);
|
|
102
|
+
const root = process.cwd();
|
|
103
|
+
const config = await loadConfig(root);
|
|
104
|
+
const outPath = path2.join(root, "AGENTS.md");
|
|
105
|
+
if (!jsonMode) intro("\u{1F916} Generating AGENTS.md");
|
|
106
|
+
if (await fs2.pathExists(outPath) && !force) {
|
|
107
|
+
const msg = "AGENTS.md already exists (use --force to overwrite).";
|
|
108
|
+
if (jsonMode) {
|
|
109
|
+
emitJson({ command: "agents:init", ok: false, dryRun: false, files: [], warnings: [], error: msg });
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
log.error(msg);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const content = buildAgentsMd({
|
|
116
|
+
featuresDir: config.featuresDir,
|
|
117
|
+
sharedDir: config.sharedDir,
|
|
118
|
+
alias: config.alias,
|
|
119
|
+
httpClient: config.httpClient,
|
|
120
|
+
usesPinia: config.usesPinia,
|
|
121
|
+
usesVueRouter: config.usesVueRouter,
|
|
122
|
+
usesTanstackQuery: config.usesTanstackQuery
|
|
123
|
+
});
|
|
124
|
+
await fs2.writeFile(outPath, content, "utf-8");
|
|
125
|
+
const rel = path2.relative(root, outPath);
|
|
126
|
+
const result = {
|
|
127
|
+
command: "agents:init",
|
|
128
|
+
ok: true,
|
|
129
|
+
dryRun: false,
|
|
130
|
+
files: [{ template: "agents-md", path: rel, status: "created" }],
|
|
131
|
+
warnings: []
|
|
132
|
+
};
|
|
133
|
+
if (jsonMode) emitJson(result);
|
|
134
|
+
else outro(`Created: ${rel} \u2014 commit it so every agent follows the same conventions.`);
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/commands/doctor.ts
|
|
139
|
+
import fs5 from "fs-extra";
|
|
140
|
+
import path5 from "path";
|
|
141
|
+
|
|
142
|
+
// src/utils/feat-layers.ts
|
|
143
|
+
var FEAT_LAYERS = [
|
|
144
|
+
"service",
|
|
145
|
+
"serviceComposable",
|
|
146
|
+
"pageComposable",
|
|
147
|
+
"store",
|
|
148
|
+
"types",
|
|
149
|
+
"index",
|
|
150
|
+
"routes",
|
|
151
|
+
"view"
|
|
152
|
+
];
|
|
153
|
+
var ALIASES = {
|
|
154
|
+
service: "service",
|
|
155
|
+
store: "store",
|
|
156
|
+
types: "types",
|
|
157
|
+
type: "types",
|
|
158
|
+
index: "index",
|
|
159
|
+
routes: "routes",
|
|
160
|
+
route: "routes",
|
|
161
|
+
view: "view",
|
|
162
|
+
views: "view",
|
|
163
|
+
composables: "composables",
|
|
164
|
+
composable: "composables",
|
|
165
|
+
servicecomposable: "serviceComposable",
|
|
166
|
+
"service-composable": "serviceComposable",
|
|
167
|
+
service_composable: "serviceComposable",
|
|
168
|
+
useservices: "serviceComposable",
|
|
169
|
+
pagecomposable: "pageComposable",
|
|
170
|
+
"page-composable": "pageComposable",
|
|
171
|
+
page_composable: "pageComposable",
|
|
172
|
+
usepage: "pageComposable"
|
|
41
173
|
};
|
|
42
|
-
|
|
174
|
+
function parseLayerList(input) {
|
|
175
|
+
if (!input) return [];
|
|
176
|
+
const out = [];
|
|
177
|
+
for (const raw of input.split(",")) {
|
|
178
|
+
const key = raw.trim().toLowerCase().replace(/_/g, "-");
|
|
179
|
+
if (!key) continue;
|
|
180
|
+
const mapped = ALIASES[key];
|
|
181
|
+
if (!mapped) throw new Error(`Unknown layer "${raw.trim()}". Valid: ${FEAT_LAYERS.join(", ")} (+ "composables" for both)`);
|
|
182
|
+
if (mapped === "composables") {
|
|
183
|
+
out.push("serviceComposable", "pageComposable");
|
|
184
|
+
} else if (!out.includes(mapped)) {
|
|
185
|
+
out.push(mapped);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return [...new Set(out)];
|
|
189
|
+
}
|
|
190
|
+
function resolveFeatLayers(config, options = {}) {
|
|
191
|
+
const warnings = [];
|
|
192
|
+
const enabled = new Set(FEAT_LAYERS);
|
|
193
|
+
for (const layer of FEAT_LAYERS) {
|
|
194
|
+
if (config.layers?.[layer] === false) enabled.delete(layer);
|
|
195
|
+
}
|
|
196
|
+
if (!config.usesVueRouter) {
|
|
197
|
+
enabled.delete("routes");
|
|
198
|
+
enabled.delete("view");
|
|
199
|
+
}
|
|
200
|
+
if (options.store === false) enabled.delete("store");
|
|
201
|
+
if (options.service === false) enabled.delete("service");
|
|
202
|
+
if (options.types === false) enabled.delete("types");
|
|
203
|
+
if (options.routes === false) enabled.delete("routes");
|
|
204
|
+
if (options.view === false) enabled.delete("view");
|
|
205
|
+
if (options.index === false) enabled.delete("index");
|
|
206
|
+
if (options.composables === false) {
|
|
207
|
+
enabled.delete("serviceComposable");
|
|
208
|
+
enabled.delete("pageComposable");
|
|
209
|
+
}
|
|
210
|
+
if (options.exclude) {
|
|
211
|
+
for (const layer of parseLayerList(options.exclude)) enabled.delete(layer);
|
|
212
|
+
}
|
|
213
|
+
if (options.only) {
|
|
214
|
+
const only = parseLayerList(options.only);
|
|
215
|
+
const next = /* @__PURE__ */ new Set();
|
|
216
|
+
for (const layer of only) {
|
|
217
|
+
if ((layer === "routes" || layer === "view") && !config.usesVueRouter) {
|
|
218
|
+
warnings.push(`Layer "${layer}" requires usesVueRouter: true \u2014 skipped.`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
next.add(layer);
|
|
222
|
+
}
|
|
223
|
+
return { enabled: next, warnings };
|
|
224
|
+
}
|
|
225
|
+
return { enabled, warnings };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/utils/package.ts
|
|
229
|
+
import fs3 from "fs-extra";
|
|
230
|
+
import path3 from "path";
|
|
231
|
+
async function hasDependency(root, name) {
|
|
232
|
+
const pkgPath = path3.join(root, "package.json");
|
|
233
|
+
if (!await fs3.pathExists(pkgPath)) return false;
|
|
234
|
+
const pkg = await fs3.readJson(pkgPath);
|
|
235
|
+
return Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
|
|
236
|
+
}
|
|
43
237
|
|
|
44
238
|
// src/utils/render.ts
|
|
45
|
-
import
|
|
239
|
+
import fs4 from "fs-extra";
|
|
46
240
|
import Handlebars from "handlebars";
|
|
47
|
-
import
|
|
241
|
+
import path4 from "path";
|
|
48
242
|
import prettier from "prettier";
|
|
49
243
|
import { fileURLToPath } from "url";
|
|
50
|
-
var __dirname =
|
|
51
|
-
var TEMPLATES_DIR = __dirname.endsWith("utils") ?
|
|
244
|
+
var __dirname = path4.dirname(fileURLToPath(import.meta.url));
|
|
245
|
+
var TEMPLATES_DIR = __dirname.endsWith("utils") ? path4.resolve(__dirname, "../templates") : path4.resolve(__dirname, "templates");
|
|
52
246
|
async function resolveTemplatePath(template, root, templatesDir) {
|
|
53
|
-
const safeRoot =
|
|
54
|
-
const localPath =
|
|
247
|
+
const safeRoot = path4.resolve(root) + path4.sep;
|
|
248
|
+
const localPath = path4.resolve(root, templatesDir, template);
|
|
55
249
|
if (!localPath.startsWith(safeRoot)) {
|
|
56
250
|
throw new Error(`Security: template path "${template}" escapes the project root.`);
|
|
57
251
|
}
|
|
58
|
-
if (await
|
|
59
|
-
return
|
|
252
|
+
if (await fs4.pathExists(localPath)) return localPath;
|
|
253
|
+
return path4.join(TEMPLATES_DIR, template);
|
|
60
254
|
}
|
|
61
255
|
async function renderTemplate({
|
|
62
256
|
template,
|
|
63
257
|
outputPath,
|
|
64
258
|
context,
|
|
65
259
|
skipIfExists = false,
|
|
260
|
+
overwrite = false,
|
|
66
261
|
templatesDir,
|
|
67
262
|
root
|
|
68
263
|
}) {
|
|
69
|
-
const templatePath = templatesDir && root ? await resolveTemplatePath(template, root, templatesDir) :
|
|
70
|
-
const raw = await
|
|
264
|
+
const templatePath = templatesDir && root ? await resolveTemplatePath(template, root, templatesDir) : path4.join(TEMPLATES_DIR, template);
|
|
265
|
+
const raw = await fs4.readFile(templatePath, "utf-8");
|
|
71
266
|
const compiled = Handlebars.compile(raw);
|
|
72
267
|
let output = compiled(context);
|
|
73
268
|
const parser = outputPath.endsWith(".vue") ? "vue" : "typescript";
|
|
@@ -75,163 +270,385 @@ async function renderTemplate({
|
|
|
75
270
|
output = await prettier.format(output, { parser, semi: false, singleQuote: true });
|
|
76
271
|
} catch {
|
|
77
272
|
}
|
|
78
|
-
await
|
|
79
|
-
if (await
|
|
273
|
+
await fs4.ensureDir(path4.dirname(outputPath));
|
|
274
|
+
if (await fs4.pathExists(outputPath)) {
|
|
80
275
|
if (skipIfExists) return null;
|
|
81
|
-
throw new Error(`File already exists: ${outputPath}`);
|
|
276
|
+
if (!overwrite) throw new Error(`File already exists: ${outputPath} (use --force to overwrite)`);
|
|
82
277
|
}
|
|
83
|
-
await
|
|
278
|
+
await fs4.writeFile(outputPath, output, "utf-8");
|
|
84
279
|
return outputPath;
|
|
85
280
|
}
|
|
86
281
|
|
|
282
|
+
// src/commands/doctor.ts
|
|
283
|
+
async function doctor(options = {}) {
|
|
284
|
+
const jsonMode = Boolean(options.json);
|
|
285
|
+
const root = process.cwd();
|
|
286
|
+
const configPath = path5.join(root, CONFIG_FILENAME);
|
|
287
|
+
const hasConfig = await fs5.pathExists(configPath);
|
|
288
|
+
const config = await loadConfig(root);
|
|
289
|
+
const checks = [];
|
|
290
|
+
const warnings = [];
|
|
291
|
+
checks.push({
|
|
292
|
+
id: "config",
|
|
293
|
+
label: "vf.config.json exists",
|
|
294
|
+
ok: hasConfig,
|
|
295
|
+
detail: hasConfig ? configPath : `${CONFIG_FILENAME} not found \u2014 using defaults (${JSON.stringify(defaultConfig)})`,
|
|
296
|
+
hint: hasConfig ? void 0 : 'Run "vf init" to create it.'
|
|
297
|
+
});
|
|
298
|
+
for (const [id, dir] of [
|
|
299
|
+
["srcDir", config.srcDir],
|
|
300
|
+
["featuresDir", config.featuresDir],
|
|
301
|
+
["sharedDir", config.sharedDir]
|
|
302
|
+
]) {
|
|
303
|
+
const p = path5.join(root, dir);
|
|
304
|
+
const ok2 = await fs5.pathExists(p);
|
|
305
|
+
checks.push({ id: `dir:${id}`, label: `dir ${dir} exists`, ok: ok2, detail: p, hint: ok2 ? void 0 : `Run "vf init" to create it.` });
|
|
306
|
+
}
|
|
307
|
+
const tsconfigPath = path5.join(root, "tsconfig.json");
|
|
308
|
+
if (await fs5.pathExists(tsconfigPath)) {
|
|
309
|
+
try {
|
|
310
|
+
const tsconfig = await fs5.readJson(tsconfigPath);
|
|
311
|
+
const paths = tsconfig.compilerOptions?.paths ?? {};
|
|
312
|
+
const aliases = Object.keys(paths).map((k) => k.replace(/\/\*$/, ""));
|
|
313
|
+
const ok2 = aliases.includes(config.alias);
|
|
314
|
+
checks.push({
|
|
315
|
+
id: "alias",
|
|
316
|
+
label: `alias "${config.alias}" mapped in tsconfig`,
|
|
317
|
+
ok: ok2,
|
|
318
|
+
detail: aliases.length ? `paths: ${aliases.join(", ")}` : "no compilerOptions.paths found",
|
|
319
|
+
hint: ok2 ? void 0 : `Add { "${config.alias}/*": ["./${config.srcDir}/*"] } to tsconfig.json.`
|
|
320
|
+
});
|
|
321
|
+
} catch {
|
|
322
|
+
checks.push({ id: "alias", label: "tsconfig readable", ok: false, detail: tsconfigPath });
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
checks.push({ id: "alias", label: "tsconfig exists", ok: false, detail: "tsconfig.json not found", hint: "Alias check skipped." });
|
|
326
|
+
}
|
|
327
|
+
const depExpectations = [
|
|
328
|
+
{ dep: "pinia", enabled: config.usesPinia, label: "Pinia" },
|
|
329
|
+
{ dep: "vue-router", enabled: config.usesVueRouter, label: "Vue Router" },
|
|
330
|
+
{ dep: "@tanstack/vue-query", enabled: config.usesTanstackQuery, label: "TanStack Query" },
|
|
331
|
+
{ dep: "axios", enabled: config.httpClient === "axios", label: "axios (httpClient)" }
|
|
332
|
+
];
|
|
333
|
+
for (const { dep, enabled, label } of depExpectations) {
|
|
334
|
+
const installed = await hasDependency(root, dep);
|
|
335
|
+
const ok2 = !enabled || installed;
|
|
336
|
+
checks.push({
|
|
337
|
+
id: `dep:${dep}`,
|
|
338
|
+
label: `${label} ${enabled ? "enabled + installed" : installed ? "installed (not enabled)" : "not used"}`,
|
|
339
|
+
ok: ok2,
|
|
340
|
+
detail: `config=${enabled ? "on" : "off"}, installed=${installed ? "yes" : "no"}`,
|
|
341
|
+
hint: ok2 ? void 0 : `Install "${dep}" or disable it in ${CONFIG_FILENAME}.`
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
const httpPath = path5.join(root, config.sharedDir, "http", "client.ts");
|
|
345
|
+
const httpExists = await fs5.pathExists(httpPath);
|
|
346
|
+
checks.push({ id: "http-client", label: "shared http client exists", ok: httpExists, detail: httpPath, hint: httpExists ? void 0 : 'Run "vf init" or generate any feature to create it.' });
|
|
347
|
+
if (config.usesVueRouter) {
|
|
348
|
+
const routerPath = path5.join(root, config.srcDir, "router", "index.ts");
|
|
349
|
+
const ok2 = await fs5.pathExists(routerPath);
|
|
350
|
+
checks.push({ id: "router", label: "router file exists", ok: ok2, detail: routerPath, hint: ok2 ? void 0 : "--register-route needs this file." });
|
|
351
|
+
}
|
|
352
|
+
if (config.templatesDir) {
|
|
353
|
+
const tplPath = path5.resolve(root, config.templatesDir);
|
|
354
|
+
const ok2 = await fs5.pathExists(tplPath);
|
|
355
|
+
checks.push({ id: "templatesDir", label: `templatesDir ${config.templatesDir} exists`, ok: ok2, detail: tplPath, hint: ok2 ? void 0 : 'Run "vf templates:init".' });
|
|
356
|
+
}
|
|
357
|
+
const builtin = [];
|
|
358
|
+
try {
|
|
359
|
+
const walk = async (dir, prefix = "") => {
|
|
360
|
+
for (const entry of await fs5.readdir(dir)) {
|
|
361
|
+
const full = path5.join(dir, entry);
|
|
362
|
+
const stat = await fs5.stat(full);
|
|
363
|
+
if (stat.isDirectory()) await walk(full, `${prefix}${entry}/`);
|
|
364
|
+
else builtin.push(`${prefix}${entry}`);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
await walk(TEMPLATES_DIR);
|
|
368
|
+
} catch {
|
|
369
|
+
warnings.push("Could not list built-in templates.");
|
|
370
|
+
}
|
|
371
|
+
const ok = checks.every((c) => c.ok);
|
|
372
|
+
const result = {
|
|
373
|
+
command: "doctor",
|
|
374
|
+
ok,
|
|
375
|
+
dryRun: false,
|
|
376
|
+
files: [],
|
|
377
|
+
warnings,
|
|
378
|
+
config,
|
|
379
|
+
configPath: hasConfig ? configPath : null,
|
|
380
|
+
layers: FEAT_LAYERS,
|
|
381
|
+
checks,
|
|
382
|
+
templates: builtin.sort(),
|
|
383
|
+
templateVariables: {
|
|
384
|
+
feature: ["name", "Name", "nameCamel", "alias", "usesVueRouter"],
|
|
385
|
+
component: ["name", "Name"],
|
|
386
|
+
composable: ["name", "Name"]
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
if (jsonMode) {
|
|
390
|
+
emitJson(result);
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
console.log();
|
|
394
|
+
console.log(` vf doctor \u2014 ${ok ? "all checks passed" : "issues found"}`);
|
|
395
|
+
console.log();
|
|
396
|
+
for (const c of checks) {
|
|
397
|
+
console.log(` ${c.ok ? "\u2705" : "\u274C"} ${c.label}`);
|
|
398
|
+
console.log(` ${c.detail}`);
|
|
399
|
+
if (c.hint) console.log(` \u2192 ${c.hint}`);
|
|
400
|
+
}
|
|
401
|
+
console.log();
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/commands/generate-component.ts
|
|
406
|
+
import { cancel, intro as intro2, isCancel, log as log2, outro as outro2, text } from "@clack/prompts";
|
|
407
|
+
import fs6 from "fs-extra";
|
|
408
|
+
import path6 from "path";
|
|
409
|
+
|
|
410
|
+
// src/utils/case.ts
|
|
411
|
+
var pascalCase = (str) => str.replace(/(^\w|[-_]\w)/g, (m) => m.replace(/[-_]/, "").toUpperCase());
|
|
412
|
+
var camelCase = (str) => {
|
|
413
|
+
const p = pascalCase(str);
|
|
414
|
+
return p.charAt(0).toLowerCase() + p.slice(1);
|
|
415
|
+
};
|
|
416
|
+
var kebabCase = (str) => str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
417
|
+
|
|
87
418
|
// src/commands/generate-component.ts
|
|
88
419
|
async function generateComponent(name, options) {
|
|
420
|
+
const jsonMode = Boolean(options.json);
|
|
421
|
+
const dryRun = Boolean(options.dryRun);
|
|
422
|
+
const force = Boolean(options.force);
|
|
89
423
|
const segments = name.split("/").filter(Boolean);
|
|
90
424
|
const componentName = segments.pop();
|
|
91
|
-
const subPath = segments.join(
|
|
425
|
+
const subPath = segments.join(path6.sep);
|
|
92
426
|
const Name = pascalCase(componentName);
|
|
93
|
-
|
|
427
|
+
if (!jsonMode) intro2(`${dryRun ? "\u{1F50D} [dry-run] " : "\u{1F9F1} "}Generating component: ${Name}.vue`);
|
|
94
428
|
const root = process.cwd();
|
|
95
429
|
const config = await loadConfig(root);
|
|
96
430
|
let feature = options.feature;
|
|
97
431
|
if (feature === void 0) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
432
|
+
if (jsonMode) {
|
|
433
|
+
feature = "";
|
|
434
|
+
} else {
|
|
435
|
+
const answer = await text({
|
|
436
|
+
message: "Which feature does this component belong to? (leave empty for shared)",
|
|
437
|
+
placeholder: "e.g. acme",
|
|
438
|
+
defaultValue: ""
|
|
439
|
+
});
|
|
440
|
+
if (isCancel(answer)) {
|
|
441
|
+
cancel("Operation cancelled.");
|
|
442
|
+
process.exit(0);
|
|
443
|
+
}
|
|
444
|
+
feature = answer.trim();
|
|
106
445
|
}
|
|
107
|
-
feature = answer.trim();
|
|
108
446
|
}
|
|
109
447
|
let basePath;
|
|
110
448
|
if (feature) {
|
|
111
449
|
feature = kebabCase(feature);
|
|
112
|
-
const featurePath =
|
|
113
|
-
if (!await
|
|
114
|
-
|
|
450
|
+
const featurePath = path6.join(root, config.featuresDir, feature);
|
|
451
|
+
if (!await fs6.pathExists(featurePath)) {
|
|
452
|
+
const msg = `Feature "${feature}" not found. Run "vf generate:feat ${feature}" first.`;
|
|
453
|
+
if (jsonMode) {
|
|
454
|
+
emitJson({ command: "generate:component", ok: false, dryRun, files: [], warnings: [], error: msg });
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
log2.error(msg);
|
|
115
458
|
process.exit(1);
|
|
116
459
|
}
|
|
117
|
-
basePath =
|
|
460
|
+
basePath = path6.join(featurePath, "components", subPath);
|
|
118
461
|
} else {
|
|
119
|
-
basePath =
|
|
462
|
+
basePath = path6.join(root, config.sharedDir, "components", subPath);
|
|
463
|
+
}
|
|
464
|
+
const context = { name: kebabCase(componentName), Name };
|
|
465
|
+
const outputPath = path6.join(basePath, `${Name}.vue`);
|
|
466
|
+
const rel = path6.relative(root, outputPath);
|
|
467
|
+
if (dryRun) {
|
|
468
|
+
const exists = await fs6.pathExists(outputPath);
|
|
469
|
+
const result = {
|
|
470
|
+
command: "generate:component",
|
|
471
|
+
ok: true,
|
|
472
|
+
dryRun: true,
|
|
473
|
+
files: [{ template: "component/Component.vue.hbs", path: rel, status: "dry-run", reason: exists ? force ? "would overwrite" : "already exists" : "would create" }],
|
|
474
|
+
warnings: []
|
|
475
|
+
};
|
|
476
|
+
if (jsonMode) emitJson(result);
|
|
477
|
+
else log2.info(`[dry-run] Would create: ${rel}`);
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
const existed = await fs6.pathExists(outputPath);
|
|
482
|
+
await renderTemplate({
|
|
483
|
+
template: "component/Component.vue.hbs",
|
|
484
|
+
outputPath,
|
|
485
|
+
context,
|
|
486
|
+
templatesDir: config.templatesDir,
|
|
487
|
+
root,
|
|
488
|
+
overwrite: force
|
|
489
|
+
});
|
|
490
|
+
const result = {
|
|
491
|
+
command: "generate:component",
|
|
492
|
+
ok: true,
|
|
493
|
+
dryRun: false,
|
|
494
|
+
files: [{ template: "component/Component.vue.hbs", path: rel, status: existed && force ? "overwritten" : "created" }],
|
|
495
|
+
warnings: []
|
|
496
|
+
};
|
|
497
|
+
if (jsonMode) emitJson(result);
|
|
498
|
+
else outro2(`Created: ${rel}`);
|
|
499
|
+
return result;
|
|
500
|
+
} catch (err) {
|
|
501
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
502
|
+
if (jsonMode) {
|
|
503
|
+
emitJson({ command: "generate:component", ok: false, dryRun, files: [{ template: "component/Component.vue.hbs", path: rel, status: "exists", reason: message }], warnings: [], error: message });
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
throw err;
|
|
120
507
|
}
|
|
121
|
-
const context = {
|
|
122
|
-
name: kebabCase(componentName),
|
|
123
|
-
Name
|
|
124
|
-
};
|
|
125
|
-
const outputPath = path3.join(basePath, `${Name}.vue`);
|
|
126
|
-
const created = await renderTemplate({
|
|
127
|
-
template: "component/Component.vue.hbs",
|
|
128
|
-
outputPath,
|
|
129
|
-
context,
|
|
130
|
-
templatesDir: config.templatesDir,
|
|
131
|
-
root
|
|
132
|
-
});
|
|
133
|
-
outro(`Created: ${path3.relative(root, created)}`);
|
|
134
508
|
}
|
|
135
509
|
|
|
136
510
|
// src/commands/generate-composable.ts
|
|
137
|
-
import { cancel as cancel2, intro as
|
|
138
|
-
import
|
|
139
|
-
import
|
|
511
|
+
import { cancel as cancel2, intro as intro3, isCancel as isCancel2, log as log3, outro as outro3, text as text2 } from "@clack/prompts";
|
|
512
|
+
import fs7 from "fs-extra";
|
|
513
|
+
import path7 from "path";
|
|
140
514
|
async function generateComposable(name, options) {
|
|
141
|
-
|
|
515
|
+
const jsonMode = Boolean(options.json);
|
|
516
|
+
const dryRun = Boolean(options.dryRun);
|
|
517
|
+
const force = Boolean(options.force);
|
|
518
|
+
if (!jsonMode) intro3(`${dryRun ? "\u{1F50D} [dry-run] " : "\u{1F9E9} "}Generating composable: use${pascalCase(name)}`);
|
|
142
519
|
const root = process.cwd();
|
|
143
520
|
const config = await loadConfig(root);
|
|
144
521
|
let feature = options.feature;
|
|
145
522
|
if (feature === void 0) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
523
|
+
if (jsonMode) {
|
|
524
|
+
feature = "";
|
|
525
|
+
} else {
|
|
526
|
+
const answer = await text2({
|
|
527
|
+
message: "Which feature does this composable belong to? (leave empty for shared)",
|
|
528
|
+
placeholder: "e.g. acme",
|
|
529
|
+
defaultValue: ""
|
|
530
|
+
});
|
|
531
|
+
if (isCancel2(answer)) {
|
|
532
|
+
cancel2("Operation cancelled.");
|
|
533
|
+
process.exit(0);
|
|
534
|
+
}
|
|
535
|
+
feature = answer.trim();
|
|
154
536
|
}
|
|
155
|
-
feature = answer.trim();
|
|
156
537
|
}
|
|
157
|
-
const context = {
|
|
158
|
-
name: kebabCase(name),
|
|
159
|
-
Name: pascalCase(name)
|
|
160
|
-
};
|
|
538
|
+
const context = { name: kebabCase(name), Name: pascalCase(name) };
|
|
161
539
|
let basePath;
|
|
162
540
|
if (feature) {
|
|
163
541
|
feature = kebabCase(feature);
|
|
164
|
-
const featurePath =
|
|
165
|
-
if (!await
|
|
166
|
-
|
|
542
|
+
const featurePath = path7.join(root, config.featuresDir, feature);
|
|
543
|
+
if (!await fs7.pathExists(featurePath)) {
|
|
544
|
+
const msg = `Feature "${feature}" not found. Run "vf generate:feat ${feature}" first.`;
|
|
545
|
+
if (jsonMode) {
|
|
546
|
+
emitJson({ command: "generate:composable", ok: false, dryRun, files: [], warnings: [], error: msg });
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
log3.error(msg);
|
|
167
550
|
process.exit(1);
|
|
168
551
|
}
|
|
169
|
-
basePath =
|
|
552
|
+
basePath = path7.join(featurePath, "composables");
|
|
170
553
|
} else {
|
|
171
|
-
basePath =
|
|
554
|
+
basePath = path7.join(root, config.sharedDir, "composables");
|
|
555
|
+
}
|
|
556
|
+
const outputPath = path7.join(basePath, `use${pascalCase(name)}.ts`);
|
|
557
|
+
const rel = path7.relative(root, outputPath);
|
|
558
|
+
if (dryRun) {
|
|
559
|
+
const exists = await fs7.pathExists(outputPath);
|
|
560
|
+
const result = {
|
|
561
|
+
command: "generate:composable",
|
|
562
|
+
ok: true,
|
|
563
|
+
dryRun: true,
|
|
564
|
+
files: [{ template: "composable/composable.ts.hbs", path: rel, status: "dry-run", reason: exists ? force ? "would overwrite" : "already exists" : "would create" }],
|
|
565
|
+
warnings: []
|
|
566
|
+
};
|
|
567
|
+
if (jsonMode) emitJson(result);
|
|
568
|
+
else log3.info(`[dry-run] Would create: ${rel}`);
|
|
569
|
+
return result;
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const existed = await fs7.pathExists(outputPath);
|
|
573
|
+
await renderTemplate({
|
|
574
|
+
template: "composable/composable.ts.hbs",
|
|
575
|
+
outputPath,
|
|
576
|
+
context,
|
|
577
|
+
templatesDir: config.templatesDir,
|
|
578
|
+
root,
|
|
579
|
+
overwrite: force
|
|
580
|
+
});
|
|
581
|
+
const result = {
|
|
582
|
+
command: "generate:composable",
|
|
583
|
+
ok: true,
|
|
584
|
+
dryRun: false,
|
|
585
|
+
files: [{ template: "composable/composable.ts.hbs", path: rel, status: existed && force ? "overwritten" : "created" }],
|
|
586
|
+
warnings: []
|
|
587
|
+
};
|
|
588
|
+
if (jsonMode) emitJson(result);
|
|
589
|
+
else outro3(`Created: ${rel}`);
|
|
590
|
+
return result;
|
|
591
|
+
} catch (err) {
|
|
592
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
593
|
+
if (jsonMode) {
|
|
594
|
+
emitJson({ command: "generate:composable", ok: false, dryRun, files: [{ template: "composable/composable.ts.hbs", path: rel, status: "exists", reason: message }], warnings: [], error: message });
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
throw err;
|
|
172
598
|
}
|
|
173
|
-
const outputPath = path4.join(basePath, `use${pascalCase(name)}.ts`);
|
|
174
|
-
const created = await renderTemplate({
|
|
175
|
-
template: "composable/composable.ts.hbs",
|
|
176
|
-
outputPath,
|
|
177
|
-
context,
|
|
178
|
-
templatesDir: config.templatesDir,
|
|
179
|
-
root
|
|
180
|
-
});
|
|
181
|
-
outro2(`Created: ${path4.relative(root, created)}`);
|
|
182
599
|
}
|
|
183
600
|
|
|
184
601
|
// src/commands/generate-feat.ts
|
|
185
|
-
import { intro as
|
|
186
|
-
import
|
|
187
|
-
import
|
|
602
|
+
import { intro as intro4, log as log6, outro as outro4 } from "@clack/prompts";
|
|
603
|
+
import fs10 from "fs-extra";
|
|
604
|
+
import path10 from "path";
|
|
188
605
|
|
|
189
606
|
// src/utils/http-client.ts
|
|
190
|
-
import { log as
|
|
191
|
-
import
|
|
192
|
-
import
|
|
193
|
-
async function ensureHttpClient(root, config) {
|
|
194
|
-
const outputPath =
|
|
607
|
+
import { log as log4 } from "@clack/prompts";
|
|
608
|
+
import fs8 from "fs-extra";
|
|
609
|
+
import path8 from "path";
|
|
610
|
+
async function ensureHttpClient(root, config, opts = {}) {
|
|
611
|
+
const outputPath = path8.join(root, config.sharedDir, "http", "client.ts");
|
|
195
612
|
const template = config.httpClient === "axios" ? "shared/http-client-axios.ts.hbs" : "shared/http-client-fetch.ts.hbs";
|
|
613
|
+
const exists = await fs8.pathExists(outputPath);
|
|
614
|
+
if (exists && !opts.overwrite) {
|
|
615
|
+
if (!opts.silent && !opts.dryRun) log4.info(`HTTP client already exists at: ${path8.relative(root, outputPath)} (skipped)`);
|
|
616
|
+
return { path: outputPath, status: exists && opts.dryRun ? "dry-run" : "skipped" };
|
|
617
|
+
}
|
|
618
|
+
if (opts.dryRun) {
|
|
619
|
+
return { path: outputPath, status: exists ? "skipped" : "dry-run" };
|
|
620
|
+
}
|
|
196
621
|
const created = await renderTemplate({
|
|
197
622
|
template,
|
|
198
623
|
outputPath,
|
|
199
624
|
context: {},
|
|
200
|
-
skipIfExists:
|
|
625
|
+
skipIfExists: !opts.overwrite,
|
|
626
|
+
overwrite: opts.overwrite
|
|
201
627
|
});
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
log3.info(`HTTP client already exists at: ${path5.relative(root, outputPath)} (skipped)`);
|
|
628
|
+
if (!opts.silent) {
|
|
629
|
+
if (created) log4.success(`Created: ${path8.relative(root, created)}`);
|
|
630
|
+
else log4.info(`HTTP client already exists at: ${path8.relative(root, outputPath)} (skipped)`);
|
|
206
631
|
}
|
|
207
|
-
await ensureEnvExample(root);
|
|
632
|
+
await ensureEnvExample(root, opts.silent);
|
|
633
|
+
return { path: outputPath, status: exists ? "overwritten" : "created" };
|
|
208
634
|
}
|
|
209
|
-
async function ensureEnvExample(root) {
|
|
210
|
-
const envPath =
|
|
211
|
-
if (await
|
|
212
|
-
await
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// src/utils/package.ts
|
|
217
|
-
import fs6 from "fs-extra";
|
|
218
|
-
import path6 from "path";
|
|
219
|
-
async function hasDependency(root, name) {
|
|
220
|
-
const pkgPath = path6.join(root, "package.json");
|
|
221
|
-
if (!await fs6.pathExists(pkgPath)) return false;
|
|
222
|
-
const pkg = await fs6.readJson(pkgPath);
|
|
223
|
-
return Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
|
|
635
|
+
async function ensureEnvExample(root, silent) {
|
|
636
|
+
const envPath = path8.join(root, ".env.example");
|
|
637
|
+
if (await fs8.pathExists(envPath)) return;
|
|
638
|
+
await fs8.writeFile(envPath, "VITE_API_BASE_URL=http://localhost:3333\n", "utf-8");
|
|
639
|
+
if (!silent) log4.success(`Created: ${path8.relative(root, envPath)}`);
|
|
224
640
|
}
|
|
225
641
|
|
|
226
642
|
// src/utils/router.ts
|
|
227
|
-
import { log as
|
|
228
|
-
import
|
|
229
|
-
import
|
|
643
|
+
import { log as log5 } from "@clack/prompts";
|
|
644
|
+
import fs9 from "fs-extra";
|
|
645
|
+
import path9 from "path";
|
|
230
646
|
function insertAfterLastImport(content, importLine) {
|
|
231
647
|
const lines = content.split("\n");
|
|
232
648
|
let lastImportIdx = -1;
|
|
233
649
|
for (let i = 0; i < lines.length; i++) {
|
|
234
|
-
|
|
650
|
+
const line = lines[i];
|
|
651
|
+
if (line !== void 0 && line.trimStart().startsWith("import ")) lastImportIdx = i;
|
|
235
652
|
}
|
|
236
653
|
if (lastImportIdx === -1) return importLine + "\n" + content;
|
|
237
654
|
lines.splice(lastImportIdx + 1, 0, importLine);
|
|
@@ -273,98 +690,192 @@ ${routesIndent}` + content.slice(closeIdx);
|
|
|
273
690
|
return before + `${indent}${spread},
|
|
274
691
|
` + content.substring(lineStart);
|
|
275
692
|
}
|
|
276
|
-
async function registerRouteInRouter(root, config, featureName, nameCamel) {
|
|
277
|
-
const routerPath =
|
|
278
|
-
if (!await
|
|
279
|
-
|
|
280
|
-
|
|
693
|
+
async function registerRouteInRouter(root, config, featureName, nameCamel, opts = {}) {
|
|
694
|
+
const routerPath = path9.join(root, config.srcDir, "router", "index.ts");
|
|
695
|
+
if (!await fs9.pathExists(routerPath)) {
|
|
696
|
+
const reason = `Router not found at ${path9.relative(root, routerPath)} \u2014 skipping route registration.`;
|
|
697
|
+
if (!opts.silent) log5.warn(reason);
|
|
698
|
+
return { registered: false, reason };
|
|
281
699
|
}
|
|
282
|
-
let content = await
|
|
700
|
+
let content = await fs9.readFile(routerPath, "utf-8");
|
|
283
701
|
if (content.includes(`${nameCamel}Routes`)) {
|
|
284
|
-
|
|
285
|
-
|
|
702
|
+
const reason = `Route for "${featureName}" is already registered in the router (skipped).`;
|
|
703
|
+
if (!opts.silent) log5.info(reason);
|
|
704
|
+
return { registered: false, reason };
|
|
286
705
|
}
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
const
|
|
706
|
+
if (opts.dryRun) return { registered: false, reason: "dry-run: route would be registered" };
|
|
707
|
+
const routerDir = path9.dirname(routerPath);
|
|
708
|
+
const featureRoutesPath = path9.join(root, config.featuresDir, featureName, "routes");
|
|
709
|
+
const relImport = path9.relative(routerDir, featureRoutesPath).replace(/\\/g, "/");
|
|
290
710
|
const importLine = `import { ${nameCamel}Routes } from '${relImport.startsWith(".") ? relImport : `./${relImport}`}'`;
|
|
291
711
|
content = insertAfterLastImport(content, importLine);
|
|
292
712
|
content = insertBeforeRoutesClose(content, `...${nameCamel}Routes`);
|
|
293
|
-
await
|
|
294
|
-
|
|
713
|
+
await fs9.writeFile(routerPath, content, "utf-8");
|
|
714
|
+
if (!opts.silent) log5.success(`Registered route in ${path9.relative(root, routerPath)}`);
|
|
715
|
+
return { registered: true };
|
|
295
716
|
}
|
|
296
717
|
|
|
297
718
|
// src/commands/generate-feat.ts
|
|
298
719
|
async function generateFeat(name, options = {}) {
|
|
299
|
-
|
|
720
|
+
const jsonMode = Boolean(options.json);
|
|
721
|
+
const dryRun = Boolean(options.dryRun);
|
|
722
|
+
const force = Boolean(options.force);
|
|
723
|
+
if (!jsonMode) intro4(`${dryRun ? "\u{1F50D} [dry-run] " : "\u{1F680} "}Scaffolding feature: ${name}`);
|
|
300
724
|
const root = process.cwd();
|
|
301
725
|
const config = await loadConfig(root);
|
|
726
|
+
const warnings = [];
|
|
302
727
|
const featureName = kebabCase(name);
|
|
303
|
-
const
|
|
728
|
+
const PascalName = pascalCase(name);
|
|
729
|
+
const base = path10.join(root, config.featuresDir, featureName);
|
|
304
730
|
const context = {
|
|
305
731
|
name: featureName,
|
|
306
|
-
Name:
|
|
732
|
+
Name: PascalName,
|
|
307
733
|
nameCamel: camelCase(name),
|
|
308
734
|
alias: config.alias,
|
|
309
735
|
usesVueRouter: config.usesVueRouter
|
|
310
736
|
};
|
|
311
737
|
if (config.usesPinia && !await hasDependency(root, "pinia")) {
|
|
312
|
-
|
|
738
|
+
const msg = '"pinia" not found in package.json \u2014 run "npm install pinia" and set up createPinia() in main.ts before using the store.';
|
|
739
|
+
if (jsonMode) warnings.push(msg);
|
|
740
|
+
else log6.warn(msg);
|
|
741
|
+
}
|
|
742
|
+
let enabled;
|
|
743
|
+
try {
|
|
744
|
+
const resolved = resolveFeatLayers(config, options);
|
|
745
|
+
enabled = resolved.enabled;
|
|
746
|
+
warnings.push(...resolved.warnings);
|
|
747
|
+
} catch (err) {
|
|
748
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
749
|
+
if (jsonMode) {
|
|
750
|
+
emitJson({ command: "generate:feat", ok: false, dryRun, feature: featureName, files: [], warnings, error: message });
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
throw err;
|
|
754
|
+
}
|
|
755
|
+
if (enabled.size === 0) {
|
|
756
|
+
const msg = "No layers selected \u2014 nothing to generate. Check --only/--exclude/--no-* flags.";
|
|
757
|
+
if (jsonMode) {
|
|
758
|
+
emitJson({ command: "generate:feat", ok: false, dryRun, feature: featureName, files: [], warnings, error: msg });
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
throw new Error(msg);
|
|
313
762
|
}
|
|
314
763
|
const storeTemplate = config.usesPinia ? "feature/store.ts.hbs" : "feature/store-composable.ts.hbs";
|
|
315
764
|
const serviceTemplate = options.withCrud ? "feature/service-crud.ts.hbs" : "feature/service.ts.hbs";
|
|
316
765
|
const serviceComposableTemplate = options.withCrud ? "feature/service-composable-crud.ts.hbs" : "feature/service-composable.ts.hbs";
|
|
317
766
|
const pageComposableTemplate = options.withCrud ? "feature/page-composable-crud.ts.hbs" : "feature/page-composable.ts.hbs";
|
|
318
767
|
const typesTemplate = options.withCrud ? "feature/types-crud.ts.hbs" : "feature/types.ts.hbs";
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
768
|
+
const candidates = [];
|
|
769
|
+
if (enabled.has("service")) candidates.push({ layer: "service", template: serviceTemplate, out: path10.join(base, "services", `${featureName}.service.ts`) });
|
|
770
|
+
if (enabled.has("serviceComposable")) candidates.push({ layer: "serviceComposable", template: serviceComposableTemplate, out: path10.join(base, "composables", `use${PascalName}Service.ts`) });
|
|
771
|
+
if (enabled.has("pageComposable")) candidates.push({ layer: "pageComposable", template: pageComposableTemplate, out: path10.join(base, "composables", `use${PascalName}Page.ts`) });
|
|
772
|
+
if (enabled.has("store")) candidates.push({ layer: "store", template: storeTemplate, out: path10.join(base, "stores", `${featureName}.store.ts`) });
|
|
773
|
+
if (enabled.has("types")) candidates.push({ layer: "types", template: typesTemplate, out: path10.join(base, "types", `${featureName}.types.ts`) });
|
|
774
|
+
if (enabled.has("index")) candidates.push({ layer: "index", template: "feature/index.ts.hbs", out: path10.join(base, "index.ts") });
|
|
775
|
+
if (enabled.has("routes") && config.usesVueRouter) candidates.push({ layer: "routes", template: "feature/routes.ts.hbs", out: path10.join(base, "routes.ts") });
|
|
776
|
+
if (enabled.has("view") && config.usesVueRouter) candidates.push({ layer: "view", template: "feature/View.vue.hbs", out: path10.join(base, "views", `${PascalName}View.vue`) });
|
|
777
|
+
const files = [];
|
|
778
|
+
let failed = false;
|
|
779
|
+
for (const file of candidates) {
|
|
780
|
+
const rel = path10.relative(root, file.out);
|
|
781
|
+
if (dryRun) {
|
|
782
|
+
const exists = await fs10.pathExists(file.out);
|
|
783
|
+
files.push({ template: file.template, path: rel, status: "dry-run", reason: exists ? force ? "would overwrite" : "already exists" : "would create" });
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
try {
|
|
787
|
+
const existed = await fs10.pathExists(file.out);
|
|
788
|
+
await renderTemplate({
|
|
789
|
+
template: file.template,
|
|
790
|
+
outputPath: file.out,
|
|
791
|
+
context,
|
|
792
|
+
templatesDir: config.templatesDir,
|
|
793
|
+
root,
|
|
794
|
+
overwrite: force
|
|
795
|
+
});
|
|
796
|
+
files.push({ template: file.template, path: rel, status: existed && force ? "overwritten" : "created" });
|
|
797
|
+
if (!jsonMode) log6.success(`${existed && force ? "Overwrote" : "Created"}: ${rel}`);
|
|
798
|
+
} catch (err) {
|
|
799
|
+
failed = true;
|
|
800
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
801
|
+
files.push({ template: file.template, path: rel, status: "exists", reason: message });
|
|
802
|
+
if (!jsonMode) log6.error(message);
|
|
803
|
+
}
|
|
337
804
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
await
|
|
805
|
+
if (!dryRun) {
|
|
806
|
+
await fs10.ensureDir(path10.join(base, "components"));
|
|
807
|
+
if (!config.usesVueRouter) await fs10.ensureDir(path10.join(base, "views"));
|
|
808
|
+
const http = await ensureHttpClient(root, config, { dryRun, silent: jsonMode });
|
|
809
|
+
if (http.status !== "skipped") {
|
|
810
|
+
files.push({ template: config.httpClient === "axios" ? "shared/http-client-axios.ts.hbs" : "shared/http-client-fetch.ts.hbs", path: path10.relative(root, http.path), status: http.status });
|
|
811
|
+
}
|
|
812
|
+
} else {
|
|
813
|
+
const httpPath = path10.join(root, config.sharedDir, "http", "client.ts");
|
|
814
|
+
if (!await fs10.pathExists(httpPath)) {
|
|
815
|
+
files.push({ template: "shared/http-client-*.ts.hbs", path: path10.relative(root, httpPath), status: "dry-run", reason: "would create" });
|
|
816
|
+
}
|
|
341
817
|
}
|
|
342
|
-
|
|
818
|
+
let routeRegistered = false;
|
|
819
|
+
let routeReason;
|
|
343
820
|
if (options.registerRoute) {
|
|
344
821
|
if (!config.usesVueRouter) {
|
|
345
|
-
|
|
822
|
+
const msg = '--register-route requires "usesVueRouter: true" in vf.config.json (skipped).';
|
|
823
|
+
if (jsonMode) warnings.push(msg);
|
|
824
|
+
else log6.warn(msg);
|
|
825
|
+
routeReason = msg;
|
|
826
|
+
} else if (dryRun) {
|
|
827
|
+
routeReason = "dry-run: route would be registered";
|
|
346
828
|
} else {
|
|
347
|
-
await registerRouteInRouter(root, config, featureName, context.nameCamel);
|
|
829
|
+
const res = await registerRouteInRouter(root, config, featureName, context.nameCamel, { silent: jsonMode });
|
|
830
|
+
routeRegistered = res.registered;
|
|
831
|
+
routeReason = res.reason;
|
|
348
832
|
}
|
|
349
833
|
}
|
|
350
|
-
|
|
834
|
+
for (const w of warnings) {
|
|
835
|
+
if (!jsonMode) log6.warn(w);
|
|
836
|
+
}
|
|
837
|
+
const result = {
|
|
838
|
+
command: "generate:feat",
|
|
839
|
+
ok: !failed,
|
|
840
|
+
dryRun,
|
|
841
|
+
feature: featureName,
|
|
842
|
+
baseDir: path10.relative(root, base),
|
|
843
|
+
layers: [...enabled],
|
|
844
|
+
files,
|
|
845
|
+
warnings,
|
|
846
|
+
routeRegistered,
|
|
847
|
+
...routeReason ? { routeReason } : {}
|
|
848
|
+
};
|
|
849
|
+
if (jsonMode) {
|
|
850
|
+
emitJson(result);
|
|
851
|
+
if (failed) process.exit(1);
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
if (failed) throw new Error("Some files already exist (use --force to overwrite).");
|
|
855
|
+
outro4(`Feature "${featureName}" created at ${path10.relative(root, base)}`);
|
|
856
|
+
return result;
|
|
351
857
|
}
|
|
352
858
|
|
|
353
859
|
// src/commands/generate-service.ts
|
|
354
|
-
import { cancel as cancel3, intro as
|
|
355
|
-
import
|
|
356
|
-
import
|
|
860
|
+
import { cancel as cancel3, intro as intro5, isCancel as isCancel3, log as log7, outro as outro5, text as text3 } from "@clack/prompts";
|
|
861
|
+
import fs11 from "fs-extra";
|
|
862
|
+
import path11 from "path";
|
|
357
863
|
async function generateService(name, options) {
|
|
358
|
-
|
|
864
|
+
const jsonMode = Boolean(options.json);
|
|
865
|
+
const dryRun = Boolean(options.dryRun);
|
|
866
|
+
const force = Boolean(options.force);
|
|
867
|
+
if (!jsonMode) intro5(`${dryRun ? "\u{1F50D} [dry-run] " : "\u{1F50C} "}Generating service: ${kebabCase(name)}.service.ts`);
|
|
359
868
|
const root = process.cwd();
|
|
360
869
|
const config = await loadConfig(root);
|
|
361
|
-
await ensureHttpClient(root, config);
|
|
870
|
+
if (!dryRun) await ensureHttpClient(root, config, { silent: jsonMode, overwrite: force });
|
|
362
871
|
let feature = options.feature;
|
|
363
872
|
if (!feature) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
873
|
+
if (jsonMode) {
|
|
874
|
+
const msg = "Missing --feature. Usage: vf g:service <name> --feature <feature> [--json]";
|
|
875
|
+
emitJson({ command: "generate:service", ok: false, dryRun, files: [], warnings: [], error: msg });
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
const answer = await text3({ message: "Which feature does this service belong to?", placeholder: "e.g. acme" });
|
|
368
879
|
if (isCancel3(answer)) {
|
|
369
880
|
cancel3("Operation cancelled.");
|
|
370
881
|
process.exit(0);
|
|
@@ -372,19 +883,39 @@ async function generateService(name, options) {
|
|
|
372
883
|
feature = answer;
|
|
373
884
|
}
|
|
374
885
|
feature = kebabCase(feature);
|
|
375
|
-
const featurePath =
|
|
376
|
-
if (!await
|
|
377
|
-
|
|
886
|
+
const featurePath = path11.join(root, config.featuresDir, feature);
|
|
887
|
+
if (!await fs11.pathExists(featurePath)) {
|
|
888
|
+
const msg = `Feature "${feature}" not found. Run "vf generate:feat ${feature}" first.`;
|
|
889
|
+
if (jsonMode) {
|
|
890
|
+
emitJson({ command: "generate:service", ok: false, dryRun, files: [], warnings: [], error: msg });
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
log7.error(msg);
|
|
378
894
|
process.exit(1);
|
|
379
895
|
}
|
|
380
896
|
const resourceName = kebabCase(name);
|
|
381
|
-
const context = {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
897
|
+
const context = { name: resourceName, Name: pascalCase(name), nameCamel: camelCase(name), alias: config.alias };
|
|
898
|
+
const files = [];
|
|
899
|
+
const typesPath = path11.join(featurePath, "types", `${resourceName}.types.ts`);
|
|
900
|
+
const servicePath = path11.join(featurePath, "services", `${resourceName}.service.ts`);
|
|
901
|
+
if (dryRun) {
|
|
902
|
+
for (const [template, out] of [
|
|
903
|
+
["feature/types.ts.hbs", typesPath],
|
|
904
|
+
["feature/service.ts.hbs", servicePath]
|
|
905
|
+
]) {
|
|
906
|
+
const exists = await fs11.pathExists(out);
|
|
907
|
+
files.push({
|
|
908
|
+
template,
|
|
909
|
+
path: path11.relative(root, out),
|
|
910
|
+
status: "dry-run",
|
|
911
|
+
reason: template.startsWith("feature/types") && exists ? "would skip (already exists)" : exists ? force ? "would overwrite" : "already exists" : "would create"
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
const result2 = { command: "generate:service", ok: true, dryRun: true, files, warnings: [] };
|
|
915
|
+
if (jsonMode) emitJson(result2);
|
|
916
|
+
else for (const f of files) log7.info(`[dry-run] ${f.path} \u2014 ${f.reason}`);
|
|
917
|
+
return result2;
|
|
918
|
+
}
|
|
388
919
|
const typesCreated = await renderTemplate({
|
|
389
920
|
template: "feature/types.ts.hbs",
|
|
390
921
|
outputPath: typesPath,
|
|
@@ -394,35 +925,49 @@ async function generateService(name, options) {
|
|
|
394
925
|
root
|
|
395
926
|
});
|
|
396
927
|
if (typesCreated) {
|
|
397
|
-
|
|
928
|
+
files.push({ template: "feature/types.ts.hbs", path: path11.relative(root, typesCreated), status: "created" });
|
|
929
|
+
if (!jsonMode) log7.success(`Created: ${path11.relative(root, typesCreated)}`);
|
|
398
930
|
} else {
|
|
399
|
-
|
|
931
|
+
files.push({ template: "feature/types.ts.hbs", path: path11.relative(root, typesPath), status: "skipped", reason: "already exists" });
|
|
932
|
+
if (!jsonMode) log7.info(`Types already exist at: ${path11.relative(root, typesPath)} (skipped)`);
|
|
400
933
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
template: "feature/service.ts.hbs",
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
934
|
+
try {
|
|
935
|
+
const existed = await fs11.pathExists(servicePath);
|
|
936
|
+
await renderTemplate({ template: "feature/service.ts.hbs", outputPath: servicePath, context, templatesDir: config.templatesDir, root, overwrite: force });
|
|
937
|
+
files.push({ template: "feature/service.ts.hbs", path: path11.relative(root, servicePath), status: existed && force ? "overwritten" : "created" });
|
|
938
|
+
} catch (err) {
|
|
939
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
940
|
+
if (jsonMode) {
|
|
941
|
+
emitJson({ command: "generate:service", ok: false, dryRun, files, warnings: [], error: message });
|
|
942
|
+
process.exit(1);
|
|
943
|
+
}
|
|
944
|
+
throw err;
|
|
945
|
+
}
|
|
946
|
+
const result = { command: "generate:service", ok: true, dryRun: false, files, warnings: [] };
|
|
947
|
+
if (jsonMode) emitJson(result);
|
|
948
|
+
else outro5(`Created: ${path11.relative(root, servicePath)}`);
|
|
949
|
+
return result;
|
|
410
950
|
}
|
|
411
951
|
|
|
412
952
|
// src/commands/generate-store.ts
|
|
413
|
-
import { cancel as cancel4, intro as
|
|
414
|
-
import
|
|
415
|
-
import
|
|
953
|
+
import { cancel as cancel4, intro as intro6, isCancel as isCancel4, log as log8, outro as outro6, text as text4 } from "@clack/prompts";
|
|
954
|
+
import fs12 from "fs-extra";
|
|
955
|
+
import path12 from "path";
|
|
416
956
|
async function generateStore(name, options) {
|
|
417
|
-
|
|
957
|
+
const jsonMode = Boolean(options.json);
|
|
958
|
+
const dryRun = Boolean(options.dryRun);
|
|
959
|
+
const force = Boolean(options.force);
|
|
960
|
+
if (!jsonMode) intro6(`${dryRun ? "\u{1F50D} [dry-run] " : "\u{1F5C3}\uFE0F "}Generating store: ${kebabCase(name)}.store.ts`);
|
|
418
961
|
const root = process.cwd();
|
|
419
962
|
const config = await loadConfig(root);
|
|
420
963
|
let feature = options.feature;
|
|
421
964
|
if (!feature) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
965
|
+
if (jsonMode) {
|
|
966
|
+
const msg = "Missing --feature. Usage: vf g:store <name> --feature <feature> [--json]";
|
|
967
|
+
emitJson({ command: "generate:store", ok: false, dryRun, files: [], warnings: [], error: msg });
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
const answer = await text4({ message: "Which feature does this store belong to?", placeholder: "e.g. acme" });
|
|
426
971
|
if (isCancel4(answer)) {
|
|
427
972
|
cancel4("Operation cancelled.");
|
|
428
973
|
process.exit(0);
|
|
@@ -430,18 +975,47 @@ async function generateStore(name, options) {
|
|
|
430
975
|
feature = answer;
|
|
431
976
|
}
|
|
432
977
|
feature = kebabCase(feature);
|
|
433
|
-
const featurePath =
|
|
434
|
-
if (!await
|
|
435
|
-
|
|
978
|
+
const featurePath = path12.join(root, config.featuresDir, feature);
|
|
979
|
+
if (!await fs12.pathExists(featurePath)) {
|
|
980
|
+
const msg = `Feature "${feature}" not found. Run "vf generate:feat ${feature}" first.`;
|
|
981
|
+
if (jsonMode) {
|
|
982
|
+
emitJson({ command: "generate:store", ok: false, dryRun, files: [], warnings: [], error: msg });
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
log8.error(msg);
|
|
436
986
|
process.exit(1);
|
|
437
987
|
}
|
|
438
988
|
const resourceName = kebabCase(name);
|
|
439
|
-
const context = {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const typesPath =
|
|
989
|
+
const context = { name: resourceName, Name: pascalCase(name), nameCamel: camelCase(name) };
|
|
990
|
+
const warnings = [];
|
|
991
|
+
const storeTemplate = config.usesPinia ? "feature/store.ts.hbs" : "feature/store-composable.ts.hbs";
|
|
992
|
+
if (!config.usesPinia) warnings.push("Pinia not enabled in vf.config.json. Generating store with reactive() fallback.");
|
|
993
|
+
else if (!await hasDependency(root, "pinia")) warnings.push('"pinia" not found in package.json \u2014 run "npm install pinia" and set up createPinia() in main.ts.');
|
|
994
|
+
const typesPath = path12.join(featurePath, "types", `${resourceName}.types.ts`);
|
|
995
|
+
const storePath = path12.join(featurePath, "stores", `${resourceName}.store.ts`);
|
|
996
|
+
const files = [];
|
|
997
|
+
if (dryRun) {
|
|
998
|
+
for (const [template, out] of [
|
|
999
|
+
["feature/types.ts.hbs", typesPath],
|
|
1000
|
+
[storeTemplate, storePath]
|
|
1001
|
+
]) {
|
|
1002
|
+
const exists = await fs12.pathExists(out);
|
|
1003
|
+
files.push({
|
|
1004
|
+
template,
|
|
1005
|
+
path: path12.relative(root, out),
|
|
1006
|
+
status: "dry-run",
|
|
1007
|
+
reason: template.startsWith("feature/types") && exists ? "would skip (already exists)" : exists ? force ? "would overwrite" : "already exists" : "would create"
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
const result2 = { command: "generate:store", ok: true, dryRun: true, files, warnings };
|
|
1011
|
+
if (jsonMode) emitJson(result2);
|
|
1012
|
+
else {
|
|
1013
|
+
for (const w of warnings) log8.warn(w);
|
|
1014
|
+
for (const f of files) log8.info(`[dry-run] ${f.path} \u2014 ${f.reason}`);
|
|
1015
|
+
}
|
|
1016
|
+
return result2;
|
|
1017
|
+
}
|
|
1018
|
+
for (const w of warnings) if (!jsonMode) log8.warn(w);
|
|
445
1019
|
const typesCreated = await renderTemplate({
|
|
446
1020
|
template: "feature/types.ts.hbs",
|
|
447
1021
|
outputPath: typesPath,
|
|
@@ -451,25 +1025,28 @@ async function generateStore(name, options) {
|
|
|
451
1025
|
root
|
|
452
1026
|
});
|
|
453
1027
|
if (typesCreated) {
|
|
454
|
-
|
|
1028
|
+
files.push({ template: "feature/types.ts.hbs", path: path12.relative(root, typesCreated), status: "created" });
|
|
1029
|
+
if (!jsonMode) log8.success(`Created: ${path12.relative(root, typesCreated)}`);
|
|
455
1030
|
} else {
|
|
456
|
-
|
|
1031
|
+
files.push({ template: "feature/types.ts.hbs", path: path12.relative(root, typesPath), status: "skipped", reason: "already exists" });
|
|
1032
|
+
if (!jsonMode) log8.info(`Types already exist at: ${path12.relative(root, typesPath)} (skipped)`);
|
|
457
1033
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1034
|
+
try {
|
|
1035
|
+
const existed = await fs12.pathExists(storePath);
|
|
1036
|
+
await renderTemplate({ template: storeTemplate, outputPath: storePath, context, templatesDir: config.templatesDir, root, overwrite: force });
|
|
1037
|
+
files.push({ template: storeTemplate, path: path12.relative(root, storePath), status: existed && force ? "overwritten" : "created" });
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1040
|
+
if (jsonMode) {
|
|
1041
|
+
emitJson({ command: "generate:store", ok: false, dryRun, files, warnings, error: message });
|
|
1042
|
+
process.exit(1);
|
|
1043
|
+
}
|
|
1044
|
+
throw err;
|
|
463
1045
|
}
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
context,
|
|
469
|
-
templatesDir: config.templatesDir,
|
|
470
|
-
root
|
|
471
|
-
});
|
|
472
|
-
outro5(`Created: ${path10.relative(root, created)}`);
|
|
1046
|
+
const result = { command: "generate:store", ok: true, dryRun: false, files, warnings };
|
|
1047
|
+
if (jsonMode) emitJson(result);
|
|
1048
|
+
else outro6(`Created: ${path12.relative(root, storePath)}`);
|
|
1049
|
+
return result;
|
|
473
1050
|
}
|
|
474
1051
|
|
|
475
1052
|
// src/commands/help.ts
|
|
@@ -499,7 +1076,7 @@ var COMMAND_DOCS = {
|
|
|
499
1076
|
name: "generate:feat",
|
|
500
1077
|
alias: "g:feat",
|
|
501
1078
|
description: "Scaffold a complete feature module",
|
|
502
|
-
longDescription: "Generates the full structure of a feature (service, composables, store, types, barrel export and, when Vue Router is enabled, view and routes). Behavior is driven by vf.config.json \u2014 Pinia uses defineStore, without a router routes.ts and views/ are omitted.",
|
|
1079
|
+
longDescription: "Generates the full structure of a feature (service, composables, store, types, barrel export and, when Vue Router is enabled, view and routes). Behavior is driven by vf.config.json \u2014 Pinia uses defineStore, without a router routes.ts and views/ are omitted. Layers can be trimmed per-run with --only/--exclude/--no-* or per-project with config.layers. Every run supports --dry-run (preview) and --json (pure-JSON stdout for agents).",
|
|
503
1080
|
usage: "vf generate:feat <name> [options]",
|
|
504
1081
|
options: [
|
|
505
1082
|
{
|
|
@@ -511,28 +1088,59 @@ var COMMAND_DOCS = {
|
|
|
511
1088
|
flag: "--register-route",
|
|
512
1089
|
description: "Auto-register the feature route in the router (inserts import + spread into the routes array).",
|
|
513
1090
|
note: "Requires usesVueRouter: true in vf.config.json and src/router/index.ts to exist."
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
flag: "--only <layers>",
|
|
1094
|
+
description: "Only generate these layers (comma-separated).",
|
|
1095
|
+
note: "Layers: service, serviceComposable, pageComposable, store, types, index, routes, view (composables = both)."
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
flag: "--exclude <layers>",
|
|
1099
|
+
description: "Skip these layers (comma-separated, same layer names as --only)."
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
flag: "--no-store / --no-types / --no-service / --no-composables / --no-view / --no-routes / --no-index",
|
|
1103
|
+
description: "Convenience negations for a single layer."
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
flag: "--dry-run",
|
|
1107
|
+
description: "Preview planned files without writing anything."
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
flag: "--json",
|
|
1111
|
+
description: "Emit pure JSON { files[], warnings[] } on stdout (no spinners).",
|
|
1112
|
+
note: "Combine with --dry-run for agent previews."
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
flag: "--force",
|
|
1116
|
+
description: "Overwrite existing files instead of failing."
|
|
514
1117
|
}
|
|
515
1118
|
],
|
|
516
1119
|
examples: [
|
|
517
1120
|
{ cmd: "vf g:feat Product", note: "Basic feature without CRUD" },
|
|
518
1121
|
{ cmd: "vf g:feat Order --with-crud", note: "Feature with full CRUD methods" },
|
|
519
1122
|
{ cmd: "vf g:feat Category --with-crud --register-route", note: "CRUD + route auto-registered in the router" },
|
|
1123
|
+
{ cmd: "vf g:feat Billing --only service,store --json", note: "Partial feature (agent-friendly)" },
|
|
1124
|
+
{ cmd: "vf g:feat Billing --dry-run --json", note: "Preview without writing" },
|
|
520
1125
|
{ cmd: "vf generate:feat UserProfile", note: "Using the full command (equivalent to the alias)" }
|
|
521
1126
|
],
|
|
522
|
-
related: ["generate:service", "generate:store", "generate:composable"]
|
|
1127
|
+
related: ["generate:service", "generate:store", "generate:composable", "doctor", "agents:init"]
|
|
523
1128
|
},
|
|
524
1129
|
"generate:composable": {
|
|
525
1130
|
name: "generate:composable",
|
|
526
1131
|
alias: "g:composable",
|
|
527
1132
|
description: "Create a composable inside a feature or in shared",
|
|
528
|
-
longDescription: "Generates a TypeScript composable. If --feature is omitted, an interactive prompt lists existing features to choose from. Skipping the selection places the composable in src/shared/composables/.",
|
|
1133
|
+
longDescription: "Generates a TypeScript composable. If --feature is omitted, an interactive prompt lists existing features to choose from. Skipping the selection places the composable in src/shared/composables/. Supports --dry-run/--json/--force.",
|
|
529
1134
|
usage: "vf generate:composable <name> [options]",
|
|
530
1135
|
options: [
|
|
531
1136
|
{
|
|
532
1137
|
flag: "--feature <feature>",
|
|
533
1138
|
description: "Target feature for the composable.",
|
|
534
1139
|
note: "If omitted, the prompt lists available features. Leave blank to create in src/shared/composables/."
|
|
535
|
-
}
|
|
1140
|
+
},
|
|
1141
|
+
{ flag: "--dry-run", description: "Preview without writing." },
|
|
1142
|
+
{ flag: "--json", description: "Pure-JSON output for agents." },
|
|
1143
|
+
{ flag: "--force", description: "Overwrite existing files." }
|
|
536
1144
|
],
|
|
537
1145
|
examples: [
|
|
538
1146
|
{ cmd: "vf g:composable useFilters --feature product", note: "Composable inside the product feature" },
|
|
@@ -545,13 +1153,16 @@ var COMMAND_DOCS = {
|
|
|
545
1153
|
name: "generate:service",
|
|
546
1154
|
alias: "g:service",
|
|
547
1155
|
description: "Create a service inside an existing feature",
|
|
548
|
-
longDescription: "Adds a service file and its types file to an existing feature. Ensures the HTTP client is present at src/shared/http/client.ts before creating the service.",
|
|
1156
|
+
longDescription: "Adds a service file and its types file to an existing feature. Ensures the HTTP client is present at src/shared/http/client.ts before creating the service. Supports --dry-run/--json/--force.",
|
|
549
1157
|
usage: "vf generate:service <name> [options]",
|
|
550
1158
|
options: [
|
|
551
1159
|
{
|
|
552
1160
|
flag: "--feature <feature>",
|
|
553
1161
|
description: "Target feature (prompted interactively if omitted)."
|
|
554
|
-
}
|
|
1162
|
+
},
|
|
1163
|
+
{ flag: "--dry-run", description: "Preview without writing." },
|
|
1164
|
+
{ flag: "--json", description: "Pure-JSON output for agents." },
|
|
1165
|
+
{ flag: "--force", description: "Overwrite existing files." }
|
|
555
1166
|
],
|
|
556
1167
|
examples: [
|
|
557
1168
|
{ cmd: "vf g:service product --feature product", note: "Add a service to the product feature" },
|
|
@@ -563,13 +1174,16 @@ var COMMAND_DOCS = {
|
|
|
563
1174
|
name: "generate:component",
|
|
564
1175
|
alias: "g:component",
|
|
565
1176
|
description: "Create a Vue component inside a feature or in shared",
|
|
566
|
-
longDescription: "Generates a single-file component (.vue). Supports nested paths in the name to create subdirectories automatically. If --feature is omitted, the component is placed in src/shared/components/.",
|
|
1177
|
+
longDescription: "Generates a single-file component (.vue). Supports nested paths in the name to create subdirectories automatically. If --feature is omitted, the component is placed in src/shared/components/. Supports --dry-run/--json/--force.",
|
|
567
1178
|
usage: "vf generate:component <name> [options]",
|
|
568
1179
|
options: [
|
|
569
1180
|
{
|
|
570
1181
|
flag: "--feature <feature>",
|
|
571
1182
|
description: "Target feature. If omitted, the component is created in src/shared/components/."
|
|
572
|
-
}
|
|
1183
|
+
},
|
|
1184
|
+
{ flag: "--dry-run", description: "Preview without writing." },
|
|
1185
|
+
{ flag: "--json", description: "Pure-JSON output for agents." },
|
|
1186
|
+
{ flag: "--force", description: "Overwrite existing files." }
|
|
573
1187
|
],
|
|
574
1188
|
examples: [
|
|
575
1189
|
{ cmd: "vf g:component ProductCard --feature product", note: "Component inside the product feature" },
|
|
@@ -582,13 +1196,16 @@ var COMMAND_DOCS = {
|
|
|
582
1196
|
name: "generate:store",
|
|
583
1197
|
alias: "g:store",
|
|
584
1198
|
description: "Create a store inside an existing feature",
|
|
585
|
-
longDescription: "Adds a store to an existing feature. Uses defineStore (Pinia) when usesPinia: true in vf.config.json, or reactive() as a fallback when Pinia is not configured.",
|
|
1199
|
+
longDescription: "Adds a store to an existing feature. Uses defineStore (Pinia) when usesPinia: true in vf.config.json, or reactive() as a fallback when Pinia is not configured. Supports --dry-run/--json/--force.",
|
|
586
1200
|
usage: "vf generate:store <name> [options]",
|
|
587
1201
|
options: [
|
|
588
1202
|
{
|
|
589
1203
|
flag: "--feature <feature>",
|
|
590
1204
|
description: "Target feature (prompted interactively if omitted)."
|
|
591
|
-
}
|
|
1205
|
+
},
|
|
1206
|
+
{ flag: "--dry-run", description: "Preview without writing." },
|
|
1207
|
+
{ flag: "--json", description: "Pure-JSON output for agents." },
|
|
1208
|
+
{ flag: "--force", description: "Overwrite existing files." }
|
|
592
1209
|
],
|
|
593
1210
|
examples: [
|
|
594
1211
|
{ cmd: "vf g:store product --feature product", note: "Pinia store for the product feature" },
|
|
@@ -606,6 +1223,33 @@ var COMMAND_DOCS = {
|
|
|
606
1223
|
{ cmd: "vf templates:init", note: "Copy all templates to .vf/templates/" }
|
|
607
1224
|
],
|
|
608
1225
|
related: ["init"]
|
|
1226
|
+
},
|
|
1227
|
+
doctor: {
|
|
1228
|
+
name: "doctor",
|
|
1229
|
+
description: "Diagnose project config, deps and scaffolding readiness",
|
|
1230
|
+
longDescription: "Checks vf.config.json, required directories, tsconfig alias, installed deps vs config (Pinia, Vue Router, TanStack Query, axios), the shared http client and router file. Emits the built-in template inventory and variable names. Use --json for agent consumption.",
|
|
1231
|
+
usage: "vf doctor [--json]",
|
|
1232
|
+
options: [{ flag: "--json", description: "Pure-JSON output with checks[], config and templates." }],
|
|
1233
|
+
examples: [
|
|
1234
|
+
{ cmd: "vf doctor", note: "Human-readable diagnosis" },
|
|
1235
|
+
{ cmd: "vf doctor --json", note: "Machine-readable diagnosis for agents/CI" }
|
|
1236
|
+
],
|
|
1237
|
+
related: ["init", "generate:feat"]
|
|
1238
|
+
},
|
|
1239
|
+
"agents:init": {
|
|
1240
|
+
name: "agents:init",
|
|
1241
|
+
description: "Generate AGENTS.md with conventions for AI agents",
|
|
1242
|
+
longDescription: "Writes AGENTS.md at the project root with stack (from vf.config.json), layering rules, CLI contract (--dry-run/--json/--force, --only/--exclude) and template variables. Commit it so every agent follows the same conventions.",
|
|
1243
|
+
usage: "vf agents:init [--force] [--json]",
|
|
1244
|
+
options: [
|
|
1245
|
+
{ flag: "--force", description: "Overwrite existing AGENTS.md." },
|
|
1246
|
+
{ flag: "--json", description: "Pure-JSON output for agents." }
|
|
1247
|
+
],
|
|
1248
|
+
examples: [
|
|
1249
|
+
{ cmd: "vf agents:init", note: "Generate AGENTS.md" },
|
|
1250
|
+
{ cmd: "vf agents:init --force", note: "Regenerate after changing vf.config.json" }
|
|
1251
|
+
],
|
|
1252
|
+
related: ["doctor", "generate:feat"]
|
|
609
1253
|
}
|
|
610
1254
|
};
|
|
611
1255
|
var COMMAND_ALIASES = {
|
|
@@ -689,11 +1333,9 @@ async function help(command) {
|
|
|
689
1333
|
const choice = await select({
|
|
690
1334
|
message: "Open detailed help for a command?",
|
|
691
1335
|
options: [
|
|
692
|
-
...Object.values(COMMAND_DOCS).map(
|
|
693
|
-
value: d.name,
|
|
694
|
-
|
|
695
|
-
hint: d.alias ?? void 0
|
|
696
|
-
})),
|
|
1336
|
+
...Object.values(COMMAND_DOCS).map(
|
|
1337
|
+
(d) => d.alias ? { value: d.name, label: d.name, hint: d.alias } : { value: d.name, label: d.name }
|
|
1338
|
+
),
|
|
697
1339
|
{ value: "__exit__", label: "Exit" }
|
|
698
1340
|
]
|
|
699
1341
|
});
|
|
@@ -706,18 +1348,18 @@ async function help(command) {
|
|
|
706
1348
|
}
|
|
707
1349
|
|
|
708
1350
|
// src/commands/init.ts
|
|
709
|
-
import { cancel as cancel5, confirm, intro as
|
|
710
|
-
import
|
|
711
|
-
import
|
|
1351
|
+
import { cancel as cancel5, confirm, intro as intro7, isCancel as isCancel6, log as log9, outro as outro7, select as select2, text as text5 } from "@clack/prompts";
|
|
1352
|
+
import fs13 from "fs-extra";
|
|
1353
|
+
import path13 from "path";
|
|
712
1354
|
async function detectAlias(root) {
|
|
713
|
-
const tsconfigPath =
|
|
714
|
-
if (!await
|
|
1355
|
+
const tsconfigPath = path13.join(root, "tsconfig.json");
|
|
1356
|
+
if (!await fs13.pathExists(tsconfigPath)) return null;
|
|
715
1357
|
try {
|
|
716
|
-
const tsconfig = await
|
|
1358
|
+
const tsconfig = await fs13.readJson(tsconfigPath);
|
|
717
1359
|
const paths = tsconfig.compilerOptions?.paths ?? {};
|
|
718
1360
|
for (const key of Object.keys(paths)) {
|
|
719
1361
|
const match = key.match(/^(.+)\/\*$/);
|
|
720
|
-
if (match) return match[1];
|
|
1362
|
+
if (match?.[1]) return match[1];
|
|
721
1363
|
}
|
|
722
1364
|
} catch {
|
|
723
1365
|
return null;
|
|
@@ -733,10 +1375,10 @@ function exitIfCancelled(value) {
|
|
|
733
1375
|
}
|
|
734
1376
|
async function init(options = {}) {
|
|
735
1377
|
const { yes = false } = options;
|
|
736
|
-
|
|
1378
|
+
intro7("\u2699\uFE0F Configuring vue-feat-cli");
|
|
737
1379
|
const root = process.cwd();
|
|
738
|
-
const configPath =
|
|
739
|
-
if (await
|
|
1380
|
+
const configPath = path13.join(root, CONFIG_FILENAME);
|
|
1381
|
+
if (await fs13.pathExists(configPath)) {
|
|
740
1382
|
if (!yes) {
|
|
741
1383
|
const overwrite = exitIfCancelled(
|
|
742
1384
|
await confirm({
|
|
@@ -749,20 +1391,20 @@ async function init(options = {}) {
|
|
|
749
1391
|
process.exit(0);
|
|
750
1392
|
}
|
|
751
1393
|
} else {
|
|
752
|
-
|
|
1394
|
+
log9.info(`${CONFIG_FILENAME} already exists \u2014 overwriting (--yes).`);
|
|
753
1395
|
}
|
|
754
1396
|
}
|
|
755
|
-
|
|
1397
|
+
log9.step("Detecting project dependencies...");
|
|
756
1398
|
const hasPinia = await hasDependency(root, "pinia");
|
|
757
1399
|
const hasVueRouter = await hasDependency(root, "vue-router");
|
|
758
1400
|
const hasTanstackQuery = await hasDependency(root, "@tanstack/vue-query");
|
|
759
1401
|
const hasAxios = await hasDependency(root, "axios");
|
|
760
1402
|
const detectedAlias = await detectAlias(root);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1403
|
+
log9.info(`Pinia: ${hasPinia ? "\u2705 found" : "\u274C not found"}`);
|
|
1404
|
+
log9.info(`Vue Router: ${hasVueRouter ? "\u2705 found" : "\u274C not found"}`);
|
|
1405
|
+
log9.info(`TanStack Query: ${hasTanstackQuery ? "\u2705 found" : "\u274C not found"}`);
|
|
1406
|
+
log9.info(`Axios: ${hasAxios ? "\u2705 found" : "\u274C not found (will use fetch)"}`);
|
|
1407
|
+
log9.info(`Detected alias: ${detectedAlias ?? 'none (defaulting to "@")'}`);
|
|
766
1408
|
const usesPinia = yes ? hasPinia : exitIfCancelled(
|
|
767
1409
|
await confirm({ message: "Use Pinia for feature stores?", initialValue: hasPinia })
|
|
768
1410
|
);
|
|
@@ -810,23 +1452,23 @@ async function init(options = {}) {
|
|
|
810
1452
|
};
|
|
811
1453
|
await saveConfig(root, config);
|
|
812
1454
|
if (useCustomTemplates) {
|
|
813
|
-
await
|
|
814
|
-
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
await
|
|
818
|
-
await
|
|
819
|
-
await
|
|
820
|
-
await
|
|
821
|
-
await
|
|
1455
|
+
await fs13.ensureDir(path13.join(root, ".vf", "templates"));
|
|
1456
|
+
log9.info('Created .vf/templates/ \u2014 add .hbs files here to override defaults. Run "vf templates:init" to copy all defaults.');
|
|
1457
|
+
}
|
|
1458
|
+
log9.success(`Configuration saved to ${CONFIG_FILENAME}`);
|
|
1459
|
+
await fs13.ensureDir(path13.join(root, config.sharedDir, "components"));
|
|
1460
|
+
await fs13.ensureDir(path13.join(root, config.sharedDir, "composables"));
|
|
1461
|
+
await fs13.ensureDir(path13.join(root, config.sharedDir, "utils"));
|
|
1462
|
+
await fs13.ensureDir(path13.join(root, config.sharedDir, "http"));
|
|
1463
|
+
await fs13.ensureDir(path13.join(root, config.featuresDir));
|
|
822
1464
|
await ensureHttpClient(root, config);
|
|
823
|
-
|
|
1465
|
+
outro7('Done! Run "vf generate:feat <name>" to scaffold your first feature.');
|
|
824
1466
|
}
|
|
825
1467
|
|
|
826
1468
|
// src/commands/init-templates.ts
|
|
827
|
-
import { confirm as confirm2, intro as
|
|
828
|
-
import
|
|
829
|
-
import
|
|
1469
|
+
import { confirm as confirm2, intro as intro8, log as log10, outro as outro8 } from "@clack/prompts";
|
|
1470
|
+
import fs14 from "fs-extra";
|
|
1471
|
+
import path14 from "path";
|
|
830
1472
|
var CONTEXT_MD = `# Handlebars Context \u2014 vue-feat-cli templates
|
|
831
1473
|
|
|
832
1474
|
This file documents every variable available in each template group.
|
|
@@ -901,48 +1543,50 @@ built-in default automatically. You only need to include files you actually want
|
|
|
901
1543
|
to override \u2014 a partial set is perfectly valid.
|
|
902
1544
|
`;
|
|
903
1545
|
async function initTemplates() {
|
|
904
|
-
|
|
1546
|
+
intro8("\u{1F4C1} Initializing local templates");
|
|
905
1547
|
const root = process.cwd();
|
|
906
1548
|
const config = await loadConfig(root);
|
|
907
|
-
const dest =
|
|
908
|
-
const safeRoot =
|
|
1549
|
+
const dest = path14.resolve(root, config.templatesDir ?? ".vf/templates");
|
|
1550
|
+
const safeRoot = path14.resolve(root) + path14.sep;
|
|
909
1551
|
if (!dest.startsWith(safeRoot)) {
|
|
910
|
-
|
|
1552
|
+
log10.error(`templatesDir "${config.templatesDir}" resolves outside the project root \u2014 aborting.`);
|
|
911
1553
|
process.exit(1);
|
|
912
1554
|
}
|
|
913
|
-
const alreadyExists = await
|
|
1555
|
+
const alreadyExists = await fs14.pathExists(dest) && (await fs14.readdir(dest)).length > 0;
|
|
914
1556
|
if (alreadyExists) {
|
|
915
1557
|
const overwrite = await confirm2({
|
|
916
|
-
message: `${
|
|
1558
|
+
message: `${path14.relative(root, dest)} already has files. Overwrite?`,
|
|
917
1559
|
initialValue: false
|
|
918
1560
|
});
|
|
919
1561
|
if (!overwrite) {
|
|
920
|
-
|
|
1562
|
+
log10.warn("Aborted.");
|
|
921
1563
|
process.exit(0);
|
|
922
1564
|
}
|
|
923
1565
|
}
|
|
924
|
-
await
|
|
925
|
-
await
|
|
1566
|
+
await fs14.copy(TEMPLATES_DIR, dest, { overwrite: true });
|
|
1567
|
+
await fs14.writeFile(path14.join(dest, "CONTEXT.md"), CONTEXT_MD, "utf-8");
|
|
926
1568
|
if (!config.templatesDir) {
|
|
927
|
-
const cfg = await
|
|
928
|
-
await
|
|
929
|
-
|
|
1569
|
+
const cfg = await fs14.readJson(path14.join(root, CONFIG_FILENAME)).catch(() => ({ ...defaultConfig }));
|
|
1570
|
+
await fs14.writeJson(path14.join(root, CONFIG_FILENAME), { ...cfg, templatesDir: ".vf/templates" }, { spaces: 2 });
|
|
1571
|
+
log10.info(`Added "templatesDir": ".vf/templates" to ${CONFIG_FILENAME}`);
|
|
930
1572
|
}
|
|
931
|
-
const count = (await
|
|
932
|
-
|
|
933
|
-
|
|
1573
|
+
const count = (await fs14.readdir(dest, { recursive: true })).length;
|
|
1574
|
+
log10.info(`CONTEXT.md written \u2014 open it to see all available Handlebars variables.`);
|
|
1575
|
+
outro8(`Copied ${count} template files to ${path14.relative(root, dest)}/`);
|
|
934
1576
|
}
|
|
935
1577
|
|
|
936
1578
|
// src/cli.ts
|
|
937
1579
|
var cli = cac("vf");
|
|
938
|
-
cli.command("generate:feat <name>", "Scaffold a complete feature module").alias("g:feat").option("--with-crud", "Generate full CRUD methods in service and composables").option("--register-route", "Auto-register the route in the router file (requires usesVueRouter: true)").action(generateFeat);
|
|
939
|
-
cli.command("generate:composable <name>", "Create a composable inside a feature").alias("g:composable").option("--feature <feature>", "Feature to attach this to").action(generateComposable);
|
|
940
|
-
cli.command("generate:service <name>", "Create a service inside a feature").alias("g:service").option("--feature <feature>", "Feature to attach this to").action(generateService);
|
|
941
|
-
cli.command("generate:component <name>", "Create a component").alias("g:component").option("--feature <feature>", "Feature to attach this to (omit for shared)").action(generateComponent);
|
|
942
|
-
cli.command("generate:store <name>", "Create a Pinia store inside a feature").alias("g:store").option("--feature <feature>", "Feature to attach this to").action(generateStore);
|
|
1580
|
+
cli.command("generate:feat <name>", "Scaffold a complete feature module").alias("g:feat").option("--with-crud", "Generate full CRUD methods in service and composables").option("--register-route", "Auto-register the route in the router file (requires usesVueRouter: true)").option("--only <layers>", "Comma-separated layers to generate (e.g. service,store). Overrides defaults.").option("--exclude <layers>", "Comma-separated layers to skip").option("--no-store", "Skip the store layer").option("--no-types", "Skip the types layer").option("--no-service", "Skip the service layer").option("--no-composables", "Skip both service + page composables").option("--no-view", "Skip the view layer").option("--no-routes", "Skip the routes layer").option("--no-index", "Skip the barrel index.ts").option("--dry-run", "Preview files without writing").option("--json", "Machine-readable JSON output (pure stdout, for agents/CI)").option("--force", "Overwrite existing files").action(generateFeat);
|
|
1581
|
+
cli.command("generate:composable <name>", "Create a composable inside a feature").alias("g:composable").option("--feature <feature>", "Feature to attach this to").option("--dry-run", "Preview files without writing").option("--json", "Machine-readable JSON output").option("--force", "Overwrite existing files").action(generateComposable);
|
|
1582
|
+
cli.command("generate:service <name>", "Create a service inside a feature").alias("g:service").option("--feature <feature>", "Feature to attach this to").option("--dry-run", "Preview files without writing").option("--json", "Machine-readable JSON output").option("--force", "Overwrite existing files").action(generateService);
|
|
1583
|
+
cli.command("generate:component <name>", "Create a component").alias("g:component").option("--feature <feature>", "Feature to attach this to (omit for shared)").option("--dry-run", "Preview files without writing").option("--json", "Machine-readable JSON output").option("--force", "Overwrite existing files").action(generateComponent);
|
|
1584
|
+
cli.command("generate:store <name>", "Create a Pinia store inside a feature").alias("g:store").option("--feature <feature>", "Feature to attach this to").option("--dry-run", "Preview files without writing").option("--json", "Machine-readable JSON output").option("--force", "Overwrite existing files").action(generateStore);
|
|
943
1585
|
cli.command("init", "Configura o vue-feat-cli no projeto atual").option("--yes", "Accept all detected defaults without prompting (useful for CI)").action(init);
|
|
944
1586
|
cli.command("templates:init", "Copy default templates to .vf/templates/ for local customization").action(initTemplates);
|
|
1587
|
+
cli.command("doctor", "Diagnose project config, deps and scaffolding readiness").option("--json", "Machine-readable JSON output").action(doctor);
|
|
1588
|
+
cli.command("agents:init", "Generate AGENTS.md with conventions for AI agents").option("--force", "Overwrite existing AGENTS.md").option("--json", "Machine-readable JSON output").action(agentsInit);
|
|
945
1589
|
cli.command("help [command]", "Show detailed help for a command").action(help);
|
|
946
1590
|
cli.help();
|
|
947
|
-
cli.version("26.9.
|
|
1591
|
+
cli.version("26.9.2");
|
|
948
1592
|
cli.parse();
|