nexakit 0.1.0 → 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 (113) hide show
  1. package/.claude-plugin/marketplace.json +18 -1
  2. package/README.md +40 -30
  3. package/assets/brand/README.md +0 -2
  4. package/assets/runtime/package-lock.json +2 -2
  5. package/bin/agent-task.js +12 -10
  6. package/bin/agent-toolkit-gemini.js +11 -9
  7. package/bin/agent-toolkit-inbox.js +12 -10
  8. package/bin/agent-toolkit-mcp.js +14 -8
  9. package/bin/agent-toolkit.js +12 -9
  10. package/bin/nexakit.js +12 -9
  11. package/bin/project-memory-mcp.js +12 -9
  12. package/bin/project-memory.js +5 -3
  13. package/{skills/depurar → catalogs/skills/vercel-composition-patterns}/LICENSE.txt +6 -6
  14. package/catalogs/skills/vercel-react-best-practices/LICENSE.txt +27 -0
  15. package/catalogs/skills.json +70 -35
  16. package/docs/skills/evidencias/2026-10-28-recomendador-preparacion.md +1 -1
  17. package/package.json +5 -3
  18. package/plugins/nexakit-modelo/.claude-plugin/plugin.json +1 -0
  19. package/plugins/nexakit-modelo/hooks/hooks.json +1 -0
  20. package/plugins/nexakit-modelo/hooks/register.js +48 -0
  21. package/plugins/nexakit-modelo/hooks/status.js +8 -0
  22. package/plugins/nexakit-modelo/tests/register.test.ts +21 -0
  23. package/plugins/nexakit-panel/.claude-plugin/plugin.json +1 -0
  24. package/plugins/nexakit-panel/README.md +20 -0
  25. package/plugins/nexakit-panel/hooks/hooks.json +1 -0
  26. package/plugins/nexakit-panel/hooks/lienzo.js +382 -0
  27. package/plugins/nexakit-panel/hooks/panel.js +279 -0
  28. package/plugins/nexakit-panel/hooks/register.tsx +248 -0
  29. package/plugins/nexakit-panel/tests/lienzo.test.ts +142 -0
  30. package/plugins/nexakit-panel/tests/panel.test.ts +240 -0
  31. package/plugins/nexakit-panel/tests/register.test.ts +167 -0
  32. package/plugins/nexakit-panel/types/index.d.ts +41 -0
  33. package/plugins/nexakit-sesion/.claude-plugin/plugin.json +1 -0
  34. package/plugins/nexakit-sesion/README.md +12 -0
  35. package/plugins/nexakit-sesion/hooks/aviso.js +38 -0
  36. package/plugins/nexakit-sesion/hooks/hooks.json +1 -0
  37. package/plugins/nexakit-sesion/hooks/register.js +28 -0
  38. package/plugins/nexakit-sesion/tests/register.test.ts +96 -0
  39. package/schemas/skill-catalog.schema.json +9 -2
  40. package/skills/cerrar-memoria/SKILL.md +6 -2
  41. package/skills/cerrar-memoria/references/cierre.md +5 -1
  42. package/skills/elegir-modelo/SKILL.md +68 -0
  43. package/skills/elegir-modelo/modelos.mjs +108 -0
  44. package/skills/gemini/SKILL.md +1 -1
  45. package/skills/project-handoff/SKILL.md +39 -12
  46. package/skills/project-handoff/references/handoff-template.md +24 -17
  47. package/skills/quality-gate/SKILL.md +6 -12
  48. package/skills/revisar/SKILL.md +6 -16
  49. package/skills/{desarrollo-con-pruebas/LICENSE.txt → revisar-skill/LICENSE-find-skills.txt} +5 -5
  50. package/skills/revisar-skill/SKILL.md +94 -0
  51. package/skills/revision-cruzada/SKILL.md +53 -475
  52. package/skills/web-design-guidelines/AGENTS.md +156 -0
  53. package/skills/{spec-driven-development → web-design-guidelines}/LICENSE.txt +1 -6
  54. package/skills/web-design-guidelines/SKILL.md +20 -30
  55. package/src/agent-models.js +53 -13
  56. package/src/brand.js +117 -0
  57. package/src/cli-executable.js +5 -3
  58. package/src/cli.js +16 -9
  59. package/src/connect.js +56 -4
  60. package/src/init.js +9 -1
  61. package/src/install-lock.js +127 -0
  62. package/src/install-registry.js +61 -0
  63. package/src/legacy-repair.js +71 -0
  64. package/src/link-states.js +172 -0
  65. package/src/memory-locator.js +118 -0
  66. package/src/missing-dependencies.js +21 -0
  67. package/src/model-command.js +147 -0
  68. package/src/panel-cli.js +182 -0
  69. package/src/project-root.js +41 -0
  70. package/src/project-signals.js +339 -0
  71. package/src/recommend-report.js +77 -0
  72. package/src/review-instructions.js +211 -0
  73. package/src/review-policy.js +35 -0
  74. package/src/revision-adapters.js +157 -0
  75. package/src/revision-context.js +86 -0
  76. package/src/revision-isolation.js +222 -0
  77. package/src/revision-process.js +103 -0
  78. package/src/revision-provenance.js +89 -0
  79. package/src/revision-snapshot.js +120 -0
  80. package/src/revision.js +242 -0
  81. package/src/setup-apply.js +231 -0
  82. package/src/setup-plan.js +231 -0
  83. package/src/setup-state.js +277 -0
  84. package/src/setup-tui.js +555 -0
  85. package/src/skill-catalog-add.js +91 -0
  86. package/src/skill-catalog-audit.js +26 -27
  87. package/src/skill-files.js +39 -0
  88. package/src/skill-recommendations.js +321 -208
  89. package/src/skill-scan-report.js +14 -0
  90. package/src/skill-scan.js +178 -0
  91. package/src/stable-command.js +53 -0
  92. package/src/stable-install.js +222 -0
  93. package/src/superpowers.js +133 -0
  94. package/src/sync-apply.js +8 -5
  95. package/src/sync-files.js +2 -1
  96. package/src/terminal-text.js +68 -0
  97. package/src/toolkit-cli.js +397 -70
  98. package/src/toolkit-commands-guide.js +53 -39
  99. package/src/toolkit-setup.js +507 -110
  100. package/assets/brand/agent-toolkit-mark-v1.png +0 -0
  101. package/skills/depurar/SKILL.md +0 -69
  102. package/skills/depurar/agents/openai.yaml +0 -7
  103. package/skills/depurar/references/tecnicas-de-diagnostico.md +0 -37
  104. package/skills/desarrollo-con-pruebas/SKILL.md +0 -55
  105. package/skills/desarrollo-con-pruebas/agents/openai.yaml +0 -7
  106. package/skills/desarrollo-con-pruebas/references/buenas-pruebas.md +0 -33
  107. package/skills/estado/SKILL.md +0 -41
  108. package/skills/programar/SKILL.md +0 -19
  109. package/skills/spec-driven-development/SKILL.md +0 -48
  110. package/skills/spec-driven-development/agents/openai.yaml +0 -7
  111. package/skills/spec-driven-development/references/spec-template.md +0 -55
  112. package/skills/spec-driven-development/references/workflow.md +0 -70
  113. package/src/toolkit-tui.js +0 -961
