nio-cat 0.3.5 → 0.4.3
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/bin/nio.js +23 -70
- package/package.json +1 -1
- package/src/commands.js +51 -0
- package/src/proc.js +1 -0
- package/src/server.js +26 -0
- package/src/ssh.js +111 -0
- package/web/app/app.js +318 -140
- package/web/app/index.html +2 -0
- package/web/app/style.css +53 -0
- package/web/index.html +4 -2
- package/web/sw.js +1 -1
package/bin/nio.js
CHANGED
|
@@ -4,13 +4,13 @@ const os = require('os');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
|
-
const net = require('net');
|
|
8
7
|
const { spawn } = require('child_process');
|
|
9
8
|
const config = require('../src/config');
|
|
10
9
|
const { Hub } = require('../src/hub');
|
|
11
10
|
const { createServer } = require('../src/server');
|
|
12
11
|
const { detectAll } = require('../src/agents');
|
|
13
12
|
const { startTunnel } = require('../src/tunnel');
|
|
13
|
+
const { addServer } = require('../src/ssh');
|
|
14
14
|
const { pairLink, terminalQr, writePairPage, makeCode, codeLink, terminalQrSize } = require('../src/pair');
|
|
15
15
|
const { which } = require('../src/proc');
|
|
16
16
|
|
|
@@ -74,12 +74,14 @@ function canOpenBrowser() {
|
|
|
74
74
|
return true;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
let panelOpened = false;
|
|
77
78
|
function openDashboard() {
|
|
78
79
|
const local = `http://127.0.0.1:${cfg.port}`;
|
|
79
80
|
// Per defecte s'obre l'app en línia (sempre al dia), ja connectada a aquest ordinador. Amb --local, la còpia que porta nio.
|
|
80
81
|
const online = !flags.local;
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
// &qr=1: el panell mostra el QR per al mòbil tot just obrir-se, a la mateixa pestanya.
|
|
83
|
+
const link = pairLink(online ? flags.app || cfg.app : local, local, cfg.token) + '&qr=1';
|
|
84
|
+
if (canOpenBrowser() && openUrl(link)) { panelOpened = true; console.log(` ${c.dim('Panell:')} ${online ? (flags.app || cfg.app) + '/app' : local} ${c.dim('(obert al navegador; --no-open per evitar-ho' + (online ? '; --local per fer servir la còpia d\'aquest ordinador' : '') + ')')}`); }
|
|
83
85
|
else console.log(` ${c.dim('Panell:')} ${link}`);
|
|
84
86
|
}
|
|
85
87
|
|
|
@@ -95,8 +97,7 @@ async function showPair(publicUrl) {
|
|
|
95
97
|
console.log(`\n ${c.b('Escaneja aquest codi amb el mòbil')}\n`);
|
|
96
98
|
console.log(terminalQr(qrLink, { margin }));
|
|
97
99
|
} else {
|
|
98
|
-
console.log(`\n ${c.warn('El terminal és massa petit per al QR.')} Fes-lo més gran o obre
|
|
99
|
-
if (canOpenBrowser()) openUrl(file);
|
|
100
|
+
console.log(`\n ${c.warn('El terminal és massa petit per al QR.')} ${panelOpened ? 'El tens al panell del navegador.' : 'Fes-lo més gran, o obre el panell i prem «Continua al mòbil».'}`);
|
|
100
101
|
}
|
|
101
102
|
console.log(`\n ${c.b('Codi:')} ${code}`);
|
|
102
103
|
console.log(c.dim(' Copia\'l i enganxa\'l a Sessions → Tria un ordinador.'));
|
|
@@ -111,73 +112,26 @@ async function showPair(publicUrl) {
|
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
// nio ssh usuari@servidor: engega nio al servidor
|
|
115
|
-
const shq = (v) => `'${String(v).replace(/'/g, `'\\''`)}'`;
|
|
116
|
-
function remoteScript(port, dir) {
|
|
117
|
-
return `set -e
|
|
118
|
-
command -v node >/dev/null 2>&1 || { echo NIO_NO_NODE; exit 0; }
|
|
119
|
-
up() { (exec 3<>/dev/tcp/127.0.0.1/${port}) 2>/dev/null; }
|
|
120
|
-
if ! up; then
|
|
121
|
-
mkdir -p "$HOME/.nio"
|
|
122
|
-
if command -v setsid >/dev/null 2>&1; then S=setsid; else S=; fi
|
|
123
|
-
$S nohup npx --yes nio-cat@latest --no-open --no-tunnel --port ${port} ${dir ? shq(dir) : '"$HOME"'} > "$HOME/.nio/daemon.log" 2>&1 < /dev/null &
|
|
124
|
-
for i in $(seq 1 80); do up && break; sleep 0.5; done
|
|
125
|
-
up || { echo NIO_NO_START; tail -n 5 "$HOME/.nio/daemon.log" 2>/dev/null; exit 0; }
|
|
126
|
-
fi
|
|
127
|
-
node -e "console.log('NIO_TOKEN='+require(require('os').homedir()+'/.nio/config.json').token)"
|
|
128
|
-
`;
|
|
129
|
-
}
|
|
130
|
-
function sshBase(target) {
|
|
131
|
-
const a = ['-o', 'ServerAliveInterval=30'];
|
|
132
|
-
if (process.platform !== 'win32') a.push('-o', 'ControlMaster=auto', '-o', 'ControlPath=/tmp/nio-%C', '-o', 'ControlPersist=10m');
|
|
133
|
-
return a;
|
|
134
|
-
}
|
|
135
|
-
function waitPort(port, ms = 15000) {
|
|
136
|
-
const end = Date.now() + ms;
|
|
137
|
-
return new Promise((resolve) => {
|
|
138
|
-
const t = () => {
|
|
139
|
-
const s = net.connect(port, '127.0.0.1');
|
|
140
|
-
s.once('connect', () => { s.destroy(); resolve(true); });
|
|
141
|
-
s.once('error', () => { s.destroy(); Date.now() > end ? resolve(false) : setTimeout(t, 300); });
|
|
142
|
-
};
|
|
143
|
-
t();
|
|
144
|
-
});
|
|
145
|
-
}
|
|
115
|
+
// nio ssh usuari@servidor: engega nio al servidor amb un túnel i obre el panell amb el servidor ja afegit.
|
|
146
116
|
async function sshStart() {
|
|
147
117
|
const target = pos[0];
|
|
148
|
-
if (!target
|
|
149
|
-
console.log(`\n Ús: ${c.b('nio ssh usuari@servidor')} ${c.dim('[--port 7717] [--
|
|
118
|
+
if (!target) {
|
|
119
|
+
console.log(`\n Ús: ${c.b('nio ssh usuari@servidor')} ${c.dim('[--port 7717] [--dir /ruta/al/servidor]')}\n`);
|
|
150
120
|
process.exit(1);
|
|
151
121
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
if (out.buf.includes('NIO_NO_START')) { console.error(c.warn('\n nio no s’ha engegat al servidor:\n') + out.buf.replace('NIO_NO_START', '').trim() + '\n'); process.exit(1); }
|
|
166
|
-
const token = (out.buf.match(/NIO_TOKEN=(\S+)/) || [])[1];
|
|
167
|
-
if (!token) { console.error(c.warn('\n No he pogut llegir la clau del servidor.\n')); process.exit(1); }
|
|
168
|
-
|
|
169
|
-
const tun = spawn('ssh', [...sshBase(target), '-N', '-T', '-o', 'ExitOnForwardFailure=yes', '-L', `127.0.0.1:${lport}:127.0.0.1:${rport}`, '--', target], { stdio: ['ignore', 'ignore', 'inherit'] });
|
|
170
|
-
tun.on('close', () => { console.log(c.warn('\n S’ha tallat el túnel SSH.\n')); process.exit(0); });
|
|
171
|
-
const bye = () => { tun.kill(); process.exit(0); };
|
|
172
|
-
process.on('SIGINT', bye);
|
|
173
|
-
process.on('SIGTERM', bye);
|
|
174
|
-
if (!(await waitPort(lport))) { console.error(c.warn(`\n No puc obrir el port ${lport} en aquest ordinador. Prova amb --local-port 7719.\n`)); tun.kill(); process.exit(1); }
|
|
175
|
-
|
|
176
|
-
const local = `http://127.0.0.1:${lport}`, link = pairLink(local, local, token);
|
|
177
|
-
console.log(` ${c.ok('✓')} Connectat. nio funciona al servidor i el panell és a ${local}`);
|
|
178
|
-
if (canOpenBrowser() && openUrl(link)) console.log(` ${c.dim('Panell obert al navegador.')}`);
|
|
179
|
-
else console.log(` ${c.dim('Obre aquest enllaç:')} ${link}`);
|
|
180
|
-
console.log(c.dim('\n Deixa aquesta finestra oberta mentre facis servir el servidor. Amb Ctrl+C es tanca el túnel (nio continua al servidor).\n'));
|
|
122
|
+
console.log(`\n ${c.b('nio')} ${c.dim('· afegint')} ${target}${c.dim('…')}`);
|
|
123
|
+
let r;
|
|
124
|
+
try {
|
|
125
|
+
r = await addServer(target, { port: Number(flags.port || 7717), dir: flags.dir, batch: false, onStep: (t) => console.log(` ${c.dim('·')} ${t}…`) });
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.error(c.warn(`\n ${e.message}\n`));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const app = flags.app || cfg.app, link = pairLink(app, r.url, r.token), code = makeCode(r.url, r.token);
|
|
131
|
+
console.log(` ${c.ok('✓')} ${r.host} és a punt${r.claude ? '' : c.warn(' (Claude Code no hi és: instal·la’l i inicia sessió al servidor)')}`);
|
|
132
|
+
console.log(`\n ${c.b('Codi:')} ${code}`);
|
|
133
|
+
console.log(c.dim(' Enganxa’l a nio.cat → Ordinadors → Afegeix, al mòbil o a l’ordinador.\n'));
|
|
134
|
+
if (canOpenBrowser() && openUrl(link)) console.log(c.dim(' He obert nio.cat amb el servidor ja afegit.\n'));
|
|
181
135
|
}
|
|
182
136
|
|
|
183
137
|
async function start() {
|
|
@@ -294,9 +248,8 @@ async function start() {
|
|
|
294
248
|
--open Obre al navegador la pàgina del codi QR
|
|
295
249
|
nio pair Mostra el codi QR una altra vegada
|
|
296
250
|
|
|
297
|
-
nio ssh usuari@servidor
|
|
251
|
+
nio ssh usuari@servidor Afegeix un servidor (VPS) per SSH: hi engega nio amb un túnel i el pots fer servir des del mòbil.
|
|
298
252
|
--port <n> Port de nio al servidor (7717)
|
|
299
|
-
--local-port <n> Port en aquest ordinador (7718)
|
|
300
253
|
--dir <ruta> Carpeta del servidor on s'engega (per defecte, la teva carpeta d'inici)
|
|
301
254
|
|
|
302
255
|
Servidor amb adreça pública: nio --url https://la-teva-adreça --no-open
|
package/package.json
CHANGED
package/src/commands.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Comandes pròpies de cada agent que es poden escriure amb «/» a la sessió.
|
|
3
|
+
// Claude Code: les teves comandes (~/.claude/commands i .claude/commands del projecte) i les skills.
|
|
4
|
+
// Es passen a l'agent tal qual («/nom arguments»); el mode d'impressió les entén.
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
function frontmatter(file) {
|
|
10
|
+
let txt = '';
|
|
11
|
+
try { txt = fs.readFileSync(file, 'utf8').slice(0, 4000); } catch { return {}; }
|
|
12
|
+
const m = txt.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
13
|
+
const out = {};
|
|
14
|
+
if (m) for (const l of m[1].split('\n')) { const kv = l.match(/^([\w-]+):\s*(.*)$/); if (kv) out[kv[1]] = kv[2].trim().replace(/^["']|["']$/g, ''); }
|
|
15
|
+
if (!out.description) {
|
|
16
|
+
const first = txt.replace(/^---[\s\S]*?---\s*/, '').split('\n').map((l) => l.trim()).find((l) => l && !l.startsWith('#'));
|
|
17
|
+
if (first) out.description = first;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function listDir(dir) {
|
|
23
|
+
try { return fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function claudeCommands(cwd) {
|
|
27
|
+
const out = new Map();
|
|
28
|
+
const bases = [path.join(os.homedir(), '.claude'), cwd && path.join(cwd, '.claude')].filter(Boolean);
|
|
29
|
+
for (const base of bases) {
|
|
30
|
+
// Comandes: commands/nom.md i commands/espai/nom.md (→ «espai:nom»)
|
|
31
|
+
const cdir = path.join(base, 'commands');
|
|
32
|
+
for (const e of listDir(cdir)) {
|
|
33
|
+
if (e.isFile() && e.name.endsWith('.md')) { const n = e.name.slice(0, -3); out.set(n, { name: n, hint: frontmatter(path.join(cdir, e.name)).description || '', kind: 'comanda' }); }
|
|
34
|
+
else if (e.isDirectory()) for (const f of listDir(path.join(cdir, e.name))) if (f.isFile() && f.name.endsWith('.md')) { const n = `${e.name}:${f.name.slice(0, -3)}`; out.set(n, { name: n, hint: frontmatter(path.join(cdir, e.name, f.name)).description || '', kind: 'comanda' }); }
|
|
35
|
+
}
|
|
36
|
+
// Skills: skills/nom/SKILL.md
|
|
37
|
+
const sdir = path.join(base, 'skills');
|
|
38
|
+
for (const e of listDir(sdir)) {
|
|
39
|
+
if (!e.isDirectory() && !e.isSymbolicLink()) continue;
|
|
40
|
+
const fm = frontmatter(path.join(sdir, e.name, 'SKILL.md'));
|
|
41
|
+
if (fm.description || fs.existsSync(path.join(sdir, e.name, 'SKILL.md'))) { const n = fm.name || e.name; if (/^[\w:.-]+$/.test(n)) out.set(n, { name: n, hint: fm.description || '', kind: 'skill' }); }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return [...out.values()].map((c) => ({ ...c, hint: c.hint.slice(0, 110) })).sort((a, b) => a.name.localeCompare(b.name)).slice(0, 150);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function agentCommands(agent, cwd) {
|
|
48
|
+
return agent === 'claude' ? claudeCommands(cwd) : [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { agentCommands };
|
package/src/proc.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const { spawn, execFile } = require('child_process');
|
|
7
7
|
|
|
8
8
|
const EXTRA_DIRS = [
|
|
9
|
+
path.join(os.homedir(), '.nio', 'bin'),
|
|
9
10
|
path.join(os.homedir(), '.local', 'bin'),
|
|
10
11
|
path.join(os.homedir(), '.npm-global', 'bin'),
|
|
11
12
|
path.join(os.homedir(), '.bun', 'bin'),
|
package/src/server.js
CHANGED
|
@@ -10,6 +10,8 @@ const ollama = require('./agents/ollama');
|
|
|
10
10
|
const { save } = require('./config');
|
|
11
11
|
const { httpErr } = require('./hub');
|
|
12
12
|
const github = require('./github');
|
|
13
|
+
const { addServer, knownHosts } = require('./ssh');
|
|
14
|
+
const { agentCommands } = require('./commands');
|
|
13
15
|
const { pairLink, qrSvg, makeCode, codeLink } = require('./pair');
|
|
14
16
|
|
|
15
17
|
// L'app web: copiada dins del paquet en publicar-lo, o l'arrel del repositori en desenvolupament.
|
|
@@ -172,6 +174,7 @@ function createServer({ cfg, hub, getPublicUrl, getApp }) {
|
|
|
172
174
|
id: cfg.id,
|
|
173
175
|
};
|
|
174
176
|
}
|
|
177
|
+
if (p === '/api/ssh/hosts' && method === 'GET') return knownHosts();
|
|
175
178
|
if (p === '/api/projects' && method === 'GET') return listProjects(cfg);
|
|
176
179
|
if (p === '/api/fs' && method === 'GET') return listDir(cfg, url.searchParams.get('path'));
|
|
177
180
|
if (p === '/api/ollama/models' && method === 'GET') return (await ollama.listModels()) || [];
|
|
@@ -267,6 +270,11 @@ function createServer({ cfg, hub, getPublicUrl, getApp }) {
|
|
|
267
270
|
return { ok: true };
|
|
268
271
|
}
|
|
269
272
|
}
|
|
273
|
+
if ((r = m(/^\/api\/sessions\/([\w-]+)\/commands$/)) && method === 'GET') {
|
|
274
|
+
const s = hub.get(r[1]);
|
|
275
|
+
if (!s) throw httpErr(404, 'Aquesta sessió no existeix');
|
|
276
|
+
return agentCommands(s.agent, s.cwd);
|
|
277
|
+
}
|
|
270
278
|
if ((r = m(/^\/api\/sessions\/([\w-]+)\/git(?:\/(commit|pr))?$/))) {
|
|
271
279
|
const s = hub.get(r[1]);
|
|
272
280
|
if (!s) throw httpErr(404, 'Aquesta sessió no existeix');
|
|
@@ -361,6 +369,12 @@ function createServer({ cfg, hub, getPublicUrl, getApp }) {
|
|
|
361
369
|
}
|
|
362
370
|
}
|
|
363
371
|
|
|
372
|
+
// Sense clau: només diu que nio és aquí (la web ho fa servir per detectar «Aquest ordinador»).
|
|
373
|
+
if (url.pathname === '/api/hello' && req.method === 'GET') {
|
|
374
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
375
|
+
return res.end(JSON.stringify({ nio: true, id: cfg.id, name: os.hostname() }));
|
|
376
|
+
}
|
|
377
|
+
|
|
364
378
|
if (url.pathname.startsWith('/api/')) {
|
|
365
379
|
if (!tokenOk(cfg, req)) {
|
|
366
380
|
res.writeHead(401, { 'content-type': 'application/json' });
|
|
@@ -378,6 +392,18 @@ function createServer({ cfg, hub, getPublicUrl, getApp }) {
|
|
|
378
392
|
req.on('close', () => streams.delete(res));
|
|
379
393
|
return;
|
|
380
394
|
}
|
|
395
|
+
// Afegir un servidor per SSH: el progrés va en línies JSON (una per pas) i l'última és el resultat.
|
|
396
|
+
if (url.pathname === '/api/ssh/add' && req.method === 'POST') {
|
|
397
|
+
res.writeHead(200, { 'content-type': 'application/x-ndjson; charset=utf-8', 'cache-control': 'no-store', 'x-accel-buffering': 'no' });
|
|
398
|
+
const send = (o) => res.write(JSON.stringify(o) + '\n');
|
|
399
|
+
try {
|
|
400
|
+
const b = await readBody(req);
|
|
401
|
+
send({ done: true, ...(await addServer(String(b.target || '').trim(), { onStep: (step) => send({ step }) })) });
|
|
402
|
+
} catch (e) {
|
|
403
|
+
send({ error: e.message });
|
|
404
|
+
}
|
|
405
|
+
return res.end();
|
|
406
|
+
}
|
|
381
407
|
try {
|
|
382
408
|
const out = await api(req, res, url);
|
|
383
409
|
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
package/src/ssh.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Afegeix un servidor (VPS) per SSH: hi entra, hi engega nio amb un túnel de Cloudflare i torna l'adreça i la clau.
|
|
3
|
+
// El servidor queda connectat directament al mòbil i al panell; aquest ordinador no ha de continuar enmig.
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const TARGET = /^[A-Za-z0-9_][\w.@:\-\[\]]*$/;
|
|
10
|
+
const shq = (v) => `'${String(v).replace(/'/g, `'\\''`)}'`;
|
|
11
|
+
|
|
12
|
+
function remoteScript(port, dir) {
|
|
13
|
+
return `set -e
|
|
14
|
+
export PATH="$HOME/.nio/bin:$HOME/.local/bin:$PATH"
|
|
15
|
+
echo "NIO_STEP=Entrant al servidor"
|
|
16
|
+
command -v node >/dev/null 2>&1 || { echo NIO_NO_NODE; exit 0; }
|
|
17
|
+
up() { (exec 3<>/dev/tcp/127.0.0.1/${port}) 2>/dev/null; }
|
|
18
|
+
mkdir -p "$HOME/.nio/bin"
|
|
19
|
+
if ! command -v cloudflared >/dev/null 2>&1 && [ "$(uname -s)" = Linux ]; then
|
|
20
|
+
echo "NIO_STEP=Instal·lant cloudflared"
|
|
21
|
+
case "$(uname -m)" in x86_64) A=amd64;; aarch64|arm64) A=arm64;; *) A=;; esac
|
|
22
|
+
if [ -n "$A" ]; then
|
|
23
|
+
U="https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-$A"
|
|
24
|
+
{ curl -fsSL -o "$HOME/.nio/bin/cloudflared" "$U" 2>/dev/null || wget -qO "$HOME/.nio/bin/cloudflared" "$U" 2>/dev/null; } && chmod +x "$HOME/.nio/bin/cloudflared" || rm -f "$HOME/.nio/bin/cloudflared"
|
|
25
|
+
fi
|
|
26
|
+
fi
|
|
27
|
+
if ! up; then
|
|
28
|
+
echo "NIO_STEP=Engegant nio"
|
|
29
|
+
node -e "const f=require('os').homedir()+'/.nio/config.json',fs=require('fs');try{const c=JSON.parse(fs.readFileSync(f));delete c.lastUrl;fs.writeFileSync(f,JSON.stringify(c,null,2))}catch{}"
|
|
30
|
+
if command -v setsid >/dev/null 2>&1; then S=setsid; else S=; fi
|
|
31
|
+
$S nohup npx --yes nio-cat@latest --no-open --port ${port} ${dir ? shq(dir) : '"$HOME"'} > "$HOME/.nio/daemon.log" 2>&1 < /dev/null &
|
|
32
|
+
for i in $(seq 1 80); do up && break; sleep 0.5; done
|
|
33
|
+
up || { echo NIO_NO_START; tail -n 5 "$HOME/.nio/daemon.log" 2>/dev/null; exit 0; }
|
|
34
|
+
fi
|
|
35
|
+
echo "NIO_STEP=Obrint el túnel"
|
|
36
|
+
URL=
|
|
37
|
+
for i in $(seq 1 60); do
|
|
38
|
+
URL=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync(require('os').homedir()+'/.nio/config.json')).lastUrl||'')}catch{}")
|
|
39
|
+
[ -n "$URL" ] && break
|
|
40
|
+
sleep 1
|
|
41
|
+
done
|
|
42
|
+
[ -n "$URL" ] || { echo NIO_NO_TUNNEL; exit 0; }
|
|
43
|
+
node -e "console.log('NIO_TOKEN='+JSON.parse(require('fs').readFileSync(require('os').homedir()+'/.nio/config.json')).token)"
|
|
44
|
+
echo "NIO_URL=$URL"
|
|
45
|
+
echo "NIO_HOST=$(hostname)"
|
|
46
|
+
if command -v claude >/dev/null 2>&1; then echo NIO_CLAUDE=1; else echo NIO_CLAUDE=0; fi
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// batch: sense preguntes (des del panell). Sense batch (terminal), ssh pot demanar la contrasenya.
|
|
51
|
+
function addServer(target, { port = 7717, dir, batch = true, onStep = () => {} } = {}) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
if (!TARGET.test(String(target || ''))) return reject(new Error('Escriu l’usuari i el servidor, per exemple root@el-meu-servidor.com'));
|
|
54
|
+
const args = ['-o', 'ConnectTimeout=12', '-o', 'StrictHostKeyChecking=accept-new', ...(batch ? ['-o', 'BatchMode=yes'] : []), '-T', '--', target, 'bash -l -s'];
|
|
55
|
+
let p;
|
|
56
|
+
try { p = spawn('ssh', args, { stdio: ['pipe', 'pipe', batch ? 'pipe' : 'inherit'] }); } catch { return reject(new Error('No trobo l’ordre ssh en aquest ordinador')); }
|
|
57
|
+
let buf = '', err = '', done = false;
|
|
58
|
+
const out = {};
|
|
59
|
+
const line = (l) => {
|
|
60
|
+
const m = l.match(/^NIO_(STEP|TOKEN|URL|HOST|CLAUDE)=(.*)$/);
|
|
61
|
+
if (!m) return;
|
|
62
|
+
if (m[1] === 'STEP') onStep(m[2]);
|
|
63
|
+
else out[m[1].toLowerCase()] = m[2].trim();
|
|
64
|
+
};
|
|
65
|
+
p.stdout.on('data', (d) => {
|
|
66
|
+
buf += d;
|
|
67
|
+
let i;
|
|
68
|
+
while ((i = buf.indexOf('\n')) >= 0) { line(buf.slice(0, i)); buf = buf.slice(i + 1); }
|
|
69
|
+
if (/NIO_NO_(NODE|START|TUNNEL)/.test(buf + '')) out.flag = (buf.match(/NIO_NO_\w+/) || [])[0];
|
|
70
|
+
});
|
|
71
|
+
if (batch) p.stderr.on('data', (d) => (err += d));
|
|
72
|
+
const fail = (m) => { if (!done) { done = true; reject(new Error(m)); } };
|
|
73
|
+
p.on('error', () => fail('No trobo l’ordre ssh en aquest ordinador'));
|
|
74
|
+
p.on('close', (code) => {
|
|
75
|
+
if (buf) line(buf);
|
|
76
|
+
if (done) return;
|
|
77
|
+
done = true;
|
|
78
|
+
const text = `${err}\n${buf}`;
|
|
79
|
+
if (/NIO_NO_NODE/.test(text)) return reject(new Error('Al servidor falta Node.js 18 o més nou. Instal·la’l i torna-ho a provar.'));
|
|
80
|
+
if (/NIO_NO_START/.test(text)) return reject(new Error('nio no s’ha pogut engegar al servidor. Mira ~/.nio/daemon.log allà.'));
|
|
81
|
+
if (/NIO_NO_TUNNEL/.test(text)) return reject(new Error('El túnel no s’ha obert. Al servidor cal Linux amb curl o wget (per instal·lar cloudflared), o una adreça pròpia amb --url.'));
|
|
82
|
+
if (out.token && out.url) return resolve({ url: out.url, token: out.token, host: out.host || target, claude: out.claude === '1' });
|
|
83
|
+
if (/Permission denied|publickey/i.test(err)) return reject(new Error(`No puc entrar sense contrasenya. Al terminal d’aquest ordinador executa: ssh-copy-id ${target}`));
|
|
84
|
+
if (/Could not resolve|Connection (refused|timed out)|No route|timed out/i.test(err)) return reject(new Error('No arribo al servidor. Comprova l’adreça i que el SSH hi està obert.'));
|
|
85
|
+
reject(new Error((err.trim().split('\n').pop() || 'No he pogut afegir el servidor') + (code ? ` (ssh ${code})` : '')));
|
|
86
|
+
});
|
|
87
|
+
p.stdin.end(remoteScript(port, dir));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Servidors que ja tens a ~/.ssh/config (sense comodins), perquè el panell els ofereixi i no calgui escriure'ls.
|
|
92
|
+
function knownHosts() {
|
|
93
|
+
let txt = '';
|
|
94
|
+
try { txt = fs.readFileSync(path.join(os.homedir(), '.ssh', 'config'), 'utf8'); } catch { return []; }
|
|
95
|
+
const out = [];
|
|
96
|
+
let cur = null;
|
|
97
|
+
for (const raw of txt.split('\n')) {
|
|
98
|
+
const l = raw.trim();
|
|
99
|
+
const h = l.match(/^Host\s+(.+)$/i);
|
|
100
|
+
if (h) {
|
|
101
|
+
cur = null;
|
|
102
|
+
for (const name of h[1].split(/\s+/)) if (TARGET.test(name) && !/[*?!]/.test(name)) { cur = { target: name, host: '' }; out.push(cur); }
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const hn = l.match(/^HostName\s+(\S+)/i);
|
|
106
|
+
if (hn && cur) cur.host = hn[1];
|
|
107
|
+
}
|
|
108
|
+
return out.slice(0, 30);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { addServer, remoteScript, knownHosts, TARGET };
|