mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 06:56:20 +00:00
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
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
install.sh text eol=lf
|
||||
install.mjs text eol=lf
|
||||
*.sh text eol=lf
|
||||
*.bat text eol=crlf
|
||||
@@ -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=<json5\|legacy\|auto>` — 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\)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
node "%~dp0install.mjs" %*
|
||||
+556
@@ -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 + '=<value>' + ' '.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 --<key>=<value>',
|
||||
' --json-mode=<json5|legacy|auto> 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);
|
||||
});
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec node "$DIR/install.mjs" "$@"
|
||||
Reference in New Issue
Block a user