@@ -0,0 +1,68 @@
1
+ // Text helpers shared by nexakit's terminal screens: color that honours NO_COLOR, the width a character really
2
+ // takes in a terminal cell, wrapping, and cutting a line to fit.
3
+ export function style(colorEnabled) {
4
+ if (!colorEnabled) {
5
+ const plain = text => String(text);
6
+ return { bold: plain, cyan: plain, green: plain, yellow: plain, dim: plain, red: plain, magenta: plain };
7
+ }
8
+ const wrap = (open, close) => text => `\x1b[${open}m${text}\x1b[${close}m`;
9
+ return {
10
+ bold: wrap(1, 22),
11
+ dim: wrap(2, 22),
12
+ red: wrap(31, 39),
13
+ green: wrap(32, 39),
14
+ yellow: wrap(33, 39),
15
+ magenta: wrap(35, 39),
16
+ cyan: wrap(36, 39)
17
+ };
18
+ }
19
+
20
+ export function cellWidth(text) {
21
+ return [...text].reduce((width, char) => {
22
+ const cp = char.codePointAt(0);
23
+ if (/\p{Mark}/u.test(char) || cp === 0x200d || cp === 0xfe0f) return width;
24
+ const wide = /\p{Extended_Pictographic}/u.test(char)
25
+ || (cp >= 0x1100 && (cp <= 0x115f || cp === 0x2329 || cp === 0x232a
26
+ || (cp >= 0x2e80 && cp <= 0xa4cf) || (cp >= 0xac00 && cp <= 0xd7a3)
27
+ || (cp >= 0xf900 && cp <= 0xfaff) || (cp >= 0xfe10 && cp <= 0xfe19)
28
+ || (cp >= 0xfe30 && cp <= 0xfe6f) || (cp >= 0xff00 && cp <= 0xff60)
29
+ || (cp >= 0xffe0 && cp <= 0xffe6) || cp >= 0x20000));
30
+ return width + (wide ? 2 : 1);
31
+ }, 0);
32
+ }
33
+
34
+ export function wrapText(text, width) {
35
+ const lines = [];
36
+ for (const paragraph of String(text).split('\n')) {
37
+ let line = '';
38
+ for (const token of paragraph.match(/\s*\S+/gu) ?? []) {
39
+ const word = token.trimStart();
40
+ let gap = token.slice(0, token.length - word.length);
41
+ if (line && cellWidth(line + gap + word) > width) {
42
+ lines.push(line.trimEnd());
43
+ line = '';
44
+ gap = '';
45
+ }
46
+ for (const char of gap + word) {
47
+ if (cellWidth(line + char) > width) {
48
+ lines.push(line);
49
+ line = '';
50
+ }
51
+ line += char;
52
+ }
53
+ }
54
+ lines.push(line);
55
+ }
56
+ return lines;
57
+ }
58
+
59
+ // Cuts a line to `width` cells, ending in "…" when something was cut.
60
+ export function fitLine(text, width) {
61
+ if (cellWidth(text) <= width) return text;
62
+ let line = '';
63
+ for (const char of text) {
64
+ if (cellWidth(line + char) > width - 1) break;
65
+ line += char;
66
+ }
67
+ return `${line}…`;
68
+ }
@@ -1,99 +1,148 @@
1
+ import os from 'node:os';
2
+ import { runCli } from './cli.js';
3
+ import { runModelCommand } from './model-command.js';
4
+ import { runPanelCommand } from './panel-cli.js';
5
+ import { runRevisionCommand } from './revision.js';
1
6
  import path from 'node:path';
7
+ import readline from 'node:readline';
2
8
  import { pathToFileURL } from 'node:url';
