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.
Files changed (3) hide show
  1. package/README.md +47 -2
  2. package/dist/cli.js +932 -288
  3. 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/generate-component.ts
5
- import { cancel, intro, isCancel, log, outro, text } from "@clack/prompts";
6
- import fs3 from "fs-extra";
7
- import path3 from "path";
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/case.ts
37
- var pascalCase = (str) => str.replace(/(^\w|[-_]\w)/g, (m) => m.replace(/[-_]/, "").toUpperCase());
38
- var camelCase = (str) => {
39
- const p = pascalCase(str);
40
- return p.charAt(0).toLowerCase() + p.slice(1);
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
- var kebabCase = (str) => str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
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 fs2 from "fs-extra";
239
+ import fs4 from "fs-extra";
46
240
  import Handlebars from "handlebars";
47
- import path2 from "path";
241
+ import path4 from "path";
48
242
  import prettier from "prettier";
49
243
  import { fileURLToPath } from "url";
50
- var __dirname = path2.dirname(fileURLToPath(import.meta.url));
51
- var TEMPLATES_DIR = __dirname.endsWith("utils") ? path2.resolve(__dirname, "../templates") : path2.resolve(__dirname, "templates");
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 = path2.resolve(root) + path2.sep;
54
- const localPath = path2.resolve(root, templatesDir, template);
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 fs2.pathExists(localPath)) return localPath;
59
- return path2.join(TEMPLATES_DIR, template);
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) : path2.join(TEMPLATES_DIR, template);
70
- const raw = await fs2.readFile(templatePath, "utf-8");
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 fs2.ensureDir(path2.dirname(outputPath));
79
- if (await fs2.pathExists(outputPath)) {
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 fs2.writeFile(outputPath, output, "utf-8");
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(path3.sep);
425
+ const subPath = segments.join(path6.sep);
92
426
  const Name = pascalCase(componentName);
93
- intro(`\u{1F9F1} Generating component: ${Name}.vue`);
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
- const answer = await text({
99
- message: "Which feature does this component belong to? (leave empty for shared)",
100
- placeholder: "e.g. acme",
101
- defaultValue: ""
102
- });
103
- if (isCancel(answer)) {
104
- cancel("Operation cancelled.");
105
- process.exit(0);
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 = path3.join(root, config.featuresDir, feature);
113
- if (!await fs3.pathExists(featurePath)) {
114
- log.error(`Feature "${feature}" not found. Run "vf generate:feat ${feature}" first.`);
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 = path3.join(featurePath, "components", subPath);
460
+ basePath = path6.join(featurePath, "components", subPath);
118
461
  } else {
119
- basePath = path3.join(root, config.sharedDir, "components", subPath);
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 intro2, isCancel as isCancel2, log as log2, outro as outro2, text as text2 } from "@clack/prompts";
138
- import fs4 from "fs-extra";
139
- import path4 from "path";
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
- intro2(`\u{1F9E9} Generating composable: use${pascalCase(name)}`);
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
- const answer = await text2({
147
- message: "Which feature does this composable belong to? (leave empty for shared)",
148
- placeholder: "e.g. acme",
149
- defaultValue: ""
150
- });
151
- if (isCancel2(answer)) {
152
- cancel2("Operation cancelled.");
153
- process.exit(0);
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 = path4.join(root, config.featuresDir, feature);
165
- if (!await fs4.pathExists(featurePath)) {
166
- log2.error(`Feature "${feature}" not found. Run "vf generate:feat ${feature}" first.`);
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 = path4.join(featurePath, "composables");
552
+ basePath = path7.join(featurePath, "composables");
170
553
  } else {
171
- basePath = path4.join(root, config.sharedDir, "composables");
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 intro3, log as log5, outro as outro3 } from "@clack/prompts";
186
- import fs8 from "fs-extra";
187
- import path8 from "path";
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 log3 } from "@clack/prompts";
191
- import fs5 from "fs-extra";
192
- import path5 from "path";
193
- async function ensureHttpClient(root, config) {
194
- const outputPath = path5.join(root, config.sharedDir, "http", "client.ts");
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: true
625
+ skipIfExists: !opts.overwrite,
626
+ overwrite: opts.overwrite
201
627
  });
202
- if (created) {
203
- log3.success(`Created: ${path5.relative(root, created)}`);
204
- } else {
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 = path5.join(root, ".env.example");
211
- if (await fs5.pathExists(envPath)) return;
212
- await fs5.writeFile(envPath, "VITE_API_BASE_URL=http://localhost:3333\n", "utf-8");
213
- log3.success(`Created: ${path5.relative(root, envPath)}`);
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 log4 } from "@clack/prompts";
228
- import fs7 from "fs-extra";
229
- import path7 from "path";
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
- if (lines[i].trimStart().startsWith("import ")) lastImportIdx = i;
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 = path7.join(root, config.srcDir, "router", "index.ts");
278
- if (!await fs7.pathExists(routerPath)) {
279
- log4.warn(`Router not found at ${path7.relative(root, routerPath)} \u2014 skipping route registration.`);
280
- return;
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 fs7.readFile(routerPath, "utf-8");
700
+ let content = await fs9.readFile(routerPath, "utf-8");
283
701
  if (content.includes(`${nameCamel}Routes`)) {
284
- log4.info(`Route for "${featureName}" is already registered in the router (skipped).`);
285
- return;
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
- const routerDir = path7.dirname(routerPath);
288
- const featureRoutesPath = path7.join(root, config.featuresDir, featureName, "routes");
289
- const relImport = path7.relative(routerDir, featureRoutesPath).replace(/\\/g, "/");
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 fs7.writeFile(routerPath, content, "utf-8");
294
- log4.success(`Registered route in ${path7.relative(root, routerPath)}`);
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
- intro3(`\u{1F680} Scaffolding feature: ${name}`);
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 base = path8.join(root, config.featuresDir, featureName);
728
+ const PascalName = pascalCase(name);
729
+ const base = path10.join(root, config.featuresDir, featureName);
304
730
  const context = {
305
731
  name: featureName,
306
- Name: pascalCase(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
- log5.warn('"pinia" not found in package.json \u2014 run "npm install pinia" and set up createPinia() in main.ts before using the store.');
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 PascalName = pascalCase(name);
320
- const files = [
321
- { template: serviceTemplate, out: path8.join(base, "services", `${featureName}.service.ts`) },
322
- { template: serviceComposableTemplate, out: path8.join(base, "composables", `use${PascalName}Service.ts`) },
323
- { template: pageComposableTemplate, out: path8.join(base, "composables", `use${PascalName}Page.ts`) },
324
- { template: storeTemplate, out: path8.join(base, "stores", `${featureName}.store.ts`) },
325
- { template: typesTemplate, out: path8.join(base, "types", `${featureName}.types.ts`) },
326
- { template: "feature/index.ts.hbs", out: path8.join(base, "index.ts") }
327
- ];
328
- if (config.usesVueRouter) {
329
- files.push(
330
- { template: "feature/routes.ts.hbs", out: path8.join(base, "routes.ts") },
331
- { template: "feature/View.vue.hbs", out: path8.join(base, "views", `${PascalName}View.vue`) }
332
- );
333
- }
334
- for (const file of files) {
335
- const created = await renderTemplate({ ...file, outputPath: file.out, context, templatesDir: config.templatesDir, root });
336
- log5.success(`Created: ${path8.relative(root, created)}`);
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
- await fs8.ensureDir(path8.join(base, "components"));
339
- if (!config.usesVueRouter) {
340
- await fs8.ensureDir(path8.join(base, "views"));
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
- await ensureHttpClient(root, config);
818
+ let routeRegistered = false;
819
+ let routeReason;
343
820
  if (options.registerRoute) {
344
821
  if (!config.usesVueRouter) {
345
- log5.warn('--register-route requires "usesVueRouter: true" in vf.config.json (skipped).');
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
- outro3(`Feature "${featureName}" created at ${path8.relative(root, base)}`);
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 intro4, isCancel as isCancel3, log as log6, outro as outro4, text as text3 } from "@clack/prompts";
355
- import fs9 from "fs-extra";
356
- import path9 from "path";
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
- intro4(`\u{1F50C} Generating service: ${kebabCase(name)}.service.ts`);
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
- const answer = await text3({
365
- message: "Which feature does this service belong to?",
366
- placeholder: "e.g. acme"
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 = path9.join(root, config.featuresDir, feature);
376
- if (!await fs9.pathExists(featurePath)) {
377
- log6.error(`Feature "${feature}" not found. Run "vf generate:feat ${feature}" first.`);
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
- name: resourceName,
383
- Name: pascalCase(name),
384
- nameCamel: camelCase(name),
385
- alias: config.alias
386
- };
387
- const typesPath = path9.join(featurePath, "types", `${resourceName}.types.ts`);
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
- log6.success(`Created: ${path9.relative(root, typesCreated)}`);
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
- log6.info(`Types already exist at: ${path9.relative(root, typesPath)} (skipped)`);
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
- const servicePath = path9.join(featurePath, "services", `${resourceName}.service.ts`);
402
- const created = await renderTemplate({
403
- template: "feature/service.ts.hbs",
404
- outputPath: servicePath,
405
- context,
406
- templatesDir: config.templatesDir,
407
- root
408
- });
409
- outro4(`Created: ${path9.relative(root, created)}`);
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 intro5, isCancel as isCancel4, log as log7, outro as outro5, text as text4 } from "@clack/prompts";
414
- import fs10 from "fs-extra";
415
- import path10 from "path";
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
- intro5(`\u{1F5C3}\uFE0F Generating store: ${kebabCase(name)}.store.ts`);
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
- const answer = await text4({
423
- message: "Which feature does this store belong to?",
424
- placeholder: "e.g. acme"
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 = path10.join(root, config.featuresDir, feature);
434
- if (!await fs10.pathExists(featurePath)) {
435
- log7.error(`Feature "${feature}" not found. Run "vf generate:feat ${feature}" first.`);
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
- name: resourceName,
441
- Name: pascalCase(name),
442
- nameCamel: camelCase(name)
443
- };
444
- const typesPath = path10.join(featurePath, "types", `${resourceName}.types.ts`);
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
- log7.success(`Created: ${path10.relative(root, typesCreated)}`);
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
- log7.info(`Types already exist at: ${path10.relative(root, typesPath)} (skipped)`);
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
- const storeTemplate = config.usesPinia ? "feature/store.ts.hbs" : "feature/store-composable.ts.hbs";
459
- if (!config.usesPinia) {
460
- log7.warn("Pinia not enabled in vf.config.json. Generating store with reactive() fallback.");
461
- } else if (!await hasDependency(root, "pinia")) {
462
- log7.warn('"pinia" not found in package.json \u2014 run "npm install pinia" and set up createPinia() in main.ts.');
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 storePath = path10.join(featurePath, "stores", `${resourceName}.store.ts`);
465
- const created = await renderTemplate({
466
- template: storeTemplate,
467
- outputPath: storePath,
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((d) => ({
693
- value: d.name,
694
- label: d.name,
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 intro6, isCancel as isCancel6, log as log8, outro as outro6, select as select2, text as text5 } from "@clack/prompts";
710
- import fs11 from "fs-extra";
711
- import path11 from "path";
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 = path11.join(root, "tsconfig.json");
714
- if (!await fs11.pathExists(tsconfigPath)) return null;
1355
+ const tsconfigPath = path13.join(root, "tsconfig.json");
1356
+ if (!await fs13.pathExists(tsconfigPath)) return null;
715
1357
  try {
716
- const tsconfig = await fs11.readJson(tsconfigPath);
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
- intro6("\u2699\uFE0F Configuring vue-feat-cli");
1378
+ intro7("\u2699\uFE0F Configuring vue-feat-cli");
737
1379
  const root = process.cwd();
738
- const configPath = path11.join(root, CONFIG_FILENAME);
739
- if (await fs11.pathExists(configPath)) {
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
- log8.info(`${CONFIG_FILENAME} already exists \u2014 overwriting (--yes).`);
1394
+ log9.info(`${CONFIG_FILENAME} already exists \u2014 overwriting (--yes).`);
753
1395
  }
754
1396
  }
755
- log8.step("Detecting project dependencies...");
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
- log8.info(`Pinia: ${hasPinia ? "\u2705 found" : "\u274C not found"}`);
762
- log8.info(`Vue Router: ${hasVueRouter ? "\u2705 found" : "\u274C not found"}`);
763
- log8.info(`TanStack Query: ${hasTanstackQuery ? "\u2705 found" : "\u274C not found"}`);
764
- log8.info(`Axios: ${hasAxios ? "\u2705 found" : "\u274C not found (will use fetch)"}`);
765
- log8.info(`Detected alias: ${detectedAlias ?? 'none (defaulting to "@")'}`);
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 fs11.ensureDir(path11.join(root, ".vf", "templates"));
814
- log8.info('Created .vf/templates/ \u2014 add .hbs files here to override defaults. Run "vf templates:init" to copy all defaults.');
815
- }
816
- log8.success(`Configuration saved to ${CONFIG_FILENAME}`);
817
- await fs11.ensureDir(path11.join(root, config.sharedDir, "components"));
818
- await fs11.ensureDir(path11.join(root, config.sharedDir, "composables"));
819
- await fs11.ensureDir(path11.join(root, config.sharedDir, "utils"));
820
- await fs11.ensureDir(path11.join(root, config.sharedDir, "http"));
821
- await fs11.ensureDir(path11.join(root, config.featuresDir));
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
- outro6('Done! Run "vf generate:feat <name>" to scaffold your first feature.');
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 intro7, log as log9, outro as outro7 } from "@clack/prompts";
828
- import fs12 from "fs-extra";
829
- import path12 from "path";
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
- intro7("\u{1F4C1} Initializing local templates");
1546
+ intro8("\u{1F4C1} Initializing local templates");
905
1547
  const root = process.cwd();
906
1548
  const config = await loadConfig(root);
907
- const dest = path12.resolve(root, config.templatesDir ?? ".vf/templates");
908
- const safeRoot = path12.resolve(root) + path12.sep;
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
- log9.error(`templatesDir "${config.templatesDir}" resolves outside the project root \u2014 aborting.`);
1552
+ log10.error(`templatesDir "${config.templatesDir}" resolves outside the project root \u2014 aborting.`);
911
1553
  process.exit(1);
912
1554
  }
913
- const alreadyExists = await fs12.pathExists(dest) && (await fs12.readdir(dest)).length > 0;
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: `${path12.relative(root, dest)} already has files. Overwrite?`,
1558
+ message: `${path14.relative(root, dest)} already has files. Overwrite?`,
917
1559
  initialValue: false
918
1560
  });
919
1561
  if (!overwrite) {
920
- log9.warn("Aborted.");
1562
+ log10.warn("Aborted.");
921
1563
  process.exit(0);
922
1564
  }
923
1565
  }
924
- await fs12.copy(TEMPLATES_DIR, dest, { overwrite: true });
925
- await fs12.writeFile(path12.join(dest, "CONTEXT.md"), CONTEXT_MD, "utf-8");
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 fs12.readJson(path12.join(root, CONFIG_FILENAME)).catch(() => ({ ...defaultConfig }));
928
- await fs12.writeJson(path12.join(root, CONFIG_FILENAME), { ...cfg, templatesDir: ".vf/templates" }, { spaces: 2 });
929
- log9.info(`Added "templatesDir": ".vf/templates" to ${CONFIG_FILENAME}`);
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 fs12.readdir(dest, { recursive: true })).length;
932
- log9.info(`CONTEXT.md written \u2014 open it to see all available Handlebars variables.`);
933
- outro7(`Copied ${count} template files to ${path12.relative(root, dest)}/`);
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.0");
1591
+ cli.version("26.9.2");
948
1592
  cli.parse();