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>

5d56992ea832657a148d948a33cf6a08cee34d4d

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
10 files changed, +236 -108Ignore whitespace
.github/readme.md+51 -18
@@ -330,12 +330,44 @@ chmod +x launcher.sh && ./launcher.sh
330> \[!NOTE]330> \[!NOTE]
331> **SillyTavern can be run natively on Android devices using Termux, but we do not provide official support for this use case.**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> **Please refer to this guide by ArroganceComplex#2659:**333> **Please refer to the documentation for more information:**
334>334>
335> * <https://rentry.org/STAI-Termux>335> * <https://docs.sillytavern.app/installation/android-(termux)/>
336336
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.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.
338338
339## Global / Standalone mode
340
341There 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
3671. Pass the `--global` argument to the server startup command (e.g. `node server.js --global`).
3682. Pass the `--global` argument to the shell startup script (e.g. `Start.bat --global` or `./start.sh --global`).
3693. Use the `start:global` script in the `package.json` file (e.g. `npm run start:global`).
370
339## Command-line arguments371## Command-line arguments
340372
341You can pass command-line arguments to SillyTavern server startup to override some settings in `config.yaml`.373You 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
357389
358| Option | Description | Type |390| Option | Description | Type |
359|---------------------------------|----------------------------------------------------------------------|----------|391|---------------------------------|----------------------------------------------------------------------|----------|
360| `--version` | Show version number | boolean |392| `--version` | Shows the version number | boolean |
361| `--configPath` | Override the path to the config.yaml file | string |393| `--global` | Forces the use of system-wide paths for application data (see above) | boolean |
362| `--dataRoot` | Root directory for data storage | string |394| `--configPath` | Overrides the path to the config.yaml file (standalone mode only) | string |
395| `--dataRoot` | Sets the root directory for data storage (standalone mode only) | string |
363| `--port` | Sets the port under which SillyTavern will run | number |396| `--port` | Sets the port under which SillyTavern will run | number |
364| `--listen` | SillyTavern will listen on all network interfaces | boolean |397| `--listen` | Makes SillyTavern listen on all network interfaces | boolean |
365| `--whitelist` | Enables whitelist mode | boolean |398| `--whitelist` | Enables whitelist mode | boolean |
366| `--basicAuthMode` | Enables basic authentication | boolean |399| `--basicAuthMode` | Enables basic authentication | boolean |
367| `--enableIPv4` | Enables IPv4 protocol | boolean |400| `--enableIPv4` | Enables the IPv4 protocol | boolean |
368| `--enableIPv6` | Enables IPv6 protocol | boolean |401| `--enableIPv6` | Enables the IPv6 protocol | boolean |
369| `--listenAddressIPv4` | Specific IPv4 address to listen to | string |402| `--listenAddressIPv4` | Specifies the IPv4 address to listen on | string |
370| `--listenAddressIPv6` | Specific IPv6 address to listen to | string |403| `--listenAddressIPv6` | Specifies the IPv6 address to listen on | string |
371| `--dnsPreferIPv6` | Prefers IPv6 for DNS | boolean |404| `--dnsPreferIPv6` | Prefers IPv6 for DNS | boolean |
372| `--ssl` | Enables SSL | boolean |405| `--ssl` | Enables SSL | boolean |
373| `--certPath` | Path to your certificate file | string |406| `--certPath` | Sets the path to your certificate file | string |
374| `--keyPath` | Path to your private key file | string |407| `--keyPath` | Sets the path to your private key file | string |
375| `--browserLaunchEnabled` | Automatically launch SillyTavern in the browser | boolean |408| `--browserLaunchEnabled` | Automatically launches SillyTavern in the browser | boolean |
376| `--browserLaunchHostname` | Browser launch hostname | string |409| `--browserLaunchHostname` | Sets the browser launch hostname | string |
377| `--browserLaunchPort` | Overrides the port for browser launch | string |410| `--browserLaunchPort` | Overrides the port for browser launch | string |
378| `--browserLaunchAvoidLocalhost` | Avoids using 'localhost' for browser launch in auto mode | boolean |411| `--browserLaunchAvoidLocalhost` | Avoids using 'localhost' for browser launch in auto mode | boolean |
379| `--corsProxy` | Enables CORS proxy | boolean |412| `--corsProxy` | Enables the CORS proxy | boolean |
380| `--requestProxyEnabled` | Enables a use of proxy for outgoing requests | boolean |413| `--requestProxyEnabled` | Enables the use of a proxy for outgoing requests | boolean |
381| `--requestProxyUrl` | Request proxy URL (HTTP or SOCKS protocols) | string |414| `--requestProxyUrl` | Sets the request proxy URL (HTTP or SOCKS protocols) | string |
382| `--requestProxyBypass` | Request proxy bypass list (space separated list of hosts) | array |415| `--requestProxyBypass` | Sets the request proxy bypass list (space-separated list of hosts) | array |
383| `--disableCsrf` | Disables CSRF protection (NOT RECOMMENDED) | boolean |416| `--disableCsrf` | Disables CSRF protection (NOT RECOMMENDED) | boolean |
384417
385## Remote connections418## Remote connections
index.d.ts+5 -0
@@ -66,4 +66,9 @@ declare global {
66 * Parsed command line arguments.66 * Parsed command line arguments.
67 */67 */
68 var COMMAND_LINE_ARGS: CommandLineArguments;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}
package-lock.json+54 -26
@@ -40,7 +40,7 @@
40 "@popperjs/core": "^2.11.8",40 "@popperjs/core": "^2.11.8",
41 "@zeldafan0225/ai_horde": "^5.2.0",41 "@zeldafan0225/ai_horde": "^5.2.0",
42 "archiver": "^7.0.1",42 "archiver": "^7.0.1",
43 "bing-translate-api": "^4.0.2",43 "bing-translate-api": "^4.1.0",
44 "body-parser": "^1.20.2",44 "body-parser": "^1.20.2",
45 "bowser": "^2.11.0",45 "bowser": "^2.11.0",
46 "bytes": "^3.1.2",46 "bytes": "^3.1.2",
@@ -55,8 +55,9 @@
55 "diff-match-patch": "^1.0.5",55 "diff-match-patch": "^1.0.5",
56 "dompurify": "^3.2.6",56 "dompurify": "^3.2.6",
57 "droll": "^0.2.1",57 "droll": "^0.2.1",
58 "env-paths": "^3.0.0",
58 "express": "^4.21.0",59 "express": "^4.21.0",
59 "form-data": "^4.0.3",60 "form-data": "^4.0.4",
60 "fuse.js": "^7.1.0",61 "fuse.js": "^7.1.0",
61 "google-translate-api-browser": "^3.0.1",62 "google-translate-api-browser": "^3.0.1",
62 "google-translate-api-x": "^10.7.2",63 "google-translate-api-x": "^10.7.2",
@@ -77,7 +78,7 @@
77 "multer": "^2.0.2",78 "multer": "^2.0.2",
78 "node-fetch": "^3.3.2",79 "node-fetch": "^3.3.2",
79 "node-persist": "^4.0.4",80 "node-persist": "^4.0.4",
80 "open": "^10.1.2",81 "open": "^10.2.0",
81 "png-chunk-text": "^1.0.0",82 "png-chunk-text": "^1.0.0",
82 "png-chunks-extract": "^1.0.0",83 "png-chunks-extract": "^1.0.0",
83 "proxy-agent": "^6.5.0",84 "proxy-agent": "^6.5.0",
@@ -95,13 +96,13 @@
95 "wavefile": "^11.0.0",96 "wavefile": "^11.0.0",
96 "webpack": "^5.98.0",97 "webpack": "^5.98.0",
97 "write-file-atomic": "^5.0.1",98 "write-file-atomic": "^5.0.1",
98 "ws": "^8.18.2",99 "ws": "^8.18.3",
99 "yaml": "^2.8.0",100 "yaml": "^2.8.0",
100 "yargs": "^17.7.1",101 "yargs": "^17.7.1",
101 "yauzl": "^3.2.0"102 "yauzl": "^3.2.0"
102 },103 },
103 "bin": {104 "bin": {
104 "sillytavern": "server.js"105 "sillytavern": "src/server-global.js"
105 },106 },
106 "devDependencies": {107 "devDependencies": {
107 "@types/archiver": "^6.0.3",108 "@types/archiver": "^6.0.3",
@@ -117,9 +118,9 @@
117 "@types/jquery-cropper": "^1.0.4",118 "@types/jquery-cropper": "^1.0.4",
118 "@types/jquery.transit": "^0.9.33",119 "@types/jquery.transit": "^0.9.33",
119 "@types/jqueryui": "^1.12.24",120 "@types/jqueryui": "^1.12.24",
120 "@types/lodash": "^4.17.17",121 "@types/lodash": "^4.17.20",
121 "@types/mime-types": "^3.0.1",122 "@types/mime-types": "^3.0.1",
122 "@types/multer": "^1.4.13",123 "@types/multer": "^2.0.0",
123 "@types/node": "^18.19.84",124 "@types/node": "^18.19.84",
124 "@types/node-persist": "^3.1.8",125 "@types/node-persist": "^3.1.8",
125 "@types/png-chunk-text": "^1.0.3",126 "@types/png-chunk-text": "^1.0.3",
@@ -1972,9 +1973,9 @@
1972 }1973 }
1973 },1974 },
1974 "node_modules/@types/lodash": {1975 "node_modules/@types/lodash": {
1975 "version": "4.17.17",1976 "version": "4.17.20",
1976 "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz",1977 "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
1977 "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==",1978 "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
1978 "dev": true,1979 "dev": true,
1979 "license": "MIT"1980 "license": "MIT"
1980 },1981 },
@@ -1999,9 +2000,9 @@
1999 "license": "MIT"2000 "license": "MIT"
2000 },2001 },
2001 "node_modules/@types/multer": {2002 "node_modules/@types/multer": {
2002 "version": "1.4.13",2003 "version": "2.0.0",
2003 "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz",2004 "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
2004 "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==",2005 "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==",
2005 "dev": true,2006 "dev": true,
2006 "license": "MIT",2007 "license": "MIT",
2007 "dependencies": {2008 "dependencies": {
@@ -2832,9 +2833,9 @@
2832 }2833 }
2833 },2834 },
2834 "node_modules/bing-translate-api": {2835 "node_modules/bing-translate-api": {
2835 "version": "4.0.2",2836 "version": "4.1.0",
2836 "resolved": "https://registry.npmjs.org/bing-translate-api/-/bing-translate-api-4.0.2.tgz",2837 "resolved": "https://registry.npmjs.org/bing-translate-api/-/bing-translate-api-4.1.0.tgz",
2837 "integrity": "sha512-JJ8XUehnxzOhHU91oy86xEtp8OOMjVEjCZJX042fKxoO19NNvxJ5omeCcxQNFoPbDqVpBJwqiGVquL0oPdQm1Q==",2838 "integrity": "sha512-oP2663Yd5MXX4kbB/3LdS9YgPiE+ls9+2iFZH2ZXigWhWyHT3R4m6aCup4TNJd3/U4gqHHnQoxTaIW7uOf4+vA==",
2838 "license": "MIT",2839 "license": "MIT",
2839 "dependencies": {2840 "dependencies": {
2840 "got": "^11.8.6"2841 "got": "^11.8.6"
@@ -4087,6 +4088,18 @@
4087 "url": "https://github.com/fb55/entities?sponsor=1"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 "node_modules/es-define-property": {4103 "node_modules/es-define-property": {
4091 "version": "1.0.1",4104 "version": "1.0.1",
4092 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",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 "node_modules/form-data": {4801 "node_modules/form-data": {
4789 "version": "4.0.3",4802 "version": "4.0.4",
4790 "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",4803 "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
4791 "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",4804 "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
4792 "license": "MIT",4805 "license": "MIT",
4793 "dependencies": {4806 "dependencies": {
4794 "asynckit": "^0.4.0",4807 "asynckit": "^0.4.0",
@@ -6420,15 +6433,15 @@
6420 }6433 }
6421 },6434 },
6422 "node_modules/open": {6435 "node_modules/open": {
6423 "version": "10.1.2",6436 "version": "10.2.0",
6424 "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz",6437 "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
6425 "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==",6438 "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
6426 "license": "MIT",6439 "license": "MIT",
6427 "dependencies": {6440 "dependencies": {
6428 "default-browser": "^5.2.1",6441 "default-browser": "^5.2.1",
6429 "define-lazy-prop": "^3.0.0",6442 "define-lazy-prop": "^3.0.0",
6430 "is-inside-container": "^1.0.0",6443 "is-inside-container": "^1.0.0",
6431 "is-wsl": "^3.1.0"6444 "wsl-utils": "^0.1.0"
6432 },6445 },
6433 "engines": {6446 "engines": {
6434 "node": ">=18"6447 "node": ">=18"
@@ -8503,9 +8516,9 @@
8503 }8516 }
8504 },8517 },
8505 "node_modules/ws": {8518 "node_modules/ws": {
8506 "version": "8.18.2",8519 "version": "8.18.3",
8507 "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",8520 "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
8508 "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",8521 "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
8509 "license": "MIT",8522 "license": "MIT",
8510 "engines": {8523 "engines": {
8511 "node": ">=10.0.0"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 "node_modules/xhr": {8554 "node_modules/xhr": {
8527 "version": "2.6.0",8555 "version": "2.6.0",
8528 "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz",8556 "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz",
package.json+9 -7
@@ -30,7 +30,7 @@
30 "@popperjs/core": "^2.11.8",30 "@popperjs/core": "^2.11.8",
31 "@zeldafan0225/ai_horde": "^5.2.0",31 "@zeldafan0225/ai_horde": "^5.2.0",
32 "archiver": "^7.0.1",32 "archiver": "^7.0.1",
33 "bing-translate-api": "^4.0.2",33 "bing-translate-api": "^4.1.0",
34 "body-parser": "^1.20.2",34 "body-parser": "^1.20.2",
35 "bowser": "^2.11.0",35 "bowser": "^2.11.0",
36 "bytes": "^3.1.2",36 "bytes": "^3.1.2",
@@ -45,8 +45,9 @@
45 "diff-match-patch": "^1.0.5",45 "diff-match-patch": "^1.0.5",
46 "dompurify": "^3.2.6",46 "dompurify": "^3.2.6",
47 "droll": "^0.2.1",47 "droll": "^0.2.1",
48 "env-paths": "^3.0.0",
48 "express": "^4.21.0",49 "express": "^4.21.0",
49 "form-data": "^4.0.3",50 "form-data": "^4.0.4",
50 "fuse.js": "^7.1.0",51 "fuse.js": "^7.1.0",
51 "google-translate-api-browser": "^3.0.1",52 "google-translate-api-browser": "^3.0.1",
52 "google-translate-api-x": "^10.7.2",53 "google-translate-api-x": "^10.7.2",
@@ -67,7 +68,7 @@
67 "multer": "^2.0.2",68 "multer": "^2.0.2",
68 "node-fetch": "^3.3.2",69 "node-fetch": "^3.3.2",
69 "node-persist": "^4.0.4",70 "node-persist": "^4.0.4",
70 "open": "^10.1.2",71 "open": "^10.2.0",
71 "png-chunk-text": "^1.0.0",72 "png-chunk-text": "^1.0.0",
72 "png-chunks-extract": "^1.0.0",73 "png-chunks-extract": "^1.0.0",
73 "proxy-agent": "^6.5.0",74 "proxy-agent": "^6.5.0",
@@ -85,7 +86,7 @@
85 "wavefile": "^11.0.0",86 "wavefile": "^11.0.0",
86 "webpack": "^5.98.0",87 "webpack": "^5.98.0",
87 "write-file-atomic": "^5.0.1",88 "write-file-atomic": "^5.0.1",
88 "ws": "^8.18.2",89 "ws": "^8.18.3",
89 "yaml": "^2.8.0",90 "yaml": "^2.8.0",
90 "yargs": "^17.7.1",91 "yargs": "^17.7.1",
91 "yauzl": "^3.2.0"92 "yauzl": "^3.2.0"
@@ -115,6 +116,7 @@
115 "scripts": {116 "scripts": {
116 "start": "node server.js",117 "start": "node server.js",
117 "debug": "node --inspect server.js",118 "debug": "node --inspect server.js",
119 "start:global": "node server.js --global",
118 "start:electron": "cd ./src/electron && npm run start",120 "start:electron": "cd ./src/electron && npm run start",
119 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",121 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
120 "start:bun": "bun server.js",122 "start:bun": "bun server.js",
@@ -126,7 +128,7 @@
126 "plugins:install": "node plugins install"128 "plugins:install": "node plugins install"
127 },129 },
128 "bin": {130 "bin": {
129 "sillytavern": "./server.js"131 "sillytavern": "./src/server-global.js"
130 },132 },
131 "rules": {133 "rules": {
132 "no-path-concat": "off",134 "no-path-concat": "off",
@@ -147,9 +149,9 @@
147 "@types/jquery-cropper": "^1.0.4",149 "@types/jquery-cropper": "^1.0.4",
148 "@types/jquery.transit": "^0.9.33",150 "@types/jquery.transit": "^0.9.33",
149 "@types/jqueryui": "^1.12.24",151 "@types/jqueryui": "^1.12.24",
150 "@types/lodash": "^4.17.17",152 "@types/lodash": "^4.17.20",
151 "@types/mime-types": "^3.0.1",153 "@types/mime-types": "^3.0.1",
152 "@types/multer": "^1.4.13",154 "@types/multer": "^2.0.0",
153 "@types/node": "^18.19.84",155 "@types/node": "^18.19.84",
154 "@types/node-persist": "^3.1.8",156 "@types/node-persist": "^3.1.8",
155 "@types/png-chunk-text": "^1.0.3",157 "@types/png-chunk-text": "^1.0.3",
server.js+2 -0
@@ -2,6 +2,8 @@
2import { CommandLineParser } from './src/command-line.js';2import { CommandLineParser } from './src/command-line.js';
3import { serverDirectory } from './src/server-directory.js';3import { serverDirectory } from './src/server-directory.js';
44
5console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`);
6
5// config.yaml will be set when parsing command line arguments7// config.yaml will be set when parsing command line arguments
6const cliArgs = new CommandLineParser().parse(process.argv);8const cliArgs = new CommandLineParser().parse(process.argv);
7globalThis.DATA_ROOT = cliArgs.dataRoot;9globalThis.DATA_ROOT = cliArgs.dataRoot;
src/command-line.js+75 -34
@@ -1,6 +1,9 @@
1import fs from 'node:fs';
2import path from 'node:path';
1import yargs from 'yargs/yargs';3import yargs from 'yargs/yargs';
2import { hideBin } from 'yargs/helpers';4import { hideBin } from 'yargs/helpers';
3import ipRegex from 'ip-regex';5import ipRegex from 'ip-regex';
6import envPaths from 'env-paths';
4import { canResolve, color, getConfigValue, stringToBool } from './util.js';7import { canResolve, color, getConfigValue, stringToBool } from './util.js';
5import { initConfig } from './config-init.js';8import { initConfig } from './config-init.js';
69
@@ -39,11 +42,18 @@ import { initConfig } from './config-init.js';
39 * Provides a command line arguments parser.42 * Provides a command line arguments parser.
40 */43 */
41export class CommandLineParser {44export 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 port: 8000,57 port: 8000,
48 listen: false,58 listen: false,
49 listenAddressIPv6: '[::]',59 listenAddressIPv6: '[::]',
@@ -78,7 +88,9 @@ export class CommandLineParser {
78 throw new Error('getBrowserLaunchUrl is not implemented');88 throw new Error('getBrowserLaunchUrl is not implemented');
79 },89 },
80 });90 });
91 }
8192
93 constructor() {
82 this.booleanAutoOptions = [true, false, 'auto'];94 this.booleanAutoOptions = [true, false, 'auto'];
83 }95 }
8496
@@ -91,10 +103,15 @@ export class CommandLineParser {
91 parse(args) {103 parse(args) {
92 const cliArguments = yargs(hideBin(args))104 const cliArguments = yargs(hideBin(args))
93 .usage('Usage: <your-start-script> [options]\nOptions that are not provided will be filled with config values.')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 .option('configPath', {111 .option('configPath', {
95 type: 'string',112 type: 'string',
96 default: null,113 default: null,
97 describe: 'Path to the config file',114 describe: 'Path to the config file (only for standalone mode)',
98 })115 })
99 .option('enableIPv6', {116 .option('enableIPv6', {
100 type: 'string',117 type: 'string',
@@ -184,7 +201,7 @@ export class CommandLineParser {
184 .option('dataRoot', {201 .option('dataRoot', {
185 type: 'string',202 type: 'string',
186 default: null,203 default: null,
187 describe: 'Root directory for data storage',204 describe: 'Root directory for data storage (only for standalone mode)',
188 })205 })
189 .option('basicAuthMode', {206 .option('basicAuthMode', {
190 type: 'boolean',207 type: 'boolean',
@@ -228,33 +245,57 @@ export class CommandLineParser {
228 })245 })
229 .parseSync();246 .parseSync();
230247
231 const configPath = cliArguments.configPath ?? this.default.configPath;248 const isGlobal = globalThis.FORCE_GLOBAL_MODE ?? cliArguments.global ?? 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 initConfig(configPath);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 /** @type {CommandLineArguments} */274 /** @type {CommandLineArguments} */
234 const result = {275 const result = {
235 configPath: configPath,276 configPath: configPath,
236 dataRoot: cliArguments.dataRoot ?? getConfigValue('dataRoot', this.default.dataRoot),277 dataRoot: dataRoot,
237 port: cliArguments.port ?? getConfigValue('port', this.default.port, 'number'),278 port: cliArguments.port ?? getConfigValue('port', defaultConfig.port, 'number'),
238 listen: cliArguments.listen ?? getConfigValue('listen', this.default.listen, 'boolean'),279 listen: cliArguments.listen ?? getConfigValue('listen', defaultConfig.listen, 'boolean'),
239 listenAddressIPv6: cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', this.default.listenAddressIPv6),280 listenAddressIPv6: cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', defaultConfig.listenAddressIPv6),
240 listenAddressIPv4: cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', this.default.listenAddressIPv4),281 listenAddressIPv4: cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', defaultConfig.listenAddressIPv4),
241 enableIPv4: stringToBool(cliArguments.enableIPv4) ?? stringToBool(getConfigValue('protocol.ipv4', this.default.enableIPv4)) ?? this.default.enableIPv4,282 enableIPv4: stringToBool(cliArguments.enableIPv4) ?? stringToBool(getConfigValue('protocol.ipv4', defaultConfig.enableIPv4)) ?? defaultConfig.enableIPv4,
242 enableIPv6: stringToBool(cliArguments.enableIPv6) ?? stringToBool(getConfigValue('protocol.ipv6', this.default.enableIPv6)) ?? this.default.enableIPv6,283 enableIPv6: stringToBool(cliArguments.enableIPv6) ?? stringToBool(getConfigValue('protocol.ipv6', defaultConfig.enableIPv6)) ?? defaultConfig.enableIPv6,
243 dnsPreferIPv6: cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', this.default.dnsPreferIPv6, 'boolean'),284 dnsPreferIPv6: cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', defaultConfig.dnsPreferIPv6, 'boolean'),
244 browserLaunchEnabled: cliArguments.browserLaunchEnabled ?? cliArguments.autorun ?? getConfigValue('browserLaunch.enabled', this.default.browserLaunchEnabled, 'boolean'),285 browserLaunchEnabled: cliArguments.browserLaunchEnabled ?? cliArguments.autorun ?? getConfigValue('browserLaunch.enabled', defaultConfig.browserLaunchEnabled, 'boolean'),
245 browserLaunchHostname: cliArguments.browserLaunchHostname ?? cliArguments.autorunHostname ?? getConfigValue('browserLaunch.hostname', this.default.browserLaunchHostname),286 browserLaunchHostname: cliArguments.browserLaunchHostname ?? cliArguments.autorunHostname ?? getConfigValue('browserLaunch.hostname', defaultConfig.browserLaunchHostname),
246 browserLaunchPort: cliArguments.browserLaunchPort ?? cliArguments.autorunPortOverride ?? getConfigValue('browserLaunch.port', this.default.browserLaunchPort, 'number'),287 browserLaunchPort: cliArguments.browserLaunchPort ?? cliArguments.autorunPortOverride ?? getConfigValue('browserLaunch.port', defaultConfig.browserLaunchPort, 'number'),
247 browserLaunchAvoidLocalhost: cliArguments.browserLaunchAvoidLocalhost ?? cliArguments.avoidLocalhost ?? getConfigValue('browserLaunch.avoidLocalhost', this.default.browserLaunchAvoidLocalhost, 'boolean'),288 browserLaunchAvoidLocalhost: cliArguments.browserLaunchAvoidLocalhost ?? cliArguments.avoidLocalhost ?? getConfigValue('browserLaunch.avoidLocalhost', defaultConfig.browserLaunchAvoidLocalhost, 'boolean'),
248 enableCorsProxy: cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', this.default.enableCorsProxy, 'boolean'),289 enableCorsProxy: cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', defaultConfig.enableCorsProxy, 'boolean'),
249 disableCsrf: cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', this.default.disableCsrf, 'boolean'),290 disableCsrf: cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', defaultConfig.disableCsrf, 'boolean'),
250 ssl: cliArguments.ssl ?? getConfigValue('ssl.enabled', this.default.ssl, 'boolean'),291 ssl: cliArguments.ssl ?? getConfigValue('ssl.enabled', defaultConfig.ssl, 'boolean'),
251 certPath: cliArguments.certPath ?? getConfigValue('ssl.certPath', this.default.certPath),292 certPath: cliArguments.certPath ?? getConfigValue('ssl.certPath', defaultConfig.certPath),
252 keyPath: cliArguments.keyPath ?? getConfigValue('ssl.keyPath', this.default.keyPath),293 keyPath: cliArguments.keyPath ?? getConfigValue('ssl.keyPath', defaultConfig.keyPath),
253 whitelistMode: cliArguments.whitelist ?? getConfigValue('whitelistMode', this.default.whitelistMode, 'boolean'),294 whitelistMode: cliArguments.whitelist ?? getConfigValue('whitelistMode', defaultConfig.whitelistMode, 'boolean'),
254 basicAuthMode: cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', this.default.basicAuthMode, 'boolean'),295 basicAuthMode: cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', defaultConfig.basicAuthMode, 'boolean'),
255 requestProxyEnabled: cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', this.default.requestProxyEnabled, 'boolean'),296 requestProxyEnabled: cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', defaultConfig.requestProxyEnabled, 'boolean'),
256 requestProxyUrl: cliArguments.requestProxyUrl ?? getConfigValue('requestProxy.url', this.default.requestProxyUrl),297 requestProxyUrl: cliArguments.requestProxyUrl ?? getConfigValue('requestProxy.url', defaultConfig.requestProxyUrl),
257 requestProxyBypass: cliArguments.requestProxyBypass ?? getConfigValue('requestProxy.bypass', this.default.requestProxyBypass),298 requestProxyBypass: cliArguments.requestProxyBypass ?? getConfigValue('requestProxy.bypass', defaultConfig.requestProxyBypass),
258 getIPv4ListenUrl: function () {299 getIPv4ListenUrl: function () {
259 const isValid = ipRegex.v4({ exact: true }).test(this.listenAddressIPv4);300 const isValid = ipRegex.v4({ exact: true }).test(this.listenAddressIPv4);
260 return new URL(301 return new URL(
@@ -302,13 +343,13 @@ export class CommandLineParser {
302 };343 };
303344
304 if (!this.booleanAutoOptions.includes(result.enableIPv6)) {345 if (!this.booleanAutoOptions.includes(result.enableIPv6)) {
305 console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', this.booleanAutoOptions, '\n setting to:', this.default.enableIPv6);346 console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', this.booleanAutoOptions, '\n setting to:', defaultConfig.enableIPv6);
306 result.enableIPv6 = this.default.enableIPv6;347 result.enableIPv6 = defaultConfig.enableIPv6;
307 }348 }
308349
309 if (!this.booleanAutoOptions.includes(result.enableIPv4)) {350 if (!this.booleanAutoOptions.includes(result.enableIPv4)) {
310 console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', this.booleanAutoOptions, '\n setting to:', this.default.enableIPv4);351 console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', this.booleanAutoOptions, '\n setting to:', defaultConfig.enableIPv4);
311 result.enableIPv4 = this.default.enableIPv4;352 result.enableIPv4 = defaultConfig.enableIPv4;
312 }353 }
313354
314 return result;355 return result;
src/config-init.js+8 -0
@@ -148,6 +148,13 @@ function getAllKeys(obj, prefix = '') {
148export function addMissingConfigValues(configPath) {148export function addMissingConfigValues(configPath) {
149 try {149 try {
150 const defaultConfig = yaml.parse(fs.readFileSync(path.join(serverDirectory, './default/config.yaml'), 'utf8'));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 let config = yaml.parse(fs.readFileSync(configPath, 'utf8'));158 let config = yaml.parse(fs.readFileSync(configPath, 'utf8'));
152159
153 // Migrate old keys to new keys160 // Migrate old keys to new keys
@@ -224,6 +231,7 @@ export function addMissingConfigValues(configPath) {
224 * @param {string} configPath Path to config.yaml231 * @param {string} configPath Path to config.yaml
225 */232 */
226export function initConfig(configPath) {233export function initConfig(configPath) {
234 console.log('Using config path:', color.green(configPath));
227 setConfigFilePath(configPath);235 setConfigFilePath(configPath);
228 addMissingConfigValues(configPath);236 addMissingConfigValues(configPath);
229}237}
src/electron/index.js+1 -1
@@ -41,7 +41,7 @@ function startServer() {
41 const sillyTavernRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');41 const sillyTavernRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
42 process.chdir(sillyTavernRoot);42 process.chdir(sillyTavernRoot);
4343
44 import('../../server.js');44 import('../server-global.js');
45 });45 });
46}46}
4747
src/server-global.js+5 -0
@@ -0,0 +1,5 @@
1#!/usr/bin/env node
2globalThis.FORCE_GLOBAL_MODE = true;
3await import('../server.js');
4
5export {};
src/server-main.js+26 -22
@@ -19,16 +19,6 @@ import bodyParser from 'body-parser';
19import './fetch-patch.js';19import './fetch-patch.js';
20import { serverDirectory } from './server-directory.js';20import { serverDirectory } from './server-directory.js';
2121
22console.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.
27if (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
32import { serverEvents, EVENT_NAMES } from './server-events.js';22import { serverEvents, EVENT_NAMES } from './server-events.js';
33import { loadPlugins } from './plugin-loader.js';23import { loadPlugins } from './plugin-loader.js';
34import {24import {
@@ -78,6 +68,14 @@ import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } fro
78import { diskCache } from './endpoints/characters.js';68import { diskCache } from './endpoints/characters.js';
79import { migrateFlatSecrets } from './endpoints/secrets.js';69import { migrateFlatSecrets } from './endpoints/secrets.js';
8070
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.
74if (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// Unrestrict console logs display limit79// Unrestrict console logs display limit
82util.inspect.defaultOptions.maxArrayLength = null;80util.inspect.defaultOptions.maxArrayLength = null;
83util.inspect.defaultOptions.maxStringLength = null;81util.inspect.defaultOptions.maxStringLength = null;
@@ -91,18 +89,6 @@ if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) {
91 process.exit(1);89 process.exit(1);
92}90}
9391
94try {
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
106const app = express();92const app = express();
107app.use(helmet({93app.use(helmet({
108 contentSecurityPolicy: false,94 contentSecurityPolicy: false,
@@ -400,8 +386,26 @@ function apply404Middleware() {
400 });386 });
401}387}
402388
389/**
390 * Sets the DNS resolution order based on the command line arguments.
391 */
392function 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// User storage module needs to be initialized before starting the server406// User storage module needs to be initialized before starting the server
404initUserStorage(globalThis.DATA_ROOT)407initUserStorage(globalThis.DATA_ROOT)
408 .then(setDnsResolutionOrder)
405 .then(ensurePublicDirectoriesExist)409 .then(ensurePublicDirectoriesExist)
406 .then(migrateUserData)410 .then(migrateUserData)
407 .then(migrateSystemPrompts)411 .then(migrateSystemPrompts)