9
+ import { resolveProjectRoot } from './project-root.js';
3
10
  import { loadSkillCatalog } from './skill-catalog.js';
11
+ import { scanSkillDirectory } from './skill-scan.js';
12
+ import { formatScanReport } from './skill-scan-report.js';
4
13
  import {
5
14
  applyToolkitDetach,
6
15
  formatDetachReport,
7
16
  inspectSkillProject,
8
17
  planToolkitDetach,
9
- recommendSkills
18
+ recommendSkills,
10
19
  } from './skill-recommendations.js';
11
- import { formatRecommendationReport, runToolkitTui } from './toolkit-tui.js';
20
+ import { formatRecommendationReport } from './recommend-report.js';
21
+ import { applySetup } from './setup-apply.js';
22
+ import { buildConfirmation, suggestedAnswers } from './setup-plan.js';
23
+ import { gatherSetupState } from './setup-state.js';
24
+ import { runSetupTui } from './setup-tui.js';
12
25
  import { doctorToolkit, setupToolkit } from './toolkit-setup.js';
26
+ import { applyLegacyRepair, planLegacyRepair } from './legacy-repair.js';
27
+ import { READY_LINK_STATES, REMOVED_BY_USER_STATE, SETTLED_LINK_STATES } from './link-states.js';
13
28
 
14
29
  const PROFILE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
30
+ const SKILL_NAME_RE = /^[a-z0-9-]+$/u;
15
31
  const RECOMMEND_PROFILES = ['frontend', 'backend', 'mcp', 'security'];
16
32
 
17
- const HELP = `Uso: npx nexakit [tui] [--project <ruta>] [--agent codex|claude|all] [--perfil-codex <nombre>]
33
+ const HELP = `Uso: npx nexakit [--project <ruta>] [--si]
34
+ npx nexakit comprobar [--agent codex|claude|antigravity|all] [--perfil-codex <nombre>] [--json]
35
+ npx nexakit quitar [--project <ruta>] [--global] [--agent codex|claude|antigravity|all] [--si|--apply] [--json]
36
+ npx nexakit reparar --desde <ruta> [--agent codex|claude|antigravity|all]
37
+ npx nexakit memoria <subcomando> [opciones]
38
+ nexakit revision --disponibles [--principal claude|codex|antigravity]
39
+ nexakit revision --con <ia> --modelo <m> --esfuerzo <e> --encargo <archivo> [--base <rama>]
40
+ nexakit modelo [--nivel <nivel>] [--ia claude|codex|antigravity] [--perfil-codex <nombre>]
41
+ nexakit panel [--demo] [--una-vez]
18
42
  npx nexakit recommend [--project <ruta>] [--profile frontend|backend|mcp|security] [--client codex|claude] [--dry-run] [--json]
19
- npx nexakit detach [--project <ruta>] [--global] [--agent codex|claude|all] [--apply] [--json]
20
- npx nexakit doctor [--agent codex|claude|all] [--perfil-codex <nombre>] [--json]
21
- npx nexakit setup [--agent codex|claude|all] [--perfil-codex <nombre>] [--apply] [--json]
43
+ npx nexakit scan <carpeta de skill> [--json]
44
+ npx nexakit doctor [--agent codex|claude|antigravity|all] [--perfil-codex <nombre>] [--json]
45
+ npx nexakit setup [--agent codex|claude|antigravity|all] [--perfil-codex <nombre>] [--omitir <skill>] [--reponer <skill>] [--apply] [--json]
22
46
 
47
+ npx nexakit hace tres preguntas con la respuesta ya sugerida (skills, memoria e IA)
48
+ y, con una sola confirmación, deja listo el proyecto: skills, memoria conectada a
49
+ tus IA y la orden ~/.nexakit/nexakit. Si todo está listo, no pregunta nada.
50
+ Sin terminal interactiva muestra lo que haría; --si lo aplica sin preguntas.
51
+ comprobar (o doctor) revisa en solo lectura Node, tus IA, skills base y restos antiguos.
52
+ quitar (o detach) retira las skills del proyecto, .agent-toolkit/ y su bloque de
53
+ .gitignore sin tocar archivos tuyos (--si o --apply para ejecutar; --global incluye las globales).
54
+ scan revisa el contenido de una skill sin ejecutarla (1 si encuentra un bloqueo).
55
+ reparar re-apunta enlaces rotos de una instalación anterior (--desde es la carpeta
56
+ que ya no existe); lista lo que encontró y siempre pregunta.
57
+ memoria ejecuta los subcomandos de project-memory (init, connect, doctor, status, sync).
58
+ revision y modelo piden una revisión a otra IA o consultan los modelos disponibles.
59
+ panel dibuja a pantalla completa la orquesta de tu sesión de Claude Code (ábrelo en otra división).
60
+ recommend inspecciona el proyecto en solo lectura; setup prepara las skills base globales y respeta las que quitaste
61
+ (--omitir <skill> declara quitada una que ya no tiene enlace; --reponer <skill> devuelve una; ambos se repiten).
23
62
  (Alias local compatible: agent-toolkit)
