@duffcloudservices/cli 0.1.0 → 0.1.1

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/index.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import chalk7 from "chalk";
5
+ import chalk8 from "chalk";
6
6
 
7
7
  // package.json
8
- var version = "0.1.0";
8
+ var version = "0.1.1";
9
9
 
10
10
  // src/commands/auth.ts
11
11
  import chalk2 from "chalk";
@@ -251,7 +251,7 @@ var PortalClient = class {
251
251
  /**
252
252
  * Make an authenticated API request.
253
253
  */
254
- async request(path4, options = {}) {
254
+ async request(path5, options = {}) {
255
255
  let accessToken = getAccessToken();
256
256
  if (!isAuthenticated() && getRefreshToken()) {
257
257
  const refreshToken = getRefreshToken();
@@ -268,7 +268,7 @@ var PortalClient = class {
268
268
  if (!accessToken) {
269
269
  throw new Error("Not authenticated. Please login with `dcs login`.");
270
270
  }
271
- const response = await fetch(`${this.apiUrl}${path4}`, {
271
+ const response = await fetch(`${this.apiUrl}${path5}`, {
272
272
  ...options,
273
273
  headers: {
274
274
  ...options.headers,
@@ -2082,6 +2082,572 @@ Once verified:
2082
2082
  `;
2083
2083
  }
2084
2084
 
2085
+ // src/commands/capture-snapshots.ts
2086
+ import fs4 from "fs";
2087
+ import path4 from "path";
2088
+ import yaml3 from "js-yaml";
2089
+ import chalk7 from "chalk";
2090
+ import ora3 from "ora";
2091
+ var SECTION_SELECTORS = {
2092
+ explicit: "[data-section]",
2093
+ header: 'header, [role="banner"], .header, #header',
2094
+ footer: 'footer, [role="contentinfo"], .footer, #footer',
2095
+ hero: '[class*="hero"], [data-section-type="hero"], .hero-section, #hero',
2096
+ content: 'main, [role="main"], article, .content, .main-content',
2097
+ gallery: '[class*="gallery"], [data-section-type="gallery"]',
2098
+ cta: '[class*="cta"], [data-section-type="cta"], .call-to-action'
2099
+ };
2100
+ function loadPagesConfig(targetDir) {
2101
+ const configPath = path4.join(targetDir, ".dcs", "pages.yaml");
2102
+ if (!fs4.existsSync(configPath)) {
2103
+ throw new Error(`Configuration file not found: ${configPath}`);
2104
+ }
2105
+ const content = fs4.readFileSync(configPath, "utf-8");
2106
+ return yaml3.load(content);
2107
+ }
2108
+ function loadContentConfig(targetDir) {
2109
+ const contentPath = path4.join(targetDir, ".dcs", "content.yaml");
2110
+ if (!fs4.existsSync(contentPath)) {
2111
+ return null;
2112
+ }
2113
+ const content = fs4.readFileSync(contentPath, "utf-8");
2114
+ return yaml3.load(content);
2115
+ }
2116
+ function loadTargetedSnapshotsConfig(targetDir) {
2117
+ const targetedPath = path4.join(targetDir, ".dcs", "targeted-snapshots.yaml");
2118
+ if (!fs4.existsSync(targetedPath)) {
2119
+ return null;
2120
+ }
2121
+ const content = fs4.readFileSync(targetedPath, "utf-8");
2122
+ return yaml3.load(content);
2123
+ }
2124
+ function getValidTextKeysForPage(contentConfig, pageSlug) {
2125
+ const keys = /* @__PURE__ */ new Map();
2126
+ if (!contentConfig) return keys;
2127
+ if (contentConfig.global) {
2128
+ for (const [key, value] of Object.entries(contentConfig.global)) {
2129
+ keys.set(key, typeof value === "string" ? value : JSON.stringify(value));
2130
+ }
2131
+ }
2132
+ const pageContent = contentConfig.pages?.[pageSlug];
2133
+ if (pageContent) {
2134
+ for (const [key, value] of Object.entries(pageContent)) {
2135
+ const fullKey = `${pageSlug}.${key}`;
2136
+ keys.set(fullKey, typeof value === "string" ? value : JSON.stringify(value));
2137
+ }
2138
+ }
2139
+ return keys;
2140
+ }
2141
+ async function resolveAllPages(config2) {
2142
+ const resolved = [];
2143
+ for (const page of config2.pages) {
2144
+ if (page.type === "dynamic" && page.instances) {
2145
+ for (const instance of page.instances) {
2146
+ const instancePath = page.pathTemplate ? page.pathTemplate.replace(/:[\w]+/, instance) : `${page.path}/${instance}`;
2147
+ resolved.push({
2148
+ slug: `${page.slug}-${instance}`,
2149
+ path: instancePath,
2150
+ config: { ...page, slug: `${page.slug}-${instance}` }
2151
+ });
2152
+ }
2153
+ } else {
2154
+ resolved.push({
2155
+ slug: page.slug,
2156
+ path: page.path,
2157
+ config: page
2158
+ });
2159
+ }
2160
+ }
2161
+ return resolved;
2162
+ }
2163
+ async function getDomPath(element) {
2164
+ return element.evaluate((el) => {
2165
+ const parts = [];
2166
+ let current = el;
2167
+ while (current && current !== document.body) {
2168
+ let selector = current.tagName.toLowerCase();
2169
+ if (current.id) {
2170
+ selector += `#${current.id}`;
2171
+ } else {
2172
+ const classes = Array.from(current.classList).filter((c) => !c.match(/^(v-|nuxt-|vue-|react-|ng-|_)/)).slice(0, 2).join(".");
2173
+ if (classes) {
2174
+ selector += `.${classes}`;
2175
+ }
2176
+ }
2177
+ parts.unshift(selector);
2178
+ current = current.parentElement;
2179
+ }
2180
+ return parts.join(" > ");
2181
+ });
2182
+ }
2183
+ async function generateSectionDisplayName(element, sectionType, index) {
2184
+ const headingText = await element.evaluate((el) => {
2185
+ const heading = el.querySelector('h1, h2, h3, h4, [class*="title"], [class*="heading"]');
2186
+ if (heading && heading.textContent) {
2187
+ return heading.textContent.trim().slice(0, 50);
2188
+ }
2189
+ return null;
2190
+ });
2191
+ if (headingText) return headingText;
2192
+ const typeLabels = {
2193
+ header: "Header",
2194
+ footer: "Footer",
2195
+ hero: "Hero Section",
2196
+ content: "Content Section",
2197
+ gallery: "Gallery",
2198
+ cta: "Call to Action"
2199
+ };
2200
+ return typeLabels[sectionType] || `Section ${index + 1}`;
2201
+ }
2202
+ async function detectTextKeys(_page, sectionElement, _pageSlug, _validTextKeys) {
2203
+ const textKeys = [];
2204
+ const textKeyElements = await sectionElement.$$("[data-text-key]");
2205
+ for (const element of textKeyElements) {
2206
+ const keyInfo = await element.evaluate((el) => {
2207
+ const rect = el.getBoundingClientRect();
2208
+ return {
2209
+ key: el.getAttribute("data-text-key") || "",
2210
+ elementType: el.tagName.toLowerCase(),
2211
+ currentValue: el.textContent?.trim().slice(0, 500) || "",
2212
+ bounds: {
2213
+ x: rect.left + window.scrollX,
2214
+ y: rect.top + window.scrollY,
2215
+ width: rect.width,
2216
+ height: rect.height
2217
+ }
2218
+ };
2219
+ });
2220
+ if (keyInfo.key) {
2221
+ const domPath = await getDomPath(element);
2222
+ textKeys.push({
2223
+ key: keyInfo.key,
2224
+ currentValue: keyInfo.currentValue,
2225
+ elementType: keyInfo.elementType,
2226
+ domPath,
2227
+ bounds: {
2228
+ x: Math.round(keyInfo.bounds.x),
2229
+ y: Math.round(keyInfo.bounds.y),
2230
+ width: Math.round(keyInfo.bounds.width),
2231
+ height: Math.round(keyInfo.bounds.height)
2232
+ }
2233
+ });
2234
+ }
2235
+ }
2236
+ return textKeys;
2237
+ }
2238
+ async function detectFallbackContentSections(page, existingSections) {
2239
+ const candidates = await page.$$('section, article, div[class*="section"], div[class*="container"]');
2240
+ const results = [];
2241
+ const existingBounds = existingSections.map((s) => s.bounds);
2242
+ for (const candidate of candidates) {
2243
+ const bounds = await candidate.evaluate((el) => {
2244
+ const rect = el.getBoundingClientRect();
2245
+ return {
2246
+ y: rect.top + window.scrollY,
2247
+ height: rect.height
2248
+ };
2249
+ });
2250
+ if (bounds.height < 100) continue;
2251
+ const overlaps = existingBounds.some(
2252
+ (eb) => bounds.y < eb.y + eb.height && bounds.y + bounds.height > eb.y
2253
+ );
2254
+ if (!overlaps) {
2255
+ results.push(candidate);
2256
+ }
2257
+ }
2258
+ return results;
2259
+ }
2260
+ async function capturePageSnapshot(browser, pageInfo, snapshotConfig, siteSlug, validTextKeys, outputDir, verbose) {
2261
+ const context = await browser.newContext({
2262
+ viewport: snapshotConfig.viewport
2263
+ });
2264
+ const page = await context.newPage();
2265
+ const url = pageInfo.path.startsWith("http") ? pageInfo.path : `${process.env.SITE_BASE_URL || "http://localhost:5173"}${pageInfo.path}`;
2266
+ if (verbose) {
2267
+ console.log(` Capturing: ${pageInfo.slug} (${url})`);
2268
+ }
2269
+ try {
2270
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
2271
+ await page.waitForLoadState("networkidle", { timeout: 1e4 }).catch(() => {
2272
+ if (verbose) console.log(` Note: networkidle timeout, continuing anyway`);
2273
+ });
2274
+ } catch (error) {
2275
+ console.error(` Failed to load: ${error}`);
2276
+ await context.close();
2277
+ throw error;
2278
+ }
2279
+ await page.waitForTimeout(snapshotConfig.waitAfterLoad);
2280
+ const pageOutputDir = path4.join(outputDir, siteSlug, pageInfo.slug);
2281
+ fs4.mkdirSync(path4.join(pageOutputDir, "sections"), { recursive: true });
2282
+ const title = await page.title();
2283
+ let pageHeight = await page.evaluate(() => document.documentElement.scrollHeight);
2284
+ const scrollStep = snapshotConfig.viewport.height;
2285
+ for (let scrollY2 = 0; scrollY2 < pageHeight; scrollY2 += scrollStep) {
2286
+ await page.evaluate((y) => window.scrollTo(0, y), scrollY2);
2287
+ await page.waitForTimeout(100);
2288
+ }
2289
+ await page.evaluate(() => window.scrollTo(0, 0));
2290
+ await page.waitForTimeout(200);
2291
+ pageHeight = await page.evaluate(() => document.documentElement.scrollHeight);
2292
+ await page.evaluate(() => window.scrollTo(0, 0));
2293
+ await page.waitForTimeout(100);
2294
+ const scrollY = await page.evaluate(() => window.scrollY);
2295
+ if (scrollY !== 0) {
2296
+ console.warn(` Warning: Could not scroll to top (scrollY=${scrollY})`);
2297
+ }
2298
+ const fullPagePath = path4.join(pageOutputDir, "full-page.png");
2299
+ await page.screenshot({
2300
+ path: fullPagePath,
2301
+ fullPage: snapshotConfig.captureFullPage
2302
+ });
2303
+ const sections = [];
2304
+ const seenDomPaths = /* @__PURE__ */ new Set();
2305
+ const getAccurateBounds = async (el, sectionType) => {
2306
+ return el.evaluate((element, type) => {
2307
+ const rect = element.getBoundingClientRect();
2308
+ const style = window.getComputedStyle(element);
2309
+ const position = style.position;
2310
+ const isFixed = position === "fixed" || position === "sticky";
2311
+ let y = rect.top + window.scrollY;
2312
+ if (isFixed && (type === "header" || element.tagName.toLowerCase() === "header")) {
2313
+ y = 0;
2314
+ } else if (isFixed && (type === "footer" || element.tagName.toLowerCase() === "footer")) {
2315
+ y = document.documentElement.scrollHeight - rect.height;
2316
+ }
2317
+ return {
2318
+ x: rect.left + window.scrollX,
2319
+ y,
2320
+ width: rect.width,
2321
+ height: rect.height,
2322
+ isFixed
2323
+ };
2324
+ }, sectionType);
2325
+ };
2326
+ const explicitElements = await page.$$(SECTION_SELECTORS.explicit);
2327
+ for (let i = 0; i < explicitElements.length; i++) {
2328
+ const element = explicitElements[i];
2329
+ const domPath = await getDomPath(element);
2330
+ if (seenDomPaths.has(domPath)) continue;
2331
+ seenDomPaths.add(domPath);
2332
+ const basicMetadata = await element.evaluate((el) => {
2333
+ return {
2334
+ sectionId: el.getAttribute("data-section") || "",
2335
+ sectionLabel: el.getAttribute("data-section-label") || "",
2336
+ sectionType: el.getAttribute("data-section-type") || "",
2337
+ isDynamic: el.hasAttribute("data-dynamic")
2338
+ };
2339
+ });
2340
+ let resolvedType = "content";
2341
+ if (basicMetadata.sectionType) {
2342
+ resolvedType = basicMetadata.sectionType;
2343
+ } else if (["header", "footer"].includes(basicMetadata.sectionId)) {
2344
+ resolvedType = basicMetadata.sectionId;
2345
+ } else if (basicMetadata.sectionId.includes("hero")) {
2346
+ resolvedType = "hero";
2347
+ } else if (basicMetadata.sectionId.includes("gallery")) {
2348
+ resolvedType = "gallery";
2349
+ }
2350
+ const bounds = await getAccurateBounds(element, resolvedType);
2351
+ if (!bounds || bounds.height < 10) continue;
2352
+ const sectionId = basicMetadata.sectionId || `section-${i}`;
2353
+ const sectionPath = path4.join(pageOutputDir, "sections", `${sectionId}.png`);
2354
+ try {
2355
+ await element.screenshot({ path: sectionPath });
2356
+ } catch {
2357
+ if (verbose) console.log(` Skipped section screenshot: ${sectionId}`);
2358
+ }
2359
+ const isStructuralOnly = ["header", "footer"].includes(resolvedType);
2360
+ let textKeys = [];
2361
+ if (!isStructuralOnly) {
2362
+ textKeys = await detectTextKeys(page, element, pageInfo.slug, validTextKeys);
2363
+ }
2364
+ const isEditable = !isStructuralOnly && textKeys.length > 0;
2365
+ let displayName = basicMetadata.sectionLabel;
2366
+ if (!displayName) {
2367
+ displayName = await generateSectionDisplayName(element, resolvedType, i);
2368
+ }
2369
+ if (verbose) {
2370
+ console.log(
2371
+ ` Section: ${displayName} (${sectionId}) at y=${Math.round(bounds.y)}${basicMetadata.isDynamic ? " [dynamic]" : ""} - ${textKeys.length} text keys`
2372
+ );
2373
+ }
2374
+ sections.push({
2375
+ sectionId,
2376
+ sectionType: resolvedType,
2377
+ displayName,
2378
+ domPath,
2379
+ bounds: {
2380
+ x: Math.round(bounds.x),
2381
+ y: Math.round(bounds.y),
2382
+ width: Math.round(bounds.width),
2383
+ height: Math.round(bounds.height)
2384
+ },
2385
+ textKeys,
2386
+ isEditable,
2387
+ isStructuralOnly
2388
+ });
2389
+ }
2390
+ for (const [sectionType, selector] of Object.entries(SECTION_SELECTORS)) {
2391
+ if (sectionType === "explicit") continue;
2392
+ const elements = await page.$$(selector);
2393
+ for (let i = 0; i < elements.length; i++) {
2394
+ const element = elements[i];
2395
+ const domPath = await getDomPath(element);
2396
+ if (seenDomPaths.has(domPath)) continue;
2397
+ const isInsideExplicitSection = await element.evaluate((el) => {
2398
+ let current = el.parentElement;
2399
+ while (current) {
2400
+ if (current.dataset?.section) {
2401
+ return true;
2402
+ }
2403
+ current = current.parentElement;
2404
+ }
2405
+ return false;
2406
+ });
2407
+ if (isInsideExplicitSection) {
2408
+ continue;
2409
+ }
2410
+ const isAncestorOfExplicitSection = await element.evaluate((el) => {
2411
+ const explicitSections = el.querySelectorAll("[data-section]");
2412
+ return explicitSections.length > 0;
2413
+ });
2414
+ if (isAncestorOfExplicitSection) {
2415
+ if (verbose) {
2416
+ console.log(` Skipping ${sectionType} container (contains explicit sections)`);
2417
+ }
2418
+ continue;
2419
+ }
2420
+ seenDomPaths.add(domPath);
2421
+ const bounds = await getAccurateBounds(element, sectionType);
2422
+ if (!bounds || bounds.height < 10) continue;
2423
+ const sectionId = `${sectionType}-${i}`;
2424
+ const sectionPath = path4.join(pageOutputDir, "sections", `${sectionId}.png`);
2425
+ try {
2426
+ await element.screenshot({ path: sectionPath });
2427
+ } catch {
2428
+ if (verbose) console.log(` Skipped section screenshot: ${sectionId}`);
2429
+ }
2430
+ const isStructuralOnly = ["header", "footer"].includes(sectionType);
2431
+ const textKeys = isStructuralOnly ? [] : await detectTextKeys(page, element, pageInfo.slug, validTextKeys);
2432
+ if (sectionType === "content" && textKeys.length === 0) {
2433
+ continue;
2434
+ }
2435
+ const isEditable = !isStructuralOnly && textKeys.length > 0;
2436
+ const displayName = await generateSectionDisplayName(element, sectionType, i);
2437
+ if (verbose) {
2438
+ console.log(` Section: ${displayName} at y=${Math.round(bounds.y)}${bounds.isFixed ? " [fixed]" : ""}`);
2439
+ }
2440
+ sections.push({
2441
+ sectionId,
2442
+ sectionType,
2443
+ displayName,
2444
+ domPath,
2445
+ bounds: {
2446
+ x: Math.round(bounds.x),
2447
+ y: Math.round(bounds.y),
2448
+ width: Math.round(bounds.width),
2449
+ height: Math.round(bounds.height)
2450
+ },
2451
+ textKeys,
2452
+ isEditable,
2453
+ isStructuralOnly
2454
+ });
2455
+ }
2456
+ }
2457
+ const hasContentSection = sections.some((s) => s.sectionType === "content");
2458
+ if (!hasContentSection) {
2459
+ if (verbose) console.log(` No content sections found, trying fallback detection...`);
2460
+ const fallbackElements = await detectFallbackContentSections(page, sections);
2461
+ for (let i = 0; i < fallbackElements.length; i++) {
2462
+ const element = fallbackElements[i];
2463
+ const domPath = await getDomPath(element);
2464
+ if (seenDomPaths.has(domPath)) continue;
2465
+ seenDomPaths.add(domPath);
2466
+ const bounds = await element.evaluate((el) => {
2467
+ const rect = el.getBoundingClientRect();
2468
+ return {
2469
+ x: rect.left + window.scrollX,
2470
+ y: rect.top + window.scrollY,
2471
+ width: rect.width,
2472
+ height: rect.height
2473
+ };
2474
+ });
2475
+ if (!bounds || bounds.height < 10) continue;
2476
+ const sectionId = `content-fallback-${i}`;
2477
+ const sectionPath = path4.join(pageOutputDir, "sections", `${sectionId}.png`);
2478
+ try {
2479
+ await element.screenshot({ path: sectionPath });
2480
+ } catch {
2481
+ if (verbose) console.log(` Skipped fallback section screenshot: ${sectionId}`);
2482
+ }
2483
+ const textKeys = await detectTextKeys(page, element, pageInfo.slug, validTextKeys);
2484
+ const displayName = await generateSectionDisplayName(element, "content", i);
2485
+ if (verbose) {
2486
+ console.log(
2487
+ ` Fallback Section: ${displayName} at y=${Math.round(bounds.y)} (${textKeys.length} text keys)`
2488
+ );
2489
+ }
2490
+ sections.push({
2491
+ sectionId,
2492
+ sectionType: "content",
2493
+ displayName,
2494
+ domPath,
2495
+ bounds: {
2496
+ x: Math.round(bounds.x),
2497
+ y: Math.round(bounds.y),
2498
+ width: Math.round(bounds.width),
2499
+ height: Math.round(bounds.height)
2500
+ },
2501
+ textKeys,
2502
+ isEditable: textKeys.length > 0,
2503
+ isStructuralOnly: false
2504
+ });
2505
+ }
2506
+ }
2507
+ await context.close();
2508
+ sections.sort((a, b) => a.bounds.y - b.bounds.y);
2509
+ return {
2510
+ pageSlug: pageInfo.slug,
2511
+ pagePath: pageInfo.path,
2512
+ pageType: pageInfo.config.type,
2513
+ deletable: pageInfo.config.deletable,
2514
+ title: title || pageInfo.config.title,
2515
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
2516
+ previewUrl: url,
2517
+ viewport: snapshotConfig.viewport,
2518
+ pageHeight,
2519
+ sections
2520
+ };
2521
+ }
2522
+ async function captureSnapshotsCommand(options) {
2523
+ const { target, baseUrl, dryRun, verbose, pages: targetedPages } = options;
2524
+ const targetDir = path4.resolve(target);
2525
+ console.log(chalk7.blue("\u{1F4F8} Page Snapshot Capture"));
2526
+ console.log(chalk7.gray("========================"));
2527
+ console.log(chalk7.gray(`Target: ${targetDir}`));
2528
+ console.log(chalk7.gray(`Base URL: ${baseUrl}`));
2529
+ process.env.SITE_BASE_URL = baseUrl;
2530
+ let config2;
2531
+ try {
2532
+ config2 = loadPagesConfig(targetDir);
2533
+ } catch (error) {
2534
+ console.error(chalk7.red("Error:"), error.message);
2535
+ console.log(chalk7.yellow("\nMake sure .dcs/pages.yaml exists in the target directory."));
2536
+ process.exit(1);
2537
+ }
2538
+ console.log(chalk7.gray(`Site: ${config2.siteSlug}`));
2539
+ const outputDir = path4.join(targetDir, config2.snapshot.outputDir || ".dcs/snapshots");
2540
+ const contentConfig = loadContentConfig(targetDir);
2541
+ if (contentConfig) {
2542
+ const pageCount = Object.keys(contentConfig.pages || {}).length;
2543
+ const globalCount = Object.keys(contentConfig.global || {}).length;
2544
+ console.log(chalk7.gray(`Content: Loaded ${pageCount} pages, ${globalCount} global keys`));
2545
+ }
2546
+ const targetedConfig = loadTargetedSnapshotsConfig(targetDir);
2547
+ if (targetedConfig?.mode === "skip") {
2548
+ console.log("");
2549
+ console.log(chalk7.yellow("\u23ED\uFE0F Skip mode activated!"));
2550
+ console.log(chalk7.gray(` Reason: ${targetedConfig.reason}`));
2551
+ console.log(chalk7.gray(" No snapshots will be captured."));
2552
+ if (!dryRun) {
2553
+ fs4.mkdirSync(outputDir, { recursive: true });
2554
+ const skipMarker = {
2555
+ skipped: true,
2556
+ reason: targetedConfig.reason,
2557
+ triggeredBy: targetedConfig.triggeredBy,
2558
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2559
+ };
2560
+ fs4.writeFileSync(path4.join(outputDir, "skipped.json"), JSON.stringify(skipMarker, null, 2));
2561
+ }
2562
+ console.log(chalk7.green("\n\u2705 Snapshot capture skipped (as requested)"));
2563
+ return;
2564
+ }
2565
+ let pagesToCapture = await resolveAllPages(config2);
2566
+ console.log(chalk7.gray(`Found ${pagesToCapture.length} total pages in configuration`));
2567
+ if (targetedPages && targetedPages.length > 0) {
2568
+ const targetedSlugs = new Set(targetedPages);
2569
+ const originalCount = pagesToCapture.length;
2570
+ pagesToCapture = pagesToCapture.filter((p) => targetedSlugs.has(p.slug));
2571
+ console.log("");
2572
+ console.log(chalk7.cyan(`\u{1F3AF} Targeted mode: Filtering to ${pagesToCapture.length} of ${originalCount} pages`));
2573
+ } else if (targetedConfig?.mode === "targeted" && targetedConfig.pages.length > 0) {
2574
+ const targetedSlugs = new Set(targetedConfig.pages);
2575
+ const originalCount = pagesToCapture.length;
2576
+ pagesToCapture = pagesToCapture.filter((p) => targetedSlugs.has(p.slug));
2577
+ console.log("");
2578
+ console.log(chalk7.cyan(`\u{1F3AF} Targeted mode: Filtering to ${pagesToCapture.length} of ${originalCount} pages`));
2579
+ console.log(chalk7.gray(` Reason: ${targetedConfig.reason}`));
2580
+ }
2581
+ console.log("");
2582
+ console.log("Pages to capture:");
2583
+ for (const p of pagesToCapture) {
2584
+ console.log(chalk7.gray(` - ${p.slug} (${p.config.type}) ${p.path}`));
2585
+ }
2586
+ if (pagesToCapture.length === 0) {
2587
+ console.log(chalk7.yellow("\n\u26A0\uFE0F No pages to capture!"));
2588
+ return;
2589
+ }
2590
+ if (dryRun) {
2591
+ console.log(chalk7.yellow("\n--dry-run: Stopping before browser launch"));
2592
+ return;
2593
+ }
2594
+ const { chromium } = await import("playwright");
2595
+ if (fs4.existsSync(outputDir)) {
2596
+ fs4.rmSync(outputDir, { recursive: true });
2597
+ }
2598
+ fs4.mkdirSync(outputDir, { recursive: true });
2599
+ const spinner = ora3("Launching browser...").start();
2600
+ const browser = await chromium.launch();
2601
+ spinner.succeed("Browser launched");
2602
+ const snapshots = [];
2603
+ for (const pageInfo of pagesToCapture) {
2604
+ const pageSpinner = ora3(`Capturing ${pageInfo.slug}...`).start();
2605
+ try {
2606
+ const validTextKeys = getValidTextKeysForPage(contentConfig, pageInfo.slug);
2607
+ const snapshot = await capturePageSnapshot(
2608
+ browser,
2609
+ pageInfo,
2610
+ config2.snapshot,
2611
+ config2.siteSlug,
2612
+ validTextKeys,
2613
+ outputDir,
2614
+ verbose || false
2615
+ );
2616
+ snapshots.push(snapshot);
2617
+ const snapshotPath = path4.join(outputDir, config2.siteSlug, pageInfo.slug, "snapshot.json");
2618
+ fs4.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
2619
+ pageSpinner.succeed(`${pageInfo.slug}: ${snapshot.sections.length} sections captured`);
2620
+ } catch (error) {
2621
+ pageSpinner.fail(`${pageInfo.slug}: Failed - ${error.message}`);
2622
+ }
2623
+ }
2624
+ await browser.close();
2625
+ const manifest = {
2626
+ siteSlug: config2.siteSlug,
2627
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
2628
+ deploymentRef: process.env.GITHUB_REF || "local",
2629
+ pagesConfigVersion: config2.version,
2630
+ pages: snapshots.map((s) => ({
2631
+ pageSlug: s.pageSlug,
2632
+ pagePath: s.pagePath,
2633
+ pageType: s.pageType,
2634
+ deletable: s.deletable,
2635
+ title: s.title,
2636
+ capturedAt: s.capturedAt,
2637
+ sectionCount: s.sections.length
2638
+ }))
2639
+ };
2640
+ const manifestPath = path4.join(outputDir, config2.siteSlug, "manifest.json");
2641
+ fs4.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
2642
+ console.log("");
2643
+ console.log(chalk7.green("\u2705 Snapshot capture complete!"));
2644
+ console.log(chalk7.gray(` Captured ${snapshots.length} pages`));
2645
+ console.log(chalk7.gray(` - Static: ${snapshots.filter((s) => s.pageType === "static").length}`));
2646
+ console.log(chalk7.gray(` - Index: ${snapshots.filter((s) => s.pageType === "index").length}`));
2647
+ console.log(chalk7.gray(` - Dynamic: ${snapshots.filter((s) => s.pageType === "dynamic").length}`));
2648
+ console.log(chalk7.gray(` Output: ${outputDir}`));
2649
+ }
2650
+
2085
2651
  // src/index.ts
2086
2652
  var program = new Command();
2087
2653
  program.name("dcs").description("DCS (Duff Cloud Services) CLI for customer site management").version(version);
@@ -2124,6 +2690,16 @@ program.command("plans").description("Generate DCS integration plan files for AI
2124
2690
  force: options.force
2125
2691
  });
2126
2692
  });
2693
+ program.command("capture-snapshots").description("Capture visual snapshots of site pages for the portal page editor").option("-t, --target <dir>", "Target directory containing .dcs/pages.yaml", ".").option("-u, --base-url <url>", "Base URL of the running site", "http://localhost:5173").option("-p, --pages <slugs>", "Comma-separated list of page slugs to capture").option("--dry-run", "Show what would be captured without launching browser").option("-v, --verbose", "Show detailed output").action(async (options) => {
2694
+ const pages = options.pages ? options.pages.split(",").map((s) => s.trim()) : void 0;
2695
+ await captureSnapshotsCommand({
2696
+ target: options.target,
2697
+ baseUrl: options.baseUrl,
2698
+ dryRun: options.dryRun,
2699
+ verbose: options.verbose,
2700
+ pages
2701
+ });
2702
+ });
2127
2703
  program.exitOverride();
2128
2704
  try {
2129
2705
  await program.parseAsync(process.argv);
@@ -2132,7 +2708,7 @@ try {
2132
2708
  if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
2133
2709
  process.exit(0);
2134
2710
  }
2135
- console.error(chalk7.red("Error:"), error.message);
2711
+ console.error(chalk8.red("Error:"), error.message);
2136
2712
  process.exit(1);
2137
2713
  }
2138
2714
  }