strapi-plugin-mcp-chat 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,12 +36,12 @@ module.exports = __toCommonJS(index_exports);
36
36
  // server/src/controllers/chat.ts
37
37
  var chat_default = ({ strapi }) => ({
38
38
  async message(ctx) {
39
- const { messages, image, lang, previewUrl, autoPublish } = ctx.request.body || {};
39
+ const { messages, image, lang, previewUrl, previewStatus, autoPublish } = ctx.request.body || {};
40
40
  if (!Array.isArray(messages) || messages.length === 0) {
41
41
  return ctx.badRequest('Campo "messages" (array) \xE9 obrigat\xF3rio.');
42
42
  }
43
43
  try {
44
- const result = await strapi.plugin("mcp-chat").service("chat").chat({ messages, image, lang, previewUrl, autoPublish });
44
+ const result = await strapi.plugin("mcp-chat").service("chat").chat({ messages, image, lang, previewUrl, previewStatus, autoPublish });
45
45
  ctx.body = result;
46
46
  } catch (e) {
47
47
  strapi.log.error(`[mcp-chat] ${e?.message || e}`);
@@ -83,13 +83,13 @@ var audio_default = ({ strapi }) => ({
83
83
  });
84
84
 
85
85
  // server/src/controllers/frontend.ts
86
- var import_node_fs7 = __toESM(require("node:fs"));
87
- var import_node_path7 = __toESM(require("node:path"));
86
+ var import_node_fs8 = __toESM(require("node:fs"));
87
+ var import_node_path8 = __toESM(require("node:path"));
88
88
  var import_jszip = __toESM(require("jszip"));
89
89
 
90
90
  // server/src/provision/orchestrate.ts
91
- var import_node_fs3 = __toESM(require("node:fs"));
92
- var import_node_path3 = __toESM(require("node:path"));
91
+ var import_node_fs5 = __toESM(require("node:fs"));
92
+ var import_node_path5 = __toESM(require("node:path"));
93
93
 
94
94
  // server/src/provision/manifest.ts
95
95
  var import_utils = require("@strapi/utils");
@@ -267,8 +267,8 @@ function validateManifest(raw) {
267
267
  const parsed = manifestSchema.safeParse(raw);
268
268
  if (parsed.success) return { ok: true, data: parsed.data };
269
269
  const errors = parsed.error.issues.map((i) => {
270
- const path9 = i.path.length ? `${i.path.join(".")}: ` : "";
271
- return `${path9}${i.message}`;
270
+ const path10 = i.path.length ? `${i.path.join(".")}: ` : "";
271
+ return `${path10}${i.message}`;
272
272
  });
273
273
  return { ok: false, errors };
274
274
  }
@@ -633,8 +633,8 @@ async function seedContent(strapi, manifest) {
633
633
  }
634
634
 
635
635
  // server/src/provision/link.ts
636
- var import_node_fs2 = __toESM(require("node:fs"));
637
- var import_node_path2 = __toESM(require("node:path"));
636
+ var import_node_fs3 = __toESM(require("node:fs"));
637
+ var import_node_path3 = __toESM(require("node:path"));
638
638
 
639
639
  // server/src/provision/adapters.ts
640
640
  var nextAdapter = {
@@ -770,6 +770,179 @@ ${interfaces}
770
770
  `;
771
771
  }
772
772
 
773
+ // server/src/provision/runner.ts
774
+ var import_node_child_process = require("node:child_process");
775
+ var import_node_net = __toESM(require("node:net"));
776
+ var import_node_fs2 = __toESM(require("node:fs"));
777
+ var import_node_path2 = __toESM(require("node:path"));
778
+ var info = { state: "idle", dir: null, url: null, pm: null, error: null, log: [] };
779
+ var child = null;
780
+ var pollTimer = null;
781
+ var appRootForPid = null;
782
+ function pidFilePath(appRoot) {
783
+ return import_node_path2.default.join(appRoot, ".mcp-chat", "frontend.pid");
784
+ }
785
+ function writePid(pid) {
786
+ try {
787
+ if (!appRootForPid) return;
788
+ const pf = pidFilePath(appRootForPid);
789
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(pf), { recursive: true });
790
+ import_node_fs2.default.writeFileSync(pf, String(pid), "utf8");
791
+ } catch {
792
+ }
793
+ }
794
+ function clearPid() {
795
+ try {
796
+ if (appRootForPid) import_node_fs2.default.unlinkSync(pidFilePath(appRootForPid));
797
+ } catch {
798
+ }
799
+ }
800
+ function isAlive(pid) {
801
+ try {
802
+ process.kill(pid, 0);
803
+ return true;
804
+ } catch {
805
+ return false;
806
+ }
807
+ }
808
+ function cleanupStaleFrontend(appRoot) {
809
+ appRootForPid = appRoot;
810
+ try {
811
+ const pf = pidFilePath(appRoot);
812
+ if (!import_node_fs2.default.existsSync(pf)) return;
813
+ const pid = parseInt(import_node_fs2.default.readFileSync(pf, "utf8").trim(), 10);
814
+ if (pid && isAlive(pid)) {
815
+ try {
816
+ process.kill(pid, "SIGTERM");
817
+ } catch {
818
+ }
819
+ }
820
+ import_node_fs2.default.unlinkSync(pf);
821
+ } catch {
822
+ }
823
+ }
824
+ function detectPM(dir) {
825
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "bun.lockb")) || import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "bun.lock"))) return "bun";
826
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "pnpm-lock.yaml"))) return "pnpm";
827
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "yarn.lock"))) return "yarn";
828
+ return "npm";
829
+ }
830
+ var has = (dir, ...names) => names.some((n) => import_node_fs2.default.existsSync(import_node_path2.default.join(dir, n)));
831
+ function detectFramework(dir) {
832
+ if (has(dir, "next.config.js", "next.config.ts", "next.config.mjs")) return "next";
833
+ if (has(dir, "vite.config.js", "vite.config.ts", "vite.config.mjs")) return "vite";
834
+ return "other";
835
+ }
836
+ var FRONTEND_BASE_PORT = 4321;
837
+ function findFreePort(start) {
838
+ return new Promise((resolve) => {
839
+ const tryPort = (p) => {
840
+ if (p > start + 200) return resolve(start);
841
+ const srv = import_node_net.default.createServer();
842
+ srv.once("error", () => tryPort(p + 1));
843
+ srv.once("listening", () => srv.close(() => resolve(p)));
844
+ srv.listen(p, "0.0.0.0");
845
+ };
846
+ tryPort(start);
847
+ });
848
+ }
849
+ function pushLog(s) {
850
+ for (const line of String(s).split("\n")) {
851
+ const t = line.trim();
852
+ if (t) info.log.push(t);
853
+ }
854
+ if (info.log.length > 60) info.log = info.log.slice(-60);
855
+ }
856
+ async function urlUp(url) {
857
+ try {
858
+ const res = await fetch(url, { method: "GET" });
859
+ return res.status >= 200 && res.status < 400;
860
+ } catch {
861
+ return false;
862
+ }
863
+ }
864
+ function getRunStatus() {
865
+ return { ...info, log: info.log.slice(-15) };
866
+ }
867
+ function stopFrontend() {
868
+ if (pollTimer) {
869
+ clearInterval(pollTimer);
870
+ pollTimer = null;
871
+ }
872
+ if (child) {
873
+ try {
874
+ child.kill("SIGTERM");
875
+ } catch {
876
+ }
877
+ child = null;
878
+ }
879
+ clearPid();
880
+ if (info.state !== "error") info.state = "idle";
881
+ }
882
+ async function startFrontend(_strapi, opts) {
883
+ const { dir } = opts;
884
+ if (_strapi?.dirs?.app?.root) appRootForPid = _strapi.dirs.app.root;
885
+ if (child && info.dir === dir && ["installing", "starting", "running"].includes(info.state)) {
886
+ return getRunStatus();
887
+ }
888
+ stopFrontend();
889
+ const pm = detectPM(dir);
890
+ const framework = detectFramework(dir);
891
+ const port = await findFreePort(FRONTEND_BASE_PORT);
892
+ const url = `http://127.0.0.1:${port}`;
893
+ info = { state: "installing", dir, url, pm, error: null, log: [] };
894
+ const spawnIn = (cmd, args) => (0, import_node_child_process.spawn)(cmd, args, { cwd: dir, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
895
+ const fwArgs = framework === "next" ? ["-H", "127.0.0.1", "-p", String(port)] : framework === "vite" ? ["--host", "127.0.0.1", "--port", String(port), "--strictPort"] : ["--port", String(port)];
896
+ const devArgs = pm === "yarn" ? ["dev", ...fwArgs] : ["run", "dev", "--", ...fwArgs];
897
+ const startDev = () => {
898
+ info.state = "starting";
899
+ child = spawnIn(pm, devArgs);
900
+ if (child.pid) writePid(child.pid);
901
+ child.stdout?.on("data", (d) => pushLog(d));
902
+ child.stderr?.on("data", (d) => pushLog(d));
903
+ child.on("exit", (code) => {
904
+ child = null;
905
+ clearPid();
906
+ if (pollTimer) {
907
+ clearInterval(pollTimer);
908
+ pollTimer = null;
909
+ }
910
+ if (info.state === "running") info.state = "idle";
911
+ else {
912
+ info.state = "error";
913
+ info.error = `dev encerrou (c\xF3digo ${code}). Veja o log.`;
914
+ }
915
+ });
916
+ pollTimer = setInterval(async () => {
917
+ if (await urlUp(url)) {
918
+ info.state = "running";
919
+ if (pollTimer) {
920
+ clearInterval(pollTimer);
921
+ pollTimer = null;
922
+ }
923
+ }
924
+ }, 1500);
925
+ };
926
+ const needInstall = !import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "node_modules"));
927
+ if (needInstall) {
928
+ pushLog(`Instalando depend\xEAncias com ${pm}\u2026`);
929
+ const installArgs = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
930
+ const inst = spawnIn(pm, installArgs);
931
+ inst.stdout?.on("data", (d) => pushLog(d));
932
+ inst.stderr?.on("data", (d) => pushLog(d));
933
+ inst.on("exit", (code) => {
934
+ if (code === 0) startDev();
935
+ else {
936
+ info.state = "error";
937
+ info.error = `instala\xE7\xE3o falhou (c\xF3digo ${code}). Veja o log.`;
938
+ }
939
+ });
940
+ } else {
941
+ startDev();
942
+ }
943
+ return getRunStatus();
944
+ }
945
+
773
946
  // server/src/provision/link.ts
774
947
  function parseEnv(content) {
775
948
  const out = {};
@@ -799,48 +972,120 @@ function mergeEnv(existing, next) {
799
972
  }
800
973
  return { content, added, preserved };
801
974
  }
802
- function buildPreviewConfig(manifest) {
975
+ var PREVIEW_MARKER = "mcp-chat:preview-merged";
976
+ var PREVIEW_MODULE = "mcp-chat-preview";
977
+ function buildPreviewConfig(manifest, framework = manifest.framework) {
803
978
  const routes = {};
804
979
  for (const ct of manifest.contentTypes) {
805
- if (ct.preview?.route) routes[apiUid(ct.singularName)] = ct.preview.route;
980
+ routes[apiUid(ct.singularName)] = ct.preview?.route ?? "/";
806
981
  }
807
982
  const routesJson = JSON.stringify(routes, null, 2);
808
- return `// Preview gerado pelo mcp-chat a partir do strapi.manifest.json.
983
+ const isNext2 = framework === "next";
984
+ const urlBranch = isNext2 ? ` // Next.js: rota de draft mode que seta o cookie e redireciona p/ \`path\`.
985
+ const qs = new URLSearchParams({ secret, status: status ?? 'draft', path: pathname });
986
+ return \`\${clientUrl}/api/preview?\${qs.toString()}\`;` : ` // SPA (Vite/TanStack): abre a p\xE1gina direta; o front l\xEA ?preview/status.
987
+ const qs = new URLSearchParams({ preview: '1', status: status ?? 'draft', secret });
988
+ return \`\${clientUrl}\${pathname}?\${qs.toString()}\`;`;
989
+ return `// Preview gerado pelo mcp-chat a partir do strapi.manifest.json (framework: ${framework}).
809
990
  // Mapa uid -> rota do frontend (placeholders :campo s\xE3o preenchidos pelo doc).
991
+ // Este arquivo \xE9 mesclado em config/admin.ts \u2014 n\xE3o precisa edit\xE1-lo \xE0 m\xE3o.
810
992
  const PREVIEW_ROUTES: Record<string, string> = ${routesJson};
811
993
 
812
- export default ({ env }) => ({
813
- auth: {
814
- secret: env('ADMIN_JWT_SECRET'),
815
- },
816
- apiToken: { salt: env('API_TOKEN_SALT') },
817
- transfer: { token: { salt: env('TRANSFER_TOKEN_SALT') } },
994
+ export default ({ env }: { env: any }) => ({
818
995
  preview: {
819
996
  enabled: true,
820
997
  config: {
821
998
  allowedOrigins: [env('CLIENT_URL', 'http://localhost:3000')],
822
999
  async handler(uid: string, { documentId, locale, status }: any) {
823
- const route = PREVIEW_ROUTES[uid];
824
- if (!route) return null;
825
- const doc = await strapi.documents(uid as any).findOne({ documentId, locale });
826
- if (!doc) return null;
827
- // substitui :campo pelos valores do documento (ex.: :slug)
828
- const pathname = route.replace(/:([a-zA-Z0-9_]+)/g, (_m, f) =>
829
- encodeURIComponent(String((doc as any)[f] ?? ''))
830
- );
1000
+ const route = PREVIEW_ROUTES[uid] ?? '/';
831
1001
  const clientUrl = env('CLIENT_URL', 'http://localhost:3000');
832
1002
  const secret = env('PREVIEW_SECRET', '');
833
- const qs = new URLSearchParams({ secret, status: status ?? 'draft', path: pathname });
834
- return \`\${clientUrl}/api/preview?\${qs.toString()}\`;
1003
+
1004
+ // s\xF3 busca o doc se a rota tiver placeholders (ex.: :slug) a preencher.
1005
+ let pathname = route;
1006
+ if (pathname.includes(':')) {
1007
+ const doc = await strapi.documents(uid as any).findOne({ documentId, locale });
1008
+ if (!doc) return null;
1009
+ pathname = pathname.replace(/:([a-zA-Z0-9_]+)/g, (_m, f) =>
1010
+ encodeURIComponent(String((doc as any)[f] ?? ''))
1011
+ );
1012
+ }
1013
+
1014
+ ${urlBranch}
835
1015
  },
836
1016
  },
837
1017
  },
838
1018
  });
839
1019
  `;
840
1020
  }
1021
+ function buildStandaloneAdmin() {
1022
+ return `// ${PREVIEW_MARKER} \u2014 admin.ts gerado pelo mcp-chat (preview inclu\xEDdo).
1023
+ import previewConfig from './${PREVIEW_MODULE}';
1024
+
1025
+ export default ({ env }: { env: any }) => ({
1026
+ auth: { secret: env('ADMIN_JWT_SECRET') },
1027
+ apiToken: { salt: env('API_TOKEN_SALT') },
1028
+ transfer: { token: { salt: env('TRANSFER_TOKEN_SALT') } },
1029
+ secrets: { encryptionKey: env('ENCRYPTION_KEY') },
1030
+ flags: {
1031
+ nps: env.bool('FLAG_NPS', true),
1032
+ promoteEE: env.bool('FLAG_PROMOTE_EE', true),
1033
+ },
1034
+ ...previewConfig({ env }),
1035
+ });
1036
+ `;
1037
+ }
1038
+ function buildAdminWrapper() {
1039
+ return `// ${PREVIEW_MARKER} \u2014 preview do mcp-chat mesclado sobre o admin original.
1040
+ // Sua config original est\xE1 preservada em ./admin.base \u2014 edite l\xE1, n\xE3o aqui.
1041
+ import base from './admin.base';
1042
+ import previewConfig from './${PREVIEW_MODULE}';
1043
+
1044
+ export default (ctx: any) => {
1045
+ const b = typeof base === 'function' ? (base as any)(ctx) : (base ?? {});
1046
+ return { ...b, ...previewConfig(ctx) };
1047
+ };
1048
+ `;
1049
+ }
1050
+ var CSP_MARKER = "mcp-chat:csp-frame";
1051
+ function securityBlock() {
1052
+ return ` // ${CSP_MARKER} \u2014 libera o frame-src p/ o admin embutir o preview do frontend
1053
+ // (dev server local em qualquer porta). Sem isto a CSP padr\xE3o (default-src 'self')
1054
+ // bloqueia o iframe e o preview fica em branco.
1055
+ {
1056
+ name: 'strapi::security',
1057
+ config: {
1058
+ contentSecurityPolicy: {
1059
+ useDefaults: true,
1060
+ directives: {
1061
+ 'connect-src': ["'self'", 'https:', 'http:'],
1062
+ 'frame-src': ["'self'", 'http://localhost:*', 'http://127.0.0.1:*'],
1063
+ 'img-src': ["'self'", 'data:', 'blob:', 'market-assets.strapi.io'],
1064
+ 'media-src': ["'self'", 'data:', 'blob:'],
1065
+ upgradeInsecureRequests: null,
1066
+ },
1067
+ },
1068
+ },
1069
+ },`;
1070
+ }
1071
+ function patchSecurityMiddleware(strapiAppDir, dryRun) {
1072
+ const configDir = import_node_path3.default.join(strapiAppDir, "config");
1073
+ const file = ["middlewares.ts", "middlewares.js"].find(
1074
+ (f) => import_node_fs3.default.existsSync(import_node_path3.default.join(configDir, f))
1075
+ );
1076
+ if (!file) return "skipped";
1077
+ const p = import_node_path3.default.join(configDir, file);
1078
+ const content = import_node_fs3.default.readFileSync(p, "utf8");
1079
+ if (content.includes(CSP_MARKER) || content.includes("'frame-src'")) return "already";
1080
+ const m = content.match(/^[ \t]*['"]strapi::security['"]\s*,/m);
1081
+ if (!m) return "manual";
1082
+ const next = content.replace(m[0], securityBlock());
1083
+ if (!dryRun) import_node_fs3.default.writeFileSync(p, next, "utf8");
1084
+ return "patched";
1085
+ }
841
1086
  function ensureInside(base, target) {
842
- const n = import_node_path2.default.normalize(target);
843
- return n === base || n.startsWith(base + import_node_path2.default.sep);
1087
+ const n = import_node_path3.default.normalize(target);
1088
+ return n === base || n.startsWith(base + import_node_path3.default.sep);
844
1089
  }
845
1090
  function linkFrontend(manifest, opts) {
846
1091
  const adapter = adapterForManifest(manifest);
@@ -852,50 +1097,432 @@ function linkFrontend(manifest, opts) {
852
1097
  typesFile: "strapi-types.ts",
853
1098
  previewFile: "config/admin.ts",
854
1099
  previewAction: "skipped",
1100
+ backendEnvAdded: [],
1101
+ cspAction: "skipped",
855
1102
  errors: []
856
1103
  };
857
- if (!import_node_path2.default.isAbsolute(opts.frontendDir) || !import_node_path2.default.isAbsolute(opts.strapiAppDir)) {
1104
+ if (!import_node_path3.default.isAbsolute(opts.frontendDir) || !import_node_path3.default.isAbsolute(opts.strapiAppDir)) {
858
1105
  result.errors.push("frontendDir e strapiAppDir devem ser absolutos");
859
1106
  return result;
860
1107
  }
861
1108
  try {
862
- const envPath = import_node_path2.default.join(opts.frontendDir, adapter.envFileName);
1109
+ const envPath = import_node_path3.default.join(opts.frontendDir, adapter.envFileName);
863
1110
  if (!ensureInside(opts.frontendDir, envPath)) throw new Error("env fora do frontendDir");
864
- const existing = import_node_fs2.default.existsSync(envPath) ? import_node_fs2.default.readFileSync(envPath, "utf8") : "";
1111
+ const existing = import_node_fs3.default.existsSync(envPath) ? import_node_fs3.default.readFileSync(envPath, "utf8") : "";
865
1112
  const vars = adapter.buildEnv(opts.context);
866
1113
  const { content, added, preserved } = mergeEnv(existing, vars);
867
1114
  result.envAdded = added;
868
1115
  result.envPreserved = preserved;
869
- if (!opts.dryRun && added.length) import_node_fs2.default.writeFileSync(envPath, content, "utf8");
1116
+ if (!opts.dryRun && added.length) import_node_fs3.default.writeFileSync(envPath, content, "utf8");
870
1117
  } catch (e) {
871
1118
  result.errors.push(`env: ${e?.message ?? e}`);
872
1119
  }
873
1120
  try {
874
- const typesPath = import_node_path2.default.join(opts.frontendDir, result.typesFile);
875
- if (!ensureInside(opts.frontendDir, typesPath)) throw new Error("types fora do frontendDir");
876
- if (!opts.dryRun) import_node_fs2.default.writeFileSync(typesPath, generateTypes(manifest), "utf8");
1121
+ const typesPath = import_node_path3.default.join(opts.frontendDir, result.typesFile);
1122
+ if (!ensureInside(opts.frontendDir, typesPath)) throw new Error("types fora do frontendDir");
1123
+ if (!opts.dryRun) import_node_fs3.default.writeFileSync(typesPath, generateTypes(manifest), "utf8");
1124
+ } catch (e) {
1125
+ result.errors.push(`types: ${e?.message ?? e}`);
1126
+ }
1127
+ try {
1128
+ const configDir = import_node_path3.default.join(opts.strapiAppDir, "config");
1129
+ if (!ensureInside(opts.strapiAppDir, configDir)) throw new Error("config fora do strapiAppDir");
1130
+ const adminPath = import_node_path3.default.join(configDir, "admin.ts");
1131
+ const adminBasePath = import_node_path3.default.join(configDir, "admin.base.ts");
1132
+ const modulePath = import_node_path3.default.join(configDir, `${PREVIEW_MODULE}.ts`);
1133
+ if (!opts.dryRun) {
1134
+ import_node_fs3.default.mkdirSync(configDir, { recursive: true });
1135
+ import_node_fs3.default.writeFileSync(modulePath, buildPreviewConfig(manifest, adapter.framework), "utf8");
1136
+ try {
1137
+ import_node_fs3.default.unlinkSync(import_node_path3.default.join(configDir, "admin.mcp-chat-preview.ts"));
1138
+ } catch {
1139
+ }
1140
+ }
1141
+ result.previewFile = `config/${PREVIEW_MODULE}.ts`;
1142
+ if (!import_node_fs3.default.existsSync(adminPath)) {
1143
+ if (!opts.dryRun) import_node_fs3.default.writeFileSync(adminPath, buildStandaloneAdmin(), "utf8");
1144
+ result.previewAction = "created";
1145
+ } else {
1146
+ const adminContent = import_node_fs3.default.readFileSync(adminPath, "utf8");
1147
+ if (!adminContent.includes(PREVIEW_MARKER)) {
1148
+ if (!opts.dryRun) {
1149
+ if (!import_node_fs3.default.existsSync(adminBasePath)) {
1150
+ import_node_fs3.default.writeFileSync(adminBasePath, adminContent, "utf8");
1151
+ }
1152
+ import_node_fs3.default.writeFileSync(adminPath, buildAdminWrapper(), "utf8");
1153
+ }
1154
+ }
1155
+ result.previewAction = "merged";
1156
+ }
1157
+ } catch (e) {
1158
+ result.errors.push(`preview: ${e?.message ?? e}`);
1159
+ }
1160
+ try {
1161
+ const clientUrl = opts.context.frontendUrl || `http://localhost:${FRONTEND_BASE_PORT}`;
1162
+ const backendVars = { CLIENT_URL: clientUrl };
1163
+ if (opts.context.previewSecret) backendVars.PREVIEW_SECRET = opts.context.previewSecret;
1164
+ const backendEnvPath = import_node_path3.default.join(opts.strapiAppDir, ".env");
1165
+ if (ensureInside(opts.strapiAppDir, backendEnvPath)) {
1166
+ const existing = import_node_fs3.default.existsSync(backendEnvPath) ? import_node_fs3.default.readFileSync(backendEnvPath, "utf8") : "";
1167
+ const { content, added } = mergeEnv(existing, backendVars);
1168
+ result.backendEnvAdded = added;
1169
+ if (!opts.dryRun && added.length) import_node_fs3.default.writeFileSync(backendEnvPath, content, "utf8");
1170
+ }
1171
+ } catch (e) {
1172
+ result.errors.push(`backend env: ${e?.message ?? e}`);
1173
+ }
1174
+ try {
1175
+ result.cspAction = patchSecurityMiddleware(opts.strapiAppDir, opts.dryRun);
1176
+ if (result.cspAction === "manual") {
1177
+ result.errors.push(
1178
+ "CSP: config/middlewares j\xE1 tem strapi::security customizado \u2014 adicione manualmente frame-src 'self' http://localhost:* http://127.0.0.1:* para o preview embutir o frontend."
1179
+ );
1180
+ }
1181
+ } catch (e) {
1182
+ result.errors.push(`csp: ${e?.message ?? e}`);
1183
+ }
1184
+ result.ok = result.errors.length === 0;
1185
+ return result;
1186
+ }
1187
+
1188
+ // server/src/provision/wire.ts
1189
+ var import_node_fs4 = __toESM(require("node:fs"));
1190
+ var import_node_path4 = __toESM(require("node:path"));
1191
+ var OPENAI_URL = "https://api.openai.com/v1/chat/completions";
1192
+ var MODEL = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
1193
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1194
+ "node_modules",
1195
+ ".git",
1196
+ "dist",
1197
+ ".next",
1198
+ ".output",
1199
+ ".vinxi",
1200
+ ".tanstack",
1201
+ "build",
1202
+ "coverage",
1203
+ ".turbo",
1204
+ ".cache",
1205
+ "public"
1206
+ ]);
1207
+ function ensureInside2(base, target) {
1208
+ const b = import_node_path4.default.resolve(base);
1209
+ const t = import_node_path4.default.resolve(target);
1210
+ return t === b || t.startsWith(b + import_node_path4.default.sep);
1211
+ }
1212
+ function isNext(manifest) {
1213
+ return manifest.framework === "next";
1214
+ }
1215
+ function usesAtAlias(frontendDir) {
1216
+ for (const f of ["tsconfig.json", "tsconfig.app.json", "jsconfig.json"]) {
1217
+ try {
1218
+ const c = import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, f), "utf8");
1219
+ if (/"@\/\*"\s*:/.test(c)) return true;
1220
+ } catch {
1221
+ }
1222
+ }
1223
+ return false;
1224
+ }
1225
+ function strapiClientSrc(manifest) {
1226
+ const envExpr = isNext(manifest) ? "(typeof process !== 'undefined' ? process.env.NEXT_PUBLIC_STRAPI_URL : undefined)" : "(import.meta as any).env?.VITE_STRAPI_URL";
1227
+ return `// Gerado pelo mcp-chat \u2014 camada de acesso \xE0 Strapi (REST, flat, sem nesting).
1228
+ // N\xE3o edite \xE0 m\xE3o: \xE9 regenerado ao religar o frontend.
1229
+ export const STRAPI_URL =
1230
+ (${envExpr} || "http://localhost:1337").replace(/\\/$/, "");
1231
+
1232
+ export type PreviewMode = { isPreview: boolean; status: "draft" | "published" };
1233
+
1234
+ /** L\xEA o modo de preview da URL (?preview=1 / ?status=draft). */
1235
+ export function getPreviewMode(): PreviewMode {
1236
+ if (typeof window === "undefined") return { isPreview: false, status: "published" };
1237
+ const p = new URLSearchParams(window.location.search);
1238
+ const status = p.get("status");
1239
+ const isPreview = p.get("preview") === "1" || p.has("preview") || status === "draft";
1240
+ return { isPreview, status: status === "draft" || isPreview ? "draft" : "published" };
1241
+ }
1242
+
1243
+ /** Busca um singleType e devolve s\xF3 os atributos (objeto). null em erro. */
1244
+ export async function fetchSection<T = Record<string, any>>(
1245
+ name: string,
1246
+ status: "draft" | "published" = "published"
1247
+ ): Promise<T | null> {
1248
+ const qs = status === "draft" ? "?status=draft" : "";
1249
+ try {
1250
+ const res = await fetch(\`\${STRAPI_URL}/api/\${name}\${qs}\`);
1251
+ if (!res.ok) return null;
1252
+ const json = await res.json();
1253
+ return (json?.data ?? null) as T | null;
1254
+ } catch {
1255
+ return null;
1256
+ }
1257
+ }
1258
+
1259
+ /** Busca uma collection (lista) pelo nome plural; ordena por sortOrder se existir. */
1260
+ export async function fetchCollection<T = Record<string, any>>(
1261
+ pluralName: string,
1262
+ status: "draft" | "published" = "published"
1263
+ ): Promise<T[]> {
1264
+ const qs = new URLSearchParams({ "sort": "sortOrder:asc", "pagination[pageSize]": "100" });
1265
+ if (status === "draft") qs.set("status", "draft");
1266
+ try {
1267
+ const res = await fetch(\`\${STRAPI_URL}/api/\${pluralName}?\${qs.toString()}\`);
1268
+ if (!res.ok) return [];
1269
+ const json = await res.json();
1270
+ return (Array.isArray(json?.data) ? json.data : []) as T[];
1271
+ } catch {
1272
+ return [];
1273
+ }
1274
+ }
1275
+ `;
1276
+ }
1277
+ function hooksSrc(manifest, libImport) {
1278
+ const clientDirective = isNext(manifest) ? `"use client";
1279
+
1280
+ ` : "";
1281
+ return `${clientDirective}// Gerado pelo mcp-chat \u2014 hooks de leitura da Strapi (sem depend\xEAncias al\xE9m do React).
1282
+ // Em modo preview faz polling curto + refetch via postMessage (reflete edi\xE7\xF5es ao vivo).
1283
+ import { useEffect, useState } from "react";
1284
+ import { fetchSection, fetchCollection, getPreviewMode } from "${libImport}";
1285
+
1286
+ export function useSection<T = Record<string, any>>(name: string): Partial<T> {
1287
+ const [data, setData] = useState<Partial<T>>({});
1288
+ useEffect(() => {
1289
+ let alive = true;
1290
+ const { isPreview, status } = getPreviewMode();
1291
+ const load = () => fetchSection<T>(name, status).then((d) => { if (alive && d) setData(d as Partial<T>); });
1292
+ load();
1293
+ if (!isPreview) return () => { alive = false; };
1294
+ const id = window.setInterval(load, 2500);
1295
+ const onMsg = () => load();
1296
+ window.addEventListener("message", onMsg);
1297
+ return () => { alive = false; window.clearInterval(id); window.removeEventListener("message", onMsg); };
1298
+ }, [name]);
1299
+ return data;
1300
+ }
1301
+
1302
+ export function useCollection<T = Record<string, any>>(pluralName: string): T[] {
1303
+ const [data, setData] = useState<T[]>([]);
1304
+ useEffect(() => {
1305
+ let alive = true;
1306
+ const { isPreview, status } = getPreviewMode();
1307
+ const load = () => fetchCollection<T>(pluralName, status).then((d) => { if (alive) setData(d); });
1308
+ load();
1309
+ if (!isPreview) return () => { alive = false; };
1310
+ const id = window.setInterval(load, 2500);
1311
+ const onMsg = () => load();
1312
+ window.addEventListener("message", onMsg);
1313
+ return () => { alive = false; window.clearInterval(id); window.removeEventListener("message", onMsg); };
1314
+ }, [pluralName]);
1315
+ return data;
1316
+ }
1317
+ `;
1318
+ }
1319
+ function writeDataLayer(frontendDir, manifest, dryRun) {
1320
+ const written = [];
1321
+ const libDir = import_node_path4.default.join(frontendDir, "src", "lib");
1322
+ const hooksDir = import_node_path4.default.join(frontendDir, "src", "hooks");
1323
+ const libFile = import_node_path4.default.join(libDir, "strapi.ts");
1324
+ const hooksFile = import_node_path4.default.join(hooksDir, "useStrapi.ts");
1325
+ if (!ensureInside2(frontendDir, libFile) || !ensureInside2(frontendDir, hooksFile)) {
1326
+ throw new Error("camada de dados fora do frontendDir");
1327
+ }
1328
+ const libImport = usesAtAlias(frontendDir) ? "@/lib/strapi" : "../lib/strapi";
1329
+ if (!dryRun) {
1330
+ import_node_fs4.default.mkdirSync(libDir, { recursive: true });
1331
+ import_node_fs4.default.mkdirSync(hooksDir, { recursive: true });
1332
+ import_node_fs4.default.writeFileSync(libFile, strapiClientSrc(manifest), "utf8");
1333
+ import_node_fs4.default.writeFileSync(hooksFile, hooksSrc(manifest, libImport), "utf8");
1334
+ }
1335
+ written.push("src/lib/strapi.ts", "src/hooks/useStrapi.ts");
1336
+ return written;
1337
+ }
1338
+ var CODE_EXT = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
1339
+ var MAX_COMPONENT_CHARS = 16e3;
1340
+ function walkComponents(dir, base, out) {
1341
+ let entries;
1342
+ try {
1343
+ entries = import_node_fs4.default.readdirSync(dir, { withFileTypes: true });
1344
+ } catch {
1345
+ return;
1346
+ }
1347
+ for (const e of entries) {
1348
+ if (e.name.startsWith(".")) continue;
1349
+ const full = import_node_path4.default.join(dir, e.name);
1350
+ if (e.isDirectory()) {
1351
+ if (SKIP_DIRS.has(e.name) || e.name === "ui") continue;
1352
+ walkComponents(full, base, out);
1353
+ } else if (CODE_EXT.has(import_node_path4.default.extname(e.name))) {
1354
+ out.push(import_node_path4.default.relative(base, full));
1355
+ }
1356
+ }
1357
+ }
1358
+ function resolvePlural(strapi, singularName) {
1359
+ const real = strapi?.contentTypes?.[apiUid(singularName)]?.info?.pluralName;
1360
+ return real || `${singularName}s`;
1361
+ }
1362
+ function contentModelSummary(strapi, manifest) {
1363
+ const lines = [];
1364
+ for (const ct of manifest.contentTypes) {
1365
+ const fields = Object.keys(ct.attributes || {}).join(", ");
1366
+ if (ct.kind === "singleType") {
1367
+ lines.push(`singleType "${ct.singularName}" (useSection("${ct.singularName}")) campos: ${fields}`);
1368
+ } else {
1369
+ const plural = resolvePlural(strapi, ct.singularName);
1370
+ lines.push(`collection "${ct.singularName}" (useCollection("${plural}")) campos: ${fields}`);
1371
+ }
1372
+ }
1373
+ return lines.join("\n");
1374
+ }
1375
+ function wirePrompt(rel, source, model, hooksImport, seedSnippet) {
1376
+ return `Voc\xEA religa um componente React para ler o conte\xFAdo da Strapi, SEM quebrar nada.
1377
+
1378
+ MODELO DE CONTE\xDADO (use EXATAMENTE estes nomes de hook/campo):
1379
+ ${model}
1380
+
1381
+ DADOS SEMEADOS (para casar o texto hardcoded com o campo certo):
1382
+ ${seedSnippet}
1383
+
1384
+ REGRAS (siga \xE0 risca):
1385
+ - Importe os hooks de "${hooksImport}" (ex.: import { useSection, useCollection } from "${hooksImport}";).
1386
+ - Dentro do componente, chame os hooks necess\xE1rios (ex.: const hero = useSection("hero-section-content");).
1387
+ - Troque CADA texto hardcoded que casa com um campo por { obj.campo ?? "TEXTO ORIGINAL" } \u2014 SEMPRE mantenha o texto original como fallback no ?? .
1388
+ - Para listas (arrays hardcoded de objetos), troque o array por useCollection(...) e itere sobre ele; mantenha \xEDcones/imagens/classes/animacoes/layout EXATAMENTE como est\xE3o (n\xE3o s\xE3o conte\xFAdo).
1389
+ - N\xC3O altere imports de \xEDcones/assets, JSX estrutural, classes Tailwind, hooks de anima\xE7\xE3o, nada que n\xE3o seja texto/dado.
1390
+ - N\xC3O invente campos nem textos. Se um trecho n\xE3o casa com nenhum campo, deixe como est\xE1.
1391
+ - Mantenha o arquivo V\xC1LIDO e COMPLETO (TypeScript/TSX que compila). Responda com JSON: {"code":"<arquivo .tsx completo>"} e NADA al\xE9m disso.
1392
+
1393
+ ARQUIVO: ${rel}
1394
+ \`\`\`tsx
1395
+ ${source}
1396
+ \`\`\``;
1397
+ }
1398
+ function syntaxError(code) {
1399
+ let esbuild;
1400
+ try {
1401
+ esbuild = require("esbuild");
1402
+ } catch {
1403
+ return null;
1404
+ }
1405
+ try {
1406
+ esbuild.transformSync(code, { loader: "tsx", jsx: "automatic" });
1407
+ return "";
1408
+ } catch (e) {
1409
+ return (e?.message || "erro de sintaxe").split("\n")[0];
1410
+ }
1411
+ }
1412
+ function looksSane(original, next, hooksImport) {
1413
+ if (!next || next.length < 40) return "sa\xEDda vazia/curta demais";
1414
+ if (next.length < original.length * 0.5) return "sa\xEDda muito menor que o original (poss\xEDvel truncamento)";
1415
+ if (!/export\s+default|export\s+function|export\s+const/.test(next)) return "sem export";
1416
+ if (!next.includes(hooksImport)) return "n\xE3o importou os hooks";
1417
+ const balanced = (s, a, b) => s.split(a).length - 1 === s.split(b).length - 1;
1418
+ if (!balanced(next, "{", "}")) return "chaves desbalanceadas";
1419
+ if (!balanced(next, "(", ")")) return "par\xEAnteses desbalanceados";
1420
+ if (!balanced(next, "[", "]")) return "colchetes desbalanceados";
1421
+ if (/```/.test(next)) return "markdown na sa\xEDda";
1422
+ return null;
1423
+ }
1424
+ async function callOpenAI(apiKey, prompt) {
1425
+ const res = await fetch(OPENAI_URL, {
1426
+ method: "POST",
1427
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1428
+ body: JSON.stringify({
1429
+ model: MODEL,
1430
+ temperature: 0,
1431
+ response_format: { type: "json_object" },
1432
+ messages: [
1433
+ { role: "system", content: 'Voc\xEA religa componentes React para a Strapi e responde s\xF3 com JSON {"code": "..."} v\xE1lido.' },
1434
+ { role: "user", content: prompt }
1435
+ ]
1436
+ })
1437
+ });
1438
+ if (!res.ok) throw new Error(`OpenAI: ${await res.text()}`);
1439
+ const data = await res.json();
1440
+ const raw = JSON.parse(data.choices?.[0]?.message?.content ?? "{}");
1441
+ return typeof raw.code === "string" ? raw.code : "";
1442
+ }
1443
+ async function wireFrontend(_strapi, opts) {
1444
+ const { frontendDir, manifest } = opts;
1445
+ const result = {
1446
+ ok: false,
1447
+ dataLayer: [],
1448
+ componentsWired: [],
1449
+ componentsSkipped: [],
1450
+ warnings: [],
1451
+ errors: []
1452
+ };
1453
+ if (!import_node_path4.default.isAbsolute(frontendDir)) {
1454
+ result.errors.push("frontendDir deve ser absoluto");
1455
+ return result;
1456
+ }
1457
+ try {
1458
+ result.dataLayer = writeDataLayer(frontendDir, manifest, opts.dryRun);
877
1459
  } catch (e) {
878
- result.errors.push(`types: ${e?.message ?? e}`);
1460
+ result.errors.push(`camada de dados: ${e?.message ?? e}`);
1461
+ return result;
879
1462
  }
880
- try {
881
- const adminPath = import_node_path2.default.join(opts.strapiAppDir, "config", "admin.ts");
882
- const content = buildPreviewConfig(manifest);
883
- if (import_node_fs2.default.existsSync(adminPath)) {
884
- result.previewFile = "config/admin.mcp-chat-preview.ts";
885
- const sidecar = import_node_path2.default.join(opts.strapiAppDir, "config", "admin.mcp-chat-preview.ts");
886
- if (!opts.dryRun) import_node_fs2.default.writeFileSync(sidecar, content, "utf8");
887
- result.previewAction = "sidecar";
888
- } else {
1463
+ const apiKey = process.env.OPENAI_API_KEY;
1464
+ if (!apiKey) {
1465
+ result.warnings.push("Sem OPENAI_API_KEY: camada de dados criada, mas os componentes N\xC3O foram religados (precisa da chave).");
1466
+ result.ok = true;
1467
+ return result;
1468
+ }
1469
+ const srcDir = import_node_path4.default.join(frontendDir, "src");
1470
+ const all = [];
1471
+ walkComponents(srcDir, frontendDir, all);
1472
+ const components = all.filter((rel) => /(\/|^)(components|pages|app|routes)(\/|$)/.test(rel.replace(/\\/g, "/")));
1473
+ const model = contentModelSummary(_strapi, manifest);
1474
+ const hooksImport = usesAtAlias(frontendDir) ? "@/hooks/useStrapi" : "../hooks/useStrapi";
1475
+ const seedSnippet = JSON.stringify(manifest.seed ?? []).slice(0, 6e3);
1476
+ for (const rel of components) {
1477
+ const abs = import_node_path4.default.join(frontendDir, rel);
1478
+ if (!ensureInside2(frontendDir, abs)) continue;
1479
+ let source;
1480
+ try {
1481
+ source = import_node_fs4.default.readFileSync(abs, "utf8");
1482
+ } catch {
1483
+ continue;
1484
+ }
1485
+ if (source.length > MAX_COMPONENT_CHARS) {
1486
+ result.componentsSkipped.push({ rel, reason: "arquivo grande demais" });
1487
+ continue;
1488
+ }
1489
+ if (source.includes("useStrapi")) {
1490
+ result.componentsSkipped.push({ rel, reason: "j\xE1 religado" });
1491
+ continue;
1492
+ }
1493
+ const check = (code) => {
1494
+ const h = looksSane(source, code, hooksImport);
1495
+ if (h) return h;
1496
+ const s = syntaxError(code);
1497
+ return s ? `sintaxe: ${s}` : null;
1498
+ };
1499
+ try {
1500
+ const prompt = wirePrompt(rel, source, model, hooksImport, seedSnippet);
1501
+ let next = await callOpenAI(apiKey, prompt);
1502
+ let bad = check(next);
1503
+ if (bad) {
1504
+ const fix = `${prompt}
1505
+
1506
+ A sua tentativa anterior foi REJEITADA por: ${bad}. Corrija e responda s\xF3 com o JSON {"code":"..."} do arquivo completo e v\xE1lido.`;
1507
+ const next2 = await callOpenAI(apiKey, fix);
1508
+ const bad2 = check(next2);
1509
+ if (bad2) {
1510
+ result.componentsSkipped.push({ rel, reason: bad2 });
1511
+ continue;
1512
+ }
1513
+ next = next2;
1514
+ }
889
1515
  if (!opts.dryRun) {
890
- import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(adminPath), { recursive: true });
891
- import_node_fs2.default.writeFileSync(adminPath, content, "utf8");
1516
+ const bak = abs + ".bak";
1517
+ if (!import_node_fs4.default.existsSync(bak)) import_node_fs4.default.writeFileSync(bak, source, "utf8");
1518
+ import_node_fs4.default.writeFileSync(abs, next, "utf8");
892
1519
  }
893
- result.previewAction = "created";
1520
+ result.componentsWired.push(rel);
1521
+ } catch (e) {
1522
+ result.componentsSkipped.push({ rel, reason: `IA: ${e?.message ?? e}` });
894
1523
  }
895
- } catch (e) {
896
- result.errors.push(`preview: ${e?.message ?? e}`);
897
1524
  }
898
- result.ok = result.errors.length === 0;
1525
+ result.ok = true;
899
1526
  return result;
900
1527
  }
901
1528
 
@@ -937,17 +1564,17 @@ var MARKER_DIR = ".mcp-chat";
937
1564
  var MARKER_FILE = "pending-provision.json";
938
1565
  var DONE_FILE = "last-provision.json";
939
1566
  function markerPath(strapiAppDir) {
940
- return import_node_path3.default.join(strapiAppDir, MARKER_DIR, MARKER_FILE);
1567
+ return import_node_path5.default.join(strapiAppDir, MARKER_DIR, MARKER_FILE);
941
1568
  }
942
1569
  function donePath(strapiAppDir) {
943
- return import_node_path3.default.join(strapiAppDir, MARKER_DIR, DONE_FILE);
1570
+ return import_node_path5.default.join(strapiAppDir, MARKER_DIR, DONE_FILE);
944
1571
  }
945
1572
  function getProvisionStatus(strapiAppDir) {
946
- const pending = import_node_fs3.default.existsSync(markerPath(strapiAppDir));
1573
+ const pending = import_node_fs5.default.existsSync(markerPath(strapiAppDir));
947
1574
  let done = null;
948
1575
  try {
949
1576
  const dp = donePath(strapiAppDir);
950
- if (import_node_fs3.default.existsSync(dp)) done = JSON.parse(import_node_fs3.default.readFileSync(dp, "utf8"));
1577
+ if (import_node_fs5.default.existsSync(dp)) done = JSON.parse(import_node_fs5.default.readFileSync(dp, "utf8"));
951
1578
  } catch {
952
1579
  }
953
1580
  return { pending, done };
@@ -983,10 +1610,10 @@ function stageProvision(strapi, input) {
983
1610
  if (!input.dryRun) {
984
1611
  try {
985
1612
  const mp = markerPath(input.strapiAppDir);
986
- import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(mp), { recursive: true });
987
- import_node_fs3.default.writeFileSync(mp, JSON.stringify(marker, null, 2), "utf8");
1613
+ import_node_fs5.default.mkdirSync(import_node_path5.default.dirname(mp), { recursive: true });
1614
+ import_node_fs5.default.writeFileSync(mp, JSON.stringify(marker, null, 2), "utf8");
988
1615
  try {
989
- import_node_fs3.default.unlinkSync(donePath(input.strapiAppDir));
1616
+ import_node_fs5.default.unlinkSync(donePath(input.strapiAppDir));
990
1617
  } catch {
991
1618
  }
992
1619
  result.staged = true;
@@ -1002,10 +1629,10 @@ function stageProvision(strapi, input) {
1002
1629
  async function runPendingProvision(strapi, strapiAppDir) {
1003
1630
  const result = { ran: false, errors: [] };
1004
1631
  const mp = markerPath(strapiAppDir);
1005
- if (!import_node_fs3.default.existsSync(mp)) return result;
1632
+ if (!import_node_fs5.default.existsSync(mp)) return result;
1006
1633
  let marker;
1007
1634
  try {
1008
- marker = JSON.parse(import_node_fs3.default.readFileSync(mp, "utf8"));
1635
+ marker = JSON.parse(import_node_fs5.default.readFileSync(mp, "utf8"));
1009
1636
  } catch (e) {
1010
1637
  result.errors.push(`marcador ileg\xEDvel: ${e?.message ?? e}`);
1011
1638
  return result;
@@ -1034,8 +1661,16 @@ async function runPendingProvision(strapi, strapiAppDir) {
1034
1661
  result.errors.push(`link: ${e?.message ?? e}`);
1035
1662
  }
1036
1663
  try {
1037
- const adapter = adapterForManifest(marker.manifest);
1038
- const previewUrl = marker.context.frontendUrl || `http://localhost:${adapter.defaultPort}`;
1664
+ result.wire = await wireFrontend(strapi, {
1665
+ frontendDir: marker.frontendDir,
1666
+ manifest: marker.manifest
1667
+ });
1668
+ if (result.wire.errors.length) result.errors.push(...result.wire.errors.map((e) => `wire: ${e}`));
1669
+ } catch (e) {
1670
+ result.errors.push(`wire: ${e?.message ?? e}`);
1671
+ }
1672
+ try {
1673
+ const previewUrl = marker.context.frontendUrl || `http://localhost:${FRONTEND_BASE_PORT}`;
1039
1674
  const done = {
1040
1675
  name: marker.manifest.name,
1041
1676
  framework: marker.manifest.framework,
@@ -1047,24 +1682,24 @@ async function runPendingProvision(strapi, strapiAppDir) {
1047
1682
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1048
1683
  };
1049
1684
  const dp = donePath(strapiAppDir);
1050
- import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(dp), { recursive: true });
1051
- import_node_fs3.default.writeFileSync(dp, JSON.stringify(done, null, 2), "utf8");
1685
+ import_node_fs5.default.mkdirSync(import_node_path5.default.dirname(dp), { recursive: true });
1686
+ import_node_fs5.default.writeFileSync(dp, JSON.stringify(done, null, 2), "utf8");
1052
1687
  } catch (e) {
1053
1688
  result.errors.push(`resumo: ${e?.message ?? e}`);
1054
1689
  }
1055
1690
  try {
1056
- import_node_fs3.default.unlinkSync(mp);
1691
+ import_node_fs5.default.unlinkSync(mp);
1057
1692
  } catch {
1058
1693
  }
1059
1694
  return result;
1060
1695
  }
1061
1696
 
1062
1697
  // server/src/provision/infer.ts
1063
- var import_node_fs4 = __toESM(require("node:fs"));
1064
- var import_node_path4 = __toESM(require("node:path"));
1065
- var OPENAI_URL = "https://api.openai.com/v1/chat/completions";
1066
- var MODEL = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
1067
- var SKIP_DIRS = /* @__PURE__ */ new Set([
1698
+ var import_node_fs6 = __toESM(require("node:fs"));
1699
+ var import_node_path6 = __toESM(require("node:path"));
1700
+ var OPENAI_URL2 = "https://api.openai.com/v1/chat/completions";
1701
+ var MODEL2 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
1702
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
1068
1703
  "node_modules",
1069
1704
  ".git",
1070
1705
  "dist",
@@ -1078,7 +1713,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
1078
1713
  ".cache",
1079
1714
  "public"
1080
1715
  ]);
1081
- var CODE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
1716
+ var CODE_EXT2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
1082
1717
  var MAX_FILES = 18;
1083
1718
  var MAX_TOTAL_CHARS = 6e4;
1084
1719
  var MAX_FILE_CHARS = 12e3;
@@ -1094,38 +1729,53 @@ function score(rel) {
1094
1729
  function walk(dir, base, out) {
1095
1730
  let entries;
1096
1731
  try {
1097
- entries = import_node_fs4.default.readdirSync(dir, { withFileTypes: true });
1732
+ entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
1098
1733
  } catch {
1099
1734
  return;
1100
1735
  }
1101
1736
  for (const e of entries) {
1102
1737
  if (e.name.startsWith(".") && e.name !== ".") continue;
1103
- const full = import_node_path4.default.join(dir, e.name);
1738
+ const full = import_node_path6.default.join(dir, e.name);
1104
1739
  if (e.isDirectory()) {
1105
- if (SKIP_DIRS.has(e.name)) continue;
1740
+ if (SKIP_DIRS2.has(e.name)) continue;
1106
1741
  walk(full, base, out);
1107
- } else if (CODE_EXT.has(import_node_path4.default.extname(e.name))) {
1108
- out.push(import_node_path4.default.relative(base, full));
1742
+ } else if (CODE_EXT2.has(import_node_path6.default.extname(e.name))) {
1743
+ out.push(import_node_path6.default.relative(base, full));
1109
1744
  }
1110
1745
  }
1111
1746
  }
1747
+ function hasInlineDataArray(content) {
1748
+ return /(?:export\s+)?const\s+\w+\s*(?::[^=\n]+)?=\s*\[\s*\{/.test(content);
1749
+ }
1112
1750
  function collectFiles(frontendDir) {
1113
1751
  const all = [];
1114
1752
  walk(frontendDir, frontendDir, all);
1115
1753
  const tree = all.slice().sort();
1116
- const ranked = all.map((rel) => ({ rel, s: score(rel) })).filter((x) => x.s > 0).sort((a, b) => b.s - a.s);
1117
- const files = [];
1118
- let total = 0;
1119
- for (const { rel } of ranked) {
1120
- if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
1754
+ const scored = [];
1755
+ for (const rel of all) {
1756
+ let content;
1121
1757
  try {
1122
- let content = import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, rel), "utf8");
1123
- if (!/export\s+(const|default|type|interface)/.test(content)) continue;
1124
- if (content.length > MAX_FILE_CHARS) content = content.slice(0, MAX_FILE_CHARS) + "\n/* \u2026truncado\u2026 */";
1125
- files.push({ rel, content });
1126
- total += content.length;
1758
+ content = import_node_fs6.default.readFileSync(import_node_path6.default.join(frontendDir, rel), "utf8");
1127
1759
  } catch {
1760
+ continue;
1128
1761
  }
1762
+ const dataArray = hasInlineDataArray(content);
1763
+ const hasExport = /export\s+(const|default|type|interface)/.test(content);
1764
+ if (!hasExport && !dataArray) continue;
1765
+ let s = score(rel);
1766
+ if (dataArray) s += 8;
1767
+ if (s <= 0) continue;
1768
+ scored.push({ rel, content, s });
1769
+ }
1770
+ scored.sort((a, b) => b.s - a.s);
1771
+ const files = [];
1772
+ let total = 0;
1773
+ for (const it of scored) {
1774
+ if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
1775
+ let content = it.content;
1776
+ if (content.length > MAX_FILE_CHARS) content = content.slice(0, MAX_FILE_CHARS) + "\n/* \u2026truncado\u2026 */";
1777
+ files.push({ rel: it.rel, content });
1778
+ total += content.length;
1129
1779
  }
1130
1780
  return { files, tree };
1131
1781
  }
@@ -1160,7 +1810,7 @@ function collectPageTexts(frontendDir) {
1160
1810
  for (const rel of ranked) {
1161
1811
  if (out.length >= MAX_TEXT_FILES) break;
1162
1812
  try {
1163
- const texts = extractTexts(import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, rel), "utf8"));
1813
+ const texts = extractTexts(import_node_fs6.default.readFileSync(import_node_path6.default.join(frontendDir, rel), "utf8"));
1164
1814
  if (texts.length) out.push({ rel, texts });
1165
1815
  } catch {
1166
1816
  }
@@ -1248,9 +1898,9 @@ function buildPageContentTypes(pageTexts, budget) {
1248
1898
  }
1249
1899
  return { contentTypes, seed };
1250
1900
  }
1251
- function detectFramework(frontendDir) {
1901
+ function detectFramework2(frontendDir) {
1252
1902
  try {
1253
- const pkg = JSON.parse(import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, "package.json"), "utf8"));
1903
+ const pkg = JSON.parse(import_node_fs6.default.readFileSync(import_node_path6.default.join(frontendDir, "package.json"), "utf8"));
1254
1904
  const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
1255
1905
  if (deps.next) return "next";
1256
1906
  if (deps["@tanstack/react-start"]) return "tanstack";
@@ -1294,13 +1944,15 @@ Gere um JSON "strapi.manifest.json" com ESTE formato:
1294
1944
 
1295
1945
  REGRAS:
1296
1946
  - Crie uma content-type para cada COLE\xC7\xC3O de dados (arrays de objetos). Use os MESMOS nomes de campo do c\xF3digo.
1947
+ - IMPORTANTE: muitos frontends guardam os dados em arrays declarados INLINE dentro de componentes (.tsx/.jsx), ex.: \`const services = [{ name, price, desc }]\`, \`const reviews = [...]\`, \`const hours = [...]\`. TRATE esses arrays como cole\xE7\xF5es e modele cada um como um collectionType, mesmo que estejam dentro de um componente de UI.
1948
+ - Ao modelar um array desses, inclua SOMENTE os campos que s\xE3o dados/texto (ex.: name, price, desc, label, href, value). IGNORE props que s\xE3o c\xF3digo/apresenta\xE7\xE3o: componentes de \xEDcone (ex.: \`icon: Scissors\`), elementos React, fun\xE7\xF5es, classes CSS, imports de imagem.
1297
1949
  - Dados de "configura\xE7\xE3o do site" (objeto \xFAnico: nome, telefone, etc.) \u2192 singleType.
1298
1950
  - Campos string longos/descri\xE7\xF5es \u2192 "text" ou "richtext". Listas de strings \u2192 "json".
1299
1951
  - Use "date"/"datetime" SOMENTE para datas ISO completas (YYYY-MM-DD). Datas parciais como "2025-04" ou textos livres \u2192 use "string" (sen\xE3o o seed falha).
1300
1952
  - Imagens (imports de assets ou caminhos) \u2192 "media" (N\xC3O coloque o valor da imagem no seed; omita o campo no seed).
1301
- - Em "seed", extraia o conte\xFAdo REAL hardcoded no c\xF3digo, omitindo campos de m\xEDdia e rela\xE7\xF5es.
1953
+ - Em "seed", copie o conte\xFAdo REAL hardcoded no c\xF3digo, VERBATIM (exatamente como est\xE1, sem reescrever, traduzir ou inventar), omitindo campos de m\xEDdia e rela\xE7\xF5es. Todo valor de seed TEM que existir literalmente no c\xF3digo fornecido.
1302
1954
  - Foque APENAS em cole\xE7\xF5es/objetos de dados \u2014 N\xC3O precisa modelar textos soltos de UI (isso \xE9 tratado \xE0 parte).
1303
- - N\xC3O invente. singularName kebab-case, sem repetir. Rela\xE7\xF5es s\xF3 apontam para types definidos por voc\xEA.
1955
+ - N\xC3O invente NADA. Se n\xE3o tiver certeza de um valor, omita-o. singularName kebab-case, sem repetir. Rela\xE7\xF5es s\xF3 apontam para types definidos por voc\xEA.
1304
1956
  - Se n\xE3o houver cole\xE7\xF5es de dados, devolva contentTypes: [] e seed: [].
1305
1957
  - Responda APENAS com o JSON, nada de markdown.
1306
1958
 
@@ -1311,7 +1963,7 @@ Arquivos de dados (cole\xE7\xF5es):
1311
1963
  ${filesBlock}`;
1312
1964
  }
1313
1965
  async function inferManifest(strapi, frontendDir, opts) {
1314
- const framework = detectFramework(frontendDir);
1966
+ const framework = detectFramework2(frontendDir);
1315
1967
  const result = {
1316
1968
  ok: false,
1317
1969
  inferred: true,
@@ -1320,10 +1972,10 @@ async function inferManifest(strapi, frontendDir, opts) {
1320
1972
  warnings: [],
1321
1973
  errors: []
1322
1974
  };
1323
- const existing = import_node_path4.default.join(frontendDir, "strapi.manifest.json");
1324
- if (import_node_fs4.default.existsSync(existing)) {
1975
+ const existing = import_node_path6.default.join(frontendDir, "strapi.manifest.json");
1976
+ if (import_node_fs6.default.existsSync(existing)) {
1325
1977
  try {
1326
- const raw = JSON.parse(import_node_fs4.default.readFileSync(existing, "utf8"));
1978
+ const raw = JSON.parse(import_node_fs6.default.readFileSync(existing, "utf8"));
1327
1979
  const v2 = validateManifest(raw);
1328
1980
  result.inferred = false;
1329
1981
  result.rawManifest = raw;
@@ -1360,11 +2012,11 @@ async function inferManifest(strapi, frontendDir, opts) {
1360
2012
  let dataSeed = [];
1361
2013
  const apiKey = process.env.OPENAI_API_KEY;
1362
2014
  if (apiKey && collected.files.length) {
1363
- const callOpenAI = async (messages2) => {
1364
- const res = await fetch(OPENAI_URL, {
2015
+ const callOpenAI2 = async (messages2) => {
2016
+ const res = await fetch(OPENAI_URL2, {
1365
2017
  method: "POST",
1366
2018
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1367
- body: JSON.stringify({ model: MODEL, temperature: 0, response_format: { type: "json_object" }, messages: messages2 })
2019
+ body: JSON.stringify({ model: MODEL2, temperature: 0, response_format: { type: "json_object" }, messages: messages2 })
1368
2020
  });
1369
2021
  if (!res.ok) throw new Error(`OpenAI: ${await res.text()}`);
1370
2022
  return res.json();
@@ -1375,7 +2027,7 @@ async function inferManifest(strapi, frontendDir, opts) {
1375
2027
  ];
1376
2028
  try {
1377
2029
  for (let attempt = 0; attempt < 2; attempt++) {
1378
- const data = await callOpenAI(messages);
2030
+ const data = await callOpenAI2(messages);
1379
2031
  const raw = JSON.parse(data.choices?.[0]?.message?.content ?? "{}");
1380
2032
  const candidate = {
1381
2033
  manifestVersion: 1,
@@ -1406,6 +2058,48 @@ async function inferManifest(strapi, frontendDir, opts) {
1406
2058
  } else if (!apiKey) {
1407
2059
  result.warnings.push("Sem OPENAI_API_KEY: modelando os TEXTOS (determin\xEDstico); cole\xE7\xF5es de dados n\xE3o inferidas.");
1408
2060
  }
2061
+ if (dataCts.length && dataSeed.length) {
2062
+ const parts = [];
2063
+ for (const f of collected.files) {
2064
+ try {
2065
+ parts.push(import_node_fs6.default.readFileSync(import_node_path6.default.join(frontendDir, f.rel), "utf8"));
2066
+ } catch {
2067
+ parts.push(f.content);
2068
+ }
2069
+ }
2070
+ const norm = (x) => String(x).toLowerCase().replace(/[^a-z0-9]+/g, "");
2071
+ const hay = norm(parts.join("\n"));
2072
+ const present = (v2) => {
2073
+ if (typeof v2 !== "string") return false;
2074
+ const n = norm(v2);
2075
+ return n.length >= 4 && hay.includes(n);
2076
+ };
2077
+ const keep = /* @__PURE__ */ new Set();
2078
+ const verifiedSeed = [];
2079
+ let droppedEntries = 0;
2080
+ for (const grp of dataSeed) {
2081
+ const entries = (grp.entries ?? []).filter((e) => {
2082
+ const ok = Object.values(e).some(present);
2083
+ if (!ok) droppedEntries++;
2084
+ return ok;
2085
+ });
2086
+ if (entries.length) {
2087
+ verifiedSeed.push({ ...grp, entries });
2088
+ keep.add(grp.singularName);
2089
+ }
2090
+ }
2091
+ const verifiedCts = dataCts.filter(
2092
+ (ct) => ct.kind === "singleType" || keep.has(ct.singularName)
2093
+ );
2094
+ const droppedCts = dataCts.length - verifiedCts.length;
2095
+ if (droppedEntries || droppedCts) {
2096
+ result.warnings.push(
2097
+ `Anti-alucina\xE7\xE3o: descartei ${droppedEntries} entrada(s) e ${droppedCts} content-type(s) cujos valores n\xE3o batiam com o c\xF3digo.`
2098
+ );
2099
+ }
2100
+ dataCts = verifiedCts;
2101
+ dataSeed = verifiedSeed;
2102
+ }
1409
2103
  const budget = 60 - dataCts.length - 1;
1410
2104
  const page = buildPageContentTypes(pageTexts, budget);
1411
2105
  const finalManifest = {
@@ -1434,139 +2128,13 @@ async function inferManifest(strapi, frontendDir, opts) {
1434
2128
  return result;
1435
2129
  }
1436
2130
 
1437
- // server/src/provision/runner.ts
1438
- var import_node_child_process = require("node:child_process");
1439
- var import_node_net = __toESM(require("node:net"));
1440
- var import_node_fs5 = __toESM(require("node:fs"));
1441
- var import_node_path5 = __toESM(require("node:path"));
1442
- var info = { state: "idle", dir: null, url: null, pm: null, error: null, log: [] };
1443
- var child = null;
1444
- var pollTimer = null;
1445
- function detectPM(dir) {
1446
- if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "bun.lockb")) || import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "bun.lock"))) return "bun";
1447
- if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "pnpm-lock.yaml"))) return "pnpm";
1448
- if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "yarn.lock"))) return "yarn";
1449
- return "npm";
1450
- }
1451
- var has = (dir, ...names) => names.some((n) => import_node_fs5.default.existsSync(import_node_path5.default.join(dir, n)));
1452
- function detectFramework2(dir) {
1453
- if (has(dir, "next.config.js", "next.config.ts", "next.config.mjs")) return "next";
1454
- if (has(dir, "vite.config.js", "vite.config.ts", "vite.config.mjs")) return "vite";
1455
- return "other";
1456
- }
1457
- var FRONTEND_BASE_PORT = 4321;
1458
- function findFreePort(start) {
1459
- return new Promise((resolve) => {
1460
- const tryPort = (p) => {
1461
- if (p > start + 200) return resolve(start);
1462
- const srv = import_node_net.default.createServer();
1463
- srv.once("error", () => tryPort(p + 1));
1464
- srv.once("listening", () => srv.close(() => resolve(p)));
1465
- srv.listen(p, "0.0.0.0");
1466
- };
1467
- tryPort(start);
1468
- });
1469
- }
1470
- function pushLog(s) {
1471
- for (const line of String(s).split("\n")) {
1472
- const t = line.trim();
1473
- if (t) info.log.push(t);
1474
- }
1475
- if (info.log.length > 60) info.log = info.log.slice(-60);
1476
- }
1477
- async function urlUp(url) {
1478
- try {
1479
- const res = await fetch(url, { method: "GET" });
1480
- return res.status >= 200 && res.status < 400;
1481
- } catch {
1482
- return false;
1483
- }
1484
- }
1485
- function getRunStatus() {
1486
- return { ...info, log: info.log.slice(-15) };
1487
- }
1488
- function stopFrontend() {
1489
- if (pollTimer) {
1490
- clearInterval(pollTimer);
1491
- pollTimer = null;
1492
- }
1493
- if (child) {
1494
- try {
1495
- child.kill("SIGTERM");
1496
- } catch {
1497
- }
1498
- child = null;
1499
- }
1500
- if (info.state !== "error") info.state = "idle";
1501
- }
1502
- async function startFrontend(_strapi, opts) {
1503
- const { dir } = opts;
1504
- if (child && info.dir === dir && ["installing", "starting", "running"].includes(info.state)) {
1505
- return getRunStatus();
1506
- }
1507
- stopFrontend();
1508
- const pm = detectPM(dir);
1509
- const framework = detectFramework2(dir);
1510
- const port = await findFreePort(FRONTEND_BASE_PORT);
1511
- const url = `http://127.0.0.1:${port}`;
1512
- info = { state: "installing", dir, url, pm, error: null, log: [] };
1513
- const spawnIn = (cmd, args) => (0, import_node_child_process.spawn)(cmd, args, { cwd: dir, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
1514
- const fwArgs = framework === "next" ? ["-H", "127.0.0.1", "-p", String(port)] : framework === "vite" ? ["--host", "127.0.0.1", "--port", String(port), "--strictPort"] : ["--port", String(port)];
1515
- const devArgs = pm === "yarn" ? ["dev", ...fwArgs] : ["run", "dev", "--", ...fwArgs];
1516
- const startDev = () => {
1517
- info.state = "starting";
1518
- child = spawnIn(pm, devArgs);
1519
- child.stdout?.on("data", (d) => pushLog(d));
1520
- child.stderr?.on("data", (d) => pushLog(d));
1521
- child.on("exit", (code) => {
1522
- child = null;
1523
- if (pollTimer) {
1524
- clearInterval(pollTimer);
1525
- pollTimer = null;
1526
- }
1527
- if (info.state === "running") info.state = "idle";
1528
- else {
1529
- info.state = "error";
1530
- info.error = `dev encerrou (c\xF3digo ${code}). Veja o log.`;
1531
- }
1532
- });
1533
- pollTimer = setInterval(async () => {
1534
- if (await urlUp(url)) {
1535
- info.state = "running";
1536
- if (pollTimer) {
1537
- clearInterval(pollTimer);
1538
- pollTimer = null;
1539
- }
1540
- }
1541
- }, 1500);
1542
- };
1543
- const needInstall = !import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "node_modules"));
1544
- if (needInstall) {
1545
- pushLog(`Instalando depend\xEAncias com ${pm}\u2026`);
1546
- const installArgs = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
1547
- const inst = spawnIn(pm, installArgs);
1548
- inst.stdout?.on("data", (d) => pushLog(d));
1549
- inst.stderr?.on("data", (d) => pushLog(d));
1550
- inst.on("exit", (code) => {
1551
- if (code === 0) startDev();
1552
- else {
1553
- info.state = "error";
1554
- info.error = `instala\xE7\xE3o falhou (c\xF3digo ${code}). Veja o log.`;
1555
- }
1556
- });
1557
- } else {
1558
- startDev();
1559
- }
1560
- return getRunStatus();
1561
- }
1562
-
1563
2131
  // server/src/provision/integrate.ts
1564
- var import_node_fs6 = __toESM(require("node:fs"));
1565
- var import_node_path6 = __toESM(require("node:path"));
2132
+ var import_node_fs7 = __toESM(require("node:fs"));
2133
+ var import_node_path7 = __toESM(require("node:path"));
1566
2134
  var import_node_child_process2 = require("node:child_process");
1567
- var OPENAI_URL2 = "https://api.openai.com/v1/chat/completions";
1568
- var MODEL2 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
1569
- var SKIP_DIRS2 = /* @__PURE__ */ new Set([
2135
+ var OPENAI_URL3 = "https://api.openai.com/v1/chat/completions";
2136
+ var MODEL3 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
2137
+ var SKIP_DIRS3 = /* @__PURE__ */ new Set([
1570
2138
  "node_modules",
1571
2139
  ".git",
1572
2140
  "dist",
@@ -1580,7 +2148,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
1580
2148
  ".cache",
1581
2149
  "public"
1582
2150
  ]);
1583
- var CODE_EXT2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
2151
+ var CODE_EXT3 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
1584
2152
  var MAX_DATA_FILES = 3;
1585
2153
  var MAX_FILE_CHARS2 = 16e3;
1586
2154
  function score2(rel) {
@@ -1595,18 +2163,18 @@ function score2(rel) {
1595
2163
  function walk2(dir, base, out) {
1596
2164
  let entries;
1597
2165
  try {
1598
- entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
2166
+ entries = import_node_fs7.default.readdirSync(dir, { withFileTypes: true });
1599
2167
  } catch {
1600
2168
  return;
1601
2169
  }
1602
2170
  for (const e of entries) {
1603
2171
  if (e.name.startsWith(".")) continue;
1604
- const full = import_node_path6.default.join(dir, e.name);
2172
+ const full = import_node_path7.default.join(dir, e.name);
1605
2173
  if (e.isDirectory()) {
1606
- if (SKIP_DIRS2.has(e.name)) continue;
2174
+ if (SKIP_DIRS3.has(e.name)) continue;
1607
2175
  walk2(full, base, out);
1608
- } else if (CODE_EXT2.has(import_node_path6.default.extname(e.name))) {
1609
- out.push(import_node_path6.default.relative(base, full));
2176
+ } else if (CODE_EXT3.has(import_node_path7.default.extname(e.name))) {
2177
+ out.push(import_node_path7.default.relative(base, full));
1610
2178
  }
1611
2179
  }
1612
2180
  }
@@ -1615,7 +2183,7 @@ function findDataFiles(frontendDir) {
1615
2183
  walk2(frontendDir, frontendDir, all);
1616
2184
  return all.map((rel) => ({ rel, s: score2(rel) })).filter((x) => x.s > 0).filter(({ rel }) => {
1617
2185
  try {
1618
- const c = import_node_fs6.default.readFileSync(import_node_path6.default.join(frontendDir, rel), "utf8");
2186
+ const c = import_node_fs7.default.readFileSync(import_node_path7.default.join(frontendDir, rel), "utf8");
1619
2187
  return /export\s+const\s+\w+\s*[:=]\s*(\[|\{)/.test(c);
1620
2188
  } catch {
1621
2189
  return false;
@@ -1684,19 +2252,19 @@ export async function fetchSingle(singular: string, o: FetchOpts = {}): Promise<
1684
2252
  }
1685
2253
  `;
1686
2254
  async function ensureClientDep(frontendDir, warnings) {
1687
- if (import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, "node_modules", "@strapi", "client"))) return;
2255
+ if (import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, "node_modules", "@strapi", "client"))) return;
1688
2256
  try {
1689
- const pkgPath = import_node_path6.default.join(frontendDir, "package.json");
1690
- const pkg = JSON.parse(import_node_fs6.default.readFileSync(pkgPath, "utf8"));
2257
+ const pkgPath = import_node_path7.default.join(frontendDir, "package.json");
2258
+ const pkg = JSON.parse(import_node_fs7.default.readFileSync(pkgPath, "utf8"));
1691
2259
  pkg.dependencies = pkg.dependencies || {};
1692
2260
  if (!pkg.dependencies["@strapi/client"]) {
1693
2261
  pkg.dependencies["@strapi/client"] = "^1.6.2";
1694
- import_node_fs6.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
2262
+ import_node_fs7.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
1695
2263
  }
1696
2264
  } catch (e) {
1697
2265
  warnings.push(`package.json do frontend: ${e?.message ?? e}`);
1698
2266
  }
1699
- const pm = import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, "bun.lock")) || import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, "bun.lockb")) ? "bun" : import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, "pnpm-lock.yaml")) ? "pnpm" : import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, "yarn.lock")) ? "yarn" : "npm";
2267
+ const pm = import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, "bun.lock")) || import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, "bun.lockb")) ? "bun" : import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, "pnpm-lock.yaml")) ? "pnpm" : import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, "yarn.lock")) ? "yarn" : "npm";
1700
2268
  const args = pm === "npm" ? ["install", "@strapi/client", "--no-audit", "--no-fund"] : ["add", "@strapi/client"];
1701
2269
  await new Promise((resolve) => {
1702
2270
  try {
@@ -1748,11 +2316,11 @@ ${baseSrc.length > MAX_FILE_CHARS2 ? baseSrc.slice(0, MAX_FILE_CHARS2) : baseSrc
1748
2316
 
1749
2317
  AMOSTRA REAL DA RESPOSTA DO STRAPI (JSON, por singularName):
1750
2318
  ${JSON.stringify(sample, null, 1).slice(0, 18e3)}`;
1751
- const res = await fetch(OPENAI_URL2, {
2319
+ const res = await fetch(OPENAI_URL3, {
1752
2320
  method: "POST",
1753
2321
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1754
2322
  body: JSON.stringify({
1755
- model: MODEL2,
2323
+ model: MODEL3,
1756
2324
  temperature: 0,
1757
2325
  messages: [
1758
2326
  { role: "system", content: "Voc\xEA gera uma fun\xE7\xE3o pura de mapeamento Strapi\u2192shape do frontend. Responde s\xF3 com a fun\xE7\xE3o." },
@@ -1906,15 +2474,15 @@ export function LanguageSwitcher() {
1906
2474
  export default LanguageSwitcher;
1907
2475
  `;
1908
2476
  function injectSwitcher(frontendDir, warnings, dataImport = "@/data/site") {
1909
- const compDir = import_node_path6.default.join(frontendDir, "src", "components");
1910
- import_node_fs6.default.mkdirSync(compDir, { recursive: true });
1911
- import_node_fs6.default.writeFileSync(import_node_path6.default.join(compDir, "LanguageSwitcher.tsx"), switcherTsx(dataImport), "utf8");
2477
+ const compDir = import_node_path7.default.join(frontendDir, "src", "components");
2478
+ import_node_fs7.default.mkdirSync(compDir, { recursive: true });
2479
+ import_node_fs7.default.writeFileSync(import_node_path7.default.join(compDir, "LanguageSwitcher.tsx"), switcherTsx(dataImport), "utf8");
1912
2480
  const rootRel = ["src/routes/__root.tsx", "src/routes/__root.jsx"].find(
1913
- (r) => import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, r))
2481
+ (r) => import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, r))
1914
2482
  );
1915
2483
  if (rootRel) {
1916
- const abs = import_node_path6.default.join(frontendDir, rootRel);
1917
- let src = import_node_fs6.default.readFileSync(abs, "utf8");
2484
+ const abs = import_node_path7.default.join(frontendDir, rootRel);
2485
+ let src = import_node_fs7.default.readFileSync(abs, "utf8");
1918
2486
  if (!src.includes("loadAllData")) {
1919
2487
  src = `import { loadAllData } from "${dataImport}";
1920
2488
  ` + src;
@@ -1928,17 +2496,17 @@ function injectSwitcher(frontendDir, warnings, dataImport = "@/data/site") {
1928
2496
  } else {
1929
2497
  warnings.push("n\xE3o consegui injetar o loader no __root (padr\xE3o n\xE3o encontrado).");
1930
2498
  }
1931
- import_node_fs6.default.writeFileSync(abs, src, "utf8");
2499
+ import_node_fs7.default.writeFileSync(abs, src, "utf8");
1932
2500
  }
1933
2501
  } else {
1934
2502
  warnings.push("__root n\xE3o encontrado \u2014 dados ao vivo n\xE3o ligados ao SSR.");
1935
2503
  }
1936
2504
  const headerRel = ["src/components/Header.tsx", "src/components/Header.jsx"].find(
1937
- (r) => import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, r))
2505
+ (r) => import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, r))
1938
2506
  );
1939
2507
  if (headerRel) {
1940
- const abs = import_node_path6.default.join(frontendDir, headerRel);
1941
- let src = import_node_fs6.default.readFileSync(abs, "utf8");
2508
+ const abs = import_node_path7.default.join(frontendDir, headerRel);
2509
+ let src = import_node_fs7.default.readFileSync(abs, "utf8");
1942
2510
  if (!src.includes("LanguageSwitcher")) {
1943
2511
  src = `import { LanguageSwitcher } from "@/components/LanguageSwitcher";
1944
2512
  ` + src;
@@ -1948,7 +2516,7 @@ function injectSwitcher(frontendDir, warnings, dataImport = "@/data/site") {
1948
2516
  src = src.replace(/<\/header>/, ` <LanguageSwitcher />
1949
2517
  </header>`);
1950
2518
  }
1951
- import_node_fs6.default.writeFileSync(abs, src, "utf8");
2519
+ import_node_fs7.default.writeFileSync(abs, src, "utf8");
1952
2520
  }
1953
2521
  } else {
1954
2522
  warnings.push("Header n\xE3o encontrado \u2014 adicione <LanguageSwitcher/> manualmente.");
@@ -1994,11 +2562,11 @@ REGRAS ESTRITAS:
1994
2562
 
1995
2563
  ARQUIVO:
1996
2564
  ${src.length > MAX_FILE_CHARS2 ? src.slice(0, MAX_FILE_CHARS2) : src}`;
1997
- const res = await fetch(OPENAI_URL2, {
2565
+ const res = await fetch(OPENAI_URL3, {
1998
2566
  method: "POST",
1999
2567
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
2000
2568
  body: JSON.stringify({
2001
- model: MODEL2,
2569
+ model: MODEL3,
2002
2570
  temperature: 0,
2003
2571
  messages: [
2004
2572
  { role: "system", content: "Voc\xEA religa componentes ao CMS trocando s\xF3 os textos do mapa por express\xF5es. Responde s\xF3 com o c\xF3digo." },
@@ -2072,10 +2640,10 @@ async function rewireComponents(strapi, opts, warnings) {
2072
2640
  (rel) => /\.(tsx|jsx)$/.test(rel) && !/__root|routeTree\.gen|\/api\/|LanguageSwitcher|PreviewBridge|\/ui\//.test(rel)
2073
2641
  );
2074
2642
  for (const rel of targets) {
2075
- const abs = import_node_path6.default.join(opts.frontendDir, rel);
2643
+ const abs = import_node_path7.default.join(opts.frontendDir, rel);
2076
2644
  let src;
2077
2645
  try {
2078
- src = import_node_fs6.default.readFileSync(abs, "utf8");
2646
+ src = import_node_fs7.default.readFileSync(abs, "utf8");
2079
2647
  } catch {
2080
2648
  continue;
2081
2649
  }
@@ -2094,8 +2662,8 @@ async function rewireComponents(strapi, opts, warnings) {
2094
2662
  continue;
2095
2663
  }
2096
2664
  const bak = abs + ".bak";
2097
- if (!import_node_fs6.default.existsSync(bak)) import_node_fs6.default.writeFileSync(bak, src, "utf8");
2098
- import_node_fs6.default.writeFileSync(abs, out, "utf8");
2665
+ if (!import_node_fs7.default.existsSync(bak)) import_node_fs7.default.writeFileSync(bak, src, "utf8");
2666
+ import_node_fs7.default.writeFileSync(abs, out, "utf8");
2099
2667
  rewired.push(rel);
2100
2668
  } catch (e) {
2101
2669
  warnings.push(`${rel}: rewire falhou (${e?.message ?? e}).`);
@@ -2134,19 +2702,19 @@ async function integrateFrontend(strapi, opts) {
2134
2702
  count: Array.isArray(sample[c.singularName]) ? sample[c.singularName].length : sample[c.singularName] ? 1 : 0
2135
2703
  }));
2136
2704
  for (const rel of dataFiles) {
2137
- const abs = import_node_path6.default.join(opts.frontendDir, rel);
2705
+ const abs = import_node_path7.default.join(opts.frontendDir, rel);
2138
2706
  try {
2139
- const original = import_node_fs6.default.readFileSync(abs, "utf8");
2707
+ const original = import_node_fs7.default.readFileSync(abs, "utf8");
2140
2708
  const bak = abs + ".bak";
2141
- if (!import_node_fs6.default.existsSync(bak)) import_node_fs6.default.writeFileSync(bak, original, "utf8");
2142
- const baseSrc = import_node_fs6.default.existsSync(bak) ? import_node_fs6.default.readFileSync(bak, "utf8") : original;
2709
+ if (!import_node_fs7.default.existsSync(bak)) import_node_fs7.default.writeFileSync(bak, original, "utf8");
2710
+ const baseSrc = import_node_fs7.default.existsSync(bak) ? import_node_fs7.default.readFileSync(bak, "utf8") : original;
2143
2711
  const mapper = await generateMapper(apiKey, baseSrc, sample, assetImportIds(baseSrc));
2144
2712
  if (!mapper || !/mapStrapiToData/.test(mapper)) {
2145
2713
  result.warnings.push(`${rel}: mapeador inv\xE1lido, pulado.`);
2146
2714
  continue;
2147
2715
  }
2148
2716
  const moduleSrc = buildLiveDataModule(baseSrc, mapper, ctMeta, locales, def);
2149
- import_node_fs6.default.writeFileSync(abs, moduleSrc, "utf8");
2717
+ import_node_fs7.default.writeFileSync(abs, moduleSrc, "utf8");
2150
2718
  result.filesRewritten.push(rel);
2151
2719
  } catch (e) {
2152
2720
  result.errors.push(`${rel}: ${e?.message ?? e}`);
@@ -2155,8 +2723,8 @@ async function integrateFrontend(strapi, opts) {
2155
2723
  if (result.filesRewritten.length > 0) {
2156
2724
  try {
2157
2725
  const rel0 = result.filesRewritten[0];
2158
- const dataDir = import_node_path6.default.dirname(import_node_path6.default.join(opts.frontendDir, rel0));
2159
- import_node_fs6.default.writeFileSync(import_node_path6.default.join(dataDir, "strapi-client.ts"), STRAPI_CLIENT_TS, "utf8");
2726
+ const dataDir = import_node_path7.default.dirname(import_node_path7.default.join(opts.frontendDir, rel0));
2727
+ import_node_fs7.default.writeFileSync(import_node_path7.default.join(dataDir, "strapi-client.ts"), STRAPI_CLIENT_TS, "utf8");
2160
2728
  const dataImport = "@/" + rel0.replace(/^src\//, "").replace(/\.(tsx?|jsx?)$/, "");
2161
2729
  injectSwitcher(opts.frontendDir, result.warnings, dataImport);
2162
2730
  await ensureClientDep(opts.frontendDir, result.warnings);
@@ -2180,12 +2748,12 @@ async function integrateFrontend(strapi, opts) {
2180
2748
 
2181
2749
  // server/src/controllers/frontend.ts
2182
2750
  var MANIFEST_NAME = "strapi.manifest.json";
2183
- function ensureInside2(base, target) {
2184
- const b = import_node_path7.default.resolve(base);
2185
- const t = import_node_path7.default.resolve(target);
2751
+ function ensureInside3(base, target) {
2752
+ const b = import_node_path8.default.resolve(base);
2753
+ const t = import_node_path8.default.resolve(target);
2186
2754
  if (t === b) return true;
2187
- const rel = import_node_path7.default.relative(b, t);
2188
- return !!rel && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
2755
+ const rel = import_node_path8.default.relative(b, t);
2756
+ return !!rel && !rel.startsWith("..") && !import_node_path8.default.isAbsolute(rel);
2189
2757
  }
2190
2758
  function toKebab(input) {
2191
2759
  const s = (input || "frontend").toLowerCase().replace(/\.zip$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^[^a-z]+/, "");
@@ -2213,13 +2781,13 @@ var frontend_default = {
2213
2781
  const originalName = file.originalFilename || file.name || "frontend";
2214
2782
  let zip;
2215
2783
  try {
2216
- zip = await import_jszip.default.loadAsync(import_node_fs7.default.readFileSync(filepath));
2784
+ zip = await import_jszip.default.loadAsync(import_node_fs8.default.readFileSync(filepath));
2217
2785
  } catch (e) {
2218
2786
  return ctx.badRequest(`Zip inv\xE1lido: ${e?.message ?? e}`);
2219
2787
  }
2220
2788
  const entryNames = Object.keys(zip.files);
2221
2789
  const manifestEntry = entryNames.find(
2222
- (p) => import_node_path7.default.basename(p) === MANIFEST_NAME && !zip.files[p].dir
2790
+ (p) => import_node_path8.default.basename(p) === MANIFEST_NAME && !zip.files[p].dir
2223
2791
  );
2224
2792
  let rootPrefix = "";
2225
2793
  if (manifestEntry) {
@@ -2232,27 +2800,27 @@ var frontend_default = {
2232
2800
  }
2233
2801
  const name = toKebab(originalName);
2234
2802
  const strapiAppDir = strapi.dirs.app.root;
2235
- const frontendDir = import_node_path7.default.resolve(strapiAppDir, "..", name);
2236
- if (import_node_fs7.default.existsSync(frontendDir) && import_node_fs7.default.readdirSync(frontendDir).length > 0) {
2803
+ const frontendDir = import_node_path8.default.resolve(strapiAppDir, "..", name);
2804
+ if (import_node_fs8.default.existsSync(frontendDir) && import_node_fs8.default.readdirSync(frontendDir).length > 0) {
2237
2805
  return ctx.badRequest(
2238
2806
  `A pasta de destino j\xE1 existe e n\xE3o est\xE1 vazia: ${frontendDir}. Renomeie o .zip ou remova a pasta.`
2239
2807
  );
2240
2808
  }
2241
2809
  try {
2242
- import_node_fs7.default.mkdirSync(frontendDir, { recursive: true });
2810
+ import_node_fs8.default.mkdirSync(frontendDir, { recursive: true });
2243
2811
  for (const entryName of entryNames) {
2244
2812
  const entry = zip.files[entryName];
2245
2813
  const rel = rootPrefix && entryName.startsWith(rootPrefix) ? entryName.slice(rootPrefix.length) : entryName;
2246
2814
  if (!rel) continue;
2247
- const dest = import_node_path7.default.join(frontendDir, rel);
2248
- if (!ensureInside2(frontendDir, dest)) {
2815
+ const dest = import_node_path8.default.join(frontendDir, rel);
2816
+ if (!ensureInside3(frontendDir, dest)) {
2249
2817
  throw new Error(`entrada perigosa no zip bloqueada: ${entryName}`);
2250
2818
  }
2251
2819
  if (entry.dir) {
2252
- import_node_fs7.default.mkdirSync(dest, { recursive: true });
2820
+ import_node_fs8.default.mkdirSync(dest, { recursive: true });
2253
2821
  } else {
2254
- import_node_fs7.default.mkdirSync(import_node_path7.default.dirname(dest), { recursive: true });
2255
- import_node_fs7.default.writeFileSync(dest, await entry.async("nodebuffer"));
2822
+ import_node_fs8.default.mkdirSync(import_node_path8.default.dirname(dest), { recursive: true });
2823
+ import_node_fs8.default.writeFileSync(dest, await entry.async("nodebuffer"));
2256
2824
  }
2257
2825
  }
2258
2826
  } catch (e) {
@@ -2280,16 +2848,16 @@ var frontend_default = {
2280
2848
  const rawManifest = body.manifest;
2281
2849
  const frontendDir = body.frontendDir;
2282
2850
  if (!rawManifest) return ctx.badRequest('Envie o "manifest".');
2283
- if (!frontendDir || !import_node_path7.default.isAbsolute(frontendDir)) {
2851
+ if (!frontendDir || !import_node_path8.default.isAbsolute(frontendDir)) {
2284
2852
  return ctx.badRequest("frontendDir ausente ou inv\xE1lido.");
2285
2853
  }
2286
2854
  const strapiAppDir = strapi.dirs.app.root;
2287
2855
  const apiRoot = strapi.dirs.app.api;
2288
- const parent = import_node_path7.default.resolve(strapiAppDir, "..");
2289
- if (!ensureInside2(parent, frontendDir) || frontendDir === parent) {
2856
+ const parent = import_node_path8.default.resolve(strapiAppDir, "..");
2857
+ if (!ensureInside3(parent, frontendDir) || frontendDir === parent) {
2290
2858
  return ctx.badRequest("frontendDir fora da pasta permitida.");
2291
2859
  }
2292
- if (!import_node_fs7.default.existsSync(frontendDir)) {
2860
+ if (!import_node_fs8.default.existsSync(frontendDir)) {
2293
2861
  return ctx.badRequest("frontendDir n\xE3o existe (rode a an\xE1lise primeiro).");
2294
2862
  }
2295
2863
  const v = validateManifest(rawManifest);
@@ -2297,8 +2865,8 @@ var frontend_default = {
2297
2865
  return ctx.badRequest({ message: "Manifest inv\xE1lido", errors: v.errors });
2298
2866
  }
2299
2867
  try {
2300
- import_node_fs7.default.writeFileSync(
2301
- import_node_path7.default.join(frontendDir, MANIFEST_NAME),
2868
+ import_node_fs8.default.writeFileSync(
2869
+ import_node_path8.default.join(frontendDir, MANIFEST_NAME),
2302
2870
  JSON.stringify(v.data, null, 2),
2303
2871
  "utf8"
2304
2872
  );
@@ -2360,8 +2928,8 @@ var frontend_default = {
2360
2928
  if (!dir || !url) {
2361
2929
  return ctx.badRequest("Nenhum frontend provisionado para rodar.");
2362
2930
  }
2363
- const parent = import_node_path7.default.resolve(strapi.dirs.app.root, "..");
2364
- if (!ensureInside2(parent, dir) || !import_node_fs7.default.existsSync(dir)) {
2931
+ const parent = import_node_path8.default.resolve(strapi.dirs.app.root, "..");
2932
+ if (!ensureInside3(parent, dir) || !import_node_fs8.default.existsSync(dir)) {
2365
2933
  return ctx.badRequest("Pasta do frontend inv\xE1lida.");
2366
2934
  }
2367
2935
  ctx.body = await startFrontend(strapi, { dir, url });
@@ -2385,19 +2953,48 @@ var frontend_default = {
2385
2953
  frontendDir = st.done?.frontendDir || "";
2386
2954
  }
2387
2955
  if (!frontendDir) return ctx.badRequest("Nenhum frontend provisionado.");
2388
- const parent = import_node_path7.default.resolve(strapi.dirs.app.root, "..");
2389
- if (!ensureInside2(parent, frontendDir) || !import_node_fs7.default.existsSync(frontendDir)) {
2956
+ const parent = import_node_path8.default.resolve(strapi.dirs.app.root, "..");
2957
+ if (!ensureInside3(parent, frontendDir) || !import_node_fs8.default.existsSync(frontendDir)) {
2390
2958
  return ctx.badRequest("Pasta do frontend inv\xE1lida.");
2391
2959
  }
2392
2960
  let manifest;
2393
2961
  try {
2394
- manifest = JSON.parse(import_node_fs7.default.readFileSync(import_node_path7.default.join(frontendDir, MANIFEST_NAME), "utf8"));
2962
+ manifest = JSON.parse(import_node_fs8.default.readFileSync(import_node_path8.default.join(frontendDir, MANIFEST_NAME), "utf8"));
2395
2963
  } catch {
2396
2964
  return ctx.badRequest("Manifest do projeto n\xE3o encontrado (rode a provis\xE3o primeiro).");
2397
2965
  }
2398
2966
  const v = validateManifest(manifest);
2399
2967
  if (!v.ok) return ctx.badRequest({ message: "Manifest inv\xE1lido", errors: v.errors });
2400
2968
  ctx.body = await integrateFrontend(strapi, { frontendDir, manifest: v.data });
2969
+ },
2970
+ /**
2971
+ * Religa o frontend à Strapi por FETCH AO VIVO: gera a camada de dados (REST,
2972
+ * flat) e religa os componentes (com .bak + fallback + sanidade). Só escreve no
2973
+ * frontend, nunca na Strapi. Usa o último provisionado por padrão.
2974
+ */
2975
+ async wire(ctx) {
2976
+ const strapi = ctx.strapi ?? global.strapi;
2977
+ if (!devOnly(ctx)) return;
2978
+ const body = ctx.request.body || {};
2979
+ let frontendDir = body.frontendDir;
2980
+ if (!frontendDir) {
2981
+ const st = getProvisionStatus(strapi.dirs.app.root);
2982
+ frontendDir = st.done?.frontendDir || "";
2983
+ }
2984
+ if (!frontendDir) return ctx.badRequest("Nenhum frontend provisionado.");
2985
+ const parent = import_node_path8.default.resolve(strapi.dirs.app.root, "..");
2986
+ if (!ensureInside3(parent, frontendDir) || !import_node_fs8.default.existsSync(frontendDir)) {
2987
+ return ctx.badRequest("Pasta do frontend inv\xE1lida.");
2988
+ }
2989
+ let manifest;
2990
+ try {
2991
+ manifest = JSON.parse(import_node_fs8.default.readFileSync(import_node_path8.default.join(frontendDir, MANIFEST_NAME), "utf8"));
2992
+ } catch {
2993
+ return ctx.badRequest("Manifest do projeto n\xE3o encontrado (rode a provis\xE3o primeiro).");
2994
+ }
2995
+ const v = validateManifest(manifest);
2996
+ if (!v.ok) return ctx.badRequest({ message: "Manifest inv\xE1lido", errors: v.errors });
2997
+ ctx.body = await wireFrontend(strapi, { frontendDir, manifest: v.data });
2401
2998
  }
2402
2999
  };
2403
3000
 
@@ -2481,8 +3078,8 @@ var McpClient = class {
2481
3078
  };
2482
3079
 
2483
3080
  // server/src/provision/translate.ts
2484
- var OPENAI_URL3 = "https://api.openai.com/v1/chat/completions";
2485
- var MODEL3 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
3081
+ var OPENAI_URL4 = "https://api.openai.com/v1/chat/completions";
3082
+ var MODEL4 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
2486
3083
  var approxTokens = (s) => Math.ceil((s || "").length / 4);
2487
3084
  var MAX_CHUNK_TOKENS = 1200;
2488
3085
  var splitParagraphs = (text) => text.split(/\n{2,}/);
@@ -2527,7 +3124,7 @@ function splitForTranslation(value, _type) {
2527
3124
  async function translateChunk(apiKey, text, sourceLang, targetLang) {
2528
3125
  if (!text || !text.trim()) return text;
2529
3126
  const body = {
2530
- model: MODEL3,
3127
+ model: MODEL4,
2531
3128
  temperature: 0,
2532
3129
  max_tokens: Math.min(4e3, approxTokens(text) * 3 + 256),
2533
3130
  messages: [
@@ -2538,7 +3135,7 @@ async function translateChunk(apiKey, text, sourceLang, targetLang) {
2538
3135
  { role: "user", content: text }
2539
3136
  ]
2540
3137
  };
2541
- const res = await fetch(OPENAI_URL3, {
3138
+ const res = await fetch(OPENAI_URL4, {
2542
3139
  method: "POST",
2543
3140
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
2544
3141
  body: JSON.stringify(body)
@@ -2605,31 +3202,32 @@ function createContentTools(strapi) {
2605
3202
  for (const [name, a] of Object.entries(attributes)) {
2606
3203
  const v = node[name];
2607
3204
  if (v == null) continue;
2608
- const path9 = [...basePath, name];
3205
+ const path10 = [...basePath, name];
2609
3206
  if (TEXTUAL.includes(a.type)) {
2610
3207
  if (typeof v === "string" && v.toLowerCase().includes(needle)) {
2611
- collect(path9, name, v);
3208
+ collect(path10, name, v);
2612
3209
  }
2613
3210
  } else if (a.type === "component" && a.component) {
2614
3211
  const sub = attrsOf(a.component);
2615
3212
  if (a.repeatable && Array.isArray(v)) {
2616
- v.forEach((item, i) => walkFind(item, sub, [...path9, i], needle, collect));
3213
+ v.forEach((item, i) => walkFind(item, sub, [...path10, i], needle, collect));
2617
3214
  } else {
2618
- walkFind(v, sub, path9, needle, collect);
3215
+ walkFind(v, sub, path10, needle, collect);
2619
3216
  }
2620
3217
  } else if (a.type === "dynamiczone" && Array.isArray(v)) {
2621
3218
  v.forEach((item, i) => {
2622
3219
  if (item?.__component) {
2623
- walkFind(item, attrsOf(item.__component), [...path9, i], needle, collect);
3220
+ walkFind(item, attrsOf(item.__component), [...path10, i], needle, collect);
2624
3221
  }
2625
3222
  });
2626
3223
  }
2627
3224
  }
2628
3225
  };
2629
3226
  const MAX_MATCHES = 100;
2630
- const buscarTexto = async (termo) => {
3227
+ const buscarTexto = async (termo, status = "draft") => {
2631
3228
  const needle = String(termo || "").toLowerCase().trim();
2632
3229
  if (!needle) return { erro: "termo vazio" };
3230
+ const st = status === "published" ? "published" : "draft";
2633
3231
  const matches = [];
2634
3232
  for (const ct of apiContentTypes()) {
2635
3233
  if (matches.length >= MAX_MATCHES) break;
@@ -2637,19 +3235,19 @@ function createContentTools(strapi) {
2637
3235
  const populate = buildPopulate(attributes);
2638
3236
  let entries = [];
2639
3237
  try {
2640
- const res = await strapi.documents(ct.uid).findMany({ status: "draft", populate, limit: 200 });
3238
+ const res = await strapi.documents(ct.uid).findMany({ status: st, populate, limit: 200 });
2641
3239
  entries = Array.isArray(res) ? res : res ? [res] : [];
2642
3240
  } catch {
2643
3241
  continue;
2644
3242
  }
2645
3243
  const dp = hasDraftAndPublish(ct.uid);
2646
3244
  for (const e of entries) {
2647
- walkFind(e, attributes, [], needle, (path9, campo, valor) => {
3245
+ walkFind(e, attributes, [], needle, (path10, campo, valor) => {
2648
3246
  matches.push({
2649
3247
  uid: ct.uid,
2650
3248
  tipo: ct.info?.displayName || ct.uid,
2651
3249
  documentId: e.documentId,
2652
- path: path9,
3250
+ path: path10,
2653
3251
  campo,
2654
3252
  valor_atual: valor.length > 300 ? valor.slice(0, 300) + "\u2026" : valor,
2655
3253
  // draftAndPublish=false → não há rascunho; a edição já é o conteúdo
@@ -2697,8 +3295,8 @@ function createContentTools(strapi) {
2697
3295
  }
2698
3296
  return value;
2699
3297
  };
2700
- const editarCampo = async ({ uid, documentId, path: path9, campo, novo_valor, locale }) => {
2701
- const p = Array.isArray(path9) && path9.length ? path9 : campo ? [campo] : null;
3298
+ const editarCampo = async ({ uid, documentId, path: path10, campo, novo_valor, locale }) => {
3299
+ const p = Array.isArray(path10) && path10.length ? path10 : campo ? [campo] : null;
2702
3300
  if (!p) return { erro: 'informe "path" (array) ou "campo"' };
2703
3301
  const attributes = strapi.contentTypes?.[uid]?.attributes || {};
2704
3302
  const topAttr = p[0];
@@ -3000,29 +3598,29 @@ var openAiToolSpecs = [
3000
3598
  ];
3001
3599
 
3002
3600
  // server/src/provision/enable-i18n.ts
3003
- var import_node_fs8 = __toESM(require("node:fs"));
3004
- var import_node_path8 = __toESM(require("node:path"));
3601
+ var import_node_fs9 = __toESM(require("node:fs"));
3602
+ var import_node_path9 = __toESM(require("node:path"));
3005
3603
  var LOCALIZABLE = ["string", "text", "richtext", "component", "dynamiczone"];
3006
3604
  var isDev2 = () => process.env.NODE_ENV === "development";
3007
3605
  function schemaPathFor(apiRoot, uid) {
3008
3606
  const m = /^api::([^.]+)\.([^.]+)$/.exec(uid);
3009
3607
  if (!m) return null;
3010
3608
  const [, api, ct] = m;
3011
- return import_node_path8.default.join(apiRoot, api, "content-types", ct, "schema.json");
3609
+ return import_node_path9.default.join(apiRoot, api, "content-types", ct, "schema.json");
3012
3610
  }
3013
3611
  function listAllUids(apiRoot) {
3014
3612
  const out = [];
3015
3613
  let apis = [];
3016
3614
  try {
3017
- apis = import_node_fs8.default.readdirSync(apiRoot);
3615
+ apis = import_node_fs9.default.readdirSync(apiRoot);
3018
3616
  } catch {
3019
3617
  return out;
3020
3618
  }
3021
3619
  for (const api of apis) {
3022
- const ctDir = import_node_path8.default.join(apiRoot, api, "content-types");
3023
- if (!import_node_fs8.default.existsSync(ctDir)) continue;
3024
- for (const ct of import_node_fs8.default.readdirSync(ctDir)) {
3025
- if (import_node_fs8.default.existsSync(import_node_path8.default.join(ctDir, ct, "schema.json"))) out.push(`api::${api}.${ct}`);
3620
+ const ctDir = import_node_path9.default.join(apiRoot, api, "content-types");
3621
+ if (!import_node_fs9.default.existsSync(ctDir)) continue;
3622
+ for (const ct of import_node_fs9.default.readdirSync(ctDir)) {
3623
+ if (import_node_fs9.default.existsSync(import_node_path9.default.join(ctDir, ct, "schema.json"))) out.push(`api::${api}.${ct}`);
3026
3624
  }
3027
3625
  }
3028
3626
  return out;
@@ -3032,10 +3630,10 @@ var withLocalized = (obj) => ({
3032
3630
  i18n: { ...(obj || {}).i18n || {}, localized: true }
3033
3631
  });
3034
3632
  function patchOne(file, campos) {
3035
- if (!import_node_fs8.default.existsSync(file)) return { erro: `schema.json n\xE3o encontrado (${file})` };
3633
+ if (!import_node_fs9.default.existsSync(file)) return { erro: `schema.json n\xE3o encontrado (${file})` };
3036
3634
  let schema;
3037
3635
  try {
3038
- schema = JSON.parse(import_node_fs8.default.readFileSync(file, "utf8"));
3636
+ schema = JSON.parse(import_node_fs9.default.readFileSync(file, "utf8"));
3039
3637
  } catch (e) {
3040
3638
  return { erro: `schema.json ileg\xEDvel: ${e?.message ?? e}` };
3041
3639
  }
@@ -3049,7 +3647,7 @@ function patchOne(file, campos) {
3049
3647
  changed.push(name);
3050
3648
  }
3051
3649
  try {
3052
- import_node_fs8.default.writeFileSync(file, JSON.stringify(schema, null, 2) + "\n", "utf8");
3650
+ import_node_fs9.default.writeFileSync(file, JSON.stringify(schema, null, 2) + "\n", "utf8");
3053
3651
  } catch (e) {
3054
3652
  return { erro: `falha ao gravar schema.json: ${e?.message ?? e}` };
3055
3653
  }
@@ -3060,8 +3658,8 @@ function enableI18n(opts) {
3060
3658
  if (!allowOutsideDev && !isDev2()) {
3061
3659
  return { erro: "habilitar i18n s\xF3 \xE9 permitido em desenvolvimento (NODE_ENV=development)." };
3062
3660
  }
3063
- const srcDir = strapi?.dirs?.app?.src || import_node_path8.default.join(process.cwd(), "src");
3064
- const apiRoot = import_node_path8.default.join(srcDir, "api");
3661
+ const srcDir = strapi?.dirs?.app?.src || import_node_path9.default.join(process.cwd(), "src");
3662
+ const apiRoot = import_node_path9.default.join(srcDir, "api");
3065
3663
  if (!uid || uid === "*") {
3066
3664
  const uids = listAllUids(apiRoot);
3067
3665
  if (!uids.length) return { erro: `nenhuma content-type encontrada em ${apiRoot}` };
@@ -3084,9 +3682,9 @@ function enableI18n(opts) {
3084
3682
  }
3085
3683
 
3086
3684
  // server/src/services/chat.ts
3087
- var MODEL4 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
3685
+ var MODEL5 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
3088
3686
  var MAX_TURNS = 10;
3089
- var OPENAI_URL4 = "https://api.openai.com/v1/chat/completions";
3687
+ var OPENAI_URL5 = "https://api.openai.com/v1/chat/completions";
3090
3688
  var SYSTEM = {
3091
3689
  pt: `Voc\xEA \xE9 um assistente embutido no admin do Strapi 5 deste projeto. Voc\xEA N\xC3O \xE9 s\xF3 um guia: voc\xEA consegue EDITAR e PUBLICAR conte\xFAdo de verdade atrav\xE9s das ferramentas.
3092
3690
 
@@ -3152,7 +3750,7 @@ If the user shares their screen, an image is attached to the last message \u2014
3152
3750
  Be concise and actionable. ALWAYS answer in English.`
3153
3751
  };
3154
3752
  var chat_default2 = ({ strapi }) => ({
3155
- async chat({ messages, image, lang = "pt", previewUrl, autoPublish = false }) {
3753
+ async chat({ messages, image, lang = "pt", previewUrl, previewStatus = "draft", autoPublish = false }) {
3156
3754
  const apiKey = process.env.OPENAI_API_KEY;
3157
3755
  if (!apiKey) {
3158
3756
  throw new Error(
@@ -3162,7 +3760,7 @@ var chat_default2 = ({ strapi }) => ({
3162
3760
  const language = lang === "en" ? "en" : "pt";
3163
3761
  const { buscarTexto, editarCampo, publicar, listarLocales, criarLocale, traduzir } = createContentTools(strapi);
3164
3762
  const LOCAL_TOOLS = {
3165
- buscar_texto: (a) => buscarTexto(a?.termo),
3763
+ buscar_texto: (a) => buscarTexto(a?.termo, previewStatus),
3166
3764
  editar_campo: (a) => editarCampo(a),
3167
3765
  publicar: (a) => publicar(a),
3168
3766
  listar_locales: () => listarLocales(),
@@ -3250,12 +3848,12 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
3250
3848
  convo.push({ role: m.role, content: m.content });
3251
3849
  }
3252
3850
  });
3253
- const callOpenAI = async (body) => {
3851
+ const callOpenAI2 = async (body) => {
3254
3852
  const ctrl = new AbortController();
3255
3853
  const timer = setTimeout(() => ctrl.abort(), 6e4);
3256
3854
  let res;
3257
3855
  try {
3258
- res = await fetch(OPENAI_URL4, {
3856
+ res = await fetch(OPENAI_URL5, {
3259
3857
  method: "POST",
3260
3858
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
3261
3859
  body: JSON.stringify(body),
@@ -3272,8 +3870,8 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
3272
3870
  };
3273
3871
  let didWrite = false;
3274
3872
  for (let turn = 0; turn < MAX_TURNS; turn++) {
3275
- const data = await callOpenAI({
3276
- model: MODEL4,
3873
+ const data = await callOpenAI2({
3874
+ model: MODEL5,
3277
3875
  max_tokens: 2048,
3278
3876
  messages: convo,
3279
3877
  ...tools2.length > 0 ? { tools: tools2, tool_choice: "auto" } : {}
@@ -3317,7 +3915,7 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
3317
3915
  const text = (typeof msg.content === "string" ? msg.content : "").trim();
3318
3916
  return {
3319
3917
  reply: text || "(sem resposta)",
3320
- model: MODEL4,
3918
+ model: MODEL5,
3321
3919
  lang: language,
3322
3920
  didWrite,
3323
3921
  toolsAvailable: tools2.length
@@ -3325,7 +3923,7 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
3325
3923
  }
3326
3924
  return {
3327
3925
  reply: language === "en" ? "(agent turn limit reached)" : "(limite de turnos do agente atingido)",
3328
- model: MODEL4,
3926
+ model: MODEL5,
3329
3927
  lang: language,
3330
3928
  didWrite,
3331
3929
  toolsAvailable: tools2.length
@@ -3426,6 +4024,12 @@ var routes_default = {
3426
4024
  path: "/frontend/integrate",
3427
4025
  handler: "frontend.integrate",
3428
4026
  config: { policies: [] }
4027
+ },
4028
+ {
4029
+ method: "POST",
4030
+ path: "/frontend/wire",
4031
+ handler: "frontend.wire",
4032
+ config: { policies: [] }
3429
4033
  }
3430
4034
  ]
3431
4035
  }
@@ -3640,6 +4244,10 @@ var register_default = ({ strapi }) => {
3640
4244
  var index_default = {
3641
4245
  register: register_default,
3642
4246
  async bootstrap({ strapi }) {
4247
+ try {
4248
+ cleanupStaleFrontend(strapi.dirs.app.root);
4249
+ } catch {
4250
+ }
3643
4251
  try {
3644
4252
  const r = await runPendingProvision(strapi, strapi.dirs.app.root);
3645
4253
  if (r.ran) {