Immutable public and global content management (#5390) * Use custom init script instead of postinstall * Revert changes to start scripts in src\electron * Add global data to content manager * Add migration for public overrides and user.css location update * Update npm publish workflow to use 'omit=dev' flag in npm ci commands * Rename user.css readme file * Fix indentation in userCssMiddleware function * Add directory creation for content target * Restore template compile location * Move stylesheet up in index.json * Use path.resolve for user.css file path in userCssMiddleware * Correct capitalization in "Not Found" error page title and heading * Remove init run from startup scripts * Simplify user CSS file path resolution * Update userCssMiddleware comment

8e8f501279e51768fadd61ee9073336a45dc5965

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

Signed
20 files changed, +195 -141Ignore whitespace
.github/workflows/npm-publish.yml+2 -2
@@ -19,7 +19,7 @@ jobs:
1919 - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3
2020 with:
2121 node-version: 24
2222 - run: npm ci --omit=dev --ignore-scripts
2323
2424 publish-npm:
2525 needs: build
@@ -30,5 +30,5 @@ jobs:
3030 with:
3131 node-version: 24
3232 registry-url: https://registry.npmjs.org/
3333 - run: npm ci --omit=dev --ignore-scripts
3434 - run: npm publish
Start.bat+0 -1
@@ -2,7 +2,6 @@
22pushd %~dp0
33set NODE_ENV=production
44call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts
5-call npm run init
65node server.js %*
76pause
87popd
UpdateAndStart.bat+0 -1
@@ -21,7 +21,6 @@ if %errorlevel% neq 0 (
2121)
2222set NODE_ENV=production
2323call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts
24-call npm run init
2524node server.js %*
2625:end
2726pause
UpdateForkAndStart.bat+0 -1
@@ -103,7 +103,6 @@ if %errorlevel% neq 0 (
103103echo Installing npm packages and starting server
104104set NODE_ENV=production
105105call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts
106-call npm run init
107106node server.js %*
108107
109108:end
default/public/error/forbidden-by-whitelist.html → default/content/errors/forbidden-by-whitelist.html+0 -0
default/public/error/host-not-allowed.html → default/content/errors/host-not-allowed.html+0 -0
default/public/error/unauthorized.html → default/content/errors/unauthorized.html+0 -0
default/public/error/url-not-found.html → default/content/errors/url-not-found.html+2 -2
@@ -2,11 +2,11 @@
22<html>
33
44<head>
55 <title>Not foundFound</title>
66</head>
77
88<body>
99 <h1>Not foundFound</h1>
1010 <p>
1111 The requested URL was not found on this server.
1212 </p>
default/content/index.json+20 -0
@@ -654,5 +654,25 @@
654654 {
655655 "filename": "presets/context/Gemma 4.json",
656656 "type": "context"
657+ },
658+ {
659+ "filename": "user.css",
660+ "type": "stylesheet"
661+ },
662+ {
663+ "filename": "errors/forbidden-by-whitelist.html",
664+ "type": "error_page"
665+ },
666+ {
667+ "filename": "errors/host-not-allowed.html",
668+ "type": "error_page"
669+ },
670+ {
671+ "filename": "errors/unauthorized.html",
672+ "type": "error_page"
673+ },
674+ {
675+ "filename": "errors/url-not-found.html",
676+ "type": "error_page"
657677 }
658678]
default/public/css/user.css → default/content/user.css+0 -0
public/css/!USER-CSS-README.md+7 -0
@@ -0,0 +1,7 @@
1+# Looking for user.css?
2+
3+user.css is now located under your data root directory in the "_css" folder.
4+
5+Example for the default data root:
6+
7+/data/_css/user.css
src/endpoints/content-manager.js+93 -23
@@ -53,22 +53,45 @@ export const CONTENT_TYPES = {
5353 QUICK_REPLIES: 'quick_replies',
5454 SYSPROMPT: 'sysprompt',
5555 REASONING: 'reasoning',
56+ ERROR_PAGE: 'error_page',
57+ STYLESHEET: 'stylesheet',
5658};
5759
5860/**
61+ * @enum {string}
62+ */
63+export const CONTENT_SCOPE = {
64+ USER: 'user',
65+ GLOBAL: 'global',
66+};
67+
68+/**
69+ * Gets the scope of a content type.
70+ * @param {CONTENT_TYPES} type Content type
71+ * @returns {CONTENT_SCOPE} Resolved content scope
72+ */
73+function getScopeByType(type) {
74+ const globalTypes = [
75+ CONTENT_TYPES.ERROR_PAGE,
76+ CONTENT_TYPES.STYLESHEET,
77+ ];
78+ return globalTypes.includes(type) ? CONTENT_SCOPE.GLOBAL : CONTENT_SCOPE.USER;
79+}
80+
81+/**
5982 * Gets the default presets from the content directory.
6083 * @param {import('../users.js').UserDirectoryList} directories User directories
6184 * @returns {object[]} Array of default presets
6285 */
6386export function getDefaultPresets(directories) {
6487 try {
6588 const contentIndex = getContentIndex(CONTENT_SCOPE.USER);
6689 const presets = [];
6790
6891 for (const contentItem of contentIndex) {
6992 if (contentItem.type.endsWith('_preset') || ['instruct', 'context', 'sysprompt', 'reasoning'].includes(contentItem.type)) {
7093 contentItem.name = path.parse(contentItem.filename).name;
7194 contentItem.folder = getTargetByTypegetUserTargetByType(contentItem.type, directories);
7295 presets.push(contentItem);
7396 }
7497 }
@@ -102,24 +125,18 @@ export function getDefaultPresetFile(filename) {
102125}
103126
104127/**
105128 * Seeds content forfrom a usercontent index into a target location.
106129 * @param {ContentItem[]} contentIndex Content index
107- * @param {import('../users.js').UserDirectoryList} directories User directories
130+ * @param {string} contentLogPath Path to the content log file
108131 * @param {(type: string[]}) forceCategories=> Liststring of| categoriesnull} resolveTarget Function to forceresolve checkthe (eventarget ifdirectory contentfor checka iscontent skipped)type
109- * @returns {Promise<boolean>} Whether any content was added
132+ * @param {string[]} [forceCategories] List of categories to force check (even if content check is skipped)
133+ * @returns {boolean} Whether any content was added
110134 */
111135async function seedContentForUserseedContent(contentIndex, directoriescontentLogPath, resolveTarget, forceCategories) {
112136 let anyContentAdded = false;
113-
114- if (!fs.existsSync(directories.root)) {
115- fs.mkdirSync(directories.root, { recursive: true });
116- }
117-
118- const contentLogPath = path.join(directories.root, 'content.log');
119137 const contentLog = getContentLog(contentLogPath);
120138
121139 for (const contentItem of contentIndex) {
122- // If the content item is already in the log, skip it
123140 if (contentLog.includes(contentItem.filename) && !forceCategories?.includes(contentItem.type)) {
124141 continue;
125142 }
@@ -136,7 +153,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
136153 continue;
137154 }
138155
139156 const contentTarget = getTargetByTyperesolveTarget(contentItem.type, directories);
140157
141158 if (!contentTarget) {
142159 console.warn(`Content file ${contentItem.filename} has unknown type ${contentItem.type}`);
@@ -152,6 +169,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
152169 continue;
153170 }
154171
172+ fs.mkdirSync(contentTarget, { recursive: true });
155173 fs.cpSync(contentPath, targetPath, { recursive: true, force: false });
156174 setPermissionsSync(targetPath);
157175 console.info(`Content file ${contentItem.filename} copied to ${contentTarget}`);
@@ -163,6 +181,32 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
163181}
164182
165183/**
184+ * Seeds content for a user.
185+ * @param {ContentItem[]} contentIndex Content index
186+ * @param {import('../users.js').UserDirectoryList} directories User directories
187+ * @param {string[]} forceCategories List of categories to force check (even if content check is skipped)
188+ * @returns {Promise<boolean>} Whether any content was added
189+ */
190+async function seedContentForUser(contentIndex, directories, forceCategories) {
191+ if (!fs.existsSync(directories.root)) {
192+ fs.mkdirSync(directories.root, { recursive: true });
193+ }
194+
195+ const contentLogPath = path.join(directories.root, 'content.log');
196+ return seedContent(contentIndex, contentLogPath, (type) => getUserTargetByType(type, directories), forceCategories);
197+}
198+
199+/**
200+ * Seeds global content that is not user-specific, such as error pages.
201+ * @param {ContentItem[]} contentIndex Content index
202+ * @returns {Promise<boolean>} Whether any content was added
203+ */
204+async function seedGlobalContent(contentIndex) {
205+ const contentLogPath = path.join(globalThis.DATA_ROOT, 'content.log');
206+ return seedContent(contentIndex, contentLogPath, getGlobalTargetByType);
207+}
208+
209+/**
166210 * Checks for new content and seeds it for all users.
167211 * @param {import('../users.js').UserDirectoryList[]} directoriesList List of user directories
168212 * @param {string[]} forceCategories List of categories to force check (even if content check is skipped)
@@ -175,13 +219,19 @@ export async function checkForNewContent(directoriesList, forceCategories = [])
175219 return;
176220 }
177221
178222 const contentIndexuserContentIndex = getContentIndex(CONTENT_SCOPE.USER);
223+ const globalContentIndex = getContentIndex(CONTENT_SCOPE.GLOBAL);
179224 let anyContentAdded = false;
180225
226+ const globalSeedResult = await seedGlobalContent(globalContentIndex);
227+ if (globalSeedResult) {
228+ anyContentAdded = true;
229+ }
230+
181231 for (const directories of directoriesList) {
182232 const seedResultuserSeedResult = await seedContentForUser(contentIndexuserContentIndex, directories, forceCategories);
183233
184234 if (seedResultuserSeedResult) {
185235 anyContentAdded = true;
186236 }
187237 }
@@ -198,9 +248,10 @@ export async function checkForNewContent(directoriesList, forceCategories = [])
198248
199249/**
200250 * Gets combined content index from the content and scaffold directories.
251+ * @param {CONTENT_SCOPE} scope Scope of content to get
201252 * @returns {ContentItem[]} Array of content index
202253 */
203254function getContentIndex(scope = CONTENT_SCOPE.USER) {
204255 const result = [];
205256
206257 if (fs.existsSync(scaffoldIndexPath)) {
@@ -209,6 +260,7 @@ function getContentIndex() {
209260 if (Array.isArray(scaffoldIndex)) {
210261 scaffoldIndex.forEach((item) => {
211262 item.folder = scaffoldDirectory;
263+ item.scope = getScopeByType(item.type);
212264 });
213265 result.push(...scaffoldIndex);
214266 }
@@ -220,22 +272,24 @@ function getContentIndex() {
220272 if (Array.isArray(contentIndex)) {
221273 contentIndex.forEach((item) => {
222274 item.folder = contentDirectory;
275+ item.scope = getScopeByType(item.type);
223276 });
224277 result.push(...contentIndex);
225278 }
226279 }
227280
228- return result;
281+ return result.filter((item) => item.scope === scope);
229282}
230283
231284/**
232285 * Gets content by type and format.
233286 * @param {string} type Type of content
234287 * @param {'json'|'string'|'raw'} format Format of content
288+ * @param {CONTENT_SCOPE} scope Scope of content to get
235289 * @returns {string[]|Buffer[]} Array of content
236290 */
237291export function getContentOfType(type, format, scope = CONTENT_SCOPE.USER) {
238292 const contentIndex = getContentIndex(scope);
239293 const indexItems = contentIndex.filter((item) => item.type === type && item.folder);
240294 const files = [];
241295 for (const item of indexItems) {
@@ -269,7 +323,7 @@ export function getContentOfType(type, format) {
269323 * @param {import('../users.js').UserDirectoryList} directories User directories
270324 * @returns {string | null} Target directory
271325 */
272326export function getTargetByTypegetUserTargetByType(type, directories) {
273327 switch (type) {
274328 case CONTENT_TYPES.SETTINGS:
275329 return directories.root;
@@ -313,6 +367,22 @@ function getTargetByType(type, directories) {
313367}
314368
315369/**
370+ * Gets the target directory for global content types.
371+ * @param {CONTENT_TYPES} type Content type
372+ * @returns {string | null} Target directory
373+ */
374+export function getGlobalTargetByType(type) {
375+ switch (type) {
376+ case CONTENT_TYPES.ERROR_PAGE:
377+ return path.join(globalThis.DATA_ROOT, '_errors');
378+ case CONTENT_TYPES.STYLESHEET:
379+ return path.join(globalThis.DATA_ROOT, '_css');
380+ default:
381+ return null;
382+ }
383+}
384+
385+/**
316386 * Gets the content log from the content log file.
317387 * @param {string} contentLogPath Path to the content log file
318388 * @returns {string[]} Array of content log lines
src/middleware/basicAuth.js+2 -1
@@ -3,6 +3,7 @@
33 * allow access to the endpoint after successful authentication.
44 */
55import { Buffer } from 'node:buffer';
6+import path from 'node:path';
67import storage from 'node-persist';
78import { getAllUserHandles, toKey, getPasswordHash } from '../users.js';
89import { getConfigValue, safeReadFileSync } from '../util.js';
@@ -11,7 +12,7 @@ const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean')
1112const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean');
1213
1314const basicAuthMiddleware = async function (request, response, callback) {
1415 const unauthorizedWebpage = safeReadFileSync('path./public/error/join(globalThis.DATA_ROOT, '_errors', 'unauthorized.html')) ?? '';
1516 const unauthorizedResponse = (res) => {
1617 res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
1718 return res.status(401).send(unauthorizedWebpage);
src/middleware/hostWhitelist.js+1 -4
@@ -1,6 +1,5 @@
11import path from 'node:path';
22import { color, getConfigValue, safeReadFileSync } from '../util.js';
3-import { serverDirectory } from '../server-directory.js';
43import { isHostAllowed, hostValidationMiddleware } from 'host-validation-middleware';
54
65const knownHosts = new Set();
@@ -10,11 +9,9 @@ const hostWhitelistEnabled = !!getConfigValue('hostWhitelist.enabled', false);
109const hostWhitelist = Object.freeze(getConfigValue('hostWhitelist.hosts', []));
1110const hostWhitelistScan = !!getConfigValue('hostWhitelist.scan', false, 'boolean');
1211
13-const hostNotAllowedHtml = safeReadFileSync(path.join(serverDirectory, 'public/error/host-not-allowed.html'))?.toString() ?? '';
14-
1512const validationMiddleware = hostValidationMiddleware({
1613 allowedHosts: hostWhitelist,
17- generateErrorMessage: () => hostNotAllowedHtml,
14+ generateErrorMessage: () => safeReadFileSync(path.join(globalThis.DATA_ROOT, '_errors', 'host-not-allowed.html'))?.toString() ?? '',
1815 errorResponseContentType: 'text/html',
1916});
2017
src/middleware/userCss.js+19 -0
@@ -0,0 +1,19 @@
1+import path from 'node:path';
2+import fs from 'node:fs';
3+
4+/**
5+ * Provides an Express middleware function that serves a user-defined CSS file from the data directory if it exists.
6+ * @type {import('express').Handler}
7+ */
8+export function userCssMiddleware(req, res, next) {
9+ if (req.method === 'GET' && req.path === '/css/user.css') {
10+ const userCssPath = path.resolve(path.join(globalThis.DATA_ROOT, '_css', 'user.css'));
11+ if (fs.existsSync(userCssPath)) {
12+ res.sendFile(userCssPath);
13+ return;
14+ }
15+ }
16+ next();
17+}
18+
19+export default userCssMiddleware;
src/middleware/whitelist.js+1 -1
@@ -100,7 +100,7 @@ async function addDockerHostsToWhitelist() {
100100 */
101101export default async function getWhitelistMiddleware() {
102102 const forbiddenWebpage = Handlebars.compile(
103103 safeReadFileSync('path./public/error/join(globalThis.DATA_ROOT, '_errors', 'forbidden-by-whitelist.html')) ?? '',
104104 );
105105
106106 const noLogPaths = [
src/server-init.js+0 -102
@@ -1,113 +1,11 @@
11/**
22 * Scripts to be done before starting the server for the first time.
33 */
4-import fs from 'node:fs';
54import path from 'node:path';
65import process from 'node:process';
7-import yaml from 'yaml';
8-import chalk from 'chalk';
9-import { createRequire } from 'node:module';
106import { addMissingConfigValues } from './config-init.js';
117
12-/**
13- * Colorizes console output.
14- */
15-const color = chalk;
16-
17-/**
18- * Converts the old config.conf file to the new config.yaml format.
19- */
20-function convertConfig() {
21- if (fs.existsSync('./config.conf')) {
22- if (fs.existsSync('./config.yaml')) {
23- console.log(color.yellow('Both config.conf and config.yaml exist. Please delete config.conf manually.'));
24- return;
25- }
26-
27- try {
28- console.log(color.blue('Converting config.conf to config.yaml. Your old config.conf will be renamed to config.conf.bak'));
29- fs.renameSync('./config.conf', './config.conf.cjs'); // Force loading as CommonJS
30- const require = createRequire(import.meta.url);
31- const config = require(path.join(process.cwd(), './config.conf.cjs'));
32- fs.copyFileSync('./config.conf.cjs', './config.conf.bak');
33- fs.rmSync('./config.conf.cjs');
34- fs.writeFileSync('./config.yaml', yaml.stringify(config));
35- console.log(color.green('Conversion successful. Please check your config.yaml and fix it if necessary.'));
36- } catch (error) {
37- console.error(color.red('FATAL: Config conversion failed. Please check your config.conf file and try again.'), error);
38- return;
39- }
40- }
41-}
42-
43-/**
44- * Creates the default config files if they don't exist yet.
45- */
46-function createDefaultFiles() {
47- /**
48- * @typedef DefaultItem
49- * @type {object}
50- * @property {'file' | 'directory'} type - Whether the item should be copied as a single file or merged into a directory structure.
51- * @property {string} defaultPath - The path to the default item (typically in `default/`).
52- * @property {string} productionPath - The path to the copied item for production use.
53- */
54-
55- /** @type {DefaultItem[]} */
56- const defaultItems = [
57- {
58- type: 'file',
59- defaultPath: './default/config.yaml',
60- productionPath: './config.yaml',
61- },
62- {
63- type: 'directory',
64- defaultPath: './default/public/',
65- productionPath: './public/',
66- },
67- ];
68-
69- for (const defaultItem of defaultItems) {
70- try {
71- if (defaultItem.type === 'file') {
72- if (!fs.existsSync(defaultItem.productionPath)) {
73- fs.copyFileSync(
74- defaultItem.defaultPath,
75- defaultItem.productionPath,
76- );
77- console.log(
78- color.green(`Created default file: ${defaultItem.productionPath}`),
79- );
80- }
81- } else if (defaultItem.type === 'directory') {
82- fs.cpSync(defaultItem.defaultPath, defaultItem.productionPath, {
83- force: false, // Don't overwrite existing files!
84- recursive: true,
85- });
86- console.log(
87- color.green(`Synchronized missing files: ${defaultItem.productionPath}`),
88- );
89- } else {
90- throw new Error(
91- 'FATAL: Unexpected default file format in `server-init.js#createDefaultFiles()`.',
92- );
93- }
94- } catch (error) {
95- console.error(
96- color.red(
97- `FATAL: Could not write default ${defaultItem.type}: ${defaultItem.productionPath}`,
98- ),
99- error,
100- );
101- }
102- }
103-}
104-
1058try {
106- // 0. Convert config.conf to config.yaml
107- convertConfig();
108- // 1. Create default config files
109- createDefaultFiles();
110- // 2. Add missing config values
1119 addMissingConfigValues(path.join(process.cwd(), './config.yaml'));
11210} catch (error) {
11311 console.error(error);
src/server-main.js+5 -1
@@ -37,6 +37,7 @@ import {
3737 getSessionCookieAge,
3838 verifySecuritySettings,
3939 loginPageMiddleware,
40+ migratePublicOverrides,
4041} from './users.js';
4142
4243import getWebpackServeMiddleware from './middleware/webpack-serve.js';
@@ -48,6 +49,7 @@ import initRequestProxy from './request-proxy.js';
4849import cacheBuster from './middleware/cacheBuster.js';
4950import corsProxyMiddleware from './middleware/corsProxy.js';
5051import hostWhitelistMiddleware from './middleware/hostWhitelist.js';
52+import userCssMiddleware from './middleware/userCss.js';
5153import {
5254 getVersion,
5355 color,
@@ -229,6 +231,7 @@ app.get('/login', loginPageMiddleware);
229231// Host frontend assets
230232const webpackMiddleware = getWebpackServeMiddleware();
231233app.use(webpackMiddleware);
234+app.use(userCssMiddleware);
232235app.use(express.static(path.join(serverDirectory, 'public'), {}));
233236
234237// Public API
@@ -430,7 +433,7 @@ async function postSetupTasks(result) {
430433 * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
431434 */
432435function apply404Middleware() {
433436 const notFoundWebpage = safeReadFileSync(path.join(serverDirectoryglobalThis.DATA_ROOT, 'public/error/_errors', 'url-not-found.html')) ?? '';
434437 app.use((req, res) => {
435438 res.status(404).send(notFoundWebpage);
436439 });
@@ -459,6 +462,7 @@ initUserStorage(globalThis.DATA_ROOT)
459462 .then(ensurePublicDirectoriesExist)
460463 .then(migrateUserData)
461464 .then(migrateSystemPrompts)
465+ .then(migratePublicOverrides)
462466 .then(verifySecuritySettings)
463467 .then(preSetupTasks)
464468 .then(apply404Middleware)
src/users.js+43 -1
@@ -16,7 +16,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
1616import sanitize from 'sanitize-filename';
1717
1818import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js';
1919import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache, isPathUnderParent, setPermissionsSync } from './util.js';
2020import { allowKeysExposure, readSecret, writeSecret, SECRETS_FILE } from './endpoints/secrets.js';
2121import { getContentOfType } from './endpoints/content-manager.js';
2222import { serverDirectory } from './server-directory.js';
@@ -484,6 +484,48 @@ export async function migrateSystemPrompts() {
484484 }
485485}
486486
487+export async function migratePublicOverrides() {
488+ const migrationMap = [
489+ {
490+ oldPath: path.join(serverDirectory, 'public', 'error', 'forbidden-by-whitelist.html'),
491+ newPath: path.join(globalThis.DATA_ROOT, '_errors', 'forbidden-by-whitelist.html'),
492+ },
493+ {
494+ oldPath: path.join(serverDirectory, 'public', 'error', 'host-not-allowed.html'),
495+ newPath: path.join(globalThis.DATA_ROOT, '_errors', 'host-not-allowed.html'),
496+ },
497+ {
498+ oldPath: path.join(serverDirectory, 'public', 'error', 'unauthorized.html'),
499+ newPath: path.join(globalThis.DATA_ROOT, '_errors', 'unauthorized.html'),
500+ },
501+ {
502+ oldPath: path.join(serverDirectory, 'public', 'error', 'url-not-found.html'),
503+ newPath: path.join(globalThis.DATA_ROOT, '_errors', 'url-not-found.html'),
504+ },
505+ {
506+ oldPath: path.join(serverDirectory, 'public', 'css', 'user.css'),
507+ newPath: path.join(globalThis.DATA_ROOT, '_css', 'user.css'),
508+ },
509+ ];
510+
511+ for (const { oldPath, newPath } of migrationMap) {
512+ try {
513+ if (fs.existsSync(newPath)) {
514+ continue;
515+ }
516+ if (fs.existsSync(oldPath)) {
517+ fs.mkdirSync(path.dirname(newPath), { recursive: true });
518+ fs.cpSync(oldPath, newPath, { force: true });
519+ fs.unlinkSync(oldPath);
520+ setPermissionsSync(newPath);
521+ console.log(`Migrated ${path.basename(oldPath)} to data root.`);
522+ }
523+ } catch (error) {
524+ console.error(`Error migrating ${oldPath} to ${newPath}:`, error);
525+ }
526+ }
527+}
528+
487529/**
488530 * Converts a user handle to a storage key.
489531 * @param {string} handle User handle
start.sh+0 -1
@@ -11,7 +11,6 @@ fi
1111echo "Installing Node Modules..."
1212export NODE_ENV=production
1313npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts
14-npm run init
1514
1615echo "Entering SillyTavern..."
1716node "server.js" "$@"