navori 0.2.16 → 0.2.18

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.
@@ -24,22 +24,60 @@ extract_cmd() {
24
24
  }
25
25
  cmd=$(extract_cmd)
26
26
 
27
+ # Detect the project's REAL package manager from lockfiles / package.json, so a
28
+ # gate command hardcoded to one PM (e.g. `pnpm run ...`) can still run in a repo
29
+ # that actually uses another (e.g. bun). Mirrors lib/detect.ts precedence:
30
+ # `packageManager` field first, then pnpm/bun/yarn/npm lockfiles. Prints the PM
31
+ # name (or nothing) on stdout. Git runs hooks from the repo root, so relative
32
+ # paths resolve against the project root.
33
+ detect_pm() {
34
+ if [ -f package.json ]; then
35
+ pm=$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"\([a-z]*\)@.*/\1/p' package.json | head -n1)
36
+ case "$pm" in pnpm|npm|yarn|bun) printf '%s' "$pm"; return 0 ;; esac
37
+ fi
38
+ if [ -f pnpm-lock.yaml ]; then printf 'pnpm'; return 0; fi
39
+ if [ -f bun.lockb ] || [ -f bun.lock ]; then printf 'bun'; return 0; fi
40
+ if [ -f yarn.lock ]; then printf 'yarn'; return 0; fi
41
+ if [ -f package-lock.json ]; then printf 'npm'; return 0; fi
42
+ return 0
43
+ }
44
+
45
+ # True when the argument is a JS package manager (so we only try to remap the
46
+ # leading token of the gate command when it's actually a PM, not `make`/`ruff`).
47
+ is_pm() {
48
+ case "$1" in pnpm|npm|yarn|bun) return 0 ;; *) return 1 ;; esac
49
+ }
50
+
51
+ run_gate() {
52
+ echo "[navori] running quality-gate fast: $1" >&2
53
+ eval "$1" || {
54
+ echo "[navori] quality-gate fast failed. Commit/push aborted." >&2
55
+ exit 2
56
+ }
57
+ }
58
+
27
59
  case "$cmd" in
28
60
  'git commit'*|'git push'*)
29
61
  gate="{{qualityGate.fast}}"
30
- # Defensive: skip cleanly (exit 0) when the gate's runtime isn't on PATH —
31
- # e.g. a Nix/nvm shell that hasn't loaded the project env. Never block a
32
- # commit just because the tool is missing from THIS shell.
33
62
  gate_bin="${gate%% *}"
34
- if ! command -v "$gate_bin" >/dev/null 2>&1; then
35
- echo "[navori] '$gate_bin' no está en PATH — skip quality-gate (¿shell sin el entorno del proyecto?)" >&2
36
- exit 0
63
+ if command -v "$gate_bin" >/dev/null 2>&1; then
64
+ run_gate "$gate"
65
+ else
66
+ # The declared runner isn't on PATH. If it's a package manager, detect the
67
+ # repo's real one from lockfiles and retry through it (a `pnpm run x` gate
68
+ # still runs in a bun-only checkout). #88: NEVER skip the gate silently —
69
+ # when nothing can run it, BLOCK the commit loudly instead of the old
70
+ # `exit 0` that handed a contributor zero quality gate without a word.
71
+ detected_pm="$(detect_pm)"
72
+ if is_pm "$gate_bin" && [ -n "$detected_pm" ] && [ "$detected_pm" != "$gate_bin" ] && command -v "$detected_pm" >/dev/null 2>&1; then
73
+ echo "[navori] '$gate_bin' no está en PATH; uso el package manager detectado por lockfile: '$detected_pm'." >&2
74
+ run_gate "$detected_pm ${gate#* }"
75
+ else
76
+ echo "[navori] quality-gate NO ejecutado: '$gate_bin' no está en PATH y no hay un package manager alternativo detectado que pueda correrlo." >&2
77
+ echo "[navori] Commit/push BLOQUEADO para no saltarnos el gate en silencio. Instala '$gate_bin' o usa 'git commit --no-verify' si de verdad quieres saltártelo." >&2
78
+ exit 2
79
+ fi
37
80
  fi
38
- echo "[navori] running quality-gate fast: $gate" >&2
39
- eval "$gate" || {
40
- echo "[navori] quality-gate fast failed. Commit/push aborted." >&2
41
- exit 2
42
- }
43
81
  ;;
44
82
  esac
