@mmmbuto/nexuscrew 0.8.0 → 0.8.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/README.md +66 -60
- package/bin/nexuscrew.js +10 -4
- package/frontend/dist/assets/index-BFyPeZsL.js +90 -0
- package/frontend/dist/assets/index-COsoJxXQ.css +32 -0
- package/frontend/dist/assets/qr-scanner-worker.min-D85Z9gVD.js +98 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +314 -110
- package/lib/cli/doctor.js +1 -1
- package/lib/cli/init.js +40 -10
- package/lib/cli/pidfile.js +2 -2
- package/lib/cli/service.js +0 -5
- package/lib/decks/store.js +8 -2
- package/lib/files/routes.js +3 -1
- package/lib/fleet/builtin.js +15 -5
- package/lib/fleet/managed.js +278 -107
- package/lib/mcp/server.js +24 -5
- package/lib/nodes/commands.js +27 -12
- package/lib/nodes/peering.js +116 -0
- package/lib/nodes/store.js +91 -24
- package/lib/nodes/topology-cache.js +75 -0
- package/lib/nodes/tunnel.js +22 -7
- package/lib/proxy/federation.js +263 -0
- package/lib/proxy/node-proxy.js +21 -8
- package/lib/server.js +91 -7
- package/lib/settings/routes.js +221 -5
- package/package.json +2 -2
- package/frontend/dist/assets/index-BBOgTCoO.css +0 -32
- package/frontend/dist/assets/index-DQrDBogT.js +0 -83
package/lib/cli/init.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const fs = require('node:fs');
|
|
9
9
|
const os = require('node:os');
|
|
10
10
|
const path = require('node:path');
|
|
11
|
+
const crypto = require('node:crypto');
|
|
11
12
|
const { detectPlatform, nodeBin, repoRoot, uid } = require('./platform.js');
|
|
12
13
|
const { loadOrCreateToken } = require('../auth/token.js');
|
|
13
14
|
const { generateService, installService, fileMode, installPath: svcInstallPath } = require('./service.js');
|
|
@@ -28,6 +29,24 @@ function nodeMajor() {
|
|
|
28
29
|
return parseInt(String(process.versions.node).split('.')[0], 10);
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function writeConfigAtomic(configPath, value) {
|
|
33
|
+
try {
|
|
34
|
+
if (fs.lstatSync(configPath).isSymbolicLink()) throw new Error('refusing symlink config target');
|
|
35
|
+
} catch (e) {
|
|
36
|
+
if (e.code !== 'ENOENT') throw e;
|
|
37
|
+
}
|
|
38
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
39
|
+
const tmp = path.join(path.dirname(configPath), `.${path.basename(configPath)}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
40
|
+
try {
|
|
41
|
+
fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
42
|
+
fs.chmodSync(tmp, 0o600);
|
|
43
|
+
fs.renameSync(tmp, configPath);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
// Migration rule (B2): se non c'è config.json, parse la porta dal service file esistente.
|
|
32
51
|
function readExistingPort(platform, home, installPathOverride) {
|
|
33
52
|
const p = installPathOverride || svcInstallPath(platform, home);
|
|
@@ -56,6 +75,7 @@ function runInit(opts = {}) {
|
|
|
56
75
|
const tokenPath = opts.tokenPath || path.join(configDir, 'token');
|
|
57
76
|
const filesRoot = opts.filesRoot || path.join(home, 'NexusFiles');
|
|
58
77
|
const dryRun = !!opts.dryRun;
|
|
78
|
+
const installBoot = opts.installBoot !== false;
|
|
59
79
|
const log = opts.log || (() => {});
|
|
60
80
|
const tmuxOk = opts.tmuxOk !== undefined ? opts.tmuxOk : haveTmux(opts.tmuxBin || 'tmux');
|
|
61
81
|
|
|
@@ -78,24 +98,30 @@ function runInit(opts = {}) {
|
|
|
78
98
|
fs.mkdirSync(configDir, { recursive: true });
|
|
79
99
|
if (!fs.existsSync(configPath)) {
|
|
80
100
|
if (!port) port = 41820;
|
|
81
|
-
|
|
101
|
+
writeConfigAtomic(configPath, { port });
|
|
82
102
|
actions.push(`created config ${configPath} (port ${port})`);
|
|
83
103
|
} else {
|
|
104
|
+
let current;
|
|
84
105
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
current = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
107
|
+
} catch (_) { current = {}; }
|
|
108
|
+
if (opts.port) {
|
|
109
|
+
current.port = opts.port;
|
|
110
|
+
port = opts.port;
|
|
111
|
+
writeConfigAtomic(configPath, current);
|
|
112
|
+
actions.push(`updated config ${configPath} (port ${port})`);
|
|
113
|
+
} else if (current && current.port) port = current.port;
|
|
88
114
|
actions.push(`preserved config ${configPath} (port ${port})`);
|
|
89
115
|
}
|
|
90
116
|
}
|
|
91
117
|
|
|
92
|
-
// Fleet app defaults: soltanto i
|
|
118
|
+
// Fleet app defaults: soltanto i tre client nativi. Provider cloud/Z.AI sono
|
|
93
119
|
// disponibili nel catalogo managed ma vanno aggiunti esplicitamente.
|
|
94
120
|
const fleetDefsPath = opts.fleetDefsPath || path.join(configDir, 'fleet.json');
|
|
95
121
|
if (!dryRun && !fs.existsSync(fleetDefsPath)) {
|
|
96
122
|
try {
|
|
97
123
|
writeFleet(fleetDefsPath, defaultDefinitions());
|
|
98
|
-
actions.push(`created fleet defaults ${fleetDefsPath} (claude.native, codex-vl.native)`);
|
|
124
|
+
actions.push(`created fleet defaults ${fleetDefsPath} (claude.native, codex.native, codex-vl.native)`);
|
|
99
125
|
} catch (e) {
|
|
100
126
|
actions.push(`WARN: fleet defaults non creati: ${e.message}`);
|
|
101
127
|
}
|
|
@@ -144,7 +170,9 @@ function runInit(opts = {}) {
|
|
|
144
170
|
};
|
|
145
171
|
const content = generateService(platform, svcCtx);
|
|
146
172
|
|
|
147
|
-
if (
|
|
173
|
+
if (!installBoot) {
|
|
174
|
+
actions.push(`boot opt-in: service ${platform} non installato`);
|
|
175
|
+
} else if (dryRun) {
|
|
148
176
|
actions.push(`DRY-RUN service (${platform}) generato, NON installato`);
|
|
149
177
|
} else if (!tmuxOk) {
|
|
150
178
|
// tmux mancante: abort before service install (config/token gia' creati) [M8]
|
|
@@ -171,7 +199,9 @@ function runInit(opts = {}) {
|
|
|
171
199
|
// principale: ogni errore del companion -> WARN action + return normale.
|
|
172
200
|
try {
|
|
173
201
|
const readonly = process.env.NEXUSCREW_READONLY === '1' || opts.readonly;
|
|
174
|
-
if (
|
|
202
|
+
if (!installBoot) {
|
|
203
|
+
actions.push('fleet companion: boot opt-in, non installato');
|
|
204
|
+
} else if (readonly) {
|
|
175
205
|
actions.push('fleet companion: READONLY, non installato');
|
|
176
206
|
} else {
|
|
177
207
|
// provider mode: companion SOLO se builtin (§9b). selectProviderModeSync e'
|
|
@@ -226,7 +256,7 @@ function runInit(opts = {}) {
|
|
|
226
256
|
}
|
|
227
257
|
|
|
228
258
|
// Termux:boot best-effort detection (R4)
|
|
229
|
-
if (platform === 'termux') {
|
|
259
|
+
if (platform === 'termux' && installBoot) {
|
|
230
260
|
const bootDir = path.join(home, '.termux', 'boot');
|
|
231
261
|
const bootOk = fs.existsSync(bootDir);
|
|
232
262
|
actions.push(bootOk
|
|
@@ -246,4 +276,4 @@ function runInit(opts = {}) {
|
|
|
246
276
|
return { platform, port, token, url: urlWithToken, actions, tmuxOk, dryRun };
|
|
247
277
|
}
|
|
248
278
|
|
|
249
|
-
module.exports = { runInit, readExistingPort, haveTmux, nodeMajor };
|
|
279
|
+
module.exports = { runInit, readExistingPort, haveTmux, nodeMajor, writeConfigAtomic };
|
package/lib/cli/pidfile.js
CHANGED
|
@@ -7,8 +7,8 @@ const os = require('node:os');
|
|
|
7
7
|
const path = require('node:path');
|
|
8
8
|
const { execFileSync } = require('node:child_process');
|
|
9
9
|
|
|
10
|
-
function defaultPidfilePath() {
|
|
11
|
-
return process.env.NEXUSCREW_PIDFILE || path.join(
|
|
10
|
+
function defaultPidfilePath(home = os.homedir()) {
|
|
11
|
+
return process.env.NEXUSCREW_PIDFILE || path.join(home, '.nexuscrew', 'nexuscrew.pid');
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function readPidfile(p) {
|
package/lib/cli/service.js
CHANGED
|
@@ -70,7 +70,6 @@ After=network-online.target
|
|
|
70
70
|
[Service]
|
|
71
71
|
Type=simple
|
|
72
72
|
WorkingDirectory=${repo}
|
|
73
|
-
Environment=NEXUSCREW_PORT=${ctx.port}
|
|
74
73
|
Environment=PATH=${nodeDir}:/usr/local/bin:/usr/bin:/bin
|
|
75
74
|
ExecStart=${node} ${repoBin} serve
|
|
76
75
|
Restart=on-failure
|
|
@@ -86,7 +85,6 @@ function generateMac(ctx) {
|
|
|
86
85
|
const repoBinXml = escapeXml(path.join(ctx.repoRoot, 'bin', 'nexuscrew.js'));
|
|
87
86
|
const repoXml = escapeXml(ctx.repoRoot);
|
|
88
87
|
const homeXml = escapeXml(ctx.home);
|
|
89
|
-
const port = ctx.port;
|
|
90
88
|
const launchPath = [...new Set([
|
|
91
89
|
path.dirname(ctx.nodeBin), '/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin',
|
|
92
90
|
])].join(':');
|
|
@@ -107,8 +105,6 @@ function generateMac(ctx) {
|
|
|
107
105
|
<string>${repoXml}</string>
|
|
108
106
|
<key>EnvironmentVariables</key>
|
|
109
107
|
<dict>
|
|
110
|
-
<key>NEXUSCREW_PORT</key>
|
|
111
|
-
<string>${port}</string>
|
|
112
108
|
<key>PATH</key>
|
|
113
109
|
<string>${launchPathXml}</string>
|
|
114
110
|
</dict>
|
|
@@ -136,7 +132,6 @@ function generateTermux(ctx) {
|
|
|
136
132
|
# NexusCrew boot (Termux) - loopback, localhost del telefono
|
|
137
133
|
export PATH=/data/data/com.termux/files/usr/bin:$PATH
|
|
138
134
|
export HOME=/data/data/com.termux/files/home
|
|
139
|
-
export NEXUSCREW_PORT=${ctx.port}
|
|
140
135
|
cd -- ${repoQ}
|
|
141
136
|
termux-wake-lock 2>/dev/null || true
|
|
142
137
|
mkdir -p "$HOME/.nexuscrew"
|
package/lib/decks/store.js
CHANGED
|
@@ -8,7 +8,13 @@ const SCHEMA_VERSION = 1;
|
|
|
8
8
|
const MAX_DECKS = 24;
|
|
9
9
|
const MAX_TILES = 9;
|
|
10
10
|
const NAME_RE = /^[a-z0-9-]{1,32}$/;
|
|
11
|
-
const NODE_RE = /^[a-z0-9-]{1,32}$/;
|
|
11
|
+
const NODE_RE = /^[a-z0-9-]{1,32}(?:\/[a-z0-9-]{1,32}){0,3}$/;
|
|
12
|
+
|
|
13
|
+
function validNodeRoute(node) {
|
|
14
|
+
if (!NODE_RE.test(node)) return false;
|
|
15
|
+
const parts = node.split('/');
|
|
16
|
+
return new Set(parts).size === parts.length;
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
function defaultDecksPath(home) {
|
|
14
20
|
return path.join(home || os.homedir(), '.nexuscrew', 'decks.json');
|
|
@@ -41,7 +47,7 @@ function parseLayout(raw) {
|
|
|
41
47
|
const tiles = [];
|
|
42
48
|
for (const t of c.tiles) {
|
|
43
49
|
if (!t || typeof t !== 'object' || Array.isArray(t) || !validText(t.session, 128)) return null;
|
|
44
|
-
if (t.node !== undefined && (typeof t.node !== 'string' || !
|
|
50
|
+
if (t.node !== undefined && (typeof t.node !== 'string' || !validNodeRoute(t.node))) return null;
|
|
45
51
|
const height = Number(t.height);
|
|
46
52
|
const fontSize = Number(t.fontSize);
|
|
47
53
|
if (!Number.isFinite(height) || height < 0.2 || height > 100) return null;
|
package/lib/files/routes.js
CHANGED
|
@@ -9,7 +9,7 @@ const store = require('./store.js');
|
|
|
9
9
|
|
|
10
10
|
// Router file-exchange. Nessuno stato: tutto deriva da cfg + filesystem.
|
|
11
11
|
// notifier (opzionale, MCP bridge): emette la notify di consegna file outbox.
|
|
12
|
-
function filesRoutes({ cfg, sessionExists, paste, notifier }) {
|
|
12
|
+
function filesRoutes({ cfg, sessionExists, paste, notifier, readonly = () => false }) {
|
|
13
13
|
const router = Router();
|
|
14
14
|
const upload = multer({
|
|
15
15
|
storage: multer.memoryStorage(),
|
|
@@ -17,6 +17,7 @@ function filesRoutes({ cfg, sessionExists, paste, notifier }) {
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
router.post('/upload', (req, res) => {
|
|
20
|
+
if (readonly()) return res.status(403).json({ error: 'READONLY: upload bloccato' });
|
|
20
21
|
upload.single('file')(req, res, async (err) => {
|
|
21
22
|
if (err) {
|
|
22
23
|
const status = err.code === 'LIMIT_FILE_SIZE' ? 413 : 400;
|
|
@@ -104,6 +105,7 @@ function filesRoutes({ cfg, sessionExists, paste, notifier }) {
|
|
|
104
105
|
});
|
|
105
106
|
|
|
106
107
|
router.delete('/', (req, res) => {
|
|
108
|
+
if (readonly()) return res.status(403).json({ error: 'READONLY: eliminazione file bloccata' });
|
|
107
109
|
const ok = store.removeFile(
|
|
108
110
|
cfg.filesRoot, String(req.query.session || ''), String(req.query.box || ''), String(req.query.name || ''),
|
|
109
111
|
);
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -29,7 +29,7 @@ const {
|
|
|
29
29
|
loadDefinitions, atomicWrite, CAPS,
|
|
30
30
|
} = require('./definitions.js');
|
|
31
31
|
const {
|
|
32
|
-
|
|
32
|
+
publicCatalog, describeManaged, resolveManagedEngine, discoverOllamaModels, discoverPiModels,
|
|
33
33
|
} = require('./managed.js');
|
|
34
34
|
|
|
35
35
|
const STATUS_TTL_MS = 2000;
|
|
@@ -273,6 +273,8 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
273
273
|
});
|
|
274
274
|
const needsOllama = cache.defs.engines.some((e) => e.managed?.provider === 'ollama-cloud');
|
|
275
275
|
const ollamaModels = needsOllama ? await discoverOllamaModels({ ...cfg, home }) : [];
|
|
276
|
+
const needsPi = cache.defs.engines.some((e) => e.managed?.client === 'pi');
|
|
277
|
+
const piModels = needsPi ? await discoverPiModels({ ...cfg, home }) : {};
|
|
276
278
|
const engines = cache.defs.engines.map((e) => {
|
|
277
279
|
const managed = e.managed ? describeManaged(e.managed, { ...cfg, home }) : null;
|
|
278
280
|
return {
|
|
@@ -280,7 +282,8 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
280
282
|
...(managed ? {
|
|
281
283
|
kind: 'managed', client: managed.client, provider: managed.provider,
|
|
282
284
|
model: e.managed.model || managed.defaultModel || '',
|
|
283
|
-
models: managed.provider === 'ollama-cloud' ? ollamaModels
|
|
285
|
+
models: managed.provider === 'ollama-cloud' ? ollamaModels
|
|
286
|
+
: (managed.client === 'pi' ? (e.managed.provider === 'custom' ? [e.managed.model] : (piModels[e.managed.provider] || [])) : managed.models),
|
|
284
287
|
configured: managed.configured, reason: managed.reason,
|
|
285
288
|
} : { kind: 'custom', configured: true, model: e.model?.value || '', models: [] }),
|
|
286
289
|
};
|
|
@@ -504,7 +507,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
504
507
|
return out;
|
|
505
508
|
}),
|
|
506
509
|
cells: defs.cells.map((c) => ({ ...c, ...(c.models ? { models: { ...c.models } } : {}) })),
|
|
507
|
-
managedCatalog:
|
|
510
|
+
managedCatalog: publicCatalog(),
|
|
508
511
|
managedConfig: { providerSecretsPath: cfg.providerSecretsPath || path.join(home, '.nexuscrew', 'providers.env') },
|
|
509
512
|
};
|
|
510
513
|
}
|
|
@@ -536,9 +539,16 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
536
539
|
rc: { type: 'boolean', required: false, default: false },
|
|
537
540
|
managed: {
|
|
538
541
|
type: 'object', requiredFor: 'managed',
|
|
539
|
-
client: { type: 'enum', values: ['claude', 'codex-vl'] },
|
|
540
|
-
provider: { type: '
|
|
542
|
+
client: { type: 'enum', values: ['claude', 'codex', 'codex-vl', 'pi'] },
|
|
543
|
+
provider: { type: 'catalog', source: 'managedCatalog' },
|
|
544
|
+
credentialProfile: { type: 'string', required: false, max: 32 },
|
|
541
545
|
model: { type: 'string', required: false, max: CAPS.MAX_MODEL_VAL_LEN },
|
|
546
|
+
permissionPolicy: { type: 'enum', values: ['standard', 'unsafe'], default: 'standard' },
|
|
547
|
+
displayName: { type: 'string', requiredFor: 'provider=custom', max: CAPS.MAX_LABEL_LEN },
|
|
548
|
+
protocol: { type: 'catalog', source: 'managedCatalog.protocol' },
|
|
549
|
+
baseUrl: { type: 'string', requiredFor: 'provider=custom', max: CAPS.MAX_COMMAND_LEN },
|
|
550
|
+
envKey: { type: 'string', requiredFor: 'provider=custom', pattern: '^[A-Za-z_][A-Za-z0-9_]{0,63}$' },
|
|
551
|
+
providerId: { type: 'string', requiredFor: 'provider=custom', pattern: '^[a-z][a-z0-9_-]{0,31}$' },
|
|
542
552
|
},
|
|
543
553
|
command: { type: 'string', requiredFor: 'custom', max: CAPS.MAX_COMMAND_LEN, absolute: true },
|
|
544
554
|
args: { type: 'array', of: 'string', required: false, max: CAPS.MAX_ARGS, itemMax: CAPS.MAX_ARG_LEN },
|