domma-cms 0.5.4 → 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 +40 -3
- package/bin/lib/config-merge.js +44 -0
- package/bin/update.js +542 -0
- package/config/server.json +10 -6
- package/package.json +1 -1
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 <
|
|
40
|
+
Usage: npx domma-cms <command> [options]
|
|
41
41
|
|
|
42
|
-
|
|
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
|
-
|
|
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,6 +375,19 @@ 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
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
+
}
|
package/config/server.json
CHANGED