ajustes coder

This commit is contained in:
Jordan Diaz
2026-04-21 16:55:37 +00:00
parent 362666295f
commit 62239cb0a5
7 changed files with 97 additions and 2 deletions

View File

@@ -35,6 +35,8 @@
| `navigate_browser` | Navegación | Navegar el browser del frontend a una URL | | `navigate_browser` | Navegación | Navegar el browser del frontend a una URL |
| `save_project_styles` | Proyecto | Guardar resumen de estilos en docs/project-styles.md | | `save_project_styles` | Proyecto | Guardar resumen de estilos en docs/project-styles.md |
| `rollback_git` | Git | Recuperar cambios de git remoto | | `rollback_git` | Git | Recuperar cambios de git remoto |
| `get_layout_field` | Layout | Lee el source de los campos globales del layout.json: style, javascript, header, footer |
| `set_layout_field` | Layout | Reemplaza un campo global del layout.json. **USA ESTA TOOL** para editar header/footer — NO toques los .tpl directos |
## Flujos de trabajo ## Flujos de trabajo
@@ -116,3 +118,42 @@ Reglas:
5. **recordId para imágenes** es el `num` de `builder_custom`, NO el sectionId del módulo 5. **recordId para imágenes** es el `num` de `builder_custom`, NO el sectionId del módulo
6. Tras `set_module_config_vars`, TODAS las variables del módulo (incluyendo upload) reciben config-vars automáticamente 6. Tras `set_module_config_vars`, TODAS las variables del módulo (incluyendo upload) reciben config-vars automáticamente
7. Si el token expira (error 403), usar `refresh_acai_token` 7. Si el token expira (error 403), usar `refresh_acai_token`
## Layout global (header, footer, style, javascript)
Los 4 campos globales del proyecto (`style.css`, `script.js`, `header`, `footer`) viven en `cms/lib/plugins/builder_saas/layout.json`.
### REGLA CRÍTICA
**NUNCA uses `acai-view`, `acai-line-replace`, `acai-write` ni `acai-delete` sobre**:
- `cms/lib/plugins/builder_saas/layout.json`
- `template/estandar/modulos/custom-header-twig/*`
- `template/estandar/modulos/custom-footer-twig/*`
- `template/estandar/modulos/custom-header/*`
- `template/estandar/modulos/custom-footer/*`
Esos ficheros son **artefactos generados** a partir del `layout.json`. Editarlos directamente provoca:
- Desincronización con `layout.json.{header,footer}ModuleCustom.htmlParsed`.
- Sobrescritura de tus cambios cuando el usuario abre el builder visual y guarda.
- Comportamiento inconsistente entre el render público y el builder.
### Workflow correcto
Para leer:
```
get_layout_field({ field: "header" }) // devuelve el source Twig del header
get_layout_field({ field: "footer" })
get_layout_field({ field: "style" }) // CSS global
get_layout_field({ field: "javascript" }) // JS global
```
Para editar:
```
set_layout_field({ field: "footer", content: "<footer>...nuevo HTML/Twig...</footer>" })
```
El backend:
1. Escribe el source en `layout.json.{field}`.
2. Sincroniza `layout.json.{field}ModuleCustom.htmlParsed`.
3. Regenera los `.tpl` del módulo `custom-{field}-twig/`.
4. Compila el Twig a PHP.

View File

