@danieltmn/tamnora 0.1.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 (123) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +130 -0
  4. package/SECURITY.md +31 -0
  5. package/harness/packages/cli/bin/tamnora.mjs +11 -0
  6. package/harness/packages/cli/package.json +10 -0
  7. package/harness/packages/cli/src/cli.js +90 -0
  8. package/harness/packages/cli/src/commands/autostart.js +121 -0
  9. package/harness/packages/cli/src/commands/bridge.js +274 -0
  10. package/harness/packages/cli/src/commands/doctor.js +73 -0
  11. package/harness/packages/cli/src/commands/eval.js +183 -0
  12. package/harness/packages/cli/src/commands/init.js +31 -0
  13. package/harness/packages/cli/src/commands/mcp.js +101 -0
  14. package/harness/packages/cli/src/commands/repl.js +172 -0
  15. package/harness/packages/cli/src/commands/run.js +165 -0
  16. package/harness/packages/cli/src/commands/serve.js +1 -0
  17. package/harness/packages/cli/src/commands/server.js +345 -0
  18. package/harness/packages/cli/src/commands/tui.js +110 -0
  19. package/harness/packages/cli/src/commands/update.js +76 -0
  20. package/harness/packages/cli/src/commands/user.js +135 -0
  21. package/harness/packages/cli/src/lib/core.js +2 -0
  22. package/harness/packages/cli/src/lib/daemon.js +153 -0
  23. package/harness/packages/cli/src/lib/qr.js +203 -0
  24. package/harness/packages/cli/src/lib/runtime.js +110 -0
  25. package/harness/packages/cli/src/lib/tunnel.js +113 -0
  26. package/harness/packages/cli/src/lib/util.js +24 -0
  27. package/harness/packages/cli/src/tui/app.js +302 -0
  28. package/harness/packages/cli/src/tui/keys.js +78 -0
  29. package/harness/packages/cli/src/tui/layout.js +130 -0
  30. package/harness/packages/cli/src/tui/screen.js +13 -0
  31. package/harness/packages/cli/src/ui/color.js +15 -0
  32. package/harness/packages/cli/test/release.test.js +34 -0
  33. package/harness/packages/cli/test/tui.test.js +99 -0
  34. package/harness/packages/cli/test/tunnel.test.js +20 -0
  35. package/harness/packages/core/package.json +14 -0
  36. package/harness/packages/core/src/agent/context.js +66 -0
  37. package/harness/packages/core/src/agent/modes.js +26 -0
  38. package/harness/packages/core/src/agent/runAgent.js +257 -0
  39. package/harness/packages/core/src/config/app.js +49 -0
  40. package/harness/packages/core/src/config/home.js +98 -0
  41. package/harness/packages/core/src/config/identity.js +41 -0
  42. package/harness/packages/core/src/config/index.js +69 -0
  43. package/harness/packages/core/src/config/providers.js +24 -0
  44. package/harness/packages/core/src/env.js +19 -0
  45. package/harness/packages/core/src/eval/dataset.js +47 -0
  46. package/harness/packages/core/src/eval/judge.js +39 -0
  47. package/harness/packages/core/src/eval/report.js +48 -0
  48. package/harness/packages/core/src/eval/runTask.js +71 -0
  49. package/harness/packages/core/src/eval/runner.js +150 -0
  50. package/harness/packages/core/src/eval/scorers.js +217 -0
  51. package/harness/packages/core/src/eval/task.js +59 -0
  52. package/harness/packages/core/src/index.js +72 -0
  53. package/harness/packages/core/src/mcp/common.js +82 -0
  54. package/harness/packages/core/src/mcp/http.js +149 -0
  55. package/harness/packages/core/src/mcp/index.js +167 -0
  56. package/harness/packages/core/src/mcp/stdio.js +218 -0
  57. package/harness/packages/core/src/providers/index.js +119 -0
  58. package/harness/packages/core/src/providers/ollama.js +76 -0
  59. package/harness/packages/core/src/providers/openaiCompatible.js +57 -0
  60. package/harness/packages/core/src/providers/openrouter.js +57 -0
  61. package/harness/packages/core/src/providers/presets.js +19 -0
  62. package/harness/packages/core/src/providers/resilience.js +74 -0
  63. package/harness/packages/core/src/providers/stream.js +200 -0
  64. package/harness/packages/core/src/providers/utils.js +66 -0
  65. package/harness/packages/core/src/storage/sqlite.js +453 -0
  66. package/harness/packages/core/src/tools/diff.js +90 -0
  67. package/harness/packages/core/src/tools/executors.js +277 -0
  68. package/harness/packages/core/src/tools/glob.js +67 -0
  69. package/harness/packages/core/src/tools/index.js +28 -0
  70. package/harness/packages/core/src/tools/paths.js +29 -0
  71. package/harness/packages/core/src/tools/permissions.js +69 -0
  72. package/harness/packages/core/src/tools/registry.js +44 -0
  73. package/harness/packages/core/src/tools/schema.js +177 -0
  74. package/harness/packages/core/test/agent.test.js +237 -0
  75. package/harness/packages/core/test/config.test.js +33 -0
  76. package/harness/packages/core/test/context.test.js +44 -0
  77. package/harness/packages/core/test/diff.test.js +20 -0
  78. package/harness/packages/core/test/eval.test.js +144 -0
  79. package/harness/packages/core/test/git-tools.test.js +44 -0
  80. package/harness/packages/core/test/home.test.js +62 -0
  81. package/harness/packages/core/test/mcp.test.js +158 -0
  82. package/harness/packages/core/test/providers.test.js +80 -0
  83. package/harness/packages/core/test/resilience.test.js +61 -0
  84. package/harness/packages/core/test/storage.test.js +55 -0
  85. package/harness/packages/core/test/stream.test.js +91 -0
  86. package/harness/packages/core/test/tools.test.js +96 -0
  87. package/harness/packages/server/bin/tamnora-server.mjs +11 -0
  88. package/harness/packages/server/package.json +10 -0
  89. package/harness/packages/server/public/app.js +2188 -0
  90. package/harness/packages/server/public/img/fondo-agentes.webp +0 -0
  91. package/harness/packages/server/public/index.html +368 -0
  92. package/harness/packages/server/public/landing/index.html +523 -0
  93. package/harness/packages/server/public/landing/landing.js +89 -0
  94. package/harness/packages/server/public/markdown.js +174 -0
  95. package/harness/packages/server/public/styles.css +923 -0
  96. package/harness/packages/server/src/auth.js +146 -0
  97. package/harness/packages/server/src/cli.js +61 -0
  98. package/harness/packages/server/src/index.js +1 -0
  99. package/harness/packages/server/src/lib/approvals.js +51 -0
  100. package/harness/packages/server/src/lib/core.js +2 -0
  101. package/harness/packages/server/src/lib/http.js +74 -0
  102. package/harness/packages/server/src/lib/modelCache.js +32 -0
  103. package/harness/packages/server/src/lib/prompt.js +48 -0
  104. package/harness/packages/server/src/lib/workspace.js +74 -0
  105. package/harness/packages/server/src/routes/auth.js +56 -0
  106. package/harness/packages/server/src/routes/chat.js +222 -0
  107. package/harness/packages/server/src/routes/eval.js +100 -0
  108. package/harness/packages/server/src/routes/files.js +117 -0
  109. package/harness/packages/server/src/routes/folders.js +80 -0
  110. package/harness/packages/server/src/routes/mcp.js +50 -0
  111. package/harness/packages/server/src/routes/models.js +26 -0
  112. package/harness/packages/server/src/routes/providers.js +135 -0
  113. package/harness/packages/server/src/routes/sessions.js +157 -0
  114. package/harness/packages/server/src/server.js +249 -0
  115. package/harness/packages/server/src/static.js +67 -0
  116. package/harness/packages/server/test/markdown.test.js +36 -0
  117. package/harness/packages/server/test/modelCache.test.js +72 -0
  118. package/harness/packages/server/test/server.test.js +809 -0
  119. package/harness/scripts/example-dataset.jsonl +4 -0
  120. package/harness/scripts/mcp-echo-server.mjs +108 -0
  121. package/harness/scripts/mock-provider.mjs +134 -0
  122. package/harness/tamnora.example.json +18 -0
  123. package/package.json +70 -0
