nio-cat 0.3.1 → 0.3.2

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 CHANGED
@@ -11,7 +11,7 @@ const { Hub } = require('../src/hub');
11
11
  const { createServer } = require('../src/server');
12
12
  const { detectAll } = require('../src/agents');
13
13
  const { startTunnel } = require('../src/tunnel');
14
- const { pairLink, terminalQr, writePairPage, makeCode } = require('../src/pair');
14
+ const { pairLink, terminalQr, writePairPage, makeCode, codeLink, terminalQrSize } = require('../src/pair');
15
15
  const { which } = require('../src/proc');
16
16
 
17
17
  const argv = process.argv.slice(2);
@@ -82,10 +82,20 @@ function openDashboard() {
82
82
  }
83
83
 
84
84
  async function showPair(publicUrl) {
85
- const link = pairLink(flags.app || cfg.app, publicUrl, cfg.token), code = makeCode(publicUrl, cfg.token);
86
- const file = writePairPage(link, code);
87
- console.log(`\n ${c.b('Escaneja aquest codi amb el mòbil')} ${c.dim('(o obre ' + file + ')')}\n`);
88
- console.log(terminalQr(link));
85
+ const app = flags.app || cfg.app;
86
+ const link = pairLink(app, publicUrl, cfg.token), code = makeCode(publicUrl, cfg.token), qrLink = codeLink(app, code);
87
+ const file = writePairPage(link, code, qrLink);
88
+ // El QR ha de cabre al terminal: marge normal, marge d'1 mòdul i, si no hi cap, la pàgina del navegador.
89
+ const rows = process.stdout.rows || 999, cols = process.stdout.columns || 999;
90
+ const fits = (m) => { const z = terminalQrSize(qrLink, m); return z.rows + 9 <= rows && z.cols + 2 <= cols; };
91
+ const margin = fits(2) ? 2 : fits(1) ? 1 : 0;
92
+ if (margin) {
93
+ console.log(`\n ${c.b('Escaneja aquest codi amb el mòbil')}\n`);
94
+ console.log(terminalQr(qrLink, { margin }));
95
+ } else {
96
+ console.log(`\n ${c.warn('El terminal és massa petit per al QR.')} Fes-lo més gran o obre ${file}`);
97
+ if (canOpenBrowser()) openUrl(file);
98
+ }
89
99
  console.log(`\n ${c.b('Codi:')} ${code}`);
90
100
  console.log(c.dim(' Copia\'l i enganxa\'l a Sessions → Tria un ordinador.'));
91
101
  console.log(c.dim(' Aquest codi dona accés als teus agents. Guarda\'l com una contrasenya.\n'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nio-cat",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "nio: fes servir Claude Code, Codex, Grok, Antigravity i Ollama del teu ordinador des del mòbil.",
5
5
  "license": "MIT",
6
6
  "author": "f.cat",
package/src/pair.js CHANGED
@@ -16,13 +16,17 @@ function pairLink(app, url, token) {
16
16
  // La web ja sap posar-hi l'enllaç de davant. Les adreces de Cloudflare es comprimeixen (≈ 60 caràcters).
17
17
  const CF = '.trycloudflare.com';
18
18
  function makeCode(url, token) {
19
- const raw = Buffer.from(token, 'base64url');
19
+ const raw = Buffer.from(token, 'base64url'), tok24 = raw.length === 24 && raw.toString('base64url') === token;
20
20
  const sub = url.startsWith('https://') && url.endsWith(CF) ? url.slice(8, -CF.length) : null;
21
- const bytes = raw.length === 24 && raw.toString('base64url') === token && sub && /^[\w-]+$/.test(sub)
22
- ? Buffer.concat([Buffer.from([1]), raw, Buffer.from(sub)])
23
- : Buffer.concat([Buffer.from([0]), Buffer.from(`${url}\n${token}`)]);
21
+ const lan = url.match(/^http:\/\/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}):(\d{1,5})$/);
22
+ let bytes;
23
+ if (tok24 && sub && /^[\w-]+$/.test(sub)) bytes = Buffer.concat([Buffer.from([1]), raw, Buffer.from(sub)]);
24
+ else if (tok24 && lan && lan.slice(1, 5).every((n) => +n < 256) && +lan[5] < 65536) bytes = Buffer.concat([Buffer.from([2]), raw, Buffer.from(lan.slice(1, 5).map(Number)), Buffer.from([+lan[5] >> 8, +lan[5] & 255])]);
25
+ else bytes = Buffer.concat([Buffer.from([0]), Buffer.from(`${url}\n${token}`)]);
24
26
  return `nio-${bytes.toString('base64url')}`;
25
27
  }
28
+ // Enllaç curt per al QR: una adreça de l'app amb el codi al fragment. Un QR més petit cap millor al terminal.
29
+ const codeLink = (app, code) => `${app.replace(/\/$/, '')}/app#${code}`;
26
30
 
27
31
  function qr(text) {
28
32
  const q = qrcode(0, 'L');
@@ -33,10 +37,10 @@ function qr(text) {
33
37
 
34
38
  // QR per al terminal: cada mòdul són dos espais amb color de fons (negre o blanc). No depèn de cap glif,
35
39
  // així que no es talla amb tipus de lletra o interlineats estranys. Sempre negre sobre blanc, amb tema clar o fosc.
36
- function terminalQr(text, { color = !process.env.NO_COLOR } = {}) {
40
+ function terminalQr(text, { color = !process.env.NO_COLOR, margin = 2 } = {}) {
37
41
  const q = qr(text);
38
42
  const n = q.getModuleCount();
39
- const M = 2; // marge blanc al voltant, en mòduls
43
+ const M = margin; // marge blanc al voltant, en mòduls
40
44
  const dark = (r, c) => r >= 0 && c >= 0 && r < n && c < n && q.isDark(r, c);
41
45
  const lines = [];
42
46
  for (let r = -M; r < n + M; r++) {
@@ -72,8 +76,8 @@ function qrSvg(text) {
72
76
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" shape-rendering="crispEdges" role="img" aria-label="Codi QR"><rect width="${size}" height="${size}" fill="#fff"/><path d="${d}" fill="#000"/></svg>`;
73
77
  }
74
78
 
75
- function writePairPage(link, code) {
76
- const svg = qrSvg(link);
79
+ function writePairPage(link, code, qrLink) {
80
+ const svg = qrSvg(qrLink || link);
77
81
  const html = `<!doctype html><html lang="ca"><head><meta charset="utf-8">
78
82
  <meta name="viewport" content="width=device-width,initial-scale=1"><title>nio // connecta el mòbil</title>
79
83
  <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600&display=swap" rel="stylesheet">
@@ -88,4 +92,11 @@ code{display:block;word-break:break-all;font:12px/1.5 ui-monospace,Menlo,monospa
88
92
  return file;
89
93
  }
90
94
 
91
- module.exports = { pairLink, terminalQr, writePairPage, qrSvg, makeCode };
95
+ module.exports = { pairLink, terminalQr, writePairPage, qrSvg, makeCode, codeLink };
96
+
97
+ // Mida del QR al terminal: files i columnes que ocupa amb un marge donat.
98
+ function terminalQrSize(text, margin = 2) {
99
+ const n = qr(text).getModuleCount() + margin * 2;
100
+ return { rows: n, cols: n * 2 + 2 };
101
+ }
102
+ module.exports.terminalQrSize = terminalQrSize;
package/src/server.js CHANGED
@@ -10,7 +10,7 @@ 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 { pairLink, qrSvg, makeCode } = require('./pair');
13
+ const { pairLink, qrSvg, makeCode, codeLink } = require('./pair');
14
14
 
15
15
  // L'app web: copiada dins del paquet en publicar-lo, o l'arrel del repositori en desenvolupament.
16
16
  const WEB_DIRS = [path.join(__dirname, '..', 'web'), path.join(__dirname, '..', '..')];
@@ -232,7 +232,8 @@ function createServer({ cfg, hub, getPublicUrl, getApp }) {
232
232
  const pub = getPublicUrl();
233
233
  if (!pub || /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:|\/|$)/i.test(pub)) return { link: null };
234
234
  const link = pairLink(getApp ? getApp() : cfg.app, pub, cfg.token);
235
- return { link, code: makeCode(pub, cfg.token), svg: qrSvg(link), url: pub, lan: /^http:\/\//.test(pub) };
235
+ const code = makeCode(pub, cfg.token);
236
+ return { link, code, svg: qrSvg(codeLink(getApp ? getApp() : cfg.app, code)), url: pub, lan: /^http:\/\//.test(pub) };
236
237
  }
237
238
  if (p === '/api/github' && method === 'GET') return github.status();
238
239
  if (p === '/api/github/repos' && method === 'GET') return github.repos(url.searchParams.get('owner') || '');
package/web/app/app.js CHANGED
@@ -194,10 +194,12 @@ function parsePair(text) {
194
194
  // Codi «nio-…»: porta l'adreça i la clau dins (vegeu makeCode al programa). Es descodifica aquí, sense servidor.
195
195
  const CF = '.trycloudflare.com';
196
196
  function parseCode(text) {
197
- const m = String(text || '').replace(/\s+/g, '').match(/^nio-([\w-]+)$/i); if (!m) return null;
197
+ const m = String(text || '').replace(/\s+/g, '').match(/(?:^|#)nio-([\w-]+)$/i); if (!m) return null;
198
198
  try {
199
199
  const bin = atob(m[1].replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (m[1].length % 4)) % 4)), u = Uint8Array.from(bin, (c) => c.charCodeAt(0));
200
- if (u[0] === 1) return { url: `https://${new TextDecoder().decode(u.slice(25))}${CF}`, token: btoa(String.fromCharCode(...u.slice(1, 25))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') };
200
+ const tok = () => btoa(String.fromCharCode(...u.slice(1, 25))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
201
+ if (u[0] === 1) return { url: `https://${new TextDecoder().decode(u.slice(25))}${CF}`, token: tok() };
202
+ if (u[0] === 2) return { url: `http://${[...u.slice(25, 29)].join('.')}:${(u[29] << 8) | u[30]}`, token: tok() };
201
203
  if (u[0] === 0) { const [url, token] = new TextDecoder().decode(u.slice(1)).split('\n'); if (url && token) return { url: url.replace(/\/$/, ''), token }; }
202
204
  } catch { /* codi malmès */ }
203
205
  return null;
@@ -212,7 +214,7 @@ function addMachine({ url, token, name }) {
212
214
  connect();
213
215
  }
214
216
  function checkPairHash() {
215
- const pair = parsePair(location.hash); if (!pair) return false;
217
+ const pair = parsePair(location.hash) || parseCode(location.hash); if (!pair) return false;
216
218
  history.replaceState(null, '', location.pathname + '#/sessions');
217
219
  state.route = { name: 'sessions' };
218
220
  addMachine(pair); render(); toast('Ordinador connectat'); return true;