nexakit 0.0.0-stage → 0.2.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.
Files changed (438) hide show
  1. package/.claude-plugin/marketplace.json +28 -0
  2. package/LICENSE +26 -0
  3. package/README.md +79 -2
  4. package/agents/claude/docs-researcher.md +9 -0
  5. package/agents/claude/implementation-tutor.md +9 -0
  6. package/agents/claude/project-explorer.md +9 -0
  7. package/agents/claude/quality-reviewer.md +9 -0
  8. package/agents/claude/requirements-analyst.md +9 -0
  9. package/agents/claude/solution-architect.md +9 -0
  10. package/agents/codex/docs-researcher.toml +10 -0
  11. package/agents/codex/implementation-tutor.toml +10 -0
  12. package/agents/codex/project-explorer.toml +10 -0
  13. package/agents/codex/quality-reviewer.toml +10 -0
  14. package/agents/codex/requirements-analyst.toml +11 -0
  15. package/agents/codex/solution-architect.toml +10 -0
  16. package/assets/brand/README.md +38 -0
  17. package/assets/brand/agent-toolkit-mark.png +0 -0
  18. package/assets/brand/agent-toolkit-mark.svg +13 -0
  19. package/assets/brand/agent-toolkit-tui-hero.png +0 -0
  20. package/assets/brand/nexakit-readme-hero.png +0 -0
  21. package/assets/brand/nexakit-readme-hero.svg +21 -0
  22. package/assets/runtime/package-lock.json +1425 -0
  23. package/bin/agent-task.js +14 -0
  24. package/bin/agent-toolkit-gemini.js +13 -0
  25. package/bin/agent-toolkit-inbox.js +14 -0
  26. package/bin/agent-toolkit-mcp.js +17 -0
  27. package/bin/agent-toolkit.js +13 -0
  28. package/bin/nexakit.js +13 -0
  29. package/bin/project-memory-mcp.js +22 -0
  30. package/bin/project-memory.js +7 -0
  31. package/catalogs/skills/api-design-principles/LICENSE.txt +21 -0
  32. package/catalogs/skills/api-design-principles/SKILL.md +110 -0
  33. package/catalogs/skills/api-design-principles/assets/api-design-checklist.md +155 -0
  34. package/catalogs/skills/api-design-principles/assets/rest-api-template.py +185 -0
  35. package/catalogs/skills/api-design-principles/references/details.md +413 -0
  36. package/catalogs/skills/api-design-principles/references/graphql-schema-design.md +589 -0
  37. package/catalogs/skills/api-design-principles/references/rest-best-practices.md +408 -0
  38. package/catalogs/skills/nodejs-backend-patterns/LICENSE.txt +21 -0
  39. package/catalogs/skills/nodejs-backend-patterns/SKILL.md +45 -0
  40. package/catalogs/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
  41. package/catalogs/skills/nodejs-backend-patterns/references/details.md +599 -0
  42. package/catalogs/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
  43. package/catalogs/skills/supabase-postgres-best-practices/LICENSE.txt +21 -0
  44. package/catalogs/skills/supabase-postgres-best-practices/SKILL.md +64 -0
  45. package/catalogs/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
  46. package/catalogs/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
  47. package/catalogs/skills/supabase-postgres-best-practices/references/_template.md +34 -0
  48. package/catalogs/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
  49. package/catalogs/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
  50. package/catalogs/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
  51. package/catalogs/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
  52. package/catalogs/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
  53. package/catalogs/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
  54. package/catalogs/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
  55. package/catalogs/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
  56. package/catalogs/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
  57. package/catalogs/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
  58. package/catalogs/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
  59. package/catalogs/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
  60. package/catalogs/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
  61. package/catalogs/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
  62. package/catalogs/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
  63. package/catalogs/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
  64. package/catalogs/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
  65. package/catalogs/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
  66. package/catalogs/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
  67. package/catalogs/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
  68. package/catalogs/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
  69. package/catalogs/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
  70. package/catalogs/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
  71. package/catalogs/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
  72. package/catalogs/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
  73. package/catalogs/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
  74. package/catalogs/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
  75. package/catalogs/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
  76. package/catalogs/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
  77. package/catalogs/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
  78. package/catalogs/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
  79. package/catalogs/skills/vercel-composition-patterns/AGENTS.md +946 -0
  80. package/catalogs/skills/vercel-composition-patterns/LICENSE.txt +27 -0
  81. package/catalogs/skills/vercel-composition-patterns/README.md +60 -0
  82. package/catalogs/skills/vercel-composition-patterns/SKILL.md +89 -0
  83. package/catalogs/skills/vercel-composition-patterns/metadata.json +11 -0
  84. package/catalogs/skills/vercel-composition-patterns/rules/_sections.md +29 -0
  85. package/catalogs/skills/vercel-composition-patterns/rules/_template.md +24 -0
  86. package/catalogs/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
  87. package/catalogs/skills/vercel-composition-patterns/rules/architecture-compound-components.md +112 -0
  88. package/catalogs/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md +87 -0
  89. package/catalogs/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md +100 -0
  90. package/catalogs/skills/vercel-composition-patterns/rules/react19-no-forwardref.md +42 -0
  91. package/catalogs/skills/vercel-composition-patterns/rules/state-context-interface.md +191 -0
  92. package/catalogs/skills/vercel-composition-patterns/rules/state-decouple-implementation.md +113 -0
  93. package/catalogs/skills/vercel-composition-patterns/rules/state-lift-state.md +125 -0
  94. package/catalogs/skills/vercel-react-best-practices/AGENTS.md +3810 -0
  95. package/catalogs/skills/vercel-react-best-practices/LICENSE.txt +27 -0
  96. package/catalogs/skills/vercel-react-best-practices/README.md +123 -0
  97. package/catalogs/skills/vercel-react-best-practices/SKILL.md +149 -0
  98. package/catalogs/skills/vercel-react-best-practices/metadata.json +15 -0
  99. package/catalogs/skills/vercel-react-best-practices/rules/_sections.md +46 -0
  100. package/catalogs/skills/vercel-react-best-practices/rules/_template.md +28 -0
  101. package/catalogs/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md +56 -0
  102. package/catalogs/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  103. package/catalogs/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  104. package/catalogs/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  105. package/catalogs/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  106. package/catalogs/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md +37 -0
  107. package/catalogs/skills/vercel-react-best-practices/rules/async-defer-await.md +82 -0
  108. package/catalogs/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  109. package/catalogs/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  110. package/catalogs/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  111. package/catalogs/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md +63 -0
  112. package/catalogs/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +60 -0
  113. package/catalogs/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  114. package/catalogs/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  115. package/catalogs/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  116. package/catalogs/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  117. package/catalogs/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  118. package/catalogs/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  119. package/catalogs/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  120. package/catalogs/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  121. package/catalogs/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  122. package/catalogs/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  123. package/catalogs/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  124. package/catalogs/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  125. package/catalogs/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  126. package/catalogs/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  127. package/catalogs/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
  128. package/catalogs/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  129. package/catalogs/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  130. package/catalogs/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  131. package/catalogs/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  132. package/catalogs/skills/vercel-react-best-practices/rules/js-request-idle-callback.md +105 -0
  133. package/catalogs/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  134. package/catalogs/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  135. package/catalogs/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  136. package/catalogs/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  137. package/catalogs/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  138. package/catalogs/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  139. package/catalogs/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  140. package/catalogs/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  141. package/catalogs/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  142. package/catalogs/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
  143. package/catalogs/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
  144. package/catalogs/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  145. package/catalogs/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  146. package/catalogs/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  147. package/catalogs/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  148. package/catalogs/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  149. package/catalogs/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  150. package/catalogs/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  151. package/catalogs/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  152. package/catalogs/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  153. package/catalogs/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  154. package/catalogs/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  155. package/catalogs/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
  156. package/catalogs/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  157. package/catalogs/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md +64 -0
  158. package/catalogs/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  159. package/catalogs/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md +59 -0
  160. package/catalogs/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  161. package/catalogs/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  162. package/catalogs/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  163. package/catalogs/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  164. package/catalogs/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  165. package/catalogs/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  166. package/catalogs/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +149 -0
  167. package/catalogs/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md +50 -0
  168. package/catalogs/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  169. package/catalogs/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md +34 -0
  170. package/catalogs/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  171. package/catalogs/skills/webapp-testing/LICENSE.txt +202 -0
  172. package/catalogs/skills/webapp-testing/SKILL.md +96 -0
  173. package/catalogs/skills/webapp-testing/examples/console_logging.py +35 -0
  174. package/catalogs/skills/webapp-testing/examples/element_discovery.py +40 -0
  175. package/catalogs/skills/webapp-testing/examples/static_html_automation.py +33 -0
  176. package/catalogs/skills/webapp-testing/scripts/with_server.py +106 -0
  177. package/catalogs/skills.json +1115 -0
  178. package/docs/skills/evidencias/2026-10-28-evaluacion-inicial-mcp-seguridad.md +118 -0
  179. package/docs/skills/evidencias/2026-10-28-incorporacion-mcp-seguridad.json +152 -0
  180. package/docs/skills/evidencias/2026-10-28-recomendador-preparacion.md +50 -0
  181. package/package.json +73 -4
  182. package/plugins/nexakit-modelo/.claude-plugin/plugin.json +1 -0
  183. package/plugins/nexakit-modelo/hooks/hooks.json +1 -0
  184. package/plugins/nexakit-modelo/hooks/register.js +48 -0
  185. package/plugins/nexakit-modelo/hooks/status.js +8 -0
  186. package/plugins/nexakit-modelo/tests/register.test.ts +21 -0
  187. package/plugins/nexakit-panel/.claude-plugin/plugin.json +1 -0
  188. package/plugins/nexakit-panel/README.md +20 -0
  189. package/plugins/nexakit-panel/hooks/hooks.json +1 -0
  190. package/plugins/nexakit-panel/hooks/lienzo.js +382 -0
  191. package/plugins/nexakit-panel/hooks/panel.js +279 -0
  192. package/plugins/nexakit-panel/hooks/register.tsx +248 -0
  193. package/plugins/nexakit-panel/tests/lienzo.test.ts +142 -0
  194. package/plugins/nexakit-panel/tests/panel.test.ts +240 -0
  195. package/plugins/nexakit-panel/tests/register.test.ts +167 -0
  196. package/plugins/nexakit-panel/types/index.d.ts +41 -0
  197. package/plugins/nexakit-sesion/.claude-plugin/plugin.json +1 -0
  198. package/plugins/nexakit-sesion/README.md +12 -0
  199. package/plugins/nexakit-sesion/hooks/aviso.js +38 -0
  200. package/plugins/nexakit-sesion/hooks/hooks.json +1 -0
  201. package/plugins/nexakit-sesion/hooks/register.js +28 -0
  202. package/plugins/nexakit-sesion/tests/register.test.ts +96 -0
  203. package/plugins/niveles/.claude-plugin/plugin.json +5 -0
  204. package/plugins/niveles/agents/nivel-alto.md +13 -0
  205. package/plugins/niveles/agents/nivel-exigente.md +13 -0
  206. package/plugins/niveles/agents/nivel-ligero.md +13 -0
  207. package/plugins/niveles/agents/nivel-normal.md +13 -0
  208. package/schemas/agent-client-adapter.schema.json +786 -0
  209. package/schemas/agent-client-auth-diagnostic.schema.json +801 -0
  210. package/schemas/agent-client-isolated-variant.schema.json +105 -0
  211. package/schemas/agent-client-isolated-verification.schema.json +99 -0
  212. package/schemas/agent-client-managed-preferences-invocation.schema.json +67 -0
  213. package/schemas/agent-client-managed-preferences.schema.json +63 -0
  214. package/schemas/agent-client-native-diagnostics.schema.json +85 -0
  215. package/schemas/agent-client-native-evaluation.schema.json +171 -0
  216. package/schemas/agent-client-native-preflight.schema.json +90 -0
  217. package/schemas/agent-client-output.schema.json +551 -0
  218. package/schemas/agent-client-runtime-fixture.schema.json +90 -0
  219. package/schemas/agent-client-runtime-local.schema.json +95 -0
  220. package/schemas/agent-client-runtime.schema.json +1288 -0
  221. package/schemas/agent-client-selective-config.schema.json +77 -0
  222. package/schemas/agent-client-supervised-local.schema.json +135 -0
  223. package/schemas/agent-client-supervised.schema.json +186 -0
  224. package/schemas/agent-model-preference.schema.json +573 -0
  225. package/schemas/agent-model-selection.schema.json +311 -0
  226. package/schemas/agent-task-approvals.schema.json +362 -0
  227. package/schemas/agent-task-prepare.schema.json +1324 -0
  228. package/schemas/agent-task-verify.schema.json +198 -0
  229. package/schemas/project-memory-update.schema.json +33 -0
  230. package/schemas/project-memory.schema.json +298 -0
  231. package/schemas/skill-catalog.schema.json +246 -0
  232. package/schemas/task-workflow-plan-output.schema.json +35 -0
  233. package/scripts/prepare-package.js +6 -0
  234. package/scripts/validate-skill-catalog.js +41 -0
  235. package/skills/cerrar-avisens/SKILL.md +12 -0
  236. package/skills/cerrar-memoria/SKILL.md +96 -0
  237. package/skills/cerrar-memoria/references/cierre.md +76 -0
  238. package/skills/decision-records/SKILL.md +27 -0
  239. package/skills/decision-records/agents/openai.yaml +7 -0
  240. package/skills/decision-records/references/adr-template.md +35 -0
  241. package/skills/differential-review/LICENSE.txt +427 -0
  242. package/skills/differential-review/SKILL.md +225 -0
  243. package/skills/differential-review/adversarial.md +203 -0
  244. package/skills/differential-review/agents/openai.yaml +6 -0
  245. package/skills/differential-review/assets/trail-of-bits-mark.svg +1 -0
  246. package/skills/differential-review/methodology.md +234 -0
  247. package/skills/differential-review/patterns.md +300 -0
  248. package/skills/differential-review/reporting.md +369 -0
  249. package/skills/elegir-modelo/SKILL.md +68 -0
  250. package/skills/elegir-modelo/modelos.mjs +108 -0
  251. package/skills/frontend-design/LICENSE.txt +177 -0
  252. package/skills/frontend-design/SKILL.md +71 -0
  253. package/skills/gemini/SKILL.md +52 -0
  254. package/skills/imagen/SKILL.md +12 -0
  255. package/skills/investigar/SKILL.md +11 -0
  256. package/skills/mcp-builder/LICENSE.txt +202 -0
  257. package/skills/mcp-builder/SKILL.md +236 -0
  258. package/skills/mcp-builder/reference/evaluation.md +602 -0
  259. package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  260. package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  261. package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  262. package/skills/mcp-builder/scripts/connections.py +151 -0
  263. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  264. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  265. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  266. package/skills/ordenar-proyecto/SKILL.md +56 -0
  267. package/skills/project-handoff/SKILL.md +53 -0
  268. package/skills/project-handoff/agents/openai.yaml +7 -0
  269. package/skills/project-handoff/references/handoff-template.md +35 -0
  270. package/skills/quality-gate/LICENSE.txt +26 -0
  271. package/skills/quality-gate/SKILL.md +44 -0
  272. package/skills/quality-gate/agents/openai.yaml +7 -0
  273. package/skills/quality-gate/references/review-checklist.md +42 -0
  274. package/skills/revisar/SKILL.md +11 -0
  275. package/skills/revisar-skill/LICENSE-find-skills.txt +26 -0
  276. package/skills/revisar-skill/SKILL.md +94 -0
  277. package/skills/revision-cruzada/SKILL.md +59 -0
  278. package/skills/web-design-guidelines/AGENTS.md +156 -0
  279. package/skills/web-design-guidelines/LICENSE.txt +21 -0
  280. package/skills/web-design-guidelines/SKILL.md +29 -0
  281. package/src/agent-models.js +131 -0
  282. package/src/brand.js +117 -0
  283. package/src/claude-code-runner.js +642 -0
  284. package/src/cli-executable.js +88 -0
  285. package/src/cli.js +778 -0
  286. package/src/client-adapter-plan.js +271 -0
  287. package/src/client-auth-diagnostic-parser.js +211 -0
  288. package/src/client-auth-diagnostic.js +257 -0
  289. package/src/client-isolated-variant.js +106 -0
  290. package/src/client-isolated-verification.js +130 -0
  291. package/src/client-local-platform.js +14 -0
  292. package/src/client-managed-preferences-invocation.js +209 -0
  293. package/src/client-managed-preferences.js +78 -0
  294. package/src/client-native-access.js +134 -0
  295. package/src/client-native-diagnostics.js +253 -0
  296. package/src/client-native-evaluation.js +106 -0
  297. package/src/client-native-preflight-files.js +68 -0
  298. package/src/client-native-preflight.js +95 -0
  299. package/src/client-output-json.js +118 -0
  300. package/src/client-output-parser.js +278 -0
  301. package/src/client-runtime-admission.js +80 -0
  302. package/src/client-runtime-coordinator.js +70 -0
  303. package/src/client-runtime-core.js +92 -0
  304. package/src/client-runtime-fixture-permit.js +20 -0
  305. package/src/client-runtime-fixture-protocol.js +39 -0
  306. package/src/client-runtime-state.js +67 -0
  307. package/src/client-runtime-store-files.js +91 -0
  308. package/src/client-runtime-store.js +239 -0
  309. package/src/client-runtime-supervisor.js +146 -0
  310. package/src/client-selective-config.js +195 -0
  311. package/src/client-selective-files.js +126 -0
  312. package/src/client-supervised-codes.js +32 -0
  313. package/src/client-supervised-contract.js +52 -0
  314. package/src/client-supervised-coordinator.js +36 -0
  315. package/src/client-supervised-permit.js +20 -0
  316. package/src/client-supervised-protocol.js +37 -0
  317. package/src/client-supervised-store.js +167 -0
  318. package/src/client-supervised-supervisor.js +167 -0
  319. package/src/client-supervised.js +160 -0
  320. package/src/codex-cli-runner.js +358 -0
  321. package/src/codex-delivery.js +101 -0
  322. package/src/connect.js +703 -0
  323. package/src/doctor.js +795 -0
  324. package/src/gemini-cli.js +76 -0
  325. package/src/gemini-files.js +45 -0
  326. package/src/gemini-tools.js +249 -0
  327. package/src/init.js +473 -0
  328. package/src/install-lock.js +127 -0
  329. package/src/install-registry.js +61 -0
  330. package/src/legacy-repair.js +71 -0
  331. package/src/level-agents.js +111 -0
  332. package/src/link-states.js +172 -0
  333. package/src/memory-connection-artifacts.js +192 -0
  334. package/src/memory-connection-descriptor.js +158 -0
  335. package/src/memory-connection-diagnostic.js +177 -0
  336. package/src/memory-connection-files.js +191 -0
  337. package/src/memory-connection-manifest.js +118 -0
  338. package/src/memory-connection-package.js +176 -0
  339. package/src/memory-connection-plan.js +211 -0
  340. package/src/memory-connection-skill.js +211 -0
  341. package/src/memory-connection-toml.js +153 -0
  342. package/src/memory-locator.js +118 -0
  343. package/src/memory-mcp-policy.js +181 -0
  344. package/src/memory-mcp-server.js +166 -0
  345. package/src/memory-mcp-transport.js +59 -0
  346. package/src/missing-dependencies.js +21 -0
  347. package/src/model-command.js +147 -0
  348. package/src/model-preference.js +235 -0
  349. package/src/model-selection.js +368 -0
  350. package/src/native/managed-preferences.c +100 -0
  351. package/src/native/managed-preferences.h +17 -0
  352. package/src/package-lock-path.js +13 -0
  353. package/src/panel-cli.js +182 -0
  354. package/src/peer-handoff.js +27 -0
  355. package/src/peer-inbox-cli.js +211 -0
  356. package/src/peer-review-mcp.js +210 -0
  357. package/src/peer-review-store.js +532 -0
  358. package/src/platform.js +24 -0
  359. package/src/project-root.js +41 -0
  360. package/src/project-signals.js +339 -0
  361. package/src/recommend-report.js +77 -0
  362. package/src/review-instructions.js +211 -0
  363. package/src/review-policy.js +35 -0
  364. package/src/revision-adapters.js +157 -0
  365. package/src/revision-context.js +86 -0
  366. package/src/revision-isolation.js +222 -0
  367. package/src/revision-process.js +103 -0
  368. package/src/revision-provenance.js +89 -0
  369. package/src/revision-snapshot.js +120 -0
  370. package/src/revision.js +242 -0
  371. package/src/runtime-fixtures/runtime-supervisor/child.cjs +3 -0
  372. package/src/runtime-fixtures/runtime-supervisor/fixture.cjs +49 -0
  373. package/src/runtime-fixtures/supervised/child.cjs +3 -0
  374. package/src/runtime-fixtures/supervised/fixture.cjs +47 -0
  375. package/src/sensitive-data.js +21 -0
  376. package/src/setup-apply.js +231 -0
  377. package/src/setup-plan.js +231 -0
  378. package/src/setup-state.js +277 -0
  379. package/src/setup-tui.js +555 -0
  380. package/src/skill-catalog-add.js +91 -0
  381. package/src/skill-catalog-audit.js +68 -0
  382. package/src/skill-catalog.js +170 -0
  383. package/src/skill-files.js +39 -0
  384. package/src/skill-recommendations.js +1673 -0
  385. package/src/skill-scan-report.js +14 -0
  386. package/src/skill-scan.js +178 -0
  387. package/src/stable-command.js +53 -0
  388. package/src/stable-install.js +222 -0
  389. package/src/status.js +200 -0
  390. package/src/superpowers.js +133 -0
  391. package/src/sync-apply.js +227 -0
  392. package/src/sync-files.js +109 -0
  393. package/src/sync-inspect.js +394 -0
  394. package/src/sync-ledger.js +94 -0
  395. package/src/sync-markdown.js +151 -0
  396. package/src/sync-proposal.js +84 -0
  397. package/src/sync-record.js +459 -0
  398. package/src/sync.js +360 -0
  399. package/src/task-approvals.js +367 -0
  400. package/src/task-cli.js +79 -0
  401. package/src/task-external-verifier.js +584 -0
  402. package/src/task-prepare-files.js +115 -0
  403. package/src/task-prepare.js +180 -0
  404. package/src/task-verify-cli.js +67 -0
  405. package/src/task-verify-files.js +188 -0
  406. package/src/task-verify.js +118 -0
  407. package/src/task-workflow-mcp.js +544 -0
  408. package/src/task-workflow-store.js +254 -0
  409. package/src/terminal-text.js +68 -0
  410. package/src/toolkit-cli.js +543 -0
  411. package/src/toolkit-commands-guide.js +107 -0
  412. package/src/toolkit-setup.js +614 -0
  413. package/src/usage-metrics.js +43 -0
  414. package/templates/agent-team/01-encargo.md +158 -0
  415. package/templates/agent-team/02-propuesta.md +132 -0
  416. package/templates/agent-team/03-revision.md +150 -0
  417. package/templates/agent-team/README.md +213 -0
  418. package/templates/model-inventory/01-inventario.md +154 -0
  419. package/templates/model-inventory/README.md +115 -0
  420. package/templates/project-memory/README.md +67 -0
  421. package/templates/project-memory/project-memory.example.json +53 -0
  422. package/templates/project-memory/skill/project-id/SKILL.md +37 -0
  423. package/templates/project-memory/skill/project-id/references/project-profile.md +44 -0
  424. package/templates/project-memory/vault/00-INICIO/Estado actual.md +44 -0
  425. package/templates/project-memory/vault/00-INICIO/Fuentes de verdad.md +30 -0
  426. package/templates/project-memory/vault/00-INICIO/Inicio del proyecto.md +44 -0
  427. package/templates/project-memory/vault/00-INICIO/Mantenimiento de la memoria.md +26 -0
  428. package/templates/project-memory/vault/00-INICIO/Mapa de la memoria.md +33 -0
  429. package/templates/project-memory/vault/00-INICIO/Registro de sincronizaci/303/263n.md" +28 -0
  430. package/templates/project-memory/vault/01-PRODUCTO/Producto.md +16 -0
  431. package/templates/project-memory/vault/02-ARQUITECTURA/Arquitectura.md +16 -0
  432. package/templates/project-memory/vault/03-IMPLEMENTACION/Implementaci/303/263n.md +15 -0
  433. package/templates/project-memory/vault/04-DECISIONES/Decisiones.md +19 -0
  434. package/templates/project-memory/vault/05-ENTREGAS/Entregas.md +12 -0
  435. package/templates/project-memory/vault/06-OPERACION/Operaci/303/263n.md +15 -0
  436. package/templates/project-memory/vault/07-AGENTES/Protocolo de agentes.md +33 -0
  437. package/templates/project-memory/vault/08-RECURSOS/Recursos.md +12 -0
  438. package/templates/project-memory/vault/09-ARCHIVO/README.md +5 -0