@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { handleToolError, validateRequired } from "../helpers/errorHandler.js"; import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
import { getCurrentProjectInfo, callLocalFileEndpoint, buildLocalFileErrorResponse } from "./helpers.js"; import { getCurrentProjectInfo, callLocalFileEndpoint, buildLocalFileErrorResponse } from "./helpers.js";
import { isProtectedLayoutPath, buildProtectedLayoutPathError } from "./protectedPaths.js";
export function registerAcaiDeleteTool(server) { export function registerAcaiDeleteTool(server) {
server.tool( server.tool(
@@ -16,6 +17,10 @@ export function registerAcaiDeleteTool(server) {
const validationError = validateRequired({ file_path }, ["file_path"], "acai-delete"); const validationError = validateRequired({ file_path }, ["file_path"], "acai-delete");
if (validationError) return validationError; if (validationError) return validationError;
if (isProtectedLayoutPath(file_path)) {
return buildProtectedLayoutPathError(file_path);
}
const { projectSlug, projectDir } = getCurrentProjectInfo(); const { projectSlug, projectDir } = getCurrentProjectInfo();
const result = await callLocalFileEndpoint("POST", "/api/files/delete", { const result = await callLocalFileEndpoint("POST", "/api/files/delete", {
project: projectSlug, project: projectSlug,

View File

@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { handleToolError, validateRequired } from "../helpers/errorHandler.js"; import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
import { getCurrentProjectInfo, callLocalFileEndpoint, buildLocalFileErrorResponse } from "./helpers.js"; import { getCurrentProjectInfo, callLocalFileEndpoint, buildLocalFileErrorResponse } from "./helpers.js";
import { isProtectedLayoutPath, buildProtectedLayoutPathError } from "./protectedPaths.js";
export function registerAcaiLineReplaceTool(server) { export function registerAcaiLineReplaceTool(server) {
server.tool( server.tool(
@@ -24,6 +25,10 @@ export function registerAcaiLineReplaceTool(server) {
); );
if (validationError) return validationError; if (validationError) return validationError;
if (isProtectedLayoutPath(file_path)) {
return buildProtectedLayoutPathError(file_path);
}
const { projectSlug, projectDir } = getCurrentProjectInfo(); const { projectSlug, projectDir } = getCurrentProjectInfo();
const result = await callLocalFileEndpoint("POST", "/api/files/line-replace", { const result = await callLocalFileEndpoint("POST", "/api/files/line-replace", {
project: projectSlug, project: projectSlug,

View File

@@ -0,0 +1,39 @@
// Shared guard for generated layout artifacts. The global layout.json and the
// custom-header/custom-footer module folders are regenerated from the layout
// pipeline (see set_layout_field). Editing them directly leaves the JSON source
// out of sync and the visual builder overwrites the agent changes on next save.
const PROTECTED_LAYOUT_PATHS = [
"cms/lib/plugins/builder_saas/layout.json",
"template/estandar/modulos/custom-header-twig/",
"template/estandar/modulos/custom-footer-twig/",
"template/estandar/modulos/custom-header/",
"template/estandar/modulos/custom-footer/",
];
// Returns true when `relPath` points at the layout.json or any of the
// generated custom-{header,footer}[-twig] module folders.
export function isProtectedLayoutPath(relPath) {
if (!relPath) return false;
const norm = String(relPath).replace(/^\/+/, "");
return PROTECTED_LAYOUT_PATHS.some(p => {
// Folder entries end with "/" -> prefix match on the normalized path.
// File entries (no trailing slash) -> exact match only.
if (p.endsWith("/")) return norm === p.slice(0, -1) || norm.startsWith(p);
return norm === p;
});
}
// Builds a consistent MCP error response pointing the agent to set_layout_field.
export function buildProtectedLayoutPathError(relPath) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: `Forbidden path: ${relPath} is a generated artifact of the global layout. Use set_layout_field instead with field='header' (for custom-header-twig) or field='footer' (for custom-footer-twig).`,
}, null, 2),
}],
isError: true,
};
}

View File

@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { handleToolError, validateRequired } from "../helpers/errorHandler.js"; import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
import { getCurrentProjectInfo, callLocalFileEndpoint, buildLocalFileErrorResponse } from "./helpers.js"; import { getCurrentProjectInfo, callLocalFileEndpoint, buildLocalFileErrorResponse } from "./helpers.js";
import { isProtectedLayoutPath, buildProtectedLayoutPathError } from "./protectedPaths.js";
export function registerAcaiWriteTool(server) { export function registerAcaiWriteTool(server) {
server.tool( server.tool(
@@ -23,6 +24,10 @@ Before writing, check the matching documentation for the file type:
const validationError = validateRequired({ file_path }, ["file_path"], "acai-write"); const validationError = validateRequired({ file_path }, ["file_path"], "acai-write");
if (validationError) return validationError; if (validationError) return validationError;
if (isProtectedLayoutPath(file_path)) {
return buildProtectedLayoutPathError(file_path);
}
const { projectSlug, projectDir } = getCurrentProjectInfo(); const { projectSlug, projectDir } = getCurrentProjectInfo();
const result = await callLocalFileEndpoint("POST", "/api/files/write", { const result = await callLocalFileEndpoint("POST", "/api/files/write", {
project: projectSlug, project: projectSlug,

View File

@@ -13,7 +13,7 @@ import { getCurrentProjectInfo } from "../files/helpers.js";
export function registerGetLayoutFieldTool(server) { export function registerGetLayoutFieldTool(server) {
server.tool( server.tool(
"get_layout_field", "get_layout_field",
`Get the content of a global layout field of the project. Supported: 'style' (global CSS injected in all pages), 'javascript' (global JS), 'header' (HTML/Twig source of the site header), 'footer' (HTML/Twig source of the site footer). Use this before set_layout_field to see the current content.`, `Get the content of a global layout field: 'style', 'javascript', 'header' or 'footer'. For header/footer this is the source of truth — the .tpl files in template/estandar/modulos/custom-{header,footer}-twig/ are generated artifacts. Always prefer this over acai-view on those .tpl files when you need to read the global header/footer source.`,
withAuthParams({ withAuthParams({
field: z.enum(["style", "javascript", "header", "footer"]).describe("Which layout field: 'style', 'javascript', 'header' or 'footer'"), field: z.enum(["style", "javascript", "header", "footer"]).describe("Which layout field: 'style', 'javascript', 'header' or 'footer'"),
}), }),

View File

@@ -14,7 +14,7 @@ import { getCurrentProjectInfo } from "../files/helpers.js";
export function registerSetLayoutFieldTool(server) { export function registerSetLayoutFieldTool(server) {
server.tool( server.tool(
"set_layout_field", "set_layout_field",
`Replace the content of a global layout field. 'style'/'javascript' are simple string fields injected via CDN-like URLs (no regeneration needed). 'header'/'footer' are more complex: saving them triggers a server-side pipeline that regenerates the compiled PHP, Twig module files, and TWIG-compiled templates — changes are visible immediately. Destructive: overwrites existing content. Prefer reading with get_layout_field first.`, `Replace the content of a global layout field: 'style' (CSS), 'javascript' (JS), 'header' (Twig source of the site header), 'footer' (Twig source). CRITICAL: for header/footer, ALWAYS use this tool instead of editing template/estandar/modulos/custom-header-twig/index-base.tpl or custom-footer-twig/index-base.tpl directly with acai-line-replace or acai-write. Editing those .tpl files directly leaves layout.json.{header,footer} out of sync and the visual builder will overwrite your changes on its next save. This tool writes the source, syncs layout.json, regenerates the compiled module files, and runs the TWIG compilation in one atomic pipeline. Destructive: overwrites the full content. Pair with get_layout_field first to read the current source.`,
withAuthParams({ withAuthParams({
field: z.enum(["style", "javascript", "header", "footer"]).describe("Which layout field: 'style', 'javascript', 'header' or 'footer'"), field: z.enum(["style", "javascript", "header", "footer"]).describe("Which layout field: 'style', 'javascript', 'header' or 'footer'"),
content: z.string().describe("Full replacement content. Max 500KB."), content: z.string().describe("Full replacement content. Max 500KB."),