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 +479 -0
- package/package.json +28 -0
- package/skills/SKILL.md +69 -0
- package/src/cli.js +432 -0
- package/src/commands/generate.js +191 -0
- package/src/commands/new.js +188 -0
- package/src/templates/index.js +521 -0
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
|
+
}
|
package/skills/SKILL.md
ADDED
|
@@ -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.
|