@@ -0,0 +1,1673 @@
1
+ import { ownedReviewInstructions, removeReviewInstructions } from './review-instructions.js';
2
+ import { createHash } from 'node:crypto';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { acquireLock } from './install-lock.js';
7
+ import { isRegistered, readRegistry, recordEntries } from './install-registry.js';
8
+ import { classifyLink, relinkIfUnchanged } from './link-states.js';
9
+ import { IS_WINDOWS } from './platform.js';
10
+ import { detectSignals, SIGNAL_LABELS } from './project-signals.js';
11
+ import { resolveProjectRoot } from './project-root.js';
12
+ import { loadSkillCatalog } from './skill-catalog.js';
13
+ import { actualRoot, ensureStableInstall, nexakitDir } from './stable-install.js';
14
+ import { TOOLKIT_ROOT, globalSkillTarget, resolveRoots } from './toolkit-setup.js';
15
+
16
+ const MAX_MANIFEST_BYTES = 512 * 1024;
17
+ const MAX_SKILL_FILES = 256;
18
+ const VALID_PROFILES = new Set(['frontend', 'backend', 'mcp', 'security']);
19
+ const VALID_CLIENTS = new Set(['codex', 'claude']);
20
+ const VALID_INSTALL_MODES = new Set(['symlink', 'copy']);
21
+ const VALID_GIT_IGNORE = new Set(['gitignore', 'git-exclude', 'none']);
22
+ const SKILL_NAME_RE = /^[a-z0-9-]+$/u;
23
+ // Antigravity reads project skills from `.agents/skills`, the folder Codex uses: one link serves both.
24
+ const projectClientsFor = agent => (agent === 'all' ? ['codex', 'claude'] : [agent === 'antigravity' ? 'codex' : agent]);
25
+ const servesFor = (client, agent) => (client === 'claude' ? ['claude'] : agent === 'all' ? ['codex', 'antigravity'] : [agent === 'antigravity' ? 'antigravity' : 'codex']);
26
+ // The coarse vocabulary the rest of the code reads; `linkState` keeps the detailed one.
27
+ const COARSE_STATE = Object.freeze({
28
+ falta: 'missing', efimero: 'replace', 'roto-propio': 'replace', instalado: 'installed',
29
+ 'otra-copia': 'installed', conflicto: 'conflict', 'roto-ajeno': 'conflict'
30
+ });
31
+
32
+ export const PROJECT_TOOLKIT_DIR = '.agent-toolkit';
33
+ const PROJECT_MANIFEST_FILE = 'manifest.json';
34
+ const PROJECT_LOCKFILE_NAME = 'skills-lock.json';
35
+ const IGNORE_BLOCK_START = '# >>> agent-toolkit (managed) >>>';
36
+ const IGNORE_BLOCK_END = '# <<< agent-toolkit (managed) <<<';
37
+ const LEVEL_AGENT_HEADER = '# Generado por `agent-toolkit-inbox agentes`.';
38
+ const LEVEL_AGENT_NAMES = ['nivel_ligero.toml', 'nivel_normal.toml', 'nivel_exigente.toml', 'nivel_alto.toml'];
39
+
40
+ export { SIGNAL_LABELS };
41
+
42
+ const normalizedPath = value => (IS_WINDOWS ? value.toLowerCase() : value);
43
+ const lstatOrNull = target => fs.lstat(target).catch(err => {
44
+ if (err.code === 'ENOENT') return null;
45
+ throw err;
46
+ });
47
+
48
+ function isWithinRoot(candidate, allowedRoot) {
49
+ const rel = path.relative(normalizedPath(allowedRoot), normalizedPath(candidate));
50
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel));
51
+ }
52
+
53
+ async function hashDirectoryFiles(dir, prefix = '', maxFiles = MAX_SKILL_FILES) {
54
+ const entries = await fs.readdir(dir, { withFileTypes: true });
55
+ const files = [];
56
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
57
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
58
+ const full = path.join(dir, entry.name);
59
+ if (entry.isSymbolicLink()) return null;
60
+ if (entry.isDirectory()) {
61
+ const nested = await hashDirectoryFiles(full, rel, maxFiles - files.length);
62
+ if (!nested) return null;
63
+ files.push(...nested);
64
+ } else if (entry.isFile()) {
65
+ if (files.length >= maxFiles) return null;
66
+ const info = await fs.lstat(full);
67
+ if (info.size > MAX_MANIFEST_BYTES) return null;
68
+ const sha256 = createHash('sha256').update(await fs.readFile(full)).digest('hex');
69
+ files.push({ path: rel, sha256 });
70
+ }
71
+ }
72
+ return files;
73
+ }
74
+
75
+ async function verifyCatalogIntegrity(canonicalDir, catalogEntry) {
76
+ if (!catalogEntry) return 'untracked';
77
+ try {
78
+ const actual = await hashDirectoryFiles(canonicalDir);
79
+ if (!actual || actual.length !== catalogEntry.files.length) return 'modified';
80
+ const expected = [...catalogEntry.files].sort((a, b) => a.path.localeCompare(b.path));
81
+ for (let i = 0; i < expected.length; i++) {
82
+ if (actual[i].path !== expected[i].path || actual[i].sha256 !== expected[i].sha256) return 'modified';
83
+ }
84
+ return 'verified';
85
+ } catch {
86
+ return 'modified';
87
+ }
88
+ }
89
+
90
+ export async function verifyBundledSkillHashes({ skillEntry, toolkitRoot = TOOLKIT_ROOT } = {}) {
91
+ if (!skillEntry || !skillEntry.installedPath || !Array.isArray(skillEntry.files)) {
92
+ return { ok: false, reason: 'Entrada de skill inválida.' };
93
+ }
94
+ try {
95
+ const resolvedToolkit = await fs.realpath(toolkitRoot);
96
+ const sourceDir = path.join(resolvedToolkit, skillEntry.installedPath);
97
+ if (!isWithinRoot(sourceDir, resolvedToolkit)) {
98
+ return { ok: false, reason: 'Ruta instalada fuera de la raíz de Agent Toolkit.' };
99
+ }
100
+ const dirInfo = await lstatOrNull(sourceDir);
101
+ if (!dirInfo || !dirInfo.isDirectory() || dirInfo.isSymbolicLink()) {
102
+ return { ok: false, reason: `No existe la carpeta fuente de ${skillEntry.name}.` };
103
+ }
104
+ const integrity = await verifyCatalogIntegrity(sourceDir, skillEntry);
105
+ if (integrity !== 'verified') {
106
+ return { ok: false, reason: `Integridad SHA-256 rechazada para ${skillEntry.name}.` };
107
+ }
108
+ return { ok: true, filesCount: skillEntry.files.length, sourceDir };
109
+ } catch {
110
+ return { ok: false, reason: `No se pudo comprobar SHA-256 para ${skillEntry.name}.` };
111
+ }
112
+ }
113
+
114
+ async function inspectProjectSkills(projectRoot, toolkitRoot, catalogByName, clientFilter) {
115
+ const locations = [
116
+ ['codex', path.join(projectRoot, '.agents', 'skills')],
117
+ ['claude', path.join(projectRoot, '.claude', 'skills')]
118
+ ].filter(([c]) => !clientFilter || c === clientFilter);
119
+
120
+ const merged = new Map();
121
+ for (const [client, skillsDir] of locations) {
122
+ const dirInfo = await lstatOrNull(skillsDir);
123
+ if (!dirInfo || !dirInfo.isDirectory() || dirInfo.isSymbolicLink()) continue;
124
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
125
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
126
+ if (!SKILL_NAME_RE.test(entry.name)) continue;
127
+ const entryPath = path.join(skillsDir, entry.name);
128
+ const info = await lstatOrNull(entryPath);
129
+ if (!info) continue;
130
+
131
+ if (info.isSymbolicLink()) {
132
+ let canonicalTarget;
133
+ try {
134
+ canonicalTarget = await fs.realpath(entryPath);
135
+ } catch (err) {
136
+ if (err.code === 'ENOENT') {
137
+ const key = `broken:${client}:${entry.name}`;
138
+ merged.set(key, { name: entry.name, clients: [client], state: 'broken-link', integrity: 'unknown' });
139
+ continue;
140
+ }
141
+ throw err;
142
+ }
143
+ const insideAllowed = isWithinRoot(canonicalTarget, projectRoot) || isWithinRoot(canonicalTarget, toolkitRoot);
144
+ if (!insideAllowed) {
145
+ const key = `external:${entry.name}:${normalizedPath(canonicalTarget)}`;
146
+ const prev = merged.get(key);
147
+ if (prev) {
148
+ if (!prev.clients.includes(client)) prev.clients.push(client);
149
+ } else {
150
+ merged.set(key, { name: entry.name, clients: [client], state: 'external-link', integrity: 'unknown' });
151
+ }
152
+ continue;
153
+ }
154
+ const key = `canonical:${entry.name}:${normalizedPath(canonicalTarget)}`;
155
+ const prev = merged.get(key);
156
+ if (prev) {
157
+ if (!prev.clients.includes(client)) prev.clients.push(client);
158
+ } else {
159
+ const integrity = await verifyCatalogIntegrity(canonicalTarget, catalogByName.get(entry.name));
160
+ merged.set(key, { name: entry.name, clients: [client], state: 'linked', integrity });
161
+ }
162
+ } else if (info.isDirectory()) {
163
+ const canonicalTarget = await fs.realpath(entryPath);
164
+ const integrity = await verifyCatalogIntegrity(canonicalTarget, catalogByName.get(entry.name));
165
+ const key = integrity === 'verified'
166
+ ? `verified-copy:${entry.name}`
167
+ : `canonical:${entry.name}:${normalizedPath(canonicalTarget)}`;
168
+ const prev = merged.get(key);
169
+ if (prev) {
170
+ if (!prev.clients.includes(client)) prev.clients.push(client);
171
+ } else {
172
+ merged.set(key, { name: entry.name, clients: [client], state: 'directory', integrity });
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ return [...merged.values()].map(item => ({
179
+ ...item,
180
+ clients: [...item.clients].sort()
181
+ }));
182
+ }
183
+
184
+ async function rootPackageName(root) {
185
+ const info = await lstatOrNull(path.join(root, 'package.json'));
186
+ if (!info?.isFile() || info.isSymbolicLink() || info.size > MAX_MANIFEST_BYTES) return null;
187
+ try {
188
+ const name = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'))?.name;
189
+ return typeof name === 'string' && name.trim() ? name.trim() : null;
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ export async function inspectSkillProject(projectRoot, { client, toolkitRoot = TOOLKIT_ROOT, catalog, homeDirectory } = {}) {
196
+ if (client !== undefined && !VALID_CLIENTS.has(client)) {
197
+ throw new Error('El filtro --client solo admite codex o claude.');
198
+ }
199
+ // The project is the whole Git repository, even when nexakit runs from one of its subfolders.
200
+ const project = await resolveProjectRoot(projectRoot, homeDirectory ? { homeDirectory } : {});
201
+ const root = project.root;
202
+ const resolvedToolkit = await fs.realpath(toolkitRoot);
203
+ const activeCatalog = catalog ?? await loadSkillCatalog();
204
+ const catalogByName = new Map(activeCatalog.skills.map(s => [s.name, s]));
205
+ const { signals, limits } = await detectSignals(root, { git: project.git });
206
+ const projectName = await rootPackageName(root) ?? project.displayName;
207
+
208
+ const installed = await inspectProjectSkills(root, resolvedToolkit, catalogByName, client);
209
+ const coupling = await inspectProjectCoupling(root, resolvedToolkit);
210
+
211
+ return {
212
+ projectRoot: root,
213
+ project,
214
+ projectName,
215
+ signals,
216
+ installed,
217
+ coupling,
218
+ limits
219
+ };
220
+ }
221
+
222
+ export function upsertManagedIgnoreBlock(existingContent, relativePaths) {
223
+ const normalized = (existingContent ?? '').replace(/\r\n/gu, '\n');
224
+ const uniquePaths = ['.agent-toolkit/', ...new Set(relativePaths.map(p => p.replaceAll('\\', '/')))];
225
+ const block = [IGNORE_BLOCK_START, ...uniquePaths, IGNORE_BLOCK_END].join('\n');
226
+
227
+ const startIdx = normalized.indexOf(IGNORE_BLOCK_START);
228
+ const endIdx = startIdx >= 0 ? normalized.indexOf(IGNORE_BLOCK_END, startIdx) : -1;
229
+
230
+ if (startIdx >= 0 && endIdx > startIdx) {
231
+ const before = normalized.slice(0, startIdx);
232
+ const after = normalized.slice(endIdx + IGNORE_BLOCK_END.length);
233
+ const merged = `${before}${block}${after}`;
234
+ return merged.endsWith('\n') ? merged : `${merged}\n`;
235
+ }
236
+
237
+ const trimmedEnd = normalized.replace(/\s+$/u, '');
238
+ if (!trimmedEnd) return `${block}\n`;
239
+ return `${trimmedEnd}\n\n${block}\n`;
240
+ }
241
+
242
+ export function removeManagedIgnoreBlock(existingContent) {
243
+ const normalized = (existingContent ?? '').replace(/\r\n/gu, '\n');
244
+ const startIdx = normalized.indexOf(IGNORE_BLOCK_START);
245
+ const endIdx = startIdx >= 0 ? normalized.indexOf(IGNORE_BLOCK_END, startIdx) : -1;
246
+ if (startIdx < 0 || endIdx < startIdx) {
247
+ return { changed: false, content: existingContent ?? '' };
248
+ }
249
+
250
+ let before = normalized.slice(0, startIdx);
251
+ let after = normalized.slice(endIdx + IGNORE_BLOCK_END.length);
252
+
253
+ if (after.startsWith('\n')) after = after.slice(1);
254
+ if (!after.trim()) {
255
+ before = before.replace(/\s+$/u, '');
256
+ return { changed: true, content: before ? `${before}\n` : '' };
257
+ }
258
+ before = before.replace(/\n{2,}$/u, '\n');
259
+ const joined = `${before}${after}`.replace(/^\n+/u, '');
260
+ return { changed: true, content: joined.trim() ? (joined.endsWith('\n') ? joined : `${joined}\n`) : '' };
261
+ }
262
+
263
+ async function resolveGitExcludeTarget(projectRoot) {
264
+ const dotGit = path.join(projectRoot, '.git');
265
+ const gitInfo = await lstatOrNull(dotGit);
266
+ if (!gitInfo || gitInfo.isSymbolicLink()) {
267
+ return { targetPath: null, conflict: Boolean(gitInfo?.isSymbolicLink()), unavailable: !gitInfo };
268
+ }
269
+ let gitDir = null;
270
+ if (gitInfo.isDirectory()) {
271
+ gitDir = dotGit;
272
+ } else if (gitInfo.isFile() && gitInfo.size <= 4096) {
273
+ const text = await fs.readFile(dotGit, 'utf8').catch(() => '');
274
+ const match = text.match(/^gitdir:\s*(.+)\s*$/mu);
275
+ if (match) {
276
+ const candidate = path.resolve(projectRoot, match[1].trim());
277
+ const candInfo = await lstatOrNull(candidate);
278
+ if (candInfo?.isDirectory() && !candInfo.isSymbolicLink()) gitDir = candidate;
279
+ }
280
+ }
281
+ if (!gitDir) return { targetPath: null, conflict: false, unavailable: true };
282
+
283
+ const infoDir = path.join(gitDir, 'info');
284
+ const infoStat = await lstatOrNull(infoDir);
285
+ if (infoStat && (!infoStat.isDirectory() || infoStat.isSymbolicLink())) {
286
+ return { targetPath: path.join(infoDir, 'exclude'), conflict: true, unavailable: false };
287
+ }
288
+ const excludePath = path.join(infoDir, 'exclude');
289
+ const excludeStat = await lstatOrNull(excludePath);
290
+ if (excludeStat && (!excludeStat.isFile() || excludeStat.isSymbolicLink() || excludeStat.size > MAX_MANIFEST_BYTES)) {
291
+ return { targetPath: excludePath, conflict: true, unavailable: false };
292
+ }
293
+ return { targetPath: excludePath, conflict: false, unavailable: false };
294
+ }
295
+
296
+ async function inspectGitIgnoreTarget(projectRoot, strategy) {
297
+ if (strategy === 'none') return { strategy, targetPath: null, conflict: false, unavailable: false };
298
+ if (strategy === 'git-exclude') {
299
+ const res = await resolveGitExcludeTarget(projectRoot);
300
+ return { strategy, ...res };
301
+ }
302
+ const gitignorePath = path.join(projectRoot, '.gitignore');
303
+ const info = await lstatOrNull(gitignorePath);
304
+ if (info && (!info.isFile() || info.isSymbolicLink() || info.size > MAX_MANIFEST_BYTES)) {
305
+ return { strategy: 'gitignore', targetPath: gitignorePath, conflict: true, unavailable: false };
306
+ }
307
+ return { strategy: 'gitignore', targetPath: gitignorePath, conflict: false, unavailable: false, existedBefore: Boolean(info) };
308
+ }
309
+
310
+ const VALID_MANAGED_BY = new Set(['nexakit', '@jcabr3ra1/agent-toolkit']);
311
+
312
+ async function readProjectToolkitManifest(projectRoot) {
313
+ const manifestPath = path.join(projectRoot, PROJECT_TOOLKIT_DIR, PROJECT_MANIFEST_FILE);
314
+ const info = await lstatOrNull(manifestPath);
315
+ if (!info || !info.isFile() || info.isSymbolicLink() || info.size > MAX_MANIFEST_BYTES) return null;
316
+ try {
317
+ const parsed = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
318
+ if (
319
+ parsed &&
320
+ VALID_MANAGED_BY.has(parsed.managedBy) &&
321
+ Array.isArray(parsed.entries)
322
+ ) {
323
+ return parsed;
324
+ }
325
+ } catch {}
326
+ return null;
327
+ }
328
+
329
+ async function isLegacyToolkitLockfile(lockPath) {
330
+ const info = await lstatOrNull(lockPath);
331
+ if (!info || !info.isFile() || info.isSymbolicLink() || info.size > MAX_MANIFEST_BYTES) return null;
332
+ try {
333
+ const parsed = JSON.parse(await fs.readFile(lockPath, 'utf8'));
334
+ if (parsed && VALID_MANAGED_BY.has(parsed.generatedBy) && typeof parsed.skills === 'object') {
335
+ return parsed;
336
+ }
337
+ } catch {}
338
+ return null;
339
+ }
340
+
341
+ async function hasManagedBlockInFile(filePath) {
342
+ if (!filePath) return false;
343
+ const info = await lstatOrNull(filePath);
344
+ if (!info || !info.isFile() || info.isSymbolicLink() || info.size > MAX_MANIFEST_BYTES) return false;
345
+ try {
346
+ const content = await fs.readFile(filePath, 'utf8');
347
+ return content.includes(IGNORE_BLOCK_START) && content.includes(IGNORE_BLOCK_END);
348
+ } catch {
349
+ return false;
350
+ }
351
+ }
352
+
353
+ async function findFallbackExternalToolkitLinks(projectRoot, resolvedToolkit, clients = ['codex', 'claude'], homeDir = os.homedir()) {
354
+ try {
355
+ const resolvedHome = await fs.realpath(homeDir);
356
+ if (normalizedPath(projectRoot) === normalizedPath(resolvedHome)) return [];
357
+ } catch {}
358
+ const found = [];
359
+ for (const client of clients) {
360
+ const skillsDir = path.join(projectRoot, client === 'codex' ? '.agents' : '.claude', 'skills');
361
+ const dirInfo = await lstatOrNull(skillsDir);
362
+ if (!dirInfo || !dirInfo.isDirectory() || dirInfo.isSymbolicLink()) continue;
363
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
364
+ for (const entry of entries) {
365
+ if (!SKILL_NAME_RE.test(entry.name)) continue;
366
+ const full = path.join(skillsDir, entry.name);
367
+ const info = await lstatOrNull(full);
368
+ if (!info?.isSymbolicLink()) continue;
369
+ try {
370
+ const real = await fs.realpath(full);
371
+ if (isWithinRoot(real, resolvedToolkit) && !isWithinRoot(real, projectRoot)) {
372
+ found.push({
373
+ client,
374
+ name: entry.name,
375
+ relativePath: `${client === 'codex' ? '.agents' : '.claude'}/skills/${entry.name}`,
376
+ mode: 'symlink'
377
+ });
378
+ }
379
+ } catch {}
380
+ }
381
+ }
382
+ return found;
383
+ }
384
+
385
+ async function inspectProjectCoupling(projectRoot, resolvedToolkit) {
386
+ const stateDir = path.join(projectRoot, PROJECT_TOOLKIT_DIR);
387
+ const stateDirInfo = await lstatOrNull(stateDir);
388
+ const hasStateDir = Boolean(stateDirInfo?.isDirectory() && !stateDirInfo.isSymbolicLink());
389
+ const manifest = await readProjectToolkitManifest(projectRoot);
390
+ const legacyLock = await isLegacyToolkitLockfile(path.join(projectRoot, PROJECT_LOCKFILE_NAME));
391
+ const fallbackLinks = manifest ? [] : await findFallbackExternalToolkitLinks(projectRoot, resolvedToolkit);
392
+
393
+ const gitignoreHasBlock = await hasManagedBlockInFile(path.join(projectRoot, '.gitignore'));
394
+ const excludeRes = await resolveGitExcludeTarget(projectRoot);
395
+ const excludeHasBlock = await hasManagedBlockInFile(excludeRes.targetPath);
396
+
397
+ const managedEntriesCount = manifest
398
+ ? manifest.entries.length
399
+ : fallbackLinks.length + (legacyLock ? Object.keys(legacyLock.skills ?? {}).length : 0);
400
+
401
+ return {
402
+ coupled: Boolean(hasStateDir || managedEntriesCount > 0 || legacyLock || gitignoreHasBlock || excludeHasBlock),
403
+ hasStateDir,
404
+ gitIgnoreStrategy: manifest?.gitIgnoreStrategy ?? (excludeHasBlock ? 'git-exclude' : 'gitignore'),
405
+ mode: manifest?.mode ?? null,
406
+ managedEntriesCount
407
+ };
408
+ }
409
+
410
+ function findApplicableScenario(skill, observedSignalIds, profile, client) {
411
+ if (!client) return null;
412
+ for (const scenario of skill.scenarios) {
413
+ if (scenario.status !== 'passed') continue;
414
+ if (scenario.revision !== skill.revision) continue;
415
+ if (profile && scenario.profile !== profile) continue;
416
+ if (!skill.matching.profiles.includes(scenario.profile)) continue;
417
+ if (!skill.matching.taskClasses.includes(scenario.taskClass)) continue;
418
+ if (scenario.client !== client) continue;
419
+ if (!scenario.requiredSignals.every(req => observedSignalIds.has(req))) continue;
420
+ return scenario;
421
+ }
422
+ return null;
423
+ }
424
+
425
+ export function recommendSkills({ catalog, inspection, profile, client, clients } = {}) {
426
+ if (!catalog || !inspection) throw new Error('Se requieren catalog e inspection.');
427
+ if (profile !== undefined && !VALID_PROFILES.has(profile)) {
428
+ throw new Error('El perfil debe ser frontend, backend, mcp o security.');
429
+ }
430
+ if (client !== undefined && !VALID_CLIENTS.has(client)) {
431
+ throw new Error('El filtro --client solo admite codex o claude.');
432
+ }
433
+ if (clients !== undefined && (!Array.isArray(clients) || clients.length === 0 || clients.some(value => !VALID_CLIENTS.has(value)) || new Set(clients).size !== clients.length)) {
434
+ throw new Error('Los agentes destino deben ser una lista de codex y/o claude sin duplicados.');
435
+ }
436
+ const targetClients = clients ?? (client ? [client] : []);
437
+ if (client && clients && (clients.length !== 1 || clients[0] !== client)) {
438
+ throw new Error('Los filtros client y clients deben indicar el mismo agente.');
439
+ }
440
+
441
+ const observedIds = new Set(inspection.signals.map(s => s.id));
442
+ const signalMap = new Map(inspection.signals.map(s => [s.id, s]));
443
+ const installedByName = new Map();
444
+ for (const inst of inspection.installed) {
445
+ if (!installedByName.has(inst.name) || inst.integrity === 'verified') {
446
+ installedByName.set(inst.name, inst);
447
+ }
448
+ }
449
+
450
+ const recommended = [];
451
+ const candidates = [];
452
+ const optional = [];
453
+ const coveredSignals = new Set(['nodejs', 'typescript']);
454
+
455
+ for (const skill of catalog.skills) {
456
+ if (skill.review.status === 'rejected') continue;
457
+
458
+ const matchedSignals = skill.matching.anySignals.filter(sig => observedIds.has(sig));
459
+ for (const sig of matchedSignals) coveredSignals.add(sig);
460
+ const matchedProfile = Boolean(profile && skill.matching.profiles.includes(profile));
461
+ const installed = installedByName.get(skill.name) ?? null;
462
+
463
+ if (matchedSignals.length === 0 && !matchedProfile) {
464
+ optional.push({
465
+ name: skill.name,
466
+ summary: skill.summary,
467
+ license: skill.license,
468
+ revision: skill.revision,
469
+ installedPath: skill.installedPath,
470
+ profiles: [...skill.matching.profiles],
471
+ recommendationStatus: 'optional',
472
+ reasons: [],
473
+ overlapsWith: [...skill.overlapsWith],
474
+ reviewStatus: skill.review.status,
475
+ reviewNotes: skill.review.notes,
476
+ filesCount: skill.files.length,
477
+ installed,
478
+ installableFromToolkit: true
479
+ });
480
+ continue;
481
+ }
482
+
483
+ const reasons = [
484
+ ...matchedSignals.map(sig => `${SIGNAL_LABELS[sig] ?? sig} (${signalMap.get(sig).sources[0]})`),
485
+ ...(matchedProfile ? [`perfil ${profile}`] : [])
486
+ ];
487
+ const scenarios = targetClients.map(target => ({ client: target, scenario: findApplicableScenario(skill, observedIds, profile, target) }));
488
+ const allScenariosPassed = scenarios.length > 0 && scenarios.every(item => item.scenario);
489
+
490
+ const baseItem = {
491
+ name: skill.name,
492
+ summary: skill.summary,
493
+ license: skill.license,
494
+ revision: skill.revision,
495
+ installedPath: skill.installedPath,
496
+ reasons,
497
+ matchedSignals,
498
+ overlapsWith: [...skill.overlapsWith],
499
+ reviewStatus: skill.review.status,
500
+ reviewNotes: skill.review.notes,
501
+ filesCount: skill.files.length,
502
+ installed,
503
+ installableFromToolkit: true
504
+ };
505
+
506
+ if (allScenariosPassed && skill.review.status !== 'pending') {
507
+ recommended.push({
508
+ ...baseItem,
509
+ recommendationStatus: 'validated',
510
+ taskClass: scenarios[0].scenario.taskClass,
511
+ evidencePath: scenarios[0].scenario.evidencePath,
512
+ evidence: scenarios.map(({ client: target, scenario }) => ({ client: target, scenarioId: scenario.id, taskClass: scenario.taskClass, evidencePath: scenario.evidencePath }))
513
+ });
514
+ } else {
515
+ const pendingReasons = [];
516
+ if (skill.review.status === 'pending') pendingReasons.push('revisión de contenido pendiente');
517
+ if (targetClients.length === 0) pendingReasons.push('indica --client para comprobar un escenario del agente destino');
518
+ for (const item of scenarios) {
519
+ if (!item.scenario) pendingReasons.push(`sin escenario funcional pasado aplicable a ${item.client}`);
520
+ }
521
+ candidates.push({
522
+ ...baseItem,
523
+ recommendationStatus: 'compatible',
524
+ pendingReasons
525
+ });
526
+ }
527
+ }
528
+
529
+ optional.sort((a, b) => a.name.localeCompare(b.name));
530
+ // The skills that match more of what was detected come first; ties by name.
531
+ const byMatches = (a, b) => b.matchedSignals.length - a.matchedSignals.length || a.name.localeCompare(b.name);
532
+ recommended.sort(byMatches);
533
+ candidates.sort(byMatches);
534
+
535
+ const externalCandidates = [];
536
+ for (const ref of catalog.externalReferences) {
537
+ const matchedSignals = ref.anySignals.filter(sig => observedIds.has(sig));
538
+ for (const sig of matchedSignals) coveredSignals.add(sig);
539
+ const matchedProfile = Boolean(profile && ref.profiles.includes(profile));
540
+ if (matchedSignals.length === 0 && !matchedProfile) continue;
541
+ externalCandidates.push({
542
+ name: ref.name,
543
+ summary: ref.summary,
544
+ sourceUrl: ref.sourceUrl,
545
+ reasons: [
546
+ ...matchedSignals.map(sig => SIGNAL_LABELS[sig] ?? sig),
547
+ ...(matchedProfile ? [`perfil ${profile}`] : [])
548
+ ],
549
+ statusNote: ref.statusNote,
550
+ installableFromToolkit: false
551
+ });
552
+ }
553
+
554
+ const unmatchedSignals = inspection.signals
555
+ .filter(sig => !coveredSignals.has(sig.id))
556
+ .map(sig => ({ id: sig.id, label: sig.label }));
557
+
558
+ return {
559
+ projectName: inspection.projectName,
560
+ projectRoot: inspection.projectRoot,
561
+ profile: profile ?? null,
562
+ client: client ?? null,
563
+ clients: [...targetClients],
564
+ signals: inspection.signals,
565
+ recommended,
566
+ candidates,
567
+ optional,
568
+ externalCandidates,
569
+ unmatchedSignals,
570
+ addons: catalog.addons,
571
+ installed: inspection.installed,
572
+ coupling: inspection.coupling ?? null,
573
+ limits: inspection.limits
574
+ };
575
+ }
576
+
577
+ async function ensureSafeProjectParents(projectRoot, target, create = false) {
578
+ let current = projectRoot;
579
+ for (const part of path.relative(projectRoot, path.dirname(target)).split(path.sep)) {
580
+ if (!part) continue;
581
+ current = path.join(current, part);
582
+ let info = await lstatOrNull(current);
583
+ if (!info && create) {
584
+ await fs.mkdir(current, { mode: 0o755 }).catch(err => {
585
+ if (err.code !== 'EEXIST') throw err;
586
+ });
587
+ info = await lstatOrNull(current);
588
+ }
589
+ if (info && (!info.isDirectory() || info.isSymbolicLink())) {
590
+ throw new Error(`Un directorio padre en el proyecto está ocupado o es un enlace: ${current}`);
591
+ }
592
+ }
593
+ }
594
+
595
+ async function projectTargetState(source, target, mode, catalogEntry) {
596
+ const info = await lstatOrNull(target);
597
+ if (!info) return 'missing';
598
+ if (mode === 'symlink') {
599
+ if (!info.isSymbolicLink()) return 'conflict';
600
+ try {
601
+ const [actual, expected] = await Promise.all([fs.realpath(target), fs.realpath(source)]);
602
+ return normalizedPath(actual) === normalizedPath(expected) ? 'installed' : 'replace';
603
+ } catch (err) {
604
+ if (err.code === 'ENOENT') return 'replace';
605
+ throw err;
606
+ }
607
+ }
608
+ // mode === 'copy': links are classified by the planner; here one is never assumed replaceable.
609
+ if (info.isSymbolicLink()) return 'conflict';
610
+ if (info.isDirectory() && catalogEntry) {
611
+ const integrity = await verifyCatalogIntegrity(target, catalogEntry);
612
+ return integrity === 'verified' ? 'installed' : 'conflict';
613
+ }
614
+ return 'conflict';
615
+ }
616
+
617
+ export async function planProjectSkillInstall({
618
+ projectRoot,
619
+ skillNames,
620
+ agent = 'all',
621
+ mode = 'symlink',
622
+ gitIgnore = 'gitignore',
623
+ catalog,
624
+ toolkitRoot = TOOLKIT_ROOT,
625
+ linkRoot,
626
+ homeDirectory = os.homedir()
627
+ } = {}) {
628
+ if (!['codex', 'claude', 'antigravity', 'all'].includes(agent)) {
629
+ throw new Error('El agente debe ser codex, claude, antigravity o all.');
630
+ }
631
+ if (!VALID_INSTALL_MODES.has(mode)) {
632
+ throw new Error('El modo de instalación debe ser symlink o copy.');
633
+ }
634
+ if (!VALID_GIT_IGNORE.has(gitIgnore)) {
635
+ throw new Error('La estrategia gitIgnore debe ser gitignore, git-exclude o none.');
636
+ }
637
+ const clients = projectClientsFor(agent);
638
+ const root = await fs.realpath(projectRoot);
639
+ const resolvedToolkit = await fs.realpath(toolkitRoot);
640
+ const home = await fs.realpath(homeDirectory).catch(() => path.resolve(homeDirectory));
641
+ const roots = await resolveRoots({ toolkitRoot: resolvedToolkit, homeDirectory: home });
642
+ // An explicit linkRoot means the caller chose where to link, so no stable copy is made.
643
+ const linkRootPath = linkRoot ? path.resolve(linkRoot) : roots.linkRoot;
644
+ const ephemeral = !linkRoot && roots.ephemeral;
645
+ const activeCatalog = catalog ?? await loadSkillCatalog();
646
+ const catalogByName = new Map(activeCatalog.skills.map(s => [s.name, s]));
647
+ const uniqueSkills = [...new Set(skillNames ?? [])].sort();
648
+
649
+ // A broken link is nexakit's only with proof: the install registry (the exact link), or this project's
650
+ // manifest. An entry written by this version records the exact link too; one from 0.1.0 only has the path,
651
+ // so its link must at least have nexakit's own shape: `<root>/<where this skill lives in nexakit>`.
652
+ const registry = await readRegistry(nexakitDir(home));
653
+ const previousManifest = await readProjectToolkitManifest(root);
654
+ const manifested = new Map((previousManifest?.entries ?? []).filter(entry => entry?.relativePath).map(entry => [entry.relativePath, entry]));
655
+ const ownedBy = suffix => (target, literal) => {
656
+ if (isRegistered(registry, target, literal)) return true;
657
+ const entry = manifested.get(path.relative(root, target).split(path.sep).join('/'));
658
+ if (!entry) return false;
659
+ if (typeof entry.apunta === 'string') return entry.apunta === literal;
660
+ return path.resolve(path.dirname(target), literal).endsWith(path.sep + path.normalize(suffix));
661
+ };
662
+ const activeRoots = [linkRootPath, resolvedToolkit, actualRoot(home)];
663
+
664
+ const links = [];
665
+ const verifiedCatalogEntries = new Map();
666
+
667
+ for (const skillName of uniqueSkills) {
668
+ if (!SKILL_NAME_RE.test(skillName)) throw new Error(`Nombre de skill inválido: ${skillName}`);
669
+ const catalogEntry = catalogByName.get(skillName) ?? null;
670
+ let source;
671
+
672
+ if (catalogEntry) {
673
+ const check = await verifyBundledSkillHashes({ skillEntry: catalogEntry, toolkitRoot: resolvedToolkit });
674
+ if (!check.ok) throw new Error(check.reason);
675
+ source = check.sourceDir;
676
+ verifiedCatalogEntries.set(skillName, catalogEntry);
677
+ } else {
678
+ source = path.join(resolvedToolkit, 'skills', skillName);
679
+ const skillMd = await lstatOrNull(path.join(source, 'SKILL.md'));
680
+ if (!skillMd?.isFile() || skillMd.isSymbolicLink()) {
681
+ throw new Error(`No existe la skill empaquetada ${skillName} en Agent Toolkit.`);
682
+ }
683
+ }
684
+
685
+ for (const client of clients) {
686
+ const target = path.join(root, client === 'codex' ? '.agents' : '.claude', 'skills', skillName);
687
+ await ensureSafeProjectParents(root, target, false);
688
+ const serves = servesFor(client, agent);
689
+ const suffix = path.relative(resolvedToolkit, source);
690
+ if (mode === 'symlink') {
691
+ const info = await classifyLink({ target, suffix, activeRoots, isOwned: ownedBy(suffix) });
692
+ links.push({
693
+ client, serves, name: skillName, source: path.join(linkRootPath, suffix), target, mode,
694
+ state: COARSE_STATE[info.state], linkState: info.state,
695
+ ...(info.literal && { literal: info.literal }), ...(info.root && { root: info.root })
696
+ });
697
+ continue;
698
+ }
699
+ if ((await lstatOrNull(target))?.isSymbolicLink()) {
700
+ // A link where a copy goes is replaced only when it is provably nexakit's; any other link is the user's.
701
+ const info = await classifyLink({ target, suffix, activeRoots, isOwned: ownedBy(suffix) });
702
+ const ours = ['instalado', 'otra-copia', 'efimero', 'roto-propio'].includes(info.state);
703
+ links.push({
704
+ client, serves, name: skillName, source, target, mode, state: ours ? 'replace' : 'conflict',
705
+ linkState: info.state, ...(info.literal && { literal: info.literal })
706
+ });
707
+ continue;
708
+ }
709
+ const state = await projectTargetState(source, target, mode, catalogEntry);
710
+ links.push({ client, serves, name: skillName, source, target, state, mode });
711
+ }
712
+ }
713
+
714
+ const stateDirPath = path.join(root, PROJECT_TOOLKIT_DIR);
715
+ const stateDirInfo = await lstatOrNull(stateDirPath);
716
+ const manifestPath = path.join(stateDirPath, PROJECT_MANIFEST_FILE);
717
+ const manifestInfo = stateDirInfo?.isDirectory() && !stateDirInfo.isSymbolicLink()
718
+ ? await lstatOrNull(manifestPath)
719
+ : null;
720
+ const stateDirConflict = Boolean(
721
+ (stateDirInfo && (!stateDirInfo.isDirectory() || stateDirInfo.isSymbolicLink())) ||
722
+ (manifestInfo && (!manifestInfo.isFile() || manifestInfo.isSymbolicLink()))
723
+ );
724
+
725
+ const lockfilePath = path.join(stateDirPath, PROJECT_LOCKFILE_NAME);
726
+ const lockInfo = stateDirInfo?.isDirectory() && !stateDirInfo.isSymbolicLink()
727
+ ? await lstatOrNull(lockfilePath)
728
+ : null;
729
+ const lockfileConflict = Boolean(mode === 'copy' && lockInfo && (!lockInfo.isFile() || lockInfo.isSymbolicLink()));
730
+
731
+ const hasValidTargets = uniqueSkills.length > 0 && links.some(l => ['missing', 'replace', 'installed'].includes(l.state));
732
+ const willWriteManifest = Boolean(!stateDirConflict && hasValidTargets);
733
+ const willWriteLockfile = Boolean(mode === 'copy' && !stateDirConflict && !lockfileConflict && hasValidTargets);
734
+
735
+ const gitIgnoreInfo = await inspectGitIgnoreTarget(root, gitIgnore);
736
+ const gitIgnoreConflict = Boolean(gitIgnore !== 'none' && gitIgnoreInfo.conflict);
737
+ const willUpdateGitIgnore = Boolean(
738
+ gitIgnore !== 'none' &&
739
+ !gitIgnoreConflict &&
740
+ !gitIgnoreInfo.unavailable &&
741
+ willWriteManifest
742
+ );
743
+
744
+ return {
745
+ applied: false,
746
+ mode,
747
+ gitIgnore,
748
+ projectRoot: root,
749
+ toolkitRoot: resolvedToolkit,
750
+ linkRoot: linkRootPath,
751
+ ephemeral,
752
+ homeDirectory: home,
753
+ agent,
754
+ links,
755
+ stateDirPath,
756
+ manifestPath,
757
+ stateDirConflict,
758
+ lockfilePath,
759
+ willWriteManifest,
760
+ willWriteLockfile,
761
+ lockfileConflict,
762
+ gitIgnoreTarget: gitIgnoreInfo.targetPath,
763
+ gitIgnoreUnavailable: Boolean(gitIgnoreInfo.unavailable),
764
+ gitIgnoreConflict,
765
+ willUpdateGitIgnore,
766
+ verifiedCatalogEntries: Object.fromEntries(verifiedCatalogEntries),
767
+ created: 0,
768
+ conflicts:
769
+ links.filter(l => l.state === 'conflict').length +
770
+ (stateDirConflict ? 1 : 0) +
771
+ (lockfileConflict ? 1 : 0) +
772
+ (gitIgnoreConflict ? 1 : 0)
773
+ };
774
+ }
775
+
776
+ async function copyVerifiedSkillDirectory(sourceDir, targetDir, catalogEntry, lock) {
777
+ await lock?.assertHeld();
778
+ await fs.mkdir(targetDir, { recursive: true, mode: 0o755 });
779
+ if (catalogEntry) {
780
+ for (const file of catalogEntry.files) {
781
+ const srcFile = path.join(sourceDir, file.path);
782
+ const dstFile = path.join(targetDir, file.path);
783
+ const bytes = await fs.readFile(srcFile);
784
+ const actualHash = createHash('sha256').update(bytes).digest('hex');
785
+ if (actualHash !== file.sha256) {
786
+ throw new Error(`Integridad SHA-256 rechazada durante la copia de ${catalogEntry.name} (${file.path}).`);
787
+ }
788
+ await lock?.assertHeld();
789
+ await fs.mkdir(path.dirname(dstFile), { recursive: true, mode: 0o755 });
790
+ await fs.writeFile(dstFile, bytes, { mode: 0o644 });
791
+ }
792
+ } else {
793
+ await lock?.assertHeld();
794
+ await fs.cp(sourceDir, targetDir, { recursive: true, dereference: false });
795
+ }
796
+ }
797
+
798
+ async function writeSkillsLockfile(lockfilePath, activeEntriesBySkill, lock) {
799
+ await lock?.assertHeld();
800
+ await fs.mkdir(path.dirname(lockfilePath), { recursive: true, mode: 0o755 });
801
+ const existingInfo = await lstatOrNull(lockfilePath);
802
+ if (existingInfo && (!existingInfo.isFile() || existingInfo.isSymbolicLink())) {
803
+ return false;
804
+ }
805
+ let baseSkills = {};
806
+ if (existingInfo) {
807
+ try {
808
+ const parsed = JSON.parse(await fs.readFile(lockfilePath, 'utf8'));
809
+ if (parsed && typeof parsed.skills === 'object' && !Array.isArray(parsed.skills)) {
810
+ baseSkills = { ...parsed.skills };
811
+ }
812
+ } catch {
813
+ return false;
814
+ }
815
+ }
816
+
817
+ for (const [name, { catalogEntry, clients }] of activeEntriesBySkill.entries()) {
818
+ if (!catalogEntry) continue;
819
+ const prevClients = Array.isArray(baseSkills[name]?.clients) ? baseSkills[name].clients : [];
820
+ const mergedClients = [...new Set([...prevClients, ...clients])].sort();
821
+ baseSkills[name] = {
822
+ repository: catalogEntry.repository,
823
+ sourcePath: catalogEntry.sourcePath,
824
+ revision: catalogEntry.revision,
825
+ license: catalogEntry.license,
826
+ clients: mergedClients,
827
+ files: catalogEntry.files.map(f => ({ path: f.path, sha256: f.sha256 }))
828
+ };
829
+ }
830
+
831
+ const sortedSkills = Object.fromEntries(
832
+ Object.keys(baseSkills).sort().map(k => [k, baseSkills[k]])
833
+ );
834
+
835
+ const payload = {
836
+ lockfileVersion: 1,
837
+ generatedBy: 'nexakit',
838
+ skills: sortedSkills
839
+ };
840
+ await lock?.assertHeld();
841
+ await fs.writeFile(lockfilePath, JSON.stringify(payload, null, 2) + '\n', { mode: 0o644 });
842
+ return true;
843
+ }
844
+
845
+ async function writeProjectManifestAndIgnore({
846
+ projectRoot,
847
+ mode,
848
+ gitIgnore,
849
+ prevManifest,
850
+ coupledEntriesMap,
851
+ lock,
852
+ answers = prevManifest?.answers
853
+ }) {
854
+ const stateDir = path.join(projectRoot, PROJECT_TOOLKIT_DIR);
855
+ const stateStat = await lstatOrNull(stateDir);
856
+ if (stateStat && (!stateStat.isDirectory() || stateStat.isSymbolicLink())) {
857
+ return { manifestWritten: false, gitIgnoreUpdated: false };
858
+ }
859
+ await lock?.assertHeld();
860
+ await fs.mkdir(stateDir, { recursive: true, mode: 0o755 });
861
+
862
+ const entries = [...coupledEntriesMap.values()].sort((a, b) =>
863
+ a.relativePath.localeCompare(b.relativePath)
864
+ );
865
+
866
+ let createdIgnoreFile = Boolean(prevManifest?.createdIgnoreFile);
867
+ let gitIgnoreUpdated = false;
868
+
869
+ if (gitIgnore !== 'none') {
870
+ const info = await inspectGitIgnoreTarget(projectRoot, gitIgnore);
871
+ if (!info.conflict && !info.unavailable && info.targetPath) {
872
+ await lock?.assertHeld();
873
+ await fs.mkdir(path.dirname(info.targetPath), { recursive: true, mode: 0o755 });
874
+ const currentStat = await lstatOrNull(info.targetPath);
875
+ if (!currentStat) {
876
+ if (gitIgnore === 'gitignore') createdIgnoreFile = true;
877
+ const nextText = upsertManagedIgnoreBlock('', entries.map(e => e.relativePath));
878
+ await lock?.assertHeld();
879
+ await fs.writeFile(info.targetPath, nextText, { mode: 0o644 });
880
+ gitIgnoreUpdated = true;
881
+ } else if (currentStat.isFile() && !currentStat.isSymbolicLink()) {
882
+ const prevText = await fs.readFile(info.targetPath, 'utf8');
883
+ const nextText = upsertManagedIgnoreBlock(prevText, entries.map(e => e.relativePath));
884
+ if (nextText !== prevText) {
885
+ await lock?.assertHeld();
886
+ await fs.writeFile(info.targetPath, nextText, { mode: 0o644 });
887
+ }
888
+ gitIgnoreUpdated = true;
889
+ }
890
+
891
+ // Clean stale block from the other ignore location if user switched strategy
892
+ const otherStrategy = gitIgnore === 'gitignore' ? 'git-exclude' : 'gitignore';
893
+ const otherInfo = await inspectGitIgnoreTarget(projectRoot, otherStrategy);
894
+ if (!otherInfo.conflict && !otherInfo.unavailable && otherInfo.targetPath) {
895
+ const otherStat = await lstatOrNull(otherInfo.targetPath);
896
+ if (otherStat?.isFile() && !otherStat.isSymbolicLink()) {
897
+ const otherText = await fs.readFile(otherInfo.targetPath, 'utf8');
898
+ const cleaned = removeManagedIgnoreBlock(otherText);
899
+ if (cleaned.changed) {
900
+ await lock?.assertHeld();
901
+ if (!cleaned.content && otherStrategy === 'gitignore' && prevManifest?.createdIgnoreFile) {
902
+ await fs.unlink(otherInfo.targetPath).catch(() => {});
903
+ createdIgnoreFile = false;
904
+ } else {
905
+ await fs.writeFile(otherInfo.targetPath, cleaned.content, { mode: 0o644 });
906
+ }
907
+ }
908
+ }
909
+ }
910
+ }
911
+ }
912
+
913
+ const manifestPayload = {
914
+ manifestVersion: 1,
915
+ managedBy: 'nexakit',
916
+ mode,
917
+ gitIgnoreStrategy: gitIgnore,
918
+ createdIgnoreFile,
919
+ entries,
920
+ ...(answers && { answers })
921
+ };
922
+ // A manifest that is a link or not a regular file is not nexakit's: it is never written through.
923
+ const manifestPath = path.join(stateDir, PROJECT_MANIFEST_FILE);
924
+ const manifestStat = await lstatOrNull(manifestPath);
925
+ if (manifestStat && (!manifestStat.isFile() || manifestStat.isSymbolicLink())) {
926
+ return { manifestWritten: false, gitIgnoreUpdated };
927
+ }
928
+ const temporary = `${manifestPath}.${process.pid}.tmp`;
929
+ await fs.writeFile(temporary, JSON.stringify(manifestPayload, null, 2) + '\n', { mode: 0o644, flag: 'wx' });
930
+ try {
931
+ await lock?.assertHeld();
932
+ await fs.rename(temporary, manifestPath);
933
+ } finally {
934
+ await fs.rm(temporary, { force: true }); // ours: created exclusively above
935
+ }
936
+ return { manifestWritten: true, gitIgnoreUpdated };
937
+ }
938
+
939
+ // Links are written under the install lock: it prepares the stable copy when running from an npx cache,
940
+ // re-checks the hashes where the links will point, and records every link so a broken one can be proven ours.
941
+ export async function applyProjectSkillInstall(plan, { runNpm, packageSpec, lockOptions, acquire = acquireLock } = {}) {
942
+ if (!plan || !Array.isArray(plan.links) || !plan.projectRoot) {
943
+ throw new Error('Se requiere una vista previa válida de instalación de skills.');
944
+ }
945
+ const mode = plan.mode ?? 'symlink';
946
+ const gitIgnore = plan.gitIgnore ?? 'gitignore';
947
+ // Anything that writes (links, copies, the manifest, ignore rules or the lockfile) runs under the lock.
948
+ // With nothing to write, nothing is written, so a run that changes nothing never creates ~/.nexakit.
949
+ const pending = plan.links.some(l => ['missing', 'replace'].includes(l.state));
950
+ const previous = await readProjectToolkitManifest(plan.projectRoot);
951
+ const settingsChanged = Boolean(previous) && (previous.mode !== mode || previous.gitIgnoreStrategy !== gitIgnore);
952
+ if (!pending && !settingsChanged) return applyProjectSkillInstallLocked(plan, { registryEntries: [], lock: null, writeMetadata: false });
953
+
954
+ const home = plan.homeDirectory ?? os.homedir();
955
+ const lock = await acquire(nexakitDir(home), lockOptions);
956
+ const registryEntries = [];
957
+ try {
958
+ if (mode === 'symlink' && pending) {
959
+ if (plan.ephemeral) {
960
+ const { version } = JSON.parse(await fs.readFile(path.join(plan.toolkitRoot ?? TOOLKIT_ROOT, 'package.json'), 'utf8'));
961
+ await ensureStableInstall({
962
+ homeDirectory: home, version, lock, ...(runNpm && { runNpm }), ...(packageSpec && { packageSpec })
963
+ });
964
+ }
965
+ const linkRoot = plan.linkRoot ?? plan.toolkitRoot ?? TOOLKIT_ROOT;
966
+ if (path.resolve(linkRoot) !== path.resolve(plan.toolkitRoot ?? TOOLKIT_ROOT)) {
967
+ // The files that get linked are the ones in linkRoot, so their hashes must hold there too.
968
+ for (const entry of Object.values(plan.verifiedCatalogEntries ?? {})) {
969
+ const check = await verifyBundledSkillHashes({ skillEntry: entry, toolkitRoot: linkRoot });
970
+ if (!check.ok) throw new Error(check.reason);
971
+ }
972
+ }
973
+ }
974
+ const result = await applyProjectSkillInstallLocked(plan, { registryEntries, lock, writeMetadata: true });
975
+ await recordEntries(nexakitDir(home), registryEntries, lock);
976
+ return result;
977
+ } finally {
978
+ await lock.release();
979
+ }
980
+ }
981
+
982
+ // `lock` is null only when nothing will be written; every write checks it still holds the lock just before.
983
+ async function applyProjectSkillInstallLocked(plan, { registryEntries, lock, writeMetadata }) {
984
+ const mode = plan.mode ?? 'symlink';
985
+ const gitIgnore = plan.gitIgnore ?? 'gitignore';
986
+ const catalogEntries = plan.verifiedCatalogEntries ?? {};
987
+
988
+ // Re-verify SHA-256 of every catalog skill right before writing
989
+ for (const entry of Object.values(catalogEntries)) {
990
+ const check = await verifyBundledSkillHashes({
991
+ skillEntry: entry,
992
+ toolkitRoot: plan.toolkitRoot ?? TOOLKIT_ROOT
993
+ });
994
+ if (!check.ok) throw new Error(check.reason);
995
+ }
996
+
997
+ const prevManifest = await readProjectToolkitManifest(plan.projectRoot);
998
+ const coupledEntriesMap = new Map();
999
+ if (prevManifest) {
1000
+ for (const entry of prevManifest.entries) {
1001
+ if (!entry?.relativePath) continue;
1002
+ const existing = await lstatOrNull(path.join(plan.projectRoot, entry.relativePath));
1003
+ if (existing) coupledEntriesMap.set(`${entry.client}:${entry.name}`, entry);
1004
+ }
1005
+ }
1006
+
1007
+ let created = 0;
1008
+ const updatedLinks = [];
1009
+ const installedClientsBySkill = new Map();
1010
+
1011
+ for (const item of plan.links) {
1012
+ const catalogEntry = catalogEntries[item.name] ?? null;
1013
+ const relPath = `${item.client === 'codex' ? '.agents' : '.claude'}/skills/${item.name}`;
1014
+
1015
+ if (!['missing', 'replace'].includes(item.state)) {
1016
+ if (item.state === 'installed' && coupledEntriesMap.has(`${item.client}:${item.name}`)) {
1017
+ const list = installedClientsBySkill.get(item.name) ?? [];
1018
+ list.push(item.client);
1019
+ installedClientsBySkill.set(item.name, list);
1020
+ }
1021
+ updatedLinks.push({ ...item });
1022
+ continue;
1023
+ }
1024
+
1025
+ await lock?.assertHeld();
1026
+ await ensureSafeProjectParents(plan.projectRoot, item.target, true);
1027
+ let literal;
1028
+ if (mode === 'symlink') {
1029
+ // Re-reads the link right before writing: if it is no longer what the plan saw, it is left alone.
1030
+ await lock?.assertHeld();
1031
+ const outcome = await relinkIfUnchanged({
1032
+ target: item.target, expectedLiteral: item.linkState === 'falta' ? null : item.literal, source: item.source, relative: true
1033
+ });
1034
+ if (outcome === 'cambio') {
1035
+ updatedLinks.push({ ...item, state: 'conflict', changed: true });
1036
+ continue;
1037
+ }
1038
+ // The registry and the manifest keep what readlink returns, the value that is compared later.
1039
+ literal = await fs.readlink(item.target);
1040
+ registryEntries.push({ destino: item.target, apunta: literal, raiz: plan.linkRoot ?? plan.toolkitRoot });
1041
+ } else {
1042
+ const current = await lstatOrNull(item.target);
1043
+ if (current?.isSymbolicLink()) {
1044
+ // Only a link the plan proved to be nexakit's becomes a copy, and only if it did not change since.
1045
+ if (item.state !== 'replace' || !item.literal || await fs.readlink(item.target) !== item.literal) {
1046
+ updatedLinks.push({ ...item, state: 'conflict', changed: true });
1047
+ continue;
1048
+ }
1049
+ await lock?.assertHeld();
1050
+ await fs.unlink(item.target);
1051
+ } else {
1052
+ const currentState = await projectTargetState(item.source, item.target, mode, catalogEntry);
1053
+ if (currentState === 'conflict') {
1054
+ updatedLinks.push({ ...item, state: 'conflict' });
1055
+ continue;
1056
+ }
1057
+ if (currentState === 'installed') {
1058
+ if (coupledEntriesMap.has(`${item.client}:${item.name}`)) {
1059
+ const list = installedClientsBySkill.get(item.name) ?? [];
1060
+ list.push(item.client);
1061
+ installedClientsBySkill.set(item.name, list);
1062
+ }
1063
+ updatedLinks.push({ ...item, state: 'installed' });
1064
+ continue;
1065
+ }
1066
+ }
1067
+ await copyVerifiedSkillDirectory(item.source, item.target, catalogEntry, lock);
1068
+ }
1069
+
1070
+ created++;
1071
+ coupledEntriesMap.set(`${item.client}:${item.name}`, {
1072
+ client: item.client,
1073
+ name: item.name,
1074
+ relativePath: relPath,
1075
+ mode,
1076
+ ...(literal && { apunta: literal }),
1077
+ ...(catalogEntry?.revision && { revision: catalogEntry.revision })
1078
+ });
1079
+ const list = installedClientsBySkill.get(item.name) ?? [];
1080
+ list.push(item.client);
1081
+ installedClientsBySkill.set(item.name, list);
1082
+ updatedLinks.push({ ...item, state: 'installed' });
1083
+ }
1084
+
1085
+ let lockfileWritten = false;
1086
+ if (writeMetadata && mode === 'copy' && !plan.stateDirConflict && !plan.lockfileConflict && installedClientsBySkill.size > 0) {
1087
+ const activeMap = new Map();
1088
+ for (const [skillName, clients] of installedClientsBySkill.entries()) {
1089
+ const catalogEntry = catalogEntries[skillName];
1090
+ if (catalogEntry) activeMap.set(skillName, { catalogEntry, clients });
1091
+ }
1092
+ if (activeMap.size > 0) {
1093
+ lockfileWritten = await writeSkillsLockfile(
1094
+ plan.lockfilePath ?? path.join(plan.projectRoot, PROJECT_TOOLKIT_DIR, PROJECT_LOCKFILE_NAME),
1095
+ activeMap,
1096
+ lock
1097
+ );
1098
+ }
1099
+ }
1100
+
1101
+ let manifestWritten = false;
1102
+ let gitIgnoreUpdated = false;
1103
+ if (writeMetadata && !plan.stateDirConflict && (coupledEntriesMap.size > 0 || lockfileWritten)) {
1104
+ const res = await writeProjectManifestAndIgnore({
1105
+ projectRoot: plan.projectRoot,
1106
+ mode,
1107
+ gitIgnore,
1108
+ prevManifest,
1109
+ coupledEntriesMap,
1110
+ lock
1111
+ });
1112
+ manifestWritten = res.manifestWritten;
1113
+ gitIgnoreUpdated = res.gitIgnoreUpdated;
1114
+ }
1115
+
1116
+ return {
1117
+ applied: true,
1118
+ mode,
1119
+ gitIgnore,
1120
+ projectRoot: plan.projectRoot,
1121
+ links: updatedLinks,
1122
+ manifestWritten,
1123
+ lockfileWritten,
1124
+ gitIgnoreUpdated,
1125
+ created,
1126
+ conflicts:
1127
+ updatedLinks.filter(l => l.state === 'conflict').length +
1128
+ (plan.stateDirConflict ? 1 : 0) +
1129
+ (plan.lockfileConflict ? 1 : 0) +
1130
+ (plan.gitIgnoreConflict ? 1 : 0)
1131
+ };
1132
+ }
1133
+
1134
+ export async function planProjectSkillLinks(options = {}) {
1135
+ return planProjectSkillInstall({ ...options, mode: 'symlink' });
1136
+ }
1137
+
1138
+ export async function applyProjectSkillLinks(plan) {
1139
+ return applyProjectSkillInstall({ ...plan, mode: 'symlink' });
1140
+ }
1141
+
1142
+ // The answers given to `npx nexakit` (skills chosen and discarded, agents, memory), kept in the project manifest
1143
+ // so a second run knows what was decided and does not ask again.
1144
+ const isNameList = value => Array.isArray(value) && value.every(item => typeof item === 'string' && SKILL_NAME_RE.test(item));
1145
+ function validSetupAnswers(answers) {
1146
+ return Boolean(answers && answers.version === 1 &&
1147
+ isNameList(answers.skills?.chosen) && isNameList(answers.skills?.discarded) &&
1148
+ Array.isArray(answers.agents) && answers.agents.every(agent => ['claude', 'codex', 'antigravity'].includes(agent)) &&
1149
+ (answers.memory?.decision === 'later' ||
1150
+ (answers.memory?.decision === 'path' && typeof answers.memory.path === 'string' && path.isAbsolute(answers.memory.path))));
1151
+ }
1152
+
1153
+ export async function readSetupAnswers(projectRoot) {
1154
+ const manifest = await readProjectToolkitManifest(projectRoot);
1155
+ return validSetupAnswers(manifest?.answers) ? manifest.answers : null;
1156
+ }
1157
+
1158
+ export async function writeSetupAnswers(projectRoot, answers, lock) {
1159
+ if (!validSetupAnswers(answers)) throw new Error('Respuestas de nexakit no válidas.');
1160
+ await lock.assertHeld();
1161
+ const prevManifest = await readProjectToolkitManifest(projectRoot);
1162
+ const result = await writeProjectManifestAndIgnore({
1163
+ projectRoot,
1164
+ mode: prevManifest?.mode ?? 'symlink',
1165
+ gitIgnore: prevManifest?.gitIgnoreStrategy ?? 'gitignore',
1166
+ prevManifest,
1167
+ coupledEntriesMap: new Map((prevManifest?.entries ?? []).map(entry => [entry.relativePath, entry])),
1168
+ lock,
1169
+ answers
1170
+ });
1171
+ if (!result.manifestWritten) throw new Error(`${PROJECT_TOOLKIT_DIR} no es una carpeta normal; no se guardaron las respuestas.`);
1172
+ }
1173
+
1174
+ export async function planToolkitDetach({
1175
+ projectRoot,
1176
+ includeGlobal = false,
1177
+ agent = 'all',
1178
+ catalog,
1179
+ toolkitRoot = TOOLKIT_ROOT,
1180
+ homeDirectory = os.homedir(),
1181
+ codexHome,
1182
+ environment = {},
1183
+ skillNames
1184
+ } = {}) {
1185
+ if (!['codex', 'claude', 'antigravity', 'all'].includes(agent)) {
1186
+ throw new Error('El agente debe ser codex, claude, antigravity o all.');
1187
+ }
1188
+ const clients = agent === 'all' ? ['codex', 'claude', 'antigravity'] : [agent];
1189
+ // Antigravity shares Codex's project folder, so removing one's project links removes the other's.
1190
+ const projectClients = [...new Set(clients.map(c => (c === 'antigravity' ? 'codex' : c)))];
1191
+ const resolvedToolkit = await fs.realpath(toolkitRoot);
1192
+ const homeReal = await fs.realpath(homeDirectory).catch(() => path.resolve(homeDirectory));
1193
+ // Links made from an npx run point into the stable copy, which is nexakit's too.
1194
+ const stableVersions = path.join(nexakitDir(homeReal), 'versiones');
1195
+ const registry = await readRegistry(nexakitDir(homeReal));
1196
+ const activeCatalog = catalog ?? await loadSkillCatalog();
1197
+ const catalogByName = new Map(activeCatalog.skills.map(s => [s.name, s]));
1198
+
1199
+ const items = [];
1200
+ const gitIgnoreUpdates = [];
1201
+ let root = null;
1202
+ let manifest = null;
1203
+ let removeStateDir = false;
1204
+ let removeLegacyLockfile = false;
1205
+ let remainingManifestEntries = [];
1206
+
1207
+ if (projectRoot) {
1208
+ root = await fs.realpath(projectRoot);
1209
+ manifest = await readProjectToolkitManifest(root);
1210
+ const legacyLockPath = path.join(root, PROJECT_LOCKFILE_NAME);
1211
+ const legacyLock = await isLegacyToolkitLockfile(legacyLockPath);
1212
+
1213
+ let candidateEntries = [];
1214
+ if (manifest) {
1215
+ // With skillNames only those skills go; the rest of the entries stay in the manifest.
1216
+ const removing = e => projectClients.includes(e.client) && (!skillNames || skillNames.includes(e.name));
1217
+ candidateEntries = manifest.entries.filter(removing);
1218
+ remainingManifestEntries = manifest.entries.filter(e => !removing(e));
1219
+ } else {
1220
+ const extLinks = await findFallbackExternalToolkitLinks(root, resolvedToolkit, projectClients, homeDirectory);
1221
+ const seenKeys = new Set(extLinks.map(e => `${e.client}:${e.name}`));
1222
+ candidateEntries = [...extLinks];
1223
+ if (legacyLock) {
1224
+ for (const [skillName, meta] of Object.entries(legacyLock.skills ?? {})) {
1225
+ const lockClients = Array.isArray(meta?.clients) ? meta.clients : ['codex', 'claude'];
1226
+ for (const client of lockClients) {
1227
+ if (!projectClients.includes(client) || seenKeys.has(`${client}:${skillName}`)) continue;
1228
+ candidateEntries.push({
1229
+ client,
1230
+ name: skillName,
1231
+ relativePath: `${client === 'codex' ? '.agents' : '.claude'}/skills/${skillName}`,
1232
+ mode: 'copy'
1233
+ });
1234
+ }
1235
+ }
1236
+ }
1237
+ }
1238
+
1239
+ if (skillNames) candidateEntries = candidateEntries.filter(entry => skillNames.includes(entry.name));
1240
+ for (const entry of candidateEntries) {
1241
+ if (!SKILL_NAME_RE.test(entry.name)) continue;
1242
+ const target = path.join(root, entry.client === 'codex' ? '.agents' : '.claude', 'skills', entry.name);
1243
+ const relTarget = `${entry.client === 'codex' ? '.agents' : '.claude'}/skills/${entry.name}`;
1244
+ const info = await lstatOrNull(target);
1245
+ if (!info) continue;
1246
+
1247
+ if (info.isSymbolicLink()) {
1248
+ let safeToUnlink = Boolean(manifest);
1249
+ try {
1250
+ const real = await fs.realpath(target);
1251
+ safeToUnlink = (isWithinRoot(real, resolvedToolkit) || isWithinRoot(real, stableVersions)) && !isWithinRoot(real, root);
1252
+ } catch (err) {
1253
+ if (err.code !== 'ENOENT') throw err;
1254
+ }
1255
+ items.push({
1256
+ scope: 'project',
1257
+ client: entry.client,
1258
+ name: entry.name,
1259
+ target,
1260
+ relativePath: relTarget,
1261
+ action: safeToUnlink ? 'remove-link' : 'conflict',
1262
+ reason: safeToUnlink ? 'enlace gestionado por Agent Toolkit' : 'enlace apunta fuera de Agent Toolkit'
1263
+ });
1264
+ } else if (info.isDirectory()) {
1265
+ const catEntry = catalogByName.get(entry.name) ?? null;
1266
+ const integrity = await verifyCatalogIntegrity(target, catEntry);
1267
+ items.push({
1268
+ scope: 'project',
1269
+ client: entry.client,
1270
+ name: entry.name,
1271
+ target,
1272
+ relativePath: relTarget,
1273
+ action: integrity === 'verified' ? 'remove-copy' : 'conflict',
1274
+ reason: integrity === 'verified' ? 'copia verificada sin cambios locales' : 'carpeta modificada localmente (no se borra)',
1275
+ ...(catEntry && { catalogEntry: catEntry })
1276
+ });
1277
+ } else {
1278
+ items.push({
1279
+ scope: 'project',
1280
+ client: entry.client,
1281
+ name: entry.name,
1282
+ target,
1283
+ relativePath: relTarget,
1284
+ action: 'conflict',
1285
+ reason: 'archivo no esperado (no se borra)'
1286
+ });
1287
+ }
1288
+ }
1289
+
1290
+ const projectConflicts = items.filter(i => i.scope === 'project' && i.action === 'conflict').length;
1291
+ const stateDir = path.join(root, PROJECT_TOOLKIT_DIR);
1292
+ const stateDirInfo = await lstatOrNull(stateDir);
1293
+ if (stateDirInfo?.isDirectory() && !stateDirInfo.isSymbolicLink() && remainingManifestEntries.length === 0 && projectConflicts === 0) {
1294
+ removeStateDir = true;
1295
+ }
1296
+ if (legacyLock && remainingManifestEntries.length === 0 && projectConflicts === 0) {
1297
+ removeLegacyLockfile = true;
1298
+ }
1299
+
1300
+ // Check .gitignore and .git/info/exclude for managed blocks
1301
+ const gitignorePath = path.join(root, '.gitignore');
1302
+ if (await hasManagedBlockInFile(gitignorePath)) {
1303
+ gitIgnoreUpdates.push({
1304
+ strategy: 'gitignore',
1305
+ targetPath: gitignorePath,
1306
+ relativePath: '.gitignore',
1307
+ action: remainingManifestEntries.length > 0 ? 'update-block' : 'remove-block',
1308
+ deleteIfEmpty: Boolean(manifest?.createdIgnoreFile && remainingManifestEntries.length === 0)
1309
+ });
1310
+ }
1311
+ const excludeRes = await resolveGitExcludeTarget(root);
1312
+ if (!excludeRes.conflict && !excludeRes.unavailable && await hasManagedBlockInFile(excludeRes.targetPath)) {
1313
+ gitIgnoreUpdates.push({
1314
+ strategy: 'git-exclude',
1315
+ targetPath: excludeRes.targetPath,
1316
+ relativePath: '.git/info/exclude',
1317
+ action: remainingManifestEntries.length > 0 ? 'update-block' : 'remove-block',
1318
+ deleteIfEmpty: false
1319
+ });
1320
+ }
1321
+ }
1322
+
1323
+ const manualCommands = [];
1324
+ if (includeGlobal) {
1325
+ const home = await fs.realpath(homeDirectory);
1326
+ const resolvedCodexHome = codexHome ?? environment.CODEX_HOME ?? path.join(home, '.codex');
1327
+ const bundledSkillsDir = path.join(resolvedToolkit, 'skills');
1328
+ const bundledNames = (await fs.readdir(bundledSkillsDir, { withFileTypes: true }).catch(() => []))
1329
+ .filter(e => e.isDirectory() && SKILL_NAME_RE.test(e.name))
1330
+ .map(e => e.name)
1331
+ .sort();
1332
+
1333
+ for (const client of clients) {
1334
+ for (const name of bundledNames) {
1335
+ const target = globalSkillTarget(home, client, name);
1336
+ const info = await lstatOrNull(target);
1337
+ if (!info?.isSymbolicLink()) continue;
1338
+ // Only links that are provably nexakit's: into this copy, the stable copy, an npx cache, or recorded broken.
1339
+ const link = await classifyLink({
1340
+ target, suffix: `skills/${name}`, activeRoots: [resolvedToolkit, actualRoot(home)],
1341
+ isOwned: (at, literal) => isRegistered(registry, at, literal)
1342
+ });
1343
+ if (['instalado', 'efimero', 'roto-propio'].includes(link.state)) {
1344
+ items.push({
1345
+ scope: 'global',
1346
+ client,
1347
+ name,
1348
+ target,
1349
+ relativePath: target,
1350
+ action: 'remove-global-link',
1351
+ reason: 'enlace global de Agent Toolkit'
1352
+ });
1353
+ }
1354
+ }
1355
+ }
1356
+
1357
+ if (clients.includes('codex')) {
1358
+ for (const fileName of LEVEL_AGENT_NAMES) {
1359
+ const target = path.join(resolvedCodexHome, 'agents', fileName);
1360
+ const info = await lstatOrNull(target);
1361
+ if (!info?.isFile() || info.isSymbolicLink() || info.size > 65536) continue;
1362
+ const content = await fs.readFile(target, 'utf8').catch(() => '');
1363
+ if (content.startsWith(LEVEL_AGENT_HEADER)) {
1364
+ items.push({
1365
+ scope: 'global',
1366
+ client: 'codex',
1367
+ name: fileName,
1368
+ target,
1369
+ relativePath: target,
1370
+ action: 'remove-level-agent',
1371
+ reason: 'agente por nivel generado por Agent Toolkit'
1372
+ });
1373
+ }
1374
+ }
1375
+ manualCommands.push('codex mcp remove agent-toolkit-workflow');
1376
+ }
1377
+ if (clients.includes('claude')) {
1378
+ manualCommands.push('claude mcp remove --scope user agent-toolkit-workflow');
1379
+ }
1380
+ }
1381
+
1382
+ return {
1383
+ reviewInstructions: includeGlobal && !skillNames
1384
+ ? await ownedReviewInstructions({
1385
+ homeDirectory: homeReal,
1386
+ codexHome: codexHome ?? environment.CODEX_HOME ?? path.join(homeReal, '.codex'),
1387
+ agents: clients
1388
+ })
1389
+ : [],
1390
+ homeDirectory: homeReal,
1391
+ reviewCodexHome: codexHome ?? environment.CODEX_HOME ?? path.join(homeReal, ".codex"),
1392
+ reviewAgents: clients,
1393
+ applied: false,
1394
+ projectRoot: root,
1395
+ toolkitRoot: resolvedToolkit,
1396
+ includeGlobal,
1397
+ agent,
1398
+ items,
1399
+ gitIgnoreUpdates,
1400
+ removeStateDir,
1401
+ removeLegacyLockfile,
1402
+ remainingManifestEntries,
1403
+ prevManifest: manifest,
1404
+ manualCommands,
1405
+ conflicts: items.filter(i => i.action === 'conflict').length
1406
+ };
1407
+ }
1408
+
1409
+ async function removeVerifiedSkillCopy(targetDir, catalogEntry) {
1410
+ if (!catalogEntry) return false;
1411
+ const integrity = await verifyCatalogIntegrity(targetDir, catalogEntry);
1412
+ if (integrity !== 'verified') return false;
1413
+
1414
+ const subdirs = new Set();
1415
+ for (const file of catalogEntry.files) {
1416
+ const fullFile = path.join(targetDir, file.path);
1417
+ const bytes = await fs.readFile(fullFile);
1418
+ const actualHash = createHash('sha256').update(bytes).digest('hex');
1419
+ if (actualHash !== file.sha256) return false;
1420
+ await fs.unlink(fullFile);
1421
+ let parent = path.dirname(fullFile);
1422
+ while (normalizedPath(parent) !== normalizedPath(targetDir) && isWithinRoot(parent, targetDir)) {
1423
+ subdirs.add(parent);
1424
+ parent = path.dirname(parent);
1425
+ }
1426
+ }
1427
+ const sortedDirs = [...subdirs].sort((a, b) => b.length - a.length);
1428
+ for (const dir of sortedDirs) {
1429
+ await fs.rmdir(dir).catch(() => {});
1430
+ }
1431
+ await fs.rmdir(targetDir).catch(() => {});
1432
+ return (await lstatOrNull(targetDir)) === null;
1433
+ }
1434
+
1435
+ async function pruneEmptyDirIfSafe(dirPath) {
1436
+ const info = await lstatOrNull(dirPath);
1437
+ if (!info || !info.isDirectory() || info.isSymbolicLink()) return false;
1438
+ try {
1439
+ await fs.rmdir(dirPath);
1440
+ return true;
1441
+ } catch {
1442
+ return false;
1443
+ }
1444
+ }
1445
+
1446
+ export async function applyToolkitDetach(plan) {
1447
+ if (!plan || !Array.isArray(plan.items)) {
1448
+ throw new Error('Se requiere una vista previa válida de desacoplado.');
1449
+ }
1450
+
1451
+ if (plan.reviewInstructions?.length) {
1452
+ const lock = await acquireLock(nexakitDir(plan.homeDirectory));
1453
+ try {
1454
+ await removeReviewInstructions({
1455
+ homeDirectory: plan.homeDirectory,
1456
+ codexHome: plan.reviewCodexHome,
1457
+ agents: plan.reviewAgents
1458
+ }, lock);
1459
+ } finally {
1460
+ await lock.release();
1461
+ }
1462
+ }
1463
+ let removedCount = 0;
1464
+ let conflicts = 0;
1465
+ const updatedItems = [];
1466
+
1467
+ for (const item of plan.items) {
1468
+ if (item.action === 'conflict') {
1469
+ conflicts++;
1470
+ updatedItems.push({ ...item });
1471
+ continue;
1472
+ }
1473
+
1474
+ const info = await lstatOrNull(item.target);
1475
+ if (!info) {
1476
+ updatedItems.push({ ...item, action: 'already-absent' });
1477
+ continue;
1478
+ }
1479
+
1480
+ if (item.action === 'remove-link' || item.action === 'remove-global-link') {
1481
+ if (!info.isSymbolicLink()) {
1482
+ conflicts++;
1483
+ updatedItems.push({ ...item, action: 'conflict' });
1484
+ continue;
1485
+ }
1486
+ await fs.unlink(item.target);
1487
+ removedCount++;
1488
+ updatedItems.push({ ...item, action: 'removed' });
1489
+ continue;
1490
+ }
1491
+
1492
+ if (item.action === 'remove-copy') {
1493
+ if (!info.isDirectory() || info.isSymbolicLink()) {
1494
+ conflicts++;
1495
+ updatedItems.push({ ...item, action: 'conflict' });
1496
+ continue;
1497
+ }
1498
+ const ok = await removeVerifiedSkillCopy(item.target, item.catalogEntry);
1499
+ if (!ok) {
1500
+ conflicts++;
1501
+ updatedItems.push({ ...item, action: 'conflict' });
1502
+ continue;
1503
+ }
1504
+ removedCount++;
1505
+ updatedItems.push({ ...item, action: 'removed' });
1506
+ continue;
1507
+ }
1508
+
1509
+ if (item.action === 'remove-level-agent') {
1510
+ if (!info.isFile() || info.isSymbolicLink()) {
1511
+ conflicts++;
1512
+ updatedItems.push({ ...item, action: 'conflict' });
1513
+ continue;
1514
+ }
1515
+ const text = await fs.readFile(item.target, 'utf8').catch(() => '');
1516
+ if (!text.startsWith(LEVEL_AGENT_HEADER)) {
1517
+ conflicts++;
1518
+ updatedItems.push({ ...item, action: 'conflict' });
1519
+ continue;
1520
+ }
1521
+ await fs.unlink(item.target);
1522
+ removedCount++;
1523
+ updatedItems.push({ ...item, action: 'removed' });
1524
+ }
1525
+ }
1526
+
1527
+ let gitIgnoreCleanedCount = 0;
1528
+ if (plan.projectRoot) {
1529
+ for (const upd of plan.gitIgnoreUpdates ?? []) {
1530
+ const stat = await lstatOrNull(upd.targetPath);
1531
+ if (!stat?.isFile() || stat.isSymbolicLink()) continue;
1532
+ const prevText = await fs.readFile(upd.targetPath, 'utf8');
1533
+ if (upd.action === 'update-block' && plan.remainingManifestEntries?.length > 0) {
1534
+ const nextText = upsertManagedIgnoreBlock(
1535
+ prevText,
1536
+ plan.remainingManifestEntries.map(e => e.relativePath)
1537
+ );
1538
+ await fs.writeFile(upd.targetPath, nextText, { mode: 0o644 });
1539
+ gitIgnoreCleanedCount++;
1540
+ } else if (upd.action === 'remove-block' && conflicts === 0) {
1541
+ const cleaned = removeManagedIgnoreBlock(prevText);
1542
+ if (cleaned.changed) {
1543
+ if (!cleaned.content && upd.deleteIfEmpty) {
1544
+ await fs.unlink(upd.targetPath).catch(() => {});
1545
+ } else {
1546
+ await fs.writeFile(upd.targetPath, cleaned.content, { mode: 0o644 });
1547
+ }
1548
+ gitIgnoreCleanedCount++;
1549
+ }
1550
+ }
1551
+ }
1552
+ }
1553
+
1554
+ let stateDirRemoved = false;
1555
+ if (plan.projectRoot && conflicts === 0) {
1556
+ const stateDir = path.join(plan.projectRoot, PROJECT_TOOLKIT_DIR);
1557
+ if (plan.remainingManifestEntries?.length > 0 && plan.prevManifest) {
1558
+ await fs.writeFile(
1559
+ path.join(stateDir, PROJECT_MANIFEST_FILE),
1560
+ JSON.stringify({ ...plan.prevManifest, entries: plan.remainingManifestEntries }, null, 2) + '\n',
1561
+ { mode: 0o644 }
1562
+ );
1563
+ } else if (plan.removeStateDir) {
1564
+ const lockFile = path.join(stateDir, PROJECT_LOCKFILE_NAME);
1565
+ const manifestFile = path.join(stateDir, PROJECT_MANIFEST_FILE);
1566
+ if ((await lstatOrNull(lockFile))?.isFile()) await fs.unlink(lockFile).catch(() => {});
1567
+ if ((await lstatOrNull(manifestFile))?.isFile()) await fs.unlink(manifestFile).catch(() => {});
1568
+ stateDirRemoved = await pruneEmptyDirIfSafe(stateDir);
1569
+ }
1570
+
1571
+ if (plan.removeLegacyLockfile) {
1572
+ const legacyPath = path.join(plan.projectRoot, PROJECT_LOCKFILE_NAME);
1573
+ if (await isLegacyToolkitLockfile(legacyPath)) {
1574
+ await fs.unlink(legacyPath).catch(() => {});
1575
+ }
1576
+ }
1577
+
1578
+ // Prune empty .agents/skills, .agents, .claude/skills, .claude
1579
+ await pruneEmptyDirIfSafe(path.join(plan.projectRoot, '.agents', 'skills'));
1580
+ await pruneEmptyDirIfSafe(path.join(plan.projectRoot, '.agents'));
1581
+ await pruneEmptyDirIfSafe(path.join(plan.projectRoot, '.claude', 'skills'));
1582
+ await pruneEmptyDirIfSafe(path.join(plan.projectRoot, '.claude'));
1583
+ }
1584
+
1585
+ return {
1586
+ applied: true,
1587
+ projectRoot: plan.projectRoot,
1588
+ includeGlobal: plan.includeGlobal,
1589
+ items: updatedItems,
1590
+ removedCount,
1591
+ gitIgnoreCleanedCount,
1592
+ stateDirRemoved,
1593
+ conflicts,
1594
+ manualCommands: plan.manualCommands ?? []
1595
+ };
1596
+ }
1597
+
1598
+ export function formatDetachReport(report) {
1599
+ const isPreview = !report.applied;
1600
+ const header = isPreview
1601
+ ? 'Agent Toolkit · Vista previa de desacoplado (sin cambios en disco)'
1602
+ : 'Agent Toolkit · Desacoplado completado';
1603
+ const lines = [header];
1604
+ if (report.projectRoot) lines.push(`Proyecto: ${report.projectRoot}`);
1605
+ if (report.includeGlobal) lines.push('Alcance global: incluido (~/.agents, ~/.claude, ~/.codex/agents)');
1606
+ lines.push('');
1607
+
1608
+ if (isPreview) {
1609
+ const actionable = report.items.filter(i => i.action !== 'conflict');
1610
+ if (
1611
+ actionable.length === 0 &&
1612
+ !report.removeStateDir &&
1613
+ !report.removeLegacyLockfile &&
1614
+ (report.gitIgnoreUpdates?.length ?? 0) === 0
1615
+ ) {
1616
+ lines.push('No se encontraron artefactos acoplados por Agent Toolkit para retirar.');
1617
+ } else {
1618
+ lines.push('Elementos que se retirarán:');
1619
+ for (const item of actionable) {
1620
+ lines.push(` - [${item.scope} ${item.client}] ${item.relativePath} (${item.reason})`);
1621
+ }
1622
+ if (report.removeStateDir) {
1623
+ lines.push(` - [estado] ${PROJECT_TOOLKIT_DIR}/ (manifiesto y lockfile locales)`);
1624
+ }
1625
+ if (report.removeLegacyLockfile) {
1626
+ lines.push(' - [legacy] skills-lock.json en raíz');
1627
+ }
1628
+ for (const upd of report.gitIgnoreUpdates ?? []) {
1629
+ lines.push(` - [git] Limpiar bloque gestionado en ${upd.relativePath}`);
1630
+ }
1631
+ }
1632
+ const conflictItems = report.items.filter(i => i.action === 'conflict');
1633
+ if (conflictItems.length > 0) {
1634
+ lines.push('', 'Conflictos respetados (no se tocarán):');
1635
+ for (const c of conflictItems) {
1636
+ lines.push(` ! ${c.relativePath} — ${c.reason}`);
1637
+ }
1638
+ }
1639
+ lines.push('', 'Ejecuta con --apply para confirmar el desacoplado.');
1640
+ } else {
1641
+ lines.push(`Elementos retirados: ${report.removedCount}`);
1642
+ if (report.projectRoot) {
1643
+ lines.push(`Carpeta ${PROJECT_TOOLKIT_DIR}/ retirada: ${report.stateDirRemoved ? 'sí' : 'sin cambios'}`);
1644
+ lines.push(`Archivos ignore limpiados: ${report.gitIgnoreCleanedCount}`);
1645
+ }
1646
+ if (report.conflicts > 0) {
1647
+ lines.push(`Conflictos respetados intactos: ${report.conflicts}`);
1648
+ }
1649
+ }
1650
+
1651
+ if ((report.manualCommands?.length ?? 0) > 0) {
1652
+ lines.push('', 'Para retirar también el servidor MCP de tus clientes (opcional):');
1653
+ for (const cmd of report.manualCommands) lines.push(` $ ${cmd}`);
1654
+ }
1655
+
1656
+ if (report.reviewInstructions?.length) lines.push(` - Quitar solo el bloque de revisión de ${report.reviewInstructions.join(', ')}`);
1657
+ return lines.join('\n') + '\n';
1658
+ }
1659
+
1660
+ // Generic signals: almost every project has them, and the useful skill is the framework's.
1661
+ const GENERIC_SIGNALS = new Set(['nodejs', 'typescript']);
1662
+
1663
+ // The technologies found in the project that no catalog skill (nor external reference) covers. Only reported:
1664
+ // finding a skill for one is a request the person makes to revisar-skill, never a search nexakit starts alone.
1665
+ export function uncoveredSignals({ signals = [], catalog }) {
1666
+ const covered = new Set([
1667
+ ...(catalog?.skills ?? []).flatMap((skill) => skill.matching?.anySignals ?? []),
1668
+ ...(catalog?.externalReferences ?? []).flatMap((ref) => ref.anySignals ?? []),
1669
+ ]);
1670
+ return signals
1671
+ .filter((signal) => !GENERIC_SIGNALS.has(signal.id) && !covered.has(signal.id))
1672
+ .map((signal) => ({ id: signal.id, label: signal.label }));
1673
+ }