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
@@ -1,961 +0,0 @@
1
- import path from 'node:path';
2
- import { pathToFileURL } from 'node:url';
3
- import { loadSkillCatalog } from './skill-catalog.js';
4
- import {
5
- applyProjectSkillInstall,
6
- applyToolkitDetach,
7
- inspectSkillProject,
8
- planProjectSkillInstall,
9
- planToolkitDetach,
10
- PROJECT_TOOLKIT_DIR,
11
- recommendSkills
12
- } from './skill-recommendations.js';
13
- import { levelAgentPlan, writeLevelAgents } from './level-agents.js';
14
- import { getToolkitCommandsGuide } from './toolkit-commands-guide.js';
15
- import { doctorToolkit, setupToolkit, TOOLKIT_ROOT } from './toolkit-setup.js';
16
-
17
- const AGENTS_CYCLE = ['all', 'claude', 'codex'];
18
- const AGENT_LABELS = Object.freeze({
19
- all: 'Claude + Codex (ambos)',
20
- claude: 'Solo Claude Code',
21
- codex: 'Solo Codex'
22
- });
23
- const MODE_LABELS = Object.freeze({
24
- symlink: 'Enlaza las skills al toolkit sin duplicarlas. Recomendado para trabajar en este equipo.',
25
- copy: `Guarda una copia de las skills en el proyecto y sus hashes en ${PROJECT_TOOLKIT_DIR}/skills-lock.json. Útil si prefieres no depender de enlaces al toolkit.`
26
- });
27
- const GIT_IGNORE_LABELS = Object.freeze({
28
- gitignore: 'Oculta los archivos instalados en Git. Usa .gitignore si quieres compartir estas reglas con tu equipo.',
29
- 'git-exclude': 'Oculta los archivos instalados en Git solo en tu equipo, sin modificar el .gitignore compartido.'
30
- });
31
-
32
- function style(colorEnabled) {
33
- if (!colorEnabled) {
34
- const plain = text => String(text);
35
- return { bold: plain, cyan: plain, green: plain, yellow: plain, dim: plain, red: plain, magenta: plain };
36
- }
37
- const wrap = (open, close) => text => `\x1b[${open}m${text}\x1b[${close}m`;
38
- return {
39
- bold: wrap(1, 22),
40
- dim: wrap(2, 22),
41
- red: wrap(31, 39),
42
- green: wrap(32, 39),
43
- yellow: wrap(33, 39),
44
- magenta: wrap(35, 39),
45
- cyan: wrap(36, 39)
46
- };
47
- }
48
-
49
- export async function buildToolkitTuiSnapshot({
50
- projectRoot = process.cwd(),
51
- agent = 'all',
52
- profile = null,
53
- codexProfile,
54
- installMode = 'symlink',
55
- setupOptions = {}
56
- } = {}) {
57
- const toolkitRoot = setupOptions.toolkitRoot ?? TOOLKIT_ROOT;
58
- const catalogUrl = setupOptions.toolkitRoot
59
- ? pathToFileURL(path.join(toolkitRoot, 'catalogs/skills.json'))
60
- : undefined;
61
- const catalog = await loadSkillCatalog(catalogUrl ? { catalogUrl } : {});
62
-
63
- const [doctor, setupPreview, inspection, codexLevels] = await Promise.all([
64
- doctorToolkit({ ...setupOptions, agent, ...(codexProfile && { codexProfile }) }),
65
- setupToolkit({ ...setupOptions, agent, apply: false, ...(codexProfile && { codexProfile }) }),
66
- inspectSkillProject(projectRoot, { toolkitRoot, catalog }),
67
- agent === 'claude'
68
- ? Promise.resolve({ catalog: false, files: [] })
69
- : levelAgentPlan(setupOptions.codexHome ? { codexHome: setupOptions.codexHome } : {}).catch(() => ({ catalog: false, files: [] }))
70
- ]);
71
-
72
- const recommendation = recommendSkills({
73
- catalog,
74
- inspection,
75
- clients: agent === 'all' ? ['claude', 'codex'] : [agent],
76
- ...(profile && { profile })
77
- });
78
-
79
- const projectInstallPlan = await planProjectSkillInstall({
80
- projectRoot: inspection.projectRoot,
81
- skillNames: catalog.skills.map(s => s.name),
82
- agent,
83
- mode: installMode,
84
- catalog,
85
- toolkitRoot
86
- });
87
-
88
- const guide = getToolkitCommandsGuide({ clients: doctor.clients });
89
-
90
- return {
91
- agent,
92
- profile,
93
- codexProfile,
94
- installMode,
95
- catalog,
96
- doctor,
97
- setupPreview,
98
- codexLevels,
99
- inspection,
100
- recommendation,
101
- projectInstallPlan,
102
- guide
103
- };
104
- }
105
-
106
- function buildSkillItem(entry, linksForSkill, { isFundamental }) {
107
- const missingCount = linksForSkill.filter(l => ['missing', 'replace'].includes(l.state)).length;
108
- const conflictCount = linksForSkill.filter(l => l.state === 'conflict').length;
109
- const stateLabel = conflictCount > 0
110
- ? '⚠ conflicto respetado'
111
- : missingCount === 0
112
- ? '✓ ya instalada'
113
- : 'SHA-256 ✓';
114
-
115
- const reasonText = isFundamental
116
- ? entry.reasons.join(', ')
117
- : `perfil ${entry.profiles.join('/')}`;
118
- const validated = entry.recommendationStatus === 'validated';
119
- const checked = Boolean(isFundamental && validated && missingCount > 0);
120
-
121
- return {
122
- id: `skill:${entry.name}`,
123
- skillName: entry.name,
124
- title: entry.name,
125
- summary: entry.summary,
126
- license: entry.license,
127
- origin: `${reasonText} · ${stateLabel} (${entry.license})`,
128
- reasons: entry.reasons ?? [],
129
- pendingReasons: entry.pendingReasons ?? [],
130
- evidence: entry.evidence ?? [],
131
- overlapsWith: entry.overlapsWith ?? [],
132
- recommendationStatus: entry.recommendationStatus,
133
- installationStatus: conflictCount > 0 ? 'conflict' : missingCount === 0 ? 'installed' : 'missing',
134
- reviewStatus: entry.reviewStatus,
135
- reviewNotes: entry.reviewNotes,
136
- checked,
137
- selectionSource: checked ? 'auto' : 'default',
138
- alreadyReady: missingCount === 0 && conflictCount === 0
139
- };
140
- }
141
-
142
- export function createToolkitTuiModel(snapshot) {
143
- const pendingGlobal = snapshot.setupPreview.skills.filter(s => ['missing', 'replace'].includes(s.state)).length;
144
- const conflictGlobal = snapshot.setupPreview.conflicts;
145
- const newLevels = snapshot.codexLevels.files.filter(f => f.state === 'nuevo').length;
146
- const globalNeedsSync = (pendingGlobal > 0 && conflictGlobal === 0) || newLevels > 0;
147
-
148
- const fundamentalSource = [
149
- ...snapshot.recommendation.recommended,
150
- ...snapshot.recommendation.candidates
151
- ];
152
- const fundamentalItems = fundamentalSource.map(entry => {
153
- const links = snapshot.projectInstallPlan.links.filter(l => l.name === entry.name);
154
- return buildSkillItem(entry, links, { isFundamental: true });
155
- });
156
-
157
- const optionalItems = snapshot.recommendation.optional.map(entry => {
158
- const links = snapshot.projectInstallPlan.links.filter(l => l.name === entry.name);
159
- return buildSkillItem(entry, links, { isFundamental: false });
160
- });
161
-
162
- const coupling = snapshot.inspection.coupling ?? { coupled: false, gitIgnoreStrategy: 'gitignore' };
163
- const initialGitIgnore = coupling.gitIgnoreStrategy === 'git-exclude' ? 'git-exclude' : 'gitignore';
164
-
165
- return {
166
- step: 1, // project skills | optional skills | review | done | detach-preview | detached
167
- showCommands: false,
168
- cursors: { 1: 0, 2: 0 },
169
- configCursor: 0,
170
- configDraft: null,
171
- config: {
172
- agent: snapshot.agent,
173
- installMode: snapshot.installMode ?? coupling.mode ?? 'symlink',
174
- gitIgnore: initialGitIgnore,
175
- syncGlobalCore: globalNeedsSync,
176
- globalAlreadyReady: !globalNeedsSync && conflictGlobal === 0,
177
- pendingGlobalCount: pendingGlobal + newLevels
178
- },
179
- snapshot,
180
- fundamentalItems,
181
- optionalItems,
182
- previewData: null,
183
- detachPlan: null,
184
- resultData: null,
185
- statusMessage: null
186
- };
187
- }
188
-
189
- // Terminal cells, rather than UTF-16 length, keep borders aligned for project names.
190
- function cellWidth(text) {
191
- return [...text].reduce((width, char) => {
192
- const cp = char.codePointAt(0);
193
- if (/\p{Mark}/u.test(char) || cp === 0x200d || cp === 0xfe0f) return width;
194
- const wide = /\p{Extended_Pictographic}/u.test(char)
195
- || (cp >= 0x1100 && (cp <= 0x115f || cp === 0x2329 || cp === 0x232a
196
- || (cp >= 0x2e80 && cp <= 0xa4cf) || (cp >= 0xac00 && cp <= 0xd7a3)
197
- || (cp >= 0xf900 && cp <= 0xfaff) || (cp >= 0xfe10 && cp <= 0xfe19)
198
- || (cp >= 0xfe30 && cp <= 0xfe6f) || (cp >= 0xff00 && cp <= 0xff60)
199
- || (cp >= 0xffe0 && cp <= 0xffe6) || cp >= 0x20000));
200
- return width + (wide ? 2 : 1);
201
- }, 0);
202
- }
203
-
204
- function wrapText(text, width) {
205
- const lines = [];
206
- for (const paragraph of String(text).split('\n')) {
207
- let line = '';
208
- for (const token of paragraph.match(/\s*\S+/gu) ?? []) {
209
- const word = token.trimStart();
210
- let gap = token.slice(0, token.length - word.length);
211
- if (line && cellWidth(line + gap + word) > width) {
212
- lines.push(line.trimEnd());
213
- line = '';
214
- gap = '';
215
- }
216
- for (const char of gap + word) {
217
- if (cellWidth(line + char) > width) {
218
- lines.push(line);
219
- line = '';
220
- }
221
- line += char;
222
- }
223
- }
224
- lines.push(line);
225
- }
226
- return lines;
227
- }
228
-
229
- export function renderToolkitTuiFrame(model, { columns = 96, color = true } = {}) {
230
- const c = style(color);
231
- const plain = text => String(text);
232
- const margin = columns < 50 ? '' : ' ';
233
- const width = Math.max(12, Math.min(columns - margin.length * 2, 96));
234
- const innerWidth = width - 4;
235
- const lines = [];
236
- const body = [];
237
- const add = (text = '', paint = plain) => {
238
- for (const line of wrapText(text, innerWidth)) body.push({ text: line, paint });
239
- };
240
- const outside = (text = '', paint = plain) => {
241
- for (const line of wrapText(text, width)) lines.push(margin + paint(line));
242
- };
243
- const accent = text => c.bold(c.magenta(text));
244
- const selected = color ? text => c.magenta(`\x1b[7m${text}\x1b[27m`) : plain;
245
- const finish = (hints = []) => {
246
- lines.push(`${margin}${c.dim(`╭${'─'.repeat(width - 2)}╮`)}`);
247
- for (const row of body) {
248
- const padding = ' '.repeat(Math.max(0, innerWidth - cellWidth(row.text)));
249
- lines.push(`${margin}${c.dim('│')} ${row.paint(row.text + padding)} ${c.dim('│')}`);
250
- }
251
- lines.push(`${margin}${c.dim(`╰${'─'.repeat(width - 2)}╯`)}`);
252
- if (model.statusMessage) {
253
- outside();
254
- outside(`! ${model.statusMessage}`, c.yellow);
255
- }
256
- outside();
257
- for (const hint of hints) outside(hint, text => text.replace(/\[[^\]]+\]/gu, c.bold));
258
- return lines.join('\n') + '\n';
259
- };
260
- const doc = model.snapshot.doctor;
261
- const config = model.configDraft ?? model.config;
262
- const shortAgent = AGENT_LABELS[config.agent];
263
- const shortMode = config.installMode === 'copy' ? 'Copia verificada' : 'Enlace simbólico';
264
- const shortIgnore = config.gitIgnore === 'git-exclude' ? '.git/info/exclude' : '.gitignore';
265
- const detachHint = model.snapshot.inspection.coupling?.coupled ? ' · [d] desacoplar' : '';
266
- const workflowLabel = cmd => cmd.id === 'revisar' ? `${cmd.codex} / ${cmd.claude}` : cmd.claude;
267
-
268
- outside('◆ nexakit', accent);
269
- outside(`Proyecto: ${model.snapshot.inspection.projectName}`);
270
- outside();
271
-
272
- if (model.showCommands) {
273
- add('Comandos a mano', c.bold);
274
- add('En Claude usa /nombre; en Codex, $nombre.', c.dim);
275
- add();
276
- for (const cmd of model.snapshot.guide.workflowCommands) {
277
- add(workflowLabel(cmd), accent);
278
- add(cmd.description);
279
- add();
280
- }
281
- add('En tu terminal', c.bold);
282
- for (const cli of model.snapshot.guide.cliCommands) {
283
- add(cli.command, c.cyan);
284
- add(cli.description);
285
- add();
286
- }
287
- return finish(['[Esc / c / Tab] volver al paso actual · [q] salir']);
288
- }
289
-
290
- const step = typeof model.step === 'number' ? model.step : model.step === 'done' ? 4 : 0;
291
- if (step > 0) {
292
- const labels = columns < 60 ? ['Skills', 'Extras', 'Revisar'] : ['Tu proyecto', 'Opcionales', 'Revisar e instalar'];
293
- outside(labels.map((label, i) => {
294
- const n = i + 1;
295
- return `${n < step ? '✓' : n === step ? '●' : '○'} ${label}`;
296
- }).join(' / '), text => text.split('/').map(part => part.includes('●')
297
- ? accent(part)
298
- : part.includes('✓') ? c.green(part) : c.dim(part)).join(c.dim('/')));
299
- outside();
300
- }
301
-
302
- outside(`${doc.node.supported ? '✓' : '!'} Node ${doc.node.version} ${Object.entries(doc.clients).map(([name, info]) => `${info.available ? '✓' : '○'} ${name}`).join(' ')} ${Object.values(doc.mcp).every(m => m.state === 'configured') ? '✓ MCP' : '○ MCP pendiente'}`);
303
- const signals = model.snapshot.inspection.signals;
304
- outside(`Stack: ${signals.length ? signals.map(s => s.label).join(', ') : 'sin señales Node en la raíz'}`, c.dim);
305
- for (const signal of model.snapshot.recommendation.unmatchedSignals) outside(`${signal.label}: sin skill empaquetada aún.`, c.dim);
306
- for (const limit of model.snapshot.inspection.limits.slice(0, 2)) outside(`! ${limit}`, c.yellow);
307
- if (model.snapshot.inspection.coupling?.coupled && model.step !== 'detached') {
308
- outside(`✓ acoplado en ${PROJECT_TOOLKIT_DIR}/ · [d] desacoplar`, c.green);
309
- }
310
- outside();
311
-
312
- if (model.step === 'detached' && model.resultData) {
313
- const r = model.resultData;
314
- add('✓ Proyecto desacoplado limpiamente', c.green);
315
- add();
316
- add(`Skills retiradas del proyecto: ${r.removedCount}`);
317
- add(`Carpeta ${PROJECT_TOOLKIT_DIR}/ retirada: ${r.stateDirRemoved ? 'sí' : 'sin cambios'}`);
318
- add(`Reglas Git ignore limpiadas: ${r.gitIgnoreCleanedCount}`);
319
- if (r.conflicts > 0) add(`Archivos propios respetados: ${r.conflicts}`, c.yellow);
320
- return finish(['[Enter / q] cerrar asistente · [c] comandos']);
321
- }
322
-
323
- if (model.step === 'detach-preview' && model.detachPlan) {
324
- const dp = model.detachPlan;
325
- const actionable = dp.items.filter(item => item.action !== 'conflict');
326
- add('Vista previa de desacoplado', c.bold);
327
- add('Aún no se ha borrado nada.', c.yellow);
328
- add();
329
- for (const item of actionable.slice(0, 10)) add(`− [${item.client}] ${item.relativePath} (${item.reason})`);
330
- if (actionable.length > 10) add(`… y ${actionable.length - 10} skill(s) más.`, c.dim);
331
- if (dp.removeStateDir) add(`− [estado] ${PROJECT_TOOLKIT_DIR}/ (manifiesto y lockfile locales)`);
332
- for (const update of dp.gitIgnoreUpdates ?? []) add(`− [git] Retirar bloque gestionado de ${update.relativePath}`);
333
- for (const conflict of dp.items.filter(item => item.action === 'conflict')) add(`! ${conflict.relativePath} — ${conflict.reason}`, c.yellow);
334
- return finish(['[Enter / s] confirmar desacoplado', '[Esc / Bksp / n] cancelar sin borrar · [c] comandos · [q] salir']);
335
- }
336
-
337
- if (model.step === 'done' && model.resultData) {
338
- const r = model.resultData;
339
- add('✓ Instalación verificada completada', c.green);
340
- add();
341
- add(`Modo aplicado en proyecto: ${shortMode}`);
342
- add(`Skills instaladas en proyecto: ${r.projectLinksCreated} destino(s)`);
343
- add(`Estado aislado e ignorado en: ${PROJECT_TOOLKIT_DIR}/ (${shortIgnore})`);
344
- if (model.config.installMode === 'copy') add(`skills-lock.json actualizado: ${r.lockfileWritten ? 'sí' : 'sin cambios'}`);
345
- if (r.globalApplied || r.levelsCreated > 0) add(`Núcleo global actualizado: ${r.globalApplied ? 'skills enlazadas' : ''} ${r.levelsCreated ? `(${r.levelsCreated} niveles Codex)` : ''}`);
346
- if (r.conflicts > 0) add(`Conflictos respetados intactos: ${r.conflicts}`, c.yellow);
347
- add();
348
- add('Empieza en tu sesión', c.bold);
349
- for (const cmd of model.snapshot.guide.workflowCommands.slice(0, 5)) {
350
- add(workflowLabel(cmd), accent);
351
- add(cmd.description);
352
- }
353
- return finish(['[Enter / q] cerrar · [c] comandos', 'Para desacoplar después: npx nexakit detach']);
354
- }
355
-
356
- if (model.step === 3) {
357
- const cur = model.configCursor;
358
- const agentHint = config.agent === 'all'
359
- ? 'Instala las skills para Claude y Codex. Mantén ambos si usas los dos; cambia este ajuste si solo usas uno.'
360
- : `Instala las skills solo para ${config.agent === 'claude' ? 'Claude Code' : 'Codex'}. Elige ambos si también usas el otro agente.`;
361
- const globalHint = config.globalAlreadyReady
362
- ? 'Las skills base y los agentes de nivel de tu usuario ya están al día. Puedes dejar este ajuste desmarcado.'
363
- : `Prepara las skills base y los agentes de nivel para todos tus proyectos (${config.pendingGlobalCount} pendientes). ${config.syncGlobalCore ? 'Déjalo marcado si quieres sincronizarlos.' : 'Márcalo si quieres sincronizarlos.'}`;
364
- const rows = [
365
- { label: 'Agentes destino', value: AGENT_LABELS[config.agent], hint: agentHint },
366
- { label: 'Modo en proyecto', value: shortMode, hint: MODE_LABELS[config.installMode] },
367
- { label: 'Ignorar en Git', value: shortIgnore, hint: GIT_IGNORE_LABELS[config.gitIgnore] },
368
- { label: 'Núcleo global', value: `${config.syncGlobalCore ? '[x]' : '[ ]'} Sincronizar skills base y agentes de nivel`, hint: globalHint }
369
- ];
370
- add('Paso 3 de 3 · Revisar e instalar', c.bold);
371
- add('Aún no se ha escrito nada en disco.', c.yellow);
372
- add();
373
- const chosen = [...model.fundamentalItems, ...model.optionalItems].filter(item => item.checked);
374
- add(`Skills seleccionadas (${chosen.length})`, c.bold);
375
- add(chosen.length ? chosen.map(item => item.title).join(', ') : 'No hay skills de proyecto seleccionadas.');
376
- add();
377
- add(model.configDraft ? 'Editar configuración de instalación' : 'Configuración de instalación', c.bold);
378
- add();
379
- rows.forEach((row, i) => {
380
- const focused = model.configDraft && i === cur;
381
- add(`${model.configDraft ? (focused ? '❯ ' : ' ') : ''}${row.label}: ${row.value}`, focused ? selected : plain);
382
- });
383
- if (model.configDraft) {
384
- add('Casillas: [x] activado · [ ] desactivado.', c.dim);
385
- add();
386
- add(rows[cur].hint, c.cyan);
387
- return finish(['[↑/↓] seleccionar ajuste · [Espacio] cambiar / marcar', '[Enter] actualizar vista previa · [Esc / Bksp] descartar ajustes', '[c] comandos · [q] salir']);
388
- }
389
- add();
390
- add('Cambios previstos', c.bold);
391
- const p = model.previewData;
392
- if (!p) {
393
- add('No hay una vista previa válida. Vuelve a las skills y revisa la selección.', c.yellow);
394
- return finish(['[Bksp/←] volver a opcionales · [c] comandos · [q] salir']);
395
- }
396
- if (p.verifiedSkillsCount > 0) add(`✓ Integridad SHA-256 comprobada en ${p.verifiedSkillsCount} skill(s) del catálogo`, c.green);
397
- if (p.actions.length === 0) add('No hay cambios pendientes de escribir (todo lo seleccionado ya está al día).');
398
- for (const action of p.actions.slice(0, 12)) add(`${action.kind === 'conflict' ? '!' : '+'} ${action.label}`, action.kind === 'conflict' ? c.yellow : plain);
399
- if (p.actions.length > 12) add(`… y ${p.actions.length - 12} acción(es) más en la selección.`, c.dim);
400
- if (p.conflictsCount > 0) {
401
- add();
402
- add(`! ${p.conflictsCount} conflicto(s) se respetarán sin borrar ni sobrescribir.`, c.yellow);
403
- }
404
- if (p.manualCommands.length > 0) {
405
- add();
406
- add('Comandos externos sugeridos (opcional)', c.bold);
407
- for (const cmd of p.manualCommands.slice(0, 2)) add(`$ ${cmd}`, c.cyan);
408
- }
409
- return finish(['[Enter / s] confirmar e instalar · [e] editar configuración', `[Bksp/←] volver a opcionales · [Esc / n] cancelar${detachHint}`, '[c] comandos · [q] salir']);
410
- }
411
-
412
- if (model.step === 1 || model.step === 2) {
413
- const fundamental = model.step === 1;
414
- const items = fundamental ? model.fundamentalItems : model.optionalItems;
415
- const cur = model.cursors[model.step];
416
- const checkedCount = items.filter(item => item.checked).length;
417
- add(`Paso ${model.step} de 3 · ${fundamental ? 'Skills para tu proyecto' : 'Skills opcionales'}`, c.bold);
418
- add(`${checkedCount} de ${items.length} marcadas · archivos comprobados por SHA-256`, c.green);
419
- add(fundamental ? 'Solo las recomendaciones validadas vienen marcadas. Las compatibles las eliges tú.' : 'Fuera del stack detectado. Tú eliges qué añadir.', c.dim);
420
- add();
421
- if (items.length === 0) {
422
- add(fundamental ? 'No se detectaron frameworks adicionales que requieran skills específicas en este paso.' : 'Todas las skills del catálogo ya encajaron con tu stack en el Paso 1.');
423
- }
424
- items.forEach((item, i) => add(`${i === cur ? '❯' : ' '} ${item.checked ? '[x]' : '[ ]'} ${item.title}${item.alreadyReady ? ' · ya instalada' : ''} · ${item.recommendationStatus === 'validated' ? 'Recomendada' : fundamental ? 'Compatible' : 'Opcional'}`, i === cur ? selected : item.checked ? c.green : plain));
425
- if (items[cur]) {
426
- add();
427
- const item = items[cur];
428
- add(`Por qué: ${item.reasons.length ? item.reasons.join(', ') : 'sin coincidencia con el stack; disponible para elección manual.'}`, c.cyan);
429
- add(`Utilidad prevista: ${item.summary}`);
430
- if (item.recommendationStatus === 'validated') {
431
- add('Recomendada: escenario funcional pasado para los agentes destino.', c.green);
432
- for (const evidence of item.evidence) add(`Evidencia (${evidence.client}): ${evidence.evidencePath}`, c.dim);
433
- } else if (fundamental) {
434
- add('Compatible: utilidad sin validar para este contexto.', c.yellow);
435
- for (const reason of item.pendingReasons) add(`Pendiente: ${reason}`, c.dim);
436
- }
437
- if (item.overlapsWith.length > 0) add(`Comparte ámbito con: ${item.overlapsWith.join(', ')}. Revisa si necesitas ambas.`, c.dim);
438
- if (item.installationStatus === 'conflict') add('Instalación: hay archivos propios o modificados; se conservarán.', c.yellow);
439
- if (item.reviewStatus === 'reviewed-with-observations') add(`Observaciones: ${item.reviewNotes}`, c.yellow);
440
- add(`Integridad: SHA-256 comprobado · Licencia: ${item.license}`, c.dim);
441
- add('La integridad de los archivos no acredita su utilidad.', c.dim);
442
- }
443
- if (!fundamental && model.snapshot.recommendation.addons.length > 0) {
444
- add();
445
- add('Añadido externo opcional · sin descarga automática', c.bold);
446
- for (const addon of model.snapshot.recommendation.addons) {
447
- add(`${addon.name} (${addon.versionReviewed} · ${addon.license})`, c.magenta);
448
- add('4 prácticas base ya adaptadas.', c.dim);
449
- add(`Claude: ${addon.installHints.claude}`, c.dim);
450
- }
451
- }
452
- return finish([`[↑/↓] mover · [Espacio] marcar/desmarcar · [a] todas · [Enter] ${fundamental ? 'opcionales' : 'revisar instalación'}`, `${fundamental ? '' : '[Bksp/←] atrás · '}[c] comandos${detachHint} · [q] salir`]);
453
- }
454
-
455
- return finish();
456
- }
457
-
458
- export async function computeInstallPreview(model, setupOptions = {}) {
459
- const actions = [];
460
- let conflictsCount = 0;
461
- const activeAgent = model.config.agent;
462
- const activeMode = model.config.installMode;
463
- const activeGitIgnore = model.config.gitIgnore ?? 'gitignore';
464
-
465
- let freshSetupPreview = null;
466
- let freshLevelsPlan = null;
467
- if (model.config.syncGlobalCore) {
468
- freshSetupPreview = await setupToolkit({
469
- ...setupOptions,
470
- agent: activeAgent,
471
- apply: false,
472
- ...(model.snapshot.codexProfile && { codexProfile: model.snapshot.codexProfile })
473
- });
474
- for (const s of freshSetupPreview.skills) {
475
- if (['missing', 'replace'].includes(s.state)) {
476
- actions.push({ kind: 'create', label: `[global ${s.client}] ${s.name} → ${s.target}` });
477
- } else if (s.state === 'conflict') {
478
- conflictsCount++;
479
- actions.push({ kind: 'conflict', label: `[conflicto global ${s.client}] ${s.target} (no se toca)` });
480
- }
481
- }
482
-
483
- if (activeAgent !== 'claude') {
484
- freshLevelsPlan = await levelAgentPlan(setupOptions.codexHome ? { codexHome: setupOptions.codexHome } : {});
485
- for (const f of freshLevelsPlan.files) {
486
- if (f.state === 'nuevo') {
487
- actions.push({ kind: 'create', label: `[nivel codex] ${f.name} → ${f.path}` });
488
- } else if (f.state === 'distinto') {
489
- conflictsCount++;
490
- actions.push({ kind: 'conflict', label: `[distinto respetado] ${f.path} (no se sobrescribe)` });
491
- }
492
- }
493
- }
494
- }
495
-
496
- const selectedProjectSkills = [
497
- ...model.fundamentalItems.filter(i => i.checked).map(i => i.skillName),
498
- ...model.optionalItems.filter(i => i.checked).map(i => i.skillName)
499
- ];
500
-
501
- let freshProjectPlan = null;
502
- if (selectedProjectSkills.length > 0) {
503
- freshProjectPlan = await planProjectSkillInstall({
504
- projectRoot: model.snapshot.inspection.projectRoot,
505
- skillNames: selectedProjectSkills,
506
- agent: activeAgent,
507
- mode: activeMode,
508
- gitIgnore: activeGitIgnore,
509
- catalog: model.snapshot.catalog,
510
- toolkitRoot: setupOptions.toolkitRoot ?? TOOLKIT_ROOT
511
- });
512
- const verb = activeMode === 'copy' ? 'copia' : 'enlace';
513
- for (const l of freshProjectPlan.links) {
514
- const relTarget = path.relative(model.snapshot.inspection.projectRoot, l.target);
515
- if (['missing', 'replace'].includes(l.state)) {
516
- actions.push({ kind: 'create', label: `[${verb} ${l.client}] ${l.name} → ${relTarget}` });
517
- } else if (l.state === 'conflict') {
518
- conflictsCount++;
519
- actions.push({ kind: 'conflict', label: `[conflicto ${l.client}] ${relTarget} (no se toca)` });
520
- }
521
- }
522
- if (freshProjectPlan.stateDirConflict) {
523
- conflictsCount++;
524
- actions.push({ kind: 'conflict', label: `[conflicto estado] ${PROJECT_TOOLKIT_DIR}/ no es carpeta regular (no se toca)` });
525
- } else if (freshProjectPlan.willWriteManifest) {
526
- actions.push({ kind: 'create', label: `[estado] Guardar registro de acoplamiento en ${PROJECT_TOOLKIT_DIR}/manifest.json` });
527
- }
528
- if (freshProjectPlan.willWriteLockfile) {
529
- actions.push({ kind: 'create', label: `[lockfile] Guardar hashes SHA-256 en ${PROJECT_TOOLKIT_DIR}/skills-lock.json` });
530
- } else if (freshProjectPlan.lockfileConflict) {
531
- conflictsCount++;
532
- actions.push({ kind: 'conflict', label: `[conflicto lockfile] ${PROJECT_TOOLKIT_DIR}/skills-lock.json ocupado (no se toca)` });
533
- }
534
- const ignoreTargetLabel = activeGitIgnore === 'git-exclude' ? '.git/info/exclude' : '.gitignore';
535
- if (freshProjectPlan.willUpdateGitIgnore) {
536
- actions.push({ kind: 'create', label: `[git] Ignorar ${PROJECT_TOOLKIT_DIR}/ y skills instaladas en ${ignoreTargetLabel}` });
537
- } else if (freshProjectPlan.gitIgnoreConflict) {
538
- conflictsCount++;
539
- actions.push({ kind: 'conflict', label: `[conflicto git] ${ignoreTargetLabel} no es archivo regular (no se toca)` });
540
- } else if (freshProjectPlan.gitIgnoreUnavailable && activeGitIgnore === 'git-exclude') {
541
- actions.push({ kind: 'conflict', label: '[aviso git] No se detectó .git para .git/info/exclude; cambia a .gitignore si deseas ignorarlo' });
542
- }
543
- }
544
-
545
- const manualCommands = [];
546
- for (const [client, state] of Object.entries(model.snapshot.doctor.mcp)) {
547
- if ((activeAgent === 'all' || activeAgent === client) && state.state !== 'configured' && model.snapshot.doctor.registrationCommands[client]) {
548
- manualCommands.push(model.snapshot.doctor.registrationCommands[client]);
549
- }
550
- }
551
- if (activeAgent !== 'claude' && model.snapshot.doctor.codexSandbox?.command) {
552
- manualCommands.push(model.snapshot.doctor.codexSandbox.command);
553
- }
554
-
555
- return {
556
- actions,
557
- conflictsCount,
558
- verifiedSkillsCount: selectedProjectSkills.length,
559
- manualCommands,
560
- freshSetupPreview,
561
- freshLevelsPlan,
562
- freshProjectPlan
563
- };
564
- }
565
-
566
- export async function executeApprovedPreview(model, setupOptions = {}) {
567
- const preview = model.previewData;
568
- let globalApplied = false;
569
- let levelsCreated = 0;
570
- let projectLinksCreated = 0;
571
- let lockfileWritten = false;
572
- let conflicts = 0;
573
-
574
- if (preview.freshSetupPreview) {
575
- const res = await setupToolkit({
576
- ...setupOptions,
577
- agent: model.config.agent,
578
- apply: true,
579
- ...(model.snapshot.codexProfile && { codexProfile: model.snapshot.codexProfile })
580
- });
581
- globalApplied = res.applied;
582
- conflicts += res.conflicts;
583
- }
584
-
585
- if (preview.freshLevelsPlan) {
586
- const written = await writeLevelAgents(preview.freshLevelsPlan, { force: false });
587
- levelsCreated = written.files.filter(f => f.state === 'escrito').length;
588
- conflicts += written.files.filter(f => f.state === 'distinto').length;
589
- }
590
-
591
- if (preview.freshProjectPlan) {
592
- const appliedInstall = await applyProjectSkillInstall(preview.freshProjectPlan);
593
- projectLinksCreated = appliedInstall.created;
594
- lockfileWritten = appliedInstall.lockfileWritten;
595
- conflicts += appliedInstall.conflicts;
596
- }
597
-
598
- return {
599
- globalApplied,
600
- levelsCreated,
601
- projectLinksCreated,
602
- lockfileWritten,
603
- conflicts,
604
- restartRequired: Boolean(globalApplied || levelsCreated > 0 || projectLinksCreated > 0)
605
- };
606
- }
607
-
608
- export function formatRecommendationReport(report) {
609
- const lines = [
610
- `Agent Toolkit · Recomendaciones para ${report.projectName}`,
611
- `Raíz: ${report.projectRoot}${report.profile ? ` · Perfil: ${report.profile}` : ''}${report.client ? ` · Cliente: ${report.client}` : ''}`,
612
- '',
613
- 'Tecnologías detectadas:'
614
- ];
615
-
616
- if (report.signals.length === 0) {
617
- lines.push(' - Ninguna tecnología cubierta detectada automáticamente.');
618
- } else {
619
- for (const sig of report.signals) {
620
- lines.push(` ✓ ${sig.label} ← ${sig.sources.join(', ')}`);
621
- }
622
- }
623
- for (const unm of report.unmatchedSignals) {
624
- lines.push(` • ${unm.label} (sin skill empaquetada aún)`);
625
- }
626
-
627
- lines.push('', `Recomendaciones con escenario probado (${report.recommended.length}):`);
628
- if (report.recommended.length === 0) {
629
- lines.push(' - Ninguna cumple todavía los 4 criterios de escenario probado para este filtro.');
630
- } else {
631
- for (const rec of report.recommended) {
632
- lines.push(` - ${rec.name} (${rec.license}) ← ${rec.reasons.join(', ')} [tarea: ${rec.taskClass}]`);
633
- }
634
- }
635
-
636
- lines.push('', `Skills compatibles por tecnología o perfil, sin escenario validado (${report.candidates.length}):`);
637
- if (report.candidates.length === 0) {
638
- lines.push(' - Ninguna skill específica adicional coincide con las señales actuales.');
639
- } else {
640
- for (const cand of report.candidates) {
641
- const state = cand.installed ? ` · en proyecto: ${cand.installed.integrity}` : '';
642
- lines.push(` - ${cand.name} (${cand.license}, SHA-256 ✓) ← ${cand.reasons.join(', ')}${state}`);
643
- lines.push(` Utilidad prevista: ${cand.summary}`);
644
- for (const reason of cand.pendingReasons) lines.push(` Pendiente: ${reason}`);
645
- if (cand.overlapsWith.length) lines.push(` Comparte ámbito con: ${cand.overlapsWith.join(', ')}`);
646
- }
647
- }
648
-
649
- if (report.optional && report.optional.length > 0) {
650
- lines.push('', `Opcionales disponibles en el catálogo (${report.optional.length}):`);
651
- for (const opt of report.optional) {
652
- lines.push(` - ${opt.name} (${opt.license}) ← perfil ${opt.profiles.join('/')}`);
653
- }
654
- }
655
-
656
- if (report.addons.length > 0) {
657
- lines.push('', 'Añadidos opcionales:');
658
- for (const addon of report.addons) {
659
- lines.push(` - ${addon.name} (${addon.versionReviewed}, ${addon.license}): ${addon.summary}`);
660
- }
661
- }
662
-
663
- if (report.limits.length > 0) {
664
- lines.push('', 'Límites de la inspección:');
665
- for (const lim of report.limits) lines.push(` ! ${lim}`);
666
- }
667
- lines.push('', 'SHA-256 comprueba integridad de archivos; no acredita utilidad ni calidad.');
668
-
669
- return lines.join('\n') + '\n';
670
- }
671
-
672
- export async function runToolkitTui({
673
- input = process.stdin,
674
- output = process.stdout,
675
- projectRoot = process.cwd(),
676
- agent = 'all',
677
- codexProfile,
678
- setupOptions = {}
679
- } = {}) {
680
- const snapshot = await buildToolkitTuiSnapshot({
681
- projectRoot,
682
- agent,
683
- codexProfile,
684
- setupOptions
685
- });
686
- const model = createToolkitTuiModel(snapshot);
687
- const useColor = Boolean(output.isTTY && !process.env.NO_COLOR);
688
-
689
- const render = () => {
690
- const frame = renderToolkitTuiFrame(model, {
691
- columns: output.columns || 96,
692
- color: useColor
693
- });
694
- output.write(`\x1b[2J\x1b[H${frame}`);
695
- };
696
-
697
- const updateReview = async (config = model.config) => {
698
- let snapshot = model.snapshot;
699
- let fundamentalItems = model.fundamentalItems;
700
- let optionalItems = model.optionalItems;
701
- if (config.agent !== snapshot.agent || config.installMode !== snapshot.installMode) {
702
- snapshot = await buildToolkitTuiSnapshot({
703
- projectRoot: snapshot.inspection.projectRoot,
704
- agent: config.agent,
705
- installMode: config.installMode,
706
- profile: snapshot.profile,
707
- codexProfile: snapshot.codexProfile,
708
- setupOptions
709
- });
710
- const refreshed = createToolkitTuiModel(snapshot);
711
- const choices = new Map([...fundamentalItems, ...optionalItems].map(item => [item.skillName, item]));
712
- const preserveChoice = item => {
713
- const previous = choices.get(item.skillName);
714
- return previous?.selectionSource === 'user'
715
- ? { ...item, checked: previous.checked, selectionSource: 'user' }
716
- : item;
717
- };
718
- fundamentalItems = refreshed.fundamentalItems.map(preserveChoice);
719
- optionalItems = refreshed.optionalItems.map(preserveChoice);
720
- config = { ...config, globalAlreadyReady: refreshed.config.globalAlreadyReady, pendingGlobalCount: refreshed.config.pendingGlobalCount };
721
- }
722
- const previewData = await computeInstallPreview({ ...model, snapshot, config, fundamentalItems, optionalItems }, setupOptions);
723
- Object.assign(model, { snapshot, config, fundamentalItems, optionalItems, previewData, configDraft: null, step: 3 });
724
- };
725
-
726
- let cleanedUp = false;
727
- let processingChain = Promise.resolve();
728
-
729
- return new Promise(resolve => {
730
- const cleanup = code => {
731
- if (cleanedUp) return;
732
- cleanedUp = true;
733
- input.off?.('data', onData);
734
- output.off?.('resize', render);
735
- try {
736
- if (typeof input.setRawMode === 'function') input.setRawMode(false);
737
- } catch {}
738
- try {
739
- input.pause?.();
740
- } catch {}
741
- output.write('\x1b[?25h\n');
742
- resolve(code);
743
- };
744
-
745
- const handleKey = async key => {
746
- if (cleanedUp) return;
747
-
748
- if (key === '\x03') {
749
- cleanup(0);
750
- return;
751
- }
752
-
753
- if (model.showCommands) {
754
- if (key === 'q' || key === 'Q') {
755
- cleanup(0);
756
- return;
757
- }
758
- if (key === '\x1b' || key === 'c' || key === 'C' || key === '\t' || key === '?') {
759
- model.showCommands = false;
760
- render();
761
- }
762
- return;
763
- }
764
-
765
- if (key === 'c' || key === 'C' || key === '\t' || key === '?') {
766
- model.showCommands = true;
767
- render();
768
- return;
769
- }
770
-
771
- if (key === 'q' || key === 'Q') {
772
- cleanup(0);
773
- return;
774
- }
775
-
776
- if (model.step === 'done' || model.step === 'detached') {
777
- if (key === '\r' || key === '\n' || key === '\x1b') {
778
- cleanup(0);
779
- }
780
- return;
781
- }
782
-
783
- model.statusMessage = null;
784
-
785
- // Detach safe preview confirmation / cancel
786
- if (model.step === 'detach-preview') {
787
- if (key === '\x1b' || key === 'n' || key === 'N' || key === '\x7f' || key === '\x08' || key === '\x1b[D') {
788
- model.step = 1;
789
- model.detachPlan = null;
790
- model.statusMessage = 'Desacoplado cancelado; no se eliminó ningún archivo.';
791
- render();
792
- return;
793
- }
794
- if (key === '\r' || key === '\n' || key === 's' || key === 'S') {
795
- try {
796
- const detachedResult = await applyToolkitDetach(model.detachPlan);
797
- model.resultData = detachedResult;
798
- model.step = 'detached';
799
- } catch (err) {
800
- model.step = 1;
801
- model.statusMessage = err?.message || 'No se pudo completar el desacoplado.';
802
- }
803
- render();
804
- }
805
- return;
806
- }
807
-
808
- // [d] Detach project shortcut from Steps 1, 2 or 3
809
- if ((key === 'd' || key === 'D') && !model.configDraft && (model.step === 1 || model.step === 2 || model.step === 3)) {
810
- try {
811
- const dp = await planToolkitDetach({
812
- projectRoot: model.snapshot.inspection.projectRoot,
813
- includeGlobal: false,
814
- agent: model.config.agent,
815
- catalog: model.snapshot.catalog,
816
- toolkitRoot: setupOptions.toolkitRoot ?? TOOLKIT_ROOT
817
- });
818
- const hasWork =
819
- dp.items.length > 0 ||
820
- dp.removeStateDir ||
821
- dp.removeLegacyLockfile ||
822
- (dp.gitIgnoreUpdates?.length ?? 0) > 0;
823
- if (!hasWork) {
824
- model.statusMessage = 'Este proyecto no tiene artefactos acoplados por Agent Toolkit para retirar.';
825
- } else {
826
- model.detachPlan = dp;
827
- model.step = 'detach-preview';
828
- }
829
- } catch (err) {
830
- model.statusMessage = err?.message || 'No se pudo calcular la vista previa de desacoplado.';
831
- }
832
- render();
833
- return;
834
- }
835
-
836
- // Edit a draft at the final review. Saving only regenerates the preview.
837
- if (model.step === 3 && model.configDraft) {
838
- if (key === '\x1b' || key === '\x7f' || key === '\x08') {
839
- model.configDraft = null;
840
- } else if (key === '\x1b[A' || key === 'k') {
841
- model.configCursor = (model.configCursor + 3) % 4;
842
- } else if (key === '\x1b[B' || key === 'j') {
843
- model.configCursor = (model.configCursor + 1) % 4;
844
- } else if (key === ' ' || key === '\x1b[C' || key === '\x1b[D') {
845
- const draft = model.configDraft;
846
- if (model.configCursor === 0) draft.agent = AGENTS_CYCLE[(AGENTS_CYCLE.indexOf(draft.agent) + 1) % AGENTS_CYCLE.length];
847
- else if (model.configCursor === 1) draft.installMode = draft.installMode === 'symlink' ? 'copy' : 'symlink';
848
- else if (model.configCursor === 2) draft.gitIgnore = draft.gitIgnore === 'gitignore' ? 'git-exclude' : 'gitignore';
849
- else draft.syncGlobalCore = !draft.syncGlobalCore;
850
- } else if (key === '\r' || key === '\n') {
851
- try {
852
- await updateReview(model.configDraft);
853
- } catch (err) {
854
- model.statusMessage = err?.message || 'No se pudo actualizar la vista previa. No se ha instalado nada.';
855
- }
856
- }
857
- render();
858
- return;
859
- }
860
-
861
- if (model.step === 3) {
862
- if (key === 'e' || key === 'E') {
863
- model.configDraft = { ...model.config };
864
- model.configCursor = 0;
865
- } else if (key === '\x7f' || key === '\x08' || key === '\x1b[D') {
866
- model.step = 2;
867
- model.previewData = null;
868
- } else if (key === '\x1b' || key === 'n' || key === 'N') {
869
- model.step = 1;
870
- model.previewData = null;
871
- model.statusMessage = 'Instalación cancelada; no se modificó ningún archivo.';
872
- } else if ((key === '\r' || key === '\n' || key === 's' || key === 'S') && model.previewData) {
873
- try {
874
- model.resultData = await executeApprovedPreview(model, setupOptions);
875
- model.step = 'done';
876
- } catch (err) {
877
- model.step = 1;
878
- model.previewData = null;
879
- model.statusMessage = err?.message || 'No se pudo completar la instalación.';
880
- }
881
- }
882
- render();
883
- return;
884
- }
885
-
886
- // Back from optional skills keeps both lists and their choices intact.
887
- if (model.step === 2 && (key === '\x7f' || key === '\x08' || key === '\x1b[D')) {
888
- model.step = 1;
889
- render();
890
- return;
891
- }
892
-
893
- // Project and optional skill list controls
894
- const activeList = model.step === 1 ? model.fundamentalItems : model.optionalItems;
895
- const stepNum = model.step;
896
-
897
- if ((key === '\x1b[A' || key === 'k') && activeList.length > 0) {
898
- model.cursors[stepNum] = (model.cursors[stepNum] - 1 + activeList.length) % activeList.length;
899
- render();
900
- return;
901
- }
902
- if ((key === '\x1b[B' || key === 'j') && activeList.length > 0) {
903
- model.cursors[stepNum] = (model.cursors[stepNum] + 1) % activeList.length;
904
- render();
905
- return;
906
- }
907
- if (key === ' ' && activeList.length > 0) {
908
- const item = activeList[model.cursors[stepNum]];
909
- if (item) {
910
- item.checked = !item.checked;
911
- item.selectionSource = 'user';
912
- }
913
- render();
914
- return;
915
- }
916
- if ((key === 'a' || key === 'A') && activeList.length > 0) {
917
- const allChecked = activeList.every(i => i.checked);
918
- for (const item of activeList) {
919
- item.checked = !allChecked;
920
- item.selectionSource = 'user';
921
- }
922
- render();
923
- return;
924
- }
925
- if (key === '\r' || key === '\n') {
926
- if (model.step === 1) {
927
- model.step = 2;
928
- render();
929
- return;
930
- }
931
- // Optional skills -> final review
932
- try {
933
- await updateReview();
934
- } catch (err) {
935
- model.statusMessage = err?.message || 'No se pudo comprobar la vista previa.';
936
- }
937
- render();
938
- }
939
- };
940
-
941
- const onData = chunk => {
942
- const key = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
943
- if (key === '\x03') {
944
- cleanup(0);
945
- return;
946
- }
947
- processingChain = processingChain.then(() => handleKey(key)).catch(() => {});
948
- };
949
-
950
- try {
951
- output.write('\x1b[?25l');
952
- if (typeof input.setRawMode === 'function') input.setRawMode(true);
953
- input.resume?.();
954
- input.on('data', onData);
955
- output.on?.('resize', render);
956
- render();
957
- } catch {
958
- cleanup(1);
959
- }
960
- });
961
- }