@embeddables/cli 0.9.5 → 0.10.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,738 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { compileAllPages } from '../compiler/index.js';
5
+ import { getAccessToken } from '../auth/index.js';
6
+ import { captureException, createLogger, exit } from '../logger.js';
7
+ import * as stdout from '../stdout.js';
8
+ import { inferEmbeddableFromCwd } from '../helpers/utils.js';
9
+ import { promptForLocalEmbeddable } from '../prompts/index.js';
10
+ import { getSentryContextFromEmbeddableConfig, getSentryContextFromProjectConfig, setSentryContext, } from '../sentry-context.js';
11
+ // ─── Stable serialization (key-order-independent) ─────────────────────────────
12
+ /**
13
+ * JSON.stringify with sorted object keys at every nesting level.
14
+ * Produces identical output for semantically equal objects regardless of
15
+ * property insertion order, while preserving array element order.
16
+ */
17
+ export function stableStringify(value) {
18
+ return JSON.stringify(value, (_key, val) => {
19
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
20
+ const sorted = {};
21
+ for (const k of Object.keys(val).sort()) {
22
+ sorted[k] = val[k];
23
+ }
24
+ return sorted;
25
+ }
26
+ return val;
27
+ });
28
+ }
29
+ function createColorizer(useColor) {
30
+ if (!useColor) {
31
+ const id = (s) => s;
32
+ return { red: id, green: id, yellow: id, gray: id, bold: id, cyan: id, dim: id, white: id };
33
+ }
34
+ return pc;
35
+ }
36
+ // ─── Normalization (same as inspect/pull) ─────────────────────────────────────
37
+ function normalizeCustomValidationFunctionsInFlow(flow) {
38
+ function walkComponent(comp) {
39
+ const v = comp.custom_validation_function;
40
+ if (typeof v === 'string') {
41
+ comp.custom_validation_function = v.replace(/\\n/g, '\n');
42
+ }
43
+ const children = comp.components ?? comp.buttons;
44
+ if (Array.isArray(children)) {
45
+ for (const c of children) {
46
+ if (c && typeof c === 'object')
47
+ walkComponent(c);
48
+ }
49
+ }
50
+ }
51
+ const pages = flow.pages;
52
+ if (Array.isArray(pages)) {
53
+ for (const page of pages) {
54
+ if (page && typeof page === 'object' && Array.isArray(page.components)) {
55
+ for (const comp of page.components) {
56
+ if (comp && typeof comp === 'object')
57
+ walkComponent(comp);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ const components = flow.components;
63
+ if (Array.isArray(components)) {
64
+ for (const comp of components) {
65
+ if (comp && typeof comp === 'object')
66
+ walkComponent(comp);
67
+ }
68
+ }
69
+ }
70
+ // ─── Version specifier parsing ────────────────────────────────────────────────
71
+ const NAMED_SPECIFIERS = new Set(['local', 'latest', 'staging', 'prod']);
72
+ function parseBaseVersion(raw) {
73
+ const lower = raw.toLowerCase().trim();
74
+ if (NAMED_SPECIFIERS.has(lower))
75
+ return lower;
76
+ const num = parseInt(lower, 10);
77
+ if (!isNaN(num) && String(num) === lower)
78
+ return String(num);
79
+ throw new Error(`Invalid version specifier: "${raw}". Use a version number, "latest", "staging", "prod", or "local". Prefix with branch@ to target a specific branch.`);
80
+ }
81
+ export function parseVersionSpecifier(spec) {
82
+ const trimmed = spec.trim();
83
+ const atIndex = trimmed.lastIndexOf('@');
84
+ if (atIndex > 0) {
85
+ const branch = trimmed.slice(0, atIndex);
86
+ const versionPart = trimmed.slice(atIndex + 1);
87
+ const version = parseBaseVersion(versionPart);
88
+ if (version === 'local') {
89
+ throw new Error(`Cannot use "local" with a branch specifier: "${spec}". "local" always compiles from local files.`);
90
+ }
91
+ return { version, branch };
92
+ }
93
+ return { version: parseBaseVersion(trimmed) };
94
+ }
95
+ export function versionLabel(spec) {
96
+ const base = NAMED_SPECIFIERS.has(spec.version) ? spec.version : `v${spec.version}`;
97
+ if (spec.branch)
98
+ return `${spec.branch}@${base}`;
99
+ return base;
100
+ }
101
+ // ─── Version resolution ───────────────────────────────────────────────────────
102
+ export async function resolveLocalVersion(embeddableId) {
103
+ const pagesGlob = `embeddables/${embeddableId}/pages/**/*.page.tsx`;
104
+ const outPath = path.join('embeddables', embeddableId, '.generated', 'embeddable-diff-local.json');
105
+ const stylesDir = path.join('embeddables', embeddableId, 'styles');
106
+ await compileAllPages({
107
+ pagesGlob,
108
+ outPath,
109
+ pageKeyFrom: 'filename',
110
+ stylesDir,
111
+ embeddableId,
112
+ fixLint: true,
113
+ });
114
+ const content = fs.readFileSync(outPath, 'utf8');
115
+ try {
116
+ fs.unlinkSync(outPath);
117
+ }
118
+ catch {
119
+ /* ignore cleanup errors */
120
+ }
121
+ return JSON.parse(content);
122
+ }
123
+ export async function fetchCloudVersion(embeddableId, version, opts) {
124
+ const engineOrigin = opts.engine || 'https://engine.embeddables.com';
125
+ let url = `${engineOrigin}/${embeddableId}?version=${version}`;
126
+ if (opts.branch)
127
+ url += `&embeddable_branch=${opts.branch}`;
128
+ const accessToken = getAccessToken();
129
+ const headers = {};
130
+ if (accessToken) {
131
+ headers['Authorization'] = `Bearer ${accessToken}`;
132
+ }
133
+ const response = await fetch(url, { headers });
134
+ if (!response.ok) {
135
+ throw new Error(`Failed to fetch version "${version}": ${response.status} ${response.statusText}`);
136
+ }
137
+ const data = await response.json();
138
+ if (!data.flow) {
139
+ throw new Error(`Response for version "${version}" does not contain a 'flow' property`);
140
+ }
141
+ const flow = data.flow;
142
+ normalizeCustomValidationFunctionsInFlow(flow);
143
+ return flow;
144
+ }
145
+ async function resolveVersion(spec, embeddableId, opts) {
146
+ if (spec === 'local')
147
+ return resolveLocalVersion(embeddableId);
148
+ return fetchCloudVersion(embeddableId, spec, opts);
149
+ }
150
+ // ─── Diff computation ─────────────────────────────────────────────────────────
151
+ export function computeDiff(from, to, depth, filters = { page: null, component: null }) {
152
+ const pages = diffPages(from, to, depth, filters);
153
+ const globalComponents = diffGlobalComponents(from, to, depth, filters);
154
+ const sections = diffOtherSections(from, to);
155
+ const summary = computeSummary(pages, globalComponents);
156
+ return { pages, globalComponents, sections, summary };
157
+ }
158
+ function diffPages(from, to, depth, filters) {
159
+ const fromPages = (Array.isArray(from.pages) ? from.pages : []);
160
+ const toPages = (Array.isArray(to.pages) ? to.pages : []);
161
+ const fromByKey = new Map();
162
+ for (const p of fromPages)
163
+ fromByKey.set(p.key ?? p.id, p);
164
+ const toByKey = new Map();
165
+ for (const p of toPages)
166
+ toByKey.set(p.key ?? p.id, p);
167
+ const diffs = [];
168
+ for (const [key, page] of fromByKey) {
169
+ if (!toByKey.has(key)) {
170
+ if (!pageMatchesFilter(filters.page, key, page))
171
+ continue;
172
+ const diff = { key, title: page.title, status: 'removed' };
173
+ if (depth !== 'pages' && Array.isArray(page.components)) {
174
+ diff.components = page.components
175
+ .map((comp) => ({
176
+ id: comp.id,
177
+ key: comp.key,
178
+ type: comp.type,
179
+ status: 'removed',
180
+ }))
181
+ .filter((comp) => componentMatchesFilter(filters.component, comp));
182
+ }
183
+ if (shouldIncludePageDiff(diff, depth, filters)) {
184
+ diffs.push(diff);
185
+ }
186
+ }
187
+ }
188
+ for (const [key, page] of toByKey) {
189
+ if (!fromByKey.has(key)) {
190
+ if (!pageMatchesFilter(filters.page, key, page))
191
+ continue;
192
+ const diff = { key, title: page.title, status: 'added' };
193
+ if (depth !== 'pages' && Array.isArray(page.components)) {
194
+ diff.components = page.components
195
+ .map((comp) => ({
196
+ id: comp.id,
197
+ key: comp.key,
198
+ type: comp.type,
199
+ status: 'added',
200
+ ...(depth === 'props'
201
+ ? {
202
+ props: Object.entries(comp)
203
+ .filter(([k]) => !['id', 'key', 'type', 'tags', 'components', 'buttons'].includes(k))
204
+ .map(([name, value]) => ({
205
+ name,
206
+ status: 'added',
207
+ to: value,
208
+ })),
209
+ }
210
+ : {}),
211
+ }))
212
+ .filter((comp) => componentMatchesFilter(filters.component, comp));
213
+ }
214
+ if (shouldIncludePageDiff(diff, depth, filters)) {
215
+ diffs.push(diff);
216
+ }
217
+ }
218
+ }
219
+ for (const [key, fromPage] of fromByKey) {
220
+ const toPage = toByKey.get(key);
221
+ if (!toPage)
222
+ continue;
223
+ if (stableStringify(fromPage) === stableStringify(toPage))
224
+ continue;
225
+ if (!pageMatchesFilter(filters.page, key, fromPage, toPage))
226
+ continue;
227
+ const diff = {
228
+ key,
229
+ title: toPage.title ?? fromPage.title,
230
+ status: 'modified',
231
+ };
232
+ if (depth !== 'pages') {
233
+ diff.components = diffComponentArrays(Array.isArray(fromPage.components) ? fromPage.components : [], Array.isArray(toPage.components) ? toPage.components : [], depth, filters);
234
+ }
235
+ if (shouldIncludePageDiff(diff, depth, filters)) {
236
+ diffs.push(diff);
237
+ }
238
+ }
239
+ return diffs;
240
+ }
241
+ function shouldIncludePageDiff(diff, depth, filters) {
242
+ if (depth === 'pages')
243
+ return true;
244
+ if (!filters.component)
245
+ return true;
246
+ return (diff.components?.length ?? 0) > 0;
247
+ }
248
+ function componentMatchesFilter(componentFilter, comp) {
249
+ if (!componentFilter)
250
+ return true;
251
+ if (typeof comp.id === 'string' && componentFilter.has(comp.id))
252
+ return true;
253
+ if (typeof comp.key === 'string' && componentFilter.has(comp.key))
254
+ return true;
255
+ return false;
256
+ }
257
+ function pageMatchesFilter(pageFilter, pageKey, fromPage, toPage) {
258
+ if (!pageFilter)
259
+ return true;
260
+ if (pageFilter.has(pageKey))
261
+ return true;
262
+ const fromId = typeof fromPage?.id === 'string' ? fromPage.id : null;
263
+ const toId = typeof toPage?.id === 'string' ? toPage.id : null;
264
+ return (fromId !== null && pageFilter.has(fromId)) || (toId !== null && pageFilter.has(toId));
265
+ }
266
+ function diffComponentArrays(fromComps, toComps, depth, filters) {
267
+ const fromById = new Map();
268
+ for (const c of fromComps)
269
+ fromById.set(c.id, c);
270
+ const toById = new Map();
271
+ for (const c of toComps)
272
+ toById.set(c.id, c);
273
+ const diffs = [];
274
+ for (const [id, comp] of fromById) {
275
+ if (!toById.has(id)) {
276
+ const diff = { id, key: comp.key, type: comp.type, status: 'removed' };
277
+ if (componentMatchesFilter(filters.component, diff)) {
278
+ diffs.push(diff);
279
+ }
280
+ }
281
+ }
282
+ for (const [id, comp] of toById) {
283
+ if (!fromById.has(id)) {
284
+ const diff = { id, key: comp.key, type: comp.type, status: 'added' };
285
+ if (depth === 'props') {
286
+ diff.props = Object.entries(comp)
287
+ .filter(([k]) => !['id', 'key', 'type', 'tags', 'components', 'buttons'].includes(k))
288
+ .map(([name, value]) => ({ name, status: 'added', to: value }));
289
+ }
290
+ if (componentMatchesFilter(filters.component, diff)) {
291
+ diffs.push(diff);
292
+ }
293
+ }
294
+ }
295
+ for (const [id, fromComp] of fromById) {
296
+ const toComp = toById.get(id);
297
+ if (!toComp)
298
+ continue;
299
+ if (stableStringify(fromComp) === stableStringify(toComp))
300
+ continue;
301
+ const diff = {
302
+ id,
303
+ key: toComp.key ?? fromComp.key,
304
+ type: toComp.type ?? fromComp.type,
305
+ status: 'modified',
306
+ };
307
+ if (depth === 'props') {
308
+ diff.props = diffProps(fromComp, toComp);
309
+ }
310
+ if (componentMatchesFilter(filters.component, diff)) {
311
+ diffs.push(diff);
312
+ }
313
+ }
314
+ return diffs;
315
+ }
316
+ const COMPONENT_META_KEYS = new Set(['id', 'key', 'type', 'tags', 'components', 'buttons']);
317
+ function diffProps(fromComp, toComp) {
318
+ const allKeys = new Set([...Object.keys(fromComp), ...Object.keys(toComp)]);
319
+ const diffs = [];
320
+ for (const key of allKeys) {
321
+ if (COMPONENT_META_KEYS.has(key))
322
+ continue;
323
+ const inFrom = key in fromComp;
324
+ const inTo = key in toComp;
325
+ if (!inFrom && inTo) {
326
+ diffs.push({ name: key, status: 'added', to: toComp[key] });
327
+ }
328
+ else if (inFrom && !inTo) {
329
+ diffs.push({ name: key, status: 'removed', from: fromComp[key] });
330
+ }
331
+ else if (stableStringify(fromComp[key]) !== stableStringify(toComp[key])) {
332
+ diffs.push({ name: key, status: 'changed', from: fromComp[key], to: toComp[key] });
333
+ }
334
+ }
335
+ return diffs;
336
+ }
337
+ function diffGlobalComponents(from, to, depth, filters) {
338
+ if (depth === 'pages')
339
+ return [];
340
+ const fromComps = (Array.isArray(from.components) ? from.components : []);
341
+ const toComps = (Array.isArray(to.components) ? to.components : []);
342
+ if (fromComps.length === 0 && toComps.length === 0)
343
+ return [];
344
+ return diffComponentArrays(fromComps, toComps, depth, filters);
345
+ }
346
+ function diffOtherSections(from, to) {
347
+ const skipKeys = new Set(['pages', 'components']);
348
+ const allKeys = new Set([...Object.keys(from), ...Object.keys(to)]);
349
+ const sections = [];
350
+ for (const key of allKeys) {
351
+ if (skipKeys.has(key))
352
+ continue;
353
+ const inFrom = key in from;
354
+ const inTo = key in to;
355
+ if (!inFrom && inTo) {
356
+ sections.push({ name: key, status: 'added' });
357
+ }
358
+ else if (inFrom && !inTo) {
359
+ sections.push({ name: key, status: 'removed' });
360
+ }
361
+ else if (stableStringify(from[key]) !== stableStringify(to[key])) {
362
+ sections.push({ name: key, status: 'modified', from: from[key], to: to[key] });
363
+ }
364
+ }
365
+ return sections;
366
+ }
367
+ function computeSummary(pages, globalComponents) {
368
+ const summary = {
369
+ pagesAdded: 0,
370
+ pagesRemoved: 0,
371
+ pagesModified: 0,
372
+ componentsAdded: 0,
373
+ componentsRemoved: 0,
374
+ componentsModified: 0,
375
+ propsAdded: 0,
376
+ propsRemoved: 0,
377
+ propsChanged: 0,
378
+ };
379
+ function countComponent(comp) {
380
+ if (comp.status === 'added')
381
+ summary.componentsAdded++;
382
+ else if (comp.status === 'removed')
383
+ summary.componentsRemoved++;
384
+ else
385
+ summary.componentsModified++;
386
+ if (comp.props) {
387
+ for (const prop of comp.props) {
388
+ if (prop.status === 'added')
389
+ summary.propsAdded++;
390
+ else if (prop.status === 'removed')
391
+ summary.propsRemoved++;
392
+ else
393
+ summary.propsChanged++;
394
+ }
395
+ }
396
+ }
397
+ for (const page of pages) {
398
+ if (page.status === 'added')
399
+ summary.pagesAdded++;
400
+ else if (page.status === 'removed')
401
+ summary.pagesRemoved++;
402
+ else
403
+ summary.pagesModified++;
404
+ if (page.components) {
405
+ for (const comp of page.components)
406
+ countComponent(comp);
407
+ }
408
+ }
409
+ for (const comp of globalComponents)
410
+ countComponent(comp);
411
+ return summary;
412
+ }
413
+ // ─── Display formatting ──────────────────────────────────────────────────────
414
+ function formatValue(value) {
415
+ if (value === null || value === undefined)
416
+ return 'null';
417
+ if (typeof value === 'string') {
418
+ if (value.length > 50)
419
+ return `"${value.slice(0, 47)}..."`;
420
+ return `"${value}"`;
421
+ }
422
+ if (typeof value === 'boolean' || typeof value === 'number')
423
+ return String(value);
424
+ if (Array.isArray(value))
425
+ return `[${value.length} item${value.length !== 1 ? 's' : ''}]`;
426
+ if (typeof value === 'object') {
427
+ const keys = Object.keys(value);
428
+ return `{${keys.length} key${keys.length !== 1 ? 's' : ''}}`;
429
+ }
430
+ return String(value);
431
+ }
432
+ export function renderDiff(diff, depth, c) {
433
+ const lines = [];
434
+ const hasChanges = diff.pages.length > 0 || diff.globalComponents.length > 0 || diff.sections.length > 0;
435
+ if (!hasChanges) {
436
+ lines.push('');
437
+ lines.push(` ${c.green('No differences found.')}`);
438
+ return lines;
439
+ }
440
+ for (const page of diff.pages) {
441
+ lines.push('');
442
+ renderPage(page, depth, c, lines);
443
+ }
444
+ if (diff.globalComponents.length > 0) {
445
+ lines.push('');
446
+ lines.push(` ${c.bold('Global Components:')}`);
447
+ for (const comp of diff.globalComponents) {
448
+ renderComponentLine(comp, depth, c, lines, ' ');
449
+ }
450
+ }
451
+ if (diff.sections.length > 0) {
452
+ lines.push('');
453
+ lines.push(` ${c.bold('Other changes:')}`);
454
+ for (const section of diff.sections) {
455
+ const icon = section.status === 'added'
456
+ ? c.green('+')
457
+ : section.status === 'removed'
458
+ ? c.red('-')
459
+ : c.yellow('~');
460
+ const name = section.status === 'added'
461
+ ? c.green(section.name)
462
+ : section.status === 'removed'
463
+ ? c.red(section.name)
464
+ : c.yellow(section.name);
465
+ if (section.status === 'modified') {
466
+ lines.push(` ${icon} ${name}: ${c.yellow(`${formatValue(section.from)} → ${formatValue(section.to)}`)}`);
467
+ }
468
+ else {
469
+ lines.push(` ${icon} ${name}`);
470
+ }
471
+ }
472
+ }
473
+ lines.push('');
474
+ lines.push(renderSummary(diff.summary, depth, c));
475
+ return lines;
476
+ }
477
+ function renderPage(page, depth, c, lines) {
478
+ const label = page.title ? `${page.key} "${page.title}"` : page.key;
479
+ if (page.status === 'added') {
480
+ const compCount = page.components?.length ?? 0;
481
+ const detail = depth !== 'pages' && compCount > 0 ? c.dim(` (${compCount} components)`) : '';
482
+ lines.push(` ${c.green(`+ Page: ${label}`)}${detail}`);
483
+ if (depth === 'props' && page.components) {
484
+ for (const comp of page.components) {
485
+ renderComponentLine(comp, depth, c, lines, ' ');
486
+ }
487
+ }
488
+ }
489
+ else if (page.status === 'removed') {
490
+ const compCount = page.components?.length ?? 0;
491
+ const detail = depth !== 'pages' && compCount > 0 ? c.dim(` (${compCount} components)`) : '';
492
+ lines.push(` ${c.red(`- Page: ${label}`)}${detail}`);
493
+ }
494
+ else {
495
+ lines.push(` ${c.yellow(`~ Page: ${label}`)}`);
496
+ if (page.components && depth !== 'pages') {
497
+ for (const comp of page.components) {
498
+ renderComponentLine(comp, depth, c, lines, ' ');
499
+ }
500
+ }
501
+ }
502
+ }
503
+ function renderComponentLine(comp, depth, c, lines, indent) {
504
+ const typeLabel = comp.type ? `${comp.type} ` : '';
505
+ const keyLabel = comp.key ? `"${comp.key}"` : comp.id;
506
+ if (comp.status === 'added') {
507
+ lines.push(`${indent} ${c.green(`+ ${typeLabel}${keyLabel}`)}`);
508
+ if (depth === 'props' && comp.props) {
509
+ for (const prop of comp.props) {
510
+ renderPropLine(prop, c, lines, indent + ' ');
511
+ }
512
+ }
513
+ }
514
+ else if (comp.status === 'removed') {
515
+ lines.push(`${indent} ${c.red(`- ${typeLabel}${keyLabel}`)}`);
516
+ }
517
+ else {
518
+ lines.push(`${indent} ${c.yellow(`~ ${typeLabel}${keyLabel}`)}`);
519
+ if (depth === 'props' && comp.props) {
520
+ for (const prop of comp.props) {
521
+ renderPropLine(prop, c, lines, indent + ' ');
522
+ }
523
+ }
524
+ }
525
+ }
526
+ function renderPropLine(prop, c, lines, indent) {
527
+ if (prop.status === 'added') {
528
+ lines.push(`${indent} ${c.green(`+ ${prop.name}: ${formatValue(prop.to)}`)}`);
529
+ }
530
+ else if (prop.status === 'removed') {
531
+ lines.push(`${indent} ${c.red(`- ${prop.name}: ${formatValue(prop.from)}`)}`);
532
+ }
533
+ else {
534
+ lines.push(`${indent} ${c.yellow(`~ ${prop.name}: ${formatValue(prop.from)} → ${formatValue(prop.to)}`)}`);
535
+ }
536
+ }
537
+ function renderSummary(summary, depth, c) {
538
+ const parts = [];
539
+ const totalPages = summary.pagesAdded + summary.pagesRemoved + summary.pagesModified;
540
+ if (totalPages > 0) {
541
+ const details = [];
542
+ if (summary.pagesModified > 0)
543
+ details.push(`${summary.pagesModified} modified`);
544
+ if (summary.pagesAdded > 0)
545
+ details.push(`${summary.pagesAdded} added`);
546
+ if (summary.pagesRemoved > 0)
547
+ details.push(`${summary.pagesRemoved} removed`);
548
+ parts.push(`${totalPages} page${totalPages !== 1 ? 's' : ''} changed (${details.join(', ')})`);
549
+ }
550
+ if (depth !== 'pages') {
551
+ const totalComps = summary.componentsAdded + summary.componentsRemoved + summary.componentsModified;
552
+ if (totalComps > 0) {
553
+ const details = [];
554
+ if (summary.componentsModified > 0)
555
+ details.push(`${summary.componentsModified} modified`);
556
+ if (summary.componentsAdded > 0)
557
+ details.push(`${summary.componentsAdded} added`);
558
+ if (summary.componentsRemoved > 0)
559
+ details.push(`${summary.componentsRemoved} removed`);
560
+ parts.push(`${totalComps} component${totalComps !== 1 ? 's' : ''} (${details.join(', ')})`);
561
+ }
562
+ }
563
+ if (depth === 'props') {
564
+ const totalProps = summary.propsAdded + summary.propsRemoved + summary.propsChanged;
565
+ if (totalProps > 0) {
566
+ const details = [];
567
+ if (summary.propsChanged > 0)
568
+ details.push(`${summary.propsChanged} changed`);
569
+ if (summary.propsAdded > 0)
570
+ details.push(`${summary.propsAdded} added`);
571
+ if (summary.propsRemoved > 0)
572
+ details.push(`${summary.propsRemoved} removed`);
573
+ parts.push(`${totalProps} prop${totalProps !== 1 ? 's' : ''} (${details.join(', ')})`);
574
+ }
575
+ }
576
+ if (parts.length === 0)
577
+ return ` ${c.dim('No structural differences.')}`;
578
+ return ` ${c.bold('Summary:')} ${parts.join(', ')}`;
579
+ }
580
+ // ─── Config helper ────────────────────────────────────────────────────────────
581
+ function getBranchFromConfig(embeddableId) {
582
+ const configPath = path.join('embeddables', embeddableId, 'config.json');
583
+ if (!fs.existsSync(configPath))
584
+ return null;
585
+ try {
586
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
587
+ const branchId = config._branch_id;
588
+ if (typeof branchId !== 'string' || !branchId)
589
+ return null;
590
+ const branchName = config._branch_name;
591
+ return { branchId, branchName: typeof branchName === 'string' ? branchName : undefined };
592
+ }
593
+ catch {
594
+ return null;
595
+ }
596
+ }
597
+ // ─── Main entry point ─────────────────────────────────────────────────────────
598
+ export async function runDiff(opts) {
599
+ const logger = createLogger('runDiff');
600
+ const totalTimer = stdout.timer();
601
+ const fromSpec = parseVersionSpecifier(opts.from || 'local');
602
+ const toSpec = parseVersionSpecifier(opts.to || 'latest');
603
+ const depth = opts.depth || 'components';
604
+ const useColor = opts.color !== false;
605
+ const c = createColorizer(useColor);
606
+ const filters = {
607
+ page: parseCsvFilter(opts.page),
608
+ component: parseCsvFilter(opts.component),
609
+ };
610
+ const inferred = inferEmbeddableFromCwd();
611
+ let embeddableId = opts.id ?? inferred?.embeddableId;
612
+ if (inferred && !opts.id && embeddableId) {
613
+ process.chdir(inferred.projectRoot);
614
+ }
615
+ setSentryContext(getSentryContextFromProjectConfig());
616
+ if (!embeddableId) {
617
+ const selected = await promptForLocalEmbeddable({
618
+ message: 'Select an embeddable to diff:',
619
+ });
620
+ if (!selected) {
621
+ logger.error('no embeddable selected');
622
+ await exit(1);
623
+ return;
624
+ }
625
+ embeddableId = selected;
626
+ }
627
+ if (embeddableId) {
628
+ setSentryContext({
629
+ embeddable: { id: embeddableId },
630
+ ...getSentryContextFromEmbeddableConfig(embeddableId),
631
+ });
632
+ }
633
+ const currentBranch = opts.branch ? null : getBranchFromConfig(embeddableId);
634
+ const defaultBranch = opts.branch ?? currentBranch?.branchId;
635
+ const defaultBranchName = currentBranch?.branchName;
636
+ const fromBranch = fromSpec.branch ?? defaultBranch;
637
+ const toBranch = toSpec.branch ?? defaultBranch;
638
+ const branchesMatch = fromBranch === toBranch;
639
+ const branchLabel = branchesMatch
640
+ ? fromBranch
641
+ ? defaultBranchName && !fromSpec.branch
642
+ ? `${defaultBranchName} (${fromBranch})`
643
+ : fromBranch
644
+ : 'main'
645
+ : null;
646
+ if (defaultBranch) {
647
+ setSentryContext({ branch: { id: defaultBranch, name: defaultBranchName ?? null } });
648
+ }
649
+ const fromLabel = versionLabel(fromSpec);
650
+ const toLabel = versionLabel(toSpec);
651
+ const subtitle = branchLabel
652
+ ? `Embeddable: ${embeddableId} \u00b7 Branch: ${branchLabel} \u00b7 Depth: ${depth}`
653
+ : `Embeddable: ${embeddableId} \u00b7 Depth: ${depth}`;
654
+ stdout.header(`Diff: ${fromLabel} \u2194 ${toLabel}`, subtitle);
655
+ let fromJson;
656
+ let toJson;
657
+ try {
658
+ const fromResolveOpts = { engine: opts.engine, branch: fromBranch };
659
+ const toResolveOpts = { engine: opts.engine, branch: toBranch };
660
+ if (fromSpec.version === 'local' && toSpec.version === 'local') {
661
+ fromJson = await stdout.withSpinner('Compiling local files\u2026', async () => {
662
+ stdout.mute();
663
+ try {
664
+ return await resolveVersion('local', embeddableId, fromResolveOpts);
665
+ }
666
+ finally {
667
+ stdout.unmute();
668
+ }
669
+ }, { successText: 'Local files compiled' });
670
+ toJson = fromJson;
671
+ }
672
+ else if (fromSpec.version !== 'local' && toSpec.version !== 'local') {
673
+ const [from, to] = await stdout.withSpinner(`Fetching ${fromLabel} and ${toLabel}\u2026`, async () => Promise.all([
674
+ resolveVersion(fromSpec.version, embeddableId, fromResolveOpts),
675
+ resolveVersion(toSpec.version, embeddableId, toResolveOpts),
676
+ ]), { successText: 'Both versions fetched' });
677
+ fromJson = from;
678
+ toJson = to;
679
+ }
680
+ else if (fromSpec.version === 'local') {
681
+ fromJson = await stdout.withSpinner('Compiling local files\u2026', async () => {
682
+ stdout.mute();
683
+ try {
684
+ return await resolveVersion('local', embeddableId, fromResolveOpts);
685
+ }
686
+ finally {
687
+ stdout.unmute();
688
+ }
689
+ }, { successText: 'Local files compiled' });
690
+ toJson = await stdout.withSpinner(`Fetching ${toLabel}\u2026`, async () => resolveVersion(toSpec.version, embeddableId, toResolveOpts), { successText: `Fetched ${toLabel}` });
691
+ }
692
+ else {
693
+ fromJson = await stdout.withSpinner(`Fetching ${fromLabel}\u2026`, async () => resolveVersion(fromSpec.version, embeddableId, fromResolveOpts), { successText: `Fetched ${fromLabel}` });
694
+ toJson = await stdout.withSpinner('Compiling local files\u2026', async () => {
695
+ stdout.mute();
696
+ try {
697
+ return await resolveVersion('local', embeddableId, toResolveOpts);
698
+ }
699
+ finally {
700
+ stdout.unmute();
701
+ }
702
+ }, { successText: 'Local files compiled' });
703
+ }
704
+ }
705
+ catch (error) {
706
+ captureException(error);
707
+ stdout.error(`Failed to resolve versions: ${error instanceof Error ? error.message : String(error)}`);
708
+ logger.error('diff version resolution failed', {
709
+ message: error instanceof Error ? error.message : String(error),
710
+ });
711
+ await exit(1);
712
+ return;
713
+ }
714
+ const diff = computeDiff(fromJson, toJson, depth, filters);
715
+ const output = renderDiff(diff, depth, c);
716
+ for (const line of output) {
717
+ stdout.print(line);
718
+ }
719
+ stdout.gap();
720
+ stdout.dim(` Completed in ${totalTimer()}`);
721
+ logger.info('diff complete', {
722
+ from: fromLabel,
723
+ to: toLabel,
724
+ depth,
725
+ embeddableId,
726
+ branch: branchLabel ?? `${fromBranch ?? 'main'} / ${toBranch ?? 'main'}`,
727
+ });
728
+ }
729
+ function parseCsvFilter(value) {
730
+ if (!value)
731
+ return null;
732
+ const values = value
733
+ .split(',')
734
+ .map((v) => v.trim())
735
+ .filter((v) => v.length > 0);
736
+ return values.length > 0 ? new Set(values) : null;
737
+ }
738
+ //# sourceMappingURL=diff.js.map