nio-cat 0.9.14 → 0.9.15
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/package.json +1 -1
- package/src/agents/claude.js +6 -2
- package/src/control.js +5 -3
- package/src/hub.js +2 -1
- package/src/mcp.js +4 -3
- package/src/notes.js +4 -3
- package/src/proc.js +61 -12
- package/src/service.js +51 -4
- package/web/app/app.js +10 -5
- package/web/index.html +17 -1
- package/web/sw.js +1 -1
package/package.json
CHANGED
package/src/agents/claude.js
CHANGED
|
@@ -5,6 +5,10 @@ const path = require('path');
|
|
|
5
5
|
const { run, tryJson } = require('../proc');
|
|
6
6
|
|
|
7
7
|
const HOOK = path.join(__dirname, '..', 'claude-hook.js');
|
|
8
|
+
// L'ordre del hook la llança la shell de Claude Code (a Windows, Git Bash): el Node d'aquest mateix procés i
|
|
9
|
+
// barres «/», que entenen totes les shells.
|
|
10
|
+
const slash = (p) => (process.platform === 'win32' ? p.replace(/\\/g, '/') : p);
|
|
11
|
+
const HOOK_CMD = `"${slash(process.execPath)}" "${slash(HOOK)}"`;
|
|
8
12
|
|
|
9
13
|
module.exports = {
|
|
10
14
|
id: 'claude',
|
|
@@ -30,7 +34,7 @@ module.exports = {
|
|
|
30
34
|
const env = {};
|
|
31
35
|
const gate = (extra = {}) => {
|
|
32
36
|
// Hook PreToolUse: envia al mòbil el que cal aprovar.
|
|
33
|
-
args.push('--settings', JSON.stringify({ hooks: { PreToolUse: [{ matcher: '*', hooks: [{ type: 'command', command:
|
|
37
|
+
args.push('--settings', JSON.stringify({ hooks: { PreToolUse: [{ matcher: '*', hooks: [{ type: 'command', command: HOOK_CMD, timeout: 900 }] }] } }));
|
|
34
38
|
Object.assign(env, { NIO_HOOK_URL: ctx.hook.url, NIO_HOOK_KEY: ctx.hook.key, NIO_SESSION: session.id }, extra);
|
|
35
39
|
};
|
|
36
40
|
const control = cfg.control?.navegador || cfg.control?.escriptori;
|
|
@@ -47,7 +51,7 @@ module.exports = {
|
|
|
47
51
|
PreToolUse: [
|
|
48
52
|
{
|
|
49
53
|
matcher: '*',
|
|
50
|
-
hooks: [{ type: 'command', command:
|
|
54
|
+
hooks: [{ type: 'command', command: HOOK_CMD, timeout: 900 }],
|
|
51
55
|
},
|
|
52
56
|
],
|
|
53
57
|
},
|
package/src/control.js
CHANGED
|
@@ -10,7 +10,7 @@ const os = require('os');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const crypto = require('crypto');
|
|
12
12
|
const { spawn } = require('child_process');
|
|
13
|
-
const { which, childEnv } = require('./proc');
|
|
13
|
+
const { which, childEnv, command, killTree } = require('./proc');
|
|
14
14
|
const config = require('./config');
|
|
15
15
|
const { HOME } = config;
|
|
16
16
|
|
|
@@ -61,11 +61,13 @@ function run(bin, args, timeout = 15000) {
|
|
|
61
61
|
resolve({ ok, out, err: err || why || '', timedOut: why === 'temps' });
|
|
62
62
|
};
|
|
63
63
|
const timer = setTimeout(() => {
|
|
64
|
-
|
|
64
|
+
if (process.platform === 'win32') killTree(child?.pid);
|
|
65
|
+
else try { process.kill(-child.pid, 'SIGKILL'); } catch { try { child.kill('SIGKILL'); } catch { /* ja no hi és */ } }
|
|
65
66
|
finish(false, 'temps');
|
|
66
67
|
}, timeout);
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
+
const c = command(bin, args);
|
|
70
|
+
child = spawn(c.cmd, c.args, { env: childEnv(), stdio: ['ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32', windowsHide: true, ...c.opts });
|
|
69
71
|
} catch (e) { return finish(false, e.message); }
|
|
70
72
|
child.stdout.on('data', (d) => { if (out.length < 8 << 20) out += d; });
|
|
71
73
|
child.stderr.on('data', (d) => { if (err.length < 1 << 20) err += d; });
|
package/src/hub.js
CHANGED
|
@@ -194,7 +194,8 @@ class Hub extends EventEmitter {
|
|
|
194
194
|
codexStatus(s, bp) {
|
|
195
195
|
const { execFile } = require('child_process');
|
|
196
196
|
const codex = s.agent === 'codex', name = byId[s.agent]?.name || s.agent;
|
|
197
|
-
|
|
197
|
+
const c = require('./proc').command(bp, codex ? ['login', 'status'] : ['--version']);
|
|
198
|
+
execFile(c.cmd, c.args, { timeout: 8000, windowsHide: true, ...c.opts }, (err, out, errOut) => {
|
|
198
199
|
const rows = String(out || errOut || '').split('\n').map((l) => l.trim()).filter(Boolean), login = rows.find((l) => /logged|log in|login/i.test(l) && !/^warning/i.test(l)) || rows[0] || (err ? 'No he pogut llegir el compte' : '');
|
|
199
200
|
const lines = [codex ? `**Compte:** ${login}` : `**Versió:** ${login}`, `**Model:** ${s.model || s.modelUsed || 'el de per defecte'}`, `**Carpeta:** \`${s.cwd}\``, `**Permisos:** ${s.mode}`, `**Conversa:** ${s.nativeId ? `continua la de ${name}` : 'encara no ha començat'}`];
|
|
200
201
|
this.push(s, 'text', { text: lines.join('\n\n') });
|
package/src/mcp.js
CHANGED
|
@@ -122,10 +122,11 @@ function command() {
|
|
|
122
122
|
function registerClaude(bin, { force } = {}) {
|
|
123
123
|
if (!bin) return 'Claude Code no hi és';
|
|
124
124
|
const { cmd, args } = command();
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
const sync = (args, o) => { const c = require('./proc').command(bin, args); return spawnSync(c.cmd, c.args, { windowsHide: true, ...o, ...c.opts }); };
|
|
126
|
+
if (!force && sync(['mcp', 'get', 'nio'], { encoding: 'utf8', timeout: 15000 }).status === 0) return 'ja hi era';
|
|
127
|
+
sync(['mcp', 'remove', '--scope', 'user', 'nio'], { timeout: 15000 });
|
|
127
128
|
const env = process.env.NIO_HOME ? ['-e', `NIO_HOME=${process.env.NIO_HOME}`] : [];
|
|
128
|
-
const r =
|
|
129
|
+
const r = sync(['mcp', 'add', 'nio', '--scope', 'user', ...env, '--', cmd, ...args], { encoding: 'utf8', timeout: 15000 });
|
|
129
130
|
return r.status === 0 ? 'afegit' : `no s’ha pogut afegir: ${(r.stderr || r.stdout || '').trim().split('\n').pop()}`;
|
|
130
131
|
}
|
|
131
132
|
|
package/src/notes.js
CHANGED
|
@@ -8,7 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const crypto = require('crypto');
|
|
9
9
|
const { spawn } = require('child_process');
|
|
10
10
|
const { HOME } = require('./config');
|
|
11
|
-
const { childEnv } = require('./proc');
|
|
11
|
+
const { childEnv, command, killTree } = require('./proc');
|
|
12
12
|
const voice = require('./voice');
|
|
13
13
|
const ollama = require('./agents/ollama');
|
|
14
14
|
|
|
@@ -470,9 +470,10 @@ function parseJson(raw) {
|
|
|
470
470
|
// ---------- Execució d'un model, d'un sol cop
|
|
471
471
|
function runCli(bin, args, input, { cwd, timeout = 180000 } = {}) {
|
|
472
472
|
return new Promise((resolve, reject) => {
|
|
473
|
-
const
|
|
473
|
+
const c = command(bin, args);
|
|
474
|
+
const p = spawn(c.cmd, c.args, { cwd, env: childEnv(), stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, ...c.opts });
|
|
474
475
|
let out = '', err = '';
|
|
475
|
-
const t = setTimeout(() => { p.kill('SIGKILL'); reject(httpErr(504, 'El model ha trigat massa. Prova-ho amb un de més petit o ràpid.')); }, timeout);
|
|
476
|
+
const t = setTimeout(() => { if (process.platform === 'win32') killTree(p.pid); else p.kill('SIGKILL'); reject(httpErr(504, 'El model ha trigat massa. Prova-ho amb un de més petit o ràpid.')); }, timeout);
|
|
476
477
|
p.stdout.on('data', (d) => { out += d; });
|
|
477
478
|
p.stderr.on('data', (d) => { err += d; });
|
|
478
479
|
p.on('error', (e) => { clearTimeout(t); reject(httpErr(500, `No he pogut engegar ${path.basename(bin)}: ${e.message}`)); });
|
package/src/proc.js
CHANGED
|
@@ -5,27 +5,28 @@ const os = require('os');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { spawn, execFile } = require('child_process');
|
|
7
7
|
|
|
8
|
+
const WIN = process.platform === 'win32';
|
|
8
9
|
const EXTRA_DIRS = [
|
|
9
10
|
path.join(os.homedir(), '.nio', 'bin'),
|
|
11
|
+
// Windows: npm -g, l'instal·lador natiu de Claude Code i winget.
|
|
12
|
+
...(WIN ? [process.env.APPDATA && path.join(process.env.APPDATA, 'npm'), process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Microsoft', 'WinGet', 'Links'), process.env.ProgramFiles && path.join(process.env.ProgramFiles, 'nodejs')].filter(Boolean) : []),
|
|
10
13
|
path.join(os.homedir(), '.local', 'bin'),
|
|
11
14
|
path.join(os.homedir(), '.npm-global', 'bin'),
|
|
12
15
|
path.join(os.homedir(), '.bun', 'bin'),
|
|
13
16
|
path.join(os.homedir(), '.grok', 'bin'),
|
|
14
|
-
'/opt/homebrew/bin',
|
|
15
|
-
'/usr/local/bin',
|
|
16
|
-
'/usr/bin',
|
|
17
|
+
...(WIN ? [] : ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin']),
|
|
17
18
|
];
|
|
18
19
|
|
|
19
20
|
function searchPath() {
|
|
20
|
-
const parts = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
21
|
-
for (const d of EXTRA_DIRS) if (!parts.
|
|
21
|
+
const parts = (process.env.PATH || process.env.Path || '').split(path.delimiter).filter(Boolean);
|
|
22
|
+
for (const d of EXTRA_DIRS) if (!parts.some((x) => (WIN ? x.toLowerCase() === d.toLowerCase() : x === d))) parts.push(d);
|
|
22
23
|
return parts;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
function which(bin) {
|
|
26
27
|
if (!bin) return null;
|
|
27
28
|
if (bin.includes('/') || bin.includes('\\')) return fs.existsSync(bin) ? bin : null;
|
|
28
|
-
const exts =
|
|
29
|
+
const exts = WIN ? ['.exe', '.cmd', '.bat'] : [''];
|
|
29
30
|
for (const dir of searchPath()) {
|
|
30
31
|
for (const ext of exts) {
|
|
31
32
|
const p = path.join(dir, bin + ext);
|
|
@@ -40,9 +41,44 @@ function which(bin) {
|
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// Windows: els CLI de npm (claude.cmd, codex.cmd…) són scripts .cmd, i Node no els executa sense una shell
|
|
45
|
+
// (i passar-hi el text de l'usuari per cmd.exe seria perillós). Es llegeix l'script i s'executa directament
|
|
46
|
+
// el que hi ha a dins: «node …\cli.js» o un .exe. Si no s'entén, cmd.exe amb els arguments escapats.
|
|
47
|
+
const shimCache = new Map();
|
|
48
|
+
function command(bin, args = []) {
|
|
49
|
+
if (!WIN || !/\.(cmd|bat)$/i.test(bin)) return { cmd: bin, args, opts: {} };
|
|
50
|
+
let target = shimCache.get(bin);
|
|
51
|
+
if (target === undefined) {
|
|
52
|
+
target = null;
|
|
53
|
+
try {
|
|
54
|
+
const txt = fs.readFileSync(bin, 'utf8');
|
|
55
|
+
// El primer «"%dp0%\…"» que existeixi, sense comptar el node.exe opcional de la mateixa carpeta.
|
|
56
|
+
for (const m of txt.matchAll(/"%~?dp0%?\\?([^"%]+?\.(?:js|cjs|mjs|exe))"/gi)) {
|
|
57
|
+
const p = path.join(path.dirname(bin), m[1]);
|
|
58
|
+
if (!/^node\.exe$/i.test(m[1]) && fs.existsSync(p)) { target = p; break; }
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
/* no es pot llegir */
|
|
62
|
+
}
|
|
63
|
+
shimCache.set(bin, target);
|
|
64
|
+
}
|
|
65
|
+
if (target && /\.exe$/i.test(target)) return { cmd: target, args, opts: {} };
|
|
66
|
+
if (target) return { cmd: process.execPath, args: [target, ...args], opts: {} };
|
|
67
|
+
const line = [bin, ...args].map(cmdQuote).join(' ');
|
|
68
|
+
return { cmd: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', `"${line}"`], opts: { windowsVerbatimArguments: true } };
|
|
69
|
+
}
|
|
70
|
+
// Cometes per a cmd.exe: les de CreateProcess i, a sobre, ^ davant dels caràcters especials de cmd.
|
|
71
|
+
function cmdQuote(a) {
|
|
72
|
+
let s = String(a);
|
|
73
|
+
if (s && !/[\s"^&|<>%!()]/.test(s)) return s;
|
|
74
|
+
s = `"${s.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
75
|
+
return s.replace(/[\^&|<>%!()"]/g, '^$&');
|
|
76
|
+
}
|
|
77
|
+
|
|
43
78
|
function version(binPath, args = ['--version']) {
|
|
44
79
|
return new Promise((resolve) => {
|
|
45
|
-
|
|
80
|
+
const c = command(binPath, args);
|
|
81
|
+
execFile(c.cmd, c.args, { timeout: 4000, env: childEnv(), windowsHide: true, ...c.opts }, (err, stdout, stderr) => {
|
|
46
82
|
const out = String(stdout || stderr || '').trim().split('\n')[0];
|
|
47
83
|
resolve(err && !out ? null : out.slice(0, 60));
|
|
48
84
|
});
|
|
@@ -75,11 +111,14 @@ function run({ bin, args, cwd, env, stdin, onLine, onErr, onExit, tty }) {
|
|
|
75
111
|
argv = ['-qfec', [bin, ...args].map(shQuote).join(' '), '/dev/null'];
|
|
76
112
|
}
|
|
77
113
|
}
|
|
78
|
-
const
|
|
114
|
+
const c = command(cmd, argv);
|
|
115
|
+
const child = spawn(c.cmd, c.args, {
|
|
79
116
|
cwd,
|
|
80
117
|
env: childEnv(env),
|
|
81
118
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
82
|
-
detached:
|
|
119
|
+
detached: !WIN,
|
|
120
|
+
windowsHide: true,
|
|
121
|
+
...c.opts,
|
|
83
122
|
});
|
|
84
123
|
if (stdin != null) child.stdin.end(stdin);
|
|
85
124
|
else child.stdin.end();
|
|
@@ -109,8 +148,8 @@ function run({ bin, args, cwd, env, stdin, onLine, onErr, onExit, tty }) {
|
|
|
109
148
|
child,
|
|
110
149
|
kill() {
|
|
111
150
|
try {
|
|
112
|
-
if (
|
|
113
|
-
else child.
|
|
151
|
+
if (!WIN) process.kill(-child.pid, 'SIGTERM');
|
|
152
|
+
else killTree(child.pid);
|
|
114
153
|
} catch {
|
|
115
154
|
try {
|
|
116
155
|
child.kill();
|
|
@@ -122,6 +161,16 @@ function run({ bin, args, cwd, env, stdin, onLine, onErr, onExit, tty }) {
|
|
|
122
161
|
};
|
|
123
162
|
}
|
|
124
163
|
|
|
164
|
+
// Windows: atura el procés i tots els que ha obert (l'agent, les seves eines…).
|
|
165
|
+
function killTree(pid) {
|
|
166
|
+
if (!pid) return;
|
|
167
|
+
try {
|
|
168
|
+
spawn('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true }).on('error', () => {});
|
|
169
|
+
} catch {
|
|
170
|
+
/* ja ha acabat */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
125
174
|
function shQuote(s) {
|
|
126
175
|
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
127
176
|
}
|
|
@@ -141,4 +190,4 @@ function tryJson(line) {
|
|
|
141
190
|
}
|
|
142
191
|
}
|
|
143
192
|
|
|
144
|
-
module.exports = { which, version, run, childEnv, tryJson, stripAnsi, searchPath, shQuote };
|
|
193
|
+
module.exports = { which, version, run, childEnv, tryJson, stripAnsi, searchPath, shQuote, command, cmdQuote, killTree };
|
package/src/service.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// Servei del sistema perquè nio arrenqui sol amb l'ordinador (systemd a Linux, launchd a macOS
|
|
2
|
+
// Servei del sistema perquè nio arrenqui sol amb l'ordinador (systemd a Linux, launchd a macOS i, a Windows,
|
|
3
|
+
// un script a la carpeta Inici que l'engega amagat en iniciar sessió i el torna a obrir si es tanca).
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
const path = require('path');
|
|
@@ -66,8 +67,37 @@ ${args}
|
|
|
66
67
|
`;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
// Windows: no cal ser administrador. L'script fa de supervisor: engega nio sense finestra, n'escriu la sortida
|
|
71
|
+
// a ~/.nio/nio.log i, si es tanca, el torna a obrir al cap de 3 segons.
|
|
72
|
+
function windowsStartup() {
|
|
73
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', 'nio.vbs');
|
|
74
|
+
}
|
|
75
|
+
function windowsVbs({ roots }) {
|
|
76
|
+
const qv = (s) => String(s).replace(/"/g, '""'); // dins d'una cadena de VBScript
|
|
77
|
+
const arg = (s) => `"${s}"`;
|
|
78
|
+
// cmd /s /c "…": treu les cometes de fora i executa la resta tal qual (amb la redirecció al registre).
|
|
79
|
+
const line = qv(`cmd /d /s /c "${[process.execPath, entry(), '--no-open', ...roots].map(arg).join(' ')} >> ${arg(path.join(HOME, 'nio.log'))} 2>&1"`);
|
|
80
|
+
return `' nio · s'engega en iniciar sessió (nio service uninstall per treure-ho)
|
|
81
|
+
Set sh = CreateObject("WScript.Shell")
|
|
82
|
+
Set env = sh.Environment("PROCESS")
|
|
83
|
+
env("NO_COLOR") = "1"
|
|
84
|
+
env("PATH") = "${qv(searchPath().join(';'))}"
|
|
85
|
+
${process.env.NIO_HOME ? `env("NIO_HOME") = "${qv(process.env.NIO_HOME)}"
|
|
86
|
+
` : ''}sh.CurrentDirectory = "${qv(roots[0] || os.homedir())}"
|
|
87
|
+
Do
|
|
88
|
+
sh.Run "${line}", 0, True
|
|
89
|
+
WScript.Sleep 3000
|
|
90
|
+
Loop
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
// Atura el supervisor i nio (els processos que en porten la ruta a la línia d'ordres).
|
|
94
|
+
function windowsStop() {
|
|
95
|
+
const ps = `Get-CimInstance Win32_Process | Where-Object { ($_.Name -eq 'wscript.exe' -and $_.CommandLine -like '*nio.vbs*') -or ($_.Name -eq 'node.exe' -and $_.CommandLine -like '*${entry().replace(/'/g, "''")}*--no-open*') } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`;
|
|
96
|
+
sh('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps]);
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
function sh(cmd, args) {
|
|
70
|
-
const r = spawnSync(cmd, args, { encoding: 'utf8' });
|
|
100
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8', windowsHide: true });
|
|
71
101
|
return { ok: r.status === 0, out: `${r.stdout || ''}${r.stderr || ''}`.trim() };
|
|
72
102
|
}
|
|
73
103
|
|
|
@@ -85,7 +115,18 @@ function install({ roots, log = console.log }) {
|
|
|
85
115
|
if (!r.ok) throw new Error(`launchctl no l’ha pogut carregar: ${r.out}`);
|
|
86
116
|
return { file, logs: `tail -f ${path.join(HOME, 'nio.log')}` };
|
|
87
117
|
}
|
|
88
|
-
if (process.platform
|
|
118
|
+
if (process.platform === 'win32') {
|
|
119
|
+
const file = windowsStartup();
|
|
120
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
121
|
+
fs.mkdirSync(HOME, { recursive: true });
|
|
122
|
+
windowsStop();
|
|
123
|
+
fs.writeFileSync(file, windowsVbs({ roots }));
|
|
124
|
+
const child = require('child_process').spawn('wscript.exe', ['//B', '//Nologo', file], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
125
|
+
child.on('error', () => {});
|
|
126
|
+
child.unref();
|
|
127
|
+
return { file, logs: `Get-Content -Wait "${path.join(HOME, 'nio.log')}"` };
|
|
128
|
+
}
|
|
129
|
+
if (process.platform !== 'linux') throw new Error('El servei automàtic només funciona a Linux (systemd), macOS (launchd) i Windows.');
|
|
89
130
|
if (!sh('systemctl', ['--version']).ok) throw new Error('Aquest sistema no té systemd.');
|
|
90
131
|
const system = process.getuid && process.getuid() === 0;
|
|
91
132
|
const file = system ? '/etc/systemd/system/nio.service' : path.join(os.homedir(), '.config', 'systemd', 'user', 'nio.service');
|
|
@@ -105,6 +146,12 @@ function install({ roots, log = console.log }) {
|
|
|
105
146
|
}
|
|
106
147
|
|
|
107
148
|
function uninstall() {
|
|
149
|
+
if (process.platform === 'win32') {
|
|
150
|
+
const file = windowsStartup();
|
|
151
|
+
windowsStop();
|
|
152
|
+
fs.rmSync(file, { force: true });
|
|
153
|
+
return file;
|
|
154
|
+
}
|
|
108
155
|
if (process.platform === 'darwin') {
|
|
109
156
|
const file = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
110
157
|
sh('launchctl', ['unload', '-w', file]);
|
|
@@ -120,4 +167,4 @@ function uninstall() {
|
|
|
120
167
|
return file;
|
|
121
168
|
}
|
|
122
169
|
|
|
123
|
-
module.exports = { install, uninstall, systemdUnit, launchdPlist };
|
|
170
|
+
module.exports = { install, uninstall, systemdUnit, launchdPlist, windowsVbs };
|
package/web/app/app.js
CHANGED
|
@@ -670,7 +670,12 @@ function render() {
|
|
|
670
670
|
/* ---------- Ordinadors (llista, afegir i detall de l'actiu) */
|
|
671
671
|
const deviceId = () => { let d = LS.get('nio.device', ''); if (!d) { d = Math.random().toString(36).slice(2, 12); LS.set('nio.device', d); } return d; };
|
|
672
672
|
const isPhone = () => navigator.userAgentData?.mobile === true || /Android|iPhone|iPod|Mobile/i.test(navigator.userAgent) || (isTouch && matchMedia('(max-width: 1024px)').matches);
|
|
673
|
-
const
|
|
673
|
+
const SH_CMD = 'curl -fsSL https://www.nio.cat/install.sh | sh', WIN_CMD = 'irm https://www.nio.cat/install.ps1 | iex';
|
|
674
|
+
// En un PC amb Windows, l'ordre de PowerShell; a la resta (i al mòbil, que no sap quin ordinador tens), la de Mac i Linux.
|
|
675
|
+
const isWindows = !isPhone() && (navigator.userAgentData?.platform === 'Windows' || /Windows/.test(navigator.userAgent));
|
|
676
|
+
const INSTALL_CMD = isWindows ? WIN_CMD : SH_CMD;
|
|
677
|
+
const OPEN_TERM = isWindows ? 'Obre PowerShell: prem la tecla Windows i escriu «PowerShell».' : 'Obre el Terminal. Al Mac, prem ⌘ Espai i escriu «Terminal».';
|
|
678
|
+
const OTHER_OS = isWindows ? `Windows. A Mac o Linux: <span class="mono">${SH_CMD}</span>` : `Mac o Linux. A Windows, al PowerShell: <span class="mono">${WIN_CMD}</span>`;
|
|
674
679
|
const cmdBox = (c) => `<div class="cmd big"><code>${esc(c)}</code><button class="btn sm" data-copy="${esc(c)}" type="button">${svg(ICON.copy)}Copia</button></div>`;
|
|
675
680
|
// Estat de la resta d'ordinadors (el connectat ja té el seu flux en directe): una consulta lleugera.
|
|
676
681
|
const reach = new Map();
|
|
@@ -805,9 +810,9 @@ function viewComputers() {
|
|
|
805
810
|
if (!ms.length) {
|
|
806
811
|
const phone = isPhone();
|
|
807
812
|
const steps = phone
|
|
808
|
-
? [['Enganxa-la a l’ordinador', 'Al Terminal. Instal·la nio i el deixa engegat, també quan reinicies.'], ['Escaneja el QR', 'Surt al navegador de l’ordinador quan acaba. També hi ha un codi.'], ['Dona feina a un agent', 'Tria l’agent i la carpeta, i mira com treballa des d’aquí.']]
|
|
809
|
-
: [['Obre el Terminal', 'Al Mac, ⌘ Espai i escriu «Terminal». A Linux, Ctrl Alt T.'], ['Enganxa l’ordre', 'Instal·la nio i el deixa engegat, també quan reinicies.'], ['Escaneja el QR', 'Aquesta web es connecta sola i et mostra el QR del mòbil.']];
|
|
810
|
-
return `${head}<div class="panel empty first-run">${bot('idle')}<h2>Connecta el teu primer ordinador</h2><p>${phone ? 'nio s’instal·la a l’ordinador amb una sola ordre. Copia-la i enganxa-la allà.' : 'Una sola ordre instal·la nio en aquest ordinador i ho deixa tot a punt.'}</p>${cmdBox(INSTALL_CMD)}<ol class="stepper">${steps.map(([t, d], i) => `<li><span class="n">${i + 1}</span><div><b>${t}</b><p>${d}</p></div></li>`).join('')}</ol><div class="row" style="justify-content:center"><button class="btn ink" data-act="add-computer" type="button">${svg(ICON.plus)}${phone ? 'Tinc un codi o un QR' : 'Afegeix ordinador'}</button><button class="btn" data-act="demo" type="button">Prova una demo</button></div><p class="muted small"
|
|
813
|
+
? [['Enganxa-la a l’ordinador', 'Al Terminal (a Windows, a PowerShell). Instal·la nio i el deixa engegat, també quan reinicies.'], ['Escaneja el QR', 'Surt al navegador de l’ordinador quan acaba. També hi ha un codi.'], ['Dona feina a un agent', 'Tria l’agent i la carpeta, i mira com treballa des d’aquí.']]
|
|
814
|
+
: [isWindows ? ['Obre PowerShell', 'Prem la tecla Windows i escriu «PowerShell».'] : ['Obre el Terminal', 'Al Mac, ⌘ Espai i escriu «Terminal». A Linux, Ctrl Alt T.'], ['Enganxa l’ordre', 'Instal·la nio i el deixa engegat, també quan reinicies.'], ['Escaneja el QR', 'Aquesta web es connecta sola i et mostra el QR del mòbil.']];
|
|
815
|
+
return `${head}<div class="panel empty first-run">${bot('idle')}<h2>Connecta el teu primer ordinador</h2><p>${phone ? 'nio s’instal·la a l’ordinador amb una sola ordre. Copia-la i enganxa-la allà.' : 'Una sola ordre instal·la nio en aquest ordinador i ho deixa tot a punt.'}</p>${cmdBox(INSTALL_CMD)}<ol class="stepper">${steps.map(([t, d], i) => `<li><span class="n">${i + 1}</span><div><b>${t}</b><p>${d}</p></div></li>`).join('')}</ol><div class="row" style="justify-content:center"><button class="btn ink" data-act="add-computer" type="button">${svg(ICON.plus)}${phone ? 'Tinc un codi o un QR' : 'Afegeix ordinador'}</button><button class="btn" data-act="demo" type="button">Prova una demo</button></div><p class="muted small">${OTHER_OS}. Per provar-lo sense instal·lar res, <span class="mono">npx nio-cat</span>.</p></div>`;
|
|
811
816
|
}
|
|
812
817
|
const detail = m && state.connected ? `<h2 class="sec">${esc(m.name)}</h2>
|
|
813
818
|
<div class="grid2 one">
|
|
@@ -894,7 +899,7 @@ function addComputerSheet(tab, opts = {}) {
|
|
|
894
899
|
const saved = d && store.machines.findIndex((x) => x.id === d.id);
|
|
895
900
|
if (d && saved >= 0) set('saved', `${intro('Aquest ordinador ja és a la llista', 'El tens connectat com a «' + esc(d.name) + '».')}<div class="row"><button class="btn ink" type="button" data-open-saved="${saved}">Obre sessions</button></div>`);
|
|
896
901
|
else if (d) set('found', `${intro('nio ja funciona en aquest ordinador', 'Aquest navegador encara no hi té accés. Escriu això al Terminal i s’obrirà el panell ja connectat:')}${cmdBox('nio pair')}`);
|
|
897
|
-
else set('none', `${intro('Instal·la nio en aquest ordinador', 'Una sola ordre ho deixa tot a punt i engegat, també quan reinicies.')}${todo([
|
|
902
|
+
else set('none', `${intro('Instal·la nio en aquest ordinador', 'Una sola ordre ho deixa tot a punt i engegat, també quan reinicies.')}${todo([OPEN_TERM, `Enganxa aquesta ordre i prem Retorn:${cmdBox(INSTALL_CMD)}`, 'Quan acabi, aquesta pàgina es connectarà sola.'])}${live('Esperant nio…')}<p class="hint">${OTHER_OS}. Si ja el tens i està aturat, escriu <span class="mono">nio</span> al ${isWindows ? 'PowerShell' : 'Terminal'}.</p>${store.machines.length ? '' : '<div class="or"><span>o</span></div><button class="btn block" type="button" data-close data-act="demo">Prova una demo, sense instal·lar res</button>'}`);
|
|
898
903
|
if (saved < 0 || !d) poll = setTimeout(local, 3000);
|
|
899
904
|
};
|
|
900
905
|
const runSsh = async (target) => {
|
package/web/index.html
CHANGED
|
@@ -68,6 +68,8 @@ h1 { font-size: clamp(38px, 6.4vw, 68px); font-weight: 500; line-height: 1.04; l
|
|
|
68
68
|
.install code { background: none; border-radius: 0; flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; padding: 9px 0; font: 500 15.5px ui-monospace, "SF Mono", Menlo, monospace; scrollbar-width: none; }
|
|
69
69
|
.install code::-webkit-scrollbar { display: none; }
|
|
70
70
|
.hero .alt { margin-top: 14px; font-size: 15px; color: var(--fg-muted); }
|
|
71
|
+
.hero .os-sw { border: 0; padding: 0; background: none; font: inherit; color: inherit; text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
|
|
72
|
+
.hero .os-sw:hover { color: var(--fg); }
|
|
71
73
|
.copy { flex: none; display: inline-grid; place-items: center; width: 42px; height: 42px; border: 0; border-radius: 11px; background: rgba(255,255,255,.1); color: inherit; cursor: pointer; transition: background-color .2s, transform .12s; }
|
|
72
74
|
.copy:hover { background: rgba(255,255,255,.2); }
|
|
73
75
|
.copy:active { transform: scale(.95); }
|
|
@@ -203,7 +205,7 @@ main { padding-bottom: 8px; }
|
|
|
203
205
|
<h1>Els teus agents, a les teves màquines, des del mòbil.</h1>
|
|
204
206
|
<p class="lead">nio és un comandament per a Claude Code, Codex i altres agents. Treballen als teus ordinadors, amb els teus comptes i les teves claus. Tu els dones feina des del mòbil, els segueixes en directe i aproves el que és arriscat. Personal i gratuït.</p>
|
|
205
207
|
<div class="install"><code id="cmd">curl -fsSL https://www.nio.cat/install.sh | sh</code><button class="copy" type="button" data-copy="curl -fsSL https://www.nio.cat/install.sh | sh" aria-label="Copia l’ordre"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3"/></svg><svg class="ok" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></button></div>
|
|
206
|
-
<p class="alt"><span>Linux o Mac.</span> <span>Per provar-ho sense instal·lar: <code>npx nio-cat</code></span></p>
|
|
208
|
+
<p class="alt"><span><span id="osTxt">Linux o Mac.</span> <button class="os-sw" id="osSw" type="button">Tens Windows?</button></span> <span>Per provar-ho sense instal·lar: <code>npx nio-cat</code></span></p>
|
|
207
209
|
</div>
|
|
208
210
|
|
|
209
211
|
<section aria-labelledby="com">
|
|
@@ -255,6 +257,20 @@ main { padding-bottom: 8px; }
|
|
|
255
257
|
</footer>
|
|
256
258
|
<script src="/bot.js" defer></script>
|
|
257
259
|
<script>
|
|
260
|
+
// A Windows, l'ordre de PowerShell. Es pot canviar a mà (per instal·lar-lo en un altre ordinador).
|
|
261
|
+
(() => {
|
|
262
|
+
const CMD = { unix: 'curl -fsSL https://www.nio.cat/install.sh | sh', win: 'irm https://www.nio.cat/install.ps1 | iex' };
|
|
263
|
+
const code = document.getElementById('cmd'), copy = code.nextElementSibling, txt = document.getElementById('osTxt'), sw = document.getElementById('osSw');
|
|
264
|
+
const set = (os) => {
|
|
265
|
+
code.textContent = CMD[os]; copy.dataset.copy = CMD[os]; sw.dataset.os = os;
|
|
266
|
+
txt.textContent = os === 'win' ? 'Windows, al PowerShell.' : 'Linux o Mac.';
|
|
267
|
+
sw.textContent = os === 'win' ? 'Tens Mac o Linux?' : 'Tens Windows?';
|
|
268
|
+
};
|
|
269
|
+
sw.onclick = () => set(sw.dataset.os === 'win' ? 'unix' : 'win');
|
|
270
|
+
set(navigator.userAgentData?.platform === 'Windows' || /Windows/.test(navigator.userAgent) ? 'win' : 'unix');
|
|
271
|
+
})();
|
|
272
|
+
</script>
|
|
273
|
+
<script>
|
|
258
274
|
document.addEventListener('click', async (e) => {
|
|
259
275
|
const b = e.target.closest('[data-copy]'); if (!b) return;
|
|
260
276
|
try { await navigator.clipboard.writeText(b.dataset.copy); }
|
package/web/sw.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// nio · service worker: obre l'app encara que no hi hagi connexió i mostra els avisos.
|
|
2
|
-
const SHELL = 'nio-shell-
|
|
2
|
+
const SHELL = 'nio-shell-v62';
|
|
3
3
|
const FILES = ['/app', '/app/style.css', '/app/notes.css', '/app/call.css', '/app/app.js', '/app/notes.js', '/app/notes-live.js', '/app/call.js', '/app/demo.js', '/app/android.js', '/bot.css', '/bot.js', '/board.css', '/board.js', '/favicon.svg', '/manifest.webmanifest'];
|
|
4
4
|
|
|
5
5
|
self.addEventListener('install', (e) => {
|