domma-cms 0.5.3 → 0.6.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/bin/cli.js CHANGED
@@ -37,21 +37,44 @@ const positional = args.filter(a => !a.startsWith('--'));
37
37
 
38
38
  if (flags.has('--help') || args.includes('-h')) {
39
39
  console.log(`
40
- Usage: npx domma-cms <project-name> [options]
40
+ Usage: npx domma-cms <command> [options]
41
41
 
42
- Options:
42
+ Commands:
43
+ <project-name> Scaffold a new Domma CMS project
44
+ update Update an existing project to the latest version
45
+
46
+ Scaffold options:
43
47
  --no-install Skip npm install
44
48
  --no-setup Skip the interactive setup wizard
45
49
  --no-seed Skip seeding default pages, forms, and collections
46
50
  --help Show this help message
47
51
 
48
- Example:
52
+ Update options:
53
+ --yes Skip confirmation prompt
54
+ --no-backup Skip creating a backup before updating
55
+ --no-install Skip npm install after updating
56
+ --dry-run Preview what would change without writing anything
57
+
58
+ Examples:
49
59
  npx domma-cms my-blog
50
60
  npx domma-cms my-blog --no-install --no-setup
61
+ npx domma-cms update
62
+ npx domma-cms update --dry-run
63
+ npx domma-cms update --yes --no-backup
51
64
  `);
52
65
  process.exit(0);
53
66
  }
54
67
 
68
+ // ---------------------------------------------------------------------------
69
+ // Route subcommands
70
+ // ---------------------------------------------------------------------------
71
+
72
+ if (positional[0] === 'update') {
73
+ const {default: runUpdate} = await import('./update.js');
74
+ await runUpdate(positional.slice(1), flags);
75
+ process.exit(0);
76
+ }
77
+
55
78
  const projectName = positional[0];
56
79
 
