create-wordjs 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/index.js +339 -0
  4. package/package.json +51 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WordJS Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # create-wordjs
2
+
3
+ Bootstrap a [WordJS](https://github.com/jaimemartinez/wordjs) site with one command:
4
+
5
+ ```bash
6
+ npx create-wordjs my-site
7
+ ```
8
+
9
+ That single command takes you from nothing to the browser install wizard:
10
+
11
+ 1. Downloads the latest **pre-compiled** WordJS release ZIP from GitHub — no build step,
12
+ no TypeScript compilation on your machine.
13
+ 2. Extracts it into `my-site/` and installs the runtime dependencies (`npm run release:install`).
14
+ 3. Generates a one-time install token and starts the server (`npm run start:mono`), printing a
15
+ clickable URL:
16
+
17
+ ```
18
+ → https://localhost:3000/install?token=…
19
+ ```
20
+
21
+ Open the URL, pick your database (SQLite — zero config — or PostgreSQL), create your admin
22
+ account, and you're in.
23
+
24
+ ## Requirements
25
+
26
+ - Node.js **>= 20.9** (Node 20 or 22 LTS recommended) with npm on your PATH.
27
+
28
+ ## Options
29
+
30
+ | Option | Description |
31
+ | --- | --- |
32
+ | `--zip <path-or-url>` | Use a local release ZIP (or a direct ZIP URL) instead of querying the GitHub API. Handy offline or when rate-limited. |
33
+ | `--version <tag>` | Install a specific release (e.g. `--version v1.0.0`) instead of the latest. |
34
+ | `--http` | Serve plain HTTP instead of self-signed HTTPS (sets `WORDJS_HTTP=1`). |
35
+ | `--no-start` | Scaffold and install dependencies only — start the server yourself later. |
36
+ | `-h`, `--help` | Show usage. |
37
+
38
+ ## Good to know
39
+
40
+ - **Self-signed HTTPS**: by default the site serves HTTPS on `:3000` with a locally generated
41
+ self-signed certificate. Your browser will warn once ("Your connection is not private") —
42
+ click *Advanced → Proceed*. That is expected for localhost. Prefer plain HTTP? Use `--http`.
43
+ - **Stop / restart**: press `Ctrl+C` to stop. Start again any time with
44
+ `cd my-site && npm run start:mono`. Until setup is finished, every start prints a fresh
45
+ one-time install URL, so you never need to keep the original token around.
46
+ - **GitHub rate limit / offline**: the release lookup uses the unauthenticated GitHub API. If it
47
+ is rate-limited or you're offline, download `wordjs-v*.zip` from the
48
+ [releases page](https://github.com/jaimemartinez/wordjs/releases) and run
49
+ `npx create-wordjs my-site --zip ./wordjs-v1.0.0.zip`.
50
+ - **Existing directories**: the target directory must not exist (or must be empty) — the tool
51
+ refuses to overwrite anything.
52
+
53
+ ## What gets created
54
+
55
+ A ready-to-run WordJS bundle: backend (pre-compiled to `dist/`), frontend (pre-built `.next`),
56
+ gateway, bundled plugins and themes. Secrets (JWT, DB password, install token) are generated
57
+ locally during install — nothing sensitive ships in the bundle. See `INSTALL.md` inside the
58
+ scaffolded directory for the manual steps and `documentation/deployment.md` for production
59
+ deployment.
package/index.js ADDED
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * create-wordjs — bootstrap a WordJS site with ONE command:
6
+ *
7
+ * npx create-wordjs my-site
8
+ *
9
+ * What it does:
10
+ * 1. Downloads the latest pre-compiled WordJS release ZIP from GitHub (no build step needed).
11
+ * 2. Extracts it into <dir> and installs the runtime dependencies (npm run release:install).
12
+ * 3. Generates a one-time install token and starts the server (npm run start:mono) with it,
13
+ * printing a clickable https://localhost:3000/install?token=… URL — the browser install
14
+ * wizard takes it from there (pick SQLite/PostgreSQL, create your admin, done).
15
+ *
16
+ * Plain Node, no TypeScript. Only runtime dependency: adm-zip (ZIP extraction).
17
+ */
18
+
19
+ const REPO = 'jaimemartinez/wordjs';
20
+
21
+ // ---------------------------------------------------------------------------------------------
22
+ // Node preflight — same floor as WordJS itself (Next 16 + native modules need >= 20.9). Failing
23
+ // here with a clear message beats the cryptic EBADENGINE/native-binding crash mid-install.
24
+ // ---------------------------------------------------------------------------------------------
25
+ {
26
+ const [maj, min] = process.versions.node.split('.').map(Number);
27
+ if (maj < 20 || (maj === 20 && min < 9)) {
28
+ console.error(`\n✖ WordJS requires Node.js >= 20.9 — you are running ${process.versions.node}.`);
29
+ console.error(' Install Node 20 LTS or 22 LTS from https://nodejs.org and try again.\n');
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ const fs = require('fs');
35
+ const os = require('os');
36
+ const path = require('path');
37
+ const https = require('https');
38
+ const crypto = require('crypto');
39
+ const { spawn, spawnSync } = require('child_process');
40
+
41
+ const HELP = `
42
+ create-wordjs — bootstrap a WordJS site with one command
43
+
44
+ Usage:
45
+ npx create-wordjs <dir> [options]
46
+
47
+ Options:
48
+ --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.
52
+ -h, --help Show this help.
53
+
54
+ Examples:
55
+ npx create-wordjs my-site
56
+ npx create-wordjs my-site --version v1.0.0
57
+ npx create-wordjs my-site --zip ./wordjs-v1.0.0.zip --no-start
58
+ `;
59
+
60
+ function fail(message, hint) {
61
+ console.error(`\n✖ ${message}`);
62
+ if (hint) console.error(` ${hint}`);
63
+ console.error('');
64
+ process.exit(1);
65
+ }
66
+
67
+ function parseArgs(argv) {
68
+ const opts = { dir: null, zip: null, version: null, http: false, start: true };
69
+ for (let i = 0; i < argv.length; i++) {
70
+ const a = argv[i];
71
+ if (a === '-h' || a === '--help') { console.log(HELP); process.exit(0); }
72
+ else if (a === '--zip') { opts.zip = argv[++i] || fail('--zip needs a value (path or URL to a wordjs-*.zip).'); }
73
+ else if (a === '--version') { opts.version = argv[++i] || fail('--version needs a value (a release tag, e.g. v1.0.0).'); }
74
+ else if (a === '--http') opts.http = true;
75
+ else if (a === '--no-start') opts.start = false;
76
+ else if (a.startsWith('-')) fail(`Unknown option: ${a}`, 'Run with --help to see the available options.');
77
+ else if (!opts.dir) opts.dir = a;
78
+ else fail(`Unexpected extra argument: ${a}`);
79
+ }
80
+ if (!opts.dir) fail('Please specify a directory for your new site.', 'Example: npx create-wordjs my-site');
81
+ if (opts.version && /^\d/.test(opts.version)) opts.version = 'v' + opts.version; // accept "1.0.0" for "v1.0.0"
82
+ return opts;
83
+ }
84
+
85
+ // --- tiny https helpers (plain node:https, no token, redirects followed) -----------------------
86
+
87
+ function request(url, headers, redirectsLeft = 5) {
88
+ return new Promise((resolve, reject) => {
89
+ const req = https.get(url, { headers: { 'user-agent': 'create-wordjs', ...headers } }, (res) => {
90
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && redirectsLeft > 0) {
91
+ res.resume(); // GitHub release assets redirect to objects.githubusercontent.com
92
+ resolve(request(new URL(res.headers.location, url).toString(), headers, redirectsLeft - 1));
93
+ return;
94
+ }
95
+ resolve(res);
96
+ });
97
+ req.on('error', reject);
98
+ });
99
+ }
100
+
101
+ function readBody(res) {
102
+ return new Promise((resolve, reject) => {
103
+ let data = '';
104
+ res.setEncoding('utf8');
105
+ res.on('data', (c) => { data += c; });
106
+ res.on('end', () => resolve(data));
107
+ res.on('error', reject);
108
+ });
109
+ }
110
+
111
+ async function githubJson(url) {
112
+ let res;
113
+ try {
114
+ res = await request(url, { accept: 'application/vnd.github+json' });
115
+ } catch (e) {
116
+ fail(`Could not reach GitHub (${e.message}).`,
117
+ `Check your network — or download the ZIP yourself from https://github.com/${REPO}/releases and re-run with --zip <path-to-zip>.`);
118
+ }
119
+ const body = await readBody(res);
120
+ if (res.statusCode === 403 && res.headers['x-ratelimit-remaining'] === '0') {
121
+ fail('GitHub API rate limit reached (unauthenticated requests are limited per hour).',
122
+ `Wait a bit — or download the ZIP from https://github.com/${REPO}/releases and re-run with --zip <path-to-zip>.`);
123
+ }
124
+ if (res.statusCode === 404) return null;
125
+ if (res.statusCode !== 200) {
126
+ fail(`GitHub API returned HTTP ${res.statusCode} for ${url}.`,
127
+ `You can bypass the API entirely: download the ZIP from https://github.com/${REPO}/releases and re-run with --zip <path-to-zip>.`);
128
+ }
129
+ try { return JSON.parse(body); } catch { fail('GitHub returned an unparsable response.', 'Try again, or use --zip <path-to-zip>.'); }
130
+ }
131
+
132
+ async function resolveReleaseAsset(tag) {
133
+ const url = tag
134
+ ? `https://api.github.com/repos/${REPO}/releases/tags/${encodeURIComponent(tag)}`
135
+ : `https://api.github.com/repos/${REPO}/releases/latest`;
136
+ const release = await githubJson(url);
137
+ if (!release) {
138
+ fail(tag ? `No release found for tag "${tag}".` : `No releases found for ${REPO}.`,
139
+ `See https://github.com/${REPO}/releases for available versions, or pass --zip <path-or-url>.`);
140
+ }
141
+ const asset = (release.assets || []).find((a) => /^wordjs-.*\.zip$/i.test(a.name || ''));
142
+ if (!asset) fail(`Release ${release.tag_name} has no wordjs-*.zip asset.`, 'Pass --zip <path-or-url> instead.');
143
+ return { name: asset.name, url: asset.browser_download_url, tag: release.tag_name };
144
+ }
145
+
146
+ async function download(url, dest, label) {
147
+ const res = await request(url, { accept: 'application/octet-stream' });
148
+ if (res.statusCode !== 200) {
149
+ fail(`Download failed (HTTP ${res.statusCode}) for ${url}.`,
150
+ `Download the ZIP manually from https://github.com/${REPO}/releases and re-run with --zip <path-to-zip>.`);
151
+ }
152
+ const total = Number(res.headers['content-length']) || 0;
153
+ const mb = (n) => (n / 1048576).toFixed(1);
154
+ let done = 0;
155
+ let lastShown = -1;
156
+ await new Promise((resolve, reject) => {
157
+ const out = fs.createWriteStream(dest);
158
+ res.on('data', (chunk) => {
159
+ done += chunk.length;
160
+ if (total) {
161
+ const pct = Math.floor((done / total) * 100);
162
+ if (pct !== lastShown) {
163
+ lastShown = pct;
164
+ process.stdout.write(`\r ↓ ${label}: ${mb(done)} / ${mb(total)} MB (${pct}%) `);
165
+ }
166
+ } else if (done - lastShown >= 2 * 1048576 || lastShown === -1) {
167
+ lastShown = done;
168
+ process.stdout.write(`\r ↓ ${label}: ${mb(done)} MB `);
169
+ }
170
+ });
171
+ res.on('error', reject);
172
+ out.on('error', reject);
173
+ out.on('finish', () => { process.stdout.write('\n'); resolve(); });
174
+ res.pipe(out);
175
+ });
176
+ }
177
+
178
+ // --- extraction + scaffolding ------------------------------------------------------------------
179
+
180
+ function extractZip(zipPath, targetDir) {
181
+ const AdmZip = require('adm-zip'); // lazy so --help works even before deps are installed
182
+ const zip = new AdmZip(zipPath);
183
+ zip.extractAllTo(targetDir, true);
184
+ // Official bundles put files at the ZIP root; tolerate a single wrapper folder too.
185
+ if (!fs.existsSync(path.join(targetDir, 'package.json'))) {
186
+ const entries = fs.readdirSync(targetDir);
187
+ if (entries.length === 1) {
188
+ const inner = path.join(targetDir, entries[0]);
189
+ if (fs.statSync(inner).isDirectory() && fs.existsSync(path.join(inner, 'package.json'))) {
190
+ for (const child of fs.readdirSync(inner)) {
191
+ fs.renameSync(path.join(inner, child), path.join(targetDir, child));
192
+ }
193
+ fs.rmdirSync(inner);
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ function runNpmScript(script, cwd, extraEnv) {
200
+ // A single command string with shell:true resolves npm/npm.cmd on every platform (and avoids
201
+ // Node's DEP0190 warning about args-array + shell). The string is fixed — no user input in it.
202
+ const r = spawnSync(`npm run ${script}`, {
203
+ cwd,
204
+ stdio: 'inherit',
205
+ shell: true,
206
+ env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
207
+ });
208
+ if (r.error) fail(`Could not run "npm run ${script}": ${r.error.message}`, 'Is npm on your PATH?');
209
+ if (r.status !== 0) fail(`"npm run ${script}" exited with code ${r.status}.`, `Fix the error above, then re-run it manually inside ${cwd}.`);
210
+ }
211
+
212
+ /**
213
+ * A fresh release bundle ships WITHOUT gateway/gateway-config.json (secrets are never bundled), and
214
+ * without it the monolith would fall back to plain HTTP. Seed a minimal { "ssl": true } so the
215
+ * server self-signs HTTPS on :3000 — matching the https:// install URL we (and the backend) print.
216
+ * Never overwrites an existing config.
217
+ */
218
+ function ensureHttpsConfig(targetDir) {
219
+ const p = path.join(targetDir, 'gateway', 'gateway-config.json');
220
+ if (fs.existsSync(p)) return;
221
+ try {
222
+ fs.mkdirSync(path.dirname(p), { recursive: true });
223
+ fs.writeFileSync(p, JSON.stringify({ ssl: true }, null, 4) + '\n');
224
+ } catch (e) {
225
+ console.warn(` (could not write ${p}: ${e.message} — the server may fall back to HTTP; use the URL it prints)`);
226
+ }
227
+ }
228
+
229
+ // --- main ---------------------------------------------------------------------------------------
230
+
231
+ async function main() {
232
+ const opts = parseArgs(process.argv.slice(2));
233
+ const targetDir = path.resolve(process.cwd(), opts.dir);
234
+
235
+ // Refuse to scribble over anything that already exists (an existing EMPTY dir is fine).
236
+ if (fs.existsSync(targetDir)) {
237
+ if (!fs.statSync(targetDir).isDirectory()) fail(`"${opts.dir}" already exists and is not a directory.`);
238
+ if (fs.readdirSync(targetDir).length > 0) {
239
+ fail(`Directory "${opts.dir}" already exists and is not empty.`, 'Pick a new directory name, or empty it first.');
240
+ }
241
+ } else {
242
+ fs.mkdirSync(targetDir, { recursive: true });
243
+ }
244
+
245
+ console.log('\n🚀 create-wordjs\n');
246
+
247
+ // 1) Obtain the release ZIP (local path, direct URL, or GitHub latest/tagged release).
248
+ let tmpDir = null;
249
+ let zipPath = null;
250
+ if (opts.zip && !/^https?:\/\//i.test(opts.zip)) {
251
+ zipPath = path.resolve(process.cwd(), opts.zip);
252
+ if (!fs.existsSync(zipPath)) fail(`ZIP not found: ${zipPath}`);
253
+ console.log(` Using local bundle: ${zipPath}`);
254
+ } else {
255
+ let url = opts.zip;
256
+ let name = 'wordjs.zip';
257
+ if (!url) {
258
+ console.log(opts.version ? ` Looking up release ${opts.version} of ${REPO}…` : ` Looking up the latest release of ${REPO}…`);
259
+ const asset = await resolveReleaseAsset(opts.version);
260
+ url = asset.url;
261
+ name = asset.name;
262
+ console.log(` Found ${asset.tag} → ${asset.name}`);
263
+ }
264
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'create-wordjs-'));
265
+ zipPath = path.join(tmpDir, name);
266
+ await download(url, zipPath, name);
267
+ }
268
+
269
+ // 2) Extract + sanity-check that this really is a WordJS release bundle.
270
+ console.log(` Extracting into ${targetDir}…`);
271
+ try {
272
+ extractZip(zipPath, targetDir);
273
+ } finally {
274
+ if (tmpDir) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* best-effort */ } }
275
+ }
276
+ const pkgPath = path.join(targetDir, 'package.json');
277
+ let pkg = {};
278
+ try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch { /* handled below */ }
279
+ if (!pkg.scripts || !pkg.scripts['release:install'] || !pkg.scripts['start:mono']) {
280
+ fail('The extracted ZIP does not look like a WordJS release bundle (missing release:install / start:mono scripts).',
281
+ `Expected a wordjs-*.zip from https://github.com/${REPO}/releases.`);
282
+ }
283
+
284
+ // 3) Install runtime dependencies (pre-compiled bundle — no build step).
285
+ console.log('\n📦 Installing runtime dependencies (this downloads prebuilt binaries — a few minutes)…\n');
286
+ runNpmScript('release:install', targetDir);
287
+
288
+ // 4) Default to self-signed HTTPS unless the user explicitly asked for HTTP.
289
+ if (!opts.http) ensureHttpsConfig(targetDir);
290
+
291
+ const proto = opts.http ? 'http' : 'https';
292
+ const line = '━'.repeat(64);
293
+
294
+ if (!opts.start) {
295
+ console.log(`\n${line}`);
296
+ console.log(`✅ WordJS scaffolded into ${opts.dir} (dependencies installed).`);
297
+ console.log('');
298
+ console.log(' Start it whenever you are ready:');
299
+ console.log(` cd ${opts.dir}`);
300
+ console.log(` npm run start:mono${opts.http ? ' (with WORDJS_HTTP=1 in the environment for plain HTTP)' : ''}`);
301
+ console.log('');
302
+ console.log(` The console will print your one-time install URL (${proto}://localhost:3000/install?token=…).`);
303
+ console.log(line + '\n');
304
+ return;
305
+ }
306
+
307
+ // 5) Start the server with a one-time install token (the backend honors WORDJS_INSTALL_TOKEN
308
+ // when it is >= 16 chars; 24 random bytes = 48 hex chars, same entropy the backend generates).
309
+ const token = crypto.randomBytes(24).toString('hex');
310
+ const env = { WORDJS_INSTALL_TOKEN: token };
311
+ if (opts.http) env.WORDJS_HTTP = '1';
312
+
313
+ console.log(`\n${line}`);
314
+ console.log('✅ WordJS is ready — finish setup in your browser:');
315
+ console.log('');
316
+ console.log(` → ${proto}://localhost:3000/install?token=${token}`);
317
+ console.log('');
318
+ console.log(' • The server is starting below — give it ~15–30 seconds, then open the URL.');
319
+ if (!opts.http) {
320
+ console.log(' • HTTPS uses a locally generated self-signed certificate, so your browser will');
321
+ console.log(' warn once ("Your connection is not private") — click Advanced → Proceed.');
322
+ console.log(' That is expected for localhost. (Prefer plain HTTP? Re-run with --http.)');
323
+ }
324
+ console.log(' • Stop the server: press Ctrl+C in this window.');
325
+ console.log(` • Start it later: cd ${opts.dir} && npm run start:mono`);
326
+ console.log(' (a fresh install URL is printed on every start until setup is finished)');
327
+ console.log(line + '\n');
328
+
329
+ const child = spawn('npm run start:mono', {
330
+ cwd: targetDir,
331
+ stdio: 'inherit',
332
+ shell: true,
333
+ env: { ...process.env, ...env },
334
+ });
335
+ child.on('error', (e) => fail(`Could not start the server: ${e.message}`, `Run it manually: cd ${opts.dir} && npm run start:mono`));
336
+ child.on('exit', (code) => process.exit(code == null ? 0 : code));
337
+ }
338
+
339
+ main().catch((e) => fail(e && e.message ? e.message : String(e)));
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "create-wordjs",
3
+ "version": "1.2.0",
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
+ "license": "MIT",
6
+ "author": "Jaime Martinez (https://github.com/jaimemartinez)",
7
+ "type": "commonjs",
8
+ "bin": {
9
+ "create-wordjs": "index.js"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20.9.0"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/jaimemartinez/wordjs.git",
22
+ "directory": "packages/create-wordjs"
23
+ },
24
+ "homepage": "https://github.com/jaimemartinez/wordjs#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/jaimemartinez/wordjs/issues"
27
+ },
28
+ "keywords": [
29
+ "wordjs",
30
+ "cms",
31
+ "create",
32
+ "scaffold",
33
+ "bootstrap",
34
+ "wordpress-alternative",
35
+ "self-hosted",
36
+ "sandbox",
37
+ "sandboxed-plugins",
38
+ "plugin-permissions",
39
+ "nextjs",
40
+ "typescript",
41
+ "ssr",
42
+ "sqlite",
43
+ "no-php"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "adm-zip": "^0.5.16"
50
+ }
51
+ }