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.
- package/.claude-plugin/marketplace.json +18 -1
- package/README.md +40 -30
- package/assets/brand/README.md +0 -2
- package/assets/runtime/package-lock.json +2 -2
- package/bin/agent-task.js +12 -10
- package/bin/agent-toolkit-gemini.js +11 -9
- package/bin/agent-toolkit-inbox.js +12 -10
- package/bin/agent-toolkit-mcp.js +14 -8
- package/bin/agent-toolkit.js +12 -9
- package/bin/nexakit.js +12 -9
- package/bin/project-memory-mcp.js +12 -9
- package/bin/project-memory.js +5 -3
- package/{skills/depurar → catalogs/skills/vercel-composition-patterns}/LICENSE.txt +6 -6
- package/catalogs/skills/vercel-react-best-practices/LICENSE.txt +27 -0
- package/catalogs/skills.json +70 -35
- package/docs/skills/evidencias/2026-10-28-recomendador-preparacion.md +1 -1
- package/package.json +5 -3
- package/plugins/nexakit-modelo/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-modelo/hooks/hooks.json +1 -0
- package/plugins/nexakit-modelo/hooks/register.js +48 -0
- package/plugins/nexakit-modelo/hooks/status.js +8 -0
- package/plugins/nexakit-modelo/tests/register.test.ts +21 -0
- package/plugins/nexakit-panel/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-panel/README.md +20 -0
- package/plugins/nexakit-panel/hooks/hooks.json +1 -0
- package/plugins/nexakit-panel/hooks/lienzo.js +382 -0
- package/plugins/nexakit-panel/hooks/panel.js +279 -0
- package/plugins/nexakit-panel/hooks/register.tsx +248 -0
- package/plugins/nexakit-panel/tests/lienzo.test.ts +142 -0
- package/plugins/nexakit-panel/tests/panel.test.ts +240 -0
- package/plugins/nexakit-panel/tests/register.test.ts +167 -0
- package/plugins/nexakit-panel/types/index.d.ts +41 -0
- package/plugins/nexakit-sesion/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-sesion/README.md +12 -0
- package/plugins/nexakit-sesion/hooks/aviso.js +38 -0
- package/plugins/nexakit-sesion/hooks/hooks.json +1 -0
- package/plugins/nexakit-sesion/hooks/register.js +28 -0
- package/plugins/nexakit-sesion/tests/register.test.ts +96 -0
- package/schemas/skill-catalog.schema.json +9 -2
- package/skills/cerrar-memoria/SKILL.md +6 -2
- package/skills/cerrar-memoria/references/cierre.md +5 -1
- package/skills/elegir-modelo/SKILL.md +68 -0
- package/skills/elegir-modelo/modelos.mjs +108 -0
- package/skills/gemini/SKILL.md +1 -1
- package/skills/project-handoff/SKILL.md +39 -12
- package/skills/project-handoff/references/handoff-template.md +24 -17
- package/skills/quality-gate/SKILL.md +6 -12
- package/skills/revisar/SKILL.md +6 -16
- package/skills/{desarrollo-con-pruebas/LICENSE.txt → revisar-skill/LICENSE-find-skills.txt} +5 -5
- package/skills/revisar-skill/SKILL.md +94 -0
- package/skills/revision-cruzada/SKILL.md +53 -475
- package/skills/web-design-guidelines/AGENTS.md +156 -0
- package/skills/{spec-driven-development → web-design-guidelines}/LICENSE.txt +1 -6
- package/skills/web-design-guidelines/SKILL.md +20 -30
- package/src/agent-models.js +53 -13
- package/src/brand.js +117 -0
- package/src/cli-executable.js +5 -3
- package/src/cli.js +16 -9
- package/src/connect.js +56 -4
- package/src/init.js +9 -1
- package/src/install-lock.js +127 -0
- package/src/install-registry.js +61 -0
- package/src/legacy-repair.js +71 -0
- package/src/link-states.js +172 -0
- package/src/memory-locator.js +118 -0
- package/src/missing-dependencies.js +21 -0
- package/src/model-command.js +147 -0
- package/src/panel-cli.js +182 -0
- package/src/project-root.js +41 -0
- package/src/project-signals.js +339 -0
- package/src/recommend-report.js +77 -0
- package/src/review-instructions.js +211 -0
- package/src/review-policy.js +35 -0
- package/src/revision-adapters.js +157 -0
- package/src/revision-context.js +86 -0
- package/src/revision-isolation.js +222 -0
- package/src/revision-process.js +103 -0
- package/src/revision-provenance.js +89 -0
- package/src/revision-snapshot.js +120 -0
- package/src/revision.js +242 -0
- package/src/setup-apply.js +231 -0
- package/src/setup-plan.js +231 -0
- package/src/setup-state.js +277 -0
- package/src/setup-tui.js +555 -0
- package/src/skill-catalog-add.js +91 -0
- package/src/skill-catalog-audit.js +26 -27
- package/src/skill-files.js +39 -0
- package/src/skill-recommendations.js +321 -208
- package/src/skill-scan-report.js +14 -0
- package/src/skill-scan.js +178 -0
- package/src/stable-command.js +53 -0
- package/src/stable-install.js +222 -0
- package/src/superpowers.js +133 -0
- package/src/sync-apply.js +8 -5
- package/src/sync-files.js +2 -1
- package/src/terminal-text.js +68 -0
- package/src/toolkit-cli.js +397 -70
- package/src/toolkit-commands-guide.js +53 -39
- package/src/toolkit-setup.js +507 -110
- package/assets/brand/agent-toolkit-mark-v1.png +0 -0
- package/skills/depurar/SKILL.md +0 -69
- package/skills/depurar/agents/openai.yaml +0 -7
- package/skills/depurar/references/tecnicas-de-diagnostico.md +0 -37
- package/skills/desarrollo-con-pruebas/SKILL.md +0 -55
- package/skills/desarrollo-con-pruebas/agents/openai.yaml +0 -7
- package/skills/desarrollo-con-pruebas/references/buenas-pruebas.md +0 -33
- package/skills/estado/SKILL.md +0 -41
- package/skills/programar/SKILL.md +0 -19
- package/skills/spec-driven-development/SKILL.md +0 -48
- package/skills/spec-driven-development/agents/openai.yaml +0 -7
- package/skills/spec-driven-development/references/spec-template.md +0 -55
- package/skills/spec-driven-development/references/workflow.md +0 -70
- package/src/toolkit-tui.js +0 -961
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { getStaticTOMLValue, parseTOML } from 'toml-eslint-parser';
|
|
5
|
+
import { agentModels } from './agent-models.js';
|
|
6
|
+
import { resolveCliInvocation } from './cli-executable.js';
|
|
7
|
+
import { runRevisionProcess } from './revision-process.js';
|
|
8
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
9
|
+
|
|
10
|
+
export async function inspectModels({
|
|
11
|
+
homeDirectory = os.homedir(),
|
|
12
|
+
environment = process.env,
|
|
13
|
+
codexProfile,
|
|
14
|
+
runCommand,
|
|
15
|
+
includeAntigravity = true,
|
|
16
|
+
} = {}) {
|
|
17
|
+
const codexHome = environment.CODEX_HOME ?? path.join(homeDirectory, '.codex');
|
|
18
|
+
const claudeHome = environment.CLAUDE_CONFIG_DIR ?? path.join(homeDirectory, '.claude');
|
|
19
|
+
const old = await agentModels({ codexHome, claudeHome });
|
|
20
|
+
let cache = {},
|
|
21
|
+
config = {},
|
|
22
|
+
settings = {};
|
|
23
|
+
try {
|
|
24
|
+
cache = JSON.parse(await fs.readFile(path.join(codexHome, 'models_cache.json'), 'utf8'));
|
|
25
|
+
} catch {}
|
|
26
|
+
try {
|
|
27
|
+
config = getStaticTOMLValue(parseTOML(await fs.readFile(path.join(codexHome, 'config.toml'), 'utf8')));
|
|
28
|
+
} catch {}
|
|
29
|
+
try {
|
|
30
|
+
settings = JSON.parse(await fs.readFile(path.join(claudeHome, 'settings.json'), 'utf8'));
|
|
31
|
+
} catch {}
|
|
32
|
+
const profile = codexProfile ?? config.profile;
|
|
33
|
+
const selected = profile ? config.profiles?.[profile] : null;
|
|
34
|
+
let storedCodex =
|
|
35
|
+
profile && !selected
|
|
36
|
+
? null
|
|
37
|
+
: {
|
|
38
|
+
model: selected?.model ?? config.model ?? null,
|
|
39
|
+
effort: selected?.model_reasoning_effort ?? config.model_reasoning_effort ?? null,
|
|
40
|
+
};
|
|
41
|
+
// Modern Codex profiles can also be separate files; no shell interpolation.
|
|
42
|
+
if (profile && /^[a-zA-Z0-9._-]+$/u.test(profile)) {
|
|
43
|
+
try {
|
|
44
|
+
const layer = getStaticTOMLValue(
|
|
45
|
+
parseTOML(await fs.readFile(path.join(codexHome, `${profile}.config.toml`), 'utf8')),
|
|
46
|
+
);
|
|
47
|
+
storedCodex = {
|
|
48
|
+
model: layer.model ?? config.model ?? null,
|
|
49
|
+
effort: layer.model_reasoning_effort ?? config.model_reasoning_effort ?? null,
|
|
50
|
+
};
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
let antigravity = [],
|
|
54
|
+
error = null;
|
|
55
|
+
if (includeAntigravity) {
|
|
56
|
+
try {
|
|
57
|
+
let result;
|
|
58
|
+
if (runCommand) result = await runCommand('agy', ['models']);
|
|
59
|
+
else {
|
|
60
|
+
const cli = await resolveCliInvocation({ name: 'agy', environment });
|
|
61
|
+
result = await runRevisionProcess(
|
|
62
|
+
{ executable: cli.executable, args: [...cli.argsPrefix, 'models'], env: environment },
|
|
63
|
+
{ prompt: '', timeoutMs: 20000 },
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (result.status !== 0 || result.timedOut) throw new Error('No se pudo consultar agy models.');
|
|
67
|
+
antigravity = result.stdout.split('\n').flatMap((line) => {
|
|
68
|
+
const [model, label] = line.split('\t');
|
|
69
|
+
const effort = model?.match(/-(low|medium|high|xhigh|max)$/u)?.[1];
|
|
70
|
+
return model && label && effort
|
|
71
|
+
? [{ model, description: label, efforts: [effort], defaultEffort: effort }]
|
|
72
|
+
: [];
|
|
73
|
+
});
|
|
74
|
+
} catch (e) {
|
|
75
|
+
error = e.message;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const codex = old.codex.models.map((item) => ({
|
|
79
|
+
...item,
|
|
80
|
+
defaultEffort: cache.models?.find((m) => m.slug === item.model)?.default_reasoning_level ?? null,
|
|
81
|
+
}));
|
|
82
|
+
return {
|
|
83
|
+
catalogs: { claude: old.claude.map((m) => ({ ...m, defaultEffort: 'medium' })), codex, antigravity },
|
|
84
|
+
stored: {
|
|
85
|
+
claude: {
|
|
86
|
+
model: settings.model ?? null,
|
|
87
|
+
effort: settings.modelSettings?.[settings.model]?.effortLevel ?? settings.effortLevel ?? null,
|
|
88
|
+
},
|
|
89
|
+
codex: storedCodex,
|
|
90
|
+
antigravity: null,
|
|
91
|
+
},
|
|
92
|
+
sources: {
|
|
93
|
+
claude: `lista incluida en nexakit ${pkg.version}`,
|
|
94
|
+
codex: 'models_cache.json de Codex',
|
|
95
|
+
antigravity: error ?? 'agy models; el esfuerzo forma parte del id',
|
|
96
|
+
},
|
|
97
|
+
levels: old.levels,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function validateReviewChoice(options) {
|
|
102
|
+
const data = await inspectModels({ ...options, includeAntigravity: options.client === 'antigravity' });
|
|
103
|
+
const match = data.catalogs[options.client]?.find((item) => item.model === options.model);
|
|
104
|
+
if (!match || !match.efforts.includes(options.effort))
|
|
105
|
+
throw new Error('Modelo o esfuerzo no disponible en el catálogo real; consulta nexakit modelo.');
|
|
106
|
+
}
|
|
107
|
+
export async function runModelCommand(args, { out, err }, options = {}) {
|
|
108
|
+
let level, client, profile;
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
for (let i = 0; i < args.length; i++) {
|
|
111
|
+
const flag = args[i],
|
|
112
|
+
value = args[++i];
|
|
113
|
+
if (seen.has(flag) || !value) {
|
|
114
|
+
await err('Argumentos de modelo inválidos.\n');
|
|
115
|
+
return 2;
|
|
116
|
+
}
|
|
117
|
+
seen.add(flag);
|
|
118
|
+
if (flag === '--nivel') level = value;
|
|
119
|
+
else if (flag === '--ia') client = value;
|
|
120
|
+
else if (flag === '--perfil-codex') profile = value;
|
|
121
|
+
else return 2;
|
|
122
|
+
}
|
|
123
|
+
if (client && !['claude', 'codex', 'antigravity'].includes(client)) return 2;
|
|
124
|
+
const data = await inspectModels({ ...options, codexProfile: profile });
|
|
125
|
+
const row = level ? data.levels.find((x) => x.level.toLowerCase() === level.toLowerCase()) : null;
|
|
126
|
+
if (level && !row) return 2;
|
|
127
|
+
for (const ia of client ? [client] : ['claude', 'codex', 'antigravity']) {
|
|
128
|
+
await out(`${ia}: ${data.sources[ia]}\n`);
|
|
129
|
+
for (const model of data.catalogs[ia])
|
|
130
|
+
await out(
|
|
131
|
+
` ${model.model} · ${model.efforts.join(', ')} · defecto ${model.defaultEffort ?? 'no informado'}\n`,
|
|
132
|
+
);
|
|
133
|
+
const stored = data.stored[ia],
|
|
134
|
+
wanted = row?.[ia];
|
|
135
|
+
const mark =
|
|
136
|
+
!stored?.model || !stored?.effort || !wanted
|
|
137
|
+
? '?'
|
|
138
|
+
: stored.model === wanted.model && stored.effort === wanted.effort
|
|
139
|
+
? '✓'
|
|
140
|
+
: '→';
|
|
141
|
+
await out(
|
|
142
|
+
`${mark} Guardado: ${stored?.model ?? 'no comprobable'} · ${stored?.effort ?? 'no informado'}${wanted ? ` · recomendado ${wanted.model} · ${wanted.effort}` : ''}\n`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
await out('Compara lo guardado; una sesión abierta puede diferir. Compruébalo en /model.\n');
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
package/src/panel-cli.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// `nexakit panel`: dibuja a pantalla completa la orquesta de la sesión de Claude Code (líder, asesor, subagentes,
|
|
2
|
+
// uso). El mod nexakit-panel guarda una foto del estado real en ~/.nexakit/panel/actual.json en cada cambio; este
|
|
3
|
+
// comando la lee cada segundo y la pinta con el mismo lienzo que usa el panel dentro de Claude.
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { ANCHO, aAnsi, dibujar } from '../plugins/nexakit-panel/hooks/lienzo.js';
|
|
8
|
+
import { ejemplo, hora } from '../plugins/nexakit-panel/hooks/panel.js';
|
|
9
|
+
import { colorLevel } from './brand.js';
|
|
10
|
+
import { cellWidth, fitLine } from './terminal-text.js';
|
|
11
|
+
|
|
12
|
+
export const INACTIVA_MS = 10 * 60 * 1000;
|
|
13
|
+
const USO_EJEMPLO = { context: { window: 1000000, percent: 55 }, rateLimits: [{ kind: 'five_hour', percentUsed: 14 }, { kind: 'seven_day', percentUsed: 79 }] };
|
|
14
|
+
const AYUDA = `Uso: nexakit panel [--demo] [--una-vez]
|
|
15
|
+
|
|
16
|
+
Dibuja la orquesta de tu sesión de Claude Code a pantalla completa: ábrelo en una división de la terminal al
|
|
17
|
+
lado de Claude. Se actualiza solo cada segundo; q o Ctrl+C para salir. Necesita el mod nexakit-panel en
|
|
18
|
+
Claude Code y una ventana de al menos ${ANCHO} columnas.
|
|
19
|
+
--demo datos de ejemplo, marcados como ficticios, para ver el diseño
|
|
20
|
+
--una-vez dibuja una vez y sale
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
export async function leerFoto(homeDirectory) {
|
|
24
|
+
try {
|
|
25
|
+
const foto = JSON.parse(await fs.readFile(path.join(homeDirectory, '.nexakit', 'panel', 'actual.json'), 'utf8'));
|
|
26
|
+
return foto?.version === 1 && foto.estado && typeof foto.estado === 'object' ? foto : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// La sesión más reciente de Codex (~/.codex/sessions/AAAA/MM/DD/*.jsonl): modelo, tokens y límites. Es un formato
|
|
33
|
+
// interno de Codex: si cambia o falta algo, se devuelve null en vez de inventar cifras.
|
|
34
|
+
export async function leerCodex(codexHome) {
|
|
35
|
+
try {
|
|
36
|
+
const base = path.join(codexHome, 'sessions');
|
|
37
|
+
const desc = async (dir) => (await fs.readdir(dir, { withFileTypes: true }).catch(() => []))
|
|
38
|
+
.filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
39
|
+
const archivos = [];
|
|
40
|
+
for (const anio of (await desc(base)).slice(0, 2))
|
|
41
|
+
for (const mes of (await desc(path.join(base, anio))).slice(0, 2))
|
|
42
|
+
for (const dia of (await desc(path.join(base, anio, mes))).slice(0, 3))
|
|
43
|
+
for (const nombre of await fs.readdir(path.join(base, anio, mes, dia)).catch(() => []))
|
|
44
|
+
if (nombre.endsWith('.jsonl')) archivos.push(path.join(base, anio, mes, dia, nombre));
|
|
45
|
+
if (archivos.length === 0) return null;
|
|
46
|
+
const conFecha = await Promise.all(archivos.map(async (f) => ({ f, t: (await fs.stat(f)).mtimeMs })));
|
|
47
|
+
const { f, t } = conFecha.sort((a, b) => b.t - a.t)[0];
|
|
48
|
+
const datos = await fs.readFile(f);
|
|
49
|
+
const lineas = datos.subarray(Math.max(0, datos.length - 1_048_576)).toString('utf8').split('\n');
|
|
50
|
+
let cuenta = null, turno = null;
|
|
51
|
+
for (let i = lineas.length - 1; i >= 0 && !(cuenta && turno); i--) {
|
|
52
|
+
let d;
|
|
53
|
+
try { d = JSON.parse(lineas[i]); } catch { continue; }
|
|
54
|
+
if (!cuenta && d?.payload?.type === 'token_count' && d.payload.info?.total_token_usage) cuenta = d.payload;
|
|
55
|
+
if (!turno && d?.type === 'turn_context' && d.payload?.model) turno = d.payload;
|
|
56
|
+
}
|
|
57
|
+
if (!cuenta) return null;
|
|
58
|
+
const total = cuenta.info.total_token_usage;
|
|
59
|
+
const numero = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
60
|
+
return {
|
|
61
|
+
modelo: turno?.model ?? null,
|
|
62
|
+
esfuerzo: turno?.effort ?? null,
|
|
63
|
+
leidos: numero(total.input_tokens) ?? 0,
|
|
64
|
+
cache: numero(total.cached_input_tokens) ?? 0,
|
|
65
|
+
escritos: numero(total.output_tokens) ?? 0,
|
|
66
|
+
limite5h: numero(cuenta.rate_limits?.primary?.used_percent),
|
|
67
|
+
limiteSemana: numero(cuenta.rate_limits?.secondary?.used_percent),
|
|
68
|
+
actualizado: t,
|
|
69
|
+
};
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const centrada = (texto, columnas) => {
|
|
76
|
+
const t = fitLine(texto, columnas);
|
|
77
|
+
return ' '.repeat(Math.max(0, Math.floor((columnas - cellWidth(t)) / 2))) + t;
|
|
78
|
+
};
|
|
79
|
+
const tenue = (texto, nivel) => (nivel === 'none' ? texto : `\x1b[2m${texto}\x1b[22m`);
|
|
80
|
+
|
|
81
|
+
export function marcoDelPanel({ foto, columnas = ANCHO, filas, nivel = 'none', ahora = Date.now(), demo = false, codex = null }) {
|
|
82
|
+
if (columnas < ANCHO) {
|
|
83
|
+
return ['', centrada('NEXAKIT · ORQUESTA', columnas), '',
|
|
84
|
+
centrada(`El panel necesita ${ANCHO} columnas (ahora ${columnas}).`, columnas),
|
|
85
|
+
tenue(centrada('Ensancha esta división de la terminal.', columnas), nivel)].join('\n');
|
|
86
|
+
}
|
|
87
|
+
const estado = demo ? ejemplo(ahora) : foto?.estado;
|
|
88
|
+
if (!estado && codex) {
|
|
89
|
+
const { filas: lineas } = dibujar({ ...ejemplo(ahora), pasos: 0, agentes: [], registro: [] }, { ahora, uso: null, codex });
|
|
90
|
+
const margen = ' '.repeat(Math.floor((columnas - ANCHO) / 2));
|
|
91
|
+
return [...aAnsi(lineas, nivel).split('\n').map((l) => margen + l), tenue(centrada('Sin actividad de Claude Code todavía.', columnas), nivel)].join('\n');
|
|
92
|
+
}
|
|
93
|
+
if (!estado) {
|
|
94
|
+
return ['', centrada('NEXAKIT · ORQUESTA', columnas), '',
|
|
95
|
+
centrada('Esperando actividad de Claude Code…', columnas),
|
|
96
|
+
tenue(centrada('Abre Claude Code con el mod nexakit-panel y trabaja con normalidad: este panel se llena solo.', columnas), nivel)].join('\n');
|
|
97
|
+
}
|
|
98
|
+
const quieta = !demo && Number.isFinite(foto?.actualizado) && ahora - foto.actualizado > INACTIVA_MS;
|
|
99
|
+
const { filas: lineas } = dibujar(estado, {
|
|
100
|
+
ahora,
|
|
101
|
+
uso: demo ? USO_EJEMPLO : foto.uso,
|
|
102
|
+
carpeta: demo ? 'app' : foto.carpeta,
|
|
103
|
+
filasMax: filas ? filas - 1 - (quieta ? 1 : 0) : undefined,
|
|
104
|
+
asesorModelo: demo ? null : foto.asesorModelo,
|
|
105
|
+
esEjemplo: demo,
|
|
106
|
+
codex,
|
|
107
|
+
});
|
|
108
|
+
const margen = ' '.repeat(Math.floor((columnas - ANCHO) / 2));
|
|
109
|
+
const cuerpo = aAnsi(lineas, nivel).split('\n').map((linea) => margen + linea);
|
|
110
|
+
if (quieta) cuerpo.push(tenue(`${margen} sin actividad desde ${hora(foto.actualizado)}`, nivel));
|
|
111
|
+
return cuerpo.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function runPanelCommand(rest, { out, err, input = process.stdin, output = process.stdout, isTTY, options = {} }) {
|
|
115
|
+
const flags = new Set(rest);
|
|
116
|
+
if (rest.some((a) => !['--demo', '--una-vez', '--help', '-h'].includes(a)) || flags.size !== rest.length) {
|
|
117
|
+
await err(AYUDA);
|
|
118
|
+
return 2;
|
|
119
|
+
}
|
|
120
|
+
if (flags.has('--help') || flags.has('-h')) {
|
|
121
|
+
await out(AYUDA);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
const homeDirectory = options.homeDirectory ?? os.homedir();
|
|
125
|
+
const environment = options.environment ?? process.env;
|
|
126
|
+
const codexHome = options.codexHome ?? environment.CODEX_HOME ?? path.join(homeDirectory, '.codex');
|
|
127
|
+
const demo = flags.has('--demo');
|
|
128
|
+
const interactive = !flags.has('--una-vez') && Boolean(isTTY ?? (input?.isTTY && output?.isTTY));
|
|
129
|
+
const nivel = interactive ? colorLevel(output) : 'none';
|
|
130
|
+
const marco = async () => marcoDelPanel({
|
|
131
|
+
foto: demo ? null : await leerFoto(homeDirectory),
|
|
132
|
+
columnas: output?.columns || (interactive ? 80 : 120),
|
|
133
|
+
filas: interactive ? output?.rows : undefined,
|
|
134
|
+
nivel,
|
|
135
|
+
ahora: Date.now(),
|
|
136
|
+
demo,
|
|
137
|
+
codex: await leerCodex(codexHome),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!interactive) {
|
|
141
|
+
await out(`${await marco()}\n`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Pantalla alterna: se redibuja en el sitio, línea a línea, sin parpadeo ni llenar el historial.
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
let cerrado = false;
|
|
148
|
+
let dibujando = false;
|
|
149
|
+
const pintar = async () => {
|
|
150
|
+
if (cerrado || dibujando) return;
|
|
151
|
+
dibujando = true;
|
|
152
|
+
try {
|
|
153
|
+
const texto = await marco();
|
|
154
|
+
if (!cerrado) output.write(`\x1b[H${texto.split('\n').map((l) => `${l}\x1b[K`).join('\n')}\x1b[J`);
|
|
155
|
+
} finally {
|
|
156
|
+
dibujando = false;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const reloj = setInterval(pintar, 1000);
|
|
160
|
+
const cerrar = () => {
|
|
161
|
+
if (cerrado) return;
|
|
162
|
+
cerrado = true;
|
|
163
|
+
clearInterval(reloj);
|
|
164
|
+
input.off?.('data', alPulsar);
|
|
165
|
+
output.off?.('resize', pintar);
|
|
166
|
+
try { if (typeof input.setRawMode === 'function') input.setRawMode(false); } catch {}
|
|
167
|
+
try { input.pause?.(); } catch {}
|
|
168
|
+
output.write('\x1b[?25h\x1b[?1049l');
|
|
169
|
+
resolve(0);
|
|
170
|
+
};
|
|
171
|
+
const alPulsar = (datos) => {
|
|
172
|
+
const tecla = String(datos);
|
|
173
|
+
if (tecla === 'q' || tecla === 'Q' || tecla === '\x03') cerrar();
|
|
174
|
+
};
|
|
175
|
+
output.write('\x1b[?1049h\x1b[?25l');
|
|
176
|
+
try { if (typeof input.setRawMode === 'function') input.setRawMode(true); } catch {}
|
|
177
|
+
input.on?.('data', alPulsar);
|
|
178
|
+
input.resume?.();
|
|
179
|
+
output.on?.('resize', pintar);
|
|
180
|
+
pintar();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Decides which folder is "the project": the whole Git repository, even when nexakit runs from a subfolder, so
|
|
2
|
+
// one memory and one skill install serve the whole repository. Read-only: it only asks Git and reads paths.
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
// Same rule the memory uses for its id (src/init.js).
|
|
9
|
+
const PROJECT_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
|
|
10
|
+
const FALLBACK_ID = 'proyecto';
|
|
11
|
+
|
|
12
|
+
export const runGit = (args, cwd) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
13
|
+
|
|
14
|
+
export function projectIdentity(root) {
|
|
15
|
+
const name = path.basename(root);
|
|
16
|
+
const id = name.toLowerCase().replace(/[^a-z0-9]+/gu, '-').replace(/^-+|-+$/gu, '');
|
|
17
|
+
const valid = PROJECT_ID_PATTERN.test(id) && id.length >= 2 && id.length <= 64;
|
|
18
|
+
return { id: valid ? id : FALLBACK_ID, displayName: name.replace(/[_-]+/gu, ' ').replace(/\s+/gu, ' ').trim() || name };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const realpathOrNull = target => fs.realpath(target).catch(() => null);
|
|
22
|
+
|
|
23
|
+
// Any Git failure (not installed, not a repository, dubious ownership) means "no Git": detection never breaks on it.
|
|
24
|
+
async function gitTopLevel(startedIn, run) {
|
|
25
|
+
try {
|
|
26
|
+
const output = String(run(['rev-parse', '--show-toplevel'], startedIn) ?? '').trim();
|
|
27
|
+
return output ? await realpathOrNull(output) : null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function resolveProjectRoot(start, { homeDirectory = os.homedir(), runGit: run = runGit } = {}) {
|
|
34
|
+
const startedIn = await fs.realpath(start);
|
|
35
|
+
const topLevel = await gitTopLevel(startedIn, run);
|
|
36
|
+
// A repository at the home folder (dotfiles) is not the project: climbing there would scan and install into ~.
|
|
37
|
+
const home = await realpathOrNull(homeDirectory);
|
|
38
|
+
const git = Boolean(topLevel) && topLevel !== home;
|
|
39
|
+
const root = git ? topLevel : startedIn;
|
|
40
|
+
return { root, startedIn, git, fromSubfolder: root !== startedIn, ...projectIdentity(root) };
|
|
41
|
+
}
|