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