create-wordjs 1.5.4 → 1.6.1
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 +195 -10
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -42,17 +42,25 @@ const HELP = `
|
|
|
42
42
|
create-wordjs — bootstrap or upgrade a WordJS site with one command
|
|
43
43
|
|
|
44
44
|
Usage:
|
|
45
|
-
npx create-wordjs <dir> [options] Create a new site
|
|
45
|
+
npx create-wordjs <dir> [options] Create a new site (monolith — one machine)
|
|
46
46
|
npx create-wordjs upgrade [dir] [options] Upgrade an existing site (dir defaults to .)
|
|
47
|
+
npx create-wordjs gateway [dir] [options] Set up a SEPARATE-MODE gateway (cluster CA + join tokens)
|
|
48
|
+
npx create-wordjs join <role> [dir] [opts] Join this machine to a gateway as backend|frontend
|
|
47
49
|
|
|
48
50
|
Options:
|
|
49
51
|
--zip <path-or-url> Use a local release ZIP (or a direct ZIP URL) instead of asking GitHub.
|
|
50
52
|
--version <tag> Install/upgrade to a specific release tag (e.g. v1.0.0) instead of the latest.
|
|
51
53
|
--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.
|
|
54
|
+
--no-start Scaffold + install dependencies only; don't start the server.
|
|
53
55
|
--yes, -y Skip the confirmation prompt (required when upgrading non-interactively).
|
|
54
56
|
--force Re-apply even if already on the target version. (upgrade)
|
|
55
57
|
--no-install Swap the code only; skip 'npm run release:install'. (upgrade)
|
|
58
|
+
--host <ip/dns> (gateway) The address other machines dial to reach this gateway.
|
|
59
|
+
--gateway <ip/dns> (join) The gateway's address.
|
|
60
|
+
--token <join-token> (join) A single-use token minted on the gateway (cluster token <role>).
|
|
61
|
+
--ca-hash <sha256> (join) Pin the cluster CA fingerprint the gateway prints (MITM guard).
|
|
62
|
+
--advertise <ip/dns> (join) This node's routable address the gateway will proxy to.
|
|
63
|
+
--enroll-port <port> (join) Gateway token-enrollment port (default 3101).
|
|
56
64
|
-h, --help Show this help.
|
|
57
65
|
|
|
58
66
|
Examples:
|
|
@@ -60,7 +68,15 @@ Examples:
|
|
|
60
68
|
npx create-wordjs my-site --version v1.0.0
|
|
61
69
|
npx create-wordjs upgrade # from inside your site directory
|
|
62
70
|
npx create-wordjs upgrade ./my-site --yes
|
|
63
|
-
|
|
71
|
+
|
|
72
|
+
Separate mode (three machines) — run one command per machine:
|
|
73
|
+
# on the gateway machine (prints ready-to-paste join commands with fresh tokens):
|
|
74
|
+
npx create-wordjs gateway --host 10.0.0.1
|
|
75
|
+
# on the backend machine:
|
|
76
|
+
npx create-wordjs join backend --gateway 10.0.0.1 --token <t> --ca-hash <fp> --advertise 10.0.0.2
|
|
77
|
+
# on the frontend machine:
|
|
78
|
+
npx create-wordjs join frontend --gateway 10.0.0.1 --token <t> --ca-hash <fp> --advertise 10.0.0.3
|
|
79
|
+
(join needs 'openssl' on PATH. See documentation/separate-mode.md.)
|
|
64
80
|
|
|
65
81
|
Upgrading preserves your database (backend/data), uploads (backend/uploads), config
|
|
66
82
|
(wordjs-config.json + gateway secrets) and any user-installed plugins; it replaces the app code and
|
|
@@ -76,9 +92,13 @@ function fail(message, hint) {
|
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
function parseArgs(argv) {
|
|
79
|
-
const opts = {
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
const opts = {
|
|
96
|
+
mode: 'create', dir: null, zip: null, version: null, http: false, start: true, yes: false, force: false, install: true,
|
|
97
|
+
role: null, gateway: null, token: null, caHash: null, advertise: null, enrollPort: null, host: null,
|
|
98
|
+
};
|
|
99
|
+
// A leading subcommand selects the mode (default is the monolith create flow).
|
|
100
|
+
if (['upgrade', 'gateway', 'join'].includes(argv[0])) { opts.mode = argv[0]; argv = argv.slice(1); }
|
|
101
|
+
const positionals = [];
|
|
82
102
|
for (let i = 0; i < argv.length; i++) {
|
|
83
103
|
const a = argv[i];
|
|
84
104
|
if (a === '-h' || a === '--help') { console.log(HELP); process.exit(0); }
|
|
@@ -89,15 +109,28 @@ function parseArgs(argv) {
|
|
|
89
109
|
else if (a === '--yes' || a === '-y') opts.yes = true;
|
|
90
110
|
else if (a === '--force') opts.force = true;
|
|
91
111
|
else if (a === '--no-install') opts.install = false;
|
|
112
|
+
else if (a === '--role') opts.role = argv[++i] || fail('--role needs a value (backend or frontend).');
|
|
113
|
+
else if (a === '--gateway') opts.gateway = argv[++i] || fail('--gateway needs the gateway host/ip.');
|
|
114
|
+
else if (a === '--token') opts.token = argv[++i] || fail('--token needs the join token.');
|
|
115
|
+
else if (a === '--ca-hash') opts.caHash = argv[++i] || fail('--ca-hash needs the CA fingerprint.');
|
|
116
|
+
else if (a === '--advertise') opts.advertise = argv[++i] || fail('--advertise needs this node\'s ip/dns.');
|
|
117
|
+
else if (a === '--enroll-port') opts.enrollPort = argv[++i] || fail('--enroll-port needs a port.');
|
|
118
|
+
else if (a === '--host') opts.host = argv[++i] || fail('--host needs the gateway ip/dns.');
|
|
92
119
|
else if (a.startsWith('-')) fail(`Unknown option: ${a}`, 'Run with --help to see the available options.');
|
|
93
|
-
else
|
|
94
|
-
else fail(`Unexpected extra argument: ${a}`);
|
|
120
|
+
else positionals.push(a);
|
|
95
121
|
}
|
|
122
|
+
// Positionals: `join <role> [dir]` takes the role first; every other mode takes just [dir].
|
|
123
|
+
if (opts.mode === 'join' && !opts.role) opts.role = positionals.shift() || null;
|
|
124
|
+
opts.dir = positionals.shift() || null;
|
|
125
|
+
if (positionals.length) fail(`Unexpected extra argument: ${positionals[0]}`);
|
|
126
|
+
|
|
96
127
|
if (!opts.dir) {
|
|
97
|
-
if (opts.mode === 'upgrade') opts.dir = '.';
|
|
128
|
+
if (opts.mode === 'upgrade') opts.dir = '.'; // upgrade defaults to cwd
|
|
129
|
+
else if (opts.mode === 'gateway') opts.dir = 'wordjs-gateway';
|
|
130
|
+
else if (opts.mode === 'join') opts.dir = opts.role ? `wordjs-${opts.role}` : 'wordjs-node';
|
|
98
131
|
else fail('Please specify a directory for your new site.', 'Example: npx create-wordjs my-site');
|
|
99
132
|
}
|
|
100
|
-
if (opts.version && /^\d/.test(opts.version)) opts.version = 'v' + opts.version;
|
|
133
|
+
if (opts.version && /^\d/.test(opts.version)) opts.version = 'v' + opts.version; // accept "1.0.0" for "v1.0.0"
|
|
101
134
|
return opts;
|
|
102
135
|
}
|
|
103
136
|
|
|
@@ -228,6 +261,32 @@ function runNpmScript(script, cwd, extraEnv) {
|
|
|
228
261
|
if (r.status !== 0) fail(`"npm run ${script}" exited with code ${r.status}.`, `Fix the error above, then re-run it manually inside ${cwd}.`);
|
|
229
262
|
}
|
|
230
263
|
|
|
264
|
+
// First non-internal IPv4 — a sensible default advertise/host when the user doesn't pass one.
|
|
265
|
+
function firstLanIp() {
|
|
266
|
+
for (const ifaces of Object.values(os.networkInterfaces())) {
|
|
267
|
+
for (const i of ifaces || []) if (!i.internal && (i.family === 'IPv4' || i.family === 4)) return i.address;
|
|
268
|
+
}
|
|
269
|
+
return '127.0.0.1';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Run a BUNDLED node script (scripts/cluster.js, scripts/node-join.js) with an ARGS ARRAY and no shell,
|
|
273
|
+
// so user-supplied values (IPs, tokens) can never be interpreted by a shell. Inherits stdio.
|
|
274
|
+
function runNode(scriptRel, args, cwd, extraEnv) {
|
|
275
|
+
const r = spawnSync(process.execPath, [scriptRel, ...args], {
|
|
276
|
+
cwd, stdio: 'inherit', env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
|
|
277
|
+
});
|
|
278
|
+
if (r.error) fail(`Could not run ${scriptRel}: ${r.error.message}`, `Is node on your PATH?`);
|
|
279
|
+
if (r.status !== 0) fail(`${scriptRel} exited with code ${r.status}.`, `Fix the error above, then re-run it inside ${cwd}.`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Same, but capture stdout (to read a minted token / CA fingerprint back).
|
|
283
|
+
function runNodeCapture(scriptRel, args, cwd) {
|
|
284
|
+
const r = spawnSync(process.execPath, [scriptRel, ...args], { cwd, encoding: 'utf8' });
|
|
285
|
+
if (r.error) fail(`Could not run ${scriptRel}: ${r.error.message}`);
|
|
286
|
+
if (r.status !== 0) { process.stderr.write((r.stdout || '') + (r.stderr || '')); fail(`${scriptRel} exited with code ${r.status}.`); }
|
|
287
|
+
return r.stdout || '';
|
|
288
|
+
}
|
|
289
|
+
|
|
231
290
|
/**
|
|
232
291
|
* A fresh release bundle ships WITHOUT gateway/gateway-config.json (secrets are never bundled), and
|
|
233
292
|
* without it the monolith would fall back to plain HTTP. Seed a minimal { "ssl": true } so the
|
|
@@ -426,11 +485,137 @@ async function upgrade(opts) {
|
|
|
426
485
|
}
|
|
427
486
|
}
|
|
428
487
|
|
|
488
|
+
// --- separate mode (gateway + join) ------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
// Download + extract the release bundle into targetDir and install runtime deps. Shared by the
|
|
491
|
+
// gateway and join flows (a superset of the create flow's steps 1–3, minus the mono-specific bits).
|
|
492
|
+
async function scaffoldBundle(opts, targetDir) {
|
|
493
|
+
if (fs.existsSync(targetDir)) {
|
|
494
|
+
if (!fs.statSync(targetDir).isDirectory()) fail(`"${opts.dir}" already exists and is not a directory.`);
|
|
495
|
+
if (fs.readdirSync(targetDir).length > 0) fail(`Directory "${opts.dir}" already exists and is not empty.`, 'Pick a new directory name, or empty it first.');
|
|
496
|
+
} else {
|
|
497
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
let tmpDir = null, zipPath = null;
|
|
501
|
+
if (opts.zip && !/^https?:\/\//i.test(opts.zip)) {
|
|
502
|
+
zipPath = path.resolve(process.cwd(), opts.zip);
|
|
503
|
+
if (!fs.existsSync(zipPath)) fail(`ZIP not found: ${zipPath}`);
|
|
504
|
+
console.log(` Using local bundle: ${zipPath}`);
|
|
505
|
+
} else {
|
|
506
|
+
let url = opts.zip, name = 'wordjs.zip';
|
|
507
|
+
if (!url) {
|
|
508
|
+
console.log(opts.version ? ` Looking up release ${opts.version} of ${REPO}…` : ` Looking up the latest release of ${REPO}…`);
|
|
509
|
+
const asset = await resolveReleaseAsset(opts.version);
|
|
510
|
+
url = asset.url; name = asset.name;
|
|
511
|
+
console.log(` Found ${asset.tag} → ${asset.name}`);
|
|
512
|
+
}
|
|
513
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'create-wordjs-'));
|
|
514
|
+
zipPath = path.join(tmpDir, name);
|
|
515
|
+
await download(url, zipPath, name);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
console.log(` Extracting into ${targetDir}…`);
|
|
519
|
+
try { extractZip(zipPath, targetDir); }
|
|
520
|
+
finally { if (tmpDir) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* best-effort */ } } }
|
|
521
|
+
|
|
522
|
+
let pkg = {};
|
|
523
|
+
try { pkg = JSON.parse(fs.readFileSync(path.join(targetDir, 'package.json'), 'utf8')); } catch { /* handled below */ }
|
|
524
|
+
if (!pkg.scripts || !pkg.scripts['release:install']) {
|
|
525
|
+
fail('The extracted ZIP does not look like a WordJS release bundle.', `Expected a wordjs-*.zip from https://github.com/${REPO}/releases.`);
|
|
526
|
+
}
|
|
527
|
+
if (!fs.existsSync(path.join(targetDir, 'scripts', 'cluster.js')) || !fs.existsSync(path.join(targetDir, 'scripts', 'node-join.js'))) {
|
|
528
|
+
fail('This release bundle predates separate mode (missing scripts/cluster.js).', 'Install v1.6.1 or later, e.g. add --version v1.6.1.');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
console.log('\n📦 Installing runtime dependencies (this downloads prebuilt binaries — a few minutes)…\n');
|
|
532
|
+
runNpmScript('release:install', targetDir);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// `create-wordjs gateway` — set this machine up as the cluster gateway: install, mint the cluster CA +
|
|
536
|
+
// config, mint one join token per role, and print the ready-to-paste join commands for the other nodes.
|
|
537
|
+
async function gateway(opts) {
|
|
538
|
+
const targetDir = path.resolve(process.cwd(), opts.dir);
|
|
539
|
+
console.log('\n🚀 create-wordjs · gateway (separate mode)\n');
|
|
540
|
+
await scaffoldBundle(opts, targetDir);
|
|
541
|
+
|
|
542
|
+
const host = opts.host || firstLanIp();
|
|
543
|
+
const line = '━'.repeat(64);
|
|
544
|
+
console.log(`\n🔐 Initializing cluster gateway on ${host}…`);
|
|
545
|
+
runNode('scripts/cluster.js', ['init', '--host', host], targetDir);
|
|
546
|
+
|
|
547
|
+
// Read the CA fingerprint and mint a token per role (capturing the raw token for the join command).
|
|
548
|
+
const fp = (runNodeCapture('scripts/cluster.js', ['info'], targetDir).match(/CA fingerprint:\s*([0-9a-f]{64})/) || [])[1] || '<fingerprint>';
|
|
549
|
+
const mint = (role) => (runNodeCapture('scripts/cluster.js', ['token', role, '--ttl', '120'], targetDir)
|
|
550
|
+
.match(new RegExp(`wjc\\.${role}\\.[A-Za-z0-9_-]+`)) || [])[0] || '<token>';
|
|
551
|
+
const beTok = mint('backend'), feTok = mint('frontend');
|
|
552
|
+
|
|
553
|
+
console.log(`\n${line}`);
|
|
554
|
+
console.log('✅ Gateway ready. Public origin: ' + `https://${host}:3000`);
|
|
555
|
+
console.log('');
|
|
556
|
+
console.log(' Run ONE of these on each other machine (they auto-download + enroll + start):');
|
|
557
|
+
console.log('');
|
|
558
|
+
console.log(' # backend machine:');
|
|
559
|
+
console.log(` npx create-wordjs join backend --gateway ${host} --token ${beTok} \\`);
|
|
560
|
+
console.log(` --ca-hash ${fp} --advertise <this-backend-ip>`);
|
|
561
|
+
console.log('');
|
|
562
|
+
console.log(' # frontend machine:');
|
|
563
|
+
console.log(` npx create-wordjs join frontend --gateway ${host} --token ${feTok} \\`);
|
|
564
|
+
console.log(` --ca-hash ${fp} --advertise <this-frontend-ip>`);
|
|
565
|
+
console.log('');
|
|
566
|
+
console.log(' Tokens are single-use and expire in 120 min. Mint more anytime:');
|
|
567
|
+
console.log(` cd ${opts.dir} && node scripts/cluster.js token <backend|frontend>`);
|
|
568
|
+
console.log(line + '\n');
|
|
569
|
+
|
|
570
|
+
if (!opts.start) {
|
|
571
|
+
console.log(` Start the gateway when ready: cd ${opts.dir} && npm run prod:gateway\n`);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
console.log(' Starting the gateway below (Ctrl+C to stop) — the join commands above work once it is up.\n');
|
|
575
|
+
const child = spawn('npm run prod:gateway', { cwd: targetDir, stdio: 'inherit', shell: true, env: process.env });
|
|
576
|
+
child.on('error', (e) => fail(`Could not start the gateway: ${e.message}`, `Run it manually: cd ${opts.dir} && npm run prod:gateway`));
|
|
577
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// `create-wordjs join <backend|frontend>` — install the bundle, enroll with the gateway using the
|
|
581
|
+
// single-use token (delegates to scripts/node-join.js), then start + register the service.
|
|
582
|
+
async function join(opts) {
|
|
583
|
+
if (!['backend', 'frontend'].includes(opts.role)) {
|
|
584
|
+
fail('join needs a role: backend or frontend.', 'Example: npx create-wordjs join backend --gateway <ip> --token <t>');
|
|
585
|
+
}
|
|
586
|
+
if (!opts.gateway) fail('--gateway <gateway-ip/dns> is required for join.');
|
|
587
|
+
if (!opts.token) fail('--token <join-token> is required for join.', `Mint one on the gateway: node scripts/cluster.js token ${opts.role}`);
|
|
588
|
+
if (!opts.caHash) console.warn(' ⚠️ No --ca-hash given — skipping the MITM fingerprint check (fine on a trusted network).');
|
|
589
|
+
|
|
590
|
+
const targetDir = path.resolve(process.cwd(), opts.dir);
|
|
591
|
+
console.log(`\n🚀 create-wordjs · join ${opts.role} (separate mode)\n`);
|
|
592
|
+
await scaffoldBundle(opts, targetDir);
|
|
593
|
+
|
|
594
|
+
const advertise = opts.advertise || firstLanIp();
|
|
595
|
+
const args = ['--role', opts.role, '--gateway', opts.gateway, '--enroll-port', String(opts.enrollPort || 3101),
|
|
596
|
+
'--token', opts.token, '--advertise', advertise];
|
|
597
|
+
if (opts.caHash) args.push('--ca-hash', opts.caHash);
|
|
598
|
+
if (opts.start) args.push('--start');
|
|
599
|
+
|
|
600
|
+
console.log(`\n🎟️ Enrolling ${opts.role} with gateway ${opts.gateway} (advertise ${advertise})…\n`);
|
|
601
|
+
runNode('scripts/node-join.js', args, targetDir);
|
|
602
|
+
|
|
603
|
+
const line = '━'.repeat(64);
|
|
604
|
+
console.log(`\n${line}`);
|
|
605
|
+
console.log(`✅ ${opts.role} enrolled${opts.start ? ' and started (registered with the gateway over mTLS)' : ''}.`);
|
|
606
|
+
console.log(opts.start
|
|
607
|
+
? ` Logs: ${path.join(opts.dir, opts.role, 'cluster-start.log')}`
|
|
608
|
+
: ` Start it: cd ${opts.dir} && npm start`);
|
|
609
|
+
console.log(line + '\n');
|
|
610
|
+
}
|
|
611
|
+
|
|
429
612
|
// --- main ---------------------------------------------------------------------------------------
|
|
430
613
|
|
|
431
614
|
async function main() {
|
|
432
615
|
const opts = parseArgs(process.argv.slice(2));
|
|
433
616
|
if (opts.mode === 'upgrade') return upgrade(opts);
|
|
617
|
+
if (opts.mode === 'gateway') return gateway(opts);
|
|
618
|
+
if (opts.mode === 'join') return join(opts);
|
|
434
619
|
const targetDir = path.resolve(process.cwd(), opts.dir);
|
|
435
620
|
|
|
436
621
|
// 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.
|
|
3
|
+
"version": "1.6.1",
|
|
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)",
|