@materializecms/cli 0.0.0-stage → 0.2.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 +32 -0
- package/LICENSE +21 -0
- package/README.md +20 -2
- package/compose.yaml +36 -0
- package/install.sh +437 -0
- package/package.json +54 -4
- package/src/cli.d.ts +2 -0
- package/src/cli.js +15 -0
- package/src/commands.d.ts +1 -0
- package/src/commands.js +425 -0
- package/src/load-config.d.ts +13 -0
- package/src/load-config.js +53 -0
- package/src/site.d.ts +38 -0
- package/src/site.js +193 -0
- package/src/supervisor.d.ts +29 -0
- package/src/supervisor.js +169 -0
- package/src/updates.d.ts +14 -0
- package/src/updates.js +169 -0
package/src/commands.js
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { watch } from 'node:fs';
|
|
3
|
+
import { copyFile, mkdir, open, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { styleText } from 'node:util';
|
|
6
|
+
import { resolveRedirect } from '@materializecms/core/seo';
|
|
7
|
+
import { createActivityStore, createAuthStore, createContentStore, createSettingsStore, openDatabase, } from '@materializecms/db';
|
|
8
|
+
import { apiCaller, createFonts, createMcpServer, createMediaFiles, extractTarGz, restoreMedia, startServer, VERSION, } from '@materializecms/server';
|
|
9
|
+
import { findConfig, loadConfig } from './load-config.js';
|
|
10
|
+
import { buildSite, exportFonts, exportMedia, hasSite, ProductionSite, startDevSite } from './site.js';
|
|
11
|
+
import { RESTART } from './supervisor.js';
|
|
12
|
+
import { createUpdates } from './updates.js';
|
|
13
|
+
const dim = (s) => styleText('dim', s);
|
|
14
|
+
const accent = (s) => styleText(['bold', 'blue'], s);
|
|
15
|
+
const ok = (s) => styleText('green', s);
|
|
16
|
+
const fail = (s) => styleText('red', s);
|
|
17
|
+
const HELP = `
|
|
18
|
+
${accent('materialize')} ${dim(VERSION)}
|
|
19
|
+
|
|
20
|
+
materialize dev Avvia CMS e sito, ricarica lo schema a ogni salvataggio
|
|
21
|
+
materialize build Genera il sito statico in dist/client, immagini comprese
|
|
22
|
+
materialize start Avvia CMS e sito in produzione (il sito si rigenera a ogni pubblicazione);
|
|
23
|
+
un supervisore lo tiene acceso e applica gli aggiornamenti dal pannello
|
|
24
|
+
materialize mcp Server MCP su stdio per i client AI locali (serve il CMS avviato)
|
|
25
|
+
materialize restore <file>
|
|
26
|
+
Rimette un backup (content-….db, o l'archivio completo materialize-….tar.gz);
|
|
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 --help Mostra questo aiuto
|
|
32
|
+
|
|
33
|
+
Opzioni
|
|
34
|
+
--port <numero> Porta del server (predefinita: quella del config, 4321)
|
|
35
|
+
--host <indirizzo> Indirizzo su cui ascoltare (predefinito: tutti; 127.0.0.1 dietro un proxy)
|
|
36
|
+
--url <indirizzo> Con mcp: il CMS a cui collegarsi (predefinito: http://localhost:<porta>)
|
|
37
|
+
--no-supervisor Con start: senza supervisore (se il server lo tiene acceso un altro programma)
|
|
38
|
+
|
|
39
|
+
Il server MCP usa un token personale (Impostazioni → AI e MCP) in MATERIALIZE_TOKEN.
|
|
40
|
+
`;
|
|
41
|
+
/** Kept until the installation is done, so a restart (or the VPS installer, which reads it) keeps the same link. */
|
|
42
|
+
const INSTALL_CODE = '.materialize/install-code';
|
|
43
|
+
async function installCode(site) {
|
|
44
|
+
const path = join(site, INSTALL_CODE);
|
|
45
|
+
const kept = await readFile(path, 'utf8').catch(() => '');
|
|
46
|
+
if (/^[\w-]{12,}$/.test(kept.trim()))
|
|
47
|
+
return kept.trim();
|
|
48
|
+
const code = randomBytes(9).toString('base64url');
|
|
49
|
+
await mkdir(dirname(path), { recursive: true });
|
|
50
|
+
await writeFile(path, code, { mode: 0o600 });
|
|
51
|
+
return code;
|
|
52
|
+
}
|
|
53
|
+
function summary(config) {
|
|
54
|
+
return config.collections.map((c) => c.name).join(', ');
|
|
55
|
+
}
|
|
56
|
+
function describeError(error) {
|
|
57
|
+
return error instanceof Error ? error.message : String(error);
|
|
58
|
+
}
|
|
59
|
+
async function run(mode, args) {
|
|
60
|
+
const cwd = process.cwd();
|
|
61
|
+
const path = findConfig(cwd);
|
|
62
|
+
if (!path) {
|
|
63
|
+
console.error(fail('Non trovo materialize.config.ts in questa cartella.'));
|
|
64
|
+
console.error(dim('Crea un nuovo progetto con: npm create materialize@latest'));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
let config;
|
|
68
|
+
try {
|
|
69
|
+
config = await loadConfig(path);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error(fail(describeError(error)));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const withSite = hasSite(cwd);
|
|
76
|
+
if (mode === 'build') {
|
|
77
|
+
if (!withSite) {
|
|
78
|
+
console.error(fail('Non c’è un sito da generare: manca astro.config.ts in questa cartella.'));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const started = performance.now();
|
|
82
|
+
await buildSite(cwd, `${cwd}/dist`);
|
|
83
|
+
const images = await exportMedia(`${cwd}/dist/client`, createMediaFiles(config.media.dir));
|
|
84
|
+
await exportFonts(`${cwd}/dist/client`, createFonts(join(dirname(resolve(config.media.dir)), 'fonts')));
|
|
85
|
+
const elapsed = Math.round(performance.now() - started);
|
|
86
|
+
console.log(` ${ok('✓')} Sito generato in dist/client ${dim(`(${images} immagini, ${elapsed} ms)`)}`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
const flag = (name) => (args.includes(name) ? args[args.indexOf(name) + 1] : undefined);
|
|
90
|
+
const port = Number(flag('--port') ?? config.server.port);
|
|
91
|
+
const host = flag('--host') ?? process.env.MATERIALIZE_HOST;
|
|
92
|
+
let current = config;
|
|
93
|
+
// The site handler is swapped in once Astro is ready; until then visitors get a short wait page.
|
|
94
|
+
let siteHandler = (_req, res) => {
|
|
95
|
+
res.writeHead(503, { 'content-type': 'text/html; charset=utf-8', 'retry-after': '1' });
|
|
96
|
+
res.end('<meta http-equiv="refresh" content="1">Il sito si sta avviando…');
|
|
97
|
+
};
|
|
98
|
+
let production;
|
|
99
|
+
let contentChanged = () => { };
|
|
100
|
+
let redirectOf = async () => null;
|
|
101
|
+
let closeSite;
|
|
102
|
+
const updates = createUpdates({
|
|
103
|
+
site: cwd,
|
|
104
|
+
mode,
|
|
105
|
+
// The supervisor (supervisor.ts) swaps in the new version and starts the server again.
|
|
106
|
+
restart: async () => {
|
|
107
|
+
await closeSite?.();
|
|
108
|
+
await server.close();
|
|
109
|
+
process.exit(RESTART);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
// In production the first account needs the link printed below: a fresh server online cannot be
|
|
113
|
+
// claimed by whoever finds it first.
|
|
114
|
+
const setupToken = mode === 'start' ? await installCode(cwd) : undefined;
|
|
115
|
+
const server = await startServer(config, {
|
|
116
|
+
updates,
|
|
117
|
+
setupToken,
|
|
118
|
+
port,
|
|
119
|
+
host,
|
|
120
|
+
site: withSite
|
|
121
|
+
? async (req, res) => {
|
|
122
|
+
// Old addresses (moved pages, Impostazioni → SEO) answer with a permanent redirect.
|
|
123
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
124
|
+
const url = new URL(req.url ?? '/', 'http://site');
|
|
125
|
+
const to = await redirectOf(url.pathname);
|
|
126
|
+
if (to) {
|
|
127
|
+
res.writeHead(301, { location: to + url.search });
|
|
128
|
+
res.end();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return siteHandler(req, res);
|
|
133
|
+
}
|
|
134
|
+
: undefined,
|
|
135
|
+
onPublish: () => {
|
|
136
|
+
void production?.rebuild();
|
|
137
|
+
contentChanged();
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
if (withSite) {
|
|
141
|
+
const runtime = await import('@materializecms/astro/runtime');
|
|
142
|
+
contentChanged = runtime.contentChanged;
|
|
143
|
+
const { setRuntime } = runtime;
|
|
144
|
+
const settings = createSettingsStore(server.database.db);
|
|
145
|
+
setRuntime({
|
|
146
|
+
config: () => current,
|
|
147
|
+
content: createContentStore(server.database.db),
|
|
148
|
+
auth: createAuthStore(server.database.db),
|
|
149
|
+
settings,
|
|
150
|
+
});
|
|
151
|
+
redirectOf = async (path) => {
|
|
152
|
+
if (/\.[a-z0-9]+$/i.test(path) && !path.endsWith('.html'))
|
|
153
|
+
return null;
|
|
154
|
+
return resolveRedirect((await settings.get('redirects')) ?? [], path);
|
|
155
|
+
};
|
|
156
|
+
if (mode === 'dev') {
|
|
157
|
+
const site = await startDevSite(cwd, port + 1);
|
|
158
|
+
siteHandler = site.handle;
|
|
159
|
+
closeSite = site.close;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
let built = false;
|
|
163
|
+
production = new ProductionSite(cwd, (message, success) => {
|
|
164
|
+
built = success;
|
|
165
|
+
console.log(` ${success ? ok('✓') : fail('✗')} ${message}`);
|
|
166
|
+
});
|
|
167
|
+
siteHandler = production.handle;
|
|
168
|
+
// Under the supervisor: ready once the site is built, so a new version that cannot build it is undone.
|
|
169
|
+
void production.rebuild().then(async () => {
|
|
170
|
+
if (built)
|
|
171
|
+
await updates.ready();
|
|
172
|
+
process.send?.(built ? 'ready' : 'failed');
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!production)
|
|
177
|
+
await updates.ready().then(() => process.send?.('ready'));
|
|
178
|
+
console.log();
|
|
179
|
+
console.log(` ${accent('Materialize')} ${dim(VERSION)} ${config.site.name}`);
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(` ${dim((withSite ? 'Sito' : 'Locale').padEnd(12))}${server.url}`);
|
|
182
|
+
console.log(` ${dim('Pannello')} ${server.url}/admin`);
|
|
183
|
+
console.log(` ${dim('API')} ${server.url}/api/schema`);
|
|
184
|
+
console.log(` ${dim('Database')} ${config.database.url}`);
|
|
185
|
+
console.log(` ${dim('Collezioni')} ${summary(config)}`);
|
|
186
|
+
console.log();
|
|
187
|
+
if ((await createAuthStore(server.database.db).countUsers()) > 0) {
|
|
188
|
+
if (setupToken)
|
|
189
|
+
await rm(join(cwd, INSTALL_CODE), { force: true });
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const path = `/admin/${setupToken ? `?installa=${setupToken}` : ''}`;
|
|
193
|
+
// The public address when the site knows it (MATERIALIZE_SITE_URL in Docker or on a VPS).
|
|
194
|
+
const site = mode === 'start' ? config.site.url?.replace(/\/+$/, '') : undefined;
|
|
195
|
+
console.log(` ${accent('Installazione')} apri ${site ?? server.url}${path}`);
|
|
196
|
+
if (setupToken && !site)
|
|
197
|
+
console.log(dim(` (online: lo stesso indirizzo dal tuo dominio, https://…${path})`));
|
|
198
|
+
console.log();
|
|
199
|
+
}
|
|
200
|
+
if (mode === 'dev') {
|
|
201
|
+
let timer;
|
|
202
|
+
console.log(dim(` In ascolto delle modifiche a ${relative(cwd, path)}…`));
|
|
203
|
+
watch(path, () => {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
timer = setTimeout(async () => {
|
|
206
|
+
try {
|
|
207
|
+
const next = await loadConfig(path);
|
|
208
|
+
server.reload(next);
|
|
209
|
+
current = next;
|
|
210
|
+
console.log(` ${ok('✓')} Schema ricaricato ${dim(`(${summary(next)})`)}`);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
console.log(` ${fail('✗')} Lo schema ha un problema, resto sulla versione precedente:`);
|
|
214
|
+
console.log(dim(describeError(error).replace(/^/gm, ' ')));
|
|
215
|
+
}
|
|
216
|
+
}, 120);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const stop = async () => {
|
|
220
|
+
await closeSite?.();
|
|
221
|
+
await server.close();
|
|
222
|
+
process.exit(0);
|
|
223
|
+
};
|
|
224
|
+
process.on('SIGINT', stop);
|
|
225
|
+
process.on('SIGTERM', stop);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* MCP over stdio, for AI clients that start a local process: the same tools as /mcp, sent to a
|
|
229
|
+
* running CMS with a personal access token. Nothing but the protocol may go to stdout.
|
|
230
|
+
*/
|
|
231
|
+
async function mcp(args) {
|
|
232
|
+
const token = process.env.MATERIALIZE_TOKEN?.trim();
|
|
233
|
+
if (!token) {
|
|
234
|
+
console.error('Manca MATERIALIZE_TOKEN: crea un token in Impostazioni → AI e MCP e passalo al client.');
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
const flag = args.indexOf('--url');
|
|
238
|
+
let url = flag >= 0 ? args[flag + 1] : process.env.MATERIALIZE_URL;
|
|
239
|
+
if (!url) {
|
|
240
|
+
const path = findConfig(process.cwd());
|
|
241
|
+
const port = path ? (await loadConfig(path)).server.port : 4321;
|
|
242
|
+
url = `http://localhost:${port}`;
|
|
243
|
+
}
|
|
244
|
+
const base = url.replace(/\/+$/, '');
|
|
245
|
+
const health = await fetch(`${base}/api/health`).catch(() => null);
|
|
246
|
+
if (!health?.ok) {
|
|
247
|
+
console.error(`Il CMS non risponde su ${base}: avvialo con materialize dev (o start), oppure indica --url.`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
251
|
+
const server = createMcpServer(apiCaller(fetch, { authorization: `Bearer ${token}` }, base), VERSION);
|
|
252
|
+
await server.connect(new StdioServerTransport());
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Puts a backup back: a database copy (content-….db), whose missing images come back from the
|
|
256
|
+
* media copy beside the backups, or a whole archive (materialize-….tar.gz: database, media and
|
|
257
|
+
* fonts). The current database is kept as a backup first, so a restore can itself be undone.
|
|
258
|
+
* The server must be stopped: it holds the database open.
|
|
259
|
+
*/
|
|
260
|
+
async function restore(args) {
|
|
261
|
+
const file = args.find((a) => !a.startsWith('--'));
|
|
262
|
+
const path = findConfig(process.cwd());
|
|
263
|
+
if (!file || !path) {
|
|
264
|
+
console.error(fail(!file
|
|
265
|
+
? 'Indica il file del backup: materialize restore <file>'
|
|
266
|
+
: 'Non trovo materialize.config.ts in questa cartella.'));
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
const config = await loadConfig(path);
|
|
270
|
+
const head = Buffer.alloc(16);
|
|
271
|
+
try {
|
|
272
|
+
const handle = await open(resolve(file), 'r');
|
|
273
|
+
await handle.read(head, 0, 16, 0);
|
|
274
|
+
await handle.close();
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
console.error(fail(`Non trovo il file ${file}`));
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
// A SQLite file starts with "SQLite format 3", a .tar.gz with the gzip signature.
|
|
281
|
+
const archive = head[0] === 0x1f && head[1] === 0x8b;
|
|
282
|
+
if (!archive && head.toString('latin1') !== 'SQLite format 3\0') {
|
|
283
|
+
console.error(fail(`${file} non è un backup: serve un content-….db o un materialize-….tar.gz.`));
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
const local = config.database.url.startsWith('file:');
|
|
287
|
+
if (!local && !archive) {
|
|
288
|
+
console.error(fail('Il database è ospitato da un fornitore: ripristina il backup dal suo pannello.'));
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
const running = await fetch(`http://localhost:${config.server.port}/api/setup`, { signal: AbortSignal.timeout(1500) })
|
|
292
|
+
.then(() => true)
|
|
293
|
+
.catch(() => false);
|
|
294
|
+
if (running) {
|
|
295
|
+
console.error(fail(`Il server è acceso sulla porta ${config.server.port}: fermalo, poi ripeti il comando.`));
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
const mediaDir = resolve(config.media.dir);
|
|
299
|
+
const fontsDir = join(dirname(mediaDir), 'fonts');
|
|
300
|
+
const db = local ? resolve(config.database.url.slice('file:'.length)) : null;
|
|
301
|
+
let kept = null;
|
|
302
|
+
// The images the backup uses, read before it replaces the database (one connection, opened once).
|
|
303
|
+
let keys = [];
|
|
304
|
+
if (db) {
|
|
305
|
+
const backups = join(dirname(db), 'backups');
|
|
306
|
+
await mkdir(backups, { recursive: true });
|
|
307
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
308
|
+
const d = new Date();
|
|
309
|
+
kept = join(backups, `content-${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-prima-del-ripristino.db`);
|
|
310
|
+
// Made by SQLite, which also reads what is still in the write-ahead file.
|
|
311
|
+
const current = await openDatabase(config.database);
|
|
312
|
+
await current.client.execute({ sql: 'VACUUM INTO ?', args: [kept] });
|
|
313
|
+
if (!archive) {
|
|
314
|
+
await current.client.execute({ sql: 'ATTACH DATABASE ? AS restored', args: [resolve(file)] });
|
|
315
|
+
keys = (await current.client.execute('SELECT key FROM restored.media')).rows.map((r) => String(r.key));
|
|
316
|
+
await current.client.execute('DETACH DATABASE restored');
|
|
317
|
+
}
|
|
318
|
+
current.close();
|
|
319
|
+
// The write-ahead files belong to the old database.
|
|
320
|
+
await rm(`${db}-wal`, { force: true });
|
|
321
|
+
await rm(`${db}-shm`, { force: true });
|
|
322
|
+
}
|
|
323
|
+
if (archive) {
|
|
324
|
+
// Only what belongs where it says: the database, flat media names, fonts without "..".
|
|
325
|
+
const written = await extractTarGz(resolve(file), (name) => {
|
|
326
|
+
if (name === 'content.db')
|
|
327
|
+
return db;
|
|
328
|
+
const media = /^media\/([^/\\]+)$/.exec(name)?.[1];
|
|
329
|
+
if (media && !media.startsWith('.'))
|
|
330
|
+
return join(mediaDir, media);
|
|
331
|
+
const font = /^fonts\/(.+)$/.exec(name)?.[1];
|
|
332
|
+
if (font && !font.split('/').some((part) => part === '..' || part.startsWith('.')))
|
|
333
|
+
return join(fontsDir, font);
|
|
334
|
+
return null;
|
|
335
|
+
});
|
|
336
|
+
const media = written.filter((n) => n.startsWith('media/')).length;
|
|
337
|
+
console.log(` ${ok('✓')} Archivio ripristinato: ${relative(process.cwd(), resolve(file))}`);
|
|
338
|
+
console.log(dim(` ${written.includes('content.db') ? 'Database, ' : ''}${media} file dei media, ${written.length - media - (written.includes('content.db') ? 1 : 0)} file dei caratteri.`));
|
|
339
|
+
}
|
|
340
|
+
else if (db) {
|
|
341
|
+
await copyFile(resolve(file), db);
|
|
342
|
+
// The images this database uses that are no longer in the library come back from the media copy.
|
|
343
|
+
const back = await restoreMedia(db, mediaDir, keys);
|
|
344
|
+
console.log(` ${ok('✓')} Backup ripristinato: ${relative(process.cwd(), resolve(file))}`);
|
|
345
|
+
if (back)
|
|
346
|
+
console.log(dim(` ${back} ${back === 1 ? 'immagine tornata' : 'immagini tornate'} dalla copia dei media.`));
|
|
347
|
+
}
|
|
348
|
+
if (kept)
|
|
349
|
+
console.log(dim(` Il database di prima è in ${relative(process.cwd(), kept)}, se servisse tornare indietro.`));
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* A one-time link for a new password, printed here: the way back in for someone who cannot get it
|
|
353
|
+
* by email, such as the only administrator of a site without email. Only who can run commands on
|
|
354
|
+
* the server can ask for it. The server may be running.
|
|
355
|
+
*/
|
|
356
|
+
async function resetPassword(args) {
|
|
357
|
+
const email = args
|
|
358
|
+
.find((a) => !a.startsWith('--'))
|
|
359
|
+
?.trim()
|
|
360
|
+
.toLowerCase();
|
|
361
|
+
const path = findConfig(process.cwd());
|
|
362
|
+
if (!email || !path) {
|
|
363
|
+
console.error(fail(!email
|
|
364
|
+
? 'Indica l’email: materialize reset-password <email>'
|
|
365
|
+
: 'Non trovo materialize.config.ts in questa cartella: lancia il comando dalla cartella del sito.'));
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
const config = await loadConfig(path);
|
|
369
|
+
const database = await openDatabase(config.database);
|
|
370
|
+
try {
|
|
371
|
+
const auth = createAuthStore(database.db);
|
|
372
|
+
const user = await auth.findForLogin(email);
|
|
373
|
+
if (!user) {
|
|
374
|
+
console.error(fail(`Nessun account con l’email ${email}.`));
|
|
375
|
+
process.exitCode = 1;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const { token } = await auth.createInvite({
|
|
379
|
+
kind: 'reset',
|
|
380
|
+
email: user.email,
|
|
381
|
+
name: user.name,
|
|
382
|
+
role: user.role,
|
|
383
|
+
userId: user.id,
|
|
384
|
+
ttlMs: 60 * 60 * 1000,
|
|
385
|
+
});
|
|
386
|
+
await createActivityStore(database.db).record({
|
|
387
|
+
userId: null,
|
|
388
|
+
userName: '',
|
|
389
|
+
action: 'password.reset_cli',
|
|
390
|
+
target: user.email,
|
|
391
|
+
detail: null,
|
|
392
|
+
ip: 'server',
|
|
393
|
+
});
|
|
394
|
+
const base = config.site.url?.replace(/\/+$/, '') ?? `http://localhost:${config.server.port}`;
|
|
395
|
+
console.log(` ${ok('✓')} Link per una nuova password di ${user.name} (${user.email}), valido un’ora:`);
|
|
396
|
+
console.log();
|
|
397
|
+
console.log(` ${accent(`${base}/admin/_invite/${token}`)}`);
|
|
398
|
+
console.log();
|
|
399
|
+
console.log(dim(' Chi lo apre sceglie la password. La verifica in due passaggi, se attiva, resta.'));
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
database.close();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const [command = 'help', ...args] = process.argv.slice(2);
|
|
406
|
+
if (command === 'dev' || command === 'start' || command === 'build') {
|
|
407
|
+
await run(command, args);
|
|
408
|
+
}
|
|
409
|
+
else if (command === 'mcp') {
|
|
410
|
+
await mcp(args);
|
|
411
|
+
}
|
|
412
|
+
else if (command === 'reset-password') {
|
|
413
|
+
await resetPassword(args);
|
|
414
|
+
}
|
|
415
|
+
else if (command === 'restore') {
|
|
416
|
+
await restore(args);
|
|
417
|
+
}
|
|
418
|
+
else if (command === '--version' || command === '-v') {
|
|
419
|
+
console.log(VERSION);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
console.log(HELP);
|
|
423
|
+
if (command !== 'help' && command !== '--help' && command !== '-h')
|
|
424
|
+
process.exitCode = 1;
|
|
425
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MaterializeConfig } from '@materializecms/core';
|
|
2
|
+
export declare const CONFIG_FILES: string[];
|
|
3
|
+
export declare function findConfig(cwd: string): string | null;
|
|
4
|
+
/**
|
|
5
|
+
* Imports the config fresh every time (the query string bypasses Node's module cache),
|
|
6
|
+
* so `materialize dev` can pick up schema changes without a restart.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadConfig(path: string): Promise<MaterializeConfig>;
|
|
9
|
+
/**
|
|
10
|
+
* What belongs to the server rather than to the site, from its environment: the VPS installer
|
|
11
|
+
* (install.sh) sets these in the service, so the site's own config stays the same everywhere.
|
|
12
|
+
*/
|
|
13
|
+
export declare function withEnvironment(config: MaterializeConfig, env?: NodeJS.ProcessEnv): MaterializeConfig;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
export const CONFIG_FILES = ['materialize.config.ts', 'materialize.config.js', 'materialize.config.mjs'];
|
|
13
|
+
export function findConfig(cwd) {
|
|
14
|
+
for (const name of CONFIG_FILES) {
|
|
15
|
+
const path = resolve(cwd, name);
|
|
16
|
+
if (existsSync(path))
|
|
17
|
+
return path;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Imports the config fresh every time (the query string bypasses Node's module cache),
|
|
23
|
+
* so `materialize dev` can pick up schema changes without a restart.
|
|
24
|
+
*/
|
|
25
|
+
export async function loadConfig(path) {
|
|
26
|
+
const mod = (await import(__rewriteRelativeImportExtension(`${pathToFileURL(path).href}?v=${Date.now()}`)));
|
|
27
|
+
if (!mod.default?.collections) {
|
|
28
|
+
throw new Error(`${path} must export default defineConfig({ … })`);
|
|
29
|
+
}
|
|
30
|
+
return withEnvironment(mod.default);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* What belongs to the server rather than to the site, from its environment: the VPS installer
|
|
34
|
+
* (install.sh) sets these in the service, so the site's own config stays the same everywhere.
|
|
35
|
+
*/
|
|
36
|
+
export function withEnvironment(config, env = process.env) {
|
|
37
|
+
const url = env.MATERIALIZE_SITE_URL?.trim();
|
|
38
|
+
const port = Number(env.MATERIALIZE_PORT);
|
|
39
|
+
const trust = env.MATERIALIZE_TRUST_PROXY;
|
|
40
|
+
return {
|
|
41
|
+
...config,
|
|
42
|
+
site: config.site.url || !url ? config.site : { ...config.site, url },
|
|
43
|
+
server: {
|
|
44
|
+
...config.server,
|
|
45
|
+
port: Number.isInteger(port) && port > 0 ? port : config.server.port,
|
|
46
|
+
trustProxy: trust === '1' || trust === 'true'
|
|
47
|
+
? true
|
|
48
|
+
: trust === '0' || trust === 'false'
|
|
49
|
+
? false
|
|
50
|
+
: config.server.trustProxy,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/site.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { Fonts, MediaFiles } from '@materializecms/server';
|
|
3
|
+
export declare const ASTRO_CONFIGS: string[];
|
|
4
|
+
/** True when the project has an Astro site next to materialize.config.ts. */
|
|
5
|
+
export declare const hasSite: (root: string) => boolean;
|
|
6
|
+
export type Handler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
|
7
|
+
/** Astro's dev server, mounted inside Materialize: pages render on demand from the database. */
|
|
8
|
+
export declare function startDevSite(root: string, port: number): Promise<{
|
|
9
|
+
handle: Handler;
|
|
10
|
+
close: () => Promise<void>;
|
|
11
|
+
}>;
|
|
12
|
+
/** Builds the static site into `outDir` (client/ holds the pages, server/ the preview renderer). */
|
|
13
|
+
export declare function buildSite(root: string, outDir: string): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Copies every library image the built pages use (originals and resized copies) into the
|
|
16
|
+
* static output, so `dist/` works on any host without the Materialize server.
|
|
17
|
+
*/
|
|
18
|
+
export declare function exportMedia(client: string, files: MediaFiles): Promise<number>;
|
|
19
|
+
/** Writes the self-hosted fonts the built pages link to (/fonts/<id>.css and its files) into the static site. */
|
|
20
|
+
export declare function exportFonts(client: string, fonts: Fonts): Promise<number>;
|
|
21
|
+
/**
|
|
22
|
+
* The site in production: static files from the latest build, the preview renderer on demand,
|
|
23
|
+
* and a fresh build after every publish. Builds go to a new folder and replace the live one only
|
|
24
|
+
* when complete, so visitors never see a half-written site.
|
|
25
|
+
*/
|
|
26
|
+
export declare class ProductionSite {
|
|
27
|
+
private live;
|
|
28
|
+
private building;
|
|
29
|
+
private queued;
|
|
30
|
+
private readonly root;
|
|
31
|
+
private readonly builds;
|
|
32
|
+
private readonly log;
|
|
33
|
+
constructor(root: string, log: (message: string, ok: boolean) => void);
|
|
34
|
+
/** Rebuilds now, or right after the build in progress: rapid publishes coalesce into one extra build. */
|
|
35
|
+
rebuild(): Promise<void>;
|
|
36
|
+
private buildOnce;
|
|
37
|
+
handle: Handler;
|
|
38
|
+
}
|