@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.
- package/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/SECURITY.md +31 -0
- package/harness/packages/cli/bin/tamnora.mjs +11 -0
- package/harness/packages/cli/package.json +10 -0
- package/harness/packages/cli/src/cli.js +90 -0
- package/harness/packages/cli/src/commands/autostart.js +121 -0
- package/harness/packages/cli/src/commands/bridge.js +274 -0
- package/harness/packages/cli/src/commands/doctor.js +73 -0
- package/harness/packages/cli/src/commands/eval.js +183 -0
- package/harness/packages/cli/src/commands/init.js +31 -0
- package/harness/packages/cli/src/commands/mcp.js +101 -0
- package/harness/packages/cli/src/commands/repl.js +172 -0
- package/harness/packages/cli/src/commands/run.js +165 -0
- package/harness/packages/cli/src/commands/serve.js +1 -0
- package/harness/packages/cli/src/commands/server.js +345 -0
- package/harness/packages/cli/src/commands/tui.js +110 -0
- package/harness/packages/cli/src/commands/update.js +76 -0
- package/harness/packages/cli/src/commands/user.js +135 -0
- package/harness/packages/cli/src/lib/core.js +2 -0
- package/harness/packages/cli/src/lib/daemon.js +153 -0
- package/harness/packages/cli/src/lib/qr.js +203 -0
- package/harness/packages/cli/src/lib/runtime.js +110 -0
- package/harness/packages/cli/src/lib/tunnel.js +113 -0
- package/harness/packages/cli/src/lib/util.js +24 -0
- package/harness/packages/cli/src/tui/app.js +302 -0
- package/harness/packages/cli/src/tui/keys.js +78 -0
- package/harness/packages/cli/src/tui/layout.js +130 -0
- package/harness/packages/cli/src/tui/screen.js +13 -0
- package/harness/packages/cli/src/ui/color.js +15 -0
- package/harness/packages/cli/test/release.test.js +34 -0
- package/harness/packages/cli/test/tui.test.js +99 -0
- package/harness/packages/cli/test/tunnel.test.js +20 -0
- package/harness/packages/core/package.json +14 -0
- package/harness/packages/core/src/agent/context.js +66 -0
- package/harness/packages/core/src/agent/modes.js +26 -0
- package/harness/packages/core/src/agent/runAgent.js +257 -0
- package/harness/packages/core/src/config/app.js +49 -0
- package/harness/packages/core/src/config/home.js +98 -0
- package/harness/packages/core/src/config/identity.js +41 -0
- package/harness/packages/core/src/config/index.js +69 -0
- package/harness/packages/core/src/config/providers.js +24 -0
- package/harness/packages/core/src/env.js +19 -0
- package/harness/packages/core/src/eval/dataset.js +47 -0
- package/harness/packages/core/src/eval/judge.js +39 -0
- package/harness/packages/core/src/eval/report.js +48 -0
- package/harness/packages/core/src/eval/runTask.js +71 -0
- package/harness/packages/core/src/eval/runner.js +150 -0
- package/harness/packages/core/src/eval/scorers.js +217 -0
- package/harness/packages/core/src/eval/task.js +59 -0
- package/harness/packages/core/src/index.js +72 -0
- package/harness/packages/core/src/mcp/common.js +82 -0
- package/harness/packages/core/src/mcp/http.js +149 -0
- package/harness/packages/core/src/mcp/index.js +167 -0
- package/harness/packages/core/src/mcp/stdio.js +218 -0
- package/harness/packages/core/src/providers/index.js +119 -0
- package/harness/packages/core/src/providers/ollama.js +76 -0
- package/harness/packages/core/src/providers/openaiCompatible.js +57 -0
- package/harness/packages/core/src/providers/openrouter.js +57 -0
- package/harness/packages/core/src/providers/presets.js +19 -0
- package/harness/packages/core/src/providers/resilience.js +74 -0
- package/harness/packages/core/src/providers/stream.js +200 -0
- package/harness/packages/core/src/providers/utils.js +66 -0
- package/harness/packages/core/src/storage/sqlite.js +453 -0
- package/harness/packages/core/src/tools/diff.js +90 -0
- package/harness/packages/core/src/tools/executors.js +277 -0
- package/harness/packages/core/src/tools/glob.js +67 -0
- package/harness/packages/core/src/tools/index.js +28 -0
- package/harness/packages/core/src/tools/paths.js +29 -0
- package/harness/packages/core/src/tools/permissions.js +69 -0
- package/harness/packages/core/src/tools/registry.js +44 -0
- package/harness/packages/core/src/tools/schema.js +177 -0
- package/harness/packages/core/test/agent.test.js +237 -0
- package/harness/packages/core/test/config.test.js +33 -0
- package/harness/packages/core/test/context.test.js +44 -0
- package/harness/packages/core/test/diff.test.js +20 -0
- package/harness/packages/core/test/eval.test.js +144 -0
- package/harness/packages/core/test/git-tools.test.js +44 -0
- package/harness/packages/core/test/home.test.js +62 -0
- package/harness/packages/core/test/mcp.test.js +158 -0
- package/harness/packages/core/test/providers.test.js +80 -0
- package/harness/packages/core/test/resilience.test.js +61 -0
- package/harness/packages/core/test/storage.test.js +55 -0
- package/harness/packages/core/test/stream.test.js +91 -0
- package/harness/packages/core/test/tools.test.js +96 -0
- package/harness/packages/server/bin/tamnora-server.mjs +11 -0
- package/harness/packages/server/package.json +10 -0
- package/harness/packages/server/public/app.js +2188 -0
- package/harness/packages/server/public/img/fondo-agentes.webp +0 -0
- package/harness/packages/server/public/index.html +368 -0
- package/harness/packages/server/public/landing/index.html +523 -0
- package/harness/packages/server/public/landing/landing.js +89 -0
- package/harness/packages/server/public/markdown.js +174 -0
- package/harness/packages/server/public/styles.css +923 -0
- package/harness/packages/server/src/auth.js +146 -0
- package/harness/packages/server/src/cli.js +61 -0
- package/harness/packages/server/src/index.js +1 -0
- package/harness/packages/server/src/lib/approvals.js +51 -0
- package/harness/packages/server/src/lib/core.js +2 -0
- package/harness/packages/server/src/lib/http.js +74 -0
- package/harness/packages/server/src/lib/modelCache.js +32 -0
- package/harness/packages/server/src/lib/prompt.js +48 -0
- package/harness/packages/server/src/lib/workspace.js +74 -0
- package/harness/packages/server/src/routes/auth.js +56 -0
- package/harness/packages/server/src/routes/chat.js +222 -0
- package/harness/packages/server/src/routes/eval.js +100 -0
- package/harness/packages/server/src/routes/files.js +117 -0
- package/harness/packages/server/src/routes/folders.js +80 -0
- package/harness/packages/server/src/routes/mcp.js +50 -0
- package/harness/packages/server/src/routes/models.js +26 -0
- package/harness/packages/server/src/routes/providers.js +135 -0
- package/harness/packages/server/src/routes/sessions.js +157 -0
- package/harness/packages/server/src/server.js +249 -0
- package/harness/packages/server/src/static.js +67 -0
- package/harness/packages/server/test/markdown.test.js +36 -0
- package/harness/packages/server/test/modelCache.test.js +72 -0
- package/harness/packages/server/test/server.test.js +809 -0
- package/harness/scripts/example-dataset.jsonl +4 -0
- package/harness/scripts/mcp-echo-server.mjs +108 -0
- package/harness/scripts/mock-provider.mjs +134 -0
- package/harness/tamnora.example.json +18 -0
- 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,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
|
+
}
|