From 0028b03b6a569f38ba1788633af4c51c41050397 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Mon, 18 May 2026 20:54:29 +0200 Subject: [PATCH] feat: cross-platform installer with JSON-mode integration Adds install.bat / install.sh / install.mjs at the project root for a one-shot setup: prereqs check, renderer clone & link, dependency install, config copy, JSON parser mode selection, URL prompt with validation, and the production build. - install.bat / install.sh: thin OS-specific wrappers around install.mjs - install.mjs: 9-step installer with --help, --non-interactive, --skip-build/clone/link and per-URL override flags - new step 6 'Choose JSON parsing mode': prompts the operator (json5 recommended) or accepts --json-mode=json5|legacy|auto in CI; writes .nitro-build.json so the final 'yarn build' picks it up directly - summary now reports the selected JSON mode and its source - .gitattributes: force LF on install.sh / install.mjs so the shebang stays valid on Linux/macOS after a Windows checkout - install.sh marked executable in the index (100755) - README: new 'Quick install' section with interactive and CI usage, plus a complete --non-interactive example --- .gitattributes | 4 + README.md | 70 ++++++- install.bat | 2 + install.mjs | 556 +++++++++++++++++++++++++++++++++++++++++++++++++ install.sh | 4 + 5 files changed, 635 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 install.bat create mode 100644 install.mjs create mode 100755 install.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..df6d9a0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +install.sh text eol=lf +install.mjs text eol=lf +*.sh text eol=lf +*.bat text eol=crlf diff --git a/README.md b/README.md index 1b00a3d..05a93c7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,75 @@ - If using NodeJS < 18 remove `--openssl-legacy-provider` from the package.json scripts - [Yarn](https://yarnpkg.com/) `npm i yarn -g` -## Installation +## Quick install (recommended) + +The repository ships a cross-platform installer that performs the full setup +in one go: prerequisites check, renderer clone & link, dependency install, +config copy, JSON parsing mode selection, URL prompt with validation, and the +production build. + +After cloning Nitro V3, from its root run: + +``` +# Windows +install.bat + +# Linux / macOS +./install.sh +``` + +Both wrappers just exec `node install.mjs`, so you can also invoke it directly: + +``` +node install.mjs +``` + +The installer walks through these steps: + +``` +[1/9] Check prerequisites (node >= 18, yarn, git) +[2/9] Clone Nitro_Render_V3 +[3/9] Setup renderer (yarn install + yarn link) +[4/9] Setup client (yarn install + yarn link "@nitrots/nitro-renderer") +[5/9] Copy public/configuration/*.example -> *.json +[6/9] Choose JSON parsing mode (json5 recommended) -> writes .nitro-build.json +[7/9] Configure URLs (interactive, validated) +[8/9] Build (yarn build) +[9/9] Summary +``` + +### Headless / CI runs + +Every step can be driven from flags so the installer can be used in pipelines: + +``` +node install.mjs --non-interactive \ + --json-mode=json5 \ + --socket-url=wss://example.com/ws \ + --api-url=https://example.com \ + --asset-url=https://example.com/nitro-assets/ \ + --image-library-url=https://example.com/c_images \ + --hof-furni-url=https://example.com/hof_furni \ + --camera-url=https://example.com/camera \ + --thumbnails-url=https://example.com/thumbnails \ + --habbopages-url=/habbopages \ + --api-base-url=https://example.com \ + --plain-config-base-url=https://example.com/configuration \ + --plain-gamedata-base-url=https://example.com/gamedata \ + --skip-link +``` + +Useful workflow flags: + +- `--non-interactive` / `--skip-prompts` — keep example defaults unless a URL override is passed +- `--json-mode=` — pick the parser without the JSON mode prompt +- `--skip-build`, `--skip-clone`, `--skip-link` — re-runs without redoing those steps +- `--help` — full flag reference and per-key URL flags + +`install.mjs` is idempotent: re-running it keeps any `*.json` config files +that already exist and only patches the URL keys you pass on the CLI. + +## Installation (manual) - First you should open terminal and navigate to the folder where you want to clone Nitro and Nitro-Renderer - Clone Nitro (Expl. C:\Github\) diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..46cbf0c --- /dev/null +++ b/install.bat @@ -0,0 +1,2 @@ +@echo off +node "%~dp0install.mjs" %* diff --git a/install.mjs b/install.mjs new file mode 100644 index 0000000..186b595 --- /dev/null +++ b/install.mjs @@ -0,0 +1,556 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process'; +import { copyFile, readFile, writeFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { platform } from 'node:os'; +import * as readline from 'node:readline/promises'; +import { stdin as input, stdout as output } from 'node:process'; + +const ROOT = dirname(fileURLToPath(import.meta.url)); +const RENDERER_REPO_URL = 'https://github.com/duckietm/Nitro_Render_V3.git'; +const RENDERER_DIR = resolve(ROOT, '..', 'Nitro_Render_V3'); +const CONFIG_DIR = join(ROOT, 'public', 'configuration'); +const NITRO_BUILD_FILE = join(ROOT, '.nitro-build.json'); +const IS_WINDOWS = platform() === 'win32'; +const MIN_NODE_MAJOR = 18; +const VALID_JSON_MODES = ['json5', 'legacy', 'auto']; +const DEFAULT_JSON_MODE = 'json5'; + +const KEY_SPECS = { + 'socket.url': { type: 'url', schemes: ['ws:', 'wss:'], flag: 'socket-url' }, + 'api.url': { type: 'url', schemes: ['http:', 'https:'], flag: 'api-url' }, + 'asset.url': { type: 'url', schemes: ['http:', 'https:'], flag: 'asset-url' }, + 'image.library.url': { type: 'url', schemes: ['http:', 'https:'], flag: 'image-library-url' }, + 'hof.furni.url': { type: 'url', schemes: ['http:', 'https:'], flag: 'hof-furni-url' }, + 'camera.url': { type: 'url', schemes: ['http:', 'https:'], flag: 'camera-url' }, + 'thumbnails.url': { type: 'url', schemes: ['http:', 'https:'], flag: 'thumbnails-url' }, + 'url.prefix': { type: 'pathOrUrl', schemes: ['http:', 'https:'], flag: 'url-prefix' }, + 'habbopages.url': { type: 'pathOrUrl', schemes: ['http:', 'https:'], flag: 'habbopages-url' }, + 'apiBaseUrl': { type: 'url', schemes: ['http:', 'https:'], flag: 'api-base-url' }, + 'plainConfigBaseUrl': { type: 'url', schemes: ['http:', 'https:'], flag: 'plain-config-base-url' }, + 'plainGamedataBaseUrl': { type: 'url', schemes: ['http:', 'https:'], flag: 'plain-gamedata-base-url' } +}; + +const FLAG_TO_KEY = Object.fromEntries( + Object.entries(KEY_SPECS).map(([key, spec]) => [spec.flag, key]) +); + +const CONFIG_FILES = [ + { + example: 'renderer-config.example', + target: 'renderer-config.json', + keys: ['socket.url', 'api.url', 'asset.url', 'image.library.url', 'hof.furni.url'] + }, + { + example: 'ui-config.example', + target: 'ui-config.json', + keys: ['camera.url', 'thumbnails.url', 'url.prefix', 'habbopages.url'] + }, + { + example: 'client-mode.example', + target: 'client-mode.json', + keys: ['apiBaseUrl', 'plainConfigBaseUrl', 'plainGamedataBaseUrl'] + } +]; + +const STEPS = [ + 'Check prerequisites', + 'Clone Nitro_Render_V3', + 'Setup renderer (yarn install + yarn link)', + 'Setup client (yarn install + yarn link)', + 'Copy config files', + 'Choose JSON parsing mode', + 'Configure URLs', + 'Build (yarn build)', + 'Summary' +]; + +let currentStep = 0; +let activeReadline = null; +const summary = { + rendererCloned: false, + rendererSkipped: false, + configsCreated: [], + configsKept: [], + configsPatched: [], + jsonMode: null, + jsonModeSource: null, + buildRan: false, + buildSkipped: false +}; + +const useColor = !process.env.NO_COLOR && process.stdout.isTTY; +const c = { + reset: useColor ? '\x1b[0m' : '', + bold: useColor ? '\x1b[1m' : '', + dim: useColor ? '\x1b[2m' : '', + red: useColor ? '\x1b[31m' : '', + green: useColor ? '\x1b[32m' : '', + yellow: useColor ? '\x1b[33m' : '', + cyan: useColor ? '\x1b[36m' : '' +}; + +function info(msg) { console.log(c.cyan + '[i]' + c.reset + ' ' + msg); } +function ok(msg) { console.log(c.green + '[+]' + c.reset + ' ' + msg); } +function warn(msg) { console.log(c.yellow + '[!]' + c.reset + ' ' + msg); } +function err(msg) { console.error(c.red + '[x]' + c.reset + ' ' + msg); } + +function step(label) { + currentStep += 1; + const sep = '----------------------------------------------------------------'; + console.log(''); + console.log(c.dim + sep + c.reset); + console.log(c.bold + '[' + currentStep + '/' + STEPS.length + '] ' + label + c.reset); + console.log(c.dim + sep + c.reset); +} + +function runShell(cmdString, cwd) { + return new Promise((resolveFn, rejectFn) => { + const child = spawn(cmdString, { shell: true, cwd, stdio: 'inherit' }); + child.on('error', e => rejectFn(new Error("'" + cmdString + "' (cwd: " + cwd + ") failed to start: " + e.message))); + child.on('exit', code => { + if (code === 0) resolveFn(); + else rejectFn(new Error("'" + cmdString + "' (cwd: " + cwd + ") exited with code " + code)); + }); + }); +} + +function runCapture(cmdString) { + return new Promise((resolveFn, rejectFn) => { + const child = spawn(cmdString, { shell: true, stdio: ['ignore', 'pipe', 'pipe'] }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', d => { stdout += d.toString(); }); + child.stderr.on('data', d => { stderr += d.toString(); }); + child.on('error', e => rejectFn(new Error("'" + cmdString + "' failed to start: " + e.message))); + child.on('exit', code => { + if (code === 0) resolveFn(stdout.trim()); + else rejectFn(new Error("'" + cmdString + "' exited with code " + code + (stderr ? ': ' + stderr.trim() : ''))); + }); + }); +} + +function validateValue(value, spec) { + if (value === '' || value === undefined || value === null) { + if (spec.type === 'pathOrUrl') return { valid: true }; + return { valid: false, error: 'value cannot be empty' }; + } + if (spec.type === 'pathOrUrl' && value.startsWith('/')) { + return { valid: true }; + } + let parsed; + try { + parsed = new URL(value); + } catch { + return { valid: false, error: 'not a valid URL' }; + } + if (spec.schemes.length > 0 && !spec.schemes.includes(parsed.protocol)) { + const allowed = spec.schemes.map(s => s.replace(':', '')).join(', '); + return { valid: false, error: 'scheme must be one of: ' + allowed }; + } + return { valid: true }; +} + +function parseArgs() { + const opts = { + interactive: true, + skipBuild: false, + skipClone: false, + skipLink: false, + help: false, + jsonMode: null, + urlOverrides: {} + }; + const builtinFlags = new Set([ + '--non-interactive', '--skip-prompts', + '--skip-build', '--skip-clone', '--skip-link', + '--help', '-h' + ]); + for (const arg of process.argv.slice(2)) { + if (builtinFlags.has(arg)) { + switch (arg) { + case '--non-interactive': + case '--skip-prompts': opts.interactive = false; break; + case '--skip-build': opts.skipBuild = true; break; + case '--skip-clone': opts.skipClone = true; break; + case '--skip-link': opts.skipLink = true; break; + case '--help': + case '-h': opts.help = true; break; + } + continue; + } + const eq = arg.indexOf('='); + if (arg.startsWith('--') && eq > 2) { + const flagName = arg.slice(2, eq); + const value = arg.slice(eq + 1); + if (flagName === 'json-mode') { + if (!VALID_JSON_MODES.includes(value)) { + warn('Invalid --json-mode=' + value + ' (expected: ' + VALID_JSON_MODES.join(', ') + '), ignored'); + continue; + } + opts.jsonMode = value; + continue; + } + const key = FLAG_TO_KEY[flagName]; + if (key) { + opts.urlOverrides[key] = value; + continue; + } + } + warn('Unknown flag: ' + arg + ' (ignored)'); + } + return opts; +} + +function printUsage() { + const flagList = Object.entries(KEY_SPECS) + .map(([key, spec]) => ' --' + spec.flag + '=' + ' '.repeat(Math.max(1, 32 - spec.flag.length)) + 'Set ' + key) + .join('\n'); + console.log([ + 'Nitro-V3 cross-platform installer', + '', + 'Usage: node install.mjs [flags]', + '', + 'Workflow flags:', + ' --non-interactive, --skip-prompts Keep default URLs unless overridden by --=', + ' --json-mode= Choose the JSON parsing mode without prompting', + ' --skip-build Skip the final yarn build', + ' --skip-clone Skip cloning Nitro_Render_V3', + ' --skip-link Skip yarn link calls (useful when re-running)', + ' --help, -h Show this help and exit', + '', + 'URL override flags (override interactive prompts; combine with --non-interactive for fully automated runs):', + flagList, + '', + 'Steps performed:', + ' 1. Check Node >= ' + MIN_NODE_MAJOR + ', yarn, git', + ' 2. Clone Nitro_Render_V3 to ../Nitro_Render_V3', + ' 3. yarn install + yarn link in the renderer', + ' 4. yarn install + yarn link "@nitrots/nitro-renderer" in this project', + ' 5. Copy public/configuration/*.example -> *.json (keeps existing files)', + ' 6. Choose JSON parsing mode (json5 recommended) -> writes .nitro-build.json', + ' 7. Prompt for URLs and patch the JSON config files', + ' 8. yarn build (honours the JSON mode chosen at step 6)', + '' + ].join('\n')); +} + +async function checkPrereqs() { + const nodeVer = process.versions.node; + const major = parseInt(nodeVer.split('.')[0], 10); + if (Number.isNaN(major) || major < MIN_NODE_MAJOR) { + throw new Error('Node >= ' + MIN_NODE_MAJOR + ' required (you have v' + nodeVer + '). Install from https://nodejs.org/'); + } + ok('Node v' + nodeVer); + + try { + const v = await runCapture('yarn --version'); + ok('yarn ' + v); + } catch { + const hint = IS_WINDOWS ? 'npm i -g yarn' : 'sudo npm i -g yarn'; + throw new Error('yarn not found on PATH. Install with: ' + hint); + } + + try { + const v = await runCapture('git --version'); + ok(v); + } catch { + const hint = IS_WINDOWS ? 'winget install Git.Git' : 'sudo apt-get install git (or your distro equivalent)'; + throw new Error('git not found on PATH. Install with: ' + hint); + } +} + +async function cloneRenderer(opts) { + if (opts.skipClone) { info('--skip-clone: not cloning Nitro_Render_V3'); summary.rendererSkipped = true; return; } + if (existsSync(RENDERER_DIR)) { + warn('Nitro_Render_V3 already exists at ' + RENDERER_DIR + ' - skipping clone (yarn install/link will still run).'); + summary.rendererSkipped = true; + return; + } + await runShell('git clone ' + RENDERER_REPO_URL + ' "' + RENDERER_DIR + '"', dirname(RENDERER_DIR)); + summary.rendererCloned = true; + ok('Cloned Nitro_Render_V3 to ' + RENDERER_DIR); +} + +async function setupRenderer(opts) { + if (!existsSync(RENDERER_DIR)) { + throw new Error('Renderer directory not found: ' + RENDERER_DIR + '. Re-run without --skip-clone or clone it manually.'); + } + await runShell('yarn install', RENDERER_DIR); + if (opts.skipLink) { info('--skip-link: skipping yarn link in renderer'); return; } + try { + await runShell('yarn link', RENDERER_DIR); + } catch (e) { + warn('yarn link in renderer failed (likely already linked): ' + e.message); + } +} + +async function setupClient(opts) { + await runShell('yarn install', ROOT); + if (opts.skipLink) { info('--skip-link: skipping yarn link in client'); return; } + try { + await runShell('yarn link "@nitrots/nitro-renderer"', ROOT); + } catch (e) { + warn('yarn link "@nitrots/nitro-renderer" failed (likely already linked): ' + e.message); + } +} + +async function writeJsonMode(mode) { + const payload = { jsonMode: mode, configuredAt: new Date().toISOString() }; + await writeFile(NITRO_BUILD_FILE, JSON.stringify(payload, null, 2) + '\n', 'utf8'); +} + +async function chooseJsonMode(opts) { + if (opts.jsonMode) { + await writeJsonMode(opts.jsonMode); + summary.jsonMode = opts.jsonMode; + summary.jsonModeSource = 'CLI (--json-mode)'; + ok('JSON mode set to ' + opts.jsonMode + ' (from --json-mode)'); + return; + } + + let existing = null; + if (existsSync(NITRO_BUILD_FILE)) { + try { + const raw = await readFile(NITRO_BUILD_FILE, 'utf8'); + const parsed = JSON.parse(raw); + if (VALID_JSON_MODES.includes(parsed?.jsonMode)) existing = parsed.jsonMode; + } catch {} + } + + if (!opts.interactive) { + const mode = existing || DEFAULT_JSON_MODE; + await writeJsonMode(mode); + summary.jsonMode = mode; + summary.jsonModeSource = existing ? 'existing .nitro-build.json' : 'default (non-interactive)'; + info('--non-interactive: JSON mode = ' + mode + (existing ? ' (preserved)' : ' (default)')); + return; + } + + info('Pick how configuration files (renderer-config, ui-config, gamedata) are parsed.'); + info(' 1) JSON5 (recommended - accepts comments, trailing commas, single quotes)'); + info(' 2) JSON (legacy strict - only standard JSON valid)'); + if (existing) info(' Current value in .nitro-build.json: ' + existing); + + const rl = readline.createInterface({ input, output }); + activeReadline = rl; + let chosen = null; + try { + while (chosen === null) { + const defaultLabel = existing || '1=JSON5'; + const answer = (await rl.question(' Choice [' + defaultLabel + ']: ')).trim().toLowerCase(); + if (answer.length === 0) { + chosen = existing || DEFAULT_JSON_MODE; + } else if (answer === '1' || answer === 'json5' || answer === 'y' || answer === 'yes') { + chosen = 'json5'; + } else if (answer === '2' || answer === 'json' || answer === 'legacy' || answer === 'n' || answer === 'no') { + chosen = 'legacy'; + } else if (answer === 'auto') { + chosen = 'auto'; + } else { + warn('Invalid choice. Enter 1, 2, json5, json, legacy, or auto.'); + } + } + } finally { + activeReadline = null; + rl.close(); + } + + await writeJsonMode(chosen); + summary.jsonMode = chosen; + summary.jsonModeSource = 'interactive prompt'; + ok('JSON mode set to ' + chosen + ' -> wrote .nitro-build.json'); + if (chosen === 'legacy') { + warn('Legacy mode is strict: config files must be valid standard JSON (no comments, no trailing commas).'); + } +} + +async function copyConfigs() { + for (const entry of CONFIG_FILES) { + const src = join(CONFIG_DIR, entry.example); + const dst = join(CONFIG_DIR, entry.target); + if (!existsSync(src)) { + throw new Error('Missing example file: ' + src); + } + if (existsSync(dst)) { + warn(entry.target + ' already exists - keeping existing file (URL overrides will still patch it).'); + summary.configsKept.push(entry.target); + } else { + await copyFile(src, dst); + ok('Created ' + entry.target); + summary.configsCreated.push(entry.target); + } + } +} + +async function applyOverridesNonInteractive(opts) { + for (const entry of CONFIG_FILES) { + const dst = join(CONFIG_DIR, entry.target); + const raw = await readFile(dst, 'utf8'); + let obj; + try { + obj = JSON.parse(raw); + } catch (e) { + throw new Error('Could not parse ' + entry.target + ' as JSON: ' + e.message); + } + let changed = false; + for (const key of entry.keys) { + if (Object.prototype.hasOwnProperty.call(opts.urlOverrides, key)) { + const value = opts.urlOverrides[key]; + const result = validateValue(value, KEY_SPECS[key]); + if (!result.valid) { + throw new Error('Invalid value for --' + KEY_SPECS[key].flag + '=' + JSON.stringify(value) + ': ' + result.error); + } + if (obj[key] !== value) { + obj[key] = value; + changed = true; + } + } + } + if (changed) { + await writeFile(dst, JSON.stringify(obj, null, 4) + '\n'); + ok('Updated ' + entry.target + ' (from CLI flags)'); + summary.configsPatched.push(entry.target); + } + } +} + +async function promptConfigs(opts) { + const overrideKeys = Object.keys(opts.urlOverrides); + if (!opts.interactive) { + if (overrideKeys.length > 0) { + info('--non-interactive with ' + overrideKeys.length + ' URL override(s); applying without prompts'); + await applyOverridesNonInteractive(opts); + } else { + info('--non-interactive: keeping URL values from .example defaults'); + } + return; + } + info('Press Enter to keep the current value shown in [brackets]. URLs are validated.'); + if (overrideKeys.length > 0) { + info('CLI overrides take precedence and skip prompts: ' + overrideKeys.map(k => '--' + KEY_SPECS[k].flag).join(', ')); + } + const rl = readline.createInterface({ input, output }); + activeReadline = rl; + try { + for (const entry of CONFIG_FILES) { + const dst = join(CONFIG_DIR, entry.target); + const raw = await readFile(dst, 'utf8'); + let obj; + try { + obj = JSON.parse(raw); + } catch (e) { + throw new Error('Could not parse ' + entry.target + ' as JSON: ' + e.message); + } + console.log('\n ' + c.bold + entry.target + c.reset); + let changed = false; + for (const key of entry.keys) { + const spec = KEY_SPECS[key]; + if (Object.prototype.hasOwnProperty.call(opts.urlOverrides, key)) { + const value = opts.urlOverrides[key]; + const result = validateValue(value, spec); + if (!result.valid) { + throw new Error('Invalid value for --' + spec.flag + '=' + JSON.stringify(value) + ': ' + result.error); + } + if (obj[key] !== value) { obj[key] = value; changed = true; } + console.log(' ' + c.dim + key + ' = ' + value + ' (from --' + spec.flag + ')' + c.reset); + continue; + } + const current = obj[key] === undefined ? '' : String(obj[key]); + while (true) { + const answer = await rl.question(' ' + key + ' [' + current + ']: '); + const trimmed = answer.trim(); + if (trimmed.length === 0) break; + if (trimmed === current) break; + const result = validateValue(trimmed, spec); + if (!result.valid) { + warn('Invalid: ' + result.error + '. Try again or press Enter to keep current.'); + continue; + } + obj[key] = trimmed; + changed = true; + break; + } + } + if (changed) { + await writeFile(dst, JSON.stringify(obj, null, 4) + '\n'); + ok('Updated ' + entry.target); + summary.configsPatched.push(entry.target); + } else { + info('No changes to ' + entry.target); + } + } + } finally { + activeReadline = null; + rl.close(); + } +} + +async function runBuild(opts) { + if (opts.skipBuild) { info('--skip-build: skipping yarn build'); summary.buildSkipped = true; return; } + await runShell('yarn build', ROOT); + summary.buildRan = true; +} + +function printSummary() { + const distPath = join(ROOT, 'dist'); + console.log(''); + console.log(c.bold + '================================================================' + c.reset); + console.log(c.bold + ' Installation summary' + c.reset); + console.log(c.bold + '================================================================' + c.reset); + console.log(' Renderer: ' + RENDERER_DIR + (summary.rendererCloned ? ' (cloned)' : ' (already present)')); + if (summary.configsCreated.length) console.log(' Created: ' + summary.configsCreated.join(', ')); + if (summary.configsKept.length) console.log(' Kept: ' + summary.configsKept.join(', ')); + if (summary.configsPatched.length) console.log(' Patched: ' + summary.configsPatched.join(', ')); + if (summary.jsonMode) console.log(' JSON mode: ' + summary.jsonMode + (summary.jsonModeSource ? ' (' + summary.jsonModeSource + ')' : '')); + if (summary.buildRan) console.log(' Build: ' + c.green + 'OK' + c.reset + ' -> ' + distPath); + else if (summary.buildSkipped) console.log(' Build: skipped'); + console.log(''); + console.log(' Next steps:'); + console.log(' - Development: yarn start'); + if (summary.buildRan) { + console.log(' - Production: deploy the contents of ' + distPath + ' to your webserver'); + } else { + console.log(' - Production: yarn build, then deploy ' + distPath); + } + console.log(c.bold + '================================================================' + c.reset); +} + +async function main() { + const opts = parseArgs(); + if (opts.help) { printUsage(); process.exit(0); } + + console.log(c.bold + 'Nitro-V3 installer' + c.reset + ' (' + (IS_WINDOWS ? 'Windows' : platform()) + ')'); + console.log('Project root: ' + ROOT); + + step(STEPS[0]); await checkPrereqs(); + step(STEPS[1]); await cloneRenderer(opts); + step(STEPS[2]); await setupRenderer(opts); + step(STEPS[3]); await setupClient(opts); + step(STEPS[4]); await copyConfigs(); + step(STEPS[5]); await chooseJsonMode(opts); + step(STEPS[6]); await promptConfigs(opts); + step(STEPS[7]); await runBuild(opts); + step(STEPS[8]); printSummary(); +} + +process.on('SIGINT', () => { + if (activeReadline) { + try { activeReadline.close(); } catch {} + activeReadline = null; + } + const label = STEPS[currentStep - 1] || 'startup'; + console.error(''); + warn('Aborted at step ' + currentStep + ' (' + label + ')'); + process.exit(130); +}); + +main().catch(e => { + const label = STEPS[currentStep - 1] || 'startup'; + err(''); + err('Step ' + currentStep + ' (' + label + ') failed:'); + err(' ' + e.message); + process.exit(1); +}); diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..e75c44b --- /dev/null +++ b/install.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -e +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec node "$DIR/install.mjs" "$@"