@materializecms/cli 0.8.0 → 0.9.0
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 +34 -0
- package/package.json +10 -5
- package/src/commands.d.ts +1 -1
- package/src/commands.js +105 -66
- package/src/en/index.d.ts +5 -0
- package/src/en/index.js +7 -0
- package/src/en/supervisor.d.ts +2 -0
- package/src/en/supervisor.js +11 -0
- package/src/en/terminal.d.ts +2 -0
- package/src/en/terminal.js +93 -0
- package/src/host.d.ts +8 -0
- package/src/host.js +12 -0
- package/src/lang.d.ts +1 -0
- package/src/lang.js +9 -0
- package/src/site.d.ts +15 -1
- package/src/site.js +69 -7
- package/src/supervisor-t.d.ts +1 -0
- package/src/supervisor-t.js +11 -0
- package/src/supervisor.js +9 -8
- package/src/system-lang.d.ts +7 -0
- package/src/system-lang.js +7 -0
- package/src/t.d.ts +2 -0
- package/src/t.js +4 -0
- package/src/updates.js +28 -22
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,40 @@ Ogni versione, dalla più recente. Le novità in arrivo si scrivono sotto «Pros
|
|
|
4
4
|
|
|
5
5
|
## Prossima versione
|
|
6
6
|
|
|
7
|
+
## 0.9.0 — 2026-11-10
|
|
8
|
+
|
|
9
|
+
- **Da sapere se aggiorni.** `materialize dev` si raggiunge solo dal computer (`--host 0.0.0.0` per il telefono). Online, «Password dimenticata?» manda il link solo se è configurato l'indirizzo del sito (`site.url` o `MATERIALIZE_SITE_URL`). I webhook dei moduli verso indirizzi interni vengono rifiutati (`MATERIALIZE_PRIVATE_WEBHOOKS=1` per un servizio sullo stesso server). `getEntries` non restituisce più le pagine modello (`{ templates: true }` per averle). Il pannello parte nella lingua del browser finché ognuno non ne sceglie una. Cambiando il server dell'AI, dell'email o dei backup esterni, la chiave salvata va scritta di nuovo. Il terminale parla la lingua del sistema (`MATERIALIZE_LANG=it` per l'italiano).
|
|
10
|
+
- **Il pannello, dal controllo generale.** Da telefono si pubblica: il pulsante resta visibile, una freccia riporta all'elenco e le pagine si aprono in modalità Contenuto; annulla, ripristina, tema e cronologia sono in «Altre azioni». «Pubblicato» non sembra più un pulsante spento. Impostazioni → Sito non mostra più comandi che lì non servono.
|
|
11
|
+
- **Coerenza del nuovo aspetto**: menu delle impostazioni a pillole con una larghezza e un titolo uguali in ogni scheda; dettaglio dei media, newsletter, persone e campi su carte arrotondate; pulsanti, selettori e filtri a pillola ovunque; elenchi allineati alla loro intestazione.
|
|
12
|
+
- **Editor**: i selettori del pannello delle proprietà vanno su due righe invece di accavallare le etichette; la scheda Aggiungi mostra i nomi interi; nel pannello Contenuto le voci del menu si chiamano «Voce del menu»; i due «⋯» della tela hanno icone diverse.
|
|
13
|
+
- **Newsletter**: impostazioni del mittente in una scheda **Impostazioni** propria; «Invia ora» dice perché non si può inviare; «Esporta» spento senza iscritti; i nomi delle sezioni di iscrizione seguono la lingua del pannello.
|
|
14
|
+
- **Piccole cose**: badge dei messaggi nuovi anche su **Impostazioni** nella barra laterale; etichetta «Lingua dei contenuti»; la chiave dei webhook è nascosta, con i pulsanti per mostrarla e copiarla; un quarto tono della voce prende il posto del primo; nel registro si vede quale documento è stato «preso»; pochi accessi falliti non sono più un avviso; il menu ⋯ delle righe si vede anche sui touch; «Vai al contenuto» anche nel pannello.
|
|
15
|
+
- **Accessibilità e lingua**: il campo password dell'accesso ha la sua etichetta; i controlli del pannello delle proprietà hanno un nome per i lettori di schermo; la lingua scelta nella pagina di accesso non si perde più entrando; cambiando lingua si ricaricano i testi del server; caricamenti ed export CSV seguono la lingua di chi li fa; le notifiche dei moduli al proprietario partono nella sua lingua.
|
|
16
|
+
- **Il sito, dal controllo generale.** Un'immagine o un video vuoti non mostrano più il riquadro dell'editor sul sito. Le pagine modello non finiscono più nei menu (`getEntries` le lascia fuori; anche le loro traduzioni non hanno più indirizzo né sitemap). I link del menu condiviso a una sezione che non è nella pagina portano alla sezione nella home della stessa lingua.
|
|
17
|
+
- **Accessibilità del sito**: link «Vai al contenuto» quando la pagina comincia con un menu; il menu su telefono si apre da tastiera portando il focus sulle voci e si chiude con Esc; le etichette del pulsante sono nella lingua della pagina; dopo lo scorrimento verso una sezione la tastiera riparte da lì; le scene WebGL decorative non vengono annunciate; i moduli suggeriscono nome, email e telefono.
|
|
18
|
+
- **Prestazioni**: le immagini dicono al browser quanto spazio occupano (niente più versione da 1920 px per una scheda piccola) e la prima della pagina si carica subito; le scene WebGL partono solo quando sono vicine allo schermo e visibili, quindi su telefono un modello 3D nascosto non viene scaricato.
|
|
19
|
+
- **SEO**: i siti nuovi creati con `create-materialize` usano il componente `Seo` (descrizioni, anteprime social, dati strutturati, lingue) e un `<main>` che lascia fuori menu e piè di pagina; una pagina senza descrizione usa il primo paragrafo; le briciole di pane partono dalla home e non puntano più a pagine inesistenti; gli articoli sono di tipo «articolo» senza doverlo scegliere; `og:locale` nella forma `it_IT`.
|
|
20
|
+
- **Sicurezza del sito**: tutte le pagine, gli errori e i reindirizzamenti hanno le intestazioni di sicurezza (nosniff, referrer, nessun inserimento in frame di altri siti, HSTS su HTTPS); le immagini della libreria si possono usare nelle anteprime social e nei lettori di feed; un indirizzo malformato riceve un 400 invece di un errore del server. In sviluppo il ricaricamento automatico funziona anche da `127.0.0.1` e dalla rete locale, e i file di configurazione del progetto non sono leggibili dal browser.
|
|
21
|
+
- **Sicurezza, dal controllo generale.** Le pagine salvate vengono ripulite prima di diventare HTML: un tipo di elemento, un valore CSS, un'icona SVG o una copia di un componente non possono più portare codice nel sito (prima un editor poteva farlo, e lo script sarebbe girato anche per gli amministratori che aprivano la pagina). Le icone SVG personalizzate vengono ricostruite da zero con i soli elementi da disegno.
|
|
22
|
+
- Le copie dei componenti cambiano solo i propri contenuti; ripristinare una versione segue le stesse regole del salvataggio; la risposta automatica dei moduli la cambia solo un amministratore, come destinatari e webhook.
|
|
23
|
+
- Gli indirizzi interni sono bloccati anche nelle forme IPv6 e dopo un cambio di DNS, sia nelle importazioni da URL sia nei webhook dei moduli (`MATERIALIZE_PRIVATE_WEBHOOKS=1` per un servizio sullo stesso server).
|
|
24
|
+
- Limiti di dimensione sulle richieste al server e sulle password; più limiti ai messaggi dei moduli per indirizzo; gli allegati dei messaggi di spam non vengono salvati.
|
|
25
|
+
- «Password dimenticata?» online usa solo l'indirizzo configurato del sito (senza, il link non parte e Impostazioni → Sicurezza lo segnala) e risponde sempre negli stessi tempi; il reset con il link chiude le sessioni e revoca i token personali.
|
|
26
|
+
- L'API pubblica dei contenuti non mostra più destinatari, webhook e risposte automatiche dei moduli. Il cookie di sessione è `Secure` anche dietro un proxy HTTPS. Le prove di invio della newsletter sono limitate. Cambiando il server dell'AI, dell'email o dei backup esterni, la chiave salvata va scritta di nuovo.
|
|
27
|
+
- **Avviso di aggiornamento**: quando esce una nuova versione di Materialize, gli amministratori vedono in alto a destra una pillola con il numero della versione (al passaggio del mouse, le prime novità) e una scheda **Aggiornamento** nella Panoramica, che portano a Impostazioni → Aggiornamenti. La pillola si chiude e torna con la versione successiva. Si vede anche in sviluppo, dove si aggiorna dal terminale.
|
|
28
|
+
- **Scorrimento fluido verso le sezioni**: i link a una sezione della stessa pagina, come le voci del menu, non saltano più di colpo: la pagina scorre in modo morbido (al massimo un secondo) e la sezione si ferma sotto il menu fisso, anche se intanto finiscono di caricarsi immagini più in alto. La rotella o un tocco fermano lo scorrimento; chi ha chiesto meno movimento vede il salto di sempre.
|
|
29
|
+
- **Materialize in inglese, completo.** Oltre al pannello, sono in inglese i messaggi del server, le email a chi ha un account, i controlli SEO, di accessibilità e di prestazioni, i nomi delle sezioni della libreria e degli stili, il terminale (`materialize` e `create-materialize`), la guida (`docs/guide`) e il README (`README.en.md`).
|
|
30
|
+
- Ognuno sceglie la lingua per sé, nel profilo o nella pagina di accesso; prima di scegliere, il pannello segue la lingua del browser. Due persone possono usare lo stesso sito una in italiano e una in inglese: il server risponde a ognuna nella sua lingua, e le email partono nella lingua del profilo di chi le riceve.
|
|
31
|
+
- Il terminale parla la lingua del sistema; `MATERIALIZE_LANG=it` o `en` la sceglie a mano.
|
|
32
|
+
- Restano nella lingua del sito i testi che vedono i visitatori: pagine, moduli, banner dei cookie, newsletter.
|
|
33
|
+
- Anche i testi che il server salva per dopo si leggono nella lingua di chi guarda: motivi dello spam, errori delle pubblicazioni programmate, dei rilasci e delle newsletter, e gli stessi messaggi nel registro attività. Quelli salvati prima di questa versione restano come sono.
|
|
34
|
+
- **Pannello in inglese, terza parte: tutto il resto del pannello.** Documenti, liste e rilasci, ricerca ⌘K, campi e libreria media, newsletter, persone e verifica in due passaggi, tutte le impostazioni, l'installazione iniziale (con il selettore della lingua) e i tour.
|
|
35
|
+
- **Pannello in inglese, seconda parte: l'editor.** Tela, livelli, libreria, stili, controlli, pannello delle proprietà, animazioni, interazioni, assistente, commenti, tour e scorciatoie, esportazione e modalità Design delle raccolte a blocchi. I nuovi elementi prendono nome e testo segnaposto nella lingua del pannello.
|
|
36
|
+
- **`materialize dev` resta sul tuo computer**: prima si poteva raggiungere da tutta la rete, e su una Wi-Fi condivisa qualcun altro avrebbe potuto aprire un sito nuovo e crearne l'amministratore. Per provarlo dal telefono: `--host 0.0.0.0`. `materialize start` non cambia.
|
|
37
|
+
- **Nuovo aspetto del pannello**: menu, area di lavoro e pannelli dell'editor sono superfici arrotondate su un fondo più scuro; pulsanti, filtri e selettori sono a pillola.
|
|
38
|
+
- Ricerca, tema e profilo si sono spostati in alto a destra, in un incavo dell'area di lavoro (sul telefono, nella barra in alto). La lingua dei contenuti resta in basso a sinistra.
|
|
39
|
+
- Nell'editor i comandi sopra la tela stanno in tre incavi del suo bordo, e lo zoom è una pillola in basso a destra sulla tela. Sulle tele strette i comandi meno usati passano in un menu ⋯, così la barra non viene tagliata.
|
|
40
|
+
|
|
7
41
|
## 0.8.0 — 2026-11-07
|
|
8
42
|
|
|
9
43
|
- **Un documento alla volta**: se un'altra persona sta modificando la pagina che apri, la vedi in sola lettura con chi ci lavora e da quanto, e puoi prendere il controllo. Nessuno sovrascrive più il lavoro di un altro senza saperlo, nemmeno da due schede.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@materializecms/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Il comando materialize: sviluppo, produzione con aggiornamenti dal pannello, generazione del sito, backup, MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@materializecms/astro": "0.
|
|
25
|
-
"@materializecms/core": "0.
|
|
26
|
-
"@materializecms/db": "0.
|
|
27
|
-
"@materializecms/server": "0.
|
|
24
|
+
"@materializecms/astro": "0.9.0",
|
|
25
|
+
"@materializecms/core": "0.9.0",
|
|
26
|
+
"@materializecms/db": "0.9.0",
|
|
27
|
+
"@materializecms/server": "0.9.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "1.32.2"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"changelog": [
|
|
45
|
+
{
|
|
46
|
+
"version": "0.9.0",
|
|
47
|
+
"date": "2026-11-10",
|
|
48
|
+
"notes": "- **Da sapere se aggiorni.** `materialize dev` si raggiunge solo dal computer (`--host 0.0.0.0` per il telefono). Online, «Password dimenticata?» manda il link solo se è configurato l'indirizzo del sito (`site.url` o `MATERIALIZE_SITE_URL`). I webhook dei moduli verso indirizzi interni vengono rifiutati (`MATERIALIZE_PRIVATE_WEBHOOKS=1` per un servizio sullo stesso server). `getEntries` non restituisce più le pagine modello (`{ templates: true }` per averle). Il pannello parte nella lingua del browser finché ognuno non ne sceglie una. Cambiando il server dell'AI, dell'email o dei backup esterni, la chiave salvata va scritta di nuovo. Il terminale parla la lingua del sistema (`MATERIALIZE_LANG=it` per l'italiano).\n- **Il pannello, dal controllo generale.** Da telefono si pubblica: il pulsante resta visibile, una freccia riporta all'elenco e le pagine si aprono in modalità Contenuto; annulla, ripristina, tema e cronologia sono in «Altre azioni». «Pubblicato» non sembra più un pulsante spento. Impostazioni → Sito non mostra più comandi che lì non servono.\n- **Coerenza del nuovo aspetto**: menu delle impostazioni a pillole con una larghezza e un titolo uguali in ogni scheda; dettaglio dei media, newsletter, persone e campi su carte arrotondate; pulsanti, selettori e filtri a pillola ovunque; elenchi allineati alla loro intestazione.\n- **Editor**: i selettori del pannello delle proprietà vanno su due righe invece di accavallare le etichette; la scheda Aggiungi mostra i nomi interi; nel pannello Contenuto le voci del menu si chiamano «Voce del menu»; i due «⋯» della tela hanno icone diverse.\n- **Newsletter**: impostazioni del mittente in una scheda **Impostazioni** propria; «Invia ora» dice perché non si può inviare; «Esporta» spento senza iscritti; i nomi delle sezioni di iscrizione seguono la lingua del pannello.\n- **Piccole cose**: badge dei messaggi nuovi anche su **Impostazioni** nella barra laterale; etichetta «Lingua dei contenuti»; la chiave dei webhook è nascosta, con i pulsanti per mostrarla e copiarla; un quarto tono della voce prende il posto del primo; nel registro si vede quale documento è stato «preso»; pochi accessi falliti non sono più un avviso; il menu ⋯ delle righe si vede anche sui touch; «Vai al contenuto» anche nel pannello.\n- **Accessibilità e lingua**: il campo password dell'accesso ha la sua etichetta; i controlli del pannello delle proprietà hanno un nome per i lettori di schermo; la lingua scelta nella pagina di accesso non si perde più entrando; cambiando lingua si ricaricano i testi del server; caricamenti ed export CSV seguono la lingua di chi li fa; le notifiche dei moduli al proprietario partono nella sua lingua.\n- **Il sito, dal controllo generale.** Un'immagine o un video vuoti non mostrano più il riquadro dell'editor sul sito. Le pagine modello non finiscono più nei menu (`getEntries` le lascia fuori; anche le loro traduzioni non hanno più indirizzo né sitemap). I link del menu condiviso a una sezione che non è nella pagina portano alla sezione nella home della stessa lingua.\n- **Accessibilità del sito**: link «Vai al contenuto» quando la pagina comincia con un menu; il menu su telefono si apre da tastiera portando il focus sulle voci e si chiude con Esc; le etichette del pulsante sono nella lingua della pagina; dopo lo scorrimento verso una sezione la tastiera riparte da lì; le scene WebGL decorative non vengono annunciate; i moduli suggeriscono nome, email e telefono.\n- **Prestazioni**: le immagini dicono al browser quanto spazio occupano (niente più versione da 1920 px per una scheda piccola) e la prima della pagina si carica subito; le scene WebGL partono solo quando sono vicine allo schermo e visibili, quindi su telefono un modello 3D nascosto non viene scaricato.\n- **SEO**: i siti nuovi creati con `create-materialize` usano il componente `Seo` (descrizioni, anteprime social, dati strutturati, lingue) e un `<main>` che lascia fuori menu e piè di pagina; una pagina senza descrizione usa il primo paragrafo; le briciole di pane partono dalla home e non puntano più a pagine inesistenti; gli articoli sono di tipo «articolo» senza doverlo scegliere; `og:locale` nella forma `it_IT`.\n- **Sicurezza del sito**: tutte le pagine, gli errori e i reindirizzamenti hanno le intestazioni di sicurezza (nosniff, referrer, nessun inserimento in frame di altri siti, HSTS su HTTPS); le immagini della libreria si possono usare nelle anteprime social e nei lettori di feed; un indirizzo malformato riceve un 400 invece di un errore del server. In sviluppo il ricaricamento automatico funziona anche da `127.0.0.1` e dalla rete locale, e i file di configurazione del progetto non sono leggibili dal browser.\n- **Sicurezza, dal controllo generale.** Le pagine salvate vengono ripulite prima di diventare HTML: un tipo di elemento, un valore CSS, un'icona SVG o una copia di un componente non possono più portare codice nel sito (prima un editor poteva farlo, e lo script sarebbe girato anche per gli amministratori che aprivano la pagina). Le icone SVG personalizzate vengono ricostruite da zero con i soli elementi da disegno.\n- Le copie dei componenti cambiano solo i propri contenuti; ripristinare una versione segue le stesse regole del salvataggio; la risposta automatica dei moduli la cambia solo un amministratore, come destinatari e webhook.\n- Gli indirizzi interni sono bloccati anche nelle forme IPv6 e dopo un cambio di DNS, sia nelle importazioni da URL sia nei webhook dei moduli (`MATERIALIZE_PRIVATE_WEBHOOKS=1` per un servizio sullo stesso server).\n- Limiti di dimensione sulle richieste al server e sulle password; più limiti ai messaggi dei moduli per indirizzo; gli allegati dei messaggi di spam non vengono salvati.\n- «Password dimenticata?» online usa solo l'indirizzo configurato del sito (senza, il link non parte e Impostazioni → Sicurezza lo segnala) e risponde sempre negli stessi tempi; il reset con il link chiude le sessioni e revoca i token personali.\n- L'API pubblica dei contenuti non mostra più destinatari, webhook e risposte automatiche dei moduli. Il cookie di sessione è `Secure` anche dietro un proxy HTTPS. Le prove di invio della newsletter sono limitate. Cambiando il server dell'AI, dell'email o dei backup esterni, la chiave salvata va scritta di nuovo.\n- **Avviso di aggiornamento**: quando esce una nuova versione di Materialize, gli amministratori vedono in alto a destra una pillola con il numero della versione (al passaggio del mouse, le prime novità) e una scheda **Aggiornamento** nella Panoramica, che portano a Impostazioni → Aggiornamenti. La pillola si chiude e torna con la versione successiva. Si vede anche in sviluppo, dove si aggiorna dal terminale.\n- **Scorrimento fluido verso le sezioni**: i link a una sezione della stessa pagina, come le voci del menu, non saltano più di colpo: la pagina scorre in modo morbido (al massimo un secondo) e la sezione si ferma sotto il menu fisso, anche se intanto finiscono di caricarsi immagini più in alto. La rotella o un tocco fermano lo scorrimento; chi ha chiesto meno movimento vede il salto di sempre.\n- **Materialize in inglese, completo.** Oltre al pannello, sono in inglese i messaggi del server, le email a chi ha un account, i controlli SEO, di accessibilità e di prestazioni, i nomi delle sezioni della libreria e degli stili, il terminale (`materialize` e `create-materialize`), la guida (`docs/guide`) e il README (`README.en.md`).\n- Ognuno sceglie la lingua per sé, nel profilo o nella pagina di accesso; prima di scegliere, il pannello segue la lingua del browser. Due persone possono usare lo stesso sito una in italiano e una in inglese: il server risponde a ognuna nella sua lingua, e le email partono nella lingua del profilo di chi le riceve.\n- Il terminale parla la lingua del sistema; `MATERIALIZE_LANG=it` o `en` la sceglie a mano.\n- Restano nella lingua del sito i testi che vedono i visitatori: pagine, moduli, banner dei cookie, newsletter.\n- Anche i testi che il server salva per dopo si leggono nella lingua di chi guarda: motivi dello spam, errori delle pubblicazioni programmate, dei rilasci e delle newsletter, e gli stessi messaggi nel registro attività. Quelli salvati prima di questa versione restano come sono.\n- **Pannello in inglese, terza parte: tutto il resto del pannello.** Documenti, liste e rilasci, ricerca ⌘K, campi e libreria media, newsletter, persone e verifica in due passaggi, tutte le impostazioni, l'installazione iniziale (con il selettore della lingua) e i tour.\n- **Pannello in inglese, seconda parte: l'editor.** Tela, livelli, libreria, stili, controlli, pannello delle proprietà, animazioni, interazioni, assistente, commenti, tour e scorciatoie, esportazione e modalità Design delle raccolte a blocchi. I nuovi elementi prendono nome e testo segnaposto nella lingua del pannello.\n- **`materialize dev` resta sul tuo computer**: prima si poteva raggiungere da tutta la rete, e su una Wi-Fi condivisa qualcun altro avrebbe potuto aprire un sito nuovo e crearne l'amministratore. Per provarlo dal telefono: `--host 0.0.0.0`. `materialize start` non cambia.\n- **Nuovo aspetto del pannello**: menu, area di lavoro e pannelli dell'editor sono superfici arrotondate su un fondo più scuro; pulsanti, filtri e selettori sono a pillola.\n- Ricerca, tema e profilo si sono spostati in alto a destra, in un incavo dell'area di lavoro (sul telefono, nella barra in alto). La lingua dei contenuti resta in basso a sinistra.\n- Nell'editor i comandi sopra la tela stanno in tre incavi del suo bordo, e lo zoom è una pillola in basso a destra sulla tela. Sulle tele strette i comandi meno usati passano in un menu ⋯, così la barra non viene tagliata."
|
|
49
|
+
},
|
|
45
50
|
{
|
|
46
51
|
"version": "0.8.0",
|
|
47
52
|
"date": "2026-11-07",
|
package/src/commands.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import './lang.ts';
|
package/src/commands.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import './lang.js';
|
|
1
2
|
import { randomBytes } from 'node:crypto';
|
|
2
3
|
import { watch } from 'node:fs';
|
|
3
4
|
import { copyFile, mkdir, open, readFile, rm, writeFile } from 'node:fs/promises';
|
|
@@ -6,41 +7,61 @@ import { styleText } from 'node:util';
|
|
|
6
7
|
import { resolveRedirect } from '@materializecms/core/seo';
|
|
7
8
|
import { createActivityStore, createAuthStore, createContentStore, createPreviewLinkStore, createSettingsStore, openDatabase, } from '@materializecms/db';
|
|
8
9
|
import { apiCaller, createFonts, createMcpServer, createMediaFiles, decryptTree, extractTarGz, parseRecoveryKey, restoreMedia, startServer, VERSION, } from '@materializecms/server';
|
|
10
|
+
import { listenHost, onlyThisComputer } from './host.js';
|
|
9
11
|
import { findConfig, loadConfig } from './load-config.js';
|
|
10
12
|
import { buildSite, exportFonts, exportMedia, hasSite, ProductionSite, startDevSite } from './site.js';
|
|
11
13
|
import { RESTART } from './supervisor.js';
|
|
14
|
+
import { t } from './t.js';
|
|
12
15
|
import { createUpdates } from './updates.js';
|
|
13
16
|
const dim = (s) => styleText('dim', s);
|
|
14
17
|
const accent = (s) => styleText(['bold', 'blue'], s);
|
|
15
18
|
const ok = (s) => styleText('green', s);
|
|
16
19
|
const fail = (s) => styleText('red', s);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
20
|
+
/** Wraps a description at the help's width, aligned under its column. */
|
|
21
|
+
function wrap(text, width = 78) {
|
|
22
|
+
const lines = [];
|
|
23
|
+
let line = '';
|
|
24
|
+
for (const word of text.split(' ')) {
|
|
25
|
+
if (line && line.length + word.length + 1 > width) {
|
|
26
|
+
lines.push(line);
|
|
27
|
+
line = word;
|
|
28
|
+
}
|
|
29
|
+
else
|
|
30
|
+
line = line ? `${line} ${word}` : word;
|
|
31
|
+
}
|
|
32
|
+
return [...lines, line];
|
|
33
|
+
}
|
|
34
|
+
/** One entry of the help: the command (on its own line when long), then its description. */
|
|
35
|
+
function row(command, text) {
|
|
36
|
+
const pad = ' '.repeat(23);
|
|
37
|
+
const [first = '', ...rest] = wrap(text);
|
|
38
|
+
const head = command.length > 19 ? ` ${command}\n${pad}${first}` : ` ${command.padEnd(21)}${first}`;
|
|
39
|
+
return [head, ...rest.map((l) => pad + l)].join('\n');
|
|
40
|
+
}
|
|
41
|
+
function help() {
|
|
42
|
+
return [
|
|
43
|
+
'',
|
|
44
|
+
`${accent('materialize')} ${dim(VERSION)}`,
|
|
45
|
+
'',
|
|
46
|
+
row('materialize dev', t('Avvia CMS e sito, ricarica lo schema a ogni salvataggio')),
|
|
47
|
+
row('materialize build', t('Genera il sito statico in dist/client, immagini comprese')),
|
|
48
|
+
row('materialize start', t('Avvia CMS e sito in produzione (il sito si rigenera a ogni pubblicazione); un supervisore lo tiene acceso e applica gli aggiornamenti dal pannello')),
|
|
49
|
+
row('materialize mcp', t('Server MCP su stdio per i client AI locali (serve il CMS avviato)')),
|
|
50
|
+
row('materialize restore <file>', t('Rimette un backup (content-….db, o l’archivio completo materialize-….tar.gz); il server va fermato')),
|
|
51
|
+
row('materialize reset-password <email>', t('Un link per una nuova password, valido un’ora: per chi non può riceverlo per email (per esempio l’unico amministratore, senza email configurata)')),
|
|
52
|
+
row(`materialize decrypt <${t('cartella o file')}> [--key <${t('chiave')}>]`, t('Decifra i backup scaricati dallo spazio esterno (S3, WebDAV); la chiave di ripristino si può dare anche con MATERIALIZE_BACKUP_KEY')),
|
|
53
|
+
row('materialize --help', t('Mostra questo aiuto')),
|
|
54
|
+
'',
|
|
55
|
+
t('Opzioni'),
|
|
56
|
+
row(`--port <${t('numero')}>`, t('Porta del server (predefinita: quella del config, 4321)')),
|
|
57
|
+
row(`--host <${t('indirizzo')}>`, t('Indirizzo su cui ascoltare. Con dev: solo questo computer (0.0.0.0 per provarlo dal telefono sulla stessa rete). Con start: tutti (127.0.0.1 dietro un proxy)')),
|
|
58
|
+
row(`--url <${t('indirizzo')}>`, t('Con mcp: il CMS a cui collegarsi (predefinito: http://localhost:<porta>)')),
|
|
59
|
+
row('--no-supervisor', t('Con start: senza supervisore (se il server lo tiene acceso un altro programma)')),
|
|
60
|
+
'',
|
|
61
|
+
t('Il server MCP usa un token personale (Impostazioni → AI e MCP) in MATERIALIZE_TOKEN.'),
|
|
62
|
+
'',
|
|
63
|
+
].join('\n');
|
|
64
|
+
}
|
|
44
65
|
/** Kept until the installation is done, so a restart (or the VPS installer, which reads it) keeps the same link. */
|
|
45
66
|
const INSTALL_CODE = '.materialize/install-code';
|
|
46
67
|
async function installCode(site) {
|
|
@@ -63,8 +84,8 @@ async function run(mode, args) {
|
|
|
63
84
|
const cwd = process.cwd();
|
|
64
85
|
const path = findConfig(cwd);
|
|
65
86
|
if (!path) {
|
|
66
|
-
console.error(fail('Non trovo materialize.config.ts in questa cartella.'));
|
|
67
|
-
console.error(dim('Crea un nuovo progetto con: npm create materialize@latest'));
|
|
87
|
+
console.error(fail(t('Non trovo materialize.config.ts in questa cartella.')));
|
|
88
|
+
console.error(dim(t('Crea un nuovo progetto con: npm create materialize@latest')));
|
|
68
89
|
process.exit(1);
|
|
69
90
|
}
|
|
70
91
|
let config;
|
|
@@ -78,7 +99,7 @@ async function run(mode, args) {
|
|
|
78
99
|
const withSite = hasSite(cwd);
|
|
79
100
|
if (mode === 'build') {
|
|
80
101
|
if (!withSite) {
|
|
81
|
-
console.error(fail('Non c’è un sito da generare: manca astro.config.ts in questa cartella.'));
|
|
102
|
+
console.error(fail(t('Non c’è un sito da generare: manca astro.config.ts in questa cartella.')));
|
|
82
103
|
process.exit(1);
|
|
83
104
|
}
|
|
84
105
|
const started = performance.now();
|
|
@@ -86,12 +107,12 @@ async function run(mode, args) {
|
|
|
86
107
|
const images = await exportMedia(`${cwd}/dist/client`, createMediaFiles(config.media.dir));
|
|
87
108
|
await exportFonts(`${cwd}/dist/client`, createFonts(join(dirname(resolve(config.media.dir)), 'fonts')));
|
|
88
109
|
const elapsed = Math.round(performance.now() - started);
|
|
89
|
-
console.log(` ${ok('✓')} Sito generato in dist/client ${dim(`(${images} immagini, ${elapsed} ms)`)}`);
|
|
110
|
+
console.log(` ${ok('✓')} ${t('Sito generato in dist/client')} ${dim(`(${t.n(images, 'un’immagine', '{n} immagini')}, ${elapsed} ms)`)}`);
|
|
90
111
|
process.exit(0);
|
|
91
112
|
}
|
|
92
113
|
const flag = (name) => (args.includes(name) ? args[args.indexOf(name) + 1] : undefined);
|
|
93
114
|
const port = Number(flag('--port') ?? config.server.port);
|
|
94
|
-
const host = flag('--host')
|
|
115
|
+
const host = listenHost(mode, flag('--host'), process.env.MATERIALIZE_HOST);
|
|
95
116
|
let current = config;
|
|
96
117
|
// The site handler is swapped in once Astro is ready; until then visitors get a short wait page.
|
|
97
118
|
let siteHandler = (_req, res) => {
|
|
@@ -158,7 +179,7 @@ async function run(mode, args) {
|
|
|
158
179
|
return resolveRedirect((await settings.get('redirects')) ?? [], path);
|
|
159
180
|
};
|
|
160
181
|
if (mode === 'dev') {
|
|
161
|
-
const site = await startDevSite(cwd, port + 1);
|
|
182
|
+
const site = await startDevSite(cwd, port + 1, host);
|
|
162
183
|
siteHandler = site.handle;
|
|
163
184
|
closeSite = site.close;
|
|
164
185
|
}
|
|
@@ -182,12 +203,21 @@ async function run(mode, args) {
|
|
|
182
203
|
console.log();
|
|
183
204
|
console.log(` ${accent('Materialize')} ${dim(VERSION)} ${config.site.name}`);
|
|
184
205
|
console.log();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
206
|
+
const rows = [
|
|
207
|
+
[withSite ? t('Sito') : t('Locale'), server.url],
|
|
208
|
+
[t('Pannello'), `${server.url}/admin`],
|
|
209
|
+
['API', `${server.url}/api/schema`],
|
|
210
|
+
['Database', config.database.url],
|
|
211
|
+
[t('Collezioni'), summary(config)],
|
|
212
|
+
];
|
|
213
|
+
const width = Math.max(12, ...rows.map(([label]) => label.length + 2));
|
|
214
|
+
for (const [label, value] of rows)
|
|
215
|
+
console.log(` ${dim(label.padEnd(width))}${value}`);
|
|
190
216
|
console.log();
|
|
217
|
+
if (mode === 'dev' && onlyThisComputer(host)) {
|
|
218
|
+
console.log(dim(` ${t('Raggiungibile solo da questo computer. Per provarlo dal telefono: --host 0.0.0.0')}`));
|
|
219
|
+
console.log();
|
|
220
|
+
}
|
|
191
221
|
if ((await createAuthStore(server.database.db).countUsers()) > 0) {
|
|
192
222
|
if (setupToken)
|
|
193
223
|
await rm(join(cwd, INSTALL_CODE), { force: true });
|
|
@@ -196,14 +226,15 @@ async function run(mode, args) {
|
|
|
196
226
|
const path = `/admin/${setupToken ? `?installa=${setupToken}` : ''}`;
|
|
197
227
|
// The public address when the site knows it (MATERIALIZE_SITE_URL in Docker or on a VPS).
|
|
198
228
|
const site = mode === 'start' ? config.site.url?.replace(/\/+$/, '') : undefined;
|
|
199
|
-
|
|
229
|
+
const label = t('Installazione');
|
|
230
|
+
console.log(` ${accent(label)} ${t('apri {url}', { url: `${site ?? server.url}${path}` })}`);
|
|
200
231
|
if (setupToken && !site)
|
|
201
|
-
console.log(dim(
|
|
232
|
+
console.log(dim(`${' '.repeat(label.length + 5)}(${t('online: lo stesso indirizzo dal tuo dominio, {url}', { url: `https://…${path}` })})`));
|
|
202
233
|
console.log();
|
|
203
234
|
}
|
|
204
235
|
if (mode === 'dev') {
|
|
205
236
|
let timer;
|
|
206
|
-
console.log(dim(` In ascolto delle modifiche a
|
|
237
|
+
console.log(dim(` ${t('In ascolto delle modifiche a {file}…', { file: relative(cwd, path) })}`));
|
|
207
238
|
watch(path, () => {
|
|
208
239
|
clearTimeout(timer);
|
|
209
240
|
timer = setTimeout(async () => {
|
|
@@ -211,10 +242,10 @@ async function run(mode, args) {
|
|
|
211
242
|
const next = await loadConfig(path);
|
|
212
243
|
server.reload(next);
|
|
213
244
|
current = next;
|
|
214
|
-
console.log(` ${ok('✓')} Schema ricaricato ${dim(`(${summary(next)})`)}`);
|
|
245
|
+
console.log(` ${ok('✓')} ${t('Schema ricaricato')} ${dim(`(${summary(next)})`)}`);
|
|
215
246
|
}
|
|
216
247
|
catch (error) {
|
|
217
|
-
console.log(` ${fail('✗')} Lo schema ha un problema, resto sulla versione precedente
|
|
248
|
+
console.log(` ${fail('✗')} ${t('Lo schema ha un problema, resto sulla versione precedente:')}`);
|
|
218
249
|
console.log(dim(describeError(error).replace(/^/gm, ' ')));
|
|
219
250
|
}
|
|
220
251
|
}, 120);
|
|
@@ -235,7 +266,7 @@ async function run(mode, args) {
|
|
|
235
266
|
async function mcp(args) {
|
|
236
267
|
const token = process.env.MATERIALIZE_TOKEN?.trim();
|
|
237
268
|
if (!token) {
|
|
238
|
-
console.error('Manca MATERIALIZE_TOKEN: crea un token in Impostazioni → AI e MCP e passalo al client.');
|
|
269
|
+
console.error(t('Manca MATERIALIZE_TOKEN: crea un token in Impostazioni → AI e MCP e passalo al client.'));
|
|
239
270
|
process.exit(1);
|
|
240
271
|
}
|
|
241
272
|
const flag = args.indexOf('--url');
|
|
@@ -248,7 +279,7 @@ async function mcp(args) {
|
|
|
248
279
|
const base = url.replace(/\/+$/, '');
|
|
249
280
|
const health = await fetch(`${base}/api/health`).catch(() => null);
|
|
250
281
|
if (!health?.ok) {
|
|
251
|
-
console.error(
|
|
282
|
+
console.error(t('Il CMS non risponde su {url}: avvialo con materialize dev (o start), oppure indica --url.', { url: base }));
|
|
252
283
|
process.exit(1);
|
|
253
284
|
}
|
|
254
285
|
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
@@ -266,8 +297,8 @@ async function restore(args) {
|
|
|
266
297
|
const path = findConfig(process.cwd());
|
|
267
298
|
if (!file || !path) {
|
|
268
299
|
console.error(fail(!file
|
|
269
|
-
? 'Indica il file del backup: materialize restore <file>'
|
|
270
|
-
: 'Non trovo materialize.config.ts in questa cartella.'));
|
|
300
|
+
? t('Indica il file del backup: materialize restore <file>')
|
|
301
|
+
: t('Non trovo materialize.config.ts in questa cartella.')));
|
|
271
302
|
process.exit(1);
|
|
272
303
|
}
|
|
273
304
|
const config = await loadConfig(path);
|
|
@@ -278,25 +309,25 @@ async function restore(args) {
|
|
|
278
309
|
await handle.close();
|
|
279
310
|
}
|
|
280
311
|
catch {
|
|
281
|
-
console.error(fail(
|
|
312
|
+
console.error(fail(t('Non trovo il file {file}', { file })));
|
|
282
313
|
process.exit(1);
|
|
283
314
|
}
|
|
284
315
|
// A SQLite file starts with "SQLite format 3", a .tar.gz with the gzip signature.
|
|
285
316
|
const archive = head[0] === 0x1f && head[1] === 0x8b;
|
|
286
317
|
if (!archive && head.toString('latin1') !== 'SQLite format 3\0') {
|
|
287
|
-
console.error(fail(
|
|
318
|
+
console.error(fail(t('{file} non è un backup: serve un content-….db o un materialize-….tar.gz.', { file })));
|
|
288
319
|
process.exit(1);
|
|
289
320
|
}
|
|
290
321
|
const local = config.database.url.startsWith('file:');
|
|
291
322
|
if (!local && !archive) {
|
|
292
|
-
console.error(fail('Il database è ospitato da un fornitore: ripristina il backup dal suo pannello.'));
|
|
323
|
+
console.error(fail(t('Il database è ospitato da un fornitore: ripristina il backup dal suo pannello.')));
|
|
293
324
|
process.exit(1);
|
|
294
325
|
}
|
|
295
326
|
const running = await fetch(`http://localhost:${config.server.port}/api/setup`, { signal: AbortSignal.timeout(1500) })
|
|
296
327
|
.then(() => true)
|
|
297
328
|
.catch(() => false);
|
|
298
329
|
if (running) {
|
|
299
|
-
console.error(fail(
|
|
330
|
+
console.error(fail(t('Il server è acceso sulla porta {port}: fermalo, poi ripeti il comando.', { port: config.server.port })));
|
|
300
331
|
process.exit(1);
|
|
301
332
|
}
|
|
302
333
|
const mediaDir = resolve(config.media.dir);
|
|
@@ -344,19 +375,27 @@ async function restore(args) {
|
|
|
344
375
|
});
|
|
345
376
|
const media = written.filter((n) => n.startsWith('media/')).length;
|
|
346
377
|
const uploads = written.filter((n) => n.startsWith('uploads/')).length;
|
|
347
|
-
|
|
348
|
-
|
|
378
|
+
const withDb = written.includes('content.db');
|
|
379
|
+
const fonts = written.length - media - uploads - (withDb ? 1 : 0);
|
|
380
|
+
console.log(` ${ok('✓')} ${t('Archivio ripristinato: {file}', { file: relative(process.cwd(), resolve(file)) })}`);
|
|
381
|
+
const parts = [
|
|
382
|
+
...(withDb ? ['Database'] : []),
|
|
383
|
+
t.n(media, 'un file dei media', '{n} file dei media'),
|
|
384
|
+
t.n(fonts, 'un file dei caratteri', '{n} file dei caratteri'),
|
|
385
|
+
t.n(uploads, 'un allegato dei moduli', '{n} allegati dei moduli'),
|
|
386
|
+
];
|
|
387
|
+
console.log(dim(` ${parts.join(', ')}.`));
|
|
349
388
|
}
|
|
350
389
|
else if (db) {
|
|
351
390
|
await copyFile(resolve(file), db);
|
|
352
391
|
// The images this database uses that are no longer in the library come back from the media copy.
|
|
353
392
|
const back = await restoreMedia(db, mediaDir, keys);
|
|
354
|
-
console.log(` ${ok('✓')} Backup ripristinato:
|
|
393
|
+
console.log(` ${ok('✓')} ${t('Backup ripristinato: {file}', { file: relative(process.cwd(), resolve(file)) })}`);
|
|
355
394
|
if (back)
|
|
356
|
-
console.log(dim(` ${back
|
|
395
|
+
console.log(dim(` ${t.n(back, 'Un’immagine è tornata dalla copia dei media.', '{n} immagini sono tornate dalla copia dei media.')}`));
|
|
357
396
|
}
|
|
358
397
|
if (kept)
|
|
359
|
-
console.log(dim(` Il database di prima è in
|
|
398
|
+
console.log(dim(` ${t('Il database di prima è in {file}, se servisse tornare indietro.', { file: relative(process.cwd(), kept) })}`));
|
|
360
399
|
}
|
|
361
400
|
/**
|
|
362
401
|
* A one-time link for a new password, printed here: the way back in for someone who cannot get it
|
|
@@ -371,8 +410,8 @@ async function resetPassword(args) {
|
|
|
371
410
|
const path = findConfig(process.cwd());
|
|
372
411
|
if (!email || !path) {
|
|
373
412
|
console.error(fail(!email
|
|
374
|
-
? 'Indica l’email: materialize reset-password <email>'
|
|
375
|
-
: 'Non trovo materialize.config.ts in questa cartella: lancia il comando dalla cartella del sito.'));
|
|
413
|
+
? t('Indica l’email: materialize reset-password <email>')
|
|
414
|
+
: t('Non trovo materialize.config.ts in questa cartella: lancia il comando dalla cartella del sito.')));
|
|
376
415
|
process.exit(1);
|
|
377
416
|
}
|
|
378
417
|
const config = await loadConfig(path);
|
|
@@ -381,7 +420,7 @@ async function resetPassword(args) {
|
|
|
381
420
|
const auth = createAuthStore(database.db);
|
|
382
421
|
const user = await auth.findForLogin(email);
|
|
383
422
|
if (!user) {
|
|
384
|
-
console.error(fail(
|
|
423
|
+
console.error(fail(t('Nessun account con l’email {email}.', { email })));
|
|
385
424
|
process.exitCode = 1;
|
|
386
425
|
return;
|
|
387
426
|
}
|
|
@@ -402,11 +441,11 @@ async function resetPassword(args) {
|
|
|
402
441
|
ip: 'server',
|
|
403
442
|
});
|
|
404
443
|
const base = config.site.url?.replace(/\/+$/, '') ?? `http://localhost:${config.server.port}`;
|
|
405
|
-
console.log(` ${ok('✓')} Link per una nuova password di
|
|
444
|
+
console.log(` ${ok('✓')} ${t('Link per una nuova password di {name} ({email}), valido un’ora:', { name: user.name, email: user.email })}`);
|
|
406
445
|
console.log();
|
|
407
446
|
console.log(` ${accent(`${base}/admin/_invite/${token}`)}`);
|
|
408
447
|
console.log();
|
|
409
|
-
console.log(dim('
|
|
448
|
+
console.log(dim(` ${t('Chi lo apre sceglie la password. La verifica in due passaggi, se attiva, resta.')}`));
|
|
410
449
|
}
|
|
411
450
|
finally {
|
|
412
451
|
database.close();
|
|
@@ -421,7 +460,7 @@ async function decrypt(args) {
|
|
|
421
460
|
const flag = args.indexOf('--key');
|
|
422
461
|
const source = args.find((a, i) => !a.startsWith('--') && (flag < 0 || i !== flag + 1));
|
|
423
462
|
if (!source) {
|
|
424
|
-
console.error(fail('Indica la cartella (o il file) da decifrare: materialize decrypt <cartella> --key <chiave>'));
|
|
463
|
+
console.error(fail(t('Indica la cartella (o il file) da decifrare: materialize decrypt <cartella> --key <chiave>')));
|
|
425
464
|
process.exit(1);
|
|
426
465
|
}
|
|
427
466
|
let text = flag >= 0 ? args[flag + 1] : process.env.MATERIALIZE_BACKUP_KEY;
|
|
@@ -436,7 +475,7 @@ async function decrypt(args) {
|
|
|
436
475
|
}
|
|
437
476
|
}
|
|
438
477
|
if (!text) {
|
|
439
|
-
console.error(fail('Manca la chiave di ripristino: --key mzk1-… oppure MATERIALIZE_BACKUP_KEY.'));
|
|
478
|
+
console.error(fail(t('Manca la chiave di ripristino: --key mzk1-… oppure MATERIALIZE_BACKUP_KEY.')));
|
|
440
479
|
process.exit(1);
|
|
441
480
|
}
|
|
442
481
|
let key;
|
|
@@ -449,14 +488,14 @@ async function decrypt(args) {
|
|
|
449
488
|
}
|
|
450
489
|
try {
|
|
451
490
|
const done = await decryptTree(resolve(source), key);
|
|
452
|
-
console.log(` ${ok('✓')} ${
|
|
491
|
+
console.log(` ${ok('✓')} ${t.n(done.length, 'Un file decifrato, accanto all’originale.', '{n} file decifrati, accanto agli originali.')}`);
|
|
453
492
|
const db = done
|
|
454
493
|
.filter((f) => /content-[^/\\]+\.db$/.test(f))
|
|
455
494
|
.sort()
|
|
456
495
|
.at(-1);
|
|
457
496
|
if (db)
|
|
458
497
|
// The shorter of the two ways to write the path.
|
|
459
|
-
console.log(dim(` Per rimettere il database (a server fermo): npx materialize restore ${[relative(process.cwd(), db), db].sort((x, y) => x.length - y.length)[0]}`));
|
|
498
|
+
console.log(dim(` ${t('Per rimettere il database (a server fermo): {command}', { command: `npx materialize restore ${[relative(process.cwd(), db), db].sort((x, y) => x.length - y.length)[0]}` })}`));
|
|
460
499
|
}
|
|
461
500
|
catch (error) {
|
|
462
501
|
console.error(fail(describeError(error)));
|
|
@@ -483,7 +522,7 @@ else if (command === '--version' || command === '-v') {
|
|
|
483
522
|
console.log(VERSION);
|
|
484
523
|
}
|
|
485
524
|
else {
|
|
486
|
-
console.log(
|
|
525
|
+
console.log(help());
|
|
487
526
|
if (command !== 'help' && command !== '--help' && command !== '-h')
|
|
488
527
|
process.exitCode = 1;
|
|
489
528
|
}
|
package/src/en/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { supervisor } from './supervisor.js';
|
|
2
|
+
import { terminal } from './terminal.js';
|
|
3
|
+
/**
|
|
4
|
+
* The English texts of @materializecms/cli, by area. Keys are the Italian texts exactly as written
|
|
5
|
+
* in the code (with their {placeholders}); a test checks that every text passed to t() is here.
|
|
6
|
+
*/
|
|
7
|
+
export const en = { ...terminal, ...supervisor };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The supervisor of `materialize start`: data only, so it can load without any Materialize package. */
|
|
2
|
+
export const supervisor = {
|
|
3
|
+
'La nuova versione non è stata scaricata': 'The new version was not downloaded',
|
|
4
|
+
'Non c’è una versione precedente': 'There is no previous version',
|
|
5
|
+
'avvio la {version}…': 'starting {version}…',
|
|
6
|
+
'Con la nuova versione il sito non si compila': 'The site does not build with the new version',
|
|
7
|
+
'La nuova versione non è partita in tempo': 'The new version did not start in time',
|
|
8
|
+
'La nuova versione si è fermata all’avvio (codice {code})': 'The new version stopped while starting (code {code})',
|
|
9
|
+
'{message}: torno alla {version}': '{message}: going back to {version}',
|
|
10
|
+
'Il server si è fermato (codice {code}): riparto tra {wait} s': 'The server stopped (code {code}): restarting in {wait} s',
|
|
11
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** What the terminal prints: `materialize` commands, the supervisor, site builds, and updates. */
|
|
2
|
+
export const terminal = {
|
|
3
|
+
// --help
|
|
4
|
+
'Avvia CMS e sito, ricarica lo schema a ogni salvataggio': 'Starts the CMS and the site, reloads the schema on every save',
|
|
5
|
+
'Genera il sito statico in dist/client, immagini comprese': 'Builds the static site into dist/client, images included',
|
|
6
|
+
'Avvia CMS e sito in produzione (il sito si rigenera a ogni pubblicazione); un supervisore lo tiene acceso e applica gli aggiornamenti dal pannello': 'Starts the CMS and the site in production (the site rebuilds on every publish); a supervisor keeps it running and applies the updates from the panel',
|
|
7
|
+
'Server MCP su stdio per i client AI locali (serve il CMS avviato)': 'MCP server on stdio for local AI clients (needs the CMS running)',
|
|
8
|
+
'Rimette un backup (content-….db, o l’archivio completo materialize-….tar.gz); il server va fermato': 'Restores a backup (content-….db, or the full materialize-….tar.gz archive); stop the server first',
|
|
9
|
+
'Un link per una nuova password, valido un’ora: per chi non può riceverlo per email (per esempio l’unico amministratore, senza email configurata)': 'A link to set a new password, valid for one hour: for someone who cannot get it by email (for example the only administrator, with no email set up)',
|
|
10
|
+
'cartella o file': 'folder or file',
|
|
11
|
+
chiave: 'key',
|
|
12
|
+
'Decifra i backup scaricati dallo spazio esterno (S3, WebDAV); la chiave di ripristino si può dare anche con MATERIALIZE_BACKUP_KEY': 'Decrypts the backups downloaded from offsite storage (S3, WebDAV); the recovery key can also be given with MATERIALIZE_BACKUP_KEY',
|
|
13
|
+
'Mostra questo aiuto': 'Shows this help',
|
|
14
|
+
Opzioni: 'Options',
|
|
15
|
+
numero: 'number',
|
|
16
|
+
'Porta del server (predefinita: quella del config, 4321)': 'Server port (default: the one in the config, 4321)',
|
|
17
|
+
indirizzo: 'address',
|
|
18
|
+
'Indirizzo su cui ascoltare. Con dev: solo questo computer (0.0.0.0 per provarlo dal telefono sulla stessa rete). Con start: tutti (127.0.0.1 dietro un proxy)': 'Address to listen on. With dev: this computer only (0.0.0.0 to try it from a phone on the same network). With start: all (127.0.0.1 behind a proxy)',
|
|
19
|
+
'Con mcp: il CMS a cui collegarsi (predefinito: http://localhost:<porta>)': 'With mcp: the CMS to connect to (default: http://localhost:<port>)',
|
|
20
|
+
'Con start: senza supervisore (se il server lo tiene acceso un altro programma)': 'With start: no supervisor (when another program keeps the server running)',
|
|
21
|
+
'Il server MCP usa un token personale (Impostazioni → AI e MCP) in MATERIALIZE_TOKEN.': 'The MCP server uses a personal token (Settings → AI and MCP) in MATERIALIZE_TOKEN.',
|
|
22
|
+
// dev, start, build
|
|
23
|
+
'Non trovo materialize.config.ts in questa cartella.': 'Cannot find materialize.config.ts in this folder.',
|
|
24
|
+
'Crea un nuovo progetto con: npm create materialize@latest': 'Create a new project with: npm create materialize@latest',
|
|
25
|
+
'Non c’è un sito da generare: manca astro.config.ts in questa cartella.': 'There is no site to build: astro.config.ts is missing from this folder.',
|
|
26
|
+
'Sito generato in dist/client': 'Site built in dist/client',
|
|
27
|
+
'un’immagine': '1 image',
|
|
28
|
+
'{n} immagini': '{n} images',
|
|
29
|
+
Sito: 'Site',
|
|
30
|
+
Locale: 'Local',
|
|
31
|
+
Pannello: 'Panel',
|
|
32
|
+
Collezioni: 'Collections',
|
|
33
|
+
'Raggiungibile solo da questo computer. Per provarlo dal telefono: --host 0.0.0.0': 'Reachable from this computer only. To try it from a phone: --host 0.0.0.0',
|
|
34
|
+
Installazione: 'Setup',
|
|
35
|
+
'apri {url}': 'open {url}',
|
|
36
|
+
'online: lo stesso indirizzo dal tuo dominio, {url}': 'online: the same address on your domain, {url}',
|
|
37
|
+
'In ascolto delle modifiche a {file}…': 'Watching {file} for changes…',
|
|
38
|
+
'Schema ricaricato': 'Schema reloaded',
|
|
39
|
+
'Lo schema ha un problema, resto sulla versione precedente:': 'The schema has a problem, keeping the previous version:',
|
|
40
|
+
'Sito aggiornato in {ms} ms': 'Site updated in {ms} ms',
|
|
41
|
+
'Il sito non si è aggiornato: {error}': 'The site was not updated: {error}',
|
|
42
|
+
// mcp
|
|
43
|
+
'Manca MATERIALIZE_TOKEN: crea un token in Impostazioni → AI e MCP e passalo al client.': 'MATERIALIZE_TOKEN is missing: create a token in Settings → AI and MCP and give it to the client.',
|
|
44
|
+
'Il CMS non risponde su {url}: avvialo con materialize dev (o start), oppure indica --url.': 'The CMS is not answering at {url}: start it with materialize dev (or start), or give --url.',
|
|
45
|
+
// restore
|
|
46
|
+
'Indica il file del backup: materialize restore <file>': 'Give the backup file: materialize restore <file>',
|
|
47
|
+
'Non trovo il file {file}': 'Cannot find the file {file}',
|
|
48
|
+
'{file} non è un backup: serve un content-….db o un materialize-….tar.gz.': '{file} is not a backup: it takes a content-….db or a materialize-….tar.gz.',
|
|
49
|
+
'Il database è ospitato da un fornitore: ripristina il backup dal suo pannello.': 'The database is hosted by a provider: restore the backup from its dashboard.',
|
|
50
|
+
'Il server è acceso sulla porta {port}: fermalo, poi ripeti il comando.': 'The server is running on port {port}: stop it, then run the command again.',
|
|
51
|
+
'Archivio ripristinato: {file}': 'Archive restored: {file}',
|
|
52
|
+
'un file dei media': '1 media file',
|
|
53
|
+
'{n} file dei media': '{n} media files',
|
|
54
|
+
'un file dei caratteri': '1 font file',
|
|
55
|
+
'{n} file dei caratteri': '{n} font files',
|
|
56
|
+
'un allegato dei moduli': '1 form attachment',
|
|
57
|
+
'{n} allegati dei moduli': '{n} form attachments',
|
|
58
|
+
'Backup ripristinato: {file}': 'Backup restored: {file}',
|
|
59
|
+
'Un’immagine è tornata dalla copia dei media.': '1 image came back from the media copy.',
|
|
60
|
+
'{n} immagini sono tornate dalla copia dei media.': '{n} images came back from the media copy.',
|
|
61
|
+
'Il database di prima è in {file}, se servisse tornare indietro.': 'The previous database is in {file}, in case you need to go back.',
|
|
62
|
+
// reset-password
|
|
63
|
+
'Indica l’email: materialize reset-password <email>': 'Give the email: materialize reset-password <email>',
|
|
64
|
+
'Non trovo materialize.config.ts in questa cartella: lancia il comando dalla cartella del sito.': 'Cannot find materialize.config.ts in this folder: run the command from the site’s folder.',
|
|
65
|
+
'Nessun account con l’email {email}.': 'No account with the email {email}.',
|
|
66
|
+
'Link per una nuova password di {name} ({email}), valido un’ora:': 'Link to a new password for {name} ({email}), valid for one hour:',
|
|
67
|
+
'Chi lo apre sceglie la password. La verifica in due passaggi, se attiva, resta.': 'Whoever opens it chooses the password. Two-step verification, if on, stays on.',
|
|
68
|
+
// decrypt
|
|
69
|
+
'Indica la cartella (o il file) da decifrare: materialize decrypt <cartella> --key <chiave>': 'Give the folder (or file) to decrypt: materialize decrypt <folder> --key <key>',
|
|
70
|
+
'Manca la chiave di ripristino: --key mzk1-… oppure MATERIALIZE_BACKUP_KEY.': 'The recovery key is missing: --key mzk1-… or MATERIALIZE_BACKUP_KEY.',
|
|
71
|
+
'Un file decifrato, accanto all’originale.': '1 file decrypted, next to the original.',
|
|
72
|
+
'{n} file decifrati, accanto agli originali.': '{n} files decrypted, next to the originals.',
|
|
73
|
+
'Per rimettere il database (a server fermo): {command}': 'To restore the database (with the server stopped): {command}',
|
|
74
|
+
// Updates from the panel
|
|
75
|
+
'npm non si avvia: {error}': 'npm does not start: {error}',
|
|
76
|
+
'npm si è fermato con un errore (codice {code})': 'npm stopped with an error (code {code})',
|
|
77
|
+
'In sviluppo (materialize dev) si aggiorna da terminale: gli aggiornamenti dal pannello funzionano con materialize start.': 'In development (materialize dev) you update from the terminal: updates from the panel work with materialize start.',
|
|
78
|
+
'Il server è avviato senza il suo supervisore: aggiorna da terminale, oppure avvialo con materialize start.': 'The server was started without its supervisor: update from the terminal, or start it with materialize start.',
|
|
79
|
+
'Materialize è collegato dal codice sorgente: si aggiorna con git.': 'Materialize is linked from its source code: update it with git.',
|
|
80
|
+
'Il package.json del sito non ha {name} tra le dipendenze.': 'The site’s package.json does not have {name} among its dependencies.',
|
|
81
|
+
'Il programma non può scrivere nella cartella del sito: aggiorna da terminale, con l’utente che la possiede.': 'The program cannot write to the site’s folder: update from the terminal, as the user who owns it.',
|
|
82
|
+
'Nessuna versione pubblicata da controllare.': 'No published version to check.',
|
|
83
|
+
'Non riesco a raggiungere il registro dei pacchetti: riprova più tardi.': 'Cannot reach the package registry: try again later.',
|
|
84
|
+
'Preparo il sito con la nuova versione': 'Preparing the site with the new version',
|
|
85
|
+
Riavvio: 'Restarting',
|
|
86
|
+
'C’è già un aggiornamento in corso': 'An update is already in progress',
|
|
87
|
+
'Versione non valida': 'Invalid version',
|
|
88
|
+
'Backup del database e dei media': 'Backing up the database and the media',
|
|
89
|
+
'Scarico Materialize {version}': 'Downloading Materialize {version}',
|
|
90
|
+
'Controllo la nuova versione': 'Checking the new version',
|
|
91
|
+
'Installata la {installed} invece della {version}': 'Installed {installed} instead of {version}',
|
|
92
|
+
'Non c’è una versione precedente a cui tornare': 'There is no previous version to go back to',
|
|
93
|
+
};
|
package/src/host.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The address the server listens on. `dev` stays on this computer unless asked otherwise: a new site
|
|
3
|
+
* in development has no installation code, so on a shared Wi-Fi whoever opened it first could create
|
|
4
|
+
* the administrator. `start` listens everywhere (Docker, a VPS behind a proxy), protected by that code.
|
|
5
|
+
*/
|
|
6
|
+
export declare function listenHost(mode: string, flag: string | undefined, env: string | undefined): string | undefined;
|
|
7
|
+
/** Whether only this computer can reach the server at that address. */
|
|
8
|
+
export declare function onlyThisComputer(host: string | undefined): boolean;
|
package/src/host.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The address the server listens on. `dev` stays on this computer unless asked otherwise: a new site
|
|
3
|
+
* in development has no installation code, so on a shared Wi-Fi whoever opened it first could create
|
|
4
|
+
* the administrator. `start` listens everywhere (Docker, a VPS behind a proxy), protected by that code.
|
|
5
|
+
*/
|
|
6
|
+
export function listenHost(mode, flag, env) {
|
|
7
|
+
return flag ?? env ?? (mode === 'dev' ? '127.0.0.1' : undefined);
|
|
8
|
+
}
|
|
9
|
+
/** Whether only this computer can reach the server at that address. */
|
|
10
|
+
export function onlyThisComputer(host) {
|
|
11
|
+
return host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
12
|
+
}
|
package/src/lang.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { systemLang } from './system-lang.ts';
|
package/src/lang.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { setLangSource } from '@materializecms/core/lang';
|
|
2
|
+
import { systemLang } from './system-lang.js';
|
|
3
|
+
export { systemLang } from './system-lang.js';
|
|
4
|
+
/**
|
|
5
|
+
* Everything printed by the terminal follows the system's language (system-lang.ts). The server's
|
|
6
|
+
* answers to the panel follow each person instead (server/src/lang.ts).
|
|
7
|
+
*/
|
|
8
|
+
const lang = systemLang();
|
|
9
|
+
setLangSource(() => lang);
|
package/src/site.d.ts
CHANGED
|
@@ -4,8 +4,22 @@ export declare const ASTRO_CONFIGS: string[];
|
|
|
4
4
|
/** True when the project has an Astro site next to materialize.config.ts. */
|
|
5
5
|
export declare const hasSite: (root: string) => boolean;
|
|
6
6
|
export type Handler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Project files a browser never needs, which Vite in dev would otherwise hand to anyone who can reach
|
|
9
|
+
* it (with --host 0.0.0.0, the whole Wi-Fi): the config, the packages, secrets, the database and Git,
|
|
10
|
+
* at the root or through /@fs/. The site's own modules (/src, /node_modules/.vite) stay served, or
|
|
11
|
+
* pages would not load.
|
|
12
|
+
*/
|
|
13
|
+
export declare const PRIVATE_DEV_FILES: RegExp;
|
|
14
|
+
/**
|
|
15
|
+
* What the dev server answers before Vite sees a request: 400 to a malformed address (Astro would
|
|
16
|
+
* log a URI error and answer 500), 404 to a private file, nothing for everything else.
|
|
17
|
+
*/
|
|
18
|
+
export declare function devGuard(url: string | undefined): 400 | 404 | null;
|
|
19
|
+
/** The address Astro's dev server listens on: the CMS's own, so hot reload answers wherever the page was opened. */
|
|
20
|
+
export declare const devSiteHost: (host: string | undefined) => string | boolean;
|
|
7
21
|
/** Astro's dev server, mounted inside Materialize: pages render on demand from the database. */
|
|
8
|
-
export declare function startDevSite(root: string, port: number): Promise<{
|
|
22
|
+
export declare function startDevSite(root: string, port: number, host?: string): Promise<{
|
|
9
23
|
handle: Handler;
|
|
10
24
|
close: () => Promise<void>;
|
|
11
25
|
}>;
|
package/src/site.js
CHANGED
|
@@ -10,20 +10,73 @@ import { existsSync } from 'node:fs';
|
|
|
10
10
|
import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
11
11
|
import { dirname, extname, join, normalize, resolve, sep } from 'node:path';
|
|
12
12
|
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { t } from './t.js';
|
|
13
14
|
export const ASTRO_CONFIGS = ['astro.config.ts', 'astro.config.mjs', 'astro.config.js'];
|
|
14
15
|
/** True when the project has an Astro site next to materialize.config.ts. */
|
|
15
16
|
export const hasSite = (root) => ASTRO_CONFIGS.some((name) => existsSync(resolve(root, name)));
|
|
17
|
+
/**
|
|
18
|
+
* Project files a browser never needs, which Vite in dev would otherwise hand to anyone who can reach
|
|
19
|
+
* it (with --host 0.0.0.0, the whole Wi-Fi): the config, the packages, secrets, the database and Git,
|
|
20
|
+
* at the root or through /@fs/. The site's own modules (/src, /node_modules/.vite) stay served, or
|
|
21
|
+
* pages would not load.
|
|
22
|
+
*/
|
|
23
|
+
export const PRIVATE_DEV_FILES = /(?:^|\/)(?:(?:materialize|astro)\.config\.[cm]?[jt]s|package(?:-lock)?\.json|tsconfig\.json|\.env(?:\.[^/]*)?)$|(?:^|\/)\.(?:materialize|git)(?:\/|$)/i;
|
|
24
|
+
/**
|
|
25
|
+
* What the dev server answers before Vite sees a request: 400 to a malformed address (Astro would
|
|
26
|
+
* log a URI error and answer 500), 404 to a private file, nothing for everything else.
|
|
27
|
+
*/
|
|
28
|
+
export function devGuard(url) {
|
|
29
|
+
const path = (url ?? '/').split(/[?#]/)[0] ?? '/';
|
|
30
|
+
let decoded;
|
|
31
|
+
try {
|
|
32
|
+
decoded = decodeURIComponent(path);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return 400;
|
|
36
|
+
}
|
|
37
|
+
return PRIVATE_DEV_FILES.test(decoded.replace(/\/{2,}/g, '/')) ? 404 : null;
|
|
38
|
+
}
|
|
39
|
+
/** The address Astro's dev server listens on: the CMS's own, so hot reload answers wherever the page was opened. */
|
|
40
|
+
export const devSiteHost = (host) => host ?? true;
|
|
16
41
|
/** Astro's dev server, mounted inside Materialize: pages render on demand from the database. */
|
|
17
|
-
export async function startDevSite(root, port) {
|
|
42
|
+
export async function startDevSite(root, port, host) {
|
|
18
43
|
const { dev } = await import('astro');
|
|
19
44
|
const server = await dev({
|
|
20
45
|
root,
|
|
21
46
|
logLevel: 'warn',
|
|
22
47
|
// The canvas shows the site itself: Astro's own toolbar would get in the way.
|
|
23
48
|
devToolbar: { enabled: false },
|
|
24
|
-
// Astro also listens on its own port, where the browser connects for hot reload
|
|
25
|
-
|
|
26
|
-
|
|
49
|
+
// Astro also listens on its own port, where the browser connects for hot reload: on the same
|
|
50
|
+
// address as the CMS (127.0.0.1 by default, 0.0.0.0 with --host), never just `localhost`, which
|
|
51
|
+
// macOS may resolve to ::1 alone and leave a page opened at 127.0.0.1 without hot reload.
|
|
52
|
+
server: { port, host: devSiteHost(host) },
|
|
53
|
+
vite: {
|
|
54
|
+
// No ws.host: the browser connects to the address it opened the page at, on Astro's port.
|
|
55
|
+
server: { ws: { clientPort: port } },
|
|
56
|
+
plugins: [
|
|
57
|
+
{
|
|
58
|
+
name: 'materialize:dev-guard',
|
|
59
|
+
configureServer(vite) {
|
|
60
|
+
// At the very front, ahead of Vite's and Astro's own middleware (Astro puts its own first,
|
|
61
|
+
// and its trailing-slash check would fail on a malformed address): this hook runs after
|
|
62
|
+
// Astro's, since our plugin comes later.
|
|
63
|
+
return () => {
|
|
64
|
+
vite.middlewares.stack.unshift({
|
|
65
|
+
route: '',
|
|
66
|
+
handle: (req, res, next) => {
|
|
67
|
+
const status = devGuard(req.url);
|
|
68
|
+
if (!status)
|
|
69
|
+
return next();
|
|
70
|
+
res.statusCode = status;
|
|
71
|
+
res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
72
|
+
res.end(status === 400 ? 'Bad request' : 'Not found');
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
27
80
|
});
|
|
28
81
|
return { handle: (req, res) => server.handle(req, res), close: () => server.stop() };
|
|
29
82
|
}
|
|
@@ -151,7 +204,7 @@ export class ProductionSite {
|
|
|
151
204
|
const entry = (await import(__rewriteRelativeImportExtension(pathToFileURL(join(dir, 'server/entry.mjs')).href)));
|
|
152
205
|
const previous = this.live?.dir;
|
|
153
206
|
this.live = { dir, ssr: entry.handler };
|
|
154
|
-
this.log(
|
|
207
|
+
this.log(t('Sito aggiornato in {ms} ms', { ms: Math.round(performance.now() - started) }), true);
|
|
155
208
|
for (const old of await readdir(this.builds)) {
|
|
156
209
|
const path = join(this.builds, old);
|
|
157
210
|
if (path !== dir && path !== previous)
|
|
@@ -160,7 +213,7 @@ export class ProductionSite {
|
|
|
160
213
|
}
|
|
161
214
|
catch (error) {
|
|
162
215
|
await rm(dir, { recursive: true, force: true });
|
|
163
|
-
this.log(
|
|
216
|
+
this.log(t('Il sito non si è aggiornato: {error}', { error: error instanceof Error ? error.message : String(error) }), false);
|
|
164
217
|
}
|
|
165
218
|
}
|
|
166
219
|
handle = async (req, res) => {
|
|
@@ -174,7 +227,16 @@ export class ProductionSite {
|
|
|
174
227
|
if (url.pathname.startsWith('/_materialize/'))
|
|
175
228
|
return live.ssr(req, res);
|
|
176
229
|
const client = join(live.dir, 'client');
|
|
177
|
-
|
|
230
|
+
let path;
|
|
231
|
+
try {
|
|
232
|
+
path = decodeURIComponent(url.pathname);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// A malformed address (/%E0%A4%A): the visitor's mistake, answered without an error in the log.
|
|
236
|
+
res.writeHead(400, { 'content-type': TYPES['.txt'] });
|
|
237
|
+
res.end('Bad request');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
178
240
|
const candidates = [path, join(path, 'index.html'), `${path.replace(/\/$/, '')}.html`];
|
|
179
241
|
for (const candidate of candidates) {
|
|
180
242
|
const file = normalize(join(client, candidate));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function t(text: string, vars?: Record<string, string | number>): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { supervisor } from './en/supervisor.js';
|
|
2
|
+
import { systemLang } from './system-lang.js';
|
|
3
|
+
/**
|
|
4
|
+
* The supervisor's own translator: like t.ts, but with Node only, because the supervisor stays
|
|
5
|
+
* loaded while the packages under it are swapped (and must work even if they are broken).
|
|
6
|
+
*/
|
|
7
|
+
const lang = systemLang();
|
|
8
|
+
export function t(text, vars) {
|
|
9
|
+
const out = lang === 'it' ? text : (supervisor[text] ?? text);
|
|
10
|
+
return vars ? out.replace(/\{(\w+)\}/g, (all, k) => (k in vars ? String(vars[k]) : all)) : out;
|
|
11
|
+
}
|
package/src/supervisor.js
CHANGED
|
@@ -14,6 +14,7 @@ import { fork } from 'node:child_process';
|
|
|
14
14
|
import { existsSync } from 'node:fs';
|
|
15
15
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
|
+
import { t } from './supervisor-t.js';
|
|
17
18
|
/** Exit code with which the server asks to be started again (and to apply a pending swap). */
|
|
18
19
|
export const RESTART = 75;
|
|
19
20
|
/** How long a freshly swapped version has to report ready before it is rolled back. */
|
|
@@ -46,7 +47,7 @@ export async function applyPending(site, pending) {
|
|
|
46
47
|
if (pending.kind === 'install') {
|
|
47
48
|
const next = join(dir, 'next');
|
|
48
49
|
if (!existsSync(join(next, 'node_modules')))
|
|
49
|
-
throw new Error('La nuova versione non è stata scaricata');
|
|
50
|
+
throw new Error(t('La nuova versione non è stata scaricata'));
|
|
50
51
|
await exchange(site, next);
|
|
51
52
|
await rm(join(dir, 'previous'), { recursive: true, force: true });
|
|
52
53
|
await rename(next, join(dir, 'previous'));
|
|
@@ -54,7 +55,7 @@ export async function applyPending(site, pending) {
|
|
|
54
55
|
else {
|
|
55
56
|
const previous = join(dir, 'previous');
|
|
56
57
|
if (!existsSync(join(previous, 'node_modules')))
|
|
57
|
-
throw new Error('Non c’è una versione precedente');
|
|
58
|
+
throw new Error(t('Non c’è una versione precedente'));
|
|
58
59
|
await exchange(site, previous);
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -123,7 +124,7 @@ export async function supervise(script, args, site = process.cwd()) {
|
|
|
123
124
|
await writeFile(join(dir, 'result.json'), JSON.stringify(result));
|
|
124
125
|
continue;
|
|
125
126
|
}
|
|
126
|
-
console.log(` Materialize ${pending.from} → ${pending.to}: avvio la
|
|
127
|
+
console.log(` Materialize ${pending.from} → ${pending.to}: ${t('avvio la {version}…', { version: pending.to })}`);
|
|
127
128
|
// The new version must report ready in time; otherwise the old one comes back.
|
|
128
129
|
let ready = false;
|
|
129
130
|
let timer;
|
|
@@ -131,20 +132,20 @@ export async function supervise(script, args, site = process.cwd()) {
|
|
|
131
132
|
ready = true;
|
|
132
133
|
clearTimeout(timer);
|
|
133
134
|
}, () => {
|
|
134
|
-
result.message = 'Con la nuova versione il sito non si compila';
|
|
135
|
+
result.message = t('Con la nuova versione il sito non si compila');
|
|
135
136
|
child?.kill('SIGTERM');
|
|
136
137
|
}, pending);
|
|
137
138
|
timer = setTimeout(() => {
|
|
138
|
-
result.message = 'La nuova versione non è partita in tempo';
|
|
139
|
+
result.message = t('La nuova versione non è partita in tempo');
|
|
139
140
|
child?.kill('SIGTERM');
|
|
140
141
|
}, READY_TIMEOUT);
|
|
141
142
|
code = await running;
|
|
142
143
|
clearTimeout(timer);
|
|
143
144
|
if (!ready && !stopping) {
|
|
144
|
-
result.message ??=
|
|
145
|
+
result.message ??= t('La nuova versione si è fermata all’avvio (codice {code})', { code });
|
|
145
146
|
await revertPending(site, pending);
|
|
146
147
|
await writeFile(join(dir, 'result.json'), JSON.stringify(result));
|
|
147
|
-
console.log(` ✗ ${
|
|
148
|
+
console.log(` ✗ ${t('{message}: torno alla {version}', { message: result.message, version: pending.from })}`);
|
|
148
149
|
continue;
|
|
149
150
|
}
|
|
150
151
|
}
|
|
@@ -163,7 +164,7 @@ export async function supervise(script, args, site = process.cwd()) {
|
|
|
163
164
|
// An unexpected stop: start again, waiting a little longer each time (up to half a minute).
|
|
164
165
|
failures++;
|
|
165
166
|
const wait = Math.min(30, 2 ** Math.min(failures, 5));
|
|
166
|
-
console.log(` ✗ Il server si è fermato (codice
|
|
167
|
+
console.log(` ✗ ${t('Il server si è fermato (codice {code}): riparto tra {wait} s', { code, wait })}`);
|
|
167
168
|
await new Promise((r) => setTimeout(r, wait * 1000));
|
|
168
169
|
}
|
|
169
170
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal speaks the system's language: Italian on an Italian computer, English otherwise.
|
|
3
|
+
* MATERIALIZE_LANG=it|en chooses by hand. Only Node here: the supervisor uses it too, and must
|
|
4
|
+
* not load any Materialize package (see supervisor.ts).
|
|
5
|
+
*/
|
|
6
|
+
export type SystemLang = 'it' | 'en';
|
|
7
|
+
export declare function systemLang(env?: Record<string, string | undefined>): SystemLang;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function systemLang(env = process.env) {
|
|
2
|
+
const chosen = env.MATERIALIZE_LANG;
|
|
3
|
+
if (chosen === 'it' || chosen === 'en')
|
|
4
|
+
return chosen;
|
|
5
|
+
const tag = env.LC_ALL || env.LC_MESSAGES || env.LANG || Intl.DateTimeFormat().resolvedOptions().locale || '';
|
|
6
|
+
return /^it\b|^it[_-]/i.test(tag) ? 'it' : 'en';
|
|
7
|
+
}
|
package/src/t.d.ts
ADDED
package/src/t.js
ADDED
package/src/updates.js
CHANGED
|
@@ -6,6 +6,7 @@ import { join, sep } from 'node:path';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { newer } from '@materializecms/server';
|
|
8
8
|
import { updatesDir } from './supervisor.js';
|
|
9
|
+
import { t } from './t.js';
|
|
9
10
|
const CURRENT = createRequire(import.meta.url)('../package.json').version;
|
|
10
11
|
const CLI = '@materializecms/cli';
|
|
11
12
|
/** Runs the site's package manager; resolves with its output, rejects with the end of it. */
|
|
@@ -20,12 +21,14 @@ function npm(args, cwd, timeout = 600_000) {
|
|
|
20
21
|
let out = '';
|
|
21
22
|
child.stdout.on('data', (d) => (out += d));
|
|
22
23
|
child.stderr.on('data', (d) => (out += d));
|
|
23
|
-
child.on('error', (error) => reject(Object.assign(new Error(
|
|
24
|
+
child.on('error', (error) => reject(Object.assign(new Error(t('npm non si avvia: {error}', { error: error.message })), { log: out.slice(-4000) })));
|
|
24
25
|
child.on('close', (code) => {
|
|
25
26
|
if (code === 0)
|
|
26
27
|
resolve(out);
|
|
27
28
|
else
|
|
28
|
-
reject(Object.assign(new Error(
|
|
29
|
+
reject(Object.assign(new Error(t('npm si è fermato con un errore (codice {code})', { code })), {
|
|
30
|
+
log: out.slice(-4000),
|
|
31
|
+
}));
|
|
29
32
|
});
|
|
30
33
|
});
|
|
31
34
|
}
|
|
@@ -43,26 +46,27 @@ export function createUpdates(options) {
|
|
|
43
46
|
let latest = null;
|
|
44
47
|
let notes = [];
|
|
45
48
|
let checkedAt = null;
|
|
46
|
-
|
|
49
|
+
// Kept as a kind and written when asked, in the language of whoever reads the status.
|
|
50
|
+
let checkFailure;
|
|
47
51
|
let job = { state: 'idle' };
|
|
48
52
|
const unavailable = async () => {
|
|
49
53
|
if (options.mode === 'dev')
|
|
50
|
-
return 'In sviluppo (materialize dev) si aggiorna da terminale: gli aggiornamenti dal pannello funzionano con materialize start.';
|
|
54
|
+
return t('In sviluppo (materialize dev) si aggiorna da terminale: gli aggiornamenti dal pannello funzionano con materialize start.');
|
|
51
55
|
if (!process.env.MATERIALIZE_SUPERVISED)
|
|
52
|
-
return 'Il server è avviato senza il suo supervisore: aggiorna da terminale, oppure avvialo con materialize start.';
|
|
56
|
+
return t('Il server è avviato senza il suo supervisore: aggiorna da terminale, oppure avvialo con materialize start.');
|
|
53
57
|
// Linked from a source checkout (like the example in the repository): git updates it.
|
|
54
58
|
const here = realpathSync(fileURLToPath(import.meta.url));
|
|
55
59
|
if (!here.startsWith(realpathSync(join(site, 'node_modules')) + sep))
|
|
56
|
-
return 'Materialize è collegato dal codice sorgente: si aggiorna con git.';
|
|
60
|
+
return t('Materialize è collegato dal codice sorgente: si aggiorna con git.');
|
|
57
61
|
const pkg = await readJson(join(site, 'package.json'));
|
|
58
62
|
if (!pkg?.dependencies?.[CLI])
|
|
59
|
-
return
|
|
63
|
+
return t('Il package.json del sito non ha {name} tra le dipendenze.', { name: CLI });
|
|
60
64
|
try {
|
|
61
65
|
await access(site, constants.W_OK);
|
|
62
66
|
await access(join(site, 'node_modules'), constants.W_OK);
|
|
63
67
|
}
|
|
64
68
|
catch {
|
|
65
|
-
return 'Il programma non può scrivere nella cartella del sito: aggiorna da terminale, con l’utente che la possiede.';
|
|
69
|
+
return t('Il programma non può scrivere nella cartella del sito: aggiorna da terminale, con l’utente che la possiede.');
|
|
66
70
|
}
|
|
67
71
|
return undefined;
|
|
68
72
|
};
|
|
@@ -75,15 +79,19 @@ export function createUpdates(options) {
|
|
|
75
79
|
latest,
|
|
76
80
|
notes,
|
|
77
81
|
checkedAt,
|
|
78
|
-
checkError
|
|
82
|
+
checkError: checkFailure === 'unpublished'
|
|
83
|
+
? t('Nessuna versione pubblicata da controllare.')
|
|
84
|
+
: checkFailure === 'unreachable'
|
|
85
|
+
? t('Non riesco a raggiungere il registro dei pacchetti: riprova più tardi.')
|
|
86
|
+
: undefined,
|
|
79
87
|
unavailable: await unavailable(),
|
|
80
88
|
previous: await previous(),
|
|
81
|
-
job: swapping ? { state: 'restarting', step: 'Preparo il sito con la nuova versione' } : job,
|
|
89
|
+
job: swapping ? { state: 'restarting', step: t('Preparo il sito con la nuova versione') } : job,
|
|
82
90
|
last: await readJson(join(dir, 'result.json')),
|
|
83
91
|
});
|
|
84
92
|
const restartWith = async (pending) => {
|
|
85
93
|
await writeFile(join(dir, 'pending.json'), JSON.stringify(pending));
|
|
86
|
-
job = { state: 'restarting', step: 'Riavvio' };
|
|
94
|
+
job = { state: 'restarting', step: t('Riavvio') };
|
|
87
95
|
// A moment for the panel to hear back before the server goes away.
|
|
88
96
|
setTimeout(() => void options.restart(), 300);
|
|
89
97
|
};
|
|
@@ -103,13 +111,11 @@ export function createUpdates(options) {
|
|
|
103
111
|
const info = JSON.parse(out);
|
|
104
112
|
latest = typeof info === 'string' ? info : (info.version ?? null);
|
|
105
113
|
notes = (typeof info === 'string' ? [] : (info.changelog ?? [])).filter((r) => newer(r.version, CURRENT));
|
|
106
|
-
|
|
114
|
+
checkFailure = undefined;
|
|
107
115
|
}
|
|
108
116
|
catch (error) {
|
|
109
117
|
const log = error.log ?? '';
|
|
110
|
-
|
|
111
|
-
? 'Nessuna versione pubblicata da controllare.'
|
|
112
|
-
: 'Non riesco a raggiungere il registro dei pacchetti: riprova più tardi.';
|
|
118
|
+
checkFailure = /E404|404 Not Found/.test(log) ? 'unpublished' : 'unreachable';
|
|
113
119
|
}
|
|
114
120
|
checkedAt = new Date().toISOString();
|
|
115
121
|
return status();
|
|
@@ -119,14 +125,14 @@ export function createUpdates(options) {
|
|
|
119
125
|
if (reason)
|
|
120
126
|
throw new Error(reason);
|
|
121
127
|
if (job.state === 'working' || job.state === 'restarting')
|
|
122
|
-
throw new Error('C’è già un aggiornamento in corso');
|
|
128
|
+
throw new Error(t('C’è già un aggiornamento in corso'));
|
|
123
129
|
if (!/^\d+\.\d+\.\d+$/.test(version))
|
|
124
|
-
throw new Error('Versione non valida');
|
|
125
|
-
job = { state: 'working', step: 'Backup del database e dei media' };
|
|
130
|
+
throw new Error(t('Versione non valida'));
|
|
131
|
+
job = { state: 'working', step: t('Backup del database e dei media') };
|
|
126
132
|
void (async () => {
|
|
127
133
|
try {
|
|
128
134
|
await backup();
|
|
129
|
-
job = { state: 'working', step:
|
|
135
|
+
job = { state: 'working', step: t('Scarico Materialize {version}', { version }) };
|
|
130
136
|
const next = join(dir, 'next');
|
|
131
137
|
await rm(next, { recursive: true, force: true });
|
|
132
138
|
await mkdir(next, { recursive: true });
|
|
@@ -140,10 +146,10 @@ export function createUpdates(options) {
|
|
|
140
146
|
if (existsSync(join(site, name)))
|
|
141
147
|
await copyFile(join(site, name), join(next, name));
|
|
142
148
|
await npm(['install', '--no-audit', '--no-fund'], next);
|
|
143
|
-
job = { state: 'working', step: 'Controllo la nuova versione' };
|
|
149
|
+
job = { state: 'working', step: t('Controllo la nuova versione') };
|
|
144
150
|
const installed = await readJson(join(next, 'node_modules', CLI, 'package.json'));
|
|
145
151
|
if (installed?.version !== version)
|
|
146
|
-
throw new Error(
|
|
152
|
+
throw new Error(t('Installata la {installed} invece della {version}', { installed: installed?.version ?? '?', version }));
|
|
147
153
|
await restartWith({ kind: 'install', from: CURRENT, to: version, at: new Date().toISOString() });
|
|
148
154
|
}
|
|
149
155
|
catch (error) {
|
|
@@ -162,7 +168,7 @@ export function createUpdates(options) {
|
|
|
162
168
|
throw new Error(reason);
|
|
163
169
|
const to = await previous();
|
|
164
170
|
if (!to)
|
|
165
|
-
throw new Error('Non c’è una versione precedente a cui tornare');
|
|
171
|
+
throw new Error(t('Non c’è una versione precedente a cui tornare'));
|
|
166
172
|
await restartWith({ kind: 'rollback', from: CURRENT, to, at: new Date().toISOString() });
|
|
167
173
|
},
|
|
168
174
|
};
|