@materializecms/cli 0.1.0 → 0.3.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 CHANGED
@@ -4,6 +4,22 @@ Ogni versione, dalla più recente. Le novità in arrivo si scrivono sotto «Pros
4
4
 
5
5
  ## Prossima versione
6
6
 
7
+ ## 0.3.0 — 2026-11-01
8
+
9
+ - **Backup fuori dal server**, ogni notte e cifrati, su uno spazio S3 compatibile (Cloudflare R2, Backblaze B2, Hetzner, Amazon S3…) o in una cartella WebDAV (Nextcloud, NAS Synology e QNAP…). Si attiva in Impostazioni → Backup; `materialize decrypt` rende di nuovo leggibili i file scaricati.
10
+ - **Email più facile da configurare**: si sceglie il fornitore (Brevo, Postmark, Mailgun, Amazon SES, Gmail…) e il server si compila da solo; se la prova non riesce, il pannello spiega perché (porta bloccata, password rifiutata, mittente non verificato…); **Controlla il dominio** verifica SPF, DKIM e DMARC.
11
+ - **Pannello più chiaro**: Impostazioni → Protezione divisa in Sicurezza, Backup e Registro attività; barra dell'editor a gruppi, con gli elementi meno usati in **⋯**; pannello sinistro dell'editor allargabile; menu a tendina, intestazioni e testi d'aiuto uniformi e più leggibili; pagina dedicata per gli indirizzi del pannello che non esistono; in Persone, chi entra con la sola password si riconosce a colpo d'occhio; accesso e installazione con l'aspetto del pannello.
12
+ - **Chi ha fatto ogni versione**: la cronologia mostra il nome dell'autore accanto alla data.
13
+ - **Informazioni su Materialize** in Impostazioni: versione, autore, licenza, novità, guida e licenze di terzi.
14
+
15
+ ## 0.2.0 — 2026-11-01
16
+
17
+ - **Password dimenticata?** nella pagina di accesso: un link per email, valido un'ora. Senza email configurata, il comando `materialize reset-password` stampa il link sul server, anche per l'unico amministratore.
18
+ - **Cronologia per le pagine sulla tela**: ogni versione si guarda su desktop, tablet e mobile prima di ripristinarla.
19
+ - **Esporta la pagina in PDF o PNG** dall'editor, per mandarla al cliente: desktop, tablet e mobile, ognuno come un'unica immagine lunga.
20
+ - **Guida per chi usa Materialize**, in `docs/guida`: installazione, editor, contenuti, lingue, SEO, persone, aggiornamenti, backup e problemi comuni.
21
+ - **Sicurezza**: i link degli inviti e delle nuove password puntano sempre all'indirizzo pubblico del sito, mai a quello dichiarato da chi fa la richiesta. Consigliato aggiornare.
22
+
7
23
  ## 0.1.0 — 2026-10-31
8
24
 
9
25
  La prima versione pubblica.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Cristiano Casazza
3
+ Copyright (c) 2026 Cristiano Casazza Consulting
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/compose.yaml CHANGED
@@ -1,6 +1,7 @@
1
1
  # Materialize with HTTPS, on a server where Docker is already there:
2
2
  #
3
- # DOMAIN=example.com docker compose up -d
3
+ # echo "DOMAIN=example.com" > .env (read by every docker compose command in this folder)
4
+ # docker compose up -d
4
5
  # docker compose logs materialize (the link to finish the installation in the browser)
5
6
  #
6
7
  # Caddy gets the Let's Encrypt certificate for the domain, which must already point to the server.
@@ -13,7 +14,7 @@ services:
13
14
  volumes:
14
15
  - site:/site
15
16
  environment:
16
- MATERIALIZE_SITE_URL: https://${DOMAIN:?Indica il dominio, per esempio DOMAIN=example.com docker compose up -d}
17
+ MATERIALIZE_SITE_URL: https://${DOMAIN:?Indica il dominio nel file .env, per esempio DOMAIN=example.com}
17
18
  MATERIALIZE_TRUST_PROXY: "1"
18
19
  # Other variables for the site go here, for example ANTHROPIC_API_KEY.
19
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@materializecms/cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.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",
@@ -12,7 +12,7 @@
12
12
  "homepage": "https://github.com/materializecms/materialize#readme",
13
13
  "bugs": "https://github.com/materializecms/materialize/issues",
14
14
  "bin": {
15
- "materialize": "./src/cli.js"
15
+ "materialize": "src/cli.js"
16
16
  },
