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>
Signed| @@ -330,12 +330,44 @@ chmod +x launcher.sh && ./launcher.sh | ||
| 330 | 330 | > \[!NOTE] |
| 331 | 331 | > **SillyTavern can be run natively on Android devices using Termux, but we do not provide official support for this use case.** |
| 332 | 332 | > |
| 333 | 333 | > **Please refer to thisthe guidedocumentation byfor ArroganceComplex#2659more information:** |
| 334 | 334 | > |
| 335 | 335 | > * <https://rentrydocs.orgsillytavern.app/STAIinstallation/android-Termux(termux)/> |
| 336 | 336 | |
| 337 | 337 | **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. |
| 338 | 338 | |
| 339 | +## Global / Standalone mode | |
| 340 | + | |
| 341 | +There are two modes of running SillyTavern that differ in how they handle the configuration and data paths. | |
| 342 | + | |
| 343 | +* **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. | |
| 344 | +* **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. | |
| 345 | + | |
| 346 | +> [!NOTE] | |
| 347 | +> 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. | |
| 348 | + | |
| 349 | +### Data paths | |
| 350 | + | |
| 351 | +**Standalone mode** paths are relative to the SillyTavern installation directory: | |
| 352 | + | |
| 353 | +* **Config path**: `./config.yaml` | |
| 354 | +* **Data root**: `./data/` | |
| 355 | + | |
| 356 | +**Global mode** paths are OS-dependent: | |
| 357 | + | |
| 358 | +* **Linux**: `~/.local/share/SillyTavern/config.yaml` and `~/.local/share/SillyTavern/data/` | |
| 359 | +* **Windows**: `%APPDATA%\SillyTavern\config.yaml` and `%APPDATA%\SillyTavern\data\` | |
| 360 | +* **MacOS**: `~/Library/Application Support/SillyTavern/config.yaml` and `~/Library/Application Support/SillyTavern/data/` | |
| 361 | + | |
| 362 | +### How to run in global mode | |
| 363 | + | |
| 364 | +> [!WARNING] | |
| 365 | +> `dataRoot` and `configPath` can't be overridden with CLI arguments or config.yaml when running in global mode. | |
| 366 | + | |
| 367 | +1. Pass the `--global` argument to the server startup command (e.g. `node server.js --global`). | |
| 368 | +2. Pass the `--global` argument to the shell startup script (e.g. `Start.bat --global` or `./start.sh --global`). | |
| 369 | +3. Use the `start:global` script in the `package.json` file (e.g. `npm run start:global`). | |
| 370 | + | |
| 339 | 371 | ## Command-line arguments |
| 340 | 372 | |
| 341 | 373 | 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 | ||
| 357 | 389 | |
| 358 | 390 | | Option | Description | Type | |
| 359 | 391 | |---------------------------------|----------------------------------------------------------------------|----------| |
| 360 | 392 | | `--version` | ShowShows the version number | boolean | |
| 361 | 393 | | `--configPathglobal` | OverrideForces the pathuse toof thesystem-wide config.yamlpaths file for application data (see above) | string boolean | |
| 362 | 394 | | `--dataRootconfigPath` | RootOverrides directorythe forpath datato storage the config.yaml file (standalone mode only) | string | |
| 395 | +| `--dataRoot` | Sets the root directory for data storage (standalone mode only) | string | | |
| 363 | 396 | | `--port` | Sets the port under which SillyTavern will run | number | |
| 364 | 397 | | `--listen` | SillyTavernMakes willSillyTavern listen on all network interfaces | boolean | |
| 365 | 398 | | `--whitelist` | Enables whitelist mode | boolean | |
| 366 | 399 | | `--basicAuthMode` | Enables basic authentication | boolean | |
| 367 | 400 | | `--enableIPv4` | Enables the IPv4 protocol | boolean | |
| 368 | 401 | | `--enableIPv6` | Enables the IPv6 protocol | boolean | |
| 369 | 402 | | `--listenAddressIPv4` | SpecificSpecifies the IPv4 address to listen to on | string | |
| 370 | 403 | | `--listenAddressIPv6` | SpecificSpecifies the IPv6 address to listen to on | string | |
| 371 | 404 | | `--dnsPreferIPv6` | Prefers IPv6 for DNS | boolean | |
| 372 | 405 | | `--ssl` | Enables SSL | boolean | |
| 373 | 406 | | `--certPath` | PathSets the path to your certificate file | string | |
| 374 | 407 | | `--keyPath` | PathSets the path to your private key file | string | |
| 375 | 408 | | `--browserLaunchEnabled` | Automatically launchlaunches SillyTavern in the browser | boolean | |
| 376 | 409 | | `--browserLaunchHostname` | BrowserSets the browser launch hostname | string | |
| 377 | 410 | | `--browserLaunchPort` | Overrides the port for browser launch | string | |
| 378 | 411 | | `--browserLaunchAvoidLocalhost` | Avoids using 'localhost' for browser launch in auto mode | boolean | |
| 379 | 412 | | `--corsProxy` | Enables the CORS proxy | boolean | |
| 380 | 413 | | `--requestProxyEnabled` | Enables athe use of a proxy for outgoing requests | boolean | |
| 381 | 414 | | `--requestProxyUrl` | RequestSets the request proxy URL (HTTP or SOCKS protocols) | string | |
| 382 | 415 | | `--requestProxyBypass` | RequestSets the request proxy bypass list (space -separated list of hosts) | array | |
| 383 | 416 | | `--disableCsrf` | Disables CSRF protection (NOT RECOMMENDED) | boolean | |
| 384 | 417 | |
| 385 | 418 | ## Remote connections |
| @@ -66,4 +66,9 @@ declare global { | ||
| 66 | 66 | * Parsed command line arguments. |
| 67 | 67 | */ |
| 68 | 68 | var COMMAND_LINE_ARGS: CommandLineArguments; |
| 69 | + | |
| 70 | + /** | |
| 71 | + * Forces a global mode if set to `true` before parsing the CLI arguments. | |
| 72 | + */ | |
| 73 | + var FORCE_GLOBAL_MODE: boolean; | |
| 69 | 74 | } |
| @@ -40,7 +40,7 @@ | ||
| 40 | 40 | "@popperjs/core": "^2.11.8", |
| 41 | 41 | "@zeldafan0225/ai_horde": "^5.2.0", |
| 42 | 42 | "archiver": "^7.0.1", |
| 43 | 43 | "bing-translate-api": "^4.01.20", |
| 44 | 44 | "body-parser": "^1.20.2", |
| 45 | 45 | "bowser": "^2.11.0", |
| 46 | 46 | "bytes": "^3.1.2", |
| @@ -55,8 +55,9 @@ | ||
| 55 | 55 | "diff-match-patch": "^1.0.5", |
| 56 | 56 | "dompurify": "^3.2.6", |
| 57 | 57 | "droll": "^0.2.1", |
| 58 | + "env-paths": "^3.0.0", | |
| 58 | 59 | "express": "^4.21.0", |
| 59 | 60 | "form-data": "^4.0.34", |
| 60 | 61 | "fuse.js": "^7.1.0", |
| 61 | 62 | "google-translate-api-browser": "^3.0.1", |
| 62 | 63 | "google-translate-api-x": "^10.7.2", |
| @@ -77,7 +78,7 @@ | ||
| 77 | 78 | "multer": "^2.0.2", |
| 78 | 79 | "node-fetch": "^3.3.2", |
| 79 | 80 | "node-persist": "^4.0.4", |
| 80 | 81 | "open": "^10.1.2.0", |
| 81 | 82 | "png-chunk-text": "^1.0.0", |
| 82 | 83 | "png-chunks-extract": "^1.0.0", |
| 83 | 84 | "proxy-agent": "^6.5.0", |
| @@ -95,13 +96,13 @@ | ||
| 95 | 96 | "wavefile": "^11.0.0", |
| 96 | 97 | "webpack": "^5.98.0", |
| 97 | 98 | "write-file-atomic": "^5.0.1", |
| 98 | 99 | "ws": "^8.18.23", |
| 99 | 100 | "yaml": "^2.8.0", |
| 100 | 101 | "yargs": "^17.7.1", |
| 101 | 102 | "yauzl": "^3.2.0" |
| 102 | 103 | }, |
| 103 | 104 | "bin": { |
| 104 | 105 | "sillytavern": "src/server-global.js" |
| 105 | 106 | }, |
| 106 | 107 | "devDependencies": { |
| 107 | 108 | "@types/archiver": "^6.0.3", |
| @@ -117,9 +118,9 @@ | ||
| 117 | 118 | "@types/jquery-cropper": "^1.0.4", |
| 118 | 119 | "@types/jquery.transit": "^0.9.33", |
| 119 | 120 | "@types/jqueryui": "^1.12.24", |
| 120 | 121 | "@types/lodash": "^4.17.1720", |
| 121 | 122 | "@types/mime-types": "^3.0.1", |
| 122 | 123 | "@types/multer": "^12.40.130", |
| 123 | 124 | "@types/node": "^18.19.84", |
| 124 | 125 | "@types/node-persist": "^3.1.8", |
| 125 | 126 | "@types/png-chunk-text": "^1.0.3", |
| @@ -1972,9 +1973,9 @@ | ||
| 1972 | 1973 | } |
| 1973 | 1974 | }, |
| 1974 | 1975 | "node_modules/@types/lodash": { |
| 1975 | 1976 | "version": "4.17.1720", |
| 1976 | 1977 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.1720.tgz", |
| 1977 | 1978 | "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNoH3MHACvFUEiujabxhaI/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", |
| 1978 | 1979 | "dev": true, |
| 1979 | 1980 | "license": "MIT" |
| 1980 | 1981 | }, |
| @@ -1999,9 +2000,9 @@ | ||
| 1999 | 2000 | "license": "MIT" |
| 2000 | 2001 | }, |
| 2001 | 2002 | "node_modules/@types/multer": { |
| 2002 | 2003 | "version": "12.40.130", |
| 2003 | 2004 | "resolved": "https://registry.npmjs.org/@types/multer/-/multer-12.40.130.tgz", |
| 2004 | 2005 | "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcawC3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", |
| 2005 | 2006 | "dev": true, |
| 2006 | 2007 | "license": "MIT", |
| 2007 | 2008 | "dependencies": { |
| @@ -2832,9 +2833,9 @@ | ||
| 2832 | 2833 | } |
| 2833 | 2834 | }, |
| 2834 | 2835 | "node_modules/bing-translate-api": { |
| 2835 | 2836 | "version": "4.01.20", |
| 2836 | 2837 | "resolved": "https://registry.npmjs.org/bing-translate-api/-/bing-translate-api-4.01.20.tgz", |
| 2837 | 2838 | "integrity": "sha512-JJ8XUehnxzOhHU91oy86xEtp8OOMjVEjCZJX042fKxoO19NNvxJ5omeCcxQNFoPbDqVpBJwqiGVquL0oPdQm1QoP2663Yd5MXX4kbB/3LdS9YgPiE+ls9+2iFZH2ZXigWhWyHT3R4m6aCup4TNJd3/U4gqHHnQoxTaIW7uOf4+vA==", |
| 2838 | 2839 | "license": "MIT", |
| 2839 | 2840 | "dependencies": { |
| 2840 | 2841 | "got": "^11.8.6" |
| @@ -4087,6 +4088,18 @@ | ||
| 4087 | 4088 | "url": "https://github.com/fb55/entities?sponsor=1" |
| 4088 | 4089 | } |
| 4089 | 4090 | }, |
| 4091 | + "node_modules/env-paths": { | |
| 4092 | + "version": "3.0.0", | |
| 4093 | + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", | |
| 4094 | + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", | |
| 4095 | + "license": "MIT", | |
| 4096 | + "engines": { | |
| 4097 | + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" | |
| 4098 | + }, | |
| 4099 | + "funding": { | |
| 4100 | + "url": "https://github.com/sponsors/sindresorhus" | |
| 4101 | + } | |
| 4102 | + }, | |
| 4090 | 4103 | "node_modules/es-define-property": { |
| 4091 | 4104 | "version": "1.0.1", |
| 4092 | 4105 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", |
| @@ -4786,9 +4799,9 @@ | ||
| 4786 | 4799 | } |
| 4787 | 4800 | }, |
| 4788 | 4801 | "node_modules/form-data": { |
| 4789 | 4802 | "version": "4.0.34", |
| 4790 | 4803 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.34.tgz", |
| 4791 | 4804 | "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwuKrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA4IlGTMF0Ow==", |
| 4792 | 4805 | "license": "MIT", |
| 4793 | 4806 | "dependencies": { |
| 4794 | 4807 | "asynckit": "^0.4.0", |
| @@ -6420,15 +6433,15 @@ | ||
| 6420 | 6433 | } |
| 6421 | 6434 | }, |
| 6422 | 6435 | "node_modules/open": { |
| 6423 | 6436 | "version": "10.1.2.0", |
| 6424 | 6437 | "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.0.tgz", |
| 6425 | 6438 | "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oImYgBpdJHPyQ2UE5x+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXwhlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", |
| 6426 | 6439 | "license": "MIT", |
| 6427 | 6440 | "dependencies": { |
| 6428 | 6441 | "default-browser": "^5.2.1", |
| 6429 | 6442 | "define-lazy-prop": "^3.0.0", |
| 6430 | 6443 | "is-inside-container": "^1.0.0", |
| 6431 | 6444 | "is-wsl-utils": "^30.1.0" |
| 6432 | 6445 | }, |
| 6433 | 6446 | "engines": { |
| 6434 | 6447 | "node": ">=18" |
| @@ -8503,9 +8516,9 @@ | ||
| 8503 | 8516 | } |
| 8504 | 8517 | }, |
| 8505 | 8518 | "node_modules/ws": { |
| 8506 | 8519 | "version": "8.18.23", |
| 8507 | 8520 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.23.tgz", |
| 8508 | 8521 | "integrity": "sha512-DMricUmwGZUVr+PEIGCY5tSlUt50cqyMXfCzX+AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lpoOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQGARUDw2XV2N4tvzg==", |
| 8509 | 8522 | "license": "MIT", |
| 8510 | 8523 | "engines": { |
| 8511 | 8524 | "node": ">=10.0.0" |
| @@ -8523,6 +8536,21 @@ | ||
| 8523 | 8536 | } |
| 8524 | 8537 | } |
| 8525 | 8538 | }, |
| 8539 | + "node_modules/wsl-utils": { | |
| 8540 | + "version": "0.1.0", | |
| 8541 | + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", | |
| 8542 | + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", | |
| 8543 | + "license": "MIT", | |
| 8544 | + "dependencies": { | |
| 8545 | + "is-wsl": "^3.1.0" | |
| 8546 | + }, | |
| 8547 | + "engines": { | |
| 8548 | + "node": ">=18" | |
| 8549 | + }, | |
| 8550 | + "funding": { | |
| 8551 | + "url": "https://github.com/sponsors/sindresorhus" | |
| 8552 | + } | |
| 8553 | + }, | |
| 8526 | 8554 | "node_modules/xhr": { |
| 8527 | 8555 | "version": "2.6.0", |
| 8528 | 8556 | "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", |
| @@ -30,7 +30,7 @@ | ||
| 30 | 30 | "@popperjs/core": "^2.11.8", |
| 31 | 31 | "@zeldafan0225/ai_horde": "^5.2.0", |
| 32 | 32 | "archiver": "^7.0.1", |
| 33 | 33 | "bing-translate-api": "^4.01.20", |
| 34 | 34 | "body-parser": "^1.20.2", |
| 35 | 35 | "bowser": "^2.11.0", |
| 36 | 36 | "bytes": "^3.1.2", |
| @@ -45,8 +45,9 @@ | ||
| 45 | 45 | "diff-match-patch": "^1.0.5", |
| 46 | 46 | "dompurify": "^3.2.6", |
| 47 | 47 | "droll": "^0.2.1", |
| 48 | + "env-paths": "^3.0.0", | |
| 48 | 49 | "express": "^4.21.0", |
| 49 | 50 | "form-data": "^4.0.34", |
| 50 | 51 | "fuse.js": "^7.1.0", |
| 51 | 52 | "google-translate-api-browser": "^3.0.1", |
| 52 | 53 | "google-translate-api-x": "^10.7.2", |
| @@ -67,7 +68,7 @@ | ||
| 67 | 68 | "multer": "^2.0.2", |
| 68 | 69 | "node-fetch": "^3.3.2", |
| 69 | 70 | "node-persist": "^4.0.4", |
| 70 | 71 | "open": "^10.1.2.0", |
| 71 | 72 | "png-chunk-text": "^1.0.0", |
| 72 | 73 | "png-chunks-extract": "^1.0.0", |
| 73 | 74 | "proxy-agent": "^6.5.0", |
| @@ -85,7 +86,7 @@ | ||
| 85 | 86 | "wavefile": "^11.0.0", |
| 86 | 87 | "webpack": "^5.98.0", |
| 87 | 88 | "write-file-atomic": "^5.0.1", |
| 88 | 89 | "ws": "^8.18.23", |
| 89 | 90 | "yaml": "^2.8.0", |
| 90 | 91 | "yargs": "^17.7.1", |
| 91 | 92 | "yauzl": "^3.2.0" |
| @@ -115,6 +116,7 @@ | ||
| 115 | 116 | "scripts": { |
| 116 | 117 | "start": "node server.js", |
| 117 | 118 | "debug": "node --inspect server.js", |
| 119 | + "start:global": "node server.js --global", | |
| 118 | 120 | "start:electron": "cd ./src/electron && npm run start", |
| 119 | 121 | "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js", |
| 120 | 122 | "start:bun": "bun server.js", |
| @@ -126,7 +128,7 @@ | ||
| 126 | 128 | "plugins:install": "node plugins install" |
| 127 | 129 | }, |
| 128 | 130 | "bin": { |
| 129 | 131 | "sillytavern": "./src/server-global.js" |
| 130 | 132 | }, |
| 131 | 133 | "rules": { |
| 132 | 134 | "no-path-concat": "off", |
| @@ -147,9 +149,9 @@ | ||
| 147 | 149 | "@types/jquery-cropper": "^1.0.4", |
| 148 | 150 | "@types/jquery.transit": "^0.9.33", |
| 149 | 151 | "@types/jqueryui": "^1.12.24", |
| 150 | 152 | "@types/lodash": "^4.17.1720", |
| 151 | 153 | "@types/mime-types": "^3.0.1", |
| 152 | 154 | "@types/multer": "^12.40.130", |
| 153 | 155 | "@types/node": "^18.19.84", |
| 154 | 156 | "@types/node-persist": "^3.1.8", |
| 155 | 157 | "@types/png-chunk-text": "^1.0.3", |
| @@ -2,6 +2,8 @@ | ||
| 2 | 2 | import { CommandLineParser } from './src/command-line.js'; |
| 3 | 3 | import { serverDirectory } from './src/server-directory.js'; |
| 4 | 4 | |
| 5 | +console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`); | |
| 6 | + | |
| 5 | 7 | // config.yaml will be set when parsing command line arguments |
| 6 | 8 | const cliArgs = new CommandLineParser().parse(process.argv); |
| 7 | 9 | globalThis.DATA_ROOT = cliArgs.dataRoot; |
| @@ -1,6 +1,9 @@ | ||
| 1 | +import fs from 'node:fs'; | |
| 2 | +import path from 'node:path'; | |
| 1 | 3 | import yargs from 'yargs/yargs'; |
| 2 | 4 | import { hideBin } from 'yargs/helpers'; |
| 3 | 5 | import ipRegex from 'ip-regex'; |
| 6 | +import envPaths from 'env-paths'; | |
| 4 | 7 | import { canResolve, color, getConfigValue, stringToBool } from './util.js'; |
| 5 | 8 | import { initConfig } from './config-init.js'; |
| 6 | 9 | |
| @@ -39,11 +42,18 @@ import { initConfig } from './config-init.js'; | ||
| 39 | 42 | * Provides a command line arguments parser. |
| 40 | 43 | */ |
| 41 | 44 | export class CommandLineParser { |
| 42 | - constructor() { | |
| 45 | + /** | |
| 43 | - /** @type {CommandLineArguments} */ | |
| 46 | + * Gets the default configuration values. | |
| 44 | - this.default = Object.freeze({ | |
| 47 | + * @param {boolean} isGlobal If the configuration is global or not | |
| 45 | - configPath: './config.yaml', | |
| 48 | + * @returns {CommandLineArguments} Default configuration values | |
| 46 | - dataRoot: './data', | |
| 49 | + */ | |
| 50 | + getDefaultConfig(isGlobal) { | |
| 51 | + const appPaths = envPaths('SillyTavern', { suffix: '' }); | |
| 52 | + const configPath = isGlobal ? path.join(appPaths.data, 'config.yaml') : './config.yaml'; | |
| 53 | + const dataPath = isGlobal ? path.join(appPaths.data, 'data') : './data'; | |
| 54 | + return Object.freeze({ | |
| 55 | + configPath: configPath, | |
| 56 | + dataRoot: dataPath, | |
| 47 | 57 | port: 8000, |
| 48 | 58 | listen: false, |
| 49 | 59 | listenAddressIPv6: '[::]', |
| @@ -78,7 +88,9 @@ export class CommandLineParser { | ||
| 78 | 88 | throw new Error('getBrowserLaunchUrl is not implemented'); |
| 79 | 89 | }, |
| 80 | 90 | }); |
| 91 | + } | |
| 81 | 92 | |
| 93 | + constructor() { | |
| 82 | 94 | this.booleanAutoOptions = [true, false, 'auto']; |
| 83 | 95 | } |
| 84 | 96 | |
| @@ -91,10 +103,15 @@ export class CommandLineParser { | ||
| 91 | 103 | parse(args) { |
| 92 | 104 | const cliArguments = yargs(hideBin(args)) |
| 93 | 105 | .usage('Usage: <your-start-script> [options]\nOptions that are not provided will be filled with config values.') |
| 106 | + .option('global', { | |
| 107 | + type: 'boolean', | |
| 108 | + default: null, | |
| 109 | + describe: 'Use global data and config paths instead of the server directory', | |
| 110 | + }) | |
| 94 | 111 | .option('configPath', { |
| 95 | 112 | type: 'string', |
| 96 | 113 | default: null, |
| 97 | 114 | describe: 'Path to the config file (only for standalone mode)', |
| 98 | 115 | }) |
| 99 | 116 | .option('enableIPv6', { |
| 100 | 117 | type: 'string', |
| @@ -184,7 +201,7 @@ export class CommandLineParser { | ||
| 184 | 201 | .option('dataRoot', { |
| 185 | 202 | type: 'string', |
| 186 | 203 | default: null, |
| 187 | 204 | describe: 'Root directory for data storage (only for standalone mode)', |
| 188 | 205 | }) |
| 189 | 206 | .option('basicAuthMode', { |
| 190 | 207 | type: 'boolean', |
| @@ -228,33 +245,57 @@ export class CommandLineParser { | ||
| 228 | 245 | }) |
| 229 | 246 | .parseSync(); |
| 230 | 247 | |
| 231 | 248 | const configPathisGlobal = cliArgumentsglobalThis.configPathFORCE_GLOBAL_MODE ?? this.defaultcliArguments.configPathglobal ?? false; |
| 249 | + const defaultConfig = this.getDefaultConfig(isGlobal); | |
| 250 | + | |
| 251 | + if (isGlobal && cliArguments.configPath) { | |
| 252 | + console.warn(color.yellow('Warning: "--configPath" argument is ignored in global mode')); | |
| 253 | + } | |
| 254 | + | |
| 255 | + if (isGlobal && cliArguments.dataRoot) { | |
| 256 | + console.warn(color.yellow('Warning: "--dataRoot" argument is ignored in global mode')); | |
| 257 | + } | |
| 258 | + | |
| 259 | + const configPath = isGlobal | |
| 260 | + ? defaultConfig.configPath | |
| 261 | + : (cliArguments.configPath ?? defaultConfig.configPath); | |
| 262 | + if (isGlobal && !fs.existsSync(path.dirname(configPath))) { | |
| 263 | + fs.mkdirSync(path.dirname(configPath), { recursive: true }); | |
| 264 | + } | |
| 232 | 265 | initConfig(configPath); |
| 266 | + | |
| 267 | + const dataRoot = isGlobal | |
| 268 | + ? defaultConfig.dataRoot | |
| 269 | + : (cliArguments.dataRoot ?? getConfigValue('dataRoot', defaultConfig.dataRoot)); | |
| 270 | + if (isGlobal && !fs.existsSync(dataRoot)) { | |
| 271 | + fs.mkdirSync(dataRoot, { recursive: true }); | |
| 272 | + } | |
| 273 | + | |
| 233 | 274 | /** @type {CommandLineArguments} */ |
| 234 | 275 | const result = { |
| 235 | 276 | configPath: configPath, |
| 236 | - dataRoot: cliArguments.dataRoot ?? getConfigValue('dataRoot', this.default.dataRoot), | |
| 277 | + dataRoot: dataRoot, | |
| 237 | 278 | port: cliArguments.port ?? getConfigValue('port', this.defaultdefaultConfig.port, 'number'), |
| 238 | 279 | listen: cliArguments.listen ?? getConfigValue('listen', this.defaultdefaultConfig.listen, 'boolean'), |
| 239 | 280 | listenAddressIPv6: cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', this.defaultdefaultConfig.listenAddressIPv6), |
| 240 | 281 | listenAddressIPv4: cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', this.defaultdefaultConfig.listenAddressIPv4), |
| 241 | 282 | enableIPv4: stringToBool(cliArguments.enableIPv4) ?? stringToBool(getConfigValue('protocol.ipv4', this.defaultdefaultConfig.enableIPv4)) ?? this.defaultdefaultConfig.enableIPv4, |
| 242 | 283 | enableIPv6: stringToBool(cliArguments.enableIPv6) ?? stringToBool(getConfigValue('protocol.ipv6', this.defaultdefaultConfig.enableIPv6)) ?? this.defaultdefaultConfig.enableIPv6, |
| 243 | 284 | dnsPreferIPv6: cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', this.defaultdefaultConfig.dnsPreferIPv6, 'boolean'), |
| 244 | 285 | browserLaunchEnabled: cliArguments.browserLaunchEnabled ?? cliArguments.autorun ?? getConfigValue('browserLaunch.enabled', this.defaultdefaultConfig.browserLaunchEnabled, 'boolean'), |
| 245 | 286 | browserLaunchHostname: cliArguments.browserLaunchHostname ?? cliArguments.autorunHostname ?? getConfigValue('browserLaunch.hostname', this.defaultdefaultConfig.browserLaunchHostname), |
| 246 | 287 | browserLaunchPort: cliArguments.browserLaunchPort ?? cliArguments.autorunPortOverride ?? getConfigValue('browserLaunch.port', this.defaultdefaultConfig.browserLaunchPort, 'number'), |
| 247 | 288 | browserLaunchAvoidLocalhost: cliArguments.browserLaunchAvoidLocalhost ?? cliArguments.avoidLocalhost ?? getConfigValue('browserLaunch.avoidLocalhost', this.defaultdefaultConfig.browserLaunchAvoidLocalhost, 'boolean'), |
| 248 | 289 | enableCorsProxy: cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', this.defaultdefaultConfig.enableCorsProxy, 'boolean'), |
| 249 | 290 | disableCsrf: cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', this.defaultdefaultConfig.disableCsrf, 'boolean'), |
| 250 | 291 | ssl: cliArguments.ssl ?? getConfigValue('ssl.enabled', this.defaultdefaultConfig.ssl, 'boolean'), |
| 251 | 292 | certPath: cliArguments.certPath ?? getConfigValue('ssl.certPath', this.defaultdefaultConfig.certPath), |
| 252 | 293 | keyPath: cliArguments.keyPath ?? getConfigValue('ssl.keyPath', this.defaultdefaultConfig.keyPath), |
| 253 | 294 | whitelistMode: cliArguments.whitelist ?? getConfigValue('whitelistMode', this.defaultdefaultConfig.whitelistMode, 'boolean'), |
| 254 | 295 | basicAuthMode: cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', this.defaultdefaultConfig.basicAuthMode, 'boolean'), |
| 255 | 296 | requestProxyEnabled: cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', this.defaultdefaultConfig.requestProxyEnabled, 'boolean'), |
| 256 | 297 | requestProxyUrl: cliArguments.requestProxyUrl ?? getConfigValue('requestProxy.url', this.defaultdefaultConfig.requestProxyUrl), |
| 257 | 298 | requestProxyBypass: cliArguments.requestProxyBypass ?? getConfigValue('requestProxy.bypass', this.defaultdefaultConfig.requestProxyBypass), |
| 258 | 299 | getIPv4ListenUrl: function () { |
| 259 | 300 | const isValid = ipRegex.v4({ exact: true }).test(this.listenAddressIPv4); |
| 260 | 301 | return new URL( |
| @@ -302,13 +343,13 @@ export class CommandLineParser { | ||
| 302 | 343 | }; |
| 303 | 344 | |
| 304 | 345 | if (!this.booleanAutoOptions.includes(result.enableIPv6)) { |
| 305 | 346 | console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', this.booleanAutoOptions, '\n setting to:', this.defaultdefaultConfig.enableIPv6); |
| 306 | 347 | result.enableIPv6 = this.defaultdefaultConfig.enableIPv6; |
| 307 | 348 | } |
| 308 | 349 | |
| 309 | 350 | if (!this.booleanAutoOptions.includes(result.enableIPv4)) { |
| 310 | 351 | console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', this.booleanAutoOptions, '\n setting to:', this.defaultdefaultConfig.enableIPv4); |
| 311 | 352 | result.enableIPv4 = this.defaultdefaultConfig.enableIPv4; |
| 312 | 353 | } |
| 313 | 354 | |
| 314 | 355 | return result; |
| @@ -148,6 +148,13 @@ function getAllKeys(obj, prefix = '') { | ||
| 148 | 148 | export function addMissingConfigValues(configPath) { |
| 149 | 149 | try { |
| 150 | 150 | const defaultConfig = yaml.parse(fs.readFileSync(path.join(serverDirectory, './default/config.yaml'), 'utf8')); |
| 151 | + | |
| 152 | + if (!fs.existsSync(configPath)) { | |
| 153 | + console.warn(color.yellow(`Warning: config.yaml not found at ${configPath}. Creating a new one with default values.`)); | |
| 154 | + fs.writeFileSync(configPath, yaml.stringify(defaultConfig)); | |
| 155 | + return; | |
| 156 | + } | |
| 157 | + | |
| 151 | 158 | let config = yaml.parse(fs.readFileSync(configPath, 'utf8')); |
| 152 | 159 | |
| 153 | 160 | // Migrate old keys to new keys |
| @@ -224,6 +231,7 @@ export function addMissingConfigValues(configPath) { | ||
| 224 | 231 | * @param {string} configPath Path to config.yaml |
| 225 | 232 | */ |
| 226 | 233 | export function initConfig(configPath) { |
| 234 | + console.log('Using config path:', color.green(configPath)); | |
| 227 | 235 | setConfigFilePath(configPath); |
| 228 | 236 | addMissingConfigValues(configPath); |
| 229 | 237 | } |
| @@ -41,7 +41,7 @@ function startServer() { | ||
| 41 | 41 | const sillyTavernRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); |
| 42 | 42 | process.chdir(sillyTavernRoot); |
| 43 | 43 | |
| 44 | 44 | import('../../server-global.js'); |
| 45 | 45 | }); |
| 46 | 46 | } |
| 47 | 47 | |
| @@ -0,0 +1,5 @@ | ||
| 1 | +#!/usr/bin/env node | |
| 2 | +globalThis.FORCE_GLOBAL_MODE = true; | |
| 3 | +await import('../server.js'); | |
| 4 | + | |
| 5 | +export {}; | |
| @@ -19,16 +19,6 @@ import bodyParser from 'body-parser'; | ||
| 19 | 19 | import './fetch-patch.js'; |
| 20 | 20 | import { serverDirectory } from './server-directory.js'; |
| 21 | 21 | |
| 22 | -console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`); | |
| 23 | - | |
| 24 | -// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. | |
| 25 | -// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 | |
| 26 | -// Safe to remove once support for Node v20 is dropped. | |
| 27 | -if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) { | |
| 28 | - // @ts-ignore | |
| 29 | - if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false); | |
| 30 | -} | |
| 31 | - | |
| 32 | 22 | import { serverEvents, EVENT_NAMES } from './server-events.js'; |
| 33 | 23 | import { loadPlugins } from './plugin-loader.js'; |
| 34 | 24 | import { |
| @@ -78,6 +68,14 @@ import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } fro | ||
| 78 | 68 | import { diskCache } from './endpoints/characters.js'; |
| 79 | 69 | import { migrateFlatSecrets } from './endpoints/secrets.js'; |
| 80 | 70 | |
| 71 | +// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. | |
| 72 | +// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 | |
| 73 | +// Safe to remove once support for Node v20 is dropped. | |
| 74 | +if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) { | |
| 75 | + // @ts-ignore | |
| 76 | + if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false); | |
| 77 | +} | |
| 78 | + | |
| 81 | 79 | // Unrestrict console logs display limit |
| 82 | 80 | util.inspect.defaultOptions.maxArrayLength = null; |
| 83 | 81 | util.inspect.defaultOptions.maxStringLength = null; |
| @@ -91,18 +89,6 @@ if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) { | ||
| 91 | 89 | process.exit(1); |
| 92 | 90 | } |
| 93 | 91 | |
| 94 | -try { | |
| 95 | - if (cliArgs.dnsPreferIPv6) { | |
| 96 | - dns.setDefaultResultOrder('ipv6first'); | |
| 97 | - console.log('Preferring IPv6 for DNS resolution'); | |
| 98 | - } else { | |
| 99 | - dns.setDefaultResultOrder('ipv4first'); | |
| 100 | - console.log('Preferring IPv4 for DNS resolution'); | |
| 101 | - } | |
| 102 | -} catch (error) { | |
| 103 | - console.warn('Failed to set DNS resolution order. Possibly unsupported in this Node version.'); | |
| 104 | -} | |
| 105 | - | |
| 106 | 92 | const app = express(); |
| 107 | 93 | app.use(helmet({ |
| 108 | 94 | contentSecurityPolicy: false, |
| @@ -400,8 +386,26 @@ function apply404Middleware() { | ||
| 400 | 386 | }); |
| 401 | 387 | } |
| 402 | 388 | |
| 389 | +/** | |
| 390 | + * Sets the DNS resolution order based on the command line arguments. | |
| 391 | + */ | |
| 392 | +function setDnsResolutionOrder() { | |
| 393 | + try { | |
| 394 | + if (cliArgs.dnsPreferIPv6) { | |
| 395 | + dns.setDefaultResultOrder('ipv6first'); | |
| 396 | + console.log('Preferring IPv6 for DNS resolution'); | |
| 397 | + } else { | |
| 398 | + dns.setDefaultResultOrder('ipv4first'); | |
| 399 | + console.log('Preferring IPv4 for DNS resolution'); | |
| 400 | + } | |
| 401 | + } catch (error) { | |
| 402 | + console.warn('Failed to set DNS resolution order. Possibly unsupported in this Node version.'); | |
| 403 | + } | |
| 404 | +} | |
| 405 | + | |
| 403 | 406 | // User storage module needs to be initialized before starting the server |
| 404 | 407 | initUserStorage(globalThis.DATA_ROOT) |
| 408 | + .then(setDnsResolutionOrder) | |
| 405 | 409 | .then(ensurePublicDirectoriesExist) |
| 406 | 410 | .then(migrateUserData) |
| 407 | 411 | .then(migrateSystemPrompts) |