@mmmbuto/nexuscrew 0.9.52 → 0.9.53
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 +12 -0
- package/frontend/dist/assets/index-DBlFdkZn.js +169 -0
- package/frontend/dist/assets/{index-CcZskBmy.css → index-Da4oBb4f.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +63 -14
- package/lib/cli/messages.js +55 -0
- package/lib/cli/url.js +6 -2
- package/lib/notify/ask-answer-service.js +1 -0
- package/lib/notify/asks.js +58 -7
- package/lib/notify/event-feed-client.js +420 -48
- package/lib/notify/event-feed-routes.js +44 -3
- package/lib/notify/persist.js +69 -12
- package/lib/notify/routes.js +19 -1
- package/lib/prefs/routes.js +52 -0
- package/lib/prefs/store.js +112 -0
- package/lib/proxy/federation.js +49 -5
- package/lib/server.js +112 -34
- package/lib/update/core.js +45 -0
- package/lib/update/manager.js +31 -8
- package/lib/update/npm-env.js +109 -0
- package/lib/update/runner.js +54 -14
- package/lib/update/start-lock.js +106 -0
- package/package.json +1 -1
- package/frontend/dist/assets/index-5MtkSiOQ.js +0 -169
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Messaggi per chi sta al terminale (o legge il log del servizio) quando la porta e' occupata. Il nodo NON si sposta
|
|
3
|
+
// piu' da solo su un'altra porta: dev'essere chiaro cosa fare. Lingue: it, en, es. Se la lingua non si puo'
|
|
4
|
+
// determinare (servizio senza LANG) escono tutte e tre, una per riga: cosi' si legge comunque.
|
|
5
|
+
const LANGS = ['it', 'en', 'es'];
|
|
6
|
+
|
|
7
|
+
const WHY = {
|
|
8
|
+
en: 'A new port is a new address: the installed app would lose its token and settings.',
|
|
9
|
+
it: 'Una porta nuova è un indirizzo nuovo: l\'app installata perderebbe token e impostazioni.',
|
|
10
|
+
es: 'Un puerto nuevo es una dirección nueva: la app instalada perdería el token y los ajustes.',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const CATALOG = {
|
|
14
|
+
'busy-other': {
|
|
15
|
+
en: ({ port }) => `Port ${port} is in use by another process. The node does not move to another port on its own. ${WHY.en} Free port ${port}, or choose another one explicitly: nexuscrew init --port <N>.`,
|
|
16
|
+
it: ({ port }) => `La porta ${port} è occupata da un altro processo. Il nodo non passa da solo a un'altra porta. ${WHY.it} Libera la porta ${port}, oppure scegline un'altra in modo esplicito: nexuscrew init --port <N>.`,
|
|
17
|
+
es: ({ port }) => `El puerto ${port} está ocupado por otro proceso. El nodo no cambia de puerto por su cuenta. ${WHY.es} Libera el puerto ${port} o elige otro de forma explícita: nexuscrew init --port <N>.`,
|
|
18
|
+
},
|
|
19
|
+
'busy-paired': {
|
|
20
|
+
en: ({ port }) => `Port ${port} is busy and paired peers exist: refusing automatic port change (the peers point at this port). Free port ${port}, or choose another one explicitly and re-pair the peers: nexuscrew init --port <N>.`,
|
|
21
|
+
it: ({ port }) => `La porta ${port} è occupata e ci sono nodi accoppiati: cambio automatico di porta rifiutato (i nodi puntano a questa porta). Libera la porta ${port}, oppure scegline un'altra in modo esplicito e riaccoppia i nodi: nexuscrew init --port <N>.`,
|
|
22
|
+
es: ({ port }) => `El puerto ${port} está ocupado y hay nodos emparejados: cambio automático de puerto rechazado (los nodos apuntan a este puerto). Libera el puerto ${port} o elige otro de forma explícita y vuelve a emparejar los nodos: nexuscrew init --port <N>.`,
|
|
23
|
+
},
|
|
24
|
+
'busy-token': {
|
|
25
|
+
en: ({ port }) => `Port ${port} answers as a NexusCrew node that rejects this token. The port is not changed. Check ~/.nexuscrew/token or the instance running there.`,
|
|
26
|
+
it: ({ port }) => `La porta ${port} risponde come un nodo NexusCrew che rifiuta questo token. La porta non viene cambiata. Controlla ~/.nexuscrew/token o l'istanza che gira lì.`,
|
|
27
|
+
es: ({ port }) => `El puerto ${port} responde como un nodo NexusCrew que rechaza este token. El puerto no se cambia. Revisa ~/.nexuscrew/token o la instancia que corre allí.`,
|
|
28
|
+
},
|
|
29
|
+
'busy-own-slow': {
|
|
30
|
+
en: ({ port }) => `The NexusCrew process on port ${port} is running but does not answer yet. The port is not changed. Retry in a moment (nexuscrew show) or check the log (nexuscrew logs).`,
|
|
31
|
+
it: ({ port }) => `Il processo NexusCrew sulla porta ${port} è avviato ma non risponde ancora. La porta non viene cambiata. Riprova fra poco (nexuscrew show) o controlla il log (nexuscrew logs).`,
|
|
32
|
+
es: ({ port }) => `El proceso NexusCrew en el puerto ${port} está en marcha pero aún no responde. El puerto no se cambia. Reintenta en un momento (nexuscrew show) o revisa el log (nexuscrew logs).`,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// NEXUSCREW_LANG, poi LC_ALL, LC_MESSAGES, LANG: la prima variabile impostata decide; se non e' it/en/es -> 'all'.
|
|
37
|
+
function pickLang(env = process.env) {
|
|
38
|
+
for (const name of ['NEXUSCREW_LANG', 'LC_ALL', 'LC_MESSAGES', 'LANG']) {
|
|
39
|
+
const value = String(env[name] || '').trim();
|
|
40
|
+
if (!value || value === 'C' || value === 'POSIX' || /^C\./.test(value)) continue;
|
|
41
|
+
const base = value.slice(0, 2).toLowerCase();
|
|
42
|
+
return LANGS.includes(base) ? base : 'all';
|
|
43
|
+
}
|
|
44
|
+
return 'all';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function portMessage(key, params, { lang, env } = {}) {
|
|
48
|
+
const entry = CATALOG[key];
|
|
49
|
+
if (!entry) throw new Error(`messaggio sconosciuto: ${key}`);
|
|
50
|
+
const chosen = lang || pickLang(env);
|
|
51
|
+
if (chosen === 'all') return ['en', 'it', 'es'].map((l) => entry[l](params)).join('\n');
|
|
52
|
+
return (entry[chosen] || entry.en)(params);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { LANGS, pickLang, portMessage };
|
package/lib/cli/url.js
CHANGED
|
@@ -46,9 +46,13 @@ function loadPort(opts = {}) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// URL loopback. withToken=true e token presente -> aggiunge #token=… (mai altrove).
|
|
49
|
-
|
|
49
|
+
// deviceId (opzionale, 32 hex emesso dal nodo): identifica il profilo di preferenze del dispositivo che apre il
|
|
50
|
+
// link. Viaggia solo accanto al token e mai da solo.
|
|
51
|
+
function buildUrl(port, token, { withToken = false, deviceId } = {}) {
|
|
50
52
|
const base = `http://127.0.0.1:${port}/`;
|
|
51
|
-
|
|
53
|
+
if (!(withToken && token)) return base;
|
|
54
|
+
const device = typeof deviceId === 'string' && /^[a-f0-9]{32}$/.test(deviceId) ? `&device=${deviceId}` : '';
|
|
55
|
+
return `${base}#token=${token}${device}`;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
// QR ASCII del testo. Generazione locale (qrcode-terminal), callback sincrona.
|
|
@@ -45,6 +45,7 @@ function createAskAnswerService({ asks, paste, receipts, onClosure, labelPrefix
|
|
|
45
45
|
// for a paste that never happened.
|
|
46
46
|
if (request && receipts) receipts.finalize(request.peerId, askId, request.requestId, 'failed', `refused:${claim.reason}`);
|
|
47
47
|
if (claim.reason === 'unknown') return { ok: false, code: 404, error: 'ask inesistente', reason: 'unknown' };
|
|
48
|
+
if (claim.reason === 'store-unreadable') return { ok: false, code: 503, error: 'store delle ask illeggibile: risposta non accettata', reason: 'store-unreadable' };
|
|
48
49
|
if (claim.reason === 'dismissed') return { ok: false, code: 409, error: 'ask gia\' scartato (dismissed)', reason: 'dismissed' };
|
|
49
50
|
if (claim.reason === 'answering') return { ok: false, code: 409, error: 'risposta gia\' in corso da un\'altra richiesta', reason: 'answering' };
|
|
50
51
|
return { ok: false, code: 409, error: 'ask gia\' risposto', reason: 'answered' };
|
package/lib/notify/asks.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// claim -> (paste) -> commit/release (da revisione: una sola answer puo' vincere).
|
|
7
7
|
const path = require('node:path');
|
|
8
8
|
const crypto = require('node:crypto');
|
|
9
|
-
const {
|
|
9
|
+
const { readJsonStrict, atomicWriteJson } = require('./persist.js');
|
|
10
10
|
|
|
11
11
|
const ASKS_FILE = 'asks.json';
|
|
12
12
|
// da revisione: MAX_OPEN e' un cap DURO sugli ask aperti — al cap il nuovo ask viene
|
|
@@ -28,17 +28,56 @@ function createAsksStore(opts = {}) {
|
|
|
28
28
|
const now = opts.now || (() => Date.now());
|
|
29
29
|
|
|
30
30
|
// Carica lazy al primo accesso (niente I/O in createServer per i path reali).
|
|
31
|
+
// Tre esiti:
|
|
32
|
+
// - leggibile (assente => [], oppure oggetto con `asks` array => filtrato per
|
|
33
|
+
// id stringa): si CACHEA come oggi, perche' e' uno stato legittimo e stabile.
|
|
34
|
+
// - illeggibile (JSON malformato, oppure oggetto senza `asks` o con `asks`
|
|
35
|
+
// non array): NON si cachea. Ogni accesso rilegge il file, cosicche' se
|
|
36
|
+
// l'operatore ripara asks.json lo store torna sano senza riavvio. I reader
|
|
37
|
+
// (list/get/openCount/findImported) vedono [] — le superfici LOCALI non
|
|
38
|
+
// cambiano comportamento — mentre le MUTAZIONI rifiutano (v. unreadableGuard)
|
|
39
|
+
// per non sovrascrivere il file malformato con un defaults vuoto, che
|
|
40
|
+
// cancellerebbe le domande aperte.
|
|
31
41
|
let asks = null;
|
|
32
42
|
function load() {
|
|
33
43
|
if (asks) return asks;
|
|
34
|
-
const
|
|
35
|
-
asks =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
const r = readJsonStrict(filePath);
|
|
45
|
+
if (r.state === 'absent') { asks = []; return asks; }
|
|
46
|
+
if (r.state === 'ok' && Array.isArray(r.value.asks)) {
|
|
47
|
+
asks = r.value.asks.filter((a) => a && typeof a === 'object' && typeof a.id === 'string');
|
|
48
|
+
return asks;
|
|
49
|
+
}
|
|
50
|
+
// illeggibile: NON cacheare (reload al prossimo accesso se il file viene riparato).
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Health leggibile dal produttore dello snapshot: distingue un vuoto
|
|
55
|
+
// LEGITTIMO (store leggibile, nessuna domanda aperta) da un vuoto NON
|
|
56
|
+
// autorevole (store illeggibile). Non muta stato.
|
|
57
|
+
function health() {
|
|
58
|
+
if (asks) return { readable: true };
|
|
59
|
+
const r = readJsonStrict(filePath);
|
|
60
|
+
if (r.state === 'absent') return { readable: true };
|
|
61
|
+
if (r.state === 'ok' && Array.isArray(r.value.asks)) return { readable: true };
|
|
62
|
+
const reason = r.state === 'malformed'
|
|
63
|
+
? r.reason
|
|
64
|
+
: `forma invalida: 'asks' non e' un array (${r.value && r.value.asks !== undefined ? typeof r.value.asks : 'assente'})`;
|
|
65
|
+
return { readable: false, reason };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Null se lo store e' leggibile (e le mutazioni possono procedere), altrimenti
|
|
69
|
+
// la reason. Si usa all'inizio di ogni mutazione: uno store illeggibile non
|
|
70
|
+
// viene MAI scritto, cosi' il file malformato non viene sovrascritto.
|
|
71
|
+
function unreadableGuard() {
|
|
72
|
+
if (asks) return null;
|
|
73
|
+
const h = health();
|
|
74
|
+
return h.readable ? null : (h.reason || 'store-unreadable');
|
|
39
75
|
}
|
|
40
76
|
|
|
41
77
|
function save() {
|
|
78
|
+
// Difensivo: mai sovrascrivere uno store illeggibile. Le mutazioni hanno
|
|
79
|
+
// gia' il guard, ma questo impedisce un overwrite se una via lo saltasse.
|
|
80
|
+
if (unreadableGuard()) return;
|
|
42
81
|
// prune: mai piu' di MAX_KEEP; si scartano prima gli answered piu' vecchi.
|
|
43
82
|
const list = load();
|
|
44
83
|
if (list.length > MAX_KEEP) {
|
|
@@ -90,6 +129,8 @@ function createAsksStore(opts = {}) {
|
|
|
90
129
|
// riga resta nello storico marcata, quindi non ricompare a un reload e non
|
|
91
130
|
// torna risponibile.
|
|
92
131
|
function closeImported({ ownerId, ownerAskId, outcome }) {
|
|
132
|
+
const ur = unreadableGuard();
|
|
133
|
+
if (ur) return { ok: false, reason: 'store-unreadable', changed: false, ask: null };
|
|
93
134
|
const ask = findImported(ownerId, ownerAskId);
|
|
94
135
|
if (!ask) return { ok: true, changed: false, ask: null };
|
|
95
136
|
if (ask.answered || ask.dismissed) return { ok: true, changed: false, ask };
|
|
@@ -101,6 +142,8 @@ function createAsksStore(opts = {}) {
|
|
|
101
142
|
}
|
|
102
143
|
|
|
103
144
|
function create({ question, options, session, ownerId, ownerAskId, originNode, originCell }) {
|
|
145
|
+
const ur = unreadableGuard();
|
|
146
|
+
if (ur) return { ok: false, reason: 'store-unreadable', error: `store illeggibile (${ur}): creazione rifiutata` };
|
|
104
147
|
const v = validate({ question, options });
|
|
105
148
|
if (!v.ok) return { ok: false, reason: 'invalid', error: v.error };
|
|
106
149
|
// DEDUP: la stessa domanda puo' arrivare due volte allo stesso nodo (import
|
|
@@ -174,6 +217,8 @@ function createAsksStore(opts = {}) {
|
|
|
174
217
|
const answering = new Set();
|
|
175
218
|
|
|
176
219
|
function claim(id) {
|
|
220
|
+
const ur = unreadableGuard();
|
|
221
|
+
if (ur) return { ok: false, reason: 'store-unreadable' };
|
|
177
222
|
const ask = get(id);
|
|
178
223
|
if (!ask) return { ok: false, reason: 'unknown' };
|
|
179
224
|
// Un ask dismissato non e' piu' risponibile: senza questa guardia la
|
|
@@ -197,6 +242,8 @@ function createAsksStore(opts = {}) {
|
|
|
197
242
|
|
|
198
243
|
// Commit: paste riuscito -> answered persistito, claim rilasciato.
|
|
199
244
|
function commit(id, text) {
|
|
245
|
+
const ur = unreadableGuard();
|
|
246
|
+
if (ur) return false;
|
|
200
247
|
const ask = get(id);
|
|
201
248
|
answering.delete(id);
|
|
202
249
|
if (!ask || ask.answered) return false;
|
|
@@ -214,6 +261,8 @@ function createAsksStore(opts = {}) {
|
|
|
214
261
|
// volte non e' un errore. Un ask dismissato non e' piu' open (non appare in
|
|
215
262
|
// list({open:true})/openCount) ma resta nello storico (list({open:false})).
|
|
216
263
|
function dismiss(id) {
|
|
264
|
+
const ur = unreadableGuard();
|
|
265
|
+
if (ur) return { ok: false, reason: 'store-unreadable' };
|
|
217
266
|
const ask = get(id);
|
|
218
267
|
if (!ask) return { ok: false, reason: 'unknown' };
|
|
219
268
|
if (answering.has(id)) return { ok: false, reason: 'answering' };
|
|
@@ -230,6 +279,8 @@ function createAsksStore(opts = {}) {
|
|
|
230
279
|
// `allow-new-attempt` leaves it open. Both advance the revision, so the token
|
|
231
280
|
// the operator reconciled with can never be reused on the same generation.
|
|
232
281
|
function markReconciled(id, decision) {
|
|
282
|
+
const ur = unreadableGuard();
|
|
283
|
+
if (ur) return { ok: false, reason: 'store-unreadable' };
|
|
233
284
|
const ask = get(id);
|
|
234
285
|
if (!ask) return { ok: false, reason: 'unknown' };
|
|
235
286
|
answering.delete(id);
|
|
@@ -250,7 +301,7 @@ function createAsksStore(opts = {}) {
|
|
|
250
301
|
return commit(id, text);
|
|
251
302
|
}
|
|
252
303
|
|
|
253
|
-
return { create, get, list, openCount, isImported, findImported, closeImported, claim, release, commit, markAnswered, markReconciled, isAnswering, dismiss, validate, filePath, MAX_OPEN, MAX_OPEN_IMPORTED };
|
|
304
|
+
return { create, get, list, openCount, isImported, findImported, closeImported, claim, release, commit, markAnswered, markReconciled, isAnswering, dismiss, validate, health, filePath, MAX_OPEN, MAX_OPEN_IMPORTED };
|
|
254
305
|
}
|
|
255
306
|
|
|
256
307
|
module.exports = { createAsksStore };
|