17
17
  "exports": {
18
18
  ".": {
@@ -21,10 +21,10 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@materializecms/astro": "0.1.0",
25
- "@materializecms/core": "0.1.0",
26
- "@materializecms/db": "0.1.0",
27
- "@materializecms/server": "0.1.0",
24
+ "@materializecms/astro": "0.3.0",
25
+ "@materializecms/core": "0.3.0",
26
+ "@materializecms/db": "0.3.0",
27
+ "@materializecms/server": "0.3.0",
28
28
  "@modelcontextprotocol/sdk": "1.32.2"
29
29
  },
30
30
  "peerDependencies": {
@@ -42,6 +42,16 @@
42
42
  "access": "public"
43
43
  },
44
44
  "changelog": [
45
+ {
46
+ "version": "0.3.0",
47
+ "date": "2026-11-01",
48
+ "notes": "- **Backup fuori dal server**, ogni notte e cifrati, su uno spazio S3 compatibile (Cloudflare R2, Backblaze B2, Hetzner, Amazon S3…) o in una cartella WebDAV (Nextcloud, NAS Synology e QNAP…). Si attiva in Impostazioni → Backup; `materialize decrypt` rende di nuovo leggibili i file scaricati.\n- **Email più facile da configurare**: si sceglie il fornitore (Brevo, Postmark, Mailgun, Amazon SES, Gmail…) e il server si compila da solo; se la prova non riesce, il pannello spiega perché (porta bloccata, password rifiutata, mittente non verificato…); **Controlla il dominio** verifica SPF, DKIM e DMARC.\n- **Pannello più chiaro**: Impostazioni → Protezione divisa in Sicurezza, Backup e Registro attività; barra dell'editor a gruppi, con gli elementi meno usati in **⋯**; pannello sinistro dell'editor allargabile; menu a tendina, intestazioni e testi d'aiuto uniformi e più leggibili; pagina dedicata per gli indirizzi del pannello che non esistono; in Persone, chi entra con la sola password si riconosce a colpo d'occhio; accesso e installazione con l'aspetto del pannello.\n- **Chi ha fatto ogni versione**: la cronologia mostra il nome dell'autore accanto alla data.\n- **Informazioni su Materialize** in Impostazioni: versione, autore, licenza, novità, guida e licenze di terzi."
49
+ },
50
+ {
51
+ "version": "0.2.0",
52
+ "date": "2026-11-01",
53
+ "notes": "- **Password dimenticata?** nella pagina di accesso: un link per email, valido un'ora. Senza email configurata, il comando `materialize reset-password` stampa il link sul server, anche per l'unico amministratore.\n- **Cronologia per le pagine sulla tela**: ogni versione si guarda su desktop, tablet e mobile prima di ripristinarla.\n- **Esporta la pagina in PDF o PNG** dall'editor, per mandarla al cliente: desktop, tablet e mobile, ognuno come un'unica immagine lunga.\n- **Guida per chi usa Materialize**, in `docs/guida`: installazione, editor, contenuti, lingue, SEO, persone, aggiornamenti, backup e problemi comuni.\n- **Sicurezza**: i link degli inviti e delle nuove password puntano sempre all'indirizzo pubblico del sito, mai a quello dichiarato da chi fa la richiesta. Consigliato aggiornare."
54
+ },
45
55
  {
46
56
  "version": "0.1.0",
47
57
  "date": "2026-10-31",
package/src/commands.js CHANGED
@@ -4,8 +4,8 @@ import { copyFile, mkdir, open, readFile, rm, writeFile } from 'node:fs/promises
4
4
  import { dirname, join, relative, resolve } from 'node:path';
5
5
  import { styleText } from 'node:util';
6
6
  import { resolveRedirect } from '@materializecms/core/seo';
7
- import { createAuthStore, createContentStore, createSettingsStore, openDatabase } from '@materializecms/db';
8
- import { apiCaller, createFonts, createMcpServer, createMediaFiles, extractTarGz, restoreMedia, startServer, VERSION, } from '@materializecms/server';
7
+ import { createActivityStore, createAuthStore, createContentStore, createSettingsStore, openDatabase, } from '@materializecms/db';
8
+ import { apiCaller, createFonts, createMcpServer, createMediaFiles, decryptTree, extractTarGz, parseRecoveryKey, restoreMedia, startServer, VERSION, } from '@materializecms/server';
9
9
  import { findConfig, loadConfig } from './load-config.js';
10
10
  import { buildSite, exportFonts, exportMedia, hasSite, ProductionSite, startDevSite } from './site.js';
11
11
  import { RESTART } from './supervisor.js';
@@ -25,6 +25,12 @@ ${accent('materialize')} ${dim(VERSION)}
25
25
  materialize restore <file>
26
26
  Rimette un backup (content-….db, o l'archivio completo materialize-….tar.gz);
27
27
  il server va fermato
28
+ materialize reset-password <email>
29
+ Un link per una nuova password, valido un'ora: per chi non può riceverlo per email
30
+ (per esempio l'unico amministratore, senza email configurata)
31
+ materialize decrypt <cartella o file> [--key <chiave>]
32
+ Decifra i backup scaricati dallo spazio esterno (S3, WebDAV); la chiave di
33
+ ripristino si può dare anche con MATERIALIZE_BACKUP_KEY
28
34
  materialize --help Mostra questo aiuto
29
35
 
30
36
  Opzioni
@@ -345,6 +351,111 @@ async function restore(args) {
345
351
  if (kept)
346
352
  console.log(dim(` Il database di prima è in ${relative(process.cwd(), kept)}, se servisse tornare indietro.`));
347
353
  }
354
+ /**
355
+ * A one-time link for a new password, printed here: the way back in for someone who cannot get it
356
+ * by email, such as the only administrator of a site without email. Only who can run commands on
357
+ * the server can ask for it. The server may be running.
358
+ */
359
+ async function resetPassword(args) {
360
+ const email = args
361
+ .find((a) => !a.startsWith('--'))
362
+ ?.trim()
363
+ .toLowerCase();
364
+ const path = findConfig(process.cwd());
365
+ if (!email || !path) {
366
+ console.error(fail(!email
367
+ ? 'Indica l’email: materialize reset-password <email>'
368
+ : 'Non trovo materialize.config.ts in questa cartella: lancia il comando dalla cartella del sito.'));
369
+ process.exit(1);
370
+ }
371
+ const config = await loadConfig(path);
372
+ const database = await openDatabase(config.database);
373
+ try {
374
+ const auth = createAuthStore(database.db);
375
+ const user = await auth.findForLogin(email);
376
+ if (!user) {
377
+ console.error(fail(`Nessun account con l’email ${email}.`));
378
+ process.exitCode = 1;
379
+ return;
380
+ }
381
+ const { token } = await auth.createInvite({
382
+ kind: 'reset',
383
+ email: user.email,
384
+ name: user.name,
385
+ role: user.role,
386
+ userId: user.id,
387
+ ttlMs: 60 * 60 * 1000,
388
+ });
389
+ await createActivityStore(database.db).record({
390
+ userId: null,
391
+ userName: '',
392
+ action: 'password.reset_cli',
393
+ target: user.email,
394
+ detail: null,
395
+ ip: 'server',
396
+ });
397
+ const base = config.site.url?.replace(/\/+$/, '') ?? `http://localhost:${config.server.port}`;
398
+ console.log(` ${ok('✓')} Link per una nuova password di ${user.name} (${user.email}), valido un’ora:`);
399
+ console.log();
400
+ console.log(` ${accent(`${base}/admin/_invite/${token}`)}`);
401
+ console.log();
402
+ console.log(dim(' Chi lo apre sceglie la password. La verifica in due passaggi, se attiva, resta.'));
403
+ }
404
+ finally {
405
+ database.close();
406
+ }
407
+ }
408
+ /**
409
+ * The backups sent outside the server, downloaded with any tool, made readable again: every .mzb
410
+ * file is decrypted next to itself. The key comes from --key, MATERIALIZE_BACKUP_KEY or, in the
411
+ * folder of a site that still has its database, from its settings.
412
+ */
413
+ async function decrypt(args) {
414
+ const flag = args.indexOf('--key');
415
+ const source = args.find((a, i) => !a.startsWith('--') && (flag < 0 || i !== flag + 1));
416
+ if (!source) {
417
+ console.error(fail('Indica la cartella (o il file) da decifrare: materialize decrypt <cartella> --key <chiave>'));
418
+ process.exit(1);
419
+ }
420
+ let text = flag >= 0 ? args[flag + 1] : process.env.MATERIALIZE_BACKUP_KEY;
421
+ if (!text) {
422
+ const path = findConfig(process.cwd());
423
+ if (path) {
424
+ const database = await openDatabase((await loadConfig(path)).database).catch(() => null);
425
+ if (database) {
426
+ text = (await createSettingsStore(database.db).get('offsite'))?.key;
427
+ database.close();
428
+ }
429
+ }
430
+ }
431
+ if (!text) {
432
+ console.error(fail('Manca la chiave di ripristino: --key mzk1-… oppure MATERIALIZE_BACKUP_KEY.'));
433
+ process.exit(1);
434
+ }
435
+ let key;
436
+ try {
437
+ key = parseRecoveryKey(text);
438
+ }
439
+ catch (error) {
440
+ console.error(fail(describeError(error)));
441
+ process.exit(1);
442
+ }
443
+ try {
444
+ const done = await decryptTree(resolve(source), key);
445
+ console.log(` ${ok('✓')} ${done.length} ${done.length === 1 ? 'file decifrato' : 'file decifrati'}, accanto agli originali.`);
446
+ const db = done
447
+ .filter((f) => /content-[^/\\]+\.db$/.test(f))
448
+ .sort()
449
+ .at(-1);
450
+ if (db)
451
+ // The shorter of the two ways to write the path.
452
+ 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]}`));
453
+ }
454
+ catch (error) {
455
+ console.error(fail(describeError(error)));
456
+ process.exit(1);
457
+ }
458
+ }
348
459
  const [command = 'help', ...args] = process.argv.slice(2);
349
460
  if (command === 'dev' || command === 'start' || command === 'build') {
350
461
  await run(command, args);
@@ -352,6 +463,12 @@ if (command === 'dev' || command === 'start' || command === 'build') {
352
463
  else if (command === 'mcp') {
353
464
  await mcp(args);
354
465
  }
466
+ else if (command === 'decrypt') {
467
+ await decrypt(args);
468
+ }
469
+ else if (command === 'reset-password') {
470
+ await resetPassword(args);
471
+ }
355
472
  else if (command === 'restore') {
356
473
  await restore(args);
357
474
  }