@@ -0,0 +1,110 @@
1
+ import path from 'node:path';
2
+ import { loadConfig, loadInstructions, Store, buildMcpRegistry, resolveMode } from '../lib/core.js';
3
+ import { parse, databasePath, num } from '../lib/util.js';
4
+ import { buildSystemPrompt, resolveCliProvider } from '../lib/runtime.js';
5
+ import { runTui } from '../tui/app.js';
6
+ import { color } from '../ui/color.js';
7
+
8
+ const OPTIONS = {
9
+ provider: { type: 'string' },
10
+ model: { type: 'string' },
11
+ dir: { type: 'string' },
12
+ 'max-steps': { type: 'string' },
13
+ mode: { type: 'string' },
14
+ system: { type: 'string' },
15
+ tools: { type: 'string' },
16
+ 'no-tools': { type: 'boolean' },
17
+ yes: { type: 'boolean', short: 'y' },
18
+ help: { type: 'boolean', short: 'h' },
19
+ };
20
+
21
+ export async function tuiCommand(argv) {
22
+ const { values } = parse(argv, OPTIONS);
23
+ if (values.help) {
24
+ printHelp();
25
+ return 0;
26
+ }
27
+
28
+ const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
29
+ const config = await loadConfig(cwd);
30
+ const store = new Store(databasePath(cwd));
31
+ const { providerId, model, baseUrl, apiKey } = resolveCliProvider(store, config, {
32
+ provider: values.provider,
33
+ model: values.model,
34
+ });
35
+
36
+ if (!model) {
37
+ console.error(color.red('Falta el modelo (usá --model, Ajustes o "model" en tamnora.json).'));
38
+ store.close();
39
+ return 1;
40
+ }
41
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
42
+ console.error(color.red('La consola requiere una terminal interactiva. Usá "tamnora repl" o "tamnora run".'));
43
+ store.close();
44
+ return 1;
45
+ }
46
+
47
+ const instructions = await loadInstructions(cwd, config.instructions);
48
+ let mcp = null;
49
+ if (Array.isArray(config.mcp) && config.mcp.length) {
50
+ try {
51
+ mcp = await buildMcpRegistry(config.mcp);
52
+ } catch (err) {
53
+ process.stderr.write(color.yellow(`MCP: ${err.message}\n`));
54
+ }
55
+ }
56
+
57
+ const baseEnabled = values['no-tools']
58
+ ? []
59
+ : values.tools
60
+ ? String(values.tools).split(',').map((s) => s.trim()).filter(Boolean)
61
+ : Array.isArray(config.tools) && config.tools.length
62
+ ? config.tools
63
+ : undefined;
64
+
65
+ const mode = resolveMode(values.mode || store.getSetting('activeMode') || 'agent');
66
+ const enabled = mode.tools
67
+ ? baseEnabled
68
+ ? baseEnabled.filter((tool) => mode.tools.includes(tool))
69
+ : mode.tools
70
+ : baseEnabled;
71
+
72
+ const sessionId = store.createSession({ title: 'Consola', provider: providerId, model, cwd });
73
+
74
+ return runTui({
75
+ providerId,
76
+ model,
77
+ baseUrl,
78
+ apiKey,
79
+ systemPrompt: buildSystemPrompt(instructions, [values.system, mode.systemPrompt].filter(Boolean).join('\n\n')),
80
+ maxRounds: num(values['max-steps'], config.maxRounds),
81
+ toolOptions: {
82
+ root: cwd,
83
+ enabled,
84
+ permissions: config.permissions,
85
+ mcp,
86
+ yes: Boolean(values.yes),
87
+ },
88
+ store,
89
+ sessionId,
90
+ });
91
+ }
92
+
93
+ function printHelp() {
94
+ console.log(`Uso: tamnora [--model <id>] [opciones]
95
+
96
+ Consola full-screen (estilo opencode/codex). Atajos:
97
+ enter enviar ↑ / ↓ historial
98
+ ← → mover cursor home/end inicio / fin
99
+ pgup/pgdn scroll ctrl+l limpiar
100
+ ctrl+c cancelar / salir /salir /limpiar
101
+
102
+ Opciones:
103
+ --provider <id> Proveedor (por defecto: el activo)
104
+ --model <id> Modelo (por defecto: el activo)
105
+ --mode <id> Modo de agente: agent | plan
106
+ --dir <path> Directorio del proyecto
107
+ --max-steps <n> Máximo de rondas de herramientas
108
+ --tools <lista> Tools habilitadas (coma) --no-tools
109
+ -y, --yes Auto-aprobar tools con permiso "ask"`);
110
+ }
@@ -0,0 +1,76 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import path from 'node:path';
5
+ import { parse } from '../lib/util.js';
6
+
7
+ const PACKAGE = '@danieltmn/tamnora';
8
+ const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm';
9
+ const OPTIONS = { yes: { type: 'boolean', short: 'y' }, check: { type: 'boolean' }, tag: { type: 'string' }, help: { type: 'boolean', short: 'h' } };
10
+
11
+ function currentVersion() {
12
+ try {
13
+ const pkg = JSON.parse(
14
+ readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '../../../../package.json'), 'utf8')
15
+ );
16
+ return pkg.version || '0.0.0';
17
+ } catch {
18
+ return '0.0.0';
19
+ }
20
+ }
21
+
22
+ function npm(args) {
23
+ return spawnSync(NPM, args, { encoding: 'utf8', windowsHide: true });
24
+ }
25
+
26
+ export async function updateCommand(argv) {
27
+ const { values } = parse(argv, OPTIONS);
28
+ if (values.help) {
29
+ console.log('Uso: tamnora update [--check] [--yes]\n\nBusca una version nueva en npm y actualiza la instalacion global.');
30
+ return 0;
31
+ }
32
+
33
+ const tag = typeof values.tag === 'string' && values.tag ? values.tag : 'latest';
34
+ const spec = `${PACKAGE}@${tag}`;
35
+ const version = currentVersion();
36
+ console.log(`Version actual: ${version}`);
37
+
38
+ const view = npm(['view', spec, 'version', '--json']);
39
+ let latest = '';
40
+ try {
41
+ latest = String(JSON.parse(view.stdout)).trim();
42
+ } catch {
43
+ latest = String(view.stdout || '').trim().replace(/"/g, '');
44
+ }
45
+ if (view.status !== 0 || !/^\d+\.\d+\.\d+/.test(latest)) {
46
+ console.error(`No se pudo consultar npm (${tag}): ${(view.stderr || '').trim() || 'revisa tu conexion'}`);
47
+ return 1;
48
+ }
49
+ console.log(`Disponible (${tag}): ${latest}`);
50
+ if (latest === version) {
51
+ console.log('Ya estas en la ultima version.');
52
+ return 0;
53
+ }
54
+ if (values.check) {
55
+ console.log(`Hay una version nueva. Corre: tamnora update --yes`);
56
+ return 0;
57
+ }
58
+ if (!values.yes) {
59
+ console.log('');
60
+ console.log('Para actualizar ahora:');
61
+ console.log(` tamnora update --yes (o) npm i -g ${spec}`);
62
+ return 0;
63
+ }
64
+
65
+ console.log(`Actualizando a ${spec}...`);
66
+ const result = spawnSync(NPM, ['i', '-g', spec], {
67
+ stdio: 'inherit',
68
+ windowsHide: true,
69
+ });
70
+ if (result.status !== 0) {
71
+ console.error(`La actualizacion fallo. Proba a mano: npm i -g ${spec}`);
72
+ return 1;
73
+ }
74
+ console.log('Listo. Reinicia el server: tamnora server --reload');
75
+ return 0;
76
+ }
@@ -0,0 +1,135 @@
1
+ import {
2
+ setHome,
3
+ readApp,
4
+ writeApp,
5
+ ensureApp,
6
+ makeUser,
7
+ userValidName,
8
+ randomToken,
9
+ } from '../../../core/src/index.js';
10
+ import { parse } from '../lib/util.js';
11
+ import { stopRunningServer } from '../lib/daemon.js';
12
+
13
+ const OPTIONS = {
14
+ name: { type: 'string' },
15
+ password: { type: 'string' },
16
+ home: { type: 'string' },
17
+ yes: { type: 'boolean', short: 'y' },
18
+ help: { type: 'boolean', short: 'h' },
19
+ };
20
+
21
+ export async function userCommand(argv) {
22
+ const { values, positionals } = parse(argv, OPTIONS);
23
+ if (values.help) {
24
+ printHelp();
25
+ return 0;
26
+ }
27
+ if (values.home) setHome(values.home);
28
+
29
+ ensureApp();
30
+ const app = readApp();
31
+ const sub = (positionals[0] || (values.name || values.password ? 'set' : 'show')).toLowerCase();
32
+
33
+ if (sub === 'show' || sub === 'list') {
34
+ if (!app.user) {
35
+ console.log('Sin usuario configurado (el server no pide login).');
36
+ return 0;
37
+ }
38
+ console.log(`usuario: ${app.user.name}`);
39
+ console.log(`creado : ${app.user.created}`);
40
+ return 0;
41
+ }
42
+
43
+ if (sub === 'remove' || sub === 'del' || sub === 'rm') {
44
+ if (!app.user) {
45
+ console.log('No hay usuario configurado.');
46
+ return 0;
47
+ }
48
+ app.user = null;
49
+ persist(app, 'Usuario eliminado (el server deja de pedir login).');
50
+ return 0;
51
+ }
52
+
53
+ if (sub === 'set' || sub === 'add' || sub === 'passwd' || sub === 'password') {
54
+ const name = String(values.name || app.user?.name || '').trim();
55
+ if (!userValidName(name)) {
56
+ console.error('Nombre invalido (letras, numeros, . _ -; 1-32). Usa --name <nombre>.');
57
+ return 1;
58
+ }
59
+ let password = values.password || '';
60
+ let generated = false;
61
+ if (!password && process.stdin.isTTY) {
62
+ password = await askHidden(`Contrasena para "${name}" (enter = generar): `);
63
+ }
64
+ if (!password) {
65
+ password = randomToken(12);
66
+ generated = true;
67
+ }
68
+ const prev = app.user && app.user.name === name ? app.user : null;
69
+ app.user = makeUser(name, password, prev);
70
+ persist(app, `Usuario "${name}" configurado.`);
71
+ if (generated) console.log(` contrasena: ${password} (generada)`);
72
+ return 0;
73
+ }
74
+
75
+ printHelp();
76
+ return 1;
77
+ }
78
+
79
+ function persist(app, message) {
80
+ if (stopRunningServer()) console.log('Habia un server corriendo; lo detuve para aplicar el cambio.');
81
+ writeApp(app);
82
+ console.log(message);
83
+ console.log(' volve a arrancar: tamnora server --start');
84
+ }
85
+
86
+ function askHidden(question) {
87
+ return new Promise((resolve) => {
88
+ const stdin = process.stdin;
89
+ process.stdout.write(question);
90
+ const wasRaw = stdin.isRaw;
91
+ if (stdin.isTTY) stdin.setRawMode(true);
92
+ stdin.resume();
93
+ let value = '';
94
+ const cleanup = () => {
95
+ stdin.off('data', onData);
96
+ if (stdin.isTTY) stdin.setRawMode(Boolean(wasRaw));
97
+ stdin.pause();
98
+ };
99
+ const onData = (chunk) => {
100
+ for (const ch of chunk.toString('utf8')) {
101
+ if (ch === '\r' || ch === '\n') {
102
+ cleanup();
103
+ process.stdout.write('\n');
104
+ resolve(value);
105
+ return;
106
+ }
107
+ if (ch === '\u0003') {
108
+ cleanup();
109
+ process.stdout.write('\n');
110
+ process.exit(130);
111
+ }
112
+ if (ch === '\u007f' || ch === '\b') {
113
+ value = value.slice(0, -1);
114
+ continue;
115
+ }
116
+ if (ch >= ' ') value += ch;
117
+ }
118
+ };
119
+ stdin.on('data', onData);
120
+ });
121
+ }
122
+
123
+ function printHelp() {
124
+ console.log(`Uso: tamnora user [show|set|passwd|remove] [--name <n>] [--password <p>]
125
+
126
+ Administra el usuario local (v1: uno solo). Al cambiar el usuario se detiene el
127
+ server; volve a arrancarlo con \`tamnora server --start\`.
128
+
129
+ (sin args) Muestra el usuario actual
130
+ set | passwd Crea/cambia el usuario (--name, --password)
131
+ remove Elimina el usuario (el server deja de pedir login)
132
+
133
+ Opciones:
134
+ --home <path> Casa de Tamnora (default: TAMNORA_HOME o datos del sistema)`);
135
+ }
@@ -0,0 +1,2 @@
1
+ // Re-export the harness core so the CLI has a single import point.
2
+ export * from '../../../core/src/index.js';
@@ -0,0 +1,153 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';
3
+ import {
4
+ runtimePath,
5
+ pidPath,
6
+ bridgeLogPath,
7
+ serverLogPath,
8
+ } from '../../../core/src/index.js';
9
+
10
+ /** Mata un proceso y su arbol de hijos (taskkill en Windows; grupo en POSIX). */
11
+ export function killPid(pid) {
12
+ if (!pid) return;
13
+ try {
14
+ if (process.platform === 'win32') {
15
+ spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true });
16
+ } else {
17
+ try {
18
+ process.kill(-pid, 'SIGTERM');
19
+ } catch {
20
+ /* sin grupo propio */
21
+ }
22
+ try {
23
+ process.kill(pid, 'SIGTERM');
24
+ } catch {
25
+ /* ya no esta */
26
+ }
27
+ }
28
+ } catch {
29
+ /* ya no esta */
30
+ }
31
+ }
32
+
33
+ export function pidAlive(pid) {
34
+ if (!pid) return false;
35
+ try {
36
+ process.kill(pid, 0);
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ export function readRuntime() {
44
+ try {
45
+ return JSON.parse(readFileSync(runtimePath(), 'utf8'));
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ export function writeRuntime(data) {
52
+ try {
53
+ writeFileSync(runtimePath(), `${JSON.stringify(data, null, 2)}\n`);
54
+ } catch {
55
+ /* sin runtime no detenemos nada */
56
+ }
57
+ }
58
+
59
+ export function clearRuntime() {
60
+ for (const file of [runtimePath(), pidPath()]) {
61
+ try {
62
+ unlinkSync(file);
63
+ } catch {
64
+ /* nada */
65
+ }
66
+ }
67
+ }
68
+
69
+ /** PIDs escuchando en un puerto TCP (netstat en Windows; lsof en el resto). */
70
+ export function portOwnerPids(port) {
71
+ try {
72
+ const win = process.platform === 'win32';
73
+ const r = spawnSync(
74
+ win ? 'netstat' : 'lsof',
75
+ win ? ['-ano', '-p', 'tcp'] : ['-t', '-i', `tcp:${port}`, '-s', 'tcp:listen'],
76
+ { encoding: 'utf8', windowsHide: true, timeout: 15000 }
77
+ );
78
+ if (r.error || !r.stdout) return [];
79
+ if (!win) {
80
+ return String(r.stdout)
81
+ .split(/\s+/)
82
+ .map((n) => parseInt(n, 10))
83
+ .filter((p) => p > 0);
84
+ }
85
+ const re = new RegExp(`:${port}\\s`);
86
+ const pids = new Set();
87
+ for (const line of String(r.stdout).split(/\r?\n/)) {
88
+ if (!re.test(line) || !/LISTENING/i.test(line)) continue;
89
+ const parts = line.trim().split(/\s+/);
90
+ const pid = parseInt(parts[parts.length - 1], 10);
91
+ if (pid > 0) pids.add(pid);
92
+ }
93
+ return [...pids];
94
+ } catch {
95
+ return [];
96
+ }
97
+ }
98
+
99
+ export function describePid(pid) {
100
+ try {
101
+ if (process.platform === 'win32') {
102
+ const r = spawnSync('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], {
103
+ encoding: 'utf8',
104
+ windowsHide: true,
105
+ timeout: 8000,
106
+ });
107
+ const m = String(r.stdout || '').match(/^"([^"]+)"/m);
108
+ return m ? m[1] : '';
109
+ }
110
+ const r = spawnSync('ps', ['-p', String(pid), '-o', 'comm='], {
111
+ encoding: 'utf8',
112
+ windowsHide: true,
113
+ timeout: 8000,
114
+ });
115
+ return String(r.stdout || '').trim();
116
+ } catch {
117
+ return '';
118
+ }
119
+ }
120
+
121
+ export function portHint(port) {
122
+ const pids = portOwnerPids(port);
123
+ if (!pids.length) return '';
124
+ return pids.map((p) => `PID ${p}${describePid(p) ? ` (${describePid(p)})` : ''}`).join(', ');
125
+ }
126
+
127
+ /** Detiene el server en ejecucion (si lo hay). Devuelve true si habia uno. */
128
+ export function stopRunningServer() {
129
+ const rt = readRuntime();
130
+ if (!rt || !pidAlive(rt.pid)) {
131
+ clearRuntime();
132
+ return false;
133
+ }
134
+ killPid(rt.pid);
135
+ if (Array.isArray(rt.children)) {
136
+ for (const c of rt.children) killPid(c);
137
+ }
138
+ clearRuntime();
139
+ return true;
140
+ }
141
+
142
+ export function tailLines(file, n) {
143
+ try {
144
+ const lines = readFileSync(file, 'utf8')
145
+ .split(/\r?\n/)
146
+ .filter((l) => l.trim() !== '');
147
+ return lines.slice(-n).join('\n');
148
+ } catch {
149
+ return '';
150
+ }
151
+ }
152
+
153
+ export { serverLogPath, bridgeLogPath };
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Generador de QR minimo (ISO/IEC 18004, Model 2) sin dependencias.
3
+ *
4
+ * Modo byte (UTF-8) con correccion de errores nivel L, versiones 1 a 5 (hasta
5
+ * 106 bytes: de sobra para una URL de tunel) y un solo bloque RS. La mascara es
6
+ * fija (0): cualquier mascara es valida y la info de formato la declara.
7
+ */
8
+
9
+ const VERSIONS = {
10
+ 1: { data: 19, ecc: 7, align: [] },
11
+ 2: { data: 34, ecc: 10, align: [6, 18] },
12
+ 3: { data: 55, ecc: 15, align: [6, 22] },
13
+ 4: { data: 80, ecc: 20, align: [6, 26] },
14
+ 5: { data: 108, ecc: 26, align: [6, 30] },
15
+ };
16
+
17
+ const MASK = 0;
18
+ const ECC_FORMAT_BITS = 1; // nivel L -> 01
19
+
20
+ function gfMul(x, y) {
21
+ let z = 0;
22
+ for (let i = 7; i >= 0; i--) {
23
+ z = (z << 1) ^ ((z >>> 7) * 0x11d);
24
+ z ^= ((y >>> i) & 1) * x;
25
+ }
26
+ return z & 0xff;
27
+ }
28
+
29
+ function rsDivisor(degree) {
30
+ const result = new Array(degree).fill(0);
31
+ result[degree - 1] = 1;
32
+ let root = 1;
33
+ for (let i = 0; i < degree; i++) {
34
+ for (let j = 0; j < result.length; j++) {
35
+ result[j] = gfMul(result[j], root);
36
+ if (j + 1 < result.length) result[j] ^= result[j + 1];
37
+ }
38
+ root = gfMul(root, 0x02);
39
+ }
40
+ return result;
41
+ }
42
+
43
+ function rsRemainder(data, divisor) {
44
+ const result = divisor.map(() => 0);
45
+ for (const b of data) {
46
+ const factor = b ^ result.shift();
47
+ result.push(0);
48
+ for (let i = 0; i < divisor.length; i++) result[i] ^= gfMul(divisor[i], factor);
49
+ }
50
+ return result;
51
+ }
52
+
53
+ function getBit(x, i) {
54
+ return ((x >>> i) & 1) !== 0;
55
+ }
56
+
57
+ function chooseVersion(byteLen) {
58
+ for (let v = 1; v <= 5; v++) {
59
+ if (byteLen <= VERSIONS[v].data - 2) return v;
60
+ }
61
+ return 0;
62
+ }
63
+
64
+ function dataCodewords(bytes, version) {
65
+ const cap = VERSIONS[version].data;
66
+ const bits = [];
67
+ const push = (val, len) => {
68
+ for (let i = len - 1; i >= 0; i--) bits.push((val >>> i) & 1);
69
+ };
70
+ push(0b0100, 4); // modo byte
71
+ push(bytes.length, 8); // conteo (v1-9: 8 bits)
72
+ for (const b of bytes) push(b, 8);
73
+ for (let i = 0; i < 4 && bits.length < cap * 8; i++) bits.push(0);
74
+ while (bits.length % 8 !== 0) bits.push(0);
75
+ const out = [];
76
+ for (let i = 0; i < bits.length; i += 8) {
77
+ let b = 0;
78
+ for (let j = 0; j < 8; j++) b = (b << 1) | bits[i + j];
79
+ out.push(b);
80
+ }
81
+ const pad = [0xec, 0x11];
82
+ let k = 0;
83
+ while (out.length < cap) out.push(pad[k++ % 2]);
84
+ return out;
85
+ }
86
+
87
+ function drawFormatBits(size, modules, isFn, mask) {
88
+ const data = (ECC_FORMAT_BITS << 3) | mask;
89
+ let rem = data;
90
+ for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
91
+ const bits = ((data << 10) | rem) ^ 0x5412;
92
+ const put = (x, y, dark) => {
93
+ modules[y][x] = dark;
94
+ isFn[y][x] = true;
95
+ };
96
+
97
+ for (let i = 0; i <= 5; i++) put(8, i, getBit(bits, i));
98
+ put(8, 7, getBit(bits, 6));
99
+ put(8, 8, getBit(bits, 7));
100
+ put(7, 8, getBit(bits, 8));
101
+ for (let i = 9; i < 15; i++) put(14 - i, 8, getBit(bits, i));
102
+
103
+ for (let i = 0; i < 8; i++) put(size - 1 - i, 8, getBit(bits, i));
104
+ for (let i = 8; i < 15; i++) put(8, size - 15 + i, getBit(bits, i));
105
+ put(8, size - 8, true);
106
+ }
107
+
108
+ function buildMatrix(version, codewords) {
109
+ const size = version * 4 + 17;
110
+ const modules = Array.from({ length: size }, () => new Array(size).fill(false));
111
+ const isFn = Array.from({ length: size }, () => new Array(size).fill(false));
112
+ const set = (x, y, dark) => {
113
+ modules[y][x] = dark;
114
+ isFn[y][x] = true;
115
+ };
116
+
117
+ for (let i = 0; i < size; i++) {
118
+ set(6, i, i % 2 === 0);
119
+ set(i, 6, i % 2 === 0);
120
+ }
121
+
122
+ const drawFinder = (cx, cy) => {
123
+ for (let dy = -4; dy <= 4; dy++) {
124
+ for (let dx = -4; dx <= 4; dx++) {
125
+ const x = cx + dx;
126
+ const y = cy + dy;
127
+ if (x < 0 || x >= size || y < 0 || y >= size) continue;
128
+ const dist = Math.max(Math.abs(dx), Math.abs(dy));
129
+ set(x, y, dist !== 2 && dist !== 4);
130
+ }
131
+ }
132
+ };
133
+ drawFinder(3, 3);
134
+ drawFinder(size - 4, 3);
135
+ drawFinder(3, size - 4);
136
+
137
+ const pos = VERSIONS[version].align;
138
+ for (const cy of pos) {
139
+ for (const cx of pos) {
140
+ if ((cx === 6 && cy === 6) || (cx === 6 && cy === size - 7) || (cx === size - 7 && cy === 6)) continue;
141
+ for (let dy = -2; dy <= 2; dy++) {
142
+ for (let dx = -2; dx <= 2; dx++) {
143
+ set(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ drawFormatBits(size, modules, isFn, 0);
150
+
151
+ let i = 0;
152
+ for (let right = size - 1; right >= 1; right -= 2) {
153
+ if (right === 6) right = 5;
154
+ for (let vert = 0; vert < size; vert++) {
155
+ for (let j = 0; j < 2; j++) {
156
+ const x = right - j;
157
+ const upward = ((right + 1) & 2) === 0;
158
+ const y = upward ? size - 1 - vert : vert;
159
+ if (isFn[y][x] || i >= codewords.length * 8) continue;
160
+ modules[y][x] = getBit(codewords[i >>> 3], 7 - (i & 7));
161
+ i++;
162
+ }
163
+ }
164
+ }
165
+
166
+ for (let y = 0; y < size; y++) {
167
+ for (let x = 0; x < size; x++) {
168
+ if (!isFn[y][x] && (x + y) % 2 === 0) modules[y][x] = !modules[y][x];
169
+ }
170
+ }
171
+
172
+ drawFormatBits(size, modules, isFn, MASK);
173
+ return { size, modules };
174
+ }
175
+
176
+ function qrMatrix(text) {
177
+ const bytes = Buffer.from(String(text), 'utf8');
178
+ const version = chooseVersion(bytes.length);
179
+ if (!version) return null;
180
+ const data = dataCodewords(bytes, version);
181
+ const ecc = rsRemainder(data, rsDivisor(VERSIONS[version].ecc));
182
+ return buildMatrix(version, data.concat(ecc));
183
+ }
184
+
185
+ /** QR para terminal con medios bloques. Devuelve '' si el texto no entra. */
186
+ export function qrTerminal(text, quiet) {
187
+ const qr = qrMatrix(text);
188
+ if (!qr) return '';
189
+ const q = quiet === undefined ? 2 : quiet;
190
+ const size = qr.size;
191
+ const at = (x, y) => x >= 0 && x < size && y >= 0 && y < size && qr.modules[y][x];
192
+ const lines = [];
193
+ for (let y = -q; y < size + q; y += 2) {
194
+ let line = '';
195
+ for (let x = -q; x < size + q; x++) {
196
+ const top = at(x, y);
197
+ const bot = at(x, y + 1);
198
+ line += top && bot ? '\u2588' : top ? '\u2580' : bot ? '\u2584' : ' ';
199
+ }
200
+ lines.push(line);
201
+ }
202
+ return lines.join('\n');
203
+ }