Initial local project snapshot.

This commit is contained in:
yiulix
2026-06-15 11:42:58 +08:00
commit 1d1f7591db
19 changed files with 7312 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import fs from 'fs/promises';
import { existsSync } from 'fs';
import path from 'path';
import { defineConfig, Plugin } from 'vite';
const SAVES_DIR = path.resolve(__dirname, 'saves');
const ensureSavesDir = async () => {
await fs.mkdir(SAVES_DIR, { recursive: true });
};
const sanitizeSaveName = (name: string) => {
const cleaned = name
.replace(/\.json$/i, '')
.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_')
.trim();
return `${cleaned || `save-${Date.now()}`}.json`;
};
const readJsonBody = async (req: any) => {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const raw = Buffer.concat(chunks).toString('utf-8');
return raw ? JSON.parse(raw) : {};
};
const sendJson = (res: any, statusCode: number, payload: unknown) => {
res.statusCode = statusCode;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify(payload));
};
const localSavesPlugin = (): Plugin => ({
name: 'local-saves-api',
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (!req.url?.startsWith('/api/saves')) {
next();
return;
}
try {
await ensureSavesDir();
const url = new URL(req.url, 'http://localhost');
const fileNameFromPath = decodeURIComponent(url.pathname.replace(/^\/api\/saves\/?/, ''));
if (req.method === 'GET' && url.pathname === '/api/saves') {
const entries = await fs.readdir(SAVES_DIR);
const files = await Promise.all(
entries
.filter((entry) => entry.toLowerCase().endsWith('.json'))
.map(async (entry) => {
const filePath = path.join(SAVES_DIR, entry);
const stat = await fs.stat(filePath);
let lineCount: number | null = null;
let title = entry.replace(/\.json$/i, '');
try {
const parsed = JSON.parse(await fs.readFile(filePath, 'utf-8'));
lineCount = Array.isArray(parsed?.lines) ? parsed.lines.length : null;
title = typeof parsed?.title === 'string' && parsed.title.trim() ? parsed.title : title;
} catch {
lineCount = null;
}
return {
name: entry,
title,
savedAt: stat.mtime.toISOString(),
size: stat.size,
lineCount,
};
})
);
files.sort((a, b) => new Date(b.savedAt).getTime() - new Date(a.savedAt).getTime());
sendJson(res, 200, { files });
return;
}
if (req.method === 'GET' && fileNameFromPath) {
const fileName = sanitizeSaveName(fileNameFromPath);
const filePath = path.join(SAVES_DIR, fileName);
if (!existsSync(filePath)) {
sendJson(res, 404, { error: 'Save file not found' });
return;
}
const content = await fs.readFile(filePath, 'utf-8');
sendJson(res, 200, JSON.parse(content));
return;
}
if (req.method === 'POST' && url.pathname === '/api/saves') {
const body = await readJsonBody(req);
const fileName = sanitizeSaveName(String(body.name || 'current'));
const document = body.document;
if (!document || typeof document !== 'object') {
sendJson(res, 400, { error: 'Missing document payload' });
return;
}
const filePath = path.join(SAVES_DIR, fileName);
await fs.writeFile(filePath, JSON.stringify(document, null, 2), 'utf-8');
const stat = await fs.stat(filePath);
sendJson(res, 200, {
file: {
name: fileName,
savedAt: stat.mtime.toISOString(),
size: stat.size,
lineCount: Array.isArray(document.lines) ? document.lines.length : null,
},
});
return;
}
if (req.method === 'DELETE' && fileNameFromPath) {
const fileName = sanitizeSaveName(fileNameFromPath);
const filePath = path.join(SAVES_DIR, fileName);
if (!existsSync(filePath)) {
sendJson(res, 404, { error: 'Save file not found' });
return;
}
await fs.unlink(filePath);
sendJson(res, 200, { ok: true });
return;
}
sendJson(res, 405, { error: 'Unsupported saves API request' });
} catch (error) {
console.error(error);
sendJson(res, 500, { error: 'Local saves API failed' });
}
});
},
});
export default defineConfig(() => {
return {
plugins: [react(), tailwindcss(), localSavesPlugin()],
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
server: {
// HMR is disabled in AI Studio via DISABLE_HMR env var.
// Do not modify—file watching is disabled to prevent flickering during agent edits.
hmr: process.env.DISABLE_HMR !== 'true',
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
watch: process.env.DISABLE_HMR === 'true' ? null : {},
},
};
});