From 5d56992ea832657a148d948a33cf6a08cee34d4d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 21 Jul 2025 00:24:49 +0300 Subject: [PATCH] Feat/global install (#4289) * Update npm dependencies * Add global paths mode Closes #4209 * Update src/command-line.js Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Async => Promise.resolve * Remove unnecessary Promise.resolve() --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .github/readme.md | 69 +++++++++++++++++++------- index.d.ts | 5 ++ package-lock.json | 80 +++++++++++++++++++++---------- package.json | 16 ++++--- server.js | 2 + src/command-line.js | 109 +++++++++++++++++++++++++++++------------- src/config-init.js | 8 ++++ src/electron/index.js | 2 +- src/server-global.js | 5 ++ src/server-main.js | 48 ++++++++++--------- 10 files changed, 236 insertions(+), 108 deletions(-) mode change 100644 => 100755 server.js create mode 100755 src/server-global.js diff --git a/.github/readme.md b/.github/readme.md index 34b520891..e4c4d40b6 100644 --- a/.github/readme.md +++ b/.github/readme.md @@ -330,12 +330,44 @@ chmod +x launcher.sh && ./launcher.sh > \[!NOTE] > **SillyTavern can be run natively on Android devices using Termux, but we do not provide official support for this use case.** > -> **Please refer to this guide by ArroganceComplex#2659:** +> **Please refer to the documentation for more information:** > -> * +> * **Unsupported platform: android arm LEtime-web.** 32-bit Android requires an external dependency that can't be installed with npm. Use the following command to install it: `pkg install esbuild`. Then run the usual installation steps. +## Global / Standalone mode + +There are two modes of running SillyTavern that differ in how they handle the configuration and data paths. + +* **Standalone mode** (default) - uses the `config.yaml` file and `data` directory in the server directory. All data will be constrained to the installation path. This is the recommended mode for most users. +* **Global mode** - uses the system-wide paths for configuration and data. This is useful for installing SillyTavern as a package or when you want to share the same configuration and data across multiple installations. + +> [!NOTE] +> Installations made by using the [official npm package](https://www.npmjs.com/package/sillytavern) (e.g. `npx sillytavern@latest`) will run in global mode by default. + +### Data paths + +**Standalone mode** paths are relative to the SillyTavern installation directory: + +* **Config path**: `./config.yaml` +* **Data root**: `./data/` + +**Global mode** paths are OS-dependent: + +* **Linux**: `~/.local/share/SillyTavern/config.yaml` and `~/.local/share/SillyTavern/data/` +* **Windows**: `%APPDATA%\SillyTavern\config.yaml` and `%APPDATA%\SillyTavern\data\` +* **MacOS**: `~/Library/Application Support/SillyTavern/config.yaml` and `~/Library/Application Support/SillyTavern/data/` + +### How to run in global mode + +> [!WARNING] +> `dataRoot` and `configPath` can't be overridden with CLI arguments or config.yaml when running in global mode. + +1. Pass the `--global` argument to the server startup command (e.g. `node server.js --global`). +2. Pass the `--global` argument to the shell startup script (e.g. `Start.bat --global` or `./start.sh --global`). +3. Use the `start:global` script in the `package.json` file (e.g. `npm run start:global`). + ## Command-line arguments You can pass command-line arguments to SillyTavern server startup to override some settings in `config.yaml`. @@ -357,29 +389,30 @@ Start.bat --port 8000 --listen false | Option | Description | Type | |---------------------------------|----------------------------------------------------------------------|----------| -| `--version` | Show version number | boolean | -| `--configPath` | Override the path to the config.yaml file | string | -| `--dataRoot` | Root directory for data storage | string | +| `--version` | Shows the version number | boolean | +| `--global` | Forces the use of system-wide paths for application data (see above) | boolean | +| `--configPath` | Overrides the path to the config.yaml file (standalone mode only) | string | +| `--dataRoot` | Sets the root directory for data storage (standalone mode only) | string | | `--port` | Sets the port under which SillyTavern will run | number | -| `--listen` | SillyTavern will listen on all network interfaces | boolean | +| `--listen` | Makes SillyTavern listen on all network interfaces | boolean | | `--whitelist` | Enables whitelist mode | boolean | | `--basicAuthMode` | Enables basic authentication | boolean | -| `--enableIPv4` | Enables IPv4 protocol | boolean | -| `--enableIPv6` | Enables IPv6 protocol | boolean | -| `--listenAddressIPv4` | Specific IPv4 address to listen to | string | -| `--listenAddressIPv6` | Specific IPv6 address to listen to | string | +| `--enableIPv4` | Enables the IPv4 protocol | boolean | +| `--enableIPv6` | Enables the IPv6 protocol | boolean | +| `--listenAddressIPv4` | Specifies the IPv4 address to listen on | string | +| `--listenAddressIPv6` | Specifies the IPv6 address to listen on | string | | `--dnsPreferIPv6` | Prefers IPv6 for DNS | boolean | | `--ssl` | Enables SSL | boolean | -| `--certPath` | Path to your certificate file | string | -| `--keyPath` | Path to your private key file | string | -| `--browserLaunchEnabled` | Automatically launch SillyTavern in the browser | boolean | -| `--browserLaunchHostname` | Browser launch hostname | string | +| `--certPath` | Sets the path to your certificate file | string | +| `--keyPath` | Sets the path to your private key file | string | +| `--browserLaunchEnabled` | Automatically launches SillyTavern in the browser | boolean | +| `--browserLaunchHostname` | Sets the browser launch hostname | string | | `--browserLaunchPort` | Overrides the port for browser launch | string | | `--browserLaunchAvoidLocalhost` | Avoids using 'localhost' for browser launch in auto mode | boolean | -| `--corsProxy` | Enables CORS proxy | boolean | -| `--requestProxyEnabled` | Enables a use of proxy for outgoing requests | boolean | -| `--requestProxyUrl` | Request proxy URL (HTTP or SOCKS protocols) | string | -| `--requestProxyBypass` | Request proxy bypass list (space separated list of hosts) | array | +| `--corsProxy` | Enables the CORS proxy | boolean | +| `--requestProxyEnabled` | Enables the use of a proxy for outgoing requests | boolean | +| `--requestProxyUrl` | Sets the request proxy URL (HTTP or SOCKS protocols) | string | +| `--requestProxyBypass` | Sets the request proxy bypass list (space-separated list of hosts) | array | | `--disableCsrf` | Disables CSRF protection (NOT RECOMMENDED) | boolean | ## Remote connections diff --git a/index.d.ts b/index.d.ts index b566dc355..712253bed 100644 --- a/index.d.ts +++ b/index.d.ts @@ -66,4 +66,9 @@ declare global { * Parsed command line arguments. */ var COMMAND_LINE_ARGS: CommandLineArguments; + + /** + * Forces a global mode if set to `true` before parsing the CLI arguments. + */ + var FORCE_GLOBAL_MODE: boolean; } diff --git a/package-lock.json b/package-lock.json index 7dcb84172..d3bd7b817 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "@popperjs/core": "^2.11.8", "@zeldafan0225/ai_horde": "^5.2.0", "archiver": "^7.0.1", - "bing-translate-api": "^4.0.2", + "bing-translate-api": "^4.1.0", "body-parser": "^1.20.2", "bowser": "^2.11.0", "bytes": "^3.1.2", @@ -55,8 +55,9 @@ "diff-match-patch": "^1.0.5", "dompurify": "^3.2.6", "droll": "^0.2.1", + "env-paths": "^3.0.0", "express": "^4.21.0", - "form-data": "^4.0.3", + "form-data": "^4.0.4", "fuse.js": "^7.1.0", "google-translate-api-browser": "^3.0.1", "google-translate-api-x": "^10.7.2", @@ -77,7 +78,7 @@ "multer": "^2.0.2", "node-fetch": "^3.3.2", "node-persist": "^4.0.4", - "open": "^10.1.2", + "open": "^10.2.0", "png-chunk-text": "^1.0.0", "png-chunks-extract": "^1.0.0", "proxy-agent": "^6.5.0", @@ -95,13 +96,13 @@ "wavefile": "^11.0.0", "webpack": "^5.98.0", "write-file-atomic": "^5.0.1", - "ws": "^8.18.2", + "ws": "^8.18.3", "yaml": "^2.8.0", "yargs": "^17.7.1", "yauzl": "^3.2.0" }, "bin": { - "sillytavern": "server.js" + "sillytavern": "src/server-global.js" }, "devDependencies": { "@types/archiver": "^6.0.3", @@ -117,9 +118,9 @@ "@types/jquery-cropper": "^1.0.4", "@types/jquery.transit": "^0.9.33", "@types/jqueryui": "^1.12.24", - "@types/lodash": "^4.17.17", + "@types/lodash": "^4.17.20", "@types/mime-types": "^3.0.1", - "@types/multer": "^1.4.13", + "@types/multer": "^2.0.0", "@types/node": "^18.19.84", "@types/node-persist": "^3.1.8", "@types/png-chunk-text": "^1.0.3", @@ -1972,9 +1973,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz", - "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==", + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", "dev": true, "license": "MIT" }, @@ -1999,9 +2000,9 @@ "license": "MIT" }, "node_modules/@types/multer": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", - "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", "dev": true, "license": "MIT", "dependencies": { @@ -2832,9 +2833,9 @@ } }, "node_modules/bing-translate-api": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bing-translate-api/-/bing-translate-api-4.0.2.tgz", - "integrity": "sha512-JJ8XUehnxzOhHU91oy86xEtp8OOMjVEjCZJX042fKxoO19NNvxJ5omeCcxQNFoPbDqVpBJwqiGVquL0oPdQm1Q==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bing-translate-api/-/bing-translate-api-4.1.0.tgz", + "integrity": "sha512-oP2663Yd5MXX4kbB/3LdS9YgPiE+ls9+2iFZH2ZXigWhWyHT3R4m6aCup4TNJd3/U4gqHHnQoxTaIW7uOf4+vA==", "license": "MIT", "dependencies": { "got": "^11.8.6" @@ -4087,6 +4088,18 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -4786,9 +4799,9 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -6420,15 +6433,15 @@ } }, "node_modules/open": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", - "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "license": "MIT", "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "wsl-utils": "^0.1.0" }, "engines": { "node": ">=18" @@ -8503,9 +8516,9 @@ } }, "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -8523,6 +8536,21 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xhr": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", diff --git a/package.json b/package.json index 0c6bbc480..7d5911ec6 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@popperjs/core": "^2.11.8", "@zeldafan0225/ai_horde": "^5.2.0", "archiver": "^7.0.1", - "bing-translate-api": "^4.0.2", + "bing-translate-api": "^4.1.0", "body-parser": "^1.20.2", "bowser": "^2.11.0", "bytes": "^3.1.2", @@ -45,8 +45,9 @@ "diff-match-patch": "^1.0.5", "dompurify": "^3.2.6", "droll": "^0.2.1", + "env-paths": "^3.0.0", "express": "^4.21.0", - "form-data": "^4.0.3", + "form-data": "^4.0.4", "fuse.js": "^7.1.0", "google-translate-api-browser": "^3.0.1", "google-translate-api-x": "^10.7.2", @@ -67,7 +68,7 @@ "multer": "^2.0.2", "node-fetch": "^3.3.2", "node-persist": "^4.0.4", - "open": "^10.1.2", + "open": "^10.2.0", "png-chunk-text": "^1.0.0", "png-chunks-extract": "^1.0.0", "proxy-agent": "^6.5.0", @@ -85,7 +86,7 @@ "wavefile": "^11.0.0", "webpack": "^5.98.0", "write-file-atomic": "^5.0.1", - "ws": "^8.18.2", + "ws": "^8.18.3", "yaml": "^2.8.0", "yargs": "^17.7.1", "yauzl": "^3.2.0" @@ -115,6 +116,7 @@ "scripts": { "start": "node server.js", "debug": "node --inspect server.js", + "start:global": "node server.js --global", "start:electron": "cd ./src/electron && npm run start", "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js", "start:bun": "bun server.js", @@ -126,7 +128,7 @@ "plugins:install": "node plugins install" }, "bin": { - "sillytavern": "./server.js" + "sillytavern": "./src/server-global.js" }, "rules": { "no-path-concat": "off", @@ -147,9 +149,9 @@ "@types/jquery-cropper": "^1.0.4", "@types/jquery.transit": "^0.9.33", "@types/jqueryui": "^1.12.24", - "@types/lodash": "^4.17.17", + "@types/lodash": "^4.17.20", "@types/mime-types": "^3.0.1", - "@types/multer": "^1.4.13", + "@types/multer": "^2.0.0", "@types/node": "^18.19.84", "@types/node-persist": "^3.1.8", "@types/png-chunk-text": "^1.0.3", diff --git a/server.js b/server.js old mode 100644 new mode 100755 index 1a40b4fcc..fda9d1bc4 --- a/server.js +++ b/server.js @@ -2,6 +2,8 @@ import { CommandLineParser } from './src/command-line.js'; import { serverDirectory } from './src/server-directory.js'; +console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`); + // config.yaml will be set when parsing command line arguments const cliArgs = new CommandLineParser().parse(process.argv); globalThis.DATA_ROOT = cliArgs.dataRoot; diff --git a/src/command-line.js b/src/command-line.js index 9e6e9a4b1..fb951363e 100644 --- a/src/command-line.js +++ b/src/command-line.js @@ -1,6 +1,9 @@ +import fs from 'node:fs'; +import path from 'node:path'; import yargs from 'yargs/yargs'; import { hideBin } from 'yargs/helpers'; import ipRegex from 'ip-regex'; +import envPaths from 'env-paths'; import { canResolve, color, getConfigValue, stringToBool } from './util.js'; import { initConfig } from './config-init.js'; @@ -39,11 +42,18 @@ import { initConfig } from './config-init.js'; * Provides a command line arguments parser. */ export class CommandLineParser { - constructor() { - /** @type {CommandLineArguments} */ - this.default = Object.freeze({ - configPath: './config.yaml', - dataRoot: './data', + /** + * Gets the default configuration values. + * @param {boolean} isGlobal If the configuration is global or not + * @returns {CommandLineArguments} Default configuration values + */ + getDefaultConfig(isGlobal) { + const appPaths = envPaths('SillyTavern', { suffix: '' }); + const configPath = isGlobal ? path.join(appPaths.data, 'config.yaml') : './config.yaml'; + const dataPath = isGlobal ? path.join(appPaths.data, 'data') : './data'; + return Object.freeze({ + configPath: configPath, + dataRoot: dataPath, port: 8000, listen: false, listenAddressIPv6: '[::]', @@ -78,7 +88,9 @@ export class CommandLineParser { throw new Error('getBrowserLaunchUrl is not implemented'); }, }); + } + constructor() { this.booleanAutoOptions = [true, false, 'auto']; } @@ -91,10 +103,15 @@ export class CommandLineParser { parse(args) { const cliArguments = yargs(hideBin(args)) .usage('Usage: [options]\nOptions that are not provided will be filled with config values.') + .option('global', { + type: 'boolean', + default: null, + describe: 'Use global data and config paths instead of the server directory', + }) .option('configPath', { type: 'string', default: null, - describe: 'Path to the config file', + describe: 'Path to the config file (only for standalone mode)', }) .option('enableIPv6', { type: 'string', @@ -184,7 +201,7 @@ export class CommandLineParser { .option('dataRoot', { type: 'string', default: null, - describe: 'Root directory for data storage', + describe: 'Root directory for data storage (only for standalone mode)', }) .option('basicAuthMode', { type: 'boolean', @@ -228,33 +245,57 @@ export class CommandLineParser { }) .parseSync(); - const configPath = cliArguments.configPath ?? this.default.configPath; + const isGlobal = globalThis.FORCE_GLOBAL_MODE ?? cliArguments.global ?? false; + const defaultConfig = this.getDefaultConfig(isGlobal); + + if (isGlobal && cliArguments.configPath) { + console.warn(color.yellow('Warning: "--configPath" argument is ignored in global mode')); + } + + if (isGlobal && cliArguments.dataRoot) { + console.warn(color.yellow('Warning: "--dataRoot" argument is ignored in global mode')); + } + + const configPath = isGlobal + ? defaultConfig.configPath + : (cliArguments.configPath ?? defaultConfig.configPath); + if (isGlobal && !fs.existsSync(path.dirname(configPath))) { + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + } initConfig(configPath); + + const dataRoot = isGlobal + ? defaultConfig.dataRoot + : (cliArguments.dataRoot ?? getConfigValue('dataRoot', defaultConfig.dataRoot)); + if (isGlobal && !fs.existsSync(dataRoot)) { + fs.mkdirSync(dataRoot, { recursive: true }); + } + /** @type {CommandLineArguments} */ const result = { configPath: configPath, - dataRoot: cliArguments.dataRoot ?? getConfigValue('dataRoot', this.default.dataRoot), - port: cliArguments.port ?? getConfigValue('port', this.default.port, 'number'), - listen: cliArguments.listen ?? getConfigValue('listen', this.default.listen, 'boolean'), - listenAddressIPv6: cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', this.default.listenAddressIPv6), - listenAddressIPv4: cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', this.default.listenAddressIPv4), - enableIPv4: stringToBool(cliArguments.enableIPv4) ?? stringToBool(getConfigValue('protocol.ipv4', this.default.enableIPv4)) ?? this.default.enableIPv4, - enableIPv6: stringToBool(cliArguments.enableIPv6) ?? stringToBool(getConfigValue('protocol.ipv6', this.default.enableIPv6)) ?? this.default.enableIPv6, - dnsPreferIPv6: cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', this.default.dnsPreferIPv6, 'boolean'), - browserLaunchEnabled: cliArguments.browserLaunchEnabled ?? cliArguments.autorun ?? getConfigValue('browserLaunch.enabled', this.default.browserLaunchEnabled, 'boolean'), - browserLaunchHostname: cliArguments.browserLaunchHostname ?? cliArguments.autorunHostname ?? getConfigValue('browserLaunch.hostname', this.default.browserLaunchHostname), - browserLaunchPort: cliArguments.browserLaunchPort ?? cliArguments.autorunPortOverride ?? getConfigValue('browserLaunch.port', this.default.browserLaunchPort, 'number'), - browserLaunchAvoidLocalhost: cliArguments.browserLaunchAvoidLocalhost ?? cliArguments.avoidLocalhost ?? getConfigValue('browserLaunch.avoidLocalhost', this.default.browserLaunchAvoidLocalhost, 'boolean'), - enableCorsProxy: cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', this.default.enableCorsProxy, 'boolean'), - disableCsrf: cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', this.default.disableCsrf, 'boolean'), - ssl: cliArguments.ssl ?? getConfigValue('ssl.enabled', this.default.ssl, 'boolean'), - certPath: cliArguments.certPath ?? getConfigValue('ssl.certPath', this.default.certPath), - keyPath: cliArguments.keyPath ?? getConfigValue('ssl.keyPath', this.default.keyPath), - whitelistMode: cliArguments.whitelist ?? getConfigValue('whitelistMode', this.default.whitelistMode, 'boolean'), - basicAuthMode: cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', this.default.basicAuthMode, 'boolean'), - requestProxyEnabled: cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', this.default.requestProxyEnabled, 'boolean'), - requestProxyUrl: cliArguments.requestProxyUrl ?? getConfigValue('requestProxy.url', this.default.requestProxyUrl), - requestProxyBypass: cliArguments.requestProxyBypass ?? getConfigValue('requestProxy.bypass', this.default.requestProxyBypass), + dataRoot: dataRoot, + port: cliArguments.port ?? getConfigValue('port', defaultConfig.port, 'number'), + listen: cliArguments.listen ?? getConfigValue('listen', defaultConfig.listen, 'boolean'), + listenAddressIPv6: cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', defaultConfig.listenAddressIPv6), + listenAddressIPv4: cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', defaultConfig.listenAddressIPv4), + enableIPv4: stringToBool(cliArguments.enableIPv4) ?? stringToBool(getConfigValue('protocol.ipv4', defaultConfig.enableIPv4)) ?? defaultConfig.enableIPv4, + enableIPv6: stringToBool(cliArguments.enableIPv6) ?? stringToBool(getConfigValue('protocol.ipv6', defaultConfig.enableIPv6)) ?? defaultConfig.enableIPv6, + dnsPreferIPv6: cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', defaultConfig.dnsPreferIPv6, 'boolean'), + browserLaunchEnabled: cliArguments.browserLaunchEnabled ?? cliArguments.autorun ?? getConfigValue('browserLaunch.enabled', defaultConfig.browserLaunchEnabled, 'boolean'), + browserLaunchHostname: cliArguments.browserLaunchHostname ?? cliArguments.autorunHostname ?? getConfigValue('browserLaunch.hostname', defaultConfig.browserLaunchHostname), + browserLaunchPort: cliArguments.browserLaunchPort ?? cliArguments.autorunPortOverride ?? getConfigValue('browserLaunch.port', defaultConfig.browserLaunchPort, 'number'), + browserLaunchAvoidLocalhost: cliArguments.browserLaunchAvoidLocalhost ?? cliArguments.avoidLocalhost ?? getConfigValue('browserLaunch.avoidLocalhost', defaultConfig.browserLaunchAvoidLocalhost, 'boolean'), + enableCorsProxy: cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', defaultConfig.enableCorsProxy, 'boolean'), + disableCsrf: cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', defaultConfig.disableCsrf, 'boolean'), + ssl: cliArguments.ssl ?? getConfigValue('ssl.enabled', defaultConfig.ssl, 'boolean'), + certPath: cliArguments.certPath ?? getConfigValue('ssl.certPath', defaultConfig.certPath), + keyPath: cliArguments.keyPath ?? getConfigValue('ssl.keyPath', defaultConfig.keyPath), + whitelistMode: cliArguments.whitelist ?? getConfigValue('whitelistMode', defaultConfig.whitelistMode, 'boolean'), + basicAuthMode: cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', defaultConfig.basicAuthMode, 'boolean'), + requestProxyEnabled: cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', defaultConfig.requestProxyEnabled, 'boolean'), + requestProxyUrl: cliArguments.requestProxyUrl ?? getConfigValue('requestProxy.url', defaultConfig.requestProxyUrl), + requestProxyBypass: cliArguments.requestProxyBypass ?? getConfigValue('requestProxy.bypass', defaultConfig.requestProxyBypass), getIPv4ListenUrl: function () { const isValid = ipRegex.v4({ exact: true }).test(this.listenAddressIPv4); return new URL( @@ -302,13 +343,13 @@ export class CommandLineParser { }; if (!this.booleanAutoOptions.includes(result.enableIPv6)) { - console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', this.booleanAutoOptions, '\n setting to:', this.default.enableIPv6); - result.enableIPv6 = this.default.enableIPv6; + console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', this.booleanAutoOptions, '\n setting to:', defaultConfig.enableIPv6); + result.enableIPv6 = defaultConfig.enableIPv6; } if (!this.booleanAutoOptions.includes(result.enableIPv4)) { - console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', this.booleanAutoOptions, '\n setting to:', this.default.enableIPv4); - result.enableIPv4 = this.default.enableIPv4; + console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', this.booleanAutoOptions, '\n setting to:', defaultConfig.enableIPv4); + result.enableIPv4 = defaultConfig.enableIPv4; } return result; diff --git a/src/config-init.js b/src/config-init.js index b372f8e82..3cc622039 100644 --- a/src/config-init.js +++ b/src/config-init.js @@ -148,6 +148,13 @@ function getAllKeys(obj, prefix = '') { export function addMissingConfigValues(configPath) { try { const defaultConfig = yaml.parse(fs.readFileSync(path.join(serverDirectory, './default/config.yaml'), 'utf8')); + + if (!fs.existsSync(configPath)) { + console.warn(color.yellow(`Warning: config.yaml not found at ${configPath}. Creating a new one with default values.`)); + fs.writeFileSync(configPath, yaml.stringify(defaultConfig)); + return; + } + let config = yaml.parse(fs.readFileSync(configPath, 'utf8')); // Migrate old keys to new keys @@ -224,6 +231,7 @@ export function addMissingConfigValues(configPath) { * @param {string} configPath Path to config.yaml */ export function initConfig(configPath) { + console.log('Using config path:', color.green(configPath)); setConfigFilePath(configPath); addMissingConfigValues(configPath); } diff --git a/src/electron/index.js b/src/electron/index.js index 14d3b574c..6126ef45c 100644 --- a/src/electron/index.js +++ b/src/electron/index.js @@ -41,7 +41,7 @@ function startServer() { const sillyTavernRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); process.chdir(sillyTavernRoot); - import('../../server.js'); + import('../server-global.js'); }); } diff --git a/src/server-global.js b/src/server-global.js new file mode 100755 index 000000000..50a211f37 --- /dev/null +++ b/src/server-global.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node +globalThis.FORCE_GLOBAL_MODE = true; +await import('../server.js'); + +export {}; diff --git a/src/server-main.js b/src/server-main.js index 55b4ad932..f65eff464 100644 --- a/src/server-main.js +++ b/src/server-main.js @@ -19,16 +19,6 @@ import bodyParser from 'body-parser'; import './fetch-patch.js'; import { serverDirectory } from './server-directory.js'; -console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`); - -// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. -// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 -// Safe to remove once support for Node v20 is dropped. -if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) { - // @ts-ignore - if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false); -} - import { serverEvents, EVENT_NAMES } from './server-events.js'; import { loadPlugins } from './plugin-loader.js'; import { @@ -78,6 +68,14 @@ import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } fro import { diskCache } from './endpoints/characters.js'; import { migrateFlatSecrets } from './endpoints/secrets.js'; +// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. +// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 +// Safe to remove once support for Node v20 is dropped. +if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) { + // @ts-ignore + if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false); +} + // Unrestrict console logs display limit util.inspect.defaultOptions.maxArrayLength = null; util.inspect.defaultOptions.maxStringLength = null; @@ -91,18 +89,6 @@ if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) { process.exit(1); } -try { - if (cliArgs.dnsPreferIPv6) { - dns.setDefaultResultOrder('ipv6first'); - console.log('Preferring IPv6 for DNS resolution'); - } else { - dns.setDefaultResultOrder('ipv4first'); - console.log('Preferring IPv4 for DNS resolution'); - } -} catch (error) { - console.warn('Failed to set DNS resolution order. Possibly unsupported in this Node version.'); -} - const app = express(); app.use(helmet({ contentSecurityPolicy: false, @@ -400,8 +386,26 @@ function apply404Middleware() { }); } +/** + * Sets the DNS resolution order based on the command line arguments. + */ +function setDnsResolutionOrder() { + try { + if (cliArgs.dnsPreferIPv6) { + dns.setDefaultResultOrder('ipv6first'); + console.log('Preferring IPv6 for DNS resolution'); + } else { + dns.setDefaultResultOrder('ipv4first'); + console.log('Preferring IPv4 for DNS resolution'); + } + } catch (error) { + console.warn('Failed to set DNS resolution order. Possibly unsupported in this Node version.'); + } +} + // User storage module needs to be initialized before starting the server initUserStorage(globalThis.DATA_ROOT) + .then(setDnsResolutionOrder) .then(ensurePublicDirectoriesExist) .then(migrateUserData) .then(migrateSystemPrompts)