import fs from "fs/promises";
import path from "path";
import { validateProjectPath } from "../utils/validation.js";
import { Errors } from "../utils/errors.js";
import { HandlerResponse } from "../types/index.js";
import { withBackup, cleanupOldBackups } from "../utils/backup.js";
import { Logger } from "../utils/logger.js";
type ProjectArgs = { projectPath: string };
type WritePluginCodeArgs = ProjectArgs & { filename: string; code: string };
type UpdatePluginsConfigArgs = ProjectArgs & { plugins: unknown[] };
export async function writePluginCode(args: WritePluginCodeArgs): Promise<HandlerResponse> {
const { projectPath, filename, code } = args;
await validateProjectPath(projectPath);
// Path traversal対策: basenameでサニタイズ
const sanitizedFilename = path.basename(filename);
if (sanitizedFilename !== filename || filename.includes("..")) {
throw Errors.assetPathInvalid(filename);
}
if (!filename.endsWith(".js")) {
throw Errors.invalidParameter("filename", "Plugin filename must end with .js");
}
// ファイル名の文字制限(英数字・アンダースコア・ハイフンのみ)
if (!/^[a-zA-Z0-9_-]+\.js$/.test(filename)) {
throw Errors.invalidParameter("filename", "Invalid characters in filename. Only alphanumeric, underscore, and hyphen are allowed.");
}
const pluginsDir = path.join(projectPath, "js", "plugins");
try {
await fs.mkdir(pluginsDir, { recursive: true });
} catch (e: unknown) {
// Ignore if exists
}
const filePath = path.join(pluginsDir, sanitizedFilename);
// Write with backup
await withBackup(filePath, async () => {
await fs.writeFile(filePath, code, "utf-8");
});
// Cleanup old backups (non-blocking)
cleanupOldBackups(filePath).catch(async (e: unknown) => {
await Logger.debug(`Failed to cleanup old backups for ${filePath} (non-critical)`, e).catch(() => {
// Last resort: ignore logger errors
});
});
return {
content: [
{
type: "text",
text: `Successfully wrote plugin code to ${filename}`,
},
],
};
}
export async function getPluginsConfig(args: ProjectArgs): Promise<HandlerResponse> {
const { projectPath } = args;
await validateProjectPath(projectPath);
const pluginsConfigPath = path.join(projectPath, "js", "plugins.js");
try {
const content = await fs.readFile(pluginsConfigPath, "utf-8");
const match = content.match(/var\s+\$plugins\s*=\s*(\[[\s\S]*?\])\s*;/);
if (!match) {
throw Errors.dataFileReadError("plugins.js", "Could not parse plugins.js format");
}
const pluginsJson = match[1];
JSON.parse(pluginsJson);
return {
content: [
{
type: "text",
text: pluginsJson,
},
],
};
} catch (error: unknown) {
const err = error as NodeJS.ErrnoException;
if (err?.code === 'ENOENT') {
return {
content: [
{
type: "text",
text: "[]",
},
],
};
}
throw error;
}
}
export async function updatePluginsConfig(args: UpdatePluginsConfigArgs): Promise<HandlerResponse> {
const { projectPath, plugins } = args;
await validateProjectPath(projectPath);
const pluginsConfigPath = path.join(projectPath, "js", "plugins.js");
const content = `// Generated by RPG Maker.
// Do not edit this file directly.
var $plugins =
${JSON.stringify(plugins, null, 2)};
`;
// Write with backup
await withBackup(pluginsConfigPath, async () => {
await fs.writeFile(pluginsConfigPath, content, "utf-8");
});
// Cleanup old backups (non-blocking)
cleanupOldBackups(pluginsConfigPath).catch(async (e: unknown) => {
await Logger.debug(`Failed to cleanup old backups for ${pluginsConfigPath} (non-critical)`, e).catch(() => {
// Last resort: ignore logger errors
});
});
return {
content: [
{
type: "text",
text: "Successfully updated plugins.js",
},
],
};
}