@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,37 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
+ }) : x)(function(x) {
10
+ if (typeof require !== "undefined") return require.apply(this, arguments);
11
+ throw Error('Dynamic require of "' + x + '" is not supported');
12
+ });
13
+ var __commonJS = (cb, mod) => function __require2() {
14
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+
33
+ export {
34
+ __require,
35
+ __commonJS,
36
+ __toESM
37
+ };
@@ -0,0 +1,395 @@
1
+ import {
2
+ buildCmsOptions,
3
+ chatCompletion,
4
+ findInjectionPoints,
5
+ getTransformFunctionName,
6
+ isOpenAiError,
7
+ validateDiff
8
+ } from "./chunk-645ASJ55.js";
9
+
10
+ // src/verifier/aiReview.ts
11
+ import { readFileSync as readFileSync2, writeFileSync } from "fs";
12
+ import { join as join2 } from "path";
13
+ import consola from "consola";
14
+ import pc from "picocolors";
15
+
16
+ // src/verifier/rescan.ts
17
+ import { readFileSync } from "fs";
18
+ import { join, relative } from "path";
19
+ function rescanFile(root, filePath, framework, cms) {
20
+ const absPath = join(root, filePath);
21
+ let content;
22
+ try {
23
+ content = readFileSync(absPath, "utf-8");
24
+ } catch {
25
+ return [];
26
+ }
27
+ const hasTransformer = content.includes("@synchronized-studio/response-transformer");
28
+ if (!hasTransformer) {
29
+ return findInjectionPoints(root, framework, cms).filter((c) => c.filePath === relative(root, absPath) || c.filePath === filePath);
30
+ }
31
+ return findUnwrappedCmsCalls(content, filePath, cms);
32
+ }
33
+ var CMS_CALL_PATTERNS = [
34
+ /client\.\w+\s*\(/g,
35
+ /\$prismic\.\w+\s*\(/g,
36
+ /\$contentful\.\w+\s*\(/g,
37
+ /\$sanity\.\w+\s*\(/g,
38
+ /usePrismic\(\)/g,
39
+ /createClient\(\)/g,
40
+ /\$fetch\s*\(\s*['"`]\/api\//g
41
+ ];
42
+ var TRANSFORM_WRAPPERS = [
43
+ "transformPrismicAssetUrls",
44
+ "transformContentfulAssetUrls",
45
+ "transformSanityAssetUrls",
46
+ "transformShopifyAssetUrls",
47
+ "transformCloudinaryAssetUrls",
48
+ "transformImgixAssetUrls",
49
+ "transformGenericAssetUrls",
50
+ "transformCmsAssetUrls"
51
+ ];
52
+ function findEnclosingFunctionRange(lines, lineIdx) {
53
+ let start = lineIdx;
54
+ let braceDepth = 0;
55
+ for (let i = lineIdx; i >= 0; i--) {
56
+ for (const c of lines[i]) {
57
+ if (c === "}") braceDepth++;
58
+ else if (c === "{") braceDepth--;
59
+ }
60
+ if (/(?:async\s+)?(?:function\b|\w+\s*\(|=>\s*\{)/.test(lines[i]) && braceDepth <= 0) {
61
+ start = i;
62
+ break;
63
+ }
64
+ }
65
+ braceDepth = 0;
66
+ let end = lineIdx;
67
+ for (let i = start; i < lines.length; i++) {
68
+ for (const c of lines[i]) {
69
+ if (c === "{") braceDepth++;
70
+ else if (c === "}") braceDepth--;
71
+ }
72
+ if (braceDepth === 0 && i > start) {
73
+ end = i;
74
+ break;
75
+ }
76
+ }
77
+ return { start, end };
78
+ }
79
+ function findUnwrappedCmsCalls(content, filePath, cms) {
80
+ const lines = content.split("\n");
81
+ const candidates = [];
82
+ for (let i = 0; i < lines.length; i++) {
83
+ const line = lines[i];
84
+ const trimmed = line.trim();
85
+ if (trimmed.startsWith("import ") || trimmed.startsWith("//") || trimmed.startsWith("*")) {
86
+ continue;
87
+ }
88
+ const hasCmsCall = CMS_CALL_PATTERNS.some((p) => {
89
+ p.lastIndex = 0;
90
+ return p.test(trimmed);
91
+ });
92
+ if (!hasCmsCall) continue;
93
+ const isWrappedInline = TRANSFORM_WRAPPERS.some((fn) => trimmed.includes(fn));
94
+ if (isWrappedInline) continue;
95
+ const fnRange = findEnclosingFunctionRange(lines, i);
96
+ const fnBody = lines.slice(fnRange.start, fnRange.end + 1).join("\n");
97
+ const fnHasTransform = TRANSFORM_WRAPPERS.some((fn) => fnBody.includes(fn));
98
+ if (fnHasTransform) continue;
99
+ const simpleAssign = trimmed.match(/(?:const|let)\s+(\w+)\s*=/);
100
+ const destructAssign = trimmed.match(/(?:const|let)\s+\{([^}]+)\}\s*=/);
101
+ const varNames = [];
102
+ if (simpleAssign) varNames.push(simpleAssign[1]);
103
+ if (destructAssign) {
104
+ varNames.push(...destructAssign[1].split(",").map((s) => s.trim().split(":")[0].trim()).filter(Boolean));
105
+ }
106
+ if (varNames.length > 0) {
107
+ const ahead = lines.slice(i + 1, Math.min(lines.length, i + 40)).join("\n");
108
+ const isAnyVarWrapped = varNames.some(
109
+ (varName) => TRANSFORM_WRAPPERS.some(
110
+ (fn) => ahead.includes(`${fn}(${varName}`) || ahead.includes(`${fn}( ${varName}`) || new RegExp(`${fn}\\([\\s\\S]{0,300}\\b${varName}\\b`).test(ahead)
111
+ )
112
+ );
113
+ if (isAnyVarWrapped) continue;
114
+ }
115
+ const contextSlice = lines.slice(Math.max(0, i - 10), Math.min(lines.length, i + 15)).join("\n");
116
+ if (/transform\s*:/.test(contextSlice)) continue;
117
+ candidates.push({
118
+ filePath,
119
+ line: i + 1,
120
+ type: "return",
121
+ score: 70,
122
+ confidence: "medium",
123
+ targetCode: trimmed,
124
+ context: lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 3)).join("\n"),
125
+ reasons: ["CMS SDK call without transform wrapper (post-patch rescan)"]
126
+ });
127
+ }
128
+ return candidates;
129
+ }
130
+
131
+ // src/verifier/aiReview.ts
132
+ async function aiReviewAll(root, patchedFiles, plan, opts = {}) {
133
+ const startTime = Date.now();
134
+ const maxIterations = opts.maxIterations ?? 3;
135
+ const model = opts.model ?? "gpt-4o";
136
+ const results = [];
137
+ let totalTokens = 0;
138
+ const uniqueFiles = [...new Set(patchedFiles)];
139
+ for (const filePath of uniqueFiles) {
140
+ consola.info(`${pc.cyan("AI review:")} ${filePath}`);
141
+ const result = await reviewFileLoop(root, filePath, plan, {
142
+ maxIterations,
143
+ model
144
+ });
145
+ totalTokens += result.tokensUsed;
146
+ results.push(result.fileResult);
147
+ const icon = result.fileResult.passed ? pc.green("\u2713") : pc.red("\u2717");
148
+ const detail = result.fileResult.fixApplied ? pc.dim("(auto-fixed)") : "";
149
+ consola.info(` ${icon} ${filePath} ${detail}`);
150
+ if (!result.fileResult.passed) {
151
+ for (const issue of result.fileResult.issues) {
152
+ consola.info(` ${pc.yellow("\u26A0")} ${issue}`);
153
+ }
154
+ }
155
+ }
156
+ const filesPassed = results.filter((r) => r.passed).length;
157
+ const filesFixed = results.filter((r) => r.fixApplied).length;
158
+ const filesFailed = results.filter((r) => !r.passed).length;
159
+ return {
160
+ schemaVersion: "1.0",
161
+ filesReviewed: results.length,
162
+ filesPassed,
163
+ filesFixed,
164
+ filesFailed,
165
+ results,
166
+ totalDurationMs: Date.now() - startTime,
167
+ tokensUsed: totalTokens
168
+ };
169
+ }
170
+ async function reviewFileLoop(root, filePath, plan, opts) {
171
+ let tokensUsed = 0;
172
+ let fixAttempted = false;
173
+ let fixApplied = false;
174
+ for (let iter = 0; iter < opts.maxIterations; iter++) {
175
+ const remaining = rescanFile(
176
+ root,
177
+ filePath,
178
+ plan.scan.framework.name,
179
+ plan.scan.cms
180
+ );
181
+ const rescanIssues = remaining.map(
182
+ (c) => `Line ${c.line}: ${c.targetCode.substring(0, 80)}`
183
+ );
184
+ const absPath = join2(root, filePath);
185
+ let content;
186
+ try {
187
+ content = readFileSync2(absPath, "utf-8");
188
+ } catch {
189
+ return {
190
+ fileResult: {
191
+ filePath,
192
+ passed: false,
193
+ issues: ["Cannot read file"],
194
+ fixAttempted,
195
+ fixApplied,
196
+ iterations: iter + 1
197
+ },
198
+ tokensUsed
199
+ };
200
+ }
201
+ const reviewResult = await aiSemanticReview(content, filePath, plan, opts.model, rescanIssues);
202
+ tokensUsed += reviewResult.tokensUsed;
203
+ if (reviewResult.passed) {
204
+ return {
205
+ fileResult: {
206
+ filePath,
207
+ passed: true,
208
+ issues: [],
209
+ fixAttempted,
210
+ fixApplied,
211
+ iterations: iter + 1
212
+ },
213
+ tokensUsed
214
+ };
215
+ }
216
+ if (!reviewResult.diff) {
217
+ return {
218
+ fileResult: {
219
+ filePath,
220
+ passed: false,
221
+ issues: reviewResult.issues,
222
+ fixAttempted,
223
+ fixApplied,
224
+ iterations: iter + 1
225
+ },
226
+ tokensUsed
227
+ };
228
+ }
229
+ fixAttempted = true;
230
+ const applied = applyAiDiff(root, filePath, content, reviewResult.diff);
231
+ if (applied) {
232
+ fixApplied = true;
233
+ consola.info(` ${pc.blue("\u21BB")} Applied AI fix (iteration ${iter + 1})`);
234
+ } else {
235
+ return {
236
+ fileResult: {
237
+ filePath,
238
+ passed: false,
239
+ issues: [...reviewResult.issues, "AI fix could not be applied"],
240
+ fixAttempted,
241
+ fixApplied,
242
+ iterations: iter + 1
243
+ },
244
+ tokensUsed
245
+ };
246
+ }
247
+ }
248
+ const finalRemaining = rescanFile(root, filePath, plan.scan.framework.name, plan.scan.cms);
249
+ const passed = finalRemaining.length === 0;
250
+ return {
251
+ fileResult: {
252
+ filePath,
253
+ passed,
254
+ issues: passed ? [] : finalRemaining.map((c) => `Line ${c.line}: unwrapped CMS call \u2014 ${c.targetCode.substring(0, 80)}`),
255
+ fixAttempted,
256
+ fixApplied,
257
+ iterations: opts.maxIterations
258
+ },
259
+ tokensUsed
260
+ };
261
+ }
262
+ async function aiSemanticReview(content, filePath, plan, model, rescanIssues) {
263
+ const fn = getTransformFunctionName(plan.scan.cms.type);
264
+ const opts = buildCmsOptions(plan.scan.cms.type, plan.scan.cms.params);
265
+ const rescanSection = rescanIssues.length > 0 ? `
266
+ Rule-based rescan found these potentially unwrapped CMS calls:
267
+ ${rescanIssues.map((i) => `- ${i}`).join("\n")}
268
+ ` : "";
269
+ const prompt = `You are verifying that CMS asset URL transformations were correctly applied to a source file.
270
+
271
+ CMS: ${plan.scan.cms.type}
272
+ Transform function: ${fn}
273
+ Expected options: ${opts}
274
+ Framework: ${plan.scan.framework.name}
275
+ ${rescanSection}
276
+ File (${filePath}):
277
+ \`\`\`
278
+ ${content}
279
+ \`\`\`
280
+
281
+ Check ONLY the following \u2014 do NOT be overly aggressive:
282
+ 1. Is the import \`import { ${fn} } from '@synchronized-studio/response-transformer'\` present?
283
+ 2. For functions that RETURN CMS data objects/arrays, is the return wrapped with ${fn}()?
284
+ - Wrapping at the return level is CORRECT and SUFFICIENT.
285
+ - \`return ${fn}({ model: document }, ${opts})\` is correct.
286
+ - \`return ${fn}(data, ${opts})\` is correct.
287
+ - Do NOT flag intermediate CMS SDK calls ($prismic, $contentful, client.get) as unwrapped
288
+ when the function's return is already wrapped.
289
+ 3. Are the options (${opts}) correct in every ${fn}() call?
290
+
291
+ IMPORTANT \u2014 do NOT flag any of these as issues:
292
+ - Functions that do NOT return a value (e.g. they only commit to Vuex store) \u2014 these are FINE as-is
293
+ - Functions that return simple strings, URLs, booleans, or "#" \u2014 these are NOT CMS data
294
+ - Error returns like \`return err\`, \`return []\`, \`return undefined\` \u2014 skip these
295
+ - Functions that return \`dispatch(...)\` or delegate to another action \u2014 the target action handles the transform
296
+ - A function that already contains ${fn}() calls IS correctly handled \u2014 do not ask for more wrapping
297
+ - If the file already imports and uses ${fn} in multiple places, assume it is correctly integrated
298
+
299
+ Respond with JSON only (no markdown fences, no explanation):
300
+ - If everything is correct: {"passed":true}
301
+ - If there are issues: {"passed":false,"issues":["description of each issue"],"diff":"unified diff that fixes ALL issues"}
302
+
303
+ The diff MUST:
304
+ - Use standard unified diff format (--- a/file, +++ b/file, @@ hunks)
305
+ - Only add/modify transformer-related code
306
+ - Not remove or change any existing logic`;
307
+ const result = await chatCompletion(prompt, { model, maxTokens: 4e3 });
308
+ if (isOpenAiError(result)) {
309
+ return { passed: false, issues: [result.error], tokensUsed: 0 };
310
+ }
311
+ const raw = result.content.replace(/^```(?:json)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
312
+ try {
313
+ const parsed = JSON.parse(raw);
314
+ if (parsed.passed === true) {
315
+ return { passed: true, issues: [], tokensUsed: result.tokensUsed };
316
+ }
317
+ const issues = Array.isArray(parsed.issues) ? parsed.issues : [];
318
+ const diff = typeof parsed.diff === "string" ? parsed.diff : void 0;
319
+ return { passed: false, issues, diff, tokensUsed: result.tokensUsed };
320
+ } catch {
321
+ return {
322
+ passed: false,
323
+ issues: ["AI returned non-JSON response"],
324
+ tokensUsed: result.tokensUsed
325
+ };
326
+ }
327
+ }
328
+ function applyAiDiff(root, filePath, originalContent, diff) {
329
+ const validation = validateDiff(diff, [filePath]);
330
+ if (!validation.valid) {
331
+ consola.warn(` AI diff validation failed: ${validation.errors.join("; ")}`);
332
+ return false;
333
+ }
334
+ const newContent = applyUnifiedDiff(originalContent, diff);
335
+ if (!newContent || newContent === originalContent) {
336
+ return false;
337
+ }
338
+ try {
339
+ const absPath = join2(root, filePath);
340
+ writeFileSync(absPath, newContent, "utf-8");
341
+ return true;
342
+ } catch {
343
+ return false;
344
+ }
345
+ }
346
+ function applyUnifiedDiff(original, diff) {
347
+ const lines = original.split("\n");
348
+ const diffLines = diff.split("\n");
349
+ const result = [...lines];
350
+ let offset = 0;
351
+ for (let i = 0; i < diffLines.length; i++) {
352
+ const line = diffLines[i];
353
+ const hunkMatch = line.match(/^@@\s*-(\d+)(?:,(\d+))?\s*\+(\d+)(?:,(\d+))?\s*@@/);
354
+ if (!hunkMatch) continue;
355
+ const oldStart = parseInt(hunkMatch[1], 10) - 1;
356
+ let pos = oldStart + offset;
357
+ for (let j = i + 1; j < diffLines.length; j++) {
358
+ const hunkLine = diffLines[j];
359
+ if (hunkLine.startsWith("@@") || hunkLine.startsWith("diff ") || hunkLine.startsWith("--- ") || hunkLine.startsWith("+++ ")) {
360
+ break;
361
+ }
362
+ if (hunkLine.startsWith("-")) {
363
+ const expected = hunkLine.substring(1);
364
+ if (pos < result.length && result[pos].trim() === expected.trim()) {
365
+ result.splice(pos, 1);
366
+ offset--;
367
+ } else {
368
+ let found = false;
369
+ for (let k = Math.max(0, pos - 8); k < Math.min(result.length, pos + 9); k++) {
370
+ if (result[k].trim() === expected.trim()) {
371
+ result.splice(k, 1);
372
+ offset--;
373
+ pos = k;
374
+ found = true;
375
+ break;
376
+ }
377
+ }
378
+ if (!found) return null;
379
+ }
380
+ } else if (hunkLine.startsWith("+")) {
381
+ const newLine = hunkLine.substring(1);
382
+ result.splice(pos, 0, newLine);
383
+ pos++;
384
+ offset++;
385
+ } else if (hunkLine.startsWith(" ")) {
386
+ pos++;
387
+ }
388
+ }
389
+ }
390
+ return result.join("\n");
391
+ }
392
+
393
+ export {
394
+ aiReviewAll
395
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node