navori 0.2.17 → 0.2.19

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.
@@ -2,7 +2,7 @@
2
2
  "id": "cognitive",
3
3
  "name": "Cognitive complexity guardrails",
4
4
  "description": "Detección de complejidad cognitiva alta via ESLint rule SonarJS",
5
- "version": "0.0.3",
5
+ "version": "0.0.4",
6
6
  "managed": [
7
7
  {
8
8
  "id": "cognitive-protocol",
@@ -34,12 +34,6 @@
34
34
  "command": "bash .claude/scripts/check-cognitive.sh",
35
35
  "timeout": 180,
36
36
  "statusMessage": "navori/cognitive: complexity scan"
37
- },
38
- {
39
- "event": "Stop",
40
- "command": "bash .claude/scripts/check-cognitive.sh",
41
- "timeout": 180,
42
- "statusMessage": "navori/cognitive: complexity scan (cierre de sesión)"
43
37
  }
44
38
  ]
45
39
  }
@@ -2,7 +2,7 @@
2
2
  "id": "jscpd",
3
3
  "name": "jscpd (code duplication detector)",
4
4
  "description": "Detección de duplicación de código en el diff vs branch base",
5
- "version": "0.0.3",
5
+ "version": "0.0.4",
6
6
  "managed": [
7
7
  {
8
8
  "id": "jscpd-protocol",
@@ -33,12 +33,6 @@
33
33
  "command": "bash .claude/scripts/check-jscpd.sh",
34
34
  "timeout": 180,
35
35
  "statusMessage": "navori/jscpd: dup check"
36
- },
37
- {
38
- "event": "Stop",
39
- "command": "bash .claude/scripts/check-jscpd.sh",
40
- "timeout": 180,
41
- "statusMessage": "navori/jscpd: dup check (cierre de sesión)"
42
36
  }
43
37
  ]
44
38
  }
@@ -3,16 +3,15 @@
3
3
  # `{{branchBase}}`. Skips silently if jscpd or git are absent — the tool is
4
4
  # optional, not a hard dependency of the project.
5
5
  #
6
- # Triggered as a PreToolUse(Bash) hook (gated to git commit/push) and as a
7
- # Stop hook (runs unconditionally at session close).
6
+ # Triggered as a PreToolUse(Bash) hook, gated to git commit/push so the
7
+ # duplication check runs right before code lands, not on every turn.
8
8
 
9
9
  set -euo pipefail
10
10
 
11
- # PreToolUse(Bash) passes the command — gate to commit/push. The Stop hook
12
- # passes no command — run unconditionally at session close. Extract without
11
+ # PreToolUse(Bash) passes the command — gate to commit/push. Extract without
13
12
  # hard-depending on jq (not preinstalled on macOS): try jq, then node (Claude
14
13
  # Code's own runtime), then a best-effort sed unwrap. No command extracted →
15
- # empty $cmd → runs unconditionally.
14
+ # empty $cmd → runs unconditionally (defensive: never silently skip a commit).
16
15
  payload=$(cat)
17
16
  extract_cmd() {
18
17
  if command -v jq >/dev/null 2>&1; then
@@ -69,7 +68,7 @@ jscpd \
69
68
  --min-tokens 100 \
70
69
  --min-lines 10 \
71
70
  --mode strict \
72
- --threshold 3 \
71
+ --threshold {{jscpdThreshold}} \
73
72
  --reporters console \
74
73
  --output "$tmpdir" \
75
74
  "${files[@]}"
@@ -2,7 +2,7 @@
2
2
  "id": "semgrep",
3
3
  "name": "semgrep (security + pattern analysis)",
4
4
  "description": "Detección de vulnerabilidades y patrones inseguros local opt-in",
5
- "version": "0.0.3",
5
+ "version": "0.0.4",
6
6
  "managed": [
7
7
  {
8
8
  "id": "semgrep-protocol",
@@ -32,12 +32,6 @@
32
32
  "command": "bash .claude/scripts/check-semgrep.sh",
33
33
  "timeout": 180,
34
34
  "statusMessage": "navori/semgrep: security scan"
35
- },
36
- {
37
- "event": "Stop",
38
- "command": "bash .claude/scripts/check-semgrep.sh",
39
- "timeout": 180,
40
- "statusMessage": "navori/semgrep: security scan (cierre de sesión)"
41
35
  }
42
36
  ]
43
37
  }