strapi-plugin-mcp-chat 0.5.0 → 0.6.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.
- package/admin/src/components/AdminOverlays.tsx +8 -18
- package/admin/src/components/FloatingChat.tsx +70 -3
- package/dist/server/index.js +436 -213
- package/package.json +1 -1
- package/server/src/content-tools.ts +4 -2
- package/server/src/controllers/chat.ts +2 -2
- package/server/src/index.ts +8 -1
- package/server/src/provision/infer.ts +92 -20
- package/server/src/provision/link.ts +232 -35
- package/server/src/provision/orchestrate.ts +4 -3
- package/server/src/provision/runner.ts +44 -1
- package/server/src/services/chat.ts +8 -2
package/dist/server/index.js
CHANGED
|
@@ -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}`);
|
|
@@ -88,8 +88,8 @@ var import_node_path7 = __toESM(require("node:path"));
|
|
|
88
88
|
var import_jszip = __toESM(require("jszip"));
|
|
89
89
|
|
|
90
90
|
// server/src/provision/orchestrate.ts
|
|
91
|
-
var
|
|
92
|
-
var
|
|
91
|
+
var import_node_fs4 = __toESM(require("node:fs"));
|
|
92
|
+
var import_node_path4 = __toESM(require("node:path"));
|
|
93
93
|
|
|
94
94
|
// server/src/provision/manifest.ts
|
|
95
95
|
var import_utils = require("@strapi/utils");
|
|
@@ -633,8 +633,8 @@ async function seedContent(strapi, manifest) {
|
|
|
633
633
|
}
|
|
634
634
|
|
|
635
635
|
// server/src/provision/link.ts
|
|
636
|
-
var
|
|
637
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
980
|
+
routes[apiUid(ct.singularName)] = ct.preview?.route ?? "/";
|
|
806
981
|
}
|
|
807
982
|
const routesJson = JSON.stringify(routes, null, 2);
|
|
808
|
-
|
|
983
|
+
const isNext = framework === "next";
|
|
984
|
+
const urlBranch = isNext ? ` // 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
|
-
|
|
834
|
-
|
|
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 =
|
|
843
|
-
return n === base || n.startsWith(base +
|
|
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,49 +1097,90 @@ 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 (!
|
|
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 =
|
|
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 =
|
|
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)
|
|
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 =
|
|
1121
|
+
const typesPath = import_node_path3.default.join(opts.frontendDir, result.typesFile);
|
|
875
1122
|
if (!ensureInside(opts.frontendDir, typesPath)) throw new Error("types fora do frontendDir");
|
|
876
|
-
if (!opts.dryRun)
|
|
1123
|
+
if (!opts.dryRun) import_node_fs3.default.writeFileSync(typesPath, generateTypes(manifest), "utf8");
|
|
877
1124
|
} catch (e) {
|
|
878
1125
|
result.errors.push(`types: ${e?.message ?? e}`);
|
|
879
1126
|
}
|
|
880
1127
|
try {
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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 {
|
|
892
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");
|
|
893
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";
|
|
894
1156
|
}
|
|
895
1157
|
} catch (e) {
|
|
896
1158
|
result.errors.push(`preview: ${e?.message ?? e}`);
|
|
897
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
|
+
}
|
|
898
1184
|
result.ok = result.errors.length === 0;
|
|
899
1185
|
return result;
|
|
900
1186
|
}
|
|
@@ -937,17 +1223,17 @@ var MARKER_DIR = ".mcp-chat";
|
|
|
937
1223
|
var MARKER_FILE = "pending-provision.json";
|
|
938
1224
|
var DONE_FILE = "last-provision.json";
|
|
939
1225
|
function markerPath(strapiAppDir) {
|
|
940
|
-
return
|
|
1226
|
+
return import_node_path4.default.join(strapiAppDir, MARKER_DIR, MARKER_FILE);
|
|
941
1227
|
}
|
|
942
1228
|
function donePath(strapiAppDir) {
|
|
943
|
-
return
|
|
1229
|
+
return import_node_path4.default.join(strapiAppDir, MARKER_DIR, DONE_FILE);
|
|
944
1230
|
}
|
|
945
1231
|
function getProvisionStatus(strapiAppDir) {
|
|
946
|
-
const pending =
|
|
1232
|
+
const pending = import_node_fs4.default.existsSync(markerPath(strapiAppDir));
|
|
947
1233
|
let done = null;
|
|
948
1234
|
try {
|
|
949
1235
|
const dp = donePath(strapiAppDir);
|
|
950
|
-
if (
|
|
1236
|
+
if (import_node_fs4.default.existsSync(dp)) done = JSON.parse(import_node_fs4.default.readFileSync(dp, "utf8"));
|
|
951
1237
|
} catch {
|
|
952
1238
|
}
|
|
953
1239
|
return { pending, done };
|
|
@@ -983,10 +1269,10 @@ function stageProvision(strapi, input) {
|
|
|
983
1269
|
if (!input.dryRun) {
|
|
984
1270
|
try {
|
|
985
1271
|
const mp = markerPath(input.strapiAppDir);
|
|
986
|
-
|
|
987
|
-
|
|
1272
|
+
import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(mp), { recursive: true });
|
|
1273
|
+
import_node_fs4.default.writeFileSync(mp, JSON.stringify(marker, null, 2), "utf8");
|
|
988
1274
|
try {
|
|
989
|
-
|
|
1275
|
+
import_node_fs4.default.unlinkSync(donePath(input.strapiAppDir));
|
|
990
1276
|
} catch {
|
|
991
1277
|
}
|
|
992
1278
|
result.staged = true;
|
|
@@ -1002,10 +1288,10 @@ function stageProvision(strapi, input) {
|
|
|
1002
1288
|
async function runPendingProvision(strapi, strapiAppDir) {
|
|
1003
1289
|
const result = { ran: false, errors: [] };
|
|
1004
1290
|
const mp = markerPath(strapiAppDir);
|
|
1005
|
-
if (!
|
|
1291
|
+
if (!import_node_fs4.default.existsSync(mp)) return result;
|
|
1006
1292
|
let marker;
|
|
1007
1293
|
try {
|
|
1008
|
-
marker = JSON.parse(
|
|
1294
|
+
marker = JSON.parse(import_node_fs4.default.readFileSync(mp, "utf8"));
|
|
1009
1295
|
} catch (e) {
|
|
1010
1296
|
result.errors.push(`marcador ileg\xEDvel: ${e?.message ?? e}`);
|
|
1011
1297
|
return result;
|
|
@@ -1034,8 +1320,7 @@ async function runPendingProvision(strapi, strapiAppDir) {
|
|
|
1034
1320
|
result.errors.push(`link: ${e?.message ?? e}`);
|
|
1035
1321
|
}
|
|
1036
1322
|
try {
|
|
1037
|
-
const
|
|
1038
|
-
const previewUrl = marker.context.frontendUrl || `http://localhost:${adapter.defaultPort}`;
|
|
1323
|
+
const previewUrl = marker.context.frontendUrl || `http://localhost:${FRONTEND_BASE_PORT}`;
|
|
1039
1324
|
const done = {
|
|
1040
1325
|
name: marker.manifest.name,
|
|
1041
1326
|
framework: marker.manifest.framework,
|
|
@@ -1047,21 +1332,21 @@ async function runPendingProvision(strapi, strapiAppDir) {
|
|
|
1047
1332
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1048
1333
|
};
|
|
1049
1334
|
const dp = donePath(strapiAppDir);
|
|
1050
|
-
|
|
1051
|
-
|
|
1335
|
+
import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(dp), { recursive: true });
|
|
1336
|
+
import_node_fs4.default.writeFileSync(dp, JSON.stringify(done, null, 2), "utf8");
|
|
1052
1337
|
} catch (e) {
|
|
1053
1338
|
result.errors.push(`resumo: ${e?.message ?? e}`);
|
|
1054
1339
|
}
|
|
1055
1340
|
try {
|
|
1056
|
-
|
|
1341
|
+
import_node_fs4.default.unlinkSync(mp);
|
|
1057
1342
|
} catch {
|
|
1058
1343
|
}
|
|
1059
1344
|
return result;
|
|
1060
1345
|
}
|
|
1061
1346
|
|
|
1062
1347
|
// server/src/provision/infer.ts
|
|
1063
|
-
var
|
|
1064
|
-
var
|
|
1348
|
+
var import_node_fs5 = __toESM(require("node:fs"));
|
|
1349
|
+
var import_node_path5 = __toESM(require("node:path"));
|
|
1065
1350
|
var OPENAI_URL = "https://api.openai.com/v1/chat/completions";
|
|
1066
1351
|
var MODEL = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
|
|
1067
1352
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -1094,38 +1379,53 @@ function score(rel) {
|
|
|
1094
1379
|
function walk(dir, base, out) {
|
|
1095
1380
|
let entries;
|
|
1096
1381
|
try {
|
|
1097
|
-
entries =
|
|
1382
|
+
entries = import_node_fs5.default.readdirSync(dir, { withFileTypes: true });
|
|
1098
1383
|
} catch {
|
|
1099
1384
|
return;
|
|
1100
1385
|
}
|
|
1101
1386
|
for (const e of entries) {
|
|
1102
1387
|
if (e.name.startsWith(".") && e.name !== ".") continue;
|
|
1103
|
-
const full =
|
|
1388
|
+
const full = import_node_path5.default.join(dir, e.name);
|
|
1104
1389
|
if (e.isDirectory()) {
|
|
1105
1390
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
1106
1391
|
walk(full, base, out);
|
|
1107
|
-
} else if (CODE_EXT.has(
|
|
1108
|
-
out.push(
|
|
1392
|
+
} else if (CODE_EXT.has(import_node_path5.default.extname(e.name))) {
|
|
1393
|
+
out.push(import_node_path5.default.relative(base, full));
|
|
1109
1394
|
}
|
|
1110
1395
|
}
|
|
1111
1396
|
}
|
|
1397
|
+
function hasInlineDataArray(content) {
|
|
1398
|
+
return /(?:export\s+)?const\s+\w+\s*(?::[^=\n]+)?=\s*\[\s*\{/.test(content);
|
|
1399
|
+
}
|
|
1112
1400
|
function collectFiles(frontendDir) {
|
|
1113
1401
|
const all = [];
|
|
1114
1402
|
walk(frontendDir, frontendDir, all);
|
|
1115
1403
|
const tree = all.slice().sort();
|
|
1116
|
-
const
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
for (const { rel } of ranked) {
|
|
1120
|
-
if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
|
|
1404
|
+
const scored = [];
|
|
1405
|
+
for (const rel of all) {
|
|
1406
|
+
let content;
|
|
1121
1407
|
try {
|
|
1122
|
-
|
|
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;
|
|
1408
|
+
content = import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, rel), "utf8");
|
|
1127
1409
|
} catch {
|
|
1410
|
+
continue;
|
|
1128
1411
|
}
|
|
1412
|
+
const dataArray = hasInlineDataArray(content);
|
|
1413
|
+
const hasExport = /export\s+(const|default|type|interface)/.test(content);
|
|
1414
|
+
if (!hasExport && !dataArray) continue;
|
|
1415
|
+
let s = score(rel);
|
|
1416
|
+
if (dataArray) s += 8;
|
|
1417
|
+
if (s <= 0) continue;
|
|
1418
|
+
scored.push({ rel, content, s });
|
|
1419
|
+
}
|
|
1420
|
+
scored.sort((a, b) => b.s - a.s);
|
|
1421
|
+
const files = [];
|
|
1422
|
+
let total = 0;
|
|
1423
|
+
for (const it of scored) {
|
|
1424
|
+
if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
|
|
1425
|
+
let content = it.content;
|
|
1426
|
+
if (content.length > MAX_FILE_CHARS) content = content.slice(0, MAX_FILE_CHARS) + "\n/* \u2026truncado\u2026 */";
|
|
1427
|
+
files.push({ rel: it.rel, content });
|
|
1428
|
+
total += content.length;
|
|
1129
1429
|
}
|
|
1130
1430
|
return { files, tree };
|
|
1131
1431
|
}
|
|
@@ -1160,7 +1460,7 @@ function collectPageTexts(frontendDir) {
|
|
|
1160
1460
|
for (const rel of ranked) {
|
|
1161
1461
|
if (out.length >= MAX_TEXT_FILES) break;
|
|
1162
1462
|
try {
|
|
1163
|
-
const texts = extractTexts(
|
|
1463
|
+
const texts = extractTexts(import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, rel), "utf8"));
|
|
1164
1464
|
if (texts.length) out.push({ rel, texts });
|
|
1165
1465
|
} catch {
|
|
1166
1466
|
}
|
|
@@ -1248,9 +1548,9 @@ function buildPageContentTypes(pageTexts, budget) {
|
|
|
1248
1548
|
}
|
|
1249
1549
|
return { contentTypes, seed };
|
|
1250
1550
|
}
|
|
1251
|
-
function
|
|
1551
|
+
function detectFramework2(frontendDir) {
|
|
1252
1552
|
try {
|
|
1253
|
-
const pkg = JSON.parse(
|
|
1553
|
+
const pkg = JSON.parse(import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, "package.json"), "utf8"));
|
|
1254
1554
|
const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
1255
1555
|
if (deps.next) return "next";
|
|
1256
1556
|
if (deps["@tanstack/react-start"]) return "tanstack";
|
|
@@ -1294,13 +1594,15 @@ Gere um JSON "strapi.manifest.json" com ESTE formato:
|
|
|
1294
1594
|
|
|
1295
1595
|
REGRAS:
|
|
1296
1596
|
- Crie uma content-type para cada COLE\xC7\xC3O de dados (arrays de objetos). Use os MESMOS nomes de campo do c\xF3digo.
|
|
1597
|
+
- 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.
|
|
1598
|
+
- 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
1599
|
- Dados de "configura\xE7\xE3o do site" (objeto \xFAnico: nome, telefone, etc.) \u2192 singleType.
|
|
1298
1600
|
- Campos string longos/descri\xE7\xF5es \u2192 "text" ou "richtext". Listas de strings \u2192 "json".
|
|
1299
1601
|
- 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
1602
|
- 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",
|
|
1603
|
+
- 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
1604
|
- 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.
|
|
1605
|
+
- 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
1606
|
- Se n\xE3o houver cole\xE7\xF5es de dados, devolva contentTypes: [] e seed: [].
|
|
1305
1607
|
- Responda APENAS com o JSON, nada de markdown.
|
|
1306
1608
|
|
|
@@ -1311,7 +1613,7 @@ Arquivos de dados (cole\xE7\xF5es):
|
|
|
1311
1613
|
${filesBlock}`;
|
|
1312
1614
|
}
|
|
1313
1615
|
async function inferManifest(strapi, frontendDir, opts) {
|
|
1314
|
-
const framework =
|
|
1616
|
+
const framework = detectFramework2(frontendDir);
|
|
1315
1617
|
const result = {
|
|
1316
1618
|
ok: false,
|
|
1317
1619
|
inferred: true,
|
|
@@ -1320,10 +1622,10 @@ async function inferManifest(strapi, frontendDir, opts) {
|
|
|
1320
1622
|
warnings: [],
|
|
1321
1623
|
errors: []
|
|
1322
1624
|
};
|
|
1323
|
-
const existing =
|
|
1324
|
-
if (
|
|
1625
|
+
const existing = import_node_path5.default.join(frontendDir, "strapi.manifest.json");
|
|
1626
|
+
if (import_node_fs5.default.existsSync(existing)) {
|
|
1325
1627
|
try {
|
|
1326
|
-
const raw = JSON.parse(
|
|
1628
|
+
const raw = JSON.parse(import_node_fs5.default.readFileSync(existing, "utf8"));
|
|
1327
1629
|
const v2 = validateManifest(raw);
|
|
1328
1630
|
result.inferred = false;
|
|
1329
1631
|
result.rawManifest = raw;
|
|
@@ -1406,6 +1708,48 @@ async function inferManifest(strapi, frontendDir, opts) {
|
|
|
1406
1708
|
} else if (!apiKey) {
|
|
1407
1709
|
result.warnings.push("Sem OPENAI_API_KEY: modelando os TEXTOS (determin\xEDstico); cole\xE7\xF5es de dados n\xE3o inferidas.");
|
|
1408
1710
|
}
|
|
1711
|
+
if (dataCts.length && dataSeed.length) {
|
|
1712
|
+
const parts = [];
|
|
1713
|
+
for (const f of collected.files) {
|
|
1714
|
+
try {
|
|
1715
|
+
parts.push(import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, f.rel), "utf8"));
|
|
1716
|
+
} catch {
|
|
1717
|
+
parts.push(f.content);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
const norm = (x) => String(x).toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
1721
|
+
const hay = norm(parts.join("\n"));
|
|
1722
|
+
const present = (v2) => {
|
|
1723
|
+
if (typeof v2 !== "string") return false;
|
|
1724
|
+
const n = norm(v2);
|
|
1725
|
+
return n.length >= 4 && hay.includes(n);
|
|
1726
|
+
};
|
|
1727
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1728
|
+
const verifiedSeed = [];
|
|
1729
|
+
let droppedEntries = 0;
|
|
1730
|
+
for (const grp of dataSeed) {
|
|
1731
|
+
const entries = (grp.entries ?? []).filter((e) => {
|
|
1732
|
+
const ok = Object.values(e).some(present);
|
|
1733
|
+
if (!ok) droppedEntries++;
|
|
1734
|
+
return ok;
|
|
1735
|
+
});
|
|
1736
|
+
if (entries.length) {
|
|
1737
|
+
verifiedSeed.push({ ...grp, entries });
|
|
1738
|
+
keep.add(grp.singularName);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
const verifiedCts = dataCts.filter(
|
|
1742
|
+
(ct) => ct.kind === "singleType" || keep.has(ct.singularName)
|
|
1743
|
+
);
|
|
1744
|
+
const droppedCts = dataCts.length - verifiedCts.length;
|
|
1745
|
+
if (droppedEntries || droppedCts) {
|
|
1746
|
+
result.warnings.push(
|
|
1747
|
+
`Anti-alucina\xE7\xE3o: descartei ${droppedEntries} entrada(s) e ${droppedCts} content-type(s) cujos valores n\xE3o batiam com o c\xF3digo.`
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
dataCts = verifiedCts;
|
|
1751
|
+
dataSeed = verifiedSeed;
|
|
1752
|
+
}
|
|
1409
1753
|
const budget = 60 - dataCts.length - 1;
|
|
1410
1754
|
const page = buildPageContentTypes(pageTexts, budget);
|
|
1411
1755
|
const finalManifest = {
|
|
@@ -1434,132 +1778,6 @@ async function inferManifest(strapi, frontendDir, opts) {
|
|
|
1434
1778
|
return result;
|
|
1435
1779
|
}
|
|
1436
1780
|
|
|
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
1781
|
// server/src/provision/integrate.ts
|
|
1564
1782
|
var import_node_fs6 = __toESM(require("node:fs"));
|
|
1565
1783
|
var import_node_path6 = __toESM(require("node:path"));
|
|
@@ -2627,9 +2845,10 @@ function createContentTools(strapi) {
|
|
|
2627
2845
|
}
|
|
2628
2846
|
};
|
|
2629
2847
|
const MAX_MATCHES = 100;
|
|
2630
|
-
const buscarTexto = async (termo) => {
|
|
2848
|
+
const buscarTexto = async (termo, status = "draft") => {
|
|
2631
2849
|
const needle = String(termo || "").toLowerCase().trim();
|
|
2632
2850
|
if (!needle) return { erro: "termo vazio" };
|
|
2851
|
+
const st = status === "published" ? "published" : "draft";
|
|
2633
2852
|
const matches = [];
|
|
2634
2853
|
for (const ct of apiContentTypes()) {
|
|
2635
2854
|
if (matches.length >= MAX_MATCHES) break;
|
|
@@ -2637,7 +2856,7 @@ function createContentTools(strapi) {
|
|
|
2637
2856
|
const populate = buildPopulate(attributes);
|
|
2638
2857
|
let entries = [];
|
|
2639
2858
|
try {
|
|
2640
|
-
const res = await strapi.documents(ct.uid).findMany({ status:
|
|
2859
|
+
const res = await strapi.documents(ct.uid).findMany({ status: st, populate, limit: 200 });
|
|
2641
2860
|
entries = Array.isArray(res) ? res : res ? [res] : [];
|
|
2642
2861
|
} catch {
|
|
2643
2862
|
continue;
|
|
@@ -3152,7 +3371,7 @@ If the user shares their screen, an image is attached to the last message \u2014
|
|
|
3152
3371
|
Be concise and actionable. ALWAYS answer in English.`
|
|
3153
3372
|
};
|
|
3154
3373
|
var chat_default2 = ({ strapi }) => ({
|
|
3155
|
-
async chat({ messages, image, lang = "pt", previewUrl, autoPublish = false }) {
|
|
3374
|
+
async chat({ messages, image, lang = "pt", previewUrl, previewStatus = "draft", autoPublish = false }) {
|
|
3156
3375
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
3157
3376
|
if (!apiKey) {
|
|
3158
3377
|
throw new Error(
|
|
@@ -3162,7 +3381,7 @@ var chat_default2 = ({ strapi }) => ({
|
|
|
3162
3381
|
const language = lang === "en" ? "en" : "pt";
|
|
3163
3382
|
const { buscarTexto, editarCampo, publicar, listarLocales, criarLocale, traduzir } = createContentTools(strapi);
|
|
3164
3383
|
const LOCAL_TOOLS = {
|
|
3165
|
-
buscar_texto: (a) => buscarTexto(a?.termo),
|
|
3384
|
+
buscar_texto: (a) => buscarTexto(a?.termo, previewStatus),
|
|
3166
3385
|
editar_campo: (a) => editarCampo(a),
|
|
3167
3386
|
publicar: (a) => publicar(a),
|
|
3168
3387
|
listar_locales: () => listarLocales(),
|
|
@@ -3640,6 +3859,10 @@ var register_default = ({ strapi }) => {
|
|
|
3640
3859
|
var index_default = {
|
|
3641
3860
|
register: register_default,
|
|
3642
3861
|
async bootstrap({ strapi }) {
|
|
3862
|
+
try {
|
|
3863
|
+
cleanupStaleFrontend(strapi.dirs.app.root);
|
|
3864
|
+
} catch {
|
|
3865
|
+
}
|
|
3643
3866
|
try {
|
|
3644
3867
|
const r = await runPendingProvision(strapi, strapi.dirs.app.root);
|
|
3645
3868
|
if (r.ran) {
|