create-wordjs 1.5.1 → 1.5.3
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/index.js +209 -8
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -39,22 +39,33 @@ const crypto = require('crypto');
|
|
|
39
39
|
const { spawn, spawnSync } = require('child_process');
|
|
40
40
|
|
|
41
41
|
const HELP = `
|
|
42
|
-
create-wordjs — bootstrap a WordJS site with one command
|
|
42
|
+
create-wordjs — bootstrap or upgrade a WordJS site with one command
|
|
43
43
|
|
|
44
44
|
Usage:
|
|
45
|
-
npx create-wordjs <dir> [options]
|
|
45
|
+
npx create-wordjs <dir> [options] Create a new site
|
|
46
|
+
npx create-wordjs upgrade [dir] [options] Upgrade an existing site (dir defaults to .)
|
|
46
47
|
|
|
47
48
|
Options:
|
|
48
49
|
--zip <path-or-url> Use a local release ZIP (or a direct ZIP URL) instead of asking GitHub.
|
|
49
|
-
--version <tag> Install a specific release tag (e.g. v1.0.0) instead of the latest.
|
|
50
|
-
--http Serve plain HTTP instead of self-signed HTTPS (sets WORDJS_HTTP=1).
|
|
51
|
-
--no-start Scaffold + install dependencies only; don't start the server.
|
|
50
|
+
--version <tag> Install/upgrade to a specific release tag (e.g. v1.0.0) instead of the latest.
|
|
51
|
+
--http Serve plain HTTP instead of self-signed HTTPS (sets WORDJS_HTTP=1). (create)
|
|
52
|
+
--no-start Scaffold + install dependencies only; don't start the server. (create)
|
|
53
|
+
--yes, -y Skip the confirmation prompt (required when upgrading non-interactively).
|
|
54
|
+
--force Re-apply even if already on the target version. (upgrade)
|
|
55
|
+
--no-install Swap the code only; skip 'npm run release:install'. (upgrade)
|
|
52
56
|
-h, --help Show this help.
|
|
53
57
|
|
|
54
58
|
Examples:
|
|
55
59
|
npx create-wordjs my-site
|
|
56
60
|
npx create-wordjs my-site --version v1.0.0
|
|
57
|
-
npx create-wordjs
|
|
61
|
+
npx create-wordjs upgrade # from inside your site directory
|
|
62
|
+
npx create-wordjs upgrade ./my-site --yes
|
|
63
|
+
npx create-wordjs upgrade --version v1.5.2
|
|
64
|
+
|
|
65
|
+
Upgrading preserves your database (backend/data), uploads (backend/uploads), config
|
|
66
|
+
(wordjs-config.json + gateway secrets) and any user-installed plugins; it replaces the app code and
|
|
67
|
+
runs the dependency install. Database schema migrations apply automatically the next time the server
|
|
68
|
+
starts — then restart WordJS (e.g. 'systemctl restart wordjs', or stop it and 'npm run start:mono').
|
|
58
69
|
`;
|
|
59
70
|
|
|
60
71
|
function fail(message, hint) {
|
|
@@ -65,7 +76,9 @@ function fail(message, hint) {
|
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
function parseArgs(argv) {
|
|
68
|
-
const opts = { dir: null, zip: null, version: null, http: false, start: true };
|
|
79
|
+
const opts = { mode: 'create', dir: null, zip: null, version: null, http: false, start: true, yes: false, force: false, install: true };
|
|
80
|
+
// First positional "upgrade" selects the upgrade command (npx create-wordjs upgrade [dir]).
|
|
81
|
+
if (argv[0] === 'upgrade') { opts.mode = 'upgrade'; argv = argv.slice(1); }
|
|
69
82
|
for (let i = 0; i < argv.length; i++) {
|
|
70
83
|
const a = argv[i];
|
|
71
84
|
if (a === '-h' || a === '--help') { console.log(HELP); process.exit(0); }
|
|
@@ -73,11 +86,17 @@ function parseArgs(argv) {
|
|
|
73
86
|
else if (a === '--version') { opts.version = argv[++i] || fail('--version needs a value (a release tag, e.g. v1.0.0).'); }
|
|
74
87
|
else if (a === '--http') opts.http = true;
|
|
75
88
|
else if (a === '--no-start') opts.start = false;
|
|
89
|
+
else if (a === '--yes' || a === '-y') opts.yes = true;
|
|
90
|
+
else if (a === '--force') opts.force = true;
|
|
91
|
+
else if (a === '--no-install') opts.install = false;
|
|
76
92
|
else if (a.startsWith('-')) fail(`Unknown option: ${a}`, 'Run with --help to see the available options.');
|
|
77
93
|
else if (!opts.dir) opts.dir = a;
|
|
78
94
|
else fail(`Unexpected extra argument: ${a}`);
|
|
79
95
|
}
|
|
80
|
-
if (!opts.dir)
|
|
96
|
+
if (!opts.dir) {
|
|
97
|
+
if (opts.mode === 'upgrade') opts.dir = '.'; // upgrade defaults to the current directory
|
|
98
|
+
else fail('Please specify a directory for your new site.', 'Example: npx create-wordjs my-site');
|
|
99
|
+
}
|
|
81
100
|
if (opts.version && /^\d/.test(opts.version)) opts.version = 'v' + opts.version; // accept "1.0.0" for "v1.0.0"
|
|
82
101
|
return opts;
|
|
83
102
|
}
|
|
@@ -226,10 +245,192 @@ function ensureHttpsConfig(targetDir) {
|
|
|
226
245
|
}
|
|
227
246
|
}
|
|
228
247
|
|
|
248
|
+
// --- upgrade -----------------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
function confirm(question) {
|
|
251
|
+
return new Promise((resolve) => {
|
|
252
|
+
const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
|
|
253
|
+
rl.question(question, (ans) => { rl.close(); resolve(/^y(es)?$/i.test(String(ans).trim())); });
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Paths (relative to the install root) that hold USER STATE and must survive an upgrade untouched.
|
|
258
|
+
// `node_modules` at any depth is skipped separately (deps are re-synced by release:install).
|
|
259
|
+
const PRESERVE_ON_UPGRADE = new Set([
|
|
260
|
+
'backend/data', // the database (+ WAL/SHM, ssl/, imports/)
|
|
261
|
+
'backend/uploads', // user uploads / media / fonts
|
|
262
|
+
'backend/wordjs-config.json', // site config + secrets
|
|
263
|
+
'backend/.env',
|
|
264
|
+
'.env',
|
|
265
|
+
'gateway/gateway-config.json', // gateway TLS/secrets
|
|
266
|
+
'wordjs-config.json',
|
|
267
|
+
]);
|
|
268
|
+
// Pure build outputs (no user data): removed before the copy so the new build fully REPLACES the old
|
|
269
|
+
// one — a merge would leave orphaned chunks from the previous version behind.
|
|
270
|
+
const CLEAN_REPLACE_ON_UPGRADE = ['frontend/.next', 'backend/dist', 'gateway/dist'];
|
|
271
|
+
|
|
272
|
+
// Recursively copy `src` over `dest`, creating dirs as needed. Never deletes files that aren't in
|
|
273
|
+
// `src` (so user-installed plugins and other extra files survive). Skips node_modules and the
|
|
274
|
+
// preserve-list so user state is never overwritten.
|
|
275
|
+
function copyMerge(src, dest, rel = '') {
|
|
276
|
+
for (const name of fs.readdirSync(src)) {
|
|
277
|
+
const relPath = rel ? `${rel}/${name}` : name;
|
|
278
|
+
if (name === 'node_modules') continue;
|
|
279
|
+
if (PRESERVE_ON_UPGRADE.has(relPath)) continue;
|
|
280
|
+
const s = path.join(src, name);
|
|
281
|
+
const d = path.join(dest, name);
|
|
282
|
+
const st = fs.lstatSync(s);
|
|
283
|
+
if (st.isDirectory()) {
|
|
284
|
+
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
285
|
+
copyMerge(s, d, relPath);
|
|
286
|
+
} else {
|
|
287
|
+
fs.copyFileSync(s, d);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Download (or use a local/URL) release ZIP and extract it into a fresh temp dir. Returns the
|
|
293
|
+
// extracted app root + a cleanup fn. Reuses the same resolution the create flow uses.
|
|
294
|
+
async function obtainReleaseToTemp(opts) {
|
|
295
|
+
let tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wordjs-upgrade-'));
|
|
296
|
+
const cleanup = () => { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* best-effort */ } };
|
|
297
|
+
try {
|
|
298
|
+
let zipPath;
|
|
299
|
+
let tag = opts.version || null;
|
|
300
|
+
if (opts.zip && !/^https?:\/\//i.test(opts.zip)) {
|
|
301
|
+
zipPath = path.resolve(process.cwd(), opts.zip);
|
|
302
|
+
if (!fs.existsSync(zipPath)) fail(`ZIP not found: ${zipPath}`);
|
|
303
|
+
} else {
|
|
304
|
+
let url = opts.zip;
|
|
305
|
+
let name = 'wordjs.zip';
|
|
306
|
+
if (!url) {
|
|
307
|
+
console.log(opts.version ? ` Looking up release ${opts.version} of ${REPO}…` : ` Looking up the latest release of ${REPO}…`);
|
|
308
|
+
const asset = await resolveReleaseAsset(opts.version);
|
|
309
|
+
url = asset.url; name = asset.name; tag = asset.tag;
|
|
310
|
+
console.log(` Found ${asset.tag} → ${asset.name}`);
|
|
311
|
+
}
|
|
312
|
+
zipPath = path.join(tmpDir, name);
|
|
313
|
+
await download(url, zipPath, name);
|
|
314
|
+
}
|
|
315
|
+
const extractDir = path.join(tmpDir, 'extracted');
|
|
316
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
317
|
+
extractZip(zipPath, extractDir);
|
|
318
|
+
return { extractDir, tag, cleanup };
|
|
319
|
+
} catch (e) {
|
|
320
|
+
cleanup();
|
|
321
|
+
throw e;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function upgrade(opts) {
|
|
326
|
+
const installDir = path.resolve(process.cwd(), opts.dir);
|
|
327
|
+
const pkgPath = path.join(installDir, 'package.json');
|
|
328
|
+
const cfgPath = path.join(installDir, 'backend', 'wordjs-config.json');
|
|
329
|
+
|
|
330
|
+
// Verify this is a real, configured WordJS install (not an empty dir or the wrong folder).
|
|
331
|
+
if (!fs.existsSync(pkgPath) || !fs.existsSync(cfgPath)) {
|
|
332
|
+
fail(`"${opts.dir}" does not look like a WordJS install.`,
|
|
333
|
+
'Run this from your site directory (it must contain backend/wordjs-config.json), or pass the path: npx create-wordjs upgrade <dir>.');
|
|
334
|
+
}
|
|
335
|
+
let curPkg = {};
|
|
336
|
+
try { curPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch { /* handled below */ }
|
|
337
|
+
if (!curPkg.scripts || !curPkg.scripts['release:install'] || !curPkg.scripts['start:mono']) {
|
|
338
|
+
fail(`"${opts.dir}" has a package.json but not the WordJS release scripts.`, 'Are you pointing at the right site directory?');
|
|
339
|
+
}
|
|
340
|
+
const curVersion = curPkg.version || 'unknown';
|
|
341
|
+
|
|
342
|
+
console.log('\n🚀 create-wordjs upgrade\n');
|
|
343
|
+
console.log(` Site: ${installDir}`);
|
|
344
|
+
console.log(` Current version: v${curVersion}`);
|
|
345
|
+
|
|
346
|
+
// Fetch the target release into a temp dir and read its version.
|
|
347
|
+
const { extractDir, tag, cleanup } = await obtainReleaseToTemp(opts);
|
|
348
|
+
try {
|
|
349
|
+
const newPkgPath = path.join(extractDir, 'package.json');
|
|
350
|
+
let newPkg = {};
|
|
351
|
+
try { newPkg = JSON.parse(fs.readFileSync(newPkgPath, 'utf8')); } catch { /* handled below */ }
|
|
352
|
+
if (!newPkg.scripts || !newPkg.scripts['release:install'] || !newPkg.scripts['start:mono']) {
|
|
353
|
+
fail('The downloaded ZIP does not look like a WordJS release bundle.', `Expected a wordjs-*.zip from https://github.com/${REPO}/releases.`);
|
|
354
|
+
}
|
|
355
|
+
const newVersion = newPkg.version || (tag ? String(tag).replace(/^v/, '') : 'unknown');
|
|
356
|
+
console.log(` Target version: v${newVersion}${tag ? ` (${tag})` : ''}`);
|
|
357
|
+
|
|
358
|
+
if (curVersion === newVersion && !opts.force) {
|
|
359
|
+
console.log(`\n✅ Already on v${curVersion}. Nothing to upgrade. (use --force to re-apply the same version)\n`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Confirm before mutating an existing install.
|
|
364
|
+
if (!opts.yes) {
|
|
365
|
+
if (process.stdin.isTTY) {
|
|
366
|
+
const ok = await confirm(`\n Upgrade this site v${curVersion} → v${newVersion}? Your database, uploads and config are preserved. [y/N] `);
|
|
367
|
+
if (!ok) { console.log(' Aborted — nothing changed.\n'); return; }
|
|
368
|
+
} else {
|
|
369
|
+
fail('Refusing to upgrade non-interactively without confirmation.',
|
|
370
|
+
'Re-run with --yes to proceed (your database, uploads and config are preserved).');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Snapshot the small critical config files (belt-and-suspenders; the DB/uploads are never
|
|
375
|
+
// touched by the overlay because they are in the preserve-list).
|
|
376
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
377
|
+
const backupDir = path.join(installDir, `.upgrade-backup-${stamp}`);
|
|
378
|
+
try {
|
|
379
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
380
|
+
for (const rel of ['backend/wordjs-config.json', 'gateway/gateway-config.json', 'package.json']) {
|
|
381
|
+
const from = path.join(installDir, rel);
|
|
382
|
+
if (fs.existsSync(from)) {
|
|
383
|
+
const to = path.join(backupDir, rel.replace(/[/\\]/g, '__'));
|
|
384
|
+
fs.copyFileSync(from, to);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
console.log(`\n Backed up config to ${path.relative(installDir, backupDir) || backupDir}`);
|
|
388
|
+
} catch (e) {
|
|
389
|
+
console.warn(` (could not write config backup: ${e.message} — continuing; your DB/uploads/config are still preserved in place)`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Clean-replace the build outputs so no stale chunks linger, then overlay the rest.
|
|
393
|
+
for (const rel of CLEAN_REPLACE_ON_UPGRADE) {
|
|
394
|
+
const target = path.join(installDir, rel);
|
|
395
|
+
const fromRelease = path.join(extractDir, rel);
|
|
396
|
+
if (fs.existsSync(fromRelease) && fs.existsSync(target)) {
|
|
397
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
console.log(' Applying new code (preserving data, uploads, config and custom plugins)…');
|
|
401
|
+
copyMerge(extractDir, installDir);
|
|
402
|
+
|
|
403
|
+
if (!opts.http) ensureHttpsConfig(installDir);
|
|
404
|
+
|
|
405
|
+
// Re-sync dependencies (a new version may add/upgrade packages). Skippable for a code-only swap.
|
|
406
|
+
if (opts.install) {
|
|
407
|
+
console.log('\n📦 Syncing runtime dependencies (npm run release:install)…\n');
|
|
408
|
+
runNpmScript('release:install', installDir);
|
|
409
|
+
} else {
|
|
410
|
+
console.log('\n --no-install: skipped dependency sync. Run "npm run release:install" yourself if deps changed.');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const line = '━'.repeat(64);
|
|
414
|
+
console.log(`\n${line}`);
|
|
415
|
+
console.log(`✅ Upgraded WordJS: v${curVersion} → v${newVersion}.`);
|
|
416
|
+
console.log('');
|
|
417
|
+
console.log(' Your database, uploads and config were preserved. Restart the server to apply it —');
|
|
418
|
+
console.log(' database schema migrations run automatically on the next start:');
|
|
419
|
+
console.log(' • systemd: sudo systemctl restart wordjs');
|
|
420
|
+
console.log(` • otherwise: stop it, then cd ${opts.dir === '.' ? installDir : opts.dir} && npm run start:mono`);
|
|
421
|
+
console.log('');
|
|
422
|
+
console.log(' Rollback: re-run with --version <old-tag> (your data stays intact).');
|
|
423
|
+
console.log(line + '\n');
|
|
424
|
+
} finally {
|
|
425
|
+
cleanup();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
229
429
|
// --- main ---------------------------------------------------------------------------------------
|
|
230
430
|
|
|
231
431
|
async function main() {
|
|
232
432
|
const opts = parseArgs(process.argv.slice(2));
|
|
433
|
+
if (opts.mode === 'upgrade') return upgrade(opts);
|
|
233
434
|
const targetDir = path.resolve(process.cwd(), opts.dir);
|
|
234
435
|
|
|
235
436
|
// Refuse to scribble over anything that already exists (an existing EMPTY dir is fine).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-wordjs",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"description": "Create a WordJS site with one command — the self-hosted CMS where third-party plugins run in an OS-isolated process with per-capability permission grants. SSR/SEO out of the box, SQLite by default, no PHP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jaime Martinez (https://github.com/jaimemartinez)",
|