@synchronized-studio/cmsassets-agent 0.6.0 → 0.6.2
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/dist/aiReview-RGKHFUIW.js +9 -0
- package/dist/chunk-3D6MPYQN.js +88 -0
- package/dist/chunk-BHI66HZ7.js +492 -0
- package/dist/chunk-DJEPQELD.js +2052 -0
- package/dist/chunk-XXNYL6ZI.js +2002 -0
- package/dist/cli.js +8 -8
- package/dist/fileFromPath-BV3LA72M.js +129 -0
- package/dist/index.js +4 -4
- package/dist/openai-KWKRAYIT.js +10931 -0
- package/dist/openaiClient-ZGXSVDHJ.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2002 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WRAP_TEMPLATES,
|
|
3
|
+
buildCmsOptions,
|
|
4
|
+
findInjectionPoints,
|
|
5
|
+
getImportStatement,
|
|
6
|
+
getTransformFunctionName
|
|
7
|
+
} from "./chunk-W7TNFULQ.js";
|
|
8
|
+
import {
|
|
9
|
+
chatCompletion,
|
|
10
|
+
isOpenAiError
|
|
11
|
+
} from "./chunk-3D6MPYQN.js";
|
|
12
|
+
|
|
13
|
+
// src/scanner/index.ts
|
|
14
|
+
import { resolve } from "path";
|
|
15
|
+
|
|
16
|
+
// src/scanner/detectFramework.ts
|
|
17
|
+
import { existsSync, readFileSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
var FRAMEWORK_SIGNATURES = {
|
|
20
|
+
nuxt: {
|
|
21
|
+
packages: ["nuxt"],
|
|
22
|
+
configFiles: ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"]
|
|
23
|
+
},
|
|
24
|
+
next: {
|
|
25
|
+
packages: ["next"],
|
|
26
|
+
configFiles: ["next.config.js", "next.config.mjs", "next.config.ts"]
|
|
27
|
+
},
|
|
28
|
+
remix: {
|
|
29
|
+
packages: ["@remix-run/react", "@remix-run/node", "@remix-run/dev"],
|
|
30
|
+
configFiles: ["remix.config.js", "remix.config.ts"]
|
|
31
|
+
},
|
|
32
|
+
astro: {
|
|
33
|
+
packages: ["astro"],
|
|
34
|
+
configFiles: ["astro.config.mjs", "astro.config.ts", "astro.config.js"]
|
|
35
|
+
},
|
|
36
|
+
sveltekit: {
|
|
37
|
+
packages: ["@sveltejs/kit"],
|
|
38
|
+
configFiles: ["svelte.config.js", "svelte.config.ts"]
|
|
39
|
+
},
|
|
40
|
+
hono: {
|
|
41
|
+
packages: ["hono"],
|
|
42
|
+
configFiles: []
|
|
43
|
+
},
|
|
44
|
+
fastify: {
|
|
45
|
+
packages: ["fastify"],
|
|
46
|
+
configFiles: []
|
|
47
|
+
},
|
|
48
|
+
express: {
|
|
49
|
+
packages: ["express"],
|
|
50
|
+
configFiles: []
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var DETECTION_ORDER = [
|
|
54
|
+
"nuxt",
|
|
55
|
+
"next",
|
|
56
|
+
"remix",
|
|
57
|
+
"astro",
|
|
58
|
+
"sveltekit",
|
|
59
|
+
"hono",
|
|
60
|
+
"fastify",
|
|
61
|
+
"express"
|
|
62
|
+
];
|
|
63
|
+
function readPackageJson(root) {
|
|
64
|
+
const pkgPath = join(root, "package.json");
|
|
65
|
+
if (!existsSync(pkgPath)) return null;
|
|
66
|
+
try {
|
|
67
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
68
|
+
const pkg = JSON.parse(raw);
|
|
69
|
+
return {
|
|
70
|
+
dependencies: pkg.dependencies ?? {},
|
|
71
|
+
devDependencies: pkg.devDependencies ?? {}
|
|
72
|
+
};
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function getVersionFromPkg(allDeps, packages) {
|
|
78
|
+
for (const pkg of packages) {
|
|
79
|
+
if (allDeps[pkg]) return allDeps[pkg];
|
|
80
|
+
}
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
function detectFramework(root) {
|
|
84
|
+
const pkg = readPackageJson(root);
|
|
85
|
+
const allDeps = {
|
|
86
|
+
...pkg?.dependencies ?? {},
|
|
87
|
+
...pkg?.devDependencies ?? {}
|
|
88
|
+
};
|
|
89
|
+
for (const name of DETECTION_ORDER) {
|
|
90
|
+
const sig = FRAMEWORK_SIGNATURES[name];
|
|
91
|
+
const hasPackage = sig.packages.some((p) => p in allDeps);
|
|
92
|
+
const configFile = sig.configFiles.find((f) => existsSync(join(root, f))) ?? null;
|
|
93
|
+
if (hasPackage || configFile) {
|
|
94
|
+
let resolvedName = name;
|
|
95
|
+
const version = getVersionFromPkg(allDeps, sig.packages);
|
|
96
|
+
if (name === "nuxt") {
|
|
97
|
+
const majorVersion = parseMajorVersion(version);
|
|
98
|
+
if (majorVersion !== null && majorVersion < 3) {
|
|
99
|
+
resolvedName = "nuxt2";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
name: resolvedName,
|
|
104
|
+
version,
|
|
105
|
+
configFile
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { name: "unknown", version: "", configFile: null };
|
|
110
|
+
}
|
|
111
|
+
function parseMajorVersion(versionRange) {
|
|
112
|
+
const cleaned = versionRange.replace(/^[\^~>=<\s]+/, "");
|
|
113
|
+
const major = parseInt(cleaned, 10);
|
|
114
|
+
return Number.isFinite(major) ? major : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/scanner/detectCms.ts
|
|
118
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
119
|
+
import { join as join2, relative } from "path";
|
|
120
|
+
import fg from "fast-glob";
|
|
121
|
+
var CMS_SIGNATURES = {
|
|
122
|
+
prismic: {
|
|
123
|
+
packages: [
|
|
124
|
+
"@prismicio/client",
|
|
125
|
+
"@prismicio/vue",
|
|
126
|
+
"@prismicio/react",
|
|
127
|
+
"@prismicio/next",
|
|
128
|
+
"@prismicio/svelte",
|
|
129
|
+
"@nuxtjs/prismic"
|
|
130
|
+
],
|
|
131
|
+
urlPatterns: [
|
|
132
|
+
/https?:\/\/([a-z0-9-]+)\.cdn\.prismic\.io/gi,
|
|
133
|
+
/https?:\/\/images\.prismic\.io\/([a-z0-9-]+)/gi
|
|
134
|
+
],
|
|
135
|
+
paramExtractors: {
|
|
136
|
+
repository: (match) => {
|
|
137
|
+
const m1 = match.match(/https?:\/\/([a-z0-9-]+)\.cdn\.prismic\.io/);
|
|
138
|
+
if (m1) return m1[1];
|
|
139
|
+
const m2 = match.match(/https?:\/\/images\.prismic\.io\/([a-z0-9-]+)/);
|
|
140
|
+
if (m2) return m2[1];
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
contentful: {
|
|
146
|
+
packages: ["contentful"],
|
|
147
|
+
urlPatterns: [
|
|
148
|
+
/https?:\/\/images\.ctfassets\.net\/([a-z0-9]+)/gi,
|
|
149
|
+
/https?:\/\/videos\.ctfassets\.net\/([a-z0-9]+)/gi,
|
|
150
|
+
/https?:\/\/cdn\.contentful\.com\/spaces\/([a-z0-9]+)/gi
|
|
151
|
+
],
|
|
152
|
+
paramExtractors: {
|
|
153
|
+
spaceId: (match) => {
|
|
154
|
+
const m = match.match(/ctfassets\.net\/([a-z0-9]+)/) || match.match(/spaces\/([a-z0-9]+)/);
|
|
155
|
+
return m?.[1] ?? null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
sanity: {
|
|
160
|
+
packages: ["@sanity/client", "next-sanity", "@nuxtjs/sanity", "sanity"],
|
|
161
|
+
urlPatterns: [
|
|
162
|
+
/https?:\/\/cdn\.sanity\.io\/(images|files)\/([a-z0-9]+)\/([a-z0-9-]+)/gi
|
|
163
|
+
],
|
|
164
|
+
paramExtractors: {
|
|
165
|
+
projectId: (match) => {
|
|
166
|
+
const m = match.match(/cdn\.sanity\.io\/(?:images|files)\/([a-z0-9]+)/);
|
|
167
|
+
return m?.[1] ?? null;
|
|
168
|
+
},
|
|
169
|
+
dataset: (match) => {
|
|
170
|
+
const m = match.match(
|
|
171
|
+
/cdn\.sanity\.io\/(?:images|files)\/[a-z0-9]+\/([a-z0-9-]+)/
|
|
172
|
+
);
|
|
173
|
+
return m?.[1] ?? null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
shopify: {
|
|
178
|
+
packages: [
|
|
179
|
+
"@shopify/storefront-api-client",
|
|
180
|
+
"@shopify/hydrogen",
|
|
181
|
+
"@shopify/shopify-api"
|
|
182
|
+
],
|
|
183
|
+
urlPatterns: [
|
|
184
|
+
/https?:\/\/([a-z0-9-]+)\.myshopify\.com\/cdn\//gi,
|
|
185
|
+
/https?:\/\/([a-z0-9-]+)\.myshopify\.com\/api\//gi
|
|
186
|
+
],
|
|
187
|
+
paramExtractors: {
|
|
188
|
+
storeDomain: (match) => {
|
|
189
|
+
const m = match.match(/https?:\/\/([a-z0-9-]+\.myshopify\.com)/);
|
|
190
|
+
return m?.[1] ?? null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
cloudinary: {
|
|
195
|
+
packages: ["cloudinary", "@cloudinary/url-gen", "@cloudinary/react"],
|
|
196
|
+
urlPatterns: [/https?:\/\/res\.cloudinary\.com\/([a-z0-9_-]+)/gi],
|
|
197
|
+
paramExtractors: {
|
|
198
|
+
cloudName: (match) => {
|
|
199
|
+
const m = match.match(/res\.cloudinary\.com\/([a-z0-9_-]+)/);
|
|
200
|
+
return m?.[1] ?? null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
imgix: {
|
|
205
|
+
packages: ["@imgix/js-core", "react-imgix", "vue-imgix"],
|
|
206
|
+
urlPatterns: [/https?:\/\/([a-z0-9-]+)\.imgix\.net/gi],
|
|
207
|
+
paramExtractors: {
|
|
208
|
+
imgixDomain: (match) => {
|
|
209
|
+
const m = match.match(/https?:\/\/([a-z0-9-]+\.imgix\.net)/);
|
|
210
|
+
return m?.[1] ?? null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
var CMS_DETECTION_ORDER = [
|
|
216
|
+
"prismic",
|
|
217
|
+
"contentful",
|
|
218
|
+
"sanity",
|
|
219
|
+
"shopify",
|
|
220
|
+
"cloudinary",
|
|
221
|
+
"imgix"
|
|
222
|
+
];
|
|
223
|
+
var SOURCE_GLOBS = [
|
|
224
|
+
"**/*.ts",
|
|
225
|
+
"**/*.tsx",
|
|
226
|
+
"**/*.js",
|
|
227
|
+
"**/*.jsx",
|
|
228
|
+
"**/*.vue",
|
|
229
|
+
"**/*.svelte",
|
|
230
|
+
"**/*.astro",
|
|
231
|
+
"**/*.mjs",
|
|
232
|
+
"**/*.mts"
|
|
233
|
+
];
|
|
234
|
+
var IGNORE_DIRS = [
|
|
235
|
+
"node_modules",
|
|
236
|
+
".nuxt",
|
|
237
|
+
".next",
|
|
238
|
+
".output",
|
|
239
|
+
".svelte-kit",
|
|
240
|
+
"dist",
|
|
241
|
+
"build",
|
|
242
|
+
".git",
|
|
243
|
+
"coverage",
|
|
244
|
+
".cache"
|
|
245
|
+
];
|
|
246
|
+
function detectCms(root) {
|
|
247
|
+
const pkgPath = join2(root, "package.json");
|
|
248
|
+
let allDeps = {};
|
|
249
|
+
if (existsSync2(pkgPath)) {
|
|
250
|
+
try {
|
|
251
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
252
|
+
allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (const cmsName of CMS_DETECTION_ORDER) {
|
|
257
|
+
const sig = CMS_SIGNATURES[cmsName];
|
|
258
|
+
if (sig.packages.some((p) => p in allDeps)) {
|
|
259
|
+
const params = extractParamsFromSource(root, cmsName);
|
|
260
|
+
return {
|
|
261
|
+
type: cmsName,
|
|
262
|
+
params,
|
|
263
|
+
detectedFrom: ["package.json"]
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const sourceFiles = fg.sync(SOURCE_GLOBS, {
|
|
268
|
+
cwd: root,
|
|
269
|
+
ignore: IGNORE_DIRS.map((d) => `${d}/**`),
|
|
270
|
+
absolute: true,
|
|
271
|
+
onlyFiles: true,
|
|
272
|
+
deep: 6
|
|
273
|
+
});
|
|
274
|
+
for (const cmsName of CMS_DETECTION_ORDER) {
|
|
275
|
+
const sig = CMS_SIGNATURES[cmsName];
|
|
276
|
+
const matchingFiles = [];
|
|
277
|
+
const foundParams = {};
|
|
278
|
+
for (const file of sourceFiles) {
|
|
279
|
+
let content;
|
|
280
|
+
try {
|
|
281
|
+
content = readFileSync2(file, "utf-8");
|
|
282
|
+
} catch {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
for (const pattern of sig.urlPatterns) {
|
|
286
|
+
pattern.lastIndex = 0;
|
|
287
|
+
const matches = content.match(pattern);
|
|
288
|
+
if (matches && matches.length > 0) {
|
|
289
|
+
matchingFiles.push(relative(root, file));
|
|
290
|
+
for (const m of matches) {
|
|
291
|
+
for (const [paramName, extractor] of Object.entries(sig.paramExtractors)) {
|
|
292
|
+
if (!foundParams[paramName]) {
|
|
293
|
+
const val = extractor(m);
|
|
294
|
+
if (val) foundParams[paramName] = val;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (matchingFiles.length > 0) {
|
|
302
|
+
return {
|
|
303
|
+
type: cmsName,
|
|
304
|
+
params: foundParams,
|
|
305
|
+
detectedFrom: [...new Set(matchingFiles)]
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const genericPatterns = [
|
|
310
|
+
/https?:\/\/[a-z0-9-]+\.s3[.-][a-z0-9-]*\.amazonaws\.com/gi,
|
|
311
|
+
/https?:\/\/storage\.googleapis\.com\/[a-z0-9-]+/gi,
|
|
312
|
+
/https?:\/\/[a-z0-9-]+\.r2\.cloudflarestorage\.com/gi
|
|
313
|
+
];
|
|
314
|
+
for (const file of sourceFiles) {
|
|
315
|
+
let content;
|
|
316
|
+
try {
|
|
317
|
+
content = readFileSync2(file, "utf-8");
|
|
318
|
+
} catch {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
for (const pattern of genericPatterns) {
|
|
322
|
+
pattern.lastIndex = 0;
|
|
323
|
+
const m = content.match(pattern);
|
|
324
|
+
if (m && m.length > 0) {
|
|
325
|
+
return {
|
|
326
|
+
type: "generic",
|
|
327
|
+
params: { originUrl: m[0] },
|
|
328
|
+
detectedFrom: [relative(root, file)]
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return { type: "unknown", params: {}, detectedFrom: [] };
|
|
334
|
+
}
|
|
335
|
+
var CONFIG_FILES = [
|
|
336
|
+
"slicemachine.config.json",
|
|
337
|
+
"prismicio.config.ts",
|
|
338
|
+
"prismicio.config.js",
|
|
339
|
+
"sm.json",
|
|
340
|
+
".slicemachine.config.json",
|
|
341
|
+
"nuxt.config.ts",
|
|
342
|
+
"nuxt.config.js",
|
|
343
|
+
"next.config.js",
|
|
344
|
+
"next.config.mjs",
|
|
345
|
+
"next.config.ts",
|
|
346
|
+
"sanity.config.ts",
|
|
347
|
+
"sanity.config.js",
|
|
348
|
+
"sanity.cli.ts",
|
|
349
|
+
"sanity.cli.js"
|
|
350
|
+
];
|
|
351
|
+
var CONFIG_PARAM_PATTERNS = {
|
|
352
|
+
prismic: [
|
|
353
|
+
{ param: "repository", patterns: [
|
|
354
|
+
/["']?repositoryName["']?\s*[:=]\s*["']([a-z0-9-]+)["']/i,
|
|
355
|
+
/https?:\/\/([a-z0-9-]+)\.cdn\.prismic\.io/i,
|
|
356
|
+
/["']?apiEndpoint["']?\s*[:=]\s*["']https?:\/\/([a-z0-9-]+)\.cdn\.prismic\.io/i
|
|
357
|
+
] }
|
|
358
|
+
],
|
|
359
|
+
contentful: [
|
|
360
|
+
{ param: "spaceId", patterns: [
|
|
361
|
+
/["']?space["']?\s*[:=]\s*["']([a-z0-9]+)["']/i,
|
|
362
|
+
/["']?spaceId["']?\s*[:=]\s*["']([a-z0-9]+)["']/i
|
|
363
|
+
] }
|
|
364
|
+
],
|
|
365
|
+
sanity: [
|
|
366
|
+
{ param: "projectId", patterns: [
|
|
367
|
+
/["']?projectId["']?\s*[:=]\s*["']([a-z0-9]+)["']/i
|
|
368
|
+
] },
|
|
369
|
+
{ param: "dataset", patterns: [
|
|
370
|
+
/["']?dataset["']?\s*[:=]\s*["']([a-z0-9-]+)["']/i
|
|
371
|
+
] }
|
|
372
|
+
],
|
|
373
|
+
shopify: [
|
|
374
|
+
{ param: "storeDomain", patterns: [
|
|
375
|
+
/["']?storeDomain["']?\s*[:=]\s*["']([a-z0-9-]+\.myshopify\.com)["']/i
|
|
376
|
+
] }
|
|
377
|
+
],
|
|
378
|
+
cloudinary: [
|
|
379
|
+
{ param: "cloudName", patterns: [
|
|
380
|
+
/["']?cloud_name["']?\s*[:=]\s*["']([a-z0-9_-]+)["']/i,
|
|
381
|
+
/["']?cloudName["']?\s*[:=]\s*["']([a-z0-9_-]+)["']/i
|
|
382
|
+
] }
|
|
383
|
+
],
|
|
384
|
+
imgix: [
|
|
385
|
+
{ param: "imgixDomain", patterns: [
|
|
386
|
+
/["']?domain["']?\s*[:=]\s*["']([a-z0-9-]+\.imgix\.net)["']/i
|
|
387
|
+
] }
|
|
388
|
+
]
|
|
389
|
+
};
|
|
390
|
+
function extractParamsFromSource(root, cmsName) {
|
|
391
|
+
const sig = CMS_SIGNATURES[cmsName];
|
|
392
|
+
if (!sig) return {};
|
|
393
|
+
const params = {};
|
|
394
|
+
const configPatterns = CONFIG_PARAM_PATTERNS[cmsName];
|
|
395
|
+
if (configPatterns) {
|
|
396
|
+
for (const cfgFile of CONFIG_FILES) {
|
|
397
|
+
const cfgPath = join2(root, cfgFile);
|
|
398
|
+
if (!existsSync2(cfgPath)) continue;
|
|
399
|
+
let content;
|
|
400
|
+
try {
|
|
401
|
+
content = readFileSync2(cfgPath, "utf-8");
|
|
402
|
+
} catch {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
for (const { param, patterns } of configPatterns) {
|
|
406
|
+
if (params[param]) continue;
|
|
407
|
+
for (const pattern of patterns) {
|
|
408
|
+
const m = content.match(pattern);
|
|
409
|
+
if (m?.[1]) {
|
|
410
|
+
params[param] = m[1];
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (Object.keys(params).length === Object.keys(sig.paramExtractors).length) {
|
|
417
|
+
return params;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const sourceFiles = fg.sync(SOURCE_GLOBS, {
|
|
421
|
+
cwd: root,
|
|
422
|
+
ignore: IGNORE_DIRS.map((d) => `${d}/**`),
|
|
423
|
+
absolute: true,
|
|
424
|
+
onlyFiles: true,
|
|
425
|
+
deep: 6
|
|
426
|
+
});
|
|
427
|
+
for (const file of sourceFiles) {
|
|
428
|
+
let content;
|
|
429
|
+
try {
|
|
430
|
+
content = readFileSync2(file, "utf-8");
|
|
431
|
+
} catch {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
for (const pattern of sig.urlPatterns) {
|
|
435
|
+
pattern.lastIndex = 0;
|
|
436
|
+
const matches = content.match(pattern);
|
|
437
|
+
if (!matches) continue;
|
|
438
|
+
for (const m of matches) {
|
|
439
|
+
for (const [paramName, extractor] of Object.entries(sig.paramExtractors)) {
|
|
440
|
+
if (!params[paramName]) {
|
|
441
|
+
const val = extractor(m);
|
|
442
|
+
if (val) params[paramName] = val;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (Object.keys(params).length === Object.keys(sig.paramExtractors).length) {
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return params;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/scanner/detectPackageManager.ts
|
|
455
|
+
import { existsSync as existsSync3 } from "fs";
|
|
456
|
+
import { join as join3 } from "path";
|
|
457
|
+
var LOCKFILE_MAP = [
|
|
458
|
+
["bun.lockb", "bun"],
|
|
459
|
+
["bun.lock", "bun"],
|
|
460
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
461
|
+
["yarn.lock", "yarn"],
|
|
462
|
+
["package-lock.json", "npm"]
|
|
463
|
+
];
|
|
464
|
+
function detectPackageManager(root) {
|
|
465
|
+
for (const [lockfile, pm] of LOCKFILE_MAP) {
|
|
466
|
+
if (existsSync3(join3(root, lockfile))) return pm;
|
|
467
|
+
}
|
|
468
|
+
return "npm";
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/scanner/index.ts
|
|
472
|
+
function scan(projectRoot) {
|
|
473
|
+
const root = resolve(projectRoot ?? process.cwd());
|
|
474
|
+
const framework = detectFramework(root);
|
|
475
|
+
const cms = detectCms(root);
|
|
476
|
+
const packageManager = detectPackageManager(root);
|
|
477
|
+
const { candidates: injectionPoints, candidateGraph } = findInjectionPoints(root, framework.name, cms);
|
|
478
|
+
return {
|
|
479
|
+
framework,
|
|
480
|
+
cms,
|
|
481
|
+
injectionPoints,
|
|
482
|
+
candidateGraph,
|
|
483
|
+
packageManager,
|
|
484
|
+
projectRoot: root
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/planner/index.ts
|
|
489
|
+
import { existsSync as existsSync4 } from "fs";
|
|
490
|
+
import { join as join5 } from "path";
|
|
491
|
+
|
|
492
|
+
// src/patcher/structuralValidator.ts
|
|
493
|
+
import { Project, SyntaxKind, ScriptKind } from "ts-morph";
|
|
494
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
495
|
+
import { join as join4, extname } from "path";
|
|
496
|
+
var TRANSFORMER_PKG = "@synchronized-studio/response-transformer";
|
|
497
|
+
function takeSnapshot(root, filePath) {
|
|
498
|
+
const absPath = join4(root, filePath);
|
|
499
|
+
const ext = extname(filePath);
|
|
500
|
+
let content;
|
|
501
|
+
try {
|
|
502
|
+
content = readFileSync3(absPath, "utf-8");
|
|
503
|
+
} catch {
|
|
504
|
+
return { exports: [], functions: [], variables: [], imports: [], content: "", syntaxValid: false };
|
|
505
|
+
}
|
|
506
|
+
if ([".vue", ".svelte", ".astro"].includes(ext)) {
|
|
507
|
+
return takeSnapshotFromString(content, ext);
|
|
508
|
+
}
|
|
509
|
+
return takeSnapshotFromAst(content, filePath);
|
|
510
|
+
}
|
|
511
|
+
function takeSnapshotFromAst(content, filePath) {
|
|
512
|
+
try {
|
|
513
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
514
|
+
const ext = extname(filePath);
|
|
515
|
+
const scriptKind = ext === ".tsx" || ext === ".jsx" ? ScriptKind.TSX : ScriptKind.TS;
|
|
516
|
+
const sourceFile = project.createSourceFile("__snapshot__.tsx", content, { scriptKind });
|
|
517
|
+
const exports = sourceFile.getExportedDeclarations();
|
|
518
|
+
const exportNames = [...exports.keys()];
|
|
519
|
+
const functions = sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).map((f) => f.getName()).filter((n) => !!n);
|
|
520
|
+
const arrowFunctions = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration).filter((v) => v.getInitializerIfKind(SyntaxKind.ArrowFunction)).map((v) => v.getName());
|
|
521
|
+
const allFunctions = [.../* @__PURE__ */ new Set([...functions, ...arrowFunctions])];
|
|
522
|
+
const variables = sourceFile.getVariableDeclarations().map((v) => v.getName()).filter((name) => !allFunctions.includes(name));
|
|
523
|
+
const imports = sourceFile.getImportDeclarations().map((i) => i.getModuleSpecifierValue());
|
|
524
|
+
return {
|
|
525
|
+
exports: exportNames,
|
|
526
|
+
functions: allFunctions,
|
|
527
|
+
variables,
|
|
528
|
+
imports,
|
|
529
|
+
content,
|
|
530
|
+
syntaxValid: true
|
|
531
|
+
};
|
|
532
|
+
} catch {
|
|
533
|
+
return {
|
|
534
|
+
exports: [],
|
|
535
|
+
functions: [],
|
|
536
|
+
variables: [],
|
|
537
|
+
imports: [],
|
|
538
|
+
content,
|
|
539
|
+
syntaxValid: false
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function takeSnapshotFromString(content, ext) {
|
|
544
|
+
const scriptContent = extractScriptBlock(content, ext);
|
|
545
|
+
if (!scriptContent) {
|
|
546
|
+
return { exports: [], functions: [], variables: [], imports: [], content, syntaxValid: true };
|
|
547
|
+
}
|
|
548
|
+
const exports = extractPatterns(scriptContent, /export\s+(?:default\s+)?(?:async\s+)?(?:function|const|let|var|class)\s+(\w+)/g);
|
|
549
|
+
const functions = extractPatterns(scriptContent, /(?:async\s+)?function\s+(\w+)\s*\(/g);
|
|
550
|
+
const arrowFns = extractPatterns(scriptContent, /(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/g);
|
|
551
|
+
const variables = extractPatterns(scriptContent, /(?:const|let|var)\s+(\w+)\s*=/g).filter((v) => !functions.includes(v) && !arrowFns.includes(v));
|
|
552
|
+
const imports = extractPatterns(scriptContent, /from\s+['"]([^'"]+)['"]/g);
|
|
553
|
+
return {
|
|
554
|
+
exports,
|
|
555
|
+
functions: [.../* @__PURE__ */ new Set([...functions, ...arrowFns])],
|
|
556
|
+
variables,
|
|
557
|
+
imports,
|
|
558
|
+
content,
|
|
559
|
+
syntaxValid: true
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function extractScriptBlock(content, ext) {
|
|
563
|
+
if (ext === ".astro") {
|
|
564
|
+
const match2 = content.match(/^---\n([\s\S]*?)\n---/);
|
|
565
|
+
return match2?.[1] ?? null;
|
|
566
|
+
}
|
|
567
|
+
const match = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
568
|
+
return match?.[1] ?? null;
|
|
569
|
+
}
|
|
570
|
+
function extractPatterns(content, pattern) {
|
|
571
|
+
const names = [];
|
|
572
|
+
let m;
|
|
573
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
574
|
+
if (m[1]) names.push(m[1]);
|
|
575
|
+
}
|
|
576
|
+
return names;
|
|
577
|
+
}
|
|
578
|
+
function checkFunctionValueWrapping(originalCode, snapshot) {
|
|
579
|
+
const match = originalCode.match(/^return\s+\{([^}]+)\}\s*;?\s*$/);
|
|
580
|
+
if (!match) return { valid: true, errors: [] };
|
|
581
|
+
const inner = match[1].trim();
|
|
582
|
+
const parts = inner.split(",").map((p) => p.trim()).filter(Boolean);
|
|
583
|
+
const shorthandNames = [];
|
|
584
|
+
for (const part of parts) {
|
|
585
|
+
if (/^\w+$/.test(part)) {
|
|
586
|
+
shorthandNames.push(part);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (shorthandNames.length === 0) return { valid: true, errors: [] };
|
|
590
|
+
const functionProps = shorthandNames.filter(
|
|
591
|
+
(name) => snapshot.functions.includes(name)
|
|
592
|
+
);
|
|
593
|
+
if (functionProps.length > 0) {
|
|
594
|
+
return {
|
|
595
|
+
valid: false,
|
|
596
|
+
errors: [
|
|
597
|
+
`Wrapping object containing function references (${functionProps.join(", ")}) with JSON-serializing transform would destroy them at runtime`
|
|
598
|
+
]
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
return { valid: true, errors: [] };
|
|
602
|
+
}
|
|
603
|
+
var NON_CMS_RETURN_PATTERNS = [
|
|
604
|
+
/^return\s+\{\s*query\s*[,:}]/,
|
|
605
|
+
// return { query: ... } or { query }
|
|
606
|
+
/^return\s+\{\s*path\s*:/,
|
|
607
|
+
// return { path: '...' }
|
|
608
|
+
/^return\s+\{\s*redirect\s*:/,
|
|
609
|
+
// return { redirect: ... }
|
|
610
|
+
/^return\s+\{\s*statusCode\s*:/,
|
|
611
|
+
// return { statusCode: 301 }
|
|
612
|
+
/^return\s+\{\s*headers\s*:/,
|
|
613
|
+
// return { headers: ... }
|
|
614
|
+
/^return\s+\{\s*\.\.\.\w[\w.]*[Qq]uery\b/,
|
|
615
|
+
// return { ...route.query }
|
|
616
|
+
/^return\s+\{\s*\.\.\.\w[\w.]*[Pp]arams\b/
|
|
617
|
+
// return { ...route.params }
|
|
618
|
+
];
|
|
619
|
+
function checkNonCmsReturnWrapping(originalCode) {
|
|
620
|
+
const trimmed = originalCode.trim();
|
|
621
|
+
const matched = NON_CMS_RETURN_PATTERNS.find((p) => p.test(trimmed));
|
|
622
|
+
if (matched) {
|
|
623
|
+
return {
|
|
624
|
+
valid: false,
|
|
625
|
+
errors: [
|
|
626
|
+
`Cannot wrap router/navigation/HTTP object with CMS transformer \u2014 this return produces routing state, not CMS data: ${trimmed.substring(0, 80)}`
|
|
627
|
+
]
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
return { valid: true, errors: [] };
|
|
631
|
+
}
|
|
632
|
+
function validateContentAgainstSnapshot(newContent, filePath, snapshot) {
|
|
633
|
+
const ext = extname(filePath);
|
|
634
|
+
const errors = [];
|
|
635
|
+
let newSnapshot;
|
|
636
|
+
if ([".vue", ".svelte", ".astro"].includes(ext)) {
|
|
637
|
+
newSnapshot = takeSnapshotFromString(newContent, ext);
|
|
638
|
+
} else {
|
|
639
|
+
newSnapshot = takeSnapshotFromAst(newContent, filePath);
|
|
640
|
+
}
|
|
641
|
+
if (!newSnapshot.syntaxValid) {
|
|
642
|
+
errors.push("Patched file has syntax errors and cannot be parsed");
|
|
643
|
+
return { valid: false, errors };
|
|
644
|
+
}
|
|
645
|
+
const missingExports = snapshot.exports.filter((e) => !newSnapshot.exports.includes(e));
|
|
646
|
+
if (missingExports.length > 0) {
|
|
647
|
+
errors.push(`Missing exports after patch: ${missingExports.join(", ")}`);
|
|
648
|
+
}
|
|
649
|
+
const missingFunctions = snapshot.functions.filter((f) => !newSnapshot.functions.includes(f));
|
|
650
|
+
if (missingFunctions.length > 0) {
|
|
651
|
+
errors.push(`Missing functions after patch: ${missingFunctions.join(", ")}`);
|
|
652
|
+
}
|
|
653
|
+
const missingVariables = snapshot.variables.filter((v) => !newSnapshot.variables.includes(v));
|
|
654
|
+
if (missingVariables.length > 0) {
|
|
655
|
+
errors.push(`Missing variables after patch: ${missingVariables.join(", ")}`);
|
|
656
|
+
}
|
|
657
|
+
const removedImports = snapshot.imports.filter(
|
|
658
|
+
(i) => i !== TRANSFORMER_PKG && !newSnapshot.imports.includes(i)
|
|
659
|
+
);
|
|
660
|
+
if (removedImports.length > 0) {
|
|
661
|
+
errors.push(`Removed imports after patch: ${removedImports.join(", ")}`);
|
|
662
|
+
}
|
|
663
|
+
const addedImports = newSnapshot.imports.filter((i) => !snapshot.imports.includes(i));
|
|
664
|
+
const unexpectedImports = addedImports.filter((i) => i !== TRANSFORMER_PKG);
|
|
665
|
+
if (unexpectedImports.length > 0) {
|
|
666
|
+
errors.push(`Unexpected new imports: ${unexpectedImports.join(", ")}`);
|
|
667
|
+
}
|
|
668
|
+
const unreachable = detectUnreachableCode(newContent, filePath);
|
|
669
|
+
if (unreachable.length > 0) {
|
|
670
|
+
errors.push(`Unreachable code after return: ${unreachable.join("; ")}`);
|
|
671
|
+
}
|
|
672
|
+
const unresolvedRefs = detectUnresolvedPlaceholders(newContent);
|
|
673
|
+
if (unresolvedRefs.length > 0) {
|
|
674
|
+
errors.push(`Unresolved placeholders: ${unresolvedRefs.join("; ")}`);
|
|
675
|
+
}
|
|
676
|
+
return { valid: errors.length === 0, errors };
|
|
677
|
+
}
|
|
678
|
+
function detectUnresolvedPlaceholders(content) {
|
|
679
|
+
const issues = [];
|
|
680
|
+
if (content.includes("__runtimeConfig")) {
|
|
681
|
+
const hasDeclared = /(?:const|let|var)\s+__runtimeConfig\s*=/.test(content);
|
|
682
|
+
if (!hasDeclared) {
|
|
683
|
+
issues.push(
|
|
684
|
+
"__runtimeConfig is used but never declared \u2014 useRuntimeConfig() injection is missing"
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return issues;
|
|
689
|
+
}
|
|
690
|
+
function detectUnreachableCode(content, filePath) {
|
|
691
|
+
const issues = [];
|
|
692
|
+
const ext = extname(filePath);
|
|
693
|
+
const { code, lineOffset } = getCodeForControlFlowValidation(content, ext);
|
|
694
|
+
if (!code.trim()) return issues;
|
|
695
|
+
try {
|
|
696
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
697
|
+
const scriptKind = ext === ".js" ? ScriptKind.JS : ScriptKind.TS;
|
|
698
|
+
const sourceFile = project.createSourceFile("__unreachable_check__.ts", code, { scriptKind });
|
|
699
|
+
const blocks = sourceFile.getDescendantsOfKind(SyntaxKind.Block);
|
|
700
|
+
for (const block of blocks) {
|
|
701
|
+
const statements = block.getStatements();
|
|
702
|
+
let returnLine = null;
|
|
703
|
+
for (const stmt of statements) {
|
|
704
|
+
if (returnLine !== null) {
|
|
705
|
+
const stmtLine = stmt.getStartLineNumber() + lineOffset;
|
|
706
|
+
const retLine = returnLine + lineOffset;
|
|
707
|
+
issues.push(`line ${stmtLine} is unreachable after return at line ${retLine}`);
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
if (stmt.getKind() === SyntaxKind.ReturnStatement) {
|
|
711
|
+
returnLine = stmt.getStartLineNumber();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
} catch {
|
|
716
|
+
return [];
|
|
717
|
+
}
|
|
718
|
+
return issues;
|
|
719
|
+
}
|
|
720
|
+
function getCodeForControlFlowValidation(content, ext) {
|
|
721
|
+
if (ext === ".astro") {
|
|
722
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
723
|
+
if (!match || typeof match.index !== "number") return { code: content, lineOffset: 0 };
|
|
724
|
+
const before = content.slice(0, match.index);
|
|
725
|
+
const startLine = before.split("\n").length + 1;
|
|
726
|
+
return { code: match[1], lineOffset: startLine - 1 };
|
|
727
|
+
}
|
|
728
|
+
if (ext === ".vue" || ext === ".svelte") {
|
|
729
|
+
const match = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
730
|
+
if (!match || typeof match.index !== "number") return { code: "", lineOffset: 0 };
|
|
731
|
+
const scriptTag = match[0];
|
|
732
|
+
const bodyStartInTag = scriptTag.indexOf(">") + 1;
|
|
733
|
+
const bodyStartInFile = match.index + bodyStartInTag;
|
|
734
|
+
const before = content.slice(0, bodyStartInFile);
|
|
735
|
+
const startLine = before.split("\n").length;
|
|
736
|
+
return { code: match[1], lineOffset: startLine - 1 };
|
|
737
|
+
}
|
|
738
|
+
return { code: content, lineOffset: 0 };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/planner/index.ts
|
|
742
|
+
function resolveInstallCommand(scan2) {
|
|
743
|
+
const pkg = "@synchronized-studio/response-transformer";
|
|
744
|
+
switch (scan2.packageManager) {
|
|
745
|
+
case "pnpm":
|
|
746
|
+
return `pnpm add ${pkg}`;
|
|
747
|
+
case "yarn":
|
|
748
|
+
return `yarn add ${pkg}`;
|
|
749
|
+
case "bun":
|
|
750
|
+
return `bun add ${pkg}`;
|
|
751
|
+
default:
|
|
752
|
+
return `npm install ${pkg}`;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function resolveEnvFiles(root) {
|
|
756
|
+
const candidates = [".env", ".env.local", ".env.example", ".env.development"];
|
|
757
|
+
return candidates.filter((f) => existsSync4(join5(root, f)));
|
|
758
|
+
}
|
|
759
|
+
function createPlan(scan2) {
|
|
760
|
+
const patches = [];
|
|
761
|
+
for (const candidate of scan2.injectionPoints) {
|
|
762
|
+
const template = WRAP_TEMPLATES[candidate.type];
|
|
763
|
+
if (!template) continue;
|
|
764
|
+
const importStatement = getImportStatement(scan2.cms.type);
|
|
765
|
+
const originalCode = candidate.targetCode;
|
|
766
|
+
if (!originalCode) continue;
|
|
767
|
+
if (/\.push\s*\(/.test(originalCode.trim())) continue;
|
|
768
|
+
if (!checkNonCmsReturnWrapping(originalCode).valid) continue;
|
|
769
|
+
const isNuxt3ClientSide = scan2.framework.name === "nuxt" && !/^server\//.test(candidate.filePath);
|
|
770
|
+
const runtimeUrlExpr = isNuxt3ClientSide ? "__runtimeConfig.public.cmsAssetsUrl" : void 0;
|
|
771
|
+
const transformedCode = template.transform(
|
|
772
|
+
originalCode,
|
|
773
|
+
scan2.cms.type,
|
|
774
|
+
scan2.cms.params,
|
|
775
|
+
void 0,
|
|
776
|
+
runtimeUrlExpr
|
|
777
|
+
);
|
|
778
|
+
if (transformedCode === originalCode) continue;
|
|
779
|
+
patches.push({
|
|
780
|
+
filePath: candidate.filePath,
|
|
781
|
+
description: template.description(scan2.cms.type),
|
|
782
|
+
importToAdd: importStatement,
|
|
783
|
+
wrapTarget: {
|
|
784
|
+
line: candidate.line,
|
|
785
|
+
originalCode,
|
|
786
|
+
transformedCode
|
|
787
|
+
},
|
|
788
|
+
confidence: candidate.confidence,
|
|
789
|
+
reasons: candidate.reasons
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
const envFiles = resolveEnvFiles(scan2.projectRoot);
|
|
793
|
+
return {
|
|
794
|
+
schemaVersion: "1.0",
|
|
795
|
+
scan: scan2,
|
|
796
|
+
install: {
|
|
797
|
+
package: "@synchronized-studio/response-transformer",
|
|
798
|
+
command: resolveInstallCommand(scan2)
|
|
799
|
+
},
|
|
800
|
+
env: {
|
|
801
|
+
key: "CMS_ASSETS_URL",
|
|
802
|
+
placeholder: "https://YOUR-SLUG.cmsassets.com",
|
|
803
|
+
files: envFiles
|
|
804
|
+
},
|
|
805
|
+
patches,
|
|
806
|
+
policies: {
|
|
807
|
+
maxFilesAutoApply: 20,
|
|
808
|
+
patchMode: "hybrid",
|
|
809
|
+
verifyProfile: "quick"
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/patcher/index.ts
|
|
815
|
+
import { appendFileSync, existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
816
|
+
import { join as join9 } from "path";
|
|
817
|
+
import consola from "consola";
|
|
818
|
+
import pc from "picocolors";
|
|
819
|
+
|
|
820
|
+
// src/patcher/astPatcher.ts
|
|
821
|
+
import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
822
|
+
import { readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
823
|
+
import { join as join6, extname as extname2 } from "path";
|
|
824
|
+
var RUNTIME_CONFIG_PLACEHOLDER = "__runtimeConfig";
|
|
825
|
+
var RUNTIME_CONFIG_DECL = `const ${RUNTIME_CONFIG_PLACEHOLDER} = useRuntimeConfig()`;
|
|
826
|
+
function detectRuntimeConfigVar(content) {
|
|
827
|
+
const match = content.match(/(?:const|let|var)\s+(\w+)\s*=\s*useRuntimeConfig\s*\(/);
|
|
828
|
+
return match?.[1] ?? null;
|
|
829
|
+
}
|
|
830
|
+
function resolveRuntimeConfigRef(fileContent, transformedCode) {
|
|
831
|
+
if (!transformedCode.includes(RUNTIME_CONFIG_PLACEHOLDER)) {
|
|
832
|
+
return { resolved: transformedCode, needsDecl: false };
|
|
833
|
+
}
|
|
834
|
+
const existing = detectRuntimeConfigVar(fileContent);
|
|
835
|
+
if (existing) {
|
|
836
|
+
return {
|
|
837
|
+
resolved: transformedCode.replaceAll(RUNTIME_CONFIG_PLACEHOLDER, existing),
|
|
838
|
+
needsDecl: false
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
return { resolved: transformedCode, needsDecl: true };
|
|
842
|
+
}
|
|
843
|
+
function injectRuntimeConfigDecl(content) {
|
|
844
|
+
const lines = content.split("\n");
|
|
845
|
+
let scriptStart = -1;
|
|
846
|
+
let scriptEnd = -1;
|
|
847
|
+
for (let i = 0; i < lines.length; i++) {
|
|
848
|
+
if (/<script[^>]*>/.test(lines[i])) scriptStart = i;
|
|
849
|
+
if (/<\/script>/.test(lines[i])) {
|
|
850
|
+
scriptEnd = i;
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const searchStart = scriptStart >= 0 ? scriptStart : 0;
|
|
855
|
+
const searchEnd = scriptEnd >= 0 ? scriptEnd : lines.length;
|
|
856
|
+
for (let i = searchStart; i < searchEnd; i++) {
|
|
857
|
+
if (/\b(useAsyncData|useFetch)\s*\(/.test(lines[i])) {
|
|
858
|
+
let insertIdx = i;
|
|
859
|
+
if (/^\s*\}\s*=\s*await\s+(useAsyncData|useFetch)\b/.test(lines[i])) {
|
|
860
|
+
for (let j = i - 1; j >= searchStart; j--) {
|
|
861
|
+
if (/^\s*(const|let|var)\s*\{/.test(lines[j])) {
|
|
862
|
+
insertIdx = j;
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const indent2 = lines[insertIdx].match(/^(\s*)/)?.[1] ?? " ";
|
|
868
|
+
lines.splice(insertIdx, 0, `${indent2}${RUNTIME_CONFIG_DECL}`);
|
|
869
|
+
return lines.join("\n");
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
let lastImport = searchStart;
|
|
873
|
+
for (let i = searchStart; i < searchEnd; i++) {
|
|
874
|
+
if (/^\s*import\s/.test(lines[i])) lastImport = i;
|
|
875
|
+
}
|
|
876
|
+
const indent = lines[lastImport + 1]?.match(/^(\s*)/)?.[1] ?? " ";
|
|
877
|
+
lines.splice(lastImport + 1, 0, `${indent}${RUNTIME_CONFIG_DECL}`);
|
|
878
|
+
return lines.join("\n");
|
|
879
|
+
}
|
|
880
|
+
function applyAstPatch(root, patch, dryRun, snapshot) {
|
|
881
|
+
const absPath = join6(root, patch.filePath);
|
|
882
|
+
const ext = extname2(patch.filePath);
|
|
883
|
+
if ([".vue", ".svelte", ".astro"].includes(ext)) {
|
|
884
|
+
return applyStringPatch(root, patch, dryRun, snapshot);
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
const project = new Project2({ useInMemoryFileSystem: false });
|
|
888
|
+
const sourceFile = project.addSourceFileAtPath(absPath);
|
|
889
|
+
const originalText = sourceFile.getFullText();
|
|
890
|
+
const targetLine = patch.wrapTarget.line;
|
|
891
|
+
const original = patch.wrapTarget.originalCode.trim();
|
|
892
|
+
const transformed = patch.wrapTarget.transformedCode.trim();
|
|
893
|
+
if (original && transformed && original !== transformed) {
|
|
894
|
+
let patched = false;
|
|
895
|
+
const returnStatements = sourceFile.getDescendantsOfKind(
|
|
896
|
+
SyntaxKind2.ReturnStatement
|
|
897
|
+
);
|
|
898
|
+
for (const ret of returnStatements) {
|
|
899
|
+
const retLine = ret.getStartLineNumber();
|
|
900
|
+
if (retLine !== targetLine) continue;
|
|
901
|
+
const retText = ret.getText().trim();
|
|
902
|
+
if (retText.includes(original) || original.startsWith("return") && retText.startsWith("return") && normalizeReturnExpr(retText) === normalizeReturnExpr(original)) {
|
|
903
|
+
ret.replaceWithText(transformed);
|
|
904
|
+
patched = true;
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (!patched) {
|
|
909
|
+
const expressions = sourceFile.getDescendantsOfKind(
|
|
910
|
+
SyntaxKind2.ExpressionStatement
|
|
911
|
+
);
|
|
912
|
+
for (const expr of expressions) {
|
|
913
|
+
const exprLine = expr.getStartLineNumber();
|
|
914
|
+
if (exprLine !== targetLine) continue;
|
|
915
|
+
const exprText = expr.getText().trim();
|
|
916
|
+
if (exprText.includes("res.json") || exprText.includes("c.json")) {
|
|
917
|
+
expr.replaceWithText(transformed);
|
|
918
|
+
patched = true;
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (!patched) {
|
|
924
|
+
for (const ret of returnStatements) {
|
|
925
|
+
const retLine = ret.getStartLineNumber();
|
|
926
|
+
if (Math.abs(retLine - targetLine) > 1) continue;
|
|
927
|
+
const retText = ret.getText().trim();
|
|
928
|
+
if (original.startsWith("return") && retText.startsWith("return")) {
|
|
929
|
+
if (normalizeReturnExpr(retText) !== normalizeReturnExpr(original)) continue;
|
|
930
|
+
ret.replaceWithText(transformed);
|
|
931
|
+
patched = true;
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (!patched) {
|
|
937
|
+
const currentText = sourceFile.getFullText();
|
|
938
|
+
const lines = currentText.split("\n");
|
|
939
|
+
const targetLineContent = lines[targetLine - 1];
|
|
940
|
+
if (targetLineContent && targetLineContent.trim() === original) {
|
|
941
|
+
const indent = targetLineContent.match(/^(\s*)/)?.[1] ?? "";
|
|
942
|
+
const newContent = currentText.replace(
|
|
943
|
+
targetLineContent,
|
|
944
|
+
indent + transformed
|
|
945
|
+
);
|
|
946
|
+
if (newContent !== currentText) {
|
|
947
|
+
sourceFile.replaceWithText(newContent);
|
|
948
|
+
patched = true;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (!patched) {
|
|
953
|
+
const currentText = sourceFile.getFullText();
|
|
954
|
+
const lines = currentText.split("\n");
|
|
955
|
+
const targetIdx = Math.max(0, targetLine - 1);
|
|
956
|
+
const start = Math.max(0, targetIdx - 5);
|
|
957
|
+
const end = Math.min(lines.length - 1, targetIdx + 5);
|
|
958
|
+
for (let idx = start; idx <= end; idx++) {
|
|
959
|
+
const line = lines[idx];
|
|
960
|
+
const trimmed = line.trim();
|
|
961
|
+
if (trimmed === original || trimmed === original.replace(/;\s*$/, "")) {
|
|
962
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? "";
|
|
963
|
+
lines[idx] = indent + transformed;
|
|
964
|
+
sourceFile.replaceWithText(lines.join("\n"));
|
|
965
|
+
patched = true;
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (!patched) {
|
|
971
|
+
return { applied: false, reason: `Could not locate target code at line ${targetLine}` };
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const existingImports = sourceFile.getImportDeclarations();
|
|
975
|
+
const hasImport = existingImports.some(
|
|
976
|
+
(i) => i.getModuleSpecifierValue() === "@synchronized-studio/response-transformer"
|
|
977
|
+
);
|
|
978
|
+
if (!hasImport && patch.importToAdd) {
|
|
979
|
+
const importMatch = patch.importToAdd.match(
|
|
980
|
+
/import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/
|
|
981
|
+
);
|
|
982
|
+
if (importMatch) {
|
|
983
|
+
const namedImports = importMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
984
|
+
sourceFile.addImportDeclaration({
|
|
985
|
+
moduleSpecifier: importMatch[2],
|
|
986
|
+
namedImports
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const newText = sourceFile.getFullText();
|
|
991
|
+
if (newText === originalText) {
|
|
992
|
+
return { applied: false, reason: "No changes needed (idempotent)" };
|
|
993
|
+
}
|
|
994
|
+
if (snapshot) {
|
|
995
|
+
const validation = validateContentAgainstSnapshot(newText, patch.filePath, snapshot);
|
|
996
|
+
if (!validation.valid) {
|
|
997
|
+
return {
|
|
998
|
+
applied: false,
|
|
999
|
+
validationFailed: true,
|
|
1000
|
+
reason: `Structural validation failed: ${validation.errors.join("; ")}`
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
if (!dryRun) {
|
|
1005
|
+
sourceFile.saveSync();
|
|
1006
|
+
}
|
|
1007
|
+
return { applied: true };
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1010
|
+
return { applied: false, reason: `AST patch failed: ${msg}` };
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function normalizeReturnExpr(value) {
|
|
1014
|
+
return value.replace(/^return\s+/, "").replace(/;$/, "").replace(/\s+/g, " ").trim();
|
|
1015
|
+
}
|
|
1016
|
+
function applyStringPatch(root, patch, dryRun, snapshot) {
|
|
1017
|
+
const absPath = join6(root, patch.filePath);
|
|
1018
|
+
let content;
|
|
1019
|
+
try {
|
|
1020
|
+
content = readFileSync4(absPath, "utf-8");
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1023
|
+
return { applied: false, reason: `Cannot read file: ${msg}` };
|
|
1024
|
+
}
|
|
1025
|
+
const originalContent = content;
|
|
1026
|
+
const { originalCode } = patch.wrapTarget;
|
|
1027
|
+
let { transformedCode } = patch.wrapTarget;
|
|
1028
|
+
const { resolved, needsDecl } = resolveRuntimeConfigRef(content, transformedCode);
|
|
1029
|
+
transformedCode = resolved;
|
|
1030
|
+
if (originalCode && transformedCode && originalCode !== transformedCode) {
|
|
1031
|
+
if (content.includes(originalCode.trim())) {
|
|
1032
|
+
content = content.replace(originalCode.trim(), transformedCode.trim());
|
|
1033
|
+
} else {
|
|
1034
|
+
return {
|
|
1035
|
+
applied: false,
|
|
1036
|
+
reason: "Original code not found in file (string match)"
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (needsDecl && content.includes(RUNTIME_CONFIG_PLACEHOLDER)) {
|
|
1041
|
+
content = injectRuntimeConfigDecl(content);
|
|
1042
|
+
}
|
|
1043
|
+
if (patch.importToAdd && !content.includes("@synchronized-studio/response-transformer")) {
|
|
1044
|
+
const scriptMatch = content.match(
|
|
1045
|
+
/(<script[^>]*>)\s*\n/
|
|
1046
|
+
);
|
|
1047
|
+
if (scriptMatch) {
|
|
1048
|
+
const insertAfter = scriptMatch[0];
|
|
1049
|
+
content = content.replace(
|
|
1050
|
+
insertAfter,
|
|
1051
|
+
`${insertAfter}${patch.importToAdd}
|
|
1052
|
+
`
|
|
1053
|
+
);
|
|
1054
|
+
} else if (patch.filePath.endsWith(".astro")) {
|
|
1055
|
+
const fmMatch = content.match(/^(---\n)/);
|
|
1056
|
+
if (fmMatch) {
|
|
1057
|
+
content = content.replace(
|
|
1058
|
+
"---\n",
|
|
1059
|
+
`---
|
|
1060
|
+
${patch.importToAdd}
|
|
1061
|
+
`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (content === originalContent) {
|
|
1067
|
+
return { applied: false, reason: "No changes needed (idempotent)" };
|
|
1068
|
+
}
|
|
1069
|
+
if (snapshot) {
|
|
1070
|
+
const validation = validateContentAgainstSnapshot(content, patch.filePath, snapshot);
|
|
1071
|
+
if (!validation.valid) {
|
|
1072
|
+
return {
|
|
1073
|
+
applied: false,
|
|
1074
|
+
validationFailed: true,
|
|
1075
|
+
reason: `Structural validation failed: ${validation.errors.join("; ")}`
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (!dryRun) {
|
|
1080
|
+
writeFileSync(absPath, content, "utf-8");
|
|
1081
|
+
}
|
|
1082
|
+
return { applied: true };
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/patcher/aiFilePatch.ts
|
|
1086
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
1087
|
+
import { join as join7 } from "path";
|
|
1088
|
+
async function applyAiFilePatch(root, filePath, patches, cms, framework, snapshot, model = "gpt-4o") {
|
|
1089
|
+
const absPath = join7(root, filePath);
|
|
1090
|
+
let fileContent;
|
|
1091
|
+
try {
|
|
1092
|
+
fileContent = readFileSync5(absPath, "utf-8");
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1095
|
+
return { applied: false, reason: `Cannot read file: ${msg}` };
|
|
1096
|
+
}
|
|
1097
|
+
const fn = getTransformFunctionName(cms.type);
|
|
1098
|
+
const opts = buildCmsOptions(cms.type, cms.params);
|
|
1099
|
+
const importStatement = `import { ${fn} } from '@synchronized-studio/response-transformer'`;
|
|
1100
|
+
const resolvedPatches = patches.map((p) => {
|
|
1101
|
+
const { resolved, needsDecl } = resolveRuntimeConfigRef(fileContent, p.wrapTarget.transformedCode);
|
|
1102
|
+
return { ...p, wrapTarget: { ...p.wrapTarget, transformedCode: resolved }, needsDecl };
|
|
1103
|
+
});
|
|
1104
|
+
const needsRuntimeConfigDecl = resolvedPatches.some((p) => p.needsDecl);
|
|
1105
|
+
const wraps = resolvedPatches.map(
|
|
1106
|
+
(p, i) => `${i + 1}. Near line ${p.wrapTarget.line}:
|
|
1107
|
+
BEFORE: ${p.wrapTarget.originalCode}
|
|
1108
|
+
AFTER: ${p.wrapTarget.transformedCode}`
|
|
1109
|
+
).join("\n\n");
|
|
1110
|
+
const prompt = `You are a precise code integration agent. You must modify the file below to add CMS asset URL transformation.
|
|
1111
|
+
|
|
1112
|
+
TASK:
|
|
1113
|
+
1. Add this import at the top (with the other imports), if not already present:
|
|
1114
|
+
${importStatement}
|
|
1115
|
+
|
|
1116
|
+
2. Apply these wraps:
|
|
1117
|
+
|
|
1118
|
+
${wraps}
|
|
1119
|
+
|
|
1120
|
+
${needsRuntimeConfigDecl ? `3. INJECT RUNTIME CONFIG: The wraps use \`__runtimeConfig.public.cmsAssetsUrl\`.
|
|
1121
|
+
Since no \`useRuntimeConfig()\` variable was found in this file, you MUST add:
|
|
1122
|
+
\`const __runtimeConfig = useRuntimeConfig()\`
|
|
1123
|
+
as a NEW LINE in the outer <script setup> scope, BEFORE the first \`useAsyncData\`/\`useFetch\` call.
|
|
1124
|
+
CRITICAL: NEVER call \`useRuntimeConfig()\` directly inside a callback.` : `3. RUNTIME CONFIG: The \`__runtimeConfig\` placeholder has already been resolved to the correct variable name in the wraps above. Do NOT modify it.`}
|
|
1125
|
+
|
|
1126
|
+
CRITICAL RULES:
|
|
1127
|
+
- Return the COMPLETE modified file \u2014 every single line
|
|
1128
|
+
- Do NOT change, remove, rename, or restructure ANY other code
|
|
1129
|
+
- Do NOT add comments explaining your changes
|
|
1130
|
+
- Do NOT change formatting, indentation, or whitespace of untouched lines
|
|
1131
|
+
- Every function, variable, export, and import that exists in the original MUST remain intact
|
|
1132
|
+
- If a return statement returns an object that contains any FUNCTIONS (e.g. { getPage, fn } or { getPrimary, items }), do NOT wrap it \u2014 the transform uses JSON.stringify and would destroy function references at runtime
|
|
1133
|
+
- Only wrap returns that produce pure CMS DATA (plain objects/arrays with data, no function properties)
|
|
1134
|
+
- If unsure whether a return is data or functions, do NOT wrap it
|
|
1135
|
+
- ONLY modify the EXACT lines listed in the wraps above. Do NOT modify any other functions or lines
|
|
1136
|
+
- NEVER create unreachable code \u2014 do NOT add any statement after a return statement
|
|
1137
|
+
- NEVER place a return statement before existing side effects (commit(), setState(), dispatch(), store mutations). If a function commits data and does NOT return, wrap the data BEFORE the commit and pass the transformed value to the commit \u2014 do NOT add a new return that makes the commit unreachable
|
|
1138
|
+
- \`return dispatch(...)\` is delegation to another action \u2014 do NOT wrap it, the target action handles the transform
|
|
1139
|
+
- For Vuex/Pinia actions that commit but don't return: transform the data, then pass it to commit. Example:
|
|
1140
|
+
WRONG: return ${fn}(data, ${opts}) // \u2190 makes commit below unreachable!
|
|
1141
|
+
commit('SET_STATE', { prop: 'x', value: data })
|
|
1142
|
+
RIGHT: const transformed = ${fn}(data, ${opts})
|
|
1143
|
+
commit('SET_STATE', { prop: 'x', value: transformed })
|
|
1144
|
+
- When a Vuex action BOTH commits AND returns the same data, transform ONCE and use for both:
|
|
1145
|
+
const transformed = ${fn}(data, ${opts})
|
|
1146
|
+
commit('SET_STATE', { prop: 'x', value: transformed })
|
|
1147
|
+
return transformed
|
|
1148
|
+
- NEVER put ${fn}() inside loop/iteration callbacks (_each, forEach, map, reduce, for...of). Transform the FINAL result AFTER the loop, not each iteration
|
|
1149
|
+
- NEVER wrap returns that produce navigation/routing objects (e.g. \`{ query: ... }\`, \`{ path: ... }\`, \`{ redirect: ... }\`). These are Vue Router / framework navigation objects, NOT CMS data. If a wrap listed above targets such a return, SKIP that wrap entirely
|
|
1150
|
+
|
|
1151
|
+
CMS: ${cms.type}
|
|
1152
|
+
Framework: ${framework}
|
|
1153
|
+
Transform function: ${fn}
|
|
1154
|
+
Options: ${opts}
|
|
1155
|
+
|
|
1156
|
+
File: ${filePath}
|
|
1157
|
+
\`\`\`
|
|
1158
|
+
${fileContent}
|
|
1159
|
+
\`\`\`
|
|
1160
|
+
|
|
1161
|
+
Return ONLY the complete modified file content. No markdown fences, no explanation \u2014 just the raw file content.`;
|
|
1162
|
+
const result = await chatCompletion(prompt, {
|
|
1163
|
+
model,
|
|
1164
|
+
maxTokens: 16e3,
|
|
1165
|
+
temperature: 0
|
|
1166
|
+
});
|
|
1167
|
+
if (isOpenAiError(result)) {
|
|
1168
|
+
return { applied: false, reason: result.error };
|
|
1169
|
+
}
|
|
1170
|
+
let newContent = result.content;
|
|
1171
|
+
if (!newContent) {
|
|
1172
|
+
return { applied: false, reason: "AI returned empty response" };
|
|
1173
|
+
}
|
|
1174
|
+
newContent = stripCodeFences(newContent);
|
|
1175
|
+
if (newContent.trim() === fileContent.trim()) {
|
|
1176
|
+
return { applied: false, reason: "AI returned unchanged file" };
|
|
1177
|
+
}
|
|
1178
|
+
if (!newContent.includes(fn)) {
|
|
1179
|
+
return { applied: false, reason: "AI output missing transform function call" };
|
|
1180
|
+
}
|
|
1181
|
+
if (!newContent.includes("@synchronized-studio/response-transformer")) {
|
|
1182
|
+
return { applied: false, reason: "AI output missing transformer import" };
|
|
1183
|
+
}
|
|
1184
|
+
const fnGuard = checkFunctionValueWrapping(
|
|
1185
|
+
patches[0]?.wrapTarget.originalCode ?? "",
|
|
1186
|
+
snapshot
|
|
1187
|
+
);
|
|
1188
|
+
if (!fnGuard.valid) {
|
|
1189
|
+
return { applied: false, reason: fnGuard.errors[0] };
|
|
1190
|
+
}
|
|
1191
|
+
const validation = validateContentAgainstSnapshot(newContent, filePath, snapshot);
|
|
1192
|
+
if (!validation.valid) {
|
|
1193
|
+
return {
|
|
1194
|
+
applied: false,
|
|
1195
|
+
reason: `AI output failed structural validation: ${validation.errors.join("; ")}`
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
writeFileSync2(absPath, newContent, "utf-8");
|
|
1199
|
+
return { applied: true };
|
|
1200
|
+
}
|
|
1201
|
+
function stripCodeFences(content) {
|
|
1202
|
+
const fenceStart = /^```\w*\n/;
|
|
1203
|
+
const fenceEnd = /\n```\s*$/;
|
|
1204
|
+
if (fenceStart.test(content) && fenceEnd.test(content)) {
|
|
1205
|
+
return content.replace(fenceStart, "").replace(fenceEnd, "");
|
|
1206
|
+
}
|
|
1207
|
+
return content;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// src/patcher/idempotency.ts
|
|
1211
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1212
|
+
import { join as join8 } from "path";
|
|
1213
|
+
var TRANSFORMER_IMPORTS = [
|
|
1214
|
+
"@synchronized-studio/response-transformer",
|
|
1215
|
+
"transformCmsAssetUrls",
|
|
1216
|
+
"transformPrismicAssetUrls",
|
|
1217
|
+
"transformContentfulAssetUrls",
|
|
1218
|
+
"transformSanityAssetUrls",
|
|
1219
|
+
"transformShopifyAssetUrls",
|
|
1220
|
+
"transformCloudinaryAssetUrls",
|
|
1221
|
+
"transformImgixAssetUrls",
|
|
1222
|
+
"transformGenericAssetUrls"
|
|
1223
|
+
];
|
|
1224
|
+
function isAlreadyTransformed(root, filePath) {
|
|
1225
|
+
const absPath = join8(root, filePath);
|
|
1226
|
+
let content;
|
|
1227
|
+
try {
|
|
1228
|
+
content = readFileSync6(absPath, "utf-8");
|
|
1229
|
+
} catch {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
return TRANSFORMER_IMPORTS.some((sig) => content.includes(sig));
|
|
1233
|
+
}
|
|
1234
|
+
function isTestOrFixtureFile(filePath) {
|
|
1235
|
+
return /\.(test|spec)\.[jt]sx?$/.test(filePath) || /__tests__\//.test(filePath) || /(^|\/)tests?\//.test(filePath) || /(^|\/)fixtures?\//.test(filePath) || /(^|\/)mocks?\//.test(filePath) || /\.stories\.[jt]sx?$/.test(filePath);
|
|
1236
|
+
}
|
|
1237
|
+
function isGeneratedFile(filePath) {
|
|
1238
|
+
return /\.generated\.[jt]sx?$/.test(filePath) || /\.g\.[jt]sx?$/.test(filePath) || /(^|\/)\.nuxt\//.test(filePath) || /(^|\/)\.next\//.test(filePath) || /(^|\/)\.svelte-kit\//.test(filePath) || /(^|\/)dist\//.test(filePath) || /(^|\/)build\//.test(filePath);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// src/patcher/index.ts
|
|
1242
|
+
function groupPatchesByFile(patches) {
|
|
1243
|
+
const map = /* @__PURE__ */ new Map();
|
|
1244
|
+
for (const patch of patches) {
|
|
1245
|
+
const list = map.get(patch.filePath) ?? [];
|
|
1246
|
+
list.push(patch);
|
|
1247
|
+
map.set(patch.filePath, list);
|
|
1248
|
+
}
|
|
1249
|
+
return map;
|
|
1250
|
+
}
|
|
1251
|
+
async function applyPlan(plan, opts = {}) {
|
|
1252
|
+
const startTime = Date.now();
|
|
1253
|
+
const root = plan.scan.projectRoot;
|
|
1254
|
+
const dryRun = opts.dryRun ?? false;
|
|
1255
|
+
const includeTests = opts.includeTests ?? false;
|
|
1256
|
+
const maxFiles = opts.maxFiles ?? plan.policies.maxFilesAutoApply;
|
|
1257
|
+
const patchMode = opts.patchMode ?? plan.policies.patchMode ?? "hybrid";
|
|
1258
|
+
const aiMode = patchMode !== "ast" && (opts.aiMode ?? false);
|
|
1259
|
+
const files = [];
|
|
1260
|
+
let appliedCount = 0;
|
|
1261
|
+
const patchedFiles = /* @__PURE__ */ new Set();
|
|
1262
|
+
const alreadyTransformedAtStart = new Set(
|
|
1263
|
+
plan.patches.map((patch) => patch.filePath).filter((filePath, index, all) => all.indexOf(filePath) === index).filter((filePath) => isAlreadyTransformed(root, filePath))
|
|
1264
|
+
);
|
|
1265
|
+
const eligiblePatches = [];
|
|
1266
|
+
for (const patch of plan.patches) {
|
|
1267
|
+
if (alreadyTransformedAtStart.has(patch.filePath)) {
|
|
1268
|
+
files.push({ filePath: patch.filePath, applied: false, method: "skipped", reason: "Transformer already present in file", durationMs: 0 });
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
if (!includeTests && isTestOrFixtureFile(patch.filePath)) {
|
|
1272
|
+
files.push({ filePath: patch.filePath, applied: false, method: "skipped", reason: "Test/fixture file excluded", durationMs: 0 });
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
if (isGeneratedFile(patch.filePath)) {
|
|
1276
|
+
files.push({ filePath: patch.filePath, applied: false, method: "skipped", reason: "Generated file excluded", durationMs: 0 });
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
eligiblePatches.push(patch);
|
|
1280
|
+
}
|
|
1281
|
+
if (aiMode && patchMode === "ai") {
|
|
1282
|
+
const byFile = groupPatchesByFile(eligiblePatches);
|
|
1283
|
+
for (const [filePath, filePatches] of byFile) {
|
|
1284
|
+
const fileStart = Date.now();
|
|
1285
|
+
if (!patchedFiles.has(filePath) && appliedCount >= maxFiles) {
|
|
1286
|
+
for (const p of filePatches) {
|
|
1287
|
+
files.push({ filePath: p.filePath, applied: false, method: "skipped", reason: `Max files threshold reached (${maxFiles})`, durationMs: 0 });
|
|
1288
|
+
}
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const snapshot = takeSnapshot(root, filePath);
|
|
1292
|
+
const safePatches = filePatches.filter((p) => {
|
|
1293
|
+
const fnGuard = checkFunctionValueWrapping(p.wrapTarget.originalCode, snapshot);
|
|
1294
|
+
if (!fnGuard.valid) {
|
|
1295
|
+
consola.warn(` ${pc.yellow("skip")} ${filePath}:${p.wrapTarget.line} \u2014 ${fnGuard.errors[0]}`);
|
|
1296
|
+
files.push({ filePath, applied: false, method: "skipped", reason: fnGuard.errors[0], durationMs: 0 });
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
const nonCmsGuard = checkNonCmsReturnWrapping(p.wrapTarget.originalCode);
|
|
1300
|
+
if (!nonCmsGuard.valid) {
|
|
1301
|
+
consola.warn(` ${pc.yellow("skip")} ${filePath}:${p.wrapTarget.line} \u2014 ${nonCmsGuard.errors[0]}`);
|
|
1302
|
+
files.push({ filePath, applied: false, method: "skipped", reason: nonCmsGuard.errors[0], durationMs: 0 });
|
|
1303
|
+
return false;
|
|
1304
|
+
}
|
|
1305
|
+
return true;
|
|
1306
|
+
});
|
|
1307
|
+
if (safePatches.length === 0) continue;
|
|
1308
|
+
consola.info(`${pc.cyan("AI patch:")} ${filePath} (${safePatches.length} targets)`);
|
|
1309
|
+
if (dryRun) {
|
|
1310
|
+
for (const p of safePatches) {
|
|
1311
|
+
files.push({ filePath, applied: false, method: "skipped", reason: "Dry run", durationMs: 0 });
|
|
1312
|
+
}
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const aiResult = await applyAiFilePatch(
|
|
1316
|
+
root,
|
|
1317
|
+
filePath,
|
|
1318
|
+
safePatches,
|
|
1319
|
+
plan.scan.cms,
|
|
1320
|
+
plan.scan.framework.name,
|
|
1321
|
+
snapshot,
|
|
1322
|
+
opts.aiModel ?? "gpt-4o"
|
|
1323
|
+
);
|
|
1324
|
+
if (aiResult.applied) {
|
|
1325
|
+
if (!patchedFiles.has(filePath)) {
|
|
1326
|
+
patchedFiles.add(filePath);
|
|
1327
|
+
appliedCount++;
|
|
1328
|
+
}
|
|
1329
|
+
for (const p of safePatches) {
|
|
1330
|
+
files.push({ filePath, applied: true, method: "ai-complete", durationMs: Date.now() - fileStart });
|
|
1331
|
+
}
|
|
1332
|
+
consola.info(` ${pc.green("done")} ${filePath}`);
|
|
1333
|
+
} else {
|
|
1334
|
+
consola.warn(` ${pc.yellow("AI failed")} ${filePath}: ${aiResult.reason}`);
|
|
1335
|
+
for (const p of safePatches) {
|
|
1336
|
+
const patchStart = Date.now();
|
|
1337
|
+
const astResult = applyAstPatch(root, p, false, snapshot);
|
|
1338
|
+
if (astResult.applied) {
|
|
1339
|
+
if (!patchedFiles.has(filePath)) {
|
|
1340
|
+
patchedFiles.add(filePath);
|
|
1341
|
+
appliedCount++;
|
|
1342
|
+
}
|
|
1343
|
+
files.push({ filePath, applied: true, method: "ast", durationMs: Date.now() - patchStart });
|
|
1344
|
+
} else {
|
|
1345
|
+
files.push({ filePath, applied: false, method: "skipped", reason: astResult.reason ?? "AST fallback failed after AI", durationMs: Date.now() - patchStart });
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
} else {
|
|
1351
|
+
for (const patch of eligiblePatches) {
|
|
1352
|
+
const fileStart = Date.now();
|
|
1353
|
+
if (!patchedFiles.has(patch.filePath) && appliedCount >= maxFiles) {
|
|
1354
|
+
files.push({ filePath: patch.filePath, applied: false, method: "skipped", reason: `Max files threshold reached (${maxFiles})`, durationMs: 0 });
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
if (patch.confidence === "low") {
|
|
1358
|
+
files.push({
|
|
1359
|
+
filePath: patch.filePath,
|
|
1360
|
+
applied: false,
|
|
1361
|
+
method: "manual-review",
|
|
1362
|
+
reason: `Low confidence candidate \u2014 requires manual review (line ${patch.wrapTarget.line})`,
|
|
1363
|
+
durationMs: 0
|
|
1364
|
+
});
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
const snapshot = takeSnapshot(root, patch.filePath);
|
|
1368
|
+
const fnGuard = checkFunctionValueWrapping(patch.wrapTarget.originalCode, snapshot);
|
|
1369
|
+
if (!fnGuard.valid) {
|
|
1370
|
+
files.push({ filePath: patch.filePath, applied: false, method: "skipped", reason: fnGuard.errors[0], durationMs: Date.now() - fileStart });
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
const nonCmsGuard = checkNonCmsReturnWrapping(patch.wrapTarget.originalCode);
|
|
1374
|
+
if (!nonCmsGuard.valid) {
|
|
1375
|
+
files.push({ filePath: patch.filePath, applied: false, method: "skipped", reason: nonCmsGuard.errors[0], durationMs: Date.now() - fileStart });
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (dryRun) {
|
|
1379
|
+
files.push({ filePath: patch.filePath, applied: false, method: "skipped", reason: "Dry run", durationMs: 0 });
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
const astResult = applyAstPatch(root, patch, false, snapshot);
|
|
1383
|
+
if (astResult.applied) {
|
|
1384
|
+
if (!patchedFiles.has(patch.filePath)) {
|
|
1385
|
+
patchedFiles.add(patch.filePath);
|
|
1386
|
+
appliedCount++;
|
|
1387
|
+
}
|
|
1388
|
+
files.push({ filePath: patch.filePath, applied: true, method: "ast", durationMs: Date.now() - fileStart });
|
|
1389
|
+
} else {
|
|
1390
|
+
files.push({
|
|
1391
|
+
filePath: patch.filePath,
|
|
1392
|
+
applied: false,
|
|
1393
|
+
method: "manual-review",
|
|
1394
|
+
reason: `AST patch failed, requires manual review: ${astResult.reason}`,
|
|
1395
|
+
durationMs: Date.now() - fileStart
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
let envUpdated = false;
|
|
1401
|
+
if (!dryRun) {
|
|
1402
|
+
for (const envFile of plan.env.files) {
|
|
1403
|
+
const envPath = join9(root, envFile);
|
|
1404
|
+
if (existsSync5(envPath)) {
|
|
1405
|
+
const content = readFileSync7(envPath, "utf-8");
|
|
1406
|
+
if (!content.includes(plan.env.key)) {
|
|
1407
|
+
appendFileSync(envPath, `
|
|
1408
|
+
${plan.env.key}=${plan.env.placeholder}
|
|
1409
|
+
`);
|
|
1410
|
+
envUpdated = true;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (plan.env.files.length === 0) {
|
|
1415
|
+
const envExamplePath = join9(root, ".env.example");
|
|
1416
|
+
writeFileSync3(envExamplePath, `${plan.env.key}=${plan.env.placeholder}
|
|
1417
|
+
`, "utf-8");
|
|
1418
|
+
envUpdated = true;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const summary = {
|
|
1422
|
+
candidatesFound: plan.patches.length,
|
|
1423
|
+
patchedByAi: files.filter((f) => f.applied && f.method === "ai-complete").length,
|
|
1424
|
+
patchedByAst: files.filter((f) => f.applied && f.method === "ast").length,
|
|
1425
|
+
manualReviewRequired: files.filter((f) => f.method === "manual-review").length,
|
|
1426
|
+
skipped: files.filter((f) => !f.applied && f.method === "skipped").length
|
|
1427
|
+
};
|
|
1428
|
+
const decisionLog = files.map((f) => {
|
|
1429
|
+
const reason = f.reason ?? "";
|
|
1430
|
+
let reasonCode = "SKIP_GUARD_REJECTED";
|
|
1431
|
+
if (f.applied && f.method === "ast") reasonCode = "PATCHED_AST";
|
|
1432
|
+
else if (f.applied && f.method === "ai-complete") reasonCode = "PATCHED_AI";
|
|
1433
|
+
else if (f.method === "manual-review" && reason.includes("Low confidence")) reasonCode = "MANUAL_LOW_CONFIDENCE";
|
|
1434
|
+
else if (f.method === "manual-review") reasonCode = "MANUAL_AST_FAILED";
|
|
1435
|
+
else if (reason.includes("Transformer already present")) reasonCode = "SKIP_ALREADY_TRANSFORMED";
|
|
1436
|
+
else if (reason.includes("Test/fixture")) reasonCode = "SKIP_TEST_OR_FIXTURE";
|
|
1437
|
+
else if (reason.includes("Generated file")) reasonCode = "SKIP_GENERATED_FILE";
|
|
1438
|
+
else if (reason.includes("Max files threshold")) reasonCode = "SKIP_MAX_FILES";
|
|
1439
|
+
else if (reason.includes("Dry run")) reasonCode = "SKIP_DRY_RUN";
|
|
1440
|
+
else if (reason.includes("AST fallback failed after AI")) reasonCode = "SKIP_AI_FALLBACK_FAILED";
|
|
1441
|
+
return {
|
|
1442
|
+
filePath: f.filePath,
|
|
1443
|
+
method: f.method,
|
|
1444
|
+
applied: f.applied,
|
|
1445
|
+
reasonCode,
|
|
1446
|
+
reason: f.reason
|
|
1447
|
+
};
|
|
1448
|
+
});
|
|
1449
|
+
return {
|
|
1450
|
+
schemaVersion: "2.0",
|
|
1451
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1452
|
+
projectRoot: root,
|
|
1453
|
+
gitBranch: null,
|
|
1454
|
+
gitCommit: null,
|
|
1455
|
+
installed: false,
|
|
1456
|
+
envUpdated,
|
|
1457
|
+
files,
|
|
1458
|
+
summary,
|
|
1459
|
+
decisionLog,
|
|
1460
|
+
totalDurationMs: Date.now() - startTime
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// src/types.ts
|
|
1465
|
+
import { z } from "zod/v3";
|
|
1466
|
+
var FrameworkName = z.enum([
|
|
1467
|
+
"nuxt",
|
|
1468
|
+
"nuxt2",
|
|
1469
|
+
"next",
|
|
1470
|
+
"remix",
|
|
1471
|
+
"astro",
|
|
1472
|
+
"sveltekit",
|
|
1473
|
+
"express",
|
|
1474
|
+
"hono",
|
|
1475
|
+
"fastify",
|
|
1476
|
+
"unknown"
|
|
1477
|
+
]);
|
|
1478
|
+
var CmsType = z.enum([
|
|
1479
|
+
"prismic",
|
|
1480
|
+
"contentful",
|
|
1481
|
+
"sanity",
|
|
1482
|
+
"shopify",
|
|
1483
|
+
"cloudinary",
|
|
1484
|
+
"imgix",
|
|
1485
|
+
"generic",
|
|
1486
|
+
"unknown"
|
|
1487
|
+
]);
|
|
1488
|
+
var PackageManager = z.enum(["npm", "yarn", "pnpm", "bun"]);
|
|
1489
|
+
var Confidence = z.enum(["high", "medium", "low"]);
|
|
1490
|
+
var InjectionType = z.enum([
|
|
1491
|
+
"return",
|
|
1492
|
+
"res.json",
|
|
1493
|
+
"assignment",
|
|
1494
|
+
"useFetch-transform",
|
|
1495
|
+
"useAsyncData-transform",
|
|
1496
|
+
"loader-return",
|
|
1497
|
+
"getServerSideProps-return",
|
|
1498
|
+
"getStaticProps-return",
|
|
1499
|
+
"load-return",
|
|
1500
|
+
"frontmatter-assignment",
|
|
1501
|
+
"asyncData-return",
|
|
1502
|
+
"vuex-action-return",
|
|
1503
|
+
"vuex-commit"
|
|
1504
|
+
]);
|
|
1505
|
+
var VerifyProfile = z.enum(["quick", "full"]);
|
|
1506
|
+
var PatchMode = z.enum(["ast", "ai", "hybrid"]);
|
|
1507
|
+
var FrameworkInfo = z.object({
|
|
1508
|
+
name: FrameworkName,
|
|
1509
|
+
version: z.string(),
|
|
1510
|
+
configFile: z.string().nullable()
|
|
1511
|
+
});
|
|
1512
|
+
var CmsInfo = z.object({
|
|
1513
|
+
type: CmsType,
|
|
1514
|
+
params: z.record(z.string()),
|
|
1515
|
+
detectedFrom: z.array(z.string())
|
|
1516
|
+
});
|
|
1517
|
+
var InjectionCandidate = z.object({
|
|
1518
|
+
filePath: z.string(),
|
|
1519
|
+
line: z.number(),
|
|
1520
|
+
type: InjectionType,
|
|
1521
|
+
score: z.number().min(0).max(100),
|
|
1522
|
+
confidence: Confidence,
|
|
1523
|
+
detectedConfidence: Confidence.optional(),
|
|
1524
|
+
autoPatchConfidence: Confidence.optional(),
|
|
1525
|
+
regionId: z.string().optional(),
|
|
1526
|
+
targetCode: z.string(),
|
|
1527
|
+
context: z.string(),
|
|
1528
|
+
reasons: z.array(z.string())
|
|
1529
|
+
});
|
|
1530
|
+
var CandidateRegionType = z.enum([
|
|
1531
|
+
"function",
|
|
1532
|
+
"hook",
|
|
1533
|
+
"action",
|
|
1534
|
+
"loader",
|
|
1535
|
+
"route-handler",
|
|
1536
|
+
"unknown"
|
|
1537
|
+
]);
|
|
1538
|
+
var CandidateRegion = z.object({
|
|
1539
|
+
id: z.string(),
|
|
1540
|
+
filePath: z.string(),
|
|
1541
|
+
type: CandidateRegionType,
|
|
1542
|
+
startLine: z.number(),
|
|
1543
|
+
endLine: z.number(),
|
|
1544
|
+
parentRegionId: z.string().nullable()
|
|
1545
|
+
});
|
|
1546
|
+
var CandidateGraph = z.object({
|
|
1547
|
+
regions: z.array(CandidateRegion),
|
|
1548
|
+
candidates: z.array(InjectionCandidate)
|
|
1549
|
+
});
|
|
1550
|
+
var ScanResult = z.object({
|
|
1551
|
+
framework: FrameworkInfo,
|
|
1552
|
+
cms: CmsInfo,
|
|
1553
|
+
injectionPoints: z.array(InjectionCandidate),
|
|
1554
|
+
candidateGraph: CandidateGraph.optional(),
|
|
1555
|
+
packageManager: PackageManager,
|
|
1556
|
+
projectRoot: z.string()
|
|
1557
|
+
});
|
|
1558
|
+
var FilePatch = z.object({
|
|
1559
|
+
filePath: z.string(),
|
|
1560
|
+
description: z.string(),
|
|
1561
|
+
importToAdd: z.string(),
|
|
1562
|
+
wrapTarget: z.object({
|
|
1563
|
+
line: z.number(),
|
|
1564
|
+
originalCode: z.string(),
|
|
1565
|
+
transformedCode: z.string()
|
|
1566
|
+
}),
|
|
1567
|
+
confidence: Confidence,
|
|
1568
|
+
reasons: z.array(z.string())
|
|
1569
|
+
});
|
|
1570
|
+
var PatchPlan = z.object({
|
|
1571
|
+
schemaVersion: z.literal("1.0"),
|
|
1572
|
+
scan: ScanResult,
|
|
1573
|
+
install: z.object({
|
|
1574
|
+
package: z.literal("@synchronized-studio/response-transformer"),
|
|
1575
|
+
command: z.string()
|
|
1576
|
+
}),
|
|
1577
|
+
env: z.object({
|
|
1578
|
+
key: z.literal("CMS_ASSETS_URL"),
|
|
1579
|
+
placeholder: z.string(),
|
|
1580
|
+
files: z.array(z.string())
|
|
1581
|
+
}),
|
|
1582
|
+
patches: z.array(FilePatch),
|
|
1583
|
+
policies: z.object({
|
|
1584
|
+
maxFilesAutoApply: z.number().default(20),
|
|
1585
|
+
patchMode: PatchMode.default("hybrid"),
|
|
1586
|
+
verifyProfile: VerifyProfile.default("quick")
|
|
1587
|
+
})
|
|
1588
|
+
});
|
|
1589
|
+
var PatchMethod = z.enum(["ast", "ai-complete", "skipped", "manual-review"]);
|
|
1590
|
+
var DecisionReasonCode = z.enum([
|
|
1591
|
+
"PATCHED_AST",
|
|
1592
|
+
"PATCHED_AI",
|
|
1593
|
+
"SKIP_ALREADY_TRANSFORMED",
|
|
1594
|
+
"SKIP_TEST_OR_FIXTURE",
|
|
1595
|
+
"SKIP_GENERATED_FILE",
|
|
1596
|
+
"SKIP_MAX_FILES",
|
|
1597
|
+
"SKIP_GUARD_REJECTED",
|
|
1598
|
+
"SKIP_DRY_RUN",
|
|
1599
|
+
"MANUAL_LOW_CONFIDENCE",
|
|
1600
|
+
"MANUAL_AST_FAILED",
|
|
1601
|
+
"SKIP_AI_FALLBACK_FAILED"
|
|
1602
|
+
]);
|
|
1603
|
+
var DecisionLogEntry = z.object({
|
|
1604
|
+
filePath: z.string(),
|
|
1605
|
+
method: PatchMethod,
|
|
1606
|
+
applied: z.boolean(),
|
|
1607
|
+
reasonCode: DecisionReasonCode,
|
|
1608
|
+
reason: z.string().optional()
|
|
1609
|
+
});
|
|
1610
|
+
var PatchedFileReport = z.object({
|
|
1611
|
+
filePath: z.string(),
|
|
1612
|
+
applied: z.boolean(),
|
|
1613
|
+
method: PatchMethod,
|
|
1614
|
+
reason: z.string().optional(),
|
|
1615
|
+
durationMs: z.number()
|
|
1616
|
+
});
|
|
1617
|
+
var ApplyReport = z.object({
|
|
1618
|
+
schemaVersion: z.literal("2.0"),
|
|
1619
|
+
timestamp: z.string(),
|
|
1620
|
+
projectRoot: z.string(),
|
|
1621
|
+
gitBranch: z.string().nullable(),
|
|
1622
|
+
gitCommit: z.string().nullable(),
|
|
1623
|
+
installed: z.boolean(),
|
|
1624
|
+
envUpdated: z.boolean(),
|
|
1625
|
+
files: z.array(PatchedFileReport),
|
|
1626
|
+
summary: z.object({
|
|
1627
|
+
candidatesFound: z.number(),
|
|
1628
|
+
patchedByAi: z.number(),
|
|
1629
|
+
patchedByAst: z.number(),
|
|
1630
|
+
manualReviewRequired: z.number(),
|
|
1631
|
+
skipped: z.number()
|
|
1632
|
+
}).optional(),
|
|
1633
|
+
decisionLog: z.array(DecisionLogEntry).default([]),
|
|
1634
|
+
totalDurationMs: z.number()
|
|
1635
|
+
});
|
|
1636
|
+
var VerifyStepLog = z.object({
|
|
1637
|
+
name: z.string(),
|
|
1638
|
+
command: z.string(),
|
|
1639
|
+
passed: z.boolean(),
|
|
1640
|
+
required: z.boolean(),
|
|
1641
|
+
output: z.string()
|
|
1642
|
+
});
|
|
1643
|
+
var VerifyReport = z.object({
|
|
1644
|
+
schemaVersion: z.literal("2.0"),
|
|
1645
|
+
profile: VerifyProfile,
|
|
1646
|
+
lintPassed: z.boolean().nullable(),
|
|
1647
|
+
buildPassed: z.boolean().nullable(),
|
|
1648
|
+
testsPassed: z.boolean().nullable(),
|
|
1649
|
+
patchedFiles: z.array(z.string()),
|
|
1650
|
+
warnings: z.array(z.string()),
|
|
1651
|
+
steps: z.array(VerifyStepLog).default([]),
|
|
1652
|
+
durationMs: z.number()
|
|
1653
|
+
});
|
|
1654
|
+
var AiFileResult = z.object({
|
|
1655
|
+
filePath: z.string(),
|
|
1656
|
+
passed: z.boolean(),
|
|
1657
|
+
issues: z.array(z.string()),
|
|
1658
|
+
fixAttempted: z.boolean(),
|
|
1659
|
+
fixApplied: z.boolean(),
|
|
1660
|
+
iterations: z.number()
|
|
1661
|
+
});
|
|
1662
|
+
var AiReviewReport = z.object({
|
|
1663
|
+
schemaVersion: z.literal("1.0"),
|
|
1664
|
+
filesReviewed: z.number(),
|
|
1665
|
+
filesPassed: z.number(),
|
|
1666
|
+
filesFixed: z.number(),
|
|
1667
|
+
filesFailed: z.number(),
|
|
1668
|
+
results: z.array(AiFileResult),
|
|
1669
|
+
totalDurationMs: z.number(),
|
|
1670
|
+
tokensUsed: z.number()
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
// src/reporting/index.ts
|
|
1674
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync } from "fs";
|
|
1675
|
+
import { join as join10 } from "path";
|
|
1676
|
+
import consola2 from "consola";
|
|
1677
|
+
function createReport(parts) {
|
|
1678
|
+
return {
|
|
1679
|
+
version: "2.0",
|
|
1680
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1681
|
+
...parts
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
function saveReport(root, report) {
|
|
1685
|
+
const dir = join10(root, ".cmsassets-agent");
|
|
1686
|
+
if (!existsSync6(dir)) {
|
|
1687
|
+
mkdirSync(dir, { recursive: true });
|
|
1688
|
+
}
|
|
1689
|
+
const filename = `report-${Date.now()}.json`;
|
|
1690
|
+
const filePath = join10(dir, filename);
|
|
1691
|
+
writeFileSync4(filePath, JSON.stringify(report, null, 2), "utf-8");
|
|
1692
|
+
consola2.info(`Report saved to ${filePath}`);
|
|
1693
|
+
return filePath;
|
|
1694
|
+
}
|
|
1695
|
+
function savePlanFile(root, plan) {
|
|
1696
|
+
const filePath = join10(root, "cmsassets-agent.plan.json");
|
|
1697
|
+
writeFileSync4(filePath, JSON.stringify(plan, null, 2), "utf-8");
|
|
1698
|
+
consola2.info(`Plan saved to ${filePath}`);
|
|
1699
|
+
return filePath;
|
|
1700
|
+
}
|
|
1701
|
+
function loadPlanFile(filePath) {
|
|
1702
|
+
try {
|
|
1703
|
+
const raw = JSON.parse(readFileSync8(filePath, "utf-8"));
|
|
1704
|
+
const result = PatchPlan.safeParse(raw);
|
|
1705
|
+
if (result.success) return result.data;
|
|
1706
|
+
consola2.error("Plan file validation failed:", result.error.issues);
|
|
1707
|
+
return null;
|
|
1708
|
+
} catch (err) {
|
|
1709
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1710
|
+
consola2.error(`Failed to load plan file: ${msg}`);
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// src/git/index.ts
|
|
1716
|
+
import { execSync } from "child_process";
|
|
1717
|
+
import consola3 from "consola";
|
|
1718
|
+
function exec(cmd, cwd) {
|
|
1719
|
+
try {
|
|
1720
|
+
return execSync(cmd, { cwd, encoding: "utf-8", stdio: "pipe" }).trim();
|
|
1721
|
+
} catch {
|
|
1722
|
+
return "";
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
var gitOps = {
|
|
1726
|
+
isGitRepo(cwd) {
|
|
1727
|
+
return exec("git rev-parse --is-inside-work-tree", cwd) === "true";
|
|
1728
|
+
},
|
|
1729
|
+
isClean(cwd) {
|
|
1730
|
+
return exec("git diff --quiet && git diff --cached --quiet && echo clean", cwd) === "clean";
|
|
1731
|
+
},
|
|
1732
|
+
getCurrentBranch(cwd) {
|
|
1733
|
+
const branch = exec("git branch --show-current", cwd);
|
|
1734
|
+
return branch || null;
|
|
1735
|
+
},
|
|
1736
|
+
getHeadCommit(cwd) {
|
|
1737
|
+
const hash = exec("git rev-parse --short HEAD", cwd);
|
|
1738
|
+
return hash || null;
|
|
1739
|
+
},
|
|
1740
|
+
createBranch(cwd, name) {
|
|
1741
|
+
try {
|
|
1742
|
+
execSync(`git checkout -b ${name}`, { cwd, stdio: "pipe" });
|
|
1743
|
+
return true;
|
|
1744
|
+
} catch {
|
|
1745
|
+
consola3.warn(`Failed to create branch: ${name}`);
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
stageAll(cwd) {
|
|
1750
|
+
execSync("git add -A", { cwd, stdio: "pipe" });
|
|
1751
|
+
},
|
|
1752
|
+
commit(cwd, message) {
|
|
1753
|
+
try {
|
|
1754
|
+
execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
|
|
1755
|
+
cwd,
|
|
1756
|
+
stdio: "pipe"
|
|
1757
|
+
});
|
|
1758
|
+
return exec("git rev-parse --short HEAD", cwd);
|
|
1759
|
+
} catch {
|
|
1760
|
+
consola3.warn("Failed to create commit");
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
},
|
|
1764
|
+
hardResetToCommit(cwd, commitHash) {
|
|
1765
|
+
try {
|
|
1766
|
+
execSync(`git reset --hard ${commitHash}`, { cwd, stdio: "pipe" });
|
|
1767
|
+
return true;
|
|
1768
|
+
} catch {
|
|
1769
|
+
consola3.error(`Failed to hard reset to ${commitHash}`);
|
|
1770
|
+
return false;
|
|
1771
|
+
}
|
|
1772
|
+
},
|
|
1773
|
+
revertCommit(cwd, commitHash) {
|
|
1774
|
+
try {
|
|
1775
|
+
execSync(`git revert --no-edit ${commitHash}`, { cwd, stdio: "pipe" });
|
|
1776
|
+
return true;
|
|
1777
|
+
} catch {
|
|
1778
|
+
consola3.error(`Failed to revert commit ${commitHash}`);
|
|
1779
|
+
return false;
|
|
1780
|
+
}
|
|
1781
|
+
},
|
|
1782
|
+
getLastCommitByAgent(cwd) {
|
|
1783
|
+
const log = exec(
|
|
1784
|
+
'git log --oneline --all --grep="cmsassets-agent" -n 1 --format="%H"',
|
|
1785
|
+
cwd
|
|
1786
|
+
);
|
|
1787
|
+
return log || null;
|
|
1788
|
+
},
|
|
1789
|
+
getCommitBefore(cwd, commitHash) {
|
|
1790
|
+
const parent = exec(`git rev-parse ${commitHash}~1`, cwd);
|
|
1791
|
+
return parent || null;
|
|
1792
|
+
},
|
|
1793
|
+
push(cwd, remote = "origin") {
|
|
1794
|
+
try {
|
|
1795
|
+
const branch = exec("git branch --show-current", cwd);
|
|
1796
|
+
execSync(`git push -u ${remote} ${branch}`, { cwd, stdio: "pipe" });
|
|
1797
|
+
return true;
|
|
1798
|
+
} catch {
|
|
1799
|
+
consola3.warn("Failed to push to remote");
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
|
|
1805
|
+
// src/verifier/index.ts
|
|
1806
|
+
import { execSync as execSync2 } from "child_process";
|
|
1807
|
+
import consola4 from "consola";
|
|
1808
|
+
|
|
1809
|
+
// src/verifier/profiles.ts
|
|
1810
|
+
function runScript(pm, script) {
|
|
1811
|
+
if (pm === "pnpm") return `pnpm run ${script}`;
|
|
1812
|
+
if (pm === "yarn") return `yarn run ${script}`;
|
|
1813
|
+
if (pm === "bun") return `bun run ${script}`;
|
|
1814
|
+
return `npm run ${script}`;
|
|
1815
|
+
}
|
|
1816
|
+
function runBin(pm, binAndArgs) {
|
|
1817
|
+
if (pm === "pnpm") return `pnpm exec ${binAndArgs}`;
|
|
1818
|
+
if (pm === "yarn") return `yarn ${binAndArgs}`;
|
|
1819
|
+
if (pm === "bun") return `bunx ${binAndArgs}`;
|
|
1820
|
+
return `npx ${binAndArgs}`;
|
|
1821
|
+
}
|
|
1822
|
+
function getQuickSteps(framework, pm) {
|
|
1823
|
+
const steps = [];
|
|
1824
|
+
switch (framework) {
|
|
1825
|
+
case "nuxt":
|
|
1826
|
+
case "nuxt2":
|
|
1827
|
+
steps.push({
|
|
1828
|
+
name: "nuxt-prepare",
|
|
1829
|
+
command: runBin(pm, "nuxt prepare"),
|
|
1830
|
+
required: false
|
|
1831
|
+
});
|
|
1832
|
+
break;
|
|
1833
|
+
case "next":
|
|
1834
|
+
steps.push({
|
|
1835
|
+
name: "tsc-noEmit",
|
|
1836
|
+
command: runBin(pm, "tsc --noEmit"),
|
|
1837
|
+
required: false
|
|
1838
|
+
});
|
|
1839
|
+
break;
|
|
1840
|
+
case "astro":
|
|
1841
|
+
steps.push({
|
|
1842
|
+
name: "astro-check",
|
|
1843
|
+
command: runBin(pm, "astro check"),
|
|
1844
|
+
required: false
|
|
1845
|
+
});
|
|
1846
|
+
break;
|
|
1847
|
+
case "sveltekit":
|
|
1848
|
+
steps.push({
|
|
1849
|
+
name: "svelte-check",
|
|
1850
|
+
command: runBin(pm, "svelte-check"),
|
|
1851
|
+
required: false
|
|
1852
|
+
});
|
|
1853
|
+
break;
|
|
1854
|
+
default:
|
|
1855
|
+
steps.push({
|
|
1856
|
+
name: "tsc-noEmit",
|
|
1857
|
+
command: runBin(pm, "tsc --noEmit"),
|
|
1858
|
+
required: false
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
return steps;
|
|
1862
|
+
}
|
|
1863
|
+
function getFullSteps(framework, pm) {
|
|
1864
|
+
const steps = getQuickSteps(framework, pm);
|
|
1865
|
+
steps.push({
|
|
1866
|
+
name: "lint",
|
|
1867
|
+
command: runScript(pm, "lint"),
|
|
1868
|
+
required: false
|
|
1869
|
+
});
|
|
1870
|
+
switch (framework) {
|
|
1871
|
+
case "nuxt":
|
|
1872
|
+
case "nuxt2":
|
|
1873
|
+
steps.push({ name: "build", command: runScript(pm, "build"), required: true });
|
|
1874
|
+
break;
|
|
1875
|
+
case "next":
|
|
1876
|
+
steps.push({ name: "build", command: runScript(pm, "build"), required: true });
|
|
1877
|
+
break;
|
|
1878
|
+
case "astro":
|
|
1879
|
+
steps.push({ name: "build", command: runScript(pm, "build"), required: true });
|
|
1880
|
+
break;
|
|
1881
|
+
case "sveltekit":
|
|
1882
|
+
steps.push({ name: "build", command: runScript(pm, "build"), required: true });
|
|
1883
|
+
break;
|
|
1884
|
+
default:
|
|
1885
|
+
steps.push({ name: "build", command: runScript(pm, "build"), required: false });
|
|
1886
|
+
}
|
|
1887
|
+
steps.push({
|
|
1888
|
+
name: "test",
|
|
1889
|
+
command: runScript(pm, "test"),
|
|
1890
|
+
required: false
|
|
1891
|
+
});
|
|
1892
|
+
return steps;
|
|
1893
|
+
}
|
|
1894
|
+
function getVerifySteps(profile, framework, packageManager) {
|
|
1895
|
+
return profile === "full" ? getFullSteps(framework, packageManager) : getQuickSteps(framework, packageManager);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/verifier/index.ts
|
|
1899
|
+
function verify(root, opts = {}) {
|
|
1900
|
+
const startTime = Date.now();
|
|
1901
|
+
const profile = opts.profile ?? "quick";
|
|
1902
|
+
const framework = opts.framework ?? "unknown";
|
|
1903
|
+
const packageManager = opts.packageManager ?? "npm";
|
|
1904
|
+
const patchedFiles = opts.patchedFiles ?? [];
|
|
1905
|
+
const warnings = [];
|
|
1906
|
+
const steps = getVerifySteps(profile, framework, packageManager);
|
|
1907
|
+
let lintPassed = null;
|
|
1908
|
+
let buildPassed = null;
|
|
1909
|
+
let testsPassed = null;
|
|
1910
|
+
const stepLogs = [];
|
|
1911
|
+
for (const step of steps) {
|
|
1912
|
+
consola4.info(`Running: ${step.name} (${step.command})`);
|
|
1913
|
+
try {
|
|
1914
|
+
const output = execSync2(step.command, {
|
|
1915
|
+
cwd: root,
|
|
1916
|
+
stdio: "pipe",
|
|
1917
|
+
timeout: 12e4
|
|
1918
|
+
}).toString();
|
|
1919
|
+
if (step.name === "lint") lintPassed = true;
|
|
1920
|
+
if (step.name === "build") buildPassed = true;
|
|
1921
|
+
if (step.name === "test") testsPassed = true;
|
|
1922
|
+
consola4.success(`${step.name} passed`);
|
|
1923
|
+
stepLogs.push({
|
|
1924
|
+
name: step.name,
|
|
1925
|
+
command: step.command,
|
|
1926
|
+
passed: true,
|
|
1927
|
+
required: step.required,
|
|
1928
|
+
output: output.substring(0, 2e3)
|
|
1929
|
+
});
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
const stderr = typeof err === "object" && err && "stderr" in err ? String(err.stderr ?? "") : "";
|
|
1932
|
+
const stdout = typeof err === "object" && err && "stdout" in err ? String(err.stdout ?? "") : "";
|
|
1933
|
+
const message = [stderr, stdout, err instanceof Error ? err.message : String(err)].filter(Boolean).join("\n");
|
|
1934
|
+
const truncated = message.substring(0, 500);
|
|
1935
|
+
if (step.name === "lint") lintPassed = false;
|
|
1936
|
+
if (step.name === "build") buildPassed = false;
|
|
1937
|
+
if (step.name === "test") testsPassed = false;
|
|
1938
|
+
if (step.required) {
|
|
1939
|
+
consola4.error(`${step.name} FAILED: ${truncated}`);
|
|
1940
|
+
warnings.push(`${step.name} failed (required): ${truncated}`);
|
|
1941
|
+
} else {
|
|
1942
|
+
consola4.warn(`${step.name} failed (optional): ${truncated}`);
|
|
1943
|
+
warnings.push(`${step.name} failed (optional): ${truncated}`);
|
|
1944
|
+
}
|
|
1945
|
+
stepLogs.push({
|
|
1946
|
+
name: step.name,
|
|
1947
|
+
command: step.command,
|
|
1948
|
+
passed: false,
|
|
1949
|
+
required: step.required,
|
|
1950
|
+
output: truncated.substring(0, 2e3)
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
return {
|
|
1955
|
+
schemaVersion: "2.0",
|
|
1956
|
+
profile,
|
|
1957
|
+
lintPassed,
|
|
1958
|
+
buildPassed,
|
|
1959
|
+
testsPassed,
|
|
1960
|
+
patchedFiles,
|
|
1961
|
+
warnings,
|
|
1962
|
+
steps: stepLogs,
|
|
1963
|
+
durationMs: Date.now() - startTime
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
export {
|
|
1968
|
+
scan,
|
|
1969
|
+
createPlan,
|
|
1970
|
+
applyPlan,
|
|
1971
|
+
FrameworkName,
|
|
1972
|
+
CmsType,
|
|
1973
|
+
PackageManager,
|
|
1974
|
+
Confidence,
|
|
1975
|
+
InjectionType,
|
|
1976
|
+
VerifyProfile,
|
|
1977
|
+
PatchMode,
|
|
1978
|
+
FrameworkInfo,
|
|
1979
|
+
CmsInfo,
|
|
1980
|
+
InjectionCandidate,
|
|
1981
|
+
CandidateRegionType,
|
|
1982
|
+
CandidateRegion,
|
|
1983
|
+
CandidateGraph,
|
|
1984
|
+
ScanResult,
|
|
1985
|
+
FilePatch,
|
|
1986
|
+
PatchPlan,
|
|
1987
|
+
PatchMethod,
|
|
1988
|
+
DecisionReasonCode,
|
|
1989
|
+
DecisionLogEntry,
|
|
1990
|
+
PatchedFileReport,
|
|
1991
|
+
ApplyReport,
|
|
1992
|
+
VerifyStepLog,
|
|
1993
|
+
VerifyReport,
|
|
1994
|
+
AiFileResult,
|
|
1995
|
+
AiReviewReport,
|
|
1996
|
+
createReport,
|
|
1997
|
+
saveReport,
|
|
1998
|
+
savePlanFile,
|
|
1999
|
+
loadPlanFile,
|
|
2000
|
+
gitOps,
|
|
2001
|
+
verify
|
|
2002
|
+
};
|