|
1 | 1 | import chalk from 'chalk'; |
2 | | -import * as Diff from 'diff'; |
3 | 2 | import { ParameterOperation, PlanResponseData, ResourceOperation } from '@codifycli/schemas'; |
4 | 3 |
|
5 | 4 | import { Plan, ResourcePlan } from '../entities/plan.js'; |
@@ -227,53 +226,63 @@ function isPlainObject(value: unknown): value is Record<string, unknown> { |
227 | 226 | return typeof value === 'object' && value !== null && !Array.isArray(value); |
228 | 227 | } |
229 | 228 |
|
230 | | -function formatObjectDiff(name: string, previousValue: object, newValue: object): string { |
231 | | - const prevJson = JSON.stringify(previousValue, null, 2); |
232 | | - const newJson = JSON.stringify(newValue, null, 2); |
233 | | - const diff = Diff.diffLines(prevJson, newJson); |
234 | | - |
235 | | - const coloredLines: Array<{ text: string; added: boolean; removed: boolean }> = []; |
236 | | - for (const part of diff) { |
237 | | - const lines = part.value.split('\n').filter((l) => l.length > 0); |
238 | | - for (const line of lines) { |
239 | | - // Skip the outer { } braces — we render those as the header/footer |
240 | | - if (line === '{' || line === '}') continue; |
241 | | - coloredLines.push({ |
242 | | - text: part.added ? chalk.green(line) : part.removed ? chalk.red(line) : line, |
243 | | - added: part.added ?? false, |
244 | | - removed: part.removed ?? false, |
245 | | - }); |
| 229 | +function formatObjectDiff(name: string, previousValue: Record<string, unknown>, newValue: Record<string, unknown>): string { |
| 230 | + const allKeys = Array.from(new Set([...Object.keys(previousValue), ...Object.keys(newValue)])); |
| 231 | + |
| 232 | + type Entry = { op: 'noop' | 'add' | 'remove' | 'modify'; key: string; prev?: unknown; next?: unknown }; |
| 233 | + const entries: Entry[] = allKeys.map((key) => { |
| 234 | + const inPrev = Object.hasOwn(previousValue, key); |
| 235 | + const inNext = Object.hasOwn(newValue, key); |
| 236 | + if (!inPrev) return { op: 'add', key, next: newValue[key] }; |
| 237 | + if (!inNext) return { op: 'remove', key, prev: previousValue[key] }; |
| 238 | + if (JSON.stringify(previousValue[key]) === JSON.stringify(newValue[key])) { |
| 239 | + return { op: 'noop', key, next: newValue[key] }; |
246 | 240 | } |
247 | | - } |
| 241 | + return { op: 'modify', key, prev: previousValue[key], next: newValue[key] }; |
| 242 | + }); |
248 | 243 |
|
249 | 244 | const CONTEXT = 2; |
250 | | - const included = new Set<number>(); |
251 | | - for (let i = 0; i < coloredLines.length; i++) { |
252 | | - if (coloredLines[i].added || coloredLines[i].removed) { |
253 | | - for (let j = Math.max(0, i - CONTEXT); j <= Math.min(coloredLines.length - 1, i + CONTEXT); j++) { |
254 | | - included.add(j); |
| 245 | + const includedIndices = new Set<number>(); |
| 246 | + for (let i = 0; i < entries.length; i++) { |
| 247 | + if (entries[i].op !== 'noop') { |
| 248 | + for (let j = Math.max(0, i - CONTEXT); j <= Math.min(entries.length - 1, i + CONTEXT); j++) { |
| 249 | + includedIndices.add(j); |
255 | 250 | } |
256 | 251 | } |
257 | 252 | } |
258 | 253 |
|
259 | 254 | const resultLines: string[] = [`${chalk.yellow('~')} "${name}": {`]; |
260 | 255 | let lastIncluded = -1; |
261 | 256 |
|
262 | | - for (let i = 0; i < coloredLines.length; i++) { |
263 | | - if (!included.has(i)) continue; |
| 257 | + for (let i = 0; i < entries.length; i++) { |
| 258 | + if (!includedIndices.has(i)) continue; |
264 | 259 | if (lastIncluded !== -1 && i > lastIncluded + 1) { |
265 | 260 | resultLines.push(' ...'); |
266 | 261 | } |
267 | | - const { text, added, removed } = coloredLines[i]; |
268 | | - const symbol = added ? chalk.green('+') : removed ? chalk.red('-') : ' '; |
269 | | - resultLines.push(` ${symbol} ${text}`); |
270 | 262 | lastIncluded = i; |
| 263 | + const { op, key, prev, next } = entries[i]; |
| 264 | + |
| 265 | + if (op === 'noop') { |
| 266 | + resultLines.push(` "${key}": ${formatValue(next)},`); |
| 267 | + } else if (op === 'add') { |
| 268 | + resultLines.push(` ${chalk.green('+')} ${chalk.green(`"${key}": ${formatValue(next)},`)}`); |
| 269 | + } else if (op === 'remove') { |
| 270 | + resultLines.push(` ${chalk.red('-')} ${chalk.red(`"${key}": ${formatValue(prev)},`)}`); |
| 271 | + } else { |
| 272 | + resultLines.push(` ${chalk.yellow('~')} "${key}": ${formatValue(prev)} -> ${formatValue(next)},`); |
| 273 | + } |
271 | 274 | } |
272 | 275 |
|
273 | 276 | resultLines.push(' },'); |
274 | 277 | return resultLines.join('\n'); |
275 | 278 | } |
276 | 279 |
|
| 280 | +function formatValue(value: unknown): string { |
| 281 | + if (typeof value === 'string') return `"${value}"`; |
| 282 | + if (value === null || value === undefined) return String(value); |
| 283 | + return JSON.stringify(value); |
| 284 | +} |
| 285 | + |
277 | 286 | const OBJECT_SINGLE_SIDE_MAX_LINES = 20; |
278 | 287 |
|
279 | 288 | function formatObjectSingleSide(name: string, value: object, operation: ParameterOperation): string { |
|
0 commit comments