57
80
  if (!projectName) {
@@ -292,6 +315,7 @@ const GITIGNORE = `node_modules/
292
315
  content/users/
293
316
  content/media/
294
317
  *.log
318
+ .domma-backups/
295
319
  `;
296
320
 
297
321
  step('Writing .gitignore');
@@ -351,10 +375,30 @@ if (noSeed) {
351
375
  console.log('');
352
376
  }
353
377
 
378
+ // ---------------------------------------------------------------------------
379
+ // Write .domma version marker
380
+ // ---------------------------------------------------------------------------
381
+
382
+ step('Writing .domma marker');
383
+ const marker = {
384
+ cmsVersion: sourcePkg.version,
385
+ scaffoldedAt: new Date().toISOString(),
386
+ lastUpdatedAt: null
387
+ };
388
+ writeFileSync(path.join(target, '.domma'), JSON.stringify(marker, null, 2) + '\n', 'utf8');
389
+ done();
390
+
354
391
  // ---------------------------------------------------------------------------
355
392
  // Success
356
393
  // ---------------------------------------------------------------------------
357
394
 
395
+ let devPort = 4096;
396
+ try {
397
+ const serverCfg = JSON.parse(readFileSync(path.join(target, 'config/server.json'), 'utf8'));
398
+ if (Number.isInteger(serverCfg.port)) devPort = serverCfg.port;
399
+ } catch { /* fallback to default */
400
+ }
401
+
358
402
  console.log('');
359
403
  console.log(' ┌──────────────────────────────────────────┐');
360
404
  console.log(' │ ✓ Project created successfully! │');
@@ -366,5 +410,5 @@ if (noInstall) console.log(` npm install`);
366
410
  if (noSetup && !noInstall) console.log(` npm run setup`);
367
411
  console.log(` npm run dev`);
368
412
  console.log('');
369
- console.log(` Then open: http://localhost:4096/admin`);
413
+ console.log(` Then open: http://localhost:${devPort}/admin`);
370
414
  console.log('');
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Domma CMS — Config Merge Utility
3
+ * Merges new keys from an upstream config into an existing user config,
4
+ * without ever overwriting values the user already has.
5
+ */
6
+
7
+ /**
8
+ * Deep-merge new keys from `upstream` into `existing`.
9
+ * Existing values are never modified — only missing keys are added.
10
+ *
11
+ * @param {object} existing - The user's current config object
12
+ * @param {object} upstream - The upstream (new version) config object
13
+ * @param {string} [_prefix] - Internal: key path prefix for reporting
14
+ * @returns {{ merged: object, added: string[] }} Merged object + list of added key paths
15
+ */
16
+ export function deepMergeNewKeys(existing, upstream, _prefix = '') {
17
+ const merged = {...existing};
18
+ const added = [];
19
+
20
+ for (const [key, upstreamVal] of Object.entries(upstream)) {
21
+ const fullKey = _prefix ? `${_prefix}.${key}` : key;
22
+
23
+ if (!(key in existing)) {
24
+ // Key is entirely missing — add it wholesale
25
+ merged[key] = upstreamVal;
26
+ added.push(fullKey);
27
+ } else if (
28
+ upstreamVal !== null &&
29
+ typeof upstreamVal === 'object' &&
30
+ !Array.isArray(upstreamVal) &&
31
+ typeof existing[key] === 'object' &&
32
+ existing[key] !== null &&
33
+ !Array.isArray(existing[key])
34
+ ) {
35
+ // Both sides are plain objects — recurse
36
+ const child = deepMergeNewKeys(existing[key], upstreamVal, fullKey);
37
+ merged[key] = child.merged;
38
+ added.push(...child.added);
39
+ }
40
+ // Otherwise: existing value wins — no action
41
+ }
42
+
43
+ return {merged, added};
44
+ }
package/bin/update.js ADDED
@@ -0,0 +1,542 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Domma CMS — Project Update
4
+ * Usage: npx domma-cms update [--yes] [--no-backup] [--no-install] [--dry-run]
5
+ *
6
+ * Run from inside an existing Domma CMS project directory.
7
+ * Pulls upstream changes from the installed CLI package into the project,
8
+ * preserving all user content, config, and custom plugins.
9
+ */
10
+
11
+ import {cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync,} from 'node:fs';
12
+ import {spawnSync} from 'node:child_process';
13
+ import path from 'node:path';
14
+ import {createInterface} from 'node:readline';
15
+ import {fileURLToPath} from 'node:url';
16
+ import {deepMergeNewKeys} from './lib/config-merge.js';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Paths
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Helpers
28
+ // ---------------------------------------------------------------------------
29
+
30
+ function step(label) {
31
+ process.stdout.write(` ${label}…`);
32
+ }
33
+
34
+ function done(note = '') {
35
+ console.log(note ? ` done. ${note}` : ' done.');
36
+ }
37
+
38
+ function info(msg) {
39
+ console.log(` ${msg}`);
40
+ }
41
+
42
+ function warn(msg) {
43
+ console.log(` ⚠ ${msg}`);
44
+ }
45
+
46
+ function readJson(filePath) {
47
+ return JSON.parse(readFileSync(filePath, 'utf8'));
48
+ }
49
+
50
+ function writeJson(filePath, obj) {
51
+ writeFileSync(filePath, JSON.stringify(obj, null, 4) + '\n', 'utf8');
52
+ }
53
+
54
+ /**
55
+ * Prompt the user for a yes/no confirmation.
56
+ * @param {string} question
57
+ * @returns {Promise<boolean>}
58
+ */
59
+ function confirm(question) {
60
+ return new Promise(resolve => {
61
+ const rl = createInterface({input: process.stdin, output: process.stdout});
62
+ rl.question(` ${question} [y/N] `, answer => {
63
+ rl.close();
64
+ resolve(/^y(es)?$/i.test(answer.trim()));
65
+ });
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Recursively list all relative file paths under a directory.
71
+ * @param {string} dir - Absolute directory path
72
+ * @param {string} [base] - Base path for relative output (defaults to dir)
73
+ * @returns {string[]}
74
+ */
75
+ function listFiles(dir, base = dir) {
76
+ if (!existsSync(dir)) return [];
77
+ const result = [];
78
+ for (const entry of readdirSync(dir, {withFileTypes: true})) {
79
+ const full = path.join(dir, entry.name);
80
+ if (entry.isDirectory()) {
81
+ result.push(...listFiles(full, base));
82
+ } else {
83
+ result.push(path.relative(base, full));
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Upstream dirs that are fully replaced on update
91
+ // ---------------------------------------------------------------------------
92
+
93
+ const UPSTREAM_DIRS = ['server', 'admin', 'public', 'scripts'];
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Structural config files that get new-key merging (never full replace)
97
+ // ---------------------------------------------------------------------------
98
+
99
+ const STRUCTURAL_CONFIGS = [
100
+ 'config/server.json',
101
+ 'config/auth.json',
102
+ 'config/content.json',
103
+ 'config/presets.json',
104
+ ];
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Main update function
108
+ // ---------------------------------------------------------------------------
109
+
110
+ /**
111
+ * @param {string[]} _positional - Remaining positional args (unused)
112
+ * @param {Set<string>} flags - Parsed CLI flags
113
+ */
114
+ export default async function update(_positional, flags) {
115
+ const isDryRun = flags.has('--dry-run');
116
+ const skipConfirm = flags.has('--yes');
117
+ const skipBackup = flags.has('--no-backup');
118
+ const skipInstall = flags.has('--no-install');
119
+
120
+ const cwd = process.cwd();
121
+
122
+ // -------------------------------------------------------------------------
123
+ // 1. Detect project
124
+ // -------------------------------------------------------------------------
125
+
126
+ console.log('');
127
+ console.log(' ┌──────────────────────────────────────────┐');
128
+ console.log(' │ │');
129
+ console.log(' │ Domma CMS — Project Updater │');
130
+ console.log(' │ │');
131
+ console.log(' └──────────────────────────────────────────┘');
132
+ console.log('');
133
+
134
+ // Refuse to run inside the CLI package itself
135
+ if (path.resolve(cwd) === path.resolve(PACKAGE_ROOT)) {
136
+ console.error(' ✗ Run this command from inside your scaffolded project, not the CLI package.\n');
137
+ process.exit(1);
138
+ }
139
+
140
+ const requiredFiles = ['package.json', 'server/server.js', 'config'];
141
+ for (const f of requiredFiles) {
142
+ if (!existsSync(path.join(cwd, f))) {
143
+ console.error(` ✗ Not a Domma CMS project directory (missing ${f}).\n`);
144
+ console.error(' Run this command from inside your project folder.\n');
145
+ process.exit(1);
146
+ }
147
+ }
148
+
149
+ // Scaffolded projects are always private — reject if not
150
+ try {
151
+ const cwdPkg = readJson(path.join(cwd, 'package.json'));
152
+ if (!cwdPkg.private) {
153
+ console.error(' ✗ This does not appear to be a scaffolded Domma CMS project (package.json is not private).\n');
154
+ process.exit(1);
155
+ }
156
+ } catch { /* malformed package.json — let it proceed */
157
+ }
158
+
159
+ // -------------------------------------------------------------------------
160
+ // 2. Read version information
161
+ // -------------------------------------------------------------------------
162
+
163
+ const markerPath = path.join(cwd, '.domma');
164
+ let installedVersion = 'unknown';
165
+ let markerData = null;
166
+
167
+ if (existsSync(markerPath)) {
168
+ try {
169
+ markerData = readJson(markerPath);
170
+ installedVersion = markerData.cmsVersion ?? 'unknown';
171
+ } catch {
172
+ warn('Could not read .domma marker — treating as unknown version.');
173
+ }
174
+ } else {
175
+ warn('No .domma marker found. This project was scaffolded before version tracking was introduced.');
176
+ warn('The updater will proceed, but cannot guarantee what was previously installed.');
177
+ console.log('');
178
+ }
179
+
180
+ const sourcePkg = readJson(path.join(PACKAGE_ROOT, 'package.json'));
181
+ const incomingVersion = sourcePkg.version;
182
+
183
+ info(`Current: ${installedVersion}`);
184
+ info(`Available: ${incomingVersion}`);
185
+ console.log('');
186
+
187
+ if (installedVersion !== 'unknown' && installedVersion === incomingVersion) {
188
+ info('✓ Already up to date.');
189
+ console.log('');
190
+ process.exit(0);
191
+ }
192
+
193
+ // -------------------------------------------------------------------------
194
+ // 3. Dry-run mode — preview changes and exit
195
+ // -------------------------------------------------------------------------
196
+
197
+ if (isDryRun) {
198
+ info('Dry run — no files will be modified.\n');
199
+
200
+ info('Upstream directories that would be replaced:');
201
+ for (const dir of UPSTREAM_DIRS) {
202
+ info(` → ${dir}/`);
203
+ }
204
+ console.log('');
205
+
206
+ info('Structural configs that would be merged (new keys only):');
207
+ for (const cfg of STRUCTURAL_CONFIGS) {
208
+ if (existsSync(path.join(cwd, cfg))) {
209
+ const existing = readJson(path.join(cwd, cfg));
210
+ const upstream = existsSync(path.join(PACKAGE_ROOT, cfg))
211
+ ? readJson(path.join(PACKAGE_ROOT, cfg))
212
+ : {};
213
+ const {added} = deepMergeNewKeys(existing, upstream);
214
+ if (added.length > 0) {
215
+ info(` ${cfg}: would add ${added.length} key(s): ${added.join(', ')}`);
216
+ } else {
217
+ info(` ${cfg}: no new keys`);
218
+ }
219
+ }
220
+ }
221
+ console.log('');
222
+
223
+ info('Plugins that would be updated:');
224
+ const pluginsDirSrc = path.join(PACKAGE_ROOT, 'plugins');
225
+ const pluginsDirDst = path.join(cwd, 'plugins');
226
+ if (existsSync(pluginsDirSrc)) {
227
+ const upstreamPlugins = readdirSync(pluginsDirSrc, {withFileTypes: true})
228
+ .filter(e => e.isDirectory())
229
+ .map(e => e.name);
230
+ for (const name of upstreamPlugins) {
231
+ const exists = existsSync(path.join(pluginsDirDst, name));
232
+ info(` → plugins/${name}/ (${exists ? 'update' : 'add'})`);
233
+ }
234
+ }
235
+ console.log('');
236
+
237
+ info('Package.json: CMS deps would be replaced; user-added deps preserved.');
238
+ console.log('');
239
+ process.exit(0);
240
+ }
241
+
242
+ // -------------------------------------------------------------------------
243
+ // 4. Confirmation prompt
244
+ // -------------------------------------------------------------------------
245
+
246
+ if (!skipConfirm) {
247
+ const confirmed = await confirm(`Update project from ${installedVersion} to ${incomingVersion}?`);
248
+ if (!confirmed) {
249
+ info('Update cancelled.');
250
+ console.log('');
251
+ process.exit(0);
252
+ }
253
+ console.log('');
254
+ }
255
+
256
+ // -------------------------------------------------------------------------
257
+ // 5. Backup
258
+ // -------------------------------------------------------------------------
259
+
260
+ let backupPath = null;
261
+
262
+ if (!skipBackup) {
263
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
264
+ backupPath = path.join(cwd, '.domma-backups', timestamp);
265
+ step('Creating backup');
266
+ mkdirSync(backupPath, {recursive: true});
267
+
268
+ // Back up upstream dirs
269
+ for (const dir of UPSTREAM_DIRS) {
270
+ const src = path.join(cwd, dir);
271
+ if (existsSync(src)) {
272
+ cpSync(src, path.join(backupPath, dir), {recursive: true});
273
+ }
274
+ }
275
+
276
+ // Back up package.json and structural configs
277
+ for (const f of ['package.json', ...STRUCTURAL_CONFIGS]) {
278
+ const src = path.join(cwd, f);
279
+ if (existsSync(src)) {
280
+ const dest = path.join(backupPath, f);
281
+ mkdirSync(path.dirname(dest), {recursive: true});
282
+ cpSync(src, dest);
283
+ }
284
+ }
285
+
286
+ // Back up plugins dir (code only, not data)
287
+ const pluginsDir = path.join(cwd, 'plugins');
288
+ if (existsSync(pluginsDir)) {
289
+ cpSync(pluginsDir, path.join(backupPath, 'plugins'), {recursive: true});
290
+ }
291
+
292
+ done();
293
+ } else {
294
+ warn('Skipping backup (--no-backup).');
295
+ }
296
+
297
+ // -------------------------------------------------------------------------
298
+ // 6. Replace upstream directories
299
+ // -------------------------------------------------------------------------
300
+
301
+ step('Replacing upstream code');
302
+ for (const dir of UPSTREAM_DIRS) {
303
+ const dest = path.join(cwd, dir);
304
+ const src = path.join(PACKAGE_ROOT, dir);
305
+ if (!existsSync(src)) continue;
306
+ if (existsSync(dest)) rmSync(dest, {recursive: true, force: true});
307
+ cpSync(src, dest, {recursive: true});
308
+ }
309
+ done();
310
+
311
+ // -------------------------------------------------------------------------
312
+ // 7. Update plugins
313
+ // -------------------------------------------------------------------------
314
+
315
+ step('Updating plugins');
316
+ const pluginsDirSrc = path.join(PACKAGE_ROOT, 'plugins');
317
+ const pluginsDirDst = path.join(cwd, 'plugins');
318
+ const newPlugins = [];
319
+
320
+ if (existsSync(pluginsDirSrc)) {
321
+ mkdirSync(pluginsDirDst, {recursive: true});
322
+ const upstreamPlugins = readdirSync(pluginsDirSrc, {withFileTypes: true})
323
+ .filter(e => e.isDirectory())
324
+ .map(e => e.name);
325
+
326
+ for (const name of upstreamPlugins) {
327
+ const srcPlugin = path.join(pluginsDirSrc, name);
328
+ const dstPlugin = path.join(pluginsDirDst, name);
329
+ const isNew = !existsSync(dstPlugin);
330
+
331
+ if (isNew) {
332
+ // Brand-new plugin — copy wholesale, apply scaffold.reset
333
+ cpSync(srcPlugin, dstPlugin, {recursive: true});
334
+ try {
335
+ const manifest = readJson(path.join(dstPlugin, 'plugin.json'));
336
+ for (const {path: relPath, content} of (manifest.scaffold?.reset ?? [])) {
337
+ const absPath = path.join(dstPlugin, relPath);
338
+ mkdirSync(path.dirname(absPath), {recursive: true});
339
+ writeFileSync(absPath, content + '\n', 'utf8');
340
+ }
341
+ } catch { /* no manifest — skip reset */
342
+ }
343
+ newPlugins.push(name);
344
+ } else {
345
+ // Existing plugin — identify paths to preserve
346
+ let preservePaths = new Set(['data']);
347
+ try {
348
+ const manifest = readJson(path.join(srcPlugin, 'plugin.json'));
349
+ for (const {path: relPath} of (manifest.scaffold?.reset ?? [])) {
350
+ preservePaths.add(relPath);
351
+ }
352
+ } catch { /* no manifest */
353
+ }
354
+
355
+ // Back up preserved paths from current install
356
+ const preserved = new Map();
357
+ for (const rel of preservePaths) {
358
+ const abs = path.join(dstPlugin, rel);
359
+ if (!existsSync(abs)) continue;
360
+
361
+ if (statSync(abs).isDirectory()) {
362
+ // Preserve all files under this directory
363
+ for (const f of listFiles(abs, dstPlugin)) {
364
+ preserved.set(f, readFileSync(path.join(dstPlugin, f)));
365
+ }
366
+ } else {
367
+ // scaffold.reset entry — preserve the single file
368
+ preserved.set(rel, readFileSync(abs));
369
+ }
370
+ }
371
+
372
+ // Replace plugin directory with upstream
373
+ rmSync(dstPlugin, {recursive: true, force: true});
374
+ cpSync(srcPlugin, dstPlugin, {recursive: true});
375
+
376
+ // Restore preserved files
377
+ for (const [rel, content] of preserved) {
378
+ const abs = path.join(dstPlugin, rel);
379
+ mkdirSync(path.dirname(abs), {recursive: true});
380
+ writeFileSync(abs, content);
381
+ }
382
+ }
383
+ }
384
+ }
385
+ done();
386
+
387
+ // -------------------------------------------------------------------------
388
+ // 8. Register new plugins in plugins.json (disabled by default)
389
+ // -------------------------------------------------------------------------
390
+
391
+ const pluginsJsonPath = path.join(cwd, 'config/plugins.json');
392
+ if (newPlugins.length > 0 && existsSync(pluginsJsonPath)) {
393
+ try {
394
+ const pluginsCfg = readJson(pluginsJsonPath);
395
+ for (const name of newPlugins) {
396
+ if (!(name in pluginsCfg)) {
397
+ pluginsCfg[name] = {enabled: false, settings: {}};
398
+ }
399
+ }
400
+ writeJson(pluginsJsonPath, pluginsCfg);
401
+ } catch { /* if plugins.json is malformed, skip */
402
+ }
403
+ }
404
+
405
+ // -------------------------------------------------------------------------
406
+ // 9. Merge structural configs (new keys only)
407
+ // -------------------------------------------------------------------------
408
+
409
+ step('Merging structural configs');
410
+ const mergedKeys = {};
411
+
412
+ for (const cfgFile of STRUCTURAL_CONFIGS) {
413
+ const dstPath = path.join(cwd, cfgFile);
414
+ const srcPath = path.join(PACKAGE_ROOT, cfgFile);
415
+ if (!existsSync(dstPath) || !existsSync(srcPath)) continue;
416
+
417
+ try {
418
+ const existing = readJson(dstPath);
419
+ const upstream = readJson(srcPath);
420
+ const {merged, added} = deepMergeNewKeys(existing, upstream);
421
+ if (added.length > 0) {
422
+ writeJson(dstPath, merged);
423
+ mergedKeys[cfgFile] = added;
424
+ }
425
+ } catch { /* malformed JSON — skip */
426
+ }
427
+ }
428
+ done();
429
+
430
+ // -------------------------------------------------------------------------
431
+ // 10. Update package.json: replace CMS deps, preserve user-added deps
432
+ // -------------------------------------------------------------------------
433
+
434
+ step('Updating package.json');
435
+ const projectPkgPath = path.join(cwd, 'package.json');
436
+ try {
437
+ const projectPkg = readJson(projectPkgPath);
438
+ const cmsDeps = sourcePkg.dependencies ?? {};
439
+ const cmsOptional = sourcePkg.optionalDependencies ?? {};
440
+ const userDeps = projectPkg.dependencies ?? {};
441
+
442
+ // Preserve deps the user added (not in CMS deps at time of scaffold)
443
+ // We identify "user-added" as: present in project but not in the new upstream CMS deps
444
+ const userAddedDeps = {};
445
+ for (const [pkg, ver] of Object.entries(userDeps)) {
446
+ if (!(pkg in cmsDeps) && !(pkg in cmsOptional)) {
447
+ userAddedDeps[pkg] = ver;
448
+ }
449
+ }
450
+
451
+ // Replace CMS scripts, but never copy prepublishOnly
452
+ const newScripts = {...sourcePkg.scripts};
453
+ delete newScripts.prepublishOnly;
454
+
455
+ projectPkg.dependencies = {...cmsDeps, ...userAddedDeps};
456
+ projectPkg.optionalDependencies = cmsOptional;
457
+ projectPkg.scripts = newScripts;
458
+
459
+ writeFileSync(projectPkgPath, JSON.stringify(projectPkg, null, 2) + '\n', 'utf8');
460
+ } catch { /* malformed package.json — skip */
461
+ }
462
+ done();
463
+
464
+ // -------------------------------------------------------------------------
465
+ // 11. Ensure .domma-backups/ is in .gitignore
466
+ // -------------------------------------------------------------------------
467
+
468
+ const gitignorePath = path.join(cwd, '.gitignore');
469
+ const backupEntry = '.domma-backups/';
470
+ if (existsSync(gitignorePath)) {
471
+ const current = readFileSync(gitignorePath, 'utf8');
472
+ if (!current.includes(backupEntry)) {
473
+ writeFileSync(gitignorePath, current.trimEnd() + '\n' + backupEntry + '\n', 'utf8');
474
+ }
475
+ } else {
476
+ writeFileSync(gitignorePath, backupEntry + '\n', 'utf8');
477
+ }
478
+
479
+ // -------------------------------------------------------------------------
480
+ // 12. Write updated .domma marker
481
+ // -------------------------------------------------------------------------
482
+
483
+ step('Updating .domma marker');
484
+ const newMarker = {
485
+ cmsVersion: incomingVersion,
486
+ scaffoldedAt: markerData?.scaffoldedAt ?? null,
487
+ lastUpdatedAt: new Date().toISOString(),
488
+ };
489
+ writeFileSync(markerPath, JSON.stringify(newMarker, null, 2) + '\n', 'utf8');
490
+ done();
491
+
492
+ // -------------------------------------------------------------------------
493
+ // 13. npm install
494
+ // -------------------------------------------------------------------------
495
+
496
+ console.log('');
497
+
498
+ if (skipInstall) {
499
+ warn('Skipping npm install (--no-install).');
500
+ } else {
501
+ info('Running npm install…');
502
+ console.log('');
503
+ const result = spawnSync('npm', ['install'], {cwd, stdio: 'inherit'});
504
+ if (result.status !== 0) {
505
+ console.error('\n ✗ npm install failed.\n');
506
+ process.exit(result.status ?? 1);
507
+ }
508
+ console.log('');
509
+ }
510
+
511
+ // -------------------------------------------------------------------------
512
+ // 14. Summary
513
+ // -------------------------------------------------------------------------
514
+
515
+ console.log(' ┌──────────────────────────────────────────┐');
516
+ console.log(' │ ✓ Update complete! │');
517
+ console.log(' └──────────────────────────────────────────┘');
518
+ console.log('');
519
+
520
+ info(` ${installedVersion} → ${incomingVersion}`);
521
+ console.log('');
522
+
523
+ if (backupPath) {
524
+ info(`Backup saved to: .domma-backups/${path.basename(backupPath)}/`);
525
+ }
526
+
527
+ if (newPlugins.length > 0) {
528
+ info(`New plugins added (disabled by default): ${newPlugins.join(', ')}`);
529
+ }
530
+
531
+ const allMergedKeys = Object.entries(mergedKeys);
532
+ if (allMergedKeys.length > 0) {
533
+ info('New config keys added:');
534
+ for (const [file, keys] of allMergedKeys) {
535
+ info(` ${file}: ${keys.join(', ')}`);
536
+ }
537
+ }
538
+
539
+ console.log('');
540
+ info('Restart your dev server to apply changes: npm run dev');
541
+ console.log('');
542
+ }
@@ -1,6 +1,10 @@
1
- {
2
- "port": 4096,
3
- "host": "0.0.0.0",
4
- "cors": { "origin": false },
5
- "uploads": { "maxFileSize": 10485760 }
6
- }
1
+ {
2
+ "port": 4096,
3
+ "host": "0.0.0.0",
4
+ "cors": {
5
+ "origin": false
6
+ },
7
+ "uploads": {
8
+ "maxFileSize": 10485760
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -29,7 +29,7 @@
29
29
  "delete-users": "node -e \"import('fs').then(({readdirSync,rmSync})=>{const d='content/users';readdirSync(d).filter(f=>f.endsWith('.json')).forEach(f=>{rmSync(d+'/'+f);console.log('deleted',f)})})\"",
30
30
  "start": "node server/server.js",
31
31
  "start:cluster": "pm2 start server/server.js -i max --name domma-cms",
32
- "dev": "PORT=4096 node --watch server/server.js",
32
+ "dev": "node --watch server/server.js",
33
33
  "prod": "node server/server.js",
34
34
  "setup": "node scripts/setup.js",
35
35
  "reset": "node scripts/reset.js",
package/scripts/setup.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * 2. Create admin account (skips if users already exist)
11
11
  * 3. Set site title and tagline
12
12
  * 4. Pick a theme
13
+ * 5. Set server port
13
14
  */
14
15
  import {createInterface} from 'node:readline/promises';
15
16
  import {randomBytes} from 'node:crypto';
@@ -43,8 +44,11 @@ const THEMES = [
43
44
  'christmas-dark', 'christmas-light',
44
45
  'unicorn-dark', 'unicorn-light',
45
46
  'dreamy-dark', 'dreamy-light',
47
+ 'wedding-dark', 'wedding-light',
46
48
  ];
47
49
 
50
+ const SERVER_CONFIG = path.join(ROOT, 'config', 'server.json');
51
+
48
52
  // ---------------------------------------------------------------------------
49
53
  // Utilities
50
54
  // ---------------------------------------------------------------------------
@@ -256,6 +260,26 @@ THEMES.forEach((t, i) => {
256
260
  console.log(` ✓ Theme set to "${theme}"`);
257
261
  }
258
262
 
263
+ // ---------------------------------------------------------------------------
264
+ // Step 5 — Port
265
+ // ---------------------------------------------------------------------------
266
+ section('5. Server Port');
267
+
268
+ {
269
+ const rl = createInterface({input: process.stdin, output: process.stdout});
270
+ const input = (await rl.question(' Port (Enter = 4096): ')).trim();
271
+ rl.close();
272
+
273
+ const parsed = parseInt(input, 10);
274
+ const port = (parsed >= 1024 && parsed <= 65535) ? parsed : 4096;
275
+
276
+ const server = JSON.parse(await readFile(SERVER_CONFIG, 'utf8'));
277
+ server.port = port;
278
+ await writeFile(SERVER_CONFIG, JSON.stringify(server, null, 4) + '\n', 'utf8');
279
+
280
+ console.log(` ✓ Port set to ${port}`);
281
+ }
282
+
259
283
  // ---------------------------------------------------------------------------
260
284
  // Done
261
285
  // ---------------------------------------------------------------------------
@@ -113,7 +113,6 @@ export async function renderPage(page) {
113
113
  site.cookieConsent?.enabled ? '<script src="/public/js/cookie-consent.js"></script>' : ''
114
114
  ].filter(Boolean).join('\n'),
115
115
  dconfigScript,
116
- autoThemeScript: autoTheme ? buildAutoThemeScript(autoTheme) : '',
117
116
  };
118
117
 
119
118
  const finalVars = applyTransforms('render:beforeRender', vars, {page});
@@ -206,23 +205,6 @@ function buildFontVars(fontFamily, fontSize) {
206
205
  return {fontLink, fontOverride: rules.length ? rules.join(' ') : null};
207
206
  }
208
207
 
209
- /**
210
- * Build a self-executing inline script that applies the correct day/night theme
211
- * before the page uncloak — eliminates any flash of wrong theme.
212
- *
213
- * @param {object} at - autoTheme config object
214
- * @returns {string}
215
- */
216
- function buildAutoThemeScript(at) {
217
- const json = JSON.stringify(at).replace(/<\/script>/gi, '<\\/script>');
218
- return `(function(){var c=${json},n=new Date(),m=n.getHours()*60+n.getMinutes(),`
219
- + `ds=(c.dayStart||"07:00").split(":"),ns=(c.nightStart||"19:00").split(":"),`
220
- + `d=+ds[0]*60+(+ds[1]||0),e=+ns[0]*60+(+ns[1]||0),`
221
- + `t=(m>=d&&m<e)?c.dayTheme:c.nightTheme;`
222
- + `if(window.Domma&&window.Domma.theme)window.Domma.theme.set(t);`
223
- + `}());`;
224
- }
225
-
226
208
  /**
227
209
  * Simple template interpolation.
228
210
  * Handles {{variable}}, {{#if flag}}...{{/if}}.
@@ -101,13 +101,16 @@
101
101
  if (window.Domma && window.Domma.theme) {
102
102
  window.Domma.theme.init({ theme: '{{theme}}', persist: false });
103
103
  }
104
- {
105
- {
106
- autoThemeScript
107
- }
108
- }
109
104
  window.__CMS_NAV__ = {{navJson}};
110
105
  window.__CMS_SITE__ = {{siteJson}};
106
+ (function () {
107
+ var c = window.__CMS_SITE__ && window.__CMS_SITE__.autoTheme;
108
+ if (!c || !c.enabled) return;
109
+ var n = new Date(), m = n.getHours() * 60 + n.getMinutes(), ds = (c.dayStart || "07:00").split(":"),
110
+ ns = (c.nightStart || "19:00").split(":"), d = +ds[0] * 60 + (+ds[1] || 0),
111
+ e = +ns[0] * 60 + (+ns[1] || 0), t = (m >= d && m < e) ? c.dayTheme : c.nightTheme;
112
+ if (window.Domma && window.Domma.theme) window.Domma.theme.set(t);
113
+ }());
111
114
  {{dconfigScript}}
112
115
  </script>
113
116