24
- En una terminal interactiva, npx nexakit abre el asistente por pasos:
25
- detecta el stack del proyecto, sugiere skills verificadas por SHA-256,
26
- guarda su estado aislado e ignorado en Git bajo .agent-toolkit/ y aplica
27
- instalaciones o desacoplados ([d]) tras vista previa segura.
28
- recommend inspecciona el proyecto en solo lectura sin escribir ni usar red.
29
- detach retira las skills acopladas, .agent-toolkit/ y el bloque de ignore sin
30
- tocar archivos propios (--apply para ejecutar; --global incluye ~/.claude y ~/.codex).
31
- setup prepara enlaces globales de las skills base; --apply los crea sin reemplazar
32
- archivos existentes. Imprime los comandos MCP para registrar por separado.
33
- doctor comprueba instalación local sin iniciar modelos. Reinicia los clientes
34
- tras registrar el MCP. Con Codex, también indica si su sandbox deja escribir en
35
- ~/.agent-toolkit (lo necesita $revisar) y el comando para permitirlo. Si abres
36
- Codex con --profile, indica ese perfil con --perfil-codex.
37
- Superpowers es un plugin opcional independiente.
38
63
  `;
39
64
 
40
65
  export async function runToolkitCli(
41
66
  argv,
42
- { out, err, input = process.stdin, output = process.stdout, isTTY, options = {} } = {}
67
+ { out, err, input = process.stdin, output = process.stdout, isTTY, options = {} } = {},
43
68
  ) {
44
69
  if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) {
45
70
  await out(HELP);
46
71
  return 0;
47
72
  }
48
73
 
49
- if (argv.length === 0 || argv[0] === 'tui') {
50
- const rest = argv[0] === 'tui' ? argv.slice(1) : [];
51
- const parsed = { agent: 'all', projectRoot: process.cwd() };
74
+ if (argv.length === 0 || argv[0] === 'tui' || argv[0].startsWith('--')) {
75
+ const rest = argv[0] === 'tui' ? argv.slice(1) : argv;
76
+ const parsed = { projectRoot: process.cwd(), yes: false };
52
77
  const seen = new Set();
53
78
  for (let i = 0; i < rest.length; i++) {
54
79
  const flag = rest[i];
55
- if (seen.has(flag)) { await err(HELP); return 2; }
80
+ if (seen.has(flag)) {
81
+ await err(HELP);
82
+ return 2;
83
+ }
56
84
  seen.add(flag);
57
85
  if (flag === '--project' && rest[i + 1]) parsed.projectRoot = rest[++i];
58
- else if (flag === '--agent' && ['codex', 'claude', 'all'].includes(rest[i + 1])) parsed.agent = rest[++i];
59
- else if (flag === '--perfil-codex' && PROFILE_RE.test(rest[i + 1] ?? '')) parsed.codexProfile = rest[++i];
60
- else { await err(HELP); return 2; }
86
+ else if (flag === '--si') parsed.yes = true;
87
+ else {
88
+ await err(HELP);
89
+ return 2;
90
+ }
61
91
  }
62
- const interactive = Boolean(isTTY ?? (input?.isTTY && output?.isTTY));
63
- if (!interactive) {
92
+ return runSetup(parsed, { out, err, input, output, isTTY, options });
93
+ }
94
+
95
+ const [command, ...rest] = argv;
96
+ if (command === 'modelo') return runModelCommand(rest, { out, err }, options);
97
+ if (command === 'revision') return runRevisionCommand(rest, { out, err }, { options });
98
+ if (command === 'panel') return runPanelCommand(rest, { out, err, input, output, isTTY, options });
99
+ if (command === 'memoria') {
100
+ return runCli(
101
+ rest,
102
+ {
103
+ out: (message) => out(`${message}\n`),
104
+ err: (message) => err(`${message}\n`),
105
+ },
106
+ { ...options, commandName: 'npx nexakit memoria' },
107
+ );
108
+ }
109
+
110
+ if (command === 'scan') {
111
+ const flags = rest.filter(arg => arg.startsWith('--'));
112
+ const dirs = rest.filter(arg => !arg.startsWith('--'));
113
+ if (dirs.length !== 1 || flags.some(flag => flag !== '--json') || flags.length > 1) {
64
114
  await err(HELP);
65
115
  return 2;
66
116
  }
67
117
  try {
68
- return await runToolkitTui({
69
- input,
70
- output,
71
- projectRoot: parsed.projectRoot,
72
- agent: parsed.agent,
73
- codexProfile: parsed.codexProfile,
74
- setupOptions: options
75
- });
76
- } catch {
77
- await err('No se pudo abrir la interfaz interactiva. Revisa la ruta del proyecto y los permisos.\n');
78
- return 1;
118
+ const report = await scanSkillDirectory(dirs[0]);
119
+ await out(flags.length ? JSON.stringify(report, null, 2) + '\n' : formatScanReport(report, dirs[0]));
120
+ return report.blocked ? 1 : 0;
121
+ } catch (error) {
122
+ await err(`No se pudo escanear ${dirs[0]}: ${error.code === 'ENOENT' ? 'la carpeta no existe' : error.message}\n`);
123
+ return 2;
79
124
  }
80
125
  }
81
126
 
82
- const [command, ...rest] = argv;
83
-
84
127
  if (command === 'recommend') {
85
128
  const parsed = { projectRoot: process.cwd(), json: false };
86
129
  const seen = new Set();
87
130
  for (let i = 0; i < rest.length; i++) {
88
131
  const flag = rest[i];
89
- if (seen.has(flag)) { await err(HELP); return 2; }
132
+ if (seen.has(flag)) {
133
+ await err(HELP);
134
+ return 2;
135
+ }
90
136
  seen.add(flag);
91
137
  if (flag === '--project' && rest[i + 1]) parsed.projectRoot = rest[++i];
92
138
  else if (flag === '--profile' && RECOMMEND_PROFILES.includes(rest[i + 1])) parsed.profile = rest[++i];
93
139
  else if (flag === '--client' && ['codex', 'claude'].includes(rest[i + 1])) parsed.client = rest[++i];
94
140
  else if (flag === '--dry-run') parsed.dryRun = true;
95
141
  else if (flag === '--json') parsed.json = true;
96
- else { await err(HELP); return 2; }
142
+ else {
143
+ await err(HELP);
144
+ return 2;
145
+ }
97
146
  }
98
147
  try {
99
148
  const catalogUrl = options.toolkitRoot
@@ -103,13 +152,13 @@ export async function runToolkitCli(
103
152
  const inspection = await inspectSkillProject(parsed.projectRoot, {
104
153
  ...(parsed.client && { client: parsed.client }),
105
154
  ...(options.toolkitRoot && { toolkitRoot: options.toolkitRoot }),
106
- catalog
155
+ catalog,
107
156
  });
108
157
  const report = recommendSkills({
109
158
  catalog,
110
159
  inspection,
111
160
  ...(parsed.profile && { profile: parsed.profile }),
112
- ...(parsed.client && { client: parsed.client })
161
+ ...(parsed.client && { client: parsed.client }),
113
162
  });
114
163
  await out(parsed.json ? JSON.stringify(report) + '\n' : formatRecommendationReport(report));
115
164
  return 0;
@@ -119,38 +168,54 @@ export async function runToolkitCli(
119
168
  }
120
169
  }
121
170
 
122
- if (command === 'detach' || command === 'unlink') {
171
+ if (command === 'reparar') return runRepair(rest, { out, err, input, output, isTTY, options });
172
+
173
+ if (command === 'detach' || command === 'unlink' || command === 'quitar') {
123
174
  const parsed = { projectRoot: process.cwd(), agent: 'all', global: false, apply: false, json: false };
124
175
  const seen = new Set();
125
176
  for (let i = 0; i < rest.length; i++) {
126
177
  const flag = rest[i];
127
- if (seen.has(flag)) { await err(HELP); return 2; }
178
+ if (seen.has(flag)) {
179
+ await err(HELP);
180
+ return 2;
181
+ }
128
182
  seen.add(flag);
129
183
  if (flag === '--project' && rest[i + 1]) parsed.projectRoot = rest[++i];
130
- else if (flag === '--agent' && ['codex', 'claude', 'all'].includes(rest[i + 1])) parsed.agent = rest[++i];
184
+ else if (flag === '--agent' && ['codex', 'claude', 'antigravity', 'all'].includes(rest[i + 1]))
185
+ parsed.agent = rest[++i];
131
186
  else if (flag === '--global') parsed.global = true;
132
- else if (flag === '--apply') parsed.apply = true;
187
+ else if (flag === '--apply' || flag === '--si') parsed.apply = true;
133
188
  else if (flag === '--json') parsed.json = true;
134
- else { await err(HELP); return 2; }
189
+ else {
190
+ await err(HELP);
191
+ return 2;
192
+ }
135
193
  }
136
194
  try {
137
195
  const catalogUrl = options.toolkitRoot
138
196
  ? pathToFileURL(path.join(options.toolkitRoot, 'catalogs/skills.json'))
139
197
  : undefined;
140
198
  const catalog = await loadSkillCatalog(catalogUrl ? { catalogUrl } : {});
199
+ // Skills are installed at the repository root, so they are removed from there too.
200
+ const { root } = await resolveProjectRoot(
201
+ parsed.projectRoot,
202
+ options.homeDirectory ? { homeDirectory: options.homeDirectory } : {},
203
+ );
141
204
  const plan = await planToolkitDetach({
142
- projectRoot: parsed.projectRoot,
205
+ projectRoot: root,
143
206
  includeGlobal: parsed.global,
207
+ environment: options.environment ?? (options.homeDirectory ? {} : process.env),
144
208
  agent: parsed.agent,
145
209
  catalog,
146
210
  ...(options.toolkitRoot && { toolkitRoot: options.toolkitRoot }),
147
211
  ...(options.homeDirectory && { homeDirectory: options.homeDirectory }),
148
- ...(options.codexHome && { codexHome: options.codexHome })
212
+ ...(options.codexHome && { codexHome: options.codexHome }),
149
213
  });
150
214
  const report = parsed.apply ? await applyToolkitDetach(plan) : plan;
151
215
  if (parsed.json) {
152
216
  const cleanJson = {
153
217
  applied: report.applied,
218
+ reviewInstructions: report.reviewInstructions ?? [],
154
219
  projectRoot: report.projectRoot,
155
220
  includeGlobal: report.includeGlobal,
156
221
  items: report.items.map(({ catalogEntry, ...itemRest }) => itemRest),
@@ -158,15 +223,15 @@ export async function runToolkitCli(
158
223
  ? {
159
224
  removedCount: report.removedCount,
160
225
  gitIgnoreCleanedCount: report.gitIgnoreCleanedCount,
161
- stateDirRemoved: report.stateDirRemoved
226
+ stateDirRemoved: report.stateDirRemoved,
162
227
  }
163
228
  : {
164
229
  removeStateDir: report.removeStateDir,
165
230
  removeLegacyLockfile: report.removeLegacyLockfile,
166
- gitIgnoreUpdates: report.gitIgnoreUpdates
231
+ gitIgnoreUpdates: report.gitIgnoreUpdates,
167
232
  }),
168
233
  conflicts: report.conflicts,
169
- manualCommands: report.manualCommands
234
+ manualCommands: report.manualCommands,
170
235
  };
171
236
  await out(JSON.stringify(cleanJson) + '\n');
172
237
  } else {
@@ -179,7 +244,7 @@ export async function runToolkitCli(
179
244
  }
180
245
  }
181
246
 
182
- if (!['doctor', 'setup'].includes(command)) {
247
+ if (!['doctor', 'comprobar', 'setup'].includes(command)) {
183
248
  await err(HELP);
184
249
  return 2;
185
250
  }
@@ -188,29 +253,291 @@ export async function runToolkitCli(
188
253
  const seen = new Set();
189
254
  for (let i = 0; i < rest.length; i++) {
190
255
  const flag = rest[i];
191
- if (seen.has(flag)) { await err(HELP); return 2; }
256
+ if ((flag === '--reponer' || flag === '--omitir') && command === 'setup') {
257
+ const name = rest[i + 1] ?? '';
258
+ if (!SKILL_NAME_RE.test(name)) {
259
+ await err(HELP);
260
+ return 2;
261
+ }
262
+ (parsed[flag === '--reponer' ? 'restore' : 'omit'] ??= []).push(name);
263
+ i++;
264
+ continue;
265
+ }
266
+ if (seen.has(flag)) {
267
+ await err(HELP);
268
+ return 2;
269
+ }
192
270
  seen.add(flag);
193
- if (flag === '--agent' && ['codex', 'claude', 'all'].includes(rest[i + 1])) parsed.agent = rest[++i];
271
+ if (flag === '--agent' && ['codex', 'claude', 'antigravity', 'all'].includes(rest[i + 1]))
272
+ parsed.agent = rest[++i];
194
273
  else if (flag === '--json') parsed.json = true;
195
274
  else if (flag === '--perfil-codex' && PROFILE_RE.test(rest[i + 1] ?? '')) parsed.codexProfile = rest[++i];
196
275
  else if (flag === '--apply' && command === 'setup') parsed.apply = true;
197
- else { await err(HELP); return 2; }
276
+ else {
277
+ await err(HELP);
278
+ return 2;
279
+ }
198
280
  }
199
281
  try {
200
282
  const result = await (command === 'setup' ? setupToolkit : doctorToolkit)({ ...options, ...parsed });
201
- await out(JSON.stringify(result, null, parsed.json ? 0 : 2) + '\n');
202
- return command === 'setup'
203
- ? result.conflicts ? 1 : 0
204
- : result.node.supported &&
283
+ if (command === 'comprobar' && !parsed.json) {
284
+ await out(formatComprobarReport(result, { homeDirectory: options.homeDirectory ?? os.homedir() }));
285
+ } else {
286
+ await out(JSON.stringify(result, null, parsed.json ? 0 : 2) + '\n');
287
+ }
288
+ if (command === 'setup') return result.conflicts ? 1 : 0;
289
+ if (command === 'comprobar') {
290
+ return result.node.supported &&
205
291
  Object.values(result.dependencies).every(Boolean) &&
206
- Object.values(result.clients).every(c => c.available) &&
207
- Object.values(result.mcp).every(m => m.state === 'configured') &&
208
- result.skills.every(s => s.state === 'installed') &&
209
- (!result.codexSandbox || ['allowed', 'not-needed', 'unknown'].includes(result.codexSandbox.state))
292
+ Object.values(result.clients).some((c) => c.available) &&
293
+ result.skills.every((s) => SETTLED_LINK_STATES.includes(s.state)) &&
294
+ !(result.retiredSkills ?? []).some((s) => !s.owned) &&
295
+ superpowersReady(result)
210
296
  ? 0
211
297
  : 1;
298
+ }
299
+ return result.node.supported &&
300
+ Object.values(result.dependencies).every(Boolean) &&
301
+ Object.values(result.clients).every((c) => c.available) &&
302
+ Object.values(result.mcp).every((m) => READY_LINK_STATES.includes(m.state)) &&
303
+ result.skills.every((s) => SETTLED_LINK_STATES.includes(s.state)) &&
304
+ (!result.codexSandbox || ['allowed', 'not-needed', 'unknown'].includes(result.codexSandbox.state)) &&
305
+ superpowersReady(result)
306
+ ? 0
307
+ : 1;
308
+ } catch {
309
+ await err(
310
+ 'No se pudo inspeccionar o instalar. Revisa rutas, permisos y conflictos; no se reemplazan destinos.\n',
311
+ );
312
+ return 1;
313
+ }
314
+ }
315
+
316
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
317
+ // Required: missing or disabled fails the check; a list that could not be read is reported but does not fail.
318
+ const superpowersReady = (result) =>
319
+ !Object.values(result.superpowers?.states ?? {}).some((state) => ['falta', 'desactivado'].includes(state));
320
+
321
+ function formatComprobarReport(result, { homeDirectory }) {
322
+ const short = (value) =>
323
+ value === homeDirectory ||
324
+ value.startsWith(homeDirectory + '/') ||
325
+ value.startsWith(homeDirectory + '\\')
326
+ ? `~${value.slice(homeDirectory.length)}`
327
+ : value;
328
+ const lines = [
329
+ result.node.supported
330
+ ? `✓ Node.js ${result.node.version}`
331
+ : `! Node.js ${result.node.version} (se necesita Node.js 24 o superior)`,
332
+ ];
333
+ const missingDeps = Object.entries(result.dependencies)
334
+ .filter(([, ok]) => !ok)
335
+ .map(([name]) => name);
336
+ lines.push(
337
+ missingDeps.length === 0
338
+ ? '✓ Dependencias instaladas'
339
+ : `! Faltan dependencias: ${missingDeps.join(', ')}`,
340
+ );
341
+ const availableClients = Object.entries(result.clients)
342
+ .filter(([, state]) => state.available)
343
+ .map(([name]) => name);
344
+ lines.push(
345
+ availableClients.length > 0
346
+ ? `✓ IA disponibles: ${availableClients.join(', ')}`
347
+ : '! No encontré ninguna IA instalada (claude, codex o agy)',
348
+ );
349
+ const readySkills = result.skills.filter((s) => READY_LINK_STATES.includes(s.state));
350
+ const removedSkills = result.skills.filter((s) => s.state === REMOVED_BY_USER_STATE);
351
+ const pendingSkills = result.skills.filter((s) => !SETTLED_LINK_STATES.includes(s.state));
352
+ if (pendingSkills.length === 0) {
353
+ lines.push(`✓ Skills base listas (${readySkills.length})`);
354
+ } else {
355
+ lines.push(
356
+ `! ${plural(pendingSkills.length, 'enlace de skill base pendiente o con problema', 'enlaces de skills base pendientes o con problema')} (ejecuta npx nexakit para revisarlos)`,
357
+ );
358
+ }
359
+ if (removedSkills.length > 0) {
360
+ lines.push(
361
+ `✓ ${plural(removedSkills.length, 'skill que quitaste', 'skills que quitaste')} (nexakit no las recrea; para devolver una: npx nexakit setup --reponer <nombre> --apply)`,
362
+ );
363
+ }
364
+ for (const group of result.legacy ?? []) {
365
+ lines.push(
366
+ `! ${plural(group.count, 'enlace roto', 'enlaces rotos')} hacia ${short(group.root)} parecen de una instalación anterior. Si son de nexakit: npx nexakit reparar --desde ${short(group.root)}`,
367
+ );
368
+ }
369
+ for (const item of (result.retiredSkills ?? []).filter((s) => !s.owned)) {
370
+ lines.push(`! ${item.client} · ${item.name}: comando retirado con contenido ajeno; no lo toco`);
371
+ }
372
+ const superpowers = Object.entries(result.superpowers?.states ?? {});
373
+ const active = superpowers.filter(([, state]) => state === 'activo').map(([name]) => name);
374
+ if (active.length) lines.push(`✓ Superpowers activo en ${active.join(', ')}`);
375
+ for (const [name, state] of superpowers.filter(([, state]) => state !== 'activo')) {
376
+ lines.push(
377
+ state === 'falta'
378
+ ? `! Falta Superpowers en ${name} (ejecuta npx nexakit para instalarlo)`
379
+ : state === 'desactivado'
380
+ ? `! Superpowers está desactivado en ${name}; actívalo desde su gestor de plugins`
381
+ : `! No pude comprobar Superpowers en ${name}`,
382
+ );
383
+ }
384
+ for (const item of result.obsolete ?? []) {
385
+ lines.push(`! ${item.message} ${item.command}`);
386
+ }
387
+ return lines.join('\n') + '\n';
388
+ }
389
+
390
+ // Reads one line, and treats anything but "s" (including no answer at all) as no. The answer comes from the
391
+ // `line` event, which always precedes `close`, so an input that ends right after the answer is not lost.
392
+ async function askYesNo(input, output) {
393
+ const rl = readline.createInterface({ input, ...(output?.isTTY && { output }) });
394
+ try {
395
+ const answer = await new Promise((resolve) => {
396
+ let answered = false;
397
+ rl.once('line', (line) => {
398
+ answered = true;
399
+ resolve(line);
400
+ });
401
+ rl.once('close', () => {
402
+ if (!answered) resolve('');
403
+ });
404
+ });
405
+ return /^s(í|i)?$/iu.test(answer.trim());
406
+ } finally {
407
+ rl.close();
408
+ }
409
+ }
410
+
411
+ async function runRepair(rest, { out, err, input, output, isTTY, options }) {
412
+ const parsed = { agent: 'all' };
413
+ const seen = new Set();
414
+ for (let i = 0; i < rest.length; i++) {
415
+ const flag = rest[i];
416
+ if (flag === '--si') {
417
+ await err('reparar siempre pregunta; no admite --si.\n');
418
+ return 2;
419
+ }
420
+ if (seen.has(flag)) {
421
+ await err(HELP);
422
+ return 2;
423
+ }
424
+ seen.add(flag);
425
+ if (flag === '--desde' && rest[i + 1]) parsed.from = rest[++i];
426
+ else if (flag === '--agent' && ['codex', 'claude', 'antigravity', 'all'].includes(rest[i + 1]))
427
+ parsed.agent = rest[++i];
428
+ else {
429
+ await err(HELP);
430
+ return 2;
431
+ }
432
+ }
433
+ if (!parsed.from) {
434
+ await err(HELP);
435
+ return 2;
436
+ }
437
+
438
+ const home = options.homeDirectory ?? os.homedir();
439
+ const short = (value) =>
440
+ value === home || value.startsWith(home + path.sep) ? `~${value.slice(home.length)}` : value;
441
+ try {
442
+ const plan = await planLegacyRepair({ ...options, ...parsed });
443
+ const from = short(plan.from);
444
+ if (plan.links.length === 0) {
445
+ await out(`No encontré enlaces rotos hacia ${from}.\n`);
446
+ return 0;
447
+ }
448
+ await out(
449
+ [
450
+ `Encontré ${plural(plan.links.length, 'enlace roto', 'enlaces rotos')} hacia ${from}.`,
451
+ 'No puedo comprobar que sean de nexakit: revisa la lista.',
452
+ ...plan.links.map((link) => ` ${link.client} · ${link.name}`),
453
+ '',
454
+ ].join('\n'),
455
+ );
456
+ const interactive = Boolean(isTTY ?? (input?.isTTY && output?.isTTY));
457
+ if (!interactive) {
458
+ await err('Este comando necesita una terminal interactiva.\n');
459
+ return 2;
460
+ }
461
+ await out('¿Los re-apunto a la copia de nexakit en uso? [s/N]\n');
462
+ if (!(await askYesNo(input, output))) {
463
+ await out('No se cambió nada.\n');
464
+ return 0;
465
+ }
466
+ const result = await applyLegacyRepair(plan, options);
467
+ const lines = [`Listo: ${plural(result.repaired, 'enlace re-apuntado', 'enlaces re-apuntados')}.`];
468
+ if (result.changed)
469
+ lines.push(
470
+ `${plural(result.changed, 'enlace cambió', 'enlaces cambiaron')} mientras confirmabas y no se tocó; vuelve a ejecutar el comando.`,
471
+ );
472
+ if (result.skipped.length)
473
+ lines.push(`No se repararon ${result.skipped.join(', ')}: la copia en uso no las incluye.`);
474
+ if (result.mcpCommands.length) {
475
+ lines.push(
476
+ 'El MCP de revisión cruzada también apunta ahí. Para moverlo ejecuta:',
477
+ ...result.mcpCommands.map((command) => ` ${command}`),
478
+ );
479
+ }
480
+ if (result.repaired) lines.push('Reinicia tus agentes para que carguen las skills.');
481
+ await out(lines.join('\n') + '\n');
482
+ return result.changed || result.skipped.length ? 1 : 0;
212
483
  } catch {
213
- await err('No se pudo inspeccionar o instalar. Revisa rutas, permisos y conflictos; no se reemplazan destinos.\n');
484
+ await err('No se pudo comprobar ni reparar. Revisa rutas y permisos; no se reemplazan destinos.\n');
485
+ return 1;
486
+ }
487
+ }
488
+
489
+ // Options tests and callers pass (homeDirectory, toolkitRoot, codexHome, environment, runCommand, runNpm, ...).
490
+ function setupStateOptions(projectRoot, options) {
491
+ const { runNpm, packageSpec, acquire, ...rest } = options;
492
+ return {
493
+ stateOptions: { ...rest, start: projectRoot },
494
+ applyOptions: {
495
+ ...(runNpm && { runNpm }),
496
+ ...(packageSpec && { packageSpec }),
497
+ ...(acquire && { acquire }),
498
+ },
499
+ };
500
+ }
501
+
502
+ async function runSetup(parsed, { out, err, input, output, isTTY, options }) {
503
+ const { stateOptions, applyOptions } = setupStateOptions(parsed.projectRoot, options);
504
+ const interactive = Boolean(isTTY ?? (input?.isTTY && output?.isTTY));
505
+ try {
506
+ if (interactive && !parsed.yes)
507
+ return await runSetupTui({ input, output, options: stateOptions, applyOptions });
508
+ const state = await gatherSetupState(stateOptions);
509
+ const answers = suggestedAnswers(state);
510
+ const confirmation = buildConfirmation(state, answers);
511
+ const title = `nexakit · ${state.project.displayName}${state.project.fromSubfolder ? ' (repositorio completo)' : ''}`;
512
+ if (confirmation.allReady) {
513
+ await out(
514
+ `${title}\nTodo está listo.\n${confirmation.lines.map((line) => ` ${line.mark} ${line.text}`).join('\n')}\n`,
515
+ );
516
+ return 0;
517
+ }
518
+ if (!parsed.yes) {
519
+ await out(
520
+ `${title}\nCon las respuestas sugeridas prepararía:\n${confirmation.lines.map((line) => ` ${line.mark} ${line.text}`).join('\n')}\n\n` +
521
+ 'No hay una terminal interactiva para preguntarte.\nPara aplicarlo sin preguntas: npx nexakit --si\n',
522
+ );
523
+ return 2;
524
+ }
525
+ await out(`${title}\n`);
526
+ const labels = Object.fromEntries(confirmation.blocks.map((block) => [block.id, block.label]));
527
+ const result = await applySetup(stateOptions, answers, {
528
+ ...applyOptions,
529
+ onProgress: async (id, phase, message) => {
530
+ // The final check prints its own line with the result, below.
531
+ if (phase === 'done' && id !== 'final-check') await out(` ◇ ${labels[id] ?? id}\n`);
532
+ if (phase === 'failed') await err(` ✗ ${labels[id] ?? id}: ${message}\n`);
533
+ },
534
+ });
535
+ if (result.finalCheck)
536
+ await out(` ${result.finalCheck.ok ? '◇' : '!'} Comprobación final: ${result.finalCheck.message}\n`);
537
+ if (result.ok) await out('Listo. Cierra y abre tu IA para que cargue las skills y la memoria.\n');
538
+ return result.ok ? 0 : 1;
539
+ } catch (error) {
540
+ await err(`No se pudo preparar el proyecto: ${error instanceof Error ? error.message : String(error)}\n`);
214
541
  return 1;
215
542
  }
216
543
  }