@synchronized-studio/cmsassets-agent 0.1.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.
@@ -0,0 +1,2296 @@
1
+ // src/scanner/index.ts
2
+ import { resolve } from "path";
3
+
4
+ // src/scanner/detectFramework.ts
5
+ import { existsSync, readFileSync } from "fs";
6
+ import { join } from "path";
7
+ var FRAMEWORK_SIGNATURES = {
8
+ nuxt: {
9
+ packages: ["nuxt"],
10
+ configFiles: ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"]
11
+ },
12
+ next: {
13
+ packages: ["next"],
14
+ configFiles: ["next.config.js", "next.config.mjs", "next.config.ts"]
15
+ },
16
+ remix: {
17
+ packages: ["@remix-run/react", "@remix-run/node", "@remix-run/dev"],
18
+ configFiles: ["remix.config.js", "remix.config.ts"]
19
+ },
20
+ astro: {
21
+ packages: ["astro"],
22
+ configFiles: ["astro.config.mjs", "astro.config.ts", "astro.config.js"]
23
+ },
24
+ sveltekit: {
25
+ packages: ["@sveltejs/kit"],
26
+ configFiles: ["svelte.config.js", "svelte.config.ts"]
27
+ },
28
+ hono: {
29
+ packages: ["hono"],
30
+ configFiles: []
31
+ },
32
+ fastify: {
33
+ packages: ["fastify"],
34
+ configFiles: []
35
+ },
36
+ express: {
37
+ packages: ["express"],
38
+ configFiles: []
39
+ }
40
+ };
41
+ var DETECTION_ORDER = [
42
+ "nuxt",
43
+ "next",
44
+ "remix",
45
+ "astro",
46
+ "sveltekit",
47
+ "hono",
48
+ "fastify",
49
+ "express"
50
+ ];
51
+ function readPackageJson(root) {
52
+ const pkgPath = join(root, "package.json");
53
+ if (!existsSync(pkgPath)) return null;
54
+ try {
55
+ const raw = readFileSync(pkgPath, "utf-8");
56
+ const pkg = JSON.parse(raw);
57
+ return {
58
+ dependencies: pkg.dependencies ?? {},
59
+ devDependencies: pkg.devDependencies ?? {}
60
+ };
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+ function getVersionFromPkg(allDeps, packages) {
66
+ for (const pkg of packages) {
67
+ if (allDeps[pkg]) return allDeps[pkg];
68
+ }
69
+ return "";
70
+ }
71
+ function detectFramework(root) {
72
+ const pkg = readPackageJson(root);
73
+ const allDeps = {
74
+ ...pkg?.dependencies ?? {},
75
+ ...pkg?.devDependencies ?? {}
76
+ };
77
+ for (const name of DETECTION_ORDER) {
78
+ const sig = FRAMEWORK_SIGNATURES[name];
79
+ const hasPackage = sig.packages.some((p) => p in allDeps);
80
+ const configFile = sig.configFiles.find((f) => existsSync(join(root, f))) ?? null;
81
+ if (hasPackage || configFile) {
82
+ let resolvedName = name;
83
+ const version = getVersionFromPkg(allDeps, sig.packages);
84
+ if (name === "nuxt") {
85
+ const majorVersion = parseMajorVersion(version);
86
+ if (majorVersion !== null && majorVersion < 3) {
87
+ resolvedName = "nuxt2";
88
+ }
89
+ }
90
+ return {
91
+ name: resolvedName,
92
+ version,
93
+ configFile
94
+ };
95
+ }
96
+ }
97
+ return { name: "unknown", version: "", configFile: null };
98
+ }
99
+ function parseMajorVersion(versionRange) {
100
+ const cleaned = versionRange.replace(/^[\^~>=<\s]+/, "");
101
+ const major = parseInt(cleaned, 10);
102
+ return Number.isFinite(major) ? major : null;
103
+ }
104
+
105
+ // src/scanner/detectCms.ts
106
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
107
+ import { join as join2, relative } from "path";
108
+ import fg from "fast-glob";
109
+ var CMS_SIGNATURES = {
110
+ prismic: {
111
+ packages: [
112
+ "@prismicio/client",
113
+ "@prismicio/vue",
114
+ "@prismicio/react",
115
+ "@prismicio/next",
116
+ "@prismicio/svelte",
117
+ "@nuxtjs/prismic"
118
+ ],
119
+ urlPatterns: [
120
+ /https?:\/\/([a-z0-9-]+)\.cdn\.prismic\.io/gi,
121
+ /https?:\/\/images\.prismic\.io\/([a-z0-9-]+)/gi
122
+ ],
123
+ paramExtractors: {
124
+ repository: (match) => {
125
+ const m1 = match.match(/https?:\/\/([a-z0-9-]+)\.cdn\.prismic\.io/);
126
+ if (m1) return m1[1];
127
+ const m2 = match.match(/https?:\/\/images\.prismic\.io\/([a-z0-9-]+)/);
128
+ if (m2) return m2[1];
129
+ return null;
130
+ }
131
+ }
132
+ },
133
+ contentful: {
134
+ packages: ["contentful"],
135
+ urlPatterns: [
136
+ /https?:\/\/images\.ctfassets\.net\/([a-z0-9]+)/gi,
137
+ /https?:\/\/videos\.ctfassets\.net\/([a-z0-9]+)/gi,
138
+ /https?:\/\/cdn\.contentful\.com\/spaces\/([a-z0-9]+)/gi
139
+ ],
140
+ paramExtractors: {
141
+ spaceId: (match) => {
142
+ const m = match.match(/ctfassets\.net\/([a-z0-9]+)/) || match.match(/spaces\/([a-z0-9]+)/);
143
+ return m?.[1] ?? null;
144
+ }
145
+ }
146
+ },
147
+ sanity: {
148
+ packages: ["@sanity/client", "next-sanity", "@nuxtjs/sanity", "sanity"],
149
+ urlPatterns: [
150
+ /https?:\/\/cdn\.sanity\.io\/(images|files)\/([a-z0-9]+)\/([a-z0-9-]+)/gi
151
+ ],
152
+ paramExtractors: {
153
+ projectId: (match) => {
154
+ const m = match.match(/cdn\.sanity\.io\/(?:images|files)\/([a-z0-9]+)/);
155
+ return m?.[1] ?? null;
156
+ },
157
+ dataset: (match) => {
158
+ const m = match.match(
159
+ /cdn\.sanity\.io\/(?:images|files)\/[a-z0-9]+\/([a-z0-9-]+)/
160
+ );
161
+ return m?.[1] ?? null;
162
+ }
163
+ }
164
+ },
165
+ shopify: {
166
+ packages: [
167
+ "@shopify/storefront-api-client",
168
+ "@shopify/hydrogen",
169
+ "@shopify/shopify-api"
170
+ ],
171
+ urlPatterns: [
172
+ /https?:\/\/([a-z0-9-]+)\.myshopify\.com\/cdn\//gi,
173
+ /https?:\/\/([a-z0-9-]+)\.myshopify\.com\/api\//gi
174
+ ],
175
+ paramExtractors: {
176
+ storeDomain: (match) => {
177
+ const m = match.match(/https?:\/\/([a-z0-9-]+\.myshopify\.com)/);
178
+ return m?.[1] ?? null;
179
+ }
180
+ }
181
+ },
182
+ cloudinary: {
183
+ packages: ["cloudinary", "@cloudinary/url-gen", "@cloudinary/react"],
184
+ urlPatterns: [/https?:\/\/res\.cloudinary\.com\/([a-z0-9_-]+)/gi],
185
+ paramExtractors: {
186
+ cloudName: (match) => {
187
+ const m = match.match(/res\.cloudinary\.com\/([a-z0-9_-]+)/);
188
+ return m?.[1] ?? null;
189
+ }
190
+ }
191
+ },
192
+ imgix: {
193
+ packages: ["@imgix/js-core", "react-imgix", "vue-imgix"],
194
+ urlPatterns: [/https?:\/\/([a-z0-9-]+)\.imgix\.net/gi],
195
+ paramExtractors: {
196
+ imgixDomain: (match) => {
197
+ const m = match.match(/https?:\/\/([a-z0-9-]+\.imgix\.net)/);
198
+ return m?.[1] ?? null;
199
+ }
200
+ }
201
+ }
202
+ };
203
+ var CMS_DETECTION_ORDER = [
204
+ "prismic",
205
+ "contentful",
206
+ "sanity",
207
+ "shopify",
208
+ "cloudinary",
209
+ "imgix"
210
+ ];
211
+ var SOURCE_GLOBS = [
212
+ "**/*.ts",
213
+ "**/*.tsx",
214
+ "**/*.js",
215
+ "**/*.jsx",
216
+ "**/*.vue",
217
+ "**/*.svelte",
218
+ "**/*.astro",
219
+ "**/*.mjs",
220
+ "**/*.mts"
221
+ ];
222
+ var IGNORE_DIRS = [
223
+ "node_modules",
224
+ ".nuxt",
225
+ ".next",
226
+ ".output",
227
+ ".svelte-kit",
228
+ "dist",
229
+ "build",
230
+ ".git",
231
+ "coverage",
232
+ ".cache"
233
+ ];
234
+ function detectCms(root) {
235
+ const pkgPath = join2(root, "package.json");
236
+ let allDeps = {};
237
+ if (existsSync2(pkgPath)) {
238
+ try {
239
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
240
+ allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
241
+ } catch {
242
+ }
243
+ }
244
+ for (const cmsName of CMS_DETECTION_ORDER) {
245
+ const sig = CMS_SIGNATURES[cmsName];
246
+ if (sig.packages.some((p) => p in allDeps)) {
247
+ const params = extractParamsFromSource(root, cmsName);
248
+ return {
249
+ type: cmsName,
250
+ params,
251
+ detectedFrom: ["package.json"]
252
+ };
253
+ }
254
+ }
255
+ const sourceFiles = fg.sync(SOURCE_GLOBS, {
256
+ cwd: root,
257
+ ignore: IGNORE_DIRS.map((d) => `${d}/**`),
258
+ absolute: true,
259
+ onlyFiles: true,
260
+ deep: 6
261
+ });
262
+ for (const cmsName of CMS_DETECTION_ORDER) {
263
+ const sig = CMS_SIGNATURES[cmsName];
264
+ const matchingFiles = [];
265
+ const foundParams = {};
266
+ for (const file of sourceFiles) {
267
+ let content;
268
+ try {
269
+ content = readFileSync2(file, "utf-8");
270
+ } catch {
271
+ continue;
272
+ }
273
+ for (const pattern of sig.urlPatterns) {
274
+ pattern.lastIndex = 0;
275
+ const matches = content.match(pattern);
276
+ if (matches && matches.length > 0) {
277
+ matchingFiles.push(relative(root, file));
278
+ for (const m of matches) {
279
+ for (const [paramName, extractor] of Object.entries(sig.paramExtractors)) {
280
+ if (!foundParams[paramName]) {
281
+ const val = extractor(m);
282
+ if (val) foundParams[paramName] = val;
283
+ }
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+ if (matchingFiles.length > 0) {
290
+ return {
291
+ type: cmsName,
292
+ params: foundParams,
293
+ detectedFrom: [...new Set(matchingFiles)]
294
+ };
295
+ }
296
+ }
297
+ const genericPatterns = [
298
+ /https?:\/\/[a-z0-9-]+\.s3[.-][a-z0-9-]*\.amazonaws\.com/gi,
299
+ /https?:\/\/storage\.googleapis\.com\/[a-z0-9-]+/gi,
300
+ /https?:\/\/[a-z0-9-]+\.r2\.cloudflarestorage\.com/gi
301
+ ];
302
+ for (const file of sourceFiles) {
303
+ let content;
304
+ try {
305
+ content = readFileSync2(file, "utf-8");
306
+ } catch {
307
+ continue;
308
+ }
309
+ for (const pattern of genericPatterns) {
310
+ pattern.lastIndex = 0;
311
+ const m = content.match(pattern);
312
+ if (m && m.length > 0) {
313
+ return {
314
+ type: "generic",
315
+ params: { originUrl: m[0] },
316
+ detectedFrom: [relative(root, file)]
317
+ };
318
+ }
319
+ }
320
+ }
321
+ return { type: "unknown", params: {}, detectedFrom: [] };
322
+ }
323
+ var CONFIG_FILES = [
324
+ "slicemachine.config.json",
325
+ "prismicio.config.ts",
326
+ "prismicio.config.js",
327
+ "sm.json",
328
+ ".slicemachine.config.json",
329
+ "nuxt.config.ts",
330
+ "nuxt.config.js",
331
+ "next.config.js",
332
+ "next.config.mjs",
333
+ "next.config.ts",
334
+ "sanity.config.ts",
335
+ "sanity.config.js",
336
+ "sanity.cli.ts",
337
+ "sanity.cli.js"
338
+ ];
339
+ var CONFIG_PARAM_PATTERNS = {
340
+ prismic: [
341
+ { param: "repository", patterns: [
342
+ /["']?repositoryName["']?\s*[:=]\s*["']([a-z0-9-]+)["']/i,
343
+ /https?:\/\/([a-z0-9-]+)\.cdn\.prismic\.io/i,
344
+ /["']?apiEndpoint["']?\s*[:=]\s*["']https?:\/\/([a-z0-9-]+)\.cdn\.prismic\.io/i
345
+ ] }
346
+ ],
347
+ contentful: [
348
+ { param: "spaceId", patterns: [
349
+ /["']?space["']?\s*[:=]\s*["']([a-z0-9]+)["']/i,
350
+ /["']?spaceId["']?\s*[:=]\s*["']([a-z0-9]+)["']/i
351
+ ] }
352
+ ],
353
+ sanity: [
354
+ { param: "projectId", patterns: [
355
+ /["']?projectId["']?\s*[:=]\s*["']([a-z0-9]+)["']/i
356
+ ] },
357
+ { param: "dataset", patterns: [
358
+ /["']?dataset["']?\s*[:=]\s*["']([a-z0-9-]+)["']/i
359
+ ] }
360
+ ],
361
+ shopify: [
362
+ { param: "storeDomain", patterns: [
363
+ /["']?storeDomain["']?\s*[:=]\s*["']([a-z0-9-]+\.myshopify\.com)["']/i
364
+ ] }
365
+ ],
366
+ cloudinary: [
367
+ { param: "cloudName", patterns: [
368
+ /["']?cloud_name["']?\s*[:=]\s*["']([a-z0-9_-]+)["']/i,
369
+ /["']?cloudName["']?\s*[:=]\s*["']([a-z0-9_-]+)["']/i
370
+ ] }
371
+ ],
372
+ imgix: [
373
+ { param: "imgixDomain", patterns: [
374
+ /["']?domain["']?\s*[:=]\s*["']([a-z0-9-]+\.imgix\.net)["']/i
375
+ ] }
376
+ ]
377
+ };
378
+ function extractParamsFromSource(root, cmsName) {
379
+ const sig = CMS_SIGNATURES[cmsName];
380
+ if (!sig) return {};
381
+ const params = {};
382
+ const configPatterns = CONFIG_PARAM_PATTERNS[cmsName];
383
+ if (configPatterns) {
384
+ for (const cfgFile of CONFIG_FILES) {
385
+ const cfgPath = join2(root, cfgFile);
386
+ if (!existsSync2(cfgPath)) continue;
387
+ let content;
388
+ try {
389
+ content = readFileSync2(cfgPath, "utf-8");
390
+ } catch {
391
+ continue;
392
+ }
393
+ for (const { param, patterns } of configPatterns) {
394
+ if (params[param]) continue;
395
+ for (const pattern of patterns) {
396
+ const m = content.match(pattern);
397
+ if (m?.[1]) {
398
+ params[param] = m[1];
399
+ break;
400
+ }
401
+ }
402
+ }
403
+ }
404
+ if (Object.keys(params).length === Object.keys(sig.paramExtractors).length) {
405
+ return params;
406
+ }
407
+ }
408
+ const sourceFiles = fg.sync(SOURCE_GLOBS, {
409
+ cwd: root,
410
+ ignore: IGNORE_DIRS.map((d) => `${d}/**`),
411
+ absolute: true,
412
+ onlyFiles: true,
413
+ deep: 6
414
+ });
415
+ for (const file of sourceFiles) {
416
+ let content;
417
+ try {
418
+ content = readFileSync2(file, "utf-8");
419
+ } catch {
420
+ continue;
421
+ }
422
+ for (const pattern of sig.urlPatterns) {
423
+ pattern.lastIndex = 0;
424
+ const matches = content.match(pattern);
425
+ if (!matches) continue;
426
+ for (const m of matches) {
427
+ for (const [paramName, extractor] of Object.entries(sig.paramExtractors)) {
428
+ if (!params[paramName]) {
429
+ const val = extractor(m);
430
+ if (val) params[paramName] = val;
431
+ }
432
+ }
433
+ }
434
+ }
435
+ if (Object.keys(params).length === Object.keys(sig.paramExtractors).length) {
436
+ break;
437
+ }
438
+ }
439
+ return params;
440
+ }
441
+
442
+ // src/scanner/detectPackageManager.ts
443
+ import { existsSync as existsSync3 } from "fs";
444
+ import { join as join3 } from "path";
445
+ var LOCKFILE_MAP = [
446
+ ["bun.lockb", "bun"],
447
+ ["bun.lock", "bun"],
448
+ ["pnpm-lock.yaml", "pnpm"],
449
+ ["yarn.lock", "yarn"],
450
+ ["package-lock.json", "npm"]
451
+ ];
452
+ function detectPackageManager(root) {
453
+ for (const [lockfile, pm] of LOCKFILE_MAP) {
454
+ if (existsSync3(join3(root, lockfile))) return pm;
455
+ }
456
+ return "npm";
457
+ }
458
+
459
+ // src/scanner/findInjectionPoints.ts
460
+ import { readFileSync as readFileSync3 } from "fs";
461
+ import { relative as relative2 } from "path";
462
+ import fg2 from "fast-glob";
463
+ var IGNORE_DIRS2 = [
464
+ "node_modules",
465
+ ".nuxt",
466
+ ".next",
467
+ ".output",
468
+ ".svelte-kit",
469
+ "dist",
470
+ "build",
471
+ ".git",
472
+ "coverage",
473
+ ".cache"
474
+ ];
475
+ var TEST_PATTERNS = [
476
+ /\.test\.[jt]sx?$/,
477
+ /\.spec\.[jt]sx?$/,
478
+ /__tests__\//,
479
+ /\/tests?\//,
480
+ /\.stories\.[jt]sx?$/,
481
+ /\/fixtures?\//,
482
+ /\/mocks?\//
483
+ ];
484
+ var CMS_FETCH_PATTERNS = [
485
+ /prismic/i,
486
+ /contentful/i,
487
+ /sanity/i,
488
+ /shopify/i,
489
+ /cloudinary/i,
490
+ /imgix/i,
491
+ /ctfassets/i,
492
+ /cdn\.prismic/i,
493
+ /cdn\.sanity/i,
494
+ /myshopify/i,
495
+ /res\.cloudinary/i
496
+ ];
497
+ function hasCmsFetchSignal(content) {
498
+ return CMS_FETCH_PATTERNS.some((p) => p.test(content));
499
+ }
500
+ function isTestFile(filePath) {
501
+ return TEST_PATTERNS.some((p) => p.test(filePath));
502
+ }
503
+ function computeScore(signals) {
504
+ let score = 0;
505
+ if (signals.hasCmsFetch) score += 40;
506
+ if (signals.isServerSide) score += 30;
507
+ if (signals.returnsJson) score += 20;
508
+ if (signals.isCentralLayer) score += 10;
509
+ if (signals.isTest) score -= 20;
510
+ return Math.max(0, Math.min(100, score));
511
+ }
512
+ function scoreToConfidence(score) {
513
+ if (score >= 80) return "high";
514
+ if (score >= 60) return "medium";
515
+ return "low";
516
+ }
517
+ function buildReasons(signals) {
518
+ const reasons = [];
519
+ if (signals.hasCmsFetch) reasons.push("File contains CMS SDK import or fetch to CMS domain");
520
+ if (signals.isServerSide) reasons.push("Server-side file (API route, loader, SSR handler)");
521
+ if (signals.returnsJson) reasons.push("Returns JSON data (return statement / res.json)");
522
+ if (signals.isCentralLayer) reasons.push("Centralized data layer (composable, service, helper)");
523
+ if (signals.isTest) reasons.push("Test/fixture file (score reduced)");
524
+ return reasons;
525
+ }
526
+ function getContextLines(content, line, range = 2) {
527
+ const lines = content.split("\n");
528
+ const start = Math.max(0, line - 1 - range);
529
+ const end = Math.min(lines.length, line + range);
530
+ return lines.slice(start, end).join("\n");
531
+ }
532
+ var NON_DATA_RETURN_PATTERNS = [
533
+ /^return\s+(true|false|null|undefined|void)\b/,
534
+ /^return\s+["'`]/,
535
+ // return "string"
536
+ /^return\s+\d/,
537
+ // return 42
538
+ /^return\s+\w+\s*(===|!==|==|!=|>|<|>=|<=)/,
539
+ // return x === "true"
540
+ /^return\s+!\w/,
541
+ // return !value
542
+ /^return\s*$/,
543
+ // bare return
544
+ /^return\s*;?\s*$/,
545
+ // return;
546
+ /^return\s+err\b/,
547
+ // return err / error
548
+ /^return\s+error\b/,
549
+ /^return\s+\[\s*\]/,
550
+ // return []
551
+ /^return\s+\{\s*\}/,
552
+ // return {}
553
+ /^return\s+dispatch\s*\(/,
554
+ // return dispatch(...) -- Vuex forwarding
555
+ /^return\s+commit\s*\(/,
556
+ // return commit(...)
557
+ /^return\s+_get\s*\(/,
558
+ // return _get(...) -- lodash getter
559
+ /^return\s+_\w+\s*\(/,
560
+ // return _clone(...) etc -- lodash util
561
+ /^return\s+\w+\s*\?\s*/,
562
+ // return x ? y : z -- ternary
563
+ /^return\s+\w+\.\w+\s*(===|!==|==|!=)/,
564
+ // return link.link_type === "Web"
565
+ /^return\s+(axios|fetch|http|request)\b/,
566
+ // return axios / return fetch (the promise, not data)
567
+ /^return\s+res\.(json|send|status)\s*\(/,
568
+ // return res.json(...) -- handled separately as res.json type
569
+ /^return\s+"#"/
570
+ // return "#"
571
+ ];
572
+ var CALLBACK_PROPERTY_PATTERNS = [
573
+ /shouldBypassCache/,
574
+ /shouldInvalidateCache/,
575
+ /getKey/,
576
+ /onRequest/,
577
+ /onResponse/,
578
+ /onError/,
579
+ /transform\s*:/,
580
+ /validate\s*:/
581
+ ];
582
+ function isInsideCallbackOption(lines, returnLineIdx) {
583
+ for (let i = returnLineIdx - 1; i >= Math.max(0, returnLineIdx - 15); i--) {
584
+ const line = lines[i].trim();
585
+ if (CALLBACK_PROPERTY_PATTERNS.some((p) => p.test(line))) return true;
586
+ if (/^(export\s+default|async\s+function|function\s)/.test(line)) break;
587
+ if (/^(export\s+default\s+)?define\w+Handler/.test(line)) break;
588
+ }
589
+ return false;
590
+ }
591
+ function looksLikeDataReturn(trimmed) {
592
+ if (NON_DATA_RETURN_PATTERNS.some((p) => p.test(trimmed))) return false;
593
+ if (/^return\s+\{\s*\w/.test(trimmed)) return true;
594
+ if (/^return\s+\[\s*\w/.test(trimmed)) return true;
595
+ if (/^return\s+await\s+\w+\.\w+/.test(trimmed)) return true;
596
+ if (/^return\s+[a-zA-Z_]\w*\s*;?\s*$/.test(trimmed)) return true;
597
+ if (/^return\s+Response\.json\s*\(/.test(trimmed)) return true;
598
+ if (/^return\s+json\s*\(/.test(trimmed)) return true;
599
+ if (/^return\s+new\s+Response/.test(trimmed)) return true;
600
+ return false;
601
+ }
602
+ function findReturnStatements(content, filePath, cms, root, opts) {
603
+ const candidates = [];
604
+ const lines = content.split("\n");
605
+ const hasCms = hasCmsFetchSignal(content);
606
+ const relPath = relative2(root, filePath);
607
+ const isTest = isTestFile(relPath);
608
+ for (let i = 0; i < lines.length; i++) {
609
+ const line = lines[i];
610
+ const trimmed = line.trim();
611
+ if (/^return\s/.test(trimmed)) {
612
+ if (!looksLikeDataReturn(trimmed)) continue;
613
+ if (isInsideCallbackOption(lines, i)) continue;
614
+ const signals = {
615
+ hasCmsFetch: hasCms,
616
+ isServerSide: opts.isServerSide,
617
+ returnsJson: true,
618
+ isCentralLayer: opts.isCentralLayer,
619
+ isTest
620
+ };
621
+ const score = computeScore(signals);
622
+ if (score < 50 || !signals.hasCmsFetch && !signals.isServerSide) {
623
+ continue;
624
+ }
625
+ candidates.push({
626
+ filePath: relative2(root, filePath),
627
+ line: i + 1,
628
+ type: "return",
629
+ score,
630
+ confidence: scoreToConfidence(score),
631
+ targetCode: trimmed,
632
+ context: getContextLines(content, i + 1),
633
+ reasons: buildReasons(signals)
634
+ });
635
+ }
636
+ if (/res\.json\s*\(/.test(trimmed)) {
637
+ if (isInsideCallbackOption(lines, i)) continue;
638
+ const signals = {
639
+ hasCmsFetch: hasCms,
640
+ isServerSide: true,
641
+ returnsJson: true,
642
+ isCentralLayer: opts.isCentralLayer,
643
+ isTest
644
+ };
645
+ const score = computeScore(signals);
646
+ if (score < 50) {
647
+ continue;
648
+ }
649
+ candidates.push({
650
+ filePath: relative2(root, filePath),
651
+ line: i + 1,
652
+ type: "res.json",
653
+ score,
654
+ confidence: scoreToConfidence(score),
655
+ targetCode: trimmed,
656
+ context: getContextLines(content, i + 1),
657
+ reasons: buildReasons(signals)
658
+ });
659
+ }
660
+ }
661
+ return candidates;
662
+ }
663
+ var nuxtStrategy = {
664
+ globs: ["server/api/**/*.ts", "server/api/**/*.js", "server/routes/**/*.ts", "composables/**/*.ts", "composables/**/*.js"],
665
+ analyze(filePath, content, cms, root) {
666
+ const relPath = relative2(root, filePath);
667
+ const isServerApi = /server\/(api|routes)\//.test(filePath);
668
+ const isComposable = /composables\//.test(filePath);
669
+ const candidates = findReturnStatements(content, filePath, cms, root, {
670
+ isServerSide: isServerApi,
671
+ isCentralLayer: isComposable
672
+ });
673
+ const lines = content.split("\n");
674
+ for (let i = 0; i < lines.length; i++) {
675
+ const trimmed = lines[i].trim();
676
+ if (/\buseFetch\s*\(/.test(trimmed) && !/transform\s*:/.test(content.substring(content.indexOf(trimmed)))) {
677
+ const hasCms = hasCmsFetchSignal(content);
678
+ if (!hasCms) continue;
679
+ const signals = {
680
+ hasCmsFetch: hasCms,
681
+ isServerSide: false,
682
+ returnsJson: true,
683
+ isCentralLayer: isComposable,
684
+ isTest: isTestFile(relPath)
685
+ };
686
+ const score = computeScore(signals);
687
+ if (score < 50) continue;
688
+ candidates.push({
689
+ filePath: relPath,
690
+ line: i + 1,
691
+ type: "useFetch-transform",
692
+ score,
693
+ confidence: scoreToConfidence(score),
694
+ targetCode: trimmed,
695
+ context: getContextLines(content, i + 1),
696
+ reasons: [...buildReasons(signals), "useFetch call without transform option"]
697
+ });
698
+ }
699
+ if (/\buseAsyncData\s*\(/.test(trimmed)) {
700
+ const hasCms = hasCmsFetchSignal(content);
701
+ if (!hasCms) continue;
702
+ const signals = {
703
+ hasCmsFetch: hasCms,
704
+ isServerSide: false,
705
+ returnsJson: true,
706
+ isCentralLayer: isComposable,
707
+ isTest: isTestFile(relPath)
708
+ };
709
+ const score = computeScore(signals);
710
+ if (score < 50) continue;
711
+ candidates.push({
712
+ filePath: relPath,
713
+ line: i + 1,
714
+ type: "useAsyncData-transform",
715
+ score,
716
+ confidence: scoreToConfidence(score),
717
+ targetCode: trimmed,
718
+ context: getContextLines(content, i + 1),
719
+ reasons: [...buildReasons(signals), "useAsyncData call"]
720
+ });
721
+ }
722
+ }
723
+ return candidates;
724
+ }
725
+ };
726
+ var nextStrategy = {
727
+ globs: [
728
+ "app/**/page.tsx",
729
+ "app/**/page.ts",
730
+ "app/**/page.jsx",
731
+ "app/**/page.js",
732
+ "app/api/**/route.ts",
733
+ "app/api/**/route.js",
734
+ "pages/**/*.tsx",
735
+ "pages/**/*.ts",
736
+ "pages/**/*.jsx",
737
+ "pages/**/*.js",
738
+ "src/app/**/page.tsx",
739
+ "src/app/api/**/route.ts",
740
+ "src/pages/**/*.tsx",
741
+ "src/pages/**/*.ts",
742
+ "lib/**/*.ts",
743
+ "lib/**/*.js",
744
+ "src/lib/**/*.ts",
745
+ "src/lib/**/*.js"
746
+ ],
747
+ analyze(filePath, content, cms, root) {
748
+ const relPath = relative2(root, filePath);
749
+ const isApiRoute = /\/api\//.test(filePath) && /route\.[jt]sx?$/.test(filePath);
750
+ const isPage = /page\.[jt]sx?$/.test(filePath);
751
+ const isLib = /\blib\//.test(filePath);
752
+ const candidates = findReturnStatements(content, filePath, cms, root, {
753
+ isServerSide: isApiRoute || /getServerSideProps|getStaticProps/.test(content),
754
+ isCentralLayer: isLib
755
+ });
756
+ const lines = content.split("\n");
757
+ for (let i = 0; i < lines.length; i++) {
758
+ const trimmed = lines[i].trim();
759
+ if (/export\s+(async\s+)?function\s+getServerSideProps/.test(trimmed)) {
760
+ const signals = {
761
+ hasCmsFetch: hasCmsFetchSignal(content),
762
+ isServerSide: true,
763
+ returnsJson: true,
764
+ isCentralLayer: false,
765
+ isTest: isTestFile(relPath)
766
+ };
767
+ const score = computeScore(signals);
768
+ candidates.push({
769
+ filePath: relPath,
770
+ line: i + 1,
771
+ type: "getServerSideProps-return",
772
+ score,
773
+ confidence: scoreToConfidence(score),
774
+ targetCode: trimmed,
775
+ context: getContextLines(content, i + 1),
776
+ reasons: buildReasons(signals)
777
+ });
778
+ }
779
+ if (/export\s+(async\s+)?function\s+getStaticProps/.test(trimmed)) {
780
+ const signals = {
781
+ hasCmsFetch: hasCmsFetchSignal(content),
782
+ isServerSide: true,
783
+ returnsJson: true,
784
+ isCentralLayer: false,
785
+ isTest: isTestFile(relPath)
786
+ };
787
+ const score = computeScore(signals);
788
+ candidates.push({
789
+ filePath: relPath,
790
+ line: i + 1,
791
+ type: "getStaticProps-return",
792
+ score,
793
+ confidence: scoreToConfidence(score),
794
+ targetCode: trimmed,
795
+ context: getContextLines(content, i + 1),
796
+ reasons: buildReasons(signals)
797
+ });
798
+ }
799
+ }
800
+ return candidates;
801
+ }
802
+ };
803
+ var remixStrategy = {
804
+ globs: ["app/routes/**/*.tsx", "app/routes/**/*.ts", "app/routes/**/*.jsx", "app/routes/**/*.js"],
805
+ analyze(filePath, content, cms, root) {
806
+ const relPath = relative2(root, filePath);
807
+ const candidates = [];
808
+ const lines = content.split("\n");
809
+ for (let i = 0; i < lines.length; i++) {
810
+ const trimmed = lines[i].trim();
811
+ if (/export\s+(async\s+)?function\s+loader/.test(trimmed) || /export\s+const\s+loader/.test(trimmed)) {
812
+ const signals = {
813
+ hasCmsFetch: hasCmsFetchSignal(content),
814
+ isServerSide: true,
815
+ returnsJson: true,
816
+ isCentralLayer: false,
817
+ isTest: isTestFile(relPath)
818
+ };
819
+ const score = computeScore(signals);
820
+ candidates.push({
821
+ filePath: relPath,
822
+ line: i + 1,
823
+ type: "loader-return",
824
+ score,
825
+ confidence: scoreToConfidence(score),
826
+ targetCode: trimmed,
827
+ context: getContextLines(content, i + 1, 5),
828
+ reasons: [...buildReasons(signals), "Remix loader function"]
829
+ });
830
+ }
831
+ }
832
+ candidates.push(
833
+ ...findReturnStatements(content, filePath, cms, root, {
834
+ isServerSide: true,
835
+ isCentralLayer: false
836
+ })
837
+ );
838
+ return candidates;
839
+ }
840
+ };
841
+ var astroStrategy = {
842
+ globs: ["src/pages/**/*.astro", "src/pages/api/**/*.ts", "src/pages/api/**/*.js", "src/lib/**/*.ts"],
843
+ analyze(filePath, content, cms, root) {
844
+ const isApi = /\/api\//.test(filePath);
845
+ const isLib = /\blib\//.test(filePath);
846
+ if (filePath.endsWith(".astro")) {
847
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
848
+ if (frontmatterMatch) {
849
+ const frontmatter = frontmatterMatch[1];
850
+ if (hasCmsFetchSignal(frontmatter)) {
851
+ const assignmentPattern = /(?:const|let)\s+(\w+)\s*=\s*await\s+/g;
852
+ let m;
853
+ const candidates = [];
854
+ while ((m = assignmentPattern.exec(frontmatter)) !== null) {
855
+ const linesBefore = frontmatter.substring(0, m.index).split("\n").length;
856
+ const signals = {
857
+ hasCmsFetch: true,
858
+ isServerSide: true,
859
+ returnsJson: true,
860
+ isCentralLayer: false,
861
+ isTest: false
862
+ };
863
+ const score = computeScore(signals);
864
+ const matchedLine = frontmatter.split("\n")[linesBefore]?.trim() ?? m[0].trim();
865
+ candidates.push({
866
+ filePath: relative2(root, filePath),
867
+ line: linesBefore + 1,
868
+ type: "frontmatter-assignment",
869
+ score,
870
+ confidence: scoreToConfidence(score),
871
+ targetCode: matchedLine,
872
+ context: getContextLines(frontmatter, linesBefore + 1),
873
+ reasons: [...buildReasons(signals), "Astro frontmatter data fetch"]
874
+ });
875
+ }
876
+ return candidates;
877
+ }
878
+ }
879
+ return [];
880
+ }
881
+ return findReturnStatements(content, filePath, cms, root, {
882
+ isServerSide: isApi,
883
+ isCentralLayer: isLib
884
+ });
885
+ }
886
+ };
887
+ var sveltekitStrategy = {
888
+ globs: [
889
+ "src/routes/**/+page.server.ts",
890
+ "src/routes/**/+page.server.js",
891
+ "src/routes/**/+server.ts",
892
+ "src/routes/**/+server.js",
893
+ "src/routes/**/+layout.server.ts",
894
+ "src/routes/**/+layout.server.js",
895
+ "src/lib/**/*.ts",
896
+ "src/lib/**/*.js"
897
+ ],
898
+ analyze(filePath, content, cms, root) {
899
+ const relPath = relative2(root, filePath);
900
+ const isServer = /\+(?:page|layout)\.server\.[jt]s$/.test(filePath) || /\+server\.[jt]s$/.test(filePath);
901
+ const isLib = /src\/lib\//.test(filePath);
902
+ const candidates = [];
903
+ const lines = content.split("\n");
904
+ for (let i = 0; i < lines.length; i++) {
905
+ const trimmed = lines[i].trim();
906
+ if (/export\s+(async\s+)?function\s+load/.test(trimmed) || /export\s+const\s+load/.test(trimmed)) {
907
+ const signals = {
908
+ hasCmsFetch: hasCmsFetchSignal(content),
909
+ isServerSide: isServer,
910
+ returnsJson: true,
911
+ isCentralLayer: isLib,
912
+ isTest: isTestFile(relPath)
913
+ };
914
+ const score = computeScore(signals);
915
+ candidates.push({
916
+ filePath: relPath,
917
+ line: i + 1,
918
+ type: "load-return",
919
+ score,
920
+ confidence: scoreToConfidence(score),
921
+ targetCode: trimmed,
922
+ context: getContextLines(content, i + 1, 5),
923
+ reasons: [...buildReasons(signals), "SvelteKit load function"]
924
+ });
925
+ }
926
+ }
927
+ candidates.push(
928
+ ...findReturnStatements(content, filePath, cms, root, {
929
+ isServerSide: isServer,
930
+ isCentralLayer: isLib
931
+ })
932
+ );
933
+ return candidates;
934
+ }
935
+ };
936
+ var nuxt2Strategy = {
937
+ globs: [
938
+ "store/**/*.js",
939
+ "store/**/*.ts",
940
+ "pages/**/*.vue",
941
+ "mixins/**/*.js",
942
+ "mixins/**/*.ts",
943
+ "services/**/*.js",
944
+ "services/**/*.ts",
945
+ "api/**/*.js",
946
+ "api/**/*.ts",
947
+ "plugins/**/*.js",
948
+ "plugins/**/*.ts"
949
+ ],
950
+ analyze(filePath, content, cms, root) {
951
+ const relPath = relative2(root, filePath);
952
+ const isStore = /store\//.test(filePath);
953
+ const isService = /services\//.test(filePath);
954
+ const isApi = /\bapi\//.test(filePath);
955
+ const isPage = /pages\//.test(filePath);
956
+ const candidates = [];
957
+ const hasCms = hasCmsFetchSignal(content);
958
+ const isTest = isTestFile(relPath);
959
+ const lines = content.split("\n");
960
+ if (isPage && filePath.endsWith(".vue")) {
961
+ for (let i = 0; i < lines.length; i++) {
962
+ const trimmed = lines[i].trim();
963
+ if (/async\s+asyncData\s*\(/.test(trimmed) || /asyncData\s*\(\s*\{/.test(trimmed)) {
964
+ const signals = {
965
+ hasCmsFetch: hasCms || /\$prismic|\$contentful|\$sanity/.test(content),
966
+ isServerSide: true,
967
+ returnsJson: true,
968
+ isCentralLayer: false,
969
+ isTest
970
+ };
971
+ const score = computeScore(signals);
972
+ if (score >= 50) {
973
+ candidates.push({
974
+ filePath: relPath,
975
+ line: i + 1,
976
+ type: "asyncData-return",
977
+ score,
978
+ confidence: scoreToConfidence(score),
979
+ targetCode: trimmed,
980
+ context: getContextLines(content, i + 1, 5),
981
+ reasons: [...buildReasons(signals), "Nuxt 2 asyncData hook"]
982
+ });
983
+ }
984
+ }
985
+ }
986
+ }
987
+ if (isStore) {
988
+ const cmsCallPatterns = [
989
+ /this\.\$prismic/,
990
+ /this\.\$contentful/,
991
+ /this\.\$sanity/,
992
+ /client\.get/,
993
+ /client\.getSingle/,
994
+ /client\.getByUID/,
995
+ /client\.getAllByType/,
996
+ /client\.fetch/
997
+ ];
998
+ for (let i = 0; i < lines.length; i++) {
999
+ const trimmed = lines[i].trim();
1000
+ if (!/^async\s+\w+\s*\(/.test(trimmed)) continue;
1001
+ let bodyHasCmsCall = false;
1002
+ for (let j = i + 1; j < Math.min(lines.length, i + 50); j++) {
1003
+ const bodyLine = lines[j];
1004
+ if (/^\s*(async\s+)?\w+\s*\(/.test(bodyLine) && j > i + 1 && !bodyLine.trim().startsWith("//")) {
1005
+ if (/^\s{0,4}(async\s+)?\w+\s*\(\s*\{/.test(bodyLine)) break;
1006
+ }
1007
+ if (cmsCallPatterns.some((p) => p.test(bodyLine))) {
1008
+ bodyHasCmsCall = true;
1009
+ break;
1010
+ }
1011
+ }
1012
+ if (!bodyHasCmsCall) continue;
1013
+ const signals = {
1014
+ hasCmsFetch: true,
1015
+ isServerSide: true,
1016
+ returnsJson: true,
1017
+ isCentralLayer: true,
1018
+ isTest
1019
+ };
1020
+ const score = computeScore(signals);
1021
+ candidates.push({
1022
+ filePath: relPath,
1023
+ line: i + 1,
1024
+ type: "vuex-action-return",
1025
+ score,
1026
+ confidence: scoreToConfidence(score),
1027
+ targetCode: trimmed,
1028
+ context: getContextLines(content, i + 1, 8),
1029
+ reasons: [...buildReasons(signals), "Vuex store action with CMS fetch"]
1030
+ });
1031
+ }
1032
+ }
1033
+ if ((isService || isApi) && hasCms) {
1034
+ candidates.push(
1035
+ ...findReturnStatements(content, filePath, cms, root, {
1036
+ isServerSide: isApi,
1037
+ isCentralLayer: isService
1038
+ })
1039
+ );
1040
+ }
1041
+ return candidates;
1042
+ }
1043
+ };
1044
+ var expressLikeStrategy = {
1045
+ globs: [
1046
+ "routes/**/*.ts",
1047
+ "routes/**/*.js",
1048
+ "src/routes/**/*.ts",
1049
+ "src/routes/**/*.js",
1050
+ "src/**/*.ts",
1051
+ "src/**/*.js",
1052
+ "api/**/*.ts",
1053
+ "api/**/*.js"
1054
+ ],
1055
+ analyze(filePath, content, cms, root) {
1056
+ const isRoute = /\broutes?\//.test(filePath) || /\bapi\//.test(filePath);
1057
+ return findReturnStatements(content, filePath, cms, root, {
1058
+ isServerSide: isRoute || /res\.json|res\.send|c\.json/.test(content),
1059
+ isCentralLayer: /\b(services?|helpers?|utils?|lib)\b/.test(filePath)
1060
+ });
1061
+ }
1062
+ };
1063
+ var STRATEGIES = {
1064
+ nuxt: nuxtStrategy,
1065
+ nuxt2: nuxt2Strategy,
1066
+ next: nextStrategy,
1067
+ remix: remixStrategy,
1068
+ astro: astroStrategy,
1069
+ sveltekit: sveltekitStrategy,
1070
+ express: expressLikeStrategy,
1071
+ hono: expressLikeStrategy,
1072
+ fastify: expressLikeStrategy
1073
+ };
1074
+ function findInjectionPoints(root, frameworkName, cms) {
1075
+ const strategy = STRATEGIES[frameworkName] ?? expressLikeStrategy;
1076
+ const files = fg2.sync(strategy.globs, {
1077
+ cwd: root,
1078
+ ignore: IGNORE_DIRS2.map((d) => `${d}/**`),
1079
+ absolute: true,
1080
+ onlyFiles: true,
1081
+ deep: 8
1082
+ });
1083
+ const allCandidates = [];
1084
+ for (const file of files) {
1085
+ let content;
1086
+ try {
1087
+ content = readFileSync3(file, "utf-8");
1088
+ } catch {
1089
+ continue;
1090
+ }
1091
+ if (content.includes("@synchronized-studio/response-transformer") || content.includes("transformCmsAssetUrls") || content.includes("transformPrismicAssetUrls") || content.includes("transformContentfulAssetUrls") || content.includes("transformSanityAssetUrls") || content.includes("transformShopifyAssetUrls") || content.includes("transformCloudinaryAssetUrls") || content.includes("transformImgixAssetUrls") || content.includes("transformGenericAssetUrls")) {
1092
+ continue;
1093
+ }
1094
+ const candidates = strategy.analyze(file, content, cms, root);
1095
+ allCandidates.push(...candidates);
1096
+ }
1097
+ const seen = /* @__PURE__ */ new Set();
1098
+ return allCandidates.sort((a, b) => b.score - a.score).filter((c) => {
1099
+ const key = `${c.filePath}:${c.line}`;
1100
+ if (seen.has(key)) return false;
1101
+ seen.add(key);
1102
+ return true;
1103
+ });
1104
+ }
1105
+
1106
+ // src/scanner/index.ts
1107
+ function scan(projectRoot) {
1108
+ const root = resolve(projectRoot ?? process.cwd());
1109
+ const framework = detectFramework(root);
1110
+ const cms = detectCms(root);
1111
+ const packageManager = detectPackageManager(root);
1112
+ const injectionPoints = findInjectionPoints(root, framework.name, cms);
1113
+ return {
1114
+ framework,
1115
+ cms,
1116
+ injectionPoints,
1117
+ packageManager,
1118
+ projectRoot: root
1119
+ };
1120
+ }
1121
+
1122
+ // src/planner/index.ts
1123
+ import { existsSync as existsSync4 } from "fs";
1124
+ import { join as join4 } from "path";
1125
+
1126
+ // src/planner/templates.ts
1127
+ var PKG = "@synchronized-studio/response-transformer";
1128
+ var CMS_FUNCTION_MAP = {
1129
+ prismic: "transformPrismicAssetUrls",
1130
+ contentful: "transformContentfulAssetUrls",
1131
+ sanity: "transformSanityAssetUrls",
1132
+ shopify: "transformShopifyAssetUrls",
1133
+ cloudinary: "transformCloudinaryAssetUrls",
1134
+ imgix: "transformImgixAssetUrls",
1135
+ generic: "transformGenericAssetUrls"
1136
+ };
1137
+ var UNIFIED_FUNCTION = "transformCmsAssetUrls";
1138
+ function getTransformFunctionName(cms) {
1139
+ return CMS_FUNCTION_MAP[cms] ?? UNIFIED_FUNCTION;
1140
+ }
1141
+ function getImportStatement(cms) {
1142
+ const fn = getTransformFunctionName(cms);
1143
+ return `import { ${fn} } from '${PKG}'`;
1144
+ }
1145
+ var CMS_REQUIRED_PARAMS = {
1146
+ prismic: [{ key: "repository", placeholder: "YOUR-REPO" }],
1147
+ contentful: [{ key: "spaceId", placeholder: "YOUR-SPACE-ID" }],
1148
+ sanity: [{ key: "projectId", placeholder: "YOUR-PROJECT-ID" }],
1149
+ shopify: [{ key: "storeDomain", placeholder: "your-store.myshopify.com" }],
1150
+ cloudinary: [{ key: "cloudName", placeholder: "YOUR-CLOUD-NAME" }],
1151
+ imgix: [{ key: "imgixDomain", placeholder: "your-source.imgix.net" }],
1152
+ generic: [{ key: "originUrl", placeholder: "https://your-origin.com" }]
1153
+ };
1154
+ function buildCmsOptions(cms, params) {
1155
+ const entries = [];
1156
+ const required = CMS_REQUIRED_PARAMS[cms] ?? [];
1157
+ for (const { key, placeholder } of required) {
1158
+ const value = params[key];
1159
+ if (value) {
1160
+ entries.push(`${key}: '${value}'`);
1161
+ } else {
1162
+ entries.push(`${key}: '${placeholder}'`);
1163
+ }
1164
+ }
1165
+ if (cms === "sanity" && params.dataset && params.dataset !== "production") {
1166
+ entries.push(`dataset: '${params.dataset}'`);
1167
+ }
1168
+ if (entries.length === 0) return "{}";
1169
+ if (entries.length === 1) {
1170
+ return `{ ${entries[0]} }`;
1171
+ }
1172
+ return `{
1173
+ ${entries.join(",\n ")},
1174
+ }`;
1175
+ }
1176
+ function extractReturnVarName(code) {
1177
+ const m = code.match(/^return\s+(?:await\s+)?(\w+)/);
1178
+ if (m?.[1] && m[1] !== "await" && m[1] !== "new") return m[1];
1179
+ return "data";
1180
+ }
1181
+ function extractResJsonVarName(code) {
1182
+ const m = code.match(/res\.json\(\s*(\w+)/);
1183
+ return m?.[1] ?? "data";
1184
+ }
1185
+ var returnWrap = {
1186
+ transform(originalCode, cms, params) {
1187
+ const varName = extractReturnVarName(originalCode);
1188
+ const fn = getTransformFunctionName(cms);
1189
+ const opts = buildCmsOptions(cms, params);
1190
+ return `return ${fn}(${varName}, ${opts})`;
1191
+ },
1192
+ description(cms) {
1193
+ return `Wrap return value with ${getTransformFunctionName(cms)}()`;
1194
+ }
1195
+ };
1196
+ var resJsonWrap = {
1197
+ transform(originalCode, cms, params) {
1198
+ const varName = extractResJsonVarName(originalCode);
1199
+ const fn = getTransformFunctionName(cms);
1200
+ const opts = buildCmsOptions(cms, params);
1201
+ return originalCode.replace(
1202
+ /res\.json\(\s*(\w+)\s*\)/,
1203
+ `res.json(${fn}(${varName}, ${opts}))`
1204
+ );
1205
+ },
1206
+ description(cms) {
1207
+ return `Wrap res.json() argument with ${getTransformFunctionName(cms)}()`;
1208
+ }
1209
+ };
1210
+ var useFetchTransformWrap = {
1211
+ transform(originalCode, cms, params) {
1212
+ const fn = getTransformFunctionName(cms);
1213
+ const opts = buildCmsOptions(cms, params);
1214
+ if (originalCode.includes(")")) {
1215
+ const insertPos = originalCode.lastIndexOf(")");
1216
+ const beforeClose = originalCode.substring(0, insertPos).trimEnd();
1217
+ if (beforeClose.endsWith(",") || beforeClose.endsWith("(")) {
1218
+ return `${beforeClose} { transform: (raw) => ${fn}(raw, ${opts}) })`;
1219
+ }
1220
+ return `${beforeClose}, { transform: (raw) => ${fn}(raw, ${opts}) })`;
1221
+ }
1222
+ return originalCode;
1223
+ },
1224
+ description(cms) {
1225
+ return `Add transform option to useFetch() with ${getTransformFunctionName(cms)}()`;
1226
+ }
1227
+ };
1228
+ var useAsyncDataTransformWrap = {
1229
+ transform(originalCode, cms, params) {
1230
+ const fn = getTransformFunctionName(cms);
1231
+ const opts = buildCmsOptions(cms, params);
1232
+ if (originalCode.includes(")")) {
1233
+ const insertPos = originalCode.lastIndexOf(")");
1234
+ const beforeClose = originalCode.substring(0, insertPos).trimEnd();
1235
+ if (beforeClose.endsWith(",") || beforeClose.endsWith("(")) {
1236
+ return `${beforeClose} { transform: (raw) => ${fn}(raw, ${opts}) })`;
1237
+ }
1238
+ return `${beforeClose}, { transform: (raw) => ${fn}(raw, ${opts}) })`;
1239
+ }
1240
+ return originalCode;
1241
+ },
1242
+ description(cms) {
1243
+ return `Add transform option to useAsyncData() with ${getTransformFunctionName(cms)}()`;
1244
+ }
1245
+ };
1246
+ var loaderReturnWrap = {
1247
+ transform(originalCode, cms, params) {
1248
+ return returnWrap.transform(originalCode, cms, params);
1249
+ },
1250
+ description(cms) {
1251
+ return `Wrap Remix loader return with ${getTransformFunctionName(cms)}()`;
1252
+ }
1253
+ };
1254
+ var getServerSidePropsWrap = {
1255
+ transform(originalCode, cms, params) {
1256
+ return returnWrap.transform(originalCode, cms, params);
1257
+ },
1258
+ description(cms) {
1259
+ return `Wrap getServerSideProps return with ${getTransformFunctionName(cms)}()`;
1260
+ }
1261
+ };
1262
+ var getStaticPropsWrap = {
1263
+ transform(originalCode, cms, params) {
1264
+ return returnWrap.transform(originalCode, cms, params);
1265
+ },
1266
+ description(cms) {
1267
+ return `Wrap getStaticProps return with ${getTransformFunctionName(cms)}()`;
1268
+ }
1269
+ };
1270
+ var loadReturnWrap = {
1271
+ transform(originalCode, cms, params) {
1272
+ return returnWrap.transform(originalCode, cms, params);
1273
+ },
1274
+ description(cms) {
1275
+ return `Wrap SvelteKit load() return with ${getTransformFunctionName(cms)}()`;
1276
+ }
1277
+ };
1278
+ var frontmatterAssignmentWrap = {
1279
+ transform(originalCode, cms, params) {
1280
+ const fn = getTransformFunctionName(cms);
1281
+ const opts = buildCmsOptions(cms, params);
1282
+ const m = originalCode.match(/((?:const|let)\s+\w+\s*=\s*)(await\s+.+)/);
1283
+ if (m) {
1284
+ return `${m[1]}${fn}(${m[2]}, ${opts})`;
1285
+ }
1286
+ return originalCode;
1287
+ },
1288
+ description(cms) {
1289
+ return `Wrap Astro frontmatter assignment with ${getTransformFunctionName(cms)}()`;
1290
+ }
1291
+ };
1292
+ var asyncDataReturnWrap = {
1293
+ transform(originalCode, cms, params) {
1294
+ return returnWrap.transform(originalCode, cms, params);
1295
+ },
1296
+ description(cms) {
1297
+ return `Wrap Nuxt 2 asyncData return with ${getTransformFunctionName(cms)}()`;
1298
+ }
1299
+ };
1300
+ var vuexActionReturnWrap = {
1301
+ transform(originalCode, cms, params) {
1302
+ return returnWrap.transform(originalCode, cms, params);
1303
+ },
1304
+ description(cms) {
1305
+ return `Wrap Vuex action return with ${getTransformFunctionName(cms)}()`;
1306
+ }
1307
+ };
1308
+ var WRAP_TEMPLATES = {
1309
+ "return": returnWrap,
1310
+ "res.json": resJsonWrap,
1311
+ "useFetch-transform": useFetchTransformWrap,
1312
+ "useAsyncData-transform": useAsyncDataTransformWrap,
1313
+ "loader-return": loaderReturnWrap,
1314
+ "getServerSideProps-return": getServerSidePropsWrap,
1315
+ "getStaticProps-return": getStaticPropsWrap,
1316
+ "load-return": loadReturnWrap,
1317
+ "frontmatter-assignment": frontmatterAssignmentWrap,
1318
+ "asyncData-return": asyncDataReturnWrap,
1319
+ "vuex-action-return": vuexActionReturnWrap,
1320
+ "assignment": {
1321
+ transform(originalCode, cms, params) {
1322
+ const fn = getTransformFunctionName(cms);
1323
+ const opts = buildCmsOptions(cms, params);
1324
+ const m = originalCode.match(/((?:const|let|var)\s+\w+\s*=\s*)(.+)/);
1325
+ if (m) return `${m[1]}${fn}(${m[2]}, ${opts})`;
1326
+ return originalCode;
1327
+ },
1328
+ description(cms) {
1329
+ return `Wrap assignment with ${getTransformFunctionName(cms)}()`;
1330
+ }
1331
+ }
1332
+ };
1333
+
1334
+ // src/planner/index.ts
1335
+ function resolveInstallCommand(scan2) {
1336
+ const pkg = "@synchronized-studio/response-transformer";
1337
+ switch (scan2.packageManager) {
1338
+ case "pnpm":
1339
+ return `pnpm add ${pkg}`;
1340
+ case "yarn":
1341
+ return `yarn add ${pkg}`;
1342
+ case "bun":
1343
+ return `bun add ${pkg}`;
1344
+ default:
1345
+ return `npm install ${pkg}`;
1346
+ }
1347
+ }
1348
+ function resolveEnvFiles(root) {
1349
+ const candidates = [".env", ".env.local", ".env.example", ".env.development"];
1350
+ return candidates.filter((f) => existsSync4(join4(root, f)));
1351
+ }
1352
+ function createPlan(scan2) {
1353
+ const patches = [];
1354
+ for (const candidate of scan2.injectionPoints) {
1355
+ const template = WRAP_TEMPLATES[candidate.type];
1356
+ if (!template) continue;
1357
+ const importStatement = getImportStatement(scan2.cms.type);
1358
+ const originalCode = candidate.targetCode;
1359
+ if (!originalCode) continue;
1360
+ const transformedCode = template.transform(
1361
+ originalCode,
1362
+ scan2.cms.type,
1363
+ scan2.cms.params
1364
+ );
1365
+ if (transformedCode === originalCode) continue;
1366
+ patches.push({
1367
+ filePath: candidate.filePath,
1368
+ description: template.description(scan2.cms.type),
1369
+ importToAdd: importStatement,
1370
+ wrapTarget: {
1371
+ line: candidate.line,
1372
+ originalCode,
1373
+ transformedCode
1374
+ },
1375
+ confidence: candidate.confidence,
1376
+ reasons: candidate.reasons
1377
+ });
1378
+ }
1379
+ const envFiles = resolveEnvFiles(scan2.projectRoot);
1380
+ return {
1381
+ schemaVersion: "1.0",
1382
+ scan: scan2,
1383
+ install: {
1384
+ package: "@synchronized-studio/response-transformer",
1385
+ command: resolveInstallCommand(scan2)
1386
+ },
1387
+ env: {
1388
+ key: "CMS_ASSETS_URL",
1389
+ placeholder: "https://YOUR-SLUG.cmsassets.com",
1390
+ files: envFiles
1391
+ },
1392
+ patches,
1393
+ policies: {
1394
+ maxFilesAutoApply: 5,
1395
+ allowLlmFallback: false,
1396
+ llmFallbackForAll: false,
1397
+ llmOnly: false,
1398
+ verifyProfile: "quick"
1399
+ }
1400
+ };
1401
+ }
1402
+
1403
+ // src/types.ts
1404
+ import { z } from "zod";
1405
+ var FrameworkName = z.enum([
1406
+ "nuxt",
1407
+ "nuxt2",
1408
+ "next",
1409
+ "remix",
1410
+ "astro",
1411
+ "sveltekit",
1412
+ "express",
1413
+ "hono",
1414
+ "fastify",
1415
+ "unknown"
1416
+ ]);
1417
+ var CmsType = z.enum([
1418
+ "prismic",
1419
+ "contentful",
1420
+ "sanity",
1421
+ "shopify",
1422
+ "cloudinary",
1423
+ "imgix",
1424
+ "generic",
1425
+ "unknown"
1426
+ ]);
1427
+ var PackageManager = z.enum(["npm", "yarn", "pnpm", "bun"]);
1428
+ var Confidence = z.enum(["high", "medium", "low"]);
1429
+ var InjectionType = z.enum([
1430
+ "return",
1431
+ "res.json",
1432
+ "assignment",
1433
+ "useFetch-transform",
1434
+ "useAsyncData-transform",
1435
+ "loader-return",
1436
+ "getServerSideProps-return",
1437
+ "getStaticProps-return",
1438
+ "load-return",
1439
+ "frontmatter-assignment",
1440
+ "asyncData-return",
1441
+ "vuex-action-return"
1442
+ ]);
1443
+ var VerifyProfile = z.enum(["quick", "full"]);
1444
+ var FrameworkInfo = z.object({
1445
+ name: FrameworkName,
1446
+ version: z.string(),
1447
+ configFile: z.string().nullable()
1448
+ });
1449
+ var CmsInfo = z.object({
1450
+ type: CmsType,
1451
+ params: z.record(z.string()),
1452
+ detectedFrom: z.array(z.string())
1453
+ });
1454
+ var InjectionCandidate = z.object({
1455
+ filePath: z.string(),
1456
+ line: z.number(),
1457
+ type: InjectionType,
1458
+ score: z.number().min(0).max(100),
1459
+ confidence: Confidence,
1460
+ targetCode: z.string(),
1461
+ context: z.string(),
1462
+ reasons: z.array(z.string())
1463
+ });
1464
+ var ScanResult = z.object({
1465
+ framework: FrameworkInfo,
1466
+ cms: CmsInfo,
1467
+ injectionPoints: z.array(InjectionCandidate),
1468
+ packageManager: PackageManager,
1469
+ projectRoot: z.string()
1470
+ });
1471
+ var FilePatch = z.object({
1472
+ filePath: z.string(),
1473
+ description: z.string(),
1474
+ importToAdd: z.string(),
1475
+ wrapTarget: z.object({
1476
+ line: z.number(),
1477
+ originalCode: z.string(),
1478
+ transformedCode: z.string()
1479
+ }),
1480
+ confidence: Confidence,
1481
+ reasons: z.array(z.string())
1482
+ });
1483
+ var PatchPlan = z.object({
1484
+ schemaVersion: z.literal("1.0"),
1485
+ scan: ScanResult,
1486
+ install: z.object({
1487
+ package: z.literal("@synchronized-studio/response-transformer"),
1488
+ command: z.string()
1489
+ }),
1490
+ env: z.object({
1491
+ key: z.literal("CMS_ASSETS_URL"),
1492
+ placeholder: z.string(),
1493
+ files: z.array(z.string())
1494
+ }),
1495
+ patches: z.array(FilePatch),
1496
+ policies: z.object({
1497
+ maxFilesAutoApply: z.number().default(5),
1498
+ allowLlmFallback: z.boolean().default(false),
1499
+ /** When true, try LLM for any failed AST patch (not just low confidence). Useful for testing. */
1500
+ llmFallbackForAll: z.boolean().default(false),
1501
+ /** When true, skip AST entirely and use LLM for all patches (testing only). */
1502
+ llmOnly: z.boolean().default(false),
1503
+ verifyProfile: VerifyProfile.default("quick")
1504
+ })
1505
+ });
1506
+ var PatchedFileReport = z.object({
1507
+ filePath: z.string(),
1508
+ applied: z.boolean(),
1509
+ method: z.enum(["ast", "llm", "skipped"]),
1510
+ reason: z.string().optional(),
1511
+ durationMs: z.number()
1512
+ });
1513
+ var ApplyReport = z.object({
1514
+ schemaVersion: z.literal("1.0"),
1515
+ timestamp: z.string(),
1516
+ projectRoot: z.string(),
1517
+ gitBranch: z.string().nullable(),
1518
+ gitCommit: z.string().nullable(),
1519
+ installed: z.boolean(),
1520
+ envUpdated: z.boolean(),
1521
+ files: z.array(PatchedFileReport),
1522
+ totalDurationMs: z.number()
1523
+ });
1524
+ var VerifyReport = z.object({
1525
+ schemaVersion: z.literal("1.0"),
1526
+ profile: VerifyProfile,
1527
+ lintPassed: z.boolean().nullable(),
1528
+ buildPassed: z.boolean().nullable(),
1529
+ testsPassed: z.boolean().nullable(),
1530
+ patchedFiles: z.array(z.string()),
1531
+ warnings: z.array(z.string()),
1532
+ durationMs: z.number()
1533
+ });
1534
+
1535
+ // src/reporting/index.ts
1536
+ import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync5, mkdirSync } from "fs";
1537
+ import { join as join5 } from "path";
1538
+ import consola from "consola";
1539
+ function createReport(parts) {
1540
+ return {
1541
+ version: "1.0",
1542
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1543
+ ...parts
1544
+ };
1545
+ }
1546
+ function saveReport(root, report) {
1547
+ const dir = join5(root, ".cmsassets-agent");
1548
+ if (!existsSync5(dir)) {
1549
+ mkdirSync(dir, { recursive: true });
1550
+ }
1551
+ const filename = `report-${Date.now()}.json`;
1552
+ const filePath = join5(dir, filename);
1553
+ writeFileSync(filePath, JSON.stringify(report, null, 2), "utf-8");
1554
+ consola.info(`Report saved to ${filePath}`);
1555
+ return filePath;
1556
+ }
1557
+ function savePlanFile(root, plan) {
1558
+ const filePath = join5(root, "cmsassets-agent.plan.json");
1559
+ writeFileSync(filePath, JSON.stringify(plan, null, 2), "utf-8");
1560
+ consola.info(`Plan saved to ${filePath}`);
1561
+ return filePath;
1562
+ }
1563
+ function loadPlanFile(filePath) {
1564
+ try {
1565
+ const raw = JSON.parse(readFileSync4(filePath, "utf-8"));
1566
+ const result = PatchPlan.safeParse(raw);
1567
+ if (result.success) return result.data;
1568
+ consola.error("Plan file validation failed:", result.error.issues);
1569
+ return null;
1570
+ } catch (err) {
1571
+ const msg = err instanceof Error ? err.message : String(err);
1572
+ consola.error(`Failed to load plan file: ${msg}`);
1573
+ return null;
1574
+ }
1575
+ }
1576
+
1577
+ // src/patcher/index.ts
1578
+ import { appendFileSync, existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
1579
+ import { join as join9 } from "path";
1580
+ import consola2 from "consola";
1581
+
1582
+ // src/patcher/astPatcher.ts
1583
+ import { Project, SyntaxKind } from "ts-morph";
1584
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1585
+ import { join as join6, extname } from "path";
1586
+ function applyAstPatch(root, patch, dryRun) {
1587
+ const absPath = join6(root, patch.filePath);
1588
+ const ext = extname(patch.filePath);
1589
+ if ([".vue", ".svelte", ".astro"].includes(ext)) {
1590
+ return applyStringPatch(root, patch, dryRun);
1591
+ }
1592
+ try {
1593
+ const project = new Project({ useInMemoryFileSystem: false });
1594
+ const sourceFile = project.addSourceFileAtPath(absPath);
1595
+ const originalText = sourceFile.getFullText();
1596
+ const existingImports = sourceFile.getImportDeclarations();
1597
+ const hasImport = existingImports.some(
1598
+ (i) => i.getModuleSpecifierValue() === "@synchronized-studio/response-transformer"
1599
+ );
1600
+ if (!hasImport && patch.importToAdd) {
1601
+ const importMatch = patch.importToAdd.match(
1602
+ /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/
1603
+ );
1604
+ if (importMatch) {
1605
+ const namedImports = importMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
1606
+ sourceFile.addImportDeclaration({
1607
+ moduleSpecifier: importMatch[2],
1608
+ namedImports
1609
+ });
1610
+ }
1611
+ }
1612
+ const targetLine = patch.wrapTarget.line;
1613
+ const original = patch.wrapTarget.originalCode.trim();
1614
+ const transformed = patch.wrapTarget.transformedCode.trim();
1615
+ if (original && transformed && original !== transformed) {
1616
+ let patched = false;
1617
+ const returnStatements = sourceFile.getDescendantsOfKind(
1618
+ SyntaxKind.ReturnStatement
1619
+ );
1620
+ for (const ret of returnStatements) {
1621
+ const retLine = ret.getStartLineNumber();
1622
+ if (retLine !== targetLine) continue;
1623
+ const retText = ret.getText().trim();
1624
+ if (retText.includes(original) || original.startsWith("return")) {
1625
+ ret.replaceWithText(transformed);
1626
+ patched = true;
1627
+ break;
1628
+ }
1629
+ }
1630
+ if (!patched) {
1631
+ const expressions = sourceFile.getDescendantsOfKind(
1632
+ SyntaxKind.ExpressionStatement
1633
+ );
1634
+ for (const expr of expressions) {
1635
+ const exprLine = expr.getStartLineNumber();
1636
+ if (exprLine !== targetLine) continue;
1637
+ const exprText = expr.getText().trim();
1638
+ if (exprText.includes("res.json") || exprText.includes("c.json")) {
1639
+ expr.replaceWithText(transformed);
1640
+ patched = true;
1641
+ break;
1642
+ }
1643
+ }
1644
+ }
1645
+ if (!patched) {
1646
+ for (const ret of returnStatements) {
1647
+ const retLine = ret.getStartLineNumber();
1648
+ if (Math.abs(retLine - targetLine) > 1) continue;
1649
+ const retText = ret.getText().trim();
1650
+ if (original.startsWith("return") && retText.startsWith("return")) {
1651
+ const expectedVar = original.match(/^return\s+(?:await\s+)?(\w+)/)?.[1];
1652
+ const actualVar = retText.match(/^return\s+(?:await\s+)?(\w+)/)?.[1];
1653
+ if (expectedVar && actualVar && expectedVar !== actualVar) continue;
1654
+ ret.replaceWithText(transformed);
1655
+ patched = true;
1656
+ break;
1657
+ }
1658
+ }
1659
+ }
1660
+ if (!patched) {
1661
+ const currentText = sourceFile.getFullText();
1662
+ const lines = currentText.split("\n");
1663
+ const targetLineContent = lines[targetLine - 1];
1664
+ if (targetLineContent && targetLineContent.trim() === original) {
1665
+ const indent = targetLineContent.match(/^(\s*)/)?.[1] ?? "";
1666
+ const newContent = currentText.replace(
1667
+ targetLineContent,
1668
+ indent + transformed
1669
+ );
1670
+ if (newContent !== currentText) {
1671
+ sourceFile.replaceWithText(newContent);
1672
+ patched = true;
1673
+ }
1674
+ }
1675
+ }
1676
+ if (!patched) {
1677
+ return { applied: false, reason: `Could not locate target code at line ${targetLine}` };
1678
+ }
1679
+ }
1680
+ const newText = sourceFile.getFullText();
1681
+ if (newText === originalText) {
1682
+ return { applied: false, reason: "No changes needed (idempotent)" };
1683
+ }
1684
+ if (!dryRun) {
1685
+ sourceFile.saveSync();
1686
+ }
1687
+ return { applied: true };
1688
+ } catch (err) {
1689
+ const msg = err instanceof Error ? err.message : String(err);
1690
+ return { applied: false, reason: `AST patch failed: ${msg}` };
1691
+ }
1692
+ }
1693
+ function applyStringPatch(root, patch, dryRun) {
1694
+ const absPath = join6(root, patch.filePath);
1695
+ let content;
1696
+ try {
1697
+ content = readFileSync5(absPath, "utf-8");
1698
+ } catch (err) {
1699
+ const msg = err instanceof Error ? err.message : String(err);
1700
+ return { applied: false, reason: `Cannot read file: ${msg}` };
1701
+ }
1702
+ const original = content;
1703
+ const { originalCode, transformedCode } = patch.wrapTarget;
1704
+ if (patch.importToAdd && !content.includes("@synchronized-studio/response-transformer")) {
1705
+ const scriptMatch = content.match(
1706
+ /(<script[^>]*>)\s*\n/
1707
+ );
1708
+ if (scriptMatch) {
1709
+ const insertAfter = scriptMatch[0];
1710
+ content = content.replace(
1711
+ insertAfter,
1712
+ `${insertAfter}${patch.importToAdd}
1713
+ `
1714
+ );
1715
+ } else if (patch.filePath.endsWith(".astro")) {
1716
+ const fmMatch = content.match(/^(---\n)/);
1717
+ if (fmMatch) {
1718
+ content = content.replace(
1719
+ "---\n",
1720
+ `---
1721
+ ${patch.importToAdd}
1722
+ `
1723
+ );
1724
+ }
1725
+ }
1726
+ }
1727
+ if (originalCode && transformedCode && originalCode !== transformedCode) {
1728
+ if (content.includes(originalCode.trim())) {
1729
+ content = content.replace(originalCode.trim(), transformedCode.trim());
1730
+ } else {
1731
+ return {
1732
+ applied: false,
1733
+ reason: "Original code not found in file (string match)"
1734
+ };
1735
+ }
1736
+ }
1737
+ if (content === original) {
1738
+ return { applied: false, reason: "No changes needed (idempotent)" };
1739
+ }
1740
+ if (!dryRun) {
1741
+ writeFileSync2(absPath, content, "utf-8");
1742
+ }
1743
+ return { applied: true };
1744
+ }
1745
+
1746
+ // src/patcher/llmFallback.ts
1747
+ import { readFileSync as readFileSync6 } from "fs";
1748
+ import { join as join7 } from "path";
1749
+
1750
+ // src/patcher/diffValidator.ts
1751
+ var ALLOWED_ADD_PATTERNS = [
1752
+ /import\s+\{[^}]*transform/i,
1753
+ /response-transformer/,
1754
+ /transformCmsAssetUrls/,
1755
+ /transformPrismicAssetUrls/,
1756
+ /transformContentfulAssetUrls/,
1757
+ /transformSanityAssetUrls/,
1758
+ /transformShopifyAssetUrls/,
1759
+ /transformCloudinaryAssetUrls/,
1760
+ /transformImgixAssetUrls/,
1761
+ /transformGenericAssetUrls/
1762
+ ];
1763
+ function validateDiff(diff, allowedFiles) {
1764
+ const errors = [];
1765
+ const lines = diff.split("\n");
1766
+ const fileHeaders = lines.filter((l) => l.startsWith("--- ") || l.startsWith("+++ "));
1767
+ for (const header of fileHeaders) {
1768
+ const filePath = header.replace(/^[+-]{3}\s+[ab]\//, "").trim();
1769
+ if (filePath === "/dev/null") continue;
1770
+ if (!allowedFiles.some((f) => filePath.endsWith(f) || f.endsWith(filePath))) {
1771
+ errors.push(`Diff modifies disallowed file: ${filePath}`);
1772
+ }
1773
+ }
1774
+ const addedLines = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++"));
1775
+ const removedLines = lines.filter((l) => l.startsWith("-") && !l.startsWith("---"));
1776
+ for (const added of addedLines) {
1777
+ const lineContent = added.substring(1).trim();
1778
+ if (!lineContent) continue;
1779
+ const isAllowed = ALLOWED_ADD_PATTERNS.some((p) => p.test(lineContent));
1780
+ const hasMatchingRemoval = removedLines.some((r) => {
1781
+ const removedContent = r.substring(1).trim();
1782
+ return lineContent.includes(removedContent) || removedContent.includes(lineContent.replace(/transform\w+AssetUrls\([^)]+\)/, ""));
1783
+ });
1784
+ if (!isAllowed && !hasMatchingRemoval) {
1785
+ if (/^[{}\[\](),;\s]*$/.test(lineContent)) continue;
1786
+ errors.push(`Suspicious added line: ${lineContent.substring(0, 80)}`);
1787
+ }
1788
+ }
1789
+ if (addedLines.length > 20) {
1790
+ errors.push(`Too many additions (${addedLines.length}), expected <= 20`);
1791
+ }
1792
+ if (removedLines.length > 10) {
1793
+ errors.push(`Too many removals (${removedLines.length}), expected <= 10`);
1794
+ }
1795
+ return { valid: errors.length === 0, errors };
1796
+ }
1797
+
1798
+ // src/patcher/llmFallback.ts
1799
+ function buildPrompt(fileContent, filePath, cms, framework) {
1800
+ const fn = getTransformFunctionName(cms.type);
1801
+ const opts = buildCmsOptions(cms.type, cms.params);
1802
+ return `You are a code integration agent. Given the following file, add the
1803
+ @synchronized-studio/response-transformer to transform CMS asset URLs.
1804
+
1805
+ CMS: ${cms.type}
1806
+ Transform function: ${fn}
1807
+ Options: ${opts}
1808
+ Framework: ${framework}
1809
+
1810
+ File: ${filePath}
1811
+ \`\`\`
1812
+ ${fileContent}
1813
+ \`\`\`
1814
+
1815
+ Return ONLY a unified diff that:
1816
+ 1. Adds the import: import { ${fn} } from '@synchronized-studio/response-transformer'
1817
+ 2. Wraps the CMS data return/assignment with ${fn}(data, ${opts})
1818
+ 3. Does NOT change any other code
1819
+
1820
+ Output ONLY the diff, nothing else.`;
1821
+ }
1822
+ async function applyLlmPatch(root, patch, cms, framework) {
1823
+ const absPath = join7(root, patch.filePath);
1824
+ let fileContent;
1825
+ try {
1826
+ fileContent = readFileSync6(absPath, "utf-8");
1827
+ } catch (err) {
1828
+ const msg = err instanceof Error ? err.message : String(err);
1829
+ return { applied: false, reason: `Cannot read file: ${msg}`, requiresReview: true };
1830
+ }
1831
+ let openai;
1832
+ try {
1833
+ openai = await import("./openai-E6ORPCAV.js");
1834
+ } catch {
1835
+ return {
1836
+ applied: false,
1837
+ reason: "openai package not installed. Install it with: npm install openai",
1838
+ requiresReview: true
1839
+ };
1840
+ }
1841
+ const apiKey = process.env.OPENAI_API_KEY;
1842
+ if (!apiKey) {
1843
+ return {
1844
+ applied: false,
1845
+ reason: "OPENAI_API_KEY environment variable not set",
1846
+ requiresReview: true
1847
+ };
1848
+ }
1849
+ const prompt = buildPrompt(fileContent, patch.filePath, cms, framework);
1850
+ try {
1851
+ const client = new openai.default({ apiKey });
1852
+ const response = await client.chat.completions.create({
1853
+ model: "gpt-4o-mini",
1854
+ messages: [{ role: "user", content: prompt }],
1855
+ temperature: 0,
1856
+ max_tokens: 2e3
1857
+ });
1858
+ const diff = response.choices[0]?.message?.content?.trim() ?? "";
1859
+ if (!diff) {
1860
+ return { applied: false, reason: "LLM returned empty response", requiresReview: true };
1861
+ }
1862
+ const validation = validateDiff(diff, [patch.filePath]);
1863
+ if (!validation.valid) {
1864
+ return {
1865
+ applied: false,
1866
+ reason: `LLM diff failed validation: ${validation.errors.join("; ")}`,
1867
+ diff,
1868
+ requiresReview: true
1869
+ };
1870
+ }
1871
+ return {
1872
+ applied: false,
1873
+ reason: "LLM patch generated, requires manual review before applying",
1874
+ diff,
1875
+ requiresReview: true
1876
+ };
1877
+ } catch (err) {
1878
+ const msg = err instanceof Error ? err.message : String(err);
1879
+ return { applied: false, reason: `LLM API error: ${msg}`, requiresReview: true };
1880
+ }
1881
+ }
1882
+
1883
+ // src/patcher/idempotency.ts
1884
+ import { readFileSync as readFileSync7 } from "fs";
1885
+ import { join as join8 } from "path";
1886
+ var TRANSFORMER_IMPORTS = [
1887
+ "@synchronized-studio/response-transformer",
1888
+ "transformCmsAssetUrls",
1889
+ "transformPrismicAssetUrls",
1890
+ "transformContentfulAssetUrls",
1891
+ "transformSanityAssetUrls",
1892
+ "transformShopifyAssetUrls",
1893
+ "transformCloudinaryAssetUrls",
1894
+ "transformImgixAssetUrls",
1895
+ "transformGenericAssetUrls"
1896
+ ];
1897
+ function isAlreadyTransformed(root, filePath) {
1898
+ const absPath = join8(root, filePath);
1899
+ let content;
1900
+ try {
1901
+ content = readFileSync7(absPath, "utf-8");
1902
+ } catch {
1903
+ return false;
1904
+ }
1905
+ return TRANSFORMER_IMPORTS.some((sig) => content.includes(sig));
1906
+ }
1907
+ function isTestOrFixtureFile(filePath) {
1908
+ return /\.(test|spec)\.[jt]sx?$/.test(filePath) || /__tests__\//.test(filePath) || /(^|\/)tests?\//.test(filePath) || /(^|\/)fixtures?\//.test(filePath) || /(^|\/)mocks?\//.test(filePath) || /\.stories\.[jt]sx?$/.test(filePath);
1909
+ }
1910
+ function isGeneratedFile(filePath) {
1911
+ 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);
1912
+ }
1913
+
1914
+ // src/patcher/index.ts
1915
+ async function applyPlan(plan, opts = {}) {
1916
+ const startTime = Date.now();
1917
+ const root = plan.scan.projectRoot;
1918
+ const dryRun = opts.dryRun ?? false;
1919
+ const includeTests = opts.includeTests ?? false;
1920
+ const maxFiles = opts.maxFiles ?? plan.policies.maxFilesAutoApply;
1921
+ const files = [];
1922
+ let appliedCount = 0;
1923
+ for (const patch of plan.patches) {
1924
+ const fileStart = Date.now();
1925
+ if (isAlreadyTransformed(root, patch.filePath)) {
1926
+ files.push({
1927
+ filePath: patch.filePath,
1928
+ applied: false,
1929
+ method: "skipped",
1930
+ reason: "Transformer already present in file",
1931
+ durationMs: Date.now() - fileStart
1932
+ });
1933
+ continue;
1934
+ }
1935
+ if (!includeTests && isTestOrFixtureFile(patch.filePath)) {
1936
+ files.push({
1937
+ filePath: patch.filePath,
1938
+ applied: false,
1939
+ method: "skipped",
1940
+ reason: "Test/fixture file excluded (use --include-tests to include)",
1941
+ durationMs: Date.now() - fileStart
1942
+ });
1943
+ continue;
1944
+ }
1945
+ if (isGeneratedFile(patch.filePath)) {
1946
+ files.push({
1947
+ filePath: patch.filePath,
1948
+ applied: false,
1949
+ method: "skipped",
1950
+ reason: "Generated file excluded",
1951
+ durationMs: Date.now() - fileStart
1952
+ });
1953
+ continue;
1954
+ }
1955
+ if (appliedCount >= maxFiles) {
1956
+ files.push({
1957
+ filePath: patch.filePath,
1958
+ applied: false,
1959
+ method: "skipped",
1960
+ reason: `Max files threshold reached (${maxFiles})`,
1961
+ durationMs: Date.now() - fileStart
1962
+ });
1963
+ continue;
1964
+ }
1965
+ if (plan.policies.llmOnly && plan.policies.allowLlmFallback) {
1966
+ consola2.info(`LLM-only mode: ${patch.filePath}...`);
1967
+ const llmResult = await applyLlmPatch(
1968
+ root,
1969
+ patch,
1970
+ plan.scan.cms,
1971
+ plan.scan.framework.name
1972
+ );
1973
+ files.push({
1974
+ filePath: patch.filePath,
1975
+ applied: llmResult.applied,
1976
+ method: "llm",
1977
+ reason: llmResult.reason,
1978
+ durationMs: Date.now() - fileStart
1979
+ });
1980
+ if (llmResult.applied) appliedCount++;
1981
+ continue;
1982
+ }
1983
+ const astResult = applyAstPatch(root, patch, dryRun);
1984
+ if (astResult.applied) {
1985
+ appliedCount++;
1986
+ files.push({
1987
+ filePath: patch.filePath,
1988
+ applied: true,
1989
+ method: "ast",
1990
+ durationMs: Date.now() - fileStart
1991
+ });
1992
+ continue;
1993
+ }
1994
+ const shouldTryLlm = plan.policies.allowLlmFallback && (plan.policies.llmFallbackForAll || patch.confidence === "low");
1995
+ if (shouldTryLlm) {
1996
+ consola2.info(`AST failed for ${patch.filePath}, trying LLM fallback...`);
1997
+ const llmResult = await applyLlmPatch(
1998
+ root,
1999
+ patch,
2000
+ plan.scan.cms,
2001
+ plan.scan.framework.name
2002
+ );
2003
+ files.push({
2004
+ filePath: patch.filePath,
2005
+ applied: llmResult.applied,
2006
+ method: "llm",
2007
+ reason: llmResult.reason,
2008
+ durationMs: Date.now() - fileStart
2009
+ });
2010
+ if (llmResult.applied) appliedCount++;
2011
+ continue;
2012
+ }
2013
+ files.push({
2014
+ filePath: patch.filePath,
2015
+ applied: false,
2016
+ method: "skipped",
2017
+ reason: astResult.reason ?? "AST patch could not be applied",
2018
+ durationMs: Date.now() - fileStart
2019
+ });
2020
+ }
2021
+ let envUpdated = false;
2022
+ if (!dryRun) {
2023
+ for (const envFile of plan.env.files) {
2024
+ const envPath = join9(root, envFile);
2025
+ if (existsSync6(envPath)) {
2026
+ const content = readFileSync8(envPath, "utf-8");
2027
+ if (!content.includes(plan.env.key)) {
2028
+ appendFileSync(envPath, `
2029
+ ${plan.env.key}=${plan.env.placeholder}
2030
+ `);
2031
+ envUpdated = true;
2032
+ }
2033
+ }
2034
+ }
2035
+ if (plan.env.files.length === 0) {
2036
+ const envExamplePath = join9(root, ".env.example");
2037
+ writeFileSync4(
2038
+ envExamplePath,
2039
+ `${plan.env.key}=${plan.env.placeholder}
2040
+ `,
2041
+ "utf-8"
2042
+ );
2043
+ envUpdated = true;
2044
+ }
2045
+ }
2046
+ return {
2047
+ schemaVersion: "1.0",
2048
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2049
+ projectRoot: root,
2050
+ gitBranch: null,
2051
+ gitCommit: null,
2052
+ installed: false,
2053
+ envUpdated,
2054
+ files,
2055
+ totalDurationMs: Date.now() - startTime
2056
+ };
2057
+ }
2058
+
2059
+ // src/git/index.ts
2060
+ import { execSync } from "child_process";
2061
+ import consola3 from "consola";
2062
+ function exec(cmd, cwd) {
2063
+ try {
2064
+ return execSync(cmd, { cwd, encoding: "utf-8", stdio: "pipe" }).trim();
2065
+ } catch {
2066
+ return "";
2067
+ }
2068
+ }
2069
+ var gitOps = {
2070
+ isGitRepo(cwd) {
2071
+ return exec("git rev-parse --is-inside-work-tree", cwd) === "true";
2072
+ },
2073
+ isClean(cwd) {
2074
+ return exec("git diff --quiet && git diff --cached --quiet && echo clean", cwd) === "clean";
2075
+ },
2076
+ getCurrentBranch(cwd) {
2077
+ const branch = exec("git branch --show-current", cwd);
2078
+ return branch || null;
2079
+ },
2080
+ getHeadCommit(cwd) {
2081
+ const hash = exec("git rev-parse --short HEAD", cwd);
2082
+ return hash || null;
2083
+ },
2084
+ createBranch(cwd, name) {
2085
+ try {
2086
+ execSync(`git checkout -b ${name}`, { cwd, stdio: "pipe" });
2087
+ return true;
2088
+ } catch {
2089
+ consola3.warn(`Failed to create branch: ${name}`);
2090
+ return false;
2091
+ }
2092
+ },
2093
+ stageAll(cwd) {
2094
+ execSync("git add -A", { cwd, stdio: "pipe" });
2095
+ },
2096
+ commit(cwd, message) {
2097
+ try {
2098
+ execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
2099
+ cwd,
2100
+ stdio: "pipe"
2101
+ });
2102
+ return exec("git rev-parse --short HEAD", cwd);
2103
+ } catch {
2104
+ consola3.warn("Failed to create commit");
2105
+ return null;
2106
+ }
2107
+ },
2108
+ rollbackToCommit(cwd, commitHash) {
2109
+ try {
2110
+ execSync(`git reset --hard ${commitHash}`, { cwd, stdio: "pipe" });
2111
+ return true;
2112
+ } catch {
2113
+ consola3.error(`Failed to rollback to ${commitHash}`);
2114
+ return false;
2115
+ }
2116
+ },
2117
+ getLastCommitByAgent(cwd) {
2118
+ const log = exec(
2119
+ 'git log --oneline --all --grep="cmsassets-agent" -n 1 --format="%H"',
2120
+ cwd
2121
+ );
2122
+ return log || null;
2123
+ },
2124
+ getCommitBefore(cwd, commitHash) {
2125
+ const parent = exec(`git rev-parse ${commitHash}~1`, cwd);
2126
+ return parent || null;
2127
+ },
2128
+ push(cwd, remote = "origin") {
2129
+ try {
2130
+ const branch = exec("git branch --show-current", cwd);
2131
+ execSync(`git push -u ${remote} ${branch}`, { cwd, stdio: "pipe" });
2132
+ return true;
2133
+ } catch {
2134
+ consola3.warn("Failed to push to remote");
2135
+ return false;
2136
+ }
2137
+ }
2138
+ };
2139
+
2140
+ // src/verifier/index.ts
2141
+ import { execSync as execSync2 } from "child_process";
2142
+ import consola4 from "consola";
2143
+
2144
+ // src/verifier/profiles.ts
2145
+ function getQuickSteps(framework) {
2146
+ const steps = [];
2147
+ switch (framework) {
2148
+ case "nuxt":
2149
+ case "nuxt2":
2150
+ steps.push({
2151
+ name: "nuxt-prepare",
2152
+ command: "npx nuxt prepare",
2153
+ required: false
2154
+ });
2155
+ break;
2156
+ case "next":
2157
+ steps.push({
2158
+ name: "tsc-noEmit",
2159
+ command: "npx tsc --noEmit",
2160
+ required: false
2161
+ });
2162
+ break;
2163
+ case "astro":
2164
+ steps.push({
2165
+ name: "astro-check",
2166
+ command: "npx astro check",
2167
+ required: false
2168
+ });
2169
+ break;
2170
+ case "sveltekit":
2171
+ steps.push({
2172
+ name: "svelte-check",
2173
+ command: "npx svelte-check",
2174
+ required: false
2175
+ });
2176
+ break;
2177
+ default:
2178
+ steps.push({
2179
+ name: "tsc-noEmit",
2180
+ command: "npx tsc --noEmit",
2181
+ required: false
2182
+ });
2183
+ }
2184
+ return steps;
2185
+ }
2186
+ function getFullSteps(framework) {
2187
+ const steps = getQuickSteps(framework);
2188
+ steps.push({
2189
+ name: "lint",
2190
+ command: "npm run lint",
2191
+ required: false
2192
+ });
2193
+ switch (framework) {
2194
+ case "nuxt":
2195
+ case "nuxt2":
2196
+ steps.push({ name: "build", command: "npm run build", required: true });
2197
+ break;
2198
+ case "next":
2199
+ steps.push({ name: "build", command: "npm run build", required: true });
2200
+ break;
2201
+ case "astro":
2202
+ steps.push({ name: "build", command: "npm run build", required: true });
2203
+ break;
2204
+ case "sveltekit":
2205
+ steps.push({ name: "build", command: "npm run build", required: true });
2206
+ break;
2207
+ default:
2208
+ steps.push({ name: "build", command: "npm run build", required: false });
2209
+ }
2210
+ steps.push({
2211
+ name: "test",
2212
+ command: "npm test",
2213
+ required: false
2214
+ });
2215
+ return steps;
2216
+ }
2217
+ function getVerifySteps(profile, framework) {
2218
+ return profile === "full" ? getFullSteps(framework) : getQuickSteps(framework);
2219
+ }
2220
+
2221
+ // src/verifier/index.ts
2222
+ function verify(root, opts = {}) {
2223
+ const startTime = Date.now();
2224
+ const profile = opts.profile ?? "quick";
2225
+ const framework = opts.framework ?? "unknown";
2226
+ const patchedFiles = opts.patchedFiles ?? [];
2227
+ const warnings = [];
2228
+ const steps = getVerifySteps(profile, framework);
2229
+ let lintPassed = null;
2230
+ let buildPassed = null;
2231
+ let testsPassed = null;
2232
+ for (const step of steps) {
2233
+ consola4.info(`Running: ${step.name} (${step.command})`);
2234
+ try {
2235
+ execSync2(step.command, {
2236
+ cwd: root,
2237
+ stdio: "pipe",
2238
+ timeout: 12e4
2239
+ });
2240
+ if (step.name === "lint") lintPassed = true;
2241
+ if (step.name === "build") buildPassed = true;
2242
+ if (step.name === "test") testsPassed = true;
2243
+ consola4.success(`${step.name} passed`);
2244
+ } catch (err) {
2245
+ const message = err instanceof Error ? err.message : String(err);
2246
+ const truncated = message.substring(0, 500);
2247
+ if (step.name === "lint") lintPassed = false;
2248
+ if (step.name === "build") buildPassed = false;
2249
+ if (step.name === "test") testsPassed = false;
2250
+ if (step.required) {
2251
+ consola4.error(`${step.name} FAILED: ${truncated}`);
2252
+ warnings.push(`${step.name} failed (required): ${truncated}`);
2253
+ } else {
2254
+ consola4.warn(`${step.name} failed (optional): ${truncated}`);
2255
+ warnings.push(`${step.name} failed (optional): ${truncated}`);
2256
+ }
2257
+ }
2258
+ }
2259
+ return {
2260
+ schemaVersion: "1.0",
2261
+ profile,
2262
+ lintPassed,
2263
+ buildPassed,
2264
+ testsPassed,
2265
+ patchedFiles,
2266
+ warnings,
2267
+ durationMs: Date.now() - startTime
2268
+ };
2269
+ }
2270
+
2271
+ export {
2272
+ scan,
2273
+ createPlan,
2274
+ FrameworkName,
2275
+ CmsType,
2276
+ PackageManager,
2277
+ Confidence,
2278
+ InjectionType,
2279
+ VerifyProfile,
2280
+ FrameworkInfo,
2281
+ CmsInfo,
2282
+ InjectionCandidate,
2283
+ ScanResult,
2284
+ FilePatch,
2285
+ PatchPlan,
2286
+ PatchedFileReport,
2287
+ ApplyReport,
2288
+ VerifyReport,
2289
+ createReport,
2290
+ saveReport,
2291
+ savePlanFile,
2292
+ loadPlanFile,
2293
+ applyPlan,
2294
+ gitOps,
2295
+ verify
2296
+ };