vstruct-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AIREF.md ADDED
@@ -0,0 +1,479 @@
1
+ # vstruct — AIREF (referencia para IAs y devs)
2
+
3
+ Estructura Vue 3 predecible. Todo tiene UN lugar fijo. Cero ambigüedad.
4
+ La IA y el dev saben exactamente dónde va cada cosa SIN PENSAR.
5
+
6
+ ---
7
+
8
+ ## STACK OBLIGATORIO
9
+
10
+ | Dep | Versión | Por qué |
11
+ |-----|---------|---------|
12
+ | `vue` | `^3.4` | `<script setup>` + Composition API tipada |
13
+ | `vite` | `^5.4` | Dev server + build |
14
+ | `pinia` | `^2.1` | Stores con setup syntax (NO options API) |
15
+ | `vue-router` | `^4.3` | Router con `createWebHistory` |
16
+ | `typescript` | `^5.0` | `strict: true` obligatorio |
17
+
18
+ ---
19
+
20
+ ## TABLA CANÓNICA — dónde va cada cosa
21
+
22
+ ```
23
+ TIPOS DE ARCHIVO:
24
+
25
+ Componente UI → src/components/ui/<PascalCase>.vue
26
+ Ej: Button.vue, InputField.vue, DataTable.vue
27
+ Son componentes genéricos, reusables, sin lógica de negocio.
28
+
29
+ Componente Feature → src/components/features/<PascalCase>.vue
30
+ Ej: UserCard.vue, ProductForm.vue, OrderTimeline.vue
31
+ Son componentes de dominio, acoplados a la lógica del negocio.
32
+
33
+ Página → src/pages/<kebab-case>.vue
34
+ Ej: user-list.vue, product-detail.vue, order-create.vue
35
+ Una página por ruta. Representan una vista completa.
36
+
37
+ Layout → src/layouts/<PascalCase>.vue
38
+ Ej: DefaultLayout.vue, AdminLayout.vue, AuthLayout.vue
39
+ Contienen <slot /> para el contenido de la página.
40
+
41
+ Store (Pinia) → src/stores/<camelCase>.store.ts
42
+ Ej: auth.store.ts, cart.store.ts, toast.store.ts
43
+ Estado global. Siempre setup syntax (() => { ... }).
44
+
45
+ Service (API) → src/services/<PascalCase>.service.ts
46
+ Ej: ProductService.ts, AuthService.ts, OrderService.ts
47
+ Solo llamadas HTTP. NUNCA estado global ni stores.
48
+
49
+ Composable → src/composables/use<Xxx>.ts
50
+ Ej: useFetch.ts, usePagination.ts, useFormValidation.ts
51
+ Lógica reutilizable. SIEMPRE empieza con "use".
52
+
53
+ Tipos → src/types/index.ts
54
+ Interfaces, tipos y DTOs compartidos. NO duplicar en archivos.
55
+
56
+ Router → src/router/index.ts
57
+ Configuración de rutas. Un solo archivo.
58
+
59
+ Estilos → src/styles/main.css
60
+ Estilos globales. Componentes usan <style scoped>.
61
+ ```
62
+
63
+ ---
64
+
65
+ ## REGLAS INMUTABLES — la IA NO debe romperlas
66
+
67
+ | # | Regla | Por qué |
68
+ |---|-------|---------|
69
+ | 1 | Componentes en `components/`, NO en `pages/` ni `layouts/` | Separación de concerns rota = código inmantenible |
70
+ | 2 | Stores en `stores/`, NO en `components/` ni `services/` | Estado global mezclado con UI = bugs de reactividad |
71
+ | 3 | NUNCA `fetch()` directo en componentes | Rompe testabilidad, caching, y separación de capas |
72
+ | 4 | NUNCA `<a href="/ruta">` para navegación interna | Recarga la página. Usar `<router-link>` o `router.push()` |
73
+ | 5 | NUNCA `<script>` sin `lang="ts"` | TypeScript es obligatorio. Sin `lang="ts"` no hay type safety |
74
+ | 6 | NUNCA estilos sin `<style scoped>` en componentes | Contamina otros componentes. Si necesitás global → `main.css` |
75
+ | 7 | Store NO llama a `router` | Circular dependency. El componente decide navegar, no el store |
76
+ | 8 | Service NO importa stores | Los services son puros HTTP. El store orquesta service + estado |
77
+ | 9 | Tipos compartidos en `types/index.ts` | Evita duplicación de interfaces entre archivos |
78
+ | 10 | NUNCA `any` sin justificación | `any` desactiva TypeScript. Usar `unknown` + type guard |
79
+
80
+ ---
81
+
82
+ ## BUENAS vs MALAS PRÁCTICAS — ejemplos concretos
83
+
84
+ ### Componentes
85
+
86
+ | ✅ BIEN | ❌ MAL (causa errores) |
87
+ |---------|------------------------|
88
+ | `<router-link to="/users">` | `<a href="/users">` — recarga la SPA |
89
+ | `<script setup lang="ts">` | `<script>` sin lang="ts" — sin type checking |
90
+ | `<style scoped>` | `<style>` sin scoped — contamina otros componentes |
91
+ | `defineProps<{ name: string }>()` | `defineProps({ name: String })` — sin tipos TS |
92
+ | `defineEmits<{ select: [id: string] }>()` | `emit('select', id)` sin declarar emits |
93
+ | `const visible = ref(false)` | `let visible = false` — no reactivo |
94
+ | `v-if="item"` | `v-if="item != null"` component |
95
+ | `<slot />` en layout | hardcodear contenido en layout |
96
+
97
+ ### Stores (Pinia)
98
+
99
+ | ✅ BIEN | ❌ MAL (causa errores) |
100
+ |---------|------------------------|
101
+ | `defineStore('auth', () => { ... })` setup syntax | `defineStore('auth', { state: ... })` options API |
102
+ | `const user = ref<User \| null>(null)` | `const user = ref()` sin tipo |
103
+ | Actions async con try/catch | Actions sin manejo de error |
104
+ | Store importa Service | Store hace `fetch()` directo |
105
+ | Store retorna `{ state, getters, actions }` | Store expone todo sin estructura |
106
+ | `return { user, login, logout }` | Store no retorna lo que el componente necesita |
107
+
108
+ ### Services (API)
109
+
110
+ | ✅ BIEN | ❌ MAL (causa errores) |
111
+ |---------|------------------------|
112
+ | `const BASE_URL = '/api/products'` constante | URL hardcodeada en cada método |
113
+ | `if (!res.ok) throw new Error(...)` | `return res.json()` sin checkear errores |
114
+ | `async getAll(): Promise<Product[]>` | `async getAll()` sin tipo de retorno |
115
+ | Service exporta objeto con métodos | Service es una clase con `this` |
116
+ | Service usa `fetch()` (único lugar permitido) | Service importa pinia o vue-router |
117
+
118
+ ### Composables
119
+
120
+ | ✅ BIEN | ❌ MAL (causa errores) |
121
+ |---------|------------------------|
122
+ | `useFetch`, `useAuth`, `usePagination` | `fetchData`, `auth`, `paginate` — sin prefijo "use" |
123
+ | Retorna `{ data, loading, error, execute }` | Retorna solo `data` sin estado de carga |
124
+ | Tipado genérico: `useFetch<T>(...)` | `useFetch()` sin genérico — `data` es `any` |
125
+ | Maneja loading + error + success | Solo maneja success |
126
+ | `error.value = e.message` en catch | Error silencioso — no se asigna nunca |
127
+
128
+ ### Routing
129
+
130
+ | ✅ BIEN | ❌ MAL (causa errores) |
131
+ |---------|------------------------|
132
+ | `router.push({ name: 'user', params: { id } })` | `router.push('/user/' + id)` — string concatenation |
133
+ | `createWebHistory()` | `createWebHashHistory()` — URLs con # |
134
+ | Rutas con `name` único | Rutas sin `name` |
135
+ | `<router-link :to="{ name: 'home' }">` | `<a href="/">` — full page reload |
136
+ | Lazy load: `() => import('@/pages/X.vue')` | Import directo — bundle enorme |
137
+
138
+ ### Estructura de archivos
139
+
140
+ | ✅ BIEN | ❌ MAL (causa errores) |
141
+ |---------|------------------------|
142
+ | `stores/auth.store.ts` | `store/auth.ts` sin sufijo `.store` |
143
+ | `services/Product.service.ts` | `services/product.ts` sin sufijo `.service` |
144
+ | `composables/useFetch.ts` | `composables/fetch.ts` sin prefijo `use` |
145
+ | `components/ui/Button.vue` | `components/ui/button.vue` — PascalCase para componentes |
146
+ | `pages/user-list.vue` | `pages/UserList.vue` — kebab-case para páginas |
147
+ | `types/index.ts` centralizado | Tipos duplicados en cada archivo |
148
+
149
+ ---
150
+
151
+ ## PATRONES CANÓNICOS — copy-paste
152
+
153
+ ### Componente Vue (.vue)
154
+
155
+ ```vue
156
+ <template>
157
+ <div class="user-card">
158
+ <h3>{{ props.name }}</h3>
159
+ <p v-if="props.email">{{ props.email }}</p>
160
+ <button @click="emit('select', props.id)">Seleccionar</button>
161
+ </div>
162
+ </template>
163
+
164
+ <script setup lang="ts">
165
+ interface Props {
166
+ id: string
167
+ name: string
168
+ email?: string
169
+ }
170
+
171
+ interface Emits {
172
+ (e: 'select', id: string): void
173
+ }
174
+
175
+ const props = defineProps<Props>()
176
+ const emit = defineEmits<Emits>()
177
+ </script>
178
+
179
+ <style scoped>
180
+ .user-card {
181
+ padding: 1rem;
182
+ border: 1px solid #e5e5e5;
183
+ border-radius: 8px;
184
+ }
185
+ </style>
186
+ ```
187
+
188
+ ### Página con datos y loading
189
+
190
+ ```vue
191
+ <template>
192
+ <div class="page-product-detail">
193
+ <div v-if="loading" class="loading">Cargando...</div>
194
+ <div v-else-if="error" class="error">{{ error }}</div>
195
+ <div v-else-if="product">
196
+ <h1>{{ product.name }}</h1>
197
+ <p>{{ product.description }}</p>
198
+ </div>
199
+ </div>
200
+ </template>
201
+
202
+ <script setup lang="ts">
203
+ import { ref, onMounted } from 'vue'
204
+ import { useRoute } from 'vue-router'
205
+ import { ProductService } from '@/services/Product.service'
206
+ import type { Product } from '@/types'
207
+
208
+ const route = useRoute()
209
+ const product = ref<Product | null>(null)
210
+ const loading = ref(false)
211
+ const error = ref<string | null>(null)
212
+
213
+ onMounted(async () => {
214
+ loading.value = true
215
+ error.value = null
216
+ try {
217
+ product.value = await ProductService.getById(route.params.id as string)
218
+ } catch (e) {
219
+ error.value = e instanceof Error ? e.message : 'Error al cargar'
220
+ } finally {
221
+ loading.value = false
222
+ }
223
+ })
224
+ </script>
225
+ ```
226
+
227
+ ### Store (Pinia) con setup syntax
228
+
229
+ ```typescript
230
+ // src/stores/auth.store.ts
231
+ import { defineStore } from 'pinia'
232
+ import { ref, computed } from 'vue'
233
+ import { AuthService } from '@/services/Auth.service'
234
+ import type { User } from '@/types'
235
+
236
+ export const useAuthStore = defineStore('auth', () => {
237
+ const user = ref<User | null>(null)
238
+ const token = ref<string | null>(null)
239
+ const loading = ref(false)
240
+
241
+ const isAuthenticated = computed(() => !!token.value)
242
+
243
+ async function login(email: string, password: string) {
244
+ loading.value = true
245
+ try {
246
+ const data = await AuthService.login({ email, password })
247
+ token.value = data.token
248
+ user.value = data.user
249
+ } finally {
250
+ loading.value = false
251
+ }
252
+ }
253
+
254
+ function logout() {
255
+ token.value = null
256
+ user.value = null
257
+ }
258
+
259
+ return { user, token, loading, isAuthenticated, login, logout }
260
+ })
261
+ ```
262
+
263
+ ### Service (API) — único lugar donde se usa fetch()
264
+
265
+ ```typescript
266
+ // src/services/Product.service.ts
267
+ import type { Product, CreateProductDTO } from '@/types'
268
+
269
+ const BASE_URL = '/api/products'
270
+
271
+ export const ProductService = {
272
+ async getAll(): Promise<Product[]> {
273
+ const res = await fetch(BASE_URL)
274
+ if (!res.ok) throw new Error(`GET ${BASE_URL} failed: ${res.status}`)
275
+ return res.json()
276
+ },
277
+
278
+ async getById(id: string): Promise<Product> {
279
+ const res = await fetch(`${BASE_URL}/${id}`)
280
+ if (!res.ok) throw new Error(`Product ${id} not found`)
281
+ return res.json()
282
+ },
283
+
284
+ async create(data: CreateProductDTO): Promise<Product> {
285
+ const res = await fetch(BASE_URL, {
286
+ method: 'POST',
287
+ headers: { 'Content-Type': 'application/json' },
288
+ body: JSON.stringify(data),
289
+ })
290
+ if (!res.ok) throw new Error(`Create failed: ${res.status}`)
291
+ return res.json()
292
+ },
293
+
294
+ async update(id: string, data: Partial<CreateProductDTO>): Promise<Product> {
295
+ const res = await fetch(`${BASE_URL}/${id}`, {
296
+ method: 'PATCH',
297
+ headers: { 'Content-Type': 'application/json' },
298
+ body: JSON.stringify(data),
299
+ })
300
+ if (!res.ok) throw new Error(`Update ${id} failed: ${res.status}`)
301
+ return res.json()
302
+ },
303
+
304
+ async delete(id: string): Promise<void> {
305
+ const res = await fetch(`${BASE_URL}/${id}`, { method: 'DELETE' })
306
+ if (!res.ok) throw new Error(`Delete ${id} failed: ${res.status}`)
307
+ },
308
+ }
309
+ ```
310
+
311
+ ### Composable
312
+
313
+ ```typescript
314
+ // src/composables/useFetch.ts
315
+ import { ref, type Ref } from 'vue'
316
+
317
+ interface UseFetchReturn<T> {
318
+ data: Ref<T | null>
319
+ loading: Ref<boolean>
320
+ error: Ref<string | null>
321
+ execute: () => Promise<void>
322
+ }
323
+
324
+ export function useFetch<T>(loader: () => Promise<T>): UseFetchReturn<T> {
325
+ const data = ref<T | null>(null) as Ref<T | null>
326
+ const loading = ref(false)
327
+ const error = ref<string | null>(null)
328
+
329
+ async function execute() {
330
+ loading.value = true
331
+ error.value = null
332
+ try {
333
+ data.value = await loader()
334
+ } catch (e) {
335
+ error.value = e instanceof Error ? e.message : 'Error desconocido'
336
+ } finally {
337
+ loading.value = false
338
+ }
339
+ }
340
+
341
+ return { data, loading, error, execute }
342
+ }
343
+ ```
344
+
345
+ ### Layout con slot
346
+
347
+ ```vue
348
+ <template>
349
+ <div class="layout-admin">
350
+ <aside class="sidebar">
351
+ <nav>
352
+ <router-link to="/dashboard">Dashboard</router-link>
353
+ <router-link to="/users">Usuarios</router-link>
354
+ </nav>
355
+ </aside>
356
+ <main class="content">
357
+ <slot />
358
+ </main>
359
+ </div>
360
+ </template>
361
+
362
+ <script setup lang="ts">
363
+ // AdminLayout — no necesita lógica, solo estructura
364
+ </script>
365
+
366
+ <style scoped>
367
+ .layout-admin { display: flex; min-height: 100dvh; }
368
+ .sidebar { width: 250px; border-right: 1px solid #e5e5e5; padding: 1rem; }
369
+ .content { flex: 1; padding: 2rem; }
370
+ </style>
371
+ ```
372
+
373
+ ---
374
+
375
+ ## ANTIPATRONES QUE CAUSAN BUGS — NUNCA hacer esto
376
+
377
+ ```
378
+ ❌ fetch('/api/users') dentro de <script setup> de un .vue
379
+ → Crear Product.service.ts y llamar ProductService.getAll()
380
+
381
+ ❌ import { useRouter } from 'vue-router' dentro de un store
382
+ → El componente hace router.push() DESPUÉS de llamar al store
383
+
384
+ ❌ <a href="/dashboard">Ir</a>
385
+ → <router-link to="/dashboard">Ir</router-link>
386
+
387
+ ❌ const state = reactive({ count: 0 }) sin tipo
388
+ → const count = ref<number>(0)
389
+
390
+ ❌ <style> sin scoped en un .vue que no es layout
391
+ → <style scoped> SIEMPRE
392
+
393
+ ❌ interface User { ... } definida en 3 archivos distintos
394
+ → Ponerla UNA vez en types/index.ts y exportarla
395
+
396
+ ❌ store.auth.store.ts (PascalCase)
397
+ → auth.store.ts (camelCase.store.ts)
398
+
399
+ ❌ services/product.ts (sin sufijo .service)
400
+ → Product.service.ts
401
+
402
+ ❌ composables/fetchData.ts (sin prefijo use)
403
+ → useFetchData.ts
404
+
405
+ ❌ pages/UserList.vue (PascalCase para página)
406
+ → user-list.vue (kebab-case para páginas)
407
+
408
+ ❌ router.push('/user/' + id) (string concatenation)
409
+ → router.push({ name: 'user-detail', params: { id } })
410
+
411
+ ❌ <script> sin lang="ts"
412
+ → <script setup lang="ts">
413
+ ```
414
+
415
+ ---
416
+
417
+ ## WORKFLOW OBLIGATORIO
418
+
419
+ ```
420
+ 1. LEER vstruct.json ← fuente única de verdad
421
+ 2. USAR la tabla canónica ← cada tipo de archivo tiene UN solo lugar
422
+ 3. USAR `vstruct g` ← genera archivos en ubicación correcta automáticamente
423
+ 4. VALIDAR con `vstruct analyze` ← 0 errores = listo
424
+ ```
425
+
426
+ ---
427
+
428
+ ## CLI
429
+
430
+ ```bash
431
+ vstruct new <name> # Crear proyecto nuevo
432
+ vstruct g c <PascalCase> # Generar componente feature
433
+ vstruct g p <kebab-case> # Generar página
434
+ vstruct g s <camelCase> # Generar store (Pinia)
435
+ vstruct g sv <PascalCase> # Generar service (API)
436
+ vstruct g co <useXxx> # Generar composable
437
+ vstruct g l <PascalCase> # Generar layout
438
+ vstruct analyze # Validar estructura (debe pasar con 0 errores)
439
+ vstruct init [--force] # Crear vstruct.json en proyecto existente
440
+ vstruct manifest # Mostrar vstruct.json actual
441
+ ```
442
+
443
+ ---
444
+
445
+ ## vstruct.json — el manifiesto
446
+
447
+ ```json
448
+ {
449
+ "version": "1",
450
+ "project": { "name": "mi-app" },
451
+ "layers": {
452
+ "ui": "src/components/ui/*.vue",
453
+ "features": "src/components/features/*.vue",
454
+ "pages": "src/pages/**/*.vue",
455
+ "layouts": "src/layouts/*.vue",
456
+ "stores": "src/stores/*.store.ts",
457
+ "services": "src/services/*.service.ts",
458
+ "composables":"src/composables/*.ts"
459
+ },
460
+ "conventions": {
461
+ "naming": {
462
+ "components": "PascalCase.vue",
463
+ "pages": "kebab-case.vue",
464
+ "stores": "camelCase.store.ts",
465
+ "services": "PascalCase.service.ts",
466
+ "composables": "useXxx.ts",
467
+ "layouts": "PascalCase.vue"
468
+ }
469
+ },
470
+ "forbidden": [],
471
+ "required": {
472
+ "scriptLangTs": true,
473
+ "styleScoped": true,
474
+ "errorHandling": true
475
+ }
476
+ }
477
+ ```
478
+
479
+ **LA IA DEBE leer `vstruct.json` ANTES de generar código. Las reglas ahí son obligatorias.**
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "vstruct-cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "CLI que genera proyectos Vue 3 con estructura 100% predecible — cada archivo tiene un lugar fijo, sin pensar.",
6
+ "keywords": [
7
+ "vue",
8
+ "cli",
9
+ "scaffold",
10
+ "predictable",
11
+ "structure",
12
+ "ai-friendly",
13
+ "frontend"
14
+ ],
15
+ "author": "phantom",
16
+ "license": "MIT",
17
+ "bin": {
18
+ "vstruct": "src/cli.js"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "AIREF.md",
23
+ "skills"
24
+ ],
25
+ "scripts": {
26
+ "test": "node src/cli.js new test-app --dry-run"
27
+ }
28
+ }
@@ -0,0 +1,69 @@
1
+ # vstruct — Vue Predictable Structure
2
+
3
+ Skill para Claude/OpenCode que garantiza que la IA NUNCA improvise ubicaciones.
4
+ Cada tipo de archivo tiene UN solo lugar. Cero ambigüedad.
5
+
6
+ ## Cuándo cargar
7
+
8
+ Cargar cuando el proyecto tiene `vstruct.json` en la raíz, o cuando se pide
9
+ crear componentes, pages, stores, services o composables en un proyecto vstruct.
10
+
11
+ ## Tabla canónica
12
+
13
+ | Tipo | Ubicación | Naming | Ejemplo |
14
+ |------|-----------|--------|---------|
15
+ | Componente UI | `src/components/ui/` | `PascalCase.vue` | `Button.vue` |
16
+ | Componente Feature | `src/components/features/` | `PascalCase.vue` | `UserCard.vue` |
17
+ | Página | `src/pages/` | `kebab-case.vue` | `user-list.vue` |
18
+ | Layout | `src/layouts/` | `PascalCase.vue` | `AdminLayout.vue` |
19
+ | Store (Pinia) | `src/stores/` | `camelCase.store.ts` | `cart.store.ts` |
20
+ | Service (API) | `src/services/` | `PascalCase.service.ts` | `Product.service.ts` |
21
+ | Composable | `src/composables/` | `useXxx.ts` | `useFetch.ts` |
22
+ | Tipos | `src/types/` | `index.ts` | — |
23
+
24
+ ## Protocolo obligatorio
25
+
26
+ 1. LEER `vstruct.json` — fuente única de verdad
27
+ 2. UBICAR según la tabla — cada cosa tiene UN lugar
28
+ 3. RESPETAR naming — PascalCase para componentes, kebab-case para páginas, etc.
29
+ 4. USAR el CLI: `npx vstruct g <tipo> <nombre>` cuando sea posible
30
+ 5. VALIDAR: `npx vstruct analyze` debe dar 0 errores
31
+
32
+ ## Reglas inmutables
33
+
34
+ 1. NUNCA inventar ubicaciones — usar la tabla
35
+ 2. NUNCA `fetch()` directo en componentes → crear `services/X.service.ts`
36
+ 3. NUNCA `<a href>` para rutas → `<router-link>` o `router.push()`
37
+ 4. NUNCA `<script>` sin `lang="ts"`
38
+ 5. NUNCA `<style>` sin `scoped` en componentes
39
+ 6. Store NO importa `useRouter`
40
+ 7. Service NO importa stores
41
+
42
+ ## Anti-patrones
43
+
44
+ ```
45
+ ❌ fetch() en .vue → services/X.service.ts
46
+ ❌ <a href="/ruta"> → <router-link to="/ruta">
47
+ ❌ useRouter en store → componente hace router.push()
48
+ ❌ <style> sin scoped → <style scoped>
49
+ ❌ store/auth.ts → stores/auth.store.ts
50
+ ❌ services/product.ts → services/Product.service.ts
51
+ ❌ pages/UserList.vue → pages/user-list.vue
52
+ ❌ composables/fetch.ts → composables/useFetch.ts
53
+ ```
54
+
55
+ ## CLI rápida
56
+
57
+ ```bash
58
+ vstruct g c UserCard # componente
59
+ vstruct g p user-list # página
60
+ vstruct g s cart # store
61
+ vstruct g sv Product # service
62
+ vstruct g co useFetch # composable
63
+ vstruct g l Admin # layout
64
+ vstruct analyze # validar
65
+ ```
66
+
67
+ ## Más detalle
68
+
69
+ Leer `AIREF.md` en la raíz del proyecto para patrones canónicos completos.