45
83
 
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: axios
3
+ description: Patrones de Axios en TS — instancia central, interceptores, tipado de respuestas, manejo de errores y cancelación. Aplica al tocar llamadas HTTP a APIs.
4
+ type: reference
5
+ ---
6
+
7
+ # Axios — convenciones
8
+
9
+ ## Cuándo usar este skill
10
+
11
+ Al hacer una llamada HTTP a una API: crear un endpoint del cliente, agregar auth, mapear errores, o cancelar una request. Axios se cablea **una vez** en una instancia central con interceptores — los componentes/servicios importan esa instancia, no `axios` crudo con la URL a mano en cada llamada.
12
+
13
+ ## El patrón
14
+
15
+ Una instancia por API (baseURL + interceptores), funciones tipadas encima; nunca `axios.get(fullUrl)` disperso:
16
+
17
+ ```ts
18
+ export const api = axios.create({
19
+ baseURL: import.meta.env.VITE_APP_NEXUS_URL,
20
+ timeout: 15_000,
21
+ });
22
+
23
+ api.interceptors.request.use((config) => {
24
+ const token = getToken();
25
+ if (token) config.headers.Authorization = `Bearer ${token}`;
26
+ return config;
27
+ });
28
+
29
+ api.interceptors.response.use(
30
+ (res) => res,
31
+ (error) => Promise.reject(normalizeError(error)), // un solo shape de error
32
+ );
33
+
34
+ // Servicio tipado: el genérico es el TIPO DE DATA, no el del envelope.
35
+ export const getSession = (id: string) =>
36
+ api.get<Session>(`/sessions/${id}`).then((r) => r.data);
37
+ ```
38
+
39
+ ## Gotchas que muerden
40
+
41
+ - **`api.get<T>()` tipa `response.data`, no la respuesta entera.** El `T` describe el `data`; Axios envuelve en `{ data, status, headers }`. Devuelve `r.data` desde el servicio para que el caller vea `Session`, no `AxiosResponse<Session>`.
42
+ - **Una instancia con `baseURL`, no la URL completa por llamada.** Centraliza host/timeout/headers. Repetir `axios.get('https://…/sessions')` esparce la config y rompe al cambiar de entorno.
43
+ - **Auth/refresh/logging en interceptores, no copiados en cada request.** El token va en un `request.use`; el 401→refresh y el mapeo de error van en `response.use`. Nada de `headers: { Authorization }` a mano en cada endpoint.
44
+ - **`axios.isAxiosError(err)` antes de leer `err.response`.** En el `catch`, `err` es `unknown`. Sin el type guard, `err.response.data` explota en errores de red (donde `response` es `undefined` y solo hay `err.request`).
45
+ - **Un error de red NO es un error HTTP.** Timeout/DNS/offline no traen `response`. Distingue `err.response` (el server respondió con 4xx/5xx) de `err.request` (nunca llegó) para dar el mensaje correcto.
46
+ - **Cancela requests en vuelo con `AbortController`.** En efectos/búsqueda-as-you-type pasa `{ signal: controller.signal }` y aborta en el cleanup; sin esto, una respuesta vieja pisa a una nueva (race).
47
+ - **4xx/5xx ya rechazan la promesa.** No revises `res.status` en el `.then`; el flujo de error vive en `catch`/el interceptor. Solo `validateStatus` cambia esa regla, y rara vez la necesitas.
48
+
49
+ ## Reglas duras
50
+
51
+ 1. Todo HTTP pasa por la instancia central con `baseURL` + interceptores; nada de `axios` crudo con URL completa suelta.
52
+ 2. Auth, refresh y normalización de error en interceptores, una sola vez.
53
+ 3. Servicios tipados con `api.get<Data>(...)` que devuelven `.data`; el genérico es la data, no el envelope.
54
+ 4. En `catch`, `axios.isAxiosError` antes de tocar `.response`; distingue error de red de error HTTP.
55
+ 5. Requests cancelables (`AbortController`) donde puede haber carreras.
56
+
57
+ ## Antes de declarar listo
58
+
59
+ - La llamada usa la instancia central; sin URLs absolutas ni headers de auth repetidos.
60
+ - Respuestas tipadas devolviendo `.data`; el error se maneja con `isAxiosError` y un shape único.
61
+ - Requests que compiten se cancelan en el cleanup.
62
+ - `{{qualityGate.fast}}` en verde.
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: mantine-form
3
+ description: Patrones de @mantine/form — useForm, getInputProps, validación con zodResolver, campos anidados y listas. Aplica al crear o tocar formularios con Mantine.
4
+ type: reference
5
+ ---
6
+
7
+ # Mantine Form — convenciones
8
+
9
+ ## Cuándo usar este skill
10
+
11
+ Al crear o tocar un formulario con `@mantine/form`: cablear inputs de Mantine, validar, manejar submit, o campos anidados/listas. `useForm` es la fuente de verdad del form — no espejees sus valores en `useState`, y valida con un schema Zod (vía `mantine-form-zod-resolver`), no con funciones sueltas por campo.
12
+
13
+ ## El patrón
14
+
15
+ `useForm` + `getInputProps` (esparce value/onChange/error de un jalón) + `zodResolver` para el schema:
16
+
17
+ ```tsx
18
+ const schema = z.object({
19
+ email: z.string().email(),
20
+ role: z.enum(['coach', 'coachee']),
21
+ });
22
+
23
+ const form = useForm({
24
+ mode: 'uncontrolled', // menos re-renders; el default recomendado
25
+ initialValues: { email: '', role: 'coachee' },
26
+ validate: zodResolver(schema), // desde 'mantine-form-zod-resolver'
27
+ });
28
+
29
+ <form onSubmit={form.onSubmit((values) => save(values))}>
30
+ <TextInput {...form.getInputProps('email')} />
31
+ <Select data={['coach', 'coachee']} {...form.getInputProps('role')} />
32
+ <Button type="submit">Guardar</Button>
33
+ </form>
34
+ ```
35
+
36
+ ## Gotchas que muerden
37
+
38
+ - **`getInputProps('campo')` cablea todo; no lo desarmes.** Ya trae `value`/`onChange`/`error`/`onBlur`. Pasar `value`/`onChange` a mano encima rompe el binding — deja que el spread mande.
39
+ - **`mode: 'uncontrolled'` cambia cómo lees valores.** En uncontrolled, `form.values` no re-renderiza al teclear; para reflejar un campo en la UI usa `form.watch('campo')` o `form.getValues()`. En `controlled` sí re-renderiza cada tecla (más caro en forms grandes).
40
+ - **Validación con `zodResolver`, no funciones por campo.** `validate: { email: (v) => … }` disemina reglas y tipos. Un schema Zod + `zodResolver` da una fuente única y el tipo por `z.infer`. Requiere el paquete `mantine-form-zod-resolver`.
41
+ - **Campos anidados/listas con notación de path.** `getInputProps('address.city')`, y listas con `form.insertListItem('items', {...})` / `form.removeListItem('items', i)` + `getInputProps('items.0.name')`. No manejes el array en `useState` aparte.
42
+ - **`initialValues` define el shape; llénalo completo.** Un campo ausente arranca `undefined` → warning uncontrolled→controlled. Para edición async usa `form.setValues(data)` / `form.initialize(data)` en un efecto, no valores a mano por render.
43
+ - **Submit con `form.onSubmit(handler)`.** Corre la validación y solo llama tu handler si pasa; además expone el segundo callback `(errors) => …` para enfocar el primer inválido. No valides "a mano" antes de enviar.
44
+
45
+ ## Reglas duras
46
+
47
+ 1. `useForm` es la única fuente del estado del form; nada de `useState` espejo.
48
+ 2. Inputs cableados con `getInputProps`; no dupliques `value`/`onChange`.
49
+ 3. Validación con schema Zod vía `zodResolver`; tipo por `z.infer`, sin reglas por campo.
50
+ 4. `initialValues` completo; edición async con `setValues`/`initialize`.
51
+ 5. Submit vía `form.onSubmit`; listas/anidados con la API de path, no arrays sueltos.
52
+
53
+ ## Antes de declarar listo
54
+
55
+ - Todos los inputs usan `getInputProps`; sin estado espejo ni handlers duplicados.
56
+ - Validación centralizada en un schema Zod con `zodResolver`; tipos por `z.infer`.
57
+ - `initialValues` seteado; sin warnings uncontrolled→controlled. Submit con `form.onSubmit`.
58
+ - `{{qualityGate.fast}}` en verde.
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: react-router
3
+ description: Patrones de React Router (v6/v7) — rutas anidadas, loaders, navegación, params y guards. Aplica al crear rutas, leer params, redirigir o proteger vistas.
4
+ type: reference
5
+ ---
6
+
7
+ # React Router — convenciones
8
+
9
+ ## Cuándo usar este skill
10
+
11
+ Al tocar navegación: declarar una ruta, leer un param, redirigir, proteger una vista por rol, o cablear links. React Router es la fuente de verdad de **en qué URL estás y a dónde vas** — no dupliques la ruta en estado propio ni parsees `window.location` a mano.
12
+
13
+ ## El patrón
14
+
15
+ Rutas anidadas con layout compartido vía `<Outlet />`; navegación por hooks, no por mutar `window.location`:
16
+
17
+ ```tsx
18
+ const router = createBrowserRouter([
19
+ {
20
+ path: '/',
21
+ element: <AppLayout />, // renderiza <Outlet /> para los hijos
22
+ children: [
23
+ { index: true, element: <Home /> },
24
+ { path: 'sessions/:id', element: <SessionDetail /> },
25
+ { path: '*', element: <NotFound /> },
26
+ ],
27
+ },
28
+ ]);
29
+
30
+ function SessionDetail() {
31
+ const { id } = useParams(); // string | undefined, siempre
32
+ const navigate = useNavigate();
33
+ const [params, setParams] = useSearchParams();
34
+ // ...
35
+ navigate('/sessions', { replace: true }); // no <a href> manual
36
+ }
37
+ ```
38
+
39
+ ## Gotchas que muerden
40
+
41
+ - **`useParams()` siempre da `string | undefined`.** Nunca `number`. Convierte y valida (`Number(id)`, guard) antes de usarlo como id; una ruta mal tecleada no lanza, solo llega `undefined`.
42
+ - **Navegar imperativo con `useNavigate`, no `window.location`.** `window.location.href = …` recarga toda la SPA y tira el estado. Para volver: `navigate(-1)`; para redirigir sin dejar historial: `{ replace: true }`.
43
+ - **`<NavLink>` para tabs/menús, `<Link>` para el resto.** `NavLink` expone `isActive` en `className`/`style`/children; no reimplementes "está activo" comparando `pathname` a mano.
44
+ - **Search params son la URL, no `useState`.** Filtros/paginación viven en `useSearchParams` para que la vista sea linkeable y sobreviva al refresh. `setParams` reemplaza TODO el query — clona lo actual si solo cambias una clave.
45
+ - **Ruta protegida = un wrapper con `<Navigate>`, no un `if` suelto.** `if (!user) return <Navigate to="/login" replace />;` dentro de un guard/layout. Redirigir desde un `useEffect` parpadea la vista privada un frame.
46
+ - **Rutas relativas anidan; un `/` inicial las hace absolutas.** Dentro de `sessions/:id`, `navigate('edit')` va a `sessions/:id/edit`; `navigate('/edit')` va a la raíz. Es el error #1 al mover un componente de nivel.
47
+
48
+ ## Reglas duras
49
+
50
+ 1. Navegación por `useNavigate`/`<Link>`/`<NavLink>`; nunca `window.location` ni `<a href>` interno.
51
+ 2. `useParams` se valida antes de usar (puede ser `undefined`); ids numéricos se convierten explícito.
52
+ 3. Estado de filtros/paginación en `useSearchParams`, no en `useState` espejo.
53
+ 4. Vistas protegidas por un guard con `<Navigate replace>`, no por `if` + efecto.
54
+ 5. Layouts compartidos con rutas anidadas + `<Outlet />`; nada de repetir el chrome por página.
55
+
56
+ ## Antes de declarar listo
57
+
58
+ - Sin `window.location`/`<a href>` para navegación interna; links con `<Link>`/`<NavLink>`.
59
+ - Params validados; el estado de la URL (filtros, tab) vive en search params.
60
+ - Rutas protegidas redirigen con `<Navigate replace>`; sin parpadeo de la vista privada.
61
+ - `{{qualityGate.fast}}` en verde.
@@ -0,0 +1,11 @@
1
+ ## Stack — Monorepo (Turborepo + pnpm)
2
+
3
+ Este directorio es la **raíz de un monorepo**: orquesta, no aloja producto. El código real vive en los workspaces (`apps/*`, `packages/*`), y **cada workspace tiene su propio harness** (su `CLAUDE.md` + `.claude/`) con el preset de su stack. El mapa de workspaces vivos está en el bloque "## Monorepo — raíz".
4
+
5
+ Regla de oro: **enruta el trabajo al workspace dueño**. Un cambio de producto se hace desde el `CLAUDE.md` de su app, no desde aquí. La raíz solo se toca para lo transversal: `turbo.json`, `pnpm-workspace.yaml`, tsconfig/eslint base, scripts de CI, deps compartidas.
6
+
7
+ - **Tareas scopeadas, no globales.** Corre por workspace con el filtro de Turbo: `pnpm turbo run <task> --filter=<workspace>` (o `--filter=./apps/<x>`). Evita correr el pipeline entero cuando solo tocaste un app.
8
+ - **No cruces imports entre workspaces por ruta relativa** (`../../otro-app`). Consume un hermano por su nombre de paquete con el protocolo `workspace:*`; si no es un paquete publicable, probablemente el código debería vivir en un `packages/*` compartido.
9
+ - **La dep va en el `package.json` del workspace que la usa**, no en la raíz. Deps en la raíz son solo tooling del monorepo (turbo, changesets, linters compartidos).
10
+
11
+ Antes de tocar `turbo.json`, `pnpm-workspace.yaml` o mover deps entre workspaces, aplica el skill `turbo-workspaces`.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: turbo-workspaces
3
+ description: Cómo navegar y operar un monorepo Turborepo + pnpm — correr tareas scopeadas, agregar deps al workspace correcto, compartir código sin acoplar. Aplica antes de tocar turbo.json, pnpm-workspace.yaml o mover deps.
4
+ type: reference
5
+ ---
6
+
7
+ # Turborepo + pnpm — operación del monorepo
8
+
9
+ ## Cuándo usar este skill
10
+
11
+ Antes de: correr tareas de build/test/lint, agregar o mover una dependencia, crear un workspace nuevo, o editar `turbo.json` / `pnpm-workspace.yaml`. En un monorepo, "dónde" vive un cambio importa tanto como "qué" cambia.
12
+
13
+ ## Correr tareas (siempre scopeadas)
14
+
15
+ ```bash
16
+ # Una tarea en UN workspace (por nombre de paquete o por ruta)
17
+ pnpm turbo run build --filter=@scope/backend
18
+ pnpm turbo run test --filter=./apps/storefront
19
+
20
+ # Un workspace y todo lo que depende de él (aguas abajo)
21
+ pnpm turbo run build --filter=@scope/backend...
22
+
23
+ # Solo lo afectado por tu diff vs una base
24
+ pnpm turbo run test --filter='...[origin/main]'
25
+ ```
26
+
27
+ Regla: no corras el pipeline entero (`turbo run build`) si solo tocaste un app. Turbo cachea, pero el ruido de logs y el tiempo de arranque sí cuestan. Deja el run global para CI.
28
+
29
+ ## Agregar dependencias (al workspace correcto)
30
+
31
+ ```bash
32
+ # Dep de un app concreto — NO en la raíz
33
+ pnpm add zod --filter @scope/backend
34
+
35
+ # Dep de tooling del monorepo (turbo, changesets, prettier) — esa sí va en la raíz
36
+ pnpm add -Dw turbo
37
+ ```
38
+
39
+ - Una lib de producto (`stripe`, `@tanstack/react-query`, …) va en el `package.json` del app que la importa. Si aparece en la raíz, el harness de ese app no la "ve" y su skill no se materializa donde corresponde.
40
+ - Consumir un workspace hermano se declara explícito: `"@scope/ui": "workspace:*"` en el `package.json` del consumidor. Nunca por `import '../../ui/src/...'`.
41
+
42
+ ## Compartir código sin acoplar
43
+
44
+ - Código usado por ≥2 apps → extráelo a un `packages/*` con su propio `package.json` y su preset (`navori scan` lo detecta como workspace nuevo).
45
+ - Tipos/utilidades cross-app también van en un `packages/*`, no en `apps/*`. Un app nunca es dependencia de otro app.
46
+
47
+ ## turbo.json — lo esencial
48
+
49
+ - Cada `task` declara sus `dependsOn` (`^build` = build de las deps primero) y sus `outputs` (para cachear). Un output mal declarado = cache que no invalida o que no cachea.
50
+ - Antes de editar el pipeline, verifica el efecto con `pnpm turbo run <task> --dry-run` (lista qué correría y desde qué caché) antes de correrlo de verdad.
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://navori.dev/schema/navori.preset.v1.json",
3
+ "id": "monorepo-turbopnpm",
4
+ "displayName": "Monorepo (Turborepo + pnpm)",
5
+ "extends": "core",
6
+ "extras": {
7
+ "managed": [
8
+ {
9
+ "id": "stack-monorepo-turbopnpm",
10
+ "relPath": "presets/monorepo-turbopnpm/managed/stack.md"
11
+ }
12
+ ],
13
+ "agents": [],
14
+ "skills": [
15
+ {
16
+ "id": "turbo-workspaces",
17
+ "relPath": "presets/monorepo-turbopnpm/skills/turbo-workspaces.md",
18
+ "destRelPath": ".claude/skills/turbo-workspaces.md"
19
+ }
20
+ ],
21
+ "hooks": []
22
+ }
23
+ }